diff --git a/docs/en/spring-cloud/document-overview.md b/docs/en/spring-cloud/document-overview.md deleted file mode 100644 index c49aad44339bce14c40240b290b0bdcd2e9b7a4c..0000000000000000000000000000000000000000 --- a/docs/en/spring-cloud/document-overview.md +++ /dev/null @@ -1,38 +0,0 @@ -Spring Cloud Documentation -========== - - -This section provides a brief overview of Spring Cloud reference documentation. It serves -as a map for the rest of the document. - -[](#documentation-about)[1. About the Documentation](#documentation-about) ----------- - -The Spring Cloud reference guide is available as - -* [Multi-page HTML](https://docs.spring.io/spring-cloud/docs/2021.0.1/reference/html) - -* [Single-page HTML](https://docs.spring.io/spring-cloud/docs/2021.0.1/reference/htmlsingle) - -* [PDF](https://docs.spring.io/spring-cloud/docs/2021.0.1/reference/pdf/spring-cloud.pdf) - -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. - -[](#documentation-getting-help)[2. Getting Help](#documentation-getting-help) ----------- - -If you have trouble with Spring Cloud, we would like to help. - -* Learn the Spring Cloud basics. If you are - starting out with Spring Cloud, try one of the [guides](https://spring.io/guides). - -* Ask a question. We monitor [stackoverflow.com](https://stackoverflow.com) for questions - tagged with [`spring-cloud`](https://stackoverflow.com/tags/spring-cloud). - -* Chat with us at [Spring Cloud Gitter](https://gitter.im/spring-cloud/spring-cloud) - -| |All of Spring Cloud is open source, including the documentation. If you find
problems with the docs or if you want to improve them, please get involved.| -|---|------------------------------------------------------------------------------------------------------------------------------------------------------------| - diff --git a/docs/en/spring-cloud/documentation-overview.md b/docs/en/spring-cloud/documentation-overview.md new file mode 100644 index 0000000000000000000000000000000000000000..96ccf7ee7326e7e767c05eea95669562dd6ea302 --- /dev/null +++ b/docs/en/spring-cloud/documentation-overview.md @@ -0,0 +1,56 @@ +Spring Cloud Documentation.hidden { display: none; +} .switch { border-width: 1px 1px 0 1px; border-style: solid; border-color: #7a2518; display: inline-block; +} .switch--item { padding: 10px; background-color: #ffffff; color: #7a2518; display: inline-block; cursor: pointer; +} .switch--item:not(:first-child) { border-width: 0 0 0 1px; border-style: solid; border-color: #7a2518; +} .switch--item.selected { background-color: #7a2519; color: #ffffff; +} function addBlockSwitches() { for (var primary of document.querySelectorAll('.primary')) { var switchItem = createSwitchItem(primary, createBlockSwitch(primary)); switchItem.item.classList.add("selected"); var title = primary.querySelector('.title') title.remove(); } for (var secondary of document.querySelectorAll('.secondary')) { var primary = findPrimary(secondary); if (primary === null) { console.error("Found secondary block with no primary sibling"); } else { var switchItem = createSwitchItem(secondary, primary.querySelector('.switch')); switchItem.content.classList.add("hidden"); primary.append(switchItem.content); secondary.remove(); } } +} function createElementFromHtml(html) { var template = document.createElement('template'); template.innerHTML = html; return template.content.firstChild; +} function createBlockSwitch(primary) { var blockSwitch = createElementFromHtml('\
\'); primary.prepend(blockSwitch) return blockSwitch; +} function findPrimary(secondary) { var candidate = secondary.previousElementSibling; while (candidate != null && !candidate.classList.contains('primary')) { candidate = candidate.previousElementSibling; } return candidate; +} function createSwitchItem(block, blockSwitch) { var blockName = block.querySelector('.title').textContent; var content = block.querySelectorAll('.content').item(0); var colist = nextSibling(block, '.colist'); if (colist != null) { content.append(colist); } var item = createElementFromHtml('\
' + blockName + '\'); item.dataset.blockName = blockName; content.dataset.blockName = blockName; blockSwitch.append(item); return {'item': item, 'content': content}; +} function nextSibling(element, selector) { var sibling = element.nextElementSibling; while (sibling) { if (sibling.matches(selector)) { return sibling; } sibling = sibling.nextElementSibling; } +} function globalSwitch() { document.querySelectorAll(".switch--item").forEach(function(item) { var blockId = blockIdForSwitchItem(item); var handler = function(event) { selectedText = event.target.textContent; window.localStorage.setItem(blockId, selectedText); for (var switchItem of document.querySelectorAll(".switch--item")) { if (blockIdForSwitchItem(switchItem) === blockId && switchItem.textContent === selectedText) { select(switchItem); } } } item.addEventListener("click", handler); if (item.textContent === window.localStorage.getItem(blockId)) { select(item); } }); +} function select(selected) { for (var child of selected.parentNode.children) { child.classList.remove("selected"); } selected.classList.add("selected"); for (var child of selected.parentNode.parentNode.children) { if (child.classList.contains("content")) { if (selected.dataset.blockName === child.dataset.blockName) { child.classList.remove("hidden"); } else { child.classList.add("hidden"); } } } } function blockIdForSwitchItem(item) { idComponents = [] for (var switchItem of item.parentNode.querySelectorAll(".switch--item")) { idComponents.push(switchItem.textContent.toLowerCase()); } return idComponents.sort().join("-") +} window.onload = function() { addBlockSwitches(); globalSwitch(); +}; + +# Spring Cloud Documentation + +Table of Contents + +* [1. About the Documentation](#documentation-about) +* [2. Getting Help](#documentation-getting-help) + +This section provides a brief overview of Spring Cloud reference documentation. It serves +as a map for the rest of the document. + +## [](#documentation-about)[1. About the Documentation](#documentation-about) + +The Spring Cloud reference guide is available as + +* [Multi-page HTML](https://docs.spring.io/spring-cloud/docs/2021.0.1/reference/html) + +* [Single-page HTML](https://docs.spring.io/spring-cloud/docs/2021.0.1/reference/htmlsingle) + +* [PDF](https://docs.spring.io/spring-cloud/docs/2021.0.1/reference/pdf/spring-cloud.pdf) + +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. + +## [](#documentation-getting-help)[2. Getting Help](#documentation-getting-help) + +If you have trouble with Spring Cloud, we would like to help. + +* Learn the Spring Cloud basics. If you are + starting out with Spring Cloud, try one of the [guides](https://spring.io/guides). + +* Ask a question. We monitor [stackoverflow.com](https://stackoverflow.com) for questions + tagged with [`spring-cloud`](https://stackoverflow.com/tags/spring-cloud). + +* Chat with us at [Spring Cloud Gitter](https://gitter.im/spring-cloud/spring-cloud) + +| |All of Spring Cloud is open source, including the documentation. If you find
problems with the docs or if you want to improve them, please get involved.| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------| + +if (window.parent == window) {(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1\*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga');ga('create', 'UA-2728886-23', 'auto', {'siteSpeedSampleRate': 100});ga('send', 'pageview');} \ No newline at end of file diff --git a/docs/en/spring-cloud/legal.md b/docs/en/spring-cloud/legal.md index 58d7a335eba153c5c35465c6bd1e9f388ee299bd..0b555834efeb43e0df09904da740f2476ef7c770 100644 --- a/docs/en/spring-cloud/legal.md +++ b/docs/en/spring-cloud/legal.md @@ -1,5 +1,20 @@ -Legal -========== +Legal.hidden { display: none; +} .switch { border-width: 1px 1px 0 1px; border-style: solid; border-color: #7a2518; display: inline-block; +} .switch--item { padding: 10px; background-color: #ffffff; color: #7a2518; display: inline-block; cursor: pointer; +} .switch--item:not(:first-child) { border-width: 0 0 0 1px; border-style: solid; border-color: #7a2518; +} .switch--item.selected { background-color: #7a2519; color: #ffffff; +} function addBlockSwitches() { for (var primary of document.querySelectorAll('.primary')) { var switchItem = createSwitchItem(primary, createBlockSwitch(primary)); switchItem.item.classList.add("selected"); var title = primary.querySelector('.title') title.remove(); } for (var secondary of document.querySelectorAll('.secondary')) { var primary = findPrimary(secondary); if (primary === null) { console.error("Found secondary block with no primary sibling"); } else { var switchItem = createSwitchItem(secondary, primary.querySelector('.switch')); switchItem.content.classList.add("hidden"); primary.append(switchItem.content); secondary.remove(); } } +} function createElementFromHtml(html) { var template = document.createElement('template'); template.innerHTML = html; return template.content.firstChild; +} function createBlockSwitch(primary) { var blockSwitch = createElementFromHtml('\
\'); primary.prepend(blockSwitch) return blockSwitch; +} function findPrimary(secondary) { var candidate = secondary.previousElementSibling; while (candidate != null && !candidate.classList.contains('primary')) { candidate = candidate.previousElementSibling; } return candidate; +} function createSwitchItem(block, blockSwitch) { var blockName = block.querySelector('.title').textContent; var content = block.querySelectorAll('.content').item(0); var colist = nextSibling(block, '.colist'); if (colist != null) { content.append(colist); } var item = createElementFromHtml('\
' + blockName + '\'); item.dataset.blockName = blockName; content.dataset.blockName = blockName; blockSwitch.append(item); return {'item': item, 'content': content}; +} function nextSibling(element, selector) { var sibling = element.nextElementSibling; while (sibling) { if (sibling.matches(selector)) { return sibling; } sibling = sibling.nextElementSibling; } +} function globalSwitch() { document.querySelectorAll(".switch--item").forEach(function(item) { var blockId = blockIdForSwitchItem(item); var handler = function(event) { selectedText = event.target.textContent; window.localStorage.setItem(blockId, selectedText); for (var switchItem of document.querySelectorAll(".switch--item")) { if (blockIdForSwitchItem(switchItem) === blockId && switchItem.textContent === selectedText) { select(switchItem); } } } item.addEventListener("click", handler); if (item.textContent === window.localStorage.getItem(blockId)) { select(item); } }); +} function select(selected) { for (var child of selected.parentNode.children) { child.classList.remove("selected"); } selected.classList.add("selected"); for (var child of selected.parentNode.parentNode.children) { if (child.classList.contains("content")) { if (selected.dataset.blockName === child.dataset.blockName) { child.classList.remove("hidden"); } else { child.classList.add("hidden"); } } } } function blockIdForSwitchItem(item) { idComponents = [] for (var switchItem of item.parentNode.querySelectorAll(".switch--item")) { idComponents.push(switchItem.textContent.toLowerCase()); } return idComponents.sort().join("-") +} window.onload = function() { addBlockSwitches(); globalSwitch(); +}; + +# Legal 2021.0.1 @@ -9,3 +24,5 @@ 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. + +if (window.parent == window) {(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1\*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga');ga('create', 'UA-2728886-23', 'auto', {'siteSpeedSampleRate': 100});ga('send', 'pageview');} \ No newline at end of file diff --git a/docs/en/spring-cloud/spring-cloud-build.md b/docs/en/spring-cloud/spring-cloud-build.md index b9470949d8bcde0a6760b8cdbf3099d7c9f1e860..07bf21415283eb491b8dbc424e074bf26448e229 100644 --- a/docs/en/spring-cloud/spring-cloud-build.md +++ b/docs/en/spring-cloud/spring-cloud-build.md @@ -1,12 +1,42 @@ -Spring Cloud Build -========== - +Spring Cloud Build.hidden { display: none; +} .switch { border-width: 1px 1px 0 1px; border-style: solid; border-color: #7a2518; display: inline-block; +} .switch--item { padding: 10px; background-color: #ffffff; color: #7a2518; display: inline-block; cursor: pointer; +} .switch--item:not(:first-child) { border-width: 0 0 0 1px; border-style: solid; border-color: #7a2518; +} .switch--item.selected { background-color: #7a2519; color: #ffffff; +} function addBlockSwitches() { for (var primary of document.querySelectorAll('.primary')) { var switchItem = createSwitchItem(primary, createBlockSwitch(primary)); switchItem.item.classList.add("selected"); var title = primary.querySelector('.title') title.remove(); } for (var secondary of document.querySelectorAll('.secondary')) { var primary = findPrimary(secondary); if (primary === null) { console.error("Found secondary block with no primary sibling"); } else { var switchItem = createSwitchItem(secondary, primary.querySelector('.switch')); switchItem.content.classList.add("hidden"); primary.append(switchItem.content); secondary.remove(); } } +} function createElementFromHtml(html) { var template = document.createElement('template'); template.innerHTML = html; return template.content.firstChild; +} function createBlockSwitch(primary) { var blockSwitch = createElementFromHtml('\
\'); primary.prepend(blockSwitch) return blockSwitch; +} function findPrimary(secondary) { var candidate = secondary.previousElementSibling; while (candidate != null && !candidate.classList.contains('primary')) { candidate = candidate.previousElementSibling; } return candidate; +} function createSwitchItem(block, blockSwitch) { var blockName = block.querySelector('.title').textContent; var content = block.querySelectorAll('.content').item(0); var colist = nextSibling(block, '.colist'); if (colist != null) { content.append(colist); } var item = createElementFromHtml('\
' + blockName + '\'); item.dataset.blockName = blockName; content.dataset.blockName = blockName; blockSwitch.append(item); return {'item': item, 'content': content}; +} function nextSibling(element, selector) { var sibling = element.nextElementSibling; while (sibling) { if (sibling.matches(selector)) { return sibling; } sibling = sibling.nextElementSibling; } +} function globalSwitch() { document.querySelectorAll(".switch--item").forEach(function(item) { var blockId = blockIdForSwitchItem(item); var handler = function(event) { selectedText = event.target.textContent; window.localStorage.setItem(blockId, selectedText); for (var switchItem of document.querySelectorAll(".switch--item")) { if (blockIdForSwitchItem(switchItem) === blockId && switchItem.textContent === selectedText) { select(switchItem); } } } item.addEventListener("click", handler); if (item.textContent === window.localStorage.getItem(blockId)) { select(item); } }); +} function select(selected) { for (var child of selected.parentNode.children) { child.classList.remove("selected"); } selected.classList.add("selected"); for (var child of selected.parentNode.parentNode.children) { if (child.classList.contains("content")) { if (selected.dataset.blockName === child.dataset.blockName) { child.classList.remove("hidden"); } else { child.classList.add("hidden"); } } } } function blockIdForSwitchItem(item) { idComponents = [] for (var switchItem of item.parentNode.querySelectorAll(".switch--item")) { idComponents.push(switchItem.textContent.toLowerCase()); } return idComponents.sort().join("-") +} window.onload = function() { addBlockSwitches(); globalSwitch(); +}; + +# Spring Cloud Build + +Table of Contents + +* [Building and Deploying](#_building_and_deploying) +* [Contributing](#_contributing) + * [Sign the Contributor License Agreement](#_sign_the_contributor_license_agreement) + * [Code of Conduct](#_code_of_conduct) + * [Code Conventions and Housekeeping](#_code_conventions_and_housekeeping) + * [Checkstyle](#_checkstyle) + * [IDE setup](#_ide_setup) + * [Duplicate Finder](#_duplicate_finder) + +* [Flattening the POMs](#_flattening_the_poms) +* [Reusing the documentation](#_reusing_the_documentation) +* [Updating the guides](#_updating_the_guides) + +[![Build](https://github.com/spring-cloud/spring-cloud-build/workflows/Build/badge.svg?branch=main&style=svg)](https://github.com/spring-cloud/spring-cloud-build/actions) Spring Cloud Build is a common utility project for Spring Cloud to use for plugin and dependency management. -[Building and Deploying](#_building_and_deploying) ----------- +## [Building and Deploying](#_building_and_deploying) To install locally: @@ -40,8 +70,7 @@ $ mvn deploy -P central -DaltReleaseDeploymentRepository=sonatype-nexus-staging: (the "central" profile is available for all projects in Spring Cloud and it sets up the gpg jar signing, and the repository has to be specified separately for this project because it is a parent of the starter parent which users in turn have as their own parent). -[Contributing](#_contributing) ----------- +## [Contributing](#_contributing) Spring Cloud is released under the non-restrictive Apache 2.0 license, and follows a very standard Github development process, using Github @@ -49,7 +78,7 @@ tracker for issues and merging pull requests into master. If you want to contribute even something trivial please do not hesitate, but follow the guidelines below. -### [Sign the Contributor License Agreement](#_sign_the_contributor_license_agreement) ### +### [Sign the Contributor License Agreement](#_sign_the_contributor_license_agreement) Before we accept a non-trivial patch or pull request we will need you to sign the[Contributor License Agreement](https://cla.pivotal.io/sign/spring). Signing the contributor’s agreement does not grant anyone commit rights to the main @@ -57,13 +86,13 @@ repository, but it does mean that we can accept your contributions, and you will author credit if we do. Active contributors might be asked to join the core team, and given the ability to merge pull requests. -### [Code of Conduct](#_code_of_conduct) ### +### [Code of Conduct](#_code_of_conduct) This project adheres to the Contributor Covenant [code of conduct](https://github.com/spring-cloud/spring-cloud-build/blob/master/docs/src/main/asciidoc/code-of-conduct.adoc). By participating, you are expected to uphold this code. Please report -unacceptable behavior to [[email protected]](/cdn-cgi/l/email-protection#98ebe8eaf1f6ffb5fbf7fcfdb5f7feb5fbf7f6fcedfbecd8e8f1eef7ecf9f4b6f1f7). +unacceptable behavior to [[email protected]](/cdn-cgi/l/email-protection#dba8aba9b2b5bcf6b8b4bfbef6b4bdf6b8b4b5bfaeb8af9babb2adb4afbab7f5b2b4). -### [Code Conventions and Housekeeping](#_code_conventions_and_housekeeping) ### +### [Code Conventions and Housekeeping](#_code_conventions_and_housekeeping) None of these is essential for a pull request, but they will all help. They can also be added after the original pull request but before a merge. @@ -93,7 +122,7 @@ added after the original pull request but before a merge. if you are fixing an existing issue please add `Fixes gh-XXXX` at the end of the commit message (where XXXX is the issue number). -### [Checkstyle](#_checkstyle) ### +### [Checkstyle](#_checkstyle) Spring Cloud Build comes with a set of checkstyle rules. You can find them in the `spring-cloud-build-tools` module. The most notable files under the module are: @@ -114,7 +143,7 @@ spring-cloud-build-tools/ |**2**| File header setup | |**3**|Default suppression rules| -#### [Checkstyle configuration](#_checkstyle_configuration) #### +#### [Checkstyle configuration](#_checkstyle_configuration) Checkstyle rules are **disabled by default**. To add checkstyle to your project just define the following properties and plugins. @@ -181,9 +210,9 @@ $ curl https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/ $ touch .springformat ``` -### [IDE setup](#_ide_setup) ### +### [IDE setup](#_ide_setup) -#### [Intellij IDEA](#_intellij_idea) #### +#### [Intellij IDEA](#_intellij_idea) In order to setup Intellij you should import our coding conventions, inspection profiles and set up the checkstyle plugin. The following files can be found in the [Spring Cloud Build](https://github.com/spring-cloud/spring-cloud-build/tree/master/spring-cloud-build-tools) project. @@ -239,11 +268,11 @@ Go to `File` → `Settings` → `Other settings` → `Checkstyle`. There click o | |Remember to set the `Scan Scope` to `All sources` since we apply checkstyle rules for production and test sources.| |---|------------------------------------------------------------------------------------------------------------------| -### [Duplicate Finder](#_duplicate_finder) ### +### [Duplicate Finder](#_duplicate_finder) Spring Cloud Build brings along the `basepom:duplicate-finder-maven-plugin`, that enables flagging duplicate and conflicting classes and resources on the java classpath. -#### [Duplicate Finder configuration](#_duplicate_finder_configuration) #### +#### [Duplicate Finder configuration](#_duplicate_finder_configuration) Duplicate finder is **enabled by default** and will run in the `verify` phase of your Maven build, but it will only take effect in your project if you add the `duplicate-finder-maven-plugin` to the `build` section of the projecst’s `pom.xml`. @@ -286,8 +315,7 @@ If you need to add `ignoredClassPatterns` or `ignoredResourcePatterns` to your s ``` -[Flattening the POMs](#_flattening_the_poms) ----------- +## [Flattening the POMs](#_flattening_the_poms) To avoid propagating build setup that is required to build a Spring Cloud project, we’re using the maven flatten plugin. It has the advantage of letting you use whatever features you need while publishing "clean" pom to the repository. @@ -304,8 +332,7 @@ In order to add it, add the `org.codehaus.mojo:flatten-maven-plugin` to your `po ``` -[Reusing the documentation](#_reusing_the_documentation) ----------- +## [Reusing the documentation](#_reusing_the_documentation) Spring Cloud Build publishes its `spring-cloud-build-docs` module that contains helpful scripts (e.g. README generation ruby script) and css, xslt and images @@ -415,8 +442,7 @@ Spring Cloud Build Docs comes with a set of attributes for asciidoctor that you ``` -[Updating the guides](#_updating_the_guides) ----------- +## [Updating the guides](#_updating_the_guides) We assume that your project contains guides under the `guides` folder. @@ -448,3 +474,5 @@ what will happen is that for GA project versions, we will clone `gs-guide1`, `gs You can skip this by either not adding the `guides` profile, or passing the `-DskipGuides` system property when the profile is turned on. You can configure the project version passed to guides via the `guides-project.version` (defaults to `${project.version}`). The phase at which guides get updated can be configured by `guides-update.phase` (defaults to `deploy`). + +if (window.parent == window) {(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1\*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga');ga('create', 'UA-2728886-23', 'auto', {'siteSpeedSampleRate': 100});ga('send', 'pageview');} \ No newline at end of file diff --git a/docs/en/spring-cloud/spring-cloud-bus.md b/docs/en/spring-cloud/spring-cloud-bus.md index c7fb7df8b0e02726c60abbf19626d75c60e100db..c1d881976b2e6da51c81d9c24d13973cb15ec247 100644 --- a/docs/en/spring-cloud/spring-cloud-bus.md +++ b/docs/en/spring-cloud/spring-cloud-bus.md @@ -1,5 +1,37 @@ -Spring Cloud Bus -========== +Spring Cloud Bus.hidden { display: none; +} .switch { border-width: 1px 1px 0 1px; border-style: solid; border-color: #7a2518; display: inline-block; +} .switch--item { padding: 10px; background-color: #ffffff; color: #7a2518; display: inline-block; cursor: pointer; +} .switch--item:not(:first-child) { border-width: 0 0 0 1px; border-style: solid; border-color: #7a2518; +} .switch--item.selected { background-color: #7a2519; color: #ffffff; +} function addBlockSwitches() { for (var primary of document.querySelectorAll('.primary')) { var switchItem = createSwitchItem(primary, createBlockSwitch(primary)); switchItem.item.classList.add("selected"); var title = primary.querySelector('.title') title.remove(); } for (var secondary of document.querySelectorAll('.secondary')) { var primary = findPrimary(secondary); if (primary === null) { console.error("Found secondary block with no primary sibling"); } else { var switchItem = createSwitchItem(secondary, primary.querySelector('.switch')); switchItem.content.classList.add("hidden"); primary.append(switchItem.content); secondary.remove(); } } +} function createElementFromHtml(html) { var template = document.createElement('template'); template.innerHTML = html; return template.content.firstChild; +} function createBlockSwitch(primary) { var blockSwitch = createElementFromHtml('\
\'); primary.prepend(blockSwitch) return blockSwitch; +} function findPrimary(secondary) { var candidate = secondary.previousElementSibling; while (candidate != null && !candidate.classList.contains('primary')) { candidate = candidate.previousElementSibling; } return candidate; +} function createSwitchItem(block, blockSwitch) { var blockName = block.querySelector('.title').textContent; var content = block.querySelectorAll('.content').item(0); var colist = nextSibling(block, '.colist'); if (colist != null) { content.append(colist); } var item = createElementFromHtml('\
' + blockName + '\'); item.dataset.blockName = blockName; content.dataset.blockName = blockName; blockSwitch.append(item); return {'item': item, 'content': content}; +} function nextSibling(element, selector) { var sibling = element.nextElementSibling; while (sibling) { if (sibling.matches(selector)) { return sibling; } sibling = sibling.nextElementSibling; } +} function globalSwitch() { document.querySelectorAll(".switch--item").forEach(function(item) { var blockId = blockIdForSwitchItem(item); var handler = function(event) { selectedText = event.target.textContent; window.localStorage.setItem(blockId, selectedText); for (var switchItem of document.querySelectorAll(".switch--item")) { if (blockIdForSwitchItem(switchItem) === blockId && switchItem.textContent === selectedText) { select(switchItem); } } } item.addEventListener("click", handler); if (item.textContent === window.localStorage.getItem(blockId)) { select(item); } }); +} function select(selected) { for (var child of selected.parentNode.children) { child.classList.remove("selected"); } selected.classList.add("selected"); for (var child of selected.parentNode.parentNode.children) { if (child.classList.contains("content")) { if (selected.dataset.blockName === child.dataset.blockName) { child.classList.remove("hidden"); } else { child.classList.add("hidden"); } } } } function blockIdForSwitchItem(item) { idComponents = [] for (var switchItem of item.parentNode.querySelectorAll(".switch--item")) { idComponents.push(switchItem.textContent.toLowerCase()); } return idComponents.sort().join("-") +} window.onload = function() { addBlockSwitches(); globalSwitch(); +}; + +# Spring Cloud Bus + +Table of Contents + +* [1. Quick Start](#quick-start) +* [2. Bus Endpoints](#bus-endpoints) + * [2.1. Bus Refresh Endpoint](#bus-refresh-endpoint) + * [2.2. Bus Env Endpoint](#bus-env-endpoint) + +* [3. Addressing an Instance](#addressing-an-instance) +* [4. Addressing All Instances of a Service](#addressing-all-instances-of-a-service) +* [5. Service ID Must Be Unique](#service-id-must-be-unique) +* [6. Customizing the Message Broker](#customizing-the-message-broker) +* [7. Tracing Bus Events](#tracing-bus-events) +* [8. Broadcasting Your Own Events](#broadcasting-your-own-events) + * [8.1. Registering events in custom packages](#registering-events-in-custom-packages) + +* [9. Configuration properties](#configuration-properties) Spring Cloud Bus links the nodes of a distributed system with a lightweight message broker. This broker can then be used to broadcast state changes (such as configuration @@ -11,8 +43,7 @@ either an AMQP broker or Kafka as the transport. | |Spring Cloud is released under the non-restrictive Apache 2.0 license. If you would like to contribute to this section of the documentation or if you find an error, please find the source code and issue trackers in the project at [github](https://github.com/spring-cloud/spring-cloud-bus).| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -[](#quick-start)[1. Quick Start](#quick-start) ----------- +## [](#quick-start)[1. Quick Start](#quick-start) Spring Cloud Bus works by adding Spring Boot autconfiguration if it detects itself on the classpath. To enable the bus, add `spring-cloud-starter-bus-amqp` or`spring-cloud-starter-bus-kafka` to your dependency management. Spring Cloud takes care of @@ -41,12 +72,11 @@ application’s configuration, as though they had all been pinged on their `/ref | |The Spring Cloud Bus starters cover Rabbit and Kafka, because those are the two most
common implementations. However, Spring Cloud Stream is quite flexible, and the binder
works with `spring-cloud-bus`.| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -[](#bus-endpoints)[2. Bus Endpoints](#bus-endpoints) ----------- +## [](#bus-endpoints)[2. Bus Endpoints](#bus-endpoints) Spring Cloud Bus provides two endpoints, `/actuator/busrefresh` and `/actuator/busenv`that correspond to individual actuator endpoints in Spring Cloud Commons,`/actuator/refresh` and `/actuator/env` respectively. -### [](#bus-refresh-endpoint)[2.1. Bus Refresh Endpoint](#bus-refresh-endpoint) ### +### [](#bus-refresh-endpoint)[2.1. Bus Refresh Endpoint](#bus-refresh-endpoint) The `/actuator/busrefresh` endpoint clears the `RefreshScope` cache and rebinds`@ConfigurationProperties`. See the [Refresh Scope](#refresh-scope) documentation for more information. @@ -58,7 +88,7 @@ application: management.endpoints.web.exposure.include=busrefresh ``` -### [](#bus-env-endpoint)[2.2. Bus Env Endpoint](#bus-env-endpoint) ### +### [](#bus-env-endpoint)[2.2. Bus Env Endpoint](#bus-env-endpoint) The `/actuator/busenv` endpoint updates each instances environment with the specified key/value pair across multiple instances. @@ -79,8 +109,7 @@ The `/actuator/busenv` endpoint accepts `POST` requests with the following shape } ``` -[](#addressing-an-instance)[3. Addressing an Instance](#addressing-an-instance) ----------- +## [](#addressing-an-instance)[3. Addressing an Instance](#addressing-an-instance) Each instance of the application has a service ID, whose value can be set with`spring.cloud.bus.id` and whose value is expected to be a colon-separated list of identifiers, in order from least specific to most specific. The default value is @@ -97,16 +126,14 @@ The HTTP endpoints accept a “destination” path parameter, such as`/busrefres is owned by an instance on the bus, it processes the message, and all other instances ignore it. -[](#addressing-all-instances-of-a-service)[4. Addressing All Instances of a Service](#addressing-all-instances-of-a-service) ----------- +## [](#addressing-all-instances-of-a-service)[4. Addressing All Instances of a Service](#addressing-all-instances-of-a-service) The “destination” parameter is used in a Spring `PathMatcher` (with the path separator as a colon — `:`) to determine if an instance processes the message. Using the example from earlier, `/busenv/customers:**` targets all instances of the “customers” service regardless of the rest of the service ID. -[](#service-id-must-be-unique)[5. Service ID Must Be Unique](#service-id-must-be-unique) ----------- +## [](#service-id-must-be-unique)[5. Service ID Must Be Unique](#service-id-must-be-unique) The bus tries twice to eliminate processing an event — once from the original`ApplicationEvent` and once from the queue. To do so, it checks the sending service ID against the current service ID. If multiple instances of a service have the same ID, @@ -115,8 +142,7 @@ port, and that port is part of the ID. Cloud Foundry supplies an index to differ To ensure that the ID is unique outside Cloud Foundry, set `spring.application.index` to something unique for each instance of a service. -[](#customizing-the-message-broker)[6. Customizing the Message Broker](#customizing-the-message-broker) ----------- +## [](#customizing-the-message-broker)[6. Customizing the Message Broker](#customizing-the-message-broker) Spring Cloud Bus uses [Spring Cloud Stream](https://cloud.spring.io/spring-cloud-stream) to broadcast the messages. So, to get messages to flow, you need only include the binder @@ -130,8 +156,7 @@ middleware). Normally, the defaults suffice. To learn more about how to customize the message broker settings, consult the Spring Cloud Stream documentation. -[](#tracing-bus-events)[7. Tracing Bus Events](#tracing-bus-events) ----------- +## [](#tracing-bus-events)[7. Tracing Bus Events](#tracing-bus-events) Bus events (subclasses of `RemoteApplicationEvent`) can be traced by setting`spring.cloud.bus.trace.enabled=true`. If you do so, the Spring Boot `TraceRepository`(if it is present) shows each event sent and all the acks from each service instance. The following example comes from the `/trace` endpoint: @@ -178,8 +203,7 @@ there. | |Any Bus application can trace acks. However, sometimes, it is
useful to do this in a central service that can do more complex
queries on the data or forward it to a specialized tracing service.| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -[](#broadcasting-your-own-events)[8. Broadcasting Your Own Events](#broadcasting-your-own-events) ----------- +## [](#broadcasting-your-own-events)[8. Broadcasting Your Own Events](#broadcasting-your-own-events) The Bus can carry any event of type `RemoteApplicationEvent`. The default transport is JSON, and the deserializer needs to know which types are going to be used ahead of time. @@ -191,7 +215,7 @@ the default strategy, which is to use the simple name of the class. | |Both the producer and the consumer need access to the class definition.| |---|-----------------------------------------------------------------------| -### [](#registering-events-in-custom-packages)[8.1. Registering events in custom packages](#registering-events-in-custom-packages) ### +### [](#registering-events-in-custom-packages)[8.1. Registering events in custom packages](#registering-events-in-custom-packages) If you cannot or do not want to use a subpackage of `org.springframework.cloud.bus.event`for your custom events, you must specify which packages to scan for events of type`RemoteApplicationEvent` by using the `@RemoteApplicationEventScan` annotation. Packages specified with `@RemoteApplicationEventScan` include subpackages. @@ -240,7 +264,8 @@ All of the preceding examples of `@RemoteApplicationEventScan` are equivalent, i | |You can specify multiple base packages to scan.| |---|-----------------------------------------------| -[](#configuration-properties)[9. Configuration properties](#configuration-properties) ----------- +## [](#configuration-properties)[9. Configuration properties](#configuration-properties) To see the list of all Bus related configuration properties please check [the Appendix page](appendix.html). + +if (window.parent == window) {(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1\*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga');ga('create', 'UA-2728886-23', 'auto', {'siteSpeedSampleRate': 100});ga('send', 'pageview');} \ No newline at end of file diff --git a/docs/en/spring-cloud/spring-cloud-circuitbreaker.md b/docs/en/spring-cloud/spring-cloud-circuitbreaker.md index d8c31d04277997558cf01475b0b05175e9b90319..0df6a6df2521b099052a42ee59e3f0f8d9bc33d8 100644 --- a/docs/en/spring-cloud/spring-cloud-circuitbreaker.md +++ b/docs/en/spring-cloud/spring-cloud-circuitbreaker.md @@ -1,17 +1,78 @@ -Spring Cloud Circuit Breaker -========== - - -[](#usage-documentation)[1. Usage Documentation](#usage-documentation) ----------- +Spring Cloud Circuit Breaker.hidden { display: none; +} .switch { border-width: 1px 1px 0 1px; border-style: solid; border-color: #7a2518; display: inline-block; +} .switch--item { padding: 10px; background-color: #ffffff; color: #7a2518; display: inline-block; cursor: pointer; +} .switch--item:not(:first-child) { border-width: 0 0 0 1px; border-style: solid; border-color: #7a2518; +} .switch--item.selected { background-color: #7a2519; color: #ffffff; +} function addBlockSwitches() { for (var primary of document.querySelectorAll('.primary')) { var switchItem = createSwitchItem(primary, createBlockSwitch(primary)); switchItem.item.classList.add("selected"); var title = primary.querySelector('.title') title.remove(); } for (var secondary of document.querySelectorAll('.secondary')) { var primary = findPrimary(secondary); if (primary === null) { console.error("Found secondary block with no primary sibling"); } else { var switchItem = createSwitchItem(secondary, primary.querySelector('.switch')); switchItem.content.classList.add("hidden"); primary.append(switchItem.content); secondary.remove(); } } +} function createElementFromHtml(html) { var template = document.createElement('template'); template.innerHTML = html; return template.content.firstChild; +} function createBlockSwitch(primary) { var blockSwitch = createElementFromHtml('\
\'); primary.prepend(blockSwitch) return blockSwitch; +} function findPrimary(secondary) { var candidate = secondary.previousElementSibling; while (candidate != null && !candidate.classList.contains('primary')) { candidate = candidate.previousElementSibling; } return candidate; +} function createSwitchItem(block, blockSwitch) { var blockName = block.querySelector('.title').textContent; var content = block.querySelectorAll('.content').item(0); var colist = nextSibling(block, '.colist'); if (colist != null) { content.append(colist); } var item = createElementFromHtml('\
' + blockName + '\'); item.dataset.blockName = blockName; content.dataset.blockName = blockName; blockSwitch.append(item); return {'item': item, 'content': content}; +} function nextSibling(element, selector) { var sibling = element.nextElementSibling; while (sibling) { if (sibling.matches(selector)) { return sibling; } sibling = sibling.nextElementSibling; } +} function globalSwitch() { document.querySelectorAll(".switch--item").forEach(function(item) { var blockId = blockIdForSwitchItem(item); var handler = function(event) { selectedText = event.target.textContent; window.localStorage.setItem(blockId, selectedText); for (var switchItem of document.querySelectorAll(".switch--item")) { if (blockIdForSwitchItem(switchItem) === blockId && switchItem.textContent === selectedText) { select(switchItem); } } } item.addEventListener("click", handler); if (item.textContent === window.localStorage.getItem(blockId)) { select(item); } }); +} function select(selected) { for (var child of selected.parentNode.children) { child.classList.remove("selected"); } selected.classList.add("selected"); for (var child of selected.parentNode.parentNode.children) { if (child.classList.contains("content")) { if (selected.dataset.blockName === child.dataset.blockName) { child.classList.remove("hidden"); } else { child.classList.add("hidden"); } } } } function blockIdForSwitchItem(item) { idComponents = [] for (var switchItem of item.parentNode.querySelectorAll(".switch--item")) { idComponents.push(switchItem.textContent.toLowerCase()); } return idComponents.sort().join("-") +} window.onload = function() { addBlockSwitches(); globalSwitch(); +}; + +# Spring Cloud Circuit Breaker + +Table of Contents + +* [1. Usage Documentation](#usage-documentation) + * [1.1. Configuring Resilience4J Circuit Breakers](#configuring-resilience4j-circuit-breakers) + * [1.1.1. Starters](#starters) + * [1.1.2. Auto-Configuration](#auto-configuration) + * [1.1.3. Default Configuration](#default-configuration) + * [Reactive Example](#reactive-example) + + * [1.1.4. Specific Circuit Breaker Configuration](#specific-circuit-breaker-configuration) + * [Reactive Example](#reactive-example-2) + + * [1.1.5. Circuit Breaker Properties Configuration](#circuit-breaker-properties-configuration) + * [1.1.6. Bulkhead pattern supporting](#bulkhead-pattern-supporting) + * [1.1.7. Specific Bulkhead Configuration](#specific-bulkhead-configuration) + * [Bulkhead Example](#bulkhead-example) + * [Thread Pool Bulkhead Example](#thread-pool-bulkhead-example) + + * [1.1.8. Bulkhead Properties Configuration](#bulkhead-properties-configuration) + * [1.1.9. Collecting Metrics](#collecting-metrics) + + * [1.2. Configuring Spring Retry Circuit Breakers](#configuring-spring-retry-circuit-breakers) + * [1.2.1. Default Configuration](#default-configuration-2) + * [1.2.2. Specific Circuit Breaker Configuration](#specific-circuit-breaker-configuration-2) + +* [2. Building](#building) + * [2.1. Basic Compile and Test](#basic-compile-and-test) + * [2.2. Documentation](#documentation) + * [2.3. Working with the code](#working-with-the-code) + * [2.3.1. Activate the Spring Maven profile](#activate-the-spring-maven-profile) + * [2.3.2. Importing into eclipse with m2eclipse](#importing-into-eclipse-with-m2eclipse) + * [2.3.3. Importing into eclipse without m2eclipse](#importing-into-eclipse-without-m2eclipse) + +* [3. Contributing](#contributing) + * [3.1. Sign the Contributor License Agreement](#sign-the-contributor-license-agreement) + * [3.2. Code of Conduct](#code-of-conduct) + * [3.3. Code Conventions and Housekeeping](#code-conventions-and-housekeeping) + * [3.4. Checkstyle](#checkstyle) + * [3.4.1. Checkstyle configuration](#checkstyle-configuration) + + * [3.5. IDE setup](#ide-setup) + * [3.5.1. Intellij IDEA](#intellij-idea) + + * [3.6. Duplicate Finder](#duplicate-finder) + * [3.6.1. Duplicate Finder configuration](#duplicate-finder-configuration) + +**2.1.1** + +## [](#usage-documentation)[1. Usage Documentation](#usage-documentation) The Spring Cloud CircuitBreaker project contains implementations for Resilience4J and Spring Retry. The APIs implemented in Spring Cloud CircuitBreaker live in Spring Cloud Commons. The usage documentation for these APIs are located in the [Spring Cloud Commons documentation](https://docs.spring.io/spring-cloud-commons/docs/current/reference/html/#spring-cloud-circuit-breaker). -### [](#configuring-resilience4j-circuit-breakers)[1.1. Configuring Resilience4J Circuit Breakers](#configuring-resilience4j-circuit-breakers) ### +### [](#configuring-resilience4j-circuit-breakers)[1.1. Configuring Resilience4J Circuit Breakers](#configuring-resilience4j-circuit-breakers) -#### [](#starters)[1.1.1. Starters](#starters) #### +#### [](#starters)[1.1.1. Starters](#starters) There are two starters for the Resilience4J implementations, one for reactive applications and one for non-reactive applications. @@ -19,11 +80,11 @@ There are two starters for the Resilience4J implementations, one for reactive ap * `org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j` - reactive applications -#### [](#auto-configuration)[1.1.2. Auto-Configuration](#auto-configuration) #### +#### [](#auto-configuration)[1.1.2. Auto-Configuration](#auto-configuration) You can disable the Resilience4J auto-configuration by setting`spring.cloud.circuitbreaker.resilience4j.enabled` to `false`. -#### [](#default-configuration)[1.1.3. Default Configuration](#default-configuration) #### +#### [](#default-configuration)[1.1.3. Default Configuration](#default-configuration) To provide a default configuration for all of your circuit breakers create a `Customize` bean that is passed a`Resilience4JCircuitBreakerFactory` or `ReactiveResilience4JCircuitBreakerFactory`. The `configureDefault` method can be used to provide a default configuration. @@ -38,7 +99,7 @@ public Customizer defaultCustomizer() { } ``` -##### [](#reactive-example)[Reactive Example](#reactive-example) ##### +##### [](#reactive-example)[Reactive Example](#reactive-example) ``` @Bean @@ -49,7 +110,7 @@ public Customizer defaultCustomizer() } ``` -#### [](#specific-circuit-breaker-configuration)[1.1.4. Specific Circuit Breaker Configuration](#specific-circuit-breaker-configuration) #### +#### [](#specific-circuit-breaker-configuration)[1.1.4. Specific Circuit Breaker Configuration](#specific-circuit-breaker-configuration) Similarly to providing a default configuration, you can create a `Customize` bean this is passed a`Resilience4JCircuitBreakerFactory` or `ReactiveResilience4JCircuitBreakerFactory`. @@ -73,7 +134,7 @@ public Customizer slowCustomizer() { } ``` -##### [](#reactive-example-2)[Reactive Example](#reactive-example-2) ##### +##### [](#reactive-example-2)[Reactive Example](#reactive-example-2) ``` @Bean @@ -88,7 +149,7 @@ public Customizer slowCustomizer() { } ``` -#### [](#circuit-breaker-properties-configuration)[1.1.5. Circuit Breaker Properties Configuration](#circuit-breaker-properties-configuration) #### +#### [](#circuit-breaker-properties-configuration)[1.1.5. Circuit Breaker Properties Configuration](#circuit-breaker-properties-configuration) You can configure `CircuitBreaker` and `TimeLimiter` instances in your application’s configuration properties file. Property configuration has higher priority than Java `Customizer` configuration. @@ -118,7 +179,7 @@ resilience4j.timelimiter: For more information on Resilience4j property configuration, see [Resilience4J Spring Boot 2 Configuration](https://resilience4j.readme.io/docs/getting-started-3#configuration). -#### [](#bulkhead-pattern-supporting)[1.1.6. Bulkhead pattern supporting](#bulkhead-pattern-supporting) #### +#### [](#bulkhead-pattern-supporting)[1.1.6. Bulkhead pattern supporting](#bulkhead-pattern-supporting) If `resilience4j-bulkhead` is on the classpath, Spring Cloud CircuitBreaker will wrap all methods with a Resilience4j Bulkhead. You can disable the Resilience4j Bulkhead by setting `spring.cloud.circuitbreaker.bulkhead.resilience4j.enabled` to `false`. @@ -145,7 +206,7 @@ public Customizer defaultBulkheadCustomizer() { } ``` -#### [](#specific-bulkhead-configuration)[1.1.7. Specific Bulkhead Configuration](#specific-bulkhead-configuration) #### +#### [](#specific-bulkhead-configuration)[1.1.7. Specific Bulkhead Configuration](#specific-bulkhead-configuration) Similarly to proving a default 'Bulkhead' or 'ThreadPoolBulkhead' configuration, you can create a `Customize` bean this is passed a `Resilience4jBulkheadProvider`. @@ -162,7 +223,7 @@ public Customizer slowBulkheadProviderCustomizer() In addition to configuring the Bulkhead that is created you can also customize the bulkhead and thread pool bulkhead after they have been created but before they are returned to caller. To do this you can use the `addBulkheadCustomizer` and `addThreadPoolBulkheadCustomizer`methods. -##### [](#bulkhead-example)[Bulkhead Example](#bulkhead-example) ##### +##### [](#bulkhead-example)[Bulkhead Example](#bulkhead-example) ``` @Bean @@ -173,7 +234,7 @@ public Customizer customizer() { } ``` -##### [](#thread-pool-bulkhead-example)[Thread Pool Bulkhead Example](#thread-pool-bulkhead-example) ##### +##### [](#thread-pool-bulkhead-example)[Thread Pool Bulkhead Example](#thread-pool-bulkhead-example) ``` @Bean @@ -184,7 +245,7 @@ public Customizer slowThreadPoolBulkheadCustomizer } ``` -#### [](#bulkhead-properties-configuration)[1.1.8. Bulkhead Properties Configuration](#bulkhead-properties-configuration) #### +#### [](#bulkhead-properties-configuration)[1.1.8. Bulkhead Properties Configuration](#bulkhead-properties-configuration) You can configure ThreadPoolBulkhead and SemaphoreBulkhead instances in your application’s configuration properties file. Property configuration has higher priority than Java `Customizer` configuration. @@ -203,7 +264,7 @@ resilience4j.bulkhead: For more inforamtion on the Resilience4j property configuration, see [Resilience4J Spring Boot 2 Configuration](https://resilience4j.readme.io/docs/getting-started-3#configuration). -#### [](#collecting-metrics)[1.1.9. Collecting Metrics](#collecting-metrics) #### +#### [](#collecting-metrics)[1.1.9. Collecting Metrics](#collecting-metrics) Spring Cloud Circuit Breaker Resilience4j includes auto-configuration to setup metrics collection as long as the right dependencies are on the classpath. To enable metric collection you must include `org.springframework.boot:spring-boot-starter-actuator`, and `io.github.resilience4j:resilience4j-micrometer`. For more information on the metrics that @@ -212,7 +273,7 @@ get produced when these dependencies are present, see the [Resilience4j document | |You don’t have to include `micrometer-core` directly as it is brought in by `spring-boot-starter-actuator`| |---|----------------------------------------------------------------------------------------------------------| -### [](#configuring-spring-retry-circuit-breakers)[1.2. Configuring Spring Retry Circuit Breakers](#configuring-spring-retry-circuit-breakers) ### +### [](#configuring-spring-retry-circuit-breakers)[1.2. Configuring Spring Retry Circuit Breakers](#configuring-spring-retry-circuit-breakers) Spring Retry provides declarative retry support for Spring applications. A subset of the project includes the ability to implement circuit breaker functionality. @@ -220,7 +281,7 @@ Spring Retry provides a circuit breaker implementation via a combination of it All circuit breakers created using Spring Retry will be created using the `CircuitBreakerRetryPolicy` and a[`DefaultRetryState`](https://github.com/spring-projects/spring-retry/blob/master/src/main/java/org/springframework/retry/support/DefaultRetryState.java). Both of these classes can be configured using `SpringRetryConfigBuilder`. -#### [](#default-configuration-2)[1.2.1. Default Configuration](#default-configuration-2) #### +#### [](#default-configuration-2)[1.2.1. Default Configuration](#default-configuration-2) To provide a default configuration for all of your circuit breakers create a `Customize` bean that is passed a`SpringRetryCircuitBreakerFactory`. The `configureDefault` method can be used to provide a default configuration. @@ -233,7 +294,7 @@ public Customizer defaultCustomizer() { } ``` -#### [](#specific-circuit-breaker-configuration-2)[1.2.2. Specific Circuit Breaker Configuration](#specific-circuit-breaker-configuration-2) #### +#### [](#specific-circuit-breaker-configuration-2)[1.2.2. Specific Circuit Breaker Configuration](#specific-circuit-breaker-configuration-2) Similarly to providing a default configuration, you can create a `Customize` bean this is passed a`SpringRetryCircuitBreakerFactory`. @@ -271,10 +332,9 @@ public Customizer slowCustomizer() { } ``` -[](#building)[2. Building](#building) ----------- +## [](#building)[2. Building](#building) -### [](#basic-compile-and-test)[2.1. Basic Compile and Test](#basic-compile-and-test) ### +### [](#basic-compile-and-test)[2.1. Basic Compile and Test](#basic-compile-and-test) To build the source you will need to install JDK 17. @@ -295,7 +355,7 @@ $ ./mvnw install The projects that require middleware (i.e. Redis) for testing generally require that a local instance of [Docker]([www.docker.com/get-started](https://www.docker.com/get-started)) is installed and running. -### [](#documentation)[2.2. Documentation](#documentation) ### +### [](#documentation)[2.2. Documentation](#documentation) The spring-cloud-build module has a "docs" profile, and if you switch that on it will try to build asciidoc sources from`src/main/asciidoc`. As part of that process it will look for a`README.adoc` and process it by loading all the includes, but not @@ -303,18 +363,18 @@ parsing or rendering it, just copying it to `${main.basedir}`(defaults to `$/tmp any changes in the README it will then show up after a Maven build as a modified file in the correct place. Just commit it and push the change. -### [](#working-with-the-code)[2.3. Working with the code](#working-with-the-code) ### +### [](#working-with-the-code)[2.3. Working with the code](#working-with-the-code) If you don’t have an IDE preference we would recommend that you use[Spring Tools Suite](https://www.springsource.com/developer/sts) or[Eclipse](https://eclipse.org) when working with the code. We use the[m2eclipse](https://eclipse.org/m2e/) eclipse plugin for maven support. Other IDEs and tools should also work without issue as long as they use Maven 3.3.3 or better. -#### [](#activate-the-spring-maven-profile)[2.3.1. Activate the Spring Maven profile](#activate-the-spring-maven-profile) #### +#### [](#activate-the-spring-maven-profile)[2.3.1. Activate the Spring Maven profile](#activate-the-spring-maven-profile) Spring Cloud projects require the 'spring' Maven profile to be activated to resolve the spring milestone and snapshot repositories. Use your preferred IDE to set this profile to be active, or you may experience build errors. -#### [](#importing-into-eclipse-with-m2eclipse)[2.3.2. Importing into eclipse with m2eclipse](#importing-into-eclipse-with-m2eclipse) #### +#### [](#importing-into-eclipse-with-m2eclipse)[2.3.2. Importing into eclipse with m2eclipse](#importing-into-eclipse-with-m2eclipse) We recommend the [m2eclipse](https://eclipse.org/m2e/) eclipse plugin when working with eclipse. If you don’t already have m2eclipse installed it is available from the "eclipse @@ -323,7 +383,7 @@ marketplace". | |Older versions of m2e do not support Maven 3.3, so once the
projects are imported into Eclipse you will also need to tell
m2eclipse to use the right profile for the projects. If you
see many different errors related to the POMs in the projects, check
that you have an up to date installation. If you can’t upgrade m2e,
add the "spring" profile to your `settings.xml`. Alternatively you can
copy the repository settings from the "spring" profile of the parent
pom into your `settings.xml`.| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -#### [](#importing-into-eclipse-without-m2eclipse)[2.3.3. Importing into eclipse without m2eclipse](#importing-into-eclipse-without-m2eclipse) #### +#### [](#importing-into-eclipse-without-m2eclipse)[2.3.3. Importing into eclipse without m2eclipse](#importing-into-eclipse-without-m2eclipse) If you prefer not to use m2eclipse you can generate eclipse project metadata using the following command: @@ -334,8 +394,7 @@ $ ./mvnw eclipse:eclipse The generated eclipse projects can be imported by selecting `import existing projects`from the `file` menu. -[](#contributing)[3. Contributing](#contributing) ----------- +## [](#contributing)[3. Contributing](#contributing) Spring Cloud is released under the non-restrictive Apache 2.0 license, and follows a very standard Github development process, using Github @@ -343,7 +402,7 @@ tracker for issues and merging pull requests into master. If you want to contribute even something trivial please do not hesitate, but follow the guidelines below. -### [](#sign-the-contributor-license-agreement)[3.1. Sign the Contributor License Agreement](#sign-the-contributor-license-agreement) ### +### [](#sign-the-contributor-license-agreement)[3.1. Sign the Contributor License Agreement](#sign-the-contributor-license-agreement) Before we accept a non-trivial patch or pull request we will need you to sign the[Contributor License Agreement](https://cla.pivotal.io/sign/spring). Signing the contributor’s agreement does not grant anyone commit rights to the main @@ -351,13 +410,13 @@ repository, but it does mean that we can accept your contributions, and you will author credit if we do. Active contributors might be asked to join the core team, and given the ability to merge pull requests. -### [](#code-of-conduct)[3.2. Code of Conduct](#code-of-conduct) ### +### [](#code-of-conduct)[3.2. Code of Conduct](#code-of-conduct) This project adheres to the Contributor Covenant [code of conduct](https://github.com/spring-cloud/spring-cloud-build/blob/master/docs/src/main/asciidoc/code-of-conduct.adoc). By participating, you are expected to uphold this code. Please report -unacceptable behavior to [[email protected]](/cdn-cgi/l/email-protection#fd8e8d8f94939ad09e929998d0929bd09e929399889e89bd8d948b92899c91d39492). +unacceptable behavior to [[email protected]](/cdn-cgi/l/email-protection#4c3f3c3e25222b612f23282961232a612f232228392f380c3c253a23382d20622523). -### [](#code-conventions-and-housekeeping)[3.3. Code Conventions and Housekeeping](#code-conventions-and-housekeeping) ### +### [](#code-conventions-and-housekeeping)[3.3. Code Conventions and Housekeeping](#code-conventions-and-housekeeping) None of these is essential for a pull request, but they will all help. They can also be added after the original pull request but before a merge. @@ -387,7 +446,7 @@ added after the original pull request but before a merge. if you are fixing an existing issue please add `Fixes gh-XXXX` at the end of the commit message (where XXXX is the issue number). -### [](#checkstyle)[3.4. Checkstyle](#checkstyle) ### +### [](#checkstyle)[3.4. Checkstyle](#checkstyle) Spring Cloud Build comes with a set of checkstyle rules. You can find them in the `spring-cloud-build-tools` module. The most notable files under the module are: @@ -408,7 +467,7 @@ spring-cloud-build-tools/ |**2**| File header setup | |**3**|Default suppression rules| -#### [](#checkstyle-configuration)[3.4.1. Checkstyle configuration](#checkstyle-configuration) #### +#### [](#checkstyle-configuration)[3.4.1. Checkstyle configuration](#checkstyle-configuration) Checkstyle rules are **disabled by default**. To add checkstyle to your project just define the following properties and plugins. @@ -475,9 +534,9 @@ $ curl https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/ $ touch .springformat ``` -### [](#ide-setup)[3.5. IDE setup](#ide-setup) ### +### [](#ide-setup)[3.5. IDE setup](#ide-setup) -#### [](#intellij-idea)[3.5.1. Intellij IDEA](#intellij-idea) #### +#### [](#intellij-idea)[3.5.1. Intellij IDEA](#intellij-idea) In order to setup Intellij you should import our coding conventions, inspection profiles and set up the checkstyle plugin. The following files can be found in the [Spring Cloud Build](https://github.com/spring-cloud/spring-cloud-build/tree/master/spring-cloud-build-tools) project. @@ -533,11 +592,11 @@ Go to `File` → `Settings` → `Other settings` → `Checkstyle`. There click o | |Remember to set the `Scan Scope` to `All sources` since we apply checkstyle rules for production and test sources.| |---|------------------------------------------------------------------------------------------------------------------| -### [](#duplicate-finder)[3.6. Duplicate Finder](#duplicate-finder) ### +### [](#duplicate-finder)[3.6. Duplicate Finder](#duplicate-finder) Spring Cloud Build brings along the `basepom:duplicate-finder-maven-plugin`, that enables flagging duplicate and conflicting classes and resources on the java classpath. -#### [](#duplicate-finder-configuration)[3.6.1. Duplicate Finder configuration](#duplicate-finder-configuration) #### +#### [](#duplicate-finder-configuration)[3.6.1. Duplicate Finder configuration](#duplicate-finder-configuration) Duplicate finder is **enabled by default** and will run in the `verify` phase of your Maven build, but it will only take effect in your project if you add the `duplicate-finder-maven-plugin` to the `build` section of the projecst’s `pom.xml`. @@ -580,3 +639,4 @@ If you need to add `ignoredClassPatterns` or `ignoredResourcePatterns` to your s ``` +if (window.parent == window) {(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1\*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga');ga('create', 'UA-2728886-23', 'auto', {'siteSpeedSampleRate': 100});ga('send', 'pageview');} \ No newline at end of file diff --git a/docs/en/spring-cloud/spring-cloud-cli.md b/docs/en/spring-cloud/spring-cloud-cli.md index a9618c412a0e9f6af9a0800dff7bbe96423618b1..ef0e718b425fce54b7968ed69ff63927a85feb35 100644 --- a/docs/en/spring-cloud/spring-cloud-cli.md +++ b/docs/en/spring-cloud/spring-cloud-cli.md @@ -1,6 +1,29 @@ -Spring Boot Cloud CLI -========== - +Spring Boot Cloud CLI.hidden { display: none; +} .switch { border-width: 1px 1px 0 1px; border-style: solid; border-color: #7a2518; display: inline-block; +} .switch--item { padding: 10px; background-color: #ffffff; color: #7a2518; display: inline-block; cursor: pointer; +} .switch--item:not(:first-child) { border-width: 0 0 0 1px; border-style: solid; border-color: #7a2518; +} .switch--item.selected { background-color: #7a2519; color: #ffffff; +} function addBlockSwitches() { for (var primary of document.querySelectorAll('.primary')) { var switchItem = createSwitchItem(primary, createBlockSwitch(primary)); switchItem.item.classList.add("selected"); var title = primary.querySelector('.title') title.remove(); } for (var secondary of document.querySelectorAll('.secondary')) { var primary = findPrimary(secondary); if (primary === null) { console.error("Found secondary block with no primary sibling"); } else { var switchItem = createSwitchItem(secondary, primary.querySelector('.switch')); switchItem.content.classList.add("hidden"); primary.append(switchItem.content); secondary.remove(); } } +} function createElementFromHtml(html) { var template = document.createElement('template'); template.innerHTML = html; return template.content.firstChild; +} function createBlockSwitch(primary) { var blockSwitch = createElementFromHtml('\
\'); primary.prepend(blockSwitch) return blockSwitch; +} function findPrimary(secondary) { var candidate = secondary.previousElementSibling; while (candidate != null && !candidate.classList.contains('primary')) { candidate = candidate.previousElementSibling; } return candidate; +} function createSwitchItem(block, blockSwitch) { var blockName = block.querySelector('.title').textContent; var content = block.querySelectorAll('.content').item(0); var colist = nextSibling(block, '.colist'); if (colist != null) { content.append(colist); } var item = createElementFromHtml('\
' + blockName + '\'); item.dataset.blockName = blockName; content.dataset.blockName = blockName; blockSwitch.append(item); return {'item': item, 'content': content}; +} function nextSibling(element, selector) { var sibling = element.nextElementSibling; while (sibling) { if (sibling.matches(selector)) { return sibling; } sibling = sibling.nextElementSibling; } +} function globalSwitch() { document.querySelectorAll(".switch--item").forEach(function(item) { var blockId = blockIdForSwitchItem(item); var handler = function(event) { selectedText = event.target.textContent; window.localStorage.setItem(blockId, selectedText); for (var switchItem of document.querySelectorAll(".switch--item")) { if (blockIdForSwitchItem(switchItem) === blockId && switchItem.textContent === selectedText) { select(switchItem); } } } item.addEventListener("click", handler); if (item.textContent === window.localStorage.getItem(blockId)) { select(item); } }); +} function select(selected) { for (var child of selected.parentNode.children) { child.classList.remove("selected"); } selected.classList.add("selected"); for (var child of selected.parentNode.parentNode.children) { if (child.classList.contains("content")) { if (selected.dataset.blockName === child.dataset.blockName) { child.classList.remove("hidden"); } else { child.classList.add("hidden"); } } } } function blockIdForSwitchItem(item) { idComponents = [] for (var switchItem of item.parentNode.querySelectorAll(".switch--item")) { idComponents.push(switchItem.textContent.toLowerCase()); } return idComponents.sort().join("-") +} window.onload = function() { addBlockSwitches(); globalSwitch(); +}; + +# Spring Boot Cloud CLI + +Table of Contents + +* [Installation](#_installation) +* [Running Spring Cloud Services in Development](#_running_spring_cloud_services_in_development) + * [Adding Additional Applications](#_adding_additional_applications) + +* [Writing Groovy Scripts and Running Applications](#_writing_groovy_scripts_and_running_applications) +* [Encryption and Decryption](#_encryption_and_decryption) Spring Boot CLI provides [Spring Boot](https://projects.spring.io/spring-boot) command line features for [Spring @@ -15,8 +38,7 @@ development time). | |Spring Cloud is released under the non-restrictive Apache 2.0 license. If you would like to contribute to this section of the documentation or if you find an error, please find the source code and issue trackers in the project at [github](https://github.com/spring-cloud/spring-cloud-cli).| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -[Installation](#_installation) ----------- +## [Installation](#_installation) To install, make sure you have[Spring Boot CLI](https://github.com/spring-projects/spring-boot)(2.0.0 or better): @@ -43,8 +65,7 @@ $ spring install org.springframework.cloud:spring-cloud-cli:2.2.0.RELEASE | |**Prerequisites:** to use the encryption and decryption features
you need the full-strength JCE installed in your JVM (it’s not there by default).
You can download the "Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files"
from Oracle, and follow instructions for installation (essentially replace the 2 policy files
in the JRE lib/security directory with the ones that you downloaded).| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -[Running Spring Cloud Services in Development](#_running_spring_cloud_services_in_development) ----------- +## [Running Spring Cloud Services in Development](#_running_spring_cloud_services_in_development) The Launcher CLI can be used to run common services like Eureka, Config Server etc. from the command line. To list the available @@ -96,7 +117,7 @@ stubrunner: - com.example:beer-api-producer:+:9876 ``` -### [Adding Additional Applications](#_adding_additional_applications) ### +### [Adding Additional Applications](#_adding_additional_applications) Additional applications can be added to `./config/cloud.yml` (not`./config.yml` because that would replace the defaults), e.g. with @@ -124,8 +145,7 @@ source sink configserver dataflow eureka h2 kafka stubrunner zipkin (notice the additional apps at the start of the list). -[Writing Groovy Scripts and Running Applications](#_writing_groovy_scripts_and_running_applications) ----------- +## [Writing Groovy Scripts and Running Applications](#_writing_groovy_scripts_and_running_applications) Spring Cloud CLI has support for most of the Spring Cloud declarative features, such as the `@Enable*` class of annotations. For example, @@ -162,8 +182,7 @@ class Service { } ``` -[Encryption and Decryption](#_encryption_and_decryption) ----------- +## [Encryption and Decryption](#_encryption_and_decryption) The Spring Cloud CLI comes with an "encrypt" and a "decrypt" command. Both accept arguments in the same form with a key specified @@ -183,3 +202,5 @@ the key value with "@" and provide the file path, e.g. $ spring encrypt mysecret --key @${HOME}/.ssh/id_rsa.pub AQAjPgt3eFZQXwt8tsHAVv/QHiY5sI2dRcR+... ``` + +if (window.parent == window) {(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1\*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga');ga('create', 'UA-2728886-23', 'auto', {'siteSpeedSampleRate': 100});ga('send', 'pageview');} \ No newline at end of file diff --git a/docs/en/spring-cloud/spring-cloud-cloudfoundry.md b/docs/en/spring-cloud/spring-cloud-cloudfoundry.md index a65144335b87d2a9fdb71acb05ea590f19cd18fb..d52f61438381aaea846ecf2fac99843f363b6778 100644 --- a/docs/en/spring-cloud/spring-cloud-cloudfoundry.md +++ b/docs/en/spring-cloud/spring-cloud-cloudfoundry.md @@ -1,6 +1,26 @@ -Spring Cloud for Cloud Foundry -========== - +Spring Cloud for Cloud Foundry.hidden { display: none; +} .switch { border-width: 1px 1px 0 1px; border-style: solid; border-color: #7a2518; display: inline-block; +} .switch--item { padding: 10px; background-color: #ffffff; color: #7a2518; display: inline-block; cursor: pointer; +} .switch--item:not(:first-child) { border-width: 0 0 0 1px; border-style: solid; border-color: #7a2518; +} .switch--item.selected { background-color: #7a2519; color: #ffffff; +} function addBlockSwitches() { for (var primary of document.querySelectorAll('.primary')) { var switchItem = createSwitchItem(primary, createBlockSwitch(primary)); switchItem.item.classList.add("selected"); var title = primary.querySelector('.title') title.remove(); } for (var secondary of document.querySelectorAll('.secondary')) { var primary = findPrimary(secondary); if (primary === null) { console.error("Found secondary block with no primary sibling"); } else { var switchItem = createSwitchItem(secondary, primary.querySelector('.switch')); switchItem.content.classList.add("hidden"); primary.append(switchItem.content); secondary.remove(); } } +} function createElementFromHtml(html) { var template = document.createElement('template'); template.innerHTML = html; return template.content.firstChild; +} function createBlockSwitch(primary) { var blockSwitch = createElementFromHtml('\
\'); primary.prepend(blockSwitch) return blockSwitch; +} function findPrimary(secondary) { var candidate = secondary.previousElementSibling; while (candidate != null && !candidate.classList.contains('primary')) { candidate = candidate.previousElementSibling; } return candidate; +} function createSwitchItem(block, blockSwitch) { var blockName = block.querySelector('.title').textContent; var content = block.querySelectorAll('.content').item(0); var colist = nextSibling(block, '.colist'); if (colist != null) { content.append(colist); } var item = createElementFromHtml('\
' + blockName + '\'); item.dataset.blockName = blockName; content.dataset.blockName = blockName; blockSwitch.append(item); return {'item': item, 'content': content}; +} function nextSibling(element, selector) { var sibling = element.nextElementSibling; while (sibling) { if (sibling.matches(selector)) { return sibling; } sibling = sibling.nextElementSibling; } +} function globalSwitch() { document.querySelectorAll(".switch--item").forEach(function(item) { var blockId = blockIdForSwitchItem(item); var handler = function(event) { selectedText = event.target.textContent; window.localStorage.setItem(blockId, selectedText); for (var switchItem of document.querySelectorAll(".switch--item")) { if (blockIdForSwitchItem(switchItem) === blockId && switchItem.textContent === selectedText) { select(switchItem); } } } item.addEventListener("click", handler); if (item.textContent === window.localStorage.getItem(blockId)) { select(item); } }); +} function select(selected) { for (var child of selected.parentNode.children) { child.classList.remove("selected"); } selected.classList.add("selected"); for (var child of selected.parentNode.parentNode.children) { if (child.classList.contains("content")) { if (selected.dataset.blockName === child.dataset.blockName) { child.classList.remove("hidden"); } else { child.classList.add("hidden"); } } } } function blockIdForSwitchItem(item) { idComponents = [] for (var switchItem of item.parentNode.querySelectorAll(".switch--item")) { idComponents.push(switchItem.textContent.toLowerCase()); } return idComponents.sort().join("-") +} window.onload = function() { addBlockSwitches(); globalSwitch(); +}; + +# Spring Cloud for Cloud Foundry + +Table of Contents + +* [1. Discovery](#discovery) +* [2. Single Sign On](#single-sign-on) +* [3. Configuration](#configuration) Spring Cloud for Cloudfoundry makes it easy to run[Spring Cloud](https://github.com/spring-cloud) apps in[Cloud Foundry](https://github.com/cloudfoundry) (the Platform as a Service). Cloud Foundry has the notion of a "service", which is @@ -23,8 +43,7 @@ can use the `DiscoveryClient` directly or via a `LoadBalancerClient`. The first time you use it the discovery client might be slow owing to the fact that it has to get an access token from Cloud Foundry. -[](#discovery)[1. Discovery](#discovery) ----------- +## [](#discovery)[1. Discovery](#discovery) Here’s a Spring Cloud app with Cloud Foundry discovery: @@ -61,8 +80,7 @@ the credentials it is authenticated with, where the space defaults to the one the client is running in (if any). If neither org nor space are configured, they default per the user’s profile in Cloud Foundry. -[](#single-sign-on)[2. Single Sign On](#single-sign-on) ----------- +## [](#single-sign-on)[2. Single Sign On](#single-sign-on) | |All of the OAuth2 SSO and resource server features moved to Spring Boot
in version 1.3. You can find documentation in the[Spring Boot user guide](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/).| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| @@ -74,8 +92,8 @@ service called "sso", for instance, with credentials containing automatically to the Spring OAuth2 client that you enable with`@EnableOAuth2Sso` (from Spring Boot). The name of the service can be parameterized using `spring.oauth2.sso.serviceId`. -[](#configuration)[3. Configuration](#configuration) ----------- +## [](#configuration)[3. Configuration](#configuration) To see the list of all Spring Cloud Sloud Foundry related configuration properties please check [the Appendix page](appendix.html). +if (window.parent == window) {(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1\*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga');ga('create', 'UA-2728886-23', 'auto', {'siteSpeedSampleRate': 100});ga('send', 'pageview');} \ No newline at end of file diff --git a/docs/en/spring-cloud/spring-cloud-commons.md b/docs/en/spring-cloud/spring-cloud-commons.md index c790479244c4e35277a14ddaf4bca868a4b9cb7e..99a079ec8a2c4af9085af33ff791cd9edd881efe 100644 --- a/docs/en/spring-cloud/spring-cloud-commons.md +++ b/docs/en/spring-cloud/spring-cloud-commons.md @@ -1,5 +1,106 @@ -Cloud Native Applications -========== +Cloud Native Applications.hidden { display: none; +} .switch { border-width: 1px 1px 0 1px; border-style: solid; border-color: #7a2518; display: inline-block; +} .switch--item { padding: 10px; background-color: #ffffff; color: #7a2518; display: inline-block; cursor: pointer; +} .switch--item:not(:first-child) { border-width: 0 0 0 1px; border-style: solid; border-color: #7a2518; +} .switch--item.selected { background-color: #7a2519; color: #ffffff; +} function addBlockSwitches() { for (var primary of document.querySelectorAll('.primary')) { var switchItem = createSwitchItem(primary, createBlockSwitch(primary)); switchItem.item.classList.add("selected"); var title = primary.querySelector('.title') title.remove(); } for (var secondary of document.querySelectorAll('.secondary')) { var primary = findPrimary(secondary); if (primary === null) { console.error("Found secondary block with no primary sibling"); } else { var switchItem = createSwitchItem(secondary, primary.querySelector('.switch')); switchItem.content.classList.add("hidden"); primary.append(switchItem.content); secondary.remove(); } } +} function createElementFromHtml(html) { var template = document.createElement('template'); template.innerHTML = html; return template.content.firstChild; +} function createBlockSwitch(primary) { var blockSwitch = createElementFromHtml('\
\'); primary.prepend(blockSwitch) return blockSwitch; +} function findPrimary(secondary) { var candidate = secondary.previousElementSibling; while (candidate != null && !candidate.classList.contains('primary')) { candidate = candidate.previousElementSibling; } return candidate; +} function createSwitchItem(block, blockSwitch) { var blockName = block.querySelector('.title').textContent; var content = block.querySelectorAll('.content').item(0); var colist = nextSibling(block, '.colist'); if (colist != null) { content.append(colist); } var item = createElementFromHtml('\
' + blockName + '\'); item.dataset.blockName = blockName; content.dataset.blockName = blockName; blockSwitch.append(item); return {'item': item, 'content': content}; +} function nextSibling(element, selector) { var sibling = element.nextElementSibling; while (sibling) { if (sibling.matches(selector)) { return sibling; } sibling = sibling.nextElementSibling; } +} function globalSwitch() { document.querySelectorAll(".switch--item").forEach(function(item) { var blockId = blockIdForSwitchItem(item); var handler = function(event) { selectedText = event.target.textContent; window.localStorage.setItem(blockId, selectedText); for (var switchItem of document.querySelectorAll(".switch--item")) { if (blockIdForSwitchItem(switchItem) === blockId && switchItem.textContent === selectedText) { select(switchItem); } } } item.addEventListener("click", handler); if (item.textContent === window.localStorage.getItem(blockId)) { select(item); } }); +} function select(selected) { for (var child of selected.parentNode.children) { child.classList.remove("selected"); } selected.classList.add("selected"); for (var child of selected.parentNode.parentNode.children) { if (child.classList.contains("content")) { if (selected.dataset.blockName === child.dataset.blockName) { child.classList.remove("hidden"); } else { child.classList.add("hidden"); } } } } function blockIdForSwitchItem(item) { idComponents = [] for (var switchItem of item.parentNode.querySelectorAll(".switch--item")) { idComponents.push(switchItem.textContent.toLowerCase()); } return idComponents.sort().join("-") +} window.onload = function() { addBlockSwitches(); globalSwitch(); +}; + +# Cloud Native Applications + +Table of Contents + +* [1. Spring Cloud Context: Application Context Services](#spring-cloud-context-application-context-services) + * [1.1. The Bootstrap Application Context](#the-bootstrap-application-context) + * [1.2. Application Context Hierarchies](#application-context-hierarchies) + * [1.3. Changing the Location of Bootstrap Properties](#customizing-bootstrap-properties) + * [1.4. Overriding the Values of Remote Properties](#overriding-bootstrap-properties) + * [1.5. Customizing the Bootstrap Configuration](#customizing-the-bootstrap-configuration) + * [1.6. Customizing the Bootstrap Property Sources](#customizing-bootstrap-property-sources) + * [1.7. Logging Configuration](#logging-configuration) + * [1.8. Environment Changes](#environment-changes) + * [1.9. Refresh Scope](#refresh-scope) + * [1.10. Encryption and Decryption](#encryption-and-decryption) + * [1.11. Endpoints](#endpoints) + +* [2. Spring Cloud Commons: Common Abstractions](#spring-cloud-commons-common-abstractions) + * [2.1. The `@EnableDiscoveryClient` Annotation](#discovery-client) + * [2.1.1. Health Indicators](#health-indicators) + * [DiscoveryClientHealthIndicator](#discoveryclienthealthindicator) + * [DiscoveryCompositeHealthContributor](#discoverycompositehealthcontributor) + + * [2.1.2. Ordering `DiscoveryClient` instances](#ordering-discoveryclient-instances) + * [2.1.3. SimpleDiscoveryClient](#simplediscoveryclient) + + * [2.2. ServiceRegistry](#serviceregistry) + * [2.2.1. ServiceRegistry Auto-Registration](#serviceregistry-auto-registration) + * [ServiceRegistry Auto-Registration Events](#serviceregistry-auto-registration-events) + + * [2.2.2. Service Registry Actuator Endpoint](#service-registry-actuator-endpoint) + + * [2.3. Spring RestTemplate as a Load Balancer Client](#rest-template-loadbalancer-client) + * [2.4. Spring WebClient as a Load Balancer Client](#webclinet-loadbalancer-client) + * [2.4.1. Retrying Failed Requests](#retrying-failed-requests) + + * [2.5. Multiple `RestTemplate` Objects](#multiple-resttemplate-objects) + * [2.6. Multiple WebClient Objects](#multiple-webclient-objects) + * [2.7. Spring WebFlux `WebClient` as a Load Balancer Client](#loadbalanced-webclient) + * [2.7.1. Spring WebFlux `WebClient` with `ReactorLoadBalancerExchangeFilterFunction`](#webflux-with-reactive-loadbalancer) + * [2.7.2. Spring WebFlux `WebClient` with a Non-reactive Load Balancer Client](#load-balancer-exchange-filter-function) + + * [2.8. Ignore Network Interfaces](#ignore-network-interfaces) + * [2.9. HTTP Client Factories](#http-clients) + * [2.10. Enabled Features](#enabled-features) + * [2.10.1. Feature types](#feature-types) + * [2.10.2. Declaring features](#declaring-features) + + * [2.11. Spring Cloud Compatibility Verification](#spring-cloud-compatibility-verification) + +* [3. Spring Cloud LoadBalancer](#spring-cloud-loadbalancer) + * [3.1. Switching between the load-balancing algorithms](#switching-between-the-load-balancing-algorithms) + * [3.2. Spring Cloud LoadBalancer integrations](#spring-cloud-loadbalancer-integrations) + * [3.3. Spring Cloud LoadBalancer Caching](#loadbalancer-caching) + * [3.3.1. Caffeine-backed LoadBalancer Cache Implementation](#caffeine-backed-loadbalancer-cache-implementation) + * [3.3.2. Default LoadBalancer Cache Implementation](#default-loadbalancer-cache-implementation) + * [3.3.3. LoadBalancer Cache Configuration](#loadbalancer-cache-configuration) + + * [3.4. Zone-Based Load-Balancing](#zone-based-load-balancing) + * [3.5. Instance Health-Check for LoadBalancer](#instance-health-check-for-loadbalancer) + * [3.6. Same instance preference for LoadBalancer](#same-instance-preference-for-loadbalancer) + * [3.7. Request-based Sticky Session for LoadBalancer](#request-based-sticky-session-for-loadbalancer) + * [3.8. Spring Cloud LoadBalancer Hints](#spring-cloud-loadbalancer-hints) + * [3.9. Hint-Based Load-Balancing](#hints-based-loadbalancing) + * [3.10. Transform the load-balanced HTTP request](#transform-the-load-balanced-http-request) + * [3.11. Spring Cloud LoadBalancer Starter](#spring-cloud-loadbalancer-starter) + * [3.12. Passing Your Own Spring Cloud LoadBalancer Configuration](#custom-loadbalancer-configuration) + * [3.13. Spring Cloud LoadBalancer Lifecycle](#loadbalancer-lifecycle) + * [3.14. Spring Cloud LoadBalancer Statistics](#loadbalancer-micrometer-stats-lifecycle) + * [3.15. Configuring Individual LoadBalancerClients](#configuring-individual-loadbalancerclients) + +* [4. Spring Cloud Circuit Breaker](#spring-cloud-circuit-breaker) + * [4.1. Introduction](#introduction) + * [4.1.1. Supported Implementations](#supported-implementations) + + * [4.2. Core Concepts](#core-concepts) + * [4.2.1. Circuit Breakers In Reactive Code](#circuit-breakers-in-reactive-code) + + * [4.3. Configuration](#configuration) + +* [5. CachedRandomPropertySource](#cachedrandompropertysource) +* [6. Security](#spring-cloud-security) + * [6.1. Single Sign On](#spring-cloud-security-single-sign-on) + * [6.1.1. Client Token Relay](#spring-cloud-security-client-token-relay) + * [6.1.2. Resource Server Token Relay](#spring-cloud-security-resource-server-token-relay) + +* [7. Configuration Properties](#configuration-properties) [Cloud Native](https://pivotal.io/platform-as-a-service/migrating-to-cloud-native-application-architectures-ebook) is a style of application development that encourages easy adoption of best practices in the areas of continuous delivery and value-driven development. A related discipline is that of building [12-factor Applications](https://12factor.net/), in which development practices are aligned with delivery and operations goals — for instance, by using declarative programming and management and monitoring. @@ -23,14 +124,13 @@ Extract the files into the JDK/jre/lib/security folder for whichever version of | |Spring Cloud is released under the non-restrictive Apache 2.0 license.
If you would like to contribute to this section of the documentation or if you find an error, you can find the source code and issue trackers for the project at {docslink}[github].| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -[](#spring-cloud-context-application-context-services)[1. Spring Cloud Context: Application Context Services](#spring-cloud-context-application-context-services) ----------- +## [](#spring-cloud-context-application-context-services)[1. Spring Cloud Context: Application Context Services](#spring-cloud-context-application-context-services) Spring Boot has an opinionated view of how to build an application with Spring. For instance, it has conventional locations for common configuration files and has endpoints for common management and monitoring tasks. Spring Cloud builds on top of that and adds a few features that many components in a system would use or occasionally need. -### [](#the-bootstrap-application-context)[1.1. The Bootstrap Application Context](#the-bootstrap-application-context) ### +### [](#the-bootstrap-application-context)[1.1. The Bootstrap Application Context](#the-bootstrap-application-context) A Spring Cloud application operates by creating a “bootstrap” context, which is a parent context for the main application. This context is responsible for loading configuration properties from the external sources and for decrypting properties in the local external configuration files. @@ -59,7 +159,7 @@ If you want to retrieve specific profile configuration, you should also set `spr You can disable the bootstrap process completely by setting `spring.cloud.bootstrap.enabled=false` (for example, in system properties). -### [](#application-context-hierarchies)[1.2. Application Context Hierarchies](#application-context-hierarchies) ### +### [](#application-context-hierarchies)[1.2. Application Context Hierarchies](#application-context-hierarchies) If you build an application context from `SpringApplication` or `SpringApplicationBuilder`, the Bootstrap context is added as a parent to that context. It is a feature of Spring that child contexts inherit property sources and profiles from their parent, so the “main” application context contains additional property sources, compared to building the same context without Spring Cloud Config. @@ -88,7 +188,7 @@ the parent, by name and also by property source name. Note that the `SpringApplicationBuilder` lets you share an `Environment` amongst the whole hierarchy, but that is not the default. Thus, sibling contexts (in particular) do not need to have the same profiles or property sources, even though they may share common values with their parent. -### [](#customizing-bootstrap-properties)[1.3. Changing the Location of Bootstrap Properties](#customizing-bootstrap-properties) ### +### [](#customizing-bootstrap-properties)[1.3. Changing the Location of Bootstrap Properties](#customizing-bootstrap-properties) The `bootstrap.yml` (or `.properties`) location can be specified by setting `spring.cloud.bootstrap.name` (default: `bootstrap`), `spring.cloud.bootstrap.location` (default: empty) or `spring.cloud.bootstrap.additional-location` (default: empty) — for example, in System properties. @@ -98,7 +198,7 @@ To add locations to the list of default ones, `spring.cloud.bootstrap.additional In fact, they are used to set up the bootstrap `ApplicationContext` by setting those properties in its `Environment`. If there is an active profile (from `spring.profiles.active` or through the `Environment` API in the context you are building), properties in that profile get loaded as well, the same as in a regular Spring Boot app — for example, from `bootstrap-development.properties` for a `development` profile. -### [](#overriding-bootstrap-properties)[1.4. Overriding the Values of Remote Properties](#overriding-bootstrap-properties) ### +### [](#overriding-bootstrap-properties)[1.4. Overriding the Values of Remote Properties](#overriding-bootstrap-properties) The property sources that are added to your application by the bootstrap context are often “remote” (from example, from Spring Cloud Config Server). By default, they cannot be overridden locally. @@ -109,7 +209,7 @@ Once that flag is set, two finer-grained settings control the location of the re * `spring.cloud.config.overrideSystemProperties=false`: Only system properties, command line arguments, and environment variables (but not the local config files) should override the remote settings. -### [](#customizing-the-bootstrap-configuration)[1.5. Customizing the Bootstrap Configuration](#customizing-the-bootstrap-configuration) ### +### [](#customizing-the-bootstrap-configuration)[1.5. Customizing the Bootstrap Configuration](#customizing-the-bootstrap-configuration) The bootstrap context can be set to do anything you like by adding entries to `/META-INF/spring.factories` under a key named `org.springframework.cloud.bootstrap.BootstrapConfiguration`. This holds a comma-separated list of Spring `@Configuration` classes that are used to create the context. @@ -124,7 +224,7 @@ The bootstrap process ends by injecting initializers into the main `SpringApplic First, a bootstrap context is created from the classes found in `spring.factories`. Then, all `@Beans` of type `ApplicationContextInitializer` are added to the main `SpringApplication` before it is started. -### [](#customizing-bootstrap-property-sources)[1.6. Customizing the Bootstrap Property Sources](#customizing-bootstrap-property-sources) ### +### [](#customizing-bootstrap-property-sources)[1.6. Customizing the Bootstrap Property Sources](#customizing-bootstrap-property-sources) The default property source for external configuration added by the bootstrap process is the Spring Cloud Config Server, but you can add additional sources by adding beans of type `PropertySourceLocator` to the bootstrap context (through `spring.factories`). For instance, you can insert additional properties from a different server or from a database. @@ -153,14 +253,14 @@ If you create a jar with this class in it and then add a `META-INF/spring.factor org.springframework.cloud.bootstrap.BootstrapConfiguration=sample.custom.CustomPropertySourceLocator ``` -### [](#logging-configuration)[1.7. Logging Configuration](#logging-configuration) ### +### [](#logging-configuration)[1.7. Logging Configuration](#logging-configuration) If you use Spring Boot to configure log settings, you should place this configuration in `bootstrap.[yml | properties]` if you would like it to apply to all events. | |For Spring Cloud to initialize logging configuration properly, you cannot use a custom prefix.
For example, using `custom.loggin.logpath` is not recognized by Spring Cloud when initializing the logging system.| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#environment-changes)[1.8. Environment Changes](#environment-changes) ### +### [](#environment-changes)[1.8. Environment Changes](#environment-changes) The application listens for an `EnvironmentChangeEvent` and reacts to the change in a couple of standard ways (additional `ApplicationListeners` can be added as `@Beans` in the normal way). When an `EnvironmentChangeEvent` is observed, it has a list of key values that have changed, and the application uses those to: @@ -180,7 +280,7 @@ For instance, a `DataSource` can have its `maxPoolSize` changed at runtime (the Re-binding `@ConfigurationProperties` does not cover another large class of use cases, where you need more control over the refresh and where you need a change to be atomic over the whole `ApplicationContext`. To address those concerns, we have `@RefreshScope`. -### [](#refresh-scope)[1.9. Refresh Scope](#refresh-scope) ### +### [](#refresh-scope)[1.9. Refresh Scope](#refresh-scope) When there is a configuration change, a Spring `@Bean` that is marked as `@RefreshScope` gets special treatment. This feature addresses the problem of stateful beans that get their configuration injected only when they are initialized. @@ -213,7 +313,7 @@ management: | |`@RefreshScope` works (technically) on a `@Configuration` class, but it might lead to surprising behavior.
For example, it does not mean that all the `@Beans` defined in that class are themselves in `@RefreshScope`.
Specifically, anything that depends on those beans cannot rely on them being updated when a refresh is initiated, unless it is itself in `@RefreshScope`.
In that case, it is rebuilt on a refresh and its dependencies are re-injected.
At that point, they are re-initialized from the refreshed `@Configuration`).| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#encryption-and-decryption)[1.10. Encryption and Decryption](#encryption-and-decryption) ### +### [](#encryption-and-decryption)[1.10. Encryption and Decryption](#encryption-and-decryption) Spring Cloud has an `Environment` pre-processor for decrypting property values locally. It follows the same rules as the Spring Cloud Config Server and has the same external configuration through `encrypt.*`. @@ -231,7 +331,7 @@ See the following links for more information: Extract the files into the JDK/jre/lib/security folder for whichever version of JRE/JDK x64/x86 you use. -### [](#endpoints)[1.11. Endpoints](#endpoints) ### +### [](#endpoints)[1.11. Endpoints](#endpoints) For a Spring Boot Actuator application, some additional management endpoints are available. You can use: @@ -247,12 +347,11 @@ For a Spring Boot Actuator application, some additional management endpoints are | |If you disable the `/actuator/restart` endpoint then the `/actuator/pause` and `/actuator/resume` endpoints
will also be disabled since they are just a special case of `/actuator/restart`.| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -[](#spring-cloud-commons-common-abstractions)[2. Spring Cloud Commons: Common Abstractions](#spring-cloud-commons-common-abstractions) ----------- +## [](#spring-cloud-commons-common-abstractions)[2. Spring Cloud Commons: Common Abstractions](#spring-cloud-commons-common-abstractions) Patterns such as service discovery, load balancing, and circuit breakers lend themselves to a common abstraction layer that can be consumed by all Spring Cloud clients, independent of the implementation (for example, discovery with Eureka or Consul). -### [](#discovery-client)[2.1. The `@EnableDiscoveryClient` Annotation](#discovery-client) ### +### [](#discovery-client)[2.1. The `@EnableDiscoveryClient` Annotation](#discovery-client) Spring Cloud Commons provides the `@EnableDiscoveryClient` annotation. This looks for implementations of the `DiscoveryClient` and `ReactiveDiscoveryClient` interfaces with `META-INF/spring.factories`. @@ -269,11 +368,11 @@ This behavior can be disabled by setting `autoRegister=false` in `@EnableDiscove | |`@EnableDiscoveryClient` is no longer required.
You can put a `DiscoveryClient` implementation on the classpath to cause the Spring Boot application to register with the service discovery server.| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -#### [](#health-indicators)[2.1.1. Health Indicators](#health-indicators) #### +#### [](#health-indicators)[2.1.1. Health Indicators](#health-indicators) Commons auto-configures the following Spring Boot health indicators. -##### [](#discoveryclienthealthindicator)[DiscoveryClientHealthIndicator](#discoveryclienthealthindicator) ##### +##### [](#discoveryclienthealthindicator)[DiscoveryClientHealthIndicator](#discoveryclienthealthindicator) This health indicator is based on the currently registered `DiscoveryClient` implementation. @@ -286,12 +385,12 @@ This health indicator is based on the currently registered `DiscoveryClient` imp By default, the indicator invokes the client’s `getServices` method. In deployments with many registered services it may too costly to retrieve all services during every check. This will skip the service retrieval and instead use the client’s `probe` method. -##### [](#discoverycompositehealthcontributor)[DiscoveryCompositeHealthContributor](#discoverycompositehealthcontributor) ##### +##### [](#discoverycompositehealthcontributor)[DiscoveryCompositeHealthContributor](#discoverycompositehealthcontributor) This composite health indicator is based on all registered `DiscoveryHealthIndicator` beans. To disable, set `spring.cloud.discovery.client.composite-indicator.enabled=false`. -#### [](#ordering-discoveryclient-instances)[2.1.2. Ordering `DiscoveryClient` instances](#ordering-discoveryclient-instances) #### +#### [](#ordering-discoveryclient-instances)[2.1.2. Ordering `DiscoveryClient` instances](#ordering-discoveryclient-instances) `DiscoveryClient` interface extends `Ordered`. This is useful when using multiple discovery clients, as it allows you to define the order of the returned discovery clients, similar to @@ -299,7 +398,7 @@ how you can order the beans loaded by a Spring application. By default, the orde the `getOrder()` method so that it returns the value that is suitable for your setup. Apart from this, you can use properties to set the order of the `DiscoveryClient`implementations provided by Spring Cloud, among others `ConsulDiscoveryClient`, `EurekaDiscoveryClient` and`ZookeeperDiscoveryClient`. In order to do it, you just need to set the`spring.cloud.{clientIdentifier}.discovery.order` (or `eureka.client.order` for Eureka) property to the desired value. -#### [](#simplediscoveryclient)[2.1.3. SimpleDiscoveryClient](#simplediscoveryclient) #### +#### [](#simplediscoveryclient)[2.1.3. SimpleDiscoveryClient](#simplediscoveryclient) If there is no Service-Registry-backed `DiscoveryClient` in the classpath, `SimpleDiscoveryClient`instance, that uses properties to get information on service and instances, will be used. @@ -308,7 +407,7 @@ for the ID of the service in question, while `[0]` indicates the index number of (as visible in the example, indexes start with `0`), and then the value of `uri` is the actual URI under which the instance is available. -### [](#serviceregistry)[2.2. ServiceRegistry](#serviceregistry) ### +### [](#serviceregistry)[2.2. ServiceRegistry](#serviceregistry) Commons now provides a `ServiceRegistry` interface that provides methods such as `register(Registration)` and `deregister(Registration)`, which let you provide custom registered services.`Registration` is a marker interface. @@ -344,14 +443,14 @@ If you are using the `ServiceRegistry` interface, you are going to need to pass correct `Registry` implementation for the `ServiceRegistry` implementation you are using. -#### [](#serviceregistry-auto-registration)[2.2.1. ServiceRegistry Auto-Registration](#serviceregistry-auto-registration) #### +#### [](#serviceregistry-auto-registration)[2.2.1. ServiceRegistry Auto-Registration](#serviceregistry-auto-registration) By default, the `ServiceRegistry` implementation auto-registers the running service. To disable that behavior, you can set: \* `@EnableDiscoveryClient(autoRegister=false)` to permanently disable auto-registration. \* `spring.cloud.service-registry.auto-registration.enabled=false` to disable the behavior through configuration. -##### [](#serviceregistry-auto-registration-events)[ServiceRegistry Auto-Registration Events](#serviceregistry-auto-registration-events) ##### +##### [](#serviceregistry-auto-registration-events)[ServiceRegistry Auto-Registration Events](#serviceregistry-auto-registration-events) There are two events that will be fired when a service auto-registers. The first event, called`InstancePreRegisteredEvent`, is fired before the service is registered. The second event, called `InstanceRegisteredEvent`, is fired after the service is registered. You can register an`ApplicationListener`(s) to listen to and react to these events. @@ -359,7 +458,7 @@ event, called `InstanceRegisteredEvent`, is fired after the service is registere | |These events will not be fired if the `spring.cloud.service-registry.auto-registration.enabled` property is set to `false`.| |---|---------------------------------------------------------------------------------------------------------------------------| -#### [](#service-registry-actuator-endpoint)[2.2.2. Service Registry Actuator Endpoint](#service-registry-actuator-endpoint) #### +#### [](#service-registry-actuator-endpoint)[2.2.2. Service Registry Actuator Endpoint](#service-registry-actuator-endpoint) Spring Cloud Commons provides a `/service-registry` actuator endpoint. This endpoint relies on a `Registration` bean in the Spring Application Context. @@ -369,7 +468,7 @@ The JSON body has to include the `status` field with the preferred value. Please see the documentation of the `ServiceRegistry` implementation you use for the allowed values when updating the status and the values returned for the status. For instance, Eureka’s supported statuses are `UP`, `DOWN`, `OUT_OF_SERVICE`, and `UNKNOWN`. -### [](#rest-template-loadbalancer-client)[2.3. Spring RestTemplate as a Load Balancer Client](#rest-template-loadbalancer-client) ### +### [](#rest-template-loadbalancer-client)[2.3. Spring RestTemplate as a Load Balancer Client](#rest-template-loadbalancer-client) You can configure a `RestTemplate` to use a Load-balancer client. To create a load-balanced `RestTemplate`, create a `RestTemplate` `@Bean` and use the `@LoadBalanced` qualifier, as the following example shows: @@ -405,7 +504,7 @@ The BlockingLoadBalancerClient is used to create a full physical address. | |To use a load-balanced `RestTemplate`, you need to have a load-balancer implementation in your classpath.
Add [Spring Cloud LoadBalancer starter](#spring-cloud-loadbalancer-starter) to your project in order to use it.| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#webclinet-loadbalancer-client)[2.4. Spring WebClient as a Load Balancer Client](#webclinet-loadbalancer-client) ### +### [](#webclinet-loadbalancer-client)[2.4. Spring WebClient as a Load Balancer Client](#webclinet-loadbalancer-client) You can configure `WebClient` to automatically use a load-balancer client. To create a load-balanced `WebClient`, create a `WebClient.Builder` `@Bean` and use the `@LoadBalanced` qualifier, as follows: @@ -438,7 +537,7 @@ The Spring Cloud LoadBalancer is used to create a full physical address. | |If you want to use a `@LoadBalanced WebClient.Builder`, you need to have a load balancer
implementation in the classpath. We recommend that you add the[Spring Cloud LoadBalancer starter](#spring-cloud-loadbalancer-starter) to your project.
Then, `ReactiveLoadBalancer` is used underneath.| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -#### [](#retrying-failed-requests)[2.4.1. Retrying Failed Requests](#retrying-failed-requests) #### +#### [](#retrying-failed-requests)[2.4.1. Retrying Failed Requests](#retrying-failed-requests) A load-balanced `RestTemplate` can be configured to retry failed requests. By default, this logic is disabled. @@ -521,7 +620,7 @@ public class MyConfiguration { } ``` -### [](#multiple-resttemplate-objects)[2.5. Multiple `RestTemplate` Objects](#multiple-resttemplate-objects) ### +### [](#multiple-resttemplate-objects)[2.5. Multiple `RestTemplate` Objects](#multiple-resttemplate-objects) If you want a `RestTemplate` that is not load-balanced, create a `RestTemplate` bean and inject it. To access the load-balanced `RestTemplate`, use the `@LoadBalanced` qualifier when you create your `@Bean`, as the following example shows: @@ -567,7 +666,7 @@ private RestTemplate restTemplate; | |If you see errors such as `java.lang.IllegalArgumentException: Can not set org.springframework.web.client.RestTemplate field com.my.app.Foo.restTemplate to com.sun.proxy.$Proxy89`, try injecting `RestOperations` or setting `spring.aop.proxyTargetClass=true`.| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#multiple-webclient-objects)[2.6. Multiple WebClient Objects](#multiple-webclient-objects) ### +### [](#multiple-webclient-objects)[2.6. Multiple WebClient Objects](#multiple-webclient-objects) If you want a `WebClient` that is not load-balanced, create a `WebClient` bean and inject it. To access the load-balanced `WebClient`, use the `@LoadBalanced` qualifier when you create your `@Bean`, as the following example shows: @@ -609,7 +708,7 @@ public class MyClass { } ``` -### [](#loadbalanced-webclient)[2.7. Spring WebFlux `WebClient` as a Load Balancer Client](#loadbalanced-webclient) ### +### [](#loadbalanced-webclient)[2.7. Spring WebFlux `WebClient` as a Load Balancer Client](#loadbalanced-webclient) The Spring WebFlux can work with both reactive and non-reactive `WebClient` configurations, as the topics describe: @@ -617,7 +716,7 @@ The Spring WebFlux can work with both reactive and non-reactive `WebClient` conf * [[load-balancer-exchange-filter-functionload-balancer-exchange-filter-function]](#load-balancer-exchange-filter-functionload-balancer-exchange-filter-function) -#### [](#webflux-with-reactive-loadbalancer)[2.7.1. Spring WebFlux `WebClient` with `ReactorLoadBalancerExchangeFilterFunction`](#webflux-with-reactive-loadbalancer) #### +#### [](#webflux-with-reactive-loadbalancer)[2.7.1. Spring WebFlux `WebClient` with `ReactorLoadBalancerExchangeFilterFunction`](#webflux-with-reactive-loadbalancer) You can configure `WebClient` to use the `ReactiveLoadBalancer`. If you add [Spring Cloud LoadBalancer starter](#spring-cloud-loadbalancer-starter) to your project @@ -644,7 +743,7 @@ public class MyClass { The URI needs to use a virtual host name (that is, a service name, not a host name). The `ReactorLoadBalancer` is used to create a full physical address. -#### [](#load-balancer-exchange-filter-function)[2.7.2. Spring WebFlux `WebClient` with a Non-reactive Load Balancer Client](#load-balancer-exchange-filter-function) #### +#### [](#load-balancer-exchange-filter-function)[2.7.2. Spring WebFlux `WebClient` with a Non-reactive Load Balancer Client](#load-balancer-exchange-filter-function) If `spring-webflux` is on the classpath, `LoadBalancerExchangeFilterFunction`is auto-configured. Note, however, that this uses a non-reactive client under the hood. @@ -673,7 +772,7 @@ The `LoadBalancerClient` is used to create a full physical address. WARN: This approach is now deprecated. We suggest that you use [WebFlux with reactive Load-Balancer](#webflux-with-reactive-loadbalancer)instead. -### [](#ignore-network-interfaces)[2.8. Ignore Network Interfaces](#ignore-network-interfaces) ### +### [](#ignore-network-interfaces)[2.8. Ignore Network Interfaces](#ignore-network-interfaces) Sometimes, it is useful to ignore certain named network interfaces so that they can be excluded from Service Discovery registration (for example, when running in a Docker container). A list of regular expressions can be set to cause the desired network interfaces to be ignored. @@ -716,7 +815,7 @@ spring: See [Inet4Address.html.isSiteLocalAddress()](https://docs.oracle.com/javase/8/docs/api/java/net/Inet4Address.html#isSiteLocalAddress--) for more details about what constitutes a site-local address. -### [](#http-clients)[2.9. HTTP Client Factories](#http-clients) ### +### [](#http-clients)[2.9. HTTP Client Factories](#http-clients) Spring Cloud Commons provides beans for creating both Apache HTTP clients (`ApacheHttpClientFactory`) and OK HTTP clients (`OkHttpClientFactory`). The `OkHttpClientFactory` bean is created only if the OK HTTP jar is on the classpath. @@ -725,13 +824,13 @@ If you would like to customize how the HTTP clients are created in downstream pr In addition, if you provide a bean of type `HttpClientBuilder` or `OkHttpClient.Builder`, the default factories use these builders as the basis for the builders returned to downstream projects. You can also disable the creation of these beans by setting `spring.cloud.httpclientfactories.apache.enabled` or `spring.cloud.httpclientfactories.ok.enabled` to `false`. -### [](#enabled-features)[2.10. Enabled Features](#enabled-features) ### +### [](#enabled-features)[2.10. Enabled Features](#enabled-features) Spring Cloud Commons provides a `/features` actuator endpoint. This endpoint returns features available on the classpath and whether they are enabled. The information returned includes the feature type, name, version, and vendor. -#### [](#feature-types)[2.10.1. Feature types](#feature-types) #### +#### [](#feature-types)[2.10.1. Feature types](#feature-types) There are two types of 'features': abstract and named. @@ -741,7 +840,7 @@ The version displayed is `bean.getClass().getPackage().getImplementationVersion( Named features are features that do not have a particular class they implement. These features include “Circuit Breaker”, “API Gateway”, “Spring Cloud Bus”, and others. These features require a name and a bean type. -#### [](#declaring-features)[2.10.2. Declaring features](#declaring-features) #### +#### [](#declaring-features)[2.10.2. Declaring features](#declaring-features) Any module can declare any number of `HasFeature` beans, as the following examples show: @@ -770,7 +869,7 @@ HasFeatures localFeatures() { Each of these beans should go in an appropriately guarded `@Configuration`. -### [](#spring-cloud-compatibility-verification)[2.11. Spring Cloud Compatibility Verification](#spring-cloud-compatibility-verification) ### +### [](#spring-cloud-compatibility-verification)[2.11. Spring Cloud Compatibility Verification](#spring-cloud-compatibility-verification) Due to the fact that some users have problem with setting up Spring Cloud application, we’ve decided to add a compatibility verification mechanism. It will break if your current setup is not compatible @@ -804,8 +903,7 @@ In order to disable this feature, set `spring.cloud.compatibility-verifier.enabl If you want to override the compatible Spring Boot versions, just set the`spring.cloud.compatibility-verifier.compatible-boot-versions` property with a comma separated list of compatible Spring Boot versions. -[](#spring-cloud-loadbalancer)[3. Spring Cloud LoadBalancer](#spring-cloud-loadbalancer) ----------- +## [](#spring-cloud-loadbalancer)[3. Spring Cloud LoadBalancer](#spring-cloud-loadbalancer) Spring Cloud provides its own client-side load-balancer abstraction and implementation. For the load-balancing mechanism, `ReactiveLoadBalancer` interface has been added and a **Round-Robin-based** and **Random** implementations @@ -814,7 +912,7 @@ have been provided for it. In order to get instances to select from reactive `Se | |It is possible to disable Spring Cloud LoadBalancer by setting the value of `spring.cloud.loadbalancer.enabled` to `false`.| |---|---------------------------------------------------------------------------------------------------------------------------| -### [](#switching-between-the-load-balancing-algorithms)[3.1. Switching between the load-balancing algorithms](#switching-between-the-load-balancing-algorithms) ### +### [](#switching-between-the-load-balancing-algorithms)[3.1. Switching between the load-balancing algorithms](#switching-between-the-load-balancing-algorithms) The `ReactiveLoadBalancer` implementation that is used by default is `RoundRobinLoadBalancer`. To switch to a different implementation, either for selected services or all of them, you can use the [custom LoadBalancer configurations mechanism](#custom-loadbalancer-configuration). @@ -837,7 +935,7 @@ public class CustomLoadBalancerConfiguration { | |The classes you pass as `@LoadBalancerClient` or `@LoadBalancerClients` configuration arguments should either not be annotated with `@Configuration` or be outside component scan scope.| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#spring-cloud-loadbalancer-integrations)[3.2. Spring Cloud LoadBalancer integrations](#spring-cloud-loadbalancer-integrations) ### +### [](#spring-cloud-loadbalancer-integrations)[3.2. Spring Cloud LoadBalancer integrations](#spring-cloud-loadbalancer-integrations) In order to make it easy to use Spring Cloud LoadBalancer, we provide `ReactorLoadBalancerExchangeFilterFunction` that can be used with `WebClient` and `BlockingLoadBalancerClient` that works with `RestTemplate`. You can see more information and examples of usage in the following sections: @@ -848,11 +946,11 @@ You can see more information and examples of usage in the following sections: * [Spring WebFlux WebClient with `ReactorLoadBalancerExchangeFilterFunction`](#webflux-with-reactive-loadbalancer) -### [](#loadbalancer-caching)[3.3. Spring Cloud LoadBalancer Caching](#loadbalancer-caching) ### +### [](#loadbalancer-caching)[3.3. Spring Cloud LoadBalancer Caching](#loadbalancer-caching) Apart from the basic `ServiceInstanceListSupplier` implementation that retrieves instances via `DiscoveryClient` each time it has to choose an instance, we provide two caching implementations. -#### [](#caffeine-backed-loadbalancer-cache-implementation)[3.3.1. ](#caffeine-backed-loadbalancer-cache-implementation)[Caffeine](https://github.com/ben-manes/caffeine)-backed LoadBalancer Cache Implementation #### +#### [](#caffeine-backed-loadbalancer-cache-implementation)[3.3.1. ](#caffeine-backed-loadbalancer-cache-implementation)[Caffeine](https://github.com/ben-manes/caffeine)-backed LoadBalancer Cache Implementation If you have `com.github.ben-manes.caffeine:caffeine` in the classpath, Caffeine-based implementation will be used. See the [LoadBalancerCacheConfiguration](#loadbalancer-cache-configuration) section for information on how to configure it. @@ -861,7 +959,7 @@ If you are using Caffeine, you can also override the default Caffeine Cache setu WARN: Passing your own Caffeine specification will override any other LoadBalancerCache settings, including [General LoadBalancer Cache Configuration](#loadbalancer-cache-configuration) fields, such as `ttl` and `capacity`. -#### [](#default-loadbalancer-cache-implementation)[3.3.2. Default LoadBalancer Cache Implementation](#default-loadbalancer-cache-implementation) #### +#### [](#default-loadbalancer-cache-implementation)[3.3.2. Default LoadBalancer Cache Implementation](#default-loadbalancer-cache-implementation) If you do not have Caffeine in the classpath, the `DefaultLoadBalancerCache`, which comes automatically with `spring-cloud-starter-loadbalancer`, will be used. See the [LoadBalancerCacheConfiguration](#loadbalancer-cache-configuration) section for information on how to configure it. @@ -869,7 +967,7 @@ See the [LoadBalancerCacheConfiguration](#loadbalancer-cache-configuration) sect | |To use Caffeine instead of the default cache, add the `com.github.ben-manes.caffeine:caffeine` dependency to classpath.| |---|-----------------------------------------------------------------------------------------------------------------------| -#### [](#loadbalancer-cache-configuration)[3.3.3. LoadBalancer Cache Configuration](#loadbalancer-cache-configuration) #### +#### [](#loadbalancer-cache-configuration)[3.3.3. LoadBalancer Cache Configuration](#loadbalancer-cache-configuration) You can set your own `ttl` value (the time after write after which entries should be expired), expressed as `Duration`, by passing a `String` compliant with the [Spring Boot `String` to `Duration` converter syntax](https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config-conversion-duration). as the value of the `spring.cloud.loadbalancer.cache.ttl` property. @@ -882,7 +980,7 @@ You can also altogether disable loadBalancer caching by setting the value of `sp | |Although the basic, non-cached, implementation is useful for prototyping and testing, it’s much less efficient than the cached versions, so we recommend always using the cached version in production. If the caching is already done by the `DiscoveryClient` implementation, for example `EurekaDiscoveryClient`, the load-balancer caching should be disabled to prevent double caching.| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#zone-based-load-balancing)[3.4. Zone-Based Load-Balancing](#zone-based-load-balancing) ### +### [](#zone-based-load-balancing)[3.4. Zone-Based Load-Balancing](#zone-based-load-balancing) To enable zone-based load-balancing, we provide the `ZonePreferenceServiceInstanceListSupplier`. We use `DiscoveryClient`-specific `zone` configuration (for example, `eureka.instance.metadata-map.zone`) to pick the zone that the client tries to filter available service instances for. @@ -921,7 +1019,7 @@ public class CustomLoadBalancerConfiguration { } ``` -### [](#instance-health-check-for-loadbalancer)[3.5. Instance Health-Check for LoadBalancer](#instance-health-check-for-loadbalancer) ### +### [](#instance-health-check-for-loadbalancer)[3.5. Instance Health-Check for LoadBalancer](#instance-health-check-for-loadbalancer) It is possible to enable a scheduled HealthCheck for the LoadBalancer. The `HealthCheckServiceInstanceListSupplier`is provided for that. It regularly verifies if the instances provided by a delegate`ServiceInstanceListSupplier` are still alive and only returns the healthy instances, unless there are none - then it returns all the retrieved instances. @@ -971,7 +1069,7 @@ public class CustomLoadBalancerConfiguration { | |`HealthCheckServiceInstanceListSupplier` has its own caching mechanism based on Reactor Flux `replay()`. Therefore, if it’s being used, you may want to skip wrapping that supplier with `CachingServiceInstanceListSupplier`.| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#same-instance-preference-for-loadbalancer)[3.6. Same instance preference for LoadBalancer](#same-instance-preference-for-loadbalancer) ### +### [](#same-instance-preference-for-loadbalancer)[3.6. Same instance preference for LoadBalancer](#same-instance-preference-for-loadbalancer) You can set up the LoadBalancer in such a way that it prefers the instance that was previously selected, if that instance is available. @@ -994,7 +1092,7 @@ public class CustomLoadBalancerConfiguration { | |This is also a replacement for Zookeeper `StickyRule`.| |---|------------------------------------------------------| -### [](#request-based-sticky-session-for-loadbalancer)[3.7. Request-based Sticky Session for LoadBalancer](#request-based-sticky-session-for-loadbalancer) ### +### [](#request-based-sticky-session-for-loadbalancer)[3.7. Request-based Sticky Session for LoadBalancer](#request-based-sticky-session-for-loadbalancer) You can set up the LoadBalancer in such a way that it prefers the instance with `instanceId` provided in a request cookie. We currently support this if the request is being passed to the LoadBalancer through either `ClientRequestContext` or `ServerHttpRequestContext`, which are used by the SC LoadBalancer exchange filter functions and filters. @@ -1021,14 +1119,14 @@ By default, the name of the cookie is `sc-lb-instance-id`. You can modify it by | |This feature is currently supported for WebClient-backed load-balancing.| |---|------------------------------------------------------------------------| -### [](#spring-cloud-loadbalancer-hints)[3.8. Spring Cloud LoadBalancer Hints](#spring-cloud-loadbalancer-hints) ### +### [](#spring-cloud-loadbalancer-hints)[3.8. Spring Cloud LoadBalancer Hints](#spring-cloud-loadbalancer-hints) Spring Cloud LoadBalancer lets you set `String` hints that are passed to the LoadBalancer within the `Request` object and that can later be used in `ReactiveLoadBalancer` implementations that can handle them. You can set a default hint for all services by setting the value of the `spring.cloud.loadbalancer.hint.default` property. You can also set a specific value for any given service by setting the value of the `spring.cloud.loadbalancer.hint.[SERVICE_ID]` property, substituting `[SERVICE_ID]` with the correct ID of your service. If the hint is not set by the user, `default` is used. -### [](#hints-based-loadbalancing)[3.9. Hint-Based Load-Balancing](#hints-based-loadbalancing) ### +### [](#hints-based-loadbalancing)[3.9. Hint-Based Load-Balancing](#hints-based-loadbalancing) We also provide a `HintBasedServiceInstanceListSupplier`, which is a `ServiceInstanceListSupplier` implementation for hint-based instance selection. @@ -1057,7 +1155,7 @@ public class CustomLoadBalancerConfiguration { } ``` -### [](#transform-the-load-balanced-http-request)[3.10. Transform the load-balanced HTTP request](#transform-the-load-balanced-http-request) ### +### [](#transform-the-load-balanced-http-request)[3.10. Transform the load-balanced HTTP request](#transform-the-load-balanced-http-request) You can use the selected `ServiceInstance` to transform the load-balanced HTTP Request. @@ -1102,7 +1200,7 @@ public LoadBalancerClientRequestTransformer transformer() { If multiple transformers are defined, they are applied in the order in which Beans are defined. Alternatively, you can use `LoadBalancerRequestTransformer.DEFAULT_ORDER` or `LoadBalancerClientRequestTransformer.DEFAULT_ORDER` to specify the order. -### [](#spring-cloud-loadbalancer-starter)[3.11. Spring Cloud LoadBalancer Starter](#spring-cloud-loadbalancer-starter) ### +### [](#spring-cloud-loadbalancer-starter)[3.11. Spring Cloud LoadBalancer Starter](#spring-cloud-loadbalancer-starter) We also provide a starter that allows you to easily add Spring Cloud LoadBalancer in a Spring Boot app. In order to use it, just add `org.springframework.cloud:spring-cloud-starter-loadbalancer` to your Spring Cloud dependencies in your build file. @@ -1110,7 +1208,7 @@ In order to use it, just add `org.springframework.cloud:spring-cloud-starter-loa | |Spring Cloud LoadBalancer starter includes[Spring Boot Caching](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-caching.html)and [Evictor](https://github.com/stoyanr/Evictor).| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#custom-loadbalancer-configuration)[3.12. Passing Your Own Spring Cloud LoadBalancer Configuration](#custom-loadbalancer-configuration) ### +### [](#custom-loadbalancer-configuration)[3.12. Passing Your Own Spring Cloud LoadBalancer Configuration](#custom-loadbalancer-configuration) You can also use the `@LoadBalancerClient` annotation to pass your own load-balancer client configuration, passing the name of the load-balancer client and the configuration class, as follows: @@ -1160,7 +1258,7 @@ public class MyConfiguration { | |The classes you pass as `@LoadBalancerClient` or `@LoadBalancerClients` configuration arguments should either not be annotated with `@Configuration` or be outside component scan scope.| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#loadbalancer-lifecycle)[3.13. Spring Cloud LoadBalancer Lifecycle](#loadbalancer-lifecycle) ### +### [](#loadbalancer-lifecycle)[3.13. Spring Cloud LoadBalancer Lifecycle](#loadbalancer-lifecycle) One type of bean that it may be useful to register using [Custom LoadBalancer configuration](#custom-loadbalancer-configuration) is `LoadBalancerLifecycle`. @@ -1174,7 +1272,7 @@ Class serverTypeClass)` method can be used to determine whether the processor in | |In the preceding method calls, `RC` means `RequestContext` type, `RES` means client response type, and `T` means returned server type.| |---|--------------------------------------------------------------------------------------------------------------------------------------| -### [](#loadbalancer-micrometer-stats-lifecycle)[3.14. Spring Cloud LoadBalancer Statistics](#loadbalancer-micrometer-stats-lifecycle) ### +### [](#loadbalancer-micrometer-stats-lifecycle)[3.14. Spring Cloud LoadBalancer Statistics](#loadbalancer-micrometer-stats-lifecycle) We provide a `LoadBalancerLifecycle` bean called `MicrometerStatsLoadBalancerLifecycle`, which uses Micrometer to provide statistics for load-balanced calls. @@ -1202,7 +1300,7 @@ Additional information regarding the service instances, request data, and respon | |You can further configure the behavior of those metrics (for example, add [publishing percentiles and histograms](https://micrometer.io/docs/concepts#_histograms_and_percentiles)) by [adding `MeterFilters`](https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready-metrics-per-meter-properties).| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#configuring-individual-loadbalancerclients)[3.15. Configuring Individual LoadBalancerClients](#configuring-individual-loadbalancerclients) ### +### [](#configuring-individual-loadbalancerclients)[3.15. Configuring Individual LoadBalancerClients](#configuring-individual-loadbalancerclients) Individual Loadbalancer clients may be configured individually with a different prefix `spring.cloud.loadbalancer.clients..` **where `clientId` is the name of the loadbalancer. Default configuration values may be set in the `spring.cloud.loadbalancer.`** namespace and will be merged with the client specific values taking precedence @@ -1235,15 +1333,14 @@ The per-client configuration properties work for most of the properties, apart f | |For the properties where maps where already used, where you could specify a different value per-client without using the `clients` keyword (for example, `hints`, `health-check.path`), we have kept that behaviour in order to keep the library backwards compatible. It will be modified in the next major release.| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -[](#spring-cloud-circuit-breaker)[4. Spring Cloud Circuit Breaker](#spring-cloud-circuit-breaker) ----------- +## [](#spring-cloud-circuit-breaker)[4. Spring Cloud Circuit Breaker](#spring-cloud-circuit-breaker) -### [](#introduction)[4.1. Introduction](#introduction) ### +### [](#introduction)[4.1. Introduction](#introduction) Spring Cloud Circuit breaker provides an abstraction across different circuit breaker implementations. It provides a consistent API to use in your applications, letting you, the developer, choose the circuit breaker implementation that best fits your needs for your application. -#### [](#supported-implementations)[4.1.1. Supported Implementations](#supported-implementations) #### +#### [](#supported-implementations)[4.1.1. Supported Implementations](#supported-implementations) Spring Cloud supports the following circuit-breaker implementations: @@ -1253,7 +1350,7 @@ Spring Cloud supports the following circuit-breaker implementations: * [Spring Retry](https://github.com/spring-projects/spring-retry) -### [](#core-concepts)[4.2. Core Concepts](#core-concepts) ### +### [](#core-concepts)[4.2. Core Concepts](#core-concepts) To create a circuit breaker in your code, you can use the `CircuitBreakerFactory` API. When you include a Spring Cloud Circuit Breaker starter on your classpath, a bean that implements this API is automatically created for you. The following example shows a simple example of how to use this API: @@ -1283,7 +1380,7 @@ The `Function` is the fallback that is run if the circuit breaker is tripped. The function is passed the `Throwable` that caused the fallback to be triggered. You can optionally exclude the fallback if you do not want to provide one. -#### [](#circuit-breakers-in-reactive-code)[4.2.1. Circuit Breakers In Reactive Code](#circuit-breakers-in-reactive-code) #### +#### [](#circuit-breakers-in-reactive-code)[4.2.1. Circuit Breakers In Reactive Code](#circuit-breakers-in-reactive-code) If Project Reactor is on the class path, you can also use `ReactiveCircuitBreakerFactory` for your reactive code. The following example shows how to do so: @@ -1310,7 +1407,7 @@ The `ReactiveCircuitBreakerFactory.create` API creates an instance of a class ca The `run` method takes a `Mono` or a `Flux` and wraps it in a circuit breaker. You can optionally profile a fallback `Function`, which will be called if the circuit breaker is tripped and is passed the `Throwable`that caused the failure. -### [](#configuration)[4.3. Configuration](#configuration) ### +### [](#configuration)[4.3. Configuration](#configuration) You can configure your circuit breakers by creating beans of type `Customizer`. The `Customizer` interface has a single method (called `customize`) that takes the `Object` to customize. @@ -1337,8 +1434,7 @@ Customizer.once(circuitBreaker -> { }, CircuitBreaker::getName) ``` -[](#cachedrandompropertysource)[5. CachedRandomPropertySource](#cachedrandompropertysource) ----------- +## [](#cachedrandompropertysource)[5. CachedRandomPropertySource](#cachedrandompropertysource) Spring Cloud Context provides a `PropertySource` that caches random values based on a key. Outside of the caching functionality it works the same as Spring Boot’s [`RandomValuePropertySource`](https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/RandomValuePropertySource.java). @@ -1350,15 +1446,14 @@ be any type supported by Spring Boot’s `RandomValuePropertySource`. myrandom=${cachedrandom.appname.value} ``` -[](#spring-cloud-security)[6. Security](#spring-cloud-security) ----------- +## [](#spring-cloud-security)[6. Security](#spring-cloud-security) -### [](#spring-cloud-security-single-sign-on)[6.1. Single Sign On](#spring-cloud-security-single-sign-on) ### +### [](#spring-cloud-security-single-sign-on)[6.1. Single Sign On](#spring-cloud-security-single-sign-on) | |All of the OAuth2 SSO and resource server features moved to Spring Boot
in version 1.3. You can find documentation in the[Spring Boot user guide](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/).| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -#### [](#spring-cloud-security-client-token-relay)[6.1.1. Client Token Relay](#spring-cloud-security-client-token-relay) #### +#### [](#spring-cloud-security-client-token-relay)[6.1.1. Client Token Relay](#spring-cloud-security-client-token-relay) If your app is a user facing OAuth2 client (i.e. has declared`@EnableOAuth2Sso` or `@EnableOAuth2Client`) then it has an`OAuth2ClientContext` in request scope from Spring Boot. You can create your own `OAuth2RestTemplate` from this context and an @@ -1367,7 +1462,7 @@ always forward the access token downstream, also refreshing the access token automatically if it expires. (These are features of Spring Security and Spring Boot.) -#### [](#spring-cloud-security-resource-server-token-relay)[6.1.2. Resource Server Token Relay](#spring-cloud-security-resource-server-token-relay) #### +#### [](#spring-cloud-security-resource-server-token-relay)[6.1.2. Resource Server Token Relay](#spring-cloud-security-resource-server-token-relay) If your app has `@EnableResourceServer` you might want to relay the incoming token downstream to other services. If you use a`RestTemplate` to contact the downstream services then this is just a @@ -1425,8 +1520,8 @@ client that sent you the token), then you only need to create your own`OAuth2Con Feign clients will also pick up an interceptor that uses the`OAuth2ClientContext` if it is available, so they should also do a token relay anywhere where a `RestTemplate` would. -[](#configuration-properties)[7. Configuration Properties](#configuration-properties) ----------- +## [](#configuration-properties)[7. Configuration Properties](#configuration-properties) To see the list of all Spring Cloud Commons related configuration properties please check [the Appendix page](appendix.html). +if (window.parent == window) {(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1\*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga');ga('create', 'UA-2728886-23', 'auto', {'siteSpeedSampleRate': 100});ga('send', 'pageview');} \ No newline at end of file diff --git a/docs/en/spring-cloud/spring-cloud-config.md b/docs/en/spring-cloud/spring-cloud-config.md index a7380542bfe3a309060a6cd0ac3481eb79da936c..30bf2ab87a0fa3b0c09f529a3ae0f58346bd91b8 100644 --- a/docs/en/spring-cloud/spring-cloud-config.md +++ b/docs/en/spring-cloud/spring-cloud-config.md @@ -1,5 +1,58 @@ -Spring Cloud Config -========== +Spring Cloud Config.hidden { display: none; +} .switch { border-width: 1px 1px 0 1px; border-style: solid; border-color: #7a2518; display: inline-block; +} .switch--item { padding: 10px; background-color: #ffffff; color: #7a2518; display: inline-block; cursor: pointer; +} .switch--item:not(:first-child) { border-width: 0 0 0 1px; border-style: solid; border-color: #7a2518; +} .switch--item.selected { background-color: #7a2519; color: #ffffff; +} function addBlockSwitches() { for (var primary of document.querySelectorAll('.primary')) { var switchItem = createSwitchItem(primary, createBlockSwitch(primary)); switchItem.item.classList.add("selected"); var title = primary.querySelector('.title') title.remove(); } for (var secondary of document.querySelectorAll('.secondary')) { var primary = findPrimary(secondary); if (primary === null) { console.error("Found secondary block with no primary sibling"); } else { var switchItem = createSwitchItem(secondary, primary.querySelector('.switch')); switchItem.content.classList.add("hidden"); primary.append(switchItem.content); secondary.remove(); } } +} function createElementFromHtml(html) { var template = document.createElement('template'); template.innerHTML = html; return template.content.firstChild; +} function createBlockSwitch(primary) { var blockSwitch = createElementFromHtml('\
\'); primary.prepend(blockSwitch) return blockSwitch; +} function findPrimary(secondary) { var candidate = secondary.previousElementSibling; while (candidate != null && !candidate.classList.contains('primary')) { candidate = candidate.previousElementSibling; } return candidate; +} function createSwitchItem(block, blockSwitch) { var blockName = block.querySelector('.title').textContent; var content = block.querySelectorAll('.content').item(0); var colist = nextSibling(block, '.colist'); if (colist != null) { content.append(colist); } var item = createElementFromHtml('\
' + blockName + '\'); item.dataset.blockName = blockName; content.dataset.blockName = blockName; blockSwitch.append(item); return {'item': item, 'content': content}; +} function nextSibling(element, selector) { var sibling = element.nextElementSibling; while (sibling) { if (sibling.matches(selector)) { return sibling; } sibling = sibling.nextElementSibling; } +} function globalSwitch() { document.querySelectorAll(".switch--item").forEach(function(item) { var blockId = blockIdForSwitchItem(item); var handler = function(event) { selectedText = event.target.textContent; window.localStorage.setItem(blockId, selectedText); for (var switchItem of document.querySelectorAll(".switch--item")) { if (blockIdForSwitchItem(switchItem) === blockId && switchItem.textContent === selectedText) { select(switchItem); } } } item.addEventListener("click", handler); if (item.textContent === window.localStorage.getItem(blockId)) { select(item); } }); +} function select(selected) { for (var child of selected.parentNode.children) { child.classList.remove("selected"); } selected.classList.add("selected"); for (var child of selected.parentNode.parentNode.children) { if (child.classList.contains("content")) { if (selected.dataset.blockName === child.dataset.blockName) { child.classList.remove("hidden"); } else { child.classList.add("hidden"); } } } } function blockIdForSwitchItem(item) { idComponents = [] for (var switchItem of item.parentNode.querySelectorAll(".switch--item")) { idComponents.push(switchItem.textContent.toLowerCase()); } return idComponents.sort().join("-") +} window.onload = function() { addBlockSwitches(); globalSwitch(); +}; + +# Spring Cloud Config + +Table of Contents + +* [Quick Start](#_quick_start) + * [Client Side Usage](#_client_side_usage) + +* [Spring Cloud Config Server](#_spring_cloud_config_server) + * [Environment Repository](#_environment_repository) + * [Health Indicator](#_health_indicator) + * [Security](#_security) + * [Actuator and Security](#_actuator_and_security) + * [Encryption and Decryption](#_encryption_and_decryption) + * [Key Management](#_key_management) + * [Creating a Key Store for Testing](#_creating_a_key_store_for_testing) + * [Using Multiple Keys and Key Rotation](#_using_multiple_keys_and_key_rotation) + * [Serving Encrypted Properties](#_serving_encrypted_properties) + +* [Serving Alternative Formats](#_serving_alternative_formats) +* [Serving Plain Text](#_serving_plain_text) + * [Git, SVN, and Native Backends](#spring-cloud-config-serving-plain-text-git-svn-native-backends) + * [AWS S3](#spring-cloud-config-serving-plain-text-aws-s3) + * [Decrypting Plain Text](#_decrypting_plain_text) + +* [Embedding the Config Server](#_embedding_the_config_server) +* [Push Notifications and Spring Cloud Bus](#_push_notifications_and_spring_cloud_bus) +* [Spring Cloud Config Client](#_spring_cloud_config_client) + * [Spring Boot Config Data Import](#config-data-import) + * [Config First Bootstrap](#config-first-bootstrap) + * [Config Client Fail Fast](#config-client-fail-fast) + * [Config Client Retry](#config-client-retry) + * [Config Client Retry with spring.config.import](#_config_client_retry_with_spring_config_import) + * [Locating Remote Configuration Resources](#_locating_remote_configuration_resources) + * [Specifying Multiple Urls for the Config Server](#_specifying_multiple_urls_for_the_config_server) + * [Configuring Timeouts](#_configuring_timeouts) + * [Security](#_security_2) + * [Nested Keys In Vault](#_nested_keys_in_vault) + +**3.1.1** Spring Cloud Config provides server-side and client-side support for externalized configuration in a distributed system. With the Config Server, you have a central place to manage external properties for applications across all environments. The concepts on both client and server map identically to the Spring `Environment` and `PropertySource` abstractions, so they fit very well with Spring applications but can be used with any application running in any language. @@ -7,8 +60,7 @@ As an application moves through the deployment pipeline from dev to test and int The default implementation of the server storage backend uses git, so it easily supports labelled versions of configuration environments as well as being accessible to a wide range of tooling for managing the content. It is easy to add alternative implementations and plug them in with Spring configuration. -[Quick Start](#_quick_start) ----------- +## [Quick Start](#_quick_start) This quick start walks through using both the server and the client of Spring Cloud Config Server. @@ -88,7 +140,7 @@ spring: Other sources are any JDBC compatible database, Subversion, Hashicorp Vault, Credhub and local filesystems. -### [Client Side Usage](#_client_side_usage) ### +### [Client Side Usage](#_client_side_usage) To use these features in an application, you can build it as a Spring Boot application that depends on spring-cloud-config-client (for an example, see the test cases for the config-client or the sample application). The most convenient way to add the dependency is with a Spring Boot starter `org.springframework.cloud:spring-cloud-starter-config`. @@ -206,8 +258,7 @@ A property source called `configserver:/` c | |If you use Spring Cloud Config Client, you need to set the `spring.config.import` property in order to bind to Config Server. You can read more about it [in the Spring Cloud Config Reference Guide](https://docs.spring.io/spring-cloud-config/docs/current/reference/html/#config-data-import).| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -[Spring Cloud Config Server](#_spring_cloud_config_server) ----------- +## [Spring Cloud Config Server](#_spring_cloud_config_server) Spring Cloud Config Server provides an HTTP resource-based API for external configuration (name-value pairs or equivalent YAML content). The server is embeddable in a Spring Boot application, by using the `@EnableConfigServer` annotation. @@ -250,7 +301,7 @@ where `${user.home}/config-repo` is a git repository containing YAML and propert | |The initial clone of your configuration repository can be quick and efficient if you keep only text files in it.
If you store binary files, especially large ones, you may experience delays on the first request for configuration or encounter out of memory errors in the server.| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [Environment Repository](#_environment_repository) ### +### [Environment Repository](#_environment_repository) Where should you store the configuration data for the Config Server? The strategy that governs this behaviour is the `EnvironmentRepository`, serving `Environment` objects. @@ -286,7 +337,7 @@ Higher precedence translates to a `PropertySource` listed earlier in the `Enviro You can set spring.cloud.config.server.accept-empty to false so that Server would return a HTTP 404 status, if the application is not found.By default, this flag is set to true. -#### [Git Backend](#_git_backend) #### +#### [Git Backend](#_git_backend) The default implementation of `EnvironmentRepository` uses a Git backend, which is very convenient for managing upgrades and physical environments and for auditing changes. To change the location of the repository, you can set the `spring.cloud.config.server.git.uri` configuration property in the Config Server (for example in `application.yml`). @@ -300,7 +351,7 @@ For example, if the label is `foo/bar`, replacing the slash would result in the The inclusion of the special string `(_)` can also be applied to the `{application}` parameter. If you use a command-line client such as curl, be careful with the brackets in the URL — you should escape them from the shell with single quotes (''). -##### [Skipping SSL Certificate Validation](#_skipping_ssl_certificate_validation) ##### +##### [Skipping SSL Certificate Validation](#_skipping_ssl_certificate_validation) The configuration server’s validation of the Git server’s SSL certificate can be disabled by setting the `git.skipSslValidation` property to `true` (default is `false`). @@ -314,7 +365,7 @@ spring: skipSslValidation: true ``` -##### [Setting HTTP Connection Timeout](#_setting_http_connection_timeout) ##### +##### [Setting HTTP Connection Timeout](#_setting_http_connection_timeout) You can configure the time, in seconds, that the configuration server will wait to acquire an HTTP connection. Use the `git.timeout` property. @@ -328,7 +379,7 @@ spring: timeout: 4 ``` -##### [Placeholders in Git URI](#_placeholders_in_git_uri) ##### +##### [Placeholders in Git URI](#_placeholders_in_git_uri) Spring Cloud Config Server supports a git repository URL with placeholders for the `{application}` and `{profile}` (and `{label}` if you need it, but remember that the label is applied as a git label anyway). So you can support a “one repository per application” policy by using a structure similar to the following: @@ -358,7 +409,7 @@ spring: where `{application}` is provided at request time in the following format: `organization(_)application`. -##### [Pattern Matching and Multiple Repositories](#_pattern_matching_and_multiple_repositories) ##### +##### [Pattern Matching and Multiple Repositories](#_pattern_matching_and_multiple_repositories) Spring Cloud Config also includes support for more complex requirements with pattern matching on the application and profile name. @@ -462,7 +513,7 @@ All other repositories are not cloned until configuration from the repository is | |Setting a repository to be cloned when the Config Server starts up can help to identify a misconfigured configuration source (such as an invalid repository URI) quickly, while the Config Server is starting up.
With `cloneOnStart` not enabled for a configuration source, the Config Server may start successfully with a misconfigured or invalid configuration source and not detect an error until an application requests configuration from that configuration source.| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -##### [Authentication](#_authentication) ##### +##### [Authentication](#_authentication) To use HTTP basic authentication on the remote repository, add the `username` and `password` properties separately (not in the URL), as shown in the following example: @@ -503,7 +554,7 @@ Warning: When working with SSH keys, the expected ssh private-key must begin wit To correct the above error the RSA key must be converted to PEM format. An example using openssh is provided above for generating a new key in the appropriate format. -##### [Authentication with AWS CodeCommit](#_authentication_with_aws_codecommit) ##### +##### [Authentication with AWS CodeCommit](#_authentication_with_aws_codecommit) Spring Cloud Config Server also supports [AWS CodeCommit](https://docs.aws.amazon.com/codecommit/latest/userguide/welcome.html) authentication. AWS CodeCommit uses an authentication helper when using Git from the command line. @@ -523,7 +574,7 @@ AWS EC2 instances may use [IAM Roles for EC2 Instances](https://docs.aws.amazon. | |The `aws-java-sdk-core` jar is an optional dependency.
If the `aws-java-sdk-core` jar is not on your classpath, the AWS Code Commit credential provider is not created, regardless of the git server URI.| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -##### [Authentication with Google Cloud Source](#_authentication_with_google_cloud_source) ##### +##### [Authentication with Google Cloud Source](#_authentication_with_google_cloud_source) Spring Cloud Config Server also supports authenticating against [Google Cloud Source](https://cloud.google.com/source-repositories/) repositories. @@ -534,7 +585,7 @@ The Google Cloud Source credentials provider will use Google Cloud Platform appl | |`com.google.auth:google-auth-library-oauth2-http` is an optional dependency.
If the `google-auth-library-oauth2-http` jar is not on your classpath, the Google Cloud Source credential provider is not created, regardless of the git server URI.| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -##### [Git SSH configuration using properties](#_git_ssh_configuration_using_properties) ##### +##### [Git SSH configuration using properties](#_git_ssh_configuration_using_properties) By default, the JGit library used by Spring Cloud Config Server uses SSH configuration files such as `~/.ssh/known_hosts` and `/etc/ssh/ssh_config` when connecting to Git repositories by using an SSH URI. In cloud environments such as Cloud Foundry, the local filesystem may be ephemeral or not easily accessible. @@ -593,7 +644,7 @@ The following table describes the SSH configuration properties. | **knownHostsFile** | Location of custom `.known_hosts` file. | |**preferredAuthentications**| Override server authentication method order. This should allow for evading login prompts if server has keyboard-interactive authentication before the `publickey` method. | -##### [Placeholders in Git Search Paths](#_placeholders_in_git_search_paths) ##### +##### [Placeholders in Git Search Paths](#_placeholders_in_git_search_paths) Spring Cloud Config Server also supports a search path with placeholders for the `{application}` and `{profile}` (and `{label}` if you need it), as shown in the following example: @@ -611,7 +662,7 @@ spring: The preceding listing causes a search of the repository for files in the same name as the directory (as well as the top level). Wildcards are also valid in a search path with placeholders (any matching directory is included in the search). -##### [Force pull in Git Repositories](#_force_pull_in_git_repositories) ##### +##### [Force pull in Git Repositories](#_force_pull_in_git_repositories) As mentioned earlier, Spring Cloud Config Server makes a clone of the remote git repository in case the local copy gets dirty (for example, folder content changes by an OS process) such that Spring Cloud Config Server cannot update the local copy from remote repository. @@ -655,7 +706,7 @@ spring: | |The default value for `force-pull` property is `false`.| |---|-------------------------------------------------------| -##### [Deleting untracked branches in Git Repositories](#_deleting_untracked_branches_in_git_repositories) ##### +##### [Deleting untracked branches in Git Repositories](#_deleting_untracked_branches_in_git_repositories) As Spring Cloud Config Server has a clone of the remote git repository after check-outing branch to local repo (e.g fetching properties by label) it will keep this branch @@ -680,7 +731,7 @@ spring: | |The default value for `deleteUntrackedBranches` property is `false`.| |---|--------------------------------------------------------------------| -##### [Git Refresh Rate](#_git_refresh_rate) ##### +##### [Git Refresh Rate](#_git_refresh_rate) You can control how often the config server will fetch updated configuration data from your Git backend by using `spring.cloud.config.server.git.refreshRate`. The @@ -688,17 +739,17 @@ value of this property is specified in seconds. By default the value is 0, meani the config server will fetch updated configuration from the Git repo every time it is requested. -##### [Default Label](#_default_label) ##### +##### [Default Label](#_default_label) The default label used for Git is `main`. If you do not set `spring.cloud.config.server.git.defaultLabel` and a branch named `main`does not exist, the config server will by default also try to checkout a branch named `master`. If you would like to disable to the fallback branch behavior you can set`spring.cloud.config.server.git.tryMasterBranch` to `false`. -#### [Version Control Backend Filesystem Use](#_version_control_backend_filesystem_use) #### +#### [Version Control Backend Filesystem Use](#_version_control_backend_filesystem_use) | |With VCS-based backends (git, svn), files are checked out or cloned to the local filesystem.
By default, they are put in the system temporary directory with a prefix of `config-repo-`.
On linux, for example, it could be `/tmp/config-repo-`.
Some operating systems [routinely clean out](https://serverfault.com/questions/377348/when-does-tmp-get-cleared/377349#377349) temporary directories.
This can lead to unexpected behavior, such as missing properties.
To avoid this problem, change the directory that Config Server uses by setting `spring.cloud.config.server.git.basedir` or `spring.cloud.config.server.svn.basedir` to a directory that does not reside in the system temp structure.| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -#### [File System Backend](#_file_system_backend) #### +#### [File System Backend](#_file_system_backend) There is also a “native” profile in the Config Server that does not use Git but loads the config files from the local classpath or file system (any static URL you want to point to with `spring.cloud.config.server.native.searchLocations`). To use the native profile, launch the Config Server with `spring.profiles.active=native`. @@ -720,7 +771,7 @@ Thus, the default behaviour with no placeholders is the same as adding a search For example, `file:/tmp/config` is the same as `file:/tmp/config,file:/tmp/config/{label}`. This behavior can be disabled by setting `spring.cloud.config.server.native.addLabelLocations=false`. -#### [Vault Backend](#vault-backend) #### +#### [Vault Backend](#vault-backend) Spring Cloud Config Server also supports [Vault](https://www.vaultproject.io) as a backend. @@ -837,7 +888,7 @@ See the [Spring Cloud Vault Reference Guide](https://cloud.spring.io/spring-clou | |If you omit the X-Config-Token header and use a server property to set the authentication, the Config Server application needs an additional dependency on Spring Vault to enable the additional authentication options.
See the [Spring Vault Reference Guide](https://docs.spring.io/spring-vault/docs/current/reference/html/#dependencies) for how to add that dependency.| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -##### [Multiple Properties Sources](#_multiple_properties_sources) ##### +##### [Multiple Properties Sources](#_multiple_properties_sources) When using Vault, you can provide your applications with multiple properties sources. For example, assume you have written data to the following paths in Vault: @@ -853,7 +904,7 @@ Properties written to `secret/application` are available to [all applications us An application with the name, `myApp`, would have any properties written to `secret/myApp` and `secret/application` available to it. When `myApp` has the `dev` profile enabled, properties written to all of the above paths would be available to it, with properties in the first path in the list taking priority over the others. -#### [Accessing Backends Through a Proxy](#_accessing_backends_through_a_proxy) #### +#### [Accessing Backends Through a Proxy](#_accessing_backends_through_a_proxy) The configuration server can access a Git or Vault backend through an HTTP or HTTPS proxy. This behavior is controlled for either Git or Vault by settings under `proxy.http` and `proxy.https`. These settings are per repository, so if you are using a [composite environment repository](#composite-environment-repositories) you must configure proxy settings for each backend in the composite individually. If using a network which requires separate proxy servers for HTTP and HTTPS URLs, you can configure both the HTTP and the HTTPS proxy settings for a single backend. @@ -887,7 +938,7 @@ spring: nonProxyHosts: example.com ``` -#### [Sharing Configuration With All Applications](#_sharing_configuration_with_all_applications) #### +#### [Sharing Configuration With All Applications](#_sharing_configuration_with_all_applications) Sharing configuration between all applications varies according to which approach you take, as described in the following topics: @@ -895,7 +946,7 @@ Sharing configuration between all applications varies according to which approac * [Vault Server](#spring-cloud-config-server-vault-server) -##### [File Based Repositories](#spring-cloud-config-server-file-based-repositories) ##### +##### [File Based Repositories](#spring-cloud-config-server-file-based-repositories) With file-based (git, svn, and native) repositories, resources with file names in `application*` (`application.properties`, `application.yml`, `application-*.properties`, and so on) are shared between all client applications. You can use resources with these file names to configure global defaults and have them be overridden by application-specific files as necessary. @@ -906,7 +957,7 @@ allowed to override them locally. | |With the “native” profile (a local file system backend) , you should use an explicit search location that is not part of the server’s own configuration.
Otherwise, the `application*` resources in the default search locations get removed because they are part of the server.| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -##### [Vault Server](#spring-cloud-config-server-vault-server) ##### +##### [Vault Server](#spring-cloud-config-server-vault-server) When using Vault as a backend, you can share configuration with all applications by placing configuration in `secret/application`. For example, if you run the following Vault command, all applications using the config server will have the properties `foo` and `baz` available to them: @@ -915,7 +966,7 @@ For example, if you run the following Vault command, all applications using the $ vault write secret/application foo=bar baz=bam ``` -##### [CredHub Server](#_credhub_server) ##### +##### [CredHub Server](#_credhub_server) When using CredHub as a backend, you can share configuration with all applications by placing configuration in `/application/` or by placing it in the `default` profile for the application. For example, if you run the following CredHub command, all applications using the config server will have the properties `shared.color1` and `shared.color2` available to them: @@ -930,7 +981,7 @@ credhub set --name "/my-app/default/master/more-shared" --type=json value: {"shared.word1": "hello", "shared.word2": "world"} ``` -#### [AWS Secrets Manager](#_aws_secrets_manager) #### +#### [AWS Secrets Manager](#_aws_secrets_manager) When using AWS Secrets Manager as a backend, you can share configuration with all applications by placing configuration in `/application/` or by placing it in the `default` profile for the application. For example, if you add secrets with the following keys, all application using the config server will have the properties `shared.foo` and `shared.bar` available to them: @@ -961,7 +1012,7 @@ secret value = } ``` -##### [AWS Parameter Store](#_aws_parameter_store) ##### +##### [AWS Parameter Store](#_aws_parameter_store) When using AWS Parameter Store as a backend, you can share configuration with all applications by placing properties within the `/application` hierarchy. @@ -972,7 +1023,7 @@ For example, if you add parameters with the following names, all applications us /config/application-default/fred.baz ``` -#### [JDBC Backend](#_jdbc_backend) #### +#### [JDBC Backend](#_jdbc_backend) Spring Cloud Config Server supports JDBC (relational database) as a backend for configuration properties. You can enable this feature by adding `spring-jdbc` to the classpath and using the `jdbc` profile or by adding a bean of type `JdbcEnvironmentRepository`. @@ -984,7 +1035,7 @@ The database needs to have a table called `PROPERTIES` with columns called `APPL All fields are of type String in Java, so you can make them `VARCHAR` of whatever length you need. Property values behave in the same way as they would if they came from Spring Boot properties files named `{application}-{profile}.properties`, including all the encryption and decryption, which will be applied as post-processing steps (that is, not in the repository implementation directly). -#### [Redis Backend](#_redis_backend) #### +#### [Redis Backend](#_redis_backend) Spring Cloud Config Server supports Redis as a backend for configuration properties. You can enable this feature by adding a dependency to [Spring Data Redis](https://spring.io/projects/spring-data-redis). @@ -1031,7 +1082,7 @@ HGETALL sample-app | |When no profile is specified `default` will be used.| |---|----------------------------------------------------| -#### [AWS S3 Backend](#_aws_s3_backend) #### +#### [AWS S3 Backend](#_aws_s3_backend) Spring Cloud Config Server supports AWS S3 as a backend for configuration properties. You can enable this feature by adding a dependency to the [AWS Java SDK For Amazon S3](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/examples-s3.html). @@ -1070,7 +1121,7 @@ Configuration files are stored in your bucket as `{application}-{profile}.proper | |When no profile is specified `default` will be used.| |---|----------------------------------------------------| -#### [AWS Parameter Store Backend](#_aws_parameter_store_backend) #### +#### [AWS Parameter Store Backend](#_aws_parameter_store_backend) Spring Cloud Config Server supports AWS Parameter Store as a backend for configuration properties. You can enable this feature by adding a dependency to the [AWS Java SDK for SSM](https://github.com/aws/aws-sdk-java/tree/master/aws-java-sdk-ssm). @@ -1122,7 +1173,7 @@ Versioned parameters are already supported with the default behaviour of returni | |* When no application is specified `application` is the default, and when no profile is specified `default` is used.

* Valid values for `awsparamstore.prefix` must start with a forward slash followed by one or more valid path segments or be empty.

* Valid values for `awsparamstore.profile-separator` can only contain dots, dashes and underscores.

* Valid values for `awsparamstore.max-results` must be within the **[1, 10]** range.| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -#### [AWS Secrets Manager Backend](#_aws_secrets_manager_backend) #### +#### [AWS Secrets Manager Backend](#_aws_secrets_manager_backend) Spring Cloud Config Server supports [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) as a backend for configuration properties. You can enable this feature by adding a dependency to [AWS Java SDK for Secrets Manager](https://github.com/aws/aws-sdk-java/tree/master/aws-java-sdk-secretsmanager). @@ -1158,7 +1209,7 @@ AWS Secrets Manager API credentials are determined using [Default Credential Pro | |* When no application is specified `application` is the default, and when no profile is specified `default` is used.| |---|--------------------------------------------------------------------------------------------------------------------| -#### [CredHub Backend](#_credhub_backend) #### +#### [CredHub Backend](#_credhub_backend) Spring Cloud Config Server supports [CredHub](https://docs.cloudfoundry.org/credhub) as a backend for configuration properties. You can enable this feature by adding a dependency to [Spring CredHub](https://spring.io/projects/spring-credhub). @@ -1213,7 +1264,7 @@ All client applications with the name `spring.cloud.config.name=demo-app` will h | |When no profile is specified `default` will be used and when no label is specified `master` will be used as a default value.
NOTE: Values added to `application` will be shared by all the applications.| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -##### [OAuth 2.0](#_oauth_2_0) ##### +##### [OAuth 2.0](#_oauth_2_0) You can authenticate with [OAuth 2.0](https://oauth.net/2/) using [UAA](https://docs.cloudfoundry.org/concepts/architecture/uaa.html) as a provider. @@ -1262,7 +1313,7 @@ spring: | |The used UAA client-id should have `credhub.read` as scope.| |---|-----------------------------------------------------------| -#### [Composite Environment Repositories](#composite-environment-repositories) #### +#### [Composite Environment Repositories](#composite-environment-repositories) In some scenarios, you may wish to pull configuration data from multiple environment repositories. To do so, you can enable the `composite` profile in your configuration server’s application properties or YAML file. @@ -1324,14 +1375,14 @@ The priority order of a repository helps resolve any potential conflicts between | |When using a composite environment, it is important that all repositories contain the same labels.
If you have an environment similar to those in the preceding examples and you request configuration data with the `master` label but the Subversion repository does not contain a branch called `master`, the entire request fails.| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -##### [Custom Composite Environment Repositories](#_custom_composite_environment_repositories) ##### +##### [Custom Composite Environment Repositories](#_custom_composite_environment_repositories) In addition to using one of the environment repositories from Spring Cloud, you can also provide your own `EnvironmentRepository` bean to be included as part of a composite environment. To do so, your bean must implement the `EnvironmentRepository` interface. If you want to control the priority of your custom `EnvironmentRepository` within the composite environment, you should also implement the `Ordered` interface and override the `getOrdered` method. If you do not implement the `Ordered` interface, your `EnvironmentRepository` is given the lowest priority. -#### [Property Overrides](#property-overrides) #### +#### [Property Overrides](#property-overrides) The Config Server has an “overrides” feature that lets the operator provide configuration properties to all applications. The overridden properties cannot be accidentally changed by the application with the normal Spring Boot hooks. @@ -1359,7 +1410,7 @@ The preceding examples causes all applications that are config clients to read ` You can change the priority of all overrides in the client to be more like default values, letting applications supply their own values in environment variables or System properties, by setting the `spring.cloud.config.overrideNone=true` flag (the default is false) in the remote repository. -### [Health Indicator](#_health_indicator) ### +### [Health Indicator](#_health_indicator) Config Server comes with a Health Indicator that checks whether the configured `EnvironmentRepository` is working. By default, it asks the `EnvironmentRepository` for an application named `app`, the `default` profile, and the default label provided by the `EnvironmentRepository` implementation. @@ -1382,19 +1433,19 @@ spring: You can disable the Health Indicator by setting `management.health.config.enabled=false`. -### [Security](#_security) ### +### [Security](#_security) You can secure your Config Server in any way that makes sense to you (from physical network security to OAuth2 bearer tokens), because Spring Security and Spring Boot offer support for many security arrangements. To use the default Spring Boot-configured HTTP Basic security, include Spring Security on the classpath (for example, through `spring-boot-starter-security`). The default is a username of `user` and a randomly generated password. A random password is not useful in practice, so we recommend you configure the password (by setting `spring.security.user.password`) and encrypt it (see below for instructions on how to do that). -### [Actuator and Security](#_actuator_and_security) ### +### [Actuator and Security](#_actuator_and_security) | |Some platforms configure health checks or something similar and point to `/actuator/health` or other actuator endpoints. If actuator is not a dependency of config server, requests to `/actuator/` **would match the config server API `/{application}/{label}` possibly leaking secure information. Remember to add the `spring-boot-starter-actuator` dependency in this case and configure the users such that the user that makes calls to `/actuator/`** does not have access to the config server API at `/{application}/{label}`.| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [Encryption and Decryption](#_encryption_and_decryption) ### +### [Encryption and Decryption](#_encryption_and_decryption) | |To use the encryption and decryption features you need the full-strength JCE installed in your JVM (it is not included by default).
You can download the “Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files” from Oracle and follow the installation instructions (essentially, you need to replace the two policy files in the JRE lib/security directory with the ones that you downloaded).| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| @@ -1476,7 +1527,7 @@ AQAjPgt3eFZQXwt8tsHAVv/QHiY5sI2dRcR+... | |The `--key` argument is mandatory (despite having a `--` prefix).| |---|-----------------------------------------------------------------| -### [Key Management](#_key_management) ### +### [Key Management](#_key_management) The Config Server can use a symmetric (shared) key or an asymmetric one (RSA key pair). The asymmetric choice is superior in terms of security, but it is often more convenient to use a symmetric key since it is a single property value to configure in the `bootstrap.properties`. @@ -1504,7 +1555,7 @@ In practice, you might not want to do decrypt locally, because it spreads the ke concentrating it in the server. On the other hand, it can be a useful option if your config server is relatively insecure and only a handful of clients need the encrypted properties. -### [Creating a Key Store for Testing](#_creating_a_key_store_for_testing) ### +### [Creating a Key Store for Testing](#_creating_a_key_store_for_testing) To create a keystore for testing, you can use a command resembling the following: @@ -1533,7 +1584,7 @@ encrypt: secret: changeme ``` -### [Using Multiple Keys and Key Rotation](#_using_multiple_keys_and_key_rotation) ### +### [Using Multiple Keys and Key Rotation](#_using_multiple_keys_and_key_rotation) In addition to the `{cipher}` prefix in encrypted property values, the Config Server looks for zero or more `{name:value}` prefixes before the start of the (Base64 encoded) cipher text. The keys are passed to a `TextEncryptorLocator`, which can do whatever logic it needs to locate a `TextEncryptor` for the cipher. @@ -1557,14 +1608,13 @@ Note that the clients need to first check that the key alias is available in the | |If you want to let the Config Server handle all encryption as well as decryption, the `{name:value}` prefixes can also be added as plain text posted to the `/encrypt` endpoint, .| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [Serving Encrypted Properties](#_serving_encrypted_properties) ### +### [Serving Encrypted Properties](#_serving_encrypted_properties) Sometimes you want the clients to decrypt the configuration locally, instead of doing it in the server. In that case, if you provide the `encrypt.*` configuration to locate a key, you can still have `/encrypt` and `/decrypt` endpoints, but you need to explicitly switch off the decryption of outgoing properties by placing `spring.cloud.config.server.encrypt.enabled=false` in `bootstrap.[yml|properties]`. If you do not care about the endpoints, it should work if you do not configure either the key or the enabled flag. -[Serving Alternative Formats](#_serving_alternative_formats) ----------- +## [Serving Alternative Formats](#_serving_alternative_formats) The default JSON format from the environment endpoints is perfect for consumption by Spring applications, because it maps directly onto the `Environment` abstraction. If you prefer, you can consume the same data as YAML or Java properties by adding a suffix (".yml", ".yaml" or ".properties") to the resource path. @@ -1576,8 +1626,7 @@ This is a useful feature for consumers that do not know about the Spring placeho | |There are limitations in using the YAML or properties formats, mainly in relation to the loss of metadata.
For example, the JSON is structured as an ordered list of property sources, with names that correlate with the source.
The YAML and properties forms are coalesced into a single map, even if the origin of the values has multiple sources, and the names of the original source files are lost.
Also, the YAML representation is not necessarily a faithful representation of the YAML source in a backing repository either. It is constructed from a list of flat property sources, and assumptions have to be made about the form of the keys.| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -[Serving Plain Text](#_serving_plain_text) ----------- +## [Serving Plain Text](#_serving_plain_text) Instead of using the `Environment` abstraction (or one of the alternative representations of it in YAML or properties format), your applications might need generic plain-text configuration files that are tailored to their environment. The Config Server provides these through an additional endpoint at `/{application}/{profile}/{label}/{path}`, where `application`, `profile`, and `label` have the same meaning as the regular environment endpoint, but `path` is a path to a file name (such as `log.xml`). @@ -1602,7 +1651,7 @@ The following sections show how each one works: * [AWS S3](#spring-cloud-config-serving-plain-text-aws-s3) -### [Git, SVN, and Native Backends](#spring-cloud-config-serving-plain-text-git-svn-native-backends) ### +### [Git, SVN, and Native Backends](#spring-cloud-config-serving-plain-text-git-svn-native-backends) Consider the following example for a GIT or SVN repository or a native backend: @@ -1652,13 +1701,13 @@ server { } ``` -### [AWS S3](#spring-cloud-config-serving-plain-text-aws-s3) ### +### [AWS S3](#spring-cloud-config-serving-plain-text-aws-s3) To enable serving plain text for AWS s3, the Config Server application needs to include a dependency on Spring Cloud AWS. For details on how to set up that dependency, see the[Spring Cloud AWS Reference Guide](https://cloud.spring.io/spring-cloud-static/spring-cloud-aws/2.1.3.RELEASE/single/spring-cloud-aws.html#_spring_cloud_aws_maven_dependency_management). Then you need to configure Spring Cloud AWS, as described in the[Spring Cloud AWS Reference Guide](https://cloud.spring.io/spring-cloud-static/spring-cloud-aws/2.1.3.RELEASE/single/spring-cloud-aws.html#_configuring_credentials). -### [Decrypting Plain Text](#_decrypting_plain_text) ### +### [Decrypting Plain Text](#_decrypting_plain_text) By default, encrypted values in plain text files are not decrypted. In order to enable decryption for plain text files, set `spring.cloud.config.server.encrypt.enabled=true` and `spring.cloud.config.server.encrypt.plainTextEncrypt=true` in `bootstrap.[yml|properties]` @@ -1667,8 +1716,7 @@ By default, encrypted values in plain text files are not decrypted. In order to If this feature is enabled, and an unsupported file extention is requested, any encrypted values in the file will not be decrypted. -[Embedding the Config Server](#_embedding_the_config_server) ----------- +## [Embedding the Config Server](#_embedding_the_config_server) The Config Server runs best as a standalone application. However, if need be, you can embed it in another application. @@ -1706,8 +1754,7 @@ If you want to read the configuration for an application directly from the backe basically want an embedded config server with no endpoints. You can switch off the endpoints entirely by not using the `@EnableConfigServer` annotation (set `spring.cloud.config.server.bootstrap=true`). -[Push Notifications and Spring Cloud Bus](#_push_notifications_and_spring_cloud_bus) ----------- +## [Push Notifications and Spring Cloud Bus](#_push_notifications_and_spring_cloud_bus) Many source code repository providers (such as Github, Gitlab, Gitea, Gitee, Gogs, or Bitbucket) notify you of changes in a repository through a webhook. You can configure the webhook through the provider’s user interface as a URL and a set of events in which you are interested. @@ -1729,13 +1776,12 @@ Doing so broadcasts to applications matching the `{application}` pattern (which | |The default configuration also detects filesystem changes in local git repositories. In that case, the webhook is not used. However, as soon as you edit a config file, a refresh is broadcast.| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -[Spring Cloud Config Client](#_spring_cloud_config_client) ----------- +## [Spring Cloud Config Client](#_spring_cloud_config_client) A Spring Boot application can take immediate advantage of the Spring Config Server (or other external property sources provided by the application developer). It also picks up some additional useful features related to `Environment` change events. -### [Spring Boot Config Data Import](#config-data-import) ### +### [Spring Boot Config Data Import](#config-data-import) Spring Boot 2.4 introduced a new way to import configuration data via the `spring.config.import` property. This is now the default way to bind to Config Server. @@ -1752,7 +1798,7 @@ This will connect to the Config Server at the default location of "http://localh | |A `bootstrap` file (properties or yaml) is **not** needed for the Spring Boot Config Data method of import via `spring.config.import`.| |---|--------------------------------------------------------------------------------------------------------------------------------------| -### [Config First Bootstrap](#config-first-bootstrap) ### +### [Config First Bootstrap](#config-first-bootstrap) To use the legacy bootstrap way of connecting to Config Server, bootstrap must be enabled via a property or the `spring-cloud-starter-bootstrap` starter. The property is `spring.cloud.bootstrap.enabled=true`. It must be set as a System Property or environment variable. Once bootstrap has been enabled any application with Spring Cloud Config Client on the classpath will connect to Config Server as follows: @@ -1760,7 +1806,7 @@ When a config client starts, it binds to the Config Server (through the `spring. The net result of this behavior is that all client applications that want to consume the Config Server need a `bootstrap.yml` (or an environment variable) with the server address set in `spring.cloud.config.uri` (it defaults to "http://localhost:8888"). -#### [Discovery First Lookup](#discovery-first-bootstrap) #### +#### [Discovery First Lookup](#discovery-first-bootstrap) | |Unless you are using [config first bootstrap](#config-first-bootstrap), you will need to have a `spring.config.import` property in your configuration properties with an `optional:` prefix.
For example, `spring.config.import=optional:configserver:`.| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| @@ -1789,12 +1835,12 @@ eureka: configPath: /config ``` -#### [Discovery First Bootstrap Using Eureka And WebClient](#_discovery_first_bootstrap_using_eureka_and_webclient) #### +#### [Discovery First Bootstrap Using Eureka And WebClient](#_discovery_first_bootstrap_using_eureka_and_webclient) If you use the Eureka `DiscoveryClient` from Spring Cloud Netflix and also want to use `WebClient` instead of Jersey or `RestTemplate`, you need to include `WebClient` on your classpath as well as set `eureka.client.webclient.enabled=true`. -### [Config Client Fail Fast](#config-client-fail-fast) ### +### [Config Client Fail Fast](#config-client-fail-fast) In some cases, you may want to fail startup of a service if it cannot connect to the Config Server. If this is the desired behavior, set the bootstrap configuration property `spring.cloud.config.fail-fast=true` to make the client halt with an Exception. @@ -1802,7 +1848,7 @@ If this is the desired behavior, set the bootstrap configuration property `sprin | |To get similar functionality using `spring.config.import`, simply omit the `optional:` prefix.| |---|----------------------------------------------------------------------------------------------| -### [Config Client Retry](#config-client-retry) ### +### [Config Client Retry](#config-client-retry) If you expect that the config server may occasionally be unavailable when your application starts, you can make it keep trying after a failure. First, you need to set `spring.cloud.config.fail-fast=true`. @@ -1813,7 +1859,7 @@ You can configure these properties (and others) by setting the `spring.cloud.con | |To take full control of the retry behavior and are using legacy bootstrap, add a `@Bean` of type `RetryOperationsInterceptor` with an ID of `configServerRetryInterceptor`.
Spring Retry has a `RetryInterceptorBuilder` that supports creating one.| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [Config Client Retry with spring.config.import](#_config_client_retry_with_spring_config_import) ### +### [Config Client Retry with spring.config.import](#_config_client_retry_with_spring_config_import) Retry works with the Spring Boot `spring.config.import` statement and the normal properties work. However, if the import statement is in a profile, such as `application-prod.properties`, then you need a different way to configure retry. Configuration needs to be placed as url parameters on the import statement. @@ -1825,7 +1871,7 @@ spring.config.import=configserver:http://configserver.example.com?fail-fast=true This sets `spring.cloud.config.fail-fast=true` (notice the missing prefix above) and all the available `spring.cloud.config.retry.*` configuration properties. -### [Locating Remote Configuration Resources](#_locating_remote_configuration_resources) ### +### [Locating Remote Configuration Resources](#_locating_remote_configuration_resources) The Config Service serves property sources from `/{application}/{profile}/{label}`, where the default bindings in the client app are as follows: @@ -1846,13 +1892,13 @@ In that case, the items in the list are tried one by one until one succeeds. This behavior can be useful when working on a feature branch. For instance, you might want to align the config label with your branch but make it optional (in that case, use `spring.cloud.config.label=myfeature,develop`). -### [Specifying Multiple Urls for the Config Server](#_specifying_multiple_urls_for_the_config_server) ### +### [Specifying Multiple Urls for the Config Server](#_specifying_multiple_urls_for_the_config_server) To ensure high availability when you have multiple instances of Config Server deployed and expect one or more instances to be unavailable from time to time, you can either specify multiple URLs (as a comma-separated list under the `spring.cloud.config.uri` property) or have all your instances register in a Service Registry like Eureka ( if using Discovery-First Bootstrap mode ). Note that doing so ensures high availability only when the Config Server is not running (that is, when the application has exited) or when a connection timeout has occurred. For example, if the Config Server returns a 500 (Internal Server Error) response or the Config Client receives a 401 from the Config Server (due to bad credentials or other causes), the Config Client does not try to fetch properties from other URLs. An error of that kind indicates a user issue rather than an availability problem. If you use HTTP basic security on your Config Server, it is currently possible to support per-Config Server auth credentials only if you embed the credentials in each URL you specify under the `spring.cloud.config.uri` property. If you use any other kind of security mechanism, you cannot (currently) support per-Config Server authentication and authorization. -### [Configuring Timeouts](#_configuring_timeouts) ### +### [Configuring Timeouts](#_configuring_timeouts) If you want to configure timeout thresholds: @@ -1860,7 +1906,7 @@ If you want to configure timeout thresholds: * Connection timeouts can be configured by using the property `spring.cloud.config.request-connect-timeout`. -### [Security](#_security_2) ### +### [Security](#_security_2) If you use HTTP Basic security on the server, clients need to know the password (and username if it is not the default). You can specify the username and password through the config server URI or via separate username and password properties, as shown in the following example: @@ -1917,7 +1963,7 @@ The `spring.cloud.config.tls.enabled` needs to be true to enable config client s If you use another form of security, you might need to [provide a `RestTemplate`](#custom-rest-template) to the `ConfigServicePropertySourceLocator` (for example, by grabbing it in the bootstrap context and injecting it). -#### [Health Indicator](#_health_indicator_2) #### +#### [Health Indicator](#_health_indicator_2) The Config Client supplies a Spring Boot Health Indicator that attempts to load configuration from the Config Server. The health indicator can be disabled by setting `health.config.enabled=false`. @@ -1925,7 +1971,7 @@ The response is also cached for performance reasons. The default cache time to live is 5 minutes. To change that value, set the `health.config.time-to-live` property (in milliseconds). -#### [Providing A Custom RestTemplate](#custom-rest-template) #### +#### [Providing A Custom RestTemplate](#custom-rest-template) In some cases, you might need to customize the requests made to the config server from the client. Typically, doing so involves passing special `Authorization` headers to authenticate requests to the server. @@ -1959,7 +2005,7 @@ spring.factories org.springframework.cloud.bootstrap.BootstrapConfiguration = com.my.config.client.CustomConfigServiceBootstrapConfiguration ``` -#### [Vault](#_vault) #### +#### [Vault](#_vault) When using Vault as a backend to your config server, the client needs to supply a token for the server to retrieve values from Vault. This token can be provided within the client by setting `spring.cloud.config.token`in `bootstrap.yml`, as shown in the following example: @@ -1971,7 +2017,7 @@ spring: token: YourVaultToken ``` -### [Nested Keys In Vault](#_nested_keys_in_vault) ### +### [Nested Keys In Vault](#_nested_keys_in_vault) Vault supports the ability to nest keys in a value stored in Vault, as shown in the following example: @@ -1987,3 +2033,4 @@ String name = "World"; The preceding code would sets the value of the `name` variable to `appAsecret`. +if (window.parent == window) {(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1\*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga');ga('create', 'UA-2728886-23', 'auto', {'siteSpeedSampleRate': 100});ga('send', 'pageview');} \ No newline at end of file diff --git a/docs/en/spring-cloud/spring-cloud-consul.md b/docs/en/spring-cloud/spring-cloud-consul.md index 0179fe36eda7f51d77faaa5f64f74f3098ff77d5..5436fdf6769ab1830571cb609de7c4d2c9576d06 100644 --- a/docs/en/spring-cloud/spring-cloud-consul.md +++ b/docs/en/spring-cloud/spring-cloud-consul.md @@ -1,5 +1,69 @@ -Spring Cloud Consul -========== +Spring Cloud Consul.hidden { display: none; +} .switch { border-width: 1px 1px 0 1px; border-style: solid; border-color: #7a2518; display: inline-block; +} .switch--item { padding: 10px; background-color: #ffffff; color: #7a2518; display: inline-block; cursor: pointer; +} .switch--item:not(:first-child) { border-width: 0 0 0 1px; border-style: solid; border-color: #7a2518; +} .switch--item.selected { background-color: #7a2519; color: #ffffff; +} function addBlockSwitches() { for (var primary of document.querySelectorAll('.primary')) { var switchItem = createSwitchItem(primary, createBlockSwitch(primary)); switchItem.item.classList.add("selected"); var title = primary.querySelector('.title') title.remove(); } for (var secondary of document.querySelectorAll('.secondary')) { var primary = findPrimary(secondary); if (primary === null) { console.error("Found secondary block with no primary sibling"); } else { var switchItem = createSwitchItem(secondary, primary.querySelector('.switch')); switchItem.content.classList.add("hidden"); primary.append(switchItem.content); secondary.remove(); } } +} function createElementFromHtml(html) { var template = document.createElement('template'); template.innerHTML = html; return template.content.firstChild; +} function createBlockSwitch(primary) { var blockSwitch = createElementFromHtml('\
\'); primary.prepend(blockSwitch) return blockSwitch; +} function findPrimary(secondary) { var candidate = secondary.previousElementSibling; while (candidate != null && !candidate.classList.contains('primary')) { candidate = candidate.previousElementSibling; } return candidate; +} function createSwitchItem(block, blockSwitch) { var blockName = block.querySelector('.title').textContent; var content = block.querySelectorAll('.content').item(0); var colist = nextSibling(block, '.colist'); if (colist != null) { content.append(colist); } var item = createElementFromHtml('\
' + blockName + '\'); item.dataset.blockName = blockName; content.dataset.blockName = blockName; blockSwitch.append(item); return {'item': item, 'content': content}; +} function nextSibling(element, selector) { var sibling = element.nextElementSibling; while (sibling) { if (sibling.matches(selector)) { return sibling; } sibling = sibling.nextElementSibling; } +} function globalSwitch() { document.querySelectorAll(".switch--item").forEach(function(item) { var blockId = blockIdForSwitchItem(item); var handler = function(event) { selectedText = event.target.textContent; window.localStorage.setItem(blockId, selectedText); for (var switchItem of document.querySelectorAll(".switch--item")) { if (blockIdForSwitchItem(switchItem) === blockId && switchItem.textContent === selectedText) { select(switchItem); } } } item.addEventListener("click", handler); if (item.textContent === window.localStorage.getItem(blockId)) { select(item); } }); +} function select(selected) { for (var child of selected.parentNode.children) { child.classList.remove("selected"); } selected.classList.add("selected"); for (var child of selected.parentNode.parentNode.children) { if (child.classList.contains("content")) { if (selected.dataset.blockName === child.dataset.blockName) { child.classList.remove("hidden"); } else { child.classList.add("hidden"); } } } } function blockIdForSwitchItem(item) { idComponents = [] for (var switchItem of item.parentNode.querySelectorAll(".switch--item")) { idComponents.push(switchItem.textContent.toLowerCase()); } return idComponents.sort().join("-") +} window.onload = function() { addBlockSwitches(); globalSwitch(); +}; + +# Spring Cloud Consul + +Table of Contents + +* [1. Quick Start](#quick-start) + * [1.1. Discovery Client Usage](#discovery-client-usage) + * [1.2. Distributed Configuration Usage](#distributed-configuration-usage) + +* [2. Install Consul](#spring-cloud-consul-install) +* [3. Consul Agent](#spring-cloud-consul-agent) +* [4. Service Discovery with Consul](#spring-cloud-consul-discovery) + * [4.1. How to activate](#how-to-activate) + * [4.2. Registering with Consul](#registering-with-consul) + * [4.2.1. Registering Management as a Separate Service](#registering-management-as-a-separate-service) + * [4.2.2. HTTP Health Check](#http-health-check) + * [Applying Headers](#applying-headers) + + * [4.2.3. Actuator Health Indicator(s)](#actuator-health-indicators) + * [DiscoveryClientHealthIndicator](#discoveryclienthealthindicator) + * [ConsulHealthIndicator](#consulhealthindicator) + + * [4.2.4. Metadata](#metadata) + * [Generated Metadata](#generated-metadata) + + * [4.2.5. Making the Consul Instance ID Unique](#making-the-consul-instance-id-unique) + + * [4.3. Looking up services](#looking-up-services) + * [4.3.1. Using Load-balancer](#using-load-balancer) + * [4.3.2. Using the DiscoveryClient](#using-the-discoveryclient) + + * [4.4. Consul Catalog Watch](#consul-catalog-watch) + +* [5. Distributed Configuration with Consul](#spring-cloud-consul-config) + * [5.1. How to activate](#how-to-activate-2) + * [5.2. Spring Boot Config Data Import](#config-data-import) + * [5.3. Customizing](#customizing) + * [5.4. Config Watch](#spring-cloud-consul-config-watch) + * [5.5. YAML or Properties with Config](#spring-cloud-consul-config-format) + * [5.6. git2consul with Config](#spring-cloud-consul-config-git2consul) + * [5.7. Fail Fast](#spring-cloud-consul-failfast) + +* [6. Consul Retry](#spring-cloud-consul-retry) +* [7. Spring Cloud Bus with Consul](#spring-cloud-consul-bus) + * [7.1. How to activate](#how-to-activate-3) + +* [8. Circuit Breaker with Hystrix](#spring-cloud-consul-hystrix) +* [9. Hystrix metrics aggregation with Turbine and Consul](#spring-cloud-consul-turbine) +* [10. Configuration Properties](#configuration-properties) + +**3.1.0** This project provides Consul integrations for Spring Boot apps through autoconfiguration and binding to the Spring Environment and other Spring programming model idioms. With a few @@ -9,14 +73,13 @@ patterns provided include Service Discovery, Control Bus and Configuration. Intelligent Routing and Client Side Load Balancing, Circuit Breaker are provided by integration with other Spring Cloud projects. -[](#quick-start)[1. Quick Start](#quick-start) ----------- +## [](#quick-start)[1. Quick Start](#quick-start) This quick start walks through using Spring Cloud Consul for Service Discovery and Distributed Configuration. First, run Consul Agent on your machine. Then you can access it and use it as a Service Registry and Configuration source with Spring Cloud Consul. -### [](#discovery-client-usage)[1.1. Discovery Client Usage](#discovery-client-usage) ### +### [](#discovery-client-usage)[1.1. Discovery Client Usage](#discovery-client-usage) To use these features in an application, you can build it as a Spring Boot application that depends on `spring-cloud-consul-core`. The most convenient way to add the dependency is with a Spring Boot starter: `org.springframework.cloud:spring-cloud-starter-consul-discovery`. @@ -138,7 +201,7 @@ public String serviceUrl() { } ``` -### [](#distributed-configuration-usage)[1.2. Distributed Configuration Usage](#distributed-configuration-usage) ### +### [](#distributed-configuration-usage)[1.2. Distributed Configuration Usage](#distributed-configuration-usage) To use these features in an application, you can build it as a Spring Boot application that depends on `spring-cloud-consul-core` and `spring-cloud-consul-config`. The most convenient way to add the dependency is with a Spring Boot starter: `org.springframework.cloud:spring-cloud-starter-consul-config`. @@ -239,13 +302,11 @@ The application retrieves configuration data from Consul. | |If you use Spring Cloud Consul Config, you need to set the `spring.config.import` property in order to bind to Consul.
You can read more about it in the [Spring Boot Config Data Import section](#config-data-import).| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -[](#spring-cloud-consul-install)[2. Install Consul](#spring-cloud-consul-install) ----------- +## [](#spring-cloud-consul-install)[2. Install Consul](#spring-cloud-consul-install) Please see the [installation documentation](https://www.consul.io/intro/getting-started/install.html) for instructions on how to install Consul. -[](#spring-cloud-consul-agent)[3. Consul Agent](#spring-cloud-consul-agent) ----------- +## [](#spring-cloud-consul-agent)[3. Consul Agent](#spring-cloud-consul-agent) A Consul Agent client must be available to all Spring Cloud Consul applications. By default, the Agent client is expected to be at `localhost:8500`. See the [Agent documentation](https://consul.io/docs/agent/basics.html) for specifics on how to start an Agent client and how to connect to a cluster of Consul Agent Servers. For development, after you have installed consul, you may start a Consul Agent using the following command: @@ -255,16 +316,15 @@ A Consul Agent client must be available to all Spring Cloud Consul applications. This will start an agent in server mode on port 8500, with the ui available at [localhost:8500](http://localhost:8500) -[](#spring-cloud-consul-discovery)[4. Service Discovery with Consul](#spring-cloud-consul-discovery) ----------- +## [](#spring-cloud-consul-discovery)[4. Service Discovery with Consul](#spring-cloud-consul-discovery) Service Discovery is one of the key tenets of a microservice based architecture. Trying to hand configure each client or some form of convention can be very difficult to do and can be very brittle. Consul provides Service Discovery services via an [HTTP API](https://www.consul.io/docs/agent/http.html) and [DNS](https://www.consul.io/docs/agent/dns.html). Spring Cloud Consul leverages the HTTP API for service registration and discovery. This does not prevent non-Spring Cloud applications from leveraging the DNS interface. Consul Agents servers are run in a [cluster](https://www.consul.io/docs/internals/architecture.html) that communicates via a [gossip protocol](https://www.consul.io/docs/internals/gossip.html) and uses the [Raft consensus protocol](https://www.consul.io/docs/internals/consensus.html). -### [](#how-to-activate)[4.1. How to activate](#how-to-activate) ### +### [](#how-to-activate)[4.1. How to activate](#how-to-activate) To activate Consul Service Discovery use the starter with group `org.springframework.cloud` and artifact id `spring-cloud-starter-consul-discovery`. See the [Spring Cloud Project page](https://projects.spring.io/spring-cloud/) for details on setting up your build system with the current Spring Cloud Release Train. -### [](#registering-with-consul)[4.2. Registering with Consul](#registering-with-consul) ### +### [](#registering-with-consul)[4.2. Registering with Consul](#registering-with-consul) When a client registers with Consul, it provides meta-data about itself such as host and port, id, name and tags. An HTTP [Check](https://www.consul.io/docs/agent/checks.html) is created by default that Consul hits the `/actuator/health` endpoint every 10 seconds. If the health check fails, the service instance is marked as critical. @@ -308,7 +368,7 @@ To disable the Consul Discovery Client you can set `spring.cloud.consul.discover To disable the service registration you can set `spring.cloud.consul.discovery.register` to `false`. -#### [](#registering-management-as-a-separate-service)[4.2.1. Registering Management as a Separate Service](#registering-management-as-a-separate-service) #### +#### [](#registering-management-as-a-separate-service)[4.2.1. Registering Management as a Separate Service](#registering-management-as-a-separate-service) When management server port is set to something different than the application port, by setting `management.server.port` property, management service will be registered as a separate service than the application service. For example: @@ -387,7 +447,7 @@ spring.cloud.consul.discovery.management-suffix spring.cloud.consul.discovery.management-tags ``` -#### [](#http-health-check)[4.2.2. HTTP Health Check](#http-health-check) #### +#### [](#http-health-check)[4.2.2. HTTP Health Check](#http-health-check) The health check for a Consul instance defaults to "/actuator/health", which is the default location of the health endpoint in a Spring Boot Actuator application. You need to change this, even for an Actuator application, if you use a non-default context path or servlet path (e.g. `server.servletPath=/foo`) or management endpoint path (e.g. `management.server.servlet.context-path=/admin`). @@ -408,7 +468,7 @@ spring: You can disable the HTTP health check entirely by setting `spring.cloud.consul.discovery.register-health-check=false`. -##### [](#applying-headers)[Applying Headers](#applying-headers) ##### +##### [](#applying-headers)[Applying Headers](#applying-headers) Headers can be applied to health check requests. For example, if you’re trying to register a [Spring Cloud Config](https://cloud.spring.io/spring-cloud-config/) server that uses [Vault Backend](https://github.com/spring-cloud/spring-cloud-config/blob/master/docs/src/main/asciidoc/spring-cloud-config.adoc#vault-backend): @@ -438,16 +498,16 @@ spring: - "Some other value" ``` -#### [](#actuator-health-indicators)[4.2.3. Actuator Health Indicator(s)](#actuator-health-indicators) #### +#### [](#actuator-health-indicators)[4.2.3. Actuator Health Indicator(s)](#actuator-health-indicators) If the service instance is a Spring Boot Actuator application, it may be provided the following Actuator health indicators. -##### [](#discoveryclienthealthindicator)[DiscoveryClientHealthIndicator](#discoveryclienthealthindicator) ##### +##### [](#discoveryclienthealthindicator)[DiscoveryClientHealthIndicator](#discoveryclienthealthindicator) When Consul Service Discovery is active, a [DiscoverClientHealthIndicator](https://cloud.spring.io/spring-cloud-commons/2.2.x/reference/html/#health-indicator) is configured and made available to the Actuator health endpoint. See [here](https://cloud.spring.io/spring-cloud-commons/2.2.x/reference/html/#health-indicator) for configuration options. -##### [](#consulhealthindicator)[ConsulHealthIndicator](#consulhealthindicator) ##### +##### [](#consulhealthindicator)[ConsulHealthIndicator](#consulhealthindicator) An indicator is configured that verifies the health of the `ConsulClient`. @@ -460,7 +520,7 @@ To disable the indicator set `management.health.consul.enabled=false`. | |When the application runs in [bootstrap context mode](https://cloud.spring.io/spring-cloud-commons/2.2.x/reference/html/#the-bootstrap-application-context) (the default),
this indicator is loaded into the bootstrap context and is not made available to the Actuator health endpoint.| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -#### [](#metadata)[4.2.4. Metadata](#metadata) #### +#### [](#metadata)[4.2.4. Metadata](#metadata) Consul supports metadata on services. Spring Cloud’s `ServiceInstance` has a `Map metadata` field which is populated from a services `meta` field. To populate the `meta` field set values on `spring.cloud.consul.discovery.metadata` or `spring.cloud.consul.discovery.management-metadata` properties. @@ -478,7 +538,7 @@ spring: The above configuration will result in a service who’s meta field contains `myfield→myvalue` and `anotherfield→anothervalue`. -##### [](#generated-metadata)[Generated Metadata](#generated-metadata) ##### +##### [](#generated-metadata)[Generated Metadata](#generated-metadata) The Consul Auto Registration will generate a few entries automatically. @@ -491,7 +551,7 @@ The Consul Auto Registration will generate a few entries automatically. | |Older versions of Spring Cloud Consul populated the `ServiceInstance.getMetadata()` method from Spring Cloud Commons by parsing the `spring.cloud.consul.discovery.tags` property. This is no longer supported, please migrate to using the `spring.cloud.consul.discovery.metadata` map.| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -#### [](#making-the-consul-instance-id-unique)[4.2.5. Making the Consul Instance ID Unique](#making-the-consul-instance-id-unique) #### +#### [](#making-the-consul-instance-id-unique)[4.2.5. Making the Consul Instance ID Unique](#making-the-consul-instance-id-unique) By default a consul instance is registered with an ID that is equal to its Spring Application Context ID. By default, the Spring Application Context ID is `${spring.application.name}:comma,separated,profiles:${server.port}`. For most cases, this will allow multiple instances of one service to run on one machine. If further uniqueness is required, Using Spring Cloud you can override this by providing a unique identifier in `spring.cloud.consul.discovery.instanceId`. For example: @@ -507,9 +567,9 @@ spring: With this metadata, and multiple service instances deployed on localhost, the random value will kick in there to make the instance unique. In Cloudfoundry the `vcap.application.instance_id` will be populated automatically in a Spring Boot application, so the random value will not be needed. -### [](#looking-up-services)[4.3. Looking up services](#looking-up-services) ### +### [](#looking-up-services)[4.3. Looking up services](#looking-up-services) -#### [](#using-load-balancer)[4.3.1. Using Load-balancer](#using-load-balancer) #### +#### [](#using-load-balancer)[4.3.1. Using Load-balancer](#using-load-balancer) Spring Cloud has support for [Feign](https://github.com/spring-cloud/spring-cloud-netflix/blob/master/docs/src/main/asciidoc/spring-cloud-netflix.adoc#spring-cloud-feign) (a REST client builder) and also [Spring `RestTemplate`](https://docs.spring.io/spring-cloud-commons/docs/current/reference/html/#rest-template-loadbalancer-client)for looking up services using the logical service names/ids instead of physical URLs. Both Feign and the discovery-aware RestTemplate utilize [Spring Cloud LoadBalancer](https://docs.spring.io/spring-cloud-commons/docs/current/reference/html/#spring-cloud-loadbalancer) for client-side load balancing. @@ -541,7 +601,7 @@ where the STORES service lives. | |Spring Cloud now also offers support for[Spring Cloud LoadBalancer](https://cloud.spring.io/spring-cloud-commons/reference/html/#_spring_resttemplate_as_a_load_balancer_client).| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -#### [](#using-the-discoveryclient)[4.3.2. Using the DiscoveryClient](#using-the-discoveryclient) #### +#### [](#using-the-discoveryclient)[4.3.2. Using the DiscoveryClient](#using-the-discoveryclient) You can also use the `org.springframework.cloud.client.discovery.DiscoveryClient` which provides a simple API for discovery clients that is not specific to Netflix, e.g. @@ -558,7 +618,7 @@ public String serviceUrl() { } ``` -### [](#consul-catalog-watch)[4.4. Consul Catalog Watch](#consul-catalog-watch) ### +### [](#consul-catalog-watch)[4.4. Consul Catalog Watch](#consul-catalog-watch) The Consul Catalog Watch takes advantage of the ability of consul to [watch services](https://www.consul.io/docs/agent/watches.html#services). The Catalog Watch makes a blocking Consul HTTP API call to determine if any services have changed. If there is new service data a Heartbeat Event is published. @@ -568,8 +628,7 @@ To disable the Catalog Watch set `spring.cloud.consul.discovery.catalogServicesW The watch uses a Spring `TaskScheduler` to schedule the call to consul. By default it is a `ThreadPoolTaskScheduler` with a `poolSize` of 1. To change the `TaskScheduler`, create a bean of type `TaskScheduler` named with the `ConsulDiscoveryClientConfiguration.CATALOG_WATCH_TASK_SCHEDULER_NAME` constant. -[](#spring-cloud-consul-config)[5. Distributed Configuration with Consul](#spring-cloud-consul-config) ----------- +## [](#spring-cloud-consul-config)[5. Distributed Configuration with Consul](#spring-cloud-consul-config) Consul provides a [Key/Value Store](https://consul.io/docs/agent/http/kv.html) for storing configuration and other metadata. Spring Cloud Consul Config is an alternative to the [Config Server and Client](https://github.com/spring-cloud/spring-cloud-config). Configuration is loaded into the Spring Environment during the special "bootstrap" phase. Configuration is stored in the `/config` folder by default. Multiple `PropertySource` instances are created based on the application’s name and the active profiles that mimics the Spring Cloud Config order of resolving properties. For example, an application with the name "testApp" and with the "dev" profile will have the following property sources created: @@ -584,11 +643,11 @@ The most specific property source is at the top, with the least specific at the Configuration is currently read on startup of the application. Sending a HTTP POST to `/refresh` will cause the configuration to be reloaded. [Config Watch](#spring-cloud-consul-config-watch) will also automatically detect changes and reload the application context. -### [](#how-to-activate-2)[5.1. How to activate](#how-to-activate-2) ### +### [](#how-to-activate-2)[5.1. How to activate](#how-to-activate-2) To get started with Consul Configuration use the starter with group `org.springframework.cloud` and artifact id `spring-cloud-starter-consul-config`. See the [Spring Cloud Project page](https://projects.spring.io/spring-cloud/) for details on setting up your build system with the current Spring Cloud Release Train. -### [](#config-data-import)[5.2. Spring Boot Config Data Import](#config-data-import) ### +### [](#config-data-import)[5.2. Spring Boot Config Data Import](#config-data-import) Spring Boot 2.4 introduced a new way to import configuration data via the `spring.config.import` property. This is now the default way to get configuration from Consul. @@ -615,7 +674,7 @@ This will optionally load configuration only from `/contextone` and `/context/tw | |A `bootstrap` file (properties or yaml) is **not** needed for the Spring Boot Config Data method of import via `spring.config.import`.| |---|--------------------------------------------------------------------------------------------------------------------------------------| -### [](#customizing)[5.3. Customizing](#customizing) ### +### [](#customizing)[5.3. Customizing](#customizing) Consul Config may be customized using the following properties: @@ -641,7 +700,7 @@ spring: * `profileSeparator` sets the value of the separator used to separate the profile name in property sources with profiles -### [](#spring-cloud-consul-config-watch)[5.4. Config Watch](#spring-cloud-consul-config-watch) ### +### [](#spring-cloud-consul-config-watch)[5.4. Config Watch](#spring-cloud-consul-config-watch) The Consul Config Watch takes advantage of the ability of consul to [watch a key prefix](https://www.consul.io/docs/agent/watches.html#keyprefix). The Config Watch makes a blocking Consul HTTP API call to determine if any relevant configuration data has changed for the current application. If there is new configuration data a Refresh Event is published. This is equivalent to calling the `/refresh` actuator endpoint. @@ -651,7 +710,7 @@ To disable the Config Watch set `spring.cloud.consul.config.watch.enabled=false` The watch uses a Spring `TaskScheduler` to schedule the call to consul. By default it is a `ThreadPoolTaskScheduler` with a `poolSize` of 1. To change the `TaskScheduler`, create a bean of type `TaskScheduler` named with the `ConsulConfigAutoConfiguration.CONFIG_WATCH_TASK_SCHEDULER_NAME` constant. -### [](#spring-cloud-consul-config-format)[5.5. YAML or Properties with Config](#spring-cloud-consul-config-format) ### +### [](#spring-cloud-consul-config-format)[5.5. YAML or Properties with Config](#spring-cloud-consul-config-format) It may be more convenient to store a blob of properties in YAML or Properties format as opposed to individual key/value pairs. Set the `spring.cloud.consul.config.format` property to `YAML` or `PROPERTIES`. For example to use YAML: @@ -679,7 +738,7 @@ You could store a YAML document in any of the keys listed above. You can change the data key using `spring.cloud.consul.config.data-key`. -### [](#spring-cloud-consul-config-git2consul)[5.6. git2consul with Config](#spring-cloud-consul-config-git2consul) ### +### [](#spring-cloud-consul-config-git2consul)[5.6. git2consul with Config](#spring-cloud-consul-config-git2consul) git2consul is a Consul community project that loads files from a git repository to individual keys into Consul. By default the names of the keys are names of the files. YAML and Properties files are supported with file extensions of `.yml` and `.properties` respectively. Set the `spring.cloud.consul.config.format` property to `FILES`. For example: @@ -715,15 +774,14 @@ config/application.yml The value of each key needs to be a properly formatted YAML or Properties file. -### [](#spring-cloud-consul-failfast)[5.7. Fail Fast](#spring-cloud-consul-failfast) ### +### [](#spring-cloud-consul-failfast)[5.7. Fail Fast](#spring-cloud-consul-failfast) It may be convenient in certain circumstances (like local development or certain test scenarios) to not fail if consul isn’t available for configuration. Setting `spring.cloud.consul.config.fail-fast=false` will cause the configuration module to log a warning rather than throw an exception. This will allow the application to continue startup normally. | |If you have set `spring.cloud.bootstrap.enabled=true` or `spring.config.use-legacy-processing=true`, or included `spring-cloud-starter-bootstrap`, then the above values will need to be placed in `bootstrap.yml` instead of `application.yml`.| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -[](#spring-cloud-consul-retry)[6. Consul Retry](#spring-cloud-consul-retry) ----------- +## [](#spring-cloud-consul-retry)[6. Consul Retry](#spring-cloud-consul-retry) If you expect that the consul agent may occasionally be unavailable when your app starts, you can ask it to keep trying after a failure. You need to add`spring-retry` and `spring-boot-starter-aop` to your classpath. The default @@ -735,22 +793,19 @@ This works with both Spring Cloud Consul Config and Discovery registration. | |To take full control of the retry add a `@Bean` of type`RetryOperationsInterceptor` with id "consulRetryInterceptor". Spring
Retry has a `RetryInterceptorBuilder` that makes it easy to create one.| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -[](#spring-cloud-consul-bus)[7. Spring Cloud Bus with Consul](#spring-cloud-consul-bus) ----------- +## [](#spring-cloud-consul-bus)[7. Spring Cloud Bus with Consul](#spring-cloud-consul-bus) -### [](#how-to-activate-3)[7.1. How to activate](#how-to-activate-3) ### +### [](#how-to-activate-3)[7.1. How to activate](#how-to-activate-3) To get started with the Consul Bus use the starter with group `org.springframework.cloud` and artifact id `spring-cloud-starter-consul-bus`. See the [Spring Cloud Project page](https://projects.spring.io/spring-cloud/) for details on setting up your build system with the current Spring Cloud Release Train. See the [Spring Cloud Bus](https://cloud.spring.io/spring-cloud-bus/) documentation for the available actuator endpoints and howto send custom messages. -[](#spring-cloud-consul-hystrix)[8. Circuit Breaker with Hystrix](#spring-cloud-consul-hystrix) ----------- +## [](#spring-cloud-consul-hystrix)[8. Circuit Breaker with Hystrix](#spring-cloud-consul-hystrix) Applications can use the Hystrix Circuit Breaker provided by the Spring Cloud Netflix project by including this starter in the projects pom.xml: `spring-cloud-starter-hystrix`. Hystrix doesn’t depend on the Netflix Discovery Client. The `@EnableHystrix` annotation should be placed on a configuration class (usually the main class). Then methods can be annotated with `@HystrixCommand` to be protected by a circuit breaker. See [the documentation](https://projects.spring.io/spring-cloud/spring-cloud.html#_circuit_breaker_hystrix_clients) for more details. -[](#spring-cloud-consul-turbine)[9. Hystrix metrics aggregation with Turbine and Consul](#spring-cloud-consul-turbine) ----------- +## [](#spring-cloud-consul-turbine)[9. Hystrix metrics aggregation with Turbine and Consul](#spring-cloud-consul-turbine) Turbine (provided by the Spring Cloud Netflix project), aggregates multiple instances Hystrix metrics streams, so the dashboard can display an aggregate view. Turbine uses the `DiscoveryClient` interface to lookup relevant instances. To use Turbine with Spring Cloud Consul, configure the Turbine application in a manner similar to the following examples: @@ -794,7 +849,8 @@ public class Turbine { } ``` -[](#configuration-properties)[10. Configuration Properties](#configuration-properties) ----------- +## [](#configuration-properties)[10. Configuration Properties](#configuration-properties) To see the list of all Consul related configuration properties please check [the Appendix page](appendix.html). + +if (window.parent == window) {(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1\*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga');ga('create', 'UA-2728886-23', 'auto', {'siteSpeedSampleRate': 100});ga('send', 'pageview');} \ No newline at end of file diff --git a/docs/en/spring-cloud/spring-cloud-contract.md b/docs/en/spring-cloud/spring-cloud-contract.md index d4bc8b53d1ab0c7a549ad48f17baa3399076177a..7d48ddb31d6f12f958226fe68d2994c3241f31a0 100644 --- a/docs/en/spring-cloud/spring-cloud-contract.md +++ b/docs/en/spring-cloud/spring-cloud-contract.md @@ -1,5 +1,20 @@ -Spring Cloud Contract Reference Documentation -========== +Spring Cloud Contract Reference Documentation.hidden { display: none; +} .switch { border-width: 1px 1px 0 1px; border-style: solid; border-color: #7a2518; display: inline-block; +} .switch--item { padding: 10px; background-color: #ffffff; color: #7a2518; display: inline-block; cursor: pointer; +} .switch--item:not(:first-child) { border-width: 0 0 0 1px; border-style: solid; border-color: #7a2518; +} .switch--item.selected { background-color: #7a2519; color: #ffffff; +} function addBlockSwitches() { for (var primary of document.querySelectorAll('.primary')) { var switchItem = createSwitchItem(primary, createBlockSwitch(primary)); switchItem.item.classList.add("selected"); var title = primary.querySelector('.title') title.remove(); } for (var secondary of document.querySelectorAll('.secondary')) { var primary = findPrimary(secondary); if (primary === null) { console.error("Found secondary block with no primary sibling"); } else { var switchItem = createSwitchItem(secondary, primary.querySelector('.switch')); switchItem.content.classList.add("hidden"); primary.append(switchItem.content); secondary.remove(); } } +} function createElementFromHtml(html) { var template = document.createElement('template'); template.innerHTML = html; return template.content.firstChild; +} function createBlockSwitch(primary) { var blockSwitch = createElementFromHtml('\
\'); primary.prepend(blockSwitch) return blockSwitch; +} function findPrimary(secondary) { var candidate = secondary.previousElementSibling; while (candidate != null && !candidate.classList.contains('primary')) { candidate = candidate.previousElementSibling; } return candidate; +} function createSwitchItem(block, blockSwitch) { var blockName = block.querySelector('.title').textContent; var content = block.querySelectorAll('.content').item(0); var colist = nextSibling(block, '.colist'); if (colist != null) { content.append(colist); } var item = createElementFromHtml('\
' + blockName + '\'); item.dataset.blockName = blockName; content.dataset.blockName = blockName; blockSwitch.append(item); return {'item': item, 'content': content}; +} function nextSibling(element, selector) { var sibling = element.nextElementSibling; while (sibling) { if (sibling.matches(selector)) { return sibling; } sibling = sibling.nextElementSibling; } +} function globalSwitch() { document.querySelectorAll(".switch--item").forEach(function(item) { var blockId = blockIdForSwitchItem(item); var handler = function(event) { selectedText = event.target.textContent; window.localStorage.setItem(blockId, selectedText); for (var switchItem of document.querySelectorAll(".switch--item")) { if (blockIdForSwitchItem(switchItem) === blockId && switchItem.textContent === selectedText) { select(switchItem); } } } item.addEventListener("click", handler); if (item.textContent === window.localStorage.getItem(blockId)) { select(item); } }); +} function select(selected) { for (var child of selected.parentNode.children) { child.classList.remove("selected"); } selected.classList.add("selected"); for (var child of selected.parentNode.parentNode.children) { if (child.classList.contains("content")) { if (selected.dataset.blockName === child.dataset.blockName) { child.classList.remove("hidden"); } else { child.classList.add("hidden"); } } } } function blockIdForSwitchItem(item) { idComponents = [] for (var switchItem of item.parentNode.querySelectorAll(".switch--item")) { idComponents.push(switchItem.textContent.toLowerCase()); } return idComponents.sort().join("-") +} window.onload = function() { addBlockSwitches(); globalSwitch(); +}; + +# Spring Cloud Contract Reference Documentation Adam Dudczak, Mathias Düsterhöft, Marcin Grzejszczak, Dennis Kieselhorst, Jakub Kubryński, Karol Lassak, Olga Maciaszek-Sharma, Mariusz Smykuła, Dave Syer, Jay Bryant @@ -15,3 +30,4 @@ The reference documentation consists of the following sections: | [“How-to” Guides](howto.html#howto) | Stubs versioning, Pact integration, Debugging, and more. | | [Appendices](appendix.html#appendix) | Properties, Metadata, Configuration, Dependencies, and more. | +if (window.parent == window) {(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1\*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga');ga('create', 'UA-2728886-23', 'auto', {'siteSpeedSampleRate': 100});ga('send', 'pageview');} \ No newline at end of file diff --git a/docs/en/spring-cloud/spring-cloud-function.md b/docs/en/spring-cloud/spring-cloud-function.md index 7a851bc94620e4f9a9d2a01ef156c6f9838b1294..182589e4d920b4410f8069344072c9c766e3fe42 100644 --- a/docs/en/spring-cloud/spring-cloud-function.md +++ b/docs/en/spring-cloud/spring-cloud-function.md @@ -1,5 +1,20 @@ -Spring Cloud Function Reference Documentation -========== +Spring Cloud Function Reference Documentation.hidden { display: none; +} .switch { border-width: 1px 1px 0 1px; border-style: solid; border-color: #7a2518; display: inline-block; +} .switch--item { padding: 10px; background-color: #ffffff; color: #7a2518; display: inline-block; cursor: pointer; +} .switch--item:not(:first-child) { border-width: 0 0 0 1px; border-style: solid; border-color: #7a2518; +} .switch--item.selected { background-color: #7a2519; color: #ffffff; +} function addBlockSwitches() { for (var primary of document.querySelectorAll('.primary')) { var switchItem = createSwitchItem(primary, createBlockSwitch(primary)); switchItem.item.classList.add("selected"); var title = primary.querySelector('.title') title.remove(); } for (var secondary of document.querySelectorAll('.secondary')) { var primary = findPrimary(secondary); if (primary === null) { console.error("Found secondary block with no primary sibling"); } else { var switchItem = createSwitchItem(secondary, primary.querySelector('.switch')); switchItem.content.classList.add("hidden"); primary.append(switchItem.content); secondary.remove(); } } +} function createElementFromHtml(html) { var template = document.createElement('template'); template.innerHTML = html; return template.content.firstChild; +} function createBlockSwitch(primary) { var blockSwitch = createElementFromHtml('\
\'); primary.prepend(blockSwitch) return blockSwitch; +} function findPrimary(secondary) { var candidate = secondary.previousElementSibling; while (candidate != null && !candidate.classList.contains('primary')) { candidate = candidate.previousElementSibling; } return candidate; +} function createSwitchItem(block, blockSwitch) { var blockName = block.querySelector('.title').textContent; var content = block.querySelectorAll('.content').item(0); var colist = nextSibling(block, '.colist'); if (colist != null) { content.append(colist); } var item = createElementFromHtml('\
' + blockName + '\'); item.dataset.blockName = blockName; content.dataset.blockName = blockName; blockSwitch.append(item); return {'item': item, 'content': content}; +} function nextSibling(element, selector) { var sibling = element.nextElementSibling; while (sibling) { if (sibling.matches(selector)) { return sibling; } sibling = sibling.nextElementSibling; } +} function globalSwitch() { document.querySelectorAll(".switch--item").forEach(function(item) { var blockId = blockIdForSwitchItem(item); var handler = function(event) { selectedText = event.target.textContent; window.localStorage.setItem(blockId, selectedText); for (var switchItem of document.querySelectorAll(".switch--item")) { if (blockIdForSwitchItem(switchItem) === blockId && switchItem.textContent === selectedText) { select(switchItem); } } } item.addEventListener("click", handler); if (item.textContent === window.localStorage.getItem(blockId)) { select(item); } }); +} function select(selected) { for (var child of selected.parentNode.children) { child.classList.remove("selected"); } selected.classList.add("selected"); for (var child of selected.parentNode.parentNode.children) { if (child.classList.contains("content")) { if (selected.dataset.blockName === child.dataset.blockName) { child.classList.remove("hidden"); } else { child.classList.add("hidden"); } } } } function blockIdForSwitchItem(item) { idComponents = [] for (var switchItem of item.parentNode.querySelectorAll(".switch--item")) { idComponents.push(switchItem.textContent.toLowerCase()); } return idComponents.sort().join("-") +} window.onload = function() { addBlockSwitches(); globalSwitch(); +}; + +# Spring Cloud Function Reference Documentation Mark Fisher, Dave Syer, Oleg Zhurakousky, Anshul Mehra @@ -20,3 +35,4 @@ Relevant Links: |[Reactor](https://projectreactor.io/)|Project Reactor| |-------------------------------------|---------------| +if (window.parent == window) {(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1\*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga');ga('create', 'UA-2728886-23', 'auto', {'siteSpeedSampleRate': 100});ga('send', 'pageview');} \ No newline at end of file diff --git a/docs/en/spring-cloud/spring-cloud-gateway.md b/docs/en/spring-cloud/spring-cloud-gateway.md index f9a4e0a096895ff2cd8b84e8c6d7a8ef35a847b5..f8eca1d5bd3fa11441b80e72f54da4e59990a7f0 100644 --- a/docs/en/spring-cloud/spring-cloud-gateway.md +++ b/docs/en/spring-cloud/spring-cloud-gateway.md @@ -1,11 +1,148 @@ -Spring Cloud Gateway -========== - +Spring Cloud Gateway.hidden { display: none; +} .switch { border-width: 1px 1px 0 1px; border-style: solid; border-color: #7a2518; display: inline-block; +} .switch--item { padding: 10px; background-color: #ffffff; color: #7a2518; display: inline-block; cursor: pointer; +} .switch--item:not(:first-child) { border-width: 0 0 0 1px; border-style: solid; border-color: #7a2518; +} .switch--item.selected { background-color: #7a2519; color: #ffffff; +} function addBlockSwitches() { for (var primary of document.querySelectorAll('.primary')) { var switchItem = createSwitchItem(primary, createBlockSwitch(primary)); switchItem.item.classList.add("selected"); var title = primary.querySelector('.title') title.remove(); } for (var secondary of document.querySelectorAll('.secondary')) { var primary = findPrimary(secondary); if (primary === null) { console.error("Found secondary block with no primary sibling"); } else { var switchItem = createSwitchItem(secondary, primary.querySelector('.switch')); switchItem.content.classList.add("hidden"); primary.append(switchItem.content); secondary.remove(); } } +} function createElementFromHtml(html) { var template = document.createElement('template'); template.innerHTML = html; return template.content.firstChild; +} function createBlockSwitch(primary) { var blockSwitch = createElementFromHtml('\
\'); primary.prepend(blockSwitch) return blockSwitch; +} function findPrimary(secondary) { var candidate = secondary.previousElementSibling; while (candidate != null && !candidate.classList.contains('primary')) { candidate = candidate.previousElementSibling; } return candidate; +} function createSwitchItem(block, blockSwitch) { var blockName = block.querySelector('.title').textContent; var content = block.querySelectorAll('.content').item(0); var colist = nextSibling(block, '.colist'); if (colist != null) { content.append(colist); } var item = createElementFromHtml('\
' + blockName + '\'); item.dataset.blockName = blockName; content.dataset.blockName = blockName; blockSwitch.append(item); return {'item': item, 'content': content}; +} function nextSibling(element, selector) { var sibling = element.nextElementSibling; while (sibling) { if (sibling.matches(selector)) { return sibling; } sibling = sibling.nextElementSibling; } +} function globalSwitch() { document.querySelectorAll(".switch--item").forEach(function(item) { var blockId = blockIdForSwitchItem(item); var handler = function(event) { selectedText = event.target.textContent; window.localStorage.setItem(blockId, selectedText); for (var switchItem of document.querySelectorAll(".switch--item")) { if (blockIdForSwitchItem(switchItem) === blockId && switchItem.textContent === selectedText) { select(switchItem); } } } item.addEventListener("click", handler); if (item.textContent === window.localStorage.getItem(blockId)) { select(item); } }); +} function select(selected) { for (var child of selected.parentNode.children) { child.classList.remove("selected"); } selected.classList.add("selected"); for (var child of selected.parentNode.parentNode.children) { if (child.classList.contains("content")) { if (selected.dataset.blockName === child.dataset.blockName) { child.classList.remove("hidden"); } else { child.classList.add("hidden"); } } } } function blockIdForSwitchItem(item) { idComponents = [] for (var switchItem of item.parentNode.querySelectorAll(".switch--item")) { idComponents.push(switchItem.textContent.toLowerCase()); } return idComponents.sort().join("-") +} window.onload = function() { addBlockSwitches(); globalSwitch(); +}; + +# Spring Cloud Gateway + +Table of Contents + +* [1. How to Include Spring Cloud Gateway](#gateway-starter) +* [2. Glossary](#glossary) +* [3. How It Works](#gateway-how-it-works) +* [4. Configuring Route Predicate Factories and Gateway Filter Factories](#configuring-route-predicate-factories-and-gateway-filter-factories) + * [4.1. Shortcut Configuration](#shortcut-configuration) + * [4.2. Fully Expanded Arguments](#fully-expanded-arguments) + +* [5. Route Predicate Factories](#gateway-request-predicates-factories) + * [5.1. The After Route Predicate Factory](#the-after-route-predicate-factory) + * [5.2. The Before Route Predicate Factory](#the-before-route-predicate-factory) + * [5.3. The Between Route Predicate Factory](#the-between-route-predicate-factory) + * [5.4. The Cookie Route Predicate Factory](#the-cookie-route-predicate-factory) + * [5.5. The Header Route Predicate Factory](#the-header-route-predicate-factory) + * [5.6. The Host Route Predicate Factory](#the-host-route-predicate-factory) + * [5.7. The Method Route Predicate Factory](#the-method-route-predicate-factory) + * [5.8. The Path Route Predicate Factory](#the-path-route-predicate-factory) + * [5.9. The Query Route Predicate Factory](#the-query-route-predicate-factory) + * [5.10. The RemoteAddr Route Predicate Factory](#the-remoteaddr-route-predicate-factory) + * [5.10.1. Modifying the Way Remote Addresses Are Resolved](#modifying-the-way-remote-addresses-are-resolved) + + * [5.11. The Weight Route Predicate Factory](#the-weight-route-predicate-factory) + * [5.12. The XForwarded Remote Addr Route Predicate Factory](#the-xforwarded-remote-addr-route-predicate-factory) + +* [6. `GatewayFilter` Factories](#gatewayfilter-factories) + * [6.1. The `AddRequestHeader` `GatewayFilter` Factory](#the-addrequestheader-gatewayfilter-factory) + * [6.2. The `AddRequestParameter` `GatewayFilter` Factory](#the-addrequestparameter-gatewayfilter-factory) + * [6.3. The `AddResponseHeader` `GatewayFilter` Factory](#the-addresponseheader-gatewayfilter-factory) + * [6.4. The `DedupeResponseHeader` `GatewayFilter` Factory](#the-deduperesponseheader-gatewayfilter-factory) + * [6.5. Spring Cloud CircuitBreaker GatewayFilter Factory](#spring-cloud-circuitbreaker-filter-factory) + * [6.5.1. Tripping The Circuit Breaker On Status Codes](#circuit-breaker-status-codes) + + * [6.6. The `FallbackHeaders` `GatewayFilter` Factory](#fallback-headers) + * [6.7. The `MapRequestHeader` `GatewayFilter` Factory](#the-maprequestheader-gatewayfilter-factory) + * [6.8. The `PrefixPath` `GatewayFilter` Factory](#the-prefixpath-gatewayfilter-factory) + * [6.9. The `PreserveHostHeader` `GatewayFilter` Factory](#the-preservehostheader-gatewayfilter-factory) + * [6.10. The `RequestRateLimiter` `GatewayFilter` Factory](#the-requestratelimiter-gatewayfilter-factory) + * [6.10.1. The Redis `RateLimiter`](#the-redis-ratelimiter) + + * [6.11. The `RedirectTo` `GatewayFilter` Factory](#the-redirectto-gatewayfilter-factory) + * [6.12. The `RemoveRequestHeader` GatewayFilter Factory](#the-removerequestheader-gatewayfilter-factory) + * [6.13. `RemoveResponseHeader` `GatewayFilter` Factory](#removeresponseheader-gatewayfilter-factory) + * [6.14. The `RemoveRequestParameter` `GatewayFilter` Factory](#the-removerequestparameter-gatewayfilter-factory) + * [6.15. The `RewritePath` `GatewayFilter` Factory](#the-rewritepath-gatewayfilter-factory) + * [6.16. `RewriteLocationResponseHeader` `GatewayFilter` Factory](#rewritelocationresponseheader-gatewayfilter-factory) + * [6.17. The `RewriteResponseHeader` `GatewayFilter` Factory](#the-rewriteresponseheader-gatewayfilter-factory) + * [6.18. The `SaveSession` `GatewayFilter` Factory](#the-savesession-gatewayfilter-factory) + * [6.19. The `SecureHeaders` `GatewayFilter` Factory](#the-secureheaders-gatewayfilter-factory) + * [6.20. The `SetPath` `GatewayFilter` Factory](#the-setpath-gatewayfilter-factory) + * [6.21. The `SetRequestHeader` `GatewayFilter` Factory](#the-setrequestheader-gatewayfilter-factory) + * [6.22. The `SetResponseHeader` `GatewayFilter` Factory](#the-setresponseheader-gatewayfilter-factory) + * [6.23. The `SetStatus` `GatewayFilter` Factory](#the-setstatus-gatewayfilter-factory) + * [6.24. The `StripPrefix` `GatewayFilter` Factory](#the-stripprefix-gatewayfilter-factory) + * [6.25. The Retry `GatewayFilter` Factory](#the-retry-gatewayfilter-factory) + * [6.26. The `RequestSize` `GatewayFilter` Factory](#the-requestsize-gatewayfilter-factory) + * [6.27. The `SetRequestHostHeader` `GatewayFilter` Factory](#the-setrequesthostheader-gatewayfilter-factory) + * [6.28. Modify a Request Body `GatewayFilter` Factory](#modify-a-request-body-gatewayfilter-factory) + * [6.29. Modify a Response Body `GatewayFilter` Factory](#modify-a-response-body-gatewayfilter-factory) + * [6.30. Token Relay `GatewayFilter` Factory](#token-relay-gatewayfilter-factory) + * [6.31. The `CacheRequestBody` `GatewayFilter` Factory](#the-cacherequestbody-gatewayfilter-factory) + * [6.32. Default Filters](#default-filters) + +* [7. Global Filters](#global-filters) + * [7.1. Combined Global Filter and `GatewayFilter` Ordering](#gateway-combined-global-filter-and-gatewayfilter-ordering) + * [7.2. Forward Routing Filter](#forward-routing-filter) + * [7.3. The `ReactiveLoadBalancerClientFilter`](#reactive-loadbalancer-client-filter) + * [7.4. The Netty Routing Filter](#the-netty-routing-filter) + * [7.5. The Netty Write Response Filter](#the-netty-write-response-filter) + * [7.6. The `RouteToRequestUrl` Filter](#the-routetorequesturl-filter) + * [7.7. The Websocket Routing Filter](#the-websocket-routing-filter) + * [7.8. The Gateway Metrics Filter](#the-gateway-metrics-filter) + * [7.9. Marking An Exchange As Routed](#marking-an-exchange-as-routed) + +* [8. HttpHeadersFilters](#httpheadersfilters) + * [8.1. Forwarded Headers Filter](#forwarded-headers-filter) + * [8.2. RemoveHopByHop Headers Filter](#removehopbyhop-headers-filter) + * [8.3. XForwarded Headers Filter](#xforwarded-headers-filter) + +* [9. TLS and SSL](#tls-and-ssl) + * [9.1. TLS Handshake](#tls-handshake) + +* [10. Configuration](#configuration) + * [10.1. RouteDefinition Metrics](#routedefinition-metrics) + +* [11. Route Metadata Configuration](#route-metadata-configuration) +* [12. Http timeouts configuration](#http-timeouts-configuration) + * [12.1. Global timeouts](#global-timeouts) + * [12.2. Per-route timeouts](#per-route-timeouts) + * [12.3. Fluent Java Routes API](#fluent-java-routes-api) + * [12.4. The `DiscoveryClient` Route Definition Locator](#the-discoveryclient-route-definition-locator) + * [12.4.1. Configuring Predicates and Filters For `DiscoveryClient` Routes](#configuring-predicates-and-filters-for-discoveryclient-routes) + +* [13. Reactor Netty Access Logs](#reactor-netty-access-logs) +* [14. CORS Configuration](#cors-configuration) +* [15. Actuator API](#actuator-api) + * [15.1. Verbose Actuator Format](#verbose-actuator-format) + * [15.2. Retrieving Route Filters](#retrieving-route-filters) + * [15.2.1. Global Filters](#gateway-global-filters) + * [15.2.2. Route Filters](#gateway-route-filters) + + * [15.3. Refreshing the Route Cache](#refreshing-the-route-cache) + * [15.4. Retrieving the Routes Defined in the Gateway](#retrieving-the-routes-defined-in-the-gateway) + * [15.5. Retrieving Information about a Particular Route](#gateway-retrieving-information-about-a-particular-route) + * [15.6. Creating and Deleting a Particular Route](#creating-and-deleting-a-particular-route) + * [15.7. Recap: The List of All endpoints](#recap-the-list-of-all-endpoints) + * [15.8. Sharing Routes between multiple Gateway instances](#sharing-routes-between-multiple-gateway-instances) + +* [16. Troubleshooting](#troubleshooting) + * [16.1. Log Levels](#log-levels) + * [16.2. Wiretap](#wiretap) + +* [17. Developer Guide](#developer-guide) + * [17.1. Writing Custom Route Predicate Factories](#writing-custom-route-predicate-factories) + * [17.2. Writing Custom GatewayFilter Factories](#writing-custom-gatewayfilter-factories) + * [17.2.1. Naming Custom Filters And References In Configuration](#naming-custom-filters-and-references-in-configuration) + + * [17.3. Writing Custom Global Filters](#writing-custom-global-filters) + +* [18. Building a Simple Gateway by Using Spring MVC or Webflux](#building-a-simple-gateway-by-using-spring-mvc-or-webflux) +* [19. Configuration properties](#configuration-properties) + +**3.1.1** This project provides an API Gateway built on top of the Spring Ecosystem, including: Spring 5, Spring Boot 2 and Project Reactor. Spring Cloud Gateway aims to provide a simple, yet effective way to route to APIs and provide cross cutting concerns to them such as: security, monitoring/metrics, and resiliency. -[](#gateway-starter)[1. How to Include Spring Cloud Gateway](#gateway-starter) ----------- +## [](#gateway-starter)[1. How to Include Spring Cloud Gateway](#gateway-starter) To include Spring Cloud Gateway in your project, use the starter with a group ID of `org.springframework.cloud` and an artifact ID of `spring-cloud-starter-gateway`. See the [Spring Cloud Project page](https://projects.spring.io/spring-cloud/) for details on setting up your build system with the current Spring Cloud Release Train. @@ -18,8 +155,7 @@ If you include the starter, but you do not want the gateway to be enabled, set ` | |Spring Cloud Gateway requires the Netty runtime provided by Spring Boot and Spring Webflux.
It does not work in a traditional Servlet Container or when built as a WAR.| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -[](#glossary)[2. Glossary](#glossary) ----------- +## [](#glossary)[2. Glossary](#glossary) * **Route**: The basic building block of the gateway. It is defined by an ID, a destination URI, a collection of predicates, and a collection of filters. A route is matched if the aggregate predicate is true. @@ -30,8 +166,7 @@ If you include the starter, but you do not want the gateway to be enabled, set ` * **Filter**: These are instances of [`GatewayFilter`](https://github.com/spring-cloud/spring-cloud-gateway/tree/main/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/GatewayFilter.java) that have been constructed with a specific factory. Here, you can modify requests and responses before or after sending the downstream request. -[](#gateway-how-it-works)[3. How It Works](#gateway-how-it-works) ----------- +## [](#gateway-how-it-works)[3. How It Works](#gateway-how-it-works) The following diagram provides a high-level overview of how Spring Cloud Gateway works: @@ -45,14 +180,13 @@ All “pre” filter logic is executed. Then the proxy request is made. After th | |URIs defined in routes without a port get default port values of 80 and 443 for the HTTP and HTTPS URIs, respectively.| |---|----------------------------------------------------------------------------------------------------------------------| -[](#configuring-route-predicate-factories-and-gateway-filter-factories)[4. Configuring Route Predicate Factories and Gateway Filter Factories](#configuring-route-predicate-factories-and-gateway-filter-factories) ----------- +## [](#configuring-route-predicate-factories-and-gateway-filter-factories)[4. Configuring Route Predicate Factories and Gateway Filter Factories](#configuring-route-predicate-factories-and-gateway-filter-factories) There are two ways to configure predicates and filters: shortcuts and fully expanded arguments. Most examples below use the shortcut way. The name and argument names will be listed as `code` in the first sentance or two of the each section. The arguments are typically listed in the order that would be needed for the shortcut configuration. -### [](#shortcut-configuration)[4.1. Shortcut Configuration](#shortcut-configuration) ### +### [](#shortcut-configuration)[4.1. Shortcut Configuration](#shortcut-configuration) Shortcut configuration is recognized by the filter name, followed by an equals sign (`=`), followed by argument values separated by commas (`,`). @@ -71,7 +205,7 @@ spring: The previous sample defines the `Cookie` Route Predicate Factory with two arguments, the cookie name, `mycookie` and the value to match `mycookievalue`. -### [](#fully-expanded-arguments)[4.2. Fully Expanded Arguments](#fully-expanded-arguments) ### +### [](#fully-expanded-arguments)[4.2. Fully Expanded Arguments](#fully-expanded-arguments) Fully expanded arguments appear more like standard yaml configuration with name/value pairs. Typically, there will be a `name` key and an `args` key. The `args` key is a map of key value pairs to configure the predicate or filter. @@ -93,15 +227,14 @@ spring: This is the full configuration of the shortcut configuration of the `Cookie` predicate shown above. -[](#gateway-request-predicates-factories)[5. Route Predicate Factories](#gateway-request-predicates-factories) ----------- +## [](#gateway-request-predicates-factories)[5. Route Predicate Factories](#gateway-request-predicates-factories) Spring Cloud Gateway matches routes as part of the Spring WebFlux `HandlerMapping` infrastructure. Spring Cloud Gateway includes many built-in route predicate factories. All of these predicates match on different attributes of the HTTP request. You can combine multiple route predicate factories with logical `and` statements. -### [](#the-after-route-predicate-factory)[5.1. The After Route Predicate Factory](#the-after-route-predicate-factory) ### +### [](#the-after-route-predicate-factory)[5.1. The After Route Predicate Factory](#the-after-route-predicate-factory) The `After` route predicate factory takes one parameter, a `datetime` (which is a java `ZonedDateTime`). This predicate matches requests that happen after the specified datetime. @@ -122,7 +255,7 @@ spring: This route matches any request made after Jan 20, 2017 17:42 Mountain Time (Denver). -### [](#the-before-route-predicate-factory)[5.2. The Before Route Predicate Factory](#the-before-route-predicate-factory) ### +### [](#the-before-route-predicate-factory)[5.2. The Before Route Predicate Factory](#the-before-route-predicate-factory) The `Before` route predicate factory takes one parameter, a `datetime` (which is a java `ZonedDateTime`). This predicate matches requests that happen before the specified `datetime`. @@ -143,7 +276,7 @@ spring: This route matches any request made before Jan 20, 2017 17:42 Mountain Time (Denver). -### [](#the-between-route-predicate-factory)[5.3. The Between Route Predicate Factory](#the-between-route-predicate-factory) ### +### [](#the-between-route-predicate-factory)[5.3. The Between Route Predicate Factory](#the-between-route-predicate-factory) The `Between` route predicate factory takes two parameters, `datetime1` and `datetime2`which are java `ZonedDateTime` objects. This predicate matches requests that happen after `datetime1` and before `datetime2`. @@ -166,7 +299,7 @@ spring: This route matches any request made after Jan 20, 2017 17:42 Mountain Time (Denver) and before Jan 21, 2017 17:42 Mountain Time (Denver). This could be useful for maintenance windows. -### [](#the-cookie-route-predicate-factory)[5.4. The Cookie Route Predicate Factory](#the-cookie-route-predicate-factory) ### +### [](#the-cookie-route-predicate-factory)[5.4. The Cookie Route Predicate Factory](#the-cookie-route-predicate-factory) The `Cookie` route predicate factory takes two parameters, the cookie `name` and a `regexp` (which is a Java regular expression). This predicate matches cookies that have the given name and whose values match the regular expression. @@ -187,7 +320,7 @@ spring: This route matches requests that have a cookie named `chocolate` whose value matches the `ch.p` regular expression. -### [](#the-header-route-predicate-factory)[5.5. The Header Route Predicate Factory](#the-header-route-predicate-factory) ### +### [](#the-header-route-predicate-factory)[5.5. The Header Route Predicate Factory](#the-header-route-predicate-factory) The `Header` route predicate factory takes two parameters, the `header` and a `regexp` (which is a Java regular expression). This predicate matches with a header that has the given name whose value matches the regular expression. @@ -208,7 +341,7 @@ spring: This route matches if the request has a header named `X-Request-Id` whose value matches the `\d+` regular expression (that is, it has a value of one or more digits). -### [](#the-host-route-predicate-factory)[5.6. The Host Route Predicate Factory](#the-host-route-predicate-factory) ### +### [](#the-host-route-predicate-factory)[5.6. The Host Route Predicate Factory](#the-host-route-predicate-factory) The `Host` route predicate factory takes one parameter: a list of host name `patterns`. The pattern is an Ant-style pattern with `.` as the separator. @@ -235,7 +368,7 @@ This route matches if the request has a `Host` header with a value of `www.someh This predicate extracts the URI template variables (such as `sub`, defined in the preceding example) as a map of names and values and places it in the `ServerWebExchange.getAttributes()` with a key defined in `ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE`. Those values are then available for use by [`GatewayFilter` factories](#gateway-route-filters) -### [](#the-method-route-predicate-factory)[5.7. The Method Route Predicate Factory](#the-method-route-predicate-factory) ### +### [](#the-method-route-predicate-factory)[5.7. The Method Route Predicate Factory](#the-method-route-predicate-factory) The `Method` Route Predicate Factory takes a `methods` argument which is one or more parameters: the HTTP methods to match. The following example configures a method route predicate: @@ -255,7 +388,7 @@ spring: This route matches if the request method was a `GET` or a `POST`. -### [](#the-path-route-predicate-factory)[5.8. The Path Route Predicate Factory](#the-path-route-predicate-factory) ### +### [](#the-path-route-predicate-factory)[5.8. The Path Route Predicate Factory](#the-path-route-predicate-factory) The `Path` Route Predicate Factory takes two parameters: a list of Spring `PathMatcher` `patterns` and an optional flag called `matchTrailingSlash` (defaults to `true`). The following example configures a path route predicate: @@ -289,7 +422,7 @@ Map uriVariables = ServerWebExchangeUtils.getPathPredicateVariab String segment = uriVariables.get("segment"); ``` -### [](#the-query-route-predicate-factory)[5.9. The Query Route Predicate Factory](#the-query-route-predicate-factory) ### +### [](#the-query-route-predicate-factory)[5.9. The Query Route Predicate Factory](#the-query-route-predicate-factory) The `Query` route predicate factory takes two parameters: a required `param` and an optional `regexp` (which is a Java regular expression). The following example configures a query route predicate: @@ -324,7 +457,7 @@ spring: The preceding route matches if the request contained a `red` query parameter whose value matched the `gree.` regexp, so `green` and `greet` would match. -### [](#the-remoteaddr-route-predicate-factory)[5.10. The RemoteAddr Route Predicate Factory](#the-remoteaddr-route-predicate-factory) ### +### [](#the-remoteaddr-route-predicate-factory)[5.10. The RemoteAddr Route Predicate Factory](#the-remoteaddr-route-predicate-factory) The `RemoteAddr` route predicate factory takes a list (min size 1) of `sources`, which are CIDR-notation (IPv4 or IPv6) strings, such as `192.168.0.1/16` (where `192.168.0.1` is an IP address and `16` is a subnet mask). The following example configures a RemoteAddr route predicate: @@ -344,7 +477,7 @@ spring: This route matches if the remote address of the request was, for example, `192.168.1.10`. -#### [](#modifying-the-way-remote-addresses-are-resolved)[5.10.1. Modifying the Way Remote Addresses Are Resolved](#modifying-the-way-remote-addresses-are-resolved) #### +#### [](#modifying-the-way-remote-addresses-are-resolved)[5.10.1. Modifying the Way Remote Addresses Are Resolved](#modifying-the-way-remote-addresses-are-resolved) By default, the RemoteAddr route predicate factory uses the remote address from the incoming request. This may not match the actual client IP address if Spring Cloud Gateway sits behind a proxy layer. @@ -396,7 +529,7 @@ RemoteAddressResolver resolver = XForwardedRemoteAddressResolver ) ``` -### [](#the-weight-route-predicate-factory)[5.11. The Weight Route Predicate Factory](#the-weight-route-predicate-factory) ### +### [](#the-weight-route-predicate-factory)[5.11. The Weight Route Predicate Factory](#the-weight-route-predicate-factory) The `Weight` route predicate factory takes two arguments: `group` and `weight` (an int). The weights are calculated per group. The following example configures a weight route predicate: @@ -420,7 +553,7 @@ spring: This route would forward \~80% of traffic to [weighthigh.org](https://weighthigh.org) and \~20% of traffic to [weighlow.org](https://weighlow.org) -### [](#the-xforwarded-remote-addr-route-predicate-factory)[5.12. The XForwarded Remote Addr Route Predicate Factory](#the-xforwarded-remote-addr-route-predicate-factory) ### +### [](#the-xforwarded-remote-addr-route-predicate-factory)[5.12. The XForwarded Remote Addr Route Predicate Factory](#the-xforwarded-remote-addr-route-predicate-factory) The `XForwarded Remote Addr` route predicate factory takes a list (min size 1) of `sources`, which are CIDR-notation (IPv4 or IPv6) strings, such as `192.168.0.1/16` (where `192.168.0.1` is an IP address and `16` is a subnet mask). @@ -447,8 +580,7 @@ spring: This route matches if the `X-Forwarded-For` header contains, for example, `192.168.1.10`. -[](#gatewayfilter-factories)[6. `GatewayFilter` Factories](#gatewayfilter-factories) ----------- +## [](#gatewayfilter-factories)[6. `GatewayFilter` Factories](#gatewayfilter-factories) Route filters allow the modification of the incoming HTTP request or outgoing HTTP response in some manner. Route filters are scoped to a particular route. @@ -457,7 +589,7 @@ Spring Cloud Gateway includes many built-in GatewayFilter Factories. | |For more detailed examples of how to use any of the following filters, take a look at the [unit tests](https://github.com/spring-cloud/spring-cloud-gateway/tree/master/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory).| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#the-addrequestheader-gatewayfilter-factory)[6.1. The `AddRequestHeader` `GatewayFilter` Factory](#the-addrequestheader-gatewayfilter-factory) ### +### [](#the-addrequestheader-gatewayfilter-factory)[6.1. The `AddRequestHeader` `GatewayFilter` Factory](#the-addrequestheader-gatewayfilter-factory) The `AddRequestHeader` `GatewayFilter` factory takes a `name` and `value` parameter. The following example configures an `AddRequestHeader` `GatewayFilter`: @@ -496,7 +628,7 @@ spring: - AddRequestHeader=X-Request-Red, Blue-{segment} ``` -### [](#the-addrequestparameter-gatewayfilter-factory)[6.2. The `AddRequestParameter` `GatewayFilter` Factory](#the-addrequestparameter-gatewayfilter-factory) ### +### [](#the-addrequestparameter-gatewayfilter-factory)[6.2. The `AddRequestParameter` `GatewayFilter` Factory](#the-addrequestparameter-gatewayfilter-factory) The `AddRequestParameter` `GatewayFilter` Factory takes a `name` and `value` parameter. The following example configures an `AddRequestParameter` `GatewayFilter`: @@ -535,7 +667,7 @@ spring: - AddRequestParameter=foo, bar-{segment} ``` -### [](#the-addresponseheader-gatewayfilter-factory)[6.3. The `AddResponseHeader` `GatewayFilter` Factory](#the-addresponseheader-gatewayfilter-factory) ### +### [](#the-addresponseheader-gatewayfilter-factory)[6.3. The `AddResponseHeader` `GatewayFilter` Factory](#the-addresponseheader-gatewayfilter-factory) The `AddResponseHeader` `GatewayFilter` Factory takes a `name` and `value` parameter. The following example configures an `AddResponseHeader` `GatewayFilter`: @@ -574,7 +706,7 @@ spring: - AddResponseHeader=foo, bar-{segment} ``` -### [](#the-deduperesponseheader-gatewayfilter-factory)[6.4. The `DedupeResponseHeader` `GatewayFilter` Factory](#the-deduperesponseheader-gatewayfilter-factory) ### +### [](#the-deduperesponseheader-gatewayfilter-factory)[6.4. The `DedupeResponseHeader` `GatewayFilter` Factory](#the-deduperesponseheader-gatewayfilter-factory) The DedupeResponseHeader GatewayFilter factory takes a `name` parameter and an optional `strategy` parameter. `name` can contain a space-separated list of header names. The following example configures a `DedupeResponseHeader` `GatewayFilter`: @@ -597,7 +729,7 @@ This removes duplicate values of `Access-Control-Allow-Credentials` and `Access- The `DedupeResponseHeader` filter also accepts an optional `strategy` parameter. The accepted values are `RETAIN_FIRST` (default), `RETAIN_LAST`, and `RETAIN_UNIQUE`. -### [](#spring-cloud-circuitbreaker-filter-factory)[6.5. Spring Cloud CircuitBreaker GatewayFilter Factory](#spring-cloud-circuitbreaker-filter-factory) ### +### [](#spring-cloud-circuitbreaker-filter-factory)[6.5. Spring Cloud CircuitBreaker GatewayFilter Factory](#spring-cloud-circuitbreaker-filter-factory) The Spring Cloud CircuitBreaker GatewayFilter factory uses the Spring Cloud CircuitBreaker APIs to wrap Gateway routes in a circuit breaker. Spring Cloud CircuitBreaker supports multiple libraries that can be used with Spring Cloud Gateway. Spring Cloud supports Resilience4J out of the box. @@ -698,7 +830,7 @@ It is added to the `ServerWebExchange` as the `ServerWebExchangeUtils.CIRCUITBRE For the external controller/handler scenario, headers can be added with exception details. You can find more information on doing so in the [FallbackHeaders GatewayFilter Factory section](#fallback-headers). -#### [](#circuit-breaker-status-codes)[6.5.1. Tripping The Circuit Breaker On Status Codes](#circuit-breaker-status-codes) #### +#### [](#circuit-breaker-status-codes)[6.5.1. Tripping The Circuit Breaker On Status Codes](#circuit-breaker-status-codes) In some cases you might want to trip a circuit breaker based on the status code returned from the route it wraps. The circuit breaker config object takes a list of @@ -740,7 +872,7 @@ public RouteLocator routes(RouteLocatorBuilder builder) { } ``` -### [](#fallback-headers)[6.6. The `FallbackHeaders` `GatewayFilter` Factory](#fallback-headers) ### +### [](#fallback-headers)[6.6. The `FallbackHeaders` `GatewayFilter` Factory](#fallback-headers) The `FallbackHeaders` factory lets you add Spring Cloud CircuitBreaker execution exception details in the headers of a request forwarded to a `fallbackUri` in an external application, as in the following scenario: @@ -785,7 +917,7 @@ You can overwrite the names of the headers in the configuration by setting the v For more information on circuit breakers and the gateway see the [Spring Cloud CircuitBreaker Factory section](#spring-cloud-circuitbreaker-filter-factory). -### [](#the-maprequestheader-gatewayfilter-factory)[6.7. The `MapRequestHeader` `GatewayFilter` Factory](#the-maprequestheader-gatewayfilter-factory) ### +### [](#the-maprequestheader-gatewayfilter-factory)[6.7. The `MapRequestHeader` `GatewayFilter` Factory](#the-maprequestheader-gatewayfilter-factory) The `MapRequestHeader` `GatewayFilter` factory takes `fromHeader` and `toHeader` parameters. It creates a new named header (`toHeader`), and the value is extracted out of an existing named header (`fromHeader`) from the incoming http request. @@ -808,7 +940,7 @@ spring: This adds `X-Request-Red:` header to the downstream request with updated values from the incoming HTTP request’s `Blue` header. -### [](#the-prefixpath-gatewayfilter-factory)[6.8. The `PrefixPath` `GatewayFilter` Factory](#the-prefixpath-gatewayfilter-factory) ### +### [](#the-prefixpath-gatewayfilter-factory)[6.8. The `PrefixPath` `GatewayFilter` Factory](#the-prefixpath-gatewayfilter-factory) The `PrefixPath` `GatewayFilter` factory takes a single `prefix` parameter. The following example configures a `PrefixPath` `GatewayFilter`: @@ -829,7 +961,7 @@ spring: This will prefix `/mypath` to the path of all matching requests. So a request to `/hello` would be sent to `/mypath/hello`. -### [](#the-preservehostheader-gatewayfilter-factory)[6.9. The `PreserveHostHeader` `GatewayFilter` Factory](#the-preservehostheader-gatewayfilter-factory) ### +### [](#the-preservehostheader-gatewayfilter-factory)[6.9. The `PreserveHostHeader` `GatewayFilter` Factory](#the-preservehostheader-gatewayfilter-factory) The `PreserveHostHeader` `GatewayFilter` factory has no parameters. This filter sets a request attribute that the routing filter inspects to determine if the original host header should be sent, rather than the host header determined by the HTTP client. @@ -848,7 +980,7 @@ spring: - PreserveHostHeader ``` -### [](#the-requestratelimiter-gatewayfilter-factory)[6.10. The `RequestRateLimiter` `GatewayFilter` Factory](#the-requestratelimiter-gatewayfilter-factory) ### +### [](#the-requestratelimiter-gatewayfilter-factory)[6.10. The `RequestRateLimiter` `GatewayFilter` Factory](#the-requestratelimiter-gatewayfilter-factory) The `RequestRateLimiter` `GatewayFilter` factory uses a `RateLimiter` implementation to determine if the current request is allowed to proceed. If it is not, a status of `HTTP 429 - Too Many Requests` (by default) is returned. @@ -877,7 +1009,7 @@ You can adjust this behavior by setting the `spring.cloud.gateway.filter.request | |The `RequestRateLimiter` is not configurable with the "shortcut" notation. The following example below is *invalid*:

Example 32. application.properties

```
# INVALID SHORTCUT CONFIGURATION
spring.cloud.gateway.routes[0].filters[0]=RequestRateLimiter=2, 2, #{@userkeyresolver}
```| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -#### [](#the-redis-ratelimiter)[6.10.1. The Redis `RateLimiter`](#the-redis-ratelimiter) #### +#### [](#the-redis-ratelimiter)[6.10.1. The Redis `RateLimiter`](#the-redis-ratelimiter) The Redis implementation is based off of work done at [Stripe](https://stripe.com/blog/rate-limiters). It requires the use of the `spring-boot-starter-data-redis-reactive` Spring Boot starter. @@ -952,7 +1084,7 @@ spring: key-resolver: "#{@userKeyResolver}" ``` -### [](#the-redirectto-gatewayfilter-factory)[6.11. The `RedirectTo` `GatewayFilter` Factory](#the-redirectto-gatewayfilter-factory) ### +### [](#the-redirectto-gatewayfilter-factory)[6.11. The `RedirectTo` `GatewayFilter` Factory](#the-redirectto-gatewayfilter-factory) The `RedirectTo` `GatewayFilter` factory takes two parameters, `status` and `url`. The `status` parameter should be a 300 series redirect HTTP code, such as 301. @@ -976,7 +1108,7 @@ spring: This will send a status 302 with a `Location:https://acme.org` header to perform a redirect. -### [](#the-removerequestheader-gatewayfilter-factory)[6.12. The `RemoveRequestHeader` GatewayFilter Factory](#the-removerequestheader-gatewayfilter-factory) ### +### [](#the-removerequestheader-gatewayfilter-factory)[6.12. The `RemoveRequestHeader` GatewayFilter Factory](#the-removerequestheader-gatewayfilter-factory) The `RemoveRequestHeader` `GatewayFilter` factory takes a `name` parameter. It is the name of the header to be removed. @@ -997,7 +1129,7 @@ spring: This removes the `X-Request-Foo` header before it is sent downstream. -### [](#removeresponseheader-gatewayfilter-factory)[6.13. `RemoveResponseHeader` `GatewayFilter` Factory](#removeresponseheader-gatewayfilter-factory) ### +### [](#removeresponseheader-gatewayfilter-factory)[6.13. `RemoveResponseHeader` `GatewayFilter` Factory](#removeresponseheader-gatewayfilter-factory) The `RemoveResponseHeader` `GatewayFilter` factory takes a `name` parameter. It is the name of the header to be removed. @@ -1021,7 +1153,7 @@ This will remove the `X-Response-Foo` header from the response before it is retu To remove any kind of sensitive header, you should configure this filter for any routes for which you may want to do so. In addition, you can configure this filter once by using `spring.cloud.gateway.default-filters` and have it applied to all routes. -### [](#the-removerequestparameter-gatewayfilter-factory)[6.14. The `RemoveRequestParameter` `GatewayFilter` Factory](#the-removerequestparameter-gatewayfilter-factory) ### +### [](#the-removerequestparameter-gatewayfilter-factory)[6.14. The `RemoveRequestParameter` `GatewayFilter` Factory](#the-removerequestparameter-gatewayfilter-factory) The `RemoveRequestParameter` `GatewayFilter` factory takes a `name` parameter. It is the name of the query parameter to be removed. @@ -1042,7 +1174,7 @@ spring: This will remove the `red` parameter before it is sent downstream. -### [](#the-rewritepath-gatewayfilter-factory)[6.15. The `RewritePath` `GatewayFilter` Factory](#the-rewritepath-gatewayfilter-factory) ### +### [](#the-rewritepath-gatewayfilter-factory)[6.15. The `RewritePath` `GatewayFilter` Factory](#the-rewritepath-gatewayfilter-factory) The `RewritePath` `GatewayFilter` factory takes a path `regexp` parameter and a `replacement` parameter. This uses Java regular expressions for a flexible way to rewrite the request path. @@ -1065,7 +1197,7 @@ spring: For a request path of `/red/blue`, this sets the path to `/blue` before making the downstream request. Note that the `$` should be replaced with `$\` because of the YAML specification. -### [](#rewritelocationresponseheader-gatewayfilter-factory)[6.16. `RewriteLocationResponseHeader` `GatewayFilter` Factory](#rewritelocationresponseheader-gatewayfilter-factory) ### +### [](#rewritelocationresponseheader-gatewayfilter-factory)[6.16. `RewriteLocationResponseHeader` `GatewayFilter` Factory](#rewritelocationresponseheader-gatewayfilter-factory) The `RewriteLocationResponseHeader` `GatewayFilter` factory modifies the value of the `Location` response header, usually to get rid of backend-specific details. It takes `stripVersionMode`, `locationHeaderName`, `hostValue`, and `protocolsRegex` parameters. @@ -1101,7 +1233,7 @@ The `protocolsRegex` parameter must be a valid regex `String`, against which the If it is not matched, the filter does nothing. The default is `http|https|ftp|ftps`. -### [](#the-rewriteresponseheader-gatewayfilter-factory)[6.17. The `RewriteResponseHeader` `GatewayFilter` Factory](#the-rewriteresponseheader-gatewayfilter-factory) ### +### [](#the-rewriteresponseheader-gatewayfilter-factory)[6.17. The `RewriteResponseHeader` `GatewayFilter` Factory](#the-rewriteresponseheader-gatewayfilter-factory) The `RewriteResponseHeader` `GatewayFilter` factory takes `name`, `regexp`, and `replacement` parameters. It uses Java regular expressions for a flexible way to rewrite the response header value. @@ -1123,7 +1255,7 @@ spring: For a header value of `/42?user=ford&password=omg!what&flag=true`, it is set to `/42?user=ford&password=***&flag=true` after making the downstream request. You must use `$\` to mean `$` because of the YAML specification. -### [](#the-savesession-gatewayfilter-factory)[6.18. The `SaveSession` `GatewayFilter` Factory](#the-savesession-gatewayfilter-factory) ### +### [](#the-savesession-gatewayfilter-factory)[6.18. The `SaveSession` `GatewayFilter` Factory](#the-savesession-gatewayfilter-factory) The `SaveSession` `GatewayFilter` factory forces a `WebSession::save` operation *before* forwarding the call downstream. This is of particular use when using something like [Spring Session](https://projects.spring.io/spring-session/) with a lazy data store and you need to ensure the session state has been saved before making the forwarded call. @@ -1146,7 +1278,7 @@ spring: If you integrate [Spring Security](https://projects.spring.io/spring-security/) with Spring Session and want to ensure security details have been forwarded to the remote process, this is critical. -### [](#the-secureheaders-gatewayfilter-factory)[6.19. The `SecureHeaders` `GatewayFilter` Factory](#the-secureheaders-gatewayfilter-factory) ### +### [](#the-secureheaders-gatewayfilter-factory)[6.19. The `SecureHeaders` `GatewayFilter` Factory](#the-secureheaders-gatewayfilter-factory) The `SecureHeaders` `GatewayFilter` factory adds a number of headers to the response, per the recommendation made in [this blog post](https://blog.appcanary.com/2017/http-security-headers.html). @@ -1197,7 +1329,7 @@ spring.cloud.gateway.filter.secure-headers.disable=x-frame-options,strict-transp | |The lowercase full name of the secure header needs to be used to disable it..| |---|-----------------------------------------------------------------------------| -### [](#the-setpath-gatewayfilter-factory)[6.20. The `SetPath` `GatewayFilter` Factory](#the-setpath-gatewayfilter-factory) ### +### [](#the-setpath-gatewayfilter-factory)[6.20. The `SetPath` `GatewayFilter` Factory](#the-setpath-gatewayfilter-factory) The `SetPath` `GatewayFilter` factory takes a path `template` parameter. It offers a simple way to manipulate the request path by allowing templated segments of the path. @@ -1222,7 +1354,7 @@ spring: For a request path of `/red/blue`, this sets the path to `/blue` before making the downstream request. -### [](#the-setrequestheader-gatewayfilter-factory)[6.21. The `SetRequestHeader` `GatewayFilter` Factory](#the-setrequestheader-gatewayfilter-factory) ### +### [](#the-setrequestheader-gatewayfilter-factory)[6.21. The `SetRequestHeader` `GatewayFilter` Factory](#the-setrequestheader-gatewayfilter-factory) The `SetRequestHeader` `GatewayFilter` factory takes `name` and `value` parameters. The following listing configures a `SetRequestHeader` `GatewayFilter`: @@ -1262,7 +1394,7 @@ spring: - SetRequestHeader=foo, bar-{segment} ``` -### [](#the-setresponseheader-gatewayfilter-factory)[6.22. The `SetResponseHeader` `GatewayFilter` Factory](#the-setresponseheader-gatewayfilter-factory) ### +### [](#the-setresponseheader-gatewayfilter-factory)[6.22. The `SetResponseHeader` `GatewayFilter` Factory](#the-setresponseheader-gatewayfilter-factory) The `SetResponseHeader` `GatewayFilter` factory takes `name` and `value` parameters. The following listing configures a `SetResponseHeader` `GatewayFilter`: @@ -1302,7 +1434,7 @@ spring: - SetResponseHeader=foo, bar-{segment} ``` -### [](#the-setstatus-gatewayfilter-factory)[6.23. The `SetStatus` `GatewayFilter` Factory](#the-setstatus-gatewayfilter-factory) ### +### [](#the-setstatus-gatewayfilter-factory)[6.23. The `SetStatus` `GatewayFilter` Factory](#the-setstatus-gatewayfilter-factory) The `SetStatus` `GatewayFilter` factory takes a single parameter, `status`. It must be a valid Spring `HttpStatus`. @@ -1341,7 +1473,7 @@ spring: original-status-header-name: original-http-status ``` -### [](#the-stripprefix-gatewayfilter-factory)[6.24. The `StripPrefix` `GatewayFilter` Factory](#the-stripprefix-gatewayfilter-factory) ### +### [](#the-stripprefix-gatewayfilter-factory)[6.24. The `StripPrefix` `GatewayFilter` Factory](#the-stripprefix-gatewayfilter-factory) The `StripPrefix` `GatewayFilter` factory takes one parameter, `parts`. The `parts` parameter indicates the number of parts in the path to strip from the request before sending it downstream. @@ -1364,7 +1496,7 @@ spring: When a request is made through the gateway to `/name/blue/red`, the request made to `nameservice` looks like `[nameservice/red](https://nameservice/red)`. -### [](#the-retry-gatewayfilter-factory)[6.25. The Retry `GatewayFilter` Factory](#the-retry-gatewayfilter-factory) ### +### [](#the-retry-gatewayfilter-factory)[6.25. The Retry `GatewayFilter` Factory](#the-retry-gatewayfilter-factory) The `Retry` `GatewayFilter` factory supports the following parameters: @@ -1458,7 +1590,7 @@ spring: - Retry=3,INTERNAL_SERVER_ERROR,GET,10ms,50ms,2,false ``` -### [](#the-requestsize-gatewayfilter-factory)[6.26. The `RequestSize` `GatewayFilter` Factory](#the-requestsize-gatewayfilter-factory) ### +### [](#the-requestsize-gatewayfilter-factory)[6.26. The `RequestSize` `GatewayFilter` Factory](#the-requestsize-gatewayfilter-factory) When the request size is greater than the permissible limit, the `RequestSize` `GatewayFilter` factory can restrict a request from reaching the downstream service. The filter takes a `maxSize` parameter. @@ -1492,7 +1624,7 @@ errorMessage : Request size is larger than permissible limit. Request size is 6. | |The default request size is set to five MB if not provided as a filter argument in the route definition.| |---|--------------------------------------------------------------------------------------------------------| -### [](#the-setrequesthostheader-gatewayfilter-factory)[6.27. The `SetRequestHostHeader` `GatewayFilter` Factory](#the-setrequesthostheader-gatewayfilter-factory) ### +### [](#the-setrequesthostheader-gatewayfilter-factory)[6.27. The `SetRequestHostHeader` `GatewayFilter` Factory](#the-setrequesthostheader-gatewayfilter-factory) There are certain situation when the host header may need to be overridden. In this situation, the `SetRequestHostHeader` `GatewayFilter` factory can replace the existing host header with a specified vaue. The filter takes a `host` parameter. @@ -1517,7 +1649,7 @@ spring: The `SetRequestHostHeader` `GatewayFilter` factory replaces the value of the host header with `example.org`. -### [](#modify-a-request-body-gatewayfilter-factory)[6.28. Modify a Request Body `GatewayFilter` Factory](#modify-a-request-body-gatewayfilter-factory) ### +### [](#modify-a-request-body-gatewayfilter-factory)[6.28. Modify a Request Body `GatewayFilter` Factory](#modify-a-request-body-gatewayfilter-factory) You can use the `ModifyRequestBody` filter filter to modify the request body before it is sent downstream by the gateway. @@ -1559,7 +1691,7 @@ static class Hello { | |if the request has no body, the `RewriteFilter` will be passed `null`. `Mono.empty()` should be returned to assign a missing body in the request.| |---|-------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#modify-a-response-body-gatewayfilter-factory)[6.29. Modify a Response Body `GatewayFilter` Factory](#modify-a-response-body-gatewayfilter-factory) ### +### [](#modify-a-response-body-gatewayfilter-factory)[6.29. Modify a Response Body `GatewayFilter` Factory](#modify-a-response-body-gatewayfilter-factory) You can use the `ModifyResponseBody` filter to modify the response body before it is sent back to the client. @@ -1583,7 +1715,7 @@ public RouteLocator routes(RouteLocatorBuilder builder) { | |if the response has no body, the `RewriteFilter` will be passed `null`. `Mono.empty()` should be returned to assign a missing body in the response.| |---|---------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#token-relay-gatewayfilter-factory)[6.30. Token Relay `GatewayFilter` Factory](#token-relay-gatewayfilter-factory) ### +### [](#token-relay-gatewayfilter-factory)[6.30. Token Relay `GatewayFilter` Factory](#token-relay-gatewayfilter-factory) A Token Relay is where an OAuth2 consumer acts as a Client and forwards the incoming token to outgoing resource requests. The @@ -1643,7 +1775,7 @@ For a full working sample see [this project](https://github.com/spring-cloud-sam | |The default implementation of `ReactiveOAuth2AuthorizedClientService` used by `TokenRelayGatewayFilterFactory`uses an in-memory data store. You will need to provide your own implementation `ReactiveOAuth2AuthorizedClientService`if you need a more robust solution.| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#the-cacherequestbody-gatewayfilter-factory)[6.31. The `CacheRequestBody` `GatewayFilter` Factory](#the-cacherequestbody-gatewayfilter-factory) ### +### [](#the-cacherequestbody-gatewayfilter-factory)[6.31. The `CacheRequestBody` `GatewayFilter` Factory](#the-cacherequestbody-gatewayfilter-factory) There are certain situation need to read body.Since the request body stream can only be read once, we need to cache the request body. You can use the `CacheRequestBody` filter to cache request body before it send to the downstream and get body from exchagne attribute. @@ -1683,7 +1815,7 @@ spring: | |This filter only works with http request (including https).| |---|-----------------------------------------------------------| -### [](#default-filters)[6.32. Default Filters](#default-filters) ### +### [](#default-filters)[6.32. Default Filters](#default-filters) To add a filter and apply it to all routes, you can use `spring.cloud.gateway.default-filters`. This property takes a list of filters. @@ -1700,8 +1832,7 @@ spring: - PrefixPath=/httpbin ``` -[](#global-filters)[7. Global Filters](#global-filters) ----------- +## [](#global-filters)[7. Global Filters](#global-filters) The `GlobalFilter` interface has the same signature as `GatewayFilter`. These are special filters that are conditionally applied to all routes. @@ -1709,7 +1840,7 @@ These are special filters that are conditionally applied to all routes. | |This interface and its usage are subject to change in future milestone releases.| |---|--------------------------------------------------------------------------------| -### [](#gateway-combined-global-filter-and-gatewayfilter-ordering)[7.1. Combined Global Filter and `GatewayFilter` Ordering](#gateway-combined-global-filter-and-gatewayfilter-ordering) ### +### [](#gateway-combined-global-filter-and-gatewayfilter-ordering)[7.1. Combined Global Filter and `GatewayFilter` Ordering](#gateway-combined-global-filter-and-gatewayfilter-ordering) When a request matches a route, the filtering web handler adds all instances of `GlobalFilter` and all route-specific instances of `GatewayFilter` to a filter chain. This combined filter chain is sorted by the `org.springframework.core.Ordered` interface, which you can set by implementing the `getOrder()` method. @@ -1741,14 +1872,14 @@ public class CustomGlobalFilter implements GlobalFilter, Ordered { } ``` -### [](#forward-routing-filter)[7.2. Forward Routing Filter](#forward-routing-filter) ### +### [](#forward-routing-filter)[7.2. Forward Routing Filter](#forward-routing-filter) The `ForwardRoutingFilter` looks for a URI in the exchange attribute `ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR`. If the URL has a `forward` scheme (such as `forward:///localendpoint`), it uses the Spring `DispatcherHandler` to handle the request. The path part of the request URL is overridden with the path in the forward URL. The unmodified original URL is appended to the list in the `ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR` attribute. -### [](#reactive-loadbalancer-client-filter)[7.3. The `ReactiveLoadBalancerClientFilter`](#reactive-loadbalancer-client-filter) ### +### [](#reactive-loadbalancer-client-filter)[7.3. The `ReactiveLoadBalancerClientFilter`](#reactive-loadbalancer-client-filter) The `ReactiveLoadBalancerClientFilter` looks for a URI in the exchange attribute named `ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR`. If the URL has a `lb` scheme (such as `lb://myservice`), it uses the Spring Cloud `ReactorLoadBalancer` to resolve the name (`myservice` in this example) to an actual host and port and replaces the URI in the same attribute. @@ -1779,20 +1910,20 @@ spring: | |Gateway supports all the LoadBalancer features. You can read more about them in the [Spring Cloud Commons documentation](https://docs.spring.io/spring-cloud-commons/docs/current/reference/html/#spring-cloud-loadbalancer).| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#the-netty-routing-filter)[7.4. The Netty Routing Filter](#the-netty-routing-filter) ### +### [](#the-netty-routing-filter)[7.4. The Netty Routing Filter](#the-netty-routing-filter) The Netty routing filter runs if the URL located in the `ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR` exchange attribute has a `http` or `https` scheme. It uses the Netty `HttpClient` to make the downstream proxy request. The response is put in the `ServerWebExchangeUtils.CLIENT_RESPONSE_ATTR` exchange attribute for use in a later filter. (There is also an experimental `WebClientHttpRoutingFilter` that performs the same function but does not require Netty.) -### [](#the-netty-write-response-filter)[7.5. The Netty Write Response Filter](#the-netty-write-response-filter) ### +### [](#the-netty-write-response-filter)[7.5. The Netty Write Response Filter](#the-netty-write-response-filter) The `NettyWriteResponseFilter` runs if there is a Netty `HttpClientResponse` in the `ServerWebExchangeUtils.CLIENT_RESPONSE_ATTR` exchange attribute. It runs after all other filters have completed and writes the proxy response back to the gateway client response. (There is also an experimental `WebClientWriteResponseFilter` that performs the same function but does not require Netty.) -### [](#the-routetorequesturl-filter)[7.6. The `RouteToRequestUrl` Filter](#the-routetorequesturl-filter) ### +### [](#the-routetorequesturl-filter)[7.6. The `RouteToRequestUrl` Filter](#the-routetorequesturl-filter) If there is a `Route` object in the `ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR` exchange attribute, the `RouteToRequestUrlFilter` runs. It creates a new URI, based off of the request URI but updated with the URI attribute of the `Route` object. @@ -1800,7 +1931,7 @@ The new URI is placed in the `ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR` e If the URI has a scheme prefix, such as `lb:ws://serviceid`, the `lb` scheme is stripped from the URI and placed in the `ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR` for use later in the filter chain. -### [](#the-websocket-routing-filter)[7.7. The Websocket Routing Filter](#the-websocket-routing-filter) ### +### [](#the-websocket-routing-filter)[7.7. The Websocket Routing Filter](#the-websocket-routing-filter) If the URL located in the `ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR` exchange attribute has a `ws` or `wss` scheme, the websocket routing filter runs. It uses the Spring WebSocket infrastructure to forward the websocket request downstream. @@ -1830,7 +1961,7 @@ spring: - Path=/websocket/** ``` -### [](#the-gateway-metrics-filter)[7.8. The Gateway Metrics Filter](#the-gateway-metrics-filter) ### +### [](#the-gateway-metrics-filter)[7.8. The Gateway Metrics Filter](#the-gateway-metrics-filter) To enable gateway metrics, add spring-boot-starter-actuator as a project dependency. Then, by default, the gateway metrics filter runs as long as the property `spring.cloud.gateway.metrics.enabled` is not set to `false`. This filter adds a timer metric named `spring.cloud.gateway.requests` with the following tags: @@ -1855,7 +1986,7 @@ These metrics are then available to be scraped from `/actuator/metrics/spring.cl | |To enable the prometheus endpoint, add `micrometer-registry-prometheus` as a project dependency.| |---|------------------------------------------------------------------------------------------------| -### [](#marking-an-exchange-as-routed)[7.9. Marking An Exchange As Routed](#marking-an-exchange-as-routed) ### +### [](#marking-an-exchange-as-routed)[7.9. Marking An Exchange As Routed](#marking-an-exchange-as-routed) After the gateway has routed a `ServerWebExchange`, it marks that exchange as “routed” by adding `gatewayAlreadyRouted`to the exchange attributes. Once a request has been marked as routed, other routing filters will not route the request again, essentially skipping the filter. There are convenience methods that you can use to mark an exchange as routed @@ -1865,16 +1996,15 @@ or check if an exchange has already been routed. * `ServerWebExchangeUtils.setAlreadyRouted` takes a `ServerWebExchange` object and marks it as “routed”. -[](#httpheadersfilters)[8. HttpHeadersFilters](#httpheadersfilters) ----------- +## [](#httpheadersfilters)[8. HttpHeadersFilters](#httpheadersfilters) HttpHeadersFilters are applied to requests before sending them downstream, such as in the `NettyRoutingFilter`. -### [](#forwarded-headers-filter)[8.1. Forwarded Headers Filter](#forwarded-headers-filter) ### +### [](#forwarded-headers-filter)[8.1. Forwarded Headers Filter](#forwarded-headers-filter) The `Forwarded` Headers Filter creates a `Forwarded` header to send to the downstream service. It adds the `Host` header, scheme and port of the current request to any existing `Forwarded` header. -### [](#removehopbyhop-headers-filter)[8.2. RemoveHopByHop Headers Filter](#removehopbyhop-headers-filter) ### +### [](#removehopbyhop-headers-filter)[8.2. RemoveHopByHop Headers Filter](#removehopbyhop-headers-filter) The `RemoveHopByHop` Headers Filter removes headers from forwarded requests. The default list of headers that is removed comes from the [IETF](https://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-14#section-7.1.3). @@ -1898,7 +2028,7 @@ The default removed headers are: To change this, set the `spring.cloud.gateway.filter.remove-hop-by-hop.headers` property to the list of header names to remove. -### [](#xforwarded-headers-filter)[8.3. XForwarded Headers Filter](#xforwarded-headers-filter) ### +### [](#xforwarded-headers-filter)[8.3. XForwarded Headers Filter](#xforwarded-headers-filter) The `XForwarded` Headers Filter creates various a `X-Forwarded-*` headers to send to the downstream service. It users the `Host` header, scheme, port and path of the current request to create the various headers. @@ -1926,8 +2056,7 @@ Appending multiple headers can be controlled by the following boolean properties * `spring.cloud.gateway.x-forwarded.prefix-append` -[](#tls-and-ssl)[9. TLS and SSL](#tls-and-ssl) ----------- +## [](#tls-and-ssl)[9. TLS and SSL](#tls-and-ssl) The gateway can listen for requests on HTTPS by following the usual Spring server configuration. The following example shows how to do so: @@ -1976,7 +2105,7 @@ spring: If the Spring Cloud Gateway is not provisioned with trusted certificates, the default trust store is used (which you can override by setting the `javax.net.ssl.trustStore` system property). -### [](#tls-handshake)[9.1. TLS Handshake](#tls-handshake) ### +### [](#tls-handshake)[9.1. TLS Handshake](#tls-handshake) The gateway maintains a client pool that it uses to route to backends. When communicating over HTTPS, the client initiates a TLS handshake. @@ -1996,8 +2125,7 @@ spring: close-notify-read-timeout-millis: 0 ``` -[](#configuration)[10. Configuration](#configuration) ----------- +## [](#configuration)[10. Configuration](#configuration) Configuration for Spring Cloud Gateway is driven by a collection of `RouteDefinitionLocator` instances. The following listing shows the definition of the `RouteDefinitionLocator` interface: @@ -2036,12 +2164,11 @@ spring: For some usages of the gateway, properties are adequate, but some production use cases benefit from loading configuration from an external source, such as a database. Future milestone versions will have `RouteDefinitionLocator` implementations based off of Spring Data Repositories, such as Redis, MongoDB, and Cassandra. -### [](#routedefinition-metrics)[10.1. RouteDefinition Metrics](#routedefinition-metrics) ### +### [](#routedefinition-metrics)[10.1. RouteDefinition Metrics](#routedefinition-metrics) To enable `RouteDefinition` metrics, add spring-boot-starter-actuator as a project dependency. Then, by default, the metrics will be available as long as the property `spring.cloud.gateway.metrics.enabled` is set to `true`. A gauge metric named `spring.cloud.gateway.routes.count` will be added, whose value is the number of `RouteDefinitions`. This metric will be available from `/actuator/metrics/spring.cloud.gateway.routes.count`. -[](#route-metadata-configuration)[11. Route Metadata Configuration](#route-metadata-configuration) ----------- +## [](#route-metadata-configuration)[11. Route Metadata Configuration](#route-metadata-configuration) You can configure additional parameters for each route by using metadata, as follows: @@ -2071,12 +2198,11 @@ route.getMetadata(); route.getMetadata(someKey); ``` -[](#http-timeouts-configuration)[12. Http timeouts configuration](#http-timeouts-configuration) ----------- +## [](#http-timeouts-configuration)[12. Http timeouts configuration](#http-timeouts-configuration) Http timeouts (response and connect) can be configured for all routes and overridden for each specific route. -### [](#global-timeouts)[12.1. Global timeouts](#global-timeouts) ### +### [](#global-timeouts)[12.1. Global timeouts](#global-timeouts) To configure Global http timeouts: `connect-timeout` must be specified in milliseconds. @@ -2093,7 +2219,7 @@ spring: response-timeout: 5s ``` -### [](#per-route-timeouts)[12.2. Per-route timeouts](#per-route-timeouts) ### +### [](#per-route-timeouts)[12.2. Per-route timeouts](#per-route-timeouts) To configure per-route timeouts: `connect-timeout` must be specified in milliseconds. @@ -2146,7 +2272,7 @@ A per-route `response-timeout` with a negative value will disable the global `re response-timeout: -1 ``` -### [](#fluent-java-routes-api)[12.3. Fluent Java Routes API](#fluent-java-routes-api) ### +### [](#fluent-java-routes-api)[12.3. Fluent Java Routes API](#fluent-java-routes-api) To allow for simple configuration in Java, the `RouteLocatorBuilder` bean includes a fluent API. The following listing shows how it works: @@ -2186,13 +2312,13 @@ This style also allows for more custom predicate assertions. The predicates defined by `RouteDefinitionLocator` beans are combined using logical `and`. By using the fluent Java API, you can use the `and()`, `or()`, and `negate()` operators on the `Predicate` class. -### [](#the-discoveryclient-route-definition-locator)[12.4. The `DiscoveryClient` Route Definition Locator](#the-discoveryclient-route-definition-locator) ### +### [](#the-discoveryclient-route-definition-locator)[12.4. The `DiscoveryClient` Route Definition Locator](#the-discoveryclient-route-definition-locator) You can configure the gateway to create routes based on services registered with a `DiscoveryClient` compatible service registry. To enable this, set `spring.cloud.gateway.discovery.locator.enabled=true` and make sure a `DiscoveryClient` implementation (such as Netflix Eureka, Consul, or Zookeeper) is on the classpath and enabled. -#### [](#configuring-predicates-and-filters-for-discoveryclient-routes)[12.4.1. Configuring Predicates and Filters For `DiscoveryClient` Routes](#configuring-predicates-and-filters-for-discoveryclient-routes) #### +#### [](#configuring-predicates-and-filters-for-discoveryclient-routes)[12.4.1. Configuring Predicates and Filters For `DiscoveryClient` Routes](#configuring-predicates-and-filters-for-discoveryclient-routes) By default, the gateway defines a single predicate and filter for routes created with a `DiscoveryClient`. @@ -2220,8 +2346,7 @@ spring.cloud.gateway.discovery.locator.filters[1].args[regexp]: "'/' + serviceId spring.cloud.gateway.discovery.locator.filters[1].args[replacement]: "'/${remaining}'" ``` -[](#reactor-netty-access-logs)[13. Reactor Netty Access Logs](#reactor-netty-access-logs) ----------- +## [](#reactor-netty-access-logs)[13. Reactor Netty Access Logs](#reactor-netty-access-logs) To enable Reactor Netty access logs, set `-Dreactor.netty.http.server.accessLogEnabled=true`. @@ -2248,8 +2373,7 @@ Example 70. logback.xml ``` -[](#cors-configuration)[14. CORS Configuration](#cors-configuration) ----------- +## [](#cors-configuration)[14. CORS Configuration](#cors-configuration) You can configure the gateway to control CORS behavior. The “global” CORS configuration is a map of URL patterns to [Spring Framework `CorsConfiguration`](https://docs.spring.io/spring/docs/5.0.x/javadoc-api/org/springframework/web/cors/CorsConfiguration.html). The following example configures CORS: @@ -2273,8 +2397,7 @@ In the preceding example, CORS requests are allowed from requests that originate To provide the same CORS configuration to requests that are not handled by some gateway route predicate, set the `spring.cloud.gateway.globalcors.add-to-simple-url-handler-mapping` property to `true`. This is useful when you try to support CORS preflight requests and your route predicate does not evalute to `true` because the HTTP method is `options`. -[](#actuator-api)[15. Actuator API](#actuator-api) ----------- +## [](#actuator-api)[15. Actuator API](#actuator-api) The `/gateway` actuator endpoint lets you monitor and interact with a Spring Cloud Gateway application. To be remotely accessible, the endpoint has to be [enabled](https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-endpoints.html#production-ready-endpoints-enabling-endpoints) and [exposed over HTTP or JMX](https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-endpoints.html#production-ready-endpoints-exposing-endpoints) in the application properties. @@ -2287,7 +2410,7 @@ management.endpoint.gateway.enabled=true # default value management.endpoints.web.exposure.include=gateway ``` -### [](#verbose-actuator-format)[15.1. Verbose Actuator Format](#verbose-actuator-format) ### +### [](#verbose-actuator-format)[15.1. Verbose Actuator Format](#verbose-actuator-format) A new, more verbose format has been added to Spring Cloud Gateway. It adds more detail to each route, letting you view the predicates and filters associated with each route along with any configuration that is available. @@ -2319,7 +2442,7 @@ spring.cloud.gateway.actuator.verbose.enabled=false This will default to `true` in a future release. -### [](#retrieving-route-filters)[15.2. Retrieving Route Filters](#retrieving-route-filters) ### +### [](#retrieving-route-filters)[15.2. Retrieving Route Filters](#retrieving-route-filters) This section details how to retrieve route filters, including: @@ -2327,7 +2450,7 @@ This section details how to retrieve route filters, including: * [[gateway-route-filters]](#gateway-route-filters) -#### [](#gateway-global-filters)[15.2.1. Global Filters](#gateway-global-filters) #### +#### [](#gateway-global-filters)[15.2.1. Global Filters](#gateway-global-filters) To retrieve the [global filters](#global-filters) applied to all routes, make a `GET` request to `/actuator/gateway/globalfilters`. The resulting response is similar to the following: @@ -2347,7 +2470,7 @@ To retrieve the [global filters](#global-filters) applied to all routes, make a The response contains the details of the global filters that are in place. For each global filter, there is a string representation of the filter object (for example, `org.spring[[email protected]](/cdn-cgi/l/email-protection)77856cc5`) and the corresponding [order](#gateway-combined-global-filter-and-gatewayfilter-ordering) in the filter chain.} -#### [](#gateway-route-filters)[15.2.2. Route Filters](#gateway-route-filters) #### +#### [](#gateway-route-filters)[15.2.2. Route Filters](#gateway-route-filters) To retrieve the [`GatewayFilter` factories](#gatewayfilter-factories) applied to routes, make a `GET` request to `/actuator/gateway/routefilters`. The resulting response is similar to the following: @@ -2364,12 +2487,12 @@ The response contains the details of the `GatewayFilter` factories applied to an For each factory there is a string representation of the corresponding object (for example, `[[[email protected]](/cdn-cgi/l/email-protection) configClass = Object]`). Note that the `null` value is due to an incomplete implementation of the endpoint controller, because it tries to set the order of the object in the filter chain, which does not apply to a `GatewayFilter` factory object. -### [](#refreshing-the-route-cache)[15.3. Refreshing the Route Cache](#refreshing-the-route-cache) ### +### [](#refreshing-the-route-cache)[15.3. Refreshing the Route Cache](#refreshing-the-route-cache) To clear the routes cache, make a `POST` request to `/actuator/gateway/refresh`. The request returns a 200 without a response body. -### [](#retrieving-the-routes-defined-in-the-gateway)[15.4. Retrieving the Routes Defined in the Gateway](#retrieving-the-routes-defined-in-the-gateway) ### +### [](#retrieving-the-routes-defined-in-the-gateway)[15.4. Retrieving the Routes Defined in the Gateway](#retrieving-the-routes-defined-in-the-gateway) To retrieve the routes defined in the gateway, make a `GET` request to `/actuator/gateway/routes`. The resulting response is similar to the following: @@ -2405,7 +2528,7 @@ The following table describes the structure of each element (each is a route) of | `route_object.filters` |Array |The [`GatewayFilter` factories](#gatewayfilter-factories) applied to the route.| | `order` |Number| The route order. | -### [](#gateway-retrieving-information-about-a-particular-route)[15.5. Retrieving Information about a Particular Route](#gateway-retrieving-information-about-a-particular-route) ### +### [](#gateway-retrieving-information-about-a-particular-route)[15.5. Retrieving Information about a Particular Route](#gateway-retrieving-information-about-a-particular-route) To retrieve information about a single route, make a `GET` request to `/actuator/gateway/routes/{id}` (for example, `/actuator/gateway/routes/first_route`). The resulting response is similar to the following: @@ -2433,13 +2556,13 @@ The following table describes the structure of the response: | `uri` |String| The destination URI of the route. | | `order` |Number| The route order. | -### [](#creating-and-deleting-a-particular-route)[15.6. Creating and Deleting a Particular Route](#creating-and-deleting-a-particular-route) ### +### [](#creating-and-deleting-a-particular-route)[15.6. Creating and Deleting a Particular Route](#creating-and-deleting-a-particular-route) To create a route, make a `POST` request to `/gateway/routes/{id_route_to_create}` with a JSON body that specifies the fields of the route (see [Retrieving Information about a Particular Route](#gateway-retrieving-information-about-a-particular-route)). To delete a route, make a `DELETE` request to `/gateway/routes/{id_route_to_delete}`. -### [](#recap-the-list-of-all-endpoints)[15.7. Recap: The List of All endpoints](#recap-the-list-of-all-endpoints) ### +### [](#recap-the-list-of-all-endpoints)[15.7. Recap: The List of All endpoints](#recap-the-list-of-all-endpoints) The folloiwng table below summarizes the Spring Cloud Gateway actuator endpoints (note that each endpoint has `/actuator/gateway` as the base-path): @@ -2453,7 +2576,7 @@ The folloiwng table below summarizes the Spring Cloud Gateway actuator endpoints | `routes/{id}` | POST | Adds a new route to the gateway. | | `routes/{id}` | DELETE | Removes an existing route from the gateway. | -### [](#sharing-routes-between-multiple-gateway-instances)[15.8. Sharing Routes between multiple Gateway instances](#sharing-routes-between-multiple-gateway-instances) ### +### [](#sharing-routes-between-multiple-gateway-instances)[15.8. Sharing Routes between multiple Gateway instances](#sharing-routes-between-multiple-gateway-instances) Spring Cloud Gateway offers two `RouteDefinitionRepository` implementations. The first one is the`InMemoryRouteDefinitionRepository` which only lives within the memory of one Gateway instance. This type of Repository is not suited to populate Routes across multiple Gateway instances. @@ -2461,12 +2584,11 @@ This type of Repository is not suited to populate Routes across multiple Gateway In order to share Routes across a cluster of Spring Cloud Gateway instances, `RedisRouteDefinitionRepository` can be used. To enable this kind of repository, the following property has to set to true: `spring.cloud.gateway.redis-route-definition-repository.enabled`Likewise to the RedisRateLimiter Filter Factory it requires the use of the spring-boot-starter-data-redis-reactive Spring Boot starter. -[](#troubleshooting)[16. Troubleshooting](#troubleshooting) ----------- +## [](#troubleshooting)[16. Troubleshooting](#troubleshooting) This section covers common problems that may arise when you use Spring Cloud Gateway. -### [](#log-levels)[16.1. Log Levels](#log-levels) ### +### [](#log-levels)[16.1. Log Levels](#log-levels) The following loggers may contain valuable troubleshooting information at the `DEBUG` and `TRACE` levels: @@ -2482,18 +2604,17 @@ The following loggers may contain valuable troubleshooting information at the `D * `redisratelimiter` -### [](#wiretap)[16.2. Wiretap](#wiretap) ### +### [](#wiretap)[16.2. Wiretap](#wiretap) The Reactor Netty `HttpClient` and `HttpServer` can have wiretap enabled. When combined with setting the `reactor.netty` log level to `DEBUG` or `TRACE`, it enables the logging of information, such as headers and bodies sent and received across the wire. To enable wiretap, set `spring.cloud.gateway.httpserver.wiretap=true` or `spring.cloud.gateway.httpclient.wiretap=true` for the `HttpServer` and `HttpClient`, respectively. -[](#developer-guide)[17. Developer Guide](#developer-guide) ----------- +## [](#developer-guide)[17. Developer Guide](#developer-guide) These are basic guides to writing some custom components of the gateway. -### [](#writing-custom-route-predicate-factories)[17.1. Writing Custom Route Predicate Factories](#writing-custom-route-predicate-factories) ### +### [](#writing-custom-route-predicate-factories)[17.1. Writing Custom Route Predicate Factories](#writing-custom-route-predicate-factories) In order to write a Route Predicate you will need to implement `RoutePredicateFactory` as a bean. There is an abstract class called `AbstractRoutePredicateFactory` which you can extend. @@ -2526,7 +2647,7 @@ public class MyRoutePredicateFactory extends AbstractRoutePredicateFactoryreferenced as `AnotherThing` in configuration files. This is **not** a supported naming
convention and this syntax may be removed in future releases. Please update the filter
name to be compliant.| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#writing-custom-global-filters)[17.3. Writing Custom Global Filters](#writing-custom-global-filters) ### +### [](#writing-custom-global-filters)[17.3. Writing Custom Global Filters](#writing-custom-global-filters) To write a custom global filter, you must implement `GlobalFilter` interface as a bean. This applies the filter to all requests. @@ -2634,8 +2755,7 @@ public GlobalFilter customGlobalPostFilter() { } ``` -[](#building-a-simple-gateway-by-using-spring-mvc-or-webflux)[18. Building a Simple Gateway by Using Spring MVC or Webflux](#building-a-simple-gateway-by-using-spring-mvc-or-webflux) ----------- +## [](#building-a-simple-gateway-by-using-spring-mvc-or-webflux)[18. Building a Simple Gateway by Using Spring MVC or Webflux](#building-a-simple-gateway-by-using-spring-mvc-or-webflux) | |The following describes an alternative style gateway. None of the prior documentation applies to what follows.| |---|--------------------------------------------------------------------------------------------------------------| @@ -2704,8 +2824,7 @@ The mapper is a `Function` that takes the incoming `ResponseEntity` and converts First-class support is provided for “sensitive” headers (by default, `cookie` and `authorization`), which are not passed downstream, and for “proxy” (`x-forwarded-*`) headers. -[](#configuration-properties)[19. Configuration properties](#configuration-properties) ----------- +## [](#configuration-properties)[19. Configuration properties](#configuration-properties) To see the list of all Spring Cloud Gateway related configuration properties, see [the appendix](appendix.html). diff --git a/docs/en/spring-cloud/spring-cloud-kubernetes.md b/docs/en/spring-cloud/spring-cloud-kubernetes.md index 11dd490ce12c6ff00af3166bd0c215fc0ec85ae8..7a4c44ca241d3a50326d11e79e8eedb4df5ae0f9 100644 --- a/docs/en/spring-cloud/spring-cloud-kubernetes.md +++ b/docs/en/spring-cloud/spring-cloud-kubernetes.md @@ -1,16 +1,105 @@ -Spring Cloud Kubernetes -========== - +Spring Cloud Kubernetes.hidden { display: none; +} .switch { border-width: 1px 1px 0 1px; border-style: solid; border-color: #7a2518; display: inline-block; +} .switch--item { padding: 10px; background-color: #ffffff; color: #7a2518; display: inline-block; cursor: pointer; +} .switch--item:not(:first-child) { border-width: 0 0 0 1px; border-style: solid; border-color: #7a2518; +} .switch--item.selected { background-color: #7a2519; color: #ffffff; +} function addBlockSwitches() { for (var primary of document.querySelectorAll('.primary')) { var switchItem = createSwitchItem(primary, createBlockSwitch(primary)); switchItem.item.classList.add("selected"); var title = primary.querySelector('.title') title.remove(); } for (var secondary of document.querySelectorAll('.secondary')) { var primary = findPrimary(secondary); if (primary === null) { console.error("Found secondary block with no primary sibling"); } else { var switchItem = createSwitchItem(secondary, primary.querySelector('.switch')); switchItem.content.classList.add("hidden"); primary.append(switchItem.content); secondary.remove(); } } +} function createElementFromHtml(html) { var template = document.createElement('template'); template.innerHTML = html; return template.content.firstChild; +} function createBlockSwitch(primary) { var blockSwitch = createElementFromHtml('\
\'); primary.prepend(blockSwitch) return blockSwitch; +} function findPrimary(secondary) { var candidate = secondary.previousElementSibling; while (candidate != null && !candidate.classList.contains('primary')) { candidate = candidate.previousElementSibling; } return candidate; +} function createSwitchItem(block, blockSwitch) { var blockName = block.querySelector('.title').textContent; var content = block.querySelectorAll('.content').item(0); var colist = nextSibling(block, '.colist'); if (colist != null) { content.append(colist); } var item = createElementFromHtml('\
' + blockName + '\'); item.dataset.blockName = blockName; content.dataset.blockName = blockName; blockSwitch.append(item); return {'item': item, 'content': content}; +} function nextSibling(element, selector) { var sibling = element.nextElementSibling; while (sibling) { if (sibling.matches(selector)) { return sibling; } sibling = sibling.nextElementSibling; } +} function globalSwitch() { document.querySelectorAll(".switch--item").forEach(function(item) { var blockId = blockIdForSwitchItem(item); var handler = function(event) { selectedText = event.target.textContent; window.localStorage.setItem(blockId, selectedText); for (var switchItem of document.querySelectorAll(".switch--item")) { if (blockIdForSwitchItem(switchItem) === blockId && switchItem.textContent === selectedText) { select(switchItem); } } } item.addEventListener("click", handler); if (item.textContent === window.localStorage.getItem(blockId)) { select(item); } }); +} function select(selected) { for (var child of selected.parentNode.children) { child.classList.remove("selected"); } selected.classList.add("selected"); for (var child of selected.parentNode.parentNode.children) { if (child.classList.contains("content")) { if (selected.dataset.blockName === child.dataset.blockName) { child.classList.remove("hidden"); } else { child.classList.add("hidden"); } } } } function blockIdForSwitchItem(item) { idComponents = [] for (var switchItem of item.parentNode.querySelectorAll(".switch--item")) { idComponents.push(switchItem.textContent.toLowerCase()); } return idComponents.sort().join("-") +} window.onload = function() { addBlockSwitches(); globalSwitch(); +}; + +# Spring Cloud Kubernetes + +Table of Contents + +* [1. Why do you need Spring Cloud Kubernetes?](#why-do-you-need-spring-cloud-kubernetes) +* [2. Starters](#starters) +* [3. DiscoveryClient for Kubernetes](#discoveryclient-for-kubernetes) +* [4. Kubernetes native service discovery](#kubernetes-native-service-discovery) +* [5. Kubernetes PropertySource implementations](#kubernetes-propertysource-implementations) + * [5.1. Using a `ConfigMap` `PropertySource`](#configmap-propertysource) + * [5.2. Secrets PropertySource](#secrets-propertysource) + * [5.3. Namespace resolution](#namespace-resolution) + * [5.4. `PropertySource` Reload](#propertysource-reload) + +* [6. Kubernetes Ecosystem Awareness](#kubernetes-ecosystem-awareness) + * [6.1. Kubernetes Profile Autoconfiguration](#kubernetes-profile-autoconfiguration) + * [6.2. Istio Awareness](#istio-awareness) + +* [7. Pod Health Indicator](#pod-health-indicator) +* [8. Info Contributor](#info-contributor) +* [9. Leader Election](#leader-election) +* [10. LoadBalancer for Kubernetes](#loadbalancer-for-kubernetes) +* [11. Security Configurations Inside Kubernetes](#security-configurations-inside-kubernetes) + * [11.1. Namespace](#namespace) + * [11.2. Service Account](#service-account) + +* [12. Service Registry Implementation](#service-registry-implementation) +* [13. Spring Cloud Kubernetes Configuration Watcher](#spring-cloud-kubernetes-configuration-watcher) + * [13.1. Deployment YAML](#deployment-yaml) + * [13.2. Monitoring ConfigMaps and Secrets](#monitoring-configmaps-and-secrets) + * [13.3. HTTP Implementation](#http-implementation) + * [13.3.1. Non-Default Management Port and Actuator Path](#non-default-management-port-and-actuator-path) + + * [13.4. Messaging Implementation](#messaging-implementation) + * [13.5. Configuring RabbitMQ](#configuring-rabbitmq) + * [13.6. Configuring Kafka](#configuring-kafka) + +* [14. Spring Cloud Kubernetes Config Server](#spring-cloud-kubernetes-configserver) + * [14.1. Configuration](#configuration) + * [14.1.1. Enabling The Kubernetes Environment Repository](#enabling-the-kubernetes-environment-repository) + * [14.1.2. Config Map and Secret PropertySources](#config-map-and-secret-propertysources) + * [14.1.3. Fetching Config Map and Secret Data From Additional Namespaces](#fetching-config-map-and-secret-data-from-additional-namespaces) + * [14.1.4. Kubernetes Access Controls](#kubernetes-access-controls) + + * [14.2. Deployment Yaml](#deployment-yaml-2) + +* [15. Spring Cloud Kubernetes Discovery Server](#spring-cloud-kubernetes-discoveryserver) + * [15.1. Permissions](#permissions) + * [15.2. Endpoints](#endpoints) + * [15.2.1. `/apps`](#apps) + * [15.2.2. `/app/{name}`](#appname) + * [15.2.3. `/app/{name}/{instanceid}`](#appnameinstanceid) + + * [15.3. Deployment YAML](#deployment-yaml-3) + +* [16. Examples](#examples) +* [17. Other Resources](#other-resources) +* [18. Configuration properties](#configuration-properties) +* [19. Building](#building) + * [19.1. Basic Compile and Test](#basic-compile-and-test) + * [19.2. Documentation](#documentation) + * [19.3. Working with the code](#working-with-the-code) + * [19.3.1. Activate the Spring Maven profile](#activate-the-spring-maven-profile) + * [19.3.2. Importing into eclipse with m2eclipse](#importing-into-eclipse-with-m2eclipse) + * [19.3.3. Importing into eclipse without m2eclipse](#importing-into-eclipse-without-m2eclipse) + +* [20. Contributing](#contributing) + * [20.1. Sign the Contributor License Agreement](#sign-the-contributor-license-agreement) + * [20.2. Code of Conduct](#code-of-conduct) + * [20.3. Code Conventions and Housekeeping](#code-conventions-and-housekeeping) + * [20.4. Checkstyle](#checkstyle) + * [20.4.1. Checkstyle configuration](#checkstyle-configuration) + + * [20.5. IDE setup](#ide-setup) + * [20.5.1. Intellij IDEA](#intellij-idea) + + * [20.6. Duplicate Finder](#duplicate-finder) + * [20.6.1. Duplicate Finder configuration](#duplicate-finder-configuration) This reference guide covers how to use Spring Cloud Kubernetes. -[](#why-do-you-need-spring-cloud-kubernetes)[1. Why do you need Spring Cloud Kubernetes?](#why-do-you-need-spring-cloud-kubernetes) ----------- +## [](#why-do-you-need-spring-cloud-kubernetes)[1. Why do you need Spring Cloud Kubernetes?](#why-do-you-need-spring-cloud-kubernetes) Spring Cloud Kubernetes provides implementations of well known Spring Cloud interfaces allowing developers to build and run Spring Cloud applications on Kubernetes. While this project may be useful to you when building a cloud native application, it is also not a requirement in order to deploy a Spring Boot app on Kubernetes. If you are just getting started in your journey to running your Spring Boot app on Kubernetes you can accomplish a lot with nothing more than a basic Spring Boot app and Kubernetes itself. To learn more, you can get started by reading the [Spring Boot reference documentation for deploying to Kubernetes ](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#cloud-deployment-kubernetes) and also working through the workshop material [Spring and Kubernetes](https://hackmd.io/@ryanjbaxter/spring-on-k8s-workshop). -[](#starters)[2. Starters](#starters) ----------- +## [](#starters)[2. Starters](#starters) Starters are convenient dependency descriptors you can include in your application. Include a starter to get the dependencies and Spring Boot @@ -23,8 +112,7 @@ Starters that begin with`spring-cloud-starter-kubernetes-client` provide impleme |Fabric8 Dependency

```

org.springframework.cloud
spring-cloud-starter-kubernetes-fabric8-config

```

Kubernetes Client Dependency

```

org.springframework.cloud
spring-cloud-starter-kubernetes-client-config

```|Load application properties from Kubernetes[ConfigMaps](#configmap-propertysource) and [Secrets](#secrets-propertysource).[Reload](#propertysource-reload) application properties when a ConfigMap or
Secret changes.| | Fabric8 Dependency

```

org.springframework.cloud
spring-cloud-starter-kubernetes-fabric8-all

```

Kubernetes Client Dependency

```

org.springframework.cloud
spring-cloud-starter-kubernetes-client-all

``` | All Spring Cloud Kubernetes features. | -[](#discoveryclient-for-kubernetes)[3. DiscoveryClient for Kubernetes](#discoveryclient-for-kubernetes) ----------- +## [](#discoveryclient-for-kubernetes)[3. DiscoveryClient for Kubernetes](#discoveryclient-for-kubernetes) This project provides an implementation of [Discovery Client](https://github.com/spring-cloud/spring-cloud-commons/blob/master/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/DiscoveryClient.java)for [Kubernetes](https://kubernetes.io). This client lets you query Kubernetes endpoints (see [services](https://kubernetes.io/docs/user-guide/services/)) by name. @@ -129,8 +217,7 @@ this to work, you need to align the Kubernetes service name with the `spring.app Spring Cloud Kubernetes can also watch the Kubernetes service catalog for changes and update the`DiscoveryClient` implementation accordingly. In order to enable this functionality you need to add`@EnableScheduling` on a configuration class in your application. -[](#kubernetes-native-service-discovery)[4. Kubernetes native service discovery](#kubernetes-native-service-discovery) ----------- +## [](#kubernetes-native-service-discovery)[4. Kubernetes native service discovery](#kubernetes-native-service-discovery) Kubernetes itself is capable of (server side) service discovery (see: [kubernetes.io/docs/concepts/services-networking/service/#discovering-services](https://kubernetes.io/docs/concepts/services-networking/service/#discovering-services)). Using native kubernetes service discovery ensures compatibility with additional tooling, such as Istio ([istio.io](https://istio.io)), a service mesh that is capable of load balancing, circuit breaker, failover, and much more. @@ -143,15 +230,14 @@ Additionally, you can use Hystrix for: * Fallback functionality, by annotating the respective method with `@HystrixCommand(fallbackMethod=` -[](#kubernetes-propertysource-implementations)[5. Kubernetes PropertySource implementations](#kubernetes-propertysource-implementations) ----------- +## [](#kubernetes-propertysource-implementations)[5. Kubernetes PropertySource implementations](#kubernetes-propertysource-implementations) The most common approach to configuring your Spring Boot application is to create an `application.properties` or `application.yaml` or an `application-profile.properties` or `application-profile.yaml` file that contains key-value pairs that provide customization values to your application or Spring Boot starters. You can override these properties by specifying system properties or environment variables. -### [](#configmap-propertysource)[5.1. Using a `ConfigMap` `PropertySource`](#configmap-propertysource) ### +### [](#configmap-propertysource)[5.1. Using a `ConfigMap` `PropertySource`](#configmap-propertysource) Kubernetes provides a resource named [`ConfigMap`](https://kubernetes.io/docs/user-guide/configmap/) to externalize the parameters to pass to your application in the form of key-value pairs or embedded `application.properties` or `application.yaml` files. @@ -567,7 +653,7 @@ the maximum number of attempts, backoff options like initial interval, multiplie | `spring.cloud.kubernetes.config.retry.max-interval` | `Long` | `2000` | Maximum interval for backoff. | | `spring.cloud.kubernetes.config.retry.multiplier` |`Double` | `1.1` | Multiplier for next interval. | -### [](#secrets-propertysource)[5.2. Secrets PropertySource](#secrets-propertysource) ### +### [](#secrets-propertysource)[5.2. Secrets PropertySource](#secrets-propertysource) Kubernetes has the notion of [Secrets](https://kubernetes.io/docs/concepts/configuration/secret/) for storing sensitive data such as passwords, OAuth tokens, and so on. This project provides integration with `Secrets` to make secrets @@ -732,7 +818,7 @@ Notes: You can find an example of an application that uses secrets (though it has not been updated to use the new `spring-cloud-kubernetes` project) at[spring-boot-camel-config](https://github.com/fabric8-quickstarts/spring-boot-camel-config) -### [](#namespace-resolution)[5.3. Namespace resolution](#namespace-resolution) ### +### [](#namespace-resolution)[5.3. Namespace resolution](#namespace-resolution) Finding an application namespace happens on a best-effort basis. There are some steps that we iterate in order to find it. The easiest and most common one, is to specify it in the proper configuration, for example: @@ -771,7 +857,7 @@ Remember that the same can be done for config maps. If such a namespace is not s Failure to find a namespace from the above steps will result in an Exception being raised. -### [](#propertysource-reload)[5.4. `PropertySource` Reload](#propertysource-reload) ### +### [](#propertysource-reload)[5.4. `PropertySource` Reload](#propertysource-reload) | |This functionality has been deprecated in the 2020.0 release. Please see
the [Spring Cloud Kubernetes Configuration Watcher](#spring-cloud-kubernetes-configuration-watcher) controller for an alternative way
to achieve the same functionality.| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| @@ -873,8 +959,7 @@ Notes: \* You should not use properties under `spring.cloud.kubernetes.reload` in config maps or secrets. Changing such properties at runtime may lead to unexpected results. \* Deleting a property or the whole config map does not restore the original state of the beans when you use the `refresh` level. -[](#kubernetes-ecosystem-awareness)[6. Kubernetes Ecosystem Awareness](#kubernetes-ecosystem-awareness) ----------- +## [](#kubernetes-ecosystem-awareness)[6. Kubernetes Ecosystem Awareness](#kubernetes-ecosystem-awareness) All of the features described earlier in this guide work equally well, regardless of whether your application is running inside Kubernetes. This is really helpful for development and troubleshooting. @@ -888,13 +973,13 @@ Because of the way we set up a specific `EnvironmentPostProcessor` in `spring-cl your application via `-DSPRING_CLOUD_KUBERNETES_ENABLED=false` (any form of relaxed binding will work too). Also note that these properties: `spring.cloud.kubernetes.config.enabled` and `spring.cloud.kubernetes.secrets.enabled` only take effect when set in `bootstrap.{properties|yml}` -### [](#kubernetes-profile-autoconfiguration)[6.1. Kubernetes Profile Autoconfiguration](#kubernetes-profile-autoconfiguration) ### +### [](#kubernetes-profile-autoconfiguration)[6.1. Kubernetes Profile Autoconfiguration](#kubernetes-profile-autoconfiguration) When the application runs as a pod inside Kubernetes, a Spring profile named `kubernetes` automatically gets activated. This lets you customize the configuration, to define beans that are applied when the Spring Boot application is deployed within the Kubernetes platform (for example, different development and production configuration). -### [](#istio-awareness)[6.2. Istio Awareness](#istio-awareness) ### +### [](#istio-awareness)[6.2. Istio Awareness](#istio-awareness) When you include the `spring-cloud-kubernetes-fabric8-istio` module in the application classpath, a new profile is added to the application, provided the application is running inside a Kubernetes Cluster with [Istio](https://istio.io) installed. You can then use @@ -903,8 +988,7 @@ spring `@Profile("istio")` annotations in your Beans and `@Configuration` classe The Istio awareness module uses `me.snowdrop:istio-client` to interact with Istio APIs, letting us discover traffic rules, circuit breakers, and so on, making it easy for our Spring Boot applications to consume this data to dynamically configure themselves according to the environment. -[](#pod-health-indicator)[7. Pod Health Indicator](#pod-health-indicator) ----------- +## [](#pod-health-indicator)[7. Pod Health Indicator](#pod-health-indicator) Spring Boot uses [`HealthIndicator`](https://github.com/spring-projects/spring-boot/blob/master/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpoint.java) to expose info about the health of an application. That makes it really useful for exposing health-related information to the user and makes it a good fit for use as [readiness probes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/). @@ -917,16 +1001,14 @@ The Kubernetes health indicator (which is part of the core module) exposes the f You can disable this `HealthContributor` by setting `management.health.kubernetes.enabled`to `false` in `application.[properties | yaml]`. -[](#info-contributor)[8. Info Contributor](#info-contributor) ----------- +## [](#info-contributor)[8. Info Contributor](#info-contributor) Spring Cloud Kubernetes includes an `InfoContributor` which adds Pod information to Spring Boot’s `/info` Acturator endpoint. You can disable this `InfoContributor` by setting `management.info.kubernetes.enabled`to `false` in `application.[properties | yaml]`. -[](#leader-election)[9. Leader Election](#leader-election) ----------- +## [](#leader-election)[9. Leader Election](#leader-election) The Spring Cloud Kubernetes leader election mechanism implements the leader election API of Spring Integration using a Kubernetes ConfigMap. @@ -954,8 +1036,7 @@ To specify the name of the configmap used for leader election use the following spring.cloud.kubernetes.leader.config-map-name=leader ``` -[](#loadbalancer-for-kubernetes)[10. LoadBalancer for Kubernetes](#loadbalancer-for-kubernetes) ----------- +## [](#loadbalancer-for-kubernetes)[10. LoadBalancer for Kubernetes](#loadbalancer-for-kubernetes) This project includes Spring Cloud Load Balancer for load balancing based on Kubernetes Endpoints and provides implementation of load balancer based on Kubernetes Service. To include it to your project add the following dependency. @@ -992,10 +1073,9 @@ spring.cloud.kubernetes.discovery.all-namespaces=true If a service needs to be accessed over HTTPS you need to add a label or annotation to your service definition with the name `secured` and the value `true` and the load balancer will then use HTTPS to make requests to the service. -[](#security-configurations-inside-kubernetes)[11. Security Configurations Inside Kubernetes](#security-configurations-inside-kubernetes) ----------- +## [](#security-configurations-inside-kubernetes)[11. Security Configurations Inside Kubernetes](#security-configurations-inside-kubernetes) -### [](#namespace)[11.1. Namespace](#namespace) ### +### [](#namespace)[11.1. Namespace](#namespace) Most of the components provided in this project need to know the namespace. For Kubernetes (1.3+), the namespace is made available to the pod as part of the service account secret and is automatically detected by the client. For earlier versions, it needs to be specified as an environment variable to the pod. A quick way to do this is as follows: @@ -1008,7 +1088,7 @@ For earlier versions, it needs to be specified as an environment variable to the fieldPath: "metadata.namespace" ``` -### [](#service-account)[11.2. Service Account](#service-account) ### +### [](#service-account)[11.2. Service Account](#service-account) For distributions of Kubernetes that support more fine-grained role-based access within the cluster, you need to make sure a pod that runs with `spring-cloud-kubernetes` has access to the Kubernetes API. For any service accounts you assign to a deployment or pod, you need to make sure they have the correct roles. @@ -1054,14 +1134,12 @@ roleRef: apiGroup: "" ``` -[](#service-registry-implementation)[12. Service Registry Implementation](#service-registry-implementation) ----------- +## [](#service-registry-implementation)[12. Service Registry Implementation](#service-registry-implementation) In Kubernetes service registration is controlled by the platform, the application itself does not control registration as it may do in other platforms. For this reason using `spring.cloud.service-registry.auto-registration.enabled`or setting `@EnableDiscoveryClient(autoRegister=false)` will have no effect in Spring Cloud Kubernetes. -[](#spring-cloud-kubernetes-configuration-watcher)[13. Spring Cloud Kubernetes Configuration Watcher](#spring-cloud-kubernetes-configuration-watcher) ----------- +## [](#spring-cloud-kubernetes-configuration-watcher)[13. Spring Cloud Kubernetes Configuration Watcher](#spring-cloud-kubernetes-configuration-watcher) Kubernetes provides the ability to [mount a ConfigMap or Secret as a volume](https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#add-configmap-data-to-a-volume)in the container of your application. When the contents of the ConfigMap or Secret changes, the [mounted volume will be updated with those changes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#mounted-configmaps-are-updated-automatically). @@ -1080,7 +1158,7 @@ Spring Cloud Kubernetes Configuration Watcher can send refresh notifications to 2. Using Spring Cloud Bus, in which case you will need a message broker deployed to your custer for the application to use. -### [](#deployment-yaml)[13.1. Deployment YAML](#deployment-yaml) ### +### [](#deployment-yaml)[13.1. Deployment YAML](#deployment-yaml) Below is a sample deployment YAML you can use to deploy the Kubernetes Configuration Watcher to Kubernetes. @@ -1164,7 +1242,7 @@ items: The Service Account and associated Role Binding is important for Spring Cloud Kubernetes Configuration to work properly. The controller needs access to read data about ConfigMaps, Pods, Services, Endpoints and Secrets in the Kubernetes cluster. -### [](#monitoring-configmaps-and-secrets)[13.2. Monitoring ConfigMaps and Secrets](#monitoring-configmaps-and-secrets) ### +### [](#monitoring-configmaps-and-secrets)[13.2. Monitoring ConfigMaps and Secrets](#monitoring-configmaps-and-secrets) Spring Cloud Kubernetes Configuration Watcher will react to changes in ConfigMaps with a label of `spring.cloud.kubernetes.config` with the value `true`or any Secret with a label of `spring.cloud.kubernetes.secret` with the value `true`. If the ConfigMap or Secret does not have either of those labels or the values of those labels is not `true` then any changes will be ignored. @@ -1174,13 +1252,13 @@ The labels Spring Cloud Kubernetes Configuration Watcher looks for on ConfigMaps If a change is made to a ConfigMap or Secret with valid labels then Spring Cloud Kubernetes Configuration Watcher will take the name of the ConfigMap or Secret and send a notification to the application with that name. -### [](#http-implementation)[13.3. HTTP Implementation](#http-implementation) ### +### [](#http-implementation)[13.3. HTTP Implementation](#http-implementation) The HTTP implementation is what is used by default. When this implementation is used Spring Cloud Kubernetes Configuration Watcher and a change to a ConfigMap or Secret occurs then the HTTP implementation will use the Spring Cloud Kubernetes Discovery Client to fetch all instances of the application which match the name of the ConfigMap or Secret and send an HTTP POST request to the application’s actuator`/refresh` endpoint. By default it will send the post request to `/actuator/refresh` using the port registered in the discovery client. -#### [](#non-default-management-port-and-actuator-path)[13.3.1. Non-Default Management Port and Actuator Path](#non-default-management-port-and-actuator-path) #### +#### [](#non-default-management-port-and-actuator-path)[13.3.1. Non-Default Management Port and Actuator Path](#non-default-management-port-and-actuator-path) If the application is using a non-default actuator path and/or using a different port for the management endpoints, the Kubernetes service for the application can add an annotation called `boot.spring.io/actuator` and set its value to the path and port used by the application. For example @@ -1205,12 +1283,12 @@ spec: Another way you can choose to configure the actuator path and/or management port is by setting`spring.cloud.kubernetes.configuration.watcher.actuatorPath` and `spring.cloud.kubernetes.configuration.watcher.actuatorPort`. -### [](#messaging-implementation)[13.4. Messaging Implementation](#messaging-implementation) ### +### [](#messaging-implementation)[13.4. Messaging Implementation](#messaging-implementation) The messaging implementation can be enabled by setting profile to either `bus-amqp` (RabbitMQ) or `bus-kafka` (Kafka) when the Spring Cloud Kubernetes Configuration Watcher application is deployed to Kubernetes. -### [](#configuring-rabbitmq)[13.5. Configuring RabbitMQ](#configuring-rabbitmq) ### +### [](#configuring-rabbitmq)[13.5. Configuring RabbitMQ](#configuring-rabbitmq) When the `bus-amqp` profile is enabled you will need to configure Spring RabbitMQ to point it to the location of the RabbitMQ instance you would like to use as well as any credentials necessary to authenticate. This can be done @@ -1224,7 +1302,7 @@ spring: host: rabbitmq ``` -### [](#configuring-kafka)[13.6. Configuring Kafka](#configuring-kafka) ### +### [](#configuring-kafka)[13.6. Configuring Kafka](#configuring-kafka) When the `bus-kafka` profile is enabled you will need to configure Spring Kafka to point it to the location of the Kafka Broker instance you would like to use. This can be done by setting the standard Spring Kafka properties, for example @@ -1236,8 +1314,7 @@ spring: bootstrap-servers: localhost:9092 ``` -[](#spring-cloud-kubernetes-configserver)[14. Spring Cloud Kubernetes Config Server](#spring-cloud-kubernetes-configserver) ----------- +## [](#spring-cloud-kubernetes-configserver)[14. Spring Cloud Kubernetes Config Server](#spring-cloud-kubernetes-configserver) The Spring Cloud Kubernetes Config Server, is based on [Spring Cloud Config Server](https://spring.io/projects/spring-cloud-config) and adds an [environment repository](https://docs.spring.io/spring-cloud-config/docs/current/reference/html/#_environment_repository) for Kubernetes[Config Maps](https://kubernetes.io/docs/concepts/configuration/configmap/) and [Secrets](https://kubernetes.io/docs/concepts/configuration/secret/). @@ -1248,19 +1325,19 @@ A default image is located on [Docker Hub](https://hub.docker.com/r/springcloud/ the code and image yourself. However, if you need to customize the config server behavior you can easily build your own image from the source code on GitHub and use that. -### [](#configuration)[14.1. Configuration](#configuration) ### +### [](#configuration)[14.1. Configuration](#configuration) -#### [](#enabling-the-kubernetes-environment-repository)[14.1.1. Enabling The Kubernetes Environment Repository](#enabling-the-kubernetes-environment-repository) #### +#### [](#enabling-the-kubernetes-environment-repository)[14.1.1. Enabling The Kubernetes Environment Repository](#enabling-the-kubernetes-environment-repository) To enable the Kubernetes environment repository the `kubernetes` profile must be included in the list of active profiles. You may activate other profiles as well to use other environment repository implementations. -#### [](#config-map-and-secret-propertysources)[14.1.2. Config Map and Secret PropertySources](#config-map-and-secret-propertysources) #### +#### [](#config-map-and-secret-propertysources)[14.1.2. Config Map and Secret PropertySources](#config-map-and-secret-propertysources) By default, only Config Map data will be fetched. To enable Secrets as well you will need to set `spring.cloud.kubernetes.secrets.enableApi=true`. You can disable the Config Map `PropertySource` by setting `spring.cloud.kubernetes.config.enableApi=false`. -#### [](#fetching-config-map-and-secret-data-from-additional-namespaces)[14.1.3. Fetching Config Map and Secret Data From Additional Namespaces](#fetching-config-map-and-secret-data-from-additional-namespaces) #### +#### [](#fetching-config-map-and-secret-data-from-additional-namespaces)[14.1.3. Fetching Config Map and Secret Data From Additional Namespaces](#fetching-config-map-and-secret-data-from-additional-namespaces) By default, the Kubernetes environment repository will only fetch Config Map and Secrets from the namespace in which it is deployed. If you want to include data from other namespaces you can set `spring.cloud.kubernetes.configserver.config-map-namespaces` and/or `spring.cloud.kubernetes.configserver.secrets-namespaces` to a comma separated @@ -1269,12 +1346,12 @@ list of namespace values. | |If you set `spring.cloud.kubernetes.configserver.config-map-namespaces` and/or `spring.cloud.kubernetes.configserver.secrets-namespaces`you will need to include the namespace in which the Config Server is deployed in order to continue to fetch Config Map and Secret data from that namespace.| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -#### [](#kubernetes-access-controls)[14.1.4. Kubernetes Access Controls](#kubernetes-access-controls) #### +#### [](#kubernetes-access-controls)[14.1.4. Kubernetes Access Controls](#kubernetes-access-controls) The Kubernetes Config Server uses the Kubernetes API server to fetch Config Map and Secret data. In order for it to do that it needs ability to `get` and `list` Config Map and Secrets (depending on what you enable/disable). -### [](#deployment-yaml-2)[14.2. Deployment Yaml](#deployment-yaml-2) ### +### [](#deployment-yaml-2)[14.2. Deployment Yaml](#deployment-yaml-2) Below is a sample deployment, service and permissions configuration you can use to deploy a basic Config Server to Kubernetes. @@ -1358,26 +1435,25 @@ items: - containerPort: 8888 ``` -[](#spring-cloud-kubernetes-discoveryserver)[15. Spring Cloud Kubernetes Discovery Server](#spring-cloud-kubernetes-discoveryserver) ----------- +## [](#spring-cloud-kubernetes-discoveryserver)[15. Spring Cloud Kubernetes Discovery Server](#spring-cloud-kubernetes-discoveryserver) The Spring Cloud Kubernetes Discovery Server provides HTTP endpoints apps can use to gather information about services available within a Kubernetes cluster. The Spring Cloud Kubernetes Discovery Server can be used by apps using the `spring-cloud-starter-kubernetes-discoveryclient` to provide data to the `DiscoveryClient` implementation provided by that starter. -### [](#permissions)[15.1. Permissions](#permissions) ### +### [](#permissions)[15.1. Permissions](#permissions) The Spring Cloud Discovery server uses the Kubernetes API server to get data about Service and Endpoint resrouces so it needs list, watch, and get permissions to use those endpoints. See the below sample Kubernetes deployment YAML for an examlpe of how to configure the Service Account on Kubernetes. -### [](#endpoints)[15.2. Endpoints](#endpoints) ### +### [](#endpoints)[15.2. Endpoints](#endpoints) There are three endpoints exposed by the server. -#### [](#apps)[15.2.1. `/apps`](#apps) #### +#### [](#apps)[15.2.1. `/apps`](#apps) A `GET` request sent to `/apps` will return a JSON array of available services. Each item contains the name of the Kubernetes service and service instance information. Below is a sample response. @@ -1427,7 +1503,7 @@ the name of the Kubernetes service and service instance information. Below is a ] ``` -#### [](#appname)[15.2.2. `/app/{name}`](#appname) #### +#### [](#appname)[15.2.2. `/app/{name}`](#appname) A `GET` request to `/app/{name}` can be used to get instance data for all instances of a given service. Below is a sample response when a `GET` request is made to `/app/kubernetes`. @@ -1452,7 +1528,7 @@ service. Below is a sample response when a `GET` request is made to `/app/kubern ] ``` -#### [](#appnameinstanceid)[15.2.3. `/app/{name}/{instanceid}`](#appnameinstanceid) #### +#### [](#appnameinstanceid)[15.2.3. `/app/{name}/{instanceid}`](#appnameinstanceid) A `GET` request made to `/app/{name}/{instanceid}` will return the instance data for a specific instance of a given service. Below is a sample response when a `GET` request is made to `/app/kubernetes/1234`. @@ -1475,7 +1551,7 @@ instance of a given service. Below is a sample response when a `GET` request is } ``` -### [](#deployment-yaml-3)[15.3. Deployment YAML](#deployment-yaml-3) ### +### [](#deployment-yaml-3)[15.3. Deployment YAML](#deployment-yaml-3) An image of the Spring Cloud Discovery Server is hosted on [Docker Hub](https://hub.docker.com/r/springcloud/spring-cloud-kubernetes-discoveryserver). @@ -1558,8 +1634,7 @@ items: - containerPort: 8761 ``` -[](#examples)[16. Examples](#examples) ----------- +## [](#examples)[16. Examples](#examples) Spring Cloud Kubernetes tries to make it transparent for your applications to consume Kubernetes Native Services by following the Spring Cloud interfaces. @@ -1583,8 +1658,7 @@ The following projects highlight the usage of these dependencies and demonstrate * [Spring Boot Admin with Spring Cloud Kubernetes Discovery and Config](https://github.com/salaboy/showcase-admin-tool) -[](#other-resources)[17. Other Resources](#other-resources) ----------- +## [](#other-resources)[17. Other Resources](#other-resources) This section lists other resources, such as presentations (slides) and videos about Spring Cloud Kubernetes. @@ -1594,15 +1668,13 @@ This section lists other resources, such as presentations (slides) and videos ab Please feel free to submit other resources through pull requests to [this repository](https://github.com/spring-cloud/spring-cloud-kubernetes). -[](#configuration-properties)[18. Configuration properties](#configuration-properties) ----------- +## [](#configuration-properties)[18. Configuration properties](#configuration-properties) To see the list of all Kubernetes related configuration properties please check [the Appendix page](appendix.html). -[](#building)[19. Building](#building) ----------- +## [](#building)[19. Building](#building) -### [](#basic-compile-and-test)[19.1. Basic Compile and Test](#basic-compile-and-test) ### +### [](#basic-compile-and-test)[19.1. Basic Compile and Test](#basic-compile-and-test) To build the source you will need to install JDK 17. @@ -1623,7 +1695,7 @@ $ ./mvnw install The projects that require middleware (i.e. Redis) for testing generally require that a local instance of [Docker]([www.docker.com/get-started](https://www.docker.com/get-started)) is installed and running. -### [](#documentation)[19.2. Documentation](#documentation) ### +### [](#documentation)[19.2. Documentation](#documentation) The spring-cloud-build module has a "docs" profile, and if you switch that on it will try to build asciidoc sources from`src/main/asciidoc`. As part of that process it will look for a`README.adoc` and process it by loading all the includes, but not @@ -1631,18 +1703,18 @@ parsing or rendering it, just copying it to `${main.basedir}`(defaults to `$/tmp any changes in the README it will then show up after a Maven build as a modified file in the correct place. Just commit it and push the change. -### [](#working-with-the-code)[19.3. Working with the code](#working-with-the-code) ### +### [](#working-with-the-code)[19.3. Working with the code](#working-with-the-code) If you don’t have an IDE preference we would recommend that you use[Spring Tools Suite](https://www.springsource.com/developer/sts) or[Eclipse](https://eclipse.org) when working with the code. We use the[m2eclipse](https://eclipse.org/m2e/) eclipse plugin for maven support. Other IDEs and tools should also work without issue as long as they use Maven 3.3.3 or better. -#### [](#activate-the-spring-maven-profile)[19.3.1. Activate the Spring Maven profile](#activate-the-spring-maven-profile) #### +#### [](#activate-the-spring-maven-profile)[19.3.1. Activate the Spring Maven profile](#activate-the-spring-maven-profile) Spring Cloud projects require the 'spring' Maven profile to be activated to resolve the spring milestone and snapshot repositories. Use your preferred IDE to set this profile to be active, or you may experience build errors. -#### [](#importing-into-eclipse-with-m2eclipse)[19.3.2. Importing into eclipse with m2eclipse](#importing-into-eclipse-with-m2eclipse) #### +#### [](#importing-into-eclipse-with-m2eclipse)[19.3.2. Importing into eclipse with m2eclipse](#importing-into-eclipse-with-m2eclipse) We recommend the [m2eclipse](https://eclipse.org/m2e/) eclipse plugin when working with eclipse. If you don’t already have m2eclipse installed it is available from the "eclipse @@ -1651,7 +1723,7 @@ marketplace". | |Older versions of m2e do not support Maven 3.3, so once the
projects are imported into Eclipse you will also need to tell
m2eclipse to use the right profile for the projects. If you
see many different errors related to the POMs in the projects, check
that you have an up to date installation. If you can’t upgrade m2e,
add the "spring" profile to your `settings.xml`. Alternatively you can
copy the repository settings from the "spring" profile of the parent
pom into your `settings.xml`.| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -#### [](#importing-into-eclipse-without-m2eclipse)[19.3.3. Importing into eclipse without m2eclipse](#importing-into-eclipse-without-m2eclipse) #### +#### [](#importing-into-eclipse-without-m2eclipse)[19.3.3. Importing into eclipse without m2eclipse](#importing-into-eclipse-without-m2eclipse) If you prefer not to use m2eclipse you can generate eclipse project metadata using the following command: @@ -1662,8 +1734,7 @@ $ ./mvnw eclipse:eclipse The generated eclipse projects can be imported by selecting `import existing projects`from the `file` menu. -[](#contributing)[20. Contributing](#contributing) ----------- +## [](#contributing)[20. Contributing](#contributing) Spring Cloud is released under the non-restrictive Apache 2.0 license, and follows a very standard Github development process, using Github @@ -1671,7 +1742,7 @@ tracker for issues and merging pull requests into master. If you want to contribute even something trivial please do not hesitate, but follow the guidelines below. -### [](#sign-the-contributor-license-agreement)[20.1. Sign the Contributor License Agreement](#sign-the-contributor-license-agreement) ### +### [](#sign-the-contributor-license-agreement)[20.1. Sign the Contributor License Agreement](#sign-the-contributor-license-agreement) Before we accept a non-trivial patch or pull request we will need you to sign the[Contributor License Agreement](https://cla.pivotal.io/sign/spring). Signing the contributor’s agreement does not grant anyone commit rights to the main @@ -1679,13 +1750,13 @@ repository, but it does mean that we can accept your contributions, and you will author credit if we do. Active contributors might be asked to join the core team, and given the ability to merge pull requests. -### [](#code-of-conduct)[20.2. Code of Conduct](#code-of-conduct) ### +### [](#code-of-conduct)[20.2. Code of Conduct](#code-of-conduct) This project adheres to the Contributor Covenant [code of conduct](https://github.com/spring-cloud/spring-cloud-build/blob/master/docs/src/main/asciidoc/code-of-conduct.adoc). By participating, you are expected to uphold this code. Please report -unacceptable behavior to [[email protected]](/cdn-cgi/l/email-protection#473437352e29206a242823226a28216a2428292332243307372e312833262b692e28). +unacceptable behavior to [[email protected]](/cdn-cgi/l/email-protection#d4a7a4a6bdbab3f9b7bbb0b1f9bbb2f9b7bbbab0a1b7a094a4bda2bba0b5b8fabdbb). -### [](#code-conventions-and-housekeeping)[20.3. Code Conventions and Housekeeping](#code-conventions-and-housekeeping) ### +### [](#code-conventions-and-housekeeping)[20.3. Code Conventions and Housekeeping](#code-conventions-and-housekeeping) None of these is essential for a pull request, but they will all help. They can also be added after the original pull request but before a merge. @@ -1715,7 +1786,7 @@ added after the original pull request but before a merge. if you are fixing an existing issue please add `Fixes gh-XXXX` at the end of the commit message (where XXXX is the issue number). -### [](#checkstyle)[20.4. Checkstyle](#checkstyle) ### +### [](#checkstyle)[20.4. Checkstyle](#checkstyle) Spring Cloud Build comes with a set of checkstyle rules. You can find them in the `spring-cloud-build-tools` module. The most notable files under the module are: @@ -1736,7 +1807,7 @@ spring-cloud-build-tools/ |**2**| File header setup | |**3**|Default suppression rules| -#### [](#checkstyle-configuration)[20.4.1. Checkstyle configuration](#checkstyle-configuration) #### +#### [](#checkstyle-configuration)[20.4.1. Checkstyle configuration](#checkstyle-configuration) Checkstyle rules are **disabled by default**. To add checkstyle to your project just define the following properties and plugins. @@ -1803,9 +1874,9 @@ $ curl https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/ $ touch .springformat ``` -### [](#ide-setup)[20.5. IDE setup](#ide-setup) ### +### [](#ide-setup)[20.5. IDE setup](#ide-setup) -#### [](#intellij-idea)[20.5.1. Intellij IDEA](#intellij-idea) #### +#### [](#intellij-idea)[20.5.1. Intellij IDEA](#intellij-idea) In order to setup Intellij you should import our coding conventions, inspection profiles and set up the checkstyle plugin. The following files can be found in the [Spring Cloud Build](https://github.com/spring-cloud/spring-cloud-build/tree/master/spring-cloud-build-tools) project. @@ -1861,11 +1932,11 @@ Go to `File` → `Settings` → `Other settings` → `Checkstyle`. There click o | |Remember to set the `Scan Scope` to `All sources` since we apply checkstyle rules for production and test sources.| |---|------------------------------------------------------------------------------------------------------------------| -### [](#duplicate-finder)[20.6. Duplicate Finder](#duplicate-finder) ### +### [](#duplicate-finder)[20.6. Duplicate Finder](#duplicate-finder) Spring Cloud Build brings along the `basepom:duplicate-finder-maven-plugin`, that enables flagging duplicate and conflicting classes and resources on the java classpath. -#### [](#duplicate-finder-configuration)[20.6.1. Duplicate Finder configuration](#duplicate-finder-configuration) #### +#### [](#duplicate-finder-configuration)[20.6.1. Duplicate Finder configuration](#duplicate-finder-configuration) Duplicate finder is **enabled by default** and will run in the `verify` phase of your Maven build, but it will only take effect in your project if you add the `duplicate-finder-maven-plugin` to the `build` section of the projecst’s `pom.xml`. @@ -1907,3 +1978,5 @@ If you need to add `ignoredClassPatterns` or `ignoredResourcePatterns` to your s ``` + +if (window.parent == window) {(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1\*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga');ga('create', 'UA-2728886-23', 'auto', {'siteSpeedSampleRate': 100});ga('send', 'pageview');} \ No newline at end of file diff --git a/docs/en/spring-cloud/spring-cloud-netflix.md b/docs/en/spring-cloud/spring-cloud-netflix.md index 145b9679453d337f402c59a4d029f5bbce8fa2ce..01d0558765d19f1e0fbbc9f37e7cccc1272f8366 100644 --- a/docs/en/spring-cloud/spring-cloud-netflix.md +++ b/docs/en/spring-cloud/spring-cloud-netflix.md @@ -1,5 +1,57 @@ -Spring Cloud Netflix -========== +Spring Cloud Netflix.hidden { display: none; +} .switch { border-width: 1px 1px 0 1px; border-style: solid; border-color: #7a2518; display: inline-block; +} .switch--item { padding: 10px; background-color: #ffffff; color: #7a2518; display: inline-block; cursor: pointer; +} .switch--item:not(:first-child) { border-width: 0 0 0 1px; border-style: solid; border-color: #7a2518; +} .switch--item.selected { background-color: #7a2519; color: #ffffff; +} function addBlockSwitches() { for (var primary of document.querySelectorAll('.primary')) { var switchItem = createSwitchItem(primary, createBlockSwitch(primary)); switchItem.item.classList.add("selected"); var title = primary.querySelector('.title') title.remove(); } for (var secondary of document.querySelectorAll('.secondary')) { var primary = findPrimary(secondary); if (primary === null) { console.error("Found secondary block with no primary sibling"); } else { var switchItem = createSwitchItem(secondary, primary.querySelector('.switch')); switchItem.content.classList.add("hidden"); primary.append(switchItem.content); secondary.remove(); } } +} function createElementFromHtml(html) { var template = document.createElement('template'); template.innerHTML = html; return template.content.firstChild; +} function createBlockSwitch(primary) { var blockSwitch = createElementFromHtml('\
\'); primary.prepend(blockSwitch) return blockSwitch; +} function findPrimary(secondary) { var candidate = secondary.previousElementSibling; while (candidate != null && !candidate.classList.contains('primary')) { candidate = candidate.previousElementSibling; } return candidate; +} function createSwitchItem(block, blockSwitch) { var blockName = block.querySelector('.title').textContent; var content = block.querySelectorAll('.content').item(0); var colist = nextSibling(block, '.colist'); if (colist != null) { content.append(colist); } var item = createElementFromHtml('\
' + blockName + '\'); item.dataset.blockName = blockName; content.dataset.blockName = blockName; blockSwitch.append(item); return {'item': item, 'content': content}; +} function nextSibling(element, selector) { var sibling = element.nextElementSibling; while (sibling) { if (sibling.matches(selector)) { return sibling; } sibling = sibling.nextElementSibling; } +} function globalSwitch() { document.querySelectorAll(".switch--item").forEach(function(item) { var blockId = blockIdForSwitchItem(item); var handler = function(event) { selectedText = event.target.textContent; window.localStorage.setItem(blockId, selectedText); for (var switchItem of document.querySelectorAll(".switch--item")) { if (blockIdForSwitchItem(switchItem) === blockId && switchItem.textContent === selectedText) { select(switchItem); } } } item.addEventListener("click", handler); if (item.textContent === window.localStorage.getItem(blockId)) { select(item); } }); +} function select(selected) { for (var child of selected.parentNode.children) { child.classList.remove("selected"); } selected.classList.add("selected"); for (var child of selected.parentNode.parentNode.children) { if (child.classList.contains("content")) { if (selected.dataset.blockName === child.dataset.blockName) { child.classList.remove("hidden"); } else { child.classList.add("hidden"); } } } } function blockIdForSwitchItem(item) { idComponents = [] for (var switchItem of item.parentNode.querySelectorAll(".switch--item")) { idComponents.push(switchItem.textContent.toLowerCase()); } return idComponents.sort().join("-") +} window.onload = function() { addBlockSwitches(); globalSwitch(); +}; + +# Spring Cloud Netflix + +Table of Contents + +* [1. Service Discovery: Eureka Clients](#service-discovery-eureka-clients) + * [1.1. How to Include Eureka Client](#netflix-eureka-client-starter) + * [1.2. Registering with Eureka](#registering-with-eureka) + * [1.3. Authenticating with the Eureka Server](#authenticating-with-the-eureka-server) + * [1.4. Status Page and Health Indicator](#status-page-and-health-indicator) + * [1.5. Registering a Secure Application](#registering-a-secure-application) + * [1.6. Eureka’s Health Checks](#eurekas-health-checks) + * [1.7. Eureka Metadata for Instances and Clients](#eureka-metadata-for-instances-and-clients) + * [1.7.1. Using Eureka on Cloud Foundry](#using-eureka-on-cloud-foundry) + * [1.7.2. Using Eureka on AWS](#using-eureka-on-aws) + * [1.7.3. Changing the Eureka Instance ID](#changing-the-eureka-instance-id) + + * [1.8. Using the EurekaClient](#using-the-eurekaclient) + * [1.8.1. EurekaClient with Jersey](#eurekaclient-with-jersey) + + * [1.9. Alternatives to the Native Netflix EurekaClient](#alternatives-to-the-native-netflix-eurekaclient) + * [1.10. Why Is It so Slow to Register a Service?](#why-is-it-so-slow-to-register-a-service) + * [1.11. Zones](#zones) + * [1.12. Refreshing Eureka Clients](#refreshing-eureka-clients) + * [1.13. Using Eureka with Spring Cloud LoadBalancer](#using-eureka-with-spring-cloud-loadbalancer) + +* [2. Service Discovery: Eureka Server](#spring-cloud-eureka-server) + * [2.1. How to Include Eureka Server](#netflix-eureka-server-starter) + * [2.2. How to Run a Eureka Server](#spring-cloud-running-eureka-server) + * [2.3. High Availability, Zones and Regions](#spring-cloud-eureka-server-zones-and-regions) + * [2.4. Standalone Mode](#spring-cloud-eureka-server-standalone-mode) + * [2.5. Peer Awareness](#spring-cloud-eureka-server-peer-awareness) + * [2.6. When to Prefer IP Address](#spring-cloud-eureka-server-prefer-ip-address) + * [2.7. Securing The Eureka Server](#securing-the-eureka-server) + * [2.8. JDK 11 Support](#jdk-11-support) + +* [3. Configuration properties](#configuration-properties) + +**3.1.1** This project provides Netflix OSS integrations for Spring Boot apps through autoconfiguration and binding to the Spring Environment and other Spring programming model idioms. With a few @@ -7,20 +59,19 @@ simple annotations you can quickly enable and configure the common patterns insi application and build large distributed systems with battle-tested Netflix components. The patterns provided include Service Discovery (Eureka). -[](#service-discovery-eureka-clients)[1. Service Discovery: Eureka Clients](#service-discovery-eureka-clients) ----------- +## [](#service-discovery-eureka-clients)[1. Service Discovery: Eureka Clients](#service-discovery-eureka-clients) Service Discovery is one of the key tenets of a microservice-based architecture. Trying to hand-configure each client or some form of convention can be difficult to do and can be brittle. Eureka is the Netflix Service Discovery Server and Client. The server can be configured and deployed to be highly available, with each server replicating state about the registered services to the others. -### [](#netflix-eureka-client-starter)[1.1. How to Include Eureka Client](#netflix-eureka-client-starter) ### +### [](#netflix-eureka-client-starter)[1.1. How to Include Eureka Client](#netflix-eureka-client-starter) To include the Eureka Client in your project, use the starter with a group ID of `org.springframework.cloud` and an artifact ID of `spring-cloud-starter-netflix-eureka-client`. See the [Spring Cloud Project page](https://projects.spring.io/spring-cloud/) for details on setting up your build system with the current Spring Cloud Release Train. -### [](#registering-with-eureka)[1.2. Registering with Eureka](#registering-with-eureka) ### +### [](#registering-with-eureka)[1.2. Registering with Eureka](#registering-with-eureka) When a client registers with Eureka, it provides meta-data about itself — such as host, port, health indicator URL, home page, and other details. Eureka receives heartbeat messages from each instance belonging to a service. @@ -71,7 +122,7 @@ See [EurekaInstanceConfigBean](https://github.com/spring-cloud/spring-cloud-netf To disable the Eureka Discovery Client, you can set `eureka.client.enabled` to `false`. Eureka Discovery Client will also be disabled when `spring.cloud.discovery.enabled` is set to `false`. -### [](#authenticating-with-the-eureka-server)[1.3. Authenticating with the Eureka Server](#authenticating-with-the-eureka-server) ### +### [](#authenticating-with-the-eureka-server)[1.3. Authenticating with the Eureka Server](#authenticating-with-the-eureka-server) HTTP basic authentication is automatically added to your eureka client if one of the `eureka.client.serviceUrl.defaultZone` URLs has credentials embedded in it (curl style, as follows: `[user:[email protected]:8761/eureka](https://user:password@localhost:8761/eureka)`). For more complex needs, you can create a `@Bean` of type `DiscoveryClientOptionalArgs` and inject `ClientFilter` instances into it, all of which is applied to the calls from the client to the server. @@ -101,7 +152,7 @@ The `eureka.client.tls.enabled` needs to be true to enable Eureka client side TL If you want to customize the RestTemplate used by the Eureka HTTP Client you may want to create a bean of `EurekaClientHttpRequestFactorySupplier` and provide your own logic for generating a `ClientHttpRequestFactory` instance. -### [](#status-page-and-health-indicator)[1.4. Status Page and Health Indicator](#status-page-and-health-indicator) ### +### [](#status-page-and-health-indicator)[1.4. Status Page and Health Indicator](#status-page-and-health-indicator) The status page and health indicators for a Eureka instance default to `/info` and `/health` respectively, which are the default locations of useful endpoints in a Spring Boot Actuator application. You need to change these, even for an Actuator application if you use a non-default context path or servlet path (such as `server.servletPath=/custom`). The following example shows the default values for the two settings: @@ -120,7 +171,7 @@ These links show up in the metadata that is consumed by clients and are used in | |In Dalston it was also required to set the status and health check URLs when changing
that management context path. This requirement was removed beginning in Edgware.| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#registering-a-secure-application)[1.5. Registering a Secure Application](#registering-a-secure-application) ### +### [](#registering-a-secure-application)[1.5. Registering a Secure Application](#registering-a-secure-application) If your app wants to be contacted over HTTPS, you can set two flags in the `EurekaInstanceConfigBean`: @@ -152,7 +203,7 @@ Spring placeholders as well — for example, by using `${eureka.instance.hos | |If your application runs behind a proxy, and the SSL termination is in the proxy (for example, if you run in Cloud Foundry or other platforms as a service), then you need to ensure that the proxy “forwarded” headers are intercepted and handled by the application.
If the Tomcat container embedded in a Spring Boot application has explicit configuration for the 'X-Forwarded-\\\*` headers, this happens automatically.
The links rendered by your app to itself being wrong (the wrong host, port, or protocol) is a sign that you got this configuration wrong.| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#eurekas-health-checks)[1.6. Eureka’s Health Checks](#eurekas-health-checks) ### +### [](#eurekas-health-checks)[1.6. Eureka’s Health Checks](#eurekas-health-checks) By default, Eureka uses the client heartbeat to determine if a client is up. Unless specified otherwise, the Discovery Client does not propagate the current health check status of the application, per the Spring Boot Actuator. @@ -174,7 +225,7 @@ eureka: If you require more control over the health checks, consider implementing your own `com.netflix.appinfo.HealthCheckHandler`. -### [](#eureka-metadata-for-instances-and-clients)[1.7. Eureka Metadata for Instances and Clients](#eureka-metadata-for-instances-and-clients) ### +### [](#eureka-metadata-for-instances-and-clients)[1.7. Eureka Metadata for Instances and Clients](#eureka-metadata-for-instances-and-clients) It is worth spending a bit of time understanding how the Eureka metadata works, so you can use it in a way that makes sense in your platform. There is standard metadata for information such as hostname, IP address, port numbers, the status page, and health check. @@ -183,7 +234,7 @@ Additional metadata can be added to the instance registration in the `eureka.ins In general, additional metadata does not change the behavior of the client, unless the client is made aware of the meaning of the metadata. There are a couple of special cases, described later in this document, where Spring Cloud already assigns meaning to the metadata map. -#### [](#using-eureka-on-cloud-foundry)[1.7.1. Using Eureka on Cloud Foundry](#using-eureka-on-cloud-foundry) #### +#### [](#using-eureka-on-cloud-foundry)[1.7.1. Using Eureka on Cloud Foundry](#using-eureka-on-cloud-foundry) Cloud Foundry has a global router so that all instances of the same app have the same hostname (other PaaS solutions with a similar architecture have the same arrangement). This is not necessarily a barrier to using Eureka. @@ -203,7 +254,7 @@ eureka: Depending on the way the security rules are set up in your Cloud Foundry instance, you might be able to register and use the IP address of the host VM for direct service-to-service calls. This feature is not yet available on Pivotal Web Services ([PWS](https://run.pivotal.io)). -#### [](#using-eureka-on-aws)[1.7.2. Using Eureka on AWS](#using-eureka-on-aws) #### +#### [](#using-eureka-on-aws)[1.7.2. Using Eureka on AWS](#using-eureka-on-aws) If the application is planned to be deployed to an AWS cloud, the Eureka instance must be configured to be AWS-aware. You can do so by customizing the [EurekaInstanceConfigBean](https://github.com/spring-cloud/spring-cloud-netflix/tree/main/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaInstanceConfigBean.java) as follows: @@ -218,7 +269,7 @@ public EurekaInstanceConfigBean eurekaInstanceConfig(InetUtils inetUtils) { } ``` -#### [](#changing-the-eureka-instance-id)[1.7.3. Changing the Eureka Instance ID](#changing-the-eureka-instance-id) #### +#### [](#changing-the-eureka-instance-id)[1.7.3. Changing the Eureka Instance ID](#changing-the-eureka-instance-id) A vanilla Netflix Eureka instance is registered with an ID that is equal to its host name (that is, there is only one service per host). Spring Cloud Eureka provides a sensible default, which is defined as follows: @@ -240,7 +291,7 @@ eureka: With the metadata shown in the preceding example and multiple service instances deployed on localhost, the random value is inserted there to make the instance unique. In Cloud Foundry, the `vcap.application.instance_id` is populated automatically in a Spring Boot application, so the random value is not needed. -### [](#using-the-eurekaclient)[1.8. Using the EurekaClient](#using-the-eurekaclient) ### +### [](#using-the-eurekaclient)[1.8. Using the EurekaClient](#using-the-eurekaclient) Once you have an application that is a discovery client, you can use it to discover service instances from the [Eureka Server](#spring-cloud-eureka-server). One way to do so is to use the native `com.netflix.discovery.EurekaClient` (as opposed to the Spring Cloud `DiscoveryClient`), as shown in the following example: @@ -258,7 +309,7 @@ public String serviceUrl() { | |Do not use the `EurekaClient` in a `@PostConstruct` method or in a `@Scheduled` method (or anywhere where the `ApplicationContext` might not be started yet).
It is initialized in a `SmartLifecycle` (with `phase=0`), so the earliest you can rely on it being available is in another `SmartLifecycle` with a higher phase.| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -#### [](#eurekaclient-with-jersey)[1.8.1. EurekaClient with Jersey](#eurekaclient-with-jersey) #### +#### [](#eurekaclient-with-jersey)[1.8.1. EurekaClient with Jersey](#eurekaclient-with-jersey) By default, EurekaClient uses Spring’s `RestTemplate` for HTTP communication. If you wish to use Jersey instead, you need to add the Jersey dependencies to your classpath. @@ -279,7 +330,7 @@ The following example shows the dependencies you need to add: ``` -### [](#alternatives-to-the-native-netflix-eurekaclient)[1.9. Alternatives to the Native Netflix EurekaClient](#alternatives-to-the-native-netflix-eurekaclient) ### +### [](#alternatives-to-the-native-netflix-eurekaclient)[1.9. Alternatives to the Native Netflix EurekaClient](#alternatives-to-the-native-netflix-eurekaclient) You need not use the raw Netflix `EurekaClient`. Also, it is usually more convenient to use it behind a wrapper of some sort. @@ -300,7 +351,7 @@ public String serviceUrl() { } ``` -### [](#why-is-it-so-slow-to-register-a-service)[1.10. Why Is It so Slow to Register a Service?](#why-is-it-so-slow-to-register-a-service) ### +### [](#why-is-it-so-slow-to-register-a-service)[1.10. Why Is It so Slow to Register a Service?](#why-is-it-so-slow-to-register-a-service) Being an instance also involves a periodic heartbeat to the registry (through the client’s `serviceUrl`) with a default duration of 30 seconds. @@ -310,7 +361,7 @@ You can change the period by setting `eureka.instance.leaseRenewalIntervalInSeco Setting it to a value of less than 30 speeds up the process of getting clients connected to other services. In production, it is probably better to stick with the default, because of internal computations in the server that make assumptions about the lease renewal period. -### [](#zones)[1.11. Zones](#zones) ### +### [](#zones)[1.11. Zones](#zones) If you have deployed Eureka clients to multiple zones, you may prefer that those clients use services within the same zone before trying services in another zone. To set that up, you need to configure your Eureka clients correctly. @@ -337,14 +388,14 @@ eureka.instance.metadataMap.zone = zone2 eureka.client.preferSameZoneEureka = true ``` -### [](#refreshing-eureka-clients)[1.12. Refreshing Eureka Clients](#refreshing-eureka-clients) ### +### [](#refreshing-eureka-clients)[1.12. Refreshing Eureka Clients](#refreshing-eureka-clients) By default, the `EurekaClient` bean is refreshable, meaning the Eureka client properties can be changed and refreshed. When a refresh occurs clients will be unregistered from the Eureka server and there might be a brief moment of time where all instance of a given service are not available. One way to eliminate this from happening is to disable the ability to refresh Eureka clients. To do this set `eureka.client.refresh.enable=false`. -### [](#using-eureka-with-spring-cloud-loadbalancer)[1.13. Using Eureka with Spring Cloud LoadBalancer](#using-eureka-with-spring-cloud-loadbalancer) ### +### [](#using-eureka-with-spring-cloud-loadbalancer)[1.13. Using Eureka with Spring Cloud LoadBalancer](#using-eureka-with-spring-cloud-loadbalancer) We offer support for the Spring Cloud LoadBalancer `ZonePreferenceServiceInstanceListSupplier`. The `zone` value from the Eureka instance metadata (`eureka.instance.metadataMap.zone`) is used for setting the @@ -356,12 +407,11 @@ it can use the domain name from the server hostname as a proxy for the zone. If there is no other source of zone data, then a guess is made, based on the client configuration (as opposed to the instance configuration). We take `eureka.client.availabilityZones`, which is a map from region name to a list of zones, and pull out the first zone for the instance’s own region (that is, the `eureka.client.region`, which defaults to "us-east-1", for compatibility with native Netflix). -[](#spring-cloud-eureka-server)[2. Service Discovery: Eureka Server](#spring-cloud-eureka-server) ----------- +## [](#spring-cloud-eureka-server)[2. Service Discovery: Eureka Server](#spring-cloud-eureka-server) This section describes how to set up a Eureka server. -### [](#netflix-eureka-server-starter)[2.1. How to Include Eureka Server](#netflix-eureka-server-starter) ### +### [](#netflix-eureka-server-starter)[2.1. How to Include Eureka Server](#netflix-eureka-server-starter) To include Eureka Server in your project, use the starter with a group ID of `org.springframework.cloud` and an artifact ID of `spring-cloud-starter-netflix-eureka-server`. See the [Spring Cloud Project page](https://projects.spring.io/spring-cloud/) for details on setting up your build system with the current Spring Cloud Release Train. @@ -378,7 +428,7 @@ spring: prefer-file-system-access: false ``` -### [](#spring-cloud-running-eureka-server)[2.2. How to Run a Eureka Server](#spring-cloud-running-eureka-server) ### +### [](#spring-cloud-running-eureka-server)[2.2. How to Run a Eureka Server](#spring-cloud-running-eureka-server) The following example shows a minimal Eureka server: @@ -401,7 +451,7 @@ The following links have some Eureka background reading: [flux capacitor](https: | |Due to Gradle’s dependency resolution rules and the lack of a parent bom feature, depending on `spring-cloud-starter-netflix-eureka-server` can cause failures on application startup.
To remedy this issue, add the Spring Boot Gradle plugin and import the Spring cloud starter parent bom as follows:

build.gradle

```
buildscript {
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:{spring-boot-docs-version}")
}
}

apply plugin: "spring-boot"

dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:{spring-cloud-version}"
}
}
```| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#spring-cloud-eureka-server-zones-and-regions)[2.3. High Availability, Zones and Regions](#spring-cloud-eureka-server-zones-and-regions) ### +### [](#spring-cloud-eureka-server-zones-and-regions)[2.3. High Availability, Zones and Regions](#spring-cloud-eureka-server-zones-and-regions) The Eureka server does not have a back end store, but the service instances in the registry all have to send heartbeats to keep their registrations up to date (so this can be done in memory). Clients also have an in-memory cache of Eureka registrations (so they do not have to go to the registry for every request to a service). @@ -409,7 +459,7 @@ Clients also have an in-memory cache of Eureka registrations (so they do not hav By default, every Eureka server is also a Eureka client and requires (at least one) service URL to locate a peer. If you do not provide it, the service runs and works, but it fills your logs with a lot of noise about not being able to register with the peer. -### [](#spring-cloud-eureka-server-standalone-mode)[2.4. Standalone Mode](#spring-cloud-eureka-server-standalone-mode) ### +### [](#spring-cloud-eureka-server-standalone-mode)[2.4. Standalone Mode](#spring-cloud-eureka-server-standalone-mode) The combination of the two caches (client and server) and the heartbeats make a standalone Eureka server fairly resilient to failure, as long as there is some sort of monitor or elastic runtime (such as Cloud Foundry) keeping it alive. In standalone mode, you might prefer to switch off the client side behavior so that it does not keep trying and failing to reach its peers. @@ -433,7 +483,7 @@ eureka: Notice that the `serviceUrl` is pointing to the same host as the local instance. -### [](#spring-cloud-eureka-server-peer-awareness)[2.5. Peer Awareness](#spring-cloud-eureka-server-peer-awareness) ### +### [](#spring-cloud-eureka-server-peer-awareness)[2.5. Peer Awareness](#spring-cloud-eureka-server-peer-awareness) Eureka can be made even more resilient and available by running multiple instances and asking them to register with each other. In fact, this is the default behavior, so all you need to do to make it work is add a valid `serviceUrl` to a peer, as shown in the following example: @@ -503,7 +553,7 @@ eureka: hostname: peer3 ``` -### [](#spring-cloud-eureka-server-prefer-ip-address)[2.6. When to Prefer IP Address](#spring-cloud-eureka-server-prefer-ip-address) ### +### [](#spring-cloud-eureka-server-prefer-ip-address)[2.6. When to Prefer IP Address](#spring-cloud-eureka-server-prefer-ip-address) In some cases, it is preferable for Eureka to advertise the IP addresses of services rather than the hostname. Set `eureka.instance.preferIpAddress` to `true` and, when the application registers with eureka, it uses its IP address rather than its hostname. @@ -511,7 +561,7 @@ Set `eureka.instance.preferIpAddress` to `true` and, when the application regist | |If the hostname cannot be determined by Java, then the IP address is sent to Eureka.
Only explict way of setting the hostname is by setting `eureka.instance.hostname` property.
You can set your hostname at the run-time by using an environment variable — for example, `eureka.instance.hostname=${HOST_NAME}`.| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#securing-the-eureka-server)[2.7. Securing The Eureka Server](#securing-the-eureka-server) ### +### [](#securing-the-eureka-server)[2.7. Securing The Eureka Server](#securing-the-eureka-server) You can secure your Eureka server simply by adding Spring Security to your server’s classpath via `spring-boot-starter-security`. By default when Spring Security is on the classpath it will require that @@ -535,7 +585,7 @@ For more information on CSRF see the [Spring Security documentation](https://doc A demo Eureka Server can be found in the Spring Cloud Samples [repo](https://github.com/spring-cloud-samples/eureka/tree/Eureka-With-Security). -### [](#jdk-11-support)[2.8. JDK 11 Support](#jdk-11-support) ### +### [](#jdk-11-support)[2.8. JDK 11 Support](#jdk-11-support) The JAXB modules which the Eureka server depends upon were removed in JDK 11. If you intend to use JDK 11 when running a Eureka server you must include these dependencies in your POM or Gradle file. @@ -547,7 +597,8 @@ when running a Eureka server you must include these dependencies in your POM or ``` -[](#configuration-properties)[3. Configuration properties](#configuration-properties) ----------- +## [](#configuration-properties)[3. Configuration properties](#configuration-properties) To see the list of all Spring Cloud Netflix related configuration properties please check [the Appendix page](appendix.html). + +if (window.parent == window) {(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1\*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga');ga('create', 'UA-2728886-23', 'auto', {'siteSpeedSampleRate': 100});ga('send', 'pageview');} \ No newline at end of file diff --git a/docs/en/spring-cloud/spring-cloud-openfeign.md b/docs/en/spring-cloud/spring-cloud-openfeign.md index e7877e3db3501d731dd90a463468c08c614d6c0a..2c40ce25dbeee983fd38c61239c429dd37c80386 100644 --- a/docs/en/spring-cloud/spring-cloud-openfeign.md +++ b/docs/en/spring-cloud/spring-cloud-openfeign.md @@ -1,11 +1,58 @@ -Spring Cloud OpenFeign -========== +Spring Cloud OpenFeign.hidden { display: none; +} .switch { border-width: 1px 1px 0 1px; border-style: solid; border-color: #7a2518; display: inline-block; +} .switch--item { padding: 10px; background-color: #ffffff; color: #7a2518; display: inline-block; cursor: pointer; +} .switch--item:not(:first-child) { border-width: 0 0 0 1px; border-style: solid; border-color: #7a2518; +} .switch--item.selected { background-color: #7a2519; color: #ffffff; +} function addBlockSwitches() { for (var primary of document.querySelectorAll('.primary')) { var switchItem = createSwitchItem(primary, createBlockSwitch(primary)); switchItem.item.classList.add("selected"); var title = primary.querySelector('.title') title.remove(); } for (var secondary of document.querySelectorAll('.secondary')) { var primary = findPrimary(secondary); if (primary === null) { console.error("Found secondary block with no primary sibling"); } else { var switchItem = createSwitchItem(secondary, primary.querySelector('.switch')); switchItem.content.classList.add("hidden"); primary.append(switchItem.content); secondary.remove(); } } +} function createElementFromHtml(html) { var template = document.createElement('template'); template.innerHTML = html; return template.content.firstChild; +} function createBlockSwitch(primary) { var blockSwitch = createElementFromHtml('\
\'); primary.prepend(blockSwitch) return blockSwitch; +} function findPrimary(secondary) { var candidate = secondary.previousElementSibling; while (candidate != null && !candidate.classList.contains('primary')) { candidate = candidate.previousElementSibling; } return candidate; +} function createSwitchItem(block, blockSwitch) { var blockName = block.querySelector('.title').textContent; var content = block.querySelectorAll('.content').item(0); var colist = nextSibling(block, '.colist'); if (colist != null) { content.append(colist); } var item = createElementFromHtml('\
' + blockName + '\'); item.dataset.blockName = blockName; content.dataset.blockName = blockName; blockSwitch.append(item); return {'item': item, 'content': content}; +} function nextSibling(element, selector) { var sibling = element.nextElementSibling; while (sibling) { if (sibling.matches(selector)) { return sibling; } sibling = sibling.nextElementSibling; } +} function globalSwitch() { document.querySelectorAll(".switch--item").forEach(function(item) { var blockId = blockIdForSwitchItem(item); var handler = function(event) { selectedText = event.target.textContent; window.localStorage.setItem(blockId, selectedText); for (var switchItem of document.querySelectorAll(".switch--item")) { if (blockIdForSwitchItem(switchItem) === blockId && switchItem.textContent === selectedText) { select(switchItem); } } } item.addEventListener("click", handler); if (item.textContent === window.localStorage.getItem(blockId)) { select(item); } }); +} function select(selected) { for (var child of selected.parentNode.children) { child.classList.remove("selected"); } selected.classList.add("selected"); for (var child of selected.parentNode.parentNode.children) { if (child.classList.contains("content")) { if (selected.dataset.blockName === child.dataset.blockName) { child.classList.remove("hidden"); } else { child.classList.add("hidden"); } } } } function blockIdForSwitchItem(item) { idComponents = [] for (var switchItem of item.parentNode.querySelectorAll(".switch--item")) { idComponents.push(switchItem.textContent.toLowerCase()); } return idComponents.sort().join("-") +} window.onload = function() { addBlockSwitches(); globalSwitch(); +}; + +# Spring Cloud OpenFeign + +Table of Contents + +* [1. Declarative REST Client: Feign](#spring-cloud-feign) + * [1.1. How to Include Feign](#netflix-feign-starter) + * [1.2. Overriding Feign Defaults](#spring-cloud-feign-overriding-defaults) + * [1.2.1. `SpringEncoder` configuration](#springencoder-configuration) + + * [1.3. Timeout Handling](#timeout-handling) + * [1.4. Creating Feign Clients Manually](#creating-feign-clients-manually) + * [1.5. Feign Spring Cloud CircuitBreaker Support](#spring-cloud-feign-circuitbreaker) + * [1.6. Feign Spring Cloud CircuitBreaker Fallbacks](#spring-cloud-feign-circuitbreaker-fallback) + * [1.7. Feign and `@Primary`](#feign-and-primary) + * [1.8. Feign Inheritance Support](#spring-cloud-feign-inheritance) + * [1.9. Feign request/response compression](#feign-requestresponse-compression) + * [1.10. Feign logging](#feign-logging) + * [1.11. Feign Capability support](#feign-capability-support) + * [1.12. Feign metrics](#feign-metrics) + * [1.13. Feign Caching](#feign-caching) + * [1.14. Feign @QueryMap support](#feign-querymap-support) + * [1.15. HATEOAS support](#hateoas-support) + * [1.16. Spring @MatrixVariable Support](#spring-matrixvariable-support) + * [1.17. Feign `CollectionFormat` support](#feign-collectionformat-support) + * [1.18. Reactive Support](#reactive-support) + * [1.18.1. Early Initialization Errors](#early-initialization-errors) + + * [1.19. Spring Data Support](#spring-data-support) + * [1.20. Spring `@RefreshScope` Support](#spring-refreshscope-support) + * [1.21. OAuth2 Support](#oauth2-support) + +* [2. Configuration properties](#configuration-properties) + +**3.1.1** This project provides OpenFeign integrations for Spring Boot apps through autoconfiguration and binding to the Spring Environment and other Spring programming model idioms. -[](#spring-cloud-feign)[1. Declarative REST Client: Feign](#spring-cloud-feign) ----------- +## [](#spring-cloud-feign)[1. Declarative REST Client: Feign](#spring-cloud-feign) [Feign](https://github.com/OpenFeign/feign) is a declarative web service client. It makes writing web service clients easier. @@ -15,7 +62,7 @@ Feign also supports pluggable encoders and decoders. Spring Cloud adds support for Spring MVC annotations and for using the same `HttpMessageConverters` used by default in Spring Web. Spring Cloud integrates Eureka, Spring Cloud CircuitBreaker, as well as Spring Cloud LoadBalancer to provide a load-balanced http client when using Feign. -### [](#netflix-feign-starter)[1.1. How to Include Feign](#netflix-feign-starter) ### +### [](#netflix-feign-starter)[1.1. How to Include Feign](#netflix-feign-starter) To include Feign in your project use the starter with group `org.springframework.cloud`and artifact id `spring-cloud-starter-openfeign`. See the [Spring Cloud Project page](https://projects.spring.io/spring-cloud/)for details on setting up your build system with the current Spring Cloud Release Train. @@ -70,7 +117,7 @@ Spring Cloud OpenFeign supports all the features available for the blocking mode | |To use `@EnableFeignClients` annotation on `@Configuration`-annotated-classes, make sure to specify where the clients are located, for example:`@EnableFeignClients(basePackages = "com.example.clients")`or list them explicitly:`@EnableFeignClients(clients = InventoryServiceFeignClient.class)`| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#spring-cloud-feign-overriding-defaults)[1.2. Overriding Feign Defaults](#spring-cloud-feign-overriding-defaults) ### +### [](#spring-cloud-feign-overriding-defaults)[1.2. Overriding Feign Defaults](#spring-cloud-feign-overriding-defaults) A central concept in Spring Cloud’s Feign support is that of the named client. Each feign client is part of an ensemble of components that work together to contact a remote server on demand, and the ensemble has a name that you give it as an application developer using the `@FeignClient` annotation. Spring Cloud creates a new ensemble as an`ApplicationContext` on demand for each named client using `FeignClientsConfiguration`. This contains (amongst other things) an `feign.Decoder`, a `feign.Encoder`, and a `feign.Contract`. It is possible to override the name of that ensemble by using the `contextId`attribute of the `@FeignClient` annotation. @@ -267,13 +314,13 @@ public FeignClientConfigurer feignClientConfigurer() { | |By default, Feign clients do not encode slash `/` characters. You can change this behaviour, by setting the value of `feign.client.decodeSlash` to `false`.| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------| -#### [](#springencoder-configuration)[1.2.1. `SpringEncoder` configuration](#springencoder-configuration) #### +#### [](#springencoder-configuration)[1.2.1. `SpringEncoder` configuration](#springencoder-configuration) In the `SpringEncoder` that we provide, we set `null` charset for binary content types and `UTF-8` for all the other ones. You can modify this behaviour to derive the charset from the `Content-Type` header charset instead by setting the value of `feign.encoder.charset-from-content-type` to `true`. -### [](#timeout-handling)[1.3. Timeout Handling](#timeout-handling) ### +### [](#timeout-handling)[1.3. Timeout Handling](#timeout-handling) We can configure timeouts on both the default and the named client. OpenFeign works with two timeout parameters: @@ -284,7 +331,7 @@ We can configure timeouts on both the default and the named client. OpenFeign wo | |In case the server is not running or available a packet results in *connection refused*. The communication ends either with an error message or in a fallback. This can happen *before* the `connectTimeout` if it is set very low. The time taken to perform a lookup and to receive such a packet causes a significant part of this delay. It is subject to change based on the remote host that involves a DNS lookup.| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#creating-feign-clients-manually)[1.4. Creating Feign Clients Manually](#creating-feign-clients-manually) ### +### [](#creating-feign-clients-manually)[1.4. Creating Feign Clients Manually](#creating-feign-clients-manually) In some cases it might be necessary to customize your Feign Clients in a way that is not possible using the methods above. In this case you can create Clients using the[Feign Builder API](https://github.com/OpenFeign/feign/#basics). Below is an example @@ -332,7 +379,7 @@ class FooController { You can also use the `Builder`to configure FeignClient not to inherit beans from the parent context. You can do this by overriding calling `inheritParentContext(false)` on the `Builder`. -### [](#spring-cloud-feign-circuitbreaker)[1.5. Feign Spring Cloud CircuitBreaker Support](#spring-cloud-feign-circuitbreaker) ### +### [](#spring-cloud-feign-circuitbreaker)[1.5. Feign Spring Cloud CircuitBreaker Support](#spring-cloud-feign-circuitbreaker) If Spring Cloud CircuitBreaker is on the classpath and `feign.circuitbreaker.enabled=true`, Feign will wrap all methods with a circuit breaker. @@ -368,7 +415,7 @@ public class FooConfiguration { To enable Spring Cloud CircuitBreaker group set the `feign.circuitbreaker.group.enabled` property to `true` (by default `false`). -### [](#spring-cloud-feign-circuitbreaker-fallback)[1.6. Feign Spring Cloud CircuitBreaker Fallbacks](#spring-cloud-feign-circuitbreaker-fallback) ### +### [](#spring-cloud-feign-circuitbreaker-fallback)[1.6. Feign Spring Cloud CircuitBreaker Fallbacks](#spring-cloud-feign-circuitbreaker-fallback) Spring Cloud CircuitBreaker supports the notion of a fallback: a default code path that is executed when the circuit is open or there is an error. To enable fallbacks for a given `@FeignClient` set the `fallback` attribute to the class name that implements the fallback. You also need to declare your implementation as a Spring bean. @@ -440,7 +487,7 @@ If one needs access to the cause that made the fallback trigger, one can use the } ``` -### [](#feign-and-primary)[1.7. Feign and `@Primary`](#feign-and-primary) ### +### [](#feign-and-primary)[1.7. Feign and `@Primary`](#feign-and-primary) When using Feign with Spring Cloud CircuitBreaker fallbacks, there are multiple beans in the `ApplicationContext` of the same type. This will cause `@Autowired` to not work because there isn’t exactly one bean, or one marked as primary. To work around this, Spring Cloud OpenFeign marks all Feign instances as `@Primary`, so Spring Framework will know which bean to inject. In some cases, this may not be desirable. To turn off this behavior set the `primary` attribute of `@FeignClient` to false. @@ -451,7 +498,7 @@ public interface HelloClient { } ``` -### [](#spring-cloud-feign-inheritance)[1.8. Feign Inheritance Support](#spring-cloud-feign-inheritance) ### +### [](#spring-cloud-feign-inheritance)[1.8. Feign Inheritance Support](#spring-cloud-feign-inheritance) Feign supports boilerplate apis via single-inheritance interfaces. This allows grouping common operations into convenient base interfaces. @@ -489,7 +536,7 @@ public interface UserClient extends UserService { | |`@FeignClient` interfaces should not be shared between server and client and annotating `@FeignClient` interfaces with `@RequestMapping` on class level is no longer supported.| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#feign-requestresponse-compression)[1.9. Feign request/response compression](#feign-requestresponse-compression) ### +### [](#feign-requestresponse-compression)[1.9. Feign request/response compression](#feign-requestresponse-compression) You may consider enabling the request or response GZIP compression for your Feign requests. You can do this by enabling one of the properties: @@ -509,7 +556,7 @@ feign.compression.request.min-request-size=2048 These properties allow you to be selective about the compressed media types and minimum request threshold length. -### [](#feign-logging)[1.10. Feign logging](#feign-logging) ### +### [](#feign-logging)[1.10. Feign logging](#feign-logging) A logger is created for each Feign client created. By default the name of the logger is the full class name of the interface used to create the Feign client. Feign logging only responds to the `DEBUG` level. @@ -541,7 +588,7 @@ public class FooConfiguration { } ``` -### [](#feign-capability-support)[1.11. Feign Capability support](#feign-capability-support) ### +### [](#feign-capability-support)[1.11. Feign Capability support](#feign-capability-support) The Feign capabilities expose core Feign components so that these components can be modified. For example, the capabilities can take the `Client`, *decorate* it, and give the decorated instance back to Feign. The support for metrics libraries is a good real-life example for this. See [Feign metrics](#feign-metrics). @@ -558,7 +605,7 @@ public class FooConfiguration { } ``` -### [](#feign-metrics)[1.12. Feign metrics](#feign-metrics) ### +### [](#feign-metrics)[1.12. Feign metrics](#feign-metrics) If all of the following conditions are true, a `MicrometerCapability` bean is created and registered so that your Feign client publishes metrics to Micrometer: @@ -600,7 +647,7 @@ public class FooConfiguration { } ``` -### [](#feign-caching)[1.13. Feign Caching](#feign-caching) ### +### [](#feign-caching)[1.13. Feign Caching](#feign-caching) If `@EnableCaching` annotation is used, a `CachingCapability` bean is created and registered so that your Feign client recognizes `@Cache*` annotations on its interface: @@ -615,7 +662,7 @@ public interface DemoClient { You can also disable the feature via property `feign.cache.enabled=false`. -### [](#feign-querymap-support)[1.14. Feign @QueryMap support](#feign-querymap-support) ### +### [](#feign-querymap-support)[1.14. Feign @QueryMap support](#feign-querymap-support) The OpenFeign `@QueryMap` annotation provides support for POJOs to be used as GET parameter maps. Unfortunately, the default OpenFeign QueryMap annotation is @@ -649,7 +696,7 @@ public interface DemoTemplate { If you need more control over the generated query parameter map, you can implement a custom `QueryMapEncoder` bean. -### [](#hateoas-support)[1.15. HATEOAS support](#hateoas-support) ### +### [](#hateoas-support)[1.15. HATEOAS support](#hateoas-support) Spring provides some APIs to create REST representations that follow the [HATEOAS](https://en.wikipedia.org/wiki/HATEOAS) principle, [Spring Hateoas](https://spring.io/projects/spring-hateoas) and [Spring Data REST](https://spring.io/projects/spring-data-rest). @@ -668,7 +715,7 @@ public interface DemoTemplate { } ``` -### [](#spring-matrixvariable-support)[1.16. Spring @MatrixVariable Support](#spring-matrixvariable-support) ### +### [](#spring-matrixvariable-support)[1.16. Spring @MatrixVariable Support](#spring-matrixvariable-support) Spring Cloud OpenFeign provides support for the Spring `@MatrixVariable` annotation. @@ -699,7 +746,7 @@ public interface DemoTemplate { } ``` -### [](#feign-collectionformat-support)[1.17. Feign `CollectionFormat` support](#feign-collectionformat-support) ### +### [](#feign-collectionformat-support)[1.17. Feign `CollectionFormat` support](#feign-collectionformat-support) We support `feign.CollectionFormat` by providing the `@CollectionFormat` annotation. You can annotate a Feign client method (or the whole class to affect all methods) with it by passing the desired `feign.CollectionFormat` as annotation value. @@ -720,13 +767,13 @@ protected interface PageableFeignClient { | |Set the `CSV` format while sending `Pageable` as a query parameter in order for it to be encoded correctly.| |---|-----------------------------------------------------------------------------------------------------------| -### [](#reactive-support)[1.18. Reactive Support](#reactive-support) ### +### [](#reactive-support)[1.18. Reactive Support](#reactive-support) As the [OpenFeign project](https://github.com/OpenFeign/feign) does not currently support reactive clients, such as [Spring WebClient](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/reactive/function/client/WebClient.html), neither does Spring Cloud OpenFeign.We will add support for it here as soon as it becomes available in the core project. Until that is done, we recommend using [feign-reactive](https://github.com/Playtika/feign-reactive) for Spring WebClient support. -#### [](#early-initialization-errors)[1.18.1. Early Initialization Errors](#early-initialization-errors) #### +#### [](#early-initialization-errors)[1.18.1. Early Initialization Errors](#early-initialization-errors) Depending on how you are using your Feign clients you may see initialization errors when starting your application. To work around this problem you can use an `ObjectProvider` when autowiring your client. @@ -736,7 +783,7 @@ To work around this problem you can use an `ObjectProvider` when autowiring your ObjectProvider testFeignClient; ``` -### [](#spring-data-support)[1.19. Spring Data Support](#spring-data-support) ### +### [](#spring-data-support)[1.19. Spring Data Support](#spring-data-support) You may consider enabling Jackson Modules for the support `org.springframework.data.domain.Page` and `org.springframework.data.domain.Sort` decoding. @@ -744,7 +791,7 @@ You may consider enabling Jackson Modules for the support `org.springframework.d feign.autoconfiguration.jackson.enabled=true ``` -### [](#spring-refreshscope-support)[1.20. Spring `@RefreshScope` Support](#spring-refreshscope-support) ### +### [](#spring-refreshscope-support)[1.20. Spring `@RefreshScope` Support](#spring-refreshscope-support) If Feign client refresh is enabled, each feign client is created with `feign.Request.Options` as a refresh-scoped bean. This means properties such as `connectTimeout` and `readTimeout` can be refreshed against any Feign client instance through `POST /actuator/refresh`. @@ -757,7 +804,7 @@ feign.client.refresh-enabled=true | |DO NOT annotate the `@FeignClient` interface with the `@RefreshScope` annotation.| |---|---------------------------------------------------------------------------------| -### [](#oauth2-support)[1.21. OAuth2 Support](#oauth2-support) ### +### [](#oauth2-support)[1.21. OAuth2 Support](#oauth2-support) OAuth2 support can be enabled by setting following flag: @@ -772,7 +819,8 @@ Sometimes, when load balancing is enabled for Feign clients, you may want to use feign.oauth2.load-balanced=true ``` -[](#configuration-properties)[2. Configuration properties](#configuration-properties) ----------- +## [](#configuration-properties)[2. Configuration properties](#configuration-properties) To see the list of all Spring Cloud OpenFeign related configuration properties please check [the Appendix page](appendix.html). + +if (window.parent == window) {(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1\*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga');ga('create', 'UA-2728886-23', 'auto', {'siteSpeedSampleRate': 100});ga('send', 'pageview');} \ No newline at end of file diff --git a/docs/en/spring-cloud/spring-cloud-sleuth.md b/docs/en/spring-cloud/spring-cloud-sleuth.md index 295ecac149ae4c341348d2156c5a94b8626c5c6f..6b7d93e1b4c5a554e1034c030c6c8efc5d3780fb 100644 --- a/docs/en/spring-cloud/spring-cloud-sleuth.md +++ b/docs/en/spring-cloud/spring-cloud-sleuth.md @@ -1,5 +1,20 @@ -Spring Cloud Sleuth Reference Documentation -========== +Spring Cloud Sleuth Reference Documentation.hidden { display: none; +} .switch { border-width: 1px 1px 0 1px; border-style: solid; border-color: #7a2518; display: inline-block; +} .switch--item { padding: 10px; background-color: #ffffff; color: #7a2518; display: inline-block; cursor: pointer; +} .switch--item:not(:first-child) { border-width: 0 0 0 1px; border-style: solid; border-color: #7a2518; +} .switch--item.selected { background-color: #7a2519; color: #ffffff; +} function addBlockSwitches() { for (var primary of document.querySelectorAll('.primary')) { var switchItem = createSwitchItem(primary, createBlockSwitch(primary)); switchItem.item.classList.add("selected"); var title = primary.querySelector('.title') title.remove(); } for (var secondary of document.querySelectorAll('.secondary')) { var primary = findPrimary(secondary); if (primary === null) { console.error("Found secondary block with no primary sibling"); } else { var switchItem = createSwitchItem(secondary, primary.querySelector('.switch')); switchItem.content.classList.add("hidden"); primary.append(switchItem.content); secondary.remove(); } } +} function createElementFromHtml(html) { var template = document.createElement('template'); template.innerHTML = html; return template.content.firstChild; +} function createBlockSwitch(primary) { var blockSwitch = createElementFromHtml('\
\'); primary.prepend(blockSwitch) return blockSwitch; +} function findPrimary(secondary) { var candidate = secondary.previousElementSibling; while (candidate != null && !candidate.classList.contains('primary')) { candidate = candidate.previousElementSibling; } return candidate; +} function createSwitchItem(block, blockSwitch) { var blockName = block.querySelector('.title').textContent; var content = block.querySelectorAll('.content').item(0); var colist = nextSibling(block, '.colist'); if (colist != null) { content.append(colist); } var item = createElementFromHtml('\
' + blockName + '\'); item.dataset.blockName = blockName; content.dataset.blockName = blockName; blockSwitch.append(item); return {'item': item, 'content': content}; +} function nextSibling(element, selector) { var sibling = element.nextElementSibling; while (sibling) { if (sibling.matches(selector)) { return sibling; } sibling = sibling.nextElementSibling; } +} function globalSwitch() { document.querySelectorAll(".switch--item").forEach(function(item) { var blockId = blockIdForSwitchItem(item); var handler = function(event) { selectedText = event.target.textContent; window.localStorage.setItem(blockId, selectedText); for (var switchItem of document.querySelectorAll(".switch--item")) { if (blockIdForSwitchItem(switchItem) === blockId && switchItem.textContent === selectedText) { select(switchItem); } } } item.addEventListener("click", handler); if (item.textContent === window.localStorage.getItem(blockId)) { select(item); } }); +} function select(selected) { for (var child of selected.parentNode.children) { child.classList.remove("selected"); } selected.classList.add("selected"); for (var child of selected.parentNode.parentNode.children) { if (child.classList.contains("content")) { if (selected.dataset.blockName === child.dataset.blockName) { child.classList.remove("hidden"); } else { child.classList.add("hidden"); } } } } function blockIdForSwitchItem(item) { idComponents = [] for (var switchItem of item.parentNode.querySelectorAll(".switch--item")) { idComponents.push(switchItem.textContent.toLowerCase()); } return idComponents.sort().join("-") +} window.onload = function() { addBlockSwitches(); globalSwitch(); +}; + +# Spring Cloud Sleuth Reference Documentation Adrian Cole, Spencer Gibb, Marcin Grzejszczak, Dave Syer, Jay Bryant @@ -14,3 +29,5 @@ The reference documentation consists of the following sections: | [“How-to” Guides](howto.html#howto) | Add sampling, propagate remote tags, and more. | | [Spring Cloud Sleuth Integrations](integrations.html#sleuth-integration) | Instrumentation configuration, context propagation, and more. | | [Appendices](appendix.html#appendix) | Span definitions and configuration properties. | + +if (window.parent == window) {(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1\*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga');ga('create', 'UA-2728886-23', 'auto', {'siteSpeedSampleRate': 100});ga('send', 'pageview');} \ No newline at end of file diff --git a/docs/en/spring-cloud/spring-cloud-stream.md b/docs/en/spring-cloud/spring-cloud-stream.md index b692e6079ca528d9283bb3ea77a8d4cd9e29937f..51bb355b053d3a954125cefa2d0206d47883612a 100644 --- a/docs/en/spring-cloud/spring-cloud-stream.md +++ b/docs/en/spring-cloud/spring-cloud-stream.md @@ -1,5 +1,42 @@ -Spring Cloud Stream Reference Documentation -========== +Spring Cloud Stream Reference Documentation.hidden { display: none; +} .switch { border-width: 1px 1px 0 1px; border-style: solid; border-color: #7a2518; display: inline-block; +} .switch--item { padding: 10px; background-color: #ffffff; color: #7a2518; display: inline-block; cursor: pointer; +} .switch--item:not(:first-child) { border-width: 0 0 0 1px; border-style: solid; border-color: #7a2518; +} .switch--item.selected { background-color: #7a2519; color: #ffffff; +} function addBlockSwitches() { for (var primary of document.querySelectorAll('.primary')) { var switchItem = createSwitchItem(primary, createBlockSwitch(primary)); switchItem.item.classList.add("selected"); var title = primary.querySelector('.title') title.remove(); } for (var secondary of document.querySelectorAll('.secondary')) { var primary = findPrimary(secondary); if (primary === null) { console.error("Found secondary block with no primary sibling"); } else { var switchItem = createSwitchItem(secondary, primary.querySelector('.switch')); switchItem.content.classList.add("hidden"); primary.append(switchItem.content); secondary.remove(); } } +} function createElementFromHtml(html) { var template = document.createElement('template'); template.innerHTML = html; return template.content.firstChild; +} function createBlockSwitch(primary) { var blockSwitch = createElementFromHtml('\
\'); primary.prepend(blockSwitch) return blockSwitch; +} function findPrimary(secondary) { var candidate = secondary.previousElementSibling; while (candidate != null && !candidate.classList.contains('primary')) { candidate = candidate.previousElementSibling; } return candidate; +} function createSwitchItem(block, blockSwitch) { var blockName = block.querySelector('.title').textContent; var content = block.querySelectorAll('.content').item(0); var colist = nextSibling(block, '.colist'); if (colist != null) { content.append(colist); } var item = createElementFromHtml('\
' + blockName + '\'); item.dataset.blockName = blockName; content.dataset.blockName = blockName; blockSwitch.append(item); return {'item': item, 'content': content}; +} function nextSibling(element, selector) { var sibling = element.nextElementSibling; while (sibling) { if (sibling.matches(selector)) { return sibling; } sibling = sibling.nextElementSibling; } +} function globalSwitch() { document.querySelectorAll(".switch--item").forEach(function(item) { var blockId = blockIdForSwitchItem(item); var handler = function(event) { selectedText = event.target.textContent; window.localStorage.setItem(blockId, selectedText); for (var switchItem of document.querySelectorAll(".switch--item")) { if (blockIdForSwitchItem(switchItem) === blockId && switchItem.textContent === selectedText) { select(switchItem); } } } item.addEventListener("click", handler); if (item.textContent === window.localStorage.getItem(blockId)) { select(item); } }); +} function select(selected) { for (var child of selected.parentNode.children) { child.classList.remove("selected"); } selected.classList.add("selected"); for (var child of selected.parentNode.parentNode.children) { if (child.classList.contains("content")) { if (selected.dataset.blockName === child.dataset.blockName) { child.classList.remove("hidden"); } else { child.classList.add("hidden"); } } } } function blockIdForSwitchItem(item) { idComponents = [] for (var switchItem of item.parentNode.querySelectorAll(".switch--item")) { idComponents.push(switchItem.textContent.toLowerCase()); } return idComponents.sort().join("-") +} window.onload = function() { addBlockSwitches(); globalSwitch(); +}; + +# Spring Cloud Stream Reference Documentation + +Sabby Anandan +Marius Bogoevici +Eric Bottard +Mark Fisher +Ilayaperumal Gopinathan +Mark Heckler +Gunnar Hillert +Mark Pollack +Patrick Peralta +Glenn Renfro +Thomas Risberg +Dave Syer +David Turanski +Janne Valkealahti +Benjamin Klein +Vinicius Carvalho +Gary Russell +Oleg Zhurakousky +Jay Bryant +Soby Chacko +Domenico Sibilio **3.2.2** @@ -19,3 +56,5 @@ Relevant Links: |--------------------------------------------------------------------------------|------------------------------------------------------| |[Enterprise Integration Patterns](http://www.enterpriseintegrationpatterns.com/)|Patterns and Best Practices for Enterprise Integration| | [Spring Integration](https://spring.io/projects/spring-integration) | Spring Integration framework | + +if (window.parent == window) {(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1\*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga');ga('create', 'UA-2728886-23', 'auto', {'siteSpeedSampleRate': 100});ga('send', 'pageview');} \ No newline at end of file diff --git a/docs/en/spring-cloud/spring-cloud-task.md b/docs/en/spring-cloud/spring-cloud-task.md index 5c0bdc36227bccf1ce89b6c24e78c03d43f1c552..6a4b51bc8bd990ef27042260c2e2b5336ef4d9e4 100644 --- a/docs/en/spring-cloud/spring-cloud-task.md +++ b/docs/en/spring-cloud/spring-cloud-task.md @@ -1,16 +1,123 @@ -Spring Cloud Task Reference Guide -========== - - -[](#preface)[Preface](#preface) -========== +Spring Cloud Task Reference Guide.hidden { display: none; +} .switch { border-width: 1px 1px 0 1px; border-style: solid; border-color: #7a2518; display: inline-block; +} .switch--item { padding: 10px; background-color: #ffffff; color: #7a2518; display: inline-block; cursor: pointer; +} .switch--item:not(:first-child) { border-width: 0 0 0 1px; border-style: solid; border-color: #7a2518; +} .switch--item.selected { background-color: #7a2519; color: #ffffff; +} function addBlockSwitches() { for (var primary of document.querySelectorAll('.primary')) { var switchItem = createSwitchItem(primary, createBlockSwitch(primary)); switchItem.item.classList.add("selected"); var title = primary.querySelector('.title') title.remove(); } for (var secondary of document.querySelectorAll('.secondary')) { var primary = findPrimary(secondary); if (primary === null) { console.error("Found secondary block with no primary sibling"); } else { var switchItem = createSwitchItem(secondary, primary.querySelector('.switch')); switchItem.content.classList.add("hidden"); primary.append(switchItem.content); secondary.remove(); } } +} function createElementFromHtml(html) { var template = document.createElement('template'); template.innerHTML = html; return template.content.firstChild; +} function createBlockSwitch(primary) { var blockSwitch = createElementFromHtml('\
\'); primary.prepend(blockSwitch) return blockSwitch; +} function findPrimary(secondary) { var candidate = secondary.previousElementSibling; while (candidate != null && !candidate.classList.contains('primary')) { candidate = candidate.previousElementSibling; } return candidate; +} function createSwitchItem(block, blockSwitch) { var blockName = block.querySelector('.title').textContent; var content = block.querySelectorAll('.content').item(0); var colist = nextSibling(block, '.colist'); if (colist != null) { content.append(colist); } var item = createElementFromHtml('\
' + blockName + '\'); item.dataset.blockName = blockName; content.dataset.blockName = blockName; blockSwitch.append(item); return {'item': item, 'content': content}; +} function nextSibling(element, selector) { var sibling = element.nextElementSibling; while (sibling) { if (sibling.matches(selector)) { return sibling; } sibling = sibling.nextElementSibling; } +} function globalSwitch() { document.querySelectorAll(".switch--item").forEach(function(item) { var blockId = blockIdForSwitchItem(item); var handler = function(event) { selectedText = event.target.textContent; window.localStorage.setItem(blockId, selectedText); for (var switchItem of document.querySelectorAll(".switch--item")) { if (blockIdForSwitchItem(switchItem) === blockId && switchItem.textContent === selectedText) { select(switchItem); } } } item.addEventListener("click", handler); if (item.textContent === window.localStorage.getItem(blockId)) { select(item); } }); +} function select(selected) { for (var child of selected.parentNode.children) { child.classList.remove("selected"); } selected.classList.add("selected"); for (var child of selected.parentNode.parentNode.children) { if (child.classList.contains("content")) { if (selected.dataset.blockName === child.dataset.blockName) { child.classList.remove("hidden"); } else { child.classList.add("hidden"); } } } } function blockIdForSwitchItem(item) { idComponents = [] for (var switchItem of item.parentNode.querySelectorAll(".switch--item")) { idComponents.push(switchItem.textContent.toLowerCase()); } return idComponents.sort().join("-") +} window.onload = function() { addBlockSwitches(); globalSwitch(); +}; + +# Spring Cloud Task Reference Guide + +Michael Minella, Glenn Renfro, Jay Bryant + +Table of Contents + +* [Preface](#preface) + * [1. About the documentation](#about-the-documentation) + * [2. Getting help](#task-documentation-getting-help) + * [3. First Steps](#task-documentation-first-steps) + +* [Getting started](#getting-started) + * [4. Introducing Spring Cloud Task](#getting-started-introducing-spring-cloud-task) + * [5. System Requirements](#getting-started-system-requirements) + * [5.1. Database Requirements](#database-requirements) + + * [6. Developing Your First Spring Cloud Task Application](#getting-started-developing-first-task) + * [6.1. Creating the Spring Task Project using Spring Initializr](#getting-started-creating-project) + * [6.2. Writing the Code](#getting-started-writing-the-code) + * [6.3. Running the Example](#getting-started-running-the-example) + +* [Features](#features) + * [7. The lifecycle of a Spring Cloud Task](#features-lifecycle) + * [7.1. The TaskExecution](#features-task-execution-details) + * [7.2. Mapping Exit Codes](#features-lifecycle-exit-codes) + + * [8. Configuration](#features-configuration) + * [8.1. DataSource](#features-data-source) + * [8.2. Table Prefix](#features-table-prefix) + * [8.3. Enable/Disable table initialization](#features-table-initialization) + * [8.4. Externally Generated Task ID](#features-generated_task_id) + * [8.5. External Task Id](#features-external_task_id) + * [8.6. Parent Task Id](#features-parent_task_id) + * [8.7. TaskConfigurer](#features-task-configurer) + * [8.8. Task Name](#features-task-name) + * [8.9. Task Execution Listener](#features-task-execution-listener) + * [8.10. Restricting Spring Cloud Task Instances](#features-single-instance-enabled) + * [8.11. Disabling Spring Cloud Task Auto Configuration](#disabling-spring-cloud-task-auto-configuration) + * [8.12. Closing the Context](#closing-the-context) + +* [Batch](#batch) + * [9. Associating a Job Execution to the Task in which It Was Executed](#batch-association) + * [9.1. Overriding the TaskBatchExecutionListener](#batch-association-override) + + * [10. Remote Partitioning](#batch-partitioning) + * [10.1. Notes on Developing a Batch-partitioned application for the Kubernetes Platform](#notes-on-developing-a-batch-partitioned-application-for-the-kubernetes-platform) + * [10.2. Notes on Developing a Batch-partitioned Application for the Cloud Foundry Platform](#notes-on-developing-a-batch-partitioned-application-for-the-cloud-foundry-platform) + + * [11. Batch Informational Messages](#batch-informational-messages) + * [12. Batch Job Exit Codes](#batch-failures-and-tasks) + +* [Single Step Batch Job Starter](#batch-job-starter) + * [13. Defining a Job](#job-definition) + * [13.1. Properties](#job-definition-properties) + + * [14. Autoconfiguration for ItemReader Implementations](#item-readers) + * [14.1. AmqpItemReader](#amqpitemreader) + * [14.2. FlatFileItemReader](#flatfileitemreader) + * [14.3. JdbcCursorItemReader](#jdbcCursorItemReader) + * [14.4. KafkaItemReader](#kafkaItemReader) + + * [15. ItemProcessor Configuration](#item-processors) + * [16. Autoconfiguration for ItemWriter implementations](#item-writers) + * [16.1. AmqpItemWriter](#amqpitemwriter) + * [16.2. FlatFileItemWriter](#flatfileitemwriter) + * [16.3. JdbcBatchItemWriter](#jdbcitemwriter) + * [16.4. KafkaItemWriter](#kafkaitemwriter) + +* [Spring Cloud Stream Integration](#stream-integration) + * [17. Launching a Task from a Spring Cloud Stream](#stream-integration-launching-sink) + * [17.1. Spring Cloud Data Flow](#stream-integration-launching-sink-dataflow) + + * [18. Spring Cloud Task Events](#stream-integration-events) + * [18.1. Disabling Specific Task Events](#stream-integration-disable-task-events) + + * [19. Spring Batch Events](#stream-integration-batch-events) + * [19.1. Sending Batch Events to Different Channels](#sending-batch-events-to-different-channels) + * [19.2. Disabling Batch Events](#disabling-batch-events) + * [19.3. Emit Order for Batch Events](#emit-order-for-batch-events) + +* [Appendices](#appendix) + * [20. Task Repository Schema](#appendix-task-repository-schema) + * [20.1. Table Information](#table-information) + * [20.2. SQL Server](#sql-server) + + * [21. Building This Documentation](#appendix-building-the-documentation) + * [22. Running a Task App on Cloud Foundry](#appendix-cloud-foundry) + +Version 2.4.1 + +© 2009-2021 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. + +# [](#preface)[Preface](#preface) This section provides a brief overview of the Spring Cloud Task reference documentation. Think of it as a map for the rest of the document. You can read this reference guide in a linear fashion or you can skip sections if something does not interest you. -[](#about-the-documentation)[1. About the documentation](#about-the-documentation) ----------- +## [](#about-the-documentation)[1. About the documentation](#about-the-documentation) The Spring Cloud Task reference guide is available in [html](https://docs.spring.io/spring-cloud-task/docs/current/reference)and [pdf](https://docs.spring.io/spring-cloud-task/docs/current/reference/index.pdf),[epub](https://docs.spring.io/spring-cloud-task/docs/current/reference/index.epub) . The latest copy is available at [docs.spring.io/spring-cloud-task/docs/current-SNAPSHOT/reference/html/](https://docs.spring.io/spring-cloud-task/docs/current-SNAPSHOT/reference/html/). @@ -19,8 +126,7 @@ Copies of this document may be made for your own use and for distribution to oth 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. -[](#task-documentation-getting-help)[2. Getting help](#task-documentation-getting-help) ----------- +## [](#task-documentation-getting-help)[2. Getting help](#task-documentation-getting-help) Having trouble with Spring Cloud Task? We would like to help! @@ -32,8 +138,7 @@ Having trouble with Spring Cloud Task? We would like to help! | |All of Spring Cloud Task is open source, including the documentation. If you find
a problem with the docs or if you just want to improve them, please [get
involved](https://github.com/spring-cloud/spring-cloud-task/tree/master).| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -[](#task-documentation-first-steps)[3. First Steps](#task-documentation-first-steps) ----------- +## [](#task-documentation-first-steps)[3. First Steps](#task-documentation-first-steps) If you are just getting started with Spring Cloud Task or with 'Spring' in general, we suggesting reading the [Getting started](#getting-started) chapter. @@ -47,28 +152,25 @@ To get started from scratch, read the following sections: To follow the tutorial, read[Developing Your First Spring Cloud Task Application](#getting-started-developing-first-task) To run your example, read[Running the Example](#getting-started-running-the-example) -[](#getting-started)[Getting started](#getting-started) -========== +# [](#getting-started)[Getting started](#getting-started) If you are just getting started with Spring Cloud Task, you should read this section. Here, we answer the basic “what?”, “how?”, and “why?” questions. We start with a gentle introduction to Spring Cloud Task. We then build a Spring Cloud Task application, discussing some core principles as we go. -[](#getting-started-introducing-spring-cloud-task)[4. Introducing Spring Cloud Task](#getting-started-introducing-spring-cloud-task) ----------- +## [](#getting-started-introducing-spring-cloud-task)[4. Introducing Spring Cloud Task](#getting-started-introducing-spring-cloud-task) Spring Cloud Task makes it easy to create short-lived microservices. It provides capabilities that let short lived JVM processes be executed on demand in a production environment. -[](#getting-started-system-requirements)[5. System Requirements](#getting-started-system-requirements) ----------- +## [](#getting-started-system-requirements)[5. System Requirements](#getting-started-system-requirements) You need to have Java installed (Java 8 or better). To build, you need to have Maven installed as well. -### [](#database-requirements)[5.1. Database Requirements](#database-requirements) ### +### [](#database-requirements)[5.1. Database Requirements](#database-requirements) Spring Cloud Task uses a relational database to store the results of an executed task. While you can begin developing a task without a database (the status of the task is logged @@ -89,8 +191,7 @@ use a supported database. Spring Cloud Task currently supports the following dat * SqlServer -[](#getting-started-developing-first-task)[6. Developing Your First Spring Cloud Task Application](#getting-started-developing-first-task) ----------- +## [](#getting-started-developing-first-task)[6. Developing Your First Spring Cloud Task Application](#getting-started-developing-first-task) A good place to start is with a simple “Hello, World!” application, so we create the Spring Cloud Task equivalent to highlight the features of the framework. Most IDEs have @@ -99,7 +200,7 @@ good support for Apache Maven, so we use it as the build tool for this project. | |The spring.io web site contains many [“`Getting Started`”
guides](https://spring.io/guides) that use Spring Boot. If you need to solve a specific problem, check there first.
You can shortcut the following steps by going to the[Spring Initializr](https://start.spring.io/) and creating a new project. Doing so
automatically generates a new project structure so that you can start coding right away.
We recommend experimenting with the Spring Initializr to become familiar with it.| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#getting-started-creating-project)[6.1. Creating the Spring Task Project using Spring Initializr](#getting-started-creating-project) ### +### [](#getting-started-creating-project)[6.1. Creating the Spring Task Project using Spring Initializr](#getting-started-creating-project) Now we can create and test an application that prints `Hello, World!` to the console. @@ -119,7 +220,7 @@ To do so: 2. Unzip the helloworld.zip file and import the project into your favorite IDE. -### [](#getting-started-writing-the-code)[6.2. Writing the Code](#getting-started-writing-the-code) ### +### [](#getting-started-writing-the-code)[6.2. Writing the Code](#getting-started-writing-the-code) To finish our application, we need to update the generated `HelloworldApplication` with the following contents so that it launches a Task. @@ -172,7 +273,7 @@ logging.level.org.springframework.cloud.task=DEBUG spring.application.name=helloWorld ``` -#### [](#getting-started-at-task)[6.2.1. Task Auto Configuration](#getting-started-at-task) #### +#### [](#getting-started-at-task)[6.2.1. Task Auto Configuration](#getting-started-at-task) When including Spring Cloud Task Starter dependency, Task auto configures all beans to bootstrap it’s functionality. Part of this configuration registers the `TaskRepository` and the infrastructure for its use. @@ -187,12 +288,12 @@ Spring Cloud Task. When our sample application runs, Spring Boot launches our `HelloWorldCommandLineRunner`and outputs our “Hello, World!” message to standard out. The `TaskLifecycleListener`records the start of the task and the end of the task in the repository. -#### [](#getting-started-main-method)[6.2.2. The main method](#getting-started-main-method) #### +#### [](#getting-started-main-method)[6.2.2. The main method](#getting-started-main-method) The main method serves as the entry point to any java application. Our main method delegates to Spring Boot’s [SpringApplication](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-spring-application.html) class. -#### [](#getting-started-clr)[6.2.3. The CommandLineRunner](#getting-started-clr) #### +#### [](#getting-started-clr)[6.2.3. The CommandLineRunner](#getting-started-clr) Spring includes many ways to bootstrap an application’s logic. Spring Boot provides a convenient method of doing so in an organized manner through its `*Runner` interfaces @@ -205,7 +306,7 @@ to once they are all complete. Spring Boot lets an application use multiple`*Run | |Any processing bootstrapped from mechanisms other than a `CommandLineRunner` or`ApplicationRunner` (by using `InitializingBean#afterPropertiesSet` for example) is not
recorded by Spring Cloud Task.| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#getting-started-running-the-example)[6.3. Running the Example](#getting-started-running-the-example) ### +### [](#getting-started-running-the-example)[6.3. Running the Example](#getting-started-running-the-example) At this point, our application should work. Since this application is Spring Boot-based, we can run it from the command line by using `$ mvn spring-boot:run` from the root @@ -256,14 +357,12 @@ The preceding output has three lines that of interest to us here: | |A simple task application can be found in the samples module of the Spring Cloud
Task Project[here](https://github.com/spring-cloud/spring-cloud-task/tree/master/spring-cloud-task-samples/timestamp).| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -[](#features)[Features](#features) -========== +# [](#features)[Features](#features) This section goes into more detail about Spring Cloud Task, including how to use it, how to configure it, and the appropriate extension points. -[](#features-lifecycle)[7. The lifecycle of a Spring Cloud Task](#features-lifecycle) ----------- +## [](#features-lifecycle)[7. The lifecycle of a Spring Cloud Task](#features-lifecycle) In most cases, the modern cloud environment is designed around the execution of processes that are not expected to end. If they do end, they are typically restarted. While most @@ -304,7 +403,7 @@ updated in the repository with the results. | |If the application requires the `ApplicationContext` to be closed at the
completion of a task (all `*Runner#run` methods have been called and the task
repository has been updated), set the property `spring.cloud.task.closecontextEnabled`to true.| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#features-task-execution-details)[7.1. The TaskExecution](#features-task-execution-details) ### +### [](#features-task-execution-details)[7.1. The TaskExecution](#features-task-execution-details) The information stored in the `TaskRepository` is modeled in the `TaskExecution` class and consists of the following information: @@ -320,7 +419,7 @@ consists of the following information: |`errorMessage`| If an exception is the cause of the end of the task (as indicated by an`ApplicationFailedEvent`), the stack trace for that exception is stored here. | | `arguments` | A `List` of the string command line arguments as they were passed into the executable
boot application. | -### [](#features-lifecycle-exit-codes)[7.2. Mapping Exit Codes](#features-lifecycle-exit-codes) ### +### [](#features-lifecycle-exit-codes)[7.2. Mapping Exit Codes](#features-lifecycle-exit-codes) When a task completes, it tries to return an exit code to the OS. If we take a look at our [original example](#getting-started-developing-first-task), we can see that we are @@ -338,13 +437,12 @@ otherwise specified within the code. | |While the task is running, the exit code is stored as a null in the repository.
Once the task completes, the appropriate exit code is stored based on the guidelines described
earlier in this section.| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -[](#features-configuration)[8. Configuration](#features-configuration) ----------- +## [](#features-configuration)[8. Configuration](#features-configuration) Spring Cloud Task provides a ready-to-use configuration, as defined in the`DefaultTaskConfigurer` and `SimpleTaskConfiguration` classes. This section walks through the defaults and how to customize Spring Cloud Task for your needs. -### [](#features-data-source)[8.1. DataSource](#features-data-source) ### +### [](#features-data-source)[8.1. DataSource](#features-data-source) Spring Cloud Task uses a datasource for storing the results of task executions. By default, we provide an in-memory instance of H2 to provide a simple method of @@ -359,7 +457,7 @@ If your application uses more than one `DataSource`, you need to configure the t repository with the appropriate `DataSource`. This customization can be done through an implementation of `TaskConfigurer`. -### [](#features-table-prefix)[8.2. Table Prefix](#features-table-prefix) ### +### [](#features-table-prefix)[8.2. Table Prefix](#features-table-prefix) One modifiable property of `TaskRepository` is the table prefix for the task tables. By default, they are all prefaced with `TASK_`. `TASK_EXECUTION` and `TASK_EXECUTION_PARAMS`are two examples. However, there are potential reasons to modify this prefix. If the @@ -374,7 +472,7 @@ create the task tables that meet both the criteria for the task table schema but with modifications that are required for a user’s business needs. You can utilize the Spring Cloud Task Schema DDL as a guide when creating your own Task DDL as seen[here](https://github.com/spring-cloud/spring-cloud-task/tree/master/spring-cloud-task-core/src/main/resources/org/springframework/cloud/task). -### [](#features-table-initialization)[8.3. Enable/Disable table initialization](#features-table-initialization) ### +### [](#features-table-initialization)[8.3. Enable/Disable table initialization](#features-table-initialization) In cases where you are creating the task tables and do not wish for Spring Cloud Task to create them at task startup, set the `spring.cloud.task.initialize-enabled` property to`false`, as follows: @@ -386,7 +484,7 @@ It defaults to `true`. | |The property `spring.cloud.task.initialize.enable` has been deprecated.| |---|-----------------------------------------------------------------------| -### [](#features-generated_task_id)[8.4. Externally Generated Task ID](#features-generated_task_id) ### +### [](#features-generated_task_id)[8.4. Externally Generated Task ID](#features-generated_task_id) In some cases, you may want to allow for the time difference between when a task is requested and when the infrastructure actually launches it. Spring Cloud Task lets you @@ -403,7 +501,7 @@ following property: `spring.cloud.task.executionid=yourtaskId` -### [](#features-external_task_id)[8.5. External Task Id](#features-external_task_id) ### +### [](#features-external_task_id)[8.5. External Task Id](#features-external_task_id) Spring Cloud Task lets you store an external task ID for each`TaskExecution`. An example of this would be a task ID provided by Cloud Foundry when a task is launched on the platform. @@ -412,7 +510,7 @@ following property: `spring.cloud.task.external-execution-id=` -### [](#features-parent_task_id)[8.6. Parent Task Id](#features-parent_task_id) ### +### [](#features-parent_task_id)[8.6. Parent Task Id](#features-parent_task_id) Spring Cloud Task lets you store a parent task ID for each `TaskExecution`. An example of this would be a task that executes another task or tasks and you want to record which task @@ -420,7 +518,7 @@ launched each of the child tasks. In order to configure your Task to set a paren `spring.cloud.task.parent-execution-id=` -### [](#features-task-configurer)[8.7. TaskConfigurer](#features-task-configurer) ### +### [](#features-task-configurer)[8.7. TaskConfigurer](#features-task-configurer) The `TaskConfigurer` is a strategy interface that lets you customize the way components of Spring Cloud Task are configured. By default, we provide the `DefaultTaskConfigurer` that @@ -442,7 +540,7 @@ may be required. | |Users should not directly use getter methods from a `TaskConfigurer` directly
unless they are using it to supply implementations to be exposed as Spring Beans.| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#features-task-name)[8.8. Task Name](#features-task-name) ### +### [](#features-task-name)[8.8. Task Name](#features-task-name) In most cases, the name of the task is the application name as configured in Spring Boot. However, there are some cases where you may want to map the run of a task to a @@ -457,7 +555,7 @@ following options (in order of precedence): 2. The application name as resolved using Spring Boot’s rules (obtained through`ApplicationContext#getId`). -### [](#features-task-execution-listener)[8.9. Task Execution Listener](#features-task-execution-listener) ### +### [](#features-task-execution-listener)[8.9. Task Execution Listener](#features-task-execution-listener) `TaskExecutionListener` lets you register listeners for specific events that occur during the task lifecycle. To do so, create a class that implements the`TaskExecutionListener` interface. The class that implements the `TaskExecutionListener`interface is notified of the following events: @@ -502,7 +600,7 @@ The following example shows the three annotations in use: | |Inserting an `ApplicationListener` earlier in the chain than `TaskLifecycleListener` exists may cause unexpected effects.| |---|-------------------------------------------------------------------------------------------------------------------------| -#### [](#features-task-execution-listener-Exceptions)[8.9.1. Exceptions Thrown by Task Execution Listener](#features-task-execution-listener-Exceptions) #### +#### [](#features-task-execution-listener-Exceptions)[8.9.1. Exceptions Thrown by Task Execution Listener](#features-task-execution-listener-Exceptions) If an exception is thrown by a `TaskExecutionListener` event handler, all listener processing for that event handler stops. For example, if three `onTaskStartup` listeners @@ -520,7 +618,7 @@ If an exception is thrown in either a `onTaskEnd` or `onTaskFailed`method, the e | |In the case of an exception being thrown in a `onTaskStartup`, `onTaskEnd`, or `onTaskFailed`you can not override the exit code for the application using `ExitCodeExceptionMapper`.| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -#### [](#features-task-execution-listener-exit-messages)[8.9.2. Exit Messages](#features-task-execution-listener-exit-messages) #### +#### [](#features-task-execution-listener-exit-messages)[8.9.2. Exit Messages](#features-task-execution-listener-exit-messages) You can set the exit message for a task programmatically by using a`TaskExecutionListener`. This is done by setting the `TaskExecution’s` `exitMessage`, which then gets passed into the `TaskExecutionListener`. The following example shows @@ -545,7 +643,7 @@ For example, if you set an `exitMessage` for the `onTaskStartup` and `onTaskFail the `onTaskFailed` is stored. Also if you set the `exitMessage` with an`onTaskEnd` listener, the `exitMessage` from the `onTaskEnd` supersedes the exit messages from both the `onTaskStartup` and `onTaskFailed`. -### [](#features-single-instance-enabled)[8.10. Restricting Spring Cloud Task Instances](#features-single-instance-enabled) ### +### [](#features-single-instance-enabled)[8.10. Restricting Spring Cloud Task Instances](#features-single-instance-enabled) Spring Cloud Task lets you establish that only one task with a given task name can be run at a time. To do so, you need to establish the [task name](#features-task-name) and set`spring.cloud.task.single-instance-enabled=true` for each task execution. While the first @@ -573,7 +671,7 @@ application: | |The exit code for the application will be 1 if the task fails because this feature
is enabled and another task is running with the same task name.| |---|------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#disabling-spring-cloud-task-auto-configuration)[8.11. Disabling Spring Cloud Task Auto Configuration](#disabling-spring-cloud-task-auto-configuration) ### +### [](#disabling-spring-cloud-task-auto-configuration)[8.11. Disabling Spring Cloud Task Auto Configuration](#disabling-spring-cloud-task-auto-configuration) In cases where Spring Cloud Task should not be auto configured for an implementation, you can disable Task’s auto configuration. This can be done either by adding the following annotation to your Task application: @@ -584,7 +682,7 @@ This can be done either by adding the following annotation to your Task applicat You may also disable Task auto configuration by setting the `spring.cloud.task.autoconfiguration.enabled` property to `false`. -### [](#closing-the-context)[8.12. Closing the Context](#closing-the-context) ### +### [](#closing-the-context)[8.12. Closing the Context](#closing-the-context) If the application requires the `ApplicationContext` to be closed at the completion of a task (all `*Runner#run` methods have been called and the task @@ -597,16 +695,14 @@ set the `spring.cloud.task.closecontextEnabled` property to `true` when launchin This will close the application’s context once the task is complete. Thus allowing the application to terminate. -[](#batch)[Batch](#batch) -========== +# [](#batch)[Batch](#batch) This section goes into more detail about Spring Cloud Task’s integration with Spring Batch. Tracking the association between a job execution and the task in which it was executed as well as remote partitioning through Spring Cloud Deployer are covered in this section. -[](#batch-association)[9. Associating a Job Execution to the Task in which It Was Executed](#batch-association) ----------- +## [](#batch-association)[9. Associating a Job Execution to the Task in which It Was Executed](#batch-association) Spring Boot provides facilities for the execution of batch jobs within an über-jar. Spring Boot’s support of this functionality lets a developer execute multiple batch jobs @@ -620,7 +716,7 @@ this listener is auto configured in any context that has both a Spring Batch Job configured (by having a bean of type `Job` defined in the context) and the`spring-cloud-task-batch` jar on the classpath. The listener is injected into all jobs that meet those conditions. -### [](#batch-association-override)[9.1. Overriding the TaskBatchExecutionListener](#batch-association-override) ### +### [](#batch-association-override)[9.1. Overriding the TaskBatchExecutionListener](#batch-association-override) To prevent the listener from being injected into any batch jobs within the current context, you can disable the autoconfiguration by using standard Spring Boot mechanisms. @@ -642,8 +738,7 @@ public TaskBatchExecutionListenerBeanPostProcessor batchTaskExecutionListenerBea | |You can find a sample batch application in the samples module of the Spring Cloud
Task Project,[here](https://github.com/spring-cloud/spring-cloud-task/tree/master/spring-cloud-task-samples/batch-job).| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -[](#batch-partitioning)[10. Remote Partitioning](#batch-partitioning) ----------- +## [](#batch-partitioning)[10. Remote Partitioning](#batch-partitioning) Spring Cloud Deployer provides facilities for launching Spring Boot-based applications on most cloud infrastructures. The `DeployerPartitionHandler` and`DeployerStepExecutionHandler` delegate the launching of worker step executions to Spring @@ -714,7 +809,7 @@ public DeployerStepExecutionHandler stepExecutionHandler(JobExplorer jobExplorer | |You can find a sample remote partition application in the samples module of the
Spring Cloud Task project,[here](https://github.com/spring-cloud/spring-cloud-task/tree/master/spring-cloud-task-samples/partitioned-batch-job).| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#notes-on-developing-a-batch-partitioned-application-for-the-kubernetes-platform)[10.1. Notes on Developing a Batch-partitioned application for the Kubernetes Platform](#notes-on-developing-a-batch-partitioned-application-for-the-kubernetes-platform) ### +### [](#notes-on-developing-a-batch-partitioned-application-for-the-kubernetes-platform)[10.1. Notes on Developing a Batch-partitioned application for the Kubernetes Platform](#notes-on-developing-a-batch-partitioned-application-for-the-kubernetes-platform) * When deploying partitioned apps on the Kubernetes platform, you must use the following dependency for the Spring Cloud Kubernetes Deployer: @@ -730,7 +825,7 @@ public DeployerStepExecutionHandler stepExecutionHandler(JobExplorer jobExplorer the following regex pattern: `[a-z0-9]([-a-z0-9]*[a-z0-9])`. Otherwise, an exception is thrown. -### [](#notes-on-developing-a-batch-partitioned-application-for-the-cloud-foundry-platform)[10.2. Notes on Developing a Batch-partitioned Application for the Cloud Foundry Platform](#notes-on-developing-a-batch-partitioned-application-for-the-cloud-foundry-platform) ### +### [](#notes-on-developing-a-batch-partitioned-application-for-the-cloud-foundry-platform)[10.2. Notes on Developing a Batch-partitioned Application for the Cloud Foundry Platform](#notes-on-developing-a-batch-partitioned-application-for-the-cloud-foundry-platform) * When deploying partitioned apps on the Cloud Foundry platform, you must use the following dependencies for the Spring Cloud Foundry Deployer: @@ -790,14 +885,12 @@ spring_cloud_deployer_cloudfoundry_taskTimeout=300 | |When using PCF-Dev, the following environment variable is also required:`spring_cloud_deployer_cloudfoundry_skipSslValidation=true`| |---|-----------------------------------------------------------------------------------------------------------------------------------| -[](#batch-informational-messages)[11. Batch Informational Messages](#batch-informational-messages) ----------- +## [](#batch-informational-messages)[11. Batch Informational Messages](#batch-informational-messages) Spring Cloud Task provides the ability for batch jobs to emit informational messages. The “[Spring Batch Events](#stream-integration-batch-events)” section covers this feature in detail. -[](#batch-failures-and-tasks)[12. Batch Job Exit Codes](#batch-failures-and-tasks) ----------- +## [](#batch-failures-and-tasks)[12. Batch Job Exit Codes](#batch-failures-and-tasks) As discussed [earlier](#features-lifecycle-exit-codes), Spring Cloud Task applications support the ability to record the exit code of a task execution. However, in @@ -814,8 +907,7 @@ Boot. By default, it is configured with the same order. However, if you want to the order in which the `CommandLineRunner` is run, you can set its order by setting the`spring.cloud.task.batch.commandLineRunnerOrder` property. To have your task return the exit code based on the result of the batch job execution, you need to write your own`CommandLineRunner`. -[](#batch-job-starter)[Single Step Batch Job Starter](#batch-job-starter) -========== +# [](#batch-job-starter)[Single Step Batch Job Starter](#batch-job-starter) This section goes into how to develop a Spring Batch `Job` with a single `Step` by using the starter included in Spring Cloud Task. This starter lets you use configuration @@ -838,13 +930,12 @@ To obtain the starter for Gradle, add the following to your build: compile "org.springframework.cloud:spring-cloud-starter-single-step-batch-job:2.3.0" ``` -[](#job-definition)[13. Defining a Job](#job-definition) ----------- +## [](#job-definition)[13. Defining a Job](#job-definition) You can use the starter to define as little as an `ItemReader` or an `ItemWriter` or as much as a full `Job`. In this section, we define which properties are required to be defined to configure a`Job`. -### [](#job-definition-properties)[13.1. Properties](#job-definition-properties) ### +### [](#job-definition-properties)[13.1. Properties](#job-definition-properties) To begin, the starter provides a set of properties that let you configure the basics of a Job with one Step: @@ -865,14 +956,13 @@ mechanisms. | |If you configure your own, the input and output types must match the others in the step.
The `ItemReader` implementations and `ItemWriter` implementations in this starter all use
a `Map` as the input and the output item.| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -[](#item-readers)[14. Autoconfiguration for ItemReader Implementations](#item-readers) ----------- +## [](#item-readers)[14. Autoconfiguration for ItemReader Implementations](#item-readers) This starter provides autoconfiguration for four different `ItemReader` implementations:`AmqpItemReader`, `FlatFileItemReader`, `JdbcCursorItemReader`, and `KafkaItemReader`. In this section, we outline how to configure each of these by using the provided autoconfiguration. -### [](#amqpitemreader)[14.1. AmqpItemReader](#amqpitemreader) ### +### [](#amqpitemreader)[14.1. AmqpItemReader](#amqpitemreader) You can read from a queue or topic with AMQP by using the `AmqpItemReader`. The autoconfiguration for this `ItemReader` implementation is dependent upon two sets of @@ -888,7 +978,7 @@ by setting the following properties: For more information, see the [`AmqpItemReader` documentation](https://docs.spring.io/spring-batch/docs/4.3.x/api/org/springframework/batch/item/amqp/AmqpItemReader.html). -### [](#flatfileitemreader)[14.2. FlatFileItemReader](#flatfileitemreader) ### +### [](#flatfileitemreader)[14.2. FlatFileItemReader](#flatfileitemreader) `FlatFileItemReader` lets you read from flat files (such as CSVs and other file formats). To read from a file, you can provide some components @@ -917,7 +1007,7 @@ following properties to configure the reader: See the [`FlatFileItemReader` documentation](https://docs.spring.io/spring-batch/docs/4.3.x/api/org/springframework/batch/item/file/FlatFileItemReader.html). -### [](#jdbcCursorItemReader)[14.3. JdbcCursorItemReader](#jdbcCursorItemReader) ### +### [](#jdbcCursorItemReader)[14.3. JdbcCursorItemReader](#jdbcCursorItemReader) The `JdbcCursorItemReader` runs a query against a relational database and iterates over the resulting cursor (`ResultSet`) to provide the resulting items. This autoconfiguration @@ -941,7 +1031,7 @@ can also use the following properties to configure a `JdbcCursorItemReader`: See the [`JdbcCursorItemReader` documentation](https://docs.spring.io/spring-batch/docs/4.3.x/api/org/springframework/batch/item/database/JdbcCursorItemReader.html). -### [](#kafkaItemReader)[14.4. KafkaItemReader](#kafkaItemReader) ### +### [](#kafkaItemReader)[14.4. KafkaItemReader](#kafkaItemReader) Ingesting a partition of data from a Kafka topic is useful and exactly what the`KafkaItemReader` can do. To configure a `KafkaItemReader`, two pieces of configuration are required. First, configuring Kafka with Spring Boot’s Kafka @@ -958,22 +1048,20 @@ Once you have configured the Kafka properties from Spring Boot, you can configur See the [`KafkaItemReader` documentation](https://docs.spring.io/spring-batch/docs/4.3.x/api/org/springframework/batch/item/kafka/KafkaItemReader.html). -[](#item-processors)[15. ItemProcessor Configuration](#item-processors) ----------- +## [](#item-processors)[15. ItemProcessor Configuration](#item-processors) The single-step batch job autoconfiguration accepts an `ItemProcessor` if one is available within the `ApplicationContext`. If one is found of the correct type (`ItemProcessor, Map>`), it is autowired into the step. -[](#item-writers)[16. Autoconfiguration for ItemWriter implementations](#item-writers) ----------- +## [](#item-writers)[16. Autoconfiguration for ItemWriter implementations](#item-writers) This starter provides autoconfiguration for `ItemWriter` implementations that match the supported `ItemReader` implementations: `AmqpItemWriter`,`FlatFileItemWriter`, `JdbcItemWriter`, and `KafkaItemWriter`. This section covers how to use autoconfiguration to configure a supported `ItemWriter`. -### [](#amqpitemwriter)[16.1. AmqpItemWriter](#amqpitemwriter) ### +### [](#amqpitemwriter)[16.1. AmqpItemWriter](#amqpitemwriter) To write to a RabbitMQ queue, you need two sets of configuration. First, you need an`AmqpTemplate`. The easiest way to get this is by using Spring Boot’s RabbitMQ autoconfiguration. See the [Spring Boot RabbitMQ documentation](https://docs.spring.io/spring-boot/docs/2.4.x/reference/htmlsingle/#boot-features-amqp). @@ -985,7 +1073,7 @@ following properties: | `spring.batch.job.amqpitemwriter.enabled` |`boolean`| `false` | If `true`, the autoconfiguration runs. | |`spring.batch.job.amqpitemwriter.jsonConverterEnabled`|`boolean`| `true` |Indicates whether `Jackson2JsonMessageConverter` should be registered to convert messages.| -### [](#flatfileitemwriter)[16.2. FlatFileItemWriter](#flatfileitemwriter) ### +### [](#flatfileitemwriter)[16.2. FlatFileItemWriter](#flatfileitemwriter) To write a file as the output of the step, you can configure `FlatFileItemWriter`. Autoconfiguration accepts components that have been explicitly configured (such as `LineAggregator`,`FieldExtractor`, `FlatFileHeaderCallback`, or a `FlatFileFooterCallback`) and @@ -1014,7 +1102,7 @@ components that have been configured by setting the following properties specifi See the [`FlatFileItemWriter` documentation](https://docs.spring.io/spring-batch/docs/4.3.x/api/org/springframework/batch/item/file/FlatFileItemWriter.html). -### [](#jdbcitemwriter)[16.3. JdbcBatchItemWriter](#jdbcitemwriter) ### +### [](#jdbcitemwriter)[16.3. JdbcBatchItemWriter](#jdbcitemwriter) To write the output of a step to a relational database, this starter provides the ability to autoconfigure a `JdbcBatchItemWriter`. The autoconfiguration lets you provide your @@ -1029,7 +1117,7 @@ configuration options by setting the following properties: See the [`JdbcBatchItemWriter` documentation](https://docs.spring.io/spring-batch/docs/4.3.x/api/org/springframework/batch/item/database/JdbcBatchItemWriter.html). -### [](#kafkaitemwriter)[16.4. KafkaItemWriter](#kafkaitemwriter) ### +### [](#kafkaitemwriter)[16.4. KafkaItemWriter](#kafkaitemwriter) To write step output to a Kafka topic, you need `KafkaItemWriter`. This starter provides autoconfiguration for a `KafkaItemWriter` by using facilities from two places. @@ -1043,15 +1131,13 @@ Second, this starter lets you configure two properties on the writer. For more about the configuration options for the `KafkaItemWriter`, see the [`KafkaItemWiter` documentation](https://docs.spring.io/spring-batch/docs/4.3.x/api/org/springframework/batch/item/kafka/KafkaItemWriter.html). -[](#stream-integration)[Spring Cloud Stream Integration](#stream-integration) -========== +# [](#stream-integration)[Spring Cloud Stream Integration](#stream-integration) A task by itself can be useful, but integration of a task into a larger ecosystem lets it be useful for more complex processing and orchestration. This section covers the integration options for Spring Cloud Task with Spring Cloud Stream. -[](#stream-integration-launching-sink)[17. Launching a Task from a Spring Cloud Stream](#stream-integration-launching-sink) ----------- +## [](#stream-integration-launching-sink)[17. Launching a Task from a Spring Cloud Stream](#stream-integration-launching-sink) You can launch tasks from a stream. To do so, create a sink that listens for a message that contains a `TaskLaunchRequest` as its payload. The `TaskLaunchRequest` contains: @@ -1100,7 +1186,7 @@ shown in the following example: | |The `maven.remoteRepositories.springRepo.url` property must be set to the location
of the remote repository in which the über-jar is located. If not set, there is no remote
repository, so it relies upon the local repository only.| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#stream-integration-launching-sink-dataflow)[17.1. Spring Cloud Data Flow](#stream-integration-launching-sink-dataflow) ### +### [](#stream-integration-launching-sink-dataflow)[17.1. Spring Cloud Data Flow](#stream-integration-launching-sink-dataflow) To create a stream in Spring Cloud Data Flow, you must first register the Task Sink Application we created. In the following example, we are registering the Processor and @@ -1117,8 +1203,7 @@ The following example shows how to create a stream from the Spring Cloud Data Fl stream create foo --definition "http --server.port=9000|taskProcessor|taskSink" --deploy ``` -[](#stream-integration-events)[18. Spring Cloud Task Events](#stream-integration-events) ----------- +## [](#stream-integration-events)[18. Spring Cloud Task Events](#stream-integration-events) Spring Cloud Task provides the ability to emit events through a Spring Cloud Stream channel when the task is run through a Spring Cloud Stream channel. A task listener is @@ -1162,12 +1247,11 @@ public class TaskEventsApplication { | |A sample task event application can be found in the samples module
of the Spring Cloud Task Project,[here](https://github.com/spring-cloud/spring-cloud-task/tree/master/spring-cloud-task-samples/task-events).| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#stream-integration-disable-task-events)[18.1. Disabling Specific Task Events](#stream-integration-disable-task-events) ### +### [](#stream-integration-disable-task-events)[18.1. Disabling Specific Task Events](#stream-integration-disable-task-events) To disable task events, you can set the `spring.cloud.task.events.enabled` property to`false`. -[](#stream-integration-batch-events)[19. Spring Batch Events](#stream-integration-batch-events) ----------- +## [](#stream-integration-batch-events)[19. Spring Batch Events](#stream-integration-batch-events) When executing a Spring Batch job through a task, Spring Cloud Task can be configured to emit informational messages based on the Spring Batch listeners available in Spring Batch. @@ -1206,7 +1290,7 @@ configure the input to be `job-execution-events` as follows: | |A sample batch event application can be found in the samples module
of the Spring Cloud Task Project,[here](https://github.com/spring-cloud/spring-cloud-task/tree/master/spring-cloud-task-samples/batch-events).| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#sending-batch-events-to-different-channels)[19.1. Sending Batch Events to Different Channels](#sending-batch-events-to-different-channels) ### +### [](#sending-batch-events-to-different-channels)[19.1. Sending Batch Events to Different Channels](#sending-batch-events-to-different-channels) One of the options that Spring Cloud Task offers for batch events is the ability to alter the channel to which a specific listener can emit its messages. To do so, use the @@ -1216,7 +1300,7 @@ following configuration: `spring.cloud.stream.bindings.step-execution-events.destination=my-step-execution-events` -### [](#disabling-batch-events)[19.2. Disabling Batch Events](#disabling-batch-events) ### +### [](#disabling-batch-events)[19.2. Disabling Batch Events](#disabling-batch-events) To disable the listener functionality for all batch events, use the following configuration: @@ -1239,7 +1323,7 @@ spring.cloud.task.batch.events.item-write.enabled=false spring.cloud.task.batch.events.skip.enabled=false ``` -### [](#emit-order-for-batch-events)[19.3. Emit Order for Batch Events](#emit-order-for-batch-events) ### +### [](#emit-order-for-batch-events)[19.3. Emit Order for Batch Events](#emit-order-for-batch-events) By default, batch events have `Ordered.LOWEST_PRECEDENCE`. To change this value (for example, to 5 ), use the following configuration: @@ -1254,17 +1338,15 @@ spring.cloud.task.batch.events.item-write-order=5 spring.cloud.task.batch.events.skip-order=5 ``` -[](#appendix)[Appendices](#appendix) -========== +# [](#appendix)[Appendices](#appendix) -[](#appendix-task-repository-schema)[20. Task Repository Schema](#appendix-task-repository-schema) ----------- +## [](#appendix-task-repository-schema)[20. Task Repository Schema](#appendix-task-repository-schema) This appendix provides an ERD for the database schema used in the task repository. ![task schema](./images/task_schema.png) -### [](#table-information)[20.1. Table Information](#table-information) ### +### [](#table-information)[20.1. Table Information](#table-information) TASK\_EXECUTION @@ -1315,7 +1397,7 @@ Used for the `single-instance-enabled` feature discussed [here](#features-single | |The DDL for setting up tables for each database type can be found [here](https://github.com/spring-cloud/spring-cloud-task/tree/master/spring-cloud-task-core/src/main/resources/org/springframework/cloud/task).| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#sql-server)[20.2. SQL Server](#sql-server) ### +### [](#sql-server)[20.2. SQL Server](#sql-server) By default Spring Cloud Task uses a sequence table for determining the `TASK_EXECUTION_ID` for the `TASK_EXECUTION` table. However, when launching multiple tasks simultaneously while using SQL Server, this can cause a deadlock to occur on the `TASK_SEQ` table. @@ -1332,17 +1414,17 @@ CREATE SEQUENCE [DBO].[TASK_SEQ] AS BIGINT | |Set the `START WITH` to a higher value than your current execution id.| |---|----------------------------------------------------------------------| -[](#appendix-building-the-documentation)[21. Building This Documentation](#appendix-building-the-documentation) ----------- +## [](#appendix-building-the-documentation)[21. Building This Documentation](#appendix-building-the-documentation) This project uses Maven to generate this documentation. To generate it for yourself, run the following command: `$ ./mvnw clean package -P full`. -[](#appendix-cloud-foundry)[22. Running a Task App on Cloud Foundry](#appendix-cloud-foundry) ----------- +## [](#appendix-cloud-foundry)[22. Running a Task App on Cloud Foundry](#appendix-cloud-foundry) The simplest way to launch a Spring Cloud Task application as a task on Cloud Foundry is to use Spring Cloud Data Flow. Via Spring Cloud Data Flow you can register your task application, create a definition for it and then launch it. You then can track the task execution(s) via a RESTful API, the Spring Cloud Data Flow Shell, or the UI. To learn out to get started installing Data Flow follow the instructions in the[Getting Started](https://docs.spring.io/spring-cloud-dataflow/docs/current/reference/htmlsingle/#getting-started)section of the reference documentation. For info on how to register and launch tasks, see the [Lifecycle of a Task](https://docs.spring.io/spring-cloud-dataflow/docs/current/reference/htmlsingle/#_the_lifecycle_of_a_task) documentation. + +if (window.parent == window) {(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1\*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga');ga('create', 'UA-2728886-23', 'auto', {'siteSpeedSampleRate': 100});ga('send', 'pageview');} \ No newline at end of file diff --git a/docs/en/spring-cloud/spring-cloud-vault.md b/docs/en/spring-cloud/spring-cloud-vault.md index 82dbfc335e2ab87f84316b00f845eafa28339ee0..276bf53d687df942c96195cb1a860dad26fdf9f9 100644 --- a/docs/en/spring-cloud/spring-cloud-vault.md +++ b/docs/en/spring-cloud/spring-cloud-vault.md @@ -1,16 +1,98 @@ -Spring Cloud Vault -========== +Spring Cloud Vault.hidden { display: none; +} .switch { border-width: 1px 1px 0 1px; border-style: solid; border-color: #7a2518; display: inline-block; +} .switch--item { padding: 10px; background-color: #ffffff; color: #7a2518; display: inline-block; cursor: pointer; +} .switch--item:not(:first-child) { border-width: 0 0 0 1px; border-style: solid; border-color: #7a2518; +} .switch--item.selected { background-color: #7a2519; color: #ffffff; +} function addBlockSwitches() { for (var primary of document.querySelectorAll('.primary')) { var switchItem = createSwitchItem(primary, createBlockSwitch(primary)); switchItem.item.classList.add("selected"); var title = primary.querySelector('.title') title.remove(); } for (var secondary of document.querySelectorAll('.secondary')) { var primary = findPrimary(secondary); if (primary === null) { console.error("Found secondary block with no primary sibling"); } else { var switchItem = createSwitchItem(secondary, primary.querySelector('.switch')); switchItem.content.classList.add("hidden"); primary.append(switchItem.content); secondary.remove(); } } +} function createElementFromHtml(html) { var template = document.createElement('template'); template.innerHTML = html; return template.content.firstChild; +} function createBlockSwitch(primary) { var blockSwitch = createElementFromHtml('\
\'); primary.prepend(blockSwitch) return blockSwitch; +} function findPrimary(secondary) { var candidate = secondary.previousElementSibling; while (candidate != null && !candidate.classList.contains('primary')) { candidate = candidate.previousElementSibling; } return candidate; +} function createSwitchItem(block, blockSwitch) { var blockName = block.querySelector('.title').textContent; var content = block.querySelectorAll('.content').item(0); var colist = nextSibling(block, '.colist'); if (colist != null) { content.append(colist); } var item = createElementFromHtml('\
' + blockName + '\'); item.dataset.blockName = blockName; content.dataset.blockName = blockName; blockSwitch.append(item); return {'item': item, 'content': content}; +} function nextSibling(element, selector) { var sibling = element.nextElementSibling; while (sibling) { if (sibling.matches(selector)) { return sibling; } sibling = sibling.nextElementSibling; } +} function globalSwitch() { document.querySelectorAll(".switch--item").forEach(function(item) { var blockId = blockIdForSwitchItem(item); var handler = function(event) { selectedText = event.target.textContent; window.localStorage.setItem(blockId, selectedText); for (var switchItem of document.querySelectorAll(".switch--item")) { if (blockIdForSwitchItem(switchItem) === blockId && switchItem.textContent === selectedText) { select(switchItem); } } } item.addEventListener("click", handler); if (item.textContent === window.localStorage.getItem(blockId)) { select(item); } }); +} function select(selected) { for (var child of selected.parentNode.children) { child.classList.remove("selected"); } selected.classList.add("selected"); for (var child of selected.parentNode.parentNode.children) { if (child.classList.contains("content")) { if (selected.dataset.blockName === child.dataset.blockName) { child.classList.remove("hidden"); } else { child.classList.add("hidden"); } } } } function blockIdForSwitchItem(item) { idComponents = [] for (var switchItem of item.parentNode.querySelectorAll(".switch--item")) { idComponents.push(switchItem.textContent.toLowerCase()); } return idComponents.sort().join("-") +} window.onload = function() { addBlockSwitches(); globalSwitch(); +}; + +# Spring Cloud Vault + +version 3.1.0 + +Table of Contents + +* [1. New & Noteworthy](#new-noteworthy) + * [1.1. New in Spring Cloud Vault 3.0](#new-in-3.0.0) + +* [2. Quick Start](#quick-start) +* [3. Client Side Usage](#client-side-usage) + * [3.1. Authentication](#authentication) + +* [4. ConfigData API](#vault.configdata) + * [4.1. ConfigData Locations](#vault.configdata.locations) + * [4.2. Conditionally enable/disable Vault Configuration](#vault.configdata.location.optional) + * [4.3. Infrastructure Customization](#vault.configdata.customization) + +* [5. Authentication methods](#vault.config.authentication) + * [5.1. Token authentication](#vault.config.authentication.token) + * [5.2. Vault Agent authentication](#vault.config.authentication.vault-agent) + * [5.3. AppId authentication](#vault.config.authentication.appid) + * [5.4. AppRole authentication](#approle-authentication) + * [5.5. AWS-EC2 authentication](#vault.config.authentication.awsec2) + * [5.6. AWS-IAM authentication](#vault.config.authentication.awsiam) + * [5.7. Azure MSI authentication](#vault.config.authentication.azuremsi) + * [5.8. TLS certificate authentication](#vault.config.authentication.clientcert) + * [5.9. Cubbyhole authentication](#vault.config.authentication.cubbyhole) + * [5.10. GCP-GCE authentication](#vault.config.authentication.gcpgce) + * [5.11. GCP-IAM authentication](#vault.config.authentication.gcpiam) + * [5.12. Kubernetes authentication](#vault.config.authentication.kubernetes) + * [5.13. Pivotal CloudFoundry authentication](#vault.config.authentication.pcf) + +* [6. ACL Requirements](#vault.config.acl) + * [6.1. Authentication](#authentication-2) + * [6.2. KeyValue Mount Discovery](#keyvalue-mount-discovery) + * [6.3. SecretLeaseContainer](#secretleasecontainer) + * [6.4. Session Management](#session-management) + +* [7. Secret Backends](#vault.config.backends) + * [7.1. Key-Value Backend](#vault.config.backends.kv.versioned) + * [7.2. Consul](#vault.config.backends.consul) + * [7.3. RabbitMQ](#vault.config.backends.rabbitmq) + * [7.4. AWS](#vault.config.backends.aws) + +* [8. Database backends](#vault.config.backends.database-backends) + * [8.1. Database](#vault.config.backends.database) + * [8.2. Multiple Databases](#vault.config.backends.databases) + * [8.3. Apache Cassandra](#vault.config.backends.cassandra) + * [8.4. Couchbase Database](#vault.config.backends.couchbase) + * [8.5. Elasticsearch](#vault.config.backends.elasticsearch) + * [8.6. MongoDB](#vault.config.backends.mongodb) + * [8.7. MySQL](#vault.config.backends.mysql) + * [8.8. PostgreSQL](#vault.config.backends.postgresql) + +* [9. Customize which secret backends to expose as PropertySource](#vault.config.backends.configurer) +* [10. Custom Secret Backend Implementations](#vault.config.backends.custom) +* [11. Service Registry Configuration](#service-registry-configuration) +* [12. Vault Client Fail Fast](#vault.config.fail-fast) +* [13. Vault Enterprise Namespace Support](#vault.config.namespaces) +* [14. Vault Client SSL configuration](#vault.config.ssl) +* [15. Lease lifecycle management (renewal and revocation)](#vault-lease-renewal) +* [16. Session token lifecycle management (renewal, re-login and revocation)](#vault-session-lifecycle) +* [Appendix A: Common application properties](#common-application-properties) + +© 2016-2021 the original authors. + +| |*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.*| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| Spring Cloud Vault Config provides client-side support for externalized configuration in a distributed system. With [HashiCorp’s Vault](https://www.vaultproject.io) you have a central place to manage external secret properties for applications across all environments. Vault can manage static and dynamic secrets such as username/password for remote applications/resources and provide credentials for external services such as MySQL, PostgreSQL, Apache Cassandra, Couchbase, MongoDB, Consul, AWS and more. -[](#new-noteworthy)[1. New & Noteworthy](#new-noteworthy) ----------- +## [](#new-noteworthy)[1. New & Noteworthy](#new-noteworthy) This section briefly covers items that are new and noteworthy in the latest releases. -### [](#new-in-3.0.0)[1.1. New in Spring Cloud Vault 3.0](#new-in-3.0.0) ### +### [](#new-in-3.0.0)[1.1. New in Spring Cloud Vault 3.0](#new-in-3.0.0) * Migration of `PropertySource` initialization from Spring Cloud’s Bootstrap Context to Spring Boot’s [ConfigData API](#vault.configdata). @@ -22,8 +104,7 @@ This section briefly covers items that are new and noteworthy in the latest rele * Support to configure [Multiple Databases](#vault.config.backends.databases). -[](#quick-start)[2. Quick Start](#quick-start) ----------- +## [](#quick-start)[2. Quick Start](#quick-start) **Prerequisites** @@ -146,8 +227,7 @@ The HTTP service has resources in the form: where the "application" is injected as the `spring.application.name` in the`SpringApplication` (i.e. what is normally "application" in a regular Spring Boot app), "profile" is an active profile (or comma-separated list of properties). Properties retrieved from Vault will be used "as-is" without further prefixing of the property names. -[](#client-side-usage)[3. Client Side Usage](#client-side-usage) ----------- +## [](#client-side-usage)[3. Client Side Usage](#client-side-usage) To use these features in an application, just build it as a Spring Boot application that depends on `spring-cloud-vault-config` (e.g. see the test cases). Example Maven configuration: @@ -248,7 +328,7 @@ The vault health indicator can be enabled or disabled through the property `mana | |With Spring Cloud Vault 3.0 and Spring Boot 2.4, the bootstrap context initialization (`bootstrap.yml`, `bootstrap.properties`) of property sources was deprecated.
Instead, Spring Cloud Vault favors Spring Boot’s Config Data API which allows importing configuration from Vault. With Spring Boot Config Data approach, you need to set the `spring.config.import` property in order to bind to Vault. You can read more about it in the [Config Data Locations section](#vault.configdata.locations).
You can enable the bootstrap context either by setting the configuration property `spring.cloud.bootstrap.enabled=true` or by including the dependency `org.springframework.cloud:spring-cloud-starter-bootstrap`.| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#authentication)[3.1. Authentication](#authentication) ### +### [](#authentication)[3.1. Authentication](#authentication) Vault requires an [authentication mechanism](https://www.vaultproject.io/docs/concepts/auth.html) to [authorize client requests](https://www.vaultproject.io/docs/concepts/tokens.html). @@ -267,8 +347,7 @@ spring.config.import: vault:// | |Consider carefully your security requirements.
Static token authentication is fine if you want quickly get started with Vault, but a static token is not protected any further.
Any disclosure to unintended parties allows Vault use with the associated token roles.| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -[](#vault.configdata)[4. ConfigData API](#vault.configdata) ----------- +## [](#vault.configdata)[4. ConfigData API](#vault.configdata) Spring Boot provides since version 2.4 a ConfigData API that allows the declaration of configuration sources and importing these as property sources. @@ -279,7 +358,7 @@ The ConfigData API is much more flexible as it allows specifying which configura | |You can enable the deprecated bootstrap context either by setting the configuration property `spring.cloud.bootstrap.enabled=true` or by including the dependency `org.springframework.cloud:spring-cloud-starter-bootstrap`.| |---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#vault.configdata.locations)[4.1. ConfigData Locations](#vault.configdata.locations) ### +### [](#vault.configdata.locations)[4.1. ConfigData Locations](#vault.configdata.locations) You can mount Vault configuration through one or more `PropertySource` that are materialized from Vault. Spring Cloud Vault supports two config locations: @@ -320,7 +399,7 @@ other.secret: ${bar.secret} | |Prefixes are added as-is to all property names returned by Vault. If you want key names to be separated with a dot between the prefix and key name, make sure to add a trailing dot to the prefix.| |---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#vault.configdata.location.optional)[4.2. Conditionally enable/disable Vault Configuration](#vault.configdata.location.optional) ### +### [](#vault.configdata.location.optional)[4.2. Conditionally enable/disable Vault Configuration](#vault.configdata.location.optional) In some cases, it can be required to launch an application without Vault. You can express whether a Vault config location should be optional or mandatory (default) through the location string: @@ -333,7 +412,7 @@ Optional locations are skipped during application startup if Vault support was d | |Vault context paths that cannot be found (HTTP Status 404) are skipped regardless of whether the config location is marked optional. [Vault Client Fail Fast](#vault.config.fail-fast) allows failing on start if a Vault context path cannot be found because of HTTP Status 404.| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#vault.configdata.customization)[4.3. Infrastructure Customization](#vault.configdata.customization) ### +### [](#vault.configdata.customization)[4.3. Infrastructure Customization](#vault.configdata.customization) Spring Cloud Vault requires infrastructure classes to interact with Vault. When not using the ConfigData API (meaning that you haven’t specified `spring.config.import=vault://` or a contextual Vault path), Spring Cloud Vault defines its beans through `VaultAutoConfiguration` and `VaultReactiveAutoConfiguration`. Spring Boot bootstraps the application before a Spring Context is available. Therefore `VaultConfigDataLoader` registers beans itself to propagate these later on into the application context. @@ -352,14 +431,13 @@ application.addBootstrapper(registry -> registry.register(RestTemplateBuilder.cl See also [Customize which secret backends to expose as PropertySource](#vault.config.backends.configurer) and the source of `VaultConfigDataLoader` for customization hooks. -[](#vault.config.authentication)[5. Authentication methods](#vault.config.authentication) ----------- +## [](#vault.config.authentication)[5. Authentication methods](#vault.config.authentication) Different organizations have different requirements for security and authentication. Vault reflects that need by shipping multiple authentication methods. Spring Cloud Vault supports token and AppId authentication. -### [](#vault.config.authentication.token)[5.1. Token authentication](#vault.config.authentication.token) ### +### [](#vault.config.authentication.token)[5.1. Token authentication](#vault.config.authentication.token) Tokens are the core method for authentication within Vault. Token authentication requires a static token to be provided using the configuration. @@ -388,7 +466,7 @@ See also: * [Vault Documentation: CLI default to \~/.vault-token](https://www.vaultproject.io/docs/commands/token-helper) -### [](#vault.config.authentication.vault-agent)[5.2. Vault Agent authentication](#vault.config.authentication.vault-agent) ### +### [](#vault.config.authentication.vault-agent)[5.2. Vault Agent authentication](#vault.config.authentication.vault-agent) Vault ships a sidecar utility with Vault Agent since version 0.11.0. Vault Agent implements the functionality of Spring Vault’s `SessionManager`with its Auto-Auth feature. Applications can reuse cached session credentials by relying on Vault Agent running on `localhost`. @@ -406,7 +484,7 @@ spring.cloud.vault: See also: [Vault Documentation: Agent](https://www.vaultproject.io/docs/agent/index.html) -### [](#vault.config.authentication.appid)[5.3. AppId authentication](#vault.config.authentication.appid) ### +### [](#vault.config.authentication.appid)[5.3. AppId authentication](#vault.config.authentication.appid) Vault supports [AppId](https://www.vaultproject.io/docs/auth/app-id.html)authentication that consists of two hard to guess tokens. The AppId defaults to `spring.application.name` that is statically configured. @@ -467,7 +545,7 @@ $ echo -n 0AFEDE1234AC | sha256sum | |The Mac address is specified uppercase and without colons.
Including the line break of `echo` leads to a different hash value so make sure to include the `-n` flag.| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -#### [](#custom-userid)[5.3.1. Custom UserId](#custom-userid) #### +#### [](#custom-userid)[5.3.1. Custom UserId](#custom-userid) The UserId generation is an open mechanism. You can set`spring.cloud.vault.app-id.user-id` to any string and the configured value will be used as static UserId. @@ -500,7 +578,7 @@ public class MyUserIdMechanism implements AppIdUserIdMechanism { See also: [Vault Documentation: Using the App ID auth backend](https://www.vaultproject.io/docs/auth/app-id.html) -### [](#approle-authentication)[5.4. AppRole authentication](#approle-authentication) ### +### [](#approle-authentication)[5.4. AppRole authentication](#approle-authentication) [AppRole](https://www.vaultproject.io/docs/auth/app-id.html) is intended for machine authentication, like the deprecated (since Vault 0.6.1) [AppId authentication](#vault.config.authentication.appid). AppRole authentication consists of two hard to guess (secret) tokens: RoleId and SecretId. @@ -575,7 +653,7 @@ spring.cloud.vault: See also: [Vault Documentation: Using the AppRole auth backend](https://www.vaultproject.io/docs/auth/approle.html) -### [](#vault.config.authentication.awsec2)[5.5. AWS-EC2 authentication](#vault.config.authentication.awsec2) ### +### [](#vault.config.authentication.awsec2)[5.5. AWS-EC2 authentication](#vault.config.authentication.awsec2) The [aws-ec2](https://www.vaultproject.io/docs/auth/aws-ec2.html)auth backend provides a secure introduction mechanism for AWS EC2 instances, allowing automated retrieval of a Vault token. Unlike most Vault authentication backends, this backend does not require first-deploying, or provisioning security-sensitive credentials (tokens, username/password, client certificates, etc.). @@ -635,7 +713,7 @@ spring.cloud.vault: See also: [Vault Documentation: Using the aws auth backend](https://www.vaultproject.io/docs/auth/aws.html) -### [](#vault.config.authentication.awsiam)[5.6. AWS-IAM authentication](#vault.config.authentication.awsiam) ### +### [](#vault.config.authentication.awsiam)[5.6. AWS-IAM authentication](#vault.config.authentication.awsiam) The [aws](https://www.vaultproject.io/docs/auth/aws-ec2.html) backend provides a secure authentication mechanism for AWS IAM roles, allowing the automatic authentication with vault based on the current IAM role of the running application. Unlike most Vault authentication backends, this backend does not require first-deploying, or provisioning security-sensitive credentials (tokens, username/password, client certificates, etc.). @@ -681,7 +759,7 @@ AWS-IAM requires the AWS Java SDK dependency (`com.amazonaws:aws-java-sdk-core`) See also: [Vault Documentation: Using the aws auth backend](https://www.vaultproject.io/docs/auth/aws.html) -### [](#vault.config.authentication.azuremsi)[5.7. Azure MSI authentication](#vault.config.authentication.azuremsi) ### +### [](#vault.config.authentication.azuremsi)[5.7. Azure MSI authentication](#vault.config.authentication.azuremsi) The [azure](https://www.vaultproject.io/docs/auth/azure.html)auth backend provides a secure introduction mechanism for Azure VM instances, allowing automated retrieval of a Vault token. Unlike most Vault authentication backends, this backend does not require first-deploying, or provisioning security-sensitive credentials (tokens, username/password, client certificates, etc.). @@ -726,7 +804,7 @@ See also: * [Azure Documentation: Azure Instance Metadata Service](https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service) -### [](#vault.config.authentication.clientcert)[5.8. TLS certificate authentication](#vault.config.authentication.clientcert) ### +### [](#vault.config.authentication.clientcert)[5.8. TLS certificate authentication](#vault.config.authentication.clientcert) The `cert` auth backend allows authentication using SSL/TLS client certificates that are either signed by a CA or self-signed. @@ -752,7 +830,7 @@ spring.cloud.vault: See also: [Vault Documentation: Using the Cert auth backend](https://www.vaultproject.io/docs/auth/cert.html) -### [](#vault.config.authentication.cubbyhole)[5.9. Cubbyhole authentication](#vault.config.authentication.cubbyhole) ### +### [](#vault.config.authentication.cubbyhole)[5.9. Cubbyhole authentication](#vault.config.authentication.cubbyhole) Cubbyhole authentication uses Vault primitives to provide a secured authentication workflow. Cubbyhole authentication uses tokens as primary login method. @@ -793,7 +871,7 @@ See also: * [Vault Documentation: Response Wrapping](https://www.vaultproject.io/docs/concepts/response-wrapping.html) -### [](#vault.config.authentication.gcpgce)[5.10. GCP-GCE authentication](#vault.config.authentication.gcpgce) ### +### [](#vault.config.authentication.gcpgce)[5.10. GCP-GCE authentication](#vault.config.authentication.gcpgce) The [gcp](https://www.vaultproject.io/docs/auth/gcp.html)auth backend allows Vault login by using existing GCP (Google Cloud Platform) IAM and GCE credentials. @@ -837,7 +915,7 @@ See also: * [GCP Documentation: Verifying the Identity of Instances](https://cloud.google.com/compute/docs/instances/verifying-instance-identity) -### [](#vault.config.authentication.gcpiam)[5.11. GCP-IAM authentication](#vault.config.authentication.gcpiam) ### +### [](#vault.config.authentication.gcpiam)[5.11. GCP-IAM authentication](#vault.config.authentication.gcpiam) The [gcp](https://www.vaultproject.io/docs/auth/gcp.html)auth backend allows Vault login by using existing GCP (Google Cloud Platform) IAM and GCE credentials. @@ -901,7 +979,7 @@ See also: * [GCP Documentation: projects.serviceAccounts.signJwt](https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts/signJwt) -### [](#vault.config.authentication.kubernetes)[5.12. Kubernetes authentication](#vault.config.authentication.kubernetes) ### +### [](#vault.config.authentication.kubernetes)[5.12. Kubernetes authentication](#vault.config.authentication.kubernetes) Kubernetes authentication mechanism (since Vault 0.8.3) allows to authenticate with Vault using a Kubernetes Service Account Token. The authentication is role based and the role is bound to a service account name and a namespace. @@ -932,7 +1010,7 @@ See also: * [Kubernetes Documentation: Configure Service Accounts for Pods](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/) -### [](#vault.config.authentication.pcf)[5.13. Pivotal CloudFoundry authentication](#vault.config.authentication.pcf) ### +### [](#vault.config.authentication.pcf)[5.13. Pivotal CloudFoundry authentication](#vault.config.authentication.pcf) The [pcf](https://www.vaultproject.io/docs/auth/pcf.html)auth backend provides a secure introduction mechanism for applications running within Pivotal’s CloudFoundry instances allowing automated retrieval of a Vault token. Unlike most Vault authentication backends, this backend does not require first-deploying, or provisioning security-sensitive credentials (tokens, username/password, client certificates, etc.) as identity provisioning is handled by PCF itself. @@ -974,8 +1052,7 @@ spring.cloud.vault: See also: [Vault Documentation: Using the pcf auth backend](https://www.vaultproject.io/docs/auth/pcf.html) -[](#vault.config.acl)[6. ACL Requirements](#vault.config.acl) ----------- +## [](#vault.config.acl)[6. ACL Requirements](#vault.config.acl) This section explains which paths are accessed by Spring Vault so you can derive your policy declarations from the required capabilities. @@ -989,15 +1066,15 @@ This section explains which paths are accessed by Spring Vault so you can derive See also [www.vaultproject.io/guides/identity/policies](https://www.vaultproject.io/guides/identity/policies). -### [](#authentication-2)[6.1. Authentication](#authentication-2) ### +### [](#authentication-2)[6.1. Authentication](#authentication-2) Login: `POST auth/$authMethod/login` -### [](#keyvalue-mount-discovery)[6.2. KeyValue Mount Discovery](#keyvalue-mount-discovery) ### +### [](#keyvalue-mount-discovery)[6.2. KeyValue Mount Discovery](#keyvalue-mount-discovery) `GET sys/internal/ui/mounts/$mountPath` -### [](#secretleasecontainer)[6.3. SecretLeaseContainer](#secretleasecontainer) ### +### [](#secretleasecontainer)[6.3. SecretLeaseContainer](#secretleasecontainer) `SecretLeaseContainer` uses different paths depending on the configured lease endpoint. @@ -1013,7 +1090,7 @@ Login: `POST auth/$authMethod/login` * Renewal: `PUT sys/leases/renew` -### [](#session-management)[6.4. Session Management](#session-management) ### +### [](#session-management)[6.4. Session Management](#session-management) * Token lookup: `GET auth/token/lookup-self` @@ -1021,10 +1098,9 @@ Login: `POST auth/$authMethod/login` * Revoke: `POST auth/token/revoke-self` -[](#vault.config.backends)[7. Secret Backends](#vault.config.backends) ----------- +## [](#vault.config.backends)[7. Secret Backends](#vault.config.backends) -### [](#vault.config.backends.kv.versioned)[7.1. Key-Value Backend](#vault.config.backends.kv.versioned) ### +### [](#vault.config.backends.kv.versioned)[7.1. Key-Value Backend](#vault.config.backends.kv.versioned) Spring Cloud Vault supports both Key-Value secret backends, the versioned (v2) and unversioned (v1). The key-value backend allows storage of arbitrary values as key-value store. @@ -1103,7 +1179,7 @@ See also: * [Vault Documentation: Using the KV Secrets Engine - Version 2 (versioned key-value backend)](https://www.vaultproject.io/docs/secrets/kv/kv-v2.html) -### [](#vault.config.backends.consul)[7.2. Consul](#vault.config.backends.consul) ### +### [](#vault.config.backends.consul)[7.2. Consul](#vault.config.backends.consul) Spring Cloud Vault can obtain credentials for HashiCorp Consul. The Consul integration requires the `spring-cloud-vault-config-consul`dependency. @@ -1144,7 +1220,7 @@ spring.cloud.vault: See also: [Vault Documentation: Setting up Consul with Vault](https://www.vaultproject.io/docs/secrets/consul/index.html) -### [](#vault.config.backends.rabbitmq)[7.3. RabbitMQ](#vault.config.backends.rabbitmq) ### +### [](#vault.config.backends.rabbitmq)[7.3. RabbitMQ](#vault.config.backends.rabbitmq) Spring Cloud Vault can obtain credentials for RabbitMQ. @@ -1189,7 +1265,7 @@ spring.cloud.vault: See also: [Vault Documentation: Setting up RabbitMQ with Vault](https://www.vaultproject.io/docs/secrets/rabbitmq/index.html) -### [](#vault.config.backends.aws)[7.4. AWS](#vault.config.backends.aws) ### +### [](#vault.config.backends.aws)[7.4. AWS](#vault.config.backends.aws) Spring Cloud Vault can obtain credentials for AWS. @@ -1271,8 +1347,7 @@ spring.cloud.vault: See also: [Vault Documentation: Setting up AWS with Vault](https://www.vaultproject.io/docs/secrets/aws/index.html) -[](#vault.config.backends.database-backends)[8. Database backends](#vault.config.backends.database-backends) ----------- +## [](#vault.config.backends.database-backends)[8. Database backends](#vault.config.backends.database-backends) Vault supports several database secret backends to generate database credentials dynamically based on configured roles. This means services that need to access a database no longer need to configure credentials: they can request them from Vault, and use Vault’s leasing mechanism to more easily roll keys. @@ -1314,7 +1389,7 @@ Example 34. pom.xml | |Enabling multiple JDBC-compliant databases will generate credentials and store them by default in the same property keys hence property names for JDBC secrets need to be configured separately.| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#vault.config.backends.database)[8.1. Database](#vault.config.backends.database) ### +### [](#vault.config.backends.database)[8.1. Database](#vault.config.backends.database) Spring Cloud Vault can obtain credentials for any database listed at[www.vaultproject.io/api/secret/databases/index.html](https://www.vaultproject.io/api/secret/databases/index.html). The integration can be enabled by setting`spring.cloud.vault.database.enabled=true` (default `false`) and providing the role name with `spring.cloud.vault.database.role=…`. @@ -1334,7 +1409,7 @@ spring.cloud.vault: password-property: spring.datasource.password ``` -### [](#vault.config.backends.databases)[8.2. Multiple Databases](#vault.config.backends.databases) ### +### [](#vault.config.backends.databases)[8.2. Multiple Databases](#vault.config.backends.databases) Sometimes, credentials for a single database isn’t sufficient because an application might connect to two or more databases of the same kind. Beginning with version 3.0.5, Spring Vault supports the configuration of multiple database secret backends under the `spring.cloud.vault.databases.*` namespace. @@ -1375,7 +1450,7 @@ See also: [Vault Documentation: Database Secrets backend](https://www.vaultproje | |Spring Cloud Vault does not support getting new credentials and configuring your `DataSource` with them when the maximum lease time has been reached.
That is, if `max_ttl` of the Database role in Vault is set to `24h` that means that 24 hours after your application has started it can no longer authenticate with the database.| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#vault.config.backends.cassandra)[8.3. Apache Cassandra](#vault.config.backends.cassandra) ### +### [](#vault.config.backends.cassandra)[8.3. Apache Cassandra](#vault.config.backends.cassandra) | |The `cassandra` backend has been deprecated in Vault 0.7.1 and it is recommended to use the `database` backend and mount it as `cassandra`.| |---|-------------------------------------------------------------------------------------------------------------------------------------------| @@ -1408,7 +1483,7 @@ spring.cloud.vault: See also: [Vault Documentation: Setting up Apache Cassandra with Vault](https://www.vaultproject.io/docs/secrets/cassandra/index.html) -### [](#vault.config.backends.couchbase)[8.4. Couchbase Database](#vault.config.backends.couchbase) ### +### [](#vault.config.backends.couchbase)[8.4. Couchbase Database](#vault.config.backends.couchbase) Spring Cloud Vault can obtain credentials for Couchbase. The integration can be enabled by setting`spring.cloud.vault.couchbase.enabled=true` (default `false`) and providing the role name with `spring.cloud.vault.couchbase.role=…`. @@ -1438,7 +1513,7 @@ spring.cloud.vault: See also: [Couchbase Database Plugin Documentation](https://github.com/hashicorp/vault-plugin-database-couchbase) -### [](#vault.config.backends.elasticsearch)[8.5. Elasticsearch](#vault.config.backends.elasticsearch) ### +### [](#vault.config.backends.elasticsearch)[8.5. Elasticsearch](#vault.config.backends.elasticsearch) Spring Cloud Vault can obtain since version 3.0 credentials for Elasticsearch. The integration can be enabled by setting`spring.cloud.vault.elasticsearch.enabled=true` (default `false`) and providing the role name with `spring.cloud.vault.elasticsearch.role=…`. @@ -1468,7 +1543,7 @@ spring.cloud.vault: See also: [Vault Documentation: Setting up Elasticsearch with Vault](https://www.vaultproject.io/docs/secrets/databases/elasticdb) -### [](#vault.config.backends.mongodb)[8.6. MongoDB](#vault.config.backends.mongodb) ### +### [](#vault.config.backends.mongodb)[8.6. MongoDB](#vault.config.backends.mongodb) | |The `mongodb` backend has been deprecated in Vault 0.7.1 and it is recommended to use the `database` backend and mount it as `mongodb`.| |---|---------------------------------------------------------------------------------------------------------------------------------------| @@ -1501,7 +1576,7 @@ spring.cloud.vault: See also: [Vault Documentation: Setting up MongoDB with Vault](https://www.vaultproject.io/docs/secrets/mongodb/index.html) -### [](#vault.config.backends.mysql)[8.7. MySQL](#vault.config.backends.mysql) ### +### [](#vault.config.backends.mysql)[8.7. MySQL](#vault.config.backends.mysql) | |The `mysql` backend has been deprecated in Vault 0.7.1 and it is recommended to use the `database` backend and mount it as `mysql`.
Configuration for `spring.cloud.vault.mysql` will be removed in a future version.| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| @@ -1534,7 +1609,7 @@ spring.cloud.vault: See also: [Vault Documentation: Setting up MySQL with Vault](https://www.vaultproject.io/docs/secrets/mysql/index.html) -### [](#vault.config.backends.postgresql)[8.8. PostgreSQL](#vault.config.backends.postgresql) ### +### [](#vault.config.backends.postgresql)[8.8. PostgreSQL](#vault.config.backends.postgresql) | |The `postgresql` backend has been deprecated in Vault 0.7.1 and it is recommended to use the `database` backend and mount it as `postgresql`.
Configuration for `spring.cloud.vault.postgresql` will be removed in a future version.| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| @@ -1567,8 +1642,7 @@ spring.cloud.vault: See also: [Vault Documentation: Setting up PostgreSQL with Vault](https://www.vaultproject.io/docs/secrets/postgresql/index.html) -[](#vault.config.backends.configurer)[9. Customize which secret backends to expose as PropertySource](#vault.config.backends.configurer) ----------- +## [](#vault.config.backends.configurer)[9. Customize which secret backends to expose as PropertySource](#vault.config.backends.configurer) Spring Cloud Vault uses property-based configuration to create `PropertySource`s for key-value and discovered secret backends. @@ -1600,8 +1674,7 @@ SpringApplication application = new SpringApplication(MyApplication.class); application.addBootstrapper(VaultBootstrapper.fromConfigurer(new CustomizationBean())); ``` -[](#vault.config.backends.custom)[10. Custom Secret Backend Implementations](#vault.config.backends.custom) ----------- +## [](#vault.config.backends.custom)[10. Custom Secret Backend Implementations](#vault.config.backends.custom) Spring Cloud Vault ships with secret backend support for the most common backend integrations. You can integrate with any kind of backend by providing an implementation that describes how to obtain data from the backend you want to use and how to surface data provided by that backend by providing a `PropertyTransformer`. @@ -1618,8 +1691,7 @@ Adding a custom implementation for a backend requires implementation of two inte Both, `VaultSecretBackendDescriptor` and `SecretBackendMetadataFactory` types must be registered in `spring.factories` which is an extension mechanism provided by Spring, similar to Java’s ServiceLoader. -[](#service-registry-configuration)[11. Service Registry Configuration](#service-registry-configuration) ----------- +## [](#service-registry-configuration)[11. Service Registry Configuration](#service-registry-configuration) You can use a `DiscoveryClient` (such as from Spring Cloud Consul) to locate a Vault server by setting spring.cloud.vault.discovery.enabled=true (default `false`). The net result of that is that your apps need a application.yml (or an environment variable) with the appropriate discovery configuration. @@ -1637,8 +1709,7 @@ spring.cloud.vault.discovery: service-id: my-vault-service ``` -[](#vault.config.fail-fast)[12. Vault Client Fail Fast](#vault.config.fail-fast) ----------- +## [](#vault.config.fail-fast)[12. Vault Client Fail Fast](#vault.config.fail-fast) In some cases, it may be desirable to fail startup of a service if it cannot connect to the Vault Server. If this is the desired behavior, set the bootstrap configuration property`spring.cloud.vault.fail-fast=true` and the client will halt with an Exception. @@ -1648,8 +1719,7 @@ spring.cloud.vault: fail-fast: true ``` -[](#vault.config.namespaces)[13. Vault Enterprise Namespace Support](#vault.config.namespaces) ----------- +## [](#vault.config.namespaces)[13. Vault Enterprise Namespace Support](#vault.config.namespaces) Vault Enterprise allows using namespaces to isolate multiple Vaults on a single Vault server. Configuring a namespace by setting`spring.cloud.vault.namespace=…` enables the namespace header`X-Vault-Namespace` on every outgoing HTTP request when using the Vault`RestTemplate` or `WebClient`. @@ -1663,8 +1733,7 @@ spring.cloud.vault: See also: [Vault Enterprise: Namespaces](https://www.vaultproject.io/docs/enterprise/namespaces/index.html) -[](#vault.config.ssl)[14. Vault Client SSL configuration](#vault.config.ssl) ----------- +## [](#vault.config.ssl)[14. Vault Client SSL configuration](#vault.config.ssl) SSL can be configured declaratively by setting various properties. You can set either `javax.net.ssl.trustStore` to configure JVM-wide SSL settings or `spring.cloud.vault.ssl.trust-store`to set SSL settings only for Spring Cloud Vault Config. @@ -1692,8 +1761,7 @@ spring.cloud.vault: Please note that configuring `spring.cloud.vault.ssl.*` can be only applied when either Apache Http Components or the OkHttp client is on your class-path. -[](#vault-lease-renewal)[15. Lease lifecycle management (renewal and revocation)](#vault-lease-renewal) ----------- +## [](#vault-lease-renewal)[15. Lease lifecycle management (renewal and revocation)](#vault-lease-renewal) With every secret, Vault creates a lease: metadata containing information such as a time duration, renewability, and more. @@ -1736,8 +1804,7 @@ spring.cloud.vault: See also: [Vault Documentation: Lease, Renew, and Revoke](https://www.vaultproject.io/docs/concepts/lease.html) -[](#vault-session-lifecycle)[16. Session token lifecycle management (renewal, re-login and revocation)](#vault-session-lifecycle) ----------- +## [](#vault-session-lifecycle)[16. Session token lifecycle management (renewal, re-login and revocation)](#vault-session-lifecycle) A Vault session token (also referred to as `LoginToken`) is quite similar to a lease as it has a TTL, max TTL, and may expire. Once a login token expires, it cannot be used anymore to interact with Vault. @@ -1775,8 +1842,7 @@ spring.cloud.vault: See also: [Vault Documentation: Token Renewal](https://www.vaultproject.io/api-docs/auth/token#renew-a-token-self) -[](#common-application-properties)[Appendix A: Common application properties](#common-application-properties) ----------- +## [](#common-application-properties)[Appendix A: Common application properties](#common-application-properties) Various properties can be specified inside your `application.properties` file, inside your `application.yml` file, or as command line switches. This appendix provides a list of common Spring Cloud Vault properties and references to the underlying classes that consume them. @@ -1920,3 +1986,5 @@ This appendix provides a list of common Spring Cloud Vault properties and refere | spring.cloud.vault.ssl.trust-store-type | | Type of the trust store. @since 3.0 | | spring.cloud.vault.token | | Static vault token. Required if {@link #authentication} is {@code TOKEN}. | | spring.cloud.vault.uri | | Vault URI. Can be set with scheme, host and port. | + +if (window.parent == window) {(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1\*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga');ga('create', 'UA-2728886-23', 'auto', {'siteSpeedSampleRate': 100});ga('send', 'pageview');} \ No newline at end of file diff --git a/docs/en/spring-cloud/spring-cloud-zookeeper.md b/docs/en/spring-cloud/spring-cloud-zookeeper.md index daf2fc78e8f6879a112d96ea99f714c6d8c00307..c2dca05bc3f3d3c8b4a71f90ea84a5c8d87d55e2 100644 --- a/docs/en/spring-cloud/spring-cloud-zookeeper.md +++ b/docs/en/spring-cloud/spring-cloud-zookeeper.md @@ -1,5 +1,56 @@ -[Spring Cloud Zookeeper](#_spring_cloud_zookeeper) -========== +Spring Cloud Zookeeper.hidden { display: none; +} .switch { border-width: 1px 1px 0 1px; border-style: solid; border-color: #7a2518; display: inline-block; +} .switch--item { padding: 10px; background-color: #ffffff; color: #7a2518; display: inline-block; cursor: pointer; +} .switch--item:not(:first-child) { border-width: 0 0 0 1px; border-style: solid; border-color: #7a2518; +} .switch--item.selected { background-color: #7a2519; color: #ffffff; +} function addBlockSwitches() { for (var primary of document.querySelectorAll('.primary')) { var switchItem = createSwitchItem(primary, createBlockSwitch(primary)); switchItem.item.classList.add("selected"); var title = primary.querySelector('.title') title.remove(); } for (var secondary of document.querySelectorAll('.secondary')) { var primary = findPrimary(secondary); if (primary === null) { console.error("Found secondary block with no primary sibling"); } else { var switchItem = createSwitchItem(secondary, primary.querySelector('.switch')); switchItem.content.classList.add("hidden"); primary.append(switchItem.content); secondary.remove(); } } +} function createElementFromHtml(html) { var template = document.createElement('template'); template.innerHTML = html; return template.content.firstChild; +} function createBlockSwitch(primary) { var blockSwitch = createElementFromHtml('\
\'); primary.prepend(blockSwitch) return blockSwitch; +} function findPrimary(secondary) { var candidate = secondary.previousElementSibling; while (candidate != null && !candidate.classList.contains('primary')) { candidate = candidate.previousElementSibling; } return candidate; +} function createSwitchItem(block, blockSwitch) { var blockName = block.querySelector('.title').textContent; var content = block.querySelectorAll('.content').item(0); var colist = nextSibling(block, '.colist'); if (colist != null) { content.append(colist); } var item = createElementFromHtml('\
' + blockName + '\'); item.dataset.blockName = blockName; content.dataset.blockName = blockName; blockSwitch.append(item); return {'item': item, 'content': content}; +} function nextSibling(element, selector) { var sibling = element.nextElementSibling; while (sibling) { if (sibling.matches(selector)) { return sibling; } sibling = sibling.nextElementSibling; } +} function globalSwitch() { document.querySelectorAll(".switch--item").forEach(function(item) { var blockId = blockIdForSwitchItem(item); var handler = function(event) { selectedText = event.target.textContent; window.localStorage.setItem(blockId, selectedText); for (var switchItem of document.querySelectorAll(".switch--item")) { if (blockIdForSwitchItem(switchItem) === blockId && switchItem.textContent === selectedText) { select(switchItem); } } } item.addEventListener("click", handler); if (item.textContent === window.localStorage.getItem(blockId)) { select(item); } }); +} function select(selected) { for (var child of selected.parentNode.children) { child.classList.remove("selected"); } selected.classList.add("selected"); for (var child of selected.parentNode.parentNode.children) { if (child.classList.contains("content")) { if (selected.dataset.blockName === child.dataset.blockName) { child.classList.remove("hidden"); } else { child.classList.add("hidden"); } } } } function blockIdForSwitchItem(item) { idComponents = [] for (var switchItem of item.parentNode.querySelectorAll(".switch--item")) { idComponents.push(switchItem.textContent.toLowerCase()); } return idComponents.sort().join("-") +} window.onload = function() { addBlockSwitches(); globalSwitch(); +}; + +Table of Contents + +* [Spring Cloud Zookeeper](#_spring_cloud_zookeeper) + * [1. Quick Start](#quick-start) + * [1.1. Discovery Client Usage](#discovery-client-usage) + * [1.2. Distributed Configuration Usage](#distributed-configuration-usage) + + * [2. Install Zookeeper](#spring-cloud-zookeeper-install) + * [3. Service Discovery with Zookeeper](#spring-cloud-zookeeper-discovery) + * [3.1. Activating](#activating) + * [3.2. Registering with Zookeeper](#registering-with-zookeeper) + * [3.3. Using the DiscoveryClient](#using-the-discoveryclient) + + * [4. Using Spring Cloud Zookeeper with Spring Cloud Components](#spring-cloud-zookeeper-other-componentes) + * [4.1. Spring Cloud LoadBalancer with Zookeeper](#spring-cloud-loadbalancer-with-zookeeper) + + * [5. Spring Cloud Zookeeper and Service Registry](#spring-cloud-zookeeper-service-registry) + * [5.1. Instance Status](#instance-status) + + * [6. Zookeeper Dependencies](#spring-cloud-zookeeper-dependencies) + * [6.1. Using the Zookeeper Dependencies](#spring-cloud-zookeeper-dependencies-using) + * [6.2. Activating Zookeeper Dependencies](#spring-cloud-zookeeper-dependencies-activating) + * [6.3. Setting up Zookeeper Dependencies](#spring-cloud-zookeeper-dependencies-setting-up) + * [6.4. Configuring Spring Cloud Zookeeper Dependencies](#spring-cloud-zookeeper-dependencies-configuring) + + * [7. Spring Cloud Zookeeper Dependency Watcher](#spring-cloud-zookeeper-dependency-watcher) + * [7.1. Activating](#activating-2) + * [7.2. Registering a Listener](#registering-a-listener) + * [7.3. Using the Presence Checker](#spring-cloud-zookeeper-dependency-watcher-presence-checker) + + * [8. Distributed Configuration with Zookeeper](#spring-cloud-zookeeper-config) + * [8.1. Activating](#activating-3) + * [8.2. Spring Boot Config Data Import](#config-data-import) + * [8.3. Customizing](#customizing) + * [8.4. Access Control Lists (ACLs)](#access-control-lists-acls) + +# [Spring Cloud Zookeeper](#_spring_cloud_zookeeper) This project provides Zookeeper integrations for Spring Boot applications through autoconfiguration and binding to the Spring Environment and other Spring programming model @@ -8,14 +59,13 @@ inside your application and build large distributed systems with Zookeeper based components. The provided patterns include Service Discovery and Configuration. The project also provides client-side load-balancing via integration with Spring Cloud LoadBalancer. -[](#quick-start)[1. Quick Start](#quick-start) ----------- +## [](#quick-start)[1. Quick Start](#quick-start) This quick start walks through using Spring Cloud Zookeeper for Service Discovery and Distributed Configuration. First, run Zookeeper on your machine. Then you can access it and use it as a Service Registry and Configuration source with Spring Cloud Zookeeper. -### [](#discovery-client-usage)[1.1. Discovery Client Usage](#discovery-client-usage) ### +### [](#discovery-client-usage)[1.1. Discovery Client Usage](#discovery-client-usage) To use these features in an application, you can build it as a Spring Boot application that depends on `spring-cloud-zookeeper-core` and `spring-cloud-zookeeper-discovery`. The most convenient way to add the dependency is with a Spring Boot starter: `org.springframework.cloud:spring-cloud-starter-zookeeper-discovery`. @@ -139,7 +189,7 @@ public String serviceUrl() { } ``` -### [](#distributed-configuration-usage)[1.2. Distributed Configuration Usage](#distributed-configuration-usage) ### +### [](#distributed-configuration-usage)[1.2. Distributed Configuration Usage](#distributed-configuration-usage) To use these features in an application, you can build it as a Spring Boot application that depends on `spring-cloud-zookeeper-core` and `spring-cloud-zookeeper-config`. The most convenient way to add the dependency is with a Spring Boot starter: `org.springframework.cloud:spring-cloud-starter-zookeeper-config`. @@ -243,8 +293,7 @@ The application retrieves configuration data from Zookeeper. | |If you use Spring Cloud Zookeeper Config, you need to set the `spring.config.import` property in order to bind to Zookeeper.
You can read more about it in the [Spring Boot Config Data Import section](#config-data-import).| |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -[](#spring-cloud-zookeeper-install)[2. Install Zookeeper](#spring-cloud-zookeeper-install) ----------- +## [](#spring-cloud-zookeeper-install)[2. Install Zookeeper](#spring-cloud-zookeeper-install) See the [installation documentation](https://zookeeper.apache.org/doc/current/zookeeperStarted.html) for instructions on how to install Zookeeper. @@ -297,8 +346,7 @@ compile('org.apache.zookeeper:zookeeper:3.4.12') { } ``` -[](#spring-cloud-zookeeper-discovery)[3. Service Discovery with Zookeeper](#spring-cloud-zookeeper-discovery) ----------- +## [](#spring-cloud-zookeeper-discovery)[3. Service Discovery with Zookeeper](#spring-cloud-zookeeper-discovery) Service Discovery is one of the key tenets of a microservice based architecture. Trying to hand-configure each client or some form of convention can be difficult to do and can be @@ -307,7 +355,7 @@ Discovery through a [Service Discovery Extension](https://curator.apache.org/curator-x-discovery/). Spring Cloud Zookeeper uses this extension for service registration and discovery. -### [](#activating)[3.1. Activating](#activating) ### +### [](#activating)[3.1. Activating](#activating) Including a dependency on`org.springframework.cloud:spring-cloud-starter-zookeeper-discovery` enables autoconfiguration that sets up Spring Cloud Zookeeper Discovery. @@ -318,7 +366,7 @@ autoconfiguration that sets up Spring Cloud Zookeeper Discovery. | |When working with version 3.4 of Zookeeper you need to change
the way you include the dependency as described [here](#spring-cloud-zookeeper-install).| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#registering-with-zookeeper)[3.2. Registering with Zookeeper](#registering-with-zookeeper) ### +### [](#registering-with-zookeeper)[3.2. Registering with Zookeeper](#registering-with-zookeeper) When a client registers with Zookeeper, it provides metadata (such as host and port, ID, and name) about itself. @@ -368,7 +416,7 @@ query Zookeeper to locate other services). If you would like to disable the Zookeeper Discovery Client, you can set`spring.cloud.zookeeper.discovery.enabled` to `false`. -### [](#using-the-discoveryclient)[3.3. Using the DiscoveryClient](#using-the-discoveryclient) ### +### [](#using-the-discoveryclient)[3.3. Using the DiscoveryClient](#using-the-discoveryclient) Spring Cloud has support for[Feign](https://github.com/spring-cloud/spring-cloud-netflix/blob/master/docs/src/main/asciidoc/spring-cloud-netflix.adoc#spring-cloud-feign)(a REST client builder),[Spring`RestTemplate`](https://github.com/spring-cloud/spring-cloud-netflix/blob/master/docs/src/main/ascii) and[Spring WebFlux](https://cloud.spring.io/spring-cloud-commons/reference/html/#loadbalanced-webclient), using logical service names instead of physical URLs. @@ -389,12 +437,11 @@ public String serviceUrl() { } ``` -[](#spring-cloud-zookeeper-other-componentes)[4. Using Spring Cloud Zookeeper with Spring Cloud Components](#spring-cloud-zookeeper-other-componentes) ----------- +## [](#spring-cloud-zookeeper-other-componentes)[4. Using Spring Cloud Zookeeper with Spring Cloud Components](#spring-cloud-zookeeper-other-componentes) Feign, Spring Cloud Gateway and Spring Cloud LoadBalancer all work with Spring Cloud Zookeeper. -### [](#spring-cloud-loadbalancer-with-zookeeper)[4.1. Spring Cloud LoadBalancer with Zookeeper](#spring-cloud-loadbalancer-with-zookeeper) ### +### [](#spring-cloud-loadbalancer-with-zookeeper)[4.1. Spring Cloud LoadBalancer with Zookeeper](#spring-cloud-loadbalancer-with-zookeeper) Spring Cloud Zookeeper provides an implementation of Spring Cloud LoadBalancer `ServiceInstanceListSupplier`. When you use the `spring-cloud-starter-zookeeper-discovery`, Spring Cloud LoadBalancer is autoconfigured to use the`ZookeeperServiceInstanceListSupplier` by default. @@ -402,8 +449,7 @@ When you use the `spring-cloud-starter-zookeeper-discovery`, Spring Cloud LoadBa | |If you were previously using the StickyRule in Zookeeper, its replacement in the current stack
is the `SameInstancePreferenceServiceInstanceListSupplier` in SC LoadBalancer. You can read on how to set it up in the [Spring Cloud Commons documentation](https://docs.spring.io/spring-cloud-commons/docs/current/reference/html/#spring-cloud-loadbalancer).| |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -[](#spring-cloud-zookeeper-service-registry)[5. Spring Cloud Zookeeper and Service Registry](#spring-cloud-zookeeper-service-registry) ----------- +## [](#spring-cloud-zookeeper-service-registry)[5. Spring Cloud Zookeeper and Service Registry](#spring-cloud-zookeeper-service-registry) Spring Cloud Zookeeper implements the `ServiceRegistry` interface, letting developers register arbitrary services in a programmatic way. @@ -426,7 +472,7 @@ public void registerThings() { } ``` -### [](#instance-status)[5.1. Instance Status](#instance-status) ### +### [](#instance-status)[5.1. Instance Status](#instance-status) Netflix Eureka supports having instances that are `OUT_OF_SERVICE` registered with the server. These instances are not returned as active service instances. @@ -443,8 +489,7 @@ $ http POST http://localhost:8081/service-registry status=OUT_OF_SERVICE | |The preceding example uses the `http` command from [httpie.org](https://httpie.org).| |---|------------------------------------------------------------------------------------| -[](#spring-cloud-zookeeper-dependencies)[6. Zookeeper Dependencies](#spring-cloud-zookeeper-dependencies) ----------- +## [](#spring-cloud-zookeeper-dependencies)[6. Zookeeper Dependencies](#spring-cloud-zookeeper-dependencies) The following topics cover how to work with Spring Cloud Zookeeper dependencies: @@ -456,7 +501,7 @@ The following topics cover how to work with Spring Cloud Zookeeper dependencies: * [Configuring Spring Cloud Zookeeper Dependencies](#spring-cloud-zookeeper-dependencies-configuring) -### [](#spring-cloud-zookeeper-dependencies-using)[6.1. Using the Zookeeper Dependencies](#spring-cloud-zookeeper-dependencies-using) ### +### [](#spring-cloud-zookeeper-dependencies-using)[6.1. Using the Zookeeper Dependencies](#spring-cloud-zookeeper-dependencies-using) Spring Cloud Zookeeper gives you a possibility to provide dependencies of your application as properties. As dependencies, you can understand other applications that are registered @@ -465,13 +510,13 @@ in Zookeeper and which you would like to call through[Feign](https://github.com/ You can also use the Zookeeper Dependency Watchers functionality to control and monitor the state of your dependencies. -### [](#spring-cloud-zookeeper-dependencies-activating)[6.2. Activating Zookeeper Dependencies](#spring-cloud-zookeeper-dependencies-activating) ### +### [](#spring-cloud-zookeeper-dependencies-activating)[6.2. Activating Zookeeper Dependencies](#spring-cloud-zookeeper-dependencies-activating) Including a dependency on`org.springframework.cloud:spring-cloud-starter-zookeeper-discovery` enables autoconfiguration that sets up Spring Cloud Zookeeper Dependencies. Even if you provide the dependencies in your properties, you can turn off the dependencies. To do so, set the`spring.cloud.zookeeper.dependency.enabled` property to false (it defaults to `true`). -### [](#spring-cloud-zookeeper-dependencies-setting-up)[6.3. Setting up Zookeeper Dependencies](#spring-cloud-zookeeper-dependencies-setting-up) ### +### [](#spring-cloud-zookeeper-dependencies-setting-up)[6.3. Setting up Zookeeper Dependencies](#spring-cloud-zookeeper-dependencies-setting-up) Consider the following example of dependency representation: @@ -504,7 +549,7 @@ spring.cloud.zookeeper: The next few sections go through each part of the dependency one by one. The root property name is `spring.cloud.zookeeper.dependencies`. -#### [](#spring-cloud-zookeeper-dependencies-setting-up-aliases)[6.3.1. Aliases](#spring-cloud-zookeeper-dependencies-setting-up-aliases) #### +#### [](#spring-cloud-zookeeper-dependencies-setting-up-aliases)[6.3.1. Aliases](#spring-cloud-zookeeper-dependencies-setting-up-aliases) Below the root property you have to represent each dependency as an alias. This is due to the constraints of Spring Cloud LoadBalancer, which requires that the application ID be placed in the URL. @@ -522,14 +567,14 @@ public interface NewsletterService { } ``` -#### [](#path)[6.3.2. Path](#path) #### +#### [](#path)[6.3.2. Path](#path) The path is represented by the `path` YAML property and is the path under which the dependency is registered under Zookeeper. As described in the[previous section](#spring-cloud-zookeeper-dependencies-setting-up-aliases), Spring Cloud LoadBalancer operates on URLs. As a result, this path is not compliant with its requirement. That is why Spring Cloud Zookeeper maps the alias to the proper path. -#### [](#load-balancer-type)[6.3.3. Load Balancer Type](#load-balancer-type) #### +#### [](#load-balancer-type)[6.3.3. Load Balancer Type](#load-balancer-type) The load balancer type is represented by `loadBalancerType` YAML property. @@ -542,7 +587,7 @@ You can choose one of the following load balancing strategies: * ROUND\_ROBIN: Iterates over instances over and over again. -#### [](#content-type-template-and-version)[6.3.4. `Content-Type` Template and Version](#content-type-template-and-version) #### +#### [](#content-type-template-and-version)[6.3.4. `Content-Type` Template and Version](#content-type-template-and-version) The `Content-Type` template and version are represented by the `contentTypeTemplate` and`version` YAML properties. @@ -566,7 +611,7 @@ The combination of `contentTypeTemplate` and version results in the creation of application/vnd.newsletter.v1+json ``` -#### [](#default-headers)[6.3.5. Default Headers](#default-headers) #### +#### [](#default-headers)[6.3.5. Default Headers](#default-headers) Default headers are represented by the `headers` map in YAML. @@ -585,7 +630,7 @@ headers: That `headers` section results in adding the `Accept` and `Cache-Control` headers with appropriate list of values in your HTTP request. -#### [](#required-dependencies)[6.3.6. Required Dependencies](#required-dependencies) #### +#### [](#required-dependencies)[6.3.6. Required Dependencies](#required-dependencies) Required dependencies are represented by `required` property in YAML. @@ -598,7 +643,7 @@ start if the required dependency is not registered in Zookeeper. You can read more about Spring Cloud Zookeeper Presence Checker[later in this document](#spring-cloud-zookeeper-dependency-watcher-presence-checker). -#### [](#stubs)[6.3.7. Stubs](#stubs) #### +#### [](#stubs)[6.3.7. Stubs](#stubs) You can provide a colon-separated path to the JAR containing stubs of the dependency, as shown in the following example: @@ -618,7 +663,7 @@ example: `stubs: org.springframework:myApp` -### [](#spring-cloud-zookeeper-dependencies-configuring)[6.4. Configuring Spring Cloud Zookeeper Dependencies](#spring-cloud-zookeeper-dependencies-configuring) ### +### [](#spring-cloud-zookeeper-dependencies-configuring)[6.4. Configuring Spring Cloud Zookeeper Dependencies](#spring-cloud-zookeeper-dependencies-configuring) You can set the following properties to enable or disable parts of Zookeeper Dependencies functionalities: @@ -632,19 +677,18 @@ You can set the following properties to enable or disable parts of Zookeeper Dep * `spring.cloud.zookeeper.dependency.resttemplate.enabled` (enabled by default): When enabled, this property modifies the request headers of a `@LoadBalanced`-annotated`RestTemplate` such that it passes headers and content type with the version set in dependency configuration. Without this setting, those two parameters do not work. -[](#spring-cloud-zookeeper-dependency-watcher)[7. Spring Cloud Zookeeper Dependency Watcher](#spring-cloud-zookeeper-dependency-watcher) ----------- +## [](#spring-cloud-zookeeper-dependency-watcher)[7. Spring Cloud Zookeeper Dependency Watcher](#spring-cloud-zookeeper-dependency-watcher) The Dependency Watcher mechanism lets you register listeners to your dependencies. The functionality is, in fact, an implementation of the `Observator` pattern. When a dependency changes, its state (to either UP or DOWN), some custom logic can be applied. -### [](#activating-2)[7.1. Activating](#activating-2) ### +### [](#activating-2)[7.1. Activating](#activating-2) Spring Cloud Zookeeper Dependencies functionality needs to be enabled for you to use the Dependency Watcher mechanism. -### [](#registering-a-listener)[7.2. Registering a Listener](#registering-a-listener) ### +### [](#registering-a-listener)[7.2. Registering a Listener](#registering-a-listener) To register a listener, you must implement an interface called`org.springframework.cloud.zookeeper.discovery.watcher.DependencyWatcherListener` and register it as a bean. The interface gives you one method: @@ -657,7 +701,7 @@ If you want to register a listener for a particular dependency, the `dependencyN be the discriminator for your concrete implementation. `newState` provides you with information about whether your dependency has changed to `CONNECTED` or `DISCONNECTED`. -### [](#spring-cloud-zookeeper-dependency-watcher-presence-checker)[7.3. Using the Presence Checker](#spring-cloud-zookeeper-dependency-watcher-presence-checker) ### +### [](#spring-cloud-zookeeper-dependency-watcher-presence-checker)[7.3. Using the Presence Checker](#spring-cloud-zookeeper-dependency-watcher-presence-checker) Bound with the Dependency Watcher is the functionality called Presence Checker. It lets you provide custom behavior when your application boots, to react according to the state @@ -675,8 +719,7 @@ Because the `DefaultDependencyPresenceOnStartupVerifier` is registered only when no bean of type `DependencyPresenceOnStartupVerifier`, this functionality can be overridden. -[](#spring-cloud-zookeeper-config)[8. Distributed Configuration with Zookeeper](#spring-cloud-zookeeper-config) ----------- +## [](#spring-cloud-zookeeper-config)[8. Distributed Configuration with Zookeeper](#spring-cloud-zookeeper-config) Zookeeper provides a[hierarchical namespace](https://zookeeper.apache.org/doc/current/zookeeperOver.html#sc_dataModelNameSpace)that lets clients store arbitrary data, such as configuration data. Spring Cloud Zookeeper Config is an alternative to the[Config Server and Client](https://github.com/spring-cloud/spring-cloud-config). @@ -702,7 +745,7 @@ only to the instances of the service named `testApp`. Configuration is currently read on startup of the application. Sending a HTTP `POST`request to `/refresh` causes the configuration to be reloaded. Watching the configuration namespace (which Zookeeper supports) is not currently implemented. -### [](#activating-3)[8.1. Activating](#activating-3) ### +### [](#activating-3)[8.1. Activating](#activating-3) Including a dependency on`org.springframework.cloud:spring-cloud-starter-zookeeper-config` enables autoconfiguration that sets up Spring Cloud Zookeeper Config. @@ -710,7 +753,7 @@ autoconfiguration that sets up Spring Cloud Zookeeper Config. | |When working with version 3.4 of Zookeeper you need to change
the way you include the dependency as described [here](#spring-cloud-zookeeper-install).| |---|----------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#config-data-import)[8.2. Spring Boot Config Data Import](#config-data-import) ### +### [](#config-data-import)[8.2. Spring Boot Config Data Import](#config-data-import) Spring Boot 2.4 introduced a new way to import configuration data via the `spring.config.import` property. This is now the default way to get configuration from Zookeeper. @@ -737,7 +780,7 @@ This will optionally load configuration only from `/contextone` and `/context/tw | |A `bootstrap` file (properties or yaml) is **not** needed for the Spring Boot Config Data method of import via `spring.config.import`.| |---|--------------------------------------------------------------------------------------------------------------------------------------| -### [](#customizing)[8.3. Customizing](#customizing) ### +### [](#customizing)[8.3. Customizing](#customizing) Zookeeper Config may be customized by setting the following properties: @@ -764,7 +807,7 @@ spring: | |If you have set `spring.cloud.bootstrap.enabled=true` or `spring.config.use-legacy-processing=true`, or included `spring-cloud-starter-bootstrap`, then the above values will need to be placed in `bootstrap.yml` instead of `application.yml`.| |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -### [](#access-control-lists-acls)[8.4. Access Control Lists (ACLs)](#access-control-lists-acls) ### +### [](#access-control-lists-acls)[8.4. Access Control Lists (ACLs)](#access-control-lists-acls) You can add authentication information for Zookeeper ACLs by calling the `addAuthInfo`method of a `CuratorFramework` bean. One way to accomplish this is to provide your own`CuratorFramework` bean, as shown in the following example: @@ -807,4 +850,6 @@ resources/META-INF/spring.factories org.springframework.cloud.bootstrap.BootstrapConfiguration=\ my.project.CustomCuratorFrameworkConfig,\ my.project.DefaultCuratorFrameworkConfig -``` \ No newline at end of file +``` + +if (window.parent == window) {(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1\*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,'script','//www.google-analytics.com/analytics.js','ga');ga('create', 'UA-2728886-23', 'auto', {'siteSpeedSampleRate': 100});ga('send', 'pageview');} \ No newline at end of file diff --git a/docs/spring-boot/deployment.md b/docs/spring-boot/deployment.md index 8bbb85c11eaba4d2e574acfe9ae26abb38159c75..a7cc240dea81664d935a1c73bebe6ce0339aa5cf 100644 --- a/docs/spring-boot/deployment.md +++ b/docs/spring-boot/deployment.md @@ -499,6 +499,7 @@ $ systemctl enable myapp.service 默认脚本支持以下属性替换: + | Name |说明| Gradle default | Maven default | |--------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------|------------------------------------------------------------| | `mode` |脚本模式。| `auto` | `auto` | @@ -525,15 +526,16 @@ $ systemctl enable myapp.service 默认脚本支持以下环境属性: + | Variable |说明| -|-----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|-----------------------|---------------| | `MODE` |操作的“模式”。
默认值取决于构建 jar 的方式,但通常是`auto`(这意味着它试图通过检查来猜测它是否是一个 init 脚本)如果它是一个名为`init.d`的目录中的符号链接)。
你可以显式地将它设置为`service`,这样 `stop|start|status|restart` commands work or to `run` if you want to run the script in the foreground.| | `RUN_AS_USER` |将用于运行该应用程序的用户。
当未设置时,将使用 OWNS jar 文件的用户。| |`USE_START_STOP_DAEMON`|是否应该使用`start-stop-daemon`命令来控制进程。
默认为`true`。| | `PID_FOLDER` |PID 文件夹的根名(默认为 `/var/run’)。| | `LOG_FOLDER` |放置日志文件的文件夹的名称(默认情况下为“/var/log”)。| | `CONF_FOLDER` |读取.conf 文件的文件夹的名称(默认情况下与 jar-file 文件相同)。| -| `LOG_FILENAME` |在`LOG_FOLDER`(.log` 默认情况下)中日志文件的名称。| +| `LOG_FILENAME` |在`LOG_FOLDER`(`.log` 默认情况下)中日志文件的名称。| | `APP_NAME` |应用程序的名称。
如果 jar 是从符号链接运行的,则脚本猜测应用程序的名称。
如果不是符号链接,或者你希望显式设置应用程序名称,这可能是有用的。| | `RUN_ARGS` |要传递给程序( Spring 引导应用程序)的参数。| | `JAVA_HOME` |默认情况下,`java`可执行文件的位置是通过使用`PATH`发现的,但是如果在`$JAVA_HOME/bin/java`处有一个可执行文件,则可以显式地设置它。| @@ -542,8 +544,11 @@ $ systemctl enable myapp.service | `DEBUG` |如果不是空的,则在 shell 进程上设置`-x`标志,允许你查看脚本中的逻辑。| | `STOP_WAIT_TIME` |在强制关闭应用程序之前,停止应用程序所需的等待时间(以秒为单位)(默认为 `60’)。| -| |`PID_FOLDER`,`LOG_FOLDER`,和`LOG_FILENAME`变量仅对`init.d`服务有效。
对于`systemd`,通过使用’service’脚本进行等效的自定义。
有关更多详细信息,请参见[服务单元配置手册页](https://www.freedesktop.org/software/systemd/man/systemd.service.html)。| -|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +> `PID_FOLDER`,`LOG_FOLDER`,和`LOG_FILENAME`变量仅对`init.d`服务有效。 +> 对于`systemd`,通过使用’service’脚本进行等效的自定义。 +> 有关更多详细信息,请参见[服务单元配置手册页](https://www.freedesktop.org/software/systemd/man/systemd.service.html)。 + 除了`JARFILE`和`APP_NAME`之外,可以通过使用`.conf`文件配置上一节中列出的设置。该文件预计将位于 jar 文件的旁边,并且具有相同的名称,但后缀为`.conf`,而不是`.jar`。例如,名为`/var/myapp/myapp.jar`的 jar 使用名为`/var/myapp/myapp.conf`的配置文件,如以下示例所示: @@ -554,8 +559,7 @@ JAVA_OPTS=-Xmx1024M LOG_FOLDER=/custom/log/folder ``` -| |如果不喜欢将配置文件放在 jar 文件旁边,则可以设置`CONF_FOLDER`环境变量来定制配置文件的位置。| -|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------| +> 如果不喜欢将配置文件放在 jar 文件旁边,则可以设置`CONF_FOLDER`环境变量来定制配置文件的位置。 要了解如何适当地保护此文件,请参见[获得 init.d 服务的指导方针](#deployment.installing.nix-services.init-d.securing)。 diff --git a/docs/spring-cloud/README.md b/docs/spring-cloud/README.md new file mode 100644 index 0000000000000000000000000000000000000000..0e1a784f054e84dbe04a1c778f197a26b343ff2c --- /dev/null +++ b/docs/spring-cloud/README.md @@ -0,0 +1 @@ +# Spring 云 \ No newline at end of file diff --git a/docs/spring-cloud/document-overview.md b/docs/spring-cloud/document-overview.md new file mode 100644 index 0000000000000000000000000000000000000000..b4ca8578b675c95fa0d8f2e51207ae8b6d7531b8 --- /dev/null +++ b/docs/spring-cloud/document-overview.md @@ -0,0 +1,30 @@ +# Spring 云文档 + + +本节提供了 Spring 云参考文档的简要概述。它是这份文件其余部分的一张地图。 + +## [](#documentation-about)[1.关于文档](#documentation-about) + +Spring 云参考指南如下所示 + +* [Multi-page HTML](https://docs.spring.io/spring-cloud/docs/2021.0.1/reference/html) + +* [单页 HTML](https://docs.spring.io/spring-cloud/docs/2021.0.1/reference/htmlsingle) + +* [PDF](https://docs.spring.io/spring-cloud/docs/2021.0.1/reference/pdf/spring-cloud.pdf) + +本文件的副本可供你自己使用并分发给他人,但前提是你不对此类副本收取任何费用,并且还需每一份副本均包含本版权声明,无论是以印刷形式还是以电子方式分发。 + +## [](#documentation-getting-help)[2. Getting Help](#documentation-getting-help) + +如果你在云计算方面有困难,我们愿意提供帮助。 + +* 学习云的基础知识。如果你从 Spring Cloud 开始,请尝试使用[guides](https://spring.io/guides)中的一个。 + +* 问一个问题。我们监控[stackoverflow.com](https://stackoverflow.com)中带有[`spring-cloud`](https://stackoverflow.com/tags/spring-cloud)标记的问题。 + +* 在[Spring Cloud Gitter](https://gitter.im/spring-cloud/spring-cloud)与我们聊天 + +| |所有的云都是开源的,包括文档。如果你发现
DOCS 存在问题,或者你希望改进这些问题,请参与进来。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------| + diff --git a/docs/spring-cloud/legal.md b/docs/spring-cloud/legal.md new file mode 100644 index 0000000000000000000000000000000000000000..2ab36e85c11e8bbc13dad3363afd5376928c119a --- /dev/null +++ b/docs/spring-cloud/legal.md @@ -0,0 +1,7 @@ +# 法律 + +2021.0.1 + +版权所有 2012-2020 + +本文件的副本可供你自己使用并分发给他人,但前提是你不对此类副本收取任何费用,并且还需每一份副本均包含本版权声明,无论是以印刷形式还是以电子方式分发。 diff --git a/docs/spring-cloud/spring-cloud-build.md b/docs/spring-cloud/spring-cloud-build.md new file mode 100644 index 0000000000000000000000000000000000000000..6ac37b28b508c8d0bc34f11f0d7b95e50df7f7aa --- /dev/null +++ b/docs/spring-cloud/spring-cloud-build.md @@ -0,0 +1,423 @@ +# Spring 云构建 + + +Spring 云构建是 Spring 云用于插件和依赖管理的一个常见实用程序项目。 + +## [构建和部署](#_building_and_deploying) + + +要在本地安装: + +``` +$ mvn install -s .settings.xml +``` + +并将快照部署到 repo。 Spring.io: + +``` +$ mvn deploy -DaltSnapshotDeploymentRepository=repo.spring.io::default::https://repo.spring.io/snapshot +``` + +用于发布版本的构建使用 + +``` +$ mvn deploy -DaltReleaseDeploymentRepository=repo.spring.io::default::https://repo.spring.io/release +``` + +供 JCenter 使用 + +``` +$ mvn deploy -DaltReleaseDeploymentRepository=bintray::default::https://api.bintray.com/maven/spring/jars/org.springframework.cloud:build +``` + +供 Maven 中央使用 + +``` +$ mvn deploy -P central -DaltReleaseDeploymentRepository=sonatype-nexus-staging::default::https://oss.sonatype.org/service/local/staging/deploy/maven2 +``` + +(“central”profile 可用于 Spring Cloud 中的所有项目,并且它设置了 GPG jar 签名,并且存储库必须为该项目单独指定,因为它是启动器父程序的父程序,用户反过来将其作为自己的父程序)。 + +## [Contributing](#_contributing) + + +Spring Cloud 是在非限制性的 Apache2.0 许可下发布的,遵循非常标准的 GitHub 开发流程,使用 GitHub Tracker 处理问题,并将拉请求合并到 Master 中。如果你想贡献一些微不足道的东西,请不要犹豫,但要遵循下面的指导方针。 + +### [签署贡献者许可协议](#_sign_the_contributor_license_agreement) + +在我们接受一个重要的补丁或拉请求之前,我们需要你签署[贡献者许可协议](https://cla.pivotal.io/sign/spring)。签署贡献者协议并不会授予任何人对主库的提交权限,但这确实意味着我们可以接受你的贡献,并且如果我们接受了,你将获得作者信用。活跃的贡献者可能会被要求加入核心团队,并被赋予合并拉请求的能力。 + +### [Code of Conduct](#_code_of_conduct) + +该项目遵守贡献者契约[code of conduct](https://github.com/spring-cloud/spring-cloud-build/blob/master/docs/src/main/asciidoc/code-of-conduct.adoc)。通过参与,你将被期望坚持这一准则。请向[[电子邮件保护]]报告不可接受的行为(/cdn-cgi/l/email-protection#98ebe8EAF 1f6ffb5fbf7fcfdb5fbf7fbf6fcedfbecd8e8f1eeef7ECF 9f4b6f1f7)。 + +### [守则惯例和内部管理](#_code_conventions_and_housekeeping) + +这些都不是拉请求所必需的,但它们都会有所帮助。它们也可以在原始的拉请求之后但在合并之前添加。 + +* 使用 Spring 框架代码格式约定。如果使用 Eclipse,则可以使用[Spring Cloud Build](https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-dependencies-parent/eclipse-code-formatter.xml)项目中的 `eclipse-code-formatter.xml’文件导入格式化设置。如果使用 IntelliJ,可以使用[Eclipse 代码格式化插件](https://plugins.jetbrains.com/plugin/6546)导入相同的文件。 + +* 确保所有新的`.java`文件都有一个简单的 Javadoc 类注释,其中至少有一个“@author”标记来标识你,并且最好至少有一个段落来说明这个类的目的。 + +* 将 ASF 许可标头注释添加到所有新的`.java`文件(从项目中的现有文件复制) + +* 将自己作为`@author`添加到要进行实质性修改的.java 文件中(不仅仅是外观上的更改)。 + +* 添加一些 Javadocs,如果你更改了名称空间,还可以添加一些 XSDDOC 元素。 + +* 几个单元测试也会有很大帮助——必须有人去做。 + +* 如果没有其他人正在使用你的分支,请将它重新设置为当前的主分支(或主项目中的其他目标分支)。 + +* 在编写提交消息时,请遵循[这些约定](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html),如果你正在修复现有的问题,请在提交消息的末尾添加`Fixes gh-XXXX`(其中 xxxx 是问题编号)。 + +### [checkstyle](#_checkstyle) + +Spring 云构建附带了一组 checkstyle 规则。你可以在`spring-cloud-build-tools`模块中找到它们。该模块下最值得注意的文件是: + +Spring-云构建工具/ + +``` +└── src +    ├── checkstyle +    │   └── checkstyle-suppressions.xml (3) +    └── main +    └── resources +    ├── checkstyle-header.txt (2) +    └── checkstyle.xml (1) +``` + +|**1**|默认的 checkstyle 规则| +|-----|-------------------------| +|**2**|文件头设置| +|**3**|默认抑制规则| + +#### [checkstyle 配置](#_checkstyle_configuration) + +checkstyle 规则是**默认禁用**。要将 checkstyle 添加到项目中,只需定义以下属性和插件。 + +POM.xml + +``` + +true (1) + true + (2) + true + (3) + + + + + (4) + io.spring.javaformat + spring-javaformat-maven-plugin + + (5) + org.apache.maven.plugins + maven-checkstyle-plugin + + + + + + (5) + org.apache.maven.plugins + maven-checkstyle-plugin + + + + +``` + +|**1**|构建 checkstyle 错误失败| +|-----|--------------------------------------------------------------------------------------------------------------| +|**2**|构建 checkstyle 冲突失败| +|**3**|CheckStyle 还分析了测试源| +|**4**|添加 Spring Java 格式插件,该插件将重新格式化你的代码,以传递大多数 CheckStyle 格式设置规则| +|**5**|将 CheckStyle 插件添加到构建和报告阶段| + +如果你需要抑制一些规则(例如,行长需要更长),那么在`${project.root}/src/checkstyle/checkstyle-suppressions.xml`下定义一个文件就足够了。示例: + +projectRoot/SRC/checkstyle/checkstyle-suppresions.xml + +``` + + + + + + +``` + +建议将`${spring-cloud-build.rootFolder}/.editorconfig`和`${spring-cloud-build.rootFolder}/.springformat`复制到你的项目中。这样,将应用一些默认的格式设置规则。你可以通过运行以下脚本来实现此目的: + +``` +$ curl https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/.editorconfig -o .editorconfig +$ touch .springformat +``` + +### [IDE setup](#_ide_setup) + +#### [Intellij IDEA](#_intellij_idea) + +为了设置 IntelliJ,你应该导入我们的编码约定、检查配置文件并设置 CheckStyle 插件。以下文件可以在[Spring Cloud Build](https://github.com/spring-cloud/spring-cloud-build/tree/master/spring-cloud-build-tools)项目中找到。 + +Spring-云构建工具/ + +``` +└── src +    ├── checkstyle +    │   └── checkstyle-suppressions.xml (3) +    └── main +    └── resources +    ├── checkstyle-header.txt (2) +    ├── checkstyle.xml (1) +    └── intellij +       ├── Intellij_Project_Defaults.xml (4) +       └── Intellij_Spring_Boot_Java_Conventions.xml (5) +``` + +|**1**|默认的 checkstyle 规则| +|-----|--------------------------------------------------------------------------| +|**2**|文件头设置| +|**3**|默认抑制规则| +|**4**|适用大多数 CheckStyle 规则的 IntelliJ 的项目默认值| +|**5**|适用大多数 CheckStyle 规则的 IntelliJ 的项目风格约定| + +![Code style](https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/docs/src/main/asciidoc/images/intellij-code-style.png) + +图 1。代码样式 + +转到`File``Settings``Editor``Code style`。点击`Scheme`区域旁边的图标。在这里,单击`Import Scheme`值并选择`Intellij IDEA code style XML`选项。导入`spring-cloud-build-tools/src/main/resources/intellij/Intellij_Spring_Boot_Java_Conventions.xml`文件。 + +![Code style](https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/docs/src/main/asciidoc/images/intellij-inspections.png) + +图 2。检查剖面 + +转到`File``Settings``Editor``Inspections`。点击`Profile`区域旁边的图标。在那里,单击`Import Profile`并导入`spring-cloud-build-tools/src/main/resources/intellij/Intellij_Project_Defaults.xml`文件。 + +Checkstyle + +要让 IntelliJ 使用 CheckStyle,你必须安装`Checkstyle`插件。建议还安装`Assertions2Assertj`来自动转换 JUnit 断言 + +![Checkstyle](https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/docs/src/main/asciidoc/images/intellij-checkstyle.png) + +转到`File``Settings``Other settings``Checkstyle`。点击`Configuration file`部分中的`+`图标。在这里,你必须定义应该从哪里选择 CheckStyle 规则。在上面的图片中,我们从克隆的云构建存储库中选择了规则。但是,你可以指向 Spring Cloud Build 的 GitHub 存储库(例如`checkstyle.xml`:`[https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/main/resources/checkstyle.xml](https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/main/resources/checkstyle.xml)`)。我们需要提供以下变量: + +* `checkstyle.header.file`-请将其指向 Spring Cloud Build 的`spring-cloud-build-tools/src/main/resources/checkstyle-header.txt`文件,可以在你的克隆 repo 中,也可以通过`[https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/main/resources/checkstyle-header.txt](https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/main/resources/checkstyle-header.txt)`URL。 + +* `checkstyle.suppressions.file`-默认抑制。请将它指向 Spring Cloud Build 的`spring-cloud-build-tools/src/checkstyle/checkstyle-suppressions.xml`文件,或者在你的克隆 repo 中,或者通过`[https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/checkstyle/checkstyle-suppressions.xml](https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/checkstyle/checkstyle-suppressions.xml)`URL。 + +* `checkstyle.additional.suppressions.file`-此变量对应于本地项目中的抑制。例如,你正在处理`spring-cloud-contract`。然后指向`project-root/src/checkstyle/checkstyle-suppressions.xml`文件夹。`spring-cloud-contract`的例子是:`/home/username/spring-cloud-contract/src/checkstyle/checkstyle-suppressions.xml`。 + +| |记住将`Scan Scope`设置为`All sources`,因为我们为生产和测试源应用了 checkstyle 规则。| +|---|------------------------------------------------------------------------------------------------------------------| + +### [重复查找器](#_duplicate_finder) + +Spring 云构建带来了`basepom:duplicate-finder-maven-plugin`,它允许在 Java Classpath 上标记重复的和冲突的类和资源。 + +#### [重复查找器配置](#_duplicate_finder_configuration) + +重复查找器是**默认启用**,将在 Maven 构建的`verify`阶段运行,但是只有在项目的`duplicate-finder-maven-plugin`部分中添加`duplicate-finder-maven-plugin`,它才会在项目中生效。 + +POM.xml + +``` + + + + org.basepom.maven + duplicate-finder-maven-plugin + + + +``` + +对于其他属性,我们设置了[插件文档](https://github.com/basepom/duplicate-finder-maven-plugin/wiki)中列出的默认值。 + +你可以轻松地重写它们,但可以使用`duplicate-finder-maven-plugin`前缀设置所选属性的值。例如,将`duplicate-finder-maven-plugin.skip`设置为`true`,以便在构建中跳过重复检查。 + +如果需要将`ignoredClassPatterns`或`ignoredResourcePatterns`添加到设置中,请确保将它们添加到项目的插件配置部分中: + +``` + + + + org.basepom.maven + duplicate-finder-maven-plugin + + + org.joda.time.base.BaseDateTime + .*module-info + + + changelog.txt + + + + + +``` + +## [压平 Poms](#_flattening_the_poms) + + +为了避免传播构建 Spring 云项目所需的构建设置,我们使用了 Maven Flaten 插件。它的优点是允许你在将“Clean” POM 发布到存储库时使用所需的任何功能。 + +为了添加它,将`org.codehaus.mojo:flatten-maven-plugin`添加到你的`pom.xml`中。 + +``` + + + + org.codehaus.mojo + flatten-maven-plugin + + + +``` + +## [重用文档](#_reusing_the_documentation) + + +Spring Cloud Build 发布其`spring-cloud-build-docs`模块,该模块包含有用的脚本(例如 Readme Generation Ruby 脚本)和用于 Spring 云文档的 CSS、XSLT 和图像。如果你想遵循生成文档的相同约定方法,只需将这些插件添加到`docs`模块中 + +``` + + deploy (8) + + + + docs + + + + pl.project13.maven + git-commit-id-plugin (1) + + + org.apache.maven.plugins + maven-dependency-plugin (2) + + + org.apache.maven.plugins + maven-resources-plugin (3) + + + org.codehaus.mojo + exec-maven-plugin (4) + + + org.asciidoctor + asciidoctor-maven-plugin (5) + + + org.apache.maven.plugins + maven-antrun-plugin (6) + + + maven-deploy-plugin (7) + + + + + +``` + +|**1**|这个插件下载设置了项目的所有 Git 信息。| +|-----|----------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|此插件下载`spring-cloud-build-docs`模块的资源| +|**3**|此插件解包`spring-cloud-build-docs`模块的资源| +|**4**|这个插件生成一个`adoc`文件,其中包含 Classpath 中的所有配置属性。| +|**5**|解析 ASCIIDoctor 文档需要使用此插件。| +|**6**|需要此插件来将资源复制到正确的最终目的地,并生成主 readme.ADOC,并断言没有文件使用未解决的链接| +|**7**|此插件确保生成的 ZIP DOCS 将被发布| +|**8**|此属性打开 \<7\>的“部署”阶段| + +| |插件声明的顺序很重要!| +|---|---------------------------------------------| + +为了使构建生成带有所有配置属性的`adoc`文件,你的`docs`模块应该包含与 Classpath 相关的所有依赖项,你需要扫描这些依赖项以获取配置属性。该文件将被输出到`${docsModule}/src/main/asciidoc/_configprops.adoc`文件(可通过`configprops.path`属性进行配置)。 + +如果你想修改将哪些配置属性放入表中,你可以调整`configprops.inclusionPattern`模式,以仅包括属性的一个子集(例如`spring.sleuth.*`)。 + +Spring 云构建 DOCS 附带了一组可以重用的 ASCIIDoctor 属性。 + +``` + + shared + true + + left + 4 + true + ${project.basedir}/[email protected] + ${project.basedir}/src/main/[email protected] + ${project.basedir}/target/[email protected] + + + + ${maven.multiModuleProjectDirectory}@ + + ${docs.main}@ + https://github.com/spring-cloud/${docs.main}@ + + https://raw.githubusercontent.com/spring-cloud/${docs.main}/${github-tag}@ + + https://github.com/spring-cloud/${docs.main}/tree/${github-tag}@ + + https://github.com/spring-cloud/${docs.main}/issues/@ + https://github.com/spring-cloud/${docs.main}/[email protected] + https://github.com/spring-cloud/${docs.main}/tree/[email protected] + + ${index-link}@ + + + + ${project.version}@ + ${project.version}@ + ${github-tag}@ + ${version-type}@ + https://docs.spring.io/${docs.main}/docs/${project.version}@ + ${github-raw}@ + ${project.version}@ + ${docs.main}@ + +``` + +## [更新指南](#_updating_the_guides) + + +我们假设你的项目包含`guides`文件夹下的指南。 + +``` +. +└── guides + ├── gs-guide1 + ├── gs-guide2 + └── gs-guide3 +``` + +这意味着该项目包含 3 个指南,与 Spring Guides org 中的以下指南相对应。 + +* [https://github.com/spring-guides/gs-guide1](https://github.com/spring-guides/gs-guide1) + +* [https://github.com/spring-guides/gs-guide2](https://github.com/spring-guides/gs-guide2) + +* [https://github.com/spring-guides/gs-guide3](https://github.com/spring-guides/gs-guide3) + +如果你使用`-Pguides`配置文件来部署你的项目,则如下所示 + +``` +$ ./mvnw clean deploy -Pguides +``` + +将会发生的情况是,对于 GA 项目版本,我们将克隆`gs-guide1`、`gs-guide2`和`gs-guide3`,并使用位于`guides`项目下的内容更新它们的内容。 + +通过不添加`guides`配置文件,或者在打开配置文件时传递`-DskipGuides`系统属性,你可以跳过此操作。 + +你可以通过`guides-project.version`(默认为`${project.version}`)配置传递给指南的项目版本。指南更新的阶段可以通过`guides-update.phase`进行配置(默认为`deploy`)。 diff --git a/docs/spring-cloud/spring-cloud-bus.md b/docs/spring-cloud/spring-cloud-bus.md new file mode 100644 index 0000000000000000000000000000000000000000..6be7f7ee7b18d7f75f601da6d72a5338c73d3ec2 --- /dev/null +++ b/docs/spring-cloud/spring-cloud-bus.md @@ -0,0 +1,189 @@ +# Spring 云总线 + +Spring 云总线将分布式系统的节点与轻量级消息代理连接起来。然后可以使用此代理来广播状态更改(例如配置更改)或其他管理指令。一个关键的想法是,总线就像是 Spring 启动应用程序的分布式执行器,该应用程序是按比例扩展的。然而,它也可以用作应用程序之间的沟通渠道。该项目为 AMQP 代理或 Kafka 提供了作为传输的启动器。 + +| |Spring 云是在非限制性的 Apache2.0 许可下发布的。如果你想对文档的这一部分做出贡献,或者你发现了一个错误,请在[github](https://github.com/spring-cloud/spring-cloud-bus)上找到项目中的源代码和问题追踪器。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## [](#quick-start)[1. Quick Start](#quick-start) + +Spring 如果云总线在 Classpath 上检测到自身,则通过添加 Spring 引导自动配置来工作。要启用总线,请在依赖管理中添加`spring-cloud-starter-bus-amqp`或 ` Spring-cloud-starter-bus-kafka`。 Spring 剩下的事由云来解决。确保代理(RabbitMQ 或 Kafka)可用并进行了配置。在 LocalHost 上运行时,你不需要做任何事情。如果远程运行,请使用 Spring Cloud Connectors 或 Spring Boot 约定来定义代理凭据,如下面的 Rabbit 示例所示: + +应用程序.yml + +``` +spring: + rabbitmq: + host: mybroker.com + port: 5672 + username: user + password: secret +``` + +总线目前支持将消息发送到监听的所有节点或特定服务的所有节点(由 Eureka 定义)。执行器名称空间`/bus/*`具有一些 HTTP 端点。目前,有两个项目已经实施。第一种是`/bus/env`,它发送键/值对来更新每个节点的 Spring 环境。第二种是`/bus/refresh`,它重新加载每个应用程序的配置,就好像它们都在其`/refresh`端点上被 pinged 了一样。 + +| |Spring 云总线启动器覆盖 Rabbit 和 Kafka,因为这是两个最
的常见实现。然而, Spring 云流是相当灵活的,并且活页夹
与`spring-cloud-bus`一起工作。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## [](#bus-endpoints)[2.总线端点](#bus-endpoints) + +Spring 云总线提供了两个端点,`/actuator/busrefresh`和`/actuator/busenv`,这两个端点分别对应于 Spring 云共享空间中的各个执行器端点,`/actuator/refresh` 和`/actuator/env`。 + +### [](#bus-refresh-endpoint)[2.1.总线刷新端点](#bus-refresh-endpoint) + +`/actuator/busrefresh`端点清除`RefreshScope`缓存并重新绑定 @configrationProperties`。有关更多信息,请参见[Refresh Scope](#refresh-scope)文档。 + +要公开`/actuator/busrefresh`端点,需要向应用程序添加以下配置: + +``` +management.endpoints.web.exposure.include=busrefresh +``` + +### [](#bus-env-endpoint)[2.2.总线 ENV 端点](#bus-env-endpoint) + +`/actuator/busenv`端点使用跨多个实例的指定键/值对更新每个实例环境。 + +要公开`/actuator/busenv`端点,需要向应用程序添加以下配置: + +``` +management.endpoints.web.exposure.include=busenv +``` + +`/actuator/busenv`端点接受具有以下形状的`POST`请求: + +``` +{ + "name": "key1", + "value": "value1" +} +``` + +## [](#addressing-an-instance)[3.寻址实例](#addressing-an-instance) + +应用程序的每个实例都有一个服务 ID,其值可以用 ` Spring.cloud.bus.id` 设置,其值应该是一个以冒号分隔的标识符列表,顺序从最小特定到最特定。默认值是作为`spring.application.name`和 `server.port’(或`spring.application.index`,如果设置)的组合从环境构造的。ID 的默认值以`app:index:id`的形式构造,其中: + +* `app`是`vcap.application.name`,如果它存在,或者`spring.application.name` + +* `index`是`vcap.application.instance_index`,如果存在,` Spring.application.index`,`local.server.port`,`server.port`,或`0`(按此顺序排列)。 + +* `id`是`vcap.application.instance_id`,如果它存在,或者是一个随机值。 + +HTTP 端点接受一个“destination”路径参数,例如“/busrefresh/customers:9000”,其中`destination`是一个服务 ID。如果 ID 由总线上的一个实例拥有,那么它将处理消息,而所有其他实例将忽略它。 + +## [](#addressing-all-instances-of-a-service)[4.处理服务的所有实例](#addressing-all-instances-of-a-service) + +在 Spring `PathMatcher`(路径分隔符为冒号—`:`)中使用“destination”参数来确定实例是否处理消息。使用前面的示例,`/busenv/customers:**`的目标是“Customers”服务的所有实例,而不考虑服务 ID 的其余部分。 + +## [](#service-id-must-be-unique)[5.服务 ID 必须是唯一的](#service-id-must-be-unique) + +总线尝试两次消除处理一个事件——一次从原始的“ApplicationEvent”中删除,一次从队列中删除。为此,它会根据当前的服务 ID 检查发送服务 ID。如果一个服务的多个实例具有相同的 ID,则不会对事件进行处理。当在本地机器上运行时,每个服务都位于不同的端口上,而该端口是 ID 的一部分。Cloud Foundry 提供了一个用于区分的索引。要确保 ID 在 Cloud Foundry 之外是唯一的,请将`spring.application.index`设置为服务的每个实例都是唯一的。 + +## [](#customizing-the-message-broker)[6.自定义消息代理](#customizing-the-message-broker) + +Spring 云总线使用[Spring Cloud Stream](https://cloud.spring.io/spring-cloud-stream)来广播消息。因此,要使消息流起来,你只需要在 Classpath 中包含你选择的绑定器实现。与 AMQP 和 Kafka(` Spring-cloud-starter-bus-[AMQP|Kafka]`)的总线有方便的启动器。一般来说, Spring Cloud Stream 依赖于 Spring Boot AutoConfiguration 约定来配置中间件。例如,可以使用 ` Spring.RabbitMQ.*` 配置属性来更改 AMQP 代理地址。 Spring Cloud Bus 在`spring.cloud.bus.*`中具有少量的本机配置属性(例如,` Spring.cloud.bus.destination` 是要用作外部中间件的主题的名称)。通常情况下,默认值就足够了。 + +要了解有关如何自定义 Message Broker 设置的更多信息,请参阅 Spring Cloud Stream 文档。 + +## [](#tracing-bus-events)[7.追踪总线事件](#tracing-bus-events) + +可以通过设置 ` Spring.cloud.bus.trace.enabled=true` 来跟踪总线事件(`RemoteApplicationEvent`的子类)。如果这样做, Spring boot`TraceRepository`(如果存在)将显示发送的每个事件和来自每个服务实例的所有 ACK。以下示例来自`/trace`端点: + +``` +{ + "timestamp": "2015-11-26T10:24:44.411+0000", + "info": { + "signal": "spring.cloud.bus.ack", + "type": "RefreshRemoteApplicationEvent", + "id": "c4d374b7-58ea-4928-a312-31984def293b", + "origin": "stores:8081", + "destination": "*:**" + } + }, + { + "timestamp": "2015-11-26T10:24:41.864+0000", + "info": { + "signal": "spring.cloud.bus.sent", + "type": "RefreshRemoteApplicationEvent", + "id": "c4d374b7-58ea-4928-a312-31984def293b", + "origin": "customers:9000", + "destination": "*:**" + } + }, + { + "timestamp": "2015-11-26T10:24:41.862+0000", + "info": { + "signal": "spring.cloud.bus.ack", + "type": "RefreshRemoteApplicationEvent", + "id": "c4d374b7-58ea-4928-a312-31984def293b", + "origin": "customers:9000", + "destination": "*:**" + } +} +``` + +前面的跟踪显示,`RefreshRemoteApplicationEvent`是从 `customers:9000’发送的,向所有服务广播,并由`customers:9000`和 `stores:8081’接收。 + +为了自己处理 ACK 信号,你可以为“AckRemoteApplicationEvent”添加类型和类型到你的应用程序(并启用跟踪)。或者,你可以利用`TraceRepository`并从那里挖掘数据。 + +| |任何总线应用程序都可以跟踪 ACK。然而,有时,在可以对数据执行更复杂的
查询或将其转发给专门的跟踪服务的中心服务中执行此操作是很有用的。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## [](#broadcasting-your-own-events)[8.播放自己的活动](#broadcasting-your-own-events) + +总线可以承载`RemoteApplicationEvent`类型的任何事件。默认传输是 JSON,反序列化器需要提前知道将使用哪些类型。要注册一个新类型,你必须将其放入“org.springframework.cloud.bus.event”的子包中。 + +要自定义事件名,你可以在自定义类上使用`@JsonTypeName`,也可以使用默认策略,即使用类的简单名称。 + +| |生产者和消费者都需要访问类定义。| +|---|-----------------------------------------------------------------------| + +### [](#registering-events-in-custom-packages)[8.1.在自定义包中注册事件](#registering-events-in-custom-packages) + +如果不能或不想使用`org.springframework.cloud.bus.event`的子包处理自定义事件,则必须使用`@RemoteApplicationEventScan`注释指定要扫描 `RemoteApplicationEvent’类型事件的包。用`@RemoteApplicationEventScan`指定的包包括子包。 + +例如,考虑以下自定义事件,称为`MyEvent`: + +``` +package com.acme; + +public class MyEvent extends RemoteApplicationEvent { + ... +} +``` + +你可以通过以下方式向反序列化器注册该事件: + +``` +package com.acme; + +@Configuration +@RemoteApplicationEventScan +public class BusConfiguration { + ... +} +``` + +在不指定值的情况下,将注册使用`@RemoteApplicationEventScan`的类的包。在本例中,`com.acme`通过使用“BusConfiguration”包进行注册。 + +还可以在`@RemoteApplicationEventScan`上使用`value`、`basePackages`或`basePackageClasses`属性显式地指定要扫描的包,如以下示例所示: + +``` +package com.acme; + +@Configuration +//@RemoteApplicationEventScan({"com.acme", "foo.bar"}) +//@RemoteApplicationEventScan(basePackages = {"com.acme", "foo.bar", "fizz.buzz"}) +@RemoteApplicationEventScan(basePackageClasses = BusConfiguration.class) +public class BusConfiguration { + ... +} +``` + +上述`@RemoteApplicationEventScan`的所有示例都是等效的,因为通过在 `@remoteApplicationEventScan’上显式指定包,可以注册 `com.acme’包。 + +| |你可以指定要扫描的多个基包。| +|---|-----------------------------------------------| + +## [](#configuration-properties)[9.配置属性](#configuration-properties) + +要查看所有与总线相关的配置属性的列表,请检查[附录页](appendix.html)。 diff --git a/docs/spring-cloud/spring-cloud-circuitbreaker.md b/docs/spring-cloud/spring-cloud-circuitbreaker.md new file mode 100644 index 0000000000000000000000000000000000000000..ca7a457ca63f857f9346be66969bf7f62b83e05f --- /dev/null +++ b/docs/spring-cloud/spring-cloud-circuitbreaker.md @@ -0,0 +1,524 @@ +# Spring 云断路器 + + +[](#usage-documentation)[1.使用文档](#usage-documentation) + +Spring Cloud Circuitbreaker 项目包含 Resilience4J 和 Spring Retry 的实现。在 Spring cloud circuitbreaker 中实现的 API 是在 Spring cloud commons 中实现的。这些 API 的使用文档位于[Spring Cloud Commons documentation](https://docs.spring.io/spring-cloud-commons/docs/current/reference/html/#spring-cloud-circuit-breaker)中。 + +### [](#configuring-resilience4j-circuit-breakers)[1.1.配置弹性 4J 断路器](#configuring-resilience4j-circuit-breakers) + +#### [](#starters)[1.1.1. Starters](#starters) + +弹性 4J 实现有两个启动器,一个用于反应性应用程序,另一个用于非反应性应用程序。 + +* `org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j`-无反应应用 + +* `org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j`-反应性应用 + +#### [](#auto-configuration)[1.1.2.自动配置](#auto-configuration) + +你可以通过将 ` Spring.cloud.circuitbreaker.resilience4j.enabled` 设置为`false`来禁用 Resilience4j 自动配置。 + +#### [](#default-configuration)[1.1.3.默认配置](#default-configuration) + +要为你的所有断路器提供默认配置,请创建一个`Customize` Bean,并传递一个 `Resilience4jcircuitbreakerFactory’或`ReactiveResilience4JCircuitBreakerFactory`。`configureDefault`方法可用于提供默认配置。 + +``` +@Bean +public Customizer defaultCustomizer() { + return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id) + .timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(4)).build()) + .circuitBreakerConfig(CircuitBreakerConfig.ofDefaults()) + .build()); +} +``` + +##### [](#reactive-example)[反应式示例](#reactive-example) + +``` +@Bean +public Customizer defaultCustomizer() { + return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id) + .circuitBreakerConfig(CircuitBreakerConfig.ofDefaults()) + .timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(4)).build()).build()); +} +``` + +#### [](#specific-circuit-breaker-configuration)[1.1.4.特定断路器配置](#specific-circuit-breaker-configuration) + +与提供默认配置类似,你可以创建一个`Customize` Bean,这传递了一个 `Resilience4jcircuitbreakerFactory’或`ReactiveResilience4JCircuitBreakerFactory`。 + +``` +@Bean +public Customizer slowCustomizer() { + return factory -> factory.configure(builder -> builder.circuitBreakerConfig(CircuitBreakerConfig.ofDefaults()) + .timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(2)).build()), "slow"); +} +``` + +除了配置被创建的断路器之外,你还可以在断路器被创建之后但在断路器被返回给调用者之前自定义断路器。要做到这一点,你可以使用`addCircuitBreakerCustomizer`方法。这对于将事件处理程序添加到 Resilience4J 断路器非常有用。 + +``` +@Bean +public Customizer slowCustomizer() { + return factory -> factory.addCircuitBreakerCustomizer(circuitBreaker -> circuitBreaker.getEventPublisher() + .onError(normalFluxErrorConsumer).onSuccess(normalFluxSuccessConsumer), "normalflux"); +} +``` + +##### [](#reactive-example-2)[反应式示例](#reactive-example-2) + +``` +@Bean +public Customizer slowCustomizer() { + return factory -> { + factory.configure(builder -> builder + .timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(2)).build()) + .circuitBreakerConfig(CircuitBreakerConfig.ofDefaults()), "slow", "slowflux"); + factory.addCircuitBreakerCustomizer(circuitBreaker -> circuitBreaker.getEventPublisher() + .onError(normalFluxErrorConsumer).onSuccess(normalFluxSuccessConsumer), "normalflux"); + }; +} +``` + +#### [](#circuit-breaker-properties-configuration)[1.1.5.断路器特性配置](#circuit-breaker-properties-configuration) + +你可以在应用程序的配置属性文件中配置`CircuitBreaker`和`TimeLimiter`实例。属性配置比 Java`Customizer`配置具有更高的优先级。 + +``` +resilience4j.circuitbreaker: + instances: + backendA: + registerHealthIndicator: true + slidingWindowSize: 100 + backendB: + registerHealthIndicator: true + slidingWindowSize: 10 + permittedNumberOfCallsInHalfOpenState: 3 + slidingWindowType: TIME_BASED + recordFailurePredicate: io.github.robwin.exception.RecordFailurePredicate + +resilience4j.timelimiter: + instances: + backendA: + timeoutDuration: 2s + cancelRunningFuture: true + backendB: + timeoutDuration: 1s + cancelRunningFuture: false +``` + +有关 Resilience4j 属性配置的更多信息,请参见[Resilience4J Spring Boot 2 Configuration](https://resilience4j.readme.io/docs/getting-started-3#configuration)。 + +#### [](#bulkhead-pattern-supporting)[1.1.6.舱壁模式支撑](#bulkhead-pattern-supporting) + +如果`resilience4j-bulkhead`在 Classpath 上, Spring Cloud Circuitbreaker 将用 Resilience4J 隔板包装所有方法。你可以通过将`spring.cloud.circuitbreaker.bulkhead.resilience4j.enabled`设置为`false`来禁用 Resilience4J 舱壁。 + +Spring Cloud Circuitbreaker Resilience4J 提供了两种舱壁模式的实现方式: + +* 使用信号量的`SemaphoreBulkhead` + +* 使用有界队列和固定线程池的`FixedThreadPoolBulkhead`。 + +默认情况下, Spring Cloud Circuitbreaker Resilience4j 使用`FixedThreadPoolBulkhead`。有关舱壁模式实现的更多信息,请参见[弹性 4J 舱壁](https://resilience4j.readme.io/docs/bulkhead)。 + +`Customizer`可用于提供默认的`Bulkhead`和`ThreadPoolBulkhead`配置。 + +``` +@Bean +public Customizer defaultBulkheadCustomizer() { + return provider -> provider.configureDefault(id -> new Resilience4jBulkheadConfigurationBuilder() + .bulkheadConfig(BulkheadConfig.custom().maxConcurrentCalls(4).build()) + .threadPoolBulkheadConfig(ThreadPoolBulkheadConfig.custom().coreThreadPoolSize(1).maxThreadPoolSize(1).build()) + .build() +); +} +``` + +#### [](#specific-bulkhead-configuration)[1.1.7.特定舱壁结构](#specific-bulkhead-configuration) + +与证明默认的“bulkhead”或“threadpoolbulkhead”配置类似,你可以创建`Customize` Bean 这传递了一个`Resilience4jBulkheadProvider`。 + +``` +@Bean +public Customizer slowBulkheadProviderCustomizer() { + return provider -> provider.configure(builder -> builder + .bulkheadConfig(BulkheadConfig.custom().maxConcurrentCalls(1).build()) + .threadPoolBulkheadConfig(ThreadPoolBulkheadConfig.ofDefaults()), "slowBulkhead"); +} +``` + +除了配置所创建的舱壁之外,你还可以在舱壁和线程池的舱壁被创建之后但在它们被返回给调用方之前自定义它们。要做到这一点,你可以使用`addBulkheadCustomizer`和`addThreadPoolBulkheadCustomizer`方法。 + +##### [](#bulkhead-example)[舱壁示例](#bulkhead-example) + +``` +@Bean +public Customizer customizer() { + return provider -> provider.addBulkheadCustomizer(bulkhead -> bulkhead.getEventPublisher() + .onCallRejected(slowRejectedConsumer) + .onCallFinished(slowFinishedConsumer), "slowBulkhead"); +} +``` + +##### [](#thread-pool-bulkhead-example)[线程池隔板示例](#thread-pool-bulkhead-example) + +``` +@Bean +public Customizer slowThreadPoolBulkheadCustomizer() { + return provider -> provider.addThreadPoolBulkheadCustomizer(threadPoolBulkhead -> threadPoolBulkhead.getEventPublisher() + .onCallRejected(slowThreadPoolRejectedConsumer) + .onCallFinished(slowThreadPoolFinishedConsumer), "slowThreadPoolBulkhead"); +} +``` + +#### [](#bulkhead-properties-configuration)[1.1.8.舱壁属性配置](#bulkhead-properties-configuration) + +你可以在应用程序的配置属性文件中配置 ThreadPoolBulkhead 和 SemaphoreBulkhead 实例。属性配置比 Java`Customizer`配置具有更高的优先级。 + +``` +resilience4j.thread-pool-bulkhead: + instances: + backendA: + maxThreadPoolSize: 1 + coreThreadPoolSize: 1 +resilience4j.bulkhead: + instances: + backendB: + maxConcurrentCalls: 10 +``` + +有关 Resilience4j 属性配置的更多信息,请参见[Resilience4J Spring Boot 2 Configuration](https://resilience4j.readme.io/docs/getting-started-3#configuration)。 + +#### [](#collecting-metrics)[1.1.9.收集指标](#collecting-metrics) + +Spring 云断路器弹性 4j 包括自动配置以设置度量收集,只要正确的依赖关系是在 Classpath 上。要启用度量集合,必须包括`org.springframework.boot:spring-boot-starter-actuator`和`io.github.resilience4j:resilience4j-micrometer`。有关存在这些依赖关系时产生的度量的更多信息,请参见[复原力 4J 文档](https://resilience4j.readme.io/docs/micrometer)。 + +| |你不必直接包含`micrometer-core`,因为它是由`spring-boot-starter-actuator`引入的| +|---|----------------------------------------------------------------------------------------------------------| + +### [](#configuring-spring-retry-circuit-breakers)[1.2. Configuring Spring Retry Circuit Breakers](#configuring-spring-retry-circuit-breakers) + +Spring Retry 为 Spring 应用程序提供声明性重试支持。该项目的一个子集包括实现断路器功能的能力。 Spring 重试通过它的[“circuitbreakerretrypolicy”](https://github.com/spring-projects/spring-retry/blob/master/src/main/java/org/springframework/retry/policy/CircuitBreakerRetryPolicy.java)和[stateful retry](https://github.com/spring-projects/spring-retry#stateful-retry)的组合提供了一种断路器实现方式。使用 Spring 重试创建的所有断路器都将使用`CircuitBreakerRetryPolicy`和[“违约状态”](https://github.com/spring-projects/spring-retry/blob/master/src/main/java/org/springframework/retry/support/DefaultRetryState.java)创建。这两个类都可以使用`SpringRetryConfigBuilder`进行配置。 + +#### [](#default-configuration-2)[1.2.1.默认配置](#default-configuration-2) + +要为你的所有断路器提供默认配置,请创建一个`Customize` Bean,它传递一个“SpringretryCircuitBreakerFactory”。`configureDefault`方法可用于提供默认配置。 + +``` +@Bean +public Customizer defaultCustomizer() { + return factory -> factory.configureDefault(id -> new SpringRetryConfigBuilder(id) + .retryPolicy(new TimeoutRetryPolicy()).build()); +} +``` + +#### [](#specific-circuit-breaker-configuration-2)[1.2.2.特定断路器配置](#specific-circuit-breaker-configuration-2) + +与提供默认配置类似,你可以创建`Customize` Bean 这传递了一个“SpringRetryCircuitBreakerFactory”。 + +``` +@Bean +public Customizer slowCustomizer() { + return factory -> factory.configure(builder -> builder.retryPolicy(new SimpleRetryPolicy(1)).build(), "slow"); +} +``` + +除了配置被创建的断路器之外,你还可以在断路器被创建之后但在断路器被返回给调用者之前自定义断路器。要做到这一点,你可以使用`addRetryTemplateCustomizers`方法。这对于将事件处理程序添加到`RetryTemplate`非常有用。 + +``` +@Bean +public Customizer slowCustomizer() { + return factory -> factory.addRetryTemplateCustomizers(retryTemplate -> retryTemplate.registerListener(new RetryListener() { + + @Override + public boolean open(RetryContext context, RetryCallback callback) { + return false; + } + + @Override + public void close(RetryContext context, RetryCallback callback, Throwable throwable) { + + } + + @Override + public void onError(RetryContext context, RetryCallback callback, Throwable throwable) { + + } + })); +} +``` + +## [](#building)[2. Building](#building) + +### [](#basic-compile-and-test)[2.1.基本编译和测试](#basic-compile-and-test) + +要构建源代码,你需要安装 JDK17。 + +Spring Cloud 在大多数与构建相关的活动中使用 Maven,你应该能够通过克隆感兴趣的项目并键入来很快地启动它。 + +``` +$ ./mvnw install +``` + +| |你也可以自己安装 Maven(\>=3.3.3),并在下面的示例中运行`mvn`命令
来代替`./mvnw`。如果你这样做,那么如果你的本地 Maven 设置不
包含 Spring 预发布工件的存储库声明,那么你可能还需要添加`-P spring`。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |请注意,你可能需要通过使用
设置一个`MAVEN_OPTS`的环境变量来增加 Maven 可用的
内存量`-Xmx512m -XX:MaxPermSize=128m`这样的值。我们试图在
`.mvn`配置中覆盖此内容,因此,如果你发现必须这样做才能使
构建成功,请举出一张票来将设置添加到
源代码控制中。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +需要中间件(即 Redis)进行测试的项目通常需要安装并运行[Docker]([WWW.docker.com/get-started](https://www.docker.com/get-started))的本地实例。 + +### [](#documentation)[2.2.文件](#documentation) + +Spring-cloud-build 模块有一个“DOCS”配置文件,如果将其打开,将尝试从 `SRC/Main/ASCIIDoc’构建 ASCIIDoc 源。作为该过程的一部分,它将寻找一个“readme.ADOC”,并通过加载所有包含来处理它,但不是解析或呈现它,只是将其复制到`${main.basedir}`(默认为`$/tmp/releaser-1645116950347-0/spring-cloud-circuitbreaker/docs`,即项目的根)。如果 README 中有任何更改,那么在构建 Maven 之后,它将在正确的位置显示为经过修改的文件。只要承诺并推动改变就行了。 + +### [](#working-with-the-code)[2.3.使用代码](#working-with-the-code) + +如果你没有 IDE 偏好,我们建议你在使用代码时使用[Spring Tools Suite](https://www.springsource.com/developer/sts)或[Eclipse](https://eclipse.org)。我们使用[m2eclipse](https://eclipse.org/m2e/)Eclipse 插件来提供 Maven 支持。其他 IDE 和工具也应该在没有问题的情况下工作,只要它们使用 Maven 3.3.3 或更好。 + +#### [](#activate-the-spring-maven-profile)[2.3.1. Activate the Spring Maven profile](#activate-the-spring-maven-profile) + +Spring 云项目需要激活“ Spring” Maven 配置文件,以解析 Spring 里程碑和快照存储库。使用你首选的 IDE 将此配置文件设置为活动的,否则你可能会遇到构建错误。 + +#### [](#importing-into-eclipse-with-m2eclipse)[2.3.2.用 M2Eclipse 导入到 Eclipse 中](#importing-into-eclipse-with-m2eclipse) + +在使用 Eclipse 时,我们推荐[m2eclipse](https://eclipse.org/m2e/)Eclipse 插件。如果你还没有安装 M2Eclipse,它可以从“Eclipse 市场”获得。 + +| |较早版本的 M2E 不支持 Maven 3.3,因此,一旦
项目导入到 Eclipse 中,你还需要告诉
M2Eclipse 为项目使用正确的配置文件。如果你
在项目中看到与 POM 相关的许多不同错误,请检查
是否有最新的安装。如果你不能升级 M2E,
将“ Spring”配置文件添加到你的`settings.xml`。或者,你可以
将存储库设置从父
POM 的“ Spring”配置文件复制到你的`settings.xml`中。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#importing-into-eclipse-without-m2eclipse)[2.3.3.在没有 M2Eclipse 的情况下导入 Eclipse](#importing-into-eclipse-without-m2eclipse) + +如果不喜欢使用 M2Eclipse,可以使用以下命令生成 Eclipse 项目元数据: + +``` +$ ./mvnw eclipse:eclipse +``` + +可以通过从`file`菜单中选择`import existing projects`来导入生成的 Eclipse 项目。 + +## [](#contributing)[3. Contributing](#contributing) +---------- + +Spring Cloud 是在非限制性的 Apache2.0 许可下发布的,并遵循非常标准的 GitHub 开发流程,使用 GitHub Tracker 处理问题并将拉请求合并到 Master 中。如果你想贡献一些微不足道的东西,请不要犹豫,但要遵循下面的指导方针。 + +### [](#sign-the-contributor-license-agreement)[3.1.签署贡献者许可协议](#sign-the-contributor-license-agreement) + +在我们接受一个重要的补丁或拉请求之前,我们需要你签署[贡献者许可协议](https://cla.pivotal.io/sign/spring)。签署贡献者协议并不会授予任何人对主库的提交权限,但这确实意味着我们可以接受你的贡献,并且如果我们接受了,你将获得作者信用。活跃的贡献者可能会被要求加入核心团队,并被赋予合并拉请求的能力。 + +### [](#code-of-conduct)[3.2.行为守则](#code-of-conduct) + +该项目遵守贡献者契约[code of conduct](https://github.com/spring-cloud/spring-cloud-build/blob/master/docs/src/main/asciidoc/code-of-conduct.adoc)。通过参与,你将被期望坚持这一准则。请向[[电子邮件保护]]报告不可接受的行为(/cdn-cgi/l/email-protection#fd8e8d8f94939ad09e929998d0929bd09e92939889e89bd8d948b92899c91d39492)。 + +### [](#code-conventions-and-housekeeping)[3.3.守则惯例和内部管理](#code-conventions-and-housekeeping) + +这些都不是拉请求所必需的,但它们都会有所帮助。它们也可以在原始的拉请求之后但在合并之前添加。 + +* 使用 Spring 框架代码格式约定。如果使用 Eclipse,则可以使用[Spring Cloud Build](https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-dependencies-parent/eclipse-code-formatter.xml)项目中的 `eclipse-code-formatter.xml’文件导入格式化设置。如果使用 IntelliJ,可以使用[Eclipse 代码格式化插件](https://plugins.jetbrains.com/plugin/6546)导入相同的文件。 + +* 确保所有新的`.java`文件都有一个简单的 Javadoc 类注释,其中至少有一个 `@author’标记来标识你,并且最好至少有一个段落来说明这个类的用途。 + +* 将 ASF 许可标头注释添加到所有新的`.java`文件(从项目中的现有文件复制) + +* 将自己作为`@author`添加到要进行实质性修改的.java 文件中(不仅仅是外观上的更改)。 + +* 添加一些 Javadocs,如果你更改了名称空间,还可以添加一些 XSDDOC 元素。 + +* 几个单元测试也会有很大帮助——必须有人去做。 + +* 如果没有其他人正在使用你的分支,请将它重新设置为当前的主分支(或主项目中的其他目标分支)。 + +* 在编写提交消息时,请遵循[这些约定](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html),如果你正在修复现有的问题,请在提交消息的末尾添加`Fixes gh-XXXX`(其中 xxxx 是问题编号)。 + +### [](#checkstyle)[3.4. checkstyle](#checkstyle) + +Spring 云构建附带一组 CheckStyle 规则。你可以在`spring-cloud-build-tools`模块中找到它们。该模块下最值得注意的文件是: + +Spring-云构建工具/ + +``` +└── src +    ├── checkstyle +    │   └── checkstyle-suppressions.xml (3) +    └── main +    └── resources +    ├── checkstyle-header.txt (2) +    └── checkstyle.xml (1) +``` + +|**1**|默认的 checkstyle 规则| +|-----|-------------------------| +|**2**|文件头设置| +|**3**|默认抑制规则| + +#### [](#checkstyle-configuration)[3.4.1.checkstyle 配置](#checkstyle-configuration) + +checkstyle 规则是**默认禁用**。要将 checkstyle 添加到项目中,只需定义以下属性和插件。 + +POM.xml + +``` + +true (1) + true + (2) + true + (3) + + + + + (4) + io.spring.javaformat + spring-javaformat-maven-plugin + + (5) + org.apache.maven.plugins + maven-checkstyle-plugin + + + + + + (5) + org.apache.maven.plugins + maven-checkstyle-plugin + + + + +``` + +|**1**|构建 checkstyle 错误失败| +|-----|--------------------------------------------------------------------------------------------------------------| +|**2**|构建 checkstyle 冲突失败| +|**3**|CheckStyle 还分析了测试源| +|**4**|添加 Spring Java 格式插件,该插件将重新格式化你的代码,以传递大多数 CheckStyle 格式设置规则| +|**5**|将 CheckStyle 插件添加到构建和报告阶段| + +如果你需要抑制一些规则(例如行长需要更长),那么在`${project.root}/src/checkstyle/checkstyle-suppressions.xml`下定义一个文件就足够了。示例: + +projectRoot/SRC/checkstyle/checkstyle-suppresions.xml + +``` + + + + + + +``` + +建议将`${spring-cloud-build.rootFolder}/.editorconfig`和`${spring-cloud-build.rootFolder}/.springformat`复制到你的项目中。这样,将应用一些默认的格式设置规则。你可以通过运行以下脚本来实现此目的: + +``` +$ curl https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/.editorconfig -o .editorconfig +$ touch .springformat +``` + +### [](#ide-setup)[3.5. IDE setup](#ide-setup) + +#### [](#intellij-idea)[3.5.1.Intellij 思想](#intellij-idea) + +为了设置 IntelliJ,你应该导入我们的编码约定、检查配置文件并设置 CheckStyle 插件。以下文件可以在[Spring Cloud Build](https://github.com/spring-cloud/spring-cloud-build/tree/master/spring-cloud-build-tools)项目中找到。 + +Spring-云构建工具/ + +``` +└── src +    ├── checkstyle +    │   └── checkstyle-suppressions.xml (3) +    └── main +    └── resources +    ├── checkstyle-header.txt (2) +    ├── checkstyle.xml (1) +    └── intellij +       ├── Intellij_Project_Defaults.xml (4) +       └── Intellij_Spring_Boot_Java_Conventions.xml (5) +``` + +|**1**|默认的 checkstyle 规则| +|-----|--------------------------------------------------------------------------| +|**2**|文件头设置| +|**3**|默认抑制规则| +|**4**|适用大多数 CheckStyle 规则的 IntelliJ 的项目默认值| +|**5**|适用大多数 CheckStyle 规则的 IntelliJ 的项目风格约定| + +![Code style](https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/docs/src/main/asciidoc/images/intellij-code-style.png) + +图 1。代码样式 + +转到`File``Settings``Editor``Code style`。点击`Scheme`区域旁边的图标。在这里,单击`Import Scheme`值并选择`Intellij IDEA code style XML`选项。导入`spring-cloud-build-tools/src/main/resources/intellij/Intellij_Spring_Boot_Java_Conventions.xml`文件。 + +![Code style](https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/docs/src/main/asciidoc/images/intellij-inspections.png) + +图 2。检查剖面 + +转到`File``Settings``Editor``Inspections`。点击`Profile`区域旁边的图标。在那里,单击`Import Profile`并导入`spring-cloud-build-tools/src/main/resources/intellij/Intellij_Project_Defaults.xml`文件。 + +Checkstyle + +要让 IntelliJ 使用 CheckStyle,你必须安装`Checkstyle`插件。建议还安装`Assertions2Assertj`来自动转换 JUnit 断言 + +![Checkstyle](https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/docs/src/main/asciidoc/images/intellij-checkstyle.png) + +转到`File``Settings``Other settings``Checkstyle`。点击`Configuration file`区域中的`+`图标。在这里,你必须定义应该从哪里选择 CheckStyle 规则。在上面的图片中,我们从克隆的云构建存储库中选择了规则。但是,你可以指向 Spring Cloud Build 的 GitHub 存储库(例如`checkstyle.xml`:`[raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/main/resources/checkstyle.xml](https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/main/resources/checkstyle.xml)`)。我们需要提供以下变量: + +* `checkstyle.header.file`-请将其指向 Spring Cloud Build 的`spring-cloud-build-tools/src/main/resources/checkstyle-header.txt`文件,可以在你的克隆 repo 中,也可以通过`[raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/main/resources/checkstyle-header.txt](https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/main/resources/checkstyle-header.txt)`URL。 + +* `checkstyle.suppressions.file`-默认抑制。请将它指向 Spring Cloud Build 的`spring-cloud-build-tools/src/checkstyle/checkstyle-suppressions.xml`文件,或者在你的克隆 repo 中,或者通过`[raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/checkstyle/checkstyle-suppressions.xml](https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/checkstyle/checkstyle-suppressions.xml)`URL。 + +* `checkstyle.additional.suppressions.file`-此变量对应于本地项目中的抑制。例如,你正在处理`spring-cloud-contract`。然后指向`project-root/src/checkstyle/checkstyle-suppressions.xml`文件夹。`spring-cloud-contract`的例子是:`/home/username/spring-cloud-contract/src/checkstyle/checkstyle-suppressions.xml`。 + +| |记住将`Scan Scope`设置为`All sources`,因为我们为生产和测试源应用了 checkstyle 规则。| +|---|------------------------------------------------------------------------------------------------------------------| + +### [](#duplicate-finder)[3.6.重复查找器](#duplicate-finder) + +Spring 云构建带来了`basepom:duplicate-finder-maven-plugin`,这使得能够在 Java Classpath 上标记重复的和冲突的类和资源。 + +#### [](#duplicate-finder-configuration)[3.6.1.重复查找器配置](#duplicate-finder-configuration) + +重复查找器是**默认启用**,将在 Maven 构建的`verify`阶段运行,但是只有在将`duplicate-finder-maven-plugin`添加到项目的`build`部分`POM.xml`时,它才会在项目中生效。 + +pom.xml + +``` + + + + org.basepom.maven + duplicate-finder-maven-plugin + + + +``` + +对于其他属性,我们设置了[插件文档](https://github.com/basepom/duplicate-finder-maven-plugin/wiki)中列出的默认值。 + +你可以轻松地重写它们,但可以使用`duplicate-finder-maven-plugin`前缀设置所选属性的值。例如,将`duplicate-finder-maven-plugin.skip`设置为`true`,以便在构建中跳过重复检查。 + +如果需要将`ignoredClassPatterns`或`ignoredResourcePatterns`添加到设置中,请确保将它们添加到项目的插件配置部分中: + +``` + + + + org.basepom.maven + duplicate-finder-maven-plugin + + + org.joda.time.base.BaseDateTime + .*module-info + + + changelog.txt + + + + + +``` \ No newline at end of file diff --git a/docs/spring-cloud/spring-cloud-cli.md b/docs/spring-cloud/spring-cloud-cli.md new file mode 100644 index 0000000000000000000000000000000000000000..0321eaf32d21c946ef3599c8e702bf8b882f02b9 --- /dev/null +++ b/docs/spring-cloud/spring-cloud-cli.md @@ -0,0 +1,155 @@ +# Spring 引导云 CLI + + +Spring Boot CLI 为[Spring Boot](https://projects.spring.io/spring-boot)提供了[Spring Cloud](https://github.com/spring-cloud)命令行功能。你可以编写 Groovy 脚本来运行 Spring 云组件应用程序(例如`@EnableEurekaServer`)。你还可以轻松地进行加密和解密等操作,以支持具有秘密配置值的云配置客户机。通过 Launcher CLI,你可以方便地从命令行同时启动像 Eureka、Zipkin、Config Server 这样的服务(在开发时非常有用)。 + +| |Spring 云是在非限制性的 Apache2.0 许可下发布的。如果你想对文档的这一部分做出贡献,或者你发现了一个错误,请在[github](https://github.com/spring-cloud/spring-cloud-cli)上找到项目中的源代码和问题追踪器。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## [Installation](#_installation) + +要安装,请确保你有[Spring Boot CLI](https://github.com/spring-projects/spring-boot)(2.0.0 或更好): + +``` +$ spring version +Spring CLI v2.2.3.RELEASE +``` + +例如,对于 SDKMAN 个用户 + +``` +$ sdk install springboot 2.2.3.RELEASE +$ sdk use springboot 2.2.3.RELEASE +``` + +并安装 Spring 云插件 + +``` +$ mvn install +$ spring install org.springframework.cloud:spring-cloud-cli:2.2.0.RELEASE +``` + +| |**先决条件:**要使用加密和解密功能
,你需要在你的 JVM 中安装全强度 JCE(默认情况下不存在)。
你可以从 Oracle 下载“Java Cryptography Extension(JCE)Unlimited Strength Juridictory Policy Files”
,并遵循安装说明(基本上将 JRElib/security 目录中的 2Policy 文件
替换为你下载的文件)。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## [Running Spring Cloud Services in Development](#_running_spring_cloud_services_in_development) + +启动器 CLI 可用于从命令行运行公共服务,如 Eureka、Config Server 等。要列出你可以执行`spring cloud --list`的可用服务,并仅启动`spring cloud`的默认服务集。要选择要部署的服务,只需在命令行中列出它们,例如。 + +``` +$ spring cloud eureka configserver h2 kafka stubrunner zipkin +``` + +支持的可部署程序摘要: + +| Service | Name | Address |说明| +|------------|----------------|---------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| eureka | Eureka Server | [http://localhost:8761](http://localhost:8761) |服务注册和发现的 Eureka 服务器。默认情况下,所有其他服务都会显示在其目录中。| +|configserver| Config Server | [http://localhost:8888](http://localhost:8888) |Spring 运行在“本机”配置文件中的云配置服务器和来自本地目录的服务配置。/Launcher| +| h2 | H2 Database |[http://localhost:9095](http://localhost:9095) (console), jdbc:h2:tcp://localhost:9096/{data}|关系数据库服务。在连接时使用`{data}`的文件路径(例如`./target/test`)。请记住,你可以添加`;MODE=MYSQL`或`;MODE=POSTGRESQL`以与其他服务器类型的兼容性连接。| +| kafka | Kafka Broker | [http://localhost:9091](http://localhost:9091) (actuator endpoints), localhost:9092 | | +| dataflow |Dataflow Server | [http://localhost:9393](http://localhost:9393) |Spring 带有 ui at/admin-ui 的云数据流服务器。将 DataFlow Shell 连接到根路径上的目标。| +| zipkin | Zipkin Server | [http://localhost:9411](http://localhost:9411) |Zipkin 服务器与 UI 可视化的痕迹。存储在内存中的跨数据,并通过 JSON 数据的 HTTP POST 接受它们。| +| stubrunner |Stub Runner Boot| [http://localhost:8750](http://localhost:8750) |下载 WiRemock 存根,启动 WiRemock,并用存储的存根向启动的服务器提供信息。传递`stubrunner.ids`以传递存根坐标,然后转到`[http://localhost:8750/stubs](http://localhost:8750/stubs)`。| + +可以使用同名的本地 YAML 文件(在当前工作目录或名为“config”的子目录中或在`~/.spring-cloud`中)配置这些应用程序。例如,在`configserver.yml`中,你可能想要执行这样的操作来为后端定位一个本地 Git 存储库: + +configserver.yml + +``` +spring: + profiles: + active: git + cloud: + config: + server: + git: + uri: file://${user.home}/dev/demo/config-repo +``` + +例如,在 Stub Runner 应用程序中,你可以通过以下方式从本地`.m2`获取 stub。 + +Stubrunner.yml + +``` +stubrunner: + workOffline: true + ids: + - com.example:beer-api-producer:+:9876 +``` + +### [添加其他应用程序](#_adding_additional_applications) + +可以将其他应用程序添加到`./config/cloud.yml`(不是 `./config.yml`,因为这将替换缺省值),例如使用 + +config/cloud.yml + +``` +spring: + cloud: + launcher: + deployables: + source: + coordinates: maven://com.example:source:0.0.1-SNAPSHOT + port: 7000 + sink: + coordinates: maven://com.example:sink:0.0.1-SNAPSHOT + port: 7001 +``` + +当你列出应用程序时: + +``` +$ spring cloud --list +source sink configserver dataflow eureka h2 kafka stubrunner zipkin +``` + +(请注意列表开头的附加应用)。 + +## [编写 Groovy 脚本并运行应用程序](#_writing_groovy_scripts_and_running_applications) + +Spring 云 CLI 具有对 Spring 云的大多数声明性功能的支持,例如`@Enable*`类的注释。例如,这里有一个功能齐全的 Eureka 服务器 + +App.Groovy + +``` +@EnableEurekaServer +class Eureka {} +``` + +你可以像这样从命令行运行它 + +``` +$ spring run app.groovy +``` + +要包含额外的依赖关系,通常只需添加适当的支持特性的注释就足够了,例如`@EnableConfigServer`,`@enableOAuth2SSO’或`@EnableEurekaClient`。要手动包含依赖项,你可以使用`@Grab`和特殊的“ Spring 引导”短样式工件坐标,即只使用工件 ID(不需要组或版本信息),例如,设置一个客户端应用程序,以便在 AMQP 上监听来自 Spring 云总线的管理事件: + +App.Groovy + +``` +@Grab('spring-cloud-starter-bus-amqp') +@RestController +class Service { + @RequestMapping('/') + def home() { [message: 'Hello'] } +} +``` + +## [加密和解密](#_encryption_and_decryption) + +Spring cloud cli 带有一个“加密”和一个“解密”命令。两者都接受相同形式的参数,并将键指定为强制性的“--key”,例如。 + +``` +$ spring encrypt mysecret --key foo +682bc583f4641835fa2db009355293665d2647dade3375c0ee201de2a49f7bda +$ spring decrypt --key foo 682bc583f4641835fa2db009355293665d2647dade3375c0ee201de2a49f7bda +mysecret +``` + +要在文件中使用密钥(例如,用于 encyption 的 RSA 公钥),请在键值前加上“@”并提供文件路径。 + +``` +$ spring encrypt mysecret --key @${HOME}/.ssh/id_rsa.pub +AQAjPgt3eFZQXwt8tsHAVv/QHiY5sI2dRcR+... +``` diff --git a/docs/spring-cloud/spring-cloud-cloudfoundry.md b/docs/spring-cloud/spring-cloud-cloudfoundry.md new file mode 100644 index 0000000000000000000000000000000000000000..54c4ffca06c890f092e390d9130dabe4bdbacf53 --- /dev/null +++ b/docs/spring-cloud/spring-cloud-cloudfoundry.md @@ -0,0 +1,58 @@ +# Spring Cloud for Cloud Foundry + + +Spring Cloud for CloudFoundry 使得在[Cloud Foundry](https://github.com/cloudfoundry)(平台即服务)中运行[Spring Cloud](https://github.com/spring-cloud)应用程序变得很容易。Cloud Foundry 有一个“服务”的概念,这是一个可以“绑定”到应用程序的中间软件,本质上为它提供了一个包含凭据的环境变量(例如,用于服务的位置和用户名)。 + +`spring-cloud-cloudfoundry-commons`模块配置了基于反应堆的 Cloud Foundry Java 客户端 V3.0,并且可以独立使用。 + +`spring-cloud-cloudfoundry-web`项目为 Cloud Foundry 中 WebApps 的一些增强功能提供了基本支持:自动绑定到单点登录服务,并可选地为发现启用粘性路由。 + +`spring-cloud-cloudfoundry-discovery`项目提供了 Spring Cloud Commons`DiscoveryClient`的实现,因此你可以 `@enableDiscoveryClient’并提供你的凭据为 ` Spring.cloud.cloudfoundry.Discovery.[username,password]`(如果你没有连接到[Pivotal Web 服务](https://run.pivotal.io),也可以`*.url`),然后你可以直接使用`DiscoveryClient`,或者通过`LoadBalancerClient`。 + +第一次使用 Discovery 客户机时,由于它必须从 Cloud Foundry 获得一个访问令牌,因此它可能会比较慢。 + +## [](#discovery)[1. Discovery](#discovery) + +以下是一款带有 Cloud Foundry Discovery 的 Spring 云应用: + +App.Groovy + +``` +@Grab('org.springframework.cloud:spring-cloud-cloudfoundry') +@RestController +@EnableDiscoveryClient +class Application { + + @Autowired + DiscoveryClient client + + @RequestMapping('/') + String home() { + 'Hello from ' + client.getLocalServiceInstance() + } + +} +``` + +如果你在没有任何服务绑定的情况下运行它: + +``` +$ spring jar app.jar app.groovy +$ cf push -p app.jar +``` + +它将在主页中显示其应用程序名称。 + +`DiscoveryClient`可以根据经过身份验证的凭据列出一个空间中的所有应用程序,该空间默认为客户端运行的空间(如果有的话)。如果既不配置组织也不配置空间,那么在 Cloud Foundry 中,它们默认为用户的配置文件。 + +## [](#single-sign-on)[2.单点登录](#single-sign-on) + +| |在版本 1.3 中,所有的 OAuth2SSO 和资源服务器功能都移动到了 Spring boot
。你可以在[Spring Boot user guide](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/)中找到文档。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +该项目提供了从 CloudFoundry 服务凭据到 Spring 启动特性的自动绑定。例如,如果你有一个名为“SSO”的 CloudFoundry 服务,其凭据中包含“client\_id”、“client\_secret”和“auth\_domain”,那么它将自动绑定到你用 `@enableoAuth2SSO’启用的 Spring OAuth2 客户端(从 Spring 启动)。服务的名称可以使用`spring.oauth2.sso.serviceId`进行参数化。 + +## [](#configuration)[3.配置](#configuration) + +要查看所有 Spring Cloud Foundry 相关配置属性的列表,请检查[附录页](appendix.html)。 + diff --git a/docs/spring-cloud/spring-cloud-commons.md b/docs/spring-cloud/spring-cloud-commons.md new file mode 100644 index 0000000000000000000000000000000000000000..934604f4834f09c66c50612e2b9a74bc40afd24f --- /dev/null +++ b/docs/spring-cloud/spring-cloud-commons.md @@ -0,0 +1,1239 @@ +# 云原生应用程序 + +[Cloud Native](https://pivotal.io/platform-as-a-service/migrating-to-cloud-native-application-architectures-ebook)是一种应用程序开发风格,它鼓励在持续交付和价值驱动的开发领域轻松采用最佳实践。一个相关的规程是构建[12 因素应用程序](https://12factor.net/),在该规程中,开发实践与交付和操作目标保持一致——例如,通过使用声明式编程以及管理和监视。 Spring 云以许多特定的方式促进了这些开发风格。起点是一组特性,分布式系统中的所有组件都需要轻松访问这些特性。 + +这些特性中的许多都包含在[Spring Boot](https://projects.spring.io/spring-boot)中, Spring 云就是建立在这些特性上的。 Spring Cloud 以两个库的形式提供了更多的功能: Spring Cloud Context 和 Spring Cloud Commons。 Spring Cloud Context 为 Spring 云应用程序的`ApplicationContext`提供实用程序和特殊服务(Bootstrap 上下文、加密、刷新范围和环境端点)。 Spring Cloud Commons 是一组抽象和公共类,用于不同的云实现(例如 Spring Cloud Netflix 和 Spring Cloud Consul)。 + +如果由于“非法密钥大小”而导致异常,并且使用了 Sun 的 JDK,则需要安装 JCE 的无限强度管辖权策略文件。有关更多信息,请参见以下链接: + +* [Java 6 JCE](https://www.oracle.com/technetwork/java/javase/downloads/jce-6-download-429243.html) + +* [Java 7 JCE](https://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html) + +* [Java 8 JCE](https://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html) + +将这些文件解压缩到 JDK/JRE/lib/security 文件夹中,用于你使用的 JRE/JDK x64/x86 的任何版本。 + +| |Spring 云是在非限制性的 Apache2.0 许可下发布的。
如果你想对文档的这一部分做出贡献,或者如果你发现了一个错误,那么你可以在{docslink}[github]上找到该项目的源代码和发行追踪器。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## [](#spring-cloud-context-application-context-services)[1. Spring Cloud Context: Application Context Services](#spring-cloud-context-application-context-services) + +Spring 对于如何使用 Spring 构建应用程序,Boot 持有一种固执己见的观点。例如,它具有用于公共配置文件的常规位置,并且具有用于公共管理和监视任务的端点。 Spring 云在此基础上构建,并添加了一些系统中的许多组件将使用或偶尔需要的功能。 + +### [](#the-bootstrap-application-context)[1.1.引导程序应用程序上下文](#the-bootstrap-application-context) + +Spring 云应用程序通过创建“引导”上下文来操作,该上下文是主应用程序的父上下文。这个上下文负责从外部源加载配置属性,并对本地外部配置文件中的属性进行解密。这两个上下文共享一个`Environment`,它是任何 Spring 应用程序的外部属性的源。默认情况下,Bootstrap 属性(不是`bootstrap.properties`,而是在 Bootstrap 阶段加载的属性)是以较高的优先级添加的,因此它们不能被本地配置覆盖。 + +与主应用程序上下文相比,引导程序上下文使用不同的约定来定位外部配置。而不是`application.yml`(或`.properties`),你可以使用`bootstrap.yml`,将引导程序和主上下文的外部配置很好地分开。下面的清单展示了一个示例: + +示例 1.bootstrap.yml + +``` +spring: + application: + name: foo + cloud: + config: + uri: ${SPRING_CONFIG_URI:http://localhost:8888} +``` + +如果你的应用程序需要来自服务器的任何特定于应用程序的配置,那么最好设置`spring.application.name`(在`bootstrap.yml`或`application.yml`中)。要将属性`spring.application.name`用作应用程序的上下文 ID,必须将其设置为`bootstrap.[properties | yml]`。 + +如果要检索特定的配置文件,还应该在`bootstrap.[properties | yml]`中设置`spring.profiles.active`。 + +通过设置`spring.cloud.bootstrap.enabled=false`(例如,在系统属性中),可以完全禁用引导程序进程。 + +### [](#application-context-hierarchies)[1.2.应用程序上下文层次结构](#application-context-hierarchies) + +如果你从`SpringApplication`或`SpringApplicationBuilder`构建应用程序上下文,则引导程序上下文将作为父上下文添加到该上下文。 Spring 的一个特性是,子上下文从其父上下文继承属性源和配置文件,因此,与在没有 Spring 云配置的情况下构建相同的上下文相比,“主”应用程序上下文包含额外的属性源。额外的财产来源是: + +* “bootstrap”:如果在 bootstrap 上下文中找到任何`PropertySourceLocators`,并且它们具有非空属性,则会以高优先级出现一个可选的`CompositePropertySource`。一个例子是来自 Spring Cloud Config 服务器的属性。有关如何自定义此属性源的内容,请参见“[自定义引导程序属性源](#customizing-bootstrap-property-sources)”。 + +* “ApplicationConfig:[ Classpath:bootstrap.yml]”(如果 Spring 配置文件处于活动状态,则包含相关文件):如果你有`bootstrap.yml`(或`.properties`),则这些属性将用于配置 bootstrap 上下文。然后,当设置其父上下文时,它们被添加到子上下文中。它们的优先级低于`application.yml`(或`.properties`)和作为创建 Spring 引导应用程序过程的正常部分添加到子程序的任何其他属性源。有关如何自定义这些属性源的内容,请参见“[更改 BootStrap 属性的位置](#customizing-bootstrap-properties)”。 + +由于属性源的排序规则,“bootstrap”条目优先。然而,请注意,这些不包含来自`bootstrap.yml`的任何数据,这具有很低的优先级,但可以用来设置默认值。 + +你可以通过设置你创建的任何`ApplicationContext`的父上下文来扩展上下文层次结构——例如,通过使用它自己的接口或使用`SpringApplicationBuilder`便利方法(`parent(),`child()`和`sibling()`)。引导程序上下文是你自己创建的最高级祖先的父级。层次结构中的每个上下文都有自己的“Bootstrap”(可能是空的)属性源,以避免无意中将值从父级推广到子级。如果有一个配置服务器,层次结构中的每个上下文(原则上)也可以有不同的`spring.application.name`,因此也可以有不同的远程属性源。 Spring 正常的应用程序上下文行为规则适用于属性解析:来自子上下文的属性通过名称和属性源名覆盖父上下文中的属性。(如果子具有与父具有相同名称的属性源,则来自父的值不包含在子属性中)。 + +请注意,`SpringApplicationBuilder`允许你在整个层次结构中共享`Environment`,但这不是默认的。因此,兄弟上下文(尤其是)不需要具有相同的概要文件或属性源,即使它们可能与父上下文共享公共值。 + +### [](#customizing-bootstrap-properties)[1.3.更改 BootStrap 属性的位置](#customizing-bootstrap-properties) + +`bootstrap.yml`(或`.properties`)位置可以通过设置`spring.cloud.bootstrap.name`(默认:`bootstrap`)、`spring.cloud.bootstrap.location`(默认:空)或`spring.cloud.bootstrap.additional-location`(默认:空)来指定——例如,在系统属性中。 + +这些属性的行为类似于同名的`spring.config.*`变体。用`spring.cloud.bootstrap.location`替换默认位置,只使用指定的位置。要将位置添加到默认位置列表中,可以使用`spring.cloud.bootstrap.additional-location`。实际上,它们是用来设置 Bootstrap`ApplicationContext`的,方法是在其`Environment`中设置这些属性。如果有一个活动配置文件(来自`spring.profiles.active`或通过你正在构建的上下文中的`Environment`API),则该配置文件中的属性也将被加载,这与常规 Spring 启动应用程序中的属性相同——例如,对于`development`配置文件,从`bootstrap-development.properties`开始。 + +### [](#overriding-bootstrap-properties)[1.4.重写远程属性的值](#overriding-bootstrap-properties) + +通过引导程序上下文添加到应用程序中的属性源通常是“远程”的(例如,来自 Spring Cloud Config Server)。默认情况下,不能在本地重写它们。如果你想让你的应用程序用它们自己的系统属性或配置文件重写远程属性,那么远程属性源必须通过设置`spring.cloud.config.allowOverride=true`来授予它权限(在本地设置它是不起作用的)。一旦设置了该标志,两个更细粒度的设置将控制远程属性相对于系统属性和应用程序本地配置的位置: + +* `spring.cloud.config.overrideNone=true`:覆盖任何本地属性源。 + +* `spring.cloud.config.overrideSystemProperties=false`:只有系统属性、命令行参数和环境变量(但不包括本地配置文件)才应覆盖远程设置。 + +### [](#customizing-the-bootstrap-configuration)[1.5.自定义引导程序配置](#customizing-the-bootstrap-configuration) + +通过在名为`org.springframework.cloud.bootstrap.BootstrapConfiguration`的键下向`/META-INF/spring.factories`添加条目,可以将引导程序上下文设置为执行任何你喜欢的操作。这包含一个用逗号分隔的列表 Spring `@Configuration`类,这些类用于创建上下文。可以在这里创建你希望在主应用程序上下文中可用以进行自动连接的任何 bean。有一份`@Beans`型`ApplicationContextInitializer`的特殊合同。如果要控制启动序列,可以使用`@Order`注释标记类(默认顺序为`last`)。 + +| |在添加自定义`BootstrapConfiguration`时,请注意,所添加的类不是`@ComponentScanned`错误地添加到你的“主”应用程序上下文中,
为引导配置类使用一个单独的包名,并确保该名称尚未被`@ComponentScan`或`@SpringBootApplication`注释的配置类覆盖。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +引导程序通过将初始化器注入主`SpringApplication`实例(这是正常的 Spring 启动序列,无论它是作为独立应用程序运行还是部署在应用程序服务器中)而结束。首先,从`spring.factories`中的类创建一个引导程序上下文。然后,所有`@Beans`类型的`ApplicationContextInitializer`在启动之前被添加到主`SpringApplication`中。 + +### [](#customizing-bootstrap-property-sources)[1.6.自定义引导程序属性源](#customizing-bootstrap-property-sources) + +由 BootStrap 进程添加的用于外部配置的默认属性源是 Spring Cloud Config 服务器,但是你可以通过将类型为`PropertySourceLocator`的 bean 添加到 BootStrap 上下文(通过`spring.factories`)来添加其他源。例如,你可以从不同的服务器或数据库插入额外的属性。 + +作为示例,请考虑以下自定义定位器: + +``` +@Configuration +public class CustomPropertySourceLocator implements PropertySourceLocator { + + @Override + public PropertySource locate(Environment environment) { + return new MapPropertySource("customProperty", + Collections.singletonMap("property.from.sample.custom.source", "worked as intended")); + } + +} +``` + +传入的`Environment`是即将被创建的`ApplicationContext`的属性——换句话说,是我们为其提供额外属性源的属性。它已经具有其正常的 Spring 引导提供的属性源,因此你可以使用这些源来定位特定于`Environment`的属性源(例如,通过在`spring.application.name`上键入它,就像在默认的 Spring Cloud Config Server 属性源定位器中所做的那样)。 + +如果你创建了一个包含这个类的 jar,然后添加一个包含以下设置的`META-INF/spring.factories`,则`customProperty``PropertySource`将出现在其 Classpath 上包含该 jar 的任何应用程序中: + +``` +org.springframework.cloud.bootstrap.BootstrapConfiguration=sample.custom.CustomPropertySourceLocator +``` + +### [](#logging-configuration)[1.7.日志配置](#logging-configuration) + +如果你使用 Spring 引导来配置日志设置,那么如果你希望将此配置应用于所有事件,则应将其放置在`bootstrap.[yml | properties]`中。 + +| |对于 Spring Cloud 要正确初始化日志配置,不能使用自定义前缀。例如,在初始化日志系统时,使用不会被 Spring Cloud 识别。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#environment-changes)[1.8.环境变化](#environment-changes) + +应用程序监听`EnvironmentChangeEvent`并以两种标准方式对更改做出反应(额外的`ApplicationListeners`可以以正常方式添加为`@Beans`)。当观察到`EnvironmentChangeEvent`时,它具有已更改的键值列表,应用程序将这些值用于: + +* 在上下文中重新绑定任何`@ConfigurationProperties`bean。 + +* 在`logging.level.*`中为任何属性设置记录器级别。 + +请注意, Spring Cloud Config 客户机在默认情况下不会轮询`Environment`中的更改。通常,我们不建议使用这种方法来检测更改(尽管你可以使用“@schedule”注释对其进行设置)。如果你有一个扩展的客户机应用程序,那么最好向所有实例广播`EnvironmentChangeEvent`,而不是让它们轮询更改(例如,通过使用[Spring Cloud Bus](https://github.com/spring-cloud/spring-cloud-bus))。 + +`EnvironmentChangeEvent`涵盖了一大类刷新用例,只要你可以实际对`Environment`进行更改并发布事件。请注意,这些 API 是公共的,并且是 CORE 的一部分 Spring)。你可以通过访问`/configprops`端点(一种标准的 Spring 引导执行器功能)来验证这些更改是否绑定到`@ConfigurationProperties`bean。例如,`DataSource`可以在运行时更改其`maxPoolSize`(由 Spring 引导创建的默认`DataSource`是`@ConfigurationProperties` Bean)并动态地增加容量。重新绑定`@ConfigurationProperties`并不包括另一大类用例,其中你需要对刷新进行更多控制,并且需要对整个`ApplicationContext`进行原子级更改。为了解决这些问题,我们有`@RefreshScope`。 + +### [](#refresh-scope)[1.9.刷新范围](#refresh-scope) + +当存在配置更改时,标记为`@RefreshScope`的 Spring `@Bean`将得到特殊处理。这个特性解决了有状态 bean 仅在初始化时才注入配置的问题。例如,如果当通过`Environment`更改数据库 URL 时,`DataSource`具有打开的连接,那么你可能希望这些连接的持有者能够完成他们正在做的事情。然后,下一次当某个对象从池中借用一个连接时,它将获得一个带有新 URL 的连接。 + +有时,在某些只能初始化一次的 bean 上应用`@RefreshScope`注释甚至是强制性的。如果 Bean 是“不可变的”,则必须用`@RefreshScope`注释 Bean,或者在属性键下指定类名:`spring.cloud.refresh.extra-refreshable`。 + +| |如果你有一个`DataSource` Bean,这是一个`HikariDataSource`,它不能被
刷新。它是`spring.cloud.refresh.never-refreshable`的默认值。如果需要刷新
不同的`DataSource`实现,请选择该实现。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +Refresh Scope bean 是惰性代理,它在使用它们时(即调用方法时)进行初始化,并且作用域充当初始化值的缓存。要强制 Bean 在下一个方法调用时重新初始化,你必须使其缓存条目无效。 + +`RefreshScope`是上下文中的 Bean,并具有一个公共`refreshAll()`方法,可以通过清除目标缓存来刷新范围内的所有 bean。`/refresh`端点公开此功能(通过 HTTP 或 JMX)。要按名称刷新个人 Bean,还存在`refresh(String)`方法。 + +要公开`/refresh`端点,需要向应用程序添加以下配置: + +``` +management: + endpoints: + web: + exposure: + include: refresh +``` + +| |`@RefreshScope`在`@Configuration`类上工作(技术上),但它可能会导致令人惊讶的行为。
例如,这并不意味着在该类中定义的所有`@Beans`本身都在`@RefreshScope`中。
,具体来说,任何依赖于这些 bean 的东西都不能依赖于在启动刷新时对它们进行更新,除非它本身在`@RefreshScope`中。
在这种情况下,它会在刷新时被重建,并且其依赖项会被重新注入。
在这一点上,它们会从刷新的`@Configuration`中重新初始化。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#encryption-and-decryption)[1.10.加密和解密](#encryption-and-decryption) + +Spring 云具有用于本地解密属性值的`Environment`预处理器。它遵循与 Spring Cloud Config 服务器相同的规则,并且通过`encrypt.*`具有相同的外部配置。因此,你可以使用`{cipher}*`形式的加密值,并且,只要存在有效的密钥,就可以在主应用程序上下文获得`Environment`设置之前对它们进行解密。要在应用程序中使用加密功能,你需要在 Classpath( Maven 坐标:`org.springframework.security:spring-security-rsa`)中包括 Spring 安全 RSA,并且还需要在 JVM 中提供全强度的 JCE 扩展。 + +如果由于“非法密钥大小”而导致异常,并且使用了 Sun 的 JDK,则需要安装 JCE 的无限强度管辖权策略文件。有关更多信息,请参见以下链接: + +* [Java 6 JCE](https://www.oracle.com/technetwork/java/javase/downloads/jce-6-download-429243.html) + +* [Java 7 JCE](https://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html) + +* [Java 8 JCE](https://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html) + +将这些文件解压缩到 JDK/JRE/lib/security 文件夹中,用于你使用的 JRE/JDK x64/x86 的任何版本。 + +### [](#endpoints)[1.11. Endpoints](#endpoints) + +对于 Spring 引导执行器应用程序,一些额外的管理端点是可用的。你可以使用: + +* `POST`到`/actuator/env`以更新`Environment`并重新绑定`@ConfigurationProperties`和日志级别。要启用此端点,你必须设置`management.endpoint.env.post.enabled=true`。 + +* `/actuator/refresh`重新加载引导表带上下文并刷新`@RefreshScope`bean。 + +* `/actuator/restart`关闭`ApplicationContext`并重新启动它(默认禁用)。 + +* `/actuator/pause`和`/actuator/resume`用于在`ApplicationContext`上调用`Lifecycle`方法(`stop()’和`start()`)。 + +| |如果禁用`/actuator/restart`端点,那么`/actuator/pause`和`/actuator/resume`端点
也将被禁用,因为它们只是`/actuator/restart`的特殊情况。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## [](#spring-cloud-commons-common-abstractions)[2. Spring Cloud Commons: Common Abstractions](#spring-cloud-commons-common-abstractions) + +诸如服务发现、负载平衡和断路器之类的模式将自身扩展到一个公共抽象层,该抽象层可以被所有 Spring 云客户机使用,而不依赖于实现(例如,使用 Eureka 或 Consul 的发现)。 + +### [](#discovery-client)[2.1. The `@EnableDiscoveryClient` Annotation](#discovery-client) + +Spring Cloud Commons 提供了`@EnableDiscoveryClient`注释。这将查找带有`META-INF/spring.factories`和`ReactiveDiscoveryClient`接口的`META-INF/spring.factories`的实现。发现客户端的实现在`org.springframework.cloud.client.discovery.EnableDiscoveryClient`键下向`spring.factories`添加配置类。`DiscoveryClient`实现的示例包括[Spring Cloud Netflix Eureka](https://cloud.spring.io/spring-cloud-netflix/)、[Spring Cloud Consul Discovery](https://cloud.spring.io/spring-cloud-consul/)和[Spring Cloud Zookeeper Discovery](https://cloud.spring.io/spring-cloud-zookeeper/)。 + +Spring 默认情况下,云将提供阻塞和反应式服务发现客户端。通过设置`spring.cloud.discovery.blocking.enabled=false`或`spring.cloud.discovery.reactive.enabled=false`,你可以轻松地禁用阻塞和/或反应客户端。要完全禁用服务发现,只需设置`spring.cloud.discovery.enabled=false`。 + +默认情况下,`DiscoveryClient`的实现方式是用远程发现服务器自动注册本地 Spring 引导服务器。可以通过在`@EnableDiscoveryClient`中设置`autoRegister=false`来禁用此行为。 + +| |`@EnableDiscoveryClient`不再需要。
你可以在 Classpath 上放置一个`DiscoveryClient`实现,以使 Spring 引导应用程序向服务发现服务器注册。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#health-indicators)[2.1.1.健康指标](#health-indicators) + +Commons 自动配置以下 Spring 引导健康指示器。 + +##### [](#discoveryclienthealthindicator)[发现潜在的指示剂](#discoveryclienthealthindicator) + +此健康指标是基于当前注册的`DiscoveryClient`实现的。 + +* 要完全禁用,请设置`spring.cloud.discovery.client.health-indicator.enabled=false`。 + +* 要禁用 description 字段,请设置`spring.cloud.discovery.client.health-indicator.include-description=false`。否则,它可以像卷起的`description`的`HealthIndicator`那样冒泡。 + +* 要禁用服务检索,请设置`spring.cloud.discovery.client.health-indicator.use-services-query=false`。默认情况下,指示器调用客户机的`getServices`方法。在使用许多注册服务的部署中,在每次检查期间检索所有服务的成本可能太高。这将跳过服务检索,而是使用客户机的`probe`方法。 + +##### [](#discoverycompositehealthcontributor)[发现复合健康贡献者](#discoverycompositehealthcontributor) + +这个复合健康指示器基于所有注册的`DiscoveryHealthIndicator`bean。要禁用,请设置`spring.cloud.discovery.client.composite-indicator.enabled=false`。 + +#### [](#ordering-discoveryclient-instances)[2.1.2. Ordering `DiscoveryClient` instances](#ordering-discoveryclient-instances) + +`DiscoveryClient`接口扩展`Ordered`。这在使用多个发现客户机时非常有用,因为它允许你定义返回的发现客户机的顺序,类似于你如何订购由 Spring 应用程序加载的 bean。默认情况下,任意`DiscoveryClient`的顺序设置为 `0’。如果你想为你的自定义`DiscoveryClient`实现设置不同的顺序,你只需要覆盖`getOrder()`方法,以便它返回适合你的设置的值。除此之外,你还可以使用属性来设置由 Spring Cloud 提供的`DiscoveryClient`实现的顺序,其中包括`ConsulDiscoveryClient`、`EurekaDiscoveryClient`和 `zookeeperDiscoveryclient’。为了做到这一点,你只需要将 ` Spring.cloud.{clientifier}.discovery.order`(或`eureka.client.order`for Eureka)属性设置为所需的值。 + +#### [](#simplediscoveryclient)[2.1.3.简单的发现](#simplediscoveryclient) + +如果在 Classpath 中没有支持服务注册中心的`DiscoveryClient`,则将使用`SimpleDiscoveryClient`实例,该实例使用属性来获取有关服务和实例的信息。 + +有关可用实例的信息应通过以下格式的属性传递到:` Spring.cloud.discovery.client.simple.instants.service1[0].uri=http://s11:8080`,其中 ` Spring.cloud.discovery.client.simple.instants` 是常见的前缀,那么`service1`表示所讨论的服务的 ID,而`[0]`表示实例的索引号(在示例中可见,索引以`0`开始),然后`uri`的值是实例可用的实际 URI。 + +### [](#serviceregistry)[2.2.ServiceRegistry](#serviceregistry) + +Commons 现在提供了一个`ServiceRegistry`接口,该接口提供了`register(Registration)`和`deregister(Registration)`等方法,这些方法允许你提供自定义注册服务。`registration’是一个标记接口。 + +下面的示例显示了正在使用的`ServiceRegistry`: + +``` +@Configuration +@EnableDiscoveryClient(autoRegister=false) +public class MyConfiguration { + private ServiceRegistry registry; + + public MyConfiguration(ServiceRegistry registry) { + this.registry = registry; + } + + // called through some external process, such as an event or a custom actuator endpoint + public void register() { + Registration registration = constructRegistration(); + this.registry.register(registration); + } +} +``` + +每个`ServiceRegistry`实现都有自己的`Registry`实现。 + +* `ZookeeperRegistration`与`ZookeeperServiceRegistry`连用 + +* `EurekaRegistration`与`EurekaServiceRegistry`连用 + +* `ConsulRegistration`与`ConsulServiceRegistry`连用 + +如果你正在使用`ServiceRegistry`接口,那么你将需要为所使用的`Registry`实现传递正确的`Registry`实现。 + +#### [](#serviceregistry-auto-registration)[2.2.1.ServiceRegistry 自动注册](#serviceregistry-auto-registration) + +默认情况下,`ServiceRegistry`实现自动注册正在运行的服务。要禁用该行为,可以将:\*`@EnableDiscoveryClient(autoRegister=false)`设置为永久禁用自动注册。\*`spring.cloud.service-registry.auto-registration.enabled=false`通过配置禁用行为。 + +##### [](#serviceregistry-auto-registration-events)[ServiceRegistry 自动注册事件](#serviceregistry-auto-registration-events) + +当服务自动注册时,将触发两个事件。第一个事件称为“InstancePreRegistereRedevent”,在服务注册之前被触发。第二个事件称为`InstanceRegisteredEvent`,在服务注册后触发。你可以注册一个“ApplicationListener”来监听这些事件并对其做出反应。 + +| |如果`spring.cloud.service-registry.auto-registration.enabled`属性设置为`false`,则不会触发这些事件。| +|---|---------------------------------------------------------------------------------------------------------------------------| + +#### [](#service-registry-actuator-endpoint)[2.2.2.服务注册中心执行器端点](#service-registry-actuator-endpoint) + +Spring Cloud Commons 提供了`/service-registry`执行器端点。这个端点依赖于 Spring 应用程序上下文中的`Registration` Bean。用 get 调用`/service-registry`返回`Registration`的状态。使用 POST 到带有 JSON 主体的相同端点将当前`Registration`的状态更改为新值。JSON 主体必须包含带有首选值的`status`字段。请参阅`ServiceRegistry`实现的文档,用于更新状态时允许的值和为状态返回的值。例如,Eureka 支持的状态是`UP`、`DOWN`、`OUT_OF_SERVICE`和`UNKNOWN`。 + +### [](#rest-template-loadbalancer-client)[2.3. Spring RestTemplate as a Load Balancer Client](#rest-template-loadbalancer-client) + +你可以将`RestTemplate`配置为使用负载平衡器客户端。要创建负载平衡的`RestTemplate`,请创建`RestTemplate``@Bean`,并使用`@LoadBalanced`限定符,如下例所示: + +``` +@Configuration +public class MyConfiguration { + + @LoadBalanced + @Bean + RestTemplate restTemplate() { + return new RestTemplate(); + } +} + +public class MyClass { + @Autowired + private RestTemplate restTemplate; + + public String doOtherStuff() { + String results = restTemplate.getForObject("http://stores/stores", String.class); + return results; + } +} +``` + +| |不再通过自动配置创建`RestTemplate` Bean。
必须由各个应用程序创建它。| +|---|------------------------------------------------------------------------------------------------------------------| + +URI 需要使用虚拟主机名(即服务名,而不是主机名)。BlockingLoadBalancerClient 用于创建完整的物理地址。 + +| |要使用负载平衡的`RestTemplate`,你需要在 Classpath 中有一个负载平衡器实现。
将[Spring Cloud LoadBalancer starter](#spring-cloud-loadbalancer-starter)添加到你的项目中才能使用它。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#webclinet-loadbalancer-client)[2.4. Spring WebClient as a Load Balancer Client](#webclinet-loadbalancer-client) + +你可以将`WebClient`配置为自动使用负载均衡器客户端。要创建负载平衡的`WebClient`,请创建`WebClient.Builder``@Bean`,并使用`@LoadBalanced`限定符,如下所示: + +``` +@Configuration +public class MyConfiguration { + + @Bean + @LoadBalanced + public WebClient.Builder loadBalancedWebClientBuilder() { + return WebClient.builder(); + } +} + +public class MyClass { + @Autowired + private WebClient.Builder webClientBuilder; + + public Mono doOtherStuff() { + return webClientBuilder.build().get().uri("http://stores/stores") + .retrieve().bodyToMono(String.class); + } +} +``` + +URI 需要使用虚拟主机名(即服务名,而不是主机名)。 Spring 云负载平衡器用于创建完整的物理地址。 + +| |如果要使用`@LoadBalanced WebClient.Builder`,则需要在 Classpath 中有一个负载均衡器
实现。我们建议你将[Spring Cloud LoadBalancer starter](#spring-cloud-loadbalancer-starter)添加到你的项目中。
然后,下面使用`ReactiveLoadBalancer`。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#retrying-failed-requests)[2.4.1.重试失败的请求](#retrying-failed-requests) + +可以将负载平衡的`RestTemplate`配置为重试失败的请求。默认情况下,此逻辑是禁用的。对于非反应性版本(带有`RestTemplate`),你可以通过在应用程序的 Classpath 中添加[Spring Retry](https://github.com/spring-projects/spring-retry)来启用它。对于反应式版本(带有`WebTestClient), you need to set ` Spring.cloud.loadBalancer.retry.enabled=true`)。 + +如果希望在 Classpath 上使用 Spring 重试或反应式重试禁用重试逻辑,则可以设置`spring.cloud.loadbalancer.retry.enabled=false`。 + +对于非反应性实现,如果你希望在重试中实现`BackOffPolicy`,则需要创建类型`LoadBalancedRetryFactory`的 Bean 并覆盖`createBackOffPolicy()`方法。 + +对于反应式实现,你只需要通过将`spring.cloud.loadbalancer.retry.backoff.enabled`设置为`false`来启用它。 + +你可以设置: + +* `spring.cloud.loadbalancer.retry.maxRetriesOnSameServiceInstance`-表示在同一个`ServiceInstance`上应该重试请求多少次(对每个选定的实例单独计算) + +* `spring.cloud.loadbalancer.retry.maxRetriesOnNextServiceInstance`-表示新选择的`ServiceInstance`请求应该重试多少次 + +* `spring.cloud.loadbalancer.retry.retryableStatusCodes`-总是要重试失败请求的状态代码。 + +对于反应式实现,你可以另外设置: +- `spring.cloud.loadbalancer.retry.backoff.minBackoff`-设置最小退避持续时间(默认情况下为 5 毫秒) +- `spring.cloud.loadbalancer.retry.backoff.maxBackoff`-设置最大退避持续时间(默认情况下,最大长值为毫秒) +- `spring.cloud.loadbalancer.retry.backoff.jitter`-设置用于计算的抖动 g 每个调用的实际退避持续时间(默认情况下为 0.5)。 + +对于反应式实现,你还可以实现你自己的`LoadBalancerRetryPolicy`,以便对负载平衡的调用重试进行更详细的控制。 + +| |单独的 loadbalancer 客户机可以单独配置,具有与上面相同的属性,但前缀是`spring.cloud.loadbalancer.clients..*`,其中`clientId`是 loadbalancer 的名称。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |对于负载平衡的重试,默认情况下,我们将`ServiceInstanceListSupplier` Bean 换成`RetryAwareServiceInstanceListSupplier`,以便从先前选择的实例中选择不同的实例(如果可用的话)。可以通过将`spring.cloud.loadbalancer.retry.avoidPreviousInstance`的值设置为`false`来禁用此行为。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +``` +@Configuration +public class MyConfiguration { + @Bean + LoadBalancedRetryFactory retryFactory() { + return new LoadBalancedRetryFactory() { + @Override + public BackOffPolicy createBackOffPolicy(String service) { + return new ExponentialBackOffPolicy(); + } + }; + } +} +``` + +如果希望向重试功能中添加一个或多个`RetryListener`实现,则需要创建类型为`LoadBalancedRetryListenerFactory`的 Bean,并返回你希望用于给定服务的`RetryListener`数组,如下例所示: + +``` +@Configuration +public class MyConfiguration { + @Bean + LoadBalancedRetryListenerFactory retryListenerFactory() { + return new LoadBalancedRetryListenerFactory() { + @Override + public RetryListener[] createRetryListeners(String service) { + return new RetryListener[]{new RetryListener() { + @Override + public boolean open(RetryContext context, RetryCallback callback) { + //TODO Do you business... + return true; + } + + @Override + public void close(RetryContext context, RetryCallback callback, Throwable throwable) { + //TODO Do you business... + } + + @Override + public void onError(RetryContext context, RetryCallback callback, Throwable throwable) { + //TODO Do you business... + } + }}; + } + }; + } +} +``` + +### [](#multiple-resttemplate-objects)[2.5. Multiple `RestTemplate` Objects](#multiple-resttemplate-objects) + +如果你想要一个不是负载平衡的`RestTemplate`,请创建一个`RestTemplate` Bean 并注入它。要访问负载平衡的`RestTemplate`,在创建`@Bean`时使用`@LoadBalanced`限定符,如下例所示: + +``` +@Configuration +public class MyConfiguration { + + @LoadBalanced + @Bean + RestTemplate loadBalanced() { + return new RestTemplate(); + } + + @Primary + @Bean + RestTemplate restTemplate() { + return new RestTemplate(); + } +} + +public class MyClass { +@Autowired +private RestTemplate restTemplate; + + @Autowired + @LoadBalanced + private RestTemplate loadBalanced; + + public String doOtherStuff() { + return loadBalanced.getForObject("http://stores/stores", String.class); + } + + public String doStuff() { + return restTemplate.getForObject("http://example.com", String.class); + } +} +``` + +| |注意在前面的示例中,在普通`RestTemplate`声明中使用`@Primary`注释来消除不合格的`@Autowired`注入的歧义。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |如果你看到诸如`java.lang.IllegalArgumentException: Can not set org.springframework.web.client.RestTemplate field com.my.app.Foo.restTemplate to com.sun.proxy.$Proxy89`之类的错误,请尝试注入`RestOperations`或设置`spring.aop.proxyTargetClass=true`。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#multiple-webclient-objects)[2.6.多个 WebClient 对象](#multiple-webclient-objects) + +如果你想要一个不是负载平衡的`WebClient`,请创建一个`WebClient` Bean 并注入它。要访问负载平衡的`WebClient`,在创建`@Bean`时使用`@LoadBalanced`限定符,如下例所示: + +``` +@Configuration +public class MyConfiguration { + + @LoadBalanced + @Bean + WebClient.Builder loadBalanced() { + return WebClient.builder(); + } + + @Primary + @Bean + WebClient.Builder webClient() { + return WebClient.builder(); + } +} + +public class MyClass { + @Autowired + private WebClient.Builder webClientBuilder; + + @Autowired + @LoadBalanced + private WebClient.Builder loadBalanced; + + public Mono doOtherStuff() { + return loadBalanced.build().get().uri("http://stores/stores") + .retrieve().bodyToMono(String.class); + } + + public Mono doStuff() { + return webClientBuilder.build().get().uri("http://example.com") + .retrieve().bodyToMono(String.class); + } +} +``` + +### [](#loadbalanced-webclient)[2.7. Spring WebFlux `WebClient` as a Load Balancer Client](#loadbalanced-webclient) + +Spring WebFlux 可以同时处理反应性和非反应性`WebClient`配置,正如主题所描述的那样: + +* [Spring WebFlux `WebClient` with `ReactorLoadBalancerExchangeFilterFunction`](#webflux-with-reactive-loadbalancer) + +* [[负载平衡器-交换-过滤器-功能负载平衡器-交换-过滤器-功能]](# 负载平衡器-交换-过滤器-功能负载平衡器-交换-过滤器-功能) + +#### [](#webflux-with-reactive-loadbalancer)[2.7.1. Spring WebFlux `WebClient` with `ReactorLoadBalancerExchangeFilterFunction`](#webflux-with-reactive-loadbalancer) + +你可以将`WebClient`配置为使用`ReactiveLoadBalancer`。如果将[Spring Cloud LoadBalancer starter](#spring-cloud-loadbalancer-starter)添加到项目中,并且`spring-webflux`位于 Classpath 上,则`ReactorLoadBalancerExchangeFilterFunction`将自动配置。下面的示例展示了如何配置`WebClient`以使用无功负载均衡器: + +``` +public class MyClass { + @Autowired + private ReactorLoadBalancerExchangeFilterFunction lbFunction; + + public Mono doOtherStuff() { + return WebClient.builder().baseUrl("http://stores") + .filter(lbFunction) + .build() + .get() + .uri("/stores") + .retrieve() + .bodyToMono(String.class); + } +} +``` + +URI 需要使用虚拟主机名(即服务名,而不是主机名)。`ReactorLoadBalancer`用于创建完整的物理地址。 + +#### [](#load-balancer-exchange-filter-function)[2.7.2. Spring WebFlux `WebClient` with a Non-reactive Load Balancer Client](#load-balancer-exchange-filter-function) + +如果`spring-webflux`在 Classpath 上,则`LoadBalancerExchangeFilterFunction`是自动配置的。然而,请注意,这使用了一个无反应的客户端。下面的示例展示了如何配置`WebClient`以使用负载均衡器: + +``` +public class MyClass { + @Autowired + private LoadBalancerExchangeFilterFunction lbFunction; + + public Mono doOtherStuff() { + return WebClient.builder().baseUrl("http://stores") + .filter(lbFunction) + .build() + .get() + .uri("/stores") + .retrieve() + .bodyToMono(String.class); + } +} +``` + +URI 需要使用虚拟主机名(即服务名,而不是主机名)。`LoadBalancerClient`用于创建完整的物理地址。 + +警告:这种方法现在已经过时了。我们建议你使用[带无功负载均衡器的 WebFlux](#webflux-with-reactive-loadbalancer)代替。 + +### [](#ignore-network-interfaces)[2.8.忽略网络接口](#ignore-network-interfaces) + +有时,忽略某些已命名的网络接口是有用的,这样它们就可以被排除在服务发现注册之外(例如,在 Docker 容器中运行时)。可以设置一个正则表达式列表,以忽略所需的网络接口。以下配置忽略`docker0`接口和所有以`veth`开头的接口: + +示例 2.application.yml + +``` +spring: + cloud: + inetutils: + ignoredInterfaces: + - docker0 + - veth.* +``` + +还可以通过使用正则表达式列表强制只使用指定的网络地址,如下例所示: + +示例 3.bootstrap.yml + +``` +spring: + cloud: + inetutils: + preferredNetworks: + - 192.168 + - 10.0 +``` + +你还可以强制只使用站点本地地址,如下例所示: + +示例 4.application.yml + +``` +spring: + cloud: + inetutils: + useOnlySiteLocalInterfaces: true +``` + +有关什么是站点本地地址的更多详细信息,请参见[iNet4address.html.issitelocaladdress()](https://docs.oracle.com/javase/8/docs/api/java/net/Inet4Address.html#isSiteLocalAddress--)。 + +### [](#http-clients)[2.9.HTTP 客户端工厂](#http-clients) + +Spring Cloud Commons 提供了用于创建 Apache HTTP 客户端和 OK HTTP 客户端的 bean。只有当确定的 HTTP jar 在 Classpath 上时,才会创建`OkHttpClientFactory` Bean。此外, Spring Cloud Commons 提供了用于创建两个客户端使用的连接管理器的 bean:用于 Apache HTTP 客户端的和用于 OK HTTP 客户端的。如果你想定制如何在下游项目中创建 HTTP 客户机,那么你可以提供你自己的这些 bean 的实现。此外,如果你提供了类型`HttpClientBuilder`或`OkHttpClient.Builder`的 Bean,则默认工厂将这些构建器用作将构建器返回到下游项目的基础。还可以通过将`spring.cloud.httpclientfactories.apache.enabled`或`spring.cloud.httpclientfactories.ok.enabled`设置为`false`来禁用这些 bean 的创建。 + +### [](#enabled-features)[2.10.已启用的功能](#enabled-features) + +Spring Cloud Commons 提供了`/features`执行器端点。这个端点返回 Classpath 上可用的功能以及它们是否被启用。返回的信息包括功能类型、名称、版本和供应商。 + +#### [](#feature-types)[2.10.1.特征类型](#feature-types) + +有两种类型的“特征”:抽象的和命名的。 + +抽象特性是定义了接口或抽象类并创建了实现的特性,例如`DiscoveryClient`、`LoadBalancerClient`或`LockService`。抽象类或接口用于在上下文中查找该类型的 Bean。显示的版本是`bean.getClass().getPackage().getImplementationVersion()`。 + +命名特性是指不具有它们实现的特定类的特性。这些功能包括“断路器”、“API 网关”、“ Spring 云总线”等。这些特征需要一个名称和 Bean 类型。 + +#### [](#declaring-features)[2.10.2.声明功能](#declaring-features) + +任何模块都可以声明任意数量的`HasFeature`bean,如下例所示: + +``` +@Bean +public HasFeatures commonsFeatures() { + return HasFeatures.abstractFeatures(DiscoveryClient.class, LoadBalancerClient.class); +} + +@Bean +public HasFeatures consulFeatures() { + return HasFeatures.namedFeatures( + new NamedFeature("Spring Cloud Bus", ConsulBusAutoConfiguration.class), + new NamedFeature("Circuit Breaker", HystrixCommandAspect.class)); +} + +@Bean +HasFeatures localFeatures() { + return HasFeatures.builder() + .abstractFeature(Something.class) + .namedFeature(new NamedFeature("Some Other Feature", Someother.class)) + .abstractFeature(Somethingelse.class) + .build(); +} +``` + +这些 bean 中的每一个都应该有适当的保护`@Configuration`。 + +### [](#spring-cloud-compatibility-verification)[2.11. Spring Cloud Compatibility Verification](#spring-cloud-compatibility-verification) + +由于一些用户在设置 Spring 云应用程序时存在问题,我们决定添加一个兼容性验证机制。如果你当前的设置与 Spring 云需求不兼容,那么它将会中断,同时还会出现一份报告,显示出到底出了什么问题。 + +目前,我们正在验证将哪个版本的 Spring 启动添加到你的 Classpath 中。 + +一份报告的例子 + +``` +*************************** +APPLICATION FAILED TO START +*************************** + +Description: + +Your project setup is incompatible with our requirements due to following reasons: + +- Spring Boot [2.1.0.RELEASE] is not compatible with this Spring Cloud release train + +Action: + +Consider applying the following actions: + +- Change Spring Boot version to one of the following versions [1.2.x, 1.3.x] . +You can find the latest Spring Boot versions here [https://spring.io/projects/spring-boot#learn]. +If you want to learn more about the Spring Cloud Release train compatibility, you can visit this page [https://spring.io/projects/spring-cloud#overview] and check the [Release Trains] section. +``` + +为了禁用此功能,请将`spring.cloud.compatibility-verifier.enabled`设置为`false`。如果你想要重写兼容的 Spring 启动版本,只需用逗号分隔的兼容 Spring 启动版本列表设置“ Spring.cloud.compatibility-verifier.compatible-boot-versions”属性。 + +## [](#spring-cloud-loadbalancer)[3. Spring Cloud LoadBalancer](#spring-cloud-loadbalancer) + +Spring 云提供了其自己的客户端负载均衡器的抽象和实现。对于负载平衡机制,增加了`ReactiveLoadBalancer`接口,并为其提供了**基于循环**和**随机**实现方式。为了得到要从反应中选择的实例`ServiceInstanceListSupplier`是使用的。目前,我们支持基于服务发现的`ServiceInstanceListSupplier`实现,该实现使用 Classpath 中可用的[发现客户端](#discovery-client)从服务发现中检索可用实例。 + +| |通过将`spring.cloud.loadbalancer.enabled`的值设置为`false`,可以禁用 Spring Cloud LoadBalancer。| +|---|---------------------------------------------------------------------------------------------------------------------------| + +### [](#switching-between-the-load-balancing-algorithms)[3.1.在负载平衡算法之间切换](#switching-between-the-load-balancing-algorithms) + +默认情况下使用的`ReactiveLoadBalancer`实现是`RoundRobinLoadBalancer`。要切换到不同的实现,对于选定的服务或所有服务,可以使用[自定义负载平衡器配置机制](#custom-loadbalancer-configuration)。 + +例如,可以通过`@LoadBalancerClient`注释传递以下配置,以切换到使用`RandomLoadBalancer`: + +``` +public class CustomLoadBalancerConfiguration { + + @Bean + ReactorLoadBalancer randomLoadBalancer(Environment environment, + LoadBalancerClientFactory loadBalancerClientFactory) { + String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME); + return new RandomLoadBalancer(loadBalancerClientFactory + .getLazyProvider(name, ServiceInstanceListSupplier.class), + name); + } +} +``` + +| |作为`@LoadBalancerClient`或`@LoadBalancerClients`配置参数传递的类不应使用`@Configuration`进行注释,也不应在组件扫描范围之外。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#spring-cloud-loadbalancer-integrations)[3.2. Spring Cloud LoadBalancer integrations](#spring-cloud-loadbalancer-integrations) + +Spring 为了使云负载平衡器易于使用,我们提供了`ReactorLoadBalancerExchangeFilterFunction`可以与`WebClient`和`BlockingLoadBalancerClient`一起使用的`RestTemplate`。你可以在以下部分中看到更多信息和使用示例: + +* [Spring RestTemplate as a Load Balancer Client](#rest-template-loadbalancer-client) + +* [Spring WebClient as a Load Balancer Client](#webclinet-loadbalancer-client) + +* [Spring WebFlux WebClient with `ReactorLoadBalancerExchangeFilterFunction`](#webflux-with-reactive-loadbalancer) + +### [](#loadbalancer-caching)[3.3. Spring Cloud LoadBalancer Caching](#loadbalancer-caching) + +除了基本的`ServiceInstanceListSupplier`实现外,我们还提供了两种缓存实现,这种实现在每次必须选择实例时都通过`DiscoveryClient`检索实例。 + +#### [](#caffeine-backed-loadbalancer-cache-implementation)[3.3.1. ](#caffeine-backed-loadbalancer-cache-implementation)[Caffeine](https://github.com/ben-manes/caffeine)-支持负载平衡器缓存实现 + +如果在 Classpath 中有,则将使用基于咖啡因的实现方式。有关如何配置它的信息,请参见[负荷平衡状态](#loadbalancer-cache-configuration)部分。 + +如果你正在使用咖啡因,还可以通过在`spring.cloud.loadbalancer.cache.caffeine.spec`属性中传递你自己的[咖啡因规格](https://static.javadoc.io/com.github.ben-manes.caffeine/caffeine/2.2.2/com/github/benmanes/caffeine/cache/CaffeineSpec.html)来覆盖负载平衡器的默认咖啡因缓存设置。 + +警告:传递你自己的咖啡因规范将覆盖任何其他 loadBalancerCache 设置,包括[通用负载平衡器缓存配置](#loadbalancer-cache-configuration)字段,例如`ttl`和`capacity`。 + +#### [](#default-loadbalancer-cache-implementation)[3.3.2.默认的 LoadBalancer 缓存实现](#default-loadbalancer-cache-implementation) + +如果在 Classpath 中没有咖啡因,则将使用`DefaultLoadBalancerCache`,它自动带有`spring-cloud-starter-loadbalancer`。有关如何配置它的信息,请参见[负荷平衡状态](#loadbalancer-cache-configuration)部分。 + +| |要使用咖啡因而不是默认的缓存,请在 Classpath 中添加`com.github.ben-manes.caffeine:caffeine`依赖项。| +|---|-----------------------------------------------------------------------------------------------------------------------| + +#### [](#loadbalancer-cache-configuration)[3.3.3.LoadBalancer 缓存配置](#loadbalancer-cache-configuration) + +你可以将你自己的`ttl`值(写完之后条目应该过期的时间)表示为`Duration`,方法是传递一个与`String`兼容的[Spring Boot `String` to `Duration` converter syntax](https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config-conversion-duration).作为`spring.cloud.loadbalancer.cache.ttl`属性的值。还可以通过设置`spring.cloud.loadbalancer.cache.capacity`属性的值来设置自己的 LoadBalancer 缓存初始容量。 + +默认设置包括将`ttl`设置为 35 秒,而默认的`initialCapacity`是`256`。 + +通过将`spring.cloud.loadbalancer.cache.enabled`的值设置为`false`,你也可以完全禁用 LoadBalancer 缓存。 + +| |尽管基本的、非缓存的实现对于原型设计和测试很有用,但它的效率比缓存版本低得多,因此我们建议在生产中始终使用缓存版本。如果缓存已经由`DiscoveryClient`实现完成,例如`EurekaDiscoveryClient`,则应禁用负载平衡器缓存以防止双重缓存。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#zone-based-load-balancing)[3.4.基于区域的负载平衡](#zone-based-load-balancing) + +为了启用基于区域的负载平衡,我们提供了`ZonePreferenceServiceInstanceListSupplier`。我们使用`DiscoveryClient`-特定的`zone`配置(例如,`eureka.instance.metadata-map.zone`)来选择客户机试图为其筛选可用服务实例的区域。 + +| |你还可以通过设置`spring.cloud.loadbalancer.zone`属性的值来覆盖`DiscoveryClient`特定的区域设置。| +|---|------------------------------------------------------------------------------------------------------------------------------| + +| |目前,只有 Eureka 发现客户机被检测来设置 loadBalancer 区域。对于其他发现客户端,设置`spring.cloud.loadbalancer.zone`属性。不久还会有更多的测试。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |为了确定检索到的`ServiceInstance`的区域,我们在其元数据映射中检查`"zone"`键下的值。| +|---|----------------------------------------------------------------------------------------------------------------------| + +`ZonePreferenceServiceInstanceListSupplier`过滤检索到的实例,只返回同一区域内的实例。如果区域是`null`,或者同一区域内没有实例,则返回所有检索到的实例。 + +为了使用基于区域的负载平衡方法,你必须在[自定义配置](#custom-loadbalancer-configuration)中实例化`ZonePreferenceServiceInstanceListSupplier` Bean。 + +我们使用委托来处理`ServiceInstanceListSupplier`bean。我们建议在`ZonePreferenceServiceInstanceListSupplier`的构造函数中传递一个`DiscoveryClientServiceInstanceListSupplier`委托,然后用`CachingServiceInstanceListSupplier`包装后者,以利用[负载平衡器缓存机制](#loadbalancer-caching)。 + +你可以使用这个示例配置来设置它: + +``` +public class CustomLoadBalancerConfiguration { + + @Bean + public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier( + ConfigurableApplicationContext context) { + return ServiceInstanceListSupplier.builder() + .withDiscoveryClient() + .withZonePreference() + .withCaching() + .build(context); + } +} +``` + +### [](#instance-health-check-for-loadbalancer)[3.5.LoadBalancer 的实例健康检查](#instance-health-check-for-loadbalancer) + +可以为 loadBalancer 启用计划的 HealthCheck。为此提供了`HealthCheckServiceInstanceListSupplier`。它会定期验证委托“ServiceInstanceListSupplier”提供的实例是否还活着,并且只返回健康的实例,除非没有-然后返回所有检索到的实例。 + +| |这种机制在使用`SimpleDiscoveryClient`时特别有用。对于由实际服务注册中心支持的
客户机,没有必要使用它,因为在查询外部服务发现之后,我们已经获得了
健康的实例。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |对于每个服务
具有少量实例的设置,也建议使用此供应商,以避免在失败的实例上重试调用。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------| + +| |如果使用任何服务发现支持的供应商,通常不需要添加此健康检查机制,因为我们直接从服务注册中心检索实例的健康状态
。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |`HealthCheckServiceInstanceListSupplier`依赖于由委托通量提供的更新实例。在极少数情况下,当你希望使用不刷新实例的委托时,即使实例列表可能会更改(例如我们提供的`DiscoveryClientServiceInstanceListSupplier`),你可以将`spring.cloud.loadbalancer.health-check.refetch-instances`设置为`true`,以便通过`HealthCheckServiceInstanceListSupplier`刷新实例列表。然后,你还可以通过修改`spring.cloud.loadbalancer.health-check.refetch-instances-interval`的值来调整刷新间隔,并 OPT 通过将`spring.cloud.loadbalancer.health-check.repeat-health-check`设置为`false`来禁用额外的 HealthCheck 重复,因为每个实例 refetch
也将触发 HealthCheck。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +`HealthCheckServiceInstanceListSupplier`使用带有 ` Spring.cloud.loadBalancer.health-check` 前缀的属性。你可以为调度程序设置`initialDelay`和`interval`。你可以通过设置`spring.cloud.loadbalancer.health-check.path.default`属性的值来设置 HealthCheck URL 的默认路径。还可以通过设置`spring.cloud.loadbalancer.health-check.path.[SERVICE_ID]`属性的值,用服务的正确 ID 替换`[SERVICE_ID]`,为任何给定的服务设置特定值。如果没有指定`[SERVICE_ID]`,则默认使用`/actuator/health`。如果`[SERVICE_ID]`被设置为`null`或作为一个值为空,那么将不执行健康检查。还可以通过设置`spring.cloud.loadbalancer.health-check.port`的值来为健康检查请求设置自定义端口。如果没有设置,则请求的服务在服务实例中可用的端口。 + +| |如果你依赖默认路径,请确保将`spring-boot-starter-actuator`添加到合作者的依赖项中,除非你打算自己添加这样的端点。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +为了使用健康检查计划程序方法,你必须在[自定义配置](#custom-loadbalancer-configuration)中实例化`HealthCheckServiceInstanceListSupplier` Bean。 + +我们使用委托来处理`ServiceInstanceListSupplier`bean。我们建议在`HealthCheckServiceInstanceListSupplier`的构造函数中传递一个`DiscoveryClientServiceInstanceListSupplier`委托。 + +你可以使用这个示例配置来设置它: + +``` +public class CustomLoadBalancerConfiguration { + + @Bean + public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier( + ConfigurableApplicationContext context) { + return ServiceInstanceListSupplier.builder() + .withDiscoveryClient() + .withHealthChecks() + .build(context); + } + } +``` + +| |对于非反应性堆栈,使用`withBlockingHealthChecks()`创建此供应商。
你还可以传递你自己的`WebClient`或`RestTemplate`实例用于检查。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |`HealthCheckServiceInstanceListSupplier`有自己的基于反应器流量`replay()`的缓存机制。因此,如果正在使用它,你可能希望跳过用`CachingServiceInstanceListSupplier`包装该供应商。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#same-instance-preference-for-loadbalancer)[3.6.LoadBalancer 的相同实例首选项](#same-instance-preference-for-loadbalancer) + +你可以以这样一种方式设置 loadBalancer,即它更喜欢先前选择的实例(如果该实例可用的话)。 + +为此,你需要使用`SameInstancePreferenceServiceInstanceListSupplier`。你可以通过将`spring.cloud.loadbalancer.configurations`的值设置为`same-instance-preference`,或者通过提供你自己的`ServiceInstanceListSupplier` Bean 来配置它——例如: + +``` +public class CustomLoadBalancerConfiguration { + + @Bean + public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier( + ConfigurableApplicationContext context) { + return ServiceInstanceListSupplier.builder() + .withDiscoveryClient() + .withSameInstancePreference() + .build(context); + } + } +``` + +| |这也是 ZooKeeper`StickyRule`的替代品。| +|---|------------------------------------------------------| + +### [](#request-based-sticky-session-for-loadbalancer)[3.7.LoadBalancer 中基于请求的粘性会话](#request-based-sticky-session-for-loadbalancer) + +你可以以这样一种方式设置 loadBalancer,即它更喜欢请求 cookie 中提供的带有`instanceId`的实例。如果请求是通过`ClientRequestContext`或`ServerHttpRequestContext`传递给负载平衡器的,我们目前支持这一点,这是 SC 负载平衡器交换过滤器函数和过滤器所使用的。 + +为此,你需要使用`RequestBasedStickySessionServiceInstanceListSupplier`。你可以通过将`spring.cloud.loadbalancer.configurations`的值设置为`request-based-sticky-session`,或者通过提供你自己的`ServiceInstanceListSupplier` Bean 来配置它——例如: + +``` +public class CustomLoadBalancerConfiguration { + + @Bean + public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier( + ConfigurableApplicationContext context) { + return ServiceInstanceListSupplier.builder() + .withDiscoveryClient() + .withRequestBasedStickySession() + .build(context); + } + } +``` + +对于该功能,在向前发送请求之前,有必要更新所选的服务实例(如果该实例不可用,它可能与原始请求 cookie 中的服务实例不同)。为此,将`spring.cloud.loadbalancer.sticky-session.add-service-instance-cookie`的值设置为`true`。 + +默认情况下,cookie 的名称是`sc-lb-instance-id`。你可以通过更改`spring.cloud.loadbalancer.instance-id-cookie-name`属性的值来修改它。 + +| |此功能目前支持 WebClient 支持的负载平衡。| +|---|------------------------------------------------------------------------| + +### [](#spring-cloud-loadbalancer-hints)[3.8. Spring Cloud LoadBalancer Hints](#spring-cloud-loadbalancer-hints) + +Spring Cloud LoadBalancer 允许你设置在`Request`对象内传递给 LoadBalancer 的`String`提示,这些提示以后可以在`ReactiveLoadBalancer`实现中使用,这些实现可以处理它们。 + +通过设置`spring.cloud.loadbalancer.hint.default`属性的值,可以为所有服务设置默认提示。还可以通过设置`spring.cloud.loadbalancer.hint.[SERVICE_ID]`属性的值,用服务的正确 ID 替换`[SERVICE_ID]`,为任何给定的服务设置特定值。如果提示不是由用户设置的,则使用`default`。 + +### [](#hints-based-loadbalancing)[3.9.基于提示的负载平衡](#hints-based-loadbalancing) + +我们还提供了`HintBasedServiceInstanceListSupplier`,这是用于基于提示的实例选择的`ServiceInstanceListSupplier`实现。 + +`HintBasedServiceInstanceListSupplier`检查提示请求标头(默认标头名称是`X-SC-LB-Hint`,但你可以通过更改`spring.cloud.loadbalancer.hint-header-name`属性的值来修改它),如果它发现了提示请求标头,则使用标头中传递的提示值来过滤服务实例。 + +如果没有添加任何提示头,`HintBasedServiceInstanceListSupplier`将使用[来自属性的提示值](#spring-cloud-loadbalancer-hints)来过滤服务实例。 + +如果没有设置任何提示,则返回委托提供的所有服务实例,不管是通过头还是通过属性。 + +在筛选过程中,`HintBasedServiceInstanceListSupplier`查找在`hint`键下具有匹配值集的服务实例。如果没有找到匹配的实例,则返回委托提供的所有实例。 + +你可以使用以下示例配置来设置它: + +``` +public class CustomLoadBalancerConfiguration { + + @Bean + public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier( + ConfigurableApplicationContext context) { + return ServiceInstanceListSupplier.builder() + .withDiscoveryClient() + .withHints() + .withCaching() + .build(context); + } +} +``` + +### [](#transform-the-load-balanced-http-request)[3.10.转换负载平衡的 HTTP 请求](#transform-the-load-balanced-http-request) + +你可以使用所选的`ServiceInstance`来转换负载平衡的 HTTP 请求。 + +对于`RestTemplate`,你需要实现和定义`LoadBalancerRequestTransformer`如下: + +``` +@Bean +public LoadBalancerRequestTransformer transformer() { + return new LoadBalancerRequestTransformer() { + @Override + public HttpRequest transformRequest(HttpRequest request, ServiceInstance instance) { + return new HttpRequestWrapper(request) { + @Override + public HttpHeaders getHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.putAll(super.getHeaders()); + headers.add("X-InstanceId", instance.getInstanceId()); + return headers; + } + }; + } + }; +} +``` + +对于`WebClient`,你需要实现和定义`LoadBalancerClientRequestTransformer`如下: + +``` +@Bean +public LoadBalancerClientRequestTransformer transformer() { + return new LoadBalancerClientRequestTransformer() { + @Override + public ClientRequest transformRequest(ClientRequest request, ServiceInstance instance) { + return ClientRequest.from(request) + .header("X-InstanceId", instance.getInstanceId()) + .build(); + } + }; +} +``` + +如果定义了多个转换器,那么它们将按照定义 bean 的顺序应用。或者,你可以使用`LoadBalancerRequestTransformer.DEFAULT_ORDER`或`LoadBalancerClientRequestTransformer.DEFAULT_ORDER`来指定顺序。 + +### [](#spring-cloud-loadbalancer-starter)[3.11. Spring Cloud LoadBalancer Starter](#spring-cloud-loadbalancer-starter) + +我们还提供了一个启动器,允许你在 Spring 启动应用程序中轻松添加 Spring Cloud LoadBalancer。为了使用它,只需在构建文件中的 Spring 云依赖项中添加`org.springframework.cloud:spring-cloud-starter-loadbalancer`。 + +| |Spring 云负载平衡器启动器包括[Spring Boot Caching](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-caching.html)和[Evictor](https://github.com/stoyanr/Evictor)。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#custom-loadbalancer-configuration)[3.12. Passing Your Own Spring Cloud LoadBalancer Configuration](#custom-loadbalancer-configuration) + +你还可以使用`@LoadBalancerClient`注释来传递你自己的负载平衡器客户端配置,传递负载平衡器客户端和配置类的名称,如下所示: + +``` +@Configuration +@LoadBalancerClient(value = "stores", configuration = CustomLoadBalancerConfiguration.class) +public class MyConfiguration { + + @Bean + @LoadBalanced + public WebClient.Builder loadBalancedWebClientBuilder() { + return WebClient.builder(); + } +} +``` + +TIP + +为了使在自己的 loadBalancer 配置上的工作更容易,我们在`ServiceInstanceListSupplier`类中添加了一个`builder()`方法。 + +TIP + +你还可以将`spring.cloud.loadbalancer.configurations`属性的值设置为`zone-preference`,从而在缓存中使用`ZonePreferenceServiceInstanceListSupplier`,或者在缓存中使用`health-check`,从而替代默认的预定义配置。 + +你可以使用此功能实例化`ServiceInstanceListSupplier`或`ReactorLoadBalancer`的不同实现,这些实现可以是你编写的,也可以是我们作为替代方案提供的(例如`ZonePreferenceServiceInstanceListSupplier`),以覆盖默认设置。 + +你可以看到一个自定义配置[here](#zoned-based-custom-loadbalancer-configuration)的示例。 + +| |注释`value`参数(在上面的示例中为 `stores’)指定了我们应该通过给定的自定义配置向其发送请求的服务 ID。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +还可以通过`@LoadBalancerClients`注释传递多个配置(用于多个负载均衡器客户机),如下例所示: + +``` +@Configuration +@LoadBalancerClients({@LoadBalancerClient(value = "stores", configuration = StoresLoadBalancerClientConfiguration.class), @LoadBalancerClient(value = "customers", configuration = CustomersLoadBalancerClientConfiguration.class)}) +public class MyConfiguration { + + @Bean + @LoadBalanced + public WebClient.Builder loadBalancedWebClientBuilder() { + return WebClient.builder(); + } +} +``` + +| |作为`@LoadBalancerClient`或`@LoadBalancerClients`配置参数传递的类不应使用`@Configuration`进行注释,也不应在组件扫描范围之外。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#loadbalancer-lifecycle)[3.13. Spring Cloud LoadBalancer Lifecycle](#loadbalancer-lifecycle) + +Bean 中使用[自定义负载平衡器配置](#custom-loadbalancer-configuration)进行注册可能是有用的一种类型是`LoadBalancerLifecycle`。 + +`LoadBalancerLifecycle`bean 提供了名为`onStart(Request request)`、`onStartRequest(Request request, Response lbResponse)`和`onComplete(CompletionContext completionContext)`的回调方法,你应该实现这些方法来指定在负载平衡之前和之后应该进行哪些操作。 + +`onStart(Request request)`将`Request`对象作为参数。它包含用于选择适当实例的数据,包括下游客户机请求和[hint](#spring-cloud-loadbalancer-hints)。`onStartRequest`也接受`Request`对象,另外,`Response`对象作为参数。另一方面,将`CompletionContext`对象提供给`onComplete(CompletionContext completionContext)`方法。它包含 loadBalancer`Response`,包括所选择的服务实例、针对该服务实例执行的请求的`Status`和(如果可用的话)返回到下游客户端的响应,以及(如果发生了异常)相应的`Throwable`。 + +`supports(Class requestContextClass, Class responseClass, Class serverTypeClass)`方法可用于确定所讨论的处理器是否处理所提供类型的对象。如果未被用户重写,则返回`true`。 + +| |在前面的方法调用中,`RC`表示`RequestContext`类型,`RES`表示客户端响应类型,`T`表示返回的服务器类型。| +|---|--------------------------------------------------------------------------------------------------------------------------------------| + +### [](#loadbalancer-micrometer-stats-lifecycle)[3.14. Spring Cloud LoadBalancer Statistics](#loadbalancer-micrometer-stats-lifecycle) + +我们提供了一个名为`LoadBalancerLifecycle` Bean 的`MicrometerStatsLoadBalancerLifecycle`,它使用 Micrometer 为负载平衡调用提供统计信息。 + +为了将此 Bean 添加到你的应用程序上下文中,将`spring.cloud.loadbalancer.stats.micrometer.enabled`的值设置为`true`,并使`MeterRegistry`可用(例如,将[Spring Boot Actuator](https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html)添加到你的项目中)。 + +`MicrometerStatsLoadBalancerLifecycle`在`MeterRegistry`中记录以下仪表: + +* `loadbalancer.requests.active`:允许你监视任何服务实例当前活动请求的数量(通过标记可获得的服务实例数据)的度量标准; + +* `loadbalancer.requests.success`:一个计时器,用于度量以将响应传递给基础客户机而结束的任何负载平衡请求的执行时间; + +* `loadbalancer.requests.failed`:一个计时器,用于度量任何负载平衡请求的执行时间,这些请求以异常结束; + +* `loadbalancer.requests.discard`:一种计数器,用于测量被丢弃的负载平衡请求的数量,即负载平衡器尚未检索到要在其上运行请求的服务实例的请求。 + +有关服务实例、请求数据和响应数据的附加信息将随时通过标记添加到度量中。 + +| |对于某些实现方式,例如`BlockingLoadBalancerClient`,请求和响应数据可能不可用,因为我们从参数建立泛型类型,并且可能无法确定类型和读取数据。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |当一个给定的计价器至少增加了一条记录时,计价器在注册表中进行注册。| +|---|----------------------------------------------------------------------------------------------| + +| |你可以通过[adding `MeterFilters`](https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready-metrics-per-meter-properties)进一步配置这些指标的行为(例如,添加[发布百分位和直方图](https://micrometer.io/docs/concepts#_histograms_and_percentiles))。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#configuring-individual-loadbalancerclients)[3.15.配置单个 loadbalancerclient](#configuring-individual-loadbalancerclients) + +单独的 loadBalancer 客户机可以单独配置不同的前缀`spring.cloud.loadbalancer.clients..`**where `clientId` is the name of the loadbalancer. Default configuration values may be set in the `spring.cloud.loadbalancer.`**名称空间,并将与客户机特定值优先合并。 + +示例 5.application.yml + +``` +spring: + cloud: + loadbalancer: + health-check: + initial-delay: 1s + clients: + myclient: + health-check: + interval: 30s +``` + +上面的示例将产生一个合并的健康检查`@ConfigurationProperties`对象,其中`initial-delay=1s`和`interval=30s`。 + +除了以下全局属性外,每个客户机配置属性对大多数属性都有效: + +* `spring.cloud.loadbalancer.enabled`-全局启用或禁用负载平衡 + +* `spring.cloud.loadbalancer.retry.enabled`-全局启用或禁用负载平衡重试。如果全局启用它,仍然可以使用`client`-前缀属性禁用特定客户机的重试,但不能使用相反的方法。 + +* `spring.cloud.loadbalancer.cache.enabled`-全局启用或禁用 LoadBalancer 缓存。如果你全局启用它,你仍然可以通过创建[自定义配置](#custom-loadbalancer-configuration)来禁用特定客户机的缓存,该命令不包括`CachingServiceInstanceListSupplier`在`ServiceInstanceListSupplier`委托层次结构中的`CachingServiceInstanceListSupplier`,但不是相反。 + +* `spring.cloud.loadbalancer.stats.micrometer.enabled`-全局启用或禁用负载平衡器千分尺指标 + +| |对于已经使用的映射的属性,可以在不使用`clients`关键字(例如,`hints`,`health-check.path`)的情况下为每个客户机指定不同的值,我们保留了这种行为,以使库向后兼容。它将在下一个主要版本中进行修改。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## [](#spring-cloud-circuit-breaker)[4. Spring Cloud Circuit Breaker](#spring-cloud-circuit-breaker) + +### [](#introduction)[4.1.导言](#introduction) + +Spring 云断路器提供了跨越不同断路器实现方式的抽象。它提供了在应用程序中使用的一致的 API,允许你(开发人员)选择最适合你的应用程序需求的断路器实现。 + +#### [](#supported-implementations)[4.1.1.支持的实现](#supported-implementations) + +Spring 云支持以下断路器实现方式: + +* [Resilience4J](https://github.com/resilience4j/resilience4j) + +* [Sentinel](https://github.com/alibaba/Sentinel) + +* [Spring Retry](https://github.com/spring-projects/spring-retry) + +### [](#core-concepts)[4.2.核心概念](#core-concepts) + +要在代码中创建断路器,可以使用`CircuitBreakerFactory`API。当你在 Classpath 上包含 Spring 云断路器启动器时,将自动为你创建实现此 API 的 Bean。下面的示例展示了如何使用此 API 的一个简单示例: + +``` +@Service +public static class DemoControllerService { + private RestTemplate rest; + private CircuitBreakerFactory cbFactory; + + public DemoControllerService(RestTemplate rest, CircuitBreakerFactory cbFactory) { + this.rest = rest; + this.cbFactory = cbFactory; + } + + public String slow() { + return cbFactory.create("slow").run(() -> rest.getForObject("/slow", String.class), throwable -> "fallback"); + } + +} +``` + +`CircuitBreakerFactory.create`API 创建了一个名为`CircuitBreaker`的类的实例。`run`方法接受`Supplier`和`Function`。`Supplier`是要在断路器中封装的代码。`Function`是当断路器跳闸时运行的回退。传递函数`Throwable`,从而触发回退。如果你不想提供备份,则可以选择排除备份。 + +#### [](#circuit-breakers-in-reactive-code)[4.2.1.无功码中的断路器](#circuit-breakers-in-reactive-code) + +如果 Project Reactor 在类路径上,你也可以使用`ReactiveCircuitBreakerFactory`作为你的反应代码。下面的示例展示了如何做到这一点: + +``` +@Service +public static class DemoControllerService { + private ReactiveCircuitBreakerFactory cbFactory; + private WebClient webClient; + + public DemoControllerService(WebClient webClient, ReactiveCircuitBreakerFactory cbFactory) { + this.webClient = webClient; + this.cbFactory = cbFactory; + } + + public Mono slow() { + return webClient.get().uri("/slow").retrieve().bodyToMono(String.class).transform( + it -> cbFactory.create("slow").run(it, throwable -> return Mono.just("fallback"))); + } +} +``` + +`ReactiveCircuitBreakerFactory.create`API 创建了一个名为`ReactiveCircuitBreaker`的类的实例。`run`方法获取`Mono`或`Flux`并将其封装在断路器中。你可以选择配置一个回退`Function`,如果断路器跳闸并通过导致故障的`Throwable`,将调用该回退。 + +### [](#configuration)[4.3.配置](#configuration) + +你可以通过创建类型`Customizer`的 bean 来配置断路器。`Customizer`接口有一个用于自定义`Object`的方法(称为`customize`)。 + +有关如何定制给定实现的详细信息,请参见以下文档: + +* [Resilience4J](../../../../spring-cloud-circuitbreaker/current/reference/html/spring-cloud-circuitbreaker.html#configuring-resilience4j-circuit-breakers) + +* [Sentinel](https://github.com/alibaba/spring-cloud-alibaba/blob/master/spring-cloud-alibaba-docs/src/main/asciidoc/circuitbreaker-sentinel.adoc#circuit-breaker-spring-cloud-circuit-breaker-with-sentinel—​configuring-sentinel-circuit-breakers) + +* [Spring Retry](../../../../../spring-cloud-circuitbreaker/docs/current/reference/html/spring-cloud-circuitbreaker.html#configuring-spring-retry-circuit-breakers) + +一些`CircuitBreaker`实现方式如`Resilience4JCircuitBreaker`每次调用`customize`方法都调用`CircuitBreaker#run`。这可能是低效的。在这种情况下,可以使用`CircuitBreaker#once`方法。在多次调用`customize`没有意义的情况下,例如在[消费弹性 4J 的事件](https://resilience4j.readme.io/docs/circuitbreaker#section-consume-emitted-circuitbreakerevents)的情况下,它是有用的。 + +下面的示例显示了每个`io.github.resilience4j.circuitbreaker.CircuitBreaker`消耗事件的方式。 + +``` +Customizer.once(circuitBreaker -> { + circuitBreaker.getEventPublisher() + .onStateTransition(event -> log.info("{}: {}", event.getCircuitBreakerName(), event.getStateTransition())); +}, CircuitBreaker::getName) +``` + +## [](#cachedrandompropertysource)[5.cachedrandomPropertySource](#cachedrandompropertysource) + +Spring 云上下文提供了一个`PropertySource`,该`PropertySource`基于键缓存随机值。在缓存功能之外,它的工作原理与 Spring boot 的[“随机价值 PropertySource”](https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/env/RandomValuePropertySource.java)相同。如果你想要一个即使在 Spring 应用程序上下文重新启动之后仍然保持一致的随机值,那么这个随机值可能是有用的。属性值采取`cachedrandom.[yourkey].[type]`的形式,其中`yourkey`是缓存中的键。`type`值可以是 Spring boot 的`RandomValuePropertySource`所支持的任何类型。 + +``` +myrandom=${cachedrandom.appname.value} +``` + +## [](#spring-cloud-security)[6. Security](#spring-cloud-security) + +### [](#spring-cloud-security-single-sign-on)[6.1.单点登录](#spring-cloud-security-single-sign-on) + +| |在版本 1.3 中,所有的 OAuth2SSO 和资源服务器功能都移动到了 Spring boot
。你可以在[Spring Boot user guide](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/)中找到文档。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#spring-cloud-security-client-token-relay)[6.1.1.客户端令牌中继](#spring-cloud-security-client-token-relay) + +如果你的应用程序是一个面向 OAuth2 客户端的用户(即声明了 `@enableOAuth2SSO` 或`@EnableOAuth2Client`),那么它在 Spring 启动时的请求范围内有一个 `OAuth2ClientContext’。你可以从这个上下文创建自己的`OAuth2RestTemplate`和一个自动连线`OAuth2ProtectedResourceDetails`,然后上下文将始终向下游转发访问令牌,如果过期,还将自动刷新访问令牌。(这些是 Spring 安全性和 Spring 引导的功能。 + +#### [](#spring-cloud-security-resource-server-token-relay)[6.1.2.资源服务器令牌中继](#spring-cloud-security-resource-server-token-relay) + +如果你的应用程序有`@EnableResourceServer`,那么你可能希望将传入的令牌向下游中继到其他服务。如果你使用“RESTTemplate”来联系下游服务,那么这只是一个如何在正确的上下文中创建模板的问题。 + +如果你的服务使用`UserInfoTokenServices`来验证传入的令牌(即它正在使用`security.oauth2.user-info-uri`配置),那么你可以简单地使用自动连线`OAuth2RestTemplate`创建`OAuth2RestTemplate`(它将在到达后端代码之前由身份验证过程填充)。相当于(使用 Spring Boot1.4),你可以在配置中注入一个“userinforesttemplateFactory”并获取它的。例如: + +MyConfiguration.java + +``` +@Bean +public OAuth2RestTemplate restTemplate(UserInfoRestTemplateFactory factory) { + return factory.getUserInfoRestTemplate(); +} +``` + +然后,这个 REST 模板将具有与身份验证筛选器使用的相同的`OAuth2ClientContext`(请求作用域),因此你可以使用它发送具有相同访问令牌的请求。 + +如果你的应用程序没有使用`UserInfoTokenServices`,但仍然是一个客户端(即它声明`@EnableOAuth2Client`或`@EnableOAuth2Sso`),那么使用 Spring 安全云,用户从`@Autowired``OAuth2Context`创建的任何`OAuth2RestOperations`也将转发令牌。默认情况下,此功能是作为 MVC 处理程序拦截器实现的,因此它仅在 Spring MVC 中工作。如果你不使用 MVC,你可以使用自定义过滤器或 AOP 拦截器包装“AccessTokenContextRelay”来提供相同的功能。 + +下面是一个基本示例,展示了在其他地方创建的自动连线 REST 模板的使用情况(“foo.com”是一个资源服务器,接受与周围应用程序相同的令牌): + +mycontroller.java + +``` +@Autowired +private OAuth2RestOperations restTemplate; + +@RequestMapping("/relay") +public String relay() { + ResponseEntity response = + restTemplate.getForEntity("https://foo.com/bar", String.class); + return "Success! (" + response.getBody() + ")"; +} +``` + +如果你不想转发令牌(这是一个有效的选择,因为你可能希望充当你自己的角色,而不是向你发送令牌的客户机),那么你只需要创建自己的“OAuth2Context”,而不是自动布线默认的。 + +如果可用,Feign 客户机还将获取一个使用“OAuth2ClientContext”的拦截器,因此他们还应该在`RestTemplate`可以进行令牌中继的任何地方进行令牌中继。 + +## [](#configuration-properties)[7.配置属性](#configuration-properties) + +要查看所有 Spring Cloud Commons 相关配置属性的列表,请检查[附录页](appendix.html)。 + diff --git a/docs/spring-cloud/spring-cloud-config.md b/docs/spring-cloud/spring-cloud-config.md new file mode 100644 index 0000000000000000000000000000000000000000..e207084741ecfb8cfb61b0cf3a144b73e08414d1 --- /dev/null +++ b/docs/spring-cloud/spring-cloud-config.md @@ -0,0 +1,3012 @@ +# Spring 云配置 + +Spring 云配置为分布式系统中的外部化配置提供服务器端和客户端支持。有了 Config 服务器,你就有了一个中心位置来管理跨所有环境的应用程序的外部属性。客户机和服务器上的概念都映射到 Spring `Environment`和`PropertySource`的抽象,因此它们非常适合 Spring 应用程序,但可以用于运行在任何语言中的任何应用程序。当应用程序通过部署管道从开发到测试再到生产时,你可以管理这些环境之间的配置,并确保应用程序在迁移时拥有运行它们所需的一切。服务器存储后端的默认实现使用 Git,因此它很容易支持配置环境的标记版本,并且可以访问用于管理内容的各种工具。添加替代实现并将其插入 Spring 配置中是很容易的。 + +## [Quick Start](#_quick_start) + +这个快速启动同时使用了 Spring Cloud Config 服务器的服务器和客户端。 + +首先,启动服务器,如下所示: + +``` +$ cd spring-cloud-config-server +$ ../mvnw spring-boot:run +``` + +服务器是一个 Spring 引导应用程序,因此如果你愿意,可以从 IDE 运行它(主类是`ConfigServerApplication`)。 + +下一步测试一个客户机,如下所示: + +``` +$ curl localhost:8888/foo/development +{ + "name": "foo", + "profiles": [ + "development" + ] + .... + "propertySources": [ + { + "name": "https://github.com/spring-cloud-samples/config-repo/foo-development.properties", + "source": { + "bar": "spam", + "foo": "from foo development" + } + }, + { + "name": "https://github.com/spring-cloud-samples/config-repo/foo.properties", + "source": { + "foo": "from foo props", + "democonfigclient.message": "hello spring io" + } + }, + .... +``` + +定位属性源的默认策略是克隆一个 Git 存储库(at`spring.cloud.config.server.git.uri`),并使用它初始化一个 mini`SpringApplication`。迷你应用程序的`Environment`用于枚举属性源并在 JSON 端点上发布它们。 + +HTTP 服务具有以下形式的资源: + +``` +/{application}/{profile}[/{label}] +/{application}-{profile}.yml +/{label}/{application}-{profile}.yml +/{application}-{profile}.properties +/{label}/{application}-{profile}.properties +``` + +例如: + +``` +curl localhost:8888/foo/development +curl localhost:8888/foo/development/master +curl localhost:8888/foo/development,db/master +curl localhost:8888/foo-development.yml +curl localhost:8888/foo-db.properties +curl localhost:8888/master/foo-db.properties +``` + +其中`application`被注入为`spring.config.name`中的`spring.config.name`(在常规 Spring 引导应用程序中通常`application`),`profile`是一个活动配置文件(或逗号分隔的属性列表),`label`是一个可选的 git 标签(默认为`master`)。 + +Spring 云配置服务器从各种来源获取远程客户端的配置。下面的示例从 Git 存储库(必须提供)获取配置,如下面的示例所示: + +``` +spring: + cloud: + config: + server: + git: + uri: https://github.com/spring-cloud-samples/config-repo +``` + +其他来源包括任何与 JDBC 兼容的数据库、Subversion、HashiC或pVault、Credhub 和本地文件系统。 + +### [客户端使用](#_client_side_usage) + +要在应用程序中使用这些特性,你可以将其构建为一个依赖于 Spring-cloud-config-client 的 Spring 引导应用程序(例如,请参见 config-client 或示例应用程序的测试用例)。添加依赖项最方便的方法是使用 Spring 引导启动器`org.springframework.cloud:spring-cloud-starter-config`。还有一个用于 Maven 用户的父 POM 和 BOM(` Spring-cloud-starter-parent`),以及用于 Gradle 和 Spring CLI 用户的 Spring IO 版本管理属性文件。下面的示例显示了典型的 Maven 配置: + +POM.xml + +``` + + org.springframework.boot + spring-boot-starter-parent + {spring-boot-docs-version} + + + + + + + org.springframework.cloud + spring-cloud-dependencies + {spring-cloud-version} + pom + import + + + + + + + org.springframework.cloud + spring-cloud-starter-config + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + +``` + +现在,你可以创建一个标准的 Spring 启动应用程序,例如下面的 HTTP 服务器: + +``` +@SpringBootApplication +@RestController +public class Application { + + @RequestMapping("/") + public String home() { + return "Hello World!"; + } + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} +``` + +当此 HTTP 服务器运行时,它会从端口 8888 上的默认本地配置服务器(如果正在运行)获取外部配置。要修改启动行为,可以使用`应用程序.属性`更改配置服务器的位置,如下例所示: + +``` +spring.config.import=optional:configserver:http://myconfigserver.com +``` + +默认情况下,如果没有设置应用程序名称,将使用`application`。要修改名称,可以将以下属性添加到`应用程序.属性`文件中: + +``` +spring.application.name: myapp +``` + +| |在设置属性`${spring.application.name}`时,不要在应用程序名称前加上保留的单词`application-`,以防止解决正确的属性源问题。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +配置服务器属性在`/env`端点中显示为高优先级属性源,如下面的示例所示。 + +``` +$ curl localhost:8080/env +{ + "activeProfiles": [], + { + "name": "servletContextInitParams", + "properties": {} + }, + { + "name": "configserver:https://github.com/spring-cloud-samples/config-repo/foo.properties", + "properties": { + "foo": { + "value": "bar", + "origin": "Config Server https://github.com/spring-cloud-samples/config-repo/foo.properties:2:12" + } + } + }, + ... +} +``` + +一个名为`configserver:/`的属性源包含`foo`属性,其值为`bar`。 + +| |属性源名称中的 URL 是 Git 存储库,而不是 Config Server URL。| +|---|-------------------------------------------------------------------------------------| + +| |如果使用 Spring Cloud Config Client,则需要设置`spring.config.import`属性,以便绑定到 Config Server。你可以阅读有关它的更多信息[in the Spring Cloud Config Reference Guide](https://docs.spring.io/spring-cloud-config/docs/current/reference/html/#config-data-import)。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## [Spring Cloud Config Server](#_spring_cloud_config_server) + +Spring Cloud Config Server 提供了用于外部配置(名称-值对或等效的 YAML 内容)的基于 HTTP 资源的 API。通过使用`@EnableConfigServer`注释,服务器可嵌入到 Spring 引导应用程序中。因此,以下应用程序是一个配置服务器: + +configserver.java + +``` +@SpringBootApplication +@EnableConfigServer +public class ConfigServer { + public static void main(String[] args) { + SpringApplication.run(ConfigServer.class, args); + } +} +``` + +像所有 Spring 启动应用程序一样,它默认情况下在 8080 端口上运行,但你可以通过各种方式将其切换到更传统的 8888 端口。最简单的方法是用`spring.config.name=configserver`启动它(在配置服务器 jar 中有一个`configserver.yml`)。另一种方法是使用自己的`应用程序.属性`,如下例所示: + +application.properties + +``` +server.port: 8888 +spring.cloud.config.server.git.uri: file://${user.home}/config-repo +``` + +其中`${user.home}/config-repo`是一个包含 YAML 和 Properties 文件的 Git 存储库。 + +| |在 Windows 上,如果文件 URL 是绝对的,并带有驱动器前缀,则需要在文件 URL 中添加一个额外的“/”(例如,`[文件:///${user.home}/config-repo](file:///${user.home}/config-repo)`)。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |下面的清单显示了在前面的示例中创建 Git 存储库的方法:

``
$cd$HOME
$mkdir$HOME
$cd config-repo
$git init<132"/>$git init<133"/>$echo info.foo:bar>properties$git add-a.gt r=”135“/>$git properties-m”/>你应该在生产中使用服务器来托管你的配置存储库。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |如果只保存文本文件,那么配置存储库的初始克隆将是快速有效的。
如果存储二进制文件,特别是大的二进制文件,你可能会在第一次请求配置时遇到延迟,或者在服务器中遇到内存不足的错误。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [环境存储库](#_environment_repository) + +你应该将配置服务器的配置数据存储在哪里?管理此行为的策略是`EnvironmentRepository`,服务于`Environment`对象。这`Environment`是从 Spring `Environment`的域的浅拷贝(包括以`propertySources`为主要特征)。`Environment`资源由三个变量参数化: + +* `{application}`,它映射到客户端的`spring.application.name`。 + +* `{profile}`,它映射到客户机上的`spring.profiles.active`(以逗号分隔的列表)。 + +* `{label}`,这是一个服务器端特性,标记了一组“版本控制的”配置文件。 + +存储库实现的行为通常类似于 Spring 引导应用程序,从等于`spring.config.name`参数的`{application}`和等于`spring.profiles.active`参数的`{profiles}`中加载配置文件。配置文件的优先规则也与常规 Spring 引导应用程序中的规则相同:活动配置文件优先于默认值,并且,如果有多个配置文件,则最后一个优先(类似于将条目添加到`Map`)。 + +以下示例客户机应用程序具有此引导程序配置: + +``` +spring: + application: + name: foo + profiles: + active: dev,mysql +``` + +(与通常的 Spring 引导应用程序一样,这些属性也可以通过环境变量或命令行参数来设置)。 + +如果存储库是基于文件的,那么服务器将从`应用程序.yml`(所有客户机之间共享)和(以`foo.yml`为准)创建一个“环境”。如果 YAML 文件中有指向 Spring 配置文件的文档,则应用这些文档的优先级更高(按所列配置文件的顺序排列)。如果存在特定于配置文件的 YAML(或 Properties)文件,那么这些文件的应用优先级也要高于默认值。更高的优先级表示在`Environment`中列出的`PropertySource`。(这些相同的规则也适用于独立的引导应用程序。 + +你可以将 Spring.cloud.config.server.accept-empty 设置为 false,这样,如果没有找到应用程序,服务器将返回 HTTP404 状态。默认情况下,此标志设置为 true。 + +#### [Git Backend](#_git_backend) + +`EnvironmentRepository`的默认实现使用了 Git 后端,这对于管理升级和物理环境以及审核更改非常方便。要更改存储库的位置,可以在配置服务器中设置`spring.cloud.config.server.git.uri`配置属性(例如在`application.yml`中)。如果你使用`file:`前缀对它进行设置,那么它应该在本地存储库中工作,这样你就可以在没有服务器的情况下快速轻松地启动它。然而,在这种情况下,服务器直接在本地存储库上操作,而不克隆它(如果它不是裸露的,那也没关系,因为配置服务器从不对“远程”存储库进行更改)。要扩展配置服务器并使其高度可用,你需要让服务器的所有实例指向同一个存储库,这样只有共享文件系统才能工作。即使在这种情况下,对于共享文件系统存储库也最好使用`ssh:`协议,这样服务器就可以克隆它并使用本地工作副本作为缓存。 + +这个存储库实现将 HTTP 资源的`{label}`参数映射到一个 Git 标签(提交 ID、分支名称或标记)。如果 Git 分支或标记名包含斜杠,那么 HTTP URL 中的标签应该使用特殊字符串`(_)`来指定(以避免与其他 URL 路径产生歧义)。例如,如果标签是`foo/bar`,替换斜杠将导致以下标签:`foo(_)bar`。特殊字符串`(_)`的包含也可以应用于`{application}`参数。如果你使用命令行客户机(如 curl),请小心 URL 中的括号——你应该用单引号(“”)将它们从 shell 中转出。 + +##### [跳过 SSL 证书验证](#_skipping_ssl_certificate_validation) + +通过将`git.SkipsslValidation`属性设置为`true`(默认设置为`false`),可以禁用配置服务器对 Git 服务器的 SSL 证书的验证。 + +``` +spring: + cloud: + config: + server: + git: + uri: https://example.com/my/repo + skipSslValidation: true +``` + +##### [设置 HTTP 连接超时](#_setting_http_connection_timeout) + +你可以配置配置服务器等待获得 HTTP 连接的时间(以秒为单位)。使用`git.timeout`属性。 + +``` +spring: + cloud: + config: + server: + git: + uri: https://example.com/my/repo + timeout: 4 +``` + +##### [git uri 中的占位符](#_placeholders_in_git_uri) + +Spring Cloud Config Server 支持带有`{application}`和`{profile}`占位符的 Git Repository URL(如果需要的话,还支持`{label}`,但请记住该标签无论如何都是作为 Git 标签应用的)。因此,你可以使用类似于以下结构的结构来支持“每个应用程序一个存储库”策略: + +``` +spring: + cloud: + config: + server: + git: + uri: https://github.com/myorg/{application} +``` + +你还可以通过使用类似的模式(但使用“{profile}”)来支持“每个配置文件一个存储库”策略。 + +此外,使用`{application}`参数中的特殊字符串“(\_)”可以启用对多个组织的支持,如以下示例所示: + +``` +spring: + cloud: + config: + server: + git: + uri: https://github.com/{application} +``` + +其中`{application}`在请求时以以下格式提供:`organization(_)application`。 + +##### [模式匹配和多个存储库](#_pattern_matching_and_multiple_repositories) + +Spring 云配置还包括支持在应用程序和配置文件名称上与模式匹配的更复杂的需求。模式格式是用逗号分隔的带有通配符的`{application}/{profile}`名称列表(请注意,可能需要引用以通配符开头的模式),如下例所示: + +``` +spring: + cloud: + config: + server: + git: + uri: https://github.com/spring-cloud-samples/config-repo + repos: + simple: https://github.com/simple/config-repo + special: + pattern: special*/dev*,*special*/dev* + uri: https://github.com/special/config-repo + local: + pattern: local* + uri: file:/home/configsvc/config-repo +``` + +如果`{application}/{profile}`不匹配任何模式,则使用在`spring.cloud.config.server.git.uri`下定义的默认 URI。在上面的示例中,对于“简单”存储库,模式是`simple/*`(在所有配置文件中,它只匹配一个名为`simple`的应用程序)。“local”存储库匹配所有配置文件中以`local`开头的所有应用程序名称(`/*`后缀会自动添加到任何没有配置文件匹配器的模式中)。 + +| |“简单”示例中使用的“单行”捷径仅在要设置的唯一属性是 URI 时才能使用。
如果需要设置其他任何内容(凭据、模式等),则需要使用完整的表单。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +repo 中的`pattern`属性实际上是一个数组,因此你可以使用 YAML 数组(或`[0]`、`[1]`等属性文件中的后缀)绑定到多个模式。如果要运行带有多个配置文件的应用程序,你可能需要这样做,如以下示例所示: + +``` +spring: + cloud: + config: + server: + git: + uri: https://github.com/spring-cloud-samples/config-repo + repos: + development: + pattern: + - '*/development' + - '*/staging' + uri: https://github.com/development/config-repo + staging: + pattern: + - '*/qa' + - '*/production' + uri: https://github.com/staging/config-repo +``` + +| |Spring 云猜测,包含配置文件的模式不以`*`结束,这意味着你实际上希望匹配以该模式开始的配置文件列表(因此`*/staging`是`["*/staging", "*/staging,*"]`的快捷方式,以此类推)。
例如,这是常见的,你需要在本地运行“开发”配置文件中的应用程序,还需要远程运行“云”配置文件中的应用程序。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +每个存储库还可以选择将配置文件存储在子目录中,搜索这些目录的模式可以指定为`search-paths`。下面的示例显示了顶层的配置文件: + +``` +spring: + cloud: + config: + server: + git: + uri: https://github.com/spring-cloud-samples/config-repo + search-paths: + - foo + - bar* +``` + +在前面的示例中,服务器在顶层和`foo/`子目录以及名称以`bar`开头的任意子目录中搜索配置文件。 + +默认情况下,服务器在首次请求配置时复制远程存储库。可以将服务器配置为在启动时克隆存储库,如下面的顶级示例所示: + +``` +spring: + cloud: + config: + server: + git: + uri: https://git/common/config-repo.git + repos: + team-a: + pattern: team-a-* + cloneOnStart: true + uri: https://git/team-a/config-repo.git + team-b: + pattern: team-b-* + cloneOnStart: false + uri: https://git/team-b/config-repo.git + team-c: + pattern: team-c-* + uri: https://git/team-a/config-repo.git +``` + +在前面的示例中,服务器在接受任何请求之前,在启动时复制 Team-A 的 config-repo。在请求从存储库进行配置之前,不会克隆所有其他存储库。 + +| |在 Config 服务器启动时设置要克隆的存储库,可以帮助在 Config 服务器启动时快速识别配置错误的配置源(例如无效的存储库 URI),
with`cloneOnStart`配置源未启用,配置服务器可以使用配置错误或无效的配置源成功启动,并且在应用程序从该配置源请求配置之前不会检测到错误。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [Authentication](#_authentication) + +要在远程存储库上使用 HTTP Basic 身份验证,请分别添加`username`和`password`属性(不在 URL 中),如以下示例所示: + +``` +spring: + cloud: + config: + server: + git: + uri: https://github.com/spring-cloud-samples/config-repo + username: trolley + password: strongpassword +``` + +如果你不使用 HTTPS 和用户凭据,那么当你将密钥存储在默认目录中并且 URI 指向 SSH 位置(例如)时,SSH 也应该在开箱即用的情况下工作。在`~/.ssh/known_hosts`文件中存在用于 Git 服务器的条目,并且该条目是`ssh-rsa`格式,这一点很重要。不支持其他格式(如`ecdsa-sha2-nistp256`)。为了避免意外,你应该确保 Git 服务器的`known_hosts`文件中只有一个条目,并且它与你提供给 Config 服务器的 URL 相匹配。如果在 URL 中使用主机名,则希望在`known_hosts`文件中使用该主机名(而不是 IP)。通过使用 JGIT 访问存储库,因此你在其中找到的任何文档都应该适用。HTTPS 代理设置可以设置在`~/.git/config`中,或者(以与任何其他 JVM 进程相同的方式)使用系统属性(`-dhttps.proxyhost` 和`-Dhttps.proxyPort`)。 + +| |如果你不知道你的`~/.git`目录在哪里,请使用`git config --global`来操作设置(例如,`git config --global http.sslVerify false`)。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +JGIT 需要 PEM 格式的 RSA 密钥。下面是一个示例 ssh-keygen(来自 OpenSSH)命令,它将以 CORECT 格式生成一个键: + +``` +ssh-keygen -m PEM -t rsa -b 4096 -f ~/config_server_deploy_key.rsa +``` + +警告:在使用 SSH 密钥时,预期的 SSH 私钥必须以``-----BEGIN RSA PRIVATE KEY-----``开头。如果密钥以``-----BEGIN OPENSSH PRIVATE KEY-----``开始,那么当 Spring-cloud-config 服务器启动时,RSA 密钥将不会加载。这个错误看起来是这样的: + +``` +- Error in object 'spring.cloud.config.server.git': codes [PrivateKeyIsValid.spring.cloud.config.server.git,PrivateKeyIsValid]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [spring.cloud.config.server.git.,]; arguments []; default message []]; default message [Property 'spring.cloud.config.server.git.privateKey' is not a valid private key] +``` + +要纠正上述错误,必须将 RSA 键转换为 PEM 格式。上面提供了一个使用 OpenSSH 生成适当格式的新键的示例。 + +##### [使用 AWS codecommit 进行身份验证](#_authentication_with_aws_codecommit) + +Spring 云配置服务器还支持[AWS CodeCommit](https://docs.aws.amazon.com/codecommit/latest/userguide/welcome.html)身份验证。当从命令行使用 Git 时,AWS Codecommit 使用身份验证助手。此助手不与 JGIT 库一起使用,因此,如果 Git URI 与 AWS Codecommit 模式匹配,那么将创建用于 AWS Codecommit 的 JGit CredentialProvider。AWS codecommit URI 遵循以下模式: + +``` +https//git-codecommit.${AWS_REGION}.amazonaws.com/v1/repos/${repo}. +``` + +如果你提供了带有 AWS CodeCommit URI 的用户名和密码,那么它们必须是提供对存储库访问的[AWS AccessKeyID 和 SecretAccessKey](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSGettingStartedGuide/AWSCredentials.html)。如果你没有指定用户名和密码,那么将使用[AWS 默认凭据提供商链](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html)检索 AccessKeyID 和 SecretAccessKey。 + +如果你的 Git URI 与 codecommit URI 模式匹配(如前所述),则必须在用户名和密码中或在默认凭据提供商链支持的位置之一中提供有效的 AWS 凭据。AWS EC2 实例可以使用[EC2 实例的 IAM 角色](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html)。 + +| |`aws-java-sdk-core` jar 是一个可选的依赖项。
如果`aws-java-sdk-core` jar 不在你的 Classpath 上,则不会创建 AWS 代码提交凭据提供程序,而不管 Git 服务器的 URI 是什么。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [使用 Google Cloud Source 进行身份验证](#_authentication_with_google_cloud_source) + +Spring 云配置服务器还支持针对[Google Cloud Source](https://cloud.google.com/source-repositories/)存储库进行身份验证。 + +如果你的 Git URI 使用`http`或`https`协议,并且域名是`source.developers.google.com`,则将使用 Google Cloud Source 凭据提供商。Google Cloud Source Repository 的 URI 格式为`[https://source.developers.google.com/p/${GCP_PROJECT}/r/${REPO}](https://source.developers.google.com/p/${GCP_PROJECT}/r/${REPO})`。要获得存储库的 URI,请单击 Google Cloud Source UI 中的“Clone”,并选择“手动生成的凭据”。不生成任何凭据,只需复制显示的 URI。 + +Google Cloud Source 凭据提供商将使用 Google Cloud Platform 应用程序的默认凭据。关于如何为系统创建应用程序默认凭据,请参见[Google Cloud SDK 文档](https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login)。这种方法适用于开发环境中的用户帐户和生产环境中的服务帐户。 + +| |`com.google.auth:google-auth-library-oauth2-http`是一个可选的依赖项。
如果`google-auth-library-oauth2-http` jar 不在你的 Classpath 上,则不会创建 Google Cloud Source 凭据提供者,无论 Git 服务器的 URI 是什么。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [使用属性的 Git SSH 配置](#_git_ssh_configuration_using_properties) + +默认情况下, Spring Cloud Config Server 使用的 JGit 库在使用 SSH URI 连接到 Git 存储库时使用诸如`~/.ssh/known_hosts`和`/etc/ssh/ssh_config`等 SSH 配置文件。在云环境中,例如 Cloud Foundry,本地文件系统可能是短暂的,或者不容易访问。对于这些情况,可以使用 Java 属性设置 SSH 配置。为了激活基于属性的 SSH 配置,`spring.cloud.config.server.git.ignoreLocalSshSettings`属性必须设置为`true`,如以下示例所示: + +``` + spring: + cloud: + config: + server: + git: + uri: [email protected]:team/repo1.git + ignoreLocalSshSettings: true + hostKey: someHostKey + hostKeyAlgorithm: ssh-rsa + privateKey: | + -----BEGIN RSA PRIVATE KEY----- + MIIEpgIBAAKCAQEAx4UbaDzY5xjW6hc9jwN0mX33XpTDVW9WqHp5AKaRbtAC3DqX + IXFMPgw3K45jxRb93f8tv9vL3rD9CUG1Gv4FM+o7ds7FRES5RTjv2RT/JVNJCoqF + ol8+ngLqRZCyBtQN7zYByWMRirPGoDUqdPYrj2yq+ObBBNhg5N+hOwKjjpzdj2Ud + 1l7R+wxIqmJo1IYyy16xS8WsjyQuyC0lL456qkd5BDZ0Ag8j2X9H9D5220Ln7s9i + oezTipXipS7p7Jekf3Ywx6abJwOmB0rX79dV4qiNcGgzATnG1PkXxqt76VhcGa0W + DDVHEEYGbSQ6hIGSh0I7BQun0aLRZojfE3gqHQIDAQABAoIBAQCZmGrk8BK6tXCd + fY6yTiKxFzwb38IQP0ojIUWNrq0+9Xt+NsypviLHkXfXXCKKU4zUHeIGVRq5MN9b + BO56/RrcQHHOoJdUWuOV2qMqJvPUtC0CpGkD+valhfD75MxoXU7s3FK7yjxy3rsG + EmfA6tHV8/4a5umo5TqSd2YTm5B19AhRqiuUVI1wTB41DjULUGiMYrnYrhzQlVvj + 5MjnKTlYu3V8PoYDfv1GmxPPh6vlpafXEeEYN8VB97e5x3DGHjZ5UrurAmTLTdO8 + +AahyoKsIY612TkkQthJlt7FJAwnCGMgY6podzzvzICLFmmTXYiZ/28I4BX/mOSe + pZVnfRixAoGBAO6Uiwt40/PKs53mCEWngslSCsh9oGAaLTf/XdvMns5VmuyyAyKG + ti8Ol5wqBMi4GIUzjbgUvSUt+IowIrG3f5tN85wpjQ1UGVcpTnl5Qo9xaS1PFScQ + xrtWZ9eNj2TsIAMp/svJsyGG3OibxfnuAIpSXNQiJPwRlW3irzpGgVx/AoGBANYW + dnhshUcEHMJi3aXwR12OTDnaLoanVGLwLnkqLSYUZA7ZegpKq90UAuBdcEfgdpyi + PhKpeaeIiAaNnFo8m9aoTKr+7I6/uMTlwrVnfrsVTZv3orxjwQV20YIBCVRKD1uX + VhE0ozPZxwwKSPAFocpyWpGHGreGF1AIYBE9UBtjAoGBAI8bfPgJpyFyMiGBjO6z + FwlJc/xlFqDusrcHL7abW5qq0L4v3R+FrJw3ZYufzLTVcKfdj6GelwJJO+8wBm+R + gTKYJItEhT48duLIfTDyIpHGVm9+I1MGhh5zKuCqIhxIYr9jHloBB7kRm0rPvYY4 + VAykcNgyDvtAVODP+4m6JvhjAoGBALbtTqErKN47V0+JJpapLnF0KxGrqeGIjIRV + cYA6V4WYGr7NeIfesecfOC356PyhgPfpcVyEztwlvwTKb3RzIT1TZN8fH4YBr6Ee + KTbTjefRFhVUjQqnucAvfGi29f+9oE3Ei9f7wA+H35ocF6JvTYUsHNMIO/3gZ38N + CPjyCMa9AoGBAMhsITNe3QcbsXAbdUR00dDsIFVROzyFJ2m40i4KCRM35bC/BIBs + q0TY3we+ERB40U8Z2BvU61QuwaunJ2+uGadHo58VSVdggqAo0BSkH58innKKt96J + 69pcVH/4rmLbXdcmNYGm6iu+MlPQk4BUZknHSmVHIFdJ0EPupVaQ8RHT + -----END RSA PRIVATE KEY----- +``` + +下表描述了 SSH 配置属性。 + +| Property Name |备注| +|----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **ignoreLocalSshSettings** |如果`true`,使用基于属性而不是基于文件的 SSH 配置。必须在存储库定义中设置为`spring.cloud.config.server.git.ignoreLocalSshSettings`,**不是**。| +| **privateKey** |有效的 SSH 私钥。如果`ignoreLocalSshSettings`为 true 且 git uri 为 ssh 格式,则必须设置。| +| **hostKey** |有效的 SSH 主机键。如果`hostKeyAlgorithm`也已设置,则必须设置。| +| **hostKeyAlgorithm** |`ssh-dss, ssh-rsa, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, or ecdsa-sha2-nistp521`中的一个。如果`hostKey`也已设置,则必须设置。| +| **strictHostKeyChecking** |`true`或`false`。如果为假,请忽略使用主机键的错误。| +| **knownHostsFile** |自定义`.known_hosts`文件的位置。| +|**preferredAuthentications**|重写服务器身份验证方法命令.如果服务器在`publickey`方法之前具有键盘交互身份验证,那么这将允许规避登录提示。| + +##### [git 搜索路径中的占位符](#_placeholders_in_git_search_paths) + +Spring Cloud Config Server 还支持带有`{application}`和`{profile}`(如果需要的话,还支持`{label}`)占位符的搜索路径,如以下示例所示: + +``` +spring: + cloud: + config: + server: + git: + uri: https://github.com/spring-cloud-samples/config-repo + search-paths: '{application}' +``` + +前面的列表导致在存储库中搜索与目录(以及顶层)同名的文件。通配符在带有占位符的搜索路径中也是有效的(搜索中包含任何匹配的目录)。 + +##### [强制拉入 Git 存储库](#_force_pull_in_git_repositories) + +正如前面提到的, Spring Cloud Config 服务器复制远程 Git 存储库,以防本地副本变脏(例如,由 OS 进程更改的文件夹内容),使得 Spring Cloud Config 服务器无法从远程存储库更新本地副本。 + +为了解决这个问题,有一个`force-pull`属性,如果本地副本是脏的,该属性将使 Spring Cloud Config 服务器强制从远程存储库中拉出,如以下示例所示: + +``` +spring: + cloud: + config: + server: + git: + uri: https://github.com/spring-cloud-samples/config-repo + force-pull: true +``` + +如果具有多存储库配置,则可以为每个存储库配置`force-pull`属性,如以下示例所示: + +``` +spring: + cloud: + config: + server: + git: + uri: https://git/common/config-repo.git + force-pull: true + repos: + team-a: + pattern: team-a-* + uri: https://git/team-a/config-repo.git + force-pull: true + team-b: + pattern: team-b-* + uri: https://git/team-b/config-repo.git + force-pull: true + team-c: + pattern: team-c-* + uri: https://git/team-a/config-repo.git +``` + +| |`force-pull`属性的默认值是`false`。| +|---|-------------------------------------------------------| + +##### [删除 Git 存储库中未跟踪的分支](#_deleting_untracked_branches_in_git_repositories) + +由于 Spring Cloud Config 服务器在将分支检查到本地 repo(例如通过标签获取属性)之后具有远程 Git 存储库的克隆,因此它将永远保留该分支,或者直到下一个服务器重新启动(这将创建新的本地 repo)。因此,可能存在这样一种情况,即远程分支被删除,但其本地副本仍可用于获取。而如果 Spring Cloud Config Server Client Service 以`--spring.cloud.config.label=deletedRemoteBranch,master`开始,它将从`deletedRemoteBranch`本地分支获取属性,而不是从`master`获取属性。 + +为了保持本地存储库分支的清理和到远程-`deleteUntrackedBranches`属性可以被设置。它将使 Spring 云配置服务器**力**从本地存储库中删除未跟踪的分支。示例: + +``` +spring: + cloud: + config: + server: + git: + uri: https://github.com/spring-cloud-samples/config-repo + deleteUntrackedBranches: true +``` + +| |`deleteUntrackedBranches`属性的默认值是`false`。| +|---|--------------------------------------------------------------------| + +##### [Git 刷新率](#_git_refresh_rate) + +你可以通过使用`spring.cloud.config.server.git.refreshRate`来控制配置服务器多久会从你的 Git 后端获取更新的配置数据。此属性的值以秒为单位指定。默认情况下,该值为 0,这意味着配置服务器将在每次请求时从 Git Repo 获取更新的配置。 + +##### [Default Label](#_default_label) + +Git 使用的默认标签是`main`。如果你没有设置`spring.cloud.config.server.git.defaultLabel`,并且一个名为`main`的分支不存在,则默认情况下,配置服务器还将尝试检出一个名为`master`的分支。如果你想禁用回退分支行为,可以将 ` Spring.cloud.config.server.git.trymasterbranch` 设置为`false`。 + +#### [版本控制后端文件系统的使用](#_version_control_backend_filesystem_use) + +| |使用基于 VCS 的后端,文件将被签出或克隆到本地文件系统中,
默认情况下,它们被放入系统临时目录中,前缀为`config-repo-`,例如,在 Linux 上,
,可能是`/tmp/config-repo-`。
某些操作系统[定期清理](https://serverfault.com/questions/377348/when-does-tmp-get-cleared/377349#377349)临时目录。
这可能会导致意想不到的行为,例如丢失属性。,
为了避免这个问题,将`spring.cloud.config.server.git.basedir`或`spring.cloud.config.server.svn.basedir`设置为不驻留在系统临时结构中的目录,从而更改 Config 服务器使用的目录。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [文件系统后端](#_file_system_backend) + +配置服务器中还有一个“本机”配置文件,它不使用 Git,而是从本地 Classpath 或文件系统(你想用`spring.cloud.config.server.native.searchLocations`指向的任何静态 URL)加载配置文件。要使用本机配置文件,使用`spring.profiles.active=native`启动配置服务器。 + +| |记住对文件资源使用`file:`前缀(不带前缀的默认情况通常是 Classpath)。
与任何 Spring 引导配置一样,你可以嵌入`${}`-风格的环境占位符,但请记住,Windows 中的绝对路径需要额外的`/`(例如,`[file:///${user.home}/config-repo](file:///${user.home}/config-repo)`)。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |`searchLocations`的默认值与本地 Spring 引导应用程序(即`[classpath:/, classpath:/config,
file:./, file:./config]`)相同。
这不会将来自服务器的`application.properties`公开给所有客户端,因为服务器中存在的任何属性源在发送到客户端之前都会被删除。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |文件系统后端对于快速启动和测试非常有用。
要在生产中使用它,你需要确保文件系统是可靠的,并在配置服务器的所有实例之间共享。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +搜索位置可以包含`{application}`、`{profile}`和`{label}`的占位符。通过这种方式,你可以隔离路径中的目录,并选择对你有意义的策略(例如每个应用程序的子目录或每个配置文件的子目录)。 + +如果在搜索位置中不使用占位符,那么这个存储库还会将 HTTP 资源的`{label}`参数附加到搜索路径的后缀中,因此,属性文件是从每个搜索位置**和**一个与标签同名的子目录加载的(在 Spring 环境中,标记属性优先)。因此,不带占位符的默认行为与添加以`/{label}/`结尾的搜索位置相同。例如,`file:/tmp/config`与`file:/tmp/config,file:/tmp/config/{label}`相同。可以通过设置`spring.cloud.config.server.native.addLabelLocations=false`来禁用此行为。 + +#### [Vault Backend](#vault-backend) + +Spring Cloud Config Server 还支持[Vault](https://www.vaultproject.io)作为后端。 + +Vault 是一种安全访问机密的工具。秘密是你想要严格控制访问的任何内容,例如 API 密钥、密码、证书和其他敏感信息。Vault 在提供严格的访问控制和记录详细的审计日志的同时,为任何秘密提供了统一的接口。 + +有关 Vault 的更多信息,请参见[跳马快速启动指南](https://learn.hashicorp.com/vault/?track=getting-started#getting-started)。 + +要使 Config 服务器能够使用 Vault 后端,你可以使用`vault`配置文件运行你的 Config 服务器。例如,在配置服务器的`application.properties`中,可以添加`spring.profiles.active=vault`。 + +默认情况下, Spring Cloud Config Server 使用基于令牌的身份验证来从 Vault 获取配置。Vault 还支持其他身份验证方法,如 Approle、LDAP、JWT、CloudFoundry、Kubernetes Auth。为了使用除令牌或 X-Config-Token 头以外的任何身份验证方法,我们需要在 Classpath 上具有 Spring Vault core,以便 Config 服务器可以将身份验证委派给该库。请将以下依赖项添加到你的配置服务器应用程序中。 + +`Maven (POM.xml)` + +``` + + + org.springframework.vault + spring-vault-core + + +``` + +`Gradle (build.gradle)` + +``` +dependencies { + implementation "org.springframework.vault:spring-vault-core" +} +``` + +默认情况下,配置服务器假定你的 Vault 服务器运行在`[http://127.0.0.1:8200](http://127.0.0.1:8200)`。它还假定后端的名称是`secret`,键是`application`。所有这些默认值都可以在配置服务器的`application.properties`中进行配置。下表描述了可配置的保险库属性: + +|姓名|Default Value| +|-----------------|-------------| +|主机| 127.0.0.1 | +|港口| 8200 | +|方案| http | +|后端| secret | +|DefaultKey| application | +|Profileseparator| , | +|KVVersion| 1 | +|skipSslValidation| false | +|超时| 5 | +|名称空间| null | + +| |前一个表中的所有属性必须使用`spring.cloud.config.server.vault`作为前缀,或者放在复合配置的正确的保险库部分。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +所有可配置的属性都可以在`org.springframework.cloud.config.server.environment.VaultEnvironmentProperties`中找到。 + +| |Vault0.10.0 引入了一个版本控制的键值后端(k/v 后端版本 2),该版本公开了与早期版本不同的 API,现在它需要在挂载路径和实际上下文路径之间设置`data/`,并在`data`对象中包装秘密。设置`spring.cloud.config.server.vault.kv-version=2`将考虑到这一点。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +可选地,存在对 Vault Enterprise`X-Vault-Namespace`头的支持。要将它发送到 Vault,请设置`namespace`属性。 + +在配置服务器运行时,你可以向服务器发出 HTTP 请求,以便从保险库后端取回值。要做到这一点,你需要为你的保险库服务器提供一个令牌。 + +首先,在保险库中放置一些数据,如以下示例所示: + +``` +$ vault kv put secret/application foo=bar baz=bam +$ vault kv put secret/myapp foo=myappsbar +``` + +其次,向配置服务器发出 HTTP 请求,以检索这些值,如下例所示: + +`$ curl -X "GET" "http://localhost:8888/myapp/default" -H "X-Config-Token: yourtoken"` + +你应该会看到类似于以下内容的响应: + +``` +{ + "name":"myapp", + "profiles":[ + "default" + ], + "label":null, + "version":null, + "state":null, + "propertySources":[ + { + "name":"vault:myapp", + "source":{ + "foo":"myappsbar" + } + }, + { + "name":"vault:application", + "source":{ + "baz":"bam", + "foo":"bar" + } + } + ] +} +``` + +客户机提供必要的身份验证以让 Config Server 与 Vault 对话的默认方式是设置 X-Config-Token 头。但是,你可以通过设置与 Spring Cloud Vault 相同的配置属性,省略标题并在服务器中配置身份验证。要设置的属性是`spring.cloud.config.server.vault.authentication`。应该将其设置为受支持的身份验证方法之一。你可能还需要设置特定于你使用的身份验证方法的其他属性,方法是使用与`spring.cloud.vault`相同的属性名称,而不是使用`spring.cloud.config.server.vault`前缀。有关更多详细信息,请参见[Spring Cloud Vault Reference Guide](https://cloud.spring.io/spring-cloud-vault/reference/html/#vault.config.authentication)。 + +| |如果省略了 x-config-token 头并使用服务器属性来设置身份验证,则 Config 服务器应用程序需要对 Spring Vault 有一个额外的依赖项,以启用额外的身份验证选项。
有关如何添加该依赖项,请参见[Spring Vault Reference Guide](https://docs.spring.io/spring-vault/docs/current/reference/html/#dependencies)。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [多属性源](#_multiple_properties_sources) + +在使用 Vault 时,你可以为应用程序提供多个属性源。例如,假设你已将数据写入 Vault 中的以下路径: + +``` +secret/myApp,dev +secret/myApp +secret/application,dev +secret/application +``` + +写入`secret/application`的属性可用于[所有使用配置服务器的应用程序](#_vault_server)。名称为`myApp`的应用程序将具有写为`secret/myApp`和`secret/application`的任何属性。当`myApp`启用`dev`配置文件时,写到上述所有路径的属性将对它可用,并且列表中第一个路径中的属性优先于其他路径。 + +#### [通过代理访问后端](#_accessing_backends_through_a_proxy) + +配置服务器可以通过 HTTP 或 HTTPS 代理访问 Git 或 Vault 后端。在`proxy.http`和`proxy.https`下的设置可以控制 Git 或 Vault 的这种行为。这些设置是每个存储库设置的,因此,如果使用[复合环境存储库](#composite-environment-repositories),则必须为组合中的每个后端单独配置代理设置。如果使用的网络需要为 HTTP 和 HTTPS URL 提供单独的代理服务器,则可以为单个后端配置 HTTP 和 HTTPS 代理设置。 + +下表描述了 HTTP 和 HTTPS 代理的代理配置属性。所有这些属性都必须以`proxy.http`或`proxy.https`为前缀。 + +| Property Name |备注| +|-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **host** |代理的主机。| +| **port** |访问代理的端口。| +|**nonProxyHosts**|配置服务器应该访问代理之外的任何主机.如果同时为`proxy.http.nonProxyHosts`和`proxy.https.nonProxyHosts`提供了值,则将使用`proxy.http`值。| +| **username** |对代理进行身份验证的用户名。如果同时为`proxy.http.username`和`proxy.https.username`提供了值,则将使用`proxy.http`值。| +| **password** |用于对代理进行身份验证的密码。如果同时为`proxy.http.password`和`proxy.https.password`提供了值,则将使用`proxy.http`值。| + +以下配置使用 HTTPS 代理访问 Git 存储库。 + +``` +spring: + profiles: + active: git + cloud: + config: + server: + git: + uri: https://github.com/spring-cloud-samples/config-repo + proxy: + https: + host: my-proxy.host.io + password: myproxypassword + port: '3128' + username: myproxyusername + nonProxyHosts: example.com +``` + +#### [与所有应用程序共享配置](#_sharing_configuration_with_all_applications) + +在所有应用程序之间共享配置取决于你所采用的方法,如以下主题所述: + +* [基于文件的存储库](#spring-cloud-config-server-file-based-repositories) + +* [Vault Server](#spring-cloud-config-server-vault-server) + +##### [基于文件的存储库](#spring-cloud-config-server-file-based-repositories) + +对于基于文件的(Git、SVN 和 Native)存储库,文件名为`application*`(`application.properties’、`application.yml`、`application-*.properties`等)的资源在所有客户端应用程序之间共享。你可以使用这些文件名的资源来配置全局默认值,并在必要时让它们被特定于应用程序的文件覆盖。 + +[属性重写](#property-overrides)特性还可以用于设置全局默认值,占位符应用程序可以在本地覆盖它们。 + +| |对于“本机”配置文件(本地文件系统后端),你应该使用一个不属于服务器自身配置的显式搜索位置。
否则,默认搜索位置中的`application*`资源将被删除,因为它们是服务器的一部分。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [Vault Server](#spring-cloud-config-server-vault-server) + +当使用 Vault 作为后端时,你可以通过在`secret/application`中放置配置来与所有应用程序共享配置。例如,如果你运行下面的 Vault 命令,那么所有使用 Config 服务器的应用程序都将具有它们可用的属性`foo`和`baz`: + +``` +$ vault write secret/application foo=bar baz=bam +``` + +##### [CredHub Server](#_credhub_server) + +当使用 Credhub 作为后端时,你可以通过将配置放置在`/application/`中或将其放置在应用程序的`default`配置文件中来与所有应用程序共享配置。例如,如果你运行下面的 credhub 命令,那么所有使用 Config 服务器的应用程序都将具有它们可用的属性`shared.color1`和`shared.color2`: + +``` +credhub set --name "/application/profile/master/shared" --type=json +value: {"shared.color1": "blue", "shared.color": "red"} +``` + +``` +credhub set --name "/my-app/default/master/more-shared" --type=json +value: {"shared.word1": "hello", "shared.word2": "world"} +``` + +#### [AWS 秘密管理器](#_aws_secrets_manager) + +当使用 AWS Secrets Manager 作为后端时,你可以通过将配置放置在`/application/`中或将其放置在应用程序的`default`配置文件中来与所有应用程序共享配置。例如,如果使用以下键添加秘密,那么所有使用配置服务器的应用程序都将具有它们可用的属性`shared.foo`和`shared.bar`: + +``` +secret name = /secret/application-default/ +``` + +``` +secret value = +{ + shared.foo: foo, + shared.bar: bar +} +``` + +or + +``` +secret name = /secret/application/ +``` + +``` +secret value = +{ + shared.foo: foo, + shared.bar: bar +} +``` + +##### [AWS 参数存储](#_aws_parameter_store) + +当使用 AWS 参数存储作为后端时,你可以通过在`/application`层次结构中放置属性来与所有应用程序共享配置。 + +例如,如果使用以下名称添加参数,那么所有使用配置服务器的应用程序都将具有它们可用的属性`foo.bar`和`fred.baz`: + +``` +/config/application/foo.bar +/config/application-default/fred.baz +``` + +#### [JDBC Backend](#_jdbc_backend) + +Spring 云配置服务器支持 JDBC(关系数据库)作为配置属性的后端。你可以通过向 Classpath 中添加`spring-jdbc`并使用`jdbc`配置文件或通过添加类型`JdbcEnvironmentRepository`的 Bean 来启用此功能。如果你包括对 Classpath 的正确依赖关系(有关该依赖关系的更多详细信息,请参见用户指南),则 Spring 引导将配置数据源。 + +通过将`spring.cloud.config.server.jdbc.enabled`属性设置为`false`,可以禁用`JdbcEnvironmentRepository`的自动配置。 + +数据库需要有一个名为`PROPERTIES`的表,其中的列分别为`APPLICATION`、`PROFILE`和`LABEL`(具有通常的`Environment`含义),加上`KEY`和`VALUE`用于`Properties`样式中的键和值对。在 Java 中,所有字段都是 String 类型的,因此你可以使它们`VARCHAR`具有所需的任何长度。如果属性值来自名为`{application}-{profile}.properties`的 Spring 引导属性文件,则属性值的行为与它们的行为相同,包括所有的加密和解密,这些将作为后处理步骤应用(即,不直接在存储库实现中)。 + +#### [Redis Backend](#_redis_backend) + +Spring 云配置服务器支持 Redis 作为配置属性的后端。你可以通过向[Spring Data Redis](https://spring.io/projects/spring-data-redis)添加依赖项来启用此功能。 + +POM.xml + +``` + + + org.springframework.boot + spring-boot-starter-data-redis + + +``` + +下面的配置使用 Spring data`RedisTemplate`来访问 Redis。我们可以使用`spring.redis.*`属性来覆盖默认的连接设置。 + +``` +spring: + profiles: + active: redis + redis: + host: redis + port: 16379 +``` + +这些属性应该存储为散列中的字段。散列的名称应该与`spring.application.name`的属性或`spring.application.name`和`spring.profiles.active[n]`的连词相同。 + +``` +HMSET sample-app server.port "8100" sample.topic.name "test" test.property1 "property1" +``` + +在运行位于散列上方可见的命令之后,散列应该包含以下带值的键: + +``` +HGETALL sample-app +{ + "server.port": "8100", + "sample.topic.name": "test", + "test.property1": "property1" +} +``` + +| |当未指定配置文件时,将使用`default`。| +|---|----------------------------------------------------| + +#### [AWS S3 Backend](#_aws_s3_backend) + +Spring 云配置服务器支持 AWS S3 作为配置属性的后端。你可以通过向[亚马逊 S3 的 AWS Java SDK](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/examples-s3.html)添加依赖项来启用此功能。 + +POM.xml + +``` + + + com.amazonaws + aws-java-sdk-s3 + + +``` + +以下配置使用 AWS S3 客户端访问配置文件。我们可以使用`spring.cloud.config.server.awss3.*`属性来选择存储配置的桶。 + +``` +spring: + profiles: + active: awss3 + cloud: + config: + server: + awss3: + region: us-east-1 + bucket: bucket1 +``` + +也可以使用`spring.cloud.config.server.awss3.endpoint`将 AWS URL 指定为你的 S3 服务的[覆盖标准端点](https://aws.amazon.com/blogs/developer/using-new-regions-and-endpoints/)。这允许支持 S3 的测试版区域,以及其他与 S3 兼容的存储 API。 + +使用[默认的 AWS 凭据提供商链](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html)找到凭据。支持版本控制和加密的桶,而无需进一步配置。 + +配置文件以`{application}-{profile}.properties`、`{application}-{profile}.yml`或`{application}-{profile}.json`的形式存储在你的 bucket 中。可以提供一个可选的标签来指定文件的目录路径。 + +| |当未指定配置文件时,将使用`default`。| +|---|----------------------------------------------------| + +#### [AWS 参数存储后端](#_aws_parameter_store_backend) + +Spring 云配置服务器支持 AWS 参数存储作为配置属性的后端。你可以通过向[面向 SSM 的 AWS Java SDK](https://github.com/aws/aws-sdk-java/tree/master/aws-java-sdk-ssm)添加依赖项来启用此功能。 + +POM.xml + +``` + + com.amazonaws + aws-java-sdk-ssm + +``` + +以下配置使用 AWS SSM 客户端访问参数。 + +``` +spring: + profiles: + active: awsparamstore + cloud: + config: + server: + awsparamstore: + region: eu-west-2 + endpoint: https://ssm.eu-west-2.amazonaws.com + origin: aws:parameter: + prefix: /config/service + profile-separator: _ + recursive: true + decrypt-values: true + max-results: 5 +``` + +下表描述了 AWS 参数存储配置属性。 + +| Property Name |Required| Default Value |备注| +|---------------------|--------|--------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **region** | no | |AWS 参数存储客户端要使用的区域。如果没有显式设置,那么 SDK 将尝试使用[默认区域提供器链](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/java-dg-region-selection.html#default-region-provider-chain)来确定要使用的区域。| +| **endpoint** | no | |AWS SSM 客户端入口点的 URL。这可以用来为 API 请求指定一个替代端点。| +| **origin** | no |`aws:ssm:parameter:`|添加到属性源名称中以显示其来源的前缀。| +| **prefix** | no | `/config` |前缀表示从 AWS 参数存储区加载的每个属性的参数层次结构中的 L1 级别。| +|**profile-separator**| no | `-` |将附加的配置文件与上下文名称分隔开的字符串。| +| **recursive** | no | `true` |标志来指示对层次结构中所有 AWS 参数的检索。| +| **decrypt-values** | no | `true` |标志来指示对所有 AWS 参数的检索,并对其值进行解密。| +| **max-results** | no | `10` |AWS 参数存储 API 调用要返回的最大项数。| + +AWS 参数存储 API 凭据是使用[默认凭据提供器链](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html#credentials-default)确定的。已支持版本控制的参数,其默认行为是返回最新版本。 + +| |* 当没有指定应用程序时,`application`是默认值,当没有指定配置文件时,使用`default`。

*`awsparamstore.prefix`的有效值必须以前斜杠开始,然后是一个或多个有效路径段,否则为空,

*`awsparamstore.profile-separator`的有效值只能包含点,破折号和下划线。

*`awsparamstore.max-results`的有效值必须在**[1, 10]**范围内。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [AWS Secrets Manager 后端](#_aws_secrets_manager_backend) + +Spring Cloud Config Server 支持[AWS 秘密管理器](https://aws.amazon.com/secrets-manager/)作为配置属性的后端。你可以通过向[用于 Secrets Manager 的 AWS Java SDK](https://github.com/aws/aws-sdk-java/tree/master/aws-java-sdk-secretsmanager)添加依赖项来启用此功能。 + +POM.xml + +``` + + com.amazonaws + aws-java-sdk-secretsmanager + +``` + +以下配置使用 AWS Secrets Manager 客户端访问机密。 + +``` +spring: + profiles: + active: awssecretsmanager + cloud: + config: + server: + aws-secretsmanager: + region: us-east-1 + endpoint: https://us-east-1.console.aws.amazon.com/ + origin: aws:secrets: + prefix: /secret/foo + profileSeparator: _ +``` + +AWS Secrets Manager API 凭据是使用[默认凭据提供器链](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html#credentials-default)确定的。 + +| |* When no application is specified `application` is the default, and when no profile is specified `default` is used.| +|---|--------------------------------------------------------------------------------------------------------------------| + +#### [CredHub Backend](#_credhub_backend) + +Spring Cloud Config Server 支持[CredHub](https://docs.cloudfoundry.org/credhub)作为配置属性的后端。你可以通过向[Spring CredHub](https://spring.io/projects/spring-credhub)添加依赖项来启用此功能。 + +POM.xml + +``` + + + org.springframework.credhub + spring-credhub-starter + + +``` + +以下配置使用 Mutual TLS 访问 credhub: + +``` +spring: + profiles: + active: credhub + cloud: + config: + server: + credhub: + url: https://credhub:8844 +``` + +这些属性应该以 JSON 的形式存储,例如: + +``` +credhub set --name "/demo-app/default/master/toggles" --type=json +value: {"toggle.button": "blue", "toggle.link": "red"} +``` + +``` +credhub set --name "/demo-app/default/master/abs" --type=json +value: {"marketing.enabled": true, "external.enabled": false} +``` + +所有名称为`spring.cloud.config.name=demo-app`的客户机应用程序都将具有以下可用属性: + +``` +{ + toggle.button: "blue", + toggle.link: "red", + marketing.enabled: true, + external.enabled: false +} +``` + +| |当未指定配置文件时,将使用`default`;当未指定标签时,将使用`master`作为默认值。
注意:添加到`application`的值将被所有应用程序共享。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [OAuth 2.0](#_oauth_2_0) + +你可以使用[OAuth 2.0](https://oauth.net/2/)作为提供者进行身份验证。 + +pom.xml + +``` + + + org.springframework.security + spring-security-config + + + org.springframework.security + spring-security-oauth2-client + + +``` + +以下配置使用 OAuth2.0 和 UAA 来访问 credhub: + +``` +spring: + profiles: + active: credhub + cloud: + config: + server: + credhub: + url: https://credhub:8844 + oauth2: + registration-id: credhub-client + security: + oauth2: + client: + registration: + credhub-client: + provider: uaa + client-id: credhub_config_server + client-secret: asecret + authorization-grant-type: client_credentials + provider: + uaa: + token-uri: https://uaa:8443/oauth/token +``` + +| |所使用的 UAA 客户机 ID 应有`credhub.read`作为作用域。| +|---|-----------------------------------------------------------| + +#### [复合环境存储库](#composite-environment-repositories) + +在某些场景中,你可能希望从多个环境存储库中提取配置数据。为此,你可以在配置服务器的应用程序属性或 YAML 文件中启用`composite`配置文件。例如,如果你希望从一个 Subversion 存储库以及两个 Git 存储库中提取配置数据,那么可以为配置服务器设置以下属性: + +``` +spring: + profiles: + active: composite + cloud: + config: + server: + composite: + - + type: svn + uri: file:///path/to/svn/repo + - + type: git + uri: file:///path/to/rex/git/repo + - + type: git + uri: file:///path/to/walter/git/repo +``` + +使用此配置,优先级由`composite`键下列出存储库的顺序决定。在上面的示例中,首先列出了 Subversion 存储库,因此在 Subversion 存储库中找到的值将覆盖在一个 Git 存储库中为相同属性找到的值。在`rex`Git 存储库中找到的值将被用于在`walter`Git 存储库中为相同属性找到的值之前。 + +如果只想从不同类型的存储库中提取配置数据,那么可以在配置服务器的应用程序属性或 YAML 文件中启用相应的配置文件,而不是`composite`配置文件。例如,如果希望从单个 Git 存储库和单个 HashiCorpVault 服务器中提取配置数据,则可以为配置服务器设置以下属性: + +``` +spring: + profiles: + active: git, vault + cloud: + config: + server: + git: + uri: file:///path/to/git/repo + order: 2 + vault: + host: 127.0.0.1 + port: 8200 + order: 1 +``` + +使用此配置,可以通过`order`属性来确定优先级。你可以使用`order`属性来指定所有存储库的优先级顺序。`order`属性的数值越低,它的优先级就越高。存储库的优先级顺序有助于解决包含相同属性的值的存储库之间的任何潜在冲突。 + +| |如果你的组合环境像前面的示例一样包含一个 Vault 服务器,那么你必须在向配置服务器提出的每个请求中都包含一个 Vault 令牌。见[Vault Backend](#vault-backend)。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |当从环境存储库检索值时,任何类型的失败都会导致整个复合环境失败。
如果你希望在存储库失败时继续进行复合,则可以将`spring.cloud.config.server.failOnCompositeError`设置为`false`。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |在使用复合环境时,所有存储库都包含相同的标签是很重要的,
如果你的环境与前面示例中的环境类似,并且你使用`master`标签请求配置数据,但是 Subversion 存储库不包含一个名为`master`的分支,整个请求都失败了。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [自定义复合环境存储库](#_custom_composite_environment_repositories) + +除了使用 Spring 云中的一个环境存储库外,还可以提供你自己的`EnvironmentRepository` Bean 以作为复合环境的一部分。要做到这一点,你的 Bean 必须实现`EnvironmentRepository`接口。如果希望在复合环境中控制自定义`EnvironmentRepository`的优先级,还应该实现`Ordered`接口并覆盖`getOrdered`方法。如果你没有实现`Ordered`接口,那么你的`EnvironmentRepository`将获得最低的优先级。 + +#### [属性重写](#property-overrides) + +配置服务器具有“重写”功能,允许操作员向所有应用程序提供配置属性。使用普通 Spring 引导挂钩的应用程序不会意外地更改重写的属性。要声明重写,请将名称-值对的映射添加到`spring.cloud.config.server.overrides`,如下例所示: + +``` +spring: + cloud: + config: + server: + overrides: + foo: bar +``` + +前面的示例使所有配置客户机的应用程序读取`foo=bar`,这与它们自己的配置无关。 + +| |配置系统不能强制应用程序以任何特定方式使用配置数据。
因此,重写是不可执行的。
但是,它们确实为 Spring 云配置客户机提供了有用的默认行为。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |通常, Spring 具有`${}`的环境占位符可以通过使用反斜杠来转义(并在客户端上解析),以转义`Spring 云配置 +========== + +Spring 云配置为分布式系统中的外部化配置提供服务器端和客户端支持。有了 Config 服务器,你就有了一个中心位置来管理跨所有环境的应用程序的外部属性。客户机和服务器上的概念都映射到 Spring `Environment`和`PropertySource`的抽象,因此它们非常适合 Spring 应用程序,但可以用于运行在任何语言中的任何应用程序。当应用程序通过部署管道从开发到测试再到生产时,你可以管理这些环境之间的配置,并确保应用程序在迁移时拥有运行它们所需的一切。服务器存储后端的默认实现使用 Git,因此它很容易支持配置环境的标记版本,并且可以访问用于管理内容的各种工具。添加替代实现并将其插入 Spring 配置中是很容易的。 + +[Quick Start](#_quick_start) +---------- + +这个快速启动同时使用了 Spring Cloud Config 服务器的服务器和客户端。 + +首先,启动服务器,如下所示: + +``` +$ cd spring-cloud-config-server +$ ../mvnw spring-boot:run +``` + +服务器是一个 Spring 引导应用程序,因此如果你愿意,可以从 IDE 运行它(主类是`ConfigServerApplication`)。 + +下一步测试一个客户机,如下所示: + +``` +$ curl localhost:8888/foo/development +{ + "name": "foo", + "profiles": [ + "development" + ] + .... + "propertySources": [ + { + "name": "https://github.com/spring-cloud-samples/config-repo/foo-development.properties", + "source": { + "bar": "spam", + "foo": "from foo development" + } + }, + { + "name": "https://github.com/spring-cloud-samples/config-repo/foo.properties", + "source": { + "foo": "from foo props", + "democonfigclient.message": "hello spring io" + } + }, + .... +``` + +定位属性源的默认策略是克隆一个 Git 存储库(at`spring.cloud.config.server.git.uri`),并使用它初始化一个 mini`SpringApplication`。迷你应用程序的`Environment`用于枚举属性源并在 JSON 端点上发布它们。 + +HTTP 服务具有以下形式的资源: + +``` +/{application}/{profile}[/{label}] +/{application}-{profile}.yml +/{label}/{application}-{profile}.yml +/{application}-{profile}.properties +/{label}/{application}-{profile}.properties +``` + +例如: + +``` +curl localhost:8888/foo/development +curl localhost:8888/foo/development/master +curl localhost:8888/foo/development,db/master +curl localhost:8888/foo-development.yml +curl localhost:8888/foo-db.properties +curl localhost:8888/master/foo-db.properties +``` + +其中`application`被注入为`spring.config.name`中的`spring.config.name`(在常规 Spring 引导应用程序中通常`application`),`profile`是一个活动配置文件(或逗号分隔的属性列表),`label`是一个可选的 git 标签(默认为`master`)。 + +Spring 云配置服务器从各种来源获取远程客户端的配置。下面的示例从 Git 存储库(必须提供)获取配置,如下面的示例所示: + +``` +spring: + cloud: + config: + server: + git: + uri: https://github.com/spring-cloud-samples/config-repo +``` + +其他来源包括任何与 JDBC 兼容的数据库、Subversion、HashiC或pVault、Credhub 和本地文件系统。 + +### [客户端使用](#_client_side_usage) + +要在应用程序中使用这些特性,你可以将其构建为一个依赖于 Spring-cloud-config-client 的 Spring 引导应用程序(例如,请参见 config-client 或示例应用程序的测试用例)。添加依赖项最方便的方法是使用 Spring 引导启动器`org.springframework.cloud:spring-cloud-starter-config`。还有一个用于 Maven 用户的父 POM 和 BOM(` Spring-cloud-starter-parent`),以及用于 Gradle 和 Spring CLI 用户的 Spring IO 版本管理属性文件。下面的示例显示了典型的 Maven 配置: + +POM.xml + +``` + + org.springframework.boot + spring-boot-starter-parent + {spring-boot-docs-version} + + + + + + + org.springframework.cloud + spring-cloud-dependencies + {spring-cloud-version} + pom + import + + + + + + + org.springframework.cloud + spring-cloud-starter-config + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + +``` + +现在,你可以创建一个标准的 Spring 启动应用程序,例如下面的 HTTP 服务器: + +``` +@SpringBootApplication +@RestController +public class Application { + + @RequestMapping("/") + public String home() { + return "Hello World!"; + } + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} +``` + +当此 HTTP 服务器运行时,它会从端口 8888 上的默认本地配置服务器(如果正在运行)获取外部配置。要修改启动行为,可以使用`应用程序.属性`更改配置服务器的位置,如下例所示: + +``` +spring.config.import=optional:configserver:http://myconfigserver.com +``` + +默认情况下,如果没有设置应用程序名称,将使用`application`。要修改名称,可以将以下属性添加到`application.properties`文件中: + +``` +spring.application.name: myapp +``` + +| |在设置属性`${spring.application.name}`时,不要在应用程序名称前加上保留的单词`application-`,以防止解决正确的属性源问题。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +配置服务器属性在`/env`端点中显示为高优先级属性源,如下面的示例所示。 + +``` +$ curl localhost:8080/env +{ + "activeProfiles": [], + { + "name": "servletContextInitParams", + "properties": {} + }, + { + "name": "configserver:https://github.com/spring-cloud-samples/config-repo/foo.properties", + "properties": { + "foo": { + "value": "bar", + "origin": "Config Server https://github.com/spring-cloud-samples/config-repo/foo.properties:2:12" + } + } + }, + ... +} +``` + +一个名为`configserver:/`的属性源包含`foo`属性,其值为`bar`。 + +| |属性源名称中的 URL 是 Git 存储库,而不是 Config Server URL。| +|---|-------------------------------------------------------------------------------------| + +| |如果使用 Spring Cloud Config Client,则需要设置`spring.config.import`属性,以便绑定到 Config Server。你可以阅读有关它的更多信息[in the Spring Cloud Config Reference Guide](https://docs.spring.io/spring-cloud-config/docs/current/reference/html/#config-data-import)。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[Spring Cloud Config Server](#_spring_cloud_config_server) +---------- + +Spring Cloud Config Server 提供了用于外部配置(名称-值对或等效的 YAML 内容)的基于 HTTP 资源的 API。通过使用`@EnableConfigServer`注释,服务器可嵌入到 Spring 引导应用程序中。因此,以下应用程序是一个配置服务器: + +configserver.java + +``` +@SpringBootApplication +@EnableConfigServer +public class ConfigServer { + public static void main(String[] args) { + SpringApplication.run(ConfigServer.class, args); + } +} +``` + +像所有 Spring 启动应用程序一样,它默认情况下在 8080 端口上运行,但你可以通过各种方式将其切换到更传统的 8888 端口。最简单的方法是用`spring.config.name=configserver`启动它(在配置服务器 jar 中有一个`configserver.yml`)。另一种方法是使用自己的`application.properties`,如下例所示: + +application.properties + +``` +server.port: 8888 +spring.cloud.config.server.git.uri: file://${user.home}/config-repo +``` + +其中`${user.home}/config-repo`是一个包含 YAML 和 Properties 文件的 Git 存储库。 + +| |在 Windows 上,如果文件 URL 是绝对的,并带有驱动器前缀,则需要在文件 URL 中添加一个额外的“/”(例如,`[文件:///${user.home}/config-repo](file:///${user.home}/config-repo)`)。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |下面的清单显示了在前面的示例中创建 Git 存储库的方法:

``
$cd$HOME
$mkdir$HOME
$cd config-repo
$git init<132"/>$git init<133"/>$echo info.foo:bar>properties$git add-a.gt r=”135“/>$git properties-m”/>你应该在生产中使用服务器来托管你的配置存储库。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |如果只保存文本文件,那么配置存储库的初始克隆将是快速有效的。
如果存储二进制文件,特别是大的二进制文件,你可能会在第一次请求配置时遇到延迟,或者在服务器中遇到内存不足的错误。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [环境存储库](#_environment_repository) + +你应该将配置服务器的配置数据存储在哪里?管理此行为的策略是`EnvironmentRepository`,服务于`Environment`对象。这`Environment`是从 Spring `Environment`的域的浅拷贝(包括以`propertySources`为主要特征)。`Environment`资源由三个变量参数化: + +* `{application}`,它映射到客户端的`spring.application.name`。 + +* `{profile}`,它映射到客户机上的`spring.profiles.active`(以逗号分隔的列表)。 + +* `{label}`,这是一个服务器端特性,标记了一组“版本控制的”配置文件。 + +存储库实现的行为通常类似于 Spring 引导应用程序,从等于`spring.config.name`参数的`{application}`和等于`spring.profiles.active`参数的`{profiles}`中加载配置文件。配置文件的优先规则也与常规 Spring 引导应用程序中的规则相同:活动配置文件优先于默认值,并且,如果有多个配置文件,则最后一个优先(类似于将条目添加到`Map`)。 + +以下示例客户机应用程序具有此引导程序配置: + +``` +spring: + application: + name: foo + profiles: + active: dev,mysql +``` + +(与通常的 Spring 引导应用程序一样,这些属性也可以通过环境变量或命令行参数来设置)。 + +如果存储库是基于文件的,那么服务器将从`application.yml`(所有客户机之间共享)和(以`foo.yml`为准)创建一个“环境”。如果 YAML 文件中有指向 Spring 配置文件的文档,则应用这些文档的优先级更高(按所列配置文件的顺序排列)。如果存在特定于配置文件的 YAML(或 Properties)文件,那么这些文件的应用优先级也要高于默认值。更高的优先级表示在`Environment`中列出的`PropertySource`。(这些相同的规则也适用于独立的引导应用程序。 + +你可以将 Spring.cloud.config.server.accept-empty 设置为 false,这样,如果没有找到应用程序,服务器将返回 HTTP404 状态。默认情况下,此标志设置为 true。 + +#### [Git Backend](#_git_backend) + +`EnvironmentRepository`的默认实现使用了 Git 后端,这对于管理升级和物理环境以及审核更改非常方便。要更改存储库的位置,可以在配置服务器中设置`spring.cloud.config.server.git.uri`配置属性(例如在`application.yml`中)。如果你使用`file:`前缀对它进行设置,那么它应该在本地存储库中工作,这样你就可以在没有服务器的情况下快速轻松地启动它。然而,在这种情况下,服务器直接在本地存储库上操作,而不克隆它(如果它不是裸露的,那也没关系,因为配置服务器从不对“远程”存储库进行更改)。要扩展配置服务器并使其高度可用,你需要让服务器的所有实例指向同一个存储库,这样只有共享文件系统才能工作。即使在这种情况下,对于共享文件系统存储库也最好使用`ssh:`协议,这样服务器就可以克隆它并使用本地工作副本作为缓存。 + +这个存储库实现将 HTTP 资源的`{label}`参数映射到一个 Git 标签(提交 ID、分支名称或标记)。如果 Git 分支或标记名包含斜杠,那么 HTTP URL 中的标签应该使用特殊字符串`(_)`来指定(以避免与其他 URL 路径产生歧义)。例如,如果标签是`foo/bar`,替换斜杠将导致以下标签:`foo(_)bar`。特殊字符串`(_)`的包含也可以应用于`{application}`参数。如果你使用命令行客户机(如 curl),请小心 URL 中的括号——你应该用单引号(“”)将它们从 shell 中转出。 + +##### [跳过 SSL 证书验证](#_skipping_ssl_certificate_validation) + +通过将`git.SkipsslValidation`属性设置为`true`(默认设置为`false`),可以禁用配置服务器对 Git 服务器的 SSL 证书的验证。 + +``` +spring: + cloud: + config: + server: + git: + uri: https://example.com/my/repo + skipSslValidation: true +``` + +##### [设置 HTTP 连接超时](#_setting_http_connection_timeout) + +你可以配置配置服务器等待获得 HTTP 连接的时间(以秒为单位)。使用`git.timeout`属性。 + +``` +spring: + cloud: + config: + server: + git: + uri: https://example.com/my/repo + timeout: 4 +``` + +##### [git uri 中的占位符](#_placeholders_in_git_uri) + +Spring Cloud Config Server 支持带有`{application}`和`{profile}`占位符的 Git Repository URL(如果需要的话,还支持`{label}`,但请记住该标签无论如何都是作为 Git 标签应用的)。因此,你可以使用类似于以下结构的结构来支持“每个应用程序一个存储库”策略: + +``` +spring: + cloud: + config: + server: + git: + uri: https://github.com/myorg/{application} +``` + +你还可以通过使用类似的模式(但使用“{profile}”)来支持“每个配置文件一个存储库”策略。 + +此外,使用`{application}`参数中的特殊字符串“(\_)”可以启用对多个组织的支持,如以下示例所示: + +``` +spring: + cloud: + config: + server: + git: + uri: https://github.com/{application} +``` + +其中`{application}`在请求时以以下格式提供:`organization(_)application`。 + +##### [模式匹配和多个存储库](#_pattern_matching_and_multiple_repositories) + +Spring 云配置还包括支持在应用程序和配置文件名称上与模式匹配的更复杂的需求。模式格式是用逗号分隔的带有通配符的`{application}/{profile}`名称列表(请注意,可能需要引用以通配符开头的模式),如下例所示: + +``` +spring: + cloud: + config: + server: + git: + uri: https://github.com/spring-cloud-samples/config-repo + repos: + simple: https://github.com/simple/config-repo + special: + pattern: special*/dev*,*special*/dev* + uri: https://github.com/special/config-repo + local: + pattern: local* + uri: file:/home/configsvc/config-repo +``` + +如果`{application}/{profile}`不匹配任何模式,则使用在`spring.cloud.config.server.git.uri`下定义的默认 URI。在上面的示例中,对于“简单”存储库,模式是`simple/*`(在所有配置文件中,它只匹配一个名为`simple`的应用程序)。“local”存储库匹配所有配置文件中以`local`开头的所有应用程序名称(`/*`后缀会自动添加到任何没有配置文件匹配器的模式中)。 + +| |“简单”示例中使用的“单行”捷径仅在要设置的唯一属性是 URI 时才能使用。
如果需要设置其他任何内容(凭据、模式等),则需要使用完整的表单。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +repo 中的`pattern`属性实际上是一个数组,因此你可以使用 YAML 数组(或`[0]`、`[1]`等属性文件中的后缀)绑定到多个模式。如果要运行带有多个配置文件的应用程序,你可能需要这样做,如以下示例所示: + +``` +spring: + cloud: + config: + server: + git: + uri: https://github.com/spring-cloud-samples/config-repo + repos: + development: + pattern: + - '*/development' + - '*/staging' + uri: https://github.com/development/config-repo + staging: + pattern: + - '*/qa' + - '*/production' + uri: https://github.com/staging/config-repo +``` + +| |Spring 云猜测,包含配置文件的模式不以`*`结束,这意味着你实际上希望匹配以该模式开始的配置文件列表(因此`*/staging`是`["*/staging", "*/staging,*"]`的快捷方式,以此类推)。
例如,这是常见的,你需要在本地运行“开发”配置文件中的应用程序,还需要远程运行“云”配置文件中的应用程序。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +每个存储库还可以选择将配置文件存储在子目录中,搜索这些目录的模式可以指定为`search-paths`。下面的示例显示了顶层的配置文件: + +``` +spring: + cloud: + config: + server: + git: + uri: https://github.com/spring-cloud-samples/config-repo + search-paths: + - foo + - bar* +``` + +在前面的示例中,服务器在顶层和`foo/`子目录以及名称以`bar`开头的任意子目录中搜索配置文件。 + +默认情况下,服务器在首次请求配置时复制远程存储库。可以将服务器配置为在启动时克隆存储库,如下面的顶级示例所示: + +``` +spring: + cloud: + config: + server: + git: + uri: https://git/common/config-repo.git + repos: + team-a: + pattern: team-a-* + cloneOnStart: true + uri: https://git/team-a/config-repo.git + team-b: + pattern: team-b-* + cloneOnStart: false + uri: https://git/team-b/config-repo.git + team-c: + pattern: team-c-* + uri: https://git/team-a/config-repo.git +``` + +在前面的示例中,服务器在接受任何请求之前,在启动时复制 Team-A 的 config-repo。在请求从存储库进行配置之前,不会克隆所有其他存储库。 + +| |在 Config 服务器启动时设置要克隆的存储库,可以帮助在 Config 服务器启动时快速识别配置错误的配置源(例如无效的存储库 URI),
with`cloneOnStart`配置源未启用,配置服务器可以使用配置错误或无效的配置源成功启动,并且在应用程序从该配置源请求配置之前不会检测到错误。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [Authentication](#_authentication) + +要在远程存储库上使用 HTTP Basic 身份验证,请分别添加`username`和`password`属性(不在 URL 中),如以下示例所示: + +``` +spring: + cloud: + config: + server: + git: + uri: https://github.com/spring-cloud-samples/config-repo + username: trolley + password: strongpassword +``` + +如果你不使用 HTTPS 和用户凭据,那么当你将密钥存储在默认目录中并且 URI 指向 SSH 位置(例如)时,SSH 也应该在开箱即用的情况下工作。在`~/.ssh/known_hosts`文件中存在用于 Git 服务器的条目,并且该条目是`ssh-rsa`格式,这一点很重要。不支持其他格式(如`ecdsa-sha2-nistp256`)。为了避免意外,你应该确保 Git 服务器的`known_hosts`文件中只有一个条目,并且它与你提供给 Config 服务器的 URL 相匹配。如果在 URL 中使用主机名,则希望在`known_hosts`文件中使用该主机名(而不是 IP)。通过使用 JGIT 访问存储库,因此你在其中找到的任何文档都应该适用。HTTPS 代理设置可以设置在`~/.git/config`中,或者(以与任何其他 JVM 进程相同的方式)使用系统属性(`-dhttps.proxyhost` 和`-Dhttps.proxyPort`)。 + +| |如果你不知道你的`~/.git`目录在哪里,请使用`git config --global`来操作设置(例如,`git config --global http.sslVerify false`)。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +JGIT 需要 PEM 格式的 RSA 密钥。下面是一个示例 ssh-keygen(来自 OpenSSH)命令,它将以 CORECT 格式生成一个键: + +``` +ssh-keygen -m PEM -t rsa -b 4096 -f ~/config_server_deploy_key.rsa +``` + +警告:在使用 SSH 密钥时,预期的 SSH 私钥必须以``-----BEGIN RSA PRIVATE KEY-----``开头。如果密钥以``-----BEGIN OPENSSH PRIVATE KEY-----``开始,那么当 Spring-cloud-config 服务器启动时,RSA 密钥将不会加载。这个错误看起来是这样的: + +``` +- Error in object 'spring.cloud.config.server.git': codes [PrivateKeyIsValid.spring.cloud.config.server.git,PrivateKeyIsValid]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [spring.cloud.config.server.git.,]; arguments []; default message []]; default message [Property 'spring.cloud.config.server.git.privateKey' is not a valid private key] +``` + +要纠正上述错误,必须将 RSA 键转换为 PEM 格式。上面提供了一个使用 OpenSSH 生成适当格式的新键的示例。 + +##### [使用 AWS codecommit 进行身份验证](#_authentication_with_aws_codecommit) + +Spring 云配置服务器还支持[AWS CodeCommit](https://docs.aws.amazon.com/codecommit/latest/userguide/welcome.html)身份验证。当从命令行使用 Git 时,AWS Codecommit 使用身份验证助手。此助手不与 JGIT 库一起使用,因此,如果 Git URI 与 AWS Codecommit 模式匹配,那么将创建用于 AWS Codecommit 的 JGit CredentialProvider。AWS codecommit URI 遵循以下模式: + +``` +https//git-codecommit.${AWS_REGION}.amazonaws.com/v1/repos/${repo}. +``` + +如果你提供了带有 AWS CodeCommit URI 的用户名和密码,那么它们必须是提供对存储库访问的[AWS AccessKeyID 和 SecretAccessKey](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSGettingStartedGuide/AWSCredentials.html)。如果你没有指定用户名和密码,那么将使用[AWS 默认凭据提供商链](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html)检索 AccessKeyID 和 SecretAccessKey。 + +如果你的 Git URI 与 codecommit URI 模式匹配(如前所述),则必须在用户名和密码中或在默认凭据提供商链支持的位置之一中提供有效的 AWS 凭据。AWS EC2 实例可以使用[EC2 实例的 IAM 角色](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html)。 + +| |`aws-java-sdk-core` jar 是一个可选的依赖项。
如果`aws-java-sdk-core` jar 不在你的 Classpath 上,则不会创建 AWS 代码提交凭据提供程序,而不管 Git 服务器的 URI 是什么。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [使用 Google Cloud Source 进行身份验证](#_authentication_with_google_cloud_source) + +Spring 云配置服务器还支持针对[Google Cloud Source](https://cloud.google.com/source-repositories/)存储库进行身份验证。 + +如果你的 Git URI 使用`http`或`https`协议,并且域名是`source.developers.google.com`,则将使用 Google Cloud Source 凭据提供商。Google Cloud Source Repository 的 URI 格式为`[https://source.developers.google.com/p/${GCP_PROJECT}/r/${REPO}](https://source.developers.google.com/p/${GCP_PROJECT}/r/${REPO})`。要获得存储库的 URI,请单击 Google Cloud Source UI 中的“Clone”,并选择“手动生成的凭据”。不生成任何凭据,只需复制显示的 URI。 + +Google Cloud Source 凭据提供商将使用 Google Cloud Platform 应用程序的默认凭据。关于如何为系统创建应用程序默认凭据,请参见[Google Cloud SDK 文档](https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login)。这种方法适用于开发环境中的用户帐户和生产环境中的服务帐户。 + +| |`com.google.auth:google-auth-library-oauth2-http`是一个可选的依赖项。
如果`google-auth-library-oauth2-http` jar 不在你的 Classpath 上,则不会创建 Google Cloud Source 凭据提供者,无论 Git 服务器的 URI 是什么。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [使用属性的 Git SSH 配置](#_git_ssh_configuration_using_properties) + +默认情况下, Spring Cloud Config Server 使用的 JGit 库在使用 SSH URI 连接到 Git 存储库时使用诸如`~/.ssh/known_hosts`和`/etc/ssh/ssh_config`等 SSH 配置文件。在云环境中,例如 Cloud Foundry,本地文件系统可能是短暂的,或者不容易访问。对于这些情况,可以使用 Java 属性设置 SSH 配置。为了激活基于属性的 SSH 配置,`spring.cloud.config.server.git.ignoreLocalSshSettings`属性必须设置为`true`,如以下示例所示: + +``` + spring: + cloud: + config: + server: + git: + uri: [email protected]:team/repo1.git + ignoreLocalSshSettings: true + hostKey: someHostKey + hostKeyAlgorithm: ssh-rsa + privateKey: | + -----BEGIN RSA PRIVATE KEY----- + MIIEpgIBAAKCAQEAx4UbaDzY5xjW6hc9jwN0mX33XpTDVW9WqHp5AKaRbtAC3DqX + IXFMPgw3K45jxRb93f8tv9vL3rD9CUG1Gv4FM+o7ds7FRES5RTjv2RT/JVNJCoqF + ol8+ngLqRZCyBtQN7zYByWMRirPGoDUqdPYrj2yq+ObBBNhg5N+hOwKjjpzdj2Ud + 1l7R+wxIqmJo1IYyy16xS8WsjyQuyC0lL456qkd5BDZ0Ag8j2X9H9D5220Ln7s9i + oezTipXipS7p7Jekf3Ywx6abJwOmB0rX79dV4qiNcGgzATnG1PkXxqt76VhcGa0W + DDVHEEYGbSQ6hIGSh0I7BQun0aLRZojfE3gqHQIDAQABAoIBAQCZmGrk8BK6tXCd + fY6yTiKxFzwb38IQP0ojIUWNrq0+9Xt+NsypviLHkXfXXCKKU4zUHeIGVRq5MN9b + BO56/RrcQHHOoJdUWuOV2qMqJvPUtC0CpGkD+valhfD75MxoXU7s3FK7yjxy3rsG + EmfA6tHV8/4a5umo5TqSd2YTm5B19AhRqiuUVI1wTB41DjULUGiMYrnYrhzQlVvj + 5MjnKTlYu3V8PoYDfv1GmxPPh6vlpafXEeEYN8VB97e5x3DGHjZ5UrurAmTLTdO8 + +AahyoKsIY612TkkQthJlt7FJAwnCGMgY6podzzvzICLFmmTXYiZ/28I4BX/mOSe + pZVnfRixAoGBAO6Uiwt40/PKs53mCEWngslSCsh9oGAaLTf/XdvMns5VmuyyAyKG + ti8Ol5wqBMi4GIUzjbgUvSUt+IowIrG3f5tN85wpjQ1UGVcpTnl5Qo9xaS1PFScQ + xrtWZ9eNj2TsIAMp/svJsyGG3OibxfnuAIpSXNQiJPwRlW3irzpGgVx/AoGBANYW + dnhshUcEHMJi3aXwR12OTDnaLoanVGLwLnkqLSYUZA7ZegpKq90UAuBdcEfgdpyi + PhKpeaeIiAaNnFo8m9aoTKr+7I6/uMTlwrVnfrsVTZv3orxjwQV20YIBCVRKD1uX + VhE0ozPZxwwKSPAFocpyWpGHGreGF1AIYBE9UBtjAoGBAI8bfPgJpyFyMiGBjO6z + FwlJc/xlFqDusrcHL7abW5qq0L4v3R+FrJw3ZYufzLTVcKfdj6GelwJJO+8wBm+R + gTKYJItEhT48duLIfTDyIpHGVm9+I1MGhh5zKuCqIhxIYr9jHloBB7kRm0rPvYY4 + VAykcNgyDvtAVODP+4m6JvhjAoGBALbtTqErKN47V0+JJpapLnF0KxGrqeGIjIRV + cYA6V4WYGr7NeIfesecfOC356PyhgPfpcVyEztwlvwTKb3RzIT1TZN8fH4YBr6Ee + KTbTjefRFhVUjQqnucAvfGi29f+9oE3Ei9f7wA+H35ocF6JvTYUsHNMIO/3gZ38N + CPjyCMa9AoGBAMhsITNe3QcbsXAbdUR00dDsIFVROzyFJ2m40i4KCRM35bC/BIBs + q0TY3we+ERB40U8Z2BvU61QuwaunJ2+uGadHo58VSVdggqAo0BSkH58innKKt96J + 69pcVH/4rmLbXdcmNYGm6iu+MlPQk4BUZknHSmVHIFdJ0EPupVaQ8RHT + -----END RSA PRIVATE KEY----- +``` + +下表描述了 SSH 配置属性。 + +| Property Name |备注| +|----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **ignoreLocalSshSettings** |如果`true`,使用基于属性而不是基于文件的 SSH 配置。必须在存储库定义中设置为`spring.cloud.config.server.git.ignoreLocalSshSettings`,**不是**。| +| **privateKey** |有效的 SSH 私钥。如果`ignoreLocalSshSettings`为 true 且 git uri 为 ssh 格式,则必须设置。| +| **hostKey** |有效的 SSH 主机键。如果`hostKeyAlgorithm`也已设置,则必须设置。| +| **hostKeyAlgorithm** |`ssh-dss, ssh-rsa, ecdsa-sha2-nistp256, ecdsa-sha2-nistp384, or ecdsa-sha2-nistp521`中的一个。如果`hostKey`也已设置,则必须设置。| +| **strictHostKeyChecking** |`true`或`false`。如果为假,请忽略使用主机键的错误。| +| **knownHostsFile** |自定义`.known_hosts`文件的位置。| +|**preferredAuthentications**|重写服务器身份验证方法命令.如果服务器在`publickey`方法之前具有键盘交互身份验证,那么这将允许规避登录提示。| + +##### [git 搜索路径中的占位符](#_placeholders_in_git_search_paths) + +Spring Cloud Config Server 还支持带有`{application}`和`{profile}`(如果需要的话,还支持`{label}`)占位符的搜索路径,如以下示例所示: + +``` +spring: + cloud: + config: + server: + git: + uri: https://github.com/spring-cloud-samples/config-repo + search-paths: '{application}' +``` + +前面的列表导致在存储库中搜索与目录(以及顶层)同名的文件。通配符在带有占位符的搜索路径中也是有效的(搜索中包含任何匹配的目录)。 + +##### [强制拉入 Git 存储库](#_force_pull_in_git_repositories) + +正如前面提到的, Spring Cloud Config 服务器复制远程 Git 存储库,以防本地副本变脏(例如,由 OS 进程更改的文件夹内容),使得 Spring Cloud Config 服务器无法从远程存储库更新本地副本。 + +为了解决这个问题,有一个`force-pull`属性,如果本地副本是脏的,该属性将使 Spring Cloud Config 服务器强制从远程存储库中拉出,如以下示例所示: + +``` +spring: + cloud: + config: + server: + git: + uri: https://github.com/spring-cloud-samples/config-repo + force-pull: true +``` + +如果具有多存储库配置,则可以为每个存储库配置`force-pull`属性,如以下示例所示: + +``` +spring: + cloud: + config: + server: + git: + uri: https://git/common/config-repo.git + force-pull: true + repos: + team-a: + pattern: team-a-* + uri: https://git/team-a/config-repo.git + force-pull: true + team-b: + pattern: team-b-* + uri: https://git/team-b/config-repo.git + force-pull: true + team-c: + pattern: team-c-* + uri: https://git/team-a/config-repo.git +``` + +| |`force-pull`属性的默认值是`false`。| +|---|-------------------------------------------------------| + +##### [删除 Git 存储库中未跟踪的分支](#_deleting_untracked_branches_in_git_repositories) + +由于 Spring Cloud Config 服务器在将分支检查到本地 repo(例如通过标签获取属性)之后具有远程 Git 存储库的克隆,因此它将永远保留该分支,或者直到下一个服务器重新启动(这将创建新的本地 repo)。因此,可能存在这样一种情况,即远程分支被删除,但其本地副本仍可用于获取。而如果 Spring Cloud Config Server Client Service 以`--spring.cloud.config.label=deletedRemoteBranch,master`开始,它将从`deletedRemoteBranch`本地分支获取属性,而不是从`master`获取属性。 + +为了保持本地存储库分支的清理和到远程-`deleteUntrackedBranches`属性可以被设置。它将使 Spring 云配置服务器**力**从本地存储库中删除未跟踪的分支。示例: + +``` +spring: + cloud: + config: + server: + git: + uri: https://github.com/spring-cloud-samples/config-repo + deleteUntrackedBranches: true +``` + +| |`deleteUntrackedBranches`属性的默认值是`false`。| +|---|--------------------------------------------------------------------| + +##### [Git 刷新率](#_git_refresh_rate) + +你可以通过使用`spring.cloud.config.server.git.refreshRate`来控制配置服务器多久会从你的 Git 后端获取更新的配置数据。此属性的值以秒为单位指定。默认情况下,该值为 0,这意味着配置服务器将在每次请求时从 Git Repo 获取更新的配置。 + +##### [Default Label](#_default_label) + +Git 使用的默认标签是`main`。如果你没有设置`spring.cloud.config.server.git.defaultLabel`,并且一个名为`main`的分支不存在,则默认情况下,配置服务器还将尝试检出一个名为`master`的分支。如果你想禁用回退分支行为,可以将 ` Spring.cloud.config.server.git.trymasterbranch` 设置为`false`。 + +#### [版本控制后端文件系统的使用](#_version_control_backend_filesystem_use) + +| |使用基于 VCS 的后端,文件将被签出或克隆到本地文件系统中,
默认情况下,它们被放入系统临时目录中,前缀为`config-repo-`,例如,在 Linux 上,
,可能是`/tmp/config-repo-`。
某些操作系统[定期清理](https://serverfault.com/questions/377348/when-does-tmp-get-cleared/377349#377349)临时目录。
这可能会导致意想不到的行为,例如丢失属性。,
为了避免这个问题,将`spring.cloud.config.server.git.basedir`或`spring.cloud.config.server.svn.basedir`设置为不驻留在系统临时结构中的目录,从而更改 Config 服务器使用的目录。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [文件系统后端](#_file_system_backend) + +配置服务器中还有一个“本机”配置文件,它不使用 Git,而是从本地 Classpath 或文件系统(你想用`spring.cloud.config.server.native.searchLocations`指向的任何静态 URL)加载配置文件。要使用本机配置文件,使用`spring.profiles.active=native`启动配置服务器。 + +| |记住对文件资源使用`file:`前缀(不带前缀的默认情况通常是 Classpath)。
与任何 Spring 引导配置一样,你可以嵌入`${}`-风格的环境占位符,但请记住,Windows 中的绝对路径需要额外的`/`(例如,`[file:///${user.home}/config-repo](file:///${user.home}/config-repo)`)。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |`searchLocations`的默认值与本地 Spring 引导应用程序(即`[classpath:/, classpath:/config,
file:./, file:./config]`)相同。
这不会将来自服务器的`application.properties`公开给所有客户端,因为服务器中存在的任何属性源在发送到客户端之前都会被删除。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |文件系统后端对于快速启动和测试非常有用。
要在生产中使用它,你需要确保文件系统是可靠的,并在配置服务器的所有实例之间共享。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +搜索位置可以包含`{application}`、`{profile}`和`{label}`的占位符。通过这种方式,你可以隔离路径中的目录,并选择对你有意义的策略(例如每个应用程序的子目录或每个配置文件的子目录)。 + +如果在搜索位置中不使用占位符,那么这个存储库还会将 HTTP 资源的`{label}`参数附加到搜索路径的后缀中,因此,属性文件是从每个搜索位置**和**一个与标签同名的子目录加载的(在 Spring 环境中,标记属性优先)。因此,不带占位符的默认行为与添加以`/{label}/`结尾的搜索位置相同。例如,`file:/tmp/config`与`file:/tmp/config,file:/tmp/config/{label}`相同。可以通过设置`spring.cloud.config.server.native.addLabelLocations=false`来禁用此行为。 + +#### [Vault Backend](#vault-backend) + +Spring Cloud Config Server 还支持[Vault](https://www.vaultproject.io)作为后端。 + +Vault 是一种安全访问机密的工具。秘密是你想要严格控制访问的任何内容,例如 API 密钥、密码、证书和其他敏感信息。Vault 在提供严格的访问控制和记录详细的审计日志的同时,为任何秘密提供了统一的接口。 + +有关 Vault 的更多信息,请参见[跳马快速启动指南](https://learn.hashicorp.com/vault/?track=getting-started#getting-started)。 + +要使 Config 服务器能够使用 Vault 后端,你可以使用`vault`配置文件运行你的 Config 服务器。例如,在配置服务器的`application.properties`中,可以添加`spring.profiles.active=vault`。 + +默认情况下, Spring Cloud Config Server 使用基于令牌的身份验证来从 Vault 获取配置。Vault 还支持其他身份验证方法,如 Approle、LDAP、JWT、CloudFoundry、Kubernetes Auth。为了使用除令牌或 X-Config-Token 头以外的任何身份验证方法,我们需要在 Classpath 上具有 Spring Vault core,以便 Config 服务器可以将身份验证委派给该库。请将以下依赖项添加到你的配置服务器应用程序中。 + +`Maven (POM.xml)` + +``` + + + org.springframework.vault + spring-vault-core + + +``` + +`Gradle (build.gradle)` + +``` +dependencies { + implementation "org.springframework.vault:spring-vault-core" +} +``` + +默认情况下,配置服务器假定你的 Vault 服务器运行在`[http://127.0.0.1:8200](http://127.0.0.1:8200)`。它还假定后端的名称是`secret`,键是`application`。所有这些默认值都可以在配置服务器的`application.properties`中进行配置。下表描述了可配置的保险库属性: + +|姓名|Default Value| +|-----------------|-------------| +|主机| 127.0.0.1 | +|港口| 8200 | +|方案| http | +|后端| secret | +|DefaultKey| application | +|Profileseparator| , | +|KVVersion| 1 | +|skipSslValidation| false | +|超时| 5 | +|名称空间| null | + +| |前一个表中的所有属性必须使用`spring.cloud.config.server.vault`作为前缀,或者放在复合配置的正确的保险库部分。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +所有可配置的属性都可以在`org.springframework.cloud.config.server.environment.VaultEnvironmentProperties`中找到。 + +| |Vault0.10.0 引入了一个版本控制的键值后端(k/v 后端版本 2),该版本公开了与早期版本不同的 API,现在它需要在挂载路径和实际上下文路径之间设置`data/`,并在`data`对象中包装秘密。设置`spring.cloud.config.server.vault.kv-version=2`将考虑到这一点。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +可选地,存在对 Vault Enterprise`X-Vault-Namespace`头的支持。要将它发送到 Vault,请设置`namespace`属性。 + +在配置服务器运行时,你可以向服务器发出 HTTP 请求,以便从保险库后端取回值。要做到这一点,你需要为你的保险库服务器提供一个令牌。 + +首先,在保险库中放置一些数据,如以下示例所示: + +``` +$ vault kv put secret/application foo=bar baz=bam +$ vault kv put secret/myapp foo=myappsbar +``` + +其次,向配置服务器发出 HTTP 请求,以检索这些值,如下例所示: + +`$ curl -X "GET" "http://localhost:8888/myapp/default" -H "X-Config-Token: yourtoken"` + +你应该会看到类似于以下内容的响应: + +``` +{ + "name":"myapp", + "profiles":[ + "default" + ], + "label":null, + "version":null, + "state":null, + "propertySources":[ + { + "name":"vault:myapp", + "source":{ + "foo":"myappsbar" + } + }, + { + "name":"vault:application", + "source":{ + "baz":"bam", + "foo":"bar" + } + } + ] +} +``` + +客户机提供必要的身份验证以让 Config Server 与 Vault 对话的默认方式是设置 X-Config-Token 头。但是,你可以通过设置与 Spring Cloud Vault 相同的配置属性,省略标题并在服务器中配置身份验证。要设置的属性是`spring.cloud.config.server.vault.authentication`。应该将其设置为受支持的身份验证方法之一。你可能还需要设置特定于你使用的身份验证方法的其他属性,方法是使用与`spring.cloud.vault`相同的属性名称,而不是使用`spring.cloud.config.server.vault`前缀。有关更多详细信息,请参见[Spring Cloud Vault Reference Guide](https://cloud.spring.io/spring-cloud-vault/reference/html/#vault.config.authentication)。 + +| |如果省略了 x-config-token 头并使用服务器属性来设置身份验证,则 Config 服务器应用程序需要对 Spring Vault 有一个额外的依赖项,以启用额外的身份验证选项。
有关如何添加该依赖项,请参见[Spring Vault Reference Guide](https://docs.spring.io/spring-vault/docs/current/reference/html/#dependencies)。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [多属性源](#_multiple_properties_sources) + +在使用 Vault 时,你可以为应用程序提供多个属性源。例如,假设你已将数据写入 Vault 中的以下路径: + +``` +secret/myApp,dev +secret/myApp +secret/application,dev +secret/application +``` + +写入`secret/application`的属性可用于[所有使用配置服务器的应用程序](#_vault_server)。名称为`myApp`的应用程序将具有写为`secret/myApp`和`secret/application`的任何属性。当`myApp`启用`dev`配置文件时,写到上述所有路径的属性将对它可用,并且列表中第一个路径中的属性优先于其他路径。 + +#### [通过代理访问后端](#_accessing_backends_through_a_proxy) + +配置服务器可以通过 HTTP 或 HTTPS 代理访问 Git 或 Vault 后端。在`proxy.http`和`proxy.https`下的设置可以控制 Git 或 Vault 的这种行为。这些设置是每个存储库设置的,因此,如果使用[复合环境存储库](#composite-environment-repositories),则必须为组合中的每个后端单独配置代理设置。如果使用的网络需要为 HTTP 和 HTTPS URL 提供单独的代理服务器,则可以为单个后端配置 HTTP 和 HTTPS 代理设置。 + +下表描述了 HTTP 和 HTTPS 代理的代理配置属性。所有这些属性都必须以`proxy.http`或`proxy.https`为前缀。 + +| Property Name |备注| +|-----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **host** |代理的主机。| +| **port** |访问代理的端口。| +|**nonProxyHosts**|配置服务器应该访问代理之外的任何主机.如果同时为`proxy.http.nonProxyHosts`和`proxy.https.nonProxyHosts`提供了值,则将使用`proxy.http`值。| +| **username** |对代理进行身份验证的用户名。如果同时为`proxy.http.username`和`proxy.https.username`提供了值,则将使用`proxy.http`值。| +| **password** |用于对代理进行身份验证的密码。如果同时为`proxy.http.password`和`proxy.https.password`提供了值,则将使用`proxy.http`值。| + +以下配置使用 HTTPS 代理访问 Git 存储库。 + +``` +spring: + profiles: + active: git + cloud: + config: + server: + git: + uri: https://github.com/spring-cloud-samples/config-repo + proxy: + https: + host: my-proxy.host.io + password: myproxypassword + port: '3128' + username: myproxyusername + nonProxyHosts: example.com +``` + +#### [与所有应用程序共享配置](#_sharing_configuration_with_all_applications) + +在所有应用程序之间共享配置取决于你所采用的方法,如以下主题所述: + +* [基于文件的存储库](#spring-cloud-config-server-file-based-repositories) + +* [Vault Server](#spring-cloud-config-server-vault-server) + +##### [基于文件的存储库](#spring-cloud-config-server-file-based-repositories) + +对于基于文件的(Git、SVN 和 Native)存储库,文件名为`application*`(`application.properties’、`application.yml`、`application-*.properties`等)的资源在所有客户端应用程序之间共享。你可以使用这些文件名的资源来配置全局默认值,并在必要时让它们被特定于应用程序的文件覆盖。 + +[属性重写](#property-overrides)特性还可以用于设置全局默认值,占位符应用程序可以在本地覆盖它们。 + +| |对于“本机”配置文件(本地文件系统后端),你应该使用一个不属于服务器自身配置的显式搜索位置。
否则,默认搜索位置中的`application*`资源将被删除,因为它们是服务器的一部分。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [Vault Server](#spring-cloud-config-server-vault-server) + +当使用 Vault 作为后端时,你可以通过在`secret/application`中放置配置来与所有应用程序共享配置。例如,如果你运行下面的 Vault 命令,那么所有使用 Config 服务器的应用程序都将具有它们可用的属性`foo`和`baz`: + +``` +$ vault write secret/application foo=bar baz=bam +``` + +##### [CredHub Server](#_credhub_server) + +当使用 Credhub 作为后端时,你可以通过将配置放置在`/application/`中或将其放置在应用程序的`default`配置文件中来与所有应用程序共享配置。例如,如果你运行下面的 credhub 命令,那么所有使用 Config 服务器的应用程序都将具有它们可用的属性`shared.color1`和`shared.color2`: + +``` +credhub set --name "/application/profile/master/shared" --type=json +value: {"shared.color1": "blue", "shared.color": "red"} +``` + +``` +credhub set --name "/my-app/default/master/more-shared" --type=json +value: {"shared.word1": "hello", "shared.word2": "world"} +``` + +#### [AWS 秘密管理器](#_aws_secrets_manager) + +当使用 AWS Secrets Manager 作为后端时,你可以通过将配置放置在`/application/`中或将其放置在应用程序的`default`配置文件中来与所有应用程序共享配置。例如,如果使用以下键添加秘密,那么所有使用配置服务器的应用程序都将具有它们可用的属性`shared.foo`和`shared.bar`: + +``` +secret name = /secret/application-default/ +``` + +``` +secret value = +{ + shared.foo: foo, + shared.bar: bar +} +``` + +or + +``` +secret name = /secret/application/ +``` + +``` +secret value = +{ + shared.foo: foo, + shared.bar: bar +} +``` + +##### [AWS 参数存储](#_aws_parameter_store) + +当使用 AWS 参数存储作为后端时,你可以通过在`/application`层次结构中放置属性来与所有应用程序共享配置。 + +例如,如果使用以下名称添加参数,那么所有使用配置服务器的应用程序都将具有它们可用的属性`foo.bar`和`fred.baz`: + +``` +/config/application/foo.bar +/config/application-default/fred.baz +``` + +#### [JDBC Backend](#_jdbc_backend) + +Spring 云配置服务器支持 JDBC(关系数据库)作为配置属性的后端。你可以通过向 Classpath 中添加`spring-jdbc`并使用`jdbc`配置文件或通过添加类型`JdbcEnvironmentRepository`的 Bean 来启用此功能。如果你包括对 Classpath 的正确依赖关系(有关该依赖关系的更多详细信息,请参见用户指南),则 Spring 引导将配置数据源。 + +通过将`spring.cloud.config.server.jdbc.enabled`属性设置为`false`,可以禁用`JdbcEnvironmentRepository`的自动配置。 + +数据库需要有一个名为`PROPERTIES`的表,其中的列分别为`APPLICATION`、`PROFILE`和`LABEL`(具有通常的`Environment`含义),加上`KEY`和`VALUE`用于`Properties`样式中的键和值对。在 Java 中,所有字段都是 String 类型的,因此你可以使它们`VARCHAR`具有所需的任何长度。如果属性值来自名为`{application}-{profile}.properties`的 Spring 引导属性文件,则属性值的行为与它们的行为相同,包括所有的加密和解密,这些将作为后处理步骤应用(即,不直接在存储库实现中)。 + +#### [Redis Backend](#_redis_backend) + +Spring 云配置服务器支持 Redis 作为配置属性的后端。你可以通过向[Spring Data Redis](https://spring.io/projects/spring-data-redis)添加依赖项来启用此功能。 + +POM.xml + +``` + + + org.springframework.boot + spring-boot-starter-data-redis + + +``` + +下面的配置使用 Spring data`RedisTemplate`来访问 Redis。我们可以使用`spring.redis.*`属性来覆盖默认的连接设置。 + +``` +spring: + profiles: + active: redis + redis: + host: redis + port: 16379 +``` + +这些属性应该存储为散列中的字段。散列的名称应该与`spring.application.name`的属性或`spring.application.name`和`spring.profiles.active[n]`的连词相同。 + +``` +HMSET sample-app server.port "8100" sample.topic.name "test" test.property1 "property1" +``` + +在运行位于散列上方可见的命令之后,散列应该包含以下带值的键: + +``` +HGETALL sample-app +{ + "server.port": "8100", + "sample.topic.name": "test", + "test.property1": "property1" +} +``` + +| |当未指定配置文件时,将使用`default`。| +|---|----------------------------------------------------| + +#### [AWS S3 Backend](#_aws_s3_backend) + +Spring 云配置服务器支持 AWS S3 作为配置属性的后端。你可以通过向[亚马逊 S3 的 AWS Java SDK](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/examples-s3.html)添加依赖项来启用此功能。 + +POM.xml + +``` + + + com.amazonaws + aws-java-sdk-s3 + + +``` + +以下配置使用 AWS S3 客户端访问配置文件。我们可以使用`spring.cloud.config.server.awss3.*`属性来选择存储配置的桶。 + +``` +spring: + profiles: + active: awss3 + cloud: + config: + server: + awss3: + region: us-east-1 + bucket: bucket1 +``` + +也可以使用`spring.cloud.config.server.awss3.endpoint`将 AWS URL 指定为你的 S3 服务的[覆盖标准端点](https://aws.amazon.com/blogs/developer/using-new-regions-and-endpoints/)。这允许支持 S3 的测试版区域,以及其他与 S3 兼容的存储 API。 + +使用[默认的 AWS 凭据提供商链](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html)找到凭据。支持版本控制和加密的桶,而无需进一步配置。 + +配置文件以`{application}-{profile}.properties`、`{application}-{profile}.yml`或`{application}-{profile}.json`的形式存储在你的 bucket 中。可以提供一个可选的标签来指定文件的目录路径。 + +| |当未指定配置文件时,将使用`default`。| +|---|----------------------------------------------------| + +#### [AWS 参数存储后端](#_aws_parameter_store_backend) + +Spring 云配置服务器支持 AWS 参数存储作为配置属性的后端。你可以通过向[面向 SSM 的 AWS Java SDK](https://github.com/aws/aws-sdk-java/tree/master/aws-java-sdk-ssm)添加依赖项来启用此功能。 + +POM.xml + +``` + + com.amazonaws + aws-java-sdk-ssm + +``` + +以下配置使用 AWS SSM 客户端访问参数。 + +``` +spring: + profiles: + active: awsparamstore + cloud: + config: + server: + awsparamstore: + region: eu-west-2 + endpoint: https://ssm.eu-west-2.amazonaws.com + origin: aws:parameter: + prefix: /config/service + profile-separator: _ + recursive: true + decrypt-values: true + max-results: 5 +``` + +下表描述了 AWS 参数存储配置属性。 + +| Property Name |Required| Default Value |备注| +|---------------------|--------|--------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| **region** | no | |AWS 参数存储客户端要使用的区域。如果没有显式设置,那么 SDK 将尝试使用[默认区域提供器链](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/java-dg-region-selection.html#default-region-provider-chain)来确定要使用的区域。| +| **endpoint** | no | |AWS SSM 客户端入口点的 URL。这可以用来为 API 请求指定一个替代端点。| +| **origin** | no |`aws:ssm:parameter:`|添加到属性源名称中以显示其来源的前缀。| +| **prefix** | no | `/config` |前缀表示从 AWS 参数存储区加载的每个属性的参数层次结构中的 L1 级别。| +|**profile-separator**| no | `-` |将附加的配置文件与上下文名称分隔开的字符串。| +| **recursive** | no | `true` |标志来指示对层次结构中所有 AWS 参数的检索。| +| **decrypt-values** | no | `true` |标志来指示对所有 AWS 参数的检索,并对其值进行解密。| +| **max-results** | no | `10` |AWS 参数存储 API 调用要返回的最大项数。| + +AWS 参数存储 API 凭据是使用[默认凭据提供器链](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html#credentials-default)确定的。已支持版本控制的参数,其默认行为是返回最新版本。 + +| |* 当没有指定应用程序时,`application`是默认值,当没有指定配置文件时,使用`default`。

*`awsparamstore.prefix`的有效值必须以前斜杠开始,然后是一个或多个有效路径段,否则为空,

*`awsparamstore.profile-separator`的有效值只能包含点,破折号和下划线。

*`awsparamstore.max-results`的有效值必须在**[1, 10]**范围内。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [AWS Secrets Manager 后端](#_aws_secrets_manager_backend) + +Spring Cloud Config Server 支持[AWS 秘密管理器](https://aws.amazon.com/secrets-manager/)作为配置属性的后端。你可以通过向[用于 Secrets Manager 的 AWS Java SDK](https://github.com/aws/aws-sdk-java/tree/master/aws-java-sdk-secretsmanager)添加依赖项来启用此功能。 + +POM.xml + +``` + + com.amazonaws + aws-java-sdk-secretsmanager + +``` + +以下配置使用 AWS Secrets Manager 客户端访问机密。 + +``` +spring: + profiles: + active: awssecretsmanager + cloud: + config: + server: + aws-secretsmanager: + region: us-east-1 + endpoint: https://us-east-1.console.aws.amazon.com/ + origin: aws:secrets: + prefix: /secret/foo + profileSeparator: _ +``` + +AWS Secrets Manager API 凭据是使用[默认凭据提供器链](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html#credentials-default)确定的。 + +| |* When no application is specified `application` is the default, and when no profile is specified `default` is used.| +|---|--------------------------------------------------------------------------------------------------------------------| + +#### [CredHub Backend](#_credhub_backend) + +Spring Cloud Config Server 支持[CredHub](https://docs.cloudfoundry.org/credhub)作为配置属性的后端。你可以通过向[Spring CredHub](https://spring.io/projects/spring-credhub)添加依赖项来启用此功能。 + +POM.xml + +``` + + + org.springframework.credhub + spring-credhub-starter + + +``` + +以下配置使用 Mutual TLS 访问 credhub: + +``` +spring: + profiles: + active: credhub + cloud: + config: + server: + credhub: + url: https://credhub:8844 +``` + +这些属性应该以 JSON 的形式存储,例如: + +``` +credhub set --name "/demo-app/default/master/toggles" --type=json +value: {"toggle.button": "blue", "toggle.link": "red"} +``` + +``` +credhub set --name "/demo-app/default/master/abs" --type=json +value: {"marketing.enabled": true, "external.enabled": false} +``` + +所有名称为`spring.cloud.config.name=demo-app`的客户机应用程序都将具有以下可用属性: + +``` +{ + toggle.button: "blue", + toggle.link: "red", + marketing.enabled: true, + external.enabled: false +} +``` + +| |当未指定配置文件时,将使用`default`;当未指定标签时,将使用`master`作为默认值。
注意:添加到`application`的值将被所有应用程序共享。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [OAuth 2.0](#_oauth_2_0) + +你可以使用[OAuth 2.0](https://oauth.net/2/)作为提供者进行身份验证。 + +pom.xml + +``` + + + org.springframework.security + spring-security-config + + + org.springframework.security + spring-security-oauth2-client + + +``` + +以下配置使用 OAuth2.0 和 UAA 来访问 credhub: + +``` +spring: + profiles: + active: credhub + cloud: + config: + server: + credhub: + url: https://credhub:8844 + oauth2: + registration-id: credhub-client + security: + oauth2: + client: + registration: + credhub-client: + provider: uaa + client-id: credhub_config_server + client-secret: asecret + authorization-grant-type: client_credentials + provider: + uaa: + token-uri: https://uaa:8443/oauth/token +``` + +| |所使用的 UAA 客户机 ID 应有`credhub.read`作为作用域。| +|---|-----------------------------------------------------------| + +#### [复合环境存储库](#composite-environment-repositories) + +在某些场景中,你可能希望从多个环境存储库中提取配置数据。为此,你可以在配置服务器的应用程序属性或 YAML 文件中启用`composite`配置文件。例如,如果你希望从一个 Subversion 存储库以及两个 Git 存储库中提取配置数据,那么可以为配置服务器设置以下属性: + +``` +spring: + profiles: + active: composite + cloud: + config: + server: + composite: + - + type: svn + uri: file:///path/to/svn/repo + - + type: git + uri: file:///path/to/rex/git/repo + - + type: git + uri: file:///path/to/walter/git/repo +``` + +使用此配置,优先级由`composite`键下列出存储库的顺序决定。在上面的示例中,首先列出了 Subversion 存储库,因此在 Subversion 存储库中找到的值将覆盖在一个 Git 存储库中为相同属性找到的值。在`rex`Git 存储库中找到的值将被用于在`walter`Git 存储库中为相同属性找到的值之前。 + +如果只想从不同类型的存储库中提取配置数据,那么可以在配置服务器的应用程序属性或 YAML 文件中启用相应的配置文件,而不是`composite`配置文件。例如,如果希望从单个 Git 存储库和单个 HashiCorpVault 服务器中提取配置数据,则可以为配置服务器设置以下属性: + +``` +spring: + profiles: + active: git, vault + cloud: + config: + server: + git: + uri: file:///path/to/git/repo + order: 2 + vault: + host: 127.0.0.1 + port: 8200 + order: 1 +``` + +使用此配置,可以通过`order`属性来确定优先级。你可以使用`order`属性来指定所有存储库的优先级顺序。`order`属性的数值越低,它的优先级就越高。存储库的优先级顺序有助于解决包含相同属性的值的存储库之间的任何潜在冲突。 + +| |如果你的组合环境像前面的示例一样包含一个 Vault 服务器,那么你必须在向配置服务器提出的每个请求中都包含一个 Vault 令牌。见[Vault Backend](#vault-backend)。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |当从环境存储库检索值时,任何类型的失败都会导致整个复合环境失败。
如果你希望在存储库失败时继续进行复合,则可以将`spring.cloud.config.server.failOnCompositeError`设置为`false`。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |在使用复合环境时,所有存储库都包含相同的标签是很重要的,
如果你的环境与前面示例中的环境类似,并且你使用`master`标签请求配置数据,但是 Subversion 存储库不包含一个名为`master`的分支,整个请求都失败了。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### [自定义复合环境存储库](#_custom_composite_environment_repositories) + +除了使用 Spring 云中的一个环境存储库外,还可以提供你自己的`EnvironmentRepository` Bean 以作为复合环境的一部分。要做到这一点,你的 Bean 必须实现`EnvironmentRepository`接口。如果希望在复合环境中控制自定义`EnvironmentRepository`的优先级,还应该实现`Ordered`接口并覆盖`getOrdered`方法。如果你没有实现`Ordered`接口,那么你的`EnvironmentRepository`将获得最低的优先级。 + +#### [属性重写](#property-overrides) + +配置服务器具有“重写”功能,允许操作员向所有应用程序提供配置属性。使用普通 Spring 引导挂钩的应用程序不会意外地更改重写的属性。要声明重写,请将名称-值对的映射添加到`spring.cloud.config.server.overrides`,如下例所示: + +``` +spring: + cloud: + config: + server: + overrides: + foo: bar +``` + +前面的示例使所有配置客户机的应用程序读取`foo=bar`,这与它们自己的配置无关。 + +| |配置系统不能强制应用程序以任何特定方式使用配置数据。
因此,重写是不可执行的。
但是,它们确实为 Spring 云配置客户机提供了有用的默认行为。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |或`{`。例如,
可解析为`bar`,除非应用程序提供自己的`app.foo`。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |在 YAML 中,你不需要转义反斜杠本身。
但是,在属性文件中,当你在服务器上配置重写时,你确实需要转义反斜杠。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +通过在远程存储库中设置`spring.cloud.config.overrideNone=true`标志(默认值为 false),你可以将客户机中所有重写的优先级更改为更像默认值,让应用程序在环境变量或系统属性中提供它们自己的值。 + +### [健康指标](#_health_indicator) + +Config Server 附带一个健康指示器,用于检查配置的`EnvironmentRepository`是否工作。默认情况下,它向`EnvironmentRepository`请求名为`app`的应用程序、`default`配置文件和`EnvironmentRepository`实现提供的默认标签。 + +你可以将健康指示器配置为检查更多的应用程序以及自定义配置文件和自定义标签,如下例所示: + +``` +spring: + cloud: + config: + server: + health: + repositories: + myservice: + label: mylabel + myservice-dev: + name: myservice + profiles: development +``` + +你可以通过设置`management.health.config.enabled=false`禁用健康指示器。 + +### [Security](#_security) + +你可以以任何对你有意义的方式(从物理网络安全到 OAuth2 承载令牌)保护你的配置服务器,因为 Spring 安全性和 Spring 启动为许多安全安排提供了支持。 + +要使用默认的 Spring 引导配置的 HTTP 基本安全性,在 Classpath 上包括 Spring 安全性(例如,通过`spring-boot-starter-security`)。默认的是`user`的用户名和随机生成的密码。随机密码在实践中是没有用的,因此我们建议你配置密码(通过设置`spring.security.user.password`)并对其进行加密(请参阅下面的操作说明)。 + +### [执行器和安全性](#_actuator_and_security) + +| |一些平台配置健康检查或类似的东西,并指向`/actuator/health`或其他执行器端点。如果 Actuator 不是 Config Server 的依赖项,则对`/actuator/`**would match the config server API `/{application}/{label}` possibly leaking secure information. Remember to add the `spring-boot-starter-actuator` dependency in this case and configure the users such that the user that makes calls to `/actuator/`**的请求没有访问`/{application}/{label}`上的 Config Server API 的权限。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [加密和解密](#_encryption_and_decryption) + +| |要使用加密和解密功能,你需要在你的 JVM 中安装全强度 JCE(默认情况下不包括它)。
你可以从 Oracle 下载“Java Cryptography Extension(JCE)Unlimited Strength Juridictory Policy Files”,并按照安装说明(基本上,你需要用下载的文件替换 JRElib/security 目录中的两个策略文件)。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +如果远程属性源包含加密的内容(以`{cipher}`开头的值),则在通过 HTTP 发送到客户机之前对它们进行解密。这种设置的主要优点是,当属性值“处于静止状态”(例如,在 Git 存储库中)时,它们不需要是纯文本的。如果一个值不能被解密,它将从属性源中删除,并添加一个附加的属性,使用相同的键,但前缀是`invalid`和一个表示“不适用”的值(通常是``)。这在很大程度上是为了防止密码文本被用作密码而意外泄露。 + +如果你为配置客户机应用程序设置了一个远程配置存储库,那么它可能包含一个`application.yml`,类似于以下内容: + +application.yml + +``` +spring: + datasource: + username: dbuser + password: '{cipher}FKSAJDFGYOS8F7GLHAKERGFHLSAJ' +``` + +`application.properties`文件中的加密值不能用引号包装。否则,该值不会被解密。下面的示例显示了可以工作的值: + +application.properties + +``` +spring.datasource.username: dbuser +spring.datasource.password: {cipher}FKSAJDFGYOS8F7GLHAKERGFHLSAJ +``` + +你可以安全地将这个纯文本推送到共享的 Git 存储库,并且秘密密码仍然受到保护。 + +服务器还公开`/encrypt`和`/decrypt`端点(假设这些端点是安全的,并且仅由授权的代理访问)。如果编辑远程配置文件,可以使用配置服务器通过发布到`/encrypt`端点来加密值,如下例所示: + +``` +$ curl localhost:8888/encrypt -s -d mysecret +682bc583f4641835fa2db009355293665d2647dade3375c0ee201de2a49f7bda +``` + +| |如果你正在使用 curl 进行测试,那么使用`--data-urlencode`(而不是`-d`)并在该值前缀进行加密,使用`=`(curl 需要这样做)或设置显式`Content-Type: text/plain`,以确保 curl 在有特殊字符时正确地编码数据(’+’特别棘手)。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |一定不要在加密值中包含任何 curl 命令统计信息,这就是为什么示例使用`-s`选项来使它们保持沉默。将值输出到文件中可以帮助避免此问题。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +逆操作也可以通过`/decrypt`进行(如果服务器配置了对称密钥或全密钥对),如以下示例所示: + +``` +$ curl localhost:8888/decrypt -s -d 682bc583f4641835fa2db009355293665d2647dade3375c0ee201de2a49f7bda +mysecret +``` + +在将加密值放入 YAML 或 Properties 文件之前,在提交并将其推送到远程(可能不安全)存储之前,获取加密值并添加`{cipher}`前缀。 + +`/encrypt`和`/decrypt`端点也都以`/*/{application}/{profiles}`的形式接受路径,当客户端调用主环境资源时,该路径可用于在每个应用程序(名称)和每个配置文件的基础上控制密码学。 + +| |要以这种细粒度的方式控制加密,还必须提供类型`@Bean`的`TextEncryptorLocator`,该类型根据名称和配置文件创建不同的加密器。
默认情况下提供的加密器不会这样做(所有加密都使用相同的密钥)。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +`spring`命令行客户端(安装了 Spring Cloud CLI 扩展)也可用于加密和解密,如以下示例所示: + +``` +$ spring encrypt mysecret --key foo +682bc583f4641835fa2db009355293665d2647dade3375c0ee201de2a49f7bda +$ spring decrypt --key foo 682bc583f4641835fa2db009355293665d2647dade3375c0ee201de2a49f7bda +mysecret +``` + +要在文件中使用密钥(例如用于加密的 RSA 公钥),请在密钥值前加上“@”并提供文件路径,如以下示例所示: + +``` +$ spring encrypt mysecret --key @${HOME}/.ssh/id_rsa.pub +AQAjPgt3eFZQXwt8tsHAVv/QHiY5sI2dRcR+... +``` + +| |`--key`参数是强制性的(尽管有`--`前缀)。| +|---|-----------------------------------------------------------------| + +### [Key Management](#_key_management) + +配置服务器可以使用对称(共享)密钥或非对称(RSA 密钥对)密钥。非对称选择在安全性方面更好,但是使用对称密钥通常更方便,因为它是在`bootstrap.properties`中配置的单个属性值。 + +要配置一个对称密钥,你需要将`encrypt.key`设置为一个秘密字符串(或者使用`ENCRYPT_KEY`环境变量将其排除在纯文本配置文件之外)。 + +| |不能使用`encrypt.key`配置非对称密钥。| +|---|-----------------------------------------------------------| + +要配置非对称密钥,请使用密钥存储库(例如,由 JDK 附带的`keytool`实用程序创建)。密钥存储库属性是`encrypt.keyStore.*`,而`*`等于 + +| Property |说明| +|---------------------------|--------------------------------------------------| +|`encrypt.keyStore.location`|包含`Resource`位置| +|`encrypt.keyStore.password`|持有解锁密钥存储库的密码| +| `encrypt.keyStore.alias` |标识要使用的存储区中的哪个键| +| `encrypt.keyStore.type` |要创建的密钥存储库的类型。默认值为`jks`。| + +加密是用公钥完成的,解密需要私钥。因此,原则上,如果你只想加密(并且准备好自己在本地使用私钥解密这些值),那么你只能在服务器中配置公钥。在实践中,你可能不希望在本地进行解密,因为它将密钥管理过程分散到所有客户机,而不是将其集中在服务器中。另一方面,如果你的配置服务器相对不安全,并且只有少数客户机需要加密的属性,那么它可能是一个有用的选择。 + +### [创建用于测试的密钥库](#_creating_a_key_store_for_testing) + +要创建用于测试的密钥库,你可以使用类似于以下命令的命令: + +``` +$ keytool -genkeypair -alias mytestkey -keyalg RSA \ + -dname "CN=Web Server,OU=Unit,O=Organization,L=City,S=State,C=US" \ + -keypass changeme -keystore server.jks -storepass letmein +``` + +| |当使用 JDK11 或更高版本时,当使用上面的命令时,你可能会得到以下警告。在这种情况下,
可能需要确保`keypass`和`storepass`值匹配。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +``` +Warning: Different store and key passwords not supported for PKCS12 KeyStores. Ignoring user-specified -keypass value. +``` + +在 Classpath(例如)中放置`server.jks`文件,然后在`bootstrap.yml`中为配置服务器创建以下设置: + +``` +encrypt: + keyStore: + location: classpath:/server.jks + password: letmein + alias: mytestkey + secret: changeme +``` + +### [使用多个键和键旋转](#_using_multiple_keys_and_key_rotation) + +除了加密属性值中的`{cipher}`前缀外,配置服务器还在(base64 编码的)密码文本开始前查找零个或更多的`{name:value}`前缀。这些密钥被传递给`TextEncryptorLocator`,它可以执行所需的任何逻辑来为密码定位`TextEncryptor`。如果你已经配置了一个密钥存储库,那么默认定位器将查找由`key`前缀提供的别名的密钥,并使用类似于以下内容的密码文本: + +``` +foo: + bar: `{cipher}{key:testkey}...` +``` + +定位器查找一个名为“TestKey”的键。也可以通过在前缀中使用`{secret:…​}`值来提供秘密。但是,如果没有提供,默认情况是使用 keystore 密码(这是你在构建 keystore 而不指定秘密时获得的密码)。如果你确实提供了一个秘密,则还应该使用自定义`SecretLocator`对该秘密进行加密。 + +当密钥仅用于加密几个字节的配置数据时(也就是说,它们不在其他地方使用),出于加密的原因,几乎不需要旋转密钥。但是,你可能偶尔需要更改密钥(例如,在发生安全漏洞的情况下)。在这种情况下,所有客户机都需要更改其源配置文件(例如,在 Git 中),并在所有密码中使用新的`{key:…​}`前缀。请注意,客户端需要首先检查密钥别名在配置服务器密钥存储库中是否可用。 + +| |如果你想让配置服务器处理所有的加密和解密,`{name:value}`前缀也可以作为纯文本添加到`/encrypt`端点。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [服务加密的属性](#_serving_encrypted_properties) + +有时,你希望客户机在本地解密配置,而不是在服务器中进行解密。在那种情况下,如果你提供`encrypt.*`配置来定位一个密钥,你仍然可以拥有`/encrypt`和`/decrypt`端点,但是你需要通过将`spring.cloud.config.server.encrypt.enabled=false`放置在`bootstrap.[yml|properties]`中来显式地关闭输出属性的解密。如果你不关心端点,那么如果你不配置键或启用的标志,那么它应该可以工作。 + +[提供替代格式](#_serving_alternative_formats) +---------- + +来自环境端点的默认 JSON 格式非常适合 Spring 应用程序使用,因为它直接映射到`Environment`抽象。如果你愿意,可以通过向资源路径添加一个后缀(“.yml”、“.yaml”或“.properties”)来使用与 YAML 或 Java 属性相同的数据。这对于不关心 JSON 端点的结构或它们提供的额外元数据的应用程序来说是有用的(例如,不使用 Spring 的应用程序可能会受益于这种方法的简单性)。 + +YAML 和 Properties 表示有一个额外的标志(作为布尔查询参数提供),以表示源文档(在标准 Spring 形式中)中的占位符应该在呈现之前在输出中解析(如果可能的话)。对于不了解 Spring 占位符约定的消费者来说,这是一个有用的特性。 + +| |在使用 YAML 或 Properties 格式时有一些限制,主要是与元数据的丢失有关,
例如,JSON 被构建为一个有序的属性源列表,其名称与源相关,
YAML 和 Properties 表单合并成一个映射,即使值的原点有多个源,并且原始源文件的名称丢失。
同样,YAML 表示也不一定是备份存储库中 YAML 源的忠实表示。它是由一个平面属性源列表构建的,并且必须对键的形式进行假设。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[提供纯文本](#_serving_plain_text) +---------- + +与使用`Environment`抽象(或 YAML 或 Properties 格式的替代表示形式之一)不同,你的应用程序可能需要针对其环境定制的通用纯文本配置文件。配置服务器通过位于`/{application}/{profile}/{label}/{path}`的附加端点提供这些,其中`application`、`profile`和`label`与常规环境端点具有相同的含义,但是`path`是一个文件名的路径(例如`log.xml`)。该端点的源文件的定位方式与环境端点的定位方式相同。相同的搜索路径用于属性和 YAML 文件。然而,不是聚集所有匹配的资源,而是只返回第一个匹配的资源。 + +在找到资源之后,使用有效的`Environment`对提供的应用程序名称、配置文件和标签解析正常格式的占位符。通过这种方式,资源端点与环境端点紧密集成。 + +| |与用于环境配置的源文件一样,`profile`用于解析文件名。
因此,如果你想要一个配置文件特定的文件,`/*/development/*/logback.xml`可以通过一个名为`logback-development.xml`的文件进行解析(优先于`logback.xml`)。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |如果不想提供`label`并让服务器使用默认标签,则可以提供一个`useDefaultLabel`请求参数。
因此,前面的`default`配置文件示例可以是`/sample/default/nginx.conf?useDefaultLabel`。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +目前, Spring Cloud Config 可以为 Git、SVN、本机后台和 AWS S3 提供明文服务。对 Git、SVN 和本机后台的支持是相同的。AWS S3 的工作原理略有不同。以下几节展示了每一项的工作原理: + +* [Git、SVN 和本机后端](#spring-cloud-config-serving-plain-text-git-svn-native-backends) + +* [AWS S3](#spring-cloud-config-serving-plain-text-aws-s3) + +### [Git、SVN 和本机后端](#spring-cloud-config-serving-plain-text-git-svn-native-backends) + +考虑以下用于 Git 或 SVN 存储库或本机后端的示例: + +``` +application.yml +nginx.conf +``` + +`nginx.conf`可能类似于以下清单: + +``` +server { + listen 80; + server_name ${nginx.server.name}; +} +``` + +`application.yml`可能类似于以下清单: + +``` +nginx: + server: + name: example.com +--- +spring: + profiles: development +nginx: + server: + name: develop.com +``` + +`/sample/default/master/nginx.conf`资源可能如下: + +``` +server { + listen 80; + server_name example.com; +} +``` + +`/sample/development/master/nginx.conf`可能如下: + +``` +server { + listen 80; + server_name develop.com; +} +``` + +### [AWS S3](#spring-cloud-config-serving-plain-text-aws-s3) + +要为 AWS S3 启用纯文本服务,Config Server 应用程序需要包括对 Spring Cloud AWS 的依赖。有关如何设置该依赖项的详细信息,请参见[Spring Cloud AWS Reference Guide](https://cloud.spring.io/spring-cloud-static/spring-cloud-aws/2.1.3.RELEASE/single/spring-cloud-aws.html#_spring_cloud_aws_maven_dependency_management)。然后需要配置 Spring 云 AWS,如[Spring Cloud AWS Reference Guide](https://cloud.spring.io/spring-cloud-static/spring-cloud-aws/2.1.3.RELEASE/single/spring-cloud-aws.html#_configuring_credentials)中所述。 + +### [解密纯文本](#_decrypting_plain_text) + +默认情况下,纯文本文件中的加密值不会被解密。为了启用对纯文本文件的解密,请在`bootstrap.[yml|properties]`中设置`spring.cloud.config.server.encrypt.enabled=true`和`spring.cloud.config.server.encrypt.plainTextEncrypt=true`。 + +| |解密纯文本文件仅支持 YAML、JSON 和 Properties 文件扩展名。| +|---|---------------------------------------------------------------------------------------------| + +如果启用了此功能,并且请求了不受支持的文件扩展,则文件中的任何加密值都不会被解密。 + +[嵌入配置服务器](#_embedding_the_config_server) +---------- + +配置服务器作为独立应用程序运行得最好。但是,如果需要,你可以将其嵌入到另一个应用程序中。要做到这一点,请使用`@EnableConfigServer`注释。在这种情况下,一个名为`spring.cloud.config.server.bootstrap`的可选属性是有用的。它是一个标志,指示服务器是否应该从自己的远程存储库中配置自己。默认情况下,标志是关闭的,因为它可能会延迟启动。然而,当嵌入到另一个应用程序中时,以与任何其他应用程序相同的方式初始化是有意义的。当将`spring.cloud.config.server.bootstrap`设置为`true`时,还必须使用[复合环境存储库配置](#composite-environment-repositories)。例如 + +``` +spring: + application: + name: configserver + profiles: + active: composite + cloud: + config: + server: + composite: + - type: native + search-locations: ${HOME}/Desktop/config + bootstrap: true +``` + +| |如果使用 bootstrap 标志,配置服务器需要在`bootstrap.yml`中配置其名称和存储库 URI。| +|---|-------------------------------------------------------------------------------------------------------------------------| + +要更改服务器端点的位置,可以(可选地)设置`spring.cloud.config.server.prefix`(例如,`/config`),以在前缀下提供资源。前缀应该以`/`开始,而不是结束。它被应用到配置服务器中的`@RequestMappings`(即在 Spring 引导`server.servletPath`和`server.contextPath`前缀下)。 + +如果你想直接从后端存储库(而不是从配置服务器)读取应用程序的配置,那么基本上需要一个没有端点的嵌入式配置服务器。你可以通过不使用`@EnableConfigServer`注释(set`spring.cloud.config.server.bootstrap=true`)来完全关闭端点。 + +[Push Notifications and Spring Cloud Bus](#_push_notifications_and_spring_cloud_bus) +---------- + +许多源代码存储库提供商(如 GitHub、GitLab、Gitea、Gitee、GOGS 或 Bitbucket)通过 Webhook 通知你存储库中的更改。你可以通过提供者的用户界面将 Webhook 配置为一个 URL 和一组你感兴趣的事件。例如,[Github](https://developer.github.com/v3/activity/events/types/#pushevent)使用发送到 Webhook 的 post,其 JSON 主体包含提交列表,并将头设置为`push`。如果你在`spring-cloud-config-monitor`库上添加了一个依赖项,并激活了配置服务器中的 Spring 云总线,那么将启用一个`/monitor`端点。 + +当 Webhook 被激活时,配置服务器发送一个`RefreshRemoteApplicationEvent`,目标是它认为可能已经更改的应用程序。可以对变化检测制定策略。但是,默认情况下,它会查找与应用程序名称匹配的文件中的更改(例如,`foo.properties`是针对`foo`应用程序的,而`application.properties`是针对所有应用程序的)。当你想要重写该行为时使用的策略是`PropertyPathNotificationExtractor`,它接受请求头和主体作为参数,并返回已更改的文件路径列表。 + +对于 GitHub、GitLab、Gitea、Gitee、Gogs 或 Bitbucket,默认配置是开箱即用的。除了来自 GitHub、GitLab、Gitee 或 BitBucket 的 JSON 通知外,你还可以通过使用`/monitor`模式中的表单编码主体参数发布到`path={application}`来触发更改通知。这样做会向匹配`{application}`模式(其中可能包含通配符)的应用程序广播。 + +| |只有当`spring-cloud-bus`在配置服务器和客户机应用程序中都被激活时,才会传输`RefreshRemoteApplicationEvent`。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |默认配置还会检测本地 Git 存储库中的文件系统更改。在这种情况下,不使用 Webhook。但是,一旦编辑配置文件,就会广播刷新。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[Spring Cloud Config Client](#_spring_cloud_config_client) +---------- + +Spring 引导应用程序可以立即利用 Spring 配置服务器(或由应用程序开发人员提供的其他外部属性源)。它还获取了一些与`Environment`更改事件相关的其他有用特性。 + +### [Spring Boot Config Data Import](#config-data-import) + +Spring Boot2.4 引入了一种通过`spring.config.import`属性导入配置数据的新方法。这是现在绑定到 Config Server 的默认方式。 + +要可选地连接到配置服务器,请在应用程序中设置以下内容: + +application.properties + +``` +spring.config.import=optional:configserver: +``` + +这将在“http://localhost:8888”的默认位置连接到配置服务器。如果无法连接到 Config 服务器,删除`optional:`前缀将导致 Config 客户机失败。要更改配置服务器的位置,可以设置`spring.cloud.config.uri`,也可以将 URL 添加到`spring.config.import`语句中,例如,`spring.config.import=optional:configserver:http://myhost:8888`。导入属性中的位置优先于 URI 属性。 + +| |通过`spring.config.import`导入 Spring 引导配置数据方法所需的`bootstrap`文件(属性或 YAML)是**不是**。| +|---|--------------------------------------------------------------------------------------------------------------------------------------| + +### [配置第一引导程序](#config-first-bootstrap) + +要使用传统的 Bootstrap 方式连接到 Config 服务器,必须通过属性或`spring-cloud-starter-bootstrap`启动器启用 Bootstrap。该属性为`spring.cloud.bootstrap.enabled=true`。它必须设置为系统属性或环境变量。一旦启动引导程序启用,在 Classpath 上使用 Spring Cloud Config 客户机的任何应用程序都将按以下方式连接到 Config 服务器:当一个 Config 客户机启动时,它将绑定到 Config 服务器(通过`spring.cloud.config.uri`Bootstrap 配置属性)并使用远程属性源初始化 Spring `Environment`。 + +这种行为的最终结果是,所有想要使用配置服务器的客户端应用程序都需要一个`bootstrap.yml`(或一个环境变量),其服务器地址设置为`spring.cloud.config.uri`(默认为“http://localhost:8888”)。 + +#### [发现第一查找](#discovery-first-bootstrap) + +| |除非你使用[配置第一引导程序](#config-first-bootstrap),否则你将需要在配置属性中使用`spring.config.import`属性,并使用`optional:`前缀。
例如,`spring.config.import=optional:configserver:`。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +如果使用`DiscoveryClient`实现,例如 Spring 云 Netflix 和 Eureka 服务 Discovery 或 Spring 云 Consul,则可以将配置服务器注册到该发现服务中。 + +如果你更喜欢使用`DiscoveryClient`来定位配置服务器,那么可以通过设置`spring.cloud.config.discovery.enabled=true`(默认值为`false`)来实现。例如,对于 Spring Cloud Netflix,你需要定义 Eureka 服务器地址(例如,在`eureka.client.serviceUrl.defaultZone`中)。使用此选项的价格是在启动时进行额外的网络往返,以定位服务注册。好处是,只要发现服务是一个固定点,配置服务器就可以更改其坐标。默认的服务 ID 是`configserver`,但是你可以在客户机上通过设置`spring.cloud.config.discovery.serviceId`来更改这个 ID(在服务器上,以服务的通常方式,例如通过设置`spring.application.name`)。 + +发现客户机实现都支持某种元数据映射(例如,对于 Eureka,我们有`eureka.instance.metadataMap`)。配置服务器的一些附加属性可能需要在其服务注册元数据中进行配置,以便客户端能够正确地连接。如果配置服务器使用 HTTP Basic 进行安全保护,则可以将凭据配置为`user`和`password`。此外,如果配置服务器具有上下文路径,则可以设置`configPath`。例如,下面的 YAML 文件是针对作为 Eureka 客户机的配置服务器的: + +``` +eureka: + instance: + ... + metadataMap: + user: osufhalskjrtl + password: lviuhlszvaorhvlo5847 + configPath: /config +``` + +#### [使用 Eureka 和 WebClient 的 Discovery First Bootstrap](#_discovery_first_bootstrap_using_eureka_and_webclient) + +如果你使用 Spring Cloud Netflix 中的 Eureka,并且还希望使用而不是 Jersey 或,则需要在你的 Classpath 上包括以及设置。 + +### [配置客户端快速失败](#config-client-fail-fast) + +在某些情况下,如果服务无法连接到配置服务器,你可能希望启动失败。如果这是期望的行为,请设置 BootStrap 配置属性`spring.cloud.config.fail-fast=true`,以使客户端在出现异常时停止。 + +| |要使用`spring.config.import`获得类似的功能,只需省略`optional:`前缀。| +|---|----------------------------------------------------------------------------------------------| + +### [配置客户端重试](#config-client-retry) + +如果你希望配置服务器在应用程序启动时偶尔不可用,那么可以在出现故障后让它继续尝试。首先,需要设置`spring.cloud.config.fail-fast=true`。然后你需要将`spring-retry`和`spring-boot-starter-aop`添加到你的 Classpath。默认的行为是重试六次,初始退避间隔为 1000ms,后续退避的指数乘数为 1.1。你可以通过设置`spring.cloud.config.retry.*`配置属性来配置这些属性(以及其他属性)。 + +| |要完全控制重试行为并使用遗留引导程序,请添加一个`RetryOperationsInterceptor`类型的`@Bean`,ID 为`configServerRetryInterceptor`。
Spring 重试有一个`RetryInterceptorBuilder`,支持创建一个。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [Config Client Retry with spring.config.import](#_config_client_retry_with_spring_config_import) + +Retry 与 Spring boot`spring.config.import`语句一起工作,正常属性也可以工作。但是,如果导入语句位于配置文件中,例如`应用程序-prod.properties`,那么你需要一种不同的方式来配置重试。需要将配置作为 URL 参数放置在导入语句上。 + +application-prod.properties + +``` +spring.config.import=configserver:http://configserver.example.com?fail-fast=true&max-attempts=10&max-interval=1500&multiplier=1.2&initial-interval=1100" +``` + +这将设置`spring.cloud.config.fail-fast=true`(请注意上面缺少的前缀)和所有可用的`spring.cloud.config.retry.*`配置属性。 + +### [定位远程配置资源](#_locating_remote_configuration_resources) + +配置服务提供来自`/{application}/{profile}/{label}`的属性源,其中客户端应用程序中的默认绑定如下: + +* “application”=`${spring.application.name}` + +* “profile”=`${spring.profiles.active}`(实际`Environment.getActiveProfiles()`) + +* “label”=“master” + +| |在设置属性`${spring.application.name}`时,不要在应用程序名称前加上保留的单词`application-`,以防止解决正确属性源的问题。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +你可以通过设置`spring.cloud.config.*`(其中`*`是`name`,`profile`或`label`)来覆盖所有这些参数。`label`对于回滚到以前版本的配置非常有用。使用默认的 Config 服务器实现,它可以是 Git 标签、分支名称或提交 ID。标签也可以作为逗号分隔的列表提供。在这种情况下,将逐个尝试列表中的项目,直到成功。在处理一个特性分支时,这种行为可能是有用的。例如,你可能希望将配置标签与你的分支对齐,但将其设置为可选的(在这种情况下,使用`spring.cloud.config.label=myfeature,develop`)。 + +### [为配置服务器指定多个 URL](#_specifying_multiple_urls_for_the_config_server) + +为了确保在部署了多个配置服务器实例并预期一个或多个实例不时不可用时具有高可用性,你可以指定多个 URL(在`spring.cloud.config.uri`属性下作为逗号分隔的列表),或者让你的所有实例在像 Eureka 这样的服务注册中心中注册(如果使用发现优先引导模式)。请注意,这样做仅在配置服务器不运行时(即应用程序退出时)或发生连接超时时时时才能确保高可用性。例如,如果 Config 服务器返回 500(内部服务器错误)响应,或者 Config 客户机从 Config 服务器接收 401(由于错误的凭据或其他原因),Config 客户机不会尝试从其他 URL 获取属性。这类错误表示用户问题,而不是可用性问题。 + +如果你在配置服务器上使用 HTTP Basic Security,则当前只有在你在`spring.cloud.config.uri`属性下指定的每个 URL 中嵌入凭据时,才可能支持 per-config Server auth 凭据。如果使用任何其他类型的安全机制,则无法(当前)支持每个配置服务器的身份验证和授权。 + +### [配置超时](#_configuring_timeouts) + +如果你想配置超时阈值: + +* 可以使用属性`spring.cloud.config.request-read-timeout`配置读超时。 + +* 可以使用属性`spring.cloud.config.request-connect-timeout`配置连接超时。 + +### [Security](#_security_2) + +如果你在服务器上使用 HTTP Basic Security,客户端需要知道密码(如果不是默认的,则需要知道用户名)。你可以通过配置服务器 URI 或通过单独的用户名和密码属性指定用户名和密码,如以下示例所示: + +``` +spring: + cloud: + config: + uri: https://user:[email protected] +``` + +下面的示例展示了传递相同信息的另一种方式: + +``` +spring: + cloud: + config: + uri: https://myconfig.mycompany.com + username: user + password: secret +``` + +`spring.cloud.config.password`和`spring.cloud.config.username`值覆盖了 URI 中提供的任何内容。 + +如果在 Cloud Foundry 上部署应用程序,提供密码的最佳方式是通过服务凭据(例如在 URI 中,因为它不需要在配置文件中)。以下示例在本地工作,并适用于名为`configserver`的 Cloud Foundry 上的用户提供的服务: + +``` +spring: + cloud: + config: + uri: ${vcap.services.configserver.credentials.uri:http://user:[email protected]:8888} +``` + +如果 Config Server 需要客户端 TLS 证书,则可以通过属性配置客户端 TLS 证书和信任存储库,如以下示例所示: + +``` +spring: + cloud: + config: + uri: https://myconfig.myconfig.com + tls: + enabled: true + key-store: + key-store-type: PKCS12 + key-store-password: + key-password: + trust-store: + trust-store-type: PKCS12 + trust-store-password: +``` + +`spring.cloud.config.tls.enabled`需要为 true 才能启用配置客户端 TLS。当省略`spring.cloud.config.tls.trust-store`时,将使用一个 JVM 默认信任存储区。`spring.cloud.config.tls.key-store-type`和`spring.cloud.config.tls.trust-store-type`的默认值是 PKCS12。如果省略了密码属性,则假定密码为空。 + +如果使用另一种形式的安全性,则可能需要[provide a `RestTemplate`](#custom-rest-template)到`ConfigServicePropertySourceLocator`(例如,通过在 BootStrap 上下文中抓取它并将其注入)。 + +#### [健康指标](#_health_indicator_2) + +Config 客户机提供一个 Spring 引导健康指示器,该指示器试图从 Config 服务器加载配置。可以通过设置`health.config.enabled=false`禁用健康指示器。出于性能原因,响应也会被缓存。默认的缓存持续时间为 5 分钟。要更改该值,请设置`health.config.time-to-live`属性(以毫秒为单位)。 + +#### [提供自定义的 RESTTemplate](#custom-rest-template) + +在某些情况下,你可能需要自定义从客户机向配置服务器发出的请求。通常,这样做需要传递特殊的`Authorization`头来验证对服务器的请求。要提供自定义`RestTemplate`: + +1. 创建具有`PropertySourceLocator`实现的新配置 Bean,如以下示例所示: + +CustomConfigServiceBootStrapConfiguration.java + +``` +@Configuration +public class CustomConfigServiceBootstrapConfiguration { + @Bean + public ConfigServicePropertySourceLocator configServicePropertySourceLocator() { + ConfigClientProperties clientProperties = configClientProperties(); + ConfigServicePropertySourceLocator configServicePropertySourceLocator = new ConfigServicePropertySourceLocator(clientProperties); + configServicePropertySourceLocator.setRestTemplate(customRestTemplate(clientProperties)); + return configServicePropertySourceLocator; + } +} +``` + +| |对于添加`Authorization`头的简化方法,可以使用`spring.cloud.config.headers.*`属性。| +|---|------------------------------------------------------------------------------------------------------------------------------| + +1. 在`resources/META-INF`中,创建一个名为 ` Spring.factories` 的文件,并指定你的自定义配置,如下例所示: + +Spring.工厂 + +``` +org.springframework.cloud.bootstrap.BootstrapConfiguration = com.my.config.client.CustomConfigServiceBootstrapConfiguration +``` + +#### [Vault](#_vault) + +当使用 Vault 作为配置服务器的后端时,客户机需要为服务器提供一个令牌,以便从 Vault 检索值。通过在`bootstrap.yml`中设置`spring.cloud.config.token`,可以在客户端内提供这个令牌,如下例所示: + +``` +spring: + cloud: + config: + token: YourVaultToken +``` + +### [保险库中嵌套的钥匙](#_nested_keys_in_vault) + +Vault 支持将密钥嵌套在 Vault 中存储的值中,如以下示例所示: + +`echo -n '{"appA": {"secret": "appAsecret"}, "bar": "baz"}' | vault write secret/myapp -` + +此命令将 JSON 对象写入保险库。要访问 Spring 中的这些值,你将使用传统的点注释,如下面的示例所示 + +``` +@Value("${appA.secret}") +String name = "World"; +``` + +前面的代码将把`name`变量的值设置为`appAsecret`。 + diff --git a/docs/spring-cloud/spring-cloud-consul.md b/docs/spring-cloud/spring-cloud-consul.md new file mode 100644 index 0000000000000000000000000000000000000000..95a62a60503fcedb114159708e9a126e3a417577 --- /dev/null +++ b/docs/spring-cloud/spring-cloud-consul.md @@ -0,0 +1,777 @@ +Spring 云执政官 +========== + +该项目通过自动配置和绑定到 Spring 环境和其他 Spring 编程模型习惯用法,为 Spring 引导应用程序提供 Consul 集成。通过一些简单的注释,你可以快速启用和配置应用程序内的公共模式,并使用基于 Consul 的组件构建大型分布式系统。所提供的模式包括服务发现、控制总线和配置。智能路由和客户端负载平衡、断路器是通过与其他 Spring 云项目集成来提供的。 + +[](#quick-start)[1. Quick Start](#quick-start) +---------- + +这一快速启动将使用 Spring Cloud Consul 进行服务发现和分布式配置。 + +首先,在你的机器上运行领事代理。然后,你可以访问它,并将其作为服务注册中心和配置源使用 Spring Cloud Consul。 + +### [](#discovery-client-usage)[1.1.发现客户端使用情况](#discovery-client-usage) ### + +要在应用程序中使用这些特性,你可以将其构建为依赖于`spring-cloud-consul-core`的 Spring 引导应用程序。添加依赖项最方便的方法是使用 Spring 引导启动器:`org.springframework.cloud:spring-cloud-starter-consul-discovery`。我们建议使用依赖管理和`spring-boot-starter-parent`。下面的示例显示了典型的 Maven 配置: + +POM.xml + +``` + + + org.springframework.boot + spring-boot-starter-parent + {spring-boot-version} + + + + + + org.springframework.cloud + spring-cloud-starter-consul-discovery + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + +``` + +下面的示例显示了一个典型的 Gradle 设置: + +建造。 Gradle + +``` +plugins { + id 'org.springframework.boot' version ${spring-boot-version} + id 'io.spring.dependency-management' version ${spring-dependency-management-version} + id 'java' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.cloud:spring-cloud-starter-consul-discovery' + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} +``` + +现在,你可以创建一个标准的 Spring 启动应用程序,例如下面的 HTTP 服务器: + +``` +@SpringBootApplication +@RestController +public class Application { + + @GetMapping("/") + public String home() { + return "Hello World!"; + } + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} +``` + +当这个 HTTP 服务器运行时,它会连接到运行在默认的本地 8500 端口的 Consul 代理。要修改启动行为,可以使用`应用程序.属性`更改 Consul 代理的位置,如下例所示: + +``` +spring: + cloud: + consul: + host: localhost + port: 8500 +``` + +你现在可以使用`DiscoveryClient`、`@LoadBalanced RestTemplate`或`@LoadBalanced WebClient.Builder`从 Consul 检索服务和实例数据,如以下示例所示: + +``` +@Autowired +private DiscoveryClient discoveryClient; + +public String serviceUrl() { + List list = discoveryClient.getInstances("STORES"); + if (list != null && list.size() > 0 ) { + return list.get(0).getUri().toString(); + } + return null; +} +``` + +### [](#distributed-configuration-usage)[1.2.分布式配置使用](#distributed-configuration-usage) ### + +要在应用程序中使用这些特性,你可以将其构建为依赖于`spring-cloud-consul-core`和`spring-cloud-consul-config`的 Spring 引导应用程序。添加依赖项最方便的方法是使用 Spring 引导启动器:`org.springframework.cloud:spring-cloud-starter-consul-config`。我们建议使用依赖管理和`spring-boot-starter-parent`。下面的示例显示了典型的 Maven 配置: + +POM.xml + +``` + + + org.springframework.boot + spring-boot-starter-parent + {spring-boot-version} + + + + + + org.springframework.cloud + spring-cloud-starter-consul-config + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + +``` + +下面的示例显示了一个典型的 Gradle 设置: + +构建。 Gradle + +``` +plugins { + id 'org.springframework.boot' version ${spring-boot-version} + id 'io.spring.dependency-management' version ${spring-dependency-management-version} + id 'java' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.cloud:spring-cloud-starter-consul-config' + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} +``` + +现在,你可以创建一个标准的 Spring 启动应用程序,例如下面的 HTTP 服务器: + +``` +@SpringBootApplication +@RestController +public class Application { + + @GetMapping("/") + public String home() { + return "Hello World!"; + } + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} +``` + +应用程序从 Consul 检索配置数据。 + +| |如果使用 Spring Cloud Consul Config,则需要设置`spring.config.import`属性才能绑定到 Consul。
你可以在[Spring Boot Config Data Import section](#config-data-import)中阅读有关它的更多信息。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[](#spring-cloud-consul-install)[2.安装领事](#spring-cloud-consul-install) +---------- + +有关如何安装 Consul 的说明,请参见[安装文档](https://www.consul.io/intro/getting-started/install.html)。 + +[](#spring-cloud-consul-agent)[3. Consul Agent](#spring-cloud-consul-agent) +---------- + +Consul 代理客户端必须可用于所有 Spring 云 Consul 应用程序。默认情况下,代理客户机应该位于`localhost:8500`。有关如何启动代理客户机以及如何连接到 Consul 代理服务器集群的详细信息,请参见[代理文档](https://consul.io/docs/agent/basics.html)。为了进行开发,在安装了 Consul 之后,可以使用以下命令启动 Consul 代理: + +``` +./src/main/bash/local_run_consul.sh +``` + +这将在端口 8500 上以服务器模式启动代理,其 UI 位于[localhost:8500](http://localhost:8500)。 + +[](#spring-cloud-consul-discovery)[4.与领事的服务发现](#spring-cloud-consul-discovery) +---------- + +服务发现是基于微服务的体系结构的关键原则之一。尝试手动配置每个客户机或某种形式的约定可能非常困难,并且可能非常脆弱。领事通过[HTTP API](https://www.consul.io/docs/agent/http.html)和[DNS](https://www.consul.io/docs/agent/dns.html)提供服务发现服务。 Spring Cloud Consul 利用 HTTP API 进行服务注册和发现。这并不妨碍非 Spring 云应用程序利用 DNS 接口。Consul 代理服务器在[cluster](https://www.consul.io/docs/internals/architecture.html)中运行,该服务器通过[gossip protocol](https://www.consul.io/docs/internals/gossip.html)进行通信,并使用[RAFT 共识协议](https://www.consul.io/docs/internals/consensus.html)。 + +### [](#how-to-activate)[4.1.如何激活](#how-to-activate) ### + +要激活 Consul 服务发现,请使用分组`org.springframework.cloud`和工件 ID`spring-cloud-starter-consul-discovery`的启动器。请参阅[Spring Cloud Project page](https://projects.spring.io/spring-cloud/)以获取有关使用当前 Spring 云发布列设置构建系统的详细信息。 + +### [](#registering-with-consul)[4.2.向领事登记](#registering-with-consul) ### + +当客户机向 Consul 注册时,它提供有关自身的元数据,如主机和端口、ID、名称和标记。默认情况下会创建一个 http[Check](https://www.consul.io/docs/agent/checks.html),consul 每 10 秒钟就会点击`/actuator/health`端点。如果健康检查失败,服务实例将被标记为“关键”。 + +示例领事客户: + +``` +@SpringBootApplication +@RestController +public class Application { + + @RequestMapping("/") + public String home() { + return "Hello world"; + } + + public static void main(String[] args) { + new SpringApplicationBuilder(Application.class).web(true).run(args); + } + +} +``` + +(即完全正常的引导应用程序)。如果 Consul 客户机位于`localhost:8500`以外的地方,则需要配置来定位客户机。示例: + +应用程序.yml + +``` +spring: + cloud: + consul: + host: localhost + port: 8500 +``` + +| |如果你使用[Spring Cloud Consul Config](#spring-cloud-consul-config),并且你已经设置了`spring.cloud.bootstrap.enabled=true`或`spring.config.use-legacy-processing=true`或使用`spring-cloud-starter-bootstrap`,那么上述值将需要放置在`bootstrap.yml`而不是`应用程序.yml`中。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +默认的服务名、实例 ID 和端口(取自`Environment`)分别是`${spring.application.name}`、 Spring 上下文 ID 和`${server.port}`。 + +要禁用 Consul Discovery 客户端,你可以将`spring.cloud.consul.discovery.enabled`设置为`false`。当`spring.cloud.discovery.enabled`设置为`false`时,Consul Discovery 客户端也将被禁用。 + +要禁用服务注册,你可以将`spring.cloud.consul.discovery.register`设置为`false`。 + +#### [](#registering-management-as-a-separate-service)[4.2.1.将管理注册为一项单独的服务](#registering-management-as-a-separate-service) #### + +当管理服务器端口被设置为与应用程序端口不同的东西时,通过设置`management.server.port`属性,管理服务将被注册为独立于应用程序服务的服务。例如: + +应用程序.yml + +``` +spring: + application: + name: myApp +management: + server: + port: 4452 +``` + +上述配置将注册以下 2 项服务: + +* 应用程序服务: + +``` +ID: myApp +Name: myApp +``` + +* 管理服务: + +``` +ID: myApp-management +Name: myApp-management +``` + +管理服务将从应用程序服务继承其`instanceId`和`serviceName`。例如: + +应用程序.yml + +``` +spring: + application: + name: myApp +management: + server: + port: 4452 +spring: + cloud: + consul: + discovery: + instance-id: custom-service-id + serviceName: myprefix-${spring.application.name} +``` + +上述配置将注册以下 2 项服务: + +* 应用程序服务: + +``` +ID: custom-service-id +Name: myprefix-myApp +``` + +* 管理服务: + +``` +ID: custom-service-id-management +Name: myprefix-myApp-management +``` + +可以通过以下属性进行进一步的定制: + +``` +/** Port to register the management service under (defaults to management port) */ +spring.cloud.consul.discovery.management-port + +/** Suffix to use when registering management service (defaults to "management" */ +spring.cloud.consul.discovery.management-suffix + +/** Tags to use when registering management service (defaults to "management" */ +spring.cloud.consul.discovery.management-tags +``` + +#### [](#http-health-check)[4.2.2.HTTP 健康检查](#http-health-check) #### + +Consul 实例的健康检查默认为“/actuator/health”,这是 Spring 引导执行器应用程序中健康端点的默认位置。如果使用非默认的上下文路径或 Servlet 路径(例如`server.servletPath=/foo`)或管理端点路径(例如`management.server.servlet.context-path=/admin`),则即使对于执行器应用程序,也需要对此进行更改。 + +Consul 用于检查健康端点的间隔也可以配置。“10 秒”和“1 米”分别代表 10 秒和 1 分钟。 + +这个示例演示了上面的内容(有关更多选项,请参见[附录页](appendix.html)中的`spring.cloud.consul.discovery.health-check-*`属性)。 + +应用程序.yml + +``` +spring: + cloud: + consul: + discovery: + healthCheckPath: ${management.server.servlet.context-path}/actuator/health + healthCheckInterval: 15s +``` + +你可以通过设置`spring.cloud.consul.discovery.register-health-check=false`完全禁用 HTTP 健康检查。 + +##### [](#applying-headers)[应用头文件](#applying-headers) ##### + +头可以应用于健康检查请求。例如,如果你试图注册一个[Spring Cloud Config](https://cloud.spring.io/spring-cloud-config/)服务器,该服务器使用[Vault Backend](https://github.com/spring-cloud/spring-cloud-config/blob/master/docs/src/main/asciidoc/spring-cloud-config.adoc#vault-backend): + +应用程序.yml + +``` +spring: + cloud: + consul: + discovery: + health-check-headers: + X-Config-Token: 6442e58b-d1ea-182e-cfa5-cf9cddef0722 +``` + +根据 HTTP 标准,每个头可以有多个值,在这种情况下,可以提供一个数组: + +应用程序.yml + +``` +spring: + cloud: + consul: + discovery: + health-check-headers: + X-Config-Token: + - "6442e58b-d1ea-182e-cfa5-cf9cddef0722" + - "Some other value" +``` + +#### [](#actuator-health-indicators)[4.2.3.执行器健康指示器](#actuator-health-indicators) #### + +如果服务实例是 Spring 启动致动器应用程序,则可以提供以下致动器的健康指示器。 + +##### [](#discoveryclienthealthindicator)[发现潜在的指示剂](#discoveryclienthealthindicator) ##### + +当 Consul 服务发现是活动的时,[发现客户状态指示器](https://cloud.spring.io/spring-cloud-commons/2.2.x/reference/html/#health-indicator)被配置并使致动器健康端点可用。有关配置选项,请参见[here](https://cloud.spring.io/spring-cloud-commons/2.2.x/reference/html/#health-indicator)。 + +##### [](#consulhealthindicator)[健康咨询指示器](#consulhealthindicator) ##### + +配置了一个指示器,用于验证`ConsulClient`的健康状况。 + +默认情况下,它检索 Consul Leader 节点状态和所有已注册的服务。在具有许多注册服务的部署中,在每次健康检查中检索所有服务的成本可能很高。跳过服务检索,只检查 leader 节点状态集`spring.cloud.consul.health-indicator.include-services-query=false`。 + +禁用指示器集`management.health.consul.enabled=false`。 + +| |当应用程序在[Bootstrap 上下文模式](https://cloud.spring.io/spring-cloud-commons/2.2.x/reference/html/#the-bootstrap-application-context)(默认值)中运行时,
此指示器被加载到 BootStrap 上下文中,并且不对 Actuator Health 端点可用。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#metadata)[4.2.4. Metadata](#metadata) #### + +Consul 支持服务的元数据。 Spring 云的`ServiceInstance`具有一个`Map metadata`字段,该字段由服务`meta`字段填充。在`spring.cloud.consul.discovery.metadata`或`spring.cloud.consul.discovery.management-metadata`属性上填充`meta`字段集值。 + +应用程序.yml + +``` +spring: + cloud: + consul: + discovery: + metadata: + myfield: myvalue + anotherfield: anothervalue +``` + +上述配置将导致一个服务的元字段包含`myfield→myvalue`和`anotherfield→anothervalue`。 + +##### [](#generated-metadata)[生成的元数据](#generated-metadata) ##### + +领事自动注册将自动生成一些条目。 + +| Key |价值| +|---------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------| +| 'group' |属性`spring.cloud.consul.discovery.instance-group`。只有当`instance-group`不为空时,才会生成该值。| +| 'secure' |如果属性`spring.cloud.consul.discovery.scheme`等于“HTTPS”,则为真,否则为假。| +|Property `spring.cloud.consul.discovery.default-zone-metadata-name`, defaults to 'zone'|属性`spring.cloud.consul.discovery.instance-zone`。只有当`instance-zone`不为空时,才会生成该值。| + +| |Spring Cloud Consul 的旧版本通过解析`spring.cloud.consul.discovery.tags`属性填充了 Spring Cloud Commons 中的`ServiceInstance.getMetadata()`方法。这不再受支持,请迁移到使用`spring.cloud.consul.discovery.metadata`映射。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#making-the-consul-instance-id-unique)[4.2.5.使 Consul 实例 ID 是唯一的](#making-the-consul-instance-id-unique) #### + +默认情况下,Consul 实例注册的 ID 等于其 Spring 应用程序上下文 ID。默认情况下, Spring 应用程序上下文 ID 是`${spring.application.name}:comma,separated,profiles:${server.port}`。对于大多数情况,这将允许在一台机器上运行一个服务的多个实例。如果需要更多的唯一性,那么使用 Spring Cloud,你可以通过在`spring.cloud.consul.discovery.instanceId`中提供唯一的标识符来覆盖这一点。例如: + +应用程序.yml + +``` +spring: + cloud: + consul: + discovery: + instanceId: ${spring.application.name}:${vcap.application.instance_id:${spring.application.instance_id:${random.value}}} +``` + +有了这个元数据,以及在 LocalHost 上部署的多个服务实例,随机值就会在其中发挥作用,从而使实例具有唯一性。在 CloudFoundry 中,`vcap.application.instance_id`将在 Spring 引导应用程序中自动填充,因此不需要随机值。 + +### [](#looking-up-services)[4.3.查询服务](#looking-up-services) ### + +#### [](#using-load-balancer)[4.3.1.使用负载均衡器](#using-load-balancer) #### + +Spring 云具有对[Feign](https://github.com/spring-cloud/spring-cloud-netflix/blob/master/docs/src/main/asciidoc/spring-cloud-netflix.adoc#spring-cloud-feign)(一个 REST 客户机构建器)和[Spring `RestTemplate`](https://docs.spring.io/spring-cloud-commons/docs/current/reference/html/#rest-template-loadbalancer-client)的支持,用于使用逻辑服务名称/ID 而不是物理 URL 查找服务。Feign 和支持发现的 RESTTemplate 都利用[Spring Cloud LoadBalancer](https://docs.spring.io/spring-cloud-commons/docs/current/reference/html/#spring-cloud-loadbalancer)实现客户端负载平衡。 + +如果你想使用 RESTTemplate 访问服务存储,只需声明: + +``` +@LoadBalanced +@Bean +public RestTemplate loadbalancedRestTemplate() { + return new RestTemplate(); +} +``` + +并像这样使用它(注意我们如何使用来自 Consul 的 Stores Service Name/ID,而不是一个完全限定的域名): + +``` +@Autowired +RestTemplate restTemplate; + +public String getFirstProduct() { + return this.restTemplate.getForObject("https://STORES/products/1", String.class); +} +``` + +如果你在多个数据中心中拥有 Consul 集群,并且希望访问另一个数据中心中的服务,那么仅使用服务名称/ID 是不够的。在这种情况下,你使用属性`spring.cloud.consul.discovery.datacenters.STORES=dc-west`,其中`STORES`是服务名称/ID,`dc-west`是存储服务生命的数据中心。 + +| |Spring Cloud 现在还提供对[Spring Cloud LoadBalancer](https://cloud.spring.io/spring-cloud-commons/reference/html/#_spring_resttemplate_as_a_load_balancer_client)的支持。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#using-the-discoveryclient)[4.3.2.使用 DiscoveryClient](#using-the-discoveryclient) #### + +你也可以使用`org.springframework.cloud.client.discovery.DiscoveryClient`,它为非特定于 Netflix 的发现客户端提供了一个简单的 API,例如 + +``` +@Autowired +private DiscoveryClient discoveryClient; + +public String serviceUrl() { + List list = discoveryClient.getInstances("STORES"); + if (list != null && list.size() > 0 ) { + return list.get(0).getUri(); + } + return null; +} +``` + +### [](#consul-catalog-watch)[4.4.领事目录表](#consul-catalog-watch) ### + +领事编目表利用领事的能力[watch services](https://www.consul.io/docs/agent/watches.html#services)。Catalog Watch 进行一个阻塞的 Consul HTTP API 调用,以确定是否有任何服务发生了更改。如果有新的服务数据,则会发布心跳事件。 + +要改变配置手表的频率时,调用 change`spring.cloud.consul.config.discovery.catalog-services-watch-delay`。默认值是 1000,以毫秒为单位。延迟是上一次调用结束后和下一次调用开始后的时间量。 + +要禁用目录手表集`spring.cloud.consul.discovery.catalogServicesWatch.enabled=false`。 + +手表使用 Spring `TaskScheduler`来安排对领事的拜访。默认情况下,它是一个`ThreadPoolTaskScheduler`,其`poolSize`为 1。要更改`TaskScheduler`,请创建一个类型为`TaskScheduler`的 Bean,并以`ConsulDiscoveryClientConfiguration.CATALOG_WATCH_TASK_SCHEDULER_NAME`常数命名。 + +[](#spring-cloud-consul-config)[5.与 Consul 的分布式配置](#spring-cloud-consul-config) +---------- + +Consul 提供了[Key/Value Store](https://consul.io/docs/agent/http/kv.html)用于存储配置和其他元数据。 Spring Cloud Consul Config 是[配置服务器和客户端](https://github.com/spring-cloud/spring-cloud-config)的一种替代方案。在特殊的“引导”阶段,将配置加载到 Spring 环境中。默认情况下,配置存储在`/config`文件夹中。多个`PropertySource`实例是根据应用程序的名称和活动配置文件创建的,该配置文件模仿了解析属性的 Spring 云配置顺序。例如,名称为“TestApp”和配置文件为“Dev”的应用程序将创建以下属性源: + +``` +config/testApp,dev/ +config/testApp/ +config/application,dev/ +config/application/ +``` + +最具体的属性源位于顶部,而最不具体的属性源位于底部。`config/application`文件夹中的属性适用于使用 Consul 进行配置的所有应用程序。`config/testApp`文件夹中的属性仅对名为“TestApp”的服务实例可用。 + +当前在启动应用程序时读取配置。将 HTTP POST 发送到`/refresh`将导致重新加载配置。[Config Watch](#spring-cloud-consul-config-watch)还将自动检测更改并重新加载应用程序上下文。 + +### [](#how-to-activate-2)[5.1.如何激活](#how-to-activate-2) ### + +要开始使用 Consul 配置,请使用带有组`org.springframework.cloud`和工件 ID`spring-cloud-starter-consul-config`的启动器。请参阅[Spring Cloud Project page](https://projects.spring.io/spring-cloud/),以获取有关使用当前 Spring 云发布系列设置构建系统的详细信息。 + +### [](#config-data-import)[5.2. Spring Boot Config Data Import](#config-data-import) ### + +Spring Boot2.4 引入了一种通过`spring.config.import`属性导入配置数据的新方法。这是现在从 Consul 获得配置的默认方式。 + +可选地连接到 Consul,请在应用程序中设置以下属性: + +应用程序.属性 + +``` +spring.config.import=optional:consul: +``` + +这将在默认位置“http://localhost:8500”连接到 Consul 代理。如果无法连接到 Consul,删除`optional:`前缀将导致 Consul Config 失败。要更改 Consul Config 的连接属性,可以设置`spring.cloud.consul.host`和`spring.cloud.consul.port`,或者将主机/端口对添加到`spring.config.import`语句中,例如,`spring.config.import=optional:consul:myhost:8500`。导入属性中的位置优先于主机和端口属性。 + +Consul Config 将尝试根据`spring.cloud.consul.config.name`(默认为`spring.application.name`属性的值)和`spring.cloud.consul.config.default-context`(默认为`application`)从四个自动上下文加载值。如果你希望指定上下文,而不是使用计算的上下文,那么可以将该信息添加到`spring.config.import`语句中。 + +application.properties + +``` +spring.config.import=optional:consul:myhost:8500/contextone;/context/two +``` + +这将可选地只从`/contextone`和`/context/two`加载配置。 + +| |通过`spring.config.import`导入 Spring 引导配置数据方法所需的`bootstrap`文件(属性或 YAML)是**不是**。| +|---|--------------------------------------------------------------------------------------------------------------------------------------| + +### [](#customizing)[5.3.定制](#customizing) ### + +Consul Config 可以使用以下属性进行自定义: + +``` +spring: + cloud: + consul: + config: + enabled: true + prefix: configuration + defaultContext: apps + profileSeparator: '::' +``` + +| |如果你已经设置了`spring.cloud.bootstrap.enabled=true`或`spring.config.use-legacy-processing=true`,或者包含了`spring-cloud-starter-bootstrap`,那么上述值将需要放置在`bootstrap.yml`中,而不是`application.yml`中。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +* `enabled`将该值设置为“false”会禁用 Consul Config + +* `prefix`设置配置值的基础文件夹 + +* `defaultContext`设置所有应用程序使用的文件夹名 + +* `profileSeparator`设置用于在具有配置文件的属性源中分隔配置文件名称的分隔符的值 + +### [](#spring-cloud-consul-config-watch)[5.4.配置手表](#spring-cloud-consul-config-watch) ### + +领事配置手表利用领事的能力[观看一个键的前缀](https://www.consul.io/docs/agent/watches.html#keyprefix)。Config Watch 进行一个阻塞的 Consul HTTP API 调用,以确定当前应用程序的任何相关配置数据是否已更改。如果有新的配置数据,将发布刷新事件。这相当于调用`/refresh`执行器端点。 + +要更改配置手表时的频率,请调用 change`spring.cloud.consul.config.watch.delay`。默认值是 1000,以毫秒为单位。延迟是上一次调用结束后和下一次调用开始后的时间量。 + +要禁用配置手表集`spring.cloud.consul.config.watch.enabled=false`。 + +手表使用 Spring `TaskScheduler`来安排对领事的拜访。默认情况下,它是一个`ThreadPoolTaskScheduler`,其`poolSize`为 1。要更改`TaskScheduler`,请创建一个类型为`TaskScheduler`的 Bean,并以`ConsulConfigAutoConfiguration.CONFIG_WATCH_TASK_SCHEDULER_NAME`常数命名。 + +### [](#spring-cloud-consul-config-format)[5.5.具有配置的 YAML 或属性](#spring-cloud-consul-config-format) ### + +以 YAML 或 Properties 格式存储一组属性可能更方便,而不是单独的键/值对。将`spring.cloud.consul.config.format`属性设置为`YAML`或`PROPERTIES`。例如,使用 YAML: + +``` +spring: + cloud: + consul: + config: + format: YAML +``` + +| |如果你已经设置了`spring.cloud.bootstrap.enabled=true`或`spring.config.use-legacy-processing=true`,或者包含了`spring-cloud-starter-bootstrap`,那么上述值将需要放置在`bootstrap.yml`中,而不是`application.yml`中。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +YAML 必须在 consul 中设置适当的`data`键。使用键上面的默认值将看起来像: + +``` +config/testApp,dev/data +config/testApp/data +config/application,dev/data +config/application/data +``` + +你可以在上面列出的任何键中存储 YAML 文档。 + +你可以使用`spring.cloud.consul.config.data-key`更改数据键。 + +### [](#spring-cloud-consul-config-git2consul)[5.6.Git2config 领事](#spring-cloud-consul-config-git2consul) ### + +Git2Consul 是一个 Consul 社区项目,它将文件从 Git 存储库加载到 Consul 中的各个密钥。默认情况下,键的名称是文件的名称。YAML 和 Properties 文件分别支持`.yml`和`.properties`的文件扩展名。将`spring.cloud.consul.config.format`属性设置为`FILES`。例如: + +bootstrap.yml + +``` +spring: + cloud: + consul: + config: + format: FILES +``` + +给定`/config`中的以下键,则`development`配置文件和`foo`的应用程序名称: + +``` +.gitignore +application.yml +bar.properties +foo-development.properties +foo-production.yml +foo.properties +master.ref +``` + +将创建以下财产来源: + +``` +config/foo-development.properties +config/foo.properties +config/application.yml +``` + +每个键的值需要是一个格式正确的 YAML 或 Properties 文件。 + +### [](#spring-cloud-consul-failfast)[5.7. Fail Fast](#spring-cloud-consul-failfast) ### + +在某些情况下(如本地开发或某些测试场景),如果 Consul 不能用于配置,那么不失败可能是很方便的。设置`spring.cloud.consul.config.fail-fast=false`将导致配置模块记录警告,而不是抛出异常。这将允许应用程序继续正常启动。 + +| |如果你已经设置了`spring.cloud.bootstrap.enabled=true`或`spring.config.use-legacy-processing=true`,或者包含了`spring-cloud-starter-bootstrap`,那么上述值将需要放置在`bootstrap.yml`中,而不是`application.yml`中。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[](#spring-cloud-consul-retry)[6. Consul Retry](#spring-cloud-consul-retry) +---------- + +如果你预计,当你的应用程序启动时,Consul 代理可能会偶尔无法使用,那么你可以在出现故障后要求它继续尝试。你需要在 Classpath 中添加 ` Spring-retry` 和`spring-boot-starter-aop`。默认的行为是重试 6 次,初始退避间隔为 1000ms,后续退避的指数乘数为 1.1。你可以使用`spring.cloud.consul.retry.*`配置属性来配置这些属性(以及其他属性)。这对 Spring 云 Consul 配置和发现注册都有效。 + +| |若要完全控制重试,请添加一个`@Bean`的类型为“retryOperationsenterceptor”,ID 为“ConsultretryInterceptor”。 Spring
重试有一个`RetryInterceptorBuilder`,这使得很容易创建一个。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[](#spring-cloud-consul-bus)[7. Spring Cloud Bus with Consul](#spring-cloud-consul-bus) +---------- + +### [](#how-to-activate-3)[7.1.如何激活](#how-to-activate-3) ### + +要开始使用 Consul 总线,请使用分组`org.springframework.cloud`和工件 ID`spring-cloud-starter-consul-bus`的启动器。请参阅[Spring Cloud Project page](https://projects.spring.io/spring-cloud/),以获取有关使用当前 Spring 云发布列设置构建系统的详细信息。 + +有关可用的执行器端点和 HOWTO 发送自定义消息,请参见[Spring Cloud Bus](https://cloud.spring.io/spring-cloud-bus/)文档。 + +[](#spring-cloud-consul-hystrix)[8.带 hystrix 的断路器](#spring-cloud-consul-hystrix) +---------- + +应用程序可以通过在 POM.xml:`spring-cloud-starter-hystrix`项目中包含此启动器来使用 Spring Cloud Netflix 项目提供的 Hystrix 断路器。Hystrix 并不依赖于 Netflix 的 Discovery 客户端。`@EnableHystrix`注释应该放在配置类(通常是主类)上。然后可以用`@HystrixCommand`注释方法来由断路器保护。有关更多详细信息,请参见[文件](https://projects.spring.io/spring-cloud/spring-cloud.html#_circuit_breaker_hystrix_clients)。 + +[](#spring-cloud-consul-turbine)[9.基于涡轮机和 consul 的 hystrix 度量聚合](#spring-cloud-consul-turbine) +---------- + +Turbine(由 Spring Cloud Netflix 项目提供)聚合了多个实例 Hystrix Metrics 流,因此仪表板可以显示聚合视图。Turbine 使用`DiscoveryClient`接口来查找相关实例。 Spring 要使用带有云领事的涡轮机,以类似于以下示例的方式配置涡轮机应用程序: + +POM.xml + +``` + + org.springframework.cloud + spring-cloud-netflix-turbine + + + org.springframework.cloud + spring-cloud-starter-consul-discovery + +``` + +请注意,涡轮机依赖项不是启动器。涡轮启动器包括对 Netflix Eureka 的支持。 + +application.yml + +``` +spring.application.name: turbine +applications: consulhystrixclient +turbine: + aggregator: + clusterConfig: ${applications} + appConfig: ${applications} +``` + +`clusterConfig`和`appConfig`节必须匹配,因此将逗号分隔的服务 ID 列表放入一个单独的配置属性中非常有用。 + +Turbine.java + +``` +@EnableTurbine +@SpringBootApplication +public class Turbine { + public static void main(String[] args) { + SpringApplication.run(DemoturbinecommonsApplication.class, args); + } +} +``` + +[](#configuration-properties)[10.配置属性](#configuration-properties) +---------- + +要查看所有 consul 相关配置属性的列表,请检查[附录页](appendix.html)。 diff --git a/docs/spring-cloud/spring-cloud-contract.md b/docs/spring-cloud/spring-cloud-contract.md new file mode 100644 index 0000000000000000000000000000000000000000..396c023fba448a6d3ae0e34893edea84b22e53f8 --- /dev/null +++ b/docs/spring-cloud/spring-cloud-contract.md @@ -0,0 +1,17 @@ +Spring 云合同参考文档 +========== + +Adam Dudczak,Mathias Düsterhöft,Marcin Grzejszczak,Dennis Kieselhorst,Jakub Kubry Ski,Karol Lassak,Olga Maciaszek-Sharma,Mariusz Smyku A,DAVESyer,Jay Bryant + +参考文献包括以下部分: + +| [Legal](legal.html#legal-information) |法律信息。| +|----------------------------------------------------------------------------|------------------------------------------------------------------------------------------------| +|[Documentation Overview](documentation-overview.html#contract-documentation)|关于文档,获得帮助,第一步,等等。| +| [Getting Started](getting-started.html#getting-started) |引入 Spring Cloud Contract,开发你的第一个 Spring 基于 Cloud Contract 的应用程序| +| [Using Spring Cloud Contract](using.html#using) |Spring 云合同使用示例和工作流程。| +| [Spring Cloud Contract Features](project-features.html#features) |Contract DSL、消息传递、 Spring Cloud Contract Stub Runner 和 Spring Cloud Contract WiRemock。| +| [Build Tools](project-features.html#features-build-tools) |Maven 插件, Gradle 插件和 Docker。| +| [“How-to” Guides](howto.html#howto) |存根版本控制,契约集成,调试等。| +| [Appendices](appendix.html#appendix) |属性、元数据、配置、依赖关系等等。| + diff --git a/docs/spring-cloud/spring-cloud-function.md b/docs/spring-cloud/spring-cloud-function.md new file mode 100644 index 0000000000000000000000000000000000000000..a59a76ef813338119218db27a9f952d756b2d4e1 --- /dev/null +++ b/docs/spring-cloud/spring-cloud-function.md @@ -0,0 +1,22 @@ +Spring 云功能参考文档 +========== + +Mark Fisher,DAVESyer,Oleg Zhurakousky,Anshul Mehra + +**3.2.2** + +参考文献包括以下部分: + +|[Reference Guide](spring-cloud-function.html)|Spring Cloud Function Reference| +|------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------| +|[Cloud Events](https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-samples/function-sample-cloudevent)| Cloud Events | +|[RSocket](https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-rsocket)| RSocket | +|[AWS Adapter](aws.html)| AWS Adapter Reference | +|[Azure Adapter](azure.html)| Azure Adapter Reference | +|[GCP Adapter](gcp.html)| GCP Adapter Reference | + +相关链接: + +|[Reactor](https://projectreactor.io/)|Project Reactor| +|-------------------------------------|---------------| + diff --git a/docs/spring-cloud/spring-cloud-gateway.md b/docs/spring-cloud/spring-cloud-gateway.md new file mode 100644 index 0000000000000000000000000000000000000000..caf87089d04347ebb80f822787325fdc3e595676 --- /dev/null +++ b/docs/spring-cloud/spring-cloud-gateway.md @@ -0,0 +1,5419 @@ +Spring 云网关 +========== + + +该项目提供了一个构建在 Spring 生态系统之上的 API 网关,包括: Spring 5、 Spring Boot2 和 Project Reactor。 Spring Cloud Gateway 旨在提供一种简单但有效的方法来路由到 API,并向它们提供跨领域的关注,例如:安全性、监视/度量和弹性。 + +[](#gateway-starter)[1. How to Include Spring Cloud Gateway](#gateway-starter) +---------- + +要在项目中包含 Spring 云网关,请使用组 ID 为`org.springframework.cloud`和工件 ID 为`spring-cloud-starter-gateway`的 starter。请参阅[Spring Cloud Project page](https://projects.spring.io/spring-cloud/),以获取有关使用当前 Spring 云发布系列设置构建系统的详细信息。 + +如果包含启动器,但不希望启用网关,请设置`spring.cloud.gateway.enabled=false`。 + +| |Spring 云网关是建立在[Spring Boot 2.x](https://spring.io/projects/spring-boot#learn)、[Spring WebFlux](https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html)和[Project Reactor](https://projectreactor.io/docs)之上的。因此,当你使用 Spring 云网关时,许多你熟悉的同步库(例如 Spring 数据和 Spring 安全性)和模式可能不适用,如果你不熟悉这些项目,我们建议你在使用 Spring Cloud Gateway 之前,先阅读他们的文档,以熟悉一些新概念。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |Spring 云网关需要由 Spring 启动和 Spring WebFlux 提供的 Netty 运行时。它在传统 Servlet 容器中或构建为 WAR 时不工作。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[](#glossary)[2. Glossary](#glossary) +---------- + +* **路线**:网关的基本构件。它由一个 ID、一个目标 URI、一组谓词和一组筛选器定义。如果聚合谓词为 true,则匹配路由。 + +* **谓词**:这是[Java8 函数谓词](https://docs.oracle.com/javase/8/docs/api/java/util/function/Predicate.html)。输入类型为[Spring Framework `ServerWebExchange`](https://docs.spring.io/spring/docs/5.0.x/javadoc-api/org/springframework/web/server/ServerWebExchange.html)。这使你能够匹配 HTTP 请求中的任何内容,例如标题或参数。 + +* **过滤器**:这些是用特定工厂构造的[`GatewayFilter`](https://github.com/spring-cloud/spring-cloud-gateway/tree/main/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/GatewayFilter.java)的实例。在这里,你可以在发送下游请求之前或之后修改请求和响应。 + +[](#gateway-how-it-works)[3. How It Works](#gateway-how-it-works) +---------- + +下图提供了 Spring 云网关如何工作的高级概述: + +![Spring Cloud Gateway Diagram](./images/spring_cloud_gateway_diagram.png) + +客户端向 Spring 云网关提出请求。如果网关处理程序映射确定请求与路由匹配,则将请求发送到网关 Web 处理程序。此处理程序通过特定于该请求的筛选链来运行该请求。用虚线划分过滤器的原因是,过滤器可以在发送代理请求之前和之后运行逻辑。所有的“pre”过滤逻辑都会被执行。然后提出代理请求。在提出代理请求之后,将运行“POST”过滤逻辑。 + +| |在没有端口的路由中定义的 URI 分别获得 HTTP 和 HTTPS URI 的默认端口号 80 和 443。| +|---|----------------------------------------------------------------------------------------------------------------------| + +[](#configuring-route-predicate-factories-and-gateway-filter-factories)[4.配置路由谓词工厂和网关过滤器工厂](#configuring-route-predicate-factories-and-gateway-filter-factories) +---------- + +配置谓词和过滤器有两种方法:快捷方式和完全展开的参数。下面的大多数示例都使用了快捷方式。 + +名称和参数名称将以`code`的形式在每个部分的第一个或两个表示中列出。参数通常按快捷方式配置所需的顺序列出。 + +### [](#shortcut-configuration)[4.1.快捷方式配置](#shortcut-configuration) ### + +快捷方式配置由筛选器名称识别,后面跟着一个等号,后面是用逗号分隔的参数值。 + +应用程序.yml + +``` +spring: + cloud: + gateway: + routes: + - id: after_route + uri: https://example.org + predicates: + - Cookie=mycookie,mycookievalue +``` + +上一个示例用两个参数定义了`Cookie`路由谓词工厂,cookie 名`mycookie`和要匹配`mycookievalue`的值。 + +### [](#fully-expanded-arguments)[4.2.完全展开的论证](#fully-expanded-arguments) ### + +完全展开的参数看起来更像是带有名称/值对的标准 YAML 配置。通常,会有`name`键和`args`键。`args`键是用于配置谓词或筛选器的键值对的映射。 + +应用程序.yml + +``` +spring: + cloud: + gateway: + routes: + - id: after_route + uri: https://example.org + predicates: + - name: Cookie + args: + name: mycookie + regexp: mycookievalue +``` + +这是上面显示的`Cookie`谓词的快捷配置的完整配置。 + +[](#gateway-request-predicates-factories)[5.路线谓词工厂](#gateway-request-predicates-factories) +---------- + +Spring 云网关将路由匹配为 Spring WebFlux`HandlerMapping`基础设施的一部分。 Spring 云网关包括许多内置的路由谓词工厂。所有这些谓词在 HTTP 请求的不同属性上匹配。你可以将多个路由谓词工厂与逻辑`and`语句组合在一起。 + +### [](#the-after-route-predicate-factory)[5.1.后路由谓词工厂](#the-after-route-predicate-factory) ### + +`After`路由谓词工厂接受一个参数,一个`datetime`(这是一个 Java`ZonedDateTime`)。此谓词匹配在指定的 DateTime 之后发生的请求。下面的示例配置一个 after 路由谓词: + +示例 1.应用程序.yml + +``` +spring: + cloud: + gateway: + routes: + - id: after_route + uri: https://example.org + predicates: + - After=2017-01-20T17:42:47.789-07:00[America/Denver] +``` + +这条路线符合任何请求后提出的 JAN20,2017 年 17:42 山区时间(丹佛)。 + +### [](#the-before-route-predicate-factory)[5.2.前路由谓词工厂](#the-before-route-predicate-factory) ### + +`Before`路由谓词工厂接受一个参数,a`datetime`(这是一个 Java`ZonedDateTime`)。此谓词匹配在指定的`datetime`之前发生的请求。下面的示例配置一个 before 路由谓词: + +示例 2.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: before_route + uri: https://example.org + predicates: + - Before=2017-01-20T17:42:47.789-07:00[America/Denver] +``` + +这条路线符合任何在 JAN20,2017 年 17:42 山区时间(丹佛)之前提出的请求。 + +### [](#the-between-route-predicate-factory)[5.3.路由谓词之间的工厂](#the-between-route-predicate-factory) ### + +`Between`路由谓词工厂接受两个参数,`datetime1`和`datetime2`,它们是 Java`ZonedDateTime`对象。此谓词匹配发生在`datetime1`之后和`datetime2`之前的请求。`datetime2`参数必须位于`datetime1`之后。下面的示例配置了一个 between 路由谓词: + +示例 3.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: between_route + uri: https://example.org + predicates: + - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver] +``` + +这条路线符合在 JAN20,2017 年 17:42 山区时间(丹佛)之后和 JAN21,2017 年 17:42 山区时间(丹佛)之前提出的任何请求。这对于维护窗口可能是有用的。 + +### [](#the-cookie-route-predicate-factory)[5.4.cookie 路由谓词工厂](#the-cookie-route-predicate-factory) ### + +`Cookie`路由谓词工厂接受两个参数,cookie`name`和`regexp`(这是一个 Java 正则表达式)。此谓词匹配具有给定名称且其值与正则表达式匹配的 cookie。下面的示例配置了一个 Cookie 路由谓词工厂: + +示例 4.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: cookie_route + uri: https://example.org + predicates: + - Cookie=chocolate, ch.p +``` + +此路由匹配具有名为`chocolate`的 cookie 的请求,其值与`ch.p`正则表达式匹配。 + +### [](#the-header-route-predicate-factory)[5.5.头路由谓词工厂](#the-header-route-predicate-factory) ### + +`Header`路由谓词工厂接受两个参数,`header`和`regexp`(这是一个 Java 正则表达式)。此谓词与具有给定名称的头匹配,其值与正则表达式匹配。下面的示例配置头路由谓词: + +示例 5.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: header_route + uri: https://example.org + predicates: + - Header=X-Request-Id, \d+ +``` + +如果请求有一个名为`X-Request-Id`的头,其值与`\d+`正则表达式匹配(即它有一个或多个数字的值),则此路由匹配。 + +### [](#the-host-route-predicate-factory)[5.6.主机路由谓词工厂](#the-host-route-predicate-factory) ### + +`Host`路由谓词工厂接受一个参数:主机名列表`patterns`。该模式是一种 Ant 样式的模式,以`.`作为分隔符。此谓词匹配与模式匹配的`Host`头。下面的示例配置了一个主机路由谓词: + +示例 6.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: host_route + uri: https://example.org + predicates: + - Host=**.somehost.org,**.anotherhost.org +``` + +URI 模板变量(如`{sub}.myhost.org`)也受到支持。 + +如果请求具有`Host`头,其值为`www.somehost.org`或`beta.somehost.org`或`www.anotherhost.org`,则此路由匹配。 + +这个谓词提取 URI 模板变量(例如`sub`,在前面的示例中定义)作为名称和值的映射,并将其放置在`ServerWebExchange.getAttributes()`中,并在`ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE`中定义一个键。这些值可由[“GatewayFilter”工厂](#gateway-route-filters)使用 + +### [](#the-method-route-predicate-factory)[5.7.路由谓词工厂的方法](#the-method-route-predicate-factory) ### + +`Method`路由谓词工厂接受一个`methods`参数,该参数是一个或多个参数:要匹配的 HTTP 方法。下面的示例配置了一个方法路由谓词: + +示例 7.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: method_route + uri: https://example.org + predicates: + - Method=GET,POST +``` + +如果请求方法是`GET`或`POST`,则此路由匹配。 + +### [](#the-path-route-predicate-factory)[5.8.路径谓词工厂](#the-path-route-predicate-factory) ### + +`Path`路由谓词工厂接受两个参数: Spring `PathMatcher``patterns`的列表和一个名为`matchTrailingSlash`的可选标志(默认为`true`)。下面的示例配置了一个路径路由谓词: + +示例 8.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: path_route + uri: https://example.org + predicates: + - Path=/red/{segment},/blue/{segment} +``` + +如果请求路径是:例如:`/red/1`或`/red/1/`或`/red/blue`或`/blue/green`,则此路由匹配。 + +如果`matchTrailingSlash`被设置为`false`,那么请求路径`/red/1/`将不会被匹配。 + +这个谓词提取 URI 模板变量(例如`segment`,在前面的示例中定义)作为名称和值的映射,并将其放置在`ServerWebExchange.getAttributes()`中,并在`ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE`中定义一个键。这些值可由[“GatewayFilter”工厂](#gateway-route-filters)使用 + +一种实用方法(称为`get`)可以使访问这些变量变得更容易。下面的示例展示了如何使用`get`方法: + +``` +Map uriVariables = ServerWebExchangeUtils.getPathPredicateVariables(exchange); + +String segment = uriVariables.get("segment"); +``` + +### [](#the-query-route-predicate-factory)[5.9.查询路由谓词工厂](#the-query-route-predicate-factory) ### + +`Query`路由谓词工厂接受两个参数:一个必需的`param`和一个可选的`regexp`(这是一个 Java 正则表达式)。下面的示例配置一个查询路由谓词: + +示例 9.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: query_route + uri: https://example.org + predicates: + - Query=green +``` + +如果请求包含`green`查询参数,则前面的路由匹配。 + +application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: query_route + uri: https://example.org + predicates: + - Query=red, gree. +``` + +如果请求包含一个`red`查询参数,其值与`gree.`regexp 匹配,则前面的路由匹配,因此`green`和`greet`将匹配。 + +### [](#the-remoteaddr-route-predicate-factory)[5.10.RemoteAddr 路由谓词工厂](#the-remoteaddr-route-predicate-factory) ### + +`RemoteAddr`路由谓词工厂接受一个`sources`的列表(最小大小为 1),这些字符串是 CIDR 表示法(IPv4 或 IPv6)字符串,例如`192.168.0.1/16`(其中`192.168.0.1`是一个 IP 地址,`16`是一个子网掩码)。以下示例配置了一个 RemoteAddr 路由谓词: + +示例 10.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: remoteaddr_route + uri: https://example.org + predicates: + - RemoteAddr=192.168.1.1/24 +``` + +如果请求的远程地址是`192.168.1.10`,则此路由匹配。 + +#### [](#modifying-the-way-remote-addresses-are-resolved)[5.10.1.修改远程地址的解析方式](#modifying-the-way-remote-addresses-are-resolved) #### + +默认情况下,RemoteAddr 路由谓词工厂使用来自传入请求的远程地址。如果 Spring 云网关位于代理层的后面,这可能与实际的客户端 IP 地址不匹配。 + +你可以通过设置自定义`RemoteAddressResolver`来定制远程地址的解析方式。 Spring 云网关带有一个基于[X-forward-for 标头](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For),`XForwardedRemoteAddressResolver`的非默认远程地址解析器。 + +`XForwardedRemoteAddressResolver`有两个静态构造函数方法,它们采用不同的方法实现安全性: + +* `XForwardedRemoteAddressResolver::trustAll`返回一个`RemoteAddressResolver`,它总是使用在`X-Forwarded-For`头中找到的第一个 IP 地址。这种方法容易受到欺骗,因为恶意客户端可能会为`X-Forwarded-For`设置初始值,该初始值将被解析器接受。 + +* `XForwardedRemoteAddressResolver::maxTrustedIndex`获取一个索引,该索引与在 Spring 云网关前运行的受信任基础设施的数量相关。 Spring 如果云网关例如只能通过 HAProxy 访问,那么应该使用 1 的值。如果在访问 Spring 云网关之前需要两次跳可信的基础设施,那么应该使用 2 的值。 + +考虑以下标头值: + +``` +X-Forwarded-For: 0.0.0.1, 0.0.0.2, 0.0.0.3 +``` + +以下`maxTrustedIndex`值产生以下远程地址: + +| `maxTrustedIndex` |结果| +|------------------------|-----------------------------------------------------------| +|[`Integer.MIN_VALUE`,0] |(无效,`IllegalArgumentException`在初始化期间)| +| 1 | 0.0.0.3 | +| 2 | 0.0.0.2 | +| 3 | 0.0.0.1 | +|[4, `Integer.MAX_VALUE`]| 0.0.0.1 | + +下面的示例展示了如何使用 Java 实现相同的配置: + +例 11。gatewayconfig.java + +``` +RemoteAddressResolver resolver = XForwardedRemoteAddressResolver + .maxTrustedIndex(1); + +... + +.route("direct-route", + r -> r.remoteAddr("10.1.1.1", "10.10.1.1/24") + .uri("https://downstream1") +.route("proxied-route", + r -> r.remoteAddr(resolver, "10.10.1.1", "10.10.1.1/24") + .uri("https://downstream2") +) +``` + +### [](#the-weight-route-predicate-factory)[5.11.权重路径谓词工厂](#the-weight-route-predicate-factory) ### + +`Weight`路由谓词工厂接受两个参数:`group`和`weight`(一个 INT)。权重是按组计算的。下面的示例配置了权重路由谓词: + +示例 12.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: weight_high + uri: https://weighthigh.org + predicates: + - Weight=group1, 8 + - id: weight_low + uri: https://weightlow.org + predicates: + - Weight=group1, 2 +``` + +此路线将把 80% 的流量转发给[weighthigh.org](https://weighthigh.org),并将 20% 的流量转发给[weighlow.org](https://weighlow.org)。 + +### [](#the-xforwarded-remote-addr-route-predicate-factory)[5.12.XForwarded 远程 addr 路由谓词工厂](#the-xforwarded-remote-addr-route-predicate-factory) ### + +`XForwarded Remote Addr`路由谓词工厂接受一个`sources`的列表(最小大小为 1),这些字符串是 CIDR 表示法(IPv4 或 IPv6)字符串,例如`192.168.0.1/16`(其中`192.168.0.1`是一个 IP 地址,`16`是一个子网掩码)。 + +此路由谓词允许基于`X-Forwarded-For`HTTP 报头对请求进行过滤。 + +这可以用于反向代理,例如负载均衡器或 Web 应用程序防火墙,在这些代理中,只有当请求来自由这些反向代理使用的受信任的 IP 地址列表时,才允许请求。 + +下面的示例配置了一个 XForWardeDremoteaddr 路由谓词: + +示例 13.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: xforwarded_remoteaddr_route + uri: https://example.org + predicates: + - XForwardedRemoteAddr=192.168.1.1/24 +``` + +如果`X-Forwarded-For`标头包含`192.168.1.10`,则此路由匹配。 + +[](#gatewayfilter-factories)[6. `GatewayFilter` Factories](#gatewayfilter-factories) +---------- + +路由过滤器允许以某种方式修改传入 HTTP 请求或传出 HTTP 响应。路由过滤器的作用域是特定的路由。 Spring 云网关包括许多内置的网关过滤工厂。 + +| |有关如何使用以下任何过滤器的更详细示例,请查看[unit tests](https://github.com/spring-cloud/spring-cloud-gateway/tree/master/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory)。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#the-addrequestheader-gatewayfilter-factory)[6.1. The `AddRequestHeader` `GatewayFilter` Factory](#the-addrequestheader-gatewayfilter-factory) ### + +`AddRequestHeader``GatewayFilter`工厂接受一个`name`和`value`参数。以下示例配置`AddRequestHeader``GatewayFilter`: + +示例 14.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: add_request_header_route + uri: https://example.org + filters: + - AddRequestHeader=X-Request-red, blue +``` + +对于所有匹配的请求,此清单将`X-Request-red:blue`头添加到下游请求的头。 + +`AddRequestHeader`知道用于匹配路径或主机的 URI 变量。URI 变量可以在值中使用,并在运行时展开。下面的示例配置使用变量的`AddRequestHeader``GatewayFilter`: + +示例 15.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: add_request_header_route + uri: https://example.org + predicates: + - Path=/red/{segment} + filters: + - AddRequestHeader=X-Request-Red, Blue-{segment} +``` + +### [](#the-addrequestparameter-gatewayfilter-factory)[6.2. The `AddRequestParameter` `GatewayFilter` Factory](#the-addrequestparameter-gatewayfilter-factory) ### + +`AddRequestParameter``GatewayFilter`工厂接受`name`和`value`参数。下面的示例配置`AddRequestParameter``GatewayFilter`: + +示例 16.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: add_request_parameter_route + uri: https://example.org + filters: + - AddRequestParameter=red, blue +``` + +这将为所有匹配的请求将`red=blue`添加到下游请求的查询字符串中。 + +`AddRequestParameter`知道用于匹配路径或主机的 URI 变量。URI 变量可以在值中使用,并在运行时展开。下面的示例配置使用变量的`AddRequestParameter``GatewayFilter`: + +示例 17.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: add_request_parameter_route + uri: https://example.org + predicates: + - Host: {segment}.myhost.org + filters: + - AddRequestParameter=foo, bar-{segment} +``` + +### [](#the-addresponseheader-gatewayfilter-factory)[6.3. The `AddResponseHeader` `GatewayFilter` Factory](#the-addresponseheader-gatewayfilter-factory) ### + +`AddResponseHeader``GatewayFilter`工厂接受一个`name`和`value`参数。以下示例配置`AddResponseHeader``GatewayFilter`: + +示例 18.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: add_response_header_route + uri: https://example.org + filters: + - AddResponseHeader=X-Response-Red, Blue +``` + +这将为所有匹配的请求向下游响应的头添加`X-Response-Red:Blue`头。 + +`AddResponseHeader`知道用于匹配路径或主机的 URI 变量。URI 变量可以在值中使用,并在运行时展开。下面的示例配置了使用变量的`AddResponseHeader``GatewayFilter`: + +示例 19.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: add_response_header_route + uri: https://example.org + predicates: + - Host: {segment}.myhost.org + filters: + - AddResponseHeader=foo, bar-{segment} +``` + +### [](#the-deduperesponseheader-gatewayfilter-factory)[6.4. The `DedupeResponseHeader` `GatewayFilter` Factory](#the-deduperesponseheader-gatewayfilter-factory) ### + +DedupeResponseHeader 网关过滤器工厂接受一个`name`参数和一个可选的`strategy`参数。`name`可以包含一个以空格分隔的标题名称列表。以下示例配置`DedupeResponseHeader``GatewayFilter`: + +示例 20.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: dedupe_response_header_route + uri: https://example.org + filters: + - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin +``` + +在网关 CORS 逻辑和下游逻辑都添加响应头的情况下,这将删除重复的`Access-Control-Allow-Credentials`和`Access-Control-Allow-Origin`响应头的值。 + +`DedupeResponseHeader`过滤器还接受一个可选的`strategy`参数。接受的值是`RETAIN_FIRST`(默认)、`RETAIN_LAST`和`RETAIN_UNIQUE`。 + +### [](#spring-cloud-circuitbreaker-filter-factory)[6.5. Spring Cloud CircuitBreaker GatewayFilter Factory](#spring-cloud-circuitbreaker-filter-factory) ### + +Spring 云断路器网关过滤器工厂使用 Spring 云断路器 API 将网关路由封装在断路器中。 Spring Cloud Circuitbreaker 支持可与 Spring Cloud Gateway 一起使用的多个库。 Spring 云支持开箱即用的弹性 4J。 + +要启用 Spring 云电路断路器过滤器,你需要在 Classpath 上放置`spring-cloud-starter-circuitbreaker-reactor-resilience4j`。下面的示例配置 Spring 云电路断路器`GatewayFilter`: + +示例 21.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: circuitbreaker_route + uri: https://example.org + filters: + - CircuitBreaker=myCircuitBreaker +``` + +要配置断路器,请参阅你正在使用的底层断路器实现的配置。 + +* [复原力 4J 文档](https://cloud.spring.io/spring-cloud-circuitbreaker/reference/html/spring-cloud-circuitbreaker.html) + +Spring 云电路断路器过滤器还可以接受可选的`fallbackUri`参数。目前,只支持`forward:`模式 URI。如果回退被调用,请求将被转发到与 URI 匹配的控制器。下面的示例配置了这种回退: + +示例 22.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: circuitbreaker_route + uri: lb://backing-service:8088 + predicates: + - Path=/consumingServiceEndpoint + filters: + - name: CircuitBreaker + args: + name: myCircuitBreaker + fallbackUri: forward:/inCaseOfFailureUseThis + - RewritePath=/consumingServiceEndpoint, /backingServiceEndpoint +``` + +下面的列表在 Java 中做了相同的事情: + +例 23。application.java + +``` +@Bean +public RouteLocator routes(RouteLocatorBuilder builder) { + return builder.routes() + .route("circuitbreaker_route", r -> r.path("/consumingServiceEndpoint") + .filters(f -> f.circuitBreaker(c -> c.name("myCircuitBreaker").fallbackUri("forward:/inCaseOfFailureUseThis")) + .rewritePath("/consumingServiceEndpoint", "/backingServiceEndpoint")).uri("lb://backing-service:8088") + .build(); +} +``` + +当调用断路器回退时,此示例转发到`/inCaseofFailureUseThis`URI。请注意,此示例还演示了(可选的) Spring 云负载平衡器负载平衡(由目标 URI 上的`lb`前缀定义)。 + +主要的场景是使用`fallbackUri`在网关应用程序中定义内部控制器或处理程序。但是,你也可以将请求重新路由到外部应用程序中的控制器或处理程序,如下所示: + +示例 24.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: ingredients + uri: lb://ingredients + predicates: + - Path=//ingredients/** + filters: + - name: CircuitBreaker + args: + name: fetchIngredients + fallbackUri: forward:/fallback + - id: ingredients-fallback + uri: http://localhost:9994 + predicates: + - Path=/fallback +``` + +在此示例中,网关应用程序中没有`fallback`端点或处理程序。然而,在另一个应用程序中有一个,注册在`[localhost:9994](http://localhost:9994)`下。 + +在请求被转发到 Fallback 的情况下, Spring Cloud Circuitbreaker 网关过滤器还提供了导致它的`Throwable`。它被添加到`ServerWebExchange`中,作为`ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR`属性,该属性可以在处理网关应用程序内的回退时使用。 + +对于外部控制器/处理程序场景,可以添加带有异常详细信息的头。你可以在[FallbackHeaders GatewayFilter 工厂部分](#fallback-headers)中找到有关这样做的更多信息。 + +#### [](#circuit-breaker-status-codes)[6.5.1.按状态码切断断路器](#circuit-breaker-status-codes) #### + +在某些情况下,你可能希望基于从其封装的路由返回的状态码来跳闸断路器。断路器配置对象获取一系列状态代码,如果返回这些代码,将导致断路器跳闸。在设置要跳闸的状态码时,可以使用带有状态码值的整数,也可以使用`HttpStatus`枚举的字符串表示。 + +示例 25.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: circuitbreaker_route + uri: lb://backing-service:8088 + predicates: + - Path=/consumingServiceEndpoint + filters: + - name: CircuitBreaker + args: + name: myCircuitBreaker + fallbackUri: forward:/inCaseOfFailureUseThis + statusCodes: + - 500 + - "NOT_FOUND" +``` + +例 26。应用程序.java + +``` +@Bean +public RouteLocator routes(RouteLocatorBuilder builder) { + return builder.routes() + .route("circuitbreaker_route", r -> r.path("/consumingServiceEndpoint") + .filters(f -> f.circuitBreaker(c -> c.name("myCircuitBreaker").fallbackUri("forward:/inCaseOfFailureUseThis").addStatusCode("INTERNAL_SERVER_ERROR")) + .rewritePath("/consumingServiceEndpoint", "/backingServiceEndpoint")).uri("lb://backing-service:8088") + .build(); +} +``` + +### [](#fallback-headers)[6.6. The `FallbackHeaders` `GatewayFilter` Factory](#fallback-headers) ### + +`FallbackHeaders`工厂允许你在转发到外部应用程序中的`fallbackUri`的请求的标题中添加 Spring Cloud Circuitbreaker 执行异常详细信息,如下所示: + +示例 27.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: ingredients + uri: lb://ingredients + predicates: + - Path=//ingredients/** + filters: + - name: CircuitBreaker + args: + name: fetchIngredients + fallbackUri: forward:/fallback + - id: ingredients-fallback + uri: http://localhost:9994 + predicates: + - Path=/fallback + filters: + - name: FallbackHeaders + args: + executionExceptionTypeHeaderName: Test-Header +``` + +在本例中,在运行断路器时发生执行异常后,请求被转发到在`localhost:9994`上运行的应用程序中的`fallback`端点或处理程序。带有异常类型、消息和(如果可用的话)根原因异常类型和消息的头将由`FallbackHeaders`过滤器添加到该请求中。 + +你可以通过设置以下参数的值(以它们的默认值显示)来覆盖配置中的头的名称: + +* `executionExceptionTypeHeaderName`(`“execution-exception-type”`) + +* `executionExceptionMessageHeaderName`(`“execution-exception-message”`) + +* `rootCauseExceptionTypeHeaderName`(`“root-cause-exception-type”`) + +* `rootCauseExceptionMessageHeaderName`(`“root-cause-exception-message”`) + +有关断路器和网关的更多信息,请参见[Spring Cloud CircuitBreaker Factory section](#spring-cloud-circuitbreaker-filter-factory)。 + +### [](#the-maprequestheader-gatewayfilter-factory)[6.7. The `MapRequestHeader` `GatewayFilter` Factory](#the-maprequestheader-gatewayfilter-factory) ### + +`MapRequestHeader``GatewayFilter`工厂接受`fromHeader`和`toHeader`参数。它将创建一个新的命名头,并从传入的 HTTP 请求中从现有的命名头中提取该值。如果输入标头不存在,则过滤器不会产生任何影响。如果新的命名标头已经存在,那么它的值将被新的值扩充。以下示例配置`MapRequestHeader`: + +示例 28.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: map_request_header_route + uri: https://example.org + filters: + - MapRequestHeader=Blue, X-Request-Red +``` + +这会将`X-Request-Red:`头添加到下游请求,并从传入的 HTTP 请求的`Blue`头更新其值。 + +### [](#the-prefixpath-gatewayfilter-factory)[6.8. The `PrefixPath` `GatewayFilter` Factory](#the-prefixpath-gatewayfilter-factory) ### + +`PrefixPath``GatewayFilter`工厂接受一个`prefix`参数。下面的示例配置`PrefixPath``GatewayFilter`: + +示例 29.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: prefixpath_route + uri: https://example.org + filters: + - PrefixPath=/mypath +``` + +这将在所有匹配请求的路径前缀`/mypath`。因此,对`/hello`的请求将被发送到`/mypath/hello`。 + +### [](#the-preservehostheader-gatewayfilter-factory)[6.9. The `PreserveHostHeader` `GatewayFilter` Factory](#the-preservehostheader-gatewayfilter-factory) ### + +`PreserveHostHeader``GatewayFilter`工厂没有参数。此筛选器设置一个请求属性,由路由筛选器检查该属性,以确定是否应发送原始的主机头,而不是由 HTTP 客户机确定的主机头。以下示例配置`PreserveHostHeader``GatewayFilter`: + +示例 30.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: preserve_host_route + uri: https://example.org + filters: + - PreserveHostHeader +``` + +### [](#the-requestratelimiter-gatewayfilter-factory)[6.10. The `RequestRateLimiter` `GatewayFilter` Factory](#the-requestratelimiter-gatewayfilter-factory) ### + +`RequestRateLimiter``GatewayFilter`工厂使用`RateLimiter`实现来确定是否允许当前请求继续执行。如果不是,则返回`HTTP 429 - Too Many Requests`(默认情况下)的状态。 + +这个过滤器接受一个可选的`keyResolver`参数和特定于速率限制器的参数(将在本节后面描述)。 + +`keyResolver`是实现`KeyResolver`接口的 Bean。在配置中,使用 spel 按名称引用 Bean。`#{@mykeyresolver}` 是引用名为`myKeyResolver`的 Bean 的 spel 表达式。下面的清单显示了`KeyResolver`接口: + +例 31。keyresolver.java + +``` +public interface KeyResolver { + Mono resolve(ServerWebExchange exchange); +} +``` + +`KeyResolver`接口让可插入策略派生限制请求的键。在未来的里程碑版本中,将会有一些`KeyResolver`实现。 + +`KeyResolver`的默认实现是`PrincipalNameKeyResolver`,它从`ServerWebExchange`检索`Principal`并调用`Principal.getName()`。 + +默认情况下,如果`KeyResolver`没有找到密钥,请求将被拒绝。你可以通过设置`spring.cloud.gateway.filter.request-rate-limiter.deny-empty-key`(`true’或`false`)和`spring.cloud.gateway.filter.request-rate-limiter.empty-key-status-code`属性来调整此行为。 + +| |`RequestRateLimiter`不能使用“shortcut”符号进行配置。下面的示例是*无效*:

示例 32.application.properties

```
# 无效的快捷方式配置
Spring.cloud.gateway.routes[0].filters[0]=requestrateLimiter=2,2,#{@userkeyresolever=“426”/>```| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#the-redis-ratelimiter)[6.10.1. The Redis `RateLimiter`](#the-redis-ratelimiter) #### + +Redis 实现基于在[Stripe](https://stripe.com/blog/rate-limiters)上完成的工作。它需要使用`spring-boot-starter-data-redis-reactive` Spring 引导启动器。 + +使用的算法是[令牌桶算法](https://en.wikipedia.org/wiki/Token_bucket)。 + +`redis-rate-limiter.replenishRate`属性是你希望用户在不删除任何请求的情况下每秒可以执行多少个请求。这是令牌桶被填满的速率。 + +`redis-rate-limiter.burstCapacity`属性是用户在一秒钟内被允许执行的最大请求数。这是令牌桶可以容纳的令牌数量。将此值设置为零将阻止所有请求。 + +`redis-rate-limiter.requestedTokens`属性是一个请求需要多少令牌。这是每个请求从 bucket 中获取的令牌的数量,默认为`1`。 + +通过在`replenishRate`和`burstCapacity`中设置相同的值,可以实现稳定的速率。可以通过将`burstCapacity`设置为高于`replenishRate`来允许临时突发。在这种情况下,速率限制器需要允许在两次突发之间有一段时间(根据`replenishRate`),因为连续两次突发将导致丢弃请求(`HTTP429-太多请求’)。下面的清单配置了`redis-rate-limiter`: + +速率限制`1 request/s`通过将`replenishRate`设置为所需的请求数,`requestedTokens`设置为秒内的时间跨度,`burstCapacity`设置为`replenishRate`和`requestedTokens`的乘积,例如,设置`replenishRate=1`,`requestedTokens=60`和`burstCapacity=60`将导致`1 request/min`的限制。 + +示例 33.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: requestratelimiter_route + uri: https://example.org + filters: + - name: RequestRateLimiter + args: + redis-rate-limiter.replenishRate: 10 + redis-rate-limiter.burstCapacity: 20 + redis-rate-limiter.requestedTokens: 1 +``` + +下面的示例在 Java 中配置一个 keyresolver: + +例 34。config.java + +``` +@Bean +KeyResolver userKeyResolver() { + return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user")); +} +``` + +这定义了每个用户 10 个的请求速率限制。允许突发 20 个请求,但在接下来的一秒钟内,只有 10 个请求可用。`KeyResolver`是一个获得`user`请求参数的简单参数(请注意,这不推荐用于生产)。 + +还可以将速率限制器定义为实现`RateLimiter`接口的 Bean。在配置中,你可以使用 spel 按名称引用 Bean。`#{@myratelimiter}` 是一个 spel 表达式,它引用名为`myRateLimiter`的 Bean。下面的清单定义了一个速率限制器,该限制器使用上一个清单中定义的`KeyResolver`: + +示例 35.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: requestratelimiter_route + uri: https://example.org + filters: + - name: RequestRateLimiter + args: + rate-limiter: "#{@myRateLimiter}" + key-resolver: "#{@userKeyResolver}" +``` + +### [](#the-redirectto-gatewayfilter-factory)[6.11. The `RedirectTo` `GatewayFilter` Factory](#the-redirectto-gatewayfilter-factory) ### + +`RedirectTo``GatewayFilter`工厂接受两个参数,`status`和`url`。`status`参数应该是 300 系列重定向 HTTP 代码,例如 301。`url`参数应该是一个有效的 URL。这是`Location`标头的值。对于相对重定向,应该使用`uri: no://op`作为路由定义的 URI。下面的列表配置了`RedirectTo``GatewayFilter`: + +示例 36.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: prefixpath_route + uri: https://example.org + filters: + - RedirectTo=302, https://acme.org +``` + +这将发送带有`Location:https://acme.org`头的状态 302 来执行重定向。 + +### [](#the-removerequestheader-gatewayfilter-factory)[6.12. The `RemoveRequestHeader` GatewayFilter Factory](#the-removerequestheader-gatewayfilter-factory) ### + +`RemoveRequestHeader``GatewayFilter`工厂接受一个`name`参数。它是要删除的标头的名称。下面的列表配置了`RemoveRequestHeader``GatewayFilter`: + +示例 37.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: removerequestheader_route + uri: https://example.org + filters: + - RemoveRequestHeader=X-Request-Foo +``` + +这将在向下游发送`X-Request-Foo`头之前删除它。 + +### [](#removeresponseheader-gatewayfilter-factory)[6.13. `RemoveResponseHeader` `GatewayFilter` Factory](#removeresponseheader-gatewayfilter-factory) ### + +`RemoveResponseHeader``GatewayFilter`工厂接受一个`name`参数。它是要删除的标头的名称。下面的列表配置了`RemoveResponseHeader``GatewayFilter`: + +示例 38.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: removeresponseheader_route + uri: https://example.org + filters: + - RemoveResponseHeader=X-Response-Foo +``` + +这将在响应返回到网关客户机之前从响应中删除`X-Response-Foo`头。 + +要删除任何类型的敏感报头,你应该为你可能想要删除的任何路由配置此筛选器。此外,你可以使用`spring.cloud.gateway.default-filters`配置该过滤器一次,并将其应用于所有路由。 + +### [](#the-removerequestparameter-gatewayfilter-factory)[6.14. The `RemoveRequestParameter` `GatewayFilter` Factory](#the-removerequestparameter-gatewayfilter-factory) ### + +`RemoveRequestParameter``GatewayFilter`工厂接受一个`name`参数。它是要删除的查询参数的名称。下面的示例配置`RemoveRequestParameter``GatewayFilter`: + +示例 39.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: removerequestparameter_route + uri: https://example.org + filters: + - RemoveRequestParameter=red +``` + +这将在向下游发送`red`参数之前删除该参数。 + +### [](#the-rewritepath-gatewayfilter-factory)[6.15. The `RewritePath` `GatewayFilter` Factory](#the-rewritepath-gatewayfilter-factory) ### + +`RewritePath``GatewayFilter`工厂接受一个路径`regexp`参数和一个`replacement`参数。这使用 Java 正则表达式以灵活的方式重写请求路径。下面的列表配置了`RewritePath``GatewayFilter`: + +示例 40.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: rewritepath_route + uri: https://example.org + predicates: + - Path=/red/** + filters: + - RewritePath=/red/?(?.*), /$\{segment} +``` + +对于`/red/blue`的请求路径,在发出下游请求之前,将路径设置为`/blue`。注意,由于 YAML 规范,`Spring 云网关 +========== + + +该项目提供了一个构建在 Spring 生态系统之上的 API 网关,包括: Spring 5、 Spring Boot2 和 Project Reactor。 Spring Cloud Gateway 旨在提供一种简单但有效的方法来路由到 API,并向它们提供跨领域的关注,例如:安全性、监视/度量和弹性。 + +[](#gateway-starter)[1. How to Include Spring Cloud Gateway](#gateway-starter) +---------- + +要在项目中包含 Spring 云网关,请使用组 ID 为`org.springframework.cloud`和工件 ID 为`spring-cloud-starter-gateway`的 starter。请参阅[Spring Cloud Project page](https://projects.spring.io/spring-cloud/),以获取有关使用当前 Spring 云发布系列设置构建系统的详细信息。 + +如果包含启动器,但不希望启用网关,请设置`spring.cloud.gateway.enabled=false`。 + +| |Spring 云网关是建立在[Spring Boot 2.x](https://spring.io/projects/spring-boot#learn)、[Spring WebFlux](https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html)和[Project Reactor](https://projectreactor.io/docs)之上的。因此,当你使用 Spring 云网关时,许多你熟悉的同步库(例如 Spring 数据和 Spring 安全性)和模式可能不适用,如果你不熟悉这些项目,我们建议你在使用 Spring Cloud Gateway 之前,先阅读他们的文档,以熟悉一些新概念。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |Spring 云网关需要由 Spring 启动和 Spring WebFlux 提供的 Netty 运行时。它在传统 Servlet 容器中或构建为 WAR 时不工作。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[](#glossary)[2. Glossary](#glossary) +---------- + +* **路线**:网关的基本构件。它由一个 ID、一个目标 URI、一组谓词和一组筛选器定义。如果聚合谓词为 true,则匹配路由。 + +* **谓词**:这是[Java8 函数谓词](https://docs.oracle.com/javase/8/docs/api/java/util/function/Predicate.html)。输入类型为[Spring Framework `ServerWebExchange`](https://docs.spring.io/spring/docs/5.0.x/javadoc-api/org/springframework/web/server/ServerWebExchange.html)。这使你能够匹配 HTTP 请求中的任何内容,例如标题或参数。 + +* **过滤器**:这些是用特定工厂构造的[`GatewayFilter`](https://github.com/spring-cloud/spring-cloud-gateway/tree/main/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/GatewayFilter.java)的实例。在这里,你可以在发送下游请求之前或之后修改请求和响应。 + +[](#gateway-how-it-works)[3. How It Works](#gateway-how-it-works) +---------- + +下图提供了 Spring 云网关如何工作的高级概述: + +![Spring Cloud Gateway Diagram](./images/spring_cloud_gateway_diagram.png) + +客户端向 Spring 云网关提出请求。如果网关处理程序映射确定请求与路由匹配,则将请求发送到网关 Web 处理程序。此处理程序通过特定于该请求的筛选链来运行该请求。用虚线划分过滤器的原因是,过滤器可以在发送代理请求之前和之后运行逻辑。所有的“pre”过滤逻辑都会被执行。然后提出代理请求。在提出代理请求之后,将运行“POST”过滤逻辑。 + +| |在没有端口的路由中定义的 URI 分别获得 HTTP 和 HTTPS URI 的默认端口号 80 和 443。| +|---|----------------------------------------------------------------------------------------------------------------------| + +[](#configuring-route-predicate-factories-and-gateway-filter-factories)[4.配置路由谓词工厂和网关过滤器工厂](#configuring-route-predicate-factories-and-gateway-filter-factories) +---------- + +配置谓词和过滤器有两种方法:快捷方式和完全展开的参数。下面的大多数示例都使用了快捷方式。 + +名称和参数名称将以`code`的形式在每个部分的第一个或两个表示中列出。参数通常按快捷方式配置所需的顺序列出。 + +### [](#shortcut-configuration)[4.1.快捷方式配置](#shortcut-configuration) ### + +快捷方式配置由筛选器名称识别,后面跟着一个等号,后面是用逗号分隔的参数值。 + +应用程序.yml + +``` +spring: + cloud: + gateway: + routes: + - id: after_route + uri: https://example.org + predicates: + - Cookie=mycookie,mycookievalue +``` + +上一个示例用两个参数定义了`Cookie`路由谓词工厂,cookie 名`mycookie`和要匹配`mycookievalue`的值。 + +### [](#fully-expanded-arguments)[4.2.完全展开的论证](#fully-expanded-arguments) ### + +完全展开的参数看起来更像是带有名称/值对的标准 YAML 配置。通常,会有`name`键和`args`键。`args`键是用于配置谓词或筛选器的键值对的映射。 + +应用程序.yml + +``` +spring: + cloud: + gateway: + routes: + - id: after_route + uri: https://example.org + predicates: + - name: Cookie + args: + name: mycookie + regexp: mycookievalue +``` + +这是上面显示的`Cookie`谓词的快捷配置的完整配置。 + +[](#gateway-request-predicates-factories)[5.路线谓词工厂](#gateway-request-predicates-factories) +---------- + +Spring 云网关将路由匹配为 Spring WebFlux`HandlerMapping`基础设施的一部分。 Spring 云网关包括许多内置的路由谓词工厂。所有这些谓词在 HTTP 请求的不同属性上匹配。你可以将多个路由谓词工厂与逻辑`and`语句组合在一起。 + +### [](#the-after-route-predicate-factory)[5.1.后路由谓词工厂](#the-after-route-predicate-factory) ### + +`After`路由谓词工厂接受一个参数,一个`datetime`(这是一个 Java`ZonedDateTime`)。此谓词匹配在指定的 DateTime 之后发生的请求。下面的示例配置一个 after 路由谓词: + +示例 1.应用程序.yml + +``` +spring: + cloud: + gateway: + routes: + - id: after_route + uri: https://example.org + predicates: + - After=2017-01-20T17:42:47.789-07:00[America/Denver] +``` + +这条路线符合任何请求后提出的 JAN20,2017 年 17:42 山区时间(丹佛)。 + +### [](#the-before-route-predicate-factory)[5.2.前路由谓词工厂](#the-before-route-predicate-factory) ### + +`Before`路由谓词工厂接受一个参数,a`datetime`(这是一个 Java`ZonedDateTime`)。此谓词匹配在指定的`datetime`之前发生的请求。下面的示例配置一个 before 路由谓词: + +示例 2.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: before_route + uri: https://example.org + predicates: + - Before=2017-01-20T17:42:47.789-07:00[America/Denver] +``` + +这条路线符合任何在 JAN20,2017 年 17:42 山区时间(丹佛)之前提出的请求。 + +### [](#the-between-route-predicate-factory)[5.3.路由谓词之间的工厂](#the-between-route-predicate-factory) ### + +`Between`路由谓词工厂接受两个参数,`datetime1`和`datetime2`,它们是 Java`ZonedDateTime`对象。此谓词匹配发生在`datetime1`之后和`datetime2`之前的请求。`datetime2`参数必须位于`datetime1`之后。下面的示例配置了一个 between 路由谓词: + +示例 3.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: between_route + uri: https://example.org + predicates: + - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver] +``` + +这条路线符合在 JAN20,2017 年 17:42 山区时间(丹佛)之后和 JAN21,2017 年 17:42 山区时间(丹佛)之前提出的任何请求。这对于维护窗口可能是有用的。 + +### [](#the-cookie-route-predicate-factory)[5.4.cookie 路由谓词工厂](#the-cookie-route-predicate-factory) ### + +`Cookie`路由谓词工厂接受两个参数,cookie`name`和`regexp`(这是一个 Java 正则表达式)。此谓词匹配具有给定名称且其值与正则表达式匹配的 cookie。下面的示例配置了一个 Cookie 路由谓词工厂: + +示例 4.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: cookie_route + uri: https://example.org + predicates: + - Cookie=chocolate, ch.p +``` + +此路由匹配具有名为`chocolate`的 cookie 的请求,其值与`ch.p`正则表达式匹配。 + +### [](#the-header-route-predicate-factory)[5.5.头路由谓词工厂](#the-header-route-predicate-factory) ### + +`Header`路由谓词工厂接受两个参数,`header`和`regexp`(这是一个 Java 正则表达式)。此谓词与具有给定名称的头匹配,其值与正则表达式匹配。下面的示例配置头路由谓词: + +示例 5.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: header_route + uri: https://example.org + predicates: + - Header=X-Request-Id, \d+ +``` + +如果请求有一个名为`X-Request-Id`的头,其值与`\d+`正则表达式匹配(即它有一个或多个数字的值),则此路由匹配。 + +### [](#the-host-route-predicate-factory)[5.6.主机路由谓词工厂](#the-host-route-predicate-factory) ### + +`Host`路由谓词工厂接受一个参数:主机名列表`patterns`。该模式是一种 Ant 样式的模式,以`.`作为分隔符。此谓词匹配与模式匹配的`Host`头。下面的示例配置了一个主机路由谓词: + +示例 6.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: host_route + uri: https://example.org + predicates: + - Host=**.somehost.org,**.anotherhost.org +``` + +URI 模板变量(如`{sub}.myhost.org`)也受到支持。 + +如果请求具有`Host`头,其值为`www.somehost.org`或`beta.somehost.org`或`www.anotherhost.org`,则此路由匹配。 + +这个谓词提取 URI 模板变量(例如`sub`,在前面的示例中定义)作为名称和值的映射,并将其放置在`ServerWebExchange.getAttributes()`中,并在`ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE`中定义一个键。这些值可由[“GatewayFilter”工厂](#gateway-route-filters)使用 + +### [](#the-method-route-predicate-factory)[5.7.路由谓词工厂的方法](#the-method-route-predicate-factory) ### + +`Method`路由谓词工厂接受一个`methods`参数,该参数是一个或多个参数:要匹配的 HTTP 方法。下面的示例配置了一个方法路由谓词: + +示例 7.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: method_route + uri: https://example.org + predicates: + - Method=GET,POST +``` + +如果请求方法是`GET`或`POST`,则此路由匹配。 + +### [](#the-path-route-predicate-factory)[5.8.路径谓词工厂](#the-path-route-predicate-factory) ### + +`Path`路由谓词工厂接受两个参数: Spring `PathMatcher``patterns`的列表和一个名为`matchTrailingSlash`的可选标志(默认为`true`)。下面的示例配置了一个路径路由谓词: + +示例 8.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: path_route + uri: https://example.org + predicates: + - Path=/red/{segment},/blue/{segment} +``` + +如果请求路径是:例如:`/red/1`或`/red/1/`或`/red/blue`或`/blue/green`,则此路由匹配。 + +如果`matchTrailingSlash`被设置为`false`,那么请求路径`/red/1/`将不会被匹配。 + +这个谓词提取 URI 模板变量(例如`segment`,在前面的示例中定义)作为名称和值的映射,并将其放置在`ServerWebExchange.getAttributes()`中,并在`ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE`中定义一个键。这些值可由[“GatewayFilter”工厂](#gateway-route-filters)使用 + +一种实用方法(称为`get`)可以使访问这些变量变得更容易。下面的示例展示了如何使用`get`方法: + +``` +Map uriVariables = ServerWebExchangeUtils.getPathPredicateVariables(exchange); + +String segment = uriVariables.get("segment"); +``` + +### [](#the-query-route-predicate-factory)[5.9.查询路由谓词工厂](#the-query-route-predicate-factory) ### + +`Query`路由谓词工厂接受两个参数:一个必需的`param`和一个可选的`regexp`(这是一个 Java 正则表达式)。下面的示例配置一个查询路由谓词: + +示例 9.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: query_route + uri: https://example.org + predicates: + - Query=green +``` + +如果请求包含`green`查询参数,则前面的路由匹配。 + +application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: query_route + uri: https://example.org + predicates: + - Query=red, gree. +``` + +如果请求包含一个`red`查询参数,其值与`gree.`regexp 匹配,则前面的路由匹配,因此`green`和`greet`将匹配。 + +### [](#the-remoteaddr-route-predicate-factory)[5.10.RemoteAddr 路由谓词工厂](#the-remoteaddr-route-predicate-factory) ### + +`RemoteAddr`路由谓词工厂接受一个`sources`的列表(最小大小为 1),这些字符串是 CIDR 表示法(IPv4 或 IPv6)字符串,例如`192.168.0.1/16`(其中`192.168.0.1`是一个 IP 地址,`16`是一个子网掩码)。以下示例配置了一个 RemoteAddr 路由谓词: + +示例 10.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: remoteaddr_route + uri: https://example.org + predicates: + - RemoteAddr=192.168.1.1/24 +``` + +如果请求的远程地址是`192.168.1.10`,则此路由匹配。 + +#### [](#modifying-the-way-remote-addresses-are-resolved)[5.10.1.修改远程地址的解析方式](#modifying-the-way-remote-addresses-are-resolved) #### + +默认情况下,RemoteAddr 路由谓词工厂使用来自传入请求的远程地址。如果 Spring 云网关位于代理层的后面,这可能与实际的客户端 IP 地址不匹配。 + +你可以通过设置自定义`RemoteAddressResolver`来定制远程地址的解析方式。 Spring 云网关带有一个基于[X-forward-for 标头](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For),`XForwardedRemoteAddressResolver`的非默认远程地址解析器。 + +`XForwardedRemoteAddressResolver`有两个静态构造函数方法,它们采用不同的方法实现安全性: + +* `XForwardedRemoteAddressResolver::trustAll`返回一个`RemoteAddressResolver`,它总是使用在`X-Forwarded-For`头中找到的第一个 IP 地址。这种方法容易受到欺骗,因为恶意客户端可能会为`X-Forwarded-For`设置初始值,该初始值将被解析器接受。 + +* `XForwardedRemoteAddressResolver::maxTrustedIndex`获取一个索引,该索引与在 Spring 云网关前运行的受信任基础设施的数量相关。 Spring 如果云网关例如只能通过 HAProxy 访问,那么应该使用 1 的值。如果在访问 Spring 云网关之前需要两次跳可信的基础设施,那么应该使用 2 的值。 + +考虑以下标头值: + +``` +X-Forwarded-For: 0.0.0.1, 0.0.0.2, 0.0.0.3 +``` + +以下`maxTrustedIndex`值产生以下远程地址: + +| `maxTrustedIndex` |结果| +|------------------------|-----------------------------------------------------------| +|[`Integer.MIN_VALUE`,0] |(无效,`IllegalArgumentException`在初始化期间)| +| 1 | 0.0.0.3 | +| 2 | 0.0.0.2 | +| 3 | 0.0.0.1 | +|[4, `Integer.MAX_VALUE`]| 0.0.0.1 | + +下面的示例展示了如何使用 Java 实现相同的配置: + +例 11。gatewayconfig.java + +``` +RemoteAddressResolver resolver = XForwardedRemoteAddressResolver + .maxTrustedIndex(1); + +... + +.route("direct-route", + r -> r.remoteAddr("10.1.1.1", "10.10.1.1/24") + .uri("https://downstream1") +.route("proxied-route", + r -> r.remoteAddr(resolver, "10.10.1.1", "10.10.1.1/24") + .uri("https://downstream2") +) +``` + +### [](#the-weight-route-predicate-factory)[5.11.权重路径谓词工厂](#the-weight-route-predicate-factory) ### + +`Weight`路由谓词工厂接受两个参数:`group`和`weight`(一个 INT)。权重是按组计算的。下面的示例配置了权重路由谓词: + +示例 12.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: weight_high + uri: https://weighthigh.org + predicates: + - Weight=group1, 8 + - id: weight_low + uri: https://weightlow.org + predicates: + - Weight=group1, 2 +``` + +此路线将把 80% 的流量转发给[weighthigh.org](https://weighthigh.org),并将 20% 的流量转发给[weighlow.org](https://weighlow.org)。 + +### [](#the-xforwarded-remote-addr-route-predicate-factory)[5.12.XForwarded 远程 addr 路由谓词工厂](#the-xforwarded-remote-addr-route-predicate-factory) ### + +`XForwarded Remote Addr`路由谓词工厂接受一个`sources`的列表(最小大小为 1),这些字符串是 CIDR 表示法(IPv4 或 IPv6)字符串,例如`192.168.0.1/16`(其中`192.168.0.1`是一个 IP 地址,`16`是一个子网掩码)。 + +此路由谓词允许基于`X-Forwarded-For`HTTP 报头对请求进行过滤。 + +这可以用于反向代理,例如负载均衡器或 Web 应用程序防火墙,在这些代理中,只有当请求来自由这些反向代理使用的受信任的 IP 地址列表时,才允许请求。 + +下面的示例配置了一个 XForWardeDremoteaddr 路由谓词: + +示例 13.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: xforwarded_remoteaddr_route + uri: https://example.org + predicates: + - XForwardedRemoteAddr=192.168.1.1/24 +``` + +如果`X-Forwarded-For`标头包含`192.168.1.10`,则此路由匹配。 + +[](#gatewayfilter-factories)[6. `GatewayFilter` Factories](#gatewayfilter-factories) +---------- + +路由过滤器允许以某种方式修改传入 HTTP 请求或传出 HTTP 响应。路由过滤器的作用域是特定的路由。 Spring 云网关包括许多内置的网关过滤工厂。 + +| |有关如何使用以下任何过滤器的更详细示例,请查看[unit tests](https://github.com/spring-cloud/spring-cloud-gateway/tree/master/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory)。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#the-addrequestheader-gatewayfilter-factory)[6.1. The `AddRequestHeader` `GatewayFilter` Factory](#the-addrequestheader-gatewayfilter-factory) ### + +`AddRequestHeader``GatewayFilter`工厂接受一个`name`和`value`参数。以下示例配置`AddRequestHeader``GatewayFilter`: + +示例 14.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: add_request_header_route + uri: https://example.org + filters: + - AddRequestHeader=X-Request-red, blue +``` + +对于所有匹配的请求,此清单将`X-Request-red:blue`头添加到下游请求的头。 + +`AddRequestHeader`知道用于匹配路径或主机的 URI 变量。URI 变量可以在值中使用,并在运行时展开。下面的示例配置使用变量的`AddRequestHeader``GatewayFilter`: + +示例 15.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: add_request_header_route + uri: https://example.org + predicates: + - Path=/red/{segment} + filters: + - AddRequestHeader=X-Request-Red, Blue-{segment} +``` + +### [](#the-addrequestparameter-gatewayfilter-factory)[6.2. The `AddRequestParameter` `GatewayFilter` Factory](#the-addrequestparameter-gatewayfilter-factory) ### + +`AddRequestParameter``GatewayFilter`工厂接受`name`和`value`参数。下面的示例配置`AddRequestParameter``GatewayFilter`: + +示例 16.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: add_request_parameter_route + uri: https://example.org + filters: + - AddRequestParameter=red, blue +``` + +这将为所有匹配的请求将`red=blue`添加到下游请求的查询字符串中。 + +`AddRequestParameter`知道用于匹配路径或主机的 URI 变量。URI 变量可以在值中使用,并在运行时展开。下面的示例配置使用变量的`AddRequestParameter``GatewayFilter`: + +示例 17.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: add_request_parameter_route + uri: https://example.org + predicates: + - Host: {segment}.myhost.org + filters: + - AddRequestParameter=foo, bar-{segment} +``` + +### [](#the-addresponseheader-gatewayfilter-factory)[6.3. The `AddResponseHeader` `GatewayFilter` Factory](#the-addresponseheader-gatewayfilter-factory) ### + +`AddResponseHeader``GatewayFilter`工厂接受一个`name`和`value`参数。以下示例配置`AddResponseHeader``GatewayFilter`: + +示例 18.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: add_response_header_route + uri: https://example.org + filters: + - AddResponseHeader=X-Response-Red, Blue +``` + +这将为所有匹配的请求向下游响应的头添加`X-Response-Red:Blue`头。 + +`AddResponseHeader`知道用于匹配路径或主机的 URI 变量。URI 变量可以在值中使用,并在运行时展开。下面的示例配置了使用变量的`AddResponseHeader``GatewayFilter`: + +示例 19.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: add_response_header_route + uri: https://example.org + predicates: + - Host: {segment}.myhost.org + filters: + - AddResponseHeader=foo, bar-{segment} +``` + +### [](#the-deduperesponseheader-gatewayfilter-factory)[6.4. The `DedupeResponseHeader` `GatewayFilter` Factory](#the-deduperesponseheader-gatewayfilter-factory) ### + +DedupeResponseHeader 网关过滤器工厂接受一个`name`参数和一个可选的`strategy`参数。`name`可以包含一个以空格分隔的标题名称列表。以下示例配置`DedupeResponseHeader``GatewayFilter`: + +示例 20.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: dedupe_response_header_route + uri: https://example.org + filters: + - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin +``` + +在网关 CORS 逻辑和下游逻辑都添加响应头的情况下,这将删除重复的`Access-Control-Allow-Credentials`和`Access-Control-Allow-Origin`响应头的值。 + +`DedupeResponseHeader`过滤器还接受一个可选的`strategy`参数。接受的值是`RETAIN_FIRST`(默认)、`RETAIN_LAST`和`RETAIN_UNIQUE`。 + +### [](#spring-cloud-circuitbreaker-filter-factory)[6.5. Spring Cloud CircuitBreaker GatewayFilter Factory](#spring-cloud-circuitbreaker-filter-factory) ### + +Spring 云断路器网关过滤器工厂使用 Spring 云断路器 API 将网关路由封装在断路器中。 Spring Cloud Circuitbreaker 支持可与 Spring Cloud Gateway 一起使用的多个库。 Spring 云支持开箱即用的弹性 4J。 + +要启用 Spring 云电路断路器过滤器,你需要在 Classpath 上放置`spring-cloud-starter-circuitbreaker-reactor-resilience4j`。下面的示例配置 Spring 云电路断路器`GatewayFilter`: + +示例 21.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: circuitbreaker_route + uri: https://example.org + filters: + - CircuitBreaker=myCircuitBreaker +``` + +要配置断路器,请参阅你正在使用的底层断路器实现的配置。 + +* [复原力 4J 文档](https://cloud.spring.io/spring-cloud-circuitbreaker/reference/html/spring-cloud-circuitbreaker.html) + +Spring 云电路断路器过滤器还可以接受可选的`fallbackUri`参数。目前,只支持`forward:`模式 URI。如果回退被调用,请求将被转发到与 URI 匹配的控制器。下面的示例配置了这种回退: + +示例 22.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: circuitbreaker_route + uri: lb://backing-service:8088 + predicates: + - Path=/consumingServiceEndpoint + filters: + - name: CircuitBreaker + args: + name: myCircuitBreaker + fallbackUri: forward:/inCaseOfFailureUseThis + - RewritePath=/consumingServiceEndpoint, /backingServiceEndpoint +``` + +下面的列表在 Java 中做了相同的事情: + +例 23。application.java + +``` +@Bean +public RouteLocator routes(RouteLocatorBuilder builder) { + return builder.routes() + .route("circuitbreaker_route", r -> r.path("/consumingServiceEndpoint") + .filters(f -> f.circuitBreaker(c -> c.name("myCircuitBreaker").fallbackUri("forward:/inCaseOfFailureUseThis")) + .rewritePath("/consumingServiceEndpoint", "/backingServiceEndpoint")).uri("lb://backing-service:8088") + .build(); +} +``` + +当调用断路器回退时,此示例转发到`/inCaseofFailureUseThis`URI。请注意,此示例还演示了(可选的) Spring 云负载平衡器负载平衡(由目标 URI 上的`lb`前缀定义)。 + +主要的场景是使用`fallbackUri`在网关应用程序中定义内部控制器或处理程序。但是,你也可以将请求重新路由到外部应用程序中的控制器或处理程序,如下所示: + +示例 24.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: ingredients + uri: lb://ingredients + predicates: + - Path=//ingredients/** + filters: + - name: CircuitBreaker + args: + name: fetchIngredients + fallbackUri: forward:/fallback + - id: ingredients-fallback + uri: http://localhost:9994 + predicates: + - Path=/fallback +``` + +在此示例中,网关应用程序中没有`fallback`端点或处理程序。然而,在另一个应用程序中有一个,注册在`[localhost:9994](http://localhost:9994)`下。 + +在请求被转发到 Fallback 的情况下, Spring Cloud Circuitbreaker 网关过滤器还提供了导致它的`Throwable`。它被添加到`ServerWebExchange`中,作为`ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR`属性,该属性可以在处理网关应用程序内的回退时使用。 + +对于外部控制器/处理程序场景,可以添加带有异常详细信息的头。你可以在[FallbackHeaders GatewayFilter 工厂部分](#fallback-headers)中找到有关这样做的更多信息。 + +#### [](#circuit-breaker-status-codes)[6.5.1.按状态码切断断路器](#circuit-breaker-status-codes) #### + +在某些情况下,你可能希望基于从其封装的路由返回的状态码来跳闸断路器。断路器配置对象获取一系列状态代码,如果返回这些代码,将导致断路器跳闸。在设置要跳闸的状态码时,可以使用带有状态码值的整数,也可以使用`HttpStatus`枚举的字符串表示。 + +示例 25.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: circuitbreaker_route + uri: lb://backing-service:8088 + predicates: + - Path=/consumingServiceEndpoint + filters: + - name: CircuitBreaker + args: + name: myCircuitBreaker + fallbackUri: forward:/inCaseOfFailureUseThis + statusCodes: + - 500 + - "NOT_FOUND" +``` + +例 26。应用程序.java + +``` +@Bean +public RouteLocator routes(RouteLocatorBuilder builder) { + return builder.routes() + .route("circuitbreaker_route", r -> r.path("/consumingServiceEndpoint") + .filters(f -> f.circuitBreaker(c -> c.name("myCircuitBreaker").fallbackUri("forward:/inCaseOfFailureUseThis").addStatusCode("INTERNAL_SERVER_ERROR")) + .rewritePath("/consumingServiceEndpoint", "/backingServiceEndpoint")).uri("lb://backing-service:8088") + .build(); +} +``` + +### [](#fallback-headers)[6.6. The `FallbackHeaders` `GatewayFilter` Factory](#fallback-headers) ### + +`FallbackHeaders`工厂允许你在转发到外部应用程序中的`fallbackUri`的请求的标题中添加 Spring Cloud Circuitbreaker 执行异常详细信息,如下所示: + +示例 27.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: ingredients + uri: lb://ingredients + predicates: + - Path=//ingredients/** + filters: + - name: CircuitBreaker + args: + name: fetchIngredients + fallbackUri: forward:/fallback + - id: ingredients-fallback + uri: http://localhost:9994 + predicates: + - Path=/fallback + filters: + - name: FallbackHeaders + args: + executionExceptionTypeHeaderName: Test-Header +``` + +在本例中,在运行断路器时发生执行异常后,请求被转发到在`localhost:9994`上运行的应用程序中的`fallback`端点或处理程序。带有异常类型、消息和(如果可用的话)根原因异常类型和消息的头将由`FallbackHeaders`过滤器添加到该请求中。 + +你可以通过设置以下参数的值(以它们的默认值显示)来覆盖配置中的头的名称: + +* `executionExceptionTypeHeaderName`(`“execution-exception-type”`) + +* `executionExceptionMessageHeaderName`(`“execution-exception-message”`) + +* `rootCauseExceptionTypeHeaderName`(`“root-cause-exception-type”`) + +* `rootCauseExceptionMessageHeaderName`(`“root-cause-exception-message”`) + +有关断路器和网关的更多信息,请参见[Spring Cloud CircuitBreaker Factory section](#spring-cloud-circuitbreaker-filter-factory)。 + +### [](#the-maprequestheader-gatewayfilter-factory)[6.7. The `MapRequestHeader` `GatewayFilter` Factory](#the-maprequestheader-gatewayfilter-factory) ### + +`MapRequestHeader``GatewayFilter`工厂接受`fromHeader`和`toHeader`参数。它将创建一个新的命名头,并从传入的 HTTP 请求中从现有的命名头中提取该值。如果输入标头不存在,则过滤器不会产生任何影响。如果新的命名标头已经存在,那么它的值将被新的值扩充。以下示例配置`MapRequestHeader`: + +示例 28.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: map_request_header_route + uri: https://example.org + filters: + - MapRequestHeader=Blue, X-Request-Red +``` + +这会将`X-Request-Red:`头添加到下游请求,并从传入的 HTTP 请求的`Blue`头更新其值。 + +### [](#the-prefixpath-gatewayfilter-factory)[6.8. The `PrefixPath` `GatewayFilter` Factory](#the-prefixpath-gatewayfilter-factory) ### + +`PrefixPath``GatewayFilter`工厂接受一个`prefix`参数。下面的示例配置`PrefixPath``GatewayFilter`: + +示例 29.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: prefixpath_route + uri: https://example.org + filters: + - PrefixPath=/mypath +``` + +这将在所有匹配请求的路径前缀`/mypath`。因此,对`/hello`的请求将被发送到`/mypath/hello`。 + +### [](#the-preservehostheader-gatewayfilter-factory)[6.9. The `PreserveHostHeader` `GatewayFilter` Factory](#the-preservehostheader-gatewayfilter-factory) ### + +`PreserveHostHeader``GatewayFilter`工厂没有参数。此筛选器设置一个请求属性,由路由筛选器检查该属性,以确定是否应发送原始的主机头,而不是由 HTTP 客户机确定的主机头。以下示例配置`PreserveHostHeader``GatewayFilter`: + +示例 30.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: preserve_host_route + uri: https://example.org + filters: + - PreserveHostHeader +``` + +### [](#the-requestratelimiter-gatewayfilter-factory)[6.10. The `RequestRateLimiter` `GatewayFilter` Factory](#the-requestratelimiter-gatewayfilter-factory) ### + +`RequestRateLimiter``GatewayFilter`工厂使用`RateLimiter`实现来确定是否允许当前请求继续执行。如果不是,则返回`HTTP 429 - Too Many Requests`(默认情况下)的状态。 + +这个过滤器接受一个可选的`keyResolver`参数和特定于速率限制器的参数(将在本节后面描述)。 + +`keyResolver`是实现`KeyResolver`接口的 Bean。在配置中,使用 spel 按名称引用 Bean。`#{@mykeyresolver}` 是引用名为`myKeyResolver`的 Bean 的 spel 表达式。下面的清单显示了`KeyResolver`接口: + +例 31。keyresolver.java + +``` +public interface KeyResolver { + Mono resolve(ServerWebExchange exchange); +} +``` + +`KeyResolver`接口让可插入策略派生限制请求的键。在未来的里程碑版本中,将会有一些`KeyResolver`实现。 + +`KeyResolver`的默认实现是`PrincipalNameKeyResolver`,它从`ServerWebExchange`检索`Principal`并调用`Principal.getName()`。 + +默认情况下,如果`KeyResolver`没有找到密钥,请求将被拒绝。你可以通过设置`spring.cloud.gateway.filter.request-rate-limiter.deny-empty-key`(`true’或`false`)和`spring.cloud.gateway.filter.request-rate-limiter.empty-key-status-code`属性来调整此行为。 + +| |`RequestRateLimiter`不能使用“shortcut”符号进行配置。下面的示例是*无效*:

示例 32.application.properties

```
# 无效的快捷方式配置
Spring.cloud.gateway.routes[0].filters[0]=requestrateLimiter=2,2,#{@userkeyresolever=“426”/>```| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#the-redis-ratelimiter)[6.10.1. The Redis `RateLimiter`](#the-redis-ratelimiter) #### + +Redis 实现基于在[Stripe](https://stripe.com/blog/rate-limiters)上完成的工作。它需要使用`spring-boot-starter-data-redis-reactive` Spring 引导启动器。 + +使用的算法是[令牌桶算法](https://en.wikipedia.org/wiki/Token_bucket)。 + +`redis-rate-limiter.replenishRate`属性是你希望用户在不删除任何请求的情况下每秒可以执行多少个请求。这是令牌桶被填满的速率。 + +`redis-rate-limiter.burstCapacity`属性是用户在一秒钟内被允许执行的最大请求数。这是令牌桶可以容纳的令牌数量。将此值设置为零将阻止所有请求。 + +`redis-rate-limiter.requestedTokens`属性是一个请求需要多少令牌。这是每个请求从 bucket 中获取的令牌的数量,默认为`1`。 + +通过在`replenishRate`和`burstCapacity`中设置相同的值,可以实现稳定的速率。可以通过将`burstCapacity`设置为高于`replenishRate`来允许临时突发。在这种情况下,速率限制器需要允许在两次突发之间有一段时间(根据`replenishRate`),因为连续两次突发将导致丢弃请求(`HTTP429-太多请求’)。下面的清单配置了`redis-rate-limiter`: + +速率限制`1 request/s`通过将`replenishRate`设置为所需的请求数,`requestedTokens`设置为秒内的时间跨度,`burstCapacity`设置为`replenishRate`和`requestedTokens`的乘积,例如,设置`replenishRate=1`,`requestedTokens=60`和`burstCapacity=60`将导致`1 request/min`的限制。 + +示例 33.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: requestratelimiter_route + uri: https://example.org + filters: + - name: RequestRateLimiter + args: + redis-rate-limiter.replenishRate: 10 + redis-rate-limiter.burstCapacity: 20 + redis-rate-limiter.requestedTokens: 1 +``` + +下面的示例在 Java 中配置一个 keyresolver: + +例 34。config.java + +``` +@Bean +KeyResolver userKeyResolver() { + return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user")); +} +``` + +这定义了每个用户 10 个的请求速率限制。允许突发 20 个请求,但在接下来的一秒钟内,只有 10 个请求可用。`KeyResolver`是一个获得`user`请求参数的简单参数(请注意,这不推荐用于生产)。 + +还可以将速率限制器定义为实现`RateLimiter`接口的 Bean。在配置中,你可以使用 spel 按名称引用 Bean。`#{@myratelimiter}` 是一个 spel 表达式,它引用名为`myRateLimiter`的 Bean。下面的清单定义了一个速率限制器,该限制器使用上一个清单中定义的`KeyResolver`: + +示例 35.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: requestratelimiter_route + uri: https://example.org + filters: + - name: RequestRateLimiter + args: + rate-limiter: "#{@myRateLimiter}" + key-resolver: "#{@userKeyResolver}" +``` + +### [](#the-redirectto-gatewayfilter-factory)[6.11. The `RedirectTo` `GatewayFilter` Factory](#the-redirectto-gatewayfilter-factory) ### + +`RedirectTo``GatewayFilter`工厂接受两个参数,`status`和`url`。`status`参数应该是 300 系列重定向 HTTP 代码,例如 301。`url`参数应该是一个有效的 URL。这是`Location`标头的值。对于相对重定向,应该使用`uri: no://op`作为路由定义的 URI。下面的列表配置了`RedirectTo``GatewayFilter`: + +示例 36.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: prefixpath_route + uri: https://example.org + filters: + - RedirectTo=302, https://acme.org +``` + +这将发送带有`Location:https://acme.org`头的状态 302 来执行重定向。 + +### [](#the-removerequestheader-gatewayfilter-factory)[6.12. The `RemoveRequestHeader` GatewayFilter Factory](#the-removerequestheader-gatewayfilter-factory) ### + +`RemoveRequestHeader``GatewayFilter`工厂接受一个`name`参数。它是要删除的标头的名称。下面的列表配置了`RemoveRequestHeader``GatewayFilter`: + +示例 37.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: removerequestheader_route + uri: https://example.org + filters: + - RemoveRequestHeader=X-Request-Foo +``` + +这将在向下游发送`X-Request-Foo`头之前删除它。 + +### [](#removeresponseheader-gatewayfilter-factory)[6.13. `RemoveResponseHeader` `GatewayFilter` Factory](#removeresponseheader-gatewayfilter-factory) ### + +`RemoveResponseHeader``GatewayFilter`工厂接受一个`name`参数。它是要删除的标头的名称。下面的列表配置了`RemoveResponseHeader``GatewayFilter`: + +示例 38.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: removeresponseheader_route + uri: https://example.org + filters: + - RemoveResponseHeader=X-Response-Foo +``` + +这将在响应返回到网关客户机之前从响应中删除`X-Response-Foo`头。 + +要删除任何类型的敏感报头,你应该为你可能想要删除的任何路由配置此筛选器。此外,你可以使用`spring.cloud.gateway.default-filters`配置该过滤器一次,并将其应用于所有路由。 + +### [](#the-removerequestparameter-gatewayfilter-factory)[6.14. The `RemoveRequestParameter` `GatewayFilter` Factory](#the-removerequestparameter-gatewayfilter-factory) ### + +`RemoveRequestParameter``GatewayFilter`工厂接受一个`name`参数。它是要删除的查询参数的名称。下面的示例配置`RemoveRequestParameter``GatewayFilter`: + +示例 39.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: removerequestparameter_route + uri: https://example.org + filters: + - RemoveRequestParameter=red +``` + +这将在向下游发送`red`参数之前删除该参数。 + +### [](#the-rewritepath-gatewayfilter-factory)[6.15. The `RewritePath` `GatewayFilter` Factory](#the-rewritepath-gatewayfilter-factory) ### + +`RewritePath``GatewayFilter`工厂接受一个路径`regexp`参数和一个`replacement`参数。这使用 Java 正则表达式以灵活的方式重写请求路径。下面的列表配置了`RewritePath``GatewayFilter`: + +示例 40.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: rewritepath_route + uri: https://example.org + predicates: + - Path=/red/** + filters: + - RewritePath=/red/?(?.*), /$\{segment} +``` + +应该替换为`$\`。 + +### [](#rewritelocationresponseheader-gatewayfilter-factory)[6.16. `RewriteLocationResponseHeader` `GatewayFilter` Factory](#rewritelocationresponseheader-gatewayfilter-factory) ### + +`RewriteLocationResponseHeader``GatewayFilter`工厂修改`Location`响应头的值,通常是为了去掉后台特定的细节。它需要`stripVersionMode`,`locationHeaderName`,`hostValue`和`protocolsRegex`参数。下面的列表配置了`RewriteLocationResponseHeader``GatewayFilter`: + +示例 41.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: rewritelocationresponseheader_route + uri: http://example.org + filters: + - RewriteLocationResponseHeader=AS_IN_REQUEST, Location, , +``` + +例如,对于`POST [api.example.com/some/object/name](https://api.example.com/some/object/name)`的请求,`Location`的响应头值`[object-service.prod.example.net/v2/some/object/id](https://object-service.prod.example.net/v2/some/object/id)`被重写为`[api.example.com/some/object/id](https://api.example.com/some/object/id)`。 + +`stripVersionMode`参数有以下可能的值:`NEVER_STRIP`,`AS_IN_REQUEST`(默认),和`ALWAYS_STRIP`。 + +* `NEVER_STRIP`:即使原始请求路径不包含版本,也不会剥离版本。 + +* `AS_IN_REQUEST`只有当原始请求路径不包含版本时,才会剥离版本。 + +* `ALWAYS_STRIP`版本总是被剥离,即使原始请求路径包含版本。 + +如果提供了`hostValue`参数,则用于替换响应`host:port`头的`host:port`部分。如果没有提供,则使用`Host`请求头的值。 + +参数`protocolsRegex`必须是一个有效的 regex`String`,协议名称与该 regex 匹配。如果不匹配,过滤器就不会做任何事情。默认值为`http|https|ftp|ftps`。 + +### [](#the-rewriteresponseheader-gatewayfilter-factory)[6.17. The `RewriteResponseHeader` `GatewayFilter` Factory](#the-rewriteresponseheader-gatewayfilter-factory) ### + +`RewriteResponseHeader``GatewayFilter`工厂接受`name`、`regexp`和`replacement`参数。它使用 Java 正则表达式以一种灵活的方式重写响应头值。下面的示例配置`RewriteResponseHeader``GatewayFilter`: + +示例 42.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: rewriteresponseheader_route + uri: https://example.org + filters: + - RewriteResponseHeader=X-Response-Red, , password=[^&]+, password=*** +``` + +对于标头值`/42?user=ford&password=omg!what&flag=true`,在发出下游请求后将其设置为`/42?user=ford&password=***&flag=true`。由于 YAML 规范,你必须使用`$\`表示`Spring 云网关 +========== + + +该项目提供了一个构建在 Spring 生态系统之上的 API 网关,包括: Spring 5、 Spring Boot2 和 Project Reactor。 Spring Cloud Gateway 旨在提供一种简单但有效的方法来路由到 API,并向它们提供跨领域的关注,例如:安全性、监视/度量和弹性。 + +[](#gateway-starter)[1. How to Include Spring Cloud Gateway](#gateway-starter) +---------- + +要在项目中包含 Spring 云网关,请使用组 ID 为`org.springframework.cloud`和工件 ID 为`spring-cloud-starter-gateway`的 starter。请参阅[Spring Cloud Project page](https://projects.spring.io/spring-cloud/),以获取有关使用当前 Spring 云发布系列设置构建系统的详细信息。 + +如果包含启动器,但不希望启用网关,请设置`spring.cloud.gateway.enabled=false`。 + +| |Spring 云网关是建立在[Spring Boot 2.x](https://spring.io/projects/spring-boot#learn)、[Spring WebFlux](https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html)和[Project Reactor](https://projectreactor.io/docs)之上的。因此,当你使用 Spring 云网关时,许多你熟悉的同步库(例如 Spring 数据和 Spring 安全性)和模式可能不适用,如果你不熟悉这些项目,我们建议你在使用 Spring Cloud Gateway 之前,先阅读他们的文档,以熟悉一些新概念。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |Spring 云网关需要由 Spring 启动和 Spring WebFlux 提供的 Netty 运行时。它在传统 Servlet 容器中或构建为 WAR 时不工作。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[](#glossary)[2. Glossary](#glossary) +---------- + +* **路线**:网关的基本构件。它由一个 ID、一个目标 URI、一组谓词和一组筛选器定义。如果聚合谓词为 true,则匹配路由。 + +* **谓词**:这是[Java8 函数谓词](https://docs.oracle.com/javase/8/docs/api/java/util/function/Predicate.html)。输入类型为[Spring Framework `ServerWebExchange`](https://docs.spring.io/spring/docs/5.0.x/javadoc-api/org/springframework/web/server/ServerWebExchange.html)。这使你能够匹配 HTTP 请求中的任何内容,例如标题或参数。 + +* **过滤器**:这些是用特定工厂构造的[`GatewayFilter`](https://github.com/spring-cloud/spring-cloud-gateway/tree/main/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/GatewayFilter.java)的实例。在这里,你可以在发送下游请求之前或之后修改请求和响应。 + +[](#gateway-how-it-works)[3. How It Works](#gateway-how-it-works) +---------- + +下图提供了 Spring 云网关如何工作的高级概述: + +![Spring Cloud Gateway Diagram](./images/spring_cloud_gateway_diagram.png) + +客户端向 Spring 云网关提出请求。如果网关处理程序映射确定请求与路由匹配,则将请求发送到网关 Web 处理程序。此处理程序通过特定于该请求的筛选链来运行该请求。用虚线划分过滤器的原因是,过滤器可以在发送代理请求之前和之后运行逻辑。所有的“pre”过滤逻辑都会被执行。然后提出代理请求。在提出代理请求之后,将运行“POST”过滤逻辑。 + +| |在没有端口的路由中定义的 URI 分别获得 HTTP 和 HTTPS URI 的默认端口号 80 和 443。| +|---|----------------------------------------------------------------------------------------------------------------------| + +[](#configuring-route-predicate-factories-and-gateway-filter-factories)[4.配置路由谓词工厂和网关过滤器工厂](#configuring-route-predicate-factories-and-gateway-filter-factories) +---------- + +配置谓词和过滤器有两种方法:快捷方式和完全展开的参数。下面的大多数示例都使用了快捷方式。 + +名称和参数名称将以`code`的形式在每个部分的第一个或两个表示中列出。参数通常按快捷方式配置所需的顺序列出。 + +### [](#shortcut-configuration)[4.1.快捷方式配置](#shortcut-configuration) ### + +快捷方式配置由筛选器名称识别,后面跟着一个等号,后面是用逗号分隔的参数值。 + +应用程序.yml + +``` +spring: + cloud: + gateway: + routes: + - id: after_route + uri: https://example.org + predicates: + - Cookie=mycookie,mycookievalue +``` + +上一个示例用两个参数定义了`Cookie`路由谓词工厂,cookie 名`mycookie`和要匹配`mycookievalue`的值。 + +### [](#fully-expanded-arguments)[4.2.完全展开的论证](#fully-expanded-arguments) ### + +完全展开的参数看起来更像是带有名称/值对的标准 YAML 配置。通常,会有`name`键和`args`键。`args`键是用于配置谓词或筛选器的键值对的映射。 + +应用程序.yml + +``` +spring: + cloud: + gateway: + routes: + - id: after_route + uri: https://example.org + predicates: + - name: Cookie + args: + name: mycookie + regexp: mycookievalue +``` + +这是上面显示的`Cookie`谓词的快捷配置的完整配置。 + +[](#gateway-request-predicates-factories)[5.路线谓词工厂](#gateway-request-predicates-factories) +---------- + +Spring 云网关将路由匹配为 Spring WebFlux`HandlerMapping`基础设施的一部分。 Spring 云网关包括许多内置的路由谓词工厂。所有这些谓词在 HTTP 请求的不同属性上匹配。你可以将多个路由谓词工厂与逻辑`and`语句组合在一起。 + +### [](#the-after-route-predicate-factory)[5.1.后路由谓词工厂](#the-after-route-predicate-factory) ### + +`After`路由谓词工厂接受一个参数,一个`datetime`(这是一个 Java`ZonedDateTime`)。此谓词匹配在指定的 DateTime 之后发生的请求。下面的示例配置一个 after 路由谓词: + +示例 1.应用程序.yml + +``` +spring: + cloud: + gateway: + routes: + - id: after_route + uri: https://example.org + predicates: + - After=2017-01-20T17:42:47.789-07:00[America/Denver] +``` + +这条路线符合任何请求后提出的 JAN20,2017 年 17:42 山区时间(丹佛)。 + +### [](#the-before-route-predicate-factory)[5.2.前路由谓词工厂](#the-before-route-predicate-factory) ### + +`Before`路由谓词工厂接受一个参数,a`datetime`(这是一个 Java`ZonedDateTime`)。此谓词匹配在指定的`datetime`之前发生的请求。下面的示例配置一个 before 路由谓词: + +示例 2.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: before_route + uri: https://example.org + predicates: + - Before=2017-01-20T17:42:47.789-07:00[America/Denver] +``` + +这条路线符合任何在 JAN20,2017 年 17:42 山区时间(丹佛)之前提出的请求。 + +### [](#the-between-route-predicate-factory)[5.3.路由谓词之间的工厂](#the-between-route-predicate-factory) ### + +`Between`路由谓词工厂接受两个参数,`datetime1`和`datetime2`,它们是 Java`ZonedDateTime`对象。此谓词匹配发生在`datetime1`之后和`datetime2`之前的请求。`datetime2`参数必须位于`datetime1`之后。下面的示例配置了一个 between 路由谓词: + +示例 3.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: between_route + uri: https://example.org + predicates: + - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver] +``` + +这条路线符合在 JAN20,2017 年 17:42 山区时间(丹佛)之后和 JAN21,2017 年 17:42 山区时间(丹佛)之前提出的任何请求。这对于维护窗口可能是有用的。 + +### [](#the-cookie-route-predicate-factory)[5.4.cookie 路由谓词工厂](#the-cookie-route-predicate-factory) ### + +`Cookie`路由谓词工厂接受两个参数,cookie`name`和`regexp`(这是一个 Java 正则表达式)。此谓词匹配具有给定名称且其值与正则表达式匹配的 cookie。下面的示例配置了一个 Cookie 路由谓词工厂: + +示例 4.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: cookie_route + uri: https://example.org + predicates: + - Cookie=chocolate, ch.p +``` + +此路由匹配具有名为`chocolate`的 cookie 的请求,其值与`ch.p`正则表达式匹配。 + +### [](#the-header-route-predicate-factory)[5.5.头路由谓词工厂](#the-header-route-predicate-factory) ### + +`Header`路由谓词工厂接受两个参数,`header`和`regexp`(这是一个 Java 正则表达式)。此谓词与具有给定名称的头匹配,其值与正则表达式匹配。下面的示例配置头路由谓词: + +示例 5.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: header_route + uri: https://example.org + predicates: + - Header=X-Request-Id, \d+ +``` + +如果请求有一个名为`X-Request-Id`的头,其值与`\d+`正则表达式匹配(即它有一个或多个数字的值),则此路由匹配。 + +### [](#the-host-route-predicate-factory)[5.6.主机路由谓词工厂](#the-host-route-predicate-factory) ### + +`Host`路由谓词工厂接受一个参数:主机名列表`patterns`。该模式是一种 Ant 样式的模式,以`.`作为分隔符。此谓词匹配与模式匹配的`Host`头。下面的示例配置了一个主机路由谓词: + +示例 6.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: host_route + uri: https://example.org + predicates: + - Host=**.somehost.org,**.anotherhost.org +``` + +URI 模板变量(如`{sub}.myhost.org`)也受到支持。 + +如果请求具有`Host`头,其值为`www.somehost.org`或`beta.somehost.org`或`www.anotherhost.org`,则此路由匹配。 + +这个谓词提取 URI 模板变量(例如`sub`,在前面的示例中定义)作为名称和值的映射,并将其放置在`ServerWebExchange.getAttributes()`中,并在`ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE`中定义一个键。这些值可由[“GatewayFilter”工厂](#gateway-route-filters)使用 + +### [](#the-method-route-predicate-factory)[5.7.路由谓词工厂的方法](#the-method-route-predicate-factory) ### + +`Method`路由谓词工厂接受一个`methods`参数,该参数是一个或多个参数:要匹配的 HTTP 方法。下面的示例配置了一个方法路由谓词: + +示例 7.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: method_route + uri: https://example.org + predicates: + - Method=GET,POST +``` + +如果请求方法是`GET`或`POST`,则此路由匹配。 + +### [](#the-path-route-predicate-factory)[5.8.路径谓词工厂](#the-path-route-predicate-factory) ### + +`Path`路由谓词工厂接受两个参数: Spring `PathMatcher``patterns`的列表和一个名为`matchTrailingSlash`的可选标志(默认为`true`)。下面的示例配置了一个路径路由谓词: + +示例 8.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: path_route + uri: https://example.org + predicates: + - Path=/red/{segment},/blue/{segment} +``` + +如果请求路径是:例如:`/red/1`或`/red/1/`或`/red/blue`或`/blue/green`,则此路由匹配。 + +如果`matchTrailingSlash`被设置为`false`,那么请求路径`/red/1/`将不会被匹配。 + +这个谓词提取 URI 模板变量(例如`segment`,在前面的示例中定义)作为名称和值的映射,并将其放置在`ServerWebExchange.getAttributes()`中,并在`ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE`中定义一个键。这些值可由[“GatewayFilter”工厂](#gateway-route-filters)使用 + +一种实用方法(称为`get`)可以使访问这些变量变得更容易。下面的示例展示了如何使用`get`方法: + +``` +Map uriVariables = ServerWebExchangeUtils.getPathPredicateVariables(exchange); + +String segment = uriVariables.get("segment"); +``` + +### [](#the-query-route-predicate-factory)[5.9.查询路由谓词工厂](#the-query-route-predicate-factory) ### + +`Query`路由谓词工厂接受两个参数:一个必需的`param`和一个可选的`regexp`(这是一个 Java 正则表达式)。下面的示例配置一个查询路由谓词: + +示例 9.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: query_route + uri: https://example.org + predicates: + - Query=green +``` + +如果请求包含`green`查询参数,则前面的路由匹配。 + +application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: query_route + uri: https://example.org + predicates: + - Query=red, gree. +``` + +如果请求包含一个`red`查询参数,其值与`gree.`regexp 匹配,则前面的路由匹配,因此`green`和`greet`将匹配。 + +### [](#the-remoteaddr-route-predicate-factory)[5.10.RemoteAddr 路由谓词工厂](#the-remoteaddr-route-predicate-factory) ### + +`RemoteAddr`路由谓词工厂接受一个`sources`的列表(最小大小为 1),这些字符串是 CIDR 表示法(IPv4 或 IPv6)字符串,例如`192.168.0.1/16`(其中`192.168.0.1`是一个 IP 地址,`16`是一个子网掩码)。以下示例配置了一个 RemoteAddr 路由谓词: + +示例 10.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: remoteaddr_route + uri: https://example.org + predicates: + - RemoteAddr=192.168.1.1/24 +``` + +如果请求的远程地址是`192.168.1.10`,则此路由匹配。 + +#### [](#modifying-the-way-remote-addresses-are-resolved)[5.10.1.修改远程地址的解析方式](#modifying-the-way-remote-addresses-are-resolved) #### + +默认情况下,RemoteAddr 路由谓词工厂使用来自传入请求的远程地址。如果 Spring 云网关位于代理层的后面,这可能与实际的客户端 IP 地址不匹配。 + +你可以通过设置自定义`RemoteAddressResolver`来定制远程地址的解析方式。 Spring 云网关带有一个基于[X-forward-for 标头](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For),`XForwardedRemoteAddressResolver`的非默认远程地址解析器。 + +`XForwardedRemoteAddressResolver`有两个静态构造函数方法,它们采用不同的方法实现安全性: + +* `XForwardedRemoteAddressResolver::trustAll`返回一个`RemoteAddressResolver`,它总是使用在`X-Forwarded-For`头中找到的第一个 IP 地址。这种方法容易受到欺骗,因为恶意客户端可能会为`X-Forwarded-For`设置初始值,该初始值将被解析器接受。 + +* `XForwardedRemoteAddressResolver::maxTrustedIndex`获取一个索引,该索引与在 Spring 云网关前运行的受信任基础设施的数量相关。 Spring 如果云网关例如只能通过 HAProxy 访问,那么应该使用 1 的值。如果在访问 Spring 云网关之前需要两次跳可信的基础设施,那么应该使用 2 的值。 + +考虑以下标头值: + +``` +X-Forwarded-For: 0.0.0.1, 0.0.0.2, 0.0.0.3 +``` + +以下`maxTrustedIndex`值产生以下远程地址: + +| `maxTrustedIndex` |结果| +|------------------------|-----------------------------------------------------------| +|[`Integer.MIN_VALUE`,0] |(无效,`IllegalArgumentException`在初始化期间)| +| 1 | 0.0.0.3 | +| 2 | 0.0.0.2 | +| 3 | 0.0.0.1 | +|[4, `Integer.MAX_VALUE`]| 0.0.0.1 | + +下面的示例展示了如何使用 Java 实现相同的配置: + +例 11。gatewayconfig.java + +``` +RemoteAddressResolver resolver = XForwardedRemoteAddressResolver + .maxTrustedIndex(1); + +... + +.route("direct-route", + r -> r.remoteAddr("10.1.1.1", "10.10.1.1/24") + .uri("https://downstream1") +.route("proxied-route", + r -> r.remoteAddr(resolver, "10.10.1.1", "10.10.1.1/24") + .uri("https://downstream2") +) +``` + +### [](#the-weight-route-predicate-factory)[5.11.权重路径谓词工厂](#the-weight-route-predicate-factory) ### + +`Weight`路由谓词工厂接受两个参数:`group`和`weight`(一个 INT)。权重是按组计算的。下面的示例配置了权重路由谓词: + +示例 12.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: weight_high + uri: https://weighthigh.org + predicates: + - Weight=group1, 8 + - id: weight_low + uri: https://weightlow.org + predicates: + - Weight=group1, 2 +``` + +此路线将把 80% 的流量转发给[weighthigh.org](https://weighthigh.org),并将 20% 的流量转发给[weighlow.org](https://weighlow.org)。 + +### [](#the-xforwarded-remote-addr-route-predicate-factory)[5.12.XForwarded 远程 addr 路由谓词工厂](#the-xforwarded-remote-addr-route-predicate-factory) ### + +`XForwarded Remote Addr`路由谓词工厂接受一个`sources`的列表(最小大小为 1),这些字符串是 CIDR 表示法(IPv4 或 IPv6)字符串,例如`192.168.0.1/16`(其中`192.168.0.1`是一个 IP 地址,`16`是一个子网掩码)。 + +此路由谓词允许基于`X-Forwarded-For`HTTP 报头对请求进行过滤。 + +这可以用于反向代理,例如负载均衡器或 Web 应用程序防火墙,在这些代理中,只有当请求来自由这些反向代理使用的受信任的 IP 地址列表时,才允许请求。 + +下面的示例配置了一个 XForWardeDremoteaddr 路由谓词: + +示例 13.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: xforwarded_remoteaddr_route + uri: https://example.org + predicates: + - XForwardedRemoteAddr=192.168.1.1/24 +``` + +如果`X-Forwarded-For`标头包含`192.168.1.10`,则此路由匹配。 + +[](#gatewayfilter-factories)[6. `GatewayFilter` Factories](#gatewayfilter-factories) +---------- + +路由过滤器允许以某种方式修改传入 HTTP 请求或传出 HTTP 响应。路由过滤器的作用域是特定的路由。 Spring 云网关包括许多内置的网关过滤工厂。 + +| |有关如何使用以下任何过滤器的更详细示例,请查看[unit tests](https://github.com/spring-cloud/spring-cloud-gateway/tree/master/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory)。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#the-addrequestheader-gatewayfilter-factory)[6.1. The `AddRequestHeader` `GatewayFilter` Factory](#the-addrequestheader-gatewayfilter-factory) ### + +`AddRequestHeader``GatewayFilter`工厂接受一个`name`和`value`参数。以下示例配置`AddRequestHeader``GatewayFilter`: + +示例 14.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: add_request_header_route + uri: https://example.org + filters: + - AddRequestHeader=X-Request-red, blue +``` + +对于所有匹配的请求,此清单将`X-Request-red:blue`头添加到下游请求的头。 + +`AddRequestHeader`知道用于匹配路径或主机的 URI 变量。URI 变量可以在值中使用,并在运行时展开。下面的示例配置使用变量的`AddRequestHeader``GatewayFilter`: + +示例 15.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: add_request_header_route + uri: https://example.org + predicates: + - Path=/red/{segment} + filters: + - AddRequestHeader=X-Request-Red, Blue-{segment} +``` + +### [](#the-addrequestparameter-gatewayfilter-factory)[6.2. The `AddRequestParameter` `GatewayFilter` Factory](#the-addrequestparameter-gatewayfilter-factory) ### + +`AddRequestParameter``GatewayFilter`工厂接受`name`和`value`参数。下面的示例配置`AddRequestParameter``GatewayFilter`: + +示例 16.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: add_request_parameter_route + uri: https://example.org + filters: + - AddRequestParameter=red, blue +``` + +这将为所有匹配的请求将`red=blue`添加到下游请求的查询字符串中。 + +`AddRequestParameter`知道用于匹配路径或主机的 URI 变量。URI 变量可以在值中使用,并在运行时展开。下面的示例配置使用变量的`AddRequestParameter``GatewayFilter`: + +示例 17.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: add_request_parameter_route + uri: https://example.org + predicates: + - Host: {segment}.myhost.org + filters: + - AddRequestParameter=foo, bar-{segment} +``` + +### [](#the-addresponseheader-gatewayfilter-factory)[6.3. The `AddResponseHeader` `GatewayFilter` Factory](#the-addresponseheader-gatewayfilter-factory) ### + +`AddResponseHeader``GatewayFilter`工厂接受一个`name`和`value`参数。以下示例配置`AddResponseHeader``GatewayFilter`: + +示例 18.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: add_response_header_route + uri: https://example.org + filters: + - AddResponseHeader=X-Response-Red, Blue +``` + +这将为所有匹配的请求向下游响应的头添加`X-Response-Red:Blue`头。 + +`AddResponseHeader`知道用于匹配路径或主机的 URI 变量。URI 变量可以在值中使用,并在运行时展开。下面的示例配置了使用变量的`AddResponseHeader``GatewayFilter`: + +示例 19.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: add_response_header_route + uri: https://example.org + predicates: + - Host: {segment}.myhost.org + filters: + - AddResponseHeader=foo, bar-{segment} +``` + +### [](#the-deduperesponseheader-gatewayfilter-factory)[6.4. The `DedupeResponseHeader` `GatewayFilter` Factory](#the-deduperesponseheader-gatewayfilter-factory) ### + +DedupeResponseHeader 网关过滤器工厂接受一个`name`参数和一个可选的`strategy`参数。`name`可以包含一个以空格分隔的标题名称列表。以下示例配置`DedupeResponseHeader``GatewayFilter`: + +示例 20.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: dedupe_response_header_route + uri: https://example.org + filters: + - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin +``` + +在网关 CORS 逻辑和下游逻辑都添加响应头的情况下,这将删除重复的`Access-Control-Allow-Credentials`和`Access-Control-Allow-Origin`响应头的值。 + +`DedupeResponseHeader`过滤器还接受一个可选的`strategy`参数。接受的值是`RETAIN_FIRST`(默认)、`RETAIN_LAST`和`RETAIN_UNIQUE`。 + +### [](#spring-cloud-circuitbreaker-filter-factory)[6.5. Spring Cloud CircuitBreaker GatewayFilter Factory](#spring-cloud-circuitbreaker-filter-factory) ### + +Spring 云断路器网关过滤器工厂使用 Spring 云断路器 API 将网关路由封装在断路器中。 Spring Cloud Circuitbreaker 支持可与 Spring Cloud Gateway 一起使用的多个库。 Spring 云支持开箱即用的弹性 4J。 + +要启用 Spring 云电路断路器过滤器,你需要在 Classpath 上放置`spring-cloud-starter-circuitbreaker-reactor-resilience4j`。下面的示例配置 Spring 云电路断路器`GatewayFilter`: + +示例 21.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: circuitbreaker_route + uri: https://example.org + filters: + - CircuitBreaker=myCircuitBreaker +``` + +要配置断路器,请参阅你正在使用的底层断路器实现的配置。 + +* [复原力 4J 文档](https://cloud.spring.io/spring-cloud-circuitbreaker/reference/html/spring-cloud-circuitbreaker.html) + +Spring 云电路断路器过滤器还可以接受可选的`fallbackUri`参数。目前,只支持`forward:`模式 URI。如果回退被调用,请求将被转发到与 URI 匹配的控制器。下面的示例配置了这种回退: + +示例 22.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: circuitbreaker_route + uri: lb://backing-service:8088 + predicates: + - Path=/consumingServiceEndpoint + filters: + - name: CircuitBreaker + args: + name: myCircuitBreaker + fallbackUri: forward:/inCaseOfFailureUseThis + - RewritePath=/consumingServiceEndpoint, /backingServiceEndpoint +``` + +下面的列表在 Java 中做了相同的事情: + +例 23。application.java + +``` +@Bean +public RouteLocator routes(RouteLocatorBuilder builder) { + return builder.routes() + .route("circuitbreaker_route", r -> r.path("/consumingServiceEndpoint") + .filters(f -> f.circuitBreaker(c -> c.name("myCircuitBreaker").fallbackUri("forward:/inCaseOfFailureUseThis")) + .rewritePath("/consumingServiceEndpoint", "/backingServiceEndpoint")).uri("lb://backing-service:8088") + .build(); +} +``` + +当调用断路器回退时,此示例转发到`/inCaseofFailureUseThis`URI。请注意,此示例还演示了(可选的) Spring 云负载平衡器负载平衡(由目标 URI 上的`lb`前缀定义)。 + +主要的场景是使用`fallbackUri`在网关应用程序中定义内部控制器或处理程序。但是,你也可以将请求重新路由到外部应用程序中的控制器或处理程序,如下所示: + +示例 24.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: ingredients + uri: lb://ingredients + predicates: + - Path=//ingredients/** + filters: + - name: CircuitBreaker + args: + name: fetchIngredients + fallbackUri: forward:/fallback + - id: ingredients-fallback + uri: http://localhost:9994 + predicates: + - Path=/fallback +``` + +在此示例中,网关应用程序中没有`fallback`端点或处理程序。然而,在另一个应用程序中有一个,注册在`[localhost:9994](http://localhost:9994)`下。 + +在请求被转发到 Fallback 的情况下, Spring Cloud Circuitbreaker 网关过滤器还提供了导致它的`Throwable`。它被添加到`ServerWebExchange`中,作为`ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR`属性,该属性可以在处理网关应用程序内的回退时使用。 + +对于外部控制器/处理程序场景,可以添加带有异常详细信息的头。你可以在[FallbackHeaders GatewayFilter 工厂部分](#fallback-headers)中找到有关这样做的更多信息。 + +#### [](#circuit-breaker-status-codes)[6.5.1.按状态码切断断路器](#circuit-breaker-status-codes) #### + +在某些情况下,你可能希望基于从其封装的路由返回的状态码来跳闸断路器。断路器配置对象获取一系列状态代码,如果返回这些代码,将导致断路器跳闸。在设置要跳闸的状态码时,可以使用带有状态码值的整数,也可以使用`HttpStatus`枚举的字符串表示。 + +示例 25.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: circuitbreaker_route + uri: lb://backing-service:8088 + predicates: + - Path=/consumingServiceEndpoint + filters: + - name: CircuitBreaker + args: + name: myCircuitBreaker + fallbackUri: forward:/inCaseOfFailureUseThis + statusCodes: + - 500 + - "NOT_FOUND" +``` + +例 26。应用程序.java + +``` +@Bean +public RouteLocator routes(RouteLocatorBuilder builder) { + return builder.routes() + .route("circuitbreaker_route", r -> r.path("/consumingServiceEndpoint") + .filters(f -> f.circuitBreaker(c -> c.name("myCircuitBreaker").fallbackUri("forward:/inCaseOfFailureUseThis").addStatusCode("INTERNAL_SERVER_ERROR")) + .rewritePath("/consumingServiceEndpoint", "/backingServiceEndpoint")).uri("lb://backing-service:8088") + .build(); +} +``` + +### [](#fallback-headers)[6.6. The `FallbackHeaders` `GatewayFilter` Factory](#fallback-headers) ### + +`FallbackHeaders`工厂允许你在转发到外部应用程序中的`fallbackUri`的请求的标题中添加 Spring Cloud Circuitbreaker 执行异常详细信息,如下所示: + +示例 27.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: ingredients + uri: lb://ingredients + predicates: + - Path=//ingredients/** + filters: + - name: CircuitBreaker + args: + name: fetchIngredients + fallbackUri: forward:/fallback + - id: ingredients-fallback + uri: http://localhost:9994 + predicates: + - Path=/fallback + filters: + - name: FallbackHeaders + args: + executionExceptionTypeHeaderName: Test-Header +``` + +在本例中,在运行断路器时发生执行异常后,请求被转发到在`localhost:9994`上运行的应用程序中的`fallback`端点或处理程序。带有异常类型、消息和(如果可用的话)根原因异常类型和消息的头将由`FallbackHeaders`过滤器添加到该请求中。 + +你可以通过设置以下参数的值(以它们的默认值显示)来覆盖配置中的头的名称: + +* `executionExceptionTypeHeaderName`(`“execution-exception-type”`) + +* `executionExceptionMessageHeaderName`(`“execution-exception-message”`) + +* `rootCauseExceptionTypeHeaderName`(`“root-cause-exception-type”`) + +* `rootCauseExceptionMessageHeaderName`(`“root-cause-exception-message”`) + +有关断路器和网关的更多信息,请参见[Spring Cloud CircuitBreaker Factory section](#spring-cloud-circuitbreaker-filter-factory)。 + +### [](#the-maprequestheader-gatewayfilter-factory)[6.7. The `MapRequestHeader` `GatewayFilter` Factory](#the-maprequestheader-gatewayfilter-factory) ### + +`MapRequestHeader``GatewayFilter`工厂接受`fromHeader`和`toHeader`参数。它将创建一个新的命名头,并从传入的 HTTP 请求中从现有的命名头中提取该值。如果输入标头不存在,则过滤器不会产生任何影响。如果新的命名标头已经存在,那么它的值将被新的值扩充。以下示例配置`MapRequestHeader`: + +示例 28.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: map_request_header_route + uri: https://example.org + filters: + - MapRequestHeader=Blue, X-Request-Red +``` + +这会将`X-Request-Red:`头添加到下游请求,并从传入的 HTTP 请求的`Blue`头更新其值。 + +### [](#the-prefixpath-gatewayfilter-factory)[6.8. The `PrefixPath` `GatewayFilter` Factory](#the-prefixpath-gatewayfilter-factory) ### + +`PrefixPath``GatewayFilter`工厂接受一个`prefix`参数。下面的示例配置`PrefixPath``GatewayFilter`: + +示例 29.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: prefixpath_route + uri: https://example.org + filters: + - PrefixPath=/mypath +``` + +这将在所有匹配请求的路径前缀`/mypath`。因此,对`/hello`的请求将被发送到`/mypath/hello`。 + +### [](#the-preservehostheader-gatewayfilter-factory)[6.9. The `PreserveHostHeader` `GatewayFilter` Factory](#the-preservehostheader-gatewayfilter-factory) ### + +`PreserveHostHeader``GatewayFilter`工厂没有参数。此筛选器设置一个请求属性,由路由筛选器检查该属性,以确定是否应发送原始的主机头,而不是由 HTTP 客户机确定的主机头。以下示例配置`PreserveHostHeader``GatewayFilter`: + +示例 30.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: preserve_host_route + uri: https://example.org + filters: + - PreserveHostHeader +``` + +### [](#the-requestratelimiter-gatewayfilter-factory)[6.10. The `RequestRateLimiter` `GatewayFilter` Factory](#the-requestratelimiter-gatewayfilter-factory) ### + +`RequestRateLimiter``GatewayFilter`工厂使用`RateLimiter`实现来确定是否允许当前请求继续执行。如果不是,则返回`HTTP 429 - Too Many Requests`(默认情况下)的状态。 + +这个过滤器接受一个可选的`keyResolver`参数和特定于速率限制器的参数(将在本节后面描述)。 + +`keyResolver`是实现`KeyResolver`接口的 Bean。在配置中,使用 spel 按名称引用 Bean。`#{@mykeyresolver}` 是引用名为`myKeyResolver`的 Bean 的 spel 表达式。下面的清单显示了`KeyResolver`接口: + +例 31。keyresolver.java + +``` +public interface KeyResolver { + Mono resolve(ServerWebExchange exchange); +} +``` + +`KeyResolver`接口让可插入策略派生限制请求的键。在未来的里程碑版本中,将会有一些`KeyResolver`实现。 + +`KeyResolver`的默认实现是`PrincipalNameKeyResolver`,它从`ServerWebExchange`检索`Principal`并调用`Principal.getName()`。 + +默认情况下,如果`KeyResolver`没有找到密钥,请求将被拒绝。你可以通过设置`spring.cloud.gateway.filter.request-rate-limiter.deny-empty-key`(`true’或`false`)和`spring.cloud.gateway.filter.request-rate-limiter.empty-key-status-code`属性来调整此行为。 + +| |`RequestRateLimiter`不能使用“shortcut”符号进行配置。下面的示例是*无效*:

示例 32.application.properties

```
# 无效的快捷方式配置
Spring.cloud.gateway.routes[0].filters[0]=requestrateLimiter=2,2,#{@userkeyresolever=“426”/>```| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#the-redis-ratelimiter)[6.10.1. The Redis `RateLimiter`](#the-redis-ratelimiter) #### + +Redis 实现基于在[Stripe](https://stripe.com/blog/rate-limiters)上完成的工作。它需要使用`spring-boot-starter-data-redis-reactive` Spring 引导启动器。 + +使用的算法是[令牌桶算法](https://en.wikipedia.org/wiki/Token_bucket)。 + +`redis-rate-limiter.replenishRate`属性是你希望用户在不删除任何请求的情况下每秒可以执行多少个请求。这是令牌桶被填满的速率。 + +`redis-rate-limiter.burstCapacity`属性是用户在一秒钟内被允许执行的最大请求数。这是令牌桶可以容纳的令牌数量。将此值设置为零将阻止所有请求。 + +`redis-rate-limiter.requestedTokens`属性是一个请求需要多少令牌。这是每个请求从 bucket 中获取的令牌的数量,默认为`1`。 + +通过在`replenishRate`和`burstCapacity`中设置相同的值,可以实现稳定的速率。可以通过将`burstCapacity`设置为高于`replenishRate`来允许临时突发。在这种情况下,速率限制器需要允许在两次突发之间有一段时间(根据`replenishRate`),因为连续两次突发将导致丢弃请求(`HTTP429-太多请求’)。下面的清单配置了`redis-rate-limiter`: + +速率限制`1 request/s`通过将`replenishRate`设置为所需的请求数,`requestedTokens`设置为秒内的时间跨度,`burstCapacity`设置为`replenishRate`和`requestedTokens`的乘积,例如,设置`replenishRate=1`,`requestedTokens=60`和`burstCapacity=60`将导致`1 request/min`的限制。 + +示例 33.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: requestratelimiter_route + uri: https://example.org + filters: + - name: RequestRateLimiter + args: + redis-rate-limiter.replenishRate: 10 + redis-rate-limiter.burstCapacity: 20 + redis-rate-limiter.requestedTokens: 1 +``` + +下面的示例在 Java 中配置一个 keyresolver: + +例 34。config.java + +``` +@Bean +KeyResolver userKeyResolver() { + return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user")); +} +``` + +这定义了每个用户 10 个的请求速率限制。允许突发 20 个请求,但在接下来的一秒钟内,只有 10 个请求可用。`KeyResolver`是一个获得`user`请求参数的简单参数(请注意,这不推荐用于生产)。 + +还可以将速率限制器定义为实现`RateLimiter`接口的 Bean。在配置中,你可以使用 spel 按名称引用 Bean。`#{@myratelimiter}` 是一个 spel 表达式,它引用名为`myRateLimiter`的 Bean。下面的清单定义了一个速率限制器,该限制器使用上一个清单中定义的`KeyResolver`: + +示例 35.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: requestratelimiter_route + uri: https://example.org + filters: + - name: RequestRateLimiter + args: + rate-limiter: "#{@myRateLimiter}" + key-resolver: "#{@userKeyResolver}" +``` + +### [](#the-redirectto-gatewayfilter-factory)[6.11. The `RedirectTo` `GatewayFilter` Factory](#the-redirectto-gatewayfilter-factory) ### + +`RedirectTo``GatewayFilter`工厂接受两个参数,`status`和`url`。`status`参数应该是 300 系列重定向 HTTP 代码,例如 301。`url`参数应该是一个有效的 URL。这是`Location`标头的值。对于相对重定向,应该使用`uri: no://op`作为路由定义的 URI。下面的列表配置了`RedirectTo``GatewayFilter`: + +示例 36.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: prefixpath_route + uri: https://example.org + filters: + - RedirectTo=302, https://acme.org +``` + +这将发送带有`Location:https://acme.org`头的状态 302 来执行重定向。 + +### [](#the-removerequestheader-gatewayfilter-factory)[6.12. The `RemoveRequestHeader` GatewayFilter Factory](#the-removerequestheader-gatewayfilter-factory) ### + +`RemoveRequestHeader``GatewayFilter`工厂接受一个`name`参数。它是要删除的标头的名称。下面的列表配置了`RemoveRequestHeader``GatewayFilter`: + +示例 37.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: removerequestheader_route + uri: https://example.org + filters: + - RemoveRequestHeader=X-Request-Foo +``` + +这将在向下游发送`X-Request-Foo`头之前删除它。 + +### [](#removeresponseheader-gatewayfilter-factory)[6.13. `RemoveResponseHeader` `GatewayFilter` Factory](#removeresponseheader-gatewayfilter-factory) ### + +`RemoveResponseHeader``GatewayFilter`工厂接受一个`name`参数。它是要删除的标头的名称。下面的列表配置了`RemoveResponseHeader``GatewayFilter`: + +示例 38.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: removeresponseheader_route + uri: https://example.org + filters: + - RemoveResponseHeader=X-Response-Foo +``` + +这将在响应返回到网关客户机之前从响应中删除`X-Response-Foo`头。 + +要删除任何类型的敏感报头,你应该为你可能想要删除的任何路由配置此筛选器。此外,你可以使用`spring.cloud.gateway.default-filters`配置该过滤器一次,并将其应用于所有路由。 + +### [](#the-removerequestparameter-gatewayfilter-factory)[6.14. The `RemoveRequestParameter` `GatewayFilter` Factory](#the-removerequestparameter-gatewayfilter-factory) ### + +`RemoveRequestParameter``GatewayFilter`工厂接受一个`name`参数。它是要删除的查询参数的名称。下面的示例配置`RemoveRequestParameter``GatewayFilter`: + +示例 39.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: removerequestparameter_route + uri: https://example.org + filters: + - RemoveRequestParameter=red +``` + +这将在向下游发送`red`参数之前删除该参数。 + +### [](#the-rewritepath-gatewayfilter-factory)[6.15. The `RewritePath` `GatewayFilter` Factory](#the-rewritepath-gatewayfilter-factory) ### + +`RewritePath``GatewayFilter`工厂接受一个路径`regexp`参数和一个`replacement`参数。这使用 Java 正则表达式以灵活的方式重写请求路径。下面的列表配置了`RewritePath``GatewayFilter`: + +示例 40.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: rewritepath_route + uri: https://example.org + predicates: + - Path=/red/** + filters: + - RewritePath=/red/?(?.*), /$\{segment} +``` + +对于`/red/blue`的请求路径,在发出下游请求之前,将路径设置为`/blue`。注意,由于 YAML 规范,`Spring 云网关 +========== + + +该项目提供了一个构建在 Spring 生态系统之上的 API 网关,包括: Spring 5、 Spring Boot2 和 Project Reactor。 Spring Cloud Gateway 旨在提供一种简单但有效的方法来路由到 API,并向它们提供跨领域的关注,例如:安全性、监视/度量和弹性。 + +[](#gateway-starter)[1. How to Include Spring Cloud Gateway](#gateway-starter) +---------- + +要在项目中包含 Spring 云网关,请使用组 ID 为`org.springframework.cloud`和工件 ID 为`spring-cloud-starter-gateway`的 starter。请参阅[Spring Cloud Project page](https://projects.spring.io/spring-cloud/),以获取有关使用当前 Spring 云发布系列设置构建系统的详细信息。 + +如果包含启动器,但不希望启用网关,请设置`spring.cloud.gateway.enabled=false`。 + +| |Spring 云网关是建立在[Spring Boot 2.x](https://spring.io/projects/spring-boot#learn)、[Spring WebFlux](https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html)和[Project Reactor](https://projectreactor.io/docs)之上的。因此,当你使用 Spring 云网关时,许多你熟悉的同步库(例如 Spring 数据和 Spring 安全性)和模式可能不适用,如果你不熟悉这些项目,我们建议你在使用 Spring Cloud Gateway 之前,先阅读他们的文档,以熟悉一些新概念。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |Spring 云网关需要由 Spring 启动和 Spring WebFlux 提供的 Netty 运行时。它在传统 Servlet 容器中或构建为 WAR 时不工作。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[](#glossary)[2. Glossary](#glossary) +---------- + +* **路线**:网关的基本构件。它由一个 ID、一个目标 URI、一组谓词和一组筛选器定义。如果聚合谓词为 true,则匹配路由。 + +* **谓词**:这是[Java8 函数谓词](https://docs.oracle.com/javase/8/docs/api/java/util/function/Predicate.html)。输入类型为[Spring Framework `ServerWebExchange`](https://docs.spring.io/spring/docs/5.0.x/javadoc-api/org/springframework/web/server/ServerWebExchange.html)。这使你能够匹配 HTTP 请求中的任何内容,例如标题或参数。 + +* **过滤器**:这些是用特定工厂构造的[`GatewayFilter`](https://github.com/spring-cloud/spring-cloud-gateway/tree/main/spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/filter/GatewayFilter.java)的实例。在这里,你可以在发送下游请求之前或之后修改请求和响应。 + +[](#gateway-how-it-works)[3. How It Works](#gateway-how-it-works) +---------- + +下图提供了 Spring 云网关如何工作的高级概述: + +![Spring Cloud Gateway Diagram](./images/spring_cloud_gateway_diagram.png) + +客户端向 Spring 云网关提出请求。如果网关处理程序映射确定请求与路由匹配,则将请求发送到网关 Web 处理程序。此处理程序通过特定于该请求的筛选链来运行该请求。用虚线划分过滤器的原因是,过滤器可以在发送代理请求之前和之后运行逻辑。所有的“pre”过滤逻辑都会被执行。然后提出代理请求。在提出代理请求之后,将运行“POST”过滤逻辑。 + +| |在没有端口的路由中定义的 URI 分别获得 HTTP 和 HTTPS URI 的默认端口号 80 和 443。| +|---|----------------------------------------------------------------------------------------------------------------------| + +[](#configuring-route-predicate-factories-and-gateway-filter-factories)[4.配置路由谓词工厂和网关过滤器工厂](#configuring-route-predicate-factories-and-gateway-filter-factories) +---------- + +配置谓词和过滤器有两种方法:快捷方式和完全展开的参数。下面的大多数示例都使用了快捷方式。 + +名称和参数名称将以`code`的形式在每个部分的第一个或两个表示中列出。参数通常按快捷方式配置所需的顺序列出。 + +### [](#shortcut-configuration)[4.1.快捷方式配置](#shortcut-configuration) ### + +快捷方式配置由筛选器名称识别,后面跟着一个等号,后面是用逗号分隔的参数值。 + +应用程序.yml + +``` +spring: + cloud: + gateway: + routes: + - id: after_route + uri: https://example.org + predicates: + - Cookie=mycookie,mycookievalue +``` + +上一个示例用两个参数定义了`Cookie`路由谓词工厂,cookie 名`mycookie`和要匹配`mycookievalue`的值。 + +### [](#fully-expanded-arguments)[4.2.完全展开的论证](#fully-expanded-arguments) ### + +完全展开的参数看起来更像是带有名称/值对的标准 YAML 配置。通常,会有`name`键和`args`键。`args`键是用于配置谓词或筛选器的键值对的映射。 + +应用程序.yml + +``` +spring: + cloud: + gateway: + routes: + - id: after_route + uri: https://example.org + predicates: + - name: Cookie + args: + name: mycookie + regexp: mycookievalue +``` + +这是上面显示的`Cookie`谓词的快捷配置的完整配置。 + +[](#gateway-request-predicates-factories)[5.路线谓词工厂](#gateway-request-predicates-factories) +---------- + +Spring 云网关将路由匹配为 Spring WebFlux`HandlerMapping`基础设施的一部分。 Spring 云网关包括许多内置的路由谓词工厂。所有这些谓词在 HTTP 请求的不同属性上匹配。你可以将多个路由谓词工厂与逻辑`and`语句组合在一起。 + +### [](#the-after-route-predicate-factory)[5.1.后路由谓词工厂](#the-after-route-predicate-factory) ### + +`After`路由谓词工厂接受一个参数,一个`datetime`(这是一个 Java`ZonedDateTime`)。此谓词匹配在指定的 DateTime 之后发生的请求。下面的示例配置一个 after 路由谓词: + +示例 1.应用程序.yml + +``` +spring: + cloud: + gateway: + routes: + - id: after_route + uri: https://example.org + predicates: + - After=2017-01-20T17:42:47.789-07:00[America/Denver] +``` + +这条路线符合任何请求后提出的 JAN20,2017 年 17:42 山区时间(丹佛)。 + +### [](#the-before-route-predicate-factory)[5.2.前路由谓词工厂](#the-before-route-predicate-factory) ### + +`Before`路由谓词工厂接受一个参数,a`datetime`(这是一个 Java`ZonedDateTime`)。此谓词匹配在指定的`datetime`之前发生的请求。下面的示例配置一个 before 路由谓词: + +示例 2.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: before_route + uri: https://example.org + predicates: + - Before=2017-01-20T17:42:47.789-07:00[America/Denver] +``` + +这条路线符合任何在 JAN20,2017 年 17:42 山区时间(丹佛)之前提出的请求。 + +### [](#the-between-route-predicate-factory)[5.3.路由谓词之间的工厂](#the-between-route-predicate-factory) ### + +`Between`路由谓词工厂接受两个参数,`datetime1`和`datetime2`,它们是 Java`ZonedDateTime`对象。此谓词匹配发生在`datetime1`之后和`datetime2`之前的请求。`datetime2`参数必须位于`datetime1`之后。下面的示例配置了一个 between 路由谓词: + +示例 3.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: between_route + uri: https://example.org + predicates: + - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver] +``` + +这条路线符合在 JAN20,2017 年 17:42 山区时间(丹佛)之后和 JAN21,2017 年 17:42 山区时间(丹佛)之前提出的任何请求。这对于维护窗口可能是有用的。 + +### [](#the-cookie-route-predicate-factory)[5.4.cookie 路由谓词工厂](#the-cookie-route-predicate-factory) ### + +`Cookie`路由谓词工厂接受两个参数,cookie`name`和`regexp`(这是一个 Java 正则表达式)。此谓词匹配具有给定名称且其值与正则表达式匹配的 cookie。下面的示例配置了一个 Cookie 路由谓词工厂: + +示例 4.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: cookie_route + uri: https://example.org + predicates: + - Cookie=chocolate, ch.p +``` + +此路由匹配具有名为`chocolate`的 cookie 的请求,其值与`ch.p`正则表达式匹配。 + +### [](#the-header-route-predicate-factory)[5.5.头路由谓词工厂](#the-header-route-predicate-factory) ### + +`Header`路由谓词工厂接受两个参数,`header`和`regexp`(这是一个 Java 正则表达式)。此谓词与具有给定名称的头匹配,其值与正则表达式匹配。下面的示例配置头路由谓词: + +示例 5.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: header_route + uri: https://example.org + predicates: + - Header=X-Request-Id, \d+ +``` + +如果请求有一个名为`X-Request-Id`的头,其值与`\d+`正则表达式匹配(即它有一个或多个数字的值),则此路由匹配。 + +### [](#the-host-route-predicate-factory)[5.6.主机路由谓词工厂](#the-host-route-predicate-factory) ### + +`Host`路由谓词工厂接受一个参数:主机名列表`patterns`。该模式是一种 Ant 样式的模式,以`.`作为分隔符。此谓词匹配与模式匹配的`Host`头。下面的示例配置了一个主机路由谓词: + +示例 6.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: host_route + uri: https://example.org + predicates: + - Host=**.somehost.org,**.anotherhost.org +``` + +URI 模板变量(如`{sub}.myhost.org`)也受到支持。 + +如果请求具有`Host`头,其值为`www.somehost.org`或`beta.somehost.org`或`www.anotherhost.org`,则此路由匹配。 + +这个谓词提取 URI 模板变量(例如`sub`,在前面的示例中定义)作为名称和值的映射,并将其放置在`ServerWebExchange.getAttributes()`中,并在`ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE`中定义一个键。这些值可由[“GatewayFilter”工厂](#gateway-route-filters)使用 + +### [](#the-method-route-predicate-factory)[5.7.路由谓词工厂的方法](#the-method-route-predicate-factory) ### + +`Method`路由谓词工厂接受一个`methods`参数,该参数是一个或多个参数:要匹配的 HTTP 方法。下面的示例配置了一个方法路由谓词: + +示例 7.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: method_route + uri: https://example.org + predicates: + - Method=GET,POST +``` + +如果请求方法是`GET`或`POST`,则此路由匹配。 + +### [](#the-path-route-predicate-factory)[5.8.路径谓词工厂](#the-path-route-predicate-factory) ### + +`Path`路由谓词工厂接受两个参数: Spring `PathMatcher``patterns`的列表和一个名为`matchTrailingSlash`的可选标志(默认为`true`)。下面的示例配置了一个路径路由谓词: + +示例 8.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: path_route + uri: https://example.org + predicates: + - Path=/red/{segment},/blue/{segment} +``` + +如果请求路径是:例如:`/red/1`或`/red/1/`或`/red/blue`或`/blue/green`,则此路由匹配。 + +如果`matchTrailingSlash`被设置为`false`,那么请求路径`/red/1/`将不会被匹配。 + +这个谓词提取 URI 模板变量(例如`segment`,在前面的示例中定义)作为名称和值的映射,并将其放置在`ServerWebExchange.getAttributes()`中,并在`ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE`中定义一个键。这些值可由[“GatewayFilter”工厂](#gateway-route-filters)使用 + +一种实用方法(称为`get`)可以使访问这些变量变得更容易。下面的示例展示了如何使用`get`方法: + +``` +Map uriVariables = ServerWebExchangeUtils.getPathPredicateVariables(exchange); + +String segment = uriVariables.get("segment"); +``` + +### [](#the-query-route-predicate-factory)[5.9.查询路由谓词工厂](#the-query-route-predicate-factory) ### + +`Query`路由谓词工厂接受两个参数:一个必需的`param`和一个可选的`regexp`(这是一个 Java 正则表达式)。下面的示例配置一个查询路由谓词: + +示例 9.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: query_route + uri: https://example.org + predicates: + - Query=green +``` + +如果请求包含`green`查询参数,则前面的路由匹配。 + +application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: query_route + uri: https://example.org + predicates: + - Query=red, gree. +``` + +如果请求包含一个`red`查询参数,其值与`gree.`regexp 匹配,则前面的路由匹配,因此`green`和`greet`将匹配。 + +### [](#the-remoteaddr-route-predicate-factory)[5.10.RemoteAddr 路由谓词工厂](#the-remoteaddr-route-predicate-factory) ### + +`RemoteAddr`路由谓词工厂接受一个`sources`的列表(最小大小为 1),这些字符串是 CIDR 表示法(IPv4 或 IPv6)字符串,例如`192.168.0.1/16`(其中`192.168.0.1`是一个 IP 地址,`16`是一个子网掩码)。以下示例配置了一个 RemoteAddr 路由谓词: + +示例 10.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: remoteaddr_route + uri: https://example.org + predicates: + - RemoteAddr=192.168.1.1/24 +``` + +如果请求的远程地址是`192.168.1.10`,则此路由匹配。 + +#### [](#modifying-the-way-remote-addresses-are-resolved)[5.10.1.修改远程地址的解析方式](#modifying-the-way-remote-addresses-are-resolved) #### + +默认情况下,RemoteAddr 路由谓词工厂使用来自传入请求的远程地址。如果 Spring 云网关位于代理层的后面,这可能与实际的客户端 IP 地址不匹配。 + +你可以通过设置自定义`RemoteAddressResolver`来定制远程地址的解析方式。 Spring 云网关带有一个基于[X-forward-for 标头](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For),`XForwardedRemoteAddressResolver`的非默认远程地址解析器。 + +`XForwardedRemoteAddressResolver`有两个静态构造函数方法,它们采用不同的方法实现安全性: + +* `XForwardedRemoteAddressResolver::trustAll`返回一个`RemoteAddressResolver`,它总是使用在`X-Forwarded-For`头中找到的第一个 IP 地址。这种方法容易受到欺骗,因为恶意客户端可能会为`X-Forwarded-For`设置初始值,该初始值将被解析器接受。 + +* `XForwardedRemoteAddressResolver::maxTrustedIndex`获取一个索引,该索引与在 Spring 云网关前运行的受信任基础设施的数量相关。 Spring 如果云网关例如只能通过 HAProxy 访问,那么应该使用 1 的值。如果在访问 Spring 云网关之前需要两次跳可信的基础设施,那么应该使用 2 的值。 + +考虑以下标头值: + +``` +X-Forwarded-For: 0.0.0.1, 0.0.0.2, 0.0.0.3 +``` + +以下`maxTrustedIndex`值产生以下远程地址: + +| `maxTrustedIndex` |结果| +|------------------------|-----------------------------------------------------------| +|[`Integer.MIN_VALUE`,0] |(无效,`IllegalArgumentException`在初始化期间)| +| 1 | 0.0.0.3 | +| 2 | 0.0.0.2 | +| 3 | 0.0.0.1 | +|[4, `Integer.MAX_VALUE`]| 0.0.0.1 | + +下面的示例展示了如何使用 Java 实现相同的配置: + +例 11。gatewayconfig.java + +``` +RemoteAddressResolver resolver = XForwardedRemoteAddressResolver + .maxTrustedIndex(1); + +... + +.route("direct-route", + r -> r.remoteAddr("10.1.1.1", "10.10.1.1/24") + .uri("https://downstream1") +.route("proxied-route", + r -> r.remoteAddr(resolver, "10.10.1.1", "10.10.1.1/24") + .uri("https://downstream2") +) +``` + +### [](#the-weight-route-predicate-factory)[5.11.权重路径谓词工厂](#the-weight-route-predicate-factory) ### + +`Weight`路由谓词工厂接受两个参数:`group`和`weight`(一个 INT)。权重是按组计算的。下面的示例配置了权重路由谓词: + +示例 12.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: weight_high + uri: https://weighthigh.org + predicates: + - Weight=group1, 8 + - id: weight_low + uri: https://weightlow.org + predicates: + - Weight=group1, 2 +``` + +此路线将把 80% 的流量转发给[weighthigh.org](https://weighthigh.org),并将 20% 的流量转发给[weighlow.org](https://weighlow.org)。 + +### [](#the-xforwarded-remote-addr-route-predicate-factory)[5.12.XForwarded 远程 addr 路由谓词工厂](#the-xforwarded-remote-addr-route-predicate-factory) ### + +`XForwarded Remote Addr`路由谓词工厂接受一个`sources`的列表(最小大小为 1),这些字符串是 CIDR 表示法(IPv4 或 IPv6)字符串,例如`192.168.0.1/16`(其中`192.168.0.1`是一个 IP 地址,`16`是一个子网掩码)。 + +此路由谓词允许基于`X-Forwarded-For`HTTP 报头对请求进行过滤。 + +这可以用于反向代理,例如负载均衡器或 Web 应用程序防火墙,在这些代理中,只有当请求来自由这些反向代理使用的受信任的 IP 地址列表时,才允许请求。 + +下面的示例配置了一个 XForWardeDremoteaddr 路由谓词: + +示例 13.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: xforwarded_remoteaddr_route + uri: https://example.org + predicates: + - XForwardedRemoteAddr=192.168.1.1/24 +``` + +如果`X-Forwarded-For`标头包含`192.168.1.10`,则此路由匹配。 + +[](#gatewayfilter-factories)[6. `GatewayFilter` Factories](#gatewayfilter-factories) +---------- + +路由过滤器允许以某种方式修改传入 HTTP 请求或传出 HTTP 响应。路由过滤器的作用域是特定的路由。 Spring 云网关包括许多内置的网关过滤工厂。 + +| |有关如何使用以下任何过滤器的更详细示例,请查看[unit tests](https://github.com/spring-cloud/spring-cloud-gateway/tree/master/spring-cloud-gateway-server/src/test/java/org/springframework/cloud/gateway/filter/factory)。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#the-addrequestheader-gatewayfilter-factory)[6.1. The `AddRequestHeader` `GatewayFilter` Factory](#the-addrequestheader-gatewayfilter-factory) ### + +`AddRequestHeader``GatewayFilter`工厂接受一个`name`和`value`参数。以下示例配置`AddRequestHeader``GatewayFilter`: + +示例 14.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: add_request_header_route + uri: https://example.org + filters: + - AddRequestHeader=X-Request-red, blue +``` + +对于所有匹配的请求,此清单将`X-Request-red:blue`头添加到下游请求的头。 + +`AddRequestHeader`知道用于匹配路径或主机的 URI 变量。URI 变量可以在值中使用,并在运行时展开。下面的示例配置使用变量的`AddRequestHeader``GatewayFilter`: + +示例 15.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: add_request_header_route + uri: https://example.org + predicates: + - Path=/red/{segment} + filters: + - AddRequestHeader=X-Request-Red, Blue-{segment} +``` + +### [](#the-addrequestparameter-gatewayfilter-factory)[6.2. The `AddRequestParameter` `GatewayFilter` Factory](#the-addrequestparameter-gatewayfilter-factory) ### + +`AddRequestParameter``GatewayFilter`工厂接受`name`和`value`参数。下面的示例配置`AddRequestParameter``GatewayFilter`: + +示例 16.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: add_request_parameter_route + uri: https://example.org + filters: + - AddRequestParameter=red, blue +``` + +这将为所有匹配的请求将`red=blue`添加到下游请求的查询字符串中。 + +`AddRequestParameter`知道用于匹配路径或主机的 URI 变量。URI 变量可以在值中使用,并在运行时展开。下面的示例配置使用变量的`AddRequestParameter``GatewayFilter`: + +示例 17.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: add_request_parameter_route + uri: https://example.org + predicates: + - Host: {segment}.myhost.org + filters: + - AddRequestParameter=foo, bar-{segment} +``` + +### [](#the-addresponseheader-gatewayfilter-factory)[6.3. The `AddResponseHeader` `GatewayFilter` Factory](#the-addresponseheader-gatewayfilter-factory) ### + +`AddResponseHeader``GatewayFilter`工厂接受一个`name`和`value`参数。以下示例配置`AddResponseHeader``GatewayFilter`: + +示例 18.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: add_response_header_route + uri: https://example.org + filters: + - AddResponseHeader=X-Response-Red, Blue +``` + +这将为所有匹配的请求向下游响应的头添加`X-Response-Red:Blue`头。 + +`AddResponseHeader`知道用于匹配路径或主机的 URI 变量。URI 变量可以在值中使用,并在运行时展开。下面的示例配置了使用变量的`AddResponseHeader``GatewayFilter`: + +示例 19.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: add_response_header_route + uri: https://example.org + predicates: + - Host: {segment}.myhost.org + filters: + - AddResponseHeader=foo, bar-{segment} +``` + +### [](#the-deduperesponseheader-gatewayfilter-factory)[6.4. The `DedupeResponseHeader` `GatewayFilter` Factory](#the-deduperesponseheader-gatewayfilter-factory) ### + +DedupeResponseHeader 网关过滤器工厂接受一个`name`参数和一个可选的`strategy`参数。`name`可以包含一个以空格分隔的标题名称列表。以下示例配置`DedupeResponseHeader``GatewayFilter`: + +示例 20.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: dedupe_response_header_route + uri: https://example.org + filters: + - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin +``` + +在网关 CORS 逻辑和下游逻辑都添加响应头的情况下,这将删除重复的`Access-Control-Allow-Credentials`和`Access-Control-Allow-Origin`响应头的值。 + +`DedupeResponseHeader`过滤器还接受一个可选的`strategy`参数。接受的值是`RETAIN_FIRST`(默认)、`RETAIN_LAST`和`RETAIN_UNIQUE`。 + +### [](#spring-cloud-circuitbreaker-filter-factory)[6.5. Spring Cloud CircuitBreaker GatewayFilter Factory](#spring-cloud-circuitbreaker-filter-factory) ### + +Spring 云断路器网关过滤器工厂使用 Spring 云断路器 API 将网关路由封装在断路器中。 Spring Cloud Circuitbreaker 支持可与 Spring Cloud Gateway 一起使用的多个库。 Spring 云支持开箱即用的弹性 4J。 + +要启用 Spring 云电路断路器过滤器,你需要在 Classpath 上放置`spring-cloud-starter-circuitbreaker-reactor-resilience4j`。下面的示例配置 Spring 云电路断路器`GatewayFilter`: + +示例 21.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: circuitbreaker_route + uri: https://example.org + filters: + - CircuitBreaker=myCircuitBreaker +``` + +要配置断路器,请参阅你正在使用的底层断路器实现的配置。 + +* [复原力 4J 文档](https://cloud.spring.io/spring-cloud-circuitbreaker/reference/html/spring-cloud-circuitbreaker.html) + +Spring 云电路断路器过滤器还可以接受可选的`fallbackUri`参数。目前,只支持`forward:`模式 URI。如果回退被调用,请求将被转发到与 URI 匹配的控制器。下面的示例配置了这种回退: + +示例 22.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: circuitbreaker_route + uri: lb://backing-service:8088 + predicates: + - Path=/consumingServiceEndpoint + filters: + - name: CircuitBreaker + args: + name: myCircuitBreaker + fallbackUri: forward:/inCaseOfFailureUseThis + - RewritePath=/consumingServiceEndpoint, /backingServiceEndpoint +``` + +下面的列表在 Java 中做了相同的事情: + +例 23。application.java + +``` +@Bean +public RouteLocator routes(RouteLocatorBuilder builder) { + return builder.routes() + .route("circuitbreaker_route", r -> r.path("/consumingServiceEndpoint") + .filters(f -> f.circuitBreaker(c -> c.name("myCircuitBreaker").fallbackUri("forward:/inCaseOfFailureUseThis")) + .rewritePath("/consumingServiceEndpoint", "/backingServiceEndpoint")).uri("lb://backing-service:8088") + .build(); +} +``` + +当调用断路器回退时,此示例转发到`/inCaseofFailureUseThis`URI。请注意,此示例还演示了(可选的) Spring 云负载平衡器负载平衡(由目标 URI 上的`lb`前缀定义)。 + +主要的场景是使用`fallbackUri`在网关应用程序中定义内部控制器或处理程序。但是,你也可以将请求重新路由到外部应用程序中的控制器或处理程序,如下所示: + +示例 24.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: ingredients + uri: lb://ingredients + predicates: + - Path=//ingredients/** + filters: + - name: CircuitBreaker + args: + name: fetchIngredients + fallbackUri: forward:/fallback + - id: ingredients-fallback + uri: http://localhost:9994 + predicates: + - Path=/fallback +``` + +在此示例中,网关应用程序中没有`fallback`端点或处理程序。然而,在另一个应用程序中有一个,注册在`[localhost:9994](http://localhost:9994)`下。 + +在请求被转发到 Fallback 的情况下, Spring Cloud Circuitbreaker 网关过滤器还提供了导致它的`Throwable`。它被添加到`ServerWebExchange`中,作为`ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR`属性,该属性可以在处理网关应用程序内的回退时使用。 + +对于外部控制器/处理程序场景,可以添加带有异常详细信息的头。你可以在[FallbackHeaders GatewayFilter 工厂部分](#fallback-headers)中找到有关这样做的更多信息。 + +#### [](#circuit-breaker-status-codes)[6.5.1.按状态码切断断路器](#circuit-breaker-status-codes) #### + +在某些情况下,你可能希望基于从其封装的路由返回的状态码来跳闸断路器。断路器配置对象获取一系列状态代码,如果返回这些代码,将导致断路器跳闸。在设置要跳闸的状态码时,可以使用带有状态码值的整数,也可以使用`HttpStatus`枚举的字符串表示。 + +示例 25.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: circuitbreaker_route + uri: lb://backing-service:8088 + predicates: + - Path=/consumingServiceEndpoint + filters: + - name: CircuitBreaker + args: + name: myCircuitBreaker + fallbackUri: forward:/inCaseOfFailureUseThis + statusCodes: + - 500 + - "NOT_FOUND" +``` + +例 26。应用程序.java + +``` +@Bean +public RouteLocator routes(RouteLocatorBuilder builder) { + return builder.routes() + .route("circuitbreaker_route", r -> r.path("/consumingServiceEndpoint") + .filters(f -> f.circuitBreaker(c -> c.name("myCircuitBreaker").fallbackUri("forward:/inCaseOfFailureUseThis").addStatusCode("INTERNAL_SERVER_ERROR")) + .rewritePath("/consumingServiceEndpoint", "/backingServiceEndpoint")).uri("lb://backing-service:8088") + .build(); +} +``` + +### [](#fallback-headers)[6.6. The `FallbackHeaders` `GatewayFilter` Factory](#fallback-headers) ### + +`FallbackHeaders`工厂允许你在转发到外部应用程序中的`fallbackUri`的请求的标题中添加 Spring Cloud Circuitbreaker 执行异常详细信息,如下所示: + +示例 27.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: ingredients + uri: lb://ingredients + predicates: + - Path=//ingredients/** + filters: + - name: CircuitBreaker + args: + name: fetchIngredients + fallbackUri: forward:/fallback + - id: ingredients-fallback + uri: http://localhost:9994 + predicates: + - Path=/fallback + filters: + - name: FallbackHeaders + args: + executionExceptionTypeHeaderName: Test-Header +``` + +在本例中,在运行断路器时发生执行异常后,请求被转发到在`localhost:9994`上运行的应用程序中的`fallback`端点或处理程序。带有异常类型、消息和(如果可用的话)根原因异常类型和消息的头将由`FallbackHeaders`过滤器添加到该请求中。 + +你可以通过设置以下参数的值(以它们的默认值显示)来覆盖配置中的头的名称: + +* `executionExceptionTypeHeaderName`(`“execution-exception-type”`) + +* `executionExceptionMessageHeaderName`(`“execution-exception-message”`) + +* `rootCauseExceptionTypeHeaderName`(`“root-cause-exception-type”`) + +* `rootCauseExceptionMessageHeaderName`(`“root-cause-exception-message”`) + +有关断路器和网关的更多信息,请参见[Spring Cloud CircuitBreaker Factory section](#spring-cloud-circuitbreaker-filter-factory)。 + +### [](#the-maprequestheader-gatewayfilter-factory)[6.7. The `MapRequestHeader` `GatewayFilter` Factory](#the-maprequestheader-gatewayfilter-factory) ### + +`MapRequestHeader``GatewayFilter`工厂接受`fromHeader`和`toHeader`参数。它将创建一个新的命名头,并从传入的 HTTP 请求中从现有的命名头中提取该值。如果输入标头不存在,则过滤器不会产生任何影响。如果新的命名标头已经存在,那么它的值将被新的值扩充。以下示例配置`MapRequestHeader`: + +示例 28.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: map_request_header_route + uri: https://example.org + filters: + - MapRequestHeader=Blue, X-Request-Red +``` + +这会将`X-Request-Red:`头添加到下游请求,并从传入的 HTTP 请求的`Blue`头更新其值。 + +### [](#the-prefixpath-gatewayfilter-factory)[6.8. The `PrefixPath` `GatewayFilter` Factory](#the-prefixpath-gatewayfilter-factory) ### + +`PrefixPath``GatewayFilter`工厂接受一个`prefix`参数。下面的示例配置`PrefixPath``GatewayFilter`: + +示例 29.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: prefixpath_route + uri: https://example.org + filters: + - PrefixPath=/mypath +``` + +这将在所有匹配请求的路径前缀`/mypath`。因此,对`/hello`的请求将被发送到`/mypath/hello`。 + +### [](#the-preservehostheader-gatewayfilter-factory)[6.9. The `PreserveHostHeader` `GatewayFilter` Factory](#the-preservehostheader-gatewayfilter-factory) ### + +`PreserveHostHeader``GatewayFilter`工厂没有参数。此筛选器设置一个请求属性,由路由筛选器检查该属性,以确定是否应发送原始的主机头,而不是由 HTTP 客户机确定的主机头。以下示例配置`PreserveHostHeader``GatewayFilter`: + +示例 30.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: preserve_host_route + uri: https://example.org + filters: + - PreserveHostHeader +``` + +### [](#the-requestratelimiter-gatewayfilter-factory)[6.10. The `RequestRateLimiter` `GatewayFilter` Factory](#the-requestratelimiter-gatewayfilter-factory) ### + +`RequestRateLimiter``GatewayFilter`工厂使用`RateLimiter`实现来确定是否允许当前请求继续执行。如果不是,则返回`HTTP 429 - Too Many Requests`(默认情况下)的状态。 + +这个过滤器接受一个可选的`keyResolver`参数和特定于速率限制器的参数(将在本节后面描述)。 + +`keyResolver`是实现`KeyResolver`接口的 Bean。在配置中,使用 spel 按名称引用 Bean。`#{@mykeyresolver}` 是引用名为`myKeyResolver`的 Bean 的 spel 表达式。下面的清单显示了`KeyResolver`接口: + +例 31。keyresolver.java + +``` +public interface KeyResolver { + Mono resolve(ServerWebExchange exchange); +} +``` + +`KeyResolver`接口让可插入策略派生限制请求的键。在未来的里程碑版本中,将会有一些`KeyResolver`实现。 + +`KeyResolver`的默认实现是`PrincipalNameKeyResolver`,它从`ServerWebExchange`检索`Principal`并调用`Principal.getName()`。 + +默认情况下,如果`KeyResolver`没有找到密钥,请求将被拒绝。你可以通过设置`spring.cloud.gateway.filter.request-rate-limiter.deny-empty-key`(`true’或`false`)和`spring.cloud.gateway.filter.request-rate-limiter.empty-key-status-code`属性来调整此行为。 + +| |`RequestRateLimiter`不能使用“shortcut”符号进行配置。下面的示例是*无效*:

示例 32.application.properties

```
# 无效的快捷方式配置
Spring.cloud.gateway.routes[0].filters[0]=requestrateLimiter=2,2,#{@userkeyresolever=“426”/>```| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#the-redis-ratelimiter)[6.10.1. The Redis `RateLimiter`](#the-redis-ratelimiter) #### + +Redis 实现基于在[Stripe](https://stripe.com/blog/rate-limiters)上完成的工作。它需要使用`spring-boot-starter-data-redis-reactive` Spring 引导启动器。 + +使用的算法是[令牌桶算法](https://en.wikipedia.org/wiki/Token_bucket)。 + +`redis-rate-limiter.replenishRate`属性是你希望用户在不删除任何请求的情况下每秒可以执行多少个请求。这是令牌桶被填满的速率。 + +`redis-rate-limiter.burstCapacity`属性是用户在一秒钟内被允许执行的最大请求数。这是令牌桶可以容纳的令牌数量。将此值设置为零将阻止所有请求。 + +`redis-rate-limiter.requestedTokens`属性是一个请求需要多少令牌。这是每个请求从 bucket 中获取的令牌的数量,默认为`1`。 + +通过在`replenishRate`和`burstCapacity`中设置相同的值,可以实现稳定的速率。可以通过将`burstCapacity`设置为高于`replenishRate`来允许临时突发。在这种情况下,速率限制器需要允许在两次突发之间有一段时间(根据`replenishRate`),因为连续两次突发将导致丢弃请求(`HTTP429-太多请求’)。下面的清单配置了`redis-rate-limiter`: + +速率限制`1 request/s`通过将`replenishRate`设置为所需的请求数,`requestedTokens`设置为秒内的时间跨度,`burstCapacity`设置为`replenishRate`和`requestedTokens`的乘积,例如,设置`replenishRate=1`,`requestedTokens=60`和`burstCapacity=60`将导致`1 request/min`的限制。 + +示例 33.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: requestratelimiter_route + uri: https://example.org + filters: + - name: RequestRateLimiter + args: + redis-rate-limiter.replenishRate: 10 + redis-rate-limiter.burstCapacity: 20 + redis-rate-limiter.requestedTokens: 1 +``` + +下面的示例在 Java 中配置一个 keyresolver: + +例 34。config.java + +``` +@Bean +KeyResolver userKeyResolver() { + return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user")); +} +``` + +这定义了每个用户 10 个的请求速率限制。允许突发 20 个请求,但在接下来的一秒钟内,只有 10 个请求可用。`KeyResolver`是一个获得`user`请求参数的简单参数(请注意,这不推荐用于生产)。 + +还可以将速率限制器定义为实现`RateLimiter`接口的 Bean。在配置中,你可以使用 spel 按名称引用 Bean。`#{@myratelimiter}` 是一个 spel 表达式,它引用名为`myRateLimiter`的 Bean。下面的清单定义了一个速率限制器,该限制器使用上一个清单中定义的`KeyResolver`: + +示例 35.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: requestratelimiter_route + uri: https://example.org + filters: + - name: RequestRateLimiter + args: + rate-limiter: "#{@myRateLimiter}" + key-resolver: "#{@userKeyResolver}" +``` + +### [](#the-redirectto-gatewayfilter-factory)[6.11. The `RedirectTo` `GatewayFilter` Factory](#the-redirectto-gatewayfilter-factory) ### + +`RedirectTo``GatewayFilter`工厂接受两个参数,`status`和`url`。`status`参数应该是 300 系列重定向 HTTP 代码,例如 301。`url`参数应该是一个有效的 URL。这是`Location`标头的值。对于相对重定向,应该使用`uri: no://op`作为路由定义的 URI。下面的列表配置了`RedirectTo``GatewayFilter`: + +示例 36.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: prefixpath_route + uri: https://example.org + filters: + - RedirectTo=302, https://acme.org +``` + +这将发送带有`Location:https://acme.org`头的状态 302 来执行重定向。 + +### [](#the-removerequestheader-gatewayfilter-factory)[6.12. The `RemoveRequestHeader` GatewayFilter Factory](#the-removerequestheader-gatewayfilter-factory) ### + +`RemoveRequestHeader``GatewayFilter`工厂接受一个`name`参数。它是要删除的标头的名称。下面的列表配置了`RemoveRequestHeader``GatewayFilter`: + +示例 37.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: removerequestheader_route + uri: https://example.org + filters: + - RemoveRequestHeader=X-Request-Foo +``` + +这将在向下游发送`X-Request-Foo`头之前删除它。 + +### [](#removeresponseheader-gatewayfilter-factory)[6.13. `RemoveResponseHeader` `GatewayFilter` Factory](#removeresponseheader-gatewayfilter-factory) ### + +`RemoveResponseHeader``GatewayFilter`工厂接受一个`name`参数。它是要删除的标头的名称。下面的列表配置了`RemoveResponseHeader``GatewayFilter`: + +示例 38.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: removeresponseheader_route + uri: https://example.org + filters: + - RemoveResponseHeader=X-Response-Foo +``` + +这将在响应返回到网关客户机之前从响应中删除`X-Response-Foo`头。 + +要删除任何类型的敏感报头,你应该为你可能想要删除的任何路由配置此筛选器。此外,你可以使用`spring.cloud.gateway.default-filters`配置该过滤器一次,并将其应用于所有路由。 + +### [](#the-removerequestparameter-gatewayfilter-factory)[6.14. The `RemoveRequestParameter` `GatewayFilter` Factory](#the-removerequestparameter-gatewayfilter-factory) ### + +`RemoveRequestParameter``GatewayFilter`工厂接受一个`name`参数。它是要删除的查询参数的名称。下面的示例配置`RemoveRequestParameter``GatewayFilter`: + +示例 39.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: removerequestparameter_route + uri: https://example.org + filters: + - RemoveRequestParameter=red +``` + +这将在向下游发送`red`参数之前删除该参数。 + +### [](#the-rewritepath-gatewayfilter-factory)[6.15. The `RewritePath` `GatewayFilter` Factory](#the-rewritepath-gatewayfilter-factory) ### + +`RewritePath``GatewayFilter`工厂接受一个路径`regexp`参数和一个`replacement`参数。这使用 Java 正则表达式以灵活的方式重写请求路径。下面的列表配置了`RewritePath``GatewayFilter`: + +示例 40.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: rewritepath_route + uri: https://example.org + predicates: + - Path=/red/** + filters: + - RewritePath=/red/?(?.*), /$\{segment} +``` + +应该替换为`$\`。 + +### [](#rewritelocationresponseheader-gatewayfilter-factory)[6.16. `RewriteLocationResponseHeader` `GatewayFilter` Factory](#rewritelocationresponseheader-gatewayfilter-factory) ### + +`RewriteLocationResponseHeader``GatewayFilter`工厂修改`Location`响应头的值,通常是为了去掉后台特定的细节。它需要`stripVersionMode`,`locationHeaderName`,`hostValue`和`protocolsRegex`参数。下面的列表配置了`RewriteLocationResponseHeader``GatewayFilter`: + +示例 41.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: rewritelocationresponseheader_route + uri: http://example.org + filters: + - RewriteLocationResponseHeader=AS_IN_REQUEST, Location, , +``` + +例如,对于`POST [api.example.com/some/object/name](https://api.example.com/some/object/name)`的请求,`Location`的响应头值`[object-service.prod.example.net/v2/some/object/id](https://object-service.prod.example.net/v2/some/object/id)`被重写为`[api.example.com/some/object/id](https://api.example.com/some/object/id)`。 + +`stripVersionMode`参数有以下可能的值:`NEVER_STRIP`,`AS_IN_REQUEST`(默认),和`ALWAYS_STRIP`。 + +* `NEVER_STRIP`:即使原始请求路径不包含版本,也不会剥离版本。 + +* `AS_IN_REQUEST`只有当原始请求路径不包含版本时,才会剥离版本。 + +* `ALWAYS_STRIP`版本总是被剥离,即使原始请求路径包含版本。 + +如果提供了`hostValue`参数,则用于替换响应`host:port`头的`host:port`部分。如果没有提供,则使用`Host`请求头的值。 + +参数`protocolsRegex`必须是一个有效的 regex`String`,协议名称与该 regex 匹配。如果不匹配,过滤器就不会做任何事情。默认值为`http|https|ftp|ftps`。 + +### [](#the-rewriteresponseheader-gatewayfilter-factory)[6.17. The `RewriteResponseHeader` `GatewayFilter` Factory](#the-rewriteresponseheader-gatewayfilter-factory) ### + +`RewriteResponseHeader``GatewayFilter`工厂接受`name`、`regexp`和`replacement`参数。它使用 Java 正则表达式以一种灵活的方式重写响应头值。下面的示例配置`RewriteResponseHeader``GatewayFilter`: + +示例 42.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: rewriteresponseheader_route + uri: https://example.org + filters: + - RewriteResponseHeader=X-Response-Red, , password=[^&]+, password=*** +``` + +。 + +### [](#the-savesession-gatewayfilter-factory)[6.18. The `SaveSession` `GatewayFilter` Factory](#the-savesession-gatewayfilter-factory) ### + +`SaveSession``GatewayFilter`工厂强制执行`WebSession::save`操作*在此之前*向下游转发呼叫。这在使用带有惰性数据存储的[Spring Session](https://projects.spring.io/spring-session/)之类的东西时特别有用,并且需要确保在进行转发调用之前保存了会话状态。以下示例配置`SaveSession``GatewayFilter`: + +示例 43.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: save_session + uri: https://example.org + predicates: + - Path=/foo/** + filters: + - SaveSession +``` + +如果你将[Spring Security](https://projects.spring.io/spring-security/)与 Spring 会话集成,并希望确保已将安全细节转发到远程进程,这是非常关键的。 + +### [](#the-secureheaders-gatewayfilter-factory)[6.19. The `SecureHeaders` `GatewayFilter` Factory](#the-secureheaders-gatewayfilter-factory) ### + +`SecureHeaders``GatewayFilter`工厂根据[this blog post](https://blog.appcanary.com/2017/http-security-headers.html)中提出的建议,向响应添加了许多标题。 + +添加了以下标题(以其默认值显示): + +* `X-Xss-Protection:1 (mode=block`) + +* `Strict-Transport-Security (max-age=631138519`) + +* `X-Frame-Options (DENY)` + +* `X-Content-Type-Options (nosniff)` + +* `Referrer-Policy (no-referrer)` + +* `Content-Security-Policy (default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline)'` + +* `X-Download-Options (noopen)` + +* `X-Permitted-Cross-Domain-Policies (none)` + +要更改默认值,请在`spring.cloud.gateway.filter.secure-headers`名称空间中设置适当的属性。以下属性可供选择: + +* `xss-protection-header` + +* `strict-transport-security` + +* `x-frame-options` + +* `x-content-type-options` + +* `referrer-policy` + +* `content-security-policy` + +* `x-download-options` + +* `x-permitted-cross-domain-policies` + +要禁用默认值,请使用逗号分隔的值设置`spring.cloud.gateway.filter.secure-headers.disable`属性。下面的示例展示了如何做到这一点: + +``` +spring.cloud.gateway.filter.secure-headers.disable=x-frame-options,strict-transport-security +``` + +| |需要使用安全报头的小写字母全名来禁用它。| +|---|-----------------------------------------------------------------------------| + +### [](#the-setpath-gatewayfilter-factory)[6.20. The `SetPath` `GatewayFilter` Factory](#the-setpath-gatewayfilter-factory) ### + +`SetPath``GatewayFilter`工厂接受一个路径`template`参数。它提供了一种简单的方法,通过允许模板化的路径段来操作请求路径。这使用了 Spring Framework 中的 URI 模板。允许多个匹配段。以下示例配置`SetPath``GatewayFilter`: + +示例 44.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: setpath_route + uri: https://example.org + predicates: + - Path=/red/{segment} + filters: + - SetPath=/{segment} +``` + +对于`/red/blue`的请求路径,在发出下游请求之前,将路径设置为`/blue`。 + +### [](#the-setrequestheader-gatewayfilter-factory)[6.21. The `SetRequestHeader` `GatewayFilter` Factory](#the-setrequestheader-gatewayfilter-factory) ### + +`SetRequestHeader``GatewayFilter`工厂接受`name`和`value`参数。下面的列表配置了`SetRequestHeader``GatewayFilter`: + +示例 45.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: setrequestheader_route + uri: https://example.org + filters: + - SetRequestHeader=X-Request-Red, Blue +``` + +这个`GatewayFilter`用给定的名称替换(而不是添加)所有的头。因此,如果下游服务器用`X-Request-Red:1234`响应,这将被替换为`X-Request-Red:Blue`,这是下游服务将接收的内容。 + +`SetRequestHeader`知道用于匹配路径或主机的 URI 变量。URI 变量可以在值中使用,并在运行时展开。下面的示例配置使用变量的`SetRequestHeader``GatewayFilter`: + +示例 46.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: setrequestheader_route + uri: https://example.org + predicates: + - Host: {segment}.myhost.org + filters: + - SetRequestHeader=foo, bar-{segment} +``` + +### [](#the-setresponseheader-gatewayfilter-factory)[6.22. The `SetResponseHeader` `GatewayFilter` Factory](#the-setresponseheader-gatewayfilter-factory) ### + +`SetResponseHeader``GatewayFilter`工厂接受`name`和`value`参数。下面的列表配置了`SetResponseHeader``GatewayFilter`: + +示例 47.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: setresponseheader_route + uri: https://example.org + filters: + - SetResponseHeader=X-Response-Red, Blue +``` + +这个 GatewayFilter 用给定的名称替换(而不是添加)所有的头。因此,如果下游服务器使用`X-Response-Red:1234`进行响应,则将其替换为`X-Response-Red:Blue`,这是网关客户机将接收的内容。 + +`SetResponseHeader`知道用于匹配路径或主机的 URI 变量。URI 变量可以在值中使用,并将在运行时展开。下面的示例配置使用变量的`SetResponseHeader``GatewayFilter`: + +示例 48.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: setresponseheader_route + uri: https://example.org + predicates: + - Host: {segment}.myhost.org + filters: + - SetResponseHeader=foo, bar-{segment} +``` + +### [](#the-setstatus-gatewayfilter-factory)[6.23. The `SetStatus` `GatewayFilter` Factory](#the-setstatus-gatewayfilter-factory) ### + +`SetStatus``GatewayFilter`工厂只接受一个参数,`status`。它必须是有效的 Spring `HttpStatus`。它可以是整数值`404`或枚举的字符串表示:`NOT_FOUND`。下面的列表配置了`SetStatus``GatewayFilter`: + +示例 49.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: setstatusstring_route + uri: https://example.org + filters: + - SetStatus=UNAUTHORIZED + - id: setstatusint_route + uri: https://example.org + filters: + - SetStatus=401 +``` + +在这两种情况下,响应的 HTTP 状态都设置为 401。 + +你可以将`SetStatus``GatewayFilter`配置为从响应中的报头中的代理请求返回原始 HTTP 状态代码。如果配置了以下属性,则会将头添加到响应中: + +示例 50.application.yml + +``` +spring: + cloud: + gateway: + set-status: + original-status-header-name: original-http-status +``` + +### [](#the-stripprefix-gatewayfilter-factory)[6.24. The `StripPrefix` `GatewayFilter` Factory](#the-stripprefix-gatewayfilter-factory) ### + +`StripPrefix``GatewayFilter`工厂接受一个参数,`parts`。`parts`参数指示在向下游发送请求之前要从请求中剥离的路径中的部件数量。下面的列表配置了`StripPrefix``GatewayFilter`: + +示例 51.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: nameRoot + uri: https://nameservice + predicates: + - Path=/name/** + filters: + - StripPrefix=2 +``` + +当通过网关向`/name/blue/red`发出请求时,向`nameservice`发出的请求看起来像`[nameservice/red](https://nameservice/red)`。 + +### [](#the-retry-gatewayfilter-factory)[6.25. The Retry `GatewayFilter` Factory](#the-retry-gatewayfilter-factory) ### + +`Retry``GatewayFilter`工厂支持以下参数: + +* `retries`:应该尝试的重试次数。 + +* `statuses`:应该重试的 HTTP 状态代码,用`org.springframework.http.HttpStatus`表示。 + +* `methods`:应该重试的 HTTP 方法,用`org.springframework.http.HttpMethod`表示。 + +* `series`:要重试的一系列状态代码,用`org.springframework.http.HttpStatus.Series`表示。 + +* `exceptions`:应该重试的抛出的异常列表。 + +* `backoff`:为重试配置的指数退避。在回退间隔`firstBackoff * (factor ^ n)`之后执行重试,其中`n`是迭代。如果`maxBackoff`已配置,则应用的最大退避限制为`maxBackoff`。如果`basedOnPreviousValue`为真,则按`prevBackoff * factor`计算退避。 + +如果启用,以下默认值将配置为`Retry`过滤器: + +* `retries`:三次 + +* `series`:5XX 系列 + +* `methods`:get 方法 + +* `exceptions`:`IOException`和`TimeoutException` + +* `backoff`:禁用 + +下面的列表配置了重试`GatewayFilter`: + +示例 52.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: retry_test + uri: http://localhost:8080/flakey + predicates: + - Host=*.retry.com + filters: + - name: Retry + args: + retries: 3 + statuses: BAD_GATEWAY + methods: GET,POST + backoff: + firstBackoff: 10ms + maxBackoff: 50ms + factor: 2 + basedOnPreviousValue: false +``` + +| |当使用带有`forward:`前缀 URL 的重试筛选器时,目标端点应该仔细地编写,以便在发生错误的情况下,它不会执行任何可能导致将响应发送到客户机并提交的操作,例如,
,如果目标端点是带注释的控制器,则目标控制器方法不应该返回带有错误状态码的`ResponseEntity`,而是应该抛出
,或者发出错误信号(例如,通过`Mono.error(ex)`返回值),重试筛选器可以配置为通过重试来处理该筛选器。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |当在带有主体的任何 HTTP 方法中使用 Retry 过滤器时,主体将被缓存,网关将成为内存约束。主体缓存在由`ServerWebExchangeUtils.CACHED_REQUEST_BODY_ATTR`定义的请求属性中。对象的类型是`org.springframework.core.io.buffer.DataBuffer`。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +简化的“shortcut”符号可以添加一个`status`和`method`。 + +以下两个例子是等价的: + +示例 53.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: retry_route + uri: https://example.org + filters: + - name: Retry + args: + retries: 3 + statuses: INTERNAL_SERVER_ERROR + methods: GET + backoff: + firstBackoff: 10ms + maxBackoff: 50ms + factor: 2 + basedOnPreviousValue: false + + - id: retryshortcut_route + uri: https://example.org + filters: + - Retry=3,INTERNAL_SERVER_ERROR,GET,10ms,50ms,2,false +``` + +### [](#the-requestsize-gatewayfilter-factory)[6.26. The `RequestSize` `GatewayFilter` Factory](#the-requestsize-gatewayfilter-factory) ### + +当请求大小大于允许的限制时,`RequestSize``GatewayFilter`工厂可以限制请求到达下游服务。过滤器接受`maxSize`参数。`maxSize`是`DataSize`类型,因此值可以定义为一个数字,后面跟着一个可选的`DataUnit`后缀,例如“KB”或“MB”。对于字节,默认值为“B”。它是以字节为单位定义的请求的允许大小限制。下面的列表配置了`RequestSize``GatewayFilter`: + +示例 54.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: request_size_route + uri: http://localhost:8080/upload + predicates: + - Path=/upload + filters: + - name: RequestSize + args: + maxSize: 5000000 +``` + +当请求由于大小而被拒绝时,`RequestSize`工厂将响应状态设置为`413 Payload Too Large`,并带有一个附加的头`errorMessage`。下面的示例显示了这样的`errorMessage`: + +``` +errorMessage : Request size is larger than permissible limit. Request size is 6.0 MB where permissible limit is 5.0 MB +``` + +| |如果在路由定义中没有作为筛选参数提供,则默认的请求大小设置为 5MB。| +|---|--------------------------------------------------------------------------------------------------------| + +### [](#the-setrequesthostheader-gatewayfilter-factory)[6.27. The `SetRequestHostHeader` `GatewayFilter` Factory](#the-setrequesthostheader-gatewayfilter-factory) ### + +在某些情况下,主机标题可能需要被重写。在这种情况下,`SetRequestHostHeader``GatewayFilter`工厂可以用指定的 vaue 替换现有的主机报头。过滤器接受`host`参数。下面的列表配置了`SetRequestHostHeader``GatewayFilter`: + +示例 55.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: set_request_host_header_route + uri: http://localhost:8080/headers + predicates: + - Path=/headers + filters: + - name: SetRequestHostHeader + args: + host: example.org +``` + +`SetRequestHostHeader``GatewayFilter`工厂将主机报头的值替换为`example.org`。 + +### [](#modify-a-request-body-gatewayfilter-factory)[6.28. Modify a Request Body `GatewayFilter` Factory](#modify-a-request-body-gatewayfilter-factory) ### + +你可以使用`ModifyRequestBody`过滤器过滤器来修改请求主体,然后再由网关向下游发送。 + +| |这个过滤器只能通过使用 Java DSL 进行配置。| +|---|---------------------------------------------------------| + +下面的清单显示了如何修改请求主体`GatewayFilter`: + +``` +@Bean +public RouteLocator routes(RouteLocatorBuilder builder) { + return builder.routes() + .route("rewrite_request_obj", r -> r.host("*.rewriterequestobj.org") + .filters(f -> f.prefixPath("/httpbin") + .modifyRequestBody(String.class, Hello.class, MediaType.APPLICATION_JSON_VALUE, + (exchange, s) -> return Mono.just(new Hello(s.toUpperCase())))).uri(uri)) + .build(); +} + +static class Hello { + String message; + + public Hello() { } + + public Hello(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} +``` + +| |如果请求没有正文,则将传递`RewriteFilter`。应该返回`Mono.empty()`以分配请求中缺少的主体。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#modify-a-response-body-gatewayfilter-factory)[6.29. Modify a Response Body `GatewayFilter` Factory](#modify-a-response-body-gatewayfilter-factory) ### + +你可以使用`ModifyResponseBody`过滤器来修改响应主体,然后再将其发送回客户机。 + +| |这个过滤器只能通过使用 Java DSL 进行配置。| +|---|---------------------------------------------------------| + +下面的清单显示了如何修改响应主体`GatewayFilter`: + +``` +@Bean +public RouteLocator routes(RouteLocatorBuilder builder) { + return builder.routes() + .route("rewrite_response_upper", r -> r.host("*.rewriteresponseupper.org") + .filters(f -> f.prefixPath("/httpbin") + .modifyResponseBody(String.class, String.class, + (exchange, s) -> Mono.just(s.toUpperCase()))).uri(uri)) + .build(); +} +``` + +| |如果响应没有主体,那么`RewriteFilter`将被传递`null`。应该返回`Mono.empty()`,以便在响应中分配一个缺少的主体。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#token-relay-gatewayfilter-factory)[6.30. Token Relay `GatewayFilter` Factory](#token-relay-gatewayfilter-factory) ### + +令牌中继是 OAuth2 使用者充当客户端并将传入的令牌转发给传出的资源请求的一种方式。使用者可以是纯客户机(如 SSO 应用程序)或资源服务器。 + +Spring 云网关可以将 OAuth2 访问令牌转发到其代理的服务的下游。要将此功能添加到 Gateway,你需要添加“TokenRelayGatewayFilterFactory”,如下所示: + +app.java + +``` +@Bean +public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { + return builder.routes() + .route("resource", r -> r.path("/resource") + .filters(f -> f.tokenRelay()) + .uri("http://localhost:9000")) + .build(); +} +``` + +或者这个 + +应用程序.YAML + +``` +spring: + cloud: + gateway: + routes: + - id: resource + uri: http://localhost:9000 + predicates: + - Path=/resource + filters: + - TokenRelay= +``` + +并且它将(除了登录用户并获取令牌之外)向下游传递身份验证令牌到服务(在这种情况下是 `/resource`)。 + +要为 Spring 云网关启用此功能,请添加以下依赖项 + +* `org.springframework.boot:spring-boot-starter-oauth2-client` + +它是如何工作的?{githubmaster}/SRC/main/java/org/springframework/cloud/gateway/security/tokenrelaygatewayfilterfactory.java[filter]从当前经过身份验证的用户中提取一个访问令牌,并将其放在下游请求的请求头中。 + +有关完整的工作示例,请参见[this project](https://github.com/spring-cloud-samples/sample-gateway-oauth2login)。 + +| |只有设置了适当的`spring.security.oauth2.client.*`属性才会创建`TokenRelayGatewayFilterFactory` Bean,这将触发创建`ReactiveClientRegistrationRepository` Bean。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |`TokenRelayGatewayFilterFactory`使用的`ReactiveOAuth2AuthorizedClientService`的默认实现使用内存中的数据存储。如果需要更健壮的解决方案,则需要提供自己的实现`ReactiveOAuth2AuthorizedClientService`。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#the-cacherequestbody-gatewayfilter-factory)[6.31. The `CacheRequestBody` `GatewayFilter` Factory](#the-cacherequestbody-gatewayfilter-factory) ### + +由于请求体流只能被读取一次,所以需要对请求体流进行缓存。你可以使用`CacheRequestBody`过滤器来缓存请求主体,然后再将其发送到下游,并从 exchagne 属性获取主体。 + +下面的清单显示了如何缓存请求主体`GatewayFilter`: + +``` +@Bean +public RouteLocator routes(RouteLocatorBuilder builder) { + return builder.routes() + .route("cache_request_body_route", r -> r.path("/downstream/**") + .filters(f -> f.prefixPath("/httpbin") + .cacheRequestBody(String.class).uri(uri)) + .build(); +} +``` + +示例 56.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: cache_request_body_route + uri: lb://downstream + predicates: + - Path=/downstream/** + filters: + - name: CacheRequestBody + args: + bodyClass: java.lang.String +``` + +`CacheRequestBody`将提取请求主体并将其转换为主体类(如`java.lang.String`,在前面的示例中定义)。然后将其放置在`ServerWebExchange.getAttributes()`中,并在`ServerWebExchangeUtils.CACHED_REQUEST_BODY_ATTR`中定义一个键。 + +| |此过滤器仅适用于 HTTP 请求(包括 HTTPS)。| +|---|-----------------------------------------------------------| + +### [](#default-filters)[6.32.默认过滤器](#default-filters) ### + +要添加筛选器并将其应用到所有路由,你可以使用`spring.cloud.gateway.default-filters`。此属性接受一个过滤器列表。下面的清单定义了一组默认筛选器: + +示例 57.application.yml + +``` +spring: + cloud: + gateway: + default-filters: + - AddResponseHeader=X-Response-Default-Red, Default-Blue + - PrefixPath=/httpbin +``` + +[](#global-filters)[7.全局过滤器](#global-filters) +---------- + +`GlobalFilter`接口具有与`GatewayFilter`相同的签名。这些是有条件地应用于所有路由的特殊过滤器。 + +| |该接口及其使用情况可能会在未来的里程碑版本中发生更改。| +|---|--------------------------------------------------------------------------------| + +### [](#gateway-combined-global-filter-and-gatewayfilter-ordering)[7.1. Combined Global Filter and `GatewayFilter` Ordering](#gateway-combined-global-filter-and-gatewayfilter-ordering) ### + +当请求与路由匹配时,过滤 Web 处理程序将`GlobalFilter`的所有实例和`GatewayFilter`的所有特定于路由的实例添加到筛选链中。这个组合的过滤器链通过`org.springframework.core.Ordered`接口进行排序,你可以通过实现`getOrder()`方法来设置该接口。 + +Spring 由于云网关对用于过滤器逻辑执行的“pre”和“post”阶段进行了区分(参见[How it Works](#gateway-how-it-works)),优先级最高的过滤器是“pre”阶段中的第一个,“post”阶段中的最后一个。 + +下面的清单配置了一个过滤器链: + +例 58。示例 configuration.java + +``` +@Bean +public GlobalFilter customFilter() { + return new CustomGlobalFilter(); +} + +public class CustomGlobalFilter implements GlobalFilter, Ordered { + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + log.info("custom global filter"); + return chain.filter(exchange); + } + + @Override + public int getOrder() { + return -1; + } +} +``` + +### [](#forward-routing-filter)[7.2.前向路由滤波器](#forward-routing-filter) ### + +`ForwardRoutingFilter`在 exchange 属性`ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR`中查找 URI。如果 URL 具有`forward`方案(例如`forward:///localendpoint`),则它使用 Spring `DispatcherHandler`来处理请求。请求 URL 的路径部分被转发 URL 中的路径覆盖。未修改的原始 URL 被追加到`ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR`属性中的列表中。 + +### [](#reactive-loadbalancer-client-filter)[7.3. The `ReactiveLoadBalancerClientFilter`](#reactive-loadbalancer-client-filter) ### + +`ReactiveLoadBalancerClientFilter`在名为`ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR`的 exchange 属性中查找 URI。如果 URL 具有`lb`方案(例如`lb://myservice`),则它使用 Spring cloud`ReactorLoadBalancer`将名称(本例中的 `myservice’)解析为实际的主机和端口,并替换相同属性中的 URI。未修改的原始 URL 被追加到`ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR`属性中的列表中。过滤器还查看`ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR`属性,以查看它是否等于`lb`。如果是这样,也适用同样的规则。下面的列表配置了`ReactiveLoadBalancerClientFilter`: + +示例 59.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: myRoute + uri: lb://service + predicates: + - Path=/service/** +``` + +| |默认情况下,当`ReactorLoadBalancer`无法找到服务实例时,将返回一个`503`。
通过设置`spring.cloud.gateway.loadbalancer.use404=true`,你可以将网关配置为返回一个`404`。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |从`ReactiveLoadBalancerClientFilter`返回的`isSecure`值的`ServiceInstance`覆盖了
向网关提出的请求中指定的方案。
例如,如果请求通过`HTTPS`进入网关,但`ServiceInstance`表示它不安全,下游请求是在`HTTP`上提出的。
相反的情况也可以适用。
但是,如果`GATEWAY_SCHEME_PREFIX_ATTR`在网关配置中为该路由指定了前缀,则从路由 URL 中得到的方案将覆盖`ServiceInstance`配置。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |网关支持所有的负载平衡器功能。你可以在[Spring Cloud Commons documentation](https://docs.spring.io/spring-cloud-commons/docs/current/reference/html/#spring-cloud-loadbalancer)中阅读有关它们的更多信息。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#the-netty-routing-filter)[7.4.Netty 路由过滤器](#the-netty-routing-filter) ### + +如果`ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR`Exchange 属性中的 URL 具有`http`或`https`方案,则运行 Netty 路由过滤器。它使用 netty`HttpClient`发出下游代理请求。将响应放入`ServerWebExchangeUtils.CLIENT_RESPONSE_ATTR`exchange 属性中,以便在以后的筛选器中使用。(还有一个实验性的`WebClientHttpRoutingFilter`,它执行相同的功能,但不需要 netty。 + +### [](#the-netty-write-response-filter)[7.5.Netty 写响应过滤器](#the-netty-write-response-filter) ### + +如果`ServerWebExchangeUtils.CLIENT_RESPONSE_ATTR`exchange 属性中有一个 netty`HttpClientResponse`,则运行`NettyWriteResponseFilter`。它在所有其他过滤器完成并将代理响应写回网关客户机响应之后运行。(还有一个实验性的`WebClientWriteResponseFilter`,它执行相同的功能,但不需要 netty。 + +### [](#the-routetorequesturl-filter)[7.6. The `RouteToRequestUrl` Filter](#the-routetorequesturl-filter) ### + +如果在`ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR`exchange 属性中有一个`Route`对象,则运行`RouteToRequestUrlFilter`。它根据请求 URI 创建一个新的 URI,但使用`Route`对象的 URI 属性进行更新。新的 URI 放在`ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR`exchange 属性中。 + +如果 URI 有一个方案前缀,例如`lb:ws://serviceid`,则将从 URI 中剥离`lb`方案,并将其放置在`ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR`中,以便稍后在筛选链中使用。 + +### [](#the-websocket-routing-filter)[7.7. The Websocket Routing Filter](#the-websocket-routing-filter) ### + +如果位于`ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR`Exchange 属性中的 URL 具有`ws`或`wss`方案,则运行 WebSocket 路由过滤器。它使用 Spring WebSocket 基础设施向下游转发 WebSocket 请求。 + +你可以通过在 URI 前加上`lb`来实现 WebSockets 的负载平衡,比如`lb:ws://serviceid`。 + +| |如果使用[SockJS](https://github.com/sockjs)作为普通 HTTP 的后备,则应该配置普通 HTTP 路由和 WebSocket 路由。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------| + +下面的清单配置了一个 WebSocket 路由过滤器: + +示例 60.application.yml + +``` +spring: + cloud: + gateway: + routes: + # SockJS route + - id: websocket_sockjs_route + uri: http://localhost:3001 + predicates: + - Path=/websocket/info/** + # Normal Websocket route + - id: websocket_route + uri: ws://localhost:3001 + predicates: + - Path=/websocket/** +``` + +### [](#the-gateway-metrics-filter)[7.8.网关指标过滤器](#the-gateway-metrics-filter) ### + +要启用网关度量,可以添加 Spring-boot-starter-actuator 作为项目依赖项。然后,默认情况下,只要属性`spring.cloud.gateway.metrics.enabled`未设置为`false`,网关指标过滤器就会运行。该过滤器添加了一个名为`spring.cloud.gateway.requests`的计时器度量,并带有以下标记: + +* `routeId`:路径 ID。 + +* `routeUri`:将 API 路由到的 URI。 + +* `outcome`:结果,按[HttpStatus.series](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/http/HttpStatus.Series.html)分类。 + +* `status`:返回给客户机的请求的 HTTP 状态。 + +* `httpStatusCode`:返回给客户机的请求的 HTTP 状态。 + +* `httpMethod`:用于请求的 HTTP 方法。 + +此外,通过属性`spring.cloud.gateway.metrics.tags.path.enabled`(默认情况下,设置为 false),你可以使用标记激活一个额外的度量指标: + +* `path`:请求的路径。 + +然后可以从`/actuator/metrics/spring.cloud.gateway.requests`中获取这些指标,并且可以轻松地与 Prometheus 集成以创建[Grafana](images/gateway-grafana-dashboard.jpeg)[dashboard](gateway-grafana-dashboard.json)。 + +| |要启用 Prometheus 端点,请添加`micrometer-registry-prometheus`作为项目依赖项。| +|---|------------------------------------------------------------------------------------------------| + +### [](#marking-an-exchange-as-routed)[7.9.将交易所标记为路由](#marking-an-exchange-as-routed) ### + +在网关路由了`ServerWebExchange`之后,它通过在 exchange 属性中添加`gatewayAlreadyRouted`将该 exchange 标记为“路由”。一旦一个请求被标记为路由,其他路由过滤器将不会再次路由该请求,基本上跳过该过滤器。有一些方便的方法,你可以使用它来标记一个交换为路由,或者检查一个交换是否已经被路由。 + +* `ServerWebExchangeUtils.isAlreadyRouted`接受一个`ServerWebExchange`对象,并检查它是否已被“路由”。 + +* `ServerWebExchangeUtils.setAlreadyRouted`接受一个`ServerWebExchange`对象,并将其标记为“路由”。 + +[](#httpheadersfilters)[8.HttpHeadersFilters](#httpheadersfilters) +---------- + +HttpHeadersFilters 在向下游发送请求之前应用于请求,例如在`NettyRoutingFilter`中。 + +### [](#forwarded-headers-filter)[8.1.转发头过滤器](#forwarded-headers-filter) ### + +`Forwarded`headers filter 创建一个`Forwarded`header 以发送到下游服务。它将当前请求的`Host`头、方案和端口添加到任何现有的`Forwarded`头。 + +### [](#removehopbyhop-headers-filter)[8.2.RemoveHopbyHop Headers 过滤器](#removehopbyhop-headers-filter) ### + +`RemoveHopByHop`headers 过滤器从转发的请求中删除 header。被删除的头的默认列表来自[IETF](https://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-14#section-7.1.3)。 + +默认删除的标题是: + +* 连接 + +* 保持活力 + +* 代理身份验证 + +* 代理授权 + +* TE + +* 预告片 + +* 传输编码 + +* 升级 + +要更改这一点,请将`spring.cloud.gateway.filter.remove-hop-by-hop.headers`属性设置为要删除的头名称列表。 + +### [](#xforwarded-headers-filter)[8.3.XForwarded Headers 过滤器](#xforwarded-headers-filter) ### + +`XForwarded`headers filter 创建了各种`X-Forwarded-*`headers 以发送到下游服务。它使用`Host`报头、方案、端口和当前请求的路径来创建各种报头。 + +可以通过以下布尔属性(默认为 true)来控制单个标题的创建: + +* `spring.cloud.gateway.x-forwarded.for-enabled` + +* `spring.cloud.gateway.x-forwarded.host-enabled` + +* `spring.cloud.gateway.x-forwarded.port-enabled` + +* `spring.cloud.gateway.x-forwarded.proto-enabled` + +* `spring.cloud.gateway.x-forwarded.prefix-enabled` + +追加多个头可以由以下布尔属性控制(默认为 true): + +* `spring.cloud.gateway.x-forwarded.for-append` + +* `spring.cloud.gateway.x-forwarded.host-append` + +* `spring.cloud.gateway.x-forwarded.port-append` + +* `spring.cloud.gateway.x-forwarded.proto-append` + +* `spring.cloud.gateway.x-forwarded.prefix-append` + +[](#tls-and-ssl)[9. TLS and SSL](#tls-and-ssl) +---------- + +网关可以通过遵循通常的 Spring 服务器配置来监听 HTTPS 上的请求。下面的示例展示了如何做到这一点: + +示例 61.application.yml + +``` +server: + ssl: + enabled: true + key-alias: scg + key-store-password: scg1234 + key-store: classpath:scg-keystore.p12 + key-store-type: PKCS12 +``` + +你可以将网关路由到 HTTP 和 HTTPS 后端。如果要路由到 HTTPS 后端,则可以通过以下配置将网关配置为信任所有下游证书: + +示例 62.application.yml + +``` +spring: + cloud: + gateway: + httpclient: + ssl: + useInsecureTrustManager: true +``` + +使用不安全的信任管理器不适合于生产。对于产品部署,你可以使用一组已知证书来配置网关,它可以通过以下配置来信任这些证书: + +示例 63.application.yml + +``` +spring: + cloud: + gateway: + httpclient: + ssl: + trustedX509Certificates: + - cert1.pem + - cert2.pem +``` + +如果 Spring 云网关没有提供受信任的证书,则使用默认的信任存储区(你可以通过设置`javax.net.ssl.trustStore`系统属性来覆盖该存储区)。 + +### [](#tls-handshake)[9.1.TLS 握手](#tls-handshake) ### + +网关维护一个用于路由到后端的客户机池。当通过 HTTPS 进行通信时,客户机发起 TLS 握手。与此握手相关的超时次数很多。你可以配置这些超时可以配置(默认显示)如下: + +示例 64.application.yml + +``` +spring: + cloud: + gateway: + httpclient: + ssl: + handshake-timeout-millis: 10000 + close-notify-flush-timeout-millis: 3000 + close-notify-read-timeout-millis: 0 +``` + +[](#configuration)[10.配置](#configuration) +---------- + +Spring 云网关的配置由`RouteDefinitionLocator`实例的集合驱动。下面的清单显示了`RouteDefinitionLocator`接口的定义: + +例 65。RouteDefinitionLocator.java + +``` +public interface RouteDefinitionLocator { + Flux getRouteDefinitions(); +} +``` + +默认情况下,`PropertiesRouteDefinitionLocator`通过使用 Spring boot 的`@ConfigurationProperties`机制加载属性。 + +前面的配置示例都使用了一个快捷方式,它使用位置参数而不是命名参数。以下两个例子是等价的: + +示例 66.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: setstatus_route + uri: https://example.org + filters: + - name: SetStatus + args: + status: 401 + - id: setstatusshortcut_route + uri: https://example.org + filters: + - SetStatus=401 +``` + +对于网关的某些用法,属性是足够的,但是一些生产用例受益于从外部源(例如数据库)加载配置。未来的里程碑版本将具有基于 Spring 数据存储库的`RouteDefinitionLocator`实现,例如 Redis、MongoDB 和 Cassandra。 + +### [](#routedefinition-metrics)[10.1.路由定义度量](#routedefinition-metrics) ### + +要启用`RouteDefinition`度量,请添加 Spring-boot-starter-actuator 作为项目依赖项。然后,默认情况下,只要将属性`spring.cloud.gateway.metrics.enabled`设置为`true`,度量就可用。将添加一个名为`spring.cloud.gateway.routes.count`的规范度量,其值是`RouteDefinitions`的数量。该指标将从`/actuator/metrics/spring.cloud.gateway.routes.count`开始提供。 + +[](#route-metadata-configuration)[11.路由元数据配置](#route-metadata-configuration) +---------- + +你可以通过使用元数据为每个路由配置附加参数,如下所示: + +示例 67.application.yml + +``` +spring: + cloud: + gateway: + routes: + - id: route_with_metadata + uri: https://example.org + metadata: + optionName: "OptionValue" + compositeObject: + name: "value" + iAmNumber: 1 +``` + +你可以从 Exchange 获取所有元数据属性,如下所示: + +``` +Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR); +// get all metadata properties +route.getMetadata(); +// get a single metadata property +route.getMetadata(someKey); +``` + +[](#http-timeouts-configuration)[12.HTTP 超时配置](#http-timeouts-configuration) +---------- + +可以为所有路由配置 HTTP 超时(响应和连接),并为每个特定的路由重写。 + +### [](#global-timeouts)[12.1.全球超时](#global-timeouts) ### + +要配置全局 HTTP 超时:`connect-timeout`必须以毫秒为单位指定。`response-timeout`必须指定为 java.time.duration + +全局 HTTP 超时示例 + +``` +spring: + cloud: + gateway: + httpclient: + connect-timeout: 1000 + response-timeout: 5s +``` + +### [](#per-route-timeouts)[12.2.每条路线超时](#per-route-timeouts) ### + +要配置每个路由超时:`connect-timeout`必须以毫秒为单位指定。`response-timeout`必须以毫秒为单位指定。 + +通过配置进行每路由 HTTP 超时配置 + +``` + - id: per_route_timeouts + uri: https://example.org + predicates: + - name: Path + args: + pattern: /delay/{timeout} + metadata: + response-timeout: 200 + connect-timeout: 200 +``` + +使用 Java DSL 的每路由超时配置 + +``` +import static org.springframework.cloud.gateway.support.RouteMetadataUtils.CONNECT_TIMEOUT_ATTR; +import static org.springframework.cloud.gateway.support.RouteMetadataUtils.RESPONSE_TIMEOUT_ATTR; + + @Bean + public RouteLocator customRouteLocator(RouteLocatorBuilder routeBuilder){ + return routeBuilder.routes() + .route("test1", r -> { + return r.host("*.somehost.org").and().path("/somepath") + .filters(f -> f.addRequestHeader("header1", "header-value-1")) + .uri("http://someuri") + .metadata(RESPONSE_TIMEOUT_ATTR, 200) + .metadata(CONNECT_TIMEOUT_ATTR, 200); + }) + .build(); + } +``` + +带负值的每路由`response-timeout`将禁用全局`response-timeout`值。 + +``` + - id: per_route_timeouts + uri: https://example.org + predicates: + - name: Path + args: + pattern: /delay/{timeout} + metadata: + response-timeout: -1 +``` + +### [](#fluent-java-routes-api)[12.3.Fluent Java Routes API](#fluent-java-routes-api) ### + +为了允许在 Java 中进行简单的配置,`RouteLocatorBuilder` Bean 包含了一个 Fluent API。下面的清单展示了它的工作原理: + +例 68。GatewaySampleApplication.java + +``` +// static imports from GatewayFilters and RoutePredicates +@Bean +public RouteLocator customRouteLocator(RouteLocatorBuilder builder, ThrottleGatewayFilterFactory throttle) { + return builder.routes() + .route(r -> r.host("**.abc.org").and().path("/image/png") + .filters(f -> + f.addResponseHeader("X-TestHeader", "foobar")) + .uri("http://httpbin.org:80") + ) + .route(r -> r.path("/image/webp") + .filters(f -> + f.addResponseHeader("X-AnotherHeader", "baz")) + .uri("http://httpbin.org:80") + .metadata("key", "value") + ) + .route(r -> r.order(-1) + .host("**.throttle.org").and().path("/get") + .filters(f -> f.filter(throttle.apply(1, + 1, + 10, + TimeUnit.SECONDS))) + .uri("http://httpbin.org:80") + .metadata("key", "value") + ) + .build(); +} +``` + +这种样式还允许更多的自定义谓词断言。由`RouteDefinitionLocator`bean 定义的谓词使用逻辑`and`进行组合。通过使用 Fluent Java API,你可以在`Predicate`类上使用`and()`、`or()`和`negate()`运算符。 + +### [](#the-discoveryclient-route-definition-locator)[12.4. The `DiscoveryClient` Route Definition Locator](#the-discoveryclient-route-definition-locator) ### + +你可以将网关配置为基于在`DiscoveryClient`兼容的服务注册中心注册的服务创建路由。 + +要启用此功能,请设置`spring.cloud.gateway.discovery.locator.enabled=true`,并确保在 Classpath 上启用了`DiscoveryClient`实现(例如 Netflix Eureka、Consul 或 ZooKeeper)。 + +#### [](#configuring-predicates-and-filters-for-discoveryclient-routes)[12.4.1. Configuring Predicates and Filters For `DiscoveryClient` Routes](#configuring-predicates-and-filters-for-discoveryclient-routes) #### + +默认情况下,网关为使用`DiscoveryClient`创建的路由定义一个谓词和过滤器。 + +缺省谓词是用模式`/serviceId/**`定义的路径谓词,其中`serviceId`是来自`DiscoveryClient`的服务的 ID。 + +默认的过滤器是一个重写路径过滤器,regex`/serviceId/?(?.*)`和替换`/${remaining}`。这将在向下游发送请求之前从路径中剥离服务 ID。 + +如果要自定义`DiscoveryClient`路由所使用的谓词或筛选器,请设置`spring.cloud.gateway.discovery.locator.predicates[x]`和`spring.cloud.gateway.discovery.locator.filters[y]`。这样做时,如果你想保留该功能,则需要确保包含前面显示的缺省谓词和筛选器。下面的示例显示了这是什么样子的: + +示例 69.application.properties + +``` +spring.cloud.gateway.discovery.locator.predicates[0].name: Path +spring.cloud.gateway.discovery.locator.predicates[0].args[pattern]: "'/'+serviceId+'/**'" +spring.cloud.gateway.discovery.locator.predicates[1].name: Host +spring.cloud.gateway.discovery.locator.predicates[1].args[pattern]: "'**.foo.com'" +spring.cloud.gateway.discovery.locator.filters[0].name: CircuitBreaker +spring.cloud.gateway.discovery.locator.filters[0].args[name]: serviceId +spring.cloud.gateway.discovery.locator.filters[1].name: RewritePath +spring.cloud.gateway.discovery.locator.filters[1].args[regexp]: "'/' + serviceId + '/?(?.*)'" +spring.cloud.gateway.discovery.locator.filters[1].args[replacement]: "'/${remaining}'" +``` + +[](#reactor-netty-access-logs)[13.反应堆网络访问日志](#reactor-netty-access-logs) +---------- + +要启用反应堆网络访问日志,请设置`-Dreactor.netty.http.server.accessLogEnabled=true`。 + +| |它必须是一个 Java 系统属性,而不是 Spring 引导属性。| +|---|--------------------------------------------------------------| + +你可以将日志系统配置为具有一个单独的访问日志文件。下面的示例创建了一个注销配置: + +示例 70.logback.xml + +``` + + access_log.log + + %msg%n + + + + + + + + + +``` + +[](#cors-configuration)[14.CORS 配置](#cors-configuration) +---------- + +你可以配置网关来控制 CORS 行为。“全局”CORS 配置是 URL 模式到[Spring Framework `CorsConfiguration`](https://docs.spring.io/spring/docs/5.0.x/javadoc-api/org/springframework/web/cors/CorsConfiguration.html)的映射。以下示例配置 CORS: + +示例 71.application.yml + +``` +spring: + cloud: + gateway: + globalcors: + cors-configurations: + '[/**]': + allowedOrigins: "https://docs.spring.io" + allowedMethods: + - GET +``` + +在前面的示例中,对于所有 GET 请求的路径,允许从源自`docs.spring.io`的请求发出 CORS 请求。 + +要为某些网关路由谓词不处理的请求提供相同的 CORS 配置,请将`spring.cloud.gateway.globalcors.add-to-simple-url-handler-mapping`属性设置为`true`。当你尝试支持 CORS 的 Preflight 请求,而你的路由谓词不求值到`true`时,这是有用的,因为 HTTP 方法是`options`。 + +[](#actuator-api)[15.执行机构 API](#actuator-api) +---------- + +执行器端点`/gateway`允许你监视 Spring 云网关应用程序并与之交互。要实现远程访问,端点必须是应用程序属性中的[enabled](https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-endpoints.html#production-ready-endpoints-enabling-endpoints)和[通过 HTTP 或 JMX 公开](https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-endpoints.html#production-ready-endpoints-exposing-endpoints)。下面的清单展示了如何做到这一点: + +示例 72.application.properties + +``` +management.endpoint.gateway.enabled=true # default value +management.endpoints.web.exposure.include=gateway +``` + +### [](#verbose-actuator-format)[15.1.详细执行器格式](#verbose-actuator-format) ### + +一种新的、更详细的格式已添加到 Spring Cloud Gateway 中。它为每条路由添加了更多细节,允许你查看与每条路由相关的谓词和筛选器,以及可用的任何配置。以下示例配置`/actuator/gateway/routes`: + +``` +[ + { + "predicate": "(Hosts: [**.addrequestheader.org] && Paths: [/headers], match trailing slash: true)", + "route_id": "add_request_header_test", + "filters": [ + "[[AddResponseHeader X-Response-Default-Foo = 'Default-Bar'], order = 1]", + "[[AddRequestHeader X-Request-Foo = 'Bar'], order = 1]", + "[[PrefixPath prefix = '/httpbin'], order = 2]" + ], + "uri": "lb://testservice", + "order": 0 + } +] +``` + +默认情况下启用此功能。要禁用它,请设置以下属性: + +示例 73.application.properties + +``` +spring.cloud.gateway.actuator.verbose.enabled=false +``` + +在将来的版本中,这将默认为`true`。 + +### [](#retrieving-route-filters)[15.2.检索路由过滤器](#retrieving-route-filters) ### + +本节详细介绍了如何检索路由过滤器,包括: + +* [Global Filters](#gateway-global-filters) + +* [[gateway-route-filters]](#gateway-route-filters) + +#### [](#gateway-global-filters)[15.2.1.全局过滤器](#gateway-global-filters) #### + +要检索应用于所有路由的[global filters](#global-filters),请对`GET`请求`/actuator/gateway/globalfilters`。由此产生的反应类似于以下情况: + +``` +{ + "org.spring[email protected]77856cc5": 10100, + "o[email protected]4f6fd101": 10000, + "or[email protected]32d22650": -1, + "[email protected]6459d9": 2147483647, + "[email protected]5e0": 2147483647, + "[email protected]d23": 0, + "org.s[email protected]135064ea": 2147483637, + "[email protected]23c05889": 2147483646 +} +``` + +响应包含已到位的全局过滤器的详细信息。对于每个全局过滤器,有一个字符串表示过滤器对象(例如,`org.spring[[email protected]](/cdn-cgi/l/email-protection)77856cc5`)和相应的`get()`在过滤器链中。} + +#### [](#gateway-route-filters)[15.2.2.路由过滤器](#gateway-route-filters) #### + +要检索应用于路由的[“GatewayFilter”工厂](#gatewayfilter-factories),请对`GET`请求`/actuator/gateway/routefilters`。由此产生的反应类似于以下情况: + +``` +{ + "[[email protected] configClass = AbstractNameValueGatewayFilterFactory.NameValueConfig]": null, + "[[email protected] configClass = Object]": null, + "[[email protected] configClass = Object]": null +} +``` + +响应包含应用于任何特定路径的`GatewayFilter`工厂的详细信息。对于每个工厂,都有一个对应对象的字符串表示(例如,`[[[email protected]](/cdn-cgi/l/email-protection) configClass = Object]`)。请注意,`null`值是由于端点控制器的实现不完整造成的,因为它试图设置过滤器链中对象的顺序,这不适用于`GatewayFilter`工厂对象。 + +### [](#refreshing-the-route-cache)[15.3.刷新路径缓存](#refreshing-the-route-cache) ### + +要清除路由缓存,请对`POST`请求`/actuator/gateway/refresh`。该请求返回一个没有响应体的 200。 + +### [](#retrieving-the-routes-defined-in-the-gateway)[15.4.检索在网关中定义的路由](#retrieving-the-routes-defined-in-the-gateway) ### + +要检索在网关中定义的路由,请对`GET`请求`/actuator/gateway/routes`。由此产生的反应类似于以下情况: + +``` +[{ + "route_id": "first_route", + "route_object": { + "predicate": "org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory$$Lambda$432/[email protected]", + "filters": [ + "OrderedGatewayFilter{delegate=org.springframework.cloud.gateway.filter.factory.PreserveHostHeaderGatewayFilterFactory$$Lambda$436/[email protected], order=0}" + ] + }, + "order": 0 +}, +{ + "route_id": "second_route", + "route_object": { + "predicate": "org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory$$Lambda$432/[email protected]", + "filters": [] + }, + "order": 0 +}] +``` + +响应包含网关中定义的所有路由的详细信息。下表描述了响应的每个元素(每个都是路由)的结构: + +| Path | Type |说明| +|------------------------|------|-------------------------------------------------------------------------------| +| `route_id` |String|路线 ID。| +|`route_object.predicate`|Object|路线谓词。| +| `route_object.filters` |Array |将[“GatewayFilter”工厂](#gatewayfilter-factories)应用于该路线。| +| `order` |Number|路线顺序。| + +### [](#gateway-retrieving-information-about-a-particular-route)[15.5.检索有关特定路线的信息](#gateway-retrieving-information-about-a-particular-route) ### + +要检索有关单个路由的信息,请对`GET`请求`/actuator/gateway/routes/{id}`(例如,`/actuator/gateway/routes/first_route`)。由此产生的反应类似于以下情况: + +``` +{ + "id": "first_route", + "predicates": [{ + "name": "Path", + "args": {"_genkey_0":"/first"} + }], + "filters": [], + "uri": "https://www.uri-destination.org", + "order": 0 +} +``` + +下表描述了响应的结构: + +| Path | Type |说明| +|------------|------|------------------------------------------------------------------------------------------------------| +| `id` |String|路线 ID。| +|`predicates`|Array |路由谓词的集合。每个项定义给定谓词的名称和参数。| +| `filters` |Array |应用于路由的过滤器的集合。| +| `uri` |String|路线的目的地 URI。| +| `order` |Number|路线顺序。| + +### [](#creating-and-deleting-a-particular-route)[15.6.创建和删除特定的路由](#creating-and-deleting-a-particular-route) ### + +要创建路由,请使用指定路由字段的 JSON 主体对`POST`请求`/gateway/routes/{id_route_to_create}`(参见)。 + +要删除一个路由,请对`DELETE`请求`/gateway/routes/{id_route_to_delete}`。 + +### [](#recap-the-list-of-all-endpoints)[15.7.回顾:所有端点的列表](#recap-the-list-of-all-endpoints) ### + +下面的 FolloiWNG 表总结了 Spring 云网关执行器端点(请注意,每个端点都以`/actuator/gateway`作为基本路径): + +| ID |HTTP Method|说明| +|---------------|-----------|-----------------------------------------------------------------------------| +|`globalfilters`| GET |显示应用于路由的全局筛选器列表。| +|`routefilters` | GET |显示应用于特定路径的`GatewayFilter`工厂的列表。| +| `refresh` | POST |清除路由缓存。| +| `routes` | GET |显示在网关中定义的路由列表。| +| `routes/{id}` | GET |显示有关特定路线的信息。| +| `routes/{id}` | POST |为网关添加了一条新的路径。| +| `routes/{id}` | DELETE |从网关删除现有的路由。| + +### [](#sharing-routes-between-multiple-gateway-instances)[15.8.在多个网关实例之间共享路由](#sharing-routes-between-multiple-gateway-instances) ### + +Spring 云网关提供了两种`RouteDefinitionRepository`实现方式。第一个是“InmemoryRouteDefinitionRepository”,它只存在于一个网关实例的内存中。这种类型的存储库不适合跨多个网关实例填充路由。 + +为了跨 Spring 个云网关实例的集群共享路由,可以使用`RedisRouteDefinitionRepository`。要启用这种存储库,以下属性必须设置为 true:`spring.cloud.gateway.redis-route-definition-repository.enabled`同样,对于 RedisrateLimiter 过滤器工厂,它需要使用 Spring-boot-starter-data-redis-active Spring boot starter。 + +[](#troubleshooting)[16.故障排除](#troubleshooting) +---------- + +本节介绍了使用 Spring 云网关时可能出现的常见问题。 + +### [](#log-levels)[16.1.日志级别](#log-levels) ### + +以下记录器可能在`DEBUG`和`TRACE`级别包含有价值的故障排除信息: + +* `org.springframework.cloud.gateway` + +* `org.springframework.http.server.reactive` + +* `org.springframework.web.reactive` + +* `org.springframework.boot.autoconfigure.web` + +* `reactor.netty` + +* `redisratelimiter` + +### [](#wiretap)[16.2. Wiretap](#wiretap) ### + +反应堆网络`HttpClient`和`HttpServer`可以启用窃听功能。当与将`reactor.netty`日志级别设置为`DEBUG`或`HttpServer`相结合时,它启用了对信息的日志记录,例如通过连接发送和接收的标题和主体。要启用窃听,请分别为`HttpServer`和设置`spring.cloud.gateway.httpclient.wiretap=true`。 + +[](#developer-guide)[17.开发者指南](#developer-guide) +---------- + +这些是编写网关的一些自定义组件的基本指南。 + +### [](#writing-custom-route-predicate-factories)[17.1.编写自定义路由谓词工厂](#writing-custom-route-predicate-factories) ### + +为了编写路由谓词,你需要将`RoutePredicateFactory`实现为 Bean。有一个名为`AbstractRoutePredicateFactory`的抽象类,你可以对其进行扩展。 + +MyRoutepredicateFactory.java + +``` +@Component +public class MyRoutePredicateFactory extends AbstractRoutePredicateFactory { + + public MyRoutePredicateFactory() { + super(Config.class); + } + + @Override + public Predicate apply(Config config) { + // grab configuration from Config object + return exchange -> { + //grab the request + ServerHttpRequest request = exchange.getRequest(); + //take information from the request to see if it + //matches configuration. + return matches(config, request); + }; + } + + public static class Config { + //Put the configuration properties for your filter here + } + +} +``` + +### [](#writing-custom-gatewayfilter-factories)[17.2.编写自定义网关过滤器工厂](#writing-custom-gatewayfilter-factories) ### + +要编写`GatewayFilter`,你必须将`GatewayFilterFactory`实现为 Bean。你可以扩展一个名为`AbstractGatewayFilterFactory`的抽象类。下面的例子说明了如何做到这一点: + +例 74。Pregatewayfilterfactory.java + +``` +@Component +public class PreGatewayFilterFactory extends AbstractGatewayFilterFactory { + + public PreGatewayFilterFactory() { + super(Config.class); + } + + @Override + public GatewayFilter apply(Config config) { + // grab configuration from Config object + return (exchange, chain) -> { + //If you want to build a "pre" filter you need to manipulate the + //request before calling chain.filter + ServerHttpRequest.Builder builder = exchange.getRequest().mutate(); + //use builder to manipulate the request + return chain.filter(exchange.mutate().request(builder.build()).build()); + }; + } + + public static class Config { + //Put the configuration properties for your filter here + } + +} +``` + +Postgatewayfilterfactory.java + +``` +@Component +public class PostGatewayFilterFactory extends AbstractGatewayFilterFactory { + + public PostGatewayFilterFactory() { + super(Config.class); + } + + @Override + public GatewayFilter apply(Config config) { + // grab configuration from Config object + return (exchange, chain) -> { + return chain.filter(exchange).then(Mono.fromRunnable(() -> { + ServerHttpResponse response = exchange.getResponse(); + //Manipulate the response in some way + })); + }; + } + + public static class Config { + //Put the configuration properties for your filter here + } + +} +``` + +#### [](#naming-custom-filters-and-references-in-configuration)[17.2.1.在配置中命名自定义过滤器和引用](#naming-custom-filters-and-references-in-configuration) #### + +自定义过滤器类名称应该以`GatewayFilterFactory`结尾。 + +例如,要在配置文件中引用名为`Something`的过滤器,该过滤器必须位于名为[](#log-levels)的类中。 + +| |可以创建一个没有“gatewayfilterfactory”后缀的网关过滤器,例如`class AnotherThing`。这个过滤器可以在配置文件中被`AnotherThing`引用为`AnotherThing`。这是**不是**所支持的命名
约定,该语法可能会在未来的版本中删除。请更新过滤器
名称以使其兼容。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#writing-custom-global-filters)[17.3.编写自定义全局过滤器](#writing-custom-global-filters) ### + +要编写自定义的全局过滤器,你必须将[](#writing-custom-global-filters)接口实现为 Bean。这将过滤器应用于所有请求。 + +以下示例分别展示了如何设置全局 pre 和 post 过滤器: + +``` +@Bean +public GlobalFilter customGlobalFilter() { + return (exchange, chain) -> exchange.getPrincipal() + .map(Principal::getName) + .defaultIfEmpty("Default User") + .map(userName -> { + //adds header to proxied request + exchange.getRequest().mutate().header("CUSTOM-REQUEST-HEADER", userName).build(); + return exchange; + }) + .flatMap(chain::filter); +} + +@Bean +public GlobalFilter customGlobalPostFilter() { + return (exchange, chain) -> chain.filter(exchange) + .then(Mono.just(exchange)) + .map(serverWebExchange -> { + //adds header to response + serverWebExchange.getResponse().getHeaders().set("CUSTOM-RESPONSE-HEADER", + HttpStatus.OK.equals(serverWebExchange.getResponse().getStatusCode()) ? "It worked": "It did not work"); + return serverWebExchange; + }) + .then(); +} +``` + +[](#building-a-simple-gateway-by-using-spring-mvc-or-webflux)[18. Building a Simple Gateway by Using Spring MVC or Webflux](#building-a-simple-gateway-by-using-spring-mvc-or-webflux) +---------- + +| |下面描述了一种替代样式的网关。以前的文件都不适用于下面的内容。| +|---|--------------------------------------------------------------------------------------------------------------| + +Spring 云网关提供了一种名为`ProxyExchange`的实用工具对象。你可以在常规的 Spring Web 处理程序中使用它作为方法参数。它通过镜像 HTTP 动词的方法支持基本的下游 HTTP 交换。对于 MVC,它还支持通过`forward()`方法转发到本地处理程序。要使用`ProxyExchange`,在 Classpath 中包含正确的模块(`spring-cloud-gateway-mvc`或`spring-cloud-gateway-webflux`)。 + +下面的 MVC 示例将请求代理到`/test`下游的远程服务器: + +``` +@RestController +@SpringBootApplication +public class GatewaySampleApplication { + + @Value("${remote.home}") + private URI home; + + @GetMapping("/test") + public ResponseEntity proxy(ProxyExchange proxy) throws Exception { + return proxy.uri(home.toString() + "/image/png").get(); + } + +} +``` + +下面的示例对 WebFlux 做了相同的处理: + +``` +@RestController +@SpringBootApplication +public class GatewaySampleApplication { + + @Value("${remote.home}") + private URI home; + + @GetMapping("/test") + public Mono> proxy(ProxyExchange proxy) throws Exception { + return proxy.uri(home.toString() + "/image/png").get(); + } + +} +``` + +`ProxyExchange`上的便利方法使处理程序方法能够发现并增强传入请求的 URI 路径。例如,你可能希望提取路径的尾随元素,以便向下游传递它们: + +``` +@GetMapping("/proxy/path/**") +public ResponseEntity proxyPath(ProxyExchange proxy) throws Exception { + String path = proxy.path("/proxy/path/"); + return proxy.uri(home.toString() + "/foos/" + path).get(); +} +``` + +Spring MVC 和 WebFlux 的所有特性都可用于网关处理程序方法。因此,例如,你可以插入请求头和查询参数,并且可以通过映射注释中的声明来约束传入的请求。有关这些特性的更多详细信息,请参见 Spring MVC 中`@RequestMapping`的文档。 + +你可以使用`ProxyExchange`上的`header()`方法向下游响应添加标题。 + +你还可以通过向`get()`方法(和其他方法)添加一个映射器来操作响应头(以及响应中喜欢的任何其他方法)。映射器是一个`ResponseEntity`,它接收传入的`ResponseEntity`并将其转换为传出的。 + +为“敏感”头(默认情况下,`cookie`和`authorization`)和“代理”头(x-forwarded-*`)提供了一流的支持。 + +[](#configuration-properties)[19.配置属性](#configuration-properties) +---------- + +要查看所有 Spring 云网关相关配置属性的列表,请参见[the appendix](appendix.html)。 + +如果{{{i[’GoogleAnalyticsObject’]=r;i[r]=i[r]|function(){q=i[r].push(参数)},i[r].l=1\*new date();a=s.createElement(o),m=s.getelementsbyName(0);a.parentsName(1);a.A.SRC=g;m.M.analytnode(gua,m.com.com);(google=document=’,’,’’’’’’’’’’’),’documents’,’’’.’’’’’’’’’’,’’’ \ No newline at end of file diff --git a/docs/spring-cloud/spring-cloud-kubernetes.md b/docs/spring-cloud/spring-cloud-kubernetes.md new file mode 100644 index 0000000000000000000000000000000000000000..c31943e0e54a0329b0766c44adbb93aba968ecc5 --- /dev/null +++ b/docs/spring-cloud/spring-cloud-kubernetes.md @@ -0,0 +1,1743 @@ +Spring 云 Kubernetes +========== + + +本参考指南介绍了如何使用 Spring Cloud Kubernetes。 + +[](#why-do-you-need-spring-cloud-kubernetes)[1. Why do you need Spring Cloud Kubernetes?](#why-do-you-need-spring-cloud-kubernetes) +---------- + +Spring Cloud Kubernetes 提供了众所周知的 Spring 云接口的实现,允许开发人员在 Kubernetes 上构建和运行 Spring 云应用程序。虽然这个项目在构建云原生应用程序时可能对你很有用,但在 Kubernetes 上部署 Spring 启动应用程序也不是必需的。如果你刚刚开始在 Kubernetes 上运行你的启动应用程序,你只需要一个基本的启动应用程序和 Kubernetes 本身就可以完成很多事情。要了解更多信息,你可以从阅读[Spring Boot reference documentation for deploying to Kubernetes ](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#cloud-deployment-kubernetes)开始,还可以阅读研讨会材料[Spring 和 Kubernetes](https://hackmd.io/@ryanjbaxter/spring-on-k8s-workshop)。 + +[](#starters)[2. Starters](#starters) +---------- + +启动器是可以包含在应用程序中的方便的依赖关系描述符。包括一个启动器,以获得依赖关系和 Spring 引导自动配置的功能集.以`spring-cloud-starter-kubernetes-fabric8`开头的启动器使用[Fabric8Kubernetes Java 客户端](https://github.com/fabric8io/kubernetes-client)提供实现。以“ Spring-cloud-starter-kubernetes-client”开头的启动器使用[Kubernetes Java 客户端](https://github.com/kubernetes-client/java)提供实现。 + +|起动器| Features | +|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|fabric8dependency

``
org.springframework.cloud
Spring-cloud-starter-kubernetes-gt8<>
<><89"/><90"/"><<<<<resolves service names to Kubernetes Services. | +|fabric8dependency

``
org.springframework.cloud<111"/>
Spring-cloud-starter-kubernetes-gt-fabric8-config<114"/>
<><><115r="117"/><<<"><“>”>“client=”client=“><<119"><<<<<|Load application properties from Kubernetes[ConfigMaps](#configmap-propertysource) and [Secrets](#secrets-propertysource).[Reload](#propertysource-reload) application properties when a ConfigMap or
Secret changes.| +|fabric8dependency


org.springframework.cloud
<<145"/><”><<<“>”><<<<“><<<<<| All Spring Cloud Kubernetes features. | + +[](#discoveryclient-for-kubernetes)[3.Kubernetes 的发现](#discoveryclient-for-kubernetes) +---------- + +此项目为[Kubernetes](https://kubernetes.io)提供了[发现客户端](https://github.com/spring-cloud/spring-cloud-commons/blob/master/spring-cloud-commons/src/main/java/org/springframework/cloud/client/discovery/DiscoveryClient.java)的实现。这个客户机允许你按名称查询 Kubernetes 端点(参见[services](https://kubernetes.io/docs/user-guide/services/))。Kubernetes API 服务器通常将服务公开为表示`http`和`https`地址的端点集合,客户端可以从以 POD 形式运行的 Spring 启动应用程序访问这些端点。 + +这是通过在项目中添加以下依赖项而免费获得的: + +基于 http`DiscoveryClient` + +``` + + org.springframework.cloud + spring-cloud-starter-kubernetes-discoveryclient + +``` + +| |`spring-cloud-starter-kubernetes-discoveryclient`被设计为与[Spring Cloud Kubernetes DiscoveryServer](#spring-cloud-kubernetes-discoveryserver)一起使用。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +Fabric8Kubernetes 客户端 + +``` + + org.springframework.cloud + spring-cloud-starter-kubernetes-fabric8 + +``` + +Kubernetes Java 客户端 + +``` + + org.springframework.cloud + spring-cloud-starter-kubernetes-client + +``` + +要启用`DiscoveryClient`的加载,请将`@EnableDiscoveryClient`添加到相应的配置或应用程序类中,如下例所示: + +``` +@SpringBootApplication +@EnableDiscoveryClient +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} +``` + +然后,你可以通过自动布线将客户机插入到代码中,如下例所示: + +``` +@Autowired +private DiscoveryClient discoveryClient; +``` + +通过在`application.properties`中设置以下属性,可以从所有名称空间中选择启用`DiscoveryClient`: + +``` +spring.cloud.kubernetes.discovery.all-namespaces=true +``` + +要发现未被 Kubernetes API 服务器标记为“ready”的服务端点地址,可以在`application.properties`(默认:false)中设置以下属性: + +``` +spring.cloud.kubernetes.discovery.include-not-ready-addresses=true +``` + +| |在为监视目的发现服务时,这可能是有用的,并且将允许检查未准备好的服务实例的`/health`端点。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------| + +如果你的服务公开了多个端口,那么你将需要指定`DiscoveryClient`应该使用哪个端口。`DiscoveryClient`将使用以下逻辑选择端口。 + +1. 如果服务有一个标签`primary-port-name`,那么它将使用在标签的值中指定名称的端口。 + +2. 如果不存在标签,则将使用`spring.cloud.kubernetes.discovery.primary-port-name`中指定的端口号。 + +3. 如果上面两个都没有指定,它将使用名为`https`的端口。 + +4. 如果上述条件都不满足,它将使用名为`http`的端口。 + +5. 作为最后的手段,它 WIL 在港口列表中选择第一个港口。 + +| |最后一个选项可能会导致非确定性行为。
请确保相应地配置你的服务和/或应用程序。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------| + +默认情况下,所有端口及其名称都将添加到`ServiceInstance`的元数据中。 + +如果出于任何原因,需要禁用`DiscoveryClient`,则可以在`application.properties`中设置以下属性: + +``` +spring.cloud.kubernetes.discovery.enabled=false +``` + +Spring 一些云组件使用`DiscoveryClient`以便获得有关本地服务实例的信息。为此,你需要将 Kubernetes 服务名称与`spring.application.name`属性对齐。 + +| |`spring.application.name`对于在 Kubernetes 中为应用程序注册的名称没有任何效力| +|---|-----------------------------------------------------------------------------------------------------------| + +Spring Cloud Kubernetes 还可以监视 Kubernetes 服务目录中的更改,并相应地更新 `DiscoveryClient’实现。为了启用此功能,你需要在应用程序中的配置类中添加“@enablescheduling”。 + +[](#kubernetes-native-service-discovery)[4.Kubernetes 原生服务发现](#kubernetes-native-service-discovery) +---------- + +Kubernetes 本身能够(服务器端)发现服务(参见:[Kubernetes.io/DOCS/概念/服务-网络/服务/# 发现-服务](https://kubernetes.io/docs/concepts/services-networking/service/#discovering-services))。使用本机 Kubernetes 服务发现可以确保与其他工具的兼容性,例如 Istio([istio.io](https://istio.io)),这是一种能够实现负载平衡、断路器、故障转移等功能的服务网格。 + +然后,调用方服务只需要引用在特定的 Kubernetes 集群中可解析的名称。一个简单的实现可以使用一个 Spring `RestTemplate`,它引用一个完全限定的域名,例如`[{service-name}.{namespace}.svc.{cluster}.local:{service-port}](https://{service-name}.{namespace}.svc.{cluster}.local:{service-port})`。 + +此外,你可以将 Hystrix 用于: + +* 在调用方实现断路器,通过用`@EnableCircuitBreaker`注释 Spring 引导应用程序类 + +* 回退功能,通过使用`@HystrixCommand(fallbackMethod=`注释相应的方法 + +[](#kubernetes-propertysource-implementations)[5.Kubernetes PropertySource 实现](#kubernetes-propertysource-implementations) +---------- + +配置 Spring 启动应用程序的最常见方法是创建`application.properties`或`application.yaml`或`application-profile.properties`或`application-profile.yaml`文件,该文件包含为应用程序或 Spring 启动程序提供定制值的键值对。你可以通过指定系统属性或环境变量来覆盖这些属性。 + +### [](#configmap-propertysource)[5.1. Using a `ConfigMap` `PropertySource`](#configmap-propertysource) ### + +Kubernetes 提供了一个名为[`ConfigMap`](https://kubernetes.io/docs/user-guide/configmap/)的资源,以外部化以键值对的形式传递给应用程序的参数,或嵌入`application.properties`或`application.yaml`文件。[Spring Cloud Kubernetes Config](https://github.com/spring-cloud/spring-cloud-kubernetes/tree/master/spring-cloud-kubernetes-fabric8-config)项目使 Kubernetes`ConfigMap`实例在应用程序引导过程中可用,并在观察到的`ConfigMap`实例上检测到更改时触发 bean 或 Spring 上下文的热重载。 + +默认的行为是基于一个 Kubernetes`Fabric8ConfigMapPropertySource`创建一个`Fabric8ConfigMapPropertySource`,它具有一个`metadata.name`值,该值要么是你的 Spring 应用程序的名称(由其`spring.application.name`属性定义),要么是在“bootstrap.properties”文件中定义的自定义名称,位于以下键:`spring.cloud.kubernetes.config.name`下。 + +然而,在可以使用多个`ConfigMap`实例的情况下,可以进行更高级的配置。`spring.cloud.kubernetes.config.sources`列表使这成为可能。例如,你可以定义以下`ConfigMap`实例: + +``` +spring: + application: + name: cloud-k8s-app + cloud: + kubernetes: + config: + name: default-name + namespace: default-namespace + sources: + # Spring Cloud Kubernetes looks up a ConfigMap named c1 in namespace default-namespace + - name: c1 + # Spring Cloud Kubernetes looks up a ConfigMap named default-name in whatever namespace n2 + - namespace: n2 + # Spring Cloud Kubernetes looks up a ConfigMap named c3 in namespace n3 + - namespace: n3 + name: c3 +``` + +在前面的示例中,如果没有设置`spring.cloud.kubernetes.config.namespace`,则将在应用程序运行的名称空间中查找名为`ConfigMap`的`ConfigMap`。请参阅[名称空间解析](#namespace-resolution),以更好地了解如何解析应用程序的名称空间。 + +找到的任何匹配的`ConfigMap`将按以下方式进行处理: + +* 应用单独的配置属性。 + +* 将任何名为`application.yaml`的属性的内容应用为`yaml`。 + +* 将任何名为`application.properties`的属性的内容作为属性文件应用。 + +上述流的一个例外是,当`ConfigMap`包含一个**单身**键时,该键指示该文件是 YAML 或 Properties 文件。在这种情况下,键的名称不必是`application.yaml`或 `application.properties’(它可以是任何东西),并且正确地处理了该属性的值。这个特性促进了`ConfigMap`是通过使用如下内容创建的用例: + +``` +kubectl create configmap game-config --from-file=/path/to/app-config.yaml +``` + +假设我们有一个名为`demo`的 Spring 引导应用程序,该应用程序使用以下属性来读取其线程池配置。 + +* `pool.size.core` + +* `pool.size.maximum` + +这可以外部化为`yaml`格式的配置映射,如下所示: + +``` +kind: ConfigMap +apiVersion: v1 +metadata: + name: demo +data: + pool.size.core: 1 + pool.size.max: 16 +``` + +在大多数情况下,单个属性都可以正常工作。然而,有时,嵌入式`yaml`更方便。在这种情况下,我们使用一个名为`application.yaml`的属性嵌入我们的`yaml`,如下所示: + +``` +kind: ConfigMap +apiVersion: v1 +metadata: + name: demo +data: + application.yaml: |- + pool: + size: + core: 1 + max:16 +``` + +下面的示例也有效: + +``` +kind: ConfigMap +apiVersion: v1 +metadata: + name: demo +data: + custom-name.yaml: |- + pool: + size: + core: 1 + max:16 +``` + +你还可以根据读取`ConfigMap`时合并在一起的活动配置文件来不同地配置 Spring 引导应用程序。可以通过使用“application.properties”或`application.yaml`属性为不同的配置文件提供不同的属性值,指定配置文件特定的值,每个值都在各自的文档中(由`---`序列指示),如下所示: + +``` +kind: ConfigMap +apiVersion: v1 +metadata: + name: demo +data: + application.yml: |- + greeting: + message: Say Hello to the World + farewell: + message: Say Goodbye + --- + spring: + profiles: development + greeting: + message: Say Hello to the Developers + farewell: + message: Say Goodbye to the Developers + --- + spring: + profiles: production + greeting: + message: Say Hello to the Ops +``` + +在前一种情况下,使用`development`配置文件加载到 Spring 应用程序中的配置如下: + +``` + greeting: + message: Say Hello to the Developers + farewell: + message: Say Goodbye to the Developers +``` + +但是,如果`production`配置文件是活动的,那么配置将变为: + +``` + greeting: + message: Say Hello to the Ops + farewell: + message: Say Goodbye +``` + +如果这两个配置文件都是活动的,则在`ConfigMap`中最后出现的属性将覆盖前面的任何值。 + +另一种选择是为每个配置文件创建不同的配置映射,并且 Spring 启动将基于活动配置文件自动获取它 + +``` +kind: ConfigMap +apiVersion: v1 +metadata: + name: demo +data: + application.yml: |- + greeting: + message: Say Hello to the World + farewell: + message: Say Goodbye +``` + +``` +kind: ConfigMap +apiVersion: v1 +metadata: + name: demo-development +data: + application.yml: |- + spring: + profiles: development + greeting: + message: Say Hello to the Developers + farewell: + message: Say Goodbye to the Developers +``` + +``` +kind: ConfigMap +apiVersion: v1 +metadata: + name: demo-production +data: + application.yml: |- + spring: + profiles: production + greeting: + message: Say Hello to the Ops + farewell: + message: Say Goodbye +``` + +要告诉 Spring 在引导时应该启用哪个`profile`,可以传递`SPRING_PROFILES_ACTIVE`环境变量。为此,你可以使用一个环境变量启动你的 Spring 引导应用程序,你可以在容器规范的 PODSpec 中定义该环境变量。部署资源文件,如下所示: + +``` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: deployment-name + labels: + app: deployment-name +spec: + replicas: 1 + selector: + matchLabels: + app: deployment-name + template: + metadata: + labels: + app: deployment-name + spec: + containers: + - name: container-name + image: your-image + env: + - name: SPRING_PROFILES_ACTIVE + value: "development" +``` + +你可能会遇到这样的情况,即有多个配置映射具有相同的属性名。例如: + +``` +kind: ConfigMap +apiVersion: v1 +metadata: + name: config-map-one +data: + application.yml: |- + greeting: + message: Say Hello from one +``` + +and + +``` +kind: ConfigMap +apiVersion: v1 +metadata: + name: config-map-two +data: + application.yml: |- + greeting: + message: Say Hello from two +``` + +根据在`bootstrap.yaml|properties`中放置这些参数的顺序,你可能最终会得到一个未预期的结果(最后一个配置映射获胜)。例如: + +``` +spring: + application: + name: cloud-k8s-app + cloud: + kubernetes: + config: + namespace: default-namespace + sources: + - name: config-map-two + - name: config-map-one +``` + +将导致属性`greetings.message`为`Say Hello from one`。 + +有一种方法可以通过指定`useNameAsPrefix`来更改此默认配置。例如: + +``` +spring: + application: + name: with-prefix + cloud: + kubernetes: + config: + useNameAsPrefix: true + namespace: default-namespace + sources: + - name: config-map-one + useNameAsPrefix: false + - name: config-map-two +``` + +这样的配置将生成两个属性: + +* `greetings.message`等于`Say Hello from one`。 + +* `config-map-two.greetings.message`等于`Say Hello from two` + +注意,`spring.cloud.kubernetes.config.useNameAsPrefix`比`spring.cloud.kubernetes.config.sources.useNameAsPrefix`具有*下层*的优先权。这允许你为所有源设置一个“默认”策略,同时只允许覆盖少数几个源。 + +如果使用配置映射名称不是一个选项,则可以指定一个不同的策略,称为:`explicitPrefix`。由于这是你选择的*显式*前缀,因此它只能提供给`sources`级别。同时它具有比`useNameAsPrefix`更高的优先级。假设我们有第三个配置映射,它包含以下条目: + +``` +kind: ConfigMap +apiVersion: v1 +metadata: + name: config-map-three +data: + application.yml: |- + greeting: + message: Say Hello from three +``` + +如下所示的配置: + +``` +spring: + application: + name: with-prefix + cloud: + kubernetes: + config: + useNameAsPrefix: true + namespace: default-namespace + sources: + - name: config-map-one + useNameAsPrefix: false + - name: config-map-two + explicitPrefix: two + - name: config-map-three +``` + +将生成三个属性: + +* `greetings.message`等于`Say Hello from one`。 + +* `two.greetings.message`等于`Say Hello from two`。 + +* `config-map-three.greetings.message`等于`Say Hello from three`。 + +默认情况下,除了读取`sources`配置中指定的配置映射外, Spring 还将尝试从“配置文件感知”源读取所有属性。最简单的解释方法是通过一个例子。让我们假设你的应用程序启用了一个名为“dev”的配置文件,并且你的配置如下所示: + +``` +spring: + application: + name: spring-k8s + cloud: + kubernetes: + config: + namespace: default-namespace + sources: + - name: config-map-one +``` + +除了读取`config-map-one`外, Spring 还将尝试按此特定顺序读取`config-map-one-dev`;。每个活动配置文件生成这样的配置文件感知配置映射。 + +虽然你的应用程序不应该受到这样的配置映射的影响,但如果需要,可以禁用它: + +``` +spring: + application: + name: spring-k8s + cloud: + kubernetes: + config: + includeProfileSpecificSources: false + namespace: default-namespace + sources: + - name: config-map-one + includeProfileSpecificSources: false +``` + +注意,和前面一样,你可以在两个级别中指定此属性:对于所有配置映射或对于单个配置映射;后者具有更高的优先级。 + +| |你应该检查安全配置部分。要从 POD 内部访问配置映射,你需要拥有正确的
Kubernetes 服务帐户、角色和角色绑定。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +使用`ConfigMap`实例的另一种选择是,通过运行 Spring Cloud Kubernetes 应用程序并让 Spring Cloud Kubernetes 从文件系统中读取它们,将它们安装到 POD 中。此行为由`spring.cloud.kubernetes.config.paths`属性控制。你可以在前面描述的机制之外使用它,也可以使用它来代替它。通过使用`,`分隔符,可以在`spring.cloud.kubernetes.config.paths`中指定多个(精确的)文件路径。 + +| |你必须为每个属性文件提供完整的精确路径,因为目录不是递归解析的。| +|---|--------------------------------------------------------------------------------------------------------------------| + +| |如果使用`spring.cloud.kubernetes.config.paths`或`spring.cloud.kubernetes.secrets.path`,则自动重新加载
功能将无法工作。你需要向`/actuator/refresh`端点或
重新启动/重新部署应用程序发出`POST`请求。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在某些情况下,你的应用程序可能无法使用 Kubernetes API 加载某些`ConfigMaps`。如果在这种情况下希望你的应用程序在启动过程中失败,可以设置 ` Spring.cloud.kubernetes.config.fail-fast=true`,以使应用程序启动过程中出现异常失败。 + +你还可以使你的应用程序在出现故障时重试加载`ConfigMap`属性源。首先,需要设置`spring.cloud.kubernetes.config.fail-fast=true`。然后你需要在 Classpath 中添加`spring-retry`和`spring-boot-starter-aop`。你可以通过设置“ Spring.cloud.kubernetes.config.retry.*”属性来配置重试属性,如最大尝试次数、退避选项(如初始间隔、乘数、最大间隔)。 + +| |如果由于某种原因在 Classpath 上已经有`spring-retry`和`spring-boot-starter-aop`
并且希望启用 fail-fast,但是不希望被启用重试;你可以通过设置`spring.cloud.kubernetes.config.retry.enabled=false`来禁用的重试。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| Name | Type | Default |说明| +|-------------------------------------------------------|---------|----------------------------|-----------------------------------------------------------------------------------------------------| +| `spring.cloud.kubernetes.config.enabled` |`Boolean`| `true` |启用配置图`PropertySource`| +| `spring.cloud.kubernetes.config.name` |`String` |`${spring.application.name}`|设置`ConfigMap`的名称以查找| +| `spring.cloud.kubernetes.config.namespace` |`String` | Client namespace |设置要查找的 Kubernetes 名称空间| +| `spring.cloud.kubernetes.config.paths` | `List` | `null` |设置安装`ConfigMap`实例的路径| +| `spring.cloud.kubernetes.config.enableApi` |`Boolean`| `true` |通过 API 启用或禁用使用`ConfigMap`实例| +| `spring.cloud.kubernetes.config.fail-fast` |`Boolean`| `false` |当加载`ConfigMap`时发生错误时,启用或禁用失败的应用程序启动| +| `spring.cloud.kubernetes.config.retry.enabled` |`Boolean`| `true` |启用或禁用配置重试。| +|`spring.cloud.kubernetes.config.retry.initial-interval`| `Long` | `1000` |初始重试间隔(以毫秒为单位)。| +| `spring.cloud.kubernetes.config.retry.max-attempts` |`Integer`| `6` |最大尝试次数。| +| `spring.cloud.kubernetes.config.retry.max-interval` | `Long` | `2000` |退场的最大间隔。| +| `spring.cloud.kubernetes.config.retry.multiplier` |`Double` | `1.1` |下一个区间的乘数。| + +### [](#secrets-propertysource)[5.2.秘密 PropertySource](#secrets-propertysource) ### + +Kubernetes 有[Secrets](https://kubernetes.io/docs/concepts/configuration/secret/)的概念,用于存储诸如密码、OAuth 令牌等敏感数据。该项目提供了与`Secrets`的集成,以使 Spring 引导应用程序能够访问秘密。你可以通过设置`spring.cloud.kubernetes.secrets.enabled`属性显式地启用或禁用此功能。 + +启用后,`Fabric8SecretsPropertySource`将从以下来源查找`Secrets`的 Kubernetes: + +1. 从秘密挂载中递归地读取 + +2. 以应用程序命名(由`spring.application.name`定义) + +3. 匹配一些标签 + +**注:** + +默认情况下,出于安全原因,通过 API(以上第 2 点和第 3 点)**未启用**消耗秘密。Secrets 上的权限“List”允许客户端检查指定名称空间中的 Secrets 值。此外,我们建议容器通过安装的卷共享秘密。 + +如果你允许通过 API 使用机密,我们建议你使用授权策略(例如 RBAC)来限制对机密的访问。有关通过 API 使用秘密时的风险和最佳实践的更多信息,请参阅[this doc](https://kubernetes.io/docs/concepts/configuration/secret/#best-practices)。 + +如果发现了这些秘密,它们的数据就会提供给应用程序。 + +假设我们有一个名为`demo`的 Spring 引导应用程序,该应用程序使用属性来读取其数据库配置。我们可以使用以下命令创建一个 Kubernetes 秘密: + +``` +kubectl create secret generic db-secret --from-literal=username=user --from-literal=password=p455w0rd +``` + +前面的命令将创建以下秘密(你可以使用`kubectl get secrets db-secret -o yaml`查看该秘密): + +``` +apiVersion: v1 +data: + password: cDQ1NXcwcmQ= + username: dXNlcg== +kind: Secret +metadata: + creationTimestamp: 2017-07-04T09:15:57Z + name: db-secret + namespace: default + resourceVersion: "357496" + selfLink: /api/v1/namespaces/default/secrets/db-secret + uid: 63c89263-6099-11e7-b3da-76d6186905a8 +type: Opaque +``` + +请注意,该数据包含由`create`命令提供的基本 64 编码版本的文字。 + +然后,你的应用程序可以使用这个秘密——例如,通过将秘密的值导出为环境变量: + +``` +apiVersion: v1 +kind: Deployment +metadata: + name: ${project.artifactId} +spec: + template: + spec: + containers: + - env: + - name: DB_USERNAME + valueFrom: + secretKeyRef: + name: db-secret + key: username + - name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: db-secret + key: password +``` + +你可以通过多种方式选择要使用的秘密: + +1. 通过列出映射秘密的目录: + + ``` + -Dspring.cloud.kubernetes.secrets.paths=/etc/secrets/db-secret,etc/secrets/postgresql + ``` + + 如果你将所有的秘密映射到一个公共根,则可以将它们设置为: + + ``` + -Dspring.cloud.kubernetes.secrets.paths=/etc/secrets + ``` + +2. 通过设置一个命名的秘密: + + ``` + -Dspring.cloud.kubernetes.secrets.name=db-secret + ``` + +3. 通过定义标签列表: + + ``` + -Dspring.cloud.kubernetes.secrets.labels.broker=activemq + -Dspring.cloud.kubernetes.secrets.labels.db=postgresql + ``` + +与`ConfigMap`的情况一样,在可以使用多个`Secret`实例的情况下,也可以进行更高级的配置。`spring.cloud.kubernetes.secrets.sources`列表使这成为可能。例如,你可以定义以下`Secret`实例: + +``` +spring: + application: + name: cloud-k8s-app + cloud: + kubernetes: + secrets: + name: default-name + namespace: default-namespace + sources: + # Spring Cloud Kubernetes looks up a Secret named s1 in namespace default-namespace + - name: s1 + # Spring Cloud Kubernetes looks up a Secret named default-name in namespace n2 + - namespace: n2 + # Spring Cloud Kubernetes looks up a Secret named s3 in namespace n3 + - namespace: n3 + name: s3 +``` + +在前面的示例中,如果没有设置`spring.cloud.kubernetes.secrets.namespace`,则将在应用程序运行的名称空间中查找名为`Secret`的`s1`。请参阅[名称空间分辨率](#namespace-resolution),以更好地了解如何解析应用程序的名称空间。 + +[Similar to the `ConfigMaps`](#config-map-fail-fast);如果你希望你的应用程序在无法加载`Secrets`属性源时无法启动,则可以设置`spring.cloud.kubernetes.secrets.fail-fast=true`。 + +也可以为`Secret`属性源[like the `ConfigMaps`](#config-map-retry)启用重试。与`ConfigMap`属性源一样,首先需要设置`spring.cloud.kubernetes.secrets.fail-fast=true`。然后你需要在 Classpath 中添加`spring-retry`和`spring-boot-starter-aop`。可以通过设置`spring.cloud.kubernetes.secrets.retry.*`属性来配置`Secret`属性源的重试行为。 + +| |如果在 Classpath 上已经有`spring-retry`和`spring-boot-starter-aop`由于某种原因
并希望启用 fail-fast,但不希望被启用重试;你可以通过设置`spring.cloud.kubernetes.secrets.retry.enabled=false`来禁用`PropertySources`的重试。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| Name | Type | Default |说明| +|--------------------------------------------------------|---------|----------------------------|--------------------------------------------------------------------------------------------------| +| `spring.cloud.kubernetes.secrets.enabled` |`Boolean`| `true` |启用秘密`PropertySource`| +| `spring.cloud.kubernetes.secrets.name` |`String` |`${spring.application.name}`|设置要查找的秘密的名称| +| `spring.cloud.kubernetes.secrets.namespace` |`String` | Client namespace |将 Kubernetes 名称空间设置为在何处查找| +| `spring.cloud.kubernetes.secrets.labels` | `Map` | `null` |设置用于查找秘密的标签| +| `spring.cloud.kubernetes.secrets.paths` | `List` | `null` |设置安装秘密的路径(示例 1)| +| `spring.cloud.kubernetes.secrets.enableApi` |`Boolean`| `false` |通过 API 启用或禁用消耗秘密(示例 2 和 3)| +| `spring.cloud.kubernetes.secrets.fail-fast` |`Boolean`| `false` |当加载`Secret`时发生错误时,启用或禁用失败的应用程序启动| +| `spring.cloud.kubernetes.secrets.retry.enabled` |`Boolean`| `true` |启用或禁用秘密重试。| +|`spring.cloud.kubernetes.secrets.retry.initial-interval`| `Long` | `1000` |初始重试间隔(以毫秒为单位)。| +| `spring.cloud.kubernetes.secrets.retry.max-attempts` |`Integer`| `6` |最大尝试次数。| +| `spring.cloud.kubernetes.secrets.retry.max-interval` | `Long` | `2000` |退场的最大间隔。| +| `spring.cloud.kubernetes.secrets.retry.multiplier` |`Double` | `1.1` |下一个区间的乘数。| + +注: + +* `spring.cloud.kubernetes.secrets.labels`属性的行为由[基于地图的绑定](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-Configuration-Binding#map-based-binding)定义。 + +* `spring.cloud.kubernetes.secrets.paths`属性的行为由[基于集合的绑定](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-Configuration-Binding#collection-based-binding)定义。 + +* 出于安全原因,通过 API 访问机密可能会受到限制。最好的办法是把秘密装在吊舱里。 + +你可以在[spring-boot-camel-config](https://github.com/fabric8-quickstarts/spring-boot-camel-config)找到一个使用秘密的应用程序示例(尽管它尚未更新为使用新的`spring-cloud-kubernetes`项目) + +### [](#namespace-resolution)[5.3.名称空间解析](#namespace-resolution) ### + +查找应用程序名称空间是在尽力而为的基础上进行的。为了找到它,我们迭代了一些步骤。最简单也是最常见的一种方法是在适当的配置中指定它,例如: + +``` +spring: + application: + name: app + cloud: + kubernetes: + secrets: + name: secret + namespace: default + sources: + # Spring Cloud Kubernetes looks up a Secret named 'a' in namespace 'default' + - name: a + # Spring Cloud Kubernetes looks up a Secret named 'secret' in namespace 'b' + - namespace: b + # Spring Cloud Kubernetes looks up a Secret named 'd' in namespace 'c' + - namespace: c + name: d +``` + +请记住,配置映射也可以这样做。如果没有指定这样的命名空间,则将读取它(按此顺序): + +1. 来自 property`spring.cloud.kubernetes.client.namespace` + +2. 来自驻留在由`spring.cloud.kubernetes.client.serviceAccountNamespacePath`属性表示的文件中的字符串 + +3. 来自驻留在`/var/run/secrets/kubernetes.io/serviceaccount/namespace`文件中的字符串(Kubernetes 缺省名称空间路径) + +4. 从指定的客户端方法调用(例如 Fabric8 的:`KubernetesClient::getNamespace`),如果客户端提供了这样的方法。这又可以通过环境属性进行配置。例如,可以通过“Kubernetes\_Namespace”属性配置 Fabric8 客户机;有关详细信息,请参阅客户机文档。 + +未能从上述步骤中找到名称空间将导致引发异常。 + +### [](#propertysource-reload)[5.4. `PropertySource` Reload](#propertysource-reload) ### + +| |该功能在 2020.0 版本中已被弃用。请参阅
的[Spring Cloud Kubernetes Configuration Watcher](#spring-cloud-kubernetes-configuration-watcher)控制器,以获得与
相同的功能的替代方式。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +一些应用程序可能需要检测外部属性源上的更改,并更新其内部状态以反映新的配置。 Spring Cloud Kubernetes 的重新加载特性能够在相关的`ConfigMap`或 `secret’发生变化时触发应用程序重新加载。 + +默认情况下,此功能将被禁用。你可以通过使用`spring.cloud.kubernetes.reload.enabled=true`配置属性(例如,在`application.properties`文件中)来启用它。 + +支持以下级别的重新加载(通过设置`spring.cloud.kubernetes.reload.strategy`属性): + +* `refresh`(缺省):只有注解为`@ConfigurationProperties`或`@RefreshScope`的配置 bean 才会重新加载。这个重新加载级别利用了 Spring 云上下文的刷新特性。 + +* `restart_context`:整个 Spring `ApplicationContext`被优雅地重新启动。使用新配置重新创建 bean。为了使 Restart 上下文功能正常工作,你必须启用并公开 Restart Actuator 端点 + +``` +management: + endpoint: + restart: + enabled: true + endpoints: + web: + exposure: + include: restart +``` + +* `shutdown`:关闭 Spring `ApplicationContext`以激活容器的重新启动。使用此级别时,请确保所有非守护进程线程的生命周期都绑定到`ApplicationContext`,并且配置了复制控制器或复制集来重新启动 POD。 + +假设使用默认设置(“刷新”模式)启用了 Reload 功能,那么在配置映射发生更改时将刷新以下 Bean: + +``` +@Configuration +@ConfigurationProperties(prefix = "bean") +public class MyConfig { + + private String message = "a message that can be changed live"; + + // getter and setters + +} +``` + +为了看到更改有效地发生,你可以创建另一个定期打印消息的 Bean,如下所示 + +``` +@Component +public class MyBean { + + @Autowired + private MyConfig config; + + @Scheduled(fixedDelay = 5000) + public void hello() { + System.out.println("The message is: " + config.getMessage()); + } +} +``` + +可以使用`ConfigMap`更改应用程序打印的消息,如下所示: + +``` +apiVersion: v1 +kind: ConfigMap +metadata: + name: reload-example +data: + application.properties: |- + bean.message=Hello World! +``` + +与 POD 相关的`ConfigMap`中名为`bean.message`的属性的任何更改都会反映在输出中。更一般地说,与前缀为`@ConfigurationProperties`注释的`prefix`字段所定义的值的属性相关的更改将被检测并反映在应用程序中。[Associating a `ConfigMap` with a pod](#configmap-propertysource)在本章的前面进行了解释。 + +完整示例可在[`spring-cloud-kubernetes-reload-example`](https://github.com/spring-cloud/spring-cloud-kubernetes/tree/main/spring-cloud-kubernetes-examples/kubernetes-reload-example)中找到。 + +Reload 功能支持两种操作模式:\* 事件(默认):通过使用 Kubernetes API(Web 套接字)监视配置映射或秘密中的更改。任何事件都会产生对配置的重新检查,如果发生更改,还会产生重新加载。为了监听配置映射更改,服务帐户上的`view`角色是必需的。机密需要更高级别的角色(例如`edit`)(默认情况下,机密不受监视)。\* 轮询:周期性地从配置映射和秘密中重新创建配置,以查看它是否已更改。可以使用`spring.cloud.kubernetes.reload.period`属性配置轮询周期,默认设置为 15 秒。它需要与受监视的属性源具有相同的角色。这意味着,例如,在文件挂载的秘密源上使用轮询不需要特定的特权。 + +| Name | Type | Default |说明| +|-------------------------------------------------------|----------|---------|--------------------------------------------------------------------------------------| +| `spring.cloud.kubernetes.reload.enabled` |`Boolean` | `false` |启用对属性源和配置重新加载的监视| +|`spring.cloud.kubernetes.reload.monitoring-config-maps`|`Boolean` | `true` |允许监视配置映射中的更改| +| `spring.cloud.kubernetes.reload.monitoring-secrets` |`Boolean` | `false` |允许监视机密的更改| +| `spring.cloud.kubernetes.reload.strategy` | `Enum` |`refresh`|触发重新加载时使用的策略(`refresh’,`restart_context`,或`shutdown`)| +| `spring.cloud.kubernetes.reload.mode` | `Enum` | `event` |指定如何侦听属性源中的更改(`event’或`polling`)| +| `spring.cloud.kubernetes.reload.period` |`Duration`| `15s` |使用`polling`策略时验证更改的周期| + +注意:\* 你不应该在配置映射或秘密中使用`spring.cloud.kubernetes.reload`下的属性。在运行时更改这些属性可能会导致意想不到的结果。\* 当你使用`refresh`级别时,删除属性或整个配置映射不会恢复 bean 的原始状态。 + +[](#kubernetes-ecosystem-awareness)[6.Kubernetes 生态系统意识](#kubernetes-ecosystem-awareness) +---------- + +无论你的应用程序是否在 Kubernetes 内部运行,本指南前面描述的所有功能都同样有效。这对于开发和故障排除非常有帮助。从开发的角度来看,这允许你启动 Spring 引导应用程序,并调试该项目中的一个模块。你不需要在 Kubernetes 中部署它,因为项目的代码依赖于[Fabric8Kubernetes Java 客户端](https://github.com/fabric8io/kubernetes-client),这是一种 Fluent DSL,可以通过使用`http`协议与 Kubernetes 服务器的 REST API 进行通信。 + +要禁用与 Kubernetes 的集成,你可以将`spring.cloud.kubernetes.enabled`设置为`false`。请注意,当`spring-cloud-kubernetes-config`在 Classpath 上时,` Spring.cloud.kubernetes.enabled` 应设置在`bootstrap.{properties|yml}`(或配置文件指定的一个)中,否则应设置在`application.{properties|yml}`(或配置文件指定的一个)中。由于我们在`spring-cloud-kubernetes-config`中设置特定的`EnvironmentPostProcessor`的方式,你还需要通过系统属性(或环境变量)禁用该处理器,例如,你可以通过`-DSPRING_CLOUD_KUBERNETES_ENABLED=false`启动应用程序(任何形式的放松绑定也可以工作)。还要注意这些属性:`spring.cloud.kubernetes.config.enabled`和`spring.cloud.kubernetes.secrets.enabled`仅在`bootstrap.{properties|yml}`中设置时生效 + +### [](#kubernetes-profile-autoconfiguration)[6.1.Kubernetes 配置文件自动配置](#kubernetes-profile-autoconfiguration) ### + +当应用程序在 Kubernetes 中作为 POD 运行时,一个名为`kubernetes`的 Spring 配置文件会自动被激活。这允许你定制配置,以定义在 Spring 引导应用程序部署在 Kubernetes 平台中时应用的 bean(例如,不同的开发和生产配置)。 + +### [](#istio-awareness)[6.2.Istio 意识](#istio-awareness) ### + +当在应用程序 Classpath 中包含`spring-cloud-kubernetes-fabric8-istio`模块时,将向应用程序添加一个新的配置文件,前提是该应用程序在安装了[Istio](https://istio.io)的 Kubernetes 集群中运行。然后可以在 bean 和`@Configuration`类中使用 Spring `@Profile("istio")`注释。 + +Istio 感知模块使用`me.snowdrop:istio-client`与 Istio API 进行交互,让我们发现交通规则、断路器等等,使得我们的 Spring 引导应用程序很容易消耗这些数据来根据环境动态配置自己。 + +[](#pod-health-indicator)[7.POD 健康指示器](#pod-health-indicator) +---------- + +Spring 引导使用[` 健康指示器 `](https://github.com/spring-projects/spring-boot/blob/master/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/health/HealthEndpoint.java)公开有关应用程序健康状况的信息。这使得它对于向用户公开与健康相关的信息非常有用,并且非常适合作为[准备状态调查](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/)使用。 + +Kubernetes 健康指示器(它是核心模块的一部分)公开了以下信息: + +* POD 名称、IP 地址、命名空间、服务帐户、节点名称及其 IP 地址 + +* 指示 Spring 引导应用程序是 Kubernetes 的内部还是外部的标志 + +你可以通过在`application.[properties | yaml]`中将`management.health.kubernetes.enabled`设置为`false`来禁用此`HealthContributor`。 + +[](#info-contributor)[8.信息贡献者](#info-contributor) +---------- + +Spring Cloud Kubernetes 包括一个`InfoContributor`,它将 POD 信息添加到 Spring Boot 的`/info`Acturator 端点。 + +你可以通过在`application.[properties | yaml]`中将`management.info.kubernetes.enabled`设置为`false`来禁用此`InfoContributor`。 + +[](#leader-election)[9.领导人选举](#leader-election) +---------- + +Spring 云 Kubernetes 领导人选举机制使用 Kubernetes 配置图实现 Spring 集成的领导人选举 API。 + +多个应用程序实例竞争领导权,但领导权只授予一个实例。当授予领导力时,领导者应用程序将接收带有领导力`OnGrantedEvent`的`Context`应用程序事件。应用程序会定期尝试获得领导力,并将领导力授予第一个呼叫者。领导者将一直是领导者,直到被从集群中移除,或者交出领导者。当领导层被撤职时,前一位领导人将收到`OnRevokedEvent`应用程序事件。删除后,集群中的任何实例都可能成为新的领导者,包括旧的领导者。 + +要将它包含在项目中,请添加以下依赖项。 + +Fabric8Leader 实现 + +``` + + org.springframework.cloud + spring-cloud-kubernetes-fabric8-leader + +``` + +要指定用于领导者选举的配置图的名称,请使用以下属性。 + +``` +spring.cloud.kubernetes.leader.config-map-name=leader +``` + +[](#loadbalancer-for-kubernetes)[10.Kubernetes 负载平衡器](#loadbalancer-for-kubernetes) +---------- + +Spring 该项目包括用于基于 Kubernetes 端点的负载平衡的云负载平衡器和提供基于 Kubernetes 服务的负载平衡器的实现。要将它包含到项目中,请添加以下依赖项。 + +Fabric8 的实现 + +``` + + org.springframework.cloud + spring-cloud-starter-kubernetes-fabric8-loadbalancer + +``` + +Kubernetes Java 客户端实现 + +``` + + org.springframework.cloud + spring-cloud-starter-kubernetes-client-loadbalancer + +``` + +要启用基于 Kubernetes 服务名称的负载平衡,请使用以下属性。然后负载均衡器将尝试使用地址调用应用程序,例如`service-a.default.svc.cluster.local` + +``` +spring.cloud.kubernetes.loadbalancer.mode=SERVICE +``` + +要启用跨所有名称空间的负载平衡,请使用以下属性。来自`spring-cloud-kubernetes-discovery`模块的属性是受尊重的。 + +``` +spring.cloud.kubernetes.discovery.all-namespaces=true +``` + +如果需要通过 HTTPS 访问服务,则需要在服务定义中添加名称`secured`和值`true`的标签或注释,然后负载均衡器将使用 HTTPS 向服务发出请求。 + +[](#security-configurations-inside-kubernetes)[11.Kubernetes 内部的安全配置](#security-configurations-inside-kubernetes) +---------- + +### [](#namespace)[11.1. Namespace](#namespace) ### + +这个项目中提供的大多数组件都需要知道名称空间。对于 Kubernetes(1.3+),命名空间作为服务帐户秘密的一部分提供给 POD,并由客户端自动检测。对于早期版本,需要将其指定为 POD 的环境变量。实现这一点的快速方法如下: + +``` + env: + - name: "KUBERNETES_NAMESPACE" + valueFrom: + fieldRef: + fieldPath: "metadata.namespace" +``` + +### [](#service-account)[11.2.服务帐户](#service-account) ### + +对于支持集群中更细粒度的基于角色的访问的 Kubernetes 发行版,你需要确保使用`spring-cloud-kubernetes`运行的 POD 能够访问 Kubernetes API。对于分配给部署或 POD 的任何服务帐户,你需要确保它们具有正确的角色。 + +根据需求,你将需要`get`、`list`和`watch`对以下资源的权限: + +|依赖性| Resources | +|----------------------------------------------|-------------------------| +|Spring-cloud-starter-kubernetes-fabric8|pods, services, endpoints| +|Spring-cloud-starter-kubernetes-fabric8-config| configmaps, secrets | +|Spring-cloud-starter-kubernetes-client|pods, services, endpoints| +|Spring-cloud-starter-kubernetes-client-config| configmaps, secrets | + +出于开发目的,你可以将`cluster-reader`权限添加到你的`default`服务帐户。在生产系统中,你可能希望提供更细粒度的权限。 + +下面的 Role 和 Rolebinding 是`default`帐户的名称空间权限示例: + +``` +kind: Role +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + namespace: YOUR-NAME-SPACE + name: namespace-reader +rules: + - apiGroups: [""] + resources: ["configmaps", "pods", "services", "endpoints", "secrets"] + verbs: ["get", "list", "watch"] + +--- + +kind: RoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: namespace-reader-binding + namespace: YOUR-NAME-SPACE +subjects: +- kind: ServiceAccount + name: default + apiGroup: "" +roleRef: + kind: Role + name: namespace-reader + apiGroup: "" +``` + +[](#service-registry-implementation)[12.服务注册中心实现](#service-registry-implementation) +---------- + +在 Kubernetes 服务注册是由平台控制的情况下,应用程序本身并不像在其他平台中那样控制注册。因此,使用`spring.cloud.service-registry.auto-registration.enabled`或设置`@EnableDiscoveryClient(autoRegister=false)`将不会在 Spring Cloud Kubernetes 中产生任何影响。 + +[](#spring-cloud-kubernetes-configuration-watcher)[13. Spring Cloud Kubernetes Configuration Watcher](#spring-cloud-kubernetes-configuration-watcher) +---------- + +Kubernetes 提供了在应用程序的容器中[将配置图或秘密挂载为卷](https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#add-configmap-data-to-a-volume)的功能。当配置图或秘密的内容发生变化时,[安装的卷将会随着这些变化而更新。](https://kubernetes.io/docs/tasks/configure-pod-container/configure-pod-configmap/#mounted-configmaps-are-updated-automatically)。 + +然而, Spring 启动不会自动更新这些更改,除非重新启动应用程序。 Spring 云提供了在不重新启动应用程序的情况下刷新应用程序上下文的能力,方法是通过点击执行器端点`/refresh`或通过使用 Spring 云总线发布`RefreshRemoteApplicationEvent`。 + +要实现在 Kubernetes 上运行的 Spring 云应用程序的这种配置刷新,你可以将 Spring Cloud Kubernetes Configuration Watcher 控制器部署到你的 Kubernetes 集群中。 + +该应用程序作为容器发布,并在[Docker Hub](https://hub.docker.com/r/springcloud/spring-cloud-kubernetes-configuration-watcher)上可用。 + +Spring 云 Kubernetes 配置观察者可以通过两种方式向应用程序发送刷新通知。 + +1. 在 HTTP 上,在这种情况下,被通知的应用程序必须将`/refresh`执行器端点公开并可从集群内访问 + +2. Spring 使用云总线,在这种情况下,你将需要部署到你的 Custer 的消息代理,以便应用程序使用。 + +### [](#deployment-yaml)[13.1.部署 YAML](#deployment-yaml) ### + +下面是一个示例部署 YAML,你可以使用它将 Kubernetes 配置监视器部署到 Kubernetes。 + +``` +--- +apiVersion: v1 +kind: List +items: + - apiVersion: v1 + kind: Service + metadata: + labels: + app: spring-cloud-kubernetes-configuration-watcher + name: spring-cloud-kubernetes-configuration-watcher + spec: + ports: + - name: http + port: 8888 + targetPort: 8888 + selector: + app: spring-cloud-kubernetes-configuration-watcher + type: ClusterIP + - apiVersion: v1 + kind: ServiceAccount + metadata: + labels: + app: spring-cloud-kubernetes-configuration-watcher + name: spring-cloud-kubernetes-configuration-watcher + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + labels: + app: spring-cloud-kubernetes-configuration-watcher + name: spring-cloud-kubernetes-configuration-watcher:view + roleRef: + kind: Role + apiGroup: rbac.authorization.k8s.io + name: namespace-reader + subjects: + - kind: ServiceAccount + name: spring-cloud-kubernetes-configuration-watcher + - apiVersion: rbac.authorization.k8s.io/v1 + kind: Role + metadata: + namespace: default + name: namespace-reader + rules: + - apiGroups: ["", "extensions", "apps"] + resources: ["configmaps", "pods", "services", "endpoints", "secrets"] + verbs: ["get", "list", "watch"] + - apiVersion: apps/v1 + kind: Deployment + metadata: + name: spring-cloud-kubernetes-configuration-watcher-deployment + spec: + selector: + matchLabels: + app: spring-cloud-kubernetes-configuration-watcher + template: + metadata: + labels: + app: spring-cloud-kubernetes-configuration-watcher + spec: + serviceAccount: spring-cloud-kubernetes-configuration-watcher + containers: + - name: spring-cloud-kubernetes-configuration-watcher + image: springcloud/spring-cloud-kubernetes-configuration-watcher:2.0.1-SNAPSHOT + imagePullPolicy: IfNotPresent + readinessProbe: + httpGet: + port: 8888 + path: /actuator/health/readiness + livenessProbe: + httpGet: + port: 8888 + path: /actuator/health/liveness + ports: + - containerPort: 8888 +``` + +Spring Cloud Kubernetes 配置要想正常工作,服务帐户和相关的角色绑定是很重要的。控制器需要访问来读取关于 Kubernetes 集群中的配置映射、POD、服务、端点和秘密的数据。 + +### [](#monitoring-configmaps-and-secrets)[13.2.监视配置图和秘密](#monitoring-configmaps-and-secrets) ### + +Spring Cloud Kubernetes Configuration Watcher 将对带有值`true`的标签的`spring.cloud.kubernetes.config`的配置映射中的更改或带有值`true`的标签的`spring.cloud.kubernetes.secret`的任何秘密做出反应。如果配置图或秘密没有这些标签中的任何一个,或者这些标签的值不是`true`,那么任何更改都将被忽略。 + +Spring Cloud Kubernetes Configmaps 上的配置观察者所寻找的标签和秘密可以分别通过设置 ` Spring.cloud.Kubernetes.configlabel` 和`spring.cloud.kubernetes.configuration.watcher.secretLabel`来更改。 + +如果对具有有效标签的配置图或秘密进行了更改,则 Spring Cloud Kubernetes 配置观察者将获取配置图或秘密的名称,并向具有该名称的应用程序发送通知。 + +### [](#http-implementation)[13.3.HTTP 实现](#http-implementation) ### + +默认情况下使用的是 HTTP 实现。当使用 Spring Cloud Kubernetes 配置监视器并发生对配置图或秘密的更改时,则 HTTP 实现将使用 Spring Cloud Kubernetes 发现客户端来获取与配置图或秘密的名称相匹配的应用程序的所有实例并向应用程序的执行器“/refresh”端点发送 HTTP POST 请求。默认情况下,它将使用在发现客户端中注册的端口向`/actuator/refresh`发送 POST 请求。 + +#### [](#non-default-management-port-and-actuator-path)[13.3.1.非默认管理端口和执行器路径](#non-default-management-port-and-actuator-path) #### + +如果应用程序正在使用非默认的执行器路径和/或使用用于管理端点的不同端口,则用于应用程序的 Kubernetes 服务可以添加一个名为`boot.spring.io/actuator`的注释,并将其值设置为应用程序使用的路径和端口。例如 + +``` +apiVersion: v1 +kind: Service +metadata: + labels: + app: config-map-demo + name: config-map-demo + annotations: + boot.spring.io/actuator: http://:9090/myactuator/home +spec: + ports: + - name: http + port: 8080 + targetPort: 8080 + selector: + app: config-map-demo +``` + +你可以选择配置执行器路径和/或管理端口的另一种方式是通过设置 ` Spring.cloud.kubernetes.configuration.watcher.actuatorpath` 和`spring.cloud.kubernetes.configuration.watcher.actuatorPort`。 + +### [](#messaging-implementation)[13.4.消息传递实现](#messaging-implementation) ### + +当 Spring Cloud Kubernetes Configuration Watcher 应用程序部署到 Kubernetes 时,可以通过将配置文件设置为或来启用消息传递实现。 + +### [](#configuring-rabbitmq)[13.5.配置 RabbitMQ](#configuring-rabbitmq) ### + +当`bus-amqp`配置文件被启用时,你将需要配置 Spring RabbitMQ 以将其指向你想要使用的 RabbitMQ 实例的位置,以及进行身份验证所需的任何凭据。这可以通过设置标准 Spring RabbitMQ 属性来完成,例如 + +``` +spring: + rabbitmq: + username: user + password: password + host: rabbitmq +``` + +### [](#configuring-kafka)[13.6.配置 Kafka](#configuring-kafka) ### + +启用`bus-kafka`配置文件后,你将需要配置 Spring Kafka,将其指向你想要使用的 Kafka 代理实例的位置。这可以通过设置标准 Spring Kafka 属性来完成,例如 + +``` +spring: + kafka: + producer: + bootstrap-servers: localhost:9092 +``` + +[](#spring-cloud-kubernetes-configserver)[14. Spring Cloud Kubernetes Config Server](#spring-cloud-kubernetes-configserver) +---------- + +Spring Cloud Kubernetes Config 服务器基于[Spring Cloud Config Server](https://spring.io/projects/spring-cloud-config),并为 Kubernetes[Config Maps](https://kubernetes.io/docs/concepts/configuration/configmap/)和[Secrets](https://kubernetes.io/docs/concepts/configuration/secret/)添加了[环境存储库](https://docs.spring.io/spring-cloud-config/docs/current/reference/html/#_environment_repository)。 + +这是一个完全可选的组件。然而,它允许你继续利用你在 Kubernetes 上运行的应用程序来利用可能存储在现有环境存储库(Git、SVN、Vault 等)中的配置。 + +默认映像位于[Docker Hub](https://hub.docker.com/r/springcloud/spring-cloud-kubernetes-configserver)上,这将允许你轻松地在 Kubernetes 上部署配置服务器,而无需自己构建代码和映像。但是,如果你需要自定义配置服务器行为,你可以轻松地从 Github 上的源代码构建自己的映像并使用它。 + +### [](#configuration)[14.1.配置](#configuration) ### + +#### [](#enabling-the-kubernetes-environment-repository)[14.1.1.启用 Kubernetes 环境存储库](#enabling-the-kubernetes-environment-repository) #### + +要启用 Kubernetes 环境存储库,`kubernetes`配置文件必须包含在活动配置文件列表中。你也可以激活其他配置文件来使用其他环境存储库实现。 + +#### [](#config-map-and-secret-propertysources)[14.1.2.配置映射和秘密 PropertySources](#config-map-and-secret-propertysources) #### + +默认情况下,将只获取配置映射数据。要启用秘密,还需要设置`spring.cloud.kubernetes.secrets.enableApi=true`。你可以通过设置`spring.cloud.kubernetes.config.enableApi=false`来禁用配置映射`PropertySource`。 + +#### [](#fetching-config-map-and-secret-data-from-additional-namespaces)[14.1.3.从其他名称空间获取配置映射和秘密数据](#fetching-config-map-and-secret-data-from-additional-namespaces) #### + +默认情况下,Kubernetes 环境存储库将仅从部署它的名称空间获取配置映射和秘密。如果希望包括来自其他名称空间的数据,则可以将`spring.cloud.kubernetes.configserver.config-map-namespaces`和/或`spring.cloud.kubernetes.configserver.secrets-namespaces`设置为以逗号分隔的名称空间值列表。 + +| |如果你设置`spring.cloud.kubernetes.configserver.config-map-namespaces`和/或`spring.cloud.kubernetes.configserver.secrets-namespaces`,则需要包括部署配置服务器的名称空间,以便继续从该名称空间获取配置映射和秘密数据。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#kubernetes-access-controls)[14.1.4.Kubernetes 访问控制](#kubernetes-access-controls) #### + +Kubernetes 配置服务器使用 Kubernetes API 服务器获取配置映射和秘密数据。为了做到这一点,它需要`get`和`list`配置映射和秘密(取决于你启用/禁用的内容)。 + +### [](#deployment-yaml-2)[14.2.部署 YAML](#deployment-yaml-2) ### + +下面是一个示例部署、服务和权限配置,你可以使用它将基本配置服务器部署到 Kubernetes。 + +``` +--- +apiVersion: v1 +kind: List +items: + - apiVersion: v1 + kind: Service + metadata: + labels: + app: spring-cloud-kubernetes-configserver + name: spring-cloud-kubernetes-configserver + spec: + ports: + - name: http + port: 8888 + targetPort: 8888 + selector: + app: spring-cloud-kubernetes-configserver + type: ClusterIP + - apiVersion: v1 + kind: ServiceAccount + metadata: + labels: + app: spring-cloud-kubernetes-configserver + name: spring-cloud-kubernetes-configserver + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + labels: + app: spring-cloud-kubernetes-configserver + name: spring-cloud-kubernetes-configserver:view + roleRef: + kind: Role + apiGroup: rbac.authorization.k8s.io + name: namespace-reader + subjects: + - kind: ServiceAccount + name: spring-cloud-kubernetes-configserver + - apiVersion: rbac.authorization.k8s.io/v1 + kind: Role + metadata: + namespace: default + name: namespace-reader + rules: + - apiGroups: ["", "extensions", "apps"] + resources: ["configmaps", "secrets"] + verbs: ["get", "list"] + - apiVersion: apps/v1 + kind: Deployment + metadata: + name: spring-cloud-kubernetes-configserver-deployment + spec: + selector: + matchLabels: + app: spring-cloud-kubernetes-configserver + template: + metadata: + labels: + app: spring-cloud-kubernetes-configserver + spec: + serviceAccount: spring-cloud-kubernetes-configserver + containers: + - name: spring-cloud-kubernetes-configserver + image: springcloud/spring-cloud-kubernetes-configserver + imagePullPolicy: IfNotPresent + env: + - name: SPRING_PROFILES_INCLUDE + value: "kubernetes" + readinessProbe: + httpGet: + port: 8888 + path: /actuator/health/readiness + livenessProbe: + httpGet: + port: 8888 + path: /actuator/health/liveness + ports: + - containerPort: 8888 +``` + +[](#spring-cloud-kubernetes-discoveryserver)[15. Spring Cloud Kubernetes Discovery Server](#spring-cloud-kubernetes-discoveryserver) +---------- + +Spring 云 Kubernetes 发现服务器提供了应用程序可以用来收集关于 Kubernetes 集群中可用的服务的信息的 HTTP 端点。 Spring 云 Kubernetes 发现服务器可以由使用`spring-cloud-starter-kubernetes-discoveryclient`的应用程序使用,以向由该启动器提供的`DiscoveryClient`实现提供数据。 + +### [](#permissions)[15.1.权限](#permissions) ### + +Spring 云发现服务器使用 Kubernetes API 服务器来获取有关服务和端点重排的数据,因此它需要列表、监视和获得使用这些端点的权限。有关如何在 Kubernetes 上配置服务帐户的示例,请参见下面的示例 Kubernetes Deployment YAML。 + +### [](#endpoints)[15.2. Endpoints](#endpoints) ### + +服务器公开了三个端点。 + +#### [](#apps)[15.2.1. `/apps`](#apps) #### + +发送到`/apps`的`GET`请求将返回可用服务的 JSON 数组。每个项都包含 Kubernetes 服务的名称和服务实例信息。下面是一个示例响应。 + +``` +[ + { + "name":"spring-cloud-kubernetes-discoveryserver", + "serviceInstances":[ + { + "instanceId":"836a2f25-daee-4af2-a1be-aab9ce2b938f", + "serviceId":"spring-cloud-kubernetes-discoveryserver", + "host":"10.244.1.6", + "port":8761, + "uri":"http://10.244.1.6:8761", + "secure":false, + "metadata":{ + "app":"spring-cloud-kubernetes-discoveryserver", + "kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"annotations\":{},\"labels\":{\"app\":\"spring-cloud-kubernetes-discoveryserver\"},\"name\":\"spring-cloud-kubernetes-discoveryserver\",\"namespace\":\"default\"},\"spec\":{\"ports\":[{\"name\":\"http\",\"port\":80,\"targetPort\":8761}],\"selector\":{\"app\":\"spring-cloud-kubernetes-discoveryserver\"},\"type\":\"ClusterIP\"}}\n", + "http":"8761" + }, + "namespace":"default", + "scheme":"http" + } + ] + }, + { + "name":"kubernetes", + "serviceInstances":[ + { + "instanceId":"1234", + "serviceId":"kubernetes", + "host":"172.18.0.3", + "port":6443, + "uri":"http://172.18.0.3:6443", + "secure":false, + "metadata":{ + "provider":"kubernetes", + "component":"apiserver", + "https":"6443" + }, + "namespace":"default", + "scheme":"http" + } + ] + } +] +``` + +#### [](#appname)[15.2.2. `/app/{name}`](#appname) #### + +可以使用对`/app/{name}`的`GET`请求来获取给定服务的所有实例的实例数据。下面是当`GET`请求被发送到`/app/kubernetes`时的示例响应。 + +``` +[ + { + "instanceId":"1234", + "serviceId":"kubernetes", + "host":"172.18.0.3", + "port":6443, + "uri":"http://172.18.0.3:6443", + "secure":false, + "metadata":{ + "provider":"kubernetes", + "component":"apiserver", + "https":"6443" + }, + "namespace":"default", + "scheme":"http" + } +] +``` + +#### [](#appnameinstanceid)[15.2.3. `/app/{name}/{instanceid}`](#appnameinstanceid) #### + +向`/app/{name}/{instanceid}`发出的`GET`请求将返回给定服务的特定实例的实例数据。下面是当`GET`请求被发送到`/app/kubernetes/1234`时的示例响应。 + +``` + { + "instanceId":"1234", + "serviceId":"kubernetes", + "host":"172.18.0.3", + "port":6443, + "uri":"http://172.18.0.3:6443", + "secure":false, + "metadata":{ + "provider":"kubernetes", + "component":"apiserver", + "https":"6443" + }, + "namespace":"default", + "scheme":"http" + } +``` + +### [](#deployment-yaml-3)[15.3.部署 YAML](#deployment-yaml-3) ### + +Spring 云发现服务器的图像托管在[Docker Hub](https://hub.docker.com/r/springcloud/spring-cloud-kubernetes-discoveryserver)上。 + +下面是一个示例部署 YAML,你可以使用它将 Kubernetes 配置监视器部署到 Kubernetes。 + +``` +--- +apiVersion: v1 +kind: List +items: + - apiVersion: v1 + kind: Service + metadata: + labels: + app: spring-cloud-kubernetes-discoveryserver + name: spring-cloud-kubernetes-discoveryserver + spec: + ports: + - name: http + port: 80 + targetPort: 8761 + selector: + app: spring-cloud-kubernetes-discoveryserver + type: ClusterIP + - apiVersion: v1 + kind: ServiceAccount + metadata: + labels: + app: spring-cloud-kubernetes-discoveryserver + name: spring-cloud-kubernetes-discoveryserver + - apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + labels: + app: spring-cloud-kubernetes-discoveryserver + name: spring-cloud-kubernetes-discoveryserver:view + roleRef: + kind: Role + apiGroup: rbac.authorization.k8s.io + name: namespace-reader + subjects: + - kind: ServiceAccount + name: spring-cloud-kubernetes-discoveryserver + - apiVersion: rbac.authorization.k8s.io/v1 + kind: Role + metadata: + namespace: default + name: namespace-reader + rules: + - apiGroups: ["", "extensions", "apps"] + resources: ["services", "endpoints"] + verbs: ["get", "list", "watch"] + - apiVersion: apps/v1 + kind: Deployment + metadata: + name: spring-cloud-kubernetes-discoveryserver-deployment + spec: + selector: + matchLabels: + app: spring-cloud-kubernetes-discoveryserver + template: + metadata: + labels: + app: spring-cloud-kubernetes-discoveryserver + spec: + serviceAccount: spring-cloud-kubernetes-discoveryserver + containers: + - name: spring-cloud-kubernetes-discoveryserver + image: springcloud/spring-cloud-kubernetes-discoveryserver:2.1.0-SNAPSHOT + imagePullPolicy: IfNotPresent + readinessProbe: + httpGet: + port: 8761 + path: /actuator/health/readiness + livenessProbe: + httpGet: + port: 8761 + path: /actuator/health/liveness + ports: + - containerPort: 8761 +``` + +[](#examples)[16. Examples](#examples) +---------- + +Spring Cloud Kubernetes 试图通过遵循 Spring Cloud 接口使你的应用程序使用 Kubernetes 原生服务变得透明。 + +在应用程序中,需要将`spring-cloud-kubernetes-discovery`依赖项添加到 Classpath 中,并删除包含`DiscoveryClient`实现的任何其他依赖项(即 Eureka 发现客户机)。这同样适用于`PropertySourceLocator`,其中你需要向 Classpath 中添加`spring-cloud-kubernetes-config`,并删除包含`PropertySourceLocator`实现(即配置服务器客户端)的任何其他依赖项。 + +下面的项目重点介绍了这些依赖关系的用法,并演示了如何从任何 Spring 启动应用程序中使用这些库: + +* [Spring Cloud Kubernetes Examples](https://github.com/spring-cloud/spring-cloud-kubernetes/tree/master/spring-cloud-kubernetes-examples):位于此存储库中的那些。 + +* Spring Cloud Kubernetes 完整示例:小黄人和老板 + + * [Minion](https://github.com/salaboy/spring-cloud-k8s-minion) + + * [Boss](https://github.com/salaboy/spring-cloud-k8s-boss) + +* Spring Cloud Kubernetes 完整示例:[Springone 站台售票服务](https://github.com/salaboy/s1p_docs) + +* [Spring Cloud Gateway with Spring Cloud Kubernetes Discovery and Config](https://github.com/salaboy/s1p_gateway) + +* [Spring Boot Admin with Spring Cloud Kubernetes Discovery and Config](https://github.com/salaboy/showcase-admin-tool) + +[](#other-resources)[17.其他资源](#other-resources) +---------- + +本节列出了其他资源,例如关于 Spring Cloud Kubernetes 的演示文稿(幻灯片)和视频。 + +* [S1P Spring Cloud on PKS](https://salaboy.com/2018/09/27/the-s1p-experience/) + +* [Spring Cloud, Docker, Kubernetes → London Java Community July 2018](https://salaboy.com/2018/07/18/ljc-july-18-spring-cloud-docker-k8s/) + +请随时通过 pull 请求提交其他资源到[this repository](https://github.com/spring-cloud/spring-cloud-kubernetes)。 + +[](#configuration-properties)[18.配置属性](#configuration-properties) +---------- + +要查看所有 Kubernetes 相关配置属性的列表,请检查[附录页](appendix.html)。 + +[](#building)[19. Building](#building) +---------- + +### [](#basic-compile-and-test)[19.1.基本编译和测试](#basic-compile-and-test) ### + +要构建源代码,你需要安装 JDK17。 + +Spring Cloud 在大多数与构建相关的活动中使用 Maven,你应该能够通过克隆感兴趣的项目并键入来很快地启动它。 + +``` +$ ./mvnw install +``` + +| |你也可以自己安装 Maven(\>=3.3.3),并在下面的示例中运行`mvn`命令
来代替`./mvnw`。如果你这样做,那么如果你的本地 Maven 设置不
包含 Spring 预发布工件的存储库声明,那么你可能还需要添加`-P spring`。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |请注意,你可能需要通过使用
设置一个`MAVEN_OPTS`的环境变量来增加 Maven 可用的
内存量`-Xmx512m -XX:MaxPermSize=128m`这样的值。我们试图在
的`.mvn`配置中涵盖这一点,因此,如果你发现必须这样做才能使
构建成功,请举出一张票来将设置添加到
源代码控制中。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +需要中间件(即 Redis)进行测试的项目通常需要安装并运行[Docker]([WWW.docker.com/get-started](https://www.docker.com/get-started))的本地实例。 + +### [](#documentation)[19.2.文件](#documentation) ### + +Spring-cloud-build 模块有一个“DOCS”配置文件,如果将其打开,将尝试从 `SRC/Main/ASCIIDoc’构建 ASCIIDoc 源。作为该过程的一部分,它将寻找一个“readme.ADOC”,并通过加载所有包含来处理它,但不是解析或呈现它,只是将其复制到`${main.basedir}`(默认为`$/tmp/releaser-1645122597379-0/spring-cloud-kubernetes/docs`,即项目的根)。如果 README 中有任何更改,那么在构建 Maven 之后,它将在正确的位置显示为经过修改的文件。只要承诺并推动改变就行了。 + +### [](#working-with-the-code)[19.3.使用代码](#working-with-the-code) ### + +如果你没有 IDE 偏好,我们建议你在使用代码时使用[Spring Tools Suite](https://www.springsource.com/developer/sts)或[Eclipse](https://eclipse.org)。我们使用[m2eclipse](https://eclipse.org/m2e/)Eclipse 插件来提供 Maven 支持。其他 IDE 和工具也应该在没有问题的情况下工作,只要它们使用 Maven 3.3.3 或更好。 + +#### [](#activate-the-spring-maven-profile)[19.3.1. Activate the Spring Maven profile](#activate-the-spring-maven-profile) #### + +Spring 云项目需要激活“ Spring” Maven 配置文件以解析 Spring 里程碑和快照存储库。使用你首选的 IDE 将此配置文件设置为活动的,否则你可能会遇到构建错误。 + +#### [](#importing-into-eclipse-with-m2eclipse)[19.3.2.用 M2Eclipse 导入到 Eclipse 中](#importing-into-eclipse-with-m2eclipse) #### + +在使用 Eclipse 时,我们推荐[m2eclipse](https://eclipse.org/m2e/)Eclipse 插件。如果你还没有安装 M2Eclipse,它可以从“Eclipse 市场”获得。 + +| |较早版本的 M2E 不支持 Maven 3.3,因此,一旦
项目导入到 Eclipse 中,你还需要告诉
M2Eclipse 为项目使用正确的配置文件。如果你
在项目中看到与 POM 相关的许多不同错误,请检查
是否有最新的安装。如果你不能升级 M2E,
将“ Spring”配置文件添加到你的`settings.xml`。或者,你可以
将存储库设置从父
POM 的“ Spring”配置文件复制到你的`settings.xml`中。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#importing-into-eclipse-without-m2eclipse)[19.3.3.在没有 M2Eclipse 的情况下导入 Eclipse](#importing-into-eclipse-without-m2eclipse) #### + +如果不喜欢使用 M2Eclipse,可以使用以下命令生成 Eclipse 项目元数据: + +``` +$ ./mvnw eclipse:eclipse +``` + +可以通过从`file`菜单中选择`import existing projects`来导入生成的 Eclipse 项目。 + +[](#contributing)[20.贡献](#contributing) +---------- + +Spring Cloud 是在非限制性的 Apache2.0 许可下发布的,并遵循非常标准的 GitHub 开发流程,使用 GitHub Tracker 处理问题并将拉请求合并到 Master 中。如果你想贡献一些微不足道的东西,请不要犹豫,但要遵循下面的指导方针。 + +### [](#sign-the-contributor-license-agreement)[20.1.签署贡献者许可协议](#sign-the-contributor-license-agreement) ### + +在我们接受一个重要的补丁或拉请求之前,我们需要你签署[贡献者许可协议](https://cla.pivotal.io/sign/spring)。签署贡献者协议并不会授予任何人对主库的提交权限,但这确实意味着我们可以接受你的贡献,并且如果我们接受了,你将获得作者信用。活跃的贡献者可能会被要求加入核心团队,并被赋予合并拉请求的能力。 + +### [](#code-of-conduct)[20.2.行为守则](#code-of-conduct) ### + +此项目遵守贡献者契约[code of conduct](https://github.com/spring-cloud/spring-cloud-build/blob/master/docs/src/main/asciidoc/code-of-conduct.adoc)。通过参与,你将被期望坚持这一准则。请向[[电子邮件保护]]报告不可接受的行为(/cdn-cgi/l/email-protection#473437352e29206a242823226a28216a242829232243073372e312833262b692e28)。 + +### [](#code-conventions-and-housekeeping)[20.3.守则惯例和内部管理](#code-conventions-and-housekeeping) ### + +这些都不是拉请求所必需的,但它们都会有所帮助。它们也可以在原始的拉请求之后但在合并之前添加。 + +* 使用 Spring 框架代码格式约定。如果使用 Eclipse,则可以使用[Spring Cloud Build](https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-dependencies-parent/eclipse-code-formatter.xml)项目中的 `eclipse-code-formatter.xml’文件导入格式化设置。如果使用 IntelliJ,可以使用[Eclipse 代码格式化插件](https://plugins.jetbrains.com/plugin/6546)导入相同的文件。 + +* 确保所有新的`.java`文件都有一个简单的 Javadoc 类注释,其中至少有一个“@author”标记来标识你,并且最好至少有一个段落来说明这个类的目的。 + +* 将 ASF 许可标头注释添加到所有新的`.java`文件(从项目中的现有文件复制) + +* 将自己作为`@author`添加到要进行实质性修改的.java 文件中(不仅仅是外观上的更改)。 + +* 添加一些 Javadocs,如果你更改了名称空间,还可以添加一些 XSDDOC 元素。 + +* 几个单元测试也会有很大帮助——必须有人去做。 + +* 如果没有其他人正在使用你的分支,请将它重新设置为当前的主分支(或主项目中的其他目标分支)。 + +* 在编写提交消息时,请遵循[这些约定](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html),如果你正在修复现有的问题,请在提交消息的末尾添加`Fixes gh-XXXX`(其中 xxxx 是问题编号)。 + +### [](#checkstyle)[20.4.checkstyle](#checkstyle) ### + +Spring 云构建附带一组 checkstyle 规则。你可以在`spring-cloud-build-tools`模块中找到它们。该模块下最值得注意的文件是: + +Spring-云构建工具/ + +``` +└── src +    ├── checkstyle +    │   └── checkstyle-suppressions.xml (3) +    └── main +    └── resources +    ├── checkstyle-header.txt (2) +    └── checkstyle.xml (1) +``` + +|**1**|默认的 checkstyle 规则| +|-----|-------------------------| +|**2**|文件头设置| +|**3**|默认抑制规则| + +#### [](#checkstyle-configuration)[20.4.1.checkstyle 配置](#checkstyle-configuration) #### + +checkstyle 规则是**默认禁用**。要将 checkstyle 添加到项目中,只需定义以下属性和插件。 + +POM.xml + +``` + +true (1) + true + (2) + true + (3) + + + + + (4) + io.spring.javaformat + spring-javaformat-maven-plugin + + (5) + org.apache.maven.plugins + maven-checkstyle-plugin + + + + + + (5) + org.apache.maven.plugins + maven-checkstyle-plugin + + + + +``` + +|**1**|构建 checkstyle 错误失败| +|-----|--------------------------------------------------------------------------------------------------------------| +|**2**|构建 checkstyle 冲突失败| +|**3**|CheckStyle 还分析了测试源| +|**4**|添加 Spring Java 格式插件,该插件将重新格式化你的代码,以传递大多数 CheckStyle 格式设置规则| +|**5**|将 CheckStyle 插件添加到构建和报告阶段| + +如果你需要抑制一些规则(例如,行长需要更长),那么你就可以在`${project.root}/src/checkstyle/checkstyle-suppressions.xml`下定义一个带有抑制的文件。示例: + +projectRoot/SRC/checkstyle/checkstyle-suppresions.xml + +``` + + + + + + +``` + +建议将`${spring-cloud-build.rootFolder}/.editorconfig`和`${spring-cloud-build.rootFolder}/.springformat`复制到你的项目中。这样,将应用一些默认的格式设置规则。你可以通过运行以下脚本来实现此目的: + +``` +$ curl https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/.editorconfig -o .editorconfig +$ touch .springformat +``` + +### [](#ide-setup)[20.5. IDE setup](#ide-setup) ### + +#### [](#intellij-idea)[20.5.1.Intellij 思想](#intellij-idea) #### + +为了设置 IntelliJ,你应该导入我们的编码约定、检查配置文件并设置 CheckStyle 插件。以下文件可以在[Spring Cloud Build](https://github.com/spring-cloud/spring-cloud-build/tree/master/spring-cloud-build-tools)项目中找到。 + +Spring-云构建工具/ + +``` +└── src +    ├── checkstyle +    │   └── checkstyle-suppressions.xml (3) +    └── main +    └── resources +    ├── checkstyle-header.txt (2) +    ├── checkstyle.xml (1) +    └── intellij +       ├── Intellij_Project_Defaults.xml (4) +       └── Intellij_Spring_Boot_Java_Conventions.xml (5) +``` + +|**1**|默认的 checkstyle 规则| +|-----|--------------------------------------------------------------------------| +|**2**|文件头设置| +|**3**|默认抑制规则| +|**4**|适用大多数 CheckStyle 规则的 IntelliJ 的项目默认值| +|**5**|适用大多数 CheckStyle 规则的 IntelliJ 的项目风格约定| + +![Code style](https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/docs/src/main/asciidoc/images/intellij-code-style.png) + +图 1。代码样式 + +转到`File``Settings``Editor``Code style`。点击`Scheme`区域旁边的图标。在这里,单击`Import Scheme`值并选择`Intellij IDEA code style XML`选项。导入`spring-cloud-build-tools/src/main/resources/intellij/Intellij_Spring_Boot_Java_Conventions.xml`文件。 + +![Code style](https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/docs/src/main/asciidoc/images/intellij-inspections.png) + +图 2。检查剖面 + +转到`File``Settings``Editor``Inspections`。点击`Profile`区域旁边的图标。在那里,单击`Import Profile`并导入`spring-cloud-build-tools/src/main/resources/intellij/Intellij_Project_Defaults.xml`文件。 + +checkstyle + +要让 IntelliJ 使用 CheckStyle,你必须安装`Checkstyle`插件。建议还安装`Assertions2Assertj`来自动转换 JUnit 断言 + +![Checkstyle](https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/docs/src/main/asciidoc/images/intellij-checkstyle.png) + +转到`File``Settings``Other settings``Checkstyle`。点击`Configuration file`区域中的`+`图标。在这里,你必须定义应该从哪里选择 CheckStyle 规则。在上面的图片中,我们从克隆的 Spring 云构建存储库中选择了规则。但是,你可以指向 Spring Cloud Build 的 GitHub 存储库(例如`checkstyle.xml`:`[raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/main/resources/checkstyle.xml](https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/main/resources/checkstyle.xml)`)。我们需要提供以下变量: + +* `checkstyle.header.file`-请将其指向 Spring Cloud Build 的`spring-cloud-build-tools/src/main/resources/checkstyle-header.txt`文件,可以在你的克隆 repo 中,也可以通过`[raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/main/resources/checkstyle-header.txt](https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/main/resources/checkstyle-header.txt)`URL。 + +* `checkstyle.suppressions.file`-默认抑制。请将它指向 Spring Cloud Build 的`spring-cloud-build-tools/src/checkstyle/checkstyle-suppressions.xml`文件,或者在你的克隆 repo 中,或者通过`[raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/checkstyle/checkstyle-suppressions.xml](https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/checkstyle/checkstyle-suppressions.xml)`URL。 + +* `checkstyle.additional.suppressions.file`-此变量对应于本地项目中的抑制。例如,你正在处理`spring-cloud-contract`。然后指向`project-root/src/checkstyle/checkstyle-suppressions.xml`文件夹。`spring-cloud-contract`的例子是:`/home/username/spring-cloud-contract/src/checkstyle/checkstyle-suppressions.xml`。 + +| |请记住将`Scan Scope`设置为`All sources`,因为我们为生产和测试源应用了 checkstyle 规则。| +|---|------------------------------------------------------------------------------------------------------------------| + +### [](#duplicate-finder)[20.6.重复查找器](#duplicate-finder) ### + +Spring 云构建带来了`basepom:duplicate-finder-maven-plugin`,这使得能够在 Java Classpath 上标记重复的和冲突的类和资源。 + +#### [](#duplicate-finder-configuration)[20.6.1.重复查找器配置](#duplicate-finder-configuration) #### + +重复查找器是**默认启用**,将在 Maven 构建的`verify`阶段运行,但是只有在项目的`duplicate-finder-maven-plugin`部分中添加`duplicate-finder-maven-plugin`,它才会在项目中生效。 + +POM.xml + +``` + + + + org.basepom.maven + duplicate-finder-maven-plugin + + + +``` + +对于其他属性,我们设置了[插件文档](https://github.com/basepom/duplicate-finder-maven-plugin/wiki)中列出的默认值。 + +你可以轻松地重写它们,但可以使用`duplicate-finder-maven-plugin`前缀设置所选属性的值。例如,将`duplicate-finder-maven-plugin.skip`设置为`true`,以便在构建中跳过重复检查。 + +如果需要将`ignoredClassPatterns`或`ignoredResourcePatterns`添加到设置中,请确保将它们添加到项目的插件配置部分中: + +``` + + + + org.basepom.maven + duplicate-finder-maven-plugin + + + org.joda.time.base.BaseDateTime + .*module-info + + + changelog.txt + + + + + +``` diff --git a/docs/spring-cloud/spring-cloud-netflix.md b/docs/spring-cloud/spring-cloud-netflix.md new file mode 100644 index 0000000000000000000000000000000000000000..ed0e2afb70d4792742ae70d9b9fe007a4fd859e2 --- /dev/null +++ b/docs/spring-cloud/spring-cloud-netflix.md @@ -0,0 +1,476 @@ +Spring 云 Netflix +========== + +该项目通过自动配置和绑定到 Spring 环境和其他 Spring 编程模型习惯用法,为 Spring 引导应用程序提供 Netflix OSS 集成。通过一些简单的注释,你可以快速启用和配置应用程序中的常见模式,并使用经过战斗测试的 Netflix 组件构建大型分布式系统。提供的模式包括服务发现。 + +[](#service-discovery-eureka-clients)[1.服务发现:尤里卡客户](#service-discovery-eureka-clients) +---------- + +服务发现是基于微服务的体系结构的关键原则之一。尝试手动配置每个客户机或某种形式的约定可能很难做到,并且可能很脆弱。Eureka 是 Netflix 的服务发现服务器和客户端。可以将服务器配置和部署为高度可用,每个服务器都将有关已注册服务的状态复制到其他服务器。 + +### [](#netflix-eureka-client-starter)[1.1.如何包含 Eureka 客户端](#netflix-eureka-client-starter) ### + +要在项目中包含 Eureka 客户机,请使用组 ID 为`org.springframework.cloud`和工件 ID 为`spring-cloud-starter-netflix-eureka-client`的 starter。请参阅[Spring Cloud Project page](https://projects.spring.io/spring-cloud/),以获取有关使用当前 Spring 云发布系列设置构建系统的详细信息。 + +### [](#registering-with-eureka)[1.2.在尤里卡注册](#registering-with-eureka) ### + +当客户机向 Eureka 注册时,它会提供有关自身的元数据——例如主机、端口、健康指示器 URL、主页和其他详细信息。Eureka 从属于某个服务的每个实例接收心跳消息。如果心跳在可配置的时间表上失败,那么实例通常会从注册表中删除。 + +下面的示例展示了一个最小的 Eureka 客户机应用程序: + +``` +@SpringBootApplication +@RestController +public class Application { + + @RequestMapping("/") + public String home() { + return "Hello world"; + } + + public static void main(String[] args) { + new SpringApplicationBuilder(Application.class).web(true).run(args); + } + +} +``` + +请注意,前面的示例显示了一个正常的[Spring Boot](https://projects.spring.io/spring-boot/)应用程序。通过在 Classpath 上具有`spring-cloud-starter-netflix-eureka-client`,你的应用程序将自动向 Eureka 服务器注册。需要配置来定位 Eureka 服务器,如以下示例所示: + +应用程序.yml + +``` +eureka: + client: + serviceUrl: + defaultZone: http://localhost:8761/eureka/ +``` + +在前面的示例中,`defaultZone`是一个 magic string fallback 值,它为不表示偏好的任何客户机提供服务 URL(换句话说,它是一个有用的默认值)。 + +| |`defaultZone`属性是区分大小写的,并且需要大小写,因为`serviceUrl`属性是`Map`。因此,`defaultZone`属性并不遵循正常的 Spring boot snake-case 约定`default-zone`。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +默认的应用程序名称(即服务 ID)、虚拟主机和非安全端口(取自`Environment`)分别为`${spring.application.name}`、`${spring.application.name}`和`${server.port}`。 + +在 Classpath 上有`spring-cloud-starter-netflix-eureka-client`使应用程序既成为一个 Eureka“实例”(即它自己注册),又成为一个“客户端”(它可以查询注册中心以找到其他服务)。实例行为是由`eureka.instance.*`配置键驱动的,但是如果你确保你的应用程序的值为`spring.application.name`(这是 Eureka 服务 ID 或 VIP 的默认值),那么默认值就可以了。 + +有关可配置选项的更多详细信息,请参见[Eurekainstanconfigbean](https://github.com/spring-cloud/spring-cloud-netflix/tree/main/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaInstanceConfigBean.java)和[EurekaclientConfigBean](https://github.com/spring-cloud/spring-cloud-netflix/tree/main/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaClientConfigBean.java)。 + +要禁用 Eureka 发现客户端,可以将`eureka.client.enabled`设置为`false`。当`spring.cloud.discovery.enabled`设置为`false`时,Eureka 发现客户端也将被禁用。 + +### [](#authenticating-with-the-eureka-server)[1.3.使用 Eureka 服务器进行身份验证](#authenticating-with-the-eureka-server) ### + +如果`eureka.client.serviceUrl.defaultZone`URL 中有一个内嵌凭据(curl 样式,如下所示:`[user:[email protected]:8761/eureka](https://user:password@localhost:8761/eureka)`),则 HTTP Basic 身份验证将自动添加到 Eureka 客户机。对于更复杂的需求,可以创建`@Bean`类型的`DiscoveryClientOptionalArgs`,并将`ClientFilter`实例注入其中,所有这些都应用于从客户机到服务器的调用。 + +当 Eureka 服务器需要客户端证书进行身份验证时,可以通过属性配置客户端证书和信任存储区,如以下示例所示: + +应用程序.yml + +``` +eureka: + client: + tls: + enabled: true + key-store: + key-store-type: PKCS12 + key-store-password: + key-password: + trust-store: + trust-store-type: PKCS12 + trust-store-password: +``` + +`eureka.client.tls.enabled`需要为 true 才能启用 Eureka 客户端 TLS。当省略`eureka.client.tls.trust-store`时,将使用一个 JVM 默认信任存储区。`eureka.client.tls.key-store-type`和`eureka.client.tls.trust-store-type`的默认值是 pkcs12。如果省略了密码属性,则假定密码为空。 + +| |由于 Eureka 中的限制,不可能支持每个服务器的基本身份验证凭据,因此只使用找到的第一组。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------| + +如果你想定制 Eureka HTTP 客户端使用的 RESTTemplate,那么你可能想要创建`EurekaClientHttpRequestFactorySupplier`的 Bean,并为生成`ClientHttpRequestFactory`实例提供你自己的逻辑。 + +### [](#status-page-and-health-indicator)[1.4.状态页和健康指标](#status-page-and-health-indicator) ### + +Eureka 实例的状态页和健康指示器分别默认为`/info`和`/health`,这是 Spring 引导执行器应用程序中有用端点的默认位置。你需要更改这些,即使对于执行器应用程序,如果你使用非默认的上下文路径或 Servlet 路径(例如`server.servletPath=/custom`),也需要进行更改。下面的示例显示了这两个设置的默认值: + +应用程序.yml + +``` +eureka: + instance: + statusPageUrlPath: ${server.servletPath}/info + healthCheckUrlPath: ${server.servletPath}/health +``` + +这些链接会显示在客户使用的元数据中,并在某些场景中用于决定是否将请求发送到应用程序,因此如果它们是准确的,则会很有帮助。 + +| |在 Dalston 中,当更改
管理上下文路径时,还需要设置状态和健康检查 URL。这一要求从 Edgware 开始就被删除了。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#registering-a-secure-application)[1.5.注册安全应用程序](#registering-a-secure-application) ### + +如果你的应用程序希望通过 HTTPS 进行联系,则可以在`EurekaInstanceConfigBean`中设置两个标志: + +* `eureka.instance.[nonSecurePortEnabled]=[false]` + +* `eureka.instance.[securePortEnabled]=[true]` + +这样做使得 Eureka 发布实例信息,显示出对安全通信的明确偏好。 Spring 对于以这种方式配置的服务,云`DiscoveryClient`总是返回一个以`https`开头的 URI。类似地,当以这种方式配置服务时,Eureka(本地)实例信息具有安全的健康检查 URL。 + +由于 Eureka 的内部工作方式,它仍然为状态和主页发布一个非安全的 URL,除非你也显式地覆盖这些 URL。你可以使用占位符来配置 Eureka 实例 URL,如下例所示: + +应用程序.yml + +``` +eureka: + instance: + statusPageUrl: https://${eureka.hostname}/info + healthCheckUrl: https://${eureka.hostname}/health + homePageUrl: https://${eureka.hostname}/ +``` + +(请注意,`${eureka.hostname}`是一个本机占位符,仅在 Eureka 的后续版本中可用。你也可以使用 Spring 占位符实现同样的功能——例如,通过使用`${eureka.instance.hostName}`。 + +| |如果你的应用程序运行在代理之后,并且 SSL 终止在代理中(例如,如果你在 Cloud Foundry 或其他平台即服务中运行),然后,你需要确保代理“转发”头文件被应用程序拦截并处理。
如果嵌入在 Spring 启动应用程序中的 Tomcat 容器对“X-forwarded-\\**”头文件有明确的配置,这种情况会自动发生。
应用程序向自身呈现的链接是错误的(错误的主机,Port 或 Protocol)是你错误配置的标志。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#eurekas-health-checks)[1.6.尤里卡的健康检查](#eurekas-health-checks) ### + +默认情况下,Eureka 使用客户端心跳来确定客户端是否启动。除非另有指定,否则发现客户端不会根据 Spring 引导执行器传播应用程序的当前健康检查状态。因此,在成功注册之后,Eureka 总是宣布该应用程序处于“启动”状态。可以通过启用 Eureka 健康检查来改变此行为,从而将应用程序状态传播到 Eureka。因此,其他所有应用程序都不会将通信量发送到其他“向上”状态的应用程序。下面的示例展示了如何为客户机启用健康检查: + +应用程序.yml + +``` +eureka: + client: + healthcheck: + enabled: true +``` + +| |`eureka.client.healthcheck.enabled=true`应该只设置在`应用程序.yml`中。在`bootstrap.yml`中设置该值会导致不良的副作用,例如在 Eureka 中使用`UNKNOWN`状态注册。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +如果你需要对健康检查进行更多的控制,请考虑实现你自己的`com.netflix.appinfo.HealthCheckHandler`。 + +### [](#eureka-metadata-for-instances-and-clients)[1.7.用于实例和客户机的 Eureka 元数据](#eureka-metadata-for-instances-and-clients) ### + +值得花一些时间来了解 Eureka 元数据是如何工作的,这样你就可以以一种在你的平台中有意义的方式使用它。对于诸如主机名、IP 地址、端口号、状态页和健康检查等信息,有标准的元数据。它们发布在服务注册中心中,并由客户机使用,以一种简单的方式与服务联系。可以在`eureka.instance.metadataMap`中将其他元数据添加到实例注册中,并且可以在远程客户端中访问此元数据。通常,附加的元数据不会改变客户机的行为,除非使客户机意识到元数据的含义。有几个特殊情况(在本文后面描述),其中 Spring Cloud 已经为元数据映射分配了含义。 + +#### [](#using-eureka-on-cloud-foundry)[1.7.1.在 Cloud Foundry 上使用 Eureka](#using-eureka-on-cloud-foundry) #### + +Cloud Foundry 有一个全球路由器,因此同一应用程序的所有实例都具有相同的主机名(具有类似架构的其他 PaaS 解决方案具有相同的安排)。这不一定是使用 Eureka 的障碍。但是,如果你使用路由器(根据你的平台设置方式,推荐甚至强制使用),则需要显式地设置主机名和端口号(安全或非安全),以便它们使用路由器。你可能还希望使用实例元数据,以便能够区分客户机上的实例(例如,在自定义负载均衡器中)。默认情况下,`eureka.instance.instanceId`是`vcap.application.instance_id`,如下例所示: + +应用程序.yml + +``` +eureka: + instance: + hostname: ${vcap.application.uris[0]} + nonSecurePort: 80 +``` + +根据在 Cloud Foundry 实例中设置安全规则的方式,你可能能够注册并使用主机 VM 的 IP 地址进行直接的服务到服务调用。此功能在 Pivotal Web 服务上还不可用([PWS](https://run.pivotal.io))。 + +#### [](#using-eureka-on-aws)[1.7.2.在 AWS 上使用 Eureka](#using-eureka-on-aws) #### + +如果计划将应用程序部署到 AWS 云中,那么必须将 Eureka 实例配置为 AWS 感知的。你可以通过如下方式定制[Eurekainstanconfigbean](https://github.com/spring-cloud/spring-cloud-netflix/tree/main/spring-cloud-netflix-eureka-client/src/main/java/org/springframework/cloud/netflix/eureka/EurekaInstanceConfigBean.java)来实现此目的: + +``` +@Bean +@Profile("!default") +public EurekaInstanceConfigBean eurekaInstanceConfig(InetUtils inetUtils) { + EurekaInstanceConfigBean bean = new EurekaInstanceConfigBean(inetUtils); + AmazonInfo info = AmazonInfo.Builder.newBuilder().autoBuild("eureka"); + bean.setDataCenterInfo(info); + return bean; +} +``` + +#### [](#changing-the-eureka-instance-id)[1.7.3.更改 Eureka 实例 ID](#changing-the-eureka-instance-id) #### + +一个普通的 Netflix Eureka 实例注册的 ID 等于其主机名(也就是说,每个主机只有一个服务)。 Spring Cloud Eureka 提供了一个合理的默认值,其定义如下: + +`${spring.cloud.client.hostname}:${spring.application.name}:${spring.application.instance_id:${server.port}}` + +一个例子是`myhost:myappname:8080`。 + +通过使用 Spring Cloud,你可以通过在`eureka.instance.instanceId`中提供唯一的标识符来覆盖该值,如下例所示: + +应用程序.yml + +``` +eureka: + instance: + instanceId: ${spring.application.name}:${vcap.application.instance_id:${spring.application.instance_id:${random.value}}} +``` + +使用前面示例中显示的元数据和在 LocalHost 上部署的多个服务实例,将在其中插入随机值,以使实例具有唯一性。在 Cloud Foundry 中,`vcap.application.instance_id`在 Spring 引导应用程序中自动填充,因此不需要随机值。 + +### [](#using-the-eurekaclient)[1.8.使用 Eurekaclient](#using-the-eurekaclient) ### + +一旦你有了一个作为发现客户机的应用程序,你就可以使用它从[Eureka Server](#spring-cloud-eureka-server)中发现服务实例。这样做的一种方法是使用本机`com.netflix.discovery.EurekaClient`(而不是 Spring 云`DiscoveryClient`),如以下示例所示: + +``` +@Autowired +private EurekaClient discoveryClient; + +public String serviceUrl() { + InstanceInfo instance = discoveryClient.getNextServerFromEureka("STORES", false); + return instance.getHomePageUrl(); +} +``` + +| |不要在`@PostConstruct`方法或`@Scheduled`方法(或尚未启动`ApplicationContext`的任何地方)中使用`EurekaClient`。
它是在`SmartLifecycle`(与`phase=0`一起)中初始化的,因此,最早可以依赖它的是在另一个具有更高相位的`SmartLifecycle`中。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#eurekaclient-with-jersey)[1.8.1.穿着球衣的 Eurekaclient](#eurekaclient-with-jersey) #### + +默认情况下,Eurekaclient 使用 Spring 的`RestTemplate`进行 HTTP 通信。如果希望使用 Jersey,则需要将 Jersey 依赖项添加到 Classpath 中。下面的示例显示了你需要添加的依赖关系: + +``` + + com.sun.jersey + jersey-client + + + com.sun.jersey + jersey-core + + + com.sun.jersey.contribs + jersey-apache-client4 + +``` + +### [](#alternatives-to-the-native-netflix-eurekaclient)[1.9.原生 Netflix Eurekaclient 的替代品](#alternatives-to-the-native-netflix-eurekaclient) ### + +你不需要使用原始的 Netflix`EurekaClient`。而且,通常在某种包装纸后面使用它会更方便。 Spring 云通过逻辑 Eureka 服务标识符(VIPS)而不是物理 URL 支持(一个 REST 客户机构建器)和。 + +你也可以使用`org.springframework.cloud.client.discovery.DiscoveryClient`,它为 Discovery 客户机提供了一个简单的 API(不特定于 Netflix),如以下示例所示: + +``` +@Autowired +private DiscoveryClient discoveryClient; + +public String serviceUrl() { + List list = discoveryClient.getInstances("STORES"); + if (list != null && list.size() > 0 ) { + return list.get(0).getUri(); + } + return null; +} +``` + +### [](#why-is-it-so-slow-to-register-a-service)[1.10.为什么注册服务这么慢?](#why-is-it-so-slow-to-register-a-service) ### + +作为一个实例,注册中心还需要一个周期性的心跳(通过客户机的`serviceUrl`),默认持续时间为 30 秒。在实例、服务器和客户机的本地缓存中都有相同的元数据(因此可能需要 3 次心跳)之前,客户端无法发现服务。你可以通过设置`eureka.instance.leaseRenewalIntervalInSeconds`来更改句号。将它设置为小于 30 的值可以加快客户连接到其他服务的过程。在生产中,最好坚持使用默认值,因为服务器中的内部计算对租赁续约期进行了假设。 + +### [](#zones)[1.11. Zones](#zones) ### + +如果你已经将 Eureka 客户机部署到多个区域,那么你可能希望这些客户机在尝试另一个区域中的服务之前,先在同一个区域中使用服务。要设置它,你需要正确配置你的 Eureka 客户机。 + +首先,你需要确保每个区域都部署了 Eureka 服务器,并且它们是彼此的对等服务器。有关更多信息,请参见[区域和区域](#spring-cloud-eureka-server-zones-and-regions)一节。 + +接下来,你需要告诉尤里卡你的服务在哪个区域。你可以通过使用`metadataMap`属性来实现这一点。例如,如果`service 1`被部署到`zone 1`和`zone 2`,则需要在`service 1`中设置以下 Eureka 属性: + +**1 区服务 1** + +``` +eureka.instance.metadataMap.zone = zone1 +eureka.client.preferSameZoneEureka = true +``` + +**2 区服务 1** + +``` +eureka.instance.metadataMap.zone = zone2 +eureka.client.preferSameZoneEureka = true +``` + +### [](#refreshing-eureka-clients)[1.12.刷新 Eureka 客户端](#refreshing-eureka-clients) ### + +默认情况下,`EurekaClient` Bean 是可刷新的,这意味着可以更改和刷新 Eureka 客户机属性。当刷新发生时,客户端将从 Eureka 服务器上被取消注册,并且可能会有一个短暂的时刻,其中给定服务的所有实例都不可用。避免这种情况发生的一种方法是禁用刷新 Eureka 客户机的功能。要执行此设置`eureka.client.refresh.enable=false`。 + +### [](#using-eureka-with-spring-cloud-loadbalancer)[1.13. Using Eureka with Spring Cloud LoadBalancer](#using-eureka-with-spring-cloud-loadbalancer) ### + +我们为 Spring Cloud LoadBalancer`ZonePreferenceServiceInstanceListSupplier`提供支持。来自 Eureka 实例元数据的`zone`值用于设置`spring-cloud-loadbalancer-zone`属性的值,该属性用于按区域筛选服务实例。 + +如果缺少此项,并且如果`spring.cloud.loadbalancer.eureka.approximateZoneFromHostname`标志设置为`true`,则可以使用来自服务器主机名的域名作为该区域的代理。 + +如果没有其他区域数据源,则根据客户机配置(而不是实例配置)进行猜测。我们使用`eureka.client.availabilityZones`,这是从区域名称到区域列表的映射,并为实例自己的区域(即`eureka.client.region`,默认为“US-East-1”,以兼容原生 Netflix)拉出第一个区域。 + +[](#spring-cloud-eureka-server)[2.服务发现:Eureka 服务器](#spring-cloud-eureka-server) +---------- + +本节介绍如何设置 Eureka 服务器。 + +### [](#netflix-eureka-server-starter)[2.1.如何包含 Eureka 服务器](#netflix-eureka-server-starter) ### + +要在项目中包含 Eureka 服务器,请使用组 ID 为`org.springframework.cloud`和工件 ID 为`spring-cloud-starter-netflix-eureka-server`的 starter。请参阅[Spring Cloud Project page](https://projects.spring.io/spring-cloud/)以获取有关使用当前 Spring 云发布列设置构建系统的详细信息。 + +| |如果你的项目已经使用 ThymeLeaf 作为其模板引擎,那么 Eureka 服务器的 Freemarker 模板可能无法正确加载。在这种情况下,需要手动配置模板加载器:| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +application.yml + +``` +spring: + freemarker: + template-loader-path: classpath:/templates/ + prefer-file-system-access: false +``` + +### [](#spring-cloud-running-eureka-server)[2.2.如何运行 Eureka 服务器](#spring-cloud-running-eureka-server) ### + +下面的示例展示了一个最小的 Eureka 服务器: + +``` +@SpringBootApplication +@EnableEurekaServer +public class Application { + + public static void main(String[] args) { + new SpringApplicationBuilder(Application.class).web(true).run(args); + } + +} +``` + +该服务器有一个主页,在`/eureka/*`下具有正常 Eureka 功能的 UI 和 HTTP API 端点。 + +以下链接有一些 Eureka 背景读数:[flux capacitor](https://github.com/cfregly/fluxcapacitor/wiki/NetflixOSS-FAQ#eureka-service-discovery-load-balancer)和[谷歌小组讨论](https://groups.google.com/forum/?fromgroups#!topic/eureka_netflix/g3p2r7gHnN0)。 + +| |由于 Gradle 的依赖关系解析规则和缺乏父 BOM 功能,依赖于`spring-cloud-starter-netflix-eureka-server`可能会导致应用程序启动失败。
来解决此问题,添加 Spring 引导 Gradle 插件并导入 Spring Cloud Starter 父 bom 如下:

build. Gradle

``
buildscript{
依赖关系{<
Classpath(“org.springframework.: Spring-boot- Gradle-plugin:<< Spring-gt-gt-DOCS-version r=”177“/><>r=”>“>”><<<<<<178">r=”/>">>>>>><<| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#spring-cloud-eureka-server-zones-and-regions)[2.3.高可用性、区域和区域](#spring-cloud-eureka-server-zones-and-regions) ### + +Eureka 服务器没有后端存储,但是注册表中的服务实例都必须发送心跳以保持其注册的最新状态(因此可以在内存中完成)。客户机还具有 Eureka 注册的内存缓存(因此,对于服务的每一个请求,客户机都不需要访问注册表)。 + +默认情况下,每个 Eureka 服务器也是一个 Eureka 客户机,并且需要(至少一个)服务 URL 来定位对等方。如果你不提供它,那么该服务就会运行并正常工作,但是它会在你的日志中充满大量关于无法向对等方注册的噪音。 + +### [](#spring-cloud-eureka-server-standalone-mode)[2.4.独立模式](#spring-cloud-eureka-server-standalone-mode) ### + +这两个缓存(客户机和服务器)和心跳的组合使独立的 Eureka 服务器能够相当好地抵御故障,只要有某种监视器或弹性运行时(例如 Cloud Foundry)来保持它的活力。在独立模式下,你可能更喜欢关闭客户端行为,这样它就不会继续尝试而无法与其对等方联系。下面的示例展示了如何关闭客户端行为: + +application.yml(独立的 Eureka 服务器) + +``` +server: + port: 8761 + +eureka: + instance: + hostname: localhost + client: + registerWithEureka: false + fetchRegistry: false + serviceUrl: + defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ +``` + +请注意,`serviceUrl`指向与本地实例相同的主机。 + +### [](#spring-cloud-eureka-server-peer-awareness)[2.5.同伴意识](#spring-cloud-eureka-server-peer-awareness) ### + +通过运行多个实例并要求它们彼此注册,可以使 Eureka 更具弹性和可用性。实际上,这是默认的行为,因此要使其工作,你所需要做的就是向对等方添加一个有效的`serviceUrl`,如下面的示例所示: + +application.yml(两个对等的 Eureka 服务器) + +``` +--- +spring: + profiles: peer1 +eureka: + instance: + hostname: peer1 + client: + serviceUrl: + defaultZone: https://peer2/eureka/ + +--- +spring: + profiles: peer2 +eureka: + instance: + hostname: peer2 + client: + serviceUrl: + defaultZone: https://peer1/eureka/ +``` + +在前面的示例中,我们有一个 YAML 文件,通过在不同的 Spring 配置文件中运行它,可以使用该文件在两台主机(`Peer1’和)上运行相同的服务器。通过操纵`/etc/hosts`来解析主机名,你可以使用此配置来测试单个主机上的对等感知(在生产中这样做没有太大价值)。实际上,如果你在知道自己的主机名的机器上运行`eureka.instance.hostname`,则不需要`eureka.instance.hostname`(默认情况下,通过使用`java.net.InetAddress`查找)。 + +你可以向系统中添加多个对等节点,并且,只要它们都通过至少一个边缘彼此连接,它们就会在它们之间同步注册。如果对等点在物理上是分开的(在一个数据中心内部或多个数据中心之间),那么原则上,系统可以经受住“分裂大脑”式的故障。你可以将多个对等节点添加到系统中,并且只要它们都直接连接到彼此,它们就会在它们之间同步注册。 + +应用程序.yml(三个对等的 Eureka 服务器) + +``` +eureka: + client: + serviceUrl: + defaultZone: https://peer1/eureka/,http://peer2/eureka/,http://peer3/eureka/ + +--- +spring: + profiles: peer1 +eureka: + instance: + hostname: peer1 + +--- +spring: + profiles: peer2 +eureka: + instance: + hostname: peer2 + +--- +spring: + profiles: peer3 +eureka: + instance: + hostname: peer3 +``` + +### [](#spring-cloud-eureka-server-prefer-ip-address)[2.6.何时选择 IP 地址](#spring-cloud-eureka-server-prefer-ip-address) ### + +在某些情况下,对 Eureka 来说,更好的方法是宣传服务的 IP 地址,而不是主机名。将`eureka.instance.preferIpAddress`设置为`true`,并且,当应用程序向 Eureka 注册时,它使用其 IP 地址而不是其主机名。 + +| |如果主机名不能由 Java 确定,则将 IP 地址发送到 Eureka。
设置主机名的唯一方法是通过设置`eureka.instance.hostname`属性。
你可以在运行时使用环境变量设置你的主机名——例如,`eureka.instance.hostname=${HOST_NAME}`。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#securing-the-eureka-server)[2.7.保护 Eureka 服务器](#securing-the-eureka-server) ### + +你只需通过`spring-boot-starter-security`将 Spring 安全性添加到你的服务器的 Classpath,就可以保护你的 Eureka 服务器。默认情况下,当 Spring 安全性位于 Classpath 上时,它将要求在向应用程序发送每个请求时都发送一个有效的 CSRF 令牌。Eureka 客户机通常不会拥有有效的跨站点请求伪造令牌,你将需要为`/eureka/**`端点禁用此要求。例如: + +``` +@EnableWebSecurity +class WebSecurityConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.csrf().ignoringAntMatchers("/eureka/**"); + super.configure(http); + } +} +``` + +有关 CSRF 的更多信息,请参见[Spring Security documentation](https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#csrf)。 + +可以在 Spring 云样本[repo](https://github.com/spring-cloud-samples/eureka/tree/Eureka-With-Security)中找到演示 Eureka 服务器。 + +### [](#jdk-11-support)[2.8.JDK11 支援](#jdk-11-support) ### + +在 JDK11 中删除了 Eureka 服务器所依赖的 JAXB 模块。如果打算在运行 Eureka 服务器时使用 JDK11,则必须在 POM 或 Gradle 文件中包含这些依赖关系。 + +``` + + org.glassfish.jaxb + jaxb-runtime + +``` + +[](#configuration-properties)[3.配置属性](#configuration-properties) +---------- + +要查看所有 Spring Cloud Netflix 相关配置属性的列表,请检查[附录页](appendix.html)。 diff --git a/docs/spring-cloud/spring-cloud-openfeign.md b/docs/spring-cloud/spring-cloud-openfeign.md new file mode 100644 index 0000000000000000000000000000000000000000..c32bc1f5e8f6d4ebc3c85603a52d0a6394af795f --- /dev/null +++ b/docs/spring-cloud/spring-cloud-openfeign.md @@ -0,0 +1,735 @@ +Spring 云 OpenFeign +========== + +该项目通过自动配置和绑定到 Spring 环境和其他 Spring 编程模型习惯用法,为 Spring 引导应用程序提供 OpenFeign 集成。 + +[](#spring-cloud-feign)[1.声明式 REST 客户端:假装](#spring-cloud-feign) +---------- + +[Feign](https://github.com/OpenFeign/feign)是一个声明性 Web 服务客户机。它使编写 Web 服务客户机变得更加容易。要使用 feign 创建一个界面并对其进行注释。它具有可插入的注释支持,包括伪装注释和 JAX-RS 注释。Feign 还支持可插拔的编码器和解码器。 Spring Cloud 增加了对 Spring MVC 注释的支持,并支持使用与 Spring Web 中默认使用的`HttpMessageConverters`相同的`HttpMessageConverters`。 Spring Cloud 集成了 Eureka、 Spring Cloud Circuitbreaker 以及 Spring Cloud LoadBalancer,以在使用 Feign 时提供负载平衡的 HTTP 客户端。 + +### [](#netflix-feign-starter)[1.1.如何包含佯装](#netflix-feign-starter) ### + +要在项目中包含假动作,请使用组合`org.springframework.cloud`和工件 ID`spring-cloud-starter-openfeign`的启动器。请参阅[Spring Cloud Project page](https://projects.spring.io/spring-cloud/),以获取有关使用当前 Spring 云发布系列设置构建系统的详细信息。 + +示例 Spring 启动应用程序 + +``` +@SpringBootApplication +@EnableFeignClients +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} +``` + +Storeclient.java + +``` +@FeignClient("stores") +public interface StoreClient { + @RequestMapping(method = RequestMethod.GET, value = "/stores") + List getStores(); + + @RequestMapping(method = RequestMethod.GET, value = "/stores") + Page getStores(Pageable pageable); + + @RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json") + Store update(@PathVariable("storeId") Long storeId, Store store); + + @RequestMapping(method = RequestMethod.DELETE, value = "/stores/{storeId:\\d+}") + void delete(@PathVariable Long storeId); +} +``` + +在`@FeignClient`注释中,字符串值(上面的“存储”)是一个任意的客户端名称,用于创建[Spring Cloud LoadBalancer client](https://github.com/spring-cloud/spring-cloud-commons/blob/main/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/blocking/client/BlockingLoadBalancerClient.java)。你还可以使用`url`属性(绝对值或只是一个主机名)指定一个 URL。应用程序上下文中 Bean 的名称是接口的完全限定名称。要指定你自己的别名值,你可以使用`qualifiers`注释的`@FeignClient`值。 + +上面的负载平衡器客户机将希望发现“存储”服务的物理地址。如果你的应用程序是 Eureka 客户机,那么它将解析 Eureka 服务注册中心中的服务。如果不想使用 Eureka,可以使用[“简单发现”](https://docs.spring.io/spring-cloud-commons/docs/current/reference/html/#simplediscoveryclient)在外部配置中配置服务器列表。 + +Spring Cloud OpenFeign 支持用于 Spring Cloud LoadBalancer 的阻塞模式的所有可用功能。你可以在[项目文件](https://docs.spring.io/spring-cloud-commons/docs/current/reference/html/#spring-cloud-loadbalancer)中阅读有关它们的更多信息。 + +| |要在`@EnableFeignClients`上使用`@Configuration`-annotated-classes 注释,请确保指定客户机的位置,例如:@enableFeignclients(basepackages=“com.example.clients”),或显式列出它们:@enableFeignclients(clients=inventoryserviceFeignclient.class)| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#spring-cloud-feign-overriding-defaults)[1.2.覆盖假装默认值](#spring-cloud-feign-overriding-defaults) ### + +Spring Cloud 的假装支持中的一个核心概念是命名客户端。每个假客户机都是组件集合的一部分,这些组件组合在一起工作,以便按需联系远程服务器,并且该集合有一个名称,你可以使用`@FeignClient`注释将其命名为应用程序开发人员。 Spring 云使用`FeignClientsConfiguration`按需为每个命名的客户机创建一个新的集成,作为 `ApplicationContext’。其中包括`feign.Decoder`、`feign.Encoder`和`feign.Contract`。通过使用`@FeignClient`注释的`contextId`属性,可以覆盖该集合的名称。 + +Spring Cloud 允许你通过使用`@FeignClient`声明附加配置(在`FeignClientsConfiguration`之上)来完全控制假客户端。示例: + +``` +@FeignClient(name = "stores", configuration = FooConfiguration.class) +public interface StoreClient { + //.. +} +``` + +在这种情况下,客户机是由已经在`FeignClientsConfiguration`中的组件以及`FooConfiguration`中的任何组件组成的(后者将覆盖前者)。 + +| |`FooConfiguration`不需要用`@Configuration`进行注释。但是,如果是,那么请注意将它从任何`@ComponentScan`中排除,否则将包括此配置,因为当指定时,它将成为`feign.Decoder`、`feign.Encoder`、`feign.Contract`等的默认源。这可以通过将其放在一个单独的、不重叠的包中来避免,而不是使用`@ComponentScan`或`@SpringBootApplication`,或者可以在`@ComponentScan`中显式地排除它。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |使用`contextId``@FeignClient`注释的`contextId`属性,除了更改
的名称外,还将覆盖客户端名称
的别名,并将其用作为该客户端创建的配置 Bean 名称的一部分。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |以前,使用`url`属性时,不需要`name`属性。现在需要使用`name`。| +|---|----------------------------------------------------------------------------------------------------------| + +在`name`和`url`属性中支持占位符。 + +``` +@FeignClient(name = "${feign.name}", url = "${feign.url}") +public interface StoreClient { + //.. +} +``` + +Spring Cloud OpenFeign 默认情况下为 Feign 提供以下 bean(`beantype`beanname:`ClassName`): + +* `Decoder`伪译码:`ResponseEntityDecoder`(其中包含一个`SpringDecoder`) + +* `Encoder`伪编码器:`SpringEncoder` + +* `Logger`伪装者:`Slf4jLogger` + +* `MicrometerCapability`MicrometerCapability:如果`feign-micrometer`在 Classpath 上并且`MeterRegistry`是可用的 + +* `CachingCapability`缓存能力:如果使用`@EnableCaching`注释。可以通过`feign.cache.enabled`禁用。 + +* `Contract`假装合同:`SpringMvcContract` + +* `Feign.Builder`FeignBuilder:`FeignCircuitBreaker.Builder` + +* `Client`FeignClient:如果 Spring Cloud LoadBalancer 在 Classpath 上,则使用`FeignBlockingLoadBalancerClient`。如果它们都不在 Classpath 上,则使用默认的假装客户端。 + +| |`spring-cloud-starter-openfeign`支持`spring-cloud-starter-loadbalancer`。然而,作为一个可选的依赖项,如果你想使用它,你需要确保它已被添加到你的项目中。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +通过将`feign.okhttp.enabled`或`feign.httpclient.enabled`或`feign.httpclient.hc5.enabled`分别设置为`true`,并将其设置在 Classpath 上,可以使用 OkHtttpClient 和 ApacheHttpClient 和 ApachehtpClient5 假客户端。你可以通过在使用 Apache 时提供`org.apache.http.impl.client.CloseableHttpClient`的 Bean 或在使用 OK HTTP 时提供`okhttp3.OkHttpClient`或在使用 Apache HC5 时提供`org.apache.hc.client5.http.impl.classic.CloseableHttpClient`的 Bean 来定制所使用的 HTTP 客户端。 + +Spring Cloud OpenFeign*不是*默认情况下提供以下 bean 用于伪装,但仍然从应用程序上下文中查找这些类型的 bean 以创建伪装客户端: + +* `Logger.Level` + +* `Retryer` + +* `ErrorDecoder` + +* `Request.Options` + +* `Collection` + +* `SetterFactory` + +* `QueryMapEncoder` + +* `Capability`(默认情况下提供 `micrometerability’和`CachingCapability`) + +默认情况下创建了类型`Retryer.NEVER_RETRY`的`Retryer`的 Bean,这将禁用重试。请注意,这种重试行为与假装默认的行为不同,在这种情况下,它会自动重试 ioExceptions,将它们视为与网络相关的瞬态异常,以及从错误解码器中抛出的任何 RetryableException。 + +创建其中一种类型的 Bean,并将其放置在`@FeignClient`配置中(例如上面的`FooConfiguration`),这样就可以覆盖所描述的每个 bean。示例: + +``` +@Configuration +public class FooConfiguration { + @Bean + public Contract feignContract() { + return new feign.Contract.Default(); + } + + @Bean + public BasicAuthRequestInterceptor basicAuthRequestInterceptor() { + return new BasicAuthRequestInterceptor("user", "password"); + } +} +``` + +这将`SpringMvcContract`替换为`feign.Contract.Default`,并将`RequestInterceptor`添加到`RequestInterceptor`的集合中。 + +`@FeignClient`还可以使用配置属性进行配置。 + +应用程序.yml + +``` +feign: + client: + config: + feignName: + connectTimeout: 5000 + readTimeout: 5000 + loggerLevel: full + errorDecoder: com.example.SimpleErrorDecoder + retryer: com.example.SimpleRetryer + defaultQueryParameters: + query: queryValue + defaultRequestHeaders: + header: headerValue + requestInterceptors: + - com.example.FooRequestInterceptor + - com.example.BarRequestInterceptor + decode404: false + encoder: com.example.SimpleEncoder + decoder: com.example.SimpleDecoder + contract: com.example.SimpleContract + capabilities: + - com.example.FooCapability + - com.example.BarCapability + queryMapEncoder: com.example.SimpleQueryMapEncoder + metrics.enabled: false +``` + +默认配置可以在`@EnableFeignClients`属性`defaultConfiguration`中以与上述类似的方式指定。不同之处在于,此配置将应用于*全部*假客户机。 + +如果你更喜欢使用配置属性来配置所有`@FeignClient`,那么你可以使用`default`假名创建配置属性。 + +你可以使用`feign.client.config.feignName.defaultQueryParameters`和`feign.client.config.feignName.defaultRequestHeaders`来指定查询参数和标题,这些参数和标题将与名为`feignName`的客户机的每个请求一起发送。 + +应用程序.yml + +``` +feign: + client: + config: + default: + connectTimeout: 5000 + readTimeout: 5000 + loggerLevel: basic +``` + +如果我们同时创建`@Configuration` Bean 和配置属性,配置属性将会胜出。它将覆盖`@Configuration`值。但如果要将优先级更改为`@Configuration`,则可以将`feign.client.default-to-properties`更改为`false`。 + +如果我们想要创建多个具有相同名称或 URL 的假客户机,那么它们将指向相同的服务器,但每个服务器具有不同的自定义配置,那么我们必须使用`contextId`属性的`@FeignClient`,以避免这些配置 bean 的名称冲突。 + +``` +@FeignClient(contextId = "fooClient", name = "stores", configuration = FooConfiguration.class) +public interface FooClient { + //.. +} +``` + +``` +@FeignClient(contextId = "barClient", name = "stores", configuration = BarConfiguration.class) +public interface BarClient { + //.. +} +``` + +也可以将 FeignClient 配置为不从父上下文继承 bean。你可以通过覆盖`FeignClientConfigurer` Bean 中的`inheritParentConfiguration()`来返回`false`: + +``` +@Configuration +public class CustomConfiguration{ + +@Bean +public FeignClientConfigurer feignClientConfigurer() { + return new FeignClientConfigurer() { + + @Override + public boolean inheritParentConfiguration() { + return false; + } + }; + + } +} +``` + +| |默认情况下,假客户端不对斜杠`/`字符进行编码。可以通过将`feign.client.decodeSlash`的值设置为`false`来更改此行为。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#springencoder-configuration)[1.2.1. `SpringEncoder` configuration](#springencoder-configuration) #### + +在我们提供的`SpringEncoder`中,我们为二进制内容类型设置`null`字符集,为所有其他类型设置`UTF-8`字符集。 + +通过将`feign.encoder.charset-from-content-type`的值设置为`true`,可以修改此行为以从`Content-Type`头字符集派生字符集。 + +### [](#timeout-handling)[1.3.超时处理](#timeout-handling) ### + +我们可以在默认值和指定的客户机上配置超时。OpenFeign 使用两个超时参数: + +* `connectTimeout`可以防止由于服务器处理时间过长而阻塞调用方。 + +* `readTimeout`从建立连接的时间开始应用,并在返回响应花费太长时间时触发。 + +| |在服务器不运行或可用的情况下,数据包将导致*连接被拒绝*。通信以错误消息或回退结束。这可能发生*在此之前*如果`connectTimeout`设置得很低。执行查找和接收这样的数据包所需的时间造成了该延迟的很大一部分。它可能会根据涉及 DNS 查找的远程主机进行更改。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#creating-feign-clients-manually)[1.4.手动创建假客户端](#creating-feign-clients-manually) ### + +在某些情况下,可能有必要以一种不可能使用上述方法的方式定制你的假装客户机。在这种情况下,你可以使用[Feign Builder API](https://github.com/OpenFeign/feign/#basics)创建客户机。下面是一个示例,该示例使用相同的接口创建两个假客户机,但将每个客户机配置为单独的请求拦截器。 + +``` +@Import(FeignClientsConfiguration.class) +class FooController { + + private FooClient fooClient; + + private FooClient adminClient; + + @Autowired + public FooController(Client client, Encoder encoder, Decoder decoder, Contract contract, MicrometerCapability micrometerCapability) { + this.fooClient = Feign.builder().client(client) + .encoder(encoder) + .decoder(decoder) + .contract(contract) + .addCapability(micrometerCapability) + .requestInterceptor(new BasicAuthRequestInterceptor("user", "user")) + .target(FooClient.class, "https://PROD-SVC"); + + this.adminClient = Feign.builder().client(client) + .encoder(encoder) + .decoder(decoder) + .contract(contract) + .addCapability(micrometerCapability) + .requestInterceptor(new BasicAuthRequestInterceptor("admin", "admin")) + .target(FooClient.class, "https://PROD-SVC"); + } +} +``` + +| |在上面的示例中`FeignClientsConfiguration.class`是由 Spring Cloud OpenFeign 提供的默认配置
。| +|---|---------------------------------------------------------------------------------------------------------------------------| + +| |`PROD-SVC`是客户机将向其发出请求的服务的名称。| +|---|-----------------------------------------------------------------------------| + +| |feign`Contract`对象定义了哪些注释和值在接口上是有效的。
autowired`Contract` Bean 提供了对 SpringMVC 注释的支持,而不是
默认的伪装原生注释。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +你还可以使用`Builder`来配置 FeignClient,使其不从父上下文继承 bean。你可以通过在`Builder`上重写调用`inheritParentContext(false)`来实现此目的。 + +### [](#spring-cloud-feign-circuitbreaker)[1.5. Feign Spring Cloud CircuitBreaker Support](#spring-cloud-feign-circuitbreaker) ### + +如果 Spring 云电路断路器在 Classpath 和`feign.circuitbreaker.enabled=true`上,Feign 将用一个断路器封装所有方法。 + +要在每个客户端的基础上禁用 Spring 云电路断路器支持,请使用“原型”范围创建一个普通的`Feign.Builder`,例如: + +``` +@Configuration +public class FooConfiguration { + @Bean + @Scope("prototype") + public Feign.Builder feignBuilder() { + return Feign.builder(); + } +} +``` + +断路器名称遵循此模式`#()`。当调用带有`FooClient`接口的`@FeignClient`时,所调用的接口方法没有参数是`bar`,那么断路器的名称将是`FooClient#bar()`。 + +| |截至 2020.0.2,断路器名称模式已从`_`变为
使用 2020.0.4 引入的`CircuitBreakerNameResolver`,断路器名称可以保留旧模式。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +提供 Bean 的`CircuitBreakerNameResolver`,可以更改断路器名称模式。 + +``` +@Configuration +public class FooConfiguration { + @Bean + public CircuitBreakerNameResolver circuitBreakerNameResolver() { + return (String feignClientName, Target target, Method method) -> feignClientName + "_" + method.getName(); + } +} +``` + +要启用 Spring Cloud Circuitbreaker 组,将`feign.circuitbreaker.group.enabled`属性设置为`true`(默认情况下`false`)。 + +### [](#spring-cloud-feign-circuitbreaker-fallback)[1.6. Feign Spring Cloud CircuitBreaker Fallbacks](#spring-cloud-feign-circuitbreaker-fallback) ### + +Spring 云电路断路器支持回退的概念:当电路打开或存在错误时执行的默认代码路径。要为给定的`@FeignClient`启用回退,请将`fallback`属性设置为实现回退的类名。你还需要将你的实现声明为 Spring Bean。 + +``` +@FeignClient(name = "test", url = "http://localhost:${server.port}/", fallback = Fallback.class) + protected interface TestClient { + + @RequestMapping(method = RequestMethod.GET, value = "/hello") + Hello getHello(); + + @RequestMapping(method = RequestMethod.GET, value = "/hellonotfound") + String getException(); + + } + + @Component + static class Fallback implements TestClient { + + @Override + public Hello getHello() { + throw new NoFallbackAvailableException("Boom!", new RuntimeException()); + } + + @Override + public String getException() { + return "Fixed response"; + } + + } +``` + +如果你需要访问触发回退的原因,则可以在`@FeignClient`中使用`fallbackFactory`属性。 + +``` +@FeignClient(name = "testClientWithFactory", url = "http://localhost:${server.port}/", + fallbackFactory = TestFallbackFactory.class) + protected interface TestClientWithFactory { + + @RequestMapping(method = RequestMethod.GET, value = "/hello") + Hello getHello(); + + @RequestMapping(method = RequestMethod.GET, value = "/hellonotfound") + String getException(); + + } + + @Component + static class TestFallbackFactory implements FallbackFactory { + + @Override + public FallbackWithFactory create(Throwable cause) { + return new FallbackWithFactory(); + } + + } + + static class FallbackWithFactory implements TestClientWithFactory { + + @Override + public Hello getHello() { + throw new NoFallbackAvailableException("Boom!", new RuntimeException()); + } + + @Override + public String getException() { + return "Fixed response"; + } + + } +``` + +### [](#feign-and-primary)[1.7. Feign and `@Primary`](#feign-and-primary) ### + +当使用 Feign with Spring Cloud Circuitbreaker 回退时,在`ApplicationContext`中有多个相同类型的 bean。这将导致`@Autowired`不工作,因为没有一个 Bean,或一个标记为主要的。为了解决这个问题, Spring Cloud OpenFeign 将所有的 Feign 实例标记为`@Primary`,因此 Spring Framework 将知道要注入哪个 Bean。在某些情况下,这可能是不可取的。要关闭此行为,请将`@FeignClient`的`primary`属性设置为 false。 + +``` +@FeignClient(name = "hello", primary = false) +public interface HelloClient { + // methods here +} +``` + +### [](#spring-cloud-feign-inheritance)[1.8.假装继承支持](#spring-cloud-feign-inheritance) ### + +Feign 通过单继承接口支持样板 API。这允许将公共操作分组到方便的基本接口中。 + +userservice.java + +``` +public interface UserService { + + @RequestMapping(method = RequestMethod.GET, value ="/users/{id}") + User getUser(@PathVariable("id") long id); +} +``` + +userresource.java + +``` +@RestController +public class UserResource implements UserService { + +} +``` + +userclient.java + +``` +package project.user; + +@FeignClient("users") +public interface UserClient extends UserService { + +} +``` + +| |`@FeignClient`接口不应在服务器和客户机之间共享,并且不再支持在类级别上注释带有`@RequestMapping`的`@FeignClient`接口。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#feign-requestresponse-compression)[1.9.假装请求/响应压缩](#feign-requestresponse-compression) ### + +你可以考虑为你的假请求启用请求或响应 gzip 压缩。你可以通过启用其中一个属性来实现这一点: + +``` +feign.compression.request.enabled=true +feign.compression.response.enabled=true +``` + +Feign Request Compression 为你提供与你为 Web 服务器设置的设置类似的设置: + +``` +feign.compression.request.enabled=true +feign.compression.request.mime-types=text/xml,application/xml,application/json +feign.compression.request.min-request-size=2048 +``` + +这些属性允许你对压缩媒体类型和最小请求阈值长度进行选择。 + +### [](#feign-logging)[1.10.伪测井](#feign-logging) ### + +为创建的每个假客户机创建一个记录器。默认情况下,记录器的名称是用于创建假客户端的接口的完整类名。假装日志记录只响应`DEBUG`级别。 + +应用程序.yml + +``` +logging.level.project.user.UserClient: DEBUG +``` + +你可以为每个客户机配置的`Logger.Level`对象告诉你要记录多少。选择如下: + +* `NONE`,没有日志记录(** 默认 **)。 + +* `BASIC`,只记录请求方法和 URL 以及响应状态代码和执行时间。 + +* `HEADERS`,记录基本信息以及请求和响应头。 + +* `FULL`,记录请求和响应的标题、主体和元数据。 + +例如,下面将`Logger.Level`设置为`FULL`: + +``` +@Configuration +public class FooConfiguration { + @Bean + Logger.Level feignLoggerLevel() { + return Logger.Level.FULL; + } +} +``` + +### [](#feign-capability-support)[1.11.佯装能力支持](#feign-capability-support) ### + +伪装功能公开了核心伪装组件,以便可以修改这些组件。例如,这些功能可以使用`Client`、*装饰*,并将修饰过的实例返回到 feign 中。对 Metrics 库的支持就是一个很好的实例。见[Feign metrics](#feign-metrics)。 + +创建一个或多个`Capability`bean 并将其放置在`@FeignClient`配置中,可以注册它们并修改所涉及的客户机的行为。 + +``` +@Configuration +public class FooConfiguration { + @Bean + Capability customCapability() { + return new CustomCapability(); + } +} +``` + +### [](#feign-metrics)[1.12.假装度量](#feign-metrics) ### + +如果以下所有条件均为真,则创建并注册一个`MicrometerCapability` Bean,以便你的假客户机将度量发布到 Micrometer: + +* `feign-micrometer`在 Classpath 上 + +* a`MeterRegistry` Bean 可用 + +* 假装度量属性设置为`true`(默认情况下) + + * `feign.metrics.enabled=true`(适用于所有客户) + + * `feign.client.config.feignName.metrics.enabled=true`(对于单个客户端) + +| |如果你的应用程序已经使用了 Micrometer,那么启用度量就像将`feign-micrometer`放在 Classpath 上一样简单。| +|---|-----------------------------------------------------------------------------------------------------------------------------| + +你还可以通过以下方式禁用该功能: + +* 从你的 Classpath 中排除`feign-micrometer` + +* 将一个假的度量属性设置为`false` + + * `feign.metrics.enabled=false` + + * `feign.client.config.feignName.metrics.enabled=false` + +| |`feign.metrics.enabled=false`禁用对**全部**冒充客户端的度量支持,而不管客户端级别标志的值是多少:`feign.client.config.feignName.metrics.enabled`。
如果你想在每个客户端启用或禁用 Merics,请不要设置`feign.metrics.enabled`,而使用`feign.client.config.feignName.metrics.enabled`。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +你还可以通过注册自己的 Bean 来自定义`MicrometerCapability`: + +``` +@Configuration +public class FooConfiguration { + @Bean + public MicrometerCapability micrometerCapability(MeterRegistry meterRegistry) { + return new MicrometerCapability(meterRegistry); + } +} +``` + +### [](#feign-caching)[1.13.假装缓存](#feign-caching) ### + +如果使用`@EnableCaching`注释,则创建并注册一个`CachingCapability` Bean,以便你的假客户端在其接口上识别`@Cache*`注释: + +``` +public interface DemoClient { + + @GetMapping("/demo/{filterParam}") + @Cacheable(cacheNames = "demo-cache", key = "#keyParam") + String demoEndpoint(String keyParam, @PathVariable String filterParam); +} +``` + +你还可以通过属性`feign.cache.enabled=false`禁用该功能。 + +### [](#feign-querymap-support)[1.14.假装 @QueryMap 支持](#feign-querymap-support) ### + +OpenFeign`@QueryMap`注释支持将 POJO 用作 GET 参数映射。遗憾的是,缺省的 OpenFeign QueryMap 注释与 Spring 不兼容,因为它缺少`value`属性。 + +Spring Cloud OpenFeign 提供了一个等效的`@SpringQueryMap`注释,其用于将一个 POJO 或 MAP 参数注释为查询参数映射。 + +例如,`Params`类定义了参数`param1`和`param2`: + +``` +// Params.java +public class Params { + private String param1; + private String param2; + + // [Getters and setters omitted for brevity] +} +``` + +下面的假客户机通过使用`@SpringQueryMap`注释来使用`Params`类: + +``` +@FeignClient("demo") +public interface DemoTemplate { + + @GetMapping(path = "/demo") + String demoEndpoint(@SpringQueryMap Params params); +} +``` + +如果需要对生成的查询参数映射进行更多控制,则可以实现自定义的`QueryMapEncoder` Bean。 + +### [](#hateoas-support)[1.15.Hateoas 支持](#hateoas-support) ### + +Spring 提供了一些 API 来创建遵循[HATEOAS](https://en.wikipedia.org/wiki/HATEOAS)原则、[Spring Hateoas](https://spring.io/projects/spring-hateoas)和[Spring Data REST](https://spring.io/projects/spring-data-rest)的 REST 表示。 + +如果你的项目使用`org.springframework.boot:spring-boot-starter-hateoas`starter 或`org.springframework.boot:spring-boot-starter-data-rest`starter,则默认情况下会启用 feignhateoas 支持。 + +当启用 Hateoas 支持时,允许 Feign 客户端序列化和反序列化 Hateoas 表示模型:[EntityModel](https://docs.spring.io/spring-hateoas/docs/1.0.0.M1/apidocs/org/springframework/hateoas/EntityModel.html),[CollectionModel](https://docs.spring.io/spring-hateoas/docs/1.0.0.M1/apidocs/org/springframework/hateoas/CollectionModel.html)和[PagedModel](https://docs.spring.io/spring-hateoas/docs/1.0.0.M1/apidocs/org/springframework/hateoas/PagedModel.html)。 + +``` +@FeignClient("demo") +public interface DemoTemplate { + + @GetMapping(path = "/stores") + CollectionModel getStores(); +} +``` + +### [](#spring-matrixvariable-support)[1.16. Spring @MatrixVariable Support](#spring-matrixvariable-support) ### + +Spring Cloud OpenFeign 提供了对 Spring `@MatrixVariable`注释的支持。 + +如果将映射作为方法参数传递,则通过将映射中的键值对与`=`连接起来来创建`@MatrixVariable`路径段。 + +如果传递了一个不同的对象,那么`@MatrixVariable`注释(如果已定义)中提供的`name`或者注释的变量名将使用`=`与提供的方法参数连接。 + +IMPORTANT + +尽管如此,在服务器端, Spring 并不要求用户将路径段占位符的名称与矩阵变量的名称相同,因为在客户端,该名称将过于模棱两可, Spring 云 OpenFeign 要求你添加一个路径段占位符,其名称与`@MatrixVariable`注释(如果已定义)中提供的`name`或注释的变量名称相匹配。 + +例如: + +``` +@GetMapping("/objects/links/{matrixVars}") +Map> getObjects(@MatrixVariable Map> matrixVars); +``` + +注意,变量名和路径段占位符都被称为`matrixVars`。 + +``` +@FeignClient("demo") +public interface DemoTemplate { + + @GetMapping(path = "/stores") + CollectionModel getStores(); +} +``` + +### [](#feign-collectionformat-support)[1.17. Feign `CollectionFormat` support](#feign-collectionformat-support) ### + +我们通过提供`@CollectionFormat`注释来支持`feign.CollectionFormat`。你可以通过传递所需的`feign.CollectionFormat`作为注释值,用它来对假客户机方法(或整个类进行注释以影响所有方法)进行注释。 + +在下面的示例中,使用`CSV`格式而不是默认的`EXPLODED`来处理该方法。 + +``` +@FeignClient(name = "demo") +protected interface PageableFeignClient { + + @CollectionFormat(feign.CollectionFormat.CSV) + @GetMapping(path = "/page") + ResponseEntity performRequest(Pageable page); + +} +``` + +| |在发送`Pageable`作为查询参数时,设置`CSV`格式,以便对其进行正确编码。| +|---|-----------------------------------------------------------------------------------------------------------| + +### [](#reactive-support)[1.18.反应性支持](#reactive-support) ### + +由于[OpenFeign 项目](https://github.com/OpenFeign/feign)目前不支持反应式客户端,例如[Spring WebClient](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/reactive/function/client/WebClient.html), Spring Cloud OpenFeign 也不支持。我们将在此添加对它的支持,只要它在核心项目中可用。 + +在此之前,我们建议使用[feign-reactive](https://github.com/Playtika/feign-reactive)作为 Spring WebClient 支持。 + +#### [](#early-initialization-errors)[1.18.1.早期初始化错误](#early-initialization-errors) #### + +在启动应用程序时,你可能会看到初始化错误,这取决于你如何使用你的假客户机。要解决这个问题,你可以在自动连接客户机时使用`ObjectProvider`。 + +``` +@Autowired +ObjectProvider testFeignClient; +``` + +### [](#spring-data-support)[1.19. Spring Data Support](#spring-data-support) ### + +可以考虑启用用于支持`org.springframework.data.domain.Page`和`org.springframework.data.domain.Sort`解码的 Jackson 模块。 + +``` +feign.autoconfiguration.jackson.enabled=true +``` + +### [](#spring-refreshscope-support)[1.20. Spring `@RefreshScope` Support](#spring-refreshscope-support) ### + +如果启用了假客户端刷新,那么每个假客户端都是以`feign.Request.Options`作为刷新范围来创建的 Bean。这意味着诸如`connectTimeout`和`readTimeout`之类的属性可以通过`POST /actuator/refresh`针对任何伪装客户端实例进行刷新。 + +默认情况下,feign 客户端中的刷新行为是禁用的。使用以下属性启用刷新行为: + +``` +feign.client.refresh-enabled=true +``` + +| |不要使用`@RefreshScope`注释对`@FeignClient`接口进行注释。| +|---|---------------------------------------------------------------------------------| + +### [](#oauth2-support)[1.21.OAuth2 支持](#oauth2-support) ### + +可以通过设置以下标志来启用 OAuth2 支持: + +``` +feign.oauth2.enabled=true +``` + +当标志设置为 true,并且出现了 OAuth2 客户机上下文资源详细信息时,将创建 Bean 类`OAuth2FeignRequestInterceptor`。在每个请求之前,拦截器解析所需的访问令牌,并将其作为报头。有时,当为假装客户机启用负载平衡时,你可能也希望使用负载平衡来获取访问令牌。为此,你应该确保负载平衡器位于 Classpath( Spring-cloud-starter-loadbalancer)上,并通过设置以下标志显式地启用 OAuth2FeignRequestInterceptor 的负载平衡: + +``` +feign.oauth2.load-balanced=true +``` + +[](#configuration-properties)[2.配置属性](#configuration-properties) +---------- + +要查看所有 Spring Cloud OpenFeign 相关配置属性的列表,请检查[附录页](appendix.html)。 diff --git a/docs/spring-cloud/spring-cloud-sleuth.md b/docs/spring-cloud/spring-cloud-sleuth.md new file mode 100644 index 0000000000000000000000000000000000000000..8b024e70f0976787885988e83ab3653183dd55a3 --- /dev/null +++ b/docs/spring-cloud/spring-cloud-sleuth.md @@ -0,0 +1,16 @@ +Spring 云侦探参考文献 +========== + +Adrian Cole,Spencer Gibb,Marcin Grzejszczak,DAVESyer,Jay Bryant + +参考文献包括以下部分: + +| [Legal](legal.html#legal) |法律信息。| +|--------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------| +|[Documentation Overview](documentation-overview.html#sleuth-documentation-about)|关于文档,获得帮助,第一步,等等。| +| [Getting Started](getting-started.html#getting-started) |介绍 Spring Cloud Sleuth,开发你的第一个 Spring 基于 Cloud Sleuth 的应用程序| +| [Using Spring Cloud Sleuth](using.html#using) |Spring 云侦探的使用示例和工作流程。| +| [Spring Cloud Sleuth Features](project-features.html#features) |跨越创建、上下文传播等。| +| [“How-to” Guides](howto.html#howto) |添加采样、传播远程标记等等。| +| [Spring Cloud Sleuth Integrations](integrations.html#sleuth-integration) |插装配置、上下文传播等。| +| [Appendices](appendix.html#appendix) |span 定义和配置属性。| diff --git a/docs/spring-cloud/spring-cloud-stream.md b/docs/spring-cloud/spring-cloud-stream.md new file mode 100644 index 0000000000000000000000000000000000000000..45569b6abdeb138977a7fe54b484c5f2b1326e18 --- /dev/null +++ b/docs/spring-cloud/spring-cloud-stream.md @@ -0,0 +1,21 @@ +Spring 云流参考文档 +========== + +**3.2.2** + +参考文献包括以下部分: + +|[Overview](spring-cloud-stream.html#spring-cloud-stream-reference)| History, Quick Start, Concepts, Architecture Overview, Binder Abstraction, and Core Features | +|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------| +|[Rabbit MQ 活页夹](https://docs.spring.io/spring-cloud-stream-binder-rabbit/docs/3.2.2/reference/html/spring-cloud-stream-binder-rabbit.html)| Spring Cloud Stream binder reference for Rabbit MQ | +|[Apache Kafka 活页夹](https://docs.spring.io/spring-cloud-stream-binder-kafka/docs/3.2.2/reference/html/spring-cloud-stream-binder-kafka.html#_apache_kafka_binder)| Spring Cloud Stream binder reference for Apache Kafka | +|[Apache Kafka Streams 活页夹](https://docs.spring.io/spring-cloud-stream-binder-kafka/docs/3.2.2/reference/html/spring-cloud-stream-binder-kafka.html#_kafka_streams_binder)| Spring Cloud Stream binder reference for Apache Kafka Streams | +|[附加粘合剂](binders.html#binders)|A collection of Partner maintained binder implementations for Spring Cloud Stream (e.g., Azure Event Hubs, Google PubSub, Solace PubSub+)| +|[Spring Cloud Stream Samples](https://github.com/spring-cloud/spring-cloud-stream-samples/)| A curated collection of repeatable Spring Cloud Stream samples to walk through the features | + +相关链接: + +|[Spring Cloud Data Flow](https://cloud.spring.io/spring-cloud-dataflow/)| Spring Cloud Data Flow | +|--------------------------------------------------------------------------------|------------------------------------------------------| +|[Enterprise 整合模式](http://www.enterpriseintegrationpatterns.com/)|Patterns and Best Practices for Enterprise Integration| +|[Spring Integration](https://spring.io/projects/spring-integration)| Spring Integration framework | diff --git a/docs/spring-cloud/spring-cloud-task.md b/docs/spring-cloud/spring-cloud-task.md new file mode 100644 index 0000000000000000000000000000000000000000..2920301f234860340887295de2a6b845912c2d01 --- /dev/null +++ b/docs/spring-cloud/spring-cloud-task.md @@ -0,0 +1,1087 @@ +Spring 云任务参考指南 +========== + + +[](#preface)[Preface](#preface) +========== + +本节提供了 Spring 云任务参考文档的简要概述。把它看作是文档其余部分的一张地图。你可以以线性方式阅读此参考指南,或者如果你对某些内容不感兴趣,可以跳过部分。 + +[](#about-the-documentation)[1.关于文档](#about-the-documentation) +---------- + +Spring 云任务参考指南在[html](https://docs.spring.io/spring-cloud-task/docs/current/reference)和[pdf](https://docs.spring.io/spring-cloud-task/docs/current/reference/index.pdf),[epub](https://docs.spring.io/spring-cloud-task/docs/current/reference/index.epub)中可用。最新版本可在[docs.spring.io/spring-cloud-task/docs/current-SNAPSHOT/reference/html/](https://docs.spring.io/spring-cloud-task/docs/current-SNAPSHOT/reference/html/)处获得。 + +本文件的副本可供你自己使用并分发给他人,但前提是你不对此类副本收取任何费用,并且还需每一份副本均包含本版权声明,无论是以印刷形式还是以电子方式分发。 + +[](#task-documentation-getting-help)[2. Getting help](#task-documentation-getting-help) +---------- + +云任务有问题吗?我们愿意提供帮助! + +* 问一个问题。我们监控[stackoverflow.com](https://stackoverflow.com)中带有[`spring-cloud-task`](https://stackoverflow.com/tags/spring-cloud-task)标记的问题。 + +* 使用 Spring 云任务在[github.com/spring-cloud/spring-cloud-task/issues](https://github.com/spring-cloud/spring-cloud-task/issues)上报告错误。 + +| |所有的云任务都是开源的,包括文档。如果你发现
是 DOCS 的问题,或者你只是想改进它们,请[get
involved](https://github.com/spring-cloud/spring-cloud-task/tree/master)。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[](#task-documentation-first-steps)[3. First Steps](#task-documentation-first-steps) +---------- + +如果你刚刚开始使用 Spring 云任务或一般的“ Spring”,我们建议你阅读[Getting started](#getting-started)章节。 + +要从头开始,请阅读以下部分: + +* [Introducing Spring Cloud Task](#getting-started-introducing-spring-cloud-task) + +* [系统要求](#getting-started-system-requirements) + +要遵循本教程,请阅读[Developing Your First Spring Cloud Task Application](#getting-started-developing-first-task)以运行你的示例,请阅读[运行示例](#getting-started-running-the-example) + +[](#getting-started)[Getting started](#getting-started) +========== + +如果你刚刚开始使用 Spring Cloud Task,那么你应该阅读这一部分。在这里,我们回答基本的“什么?”、“怎么做?”和“为什么?”的问题。我们从温和地介绍云任务开始。然后,我们构建了一个 Spring 云任务应用程序,讨论了一些核心原则。 + +[](#getting-started-introducing-spring-cloud-task)[4. Introducing Spring Cloud Task](#getting-started-introducing-spring-cloud-task) +---------- + +Spring 云任务使创建短期微服务变得容易。它提供了允许在生产环境中按需执行短期 JVM 流程的功能。 + +[](#getting-started-system-requirements)[5.系统要求](#getting-started-system-requirements) +---------- + +你需要安装 Java(Java8 或更好)。要进行构建,还需要安装 Maven。 + +### [](#database-requirements)[5.1.数据库需求](#database-requirements) ### + +Spring 云任务使用关系数据库来存储已执行任务的结果。虽然可以在没有数据库的情况下开始开发任务(任务的状态作为任务存储库更新的一部分记录),但对于生产环境,你希望使用受支持的数据库。 Spring 云任务当前支持以下数据库: + +* DB2 + +* H2 + +* HSQLDB + +* MySQL + +* 甲骨文 + +* Postgres + +* SQLServer + +[](#getting-started-developing-first-task)[6. Developing Your First Spring Cloud Task Application](#getting-started-developing-first-task) +---------- + +一个很好的起点是使用一个简单的“你好,世界!”应用程序,因此我们创建了相当于突出该框架功能的 Spring Cloud 任务。大多数 IDE 都对 Apache Maven 有很好的支持,因此我们将它用作这个项目的构建工具。 + +| |Spring.io 网站包含许多使用 Spring 引导的[“`Getting Started`”
guides](https://spring.io/guides)。如果你需要解决某个特定的问题,请先在此进行检查。
你可以通过执行[Spring Initializr](https://start.spring.io/)并创建一个新项目来快捷执行以下步骤。这样做
会自动生成一个新的项目结构,这样你就可以立即开始编码。
我们建议你尝试使用 Spring initializr 来熟悉它。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#getting-started-creating-project)[6.1. Creating the Spring Task Project using Spring Initializr](#getting-started-creating-project) ### + +现在,我们可以创建并测试一个将`Hello, World!`打印到控制台的应用程序。 + +这样做: + +1. 访问[Spring Initialzr](https://start.spring.io/)网站。 + + 1. 创建一个新的 Maven 项目,其**集团**的名称为`io.spring.demo`,而**人工制品**的名称为`helloworld`。 + + 2. 在“依赖关系”文本框中,键入`task`,然后选择`Cloud Task`依赖关系。 + + 3. 在“依赖项”文本框中,键入`jdbc`,然后选择`JDBC`依赖项。 + + 4. 在“依赖关系”文本框中,键入`h2`,然后选择`H2`。(或者你最喜欢的数据库) + + 5. 点击**生成项目**按钮 + +2. 解压 helloworld.zip 文件并将项目导入到你最喜欢的 IDE 中。 + +### [](#getting-started-writing-the-code)[6.2.编写代码](#getting-started-writing-the-code) ### + +要完成我们的应用程序,我们需要用以下内容更新生成的`HelloworldApplication`,以便它启动一个任务。 + +``` +package io.spring.demo.helloworld; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +@SpringBootApplication +@EnableTask +public class HelloworldApplication { + + @Bean + public CommandLineRunner commandLineRunner() { + return new HelloWorldCommandLineRunner(); + } + + public static void main(String[] args) { + SpringApplication.run(HelloworldApplication.class, args); + } + + public static class HelloWorldCommandLineRunner implements CommandLineRunner { + + @Override + public void run(String... strings) throws Exception { + System.out.println("Hello, World!"); + } + } +} +``` + +虽然它看起来很小,但相当多的事情正在发生。有关 Spring 引导细节的更多信息,请参见[Spring Boot reference documentation](https://docs.spring.io/spring-boot/docs/current/reference/html/)。 + +现在我们可以在`src/main/resources`中打开`application.properties`文件。我们需要在`application.properties`中配置两个属性: + +* `application.name`:设置应用程序名(已转换为任务名) + +* `logging.level`:将 Spring 云任务的日志设置为`DEBUG`,以便查看正在发生的事情。 + +下面的示例展示了如何同时做到这两点: + +``` +logging.level.org.springframework.cloud.task=DEBUG +spring.application.name=helloWorld +``` + +#### [](#getting-started-at-task)[6.2.1.任务自动配置](#getting-started-at-task) #### + +当包含 Spring Cloud Task Starter 依赖项时,Task Auto 会配置所有 bean 以引导其功能。此配置的一部分注册了`TaskRepository`及其使用的基础结构。 + +在我们的演示中,`TaskRepository`使用嵌入式 H2 数据库来记录任务的结果。这种 H2 嵌入式数据库对于生产环境不是一种实用的解决方案,因为一旦任务结束,H2DB 就会消失。然而,为了获得快速的入门体验,我们可以在示例中使用它,也可以将存储库中正在更新的内容与日志相呼应。在[Configuration](#features-configuration)小节(在本文档的后面)中,我们介绍了如何定制 Spring Cloud Task 提供的组件的配置。 + +当我们的示例应用程序运行时, Spring 启动我们的`HelloWorldCommandLineRunner`,并将我们的“你好,世界!”消息输出为标准输出。`TaskLifecycleListener`在存储库中记录任务的开始和结束。 + +#### [](#getting-started-main-method)[6.2.2.主要方法](#getting-started-main-method) #### + +Main 方法是任何 Java 应用程序的入口点。我们的主方法将委托给 Spring Boot 的[SpringApplication](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-spring-application.html)类。 + +#### [](#getting-started-clr)[6.2.3.The CommandlineRunner](#getting-started-clr) #### + +Spring 包括引导应用程序逻辑的许多方法。 Spring Boot 通过其`*Runner`接口(`CommandlineRunner` 或`ApplicationRunner`)以有组织的方式提供了一种方便的方法。一个表现良好的任务可以通过使用这两个运行器中的一个来引导任何逻辑。 + +任务的生命周期是从`*Runner#run`方法被执行到它们全部完成之前考虑的。 Spring 引导允许应用程序使用多个 `*runner’实现,就像 Spring 云任务一样。 + +| |除`CommandLineRunner`或 `ApplicationRunner’(例如,通过使用`InitializingBean#afterPropertiesSet`)以外的机制引导的任何处理都不是由 Spring 云任务记录的
。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#getting-started-running-the-example)[6.3.运行示例](#getting-started-running-the-example) ### + +在这一点上,我们的应用程序应该可以工作。由于此应用程序是基于 Spring 引导的,因此我们可以从应用程序的根使用`$ mvn spring-boot:run`从命令行运行它,如下例所示(其输出): + +``` +$ mvn clean spring-boot:run +....... . . . +....... . . . (Maven log output here) +....... . . . + + . ____ _ __ _ _ + /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ +( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ + \\/ ___)| |_)| | | | | || (_| | ) ) ) ) + ' |____| .__|_| |_|_| |_\__, | / / / / + =========|_|==============|___/=/_/_/_/ + :: Spring Boot :: (v2.0.3.RELEASE) + +2018-07-23 17:44:34.426 INFO 1978 --- [ main] i.s.d.helloworld.HelloworldApplication : Starting HelloworldApplication on Glenns-MBP-2.attlocal.net with PID 1978 (/Users/glennrenfro/project/helloworld/target/classes started by glennrenfro in /Users/glennrenfro/project/helloworld) +2018-07-23 17:44:34.430 INFO 1978 --- [ main] i.s.d.helloworld.HelloworldApplication : No active profile set, falling back to default profiles: default +2018-07-23 17:44:34.472 INFO 1978 --- [ main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.spring[email protected]1d24f32d: startup date [Mon Jul 23 17:44:34 EDT 2018]; root of context hierarchy +2018-07-23 17:44:35.280 INFO 1978 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... +2018-07-23 17:44:35.410 INFO 1978 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. +2018-07-23 17:44:35.419 DEBUG 1978 --- [ main] o.s.c.t.c.SimpleTaskConfiguration : Using org.springframework.cloud.task.configuration.DefaultTaskConfigurer TaskConfigurer +2018-07-23 17:44:35.420 DEBUG 1978 --- [ main] o.s.c.t.c.DefaultTaskConfigurer : No EntityManager was found, using DataSourceTransactionManager +2018-07-23 17:44:35.522 DEBUG 1978 --- [ main] o.s.c.t.r.s.TaskRepositoryInitializer : Initializing task schema for h2 database +2018-07-23 17:44:35.525 INFO 1978 --- [ main] o.s.jdbc.datasource.init.ScriptUtils : Executing SQL script from class path resource [org/springframework/cloud/task/schema-h2.sql] +2018-07-23 17:44:35.558 INFO 1978 --- [ main] o.s.jdbc.datasource.init.ScriptUtils : Executed SQL script from class path resource [org/springframework/cloud/task/schema-h2.sql] in 33 ms. +2018-07-23 17:44:35.728 INFO 1978 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup +2018-07-23 17:44:35.730 INFO 1978 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Bean with name 'dataSource' has been autodetected for JMX exposure +2018-07-23 17:44:35.733 INFO 1978 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Located MBean 'dataSource': registering with JMX server as MBean [com.zaxxer.hikari:name=dataSource,type=HikariDataSource] +2018-07-23 17:44:35.738 INFO 1978 --- [ main] o.s.c.support.DefaultLifecycleProcessor : Starting beans in phase 0 +2018-07-23 17:44:35.762 DEBUG 1978 --- [ main] o.s.c.t.r.support.SimpleTaskRepository : Creating: TaskExecution{executionId=0, parentExecutionId=null, exitCode=null, taskName='application', startTime=Mon Jul 23 17:44:35 EDT 2018, endTime=null, exitMessage='null', externalExecutionId='null', errorMessage='null', arguments=[]} +2018-07-23 17:44:35.772 INFO 1978 --- [ main] i.s.d.helloworld.HelloworldApplication : Started HelloworldApplication in 1.625 seconds (JVM running for 4.764) +Hello, World! +2018-07-23 17:44:35.782 DEBUG 1978 --- [ main] o.s.c.t.r.support.SimpleTaskRepository : Updating: TaskExecution with executionId=1 with the following {exitCode=0, endTime=Mon Jul 23 17:44:35 EDT 2018, exitMessage='null', errorMessage='null'} +``` + +前面的输出有三行我们感兴趣的内容: + +* `SimpleTaskRepository`在`TaskRepository`中记录了条目的创建。 + +* 我们的`CommandLineRunner`的执行,通过“你好,世界!”输出进行了演示。 + +* `SimpleTaskRepository`在`TaskRepository`中记录任务的完成情况。 + +| |一个简单的任务应用程序可以在 Spring 云
任务项目[here](https://github.com/spring-cloud/spring-cloud-task/tree/master/spring-cloud-task-samples/timestamp)的样例模块中找到。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[](#features)[Features](#features) +========== + +本节将更详细地介绍 Spring 云任务,包括如何使用它,如何配置它,以及适当的扩展点。 + +[](#features-lifecycle)[7. The lifecycle of a Spring Cloud Task](#features-lifecycle) +---------- + +在大多数情况下,现代云环境是围绕预期不会结束的流程的执行而设计的。如果它们结束了,它们通常会重新启动。虽然大多数平台确实有某种方式来运行一个在结束时不会重新启动的流程,但该运行的结果通常不会以可消耗的方式维护。 Spring 云任务提供了在环境中执行短期过程并记录结果的能力。这样做允许围绕短期流程的微服务架构,以及通过消息集成任务来运行较长时间的服务。 + +虽然这种功能在云环境中很有用,但在传统的部署模型中也可能出现同样的问题。 Spring 当启动应用程序与诸如 CRON 的调度程序一起运行时,能够在其完成后监视应用程序的结果是有用的。 + +Spring 云任务采取的方法是, Spring 引导应用程序可以有一个开始和一个结束,并且仍然是成功的。批处理应用程序就是一个例子,它说明了预期结束的过程(通常是短暂的)是如何有用的。 + +Spring 云任务记录给定任务的生命周期事件。以大多数 Web 应用程序为代表的大多数长时间运行的进程都不保存其生命周期事件。 Spring 云任务的核心任务就是这样做的。 + +生命周期由单个任务执行组成。这是一个 Spring 引导应用程序的物理执行,该应用程序被配置为一个任务(即,它具有 Sprint 云任务的依赖性)。 + +在任务开始时,在运行任何`CommandLineRunner`或`ApplicationRunner`实现之前,将在`TaskRepository`中创建一个记录开始事件的条目。此事件是通过由 Spring 框架触发的`SmartLifecycle#start`触发的。这向系统指示,所有 bean 都已准备好使用,并且在运行 Spring 引导提供的`CommandLineRunner`或`ApplicationRunner`实现之前就会出现。 + +| |任务的记录只有在成功引导“ApplicationContext”时才会发生。如果上下文根本无法引导,则不会记录任务的运行
。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在完成来自 Spring 引导的所有`*Runner#run`调用或“ApplicationContext”失败(由`ApplicationFailedEvent`表示)后,存储库中的任务执行将与结果一起更新。 + +| |如果应用程序要求在
处关闭`ApplicationContext`任务的完成(所有`*Runner#run`方法都已调用,并且任务
存储库已更新),则将属性`spring.cloud.task.closecontextEnabled`设置为 true。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#features-task-execution-details)[7.1.任务执行](#features-task-execution-details) ### + +存储在`TaskRepository`中的信息在`TaskExecution`类中建模,并由以下信息组成: + +| Field |说明| +|--------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|`executionid` |任务运行的唯一 ID。| +| `exitCode` |从`ExitCodeExceptionMapper`实现生成的退出代码。如果没有生成
退出代码,但抛出了`ApplicationFailedEvent`,则设置 1。否则,它是
假定为 0。| +| `taskName` |任务的名称,由配置的`TaskNameResolver`确定。| +| `startTime` |任务启动的时间,如`SmartLifecycle#start`调用所示。| +| `endTime` |任务完成的时间,如`ApplicationReadyEvent`所示。| +|`exitMessage` |退出时可获得的任何信息。这可以通过编程由“TaskExecutionListener”设置。| +|`errorMessage`|如果异常是任务结束的原因(如“applicationfailedevent”所示),则该异常的堆栈跟踪存储在此。| +| `arguments` |一个`List`的字符串命令行参数,因为它们被传递到可执行的
引导应用程序中。| + +### [](#features-lifecycle-exit-codes)[7.2.映射退出代码](#features-lifecycle-exit-codes) ### + +当任务完成时,它会尝试将退出代码返回到操作系统。如果我们看一下我们的[原始示例](#getting-started-developing-first-task),我们可以看到我们并没有控制我们应用程序的那个方面。因此,如果抛出了异常,JVM 将返回一段代码,该代码在调试中可能对你有任何用处,也可能对你没有任何用处。 + +因此, Spring Boot 提供了一个接口`ExitCodeExceptionMapper`,它允许你将未捕获的异常映射到退出代码。这样做可以让你在退出代码的层面上指出出了什么问题。另外,通过以这种方式映射退出代码, Spring 云任务记录返回的退出代码。 + +如果任务以 SIG-INT 或 SIG-项结束,则除非代码中另有指定,否则退出代码为零。 + +| |在任务运行时,退出代码以空的形式存储在存储库中。
一旦任务完成,相应的退出代码将根据本节前面描述的
准则进行存储。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[](#features-configuration)[8.配置](#features-configuration) +---------- + +Spring 云任务提供了一种随时可用的配置,如在“DefaultAskConfigurer”和`SimpleTaskConfiguration`类中定义的那样。本节将介绍默认值以及如何根据你的需要定制 Spring 云任务。 + +### [](#features-data-source)[8.1. DataSource](#features-data-source) ### + +Spring 云任务使用数据源存储任务执行的结果。默认情况下,我们提供了一个 H2 的内存实例,以提供一种简单的引导开发方法。但是,在生产环境中,你可能希望配置自己的`DataSource`。 + +如果你的应用程序只使用一个`DataSource`并同时作为你的业务模式和任务存储库,那么你所需要做的就是提供任何`DataSource`(这样做的最简单方法是通过 Spring Boot 的配置约定)。 Spring 云任务为存储库自动使用这个“数据源”。 + +如果应用程序使用多个`DataSource`,则需要使用适当的`DataSource`配置任务存储库。这种定制可以通过`TaskConfigurer`的实现来完成。 + +### [](#features-table-prefix)[8.2.表格前缀](#features-table-prefix) ### + +`TaskRepository`的一个可修改的属性是任务表的表前缀。默认情况下,它们都以`TASK_`开头。`TASK_EXECUTION`和`TASK_EXECUTION_PARAMS`是两个例子。然而,有潜在的理由修改这个前缀。如果需要将模式名前置到表名,或者如果同一模式中需要一组以上的任务表,则必须更改表前缀。可以将`spring.cloud.task.tablePrefix`设置为所需的前缀,如下所示: + +`spring.cloud.task.tablePrefix=yourPrefix` + +通过使用`spring.cloud.task.tablePrefix`,用户承担了创建任务表的责任,这些任务表满足任务表模式的两个标准,但需要进行用户业务需求所需的修改。在创建你自己的任务 DDL 时,可以使用 Spring 云任务模式 DDL 作为指导,如[here](https://github.com/spring-cloud/spring-cloud-task/tree/master/spring-cloud-task-core/src/main/resources/org/springframework/cloud/task)所示。 + +### [](#features-table-initialization)[8.3.启用/禁用表初始化](#features-table-initialization) ### + +如果你正在创建任务表,并且不希望 Spring Cloud Task 在任务启动时创建它们,请将`spring.cloud.task.initialize-enabled`属性设置为 `false’,如下所示: + +`spring.cloud.task.initialize-enabled=false` + +它默认为`true`。 + +| |属性`spring.cloud.task.initialize.enable`已被弃用。| +|---|-----------------------------------------------------------------------| + +### [](#features-generated_task_id)[8.4.外部生成的任务 ID](#features-generated_task_id) ### + +在某些情况下,你可能希望在请求任务和基础设施实际启动任务之间留出时间差。 Spring 云任务允许你在任务被请求时创建`TaskExecution`。然后将生成的`TaskExecution`的执行 ID 传递给任务,以便它可以在任务的生命周期中更新`TaskExecution`。 + +可以通过在`TaskRepository`的实现上调用`TaskExecution`方法创建`createTaskExecution`方法,该实现引用了保存`TaskExecution`对象的数据存储。 + +为了将任务配置为使用生成的`TaskExecutionId`,请添加以下属性: + +`spring.cloud.task.executionid=yourtaskId` + +### [](#features-external_task_id)[8.5.外部任务 ID](#features-external_task_id) ### + +Spring Cloud Task 允许你为每个“taskexecution”存储一个外部任务 ID。这方面的一个例子是,当一个任务在平台上启动时,Cloud Foundry 提供了一个任务 ID。为了将任务配置为使用生成的`TaskExecutionId`,请添加以下属性: + +`spring.cloud.task.external-execution-id=` + +### [](#features-parent_task_id)[8.6.父任务 ID](#features-parent_task_id) ### + +Spring Cloud Task 允许你为每个`TaskExecution`存储父任务 ID。这方面的一个例子是一个执行另一个或多个任务的任务,你希望记录每个子任务启动的任务。为了将任务配置为设置父“taskexecutionID”,在子任务上添加以下属性: + +`spring.cloud.task.parent-execution-id=` + +### [](#features-task-configurer)[8.7.任务配置器](#features-task-configurer) ### + +`TaskConfigurer`是一个策略接口,允许你定制 Spring 云任务组件的配置方式。默认情况下,我们提供了提供逻辑默认值的`DefaultTaskConfigurer`:基于`Map`的内存中组件(如果没有提供 ` 数据源’,则对开发有用)和基于 JDBC 的组件(如果有`DataSource`可用,则很有用)。 + +`TaskConfigurer`允许你配置三个主要组件: + +| Component |说明| Default (provided by `DefaultTaskConfigurer`) | +|----------------------------|------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------| +| `TaskRepository` |要使用的`TaskRepository`的实现。| `SimpleTaskRepository` | +| `TaskExplorer` |要使用的`TaskExplorer`(用于对任务
存储库进行只读访问的组件)的实现。| `SimpleTaskExplorer` | +|`PlatformTransactionManager`|运行任务更新时使用的事务管理器。|`DataSourceTransactionManager` if a `DataSource` is used.`ResourcelessTransactionManager` if it is not.| + +你可以通过创建`TaskConfigurer`接口的自定义实现来定制上表中描述的任何组件。通常,扩展“defaultTaskConfigurer”(如果没有找到`TaskConfigurer`,则提供它)并重写所需的 getter 就足够了。然而,可能需要从头开始实现自己的功能。 + +| |用户不应该直接使用来自`TaskConfigurer`的 getter 方法直接
,除非他们正在使用它来提供将被公开为 Spring bean 的实现。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#features-task-name)[8.8. Task Name](#features-task-name) ### + +在大多数情况下,任务的名称是在 Spring 引导中配置的应用程序名称。但是,在某些情况下,你可能希望将任务的运行映射到不同的名称。 Spring 云数据流就是这方面的一个例子(因为你可能希望以任务定义的名称运行该任务)。因此,我们提供了通过`TaskNameResolver`接口定制任务命名方式的能力。 + +默认情况下, Spring Cloud Task 提供`SimpleTaskNameResolver`,它使用以下选项(按优先顺序排列): + +1. Spring 引导属性(以 Spring 引导允许的任何方式进行配置)称为 ` Spring.cloud.task.name`。 + +2. 使用 Spring 引导规则解析的应用程序名称(通过“ApplicationContext#GetID”获得)。 + +### [](#features-task-execution-listener)[8.9.任务执行监听器](#features-task-execution-listener) ### + +`TaskExecutionListener`允许你为任务生命周期中发生的特定事件注册侦听器。为此,创建一个实现“TaskExecutionListener”接口的类。实现`TaskExecutionListener`接口的类将收到以下事件的通知: + +* `onTaskStartup`:在将`TaskExecution`存储到`TaskRepository`之前。 + +* `onTaskEnd`:在更新`TaskRepository`中的`TaskExecution`条目并标记任务的最终状态之前。 + +* `onTaskFailed`:在任务引发未处理异常时调用`onTaskEnd`方法之前。 + +Spring Cloud Task 还允许你通过使用以下方法注释将`TaskExecution`侦听器添加到 Bean 内的方法: + +* `@BeforeTask`:在将`TaskExecution`存储到`TaskRepository`之前 + +* `@AfterTask`:在更新`TaskExecution`条目之前,在`TaskRepository`中标记任务的最终状态。 + +* `@FailedTask`:在任务抛出未处理的异常时调用`@AfterTask`方法之前。 + +下面的示例显示了正在使用的三种注释: + +``` + public class MyBean { + + @BeforeTask + public void methodA(TaskExecution taskExecution) { + } + + @AfterTask + public void methodB(TaskExecution taskExecution) { + } + + @FailedTask + public void methodC(TaskExecution taskExecution, Throwable throwable) { + } +} +``` + +| |在链中比`TaskLifecycleListener`存在更早地插入`ApplicationListener`可能会导致意想不到的影响。| +|---|-------------------------------------------------------------------------------------------------------------------------| + +#### [](#features-task-execution-listener-Exceptions)[8.9.1.任务执行侦听器抛出的异常](#features-task-execution-listener-Exceptions) #### + +如果`TaskExecutionListener`事件处理程序引发异常,则该事件处理程序的所有侦听器处理都将停止。例如,如果三个`onTaskStartup`侦听器已经启动,并且第一个`onTaskStartup`事件处理程序抛出一个异常,则不调用另外两个`onTaskStartup`方法。但是,调用`TaskExecutionListeners`的其他事件处理程序(`ontaskend` 和`onTaskFailed`)。 + +当`TaskExecutionListener`事件处理程序抛出异常时返回的退出代码是[ExitCodeEvent](https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/ExitCodeEvent.html)报告的退出代码。如果没有`ExitCodeEvent`发出,则对抛出的异常进行评估,以查看它是否为[exitcodegenerator](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-application-exit)类型。如果是,则返回来自`ExitCodeGenerator`的退出代码。否则,将返回`1`。 + +在`onTaskStartup`方法中抛出异常的情况下,应用程序的退出代码将是`1`。如果在`onTaskEnd`或`onTaskFailed`方法中抛出异常,则应用程序的退出代码将是使用上面列举的规则建立的代码。 + +| |在`onTaskStartup`、`onTaskEnd`或`onTaskFailed`中抛出异常的情况下,你无法使用`ExitCodeExceptionMapper`覆盖应用程序的退出代码。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#features-task-execution-listener-exit-messages)[8.9.2.退出消息](#features-task-execution-listener-exit-messages) #### + +你可以通过使用“TaskExecutionListener”以编程方式设置任务的退出消息。这是通过设置`TaskExecution’s``exitMessage`来完成的,然后将其传递到`TaskExecutionListener`中。下面的示例显示了一个用`@AfterTask``ExecutionListener`进行注释的方法: + +``` +@AfterTask +public void afterMe(TaskExecution taskExecution) { + taskExecution.setExitMessage("AFTER EXIT MESSAGE"); +} +``` + +可以在任何侦听器事件(“ontaskstartup”、“ontaskfailed”和`onTaskEnd`)上设置`ExitMessage`。这三个侦听器的优先顺序如下: + +1. `onTaskEnd` + +2. `onTaskFailed` + +3. `onTaskStartup` + +例如,如果你为`onTaskStartup`和`onTaskFailed`侦听器设置了`exitMessage`,并且任务没有失败就结束了,则来自`onTaskStartup`的`exitMessage`将存储在存储库中。否则,如果发生故障,则存储来自`onTaskFailed`的`exitMessage`。同样,如果你使用 `ontaskend’侦听器设置`exitMessage`,则来自`onTaskEnd`的`exitMessage`将取代来自`onTaskStartup`和`onTaskFailed`的退出消息。 + +### [](#features-single-instance-enabled)[8.10. Restricting Spring Cloud Task Instances](#features-single-instance-enabled) ### + +Spring 云任务允许你确定一次只能运行一个具有给定任务名的任务。为此,你需要为每个任务执行建立[task name](#features-task-name)并设置 ` Spring.cloud.task.single-instance-enabled=true`。当第一个任务执行正在运行时,当你尝试运行一个具有相同[task name](#features-task-name)和 ` Spring.cloud.task.single-instance-enabled=true` 的任务时,该任务会失败,并出现以下错误消息:`Task with name "application" is already running.``spring.cloud.task.single-instance-enabled`的默认值为`false`。下面的示例展示了如何将`spring.cloud.task.single-instance-enabled`设置为`true`: + +`spring.cloud.task.single-instance-enabled=true or false` + +要使用此功能,你必须向应用程序添加以下 Spring 集成依赖项: + +``` + + org.springframework.integration + spring-integration-core + + + org.springframework.integration + spring-integration-jdbc + +``` + +| |如果任务失败,则应用程序的退出代码将为 1,因为启用了此功能
,并且另一个任务正在以相同的任务名运行。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#disabling-spring-cloud-task-auto-configuration)[8.11. Disabling Spring Cloud Task Auto Configuration](#disabling-spring-cloud-task-auto-configuration) ### + +在不应该为某个实现自动配置 Spring 云任务的情况下,你可以禁用 Task 的自动配置。这可以通过向任务应用程序添加以下注释来完成: + +``` +@EnableAutoConfiguration(exclude={SimpleTaskAutoConfiguration.class}) +``` + +还可以通过将`spring.cloud.task.autoconfiguration.enabled`属性设置为`false`来禁用任务自动配置。 + +### [](#closing-the-context)[8.12.结束上下文](#closing-the-context) ### + +如果应用程序要求在任务完成时关闭`ApplicationContext`(所有`*Runner#run`方法都已被调用,并且任务存储库已更新),则将属性`spring.cloud.task.closecontextEnabled`设置为`true`。 + +关闭上下文的另一种情况是当任务执行完成但应用程序没有终止时。在这些情况下,上下文是开放的,因为已经分配了一个线程(例如:如果你正在使用 TaskExecutor)。在这些情况下,在启动任务时将`spring.cloud.task.closecontextEnabled`属性设置为`true`。一旦任务完成,这将关闭应用程序的上下文。从而允许应用程序终止。 + +[](#batch)[Batch](#batch) +========== + +本节将更详细地介绍 Spring Cloud Task 与 Spring Batch 的集成。跟踪作业执行与其执行的任务之间的关联,以及通过 Spring Cloud Deployer 进行远程分区,将在本节中介绍。 + +[](#batch-association)[9.将作业执行与其执行的任务关联起来](#batch-association) +---------- + +Spring Boot 提供了用于在 anüber- jar 内执行批处理作业的设施。 Spring Boot 对该功能的支持使开发人员能够在该执行中执行多个批处理任务。 Spring 云任务提供了将作业(作业执行)的执行与任务的执行相关联的能力,以便一个可以追溯到另一个。 + +Spring 云任务通过使用`TaskBatchExecutionListener`来实现这一功能。默认情况下,此侦听器在任何上下文中自动配置,该上下文同时配置了 Spring 批作业(通过在上下文中定义了类型`Job`的 Bean)和 Classpath 上的 ` Spring-cloud-task-batch` jar。所有符合这些条件的工作都会被注入监听器。 + +### [](#batch-association-override)[9.1.覆盖 TaskBatchExecutionListener](#batch-association-override) ### + +为了防止侦听器被注入到当前上下文中的任何批处理作业中,你可以使用标准的 Spring 引导机制禁用自动配置。 + +要仅将侦听器注入到上下文中的特定作业中,请覆盖“BatchtaskExecutionListentenerBeanPostProcessor”,并提供作业 Bean ID 的列表,如以下示例所示: + +``` +public TaskBatchExecutionListenerBeanPostProcessor batchTaskExecutionListenerBeanPostProcessor() { + TaskBatchExecutionListenerBeanPostProcessor postProcessor = + new TaskBatchExecutionListenerBeanPostProcessor(); + + postProcessor.setJobNames(Arrays.asList(new String[] {"job1", "job2"})); + + return postProcessor; +} +``` + +| |你可以在 Spring Cloud
任务项目的样例模块中找到样例批处理应用程序,[here](https://github.com/spring-cloud/spring-cloud-task/tree/master/spring-cloud-task-samples/batch-job)。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[](#batch-partitioning)[10.远程分区](#batch-partitioning) +---------- + +Spring Cloud Deployer 提供了用于在大多数云基础设施上启动 Spring 基于引导的应用程序的设施。`DeployerPartitionHandler`和 `DeployerStepExecutionHandler’将工人步骤执行的启动委托给 Spring Cloud Deployer。 + +要配置`DeployerStepExecutionHandler`,必须提供一个表示要执行的 Spring bootüber- jar 的`Resource`、一个`TaskLauncher`和一个 `jobexplorer’。你可以配置任何环境属性,以及一次执行的工作人员的最大数量、轮询结果的间隔(默认为 10 秒)和超时(默认为-1 或无超时)。下面的示例显示了如何配置`PartitionHandler`: + +``` +@Bean +public PartitionHandler partitionHandler(TaskLauncher taskLauncher, + JobExplorer jobExplorer) throws Exception { + + MavenProperties mavenProperties = new MavenProperties(); + mavenProperties.setRemoteRepositories(new HashMap<>(Collections.singletonMap("springRepo", + new MavenProperties.RemoteRepository(repository)))); + + Resource resource = + MavenResource.parse(String.format("%s:%s:%s", + "io.spring.cloud", + "partitioned-batch-job", + "1.1.0.RELEASE"), mavenProperties); + + DeployerPartitionHandler partitionHandler = + new DeployerPartitionHandler(taskLauncher, jobExplorer, resource, "workerStep"); + + List commandLineArgs = new ArrayList<>(3); + commandLineArgs.add("--spring.profiles.active=worker"); + commandLineArgs.add("--spring.cloud.task.initialize.enable=false"); + commandLineArgs.add("--spring.batch.initializer.enabled=false"); + + partitionHandler.setCommandLineArgsProvider( + new PassThroughCommandLineArgsProvider(commandLineArgs)); + partitionHandler.setEnvironmentVariablesProvider(new NoOpEnvironmentVariablesProvider()); + partitionHandler.setMaxWorkers(2); + partitionHandler.setApplicationName("PartitionedBatchJobTask"); + + return partitionHandler; +} +``` + +| |当将环境变量传递给分区时,每个分区可能
位于具有不同环境设置的不同机器上。因此,你应该仅传递所需的那些环境变量。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +请注意,在上面的示例中,我们将工人的最大数量设置为 2。设置工人的最大值可以确定一次应该运行的分区的最大数量。 + +要执行的`Resource`应该是一个 Spring bootüber- jar,其中的 `DeployerStepExecutionHandler’在当前上下文中被配置为`CommandLineRunner`。前面示例中列举的存储库应该是 über- jar 所在的远程存储库。Manager 和 Worker 都应该对作为作业存储库和任务存储库使用的同一数据存储具有可见性。一旦底层基础结构引导了 Spring boot jar,并且 Spring boot 启动了`DeployerStepExecutionHandler`,步骤处理程序将执行请求的 `step’。下面的示例展示了如何配置`DeployerStepExecutionHandler`: + +``` +@Bean +public DeployerStepExecutionHandler stepExecutionHandler(JobExplorer jobExplorer) { + DeployerStepExecutionHandler handler = + new DeployerStepExecutionHandler(this.context, jobExplorer, this.jobRepository); + + return handler; +} +``` + +| |你可以在
Spring 云任务项目的样例模块[here](https://github.com/spring-cloud/spring-cloud-task/tree/master/spring-cloud-task-samples/partitioned-batch-job)中找到一个样例远程分区应用程序。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#notes-on-developing-a-batch-partitioned-application-for-the-kubernetes-platform)[10.1.关于为 Kubernetes 平台开发批处理分区应用程序的说明](#notes-on-developing-a-batch-partitioned-application-for-the-kubernetes-platform) ### + +* 在 Kubernetes 平台上部署分区应用程序时,你必须对 Spring Cloud Kubernetes 部署程序使用以下依赖关系: + + ``` + + org.springframework.cloud + spring-cloud-starter-deployer-kubernetes + + ``` + +* 任务应用程序及其分区的应用程序名称需要遵循以下正则表达式模式:`[a-z0-9]([-a-z0-9]*[a-z0-9])`。否则,将抛出异常。 + +### [](#notes-on-developing-a-batch-partitioned-application-for-the-cloud-foundry-platform)[10.2.为 Cloud Foundry 平台开发批处理分区应用程序的注意事项](#notes-on-developing-a-batch-partitioned-application-for-the-cloud-foundry-platform) ### + +* 在 Cloud Foundry 平台上部署分区应用程序时,对于 Spring Cloud Foundry 部署人员,你必须使用以下依赖关系: + + ``` + + org.springframework.cloud + spring-cloud-deployer-cloudfoundry + + + io.projectreactor + reactor-core + 3.1.5.RELEASE + + + io.projectreactor.ipc + reactor-netty + 0.7.5.RELEASE + + ``` + +* 在配置分区处理程序时,需要建立 Cloud Foundry 部署环境变量,以便分区处理程序可以启动分区。下面的列表显示了所需的环境变量: + + * `spring_cloud_deployer_cloudfoundry_url` + + * `spring_cloud_deployer_cloudfoundry_org` + + * `spring_cloud_deployer_cloudfoundry_space` + + * `spring_cloud_deployer_cloudfoundry_domain` + + * `spring_cloud_deployer_cloudfoundry_username` + + * `spring_cloud_deployer_cloudfoundry_password` + + * `spring_cloud_deployer_cloudfoundry_services` + + * `spring_cloud_deployer_cloudfoundry_taskTimeout` + +使用`mysql`数据库服务的分区任务的部署环境变量示例集可能类似于以下内容: + +``` +spring_cloud_deployer_cloudfoundry_url=https://api.local.pcfdev.io +spring_cloud_deployer_cloudfoundry_org=pcfdev-org +spring_cloud_deployer_cloudfoundry_space=pcfdev-space +spring_cloud_deployer_cloudfoundry_domain=local.pcfdev.io +spring_cloud_deployer_cloudfoundry_username=admin +spring_cloud_deployer_cloudfoundry_password=admin +spring_cloud_deployer_cloudfoundry_services=mysql +spring_cloud_deployer_cloudfoundry_taskTimeout=300 +``` + +| |在使用 PCF-Dev 时,还需要使用以下环境变量:`Spring_Cloud_Deployer_CloudFoundry_SkipsslValidation=true’| +|---|-----------------------------------------------------------------------------------------------------------------------------------| + +[](#batch-informational-messages)[11.批处理信息消息](#batch-informational-messages) +---------- + +Spring 云任务为批处理作业提供了发出信息消息的能力。“[Spring Batch Events](#stream-integration-batch-events)”部分详细介绍了此功能。 + +[](#batch-failures-and-tasks)[12.批处理作业退出代码](#batch-failures-and-tasks) +---------- + +正如[earlier](#features-lifecycle-exit-codes)所讨论的, Spring 云任务应用程序支持记录任务执行的退出代码的能力。然而,在任务中运行 Spring 批处理作业的情况下,无论批处理作业如何执行,当使用默认的批处理/引导行为时,任务的结果始终为零。请记住,任务是一个引导应用程序,从该任务返回的退出代码与引导应用程序相同。要重写此行为并允许任务在批处理作业返回`FAILED`的[BatchStatus](https://docs.spring.io/spring-batch/4.0.x/reference/html/step.html#batchStatusVsExitStatus)时返回除零以外的退出代码,请将`spring.cloud.task.batch.fail-on-job-failure`设置为`true`。然后退出代码可以是 1(默认值),也可以基于[指定的“exitcodegenerator”](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-spring-application.html#boot-features-application-exit)) + +这个功能使用了一个新的`CommandLineRunner`,它取代了 Spring boot 提供的功能。默认情况下,它的配置顺序相同。但是,如果你想定制`CommandLineRunner`的运行顺序,可以通过设置 ` Spring.cloud.task.batch.CommandlineRunnerOrder` 属性来设置其顺序。要让你的任务返回基于批处理作业执行结果的退出代码,你需要编写自己的“CommandLineRunner”。 + +[](#batch-job-starter)[单步批处理作业启动器](#batch-job-starter) +========== + +本节将讨论如何通过使用 Spring 云任务中包含的启动器来开发具有单个`Step`的 Spring 批处理`Job`。这个启动器允许你使用配置来定义`ItemReader`、`ItemWriter`或完整的单步 Spring 批处理`Job`。有关 Spring 批处理及其功能的更多信息,请参见[Spring Batch documentation](https://spring.io/projects/spring-batch)。 + +要获得 Maven 的启动器,请在构建中添加以下内容: + +``` + + org.springframework.cloud + spring-cloud-starter-single-step-batch-job + 2.3.0 + +``` + +要获得 Gradle 的启动器,请在构建中添加以下内容: + +``` +compile "org.springframework.cloud:spring-cloud-starter-single-step-batch-job:2.3.0" +``` + +[](#job-definition)[13.定义工作](#job-definition) +---------- + +你可以使用 starter 来定义很少的`ItemReader`或`ItemWriter`,也可以定义很多的`Job`。在本节中,我们定义了配置“作业”所需定义的属性。 + +### [](#job-definition-properties)[13.1.属性](#job-definition-properties) ### + +首先,Starter 提供了一组属性,让你只需一步就可以配置作业的基本知识: + +| Property | Type |Default Value|说明| +|----------------------------|---------|-------------|----------------------------------------------------| +| `spring.batch.job.jobName` |`String` | `null` |工作的名字。| +|`spring.batch.job.stepName` |`String` | `null` |步骤的名称。| +|`spring.batch.job.chunkSize`|`Integer`| `null` |每笔交易要处理的项目数量。| + +配置了上述属性后,你就可以使用一个基于块的步骤来执行作业了。这个基于块的步骤读取、处理和写入`Map`实例作为项。然而,这一步还没有起到任何作用。你需要配置一个`ItemReader`、一个可选的`ItemProcessor`和一个`ItemWriter`,以便让它做一些事情。要配置其中之一,你可以使用属性并配置已提供自动配置的选项之一,也可以使用标准 Spring 配置机制配置你自己的选项。 + +| |如果配置自己的,则输入和输出类型必须与步骤中的其他类型匹配。
该启动器中的`ItemReader`实现和`ItemWriter`实现都使用
a`Map`作为输入和输出项。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[](#item-readers)[14.ItemReader 实现的自动配置](#item-readers) +---------- + +这个启动器为四个不同的`ItemReader`实现提供自动配置:`amqpitemreader`,`FlatFileItemReader`,`JdbcCursorItemReader`,和`KafkaItemReader`。在本节中,我们将概述如何通过使用提供的自动配置来配置其中的每一个。 + +### [](#amqpitemreader)[14.1.AMQPitemReader](#amqpitemreader) ### + +你可以使用`AmqpItemReader`使用 AMQP 读取队列或主题。这个`ItemReader`实现的自动配置依赖于两组配置。第一个是`AmqpTemplate`的配置。你可以自己对此进行配置,也可以使用 Spring Boot 提供的自动配置。参见[Spring Boot AMQP documentation](https://docs.spring.io/spring-boot/docs/2.4.x/reference/htmlsingle/#boot-features-amqp)。一旦配置了`AmqpTemplate`,就可以通过设置以下属性来启用批处理功能来支持它: + +| Property | Type |Default Value|说明| +|------------------------------------------------------|---------|-------------|---------------------------------------------------------------------------------------| +| `spring.batch.job.amqpitemreader.enabled` |`boolean`| `false` |如果`true`,则自动配置将执行。| +|`spring.batch.job.amqpitemreader.jsonConverterEnabled`|`boolean`| `true` |指示是否应注册`Jackson2JsonMessageConverter`以解析消息。| + +有关更多信息,请参见[“AmqPitemReader”文档](https://docs.spring.io/spring-batch/docs/4.3.x/api/org/springframework/batch/item/amqp/AmqpItemReader.html)。 + +### [](#flatfileitemreader)[14.2.平板文件阅读器](#flatfileitemreader) ### + +`FlatFileItemReader`允许你从平面文件(例如 CSV 和其他文件格式)进行读取。要从文件中读取数据,你可以通过正常的 Spring 配置(’linetokenizer’,`RecordSeparatorPolicy`,’fieldsetmapper’,`LineMapper`,或`SkippedLinesCallback`)自己提供一些组件。你还可以使用以下属性来配置阅读器: + +| Property | Type | Default Value |说明| +|------------------------------------------------------|---------------|------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `spring.batch.job.flatfileitemreader.saveState` | `boolean` | `true` |确定是否应将状态保存以重新启动。| +| `spring.batch.job.flatfileitemreader.name` | `String` | `null` |用于在`ExecutionContext`中提供唯一键的名称。| +| `spring.batch.job.flatfileitemreader.maxItemcount` | `int` | `Integer.MAX_VALUE` |从文件中读取的项目的最大数量。| +|`spring.batch.job.flatfileitemreader.currentItemCount`| `int` | 0 |已读项目的数量。用于重启。| +| `spring.batch.job.flatfileitemreader.comments` |`List` | empty List |指示文件中已注释的行(要忽略的行)的字符串列表。| +| `spring.batch.job.flatfileitemreader.resource` | `Resource` | `null` |要读取的资源。| +| `spring.batch.job.flatfileitemreader.strict` | `boolean` | `true` |如果设置为`true`,则在未找到资源的情况下,读取器将抛出一个异常。| +| `spring.batch.job.flatfileitemreader.encoding` | `String` | `FlatFileItemReader.DEFAULT_CHARSET` |读取文件时使用的编码。| +| `spring.batch.job.flatfileitemreader.linesToSkip` | `int` | 0 |指示文件开始时要跳过的行数。| +| `spring.batch.job.flatfileitemreader.delimited` | `boolean` | `false` |指示该文件是否为分隔文件(CSV 和其他格式)。此属性中只有一个或`spring.batch.job.flatfileitemreader.fixedLength`可以同时是`true`。| +| `spring.batch.job.flatfileitemreader.delimiter` | `String` | `DelimitedLineTokenizer.DELIMITER_COMMA` |如果读取分隔符文件,则指示要解析的分隔符。| +| `spring.batch.job.flatfileitemreader.quoteCharacter` | `char` |`DelimitedLineTokenizer.DEFAULT_QUOTE_CHARACTER`|用于确定用于引用值的字符。| +| `spring.batch.job.flatfileitemreader.includedFields` |`List`| empty list |一个索引列表,用于确定记录中的哪些字段要包含在项中。| +| `spring.batch.job.flatfileitemreader.fixedLength` | `boolean` | `false` |指示文件的记录是否由列号解析。此属性中只有一个或`spring.batch.job.flatfileitemreader.delimited`可以同时是`true`。| +| `spring.batch.job.flatfileitemreader.ranges` | `List` | empty list |用于解析固定宽度记录的列范围列表。参见[范围文档](https://docs.spring.io/spring-batch/docs/4.3.x/api/org/springframework/batch/item/file/transform/Range.html)。| +| `spring.batch.job.flatfileitemreader.names` | `String []` | `null` |从记录中解析的每个字段的名称列表。这些名称是从这个`ItemReader`返回的项中的`Map`中的键。| +| `spring.batch.job.flatfileitemreader.parsingStrict` | `boolean` | `true` |如果设置为`true`,则如果无法映射字段,则映射失败。| + +参见[“平板文件阅读器”文件](https://docs.spring.io/spring-batch/docs/4.3.x/api/org/springframework/batch/item/file/FlatFileItemReader.html)。 + +### [](#jdbcCursorItemReader)[14.3.JDBCCursoritemReader](#jdbcCursorItemReader) ### + +`JdbcCursorItemReader`针对关系数据库运行一个查询,并在结果游标上进行迭代,以提供结果项。此自动配置允许你提供`PreparedStatementSetter`、`RowMapper`或两者。你还可以使用以下属性来配置`JdbcCursorItemReader`: + +| Property | Type | Default Value |说明| +|-------------------------------------------------------------------|---------|-------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `spring.batch.job.jdbccursoritemreader.saveState` |`boolean`| `true` |确定是否应将状态保存以重新启动。| +| `spring.batch.job.jdbccursoritemreader.name` |`String` | `null` |用于在`ExecutionContext`中提供唯一键的名称。| +| `spring.batch.job.jdbccursoritemreader.maxItemcount` | `int` |`Integer.MAX_VALUE`|从文件中读取的项目的最大数量。| +| `spring.batch.job.jdbccursoritemreader.currentItemCount` | `int` | 0 |已读项目的数量。用于重启。| +| `spring.batch.job.jdbccursoritemreader.fetchSize` | `int` | |给驱动程序的一个提示,指示每次调用数据库系统要检索多少记录。为了获得最佳性能,你通常希望将其设置为与块大小匹配。| +| `spring.batch.job.jdbccursoritemreader.maxRows` | `int` | |从数据库中读取的项数的最大值。| +| `spring.batch.job.jdbccursoritemreader.queryTimeout` | `int` | |查询超时的毫秒数。| +| `spring.batch.job.jdbccursoritemreader.ignoreWarnings` |`boolean`| `true` |确定读取器在处理时是否应忽略 SQL 警告。| +| `spring.batch.job.jdbccursoritemreader.verifyCursorPosition` |`boolean`| `true` |指示是否应在每次读取后验证光标的位置,以验证`RowMapper`没有使光标前进。| +| `spring.batch.job.jdbccursoritemreader.driverSupportsAbsolute` |`boolean`| `false` |指示驱动程序是否支持光标的绝对定位。| +|`spring.batch.job.jdbccursoritemreader.useSharedExtendedConnection`|`boolean`| `false` |指示连接是否与其他处理共享(因此是事务的一部分)。| +| `spring.batch.job.jdbccursoritemreader.sql` |`String` | `null` |要读取的 SQL 查询。| + +参见[JDBCCursoritemReader 文档](https://docs.spring.io/spring-batch/docs/4.3.x/api/org/springframework/batch/item/database/JdbcCursorItemReader.html)。 + +### [](#kafkaItemReader)[14.4.Kafkaitemreader](#kafkaItemReader) ### + +从 Kafka 主题中获取数据分区非常有用,也正是“Kafkaitemreader”所能做的。要配置`KafkaItemReader`,需要进行两部分配置。首先,需要使用 Spring Boot 的 Kafka 自动配置来配置 Kafka(参见[Spring Boot Kafka documentation](https://docs.spring.io/spring-boot/docs/2.4.x/reference/htmlsingle/#boot-features-kafka))。在配置了 Spring 引导中的 Kafka 属性之后,可以通过设置以下属性来配置`KafkaItemReader`本身: + +| Property | Type |Default Value|说明| +|-------------------------------------------------------|---------------|-------------|-----------------------------------------------------------| +| `spring.batch.job.kafkaitemreader.name` | `String` | `null` |用于在`ExecutionContext`中提供唯一键的名称。| +| `spring.batch.job.kafkaitemreader.topic` | `String` | `null` |阅读主题的名称。| +| `spring.batch.job.kafkaitemreader.partitions` |`List`| empty list |要读取的分区索引的列表。| +|`spring.batch.job.kafkaitemreader.pollTimeOutInSeconds`| `long` | 30 |`poll()`操作的超时。| +| `spring.batch.job.kafkaitemreader.saveState` | `boolean` | `true` |确定是否应将状态保存以重新启动。| + +参见[“KafkaitemReader”文件](https://docs.spring.io/spring-batch/docs/4.3.x/api/org/springframework/batch/item/kafka/KafkaItemReader.html)。 + +[](#item-processors)[15.项目处理器配置](#item-processors) +---------- + +如果`ApplicationContext`中有一个选项可用,那么单步批处理作业自动配置接受`ItemProcessor`。如果找到了正确的类型(`itemprocessor,map>`),则自动连线到步骤中。 + +[](#item-writers)[16.ItemWriter 实现的自动配置](#item-writers) +---------- + +此启动器为`ItemWriter`实现提供自动配置,这些实现匹配所支持的`ItemReader`实现:`AmqpItemWriter`、`中由此`ItemWriter`接收的项的键。| +| `spring.batch.job.flatfileitemwriter.append` | `boolean` | `false` |指示如果找到输出文件,是否应将文件追加到该文件。| +| `spring.batch.job.flatfileitemwriter.lineSeparator` | `String` |`FlatFileItemWriter.DEFAULT_LINE_SEPARATOR`|用什么`String`来分隔输出文件中的行。| +| `spring.batch.job.flatfileitemwriter.name` | `String` | `null` |用于在`ExecutionContext`中提供唯一密钥的名称。| +| `spring.batch.job.flatfileitemwriter.saveState` | `boolean` | `true` |确定是否应将状态保存以重新启动。| +|`spring.batch.job.flatfileitemwriter.shouldDeleteIfEmpty` | `boolean` | `false` |如果设置为`true`,则在作业完成时将删除一个空文件(没有输出)。| +|`spring.batch.job.flatfileitemwriter.shouldDeleteIfExists`| `boolean` | `true` |如果设置为`true`,并且在输出文件应该在的位置找到一个文件,则在步骤开始之前将其删除。| +| `spring.batch.job.flatfileitemwriter.transactional` | `boolean` |`FlatFileItemWriter.DEFAULT_TRANSACTIONAL` |指示读取器是否为事务性队列(表示读取的项在出现故障时返回到队列中)。| + +参见[FlatFileitemWriter 文档](https://docs.spring.io/spring-batch/docs/4.3.x/api/org/springframework/batch/item/file/FlatFileItemWriter.html)。 + +### [](#jdbcitemwriter)[16.3.JDBCBatchitemwriter](#jdbcitemwriter) ### + +要将一个步骤的输出写到关系数据库中,此启动器提供了自动配置`JdbcBatchItemWriter`的功能。自动配置允许你通过设置以下属性来提供自己的`ItemPreparedStatementSetter`或`ItemSqlParameterSourceProvider`和配置选项: + +| Property | Type |Default Value|说明| +|----------------------------------------------------|---------|-------------|---------------------------------------------------------------------------------| +| `spring.batch.job.jdbcbatchitemwriter.name` |`String` | `null` |用于在`ExecutionContext`中提供唯一键的名称。| +| `spring.batch.job.jdbcbatchitemwriter.sql` |`String` | `null` |用于插入每个项的 SQL。| +|`spring.batch.job.jdbcbatchitemwriter.assertUpdates`|`boolean`| `true` |是否要验证每个插入都会导致至少一条记录的更新。| + +参见[JDBCBatchitemWriter 文档](https://docs.spring.io/spring-batch/docs/4.3.x/api/org/springframework/batch/item/database/JdbcBatchItemWriter.html)。 + +### [](#kafkaitemwriter)[16.4.KafkaitemWriter](#kafkaitemwriter) ### + +要将步骤输出写入 Kafka 主题,你需要`KafkaItemWriter`。这个启动器通过使用来自两个地方的设备为`KafkaItemWriter`提供自动配置。第一个是 Spring Boot 的 Kafka 自动配置。(参见[Spring Boot Kafka documentation](https://docs.spring.io/spring-boot/docs/2.4.x/reference/htmlsingle/#boot-features-kafka)。)其次,这个启动器允许你在 Writer 上配置两个属性。 + +| Property | Type |Default Value|说明| +|-----------------------------------------|---------|-------------|----------------------------------------------------------------------------------------------| +|`spring.batch.job.kafkaitemwriter.topic` |`String` | `null` |写卡夫卡的主题。| +|`spring.batch.job.kafkaitemwriter.delete`|`boolean`| `false` |被传递给 Writer 的项目是否都将作为删除事件发送到主题。| + +有关`KafkaItemWriter`的配置选项的更多信息,请参见[“Kafkaitemwiter”文件](https://docs.spring.io/spring-batch/docs/4.3.x/api/org/springframework/batch/item/kafka/KafkaItemWriter.html)。 + +[](#stream-integration)[Spring Cloud Stream Integration](#stream-integration) +========== + +任务本身可能是有用的,但是将任务集成到一个更大的生态系统中,可以使它对更复杂的处理和编排非常有用。本节介绍了 Spring 云任务与 Spring 云流的集成选项。 + +[](#stream-integration-launching-sink)[17. Launching a Task from a Spring Cloud Stream](#stream-integration-launching-sink) +---------- + +你可以从流启动任务。为此,创建一个接收器,该接收器监听包含`TaskLaunchRequest`作为其有效负载的消息。`TaskLaunchRequest`包含: + +* `uri`:到要执行的任务工件。 + +* `applicationName`:与任务关联的名称。如果未设置应用程序名,`TaskLaunchRequest`将生成一个由以下内容组成的任务名:`Task-`。 + +* `commandLineArguments`:包含任务的命令行参数的列表。 + +* `environmentProperties`:包含任务要使用的环境变量的映射。 + +* `deploymentProperties`:包含部署人员用于部署任务的属性的映射。 + +| |如果有效载荷的类型不同,则接收器将抛出一个异常。| +|---|--------------------------------------------------------------------| + +例如,可以创建一个流,该流具有一个处理器,该处理器从 HTTP 源接收数据,并创建一个包含`GenericMessage`并将消息发送到其输出通道的`TaskLaunchRequest`。然后,任务接收器将从其输入通道接收消息,然后启动任务。 + +要创建 TaskSink,你只需要创建一个包含“EnabletAskLauncher”注释的 Spring 启动应用程序,如下例所示: + +``` +@SpringBootApplication +@EnableTaskLauncher +public class TaskSinkApplication { + public static void main(String[] args) { + SpringApplication.run(TaskSinkApplication.class, args); + } +} +``` + +Spring 云任务项目的[samples module](https://github.com/spring-cloud/spring-cloud-task/tree/master/spring-cloud-task-samples)包含一个样例接收器和处理器。要将这些示例安装到本地 Maven 存储库中,请从 ` Spring-cloud-task-samples’目录运行一个 Maven 构建,并将`skipInstall`属性设置为`false`,如以下示例所示: + +`mvn clean install` + +| |必须将`maven.remoteRepositories.springRepo.url`属性设置为 über- jar 所在的远程存储库的位置
。如果未设置,则不存在远程
存储库,因此它仅依赖于本地存储库。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#stream-integration-launching-sink-dataflow)[17.1. Spring Cloud Data Flow](#stream-integration-launching-sink-dataflow) ### + +要在 Spring 云数据流中创建流,你必须首先注册我们创建的任务接收应用程序。在下面的示例中,我们使用 Spring 云数据流壳来注册处理器和接收器示例应用程序: + +``` +app register --name taskSink --type sink --uri maven://io.spring.cloud:tasksink: +app register --name taskProcessor --type processor --uri maven:io.spring.cloud:taskprocessor: +``` + +下面的示例展示了如何从 Spring 云数据流壳层创建流: + +``` +stream create foo --definition "http --server.port=9000|taskProcessor|taskSink" --deploy +``` + +[](#stream-integration-events)[18. Spring Cloud Task Events](#stream-integration-events) +---------- + +Spring 云任务提供了当该任务通过 Spring 云流通道运行时通过 Spring 云流通道发出事件的能力。任务侦听器用于在名为`task-events`的消息通道上发布`TaskExecution`。此功能可自动连接到任何具有`spring-cloud-stream`、`spring-cloud-stream-`以及在其 Classpath 上定义的任务的任务中。 + +| |要禁用事件发送侦听器,请将`spring.cloud.task.events.enabled`属性设置为`false`。| +|---|------------------------------------------------------------------------------------------------------| + +在定义了适当的 Classpath 之后,以下任务将在`task-events`通道上(在任务的开始和结束时)发出`TaskExecution`作为事件: + +``` +@SpringBootApplication +public class TaskEventsApplication { + + public static void main(String[] args) { + SpringApplication.run(TaskEventsApplication.class, args); + } + + @Configuration + public static class TaskConfiguration { + + @Bean + public CommandLineRunner commandLineRunner() { + return new CommandLineRunner() { + @Override + public void run(String... args) throws Exception { + System.out.println("The CommandLineRunner was executed"); + } + }; + } + } +} +``` + +| |Classpath 上还需要一个粘合剂实现。| +|---|----------------------------------------------------------------| + +| |一个示例任务事件应用程序可以在 Spring 云任务项目的示例模块
中找到,[here](https://github.com/spring-cloud/spring-cloud-task/tree/master/spring-cloud-task-samples/task-events)。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#stream-integration-disable-task-events)[18.1.禁用特定的任务事件](#stream-integration-disable-task-events) ### + +要禁用任务事件,可以将`spring.cloud.task.events.enabled`属性设置为 `false’。 + +[](#stream-integration-batch-events)[19. Spring Batch Events](#stream-integration-batch-events) +---------- + +当通过任务执行 Spring 批处理作业时, Spring 云任务可以被配置为基于 Spring 批处理中可用的 Spring 批处理侦听器发出信息消息。具体地,以下 Spring 批处理侦听器被自动配置到每个批处理作业中,并在通过 Spring 云任务运行时在相关联的 Spring 云流通道上发出消息: + +* `JobExecutionListener`监听`job-execution-events` + +* `StepExecutionListener`监听`step-execution-events` + +* `ChunkListener`监听`chunk-events` + +* `ItemReadListener`监听`item-read-events` + +* `ItemProcessListener`监听`item-process-events` + +* `ItemWriteListener`监听`item-write-events` + +* `SkipListener`监听`skip-events` + +当上下文中存在适当的 bean(a`Job`和 a`TaskLifecycleListener`)时,这些侦听器将自动配置为任意`AbstractJob`。对侦听这些事件的配置的处理方式与绑定到任何其他 Spring 云流通道的处理方式相同。我们的任务(运行批处理作业的任务)充当“源”,监听应用程序充当`Processor`或`Sink`。 + +例如,可以让一个应用程序监听`job-execution-events`通道来启动和停止作业。要配置监听应用程序,你可以将输入配置为`job-execution-events`,如下所示: + +`spring.cloud.stream.bindings.input.destination=job-execution-events` + +| |Classpath 上还需要一个绑定器实现。| +|---|----------------------------------------------------------------| + +| |一个样例批处理事件应用程序可以在 Spring 云任务项目的样例模块
中找到,[here](https://github.com/spring-cloud/spring-cloud-task/tree/master/spring-cloud-task-samples/batch-events)。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#sending-batch-events-to-different-channels)[19.1.将批处理事件发送到不同的通道](#sending-batch-events-to-different-channels) ### + +Spring Cloud Task 为批处理事件提供的选项之一是能够更改特定侦听器可以向其发送消息的通道。为此,使用以下配置:` Spring.cloud.stream.bindings..destination=`。例如,如果`StepExecutionListener`需要将其消息发送到另一个名为 `my-step-execution-events’的通道,而不是默认的`step-execution-events`,则可以添加以下配置: + +`spring.cloud.stream.bindings.step-execution-events.destination=my-step-execution-events` + +### [](#disabling-batch-events)[19.2.禁用批处理事件](#disabling-batch-events) ### + +要禁用所有批处理事件的侦听器功能,请使用以下配置: + +`spring.cloud.task.batch.events.enabled=false` + +要禁用特定的批处理事件,请使用以下配置: + +`spring.cloud.task.batch.events..enabled=false`: + +下面的清单显示了你可以禁用的各个侦听器: + +``` +spring.cloud.task.batch.events.job-execution.enabled=false +spring.cloud.task.batch.events.step-execution.enabled=false +spring.cloud.task.batch.events.chunk.enabled=false +spring.cloud.task.batch.events.item-read.enabled=false +spring.cloud.task.batch.events.item-process.enabled=false +spring.cloud.task.batch.events.item-write.enabled=false +spring.cloud.task.batch.events.skip.enabled=false +``` + +### [](#emit-order-for-batch-events)[19.3.为批处理事件发出命令](#emit-order-for-batch-events) ### + +默认情况下,批处理事件具有`Ordered.LOWEST_PRECEDENCE`。要更改该值(例如,为 5),请使用以下配置: + +``` +spring.cloud.task.batch.events.job-execution-order=5 +spring.cloud.task.batch.events.step-execution-order=5 +spring.cloud.task.batch.events.chunk-order=5 +spring.cloud.task.batch.events.item-read-order=5 +spring.cloud.task.batch.events.item-process-order=5 +spring.cloud.task.batch.events.item-write-order=5 +spring.cloud.task.batch.events.skip-order=5 +``` + +[](#appendix)[Appendices](#appendix) +========== + +[](#appendix-task-repository-schema)[20.任务存储库模式](#appendix-task-repository-schema) +---------- + +本附录为任务存储库中使用的数据库模式提供了 ERD。 + +![task schema](./images/task_schema.png) + +### [](#table-information)[20.1.表格信息](#table-information) ### + +任务 \_ 执行 + +存储任务执行信息。 + +| 列名称 |Required| Type |Field Length|笔记| +|---------------------------|--------|--------|------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 任务 \_ 执行 \_ID | TRUE | BIGINT | X |Spring 云任务框架在应用程序启动时建立如从`TASK_SEQ`中获得的下一个可用 ID。或者,如果记录是在任务之外创建的,那么值必须在记录创建时填充。| +| START\_TIME | FALSE |DATETIME| X |Spring 云任务框架在应用程序启动时建立了价值。| +| END\_TIME | FALSE |DATETIME| X |Spring 云任务框架在应用程序出口处建立了价值.| +| TASK\_NAME | FALSE |VARCHAR | 100 |Spring 云任务框架在应用程序启动时将此设置为“应用程序”,除非用户使用所讨论的 Spring.cloud.task.name 建立名称[here](#features-task-name)| +| EXIT\_CODE | FALSE |INTEGER | X |遵循 Spring 引导默认值,除非如[here](https://docs.spring.io/spring-cloud-task/docs/current/reference/#features-lifecycle-exit-codes)所讨论的那样被用户重写。| +| EXIT\_MESSAGE | FALSE |VARCHAR | 2500 |用户定义为讨论[here](https://docs.spring.io/spring-cloud-task/docs/current/reference/#features-task-execution-listener-exit-messages)。| +| ERROR\_MESSAGE | FALSE |VARCHAR | 2500 |Spring 云任务框架在应用程序出口建立的价值.| +| LAST\_UPDATED | TRUE |DATETIME| X |Spring 云任务框架在应用程序启动时建立的价值。或者,如果记录是在任务之外创建的,那么值必须在记录创建时填充。| +| EXTERNAL\_EXECUTION\_ID | FALSE |VARCHAR | 250 |如果设置了`spring.cloud.task.external-execution-id`属性,那么应用程序启动时的 Spring Cloud Task Framework 将把它设置为指定的值。更多信息请访问[here](#features-external_task_id)| +|PARENT\_任务 \_ 执行 \_ID| FALSE | BIGINT | X |如果设置了`spring.cloud.task.parent-execution-id`属性,那么应用程序启动时的 Spring Cloud Task Framework 将把它设置为指定的值。更多信息请访问[here](#features-parent_task_id)| + +任务 \_ 执行 \_ 参数 + +存储用于执行任务的参数 + +|列名称|Required| Type |Field Length| +|-------------------|--------|-------|------------| +|TASK\_EXECUTION\_ID| TRUE |BIGINT | X | +|任务 \_param| FALSE |VARCHAR| 2500 | + +任务 \_ 任务 \_ 批处理 + +用于将任务执行链接到批处理执行。 + +| Column Name |Required| Type |Field Length| +|-------------------|--------|------|------------| +|TASK\_EXECUTION\_ID| TRUE |BIGINT| X | +|作业 \_ 执行 \_ID| TRUE |BIGINT| X | + +任务 \_ 锁定 + +用于讨论`single-instance-enabled`的功能[here](#features-single-instance-enabled)。 + +| Column Name |Required| Type |Field Length|笔记| +|-------------|--------|--------|------------|----------------------------------------------------------------| +| LOCK\_KEY | TRUE | CHAR | 36 |这把锁的 UUID| +| REGION | TRUE |VARCHAR | 100 |用户可以使用该字段建立一组锁。| +| CLIENT\_ID | TRUE | CHAR | 36 |包含要锁定的应用程序名称的任务执行 ID。| +|CREATED\_DATE| TRUE |DATETIME| X |创建条目的日期| + +| |可以找到用于为每个数据库类型设置表的 DDL[here](https://github.com/spring-cloud/spring-cloud-task/tree/master/spring-cloud-task-core/src/main/resources/org/springframework/cloud/task)。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#sql-server)[20.2.SQL 服务器](#sql-server) ### + +Spring 云任务默认情况下使用一个序列表来确定`TASK_EXECUTION_ID`的`TASK_EXECUTION`表。但是,当使用 SQL Server 同时启动多个任务时,这可能会导致`TASK_SEQ`表上出现死锁。解决方法是删除`TASK_EXECUTION_SEQ`表,并使用相同的名称创建一个序列。例如: + +``` +DROP TABLE TASK_SEQ; + +CREATE SEQUENCE [DBO].[TASK_SEQ] AS BIGINT + START WITH 1 + INCREMENT BY 1; +``` + +| |将`START WITH`设置为高于当前执行 ID 的值。| +|---|----------------------------------------------------------------------| + +[](#appendix-building-the-documentation)[21.构建这个文档](#appendix-building-the-documentation) +---------- + +该项目使用 Maven 来生成该文档。要为自己生成它,请运行以下命令:`$ ./mvnw clean package -P full`。 + +[](#appendix-cloud-foundry)[22.在 Cloud Foundry 上运行任务应用程序](#appendix-cloud-foundry) +---------- + +Spring 云任务应用程序在 Cloud Foundry 上作为任务启动的最简单方法是使用 Spring 云数据流。 Spring 通过云数据流,你可以注册你的任务应用程序,为其创建定义,然后启动它。然后,你可以通过 RESTful API、 Spring Cloud Data Flow Shell 或 UI 跟踪任务执行。要了解如何开始安装数据流,请遵循参考文档[Getting Started](https://docs.spring.io/spring-cloud-dataflow/docs/current/reference/htmlsingle/#getting-started)部分中的说明。有关如何注册和启动任务的信息,请参见[任务的生命周期](https://docs.spring.io/spring-cloud-dataflow/docs/current/reference/htmlsingle/#_the_lifecycle_of_a_task)文档。 diff --git a/docs/spring-cloud/spring-cloud-vault.md b/docs/spring-cloud/spring-cloud-vault.md new file mode 100644 index 0000000000000000000000000000000000000000..1a3b72f4da89b33fc857500b28402c6a5f708160 --- /dev/null +++ b/docs/spring-cloud/spring-cloud-vault.md @@ -0,0 +1,1772 @@ +Spring Cloud Vault +========== + +Spring Cloud Vault Config 为分布式系统中的外部化配置提供了客户端支持。有了[HashiCorp 的保险库](https://www.vaultproject.io),你就有了一个中心位置来管理跨所有环境的应用程序的外部秘密属性。Vault 可以管理静态和动态秘密,例如远程应用程序/资源的用户名/密码,并为外部服务(例如 MySQL、PostgreSQL、Apache Cassandra、CouchBase、MongoDB、Consul、AWS 等)提供凭据。 + +[](#new-noteworthy)[1.新的和值得注意的](#new-noteworthy) +---------- + +本节简要介绍了最新版本中新的和值得注意的项目。 + +### [](#new-in-3.0.0)[1.1. New in Spring Cloud Vault 3.0](#new-in-3.0.0) ### + +* 将`PropertySource`初始化从 Spring cloud 的 bootstrap 上下文迁移到 Spring boot 的[ConfigData API](#vault.configdata)。 + +* 支持[CouchBase 数据库](#vault.config.backends.couchbase)后端。 + +* 通过`spring.cloud.vault.ssl.key-store-type=…`/` Spring.cloud.vault.ssl.trust-store-type=…` 配置 keystore/truststore 类型,包括 PEM 支持。 + +* 通过配置`ReactiveVaultEndpointProvider`来支持`ReactiveDiscoveryClient`。 + +* 支持配置[多个数据库](#vault.config.backends.databases)。 + +[](#quick-start)[2. Quick Start](#quick-start) +---------- + +**先决条件** + +要开始使用 Vault 和本指南,你需要一个 \* 类似于 Nix 的操作系统,该操作系统提供: + +* `wget`,`openssl`和`unzip` + +* 至少有一个 Java8 和一个正确配置的`JAVA_HOME`环境变量 + +| |本指南从 Spring Cloud Vault 的角度解释了 Vault 的设置,用于集成测试。
你可以直接在 Vault 项目站点上找到入门指南:[learn.HashiCorp.com/vault](https://learn.hashicorp.com/vault)| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +**安装保险库** + +``` +$ wget https://releases.hashicorp.com/vault/${vault_version}/vault_${vault_version}_${platform}.zip +$ unzip vault_${vault_version}_${platform}.zip +``` + +| |这些步骤可以通过下载和运行[install_vault.sh](https://github.com/spring-cloud/spring-cloud-vault/blob/master/src/test/bash/install_vault.sh)来实现。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +**为 Vault 创建 SSL 证书** + +接下来,你需要生成一组证书: + +* 根 CA + +* 保险库证书(解密密钥`work/ca/private/localhost.decrypted.key.pem`和证书`work/ca/certs/localhost.cert.pem`) + +确保将根证书导入到兼容 Java 的信任存储库中。 + +实现这一点的最简单方法是使用 OpenSSL。 + +| |[create_certificates.sh](https://github.com/spring-cloud/spring-cloud-vault/blob/master/src/test/bash/)在`work/ca`和一个 JKS 信任存储库`work/keystore.jks`中创建证书。
如果你想使用此快速启动指南运行 Spring Cloud Vault,则需要将信任存储库中的`spring.cloud.vault.ssl.trust-store`属性配置为`file:work/keystore.jks`。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +**启动 Vault 服务器** + +接下来,按照以下内容创建一个配置文件: + +``` +backend "inmem" { +} + +listener "tcp" { + address = "0.0.0.0:8200" + tls_cert_file = "work/ca/certs/localhost.cert.pem" + tls_key_file = "work/ca/private/localhost.decrypted.key.pem" +} + +disable_mlock = true +``` + +| |你可以在[`vault.conf`](https://github.com/spring-clod/spring-cloud-vault/blob/master/src/test/bash/vault.conf)找到一个示例配置文件。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------| + +``` +$ vault server -config=vault.conf +``` + +使用`inmem`存储和`https`在`0.0.0.0:8200`上开始监听 Vault。保险库是密封的,在启动时没有初始化。 + +| |如果要运行测试,请保持 Vault 未初始化。
测试将初始化 Vault 并创建根令牌`00000000-0000-0000-0000-000000000000`。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------| + +如果你想为你的应用程序使用 Vault,或者尝试一下它,那么你需要首先对它进行初始化。 + +``` +$ export VAULT_ADDR="https://localhost:8200" +$ export VAULT_SKIP_VERIFY=true # Don't do this for production +$ vault operator init +``` + +你应该看到这样的东西: + +``` +Key 1: 7149c6a2e16b8833f6eb1e76df03e47f6113a3288b3093faf5033d44f0e70fe701 +Key 2: 901c534c7988c18c20435a85213c683bdcf0efcd82e38e2893779f152978c18c02 +Key 3: 03ff3948575b1165a20c20ee7c3e6edf04f4cdbe0e82dbff5be49c63f98bc03a03 +Key 4: 216ae5cc3ddaf93ceb8e1d15bb9fc3176653f5b738f5f3d1ee00cd7dccbe926e04 +Key 5: b2898fc8130929d569c1677ee69dc5f3be57d7c4b494a6062693ce0b1c4d93d805 +Initial Root Token: 19aefa97-cccc-bbbb-aaaa-225940e63d76 + +Vault initialized with 5 keys and a key threshold of 3. Please +securely distribute the above keys. When the Vault is re-sealed, +restarted, or stopped, you must provide at least 3 of these keys +to unseal it again. + +Vault does not store the master key. Without at least 3 keys, +your Vault will remain permanently sealed. +``` + +Vault 将初始化并返回一组解封键和根令牌。挑出 3 把钥匙,打开保险库。将 Vault 令牌存储在`VAULT_TOKEN`环境变量中。 + +``` +$ vault operator unseal (Key 1) +$ vault operator unseal (Key 2) +$ vault operator unseal (Key 3) +$ export VAULT_TOKEN=(Root token) +# Required to run Spring Cloud Vault tests after manual initialization +$ vault token create -id="00000000-0000-0000-0000-000000000000" -policy="root" +``` + +Spring Cloud Vault 访问不同的资源。默认情况下,秘密后台是启用的,它通过 JSON 端点访问秘密配置设置。 + +HTTP 服务具有以下形式的资源: + +``` +/secret/{application}/{profile} +/secret/{application} +/secret/{defaultContext}/{profile} +/secret/{defaultContext} +``` + +如果在“SpringApplication”(即在常规 Spring 引导应用程序中通常是“Application”)中,将“Application”作为`spring.application.name`注入,则“Profile”是一个活动的配置文件(或以逗号分隔的属性列表)。从 Vault 检索到的属性将按“原样”使用,而不会对属性名称作进一步的前缀。 + +[](#client-side-usage)[3.客户端使用](#client-side-usage) +---------- + +要在应用程序中使用这些特性,只需将其构建为依赖于`spring-cloud-vault-config`的 Spring 引导应用程序(例如,请参见测试用例)。示例 Maven 配置: + +例 1。 POM.xml + +``` + + org.springframework.boot + spring-boot-starter-parent + 2.4.0.RELEASE + + + + + + org.springframework.cloud + spring-cloud-starter-vault-config + 3.1.0 + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + +``` + +然后,你可以创建一个标准的 Spring 启动应用程序,就像这个简单的 HTTP 服务器: + +``` +@SpringBootApplication +@RestController +public class Application { + + @RequestMapping("/") + public String home() { + return "Hello World!"; + } + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} +``` + +当它运行时,如果它正在运行,它将从端口`8200`上的默认本地 Vault 服务器获取外部配置。要修改启动行为,你可以使用`application.properties`更改保险库服务器的位置,例如 + +示例 2.application.yml + +``` +spring.cloud.vault: + host: localhost + port: 8200 + scheme: https + uri: https://localhost:8200 + connection-timeout: 5000 + read-timeout: 15000 + config: +spring.config.import: vault:// +``` + +* `host`设置保险库主机的主机名。主机名将用于 SSL 证书验证 + +* `port`设置保险库端口 + +* `scheme`将方案设置为`http`将使用普通的 HTTP。支持的方案是`http`和`https`。 + +* `uri`使用 URI 配置保险库端点。优先于主机/端口/方案配置 + +* `connection-timeout`以毫秒为单位设置连接超时 + +* `read-timeout`以毫秒为单位设置读取超时 + +* `spring.config.import`使用所有启用的秘密后端(默认启用键值)将 Vault 挂载为`PropertySource` + +启用进一步的集成需要额外的依赖关系和配置。根据设置 Vault 的方式,你可能需要额外的配置,如[SSL](https://cloud.spring.io/spring-cloud-vault/reference/html/#vault.config.ssl)和[authentication](https://cloud.spring.io/spring-cloud-vault/reference/html/#vault.config.authentication)。 + +如果应用程序导入`spring-boot-starter-actuator`项目,那么 Vault 服务器的状态将通过`/health`端点可用。 + +可以通过属性`management.health.vault.enabled`启用或禁用 Vault 健康指示器(默认为`true`)。 + +| |在 Spring Cloud Vault3.0 和 Spring Boot2.4 中,对属性源的 Bootstrap 上下文初始化(`bootstrap.yml`,`bootstrap.properties`)被弃用。相反, Spring Cloud Vault 支持 Spring Boot 的 Config Data API,该 API 允许从 Vault 导入配置。使用 Spring 引导配置数据方法,你需要设置`spring.config.import`属性才能绑定到 Vault。你可以在[配置数据位置部分](#vault.configdata.locations)中阅读有关它的更多信息。
你可以通过设置配置属性`spring.cloud.bootstrap.enabled=true`或包括依赖项`org.springframework.cloud:spring-cloud-starter-bootstrap`来启用引导程序上下文。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#authentication)[3.1.认证](#authentication) ### + +Vault 需要[认证机制](https://www.vaultproject.io/docs/concepts/auth.html)到[授权客户请求](https://www.vaultproject.io/docs/concepts/tokens.html)。 + +Spring Cloud Vault 支持多个[认证机制](https://cloud.spring.io/spring-cloud-vault/reference/html/#vault.config.authentication)来使用 Vault 对应用程序进行身份验证。 + +对于快速启动,使用由[保险库初始化](#quickstart.vault.start)打印的根令牌。 + +示例 3.application.yml + +``` +spring.cloud.vault: + token: 19aefa97-cccc-bbbb-aaaa-225940e63d76 +spring.config.import: vault:// +``` + +| |仔细考虑你的安全需求。
静态令牌身份验证很好,如果你想快速地开始使用 Vault,但是静态令牌不会受到进一步的保护。
向非预期的各方披露的任何信息都允许 Vault 与相关的令牌角色一起使用。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[](#vault.configdata)[4.配置数据 API](#vault.configdata) +---------- + +Spring 自版本 2.4 起,启动提供了一个 ConfigData API,该 API 允许声明配置源并将其导入为属性源。 + +Spring Cloud Vault 从 3.0 版本开始使用 ConfigData API 来挂载 Vault 的秘密后端作为属性源。在以前的版本中,使用了引导程序上下文。ConfigData API 要灵活得多,因为它允许指定要导入哪些配置系统以及导入的顺序。 + +| |你可以通过设置配置属性`spring.cloud.bootstrap.enabled=true`或包括依赖项`org.springframework.cloud:spring-cloud-starter-bootstrap`来启用已弃用的引导程序上下文。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#vault.configdata.locations)[4.1.配置数据位置](#vault.configdata.locations) ### + +你可以通过从 Vault 实现的一个或多个`PropertySource`挂载 Vault 配置。 Spring Cloud Vault 支持两个配置位置: + +* `vault://`(默认位置) + +* `vault:///`(上下文位置) + +对所有启用的[Secret Backends](#vault.config.backends)使用默认的位置挂载属性源。在没有进一步配置的情况下, Spring Cloud Vault 在`/secret/${spring.application.name}`处挂载键值后端。每个激活的配置文件都会在`/secret/${spring.application.name}/${profile}`表单之后添加另一个上下文路径。向 Classpath 中添加更多的模块,例如`spring-cloud-config-databases`,提供了额外的秘密后端配置选项,如果启用,这些选项将被挂载为属性源。 + +如果要控制从 Vault 挂载哪些上下文路径为`PropertySource`,可以使用上下文位置或配置[“VaultConfigurer”](#vault.config.backends.configurer)。 + +上下文位置是单独指定和挂载的。 Spring Cloud Vault 将每个位置挂载为唯一的`PropertySource`。你可以将默认位置与上下文位置(或其他配置系统)混合,以控制属性源的顺序。如果你想禁用缺省键值路径计算并自己挂载每个键值后端,那么这种方法特别有用。 + +示例 4.application.yml + +``` +spring.config.import: vault://first/context/path, vault://other/path, vault:// +``` + +Spring `Environment`中的属性名称必须是唯一的,以避免出现阴影。如果你在不同的上下文路径中使用相同的秘密名称,并且希望将这些秘密名称作为单独的属性公开,则可以通过向位置添加`prefix`查询参数来区分它们。 + +示例 5.application.yml + +``` +spring.config.import: vault://my/path?prefix=foo., vault://my/other/path?prefix=bar. +secret: ${foo.secret} +other.secret: ${bar.secret} +``` + +| |前缀按原样添加到 Vault 返回的所有属性名称中。如果你希望在前缀和键名之间用一个点分隔键名,请确保在前缀中添加一个尾随的点。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#vault.configdata.location.optional)[4.2.有条件地启用/禁用保险库配置](#vault.configdata.location.optional) ### + +在某些情况下,可能需要在没有 Vault 的情况下启动应用程序。你可以通过 Location 字符串表示 Vault Config 位置应该是可选的还是强制的(默认): + +* `optional:vault://`(默认位置) + +* `optional:vault:///`(上下文位置) + +如果通过`spring.cloud.vault.enabled=false`禁用了 Vault 支持,则在应用程序启动期间跳过可选位置。 + +| |无论配置位置是否标记为可选的,都会跳过无法找到的 Vault 上下文路径(HTTP STATUS404)。[Vault 客户端快速失败](#vault.config.fail-fast)如果由于 HTTP STATUS404 而找不到保险库上下文路径,则允许在启动时失败。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#vault.configdata.customization)[4.3.基础设施定制](#vault.configdata.customization) ### + +Spring Cloud Vault 需要基础设施类与 Vault 进行交互。当不使用 ConfigData API(这意味着你还没有指定`spring.config.import=vault://`或上下文 Vault 路径)时, Spring Cloud Vault 通过`VaultAutoConfiguration`和`VaultReactiveAutoConfiguration`定义其 bean。 Spring 在 Spring 上下文可用之前引导应用程序。因此`VaultConfigDataLoader`注册 bean 本身,以便稍后将其传播到应用程序上下文中。 + +你可以通过使用`Bootstrapper`API 注册自定义实例来定制 Spring Cloud Vault 使用的基础架构: + +``` +InstanceSupplier builderSupplier = ctx -> RestTemplateBuilder + .builder() + .requestFactory(ctx.get(ClientFactoryWrapper.class).getClientHttpRequestFactory()) + .defaultHeader("X-Vault-Namespace", "my-namespace"); + +SpringApplication application = new SpringApplication(MyApplication.class); +application.addBootstrapper(registry -> registry.register(RestTemplateBuilder.class, builderSupplier)); +``` + +另请参见[自定义要作为 PropertySource 公开的秘密后端](#vault.config.backends.configurer)和`VaultConfigDataLoader`的自定义钩源。 + +[](#vault.config.authentication)[5.认证方法](#vault.config.authentication) +---------- + +不同的组织对安全性和身份验证有不同的要求。Vault 通过提供多种身份验证方法来反映这种需求。 Spring Cloud Vault 支持令牌和 APPID 身份验证。 + +### [](#vault.config.authentication.token)[5.1.令牌认证](#vault.config.authentication.token) ### + +令牌是在 Vault 中进行身份验证的核心方法。令牌身份验证需要使用配置提供一个静态令牌。作为后备,也可以从`~/.vault-token`检索令牌,这是 Vault CLI 用于缓存令牌的默认位置。 + +| |令牌身份验证是默认的身份验证方法。
如果一个令牌被公开,则非预期的一方获得对保险库的访问权限,并可以为预期的客户端访问机密。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +示例 6.application.yml + +``` +spring.cloud.vault: + authentication: TOKEN + token: 00000000-0000-0000-0000-000000000000 +``` + +* `authentication`将该值设置为`TOKEN`选择令牌身份验证方法 + +* `token`设置要使用的静态令牌。如果丢失或为空,则将尝试从 \~/.vault-token 检索令牌。 + +另见: + +* [保险库文档:令牌](https://www.vaultproject.io/docs/concepts/tokens.html) + +* [保险库文档:CLI 登录](https://www.vaultproject.io/docs/commands/login) + +* [Vault 文档:CLI 默认为 \~/.vault-token](https://www.vaultproject.io/docs/commands/token-helper) + +### [](#vault.config.authentication.vault-agent)[5.2.保险库代理身份验证](#vault.config.authentication.vault-agent) ### + +Vault 自 0.11.0 版本以来,通过 Vault Agent 提供了一个 Sidecar 实用程序。Vault Agent 通过其自动验证功能实现了 Spring Vault 的`SessionManager`的功能。应用程序可以通过依赖运行在`localhost`上的 Vault 代理重用缓存的会话凭据。 Spring Vault 可以在没有“X-Vault-Token”头的情况下发送请求。禁用 Spring Vault 的身份验证基础设施,以禁用客户端身份验证和会话管理。 + +示例 7.application.yml + +``` +spring.cloud.vault: + authentication: NONE +``` + +* `authentication`将该值设置为`NONE`将禁用`ClientAuthentication`和`SessionManager`。 + +另见:[保险库文档:代理](https://www.vaultproject.io/docs/agent/index.html) + +### [](#vault.config.authentication.appid)[5.3.APPID 身份验证](#vault.config.authentication.appid) ### + +Vault 支持[AppId](https://www.vaultproject.io/docs/auth/app-id.html)身份验证,该验证由两个难以猜测的令牌组成。APPID 默认为静态配置的`spring.application.name`。第二个标记是 userid,它是由应用程序决定的一部分,通常与运行时环境相关。IP 地址、MAC 地址或 Docker 容器名称都是很好的例子。 Spring Cloud Vault Config 支持 IP 地址、MAC 地址和静态用户 ID(例如,通过系统属性提供)。IP 和 MAC 地址表示为十六进制编码的 SHA256 散列。 + +基于 IP 地址的用户 ID 使用本地主机的 IP 地址。 + +示例 8.使用 SHA256IP 地址 userid 的 application.yml + +``` +spring.cloud.vault: + authentication: APPID + app-id: + user-id: IP_ADDRESS +``` + +* `authentication`将该值设置为`APPID`选择 APPID 身份验证方法 + +* `app-id-path`设置要使用的 appid 挂载的路径 + +* `user-id`设置 userid 方法。可能的值是`IP_ADDRESS`、`mac_address’或实现自定义`AppIdUserIdMechanism`的类名 + +从命令行生成 IP 地址 userid 的相应命令是: + +``` +$ echo -n 192.168.99.1 | sha256sum +``` + +| |包含`echo`的换行将导致不同的散列值,因此请确保包含`-n`标志。| +|---|---------------------------------------------------------------------------------------------------------| + +基于 MAC 地址的用户 ID 从本地主机绑定的设备获得他们的网络设备。该配置还允许指定`network-interface`提示来选择正确的设备。“network-interface”的值是可选的,可以是接口名称或接口索引(基于 0)。 + +示例 9.使用 SHA256MAC-Address Userid 的 application.yml + +``` +spring.cloud.vault: + authentication: APPID + app-id: + user-id: MAC_ADDRESS + network-interface: eth0 +``` + +* `network-interface`设置网络接口以获取物理地址 + +从命令行生成 IP 地址 userid 的相应命令是: + +``` +$ echo -n 0AFEDE1234AC | sha256sum +``` + +| |MAC 地址是大写的,不带冒号。
包括`echo`的换行将导致不同的散列值,因此请确保包含`-n`标志。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### [](#custom-userid)[5.3.1.自定义用户 ID](#custom-userid) #### + +用户 ID 生成是一种开放机制。你可以将 ` Spring.cloud.vault.app-id.user-id` 设置为任意字符串,配置的值将用作静态 userid。 + +一种更高级的方法允许你将`spring.cloud.vault.app-id.user-id`设置为类名。这个类必须位于你的 Classpath 上,并且必须实现`org.springframework.cloud.vault.AppIdUserIdMechanism`接口和`createUserId`方法。 Spring Cloud Vault 将在每次使用 APPID 进行身份验证以获得令牌时通过调用来获得用户 ID。 + +示例 10.application.yml + +``` +spring.cloud.vault: + authentication: APPID + app-id: + user-id: com.examlple.MyUserIdMechanism +``` + +例 11。MyuseridMechanism.java + +``` +public class MyUserIdMechanism implements AppIdUserIdMechanism { + + @Override + public String createUserId() { + String userId = ... + return userId; + } +} +``` + +另见:[Vault 文档:使用应用程序 ID Auth 后台](https://www.vaultproject.io/docs/auth/app-id.html) + +### [](#approle-authentication)[5.4.Approle 身份验证](#approle-authentication) ### + +[AppRole](https://www.vaultproject.io/docs/auth/app-id.html)用于机器身份验证,就像不推荐的(因为 Vault0.6.1)[APPID 身份验证](#vault.config.authentication.appid)一样。Approle 身份验证由两个难以猜测的(秘密)令牌组成:ROLEID 和 SECTROTID。 + +Spring Vault 支持各种接近场景(推/拉模式和包装)。 + +Roleid 和可选的 SecretID 必须由配置提供, Spring Vault 不会查找这些或创建自定义的 SecretID。 + +示例 12.approle 身份验证属性的 application.yml + +``` +spring.cloud.vault: + authentication: APPROLE + app-role: + role-id: bde2076b-cccb-3cf0-d57e-bca7b1e83a52 +``` + +根据所需的配置细节,支持以下场景: + +|**方法**|**RoleId**|**SecretId**|**RoleName**|**Token**| +|---------------------------------|----------|------------|------------|---------| +|提供了 ROLEID/SECTRID| Provided | Provided | | | +|提供不带分泌物的轮状结构| Provided | | | | +|提供 Roleid,pull secretid| Provided | Provided | Provided |Provided | +|拉 Roleid,提供秘密| | Provided | Provided |Provided | +|全拉模式| | | Provided |Provided | +|包装| | | |Provided | +|包裹罗雷德,提供秘密| Provided | | |Provided | +|提供保鲜膜,包装保鲜膜| | Provided | |Provided | + +|**RoleId**|**SecretId**|**支持**| +|----------|------------|-------------| +| Provided | Provided |✅| +| Provided | Pull |✅| +| Provided | Wrapped |✅| +| Provided | Absent |✅| +| Pull | Provided |✅| +| Pull | Pull |✅| +| Pull | Wrapped |❌| +| Pull | Absent |❌| +| Wrapped | Provided |✅| +| Wrapped | Pull |❌| +| Wrapped | Wrapped |✅| +| Wrapped | Absent |❌| + +| |通过在上下文中提供已配置的`AppRoleAuthentication` Bean,仍然可以使用推/拉/包装模式的所有组合。
Spring Cloud Vault 无法从配置属性中派生出所有可能的近似组合。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |Approle 身份验证仅限于使用反应性基础设施的简单 pull 模式。
尚未支持完全 pull 模式。
使用 Spring Cloud Vault 和 Spring WebFlux 堆栈,可以通过设置`spring.cloud.vault.reactive.enabled=false`禁用 Vault 的反应性自动配置。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +示例 13.application.yml 与所有 approle 身份验证属性 + +``` +spring.cloud.vault: + authentication: APPROLE + app-role: + role-id: bde2076b-cccb-3cf0-d57e-bca7b1e83a52 + secret-id: 1696536f-1976-73b1-b241-0b4213908d39 + role: my-role + app-role-path: approle +``` + +* `role-id`设置 ROLEID。 + +* `secret-id`设置分泌物。如果配置了 approle 而不需要 secretid,则可以省略 secretid(参见`bind_secret_id`)。 + +* `role`:设置 pull 模式的 approle 名称。 + +* `app-role-path`设置要使用的 approle 身份验证挂载的路径。 + +另见:[Vault 文档:使用 Approle Auth 后端](https://www.vaultproject.io/docs/auth/approle.html) + +### [](#vault.config.authentication.awsec2)[5.5.AWS-EC2 身份验证](#vault.config.authentication.awsec2) ### + +[aws-ec2](https://www.vaultproject.io/docs/auth/aws-ec2.html)Auth 后端为 AWS EC2 实例提供了一种安全的引入机制,允许自动检索保险库令牌。与大多数 Vault 身份验证后端不同,该后端不需要首次部署或提供安全敏感的凭据(令牌、用户名/密码、客户端证书等)。相反,它将 AWS 视为受信任的第三方,并使用以密码签名的动态元数据信息来唯一地表示每个 EC2 实例。 + +示例 14.使用 AWS-EC2 身份验证的 application.yml + +``` +spring.cloud.vault: + authentication: AWS_EC2 +``` + +在默认情况下,AWS-EC2 身份验证使 Nonce 能够遵循信任第一次使用(Tofu)原则。任何意外获得 PKCS#7 身份元数据访问权限的一方都可以对 Vault 进行身份验证。 + +在第一次登录期间, Spring Cloud Vault 生成一个 Nonce,该 Nonce 存储在实例 ID 旁边的 auth 后台。重新验证需要发送相同的 nonce。其他任何一方都没有 Nonce,可以在 Vault 中发出警报,以进行进一步的调查。 + +nonce 保存在内存中,并在应用程序重新启动时丢失。你可以使用`spring.cloud.vault.aws-ec2.nonce`配置静态 nonce。 + +AWS-EC2 身份验证角色是可选的,并且是 AMI 的默认值。你可以通过设置“ Spring.cloud.vault.aws-ec2.role”属性来配置身份验证角色。 + +示例 15.已配置角色的 application.yml + +``` +spring.cloud.vault: + authentication: AWS_EC2 + aws-ec2: + role: application-server +``` + +示例 16.具有所有 AWS EC2 身份验证属性的 application.yml + +``` +spring.cloud.vault: + authentication: AWS_EC2 + aws-ec2: + role: application-server + aws-ec2-path: aws-ec2 + identity-document: http://... + nonce: my-static-nonce +``` + +* `authentication`将该值设置为`AWS_EC2`选择 AWS EC2 身份验证方法 + +* `role`设置试图登录的角色的名称。 + +* `aws-ec2-path`设置要使用的 AWS EC2 挂载的路径 + +* `identity-document`设置 PKCS#7AWS EC2 身份文档的 URL + +* `nonce`用于 AWS-EC2 身份验证。空的 nonce 默认为 nonce 生成 + +另见:[Vault 文档:使用 AWS Auth 后端](https://www.vaultproject.io/docs/auth/aws.html) + +### [](#vault.config.authentication.awsiam)[5.6.AWS-IAM 身份验证](#vault.config.authentication.awsiam) ### + +[aws](https://www.vaultproject.io/docs/auth/aws-ec2.html)后端为 AWS IAM 角色提供了一种安全的身份验证机制,允许基于正在运行的应用程序的当前 IAM 角色使用 Vault 进行自动身份验证。与大多数 Vault 身份验证后端不同,该后端不需要首次部署或提供安全敏感的凭据(令牌、用户名/密码、客户端证书等)。相反,它将 AWS 视为受信任的第三方,并使用调用者与其 IAM 凭据签署的 4 条信息来验证调用者确实在使用该 IAM 角色。 + +自动计算应用程序正在运行的当前 IAM 角色。如果你在 AWS ECS 上运行你的应用程序,那么应用程序将使用分配给正在运行的容器的 ECS 任务的 IAM 角色。如果你在 EC2 实例之上裸体运行你的应用程序,那么使用的 IAM 角色将是分配给 EC2 实例的角色。 + +当使用 AWS-IAM 身份验证时,你必须在 Vault 中创建一个角色,并将其分配给你的 IAM 角色。一个空的`role`默认为当前 IAM 角色的友好名称。 + +示例 17.application.yml 具有所需的 AWS-IAM 身份验证属性 + +``` +spring.cloud.vault: + authentication: AWS_IAM +``` + +示例 18.具有所有 AWS-IAM 身份验证属性的 application.yml + +``` +spring.cloud.vault: + authentication: AWS_IAM + aws-iam: + role: my-dev-role + aws-path: aws + server-name: some.server.name + endpoint-uri: https://sts.eu-central-1.amazonaws.com +``` + +* `role`设置试图登录的角色的名称。这应该与你的 IAM 角色绑定在一起。如果没有提供,那么将使用当前 IAM 用户的友好名称作为 Vault 角色。 + +* `aws-path`设置要使用的 AWS 挂载的路径 + +* `server-name`设置用于`X-Vault-AWS-IAM-Server-ID`头部的值,以防止某些类型的重播攻击。 + +* `endpoint-uri`设置用于`iam_request_url`参数的 AWS STS API 的值。 + +AWS-IAM 需要 AWS Java SDK 依赖关系(“com.amazonaws:AWS-Java-SDK-Core”),因为身份验证实现使用 AWS SDK 类型来进行凭据和请求签名。 + +另见:[Vault 文档:使用 AWS Auth 后端](https://www.vaultproject.io/docs/auth/aws.html) + +### [](#vault.config.authentication.azuremsi)[5.7.Azure MSI 认证](#vault.config.authentication.azuremsi) ### + +[azure](https://www.vaultproject.io/docs/auth/azure.html)Auth 后端为 Azure VM 实例提供了一种安全的引入机制,允许自动检索 Vault 令牌。与大多数 Vault 身份验证后端不同,该后端不需要首次部署或提供安全敏感的凭据(令牌、用户名/密码、客户端证书等)。相反,它将 Azure 视为可信任的第三方,并使用可绑定到 VM 实例的托管服务标识和实例元数据信息。 + +示例 19.application.yml 与所需的 Azure 身份验证属性 + +``` +spring.cloud.vault: + authentication: AZURE_MSI + azure-msi: + role: my-dev-role +``` + +示例 20.带有所有 Azure 身份验证属性的 application.yml + +``` +spring.cloud.vault: + authentication: AZURE_MSI + azure-msi: + role: my-dev-role + azure-path: azure + metadata-service: http://169.254.169.254/metadata/instance… + identity-token-service: http://169.254.169.254/metadata/identity… +``` + +* `role`设置试图登录的角色的名称。 + +* `azure-path`设置要使用的 Azure mount 的路径 + +* `metadata-service`设置访问实例元数据服务的 URI + +* `identity-token-service`设置访问身份令牌服务的 URI + +Azure MSI 身份验证从实例元数据服务获得有关虚拟机的环境详细信息(订阅 ID、资源组、VM 名称)。Vault 服务器的资源 ID 默认为`[vault.hashicorp.com](https://vault.hashicorp.com)`。要改变这一点,请相应地设置`spring.cloud.vault.azure-msi.identity-token-service`。 + +另见: + +* [Vault 文档:使用 Azure Auth 后台](https://www.vaultproject.io/docs/auth/azure.html) + +* [Azure 文档:Azure 实例元数据服务](https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service) + +### [](#vault.config.authentication.clientcert)[5.8.TLS 证书认证](#vault.config.authentication.clientcert) ### + +`cert`Auth 后台允许使用 SSL/TLS 客户端证书进行身份验证,这些证书由 CA 签名或自签名。 + +要启用`cert`身份验证,你需要: + +1. 使用 SSL,参见[Vault 客户端 SSL 配置](#vault.config.ssl) + +2. 配置包含客户端证书和私钥的 Java`Keystore` + +3. 将`spring.cloud.vault.authentication`设置为`CERT` + +示例 21.application.yml + +``` +spring.cloud.vault: + authentication: CERT + ssl: + key-store: classpath:keystore.jks + key-store-password: changeit + key-store-type: JKS + cert-auth-path: cert +``` + +另见:[Vault 文档:使用 CERTAuth 后端](https://www.vaultproject.io/docs/auth/cert.html) + +### [](#vault.config.authentication.cubbyhole)[5.9.空穴身份验证](#vault.config.authentication.cubbyhole) ### + +Cubbyhole 身份验证使用 Vault 原语提供安全的身份验证工作流。Cubbyhole 身份验证使用令牌作为主要登录方法。一个短暂的令牌用于从 Vault 的 Cubbyhole 秘密后端获得第二个登录 VaultToken。登录令牌通常寿命更长,并用于与 Vault 交互。登录令牌将从存储在`/cubbyhole/response`的包装响应中检索。 + +**创建一个包装好的令牌** + +| |令牌创建的响应包装需要 Vault0.6.0 或更高版本。| +|---|--------------------------------------------------------------------| + +例 22。创建和存储令牌 + +``` +$ vault token-create -wrap-ttl="10m" +Key Value +--- ----- +wrapping_token: 397ccb93-ff6c-b17b-9389-380b01ca2645 +wrapping_token_ttl: 0h10m0s +wrapping_token_creation_time: 2016-09-18 20:29:48.652957077 +0200 CEST +wrapped_accessor: 46b6aebb-187f-932a-26d7-4f3d86a68319 +``` + +示例 23.application.yml + +``` +spring.cloud.vault: + authentication: CUBBYHOLE + token: 397ccb93-ff6c-b17b-9389-380b01ca2645 +``` + +另见: + +* [保险库文档:令牌](https://www.vaultproject.io/docs/concepts/tokens.html) + +* [Vault 文档:Cubbyhole 秘密后端](https://www.vaultproject.io/docs/secrets/cubbyhole/index.html) + +* [保险库文档:响应包装](https://www.vaultproject.io/docs/concepts/response-wrapping.html) + +### [](#vault.config.authentication.gcpgce)[5.10.GCP-GCE 认证](#vault.config.authentication.gcpgce) ### + +[gcp](https://www.vaultproject.io/docs/auth/gcp.html)Auth 后端允许 Vault 通过使用现有的 GCP(Google Cloud Platform)IAM 和 GCE 凭据登录。 + +GCPGCE(Google Compute Engine,Google Compute Engine)身份验证以 JSON Web 令牌的形式为服务帐户创建签名。使用[实例标识](https://cloud.google.com/compute/docs/instances/verifying-instance-identity)从 GCE 元数据服务获得计算引擎实例的 JWT。该 API 创建了一个 JSON Web 令牌,该令牌可用于确认实例标识。 + +与大多数 Vault 身份验证后端不同,该后端不需要首次部署或提供安全敏感的凭据(令牌、用户名/密码、客户端证书等)。相反,它将 GCP 视为受信任的第三方,并使用加密签名的动态元数据信息,该信息唯一地表示每个 GCP 服务帐户。 + +示例 24.application.yml 与所需的 GCP-gce 身份验证属性 + +``` +spring.cloud.vault: + authentication: GCP_GCE + gcp-gce: + role: my-dev-role +``` + +示例 25.具有所有 GCP-GCE 身份验证属性的 application.yml + +``` +spring.cloud.vault: + authentication: GCP_GCE + gcp-gce: + gcp-path: gcp + role: my-dev-role + service-account: [email protected] +``` + +* `role`设置试图登录的角色的名称。 + +* `gcp-path`设置要使用的 GCP 挂载的路径 + +* `service-account`允许将服务帐户 ID 重写为特定值。默认设置为`default`服务帐户。 + +另见: + +* [Vault 文档:使用 GCPAuth 后端](https://www.vaultproject.io/docs/auth/gcp.html) + +* [GCP 文件:验证实例的身份](https://cloud.google.com/compute/docs/instances/verifying-instance-identity) + +### [](#vault.config.authentication.gcpiam)[5.11.GCP-IAM 认证](#vault.config.authentication.gcpiam) ### + +[gcp](https://www.vaultproject.io/docs/auth/gcp.html)Auth 后端允许 Vault 通过使用现有的 GCP(Google Cloud Platform)IAM 和 GCE 凭据登录。 + +GCP,IAM 身份验证以 JSON Web 令牌的形式为服务帐户创建签名。通过调用 GCPIAM 的[`Projects.ServiceAccounts.signjwt’](https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts/signJwt)API,可以获得服务帐户的 JWT。调用者针对 GCPIAM 进行身份验证,并由此证明其身份。此保险库后端将 GCP 视为受信任的第三方。 + +IAM 凭据可以从运行时环境(特别是[google_application_creditions’](https://cloud.google.com/docs/authentication/production)环境变量、Google Compute 元数据服务)获得,也可以从外部提供,例如 JSON 或 Base64 编码。JSON 是首选的表单,因为它带有调用`projects.serviceAccounts.signJwt`所需的项目 ID 和服务帐户标识符。 + +示例 26.带有所需 GCP 的 application.yml-IAM 身份验证属性 + +``` +spring.cloud.vault: + authentication: GCP_IAM + gcp-iam: + role: my-dev-role +``` + +示例 27.具有所有 GCP-IAM 身份验证属性的 application.yml + +``` +spring.cloud.vault: + authentication: GCP_IAM + gcp-iam: + credentials: + location: classpath:credentials.json + encoded-key: e+KApn0= + gcp-path: gcp + jwt-validity: 15m + project-id: my-project-id + role: my-dev-role + service-account-id: [email protected] +``` + +* `role`设置试图登录的角色的名称。 + +* `credentials.location`到包含 JSON 格式的 Google 凭据的凭据资源的路径。 + +* `credentials.encoded-key`以 JSON 格式编码的 OAuth2 帐户私钥的 base64 编码内容。 + +* `gcp-path`设置要使用的 GCP 挂载的路径 + +* `jwt-validity`配置 JWT 令牌有效性。默认值为 15 分钟。 + +* `project-id`允许将项目 ID 重写为特定值。从获得的凭据中获得的项目 ID 的默认值。 + +* `service-account`允许将服务帐户 ID 重写为特定值。从获得的凭据到服务帐户的默认值。 + +GCP 的 IAM 认证需要 Google Cloud Java SDK 依赖关系(“com.google.apis:Google-api-services-IAM”和`com.google.auth:google-auth-library-oauth2-http`),因为认证实现使用 Google API 进行凭据和 JWT 签名。 + +| |Google 凭据需要一个 OAuth2 令牌来维护令牌生命周期。
所有 API 都是同步的,因此,`GcpIamAuthentication`不支持反应性使用所需的`AuthenticationSteps`。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +另见: + +* [Vault 文档:使用 GCPAuth 后端](https://www.vaultproject.io/docs/auth/gcp.html) + +* [GCP 文档:projects.serviceaccounts.signjwt](https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts/signJwt) + +### [](#vault.config.authentication.kubernetes)[5.12.Kubernetes 认证](#vault.config.authentication.kubernetes) ### + +Kubernetes 身份验证机制(从 Vault0.8.3 开始)允许使用 Kubernetes 服务帐户令牌对 Vault 进行身份验证。身份验证是基于角色的,角色绑定到服务帐户名和名称空间。 + +包含 POD 服务帐户的 JWT 令牌的文件将自动挂载在`/var/run/secrets/kubernetes.io/serviceaccount/token`。 + +示例 28.具有所有 Kubernetes 身份验证属性的 application.yml + +``` +spring.cloud.vault: + authentication: KUBERNETES + kubernetes: + role: my-dev-role + kubernetes-path: kubernetes + service-account-token-file: /var/run/secrets/kubernetes.io/serviceaccount/token +``` + +* `role`设置角色。 + +* `kubernetes-path`设置要使用的 Kubernetes 挂载的路径。 + +* `service-account-token-file`设置包含 Kubernetes 服务帐户令牌的文件的位置。默认值为`/var/run/secrets/kubernetes.io/serviceaccount/token`。 + +另见: + +* [保险库文档:Kubernetes](https://www.vaultproject.io/docs/auth/kubernetes.html) + +* [Kubernetes 文档:为 PODS 配置服务帐户](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/) + +### [](#vault.config.authentication.pcf)[5.13.Pivotal CloudFoundry 身份验证](#vault.config.authentication.pcf) ### + +[pcf](https://www.vaultproject.io/docs/auth/pcf.html)Auth 后端为在 Pivotal 的 CloudFoundry 实例中运行的应用程序提供了一种安全的引入机制,允许自动检索保险库令牌。与大多数 Vault 身份验证后端不同,该后端不需要首次部署或配置安全敏感的凭据(令牌、用户名/密码、客户端证书等),因为身份配置由 PCF 本身处理。相反,它将 PCF 视为受信任的第三方,并使用托管实例标识。 + +示例 29.带有必需的 PCF 身份验证属性的 application.yml + +``` +spring.cloud.vault: + authentication: PCF + pcf: + role: my-dev-role +``` + +具有所有 PCF 身份验证属性的 application.yml + +``` +spring.cloud.vault: + authentication: PCF + pcf: + role: my-dev-role + pcf-path: path + instance-certificate: /etc/cf-instance-credentials/instance.crt + instance-key: /etc/cf-instance-credentials/instance.key +``` + +* `role`设置试图登录的角色的名称。 + +* `pcf-path`设置要使用的 PCF 挂载的路径。 + +* `instance-certificate`设置到 PCF 实例身份证书的路径。默认值为`${CF_INSTANCE_CERT}`ENV 变量。 + +* `instance-key`设置到 PCF 实例标识密钥的路径。默认值为`${CF_INSTANCE_KEY}`ENV 变量。 + +| |PCF 身份验证需要 BouncyCastle(BCPKIX-JDK15on)在 Classpath 上进行 RSA PSS 签名。| +|---|-----------------------------------------------------------------------------------------------------| + +另见:[Vault 文档:使用 PCF Auth 后端](https://www.vaultproject.io/docs/auth/pcf.html) + +[](#vault.config.acl)[6.ACL 要求](#vault.config.acl) +---------- + +本节将解释 Spring Vault 访问哪些路径,以便你可以从所需的功能中获得策略声明。 + +|Capability|关联的 HTTP 动词| +|----------|---------------------| +| create |`POST`/`put`| +| read |`GET`| +| update |`POST`/`put`| +| delete |`DELETE`| +| list |`LIST`| + +另见[WWW.vaultproject.io/guides/identity/policies](https://www.vaultproject.io/guides/identity/policies)。 + +### [](#authentication-2)[6.1.认证](#authentication-2) ### + +登录:`POST auth/$authMethod/login` + +### [](#keyvalue-mount-discovery)[6.2.KeyValue Mount 发现](#keyvalue-mount-discovery) ### + +`GET sys/internal/ui/mounts/$mountPath` + +### [](#secretleasecontainer)[6.3.分泌物酶抑制剂](#secretleasecontainer) ### + +`SecretLeaseContainer`根据配置的租赁端点使用不同的路径。 + +`LeaseEndpoints.Legacy` + +* 撤销:`PUT sys/revoke` + +* 续约:`PUT sys/renew` + +`LeaseEndpoints.Leases` + +* 撤销:`PUT sys/leases/revoke` + +* 续约:`PUT sys/leases/renew` + +### [](#session-management)[6.4.会话管理](#session-management) ### + +* 令牌查找:`GET auth/token/lookup-self` + +* 续约:`POST auth/token/renew-self` + +* 撤销:`POST auth/token/revoke-self` + +[](#vault.config.backends)[7.秘密后端](#vault.config.backends) +---------- + +### [](#vault.config.backends.kv.versioned)[7.1.键值后端](#vault.config.backends.kv.versioned) ### + +Spring Cloud Vault 支持两个键值秘密后端,版本控制的(V2)和未版本控制的(V1)。键值后端允许将任意值存储为键值存储。单个上下文可以存储一个或多个键值元组。上下文可以按层次进行组织。 Spring Cloud Vault 确定自己的秘密是否正在使用版本控制,并将路径映射到其适当的 URL。 Spring Cloud Vault 允许使用应用程序名称,以及与活动配置文件相结合的默认上下文名。 + +``` +/secret/{application}/{profile} +/secret/{application} +/secret/{default-context}/{profile} +/secret/{default-context} +``` + +应用程序名称由属性决定: + +* `spring.cloud.vault.kv.application-name` + +* `spring.cloud.vault.application-name` + +* `spring.application.name` + +配置文件是由属性决定的: + +* `spring.cloud.vault.kv.profiles` + +* `spring.profiles.active` + +秘密可以从键值后端的其他上下文中获得,方法是将它们的路径添加到应用程序名称中,并用逗号分隔。例如,给定应用程序名`usefulapp,mysql1,projectx/aws`,将使用这些文件夹中的每个文件夹: + +* `/secret/usefulapp` + +* `/secret/mysql1` + +* `/secret/projectx/aws` + +Spring Cloud Vault 将所有活动配置文件添加到可能的上下文路径列表中。任何活动配置文件都不会跳过使用配置文件名称访问上下文。 + +属性像存储一样公开(即没有额外的前缀)。 + +| |Spring Cloud Vault 在挂载路径和实际上下文路径之间添加`data/`上下文,这取决于挂载是否使用版本控制的键值后端。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +``` +spring.cloud.vault: + kv: + enabled: true + backend: secret + profile-separator: '/' + default-context: application + application-name: my-app + profiles: local, cloud +``` + +* `enabled`将该值设置为`false`将禁用秘密后台配置使用 + +* `backend`设置要使用的秘密挂载的路径 + +* `default-context`设置所有应用程序使用的上下文名 + +* `application-name`覆盖在键值后端中使用的应用程序名 + +* `profiles`覆盖在键值后端中使用的活动配置文件 + +* `profile-separator`在具有配置文件的属性源中将配置文件名与上下文分隔开 + +| |键值秘密后端可以在版本控制(V2)和非版本控制(V1)模式下进行操作。| +|---|--------------------------------------------------------------------------------------------| + +另见: + +* [Vault 文档:使用 KV Secrets 引擎-Version1(通用秘密后端)](https://www.vaultproject.io/docs/secrets/kv/kv-v1.html) + +* [Vault 文档:使用 KV Secrets 引擎-Version2(版本管理的键值后端)](https://www.vaultproject.io/docs/secrets/kv/kv-v2.html) + +### [](#vault.config.backends.consul)[7.2. Consul](#vault.config.backends.consul) ### + +Spring Cloud Vault 可以获得用于 HashiCorp 领事的凭据。Consul 集成要求`spring-cloud-vault-config-consul`依赖关系。 + +例 31。 POM.xml + +``` + + + org.springframework.cloud + spring-cloud-vault-config-consul + 3.1.0 + + +``` + +可以通过设置 ` Spring.cloud.vault.consul.enabled=true`(默认`false`)并提供带有`spring.cloud.vault.consul.role=…`的角色名来启用集成。 + +所获得的令牌存储在`spring.cloud.consul.token`中,因此使用 Spring Cloud Consul 可以提取生成的凭据,而无需进一步配置。你可以通过设置`spring.cloud.vault.consul.token-property`来配置属性名称。 + +``` +spring.cloud.vault: + consul: + enabled: true + role: readonly + backend: consul + token-property: spring.cloud.consul.token +``` + +* `enabled`将该值设置为`true`可启用 Consul 后台配置使用 + +* `role`设置 consul 角色定义的角色名 + +* `backend`设置要使用的 Consul 坐骑的路径 + +* `token-property`设置用于存储 consul ACL 令牌的属性名称 + +另见:[保险库文档:与保险库建立领事关系](https://www.vaultproject.io/docs/secrets/consul/index.html) + +### [](#vault.config.backends.rabbitmq)[7.3. RabbitMQ](#vault.config.backends.rabbitmq) ### + +Spring Cloud Vault 可以获得 RabbitMQ 的凭据。 + +RabbitMQ 集成需要`spring-cloud-vault-config-rabbitmq`依赖项。 + +例 32。 POM.xml + +``` + + + org.springframework.cloud + spring-cloud-vault-config-rabbitmq + 3.1.0 + + +``` + +可以通过设置 ` Spring.cloud.vault.rabbitmq.enabled=true`(默认`false`)并提供带有`spring.cloud.vault.rabbitmq.role=…`的角色名来启用集成。 + +用户名和密码存储在`spring.rabbitmq.username`和`spring.rabbitmq.password`中,因此使用 Spring 引导将获取生成的凭据,而无需进一步配置。你可以通过设置`spring.cloud.vault.rabbitmq.username-property`和 ` Spring.cloud.vault.rabbitmq.password-property` 来配置属性名称。 + +``` +spring.cloud.vault: + rabbitmq: + enabled: true + role: readonly + backend: rabbitmq + username-property: spring.rabbitmq.username + password-property: spring.rabbitmq.password +``` + +* `enabled`将该值设置为`true`可启用 RabbitMQ 后台配置使用 + +* `role`设置 RabbitMQ 角色定义的角色名 + +* `backend`设置要使用的 RabbitMQ 挂载的路径 + +* `username-property`设置存储 RabbitMQ 用户名的属性名 + +* `password-property`设置存储 RabbitMQ 密码的属性名 + +另见:[Vault 文档:使用 Vault 设置 RabbitMQ](https://www.vaultproject.io/docs/secrets/rabbitmq/index.html) + +### [](#vault.config.backends.aws)[7.4. AWS](#vault.config.backends.aws) ### + +Spring Cloud Vault 可以获得 AWS 的凭据。 + +AWS 集成需要`spring-cloud-vault-config-aws`依赖关系。 + +例 33。 POM.xml + +``` + + + org.springframework.cloud + spring-cloud-vault-config-aws + 3.1.0 + + +``` + +可以通过设置 ` Spring.cloud.vault.aws=true`(默认`false`)并提供带有`spring.cloud.vault.aws.role=…`的角色名来启用集成。 + +支持的 AWS 凭据类型: + +* iam\_user(默认) + +* 假定 \_role + +* Federation\_Token + +访问密钥和密钥存储在`cloud.aws.credentials.accessKey`和`cloud.aws.credentials.secretKey`中。因此,使用 Spring Cloud AWS 将在不需要进一步配置的情况下获取生成的凭据。 + +你可以通过设置`spring.cloud.vault.aws.access-key-property`和 ` Spring.cloud.vault.aws.secret-key-property` 来配置属性名称。 + +对于 STS 安全令牌,你可以通过设置`spring.cloud.vault.aws.session-token-key-property`来配置属性名。安全令牌存储在`cloud.aws.credentials.sessionToken`(默认)下。 + +示例:iam\_user + +``` +spring.cloud.vault: + aws: + enabled: true + role: readonly + backend: aws + access-key-property: cloud.aws.credentials.accessKey + secret-key-property: cloud.aws.credentials.secretKey +``` + +示例:假定 \_role + +``` +spring.cloud.vault: + aws: + enabled: true + role: sts-vault-role + backend: aws + credential-type: assumed_role + access-key-property: cloud.aws.credentials.accessKey + secret-key-property: cloud.aws.credentials.secretKey + session-token-key-property: cloud.aws.credentials.sessionToken + ttl: 3600s + role-arn: arn:aws:iam::${AWS_ACCOUNT}:role/sts-app-role +``` + +* `enabled`将该值设置为`true`可启用 AWS 后台配置使用 + +* `role`设置 AWS 角色定义的角色名 + +* `backend`设置要使用的 AWS 挂载的路径 + +* `access-key-property`设置用于存储 AWS 访问密钥的属性名 + +* `secret-key-property`设置存储 AWS 密钥的属性名 + +* `session-token-key-property`设置用于存储 AWS STS 安全令牌的属性名称。 + +* `credential-type`设置用于此后端的 AWS 凭据类型。默认值为`iam_user` + +* 当使用`assumed_role`或`federation_token`时,`ttl`设置 STS 令牌的 TTL。Vault 角色指定的 TTL 的默认值。最小/最大值也仅限于 AWS 对 STS 的支持。 + +* `role-arn`将 IAM 角色设置为在使用`assumed_role`时假设为保险库角色配置了多个 IAM 角色。 + +另见:[Vault 文档:使用 Vault 设置 AWS](https://www.vaultproject.io/docs/secrets/aws/index.html) + +[](#vault.config.backends.database-backends)[8.数据库后端](#vault.config.backends.database-backends) +---------- + +Vault 支持多个数据库秘密后端,以根据配置的角色动态生成数据库凭据。这意味着需要访问数据库的服务不再需要配置凭据:它们可以从 Vault 请求凭据,并使用 Vault 的租赁机制更容易地滚动密钥。 + +Spring Cloud Vault 与这些后端集成: + +* [Database](#vault.config.backends.database) + +* [Apache Cassandra](#vault.config.backends.cassandra) + +* [CouchBase 数据库](#vault.config.backends.couchbase) + +* [Elasticsearch](#vault.config.backends.elasticsearch) + +* [MongoDB](#vault.config.backends.mongodb) + +* [MySQL](#vault.config.backends.mysql) + +* [PostgreSQL](#vault.config.backends.postgresql) + +使用数据库秘密后台需要在配置中启用后台和`spring-cloud-vault-config-databases`依赖关系。 + +Vault 从 0.7.1 开始提供专用的`database`秘密后端,允许通过插件进行数据库集成。你可以通过使用通用数据库后端来使用该特定的后端。确保指定适当的后端路径,例如`spring.cloud.vault.mysql.role.backend=database`。 + +例 34。 POM.xml + +``` + + + org.springframework.cloud + spring-cloud-vault-config-databases + 3.1.0 + + +``` + +| |启用多个符合 JDBC 的数据库将生成凭据,并在默认情况下将它们存储在相同的属性键中,因此需要单独配置 JDBC 秘密的属性名。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#vault.config.backends.database)[8.1. Database](#vault.config.backends.database) ### + +Spring Cloud Vault 可以获得在[WWW.vaultproject.io/api/secret/databases/index.html](https://www.vaultproject.io/api/secret/databases/index.html)处列出的任何数据库的凭据。可以通过设置 ` Spring.cloud.vault.database.enabled=true`(默认`false`)并提供带有`spring.cloud.vault.database.role=…`的角色名来启用集成。 + +虽然数据库后端是通用的,但`spring.cloud.vault.database`专门针对 JDBC 数据库。用户名和密码可从`spring.datasource.username`和`spring.datasource.password`属性中获得,因此使用 Spring 引导将为你的`DataSource`获取生成的凭据,而无需进一步配置。你可以通过设置“ Spring.cloud.vault.database.username-property”和“ Spring.cloud.vault.database.password-property”来配置属性名称。 + +``` +spring.cloud.vault: + database: + enabled: true + role: readonly + backend: database + username-property: spring.datasource.username + password-property: spring.datasource.password +``` + +### [](#vault.config.backends.databases)[8.2.多个数据库](#vault.config.backends.databases) ### + +有时,单个数据库的凭据是不够的,因为一个应用程序可能会连接到两个或更多个同类数据库。从版本 3.0.5 开始, Spring Vault 支持在`spring.cloud.vault.databases.*`名称空间下配置多个数据库秘密后端。 + +配置接受多个数据库后端,以将凭据具体化到指定的属性中。确保适当地配置`username-property`和`password-property`。 + +``` +spring.cloud.vault: + databases: + primary: + enabled: true + role: readwrite + backend: database + username-property: spring.primary-datasource.username + password-property: spring.primary-datasource.password + other-database: + enabled: true + role: readonly + backend: database + username-property: spring.secondary-datasource.username + password-property: spring.secondary-datasource.password +``` + +* ``数据库配置的描述性名称。 + +* `.enabled`将该值设置为`true`可启用数据库后台配置使用 + +* `.role`设置数据库角色定义的角色名 + +* `.backend`设置要使用的数据库挂载的路径 + +* `.username-property`设置存储数据库用户名的属性名。确保使用唯一的属性名称,以避免属性跟踪。 + +* `.password-property`设置存储数据库密码的属性名称,确保使用唯一的属性名称,以避免属性跟踪。 + +另见:[保险库文档:数据库秘密后端](https://www.vaultproject.io/docs/secrets/databases/index.html) + +| |Spring Cloud Vault 不支持在达到最大租赁时间时获取新的凭据并用它们配置你的`DataSource`。
即,如果 Vault 中数据库角色的`max_ttl`被设置为`24h`,这意味着在你的应用程序启动 24 小时后,它将不再能够使用数据库进行身份验证。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#vault.config.backends.cassandra)[8.3.Apache Cassandra](#vault.config.backends.cassandra) ### + +| |`cassandra`后端在 Vault0.7.1 中已被弃用,建议使用`database`后端并将其挂载为`cassandra`。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------| + +Spring Cloud Vault 可以获得 Apache Cassandra 的凭据。可以通过设置 ` Spring.cloud.vault.cassandra.enabled=true`(默认`false`)并提供带有`spring.cloud.vault.cassandra.role=…`的角色名来启用集成。 + +用户名和密码可从`spring.data.cassandra.username`和`spring.data.cassandra.password`属性中获得,因此使用 Spring 引导将获取生成的凭据,而无需进一步配置。你可以通过设置“ Spring.cloud.vault.cassandra.username-property”和“ Spring.cloud.vault.cassandra.password-property”来配置属性名称。 + +``` +spring.cloud.vault: + cassandra: + enabled: true + role: readonly + backend: cassandra + username-property: spring.data.cassandra.username + password-property: spring.data.cassandra.password +``` + +* `enabled`将该值设置为`true`可启用 Cassandra 后台配置使用 + +* `role`设置 Cassandra 角色定义的角色名 + +* `backend`设置要使用的 Cassandra 坐骑的路径 + +* `username-property`设置存储 Cassandra 用户名的属性名 + +* `password-property`设置存储 Cassandra 密码的属性名 + +另见:[Vault 文档:使用 Vault 设置 Apache Cassandra](https://www.vaultproject.io/docs/secrets/cassandra/index.html) + +### [](#vault.config.backends.couchbase)[8.4.CouchBase 数据库](#vault.config.backends.couchbase) ### + +Spring Cloud Vault 可以获得用于 CouchBase 的凭据。可以通过设置 ` Spring.cloud.vault.couchbase.enabled=true`(默认`false`)并提供带有`spring.cloud.vault.couchbase.role=…`的角色名来启用集成。 + +用户名和密码可从`spring.couchbase.username`和`spring.couchbase.password`属性中获得,因此使用 Spring 引导将获取生成的凭据,而无需进一步配置。你可以通过设置 ` Spring.cloud.vault.couchbase.username-property` 和 ` Spring.cloud.vault.couchbase.password-property` 来配置属性名称。 + +``` +spring.cloud.vault: + couchbase: + enabled: true + role: readonly + backend: database + username-property: spring.couchbase.username + password-property: spring.couchbase.password +``` + +* `enabled`将该值设置为`true`可启用 CouchBase 后台配置使用 + +* `role`设置 Couchbase 角色定义的角色名 + +* `backend`设置要使用的 CouchBase 挂载的路径 + +* `username-property`设置存储 CouchBase 用户名的属性名 + +* `password-property`设置存储 Couchbase 密码的属性名 + +另见:[CouchBase 数据库插件文档](https://github.com/hashicorp/vault-plugin-database-couchbase) + +### [](#vault.config.backends.elasticsearch)[8.5.Elasticsearch](#vault.config.backends.elasticsearch) ### + +Spring Cloud Vault 可以获得自 3.0 版本以来用于 ElasticSearch 的凭据。可以通过设置 ` Spring.cloud.vault.elasticsearch.enabled=true`(默认`false`)并提供带有`spring.cloud.vault.elasticsearch.role=…`的角色名来启用集成。 + +用户名和密码可从`spring.elasticsearch.rest.username`和`spring.elasticsearch.rest.password`属性中获得,因此使用 Spring 引导将获取生成的凭据,而无需进一步配置。你可以通过设置 ` Spring.cloud.vault.elasticsearch.username-property` 和 ` Spring.cloud.vault.elasticsearch.password-property` 来配置属性名称。 + +``` +spring.cloud.vault: + elasticsearch: + enabled: true + role: readonly + backend: mongodb + username-property: spring.elasticsearch.rest.username + password-property: spring.elasticsearch.rest.password +``` + +* `enabled`将该值设置为`true`可启用 ElasticSearch 数据库后台配置使用 + +* `role`设置 ElasticSearch 角色定义的角色名 + +* `backend`设置要使用的 ElasticSearch 挂载的路径 + +* `username-property`设置存储 ElasticSearch 用户名的属性名 + +* `password-property`设置存储 ElasticSearch 密码的属性名 + +另见:[Vault 文档:使用 Vault 设置 ElasticSearch](https://www.vaultproject.io/docs/secrets/databases/elasticdb) + +### [](#vault.config.backends.mongodb)[8.6. MongoDB](#vault.config.backends.mongodb) ### + +| |`mongodb`后端在 Vault0.7.1 中已被弃用,建议使用`database`后端并将其挂载为`mongodb`。| +|---|---------------------------------------------------------------------------------------------------------------------------------------| + +Spring Cloud Vault 可以获得 MongoDB 的凭据。可以通过设置 ` Spring.cloud.vault.mongodb.enabled=true`(默认`false`)并提供带有`spring.cloud.vault.mongodb.role=…`的角色名来启用集成。 + +用户名和密码存储在`spring.data.mongodb.username`和`spring.data.mongodb.password`中,因此使用 Spring 引导将获取生成的凭据,而无需进一步配置。你可以通过设置 ` Spring.cloud.vault.mongodb.username-property` 和 ` Spring.cloud.vault.mongodb.password-property` 来配置属性名称。 + +``` +spring.cloud.vault: + mongodb: + enabled: true + role: readonly + backend: mongodb + username-property: spring.data.mongodb.username + password-property: spring.data.mongodb.password +``` + +* `enabled`将该值设置为`true`可启用 MongoDB 后台配置使用 + +* `role`设置 MongoDB 角色定义的角色名 + +* `backend`设置要使用的 MongoDB mount 的路径 + +* `username-property`设置存储 MongoDB 用户名的属性名 + +* `password-property`设置存储 MongoDB 密码的属性名 + +另见:[Vault 文档:使用 Vault 建立 MongoDB](https://www.vaultproject.io/docs/secrets/mongodb/index.html) + +### [](#vault.config.backends.mysql)[8.7. MySQL](#vault.config.backends.mysql) ### + +| |`mysql`后端在 Vault0.7.1 中已被弃用,建议使用`database`后端并将其挂载为`mysql`。
`spring.cloud.vault.mysql`的配置将在未来的版本中删除。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +Spring Cloud Vault 可以获得用于 MySQL 的凭据。可以通过设置 ` Spring.cloud.vault.mysql.enabled=true`(默认`false`)并提供带有`spring.cloud.vault.mysql.role=…`的角色名来启用集成。 + +用户名和密码可从`spring.datasource.username`和`spring.datasource.password`属性中获得,因此使用 Spring 引导将获取生成的凭据,而无需进一步配置。你可以通过设置“ Spring.cloud.vault.mysql.username-property”和“ Spring.cloud.vault.mysql.password-property”来配置属性名称。 + +``` +spring.cloud.vault: + mysql: + enabled: true + role: readonly + backend: mysql + username-property: spring.datasource.username + password-property: spring.datasource.password +``` + +* `enabled`将该值设置为`true`可启用 MySQL 后台配置使用 + +* `role`设置 MySQL 角色定义的角色名 + +* `backend`设置要使用的 MySQL 挂载的路径 + +* `username-property`设置存储 MySQL 用户名的属性名 + +* `password-property`设置存储 MySQL 密码的属性名 + +另见:[Vault 文档:使用 Vault 设置 MySQL](https://www.vaultproject.io/docs/secrets/mysql/index.html) + +### [](#vault.config.backends.postgresql)[8.8. PostgreSQL](#vault.config.backends.postgresql) ### + +| |`postgresql`后端在 Vault0.7.1 中已被弃用,建议使用`database`后端并将其挂载为`postgresql`。
`spring.cloud.vault.postgresql`的配置将在未来的版本中删除。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +Spring Cloud Vault 可以获得用于 PostgreSQL 的凭据。可以通过设置 ` Spring.cloud.vault.postgreSQL.enabled=true`(默认`false`)并提供带有`spring.cloud.vault.postgresql.role=…`的角色名来启用集成。 + +用户名和密码可从`spring.datasource.username`和`spring.datasource.password`属性中获得,因此使用 Spring 引导将获取生成的凭据,而无需进一步配置。你可以通过设置 ` Spring.cloud.vault.postgreSQL.username-property` 和 ` Spring.cloud.vault.postgreSQL.password-property` 来配置属性名称。 + +``` +spring.cloud.vault: + postgresql: + enabled: true + role: readonly + backend: postgresql + username-property: spring.datasource.username + password-property: spring.datasource.password +``` + +* `enabled`将该值设置为`true`可启用 PostgreSQL 后台配置使用 + +* `role`设置 PostgreSQL 角色定义的角色名 + +* `backend`设置要使用的 PostgreSQL 挂载的路径 + +* `username-property`设置存储 PostgreSQL 用户名的属性名 + +* `password-property`设置存储 PostgreSQL 密码的属性名 + +另见:[Vault 文档:使用 Vault 设置 PostgreSQL](https://www.vaultproject.io/docs/secrets/postgresql/index.html) + +[](#vault.config.backends.configurer)[9.自定义要作为 PropertySource 公开的秘密后端](#vault.config.backends.configurer) +---------- + +Spring Cloud Vault 使用基于属性的配置来为键值和已发现的秘密后端创建`PropertySource`s。 + +已发现的后端提供`VaultSecretBackendDescriptor`bean 来描述使用秘密后端的配置状态为`PropertySource`。需要一个`SecretBackendMetadataFactory`来创建一个`SecretBackendMetadata`对象,该对象包含路径、名称和属性转换配置。 + +`SecretBackendMetadata`用于支持特定的`PropertySource`。 + +你可以注册`VaultConfigurer`以进行定制。如果提供`VaultConfigurer`,则禁用默认的键值和已发现的后端注册。但是,你可以使用“secretbackendconfigurer.registerdefaultKeyValuesecretBackends()”和启用默认注册。 + +``` +public class CustomizationBean implements VaultConfigurer { + + @Override + public void addSecretBackends(SecretBackendConfigurer configurer) { + + configurer.add("secret/my-application"); + + configurer.registerDefaultKeyValueSecretBackends(false); + configurer.registerDefaultDiscoveredSecretBackends(true); + } +} +``` + +``` +SpringApplication application = new SpringApplication(MyApplication.class); +application.addBootstrapper(VaultBootstrapper.fromConfigurer(new CustomizationBean())); +``` + +[](#vault.config.backends.custom)[10.自定义秘密后端实现](#vault.config.backends.custom) +---------- + +Spring Cloud Vault 附带对最常见的后端集成的秘密后端支持。你可以通过提供一个实现来与任何类型的后端集成,该实现描述了如何从要使用的后端获取数据,以及如何通过提供`PropertyTransformer`来处理该后端提供的数据。 + +为后端添加自定义实现需要实现两个接口: + +* `org.springframework.cloud.vault.config.VaultSecretBackendDescriptor` + +* `org.springframework.cloud.vault.config.SecretBackendMetadataFactory` + +`VaultSecretBackendDescriptor`通常是保存配置数据的对象,例如`VaultDatabaseProperties`。 Spring Cloud Vault 要求你的类型被注释为`@ConfigurationProperties`,以从配置中实现类。 + +`SecretBackendMetadataFactory`接受`VaultSecretBackendDescriptor`以创建实际的`SecretBackendMetadata`对象,该对象保存 Vault 服务器内的上下文路径、解析参数化上下文路径所需的任何路径变量和`PropertyTransformer`。 + +`VaultSecretBackendDescriptor`和`SecretBackendMetadataFactory`类型都必须在`spring.factories`中注册,这是 Spring 提供的一种扩展机制,类似于 Java 的 ServiceLoader。 + +[](#service-registry-configuration)[11.服务注册中心配置](#service-registry-configuration) +---------- + +你可以使用`DiscoveryClient`(例如 from Spring Cloud Consul)通过设置 Spring.cloud.vault.discovery.enabled=true(默认`false`)来定位 Vault 服务器。这样做的最终结果是,你的应用程序需要一个具有适当的发现配置的 application.yml(或环境变量)。好处是,只要发现服务是一个固定点,保险库就可以更改其坐标。默认的服务 ID 是`vault`,但是你可以在客户机上使用 ` Spring.cloud.vault.Discovery.ServiceID’更改它。 + +发现客户机实现都支持某种元数据映射(例如,对于 Eureka,我们有 eureka.instance.metadatamap)。服务的一些附加属性可能需要在其服务注册元数据中进行配置,以便客户端能够正确地连接。不提供传输层安全性详细信息的服务注册中心需要提供一个`scheme`元数据条目,将其设置为`https`或`http`。如果没有配置任何方案,并且该服务不作为安全服务公开,那么配置默认为`spring.cloud.vault.scheme`,如果未设置该配置,则为`https`。 + +``` +spring.cloud.vault.discovery: + enabled: true + service-id: my-vault-service +``` + +[](#vault.config.fail-fast)[12.Vault 客户端快速失败](#vault.config.fail-fast) +---------- + +在某些情况下,如果服务无法连接到 Vault 服务器,则可能希望服务启动失败。如果这是期望的行为,那么设置 bootstrap 配置属性 ` Spring.cloud.vault.fail-fast=true`,客户端将异常停止。 + +``` +spring.cloud.vault: + fail-fast: true +``` + +[](#vault.config.namespaces)[13.Vault Enterprise 命名空间支持](#vault.config.namespaces) +---------- + +Vault Enterprise 允许使用名称空间来隔离单个 Vault 服务器上的多个 Vault。在使用 vault`resttemplate’或`WebClient`时,通过设置 ` Spring.cloud.vault.namespace=…` 在每个传出的 HTTP 请求上启用名称空间头 `x-vault-namespace’来配置名称空间。 + +请注意,Vault Community Edition 不支持此功能,并且对 Vault 操作没有影响。 + +``` +spring.cloud.vault: + namespace: my-namespace +``` + +另见:[Vault Enterprise:名称空间](https://www.vaultproject.io/docs/enterprise/namespaces/index.html) + +[](#vault.config.ssl)[14.Vault 客户端 SSL 配置](#vault.config.ssl) +---------- + +可以通过设置各种属性来声明性地配置 SSL。你可以设置`javax.net.ssl.trustStore`来配置 JVM 范围内的 SSL 设置,也可以设置`spring.cloud.vault.ssl.trust-store`来仅为 Spring Cloud Vault 配置设置 SSL 设置。 + +``` +spring.cloud.vault: + ssl: + trust-store: classpath:keystore.jks + trust-store-password: changeit + trust-store-type: JKS + enabled-protocols: TLSv1.2,TLSv1.3 + enabled-cipher-suites: TLS_AES_128_GCM_SHA256 +``` + +* `trust-store`设置信任存储区的资源。SSL 安全的 Vault 通信将使用指定的信任存储区验证 Vault SSL 证书。 + +* `trust-store-password`设置信任存储密码 + +* `trust-store-type`设置信任存储类型。支持的值都支持`KeyStore`类型,包括`PEM`。 + +* `enabled-protocols`设置启用的 SSL/TLS 协议的列表(自 3.0.2 起)。 + +* `enabled-cipher-suites`设置启用的 SSL/TLS 密码套件的列表(自 3.0.2 起)。 + +请注意,配置`spring.cloud.vault.ssl.*`只能在 Apache HTTP 组件或 OKHTTP 客户机位于类路径上时才能应用。 + +[](#vault-lease-renewal)[15.租赁生命周期管理(更新和撤销)](#vault-lease-renewal) +---------- + +对于每个秘密,Vault 都会创建一个租约:元数据,其中包含时间持续时间、可更新性等信息。 + +Vault 承诺,这些数据将在给定的持续时间或生存时间内有效。一旦租约到期,Vault 可以撤销该数据,并且该秘密的使用者不能再确定它是否有效。 + +Spring Cloud Vault 维护的租赁生命周期超出了登录令牌和秘密的创建。也就是说,与租赁相关的登录令牌和秘密将在租赁到期之前进行更新,直到终端到期。应用程序关闭撤销已获得的登录令牌和可更新的租约。 + +秘密服务和数据库后台(例如 MongoDB 或 MySQL)通常会生成可更新的租约,因此生成的凭据将在应用程序关闭时禁用。 + +| |静态令牌不会更新或撤销。| +|---|-----------------------------------------| + +默认情况下,租赁续订和撤销是启用的,可以通过将`spring.cloud.vault.config.lifecycle.enabled`设置为`false`来禁用。不建议这样做,因为租约可能会到期,并且 Spring Cloud Vault 无法使用生成的凭据访问 Vault 或服务,并且在应用程序关闭后,有效的凭据仍然处于活动状态。 + +``` +spring.cloud.vault: + config.lifecycle: + enabled: true + min-renewal: 10s + expiry-threshold: 1m + lease-endpoints: Legacy +``` + +* `enabled`控制与秘密相关的租赁是否被认为是续签的,过期的秘密是否被旋转。默认情况下启用。 + +* `min-renewal`设置续租前至少需要的期限。这种设置可以防止更新太频繁。 + +* `expiry-threshold`设置到期阈值。租约在到期前会在配置的期限内续签。 + +* `lease-endpoints`设置更新和撤销的端点。旧版的 Vault 版本在 0.8 之前,Sysleases 版本在以后。 + +另见:[保险库文档:租赁、续订和撤销](https://www.vaultproject.io/docs/concepts/lease.html) + +[](#vault-session-lifecycle)[16.会话令牌生命周期管理(更新、重新登录和撤销)](#vault-session-lifecycle) +---------- + +Vault 会话令牌(也称为`LoginToken`)与租赁非常相似,因为它具有 TTL,最大 TTL,并且可能会过期。一旦登录令牌过期,就不能再使用它与 Vault 进行交互。因此, Spring Vault 提供了一个`SessionManager`API,用于命令式和反应式使用。 + +Spring 默认情况下,Cloud Vault 维护会话令牌生命周期。会话令牌是懒洋洋地获得的,因此实际的登录被推迟到第一次使用 Vault 的会话绑定时。一旦 Spring Cloud Vault 获得会话令牌,它将保留它直到到期。下一次使用会话绑定活动时, Spring Cloud Vault 重新登录到 Vault 并获得一个新的会话令牌。在应用程序关闭时, Spring Cloud Vault 撤销令牌,如果它仍然处于活动状态以终止会话。 + +默认情况下,会话生命周期是启用的,可以通过将`spring.cloud.vault.session.lifecycle.enabled`设置为`false`来禁用。不建议禁用,因为会话令牌可能会过期,并且 Spring Cloud Vault 无法更长时间访问 Vault。 + +``` +spring.cloud.vault: + session.lifecycle: + enabled: true + refresh-before-expiry: 10s + expiry-threshold: 20s +``` + +* `enabled`控制是否启用会话生命周期管理以更新会话令牌。默认情况下启用。 + +* `refresh-before-expiry`控制会话令牌更新的时间点。通过从令牌到期时间减去`refresh-before-expiry`来计算刷新时间。默认值为`5 seconds`。 + +* `expiry-threshold`设置到期阈值。该阈值表示将会话令牌视为有效的最小 TTL 持续时间。具有较短 TTL 的令牌将被视为过期,不再使用。应该大于`refresh-before-expiry`以防止令牌过期。默认值为`7 seconds`。 + +另见:[保险库文档:令牌更新](https://www.vaultproject.io/api-docs/auth/token#renew-a-token-self) + +[](#common-application-properties)[附录 A:通用应用程序属性](#common-application-properties) +---------- + +可以在`application.properties`文件内、`application.yml`文件内或作为命令行开关指定各种属性。本附录提供了一个常见的 Spring Cloud Vault 属性的列表,以及对使用它们的基础类的引用。 + +| |属性贡献可以来自你的 Classpath 上的其他 jar 文件,因此你不应将其视为详尽的列表。
此外,你可以定义自己的属性。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| Name | Default |说明| +|----------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| spring.cloud.vault.app-id.app-id-path | `app-id` |安装 APPID 身份验证后端的路径。| +| spring.cloud.vault.app-id.network-interface | |网络接口提示为“MAC\_Address”用户 ID 机制。| +| spring.cloud.vault.app-id.user-id | `MAC_ADDRESS` |userid 机制。可以是“mac\_address”、“ip\_address”、字符串或类名。| +| spring.cloud.vault.app-role.app-role-path | `approle` |Approle 身份验证后端的挂载路径。| +| spring.cloud.vault.app-role.role | |角色的名称,可选的,用于拉模式.| +| spring.cloud.vault.app-role.role-id | |罗里德。| +| spring.cloud.vault.app-role.secret-id | |秘密组织。| +| spring.cloud.vault.application-name | `application` |用于 APPID 身份验证的应用程序名称。| +| spring.cloud.vault.authentication | | | +| spring.cloud.vault.aws-ec2.aws-ec2-path | `aws-ec2` |安装 AWS-EC2 身份验证后端的路径。| +| spring.cloud.vault.aws-ec2.identity-document |`[169.254.169.254/latest/dynamic/instance-identity/pkcs7](http://169.254.169.254/latest/dynamic/instance-identity/pkcs7)`|AWS-EC2PKCS7 身份文档的 URL。| +| spring.cloud.vault.aws-ec2.nonce | |once 用于 AWS-EC2 身份验证。空的 nonce 默认为 nonce 生成。| +| spring.cloud.vault.aws-ec2.role | |角色的名称,可选的。| +| spring.cloud.vault.aws-iam.aws-path | `aws` |安装 AWS 身份验证后端的路径。| +| spring.cloud.vault.aws-iam.endpoint-uri | |STS 服务器 URI。@ 自 2.2 起| +| spring.cloud.vault.aws-iam.role | |角色的名称,可选的。如果未设置,则默认为友好的 IAM 名称。| +| spring.cloud.vault.aws-iam.server-name | |用于在登录请求的头中设置{@code x-vault-aws-iam-server-id}头的服务器名称。| +| spring.cloud.vault.aws.access-key-property | `cloud.aws.credentials.accessKey` |获取的访问密钥的目标属性。| +| spring.cloud.vault.aws.backend | `aws` |AWS 后端路径。| +| spring.cloud.vault.aws.credential-type | |AWS 凭据类型| +| spring.cloud.vault.aws.enabled | `false` |启用 AWS 后端使用。| +| spring.cloud.vault.aws.role | |凭证的角色名。| +| spring.cloud.vault.aws.role-arn | |假定的 \_role 的 Role ARN 如果我们有多个与 vault 角色相关的角色。@since3.0.2| +| spring.cloud.vault.aws.secret-key-property | `cloud.aws.credentials.secretKey` |获取的密钥的目标属性。| +| spring.cloud.vault.aws.session-token-key-property | `cloud.aws.credentials.sessionToken` |获取的密钥的目标属性。| +| spring.cloud.vault.aws.ttl | `0` |STS 代币的 TTL。默认情况下,无论保险库角色可能对 MAX 有什么影响。也仅限于 AWS 支持的 STS 的最大值。@since3.0.2| +| spring.cloud.vault.azure-msi.azure-path | `azure` |安装路径的 Azure MSI 身份验证后端。| +| spring.cloud.vault.azure-msi.identity-token-service | |身份令牌服务 URI。@since3.0| +| spring.cloud.vault.azure-msi.metadata-service | |实例元数据服务 URI。@since3.0| +| spring.cloud.vault.azure-msi.role | |角色的名称。| +| spring.cloud.vault.cassandra.backend | `cassandra` |Cassandra 后端路径。| +| spring.cloud.vault.cassandra.enabled | `false` |启用 Cassandra 后端使用。| +| spring.cloud.vault.cassandra.password-property | `spring.data.cassandra.password` |获取的密码的目标属性。| +| spring.cloud.vault.cassandra.role | |凭证的角色名。| +| spring.cloud.vault.cassandra.static-role | `false` |启用静态角色用法。@ 自 2.2 起| +| spring.cloud.vault.cassandra.username-property | `spring.data.cassandra.username` |获取的用户名的目标属性。| +| spring.cloud.vault.config.lifecycle.enabled | `true` |启用生命周期管理。| +| spring.cloud.vault.config.lifecycle.expiry-threshold | |到期日门槛。{@link lease}在给定的{@link duration}到期前更新。@ 自 2.2 起| +| spring.cloud.vault.config.lifecycle.lease-endpoints | |将{@Link LeaseEndpoints}设置为将续订/撤销调用委托给。{@link leaseendpoints}封装了影响更新/撤销端点位置的 Vault 版本之间的差异。对于 Vault 的 0.8 或更高版本,可以使用{@Link LeaseEndpoints#sysleases},对于较旧的版本,可以使用{@Link LeaseEndpoints#Legacy}(默认)。@ 自 2.2 起| +| spring.cloud.vault.config.lifecycle.min-renewal | |在续约前至少需要的时间。@ 自 2.2 起| +| spring.cloud.vault.config.order | `0` |用于设置{@link org.springframework.core.ENV.PropertySource}的优先级。这对于使用 Vault 作为对其他属性源的覆盖是有用的。@see org.springframework.core.priorityordered| +| spring.cloud.vault.connection-timeout | `5000` |连接超时。| +| spring.cloud.vault.consul.backend | `consul` |领事后台路径。| +| spring.cloud.vault.consul.enabled | `false` |启用 Consul 后端使用。| +| spring.cloud.vault.consul.role | |凭证的角色名。| +| spring.cloud.vault.consul.token-property | `spring.cloud.consul.token` |获取的令牌的目标属性。| +| spring.cloud.vault.couchbase.backend | `database` |Couchbase 后端路径。| +| spring.cloud.vault.couchbase.enabled | `false` |启用 CouchBase 后端使用。| +| spring.cloud.vault.couchbase.password-property | `spring.couchbase.password` |获取的密码的目标属性。| +| spring.cloud.vault.couchbase.role | |凭证的角色名。| +| spring.cloud.vault.couchbase.static-role | `false` |启用静态角色用法。| +| spring.cloud.vault.couchbase.username-property | `spring.couchbase.username` |获取的用户名的目标属性。| +| spring.cloud.vault.database.backend | `database` |数据库后端路径。| +| spring.cloud.vault.database.enabled | `false` |启用数据库后端使用。| +| spring.cloud.vault.database.password-property | `spring.datasource.password` |获取的密码的目标属性。| +| spring.cloud.vault.database.role | |凭证的角色名。| +| spring.cloud.vault.database.static-role | `false` |启用静态角色用法。| +| spring.cloud.vault.database.username-property | `spring.datasource.username` |获取的用户名的目标属性。| +| spring.cloud.vault.databases | | | +| spring.cloud.vault.discovery.enabled | `false` |标志,指示已启用 Vault Server Discovery(将通过 Discovery 查找 Vault Server URL)。| +| spring.cloud.vault.discovery.service-id | `vault` |定位保险库的服务 ID。| +| spring.cloud.vault.elasticsearch.backend | `database` |数据库后端路径。| +| spring.cloud.vault.elasticsearch.enabled | `false` |启用 ElasticSearch 后端使用。| +| spring.cloud.vault.elasticsearch.password-property | `spring.elasticsearch.rest.password` |获取的密码的目标属性。| +| spring.cloud.vault.elasticsearch.role | |凭证的角色名。| +| spring.cloud.vault.elasticsearch.static-role | `false` |启用静态角色用法。| +| spring.cloud.vault.elasticsearch.username-property | `spring.elasticsearch.rest.username` |获取的用户名的目标属性。| +| spring.cloud.vault.enabled | `true` |启用 Vault Config 服务器。| +| spring.cloud.vault.fail-fast | `false` |如果不能从保险库获取数据,则会迅速失效。| +| spring.cloud.vault.gcp-gce.gcp-path | `gcp` |Kubernetes 身份验证后端的挂载路径。| +| spring.cloud.vault.gcp-gce.role | |尝试登录所针对的角色的名称。| +| spring.cloud.vault.gcp-gce.service-account | |可选服务帐户 ID。如果未配置,则使用默认 ID。| +| spring.cloud.vault.gcp-iam.credentials.encoded-key | |base64 以 JSON 格式对 OAuth2 帐户私钥的内容进行了编码。| +| spring.cloud.vault.gcp-iam.credentials.location | |OAuth2 凭证私钥的位置。\由于这是一种资源,私钥可以位于多种位置,例如本地文件系统、 Classpath、URL 等。| +| spring.cloud.vault.gcp-iam.gcp-path | `gcp` |Kubernetes 身份验证后端的挂载路径。| +| spring.cloud.vault.gcp-iam.jwt-validity | `15m` |JWT 令牌的有效性。| +| spring.cloud.vault.gcp-iam.project-id | |重写 GCP 项目 ID。| +| spring.cloud.vault.gcp-iam.role | |尝试登录所针对的角色的名称。| +| spring.cloud.vault.gcp-iam.service-account-id | |覆盖 GCP 服务帐户 ID。| +| spring.cloud.vault.host | `localhost` |Vault 服务器主机。| +| spring.cloud.vault.kubernetes.kubernetes-path | `kubernetes` |Kubernetes 身份验证后端的挂载路径。| +| spring.cloud.vault.kubernetes.role | |尝试登录所针对的角色的名称。| +| spring.cloud.vault.kubernetes.service-account-token-file | `/var/run/secrets/kubernetes.io/serviceaccount/token` |服务帐户令牌文件的路径。| +| spring.cloud.vault.kv.application-name | `application` |用于上下文的应用程序名。| +| spring.cloud.vault.kv.backend | `secret` |默认后端名称。| +| spring.cloud.vault.kv.backend-version | `2` |键值后端版本。当前支持的版本有:\\版本 1(未版本化键-值后端)。\\版本 2(版本化键-值后端)。\\| +| spring.cloud.vault.kv.default-context | `application` |默认上下文的名称。| +| spring.cloud.vault.kv.enabled | `true` |启用 kev-value 后端。| +| spring.cloud.vault.kv.profile-separator | `/` |profile-separator 组合应用程序名和 profile。| +| spring.cloud.vault.kv.profiles | |活动配置文件列表。@since3.0| +| spring.cloud.vault.mongodb.backend | `mongodb` |MongoDB 后端路径。| +| spring.cloud.vault.mongodb.enabled | `false` |启用 MongoDB 后端使用。| +| spring.cloud.vault.mongodb.password-property | `spring.data.mongodb.password` |获取的密码的目标属性。| +| spring.cloud.vault.mongodb.role | |凭证的角色名。| +| spring.cloud.vault.mongodb.static-role | `false` |启用静态角色用法。@ 自 2.2 起| +| spring.cloud.vault.mongodb.username-property | `spring.data.mongodb.username` |获取的用户名的目标属性。| +| spring.cloud.vault.mysql.backend | `mysql` |MySQL 后端路径。| +| spring.cloud.vault.mysql.enabled | `false` |启用 MySQL 后端使用。| +| spring.cloud.vault.mysql.password-property | `spring.datasource.password` |获取的用户名的目标属性。| +| spring.cloud.vault.mysql.role | |凭证的角色名。| +| spring.cloud.vault.mysql.username-property | `spring.datasource.username` |获取的用户名的目标属性。| +| spring.cloud.vault.namespace | |Vault 名称空间(需要 Vault Enterprise)。| +| spring.cloud.vault.pcf.instance-certificate | |到实例证书的路径。默认为{@code cf\_instance\_CERT}ENV 变量。| +| spring.cloud.vault.pcf.instance-key | |实例键的路径。默认为{@code cf\_instance\_key}ENV 变量。| +| spring.cloud.vault.pcf.pcf-path | `pcf` |Kubernetes 身份验证后端的挂载路径。| +| spring.cloud.vault.pcf.role | |尝试登录所针对的角色的名称。| +| spring.cloud.vault.port | `8200` |Vault 服务器端口。| +| spring.cloud.vault.postgresql.backend | `postgresql` |PostgreSQL 后端路径。| +| spring.cloud.vault.postgresql.enabled | `false` |启用 PostgreSQL 后端使用。| +| spring.cloud.vault.postgresql.password-property | `spring.datasource.password` |获取的用户名的目标属性。| +| spring.cloud.vault.postgresql.role | |凭证的角色名。| +| spring.cloud.vault.postgresql.username-property | `spring.datasource.username` |获取的用户名的目标属性。| +| spring.cloud.vault.rabbitmq.backend | `rabbitmq` |RabbitMQ 后端路径。| +| spring.cloud.vault.rabbitmq.enabled | `false` |启用 RabbitMQ 后端使用。| +| spring.cloud.vault.rabbitmq.password-property | `spring.rabbitmq.password` |获取的密码的目标属性。| +| spring.cloud.vault.rabbitmq.role | |凭证的角色名。| +| spring.cloud.vault.rabbitmq.username-property | `spring.rabbitmq.username` |获取的用户名的目标属性。| +| spring.cloud.vault.reactive.enabled | `true` |指示激活了反应性发现的标志| +| spring.cloud.vault.read-timeout | `15000` |读超时。| +| spring.cloud.vault.scheme | `https` |协议方案。可以是“HTTP”或“HTTPS”。| +| spring.cloud.vault.session.lifecycle.enabled | `true` |启用会话生命周期管理。| +| spring.cloud.vault.session.lifecycle.expiry-threshold | `7s` |{@link logintoken}的过期阈值。该阈值表示将登录令牌视为有效的最小 TTL 持续时间。具有较短 TTL 的令牌将被视为过期,不再使用。应该大于{@code refreshbeForexilless}以防止令牌过期。| +|spring.cloud.vault.session.lifecycle.refresh-before-expiry| `5s` |更新{@link logintoken}之前至少需要的时间段。| +| spring.cloud.vault.ssl.cert-auth-path | `cert` |安装路径的 TLS CERT 身份验证后端。| +| spring.cloud.vault.ssl.enabled-cipher-suites | |启用的 SSL/TLS 密码套件的列表。@since3.0.2| +| spring.cloud.vault.ssl.enabled-protocols | |启用的 SSL/TLS 协议的列表。@since3.0.2| +| spring.cloud.vault.ssl.key-store | |保存证书和私钥的信任存储区。| +| spring.cloud.vault.ssl.key-store-password | |用于访问密钥存储区的密码。| +| spring.cloud.vault.ssl.key-store-type | |类型的密钥存储。@since3.0| +| spring.cloud.vault.ssl.trust-store | |持有 SSL 证书的信任存储区。| +| spring.cloud.vault.ssl.trust-store-password | |用于访问信任存储区的密码。| +| spring.cloud.vault.ssl.trust-store-type | |信任存储的类型。@since3.0| +| spring.cloud.vault.token | |静态保险库令牌。如果{@link#authentication}是{@code token},则需要。| +| spring.cloud.vault.uri | |Vault Uri。可以用方案、主机和端口进行设置.| diff --git a/docs/spring-cloud/spring-cloud-zookeeper.md b/docs/spring-cloud/spring-cloud-zookeeper.md new file mode 100644 index 0000000000000000000000000000000000000000..50cb6c9488b591e53104742f102315462ab818a2 --- /dev/null +++ b/docs/spring-cloud/spring-cloud-zookeeper.md @@ -0,0 +1,718 @@ +[Spring Cloud Zookeeper](#_spring_cloud_zookeeper) +========== + +该项目通过自动配置和绑定到 Spring 环境和其他 Spring 编程模型习惯用法,为 Spring 引导应用程序提供 ZooKeeper 集成。通过一些注释,你可以快速启用和配置应用程序中的常见模式,并使用基于 ZooKeeper 的组件构建大型分布式系统。所提供的模式包括服务发现和配置。该项目还通过集成 Spring Cloud LoadBalancer 提供客户端负载平衡。 + +[](#quick-start)[1. Quick Start](#quick-start) +---------- + +这个快速的开始将使用 Spring Cloud ZooKeeper 进行服务发现和分布式配置。 + +首先,在你的机器上运行 ZooKeeper。然后,你可以访问它,并将其作为服务注册表和配置源使用 Spring Cloud ZooKeeper。 + +### [](#discovery-client-usage)[1.1.发现客户端使用情况](#discovery-client-usage) ### + +要在应用程序中使用这些特性,你可以将其构建为依赖于`spring-cloud-zookeeper-core`和`spring-cloud-zookeeper-discovery`的 Spring 引导应用程序。添加依赖项最方便的方法是使用 Spring 引导启动器:`org.springframework.cloud:spring-cloud-starter-zookeeper-discovery`。我们建议使用依赖管理和`spring-boot-starter-parent`。下面的示例展示了一个典型的 Maven 配置: + +POM.xml + +``` + + + org.springframework.boot + spring-boot-starter-parent + {spring-boot-version} + + + + + + org.springframework.cloud + spring-cloud-starter-zookeeper-discovery + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + +``` + +下面的示例显示了一个典型的 Gradle 设置: + +构建。 Gradle + +``` +plugins { + id 'org.springframework.boot' version ${spring-boot-version} + id 'io.spring.dependency-management' version ${spring-dependency-management-version} + id 'java' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.cloud:spring-cloud-starter-zookeeper-discovery' + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} +``` + +| |根据你使用的版本,你可能需要调整你的项目中使用的 Apache ZooKeeper 版本。
你可以在[安装动物园管理员部分](#spring-cloud-zookeeper-install)中阅读有关它的更多信息。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +现在,你可以创建一个标准的 Spring 启动应用程序,例如下面的 HTTP 服务器: + +``` +@SpringBootApplication +@RestController +public class Application { + + @GetMapping("/") + public String home() { + return "Hello World!"; + } + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} +``` + +当这个 HTTP 服务器运行时,它会连接到 ZooKeeper,它在默认的本地端口(2181)上运行。要修改启动行为,可以使用`应用程序.属性`更改 ZooKeeper 的位置,如下例所示: + +``` +spring: + cloud: + zookeeper: + connect-string: localhost:2181 +``` + +现在可以使用`DiscoveryClient`、`@LoadBalanced RestTemplate`或`@LoadBalanced WebClient.Builder`从 ZooKeeper 检索服务和实例数据,如以下示例所示: + +``` +@Autowired +private DiscoveryClient discoveryClient; + +public String serviceUrl() { + List list = discoveryClient.getInstances("STORES"); + if (list != null && list.size() > 0 ) { + return list.get(0).getUri().toString(); + } + return null; +} +``` + +### [](#distributed-configuration-usage)[1.2.分布式配置使用](#distributed-configuration-usage) ### + +要在应用程序中使用这些特性,你可以将其构建为依赖于`spring-cloud-zookeeper-core`和`spring-cloud-zookeeper-config`的 Spring 引导应用程序。添加依赖项最方便的方法是使用 Spring 引导启动器:`org.springframework.cloud:spring-cloud-starter-zookeeper-config`。我们建议使用依赖管理和`spring-boot-starter-parent`。下面的示例显示了典型的 Maven 配置: + +POM.xml + +``` + + + org.springframework.boot + spring-boot-starter-parent + {spring-boot-version} + + + + + + org.springframework.cloud + spring-cloud-starter-zookeeper-config + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + +``` + +下面的示例显示了一个典型的 Gradle 设置: + +构建。 Gradle + +``` +plugins { + id 'org.springframework.boot' version ${spring-boot-version} + id 'io.spring.dependency-management' version ${spring-dependency-management-version} + id 'java' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.cloud:spring-cloud-starter-zookeeper-config' + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} +``` + +| |根据你使用的版本,你可能需要调整你的项目中使用的 Apache ZooKeeper 版本。
你可以在[安装动物园管理员部分](#spring-cloud-zookeeper-install)中阅读有关它的更多信息。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +现在,你可以创建一个标准的 Spring 启动应用程序,例如下面的 HTTP 服务器: + +``` +@SpringBootApplication +@RestController +public class Application { + + @GetMapping("/") + public String home() { + return "Hello World!"; + } + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} +``` + +应用程序从 ZooKeeper 检索配置数据。 + +| |如果使用 Spring Cloud ZooKeeper Config,则需要设置`spring.config.import`属性才能绑定到 ZooKeeper。
你可以在[Spring Boot Config Data Import section](#config-data-import)中阅读有关它的更多信息。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[](#spring-cloud-zookeeper-install)[2.安装 ZooKeeper](#spring-cloud-zookeeper-install) +---------- + +有关如何安装 ZooKeeper 的说明,请参见[安装文档](https://zookeeper.apache.org/doc/current/zookeeperStarted.html)。 + +Spring Cloud ZooKeeper 在幕后使用 Apache 策展人。虽然 ZooKeeper3.5.x 仍然被 ZooKeeper 开发团队认为是“测试版”,但现实情况是,许多用户都在生产中使用它。然而,ZooKeeper3.4.x 也在生产中使用。在 Apache Curator4.0 之前,两个版本的 ZooKeeper 都是通过两个版本的 Apache Curator 支持的。从 Curator4.0 开始,ZooKeeper 的两个版本都通过相同的 Curator 库支持。 + +如果要与版本 3.4 集成,则需要更改`curator`附带的 ZooKeeper 依赖项,从而更改`spring-cloud-zookeeper`。要做到这一点,只需排除该依赖关系,并添加如下所示的 3.4.x 版本。 + +Maven + +``` + + org.springframework.cloud + spring-cloud-starter-zookeeper-all + + + org.apache.zookeeper + zookeeper + + + + + org.apache.zookeeper + zookeeper + 3.4.12 + + + org.slf4j + slf4j-log4j12 + + + +``` + +Gradle + +``` +compile('org.springframework.cloud:spring-cloud-starter-zookeeper-all') { + exclude group: 'org.apache.zookeeper', module: 'zookeeper' +} +compile('org.apache.zookeeper:zookeeper:3.4.12') { + exclude group: 'org.slf4j', module: 'slf4j-log4j12' +} +``` + +[](#spring-cloud-zookeeper-discovery)[3.使用 ZooKeeper 进行服务发现](#spring-cloud-zookeeper-discovery) +---------- + +服务发现是基于微服务的体系结构的关键原则之一。尝试手动配置每个客户机或某种形式的约定可能很难做到,并且可能很脆弱。[Curator](https://curator.apache.org)(ZooKeeper 的 Java 库)通过[服务发现扩展](https://curator.apache.org/curator-x-discovery/)提供服务发现。 Spring Cloud ZooKeeper 将此扩展用于服务注册和发现。 + +### [](#activating)[3.1. Activating](#activating) ### + +包括对 `org.springframework.cloud: Spring-cloud-starter-zookeeper-discovery’的依赖,使自动配置能够设置 Spring cloud zookeeper discovery。 + +| |对于 Web 功能,仍然需要包含“org.springframework.boot: Spring-boot-starter-web”。| +|---|---------------------------------------------------------------------------------------------------| + +| |在使用 ZooKeeper 的 3.4 版本时,你需要更改
包含依赖项的方式,如[here](#spring-cloud-zookeeper-install)所述。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#registering-with-zookeeper)[3.2.在动物园管理员处注册](#registering-with-zookeeper) ### + +当客户机向 ZooKeeper 注册时,它提供有关自身的元数据(如主机和端口、ID 和名称)。 + +下面的示例展示了一个 ZooKeeper 客户端: + +``` +@SpringBootApplication +@RestController +public class Application { + + @RequestMapping("/") + public String home() { + return "Hello world"; + } + + public static void main(String[] args) { + new SpringApplicationBuilder(Application.class).web(true).run(args); + } + +} +``` + +| |前面的示例是一个普通的引导应用程序 Spring。| +|---|----------------------------------------------------------| + +如果 ZooKeeper 位于`localhost:2181`以外的地方,则配置必须提供服务器的位置,如以下示例所示: + +应用程序.yml + +``` +spring: + cloud: + zookeeper: + connect-string: localhost:2181 +``` + +| |如果使用[Spring Cloud Zookeeper Config](#spring-cloud-zookeeper-config),则前面示例中显示的
值需要在`bootstrap.yml`中,而不是在 `应用程序.yml` 中。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +默认的服务名称、实例 ID 和端口(取自`Environment`)分别是 `${ Spring.application.name}`、 Spring 上下文 ID 和`${server.port}`。 + +在 Classpath 上具有`spring-cloud-starter-zookeeper-discovery`使得该应用程序既可以成为 ZooKeeper“服务”(即它自己注册),也可以成为“客户端”(即它可以查询 ZooKeeper 以定位其他服务)。 + +如果你想禁用 ZooKeeper Discovery 客户端,可以将 ` Spring.cloud.zooKeeper.Discovery.enabled` 设置为`false`。 + +### [](#using-the-discoveryclient)[3.3.使用 DiscoveryClient](#using-the-discoveryclient) ### + +Spring 云具有对[Feign](https://github.com/spring-cloud/spring-cloud-netflix/blob/master/docs/src/main/asciidoc/spring-cloud-netflix.adoc#spring-cloud-feign)(一个 REST 客户机构建器)、[Spring`RestTemplate`](https://github.com/spring-cloud/spring-cloud-netflix/blob/master/docs/src/main/ascii)和[Spring WebFlux](https://cloud.spring.io/spring-cloud-commons/reference/html/#loadbalanced-webclient)的支持,使用逻辑服务名称而不是物理 URL。 + +你也可以使用`org.springframework.cloud.client.discovery.DiscoveryClient`,它为不特定于 Netflix 的发现客户端提供了一个简单的 API,如下例所示: + +``` +@Autowired +private DiscoveryClient discoveryClient; + +public String serviceUrl() { + List list = discoveryClient.getInstances("STORES"); + if (list != null && list.size() > 0 ) { + return list.get(0).getUri().toString(); + } + return null; +} +``` + +[](#spring-cloud-zookeeper-other-componentes)[4. Using Spring Cloud Zookeeper with Spring Cloud Components](#spring-cloud-zookeeper-other-componentes) +---------- + +佯装、 Spring 云网关和 Spring 云负载均衡器都与 Spring 云 ZooKeeper 一起工作。 + +### [](#spring-cloud-loadbalancer-with-zookeeper)[4.1. Spring Cloud LoadBalancer with Zookeeper](#spring-cloud-loadbalancer-with-zookeeper) ### + +Spring Cloud ZooKeeper 提供了 Spring Cloud LoadBalancer`ServiceInstanceListSupplier`的实现方式。当你使用`spring-cloud-starter-zookeeper-discovery`时, Spring 云负载平衡器被自动配置为默认使用“ZooKeeperServiceInstanceListSupplier”。 + +| |如果你以前在 ZooKeeper 中使用了 StickyRule,那么当前堆栈中的 stickyRule
中的替换项是 SC LoadBalancer 中的`SameInstancePreferenceServiceInstanceListSupplier`。你可以在[Spring Cloud Commons documentation](https://docs.spring.io/spring-cloud-commons/docs/current/reference/html/#spring-cloud-loadbalancer)中了解如何设置它。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[](#spring-cloud-zookeeper-service-registry)[5. Spring Cloud Zookeeper and Service Registry](#spring-cloud-zookeeper-service-registry) +---------- + +Spring Cloud ZooKeeper 实现了`ServiceRegistry`接口,允许开发人员以编程的方式注册任意服务。 + +`ServiceInstanceRegistration`类提供了一个`builder()`方法来创建一个`ServiceRegistry`可以使用的 `registration’对象,如以下示例所示: + +``` +@Autowired +private ZookeeperServiceRegistry serviceRegistry; + +public void registerThings() { + ZookeeperRegistration registration = ServiceInstanceRegistration.builder() + .defaultUriSpec() + .address("anyUrl") + .port(10) + .name("/a/b/c/d/anotherservice") + .build(); + this.serviceRegistry.register(registration); +} +``` + +### [](#instance-status)[5.1.实例状态](#instance-status) ### + +Netflix Eureka 支持在服务器上注册`OUT_OF_SERVICE`实例。这些实例不作为活动服务实例返回。这对于诸如蓝色/绿色部署之类的行为很有用。(注意,Curator 服务发现配方不支持此行为。)利用灵活的有效负载, Spring Cloud ZooKeeper 可以通过更新一些特定的元数据,然后在 Spring Cloud LoadBalancer中对该元数据进行过滤来实现。`ZookeeperServiceInstanceListSupplier`过滤掉所有不等于`UP`的非空实例状态。如果实例状态字段为空,则认为它是`UP`,用于向后兼容。要更改实例的状态,请将`POST`与`OUT_OF_SERVICE`连接到`ServiceRegistry`实例状态执行器端点,如以下示例所示: + +``` +$ http POST http://localhost:8081/service-registry status=OUT_OF_SERVICE +``` + +| |前面的示例使用[httpie.org](https://httpie.org)中的`http`命令。| +|---|------------------------------------------------------------------------------------| + +[](#spring-cloud-zookeeper-dependencies)[6.动物园管理员依赖关系](#spring-cloud-zookeeper-dependencies) +---------- + +以下主题涵盖了如何使用 Spring Cloud ZooKeeper 依赖项: + +* [使用 ZooKeeper 依赖项](#spring-cloud-zookeeper-dependencies-using) + +* [激活 ZooKeeper 依赖项](#spring-cloud-zookeeper-dependencies-activating) + +* [设置 ZooKeeper 依赖项](#spring-cloud-zookeeper-dependencies-setting-up) + +* [Configuring Spring Cloud Zookeeper Dependencies](#spring-cloud-zookeeper-dependencies-configuring) + +### [](#spring-cloud-zookeeper-dependencies-using)[6.1.使用 ZooKeeper 依赖项](#spring-cloud-zookeeper-dependencies-using) ### + +Spring Cloud ZooKeeper 为你提供了一种可能性,可以将你的应用程序的依赖关系作为属性提供。作为依赖关系,你可以理解在 ZooKeeper 中注册的其他应用程序,你希望通过[Feign](https://github.com/spring-cloud/spring-cloud-netflix/blob/master/docs/src/main/asciidoc/spring-cloud-netflix.adoc#spring-cloud-feign)(REST 客户机生成器)、[Spring`RestTemplate`](https://github.com/spring-cloud/spring-cloud-netflix/blob/master/docs/src/main/ascii)和[Spring WebFlux](https://cloud.spring.io/spring-cloud-commons/reference/html/#loadbalanced-webclient)调用这些应用程序。 + +你还可以使用 ZooKeeper Dependency Watchers 功能来控制和监视你的依赖关系的状态。 + +### [](#spring-cloud-zookeeper-dependencies-activating)[6.2.激活 ZooKeeper 依赖项](#spring-cloud-zookeeper-dependencies-activating) ### + +包括对 `org.springframework.cloud: Spring-cloud-starter-zookeeper-discovery’的依赖,使自动配置能够建立 Spring cloud zookeeper 依赖关系。即使你在属性中提供了依赖关系,也可以关闭依赖关系。为此,将 ` Spring.cloud.zookeeper.dependency.enabled’属性设置为 false(默认设置为`true`)。 + +### [](#spring-cloud-zookeeper-dependencies-setting-up)[6.3.设置 ZooKeeper 依赖项](#spring-cloud-zookeeper-dependencies-setting-up) ### + +考虑下面的依赖关系表示示例: + +application.yml + +``` +spring.application.name: yourServiceName +spring.cloud.zookeeper: + dependencies: + newsletter: + path: /path/where/newsletter/has/registered/in/zookeeper + loadBalancerType: ROUND_ROBIN + contentTypeTemplate: application/vnd.newsletter.$version+json + version: v1 + headers: + header1: + - value1 + header2: + - value2 + required: false + stubs: org.springframework:foo:stubs + mailing: + path: /path/where/mailing/has/registered/in/zookeeper + loadBalancerType: ROUND_ROBIN + contentTypeTemplate: application/vnd.mailing.$version+json + version: v1 + required: true +``` + +接下来的几节将逐一介绍依赖关系的每个部分。根属性名为`spring.cloud.zookeeper.dependencies`。 + +#### [](#spring-cloud-zookeeper-dependencies-setting-up-aliases)[6.3.1. Aliases](#spring-cloud-zookeeper-dependencies-setting-up-aliases) #### + +在根属性下面,你必须将每个依赖项表示为别名。这是由于 Spring Cloud LoadBalancer 的限制,它要求将应用程序 ID 放置在 URL 中。因此,你不能通过任何复杂的路径,例如`/myApp/myRoute/name`)。别名是你使用的名称,而不是`DiscoveryClient`,`Feign`或 `resttemplate’的`serviceId`。 + +在前面的示例中,别名是`newsletter`和`mailing`。下面的示例显示了使用`newsletter`别名的假装用法: + +``` +@FeignClient("newsletter") +public interface NewsletterService { + @RequestMapping(method = RequestMethod.GET, value = "/newsletter") + String getNewsletters(); +} +``` + +#### [](#path)[6.3.2. Path](#path) #### + +该路径由`path`YAML 属性表示,并且是在 ZooKeeper 下注册依赖项的路径。如[上一节](#spring-cloud-zookeeper-dependencies-setting-up-aliases)中所述, Spring Cloud LoadBalancer 在 URL 上运行。因此,此路径不符合其需求。这就是为什么 Spring Cloud ZooKeeper 将别名映射到正确的路径。 + +#### [](#load-balancer-type)[6.3.3.负载平衡器类型](#load-balancer-type) #### + +负载均衡器类型由`loadBalancerType`YAML 属性表示。 + +如果你知道在调用这个特定的依赖项时必须应用哪种负载平衡策略,那么你可以在 YAML 文件中提供它,并且它会自动应用。你可以选择以下一种负载平衡策略: + +* sticky:一旦选择,实例总是被调用。 + +* 随机:随机选择一个实例。 + +* Round\_Robin:一遍又一遍地迭代实例。 + +#### [](#content-type-template-and-version)[6.3.4. `Content-Type` Template and Version](#content-type-template-and-version) #### + +`Content-Type`模板和版本由`contentTypeTemplate`和 `version’yaml 属性表示。 + +如果在`Content-Type`头中对 API 进行版本,则不希望将此头添加到每个请求中。此外,如果你想调用一个新版本的 API,那么你不希望围绕你的代码 ROAM 来提高 API 版本。这就是为什么你可以提供带有特殊`$version`占位符的 `ContentTypeTemplate’。这个占位符将由“version”YAML 属性的值来填充。考虑以下`contentTypeTemplate`的示例: + +``` +application/vnd.newsletter.$version+json +``` + +进一步考虑以下`version`: + +``` +v1 +``` + +结合`contentTypeTemplate`和版本,将为每个请求创建一个 `Content-Type’头,如下所示: + +``` +application/vnd.newsletter.v1+json +``` + +#### [](#default-headers)[6.3.5.默认标头](#default-headers) #### + +在 YAML 中,默认的头由`headers`映射表示。 + +有时,对依赖项的每次调用都需要设置一些默认的标头。要在代码中不这样做,你可以在 YAML 文件中设置它们,如下面的示例“headers”部分所示: + +``` +headers: + Accept: + - text/html + - application/xhtml+xml + Cache-Control: + - no-cache +``` + +该`headers`部分会导致在 HTTP 请求中添加带有适当的值列表的`Accept`和`Cache-Control`标题。 + +#### [](#required-dependencies)[6.3.6.所需依赖项](#required-dependencies) #### + +在 YAML 中,所需的依赖项由`required`属性表示。 + +如果在启动应用程序时需要启动某个依赖项,则可以在 YAML 文件中设置`required: true`属性。 + +如果你的应用程序无法在引导期间定位所需的依赖项,那么它将抛出一个异常,并且 Spring 上下文将无法设置。换句话说,如果所需的依赖项未在 ZooKeeper 中注册,则应用程序无法启动。 + +你可以阅读有关 Spring Cloud ZooKeeper Presence Checker[在本文的后面部分](#spring-cloud-zookeeper-dependency-watcher-presence-checker)的更多信息。 + +#### [](#stubs)[6.3.7. Stubs](#stubs) #### + +可以为 jar 包含依赖项存根的 jar 提供一个以冒号分隔的路径,如以下示例所示: + +`stubs: org.springframework:myApp:stubs` + +地点: + +* `org.springframework`是`groupId`。 + +* `myApp`是`artifactId`。 + +* `stubs`是分类器。(注意,`stubs`是默认值。 + +因为`stubs`是缺省分类器,所以前面的示例等于下面的示例: + +`stubs: org.springframework:myApp` + +### [](#spring-cloud-zookeeper-dependencies-configuring)[6.4. Configuring Spring Cloud Zookeeper Dependencies](#spring-cloud-zookeeper-dependencies-configuring) ### + +你可以设置以下属性来启用或禁用 ZooKeeper 依赖项功能的部分功能: + +* `spring.cloud.zookeeper.dependencies`:如果未设置此属性,则不能使用 ZooKeeper 依赖关系。 + +* `spring.cloud.zookeeper.dependency.loadbalancer.enabled`(默认启用):开启特定于 ZooKeeper 的定制负载平衡策略,包括`ZookeeperServiceInstanceListSupplier`和基于依赖项的负载平衡`RestTemplate`设置。 + +* `spring.cloud.zookeeper.dependency.headers.enabled`(默认情况下启用):此属性注册了一个`FeignBlockingLoadBalancerClient`,该属性会自动将适当的标题和内容类型附加到它们的版本中,如依赖项配置中所示。如果没有这个设置,这两个参数将无法工作。 + +* `spring.cloud.zookeeper.dependency.resttemplate.enabled`(默认情况下启用):启用时,此属性修改带有`@LoadBalanced`注释的 `resttemplate’的请求标头,以便将标头和内容类型与依赖项配置中设置的版本一起传递。如果没有此设置,这两个参数将无法工作。 + +[](#spring-cloud-zookeeper-dependency-watcher)[7. Spring Cloud Zookeeper Dependency Watcher](#spring-cloud-zookeeper-dependency-watcher) +---------- + +依赖项监视器机制允许你将侦听器注册到依赖项。实际上,该功能是`Observator`模式的一种实现。当依赖项改变时,它的状态(向上或向下),可以应用一些自定义逻辑。 + +### [](#activating-2)[7.1. Activating](#activating-2) ### + +Spring 云 ZooKeeper 依赖项功能需要被启用,以便你使用依赖项观察机制。 + +### [](#registering-a-listener)[7.2.注册侦听器](#registering-a-listener) ### + +要注册一个侦听器,你必须实现一个名为 `org.springframework.cloud.zookeeper.discovery.watcher.dependencywatcherlistener’的接口,并将其注册为 Bean。该接口为你提供了一种方法: + +``` +void stateChanged(String dependencyName, DependencyState newState); +``` + +如果你想注册一个特定依赖项的侦听器,那么`dependencyName`将是你的具体实现的鉴别器。`newState`为你提供有关你的依赖关系是否已更改为`CONNECTED`或`DISCONNECTED`的信息。 + +### [](#spring-cloud-zookeeper-dependency-watcher-presence-checker)[7.3.使用临场感检查器](#spring-cloud-zookeeper-dependency-watcher-presence-checker) ### + +与依赖项监视器绑定的是名为存在检查器的功能。它允许你在应用程序启动时提供自定义行为,以便根据依赖关系的状态做出反应。 + +抽象的 `org.SpringFramework.Cloud.ZooKeeper.Discovery.Watcher.Presence.DependencyPresenceOnStartupVerifier’类的默认实现是 `org.SpringFramework.Cloud.ZooKeeper.Discovery.Watcher.Presence.DefaultDependencyPresenceOnStartupVerifier’,其工作方式如下。 + +1. 如果依赖项被标记为 US`required`,并且不在 ZooKeeper 中,那么当应用程序启动时,它将抛出一个异常并关闭。 + +2. 如果依赖项不是`required`,则 `org.springframework.cloud.zookeeper.discovery.watcher.presence.logmissingDependencyChecker’记录在`WARN`级别缺少依赖项。 + +因为`DefaultDependencyPresenceOnStartupVerifier`只有在没有`DependencyPresenceOnStartupVerifier`类型的 Bean 时才注册,所以可以重写此功能。 + +[](#spring-cloud-zookeeper-config)[8.使用 ZooKeeper 的分布式配置](#spring-cloud-zookeeper-config) +---------- + +ZooKeeper 提供了一个[层次命名空间](https://zookeeper.apache.org/doc/current/zookeeperOver.html#sc_dataModelNameSpace),它允许客户端存储任意数据,例如配置数据。 Spring Cloud ZooKeeper Config 是[配置服务器和客户端](https://github.com/spring-cloud/spring-cloud-config)的一种替代方案。在特殊的“引导”阶段,将配置加载到 Spring 环境中。默认情况下,配置存储在`/config`名称空间中。根据应用程序的名称和活动配置文件创建了多个“PropertySource”实例,以模拟解析属性的 Spring 云配置顺序。例如,名称为`testApp`且配置文件为`dev`的应用程序具有为其创建的以下属性源: + +* `config/testApp,dev` + +* `config/testApp` + +* `config/application,dev` + +* `config/application` + +最具体的属性源位于顶部,而最不具体的属性源位于底部。`config/application`命名空间中的属性应用于所有使用 ZooKeeper 进行配置的应用程序。名称空间`config/testApp`中的属性仅对名为`testApp`的服务实例可用。 + +当前在启动应用程序时读取配置。向`/refresh`发送一个 HTTP`POST`请求会导致重新加载配置。监视配置名称空间(ZooKeeper 支持的)当前未实现。 + +### [](#activating-3)[8.1. Activating](#activating-3) ### + +包括对 `org.springframework.cloud: Spring-cloud-starter-zookeeper-config 的依赖,使自动配置能够设置 Spring cloud zookeeper config。 + +| |在使用 ZooKeeper 的 3.4 版本时,你需要更改
包含依赖项的方式,如[here](#spring-cloud-zookeeper-install)所述。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#config-data-import)[8.2. Spring Boot Config Data Import](#config-data-import) ### + +Spring Boot2.4 引入了一种通过`spring.config.import`属性导入配置数据的新方法。这是现在从 ZooKeeper 获得配置的默认方式。 + +要在应用程序中可选地连接到 ZooKeeper 以进行配置,请设置以下内容: + +应用程序.属性 + +``` +spring.config.import=optional:zookeeper: +``` + +这将在“localhost:2181”的默认位置连接到 ZooKeeper。如果无法连接到 ZooKeeper,删除`optional:`前缀将导致 ZooKeeper 配置失败。要更改 ZooKeeper Config 的连接属性,可以设置`spring.cloud.zookeeper.connect-string`,也可以将 connect 字符串添加到`spring.config.import`语句中,例如,`spring.config.import=optional:zookeeper:myhost:2818`。导入属性中的位置优先于`connect-string`属性。 + +ZooKeeper Config 将尝试根据`spring.cloud.zookeeper.config.name`(默认为`spring.application.name`属性的值)和`spring.cloud.zookeeper.config.default-context`(默认为`application`)从四个自动上下文加载值。如果你希望指定上下文而不是使用计算的上下文,那么可以将该信息添加到`spring.config.import`语句中。 + +application.properties + +``` +spring.config.import=optional:zookeeper:myhost:2181/contextone;/context/two +``` + +这将可选地只从`/contextone`和`/context/two`加载配置。 + +| |通过`spring.config.import`导入 Spring 引导配置数据方法所需的`bootstrap`文件(属性或 YAML)是**不是**。| +|---|--------------------------------------------------------------------------------------------------------------------------------------| + +### [](#customizing)[8.3.定制](#customizing) ### + +可以通过设置以下属性来定制 ZooKeeper 配置: + +``` +spring: + cloud: + zookeeper: + config: + enabled: true + root: configuration + defaultContext: apps + profileSeparator: '::' +``` + +* `enabled`:将该值设置为`false`将禁用 ZooKeeper 配置。 + +* `root`:设置配置值的基本名称空间。 + +* `defaultContext`:设置所有应用程序使用的名称。 + +* `profileSeparator`:设置用于在具有配置文件的属性源中分隔配置文件名称的分隔符的值。 + +| |如果你已经设置了`spring.cloud.bootstrap.enabled=true`或`spring.config.use-legacy-processing=true`,或者包含了`spring-cloud-starter-bootstrap`,那么上述值将需要放置在`bootstrap.yml`中,而不是`application.yml`中。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### [](#access-control-lists-acls)[8.4.访问控制列表(ACLS)](#access-control-lists-acls) ### + +通过调用`CuratorFramework` Bean 的`addAuthInfo`方法,可以为 ZooKeeper ACLS 添加身份验证信息。实现这一点的一种方法是提供自己的“curatorframework” Bean,如下例所示: + +``` +@BoostrapConfiguration +public class CustomCuratorFrameworkConfig { + + @Bean + public CuratorFramework curatorFramework() { + CuratorFramework curator = new CuratorFramework(); + curator.addAuthInfo("digest", "user:password".getBytes()); + return curator; + } + +} +``` + +请参阅[ZookeeperAutoConfiguration 类](https://github.com/spring-cloud/spring-cloud-zookeeper/blob/master/spring-cloud-zookeeper-core/src/main/java/org/springframework/cloud/zookeeper/ZookeeperAutoConfiguration.java)以查看`CuratorFramework` Bean 的默认配置。 + +或者,你可以从依赖于现有的“curatorFramework” Bean 的类中添加你的凭据,如下例所示: + +``` +@BoostrapConfiguration +public class DefaultCuratorFrameworkConfig { + + public ZookeeperConfig(CuratorFramework curator) { + curator.addAuthInfo("digest", "user:password".getBytes()); + } + +} +``` + +此 Bean 的创建必须在引导阶段发生。你可以注册要在此阶段运行的配置类,方法是使用“@bootstrapconfiguration”对它们进行注释,并将它们包含在一个逗号分隔的列表中,你将该列表设置为“resources/meta-inf/ Spring.factories”文件中`org.springframework.cloud.bootstrap.BootstrapConfiguration`属性的值,如以下示例所示: + +Party-INF/ Spring.Factories + +``` +org.springframework.cloud.bootstrap.BootstrapConfiguration=\ +my.project.CustomCuratorFrameworkConfig,\ +my.project.DefaultCuratorFrameworkConfig +``` \ No newline at end of file diff --git a/docs/spring-data/README.md b/docs/spring-data/README.md new file mode 100644 index 0000000000000000000000000000000000000000..0c3e4693766b2494e95210efd4474bac76003a9b --- /dev/null +++ b/docs/spring-data/README.md @@ -0,0 +1 @@ +# Spring 数据 \ No newline at end of file diff --git a/docs/spring-data/spring-data.md b/docs/spring-data/spring-data.md new file mode 100644 index 0000000000000000000000000000000000000000..04d6c70b080b6d72614e7177742fb2d5cd454fce --- /dev/null +++ b/docs/spring-data/spring-data.md @@ -0,0 +1,2457 @@ +# 前言 + +Spring Data Commons 项目将核心 Spring 概念应用于使用许多关系和非关系数据存储的解决方案的开发。 + +## 1. 项目元数据 + +* 版本控制:[https://github.com/spring-projects/spring-data-commons](https://github.com/spring-projects/spring-data-commons) + +* Bugtracker:[https://github.com/spring-projects/spring-data-commons/issues](https://github.com/spring-projects/spring-data-commons/issues) + +* 发布存储库:[https://repo.spring.io/libs-release](https://repo.spring.io/libs-release) + +* 里程碑存储库:[https://repo.spring.io/libs-milestone](https://repo.spring.io/libs-milestone) + +* 快照存储库:[https://repo.spring.io/libs-snapshot](https://repo.spring.io/libs-snapshot) + +## 参考文献 + +## 2. 依赖关系 + +由于每个 Spring 数据模块的启动日期不同,它们中的大多数都带有不同的主要版本号和次要版本号。找到兼容版本的最简单的方法是依赖 Spring 数据发布列 BOM,我们提供的是定义的兼容版本。在 Maven 项目中,你将在 POM 的``部分中声明此依赖项,如下所示: + +例 1。使用 Spring 数据发布列表 BOM + +``` + + + + org.springframework.data + spring-data-bom + 2021.1.2 + import + pom + + + +``` + +当前的发行版本为`2021.1.2`。列车版本使用[calver](https://calver.org/)的模式`YYYY.MINOR.MICRO`。对于 GA 版本和服务版本,版本名如下:`${calver}`,对于所有其他版本,版本名如下:`${calver}-${modifier}`,其中`modifier`可以是以下几种类型之一: + +* `SNAPSHOT`:当前快照 + +* `M1`,`M2`,以此类推:里程碑 + +* `RC1`,`RC2`,以此类推:释放候选项 + +你可以在[Spring Data examples repository](https://github.com/spring-projects/spring-data-examples/tree/master/bom)中找到使用 BOMS 的工作示例。有了这一点,你就可以在``块中声明希望使用的 Spring 数据模块,而不使用版本,如下所示: + +例 2。声明对 Spring 数据模块的依赖项 + +``` + + + org.springframework.data + spring-data-jpa + + +``` + +### 2.1.具有 Spring 引导的依赖管理 + +Spring 引导为你选择 Spring 数据模块的最新版本。如果你仍然希望升级到较新的版本,请将`spring-data-releasetrain.version`属性设置为希望使用的[训练版本和迭代](#dependencies.train-version)。 + +### 2.2. Spring 框架 + +当前版本的 Spring 数据模块需要 Spring 框架 5.3.16 或更好。这些模块还可以与该小版本的旧 Bugfix 版本一起工作。但是,强烈建议你在这一代中使用最新的版本。 + +## 3. 对象映射基础 + +本节介绍了 Spring 数据对象映射、对象创建、字段和属性访问、可变性和不可变性的基本原理。注意,本节仅适用于不使用底层数据存储的对象映射的 Spring 数据模块(如 JPA)。还要确保查阅存储特定的部分以获得存储特定的对象映射,例如索引、自定义列或字段名称等。 + +Spring 数据对象映射的核心职责是创建域对象的实例,并将存储本机数据结构映射到这些实例上。这意味着我们需要两个基本步骤: + +1. 通过使用公开的构造函数之一创建实例。 + +2. 实例填充以实体化所有公开的属性。 + +### 3.1.对象创建 + +Spring 数据自动地尝试检测用于实现该类型对象的持久性实体的构造函数。分辨率算法的工作原理如下: + +1. 如果只有一个构造函数,就使用它。 + +2. 如果有多个构造函数,并且正好有一个用`@PersistenceConstructor`注释,则使用它。 + +3. 如果有一个无参数构造函数,就使用它。其他构造函数将被忽略。 + +值解析假定构造函数参数名称与实体的属性名称匹配,即解析将被执行,就像要填充属性一样,包括映射中的所有自定义(不同的数据存储栏或字段名称等)。这还需要类文件中可用的参数名称信息,或者构造函数中存在`@ConstructorProperties`注释。 + +值分辨率可以通过使用 Spring Framework 的`@Value`使用特定于存储的 SPEL 表达式的值注释来定制。请参阅有关商店特定映射的部分以获取更多详细信息。 + +对象创建内部 + +Spring 为了避免反射的开销,数据对象创建默认使用在运行时生成的工厂类,它将直接调用域类构造函数。例如,对于这个示例类型: + +``` +class Person { + Person(String firstname, String lastname) { … } +} +``` + +我们将在运行时创建一个在语义上与这个类等价的工厂类: + +``` +class PersonObjectInstantiator implements ObjectInstantiator { + + Object newInstance(Object... args) { + return new Person((String) args[0], (String) args[1]); + } +} +``` + +这给了我们一个迂回的 10% 的性能提升超过反映。为了使域类有资格进行这种优化,它需要遵守一组约束: + +* 它一定不是一个私人班级。 + +* 它不能是非静态的内部类 + +* 它一定不是一个 CGlib 代理类 + +* Spring 数据使用的构造函数不能是私有的 + +如果这些条件中的任何一个匹配, Spring 数据将通过反射返回到实体实例化。 + +### 3.2.财产人口 + +一旦创建了实体的实例, Spring 数据就会填充该类的所有剩余的持久性属性。除非已经由实体的构造函数填充(即通过其构造函数参数列表填充),否则将首先填充标识符属性,以允许解析循环对象引用。在此之后,构造函数尚未填充的所有非瞬态属性都将在实体实例上设置。为此,我们使用以下算法: + +1. 如果属性是不可变的,但是公开了`with…`方法(见下文),那么我们使用`with…`方法使用新的属性值创建一个新的实体实例。 + +2. 如果定义了属性访问(即通过 getter 和 setter 进行访问),那么我们调用的是 setter 方法。 + +3. 如果属性是可变的,我们直接设置字段。 + +4. 如果该属性是不可变的,那么我们将使用持久化操作使用的构造函数(参见[Object creation](#mapping.object-creation))来创建实例的副本。 + +5. 默认情况下,我们直接设置字段的值。 + +财产人口内部 + +与我们的[对象构造中的优化](#mapping.object-creation.details)类似,我们也使用 Spring 数据运行时生成的访问器类与实体实例交互。 + +``` +class Person { + + private final Long id; + private String firstname; + private @AccessType(Type.PROPERTY) String lastname; + + Person() { + this.id = null; + } + + Person(Long id, String firstname, String lastname) { + // Field assignments + } + + Person withId(Long id) { + return new Person(id, this.firstname, this.lastame); + } + + void setLastname(String lastname) { + this.lastname = lastname; + } +} +``` + +例 3。生成的属性访问器 + +``` +class PersonPropertyAccessor implements PersistentPropertyAccessor { + + private static final MethodHandle firstname; (2) + + private Person person; (1) + + public void setProperty(PersistentProperty property, Object value) { + + String name = property.getName(); + + if ("firstname".equals(name)) { + firstname.invoke(person, (String) value); (2) + } else if ("id".equals(name)) { + this.person = person.withId((Long) value); (3) + } else if ("lastname".equals(name)) { + this.person.setLastname((String) value); (4) + } + } +} +``` + +|**1**|PropertyAccessor 持有底层对象的可变实例。这是为了使本来不可变的属性发生突变。| +|-----|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|默认情况下, Spring Data 使用字段访问来读写属性值。根据`private`字段的可见性规则,`MethodHandles`用于与字段交互。| +|**3**|该类公开了一个`withId(…)`方法,该方法用于设置标识符,例如,当一个实例被插入到数据存储中并生成了一个标识符时。调用`withId(…)`将创建一个新的`Person`对象。所有后续的突变都将发生在新实例中,而前一个实例将保持不变。| +|**4**|使用属性访问允许直接调用方法,而不使用`MethodHandles`。| + +这给了我们一个迂回 25% 的性能提升超过反映。为了使域类有资格进行这种优化,它需要遵守一组约束: + +* 类型不能驻留在默认值中或`java`包下。 + +* 类型及其构造函数必须`public` + +* 内部类的类型必须是`static`。 + +* 使用的 Java 运行时必须允许在初始化`ClassLoader`中声明类。Java9 和更新版本施加了一定的限制。 + +默认情况下, Spring 数据尝试使用生成的属性访问器,如果检测到限制,则返回到基于反射的属性访问器。 + +让我们来看看下面这个实体: + +例 4。一个样本实体 + +``` +class Person { + + private final @Id Long id; (1) + private final String firstname, lastname; (2) + private final LocalDate birthday; + private final int age; (3) + + private String comment; (4) + private @AccessType(Type.PROPERTY) String remarks; (5) + + static Person of(String firstname, String lastname, LocalDate birthday) { (6) + + return new Person(null, firstname, lastname, birthday, + Period.between(birthday, LocalDate.now()).getYears()); + } + + Person(Long id, String firstname, String lastname, LocalDate birthday, int age) { (6) + + this.id = id; + this.firstname = firstname; + this.lastname = lastname; + this.birthday = birthday; + this.age = age; + } + + Person withId(Long id) { (1) + return new Person(id, this.firstname, this.lastname, this.birthday, this.age); + } + + void setRemarks(String remarks) { (5) + this.remarks = remarks; + } +} +``` + +|**1**|标识符属性是最终的,但在构造函数中设置为`null`,
该类公开了一个`withId(…)`方法,该方法用于设置标识符,例如,当一个实例被插入到数据存储中并生成了一个标识符时。
当创建一个新的实例时,原始的`Person`实例保持不变。
相同的模式通常应用于其他属性 wither 方法是可选的,因为持久性构造函数(参见 6)实际上是一个复制构造函数,并且设置该属性将被转换为创建一个新的实例,并应用新的标识符。| +|-----|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|`firstname`和`lastname`属性是普通的不可变属性,可能通过 getter 公开。| +|**3**|`age`属性是一个不可变的属性,但它是从`birthday`属性派生出来的,
根据所示的设计,数据库值将超过默认值,因为 Spring 数据使用的是唯一声明的构造函数,
即使这样做的目的是为了更好地进行计算,重要的是,这个构造函数还将`age`作为参数(可能会忽略它),否则属性填充步骤将尝试设置 Age 字段并失败,因为它是不可变的,并且不存在`with…`方法。| +|**4**|`comment`属性 is mutable 是通过直接设置其字段来填充的。| +|**5**|`remarks`属性是可变的,可以通过直接设置`comment`字段或调用 setter 方法来填充| +|**6**|该类公开了用于创建对象的工厂方法和构造函数。
这里的核心思想是使用工厂方法而不是附加的构造函数,以避免通过`@PersistenceConstructor`消除构造函数歧义的需要。
相反,默认属性将在工厂方法中处理。| + +### 3.3.一般性建议 + +* *尽量坚持使用不变的对象*—不可变对象很容易创建,因为物化一个对象只需要调用它的构造函数。此外,这也避免了你的域对象中充斥着允许客户端代码操作对象状态的 setter 方法。如果你需要这些,可以选择对它们进行包保护,以便它们只能被有限数量的合用类型调用。只有建造者的物化比财产人口快多达 30%。 + +* *提供一个 All-Args 构造器*——即使你不能或不想将实体建模为不可变的值,提供一个构造函数仍然有价值,该构造函数将实体的所有属性作为参数,包括可变的属性,因为这允许对象映射跳过属性总体以获得最佳性能。 + +* *Use factory methods instead of overloaded constructors to avoid `@PersistenceConstructor`*—有了最佳性能所需的全参数构造函数,我们通常希望公开更多的应用程序用例特定的构造函数,这些构造函数省略了自动生成的标识符等内容。使用静态工厂方法来公开 All-Args 构造函数的这些变体是一种已有的模式。 + +* *确保遵守允许使用生成的实例器和属性访问器类的约束*— + +* *For identifiers to be generated, still use a final field in combination with an all-arguments persistence constructor (preferred) or a `with…` method*— + +* *使用 Lombok 避免样板代码*—因为持久性操作通常需要一个构造函数来接受所有参数,所以它们的声明变成了对字段分配的样板参数的繁琐重复,而使用 Lombok 的`@AllArgsConstructor`可以最好地避免这种重复。 + +#### 3.3.1.覆盖属性 + +Java 允许域类的灵活设计,其中一个子类可以定义一个属性,该属性已经在其超类中以相同的名称声明了。考虑以下示例: + +``` +public class SuperType { + + private CharSequence field; + + public SuperType(CharSequence field) { + this.field = field; + } + + public CharSequence getField() { + return this.field; + } + + public void setField(CharSequence field) { + this.field = field; + } +} + +public class SubType extends SuperType { + + private String field; + + public SubType(String field) { + super(field); + this.field = field; + } + + @Override + public String getField() { + return this.field; + } + + public void setField(String field) { + this.field = field; + + // optional + super.setField(field); + } +} +``` + +这两个类都使用可分配类型定义`field`。`SubType`然而阴影`SuperType.field`。根据类的设计,使用构造函数可能是设置`SuperType.field`的唯一默认方法。或者,在 setter 中调用`super.setField(…)`可以在`SuperType`中设置`field`。所有这些机制在某种程度上都会产生冲突,因为这些属性共享相同的名称,但可能表示两个不同的值。 Spring 如果类型是不可分配的,则数据跳过超类型属性。也就是说,重写的属性的类型必须可分配给它的超类型属性类型,以注册为重写,否则超类型属性被认为是瞬态的。我们通常建议使用不同的属性名称。 + +Spring 数据模块通常支持持有不同值的重写属性。从编程模型的角度来看,有几点需要考虑: + +1. 应该持久化哪个属性(默认为所有声明的属性)?你可以通过使用`@Transient`注释这些属性来排除这些属性。 + +2. 如何表示你的数据存储中的属性?对不同的值使用相同的字段/列名称通常会导致数据损坏,因此你应该使用显式的字段/列名称对至少一个属性进行注释。 + +3. 使用`@AccessType(PROPERTY)`不能作为超级属性,如果不对 setter 实现进行任何进一步的假设,则通常不能进行设置。 + +### 3.4. Kotlin 支持 + +Spring 数据适应 Kotlin 的细节以允许对象创建和突变。 + +#### 3.4.1. Kotlin 对象创建 ### + +Kotlin 支持实例化类,所有类在默认情况下都是不可变的,并且需要显式的属性声明来定义可变属性。考虑以下`data`类`Person`: + +``` +data class Person(val id: String, val name: String) +``` + +上面的类使用显式构造函数编译为一个典型的类。我们可以通过添加另一个构造函数来自定义这个类,并用`@PersistenceConstructor`对它进行注释,以表示构造函数的首选项: + +``` +data class Person(var id: String, val name: String) { + + @PersistenceConstructor + constructor(id: String) : this(id, "unknown") +} +``` + +Kotlin 如果没有提供参数,则允许使用默认值,从而支持参数的可选性。当 Spring 数据检测到具有参数 default 的构造函数时,如果数据存储区不提供值(或简单地返回`null`),则不存在这些参数,因此 Kotlin 可以应用参数 default。考虑为`name`应用参数 default 的以下类 + +``` +data class Person(var id: String, val name: String = "unknown") +``` + +每当`name`参数不是结果的一部分或其值`null`时,则`name`默认为`unknown`。 + +#### 3.4.2. Kotlin 数据类的属性总体 #### + +在 Kotlin 中,所有类在默认情况下都是不可变的,并且需要显式的属性声明来定义可变属性。考虑以下`data`类`Person`: + +``` +data class Person(val id: String, val name: String) +``` + +这个类实际上是不可变的。它允许创建新实例,因为 Kotlin 生成了一个`copy(…)`方法,该方法创建新的对象实例,从现有对象复制所有属性值,并将作为参数提供的属性值应用到该方法。 + +#### 3.4.3. Kotlin 最重要的属性 + +Kotlin 允许声明[属性重写](https://kotlinlang.org/docs/inheritance.html#overriding-properties)来改变子类中的属性。 + +``` +open class SuperType(open var field: Int) + +class SubType(override var field: Int = 1) : + SuperType(field) { +} +``` + +这样的安排呈现了两个名为`field`的属性。 Kotlin 为每个类中的每个属性生成属性访问器(getter 和 setter)。实际上,代码如下所示: + +``` +public class SuperType { + + private int field; + + public SuperType(int field) { + this.field = field; + } + + public int getField() { + return this.field; + } + + public void setField(int field) { + this.field = field; + } +} + +public final class SubType extends SuperType { + + private int field; + + public SubType(int field) { + super(field); + this.field = field; + } + + public int getField() { + return this.field; + } + + public void setField(int field) { + this.field = field; + } +} +``` + +在`SubType`上的 getters 和 setters 只设置`SubType.field`而不是`SuperType.field`。在这种安排中,使用构造函数是设置`SuperType.field`的唯一缺省方法。通过`this.SuperType.field = …`向`SubType`添加一个方法来设置`SuperType.field`是可能的,但不属于受支持的约定。属性重写在一定程度上造成了冲突,因为这些属性共享相同的名称,但可能表示两个不同的值。我们通常建议使用不同的属性名称。 + +Spring 数据模块通常支持持有不同值的重写属性。从编程模型的角度来看,有几点需要考虑: + +1. 应该持久化哪个属性(默认为所有声明的属性)?你可以通过使用`@Transient`注释这些属性来排除这些属性。 + +2. 如何表示你的数据存储中的属性?对不同的值使用相同的字段/列名称通常会导致数据损坏,因此你应该使用显式的字段/列名称对至少一个属性进行注释。 + +3. 不能使用`@AccessType(PROPERTY)`作为不能设置的超级属性。 + +## 4. 使用 Spring 数据存储库 + +Spring 数据存储库抽象的目标是显著减少为各种持久性存储实现数据访问层所需的样板代码的数量。 + +| |*Spring Data repository documentation and your module*

本章解释了 Spring 数据存储库的核心概念和接口。
本章中的信息是从 Spring 数据共享模块中提取的。
它使用了配置以及 Java 持久性 API( JPA)模块的代码示例。
你应该调整 XML 名称空间声明和要扩展的类型,使其与你所使用的特定模块的等同物。“[名称空间引用](#repositories.namespace-reference)”涵盖了 XML 配置,该配置在支持存储库 API 的所有 Spring 数据模块中都受到支持。“[存储库查询关键字](#repository-query-keywords)”一般涵盖了存储库抽象支持的查询方法关键字。
有关模块的具体功能的详细信息,请参见本文档中有关该模块的章节。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 4.1.核心概念 + +Spring 数据存储库抽象中的中心接口是`Repository`。它把要管理的域类以及域类的 ID 类型作为类型参数。这个接口主要充当一个标记接口,用于捕获要使用的类型,并帮助你发现扩展这个类型的接口。[“粗栓剂”](https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/CrudRepository.html)接口为正在管理的实体类提供了复杂的增删改查功能。 + +例 5。`CrudRepository`接口 + +``` +public interface CrudRepository extends Repository { + + S save(S entity); (1) + + Optional findById(ID primaryKey); (2) + + Iterable findAll(); (3) + + long count(); (4) + + void delete(T entity); (5) + + boolean existsById(ID primaryKey); (6) + + // … more functionality omitted. +} +``` + +|**1**|保存给定的实体。| +|-----|-----------------------------------------------------| +|**2**|返回由给定 ID 标识的实体。| +|**3**|返回所有实体。| +|**4**|返回实体的数量。| +|**5**|删除给定的实体。| +|**6**|指示是否存在具有给定 ID 的实体。| + +| |我们还提供了特定于持久性技术的抽象,例如`JpaRepository`或`MongoRepository`。
这些接口扩展了`CrudRepository`,并且除了比较通用的持久性技术之外,还公开了底层持久性技术的功能,这些接口与持久性技术无关,例如`CrudRepository`。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在`CrudRepository`之上,有一个[“pagingandsortingrepository”](https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/PagingAndSortingRepository.html)抽象,它添加了其他方法,以方便对实体的分页访问: + +例 6。`PagingAndSortingRepository`接口 + +``` +public interface PagingAndSortingRepository extends CrudRepository { + + Iterable findAll(Sort sort); + + Page findAll(Pageable pageable); +} +``` + +要以 20 的页面大小访问`User`的第二页,可以执行以下操作: + +``` +PagingAndSortingRepository repository = // … get access to a bean +Page users = repository.findAll(PageRequest.of(1, 20)); +``` + +除了查询方法之外,还可以对 Count 和 Delete 查询进行查询派生。下面的列表显示了派生的 Count 查询的接口定义: + +例 7。派生计数查询 + +``` +interface UserRepository extends CrudRepository { + + long countByLastname(String lastname); +} +``` + +下面的清单显示了派生删除查询的接口定义: + +例 8。派生删除查询 + +``` +interface UserRepository extends CrudRepository { + + long deleteByLastname(String lastname); + + List removeByLastname(String lastname); +} +``` + +### 4.2.查询方法 + +标准增删改查功能存储库通常对底层数据存储进行查询。对于 Spring 数据,声明这些查询变成了一个四步过程: + +1. 声明一个扩展存储库或其子接口之一的接口,并将其键入它应该处理的域类和 ID 类型,如以下示例所示: + + ``` + interface PersonRepository extends Repository { … } + ``` + +2. 在接口上声明查询方法。 + + ``` + interface PersonRepository extends Repository { + List findByLastname(String lastname); + } + ``` + +3. 设置 Spring 来为这些接口创建代理实例,或者使用[JavaConfig](#repositories.create-instances.java-config),或者使用[XML 配置](#repositories.create-instances)。 + + 1. 要使用 Java 配置,请创建一个类似于以下内容的类: + + ``` + import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + + @EnableJpaRepositories + class Config { … } + ``` + + 2. 要使用 XML 配置,请定义类似于以下内容的 Bean: + + ``` + + + + + + + ``` + + JPA 名称空间在本例中使用。如果你对任何其他存储使用存储库抽象,则需要将其更改为存储模块的适当名称空间声明。换句话说,你应该使用`jpa`来交换,例如,`mongodb`。 + + 另外,请注意,JavaConfig 变体不会显式地配置包,因为默认情况下使用的是带注释的类的包。要定制要扫描的包,请使用数据存储特定存储库的`@Enable${store}Repositories`-注释的`basePackage…`属性之一。 + +4. 注入存储库实例并使用它,如以下示例所示: + + ``` + class SomeClient { + + private final PersonRepository repository; + + SomeClient(PersonRepository repository) { + this.repository = repository; + } + + void doSomething() { + List persons = repository.findByLastname("Matthews"); + } + } + ``` + +下面的小节详细解释了每个步骤: + +* [定义存储库接口](#repositories.definition) + +* [定义查询方法](#repositories.query-methods.details) + +* [创建存储库实例](#repositories.create-instances) + +* [Custom Implementations for Spring Data Repositories](#repositories.custom-implementations) + +### 4.3.定义存储库接口 + +要定义存储库接口,首先需要定义一个特定于域类的存储库接口。接口必须扩展`Repository`,并键入到域类和 ID 类型。如果希望公开该域类型的增删改查方法,请扩展`CrudRepository`,而不是`Repository`。 + +#### 4.3.1.微调存储库定义 + +通常,存储库接口扩展`Repository`、`CrudRepository`或`PagingAndSortingRepository`。或者,如果不想扩展 Spring 数据接口,也可以使用`@RepositoryDefinition`对存储库接口进行注释。扩展`CrudRepository`公开了一组完整的方法来操作你的实体。如果你希望对要公开的方法有所选择,请将要公开的方法从`CrudRepository`复制到你的域存储库中。 + +| |这样做可以让你在所提供的 Spring 数据存储库功能之上定义自己的抽象。| +|---|-------------------------------------------------------------------------------------------------------------| + +下面的示例显示了如何有选择地公开增删改查方法(在本例中为 `findbyid’和`save`): + +例 9。有选择地公开增删改查方法 + +``` +@NoRepositoryBean +interface MyBaseRepository extends Repository { + + Optional findById(ID id); + + S save(S entity); +} + +interface UserRepository extends MyBaseRepository { + User findByEmailAddress(EmailAddress emailAddress); +} +``` + +在前面的示例中,你为所有域存储库定义了一个公共的基本接口,并公开了`findById(…)`以及`save(…)`,这些方法被路由到 Spring 数据提供的你选择的存储库的基本存储库实现(例如,如果你使用 JPA,实现是`SimpleJpaRepository`),因为它们匹配`CrudRepository`中的方法签名。因此`UserRepository`现在可以保存用户,通过 ID 查找单个用户,并触发查询以通过电子邮件地址查找`Users`。 + +| |中间存储库接口用`@NoRepositoryBean`注释。
确保将该注释添加到所有存储库接口中,其中 Spring 数据不应在运行时为其创建实例。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 4.3.2.使用具有多个 Spring 数据模块的存储库 + +在应用程序中使用唯一的 Spring 数据模块使事情变得简单,因为定义的作用域中的所有存储库接口都绑定到 Spring 数据模块。有时,应用程序需要使用多个 Spring 数据模块。在这种情况下,存储库定义必须区分持久性技术。 Spring 当检测到类路径上的多个存储库工厂时,数据进入严格的存储库配置模式。严格配置使用存储库或域类的详细信息来决定 Spring 存储库定义的数据模块绑定: + +1. 如果存储库定义[扩展特定于模块的存储库](#repositories.multiple-modules.types),则它是特定 Spring 数据模块的有效候选者。 + +2. 如果域类是[使用特定于模块的类型注释](#repositories.multiple-modules.annotations),则它是特定 Spring 数据模块的有效候选者。 Spring 数据模块要么接受第三方注释(例如 JPA 的`@Entity`),要么提供自己的注释(例如`@Document`用于 Spring Data MongoDB 和 Spring Data ElasticSearch)。 + +下面的示例展示了一个使用特定于模块的接口的存储库(本例中为 JPA): + +例 10。使用特定于模块的接口的存储库定义 + +``` +interface MyRepository extends JpaRepository { } + +@NoRepositoryBean +interface MyBaseRepository extends JpaRepository { … } + +interface UserRepository extends MyBaseRepository { … } +``` + +`MyRepository`和`UserRepository`在其类型层次结构中扩展`JpaRepository`。它们是 Spring 数据 JPA 模块的有效候选者。 + +下面的示例展示了一个使用通用接口的存储库: + +例 11。使用通用接口的存储库定义 + +``` +interface AmbiguousRepository extends Repository { … } + +@NoRepositoryBean +interface MyBaseRepository extends CrudRepository { … } + +interface AmbiguousUserRepository extends MyBaseRepository { … } +``` + +`AmbiguousRepository`和`AmbiguousUserRepository`在其类型层次结构中仅扩展`Repository`和`CrudRepository`。虽然在使用唯一的 Spring 数据模块时这是很好的,但多个模块无法区分这些存储库应该绑定到哪个特定的 Spring 数据。 + +下面的示例展示了一个使用带有注释的域类的存储库: + +例 12。使用带有注释的域类的存储库定义 + +``` +interface PersonRepository extends Repository { … } + +@Entity +class Person { … } + +interface UserRepository extends Repository { … } + +@Document +class User { … } +``` + +`PersonRepository`引用了`Person`,这是用 JPA `@Entity`注释的,所以这个存储库显然属于 Spring 数据 JPA。`UserRepository`引用`User`,这是用 Spring Data MongoDB 的`@Document`注释的。 + +下面的糟糕示例展示了一个存储库,它使用带有混合注释的域类: + +例 13。使用带有混合注释的域类的存储库定义 + +``` +interface JpaPersonRepository extends Repository { … } + +interface MongoDBPersonRepository extends Repository { … } + +@Entity +@Document +class Person { … } +``` + +这个示例展示了一个同时使用 JPA 和 Spring 数据 MongoDB 注释的域类。它定义了两个存储库,`JpaPersonRepository`和`MongoDBPersonRepository`。一个用于 JPA,另一个用于 MongoDB 的使用。 Spring 数据不再能够区分存储库,这导致未定义的行为。 + +[存储库类型详细信息](#repositories.multiple-modules.types)和[区分域类注释](#repositories.multiple-modules.annotations)用于严格的存储库配置,以识别特定 Spring 数据模块的存储库候选。在同一域类型上使用多个特定于持久化技术的注释是可能的,并且允许跨多个持久化技术重用域类型。然而, Spring 这样的数据就不能再确定与存储库绑定的唯一模块。 + +区分存储库的最后一种方法是对存储库基包进行范围界定。基包定义了扫描存储库接口定义的起点,这意味着存储库定义位于适当的包中。默认情况下,注释驱动的配置使用配置类的包。[基于 XML 的配置中的基本包](#repositories.create-instances.spring)是强制性的。 + +下面的示例展示了基本包的注释驱动配置: + +例 14。注解驱动的基包配置 + +``` +@EnableJpaRepositories(basePackages = "com.acme.repositories.jpa") +@EnableMongoRepositories(basePackages = "com.acme.repositories.mongo") +class Configuration { … } +``` + +### 4.4.定义查询方法 + +存储库代理有两种方式可以从方法名派生特定于存储的查询: + +* 通过直接从方法名派生查询。 + +* 通过使用手动定义的查询。 + +可用的选项取决于实际的商店。但是,必须有一种策略来决定实际创建了什么查询。下一节描述了可用的选项。 + +#### 4.4.1.查询查找策略 + +以下策略可用于存储库基础结构来解析查询。使用 XML 配置,你可以通过`query-lookup-strategy`属性在名称空间配置策略。对于 Java 配置,可以使用`Enable${store}Repositories`注释的`queryLookupStrategy`属性。某些策略可能不支持特定的数据存储。 + +* `CREATE`尝试从查询方法名构造特定于存储的查询。一般的方法是从方法名称中删除一组已知的前缀,并解析方法的其余部分。你可以在“[Query Creation](#repositories.query-methods.query-creation)”中阅读有关查询构造的更多信息。 + +* `USE_DECLARED_QUERY`尝试查找已声明的查询,如果找不到异常,则抛出异常。查询可以通过某个地方的注释来定义,也可以通过其他方式进行声明。请参阅特定存储的文档,以查找该存储的可用选项。如果存储库基础结构在引导过程中未找到该方法的已声明查询,则该查询将失败。 + +* `CREATE_IF_NOT_FOUND`(默认)组合`CREATE`和`USE_DECLARED_QUERY`。它首先查找已声明的查询,如果没有找到已声明的查询,则创建一个基于名称的自定义方法查询。这是默认的查找策略,因此,如果你没有显式地配置任何内容,就会使用该策略。它允许通过方法名快速定义查询,也可以根据需要引入声明的查询,从而对这些查询进行自定义优化。 + +#### 4.4.2.查询创建 + +Spring 数据存储库基础设施中内置的查询生成器机制对于在存储库的实体上构建约束查询非常有用。 + +下面的示例展示了如何创建许多查询: + +例 15。从方法名创建查询 + +``` +interface PersonRepository extends Repository { + + List findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname); + + // Enables the distinct flag for the query + List findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname); + List findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname); + + // Enabling ignoring case for an individual property + List findByLastnameIgnoreCase(String lastname); + // Enabling ignoring case for all suitable properties + List findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname); + + // Enabling static ORDER BY for a query + List findByLastnameOrderByFirstnameAsc(String lastname); + List findByLastnameOrderByFirstnameDesc(String lastname); +} +``` + +解析查询方法名分为主语和谓语。第一部分(“find…by”,`exists…By`)定义了查询的主题,第二部分形成了谓词。引入子句(主语)可以包含更多的表达形式。在`find`(或其他介绍关键字)和`By`之间的任何文本都被认为是描述性的,除非使用结果限制关键字之一,例如`Distinct`在要创建的查询或[“top”/“first”限制查询结果](#repositories.limit-query-result)上设置一个不同的标志。 + +附录中包含[查询方法主题关键字的完整列表](#appendix.query.method.subject)和[查询方法包括排序和大小写修饰符的谓词关键字](#appendix.query.method.predicate)。但是,第一个`By`充当分隔符,指示实际条件谓词的开始。在非常基本的级别上,你可以定义实体属性的条件,并将它们与`And`和`Or`连接起来。 + +解析该方法的实际结果取决于为其创建查询的持久性存储。然而,有一些一般性的事情需要注意: + +* 表达式通常是属性遍历,与可以级联的运算符结合在一起。可以将属性表达式与`AND`和`OR`合并。对于属性表达式,还可以支持诸如`Between`、`LessThan`、`GreaterThan`和`Like`之类的运算符。受支持的操作符可以因数据存储而异,因此请参阅参考文档的相应部分。 + +* 方法解析器支持为单个属性(例如,`findByLastnameIgnoreCase(…)`)或支持忽略 case 的类型的所有属性(通常是`String`实例——例如,`findByLastnameAndFirstnameAllIgnoreCase(…)`)设置`IgnoreCase`标志。是否支持忽略情况可能会因存储而异,因此请参阅引用文档中的相关部分以获取特定于存储的查询方法。 + +* 可以通过在引用属性的查询方法中附加`OrderBy`子句并提供排序方向(`ASC’或`Desc`)来应用静态排序。要创建支持动态排序的查询方法,请参见“[特殊参数处理](#repositories.special-parameters)”。 + +#### 4.4.3.属性表达式 + +属性表达式只能引用受管实体的直接属性,如前面的示例所示。在查询创建时,你已经确保解析的属性是托管域类的属性。但是,你也可以通过遍历嵌套属性来定义约束。考虑以下方法签名: + +``` +List findByAddressZipCode(ZipCode zipCode); +``` + +假设 a`Person`具有`Address`和`ZipCode`。在这种情况下,该方法将创建`x.address.zipCode`属性遍历。解析算法首先将整个部分(“addresszipcode”)解释为属性,然后检查域类中是否有该名称的属性(未大写)。如果算法成功,它将使用该属性。如果不是这样,算法就会将源从右侧的驼峰部分分割为头部和尾部,并尝试找到相应的属性——在我们的示例中,`AddressZip`和`Code`。如果算法找到了一个带有头部的属性,它就会获取尾部,并继续从那里构建树,按照刚才描述的方式将尾部分割开来。如果第一次分割不匹配,则算法将分割点向左移动(“address”,`ZipCode`)并继续。 + +尽管这在大多数情况下都适用,但算法可能会选择错误的属性。假设`Person`类也有一个`addressZip`属性。该算法将在第一轮分割中匹配,选择错误的属性,并失败(因为`addressZip`的类型可能没有`code`属性)。 + +要解决这种歧义,你可以在方法名中使用`_`来手动定义遍历点。因此,我们的方法名称如下: + +``` +List findByAddress_ZipCode(ZipCode zipCode); +``` + +因为我们将下划线字符视为保留字符,所以我们强烈建议遵循标准的 Java 命名约定(即不在属性名称中使用下划线,而是使用驼峰大小写)。 + +#### 4.4.4.特殊参数处理 + +要处理查询中的参数,请定义方法参数,如前面的示例中所示。除此之外,基础结构还可以识别某些特定类型,如`Pageable`和`Sort`,以便动态地对查询应用分页和排序。下面的示例演示了这些特性: + +例 16。在查询方法中使用`Pageable`、`Slice`和`Sort` + +``` +Page findByLastname(String lastname, Pageable pageable); + +Slice findByLastname(String lastname, Pageable pageable); + +List findByLastname(String lastname, Sort sort); + +List findByLastname(String lastname, Pageable pageable); +``` + +| |获取`Sort`和`Pageable`的 API 期望将非 `null’值传递到方法中。
如果不想应用任何排序或分页,请使用`Sort.unsorted()`和`Pageable.unpaged()`。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +第一个方法允许你将`org.springframework.data.domain.Pageable`实例传递给查询方法,以动态地将分页添加到静态定义的查询中。a`Page`知道可用的元素和页面的总数。它通过触发 Count 查询来计算总数量的基础设施来实现这一点。因为这可能很昂贵(取决于使用的存储空间),所以你可以返回`Slice`。a`Slice`只知道 next`Slice`是否可用,当遍历更大的结果集时,这可能就足够了。 + +排序选项也通过`Pageable`实例处理。如果只需要排序,请向方法中添加`org.springframework.data.domain.Sort`参数。正如你所看到的,返回`List`也是可能的。在这种情况下,不会创建构建实际`Page`实例所需的附加元数据(这反过来意味着不会发出本来需要的附加计数查询)。相反,它将查询限制为仅查找给定的实体范围。 + +| |要找出整个查询有多少页,你必须触发一个额外的 Count 查询。
默认情况下,该查询是从你实际触发的查询派生出来的。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 分页和排序 + +你可以使用属性名称来定义简单的排序表达式。你可以将表达式串联起来,以便将多个条件收集到一个表达式中。 + +例 17。定义排序表达式 + +``` +Sort sort = Sort.by("firstname").ascending() + .and(Sort.by("lastname").descending()); +``` + +要获得一种更安全的类型定义排序表达式的方法,请从要为其定义排序表达式的类型开始,并使用方法引用来定义要对其进行排序的属性。 + +例 18。使用类型安全的 API 定义排序表达式 + +``` +TypedSort person = Sort.sort(Person.class); + +Sort sort = person.by(Person::getFirstname).ascending() + .and(person.by(Person::getLastname).descending()); +``` + +| |`TypedSort.by(…)`通过(通常)使用 CGlib 来使用运行时代理,当使用诸如 Graal VM Native 之类的工具时,这可能会干扰本机图像编译。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +如果你的存储实现支持 QueryDSL,那么你也可以使用生成的元模型类型来定义排序表达式: + +例 19。使用 QueryDSL API 定义排序表达式 + +``` +QSort sort = QSort.by(QPerson.firstname.asc()) + .and(QSort.by(QPerson.lastname.desc())); +``` + +#### 4.4.5.限制查询结果 + +你可以使用`first`或`top`关键字来限制查询方法的结果,这些关键字可以互换使用。可以在`top`或`first`中添加一个可选的数值,以指定要返回的最大结果大小。如果省略了这个数字,则假定结果大小为 1。下面的示例展示了如何限制查询大小: + +例 20。用`Top`和`First`限制查询的结果大小 + +``` +User findFirstByOrderByLastnameAsc(); + +User findTopByOrderByAgeDesc(); + +Page queryFirst10ByLastname(String lastname, Pageable pageable); + +Slice findTop3ByLastname(String lastname, Pageable pageable); + +List findFirst10ByLastname(String lastname, Sort sort); + +List findTop10ByLastname(String lastname, Pageable pageable); +``` + +对于支持不同查询的数据存储,Limiting 表达式还支持`Distinct`关键字。此外,对于将结果集限制为一个实例的查询,支持用`Optional`关键字包装结果。 + +如果将分页或切片应用于限制性查询分页(以及可用页数的计算),则将在有限的结果中应用该分页。 + +| |通过使用`Sort`参数将结果与动态排序结合起来进行限制,这样就可以表达最小的“k”和最大的“k”元素的查询方法。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 4.4.6.返回集合或迭代的存储库方法 + +返回多个结果的查询方法可以使用标准的 Java`Iterable`、`List`和`Set`。除此之外,我们还支持返回 Spring 数据的`Streamable`、`Iterable`的自定义扩展以及[Vavr](https://www.vavr.io/)提供的集合类型。参考附录解释所有可能的[查询方法返回类型](#appendix.query.return.types)。 + +##### 使用 streamable 作为查询方法返回类型 + +你可以使用`Streamable`作为`Iterable`或任何集合类型的替代。它提供了方便的方法来访问非并行的`Stream`(从`Iterable`中缺少)和能够直接`….filter(…)`和`….map(…)`的元素,并将`Streamable`连接到其他元素: + +例 21。使用 streamable 组合查询方法的结果 + +``` +interface PersonRepository extends Repository { + Streamable findByFirstnameContaining(String firstname); + Streamable findByLastnameContaining(String lastname); +} + +Streamable result = repository.findByFirstnameContaining("av") + .and(repository.findByLastnameContaining("ea")); +``` + +##### 返回自定义的可刷新包装器类型 + +为集合提供专用的包装器类型是一种常用的模式,用于为返回多个元素的查询结果提供 API。通常,通过调用存储库方法返回类集合类型并手动创建包装器类型的实例来使用这些类型。你可以避免额外的步骤,因为 Spring Data 允许你使用这些包装器类型作为查询方法返回类型,如果它们满足以下条件的话: + +1. 类型实现`Streamable`。 + +2. 该类型公开了一个构造函数或一个名为`of(…)`或`valueOf(…)`的静态工厂方法,该方法以`Streamable`为参数。 + +下面的清单展示了一个示例: + +``` +class Product { (1) + MonetaryAmount getPrice() { … } +} + +@RequiredArgsConstructor(staticName = "of") +class Products implements Streamable { (2) + + private final Streamable streamable; + + public MonetaryAmount getTotal() { (3) + return streamable.stream() + .map(Priced::getPrice) + .reduce(Money.of(0), MonetaryAmount::add); + } + + @Override + public Iterator iterator() { (4) + return streamable.iterator(); + } +} + +interface ProductRepository implements Repository { + Products findAllByDescriptionContaining(String text); (5) +} +``` + +|**1**|允许 API 访问产品价格的`Product`实体。| +|-----|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|一个`Streamable`的包装器类型,它可以通过使用`Products.of(…)`(使用 Lombok 注释创建的工厂方法)来构造。
一个使用`Streamable`的标准构造函数也可以这样做。| +|**3**|包装器类型公开了一个额外的 API,在`Streamable`上计算新值。| +|**4**|实现`Streamable`接口并将其委托给实际结果。| +|**5**|该包装器类型`Products`可以直接用作返回类型的查询方法。
你不需要返回`Streamable`并在存储库客户端中进行查询后手动包装它。| + +##### 对 VAVR 收藏的支持 + +[Vavr](https://www.vavr.io/)是一个包含 Java 函数式编程概念的库。它附带了一组自定义的集合类型,你可以将其用作查询方法返回类型,如下表所示: + +| Vavr collection type |使用的 VAVR 实现类型|Valid Java source types| +|------------------------|----------------------------------|-----------------------| +|`io.vavr.collection.Seq`|`io.vavr.collection.List`| `java.util.Iterable` | +|`io.vavr.collection.Set`|`io.vavr.collection.LinkedHashSet`| `java.util.Iterable` | +|`io.vavr.collection.Map`|`io.vavr.collection.LinkedHashMap`| `java.util.Map` | + +你可以使用第一列中的类型(或其子类型)作为查询方法返回类型,并获取第二列中的类型作为实现类型,这取决于实际查询结果的 Java 类型(第三列)。或者,你可以声明`Traversable`(VAVR`Iterable`等价值),然后我们从实际的返回值派生实现类。即 a`java.util.List`变成 vAVR`List`或`Seq`,a`java.util.Set`变成 vAVR`LinkedHashSet``Set`,以此类推。 + +#### 4.4.7.存储库方法的空处理 + +在 Spring Data2.0 中,返回单个聚合实例的 Repository增删改查方法使用 Java8 的`Optional`来指示潜在的值不存在。除此之外, Spring Data 支持在查询方法上返回以下包装器类型: + +* `com.google.common.base.Optional` + +* `scala.Option` + +* `io.vavr.control.Option` + +或者,查询方法可以选择完全不使用包装器类型。然后通过返回`null`来表示没有查询结果。返回集合、集合替代方案、包装器和流的存储库方法保证永远不返回`null`,而是返回相应的空表示。有关详细信息,请参见“[存储库查询返回类型](#repository-query-return-types)”。 + +##### 可否定性注释 + +你可以通过使用[Spring Framework’s nullability annotations](https://docs.spring.io/spring-framework/docs/5.3.16/reference/html/core.html#null-safety)来表示存储库方法的可否定性约束。它们提供了一种工具友好的方法和 OPT-在运行时进行`null`检查,如下所示: + +* [`@NonNullApi`](https://docs.spring.io/spring/docs/5.3.16/javadoc-api/org/springframework/lang/NonNullApi.html):在包级别上用于声明参数和返回值的默认行为分别是既不接受也不产生`null`值。 + +* [`@NonNull`](https://docs.spring.io/spring/docs/5.3.16/javadoc-api/org/springframework/lang/NonNull.html):用于参数或返回值,该参数或返回值必须不是`null`(在适用`@NonNullApi`的参数和返回值上不需要)。 + +* [`@Nullable`](https://docs.spring.io/spring/docs/5.3.16/javadoc-api/org/springframework/lang/Nullable.html):用于可以是`null`的参数或返回值。 + +Spring 注释是用[JSR 305](https://jcp.org/en/jsr/detail?id=305)注释(一种休眠但广泛使用的 JSR)进行元注释的。JSR305 元注释允许工具供应商(例如[IDEA](https://www.jetbrains.com/help/idea/nullable-and-notnull-annotations.html)、[Eclipse](https://help.eclipse.org/oxygen/index.jsp?topic=/org.eclipse.jdt.doc.user/tasks/task-using_external_null_annotations.htm)和[Kotlin](https://kotlinlang.org/docs/reference/java-interop.html#null-safety-and-platform-types))以通用方式提供空安全支持,而无需对 Spring 注释进行硬编码支持。要为查询方法启用可否定性约束的运行时检查,你需要在包级别上通过在`package-info.java`中使用 Spring 的`@NonNullApi`来激活非可否定性,如以下示例所示: + +例 22。在`package-info.java`中声明不可无效 + +``` +@org.springframework.lang.NonNullApi +package com.acme; +``` + +一旦非空默认值到位,存储库查询方法调用将在运行时针对无效约束进行验证。如果查询结果违反了定义的约束,将引发异常。当该方法返回`null`但被声明为不可空(默认情况下,在存储库所在的包上定义了注释)时,就会发生这种情况。如果你希望再次 OPT 到无效的结果,可以在单个方法上选择性地使用`@Nullable`。使用本节开头提到的结果包装器类型将继续按预期工作:将空结果转换为表示缺省的值。 + +下面的示例展示了刚才描述的一些技术: + +例 23。使用不同的零度约束 + +``` +package com.acme; (1) + +import org.springframework.lang.Nullable; + +interface UserRepository extends Repository { + + User getByEmailAddress(EmailAddress emailAddress); (2) + + @Nullable + User findByEmailAddress(@Nullable EmailAddress emailAdress); (3) + + Optional findOptionalByEmailAddress(EmailAddress emailAddress); (4) +} +``` + +|**1**|存储库驻留在我们为其定义了非空行为的包(或子包)中。| +|-----|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|当查询不产生结果时抛出`EmptyResultDataAccessException`。当将`emailAddress`传递给方法的`null`时抛出`IllegalArgumentException`。| +|**3**|当查询不产生结果时,返回`null`。
还接受`null`作为`emailAddress`的值。| +|**4**|当查询不产生结果时,返回`Optional.empty()`。
当将`emailAddress`传递给方法的值`IllegalArgumentException`时,抛出一个`IllegalArgumentException`。| + +##### 基于 Kotlin 的存储库中的可否定性 + +Kotlin 已将[可否定性约束](https://kotlinlang.org/docs/reference/null-safety.html)的定义烘焙到语言中。 Kotlin 代码编译成字节码,其不通过方法签名而是通过编译元数据来表示可否定性约束。确保在你的项目中包含`kotlin-reflect` jar,以实现对 Kotlin 的无效约束的内省。 Spring 数据存储库使用语言机制来定义那些约束,以应用相同的运行时检查,如下所示: + +例 24。在 Kotlin 存储库上使用可否定性约束 + +``` +interface UserRepository : Repository { + + fun findByUsername(username: String): User (1) + + fun findByFirstname(firstname: String?): User? (2) +} +``` + +|**1**|该方法将参数和结果都定义为不可空( Kotlin 默认值)。
Kotlin 编译器拒绝将`null`传递给该方法的方法调用。
如果查询产生一个空结果,则抛出一个`EmptyResultDataAccessException`。| +|-----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|对于`firstname`参数,此方法接受`null`,如果查询不产生结果,则返回`null`。| + +#### 4.4.8.流式查询结果 + +你可以通过使用 Java8`Stream`作为返回类型来增量地处理查询方法的结果。不是将查询结果包装在`Stream`中,而是使用特定于数据存储的方法来执行流,如以下示例所示: + +例 25。用 java8`Stream`对查询结果进行流式处理 + +``` +@Query("select u from User u") +Stream findAllByCustomQueryAndStream(); + +Stream readAllByFirstnameNotNull(); + +@Query("select u from User u") +Stream streamAllPaged(Pageable pageable); +``` + +| |`Stream`可能会包装基础数据存储特定的资源,因此在使用后必须关闭。
你可以通过使用`close()`方法或通过使用 Java7`try-with-resources`块手动关闭`Stream`,如以下示例所示:| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +例 26。与`Stream`一起工作会导致`try-with-resources`块 + +``` +try (Stream stream = repository.findAllByCustomQueryAndStream()) { + stream.forEach(…); +} +``` + +| |并非所有 Spring 数据模块目前都支持`Stream`作为返回类型。| +|---|---------------------------------------------------------------------------| + +#### 4.4.9.异步查询结果 + +你可以使用[Spring’s asynchronous method running capability](https://docs.spring.io/spring-framework/docs/5.3.16/reference/html/integration.html#scheduling)异步运行存储库查询。这意味着当实际查询发生在已提交给 Spring `TaskExecutor`的任务中时,方法在调用后立即返回。异步查询与反应式查询不同,不应混用。有关反应性支持的更多详细信息,请参见特定于商店的文档。下面的示例显示了一些异步查询: + +``` +@Async +Future findByFirstname(String firstname); (1) + +@Async +CompletableFuture findOneByFirstname(String firstname); (2) + +@Async +ListenableFuture findOneByLastname(String lastname); (3) +``` + +|**1**|使用`java.util.concurrent.Future`作为返回类型。| +|-----|--------------------------------------------------------------------------------| +|**2**|使用 Java8`java.util.concurrent.CompletableFuture`作为返回类型。| +|**3**|使用`org.springframework.util.concurrent.ListenableFuture`作为返回类型。| + +### 4.5.创建存储库实例 + +本节介绍如何为已定义的存储库接口创建实例和 Bean 定义。这样做的一种方法是使用与每个支持存储库机制的 Spring 数据模块一起提供的 Spring 名称空间,尽管我们通常建议使用 Java 配置。 + +#### 4.5.1.XML 配置 + +Spring 每个数据模块包括一个`repositories`元素,该元素允许你定义一个基包, Spring 可以为你扫描该基包,如以下示例所示: + +例 27。通过 XML 启用 Spring 数据存储库 + +``` + + + + + + +``` + +在前面的示例中, Spring 被指示扫描及其所有子包以用于扩展的接口或其一个子接口。对于找到的每个接口,基础结构注册了与持久性技术相关的`FactoryBean`,以创建处理查询方法调用的适当代理。每个 Bean 都注册在一个 Bean 名称下,该名称来自接口名称,因此`UserRepository`的接口将注册在`userRepository`下。 Bean 嵌套式存储库接口的名称以其封闭类型名称作为前缀。`base-package`属性允许通配符,这样你就可以定义扫描包的模式。 + +##### 使用过滤器 + +默认情况下,基础结构会获取扩展位于配置的基包下的特定于持久性技术的`Repository`子接口的每个接口,并为其创建一个 Bean 实例。然而,你可能想要更细粒度的控制,来控制哪些接口为它们创建了 Bean 实例。为此,在``元素中使用``和``元素。语义与 Spring 上下文名称空间中的元素完全等价。有关这些元素的详细信息,请参见[Spring reference documentation](https://docs.spring.io/spring-framework/docs/5.3.16/reference/html/core.html#beans-scanning-filters)。 + +例如,为了将某些接口从作为存储库 bean 的实例化中排除,你可以使用以下配置: + +例 28。使用排除过滤器元件 + +``` + + + +``` + +前面的示例排除了所有以`SomeRepository`结尾的接口的实例化。 + +#### 4.5.2.Java 配置 + +你还可以通过在 Java 配置类上使用特定于存储的`@Enable${store}Repositories`注释来触发存储库基础设施。有关 Spring 容器的基于 Java 的配置的介绍,请参见[JavaConfig in the Spring reference documentation](https://docs.spring.io/spring-framework/docs/5.3.16/reference/html/core.html#beans-java)。 + +启用 Spring 数据存储库的示例配置类似于以下内容: + +例 29。基于注释的存储库配置示例 + +``` +@Configuration +@EnableJpaRepositories("com.acme.repositories") +class ApplicationConfiguration { + + @Bean + EntityManagerFactory entityManagerFactory() { + // … + } +} +``` + +| |前面的示例使用了 JPA 特定的注释,你将根据实际使用的存储模块对其进行更改。这同样适用于`EntityManagerFactory` Bean 的定义。请参阅涵盖特定于商店的配置的部分。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 4.5.3.独立使用 + +你还可以在 Spring 容器之外使用存储库基础设施——例如,在 CDI 环境中。在你的 Classpath 中仍然需要一些 Spring 库,但是,通常情况下,你也可以通过编程的方式设置存储库。 Spring 提供存储库支持的数据模块带有你可以使用的持久性技术特定的`RepositoryFactory`,如下所示: + +例 30。仓库工厂的独立使用 + +``` +RepositoryFactorySupport factory = … // Instantiate factory here +UserRepository repository = factory.getRepository(UserRepository.class); +``` + +### 4.6. Spring 数据存储库的自定义实现 + +Spring 数据提供了各种选项,以用很少的编码来创建查询方法。但是当这些选项不适合你的需求时,你也可以为存储库方法提供自己的定制实现。这一节描述了如何做到这一点。 + +#### 4.6.1.自定义各个存储库 + +要用自定义功能丰富存储库,你必须首先为自定义功能定义一个片段接口和一个实现,如下所示: + +例 31。自定义存储库功能的接口 + +``` +interface CustomizedUserRepository { + void someCustomMethod(User user); +} +``` + +例 32。自定义存储库功能的实现 + +``` +class CustomizedUserRepositoryImpl implements CustomizedUserRepository { + + public void someCustomMethod(User user) { + // Your custom implementation + } +} +``` + +| |与片段接口对应的类名中最重要的部分是`Impl`后缀。| +|---|-----------------------------------------------------------------------------------------------------------| + +该实现本身不依赖于 Spring 数据并且可以是常规的 Spring Bean。因此,可以使用标准的依赖注入行为来注入对其他 bean 的引用(例如)、参与方面,等等。 + +然后,你可以让你的存储库接口扩展片段接口,如下所示: + +例 33。对存储库接口的更改 + +``` +interface UserRepository extends CrudRepository, CustomizedUserRepository { + + // Declare query methods here +} +``` + +使用存储库接口扩展片段接口结合了增删改查和自定义功能,并使其对客户可用。 + +Spring 数据存储库是通过使用形成存储库组合的片段来实现的。片段是基本存储库、功能方面(如[QueryDsl](#core.extensions.querydsl))和自定义接口及其实现。每次向存储库接口添加一个接口时,都会通过添加一个片段来增强组合。基础存储库和存储库方面的实现由每个 Spring 数据模块提供。 + +下面的示例展示了自定义接口及其实现: + +例 34。片段及其实现 + +``` +interface HumanRepository { + void someHumanMethod(User user); +} + +class HumanRepositoryImpl implements HumanRepository { + + public void someHumanMethod(User user) { + // Your custom implementation + } +} + +interface ContactRepository { + + void someContactMethod(User user); + + User anotherContactMethod(User user); +} + +class ContactRepositoryImpl implements ContactRepository { + + public void someContactMethod(User user) { + // Your custom implementation + } + + public User anotherContactMethod(User user) { + // Your custom implementation + } +} +``` + +下面的示例展示了扩展`CrudRepository`的定制存储库的接口: + +例 35。对存储库接口的更改 + +``` +interface UserRepository extends CrudRepository, HumanRepository, ContactRepository { + + // Declare query methods here +} +``` + +存储库可以由多个自定义实现组成,这些实现是按照其声明的顺序导入的。自定义实现比基本实现和存储库方面具有更高的优先级。这种排序使你可以重写基本存储库和方面方法,并且如果两个片段提供相同的方法签名,则可以解决歧义。存储库片段不限于在单个存储库接口中使用。多个存储库可能使用一个片段接口,允许你在不同的存储库中重用定制。 + +下面的示例展示了一个存储库片段及其实现: + +例 36。覆盖`save(…)`的片段 + +``` +interface CustomizedSave { + S save(S entity); +} + +class CustomizedSaveImpl implements CustomizedSave { + + public S save(S entity) { + // Your custom implementation + } +} +``` + +下面的示例展示了一个使用前面的存储库片段的存储库: + +例 37。定制的存储库接口 + +``` +interface UserRepository extends CrudRepository, CustomizedSave { +} + +interface PersonRepository extends CrudRepository, CustomizedSave { +} +``` + +##### 配置 + +如果你使用名称空间配置,存储库基础设施将通过扫描它在其中找到存储库的包下面的类来尝试自动检测自定义实现片段。这些类需要遵循将命名空间元素的`repository-impl-postfix`属性追加到片段接口名称的命名惯例。此后缀缺省为`Impl`。下面的示例展示了一个使用默认后缀的存储库和一个为后缀设置自定义值的存储库: + +例 38。配置示例 + +``` + + + +``` + +前面示例中的第一个配置试图查找一个名为`com.acme.repository.CustomizedUserRepositoryImpl`的类,以充当自定义存储库实现。第二个示例尝试查找`com.acme.repository.CustomizedUserRepositoryMyPostfix`。 + +###### 歧义的解决 + +如果在不同的包中发现了具有匹配的类名的多个实现,则 Spring Data 使用 Bean 名称来标识要使用哪个。 + +给出了下面两个用于`CustomizedUserRepository`的自定义实现,使用了第一个实现。 Bean 它的名称是`customizedUserRepositoryImpl`,它与片段接口加上后缀`Impl`相匹配。 + +例 39。解决模棱两可的实现 + +``` +package com.acme.impl.one; + +class CustomizedUserRepositoryImpl implements CustomizedUserRepository { + + // Your custom implementation +} +``` + +``` +package com.acme.impl.two; + +@Component("specialCustomImpl") +class CustomizedUserRepositoryImpl implements CustomizedUserRepository { + + // Your custom implementation +} +``` + +如果用`@Component("specialCustom")`对`UserRepository`接口进行注释,则 Bean 名称加上`Impl`,然后与`com.acme.impl.two`中为存储库实现定义的名称匹配,并使用它来代替第一个。 + +###### 手动接线 + +如果你的自定义实现仅使用基于注释的配置和自动布线,则前面所示的方法工作得很好,因为它被视为任何其他方法 Spring Bean。如果你的实现片段 Bean 需要特殊的接线,则可以声明 Bean 并根据[前一节](#repositories.single-repository-behaviour.ambiguity)中描述的约定对其进行命名。然后,基础设施通过名称引用手动定义的 Bean 定义,而不是创建本身。下面的示例展示了如何手动连接自定义实现: + +例 40。自定义实现的手动接线 + +``` + + + + + +``` + +#### 4.6.2.自定义基本存储库 + +在[前一节](#repositories.manual-wiring)中描述的方法需要定制每个存储库接口,当你希望定制基本存储库行为时,所有存储库都会受到影响。为了改变所有存储库的行为,你可以创建一个实现来扩展特定于持久性技术的存储库基类。然后,这个类充当存储库代理的自定义基类,如下例所示: + +例 41。自定义存储库基类 + +``` +class MyRepositoryImpl + extends SimpleJpaRepository { + + private final EntityManager entityManager; + + MyRepositoryImpl(JpaEntityInformation entityInformation, + EntityManager entityManager) { + super(entityInformation, entityManager); + + // Keep the EntityManager around to used from the newly introduced methods. + this.entityManager = entityManager; + } + + @Transactional + public S save(S entity) { + // implementation goes here + } +} +``` + +| |该类需要具有存储特定的存储库工厂实现所使用的超类的构造函数。
如果存储库基类具有多个构造函数,则重写一个`EntityInformation`加上一个存储特定的基础设施对象(例如`EntityManager`或模板类)。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +最后一步是使 Spring 数据基础设施了解定制的存储库基类。在 Java 配置中,你可以通过使用`@Enable${store}Repositories`注释的`repositoryBaseClass`属性来实现这一点,如以下示例所示: + +例 42。使用 JavaConfig 配置自定义存储库基类 + +``` +@Configuration +@EnableJpaRepositories(repositoryBaseClass = MyRepositoryImpl.class) +class ApplicationConfiguration { … } +``` + +相应的属性在 XML 命名空间中可用,如以下示例所示: + +例 43。使用 XML 配置自定义存储库基类 + +``` + +``` + +### 4.7.从聚合根发布事件 + +由存储库管理的实体是聚合根。在域驱动的设计应用程序中,这些聚合根通常发布域事件。 Spring 数据提供了一种名为`@DomainEvents`的注释,你可以在你的聚合根的方法上使用该注释,以使该发布尽可能简单,如以下示例所示: + +例 44。从聚合根公开域事件 + +``` +class AnAggregateRoot { + + @DomainEvents (1) + Collection domainEvents() { + // … return events you want to get published here + } + + @AfterDomainEventPublication (2) + void callbackMethod() { + // … potentially clean up domain events list + } +} +``` + +|**1**|使用`@DomainEvents`的方法可以返回单个事件实例或事件集合。
它不能接受任何参数。| +|-----|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|在所有事件都已发布之后,我们有一个用`@AfterDomainEventPublication`注释的方法。
你可以使用它来清除要发布的事件列表(以及其他用途)。| + +每当 Spring 数据存储库的`save(…)`、`saveAll(…)`、`delete(…)`或`deleteAll(…)`方法被调用时,就会调用这些方法。 + +### 4.8. Spring 数据扩展 + +本节记录了一组 Spring 数据扩展,这些扩展允许 Spring 在各种上下文中使用数据。目前,大多数的集成是针对 Spring MVC 的。 + +#### 4.8.1.QueryDSL 扩展 + +[Querydsl](http://www.querydsl.com/)是一个框架,该框架通过其 Fluent API 支持构建静态类型的类 SQL 查询。 + +Spring 若干数据模块通过`QuerydslPredicateExecutor`提供与 QueryDSL 的集成,如下例所示: + +例 45。QueryDSLPredicateExecutor 接口 + +``` +public interface QuerydslPredicateExecutor { + + Optional findById(Predicate predicate); (1) + + Iterable findAll(Predicate predicate); (2) + + long count(Predicate predicate); (3) + + boolean exists(Predicate predicate); (4) + + // … more functionality omitted. +} +``` + +|**1**|查找并返回与`Predicate`匹配的单个实体。| +|-----|--------------------------------------------------------------| +|**2**|查找并返回所有匹配`Predicate`的实体。| +|**3**|返回匹配`Predicate`的实体的数量。| +|**4**|返回是否存在与`Predicate`匹配的实体。| + +要使用 QueryDSL 支持,请在存储库接口上扩展`QuerydslPredicateExecutor`,如下例所示: + +例 46。库上的 QueryDSL 集成 + +``` +interface UserRepository extends CrudRepository, QuerydslPredicateExecutor { +} +``` + +前面的示例允许你通过使用 QueryDSL`Predicate`实例来编写类型安全查询,如下例所示: + +``` +Predicate predicate = user.firstname.equalsIgnoreCase("dave") + .and(user.lastname.startsWithIgnoreCase("mathews")); + +userRepository.findAll(predicate); +``` + +#### 4.8.2.网络支持 + +Spring 支持存储库的数据模块编程模型提供了各种 Web 支持。与 Web 相关的组件要求 Spring MVC 容器在 Classpath 上。其中一些甚至提供了[Spring HATEOAS](https://github.com/spring-projects/spring-hateoas)的集成。通常,通过在 JavaConfig 配置类中使用`@EnableSpringDataWebSupport`注释来启用集成支持,如下例所示: + +例 47。启用 Spring 数据 Web 支持 + +``` +@Configuration +@EnableWebMvc +@EnableSpringDataWebSupport +class WebConfiguration {} +``` + +`@EnableSpringDataWebSupport`注释注册了一些组件。我们将在本节稍后讨论这些问题。它还在 Classpath 上检测 Spring 仇恨,并为其注册集成组件(如果存在)。 + +或者,如果使用 XML 配置,则将`SpringDataWebConfiguration`或`HateoasAwareSpringDataWebConfiguration`注册为 Spring bean,如下例所示(对于`SpringDataWebConfiguration`): + +例 48。启用 Spring XML 中的数据 Web 支持 + +``` + + + + +``` + +##### 基本网络支持 + +[上一节](#core.web)中显示的配置记录了一些基本组件: + +* a[使用`DomainClassConverter`类](#core.web.basic.domain-class-converter),用于让 Spring MVC 从请求参数或路径变量解析存储库管理的域类的实例。 + +* [HandlerMethodarGumentResolver’](#core.web.basic.paging-and-sorting)实现让 Spring MVC 从请求参数解析`Pageable`和`Sort`实例。 + +* [Jackson Modules](#core.web.basic.jackson-mappers)根据所使用的 Spring 数据模块,对`Point`和`Distance`之类的类型进行反/序列化,或者存储特定的类型。 + +###### Using the `DomainClassConverter` Class + +`DomainClassConverter`类允许你在 Spring MVC 控制器方法签名中直接使用域类型,这样你就不需要通过存储库手动查找实例,如下例所示: + +例 49。 Spring 在方法签名中使用域类型的 MVC 控制器 + +``` +@Controller +@RequestMapping("/users") +class UserController { + + @RequestMapping("/{id}") + String showUserForm(@PathVariable("id") User user, Model model) { + + model.addAttribute("user", user); + return "userForm"; + } +} +``` + +该方法直接接收`User`实例,无需进一步查找。可以通过让 Spring MVC 首先将路径变量转换为域类的`id`类型来解析实例,并最终通过在为域类型注册的存储库实例上调用`findById(…)`来访问实例。 + +| |目前,存储库必须实现`CrudRepository`才有资格被发现以进行转换。| +|---|-----------------------------------------------------------------------------------------------------------| + +###### 用于分页和排序的 HandlerMethodargumentResolver + +[上一节](#core.web.basic.domain-class-converter)中显示的配置片段还注册了`PageableHandlerMethodArgumentResolver`以及`SortHandlerMethodArgumentResolver`的实例。注册使`Pageable`和`Sort`成为有效的控制器方法参数,如下例所示: + +例 50。使用 Pageable 作为控制器方法参数 + +``` +@Controller +@RequestMapping("/users") +class UserController { + + private final UserRepository repository; + + UserController(UserRepository repository) { + this.repository = repository; + } + + @RequestMapping + String showUsers(Model model, Pageable pageable) { + + model.addAttribute("users", repository.findAll(pageable)); + return "users"; + } +} +``` + +Spring MVC 通过使用以下默认配置,尝试从请求参数中派生`Pageable`实例,从而得到前面的方法签名: + +|`page`|要检索的页面。索引为 0,默认值为 0。| +|------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|`size`|要检索的页面大小。默认值为 20。| +|`sort`|Properties that should be sorted by in the format `property,property(,ASC|(desc)(,ignorecase)。默认的排序方向是区分大小写的升序。如果要切换方向或大小写敏感度,请使用多个`sort`参数——例如,`?sort=firstname&sort=lastname,asc&sort=city,ignorecase`。| + +要定制此行为,请注册一个 Bean,该 Bean 分别实现`PageableHandlerMethodArgumentResolverCustomizer`接口或`SortHandlerMethodArgumentResolverCustomizer`接口。调用它的`customize()`方法,允许你更改设置,如下例所示: + +``` +@Bean SortHandlerMethodArgumentResolverCustomizer sortCustomizer() { + return s -> s.setPropertyDelimiter("<-->"); +} +``` + +如果设置现有`MethodArgumentResolver`的属性对你的目的来说是不够的,那么扩展`SpringDataWebConfiguration`或启用 Hateoas 的等效方法,覆盖`pageableResolver()`或`sortResolver()`方法,并导入自定义的配置文件,而不是使用`@Enable`注释。 + +如果需要从请求中解析多个`Pageable`或`Sort`实例(例如,对于多个表),则可以使用 Spring 的`@Qualifier`注释来区分一个实例和另一个实例。然后,请求参数必须以`${qualifier}_`作为前缀。下面的示例展示了生成的方法签名: + +``` +String showUsers(Model model, + @Qualifier("thing1") Pageable first, + @Qualifier("thing2") Pageable second) { … } +``` + +你必须填充`thing1_page`、`thing2_page`,以此类推。 + +传递到该方法中的默认`Pageable`相当于`PageRequest.of(0, 20)`,但是你可以使用`@PageableDefault`参数上的`@PageableDefault`注释来定制它。 + +##### 对页面的超媒体支持 + +Spring Hateoas 附带一个表示模型类,该表示模型类允许使用必要的`Page`元数据以及链接来丰富`Page`实例的内容,从而使客户端能够轻松地在页面中导航。将`Page`转换为`PagedResources`是通过 Spring Hateoas`ResourceAssembler`接口的实现完成的,该接口称为`PagedResourcesAssembler`。下面的示例展示了如何使用`PagedResourcesAssembler`作为控制器方法参数: + +例 51。使用 PagedResourcesAssembler 作为控制器方法参数 + +``` +@Controller +class PersonController { + + @Autowired PersonRepository repository; + + @RequestMapping(value = "/persons", method = RequestMethod.GET) + HttpEntity> persons(Pageable pageable, + PagedResourcesAssembler assembler) { + + Page persons = repository.findAll(pageable); + return new ResponseEntity<>(assembler.toResources(persons), HttpStatus.OK); + } +} +``` + +启用配置,如前面的示例所示,让`PagedResourcesAssembler`用作控制器方法参数。在其上调用`toResources(…)`具有以下效果: + +* `Page`的内容成为`PagedResources`实例的内容。 + +* `PagedResources`对象获得一个`PageMetadata`实例,并且它被填充了来自`Page`和底层`PageRequest`的信息。 + +* 根据页面的状态,`PagedResources`可能会附加`prev`和`next`链接。链接指向该方法映射到的 URI。添加到该方法中的分页参数与`PageableHandlerMethodArgumentResolver`的设置匹配,以确保以后可以解析链接。 + +假设我们在数据库中有 30 个`Person`实例。你现在可以触发一个请求(`get[http://localhost:8080/persons](http://localhost:8080/persons)`),并看到类似于以下内容的输出: + +``` +{ "links" : [ { "rel" : "next", + "href" : "http://localhost:8080/persons?page=1&size=20" } + ], + "content" : [ + … // 20 Person instances rendered here + ], + "pageMetadata" : { + "size" : 20, + "totalElements" : 30, + "totalPages" : 2, + "number" : 0 + } +} +``` + +汇编程序生成了正确的 URI,并且还选择了默认配置,以便为即将到来的请求将参数解析为`Pageable`。这意味着,如果你更改了该配置,那么链接将自动遵守更改。默认情况下,汇编器指向调用它的控制器方法,但是你可以通过传递一个自定义的`Link`作为构建分页链接的基础来定制该方法,这将重载`PagedResourcesAssembler.toResource(…)`方法。 + +##### Spring 数据 Jackson 模块 + +核心模块和一些存储特定的模块附带一组用于类型的 Jackson 模块,例如`org.springframework.data.geo.Distance`和`org.springframework.data.geo.Point`,由 Spring 数据域使用。一旦启用[web support](#core.web)并且`com.fasterxml.jackson.databind.ObjectMapper`可用,就会导入这些模块。 + +在初始化`SpringDataJacksonModules`期间,就像`SpringDataJacksonConfiguration`一样,被基础结构拾取,这样声明的`com.fasterxml.jackson.databind.Module`s 可用于 Jackson`ObjectMapper`。 + +公共基础设施注册了用于下列域类型的数据绑定 mixin。 + +``` +org.springframework.data.geo.Distance +org.springframework.data.geo.Point +org.springframework.data.geo.Box +org.springframework.data.geo.Circle +org.springframework.data.geo.Polygon +``` + +| |个别模块可以提供额外的`SpringDataJacksonModules`。
有关更多详细信息,请参阅商店特定部分。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------| + +##### 网络数据库支持 + +可以使用 Spring 数据投影(在[预测](#projections)中描述)通过使用[JSONPath](https://goessner.net/articles/JsonPath/)表达式(需要[Jayway JsonPath](https://github.com/json-path/JsonPath)或[XPath](https://www.w3.org/TR/xpath-31/)表达式(需要[XmlBeam](https://xmlbeam.org/))来绑定传入的请求有效负载,如下例所示: + +例 52。使用 JSONPath 或 XPath 表达式的 HTTP 有效负载绑定 + +``` +@ProjectedPayload +public interface UserPayload { + + @XBRead("//firstname") + @JsonPath("$..firstname") + String getFirstname(); + + @XBRead("/lastname") + @JsonPath({ "$.lastname", "$.user.lastname" }) + String getLastname(); +} +``` + +你可以使用前面示例中所示的类型作为 Spring MVC 处理程序方法参数,或者在`RestTemplate`的方法之一上使用`ParameterizedTypeReference`。前面的方法声明将尝试在给定的文档中的任何地方找到`firstname`。`lastname`XML 查找是在传入文档的顶层执行的。它的 JSON 变体首先尝试一个顶级的`lastname`,但如果前者不返回一个值,则还会尝试在`lastname`子文档中嵌套的`user`。这样,在不需要客户端调用公开的方法(通常是基于类的有效负载绑定的一个缺点)的情况下,可以轻松地减轻源文档结构中的更改。 + +支持[Projections](#projections)中所述的嵌套投影。如果方法返回复杂的非接口类型,则使用 Jackson`ObjectMapper`映射最终值。 + +对于 Spring MVC,一旦处于活动状态并且所需的依赖关系在 Classpath 上可用,所需的转换器就被自动注册。要使用`RestTemplate`,请手动注册`ProjectingJackson2HttpMessageConverter`或`XmlBeamHttpMessageConverter`。 + +有关更多信息,请参见规范[Spring Data Examples repository](https://github.com/spring-projects/spring-data-examples)中的[Web 投影示例](https://github.com/spring-projects/spring-data-examples/tree/master/web/projection)。 + +##### QueryDSL Web 支持 + +对于那些具有[QueryDSL](http://www.querydsl.com/)集成的存储,可以从`Request`查询字符串中包含的属性派生查询。 + +考虑以下查询字符串: + +``` +?firstname=Dave&lastname=Matthews +``` + +给定来自前面示例的`User`对象,你可以使用`QuerydslPredicateArgumentResolver`将查询字符串解析为下列值,如下所示: + +``` +QUser.user.firstname.eq("Dave").and(QUser.user.lastname.eq("Matthews")) +``` + +| |当在 Classpath 上找到 QueryDSL 时,该功能将与`@EnableSpringDataWebSupport`一起自动启用。| +|---|------------------------------------------------------------------------------------------------------------------------| + +将`@QuerydslPredicate`添加到方法签名中,可以提供一个现成的`Predicate`,你可以使用`QuerydslPredicateExecutor`来运行它。 + +| |类型信息通常是从方法的返回类型解析出来的。
由于该信息不一定与域类型匹配,因此使用`root`的`QuerydslPredicate`属性可能是个好主意。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +下面的示例展示了如何在方法签名中使用`@QuerydslPredicate`: + +``` +@Controller +class UserController { + + @Autowired UserRepository repository; + + @RequestMapping(value = "/", method = RequestMethod.GET) + String index(Model model, @QuerydslPredicate(root = User.class) Predicate predicate, (1) + Pageable pageable, @RequestParam MultiValueMap parameters) { + + model.addAttribute("users", repository.findAll(predicate, pageable)); + + return "index"; + } +} +``` + +|**1**|将查询字符串参数解析为匹配`Predicate`的`User`。| +|-----|------------------------------------------------------------------| + +默认绑定如下: + +* 在简单属性上`Object`为`eq`。 + +* `Object`在集合 like 属性上为`contains`。 + +* `Collection`在简单属性上为`in`。 + +你可以通过`@QuerydslPredicate`的`bindings`属性或通过使用 Java8`default methods`并将`QuerydslBinderCustomizer`方法添加到存储库接口来定制这些绑定,如下所示: + +``` +interface UserRepository extends CrudRepository, + QuerydslPredicateExecutor, (1) + QuerydslBinderCustomizer { (2) + + @Override + default void customize(QuerydslBindings bindings, QUser user) { + + bindings.bind(user.username).first((path, value) -> path.contains(value)) (3) + bindings.bind(String.class) + .first((StringPath path, String value) -> path.containsIgnoreCase(value)); (4) + bindings.excluding(user.password); (5) + } +} +``` + +|**1**|`QuerydslPredicateExecutor`提供对`Predicate`的特定查找方法的访问。| +|-----|------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|在存储库接口上定义的`QuerydslBinderCustomizer`将自动拾取并执行快捷方式`@QuerydslPredicate(bindings=…​)`。| +|**3**|将`username`属性的绑定定义为简单的`contains`绑定。| +|**4**|将`String`属性的默认绑定定义为不区分大小写的`contains`匹配。| +|**5**|将`password`属性从`Predicate`分辨率中排除。| + +| |你可以注册一个`QuerydslBinderCustomizerDefaults` Bean,保存默认的 QueryDSL 绑定,然后再从存储库应用特定的绑定,或者`@QuerydslPredicate`。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 4.8.3.存储库填充器 + +如果你使用 Spring JDBC 模块,你可能熟悉使用 SQL 脚本填充`DataSource`的支持。类似的抽象可以在存储库级别上使用,尽管它不使用 SQL 作为数据定义语言,因为它必须与存储无关。因此,populators 支持 XML(通过 Spring 的 OXM 抽象)和 JSON(通过 Jackson)来定义用于填充存储库的数据。 + +假设你有一个名为`data.json`的文件,其内容如下: + +例 53。在 JSON 中定义的数据 + +``` +[ { "_class" : "com.acme.Person", + "firstname" : "Dave", + "lastname" : "Matthews" }, + { "_class" : "com.acme.Person", + "firstname" : "Carter", + "lastname" : "Beauford" } ] +``` + +你可以通过使用 Spring Data Commons 中提供的存储库名称空间的 populator 元素来填充存储库。要将前面的数据填充到你的`PersonRepository`中,请声明一个类似于以下内容的填充器: + +例 54。声明 Jackson 存储库填充程序 + +``` + + + + + + +``` + +前面的声明导致`data.json`文件被 Jackson`ObjectMapper`读取和反序列化。 + +通过检查 JSON 文档的`_class`属性来确定解组 JSON 对象的类型。基础结构最终选择适当的存储库来处理反序列化的对象。 + +要使用 XML 来定义存储库应该使用的数据,可以使用`unmarshaller-populator`元素。你可以将其配置为使用 Spring OXM 中可用的 XML 编组器选项之一。有关详细信息,请参见[Spring reference documentation](https://docs.spring.io/spring-framework/docs/5.3.16/reference/html/data-access.html#oxm)。下面的示例展示了如何使用 JAXB 取消对存储库填充程序的约束: + +例 55。声明一个解组存储库填充程序(使用 JAXB) + +``` + + + + + + + + +``` + +## 5. Projections + +Spring 数据查询方法通常返回由存储库管理的聚合根的一个或多个实例。然而,有时基于这些类型的某些属性创建投影可能是可取的。 Spring 数据允许建模专用的返回类型,以更有选择性地检索托管聚合的部分视图。 + +想象一个存储库和聚合根类型,例如以下示例: + +例 56。样本集合和存储库 + +``` +class Person { + + @Id UUID id; + String firstname, lastname; + Address address; + + static class Address { + String zipCode, city, street; + } +} + +interface PersonRepository extends Repository { + + Collection findByLastname(String lastname); +} +``` + +现在想象一下,我们只想检索这个人的姓名属性。 Spring 数据提供了什么手段来实现这一点?本章的其余部分回答了这个问题。 + +### 5.1.基于界面的投影 + +将查询结果限制为仅限名称属性的最简单方法是声明一个接口,该接口公开要读取的属性的访问器方法,如以下示例所示: + +例 57。检索属性子集的投影接口 + +``` +interface NamesOnly { + + String getFirstname(); + String getLastname(); +} +``` + +这里重要的一点是,这里定义的属性与聚合根中的属性完全匹配。这样就可以添加一个查询方法,如下所示: + +例 58。使用基于接口的投影和查询方法的存储库 + +``` +interface PersonRepository extends Repository { + + Collection findByLastname(String lastname); +} +``` + +查询执行引擎在运行时为返回的每个元素创建该接口的代理实例,并将对公开方法的调用转发给目标对象。 + +| |在你的`Repository`中声明一个重写基本方法的方法(例如,在`CrudRepository`中声明了一个特定于存储的存储库接口,或者`Simple…Repository`)会导致对基本方法的调用,而不管声明的返回类型是什么。确保使用兼容的返回类型,因为基本方法不能用于投影。一些存储模块支持`@Query`注释,以将重写的基本方法转换为查询方法,然后可以使用该方法返回投影。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +投影可以递归地使用。如果你还想包含一些`Address`信息,那么为其创建一个投影接口,并从`getAddress()`的声明中返回该接口,如以下示例所示: + +例 59。检索属性子集的投影接口 + +``` +interface PersonSummary { + + String getFirstname(); + String getLastname(); + AddressSummary getAddress(); + + interface AddressSummary { + String getCity(); + } +} +``` + +在方法调用中,将获得目标实例的`address`属性,并依次将其包装到一个投影代理中。 + +#### 5.1.1.闭合投影 + +一个投影接口,其访问器的方法是所有匹配的属性的目标聚集被认为是一个封闭的投影。下面的示例(我们在本章前面也使用了它)是一个封闭投影: + +例 60。闭合投影 + +``` +interface NamesOnly { + + String getFirstname(); + String getLastname(); +} +``` + +Spring 如果使用闭合投影,则数据可以优化查询执行,因为我们知道用于支持投影代理所需的所有属性。有关此的更多详细信息,请参见参考文档中特定于模块的部分。 + +#### 5.1.2.开放投影 + +投影接口中的访问器方法也可以通过使用`@Value`注释来计算新值,如下例所示: + +例 61。一个开放的投影 + +``` +interface NamesOnly { + + @Value("#{target.firstname + ' ' + target.lastname}") + String getFullName(); + … +} +``` + +支持该投影的聚合根在`target`变量中可用。使用`@Value`的投影接口是一个开放的投影。 Spring 在这种情况下,数据不能应用查询执行优化,因为 SPEL 表达式可以使用聚合根的任何属性。 + +`@Value`中使用的表达式不应该太复杂——你希望避免在`String`变量中进行编程。对于非常简单的表达式,一种选择可能是求助于默认方法(在 Java8 中引入),如以下示例所示: + +例 62。使用自定义逻辑的默认方法的投影接口 + +``` +interface NamesOnly { + + String getFirstname(); + String getLastname(); + + default String getFullName() { + return getFirstname().concat(" ").concat(getLastname()); + } +} +``` + +这种方法要求你能够完全基于在投影接口上公开的其他访问器方法来实现逻辑。第二个更灵活的选项是在 Spring Bean 中实现自定义逻辑,然后从 SPEL 表达式调用该逻辑,如以下示例所示: + +例 63。样本人物对象 + +``` +@Component +class MyBean { + + String getFullName(Person person) { + … + } +} + +interface NamesOnly { + + @Value("#{@myBean.getFullName(target)}") + String getFullName(); + … +} +``` + +注意 SPEL 表达式是如何引用`myBean`并调用`getFullName(…)`方法并将投影目标作为方法参数转发的。由 SPEL 表达式评估支持的方法也可以使用方法参数,然后可以从表达式引用这些参数。该方法参数可通过名为`Object`的`args`的数组获得。下面的示例展示了如何从`args`数组中获取方法参数: + +例 64。样本人物对象 + +``` +interface NamesOnly { + + @Value("#{args[0] + ' ' + target.firstname + '!'}") + String getSalutation(String prefix); +} +``` + +同样,对于更复杂的表达式,你应该使用 Spring Bean 并让表达式调用一个方法,如[earlier](#projections.interfaces.open.bean-reference)所述。 + +#### 5.1.3.可空包装纸 + +投影接口中的吸取器可以利用可空包装器来提高零值安全性。当前支持的包装器类型如下: + +* `java.util.Optional` + +* `com.google.common.base.Optional` + +* `scala.Option` + +* `io.vavr.control.Option` + +例 65。一种使用可空包装器的投影接口 + +``` +interface NamesOnly { + + Optional getFirstname(); +} +``` + +如果底层的投影值不是`null`,那么值将使用包装器类型的当前表示返回。如果支持值是`null`,那么 getter 方法返回所使用的包装器类型的空表示。 + +### 5.2.基于类别的投影(DTO) + +定义投影的另一种方法是使用值类型 DTO(数据传输对象),它为应该检索的字段保存属性。这些 DTO 类型可以以与使用投影接口完全相同的方式使用,只是不会发生代理,也不会应用嵌套的投影。 + +如果存储通过限制要加载的字段来优化查询执行,那么要加载的字段将从公开的构造函数的参数名称中确定。 + +下面的示例显示了一个投影 DTO: + +例 66。投射 DTO + +``` +class NamesOnly { + + private final String firstname, lastname; + + NamesOnly(String firstname, String lastname) { + + this.firstname = firstname; + this.lastname = lastname; + } + + String getFirstname() { + return this.firstname; + } + + String getLastname() { + return this.lastname; + } + + // equals(…) and hashCode() implementations +} +``` + +| |避免使用投影 DTO 的样板代码

通过使用[Project Lombok](https://projectlombok.org),可以极大地简化 DTO 的代码,它提供了`@Value`注释(不要与 Spring 的`@Value`前面的接口示例中显示的注释相混淆)。
如果使用 Project Lombok 的`@Value`注释,前面显示的示例 DTO 将变成如下:

``gt@value<
class namesonly=”698"/>LastName;
}
```
缺省情况下,
字段是`private final`,该类公开了一个构造函数,该构造函数接受所有字段并自动获得`equals(…)`和`hashCode()`实现的方法。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 5.3.动态投影 + +到目前为止,我们已经使用了投影类型作为集合的返回类型或元素类型。但是,你可能想要选择要在调用时使用的类型(这使得它是动态的)。要应用动态投影,请使用以下示例中所示的查询方法: + +例 67。使用动态投影参数的存储库 + +``` +interface PersonRepository extends Repository { + + Collection findByLastname(String lastname, Class type); +} +``` + +通过这种方式,可以使用该方法来获得如当前所应用的或具有投影的聚合体,如以下示例所示: + +例 68。使用具有动态投影的存储库 + +``` +void someMethod(PersonRepository people) { + + Collection aggregates = + people.findByLastname("Matthews", Person.class); + + Collection aggregates = + people.findByLastname("Matthews", NamesOnly.class); +} +``` + +| |如果查询的实际返回类型等于`Class`参数的泛型参数类型,则检查类型
的查询参数是否符合动态投影参数的条件,然后匹配的`Class`参数在查询或 SPEL 表达式中不可用。
如果你想使用`Class`参数作为查询参数,那么请确保使用不同的泛型参数,例如`Class`。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## 6. 示例查询 + +### 6.1.导言 + +本章通过示例介绍了查询,并解释了如何使用它。 + +示例查询是一种用户友好的查询技术,具有简单的界面。它允许动态查询创建,并且不需要你编写包含字段名的查询。实际上,通过示例查询根本不需要你通过使用特定于商店的查询语言来编写查询。 + +### 6.2.用法 + +通过示例 API 进行的查询由三部分组成: + +* Probe:包含填充字段的域对象的实际示例。 + +* `ExampleMatcher`:`ExampleMatcher`包含有关如何匹配特定字段的详细信息。它可以在多个示例中重用。 + +* `Example`:an`Example`由探针和`ExampleMatcher`组成。它用于创建查询。 + +示例查询非常适合于以下几种用例: + +* 使用一组静态或动态约束来查询你的数据存储。 + +* 频繁地重构域对象,而不必担心破坏现有的查询。 + +* 独立于底层数据存储 API 工作。 + +示例查询也有几个限制: + +* 不支持嵌套或分组的属性约束,例如`firstname = ?0 or (firstname = ?1 and lastname = ?2)`。 + +* 只支持字符串的开始/包含/结束/正则表达式匹配,以及其他属性类型的精确匹配。 + +在开始使用示例查询之前,你需要有一个域对象。要开始,请为你的存储库创建一个接口,如以下示例所示: + +例 69。样本人物对象 + +``` +public class Person { + + @Id + private String id; + private String firstname; + private String lastname; + private Address address; + + // … getters and setters omitted +} +``` + +前面的示例展示了一个简单的域对象。你可以使用它来创建`Example`。默认情况下,具有`null`值的字段将被忽略,字符串将通过使用特定于存储的默认值进行匹配。 + +| |根据示例条件将属性包含到查询中是基于可否定性的。除非[忽略属性路径](#query-by-example.matchers),否则始终包括使用基元类型(`INT’,`double`,…)的属性。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +可以通过使用`of`工厂方法或使用[`examplematcher’](#query-by-example.matchers)构建示例。`Example`是不变的。下面的清单展示了一个简单的示例: + +例 70。简单的例子 + +``` +Person person = new Person(); (1) +person.setFirstname("Dave"); (2) + +Example example = Example.of(person); (3) +``` + +|**1**|创建域对象的新实例。| +|-----|-------------------------------------------| +|**2**|将属性设置为“查询”。| +|**3**|创建`Example`。| + +你可以通过使用存储库运行示例查询。为此,让你的存储库接口扩展`QueryByExampleExecutor`。下面的清单显示了`QueryByExampleExecutor`接口的摘录: + +例 71。the`QueryByExampleExecutor` + +``` +public interface QueryByExampleExecutor { + + S findOne(Example example); + + Iterable findAll(Example example); + + // … more functionality omitted. +} +``` + +### 6.3.示例匹配器 + +示例不限于默认设置。你可以使用`ExampleMatcher`为字符串匹配、空值处理和特定于属性的设置指定自己的默认值,如下例所示: + +例 72。具有定制匹配的示例匹配器 + +``` +Person person = new Person(); (1) +person.setFirstname("Dave"); (2) + +ExampleMatcher matcher = ExampleMatcher.matching() (3) + .withIgnorePaths("lastname") (4) + .withIncludeNullValues() (5) + .withStringMatcher(StringMatcher.ENDING); (6) + +Example example = Example.of(person, matcher); (7) +``` + +|**1**|创建域对象的新实例。| +|-----|---------------------------------------------------------------------------------------------------------------------------------------| +|**2**|设置属性。| +|**3**|创建`ExampleMatcher`以期望所有值匹配。
在此阶段,即使没有进一步的配置,它也是可用的。| +|**4**|构造一个新的`ExampleMatcher`来忽略`lastname`属性路径。| +|**5**|构造一个新的`ExampleMatcher`以忽略`lastname`属性路径并包括空值。| +|**6**|构造一个新的`ExampleMatcher`以忽略`lastname`属性路径,包括空值,并执行后缀字符串匹配。| +|**7**|基于域对象和配置的`ExampleMatcher`创建一个新的`Example`。| + +默认情况下,`ExampleMatcher`期望探测上设置的所有值都匹配。如果希望获得与隐式定义的任何谓词匹配的结果,请使用`ExampleMatcher.matchingAny()`。 + +你可以为各个属性指定行为(例如“firstname”和“lastname”,或者,对于嵌套属性,“address.city”)。你可以使用匹配的选项和大小写敏感性来调整它,如以下示例所示: + +例 73。配置匹配器选项 + +``` +ExampleMatcher matcher = ExampleMatcher.matching() + .withMatcher("firstname", endsWith()) + .withMatcher("lastname", startsWith().ignoreCase()); +} +``` + +配置 Matcher 选项的另一种方法是使用 lambdas(在 Java8 中引入)。这种方法创建了一个回调,该回调要求实现器修改匹配器。你不需要返回 Matcher,因为配置选项保存在 Matcher 实例中。下面的示例展示了一个使用 lambdas 的匹配器: + +例 74。使用 lambdas 配置匹配器选项 + +``` +ExampleMatcher matcher = ExampleMatcher.matching() + .withMatcher("firstname", match -> match.endsWith()) + .withMatcher("firstname", match -> match.startsWith()); +} +``` + +由`Example`创建的查询使用配置的合并视图。默认的匹配设置可以设置在`ExampleMatcher`级别,而单独的设置可以应用于特定的属性路径。设置在`ExampleMatcher`上的设置由属性路径设置继承,除非它们是显式定义的。属性修补程序上的设置比默认设置具有更高的优先级。下表描述了各种`ExampleMatcher`设置的作用域: + +| Setting |范围| +|--------------------|----------------------------------| +| Null-handling |`ExampleMatcher`| +| String matching |`ExampleMatcher`和属性路径| +|Ignoring properties |属性路径| +| Case sensitivity |`ExampleMatcher`和属性路径| +|Value transformation|属性路径| + +## 7. 审计 + +### 7.1.基础知识 + +Spring 数据提供了复杂的支持,以透明地跟踪谁创建或更改了一个实体以及何时发生了更改。为了从该功能中受益,你必须为你的实体类配备审计元数据,这些元数据可以使用注释或通过实现接口来定义。此外,必须通过注释配置或 XML 配置来启用审计,以注册所需的基础设施组件。有关配置示例,请参阅特定于商店的部分。 + +| |只跟踪创建和修改日期的应用程序不需要指定[`AuditorAware`](#auditing.auditor-aware)。| +|---|---------------------------------------------------------------------------------------------------------------------------------| + +#### 7.1.1.基于注释的审计元数据 + +我们提供`@CreatedBy`和`@LastModifiedBy`来捕获创建或修改实体的用户,以及`@CreatedDate`和`@LastModifiedDate`来捕获更改发生的时间。 + +例 75。被审计实体 + +``` +class Customer { + + @CreatedBy + private User user; + + @CreatedDate + private Instant createdDate; + + // … further properties omitted +} +``` + +正如你所看到的,根据你想要捕获的信息,可以有选择地应用这些注释。捕获何时进行了更改的注释可以用于类型 joda-time、`DateTime`、遗留 Java`Date`和`Calendar`、JDK8 日期和时间类型以及`long`或`Long`的属性。 + +审核元数据不一定需要存在于根级实体中,但可以添加到嵌入式实体中(取决于实际使用的存储),如下面的剪辑所示。 + +例 76。嵌入式实体中的审计元数据 + +``` +class Customer { + + private AuditMetadata auditingMetadata; + + // … further properties omitted +} + +class AuditMetadata { + + @CreatedBy + private User user; + + @CreatedDate + private Instant createdDate; + +} +``` + +#### 7.1.2.基于接口的审计元数据 + +如果你不想使用注释来定义审计元数据,那么可以让你的域类实现`Auditable`接口。它公开了所有审计属性的 setter 方法。 + +#### 7.1.3.`AuditorAware` + +在使用`@CreatedBy`或`@LastModifiedBy`的情况下,审计基础结构需要以某种方式意识到当前的主体。为了做到这一点,我们提供了一个`AuditorAware`SPI 接口,你必须实现该接口,以告知基础设施当前与应用程序交互的用户或系统是谁。泛型类型`T`定义了用`@CreatedBy`或`@LastModifiedBy`注释的属性必须是什么类型。 + +下面的示例展示了使用 Spring Security 的`Authentication`对象的接口的实现: + +例 77。基于 Spring 安全性的`AuditorAware`的实现 + +``` +class SpringSecurityAuditorAware implements AuditorAware { + + @Override + public Optional getCurrentAuditor() { + + return Optional.ofNullable(SecurityContextHolder.getContext()) + .map(SecurityContext::getAuthentication) + .filter(Authentication::isAuthenticated) + .map(Authentication::getPrincipal) + .map(User.class::cast); + } +} +``` + +该实现访问由 Spring Security 提供的`Authentication`对象,并查找你在`UserDetailsService`实现中创建的自定义`UserDetails`实例。这里我们假设你通过`UserDetails`实现公开了域用户,但是,基于找到的`Authentication`,你也可以从任何地方查找它。 + +#### 7.1.4.`ReactiveAuditorAware` + +当使用反应性基础结构时,你可能希望利用上下文信息来提供`@CreatedBy`或`@LastModifiedBy`信息。我们提供了一个`ReactiveAuditorAware`SPI 接口,你必须实现该接口,以告知基础结构当前与应用程序交互的用户或系统是谁。泛型类型`T`定义了用`@CreatedBy`或`@LastModifiedBy`注释的属性必须是什么类型。 + +下面的示例展示了接口的一个实现,该接口使用了 Responable Spring Security 的`Authentication`对象: + +例 78。基于 Spring 安全性的`ReactiveAuditorAware`的实现 + +``` +class SpringSecurityAuditorAware implements ReactiveAuditorAware { + + @Override + public Mono getCurrentAuditor() { + + return ReactiveSecurityContextHolder.getContext() + .map(SecurityContext::getAuthentication) + .filter(Authentication::isAuthenticated) + .map(Authentication::getPrincipal) + .map(User.class::cast); + } +} +``` + +该实现访问由 Spring Security 提供的`Authentication`对象,并查找你在`UserDetailsService`实现中创建的自定义`UserDetails`实例。这里我们假设你通过`UserDetails`实现公开了域用户,但是,基于找到的`Authentication`,你也可以从任何地方查找它。 + +## 附录 + +## 附录 A:名称空间引用 + +### ``元素 + +``元素触发了 Spring 数据存储库基础设施的设置。最重要的属性是`base-package`,它定义了要扫描 Spring 数据存储库接口的包。见“[XML 配置](#repositories.create-instances.spring)”。下表描述了``元素的属性: + +| Name |说明| +|------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `base-package` |定义要扫描存储库接口的包,这些存储库接口在自动检测模式下扩展`*Repository`(实际接口由特定的 Spring 数据模块决定)。配置包下面的所有包也会被扫描。允许使用通配符。| +| `repository-impl-postfix` |定义后缀以自动检测自定义存储库实现。名称以配置后缀结尾的类被视为候选类。默认值为`Impl`。| +| `query-lookup-strategy` |确定用于创建查找器查询的策略。详见“[查询查找策略](#repositories.query-methods.query-lookup-strategies)”。默认值为`create-if-not-found`。| +| `named-queries-location` |定义用于搜索包含外部定义的查询的属性文件的位置。| +|`consider-nested-repositories`|是否应该考虑嵌套的存储库接口定义。默认值为`false`。| + +## 附录 B:Populators 名称空间引用 + +### \元素 + +``元素允许通过 Spring 数据存储库基础设施填充数据存储。[1] + +| Name |说明| +|-----------|----------------------------------------------------------------------------------------| +|`locations`|从存储库中查找要读取对象的文件的位置应该是填充的。| + +## 附录 C:存储库查询关键字 + +### 支持的查询方法主题关键字 + +下表列出了通常由 Spring 数据存储库支持的用于表示谓词的查询派生机制的主题关键字。请参阅特定于商店的文档以获得所支持的关键字的确切列表,因为此处列出的某些关键字在特定商店中可能不受支持。 + +| Keyword |说明| +|--------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|`find…By`, `read…By`, `get…By`, `query…By`, `search…By`, `stream…By`|一般的查询方法通常返回存储库类型、`Collection`或`Streamable`子类型或结果包装器,如`Page`、`GeoResults`或任何其他特定于存储的结果包装器。可以用作`findBy…`、`findMyDomainTypeBy…`或与其他关键字组合使用。| +| `exists…By` |存在投影,通常返回`boolean`结果。| +| `count…By` |计数投影返回一个数字结果。| +| `delete…By`, `remove…By` |Delete 查询方法,返回无结果(“void”)或删除计数。| +| `…First…`, `…Top…` |将查询结果限制为结果的第一个``。这个关键字可以出现在`find`(和其他关键字)和`by`之间的主题的任何位置。| +| `…Distinct…` |使用不同的查询只返回唯一的结果。查看特定于商店的文档是否支持该功能。这个关键字可以出现在`find`(和其他关键字)和`by`之间的主题的任何位置。| + +### 支持的查询方法谓词关键字和修饰符 + +下表列出了通常由 Spring 数据存储库查询派生机制支持的谓词关键字。但是,请参阅特定于商店的文档以获得所支持的关键字的确切列表,因为此处列出的某些关键字在特定商店中可能不受支持。 + +| Logical keyword |关键字表达式| +|---------------------|----------------------------------------------| +| `AND` |`And`| +| `OR` |`Or`| +| `AFTER` |`After`, `IsAfter`| +| `BEFORE` |`Before`, `IsBefore`| +| `CONTAINING` |`Containing`, `IsContaining`, `Contains`| +| `BETWEEN` |`Between`, `IsBetween`| +| `ENDING_WITH` |`EndingWith`, `IsEndingWith`, `EndsWith`| +| `EXISTS` |`Exists`| +| `FALSE` |`False`, `IsFalse`| +| `GREATER_THAN` |`GreaterThan`, `IsGreaterThan`| +|`GREATER_THAN_EQUALS`|`GreaterThanEqual`, `IsGreaterThanEqual`| +| `IN` |`In`, `IsIn`| +| `IS` |`Is`,`Equals`,(或者没有关键字)| +| `IS_EMPTY` |`IsEmpty`, `Empty`| +| `IS_NOT_EMPTY` |`IsNotEmpty`, `NotEmpty`| +| `IS_NOT_NULL` |`NotNull`, `IsNotNull`| +| `IS_NULL` |`Null`, `IsNull`| +| `LESS_THAN` |`LessThan`, `IsLessThan`| +| `LESS_THAN_EQUAL` |`LessThanEqual`, `IsLessThanEqual`| +| `LIKE` |`Like`, `IsLike`| +| `NEAR` |`Near`, `IsNear`| +| `NOT` |`Not`, `IsNot`| +| `NOT_IN` |`NotIn`, `IsNotIn`| +| `NOT_LIKE` |`NotLike`, `IsNotLike`| +| `REGEX` |`Regex`, `MatchesRegex`, `Matches`| +| `STARTING_WITH` |`StartingWith`, `IsStartingWith`, `StartsWith`| +| `TRUE` |`True`, `IsTrue`| +| `WITHIN` |`Within`, `IsWithin`| + +除了过滤谓词外,还支持以下修饰符列表: + +| Keyword |说明| +|----------------------------------|---------------------------------------------------------------------------------------------------------------------| +| `IgnoreCase`, `IgnoringCase` |与谓词关键字一起使用,用于不区分大小写的比较。| +|`AllIgnoreCase`, `AllIgnoringCase`|忽略所有合适属性的大小写。在查询方法谓词中的某个地方使用。| +| `OrderBy…` |指定一个静态排序顺序,后面是属性路径和方向(例如`OrderByFirstnameAscLastnameDesc`)。| + +## 附录 D:存储库查询返回类型 + +### 支持的查询返回类型 + +下表列出了 Spring 数据存储库通常支持的返回类型。但是,请参阅特定于商店的文档,以获得所支持的返回类型的确切列表,因为此处列出的某些类型在特定商店中可能不受支持。 + +| |地理空间类型(例如`GeoResult`、`GeoResults`和`GeoPage`)仅适用于支持地理空间查询的数据存储。
一些存储模块可能会定义自己的结果包装器类型。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| Return type |说明| +|------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `void` |表示没有返回值。| +| Primitives |Java 原语。| +| Wrapper types |Java 包装器类型。| +| `T` |一个独特的实体。期望查询方法最多返回一个结果。如果没有找到结果,则返回`null`。多个结果触发`IncorrectResultSizeDataAccessException`。| +| `Iterator` |an`Iterator`。| +| `Collection` |a`Collection`。| +| `List` |a`List`。| +| `Optional` |一个 Java8 或番石榴`Optional`。期望查询方法最多返回一个结果。如果没有找到结果,则返回`Optional.empty()`或`Optional.absent()`。多个结果触发`IncorrectResultSizeDataAccessException`。| +| `Option` |要么是 scala,要么是 vavr`Option`类型。在语义上与前面描述的 Java8 的`Optional`相同。| +| `Stream` |a java8`Stream`。| +| `Streamable` |这是`Iterable`的一个方便的扩展,Directy 将方法公开于流、映射和筛选结果、将它们连接等。| +|Types that implement `Streamable` and take a `Streamable` constructor or factory method argument|公开构造函数或`….of(…)`/`….valueof(…)` 工厂方法的类型,以`Streamable`为参数。详见[返回自定义的可刷新包装器类型](#repositories.collections-and-iterables.streamable-wrapper)。| +| Vavr `Seq`, `List`, `Map`, `Set` |VAVR 集合类型。详见[对 VAVR 收藏的支持](#repositories.collections-and-iterables.vavr)。| +| `Future` |a`Future`。期望对方法进行`@Async`注释,并且需要启用 Spring 的异步方法执行功能。| +| `CompletableFuture` |a java8`CompletableFuture`。期望对方法进行`@Async`注释,并且需要启用 Spring 的异步方法执行功能。| +| `ListenableFuture` |a`org.springframework.util.concurrent.ListenableFuture`。期望对方法进行`@Async`注释,并且需要启用 Spring 的异步方法执行功能。| +| `Slice` |表示是否有更多可用数据的大小的数据块。需要`Pageable`方法参数。| +| `Page` |a`Slice`带有附加信息,如结果的总数。需要`Pageable`方法参数。| +| `GeoResult` |带有附加信息的结果条目,例如到参考位置的距离。| +| `GeoResults` |带有附加信息的`GeoResult`列表,例如到参考位置的平均距离。| +| `GeoPage` |a`Page`与`GeoResult`等参考位置的平均距离。| +| `Mono` |一个项目反应器`Mono`使用反应性存储库发射零或一个元素。期望查询方法最多返回一个结果。如果没有找到结果,则返回`Mono.empty()`。多个结果触发`IncorrectResultSizeDataAccessException`。| +| `Flux` |一个项目反应堆`Flux`使用反应性存储库发射零、一个或多个元素。返回`Flux`的查询也可以发出无限数量的元素。| +| `Single` |一个 RxJava`Single`使用反应库发射单个元素。期望查询方法最多返回一个结果。如果没有找到结果,则返回`Mono.empty()`。多个结果会触发`IncorrectResultSizeDataAccessException`。| +| `Maybe` |RxJava`Maybe`使用反应库发射零或一个元素。期望查询方法最多返回一个结果。如果没有找到结果,则返回`Mono.empty()`。多个结果会触发`IncorrectResultSizeDataAccessException`。| +| `Flowable` |RxJava`Flowable`使用反应库发射零、一个或多个元素。返回`Flowable`的查询也可以发出无限数量的元素。| + +--- + +[1](#_footnoteref_1)。参见[XML 配置](#repositories.create-instances.spring) + diff --git a/docs/spring-framework/README.md b/docs/spring-framework/README.md new file mode 100644 index 0000000000000000000000000000000000000000..f963b401cd4dab39ea9419e4d4a74ba83fee8037 --- /dev/null +++ b/docs/spring-framework/README.md @@ -0,0 +1 @@ +# Spring 框架 diff --git a/docs/spring-framework/core.md b/docs/spring-framework/core.md new file mode 100644 index 0000000000000000000000000000000000000000..61b39eb18f02aa53cdf81876adde2c93b4d493b1 --- /dev/null +++ b/docs/spring-framework/core.md @@ -0,0 +1,18179 @@ +# 核心技术 + +参考文档的这一部分涵盖了对 Spring 框架绝对必要的所有技术。 + +其中最重要的是 Spring 框架的控制反转容器。在对 Spring 框架的 IOC 容器进行了全面的介绍之后,还对 Spring 的面向方面编程( AOP)技术进行了全面的介绍。 Spring 框架有其自己的 AOP 框架,该框架在概念上易于理解,并且成功地解决了 爪哇 Enterprise 编程中 AOP 需求的 80% 的甜蜜点。 + +还提供了对 Spring 与 AspectJ 的集成的覆盖(就功能而言,目前是最丰富的——当然也是 爪哇 Enterprise 领域中最成熟的 AOP 实现)。 + +## 1. IOC 容器 + +本章介绍 Spring 的控制反转容器。 + +### 1.1. Spring IOC 容器和 bean 介绍 + +这一章涵盖了 Spring 倒置控制原则的框架实现。IoC 也被称为依赖注入。在这个过程中,对象仅通过构造函数参数、工厂方法的参数或在对象实例被构造或从工厂方法返回后在对象实例上设置的属性来定义它们的依赖关系(即它们所使用的其他对象)。然后,容器在创建 Bean 时注入这些依赖项。这个过程从根本上讲是 Bean 本身的逆过程(因此称为控制的逆过程),通过使用类的直接构造或诸如服务定位器模式的机制来控制其依赖关系的实例化或位置。 + +`org.springframework.beans`和`org.springframework.context`包是 Spring Framework 的 IOC 容器的基础。[`BeanFactory`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/factory/BeanFactory.html)接口提供了一种能够管理任何类型对象的高级配置机制。[` 应用上下文’](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/ApplicationContext.html)是`BeanFactory`的子接口。它补充道: + +* 与 Spring AOP 的特性更容易集成 + +* 消息资源处理(用于国际化) + +* 事件发布 + +* 应用程序层特定的上下文,例如在 Web 应用程序中使用的`WebApplicationContext`。 + +简而言之,`BeanFactory`提供了配置框架和基本功能,而`ApplicationContext`增加了更多特定于 Enterprise 的功能。`ApplicationContext`是`BeanFactory`的完整超集,在本章 Spring 的 IOC 容器的描述中专门使用。有关使用`BeanFactory`而不是`ApplicationContext,`的更多信息,请参见[The `BeanFactory`](#beans-beanfactory)。 + +在 Spring 中,构成应用程序主干并由 Spring IOC 容器管理的对象称为 bean。 Bean 是由 Spring IOC 容器实例化、组装和管理的对象。否则, Bean 只是应用程序中的许多对象之一。bean 和它们之间的依赖关系反映在容器使用的配置元数据中。 + +### 1.2.集装箱概述 + +`org.springframework.context.ApplicationContext`接口表示 Spring IOC 容器,并负责实例化、配置和组装 bean。容器通过读取配置元数据获得有关要实例化、配置和组装哪些对象的指令。配置元数据以 XML、爪哇 注释或 爪哇 代码表示。它允许你表达组成应用程序的对象,以及这些对象之间丰富的相互依赖关系。 + +Spring 提供了`ApplicationContext`接口的几种实现方式。在独立应用程序中,通常创建[“ClassPathXMLApplicationContext”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/support/ClassPathXmlApplicationContext.html)或[“FilesyStemXMLApplicationContext”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/support/FileSystemXmlApplicationContext.html)的实例。尽管 XML 一直是定义配置元数据的传统格式,但你可以通过提供少量的 XML 配置来指示容器使用 爪哇 注释或代码作为元数据格式,从而声明性地支持这些附加的元数据格式。 + +在大多数应用程序场景中,不需要显式的用户代码来实例化 Spring IOC 容器的一个或多个实例。例如,在一个 Web 应用程序场景中,应用程序的`web.xml`文件中的一个简单的 8 行样板 Web 描述符 XML 通常就足够了(参见[用于 Web 应用程序的方便的应用程序上下文实例化](#context-create))。如果使用`name`(Eclipse 驱动的开发环境),只需单击几下鼠标或击键,就可以轻松地创建这个样板配置。 + +下图显示了 Spring 如何工作的高级视图。你的应用程序类与配置元数据相结合,这样,在创建和初始化`ApplicationContext`之后,你就拥有了一个完全配置和可执行的系统或应用程序。 + +![container magic](images/container-magic.png) + +图 1。 Spring IOC 容器 + +#### 1.2.1.配置元数据 + +如上图所示, Spring IOC 容器使用一种形式的配置元数据。这个配置元数据表示你作为应用程序开发人员如何告诉 Spring 容器实例化、配置和组装应用程序中的对象。 + +传统上,配置元数据是以简单直观的 XML 格式提供的,本章的大部分内容都使用这种格式来传达 Spring IoC 容器的关键概念和特性。 + +| |基于 XML 的元数据并不是配置元数据的唯一允许的形式。
Spring IOC 容器本身与
配置元数据实际使用的格式完全解耦。如今,许多开发人员为他们的 Spring 应用程序选择[基于 爪哇 的配置](#beans-java)。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +有关在 Spring 容器中使用其他形式的元数据的信息,请参见: + +* [基于注释的配置](#beans-annotation-config): Spring 2.5 引入了对基于注释的配置元数据的支持。 + +* [基于 爪哇 的配置](#beans-java):从 Spring 3.0 开始, Spring 爪哇Config 项目提供的许多特性成为了核心 Spring 框架的一部分。因此,你可以使用 爪哇 而不是 XML 文件来定义应用程序类外部的 bean。要使用这些新功能,请参见[@configuration](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Configuration.html)、[`@Bean`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Bean.html)、[`@Import`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Import.html)和[`@DependsOn`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/DependsOn.html)注释。 + +Spring 配置包括容器必须管理的至少一个且通常不止一个定义。基于 XML 的配置元数据将这些 bean 配置为顶级``元素中的``元素。爪哇 配置通常在`@Configuration`类中使用`@Bean`-带注释的方法。 + +Bean 这些定义对应于构成你的应用程序的实际对象。通常,你定义服务层对象、数据访问对象、表示对象(如 Struts实例)、基础设施对象(如 Hibernate `SessionFactory’、JMS)等等。通常,人们不会在容器中配置细粒度的域对象,因为通常是 DAO 和业务逻辑负责创建和加载域对象。但是,你可以使用 Spring 与 AspectJ 的集成来配置在 IOC 容器控制范围之外创建的对象。见[Using AspectJ to dependency-inject domain objects with Spring](#aop-atconfigurable)。 + +下面的示例展示了基于 XML 的配置元数据的基本结构: + +``` + + + + (1) (2) + + + + + + + + + + +``` + +|**1**|`id`属性是一个字符串,用于标识单独的 Bean 定义。| +|-----|----------------------------------------------------------------------------------------------| +|**2**|`class`属性定义 Bean 的类型,并使用完全限定的
类名。| + +`id`属性的值是指协作对象。本例中没有显示用于引用协作对象的 XML。有关更多信息,请参见[Dependencies](#beans-dependencies)。 + +#### 1.2.2.实例化容器 + +提供给`ApplicationContext`构造函数的位置路径是资源字符串,这些资源字符串允许容器从各种外部资源加载配置元数据,例如本地文件系统、爪哇`CLASSPATH`,等等。 + +爪哇 + +``` +ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml"); +``` + +Kotlin + +``` +val context = ClassPathXmlApplicationContext("services.xml", "daos.xml") +``` + +| |在了解了 Spring 的 IoC 容器之后,你可能想更多地了解 Spring 的 `resource`abstraction(如[Resources](#resources)中所述),它提供了一种方便的`adminEmails`机制,用于从在 URI 语法中定义的位置读取 InputStream。特别是,“资源”路径用于构造应用程序上下文,如[应用程序上下文和资源路径](#resources-app-ctx)中所述。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +下面的示例显示了服务层对象`(services.xml)`配置文件: + +``` + + + + + + + + + + + + + + +``` + +下面的示例显示了数据访问对象`daos.xml`文件: + +``` + + + + + + + + + + + + + + +``` + +在前面的示例中,服务层由`PetStoreServiceImpl`类和`JpaAccountDao`和`JpaItemDao`类型的两个数据访问对象组成(基于 JPA 对象关系映射标准)。`property name`元素指的是 爪哇Bean 属性的名称,而[@configuration](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Configuration.html)元素指的是另一个 Bean 定义的名称。`id`和`ref`元素之间的这种链接表达了协作对象之间的依赖关系。有关配置对象依赖项的详细信息,请参见[Dependencies](#beans-dependencies)。 + +##### 编写基于 XML 的配置元数据 + +Bean 定义跨越多个 XML 文件是有用的。通常,每个单独的 XML 配置文件都表示体系结构中的逻辑层或模块。 + +你可以使用应用程序上下文构造函数从所有这些 XML 片段加载 Bean 定义。这个构造函数接受多个`Resource`位置,如[上一节](#beans-factory-instantiation)中所示。或者,使用``元素的一个或多个出现来从另一个或多个文件加载 Bean 定义。下面的示例展示了如何做到这一点: + +``` + + + + + + + + +``` + +在前面的示例中,外部 Bean 定义是从三个文件加载的:`services.xml`、`messageSource.xml`和`themeSource.xml`。所有的位置路径都与执行导入的定义文件相关,因此`services.xml`必须与执行导入的文件位于同一目录或 Classpath 位置,而 `messagesource.xml` 和`themeSource.xml`必须位于导入文件位置下方的`resources`位置。正如你所看到的,前导斜杠被忽略了。然而,鉴于这些路径是相对的,最好的形式是根本不使用斜杠。根据 Spring 模式,要导入的文件的内容,包括顶层``元素,必须是有效的 XML Bean 定义。 + +| |使用
relative“../”路径引用父目录中的文件是可能的,但不推荐。这样做会在当前
应用程序之外的文件上创建一个依赖项。特别是,对于`classpath:`URL(对于
示例,`classpath:../services.xml`),不建议使用该引用,在该示例中,运行时解析过程选择
“最近的”根目录,然后查看其父目录。 Classpath
配置更改可能会导致选择不同的、不正确的目录。

你总是可以使用完全限定的资源位置,而不是相对路径:对于`file:C:/config/services.xml`示例,`file:C:/config/services.xml`或`ApplicationContext`。但是,请注意,你正在将应用程序的配置耦合到特定的绝对位置
。对于这样的绝对[Bean Scopes](#beans-factory-scopes)位置,通常最好是保持间接的——例如,通过在运行时针对 JVM
系统属性解析的“${…}”占位符。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +名称空间本身提供了导入指令功能。除了普通的 Bean 定义之外,在 Spring 提供的一系列 XML 命名空间中还可以获得更多的配置特性——例如,`context`和`util`命名空间。 + +##### Groovy Bean 定义 DSL + +作为外部化配置元数据的另一个示例, Bean 定义也可以在 Spring 的 Groovy Bean 定义 DSL 中表示,正如 Grails 框架中所知的那样。通常,这样的配置存在于一个“.groovy”文件中,其结构如以下示例所示: + +``` +beans { + dataSource(BasicDataSource) { + driverClassName = "org.hsqldb.jdbcDriver" + url = "jdbc:hsqldb:mem:grailsDB" + username = "sa" + password = "" + settings = [mynew:"setting"] + } + sessionFactory(SessionFactory) { + dataSource = dataSource + } + myService(MyService) { + nestedBean = { AnotherBean bean -> + dataSource = dataSource + } + } +} +``` + +这种配置风格在很大程度上等同于 XML Bean 定义,甚至支持 Spring 的 XML 配置名称空间。它还允许通过`importBeans`指令导入 XML Bean 定义文件。 + +#### 1.2.3.使用容器 + +`ApplicationContext`是高级工厂的接口,该工厂能够维护不同 bean 及其依赖项的注册表。通过使用方法 `t getBean(字符串名称,类RequiredType)`,你可以检索 bean 的实例。 + +`ApplicationContext`允许你读取 Bean 定义并访问它们,如下例所示: + +爪哇 + +``` +// create and configure beans +ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml"); + +// retrieve configured instance +PetStoreService service = context.getBean("petStore", PetStoreService.class); + +// use configured instance +List userList = service.getUsernameList(); +``` + +Kotlin + +``` +import org.springframework.beans.factory.getBean + +// create and configure beans +val context = ClassPathXmlApplicationContext("services.xml", "daos.xml") + +// retrieve configured instance +val service = context.getBean("petStore") + +// use configured instance +var userList = service.getUsernameList() +``` + +在 Groovy 配置下,引导看起来非常相似。它有一个不同的上下文实现类,它是 Groovy 感知的(但也理解 XML Bean 定义)。下面的示例展示了 Groovy 配置: + +爪哇 + +``` +ApplicationContext context = new GenericGroovyApplicationContext("services.groovy", "daos.groovy"); +``` + +Kotlin + +``` +val context = GenericGroovyApplicationContext("services.groovy", "daos.groovy") +``` + +最灵活的变体是结合阅读器委托的`GenericApplicationContext`,例如,对于 XML 文件,使用`XmlBeanDefinitionReader`,如下例所示: + +爪哇 + +``` +GenericApplicationContext context = new GenericApplicationContext(); +new XmlBeanDefinitionReader(context).loadBeanDefinitions("services.xml", "daos.xml"); +context.refresh(); +``` + +Kotlin + +``` +val context = GenericApplicationContext() +XmlBeanDefinitionReader(context).loadBeanDefinitions("services.xml", "daos.xml") +context.refresh() +``` + +对于 Groovy 文件,也可以使用`GroovyBeanDefinitionReader`,如下例所示: + +爪哇 + +``` +GenericApplicationContext context = new GenericApplicationContext(); +new GroovyBeanDefinitionReader(context).loadBeanDefinitions("services.groovy", "daos.groovy"); +context.refresh(); +``` + +Kotlin + +``` +val context = GenericApplicationContext() +GroovyBeanDefinitionReader(context).loadBeanDefinitions("services.groovy", "daos.groovy") +context.refresh() +``` + +你可以在相同的`ApplicationContext`上混合和匹配这样的读取器委托,读取来自不同配置源的 Bean 定义。 + +然后,你可以使用`getBean`来检索你的 bean 实例。`ApplicationContext`接口有一些其他方法来检索 bean,但是,理想情况下,应用程序代码不应该使用它们。实际上,你的应用程序代码应该完全没有对“getBean()”方法的调用,因此完全没有对 Spring API 的依赖。例如, Spring 与 Web Frameworks 的集成为各种 Web Framework 组件(例如控制器和 JSF 管理的 bean)提供了依赖注入,允许你通过元数据(例如自动连接注释)声明对特定 Bean 的依赖。 + +### 1.3. Bean 概述 + +Spring IOC 容器管理一个或多个 bean。这些 bean 是使用你提供给容器的配置元数据创建的(例如,以 XML`` 定义的形式)。 + +在容器本身内,这些 Bean 定义被表示为`BeanDefinition`对象,其中包含(除其他信息外)以下元数据: + +* 包限定类名称:通常是正在定义的 Bean 的实际实现类。 + +* Bean 行为配置元素,其中说明 Bean 在容器中应该如何表现(作用域、生命周期回调,等等)。 + +* 引用 Bean 完成其工作所需的其他 bean。这些引用也被称为协作者或依赖项。 + +* 在新创建的对象中设置的其他配置设置——例如,池的大小限制或在管理连接池的 Bean 中使用的连接数量。 + +该元数据转换为一组属性,这些属性构成了每个 Bean 定义。下表描述了这些属性: + +| Property |解释在…| +|------------------------|---------------------------------------------------------------------| +| Class |[实例化豆类](#beans-factory-class)| +| Name |[Naming Beans](#beans-beanname)| +| Scope |[Bean Scopes](#beans-factory-scopes)| +| Constructor arguments |[依赖注入](#beans-factory-collaborators)| +| Properties |[依赖注入](#beans-factory-collaborators)| +| Autowiring mode |[自动布线合作者](#beans-factory-autowire)| +|Lazy initialization mode|[惰性初始化的 bean](#beans-factory-lazy-init)| +| Initialization method |[初始化回调](#beans-factory-lifecycle-initializingbean)| +| Destruction method |[销毁回调](#beans-factory-lifecycle-disposablebean)| + +Bean 定义包含关于如何创建特定 Bean 的信息,此外,[初始化回调](#beans-factory-lifecycle-initializingbean)实现还允许注册在容器之外(由用户)创建的现有对象。这是通过通过`getBeanFactory()`方法访问应用程序上下文的 BeanFactory 来完成的,该方法返回 BeanFactory`DefaultListableBeanFactory`实现。`DefaultListableBeanFactory`通过`registerSingleton(..)`和“registerBeanDefinition”方法支持这种注册。然而,典型的应用程序仅使用通过常规 Bean 定义元数据定义的 bean。 + +| |Bean 元数据和手动提供的单例实例需要尽可能早地
进行注册,以便容器在自动布线
和其他内省步骤期间对它们进行适当的推理。虽然在一定程度上支持重写现有元数据和现有的
单例实例,但官方不支持在
运行时注册新的 bean(与工厂的实时访问同时进行),并且
可能导致并发访问异常、 Bean 容器中的不一致状态,或者两者兼而有之。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.3.1.命名 bean + +每个 Bean 具有一个或多个标识符。在承载 Bean 的容器中,这些标识符必须是唯一的。 Bean 通常只有一个标识符。但是,如果它需要多个,那么额外的一个可以被视为别名。 + +在基于 XML 的配置元数据中,使用`id`属性、`name`属性或两者来指定 Bean 标识符。`id`属性允许你精确地指定一个 ID。传统上,这些名称是字母数字(“MyBean”、“Someservice”等),但它们也可以包含特殊字符。如果要为 Bean 引入其他别名,还可以在`name`属性中指定它们,并用逗号、分号或空格分隔。作为一个历史记录,在 Spring 3.1 之前的版本中,`id`属性被定义为`xsd:ID`类型,该类型限制了可能的字符。在 3.1 中,它被定义为`xsd:string`类型。注意, Bean `id`唯一性仍然由容器强制执行,尽管不再由 XML 解析器执行。 + +你不需要为 Bean 提供`name`或`id`。如果没有显式地提供 `name’或`id`,则容器将为该 Bean 生成唯一的名称。但是,如果你希望通过使用`ref`元素或服务定位器样式查找来按名称引用该 Bean,则必须提供一个名称。不提供名称的动机与使用[inner beans](#beans-inner-beans)和[inner beans](#beans-inner-beans)有关。 + +Bean 命名约定 + +约定是在命名 bean 时对实例字段名称使用标准 爪哇 约定。也就是说, Bean 名称以小写字母开头,并从那里以驼峰式开头。这类名称的例子包括`accountManager`、`accountService’、`userDao`、`loginController`,等等。 + +始终如一地命名 bean 会使你的配置更易于阅读和理解。此外,如果你使用 Spring AOP,那么在将建议应用到一组与名称相关的 bean 时,它会有很大帮助。 + +| |使用 Classpath 中的组件扫描, Spring 为未命名的
组件生成 Bean 名称,遵循前面描述的规则:本质上,采用简单的类名
并将其初始字符转换为小写字母。然而,在(不寻常的)特殊`id`情况下,当有多个字符并且第一个和第二个字符
都是大写时,原始的外壳得到保留。这些规则与
定义的`java.beans.Introspector.decapitalize`( Spring 在此使用)相同。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 在 Bean 定义之外别名 Bean + +在 Bean 定义本身中,可以通过使用由`id`属性指定的最多一个名称和`name`属性中的任意数量的其他名称的组合,为 Bean 提供多个名称。这些名称可以是相同的 Bean 的等价别名,并且在某些情况下是有用的,例如通过使用特定于该组件本身的 Bean 名称,让应用程序中的每个组件引用公共依赖项。 + +然而,在 Bean 实际定义的地方指定所有别名并不总是足够的。有时需要为在别处定义的 Bean 引入别名。在大型系统中,配置通常在每个子系统之间进行分配,每个子系统都有自己的一组对象定义,这种情况很常见。在基于 XML 的配置元数据中,可以使用``元素来实现这一点。下面的示例展示了如何做到这一点: + +``` + +``` + +在这种情况下, Bean(在相同的容器中)名为的还可以在使用这种别名定义之后,被称为。 + +例如,子系统 A 的配置元数据可以通过`subsystemA-dataSource`的名称引用数据源。子系统 B 的配置元数据可以引用名为`subsystemB-dataSource`的数据源。在组成使用这两个子系统的主应用程序时,主应用程序以`myApp-dataSource`的名称引用数据源。要让这三个名称都引用同一个对象,可以向配置元数据添加以下别名定义: + +``` + + +``` + +现在,每个组件和主应用程序都可以通过一个唯一的名称来引用数据源,并保证不会与任何其他定义冲突(有效地创建一个名称空间),但是它们引用的是相同的 Bean。 + +爪哇 配置 + +如果使用 爪哇Configuration,可以使用`@Bean`注释来提供别名。详见[Using the `@Bean` Annotation](#beans-java-bean-annotation)。 + +#### 1.3.2.实例化豆类 + +Bean 定义本质上是用于创建一个或多个对象的配方。容器在被请求时查看命名 Bean 的配方,并使用由该 Bean 定义封装的配置元数据来创建(或获取)实际对象。 + +如果使用基于 XML 的配置元数据,则可以在``元素的`class`属性中指定要实例化的对象类型(或类)。这个 `class` 属性(在内部是`Class`实例上的`Class`属性)通常是强制的。(有关异常,请参见[通过使用实例工厂方法实现实例化](#beans-factory-class-instance-factory-method)和[Bean Definition Inheritance](#beans-child-bean-definitions)。)你可以通过以下两种方式之一使用`Class`属性: + +* 通常,在容器本身通过反射地调用其构造函数直接创建 Bean 的情况下,指定要构造的 Bean 类,这在某种程度上相当于使用`new`操作符的 爪哇 代码。 + +* 要指定包含用于创建对象的`static`工厂方法的实际类,在不太常见的情况下,容器调用类上的 ` 静态’工厂方法来创建 Bean。调用`static`工厂方法返回的对象类型可以是同一个类,也可以完全是另一个类。 + +嵌套类名 + +如果你希望为嵌套类配置 Bean 定义,那么你可以使用嵌套类的二进制名称或源名。 + +例如,如果在`com.example`包中有一个名为`SomeThing`的类,而这个`SomeThing`类有一个名为`static`的嵌套类,它们可以用美元符号或点分隔。因此,在 Bean 定义中,`class`属性的值将是`com.example.SomeThing$OtherThing`或 `com.example.something.otherthing’。 + +##### 使用构造函数实例化 + +当通过构造函数方法创建 Bean 时,所有普通类都可由 Spring 使用并与 Spring 兼容。也就是说,正在开发的类不需要实现任何特定的接口,也不需要以特定的方式进行编码。只需指定 Bean 类就足够了。然而,根据你为该特定 Bean 使用的 IOC 类型,你可能需要一个默认(空)构造函数。 + +Spring IOC 容器实际上可以管理你希望它管理的任何类。它不仅限于管理真正的 爪哇Beans。 Spring 大多数用户更喜欢实际的 爪哇Bean,它只有一个默认的(无参数的)构造函数,以及按照容器中的属性建模的适当的 setter 和 getter。你还可以在你的容器中有更多奇异的非 Bean 样式类。例如,如果你需要使用一个绝对不遵守 爪哇Bean 规范的遗留连接池, Spring 也可以对其进行管理。 + +使用基于 XML 的配置元数据,你可以按以下方式指定你的 Bean 类: + +``` + + + +``` + +有关在构造对象之后向构造函数提供参数和设置对象实例属性的机制的详细信息,请参见[注入依赖项](#beans-factory-collaborators)。 + +##### 使用静态工厂方法的实例化 + +在定义使用静态工厂方法创建的 Bean 时,使用`class`属性指定包含`static`工厂方法和一个名为`factory-method`的属性的类,以指定工厂方法本身的名称。你应该能够调用这个方法(使用可选参数,如后面所述)并返回一个活动对象,随后将其视为通过构造函数创建的对象。这种 Bean 定义的一种用途是在遗留代码中调用`static`工厂。 + +下面的 Bean 定义指定通过调用工厂方法来创建 Bean。该定义没有指定返回对象的类型(类),只指定了包含工厂方法的类。在本例中,`createInstance()`方法必须是静态方法。下面的示例展示了如何指定工厂方法: + +``` + +``` + +下面的示例展示了一个将与前面的 Bean 定义一起工作的类: + +爪哇 + +``` +public class ClientService { + private static ClientService clientService = new ClientService(); + private ClientService() {} + + public static ClientService createInstance() { + return clientService; + } +} +``` + +Kotlin + +``` +class ClientService private constructor() { + companion object { + private val clientService = ClientService() + fun createInstance() = clientService + } +} +``` + +有关向工厂方法提供(可选的)参数并在从工厂返回对象后设置对象实例属性的机制的详细信息,请参见[详细介绍依赖关系和配置](#beans-factory-properties-detailed)。 + +##### 通过使用实例工厂方法实现实例化 + +与通过[静态工厂法](#beans-factory-class-static-factory-method)的实例化类似,使用实例工厂方法的实例化从容器调用现有 Bean 的非静态方法来创建新的 Bean。要使用此机制,将`class`属性保留为空,并在`factory-bean`属性中,指定当前(或父容器或祖先容器)中的 Bean 的名称,该容器包含将被调用以创建对象的实例方法。使用`factory-method`属性设置工厂方法本身的名称。下面的示例显示了如何配置这样的 Bean: + +``` + + + + + + + +``` + +下面的示例展示了相应的类: + +爪哇 + +``` +public class DefaultServiceLocator { + + private static ClientService clientService = new ClientServiceImpl(); + + public ClientService createClientServiceInstance() { + return clientService; + } +} +``` + +Kotlin + +``` +class DefaultServiceLocator { + companion object { + private val clientService = ClientServiceImpl() + } + fun createClientServiceInstance(): ClientService { + return clientService + } +} +``` + +一个工厂类也可以包含多个工厂方法,如下例所示: + +``` + + + + + + + +``` + +下面的示例展示了相应的类: + +爪哇 + +``` +public class DefaultServiceLocator { + + private static ClientService clientService = new ClientServiceImpl(); + + private static AccountService accountService = new AccountServiceImpl(); + + public ClientService createClientServiceInstance() { + return clientService; + } + + public AccountService createAccountServiceInstance() { + return accountService; + } +} +``` + +Kotlin + +``` +class DefaultServiceLocator { + companion object { + private val clientService = ClientServiceImpl() + private val accountService = AccountServiceImpl() + } + + fun createClientServiceInstance(): ClientService { + return clientService + } + + fun createAccountServiceInstance(): AccountService { + return accountService + } +} +``` + +这种方法表明, Bean 工厂本身可以通过依赖注入进行管理和配置。见[详细介绍依赖关系和配置](#beans-factory-properties-detailed)。 + +| |在 Spring 文档中,“factory Bean”是指在
Spring 容器中配置并通过[instance](#beans-factory-class-instance-factory-method)或[static](#beans-factory-class-static-factory-method)工厂方法创建对象的 Bean。相比之下,“FactoryBean”(请注意大写)是指特定于 Spring 的[`FactoryBean`](#beans-factory-extension-factorybean)实现类。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 确定 Bean 的运行时类型 + +Bean 特定的运行时类型是不可平凡地确定的。 Bean 元数据定义中的指定类只是一个初始类引用,可能与声明的工厂方法结合在一起,或者是一个`FactoryBean`类,这可能导致 Bean 的不同运行时类型,或者在实例级工厂方法的情况下根本没有设置(而是通过指定的`factory-bean`名称进行解析)。此外, AOP 代理可以用基于接口的代理包装 Bean 实例,该代理有限地暴露目标 Bean 的实际类型(仅包括其实现的接口)。 + +查找特定 Bean 的实际运行时类型的推荐方法是对指定的 Bean 名称进行`BeanFactory.getType`调用。这将考虑上述所有情况,并返回`BeanFactory.getBean`调用将为相同的 Bean 名称返回的对象类型。 + +### 1.4.依赖关系 + +典型的 Enterprise 应用程序不包括单个对象(或者在 Spring 的说法中是 Bean)。即使是最简单的应用程序也有几个对象一起工作,以呈现最终用户认为是一致的应用程序。下一节将解释如何从定义多个 Bean 单独的定义发展到一个完全实现的应用程序,在该应用程序中,对象协作以实现一个目标。 + +#### 1.4.1.依赖注入 + +依赖注入是一个过程,在这个过程中,对象仅通过构造函数参数、工厂方法的参数或在对象实例被构造或从工厂方法返回后在对象实例上设置的属性来定义它们的依赖关系(即它们所使用的其他对象)。然后,容器在创建 Bean 时注入这些依赖项。这个过程从根本上讲是 Bean 本身的逆过程(因此称为控制的逆过程),通过使用类的直接构造或服务定位器模式来控制其依赖项的实例化或位置。 + +使用 DI 原则的代码更干净,当对象提供了它们的依赖关系时,解耦更有效。对象不会查找其依赖项,也不知道依赖项的位置或类。结果,类变得更容易测试,特别是当依赖于接口或抽象基类时,这允许在单元测试中使用存根或模拟实现。 + +DI 有两个主要的变体:[基于构造函数的依赖注入](#beans-constructor-injection)和[基于 setter 的依赖注入](#beans-setter-injection)。 + +##### 基于构造函数的依赖注入 + +基于构造函数的 DI 是由容器调用具有多个参数的构造函数来完成的,每个参数表示一个依赖项。调用带有特定参数的`static`工厂方法来构造 Bean 几乎是等效的,并且此讨论将参数处理为构造函数和`static`工厂方法。下面的示例展示了一个只能通过构造函数注入进行依赖注入的类: + +爪哇 + +``` +public class SimpleMovieLister { + + // the SimpleMovieLister has a dependency on a MovieFinder + private final MovieFinder movieFinder; + + // a constructor so that the Spring container can inject a MovieFinder + public SimpleMovieLister(MovieFinder movieFinder) { + this.movieFinder = movieFinder; + } + + // business logic that actually uses the injected MovieFinder is omitted... +} +``` + +Kotlin + +``` +// a constructor so that the Spring container can inject a MovieFinder +class SimpleMovieLister(private val movieFinder: MovieFinder) { + // business logic that actually uses the injected MovieFinder is omitted... +} +``` + +请注意,这门课没有什么特别之处。它是一个 POJO,不依赖于特定于容器的接口、基类或注释。 + +###### 构造函数参数解析 + +构造函数的参数解析匹配是通过使用参数的类型来实现的。如果在 Bean 定义的构造函数参数中不存在潜在的歧义,则在 Bean 定义中定义构造函数参数的顺序是在 Bean 被实例化时将这些参数提供给适当的构造函数的顺序。考虑以下类: + +爪哇 + +``` +package x.y; + +public class ThingOne { + + public ThingOne(ThingTwo thingTwo, ThingThree thingThree) { + // ... + } +} +``` + +Kotlin + +``` +package x.y + +class ThingOne(thingTwo: ThingTwo, thingThree: ThingThree) +``` + +假设`ThingTwo`和`ThingThree`类之间没有继承关系,则不存在潜在的歧义。因此,下面的配置工作正常,并且你不需要在 `` 元素中显式地指定构造函数参数索引或类型。 + +``` + + + + + + + + + + +``` + +当引用另一个 Bean 时,类型是已知的,并且可以发生匹配(与前面的示例一样)。当使用简单的类型时,例如 `true`, Spring 无法确定该值的类型,因此在没有帮助的情况下无法按类型进行匹配。考虑以下类: + +爪哇 + +``` +package examples; + +public class ExampleBean { + + // Number of years to calculate the Ultimate Answer + private final int years; + + // The Answer to Life, the Universe, and Everything + private final String ultimateAnswer; + + public ExampleBean(int years, String ultimateAnswer) { + this.years = years; + this.ultimateAnswer = ultimateAnswer; + } +} +``` + +Kotlin + +``` +package examples + +class ExampleBean( + private val years: Int, // Number of years to calculate the Ultimate Answer + private val ultimateAnswer: String // The Answer to Life, the Universe, and Everything +) +``` + +[]()构造函数参数类型匹配 + +在前面的场景中,如果你使用`type`属性显式地指定构造函数参数的类型,则容器可以使用与简单类型匹配的类型,如下例所示: + +``` + + + + +``` + +[]()构造函数参数索引 + +可以使用`index`属性显式指定构造函数参数的索引,如下例所示: + +``` + + + + +``` + +除了解决多个简单值的歧义之外,在构造函数具有两个相同类型的参数的情况下,指定索引还可以解决歧义。 + +| |该索引是以 0 为基础的。| +|---|---------------------| + +[]()构造函数参数 Name + +还可以使用构造函数参数名称来消歧,如下例所示: + +``` + + + + +``` + +请记住,要使这项工作开箱即用,你的代码必须使用启用的调试标志进行编译,以便 Spring 可以从构造函数中查找参数名称。如果不能或不想使用 Debug 标志编译代码,可以使用[@constructorProperties](https://download.oracle.com/javase/8/docs/api/java/beans/ConstructorProperties.html)JDK 注释显式地命名构造函数参数。然后,示例类必须如下所示: + +爪哇 + +``` +package examples; + +public class ExampleBean { + + // Fields omitted + + @ConstructorProperties({"years", "ultimateAnswer"}) + public ExampleBean(int years, String ultimateAnswer) { + this.years = years; + this.ultimateAnswer = ultimateAnswer; + } +} +``` + +Kotlin + +``` +package examples + +class ExampleBean +@ConstructorProperties("years", "ultimateAnswer") +constructor(val years: Int, val ultimateAnswer: String) +``` + +##### 基于 setter 的依赖注入 + +基于 setter 的 DI 是在调用一个无参数构造函数或一个无参数`static`工厂方法实例化你的 Bean 之后,由容器调用 bean 上的 setter 方法来完成的。 + +下面的示例展示了一个只能通过使用纯 setter 注入进行依赖注入的类。这个类是传统的 爪哇。它是一个 POJO,不依赖于特定于容器的接口、基类或注释。 + +Java + +``` +public class SimpleMovieLister { + + // the SimpleMovieLister has a dependency on the MovieFinder + private MovieFinder movieFinder; + + // a setter method so that the Spring container can inject a MovieFinder + public void setMovieFinder(MovieFinder movieFinder) { + this.movieFinder = movieFinder; + } + + // business logic that actually uses the injected MovieFinder is omitted... +} +``` + +Kotlin + +``` +class SimpleMovieLister { + + // a late-initialized property so that the Spring container can inject a MovieFinder + lateinit var movieFinder: MovieFinder + + // business logic that actually uses the injected MovieFinder is omitted... +} +``` + +`ApplicationContext`为其管理的 bean 支持基于构造函数和基于 setter 的 DI。在通过构造函数方法注入了一些依赖项之后,它还支持基于 setter 的 DI。你以`BeanDefinition`的形式配置依赖项,该依赖项与`ApplicationContext`实例一起使用,以将属性从一种格式转换为另一种格式。然而,大多数 Spring 用户并不直接使用这些类(即编程),而是使用 XML`bean`定义、带注释的组件(即用`@Component`、`@controller` 等注释的类),或者基于 Java 的`ApplicationContext`类中的`ApplicationContext`方法。然后将这些源在内部转换为`BeanDefinition`的实例,并用于加载整个 Spring IoC 容器实例。 + +基于构造器还是基于设置器的 DI? + +由于可以混合使用基于构造函数和基于 setter 的 DI,因此使用构造函数来实现强制依赖项,使用 setter 方法或配置方法来实现可选依赖项是一个很好的经验法则。请注意,在 setter 方法上使用`ApplicationContext`注释可以使属性成为一个必需的依赖项;但是,更好的方法是使用带参数的编程验证的构造函数注入。 + +Spring 团队通常提倡构造函数注入,因为它允许将应用程序组件实现为不可变对象,并确保所需的依赖关系不是`null`。此外,构造函数注入的组件总是以完全初始化的状态返回给客户机(调用)代码。作为附带说明,大量的构造函数参数是一种糟糕的代码气味,这意味着类可能有太多的责任,应该进行重构以更好地解决关注的适当分离。 + +Setter 注入应该主要用于可选的依赖项,这些依赖项可以在类中分配合理的默认值。否则,在代码使用依赖项的所有地方都必须执行 not-null 检查。setter 注入的一个好处是,setter 方法使该类的对象可以在以后进行重新配置或重新注入。因此,通过[JMX MBeans](integration.html#jmx)进行管理是 Setter 注入的一个引人注目的用例。 + +使用对特定类最有意义的 DI 样式。有时,在处理你没有源代码的第三方类时,你需要做出选择。例如,如果第三方类不公开任何 setter 方法,那么构造函数注入可能是唯一可用的 DI 形式。 + +##### 依赖关系解决过程 + +容器执行 Bean 依赖项解析,如下所示: + +* 使用描述所有 bean 的配置元数据创建和初始化`ApplicationContext`。配置元数据可以通过 XML、Java 代码或注释来指定。 + +* 对于每个 Bean,其依赖关系以属性、构造函数参数或静态工厂方法的参数的形式表示(如果你使用它而不是普通的构造函数)。这些依赖关系被提供给 Bean,当 Bean 实际被创建时。 + +* Bean 每个属性或构造函数参数都是要设置的值的实际定义,或者是对容器中另一个值的引用。 + +* 作为值的每个属性或构造函数参数都将从其指定的格式转换为该属性或构造函数参数的实际类型。默认情况下, Spring 可以将以字符串格式提供的值转换为所有内置类型,例如`int`、`long’、`String`、`boolean`,等等。 + +Spring 容器在创建容器时验证每个 Bean 的配置。然而,在实际创建 Bean 之前, Bean 属性本身不会被设置。当创建容器时,将创建单实例范围并设置为预实例化(默认)的 bean。作用域在[Bean Scopes](#beans-factory-scopes)中定义。否则, Bean 仅在被请求时才被创建。 Bean 的创建可能会导致创建 bean 的图形,因为 Bean 的依赖项及其依赖项的依赖项(等等)被创建和分配。请注意,这些依赖项之间的分辨率不匹配可能会出现在较晚的时候——也就是说,在第一次创建受影响的 Bean 时。 + +循环依赖 + +如果主要使用构造函数注入,则有可能创建一个不可解析的循环依赖场景。 + +例如:类 A 通过构造函数注入需要类 B 的实例,而类 B 通过构造函数注入需要类 A 的实例。如果将 bean 配置为类 A 和类 B 相互注入, Spring IOC 容器在运行时检测到这个循环引用,并抛出一个“BeanCurrentlyIncreationException”。 + +一种可能的解决方案是编辑一些类的源代码,由 setter 而不是构造函数来配置。或者,避免构造函数注入,而只使用 setter 注入。换句话说,尽管不推荐使用它,但你可以使用 setter 注入配置循环依赖项。 + +与典型的情况(没有循环依赖关系)不同, Bean a 和 Bean b 之间的循环依赖关系迫使其中一个 bean 在完全初始化自身之前被注入到另一个 bean 中(典型的先有鸡后有蛋的场景)。 + +你通常可以相信 Spring 会做正确的事。它在容器加载时检测配置问题,例如对不存在的 bean 的引用和循环依赖关系。 Spring 设置属性并尽可能晚地解决依赖关系,当 Bean 实际创建时。这意味着,当你请求一个对象时,如果在创建该对象或其依赖关系中存在问题时,已经正确加载的 Spring 容器可以在以后生成一个异常——例如, Bean 抛出一个异常是由于缺少或无效的属性造成的。这种对某些配置问题的潜在延迟可见性是`ApplicationContext`实现默认预实例化单例 bean 的原因。在实际需要这些 bean 之前创建这些 bean 需要一些前期时间和内存,但在创建`ApplicationContext`时(而不是以后),你会发现配置问题。你仍然可以重写此缺省行为,以便 Singleton Bean 可以惰性地初始化,而不是急切地预先实例化。 + +如果不存在循环依赖关系,则当一个或多个协作 bean 被注入到依赖的 Bean 中时,每个协作的 Bean 在被注入到依赖的 Bean 中之前被完全配置。这意味着,如果 Bean a 对 Bean b 具有依赖性,则 Spring IOC 容器在 Bean a 上调用 setter 方法之前完全配置 Bean b。换句话说, Bean 是实例化的(如果它不是预实例化的单例),它的依赖项被设置,并且相关的生命周期方法(例如[初始化 bean 回调方法](#beans-factory-lifecycle-initializingbean)或[初始化 bean 回调方法](#beans-factory-lifecycle-initializingbean))被调用。 + +##### 依赖注入示例 + +下面的示例为基于 setter 的 DI 使用基于 XML 的配置元数据。 Spring XML 配置文件的一小部分指定了如下一些 Bean 定义: + +``` + + + + + + + + + + + + + +``` + +下面的示例显示了相应的`ExampleBean`类: + +Java + +``` +public class ExampleBean { + + private AnotherBean beanOne; + + private YetAnotherBean beanTwo; + + private int i; + + public void setBeanOne(AnotherBean beanOne) { + this.beanOne = beanOne; + } + + public void setBeanTwo(YetAnotherBean beanTwo) { + this.beanTwo = beanTwo; + } + + public void setIntegerProperty(int i) { + this.i = i; + } +} +``` + +Kotlin + +``` +class ExampleBean { + lateinit var beanOne: AnotherBean + lateinit var beanTwo: YetAnotherBean + var i: Int = 0 +} +``` + +在前面的示例中,setter 被声明为与 XML 文件中指定的属性匹配。下面的示例使用基于构造函数的 DI: + +``` + + + + + + + + + + + + + + +``` + +下面的示例显示了相应的`ExampleBean`类: + +Java + +``` +public class ExampleBean { + + private AnotherBean beanOne; + + private YetAnotherBean beanTwo; + + private int i; + + public ExampleBean( + AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) { + this.beanOne = anotherBean; + this.beanTwo = yetAnotherBean; + this.i = i; + } +} +``` + +Kotlin + +``` +class ExampleBean( + private val beanOne: AnotherBean, + private val beanTwo: YetAnotherBean, + private val i: Int) +``` + +Bean 定义中指定的构造函数参数被用作`ExampleBean`构造函数的参数。 + +现在考虑这个示例的一个变体,其中, Spring 被告知调用`static`Factory 方法来返回对象的实例,而不是使用构造函数: + +``` + + + + + + + + +``` + +下面的示例显示了相应的`ExampleBean`类: + +Java + +``` +public class ExampleBean { + + // a private constructor + private ExampleBean(...) { + ... + } + + // a static factory method; the arguments to this method can be + // considered the dependencies of the bean that is returned, + // regardless of how those arguments are actually used. + public static ExampleBean createInstance ( + AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) { + + ExampleBean eb = new ExampleBean (...); + // some other operations... + return eb; + } +} +``` + +Kotlin + +``` +class ExampleBean private constructor() { + companion object { + // a static factory method; the arguments to this method can be + // considered the dependencies of the bean that is returned, + // regardless of how those arguments are actually used. + fun createInstance(anotherBean: AnotherBean, yetAnotherBean: YetAnotherBean, i: Int): ExampleBean { + val eb = ExampleBean (...) + // some other operations... + return eb + } + } +} +``` + +`static`工厂方法的参数由``元素提供,这与实际使用构造函数的情况完全相同。工厂方法返回的类的类型不必与包含`static`工厂方法的类的类型相同(尽管在本例中是这样)。实例(非静态)工厂方法可以以基本相同的方式使用(除了使用`factory-bean`属性而不是`class`属性),因此我们在这里不讨论这些细节。 + +#### 1.4.2.详细介绍依赖关系和配置 + +正如[上一节](#beans-factory-collaborators)中提到的,你可以将 Bean 属性和构造函数参数定义为对其他托管 bean(协作者)的引用,或者作为内联定义的值。 Spring 的基于 XML 的配置元数据为此支持其``和`ExampleBean`元素中的子元素类型。 + +##### 直值(原语、字符串等) + +``元素的`value`属性将一个属性或构造函数参数指定为人类可读的字符串表示形式。 Spring 的[转换服务](#core-convert-ConversionService-API)用于将这些值从`String`转换为属性或参数的实际类型。下面的示例显示了正在设置的各种值: + +``` + + + + + + + +``` + +下面的示例使用[p-namespace](#beans-p-namespace)实现更简洁的 XML 配置: + +``` + + + + + +``` + +前面的 XML 更简洁。但是,除非你使用一个 IDE(例如[IntelliJ IDEA](https://www.jetbrains.com/idea/)或[Spring Tools for Eclipse](https://spring.io/tools))来支持在创建 Bean 定义时自动完成属性,否则会在运行时而不是在设计时发现打字错误。强烈推荐这种 IDE 协助。 + +你还可以配置`java.util.Properties`实例,如下所示: + +``` + + + + + + jdbc.driver.className=com.mysql.jdbc.Driver + jdbc.url=jdbc:mysql://localhost:3306/mydb + + + +``` + +Spring 容器使用 JavaBeans``机制将``元素内的文本转换为 `java.util.properties’实例。这是一个很好的快捷方式,并且是 Spring 团队确实支持使用嵌套``元素而不是`value`属性样式的少数几个地方之一。 + +###### `idref`元素 + +`idref`元素只是一种防错误的方式,可以将容器中另一个 Bean 的`id`(一个字符串值-不是引用)传递给``或``元素。下面的示例展示了如何使用它: + +``` + + + + + + + +``` + +Bean 前面的定义片段(在运行时)与下面的片段完全等效: + +``` + + + + + +``` + +第一种形式比第二种形式更好,因为使用`idref`标记可以让容器在部署时验证所引用的名为 Bean 的容器实际存在。在第二个变化中,不对传递给`client` Bean 的`targetName`属性的值执行验证。只有当`client` Bean 被实际实例化时,才会发现错别字(最有可能导致致命的结果)。如果`client` Bean 是[prototype](#beans-factory-scopes) Bean,则只有在部署容器很长时间后才能发现该错别字和由此产生的异常。 + +| |在 4.0bean
XSD 中,`local`元素上的`local`属性不再受支持,因为它不再为常规的`bean`引用提供值。在升级到 4.0 模式时,将现有的
引用更改为`idref bean`。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +``元素带来价值的一个常见位置(至少在 Spring 2.0 之前的版本中)是在 `proxyFactoryBean` Bean 定义中的[AOP interceptors](#aop-pfb-1)的配置中。在指定拦截器名称时使用``元素可以防止错误拼写拦截器 ID。 + +##### 对其他 bean 的引用(协作者) + +`ref`元素是``或``定义元素中的最后一个元素。在这里,你将 Bean 的指定属性的值设置为对由容器管理的另一个 Bean(协作者)的引用。引用的 Bean 是要设置其属性的 Bean 的依赖项,并且在设置该属性之前根据需要对其进行初始化。(如果协作者是单例 Bean,它可能已经被容器初始化了。)所有引用最终都是对另一个对象的引用。范围和验证取决于你是通过`bean`还是`parent`属性指定另一个对象的 ID 或名称。 + +通过``标记的`bean`属性指定目标 Bean 是最通用的形式,并且允许在相同的容器或父容器中创建对任何 Bean 的引用,无论它是否在相同的 XML 文件中。 Bean 属性的值可以与目标 Bean 的`id`属性相同,或者与目标 Bean 的`name`属性中的一个值相同。下面的示例展示了如何使用`ref`元素: + +``` + +``` + +通过`parent`属性指定目标 Bean 将创建对当前容器的父容器中的 Bean 的引用。`parent`属性的值可以与目标 Bean 的`id`属性相同,也可以与目标 Bean 的`name`属性中的一个值相同。目标 Bean 必须位于当前容器的父容器中。当你有一个容器的层次结构,并且希望用一个与父 Bean 名称相同的代理来包装父容器中的现有 Bean 时,你应该主要使用这个 Bean 引用变体。下面的一对清单展示了如何使用`parent`属性: + +``` + + + + +``` + +``` + + + class="org.springframework.aop.framework.ProxyFactoryBean"> + + + + + +``` + +| |在 4.0bean
XSD 中,`local`元素上的`local`属性不再受支持,因为它不再为常规的`bean`引用提供值。在升级到 4.0 模式时,将你现有的
引用更改为`ref bean`。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 内豆 + +一个``元素内部的``或``元素定义了一个内部 Bean,如下例所示: + +``` + + + + + + + + + +``` + +内部 Bean 定义不需要定义的 ID 或名称。如果指定,则容器不使用这样的值作为标识符。容器还在创建时忽略`scope`标志,因为内部 bean 总是匿名的,并且总是与外部 bean 一起创建 Bean。 Bean 不可能独立地访问内部 bean 或将它们注入协作 bean 中。 + +作为一个角的情况,可以从自定义范围接收销毁回调——例如,对于包含在单例 Bean 中的请求范围的内部 Bean。内部 Bean 实例的创建与其包含的 Bean 绑定在一起,但是销毁回调允许它参与请求作用域的生命周期。这种情况并不常见。内部 bean 通常只共享其包含 Bean 的范围。 + +##### 收藏 + +``、``、``和``元素分别设置 Java`Collection`类型`List`、`Set`、`Set`和`Properties`的属性和参数。下面的示例展示了如何使用它们: + +``` + + + + + [email protected] + [email protected] + [email protected] + + + + + + a list element followed by a reference + + + + + + + + + + + + + + just some string + + + + +``` + +映射键或值或设定值的值也可以是以下任何元素: + +``` +bean | ref | idref | list | set | map | props | value | null +``` + +###### 集合合并 + +Spring 容器还支持合并集合。应用程序开发人员可以定义父``、``、``或``元素,并具有子元素``、``、``或``元素来继承和重写来自父集合的值。也就是说,子集合的值是合并父集合和子集合的元素的结果,而子集合的元素重写了父集合中指定的值。 + +关于合并的这一节讨论了父-子机制 Bean。 Bean 不熟悉父和子定义的读者在继续之前可能希望阅读[相关部分](#beans-child-bean-definitions)。 + +下面的示例演示集合合并: + +``` + + + + + [email protected] + [email protected] + + + + + + + + [email protected] + [email protected] + + + + +``` + +注意在`child` Bean 定义的 `adminemails’属性的``元素上使用`merge=true`属性。当容器解析并实例化`child` Bean 时,生成的实例具有一个`adminEmails``properties` 集合,该集合包含将子的 `adminemails’集合与父的`adminEmails`集合合并的结果。下面的列表显示了结果: + +``` +[email protected] +[email protected] +[email protected] +``` + +子`Properties`集合的值集继承了父``中的所有属性元素,而子`support`值的值覆盖了父集合中的值。 + +这种合并行为类似于``、``和``集合类型。在``元素的特定情况下,维护与`List`集合类型(即`ordered`值集合的概念)相关联的语义。父级的值在所有子列表的值之前。在`Map`、`Set`和`Properties`集合类型的情况下,不存在排序。因此,对于容器内部使用的关联`Map`、`Set`和`Properties`实现类型的集合类型,没有任何排序语义。 + +###### 集合合并的局限性 + +不能合并不同的集合类型(例如`Map`和`List`)。如果你尝试这样做,将抛出一个适当的`Exception`。`merge`属性必须在较低的、继承的子定义上指定。在父集合定义上指定`merge`属性是多余的,不会导致所需的合并。 + +###### 强类型集合 + +通过在 Java5 中引入泛型类型,你可以使用强类型集合。也就是说,可以声明`Collection`类型,使得它只能包含(例如)`String`元素。如果使用 Spring 来将强类型的``注入到 Bean 中,则可以利用 Spring 的类型转换支持,使得强类型的`Collection`实例的元素在被添加到`Collection`之前被转换为适当的类型。下面的 Java 类和 Bean 定义展示了如何做到这一点: + +Java + +``` +public class SomeClass { + + private Map accounts; + + public void setAccounts(Map accounts) { + this.accounts = accounts; + } +} +``` + +Kotlin + +``` +class SomeClass { + lateinit var accounts: Map +} +``` + +``` + + + + + + + + + + + +``` + +当`accounts`属性的`something`被准备注入时,关于强类型的元素类型的泛型信息`Map`通过反射是可用的。因此, Spring 的类型转换基础结构将各种值元素识别为类型,并且将字符串值(`9.99’、和 `3.99’)转换为实际的类型。 + +##### 空字符串符值和空字符串符值 + +Spring 将属性等的空参数视为空`Strings`。以下基于 XML 的配置元数据片段将`email`属性设置为空的 `string`value。 + +``` + + + +``` + +前面的示例相当于下面的 Java 代码: + +Java + +``` +exampleBean.setEmail(""); +``` + +Kotlin + +``` +exampleBean.email = "" +``` + +``元素处理`null`值。下面的清单展示了一个示例: + +``` + + + + + +``` + +前面的配置相当于下面的 Java 代码: + +Java + +``` +exampleBean.setEmail(null); +``` + +Kotlin + +``` +exampleBean.email = null +``` + +##### 带有 P-namespace 的 XML 快捷方式 + +P-Namespace 允许你使用`bean`元素的属性(而不是嵌套的 `` 元素)来描述你的属性值协作 bean,或两者兼而有之。 + +Spring 支持可扩展的配置格式[with namespaces](#xsd-schemas),其基于 XML 模式定义。本章讨论的`beans`配置格式是在 XML 模式文档中定义的。然而,P-命名空间不是在 XSD 文件中定义的,并且仅存在于 Spring 的核心中。 + +下面的示例展示了两个解析为相同结果的 XML 片段(第一个使用标准 XML 格式,第二个使用 P 名称空间): + +``` + + + + + + + + +``` + +该示例在 Bean 定义中显示了一个名为`email`的 P-namespace 中的属性。这告诉 Spring 要包括一个属性声明。如前所述,P-Namespace 没有模式定义,因此你可以将属性的名称设置为属性名称。 + +下一个示例包括另外两个 Bean 定义,这两个定义都具有对另一个 Bean 的引用: + +``` + + + + + + + + + + + + + +``` + +这个示例不仅包括使用 P 名称空间的属性值,还使用一种特殊格式声明属性引用。鉴于第一个 Bean 定义使用``来创建从 Bean `John` 到 Bean `jane`的引用,而第二个 Bean 定义使用`p:spouse-ref="jane"`作为属性来做完全相同的事情。在这种情况下,`spouse`是属性名,而`-ref`部分表明这不是一个直值,而是对另一个值的引用 Bean。 + +| |P-namespace 不像标准 XML 格式那样灵活。例如,用于声明属性引用的
格式与以`Ref`结尾的属性相冲突,而
标准 XML 格式则不冲突。我们建议你谨慎地选择你的方法,并将
与你的团队成员进行沟通,以避免生成同时使用所有
三种方法的 XML 文档。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 带有 C-Namespace 的 XML 快捷方式 + +与[带有 P-namespace 的 XML 快捷方式](#beans-p-namespace)类似, Spring 3.1 中引入的 C-namespace 允许内联属性用于配置构造函数参数,而不是嵌套`constructor-arg`元素。 + +下面的示例使用`c:`名称空间来执行与 from[基于构造函数的依赖注入](#beans-constructor-injection)相同的操作: + +``` + + + + + + + + + + + + + + + + +``` + +`c:`名称空间使用与`p:`1( Bean 引用的尾随`-ref`)相同的约定来根据构造函数参数的名称设置它们。类似地,它需要在 XML 文件中声明,即使它不是在 XSD 模式中定义的(它存在于 Spring 内核中)。 + +对于构造函数参数名称不可用的罕见情况(通常如果字节码是在没有调试信息的情况下编译的),可以使用回退到参数索引,如下所示: + +``` + + +``` + +| |由于 XML 语法的原因,由于 XML 属性名称不能以数字开头(即使某些 IDE 允许),因此索引符号需要存在前导数`_`,

也可以使用相应的索引符号。对于``元素但
不常用,因为声明的普通顺序通常在那里就足够了。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在实践中,构造函数分辨率[mechanism](#beans-factory-ctor-arguments-resolution)在匹配参数方面非常有效,因此,除非你确实需要,否则我们建议在整个配置中使用名称符号。 + +##### 复合属性名称 + +在设置 Bean 属性时,可以使用复合或嵌套的属性名称,只要路径的所有组件(除了最终的属性名称)不是`null`。考虑以下 Bean 定义: + +``` + + + +``` + +`something` Bean 有一个`fred`属性,它有一个`bob`属性,它有一个`sammy`属性,最后的`sammy`属性被设置为一个值`123`。为了使其工作,`fred`的`something`属性和`bob`的`bob`属性在构造 Bean 之后一定不能是`null`。否则,将抛出一个`NullPointerException`。 + +#### 1.4.3.使用`depends-on` + +如果一个 Bean 是另一个 Bean 的依赖项,这通常意味着一个 Bean 被设置为另一个 Bean 的属性。通常,你使用基于 XML 的配置元数据中的[``element](#beans-ref-element)来实现这一点。然而,有时 bean 之间的依赖关系并不那么直接。一个例子是当需要触发类中的静态初始化器时,例如用于数据库驱动程序注册。`depends-on`属性可以显式地强制一个或多个 bean 在使用该元素初始化 Bean 之前被初始化。下面的示例使用`depends-on`属性来表示对单个 Bean 的依赖关系: + +``` + + +``` + +要表示对多个 bean 的依赖关系,请提供一个 Bean 名称列表,作为`depends-on`属性的值(逗号、空格和分号是有效的分隔符): + +``` + + + + + + +``` + +| |`depends-on`属性既可以指定一个初始化-时间依赖项,也可以指定
在[singleton](#beans-factory-scopes-singleton)bean 的情况下,对应的
销毁-时间依赖项。定义与给定 Bean 的`depends-on`关系
的依赖 bean 首先被销毁,在给定 Bean 本身被销毁之前。
因此,`depends-on`也可以控制关闭顺序。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.4.4.惰性初始化的 bean + +默认情况下,`ApplicationContext`实现急切地创建和配置所有[singleton](#beans-factory-scopes-singleton)bean,作为初始化过程的一部分。通常,这种预实例化是可取的,因为配置或周围环境中的错误会立即被发现,而不是在几个小时甚至几天之后。当这种行为不可取时,可以通过将 Bean 定义标记为惰性初始化来防止单例 Bean 的预实例化。惰性初始化的 Bean 告诉 IOC 容器在首次请求时而不是在启动时创建 Bean 实例。 + +在 XML 中,这种行为由``元素上的`lazy-init`属性控制,如下例所示: + +``` + + +``` + +当前面的配置被`ApplicationContext`消耗时,当`ApplicationContext`开始时,`lazy` Bean 不会急切地预先实例化,而`not.lazy` Bean 会急切地预先实例化。 + +然而,当惰性初始化的 Bean 是未惰性初始化的单例 Bean 的依赖项时,`ApplicationContext`在启动时创建惰性初始化的 Bean,因为它必须满足单例的依赖项。将惰性初始化的 Bean 注入到其他未惰性初始化的单例 Bean 中。 + +你还可以在容器级别使用``元素上的 `default-lazy-init’属性来控制 lazy-initialization,如下例所示: + +``` + + + +``` + +#### 1.4.5.自动布线合作者 + +Spring 容器可以自动连接协作 bean 之间的关系。你可以通过检查`ApplicationContext`的内容,让 Spring 为你的 Bean 自动解析协作者(其他 bean)。自动布线有以下优点: + +* 自动布线可以大大减少指定属性或构造函数参数的需要。(其他机制,如 Bean 模板[在本章的其他地方讨论过](#beans-child-bean-definitions)在这方面也很有价值。) + +* 随着对象的发展,自动布线可以更新配置。例如,如果需要向类添加依赖项,则可以自动满足该依赖项,而无需修改配置。因此,在开发过程中,自动布线特别有用,而无需否定当代码库变得更稳定时切换到显式布线的选项。 + +当使用基于 XML 的配置元数据(参见[依赖注入](#beans-factory-collaborators))时,你可以使用 `` 元素的`autowire`属性为 Bean 定义指定 AutoWire 模式。自动接线功能有四种模式。你可以根据 Bean 指定自动接线,因此可以选择要自动接线的接线方式。下表描述了四种自动布线模式: + +| Mode |解释| +|-------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `no` |(默认)没有自动接线。 Bean 引用必须由`ref`元素定义。对于较大的部署,不建议更改
缺省设置,因为显式地指定
协作者可获得更大的控制和清晰度。在某种程度上,它
记录了一个系统的结构。| +| `byName` |按属性名称自动接线。 Spring 寻找与
属性同名的需要自动连线的 Bean。例如,如果一个 Bean 定义通过名称设置为
AutoWire,并且它包含一个`master`属性(即,它有一个 `setmaster(..)’方法), Spring 查找一个名为`master`的 Bean 定义,并使用
它来设置该属性。| +| `byType` |如果在
容器中正好存在一个属性类型 Bean,则让属性自动连线。如果存在多个异常,将抛出一个致命的异常,该异常表示
你可能不会为此使用`byType`自动布线 Bean。如果没有匹配的
bean,则不会发生任何事情(未设置该属性)。| +|`constructor`|类似于`byType`,但适用于构造函数参数。如果容器中没有一个 Bean 构造函数参数类型的
,则会产生致命的错误。| + +使用`byType`或`constructor`自动布线模式,可以连接数组和类型化集合。在这种情况下,将提供容器中匹配预期类型的所有 AutoWire 候选项,以满足依赖关系。如果期望的键类型是`String`,则可以自动连接强类型`Map`实例。AutoWired`Map`实例的值由所有与预期类型匹配的 Bean 实例组成,而 `map’实例的键包含相应的 Bean 名称。 + +##### 自动布线的局限性和缺点 + +自动布线在项目中一致使用时效果最好。如果通常不使用自动布线,那么只使用一个或两个定义来连接可能会使开发人员感到困惑 Bean。 + +考虑一下自动布线的局限性和缺点: + +* `property`和`constructor-arg`设置中的显式依赖总是覆盖自动布线。你不能自动连接简单的属性,例如原语、`strings’和`Classes`(以及此类简单属性的数组)。这种限制是人为设计的。 + +* 自动布线不像显式布线那样精确。尽管,正如在前面的表中所指出的, Spring 小心地避免在可能产生意外结果的模棱两可的情况下进行猜测。你的 Spring-托管对象之间的关系不再被显式地记录。 + +* 接线信息可能不能用于可能从 Spring 容器生成文档的工具。 + +* Bean 容器内的多个定义可以匹配由 setter 方法或构造函数参数指定的类型以进行自动连线。对于数组、集合或“map”实例,这不一定是个问题。然而,对于期望单个值的依赖关系,这种歧义不能任意解决。如果没有唯一的 Bean 定义可用,则抛出一个异常。 + +在后一种情况下,你有几种选择: + +* 放弃自动布线,改用显式布线。 + +* 通过将其`autowire-candidate`属性设置为`false`,避免 Bean 定义的自动布线,如[next section](#beans-factory-autowire-candidate)中所述。 + +* 通过将其``元素的 `primary’属性设置为`true`,指定一个 Bean 定义作为主要候选者。 + +* 用基于注释的配置实现更细粒度的控件,如[基于注释的容器配置](#beans-annotation-config)中所述。 + +##### 从自动接线中排除 A Bean + +在每 Bean 个基础上,可以将 Bean 个从自动布线中排除。在 Spring 的 XML 格式中,将`autowire-candidate`元素的`autowire-candidate`属性设置为`false`。容器使得该特定的 Bean 定义对自动布线基础设施不可用(包括注释样式配置,例如[`@Autowired`](#beans-autowired-annotation))。 + +| |`autowire-candidate`属性被设计为仅影响基于类型的自动连接。
它不会影响通过名称的显式引用,即使指定的 Bean
未标记为自动连接候选项,也会得到解析。因此,如果名称匹配,则按名称自动布线
仍然会注入 Bean。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +还可以根据模式匹配对 Bean 名称限制自动连接候选项。顶层``元素在其 `default-autowire-candidates’属性中接受一个或多个模式。例如,要将 AutoWire 候选状态限制为名称以`Repository`结尾的任何 Bean,请提供一个值`*Repository`。要提供多个模式,请在逗号分隔的列表中定义它们。对于 Bean 定义的`autowire-candidate`属性,显式的值 `true’或`false`总是优先的。对于这样的 bean,模式匹配规则不适用。 + +对于那些你永远不希望通过自动布线被注入到其他 bean 中的 bean,这些技术非常有用。这并不意味着被排除的 Bean 本身不能通过使用自动布线来进行配置。相反, Bean 本身并不是自动连接其他 bean 的候选者。 + +#### 1.4.6.方法注入 + +在大多数应用程序场景中,容器中的大多数 bean 都是[singletons](#beans-factory-scopes-singleton)。当单例 Bean 需要与另一个单例 Bean 协作时,或者非单例 Bean 需要与另一个非单例 Bean 协作时,通常通过将一个 Bean 定义为另一个的属性来处理依赖关系。当 Bean 生命周期不同时会出现问题。假设单例 Bean a 需要使用非单例(原型) Bean b,也许是在 a 上的每个方法调用上。容器只创建单例 Bean a 一次,因此只获得一次设置属性的机会。容器不能在每次需要 Bean b 的新实例时向 Bean a 提供新实例。 + +一种解决办法是放弃某种程度的控制权倒置。通过实现`ApplicationContextAware`接口,并通过[making a `getBean("B")` call to the container](#beans-factory-client)在每次 Bean a 需要它时请求(通常是新的) Bean b 实例,可以[make bean A aware of the container](#beans-factory-aware)。下面的示例展示了这种方法: + +Java + +``` +// a class that uses a stateful Command-style class to perform some processing +package fiona.apple; + +// Spring-API imports +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +public class CommandManager implements ApplicationContextAware { + + private ApplicationContext applicationContext; + + public Object process(Map commandState) { + // grab a new instance of the appropriate Command + Command command = createCommand(); + // set the state on the (hopefully brand new) Command instance + command.setState(commandState); + return command.execute(); + } + + protected Command createCommand() { + // notice the Spring API dependency! + return this.applicationContext.getBean("command", Command.class); + } + + public void setApplicationContext( + ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } +} +``` + +Kotlin + +``` +// a class that uses a stateful Command-style class to perform some processing +package fiona.apple + +// Spring-API imports +import org.springframework.context.ApplicationContext +import org.springframework.context.ApplicationContextAware + +class CommandManager : ApplicationContextAware { + + private lateinit var applicationContext: ApplicationContext + + fun process(commandState: Map<*, *>): Any { + // grab a new instance of the appropriate Command + val command = createCommand() + // set the state on the (hopefully brand new) Command instance + command.state = commandState + return command.execute() + } + + // notice the Spring API dependency! + protected fun createCommand() = + applicationContext.getBean("command", Command::class.java) + + override fun setApplicationContext(applicationContext: ApplicationContext) { + this.applicationContext = applicationContext + } +} +``` + +上述方法是不可取的,因为业务代码知道并耦合到 Spring 框架。方法注入是 Spring IOC 容器的一个比较高级的特性,它允许你干净地处理这个用例。 + +你可以在[this blog entry](https://spring.io/blog/2004/08/06/method-injection/)中阅读有关方法注入的动机的更多信息。 + +##### 查找方法注入 + +查找方法注入是容器重写容器管理的 bean 上的方法并返回容器中另一个名为 Bean 的查找结果的能力。查找通常涉及原型 Bean,如[上一节](#beans-factory-method-injection)中描述的场景中所述的那样。 Spring 框架通过使用来自 CGlib 库的字节码生成来动态地生成覆盖该方法的子类,从而实现了该方法注入。 + +| |* For this dynamic subclassing to work, the class that the Spring bean container
subclasses cannot be `final`, and the method to be overridden cannot be `final`, either.

*单元-测试具有`abstract`方法的类需要你自己对类
进行子类分类,并提供`abstract`方法的存根实现。

* 组件扫描也需要具体的方法,这需要具体的
类来拾取。

* 另一个关键限制是,查找方法不适用于工厂方法,而且
尤其不适用于配置类中的`@Bean`方法,因为,在这种情况下,
容器不负责创建实例,因此不能动态创建
运行时生成的子类。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在前面的代码片段中的`CommandManager`类的情况下, Spring 容器动态地覆盖`createCommand()`方法的实现。正如重做的示例所示,`CommandManager`类没有任何 Spring 依赖关系: + +Java + +``` +package fiona.apple; + +// no more Spring imports! + +public abstract class CommandManager { + + public Object process(Object commandState) { + // grab a new instance of the appropriate Command interface + Command command = createCommand(); + // set the state on the (hopefully brand new) Command instance + command.setState(commandState); + return command.execute(); + } + + // okay... but where is the implementation of this method? + protected abstract Command createCommand(); +} +``` + +Kotlin + +``` +package fiona.apple + +// no more Spring imports! + +abstract class CommandManager { + + fun process(commandState: Any): Any { + // grab a new instance of the appropriate Command interface + val command = createCommand() + // set the state on the (hopefully brand new) Command instance + command.state = commandState + return command.execute() + } + + // okay... but where is the implementation of this method? + protected abstract fun createCommand(): Command +} +``` + +在包含要注入的方法的客户机类中(在本例中为`CommandManager`),要注入的方法需要以下形式的签名: + +``` + [abstract] theMethodName(no-arguments); +``` + +如果方法是`abstract`,则动态生成的子类实现该方法。否则,动态生成的子类将覆盖在原始类中定义的具体方法。考虑以下示例: + +``` + + + + + + + + + +``` + +Bean 标识为`commandManager`的方法在需要`myCommand` Bean 的新实例时调用它自己的`createCommand()`方法。如果确实需要将`myCommand` Bean 作为原型部署,则必须小心。如果是[singleton](#beans-factory-scopes-singleton),则每次都返回相同的`myCommand` Bean 实例。 + +或者,在基于注释的组件模型中,可以通过`@Lookup`注释声明查找方法,如下例所示: + +Java + +``` +public abstract class CommandManager { + + public Object process(Object commandState) { + Command command = createCommand(); + command.setState(commandState); + return command.execute(); + } + + @Lookup("myCommand") + protected abstract Command createCommand(); +} +``` + +Kotlin + +``` +abstract class CommandManager { + + fun process(commandState: Any): Any { + val command = createCommand() + command.state = commandState + return command.execute() + } + + @Lookup("myCommand") + protected abstract fun createCommand(): Command +} +``` + +或者,更常见的是,你可以依赖于目标 Bean 根据查找方法的声明的返回类型进行解析: + +Java + +``` +public abstract class CommandManager { + + public Object process(Object commandState) { + Command command = createCommand(); + command.setState(commandState); + return command.execute(); + } + + @Lookup + protected abstract Command createCommand(); +} +``` + +Kotlin + +``` +abstract class CommandManager { + + fun process(commandState: Any): Any { + val command = createCommand() + command.state = commandState + return command.execute() + } + + @Lookup + protected abstract fun createCommand(): Command +} +``` + +请注意,你通常应该使用一个具体的存根实现来声明这种带注释的查找方法,以便它们与 Spring 的组件扫描规则兼容,在组件扫描规则中,抽象类在默认情况下会被忽略。此限制不适用于显式注册或显式导入的 Bean 类。 + +| |访问不同范围的目标 bean 的另一种方法是`ObjectFactory`/`provider’注入点。参见[作为依赖项的作用域 bean](#beans-factory-scopes-other-injection).

你也可能会发现`ServiceLocatorFactoryBean`(在 `org.springframework.beans.factory.config` 包中)是有用的。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 任意方法替换 + +Bean 与查找方法注入相比,方法注入的一种不那么有用的形式是能够用另一种方法实现来替换托管中的任意方法。你可以安全地跳过本节的其余部分,直到你真正需要此功能为止。 + +对于基于 XML 的配置元数据,你可以使用`replaced-method`元素来替换已部署的另一个方法实现。考虑下面的类,它有一个名为`computeValue`的方法,我们想要覆盖它: + +Java + +``` +public class MyValueCalculator { + + public String computeValue(String input) { + // some real code... + } + + // some other methods... +} +``` + +Kotlin + +``` +class MyValueCalculator { + + fun computeValue(input: String): String { + // some real code... + } + + // some other methods... +} +``` + +实现`org.springframework.beans.factory.support.MethodReplacer`接口的类提供了新的方法定义,如下例所示: + +Java + +``` +/** + * meant to be used to override the existing computeValue(String) + * implementation in MyValueCalculator + */ +public class ReplacementComputeValue implements MethodReplacer { + + public Object reimplement(Object o, Method m, Object[] args) throws Throwable { + // get the input value, work with it, and return a computed result + String input = (String) args[0]; + ... + return ...; + } +} +``` + +Kotlin + +``` +/** + * meant to be used to override the existing computeValue(String) + * implementation in MyValueCalculator + */ +class ReplacementComputeValue : MethodReplacer { + + override fun reimplement(obj: Any, method: Method, args: Array): Any { + // get the input value, work with it, and return a computed result + val input = args[0] as String; + ... + return ...; + } +} +``` + +Bean 用于部署原始类并指定方法覆盖的定义将类似于以下示例: + +``` + + + + String + + + + +``` + +可以在``元素中使用一个或多个``元素来指示要重写的方法的方法签名。只有当方法重载并且类中存在多个变量时,参数的签名才是必需的。为了方便起见,参数的类型字符串可以是完全限定类型名称的子字符串。例如,下面的 all match`java.lang.string’: + +``` +java.lang.String +String +Str +``` + +因为参数的数量通常足以区分每个可能的选择,所以通过允许你只键入与参数类型匹配的最短字符串,此快捷方式可以节省大量的输入。 + +### 1.5. Bean 范围 + +当你创建 Bean 定义时,你创建了用于创建由该 Bean 定义定义的类的实际实例的配方。 Bean 定义是一个配方的想法很重要,因为它意味着,与类一样,你可以从一个配方创建许多对象实例。 + +你不仅可以控制要插入到从特定 Bean 定义创建的对象中的各种依赖关系和配置值,还可以控制从特定 Bean 定义创建的对象的范围。这种方法功能强大且灵活,因为你可以选择通过配置创建的对象的范围,而不必在 Java 类级别上烘烤对象的范围。可以将 bean 定义为部署在多个作用域中的一个。 Spring 框架支持六个作用域,其中四个作用域只有在使用 Web 感知`ApplicationContext`时才可用。你还可以创建[a custom scope.](#beans-factory-scopes-custom) + +下表描述了受支持的作用域: + +| Scope |说明| +|-----------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [singleton](#beans-factory-scopes-singleton) |(缺省)将单个 Bean 定义作用于每个 Spring IOC
容器的单个对象实例。| +| [prototype](#beans-factory-scopes-prototype) |将单个 Bean 定义作用于任意数量的对象实例。| +| [request](#beans-factory-scopes-request) |Bean 将单个定义作用于单个 HTTP 请求的生命周期。即,
每个 HTTP 请求都有自己的实例 Bean,该实例是在单个 Bean
定义的后面创建的。 Spring `ApplicationContext`仅在可感知 Web 的上下文中有效。| +| [session](#beans-factory-scopes-session) |Bean 将单个定义作用于 HTTP`Session`的生命周期。 Spring
仅在网络感知的上下文中有效。| +| [application](#beans-factory-scopes-application) |将一个 Bean 定义的范围应用于`ServletContext`的生命周期。 Spring `ApplicationContext`仅在网络感知的上下文`ApplicationContext`中有效。| +|[websocket](web.html#websocket-stomp-websocket-scope)|将一个 Bean 定义作用于`WebSocket`的生命周期。 Spring `ApplicationContext`仅在网络感知的上下文中有效。| + +| |从 Spring 3.0 开始,线程作用域是可用的,但默认情况下不会注册。有关
的更多信息,请参见[“SimpleThreadscope”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/support/SimpleThreadScope.html)的文档。
有关如何注册此范围或任何其他自定义范围的说明,请参见[使用自定义作用域](#beans-factory-scopes-custom-using)。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.5.1.单例范围 + +只管理单例 Bean 的一个共享实例,对具有与 Bean 定义相匹配的一个或多个 ID 的 bean 的所有请求导致由 Spring 容器返回一个特定的 Bean 实例。 + +换句话说,当你定义 Bean 定义并将其作为单例作用域时, Spring IoC 容器将创建由该 Bean 定义定义的对象的一个实例。这个单个实例存储在这样的单例 bean 的缓存中,并且所有后续的请求和对命名为 Bean 的引用都返回缓存的对象。下图显示了单例范围的工作原理: + +![singleton](images/singleton.png) + +Spring 的单例模式的概念 Bean 与四人帮模式书中定义的单例模式不同。GOF 单例硬编码对象的作用域,使得每个类装入器只创建一个特定类的实例。 Spring 单例的范围最好描述为每个容器和每个- Bean。这意味着,如果在单个 Spring 容器中为特定类定义一个 Bean,则 Spring 容器创建由该 Bean 定义的类的一个且只有一个实例。Singleton 作用域是 Spring 中的默认作用域。要将 Bean 定义为 XML 中的单例,可以定义 Bean,如以下示例所示: + +``` + + + + +``` + +#### 1.5.2.原型范围 + +Bean 部署的非单例原型作用域在每次提出对该特定 Bean 的请求时都会创建一个新的 Bean 实例。即,将 Bean 注入到另一个 Bean 中,或者通过容器上的`getBean()`方法调用来请求它。通常,你应该为所有有状态 bean 使用原型作用域,为无状态 bean 使用单例作用域。 + +下图说明了 Spring 原型范围: + +![prototype](images/prototype.png) + +(数据访问对象(DAO)通常不被配置为原型,因为典型的 DAO 不持有任何会话状态。对我们来说,重用单例图的核心更容易。 + +下面的示例将 Bean 定义为 XML 的原型: + +``` + +``` + +Spring 与其他范围相反,不管理原型的完整生命周期 Bean。容器实例化、配置和以其他方式组装一个原型对象,并将其交给客户机,而不需要进一步记录该原型实例。因此,尽管在所有对象上调用初始化生命周期回调方法,而不考虑作用域,但在原型的情况下,不调用已配置的销毁生命周期回调。客户机代码必须清理原型范围内的对象,并释放原型 bean 所拥有的昂贵资源。要获得 Spring 容器以释放由原型范围的 bean 持有的资源,请尝试使用自定义的[bean post-processor](#beans-factory-extension-bpp),它保存了对需要清理的 bean 的引用。 + +在某些方面, Spring 容器对于原型范围 Bean 的作用是 Java`new`操作符的替代。超过这一点的所有生命周期管理都必须由客户机处理。(有关 Spring 容器中 Bean 的生命周期的详细信息,请参见[生命周期回调](#beans-factory-lifecycle)。 + +#### 1.5.3.具有原型依赖关系的单例 bean- Bean + +当你使用依赖于原型 bean 的单例作用域 bean 时,请注意,依赖关系在实例化时已得到解决。因此,如果你将依赖-注入原型范围的 Bean 到单例范围的 Bean 中,那么将实例化一个新的原型 Bean,然后将依赖-注入到单例范围 Bean 中。 Bean 原型实例是提供给单例作用域的唯一实例。 + +然而,假设你希望单例作用域 Bean 在运行时反复获得原型作用域 Bean 的一个新实例。你不能将一个原型范围的 Bean 注入到你的单例 Bean 中,因为该注入仅在 Spring 容器实例化单例 Bean 并解析和注入其依赖项时发生一次。如果在运行时不止一次需要原型 Bean 的新实例,请参见[方法注入](#beans-factory-method-injection)。 + +#### 1.5.4.请求、会话、应用和 WebSocket 范围 + +`request`、`session`、`application`和`websocket`作用域只有在使用 Web-aware Spring `ApplicationContext`实现(例如 `xmlWebApplicationContext`)时才可用。如果你在常规 Spring IOC 容器中使用这些作用域,例如`ClassPathXmlApplicationContext`,则抛出一个抱怨未知 Bean 作用域的`IllegalStateException`。 + +##### 初始 Web 配置 + +为了支持在`request`、`session`、`application`和 ` WebSocket ` 级别(Web 范围的 bean)上对 bean 进行范围界定,在定义 bean 之前,需要进行一些较小的初始配置。(对于标准作用域:`singleton`和`prototype`,不需要这种初始设置。) + +如何完成这个初始设置取决于你的特定环境 Servlet。 + +如果你在 Spring Web MVC 中访问范围 bean,实际上是在 Spring `DispatcherServlet`处理的请求中访问,则无需进行特殊的设置。 + +如果使用 Servlet 2.5Web 容器,在 Spring 的 `DispatcherServlet’之外处理请求(例如,当使用 JSF 或 Struts 时),则需要注册 `org.springframework.web.context.request.requestContextListener`。对于 Servlet 3.0+,这可以通过使用`WebApplicationInitializer`接口以编程方式完成。或者,对于较旧的容器,可以将以下声明添加到 Web 应用程序的`web.xml`文件中: + +``` + + ... + + + org.springframework.web.context.request.RequestContextListener + + + ... + +``` + +或者,如果你的侦听器设置有问题,请考虑使用 Spring 的“RequestContextFilter”。筛选器映射依赖于周围的 Web 应用程序配置,因此你必须对其进行适当的更改。下面的清单显示了 Web 应用程序的过滤器部分: + +``` + + ... + + requestContextFilter + org.springframework.web.filter.RequestContextFilter + + + requestContextFilter + /* + + ... + +``` + +`DispatcherServlet`,`RequestContextListener`,和`RequestContextFilter`都做完全相同的事情,即将 HTTP 请求对象绑定到服务该请求的`Thread`。这使得请求和会话范围的 bean 可以在调用链的更下游使用。 + +##### 请求范围 + +考虑 Bean 定义的以下 XML 配置: + +``` + +``` + +Spring 容器通过对每个 HTTP 请求使用 `LoginAction’ Bean 定义来创建`LoginAction` Bean 的新实例。也就是说,“LoginAction” Bean 的作用域在 HTTP 请求级别。你可以随意更改所创建实例的内部状态,因为从相同的`loginAction` Bean 定义创建的其他实例在状态上看不到这些更改。它们是针对个人要求的。当请求完成处理时, Bean 范围内的请求被丢弃。 + +当使用注释驱动的组件或 Java 配置时,`@RequestScope`注释可用于将组件分配给`request`范围。下面的示例展示了如何做到这一点: + +Java + +``` +@RequestScope +@Component +public class LoginAction { + // ... +} +``` + +Kotlin + +``` +@RequestScope +@Component +class LoginAction { + // ... +} +``` + +##### 会话范围 + +考虑 Bean 定义的以下 XML 配置: + +``` + +``` + +Spring 容器通过对单个 HTTP的生命期使用 `userPreferences’ Bean 定义来创建 Bean 的新实例。换句话说,`userPreferences` Bean 在 http`Session`级别有效地起作用。与请求范围的 bean 一样,你可以更改所创建的实例的内部状态,只要你愿意,就可以知道其他 HTTP实例也在使用从相同的 Bean 定义创建的实例,它们在状态上看不到这些更改,因为它们是特定于单个 http`Session`的。当 HTTP`Session`最终被丢弃时,作用域为该特定 HTTP`session’的 Bean 也将被丢弃。 + +当使用注释驱动的组件或 Java 配置时,你可以使用“@SessionScope”注释将组件分配给`session`范围。 + +Java + +``` +@SessionScope +@Component +public class UserPreferences { + // ... +} +``` + +Kotlin + +``` +@SessionScope +@Component +class UserPreferences { + // ... +} +``` + +##### 应用范围 + +考虑 Bean 定义的以下 XML 配置: + +``` + +``` + +Spring 容器通过对整个 Web 应用程序使用一次“AppPreferences” Bean 定义来创建`AppPreferences` Bean 的新实例。也就是说,“AppPreferences” Bean 的作用域在`ServletContext`级别,并存储为常规的“servletContext”属性。这在某种程度上类似于 Spring 单例 Bean,但在两个重要方面有所不同:它是每个`ServletContext`的单例,而不是每个 Spring `ApplicationContext’(在任何给定的 Web 应用程序中可能有几个),并且它实际上是公开的,因此可以作为`ServletContext`属性显示。 + +当使用注释驱动的组件或 Java 配置时,你可以使用“@ApplicationScope”注释将组件分配给`application`范围。下面的示例展示了如何做到这一点: + +Java + +``` +@ApplicationScope +@Component +public class AppPreferences { + // ... +} +``` + +Kotlin + +``` +@ApplicationScope +@Component +class AppPreferences { + // ... +} +``` + +##### WebSocket 范围 + +WebSocket 范围与 WebSocket 会话的生命周期相关联并应用于 WebSocket 应用程序上的 stomp,有关更多详细信息,请参见[WebSocket scope](web.html#websocket-stomp-websocket-scope)。 + +##### 作为依赖项的作用域 bean + +Spring IoC 容器不仅管理对象的实例化,还管理协作者(或依赖关系)的连接。如果希望将(例如)HTTP 请求作用域 Bean 注入到另一个作用域更长的 Bean 中,则可以选择注入 AOP 代理来代替作用域 Bean。也就是说,你需要注入一个代理对象,该代理对象公开了与作用域对象相同的公共接口,但它也可以从相关作用域(例如 HTTP 请求)检索真实目标对象,并将委托方法调用到真实对象上。 + +| |你还可以在作用域为``、
的 bean 之间使用``,然后与引用一起通过一个中间代理,该代理是可序列化的
,因此能够在反序列化时重新获得目标单例 Bean。

当声明``的作用域< Bean 时,在共享代理上调用的每个方法
都会导致创建一个新的目标实例,然后将
调用转发到该实例。

此外,有作用域的代理并不是以
生命周期安全方式从较短的作用域中访问 bean 的唯一方法。你也可以声明你的注射点(即,将
构造函数或 setter 参数或 autowired 字段)设为`ObjectFactory`,
允许一个`getObject()`调用,以便在需要的每一次
时间按需检索当前实例——而不保留实例或单独存储实例。

作为扩展变体,你可以声明`ObjectProvider`,它提供了
几个额外的访问变量,包括`getIfAvailable`和`getIfUnique`。

这一类型的 JSR-330 变体被称为`Provider`,并与`Provider`声明和相应的`get()`每次检索尝试都调用。
有关 JSR-330 的更多详细信息,请参见[here](#beans-standard-annotations)。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +下面示例中的配置只有一行,但理解其背后的“为什么”和“如何”非常重要: + +``` + + + + + + + (1) + + + + + + + + +``` + +|**1**|定义代理的那条线。| +|-----|--------------------------------| + +要创建这样的代理,你需要在 Bean 定义的范围内插入一个子元素``(参见[选择要创建的代理的类型](#beans-factory-scopes-other-injection-proxies)和[基于 XML 模式的配置](#xsd-schemas))。为什么在`request`、`session`和自定义范围级别范围内的 bean 定义需要``元素?考虑以下单例 Bean 定义,并将其与你需要为上述范围定义的内容进行对比(请注意,以下“用户偏好” Bean 定义目前是不完整的): + +``` + + + + + +``` + +在前面的示例中,单例 Bean(“usermanager”)被注入了对 HTTP`Session`-作用域 Bean(“userpreferences”)的引用。这里的要点是,“usermanager” Bean 是一个单例:每个容器只实例化一次,它的依赖项(在本例中只有一个,`userPreferences` Bean)也只注入一次。这意味着`userManager` Bean 仅对完全相同的`userPreferences`对象(即最初注入它的对象)进行操作。 + +这不是你在将较短的作用域 Bean 注入较长的作用域 Bean 时想要的行为(例如,将一个 HTTP作用域协作 Bean 作为一个依赖项注入到单例 Bean)。相反,你需要一个`userManager`对象,并且,对于一个 http`Session`的生命周期,你需要一个`userPreferences`对象,该对象是特定于 http`Session`的。因此,容器创建了一个对象,该对象公开了与`UserPreferences`类完全相同的公共接口(理想情况下是一个`UserPreferences`实例的对象),它可以从范围机制(HTTP Request,`Session`,等等)中获取真正的 `userPreferences’对象。容器将该代理对象注入到`userManager` Bean 中,该对象不知道该`UserPreferences`引用是代理。在本例中,当“UserManager”实例调用依赖注入的`UserPreferences`对象上的方法时,它实际上是在调用代理上的方法。然后,代理从(在本例中)http`Session`中获取真正的 `userpreferences’对象,并将方法调用委托给检索到的真正的`UserPreferences`对象。 + +因此,在将 `request-` 和`session-scoped`bean 注入协作对象时,你需要以下(正确且完整的)配置,如下例所示: + +``` + + + + + + + +``` + +###### 选择要创建的代理的类型 + +默认情况下,当 Spring 容器为用``元素标记的 Bean 创建代理时,将创建一个基于 CGLIB 的类代理。 + +| |CGLIB 代理仅拦截公共方法调用!不要在这样的代理上调用非公共方法
。它们不会被委托给实际作用域的目标对象。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------| + +或者,你可以通过为`proxy-target-class`元素的`false`属性的值指定`false`,来配置 Spring 容器来为此类作用域 bean 创建标准的基于 JDK 接口的代理。 Classpath 使用基于 JDK 接口的代理意味着你不需要应用程序中的额外库来影响这样的代理。然而,这也意味着范围 Bean 的类必须实现至少一个接口,并且将范围 Bean 注入其中的所有协作者必须通过其接口之一引用 Bean。下面的示例展示了一个基于接口的代理: + +``` + + + + + + + + +``` + +有关选择基于类或基于接口的代理的更详细信息,请参见[代理机制](#aop-proxying)。 + +#### 1.5.5.自定义范围 + +Bean 范围界定机制是可扩展的。你可以定义自己的作用域,甚至重新定义现有的作用域,尽管后者被认为是错误的实践,并且你无法覆盖内置的`singleton`和`prototype`作用域。 + +##### 创建自定义范围 + +要将你的自定义作用域集成到 Spring 容器中,你需要实现 `org.springframework.beans.factory.config.scope` 接口,这将在本节中描述。有关如何实现自己的作用域的想法,请参见 Spring 框架本身和[`Scope`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/factory/config/Scope.html)Javadoc 提供的`Scope`实现,它更详细地解释了你需要实现的方法。 + +`Scope`接口有四个方法来从作用域中获取对象,将它们从作用域中删除,然后销毁它们。 + +例如,会话作用域实现返回会话作用域 Bean(如果不存在,则该方法在将其绑定到会话以供将来引用之后,返回 Bean 的新实例)。以下方法从底层作用域返回对象: + +Java + +``` +Object get(String name, ObjectFactory objectFactory) +``` + +Kotlin + +``` +fun get(name: String, objectFactory: ObjectFactory<*>): Any +``` + +例如,会话范围实现从基础会话中删除会话范围 Bean。应该返回该对象,但是如果没有找到指定名称的对象,则可以返回`null`。以下方法将该对象从基础作用域中删除: + +Java + +``` +Object remove(String name) +``` + +Kotlin + +``` +fun remove(name: String): Any +``` + +以下方法注册了一个回调,当范围被销毁或范围中的指定对象被销毁时,该范围应调用该回调: + +Java + +``` +void registerDestructionCallback(String name, Runnable destructionCallback) +``` + +Kotlin + +``` +fun registerDestructionCallback(name: String, destructionCallback: Runnable) +``` + +有关销毁回调的更多信息,请参见[javadoc](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/factory/config/Scope.html#registerDestructionCallback)或 Spring 范围实现。 + +以下方法获得底层作用域的会话标识符: + +Java + +``` +String getConversationId() +``` + +Kotlin + +``` +fun getConversationId(): String +``` + +这个标识符对于每个作用域都是不同的。对于会话范围的实现,这个标识符可以是会话标识符。 + +##### 使用自定义作用域 + +在编写和测试一个或多个自定义`Scope`实现之后,你需要让 Spring 容器知道你的新作用域。下面的方法是在 Spring 容器中注册一个新的`Scope`的中心方法: + +爪哇 + +``` +void registerScope(String scopeName, Scope scope); +``` + +Kotlin + +``` +fun registerScope(scopeName: String, scope: Scope) +``` + +此方法在`ConfigurableBeanFactory`接口上声明,该接口可通过`BeanFactory`属性在 Spring 附带的大多数具体`ApplicationContext`实现上使用。 + +`registerScope(..)`方法的第一个参数是与作用域关联的唯一名称。 Spring 容器本身中的此类名称的示例是`singleton`和 `prototype’。`registerScope(..)`方法的第二个参数是你希望注册和使用的自定义`Scope`实现的实际实例。 + +假设你编写了你的自定义`Scope`实现,然后将其注册为下一个示例所示。 + +| |下一个示例使用`SimpleThreadScope`,它包含在 Spring 中,但不是默认注册的
。对于你自己的自定义`Scope`实现,指令将是相同的。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +爪哇 + +``` +Scope threadScope = new SimpleThreadScope(); +beanFactory.registerScope("thread", threadScope); +``` + +Kotlin + +``` +val threadScope = SimpleThreadScope() +beanFactory.registerScope("thread", threadScope) +``` + +然后,你可以创建遵循自定义“范围”的范围规则的 Bean 定义,如下所示: + +``` + +``` + +使用自定义的`Scope`实现,你不仅限于编程注册的范围。你还可以使用“CustomScopeConfigurer”类,以声明式的方式进行`Scope`注册,如下例所示: + +``` + + + + + + + + + + + + + + + + + + + + + + + +``` + +| |当将``放置在 `FactoryBean’实现的``声明中时,作用域是工厂 Bean 本身,而不是从`getObject()`返回的对象
。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 1.6.自定义 A 的性质 Bean + +Spring 框架提供了许多接口,你可以使用这些接口来自定义 Bean 的性质。本节将其分类如下: + +* [生命周期回调](#beans-factory-lifecycle) + +* [`ApplicationContextAware` and `BeanNameAware`](#beans-factory-aware) + +* [Other `Aware` Interfaces](#aware-list) + +#### 1.6.1.生命周期回调 + +要与容器的 Bean 生命周期管理交互,你可以实现 Spring `InitializingBean`和`DisposableBean`接口。容器为前者调用 `AfterPropertiesset()`,为后者调用`destroy()`,让 Bean 在初始化和销毁 bean 时执行某些操作。 + +| |JSR-250`@PostConstruct`和`@PreDestroy`注释通常被认为是在现代 Spring 应用程序中接收生命周期回调的最佳
实践。使用这些
注释意味着你的 bean 没有耦合到 Spring 特定的接口。
有关详细信息,请参见[Using `@PostConstruct` and `@PreDestroy`](#beans-postconstruct-and-predestroy-annotations)。

如果你不想使用 JSR-250 注释,但仍然希望删除
耦合,请考虑`init-method`和 +``` + +爪哇 + +``` +public class ExampleBean { + + public void init() { + // do some initialization work + } +} +``` + +Kotlin + +``` +class ExampleBean { + + fun init() { + // do some initialization work + } +} +``` + +前面的示例与下面的示例(由两个列表组成)的效果几乎完全相同: + +``` + +``` + +爪哇 + +``` +public class AnotherExampleBean implements InitializingBean { + + @Override + public void afterPropertiesSet() { + // do some initialization work + } +} +``` + +Kotlin + +``` +class AnotherExampleBean : InitializingBean { + + override fun afterPropertiesSet() { + // do some initialization work + } +} +``` + +然而,前面的两个示例中的第一个不将代码与 Spring 耦合。 + +##### 销毁回调 + +实现`org.springframework.beans.factory.DisposableBean`接口可以让 Bean 在包含它的容器被销毁时获得回调。“DisposableBean”接口指定了一个方法: + +``` +void destroy() throws Exception; +``` + +我们建议你不要使用`DisposableBean`回调接口,因为它不必要地将代码耦合到 Spring。或者,我们建议使用[`@PreDestroy`](#beans-postconstruct-and-predestroy-annotations)注释或指定 Bean 定义支持的通用方法。对于基于 XML 的配置元数据,你可以在``上使用`destroy-method`属性。使用 爪哇 配置,可以使用`@Bean`的`destroyMethod`属性。见[接收生命周期回调](#beans-java-lifecycle-callbacks)。考虑以下定义: + +``` + +``` + +爪哇 + +``` +public class ExampleBean { + + public void cleanup() { + // do some destruction work (like releasing pooled connections) + } +} +``` + +Kotlin + +``` +class ExampleBean { + + fun cleanup() { + // do some destruction work (like releasing pooled connections) + } +} +``` + +上述定义与以下定义的效果几乎完全相同: + +``` + +``` + +爪哇 + +``` +public class AnotherExampleBean implements DisposableBean { + + @Override + public void destroy() { + // do some destruction work (like releasing pooled connections) + } +} +``` + +Kotlin + +``` +class AnotherExampleBean : DisposableBean { + + override fun destroy() { + // do some destruction work (like releasing pooled connections) + } +} +``` + +然而,前面的两个定义中的第一个并不将代码耦合到 Spring。 + +| |你可以为``元素的`destroy-method`属性分配一个特殊的 `(推断)’值,该值指示 Spring 在特定的 Bean 类上自动检测一个公共的`close`或 `shutdown’方法。(因此,任何实现 `java.lang.autocloseable’或`java.io.Closeable`的类都将匹配。)你还可以在
元素的`default-destroy-method`属性上设置这个特殊的`(inferred)`值,以将此行为应用到整个 bean 集(参见[默认的初始化和销毁方法](#beans-factory-lifecycle-default-init-destroy-methods))。请注意,这是使用 爪哇 配置的
默认行为。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 默认的初始化和销毁方法 + +当你编写初始化并销毁不使用 Spring 特定的`InitializingBean`和`DisposableBean`回调接口的方法回调时,通常使用`init()`、`initialize()`、`dispose()`等名称编写方法。理想情况下,这样的生命周期回调方法的名称在整个项目中是标准化的,以便所有开发人员使用相同的方法名称并确保一致性。 + +你可以将 Spring 容器配置为“查找”命名的初始化,并在每个 Bean 上销毁回调方法名称。这意味着,作为应用程序开发人员,你可以编写应用程序类并使用名为 `init()’的初始化回调,而无需为每个 Bean 定义配置`init-method="init"`属性。 Spring IOC 容器在创建 Bean 时调用该方法(并且根据标准的生命周期回调契约[先前描述过](#beans-factory-lifecycle))。此特性还强制执行一个用于初始化和销毁方法回调的一致的命名约定。 + +假设你的初始化回调方法名为`init()`,而销毁回调方法名为`destroy()`。在下面的示例中,你的类与类类似: + +爪哇 + +``` +public class DefaultBlogService implements BlogService { + + private BlogDao blogDao; + + public void setBlogDao(BlogDao blogDao) { + this.blogDao = blogDao; + } + + // this is (unsurprisingly) the initialization callback method + public void init() { + if (this.blogDao == null) { + throw new IllegalStateException("The [blogDao] property must be set."); + } + } +} +``` + +Kotlin + +``` +class DefaultBlogService : BlogService { + + private var blogDao: BlogDao? = null + + // this is (unsurprisingly) the initialization callback method + fun init() { + if (blogDao == null) { + throw IllegalStateException("The [blogDao] property must be set.") + } + } +} +``` + +然后,你可以在类似于以下内容的 Bean 中使用该类: + +``` + + + + + + + +``` + +顶层`default-init-method`元素属性上的`default-init-method`属性的存在导致 Spring IoC 容器将 Bean 类上的一个名为`init`的方法识别为初始化方法回调。当 Bean 被创建和组装时,如果 Bean 类具有这样的方法,则在适当的时间调用它。 + +通过使用顶层``元素上的 `default-destroy-method’属性,可以类似地(在 XML 中)配置 destroy 方法回调。 + +如果现有的 Bean 类已经具有根据约定命名的回调方法,则可以通过使用`init-method`本身的``属性指定(在 XML 中,即)方法名来覆盖缺省的方法。 + +Spring 容器保证在 Bean 提供了所有依赖项之后立即调用已配置的初始化回调。因此,在 Raw Bean 引用上调用初始化回调,这意味着 AOP 拦截器等等尚未应用到 Bean。首先完全创建目标 Bean,然后应用具有拦截链的 AOP 代理(例如)。如果目标 Bean 和代理是分开定义的,那么你的代码甚至可以绕过代理与原始目标 Bean 进行交互。因此,将拦截器应用于`init`方法将是不一致的,因为这样做将把目标 Bean 的生命周期与其代理或拦截器耦合在一起,并在你的代码直接与原始目标 Bean 交互时留下奇怪的语义。 + +##### 结合生命周期机制 + +从 Spring 2.5 开始,你有三种选择来控制 Bean 生命周期行为: + +* [“初始化 bean”](#beans-factory-lifecycle-initializingbean)和[`DisposableBean’](#beans-factory-lifecycle-disposablebean)回调接口 + +* 自定义`init()`和`destroy()`方法 + +* [`@PostConstruct` and `@PreDestroy`annotations](#beans-postconstruct-and-predestroy-annotations)。你可以结合这些机制来控制给定的 Bean。 + +| |如果为 Bean 配置了多个生命周期机制,并且每个机制
配置了不同的方法名,那么每个配置的方法都是在
顺序中运行的,顺序在此注释之后列出。但是,如果为这些生命周期机制中的一个以上配置了相同的方法名(例如,初始化方法的’init()’),则
该方法将运行一次,如[前一节](#beans-factory-lifecycle-default-init-destroy-methods)中所解释的那样。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +Bean 使用不同的初始化方法为相同的 Bean 配置的多个生命周期机制调用如下: + +1. 用`@PostConstruct`注释的方法 + +2. `afterPropertiesSet()`由`InitializingBean`回调接口定义 + +3. 自定义配置的`init()`方法 + +销毁方法的调用顺序相同: + +1. 用`@PreDestroy`注释的方法 + +2. `destroy()`由`DisposableBean`回调接口定义 + +3. 自定义配置的`destroy()`方法 + +##### 启动和关闭回调 + +`Lifecycle`接口定义了任何对象的基本方法,这些对象具有自己的生命周期要求(例如启动和停止某些后台进程): + +``` +public interface Lifecycle { + + void start(); + + void stop(); + + boolean isRunning(); +} +``` + +任何 Spring-托管对象都可以实现`Lifecycle`接口。然后,当“ApplicationContext”本身接收开始和停止信号(例如,对于运行时的停止/重新启动场景)时,它将这些调用级联到在该上下文中定义的所有`Lifecycle`实现。它通过委托给`LifecycleProcessor`来实现这一点,如下面的清单所示: + +``` +public interface LifecycleProcessor extends Lifecycle { + + void onRefresh(); + + void onClose(); +} +``` + +注意,`LifecycleProcessor`本身是`Lifecycle`接口的扩展。它还添加了另外两个方法,用于对正在刷新和关闭的上下文做出反应。 + +| |请注意,常规的`org.springframework.context.Lifecycle`接口是用于显式启动和停止通知的普通
契约,并不意味着在上下文
刷新时间自动启动。对于特定 Bean 的自动启动(包括启动阶段)的细粒度控制,
可以考虑实现`org.springframework.context.SmartLifecycle`,而不是,

此外,请注意,停止通知不能保证在销毁之前发出。
在常规关闭时,所有`Lifecycle`bean 在
一般销毁回调被传播之前首先收到停止通知。但是,在
上下文的生命期内进行热刷新或停止刷新尝试时,只调用销毁方法。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +启动和关闭调用的顺序可能很重要。如果在任何两个对象之间存在“依赖”关系,则依赖方在其依赖项之后开始,在其依赖项之前停止。然而,有时,直接的依赖关系是未知的。你可能只知道,某种类型的对象应该先于另一种类型的对象启动。在这些情况下,`SmartLifecycle`接口定义了另一个选项,即在其超级接口上定义的`getPhase()`方法,即 ` 分阶段’。下面的清单显示了`Phased`接口的定义: + +``` +public interface Phased { + + int getPhase(); +} +``` + +下面的清单显示了`SmartLifecycle`接口的定义: + +``` +public interface SmartLifecycle extends Lifecycle, Phased { + + boolean isAutoStartup(); + + void stop(Runnable callback); +} +``` + +启动时,相位最低的对象首先启动。停止时,将遵循相反的顺序。因此,一个实现`SmartLifecycle`并且其`getPhase()`方法返回`Integer.MIN_VALUE`的对象将是最先启动和最后停止的对象。在频谱的另一端,“integer.max_value”的相位值将表示对象应该最后启动并首先停止(可能是因为它依赖于要运行的其他进程)。当考虑相位值时,同样重要的是要知道,对于任何不实现`SmartLifecycle`的“正常”“生命周期”对象,缺省阶段是`0`。因此,任何负相位值都表示对象应该在这些标准组件之前启动(在它们之后停止)。对于任何正相位值,反之亦然。 + +由`SmartLifecycle`定义的停止方法接受回调。在该实现的关闭过程完成之后,任何实现都必须调用该回调的`run()`方法。这将在必要时支持异步关闭,因为`LifecycleProcessor`接口的默认实现 `DefaultLifecycleProcessor’在每个阶段中等待对象组的超时值来调用该回调。默认的每阶段超时是 30 秒。你可以通过在上下文中定义一个名为“LifecycleProcessor”的 Bean 来覆盖默认的生命周期处理器实例。如果你只想修改超时,定义以下内容就足够了: + +``` + + + + +``` + +如前所述,`LifecycleProcessor`接口还定义了用于刷新和关闭上下文的回调方法。后者驱动关机过程,就好像`stop()`已被显式调用一样,但它发生在上下文关闭时。另一方面,“刷新”回调启用了“smartlifecycle”bean 的另一项功能。当刷新上下文时(在所有对象都已实例化和初始化之后),将调用该回调。这时,默认的生命周期处理器会检查每个“SmartLifecycle”对象的`isAutoStartup()`方法返回的布尔值。如果`true`,则该对象在该点启动,而不是等待对上下文或其自身的`start()`方法的显式调用(与上下文刷新不同,对于标准的上下文实现,上下文启动不会自动发生)。正如前面所描述的,`phase`值和任何“依赖”关系决定启动顺序。 + +##### 在非 Web 应用程序中优雅地关闭 Spring IoC 容器 + +| |本节仅适用于非 Web 应用程序。 Spring 的基于 Web 的“ApplicationContext”实现已经有了适当的代码,可以在相关 Web 应用程序关闭时优雅地关闭
Spring IoC 容器。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +如果在非 Web 应用程序环境(例如,在富客户端桌面环境中)中使用 Spring 的 IoC 容器,请向 JVM 注册一个关闭钩子。这样做可以确保良好的关机,并在单例 bean 上调用相关的销毁方法,以便释放所有资源。你仍然必须正确地配置和实现这些 destroy 回调。 + +要注册关机钩子,请调用在`ConfigurableApplicationContext`接口上声明的`registerShutdownHook()`方法,如下例所示: + +爪哇 + +``` +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +public final class Boot { + + public static void main(final String[] args) throws Exception { + ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml"); + + // add a shutdown hook for the above context... + ctx.registerShutdownHook(); + + // app runs here... + + // main method exits, hook is called prior to the app shutting down... + } +} +``` + +Kotlin + +``` +import org.springframework.context.support.ClassPathXmlApplicationContext + +fun main() { + val ctx = ClassPathXmlApplicationContext("beans.xml") + + // add a shutdown hook for the above context... + ctx.registerShutdownHook() + + // app runs here... + + // main method exits, hook is called prior to the app shutting down... +} +``` + +#### 1.6.2.`ApplicationContextAware`和`BeanNameAware` + +当`ApplicationContext`创建一个实现 `org.springframework.context.ApplicationContextAware` 接口的对象实例时,将为该实例提供一个对`ApplicationContext`的引用。下面的清单显示了`ApplicationContextAware`接口的定义: + +``` +public interface ApplicationContextAware { + + void setApplicationContext(ApplicationContext applicationContext) throws BeansException; +} +``` + +因此,bean 可以通过`ApplicationContext`接口或通过对该接口的一个已知子类(例如`ConfigurableApplicationContext`,该接口公开了额外的功能)的引用,以编程方式操作创建它们的`ApplicationContext`。一种用途是对其他 bean 进行程序化检索。有时这种能力是有用的。但是,通常情况下,你应该避免它,因为它将代码与 Spring 耦合,并且不遵循控制样式的反转,在这种情况下,协作者被提供给 bean 作为属性。“ApplicationContext”的其他方法提供对文件资源的访问、发布应用程序事件以及访问`MessageSource`。这些附加特征在[Additional Capabilities of the `ApplicationContext`](#context-introduction)中进行了描述。 + +自动布线是获得对“ApplicationContext”的引用的另一种选择。*传统的*`constructor`和`byType`自动布线模式(如[自动布线合作者](#beans-factory-autowire)中所述)可以分别为构造函数参数或 setter 方法参数提供类型 `ApplicationContext’的依赖项。为了具有更大的灵活性,包括能够自动连接字段和多个参数的方法,可以使用基于注释的自动连接功能。如果你这样做,`ApplicationContext`将自动连接到一个字段、构造函数参数或方法参数中,如果所讨论的字段、构造函数或方法带有`ApplicationContext`注释,则该参数将需要`@Autowired`类型。有关更多信息,请参见[Using `@Autowired`](#beans-autowired-annotation)。 + +当`ApplicationContext`创建一个实现 `org.springframework.beans.factory.beannamaware` 接口的类时,将为该类提供一个对其关联对象定义中定义的名称的引用。下面的清单显示了 BeannaMeAware 接口的定义: + +``` +public interface BeanNameAware { + + void setBeanName(String name) throws BeansException; +} +``` + +回调是在正常 Bean 属性的填充之后,但在初始化回调(如`InitializingBean.afterPropertiesSet()`或自定义 init-method)之前调用的。 + +#### 1.6.3.其它`Aware`接口 + +除了`ApplicationContextAware`和`BeanNameAware`(讨论了[earlier](#beans-factory-aware))之外, Spring 还提供了一系列`Aware`回调接口,这些接口使 bean 向容器表明它们需要某种基础设施依赖关系。作为一般规则,名称指示依赖类型。下表总结了最重要的`Aware`接口: + +| Name |注入依赖性| Explained in…​ | +|--------------------------------|-----------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------| +| `ApplicationContextAware` |声明`ApplicationContext`。| [`ApplicationContextAware` and `BeanNameAware`](#beans-factory-aware) | +|`ApplicationEventPublisherAware`|附件`ApplicationContext`的事件发布者。|[Additional Capabilities of the `ApplicationContext`](#context-introduction)| +| `BeanClassLoaderAware` |类装入器用于装入 Bean 类。| [Instantiating Beans](#beans-factory-class) | +| `BeanFactoryAware` |声明`BeanFactory`。| [The `BeanFactory`](#beans-beanfactory) | +| `BeanNameAware` |声明的名称 Bean。| [`ApplicationContextAware` and `BeanNameAware`](#beans-factory-aware) | +| `LoadTimeWeaverAware` |定义了用于在加载时处理类定义的 Weaver。| [Load-time Weaving with AspectJ in the Spring Framework](#aop-aj-ltw) | +| `MessageSourceAware` |用于解析消息的已配置策略(支持参数化和
国际化)。|[Additional Capabilities of the `ApplicationContext`](#context-introduction)| +| `NotificationPublisherAware` |Spring JMX 通知发布者。| [Notifications](integration.html#jmx-notifications) | +| `ResourceLoaderAware` |为低级访问资源配置了加载器。| [Resources](#resources) | +| `ServletConfigAware` |current`ServletConfig`容器运行。只有在网络感知的“应用上下文”中才有效 Spring。| [Spring MVC](web.html#mvc) | +| `ServletContextAware` |current`ServletContext`容器运行。仅在网络感知的“应用上下文”中有效 Spring。| [Spring MVC](web.html#mvc) | + +再次注意,使用这些接口将你的代码绑定到 Spring API,并且不遵循控制样式的反转。因此,对于需要对容器进行编程访问的基础设施 bean,我们推荐使用它们。 + +### 1.7. Bean 定义继承 + +Bean 定义可以包含大量的配置信息,包括构造函数参数、属性值和特定于容器的信息,例如初始化方法、静态工厂方法名称等。 Bean 子定义从父定义继承配置数据。子定义可以根据需要覆盖某些值或添加其他值。 Bean 使用父类和子类定义可以节省大量的输入。实际上,这是模板化的一种形式。 + +如果以编程方式处理`ApplicationContext`接口,则子 Bean 定义由`ChildBeanDefinition`类表示。大多数用户不会在这个级别上与他们合作。相反,它们以声明性的方式在一个类中配置 Bean 定义,例如`ClassPathXmlApplicationContext`。当你使用基于 XML 的配置元数据时,你可以通过使用`parent`属性来指示一个子 Bean 定义,并指定父 Bean 作为该属性的值。下面的示例展示了如何做到这一点: + +``` + + + + + + (1) + + + +``` + +|**1**|注意`parent`属性。| +|-----|----------------------------| + +子 Bean 定义使用来自父定义的 Bean 类,如果没有指定,但也可以覆盖它。在后一种情况下,子 Bean 类必须与父类兼容(也就是说,它必须接受父的属性值)。 + +Bean 子定义从父定义继承范围、构造函数参数值、属性值和方法重写,并带有添加新值的选项。指定覆盖相应父设置的任何作用域、初始化方法、销毁方法或`static`工厂方法设置。 + +其余的设置总是从子定义中获取:Dependent on、AutoWire 模式、依赖检查、单例和惰性 init。 + +前面的示例通过使用`abstract`属性显式地将父 Bean 定义标记为抽象。如果父定义没有指定类,则需要显式地将父 Bean 定义标记为`abstract`,如下例所示: + +``` + + + + + + + + + +``` + +父 Bean 不能单独实例化,因为它是不完整的,并且它也显式地标记为`abstract`。当一个定义是`abstract`时,它只能作为一个纯粹的模板 Bean 定义使用,作为子定义的父定义。试图单独使用这样的`abstract`父 Bean,方法是将其称为另一个 Bean 的 ref 属性,或者使用父 Bean ID 执行显式的`getBean()`调用,返回一个错误。类似地,容器的内部“preinstantiatesingletons()”方法忽略了定义为抽象的 Bean 定义。 + +| |`ApplicationContext`默认情况下预先实例化所有单例。因此,如果你有一个(父) Bean 定义
,并且只打算将其用作模板,并且该定义指定了一个类,那么
是很重要的(至少对于单例 bean 来说是如此),你必须确保将*摘要*属性设置为*true*,否则,应用程序
上下文实际上(试图)预先实例化`abstract` Bean。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 1.8.容器延伸点 + +通常,应用程序开发人员不需要子类`ApplicationContext`实现类。相反, Spring IOC 容器可以通过插入特殊集成接口的实现来扩展。接下来的几节将描述这些集成接口。 + +#### 1.8.1.使用`BeanPostProcessor`自定义 bean + +`BeanPostProcessor`接口定义了回调方法,你可以实现这些方法来提供你自己的(或覆盖容器的默认)实例化逻辑、依赖项解析逻辑,等等。如果希望在 Spring 容器完成实例、配置和初始化 Bean 之后实现一些自定义逻辑,则可以插入一个或多个自定义`BeanPostProcessor`实现。 + +你可以配置多个`BeanPostProcessor`实例,并且可以通过设置`order`属性来控制这些`BeanPostProcessor`实例的运行顺序。只有当`BeanPostProcessor`实现`Ordered`接口时,才可以设置此属性。如果你编写自己的`BeanPostProcessor`,那么也应该考虑实现`Ordered`接口。有关更多详细信息,请参见[“BeanPostProcessor”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/factory/config/BeanPostProcessor.html)和[`Ordered`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/core/Ordered.html)接口的 爪哇doc。另见[programmatic registration of `BeanPostProcessor` instances](#beans-factory-programmatically-registering-beanpostprocessors)的注释。 + +| |`BeanPostProcessor`实例对 Bean(或对象)实例进行操作。即,
Spring IOC 容器实例化一个 Bean 实例,然后`BeanPostProcessor`实例执行它们的工作。

“BeanPostProcessor”实例是每个容器的作用域。只有当你
使用容器层次结构时,这才是相关的。如果你在一个容器中定义了`BeanPostProcessor`,则
只对该容器中的 bean 进行后处理。换句话说,在一个容器中定义
的 bean 不会被在
中定义的`BeanPostProcessor`另一个容器进行后处理,即使这两个容器都是同一层次结构的一部分。

来更改实际的 Bean 定义(即,定义 Bean)、
的蓝图需要使用`BeanFactoryPostProcessor`,如[Customizing Configuration Metadata with a `BeanFactoryPostProcessor`](#beans-factory-extension-factory-postprocessors)中所述。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +`org.springframework.beans.factory.config.BeanPostProcessor`接口正好由两个回调方法组成。当这样的类在容器中注册为后处理器时,对于由容器创建的每个 Bean 实例,后处理器在调用容器初始化方法(例如`InitializingBean.afterPropertiesSet()`或任何声明的`init`方法)之前都会从容器获得一个回调,并且在任何 Bean 初始化回调之后。后处理器可以对 Bean 实例采取任何操作,包括完全忽略回调。 Bean 后处理器通常检查回调接口,或者它可以用代理包装 Bean。一些 Spring AOP 基础设施类被实现为 Bean 后处理器,以便提供代理包装逻辑。 + +`ApplicationContext`会自动检测在实现`BeanPostProcessor`接口的配置元数据中定义的任何 bean。“ApplicationContext”将这些 bean 注册为后处理程序,以便稍后在 Bean 创建时调用它们。 Bean 后处理器可以以与任何其他 bean 相同的方式部署在容器中。 + +注意,当通过在配置类上使用`@Bean`工厂方法声明`BeanPostProcessor`时,工厂方法的返回类型应该是实现类本身,或者至少是`org.springframework.beans.factory.config.BeanPostProcessor`接口,这清楚地表明了该 Bean 的后处理器性质。否则,“ApplicationContext”无法在完全创建它之前按类型自动检测它。由于`BeanPostProcessor`需要早期实例化,以应用于上下文中其他 bean 的初始化,因此这种早期类型检测非常关键。 + +| |通过编程方式注册`BeanPostProcessor`实例

虽然推荐的`BeanPostProcessor`注册方法是通过 `ApplicationContext’自动检测(如前所述),但你可以通过使用`addBeanPostProcessor`方法对`ConfigurableBeanFactory`实例进行编程注册。当你需要在
注册之前计算条件逻辑时,或者甚至在层次结构中跨上下文复制 Bean 后处理程序时,这可能是有用的。
但是,注意,以编程方式添加的`BeanPostProcessor`实例并不尊重
的`Ordered`接口。在这里,登记的顺序决定了执行的顺序
。还请注意,无论是否有任何
显式排序,以编程方式注册的
实例总是在通过自动检测注册的实例之前进行处理。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |`BeanPostProcessor`实例和 AOP 自动代理
实现
接口的`BeanPostProcessor`类是特殊的,并且被容器以不同的方式处理
。所有`BeanPostProcessor`实例和它们
直接引用的 bean 都在启动时实例化,作为`ApplicationContext`的特殊启动阶段
的一部分。接下来,以排序的方式注册所有`BeanPostProcessor`实例
,并将其应用于容器中的所有其他 bean。因为
自动代理是作为`BeanPostProcessor`本身实现的,所以`BeanPostProcessor`实例和它们直接引用的 bean 都不符合自动代理的条件,并且,
因此,它们不具有交织在其中的方面。,对于任何这样的 Bean,

,你应该会看到一个信息日志消息:`Bean someBean is not
eligible for getting processed by all BeanPostProcessor interfaces (for example: not
eligible for auto-proxying)`。

如果你的`BeanPostProcessor`中连接了 bean,使用自动连接或 `@resource’(这可能会退回到自动连接), Spring 在搜索类型匹配的依赖项时可能会访问意外的 bean
,因此,使它们
没有资格进行自动代理或其他类型的 Bean 后处理。例如,如果
有一个用`@Resource`注释的依赖项,其中字段或 setter 名称不
直接对应于 Bean 的声明名称,并且不使用 name 属性,则
Spring 访问其他 bean 以通过类型匹配它们。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +下面的示例展示了如何在`ApplicationContext`中编写、注册和使用`BeanPostProcessor`实例。 + +##### 例如:Hello World,`BeanPostProcessor`-style + +第一个例子说明了基本用法。该示例显示了一个自定义的“BeanPostProcessor”实现,该实现在容器创建每个 Bean 的时候调用`toString()`方法,并将生成的字符串打印到系统控制台。 + +下面的清单显示了自定义`BeanPostProcessor`实现类定义: + +爪哇 + +``` +package scripting; + +import org.springframework.beans.factory.config.BeanPostProcessor; + +public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor { + + // simply return the instantiated bean as-is + public Object postProcessBeforeInitialization(Object bean, String beanName) { + return bean; // we could potentially return any object reference here... + } + + public Object postProcessAfterInitialization(Object bean, String beanName) { + System.out.println("Bean '" + beanName + "' created : " + bean.toString()); + return bean; + } +} +``` + +Kotlin + +``` +import org.springframework.beans.factory.config.BeanPostProcessor + +class InstantiationTracingBeanPostProcessor : BeanPostProcessor { + + // simply return the instantiated bean as-is + override fun postProcessBeforeInitialization(bean: Any, beanName: String): Any? { + return bean // we could potentially return any object reference here... + } + + override fun postProcessAfterInitialization(bean: Any, beanName: String): Any? { + println("Bean '$beanName' created : $bean") + return bean + } +} +``` + +下面的`beans`元素使用`InstantiationTracingBeanPostProcessor`: + +``` + + + + + + + + + + + +``` + +注意`InstantiationTracingBeanPostProcessor`仅仅是如何定义的。它甚至没有名称,并且,由于它是一个 Bean,所以它可以像其他任何 Bean 一样被注入依赖关系。(前面的配置还定义了由 Groovy 脚本支持的 Bean。 Spring 动态语言支持在标题为[动态语言支持](languages.html#dynamic-language)的章节中进行了详细介绍。 + +下面的 爪哇 应用程序运行前面的代码和配置: + +爪哇 + +``` +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.scripting.Messenger; + +public final class Boot { + + public static void main(final String[] args) throws Exception { + ApplicationContext ctx = new ClassPathXmlApplicationContext("scripting/beans.xml"); + Messenger messenger = ctx.getBean("messenger", Messenger.class); + System.out.println(messenger); + } + +} +``` + +Kotlin + +``` +import org.springframework.beans.factory.getBean + +fun main() { + val ctx = ClassPathXmlApplicationContext("scripting/beans.xml") + val messenger = ctx.getBean("messenger") + println(messenger) +} +``` + +上述应用程序的输出类似于以下内容: + +``` +Bean 'messenger' created : [email protected] +[email protected] +``` + +##### 示例:`AutowiredAnnotationBeanPostProcessor` + +结合自定义`BeanPostProcessor`实现使用回调接口或注释是扩展 Spring IoC 容器的一种常见方法。一个例子是 Spring 的`AutowiredAnnotationBeanPostProcessor`—一个`BeanPostProcessor`实现,该实现附带 Spring 分发和 AutoWires 注释字段、setter 方法和任意配置方法。 + +#### 1.8.2.使用`BeanFactoryPostProcessor`自定义配置元数据 ### + +我们看的下一个扩展点是“org.springframework.beans.factory.config.BeanFactoryPostprocessor”。该接口的语义类似于`BeanPostProcessor`的语义,但有一个主要区别:`BeanFactoryPostProcessor`对 Bean 配置元数据进行操作。也就是说, Spring IOC 容器允许`BeanFactoryPostProcessor`读取配置元数据,并可能更改它*在此之前*容器实例化除`BeanFactoryPostProcessor`实例之外的任何 bean。 + +你可以配置多个`BeanFactoryPostProcessor`实例,并且可以通过设置`order`属性来控制这些`BeanFactoryPostProcessor`实例的运行顺序。但是,只有当`BeanFactoryPostProcessor`实现了 `Order’接口时,才能设置此属性。如果你编写自己的`BeanFactoryPostProcessor`,那么你也应该考虑实现`Ordered`接口。有关更多详细信息,请参见[“BeanFactoryPostProcessor”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/factory/config/BeanFactoryPostProcessor.html)和[`Ordered`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/core/Ordered.html)接口的 爪哇doc。 + +| |如果要更改实际的 Bean 实例(即,从配置元数据创建
的对象),则需要使用`BeanPostProcessor`(在[Customizing Beans by Using a `BeanPostProcessor`](#beans-factory-extension-bpp)中进行了描述)。虽然在技术上可以
处理`BeanFactoryPostProcessor`内的 Bean 实例(例如,通过使用’BeanFactory.GetBean()’),但这样做会导致过早的 Bean 实例化,违反
标准容器生命周期。这可能会导致负面的副作用,例如绕过
Bean 后处理。

同样,`BeanFactoryPostProcessor`实例是每个容器的作用域。如果使用容器层次结构,这只与
相关。如果你在一个
容器中定义了`BeanFactoryPostProcessor`,则它仅应用于该容器中的 Bean 定义。 Bean 在一个容器中的定义
不被在另一个
容器中的`BeanFactoryPostProcessor`实例进行后处理,即使这两个容器都是同一层次结构的一部分。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +Bean 工厂后处理器在“ApplicationContext”内声明时自动运行,以便对定义容器的配置元数据应用更改。 Spring 包括一些预定义的 Bean 工厂后处理器,例如`PropertyOverrideConfigurer`和 `PropertySourcesPlaceHolderConfigurer’。你还可以使用自定义`BeanFactoryPostProcessor`——例如,注册自定义属性编辑器。 + +`ApplicationContext`会自动检测部署到其中的任何 bean,这些 bean 实现`BeanFactoryPostProcessor`接口。它在适当的时候使用这些 bean 作为 Bean 工厂的后处理器。你可以部署这些后处理器 bean,就像部署任何其他的 Bean。 + +| |与`BeanPostProcessor`s 一样,你通常不希望为延迟初始化配置“BeanFactoryPostProcessor”。如果没有其他 Bean 引用 ` Bean(工厂)后处理器 `,则该后处理器将根本不会实例化。因此,将其标记为惰性初始化将被忽略,并且 ` Bean(工厂)后处理器 ` 将被急切地实例化,即使你在你的``元素的声明上将 `default-lazy-init` 属性设置为`true`。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 示例:类名替换`PropertySourcesPlaceholderConfigurer`##### + +通过使用标准的 爪哇`Properties`格式,可以使用`PropertySourcesPlaceholderConfigurer`将 Bean 定义中的属性值外部化到单独的文件中。这样做可以使部署应用程序的人员定制特定于环境的属性,例如数据库 URL 和密码,而无需修改主 XML 定义文件或容器的文件的复杂性或风险。 + +考虑以下基于 XML 的配置元数据片段,其中定义了带有占位值的`DataSource`: + +``` + + + + + + + + + + +``` + +该示例显示了从外部`Properties`文件配置的属性。在运行时,将`PropertySourcesPlaceholderConfigurer`应用于元数据,以替换数据源的某些属性。要替换的值被指定为`${property-name}`形式的占位符,它遵循 Ant 和 log4j 和 jsp el 样式。 + +实际值来自另一个标准 爪哇`Properties`格式的文件: + +``` +jdbc.driverClassName=org.hsqldb.jdbcDriver +jdbc.url=jdbc:hsqldb:hsql://production:9002 +jdbc.username=sa +jdbc.password=root +``` + +因此,`${jdbc.username}`字符串在运行时被替换为值“sa”,这同样适用于匹配属性文件中的键的其他占位符。在 Bean 定义的大多数属性和属性中,`PropertySourcesPlaceholderConfigurer`检查占位符。此外,你还可以自定义占位符前缀和后缀。 + +使用 Spring 2.5 中引入的`context`名称空间,你可以使用一个专用的配置元素来配置属性占位符。可以在`location`属性中以逗号分隔的列表形式提供一个或多个位置,如下例所示: + +``` + +``` + +`PropertySourcesPlaceholderConfigurer`不仅在你指定的`Properties`文件中查找属性。默认情况下,如果它在指定的属性文件中找不到属性,它将检查 Spring `Environment`属性和常规的 爪哇`System`属性。 + +| |可以使用`PropertySourcesPlaceholderConfigurer`替换类名称,其中
有时是有用的当你必须在运行时选择一个特定的实现类时。
下面的示例显示了如何做到:

`



<><1995"gt=">>><“tcase”/>>>>>“tcase的解析在它即将被创建时失败,这是在`preInstantiateSingletons()`对于非惰性-init 的`ApplicationContext`阶段期间 Bean。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 示例:`PropertyOverrideConfigurer` + +另一个 Bean 工厂后处理器`PropertyOverrideConfigurer`类似于 `PropertySourcesPlaceHolderConfigurer’,但与后者不同的是,对于 Bean 属性,原始定义可以有默认值,也可以完全没有值。如果重写的 `properties’文件中没有某个 Bean 属性的条目,则使用默认的上下文定义。 + +请注意, Bean 定义并不知道是否被重写,因此从 XML 定义文件中并不能立即看出是否正在使用重写配置器。在多个`PropertyOverrideConfigurer`实例为相同的 Bean 属性定义不同的值的情况下,由于覆盖机制,最后一个实例获胜。 + +属性文件配置行采用以下格式: + +``` +beanName.property=value +``` + +下面的清单展示了这种格式的一个示例: + +``` +dataSource.driverClassName=com.mysql.jdbc.Driver +dataSource.url=jdbc:mysql:mydb +``` + +这个示例文件可以与容器定义一起使用,该容器定义包含一个名为 `DataSource’的 Bean,它具有`driver`和`url`属性。 + +复合属性名称也是受支持的,只要路径的每个组件(除了被重写的最终属性)已经是非空的(大概是由构造函数初始化的)。在下面的示例中,将`tom` Bean 的`fred`属性的`sammy`属性设置为标量值`123`: + +``` +tom.fred.bob.sammy=123 +``` + +| |指定的重写值总是文字值。它们没有被翻译成
Bean 引用。当 XML Bean 定义中的原始值指定 Bean 引用时,该约定也适用。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +通过 Spring 2.5 中引入的`context`名称空间,可以使用专用的配置元素配置属性重写,如下例所示: + +``` + +``` + +#### 1.8.3.使用`FactoryBean`自定义实例化逻辑 + +你可以为本身是工厂的对象实现`org.springframework.beans.factory.FactoryBean`接口。 + +`FactoryBean`接口是可插入 Spring IoC 容器的实例化逻辑的一个点。如果你的复杂初始化代码更好地用 爪哇 来表达,而不是(可能)使用冗长的 XML,那么你可以创建自己的“FactoryBean”,在该类中编写复杂的初始化,然后将你的自定义`FactoryBean`插入到容器中。 + +`FactoryBean`接口提供了三种方法: + +* `T getObject()`:返回这个工厂创建的对象的实例。这个实例可以被共享,这取决于这个工厂返回的是单例还是原型。 + +* `boolean isSingleton()`:返回`true`如果这个`FactoryBean`返回单例,否则返回 `false’。此方法的默认实现返回`true`。 + +* `Class getObjectType()`:返回由`getObject()`方法返回的对象类型或`null`如果类型事先不知道的话。 + +`FactoryBean`概念和接口在 Spring 框架内的许多地方被使用。50 多个实现方式的`FactoryBean`与 Spring 本身相关联的接口。 + +当你需要向容器请求一个实际的`FactoryBean`实例本身,而不是它生成的 Bean 实例时,在调用`id`方法的`getBean()`时,在 Bean 的`id`前加上符号。因此,对于给定的`FactoryBean`和`id`的`myBean`,在容器上调用`getBean("myBean")`将返回`FactoryBean`的乘积,而调用`getBean("&myBean")`将返回 `FactoryBean’实例本身。 + +### 1.9.基于注释的容器配置 + +在配置 Spring 方面,注释是否比 XML 更好? + +基于注释的配置的引入提出了这样一个问题:这种方法是否比 XML“更好”。简短的回答是“视情况而定。”长期的答案是,每种方法都有其优点和缺点,通常,由开发人员来决定哪种策略更适合他们。由于它们的定义方式,注释在其声明中提供了大量的上下文,从而导致更短和更简洁的配置。然而,XML 擅长将组件连接起来,而不会涉及它们的源代码或重新编译它们。一些开发人员倾向于让连接更接近源代码,而另一些开发人员则认为,带注释的类不再是 POJO,此外,配置变得分散且更难控制。 + +无论如何选择, Spring 都可以容纳这两种风格,甚至可以将它们混合在一起。值得指出的是,通过其[爪哇Config](#beans-java)选项, Spring 允许以非侵入性的方式使用注释,而不涉及目标组件源代码,并且,就工具而言,[Spring Tools for Eclipse](https://spring.io/tools)支持所有配置样式。 + +基于注释的配置提供了 XML 设置的另一种选择,它依赖字节码元数据来连接组件,而不是角括号声明。开发人员不使用 XML 来描述 Bean 连接,而是通过使用相关类、方法或字段声明上的注释将配置移动到组件类本身。正如在[Example: The `AutowiredAnnotationBeanPostProcessor`](#beans-factory-extension-bpp-examples-aabpp)中提到的那样,结合注释使用`BeanPostProcessor`是扩展 Spring IOC 容器的一种常见方法。例如, Spring 2.0 引入了使用[`@Required`](#beans-required-annotation)注释强制执行所需属性的可能性。 Spring 2.5 使得有可能遵循相同的通用方法来驱动 Spring 的依赖注入。从本质上讲,`@Autowired`注释提供了与[自动布线合作者](#beans-factory-autowire)中描述的相同的功能,但具有更细粒度的控制和更广泛的适用性。 Spring 2.5 还增加了对 JSR-250 注释的支持,例如 `@postConstruct` 和`@PreDestroy`。 Spring 3.0 增加了对`javax.inject`包中包含的 JSR-330(用于 爪哇 的依赖注入)注释的支持,例如`@Inject`和`@Named`。有关这些注释的详细信息可以在[相关部分](#beans-standard-annotations)中找到。 + +| |注释注入是在 XML 注入之前执行的。因此,XML 配置
覆盖了通过这两种方法连接的属性的注释。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------| + +与往常一样,你可以将后处理器注册为单独的 Bean 定义,但也可以通过在基于 XML 的 Spring 配置中包含以下标记来隐式地注册它们(请注意包含`context`名称空间): + +``` + + + + + + +``` + +``元素隐式地注册了以下后处理程序: + +* [“配置 classpostprocessor”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/annotation/ConfigurationClassPostProcessor.html) + +* [“自动 WireDannotationBeanPostProcessor”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.html) + +* [“CommonAnnotationBeanPostProcessor”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.html) + +* [“坚持注解后置处理器”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/orm/jpa/support/PersistenceAnnotationBeanPostProcessor.html) + +* [“EventListenerMethodProcessor”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/event/EventListenerMethodProcessor.html) + +| |``只在与其定义相同的
应用程序上下文中查找 bean 上的注释。这意味着,如果你将置于`WebApplicationContext`中的`DispatcherServlet`中,
则只检查控制器中的`@Autowired`bean,而不是你的服务。有关更多信息,请参见[DispatcherServlet](web.html#mvc-servlet)。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.9.1.@required + +`@Required`注释适用于 Bean 属性设置器方法,如下例所示: + +爪哇 + +``` +public class SimpleMovieLister { + + private MovieFinder movieFinder; + + @Required + public void setMovieFinder(MovieFinder movieFinder) { + this.movieFinder = movieFinder; + } + + // ... +} +``` + +Kotlin + +``` +class SimpleMovieLister { + + @Required + lateinit var movieFinder: MovieFinder + + // ... +} +``` + +该注释表明,受影响的 Bean 属性必须在配置时通过 Bean 定义中的显式属性值或通过自动布线来填充。如果未填充受影响的 Bean 属性,则容器抛出一个异常。这允许急切和明确的失败,避免`NullPointerException`实例或类似的情况。我们仍然建议你将断言放入 Bean 类本身(例如,放入 init 方法)。即使在容器之外使用类,这样做也会强制执行那些必需的引用和值。 + +| |[“RequiredAnNotationBeanPostProcessor”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/factory/annotation/RequiredAnnotationBeanPostProcessor.html)必须注册为 Bean,以启用对`@Required`注释的支持。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |在 Spring Framework5.1 中,`@Required`注释和`RequiredAnnotationBeanPostProcessor`正式地被
弃用,这有利于使用构造函数注入用于
所需的设置(或`InitializingBean.afterPropertiesSet()`的自定义实现或带有 Bean 属性设定器方法的自定义`@PostConstruct`方法)。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.9.2.使用`@Autowired` + +| |JSR330 的`@Inject`注释可以用来代替 Spring 的`@Autowired`注释。有关更多详细信息,请参见[here](#beans-standard-annotations)。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +你可以将`@Autowired`注释应用于构造函数,如下例所示: + +爪哇 + +``` +public class MovieRecommender { + + private final CustomerPreferenceDao customerPreferenceDao; + + @Autowired + public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) { + this.customerPreferenceDao = customerPreferenceDao; + } + + // ... +} +``` + +Kotlin + +``` +class MovieRecommender @Autowired constructor( + private val customerPreferenceDao: CustomerPreferenceDao) +``` + +| |从 Spring Framework4.3 开始,如果目标 Bean 只定义了一个要开始使用的构造函数,那么在这样的构造函数上的`@Autowired`注释就不再是
所必需的了。但是,如果
有几个构造函数可用,并且没有主/缺省构造函数,则至少
其中一个构造函数必须用`@Autowired`进行注释,以便指示
容器使用哪个。详见[构造器分辨率](#beans-autowired-annotation-constructor-resolution)上的讨论。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +还可以将`@Autowired`注释应用于*传统的*setter 方法,如下例所示: + +爪哇 + +``` +public class SimpleMovieLister { + + private MovieFinder movieFinder; + + @Autowired + public void setMovieFinder(MovieFinder movieFinder) { + this.movieFinder = movieFinder; + } + + // ... +} +``` + +Kotlin + +``` +class SimpleMovieLister { + + @Autowired + lateinit var movieFinder: MovieFinder + + // ... + +} +``` + +你还可以将注释应用于具有任意名称和多个参数的方法,如下例所示: + +爪哇 + +``` +public class MovieRecommender { + + private MovieCatalog movieCatalog; + + private CustomerPreferenceDao customerPreferenceDao; + + @Autowired + public void prepare(MovieCatalog movieCatalog, + CustomerPreferenceDao customerPreferenceDao) { + this.movieCatalog = movieCatalog; + this.customerPreferenceDao = customerPreferenceDao; + } + + // ... +} +``` + +Kotlin + +``` +class MovieRecommender { + + private lateinit var movieCatalog: MovieCatalog + + private lateinit var customerPreferenceDao: CustomerPreferenceDao + + @Autowired + fun prepare(movieCatalog: MovieCatalog, + customerPreferenceDao: CustomerPreferenceDao) { + this.movieCatalog = movieCatalog + this.customerPreferenceDao = customerPreferenceDao + } + + // ... +} +``` + +你还可以将`@Autowired`应用于字段,甚至将其与构造函数混合使用,如下例所示: + +爪哇 + +``` +public class MovieRecommender { + + private final CustomerPreferenceDao customerPreferenceDao; + + @Autowired + private MovieCatalog movieCatalog; + + @Autowired + public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) { + this.customerPreferenceDao = customerPreferenceDao; + } + + // ... +} +``` + +Kotlin + +``` +class MovieRecommender @Autowired constructor( + private val customerPreferenceDao: CustomerPreferenceDao) { + + @Autowired + private lateinit var movieCatalog: MovieCatalog + + // ... +} +``` + +| |确保你的目标组件(例如,`MovieCatalog`或`CustomerPreferenceDao`)
由你为`@Autowired`使用的类型一致地声明-注释
注入点。否则,注入可能会由于运行时的“未找到类型匹配”错误而失败。

对于通过 Classpath 扫描找到的 XML 定义的 bean 或组件类,容器
通常预先知道具体的类型。但是,对于`@Bean`工厂方法,你需要
来确保声明的返回类型具有足够的表达能力。对于实现多个接口的组件
,或者对于由其
实现类型可能引用的组件,考虑在工厂
方法上声明最特定的返回类型(至少与引用你的 Bean 的注入点所要求的特定类型一样)。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +还可以指示 Spring 通过将`@Autowired`注释添加到需要该类型数组的字段或方法,从 `ApplicationContext’中提供特定类型的所有 bean,如下例所示: + +爪哇 + +``` +public class MovieRecommender { + + @Autowired + private MovieCatalog[] movieCatalogs; + + // ... +} +``` + +Kotlin + +``` +class MovieRecommender { + + @Autowired + private lateinit var movieCatalogs: Array + + // ... +} +``` + +这同样适用于类型化集合,如下例所示: + +爪哇 + +``` +public class MovieRecommender { + + private Set movieCatalogs; + + @Autowired + public void setMovieCatalogs(Set movieCatalogs) { + this.movieCatalogs = movieCatalogs; + } + + // ... +} +``` + +Kotlin + +``` +class MovieRecommender { + + @Autowired + lateinit var movieCatalogs: Set + + // ... +} +``` + +| |你的目标 bean 可以实现`org.springframework.core.Ordered`接口或使用
`@Order`或标准`@Priority`注释,如果你希望数组或列表中的项
按特定顺序排序的话。否则,它们的顺序遵循在容器中对应的目标 Bean 定义的注册顺序


可以在目标类级别和`@Order`方法上声明`@Bean`注释,
可能用于单独的 Bean 定义(在多个定义
使用相同的 Bean 类的情况下)。`@Order`值可能会影响注入点的优先级,
但要注意,它们不会影响单例启动顺序,这是由依赖关系和`@DependsOn`声明确定的正交关系。,

注意,标准`javax.annotation.Priority`注释在 `@ Bean ` 级别不可用,因为它不能在方法上声明。它的语义可以通过`@Order`值与`@Primary`值结合,在单个 Bean 上为每个类型建模`@Primary`。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +即使类型`Map`实例也可以自动连线,只要所期望的键类型是`String`。映射值包含预期类型的所有 bean,键包含相应的 Bean 名称,如下例所示: + +爪哇 + +``` +public class MovieRecommender { + + private Map movieCatalogs; + + @Autowired + public void setMovieCatalogs(Map movieCatalogs) { + this.movieCatalogs = movieCatalogs; + } + + // ... +} +``` + +Kotlin + +``` +class MovieRecommender { + + @Autowired + lateinit var movieCatalogs: Map + + // ... +} +``` + +默认情况下,当给定的注入点没有匹配的候选 bean 可用时,自动布线失败。在声明的数组、集合或映射的情况下,需要至少一个匹配元素。 + +默认的行为是将带注释的方法和字段视为指示所需的依赖项。你可以改变这种行为,如下例所示,通过将一个不可满足的注入点标记为非必需的,使框架能够跳过该点(即,通过将`required`中的`@Autowired`属性设置为`false`): + +爪哇 + +``` +public class SimpleMovieLister { + + private MovieFinder movieFinder; + + @Autowired(required = false) + public void setMovieFinder(MovieFinder movieFinder) { + this.movieFinder = movieFinder; + } + + // ... +} +``` + +Kotlin + +``` +class SimpleMovieLister { + + @Autowired(required = false) + var movieFinder: MovieFinder? = null + + // ... +} +``` + +如果一个非必需的方法的依赖项(或者在多个参数的情况下,它的一个依赖项)是不可用的,那么它将不会被调用。在这种情况下,不需要的字段将根本不会被填充,从而保留其默认值。 + +注入的构造函数和工厂方法参数是一种特殊情况,因为`@Autowired`中的`required`属性具有某种不同的含义,这是由于 Spring 的构造函数解析算法可能会处理多个构造函数。默认情况下,构造函数和工厂方法参数实际上是必需的,但在单个构造函数场景中需要一些特殊规则,例如,如果没有匹配的 bean 可用,则将多元素注入点(数组、集合、映射)解析为空实例。这允许一种常见的实现模式,在这种模式中,所有依赖项都可以在唯一的多参数构造函数中声明——例如,在没有`@Autowired`注释的情况下,声明为单个公共构造函数。 + +| |Bean 类中只有一个构造函数可以声明`@Autowired`,其`required`属性设置为`true`,表示*The*构造函数在用作 Spring
Bean 时自动连接。因此,如果`required`属性保留在其默认值`true`处,
只能用`@Autowired`注释单个构造函数。如果多个构造函数
声明注释,它们都必须声明`required=false`,以便将
视为自动布线的候选项(类似于 XML 中的`autowire=constructor`)。
将选择通过在 Spring 容器中匹配
bean 而能够满足最多依赖项的构造函数。如果不能满足所有候选项,
则将使用主/缺省构造函数(如果存在)。类似地,如果类
声明了多个构造函数,但其中没有一个被`@Autowired`注释,那么将使用
主/缺省构造函数(如果存在)。如果一个类只声明一个
构造函数,那么它将始终被使用,即使没有注释。注意,一个
带注释的构造函数并不一定是公共的。

`required`属性在 setter 方法上的`@Autowired`注释上是推荐的。将`required`属性设置为`false`表示
属性不是自动布线所必需的,如果
不能自动布线,则忽略该属性。而`@Required`则更强,因为它强制使用容器支持的任何方式来设置
属性,并且如果没有定义值,
将引发相应的异常。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +或者,你可以通过 爪哇8 的`java.util.Optional`表示特定依赖项的非必需性质,如下例所示: + +``` +public class SimpleMovieLister { + + @Autowired + public void setMovieFinder(Optional movieFinder) { + ... + } +} +``` + +在 Spring Framework5.0 中,你还可以使用`@Nullable`注释(在任何包中的任何类型——例如,来自 JSR-305 的`javax.annotation.Nullable`)或仅利用 Kotlin 内建空安全支持: + +爪哇 + +``` +public class SimpleMovieLister { + + @Autowired + public void setMovieFinder(@Nullable MovieFinder movieFinder) { + ... + } +} +``` + +Kotlin + +``` +class SimpleMovieLister { + + @Autowired + var movieFinder: MovieFinder? = null + + // ... +} +``` + +对于众所周知的可解析依赖关系的接口,也可以使用`@Autowired`:`BeanFactory`,`ApplicationContext`,`Environment`,`ResourceLoader`,这些类型必须通过使用 XML 或 Spring `@Bean`方法显式地“连接起来”。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.9.3.使用`@Primary`微调基于注释的自动连线 + +由于按类型自动布线可能会导致多个候选者,因此通常需要对选择过程有更多的控制。实现这一点的一种方法是使用 Spring 的“@primary”注释。`@Primary`表示当多个 bean 是要自动连接到单值依赖项的候选 bean 时,应该给予特定的 Bean 优先权。如果在候选者之间正好存在一个主 Bean,则它成为自动连线值。 + +考虑以下配置,该配置将`firstMovieCatalog`定义为主要的`MovieCatalog`: + +爪哇 + +``` +@Configuration +public class MovieConfiguration { + + @Bean + @Primary + public MovieCatalog firstMovieCatalog() { ... } + + @Bean + public MovieCatalog secondMovieCatalog() { ... } + + // ... +} +``` + +Kotlin + +``` +@Configuration +class MovieConfiguration { + + @Bean + @Primary + fun firstMovieCatalog(): MovieCatalog { ... } + + @Bean + fun secondMovieCatalog(): MovieCatalog { ... } + + // ... +} +``` + +在前面的配置中,下面的`MovieRecommender`与“FirstMoviecatalog”自动连线: + +爪哇 + +``` +public class MovieRecommender { + + @Autowired + private MovieCatalog movieCatalog; + + // ... +} +``` + +Kotlin + +``` +class MovieRecommender { + + @Autowired + private lateinit var movieCatalog: MovieCatalog + + // ... +} +``` + +相应的 Bean 定义如下: + +``` + + + + + + + + + + + + + + + + +``` + +#### 1.9.4.使用限定符对基于注释的自动连线进行微调 + +`@Primary`是一种在可以确定一个主要候选者的情况下,通过多个实例使用自动布线的有效方法。当需要对选择过程进行更多控制时,可以使用 Spring 的`@Qualifier`注释。你可以将限定符值与特定的参数关联起来,从而缩小类型匹配的集合,以便为每个参数选择一个特定的 Bean。在最简单的情况下,这可以是一个简单的描述性值,如以下示例所示: + +爪哇 + +``` +public class MovieRecommender { + + @Autowired + @Qualifier("main") + private MovieCatalog movieCatalog; + + // ... +} +``` + +Kotlin + +``` +class MovieRecommender { + + @Autowired + @Qualifier("main") + private lateinit var movieCatalog: MovieCatalog + + // ... +} +``` + +你还可以对单个构造函数参数或方法参数指定`@Qualifier`注释,如以下示例所示: + +爪哇 + +``` +public class MovieRecommender { + + private MovieCatalog movieCatalog; + + private CustomerPreferenceDao customerPreferenceDao; + + @Autowired + public void prepare(@Qualifier("main") MovieCatalog movieCatalog, + CustomerPreferenceDao customerPreferenceDao) { + this.movieCatalog = movieCatalog; + this.customerPreferenceDao = customerPreferenceDao; + } + + // ... +} +``` + +Kotlin + +``` +class MovieRecommender { + + private lateinit var movieCatalog: MovieCatalog + + private lateinit var customerPreferenceDao: CustomerPreferenceDao + + @Autowired + fun prepare(@Qualifier("main") movieCatalog: MovieCatalog, + customerPreferenceDao: CustomerPreferenceDao) { + this.movieCatalog = movieCatalog + this.customerPreferenceDao = customerPreferenceDao + } + + // ... +} +``` + +下面的示例显示了相应的 Bean 定义。 + +``` + + + + + + + (1) + + + + + + (2) + + + + + + + +``` + +|**1**|具有`main`限定符值的 Bean 与构造函数参数连线,即
具有相同的限定符值。| +|-----|----------------------------------------------------------------------------------------------------------------------------| +|**2**|具有`action`限定符值的 Bean 与构造函数参数连线,即
具有相同的限定符值。| + +对于回退匹配, Bean 名称被认为是缺省限定符值。因此,可以使用`id`的`main`来定义 Bean,而不是使用嵌套的限定符元素,从而导致相同的匹配结果。然而,尽管可以使用此约定按名称引用特定的 bean,但`@Autowired`基本上是关于类型驱动的注入和可选的语义限定符。这意味着,即使使用 Bean Name Fallback,限定符值在类型匹配集合中也始终具有狭窄的语义。它们在语义上不表示对唯一 Bean `id`的引用。好的限定符值是`main`或`EMEA`或`persistent`,表示独立于 Bean `id`的特定组件的特征,在匿名 Bean 定义的情况下,例如前面示例中的定义,该特征可以自动生成。 + +限定符也适用于类型化集合,如前面讨论的那样——例如,适用于 `set`。在本例中,根据声明的限定符,所有匹配的 bean 都作为集合注入。这意味着限定词不必是唯一的。相反,它们构成了过滤标准。例如,你可以使用相同的限定符值“action”来定义多个`MovieCatalog`bean,所有这些 bean 都被注入到`Set`注释为`@Qualifier("action")`的 bean 中。 + +| |在类型匹配的
候选项中,让限定符值根据目标 Bean 名称进行选择,不需要在注入点进行`@Qualifier`注释。
如果没有其他分辨率指示符(例如限定符或主标记),则对于非惟一依赖关系情况,
, Spring 将注入点名称
(即字段名称或参数名称)与目标 Bean 名称匹配,并选择
相同名称的候选项,如果有的话。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +也就是说,如果打算通过名称表示注释驱动的注入,则不主要使用`@Autowired`,即使它能够通过 Bean 在类型匹配候选者中选择名称。相反,使用 JSR-250`@Resource`注释,该注释在语义上定义为通过其唯一名称来标识特定的目标组件,声明的类型与匹配过程无关。`@Autowired`具有相当不同的语义:在按类型选择候选 bean 之后,指定的`String`限定符值仅被考虑在那些类型选择的候选值中(例如,将`account`限定符与标记有相同限定符标签的 bean 匹配)。 + +对于本身被定义为集合的 bean,`Map`或数组类型,`@Resource`是一个很好的解决方案,通过唯一的名称引用特定的集合或数组 Bean。也就是说,从 4.3 开始,你也可以通过 Spring 的“@autowired”类型匹配算法来匹配集合、`Map`和数组类型,只要元素类型信息保留在`@Bean`返回类型签名或集合继承层次结构中。在这种情况下,你可以使用限定符值在相同类型的集合中进行选择,如上一段所概述的那样。 + +截至 4.3,`@Autowired`还考虑用于注入的自引用(即,对当前注入的 Bean 的引用)。请注意,自我注入是一种回退。对其他组件的常规依赖总是具有优先权的。从这个意义上说,自我参照不参与常规的候选人选择,因此,特别是从来没有主要的。相反,它们最终总是排在最靠后的位置。在实践中,你应该仅将自引用作为最后的手段(例如,通过 Bean 的事务代理调用同一实例上的其他方法)。考虑在这样的场景中将受影响的方法分解为单独的委托 Bean。或者,可以使用`@Resource`,这可以获得返回当前 Bean 的代理的唯一名称。 + +| |试图在相同的配置类上注入来自`@Bean`方法的结果实际上也是一个自引用场景。在实际需要的方法签名中(而不是配置类中的自动连线字段
),懒惰地解析此类引用
,或者将受影响的`@Bean`方法声明为`static`,
将它们与包含的配置类实例及其生命周期分离。否则,这样的 bean 仅在后备阶段被考虑,在其他配置类上匹配的 bean
被选为主要候选者(如果可用的话)。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +`@Autowired`应用于字段、构造函数和多参数方法,允许在参数级别上通过限定符注释进行缩小。与此相反,`@Resource`仅支持具有单个参数的字段和 Bean 属性设置器方法。因此,如果注入目标是构造函数或多参数方法,则应该坚持使用限定符。 + +你可以创建自己的自定义限定符注释。要做到这一点,请定义一个注释,并在你的定义中提供`@Qualifier`注释,如下例所示: + +爪哇 + +``` +@Target({ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Qualifier +public @interface Genre { + + String value(); +} +``` + +Kotlin + +``` +@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +@Qualifier +annotation class Genre(val value: String) +``` + +然后,你可以在 AutoWired 字段和参数上提供自定义限定符,如下例所示: + +爪哇 + +``` +public class MovieRecommender { + + @Autowired + @Genre("Action") + private MovieCatalog actionCatalog; + + private MovieCatalog comedyCatalog; + + @Autowired + public void setComedyCatalog(@Genre("Comedy") MovieCatalog comedyCatalog) { + this.comedyCatalog = comedyCatalog; + } + + // ... +} +``` + +Kotlin + +``` +class MovieRecommender { + + @Autowired + @Genre("Action") + private lateinit var actionCatalog: MovieCatalog + + private lateinit var comedyCatalog: MovieCatalog + + @Autowired + fun setComedyCatalog(@Genre("Comedy") comedyCatalog: MovieCatalog) { + this.comedyCatalog = comedyCatalog + } + + // ... +} +``` + +接下来,可以为候选 Bean 定义提供信息。你可以添加 `标记作为``标记的子元素,然后指定`type`和 `value’以匹配自定义限定符注释。类型与注释的完全限定类名称匹配。另外,如果不存在名称冲突的风险,为了方便起见,你可以使用较短的类名。下面的示例演示了这两种方法: + +``` + + + + + + + + + + + + + + + + + + +``` + +在[Classpath Scanning and Managed Components](#beans-classpath-scanning)中,你可以看到一种基于注释的替代方法,用于在 XML 中提供限定符元数据。具体见[提供带有注释的限定符元数据](#beans-scanning-qualifiers)。 + +在某些情况下,使用没有值的注释可能就足够了。当注释服务于更通用的目的并且可以跨几种不同类型的依赖关系应用时,这可能是有用的。例如,你可以提供一个脱机目录,当没有可用的 Internet 连接时可以对其进行搜索。首先,定义简单的注释,如下例所示: + +爪哇 + +``` +@Target({ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Qualifier +public @interface Offline { + +} +``` + +Kotlin + +``` +@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +@Qualifier +annotation class Offline +``` + +然后将注释添加到要自动连接的字段或属性中,如以下示例所示: + +爪哇 + +``` +public class MovieRecommender { + + @Autowired + @Offline (1) + private MovieCatalog offlineCatalog; + + // ... +} +``` + +|**1**|这一行添加了`@Offline`注释。| +|-----|-----------------------------------------| + +Kotlin + +``` +class MovieRecommender { + + @Autowired + @Offline (1) + private lateinit var offlineCatalog: MovieCatalog + + // ... +} +``` + +|**1**|这一行添加了`@Offline`注释。| +|-----|-----------------------------------------| + +现在 Bean 定义只需要一个限定符`type`,如下例所示: + +``` + + (1) + + +``` + +|**1**|此元素指定限定符。| +|-----|-------------------------------------| + +你还可以定义自定义限定符注释,这些注释除了接受简单的`value`属性之外,还接受命名属性。 Bean 如果随后在要自动连线的字段或参数上指定了多个属性值,则定义必须匹配所有这样的属性值才能被视为自动连线候选值。作为示例,请考虑以下注释定义: + +爪哇 + +``` +@Target({ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Qualifier +public @interface MovieQualifier { + + String genre(); + + Format format(); +} +``` + +Kotlin + +``` +@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +@Qualifier +annotation class MovieQualifier(val genre: String, val format: Format) +``` + +在这种情况下,`Format`是一个枚举,定义如下: + +爪哇 + +``` +public enum Format { + VHS, DVD, BLURAY +} +``` + +Kotlin + +``` +enum class Format { + VHS, DVD, BLURAY +} +``` + +要自动连接的字段将使用自定义限定符进行注释,并包括两个属性的值:`genre`和`format`,如下例所示: + +爪哇 + +``` +public class MovieRecommender { + + @Autowired + @MovieQualifier(format=Format.VHS, genre="Action") + private MovieCatalog actionVhsCatalog; + + @Autowired + @MovieQualifier(format=Format.VHS, genre="Comedy") + private MovieCatalog comedyVhsCatalog; + + @Autowired + @MovieQualifier(format=Format.DVD, genre="Action") + private MovieCatalog actionDvdCatalog; + + @Autowired + @MovieQualifier(format=Format.BLURAY, genre="Comedy") + private MovieCatalog comedyBluRayCatalog; + + // ... +} +``` + +Kotlin + +``` +class MovieRecommender { + + @Autowired + @MovieQualifier(format = Format.VHS, genre = "Action") + private lateinit var actionVhsCatalog: MovieCatalog + + @Autowired + @MovieQualifier(format = Format.VHS, genre = "Comedy") + private lateinit var comedyVhsCatalog: MovieCatalog + + @Autowired + @MovieQualifier(format = Format.DVD, genre = "Action") + private lateinit var actionDvdCatalog: MovieCatalog + + @Autowired + @MovieQualifier(format = Format.BLURAY, genre = "Comedy") + private lateinit var comedyBluRayCatalog: MovieCatalog + + // ... +} +``` + +最后, Bean 定义应该包含匹配的限定符值。这个示例还演示了你可以使用 Bean 元属性而不是 `` 元素。如果可用,则``元素及其属性优先,但是,如果不存在这样的限定符,则自动连接机制将落在 `标记内提供的值上,如下例中的最后两个 Bean 定义所示: + +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +#### 1.9.5.使用泛型作为自动连线限定符 + +除了`@Qualifier`注释之外,你还可以使用 爪哇 泛型类型作为一种隐含的限定形式。例如,假设你有以下配置: + +爪哇 + +``` +@Configuration +public class MyConfiguration { + + @Bean + public StringStore stringStore() { + return new StringStore(); + } + + @Bean + public IntegerStore integerStore() { + return new IntegerStore(); + } +} +``` + +Kotlin + +``` +@Configuration +class MyConfiguration { + + @Bean + fun stringStore() = StringStore() + + @Bean + fun integerStore() = IntegerStore() +} +``` + +假设前面的 bean 实现了一个泛型接口(即`Store`和 `store`),则可以`@Autowire`将`Store`接口和泛型用作限定符,如下例所示: + +爪哇 + +``` +@Autowired +private Store s1; // qualifier, injects the stringStore bean + +@Autowired +private Store s2; // qualifier, injects the integerStore bean +``` + +Kotlin + +``` +@Autowired +private lateinit var s1: Store // qualifier, injects the stringStore bean + +@Autowired +private lateinit var s2: Store // qualifier, injects the integerStore bean +``` + +当自动连接列表、`Map`实例和数组时,通用限定符也会应用。以下示例自动连接通用`List`: + +爪哇 + +``` +// Inject all Store beans as long as they have an generic +// Store beans will not appear in this list +@Autowired +private List> s; +``` + +Kotlin + +``` +// Inject all Store beans as long as they have an generic +// Store beans will not appear in this list +@Autowired +private lateinit var s: List> +``` + +#### 1.9.6.使用`CustomAutowireConfigurer` + +[“CustomAutoWireConfigurer”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/factory/annotation/CustomAutowireConfigurer.html)是一个`BeanFactoryPostProcessor`,它允许你注册自己的自定义限定符注释类型,即使它们没有使用 Spring 的`@Qualifier`注释。下面的示例展示了如何使用`CustomAutowireConfigurer`: + +``` + + + + example.CustomQualifier + + + +``` + +`AutowireCandidateResolver`通过以下方式确定 AutoWire 候选项: + +* 每个 Bean 定义的`autowire-candidate`值 + +* 在``元素上可用的任何`default-autowire-candidates`模式 + +* 存在`@Qualifier`注释和在`CustomAutowireConfigurer`中注册的任何自定义注释 + +当多个 bean 符合自动连接候选项的条件时,“主”的确定如下:如果在候选项中恰好有一个 Bean 定义将`primary`属性设置为`true`,则选中它。 + +#### 1.9.7.注入`@Resource` + +Spring 还通过在字段或 Bean 属性设置器方法上使用 JSR-250`@Resource`注释(`javax.annotation.resource`)来支持注入。这是 爪哇 EE 中的一种常见模式:例如,在 JSF 管理的 Bean 和 JAX-WS 端点中。 Spring 对于 Spring-托管对象也支持这种模式。 + +`@Resource`接受一个 name 属性。默认情况下, Spring 将该值解释为要注入的 Bean 名称。换句话说,它遵循副名语义,如下例所示: + +爪哇 + +``` +public class SimpleMovieLister { + + private MovieFinder movieFinder; + + @Resource(name="myMovieFinder") (1) + public void setMovieFinder(MovieFinder movieFinder) { + this.movieFinder = movieFinder; + } +} +``` + +|**1**|这一行注入一个`@Resource`。| +|-----|--------------------------------| + +Kotlin + +``` +class SimpleMovieLister { + + @Resource(name="myMovieFinder") (1) + private lateinit var movieFinder:MovieFinder +} +``` + +|**1**|这一行注入一个`@Resource`。| +|-----|--------------------------------| + +如果没有显式指定名称,则缺省名称将从字段名称或 setter 方法派生。在字段的情况下,它采用字段名称。对于 setter 方法,它使用 Bean 属性名。下面的示例将把名为`movieFinder`的 Bean 注入到其 setter 方法中: + +爪哇 + +``` +public class SimpleMovieLister { + + private MovieFinder movieFinder; + + @Resource + public void setMovieFinder(MovieFinder movieFinder) { + this.movieFinder = movieFinder; + } +} +``` + +Kotlin + +``` +class SimpleMovieLister { + + @Resource + private lateinit var movieFinder: MovieFinder + +} +``` + +| |注释提供的名称通过`CommonAnnotationBeanPostProcessor`知道的 `applicationContext’解析为 Bean 名称。
如果显式地配置 Spring 的[“SimplejndibeanFactory”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jndi/support/SimpleJndiBeanFactory.html),则可以通过 JNDI 解析名称。但是,我们建议你依赖默认的行为,并且
使用 Spring 的 JNDI 查找功能来保持间接的级别。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在不指定显式名称的`@Resource`用法的独占情况下,并且类似于`@Autowired`,`@Resource`找到一个主类型匹配,而不是一个特定的命名 Bean,并解决众所周知的可解析依赖项:`BeanFactory`,`applicationContext`,`ResourceLoader`,`ApplicationEventPublisher`接口。 + +因此,在下面的示例中,`customerPreferenceDao`字段首先查找名为“CustomerPreferenceDAO”的 Bean,然后返回到类型 `CustomerPreferenceDAO’的主类型匹配: + +爪哇 + +``` +public class MovieRecommender { + + @Resource + private CustomerPreferenceDao customerPreferenceDao; + + @Resource + private ApplicationContext context; (1) + + public MovieRecommender() { + } + + // ... +} +``` + +|**1**|基于已知的可解析依赖类型:“ApplicationContext”注入`context`字段。| +|-----|---------------------------------------------------------------------------------------------------| + +Kotlin + +``` +class MovieRecommender { + + @Resource + private lateinit var customerPreferenceDao: CustomerPreferenceDao + + @Resource + private lateinit var context: ApplicationContext (1) + + // ... +} +``` + +|**1**|基于已知的可解析依赖类型:“ApplicationContext”注入`context`字段。| +|-----|---------------------------------------------------------------------------------------------------| + +#### 1.9.8.使用`@Value` + +`@Value`通常用于注入外部化属性: + +爪哇 + +``` +@Component +public class MovieRecommender { + + private final String catalog; + + public MovieRecommender(@Value("${catalog.name}") String catalog) { + this.catalog = catalog; + } +} +``` + +Kotlin + +``` +@Component +class MovieRecommender(@Value("\${catalog.name}") private val catalog: String) +``` + +具有以下配置: + +Java + +``` +@Configuration +@PropertySource("classpath:application.properties") +public class AppConfig { } +``` + +Kotlin + +``` +@Configuration +@PropertySource("classpath:application.properties") +class AppConfig +``` + +以及下面的`application.properties`文件: + +``` +catalog.name=MovieCatalog +``` + +在这种情况下,`catalog`参数和字段将等于`MovieCatalog`值。 + +Spring 提供了一个默认的宽松内含价值解析器。它将尝试解析属性值,如果无法解析,则将注入属性名(例如`${catalog.name}`)作为该值。如果希望对不存在的值保持严格控制,则应该声明`PropertySourcesPlaceholderConfigurer` Bean,如下例所示: + +Java + +``` +@Configuration +public class AppConfig { + + @Bean + public static PropertySourcesPlaceholderConfigurer propertyPlaceholderConfigurer() { + return new PropertySourcesPlaceholderConfigurer(); + } +} +``` + +Kotlin + +``` +@Configuration +class AppConfig { + + @Bean + fun propertyPlaceholderConfigurer() = PropertySourcesPlaceholderConfigurer() +} +``` + +| |当使用 JavaConfig 配置`PropertySourcesPlaceholderConfigurer`时,`@ Bean ` 方法必须是`static`。| +|---|---------------------------------------------------------------------------------------------------------------| + +如果无法解决任何`${}`占位符,则使用上述配置可确保初始化失败。也可以使用“setPlaceHolderPrefix”、`setPlaceholderSuffix`或`setValueSeparator`等方法来定制占位符。 + +| |Spring 在默认情况下,引导配置一个`PropertySourcesPlaceholderConfigurer` Bean,即
将从`application.properties`和`application.yml`文件获得属性。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +Spring 提供的内置转换器支持允许自动处理简单的类型转换(例如到`Integer`或`int`)。多个逗号分隔的值可以自动转换为`String`数组,无需额外的工作。 + +可以提供如下默认值: + +Java + +``` +@Component +public class MovieRecommender { + + private final String catalog; + + public MovieRecommender(@Value("${catalog.name:defaultCatalog}") String catalog) { + this.catalog = catalog; + } +} +``` + +Kotlin + +``` +@Component +class MovieRecommender(@Value("\${catalog.name:defaultCatalog}") private val catalog: String) +``` + +Spring `BeanPostProcessor`在幕后使用`ConversionService`来处理将`String`中的`String`值转换为目标类型的过程。如果你想为自己的自定义类型提供转换支持,可以提供自己的“ConversionService” Bean 实例,如下例所示: + +Java + +``` +@Configuration +public class AppConfig { + + @Bean + public ConversionService conversionService() { + DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(); + conversionService.addConverter(new MyCustomConverter()); + return conversionService; + } +} +``` + +Kotlin + +``` +@Configuration +class AppConfig { + + @Bean + fun conversionService(): ConversionService { + return DefaultFormattingConversionService().apply { + addConverter(MyCustomConverter()) + } + } +} +``` + +当`@Value`包含[“spel”表达式](#expressions)时,将在运行时动态计算值,如下例所示: + +Java + +``` +@Component +public class MovieRecommender { + + private final String catalog; + + public MovieRecommender(@Value("#{systemProperties['user.catalog'] + 'Catalog' }") String catalog) { + this.catalog = catalog; + } +} +``` + +Kotlin + +``` +@Component +class MovieRecommender( + @Value("#{systemProperties['user.catalog'] + 'Catalog' }") private val catalog: String) +``` + +SPEL 还支持使用更复杂的数据结构: + +Java + +``` +@Component +public class MovieRecommender { + + private final Map countOfMoviesPerCatalog; + + public MovieRecommender( + @Value("#{{'Thriller': 100, 'Comedy': 300}}") Map countOfMoviesPerCatalog) { + this.countOfMoviesPerCatalog = countOfMoviesPerCatalog; + } +} +``` + +Kotlin + +``` +@Component +class MovieRecommender( + @Value("#{{'Thriller': 100, 'Comedy': 300}}") private val countOfMoviesPerCatalog: Map) +``` + +#### 1.9.9.使用`@PostConstruct`和`@PreDestroy` + +`CommonAnnotationBeanPostProcessor`不仅可以识别`@Resource`注释,还可以识别 JSR-250 生命周期注释:`javax.annotation.PostConstruct`和 `javax.annotation.predestroy’。在 Spring 2.5 中引入的,对这些注释的支持为[初始化回调](#beans-factory-lifecycle-initializingbean)和[销毁回调](#beans-factory-lifecycle-disposablebean)中描述的生命周期回调机制提供了一种替代方案。如果在 Spring `ApplicationContext`中注册了 `CommonAnnotationBeanPostProcessor’,则携带这些注释之一的方法在生命周期的同一点被调用,作为相应的 Spring 生命周期接口方法或显式声明的回调方法。在下面的示例中,缓存在初始化时被预先填充,在销毁时被清除: + +Java + +``` +public class CachingMovieLister { + + @PostConstruct + public void populateMovieCache() { + // populates the movie cache upon initialization... + } + + @PreDestroy + public void clearMovieCache() { + // clears the movie cache upon destruction... + } +} +``` + +Kotlin + +``` +class CachingMovieLister { + + @PostConstruct + fun populateMovieCache() { + // populates the movie cache upon initialization... + } + + @PreDestroy + fun clearMovieCache() { + // clears the movie cache upon destruction... + } +} +``` + +有关结合各种生命周期机制的影响的详细信息,请参见[结合生命周期机制](#beans-factory-lifecycle-combined-effects)。 + +| |和`@Resource`一样,`@PostConstruct`和`@PreDestroy`注释类型也是从 JDK6 到 8 的标准 Java 库中
的一部分。然而,整个`javax.annotation`包在 JDK9 中与核心 Java 模块分离,并最终在
JDK11 中删除。如果需要,现在需要通过 Maven `javax.annotation-api`Central 获得
工件,只需像任何其他库一样添加到应用程序的 Classpath 中。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 1.10. Classpath 扫描和管理组件 + +本章中的大多数示例都使用 XML 来指定在 Spring 容器中产生每个`BeanDefinition`的配置元数据。上一节([基于注释的容器配置](#beans-annotation-config))演示了如何通过源级注释提供大量配置元数据。然而,即使在这些示例中,“基本” Bean 定义也是在 XML 文件中明确定义的,而注释仅驱动依赖注入。本节描述了用于通过扫描 Classpath 隐式地检测候选组件的选项。 Bean 候选组件是与筛选条件匹配的类,并且具有与容器注册的相应定义。这消除了使用 XML 来执行 Bean 注册的需要。相反,你可以使用注释(例如,`@Component`)、AspectJ 类型表达式或你自己的自定义筛选条件来选择哪些类具有在容器中注册的 Bean 定义。 + +| |从 Spring 3.0 开始, Spring JavaConfig 项目提供的许多特性都是
核心 Spring 框架的一部分。这允许你使用 Java 来定义 bean,而不是使用传统的 XML 文件
。看看`@Configuration`、`@Bean`、`@import’和`@DependsOn`注释,以了解如何使用这些新功能。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.10.1.`@Component`和进一步的原型注释 + +`@Repository`注释是满足存储库角色或原型(也称为数据访问对象或 DAO)的任何类的标记。此标记的用途之一是异常的自动转换,如[异常转换](data-access.html#orm-exception-translation)中所述。 + +Spring 提供了进一步的原型注释:`@Component`,`@Service`,和 `@controller`。`@Component`是任何 Spring-托管组件的通用原型。@repository`、`@Service`和`@Controller`是`@Component`的专门化,用于更具体的用例(分别在持久性、服务层和表示层中)。因此,你可以使用“@Component”对组件类进行注释,但是,通过使用`@Repository`、`@Service`或`@Controller`对它们进行注释,你的类更适合于通过工具进行处理或与方面进行关联。例如,这些原型注释为切入点提供了理想的目标。`@Repository`、`@Service`和`@Controller`还可以在 Spring 框架的未来版本中携带额外的语义。因此,如果你在使用`@Component`或`@Service`作为服务层时进行选择,`@Service`显然是更好的选择。类似地,如前所述,`@Repository`已经被支持作为持久性层中自动异常转换的标记。 + +#### 1.10.2.使用元注释和组合注释 + +Spring 提供的许多注释可以在你自己的代码中用作元注释。元注释是一种可以应用于另一种注释的注释。例如,所提到的`@Service`注释[earlier](#beans-stereotype-annotations)被元注释为`@Component`,如下例所示: + +Java + +``` +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component (1) +public @interface Service { + + // ... +} +``` + +|**1**|`@Component`导致`@Service`被以与`@Component`相同的方式处理。| +|-----|---------------------------------------------------------------------------------| + +Kotlin + +``` +@Target(AnnotationTarget.TYPE) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +@Component (1) +annotation class Service { + + // ... +} +``` + +|**1**|`@Component`导致`@Service`被以与`@Component`相同的方式处理。| +|-----|---------------------------------------------------------------------------------| + +你还可以合并元注释来创建“组合注释”。例如,来自 Spring MVC 的`@RestController`注释由`@Controller`和 `@responsebody’组成。 + +此外,合成注释可以选择从元注释中重新声明属性以允许定制。当你只想公开元注释属性的一个子集时,这可能特别有用。例如, Spring 的“@SessionScope”注释将作用域名称硬编码为`session`,但仍然允许自定义`proxyMode`。下面的清单显示了“SessionScope”注释的定义: + +Java + +``` +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Scope(WebApplicationContext.SCOPE_SESSION) +public @interface SessionScope { + + /** + * Alias for {@link Scope#proxyMode}. + *

Defaults to {@link ScopedProxyMode#TARGET_CLASS}. + */ + @AliasFor(annotation = Scope.class) + ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS; + +} +``` + +Kotlin + +``` +@Target(AnnotationTarget.TYPE, AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +@Scope(WebApplicationContext.SCOPE_SESSION) +annotation class SessionScope( + @get:AliasFor(annotation = Scope::class) + val proxyMode: ScopedProxyMode = ScopedProxyMode.TARGET_CLASS +) +``` + +然后,你可以使用`@SessionScope`,而不声明`proxyMode`,如下所示: + +Java + +``` +@Service +@SessionScope +public class SessionScopedService { + // ... +} +``` + +Kotlin + +``` +@Service +@SessionScope +class SessionScopedService { + // ... +} +``` + +还可以重写`proxyMode`的值,如下例所示: + +Java + +``` +@Service +@SessionScope(proxyMode = ScopedProxyMode.INTERFACES) +public class SessionScopedUserService implements UserService { + // ... +} +``` + +Kotlin + +``` +@Service +@SessionScope(proxyMode = ScopedProxyMode.INTERFACES) +class SessionScopedUserService : UserService { + // ... +} +``` + +有关更多详细信息,请参见[Spring Annotation Programming Model](https://github.com/spring-projects/spring-framework/wiki/Spring-Annotation-Programming-Model)维基页面。 + +#### 1.10.3.自动检测类并注册 Bean 定义 + +Spring 可以自动地检测到原型类并用`ApplicationContext`注册相应的 `BeanDefinition’实例。例如,以下两个类可以进行这种自动检测: + +Java + +``` +@Service +public class SimpleMovieLister { + + private MovieFinder movieFinder; + + public SimpleMovieLister(MovieFinder movieFinder) { + this.movieFinder = movieFinder; + } +} +``` + +Kotlin + +``` +@Service +class SimpleMovieLister(private val movieFinder: MovieFinder) +``` + +Java + +``` +@Repository +public class JpaMovieFinder implements MovieFinder { + // implementation elided for clarity +} +``` + +Kotlin + +``` +@Repository +class JpaMovieFinder : MovieFinder { + // implementation elided for clarity +} +``` + +要自动检测这些类并注册相应的 bean,你需要在`@Configuration`类中添加 `@ComponentScan`,其中`basePackages`属性是这两个类的公共父包。(或者,你可以指定一个逗号或分号或空格分隔的列表,其中包括每个类的父包。 + +Java + +``` +@Configuration +@ComponentScan(basePackages = "org.example") +public class AppConfig { + // ... +} +``` + +Kotlin + +``` +@Configuration +@ComponentScan(basePackages = ["org.example"]) +class AppConfig { + // ... +} +``` + +| |为了简洁起见,前面的示例可以使用`value`注释(即`@ComponentScan("org.example")`)的`value`属性。| +|---|------------------------------------------------------------------------------------------------------------------------------------------| + +以下替代方案使用 XML: + +``` + + + + + + +``` + +| |使用``隐式启用了 `` 的功能。在使用``时,通常不需要包含 `` 元素。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |Classpath 包的扫描需要在 Classpath 中存在相应的目录
条目。当你使用 Ant 构建 JAR 时,请确保没有
激活 jar 任务的仅文件开关。另外,在某些环境中,基于安全策略,目录可能不会
公开,例如,在
jdk1.7.0\_45 或更高版本上的独立应用程序(这需要在你的清单中进行“信任库”设置——参见[https://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources](https://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources)),
在 JDK9 的模块路径(拼图)上
, Spring 的 Classpath 扫描通常按预期工作。
但是,请确保你的组件类在`module-info`描述符中导出。如果你希望 Spring 调用类的非公共成员,请确保
它们是“打开的”(即它们在你的`opens`描述符中使用`module-info`声明,而不是 `exports’声明)。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +此外,当你使用 Component-Scan 元素时,`AutowiredAnnotationBeanPostProcessor`和 `CommonAnnotationBeanPostProcessor’都是隐式包含的。这意味着这两个组件是自动检测和连接在一起的——所有这些都没有 XML 提供的任何 Bean 配置元数据。 + +| |你可以禁用`AutowiredAnnotationBeanPostProcessor`和 `commonAnnotationBeanPostProcessor’的注册,方法是使用`annotation-config`属性
,其值为`false`。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.10.4.使用筛选器自定义扫描 + +默认情况下,使用`@Component`、`@Repository`、`@Service`、`@Controller`、`@configuration’或自身使用`@Component`进行注释的自定义注释的类是唯一检测到的候选组件。但是,你可以通过应用自定义过滤器来修改和扩展此行为。将它们添加为`includeFilters`或`excludeFilters`注释的属性(或在 XML 配置中添加``或<``元素的子元素)。每个筛选器元素都需要`type`和`expression`属性。下表介绍了筛选选项: + +| Filter Type | Example Expression |说明| +|--------------------|----------------------------|------------------------------------------------------------------------------------------| +|annotation (default)|`org.example.SomeAnnotation`|在目标组件的类型级别上,注释为*礼物*或*元存在*。| +| assignable | `org.example.SomeClass` |可将目标组件分配给(扩展或实现)的类(或接口)。| +| aspectj | `org.example..*Service+` |由目标组件匹配的 AspectJ 类型表达式。| +| regex | `org\.example\.Default.*` |由目标组件的类名匹配的正则表达式。| +| custom | `org.example.MyTypeFilter` |`org.springframework.core.type.TypeFilter`接口的自定义实现。| + +下面的示例显示了忽略所有`@Repository`注释并使用“存根”存储库的配置: + +Java + +``` +@Configuration +@ComponentScan(basePackages = "org.example", + includeFilters = @Filter(type = FilterType.REGEX, pattern = ".*Stub.*Repository"), + excludeFilters = @Filter(Repository.class)) +public class AppConfig { + // ... +} +``` + +Kotlin + +``` +@Configuration +@ComponentScan(basePackages = "org.example", + includeFilters = [Filter(type = FilterType.REGEX, pattern = [".*Stub.*Repository"])], + excludeFilters = [Filter(Repository::class)]) +class AppConfig { + // ... +} +``` + +下面的清单显示了等效的 XML: + +``` + + + + + + +``` + +| |你还可以通过在
注释上设置`useDefaultFilters=false`或通过提供`use-default-filters="false"`作为 `` 元素的属性来禁用默认过滤器。这有效地禁用了对带有
注释或 meta 注释的类`@Component`、`@Repository`、`@Service`、`@Controller`、`@restcontroller` 或`@Configuration`的自动检测。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.10.5.在组件中定义 Bean 元数据 + +Spring 组件还可以向容器贡献 Bean 定义元数据。你可以使用与`@Bean`注释类中定义 Bean 元数据相同的`@Configuration`注释来完成此操作。下面的示例展示了如何做到这一点: + +爪哇 + +``` +@Component +public class FactoryMethodComponent { + + @Bean + @Qualifier("public") + public TestBean publicInstance() { + return new TestBean("publicInstance"); + } + + public void doWork() { + // Component method implementation omitted + } +} +``` + +Kotlin + +``` +@Component +class FactoryMethodComponent { + + @Bean + @Qualifier("public") + fun publicInstance() = TestBean("publicInstance") + + fun doWork() { + // Component method implementation omitted + } +} +``` + +前面的类是一个 Spring 组件,在其“dowork()”方法中具有特定于应用程序的代码。然而,它还贡献了 Bean 定义,该定义具有引用方法`publicInstance()`的工厂方法。`@Bean`注释标识了工厂方法和其他 Bean 定义属性,例如通过`@Qualifier`注释的限定符值。可以指定的其他方法级注释是 `@scope’、`@Lazy`和自定义限定符注释。 + +| |除了组件初始化的作用外,还可以将`@Lazy`注释放置在标记有`@Autowired`或`@Inject`的注入点上。在这种情况下,
将导致注入一个延迟分辨率代理。然而,这样的代理方法
是相当有限的。对于复杂的惰性交互,特别是结合
和可选依赖项的情况,我们建议使用`ObjectProvider`代替。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +正如前面所讨论的,支持自动连线字段和方法,同时还支持`@Bean`方法的自动连线。下面的示例展示了如何做到这一点: + +爪哇 + +``` +@Component +public class FactoryMethodComponent { + + private static int i; + + @Bean + @Qualifier("public") + public TestBean publicInstance() { + return new TestBean("publicInstance"); + } + + // use of a custom qualifier and autowiring of method parameters + @Bean + protected TestBean protectedInstance( + @Qualifier("public") TestBean spouse, + @Value("#{privateInstance.age}") String country) { + TestBean tb = new TestBean("protectedInstance", 1); + tb.setSpouse(spouse); + tb.setCountry(country); + return tb; + } + + @Bean + private TestBean privateInstance() { + return new TestBean("privateInstance", i++); + } + + @Bean + @RequestScope + public TestBean requestScopedInstance() { + return new TestBean("requestScopedInstance", 3); + } +} +``` + +Kotlin + +``` +@Component +class FactoryMethodComponent { + + companion object { + private var i: Int = 0 + } + + @Bean + @Qualifier("public") + fun publicInstance() = TestBean("publicInstance") + + // use of a custom qualifier and autowiring of method parameters + @Bean + protected fun protectedInstance( + @Qualifier("public") spouse: TestBean, + @Value("#{privateInstance.age}") country: String) = TestBean("protectedInstance", 1).apply { + this.spouse = spouse + this.country = country + } + + @Bean + private fun privateInstance() = TestBean("privateInstance", i++) + + @Bean + @RequestScope + fun requestScopedInstance() = TestBean("requestScopedInstance", 3) +} +``` + +该示例将`String`方法参数`country`自动连接到另一个名为`privateInstance`的 Bean 上的`age`属性的值。 Spring 表达式语言元素通过记号`#{ }`来定义该属性的值。对于`@Value`注释,表达式解析程序预先配置为在解析表达式文本时查找 Bean 名称。 + +在 Spring Framework4.3 中,还可以声明类型为 `injectionPoint’的工厂方法参数(或其更具体的子类:`DependencyDescriptor`),以访问触发当前 Bean 创建的请求注入点。请注意,这仅适用于 Bean 实例的实际创建,而不适用于现有实例的注入。因此,对于原型范围的 bean 来说,这个特性是最有意义的。对于其他作用域,工厂方法只会看到在给定的作用域中触发创建新 Bean 实例的注入点(例如,触发创建惰性单例 Bean 的依赖项)。在这样的场景中,你可以使用提供的注入点元数据进行语义维护。下面的示例展示了如何使用`InjectionPoint`: + +爪哇 + +``` +@Component +public class FactoryMethodComponent { + + @Bean @Scope("prototype") + public TestBean prototypeInstance(InjectionPoint injectionPoint) { + return new TestBean("prototypeInstance for " + injectionPoint.getMember()); + } +} +``` + +Kotlin + +``` +@Component +class FactoryMethodComponent { + + @Bean + @Scope("prototype") + fun prototypeInstance(injectionPoint: InjectionPoint) = + TestBean("prototypeInstance for ${injectionPoint.member}") +} +``` + +普通 Spring 组件中的`@Bean`方法的处理方式与 Spring `@Configuration`类中的方法的处理方式不同。不同之处在于,`@Component`类不会用 CGlib 进行增强,以拦截方法和字段的调用。CGLIB 代理是在`@Configuration`中调用`@Bean`方法中的方法或字段创建 Bean 对协作对象的元数据引用的一种方法。这样的方法不是用普通的 爪哇 语义来调用的,而是通过容器来提供 Spring bean 的通常的生命周期管理和代理,即使是通过对`@Bean`方法的编程调用来引用其他 bean 时也是如此。相反,在普通的`@Component`类中调用`@Bean`方法中的方法或字段具有标准的 爪哇 语义,没有应用特殊的 CGlib 处理或其他约束。 + +| |你可以将`@Bean`方法声明为`static`,这样就可以调用它们,而不需要将其包含的配置类创建为实例。在定义后处理器 bean(例如,类型`BeanFactoryPostProcessor`或`BeanPostProcessor`)时,这使得
具有特殊意义,因为这样的 bean 在容器
生命周期的早期就被初始化了,并且应该避免在那个时间点触发配置的其他部分,

对静态`@Bean`方法的调用永远不会被容器拦截,甚至在 `@configuration’类中也不会被拦截,(如本节前面所述),由于技术
的限制:CGLIB 子类只能覆盖非静态方法。因此,
直接调用另一个`@Bean`方法具有标准的 爪哇 语义,结果
在一个独立的实例中被直接从工厂方法本身返回。

`@Bean`方法的 爪哇 语言可见性对
Spring 容器中的结果 Bean 定义没有立即的影响。你可以自由地声明你的
工厂方法,这在非 `@configuration’类中是合适的,在任何地方也适用于静态
方法。但是,`@Bean`类中的常规`@Configuration`方法需要
才能被重写——也就是说,它们不能被声明为`private`或`final`。

@ Bean ` 方法也可以在给定组件的基类上发现,或者
配置类,以及在接口
中声明的 8 个默认方法上由组件或配置类实现。这允许在组成复杂的配置安排时有很多
的灵活性,即使是多个
继承也可以通过 爪哇8 的默认方法实现,截至 Spring 4.2,

最后,对于相同的
Bean,单个类可以容纳多个`@Bean`方法,根据运行时可用的
依赖关系,作为使用的多个工厂方法的安排。这与在其他配置场景中选择“greediest”
构造函数或工厂方法的算法相同:具有
的变量在构造时选择最大数量的可满足依赖项,
类似于容器如何在多个`@Autowired`构造函数之间进行选择。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.10.6.命名自动检测到的组件 + +当组件作为扫描过程的一部分被自动检测时,其名称由该扫描仪已知的`BeanNameGenerator`策略生成。默认情况下,任何包含名称`value`的 Spring 原型注释(`@component`,`@Repository`,`@Service`,和 `@controller`)都将该名称提供给相应的 Bean 定义。 + +如果这样的注释不包含名称`value`或任何其他检测到的组件(例如由自定义过滤器发现的组件),则默认的 Bean Name Generator 返回未大写的非限定类名称。例如,如果检测到以下组件类,则名称将为`myMovieLister`和`movieFinderImpl`: + +爪哇 + +``` +@Service("myMovieLister") +public class SimpleMovieLister { + // ... +} +``` + +Kotlin + +``` +@Service("myMovieLister") +class SimpleMovieLister { + // ... +} +``` + +爪哇 + +``` +@Repository +public class MovieFinderImpl implements MovieFinder { + // ... +} +``` + +Kotlin + +``` +@Repository +class MovieFinderImpl : MovieFinder { + // ... +} +``` + +如果不想依赖默认的 Bean 命名策略,则可以提供自定义的 Bean 命名策略。首先,实现[“BeannameGenerator”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/factory/support/BeanNameGenerator.html)接口,并确保包含一个默认的无参数构造函数。然后,在配置扫描仪时提供完全限定的类名,如下面的示例注释和 Bean 定义所示。 + +| |如果由于多个具有
相同的非限定类名称(即名称相同但位于
不同包中的类)的自动检测组件而导致命名冲突,则可能需要为生成的 Bean 名称配置一个`BeanNameGenerator`默认为
完全限定类名称的`BeanNameGenerator`类。截至 Spring 框架 5.2.3,位于包 `org.SpringFramework.Context.Annotation’中的 `FullyQualifiedAnnotationBeannameGenerator’可用于此目的。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +爪哇 + +``` +@Configuration +@ComponentScan(basePackages = "org.example", nameGenerator = MyNameGenerator.class) +public class AppConfig { + // ... +} +``` + +Kotlin + +``` +@Configuration +@ComponentScan(basePackages = ["org.example"], nameGenerator = MyNameGenerator::class) +class AppConfig { + // ... +} +``` + +``` + + + +``` + +作为一条一般规则,当其他组件可能对其进行显式引用时,可以考虑使用注释来指定名称。另一方面,只要容器负责布线,自动生成的名称就足够了。 + +#### 1.10.7.为自动检测的组件提供一个范围 + +与一般的 Spring-管理组件一样,自动检测组件的默认和最常见的作用域是`singleton`。然而,有时你需要一个不同的作用域,该作用域可以由`@Scope`注释指定。你可以在注释中提供作用域的名称,如下例所示: + +爪哇 + +``` +@Scope("prototype") +@Repository +public class MovieFinderImpl implements MovieFinder { + // ... +} +``` + +Kotlin + +``` +@Scope("prototype") +@Repository +class MovieFinderImpl : MovieFinder { + // ... +} +``` + +| |`@Scope`注释仅在具体的 Bean 类(对于注释的
组件)或工厂方法(对于`@Bean`方法)上进行内省。与 XML Bean
定义相反,没有 Bean 定义继承的概念,并且在类级别上的继承
层次结构对于元数据目的是无关的。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +有关 Spring 上下文中的“请求”或“会话”等特定于 Web 的作用域的详细信息,请参见[Request, Session, Application, and WebSocket Scopes](#beans-factory-scopes-other)。与这些作用域的预构建注释一样,你也可以使用 Spring 的元注释方法来编写自己的范围注释:例如,使用`@Scope("prototype")`进行自定义注释的元注释,也可能声明自定义范围代理模式。 + +| |为了提供用于范围解析的自定义策略,而不是依赖于
基于注释的方法,你可以实现[ScopeMetaDataResolver](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/annotation/ScopeMetadataResolver.html)接口。一定要包含一个默认的无参数构造函数。然后可以在配置扫描仪时提供
完全限定的类名,如以下
注释和 Bean 定义的示例所示:| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +爪哇 + +``` +@Configuration +@ComponentScan(basePackages = "org.example", scopeResolver = MyScopeResolver.class) +public class AppConfig { + // ... +} +``` + +Kotlin + +``` +@Configuration +@ComponentScan(basePackages = ["org.example"], scopeResolver = MyScopeResolver::class) +class AppConfig { + // ... +} +``` + +``` + + + +``` + +当使用某些非单例作用域时,可能需要为作用域对象生成代理。推理在[作为依赖项的作用域 bean](#beans-factory-scopes-other-injection)中进行了描述。为此,在 Component-Scan 元素上提供了一个作用域-Proxy 属性。这三个可能的值是:`no`,`interfaces`,和`targetClass`。例如,以下配置将生成标准的 JDK 动态代理: + +爪哇 + +``` +@Configuration +@ComponentScan(basePackages = "org.example", scopedProxy = ScopedProxyMode.INTERFACES) +public class AppConfig { + // ... +} +``` + +Kotlin + +``` +@Configuration +@ComponentScan(basePackages = ["org.example"], scopedProxy = ScopedProxyMode.INTERFACES) +class AppConfig { + // ... +} +``` + +``` + + + +``` + +#### 1.10.8.提供带有注释的限定符元数据 + +`@Qualifier`注释在[使用限定符对基于注释的自动连线进行微调](#beans-autowired-annotation-qualifiers)中讨论。该部分中的示例演示了在解析 AutoWire 候选项时使用`@Qualifier`注释和自定义限定符注释来提供细粒度的控制。因为这些示例是基于 XML Bean 定义的,所以通过在 XML 中使用`qualifier`或`meta`元素中的`bean`子元素,在候选 Bean 定义上提供了限定符元数据。当依赖 Classpath 扫描来自动检测组件时,你可以在候选类上为限定符元数据提供类型级别的注释。以下三个示例演示了这种技术: + +爪哇 + +``` +@Component +@Qualifier("Action") +public class ActionMovieCatalog implements MovieCatalog { + // ... +} +``` + +Kotlin + +``` +@Component +@Qualifier("Action") +class ActionMovieCatalog : MovieCatalog +``` + +爪哇 + +``` +@Component +@Genre("Action") +public class ActionMovieCatalog implements MovieCatalog { + // ... +} +``` + +Kotlin + +``` +@Component +@Genre("Action") +class ActionMovieCatalog : MovieCatalog { + // ... +} +``` + +爪哇 + +``` +@Component +@Offline +public class CachingMovieCatalog implements MovieCatalog { + // ... +} +``` + +Kotlin + +``` +@Component +@Offline +class CachingMovieCatalog : MovieCatalog { + // ... +} +``` + +| |与大多数基于注释的替代方法一样,请记住注释元数据是
绑定到类定义本身的,而使用 XML 允许多个相同类型的 bean
在其限定符元数据中提供变体,因为
元数据是按实例而不是按类提供的。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.10.9.生成候选组件的索引 + +Classpath 虽然扫描非常快,但通过在编译时创建候选的静态列表,可以提高大型应用程序的启动性能。在这种模式下,所有组件扫描的目标模块都必须使用这种机制。 + +| |你现有的`@ComponentScan`或``指令必须保持
不变,以请求上下文扫描某些包中的候选项。当“ApplicationContext”检测到这样的索引时,它会自动使用它,而不是扫描
Classpath。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +要生成索引,请向每个包含组件的模块添加一个附加依赖项,这些组件是组件扫描指令的目标。下面的示例展示了如何使用 Maven 来实现这一点: + +``` + + + org.springframework + spring-context-indexer + 5.3.16 + true + + +``` + +对于 Gradle 4.5 或更早的版本,应该在`compileOnly`配置中声明依赖项,如下例所示: + +``` +dependencies { + compileOnly "org.springframework:spring-context-indexer:5.3.16" +} +``` + +对于 Gradle 4.6 及更高版本,依赖关系应该在`annotationProcessor`配置中声明,如以下示例所示: + +``` +dependencies { + annotationProcessor "org.springframework:spring-context-indexer:5.3.16" +} +``` + +`spring-context-indexer`工件生成一个`META-INF/spring.components`文件,该文件包含在 jar 文件中。 + +| |在 IDE 中使用此模式时,`spring-context-indexer`必须将
注册为注释处理器,以确保在更新
候选组件时索引是最新的。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |当在 Classpath 上找到`META-INF/spring.components`文件
时,将自动启用索引。如果对于某些库(或用例)
有部分可用的索引,但无法为整个应用程序构建索引,则可以通过将
设置为 `true’,返回到常规的 Classpath
安排(就像根本没有索引一样),可以作为 JVM 系统属性,也可以通过[“SpringProperties”](appendix.html#appendix-spring-properties)机制。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 1.11.使用 JSR330 标准注释 + +从 Spring 3.0 开始, Spring 提供了对 JSR-330 标准注释(依赖注入)的支持。以与 Spring 注释相同的方式扫描这些注释。要使用它们,你需要在你的 Classpath 中有相关的罐子。 + +| |如果你使用 Maven,则`javax.inject`工件在标准 Maven
存储库([https://repo1.maven.org/maven2/javax/inject/javax.inject/1/](https://repo1.maven.org/maven2/javax/inject/javax.inject/1/))中可用。
你可以将以下依赖项添加到你的文件 POM.xml:




javax=2583”/>”javav=2587“/>”>“>”gt="gt=2587| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.11.1.依赖注入与`@Inject`和`@Named` + +而不是`@Autowired`,你可以使用`@javax.inject.Inject`,如下所示: + +爪哇 + +``` +import javax.inject.Inject; + +public class SimpleMovieLister { + + private MovieFinder movieFinder; + + @Inject + public void setMovieFinder(MovieFinder movieFinder) { + this.movieFinder = movieFinder; + } + + public void listMovies() { + this.movieFinder.findMovies(...); + // ... + } +} +``` + +Kotlin + +``` +import javax.inject.Inject + +class SimpleMovieLister { + + @Inject + lateinit var movieFinder: MovieFinder + + fun listMovies() { + movieFinder.findMovies(...) + // ... + } +} +``` + +与`@Autowired`一样,你可以在字段级、方法级和构造函数参数级使用`@Inject`。此外,你可以将注入点声明为“provider”,允许按需访问较短范围的 bean,或者通过`Provider.get()`调用延迟访问其他 bean。下面的示例提供了前面示例的一个变体: + +爪哇 + +``` +import javax.inject.Inject; +import javax.inject.Provider; + +public class SimpleMovieLister { + + private Provider movieFinder; + + @Inject + public void setMovieFinder(Provider movieFinder) { + this.movieFinder = movieFinder; + } + + public void listMovies() { + this.movieFinder.get().findMovies(...); + // ... + } +} +``` + +Kotlin + +``` +import javax.inject.Inject + +class SimpleMovieLister { + + @Inject + lateinit var movieFinder: MovieFinder + + fun listMovies() { + movieFinder.findMovies(...) + // ... + } +} +``` + +如果你希望为应该注入的依赖项使用限定名称,那么你应该使用`@Named`注释,如下例所示: + +爪哇 + +``` +import javax.inject.Inject; +import javax.inject.Named; + +public class SimpleMovieLister { + + private MovieFinder movieFinder; + + @Inject + public void setMovieFinder(@Named("main") MovieFinder movieFinder) { + this.movieFinder = movieFinder; + } + + // ... +} +``` + +Kotlin + +``` +import javax.inject.Inject +import javax.inject.Named + +class SimpleMovieLister { + + private lateinit var movieFinder: MovieFinder + + @Inject + fun setMovieFinder(@Named("main") movieFinder: MovieFinder) { + this.movieFinder = movieFinder + } + + // ... +} +``` + +与`@Autowired`一样,`@Inject`也可以与`java.util.Optional`或 `@nullable’一起使用。这在这里甚至更适用,因为`@Inject`不具有`required`属性。以下两个示例展示了如何使用`@Inject`和 `@nullable’: + +``` +public class SimpleMovieLister { + + @Inject + public void setMovieFinder(Optional movieFinder) { + // ... + } +} +``` + +爪哇 + +``` +public class SimpleMovieLister { + + @Inject + public void setMovieFinder(@Nullable MovieFinder movieFinder) { + // ... + } +} +``` + +Kotlin + +``` +class SimpleMovieLister { + + @Inject + var movieFinder: MovieFinder? = null +} +``` + +#### 1.11.2.`@Named`和`@ManagedBean`:`@Component`注释的标准等价物 + +而不是`@Component`,你可以使用`@javax.inject.Named`或`javax.annotation.ManagedBean`,如下例所示: + +爪哇 + +``` +import javax.inject.Inject; +import javax.inject.Named; + +@Named("movieListener") // @ManagedBean("movieListener") could be used as well +public class SimpleMovieLister { + + private MovieFinder movieFinder; + + @Inject + public void setMovieFinder(MovieFinder movieFinder) { + this.movieFinder = movieFinder; + } + + // ... +} +``` + +Kotlin + +``` +import javax.inject.Inject +import javax.inject.Named + +@Named("movieListener") // @ManagedBean("movieListener") could be used as well +class SimpleMovieLister { + + @Inject + lateinit var movieFinder: MovieFinder + + // ... +} +``` + +在不指定组件名称的情况下使用`@Component`是非常常见的。@named 可以以类似的方式使用,如下例所示: + +爪哇 + +``` +import javax.inject.Inject; +import javax.inject.Named; + +@Named +public class SimpleMovieLister { + + private MovieFinder movieFinder; + + @Inject + public void setMovieFinder(MovieFinder movieFinder) { + this.movieFinder = movieFinder; + } + + // ... +} +``` + +Kotlin + +``` +import javax.inject.Inject +import javax.inject.Named + +@Named +class SimpleMovieLister { + + @Inject + lateinit var movieFinder: MovieFinder + + // ... +} +``` + +当你使用`@Named`或`@ManagedBean`时,你可以使用组件扫描,其方式与使用 Spring 注释时的方式完全相同,如下例所示: + +爪哇 + +``` +@Configuration +@ComponentScan(basePackages = "org.example") +public class AppConfig { + // ... +} +``` + +Kotlin + +``` +@Configuration +@ComponentScan(basePackages = ["org.example"]) +class AppConfig { + // ... +} +``` + +| |与`@Component`相反,JSR-330`@Named`和 JSR-250`@ManagedBean`注释是不可组合的。你应该使用 Spring 的原型模型来构建
自定义组件注释。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.11.3.JSR-330 标准注释的局限性 + +当你使用标准注释时,你应该知道一些重要的特性是不可用的,如下表所示: + +| Spring | javax.inject.\* |javax.inject 限制/注释| +|-------------------|---------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| @Autowired | @Inject |`@Inject`没有“required”属性。可以与 爪哇8 的`Optional`一起使用。| +| @Component |@Named / @ManagedBean|JSR-330 不提供可组合的模型,只提供一种识别命名组件的方法。| +|@Scope("singleton")| @Singleton |JSR-330 缺省作用域类似于 Spring 的`prototype`。然而,为了使其
与 Spring 的一般默认值保持一致,在 Spring
容器中声明的 JSR-330 Bean 默认为`singleton`。为了使用`singleton`、
以外的作用域,你应该使用 Spring 的`@Scope`注释。`javax.inject`还提供了[@Scope](https://download.oracle.com/javaee/6/api/javax/inject/Scope.html)注释。
尽管如此,这个注释仅用于创建你自己的注释。| +| @Qualifier | @Qualifier / @Named |`javax.inject.Qualifier`只是构建自定义限定符的元注释。
具体`String`限定符(就像 Spring 的`@Qualifier`与一个值)可以关联
到`javax.inject.Named`。| +| @Value | \- |没有等效的| +| @Required | \- |没有等效的| +| @Lazy | \- |没有等效的| +| ObjectFactory | Provider |`javax.inject.Provider`是 Spring 的`ObjectFactory`,
的直接替代方法,只使用更短的`get()`方法名。它也可以与
Spring 的`@Autowired`结合使用,或者与非注释的构造函数和 setter 方法结合使用。| + +### 1.12.基于 爪哇 的容器配置 + +本节介绍如何在 爪哇 代码中使用注释来配置 Spring 容器。它包括以下主题: + +* [Basic Concepts: `@Bean` and `@Configuration`](#beans-java-basic-concepts) + +* [Instantiating the Spring Container by Using `AnnotationConfigApplicationContext`](#beans-java-instantiating-container) + +* [Using the `@Bean` Annotation](#beans-java-bean-annotation) + +* [Using the `@Configuration` annotation](#beans-java-configuration-annotation) + +* [编写基于 爪哇 的配置](#beans-java-composing-configuration-classes) + +* [Bean Definition Profiles](#beans-definition-profiles) + +* [“PropertySource”抽象](#beans-property-source-abstraction) + +* [Using `@PropertySource`](#beans-using-propertysource) + +* [语句中的占位符解析](#beans-placeholder-resolution-in-statements) + +#### 1.12.1.基本概念:`@Bean`和`@Configuration` + +Spring 新的 爪哇-Configuration 支持中的核心构件是“@configuration”-带注释的类和`@Bean`-带注释的方法。 + +`@Bean`注释用于指示方法实例化、配置和初始化一个新对象,该对象将由 Spring IOC 容器管理。对于那些熟悉 Spring 的``XML 配置的人来说,`@Bean`注释所起的作用与``元素相同。你可以使用`@Bean`-带注释的方法处理任何 Spring `@component`。然而,它们最常用于`@Configuration`bean。 + +用`@Configuration`注释一个类表明,它的主要目的是作为 Bean 定义的来源。此外,`@Configuration`类通过调用同一类中的其他`@Bean`方法来定义 Bean 之间的依赖关系。最简单的`@Configuration`类如下: + +爪哇 + +``` +@Configuration +public class AppConfig { + + @Bean + public MyService myService() { + return new MyServiceImpl(); + } +} +``` + +Kotlin + +``` +@Configuration +class AppConfig { + + @Bean + fun myService(): MyService { + return MyServiceImpl() + } +} +``` + +前面的`AppConfig`类等价于下面的 Spring ``XML: + +``` + + + +``` + +完整的 @ 配置 VS“Lite”@ Bean 模式? + +当`@Bean`方法在没有使用 `@Configuration’注释的类中声明时,它们被称为正在以“精简”模式进行处理。 Bean 在`@Component`中声明的方法,甚至在一个普通的旧类中声明的方法,都被认为是“lite”,具有不同的包含类的主要目的,而`@Bean`方法是那里的一种奖励。例如,服务组件可以在每个适用的组件类上通过一个额外的`@Bean`方法向容器公开管理视图。在这样的场景中,`@Bean`方法是一种通用的工厂方法机制。 + +与 full`@Configuration`不同,lite`@Bean`方法不能声明 Bean 之间的依赖关系。相反,它们对包含它们的组件的内部状态进行操作,并可选地对它们可能声明的参数进行操作。因此,这样的`@Bean`方法不应该调用其他 `@ Bean ` 方法。每个这样的方法实际上只是用于特定 Bean 引用的工厂方法,没有任何特殊的运行时语义。这里的积极的副作用是,在运行时不需要应用 CGLIB 子类,因此在类设计方面没有限制(即,包含的类可能是`final`等)。 + +在常见的场景中,`@Bean`方法要在`@Configuration`类中声明,以确保始终使用“完全”模式,并确保跨方法引用因此被重定向到容器的生命周期管理。这可以防止通过常规的 爪哇 调用意外调用相同的“@ Bean”方法,这有助于减少在“精简”模式下操作时很难追踪到的细微错误。 + +下面的部分将深入讨论`@Bean`和`@Configuration`注释。然而,首先,我们介绍了通过使用基于 爪哇 的配置来创建 Spring 容器的各种方法。 + +#### 1.12.2.使用`AnnotationConfigApplicationContext`### 实例化 Spring 容器 + +下面的部分记录了 Spring 的`AnnotationConfigApplicationContext`,在 Spring 3.0 中介绍了它。这种通用的`ApplicationContext`实现不仅能够接受 `@Configuration’类作为输入,而且还能够接受用 JSR-330 元数据注释的普通`@Component`类和类。 + +当`@Configuration`类被提供为输入时,`@Configuration`类本身被注册为 Bean 定义,并且类中所有声明的`@Bean`方法也被注册为 Bean 定义。 + +当`@Component`和 JSR-330 类被提供时,它们被注册为 Bean 定义,并且假定在必要的情况下,在那些类中使用诸如`@Autowired`或`@Inject`的 DI 元数据。 + +##### 简单的构造 + +就像 Spring XML 文件在实例化 `ClassPathXMLApplicationContext’时用作输入一样,在实例化`@Configuration`时,可以使用`AnnotationConfigApplicationContext`类作为输入。这允许完全无 XML 地使用 Spring 容器,如下例所示: + +爪哇 + +``` +public static void main(String[] args) { + ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class); + MyService myService = ctx.getBean(MyService.class); + myService.doStuff(); +} +``` + +Kotlin + +``` +import org.springframework.beans.factory.getBean + +fun main() { + val ctx = AnnotationConfigApplicationContext(AppConfig::class.java) + val myService = ctx.getBean() + myService.doStuff() +} +``` + +如前所述,`AnnotationConfigApplicationContext`并不限于仅与`@Configuration`类一起工作。任何`@Component`或 JSR-330 注释类都可以作为构造函数的输入提供,如下例所示: + +爪哇 + +``` +public static void main(String[] args) { + ApplicationContext ctx = new AnnotationConfigApplicationContext(MyServiceImpl.class, Dependency1.class, Dependency2.class); + MyService myService = ctx.getBean(MyService.class); + myService.doStuff(); +} +``` + +Kotlin + +``` +import org.springframework.beans.factory.getBean + +fun main() { + val ctx = AnnotationConfigApplicationContext(MyServiceImpl::class.java, Dependency1::class.java, Dependency2::class.java) + val myService = ctx.getBean() + myService.doStuff() +} +``` + +前面的示例假设`MyServiceImpl`、`Dependency1`和`Dependency2`使用 Spring 依赖注入注释,例如`@Autowired`。 + +##### 通过使用`register(Class…​)`#### 以编程方式构建容器 + +你可以使用 no-arg 构造函数实例化`AnnotationConfigApplicationContext`,然后使用`register()`方法对其进行配置。当以编程方式构建`AnnotationConfigApplicationContext`时,这种方法特别有用。下面的示例展示了如何做到这一点: + +爪哇 + +``` +public static void main(String[] args) { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(AppConfig.class, OtherConfig.class); + ctx.register(AdditionalConfig.class); + ctx.refresh(); + MyService myService = ctx.getBean(MyService.class); + myService.doStuff(); +} +``` + +Kotlin + +``` +import org.springframework.beans.factory.getBean + +fun main() { + val ctx = AnnotationConfigApplicationContext() + ctx.register(AppConfig::class.java, OtherConfig::class.java) + ctx.register(AdditionalConfig::class.java) + ctx.refresh() + val myService = ctx.getBean() + myService.doStuff() +} +``` + +##### 使用`scan(String…​)`启用组件扫描 + +要启用组件扫描,你可以对`@Configuration`类作如下注释: + +爪哇 + +``` +@Configuration +@ComponentScan(basePackages = "com.acme") (1) +public class AppConfig { + // ... +} +``` + +|**1**|此注释使组件扫描成为可能。| +|-----|-------------------------------------------| + +Kotlin + +``` +@Configuration +@ComponentScan(basePackages = ["com.acme"]) (1) +class AppConfig { + // ... +} +``` + +|**1**|此注释使组件扫描成为可能。| +|-----|-------------------------------------------| + +| |有经验的 Spring 用户可能熟悉来自
Spring 的`context:`名称空间的等效 XML 声明,如以下示例所示:




<>>>>
<2722"| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在前面的示例中,扫描`com.acme`包以查找任何 `@component’-注释的类,并且这些类被注册为容器内的 Spring Bean 定义。`AnnotationConfigApplicationContext`公开了 `scan’方法,以允许相同的组件扫描功能,如下例所示: + +爪哇 + +``` +public static void main(String[] args) { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.scan("com.acme"); + ctx.refresh(); + MyService myService = ctx.getBean(MyService.class); +} +``` + +Kotlin + +``` +fun main() { + val ctx = AnnotationConfigApplicationContext() + ctx.scan("com.acme") + ctx.refresh() + val myService = ctx.getBean() +} +``` + +| |请记住,`@Configuration`类是[meta-annotated](#beans-meta-annotations)和`@Component`类,因此它们是组件扫描的候选对象。在前面的示例中,假设
在`com.acme`包(或下面的任何包
)中声明了`AppConfig`,则在调用`scan()`时将其拾取。在`refresh()`时,其所有`@Bean`方法都被处理并注册为容器内的 Bean 定义。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 对`AnnotationConfigWebApplicationContext`#### 的 Web 应用程序的支持 + +一个`WebApplicationContext`的`AnnotationConfigApplicationContext`变种可与`AnnotationConfigWebApplicationContext`一起使用。你可以在配置 Spring `ContextLoaderListener` Servlet 侦听器、 Spring MVC`DispatcherServlet’等时使用此实现。下面的`web.xml`片段配置了典型的 Spring MVC Web 应用程序(请注意使用`contextClass`context-param 和 init-param): + +``` + + + + contextClass + + org.springframework.web.context.support.AnnotationConfigWebApplicationContext + + + + + + contextConfigLocation + com.acme.AppConfig + + + + + org.springframework.web.context.ContextLoaderListener + + + + + dispatcher + org.springframework.web.servlet.DispatcherServlet + + + contextClass + + org.springframework.web.context.support.AnnotationConfigWebApplicationContext + + + + + contextConfigLocation + com.acme.web.MvcConfig + + + + + + dispatcher + /app/* + + +``` + +| |对于编程用例,`GenericWebApplicationContext`可以用作
的`AnnotationConfigWebApplicationContext`的替代。有关详细信息,请参见[GenericWebApplicationContext](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/context/support/GenericWebApplicationContext.html)爪哇doc。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.12.3.使用`@Bean`注释 + +`@Bean`是一个方法级的注释,是 XML``元素的直接模拟。该注释支持``提供的一些属性,例如: + +* [init-method](#beans-factory-lifecycle-initializingbean) + +* [destroy-method](#beans-factory-lifecycle-disposablebean) + +* [autowiring](#beans-factory-autowire) + +* `name`. + +你可以在`@Configuration`-注释的类中使用`@Bean`注释或在 `@component`-注释的类中使用`@Bean`注释。 + +##### 宣告 A Bean + +要声明 Bean,你可以使用`@Bean`注释对方法进行注释。你可以使用此方法在`ApplicationContext`中注册一个 Bean 定义,该定义的类型指定为方法的返回值。默认情况下, Bean 名称与方法名称相同。下面的示例显示了`@Bean`方法声明: + +爪哇 + +``` +@Configuration +public class AppConfig { + + @Bean + public TransferServiceImpl transferService() { + return new TransferServiceImpl(); + } +} +``` + +Kotlin + +``` +@Configuration +class AppConfig { + + @Bean + fun transferService() = TransferServiceImpl() +} +``` + +前面的配置与下面的 Spring XML 完全等价: + +``` + + + +``` + +这两个声明使得名为`transferService`的 Bean 在“ApplicationContext”中可用,并绑定到类型`TransferServiceImpl`的对象实例,如以下文本图像所示: + +``` +transferService -> com.acme.TransferServiceImpl +``` + +你也可以使用默认方法来定义 bean。这允许通过在默认方法上实现带有 Bean 定义的接口来组合 Bean 配置。 + +爪哇 + +``` +public interface BaseConfig { + + @Bean + default TransferServiceImpl transferService() { + return new TransferServiceImpl(); + } +} + +@Configuration +public class AppConfig implements BaseConfig { + +} +``` + +你还可以使用接口(或基类)返回类型声明你的`@Bean`方法,如下例所示: + +爪哇 + +``` +@Configuration +public class AppConfig { + + @Bean + public TransferService transferService() { + return new TransferServiceImpl(); + } +} +``` + +Kotlin + +``` +@Configuration +class AppConfig { + + @Bean + fun transferService(): TransferService { + return TransferServiceImpl() + } +} +``` + +但是,这将预先类型预测的可见性限制为指定的接口类型(“TransferService”)。然后,只在受影响的单例 Bean 被实例化之后,使用容器已知的完整类型。非惰性单例 bean 将根据其声明顺序进行实例化,因此你可能会看到不同的类型匹配结果,这取决于另一个组件何时尝试使用未声明的类型进行匹配(例如`@Autowired TransferServiceImpl`,它仅在`transferService` Bean 被实例化之后进行解析)。 + +| |如果你始终使用声明的服务接口来引用你的类型,那么你的“@ Bean”返回类型可以安全地加入该设计决策。但是,对于实现多个接口的组件
,或者对于由其
实现类型可能引用的组件,更安全的做法是声明尽可能具体的返回类型
(至少与引用你的 Bean 的注入点所要求的特定类型一样)。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### Bean 相依性 + +带注释的`@Bean`方法可以具有任意数量的参数,这些参数描述了构建 Bean 所需的依赖关系。例如,如果我们的`TransferService`需要`AccountRepository`,则可以使用方法参数实现该依赖关系,如下例所示: + +爪哇 + +``` +@Configuration +public class AppConfig { + + @Bean + public TransferService transferService(AccountRepository accountRepository) { + return new TransferServiceImpl(accountRepository); + } +} +``` + +Kotlin + +``` +@Configuration +class AppConfig { + + @Bean + fun transferService(accountRepository: AccountRepository): TransferService { + return TransferServiceImpl(accountRepository) + } +} +``` + +解析机制与基于构造函数的依赖注入几乎相同。有关更多详细信息,请参见[有关部分](#beans-constructor-injection)。 + +##### 接收生命周期回调 + +使用`@Bean`注释定义的任何类都支持常规的生命周期回调,并且可以使用 JSR-250 中的`@PostConstruct`和`@PreDestroy`注释。详情见[JSR-250 注解](#beans-postconstruct-and-predestroy-annotations)。 + +常规的 Spring [lifecycle](#beans-factory-nature)回调也完全支持。如果 Bean 实现了`InitializingBean`、`DisposableBean`或`Lifecycle`,则容器调用它们各自的方法。 + +标准的`*Aware`接口集合(如[BeanFactoryAware](#beans-beanfactory),[BeanNameAware](#beans-factory-aware),[MessagesourceAware](#context-functionality-messagesource),[应用情境软件](#beans-factory-aware),等等)也是完全支持的。 + +`@Bean`注释支持指定任意的初始化和销毁回调方法,很像 Spring XML 的`init-method`和`destroy-method`元素上的属性,如下例所示: + +爪哇 + +``` +public class BeanOne { + + public void init() { + // initialization logic + } +} + +public class BeanTwo { + + public void cleanup() { + // destruction logic + } +} + +@Configuration +public class AppConfig { + + @Bean(initMethod = "init") + public BeanOne beanOne() { + return new BeanOne(); + } + + @Bean(destroyMethod = "cleanup") + public BeanTwo beanTwo() { + return new BeanTwo(); + } +} +``` + +Kotlin + +``` +class BeanOne { + + fun init() { + // initialization logic + } +} + +class BeanTwo { + + fun cleanup() { + // destruction logic + } +} + +@Configuration +class AppConfig { + + @Bean(initMethod = "init") + fun beanOne() = BeanOne() + + @Bean(destroyMethod = "cleanup") + fun beanTwo() = BeanTwo() +} +``` + +| |默认情况下,使用 爪哇 配置定义的具有公共`close`或`shutdown`方法的 bean 将自动加入销毁回调。如果你有一个公共的 `close’或`shutdown`方法,并且不希望在容器
关闭时调用它,你可以将`@Bean(destroyMethod="")`添加到你的 Bean 定义中,以禁用
默认`(inferred)`模式。

对于你通过 JNDI 获得的资源,你可能希望在默认情况下这样做,因为其
生命周期是在应用程序之外管理的。特别是,对于`DataSource`,要确保始终执行
,因为它在 爪哇 EE 应用程序服务器上是有问题的。

下面的示例展示了如何防止自动销毁回调对于一个“数据源”:

爪哇

`
@ Bean
公共数据源数据源()抛出名称异常
返回 jndiource(“mydr”=“2815”/>“2816”/r=“2817”/>“<<19><<<<<>>>”gt=“<19"r="<19>>通过
使用 Spring 的`JndiTemplate`或`JndiLocatorDelegate`助手或直接使用 jndi`initialcontext` 用法,但不使用`JndiObjectFactoryBean`变体(这将强制
将返回类型声明为`FactoryBean`类型,而不是实际的目标
类型,使得在其他`@Bean`方法中使用交叉引用调用变得更加困难,这些方法
打算在此引用所提供的资源)。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在上面的例子`BeanOne`的情况下,在构造过程中直接调用`init()`方法将同样有效,如下例所示: + +爪哇 + +``` +@Configuration +public class AppConfig { + + @Bean + public BeanOne beanOne() { + BeanOne beanOne = new BeanOne(); + beanOne.init(); + return beanOne; + } + + // ... +} +``` + +Kotlin + +``` +@Configuration +class AppConfig { + + @Bean + fun beanOne() = BeanOne().apply { + init() + } + + // ... +} +``` + +| |当你直接在 爪哇 中工作时,你可以对对象执行任何你喜欢的操作,并且执行
,并不总是需要依赖于容器的生命周期。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------| + +##### 指定 Bean 范围 + +Spring 包括`@Scope`注释,以便可以指定 Bean 的范围。 + +###### 使用`@Scope`注释 + +你可以指定使用`@Bean`注释定义的 bean 应该具有特定的作用域。你可以使用[Bean Scopes](#beans-factory-scopes)部分中指定的任何标准作用域。 + +默认的作用域是`singleton`,但是你可以使用`@Scope`注释来覆盖此范围,如下例所示: + +爪哇 + +``` +@Configuration +public class MyConfiguration { + + @Bean + @Scope("prototype") + public Encryptor encryptor() { + // ... + } +} +``` + +Kotlin + +``` +@Configuration +class MyConfiguration { + + @Bean + @Scope("prototype") + fun encryptor(): Encryptor { + // ... + } +} +``` + +###### `@Scope`和`scoped-proxy` + +Spring 通过[scoped proxies](#beans-factory-scopes-other-injection)提供了一种处理作用域依赖关系的方便方法。在使用 XML 配置时,创建这样的代理的最简单的方法是``元素。使用`@Scope`注释在 爪哇 中配置 bean,可以提供与`proxyMode`属性相同的支持。默认值是`ScopedProxyMode.DEFAULT`,这通常表示除非在组件扫描指令级配置了不同的默认值,否则不应创建范围代理。可以指定 `ScopedProxyMode.target_class`,`ScopedProxyMode.INTERFACES`或`ScopedProxyMode.NO`。 + +如果你使用 爪哇 将范围代理示例从 XML 引用文档(参见[scoped proxies](#beans-factory-scopes-other-injection))移植到我们的`@Bean`,它类似于以下内容: + +爪哇 + +``` +// an HTTP Session-scoped bean exposed as a proxy +@Bean +@SessionScope +public UserPreferences userPreferences() { + return new UserPreferences(); +} + +@Bean +public Service userService() { + UserService service = new SimpleUserService(); + // a reference to the proxied userPreferences bean + service.setUserPreferences(userPreferences()); + return service; +} +``` + +Kotlin + +``` +// an HTTP Session-scoped bean exposed as a proxy +@Bean +@SessionScope +fun userPreferences() = UserPreferences() + +@Bean +fun userService(): Service { + return SimpleUserService().apply { + // a reference to the proxied userPreferences bean + setUserPreferences(userPreferences()) + } +} +``` + +##### 自定义 Bean 命名 + +默认情况下,配置类使用`@Bean`方法的名称作为结果 Bean 的名称。但是,可以使用`name`属性重写此功能,如下例所示: + +爪哇 + +``` +@Configuration +public class AppConfig { + + @Bean("myThing") + public Thing thing() { + return new Thing(); + } +} +``` + +Kotlin + +``` +@Configuration +class AppConfig { + + @Bean("myThing") + fun thing() = Thing() +} +``` + +##### Bean 别名 + +如[Naming Beans](#beans-beanname)中所讨论的,有时希望给出一个 Bean 多个名称,也称为 Bean 别名。为此目的,`@Bean`注释的`name`属性接受字符串数组。下面的示例显示了如何为 Bean 设置多个别名: + +爪哇 + +``` +@Configuration +public class AppConfig { + + @Bean({"dataSource", "subsystemA-dataSource", "subsystemB-dataSource"}) + public DataSource dataSource() { + // instantiate, configure and return DataSource bean... + } +} +``` + +Kotlin + +``` +@Configuration +class AppConfig { + + @Bean("dataSource", "subsystemA-dataSource", "subsystemB-dataSource") + fun dataSource(): DataSource { + // instantiate, configure and return DataSource bean... + } +} +``` + +##### Bean 描述 + +有时,提供对 A Bean 的更详细的文本描述是有帮助的。当公开 bean(可能通过 JMX)以进行监视时,这可能特别有用。 + +要向`@Bean`添加描述,可以使用[`@Description`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/annotation/Description.html)注释,如下例所示: + +爪哇 + +``` +@Configuration +public class AppConfig { + + @Bean + @Description("Provides a basic example of a bean") + public Thing thing() { + return new Thing(); + } +} +``` + +Kotlin + +``` +@Configuration +class AppConfig { + + @Bean + @Description("Provides a basic example of a bean") + fun thing() = Thing() +} +``` + +#### 1.12.4.使用`@Configuration`注释 + +`@Configuration`是一个类级注释,指示对象是 Bean 定义的源。`@Configuration`类通过`@Bean`-带注释的方法声明 bean。对`@Configuration`类上的`@Bean`方法的调用也可用于定义 Bean 之间的依赖关系。一般介绍见[Basic Concepts: `@Bean` and `@Configuration`](#beans-java-basic-concepts)。 + +##### 注入相互 Bean 的依赖关系 + +当 bean 彼此之间存在依赖关系时,表示这种依赖关系就像让一个方法调用另一个方法一样简单,如下例所示: + +爪哇 + +``` +@Configuration +public class AppConfig { + + @Bean + public BeanOne beanOne() { + return new BeanOne(beanTwo()); + } + + @Bean + public BeanTwo beanTwo() { + return new BeanTwo(); + } +} +``` + +Kotlin + +``` +@Configuration +class AppConfig { + + @Bean + fun beanOne() = BeanOne(beanTwo()) + + @Bean + fun beanTwo() = BeanTwo() +} +``` + +在前面的示例中,`beanOne`通过构造函数注入接收对`beanTwo`的引用。 + +| |这种声明互 Bean 依赖关系的方法仅在`@Bean`方法
在`@Configuration`类中声明时才有效。不能使用普通的`@Component`类来声明 Bean 之间的依赖关系
。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 查找方法注入 + +如前所述,[查找方法注入](#beans-factory-method-injection)是一个很少使用的高级特性。在单作用域 Bean 对原型作用域 Bean 具有依赖性的情况下,它是有用的。对这种类型的配置使用 爪哇 为实现这种模式提供了一种自然的方法。下面的示例展示了如何使用查找方法注入: + +爪哇 + +``` +public abstract class CommandManager { + public Object process(Object commandState) { + // grab a new instance of the appropriate Command interface + Command command = createCommand(); + // set the state on the (hopefully brand new) Command instance + command.setState(commandState); + return command.execute(); + } + + // okay... but where is the implementation of this method? + protected abstract Command createCommand(); +} +``` + +Kotlin + +``` +abstract class CommandManager { + fun process(commandState: Any): Any { + // grab a new instance of the appropriate Command interface + val command = createCommand() + // set the state on the (hopefully brand new) Command instance + command.setState(commandState) + return command.execute() + } + + // okay... but where is the implementation of this method? + protected abstract fun createCommand(): Command +} +``` + +通过使用 爪哇 配置,你可以创建`CommandManager`的子类,其中抽象`createCommand()`方法以这样一种方式被重写,即它会查找一个新的(原型)命令对象。下面的示例展示了如何做到这一点: + +爪哇 + +``` +@Bean +@Scope("prototype") +public AsyncCommand asyncCommand() { + AsyncCommand command = new AsyncCommand(); + // inject dependencies here as required + return command; +} + +@Bean +public CommandManager commandManager() { + // return new anonymous implementation of CommandManager with createCommand() + // overridden to return a new prototype Command object + return new CommandManager() { + protected Command createCommand() { + return asyncCommand(); + } + } +} +``` + +Kotlin + +``` +@Bean +@Scope("prototype") +fun asyncCommand(): AsyncCommand { + val command = AsyncCommand() + // inject dependencies here as required + return command +} + +@Bean +fun commandManager(): CommandManager { + // return new anonymous implementation of CommandManager with createCommand() + // overridden to return a new prototype Command object + return object : CommandManager() { + override fun createCommand(): Command { + return asyncCommand() + } + } +} +``` + +##### 有关基于 爪哇 的配置如何在内部工作的更多信息 ##### + +考虑以下示例,该示例显示了一个`@Bean`带注释的方法被调用了两次: + +爪哇 + +``` +@Configuration +public class AppConfig { + + @Bean + public ClientService clientService1() { + ClientServiceImpl clientService = new ClientServiceImpl(); + clientService.setClientDao(clientDao()); + return clientService; + } + + @Bean + public ClientService clientService2() { + ClientServiceImpl clientService = new ClientServiceImpl(); + clientService.setClientDao(clientDao()); + return clientService; + } + + @Bean + public ClientDao clientDao() { + return new ClientDaoImpl(); + } +} +``` + +Kotlin + +``` +@Configuration +class AppConfig { + + @Bean + fun clientService1(): ClientService { + return ClientServiceImpl().apply { + clientDao = clientDao() + } + } + + @Bean + fun clientService2(): ClientService { + return ClientServiceImpl().apply { + clientDao = clientDao() + } + } + + @Bean + fun clientDao(): ClientDao { + return ClientDaoImpl() + } +} +``` + +`clientDao()`在`clientService1()`中调用过一次,在`clientService2()`中调用过一次。由于此方法创建了`ClientDaoImpl`的新实例并返回它,因此你通常希望有两个实例(每个服务一个)。这肯定是有问题的:在 Spring 中,实例化的 bean 默认具有`singleton`作用域。这就是神奇之处:所有`@Configuration`类在启动时都用`CGLIB`子类。在子类中,子方法在调用父方法并创建新实例之前,首先检查容器中是否有任何缓存的(作用域)bean。 + +| |根据你的 Bean 的范围,行为可能是不同的。我们在这里讨论的是
关于单身汉的问题。| +|---|--------------------------------------------------------------------------------------------------------------| + +| |从 Spring 3.2 开始,不再需要将 CGLIB 添加到你的 Classpath 中,因为 CGLIB类已经在下重新打包,并直接包含在 Spring-core jar 中。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |由于 CGlib 在
启动时动态添加特性,因此有一些限制。特别是,配置类不能是最终的。但是,正如
of4.3,在配置类上允许使用任何构造函数,包括使用 `@autowired’或为默认注入使用单个非默认构造函数声明。

如果你希望避免任何 CGLIB 施加的限制,请考虑在非 `@configuration` 类上声明你的`@Bean`方法(例如,在普通的`@Component`类上)。
方法之间的跨方法调用不会被截获,因此你有
在构造函数或方法级别完全依赖于依赖注入。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.12.5.编写基于 爪哇 的配置 + +Spring 的基于 爪哇 的配置特性允许你编写注释,这可以降低配置的复杂性。 + +##### 使用`@Import`注释 + +正如在 Spring XML 文件中使用``元素来帮助模块化配置一样,`@Import`注释允许从另一个配置类加载`@Bean`定义,如下例所示: + +爪哇 + +``` +@Configuration +public class ConfigA { + + @Bean + public A a() { + return new A(); + } +} + +@Configuration +@Import(ConfigA.class) +public class ConfigB { + + @Bean + public B b() { + return new B(); + } +} +``` + +Kotlin + +``` +@Configuration +class ConfigA { + + @Bean + fun a() = A() +} + +@Configuration +@Import(ConfigA::class) +class ConfigB { + + @Bean + fun b() = B() +} +``` + +现在,在实例化上下文时,不需要同时指定`ConfigA.class`和`ConfigB.class`,只需要显式地提供`ConfigB`,如下例所示: + +爪哇 + +``` +public static void main(String[] args) { + ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class); + + // now both beans A and B will be available... + A a = ctx.getBean(A.class); + B b = ctx.getBean(B.class); +} +``` + +Kotlin + +``` +import org.springframework.beans.factory.getBean + +fun main() { + val ctx = AnnotationConfigApplicationContext(ConfigB::class.java) + + // now both beans A and B will be available... + val a = ctx.getBean() + val b = ctx.getBean() +} +``` + +这种方法简化了容器实例化,因为只需要处理一个类,而不是要求你在构建过程中记住可能大量的“@configuration”类。 + +| |在 Spring Framework4.2 中,`@Import`还支持对常规组件
类的引用,类似于`AnnotationConfigApplicationContext.register`方法。
如果你想要避免组件扫描,通过使用几个
配置类作为切入点来显式地定义所有组件,这是特别有用的。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +###### 对导入的`@Bean`定义注入依赖项 + +前面的例子是可行的,但过于简单化了。在大多数实际场景中,跨配置类的 bean 彼此具有依赖性。在使用 XML 时,这不是一个问题,因为不涉及编译器,你可以声明 `ref=“somebean”`,并信任 Spring 在容器初始化期间解决它。当使用`@Configuration`类时,Java 编译器对配置模型施加约束,因为对其他 bean 的引用必须是有效的 Java 语法。 + +幸运的是,解决这个问题很简单。作为[我们已经讨论过了](#beans-java-dependencies),`@Bean`方法可以具有任意数量的描述 Bean 依赖关系的参数。考虑以下更现实的场景,其中包含几个`@Configuration`类,每个类取决于在其他类中声明的 bean: + +Java + +``` +@Configuration +public class ServiceConfig { + + @Bean + public TransferService transferService(AccountRepository accountRepository) { + return new TransferServiceImpl(accountRepository); + } +} + +@Configuration +public class RepositoryConfig { + + @Bean + public AccountRepository accountRepository(DataSource dataSource) { + return new JdbcAccountRepository(dataSource); + } +} + +@Configuration +@Import({ServiceConfig.class, RepositoryConfig.class}) +public class SystemTestConfig { + + @Bean + public DataSource dataSource() { + // return new DataSource + } +} + +public static void main(String[] args) { + ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class); + // everything wires up across configuration classes... + TransferService transferService = ctx.getBean(TransferService.class); + transferService.transfer(100.00, "A123", "C456"); +} +``` + +Kotlin + +``` +import org.springframework.beans.factory.getBean + +@Configuration +class ServiceConfig { + + @Bean + fun transferService(accountRepository: AccountRepository): TransferService { + return TransferServiceImpl(accountRepository) + } +} + +@Configuration +class RepositoryConfig { + + @Bean + fun accountRepository(dataSource: DataSource): AccountRepository { + return JdbcAccountRepository(dataSource) + } +} + +@Configuration +@Import(ServiceConfig::class, RepositoryConfig::class) +class SystemTestConfig { + + @Bean + fun dataSource(): DataSource { + // return new DataSource + } +} + +fun main() { + val ctx = AnnotationConfigApplicationContext(SystemTestConfig::class.java) + // everything wires up across configuration classes... + val transferService = ctx.getBean() + transferService.transfer(100.00, "A123", "C456") +} +``` + +还有另一种方法可以达到同样的效果。请记住,`@Configuration`类最终只是容器中的另一个 Bean:这意味着它们可以利用与任何其他 Bean 相同的 `@autowired’和`@Value`注入和其他特征。 + +| |确保以这种方式注入的依赖关系仅是最简单的依赖关系。`@Configuration`类在上下文的初始化过程中很早就被处理了,并且强制以这种方式注入依赖项
可能会导致意外的早期初始化。只要有可能,就求助于
基于参数的注入,就像在前面的示例中一样。

此外,要特别小心`BeanPostProcessor`和`BeanFactoryPostProcessor`定义
通过`@Bean`。这些方法通常应该声明为`static @Bean`方法,而不是触发其包含的配置类的
实例化。否则,`@Autowired`和`@Value`可能不会在配置类本身上工作,因为可以在
之前将其创建为 Bean 实例。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +下面的示例显示了一个 Bean 如何能够自动连接到另一个 Bean: + +Java + +``` +@Configuration +public class ServiceConfig { + + @Autowired + private AccountRepository accountRepository; + + @Bean + public TransferService transferService() { + return new TransferServiceImpl(accountRepository); + } +} + +@Configuration +public class RepositoryConfig { + + private final DataSource dataSource; + + public RepositoryConfig(DataSource dataSource) { + this.dataSource = dataSource; + } + + @Bean + public AccountRepository accountRepository() { + return new JdbcAccountRepository(dataSource); + } +} + +@Configuration +@Import({ServiceConfig.class, RepositoryConfig.class}) +public class SystemTestConfig { + + @Bean + public DataSource dataSource() { + // return new DataSource + } +} + +public static void main(String[] args) { + ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class); + // everything wires up across configuration classes... + TransferService transferService = ctx.getBean(TransferService.class); + transferService.transfer(100.00, "A123", "C456"); +} +``` + +Kotlin + +``` +import org.springframework.beans.factory.getBean + +@Configuration +class ServiceConfig { + + @Autowired + lateinit var accountRepository: AccountRepository + + @Bean + fun transferService(): TransferService { + return TransferServiceImpl(accountRepository) + } +} + +@Configuration +class RepositoryConfig(private val dataSource: DataSource) { + + @Bean + fun accountRepository(): AccountRepository { + return JdbcAccountRepository(dataSource) + } +} + +@Configuration +@Import(ServiceConfig::class, RepositoryConfig::class) +class SystemTestConfig { + + @Bean + fun dataSource(): DataSource { + // return new DataSource + } +} + +fun main() { + val ctx = AnnotationConfigApplicationContext(SystemTestConfig::class.java) + // everything wires up across configuration classes... + val transferService = ctx.getBean() + transferService.transfer(100.00, "A123", "C456") +} +``` + +| |在`@Configuration`类中的构造函数注入仅在 Spring
Framework4.3 时支持。还请注意,如果目标
Bean 只定义了一个构造函数,则不需要指定`@Autowired`。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[]()完全合格的导入 bean,便于导航 + +在前面的场景中,使用`@Autowired`可以很好地工作并提供所需的模块化,但是确定 AutoWired Bean 定义的确切声明位置仍然有些模棱两可。例如,作为查看`ServiceConfig`的开发人员,你如何准确地知道`@Autowired AccountRepository` Bean 在哪里声明?它在代码中不是显式的,这可能就很好了。请记住,[Spring Tools for Eclipse](https://spring.io/tools)提供了工具,可以呈现显示所有事物如何连接的图形,这可能就是你所需要的。另外,你的 Java IDE 可以很容易地找到`AccountRepository`类型的所有声明和用法,并快速向你显示返回该类型的`@Bean`方法的位置。 + +如果这种歧义是不可接受的,并且你希望在 IDE 中从一个`@Configuration`类直接导航到另一个类,请考虑自动连接配置类本身。下面的示例展示了如何做到这一点: + +Java + +``` +@Configuration +public class ServiceConfig { + + @Autowired + private RepositoryConfig repositoryConfig; + + @Bean + public TransferService transferService() { + // navigate 'through' the config class to the @Bean method! + return new TransferServiceImpl(repositoryConfig.accountRepository()); + } +} +``` + +Kotlin + +``` +@Configuration +class ServiceConfig { + + @Autowired + private lateinit var repositoryConfig: RepositoryConfig + + @Bean + fun transferService(): TransferService { + // navigate 'through' the config class to the @Bean method! + return TransferServiceImpl(repositoryConfig.accountRepository()) + } +} +``` + +在前面的情况中,其中`AccountRepository`是完全显式定义的。然而,`ServiceConfig`现在与`RepositoryConfig`紧密耦合。这是一种权衡。通过使用基于接口或基于抽象类的`@Configuration`类,可以在一定程度上减轻这种紧密耦合。考虑以下示例: + +Java + +``` +@Configuration +public class ServiceConfig { + + @Autowired + private RepositoryConfig repositoryConfig; + + @Bean + public TransferService transferService() { + return new TransferServiceImpl(repositoryConfig.accountRepository()); + } +} + +@Configuration +public interface RepositoryConfig { + + @Bean + AccountRepository accountRepository(); +} + +@Configuration +public class DefaultRepositoryConfig implements RepositoryConfig { + + @Bean + public AccountRepository accountRepository() { + return new JdbcAccountRepository(...); + } +} + +@Configuration +@Import({ServiceConfig.class, DefaultRepositoryConfig.class}) // import the concrete config! +public class SystemTestConfig { + + @Bean + public DataSource dataSource() { + // return DataSource + } + +} + +public static void main(String[] args) { + ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class); + TransferService transferService = ctx.getBean(TransferService.class); + transferService.transfer(100.00, "A123", "C456"); +} +``` + +Kotlin + +``` +import org.springframework.beans.factory.getBean + +@Configuration +class ServiceConfig { + + @Autowired + private lateinit var repositoryConfig: RepositoryConfig + + @Bean + fun transferService(): TransferService { + return TransferServiceImpl(repositoryConfig.accountRepository()) + } +} + +@Configuration +interface RepositoryConfig { + + @Bean + fun accountRepository(): AccountRepository +} + +@Configuration +class DefaultRepositoryConfig : RepositoryConfig { + + @Bean + fun accountRepository(): AccountRepository { + return JdbcAccountRepository(...) + } +} + +@Configuration +@Import(ServiceConfig::class, DefaultRepositoryConfig::class) // import the concrete config! +class SystemTestConfig { + + @Bean + fun dataSource(): DataSource { + // return DataSource + } + +} + +fun main() { + val ctx = AnnotationConfigApplicationContext(SystemTestConfig::class.java) + val transferService = ctx.getBean() + transferService.transfer(100.00, "A123", "C456") +} +``` + +现在`ServiceConfig`相对于具体的 `defaultRepositoryConfig’是松耦合的,并且内置的 IDE 工具仍然很有用:你可以轻松地获得`RepositoryConfig`实现的类型层次结构。通过这种方式,导航`@Configuration`类及其依赖关系与导航基于接口的代码的通常过程没有什么不同。 + +| |如果你想要影响某些 bean 的启动创建顺序,请考虑
声明其中一些为`@Lazy`(用于在 First Access 上而不是在 Startup 上创建)
或作为`@DependsOn`某些其他 bean(确保特定的其他 bean 是在当前 Bean 之前创建的
,而后者的直接依赖所隐含的意义则更大)。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 有条件地包含`@Configuration`类或`@Bean`方法 + +基于某些任意的系统状态,有条件地启用或禁用完整的`@Configuration`类或甚至单个`@Bean`方法通常是有用的。一个常见的例子是,只有在 Spring `Environment`中启用了特定配置文件时,才使用`@Profile`注释来激活 bean(有关详细信息,请参见[Bean Definition Profiles](#beans-definition-profiles))。 + +`@Profile`注释实际上是通过使用一种更灵活的注释[`@Conditional`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/annotation/Conditional.html)来实现的。`@Conditional`注释表示特定的 `org.springframework.context.annotation.condition` 实现,在注册`@Bean`之前应该参考这些实现。 + +`Condition`接口的实现提供了一个`matches(…​)`方法,该方法返回`true`或`false`。例如,下面的清单显示了用于`@Profile`的实际 ` 条件’实现: + +Java + +``` +@Override +public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + // Read the @Profile annotation attributes + MultiValueMap attrs = metadata.getAllAnnotationAttributes(Profile.class.getName()); + if (attrs != null) { + for (Object value : attrs.get("value")) { + if (context.getEnvironment().acceptsProfiles(((String[]) value))) { + return true; + } + } + return false; + } + return true; +} +``` + +Kotlin + +``` +override fun matches(context: ConditionContext, metadata: AnnotatedTypeMetadata): Boolean { + // Read the @Profile annotation attributes + val attrs = metadata.getAllAnnotationAttributes(Profile::class.java.name) + if (attrs != null) { + for (value in attrs["value"]!!) { + if (context.environment.acceptsProfiles(Profiles.of(*value as Array))) { + return true + } + } + return false + } + return true +} +``` + +有关更多详细信息,请参见[`@Conditional`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/annotation/Conditional.html)Javadoc。 + +##### 结合 Java 和 XML 配置 + +Spring 的`@Configuration`类支持并不是为了 100% 地完全替代 Spring XML。一些工具,例如 Spring XML 名称空间,仍然是配置容器的理想方式。在 XML 方便或必要的情况下,你可以选择:以“以 XML 为中心”的方式实例化容器,例如使用“ClassPathXMLApplicationContext”,或者以“以 Java 为中心”的方式实例化容器,使用“AnnotationConfigApplicationContext”和注释来根据需要导入 XML。 + +###### 以 XML 为中心使用`@Configuration`类 + +最好从 XML 引导 Spring 容器,并以特别的方式包括 `@Configuration’类。例如,在使用 Spring XML 的大型现有代码库中,更容易根据需要创建`@Configuration`类,并从现有的 XML 文件中包含它们。在本节的后面,我们将介绍在这种“以 XML 为中心”的情况下使用`@Configuration`类的选项。 + +[]()将`@Configuration`类声明为普通 Spring ``元素 + +请记住,`@Configuration`类最终是容器中的 Bean 定义。在本系列示例中,我们创建了一个名为`@Configuration`的`AppConfig`类,并将其包含在`system-test-config.xml`中,作为``的定义。因为打开了 ``,容器识别 `@configuration’注释并正确处理`@Bean`中声明的`AppConfig`方法。 + +下面的示例展示了一个普通的 Java 配置类: + +Java + +``` +@Configuration +public class AppConfig { + + @Autowired + private DataSource dataSource; + + @Bean + public AccountRepository accountRepository() { + return new JdbcAccountRepository(dataSource); + } + + @Bean + public TransferService transferService() { + return new TransferService(accountRepository()); + } +} +``` + +Kotlin + +``` +@Configuration +class AppConfig { + + @Autowired + private lateinit var dataSource: DataSource + + @Bean + fun accountRepository(): AccountRepository { + return JdbcAccountRepository(dataSource) + } + + @Bean + fun transferService() = TransferService(accountRepository()) +} +``` + +下面的示例显示了`system-test-config.xml`文件示例的一部分: + +``` + + + + + + + + + + + + + +``` + +下面的示例显示了一个可能的`jdbc.properties`文件: + +``` +jdbc.url=jdbc:hsqldb:hsql://localhost/xdb +jdbc.username=sa +jdbc.password= +``` + +Java + +``` +public static void main(String[] args) { + ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml"); + TransferService transferService = ctx.getBean(TransferService.class); + // ... +} +``` + +Kotlin + +``` +fun main() { + val ctx = ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml") + val transferService = ctx.getBean() + // ... +} +``` + +| |在`system-test-config.xml`文件中,`AppConfig```不声明`id`元素。虽然这样做是可以接受的,但这是不必要的,因为没有其他 Bean
引用过它,并且不太可能通过名称从容器中显式地获取它,类似地,
Bean 也只能通过类型自动连线,所以显式 Bean `id`并不是严格要求的。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[]()使用 \获取`@Configuration`类 + +因为`@Configuration`是用`@Component`进行元注释的,所以`@Configuration`-注释的类自动成为组件扫描的候选类。使用与前面示例中描述的相同的场景,我们可以重新定义`system-test-config.xml`以利用组件扫描。注意,在这种情况下,我们不需要显式声明 ``,因为``启用了相同的功能。 + +下面的示例显示了修改后的`system-test-config.xml`文件: + +``` + + + + + + + + + + + +``` + +###### `@Configuration`以类为中心使用`@ImportResource`###### + +在以`@Configuration`类为配置容器的主要机制的应用程序中,仍然可能需要至少使用一些 XML。在这些场景中,你可以使用`@ImportResource`并只定义所需的 XML。这样就实现了一种“以 Java 为中心”的方法来配置容器,并将 XML 保持在最低限度。下面的示例(包括一个配置类、一个定义 Bean 的 XML 文件、一个属性文件和`main`类)展示了如何使用`@ImportResource`注释来实现根据需要使用 XML 的“以 Java 为中心”的配置: + +Java + +``` +@Configuration +@ImportResource("classpath:/com/acme/properties-config.xml") +public class AppConfig { + + @Value("${jdbc.url}") + private String url; + + @Value("${jdbc.username}") + private String username; + + @Value("${jdbc.password}") + private String password; + + @Bean + public DataSource dataSource() { + return new DriverManagerDataSource(url, username, password); + } +} +``` + +Kotlin + +``` +@Configuration +@ImportResource("classpath:/com/acme/properties-config.xml") +class AppConfig { + + @Value("\${jdbc.url}") + private lateinit var url: String + + @Value("\${jdbc.username}") + private lateinit var username: String + + @Value("\${jdbc.password}") + private lateinit var password: String + + @Bean + fun dataSource(): DataSource { + return DriverManagerDataSource(url, username, password) + } +} +``` + +``` +properties-config.xml + + + +``` + +``` +jdbc.properties +jdbc.url=jdbc:hsqldb:hsql://localhost/xdb +jdbc.username=sa +jdbc.password= +``` + +Java + +``` +public static void main(String[] args) { + ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class); + TransferService transferService = ctx.getBean(TransferService.class); + // ... +} +``` + +Kotlin + +``` +import org.springframework.beans.factory.getBean + +fun main() { + val ctx = AnnotationConfigApplicationContext(AppConfig::class.java) + val transferService = ctx.getBean() + // ... +} +``` + +### 1.13.环境抽象 + +[`Environment`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/core/env/Environment.html)接口是集成在容器中的一个抽象,它为应用程序环境的两个关键方面建模:[profiles](#beans-definition-profiles)和[properties](#beans-property-source-abstraction)。 + +Bean 配置文件是由 Bean 个定义组成的一个命名的逻辑组,只有在给定的配置文件处于活动状态时才向容器注册。可以将 bean 分配给一个配置文件,无论是用 XML 定义的还是用注释定义的。相对于配置文件,`Environment`对象的作用是确定哪些配置文件(如果有的话)当前处于活动状态,以及默认情况下哪些配置文件(如果有的话)应该处于活动状态。 + +属性在几乎所有的应用程序中都扮演着重要的角色,并且可能来自各种来源:属性文件、JVM 系统属性、系统环境变量、JNDI、 Servlet 上下文参数、ad-hoc`Properties`对象、`Map`对象,等等。与属性相关的`Environment`对象的作用是为用户提供一个方便的服务接口,用于配置属性源并从它们解析属性。 + +#### 1.13.1. Bean 定义配置文件 + +Bean 定义配置文件在核心容器中提供了一种机制,该机制允许在不同的环境中注册不同的 bean。“环境”这个词对不同的用户来说可能意味着不同的东西,这个功能可以帮助解决许多用例,包括: + +* 在开发中使用内存中的数据源,而不是在 QA 或生产中从 JNDI 中查找相同的数据源。 + +* 仅在将应用程序部署到性能环境时注册监视基础结构。 + +* 为客户 A 和客户 B 的部署注册定制的 bean 实现。 + +考虑实际应用程序中需要“数据源”的第一个用例。在测试环境中,配置可能类似于以下内容: + +Java + +``` +@Bean +public DataSource dataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.HSQL) + .addScript("my-schema.sql") + .addScript("my-test-data.sql") + .build(); +} +``` + +Kotlin + +``` +@Bean +fun dataSource(): DataSource { + return EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.HSQL) + .addScript("my-schema.sql") + .addScript("my-test-data.sql") + .build() +} +``` + +现在考虑如何将此应用程序部署到 QA 或生产环境中,假设应用程序的数据源已注册到生产应用程序服务器的 JNDI 目录中。我们的`dataSource` Bean 现在看起来像以下清单: + +Java + +``` +@Bean(destroyMethod="") +public DataSource dataSource() throws Exception { + Context ctx = new InitialContext(); + return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource"); +} +``` + +Kotlin + +``` +@Bean(destroyMethod = "") +fun dataSource(): DataSource { + val ctx = InitialContext() + return ctx.lookup("java:comp/env/jdbc/datasource") as DataSource +} +``` + +问题是如何根据当前环境在使用这两种变体之间进行切换。 Spring 随着时间的推移,用户已经设计了许多方法来完成这一工作,通常依赖于系统环境变量和 XML语句的组合,这些语句包含令牌,这些令牌根据环境变量的值解析为正确的配置文件路径。 Bean 定义简档是容器的核心特征,其提供了解决该问题的方法。 + +如果我们推广前面的例子中所示的特定于环境的定义 Bean 的用例,那么我们最终需要在某些上下文中注册某些 Bean 定义,而不是在其他上下文中注册。可以说,你希望在情况 A 中注册 Bean 定义的特定配置文件,而在情况 B 中注册不同的配置文件。我们从更新配置开始,以反映这种需求。 + +##### 使用`@Profile` + +[`@Profile`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/annotation/Profile.html)注释使你可以指示,当一个或多个指定的配置文件处于活动状态时,组件有资格进行注册。使用前面的示例,我们可以按以下方式重写`dataSource`配置: + +Java + +``` +@Configuration +@Profile("development") +public class StandaloneDataConfig { + + @Bean + public DataSource dataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.HSQL) + .addScript("classpath:com/bank/config/sql/schema.sql") + .addScript("classpath:com/bank/config/sql/test-data.sql") + .build(); + } +} +``` + +Kotlin + +``` +@Configuration +@Profile("development") +class StandaloneDataConfig { + + @Bean + fun dataSource(): DataSource { + return EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.HSQL) + .addScript("classpath:com/bank/config/sql/schema.sql") + .addScript("classpath:com/bank/config/sql/test-data.sql") + .build() + } +} +``` + +Java + +``` +@Configuration +@Profile("production") +public class JndiDataConfig { + + @Bean(destroyMethod="") + public DataSource dataSource() throws Exception { + Context ctx = new InitialContext(); + return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource"); + } +} +``` + +Kotlin + +``` +@Configuration +@Profile("production") +class JndiDataConfig { + + @Bean(destroyMethod = "") + fun dataSource(): DataSource { + val ctx = InitialContext() + return ctx.lookup("java:comp/env/jdbc/datasource") as DataSource + } +} +``` + +| |如前所述,对于`@Bean`方法,你通常会选择使用程序化的
JNDI 查找,方法是使用 Spring 的`JndiTemplate`/`jndilocatordelegate`helpers 或
直接的 JNDI`InitialContext`用法,但不使用`JndiObjectFactoryBean`变体,这将迫使你将返回类型声明为`FactoryBean`类型。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +配置文件字符串可以包含一个简单的配置文件名(例如,`production`)或一个配置文件表达式。配置文件表达式允许表达更复杂的配置文件逻辑(例如,`production & us-east`)。配置文件表达式中支持以下操作符: + +* `!`:配置文件的逻辑“not” + +* `&`:配置文件的逻辑“和” + +* `|`:配置文件的逻辑“或” + +| |You cannot mix the `&` and `|` operators without using parentheses. For example,`production & us-east |“eu-central”不是一个有效的表达方式。它必须表示为“Production&(美国东部)”| eu-central)`.| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +你可以使用`@Profile`作为[meta-annotation](#beans-meta-annotations)来创建自定义组合注释。下面的示例定义了一个自定义的“@production”注释,你可以将其用作“@profile(“production”)”的插入替换: + +Java + +``` +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Profile("production") +public @interface Production { +} +``` + +Kotlin + +``` +@Target(AnnotationTarget.TYPE) +@Retention(AnnotationRetention.RUNTIME) +@Profile("production") +annotation class Production +``` + +| |如果`@Configuration`类被标记为`@Profile`,则所有`@Bean`方法和与该类关联的 `@import’注释都将被忽略,除非
中的一个或多个指定的配置文件处于活动状态。如果一个`@Component`或`@Configuration`类被标记为
并带有`@Profile({"p1", "p2"})`,则除非
配置文件“p1”或“p2”已被激活,否则该类不会被注册或处理。如果给定概要文件的前缀是
not 运算符,则只有在概要文件不是
活动的情况下,才会注册带注释的元素。例如,给定`@Profile({"p1", "!p2"})`,如果配置文件
’P1’是活动的,或者如果配置文件’P2’不是活动的,就会发生注册。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +`@Profile`还可以在方法级别上声明仅包括一个特定 Bean 配置类的一个特定 Bean(例如,对于特定 Bean 的可选变体),如以下示例所示: + +Java + +``` +@Configuration +public class AppConfig { + + @Bean("dataSource") + @Profile("development") (1) + public DataSource standaloneDataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.HSQL) + .addScript("classpath:com/bank/config/sql/schema.sql") + .addScript("classpath:com/bank/config/sql/test-data.sql") + .build(); + } + + @Bean("dataSource") + @Profile("production") (2) + public DataSource jndiDataSource() throws Exception { + Context ctx = new InitialContext(); + return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource"); + } +} +``` + +|**1**|`standaloneDataSource`方法仅在`development`配置文件中可用。| +|-----|---------------------------------------------------------------------------------| +|**2**|`jndiDataSource`方法仅在`production`配置文件中可用。| + +Kotlin + +``` +@Configuration +class AppConfig { + + @Bean("dataSource") + @Profile("development") (1) + fun standaloneDataSource(): DataSource { + return EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.HSQL) + .addScript("classpath:com/bank/config/sql/schema.sql") + .addScript("classpath:com/bank/config/sql/test-data.sql") + .build() + } + + @Bean("dataSource") + @Profile("production") (2) + fun jndiDataSource() = + InitialContext().lookup("java:comp/env/jdbc/datasource") as DataSource +} +``` + +|**1**|`standaloneDataSource`方法仅在`development`配置文件中可用。| +|-----|---------------------------------------------------------------------------------| +|**2**|`jndiDataSource`方法仅在`production`配置文件中可用。| + +| |对于`@Profile`on`@Bean`方法,可能会应用一个特殊的场景:在
重载`@Bean`相同 Java 方法名的方法的情况下(类似于构造函数
重载),需要在所有`@Profile`重载方法上一致地声明
条件。如果条件不一致,在重载的方法中,只有
第一个声明上的条件才重要。因此,`@Profile`可以不使用

上选择具有特定参数签名的重载方法。相同 Bean 的所有工厂方法之间的解析遵循 Spring 的
构造函数在创建时的解析算法。

如果你想定义具有不同配置文件条件的替代 bean,
使用不同的 Java 方法名称,通过使用`@Bean`名称
属性指向相同的 Bean 名称,如前面的例子所示。如果所有的参数签名都是
相同的(例如,所有的变量都有 no-arg 工厂方法),这是唯一的
在有效的 Java 类中表示这样的安排的方式,在第一个位置
(因为只能有一个方法的特定名称和参数签名)。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### XML Bean 定义配置文件 + +XML 对应的是`profile`元素的``属性。我们前面的示例配置可以在两个 XML 文件中重写,如下所示: + +``` + + + + + + + +``` + +``` + + + + +``` + +也可以避免在同一个文件中分割和嵌套``元素,如下例所示: + +``` + + + + + + + + + + + + + + + +``` + +`spring-bean.xsd`已被限制为仅允许将此类元素作为文件中的最后一个元素。这应该有助于提供灵活性,而不会在 XML 文件中造成混乱。 + +| |XML 对应方不支持前面描述的配置文件表达式。但是,
可以使用`!`操作符来否定配置文件。通过嵌套配置文件,也可以应用逻辑
“and”,如下例所示:

``
xmlns.w3.org/2001/xmlschema-instance“<xmlns:jdbc=”http:///WWW.springframework.jdbc/jdbc=“schemframework=”<99"/schemframework=">应用程序。[“标准服务环境”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/context/support/StandardServletEnvironment.html)填充了额外的默认属性源,包括 Servlet config 和 Servlet
上下文参数。它可以选择性地启用[jndipropertysource’](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jndi/JndiPropertySource.html)。
有关详细信息,请参见 Javadoc。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +具体地说,当使用`StandardEnvironment`时,如果在运行时存在`my-property`系统属性或`my-property`环境变量,则对`env.containsProperty("my-property")`的调用将返回 true。 + +| |执行的搜索是分层次的。默认情况下,系统属性优先于
环境变量。因此,如果`my-property`属性在
调用`env.getProperty("my-property")`期间恰好在这两个地方都设置了,则系统属性值“wins”并返回。,
注意,属性值不是合并
,而是完全被前面的一个条目覆盖,

对于一个常见的`StandardServletEnvironment`,完整的层次结构如下,
优先级最高的条目位于顶部:

1。ServletConfig 参数(如果适用——例如,在`DispatcherServlet`上下文的情况下)

2。ServletContext 参数

3。ENV 变量

4。JVM 系统属性(`-d` 命令行参数)

5。JVM 系统环境(操作系统环境变量)| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +最重要的是,整个机制是可配置的。也许你有一个想要集成到此搜索中的自定义属性源。为此,实现并实例化你自己的`PropertySource`,并将其添加到当前`PropertySources`的`PropertySources`集合中。下面的示例展示了如何做到这一点: + +Java + +``` +ConfigurableApplicationContext ctx = new GenericApplicationContext(); +MutablePropertySources sources = ctx.getEnvironment().getPropertySources(); +sources.addFirst(new MyPropertySource()); +``` + +Kotlin + +``` +val ctx = GenericApplicationContext() +val sources = ctx.environment.propertySources +sources.addFirst(MyPropertySource()) +``` + +在前面的代码中,`MyPropertySource`已被添加到搜索中具有最高优先级的位置。如果它包含一个`my-property`属性,则检测并返回该属性,这有利于在任何其他`my-property`中的任何`PropertySource`属性。[“可变 PropertySources”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/core/env/MutablePropertySources.html)API 公开了许多方法,这些方法允许对属性源集合进行精确操作。 + +#### 1.13.3.使用`@PropertySource` + +[@PropertySource](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/annotation/PropertySource.html)注释提供了一种方便的声明性机制,用于将`PropertySource`添加到 Spring 的`Environment`中。 + +给定一个名为`app.properties`的文件,该文件包含键-值对`testbean.name=myTestBean`,下面的`@Configuration`类使用`@PropertySource`的方式使得对`testBean.getName()`的调用返回`myTestBean`: + +Java + +``` +@Configuration +@PropertySource("classpath:/com/myco/app.properties") +public class AppConfig { + + @Autowired + Environment env; + + @Bean + public TestBean testBean() { + TestBean testBean = new TestBean(); + testBean.setName(env.getProperty("testbean.name")); + return testBean; + } +} +``` + +Kotlin + +``` +@Configuration +@PropertySource("classpath:/com/myco/app.properties") +class AppConfig { + + @Autowired + private lateinit var env: Environment + + @Bean + fun testBean() = TestBean().apply { + name = env.getProperty("testbean.name")!! + } +} +``` + +在`@PropertySource`资源位置中存在的任何`${…​}`占位符都将根据已经针对该环境注册的一组属性源进行解析,如下例所示: + +Java + +``` +@Configuration +@PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties") +public class AppConfig { + + @Autowired + Environment env; + + @Bean + public TestBean testBean() { + TestBean testBean = new TestBean(); + testBean.setName(env.getProperty("testbean.name")); + return testBean; + } +} +``` + +Kotlin + +``` +@Configuration +@PropertySource("classpath:/com/\${my.placeholder:default/path}/app.properties") +class AppConfig { + + @Autowired + private lateinit var env: Environment + + @Bean + fun testBean() = TestBean().apply { + name = env.getProperty("testbean.name")!! + } +} +``` + +假设`my.placeholder`存在于已经注册的一个属性源中(例如,系统属性或环境变量),则将占位符解析为相应的值。如果不是,则将`default/path`用作默认值。如果没有指定默认值,并且无法解析某个属性,则抛出“非法 argumenTexception”。 + +| |根据 Java8 约定,`@PropertySource`注释是可重复的。
然而,所有此类`@PropertySource`注释都需要在相同的
级别上声明,可以直接在配置类上声明,也可以在
相同的自定义注释中声明元注释。混合直接注释和元注释不是
推荐的,因为直接注释有效地覆盖了元注释。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.13.4.语句中的占位符解析 + +从历史上看,元素中占位符的值只能根据 JVM 系统属性或环境变量来解析。现在已经不是这样了。因为`Environment`抽象集成在整个容器中,所以很容易通过它来路由占位符的解析。这意味着你可以以你喜欢的任何方式配置解析过程。你可以更改通过系统属性和环境变量进行搜索的优先级,或者完全删除它们。你还可以将自己的属性源添加到该组合中,视情况而定。 + +具体地说,无论`customer`属性在哪里定义,只要它在`Environment`中可用,以下语句都可以工作: + +``` + + + +``` + +### 1.14.注册`LoadTimeWeaver` + +Spring 使用`LoadTimeWeaver`在类被加载到 Java 虚拟机时对其进行动态转换。 + +要启用加载时编织,可以将`@EnableLoadTimeWeaving`添加到一个 `@Configuration’类中,如下例所示: + +Java + +``` +@Configuration +@EnableLoadTimeWeaving +public class AppConfig { +} +``` + +Kotlin + +``` +@Configuration +@EnableLoadTimeWeaving +class AppConfig +``` + +或者,对于 XML 配置,你可以使用`context:load-time-weaver`元素: + +``` + + + +``` + +一旦为`ApplicationContext`配置,在该`ApplicationContext`内的任何 Bean 都可以实现`LoadTimeWeaverAware`,从而接收到对加载时 Weaver 实例的引用。这在与[Spring’s JPA support](data-access.html#orm-jpa)的结合中特别有用,其中 JPA 类转换可能需要进行加载时的编织。有关更多详细信息,请咨询[“本地包含 rentitymanagerfactorybean”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.html)Javadoc。有关 AspectJ 加载时编织的更多信息,请参见[Load-time Weaving with AspectJ in the Spring Framework](#aop-aj-ltw)。 + +### 1.15.`ApplicationContext`的附加功能 + +正如[章节介绍](#beans)中所讨论的,`org.springframework.beans.factory`包提供了用于管理和操作 bean 的基本功能,包括以编程方式。`org.springframework.context`包添加了[` 应用上下文’](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/ApplicationContext.html)接口,该接口扩展了`BeanFactory`接口,此外还扩展了其他接口,以便以更面向应用程序框架的风格提供附加功能。许多人使用`ApplicationContext`完全是声明式的,甚至不是以编程方式创建它,而是依靠`ContextLoader`之类的支持类来自动实例化“ApplicationContext”,作为 Java EE Web 应用程序正常启动过程的一部分。 + +为了以更面向框架的风格增强`BeanFactory`功能,上下文包还提供了以下功能: + +* 通过`MessageSource`接口访问 i18n 样式的消息。 + +* 通过`ResourceLoader`接口访问资源,例如 URL 和文件。 + +* 事件发布,即通过使用`ApplicationEventPublisher`接口来实现`ApplicationListener`接口的 bean。 + +* 通过“HierarChicalBeanFactory”接口,加载多个(分层的)上下文,使每个上下文都集中在一个特定的层上,例如应用程序的 Web 层。 + +#### 1.15.1.使用`MessageSource`的国际化 + +`ApplicationContext`接口扩展了一个名为`MessageSource`的接口,因此提供了国际化(“i18n”)功能。 Spring 还提供了 `HierarChicalMessageSource’接口,该接口可以按层次解析消息。这些接口一起提供了 Spring 影响消息解析的基础。在这些接口上定义的方法包括: + +* `String getMessage(String code, Object[] args, String default, Locale loc)`:用于从`MessageSource`中检索消息的基本方法。当未找到指定区域设置的消息时,将使用默认消息。使用标准库提供的`MessageFormat`功能,传入的任何参数都将成为替换值。 + +* `String getMessage(String code, Object[] args, Locale loc)`:与以前的方法基本相同,但有一个区别:不能指定默认消息。如果找不到消息,则抛出`NoSuchMessageException`。 + +* `String getMessage(MessageSourceResolvable resolvable, Locale locale)`:前面的方法中使用的所有属性也包装在一个名为“MessageSourceResolvable”的类中,你可以在这个方法中使用它。 + +当加载`ApplicationContext`时,它会自动搜索上下文中定义的`MessageSource` Bean。 Bean 必须具有`messageSource`的名称。如果找到了这样的 Bean,那么对前面的方法的所有调用都被委托给消息源。如果找不到消息源,`ApplicationContext`将尝试查找包含 Bean 同名的父消息。如果是,则使用 Bean 作为`MessageSource`。如果“ApplicationContext”找不到任何消息源,则实例化一个空的“delegatingmessagesource”,以便能够接受对上述定义的方法的调用。 + +Spring 提供了三个`MessageSource`实现方式,`ResourceBundleMessageSource`、`ReloadableResourceBundleMessageSource`和`StaticMessageSource`。它们都实现`HierarchicalMessageSource`以执行嵌套消息传递。`StaticMessageSource`很少使用,但提供了向源添加消息的编程方法。下面的示例显示`ResourceBundleMessageSource`: + +``` + + + + + format + exceptions + windows + + + + +``` + +该示例假定你在 Classpath 中定义了三个资源包,分别称为`format`、`exceptions`和`windows`。通过`ResourceBundle`对象以 JDK 标准的方式处理任何解析消息的请求。为了示例的目的,假设上述两个资源包文件的内容如下: + +``` + # in format.properties + message=Alligators rock! +``` + +``` + # in exceptions.properties + argument.required=The {0} argument is required. +``` + +下一个示例显示了运行`MessageSource`功能的程序。请记住,所有`ApplicationContext`实现也是`MessageSource`实现,因此可以强制转换到`MessageSource`接口。 + +Java + +``` +public static void main(String[] args) { + MessageSource resources = new ClassPathXmlApplicationContext("beans.xml"); + String message = resources.getMessage("message", null, "Default", Locale.ENGLISH); + System.out.println(message); +} +``` + +Kotlin + +``` +fun main() { + val resources = ClassPathXmlApplicationContext("beans.xml") + val message = resources.getMessage("message", null, "Default", Locale.ENGLISH) + println(message) +} +``` + +上述程序的输出结果如下: + +``` +Alligators rock! +``` + +总而言之,`MessageSource`是在一个名为`beans.xml`的文件中定义的,该文件存在于 Classpath 的根。`messageSource` Bean 定义通过其`basenames`属性引用了许多资源包。在列表中传递给`basenames`属性的三个文件作为文件存在于 Classpath 的根目录中,分别称为`format.properties`、`exceptions.properties`和 `windows.properties’。 + +下一个示例显示传递给消息查找的参数。这些参数被转换为`String`对象,并插入到查找消息中的占位符中。 + +``` + + + + + + + + + + + + + +``` + +爪哇 + +``` +public class Example { + + private MessageSource messages; + + public void setMessages(MessageSource messages) { + this.messages = messages; + } + + public void execute() { + String message = this.messages.getMessage("argument.required", + new Object [] {"userDao"}, "Required", Locale.ENGLISH); + System.out.println(message); + } +} +``` + +Kotlin + +``` + class Example { + + lateinit var messages: MessageSource + + fun execute() { + val message = messages.getMessage("argument.required", + arrayOf("userDao"), "Required", Locale.ENGLISH) + println(message) + } +} +``` + +调用`execute()`方法的结果如下: + +``` +The userDao argument is required. +``` + +关于国际化(“i18n”), Spring 的各种`MessageSource`实现遵循与标准 JDK`ResourceBundle’相同的语言环境解析和后备规则。简而言之,继续前面定义的示例`messageSource`,如果你想要针对英国语言环境解析消息,那么可以分别创建名为`format_en_GB.properties`、`exceptions_en_GB.properties`和 `windows_en_gb.properties’的文件。 + +通常,区域设置解析由应用程序的周围环境管理。在下面的示例中,将手动指定用于解析(英式)消息的区域设置: + +``` +# in exceptions_en_GB.properties +argument.required=Ebagum lad, the ''{0}'' argument is required, I say, required. +``` + +爪哇 + +``` +public static void main(final String[] args) { + MessageSource resources = new ClassPathXmlApplicationContext("beans.xml"); + String message = resources.getMessage("argument.required", + new Object [] {"userDao"}, "Required", Locale.UK); + System.out.println(message); +} +``` + +Kotlin + +``` +fun main() { + val resources = ClassPathXmlApplicationContext("beans.xml") + val message = resources.getMessage("argument.required", + arrayOf("userDao"), "Required", Locale.UK) + println(message) +} +``` + +运行上述程序的结果如下: + +``` +Ebagum lad, the 'userDao' argument is required, I say, required. +``` + +你还可以使用`MessageSourceAware`接口获取对已定义的任何“MessageSource”的引用。在实现`MessageSourceAware`接口的 `ApplicationContext’中定义的任何 Bean 都会在创建和配置 Bean 时注入应用程序上下文的`MessageSource`。 + +| |因为 Spring 的`MessageSource`是基于 爪哇 的`ResourceBundle`,所以它不合并具有相同基名的
bundle,而只使用找到的第一个 bundle。
具有相同基名的后续消息 bundle 将被忽略。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |作为`ResourceBundleMessageSource`的替代方案, Spring 提供了一个“ReloadableResourceBundleMessageSource”类。这种变体支持相同的 bundle
文件格式,但比基于标准 JDK 的“ResourceBundleMessageSource”实现更灵活。特别地,它允许从任何 Spring 资源位置读取
文件(不仅是从 Classpath),并且支持 hot
重新加载 bundle 属性文件(同时在它们之间有效地缓存它们)。
有关详细信息,请参见[“重新提供资源,加强信息来源”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/support/ReloadableResourceBundleMessageSource.html)爪哇doc。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.15.2.标准和自定义事件 + +`ApplicationContext`中的事件处理是通过`ApplicationEvent`类和`ApplicationListener`接口提供的。如果实现了“ApplicationListener”接口的 Bean 被部署到上下文中,那么每当一个“ApplicationEvent”被发布到`ApplicationContext`时, Bean 就会被通知。本质上,这是标准的观察者设计模式。 + +| |截至 Spring 4.2,事件基础结构已经得到了显著的改进,并且提供了
和[基于注释的模型](#context-functionality-events-annotation)以及
发布任意事件的能力(即,不一定从
扩展的对象`ApplicationEvent`)。当这样的对象被发布时,我们将它包装为
事件。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +下表描述了 Spring 提供的标准事件: + +| Event |解释| +|----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `ContextRefreshedEvent` |当`ApplicationContext`被初始化或刷新时发布(例如,通过
在`ConfigurableApplicationContext`接口上使用`refresh()`方法)。
在这里,“初始化”意味着加载所有 bean,检测到后处理器 bean
并激活,预先实例化单例,`ApplicationContext`对象为<3300“gt r=”准备好使用。只要上下文尚未关闭,就可以多次触发
刷新,前提是所选的`ApplicationContext`实际上支持这样的
“热”刷新。例如,`XmlWebApplicationContext`支持热刷新,但 `GenericApplicationContext’不支持。| +| `ContextStartedEvent` |当`ApplicationContext`通过使用 `ConfigurableApplicationContext’接口上的方法启动时发布。在这里,“started”表示所有`Lifecycle`bean 都接收到一个显式的开始信号。通常,此信号用于在显式停止后重新启动 bean
,但也可用于启动未被
配置为自动启动的组件(例如,在
初始化上尚未启动的组件)。| +| `ContextStoppedEvent` |在“ConfigurableApplicationContext”接口上使用方法停止时发布。在这里,“stopped”表示所有`Lifecycle`bean 都接收到一个明确的停止信号。可以通过“start()”调用重新启动已停止的上下文。| +| `ContextClosedEvent` |当`ApplicationContext`正在通过`close()`接口上的
方法
关闭`ApplicationContext`时或通过 JVM 关机钩子关闭时发布。在这里,
“closed”表示所有单例 bean 都将被销毁。一旦上下文被关闭,
它就到达了生命周期的终点,不能刷新或重新启动。| +| `RequestHandledEvent` |一个特定于 Web 的事件,告诉所有 bean 一个 HTTP 请求已被服务。此
事件将在请求完成后发布。此事件仅适用于使用 Spring 的
的`DispatcherServlet`的 Web 应用程序。| +|`ServletRequestHandledEvent`|添加 Servlet 特定上下文信息的`RequestHandledEvent`子类。| + +你还可以创建和发布自己的自定义事件。下面的示例展示了一个扩展 Spring 的`ApplicationEvent`基类的简单类: + +爪哇 + +``` +public class BlockedListEvent extends ApplicationEvent { + + private final String address; + private final String content; + + public BlockedListEvent(Object source, String address, String content) { + super(source); + this.address = address; + this.content = content; + } + + // accessor and other methods... +} +``` + +Kotlin + +``` +class BlockedListEvent(source: Any, + val address: String, + val content: String) : ApplicationEvent(source) +``` + +要发布自定义`ApplicationEvent`,请在 `ApplicationEventPublisher’上调用`publishEvent()`方法。通常,这是通过创建一个实现“ApplicationEventPublisherAware”的类并将其注册为 Spring Bean 来完成的。下面的示例展示了这样一个类: + +爪哇 + +``` +public class EmailService implements ApplicationEventPublisherAware { + + private List blockedList; + private ApplicationEventPublisher publisher; + + public void setBlockedList(List blockedList) { + this.blockedList = blockedList; + } + + public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { + this.publisher = publisher; + } + + public void sendEmail(String address, String content) { + if (blockedList.contains(address)) { + publisher.publishEvent(new BlockedListEvent(this, address, content)); + return; + } + // send email... + } +} +``` + +Kotlin + +``` +class EmailService : ApplicationEventPublisherAware { + + private lateinit var blockedList: List + private lateinit var publisher: ApplicationEventPublisher + + fun setBlockedList(blockedList: List) { + this.blockedList = blockedList + } + + override fun setApplicationEventPublisher(publisher: ApplicationEventPublisher) { + this.publisher = publisher + } + + fun sendEmail(address: String, content: String) { + if (blockedList!!.contains(address)) { + publisher!!.publishEvent(BlockedListEvent(this, address, content)) + return + } + // send email... + } +} +``` + +在配置时, Spring 容器检测到`EmailService`实现了 `ApplicationEventPublisherAware’,并自动调用 `setApplicationEventPublisher()’。实际上,传入的参数是 Spring 容器本身。你正在通过其“ApplicationEventPublisher”接口与应用程序上下文交互。 + +要接收自定义`ApplicationEvent`,你可以创建一个实现 `ApplicationListener’的类,并将其注册为 Spring Bean。下面的示例展示了这样一个类: + +爪哇 + +``` +public class BlockedListNotifier implements ApplicationListener { + + private String notificationAddress; + + public void setNotificationAddress(String notificationAddress) { + this.notificationAddress = notificationAddress; + } + + public void onApplicationEvent(BlockedListEvent event) { + // notify appropriate parties via notificationAddress... + } +} +``` + +Kotlin + +``` +class BlockedListNotifier : ApplicationListener { + + lateinit var notificationAddres: String + + override fun onApplicationEvent(event: BlockedListEvent) { + // notify appropriate parties via notificationAddress... + } +} +``` + +请注意,`ApplicationListener`通常是用自定义事件的类型参数化的(在前面的示例中是 `blockedlistevent’)。这意味着“onApplicationEvent()”方法可以保持类型安全,从而避免了向下转换的任何需要。你可以注册任意多的事件侦听器,但请注意,默认情况下,事件侦听器会同步接收事件。这意味着`publishEvent()`方法将阻塞,直到所有侦听器都完成了对事件的处理。这种同步和单线程方法的一个优点是,当侦听器接收到一个事件时,如果事务上下文可用,它将在发布者的事务上下文中进行操作。如果需要另一种事件发布策略,请参阅 爪哇doc 获取 Spring 的[“应用 EventMulticaster”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/event/ApplicationEventMulticaster.html)接口和[SimpleApplication EventMulticaster’](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/event/SimpleApplicationEventMulticaster.html)实现的配置选项。 + +下面的示例显示了用于注册和配置上述每个类的 Bean 定义: + +``` + + + + [email protected] + [email protected] + [email protected] + + + + + + + +``` + +将所有这些放在一起,当调用`emailService` Bean 的`sendEmail()`方法时,如果有任何应该被阻止的电子邮件消息,则会发布类型为 `blockedlistevent’的自定义事件。将`blockedListNotifier` Bean 注册为 ` 应用监听器’,并接收`BlockedListEvent`,此时它可以通知适当的当事人。 + +| |Spring 的事件机制设计用于在相同的应用程序上下文中 Spring bean
之间进行简单的通信。然而,对于更复杂的 Enterprise
集成需求,单独维护的[Spring Integration](https://projects.spring.io/spring-integration/)项目提供了
构建轻量级、[以模式为导向](https://www.enterpriseintegrationpatterns.com)事件驱动的
架构的完整支持,这些架构建立在著名的 Spring 编程模型上。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 基于注释的事件侦听器 + +你可以使用“@EventListener”注释在托管 Bean 的任何方法上注册事件侦听器。`BlockedListNotifier`可以重写如下: + +爪哇 + +``` +public class BlockedListNotifier { + + private String notificationAddress; + + public void setNotificationAddress(String notificationAddress) { + this.notificationAddress = notificationAddress; + } + + @EventListener + public void processBlockedListEvent(BlockedListEvent event) { + // notify appropriate parties via notificationAddress... + } +} +``` + +Kotlin + +``` +class BlockedListNotifier { + + lateinit var notificationAddress: String + + @EventListener + fun processBlockedListEvent(event: BlockedListEvent) { + // notify appropriate parties via notificationAddress... + } +} +``` + +方法签名再次声明它监听的事件类型,但是,这一次,使用灵活的名称,并且不实现特定的监听器接口。只要实际的事件类型在其实现层次结构中解析你的泛型参数,就可以通过泛型来缩小事件类型的范围。 + +如果你的方法应该监听多个事件,或者如果你想在完全不使用参数的情况下定义它,那么也可以在注释本身上指定事件类型。下面的示例展示了如何做到这一点: + +爪哇 + +``` +@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class}) +public void handleContextStart() { + // ... +} +``` + +Kotlin + +``` +@EventListener(ContextStartedEvent::class, ContextRefreshedEvent::class) +fun handleContextStart() { + // ... +} +``` + +还可以通过使用注释的`condition`属性添加额外的运行时过滤,该注释定义了[“spel”表达式](#expressions),该属性应该与实际调用特定事件的方法匹配。 + +下面的示例展示了只有当事件的 `Content’属性等于`my-event`时,我们的通知程序才能被重写以被调用: + +爪哇 + +``` +@EventListener(condition = "#blEvent.content == 'my-event'") +public void processBlockedListEvent(BlockedListEvent blEvent) { + // notify appropriate parties via notificationAddress... +} +``` + +Kotlin + +``` +@EventListener(condition = "#blEvent.content == 'my-event'") +fun processBlockedListEvent(blEvent: BlockedListEvent) { + // notify appropriate parties via notificationAddress... +} +``` + +每个`SpEL`表达式都针对一个专用上下文进行计算。下表列出了对上下文可用的项,以便你可以将它们用于条件事件处理: + +| Name | Location |说明| Example | +|---------------|------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------| +| Event | root object |实际`ApplicationEvent`。| `#root.event` or `event` | +|Arguments array| root object |用于调用方法的参数(作为对象数组)。| `#root.args` or `args`; `args[0]` to access the first argument, etc. | +|*Argument name*|evaluation context|任何方法参数的名称。如果由于某种原因,名称不可用
(例如,因为编译的字节代码中没有调试信息),则单独的
参数也可以使用`#a<#arg>`语法,其中`<#arg>`表示
参数索引(从 0 开始)。|`#blEvent` or `#a0` (you can also use `#p0` or `#p<#arg>` parameter notation as an alias)| + +请注意,`#root.event`允许你访问底层事件,即使你的方法签名实际上引用了已发布的任意对象。 + +如果需要发布一个事件作为处理另一个事件的结果,则可以更改方法签名以返回应该发布的事件,如下例所示: + +爪哇 + +``` +@EventListener +public ListUpdateEvent handleBlockedListEvent(BlockedListEvent event) { + // notify appropriate parties via notificationAddress and + // then publish a ListUpdateEvent... +} +``` + +Kotlin + +``` +@EventListener +fun handleBlockedListEvent(event: BlockedListEvent): ListUpdateEvent { + // notify appropriate parties via notificationAddress and + // then publish a ListUpdateEvent... +} +``` + +| |[异步侦听器](#context-functionality-events-async)不支持此功能。| +|---|-----------------------------------------------------------------------------------------------| + +`handleBlockedListEvent()`方法为它处理的每个 `blockedlistevent’发布一个新的`ListUpdateEvent`。如果需要发布多个事件,可以返回`Collection`或一个事件数组。 + +##### 异步侦听器 + +如果你希望一个特定的侦听器异步地处理事件,那么可以重用[regular `@Async` support](integration.html#scheduling-annotation-support-async)。下面的示例展示了如何做到这一点: + +爪哇 + +``` +@EventListener +@Async +public void processBlockedListEvent(BlockedListEvent event) { + // BlockedListEvent is processed in a separate thread +} +``` + +Kotlin + +``` +@EventListener +@Async +fun processBlockedListEvent(event: BlockedListEvent) { + // BlockedListEvent is processed in a separate thread +} +``` + +在使用异步事件时要注意以下限制: + +* 如果异步事件侦听器抛出`Exception`,则不会将其传播给调用方。有关更多详细信息,请参见[“Asyncuncaughtexceptionhandler”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/aop/interceptor/AsyncUncaughtExceptionHandler.html)。 + +* 异步事件侦听器方法不能通过返回值来发布后续事件。如果需要发布另一个事件作为处理的结果,则注入[“ApplicationEventPublisher”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/ApplicationEventPublisher.html)手动发布该事件。 + +##### 命令听众 + +如果需要在调用另一个侦听器之前调用一个侦听器,则可以将`@Order`注释添加到方法声明中,如下例所示: + +爪哇 + +``` +@EventListener +@Order(42) +public void processBlockedListEvent(BlockedListEvent event) { + // notify appropriate parties via notificationAddress... +} +``` + +Kotlin + +``` +@EventListener +@Order(42) +fun processBlockedListEvent(event: BlockedListEvent) { + // notify appropriate parties via notificationAddress... +} +``` + +##### 一般事件 + +你还可以使用泛型来进一步定义事件的结构。考虑使用 `EntityCreateDevent` 其中`T`是实际创建的实体的类型。例如,你可以创建以下监听器定义,以便只接收 `person’的`EntityCreatedEvent`: + +爪哇 + +``` +@EventListener +public void onPersonCreated(EntityCreatedEvent event) { + // ... +} +``` + +Kotlin + +``` +@EventListener +fun onPersonCreated(event: EntityCreatedEvent) { + // ... +} +``` + +由于类型擦除,只有当触发的事件解析了事件侦听器过滤的通用参数(即,类似于 `Class PersonCreateDevent Extends EntityCreateDevent{…}`)时,这种方法才有效。 + +在某些情况下,如果所有事件都遵循相同的结构,这可能会变得非常乏味(前面示例中的事件就是这种情况)。在这种情况下,你可以实现`ResolvableTypeProvider`以指导超出运行时环境所提供的框架。以下事件展示了如何做到这一点: + +爪哇 + +``` +public class EntityCreatedEvent extends ApplicationEvent implements ResolvableTypeProvider { + + public EntityCreatedEvent(T entity) { + super(entity); + } + + @Override + public ResolvableType getResolvableType() { + return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(getSource())); + } +} +``` + +Kotlin + +``` +class EntityCreatedEvent(entity: T) : ApplicationEvent(entity), ResolvableTypeProvider { + + override fun getResolvableType(): ResolvableType? { + return ResolvableType.forClassWithGenerics(javaClass, ResolvableType.forInstance(getSource())) + } +} +``` + +| |这不仅适用于`ApplicationEvent`,还适用于作为
事件发送的任意对象。| +|---|--------------------------------------------------------------------------------------------------| + +#### 1.15.3.对低级资源的方便访问 + +为了优化应用程序上下文的使用和理解,你应该熟悉 Spring 的`Resource`抽象,如[资源](#resources)中所述。 + +应用程序上下文是`ResourceLoader`,它可以用来加载`Resource`对象。a`Resource`本质上是 JDK`java.net.URL`类的一个功能更丰富的版本。实际上,在`Resource`的实现中,在适当的情况下包装`java.net.URL`的实例。a`Resource`可以以透明的方式从几乎任何位置获得低级资源,包括从 Classpath、文件系统位置、标准 URL 可描述的任何地方以及其他一些变体。如果资源位置字符串是一个没有任何特殊前缀的简单路径,那么这些资源的来源是特定的,并且适合实际的应用程序上下文类型。 + +你可以配置部署到应用程序上下文中的 Bean,以实现特殊的回调接口`ResourceLoaderAware`,在初始化时自动回调,并将应用程序上下文本身作为`ResourceLoader`传入。还可以公开类型`Resource`的属性,用于访问静态资源。它们像其他任何属性一样被注入其中。你可以将这些`Resource`属性指定为简单的`String`路径,并依赖于在部署 Bean 时将这些文本字符串自动转换为实际的`Resource`对象。 + +提供给`ApplicationContext`构造函数的位置路径实际上是资源字符串,并且以简单的形式根据特定的上下文实现进行适当的处理。例如,`ClassPathXmlApplicationContext`将简单的位置路径视为 Classpath 位置。你还可以使用带有特殊前缀的位置路径(资源字符串)来强制从 Classpath 或 URL 加载定义,而不管实际的上下文类型是什么。 + +#### 1.15.4.应用程序启动跟踪 + +`ApplicationContext`管理 Spring 应用程序的生命周期,并提供围绕组件的丰富的编程模型。因此,复杂的应用程序可以具有同样复杂的组件图和启动阶段。 + +使用特定的指标跟踪应用程序的启动步骤可以帮助了解在启动阶段花费的时间,但也可以将其用作更好地理解整个上下文生命周期的一种方式。 + +`AbstractApplicationContext`(及其子类)用一个 `ApplicationStartup’进行检测,它收集关于各种启动阶段的`StartupStep`数据: + +* 应用程序上下文生命周期(基本包扫描、配置类管理) + +* bean 生命周期(实例化、智能初始化、后处理) + +* 应用程序事件处理 + +下面是`AnnotationConfigApplicationContext`中的一个插装示例: + +爪哇 + +``` +// create a startup step and start recording +StartupStep scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan"); +// add tagging information to the current step +scanPackages.tag("packages", () -> Arrays.toString(basePackages)); +// perform the actual phase we're instrumenting +this.scanner.scan(basePackages); +// end the current step +scanPackages.end(); +``` + +Kotlin + +``` +// create a startup step and start recording +val scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan") +// add tagging information to the current step +scanPackages.tag("packages", () -> Arrays.toString(basePackages)) +// perform the actual phase we're instrumenting +this.scanner.scan(basePackages) +// end the current step +scanPackages.end() +``` + +应用程序上下文已经通过多个步骤进行了检测。一旦记录了这些启动步骤,就可以用特定的工具收集、显示和分析这些步骤。有关现有启动步骤的完整列表,你可以查看[专用附录部分](#application-startup-steps)。 + +默认的`ApplicationStartup`实现是一个无操作的变体,以减少开销。这意味着默认情况下,在应用程序启动期间不会收集任何指标。 Spring Framework 附带了一种使用 爪哇 飞行记录器跟踪启动步骤的实现:“FlightRecorderApplicationStartup”。要使用这个变体,你必须在创建它之后立即将它的实例配置为`ApplicationContext`。 + +如果开发人员提供自己的“AbstractApplicationContext”子类,或者如果他们希望收集更精确的数据,那么他们也可以使用`ApplicationStartup`基础架构。 + +| |`ApplicationStartup`仅在应用程序启动期间使用,并且用于
核心容器;这绝不是对 爪哇 探查器或
等度量类库的替代。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +要开始收集自定义`StartupStep`,组件可以直接从应用程序上下文获得`ApplicationStartup`实例,使其组件实现`ApplicationStartupAware`,或者在任何注入点上请求`ApplicationStartup`类型。 + +| |开发人员在创建自定义启动步骤时不应使用`"spring.*"`名称空间。
此名称空间是为内部 Spring 使用而保留的,并且可能会发生更改。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.15.5.用于 Web 应用程序的方便的应用程序上下文实例化 + +例如,你可以使用“contextloader”来声明性地创建`ApplicationContext`实例。当然,你也可以通过使用`ApplicationContext`实现之一以编程方式创建`ApplicationContext`实例。 + +你可以使用`ContextLoaderListener`注册`ApplicationContext`,如下例所示: + +``` + + contextConfigLocation + /WEB-INF/daoContext.xml /WEB-INF/applicationContext.xml + + + + org.springframework.web.context.ContextLoaderListener + +``` + +侦听器检查`contextConfigLocation`参数。如果参数不存在,侦听器将使用`/WEB-INF/applicationContext.xml`作为默认值。当参数确实存在时,侦听器使用预定义的分隔符(逗号、分号和空格)分隔`String`,并将这些值用作搜索应用程序上下文的位置。 Ant-样式的路径模式也被支持。例如`/WEB-INF/*Context.xml`(对于所有名称以 `context.xml’结尾且位于`WEB-INF`目录中的文件)和`/WEB-INF/**/*Context.xml`(对于`WEB-INF`任意子目录中的所有此类文件)。 + +#### 1.15.6.将 Spring `ApplicationContext`部署为 爪哇 EE RAR 文件 + +可以将 Spring `ApplicationContext`部署为 RAR 文件,将上下文及其所需的 Bean 类和库 JAR 封装在 爪哇 EE RAR 部署单元中。这相当于引导一个独立的`ApplicationContext`(仅托管在 爪哇 EE 环境中)能够访问 爪哇 EE 服务器设施。RAR 部署是部署无头 WAR 文件的一种更自然的替代方案——实际上,这是一个没有任何 HTTP 入口点的 WAR 文件,仅用于在 爪哇 EE 环境中引导 Spring“ApplicationContext”。 + +对于不需要 HTTP 入口点,而只包含消息端点和计划作业的应用程序上下文,RAR 部署是理想的。在这样的上下文中,Bean 可以使用应用服务器资源,例如 JTA 事务管理器和绑定 JNDI 的 JDBC“数据源”实例和 JMS`ConnectionFactory`实例,还可以通过 Spring 的标准事务管理和 JNDI 和 JMX 支持设施,向平台的 JMX 服务器注册。应用程序组件还可以通过 Spring 的`TaskExecutor`抽象与应用程序服务器的 JCA`WorkManager`进行交互。 + +有关 RAR 部署所涉及的配置细节,请参见[“SpringContextResourceAdapter”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jca/context/SpringContextResourceAdapter.html)类的 爪哇doc。 + +将 Spring ApplicationContext 作为 爪哇 EE RAR 文件进行简单部署: + +1. 将所有应用程序类打包到一个 RAR 文件中(这是一个标准的 jar 文件,具有不同的文件扩展名)。 + +2. 将所有必需的库 JAR 添加到 RAR 归档的根目录中。 + +3. 添加 `meta-inf/ra.xml` 部署描述符(如[javadoc for `SpringContextResourceAdapter`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jca/context/SpringContextResourceAdapter.html)所示)和相应的 Spring xml Bean 定义文件(通常为 `meta-inf/applicationcontext.xml`)。 + +4. 将生成的 RAR 文件放入应用程序服务器的部署目录中。 + +| |这样的 RAR 部署单元通常是独立的。它们不向外部世界公开组件
,甚至不向同一应用程序的其他模块公开组件。与基于
RAR 的`ApplicationContext`的交互通常通过它与
其他模块共享的 JMS 目的地进行。基于 RAR 的`ApplicationContext`也可以,例如,调度一些作业
或对文件系统中的新文件做出反应(或类似)。如果它需要允许来自外部的同步
访问,则它可以(例如)导出 RMI 端点,这可以由同一台机器上的其他应用程序模块使用
。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 1.16.the`BeanFactory` + +`BeanFactory`API 为 Spring 的 IOC 功能提供了底层基础。它的特定契约主要用于与 Spring 和相关的第三方框架的其他部分的集成,并且它的`DefaultListableBeanFactory`实现是更高级别`GenericApplicationContext`容器内的一个关键委托。 + +`BeanFactory`和相关接口(如`BeanFactoryAware`,`InitializingBean`,`DisaBleBean`)是其他框架组件的重要集成点。通过不需要任何注释,甚至不需要反射,它们允许容器与其组件之间进行非常有效的交互。应用程序级 bean 可能使用相同的回调接口,但通常更喜欢声明性依赖注入,或者通过注释,或者通过编程配置。 + +请注意,核心`BeanFactory`API 级别及其`DefaultListableBeanFactory`实现不对要使用的配置格式或任何组件注释进行假设。所有这些风格都是通过扩展(例如`XmlBeanDefinitionReader`和`AutowiredAnnotationBeanPostProcessor`)来实现的,并在共享的`BeanDefinition`对象上作为核心元数据表示进行操作。这就是使 Spring 的容器如此灵活和可扩展的本质。 + +#### 1.16.1.`BeanFactory`还是`ApplicationContext`? + +本节解释`BeanFactory`和 `ApplicationContext’容器级别之间的区别以及对引导的影响。 + +除非你有充分的理由不这样做,否则你应该使用`ApplicationContext`,并使用 `GenericApplicationContext’及其子类`AnnotationConfigApplicationContext`作为自定义引导的常见实现。这些是 Spring 核心容器的主要入口点,用于所有常见目的:加载配置文件,触发 Classpath 扫描,以编程方式注册 Bean 定义和注释的类,以及(截至 5.0)注册功能 Bean 定义。 + +因为`ApplicationContext`包含了`BeanFactory`的所有功能,所以一般建议使用普通的`BeanFactory`,除非需要对 Bean 处理进行完全控制。在`ApplicationContext`(例如“GenericApplicationContext”实现)中,根据约定(即通过 Bean name 或 Bean type——特别是后处理器)检测几种 bean,而普通的`DefaultListableBeanFactory`对任何特殊的 bean 都是不可知的。 + +对于许多扩展的容器特性,例如注释处理和 AOP 代理,[“BeanPostProcessor”扩展点](#beans-factory-extension-bpp)是必不可少的。如果只使用普通的`DefaultListableBeanFactory`,则默认情况下不会检测到并激活这样的后处理器。这种情况可能会令人困惑,因为你的 Bean 配置实际上没有任何问题。相反,在这种情况下,容器需要通过额外的设置进行完全引导。 + +下表列出了`BeanFactory`和 `ApplicationContext’接口和实现提供的功能。 + +|特征|`BeanFactory`|`ApplicationContext`| +|------------------------------------------------------------|-------------|--------------------| +|Bean 实例化/布线| Yes | Yes | +|集成的生命周期管理| No | Yes | +|自动`BeanPostProcessor`注册| No | Yes | +|自动`BeanFactoryPostProcessor`注册| No | Yes | +|方便`MessageSource`访问(用于国际化)| No | Yes | +|内置`ApplicationEvent`发布机制| No | Yes | + +要显式地用`DefaultListableBeanFactory`注册 Bean 后处理器,需要以编程方式调用`addBeanPostProcessor`,如下例所示: + +爪哇 + +``` +DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); +// populate the factory with bean definitions + +// now register any needed BeanPostProcessor instances +factory.addBeanPostProcessor(new AutowiredAnnotationBeanPostProcessor()); +factory.addBeanPostProcessor(new MyBeanPostProcessor()); + +// now start using the factory +``` + +Kotlin + +``` +val factory = DefaultListableBeanFactory() +// populate the factory with bean definitions + +// now register any needed BeanPostProcessor instances +factory.addBeanPostProcessor(AutowiredAnnotationBeanPostProcessor()) +factory.addBeanPostProcessor(MyBeanPostProcessor()) + +// now start using the factory +``` + +要将`BeanFactoryPostProcessor`应用于普通的`DefaultListableBeanFactory`,需要调用其`postProcessBeanFactory`方法,如下例所示: + +爪哇 + +``` +DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); +XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory); +reader.loadBeanDefinitions(new FileSystemResource("beans.xml")); + +// bring in some property values from a Properties file +PropertySourcesPlaceholderConfigurer cfg = new PropertySourcesPlaceholderConfigurer(); +cfg.setLocation(new FileSystemResource("jdbc.properties")); + +// now actually do the replacement +cfg.postProcessBeanFactory(factory); +``` + +Kotlin + +``` +val factory = DefaultListableBeanFactory() +val reader = XmlBeanDefinitionReader(factory) +reader.loadBeanDefinitions(FileSystemResource("beans.xml")) + +// bring in some property values from a Properties file +val cfg = PropertySourcesPlaceholderConfigurer() +cfg.setLocation(FileSystemResource("jdbc.properties")) + +// now actually do the replacement +cfg.postProcessBeanFactory(factory) +``` + +在这两种情况下,显式的注册步骤都是不方便的,这就是为什么在 Spring 支持的应用程序中,各种`ApplicationContext`变体比普通的 `DefaultListableBeanFactory’更受欢迎,特别是在典型的 Enterprise 设置中依赖`BeanFactoryPostProcessor`和`BeanPostProcessor`实例实现扩展容器功能的情况下。 + +| |一个`AnnotationConfigApplicationContext`已经注册了所有常见的注释后处理器
,并且可以通过配置注释在
下面引入额外的处理器,例如`@EnableTransactionManagement`。
在 Spring 的基于注释的配置模型的抽象级,
后处理器的概念变成了仅仅是内部容器的详细信息。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## 2. Resources + +本章介绍了 Spring 如何处理资源,以及如何使用 Spring 中的资源。它包括以下主题: + +* [Introduction](#resources-introduction) + +* [The `Resource` Interface](#resources-resource) + +* [Built-in `Resource` Implementations](#resources-implementations) + +* [The `ResourceLoader` Interface](#resources-resourceloader) + +* [The `ResourcePatternResolver` Interface](#resources-resourcepatternresolver) + +* [The `ResourceLoaderAware` Interface](#resources-resourceloaderaware) + +* [作为依赖关系的资源](#resources-as-dependencies) + +* [应用程序上下文和资源路径](#resources-app-ctx) + +### 2.1.导言 + +遗憾的是,爪哇 的标准`java.net.URL`类和用于各种 URL 前缀的标准处理程序,对于所有对低级资源的访问都不够充分。例如,没有标准的`URL`实现可用于访问需要从 Classpath 或相对于“servletContext”获得的资源。虽然可以为专门的`URL`前缀注册新的处理程序(类似于现有的用于`http:`等前缀的处理程序),但这通常是相当复杂的,而且`URL`接口仍然缺乏一些理想的功能,例如检查所指向的资源是否存在的方法。 + +### 2.2.`Resource`接口 + +Spring 的`Resource`接口位于`org.springframework.core.io.`包中,这意味着它是一个更有能力的接口,用于抽象对低级资源的访问。下面的清单提供了`Resource`接口的概述。有关更多详细信息,请参见[`Resource`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/core/io/Resource.html)爪哇doc。 + +``` +public interface Resource extends InputStreamSource { + + boolean exists(); + + boolean isReadable(); + + boolean isOpen(); + + boolean isFile(); + + URL getURL() throws IOException; + + URI getURI() throws IOException; + + File getFile() throws IOException; + + ReadableByteChannel readableChannel() throws IOException; + + long contentLength() throws IOException; + + long lastModified() throws IOException; + + Resource createRelative(String relativePath) throws IOException; + + String getFilename(); + + String getDescription(); +} +``` + +正如`Resource`接口的定义所示,它扩展了`InputStreamSource`接口。下面的清单显示了`InputStreamSource`接口的定义: + +``` +public interface InputStreamSource { + + InputStream getInputStream() throws IOException; +} +``` + +来自`Resource`接口的一些最重要的方法是: + +* `getInputStream()`:定位并打开资源,返回一个`InputStream`用于从资源中读取。预计每次调用都会返回一个新的“inputstream”。这是来电者的责任,以关闭该流。 + +* `exists()`:返回一个`boolean`,指示此资源是否实际以物理形式存在。 + +* `isOpen()`:返回一个`boolean`,指示此资源是否表示具有开放流的句柄。如果`true`,则`InputStream`不能多次读取,必须只读一次,然后关闭,以避免资源泄漏。对于所有常见的资源实现,返回`false`,但`InputStreamResource`除外。 + +* `getDescription()`:返回此资源的描述,用于在使用该资源时进行错误输出。这通常是完全限定的文件名或资源的实际 URL。 + +其他方法允许你获得代表资源的实际`URL`或`File`对象(如果底层实现是兼容的并且支持该功能)。 + +`Resource`接口的一些实现还实现了用于支持对其进行写入的资源的扩展[“可写资源”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/core/io/WritableResource.html)接口。 + +Spring 本身广泛地使用`Resource`抽象,在需要资源时作为许多方法签名中的参数类型。在一些 Spring API 中的其他方法(例如各种`ApplicationContext`实现的构造函数)采用一个 `string’,该 string’以朴素或简单的形式用于创建适合该上下文实现的`Resource`,或者通过`String`路径上的特殊前缀,让调用者指定必须创建和使用特定的`Resource`实现。 + +虽然`Resource`接口在 Spring 和 Spring 中被大量使用,但实际上,在你自己的代码中将其本身用作一个通用实用程序类来访问资源是非常方便的,即使你的代码不知道或不关心 Spring 的任何其他部分。虽然这将你的代码耦合到 Spring,但它实际上只将它耦合到这一小组实用程序类,它可以作为`URL`的更强大的替代,并且可以被认为与你将用于此目的的任何其他库等同。 + +| |`Resource`抽象不会取代功能。它将它封装在
可能的位置。例如,`UrlResource`包装一个 URL,并使用包装好的`URL`来完成其
工作。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 2.3.内置`Resource`实现 + +Spring 包括几个内置`Resource`实现方式: + +* [`UrlResource`](#resources-implementations-urlresource) + +* [“classpathresource”](#resources-implementations-classpathresource) + +* [“文件系统资源”](#resources-implementations-filesystemresource) + +* [`PathResource`](#resources-implementations-pathresource) + +* [“ServletContextResource”](#resources-implementations-servletcontextresource) + +* [InputStreamResource](#resources-implementations-inputstreamresource) + +* [ByteArrayResource](#resources-implementations-bytearrayresource) + +有关 Spring 中可用的`Resource`实现的完整列表,请参阅[`Resource`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/core/io/Resource.html)爪哇doc 中的“所有已知实现类”部分。 + +#### 2.3.1.`UrlResource` + +`UrlResource`封装`java.net.URL`,并可用于访问通常通过 URL 可访问的任何对象,例如文件、HTTPS 目标、FTP 目标和其他对象。所有的 URL 都有一个标准化的`String`表示,使得适当的标准化前缀被用来从另一个 URL 类型中指示一个 URL 类型。这包括用于访问文件系统路径的“file:”,用于通过 HTTPS 协议访问资源的`https:`,用于通过 FTP 访问资源的`ftp:`,以及其他。 + +`UrlResource`是由 爪哇 代码通过显式地使用`UrlResource`构造函数创建的,但是当你调用一个 API 方法时,它通常是隐式创建的,该 API 方法接受一个旨在表示路径的`String`参数。对于后一种情况,爪哇Beans`PropertyEditor`最终决定创建哪种类型的`Resource`。如果路径字符串包含一个众所周知的前缀(对于属性编辑器来说,就是)(例如`classpath:`),那么它将为该前缀创建一个适当的专门的`Resource`。但是,如果它不能识别前缀,它将假设字符串是一个标准的 URL 字符串,并创建一个`UrlResource`。 + +#### 2.3.2.`ClassPathResource` + +这个类表示应该从 Classpath 获得的资源。它使用线程上下文类加载器、给定类加载器或给定类来加载资源。 + +如果类路径资源驻留在文件系统中,而不是驻留在 jar 中且尚未(通过 Servlet 引擎或任何环境)扩展到文件系统的 Classpath 资源,则此`Resource`实现支持解析为`java.io.File`。为了解决这个问题,各种`Resource`实现总是支持分辨率为`java.net.URL`。 + +`ClassPathResource`是由 爪哇 代码通过显式地使用`ClassPathResource`构造函数创建的,但是当你调用一个 API 方法时,它通常是隐式地创建的,该 API 方法接受一个表示路径的 `string’参数。对于后一种情况,爪哇Beans`PropertyEditor` 识别字符串路径上的特殊前缀`classpath:`,并在这种情况下创建`ClassPathResource`。 + +#### 2.3.3.`FileSystemResource` + +这是`Resource`句柄的`java.io.File`实现。它还支持 `java.蔚来.file.path` 句柄,应用 Spring 的标准基于字符串的路径转换,但通过`java.nio.file.Files`API 执行所有操作。对于纯粹的基于“java.蔚来.path.path”的支持,可以使用`PathResource`。`FileSystemResource`支持分辨率为`File`和`URL`。 + +#### 2.3.4.`PathResource` + +这是用于`Resource`句柄的`java.nio.file.Path`实现,通过`Path`API 执行所有操作和转换。它以`File`和`URL`的形式支持分辨率,并且还实现了扩展的`WritableResource`接口。`PathResource`实际上是一种纯粹的基于`java.nio.path.Path`的替代方法,与`FileSystemResource`具有不同的`createRelative`行为。 + +#### 2.3.5.`ServletContextResource` + +这是`Resource`资源的`ServletContext`实现,用于解释相关 Web 应用程序根目录中的相对路径。 + +它始终支持流访问和 URL 访问,但仅当 Web 应用程序归档被扩展并且资源在文件系统上时才允许`java.io.File`访问。它是否被扩展并在文件系统上或直接从 jar 或类似数据库的其他地方(这是可以想象的)访问,实际上取决于 Servlet 容器。 + +#### 2.3.6.`InputStreamResource` + +`InputStreamResource`是给定`InputStream`的`Resource`实现。只有在不适用特定的`Resource`实现的情况下,才应该使用它。特别地,在可能的情况下优选`ByteArrayResource`或任何基于文件的`Resource`实现方式。 + +与其他`Resource`实现不同,这是对已经打开的资源的描述符。因此,它从`isOpen()`返回`true`。如果你需要将资源描述符保存在某个地方,或者如果你需要多次读取流,请不要使用它。 + +#### 2.3.7.`ByteArrayResource` + +这是给定字节数组的`Resource`实现。它为给定字节数组创建一个“BytearrayInputStream”。 + +它对于从任何给定的字节数组加载内容都很有用,而不必使用一次性`InputStreamResource`。 + +### 2.4.`ResourceLoader`接口 + +`ResourceLoader`接口旨在通过可以返回(即加载)`Resource`实例的对象来实现。下面的清单显示了`ResourceLoader`接口定义: + +``` +public interface ResourceLoader { + + Resource getResource(String location); + + ClassLoader getClassLoader(); +} +``` + +所有应用程序上下文都实现`ResourceLoader`接口。因此,可以使用所有应用程序上下文来获得`Resource`实例。 + +当你在特定的应用程序上下文中调用`getResource()`,并且指定的位置路径没有特定的前缀时,你将返回一个适合该特定应用程序上下文的`Resource`类型。例如,假设对`ClassPathXmlApplicationContext`实例运行了以下代码片段: + +爪哇 + +``` +Resource template = ctx.getResource("some/resource/path/myTemplate.txt"); +``` + +Kotlin + +``` +val template = ctx.getResource("some/resource/path/myTemplate.txt") +``` + +对于`ClassPathXmlApplicationContext`,该代码返回`ClassPathResource`。如果对`FileSystemXmlApplicationContext`实例运行相同的方法,它将返回`FileSystemResource`。对于`WebApplicationContext`,它将返回一个“servletContextResource”。类似地,它将为每个上下文返回适当的对象。 + +因此,你可以以适合特定应用程序上下文的方式加载资源。 + +另一方面,你也可以通过指定特殊的`classpath:`前缀来强制使用`ClassPathResource`,而不考虑应用程序的上下文类型,如下例所示: + +爪哇 + +``` +Resource template = ctx.getResource("classpath:some/resource/path/myTemplate.txt"); +``` + +Kotlin + +``` +val template = ctx.getResource("classpath:some/resource/path/myTemplate.txt") +``` + +类似地,你可以通过指定任何标准的 `java.net.url’前缀来强制使用`UrlResource`。以下示例使用`file`和`https`前缀: + +爪哇 + +``` +Resource template = ctx.getResource("file:///some/resource/path/myTemplate.txt"); +``` + +Kotlin + +``` +val template = ctx.getResource("file:///some/resource/path/myTemplate.txt") +``` + +爪哇 + +``` +Resource template = ctx.getResource("https://myhost.com/resource/path/myTemplate.txt"); +``` + +Kotlin + +``` +val template = ctx.getResource("https://myhost.com/resource/path/myTemplate.txt") +``` + +下表总结了将`String`对象转换为`Resource`对象的策略: + +| Prefix | Example |解释| +|----------|--------------------------------|----------------------------------------------------------------------------------------------------------------------| +|classpath:|`classpath:com/myapp/config.xml`|从 Classpath 加载。| +| file: | `file:///data/config.xml` |从文件系统加载为`URL`。另见[“文件系统资源”警告](#resources-filesystemresource-caveats)。| +| https: | `https://myserver/logo.png` |加载为`URL`。| +| (none) | `/data/config.xml` |取决于底层`ApplicationContext`。| + +### 2.5.`ResourcePatternResolver`接口 + +`ResourcePatternResolver`接口是`ResourceLoader`接口的扩展,它定义了将位置模式(例如, Ant 样式的路径模式)解析为`Resource`对象的策略。 + +``` +public interface ResourcePatternResolver extends ResourceLoader { + + String CLASSPATH_ALL_URL_PREFIX = "classpath*:"; + + Resource[] getResources(String locationPattern) throws IOException; +} +``` + +如上面所示,该接口还为来自类路径的所有匹配资源定义了一个特殊的`classpath*:`资源前缀。请注意,在这种情况下,资源位置应该是一个没有占位符的路径——例如,“ Classpath *:/config/beans.xml”。 jar 类路径中的文件或不同的目录可以包含具有相同路径和相同名称的多个文件。有关使用`classpath*:`资源前缀的通配符支持的更多详细信息,请参见[应用程序上下文构造函数资源路径中的通配符](#resources-app-ctx-wildcards-in-resource-paths)及其子节。 + +可以检查传入的-in`ResourceLoader`(例如,通过[“资源装载器意识”](#resources-resourceloaderaware)语义提供的一个)是否也实现了此扩展接口。 + +`PathMatchingResourcePatternResolver`是一个独立的实现,可以在`ApplicationContext`之外使用,并且`ResourceArrayPropertyEditor`也用于填充`Resource[]` Bean 属性。`PathMatchingResourcePatternResolver`能够将指定的资源定位路径解析为一个或多个匹配的`Resource`对象。源路径可以是一个简单的路径,该路径具有到目标“资源”的一对一映射,或者可替换地包含特殊的`classpath*:`前缀和/或内部 Ant 样式的正则表达式(使用 Spring 的 `org.springframework.util.antpathmatcher’实用程序进行匹配)。后者实际上都是通配符。 + +| |在任何标准`ApplicationContext`中,默认的`ResourceLoader`实际上是
的一个实例`PathMatchingResourcePatternResolver`,它实现了`ResourcePatternResolver`接口。对于`ApplicationContext`实例本身也是如此,它也
实现了`ResourcePatternResolver`接口并将其委托给默认的 `pathmatchingresourcepatternresolver’。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 2.6.`ResourceLoaderAware`接口 + +`ResourceLoaderAware`接口是一种特殊的回调接口,用于标识期望提供`ResourceLoader`引用的组件。下面的清单显示了`ResourceLoaderAware`接口的定义: + +``` +public interface ResourceLoaderAware { + + void setResourceLoader(ResourceLoader resourceLoader); +} +``` + +当一个类实现`ResourceLoaderAware`并被部署到应用程序上下文中(作为 Spring 管理的 Bean)时,应用程序上下文将其识别为`ResourceLoaderAware`。然后,应用程序上下文调用`setResourceLoader(ResourceLoader)`,将自身作为参数提供(请记住, Spring 中的所有应用程序上下文都实现`ResourceLoader`接口)。 + +由于`ApplicationContext`是`ResourceLoader`, Bean 还可以实现 `ApplicationContextAware’接口,并直接使用提供的应用程序上下文来加载资源。然而,一般来说,如果这就是你所需要的,那么最好使用专门的`ResourceLoader`接口。该代码将仅耦合到资源加载接口(可被视为实用程序接口),而不耦合到整个 Spring `ApplicationContext’接口。 + +在应用程序组件中,还可以依赖`ResourceLoader`的自动布线作为实现`ResourceLoaderAware`接口的替代方案。*传统的*`constructor’和`byType`自动布线模式(如[自动布线合作者](#beans-factory-autowire)中所述)能够分别为构造函数参数或 setter 方法参数提供`ResourceLoader`。要获得更大的灵活性(包括自动连接字段和多个参数方法的能力),请考虑使用基于注释的自动连接功能。在这种情况下,`ResourceLoader`将自动连接到一个字段、构造函数参数或方法参数中,只要所讨论的字段、构造函数或方法带有`ResourceLoader`注释,这些参数就需要`@Autowired`类型。有关更多信息,请参见[Using `@Autowired`](#beans-autowired-annotation)。 + +| |要为包含通配符
的资源路径加载一个或多个`Resource`对象,或者使用特殊的`classpath*:`资源前缀,请考虑将[“ResourcePatternResolver”](#resources-resourcepatternresolver)的实例自动连接到你的
应用程序组件中,而不是`ResourceLoader`。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 2.7.作为依赖关系的资源 + +如果 Bean 本身将通过某种动态过程来确定和提供资源路径,那么 Bean 使用`ResourceLoader`或 `ResourcePatternResolver’接口来加载资源可能是有意义的。例如,考虑加载某种类型的模板,其中所需的特定资源取决于用户的角色。如果资源是静态的,那么完全取消使用 `ResourceLoader’接口(或`ResourcePatternResolver`接口)是有意义的,让 Bean 公开它需要的`Resource`属性,并期望将它们注入其中。 + +然后注入这些属性的原因是,所有应用程序上下文都注册并使用一个特殊的 爪哇Beans`PropertyEditor`,它可以将`String`路径转换为`Resource`对象。例如,下面的`MyBean`类具有类型`template`的`Resource`属性。 + +爪哇 + +``` +package example; + +public class MyBean { + + private Resource template; + + public setTemplate(Resource template) { + this.template = template; + } + + // ... +} +``` + +Kotlin + +``` +class MyBean(var template: Resource) +``` + +在 XML 配置文件中,`template`属性可以为该资源配置一个简单的字符串,如下例所示: + +``` + + + +``` + +请注意,资源路径没有前缀。因此,由于应用程序上下文本身将被用作`ResourceLoader`,因此根据应用程序上下文的确切类型,资源将通过 `classpathresource’、`FileSystemResource`或`ServletContextResource`加载。 + +如果需要强制使用特定的`Resource`类型,则可以使用前缀。下面的两个示例展示了如何强制执行`ClassPathResource`和`UrlResource`(后者用于访问文件系统中的文件): + +``` + +``` + +``` + +``` + +如果对`MyBean`类进行重构以用于注释驱动的配置,则`myTemplate.txt`的路径可以存储在一个名为`template.path`的键下——例如,在 Spring `Environment`提供的属性文件中(参见[环境抽象](#beans-environment))。然后可以使用属性占位符通过`@Value`注释引用模板路径(参见[Using `@Value`](#beans-value-annotations))。 Spring 将检索模板路径的值作为字符串,而一个特殊的`PropertyEditor`将把字符串转换为一个`Resource`对象,以注入到`MyBean`构造函数中。下面的示例演示了如何实现这一点。 + +爪哇 + +``` +@Component +public class MyBean { + + private final Resource template; + + public MyBean(@Value("${template.path}") Resource template) { + this.template = template; + } + + // ... +} +``` + +Kotlin + +``` +@Component +class MyBean(@Value("\${template.path}") private val template: Resource) +``` + +如果我们希望支持在 Classpath 中的多个位置的相同路径下发现的多个模板——例如,在 Classpath 中的多个 JAR 中——我们可以使用特殊的`classpath*:`前缀和通配符将`templates.path`键定义为 ` Classpath *:/config/templates/*.TXT`。如果我们按如下方式重新定义`MyBean`类, Spring 将把模板路径模式转换为`Resource`对象的数组,这些对象可以被注入到`MyBean`构造函数中。 + +爪哇 + +``` +@Component +public class MyBean { + + private final Resource[] templates; + + public MyBean(@Value("${templates.path}") Resource[] templates) { + this.templates = templates; + } + + // ... +} +``` + +Kotlin + +``` +@Component +class MyBean(@Value("\${templates.path}") private val templates: Resource[]) +``` + +### 2.8.应用程序上下文和资源路径 + +本节介绍如何使用资源创建应用程序上下文,包括使用 XML 的快捷方式、如何使用通配符以及其他详细信息。 + +#### 2.8.1.构造应用程序上下文 + +应用程序上下文构造函数(对于特定的应用程序上下文类型)通常使用字符串或字符串数组作为资源的位置路径,例如构成上下文定义的 XML 文件。 + +当这样的位置路径不具有前缀时,特定的`Resource`类型从该路径构建并用于加载 Bean 定义所依赖的并且适合于特定的应用程序上下文。例如,考虑下面的示例,它创建了一个“ClassPathXMLApplicationContext”: + +爪哇 + +``` +ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml"); +``` + +Kotlin + +``` +val ctx = ClassPathXmlApplicationContext("conf/appContext.xml") +``` + +Bean 定义是从 Classpath 加载的,因为使用了`ClassPathResource`。但是,请考虑以下示例,该示例创建了`FileSystemXmlApplicationContext`: + +爪哇 + +``` +ApplicationContext ctx = + new FileSystemXmlApplicationContext("conf/appContext.xml"); +``` + +Kotlin + +``` +val ctx = FileSystemXmlApplicationContext("conf/appContext.xml") +``` + +现在 Bean 定义是从文件系统位置(在本例中,相对于当前工作目录)加载的。 + +请注意,在位置路径上使用特殊的`classpath`前缀或标准的 URL 前缀覆盖了为加载 Bean 定义而创建的默认类型`Resource`。考虑以下示例: + +爪哇 + +``` +ApplicationContext ctx = + new FileSystemXmlApplicationContext("classpath:conf/appContext.xml"); +``` + +Kotlin + +``` +val ctx = FileSystemXmlApplicationContext("classpath:conf/appContext.xml") +``` + +使用`FileSystemXmlApplicationContext`加载来自 Classpath 的 Bean 定义。然而,它仍然是`FileSystemXmlApplicationContext`。如果随后将其用作“ResourceLoader”,则任何未带前缀的路径仍将被视为文件系统路径。 + +##### 构造`ClassPathXmlApplicationContext`实例——快捷方式 + +`ClassPathXmlApplicationContext`公开了许多构造函数,以实现方便的实例化。基本思想是,你可以只提供一个字符串数组,该字符串数组仅包含 XML 文件本身的文件名(不包含前导路径信息),还可以提供`Class`。然后,`ClassPathXmlApplicationContext`从提供的类派生路径信息。 + +考虑以下目录布局: + +``` +com/ + example/ + services.xml + repositories.xml + MessengerService.class +``` + +下面的示例展示了如何实例化由名为`services.xml`和`repositories.xml`(位于 Classpath 上)的文件中定义的 bean 组成的`ClassPathXmlApplicationContext`实例: + +爪哇 + +``` +ApplicationContext ctx = new ClassPathXmlApplicationContext( + new String[] {"services.xml", "repositories.xml"}, MessengerService.class); +``` + +Kotlin + +``` +val ctx = ClassPathXmlApplicationContext(arrayOf("services.xml", "repositories.xml"), MessengerService::class.java) +``` + +有关各种构造函数的详细信息,请参见[“ClassPathXMLApplicationContext”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/support/ClassPathXmlApplicationContext.html)Javadoc。 + +#### 2.8.2.应用程序上下文构造函数资源路径中的通配符 ### + +应用程序上下文构造函数值中的资源路径可以是简单的路径(如前面所示),其中每个路径都具有到目标`Resource`的一对一映射,或者,或者,也可以包含特殊的`classpath*:`前缀或内部 Ant 样式的模式(通过使用 Spring 的`PathMatcher`实用工具进行匹配)。后者实际上都是通配符。 + +这种机制的一个用途是当你需要执行组件样式的应用程序组装时。所有组件都可以*发布*上下文定义片段到一个众所周知的位置路径,并且,当最终的应用程序上下文使用前缀为 ` Classpath ** 的相同路径创建时:`,所有组件片段都会被自动拾取。 + +请注意,此通配符是特定于在应用程序上下文构造函数中使用资源路径的(或者当你直接使用`PathMatcher`实用程序类层次结构时),并在构建时解析。它与`Resource`类型本身无关。你不能使用`classpath*:`前缀来构造一个实际的`Resource`,因为一个资源一次只指向一个资源。 + +##### Ant-样式模式 + +路径位置可以包含 Ant 样式的模式,如下例所示: + +``` +/WEB-INF/*-context.xml +com/mycompany/**/applicationContext.xml +file:C:/some/path/*-context.xml +classpath:com/mycompany/**/applicationContext.xml +``` + +当路径位置包含 Ant 样式的模式时,解析器遵循一个更复杂的过程来尝试解析通配符。它为到最后一个非通配符段的路径生成`Resource`,并从中获得一个 URL。如果此 URL 不是`jar:`URL 或特定于容器的变体(例如 WebLogic 中的`zip:`,WebSphere 中的`wsjar`,以此类推),则从中获得一个`java.io.File`,并通过遍历文件系统来解析通配符。在 jar URL 的情况下,解析器要么从它获取 `java.net.jarurlconnection’,要么手动解析 jar URL,然后遍历 jar 文件的内容以解析通配符。 + +###### 对可移植性的影响 + +如果指定的路径已经是`file`URL(由于基本的 `ResourceLoader’是一个文件系统,所以隐式地或显式地),那么通配符将保证以完全可移植的方式工作。 + +如果指定的路径是`classpath`位置,则解析器必须通过进行`Classloader.getResource()`调用来获得最后一个非通配符路径段 URL。因为这只是路径的一个节点(而不是末尾的文件),所以它实际上是未定义的(在 `classloader’Javadoc 中),在这种情况下返回的正是哪种类型的 URL。在实践中,它总是表示目录(其中 Classpath 资源解析为文件系统位置)或某种类型的 jar URL(其中 Classpath 资源解析为 jar 位置)的`java.io.File`。不过,这种操作仍然存在移植性方面的问题。 + +如果为最后一个非通配符段获得了 jar URL,则解析器必须能够从其获得`java.net.JarURLConnection`或手动解析 jar URL,以便能够遍历 jar 中的内容并解析通配符。这在大多数环境中确实有效,但在其他环境中失败,我们强烈建议在依赖它之前,在你的特定环境中对来自 JAR 的资源的通配符解析进行彻底测试。 + +##### `classpath*:`前缀 + +在构建基于 XML 的应用程序上下文时,位置字符串可以使用特殊的`classpath*:`前缀,如下例所示: + +Java + +``` +ApplicationContext ctx = + new ClassPathXmlApplicationContext("classpath*:conf/appContext.xml"); +``` + +Kotlin + +``` +val ctx = ClassPathXmlApplicationContext("classpath*:conf/appContext.xml") +``` + +这个特殊的前缀指定必须获得所有与给定名称匹配的 Classpath 资源(在内部,这基本上是通过调用 `classloader.getResources(…)’实现的),然后将其合并以形成最终的应用程序上下文定义。 + +| |通配符 Classpath 依赖于底层“classloader”的`getResources()`方法。由于现在大多数应用程序服务器都提供它们自己的`ClassLoader`实现,因此行为可能会有所不同,特别是在处理 jar 文件时。一个
检查`classpath*`是否有效的简单测试是使用`ClassLoader`在 Classpath 上的 jar 内从
加载一个文件:`getclass().getclassloader().getresources(“”)`。使用具有相同名称但驻留在两个不同位置的
文件尝试此测试——例如,在 Classpath 上具有相同名称和相同路径但位于不同 JAR 中的
文件。如果返回了
不合适的结果,请检查应用服务器文档中的设置
,这些设置可能会影响`ClassLoader`行为。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +你还可以在位置路径的其余部分(例如,`classpath*:META-INF/*-beans.xml`)中将`classpath*:`前缀与`PathMatcher`模式相结合。在这种情况下,解析策略相当简单:在最后一个非通配符路径段上使用`ClassLoader.getResources()`调用,以获取类装入器层次结构中的所有匹配资源,然后关闭每个资源,前面描述的`PathMatcher`解析策略也用于通配符的子路径。 + +##### 与通配符有关的其他注释 + +注意`classpath*:`,当与 Ant 样式的模式相结合时,在模式开始之前,仅对至少一个根目录可靠地工作,除非实际的目标文件驻留在文件系统中。这意味着,像“ Classpath *:*.xml”这样的模式可能不会从 jar 文件的根目录检索文件,而只会从扩展目录的根目录检索文件。 + +Spring 检索 Classpath 条目的能力源于 JDK 的 `classloader.getResources()’方法,该方法仅返回空字符串的文件系统位置(指示要搜索的潜在根)。 Spring 还在 jar 文件中评估 `urlclassloader` 运行时配置和`java.class.path`清单,但这不能保证导致可移植行为。 + +| |Classpath 包的扫描需要在 Classpath 中存在相应的目录
条目。当你使用 Ant 构建 JAR 时,不要激活 jar 任务的`files-only`开关。此外,在某些环境中, Classpath 目录可能不会基于安全性
策略而被公开——例如,在 JDK1.7.0\_45
上的独立应用程序以及更高的环境中(这需要在你的清单中设置“可信库”)。参见[https://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources](https://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources))。

在 JDK9 的模块路径(拼图)上, Spring 的 Classpath 扫描通常如预期的那样工作。
将资源放入专用目录在这里也是非常值得推荐的,
在搜索 jar 文件根级别时避免了前面提到的可移植性问题。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +Ant-样式模式具有资源,如果要搜索的根包在多个 Classpath 位置可用,则不能保证找到匹配的资源。考虑以下资源位置示例: + +``` +com/mycompany/package1/service-context.xml +``` + +现在考虑一个 Ant 风格的路径,可能有人会使用它来尝试找到该文件: + +``` +classpath:com/mycompany/**/service-context.xml +``` + +这样的资源可能仅存在于 Classpath 中的一个位置中,但是当使用诸如前面的示例的路径来尝试解析它时,解析器工作于由`getResource("com/mycompany");`返回的(第一个)URL。如果此基本包节点存在于多个`ClassLoader`位置中,则所需的资源可能不存在于第一个位置中。因此,在这种情况下,你应该更喜欢使用具有相同 Ant 样式模式的`classpath*:`,该模式搜索包含 `com.mycompany’基本包的所有 Classpath 位置:`classpath*:com/mycompany/**/service-context.xml`。 + +#### 2.8.3.`FileSystemResource`注意事项 + +不附加到`FileSystemResource`的`FileSystemApplicationContext`(即当`FileSystemApplicationContext`不是实际的`ResourceLoader`时)按预期处理绝对路径和相对路径。相对路径是相对于当前工作目录的,而绝对路径是相对于文件系统的根的。 + +但是,由于向后兼容性(历史原因)的原因,当“文件系统映射上下文”是`ResourceLoader`时,这种情况就会改变。“filesystemapplicationContext”强制所有附加的`FileSystemResource`实例将所有位置路径视为相对的,无论它们是否以前导斜杠开始。在实践中,这意味着以下示例是等效的: + +Java + +``` +ApplicationContext ctx = + new FileSystemXmlApplicationContext("conf/context.xml"); +``` + +Kotlin + +``` +val ctx = FileSystemXmlApplicationContext("conf/context.xml") +``` + +Java + +``` +ApplicationContext ctx = + new FileSystemXmlApplicationContext("/conf/context.xml"); +``` + +Kotlin + +``` +val ctx = FileSystemXmlApplicationContext("/conf/context.xml") +``` + +下面的例子也是等价的(尽管它们不同是有意义的,因为一种情况是相对的,另一种情况是绝对的): + +Java + +``` +FileSystemXmlApplicationContext ctx = ...; +ctx.getResource("some/resource/path/myTemplate.txt"); +``` + +Kotlin + +``` +val ctx: FileSystemXmlApplicationContext = ... +ctx.getResource("some/resource/path/myTemplate.txt") +``` + +Java + +``` +FileSystemXmlApplicationContext ctx = ...; +ctx.getResource("/some/resource/path/myTemplate.txt"); +``` + +Kotlin + +``` +val ctx: FileSystemXmlApplicationContext = ... +ctx.getResource("/some/resource/path/myTemplate.txt") +``` + +在实践中,如果需要真正的绝对文件系统路径,则应避免使用带有`FileSystemResource`或`FileSystemXmlApplicationContext`的绝对路径,并使用`UrlResource`的 URL 前缀强制使用`file:`。下面的例子说明了如何做到这一点: + +Java + +``` +// actual context type doesn't matter, the Resource will always be UrlResource +ctx.getResource("file:///some/resource/path/myTemplate.txt"); +``` + +Kotlin + +``` +// actual context type doesn't matter, the Resource will always be UrlResource +ctx.getResource("file:///some/resource/path/myTemplate.txt") +``` + +Java + +``` +// force this FileSystemXmlApplicationContext to load its definition via a UrlResource +ApplicationContext ctx = + new FileSystemXmlApplicationContext("file:///conf/context.xml"); +``` + +Kotlin + +``` +// force this FileSystemXmlApplicationContext to load its definition via a UrlResource +val ctx = FileSystemXmlApplicationContext("file:///conf/context.xml") +``` + +## 3. 验证、数据绑定和类型转换 # + +将验证视为业务逻辑有其优点和缺点, Spring 提供了一种验证(和数据绑定)设计,该设计不排除其中的任何一种。具体地说,验证不应该绑定到 Web 层,并且应该易于本地化,并且应该可以插入任何可用的验证器。考虑到这些问题, Spring 提供了`Validator`契约,该契约在应用程序的每一层中都是基本的且非常可用的。 + +数据绑定对于让用户输入与应用程序的域模型(或用于处理用户输入的任何对象)动态绑定非常有用。 Spring 提供了恰如其分的名称`DataBinder`来做到这一点。`Validator`和 `databinder’组成了`validation`包,该包主要用于但不限于 Web 层。 + +`BeanWrapper`是 Spring 框架中的一个基本概念,并在许多地方使用。但是,你可能不需要直接使用`BeanWrapper`。但是,由于这是参考文档,我们认为可能需要进行一些解释。我们将在本章中解释`BeanWrapper`,因为如果你打算使用它,那么在尝试将数据绑定到对象时,你很可能会使用它。 + +Spring 的`DataBinder`和较低级别的`BeanWrapper`都使用`PropertyEditorSupport`实现来解析和格式化属性值。`PropertyEditor`和 `PropertYeditorSupport’类型是 JavaBeans 规范的一部分,本章也对此进行了说明。 Spring 3 引入了一个`core.convert`包,该包提供了一般的类型转换功能,以及用于格式化 UI 字段值的更高级别的“格式”包。你可以使用这些包作为“propertyeditorsupport”实现的更简单的替代方案。本章还对这些问题进行了讨论。 + +Spring 通过设置基础设施和 Spring 自己的`Validator`合同的适配器支持 Java Bean 验证。应用程序可以全局启用 Bean 验证一次,如[Java Bean Validation](#validation-beanvalidation)中所述,并专门用于所有验证需求。在 Web 层中,应用程序还可以根据`DataBinder`注册控制器-Local Spring `Validator’实例,如[Configuring a `DataBinder`](#validation-binder)中所述,这对于插入自定义验证逻辑是有用的。 + +### 3.1.通过使用 Spring 的验证器接口进行验证 + +Spring 具有`Validator`接口,可以使用该接口来验证对象。“validator”接口通过使用`Errors`对象来工作,这样在验证时,验证器可以向`Errors`对象报告验证失败。 + +考虑以下一个小数据对象的示例: + +Java + +``` +public class Person { + + private String name; + private int age; + + // the usual getters and setters... +} +``` + +Kotlin + +``` +class Person(val name: String, val age: Int) +``` + +下一个示例通过实现`org.springframework.validation.Validator`接口的以下两个方法,为`Person`类提供了验证行为: + +* `supports(Class)`:这个`Validator`可以验证所提供的`Class`实例吗? + +* `validate(Object, org.springframework.validation.Errors)`:验证给定的对象,在验证错误的情况下,用给定的`Errors`对象注册那些对象。 + +实现`Validator`相当简单,特别是当你知道 Spring 框架还提供的 `Validationutils’助手类时。下面的示例为`Person`实例实现`Validator`: + +Java + +``` +public class PersonValidator implements Validator { + + /** + * This Validator validates only Person instances + */ + public boolean supports(Class clazz) { + return Person.class.equals(clazz); + } + + public void validate(Object obj, Errors e) { + ValidationUtils.rejectIfEmpty(e, "name", "name.empty"); + Person p = (Person) obj; + if (p.getAge() < 0) { + e.rejectValue("age", "negativevalue"); + } else if (p.getAge() > 110) { + e.rejectValue("age", "too.darn.old"); + } + } +} +``` + +Kotlin + +``` +class PersonValidator : Validator { + + /** + * This Validator validates only Person instances + */ + override fun supports(clazz: Class<*>): Boolean { + return Person::class.java == clazz + } + + override fun validate(obj: Any, e: Errors) { + ValidationUtils.rejectIfEmpty(e, "name", "name.empty") + val p = obj as Person + if (p.age < 0) { + e.rejectValue("age", "negativevalue") + } else if (p.age > 110) { + e.rejectValue("age", "too.darn.old") + } + } +} +``` + +在`ValidationUtils`类上的`static``rejectIfEmpty(..)`方法用于拒绝`name`属性,如果它是`null`或空字符串。看看[“验证”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/validation/ValidationUtils.html)Javadoc,看看除了前面显示的示例之外,它还提供了什么功能。 + +虽然可以实现单个`Validator`类来验证富对象中的每个嵌套对象,但最好是将每个嵌套对象类的验证逻辑封装在其自己的`Validator`实现中。“rich”对象的一个简单示例是`Customer`,它由两个`String`属性(第一个和第二个名称)和一个复杂的`Address`对象组成。`Address`对象可以独立于`Customer`对象使用,因此已经实现了一个不同的`AddressValidator`对象。如果你希望你的`CustomerValidator`重用`AddressValidator`类中包含的逻辑而不使用复制粘贴,则可以在你的`CustomerValidator`中使用依赖注入或实例化`AddressValidator`,如下例所示: + +Java + +``` +public class CustomerValidator implements Validator { + + private final Validator addressValidator; + + public CustomerValidator(Validator addressValidator) { + if (addressValidator == null) { + throw new IllegalArgumentException("The supplied [Validator] is " + + "required and must not be null."); + } + if (!addressValidator.supports(Address.class)) { + throw new IllegalArgumentException("The supplied [Validator] must " + + "support the validation of [Address] instances."); + } + this.addressValidator = addressValidator; + } + + /** + * This Validator validates Customer instances, and any subclasses of Customer too + */ + public boolean supports(Class clazz) { + return Customer.class.isAssignableFrom(clazz); + } + + public void validate(Object target, Errors errors) { + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required"); + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required"); + Customer customer = (Customer) target; + try { + errors.pushNestedPath("address"); + ValidationUtils.invokeValidator(this.addressValidator, customer.getAddress(), errors); + } finally { + errors.popNestedPath(); + } + } +} +``` + +Kotlin + +``` +class CustomerValidator(private val addressValidator: Validator) : Validator { + + init { + if (addressValidator == null) { + throw IllegalArgumentException("The supplied [Validator] is required and must not be null.") + } + if (!addressValidator.supports(Address::class.java)) { + throw IllegalArgumentException("The supplied [Validator] must support the validation of [Address] instances.") + } + } + + /* + * This Validator validates Customer instances, and any subclasses of Customer too + */ + override fun supports(clazz: Class<>): Boolean { + return Customer::class.java.isAssignableFrom(clazz) + } + + override fun validate(target: Any, errors: Errors) { + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required") + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required") + val customer = target as Customer + try { + errors.pushNestedPath("address") + ValidationUtils.invokeValidator(this.addressValidator, customer.address, errors) + } finally { + errors.popNestedPath() + } + } +} +``` + +将验证错误报告给传递给验证器的`Errors`对象。在 Spring Web MVC 的情况下,你可以使用``标记来检查错误消息,但是你也可以自己检查`Errors`对象。有关其提供的方法的更多信息,请参见[javadoc](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/validation/Errors.html)。 + +### 3.2.将代码解析为错误消息 + +我们涵盖了数据库和验证。本节介绍输出与验证错误对应的消息。在[前一节](#validator)中显示的示例中,我们拒绝了`name`和`age`字段。如果我们希望通过使用“MessageSource”输出错误消息,那么我们可以使用在拒绝字段时提供的错误代码(本例中是“name”和“age”)来输出错误消息。当你从`Errors`接口调用(通过使用`ValidationUtils`类等直接或间接调用)`rejectValue`或其他`reject`方法之一时,底层实现不仅注册了你传入的代码,而且还注册了许多额外的错误代码。`MessageCodesResolver`决定了哪个错误编码`Errors`接口寄存器。默认情况下,使用“DefaultMessageCodesResolver”,它(例如)不仅用给出的代码注册消息,还注册包含传递给拒绝方法的字段名称的消息。因此,如果通过使用`rejectValue("age", "too.darn.old")`拒绝字段,除了`too.darn.old`代码外, Spring 还会注册`too.darn.old.age`和 `too.darn.old.age.INT’(第一个包括字段名称,第二个包括字段的类型)。这样做是为了方便开发人员在定位错误消息时提供帮助。 + +有关`MessageCodesResolver`和默认策略的更多信息,可以分别在[“MessageCodesResolver”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/validation/MessageCodesResolver.html)和[“DefaultMessageCodesResolver”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/validation/DefaultMessageCodesResolver.html)的 Javadoc 中找到。 + +### 3.3. Bean 操纵和`BeanWrapper` + +`org.springframework.beans`包遵循 JavaBeans 标准。JavaBean 是一个具有缺省无参数构造函数的类,它遵循一个命名约定,其中(例如)一个名为`bingoMadness`的属性将具有一个 setter 方法`setBingoMadness(..)`和一个 getter 方法`getBingoMadness()`。有关 JavaBeans 和规范的更多信息,请参见[javabeans](https://docs.oracle.com/javase/8/docs/api/java/beans/package-summary.html)。 + +Bean 包中一个非常重要的类是`BeanWrapper`接口及其相应的实现。正如从 Javadoc 中引用的,“BeanWrapper”提供了设置和获取属性值(单独或批量)、获取属性描述符和查询属性以确定它们是否可读或可写的功能。此外,`BeanWrapper`提供了对嵌套属性的支持,使子属性上的属性的设置具有无限的深度。“BeanWrapper”还支持添加标准 JavaBeans`PropertyChangeListeners`和`VetoableChangeListeners`的功能,而不需要在目标类中支持代码。最后但并非最不重要的是,`BeanWrapper`提供了对设置索引属性的支持。`BeanWrapper`通常不被应用程序代码直接使用,而是由 `databinder’和`BeanFactory`使用。 + +`BeanWrapper`的工作方式在一定程度上由其名称表示:它封装了一个 Bean 以在该 Bean 上执行操作,例如设置和检索属性。 + +#### 3.3.1.设置和获取基本和嵌套属性 + +设置和获取属性是通过`setPropertyValue`和的重载方法变量`BeanWrapper`完成的。有关详细信息,请访问他们的 Javadoc。下表显示了这些约定的一些示例: + +| Expression |解释| +|----------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `name` |指示与`name`或`isName()`和`setName(..)`方法对应的属性`name`。| +| `account.name` |指示属性`account`的嵌套属性`name`,它对应于
(例如)`getAccount().setName()`或`getAccount().getName()`方法。| +| `account[2]` |指示索引属性`account`的*第三次*元素。索引属性
可以是类型`array`,`list`,或其他自然有序的集合。| +|`account[COMPANYNAME]`|指示由`account``Map`属性的`COMPANYNAME`键索引的映射项的值。| + +(如果你不打算直接使用`BeanWrapper`,那么下一节对你来说并不是至关重要的。如果只使用`DataBinder`和`BeanFactory`及其默认实现,则应跳过[section on `PropertyEditors`](#beans-beans-conversion)。) + +以下两个示例类使用`BeanWrapper`来获取和设置属性: + +Java + +``` +public class Company { + + private String name; + private Employee managingDirector; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public Employee getManagingDirector() { + return this.managingDirector; + } + + public void setManagingDirector(Employee managingDirector) { + this.managingDirector = managingDirector; + } +} +``` + +Kotlin + +``` +class Company { + var name: String? = null + var managingDirector: Employee? = null +} +``` + +Java + +``` +public class Employee { + + private String name; + + private float salary; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public float getSalary() { + return salary; + } + + public void setSalary(float salary) { + this.salary = salary; + } +} +``` + +Kotlin + +``` +class Employee { + var name: String? = null + var salary: Float? = null +} +``` + +以下代码片段展示了如何检索和操作实例化`Company`s 和`Employee`s 的一些属性的示例: + +Java + +``` +BeanWrapper company = new BeanWrapperImpl(new Company()); +// setting the company name.. +company.setPropertyValue("name", "Some Company Inc."); +// ... can also be done like this: +PropertyValue value = new PropertyValue("name", "Some Company Inc."); +company.setPropertyValue(value); + +// ok, let's create the director and tie it to the company: +BeanWrapper jim = new BeanWrapperImpl(new Employee()); +jim.setPropertyValue("name", "Jim Stravinsky"); +company.setPropertyValue("managingDirector", jim.getWrappedInstance()); + +// retrieving the salary of the managingDirector through the company +Float salary = (Float) company.getPropertyValue("managingDirector.salary"); +``` + +Kotlin + +``` +val company = BeanWrapperImpl(Company()) +// setting the company name.. +company.setPropertyValue("name", "Some Company Inc.") +// ... can also be done like this: +val value = PropertyValue("name", "Some Company Inc.") +company.setPropertyValue(value) + +// ok, let's create the director and tie it to the company: +val jim = BeanWrapperImpl(Employee()) +jim.setPropertyValue("name", "Jim Stravinsky") +company.setPropertyValue("managingDirector", jim.wrappedInstance) + +// retrieving the salary of the managingDirector through the company +val salary = company.getPropertyValue("managingDirector.salary") as Float? +``` + +#### 3.3.2.内置`PropertyEditor`实现 + +Spring 使用`PropertyEditor`的概念来实现 ` 对象’与`String`之间的转换。以不同于对象本身的方式表示属性可能很方便。例如,`Date`可以以人类可读的方式表示(如`String`:`'2007-14-09'`),而我们仍然可以将人类可读的表单转换回原始日期(或者,更好的是,将在人类可读表单中输入的任何日期转换回`Date`对象)。这种行为可以通过注册“java.beans.propertyeditor”类型的自定义编辑器来实现。在`BeanWrapper`上注册自定义编辑器,或者在特定的 IoC 容器中注册自定义编辑器(如前一章中提到的),可以让它了解如何将属性转换为所需的类型。有关“PropertyEditor”的更多信息,请参见[the javadoc of the `java.beans` package from Oracle](https://docs.oracle.com/javase/8/docs/api/java/beans/package-summary.html)。 + +Spring 中使用属性编辑的几个示例: + +* 在 bean 上设置属性是通过使用`PropertyEditor`实现来完成的。当使用`String`作为在 XML 文件中声明的某个 Bean 的属性的值时, Spring(如果相应属性的 setter 具有`Class`参数)使用`ClassEditor`来尝试将参数解析为`Class`对象。 + +* 在 Spring 的 MVC 框架中解析 HTTP 请求参数是通过使用各种`PropertyEditor`实现完成的,你可以在 `CommandController’的所有子类中手动绑定这些实现。 + +Spring 具有许多内置`PropertyEditor`的实现方式,以使生活变得容易。它们都位于`org.springframework.beans.propertyeditors`包中。默认情况下,大多数(但不是所有,如下表所示)是由“BeanWrapperImpl”注册的。在属性编辑器以某种方式可配置的情况下,你仍然可以注册自己的变体以覆盖默认的变体。下表描述了 Spring 提供的各种`PropertyEditor`实现: + +| Class |解释| +|-------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|`ByteArrayPropertyEditor`|字节数组的编辑器。将字符串转换为其对应的字节
表示形式。默认情况下由`BeanWrapperImpl`注册。| +| `ClassEditor` |解析将类表示为实际类的字符串,反之亦然。当未找到
类时,将抛出`IllegalArgumentException`。默认情况下,由“BeanWrapperImpl”注册。| +| `CustomBooleanEditor` |`Boolean`属性的可自定义属性编辑器。默认情况下,由“BeanWrapperImpl”注册,但可以通过将其自定义实例注册为
自定义编辑器来重写。| +|`CustomCollectionEditor` |集合的属性编辑器,将任何源`Collection`转换为给定的目标 `collection’类型。| +| `CustomDateEditor` |用于`java.util.Date`的可自定义属性编辑器,支持自定义`DateFormat`。不
默认注册。必须根据需要以适当的格式进行用户注册。| +| `CustomNumberEditor` |用于任何`Number`子类的可自定义属性编辑器,例如`Integer`,`Long`,`Float`,或 `double’。默认情况下,由`BeanWrapperImpl`注册,但可以由
将其自定义实例注册为自定义编辑器来覆盖。| +| `FileEditor` |将字符串解析为`java.io.File`对象。默认情况下,由“BeanWrapperImpl”注册。| +| `InputStreamEditor` |一种单向属性编辑器,它可以获取一个字符串并产生(通过
中间值`ResourceEditor`和`Resource`)一个`InputStream`,这样`InputStream`属性就可以直接设置为字符串。请注意,默认用法不会为你关闭
的`InputStream`。默认情况下,由`BeanWrapperImpl`注册。| +| `LocaleEditor` |可以将字符串解析为`Locale`对象,反之亦然(字符串格式为 `[language]_[country]_[variant]`,与 `locale’的`toString()`方法相同)。也接受空格作为分隔符,作为下划线的替代。
默认情况下,由`BeanWrapperImpl`注册。| +| `PatternEditor` |可以将字符串解析为`java.util.regex.Pattern`对象,反之亦然。| +| `PropertiesEditor` |可以将字符串(格式为 `java.util.properties’类的 Javadoc 中定义的格式)转换为`Properties`对象。默认情况下,由`BeanWrapperImpl`注册
。| +| `StringTrimmerEditor` |编辑字符串的属性编辑器。可选地允许将空字符串
转换为`null`值。默认情况下未注册——必须是用户注册的。| +| `URLEditor` |可以将 URL 的字符串表示解析为实际的`URL`对象。
默认情况下,由`BeanWrapperImpl`注册。| + +Spring 使用`java.beans.PropertyEditorManager`设置可能需要的属性编辑器的搜索路径。搜索路径还包括`sun.bean.editors`,其中包括`PropertyEditor`类型的实现,例如`Font`,`Color`,以及大多数原始类型。还请注意,标准 JavaBeans 基础设施会自动发现`PropertyEditor`类(无需显式地注册它们),如果它们与它们处理的类在同一个包中,并且与该类具有相同的名称,并附加`Editor`。例如,可以有以下的类和包结构,这将足以使`SomethingEditor`类被识别并用作`PropertyEditor`类型属性的`Something`。 + +``` +com + chank + pop + Something + SomethingEditor // the PropertyEditor for the Something class +``` + +注意,这里也可以使用标准的`BeanInfo`JavaBeans 机制(在一定程度上描述了[here](https://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html))。下面的示例使用`BeanInfo`机制显式地用关联类的属性注册一个或多个 `PropertyEditor’实例: + +``` +com + chank + pop + Something + SomethingBeanInfo // the BeanInfo for the Something class +``` + +下面引用的`SomethingBeanInfo`类的 Java 源代码将`CustomNumberEditor`与`age`类的`age`属性关联起来: + +Java + +``` +public class SomethingBeanInfo extends SimpleBeanInfo { + + public PropertyDescriptor[] getPropertyDescriptors() { + try { + final PropertyEditor numberPE = new CustomNumberEditor(Integer.class, true); + PropertyDescriptor ageDescriptor = new PropertyDescriptor("age", Something.class) { + @Override + public PropertyEditor createPropertyEditor(Object bean) { + return numberPE; + } + }; + return new PropertyDescriptor[] { ageDescriptor }; + } + catch (IntrospectionException ex) { + throw new Error(ex.toString()); + } + } +} +``` + +Kotlin + +``` +class SomethingBeanInfo : SimpleBeanInfo() { + + override fun getPropertyDescriptors(): Array { + try { + val numberPE = CustomNumberEditor(Int::class.java, true) + val ageDescriptor = object : PropertyDescriptor("age", Something::class.java) { + override fun createPropertyEditor(bean: Any): PropertyEditor { + return numberPE + } + } + return arrayOf(ageDescriptor) + } catch (ex: IntrospectionException) { + throw Error(ex.toString()) + } + + } +} +``` + +##### 注册额外的自定义`PropertyEditor`实现 ##### + +当将 Bean 属性设置为字符串值时, Spring IOC 容器最终使用标准 JavaBeans`PropertyEditor`实现将这些字符串转换为属性的复杂类型。 Spring 预先寄存器一些自定义的`PropertyEditor`实现(例如,将表示为字符串的类名转换为`Class`对象)。此外,Java 的标准 JavaBeans`PropertyEditor`查找机制允许对一个类的`PropertyEditor`进行适当的命名,并将其放置在与它所支持的类相同的包中,以便可以自动找到它。 + +如果需要注册其他自定义`PropertyEditors`,可以使用几种机制。通常不方便或不推荐的最手动的方法是使用“configurableBeanFactory”接口的方法,假设你有引用。另一种(稍微更方便的)机制是使用一种特殊的 Bean 工厂后处理器,称为`CustomEditorConfigurer`。尽管你可以使用带有`BeanFactory`实现的 Bean 工厂后处理器,但`CustomEditorConfigurer`具有嵌套的属性设置,因此我们强烈建议你将其用于 `ApplicationContext’,在这里你可以以类似的方式将其部署到任何其他 Bean 实现,并且可以自动检测和应用它。 + +注意,所有 Bean 工厂和应用程序上下文通过使用`BeanWrapper`自动使用许多内置的属性编辑器来处理属性转换。在[上一节](#beans-beans-conversion)中列出了`BeanWrapper`寄存器的标准属性编辑器。此外,`ApplicationContext`s 还覆盖或添加额外的编辑器,以便以适合特定应用程序上下文类型的方式处理资源查找。 + +标准 JavaBeans`PropertyEditor`实例用于将以字符串表示的属性值转换为属性的实际复杂类型。你可以使用 Bean 工厂后处理程序“CustomEditorConfigurer”方便地向`PropertyEditor`实例添加对额外`ApplicationContext`实例的支持。 + +考虑以下示例,它定义了一个名为`ExoticType`的用户类和另一个名为`DependsOnExoticType`的类,它需要`ExoticType`设置为一个属性: + +Java + +``` +package example; + +public class ExoticType { + + private String name; + + public ExoticType(String name) { + this.name = name; + } +} + +public class DependsOnExoticType { + + private ExoticType type; + + public void setType(ExoticType type) { + this.type = type; + } +} +``` + +Kotlin + +``` +package example + +class ExoticType(val name: String) + +class DependsOnExoticType { + + var type: ExoticType? = null +} +``` + +在正确设置之后,我们希望能够将 type 属性分配为字符串,然后`PropertyEditor`将其转换为实际的 `exoticType’实例。 Bean 以下定义显示了如何设置这种关系: + +``` + + + +``` + +`PropertyEditor`实现可能看起来类似于以下内容: + +Java + +``` +// converts string representation to ExoticType object +package example; + +public class ExoticTypeEditor extends PropertyEditorSupport { + + public void setAsText(String text) { + setValue(new ExoticType(text.toUpperCase())); + } +} +``` + +Kotlin + +``` +// converts string representation to ExoticType object +package example + +import java.beans.PropertyEditorSupport + +class ExoticTypeEditor : PropertyEditorSupport() { + + override fun setAsText(text: String) { + value = ExoticType(text.toUpperCase()) + } +} +``` + +最后,下面的示例展示了如何使用`CustomEditorConfigurer`将新的`PropertyEditor`注册到 `ApplicationContext’中,然后可以根据需要使用它: + +``` + + + + + + + +``` + +###### 使用`PropertyEditorRegistrar` + +用 Spring 容器注册属性编辑器的另一种机制是创建和使用`PropertyEditorRegistrar`。当你需要在几种不同的情况下使用同一组属性编辑器时,此接口特别有用。你可以编写一个相应的注册表,并在每种情况下重新使用它。`PropertYeditorRegistry’实例与一个名为 `PropertYeditorRegistry’的接口一起工作,该接口由 Spring (和)实现。`PropertyEditorRegistrar`实例在与`CustomEditorConfigurer`(描述为[here](#beans-beans-conversion-customeditor-registration))结合使用时特别方便,它公开了一个名为`setPropertyEditorRegistrars(..)`的属性。`PropertyEditorRegistrar`以这种方式添加到`CustomEditorConfigurer`的实例可以很容易地与`DataBinder`和 Spring MVC 控制器共享。此外,它避免了在自定义编辑器上进行同步的需要:一个`PropertyEditorRegistrar`被期望为每个 Bean 创建尝试创建新的`PropertyEditor`实例。 + +下面的示例展示了如何创建你自己的`PropertyEditorRegistrar`实现: + +Java + +``` +package com.foo.editors.spring; + +public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar { + + public void registerCustomEditors(PropertyEditorRegistry registry) { + + // it is expected that new PropertyEditor instances are created + registry.registerCustomEditor(ExoticType.class, new ExoticTypeEditor()); + + // you could register as many custom property editors as are required here... + } +} +``` + +Kotlin + +``` +package com.foo.editors.spring + +import org.springframework.beans.PropertyEditorRegistrar +import org.springframework.beans.PropertyEditorRegistry + +class CustomPropertyEditorRegistrar : PropertyEditorRegistrar { + + override fun registerCustomEditors(registry: PropertyEditorRegistry) { + + // it is expected that new PropertyEditor instances are created + registry.registerCustomEditor(ExoticType::class.java, ExoticTypeEditor()) + + // you could register as many custom property editors as are required here... + } +} +``` + +另请参见`org.springframework.beans.support.ResourceEditorRegistrar`中的示例 `PropertyDitorRegistrar’实现。请注意,在“RegisterCustomEditors”方法的实现中,它如何为每个属性编辑器创建新的实例。 + +下一个示例展示了如何配置`CustomEditorConfigurer`,并将我们的 `CustomPropertyDitorRegistrarer’实例注入其中: + +``` + + + + + + + + + +``` + +最后(对于那些使用[Spring’s MVC web framework](web.html#mvc)的人来说,有点偏离本章的重点),使用`PropertyEditorRegistrars`结合数据绑定`Controllers`(例如`SimpleFormController`)可以非常方便。下面的示例在`initBinder(..)`方法的实现中使用`PropertyEditorRegistrar`: + +Java + +``` +public final class RegisterUserController extends SimpleFormController { + + private final PropertyEditorRegistrar customPropertyEditorRegistrar; + + public RegisterUserController(PropertyEditorRegistrar propertyEditorRegistrar) { + this.customPropertyEditorRegistrar = propertyEditorRegistrar; + } + + protected void initBinder(HttpServletRequest request, + ServletRequestDataBinder binder) throws Exception { + this.customPropertyEditorRegistrar.registerCustomEditors(binder); + } + + // other methods to do with registering a User +} +``` + +Kotlin + +``` +class RegisterUserController( + private val customPropertyEditorRegistrar: PropertyEditorRegistrar) : SimpleFormController() { + + protected fun initBinder(request: HttpServletRequest, + binder: ServletRequestDataBinder) { + this.customPropertyEditorRegistrar.registerCustomEditors(binder) + } + + // other methods to do with registering a User +} +``` + +这种`PropertyEditor`的注册风格可以导致简洁的代码(`initBinder(..)`的实现只有一行长),并让普通的`PropertyEditor`注册代码封装在一个类中,然后根据需要在多个 `controllers’之间共享。 + +### 3.4. Spring 类型转换 + +Spring 3 介绍了一种`core.convert`包,其提供了一种通用的类型转换系统。系统定义了实现类型转换逻辑的 SPI 和在运行时执行类型转换的 API。在 Spring 容器中,可以使用该系统作为`PropertyEditor`实现的替代方案,以将外部化的 Bean 属性值字符串转换为所需的属性类型。你还可以在应用程序中需要类型转换的任何地方使用公共 API。 + +#### 3.4.1.转换器 SPI + +实现类型转换逻辑的 SPI 是简单且强类型的,如下接口定义所示: + +``` +package org.springframework.core.convert.converter; + +public interface Converter { + + T convert(S source); +} +``` + +要创建自己的转换器,请实现`Converter`接口,并将`S`参数化为要转换的类型,将`T`参数化为要转换的类型。如果需要将`S`的集合或数组转换为`T`的阵列或集合,那么也可以透明地应用这样的转换器,前提是已经注册了一个委托数组或集合转换器(默认情况下`DefaultConversionService`是这样做的)。 + +对于每个对`convert(S)`的调用,源参数保证不为空。如果转换失败,你的“转换器”可能会抛出任何未经检查的异常。具体地说,它应该抛出“IllegalArgumentException”来报告无效的源值。注意确保你的`Converter`实现是线程安全的。 + +为了方便起见,在`core.convert.support`包中提供了几种转换器实现方式。这些包括从字符串到数字的转换器和其他常见类型的转换器。下面的清单显示了`StringToInteger`类,这是一个典型的`Converter`实现: + +``` +package org.springframework.core.convert.support; + +final class StringToInteger implements Converter { + + public Integer convert(String source) { + return Integer.valueOf(source); + } +} +``` + +#### 3.4.2.使用`ConverterFactory` + +当需要对整个类层次结构集中转换逻辑时(例如,从`String`转换为`Enum`对象时),可以实现 `converterfactory’,如下例所示: + +``` +package org.springframework.core.convert.converter; + +public interface ConverterFactory { + + Converter getConverter(Class targetType); +} +``` + +参数化 S 为你要转换的类型,R 为定义你可以转换为的类的的基本类型。然后实现`getConverter(Class)`,其中 t 是 r 的一个子类。 + +以`StringToEnumConverterFactory`为例: + +``` +package org.springframework.core.convert.support; + +final class StringToEnumConverterFactory implements ConverterFactory { + + public Converter getConverter(Class targetType) { + return new StringToEnumConverter(targetType); + } + + private final class StringToEnumConverter implements Converter { + + private Class enumType; + + public StringToEnumConverter(Class enumType) { + this.enumType = enumType; + } + + public T convert(String source) { + return (T) Enum.valueOf(this.enumType, source.trim()); + } + } +} +``` + +#### 3.4.3.使用`GenericConverter` + +当你需要复杂的`Converter`实现时,请考虑使用“GenericConverter”接口。使用比`Converter`更灵活但不那么强类型的签名,`GenericConverter`支持在多个源类型和目标类型之间进行转换。此外,`GenericConverter`提供了在实现转换逻辑时可以使用的源和目标字段上下文。这样的上下文允许类型转换由字段注释或在字段签名上声明的通用信息驱动。下面的清单显示了`GenericConverter`的接口定义: + +``` +package org.springframework.core.convert.converter; + +public interface GenericConverter { + + public Set getConvertibleTypes(); + + Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType); +} +``` + +要实现`GenericConverter`,请让`getConvertibleTypes()`返回受支持的源目标类型对。然后实现`convert(Object, TypeDescriptor, TypeDescriptor)`来包含你的转换逻辑。源`TypeDescriptor`提供对保存被转换的值的源字段的访问。目标`TypeDescriptor`提供对要设置转换值的目标字段的访问。 + +`GenericConverter`的一个很好的例子是在 Java 数组和集合之间转换的转换器。这样的`ArrayToCollectionConverter`内省声明目标集合类型的字段,以解析集合的元素类型。这允许在目标字段上设置集合之前,将源数组中的每个元素转换为集合元素类型。 + +| |因为`GenericConverter`是一个更复杂的 SPI 接口,所以只有在需要时才应该使用
。青睐`Converter`或`ConverterFactory`对于基本类型
转换需要。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 使用`ConditionalGenericConverter` + +有时,只有当特定条件为真时,你才希望`Converter`运行。例如,只有在目标字段上存在特定的注释时,你才可能希望运行`Converter`,或者,只有在目标类上定义了特定的方法(例如`static valueOf`方法)时,才希望运行`Converter`。 + +``` +public interface ConditionalConverter { + + boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType); +} + +public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter { +} +``` + +`ConditionalGenericConverter`的一个很好的例子是`IdToEntityConverter`,它在持久实体标识符和实体引用之间转换。这样的`IdToEntityConverter`可能只有在目标实体类型声明了静态查找方法(例如,`FindAccount(long)`)时才匹配。你可以在“Matches”(typedescriptor,typedescriptor)的实现中执行这样的查找方法检查。 + +#### 3.4.4.`ConversionService`api + +`ConversionService`定义了用于在运行时执行类型转换逻辑的统一 API。转换器通常在以下 facade 接口后面运行: + +``` +package org.springframework.core.convert; + +public interface ConversionService { + + boolean canConvert(Class sourceType, Class targetType); + + T convert(Object source, Class targetType); + + boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType); + + Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType); +} +``` + +大多数`ConversionService`实现还实现了`ConverterRegistry`,它提供了用于注册转换器的 SPI。在内部,`ConversionService`实现委托给其注册的转换器来执行类型转换逻辑。 + +在`core.convert.support`包中提供了一个健壮的`ConversionService`实现。`GenericConversionService`是适用于大多数环境的通用实现。`ConversionServiceFactory`为创建常见的`ConversionService`配置提供了一个方便的工厂。 + +#### 3.4.5.配置`ConversionService` + +`ConversionService`是一种无状态对象,设计用于在应用程序启动时实例化,然后在多个线程之间共享。在 Spring 应用程序中,通常为每个 Spring 容器(或`ApplicationContext`)配置一个`ConversionService`实例。 Spring 获取`ConversionService`并在框架需要执行类型转换时使用它。你还可以将这个“ConversionService”注入到你的任何 bean 中,并直接调用它。 + +| |如果没有`ConversionService`被注册到 Spring,则使用原来的基于`PropertyEditor`的
系统。| +|---|------------------------------------------------------------------------------------------------------------| + +要用 Spring 注册一个默认的`ConversionService`,请添加以下 Bean 定义,并使用`id`的`conversionService`: + +``` + +``` + +默认的`ConversionService`可以在字符串、数字、枚举、集合、映射和其他常见类型之间进行转换。要用你自己的定制转换器补充或覆盖默认转换器,请设置`converters`属性。属性值可以实现`Converter`、`ConverterFactory`或`GenericConverter`接口中的任意一个。 + +``` + + + + + + + +``` + +在 Spring MVC 应用程序中使用`ConversionService`也是很常见的。参见 Spring MVC 章节中的[转换和格式化](web.html#mvc-config-conversion)。 + +在某些情况下,你可能希望在转换过程中应用格式化。有关使用`FormattingConversionServiceFactoryBean`的详细信息,请参见[The `FormatterRegistry` SPI](#format-FormatterRegistry-SPI)。 + +#### 3.4.6.以编程方式使用`ConversionService` + +要以编程方式处理`ConversionService`实例,你可以像处理任何其他实例一样,对它注入一个引用 Bean。下面的示例展示了如何做到这一点: + +Java + +``` +@Service +public class MyService { + + public MyService(ConversionService conversionService) { + this.conversionService = conversionService; + } + + public void doIt() { + this.conversionService.convert(...) + } +} +``` + +Kotlin + +``` +@Service +class MyService(private val conversionService: ConversionService) { + + fun doIt() { + conversionService.convert(...) + } +} +``` + +对于大多数用例,你可以使用`convert`方法来指定`targetType`,但是它不适用于更复杂的类型,例如参数化元素的集合。例如,如果你想要将`List`的`Integer`转换为`List`的`List`的`String`,则需要提供源类型和目标类型的正式定义。 + +幸运的是,`TypeDescriptor`提供了各种选项,以使这样做很简单,如下例所示: + +Java + +``` +DefaultConversionService cs = new DefaultConversionService(); + +List input = ... +cs.convert(input, + TypeDescriptor.forObject(input), // List type descriptor + TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class))); +``` + +Kotlin + +``` +val cs = DefaultConversionService() + +val input: List = ... +cs.convert(input, + TypeDescriptor.forObject(input), // List type descriptor + TypeDescriptor.collection(List::class.java, TypeDescriptor.valueOf(String::class.java))) +``` + +请注意,`DefaultConversionService`会自动注册适合于大多数环境的转换器。这包括集合转换器、标量转换器和基本`Object`-到 ` 字符串’转换器。通过在`DefaultConversionService`类上使用静态`addDefaultConverters`方法,可以用任何`ConverterRegistry`注册相同的转换器。 + +值类型的转换器可重用于数组和集合,因此不需要创建特定的转换器来将`Collection`的`S`转换为`T`的 `collection’的`T`,假设标准的集合处理是适当的。 + +### 3.5. Spring 字段格式 + +如上一节所讨论的,[`core.convert`](#core-convert)是一种通用的类型转换系统。它提供了一个统一的`ConversionService`API 以及一个强类型的`Converter`SPI,用于实现从一种类型到另一种类型的转换逻辑。 Spring 容器使用此系统绑定 Bean 属性值。此外, Spring 表达式语言和`DataBinder`都使用此系统绑定字段值。例如,当 SPEL 需要强制`Short`到`Long`以完成`expression.setValue(Object bean, Object value)`尝试时,`core.convert`系统执行强制。 + +现在考虑典型的客户机环境(例如 Web 或桌面应用程序)的类型转换需求。在这样的环境中,通常从`String`转换为支持客户端回发过程,以及返回`String`以支持视图呈现过程。此外,你经常需要本地化`String`值。更通用的`core.convert``Converter`SPI 并不直接解决此类格式要求。为了直接解决这些问题, Spring 3 引入了一个方便的`Formatter`SPI,它为客户机环境提供了`PropertyEditor`实现的简单而健壮的替代方案。 + +通常,当需要实现通用类型转换逻辑时,可以使用`Converter`SPI——例如,用于在`java.util.Date`和`Long`之间进行转换。当你在客户机环境(例如 Web 应用程序)中工作并且需要解析和打印本地化字段值时,可以使用`Formatter`SPI。`ConversionService`为两个 SPI 提供了统一的类型转换 API。 + +#### 3.5.1.`Formatter`SPI + +实现字段格式逻辑的`Formatter`SPI 是简单且强类型的。下面的清单显示了`Formatter`接口定义: + +``` +package org.springframework.format; + +public interface Formatter extends Printer, Parser { +} +``` + +`Formatter`扩展自`Printer`和`Parser`构建块接口。下面的清单显示了这两个接口的定义: + +``` +public interface Printer { + + String print(T fieldValue, Locale locale); +} +``` + +``` +import java.text.ParseException; + +public interface Parser { + + T parse(String clientValue, Locale locale) throws ParseException; +} +``` + +要创建自己的`Formatter`,请实现前面显示的`Formatter`接口。将`T`参数化为你希望格式化的对象类型——例如,“java.util.date”。实现`print()`操作来打印`T`的实例,以便在客户机语言环境中显示。实现`parse()`操作,从客户机语言环境返回的格式化表示中解析 `t’的实例。如果解析尝试失败,你的`Formatter`应该抛出`ParseException`或`IllegalArgumentException`。注意确保你的`Formatter`实现是线程安全的。 + +`format`子包提供了几个`Formatter`实现作为一种便利。`number`包提供`NumberStyleFormatter`、`CurrencyStyleFormatter`和 `percentStyleFormatter` 来格式化使用`Number`的对象。`datetime`包提供了一个`DateFormatter`来将`java.util.Date`对象格式化为`java.text.DateFormat`。 + +下面的`DateFormatter`是`Formatter`实现的示例: + +爪哇 + +``` +package org.springframework.format.datetime; + +public final class DateFormatter implements Formatter { + + private String pattern; + + public DateFormatter(String pattern) { + this.pattern = pattern; + } + + public String print(Date date, Locale locale) { + if (date == null) { + return ""; + } + return getDateFormat(locale).format(date); + } + + public Date parse(String formatted, Locale locale) throws ParseException { + if (formatted.length() == 0) { + return null; + } + return getDateFormat(locale).parse(formatted); + } + + protected DateFormat getDateFormat(Locale locale) { + DateFormat dateFormat = new SimpleDateFormat(this.pattern, locale); + dateFormat.setLenient(false); + return dateFormat; + } +} +``` + +Kotlin + +``` +class DateFormatter(private val pattern: String) : Formatter { + + override fun print(date: Date, locale: Locale) + = getDateFormat(locale).format(date) + + @Throws(ParseException::class) + override fun parse(formatted: String, locale: Locale) + = getDateFormat(locale).parse(formatted) + + protected fun getDateFormat(locale: Locale): DateFormat { + val dateFormat = SimpleDateFormat(this.pattern, locale) + dateFormat.isLenient = false + return dateFormat + } +} +``` + +Spring 团队欢迎社区驱动的`Formatter`贡献。参见[GitHub Issues](https://github.com/spring-projects/spring-framework/issues)。 + +#### 3.5.2.注解驱动的格式 + +字段格式可以根据字段类型或注释进行配置。要将注释绑定到`Formatter`,请实现`AnnotationFormatterFactory`。下面的清单显示了`AnnotationFormatterFactory`接口的定义: + +``` +package org.springframework.format; + +public interface AnnotationFormatterFactory
{ + + Set> getFieldTypes(); + + Printer getPrinter(A annotation, Class fieldType); + + Parser getParser(A annotation, Class fieldType); +} +``` + +要创建一个实现: + +1. 将 a 参数化为你希望与其关联的格式化逻辑的字段`annotationType`,例如`org.springframework.format.annotation.DateTimeFormat`。 + +2. 让`getFieldTypes()`返回可以使用注释的字段类型。 + +3. 让`getPrinter()`返回一个`Printer`来打印带注释字段的值。 + +4. 有`getParser()`返回一个`Parser`来解析带注释字段的`clientValue`。 + +下面的示例`AnnotationFormatterFactory`实现将`@NumberFormat`注释绑定到格式化程序,以便指定数字样式或模式: + +爪哇 + +``` +public final class NumberFormatAnnotationFormatterFactory + implements AnnotationFormatterFactory { + + public Set> getFieldTypes() { + return new HashSet>(asList(new Class[] { + Short.class, Integer.class, Long.class, Float.class, + Double.class, BigDecimal.class, BigInteger.class })); + } + + public Printer getPrinter(NumberFormat annotation, Class fieldType) { + return configureFormatterFrom(annotation, fieldType); + } + + public Parser getParser(NumberFormat annotation, Class fieldType) { + return configureFormatterFrom(annotation, fieldType); + } + + private Formatter configureFormatterFrom(NumberFormat annotation, Class fieldType) { + if (!annotation.pattern().isEmpty()) { + return new NumberStyleFormatter(annotation.pattern()); + } else { + Style style = annotation.style(); + if (style == Style.PERCENT) { + return new PercentStyleFormatter(); + } else if (style == Style.CURRENCY) { + return new CurrencyStyleFormatter(); + } else { + return new NumberStyleFormatter(); + } + } + } +} +``` + +Kotlin + +``` +class NumberFormatAnnotationFormatterFactory : AnnotationFormatterFactory { + + override fun getFieldTypes(): Set> { + return setOf(Short::class.java, Int::class.java, Long::class.java, Float::class.java, Double::class.java, BigDecimal::class.java, BigInteger::class.java) + } + + override fun getPrinter(annotation: NumberFormat, fieldType: Class<*>): Printer { + return configureFormatterFrom(annotation, fieldType) + } + + override fun getParser(annotation: NumberFormat, fieldType: Class<*>): Parser { + return configureFormatterFrom(annotation, fieldType) + } + + private fun configureFormatterFrom(annotation: NumberFormat, fieldType: Class<*>): Formatter { + return if (annotation.pattern.isNotEmpty()) { + NumberStyleFormatter(annotation.pattern) + } else { + val style = annotation.style + when { + style === NumberFormat.Style.PERCENT -> PercentStyleFormatter() + style === NumberFormat.Style.CURRENCY -> CurrencyStyleFormatter() + else -> NumberStyleFormatter() + } + } + } +} +``` + +要触发格式设置,你可以使用 @NumberFormat 对字段进行注释,如下例所示: + +爪哇 + +``` +public class MyModel { + + @NumberFormat(style=Style.CURRENCY) + private BigDecimal decimal; +} +``` + +Kotlin + +``` +class MyModel( + @field:NumberFormat(style = Style.CURRENCY) private val decimal: BigDecimal +) +``` + +##### 格式注释 API + +`org.springframework.format.annotation`包中存在一个可移植格式注释 API。你可以使用`@NumberFormat`来格式化`Number`字段,例如`Double`和 `long’,以及`@DateTimeFormat`来格式化`java.util.Date`、`java.util.Calendar`、`Long`(用于毫秒时间戳)以及 jsr-310`java.time`。 + +下面的示例使用`@DateTimeFormat`将`java.util.Date`格式化为 ISO 日期: + +爪哇 + +``` +public class MyModel { + + @DateTimeFormat(iso=ISO.DATE) + private Date date; +} +``` + +Kotlin + +``` +class MyModel( + @DateTimeFormat(iso=ISO.DATE) private val date: Date +) +``` + +#### 3.5.3.`FormatterRegistry`SPI + +`FormatterRegistry`是用于注册格式化程序和转换器的 SPI。“formattingConversionService”是`FormatterRegistry`的一种实现,适用于大多数环境。你可以通过编程或声明性地将此变体配置为 Spring Bean,例如通过使用`FormattingConversionServiceFactoryBean`。因为该实现还实现了`ConversionService`,所以你可以直接将其配置为与 Spring 的`DataBinder`和 Spring 表达式语言一起使用。 + +下面的清单显示了`FormatterRegistry`SPI: + +``` +package org.springframework.format; + +public interface FormatterRegistry extends ConverterRegistry { + + void addPrinter(Printer printer); + + void addParser(Parser parser); + + void addFormatter(Formatter formatter); + + void addFormatterForFieldType(Class fieldType, Formatter formatter); + + void addFormatterForFieldType(Class fieldType, Printer printer, Parser parser); + + void addFormatterForFieldAnnotation(AnnotationFormatterFactory annotationFormatterFactory); +} +``` + +如前面的清单所示,你可以通过字段类型或注释注册格式化程序。 + +`FormatterRegistry`SPI 允许你集中配置格式化规则,而不是在你的控制器上重复此类配置。例如,你可能希望强制所有日期字段以特定的方式格式化,或者使用特定注释的字段以特定的方式格式化。使用共享的`FormatterRegistry`,你可以定义这些规则一次,并且在需要格式化时应用它们。 + +#### 3.5.4.`FormatterRegistrar`SPI + +`FormatterRegistrar`是用于通过 FormatterRegistry 注册格式化程序和转换器的 SPI。下面的清单显示了它的接口定义: + +``` +package org.springframework.format; + +public interface FormatterRegistrar { + + void registerFormatters(FormatterRegistry registry); +} +``` + +当为给定的格式分类(例如日期格式)注册多个相关的转换器和格式化程序时,`FormatterRegistrar`是有用的。在声明性注册不足的情况下——例如,当格式化程序需要在与其本身的``不同的特定字段类型下进行索引时,或者当注册`Printer`/` 解析器 ` 对时,它也是有用的。下一节提供了有关转换器和格式化程序注册的更多信息。 + +#### 3.5.5.在 Spring MVC 中配置格式 + +参见 Spring MVC 章节中的[转换和格式化](web.html#mvc-config-conversion)。 + +### 3.6.配置全局日期和时间格式 + +默认情况下,不带`@DateTimeFormat`注释的日期和时间字段将使用`DateFormat.SHORT`样式从字符串转换。如果你愿意,你可以通过定义自己的全局格式来更改这一点。 + +要做到这一点,请确保 Spring 不注册默认格式化程序。相反,在以下帮助下手动注册格式化程序: + +* `org.springframework.format.datetime.standard.DateTimeFormatterRegistrar` + +* `org.springframework.format.datetime.DateFormatterRegistrar` + +例如,下面的 爪哇 配置注册了一个全局`yyyyMMdd`格式: + +爪哇 + +``` +@Configuration +public class AppConfig { + + @Bean + public FormattingConversionService conversionService() { + + // Use the DefaultFormattingConversionService but do not register defaults + DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false); + + // Ensure @NumberFormat is still supported + conversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory()); + + // Register JSR-310 date conversion with a specific global format + DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); + registrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyyMMdd")); + registrar.registerFormatters(conversionService); + + // Register date conversion with a specific global format + DateFormatterRegistrar registrar = new DateFormatterRegistrar(); + registrar.setFormatter(new DateFormatter("yyyyMMdd")); + registrar.registerFormatters(conversionService); + + return conversionService; + } +} +``` + +Kotlin + +``` +@Configuration +class AppConfig { + + @Bean + fun conversionService(): FormattingConversionService { + // Use the DefaultFormattingConversionService but do not register defaults + return DefaultFormattingConversionService(false).apply { + + // Ensure @NumberFormat is still supported + addFormatterForFieldAnnotation(NumberFormatAnnotationFormatterFactory()) + + // Register JSR-310 date conversion with a specific global format + val registrar = DateTimeFormatterRegistrar() + registrar.setDateFormatter(DateTimeFormatter.ofPattern("yyyyMMdd")) + registrar.registerFormatters(this) + + // Register date conversion with a specific global format + val registrar = DateFormatterRegistrar() + registrar.setFormatter(DateFormatter("yyyyMMdd")) + registrar.registerFormatters(this) + } + } +} +``` + +如果你更喜欢基于 XML 的配置,那么可以使用“formattingConversionServiceFactoryBean”。下面的示例展示了如何做到这一点: + +``` + + + + + + + + + + + + + + + + + + + +
+ +``` + +注意,在 Web 应用程序中配置日期和时间格式时还需要考虑其他因素。请参阅[WebMVC 转换和格式化](web.html#mvc-config-conversion)或[WebFlux 转换和格式化](web-reactive.html#webflux-config-conversion)。 + +### 3.7.爪哇 Bean 验证 + +Spring 框架为[爪哇 Bean Validation](https://beanvalidation.org/)API 提供支持。 + +#### 3.7.1. Bean 验证概述 + +Bean 验证通过约束声明和元数据为 爪哇 应用程序提供了一种通用的验证方式。要使用它,你需要使用声明性验证约束注释域模型属性,然后由运行时强制执行这些约束。有内置的约束,你也可以定义自己的自定义约束。 + +考虑以下示例,其中显示了一个简单的`PersonForm`模型,该模型具有两个属性: + +爪哇 + +``` +public class PersonForm { + private String name; + private int age; +} +``` + +Kotlin + +``` +class PersonForm( + private val name: String, + private val age: Int +) +``` + +Bean 验证允许你声明约束,如下例所示: + +爪哇 + +``` +public class PersonForm { + + @NotNull + @Size(max=64) + private String name; + + @Min(0) + private int age; +} +``` + +Kotlin + +``` +class PersonForm( + @get:NotNull @get:Size(max=64) + private val name: String, + @get:Min(0) + private val age: Int +) +``` + +Bean 验证验证器然后基于声明的约束对该类的实例进行验证。有关 API 的一般信息,请参见[Bean Validation](https://beanvalidation.org/)。有关特定的约束,请参见[Hibernate Validator](https://hibernate.org/validator/)文档。要了解如何将 Bean 验证提供者设置为 Spring Bean,请继续阅读。 + +#### 3.7.2.配置 Bean 验证提供程序 + +Spring 为 Bean 验证 API 提供了充分的支持,包括将 Bean 验证提供者引导为 Spring Bean。这使你可以在应用程序中需要验证的任何地方插入 `javax.validation.validatorfactory’或`javax.validation.Validator`。 + +可以使用`LocalValidatorFactoryBean`将默认验证器配置为 Spring Bean,如下例所示: + +爪哇 + +``` +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; + +@Configuration +public class AppConfig { + + @Bean + public LocalValidatorFactoryBean validator() { + return new LocalValidatorFactoryBean(); + } +} +``` + +XML + +``` + +``` + +前面示例中的基本配置触发 Bean 验证,以通过使用其默认的引导程序机制来初始化。 Bean 验证提供程序,例如 Hibernate 验证器,预计将存在于 Classpath 中并被自动检测。 + +##### 注入验证器 + +`LocalValidatorFactoryBean`实现了`javax.validation.ValidatorFactory`和 `javax.validation.validator’,以及 Spring 的`org.springframework.validation.Validator`。你可以向需要调用验证逻辑的 bean 中注入对这两个接口中任一个的引用。 + +如果你更愿意直接使用 Bean 验证 API,则可以插入对`javax.validation.Validator`的引用,如下例所示: + +爪哇 + +``` +import javax.validation.Validator; + +@Service +public class MyService { + + @Autowired + private Validator validator; +} +``` + +Kotlin + +``` +import javax.validation.Validator; + +@Service +class MyService(@Autowired private val validator: Validator) +``` + +如果你的 Bean 需要 Spring 验证 API,则可以插入对`org.springframework.validation.Validator`的引用,如下例所示: + +爪哇 + +``` +import org.springframework.validation.Validator; + +@Service +public class MyService { + + @Autowired + private Validator validator; +} +``` + +Kotlin + +``` +import org.springframework.validation.Validator + +@Service +class MyService(@Autowired private val validator: Validator) +``` + +##### 配置自定义约束 + +Bean 每个验证约束由两部分组成: + +* 声明约束及其可配置属性的`@Constraint`注释。 + +* 实现约束行为的`javax.validation.ConstraintValidator`接口的实现。 + +要将声明与实现关联,每个`@Constraint`注释引用一个对应的`ConstraintValidator`实现类。在运行时,当域模型中遇到约束注释时,一个“constraintValidatorFactory”实例化引用的实现。 + +默认情况下,`LocalValidatorFactoryBean`配置一个`SpringConstraintValidatorFactory`,它使用 Spring 来创建`ConstraintValidator`实例。这使你的自定义“约束 Validator”像任何其他 Spring Bean 一样受益于依赖注入。 + +下面的示例显示了一个自定义`@Constraint`声明,以及一个使用 Spring 进行依赖项注入的关联的 `constraintvalidator’实现: + +爪哇 + +``` +@Target({ElementType.METHOD, ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy=MyConstraintValidator.class) +public @interface MyConstraint { +} +``` + +Kotlin + +``` +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.FIELD) +@Retention(AnnotationRetention.RUNTIME) +@Constraint(validatedBy = MyConstraintValidator::class) +annotation class MyConstraint +``` + +爪哇 + +``` +import javax.validation.ConstraintValidator; + +public class MyConstraintValidator implements ConstraintValidator { + + @Autowired; + private Foo aDependency; + + // ... +} +``` + +Kotlin + +``` +import javax.validation.ConstraintValidator + +class MyConstraintValidator(private val aDependency: Foo) : ConstraintValidator { + + // ... +} +``` + +正如前面的示例所示,`ConstraintValidator`实现可以像其他任何 Spring Bean 一样具有其依赖项 `@autowired’。 + +##### Spring-驱动方法验证 + +可以通过“MethodValidationPostProcessor” Bean 定义,将 Bean Validation1.1(以及 Hibernate Validator4.3 作为自定义扩展)支持的方法验证功能集成到 Spring 上下文中: + +爪哇 + +``` +import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; + +@Configuration +public class AppConfig { + + @Bean + public MethodValidationPostProcessor validationPostProcessor() { + return new MethodValidationPostProcessor(); + } +} +``` + +XML + +``` + +``` + +为了有资格进行 Spring 驱动的方法验证,所有目标类都需要使用 Spring 的`@Validated`注释进行注释,该注释还可以选择性地声明要使用的验证组。有关 Hibernate 验证器和 Bean 验证 1.1 提供程序的设置细节,请参见[`MethodValidationPostProcessor’](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/validation/beanvalidation/MethodValidationPostProcessor.html)。 + +| |方法验证依赖于[AOP Proxies](#aop-introduction-proxies)周围的
目标类,或者是接口上的方法的 JDK 动态代理,或者是 CGlib 代理。
使用代理有一定的限制,其中一些在[Understanding AOP Proxies](#aop-understanding-aop-proxies)中进行了描述。此外,请记住
始终在代理类上使用方法和访问器;直接字段访问将不起作用。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 附加配置选项 + +对于大多数情况,默认的`LocalValidatorFactoryBean`配置就足够了。对于各种 Bean 验证构造,有许多配置选项,从消息插值到遍历解析。有关这些选项的更多信息,请参见[localvalidatorfactorybean’](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/validation/beanvalidation/LocalValidatorFactoryBean.html)爪哇doc。 + +#### 3.7.3.配置`DataBinder` + +自 Spring 3 起,可以使用`Validator`配置`DataBinder`实例。一旦配置好,你就可以通过调用`binder.validate()`来调用`Validator`。任何验证“错误”都会自动添加到活页夹的`BindingResult`中。 + +下面的示例展示了如何以编程方式使用`DataBinder`在绑定到目标对象后调用验证逻辑: + +爪哇 + +``` +Foo target = new Foo(); +DataBinder binder = new DataBinder(target); +binder.setValidator(new FooValidator()); + +// bind to the target object +binder.bind(propertyValues); + +// validate the target object +binder.validate(); + +// get BindingResult that includes any validation errors +BindingResult results = binder.getBindingResult(); +``` + +Kotlin + +``` +val target = Foo() +val binder = DataBinder(target) +binder.validator = FooValidator() + +// bind to the target object +binder.bind(propertyValues) + +// validate the target object +binder.validate() + +// get BindingResult that includes any validation errors +val results = binder.bindingResult +``` + +你还可以通过 `databinder.addvalidators’和`dataBinder.replaceValidators`配置带有多个`Validator`实例的`DataBinder`。当将全局配置的 Bean 验证与在 Databinder 实例上本地配置的 Spring `Validator`相结合时,这是有用的。见[Spring MVC Validation Configuration](web.html#mvc-config-validation)。 + +#### 3.7.4. Spring MVC3 验证 + +参见 Spring MVC 章节中的[Validation](web.html#mvc-config-validation)。 + +## 4. Spring 表达式语言 + +Spring 表达式语言(简称“SPEL”)是一种功能强大的表达式语言,支持在运行时查询和操作对象图。该语言语法类似于 Unified EL,但提供了额外的功能,最明显的是方法调用和基本的字符串模板功能。 + +虽然还有其他几种 爪哇 表达式语言可用——OGNL、MVEL 和 JBossEL,仅举几例—— Spring 表达式语言的创建是为了向 Spring 社区提供一种受良好支持的表达式语言,这种语言可以在 Spring 产品组合中的所有产品中使用。其语言特性是由 Spring 投资组合中的项目的需求驱动的,包括[Spring Tools for Eclipse](https://spring.io/tools)中的代码完成支持的工具需求。也就是说,SPEL 基于一种与技术无关的 API,该 API 允许在需要时集成其他表达式语言实现。 + +虽然 SPEL 是 Spring 投资组合中表达式求值的基础,但它不直接绑定到 Spring,可以独立使用。为了自成一体,本章中的许多示例使用 SPEL,就好像它是一种独立的表达语言。这需要创建一些引导基础设施类,比如解析器。 Spring 大多数用户不需要处理此基础结构,并且可以仅使用作者表达式字符串进行求值。这种典型用途的一个例子是将 SPEL 集成到创建 XML 或基于注释的 Bean 定义中,如[Expression support for defining bean definitions](#expressions-beandef)所示。 + +本章介绍了表达式语言的特点、API 和语言语法。在一些地方,`Inventor`和`Society`类被用作表达式求值的目标对象。这些类声明和用于填充它们的数据在本章的末尾列出。 + +表达式语言支持以下功能: + +* 字面表达式 + +* 布尔运算符和关系运算符 + +* 正则表达式 + +* 类表达式 + +* 访问属性、数组、列表和映射 + +* 方法调用 + +* 关系运算符 + +* 任务分配 + +* 调用构造函数 + +* Bean 参考文献 + +* 阵列构造 + +* 内联列表 + +* 内联地图 + +* 三元算符 + +* 变量 + +* 用户定义的函数 + +* 集合投影 + +* 收藏选择 + +* 模板化表达式 + +### 4.1.评价 + +这一节介绍了 SPEL 接口的简单用法及其表达式语言。完整的语言引用可以在[语言参考](#expressions-language-ref)中找到。 + +下面的代码引入了 SPEL API 来计算字面字符串表达式“Hello World”。 + +爪哇 + +``` +ExpressionParser parser = new SpelExpressionParser(); +Expression exp = parser.parseExpression("'Hello World'"); (1) +String message = (String) exp.getValue(); +``` + +|**1**|消息变量的值是`'Hello World'`。| +|-----|-----------------------------------------------------| + +Kotlin + +``` +val parser = SpelExpressionParser() +val exp = parser.parseExpression("'Hello World'") (1) +val message = exp.value as String +``` + +|**1**|消息变量的值是`'Hello World'`。| +|-----|-----------------------------------------------------| + +你最有可能使用的 SPEL 类和接口位于 `org.springframework.expression` 包及其子包中,例如`spel.support`。 + +`ExpressionParser`接口负责解析表达式字符串。在前面的示例中,表达式字符串是由周围的单引号表示的字符串文字。`Expression`接口负责计算先前定义的表达式字符串。当分别调用`parser.parseExpression`和`exp.getValue`时,可以抛出两个异常,`ParseException`和 `evaluationexception’。 + +SPEL 支持多种功能,例如调用方法、访问属性和调用构造函数。 + +在下面的方法调用示例中,我们在字符串字面上调用`concat`方法: + +爪哇 + +``` +ExpressionParser parser = new SpelExpressionParser(); +Expression exp = parser.parseExpression("'Hello World'.concat('!')"); (1) +String message = (String) exp.getValue(); +``` + +|**1**|`message`的值现在是“Hello World!”。| +|-----|---------------------------------------------| + +Kotlin + +``` +val parser = SpelExpressionParser() +val exp = parser.parseExpression("'Hello World'.concat('!')") (1) +val message = exp.value as String +``` + +|**1**|`message`的值现在是“Hello World!”。| +|-----|---------------------------------------------| + +下面调用 爪哇Bean 属性的示例调用`String`属性`Bytes`: + +爪哇 + +``` +ExpressionParser parser = new SpelExpressionParser(); + +// invokes 'getBytes()' +Expression exp = parser.parseExpression("'Hello World'.bytes"); (1) +byte[] bytes = (byte[]) exp.getValue(); +``` + +|**1**|这一行将字面值转换为字节数组。| +|-----|-----------------------------------------------| + +Kotlin + +``` +val parser = SpelExpressionParser() + +// invokes 'getBytes()' +val exp = parser.parseExpression("'Hello World'.bytes") (1) +val bytes = exp.value as ByteArray +``` + +|**1**|这一行将字面值转换为字节数组。| +|-----|-----------------------------------------------| + +SPEL 还通过使用标准的点记号(例如“prop1.prop2.prop3”)和相应的属性值设置来支持嵌套属性。也可以访问公共字段。 + +下面的示例展示了如何使用点记号来获取文字的长度: + +爪哇 + +``` +ExpressionParser parser = new SpelExpressionParser(); + +// invokes 'getBytes().length' +Expression exp = parser.parseExpression("'Hello World'.bytes.length"); (1) +int length = (Integer) exp.getValue(); +``` + +|**1**|`'Hello World'.bytes.length`给出了字面的长度。| +|-----|-------------------------------------------------------------| + +Kotlin + +``` +val parser = SpelExpressionParser() + +// invokes 'getBytes().length' +val exp = parser.parseExpression("'Hello World'.bytes.length") (1) +val length = exp.value as Int +``` + +|**1**|`'Hello World'.bytes.length`给出了字面的长度。| +|-----|-------------------------------------------------------------| + +可以调用字符串的构造函数,而不是使用字符串文字,如下例所示: + +爪哇 + +``` +ExpressionParser parser = new SpelExpressionParser(); +Expression exp = parser.parseExpression("new String('hello world').toUpperCase()"); (1) +String message = exp.getValue(String.class); +``` + +|**1**|从字面上构造一个新的`String`,并使其成为大写。| +|-----|--------------------------------------------------------------------| + +Kotlin + +``` +val parser = SpelExpressionParser() +val exp = parser.parseExpression("new String('hello world').toUpperCase()") (1) +val message = exp.getValue(String::class.java) +``` + +|**1**|从字面上构造一个新的`String`,并使其成为大写。| +|-----|--------------------------------------------------------------------| + +注意通用方法的使用:`public T getValue(Class desiredResultType)`。使用此方法就不需要将表达式的值强制转换为所需的结果类型。如果不能将该值强制转换为`T`类型或使用注册的类型转换器进行转换,则抛出`EvaluationException`。 + +SPEL 更常见的用法是提供一个表达式字符串,该表达式字符串是针对特定的对象实例(称为根对象)计算的。下面的示例展示了如何从`Inventor`类的实例检索`name`属性或创建布尔条件: + +爪哇 + +``` +// Create and set a calendar +GregorianCalendar c = new GregorianCalendar(); +c.set(1856, 7, 9); + +// The constructor arguments are name, birthday, and nationality. +Inventor tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian"); + +ExpressionParser parser = new SpelExpressionParser(); + +Expression exp = parser.parseExpression("name"); // Parse name as an expression +String name = (String) exp.getValue(tesla); +// name == "Nikola Tesla" + +exp = parser.parseExpression("name == 'Nikola Tesla'"); +boolean result = exp.getValue(tesla, Boolean.class); +// result == true +``` + +Kotlin + +``` +// Create and set a calendar +val c = GregorianCalendar() +c.set(1856, 7, 9) + +// The constructor arguments are name, birthday, and nationality. +val tesla = Inventor("Nikola Tesla", c.time, "Serbian") + +val parser = SpelExpressionParser() + +var exp = parser.parseExpression("name") // Parse name as an expression +val name = exp.getValue(tesla) as String +// name == "Nikola Tesla" + +exp = parser.parseExpression("name == 'Nikola Tesla'") +val result = exp.getValue(tesla, Boolean::class.java) +// result == true +``` + +#### 4.1.1.理解`EvaluationContext` + +在计算表达式以解析属性、方法或字段并帮助执行类型转换时,使用`EvaluationContext`接口。 Spring 提供了两种实现方式。 + +* `SimpleEvaluationContext`:对于不需要完整的 SPEL 语言语法的表达式类别,公开了基本的 SPEL 语言特性和配置选项的子集,并且应该进行有意义的限制。示例包括但不限于数据绑定表达式和基于属性的过滤器。 + +* `StandardEvaluationContext`:公开了全套 SPEL 语言特性和配置选项。你可以使用它来指定一个默认的根对象,并配置所有可用的与评估相关的策略。 + +`SimpleEvaluationContext`被设计成只支持 SPEL 语言语法的一个子集。它排除了 爪哇 类型引用、构造函数和 Bean 引用。它还要求你显式地选择对表达式中的属性和方法的支持级别。默认情况下,`create()`静态工厂方法仅允许对属性的读访问。你还可以获得一个构建器,以配置所需的确切支持级别,目标是以下一种或几种组合: + +* 仅自定义`PropertyAccessor`(无反射) + +* 只读访问的数据绑定属性 + +* 用于读写的数据绑定属性 + +##### 类型转换 + +默认情况下,SPEL 使用 Spring Core 中可用的转换服务。这种转换服务与许多用于公共转换的内置转换器一起提供,但也是完全可扩展的,因此你可以在类型之间添加自定义转换。此外,它还具有泛型意识。这意味着,当你在表达式中处理泛型类型时,SPEL 会尝试转换,以维护它遇到的任何对象的类型正确性。 + +这在实践中意味着什么?假设使用`setValue()`的赋值用于设置`List`属性。该属性的类型实际上是`List`。SPEL 认识到,在将列表中的元素放入其中之前,需要将其转换为`Boolean`。下面的示例展示了如何做到这一点: + +爪哇 + +``` +class Simple { + public List booleanList = new ArrayList(); +} + +Simple simple = new Simple(); +simple.booleanList.add(true); + +EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); + +// "false" is passed in here as a String. SpEL and the conversion service +// will recognize that it needs to be a Boolean and convert it accordingly. +parser.parseExpression("booleanList[0]").setValue(context, simple, "false"); + +// b is false +Boolean b = simple.booleanList.get(0); +``` + +Kotlin + +``` +class Simple { + var booleanList: MutableList = ArrayList() +} + +val simple = Simple() +simple.booleanList.add(true) + +val context = SimpleEvaluationContext.forReadOnlyDataBinding().build() + +// "false" is passed in here as a String. SpEL and the conversion service +// will recognize that it needs to be a Boolean and convert it accordingly. +parser.parseExpression("booleanList[0]").setValue(context, simple, "false") + +// b is false +val b = simple.booleanList[0] +``` + +#### 4.1.2.解析器配置 + +可以通过使用解析器配置对象来配置 SPEL 表达式解析器。配置对象控制一些表达式组件的行为。例如,如果你将索引放入一个数组或集合中,并且指定索引处的元素是`null`,那么 SPEL 可以自动创建该元素。这在使用由一系列属性引用组成的表达式时很有用。如果你将索引放入一个数组或列表中,并指定一个超出该数组或列表当前大小的索引,那么 SPEL 可以自动增加数组或列表以适应该索引。为了在指定的索引处添加元素,在设置指定值之前,SPEL 将尝试使用元素类型的默认构造函数创建元素。如果元素类型没有默认的构造函数,`null`将被添加到数组或列表中。如果没有知道如何设置该值的内置或自定义转换器,`null`将保留在指定索引处的数组或列表中。下面的示例演示了如何自动增加列表: + +爪哇 + +``` +class Demo { + public List list; +} + +// Turn on: +// - auto null reference initialization +// - auto collection growing +SpelParserConfiguration config = new SpelParserConfiguration(true, true); + +ExpressionParser parser = new SpelExpressionParser(config); + +Expression expression = parser.parseExpression("list[3]"); + +Demo demo = new Demo(); + +Object o = expression.getValue(demo); + +// demo.list will now be a real collection of 4 entries +// Each entry is a new empty String +``` + +Kotlin + +``` +class Demo { + var list: List? = null +} + +// Turn on: +// - auto null reference initialization +// - auto collection growing +val config = SpelParserConfiguration(true, true) + +val parser = SpelExpressionParser(config) + +val expression = parser.parseExpression("list[3]") + +val demo = Demo() + +val o = expression.getValue(demo) + +// demo.list will now be a real collection of 4 entries +// Each entry is a new empty String +``` + +#### 4.1.3.SPEL 编译 + +Spring Framework4.1 包括基本表达式编译器。表达式通常被解释,这在评估期间提供了很大的动态灵活性,但并不提供最佳性能。对于偶尔使用表达式来说,这是很好的,但是,当被其他组件(例如 Spring Integration)使用时,性能可能是非常重要的,并且不需要真正的动态性。 + +SPEL 编译器旨在解决这一需求。在求值过程中,编译器生成一个 爪哇 类,该类体现了运行时的表达式行为,并使用该类来实现更快的表达式求值。由于缺少表达式的类型,编译器在执行编译时使用在表达式的解释求值过程中收集的信息。例如,它并不完全从表达式中知道属性引用的类型,但是在第一次解释求值期间,它会找出它是什么。当然,如果各种表达式元素的类型随时间变化,基于这种派生信息的编译可能会在以后引起麻烦。由于这个原因,编译最适合那些类型信息不会在重复求值时发生变化的表达式。 + +考虑以下基本表达式: + +``` +someArray[0].someProperty.someOtherProperty < 0.1 +``` + +由于前面的表达式涉及到数组访问、一些属性反引用和数字操作,因此性能增益可以非常明显。在运行 50000 次迭代的 MicroBenchmark 示例中,使用解释器进行计算需要 75ms,而使用表达式的编译版本只需要 3ms。 + +##### 编译器配置 + +默认情况下,编译器不会打开,但你可以通过两种不同的方式打开它。你可以通过使用解析器配置过程([前面讨论过](#expressions-parser-configuration))打开它,或者在将 SPEL 用法嵌入到另一个组件中时使用 Spring 属性来打开它。本节讨论这两种选择。 + +编译器可以在三种模式中的一种进行操作,这三种模式在“org.springframework.expression.spel.spelcompilermode”枚举中被捕获。模式如下: + +* `OFF`(默认):编译器已关闭。 + +* `IMMEDIATE`:在即时模式下,表达式被尽快编译。这通常是在第一次解释评估之后。如果编译的表达式失败(通常是由于类型更改,如前面所述),表达式求值的调用者将收到一个异常。 + +* `MIXED`:在混合模式下,表达式会随着时间的推移在解释模式和编译模式之间默默地切换。经过一定数量的解释运行后,它们会切换到编译表单,如果编译表单出了问题(例如前面描述的类型更改),表达式会自动再次切换回解释表单。稍后,它可能会生成另一个已编译的表单并切换到它。基本上,用户在`IMMEDIATE`模式下获得的异常情况是在内部处理的。 + +`IMMEDIATE`模式的存在是因为`MIXED`模式可能会导致具有副作用的表达式出现问题。如果编译的表达式在部分成功后爆炸,那么它可能已经做了一些影响系统状态的事情。如果发生了这种情况,调用者可能不希望它以解释模式静默地重新运行,因为表达式的一部分可能运行了两次。 + +选择模式后,使用`SpelParserConfiguration`配置解析器。下面的示例展示了如何做到这一点: + +爪哇 + +``` +SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, + this.getClass().getClassLoader()); + +SpelExpressionParser parser = new SpelExpressionParser(config); + +Expression expr = parser.parseExpression("payload"); + +MyMessage message = new MyMessage(); + +Object payload = expr.getValue(message); +``` + +Kotlin + +``` +val config = SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, + this.javaClass.classLoader) + +val parser = SpelExpressionParser(config) + +val expr = parser.parseExpression("payload") + +val message = MyMessage() + +val payload = expr.getValue(message) +``` + +在指定编译器模式时,还可以指定一个类装入器(允许传递空)。编译的表达式是在一个子类加载器中定义的,这个子类加载器是在所提供的任何类型下创建的。重要的是要确保,如果指定了类装入器,它就可以看到表达式求值过程中涉及的所有类型。如果没有指定类装入器,则使用默认的类装入器(通常是在表达式求值期间运行的线程的上下文类装入器)。 + +配置编译器的第二种方法是当 SPEL 嵌入到其他组件中时使用,并且可能无法通过配置对象对其进行配置。在这些情况下,可以通过 JVM 系统属性(或通过[“SpringProperties”](appendix.html#appendix-spring-properties)机制)将`spring.expression.compiler.mode`属性设置为一个 ` 编译器模式 `enum 值(`off’,`immediate`,或`mixed`)。 + +##### 编译器的限制 + +自 Spring 框架 4.1 以来,基本的编译框架已经到位。然而,该框架还不支持编译所有类型的表达式。最初的重点是可能在性能关键上下文中使用的常见表达式。以下表达式目前无法编译: + +* 涉及赋值的表达式 + +* 依赖于转换服务的表达式 + +* 使用自定义解析器或访问器的表达式 + +* 使用选择或投影的表达式 + +将来会有更多类型的表达式可以编译。 + +### 4.2. Bean 定义中的表达式 + +你可以使用基于 XML 或基于注释的配置元数据的 SPEL 表达式来定义`BeanDefinition`实例。在这两种情况下,定义表达式的语法都是`#{ }`形式。 + +#### 4.2.1.XML 配置 + +可以通过使用表达式来设置属性或构造函数参数的值,如下例所示: + +``` + + + + + +``` + +应用程序上下文中的所有 bean 都可以作为预定义的变量使用它们的公共 Bean 名称。这包括用于访问运行时环境的标准上下文 bean,如`environment`(类型为 `org.springframework.core.ENV.environment`)以及`systemProperties`和 `systemenvironment’(类型为`Map`)。 + +下面的示例显示了将`systemProperties` Bean 作为 SPEL 变量的访问权限: + +``` + + + + + +``` + +请注意,在这里你不必在预定义的变量前加上`#`符号。 + +还可以按名称引用其他 Bean 属性,如下例所示: + +``` + + + + + + + + + + + +``` + +#### 4.2.2.注释配置 + +要指定默认值,可以将`@Value`注释放置在字段、方法和方法或构造函数参数上。 + +下面的示例设置字段的默认值: + +爪哇 + +``` +public class FieldValueTestBean { + + @Value("#{ systemProperties['user.region'] }") + private String defaultLocale; + + public void setDefaultLocale(String defaultLocale) { + this.defaultLocale = defaultLocale; + } + + public String getDefaultLocale() { + return this.defaultLocale; + } +} +``` + +Kotlin + +``` +class FieldValueTestBean { + + @Value("#{ systemProperties['user.region'] }") + var defaultLocale: String? = null +} +``` + +下面的示例展示了等价的但在属性 setter 方法上的方法: + +爪哇 + +``` +public class PropertyValueTestBean { + + private String defaultLocale; + + @Value("#{ systemProperties['user.region'] }") + public void setDefaultLocale(String defaultLocale) { + this.defaultLocale = defaultLocale; + } + + public String getDefaultLocale() { + return this.defaultLocale; + } +} +``` + +Kotlin + +``` +class PropertyValueTestBean { + + @Value("#{ systemProperties['user.region'] }") + var defaultLocale: String? = null +} +``` + +AutoWired 方法和构造函数也可以使用`@Value`注释,如下例所示: + +爪哇 + +``` +public class SimpleMovieLister { + + private MovieFinder movieFinder; + private String defaultLocale; + + @Autowired + public void configure(MovieFinder movieFinder, + @Value("#{ systemProperties['user.region'] }") String defaultLocale) { + this.movieFinder = movieFinder; + this.defaultLocale = defaultLocale; + } + + // ... +} +``` + +Kotlin + +``` +class SimpleMovieLister { + + private lateinit var movieFinder: MovieFinder + private lateinit var defaultLocale: String + + @Autowired + fun configure(movieFinder: MovieFinder, + @Value("#{ systemProperties['user.region'] }") defaultLocale: String) { + this.movieFinder = movieFinder + this.defaultLocale = defaultLocale + } + + // ... +} +``` + +爪哇 + +``` +public class MovieRecommender { + + private String defaultLocale; + + private CustomerPreferenceDao customerPreferenceDao; + + public MovieRecommender(CustomerPreferenceDao customerPreferenceDao, + @Value("#{systemProperties['user.country']}") String defaultLocale) { + this.customerPreferenceDao = customerPreferenceDao; + this.defaultLocale = defaultLocale; + } + + // ... +} +``` + +Kotlin + +``` +class MovieRecommender(private val customerPreferenceDao: CustomerPreferenceDao, + @Value("#{systemProperties['user.country']}") private val defaultLocale: String) { + // ... +} +``` + +### 4.3.语言参考 + +本节描述 Spring 表达式语言的工作方式。它涵盖以下主题: + +* [字面表达式](#expressions-ref-literal) + +* [属性、数组、列表、映射和索引器](#expressions-properties-arrays) + +* [Inline Lists](#expressions-inline-lists) + +* [Inline Maps](#expressions-inline-maps) + +* [阵列构造](#expressions-array-construction) + +* [Methods](#expressions-methods) + +* [Operators](#expressions-operators) + +* [Types](#expressions-types) + +* [Constructors](#expressions-constructors) + +* [Variables](#expressions-ref-variables) + +* [Functions](#expressions-ref-functions) + +* [Bean References](#expressions-bean-references) + +* [三元运算符(if-then-else)](#expressions-operator-ternary) + +* [猫王操作员](#expressions-operator-elvis) + +* [安全导航操作员](#expressions-operator-safe-navigation) + +#### 4.3.1.字面表达式 + +所支持的文字表达式的类型包括字符串、数值(INT、实数、十六进制)、布尔表达式和空表达式。字符串用单引号分隔。要将单引号本身放入字符串中,请使用两个单引号字符。 + +下面的清单显示了文字的简单用法。通常,它们不会像这样孤立地使用,而是作为更复杂表达式的一部分使用——例如,在逻辑比较运算符的一侧使用文字。 + +爪哇 + +``` +ExpressionParser parser = new SpelExpressionParser(); + +// evals to "Hello World" +String helloWorld = (String) parser.parseExpression("'Hello World'").getValue(); + +double avogadrosNumber = (Double) parser.parseExpression("6.0221415E+23").getValue(); + +// evals to 2147483647 +int maxValue = (Integer) parser.parseExpression("0x7FFFFFFF").getValue(); + +boolean trueValue = (Boolean) parser.parseExpression("true").getValue(); + +Object nullValue = parser.parseExpression("null").getValue(); +``` + +Kotlin + +``` +val parser = SpelExpressionParser() + +// evals to "Hello World" +val helloWorld = parser.parseExpression("'Hello World'").value as String + +val avogadrosNumber = parser.parseExpression("6.0221415E+23").value as Double + +// evals to 2147483647 +val maxValue = parser.parseExpression("0x7FFFFFFF").value as Int + +val trueValue = parser.parseExpression("true").value as Boolean + +val nullValue = parser.parseExpression("null").value +``` + +数字支持使用负号、指数记号和小数点。默认情况下,通过使用`Double.parseDouble()`解析实数。 + +#### 4.3.2.属性、数组、列表、映射和索引器 + +使用属性引用进行导航很容易。要做到这一点,请使用一个句号来指示嵌套的属性值。`Inventor`类的实例`pupin`和`tesla`的实例使用[示例中使用的类](#expressions-example-classes)部分中列出的数据填充。为了导航“向下”的对象图,并获得特斯拉的出生年份和普平的出生城市,我们使用以下表达式: + +爪哇 + +``` +// evals to 1856 +int year = (Integer) parser.parseExpression("birthdate.year + 1900").getValue(context); + +String city = (String) parser.parseExpression("placeOfBirth.city").getValue(context); +``` + +Kotlin + +``` +// evals to 1856 +val year = parser.parseExpression("birthdate.year + 1900").getValue(context) as Int + +val city = parser.parseExpression("placeOfBirth.city").getValue(context) as String +``` + +| |允许对财产名称的第一个字母不区分大小写。因此,上述示例中的
表达式可以分别写为`Birthdate.Year + 1900`和 `placeofbirth.city’。此外,可以选择通过
方法调用访问属性——例如,`getPlaceOfBirth().getCity()`而不是 `placeofbirth.city’。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +数组和列表的内容是通过使用方括号表示法获得的,如下例所示: + +爪哇 + +``` +ExpressionParser parser = new SpelExpressionParser(); +EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); + +// Inventions Array + +// evaluates to "Induction motor" +String invention = parser.parseExpression("inventions[3]").getValue( + context, tesla, String.class); + +// Members List + +// evaluates to "Nikola Tesla" +String name = parser.parseExpression("members[0].name").getValue( + context, ieee, String.class); + +// List and Array navigation +// evaluates to "Wireless communication" +String invention = parser.parseExpression("members[0].inventions[6]").getValue( + context, ieee, String.class); +``` + +Kotlin + +``` +val parser = SpelExpressionParser() +val context = SimpleEvaluationContext.forReadOnlyDataBinding().build() + +// Inventions Array + +// evaluates to "Induction motor" +val invention = parser.parseExpression("inventions[3]").getValue( + context, tesla, String::class.java) + +// Members List + +// evaluates to "Nikola Tesla" +val name = parser.parseExpression("members[0].name").getValue( + context, ieee, String::class.java) + +// List and Array navigation +// evaluates to "Wireless communication" +val invention = parser.parseExpression("members[0].inventions[6]").getValue( + context, ieee, String::class.java) +``` + +映射的内容是通过在括号中指定文字键的值来获得的。在下面的示例中,因为`officers`映射的键是字符串,所以我们可以指定字符串字面值: + +爪哇 + +``` +// Officer's Dictionary + +Inventor pupin = parser.parseExpression("officers['president']").getValue( + societyContext, Inventor.class); + +// evaluates to "Idvor" +String city = parser.parseExpression("officers['president'].placeOfBirth.city").getValue( + societyContext, String.class); + +// setting values +parser.parseExpression("officers['advisors'][0].placeOfBirth.country").setValue( + societyContext, "Croatia"); +``` + +Kotlin + +``` +// Officer's Dictionary + +val pupin = parser.parseExpression("officers['president']").getValue( + societyContext, Inventor::class.java) + +// evaluates to "Idvor" +val city = parser.parseExpression("officers['president'].placeOfBirth.city").getValue( + societyContext, String::class.java) + +// setting values +parser.parseExpression("officers['advisors'][0].placeOfBirth.country").setValue( + societyContext, "Croatia") +``` + +#### 4.3.3.内联列表 + +你可以使用`{}`符号在表达式中直接表示列表。 + +爪哇 + +``` +// evaluates to a Java list containing the four numbers +List numbers = (List) parser.parseExpression("{1,2,3,4}").getValue(context); + +List listOfLists = (List) parser.parseExpression("{{'a','b'},{'x','y'}}").getValue(context); +``` + +Kotlin + +``` +// evaluates to a Java list containing the four numbers +val numbers = parser.parseExpression("{1,2,3,4}").getValue(context) as List<*> + +val listOfLists = parser.parseExpression("{{'a','b'},{'x','y'}}").getValue(context) as List<*> +``` + +`{}`本身意味着空列表。出于性能原因,如果列表本身完全由固定的文字组成,则创建一个常量列表来表示表达式(而不是在每个求值上构建一个新的列表)。 + +#### 4.3.4.内联地图 + +你也可以使用`{key:value}`符号在表达式中直接表示映射。下面的示例展示了如何做到这一点: + +爪哇 + +``` +// evaluates to a Java map containing the two entries +Map inventorInfo = (Map) parser.parseExpression("{name:'Nikola',dob:'10-July-1856'}").getValue(context); + +Map mapOfMaps = (Map) parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context); +``` + +Kotlin + +``` +// evaluates to a Java map containing the two entries +val inventorInfo = parser.parseExpression("{name:'Nikola',dob:'10-July-1856'}").getValue(context) as Map<*, *> + +val mapOfMaps = parser.parseExpression("{name:{first:'Nikola',last:'Tesla'},dob:{day:10,month:'July',year:1856}}").getValue(context) as Map<*, *> +``` + +`{:}`本身意味着一个空地图。出于性能原因,如果映射本身由固定的文字或其他嵌套的常量结构(列表或映射)组成,则创建一个常量映射来表示表达式(而不是在每个求值上构建一个新的映射)。对映射键的引用是可选的(除非该键包含一个句号(`.`))。上面的示例不使用引号键。 + +#### 4.3.5.阵列构造 + +你可以使用熟悉的 爪哇 语法构建数组,也可以提供一个初始化器,以便在构建时填充数组。下面的示例展示了如何做到这一点: + +爪哇 + +``` +int[] numbers1 = (int[]) parser.parseExpression("new int[4]").getValue(context); + +// Array with initializer +int[] numbers2 = (int[]) parser.parseExpression("new int[]{1,2,3}").getValue(context); + +// Multi dimensional array +int[][] numbers3 = (int[][]) parser.parseExpression("new int[4][5]").getValue(context); +``` + +Kotlin + +``` +val numbers1 = parser.parseExpression("new int[4]").getValue(context) as IntArray + +// Array with initializer +val numbers2 = parser.parseExpression("new int[]{1,2,3}").getValue(context) as IntArray + +// Multi dimensional array +val numbers3 = parser.parseExpression("new int[4][5]").getValue(context) as Array +``` + +在构造多维数组时,当前不能提供初始化器。 + +#### 4.3.6.方法 + +你可以通过使用典型的 爪哇 编程语法调用方法。你还可以在字面值上调用方法。还支持变量参数。以下示例展示了如何调用方法: + +爪哇 + +``` +// string literal, evaluates to "bc" +String bc = parser.parseExpression("'abc'.substring(1, 3)").getValue(String.class); + +// evaluates to true +boolean isMember = parser.parseExpression("isMember('Mihajlo Pupin')").getValue( + societyContext, Boolean.class); +``` + +Kotlin + +``` +// string literal, evaluates to "bc" +val bc = parser.parseExpression("'abc'.substring(1, 3)").getValue(String::class.java) + +// evaluates to true +val isMember = parser.parseExpression("isMember('Mihajlo Pupin')").getValue( + societyContext, Boolean::class.java) +``` + +#### 4.3.7.操作员 + +Spring 表达式语言支持以下几种运算符: + +* [关系运算符](#expressions-operators-relational) + +* [逻辑运算符](#expressions-operators-logical) + +* [数学运算符](#expressions-operators-mathematical) + +* [赋值运算符](#expressions-assignment) + +##### 关系运算符 + +关系运算符(相等、不相等、小于、小于或等于、大于和大于或等于)通过使用标准运算符表示法来支持。下面的列表显示了几个操作符的示例: + +爪哇 + +``` +// evaluates to true +boolean trueValue = parser.parseExpression("2 == 2").getValue(Boolean.class); + +// evaluates to false +boolean falseValue = parser.parseExpression("2 < -5.0").getValue(Boolean.class); + +// evaluates to true +boolean trueValue = parser.parseExpression("'black' < 'block'").getValue(Boolean.class); +``` + +Kotlin + +``` +// evaluates to true +val trueValue = parser.parseExpression("2 == 2").getValue(Boolean::class.java) + +// evaluates to false +val falseValue = parser.parseExpression("2 < -5.0").getValue(Boolean::class.java) + +// evaluates to true +val trueValue = parser.parseExpression("'black' < 'block'").getValue(Boolean::class.java) +``` + +| |与`null`的大于或小于比较遵循一个简单的规则:`null`被视为
nothing(即不为零)。因此,任何其他值总是大于
`null`(`x>null` 总是`true`),并且没有其他值总是小于 nothing
(`x如果你更喜欢数字比较,
,避免基于数字的`null`比较
,而倾向于与零进行比较(例如,`X > 0`或`X < 0`)。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +除了标准的关系运算符,SPEL 还支持`instanceof`和基于正则表达式的`matches`运算符。下面的清单展示了这两个方面的例子: + +爪哇 + +``` +// evaluates to false +boolean falseValue = parser.parseExpression( + "'xyz' instanceof T(Integer)").getValue(Boolean.class); + +// evaluates to true +boolean trueValue = parser.parseExpression( + "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); + +// evaluates to false +boolean falseValue = parser.parseExpression( + "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean.class); +``` + +Kotlin + +``` +// evaluates to false +val falseValue = parser.parseExpression( + "'xyz' instanceof T(Integer)").getValue(Boolean::class.java) + +// evaluates to true +val trueValue = parser.parseExpression( + "'5.00' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) + +// evaluates to false +val falseValue = parser.parseExpression( + "'5.0067' matches '^-?\\d+(\\.\\d{2})?$'").getValue(Boolean::class.java) +``` + +| |在使用基本类型时要小心,因为它们会立即 Boxed 到它们的
包装器类型。例如,`1 instanceof T(int)`计算为`false`,而 `1instanceof t(integer)` 计算为`true`,正如预期的那样。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +每个符号运算符也可以指定为纯字母等价的。这避免了所使用的符号对于嵌入表达式的文档类型(例如在 XML 文档中)具有特殊含义的问题。对应的文本是: + +* `lt`(`<`) + +* `gt`(`>`) + +* `le`(`<=`) + +* `ge`(`>=`) + +* `eq`(`==`) + +* `ne`(`!=`) + +* `div`(`/`) + +* `mod`(`%`) + +* `not`(`!`). + +所有的文本运算符都是不区分大小写的。 + +##### 逻辑运算符 + +SPEL 支持以下逻辑运算符: + +* `and`(`&&`) + +* `or`(`||`) + +* `not`(`!`) + +下面的示例展示了如何使用逻辑运算符: + +爪哇 + +``` +// -- AND -- + +// evaluates to false +boolean falseValue = parser.parseExpression("true and false").getValue(Boolean.class); + +// evaluates to true +String expression = "isMember('Nikola Tesla') and isMember('Mihajlo Pupin')"; +boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class); + +// -- OR -- + +// evaluates to true +boolean trueValue = parser.parseExpression("true or false").getValue(Boolean.class); + +// evaluates to true +String expression = "isMember('Nikola Tesla') or isMember('Albert Einstein')"; +boolean trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class); + +// -- NOT -- + +// evaluates to false +boolean falseValue = parser.parseExpression("!true").getValue(Boolean.class); + +// -- AND and NOT -- +String expression = "isMember('Nikola Tesla') and !isMember('Mihajlo Pupin')"; +boolean falseValue = parser.parseExpression(expression).getValue(societyContext, Boolean.class); +``` + +Kotlin + +``` +// -- AND -- + +// evaluates to false +val falseValue = parser.parseExpression("true and false").getValue(Boolean::class.java) + +// evaluates to true +val expression = "isMember('Nikola Tesla') and isMember('Mihajlo Pupin')" +val trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean::class.java) + +// -- OR -- + +// evaluates to true +val trueValue = parser.parseExpression("true or false").getValue(Boolean::class.java) + +// evaluates to true +val expression = "isMember('Nikola Tesla') or isMember('Albert Einstein')" +val trueValue = parser.parseExpression(expression).getValue(societyContext, Boolean::class.java) + +// -- NOT -- + +// evaluates to false +val falseValue = parser.parseExpression("!true").getValue(Boolean::class.java) + +// -- AND and NOT -- +val expression = "isMember('Nikola Tesla') and !isMember('Mihajlo Pupin')" +val falseValue = parser.parseExpression(expression).getValue(societyContext, Boolean::class.java) +``` + +##### 数学运算符 + +在数字和字符串上都可以使用加法运算符。你可以只在数字上使用减法、乘法和除法运算符.你还可以在数字上使用模运算符“%”和指数幂运算符“^”。强制执行标准操作符优先级。下面的示例显示了正在使用的数学运算符: + +爪哇 + +``` +// Addition +int two = parser.parseExpression("1 + 1").getValue(Integer.class); // 2 + +String testString = parser.parseExpression( + "'test' + ' ' + 'string'").getValue(String.class); // 'test string' + +// Subtraction +int four = parser.parseExpression("1 - -3").getValue(Integer.class); // 4 + +double d = parser.parseExpression("1000.00 - 1e4").getValue(Double.class); // -9000 + +// Multiplication +int six = parser.parseExpression("-2 * -3").getValue(Integer.class); // 6 + +double twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Double.class); // 24.0 + +// Division +int minusTwo = parser.parseExpression("6 / -3").getValue(Integer.class); // -2 + +double one = parser.parseExpression("8.0 / 4e0 / 2").getValue(Double.class); // 1.0 + +// Modulus +int three = parser.parseExpression("7 % 4").getValue(Integer.class); // 3 + +int one = parser.parseExpression("8 / 5 % 2").getValue(Integer.class); // 1 + +// Operator precedence +int minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Integer.class); // -21 +``` + +Kotlin + +``` +// Addition +val two = parser.parseExpression("1 + 1").getValue(Int::class.java) // 2 + +val testString = parser.parseExpression( + "'test' + ' ' + 'string'").getValue(String::class.java) // 'test string' + +// Subtraction +val four = parser.parseExpression("1 - -3").getValue(Int::class.java) // 4 + +val d = parser.parseExpression("1000.00 - 1e4").getValue(Double::class.java) // -9000 + +// Multiplication +val six = parser.parseExpression("-2 * -3").getValue(Int::class.java) // 6 + +val twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Double::class.java) // 24.0 + +// Division +val minusTwo = parser.parseExpression("6 / -3").getValue(Int::class.java) // -2 + +val one = parser.parseExpression("8.0 / 4e0 / 2").getValue(Double::class.java) // 1.0 + +// Modulus +val three = parser.parseExpression("7 % 4").getValue(Int::class.java) // 3 + +val one = parser.parseExpression("8 / 5 % 2").getValue(Int::class.java) // 1 + +// Operator precedence +val minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Int::class.java) // -21 +``` + +##### 赋值运算符 + +要设置属性,请使用赋值运算符。这通常在对`setValue`的调用中完成,但也可以在对`getValue`的调用中完成。下面的清单显示了使用赋值操作符的两种方式: + +爪哇 + +``` +Inventor inventor = new Inventor(); +EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build(); + +parser.parseExpression("name").setValue(context, inventor, "Aleksandar Seovic"); + +// alternatively +String aleks = parser.parseExpression( + "name = 'Aleksandar Seovic'").getValue(context, inventor, String.class); +``` + +Kotlin + +``` +val inventor = Inventor() +val context = SimpleEvaluationContext.forReadWriteDataBinding().build() + +parser.parseExpression("name").setValue(context, inventor, "Aleksandar Seovic") + +// alternatively +val aleks = parser.parseExpression( + "name = 'Aleksandar Seovic'").getValue(context, inventor, String::class.java) +``` + +#### 4.3.8.类型 + +你可以使用特殊的`T`操作符来指定`java.lang.Class`(类型)的实例。静态方法也可以通过使用此操作符来调用。“StandardDeValuationContext”使用`TypeLocator`来查找类型,而“StandardTypeLocator”(可以替换)是在理解“java.lang”包的情况下构建的。这意味着对`T()`包中的类型的引用不需要完全限定,但是所有其他类型的引用必须是完全限定的。下面的示例展示了如何使用`T`运算符: + +爪哇 + +``` +Class dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class.class); + +Class stringClass = parser.parseExpression("T(String)").getValue(Class.class); + +boolean trueValue = parser.parseExpression( + "T(java.math.RoundingMode).CEILING < T(java.math.RoundingMode).FLOOR") + .getValue(Boolean.class); +``` + +Kotlin + +``` +val dateClass = parser.parseExpression("T(java.util.Date)").getValue(Class::class.java) + +val stringClass = parser.parseExpression("T(String)").getValue(Class::class.java) + +val trueValue = parser.parseExpression( + "T(java.math.RoundingMode).CEILING < T(java.math.RoundingMode).FLOOR") + .getValue(Boolean::class.java) +``` + +#### 4.3.9.构造者 + +你可以使用`new`操作符调用构造函数。除了位于`java.lang`包中的类型(`integer`,`Float`,`String`,以此类推)之外,你应该为所有类型使用完全限定的类名。下面的示例展示了如何使用“new”操作符调用构造函数: + +爪哇 + +``` +Inventor einstein = p.parseExpression( + "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") + .getValue(Inventor.class); + +// create new Inventor instance within the add() method of List +p.parseExpression( + "Members.add(new org.spring.samples.spel.inventor.Inventor( + 'Albert Einstein', 'German'))").getValue(societyContext); +``` + +Kotlin + +``` +val einstein = p.parseExpression( + "new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')") + .getValue(Inventor::class.java) + +// create new Inventor instance within the add() method of List +p.parseExpression( + "Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))") + .getValue(societyContext) +``` + +#### 4.3.10.变量 + +你可以使用`#variableName`语法在表达式中引用变量。变量是通过在`EvaluationContext`实现上使用`setVariable`方法来设置的。 + +| |有效的变量名必须由以下一个或多个受支持的
字符组成。

* 字母:`A`至`Z`至
`0`* 数字:`0`至<<>>>>11”/><<<>>>>>><>>>>>><<<<>>>>>>>>>>>>>>>| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +下面的示例展示了如何使用变量。 + +爪哇 + +``` +Inventor tesla = new Inventor("Nikola Tesla", "Serbian"); + +EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build(); +context.setVariable("newName", "Mike Tesla"); + +parser.parseExpression("name = #newName").getValue(context, tesla); +System.out.println(tesla.getName()) // "Mike Tesla" +``` + +Kotlin + +``` +val tesla = Inventor("Nikola Tesla", "Serbian") + +val context = SimpleEvaluationContext.forReadWriteDataBinding().build() +context.setVariable("newName", "Mike Tesla") + +parser.parseExpression("name = #newName").getValue(context, tesla) +println(tesla.name) // "Mike Tesla" +``` + +##### `#this`和`#root`变量 + +总是定义`#this`变量,并引用当前的求值对象(针对该对象解析不合格的引用)。始终定义`#root`变量,并引用根上下文对象。虽然`#this`可以随着表达式的组成部分的求值而变化,但`#root`总是指根。以下示例展示了如何使用`#this`和`#root`变量: + +爪哇 + +``` +// create an array of integers +List primes = new ArrayList(); +primes.addAll(Arrays.asList(2,3,5,7,11,13,17)); + +// create parser and set variable 'primes' as the array of integers +ExpressionParser parser = new SpelExpressionParser(); +EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataAccess(); +context.setVariable("primes", primes); + +// all prime numbers > 10 from the list (using selection ?{...}) +// evaluates to [11, 13, 17] +List primesGreaterThanTen = (List) parser.parseExpression( + "#primes.?[#this>10]").getValue(context); +``` + +Kotlin + +``` +// create an array of integers +val primes = ArrayList() +primes.addAll(listOf(2, 3, 5, 7, 11, 13, 17)) + +// create parser and set variable 'primes' as the array of integers +val parser = SpelExpressionParser() +val context = SimpleEvaluationContext.forReadOnlyDataAccess() +context.setVariable("primes", primes) + +// all prime numbers > 10 from the list (using selection ?{...}) +// evaluates to [11, 13, 17] +val primesGreaterThanTen = parser.parseExpression( + "#primes.?[#this>10]").getValue(context) as List +``` + +#### 4.3.11.职能 + +你可以通过注册可以在表达式字符串中调用的用户定义函数来扩展 SPEL。函数是通过`EvaluationContext`注册的。下面的示例展示了如何注册用户定义的函数: + +爪哇 + +``` +Method method = ...; + +EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); +context.setVariable("myFunction", method); +``` + +Kotlin + +``` +val method: Method = ... + +val context = SimpleEvaluationContext.forReadOnlyDataBinding().build() +context.setVariable("myFunction", method) +``` + +例如,考虑以下逆转字符串的实用工具方法: + +爪哇 + +``` +public abstract class StringUtils { + + public static String reverseString(String input) { + StringBuilder backwards = new StringBuilder(input.length()); + for (int i = 0; i < input.length(); i++) { + backwards.append(input.charAt(input.length() - 1 - i)); + } + return backwards.toString(); + } +} +``` + +Kotlin + +``` +fun reverseString(input: String): String { + val backwards = StringBuilder(input.length) + for (i in 0 until input.length) { + backwards.append(input[input.length - 1 - i]) + } + return backwards.toString() +} +``` + +然后,你可以注册并使用前面的方法,如下例所示: + +爪哇 + +``` +ExpressionParser parser = new SpelExpressionParser(); + +EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); +context.setVariable("reverseString", + StringUtils.class.getDeclaredMethod("reverseString", String.class)); + +String helloWorldReversed = parser.parseExpression( + "#reverseString('hello')").getValue(context, String.class); +``` + +Kotlin + +``` +val parser = SpelExpressionParser() + +val context = SimpleEvaluationContext.forReadOnlyDataBinding().build() +context.setVariable("reverseString", ::reverseString::javaMethod) + +val helloWorldReversed = parser.parseExpression( + "#reverseString('hello')").getValue(context, String::class.java) +``` + +#### 4.3.12. Bean 参考文献 + +如果计算上下文已配置了 Bean 解析器,则可以使用`@`符号从表达式中查找 bean。下面的示例展示了如何做到这一点: + +爪哇 + +``` +ExpressionParser parser = new SpelExpressionParser(); +StandardEvaluationContext context = new StandardEvaluationContext(); +context.setBeanResolver(new MyBeanResolver()); + +// This will end up calling resolve(context,"something") on MyBeanResolver during evaluation +Object bean = parser.parseExpression("@something").getValue(context); +``` + +Kotlin + +``` +val parser = SpelExpressionParser() +val context = StandardEvaluationContext() +context.setBeanResolver(MyBeanResolver()) + +// This will end up calling resolve(context,"something") on MyBeanResolver during evaluation +val bean = parser.parseExpression("@something").getValue(context) +``` + +要访问工厂 Bean 本身,你应该在 Bean 名称前加上`&`符号。下面的示例展示了如何做到这一点: + +Java + +``` +ExpressionParser parser = new SpelExpressionParser(); +StandardEvaluationContext context = new StandardEvaluationContext(); +context.setBeanResolver(new MyBeanResolver()); + +// This will end up calling resolve(context,"&foo") on MyBeanResolver during evaluation +Object bean = parser.parseExpression("&foo").getValue(context); +``` + +Kotlin + +``` +val parser = SpelExpressionParser() +val context = StandardEvaluationContext() +context.setBeanResolver(MyBeanResolver()) + +// This will end up calling resolve(context,"&foo") on MyBeanResolver during evaluation +val bean = parser.parseExpression("&foo").getValue(context) +``` + +#### 4.3.13.三元运算符(if-then-else) + +可以使用三值运算符在表达式中执行 if-then-else 条件逻辑。下面的清单展示了一个最小示例: + +Java + +``` +String falseString = parser.parseExpression( + "false ? 'trueExp' : 'falseExp'").getValue(String.class); +``` + +Kotlin + +``` +val falseString = parser.parseExpression( + "false ? 'trueExp' : 'falseExp'").getValue(String::class.java) +``` + +在这种情况下,布尔`false`将返回字符串值`'falseExp'`。下面是一个更现实的例子: + +Java + +``` +parser.parseExpression("name").setValue(societyContext, "IEEE"); +societyContext.setVariable("queryName", "Nikola Tesla"); + +expression = "isMember(#queryName)? #queryName + ' is a member of the ' " + + "+ Name + ' Society' : #queryName + ' is not a member of the ' + Name + ' Society'"; + +String queryResultString = parser.parseExpression(expression) + .getValue(societyContext, String.class); +// queryResultString = "Nikola Tesla is a member of the IEEE Society" +``` + +Kotlin + +``` +parser.parseExpression("name").setValue(societyContext, "IEEE") +societyContext.setVariable("queryName", "Nikola Tesla") + +expression = "isMember(#queryName)? #queryName + ' is a member of the ' " + "+ Name + ' Society' : #queryName + ' is not a member of the ' + Name + ' Society'" + +val queryResultString = parser.parseExpression(expression) + .getValue(societyContext, String::class.java) +// queryResultString = "Nikola Tesla is a member of the IEEE Society" +``` + +请参阅下一节关于 Elvis 操作符的内容,以获得更短的三元运算符语法。 + +#### 4.3.14.猫王操作员 + +Elvis 运算符是三元运算符语法的缩写,在[Groovy](http://www.groovy-lang.org/operators.html#_elvis_operator)语言中使用。使用三元运算符语法,你通常必须重复一个变量两次,如下例所示: + +``` +String name = "Elvis Presley"; +String displayName = (name != null ? name : "Unknown"); +``` + +相反,你可以使用猫王操作符(该操作符的名称与猫王的发型相似)。下面的示例展示了如何使用 Elvis 操作符: + +Java + +``` +ExpressionParser parser = new SpelExpressionParser(); + +String name = parser.parseExpression("name?:'Unknown'").getValue(new Inventor(), String.class); +System.out.println(name); // 'Unknown' +``` + +Kotlin + +``` +val parser = SpelExpressionParser() + +val name = parser.parseExpression("name?:'Unknown'").getValue(Inventor(), String::class.java) +println(name) // 'Unknown' +``` + +下面的清单展示了一个更复杂的示例: + +Java + +``` +ExpressionParser parser = new SpelExpressionParser(); +EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); + +Inventor tesla = new Inventor("Nikola Tesla", "Serbian"); +String name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String.class); +System.out.println(name); // Nikola Tesla + +tesla.setName(null); +name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String.class); +System.out.println(name); // Elvis Presley +``` + +Kotlin + +``` +val parser = SpelExpressionParser() +val context = SimpleEvaluationContext.forReadOnlyDataBinding().build() + +val tesla = Inventor("Nikola Tesla", "Serbian") +var name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String::class.java) +println(name) // Nikola Tesla + +tesla.setName(null) +name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String::class.java) +println(name) // Elvis Presley +``` + +| |可以使用 Elvis 操作符在表达式中应用默认值。下面的
示例展示了如何在`@Value`表达式中使用 Elvis 操作符:

``
@value(“#{systemproperties[’pop3.port’]:25}”)
``
如果没有定义,这将注入一个系统属性 =“4429”/>。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 4.3.15.安全导航操作员 + +安全导航操作符用于避免`NullPointerException`并来自[Groovy](http://www.groovy-lang.org/operators.html#_safe_navigation_operator)语言。通常,当你有一个对象的引用时,在访问对象的方法或属性之前,你可能需要验证它不是空的。为了避免这种情况,安全导航操作符将返回 null,而不是抛出异常。下面的示例展示了如何使用安全导航操作符: + +Java + +``` +ExpressionParser parser = new SpelExpressionParser(); +EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); + +Inventor tesla = new Inventor("Nikola Tesla", "Serbian"); +tesla.setPlaceOfBirth(new PlaceOfBirth("Smiljan")); + +String city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String.class); +System.out.println(city); // Smiljan + +tesla.setPlaceOfBirth(null); +city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String.class); +System.out.println(city); // null - does not throw NullPointerException!!! +``` + +Kotlin + +``` +val parser = SpelExpressionParser() +val context = SimpleEvaluationContext.forReadOnlyDataBinding().build() + +val tesla = Inventor("Nikola Tesla", "Serbian") +tesla.setPlaceOfBirth(PlaceOfBirth("Smiljan")) + +var city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String::class.java) +println(city) // Smiljan + +tesla.setPlaceOfBirth(null) +city = parser.parseExpression("placeOfBirth?.city").getValue(context, tesla, String::class.java) +println(city) // null - does not throw NullPointerException!!! +``` + +#### 4.3.16.收藏选择 + +选择是一种功能强大的表达式语言特性,它允许你通过从源集合的条目中进行选择来将其转换为另一个集合。 + +选择使用`.?[selectionExpression]`的语法。它对集合进行过滤,并返回一个新的集合,该集合包含原始元素的一个子集。例如,Selection 可以让我们很容易地获得一份塞尔维亚发明家的名单,如下例所示: + +Java + +``` +List list = (List) parser.parseExpression( + "members.?[nationality == 'Serbian']").getValue(societyContext); +``` + +Kotlin + +``` +val list = parser.parseExpression( + "members.?[nationality == 'Serbian']").getValue(societyContext) as List +``` + +对于数组和实现`java.lang.Iterable`或 `java.util.map’的任何东西,都支持选择。对于一个列表或数组,选择标准是根据每个单独的元素进行评估的。针对映射,根据每个映射条目(Java 类型`Map.Entry`的对象)评估选择条件。每个 map 条目都有其`key`和`value`可作为属性访问,以便在选择中使用。 + +下面的表达式返回一个新的映射,该映射由原始映射的那些元素组成,其中条目的值小于 27: + +Java + +``` +Map newMap = parser.parseExpression("map.?[value<27]").getValue(); +``` + +Kotlin + +``` +val newMap = parser.parseExpression("map.?[value<27]").getValue() +``` + +除了返回所有选定的元素外,你还可以只检索第一个或最后一个元素。要获得与所选内容匹配的第一个元素,语法为 `.^[SelectionExpression]`。要获得最后一个匹配的选择,语法是 `.$[SelectionExpression]`。 + +#### 4.3.17.集合投影 + +投影让集合驱动子表达式的求值,其结果是一个新的集合。投影的语法是`.![projectionExpression]`。例如,假设我们有一个发明家名单,但想要他们出生的城市名单。实际上,我们希望对发明家列表中的每个条目进行“出生地.城市”评估。下面的示例使用投影来实现这一点: + +Java + +``` +// returns ['Smiljan', 'Idvor' ] +List placesOfBirth = (List)parser.parseExpression("members.![placeOfBirth.city]"); +``` + +Kotlin + +``` +// returns ['Smiljan', 'Idvor' ] +val placesOfBirth = parser.parseExpression("members.![placeOfBirth.city]") as List<*> +``` + +对于数组和实现`java.lang.Iterable`或 `java.util.map’的任何东西,都支持投影。当使用映射来驱动投影时,该投影表达式针对映射中的每个条目进行求值(表示为 Java`Map.Entry`)。在映射上进行投影的结果是一个列表,该列表由针对每个映射条目的投影表达式的求值组成。 + +#### 4.3.18.表达式模板化 + +表达式模板允许将文字与一个或多个求值块混合。每个求值块都用你可以定义的前缀和后缀字符分隔。一个常见的选择是使用`#{ }`作为分隔符,如下例所示: + +Java + +``` +String randomPhrase = parser.parseExpression( + "random number is #{T(java.lang.Math).random()}", + new TemplateParserContext()).getValue(String.class); + +// evaluates to "random number is 0.7038186818312008" +``` + +Kotlin + +``` +val randomPhrase = parser.parseExpression( + "random number is #{T(java.lang.Math).random()}", + TemplateParserContext()).getValue(String::class.java) + +// evaluates to "random number is 0.7038186818312008" +``` + +通过将文本`'random number is '`与在`#{ }`分隔符内求值表达式的结果连接在一起来计算字符串(在这种情况下,调用`random()`方法的结果)。`parseExpression()`方法的第二个参数类型为`ParserContext`。`ParserContext`接口用于影响表达式的解析方式,以支持表达式模板功能。`TemplateParserContext`的定义如下: + +Java + +``` +public class TemplateParserContext implements ParserContext { + + public String getExpressionPrefix() { + return "#{"; + } + + public String getExpressionSuffix() { + return "}"; + } + + public boolean isTemplate() { + return true; + } +} +``` + +Kotlin + +``` +class TemplateParserContext : ParserContext { + + override fun getExpressionPrefix(): String { + return "#{" + } + + override fun getExpressionSuffix(): String { + return "}" + } + + override fun isTemplate(): Boolean { + return true + } +} +``` + +### 4.4.示例中使用的类 + +这一节列出了在这一章的例子中使用的类。 + +Inventor.java + +``` +package org.spring.samples.spel.inventor; + +import java.util.Date; +import java.util.GregorianCalendar; + +public class Inventor { + + private String name; + private String nationality; + private String[] inventions; + private Date birthdate; + private PlaceOfBirth placeOfBirth; + + public Inventor(String name, String nationality) { + GregorianCalendar c= new GregorianCalendar(); + this.name = name; + this.nationality = nationality; + this.birthdate = c.getTime(); + } + + public Inventor(String name, Date birthdate, String nationality) { + this.name = name; + this.nationality = nationality; + this.birthdate = birthdate; + } + + public Inventor() { + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getNationality() { + return nationality; + } + + public void setNationality(String nationality) { + this.nationality = nationality; + } + + public Date getBirthdate() { + return birthdate; + } + + public void setBirthdate(Date birthdate) { + this.birthdate = birthdate; + } + + public PlaceOfBirth getPlaceOfBirth() { + return placeOfBirth; + } + + public void setPlaceOfBirth(PlaceOfBirth placeOfBirth) { + this.placeOfBirth = placeOfBirth; + } + + public void setInventions(String[] inventions) { + this.inventions = inventions; + } + + public String[] getInventions() { + return inventions; + } +} +``` + +Inventor.kt + +``` +class Inventor( + var name: String, + var nationality: String, + var inventions: Array? = null, + var birthdate: Date = GregorianCalendar().time, + var placeOfBirth: PlaceOfBirth? = null) +``` + +placeofbirth.java + +``` +package org.spring.samples.spel.inventor; + +public class PlaceOfBirth { + + private String city; + private String country; + + public PlaceOfBirth(String city) { + this.city=city; + } + + public PlaceOfBirth(String city, String country) { + this(city); + this.country = country; + } + + public String getCity() { + return city; + } + + public void setCity(String s) { + this.city = s; + } + + public String getCountry() { + return country; + } + + public void setCountry(String country) { + this.country = country; + } +} +``` + +PlaceofBirth.kt + +``` +class PlaceOfBirth(var city: String, var country: String? = null) { +``` + +Society.java + +``` +package org.spring.samples.spel.inventor; + +import java.util.*; + +public class Society { + + private String name; + + public static String Advisors = "advisors"; + public static String President = "president"; + + private List members = new ArrayList(); + private Map officers = new HashMap(); + + public List getMembers() { + return members; + } + + public Map getOfficers() { + return officers; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isMember(String name) { + for (Inventor inventor : members) { + if (inventor.getName().equals(name)) { + return true; + } + } + return false; + } +} +``` + +Society.kt + +``` +package org.spring.samples.spel.inventor + +import java.util.* + +class Society { + + val Advisors = "advisors" + val President = "president" + + var name: String? = null + + val members = ArrayList() + val officers = mapOf() + + fun isMember(name: String): Boolean { + for (inventor in members) { + if (inventor.name == name) { + return true + } + } + return false + } +} +``` + +## 5. Spring 面向方面的编程 + +AOP 面向方面编程(Aspect-oriented Programming, AOP)通过提供关于程序结构的另一种思考方式,补充了面向对象编程。OOP 中模块化的关键单元是类,而 AOP 中模块化的单元是方面。方面支持跨多个类型和对象的关注(例如事务管理)的模块化。(在文献 AOP 中,这类担忧通常被称为“跨领域”担忧。 + +Spring 的关键组件之一是 AOP 框架。虽然 Spring IOC 容器不依赖于 AOP(这意味着如果你不想使用 AOP),但 AOP 补充了 Spring IOC 以提供非常有能力的中间件解决方案。 + +Spring AOP 带 AspectJ 切入点 + +Spring 提供了通过使用[基于模式的方法](#aop-schema)或[@AspectJ 注释样式](#aop-ataspectj)来编写自定义方面的简单而强大的方法。这两种样式都提供完全类型的建议和 AspectJ PointCut 语言的使用,同时仍然使用 Spring AOP 进行编织。 + +本章讨论了基于模式和 @AspectJ 的支持 AOP。较低级别的 AOP 支持在[接下来的一章](#aop-api)中进行了讨论。 + +AOP 在 Spring 框架中用于: + +* 提供声明性的 Enterprise 服务。最重要的这类服务是[声明式事务管理](data-access.html#transaction-declarative)。 + +* 让用户实现自定义方面,用 AOP 补充他们对 OOP 的使用。 + +| |如果你只对通用声明性服务或其他预打包的
声明性中间件服务如池感兴趣,则不需要直接使用
Spring AOP,并且可以跳过本章的大部分内容。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 5.1. AOP 概念 + +让我们首先定义一些核心的概念和术语。这些术语不是 Spring 特定的。遗憾的是, AOP 术语并不是特别直观。然而,如果 Spring 使用自己的术语,那将更加令人困惑。 + +* 方面:跨多个类的关注的模块化。事务管理是 EnterpriseJava 应用程序中横切关注点的一个很好的示例。在 Spring AOP 中,方面是通过使用常规类([基于模式的方法](#aop-schema))或使用 `@Aspect` 注释([@AspectJ style](#aop-ataspectj))的常规类来实现的。 + +* 连接点:程序执行过程中的一个点,如方法的执行或异常的处理。在 Spring AOP 中,连接点总是表示方法的执行。 + +* 建议:某一方面在某一特定连接点上采取的行动。不同类型的建议包括“周围”、“之前”和“之后”的建议。(通知类型将在后面讨论。)许多 AOP 框架,包括 Spring,将通知建模为拦截器,并在连接点周围维护拦截器的链。 + +* 切入点:匹配连接点的谓词。通知与切入点表达式关联,并在与切入点匹配的任何连接点上运行(例如,执行具有特定名称的方法)。由切入点表达式匹配的连接点的概念是 AOP 的核心,并且 Spring 默认情况下使用 AspectJ PointCut 表达式语言。 + +* 介绍:代表类型声明附加的方法或字段。 Spring AOP 让你将新的接口(和相应的实现)引入到任何建议的对象。例如,你可以使用一个介绍来使 Bean 实现一个 `ismodified’接口,以简化缓存。(在 AspectJ 社区中,介绍称为类型间声明。 + +* 目标对象:由一个或多个方面提供建议的对象。也称为“已知对象”。由于 Spring AOP 是通过使用运行时代理来实现的,因此该对象始终是代理对象。 + +* AOP 代理:由 AOP 框架创建的一个对象,以便实现方面契约(通知方法执行等)。在 Spring 框架中, AOP 代理是 JDK 动态代理或 CGLIB 代理。 + +* 编织:将方面与其他应用程序类型或对象连接起来,以创建一个建议的对象。这可以在编译时(例如使用 AspectJ 编译器)、加载时或运行时完成。 Spring AOP 与其他纯 爪哇 AOP 框架一样,在运行时执行编织。 + +Spring AOP 包括以下类型的建议: + +* 建议之前:在连接点之前运行的建议,但不具有阻止执行流继续到连接点的能力(除非抛出异常)。 + +* 返回建议后:在连接点正常完成后运行的建议(例如,如果方法返回时没有抛出异常)。 + +* 抛出建议后:如果方法通过抛出异常退出,则要运行建议。 + +* 在(最后)建议之后:无论连接点以何种方式退出(正常或异常返回),都要运行建议。 + +* 围绕建议:围绕连接点(如方法调用)的建议。这是最有力的建议。建议可以在方法调用之前和之后执行自定义行为。它还负责选择是继续进行连接点,还是通过返回自己的返回值或抛出异常来快捷所建议的方法执行。 + +围绕建议的建议是最普遍的一种建议。由于 Spring AOP 像 AspectJ 一样,提供了一系列完整的建议类型,因此我们建议你使用能够实现所需行为的功能最小的建议类型。例如,如果你只需要用一个方法的返回值来更新一个缓存,那么实现一个返回后的建议比一个环绕建议要好,尽管环绕建议可以完成同样的事情。使用最特定的建议类型可以提供一个更简单的编程模型,并且出错的可能性更小。例如,你不需要在用于 around advice 的`JoinPoint`上调用`proceed()`方法,因此,你不能失败地调用它。 + +所有通知参数都是静态类型的,因此你可以使用适当类型的通知参数(例如,方法执行中返回值的类型),而不是`Object`数组。 + +由切入点匹配的连接点的概念是 AOP 的关键,它区别于仅提供截获的旧技术。切入点使建议能够独立于面向对象的层次结构而具有针对性。例如,你可以将一个提供声明性事务管理的建议应用于一组跨越多个对象的方法(例如服务层中的所有业务操作)。 + +### 5.2. Spring AOP 能力和目标 + +Spring AOP 是用纯 爪哇 实现的。不需要特殊的编译过程。 Spring AOP 不需要控制类装入器层次结构,因此适合在 Servlet 容器或应用服务器中使用。 + +Spring AOP 目前仅支持方法执行连接点(建议在 Spring bean 上执行方法)。未实现字段截取,尽管可以在不破坏核心 Spring AOP API 的情况下添加对字段截取的支持。如果需要建议字段访问和更新连接点,请考虑使用 AspectJ 之类的语言。 + +Spring AOP 对 AOP 的方法与大多数其他 AOP 框架的方法不同。目的不是提供最完整的 AOP 实现(尽管 Spring AOP 相当有能力)。相反,目的是提供 AOP 实现和 Spring IOC 之间的紧密集成,以帮助解决 Enterprise 应用程序中的常见问题。 + +因此,例如, Spring 框架的 AOP 功能通常与 Spring IOC 容器一起使用。方面是通过使用正常的 Bean 定义语法来配置的(尽管这允许强大的“自动代理”功能)。这是与其他 AOP 实现方式的一个关键区别。对于 Spring AOP,你无法轻松或有效地完成某些事情,例如建议非常细粒度的对象(通常是域对象)。AspectJ 是这种情况下的最佳选择。然而,我们的经验是 Spring AOP 为 Enterprise爪哇 应用程序中的大多数问题提供了极好的解决方案,而这些问题是 AOP 能够解决的。 + +Spring AOP 从不努力与 AspectJ 竞争以提供全面的 AOP 解决方案。我们认为, Spring AOP 这样的基于代理的框架和 AspectJ 这样的成熟框架都是有价值的,它们是互补的,而不是竞争的。 Spring 将 Spring AOP 和 IoC 与 AspectJ 无缝集成,以使 AOP 在一致的基于 Spring 的应用程序体系结构中的所有使用成为可能。这种集成不会影响 Spring AOP API 或 AOP Alliance API。 Spring AOP 保持向后兼容。有关 Spring AOP API 的讨论,请参见[接下来的一章](#aop-api)。 + +| |Spring 框架的核心原则之一是非侵犯性。这
是这样一种想法,即你不应该被迫将特定于框架的类和
接口引入到你的业务或域模型中。然而,在某些地方, Spring Framework
确实为你提供了在
代码库中引入 Spring 特定于 Framework 的依赖项的选项。为你提供此类选项的理由是,在某些情况下,
可能更易于阅读或以
的方式编码某些特定的功能。然而, Spring 框架(几乎)总是为你提供选择:你有
做出明智决定的自由,决定哪个选项最适合你的特定使用
情况或场景。

与本章相关的一个这样的选择是选择哪种 AOP 框架(和
哪种 AOP 样式)。你可以选择 AspectJ、 Spring AOP 或两者。你
还可以选择 @AspectJ 注释样式方法或 Spring XML
配置样式方法。本章选择首先介绍
@AspectJ-style 方法,这一事实不应被视为表明 Spring 团队
更喜欢 @AspectJ 注释风格的方法,而不是 Spring XML 配置风格的方法。

参见[Choosing which AOP Declaration Style to Use](#aop-choosing)有关
每种风格的“为什么和为什么”的更完整讨论。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 5.3. AOP 代理 + +Spring AOP 对于 AOP 代理,默认使用标准的 JDK 动态代理。这使得可以代理任何接口(或一组接口)。 + +Spring AOP 还可以使用 CGLIB 代理。这对于代理类而不是接口是必需的。默认情况下,如果业务对象不实现接口,则使用 CGLIB。由于按接口而不是按类编程是一种很好的做法,所以业务类通常实现一个或多个业务接口。在以下情况下,[强制使用 CGlib](#aop-proxying)是可能的:你需要建议一个未在接口上声明的方法,或者你需要将代理对象作为具体类型传递给方法。 + +理解 Spring AOP 是基于代理的这一事实是很重要的。请参阅[Understanding AOP Proxies](#aop-understanding-aop-proxies),以全面了解此实现细节的实际含义。 + +### 5.4.@AspectJ 支持 + +@AspectJ 引用了一种将方面声明为带有注释的常规 爪哇 类的风格。@AspectJ 样式是由[AspectJ project](https://www.eclipse.org/aspectj)作为 AspectJ5 版本的一部分引入的。 Spring 使用 AspectJ 提供的用于切入点解析和匹配的库来解释与 AspectJ5 相同的注释。然而, AOP 运行时仍然是纯的 Spring AOP,并且对 AspectJ 编译器或 Weaver 没有依赖关系。 + +| |使用 AspectJ 编译器和 Weaver 可以使用完整的 AspectJ 语言,
在[Using AspectJ with Spring Applications](#aop-using-aspectj)中进行了讨论。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 5.4.1.启用 @AspectJ 支持 + +要在 Spring 配置中使用 @AspectJ 方面,你需要启用 Spring 支持,以基于 @AspectJ 方面配置 Spring AOP,并根据这些方面是否提供了建议来自动代理 bean。通过自动代理,我们的意思是,如果 Spring 确定 Bean 是由一个或多个方面建议的,则它会自动为该 Bean 生成代理,以拦截方法调用并确保根据需要运行建议。 + +可以通过 XML 或 爪哇 风格的配置来启用 @AspectJ 支持。在这两种情况下,你都需要确保 AspectJ 的`aspectjweaver.jar`库位于应用程序的 Classpath(版本 1.8 或更高版本)上。这个库可以在 AspectJ 发行版的“lib”目录中获得,也可以从 Maven 中央存储库获得。 + +##### 通过 爪哇 配置启用 @AspectJ 支持 + +要使用 爪哇`@Configuration`启用 @AspectJ 支持,请添加`@EnableAspectJAutoProxy`注释,如下例所示: + +爪哇 + +``` +@Configuration +@EnableAspectJAutoProxy +public class AppConfig { + +} +``` + +Kotlin + +``` +@Configuration +@EnableAspectJAutoProxy +class AppConfig +``` + +##### 通过 XML 配置启用 @AspectJ 支持 + +要通过基于 XML 的配置启用 @AspectJ 支持,请使用`aop:aspectj-autoproxy`元素,如下例所示: + +``` + +``` + +这假定你使用[基于 XML 模式的配置](#xsd-schemas)中描述的模式支持。有关如何导入`aop`命名空间中的标记,请参见[the AOP schema](#xsd-schemas-aop)。 + +#### 5.4.2.声明一个方面 + +启用了 @AspectJ 支持后, Spring 将自动检测 Bean 在应用程序上下文中定义的具有 @AspectJ 方面的类(具有`@Aspect`注释)的任何 Bean,并用于配置 Spring AOP。接下来的两个示例展示了一个不太有用的方面所需的最小定义。 + +这两个示例中的第一个示例显示了应用程序上下文中的常规 Bean 定义,该定义指向具有`@Aspect`注释的 Bean 类: + +``` + + + +``` + +两个示例中的第二个示例显示了`NotVeryUsefulAspect`类定义,该定义使用`org.aspectj.lang.annotation.Aspect`注释; + +爪哇 + +``` +package org.xyz; +import org.aspectj.lang.annotation.Aspect; + +@Aspect +public class NotVeryUsefulAspect { + +} +``` + +Kotlin + +``` +package org.xyz + +import org.aspectj.lang.annotation.Aspect; + +@Aspect +class NotVeryUsefulAspect +``` + +方面(用`@Aspect`注释的类)可以具有与任何其他类相同的方法和字段。它们还可以包含切入点、建议和 Introduction(类型间)声明。 + +| |通过组件扫描自动检测方面

你可以在你的 Spring XML 配置中将方面类注册为常规 bean,
通过`@Bean`类中的`@Bean`方法,或者通过
类自动检测它们,或者通过
Classpath 扫描—与任何其他 Spring 管理的 Bean 相同。然而,需要注意的是,在 Classpath 中,“@ 方面”注释对于自动检测是不够的。为了达到
的目的,你需要添加一个单独的`@Component`注释(或者,根据 Spring 的组件扫描仪的规则,添加一个自定义的
原型注释)。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |与其他方面的通知方面?

在 Spring AOP 中,方面本身不能成为来自其他
方面的通知的对象。类上的`@Aspect`注释将其标记为一个方面,因此将
从自动代理中排除。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 5.4.3.声明切入点 + +切入点确定感兴趣的连接点,从而使我们能够控制通知何时运行。 Spring AOP 仅支持 Spring bean 的方法执行连接点,因此可以将切入点视为匹配 Spring bean 上方法的执行。切入点声明有两个部分:一个包含名称和任何参数的签名,以及一个切入点表达式,该表达式确定我们感兴趣的确切方法执行。在 AOP 的 @AspectJ 注释样式中,切入点签名由一个常规方法定义提供,切入点表达式由`@Pointcut`注释表示(充当切入点签名的方法必须具有`void`返回类型)。 + +一个示例可能有助于明确切入点签名和切入点表达式之间的区别。下面的示例定义了一个名为`anyOldTransfer`的切入点,该切入点与任何名为`transfer`的方法的执行相匹配: + +爪哇 + +``` +@Pointcut("execution(* transfer(..))") // the pointcut expression +private void anyOldTransfer() {} // the pointcut signature +``` + +Kotlin + +``` +@Pointcut("execution(* transfer(..))") // the pointcut expression +private fun anyOldTransfer() {} // the pointcut signature +``` + +形成`@Pointcut`注释的值的 pointcut 表达式是一个正则 AspectJ pointcut 表达式。有关 AspectJ 的 PointCut 语言的完整讨论,请参见[AspectJ 编程指南](https://www.eclipse.org/aspectj/doc/released/progguide/index.html)(以及扩展,[AspectJ5 开发者笔记本](https://www.eclipse.org/aspectj/doc/released/adk15notebook/index.html))或 AspectJ 上的一本书(例如 Colyer 等人的*Eclipse AspectJ*,或 Ramnivas Laddad 的*AspectJ 在行动*)。 + +##### 支持的切入点指示符 + +Spring AOP 支持在切入点表达式中使用以下 AspectJ 切入点指示器: + +* `execution`:用于匹配方法执行连接点。这是在使用 Spring AOP 时要使用的主要切入点指示器。 + +* `within`:对某些类型内的连接点的匹配进行限制(在使用 Spring AOP 时在匹配类型内声明的方法的执行)。 + +* `this`:限制匹配到联结点(在使用 Spring AOP 时执行方法),其中 Bean 引用( Spring AOP 代理)是给定类型的实例。 + +* `target`:将匹配限制到连接点(在使用 Spring AOP 时执行方法),其中目标对象(正在代理的应用程序对象)是给定类型的实例。 + +* `args`:限制与连接点的匹配(在使用 Spring AOP 时方法的执行),其中参数是给定类型的实例。 + +* `@target`:限制与连接点的匹配(在使用 Spring AOP 时方法的执行),其中执行对象的类具有给定类型的注释。 + +* `@args`:限制与连接点的匹配(在使用 Spring AOP 时方法的执行),其中传递的实际参数的运行时类型具有给定类型的注释。 + +* `@within`:限制与具有给定注释的类型内的连接点的匹配(在使用 Spring AOP 时,在具有给定注释的类型中声明的方法的执行)。 + +* `@annotation`:将匹配限制为连接点的主题(在 Spring AOP 中运行的方法)具有给定注释的连接点。 + +其他切入点类型 + +完整的 AspectJ PointCut 语言支持在 Spring 中不支持的额外的 PointCut 指示器:,,,,,”4544“/>,”flowr=“<4546”,">。在由 Spring AOP 解释的切入点表达式中使用这些切入点指示符将导致抛出`IllegalArgumentException`。 + +Spring AOP 支持的切入点指示器的集合可以在将来的版本中进行扩展,以支持更多的 AspectJ 切入点指示器。 + +由于 Spring AOP 将匹配限制为仅与方法执行连接点匹配,因此前面对切入点指示器的讨论给出了比 AspectJ 编程指南中所能找到的更窄的定义。此外,AspectJ 本身具有基于类型的语义,并且在执行连接点,`this`和`target`都指向相同的对象:执行方法的对象。 Spring AOP 是一种基于代理的系统,并在代理对象本身(其被绑定到)和代理背后的目标对象(其被绑定到)之间进行区分。 + +| |由于 Spring 的 AOP 框架的基于代理的性质,根据定义,目标对象
内的调用不会被拦截。对于 JDK 代理,只能拦截代理上的公共接口方法
调用。使用 CGlib,对
代理的公共和受保护方法调用将被截获(如果需要,甚至还会截获包可见的方法)。但是,
通过代理进行的公共交互应该始终通过公共签名来设计。
注意,切入点定义通常与任何截获的方法相匹配。
如果切入点严格地说是仅用于公共的,即使在 CGLIB 代理场景中,
通过代理进行的潜在的非公共交互,

如果你的拦截需要在目标
类中包括方法调用甚至构造函数,请考虑使用 Spring 驱动的[原生 AspectJ 编织](#aop-aj-ltw)而不是 Spring 的基于代理的 AOP 框架的
。这构成了具有不同特征的 AOP 使用
的不同模式,因此在做出决定之前,请务必使自己熟悉编织
。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +Spring AOP 还支持名为`bean`的附加 PCD。此 PCD 允许你将连接点的匹配限制为特定的命名 Spring Bean 或命名的 Spring bean 的集合(当使用通配符时)。`bean`pcd 具有以下形式: + +爪哇 + +``` +bean(idOrNameOfBean) +``` + +Kotlin + +``` +bean(idOrNameOfBean) +``` + +`idOrNameOfBean`令牌可以是任何 Spring Bean 的名称。提供了使用`*`字符的有限通配符支持,因此,如果你为 Spring bean 建立了一些命名约定,则可以编写`bean`PCD 表达式来选择它们。与其他切入点指示符的情况一样,`bean`PCD 也可以与`&&`(和)、`||`(或)和`!`(否定)操作符一起使用。 + +| |`bean`PCD 仅在 Spring AOP 中支持,而在
本地 AspectJ 编织中不支持。它是标准 PCD 的 Spring 特定扩展,
AspectJ 定义了该扩展,因此,不可用对于在`@Aspect`模型中声明的方面,

`bean`PCD 在实例级运行(构建在 Spring Bean 名称
概念上)
基于实例的切入点指示器是 Spring 的
基于代理的 AOP 框架及其与 Spring Bean 工厂(其中
工厂)的紧密集成的一种特殊功能。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 结合切入点表达式 + +可以使用`&&,``||`和`!`合并切入点表达式。你也可以按名称引用切入点表达式。下面的示例显示了三个切入点表达式: + +爪哇 + +``` +@Pointcut("execution(public * *(..))") +private void anyPublicOperation() {} (1) + +@Pointcut("within(com.xyz.myapp.trading..*)") +private void inTrading() {} (2) + +@Pointcut("anyPublicOperation() && inTrading()") +private void tradingOperation() {} (3) +``` + +|**1**|如果方法执行连接点表示任何公共方法的执行
,则`anyPublicOperation`匹配。| +|-----|----------------------------------------------------------------------------------------------------------------| +|**2**|如果方法执行在交易模块中,则`inTrading`匹配。| +|**3**|如果方法执行表示
交易模块中的任何公共方法,则`tradingOperation`匹配。| + +Kotlin + +``` +@Pointcut("execution(public * *(..))") +private fun anyPublicOperation() {} (1) + +@Pointcut("within(com.xyz.myapp.trading..*)") +private fun inTrading() {} (2) + +@Pointcut("anyPublicOperation() && inTrading()") +private fun tradingOperation() {} (3) +``` + +|**1**|如果方法执行连接点表示任何公共方法的执行
,则`anyPublicOperation`匹配。| +|-----|----------------------------------------------------------------------------------------------------------------| +|**2**|如果方法执行在交易模块中,则`inTrading`匹配。| +|**3**|如果方法执行表示
交易模块中的任何公共方法,则`tradingOperation`匹配。| + +正如前面所示,从较小的命名组件中构建更复杂的切入点表达式是一种最佳实践。当按名称引用切入点时,将应用普通的 爪哇 可见性规则(你可以看到相同类型的私有切入点,层次结构中的受保护切入点,任何地方的公共切入点,等等)。可见性不影响切入点匹配。 + +##### 共享公共切入点定义 + +在使用 Enterprise 应用程序时,开发人员通常希望从几个方面引用应用程序的模块和特定的操作集。我们建议为此目的定义一个`CommonPointcuts`方面来捕获公共切入点表达式。这样的方面通常类似于以下示例: + +爪哇 + +``` +package com.xyz.myapp; + +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; + +@Aspect +public class CommonPointcuts { + + /** + * A join point is in the web layer if the method is defined + * in a type in the com.xyz.myapp.web package or any sub-package + * under that. + */ + @Pointcut("within(com.xyz.myapp.web..*)") + public void inWebLayer() {} + + /** + * A join point is in the service layer if the method is defined + * in a type in the com.xyz.myapp.service package or any sub-package + * under that. + */ + @Pointcut("within(com.xyz.myapp.service..*)") + public void inServiceLayer() {} + + /** + * A join point is in the data access layer if the method is defined + * in a type in the com.xyz.myapp.dao package or any sub-package + * under that. + */ + @Pointcut("within(com.xyz.myapp.dao..*)") + public void inDataAccessLayer() {} + + /** + * A business service is the execution of any method defined on a service + * interface. This definition assumes that interfaces are placed in the + * "service" package, and that implementation types are in sub-packages. + * + * If you group service interfaces by functional area (for example, + * in packages com.xyz.myapp.abc.service and com.xyz.myapp.def.service) then + * the pointcut expression "execution(* com.xyz.myapp..service.*.*(..))" + * could be used instead. + * + * Alternatively, you can write the expression using the 'bean' + * PCD, like so "bean(*Service)". (This assumes that you have + * named your Spring service beans in a consistent fashion.) + */ + @Pointcut("execution(* com.xyz.myapp..service.*.*(..))") + public void businessService() {} + + /** + * A data access operation is the execution of any method defined on a + * dao interface. This definition assumes that interfaces are placed in the + * "dao" package, and that implementation types are in sub-packages. + */ + @Pointcut("execution(* com.xyz.myapp.dao.*.*(..))") + public void dataAccessOperation() {} + +} +``` + +Kotlin + +``` +package com.xyz.myapp + +import org.aspectj.lang.annotation.Aspect +import org.aspectj.lang.annotation.Pointcut + +@Aspect +class CommonPointcuts { + + /** + * A join point is in the web layer if the method is defined + * in a type in the com.xyz.myapp.web package or any sub-package + * under that. + */ + @Pointcut("within(com.xyz.myapp.web..*)") + fun inWebLayer() { + } + + /** + * A join point is in the service layer if the method is defined + * in a type in the com.xyz.myapp.service package or any sub-package + * under that. + */ + @Pointcut("within(com.xyz.myapp.service..*)") + fun inServiceLayer() { + } + + /** + * A join point is in the data access layer if the method is defined + * in a type in the com.xyz.myapp.dao package or any sub-package + * under that. + */ + @Pointcut("within(com.xyz.myapp.dao..*)") + fun inDataAccessLayer() { + } + + /** + * A business service is the execution of any method defined on a service + * interface. This definition assumes that interfaces are placed in the + * "service" package, and that implementation types are in sub-packages. + * + * If you group service interfaces by functional area (for example, + * in packages com.xyz.myapp.abc.service and com.xyz.myapp.def.service) then + * the pointcut expression "execution(* com.xyz.myapp..service.*.*(..))" + * could be used instead. + * + * Alternatively, you can write the expression using the 'bean' + * PCD, like so "bean(*Service)". (This assumes that you have + * named your Spring service beans in a consistent fashion.) + */ + @Pointcut("execution(* com.xyz.myapp..service.*.*(..))") + fun businessService() { + } + + /** + * A data access operation is the execution of any method defined on a + * dao interface. This definition assumes that interfaces are placed in the + * "dao" package, and that implementation types are in sub-packages. + */ + @Pointcut("execution(* com.xyz.myapp.dao.*.*(..))") + fun dataAccessOperation() { + } + +} +``` + +你可以在需要切入点表达式的任何地方引用在这样的方面中定义的切入点。例如,要使服务层具有事务性,你可以编写以下内容: + +``` + + + + + + + + + +``` + +``和``元素在[Schema-based AOP Support](#aop-schema)中讨论。事务元素在[事务管理](data-access.html#transaction)中讨论。 + +##### 例子 + +Spring AOP 用户很可能最常使用`execution`切入点指示符。执行表达式的格式如下: + +``` + execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) + throws-pattern?) +``` + +除了返回类型模式(前一个代码片段中的“ret-type-pattern”)、名称模式和参数模式以外的所有部分都是可选的。返回类型模式决定了方法的返回类型必须是什么,才能匹配连接点。`*’是最常用的返回类型模式。它匹配任何返回类型。只有当方法返回给定的类型时,完全限定的类型名才匹配。名称模式与方法名匹配。可以使用`*`通配符作为名称模式的全部或部分。如果指定了声明类型模式,请包含一个尾随`.`,以将其连接到名称模式组件。参数模式稍微复杂一些:`()`匹配不接受参数的方法,而`(..)`匹配任意数量(零或更多)的参数。`(*)`模式匹配一个接受任何类型的一个参数的方法。`(*,字符串)` 匹配一个接受两个参数的方法。第一个可以是任何类型,而第二个必须是`String`。有关更多信息,请参阅 AspectJ 编程指南的[语言语义学](https://www.eclipse.org/aspectj/doc/released/progguide/semantics-pointcuts.html)部分。 + +以下示例展示了一些常见的切入点表达式: + +* 任何公开方式的执行: + + ``` + execution(public * *(..)) + ``` + +* 执行名称以`set`开头的任何方法: + + ``` + execution(* set*(..)) + ``` + +* 由`AccountService`接口定义的任何方法的执行: + + ``` + execution(* com.xyz.service.AccountService.*(..)) + ``` + +* 在`service`包中定义的任何方法的执行: + + ``` + execution(* com.xyz.service.*.*(..)) + ``` + +* 在服务包或其子包中定义的任何方法的执行: + + ``` + execution(* com.xyz.service..*.*(..)) + ``` + +* 服务包中的任何连接点(仅在 Spring AOP 中执行方法): + + ``` + within(com.xyz.service.*) + ``` + +* 服务包或其子包中的任何连接点(方法仅在 Spring AOP 中执行): + + ``` + within(com.xyz.service..*) + ``` + +* 代理实现“AccountService”接口的任何连接点(仅在 Spring AOP 中执行方法): + + ``` + this(com.xyz.service.AccountService) + ``` + + | |`this`更常用的是绑定形式。关于如何使代理对象在建议主体中可用,请参见[声明建议](#aop-advice)一节。| + |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------| + +* 目标对象实现`AccountService`接口的任何连接点(仅在 Spring AOP 中执行方法): + + ``` + target(com.xyz.service.AccountService) + ``` + + | |`target`更常用的是绑定形式。关于如何使目标对象在建议主体中可用,请参见[声明建议](#aop-advice)节
。| + |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +* 获取单个参数并且在运行时传递的参数是`Serializable`的任何连接点(仅在 Spring AOP 中执行方法): + + ``` + args(java.io.Serializable) + ``` + + | |`args`更常用的是绑定形式。关于如何使方法参数在建议主体中可用,请参见[声明建议](#aop-advice)节
。| + |---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------| + + 请注意,本例中给出的切入点与`execution(* *(java.io.Serializable))`不同。如果在运行时传递的参数是 `Serializable’,则 ARGS 版本匹配,如果方法签名声明一个类型为`Serializable`的参数,则执行版本匹配。 + +* 目标对象具有“@Transactional”注释的任何连接点(仅在 Spring AOP 中执行方法): + + ``` + @target(org.springframework.transaction.annotation.Transactional) + ``` + + | |也可以在绑定形式中使用`@target`。关于
如何使注释对象在建议正文中可用,请参见[声明建议](#aop-advice)小节。| + |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +* 任何连接点(仅在 Spring AOP 中执行方法),其中目标对象的声明类型具有`@Transactional`注释: + + ``` + @within(org.springframework.transaction.annotation.Transactional) + ``` + + | |也可以在绑定形式中使用`@within`。关于
如何使注释对象在建议正文中可用,请参见[声明建议](#aop-advice)小节。| + |---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +* 任何连接点(仅在 Spring AOP 中执行方法),其中执行方法具有“@transactional”注释: + + ``` + @annotation(org.springframework.transaction.annotation.Transactional) + ``` + + | |也可以在绑定形式中使用`@annotation`。关于如何使注释对象在建议正文中可用,请参见[声明建议](#aop-advice)节
。| + |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +* 任何接受单个参数的连接点(仅在 Spring AOP 中执行方法),其中传递的参数的运行时类型具有`@Classified`注释: + + ``` + @args(com.xyz.security.Classified) + ``` + + | |也可以在绑定形式中使用`@args`。请参阅[声明建议](#aop-advice)部分
如何使注释对象在建议主体中可用。| + |---|------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +* 命名为“TradeService”的 Spring Bean 上的任何连接点(方法仅在 Spring AOP 中执行): + + ``` + bean(tradeService) + ``` + +* 具有与通配符表达式`*Service`匹配的名称的 Spring bean 上的任何连接点(方法仅在 Spring AOP 中执行): + + ``` + bean(*Service) + ``` + +##### 写出好的切入点 + +在编译过程中,AspectJ 处理切入点以优化匹配性能。检查代码并确定每个连接点是否(静态或动态地)匹配给定的切入点是一个昂贵的过程。(动态匹配意味着不能从静态分析中完全确定匹配,并且在代码中进行测试以确定在代码运行时是否存在实际匹配)。在第一次遇到切入点声明时,AspectJ 将其重写为匹配过程的最佳形式。这是什么意思?基本上,切入点被重写为 DNF(析取范式),切入点的组件被排序,以便首先检查那些更便宜的评估组件。这意味着你不必担心理解各种切入点指示器的性能,并且可以在切入点声明中以任何顺序提供它们。 + +然而,AspectJ 只能根据所告知的内容工作。为了获得最佳的匹配性能,你应该考虑他们试图实现的目标,并在定义中尽可能地缩小匹配的搜索空间。现有的指示器自然分为以下三类:Kinded、范围界定和上下文关联: + +* Kinded 指示器选择一种特殊的连接点:`execution’,`get`,`set`,`call`,和`handler`。 + +* 范围指示器选择一组感兴趣的连接点(可能有多种):`within`和`withincode` + +* 上下文指示符基于上下文匹配(并可选绑定):“this”、`target`和`@annotation` + +写得好的切入点应该至少包括前两种类型(Kinded 和 Scoping)。你可以包括上下文指示符,以便根据连接点上下文进行匹配,也可以将该上下文绑定,以便在建议中使用。仅提供 Kinded 指示器或仅提供上下文指示器可以工作,但由于额外的处理和分析,可能会影响编织性能(使用的时间和内存)。范围指示器的匹配速度非常快,使用它们意味着 AspectJ 可以非常快地删除不应进一步处理的连接点组。一个好的切入点应该总是包括一个,如果可能的话。 + +#### 5.4.4.声明建议 + +通知与切入点表达式相关联,并在切入点匹配的方法执行之前、之后或周围运行。PointCut 表达式可以是对已命名的 PointCut 的简单引用,也可以是已声明的 PointCut 表达式。 + +##### 建议之前 + +你可以使用`@Before`注释在一个方面的建议之前声明: + +爪哇 + +``` +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; + +@Aspect +public class BeforeExample { + + @Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation()") + public void doAccessCheck() { + // ... + } +} +``` + +Kotlin + +``` +import org.aspectj.lang.annotation.Aspect +import org.aspectj.lang.annotation.Before + +@Aspect +class BeforeExample { + + @Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation()") + fun doAccessCheck() { + // ... + } +} +``` + +如果我们使用一个就地切入点表达式,我们可以将前面的示例重写为下面的示例: + +爪哇 + +``` +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; + +@Aspect +public class BeforeExample { + + @Before("execution(* com.xyz.myapp.dao.*.*(..))") + public void doAccessCheck() { + // ... + } +} +``` + +Kotlin + +``` +import org.aspectj.lang.annotation.Aspect +import org.aspectj.lang.annotation.Before + +@Aspect +class BeforeExample { + + @Before("execution(* com.xyz.myapp.dao.*.*(..))") + fun doAccessCheck() { + // ... + } +} +``` + +##### 在返回建议后 + +返回通知后,当匹配的方法执行正常返回时运行。你可以使用`@AfterReturning`注释来声明它: + +爪哇 + +``` +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.AfterReturning; + +@Aspect +public class AfterReturningExample { + + @AfterReturning("com.xyz.myapp.CommonPointcuts.dataAccessOperation()") + public void doAccessCheck() { + // ... + } +} +``` + +Kotlin + +``` +import org.aspectj.lang.annotation.Aspect +import org.aspectj.lang.annotation.AfterReturning + +@Aspect +class AfterReturningExample { + + @AfterReturning("com.xyz.myapp.CommonPointcuts.dataAccessOperation()") + fun doAccessCheck() { + // ... + } +} +``` + +| |你可以在同一个方面中拥有多个通知声明(以及其他成员)
。在这些
示例中,我们只显示了一个通知声明,以集中显示每个通知声明的效果。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +有时,你需要在建议主体中访问返回的实际值。可以使用绑定返回值的`@AfterReturning`形式来获得访问权限,如下例所示: + +爪哇 + +``` +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.AfterReturning; + +@Aspect +public class AfterReturningExample { + + @AfterReturning( + pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()", + returning="retVal") + public void doAccessCheck(Object retVal) { + // ... + } +} +``` + +Kotlin + +``` +import org.aspectj.lang.annotation.Aspect +import org.aspectj.lang.annotation.AfterReturning + +@Aspect +class AfterReturningExample { + + @AfterReturning( + pointcut = "com.xyz.myapp.CommonPointcuts.dataAccessOperation()", + returning = "retVal") + fun doAccessCheck(retVal: Any) { + // ... + } +} +``` + +`returning`属性中使用的名称必须与通知方法中的参数名称对应。当方法执行返回时,返回值将作为相应的参数值传递给通知方法。`returning`子句还限制只匹配那些返回指定类型的值的方法执行(在本例中,`Object`,它匹配任何返回值)。 + +请注意,在返回建议后使用时,不可能返回完全不同的参考。 + +##### 在提出建议之后 + +抛出建议后,当匹配的方法执行通过抛出异常退出时,将运行该建议。你可以使用`@AfterThrowing`注释来声明它,如下例所示: + +爪哇 + +``` +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.AfterThrowing; + +@Aspect +public class AfterThrowingExample { + + @AfterThrowing("com.xyz.myapp.CommonPointcuts.dataAccessOperation()") + public void doRecoveryActions() { + // ... + } +} +``` + +Kotlin + +``` +import org.aspectj.lang.annotation.Aspect +import org.aspectj.lang.annotation.AfterThrowing + +@Aspect +class AfterThrowingExample { + + @AfterThrowing("com.xyz.myapp.CommonPointcuts.dataAccessOperation()") + fun doRecoveryActions() { + // ... + } +} +``` + +通常,你希望仅在抛出给定类型的异常时才运行该建议,并且你还经常需要访问建议主体中抛出的异常。你可以使用`throwing`属性来限制匹配(如果需要的话,使用`Throwable`作为异常类型),并将抛出的异常绑定到一个通知参数。下面的示例展示了如何做到这一点: + +爪哇 + +``` +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.AfterThrowing; + +@Aspect +public class AfterThrowingExample { + + @AfterThrowing( + pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()", + throwing="ex") + public void doRecoveryActions(DataAccessException ex) { + // ... + } +} +``` + +Kotlin + +``` +import org.aspectj.lang.annotation.Aspect +import org.aspectj.lang.annotation.AfterThrowing + +@Aspect +class AfterThrowingExample { + + @AfterThrowing( + pointcut = "com.xyz.myapp.CommonPointcuts.dataAccessOperation()", + throwing = "ex") + fun doRecoveryActions(ex: DataAccessException) { + // ... + } +} +``` + +`throwing`属性中使用的名称必须与通知方法中的参数名称对应。当一个方法通过抛出一个异常而退出执行时,该异常将作为相应的参数值传递给通知方法。`throwing`子句还限制只匹配那些抛出指定类型异常的方法执行(本例中为 `DataAccessException’)。 + +| |注意,`@AfterThrowing`并不表示一般的异常处理回调。
具体地说,一个`@AfterThrowing`通知方法只应该从连接点(用户声明的目标方法)本身接收异常
,而不是从伴随的 `@afterreturn` 方法接收异常。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 建议 + +当匹配的方法执行退出时,After(最终)通知将运行。它是通过使用`@After`注释声明的。建议后必须准备好处理正常和异常返回条件。它通常用于释放资源和类似的目的。下面的示例展示了如何在“最终建议”之后使用它: + +爪哇 + +``` +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.After; + +@Aspect +public class AfterFinallyExample { + + @After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()") + public void doReleaseLock() { + // ... + } +} +``` + +Kotlin + +``` +import org.aspectj.lang.annotation.Aspect +import org.aspectj.lang.annotation.After + +@Aspect +class AfterFinallyExample { + + @After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()") + fun doReleaseLock() { + // ... + } +} +``` + +| |注意,AspectJ 中的`@After`通知被定义为“after finally advice”,类似于 try-catch 语句中的 finally 块。它将针对任何结果被调用,
正常返回或从连接点(用户声明的目标方法)抛出的异常,
与`@AfterReturning`相反,后者仅适用于成功的正常返回。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 围绕建议 + +最后一种建议是*周围*建议。围绕建议运行“围绕”一个匹配的方法的执行。它有机会在方法运行之前和之后都进行工作,并确定何时、如何以及即使方法实际运行了。如果你需要以线程安全的方式共享方法执行之前和之后的状态,通常会使用“周围建议”——例如,启动和停止计时器。 + +| |始终使用满足你的要求的功能最小的通知形式。

例如,如果*周围*通知足以满足你的需求,则不要使用*周围*通知。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +通过对带有`@Around`注释的方法进行注释来声明“建议周围”。方法应该声明`Object`作为其返回类型,并且方法的第一个参数必须是`ProceedingJoinPoint`类型。在通知方法的主体中,你必须在`ProceedingJoinPoint`上调用`proceed()`才能运行底层方法。在没有参数的情况下调用`proceed()`将导致调用者的原始参数在调用时被提供给底层方法。对于高级用例,有一个重载的`proceed()`方法的变体,它接受一个参数数组。当调用底层方法时,数组中的值将被用作该方法的参数。 + +| |当使用`Object[]`调用`proceed`时,
的行为与 AspectJ 编译器编译的 around 建议的`proceed`的行为略有不同。对于使用传统 AspectJ 语言编写的 around
通知,传递到 `Proceed’的参数数量必须与传递到 around 通知的参数数量匹配(而不是底层连接点的参数数量
),并且在
给定的参数位置中传递以继续进行的值取代了
值绑定到的实体在连接点处的原始值(不要担心)

Spring 所采用的方法更简单,并且更好地匹配其基于代理的,
仅执行的语义。你只需要在编译为 Spring 编写的 `@AspectJ’方面并使用与 AspectJ编译器和 Weaver 的参数时,才需要注意到这种差异。有一种方法可以在
Spring AOP 和 AspectJ 之间编写 100% 兼容的这样的方面,这在[以下关于建议参数的一节](#aop-ataspectj-advice-proceeding-with-the-call)中进行了讨论。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +around 通知返回的值是该方法的调用者看到的返回值。例如,一个简单的缓存方面可以从缓存返回一个值,如果它有一个值,或者调用`proceed()`(如果没有,则返回该值)。请注意,`proceed`可以在 around 建议的主体内被调用一次,多次,或者根本不调用。所有这些都是合法的。 + +| |如果你将 around advice 方法的返回类型声明为`void`,则`null`将始终返回给调用方,实际上忽略了
of`proceed()`的任何调用的结果。因此,建议使用 around advice 方法声明
类型的`Object`。通知方法通常应该返回从
调用`proceed()`返回的值,即使底层方法具有`void`返回类型。
但是,根据使用情况,通知可以选择返回一个缓存的值、一个包装的值或其他
值。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +下面的示例展示了如何使用“周围建议”: + +爪哇 + +``` +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.ProceedingJoinPoint; + +@Aspect +public class AroundExample { + + @Around("com.xyz.myapp.CommonPointcuts.businessService()") + public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable { + // start stopwatch + Object retVal = pjp.proceed(); + // stop stopwatch + return retVal; + } +} +``` + +Kotlin + +``` +import org.aspectj.lang.annotation.Aspect +import org.aspectj.lang.annotation.Around +import org.aspectj.lang.ProceedingJoinPoint + +@Aspect +class AroundExample { + + @Around("com.xyz.myapp.CommonPointcuts.businessService()") + fun doBasicProfiling(pjp: ProceedingJoinPoint): Any { + // start stopwatch + val retVal = pjp.proceed() + // stop stopwatch + return retVal + } +} +``` + +##### 建议参数 + +Spring 提供完全类型的建议,这意味着你在建议签名中声明所需的参数(正如我们在前面的返回和抛出示例中看到的那样),而不是始终使用`Object[]`数组。在这一节的后面部分,我们将看到如何使参数和其他上下文值可用于建议主体。首先,我们来看看如何编写通用的建议,以了解该建议目前建议的方法。 + +###### 访问当前`JoinPoint` + +任何通知方法都可以声明一个类型为 `org.aspectj.lang.joinpoint’的参数作为其第一个参数。注意,around advice 需要声明类型`ProceedingJoinPoint`的第一个参数,这是`JoinPoint`的子类。 + +`JoinPoint`接口提供了许多有用的方法: + +* `getArgs()`:返回方法参数。 + +* `getThis()`:返回代理对象。 + +* `getTarget()`:返回目标对象。 + +* `getSignature()`:返回所建议方法的描述。 + +* `toString()`:打印所建议的方法的有用描述。 + +有关更多详细信息,请参见[javadoc](https://www.eclipse.org/aspectj/doc/released/runtime-api/org/aspectj/lang/JoinPoint.html)。 + +###### 将参数传递给建议 + +我们已经了解了如何绑定返回值或异常值(在返回后和抛出建议后使用)。要使参数值对建议主体可用,你可以使用`args`的绑定形式。如果在`args`表达式中使用参数名代替类型名,则在调用通知时将相应参数的值作为参数值传递。举个例子应该能更清楚地说明这一点。假设你想建议以`Account`对象作为第一个参数的 DAO 操作的执行,并且你需要访问建议主体中的帐户。你可以写下: + +爪哇 + +``` +@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)") +public void validateAccount(Account account) { + // ... +} +``` + +Kotlin + +``` +@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)") +fun validateAccount(account: Account) { + // ... +} +``` + +切入点表达式的`args(account,..)`部分有两个目的。首先,它只限制匹配那些方法执行,其中该方法至少接受一个参数,并且传递给该参数的参数是`Account`的实例。其次,它通过`account`参数使实际的`Account`对象对通知可用。 + +另一种编写方法是声明一个切入点,该切入点在匹配连接点时“提供”`Account`对象值,然后从通知中引用命名的切入点。其内容如下: + +爪哇 + +``` +@Pointcut("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)") +private void accountDataAccessOperation(Account account) {} + +@Before("accountDataAccessOperation(account)") +public void validateAccount(Account account) { + // ... +} +``` + +Kotlin + +``` +@Pointcut("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)") +private fun accountDataAccessOperation(account: Account) { +} + +@Before("accountDataAccessOperation(account)") +fun validateAccount(account: Account) { + // ... +} +``` + +有关更多详细信息,请参见 AspectJ 编程指南。 + +代理对象(“this”)、目标对象(“target”)和注释(“@within”、“@target”、`@annotation`和`@args`)都可以以类似的方式绑定。接下来的两个示例展示了如何匹配带有`@Auditable`注释的方法的执行并提取审计代码: + +这两个示例中的第一个示例显示了`@Auditable`注释的定义: + +爪哇 + +``` +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Auditable { + AuditCode value(); +} +``` + +Kotlin + +``` +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION) +annotation class Auditable(val value: AuditCode) +``` + +这两个示例中的第二个示例显示了与`@Auditable`方法的执行相匹配的建议: + +爪哇 + +``` +@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)") +public void audit(Auditable auditable) { + AuditCode code = auditable.value(); + // ... +} +``` + +Kotlin + +``` +@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)") +fun audit(auditable: Auditable) { + val code = auditable.value() + // ... +} +``` + +###### 建议参数和泛型 + +Spring AOP 可以处理在类声明和方法参数中使用的泛型。假设你有一个通用类型,如下所示: + +爪哇 + +``` +public interface Sample { + void sampleGenericMethod(T param); + void sampleGenericCollectionMethod(Collection param); +} +``` + +Kotlin + +``` +interface Sample { + fun sampleGenericMethod(param: T) + fun sampleGenericCollectionMethod(param: Collection) +} +``` + +可以通过将通知参数与要截取方法的参数类型绑定,将方法类型的截取限制为某些参数类型: + +爪哇 + +``` +@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)") +public void beforeSampleMethod(MyType param) { + // Advice implementation +} +``` + +Kotlin + +``` +@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)") +fun beforeSampleMethod(param: MyType) { + // Advice implementation +} +``` + +这种方法不适用于泛型集合。因此,你不能如下定义切入点: + +爪哇 + +``` +@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)") +public void beforeSampleMethod(Collection param) { + // Advice implementation +} +``` + +Kotlin + +``` +@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)") +fun beforeSampleMethod(param: Collection) { + // Advice implementation +} +``` + +要实现此工作,我们必须检查集合的每个元素,这是不合理的,因为我们也无法决定如何一般地处理`null`值。要实现类似的功能,你必须将参数键入`Collection`,并手动检查元素的类型。 + +###### 确定参数名称 + +通知调用中的参数绑定依赖于将切入点表达式中使用的名称与通知和切入点方法签名中声明的参数名称进行匹配。参数名称不能通过 爪哇 反射获得,因此 Spring AOP 使用以下策略来确定参数名称: + +* 如果参数名称是由用户显式指定的,则使用指定的参数名称。通知和切入点注释都有一个可选的`argNames`属性,你可以使用该属性指定带注释方法的参数名称。这些参数名称在运行时可用。下面的示例展示了如何使用`argNames`属性: + +爪哇 + +``` +@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)", + argNames="bean,auditable") +public void audit(Object bean, Auditable auditable) { + AuditCode code = auditable.value(); + // ... use code and bean +} +``` + +Kotlin + +``` +@Before(value = "com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)", argNames = "bean,auditable") +fun audit(bean: Any, auditable: Auditable) { + val code = auditable.value() + // ... use code and bean +} +``` + +如果第一个参数是`JoinPoint`、`ProceedingJoinPoint`或 `joinpoint.staticpart`type,则可以从`argNames`属性的值中省略参数的名称。例如,如果你修改前面的通知以接收连接点对象,则`argNames`属性不需要包括它: + +爪哇 + +``` +@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)", + argNames="bean,auditable") +public void audit(JoinPoint jp, Object bean, Auditable auditable) { + AuditCode code = auditable.value(); + // ... use code, bean, and jp +} +``` + +Kotlin + +``` +@Before(value = "com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)", argNames = "bean,auditable") +fun audit(jp: JoinPoint, bean: Any, auditable: Auditable) { + val code = auditable.value() + // ... use code, bean, and jp +} +``` + +对`JoinPoint`、`proceedingjoinpoint’和`JoinPoint.StaticPart`类型的第一个参数的特殊处理对于不收集任何其他连接点上下文的建议实例特别方便。在这种情况下,你可以省略`argNames`属性。例如,以下建议不需要声明`argNames`属性: + +爪哇 + +``` +@Before("com.xyz.lib.Pointcuts.anyPublicMethod()") +public void audit(JoinPoint jp) { + // ... use jp +} +``` + +Kotlin + +``` +@Before("com.xyz.lib.Pointcuts.anyPublicMethod()") +fun audit(jp: JoinPoint) { + // ... use jp +} +``` + +* 使用`argNames`属性有点笨拙,因此,如果没有指定`argNames`属性, Spring AOP 将查看该类的调试信息,并尝试从局部变量表中确定参数名称。只要类已经编译了调试信息(“-g:vars”至少),就存在此信息。使用此标志进行编译的结果是:(1)你的代码稍微容易理解(逆向工程),(2)类文件的大小稍微大一些(通常不重要),(3)你的编译器不应用删除未使用的局部变量的优化。换句话说,你应该不会遇到任何困难,通过建设与这一标志。 + + | |如果一个 @AspectJ 方面已经由 AspectJ 编译器编译,甚至
都没有调试信息,那么你就不需要添加`argNames`属性,因为编译器
保留了所需的信息。| + |---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +* Spring AOP 如果在没有必要的调试信息的情况下编译了代码,则尝试推断绑定变量到参数的配对(例如,如果在切入点表达式中只绑定了一个变量,并且通知方法只使用了一个参数,则该配对是显而易见的)。如果给定可用信息,变量的绑定是模棱两可的,则抛出一个`AmbiguousBindingException`。 + +* 如果上述所有策略都失败,则抛出一个`IllegalArgumentException`。 + +###### 继续进行辩论 + +我们在前面提到,我们将描述如何使用在 Spring AOP 和 AspectJ 上一致工作的参数编写`proceed`调用。解决方案是确保通知签名按顺序绑定每个方法参数。下面的示例展示了如何做到这一点: + +爪哇 + +``` +@Around("execution(List find*(..)) && " + + "com.xyz.myapp.CommonPointcuts.inDataAccessLayer() && " + + "args(accountHolderNamePattern)") +public Object preProcessQueryPattern(ProceedingJoinPoint pjp, + String accountHolderNamePattern) throws Throwable { + String newPattern = preProcess(accountHolderNamePattern); + return pjp.proceed(new Object[] {newPattern}); +} +``` + +Kotlin + +``` +@Around("execution(List find*(..)) && " + + "com.xyz.myapp.CommonPointcuts.inDataAccessLayer() && " + + "args(accountHolderNamePattern)") +fun preProcessQueryPattern(pjp: ProceedingJoinPoint, + accountHolderNamePattern: String): Any { + val newPattern = preProcess(accountHolderNamePattern) + return pjp.proceed(arrayOf(newPattern)) +} +``` + +在许多情况下,无论如何都要进行这种绑定(如前面的示例)。 + +##### 建议订购 + +当多个建议都希望在同一个连接点运行时会发生什么情况? Spring AOP 遵循与 AspectJ 相同的优先规则来确定通知执行的顺序。优先级最高的建议首先“在途中”运行(因此,如果给出了两条之前的建议,优先级最高的建议将首先运行)。在从联结点“退出”时,最高优先级的通知最后运行(因此,给定两条后通知,具有最高优先级的通知将运行第二条)。 + +当在不同方面中定义的两个通知都需要在相同的连接点上运行时,除非你另有指定,否则执行的顺序是未定义的。你可以通过指定优先级来控制执行的顺序。这是通过在 Aspect 类中实现`org.springframework.core.Ordered`接口或使用`@Order`注释来以正常的方式完成的。给定两个方面,从`Ordered.getOrder()`返回较低的值(或注释值)的方面具有较高的优先权。 + +| |特定方面的每种不同的通知类型在概念上都意味着将
直接应用到连接点。因此,`@AfterThrowing`通知方法不是
应该从随附的`@After`/`@afterreturn` 方法接收异常的方法。

在 Spring framework5.2.7 中,在相同的`@Aspect`类中定义的通知方法,如果
需要在相同的连接点上运行,则根据其在
中的通知类型,按照以下顺序,从最高优先级到最低优先级:`@Around`,`@After`,<@afterreturn`,。但是,请注意,在相同的方面中,当使用任何`@AfterReturning`或`@AfterThrowing`通知方法
时,在 AspectJ 的`@After`的“after finally advice”语义之后,将有效地调用
通知方法。
当两个相同类型的通知(例如,
)时,两个`@After`通知方法)
在同一个`@Aspect`类中定义的
类都需要在相同的连接点上运行,其排序
是未定义的(因为没有办法通过
反射来检索源代码的声明顺序,用于 javac 编译的类)。考虑在每个`@Aspect`类中的每个连接点将这样的通知方法折叠成一个
通知方法,或者将这些通知片段重构为
单独的`@Aspect`类,你可以通过`Ordered`或`@Order`在方面级别订购这些类。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 5.4.5.介绍 + +引入(在 AspectJ 中称为类型间声明)使方面能够声明建议的对象实现了给定的接口,并代表这些对象提供了该接口的实现。 + +你可以使用`@DeclareParents`注释进行介绍。此注释用于声明匹配的类型有一个新的父类型(因此命名)。例如,给定一个名为`UsageTracked`的接口和一个名为 `DefaultUsageTracked’的接口的实现,以下方面声明服务接口的所有实现者也实现`UsageTracked`接口(例如,通过 JMX 进行统计): + +爪哇 + +``` +@Aspect +public class UsageTracking { + + @DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class) + public static UsageTracked mixin; + + @Before("com.xyz.myapp.CommonPointcuts.businessService() && this(usageTracked)") + public void recordUsage(UsageTracked usageTracked) { + usageTracked.incrementUseCount(); + } + +} +``` + +Kotlin + +``` +@Aspect +class UsageTracking { + + companion object { + @DeclareParents(value = "com.xzy.myapp.service.*+", defaultImpl = DefaultUsageTracked::class) + lateinit var mixin: UsageTracked + } + + @Before("com.xyz.myapp.CommonPointcuts.businessService() && this(usageTracked)") + fun recordUsage(usageTracked: UsageTracked) { + usageTracked.incrementUseCount() + } +} +``` + +要实现的接口由注释字段的类型决定。`@DeclareParents`注释的 `value’属性是一个 AspectJ 类型模式。匹配类型的任何 Bean 实现`UsageTracked`接口。注意,在前面示例的 before 通知中,服务 bean 可以直接用作`UsageTracked`接口的实现。如果以编程方式访问 Bean,你将编写以下内容: + +爪哇 + +``` +UsageTracked usageTracked = (UsageTracked) context.getBean("myService"); +``` + +Kotlin + +``` +val usageTracked = context.getBean("myService") as UsageTracked +``` + +#### 5.4.6.方面实例化模型 + +| |这是一个高级话题。如果你刚开始使用 AOP,则可以安全地跳过
它,直到稍后。| +|---|---------------------------------------------------------------------------------------------------------| + +默认情况下,在应用程序上下文中,每个方面都有一个实例。AspectJ 将其称为单例实例化模型。可以用替代的生命周期来定义方面。 Spring 目前不支持 AspectJ 的`perthis`和`pertarget`实例化模型;`percflow`、`percflowbelow`和`pertypewithin`。 + +你可以通过在`@Aspect`注释中指定`perthis`子句来声明`perthis`方面。考虑以下示例: + +爪哇 + +``` +@Aspect("perthis(com.xyz.myapp.CommonPointcuts.businessService())") +public class MyAspect { + + private int someState; + + @Before("com.xyz.myapp.CommonPointcuts.businessService()") + public void recordServiceUsage() { + // ... + } +} +``` + +Kotlin + +``` +@Aspect("perthis(com.xyz.myapp.CommonPointcuts.businessService())") +class MyAspect { + + private val someState: Int = 0 + + @Before("com.xyz.myapp.CommonPointcuts.businessService()") + fun recordServiceUsage() { + // ... + } +} +``` + +在前面的示例中,`perthis`子句的效果是,为执行业务服务的每个唯一服务对象创建一个方面实例(每个在切入点表达式匹配的连接点绑定到`this`的唯一对象)。第一次在服务对象上调用方法时,将创建方面实例。当服务对象超出范围时,方面就超出了范围。在创建方面实例之前,它中的任何建议都不会运行。一旦创建了方面实例,其中声明的通知就会在匹配的连接点上运行,但仅当服务对象是与该方面相关联的对象时才会运行。有关`per`子句的更多信息,请参见 AspectJ 编程指南。 + +`pertarget`实例化模型的工作方式与`perthis`完全相同,但它在匹配的连接点上为每个唯一的目标对象创建一个方面实例。 + +#### 5.4.7. AOP 例 + +既然你已经了解了所有组成部分是如何工作的,那么我们可以将它们组合在一起来做一些有用的事情。 + +业务服务的执行有时会由于并发性问题(例如,死锁失败者)而失败。如果该操作被重试,则很可能在下一次尝试中成功。对于在这种情况下(幂等运算不需要返回给用户以解决冲突)适合重试的业务服务,我们希望透明地重试该操作,以避免客户端看到“悲观锁定失败异常”。这是一个明显跨越服务层中多个服务的需求,因此非常适合通过一个方面来实现。 + +因为我们想要重试操作,所以我们需要使用 around advice,这样我们就可以多次调用`proceed`。下面的清单展示了基本方面的实现: + +爪哇 + +``` +@Aspect +public class ConcurrentOperationExecutor implements Ordered { + + private static final int DEFAULT_MAX_RETRIES = 2; + + private int maxRetries = DEFAULT_MAX_RETRIES; + private int order = 1; + + public void setMaxRetries(int maxRetries) { + this.maxRetries = maxRetries; + } + + public int getOrder() { + return this.order; + } + + public void setOrder(int order) { + this.order = order; + } + + @Around("com.xyz.myapp.CommonPointcuts.businessService()") + public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable { + int numAttempts = 0; + PessimisticLockingFailureException lockFailureException; + do { + numAttempts++; + try { + return pjp.proceed(); + } + catch(PessimisticLockingFailureException ex) { + lockFailureException = ex; + } + } while(numAttempts <= this.maxRetries); + throw lockFailureException; + } +} +``` + +Kotlin + +``` +@Aspect +class ConcurrentOperationExecutor : Ordered { + + private val DEFAULT_MAX_RETRIES = 2 + private var maxRetries = DEFAULT_MAX_RETRIES + private var order = 1 + + fun setMaxRetries(maxRetries: Int) { + this.maxRetries = maxRetries + } + + override fun getOrder(): Int { + return this.order + } + + fun setOrder(order: Int) { + this.order = order + } + + @Around("com.xyz.myapp.CommonPointcuts.businessService()") + fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any { + var numAttempts = 0 + var lockFailureException: PessimisticLockingFailureException + do { + numAttempts++ + try { + return pjp.proceed() + } catch (ex: PessimisticLockingFailureException) { + lockFailureException = ex + } + + } while (numAttempts <= this.maxRetries) + throw lockFailureException + } +} +``` + +注意,方面实现了`Ordered`接口,这样我们就可以将方面的优先级设置为高于事务通知的优先级(每次重试时,我们都希望有一个新的事务)。`maxRetries`和`order`属性都是由 Spring 配置的。主要的动作发生在`doConcurrentOperation`周围的建议中。请注意,目前,我们将重试逻辑应用于每个`businessService()`。我们尝试继续,如果`PessimisticLockingFailureException`失败,我们会再试一次,除非我们已经用尽了所有的重试尝试。 + +相应的 Spring 配置如下: + +``` + + + + + + +``` + +为了细化方面,使其仅重试幂等运算,我们可以定义以下 ` 幂等’注释: + +爪哇 + +``` +@Retention(RetentionPolicy.RUNTIME) +public @interface Idempotent { + // marker annotation +} +``` + +Kotlin + +``` +@Retention(AnnotationRetention.RUNTIME) +annotation class Idempotent// marker annotation +``` + +然后,我们可以使用注释来注释服务操作的实现。对只重试幂等运算的方面的更改涉及细化切入点表达式,使只有`@Idempotent`运算匹配,如下所示: + +爪哇 + +``` +@Around("com.xyz.myapp.CommonPointcuts.businessService() && " + + "@annotation(com.xyz.myapp.service.Idempotent)") +public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable { + // ... +} +``` + +Kotlin + +``` +@Around("com.xyz.myapp.CommonPointcuts.businessService() && " + + "@annotation(com.xyz.myapp.service.Idempotent)") +fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any { + // ... +} +``` + +### 5.5.基于模式的 AOP 支持 + +如果你更喜欢基于 XML 的格式, Spring 还提供了使用`aop`名称空间标记来定义方面的支持。支持与使用 @AspectJ 样式时完全相同的切入点表达式和通知类型。因此,在本节中,我们将重点讨论该语法,并请读者参考上一节中的讨论([@AspectJ 支持](#aop-ataspectj)),以了解如何编写切入点表达式和绑定建议参数。 + +要使用本节中描述的 AOP 命名空间标记,你需要导入[基于 XML 模式的配置](#xsd-schemas)中描述的 ` Spring- AOP ` 模式。有关如何导入`aop`命名空间中的标记,请参见[the AOP schema](#xsd-schemas-aop)。 + +在你的 Spring 配置中,所有方面和顾问元素都必须放置在``元素中(在应用程序上下文配置中,可以有多个``元素)。``元素可以包含 pointcut、advisor 和方面元素(请注意,这些元素必须按顺序声明)。 + +| |配置的``样式大量使用了 Spring 的[auto-proxying](#aop-autoproxy)机制。如果你已经通过使用“BeannaMeAutoProxyCreator”或类似的方式使用了显式自动代理,那么这可能会导致问题(例如建议
不被编织)。推荐的使用模式是
只使用``样式,或者只使用`AutoProxyCreator`样式和
样式,永远不要混合它们。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 5.5.1.声明一个方面 + +当你使用模式支持时,一个方面是在 Spring 应用程序上下文中定义为 Bean 的常规 爪哇 对象。在对象的字段和方法中捕获状态和行为,在 XML 中捕获切入点和通知信息。 + +你可以通过使用``元素来声明一个方面,并通过使用`ref`属性来引用 backing Bean,如下例所示: + +``` + + + ... + + + + + ... + +``` + +支持方面的 Bean(在这种情况下是 `Abean’)当然可以像任何其他 Spring Bean 一样被配置和注入依赖关系。 + +#### 5.5.2.声明切入点 + +你可以在``元素中声明一个命名的切入点,让切入点定义在多个方面和顾问之间共享。 + +表示服务层中任何业务服务的执行的切入点可以定义如下: + +``` + + + + + +``` + +请注意,PointCut 表达式本身使用与[@AspectJ 支持](#aop-ataspectj)中描述的相同的 AspectJ PointCut 表达式语言。如果使用基于模式的声明样式,则可以引用在切入点表达式中的类型(@Aspects)中定义的命名切入点。定义上述切入点的另一种方法如下: + +``` + + + + + +``` + +假设你有`CommonPointcuts`中描述的[共享公共切入点定义](#aop-common-pointcuts)方面。 + +然后在一个方面中声明一个切入点与声明一个顶级切入点非常相似,如下例所示: + +``` + + + + + + + ... + + + +``` + +与 @AspectJ 方面几乎相同,使用基于模式的定义样式声明的切入点可以收集连接点上下文。例如,下面的切入点收集`this`对象作为连接点上下文,并将其传递给通知: + +``` + + + + + + + + + ... + + + +``` + +必须声明通知,以通过包括匹配名称的参数来接收收集的连接点上下文,如下所示: + +爪哇 + +``` +public void monitor(Object service) { + // ... +} +``` + +Kotlin + +``` +fun monitor(service: Any) { + // ... +} +``` + +在组合 PointCut 子表达式时,`&&`在 XML 文档中很难处理,因此可以分别使用`and`、`or`和`not`关键字来代替`&&`、`||’和`!`。例如,前面的切入点可以更好地编写如下: + +``` + + + + + + + + + ... + + +``` + +请注意,以这种方式定义的切入点由其 XML`id`引用,并且不能用作命名的切入点来形成复合切入点。因此,基于模式的定义样式中的命名切入点支持比 @AspectJ 样式提供的支持更有限。 + +#### 5.5.3.声明建议 + +基于模式的 AOP 支持使用与 @AspectJ 样式相同的五种建议,并且它们具有完全相同的语义。 + +##### 建议之前 + +在匹配的方法执行之前运行建议之前。通过使用``元素,它在 `` 内声明,如下例所示: + +``` + + + + + ... + + +``` + +这里,`dataAccessOperation`是在顶部定义的切入点的`id`级别。要定义 PointCut 内联,将`pointcut-ref`属性替换为`pointcut`属性,如下所示: + +``` + + + + + ... + +``` + +正如我们在讨论 @AspectJ 风格时所指出的,使用命名切入点可以显著提高代码的可读性。 + +`method`属性标识了提供建议主体的方法。该方法必须为包含该建议的方面元素所引用的 Bean 定义。在执行数据访问操作(由切入点表达式匹配的方法执行连接点)之前, Bean 方面的`doAccessCheck`方法被调用。 + +##### 在返回建议后 + +返回通知后,当匹配的方法执行正常完成时运行。它是在``中声明的,与通知之前的方式相同。下面的示例展示了如何声明它: + +``` + + + + + ... + +``` + +正如在 @AspectJ 样式中一样,你可以在建议主体中获得返回值。为此,使用`returning`属性指定返回值应传递到的参数的名称,如下例所示: + +``` + + + + + ... + +``` + +`doAccessCheck`方法必须声明一个名为`retVal`的参数。该参数的类型以与`@AfterReturning`相同的方式限制匹配。例如,你可以如下声明方法签名: + +爪哇 + +``` +public void doAccessCheck(Object retVal) {... +``` + +Kotlin + +``` +fun doAccessCheck(retVal: Any) {... +``` + +##### 在提出建议之后 + +抛出建议后,当匹配的方法执行通过抛出异常退出时,将运行该建议。它是通过使用`after-throwing`元素在``内声明的,如下例所示: + +``` + + + + + ... + +``` + +正如在 @AspectJ 样式中一样,你可以在建议主体中获得抛出的异常。要做到这一点,请使用`throwing`属性指定应向其传递异常的参数的名称,如下例所示: + +``` + + + + + ... + +``` + +`doRecoveryActions`方法必须声明一个名为`dataAccessEx`的参数。该参数的类型以与“@afterthrowing”相同的方式限制匹配。例如,方法签名可以声明如下: + +爪哇 + +``` +public void doRecoveryActions(DataAccessException dataAccessEx) {... +``` + +Kotlin + +``` +fun doRecoveryActions(dataAccessEx: DataAccessException) {... +``` + +##### 在(最终)建议之后 + +无论匹配的方法执行如何退出,在(最终)通知之后都会运行。你可以使用`after`元素来声明它,如下例所示: + +``` + + + + + ... + +``` + +##### 围绕建议 + +最后一种建议是*周围*建议。围绕建议运行“围绕”一个匹配的方法的执行。它有机会在方法运行之前和之后都进行工作,并确定何时、如何以及即使方法实际运行了。如果你需要以线程安全的方式共享方法执行之前和之后的状态,通常会使用“周围建议”——例如,启动和停止计时器。 + +| |始终使用功能最小的通知形式来满足你的要求。

例如,如果*周围*通知足以满足你的需求,则不要使用*周围*通知。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +你可以使用`aop:around`元素声明通知周围的内容。通知方法应该声明`Object`作为其返回类型,并且方法的第一个参数必须是类型`ProceedingJoinPoint`。在 advice 方法的主体中,你必须在`ProceedingJoinPoint`上调用 `proceed()’,才能运行底层方法。在没有参数的情况下调用`proceed()`将导致调用者的原始参数在调用时被提供给底层方法。对于高级用例,有一个重载的`proceed()`方法的变体,它接受一个参数数组。当调用底层方法时,数组中的值将被用作该方法的参数。关于使用`Object[]`调用 `proceed’的注释,请参见[Around Advice](#aop-ataspectj-around-advice)。 + +下面的示例展示了如何在 XML 中声明有关建议的内容: + +``` + + + + + ... + +``` + +`doBasicProfiling`通知的实现可以与 @AspectJ 示例中的实现完全相同(当然要减去注释),如下例所示: + +爪哇 + +``` +public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable { + // start stopwatch + Object retVal = pjp.proceed(); + // stop stopwatch + return retVal; +} +``` + +Kotlin + +``` +fun doBasicProfiling(pjp: ProceedingJoinPoint): Any { + // start stopwatch + val retVal = pjp.proceed() + // stop stopwatch + return pjp.proceed() +} +``` + +##### 建议参数 + +基于模式的声明风格支持完全类型的通知,其方式与 @AspectJ 支持中所描述的相同——通过将切入点参数按名称与通知方法参数进行匹配。详见[建议参数](#aop-ataspectj-advice-params)。如果你希望显式地指定建议方法的参数名称(而不是依赖于前面描述的检测策略),那么你可以通过使用建议元素的`arg-names`属性来这样做,其处理方式与通知注释中的`argNames`属性相同(如[确定参数名称](#aop-ataspectj-advice-params-names)中所述)。下面的示例展示了如何在 XML 中指定参数名称: + +``` + +``` + +`arg-names`属性接受以逗号分隔的参数名列表。 + +下面稍微更详细的基于 XSD 的方法的示例展示了一些与多个强类型参数一起使用的建议: + +爪哇 + +``` +package x.y.service; + +public interface PersonService { + + Person getPerson(String personName, int age); +} + +public class DefaultPersonService implements PersonService { + + public Person getPerson(String name, int age) { + return new Person(name, age); + } +} +``` + +Kotlin + +``` +package x.y.service + +interface PersonService { + + fun getPerson(personName: String, age: Int): Person +} + +class DefaultPersonService : PersonService { + + fun getPerson(name: String, age: Int): Person { + return Person(name, age) + } +} +``` + +接下来是方面。请注意,`profile(..)`方法接受许多强类型参数,其中第一个参数恰好是用于继续方法调用的连接点。该参数的存在表明 `profile’将用作`around`通知,如下例所示: + +爪哇 + +``` +package x.y; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.springframework.util.StopWatch; + +public class SimpleProfiler { + + public Object profile(ProceedingJoinPoint call, String name, int age) throws Throwable { + StopWatch clock = new StopWatch("Profiling for '" + name + "' and '" + age + "'"); + try { + clock.start(call.toShortString()); + return call.proceed(); + } finally { + clock.stop(); + System.out.println(clock.prettyPrint()); + } + } +} +``` + +Kotlin + +``` +import org.aspectj.lang.ProceedingJoinPoint +import org.springframework.util.StopWatch + +class SimpleProfiler { + + fun profile(call: ProceedingJoinPoint, name: String, age: Int): Any { + val clock = StopWatch("Profiling for '$name' and '$age'") + try { + clock.start(call.toShortString()) + return call.proceed() + } finally { + clock.stop() + println(clock.prettyPrint()) + } + } +} +``` + +最后,下面的示例 XML 配置会影响针对特定连接点的上述建议的执行: + +``` + + + + + + + + + + + + + + + + + + + +``` + +考虑以下驱动程序脚本: + +爪哇 + +``` +import org.springframework.beans.factory.BeanFactory; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import x.y.service.PersonService; + +public final class Boot { + + public static void main(final String[] args) throws Exception { + BeanFactory ctx = new ClassPathXmlApplicationContext("x/y/plain.xml"); + PersonService person = (PersonService) ctx.getBean("personService"); + person.getPerson("Pengo", 12); + } +} +``` + +Kotlin + +``` +fun main() { + val ctx = ClassPathXmlApplicationContext("x/y/plain.xml") + val person = ctx.getBean("personService") as PersonService + person.getPerson("Pengo", 12) +} +``` + +有了这样的引导类,我们将获得类似于以下标准输出的输出: + +``` +StopWatch 'Profiling for 'Pengo' and '12': running time (millis) = 0 +----------------------------------------- +ms % Task name +----------------------------------------- +00000 ? execution(getFoo) +``` + +##### 建议订购 + +当多条通知需要在相同的连接点(执行方法)上运行时,排序规则如[Advice Ordering](#aop-ataspectj-advice-ordering)中所述。通过``元素中的`order`属性,或者通过向支持方面的 Bean 添加`@Order`注释,或者通过使 Bean 实现`Ordered`接口,确定方面之间的优先级。 + +| |与在相同的`@Aspect`类中定义的通知方法的优先规则相反,当在相同的``元素中定义的两条通知都需要
在相同的连接点上运行时,优先级由在附件``元素中声明通知
元素的顺序决定,从最高到最低
优先级。

例如,给定一个`around`通知和一个`before`通知,该通知在相同的 `元素中定义,该元素适用于相同的连接点,以确保`around`通知具有比`before`通知更高的优先级,``元素必须是在``元素之前声明的
元素。

作为一般的经验法则,如果你发现在同一个``元素中有多个定义
的建议适用于相同的连接点,考虑将
这样的建议方法在每个接入点的``元素
中折叠成一个建议方法,或者将建议片段重构为单独的``元素,你可以在方面级别订购
元素。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 5.5.4.介绍 + +介绍(在 AspectJ 中称为类型间声明)让一个方面声明被建议的对象实现一个给定的接口,并代表这些对象提供该接口的实现。 + +你可以通过在`aop:aspect`中使用`aop:declare-parents`元素来进行介绍。你可以使用`aop:declare-parents`元素来声明匹配的类型有一个新的父类型(因此命名)。例如,给定一个名为`UsageTracked`的接口和一个名为 `DefaultUsageTracked’的接口的实现,以下方面声明服务接口的所有实现者也实现`UsageTracked`接口。(例如,为了通过 JMX 公开统计信息。 + +``` + + + + + + + +``` + +然后,支持`usageTracking` Bean 的类将包含以下方法: + +爪哇 + +``` +public void recordUsage(UsageTracked usageTracked) { + usageTracked.incrementUseCount(); +} +``` + +Kotlin + +``` +fun recordUsage(usageTracked: UsageTracked) { + usageTracked.incrementUseCount() +} +``` + +要实现的接口由`implement-interface`属性决定。`types-matching`属性的值是 AspectJ 类型模式。匹配类型的任何 Bean 实现`UsageTracked`接口。注意,在前面示例的 before 通知中,服务 bean 可以直接用作`UsageTracked`接口的实现。要以编程方式访问 Bean,你可以编写以下内容: + +爪哇 + +``` +UsageTracked usageTracked = (UsageTracked) context.getBean("myService"); +``` + +Kotlin + +``` +val usageTracked = context.getBean("myService") as UsageTracked +``` + +#### 5.5.5.方面实例化模型 + +对于模式定义的方面,唯一支持的实例化模型是单例模型。其他实例化模型可能会在未来的版本中得到支持。 + +#### 5.5.6.顾问 + +“顾问”的概念来自 Spring 中定义的 AOP 支持,在 AspectJ 中没有直接的等价物。顾问就像是一个独立的小方面,只有一条建议。通知本身由 Bean 表示,并且必须实现[Advice Types in Spring](#aop-api-advice-types)中描述的通知接口之一。顾问可以利用 AspectJ 切入点表达式。 + +Spring 支持带有``元素的 advisor 概念。你最常看到它与事务性建议一起使用,在 Spring 中,事务性建议也有自己的名称空间支持。下面的示例展示了一个顾问: + +``` + + + + + + + + + + + + + +``` + +除了前面示例中使用的`pointcut-ref`属性外,你还可以使用 `pointcut’属性来内联地定义一个 pointcut 表达式。 + +要定义 advisor 的优先级,以便该建议可以参与排序,请使用`order`属性来定义 advisor 的`Ordered`值。 + +#### 5.5.7. AOP 模式示例 + +本节展示了在使用模式支持重写时,来自[An AOP Example](#aop-ataspectj-example)的并发锁定失败重试示例的外观。 + +业务服务的执行有时会由于并发性问题(例如,死锁失败者)而失败。如果该操作被重试,则很可能在下一次尝试中成功。对于在这种情况下(幂等运算不需要返回给用户以解决冲突)适合重试的业务服务,我们希望透明地重试该操作,以避免客户端看到“悲观锁定失败异常”。这是一个明显跨越服务层中多个服务的需求,因此非常适合通过一个方面来实现。 + +因为我们想要重试操作,所以我们需要使用 around advice,这样我们就可以多次调用`proceed`。下面的清单展示了基本的方面实现(这是一个使用模式支持的常规 Java 类): + +Java + +``` +public class ConcurrentOperationExecutor implements Ordered { + + private static final int DEFAULT_MAX_RETRIES = 2; + + private int maxRetries = DEFAULT_MAX_RETRIES; + private int order = 1; + + public void setMaxRetries(int maxRetries) { + this.maxRetries = maxRetries; + } + + public int getOrder() { + return this.order; + } + + public void setOrder(int order) { + this.order = order; + } + + public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable { + int numAttempts = 0; + PessimisticLockingFailureException lockFailureException; + do { + numAttempts++; + try { + return pjp.proceed(); + } + catch(PessimisticLockingFailureException ex) { + lockFailureException = ex; + } + } while(numAttempts <= this.maxRetries); + throw lockFailureException; + } +} +``` + +Kotlin + +``` +class ConcurrentOperationExecutor : Ordered { + + private val DEFAULT_MAX_RETRIES = 2 + + private var maxRetries = DEFAULT_MAX_RETRIES + private var order = 1 + + fun setMaxRetries(maxRetries: Int) { + this.maxRetries = maxRetries + } + + override fun getOrder(): Int { + return this.order + } + + fun setOrder(order: Int) { + this.order = order + } + + fun doConcurrentOperation(pjp: ProceedingJoinPoint): Any { + var numAttempts = 0 + var lockFailureException: PessimisticLockingFailureException + do { + numAttempts++ + try { + return pjp.proceed() + } catch (ex: PessimisticLockingFailureException) { + lockFailureException = ex + } + + } while (numAttempts <= this.maxRetries) + throw lockFailureException + } +} +``` + +注意,方面实现了`Ordered`接口,这样我们就可以将方面的优先级设置为高于事务通知的优先级(每次重试时,我们都希望有一个新的事务)。`maxRetries`和`order`属性都是由 Spring 配置的。主要的操作发生在`doConcurrentOperation`around advice 方法中。我们试图继续。如果我们使用`PessimisticLockingFailureException`失败,我们将再次尝试,除非我们已经用尽了所有的重试尝试。 + +| |这个类与 @AspectJ 示例中使用的类相同,但删除了
注释。| +|---|------------------------------------------------------------------------------------------------------| + +Spring 相应的配置如下: + +``` + + + + + + + + + + + + + + + + +``` + +请注意,目前我们假设所有业务服务都是幂等的。如果不是这样的话,我们可以通过引入`Idempotent`注释并使用该注释来注释服务操作的实现,从而细化方面,使其仅重试真正的幂等运算,如下例所示: + +Java + +``` +@Retention(RetentionPolicy.RUNTIME) +public @interface Idempotent { + // marker annotation +} +``` + +Kotlin + +``` +@Retention(AnnotationRetention.RUNTIME) +annotation class Idempotent { + // marker annotation +} +``` + +对只重试幂等运算的方面的更改涉及细化切入点表达式,使只有`@Idempotent`运算匹配,如下所示: + +``` + +``` + +### 5.6.选择使用哪种 AOP 声明样式 + +一旦你确定一个方面是实现给定需求的最佳方法,你如何在使用 Spring AOP 或 AspectJ 以及使用方面语言(代码)样式、@AspectJ 注释样式或 Spring XML 样式之间做出决定?这些决策受到许多因素的影响,包括应用程序需求、开发工具和团队对 AOP 的熟悉程度。 + +#### 5.6.1. Spring AOP 或完全 AspectJ? + +用最简单的能起作用的东西。 Spring AOP 比使用完整的 AspectJ 更简单,因为不需要在开发和构建过程中引入 AspectJ 编译器/Weaver。如果只需要建议在 Spring bean 上执行操作,则 Spring AOP 是正确的选择。如果你需要建议不是由 Spring 容器管理的对象(例如,通常是域对象),则需要使用 AspectJ。如果你希望建议连接点而不是简单的方法执行(例如,字段 get 或设置连接点等等),还需要使用 AspectJ。 + +在使用 AspectJ 时,你可以选择 AspectJ 语言语法(也称为“代码样式”)或 @AspectJ 注释样式。显然,如果你不使用 Java5+,那么已经为你做出了这样的选择:使用代码样式。如果方面在你的设计中起着很大的作用,并且你能够为 Eclipse 使用[AspectJ 开发工具](https://www.eclipse.org/ajdt/)插件,那么 AspectJ 语言语法是首选选项。它更简洁,更简单,因为该语言是专门为写作方面而设计的。如果你不使用 Eclipse,或者只有几个方面在你的应用程序中不起主要作用,那么你可能想要考虑使用 @AspectJ 样式,在 IDE 中坚持常规的 Java 编译,并在构建脚本中添加一个方面编织阶段。 + +#### 5.6.2. Spring AOP 的 @AspectJ 或 XML? + +如果你选择使用 Spring AOP,那么你可以选择 @AspectJ 或 XML 样式。需要考虑的权衡因素有很多。 + +现有 Spring 用户可能最熟悉 XML 风格,并且它由真正的 POJO 支持。当使用 AOP 作为配置 Enterprise 服务的工具时,XML 可以是一个很好的选择(一个很好的测试是,你是否认为切入点表达式是你可能希望独立更改的配置的一部分)。使用 XML 风格,可以从配置中更清楚地看出系统中存在哪些方面。 + +XML 样式有两个缺点。首先,它没有完全封装它在一个地方处理的需求的实现。干原理认为,系统中的任何知识都应该有一个单一的、明确的、权威的表示。在使用 XML 样式时,关于需求是如何实现的知识在配置文件中的 Backing Bean 类和 XML 的声明中进行了分割。当你使用 @AspectJ 样式时,此信息被封装在一个模块中:Aspect。其次,与 @AspectJ 风格相比,XML 风格在表达的内容上略有限制:只支持“单例”方面实例化模型,并且不可能合并 XML 中声明的命名切入点。例如,在 @AspectJ 样式中,你可以编写如下内容: + +Java + +``` +@Pointcut("execution(* get*())") +public void propertyAccess() {} + +@Pointcut("execution(org.xyz.Account+ *(..))") +public void operationReturningAnAccount() {} + +@Pointcut("propertyAccess() && operationReturningAnAccount()") +public void accountPropertyAccess() {} +``` + +Kotlin + +``` +@Pointcut("execution(* get*())") +fun propertyAccess() {} + +@Pointcut("execution(org.xyz.Account+ *(..))") +fun operationReturningAnAccount() {} + +@Pointcut("propertyAccess() && operationReturningAnAccount()") +fun accountPropertyAccess() {} +``` + +在 XML 样式中,你可以声明前两个切入点: + +``` + + + +``` + +XML 方法的缺点是,你无法通过合并这些定义来定义“AccountPropertyAccess”切入点。 + +@AspectJ 样式支持额外的实例化模型和更丰富的切入点组合。它具有保持方面为模块化单元的优点。它还具有这样的优点: Spring AOP 和 AspectJ 都可以理解(并因此使用)@AspectJ 方面。因此,如果你后来决定需要 AspectJ 的功能来实现额外的需求,则可以轻松地迁移到经典的 AspectJ 设置。总的来说, Spring 团队对于自定义方面更喜欢 @AspectJ 风格,而不是简单地配置 Enterprise 服务。 + +### 5.7.混合方面类型 + +通过使用自动代理支持、模式定义的``方面、``声明的顾问,甚至在相同配置中使用其他样式的代理和拦截器,完全可以混合 @AspectJ 样式的方面。所有这些都是通过使用相同的底层支持机制来实现的,并且可以毫无困难地共存。 + +### 5.8.代理机制 + +Spring AOP 使用 JDK 动态代理或 CGLIB 为给定的目标对象创建代理。JDK 动态代理是内置在 JDK 中的,而 CGlib 是一种常见的开源类定义库(重新打包为`spring-core`)。 + +如果要代理的目标对象实现了至少一个接口,则使用 JDK 动态代理。由目标类型实现的所有接口都是代理的。如果目标对象不实现任何接口,则创建一个 CGLIB 代理。 + +如果你想强制使用 CGlib 代理(例如,代理为目标对象定义的每个方法,而不仅仅是那些由其接口实现的方法),你可以这样做。但是,你应该考虑以下问题: + +* 对于 CGlib,不能通知`final`方法,因为它们不能在运行时生成的子类中被重写。 + +* 从 Spring 4.0 开始,Proxied 对象的构造函数不再被调用两次,因为 CGlib 代理实例是通过 ObjeNesis 创建的。只有当你的 JVM 不允许绕过构造函数时,你才可能在 Spring 的 AOP 支持中看到双重调用和相应的调试日志条目。 + +要强制使用 CGLIB 代理,请将``元素的`proxy-target-class`属性的值设置为 true,如下所示: + +``` + + + +``` + +要在使用 @AspectJ 自动代理支持时强制进行 CGLIB 代理,请将``元素的 `proxy-target-class’属性设置为`true`,如下所示: + +``` + +``` + +| |多个``节在运行时折叠成一个统一的自动代理创建器
,该创建器在运行时应用*最强*代理设置,该设置指定了任何<节(通常来自不同的 XML Bean 定义文件)。
这也适用于``和``元素。
要清楚,
元素,在``上使用`proxy-target-class="true"`,,或``元素强制使用 CGLIB
代理*对于他们三个人来说*。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 5.8.1.理解 AOP 代理 + +Spring AOP 是基于代理的。在编写自己的方面或使用 Spring 框架中提供的基于 Spring AOP 的方面之前,掌握最后一条语句的实际含义的语义是非常重要的。 + +首先考虑这样一个场景:你有一个简单的、未被代理的、没有什么特别之处的、直接的对象引用,如下面的代码片段所示: + +Java + +``` +public class SimplePojo implements Pojo { + + public void foo() { + // this next method invocation is a direct call on the 'this' reference + this.bar(); + } + + public void bar() { + // some logic... + } +} +``` + +Kotlin + +``` +class SimplePojo : Pojo { + + fun foo() { + // this next method invocation is a direct call on the 'this' reference + this.bar() + } + + fun bar() { + // some logic... + } +} +``` + +如果你在对象引用上调用一个方法,那么该方法将直接在该对象引用上调用,如下面的图像和列表所示: + +![aop proxy plain pojo call](images/aop-proxy-plain-pojo-call.png) + +Java + +``` +public class Main { + + public static void main(String[] args) { + Pojo pojo = new SimplePojo(); + // this is a direct method call on the 'pojo' reference + pojo.foo(); + } +} +``` + +Kotlin + +``` +fun main() { + val pojo = SimplePojo() + // this is a direct method call on the 'pojo' reference + pojo.foo() +} +``` + +当客户机代码的引用是代理时,情况会略有变化。考虑以下图表和代码片段: + +![aop proxy call](images/aop-proxy-call.png) + +Java + +``` +public class Main { + + public static void main(String[] args) { + ProxyFactory factory = new ProxyFactory(new SimplePojo()); + factory.addInterface(Pojo.class); + factory.addAdvice(new RetryAdvice()); + + Pojo pojo = (Pojo) factory.getProxy(); + // this is a method call on the proxy! + pojo.foo(); + } +} +``` + +Kotlin + +``` +fun main() { + val factory = ProxyFactory(SimplePojo()) + factory.addInterface(Pojo::class.java) + factory.addAdvice(RetryAdvice()) + + val pojo = factory.proxy as Pojo + // this is a method call on the proxy! + pojo.foo() +} +``` + +这里需要理解的关键是,`main(..)`类的`Main`方法中的客户端代码引用了代理。这意味着对该对象引用的方法调用是对代理的调用。因此,代理可以委托给与该特定方法调用相关的所有拦截器(通知)。然而,一旦调用最终到达目标对象(在本例中是`SimplePojo`引用),它可能对自身进行的任何方法调用,例如`this.bar()`或 `this.foo()’,都将针对`this`引用而不是代理调用。这具有重要的意义。这意味着,自我调用不会导致与方法调用相关的建议有机会运行。 + +好吧,那么我们该怎么做呢?最好的方法(这里不严格使用术语“best”)是重构代码,这样就不会发生自我调用。这确实需要你做一些工作,但这是最好的、侵入性最小的方法。下一种做法绝对是可怕的,我们不愿指出这一点,恰恰是因为它太可怕了。你可以(尽管这对我们来说是痛苦的)将你的类中的逻辑完全绑定到 Spring AOP,如下例所示: + +Java + +``` +public class SimplePojo implements Pojo { + + public void foo() { + // this works, but... gah! + ((Pojo) AopContext.currentProxy()).bar(); + } + + public void bar() { + // some logic... + } +} +``` + +Kotlin + +``` +class SimplePojo : Pojo { + + fun foo() { + // this works, but... gah! + (AopContext.currentProxy() as Pojo).bar() + } + + fun bar() { + // some logic... + } +} +``` + +这将你的代码完全耦合到 Spring AOP,并且它使类本身意识到这样一个事实,即它是在 AOP 上下文中使用的,这与 AOP 完全相反。在创建代理时,它还需要一些额外的配置,如下例所示: + +Java + +``` +public class Main { + + public static void main(String[] args) { + ProxyFactory factory = new ProxyFactory(new SimplePojo()); + factory.addInterface(Pojo.class); + factory.addAdvice(new RetryAdvice()); + factory.setExposeProxy(true); + + Pojo pojo = (Pojo) factory.getProxy(); + // this is a method call on the proxy! + pojo.foo(); + } +} +``` + +Kotlin + +``` +fun main() { + val factory = ProxyFactory(SimplePojo()) + factory.addInterface(Pojo::class.java) + factory.addAdvice(RetryAdvice()) + factory.isExposeProxy = true + + val pojo = factory.proxy as Pojo + // this is a method call on the proxy! + pojo.foo() +} +``` + +最后,必须指出的是,AspectJ 不存在这种自我调用问题,因为它不是基于代理的 AOP 框架。 + +### 5.9.程序化地创建 @AspectJ 代理 + +除了在配置中使用``或``声明方面之外,还可以通过编程方式创建代理来为目标对象提供建议。有关 Spring 的 AOP API 的全部详细信息,请参见[next chapter](#aop-api)。在这里,我们希望重点关注通过使用 @AspectJ Aspects 自动创建代理的能力。 + +你可以使用`org.springframework.aop.aspectj.annotation.AspectJProxyFactory`类为一个或多个 @AspectJ 方面建议的目标对象创建代理。这个类的基本用法非常简单,如下例所示: + +Java + +``` +// create a factory that can generate a proxy for the given target object +AspectJProxyFactory factory = new AspectJProxyFactory(targetObject); + +// add an aspect, the class must be an @AspectJ aspect +// you can call this as many times as you need with different aspects +factory.addAspect(SecurityManager.class); + +// you can also add existing aspect instances, the type of the object supplied must be an @AspectJ aspect +factory.addAspect(usageTracker); + +// now get the proxy object... +MyInterfaceType proxy = factory.getProxy(); +``` + +Kotlin + +``` +// create a factory that can generate a proxy for the given target object +val factory = AspectJProxyFactory(targetObject) + +// add an aspect, the class must be an @AspectJ aspect +// you can call this as many times as you need with different aspects +factory.addAspect(SecurityManager::class.java) + +// you can also add existing aspect instances, the type of the object supplied must be an @AspectJ aspect +factory.addAspect(usageTracker) + +// now get the proxy object... +val proxy = factory.getProxy() +``` + +有关更多信息,请参见[javadoc](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/aop/aspectj/annotation/AspectJProxyFactory.html)。 + +### 5.10.在 Spring 应用程序中使用 AspectJ + +到目前为止,我们在本章中所讨论的一切都是纯粹的 Spring AOP。在本节中,我们将研究如何使用 AspectJ 编译器或 Weaver,而不是 Spring AOP,如果你的需求超出了 Spring AOP 单独提供的功能。 + +Spring 附带了一个小的 AspectJ 方面库,它可以在你的发行版中以`spring-aspects.jar`的形式独立提供。你需要将此添加到你的 Classpath 中,以便使用其中的方面。[Using AspectJ to Dependency Inject Domain Objects with Spring](#aop-atconfigurable)和[Other Spring aspects for AspectJ](#aop-ajlib-other)讨论这个库的内容以及如何使用它。[Configuring AspectJ Aspects by Using Spring IoC](#aop-aj-configure)讨论了如何使用 AspectJ 编译器注入 AspectJ 方面的依赖关系。最后,[Load-time Weaving with AspectJ in the Spring Framework](#aop-aj-ltw)为使用 AspectJ 的 Spring 应用程序提供了加载时编织的介绍。 + +#### 5.10.1.使用 AspectJ 到依赖注入域对象 Spring + +Spring 容器实例化和配置在应用程序上下文中定义的 bean。还可以要求 Bean 工厂配置预先存在的对象,给定 Bean 定义的名称,该定义包含要应用的配置。` Spring-方面。该支持旨在用于在任何容器的控制范围之外创建的对象。域对象通常属于这一类,因为它们通常是用“new”操作符以编程方式创建的,或者是数据库查询的结果,由 ORM 工具创建的。 + +`@Configurable`注释将一个类标记为符合 Spring 驱动配置的条件。在最简单的情况下,你可以将其纯粹用作标记注释,如下例所示: + +Java + +``` +package com.xyz.myapp.domain; + +import org.springframework.beans.factory.annotation.Configurable; + +@Configurable +public class Account { + // ... +} +``` + +Kotlin + +``` +package com.xyz.myapp.domain + +import org.springframework.beans.factory.annotation.Configurable + +@Configurable +class Account { + // ... +} +``` + +当以这种方式用作标记接口时, Spring 通过使用与完全限定类型名称(`com.xyz.myapp.domain.account`)同名的 Bean 定义(通常是原型范围)来配置带注释类型(`account’,在这种情况下)的新实例。由于 Bean 的默认名称是其类型的完全限定名称,因此声明原型定义的一种方便方法是省略`id`属性,如下例所示: + +``` + + + +``` + +如果要显式地指定要使用的原型 Bean 定义的名称,可以直接在注释中这样做,如下例所示: + +爪哇 + +``` +package com.xyz.myapp.domain; + +import org.springframework.beans.factory.annotation.Configurable; + +@Configurable("account") +public class Account { + // ... +} +``` + +Kotlin + +``` +package com.xyz.myapp.domain + +import org.springframework.beans.factory.annotation.Configurable + +@Configurable("account") +class Account { + // ... +} +``` + +Spring 现在查找一个名为`account`的 Bean 定义,并使用该定义来配置新的`Account`实例。 + +你还可以使用自动布线来避免指定专门的 Bean 定义。要使 Spring 应用自动布线,请使用`autowire`注释的`@Configurable`属性。你可以指定`@Configurable(autowire=Autowire.BY_TYPE)`或 `@configurable(autowire=autowire.by_name)`,分别根据类型或名称进行自动布线。作为一种选择,最好是在字段或方法级别上通过`@Autowired`或`@Inject`为你的`@Configurable`bean 指定显式的、注释驱动的依赖注入(有关更多详细信息,请参见[基于注释的容器配置](#beans-annotation-config))。 + +最后,你可以通过使用`dependencyCheck`属性(例如,`@configurable(autowire=autowire.by_name,dependencycheck=true)’),在新创建和配置的对象中启用 Spring 依赖项检查对象引用。如果将此属性设置为`true`,则 Spring 在配置后验证已设置所有属性(不是原语或集合)。 + +请注意,使用注释本身并不会产生任何效果。是`spring-aspects.jar`中的 `AnnotationBeanConfigureRespect’作用于注释的存在。实质上,该方面表示,“在从使用`@Configurable`注释的类型的新对象的初始化返回后,根据注释的属性使用 Spring 配置新创建的对象”。在这种情况下,“初始化”指的是新实例化的对象(例如,用`new`操作符实例化的对象)以及正在进行反序列化(例如,通过[readResolve()](https://docs.oracle.com/javase/8/docs/api/java/io/Serializable.html))的`Serializable`对象。 + +| |上面一段中的关键短语之一是“本质上”。在大多数情况下,“从新对象的初始化返回后”的
精确语义是
。在这种上下文中,“在初始化之后”意味着依赖项是在对象构造完成之后注入的
。这意味着依赖项
在类的构造函数主体中不可用。如果希望在构造函数主体运行之前注入
依赖项,从而使
在构造函数主体中可用,则需要在 `@configurable’声明中定义该依赖项,如下:

爪哇

``
@configurable(preconstruction=“true)<<5018"/>`


``gt r=“5023”true(preconstruction=“5024”/>”><25"r=ttr>“>你可以找到更多的信息 +``` + +在配置方面之前创建的`@Configurable`对象的实例将导致向调试日志发出消息,并且不会发生对象的配置。一个示例可能是 Spring 配置中的 Bean,该配置在 Spring 初始化域对象时创建域对象。在这种情况下,可以使用 `Depends-on` Bean 属性来手动指定 Bean 取决于配置方面。下面的示例展示了如何使用`depends-on`属性: + +``` + + + + + +``` + +| |不要通过 Bean 配置器方面激活`@Configurable`处理,除非你
确实意味着在运行时依赖其语义。特别是,要确保在 Bean 类上使用
而不是在容器中注册为常规 Spring bean
的类上使用`@Configurable`。这样做会导致双重初始化,一次通过
容器,一次通过方面。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 单元测试`@Configurable`对象 + +`@Configurable`支持的目标之一是实现域对象的独立单元测试,而不会遇到与硬编码查找相关的困难。如果`@Configurable`类型没有被 AspectJ 编织,则注释在单元测试期间没有影响。你可以在测试对象中设置 mock 或 stub 属性引用,并按常规进行操作。如果`@Configurable`类型已由 AspectJ 编织,则仍然可以正常地在容器之外进行单元测试,但是每次构造`@Configurable`对象时,都会看到一条警告消息,表明该对象尚未由 Spring 进行配置。 + +##### 处理多个应用程序上下文 + +用于实现`AnnotationBeanConfigurerAspect`支持的`@Configurable`是 AspectJ 单例方面。单例方面的作用域与`static`成员的作用域相同:每个类装入器只有一个方面实例定义类型。这意味着,如果在相同的类装入器层次结构中定义多个应用程序上下文,则需要考虑在哪里定义`@EnableSpringConfigured` Bean,在哪里将`spring-aspects.jar`放置在 Classpath 上。 + +考虑一个典型的 Spring Web 应用程序配置,该配置具有一个共享的父应用程序上下文,该上下文定义了公共业务服务、支持这些服务所需的一切,以及每个 Servlet 的一个子应用程序上下文(其中包含专门针对该 Servlet 的定义)。所有这些上下文都在相同的类装入器层次结构中共存,因此`AnnotationBeanConfigurerAspect`只能保存对其中之一的引用。在这种情况下,我们建议在共享(父)应用程序上下文中定义`@EnableSpringConfigured` Bean。这定义了你可能希望注入到域对象中的服务。结果是,你无法通过使用 @configurable 机制(这可能不是你想要做的事情)来配置具有对子( Servlet 特定)上下文中定义的 bean 的引用的域对象。 + +当在同一个容器中部署多个 Web 应用程序时,请确保每个 Web 应用程序通过使用自己的类装入器加载`spring-aspects.jar`中的类型(例如,在`spring-aspects.jar`中放置`WEB-INF/lib`)。如果`spring-aspects.jar`仅添加到容器范围的 Classpath(因此由共享的父类加载器加载),则所有 Web 应用程序都共享相同的方面实例(这可能不是你想要的)。 + +#### 5.10.2.AspectJ 的其他 Spring 方面 + +除了`@Configurable`方面,`spring-aspects.jar`还包含一个 AspectJ 方面,你可以使用该方面来驱动 Spring 的事务管理,用于使用`@Transactional`注释的类型和方法。这主要用于希望在 Spring 容器之外使用 Spring 框架的事务支持的用户。 + +解释`@Transactional`注释的方面是 `AnnotationTransactionAspect’。当你使用这个方面时,你必须注释实现类(或该类中的方法或两者中的方法),而不是类实现的接口(如果有的话)。AspectJ 遵循 爪哇 的规则,即接口上的注释不会被继承。 + +类上的`@Transactional`注释指定了类中任何公共操作的执行的默认事务语义。 + +类中方法上的`@Transactional`注释重写了类注释给出的默认事务语义(如果存在的话)。任何可见性的方法都可以被注释,包括私有方法。直接对非公共方法进行注释是获得用于执行此类方法的事务划分的唯一方法。 + +| |Spring Framework4.2 以来,`spring-aspects`提供了一个类似的方面,该方面为标准
注释提供了完全相同的功能。查看“jtaAnnotationTransactionAspect”以获取更多详细信息。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +对于希望使用 Spring 配置和事务管理支持但不想(或不能)使用注释的 AspectJ 程序员,`spring-aspects.jar`还包含`abstract`方面,你可以扩展以提供自己的切入点定义。有关更多信息,请参见`AbstractBeanConfigurerAspect`和 `AbstractTransactionAspect’方面的来源。作为示例,下面的摘录展示了如何编写一个方面,通过使用匹配完全限定类名称的原型 Bean 定义来配置在域模型中定义的对象的所有实例: + +``` +public aspect DomainObjectConfiguration extends AbstractBeanConfigurerAspect { + + public DomainObjectConfiguration() { + setBeanWiringInfoResolver(new ClassNameBeanWiringInfoResolver()); + } + + // the creation of a new bean (any object in the domain model) + protected pointcut beanCreation(Object beanInstance) : + initialization(new(..)) && + CommonPointcuts.inDomainModel() && + this(beanInstance); +} +``` + +#### 5.10.3.使用 Spring IOC 配置 AspectJ 方面 + +当你在 Spring 应用程序中使用 AspectJ 方面时,很自然地,既希望也希望能够使用 Spring 配置这样的方面。AspectJ 运行时本身负责方面的创建,通过 Spring 配置 AspectJ 创建的方面的方法取决于方面使用的 AspectJ 实例化模型(`per-xxx`子句)。 + +大多数 AspectJ 方面都是单例方面。这些方面的配置很容易。你可以创建一个 Bean 定义,将方面类型作为常规引用,并包括`factory-method="aspectOf"` Bean 属性。这确保了 Spring 通过向 AspectJ 查询来获得 Aspect 实例,而不是试图创建实例本身。下面的示例展示了如何使用`factory-method="aspectOf"`属性: + +``` + (1) + + + +``` + +|**1**|注意`factory-method="aspectOf"`属性| +|-----|----------------------------------------------| + +非单例方面更难配置。然而,可以通过创建原型 Bean 定义并使用来自 ` Spring-方面. jar ` 的`@Configurable`支持来实现这一点,以配置 AspectJ 运行时创建的 Bean 方面实例。 + +如果你有一些要与 AspectJ 一起使用的 @AspectJ 方面(例如,对域模型类型使用加载时编织)和其他要与 Spring AOP 一起使用的 @AspectJ 方面,并且这些方面都是在 Spring 中配置的,你需要告诉 Spring AOP @AspectJ 自动代理支持程序,配置中定义的 @AspectJ 方面的哪些确切子集应该用于自动代理。你可以通过在``声明中使用一个或多个``元素来实现此目的。 Spring AOP 自动代理配置中,每个``元素指定一个名称模式,并且只有名称与至少一个模式匹配的 bean 才被使用。下面的示例展示了如何使用``元素: + +``` + + + + +``` + +| |不要被``元素的名称所误导。使用它
将导致创建 Spring AOP 代理。此处使用了 Aspect
声明的 @AspectJ 样式,但不涉及 AspectJ 运行时。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 5.10.4. Spring 框架中的 AspectJ 加载时编织 + +加载时编织(LOAD-TIME WEAWING,LTW)是指将 AspectJ 方面编织到应用程序的类文件中的过程,这些类文件正被加载到 爪哇 虚拟机中。本节的重点是在 Spring 框架的特定上下文中配置和使用 LTW。本节不是对 LTW 的一般介绍。有关 LTW 和仅使用 AspectJ 配置 LTW 的详细信息(完全不涉及 Spring),请参见[AspectJ 开发环境指南的 LTW 部分](https://www.eclipse.org/aspectj/doc/released/devguide/ltw.html)。 + +Spring 框架给 AspectJ LTW 带来的价值在于,能够对编织过程进行更细粒度的控制。“Vanilla”AspectJ LTW 是通过使用 爪哇(5+)代理来实现的,在启动 JVM 时,通过指定 VM 参数来打开该代理。因此,它是一个 JVM 范围内的设置,在某些情况下可能很好,但通常有点太粗糙了。 Spring-启用的 LTW 允许你在每个“类加载器”的基础上切换 LTW,这是更细粒度的,并且在“单 JVM 多应用程序”环境(例如在典型的应用程序服务器环境中发现的)中更有意义。 + +此外,[在某些环境中](#aop-aj-ltw-environments),这种支持使加载时编织成为可能,而无需对应用服务器的启动脚本进行任何修改,而添加`-javaagent:path/to/aspectjweaver.jar`或(正如我们在本节后面描述的)`-javaagent:path/to/spring-instrument.jar`所需的修改。开发人员将应用程序上下文配置为支持加载时编织,而不是依赖通常负责部署配置(例如启动脚本)的管理员。 + +既然销售介绍已经结束,让我们先来看一个使用 Spring 的 AspectJ LTW 的快速示例,然后是关于示例中引入的元素的详细细节。有关完整的示例,请参见[PetClinic 样本应用程序](https://github.com/spring-projects/spring-petclinic)。 + +##### 第一个例子 + +假设你是一个应用程序开发人员,负责诊断系统中某些性能问题的原因。我们将切换到一个简单的分析方面,让我们快速获得一些性能指标,而不是推出一个分析工具。然后,我们可以立即将更细粒度的分析工具应用到该特定区域。 + +| |这里展示的示例使用 XML 配置。你还可以配置
并使用 @AspectJ 和[爪哇 配置](#beans-java)。具体来说,你可以使用“@enableLoadTimeWeaving”注释作为``的替代方法(有关详细信息,请参见[below](#aop-aj-ltw-spring))。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +下面的示例展示了分析方面,这并不新奇。这是一个基于时间的探查器,它使用了方面声明的 @AspectJ 风格: + +爪哇 + +``` +package foo; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Pointcut; +import org.springframework.util.StopWatch; +import org.springframework.core.annotation.Order; + +@Aspect +public class ProfilingAspect { + + @Around("methodsToBeProfiled()") + public Object profile(ProceedingJoinPoint pjp) throws Throwable { + StopWatch sw = new StopWatch(getClass().getSimpleName()); + try { + sw.start(pjp.getSignature().getName()); + return pjp.proceed(); + } finally { + sw.stop(); + System.out.println(sw.prettyPrint()); + } + } + + @Pointcut("execution(public * foo..*.*(..))") + public void methodsToBeProfiled(){} +} +``` + +Kotlin + +``` +package foo + +import org.aspectj.lang.ProceedingJoinPoint +import org.aspectj.lang.annotation.Aspect +import org.aspectj.lang.annotation.Around +import org.aspectj.lang.annotation.Pointcut +import org.springframework.util.StopWatch +import org.springframework.core.annotation.Order + +@Aspect +class ProfilingAspect { + + @Around("methodsToBeProfiled()") + fun profile(pjp: ProceedingJoinPoint): Any { + val sw = StopWatch(javaClass.simpleName) + try { + sw.start(pjp.getSignature().getName()) + return pjp.proceed() + } finally { + sw.stop() + println(sw.prettyPrint()) + } + } + + @Pointcut("execution(public * foo..*.*(..))") + fun methodsToBeProfiled() { + } +} +``` + +我们还需要创建一个`META-INF/aop.xml`文件,以通知 AspectJ Weaver 我们要将`ProfilingAspect`编织到类中。这种文件约定,即在 爪哇 Classpath 上存在一个名为`META-INF/aop.xml`的文件(或多个文件)是标准的 AspectJ。下面的示例显示了`aop.xml`文件: + +``` + + + + + + + + + + + + + + +``` + +现在我们可以转到配置的 Spring 特定部分。我们需要配置`LoadTimeWeaver`(稍后会进行说明)。这个加载时 Weaver 是负责将一个或多个`META-INF/aop.xml`文件中的方面配置编织到应用程序中的类中的基本组件。好的方面是,它不需要大量的配置(你可以指定更多的选项,但这些选项将在后面详细说明),如下例所示: + +``` + + + + + + + + + +``` + +现在,所有必需的工件(方面、`META-INF/aop.xml`文件和 Spring 配置)都已就绪,我们可以使用`main(..)`方法创建以下驱动程序类,以演示 LTW 的实际操作: + +爪哇 + +``` +package foo; + +import org.springframework.context.support.ClassPathXmlApplicationContext; + +public final class Main { + + public static void main(String[] args) { + ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml", Main.class); + + EntitlementCalculationService entitlementCalculationService = + (EntitlementCalculationService) ctx.getBean("entitlementCalculationService"); + + // the profiling aspect is 'woven' around this method execution + entitlementCalculationService.calculateEntitlement(); + } +} +``` + +Kotlin + +``` +package foo + +import org.springframework.context.support.ClassPathXmlApplicationContext + +fun main() { + val ctx = ClassPathXmlApplicationContext("beans.xml") + + val entitlementCalculationService = ctx.getBean("entitlementCalculationService") as EntitlementCalculationService + + // the profiling aspect is 'woven' around this method execution + entitlementCalculationService.calculateEntitlement() +} +``` + +我们还有最后一件事要做。这一部分的介绍中确实提到,人们可以根据 Spring 的“类加载器”有选择地切换 LTW,这是事实。然而,对于这个示例,我们使用一个 爪哇 代理( Spring 提供的)来切换 LTW。我们使用以下命令运行前面显示的`Main`类: + +``` +java -javaagent:C:/projects/foo/lib/global/spring-instrument.jar foo.Main +``` + +`-javaagent`是用于指定和启用[测试在 JVM 上运行的程序的代理](https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/package-summary.html)的标志。 Spring 框架附带了这样的代理,`InstrumentationSavingAgent`,该代理被包装在 ` Spring-文书。 jar ` 中,该代理是作为前面示例中的`-javaagent`参数的值提供的。 + +执行`Main`程序的输出与下一个示例类似。(我已经在`Thread.sleep(..)`实现中引入了`calculateEntitlement()`语句,这样探查器实际上捕获了 0 毫秒以外的内容(`01234`毫秒不是 AOP 引入的开销)。下面的清单显示了我们运行探查器时得到的输出: + +``` +Calculating entitlement + +StopWatch 'ProfilingAspect': running time (millis) = 1234 +------ ----- ---------------------------- +ms % Task name +------ ----- ---------------------------- +01234 100% calculateEntitlement +``` + +由于这个 LTW 是通过使用完全成熟的 AspectJ 来实现的,因此我们不仅限于为 Spring bean 提供建议。以下对`Main`程序的微小更改产生了相同的结果: + +爪哇 + +``` +package foo; + +import org.springframework.context.support.ClassPathXmlApplicationContext; + +public final class Main { + + public static void main(String[] args) { + new ClassPathXmlApplicationContext("beans.xml", Main.class); + + EntitlementCalculationService entitlementCalculationService = + new StubEntitlementCalculationService(); + + // the profiling aspect will be 'woven' around this method execution + entitlementCalculationService.calculateEntitlement(); + } +} +``` + +Kotlin + +``` +package foo + +import org.springframework.context.support.ClassPathXmlApplicationContext + +fun main(args: Array) { + ClassPathXmlApplicationContext("beans.xml") + + val entitlementCalculationService = StubEntitlementCalculationService() + + // the profiling aspect will be 'woven' around this method execution + entitlementCalculationService.calculateEntitlement() +} +``` + +请注意,在前面的程序中,我们如何引导 Spring 容器,然后完全在 Spring 的上下文之外创建`StubEntitlementCalculationService`的新实例。剖析建议仍被广泛接受。 + +诚然,这个例子过于简单化了。然而, Spring 中 LTW 支持的基础已经在前面的示例中介绍了,本节的其余部分详细解释了每一位配置和使用背后的“为什么”。 + +| |本例中使用的`ProfilingAspect`可能是基本的,但它非常有用。这是开发时方面的一个很好的示例,开发人员可以在开发过程中使用
,然后很容易地将
从正在部署的应用程序的构建中排除到 UAT 或生产中。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### Aspects + +你在 LTW 中使用的方面必须是 AspectJ 方面。你可以用 AspectJ 语言本身编写它们,也可以用 @AspectJ-style 编写方面。那么你的方面就是有效的 AspectJ 和 Spring AOP 方面。此外,编译的方面类需要在 Classpath 上可用。 + +##### META-INF/ AOP.xml + +通过使用 爪哇 Classpath 上的一个或多个`META-INF/aop.xml`文件(直接或更典型地在 jar 文件中)来配置 AspectJ LTW 基础设施。 + +该文件的结构和内容在[AspectJ 参考文档](https://www.eclipse.org/aspectj/doc/released/devguide/ltw-configuration.html)的 LTW 部分中有详细说明。因为`aop.xml`文件是 100%AspectJ,所以我们在这里不再进一步描述它。 + +##### 所需的库(JAR) + +至少,你需要以下库来使用 Spring 框架对 AspectJ LTW 的支持: + +* `spring-aop.jar` + +* `aspectjweaver.jar` + +如果使用[Spring-provided agent to enable instrumentation](#aop-aj-ltw-environments-generic),还需要: + +* `spring-instrument.jar` + +##### Spring 配置 + +Spring LTW 支持中的关键组件是`LoadTimeWeaver`接口(在 `org.springframework.instrument.classloading` 包中),以及 Spring 发行版附带的众多 IT 实现。`LoadTimeWeaver`负责在运行时将一个或多个`java.lang.instrument.ClassFileTransformers`添加到`ClassLoader`中,这为各种有趣的应用程序打开了大门,其中之一恰好是方面的 LTW。 + +| |如果你不熟悉运行时类文件转换的想法,请在继续之前查看`java.lang.instrument`包的
爪哇doc API 文档。
尽管该文档并不全面,但至少你可以看到关键接口
和类(供你阅读本节时参考)。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +为特定的`ApplicationContext`配置`LoadTimeWeaver`就像添加一行一样简单。(注意,你几乎肯定需要使用“ApplicationContext”作为 Spring 容器——通常,`BeanFactory`是不够的,因为 LTW 支持使用`BeanFactoryPostProcessors`。 + +要启用 Spring 框架的 LTW 支持,你需要配置`LoadTimeWeaver`,这通常是通过使用`@EnableLoadTimeWeaving`注释来完成的,如下所示: + +爪哇 + +``` +@Configuration +@EnableLoadTimeWeaving +public class AppConfig { +} +``` + +Kotlin + +``` +@Configuration +@EnableLoadTimeWeaving +class AppConfig { +} +``` + +或者,如果你更喜欢基于 XML 的配置,可以使用 `` 元素。请注意,该元素是在“上下文”名称空间中定义的。下面的示例展示了如何使用``: + +``` + + + + + + +``` + +前面的配置为你自动定义并注册了许多 LTW 特定的基础设施 bean,例如`LoadTimeWeaver`和`AspectJWeavingEnabler`。默认的`LoadTimeWeaver`是`DefaultContextLoadTimeWeaver`类,它试图修饰自动检测到的`LoadTimeWeaver`。“自动检测”的`LoadTimeWeaver`的确切类型取决于你的运行时环境。下表总结了各种`LoadTimeWeaver`实现: + +|运行时环境|`LoadTimeWeaver` implementation| +|-----------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------| +|运行在[Apache Tomcat](https://tomcat.apache.org/)中| `TomcatLoadTimeWeaver` | +|在[GlassFish](https://eclipse-ee4j.github.io/glassfish/)中运行(仅限于 EAR 部署)| `GlassFishLoadTimeWeaver` | +|在 Red Hat 的[JBoss AS](https://www.jboss.org/jbossas/)或[WildFly](https://www.wildfly.org/)中运行| `JBossLoadTimeWeaver` | +|运行在 IBM 的[WebSphere](https://www-01.ibm.com/software/webservers/appserv/was/)中| `WebSphereLoadTimeWeaver` | +|运行在 Oracle 的[WebLogic](https://www.oracle.com/technetwork/middleware/weblogic/overview/index-085209.html)中| `WebLogicLoadTimeWeaver` | +|JVM 以 Spring `InstrumentationSavingAgent`(`java-javaagent:path/to/ Spring-instrument. jar `)开始|`InstrumentationLoadTimeWeaver`| +|fallback,期望底层类装入器遵循常见的约定
(即`addTransformer`和可选的`getThrowawayClassLoader`方法)| `ReflectiveLoadTimeWeaver` | + +请注意,该表仅列出了使用`DefaultContextLoadTimeWeaver`时自动检测到的`LoadTimeWeavers`。你可以精确地指定要使用的`LoadTimeWeaver`实现。 + +要使用 爪哇 配置指定特定的`LoadTimeWeaver`,请实现 `loadTimeWeavingConfigurer’接口并覆盖`getLoadTimeWeaver()`方法。下面的示例指定了`ReflectiveLoadTimeWeaver`: + +爪哇 + +``` +@Configuration +@EnableLoadTimeWeaving +public class AppConfig implements LoadTimeWeavingConfigurer { + + @Override + public LoadTimeWeaver getLoadTimeWeaver() { + return new ReflectiveLoadTimeWeaver(); + } +} +``` + +Kotlin + +``` +@Configuration +@EnableLoadTimeWeaving +class AppConfig : LoadTimeWeavingConfigurer { + + override fun getLoadTimeWeaver(): LoadTimeWeaver { + return ReflectiveLoadTimeWeaver() + } +} +``` + +如果使用基于 XML 的配置,则可以将完全限定的类名指定为`weaver-class`元素上的``属性的值。下面的示例再次指定了`ReflectiveLoadTimeWeaver`: + +``` + + + + + + +``` + +由配置定义和注册的`LoadTimeWeaver`可以在以后通过使用众所周知的名称`loadTimeWeaver`从 Spring 容器中检索。请记住,`LoadTimeWeaver`仅作为 Spring 的 LTW 基础结构的一种机制存在,用于添加一个或多个`ClassFileTransformers`。执行 LTW 的实际 `classfileTransformer’是`ClassPreProcessorAgentAdapter`(来自`org.aspectj.weaver.loadtime`包)类。有关更多详细信息,请参见“classpreprocessoragentapter”类的类级 爪哇doc,因为实际实现编织的细节超出了本文的范围。 + +配置的最后一个属性还有待讨论:`aspectjWeaving`属性(如果使用 XML,则为`aspectj-weaving`属性)。此属性控制是否启用 LTW。它接受三个可能的值中的一个,如果属性不存在,默认值为“autodetect”。下表总结了三个可能的值: + +|Annotation Value| XML Value |解释| +|----------------|------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `ENABLED` | `on` |AspectJ 编织是在,和方面是编织在加载时,视情况而定。| +| `DISABLED` | `off` |LTW 关闭。任何方面都不是在加载时编织的。| +| `AUTODETECT` |`autodetect`|如果 Spring LTW 基础设施能够找到至少一个`META-INF/aop.xml`文件,
则 AspectJ Weaving 处于打开状态。否则,它就关闭了。这是默认值。| + +##### 特定于环境的配置 + +最后一节包含在应用程序服务器和 Web 容器等环境中使用 Spring 的 LTW 支持时所需的任何附加设置和配置。 + +###### Tomcat,JBoss,WebSphere,WebLogic + +Tomcat、JBoss/Wildfly、IBMWebSphere Application Server 和 Oracle WebLogic Server 都提供了一个通用的应用`ClassLoader`,其能够进行本地检测。 Spring 的本机 LTW 可以利用那些类装入器实现来提供 AspectJ 编织。你可以简单地启用加载时编织,如[前面描述的](#aop-using-aspectj)。具体地说,你不需要修改 JVM 启动脚本以添加 `-javaAgent:path/to/ Spring-instrument. jar `。 + +请注意,在 JBoss 上,你可能需要禁用应用程序服务器扫描,以防止它在应用程序实际启动之前加载类。一种快速的解决方法是将名为`WEB-INF/jboss-scanning.xml`的文件添加到工件中,该文件具有以下内容: + +``` + +``` + +###### 通用 爪哇 应用程序 + +当在特定`LoadTimeWeaver`实现不支持的环境中需要类插装时,JVM 代理是通用的解决方案。对于这样的情况, Spring 提供了`InstrumentationLoadTimeWeaver`,它需要一个 Spring 特定的(但非常通用的)JVM 代理,`spring-instrument.jar`,通过常见的`@EnableLoadTimeWeaving`和``设置自动检测。 + +要使用它,你必须使用 Spring 代理通过提供以下 JVM 选项来启动虚拟机: + +``` +-javaagent:/path/to/spring-instrument.jar +``` + +请注意,这需要修改 JVM 启动脚本,这可能会阻止你在应用程序服务器环境中使用该脚本(取决于你的服务器和操作策略)。也就是说,对于每个 JVM 部署一个应用程序,例如独立的启动应用程序,通常在任何情况下都可以控制整个 JVM 设置。 + +### 5.11.更多资源 + +有关 AspectJ 的更多信息,请访问[AspectJ website](https://www.eclipse.org/aspectj)。 + +*Eclipse AspectJ*由 Adrian Colyer 等人(Addison-Wesley,2005)为 AspectJ 语言提供了全面的介绍和参考。 + +*AspectJ 在行动*,第二版由 Ramnivas Laddad(曼宁,2009 年)强烈推荐。这本书的重点是 AspectJ,但(在一定程度上)探讨了许多一般性的 AOP 主题。 + +## 6. Spring AOP API + +上一章用 @AspectJ 和基于模式的方面定义描述了 Spring 对 AOP 的支持。在这一章中,我们讨论了较低级别的 API Spring AOP。对于常见的应用程序,我们建议使用 Spring AOP 和 AspectJ 切入点,如前一章所述。 + +### 6.1. Spring 中的切入点 API + +本节描述 Spring 如何处理关键的切入点概念。 + +#### 6.1.1.概念 + +Spring 的切入点模型能够独立于建议类型实现切入点重用。你可以用相同的切入点针对不同的建议。 + +`org.springframework.aop.Pointcut`接口是中心接口,用于针对特定类和方法的建议。完整的界面如下: + +``` +public interface Pointcut { + + ClassFilter getClassFilter(); + + MethodMatcher getMethodMatcher(); +} +``` + +将`Pointcut`接口拆分成两部分,可以重用类和方法匹配部分以及细粒度的组合操作(例如与另一个方法匹配程序执行“合并”)。 + +`ClassFilter`接口用于将切入点限制为给定的一组目标类。如果`matches()`方法总是返回 true,那么所有目标类都是匹配的。下面的清单显示了`ClassFilter`接口定义: + +``` +public interface ClassFilter { + + boolean matches(Class clazz); +} +``` + +`MethodMatcher`接口通常更重要。完整的界面如下: + +``` +public interface MethodMatcher { + + boolean matches(Method m, Class targetClass); + + boolean isRuntime(); + + boolean matches(Method m, Class targetClass, Object... args); +} +``` + +`matches(Method, Class)`方法用于测试此切入点是否与目标类上的给定方法匹配。当创建 AOP 代理以避免需要对每个方法调用进行测试时,可以执行此评估。如果两个参数`matches`方法返回给定方法的`true`,而 MethodMatcher 的`isRuntime()`方法返回`true`,则在每个方法调用时都会调用三个参数匹配方法。这使得切入点可以查看在目标通知开始之前立即传递给方法调用的参数。 + +大多数`MethodMatcher`实现都是静态的,这意味着它们的`isRuntime()`方法返回`false`。在这种情况下,三参数`matches`方法永远不会被调用。 + +| |如果可能的话,尝试使切入点是静态的,允许 AOP 框架在创建 AOP 代理时缓存切入点求值的
结果。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 6.1.2.切入点上的操作 + +Spring 支持在切入点上的操作(特别是,联合和交叉)。 + +UNION 是指任何一个切入点都匹配的方法。交集表示两个切入点匹配的方法。联合通常更有用。你可以使用 `org.springframework. AOP.support.pointcuts’类中的静态方法或使用相同包中的 `composablepointcut’类来编写切入点。然而,使用 AspectJ 切入点表达式通常是一种更简单的方法。 + +#### 6.1.3.AspectJ 表达式切入点 + +自 2.0 以来, Spring 使用的最重要的切入点类型是 `org.SpringFramework. AOP.AspectJ.AspectJExpressionPointCut`。这是一个切入点,它使用 AspectJ 提供的库来解析 AspectJ PointCut 表达式字符串。 + +有关受支持的 AspectJ 切入点原语的讨论,请参见[上一章](#aop)。 + +#### 6.1.4.方便的切入点实现 + +Spring 提供了几种方便的切入点实现方式。你可以直接使用其中的一些;其他的则打算在特定于应用程序的切入点中进行子类。 + +##### 静态切入点 + +静态切入点基于方法和目标类,不能考虑方法的参数。静态切入点对于大多数用途来说已经足够了,也是最好的。 Spring 可以仅在第一次调用方法时计算一次静态切入点。在那之后,就不需要在每次方法调用时再次计算切入点了。 + +本节的其余部分描述了 Spring 中包含的一些静态切入点实现。 + +###### 正则表达式切入点 + +指定静态切入点的一个明显方法是正则表达式。 Spring 之外的几个 AOP 框架使这成为可能。org.springframework. AOP.support.jdkregexpMethodPointCut 是一种通用的正则表达式切入点,它使用了 JDK 中对正则表达式的支持。 + +使用`JdkRegexpMethodPointcut`类,你可以提供模式字符串的列表。如果其中任何一个是匹配的,则切入点计算为`true`。(因此,得到的切入点实际上是指定模式的合并。 + +下面的示例展示了如何使用`JdkRegexpMethodPointcut`: + +``` + + + + .*set.* + .*absquatulate + + + +``` + +Spring 提供了一个名为`RegexpMethodPointcutAdvisor`的方便类,它还允许我们引用一个`Advice`(请记住,一个`Advice`可以是拦截器,在通知之前,抛出通知,等等)。在幕后, Spring 使用了`JdkRegexpMethodPointcut`。使用`RegexpMethodPointcutAdvisor`简化了连接,因为 Bean 封装了切入点和建议,如下例所示: + +``` + + + + + + + .*set.* + .*absquatulate + + + +``` + +你可以使用`RegexpMethodPointcutAdvisor`与任何`Advice`类型。 + +###### 属性驱动的切入点 + +静态切入点的一种重要类型是元数据驱动的切入点。这使用了元数据属性的值(通常是源级元数据)。 + +##### 动态切入点 + +动态切入点比静态切入点的评估成本更高。它们考虑了方法参数和静态信息。这意味着每次方法调用都必须对它们进行求值,并且不能缓存结果,因为参数会发生变化。 + +主要的例子是`control flow`切入点。 + +###### 控制流切入点 + +Spring 控制流切点在概念上类似于 AspectJ切点,尽管不那么强大。(目前无法指定切入点运行在与另一个切入点匹配的连接点之下。)控制流切入点匹配当前调用堆栈。例如,如果连接点是由`com.mycompany.web`包中的方法调用的,或者是由`SomeCaller`类调用的,那么它可能会触发。控制流的切入点是通过使用`org.springframework.aop.support.ControlFlowPointcut`类来指定的。 + +| |与
其他动态切入点相比,在运行时计算控制流切入点的成本要高得多。在 爪哇1.4 中,开销大约是其他动态
切入点的 5 倍。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 6.1.5.切入点超类 + +Spring 提供了有用的切入点超类,以帮助你实现自己的切入点。 + +因为静态切入点是最有用的,所以你可能应该子类“staticmethodmatcherpointcut”。这只需要实现一个抽象方法(尽管你可以重写其他方法来定制行为)。下面的示例展示了如何子类`StaticMethodMatcherPointcut`: + +爪哇 + +``` +class TestStaticPointcut extends StaticMethodMatcherPointcut { + + public boolean matches(Method m, Class targetClass) { + // return true if custom criteria match + } +} +``` + +Kotlin + +``` +class TestStaticPointcut : StaticMethodMatcherPointcut() { + + override fun matches(method: Method, targetClass: Class<*>): Boolean { + // return true if custom criteria match + } +} +``` + +还有用于动态切入点的超类。你可以对任何通知类型使用自定义切入点。 + +#### 6.1.6.自定义切入点 + +因为 Spring AOP 中的切入点是 爪哇 类,而不是语言特性(如 AspectJ),所以你可以声明自定义的切入点,无论是静态的还是动态的。 Spring 中的自定义切入点可以是任意复杂的。但是,如果可以的话,我们建议你使用 AspectJ PointCut 表达式语言。 + +| |Spring 的后续版本可能会提供对 JAC 提供的“语义切入点”的支持——例如,“所有改变目标对象中实例变量的方法”。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 6.2. Spring 中的咨询 API + +现在我们可以研究 Spring AOP 如何处理建议。 + +#### 6.2.1.建议生命周期 + +每个建议都是 A Spring Bean。一个建议实例可以在所有被建议的对象之间共享,或者对于每个被建议的对象是唯一的。这对应于每个类或每个实例的建议。 + +每堂课的建议是最常用的。它适用于一般的建议,例如事务顾问。这些不依赖于代理对象的状态或添加新的状态。他们只是根据方法和论据行事。 + +每个实例的建议适合于介绍,以支持 mixin。在这种情况下,建议将状态添加到代理对象。 + +你可以在同一个 AOP 代理中混合使用共享和每个实例的建议。 + +#### 6.2.2. Spring 中的建议类型 + +Spring 提供了几种通知类型,并且是可扩展的,以支持任意的通知类型。本节介绍基本概念和标准建议类型。 + +##### 围绕建议的拦截 + +Spring 中最基本的建议类型是围绕建议的拦截。 + +Spring 与 AOP `Alliance`接口兼容,用于围绕使用方法拦截的建议。实现`MethodInterceptor`并围绕建议实现的类也应该实现以下接口: + +``` +public interface MethodInterceptor extends Interceptor { + + Object invoke(MethodInvocation invocation) throws Throwable; +} +``` + +`MethodInvocation`方法的`invoke()`参数公开了被调用的方法、目标连接点、 AOP 代理以及方法的参数。“invoke()”方法应该返回调用的结果:连接点的返回值。 + +下面的示例展示了一个简单的`MethodInterceptor`实现: + +爪哇 + +``` +public class DebugInterceptor implements MethodInterceptor { + + public Object invoke(MethodInvocation invocation) throws Throwable { + System.out.println("Before: invocation=[" + invocation + "]"); + Object rval = invocation.proceed(); + System.out.println("Invocation returned"); + return rval; + } +} +``` + +Kotlin + +``` +class DebugInterceptor : MethodInterceptor { + + override fun invoke(invocation: MethodInvocation): Any { + println("Before: invocation=[$invocation]") + val rval = invocation.proceed() + println("Invocation returned") + return rval + } +} +``` + +注意调用`proceed()`的`MethodInvocation`方法。这会沿着拦截器链朝向连接点。大多数拦截器调用这个方法并返回它的返回值。但是,`MethodInterceptor`与任何 around advice 一样,可以返回不同的值或抛出异常,而不是调用 proceed 方法。然而,你不想在没有充分理由的情况下这样做。 + +| |`MethodInterceptor`实现提供了与其他 AOP 联盟兼容的 AOP
实现的互操作性。在本节的剩余部分
中讨论的其他通知类型以 Spring 特定的方式实现了常见的 AOP 概念。虽然在使用最特定的建议类型时有
的优点,但如果
你可能希望在另一个 AOP 框架中运行该方面,则坚持使用`MethodInterceptor`周围的建议。请注意,PointCuts
目前在框架之间不是可互操作的,并且 AOP 联盟目前没有
定义 PointCut 接口。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 建议之前 + +一种更简单的建议类型是在给出建议之前给出的。这不需要`MethodInvocation`对象,因为只有在输入方法之前才调用它。 + +before 通知的主要优点是,不需要调用`proceed()`方法,因此,不可能因疏忽而无法继续进行拦截器链。 + +下面的清单显示了`MethodBeforeAdvice`接口: + +``` +public interface MethodBeforeAdvice extends BeforeAdvice { + + void before(Method m, Object[] args, Object target) throws Throwable; +} +``` + +( Spring 的 API 设计将允许先字段后通知,尽管通常的对象适用于字段截取,并且 Spring 不太可能实现它。) + +请注意,返回类型是`void`。before advice 可以在连接点运行之前插入自定义行为,但不能更改返回值。如果 before 通知抛出异常,它将停止拦截器链的进一步执行。异常向拦截器链传播备份。如果它未被选中,或者在被调用方法的签名上,它将直接传递给客户机。否则,它将被 AOP 代理包装在一个未经检查的异常中。 + +下面的示例显示了 Spring 中的 before 通知,该通知计算了所有方法调用: + +Java + +``` +public class CountingBeforeAdvice implements MethodBeforeAdvice { + + private int count; + + public void before(Method m, Object[] args, Object target) throws Throwable { + ++count; + } + + public int getCount() { + return count; + } +} +``` + +Kotlin + +``` +class CountingBeforeAdvice : MethodBeforeAdvice { + + var count: Int = 0 + + override fun before(m: Method, args: Array, target: Any?) { + ++count + } +} +``` + +| |在建议可以与任何切入点一起使用之前。| +|---|--------------------------------------------| + +##### 抛出建议 + +如果连接点引发异常,则在连接点返回后调用 Throws 通知。 Spring 提供打印抛出建议。请注意,这意味着“org.springframework. AOP.throwsadvice”接口不包含任何方法。它是一个标记接口,标识给定对象实现了一个或多个类型抛出建议方法。这些措施应采取以下形式: + +``` +afterThrowing([Method, args, target], subclassOfThrowable) +``` + +只有最后一个论点是必需的。方法签名可以有一个或四个参数,这取决于通知方法是否对方法和参数感兴趣。接下来的两个列表展示了抛出建议的示例类。 + +如果抛出了`RemoteException`(包括从子类),则调用以下通知: + +Java + +``` +public class RemoteThrowsAdvice implements ThrowsAdvice { + + public void afterThrowing(RemoteException ex) throws Throwable { + // Do something with remote exception + } +} +``` + +Kotlin + +``` +class RemoteThrowsAdvice : ThrowsAdvice { + + fun afterThrowing(ex: RemoteException) { + // Do something with remote exception + } +} +``` + +与前面的建议不同,下一个示例声明了四个参数,这样它就可以访问被调用的方法、方法参数和目标对象。如果抛出`ServletException`,将调用以下通知: + +Java + +``` +public class ServletThrowsAdviceWithArguments implements ThrowsAdvice { + + public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) { + // Do something with all arguments + } +} +``` + +Kotlin + +``` +class ServletThrowsAdviceWithArguments : ThrowsAdvice { + + fun afterThrowing(m: Method, args: Array, target: Any, ex: ServletException) { + // Do something with all arguments + } +} +``` + +最后一个示例说明了如何在一个同时处理`RemoteException`和`ServletException`的类中使用这两个方法。任何数量的抛出建议方法都可以合并到一个类中。下面的清单展示了最后一个示例: + +Java + +``` +public static class CombinedThrowsAdvice implements ThrowsAdvice { + + public void afterThrowing(RemoteException ex) throws Throwable { + // Do something with remote exception + } + + public void afterThrowing(Method m, Object[] args, Object target, ServletException ex) { + // Do something with all arguments + } +} +``` + +Kotlin + +``` +class CombinedThrowsAdvice : ThrowsAdvice { + + fun afterThrowing(ex: RemoteException) { + // Do something with remote exception + } + + fun afterThrowing(m: Method, args: Array, target: Any, ex: ServletException) { + // Do something with all arguments + } +} +``` + +| |如果一个 throws-advice 方法本身抛出一个异常,它将重写
原始异常(也就是说,它将更改抛出给用户的异常)。覆盖的
异常通常是一个 runtimeException,它与任何方法
签名兼容。但是,如果一个 throws-advice 方法抛出一个检查过的异常,它必须
匹配目标方法声明的异常,因此在某种程度上
耦合到特定的目标方法签名。*Do not throw an undeclared checked
exception that is incompatible with the target method’s signature!*| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |抛出建议可以与任何切入点一起使用。| +|---|--------------------------------------------| + +##### 在返回建议后 + +Spring 中的 after returningadvice 必须实现 `org.springframework. AOP.afterreturningadvice` 接口,下面的列表显示了该接口: + +``` +public interface AfterReturningAdvice extends Advice { + + void afterReturning(Object returnValue, Method m, Object[] args, Object target) + throws Throwable; +} +``` + +返回后通知可以访问返回值(它不能对其进行修改)、调用的方法、方法的参数和目标。 + +返回建议后,将计算所有未抛出异常的成功方法调用: + +Java + +``` +public class CountingAfterReturningAdvice implements AfterReturningAdvice { + + private int count; + + public void afterReturning(Object returnValue, Method m, Object[] args, Object target) + throws Throwable { + ++count; + } + + public int getCount() { + return count; + } +} +``` + +Kotlin + +``` +class CountingAfterReturningAdvice : AfterReturningAdvice { + + var count: Int = 0 + private set + + override fun afterReturning(returnValue: Any?, m: Method, args: Array, target: Any?) { + ++count + } +} +``` + +此建议不会更改执行路径。如果它抛出一个异常,它将被抛出到拦截器链中,而不是返回值。 + +| |返回后,建议可以与任何切入点一起使用。| +|---|-----------------------------------------------------| + +##### 介绍建议 + +Spring 将介绍建议视为一种特殊的拦截建议。 + +Introduction 需要一个`IntroductionAdvisor`和一个`IntroductionInterceptor`来实现以下接口: + +``` +public interface IntroductionInterceptor extends MethodInterceptor { + + boolean implementsInterface(Class intf); +} +``` + +继承自 AOP Alliance 的`invoke()`方法`MethodInterceptor`接口必须实现该介绍。也就是说,如果被调用的方法位于一个引入的接口上,则引入拦截器负责处理方法调用——它不能调用`proceed()`。 + +介绍建议不能与任何切入点一起使用,因为它只适用于类,而不是方法级别。你只能使用“介绍顾问”提供的介绍建议,它有以下方法: + +``` +public interface IntroductionAdvisor extends Advisor, IntroductionInfo { + + ClassFilter getClassFilter(); + + void validateInterfaces() throws IllegalArgumentException; +} + +public interface IntroductionInfo { + + Class[] getInterfaces(); +} +``` + +没有`MethodMatcher`,因此,没有`Pointcut`与介绍建议相关联。只有类过滤是合乎逻辑的。 + +`getInterfaces()`方法返回此顾问引入的接口。 + +在内部使用`validateInterfaces()`方法来查看是否可以通过配置的`IntroductionInterceptor`实现引入的接口。 + +考虑 Spring 测试套件中的一个示例,并假设我们希望将以下接口引入一个或多个对象: + +Java + +``` +public interface Lockable { + void lock(); + void unlock(); + boolean locked(); +} +``` + +Kotlin + +``` +interface Lockable { + fun lock() + fun unlock() + fun locked(): Boolean +} +``` + +这说明了一个 mixin。我们希望能够将建议的对象强制转换为`Lockable`,无论它们的类型如何,并调用锁定和解锁方法。如果我们调用`lock()`方法,我们希望所有 setter 方法都抛出一个`LockedException`。因此,我们可以添加一个方面,该方面提供了使对象不可变的能力,而不需要它们对此有任何了解: AOP 的一个很好的示例。 + +首先,我们需要一个`IntroductionInterceptor`来完成繁重的工作。在这种情况下,我们扩展`org.springframework.aop.support.DelegatingIntroductionInterceptor`便利类。我们可以直接实现`IntroductionInterceptor`,但是在大多数情况下使用 `delegatingintroductionInterceptor’是最好的。 + +`DelegatingIntroductionInterceptor`的设计目的是将介绍委派给所介绍的接口的实际实现,从而隐藏了拦截的使用。可以使用构造函数参数将委托设置为任何对象。默认的委托(当使用无参数构造函数时)是`this`。因此,在下一个示例中,委托是`LockMixin`的`DelegatingIntroductionInterceptor`子类。给定一个委托(默认情况下是委托本身),`DelegatingIntroductionInterceptor`实例将查找委托实现的所有接口(除了 `introductionInterceptor’),并支持针对其中任何一个接口的介绍。像`LockMixin`这样的子类可以调用`suppressInterface(Class intf)`方法来抑制不应该公开的接口。然而,无论`IntroductionInterceptor`准备支持多少个接口,“IntroductionAdvisor”都使用控件来控制哪些接口实际上是公开的。引入的接口掩盖了目标对同一接口的任何实现。 + +因此,`LockMixin`扩展了`DelegatingIntroductionInterceptor`并实现了`Lockable`本身。超类自动获取`Lockable`可以支持的引言,因此我们不需要指定它。我们可以以这种方式引入任意数量的接口。 + +注意`locked`实例变量的使用。这实际上为目标对象中的状态添加了额外的状态。 + +下面的示例显示了`LockMixin`类的示例: + +Java + +``` +public class LockMixin extends DelegatingIntroductionInterceptor implements Lockable { + + private boolean locked; + + public void lock() { + this.locked = true; + } + + public void unlock() { + this.locked = false; + } + + public boolean locked() { + return this.locked; + } + + public Object invoke(MethodInvocation invocation) throws Throwable { + if (locked() && invocation.getMethod().getName().indexOf("set") == 0) { + throw new LockedException(); + } + return super.invoke(invocation); + } + +} +``` + +Kotlin + +``` +class LockMixin : DelegatingIntroductionInterceptor(), Lockable { + + private var locked: Boolean = false + + fun lock() { + this.locked = true + } + + fun unlock() { + this.locked = false + } + + fun locked(): Boolean { + return this.locked + } + + override fun invoke(invocation: MethodInvocation): Any? { + if (locked() && invocation.method.name.indexOf("set") == 0) { + throw LockedException() + } + return super.invoke(invocation) + } + +} +``` + +通常,你不需要重写`invoke()`方法。“delegatingintroductionInterceptor”实现(如果引入了该方法,则调用`delegate`方法,否则将继续进行连接)通常就足够了。在本例中,我们需要添加一个检查:如果处于锁定模式,则不能调用 setter 方法。 + +所需的介绍只需要持有一个不同的“lockmixin”实例并指定引入的接口(在这种情况下,只需要“lockable”)。一个更复杂的示例可能会引用介绍拦截器(它将被定义为原型)。在这种情况下,不存在与`LockMixin`相关的配置,因此我们使用`new`创建它。下面的示例显示了我们的`LockMixinAdvisor`类: + +Java + +``` +public class LockMixinAdvisor extends DefaultIntroductionAdvisor { + + public LockMixinAdvisor() { + super(new LockMixin(), Lockable.class); + } +} +``` + +Kotlin + +``` +class LockMixinAdvisor : DefaultIntroductionAdvisor(LockMixin(), Lockable::class.java) +``` + +我们可以非常简单地应用此 advisor,因为它不需要配置。(然而,在没有“introductionAdvisor”的情况下,不可能使用`IntroductionInterceptor`。)与通常的介绍一样,advisor 必须是每个实例,因为它是有状态的。对于每个被建议的对象,我们需要一个`LockMixinAdvisor`的不同实例,因此也需要一个 `lockmixin’实例。顾问是被建议对象状态的一部分。 + +我们可以通过使用`Advised.addAdvisor()`方法或 XML 配置中的(推荐的方式),以编程方式应用此 Advisor,就像任何其他的 Advisor 一样。下面讨论的所有代理创建选择,包括“自动代理创建”,正确处理介绍和有状态的混合。 + +### 6.3. Spring 中的顾问 API + +在 Spring 中,advisor 是一个方面,该方面仅包含与切入点表达式相关联的单个通知对象。 + +除了介绍的特殊情况外,任何 advisor 都可以与任何 advice 一起使用。org.SpringFramework. AOP.Support.DefaultPointCutavisor 是最常用的 advisor 类。它可以与`MethodInterceptor`、`BeforeAdvice`或 `throwsadvice’一起使用。 + +在相同的 AOP 代理中混合 Spring 中的顾问和建议类型是可能的。例如,你可以在一个代理配置中使用围绕建议、抛出建议和在建议之前的拦截。 Spring 自动创建必要的拦截链。 + +### 6.4.使用`ProxyFactoryBean`创建 AOP 代理 + +如果你为你的业务对象使用 Spring IOC 容器(an`ApplicationContext`或`BeanFactory`)(而且你应该是!),那么你希望使用 Spring 的 AOP `FactoryBean’实现中的一个。(请记住,工厂 Bean 引入了间接层,允许它创建不同类型的对象。 + +| |Spring AOP 支持还在覆盖件下使用工厂 bean。| +|---|----------------------------------------------------------------| + +在 Spring 中创建 AOP 代理的基本方法是使用 `org.SpringFramework. AOP.Framework.ProxyFactoryBean’。这样就可以完全控制切入点、应用的任何建议以及它们的排序。然而,如果你不需要这种控制,那么有一些更简单的选项是更好的。 + +#### 6.4.1.基础知识 + +与其他 Spring `FactoryBean`实现方式一样,`ProxyFactoryBean`引入了间接的级别。如果定义了名为`ProxyFactoryBean`的`foo`,则引用`foo`的对象不会看到`ProxyFactoryBean`实例本身,而是由`getObject()`方法的实现在`ProxyFactoryBean`中创建的对象。该方法创建一个 AOP 代理来包装目标对象。 + +使用`ProxyFactoryBean`或另一个 IoC-aware 类来创建 AOP 代理的最重要的好处之一是,IoC 也可以管理建议和切入点。 AOP 这是一个强大的特性,使得能够使用其他框架很难实现的某些方法成为可能。例如,建议本身可以引用应用程序对象(除了目标,这应该在任何 AOP 框架中可用),从而受益于依赖注入提供的所有可插拔性。 + +#### 6.4.2.Javabean 属性 + +与 Spring 提供的大多数`FactoryBean`实现一样,`ProxyFactoryBean’类本身是一个 JavaBean。它的特性用于: + +* 指定要代理的目标。 + +* 指定是否使用 CGLIB(稍后将进行说明,还请参见[基于 JDK 和 CGLIB 的代理](#aop-pfb-proxy-types))。 + +一些键属性继承自`org.springframework.aop.framework.ProxyConfig`( Spring 中所有 AOP 代理工厂的超类)。这些关键属性包括以下内容: + +* `proxyTargetClass`:`true`如果要代理目标类,而不是目标类的接口。如果将此属性值设置为`true`,则创建 CGLIB 代理(但也请参见[基于 JDK 和 CGLIB 的代理](#aop-pfb-proxy-types))。 + +* `optimize`:控制是否对通过 CGLIB 创建的代理应用积极的优化。除非你完全了解相关的 AOP 代理如何处理优化,否则你不应随意使用此设置。这目前仅用于 CGlib 代理。它对 JDK 动态代理没有任何影响。 + +* `frozen`:如果代理配置是`frozen`,则不再允许对配置进行更改。这既是一种轻微的优化,也适用于在创建代理后不希望调用者能够操作代理(通过`Advised`接口)的情况。此属性的默认值为“false”,因此允许更改(例如添加额外的建议)。 + +* `exposeProxy`:确定当前代理是否应该在 `ThreadLocal’中公开,以便目标可以访问它。如果目标需要获得代理,并且`exposeProxy`属性设置为`true`,则目标可以使用 `aopContext.currentProxy()’方法。 + +特定于`ProxyFactoryBean`的其他属性包括以下内容: + +* `proxyInterfaces`:由`String`接口名称组成的数组。如果不提供此选项,则使用目标类的 CGLIB 代理(但也请参见[基于 JDK 和 CGLIB 的代理](#aop-pfb-proxy-types))。 + +* `interceptorNames`:要应用的`String`数组、拦截器或其他通知名称。订购是重要的,在先到先得的基础上。也就是说,列表中的第一个拦截器是第一个能够拦截调用的拦截器。 + + 这些名称是当前工厂中的 Bean 名称,包括来自祖先工厂的 Bean 名称。这里不能提及 Bean 引用,因为这样做会导致“proxyFactoryBean”忽略建议的单例设置。 + + 你可以在拦截器名称后面附加一个星号。这样做会导致所有的 advisor bean 的应用程序的名称都以要应用的星号之前的部分开始。你可以在[使用“全球”顾问](#aop-global-advisors)中找到使用此功能的示例。 + +* 单例:无论工厂是否应该返回单个对象,无论调用`getObject()`方法的频率如何。几个`FactoryBean`实现提供了这样的方法。默认值是`true`。如果你想使用有状态的建议--例如,对于有状态的 mixin--使用原型建议以及单例值“false”。 + +#### 6.4.3.基于 JDK 和 CGLIB 的代理 + +这一节是关于`ProxyFactoryBean`如何选择为特定目标对象创建基于 JDK 的代理或基于 CGlib 的代理的权威文档。 + +| |`ProxyFactoryBean`关于创建基于 JDK-或 CGLIB 的
代理的行为在 Spring 的 1.2.x 和 2.0 版本之间发生了变化。`ProxyFactoryBean`现在
在自动检测接口方面表现出与 `TransactionProxyFactoryBean’类类似的语义。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +如果要代理的目标对象的类(以下简称为目标类)不实现任何接口,则创建一个基于 CGLIB 的代理。这是最简单的场景,因为 JDK 代理是基于接口的,没有接口意味着 JDK 代理甚至是不可能的。你可以通过设置`interceptorNames`属性来插入目标 Bean 并指定拦截器列表。请注意,即使“proxyFactoryBean”的`proxyTargetClass`属性已设置为`false`,也会创建基于 CGLIB 的代理。(这样做没有意义,最好从 Bean 定义中删除,因为它最好是多余的,最坏的情况是令人困惑。 + +如果目标类实现了一个(或多个)接口,那么创建的代理类型取决于`ProxyFactoryBean`的配置。 + +如果`ProxyFactoryBean`的`proxyTargetClass`属性已设置为`true`,则创建一个基于 CGLIB 的代理。这是有道理的,也符合最少令人惊讶的原则。即使“proxyFactoryBean”的`proxyInterfaces`属性已被设置为一个或多个完全限定的接口名称,但将`proxyTargetClass`属性设置为`true`的事实将导致基于 CGLIB 的代理生效。 + +如果`proxyInterfaces`的`ProxyFactoryBean`属性已被设置为一个或多个完全限定的接口名称,则将创建一个基于 JDK 的代理。创建的代理实现了在`proxyInterfaces`属性中指定的所有接口。如果目标类实现了比`proxyInterfaces`属性中指定的接口多得多的接口,那就很好了,但是这些额外的接口不是由返回的代理实现的。 + +如果`proxyInterfaces`的`ProxyFactoryBean`属性尚未设置,但是目标类确实实现了一个(或多个)接口,则 `ProxyFactoryBean` 自动检测目标类确实实现了至少一个接口的事实,并创建了一个基于 JDK 的代理。实际代理的接口是目标类实现的所有接口。实际上,这与向`proxyInterfaces`属性提供目标类实现的每个接口的列表是一样的。然而,它的工作量要少得多,而且不太容易出现印刷错误。 + +#### 6.4.4.代理接口 + +以`ProxyFactoryBean`的一个简单示例为例。这个例子涉及到: + +* 被代理的目标 Bean。这是示例中的`personTarget` Bean 定义。 + +* 用于提供建议的`Advisor`和`Interceptor`。 + +* AOP 代理 Bean 定义来指定目标对象(`personTarget` Bean)、代理的接口以及应用的建议。 + +下面的清单展示了这个示例: + +``` + + + + + + + + + + + + + + + + + + + myAdvisor + debugInterceptor + + + +``` + +请注意,`interceptorNames`属性接受`String`的列表,该列表保存当前工厂中拦截器或顾问的 Bean 名称。你可以在返回之前、之后使用 Advisors、Interceptors,并抛出建议对象。对顾问的排序意义重大。 + +| |你可能想知道为什么列表中没有 Bean 引用。其原因是
,如果`ProxyFactoryBean`的单例属性设置为`false`,则它必须能够
返回独立的代理实例。如果任何顾问本身是一个原型,则需要返回一个
独立实例,因此必须能够从工厂获得
原型的实例。仅有引用是不够的。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +前面显示的`person` Bean 定义可以用来代替`Person`实现,如下所示: + +Java + +``` +Person person = (Person) factory.getBean("person"); +``` + +Kotlin + +``` +val person = factory.getBean("person") as Person; +``` + +在同一 IoC 上下文中的其他 bean 可以表示对它的强类型依赖,就像普通的 Java 对象一样。下面的示例展示了如何做到这一点: + +``` + + + +``` + +本例中的`PersonUser`类公开了类型`Person`的属性。就其而言, AOP 代理可以透明地代替“真实的”人实现。然而,它的类将是一个动态代理类。可以将它强制转换到`Advised`接口(稍后讨论)。 + +你可以通过使用匿名内部 Bean 来隐藏目标和代理之间的区别。只有`ProxyFactoryBean`的定义是不同的。仅为完整起见,才列入该建议。下面的示例展示了如何使用匿名内部 Bean: + +``` + + + + + + + + + + + + + + + + + + myAdvisor + debugInterceptor + + + +``` + +Bean 使用匿名内部具有的优点是只有一个类型`Person`的对象。如果我们希望防止应用程序上下文的用户获得对非建议对象的引用,或者需要避免使用 Spring IOC 自动布线的任何歧义,那么这是有用的。可以说,`ProxyFactoryBean`的定义是自包含的,这也有一个优点。然而,有时能够从工厂获得不建议的目标实际上可能是一种优势(例如,在某些测试场景中)。 + +#### 6.4.5.代理类 + +如果你需要代理一个类,而不是一个或多个接口,该怎么办? + +想象一下,在我们早期的示例中,没有`Person`接口。我们需要建议一个名为`Person`的类,它没有实现任何业务接口。在这种情况下,可以将 Spring 配置为使用 CGlib 代理,而不是动态代理。为此,将前面显示的`ProxyFactoryBean`上的 `ProxyTargetClass’属性设置为`true`。虽然最好使用接口编程,而不是类编程,但在使用遗留代码时,向不实现接口的类提供建议的能力可能是有用的。(一般来说, Spring 不是规定性的。尽管它使应用好的实践变得容易,但它避免了强制使用特定的方法。 + +如果你愿意,你可以在任何情况下强制使用 CGlib,即使你确实有接口。 + +CGLIB 代理的工作原理是在运行时生成目标类的一个子类。 Spring 将此生成的子类配置为将方法调用委托给原始目标。子类用于实现 decorator 模式,并在建议中进行编织。 + +CGLIB 代理通常应该对用户透明。然而,有一些问题需要考虑: + +* 不能通知`Final`方法,因为它们不能被重写。 + +* 没有必要将 CGlib 添加到你的 Classpath 中。截至 Spring 3.2,CGlib 被重新包装并包括在 Spring-core jar 中。换句话说,基于 CGLIB 的 AOP 工作是“开箱即用”的,JDK 动态代理也是如此。 + +CGlib 代理和动态代理之间的性能差别不大。在这种情况下,业绩不应是决定性的考虑因素。 + +#### 6.4.6.使用“全球”顾问 + +通过将星号附加到拦截器名称中,所有具有 Bean 名称的、与星号之前的部分相匹配的顾问都将添加到顾问链中。如果你需要添加一组标准的“全球”顾问,这可能会派上用场。以下示例定义了两个全球顾问: + +``` + + + + + global* + + + + + + +``` + +### 6.5.简明代理定义 + +特别是在定义事务代理时,你可能会使用许多类似的代理定义。 Bean 父定义和子定义以及内部 Bean 定义的使用可以导致更干净和更简洁的代理定义。 + +首先,我们为代理创建一个父模板 Bean 定义,如下所示: + +``` + + + + + PROPAGATION_REQUIRED + + + +``` + +这本身从来不是实例化的,因此它实际上可能是不完整的。然后,需要创建的每个代理都是一个子定义 Bean,该定义将代理的目标包装为内部定义 Bean,因为该目标本身永远不会被使用。以下示例显示了这样的子 Bean: + +``` + + + + + + +``` + +你可以从父模板重写属性。在下面的示例中,我们重写事务传播设置: + +``` + + + + + + + + PROPAGATION_REQUIRED,readOnly + PROPAGATION_REQUIRED,readOnly + PROPAGATION_REQUIRED,readOnly + PROPAGATION_REQUIRED + + + +``` + +请注意,在父 Bean 示例中,我们通过将`abstract`属性设置为`true`,显式地将父 Bean 定义标记为抽象的,如所描述的[previously](#beans-child-bean-definitions),以便它实际上可能永远不会被实例化。默认情况下,应用程序上下文(但不是简单的 Bean 工厂)预先实例化所有单例。因此,重要的是(至少对于单例 bean 而言),如果你有一个(父) Bean 定义,只打算用作模板,并且该定义指定了一个类,那么你必须确保将`abstract`属性设置为`true`。否则,应用程序上下文实际上会尝试预先实例化它。 + +### 6.6.用`ProxyFactory`以编程方式创建 AOP 代理 + +使用 Spring 以编程方式创建 AOP 代理是很容易的。这允许你使用 Spring AOP 而不依赖 Spring IOC。 + +由目标对象实现的接口是自动代理的。下面的清单展示了为目标对象创建代理的过程,其中包括一个拦截器和一个顾问: + +Java + +``` +ProxyFactory factory = new ProxyFactory(myBusinessInterfaceImpl); +factory.addAdvice(myMethodInterceptor); +factory.addAdvisor(myAdvisor); +MyBusinessInterface tb = (MyBusinessInterface) factory.getProxy(); +``` + +Kotlin + +``` +val factory = ProxyFactory(myBusinessInterfaceImpl) +factory.addAdvice(myMethodInterceptor) +factory.addAdvisor(myAdvisor) +val tb = factory.proxy as MyBusinessInterface +``` + +第一步是构造一个类型为 `org.SpringFramework. AOP.Framework.ProxyFactory’的对象。你可以使用目标对象来创建这个,就像前面的示例一样,或者指定要在替代构造函数中代理的接口。 + +你可以添加建议(将拦截器作为一种专门的建议)、顾问或两者,并在`ProxyFactory`的生命周期中对它们进行操作。如果你添加了“IntroductionInterceptionAroundVisor”,则可以使代理实现其他接口。 + +在`ProxyFactory`(继承自`AdvisedSupport`)上也有方便的方法,允许你添加其他通知类型,例如 before 和 throws advisedsupport。 + +| |AOP 在大多数
应用程序中,将代理创建与 IOC 框架集成是最佳实践。我们建议你使用 AOP,
将配置从 Java 代码中外部化,就像你通常应该使用的那样。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 6.7.操作被建议的对象 + +无论你如何创建 AOP 代理,你都可以通过使用 `org.springframework. AOP.framework.adved’interface 来操作它们。 AOP 任何代理都可以被强制转换到该接口,无论它实现了哪个其他接口。该接口包括以下方法: + +Java + +``` +Advisor[] getAdvisors(); + +void addAdvice(Advice advice) throws AopConfigException; + +void addAdvice(int pos, Advice advice) throws AopConfigException; + +void addAdvisor(Advisor advisor) throws AopConfigException; + +void addAdvisor(int pos, Advisor advisor) throws AopConfigException; + +int indexOf(Advisor advisor); + +boolean removeAdvisor(Advisor advisor) throws AopConfigException; + +void removeAdvisor(int index) throws AopConfigException; + +boolean replaceAdvisor(Advisor a, Advisor b) throws AopConfigException; + +boolean isFrozen(); +``` + +Kotlin + +``` +fun getAdvisors(): Array + +@Throws(AopConfigException::class) +fun addAdvice(advice: Advice) + +@Throws(AopConfigException::class) +fun addAdvice(pos: Int, advice: Advice) + +@Throws(AopConfigException::class) +fun addAdvisor(advisor: Advisor) + +@Throws(AopConfigException::class) +fun addAdvisor(pos: Int, advisor: Advisor) + +fun indexOf(advisor: Advisor): Int + +@Throws(AopConfigException::class) +fun removeAdvisor(advisor: Advisor): Boolean + +@Throws(AopConfigException::class) +fun removeAdvisor(index: Int) + +@Throws(AopConfigException::class) +fun replaceAdvisor(a: Advisor, b: Advisor): Boolean + +fun isFrozen(): Boolean +``` + +`getAdvisors()`方法为添加到工厂的每个 Advisor、Interceptor 或其他通知类型返回一个`Advisor`。如果你添加了`Advisor`,则此索引处返回的顾问就是你添加的对象。如果你添加了一个拦截器或其他通知类型, Spring 将其包装在一个带有切入点的 Advisor 中,该切入点总是返回`true`。因此,如果你添加了一个`MethodInterceptor`,则为该索引返回的 advisor 是一个`DefaultPointcutAdvisor`,它返回你的 `MethodInterceptor’和一个匹配所有类和方法的切入点。 + +`addAdvisor()`方法可用于添加任何`Advisor`。通常,持有切入点和建议的顾问是通用的`DefaultPointcutAdvisor`,你可以将其用于任何建议或切入点(但不用于介绍)。 + +默认情况下,即使创建了代理,也可以添加或删除顾问或拦截器。唯一的限制是,不可能添加或删除 IntroductionAdvisor,因为来自工厂的现有代理不会显示接口更改。(你可以从工厂获得一个新的代理,以避免此问题。 + +下面的示例显示了将 AOP 代理强制转换到`Advised`接口并检查和操作其建议: + +Java + +``` +Advised advised = (Advised) myObject; +Advisor[] advisors = advised.getAdvisors(); +int oldAdvisorCount = advisors.length; +System.out.println(oldAdvisorCount + " advisors"); + +// Add an advice like an interceptor without a pointcut +// Will match all proxied methods +// Can use for interceptors, before, after returning or throws advice +advised.addAdvice(new DebugInterceptor()); + +// Add selective advice using a pointcut +advised.addAdvisor(new DefaultPointcutAdvisor(mySpecialPointcut, myAdvice)); + +assertEquals("Added two advisors", oldAdvisorCount + 2, advised.getAdvisors().length); +``` + +Kotlin + +``` +val advised = myObject as Advised +val advisors = advised.advisors +val oldAdvisorCount = advisors.size +println("$oldAdvisorCount advisors") + +// Add an advice like an interceptor without a pointcut +// Will match all proxied methods +// Can use for interceptors, before, after returning or throws advice +advised.addAdvice(DebugInterceptor()) + +// Add selective advice using a pointcut +advised.addAdvisor(DefaultPointcutAdvisor(mySpecialPointcut, myAdvice)) + +assertEquals("Added two advisors", oldAdvisorCount + 2, advised.advisors.size) +``` + +| |在生产中修改对
业务对象的建议是否可取(没有双关语意思)是值得怀疑的,尽管毫无疑问存在合法的使用情况。
但是,它在开发中(例如在测试中)可能非常有用。我们有时发现
能够以拦截器或其他
建议的形式添加测试代码非常有用,从而进入我们想要测试的方法调用。(例如,建议可以
获取为该方法创建的事务的内部,也许可以运行 SQL 来检查
数据库是否已正确更新,然后标记该事务以进行回滚。)| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +根据创建代理的方式,通常可以设置`frozen`标志。在这种情况下,`Advised``isFrozen()`方法返回`true`,任何通过添加或删除修改建议的尝试都会导致`AopConfigException`。在某些情况下,冻结被建议对象的状态的能力是有用的(例如,可以防止调用代码删除安全拦截器)。 + +### 6.8.使用“自动代理”功能 + +到目前为止,我们已经考虑了通过使用`ProxyFactoryBean`或类似的工厂 Bean 显式地创建 AOP 代理。 + +Spring 还允许我们使用“自动代理” Bean 定义,其可以自动代理选择 Bean 定义。这是建立在 Spring 的“ Bean 后处理器”基础设施上的,该基础设施允许在容器加载时修改任何 Bean 定义。 + +在这个模型中,你在 XML Bean 定义文件中设置了一些特殊的 Bean 定义,以配置自动代理基础设施。这允许你声明有资格进行自动代理的目标。你不需要使用`ProxyFactoryBean`。 + +有两种方法可以做到这一点: + +* 通过使用在当前上下文中引用特定 bean 的自动代理创建器。 + +* 一个值得单独考虑的自动代理创建的特殊情况:由源级元数据属性驱动的自动代理创建。 + +#### 6.8.1.自动代理 Bean 定义 + +本节介绍了由“org.springframework. AOP.framework.autoproxy”软件包提供的自动代理创建者。 + +##### `BeanNameAutoProxyCreator` + +`BeanNameAutoProxyCreator`类是一个`BeanPostProcessor`类,它自动为名称与文字值或通配符匹配的 bean 创建 AOP 代理。下面的示例展示了如何创建`BeanNameAutoProxyCreator` Bean: + +``` + + + + + myInterceptor + + + +``` + +与`ProxyFactoryBean`一样,有一个`interceptorNames`属性,而不是拦截器列表,以允许原型顾问的正确行为。被命名的“拦截器”可以是顾问,也可以是任何类型的建议。 + +与一般的自动代理一样,使用`BeanNameAutoProxyCreator`的主要目的是以最小的配置量将相同的配置一致地应用于多个对象。它是将声明式事务应用于多个对象的流行选择。 + +Bean 名称匹配的定义,例如前面示例中的`jdkMyBean`和`onlyJdk`,是与目标类完全一致的旧定义 Bean。 AOP 代理由`BeanNameAutoProxyCreator`自动创建。同样的建议也适用于所有匹配的 bean。请注意,如果使用了 Advisors(而不是前面示例中的拦截器),那么切入点可能会以不同的方式应用于不同的 bean。 + +##### `DefaultAdvisorAutoProxyCreator` + +一个更通用且功能极其强大的自动代理创建器是“DefaultVisorAutoProxyCreator”。这自动地在当前上下文中应用合格的顾问,而不需要在自动代理顾问的 Bean 定义中包括特定的 Bean 名称。它提供了与`BeanNameAutoProxyCreator`相同的优点,即配置一致和避免重复。 + +使用这一机制涉及: + +* 指定`DefaultAdvisorAutoProxyCreator` Bean 定义。 + +* 在相同或相关的上下文中指定任意数量的顾问。请注意,这些必须是顾问,而不是拦截器或其他建议。这是必要的,因为必须有一个切入点来进行评估,以检查每个建议对候选人 Bean 定义的资格。 + +`DefaultAdvisorAutoProxyCreator`会自动计算每个 Advisor 中包含的切入点,以查看它应该对每个业务对象应用什么(如果有的话)建议(例如示例中的`businessObject1`和`businessObject2`)。 + +这意味着可以将任意数量的顾问自动应用到每个业务对象。如果任何顾问中没有切入点与业务对象中的任何方法匹配,则不代理该对象。 Bean 在为新的业务对象添加定义时,如果有必要,会自动代理它们。 + +通常,自动代理的优点是使调用者或依赖项不可能获得未通知的对象。在这个 `ApplicationContext’上调用`getBean("businessObject1")`返回一个 AOP 代理,而不是目标业务对象。(前面展示的“内心 Bean”成语也提供了这一好处。 + +下面的示例创建了`DefaultAdvisorAutoProxyCreator` Bean 和本节讨论的其他元素: + +``` + + + + + + + + + + + + + +``` + +如果你希望将相同的建议一致地应用于许多业务对象,那么`DefaultAdvisorAutoProxyCreator`非常有用。一旦基础设施定义到位,你就可以添加新的业务对象,而不需要包括特定的代理配置。你还可以很容易地删除其他方面(例如,跟踪或性能监视方面),只需对配置进行最小的更改。 + +`DefaultAdvisorAutoProxyCreator`提供了对过滤和排序的支持(通过使用命名约定,以便只对某些顾问进行评估,这允许在同一工厂中使用多个配置不同的顾问或自动代理创建者)。如果出现问题,Advisors 可以实现`org.springframework.core.Ordered`接口,以确保正确的排序。前面示例中使用的`TransactionAttributeSourceAdvisor`具有可配置的订单值。默认设置是无序的。 + +### 6.9.使用`TargetSource`实现 + +Spring 提供了`TargetSource`的概念,在 `org.SpringFramework. AOP.targetSource` 接口中表示。这个接口负责返回实现连接点的“目标对象”。 AOP 代理每次处理方法调用时,都会对`TargetSource`实现请求一个目标实例。 + +使用 Spring AOP 的开发人员通常不需要直接使用`TargetSource`实现,但是这提供了一种支持池、热插拔和其他复杂目标的强大手段。例如,池`TargetSource`可以通过使用池来管理实例,为每次调用返回不同的目标实例。 + +如果没有指定`TargetSource`,则使用默认实现来包装本地对象。每次调用都返回相同的目标(如你所料)。 + +本节的其余部分描述了 Spring 提供的标准目标源,以及如何使用它们。 + +| |当使用自定义目标源时,你的目标通常需要是一个原型
,而不是一个单例定义 Bean。这允许 Spring 在需要时创建新的目标
实例。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 6.9.1.热插拔目标源 + +`org.springframework.aop.target.HotSwappableTargetSource`的存在是为了让 AOP 代理的目标被切换,同时让调用者保留对它的引用。 + +更改目标源的目标将立即生效。“HotSwappleTargetSource”是线程安全的。 + +你可以在 HotswappleTargetSource 上使用`swap()`方法来更改目标,如下例所示: + +Java + +``` +HotSwappableTargetSource swapper = (HotSwappableTargetSource) beanFactory.getBean("swapper"); +Object oldTarget = swapper.swap(newTarget); +``` + +Kotlin + +``` +val swapper = beanFactory.getBean("swapper") as HotSwappableTargetSource +val oldTarget = swapper.swap(newTarget) +``` + +下面的示例展示了所需的 XML 定义: + +``` + + + + + + + + + +``` + +前面的`swap()`调用更改了可交换对象的目标 Bean。持有对该 Bean 的引用的客户不知道该更改,但立即开始命中新目标。 + +虽然此示例不添加任何建议(不需要添加建议来使用`TargetSource`),但任何`TargetSource`都可以与任意建议一起使用。 + +#### 6.9.2.汇集目标源 + +使用池目标源提供了类似于无状态会话 EJB 的编程模型,在这种模型中,将维护一个由相同实例组成的池,方法调用将释放池中的对象。 + +Spring 池和 SLSB 池之间的一个关键区别是 Spring 池可以应用于任何 POJO。 Spring 与一般情况下一样,该服务可以以非侵入性的方式应用。 + +Spring 提供了对 Commons Pool2.2 的支持,其提供了相当有效的池实现。你需要应用程序的 Classpath 上的`commons-pool` jar 才能使用此功能。你还可以子类 `org.springframework. AOP.target.abstractpoolingtargetSource’来支持任何其他池 API。 + +| |Commons Pool1.5+ 也受到支持,但在 Spring Framework4.2 中已不受欢迎。| +|---|---------------------------------------------------------------------------------| + +下面的清单展示了一个配置示例: + +``` + + ... properties omitted + + + + + + + + + + + +``` + +请注意,目标对象(在前面的示例中是“BusinessObjectTarget”)必须是一个原型。这使得`PoolingTargetSource`实现可以根据需要创建目标的新实例来增加池。请参阅[Javadoc’AbstractPoolingTargetSource’](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/aop/target/AbstractPoolingTargetSource.html)和你希望使用的具体子类,以获取有关其属性的信息。`maxSize`是最基本的,并且总是保证存在。 + +在这种情况下,`myInterceptor`是需要在相同的 IOC 上下文中定义的拦截器的名称。但是,你不需要指定拦截器来使用池。如果你只想要池,而不想要其他的建议,那么根本不要设置“拦截器名称”属性。 + +你可以将 Spring 配置为能够将任何池对象强制转换到 `org.springframework. AOP.target.poolingconfig` 接口,该接口通过介绍公开有关池的配置和当前大小的信息。你需要定义类似于以下内容的顾问: + +``` + + + + +``` + +这个顾问是通过调用“AbstractPoolingTargetSource”类上的方便方法获得的,因此使用`MethodInvokingFactoryBean`。此顾问的名称(这里是“poolconfigAdvisor”)必须位于公开池对象的`ProxyFactoryBean`中的拦截器名称列表中。 + +强制转换的定义如下: + +Java + +``` +PoolingConfig conf = (PoolingConfig) beanFactory.getBean("businessObject"); +System.out.println("Max pool size is " + conf.getMaxSize()); +``` + +Kotlin + +``` +val conf = beanFactory.getBean("businessObject") as PoolingConfig +println("Max pool size is " + conf.maxSize) +``` + +| |通常不需要池无状态的服务对象。我们不认为它应该是
的默认选择,因为大多数无状态对象自然是线程安全的,并且如果资源被缓存,实例
池是有问题的。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +通过使用自动代理,可以获得更简单的池。你可以设置任何自动代理创建者使用的`TargetSource`实现。 + +#### 6.9.3.原型目标源 + +设置“原型”目标源类似于设置池`TargetSource`。在这种情况下,每个方法调用都会创建一个新的目标实例。尽管在现代 JVM 中创建新对象的成本并不高,但连接新对象(满足其 IoC 依赖关系)的成本可能更高。因此,如果没有很好的理由,就不应该使用这种方法。 + +为此,你可以修改前面显示的`poolTargetSource`定义,如下所示(为了清楚起见,我们还更改了名称): + +``` + + + +``` + +唯一的属性是目标的名称 Bean。继承在“TargetSource”实现中使用,以确保一致的命名。与池目标源一样,目标 Bean 必须是原型 Bean 定义。 + +#### 6.9.4.`ThreadLocal`目标来源 + +如果需要为每个传入请求(每个线程)创建一个对象,`ThreadLocal`目标源是有用的。`ThreadLocal`的概念提供了一个 JDK 范围内的功能,可以在线程旁边透明地存储资源。设置“ThreadlocalTargetSource”与解释其他类型的目标源几乎相同,如下例所示: + +``` + + + +``` + +| |当
实例在多线程和多类加载器环境中不正确地使用它们时,会出现严重的问题(可能导致内存泄漏)。你
应该始终考虑在其他类中包装 ThreadLocal,并且永远不要直接使用
`ThreadLocal`本身(包装类中除外)。此外,你还应该
始终记住正确地设置和取消设置(后者只涉及对 `threadlocal.set(null)’的调用)线程本地的资源。在
任何情况下都应该进行重置,因为不进行重置可能会导致有问题的行为。 Spring 的“ThreadLocal”支持为你实现了这一点,并且应该始终考虑使用“ThreadLocal”实例,而不使用其他适当的处理代码。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 6.10.定义新的建议类型 + +Spring AOP 被设计为可扩展的。虽然拦截实现策略目前在内部使用,但除了围绕建议、抛出建议之前、返回建议之后的拦截之外,还可能支持任意的建议类型。 + +`org.springframework.aop.framework.adapter`包是一个 SPI 包,它允许在不改变核心框架的情况下添加对新的自定义建议类型的支持。对自定义`Advice`类型的唯一限制是,它必须实现 `org.aopalliance. AOP.advice` 标记接口。 + +有关更多信息,请参见[`org.springframework.aop.framework.adapter`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/aop/framework/adapter/package-frame.html)Javadoc。 + +## 7. 零安全 + +虽然 Java 不允许你用它的类型系统来表示空安全性,但是 Spring 框架现在在`org.springframework.lang`包中提供了以下注释,允许你声明 API 和字段的空性: + +* [`@Nullable`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/lang/Nullable.html):表示特定参数、返回值或字段可以是`null`的注释。 + +* [`@NonNull`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/lang/NonNull.html):表示特定参数、返回值或字段不能`null`的注释(对于参数/返回值和分别应用`@NonNullApi`和`@NonNullFields`的字段不需要)。 + +* [`@NonNullApi`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/lang/NonNullApi.html):在包级别的注释,声明非 null 作为参数和返回值的默认语义。 + +* [@nonnullfields’](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/lang/NonNullFields.html):在包级别的注释,声明非 null 作为字段的默认语义。 + +Spring 框架本身利用了这些注释,但它们也可以在任何基于 Spring 的 Java 项目中用于声明空安全的 API 和可选的空安全字段。泛型类型参数、varargs 和数组元素的可空性目前还不受支持,但应该在即将发布的版本中得到支持,有关最新信息,请参见[SPR-15942](https://jira.spring.io/browse/SPR-15942)。预计在 Spring 框架版本(包括较小的版本)之间会对可否定性声明进行微调。在方法体中使用的类型的可空性不在此特性的范围内。 + +| |诸如 Reactor 和 Spring Data 之类的其他公共库提供了空安全 API,
使用类似的空性安排,为
Spring 应用程序开发人员提供了一致的整体体验。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 7.1.用例 + +除了为 Spring Framework API nullability 提供显式声明外,IDE 还可以使用这些注释(例如 Idea 或 Eclipse)来提供与空安全相关的有用的警告,以便在运行时避免`NullPointerException`。 + +它们还用于在 Kotlin 项目中使 Spring API 为空安全的,因为 Kotlin 原生地支持[null-safety](https://kotlinlang.org/docs/reference/null-safety.html)。更多详细信息请参见[Kotlin support documentation](languages.html#kotlin-null-safety)。 + +### 7.2.JSR-305 元注释 + +Spring 注释是用[JSR 305](https://jcp.org/en/jsr/detail?id=305)注释(一种休眠但广泛传播的 JSR)进行元注释的。JSR-305 元注释允许 Idea 或 Kotlin 之类的工具供应商以通用的方式提供空安全支持,而无需对 Spring 注释进行硬编码支持。 + +为了利用 Spring 空安全 API,没有必要也不建议向项目 Classpath 添加 JSR-305 依赖项。只有基于 Spring 的库等在其代码库中使用空安全注释的项目才应该添加`com.google.code.findbugs:jsr305:3.0.2`带有`compileOnly` Gradle 配置或 Maven `provided`作用域,以避免编译警告。 + +## 8. 数据缓冲区和编解码器 + +Java 蔚来提供了`ByteBuffer`,但是许多库在上面构建自己的字节缓冲区 API,特别是在网络操作中,重用缓冲区和/或使用直接缓冲区有利于性能。例如,Netty 具有`ByteBuf`层次结构, Undertow 使用 XNIO, Jetty 使用带有要释放的回调的池字节缓冲区,以此类推。`spring-core`模块提供了一组用于处理各种字节缓冲区 API 的抽象,如下所示: + +* [“数据库工厂”](#databuffers-factory)抽象了数据缓冲区的创建。 + +* [`DataBuffer`](#databuffers-buffer)表示一个字节缓冲区,它可能是[pooled](#databuffers-buffer-pooled)。 + +* [` 数据库’](#databuffers-utils)提供了用于数据缓冲区的实用方法。 + +* [Codecs](#codecs)将数据缓冲流解码或编码到更高级别的对象中。 + +### 8.1.`DataBufferFactory` + +`DataBufferFactory`用于以以下两种方式之一创建数据缓冲区: + +1. 分配一个新的数据缓冲区,如果已知,可以选择预先指定容量,这是更有效的,即使`DataBuffer`的实现可以按需增长和收缩。 + +2. 包装现有的`byte[]`或`java.nio.ByteBuffer`,它使用`DataBuffer`实现来装饰给定数据,并且不涉及分配。 + +注意,WebFlux 应用程序不会直接创建`DataBufferFactory`,而是通过客户端的`ServerHttpResponse`或`ClientHttpRequest`访问它。工厂的类型取决于底层客户机或服务器,例如,对于反应堆网络,“NettyDatabufferFactory”,对于其他网络,`DefaultDataBufferFactory`。 + +### 8.2.`DataBuffer` + +`DataBuffer`接口提供了与`java.nio.ByteBuffer`类似的操作,但也带来了一些额外的好处,其中一些是受 netty`ByteBuf`的启发。以下是部分福利清单: + +* 以独立的位置读写,即不需要调用`flip()`来交替读写。 + +* 容量随需求增加,如`java.lang.StringBuilder`。 + +* 通过[“PooledDatabuffer”](#databuffers-buffer-pooled)池缓冲区和引用计数。 + +* 将缓冲区查看为`java.nio.ByteBuffer`,`InputStream`,或`OutputStream`。 + +* 确定给定字节的索引或最后一个索引。 + +### 8.3.`PooledDataBuffer` + +正如在[ByteBuffer](https://docs.oracle.com/javase/8/docs/api/java/nio/ByteBuffer.html)的 Javadoc 中所解释的,字节缓冲区可以是直接的,也可以是非直接的。直接缓冲区可以驻留在 Java 堆之外,从而消除了对本机 I/O 操作的复制需求。这使得直接缓冲区在通过套接字接收和发送数据时特别有用,但它们的创建和发布也更昂贵,这就产生了池缓冲区的想法。 + +`PooledDataBuffer`是`DataBuffer`的扩展,它有助于引用计数,这对于字节缓冲池来说是必不可少的。它是如何工作的?当分配了`PooledDataBuffer`时,引用计数为 1。调用`retain()`来增加计数,而调用`release()`来减少计数。只要计数大于 0,缓冲区就保证不会被释放。当计数减少到 0 时,可以释放池中的缓冲区,这实际上可能意味着将为缓冲区保留的内存返回到内存池。 + +请注意,在大多数情况下,与其直接在`PooledDataBuffer`上进行操作,不如使用`DataBufferUtils`中的便利方法,这些方法仅在`PooledDataBuffer`的实例的情况下才将释放或保留应用于 `databuffer’。 + +### 8.4.`DataBufferUtils` + +`DataBufferUtils`提供了许多对数据缓冲区进行操作的实用方法: + +* 如果底层的 Byte Buffer API 支持,那么将数据缓冲流连接到一个可能没有复制的缓冲区中,例如通过复合缓冲区。 + +* 将`InputStream`或蔚来`Channel`转换为`Flux`,反之亦然,将 publisher转换为`OutputStream`或蔚来`Channel`。 + +* 如果缓冲区是“PooledDatabuffer”的实例,则释放或保留`DataBuffer`的方法。 + +* 从一个字节流中跳过或获取,直到一个特定的字节计数。 + +### 8.5.编解码器 + +`org.springframework.core.codec`包提供了以下策略接口: + +* `Encoder`将`Publisher`编码到数据缓冲流中。 + +* `Decoder`将`Publisher`解码为更高级别的对象流。 + +`spring-core`模块提供`byte[]`、`ByteBuffer`、`DataBuffer`、`Resource`和 `string’编码器和解码器实现。`spring-web`模块添加了 JacksonJSON、JacksonSmile、JAXB2、协议缓冲区和其他编码器和解码器。参见 WebFlux 部分中的[Codecs](web-reactive.html#webflux-codecs)。 + +### 8.6.使用`DataBuffer` + +在使用数据缓冲区时,必须特别注意确保缓冲区被释放,因为它们可能是[pooled](#databuffers-buffer-pooled)。我们将使用编解码器来说明这是如何工作的,但这些概念更普遍地适用。让我们来看看编解码器内部必须做什么来管理数据缓冲区。 + +a`Decoder`是在创建更高级别的对象之前最后读取输入数据缓冲区的方法,因此它必须按以下方式释放它们: + +1. 如果`Decoder`只读取每个输入缓冲区并准备立即释放它,则可以通过`DataBufferUtils.release(dataBuffer)`执行。 + +2. 如果`Decoder`使用`Flux`或`Mono`运算符,如`flatMap`,`reduce`,以及其他在内部预取和缓存数据项的运算符,或使用诸如 `filter’,`skip`等运算符的运算符,则使用 `doondiscard(pooleddatuffer.class,必须将 databufferutils::release)添加到合成链中,以确保此类缓冲区在被丢弃之前被释放,也可能是由于错误或取消信号的结果。 + +3. 如果`Decoder`以任何其他方式保持一个或多个数据缓冲区,则必须确保它们在完全读取时被释放,或者在缓存的数据缓冲区被读取和释放之前发生错误或取消信号的情况下被释放。 + +请注意,`DataBufferUtils#join`提供了一种安全有效的方法,可以将数据缓冲流聚合到单个数据缓冲区中。同样,`skipUntilByteCount`和“takeuntilbytecount”也是供解码器使用的额外安全方法。 + +`Encoder`分配其他人必须读取(并释放)的数据缓冲区。所以`Encoder`不需要做太多的事情。但是,如果在用数据填充缓冲区时出现序列化错误,`Encoder`必须注意释放数据缓冲区。例如: + +Java + +``` +DataBuffer buffer = factory.allocateBuffer(); +boolean release = true; +try { + // serialize and populate buffer.. + release = false; +} +finally { + if (release) { + DataBufferUtils.release(buffer); + } +} +return buffer; +``` + +Kotlin + +``` +val buffer = factory.allocateBuffer() +var release = true +try { + // serialize and populate buffer.. + release = false +} finally { + if (release) { + DataBufferUtils.release(buffer) + } +} +return buffer +``` + +`Encoder`的使用者负责释放它接收到的数据缓冲区。在 WebFlux 应用程序中,`Encoder`的输出用于写到 HTTP 服务器的响应,或写到客户端的 HTTP 请求,在这种情况下,释放数据缓冲区是负责将代码写到服务器响应,或写到客户端请求。 + +请注意,在 Netty 上运行时,有[缓冲区泄漏故障排除](https://github.com/netty/netty/wiki/Reference-counted-objects#troubleshooting-buffer-leaks)的调试选项。 + +## 9. 伐木 + +自 Spring Framework5.0 以来, Spring 自带了在`spring-jcl`模块中实现的 Commons 日志记录桥。该实现检查 Classpath 中是否存在日志 4j2.x API 和 SLF4j1.7API,并使用其中发现的第一个作为日志实现,如果 log4j2.x 和 SLF4j 都不可用,则返回到 Java 平台的核心日志记录工具(也称为*JUL*或`java.util.logging`)。 + +在 Classpath 中放置 log4j2.x 或 logback(或另一个 SLF4j 提供程序),不需要任何额外的桥接器,并让框架自动适应你的选择。有关更多信息,请参见[Spring Boot Logging Reference Documentation](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-logging)。 + +| |Spring 的 Commons 日志记录变体仅用于核心框架和扩展中的基础设施日志记录
目的。

对于应用程序代码中的日志记录需求,请直接使用 log4j2.x、SLF4j 或 jul。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +可以通过`org.apache.commons.logging.LogFactory`检索`Log`实现,如下面的示例所示。 + +爪哇 + +``` +public class MyBean { + private final Log log = LogFactory.getLog(getClass()); + // ... +} +``` + +Kotlin + +``` +class MyBean { + private val log = LogFactory.getLog(javaClass) + // ... +} +``` + +## 10. 附录 + +### 10.1.XML 模式 + +附录的这一部分列出了与核心容器相关的 XML 模式。 + +#### 10.1.1.`util`模式 + +顾名思义,`util`标记处理常见的实用程序配置问题,例如配置集合、引用常量等等。要在`util`模式中使用标记,你需要在 Spring XML 配置文件的顶部具有以下前导符(代码片段中的文本引用了正确的模式,以便你可以使用`util`命名空间中的标记): + +``` + + + + + + +``` + +##### 使用`` + +考虑以下 Bean 定义: + +``` + + + + + +``` + +前面的配置使用 Spring `FactoryBean`实现(“fieldretrievingFactoryBean”)将 Bean 上的`isolation`属性的值设置为`java.sql.Connection.TRANSACTION_SERIALIZABLE`常量的值。这一切都很好,但它很冗长,(不必要地)向最终用户暴露了 Spring 的内部管道。 + +以下基于 XML 模式的版本更简洁,清楚地表达了开发人员的意图(“注入这个常量值”),并且读起来更好: + +``` + + + + + +``` + +###### 从字段值设置 Bean 属性或构造函数参数 + +[“fieldretrievingfactorybean”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/factory/config/FieldRetrievingFactoryBean.html)是一个`FactoryBean`,用于检索`static`或非静态字段的值。它通常用于检索`public``static`常量,然后可用于为另一个 Bean 设置属性值或构造函数参数。 + +下面的示例展示了如何使用`static`)属性公开[`staticField`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/factory/config/FieldRetrievingFactoryBean.html#setStaticField(java.lang.String)字段: + +``` + + + +``` + +还有一种方便用法表单,其中`static`字段被指定为 Bean 名称,如下例所示: + +``` + +``` + +这确实意味着,在 Bean `id`中不再有任何选择(因此,任何其他涉及它的 Bean 也必须使用这个更长的名称),但是这种形式的定义非常简洁,并且非常方便地用作内部 Bean,因为`id`不必为 Bean 引用指定,如下面的示例所示: + +``` + + + + + +``` + +你还可以访问另一个 Bean 的非静态(实例)字段,如[“fieldretrievingfactorybean”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/factory/config/FieldRetrievingFactoryBean.html)类的 API 文档中所述。 + +在 Spring 中,很容易将枚举值作为属性或构造函数参数注入到 bean 中。实际上,你不需要做任何事情,也不需要知道任何关于 Spring 内部(或者甚至关于类,例如`FieldRetrievingFactoryBean`)的事情。下面的例举枚举显示了注入一个枚举值是多么容易: + +爪哇 + +``` +package javax.persistence; + +public enum PersistenceContextType { + + TRANSACTION, + EXTENDED +} +``` + +Kotlin + +``` +package javax.persistence + +enum class PersistenceContextType { + + TRANSACTION, + EXTENDED +} +``` + +现在考虑下面的类型`PersistenceContextType`的 setter 和相应的 Bean 定义: + +爪哇 + +``` +package example; + +public class Client { + + private PersistenceContextType persistenceContextType; + + public void setPersistenceContextType(PersistenceContextType type) { + this.persistenceContextType = type; + } +} +``` + +Kotlin + +``` +package example + +class Client { + + lateinit var persistenceContextType: PersistenceContextType +} +``` + +``` + + + +``` + +##### 使用`` + +考虑以下示例: + +``` + + + + + + + + + + + + +``` + +前面的配置使用 Spring `FactoryBean`实现(’PropertyPathFactoryBean’)来创建 Bean(类型为`int`),该 Bean(类型为`testBean.age`),其值等于`testBean` Bean 的`age`属性。 + +现在考虑下面的示例,它添加了``元素: + +``` + + + + + + + + + + + + +``` + +``元素的`path`属性的值遵循’beanname.beanproperty’的形式。在这种情况下,它获取名为 `TestBean’的 Bean 的`age`属性。`age`属性的值是`10`。 + +###### 使用``设置 Bean 属性或构造函数参数 ##### + +`PropertyPathFactoryBean`是一个`FactoryBean`,它计算给定目标对象上的属性路径。目标对象可以直接指定,也可以通过 Bean 名称指定。然后,你可以在另一个 Bean 定义中使用该值作为属性值或构造函数参数。 + +下面的示例按名称显示了针对另一个 Bean 使用的路径: + +``` + + + + + + + + + + + + + + + +``` + +在下面的示例中,根据内部 Bean 对路径进行求值: + +``` + + + + + + + + + +``` + +还有一种快捷方式,其中 Bean 名称是属性路径。下面的示例显示了快捷方式: + +``` + + +``` + +这种形式确实意味着在 Bean 的名称中没有选择。对它的任何引用也必须使用相同的`id`,这是路径。如果用作内部 Bean,则根本不需要引用它,如下例所示: + +``` + + + + + +``` + +你可以在实际定义中专门设置结果类型。对于大多数用例来说,这并不是必需的,但它有时是有用的。有关此功能的更多信息,请参见 爪哇doc。 + +##### 使用`` + +考虑以下示例: + +``` + + + + +``` + +前面的配置使用 Spring `FactoryBean`实现(“propertiesFactoryBean”)实例化一个`java.util.Properties`实例,该实例的值来自提供的[`Resource`](#resources)位置)。 + +下面的示例使用`util:properties`元素来进行更简洁的表示: + +``` + + +``` + +##### 使用`` + +考虑以下示例: + +``` + + + + + [email protected] + [email protected] + [email protected] + [email protected] + + + +``` + +前面的配置使用 Spring `FactoryBean`实现(“listFactoryBean”)来创建`java.util.List`实例,并使用从提供的`sourceList`中获取的值对其进行初始化。 + +下面的示例使用``元素来进行更简洁的表示: + +``` + + + [email protected] + [email protected] + [email protected] + [email protected] + +``` + +还可以使用``元素上的`list-class`属性显式地控制实例化和填充的`List`的确切类型。例如,如果我们确实需要一个`java.util.LinkedList`来实例化,我们可以使用以下配置: + +``` + + [email protected] + [email protected] + [email protected] + d'[email protected] + +``` + +如果没有提供`list-class`属性,则容器选择一个`List`实现。 + +##### 使用`` + +考虑以下示例: + +``` + + + + + + + + + + + +``` + +前面的配置使用 Spring `FactoryBean`实现来创建`java.util.Map`实例,该实例是用从提供的`'sourceMap'`中获取的键值对初始化的。 + +下面的示例使用``元素来进行更简洁的表示: + +``` + + + + + + + +``` + +还可以使用``元素上的`'map-class'`属性显式地控制实例化和填充的`Map`的确切类型。例如,如果我们确实需要一个`java.util.TreeMap`来实例化,我们可以使用以下配置: + +``` + + + + + + +``` + +如果没有提供`'map-class'`属性,则容器选择一个`Map`实现。 + +##### 使用`` + +考虑以下示例: + +``` + + + + + [email protected] + [email protected] + [email protected] + [email protected] + + + +``` + +前面的配置使用 Spring `FactoryBean`实现(“setFactoryBean”)创建`java.util.Set`实例,该实例初始化后的值取自所提供的`sourceSet`。 + +下面的示例使用``元素来进行更简洁的表示: + +``` + + + [email protected] + [email protected] + [email protected] + [email protected] + +``` + +还可以使用``元素上的`set-class`属性显式地控制实例化和填充的`Set`的确切类型。例如,如果我们确实需要一个`java.util.TreeSet`来实例化,我们可以使用以下配置: + +``` + + [email protected] + [email protected] + [email protected] + [email protected] + +``` + +如果没有提供`set-class`属性,则容器选择一个`Set`实现。 + +#### 10.1.2.`aop`模式 + +`aop`标记用于配置 Spring 中的所有内容,包括 Spring 自己的基于代理的 AOP 框架和 Spring 与 AspectJ AOP 框架的集成。这些标签在标题为[Aspect Oriented Programming with Spring](#aop)的一章中得到了全面的介绍。 + +为了完整起见,要使用`aop`模式中的标记,你需要在 Spring XML 配置文件的顶部具有以下前导符(代码片段中的文本引用了正确的模式,以便你可以使用`aop`名称空间中的标记): + +``` + + + + + + +``` + +#### 10.1.3.`context`模式 + +`context`标记处理的是与管道相关的`ApplicationContext`配置——也就是说,通常不是对最终用户很重要的 bean,而是在 Spring 中执行许多“grunt”工作的 bean,例如`BeanfactoryPostProcessors`。下面的代码片段引用了正确的模式,因此`context`名称空间中的元素对你是可用的: + +``` + + + + + + +``` + +##### 使用`` + +此元素激活替换`${…​}`占位符,这些占位符是根据指定的属性文件解析的(如[Spring resource location](#resources))。这个元素是一个方便的机制,它为你设置了[`PropertySourcesPlaceHolderConfigurer’](#beans-factory-placeholderconfigurer)。如果你需要更多地控制特定的“PropertySourcesPlaceHolderConfigurer”设置,那么你可以自己将其明确定义为 Bean。 + +##### 使用`` + +此元素激活 Spring 基础结构以检测 Bean 类中的注释: + +* Spring 的[@configuration](#beans-factory-metadata)模型 + +* [“@autowired”/“@inject”](#beans-annotation-config),`@Value`,和`@Lookup` + +* JSR-250 的`@Resource`,`@PostConstruct`,和`@PreDestroy`(如果有) + +* JAX-WS 的`@WebServiceRef`和 EJB3 的`@EJB`(如果可用) + +* JPA 的`@PersistenceContext`和`@PersistenceUnit`(如果有) + +* Spring 的[@eventlistener](#context-functionality-events-annotation) + +或者,你可以选择显式地为这些注释激活单独的`BeanPostProcessors`。 + +| |此元素不激活 Spring 的[@transactional`](data-access.html#transaction-declarative-annotations)注释的处理;
你可以为此目的使用[``](data-access.html#tx-decl-explained)元素。类似地, Spring 的[缓存注释](integration.html#cache-annotations)也需要显式地[enabled](integration.html#cache-annotation-enable)。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 使用`` + +这个元素在[基于注释的容器配置](#beans-annotation-config)一节中有详细介绍。 + +##### 使用`` + +这个元素在[load-time weaving with AspectJ in the Spring Framework](#aop-aj-ltw)一节中有详细介绍。 + +##### 使用`` + +这个元素在[using AspectJ to dependency inject domain objects with Spring](#aop-atconfigurable)一节中有详细介绍。 + +##### 使用`` + +这个元素在[配置基于注释的 MBean 导出](integration.html#jmx-context-mbeanexport)一节中有详细介绍。 + +#### 10.1.4.Beans 模式 + +最后但并非最不重要的是,我们有`beans`模式中的元素。这些要素自该框架诞生之日起就存在于 Spring 中。这里没有显示`beans`模式中的各种元素的示例,因为它们在[详细介绍依赖关系和配置](#beans-factory-properties-detailed)(实际上,在整个[chapter](#beans)中)中得到了相当全面的覆盖。 + +请注意,你可以向``XML 定义添加零个或多个键值对。使用这个额外的元数据所做的事情完全取决于你自己的自定义逻辑(因此,通常只有当你编写自己的自定义元素时才会使用,该自定义元素在标题为[XML 模式创作](#xml-custom)的附录中进行了描述)。 + +下面的示例在周围的``的上下文中显示了``元素(请注意,如果没有任何逻辑来解释它,元数据实际上就没有用了)。 + +``` + + + + + (1) + + + + +``` + +|**1**|这就是`meta`元素的示例| +|-----|----------------------------------| + +在前面的示例中,你可以假设有一些逻辑使用 Bean 定义,并设置了一些使用所提供的元数据的缓存基础设施。 + +### 10.2.XML 模式创作 + +自版本 2.0 以来, Spring 已经提供了一种机制,用于在用于定义和配置 bean 的基本 Spring XML 格式中添加基于模式的扩展。本节介绍如何编写你自己的定制 XML Bean 定义解析器,并将这些解析器集成到 Spring IOC 容器中。 + +Spring 的可扩展 XML 配置机制是基于 XML 模式的,为了便于编写使用模式感知 XML 编辑器的配置文件。如果你不熟悉 Spring 标准 Spring 发行版附带的当前 XML 配置扩展,那么你应该首先阅读关于[XML Schemas](#xsd-schemas)的上一节。 + +要创建新的 XML 配置扩展: + +1. [Author](#xsd-custom-schema)描述自定义元素的 XML 模式。 + +2. [Code](#xsd-custom-namespacehandler)一个自定义的`NamespaceHandler`实现。 + +3. [Code](#xsd-custom-parser)一个或多个`BeanDefinitionParser`实现(这是完成实际工作的地方)。 + +4. [Register](#xsd-custom-registration)带有 Spring 的新工件。 + +对于一个统一的示例,我们创建了一个 XML 扩展(一个自定义 XML 元素),它允许我们配置类型为“SimpleDateFormat”的对象(来自`java.text`包)。完成后,我们将能够如下定义 Bean 类型`SimpleDateFormat`的定义: + +``` + +``` + +(我们将在本附录后面列出更详细的例子。第一个简单示例的目的是让你了解制作自定义扩展的基本步骤。) + +#### 10.2.1.创建模式 + +创建用于 Spring 的 IOC 容器的 XML 配置扩展,首先要创建一个 XML 模式来描述该扩展。对于我们的示例,我们使用以下模式来配置`SimpleDateFormat`对象: + +``` + + + + + + + + + + + (1) + + + + + + + +``` + +|**1**|所指示的行包含所有可识别标记
的扩展基础(这意味着它们具有`id`属性,我们可以将其用作
容器中的 Bean 标识符)。我们可以使用这个属性,因为我们导入了 Spring 提供的“beans”命名空间。| +|-----|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +前面的模式允许我们使用``元素在 XML 应用程序上下文文件中直接配置`SimpleDateFormat`对象,如下例所示: + +``` + +``` + +请注意,在我们创建了基础结构类之后,前面的 XML 片段与下面的 XML 片段本质上是相同的: + +``` + + + + +``` + +前面两个片段中的第二个片段在容器中创建了一个 Bean(由类型为 `SimpleDateFormat’的名称`dateFormat`标识),并设置了几个属性。 + +| |基于模式的创建配置格式的方法允许与具有模式感知 XML 编辑器的 IDE 紧密集成
。通过使用适当编写的模式,
可以使用自动补全功能,让用户在枚举中定义的几个配置选项
之间进行选择。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 10.2.2.编码 a`NamespaceHandler` + +除了模式之外,我们还需要一个`NamespaceHandler`来解析 Spring 在解析配置文件时遇到的这个特定名称空间的所有元素。对于这个示例,“namespaceHandler”应该负责解析`myns:dateformat`元素。 + +`NamespaceHandler`接口具有三种方法: + +* `init()`:允许初始化`NamespaceHandler`,并在使用处理程序之前由 Spring 调用。 + +* `BeanDefinition parse(Element, ParserContext)`:当 Spring 遇到顶级元素(不嵌套在 Bean 定义或不同的名称空间中)时调用。该方法本身可以注册 Bean 定义,返回 Bean 定义,或者两者兼而有之。 + +* `BeanDefinitionHolder decorate(Node, BeanDefinitionHolder, ParserContext)`:当 Spring 遇到不同名称空间的属性或嵌套元素时调用。 Bean 定义的一个或多个装饰(例如)与[scopes that Spring supports](#beans-factory-scopes)一起使用。我们首先突出显示一个简单的示例,而不使用装饰,然后我们在一个更高级的示例中显示装饰。 + +尽管你可以为整个命名空间编写自己的`NamespaceHandler`代码(因此提供了解析命名空间中每个元素的代码),通常情况下, Spring XML 配置文件中的每个顶级 XML 元素都会导致一个 Bean 定义(就像我们的情况一样,单个``元素会导致一个`SimpleDateFormat` Bean 定义)。 Spring 支持此场景的许多方便类的特征。在下面的示例中,我们使用`NamespaceHandlerSupport`类: + +爪哇 + +``` +package org.springframework.samples.xml; + +import org.springframework.beans.factory.xml.NamespaceHandlerSupport; + +public class MyNamespaceHandler extends NamespaceHandlerSupport { + + public void init() { + registerBeanDefinitionParser("dateformat", new SimpleDateFormatBeanDefinitionParser()); + } +} +``` + +Kotlin + +``` +package org.springframework.samples.xml + +import org.springframework.beans.factory.xml.NamespaceHandlerSupport + +class MyNamespaceHandler : NamespaceHandlerSupport { + + override fun init() { + registerBeanDefinitionParser("dateformat", SimpleDateFormatBeanDefinitionParser()) + } +} +``` + +你可能会注意到,在这个类中实际上并没有大量的解析逻辑。的确,`NamespaceHandlerSupport`类有一个内置的委托概念。它支持注册任意数量的`BeanDefinitionParser`实例,当需要解析名称空间中的元素时,它将委托给这些实例。这种对关注点的清晰分离使`NamespaceHandler`能够处理其名称空间中所有自定义元素的解析的编排,同时将任务委托给`BeanDefinitionParsers`来执行 XML 解析的繁重工作。这意味着每个`BeanDefinitionParser`只包含用于解析单个自定义元素的逻辑,正如我们在下一步中所看到的那样。 + +#### 10.2.3.使用`BeanDefinitionParser` + +如果`BeanDefinitionParser`遇到已映射到特定 Bean 定义解析器(本例中为 `DateFormat’)的类型的 XML 元素,则使用`BeanDefinitionParser`。换句话说,`BeanDefinitionParser`负责解析模式中定义的一个不同的顶级 XML 元素。在解析器中,我们可以访问 XML 元素(因此也可以访问它的子元素),这样我们就可以解析自定义的 XML 内容,正如你在下面的示例中所看到的那样: + +爪哇 + +``` +package org.springframework.samples.xml; + +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; +import org.springframework.util.StringUtils; +import org.w3c.dom.Element; + +import java.text.SimpleDateFormat; + +public class SimpleDateFormatBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { (1) + + protected Class getBeanClass(Element element) { + return SimpleDateFormat.class; (2) + } + + protected void doParse(Element element, BeanDefinitionBuilder bean) { + // this will never be null since the schema explicitly requires that a value be supplied + String pattern = element.getAttribute("pattern"); + bean.addConstructorArgValue(pattern); + + // this however is an optional property + String lenient = element.getAttribute("lenient"); + if (StringUtils.hasText(lenient)) { + bean.addPropertyValue("lenient", Boolean.valueOf(lenient)); + } + } + +} +``` + +|**1**|我们使用 Spring 提供的`AbstractSingleBeanDefinitionParser`来处理大量
创建单个`BeanDefinition`的基本繁重工作。| +|-----|--------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|我们为`AbstractSingleBeanDefinitionParser`超类提供了我们的
单变量`BeanDefinition`所表示的类型。| + +Kotlin + +``` +package org.springframework.samples.xml + +import org.springframework.beans.factory.support.BeanDefinitionBuilder +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser +import org.springframework.util.StringUtils +import org.w3c.dom.Element + +import java.text.SimpleDateFormat + +class SimpleDateFormatBeanDefinitionParser : AbstractSingleBeanDefinitionParser() { (1) + + override fun getBeanClass(element: Element): Class<*>? { (2) + return SimpleDateFormat::class.java + } + + override fun doParse(element: Element, bean: BeanDefinitionBuilder) { + // this will never be null since the schema explicitly requires that a value be supplied + val pattern = element.getAttribute("pattern") + bean.addConstructorArgValue(pattern) + + // this however is an optional property + val lenient = element.getAttribute("lenient") + if (StringUtils.hasText(lenient)) { + bean.addPropertyValue("lenient", java.lang.Boolean.valueOf(lenient)) + } + } +} +``` + +|**1**|我们使用 Spring 提供的`AbstractSingleBeanDefinitionParser`来处理大量
创建单个`BeanDefinition`的基本繁重工作。| +|-----|--------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|我们为`AbstractSingleBeanDefinitionParser`超类提供了我们的
单变量`BeanDefinition`所表示的类型。| + +在这个简单的例子中,这就是我们需要做的所有事情。我们的“BeanDefinition”的创建由`AbstractSingleBeanDefinitionParser`超类处理, Bean 定义的唯一标识符的提取和设置也是如此。 + +#### 10.2.4.注册处理程序和模式 + +编码完成了。剩下要做的就是使 Spring XML 解析基础结构了解我们的自定义元素。我们通过在两个特殊用途的属性文件中注册自定义的“NamespaceHandler”和自定义的 XSD 文件来实现这一目的。这些属性文件都放置在应用程序中的`META-INF`目录中,并且可以与 jar 文件中的二进制类一起分发。 Spring XML 解析基础结构通过使用这些特殊的属性文件自动获取你的新扩展名,其格式将在接下来的两节中详细介绍。 + +##### 写作`META-INF/spring.handlers` + +名为`spring.handlers`的属性文件包含 XML 模式 URI 到名称空间处理程序类的映射。对于我们的示例,我们需要编写以下内容: + +``` +http\://www.mycompany.example/schema/myns=org.springframework.samples.xml.MyNamespaceHandler +``` + +(`:`字符在 爪哇 Properties 格式中是一个有效的分隔符,因此需要使用反斜杠转义 URI 中的 `:’字符。 + +键-值对的第一部分(键)是与自定义名称空间扩展关联的 URI,并且需要完全匹配`targetNamespace`属性的值,如在自定义 XSD 模式中所指定的那样。 + +##### 撰写“meta-inf/ Spring.schemas” + +名为`spring.schemas`的属性文件包含 XML 模式位置(与模式声明一起,在使用模式作为`xsi:schemaLocation`属性的一部分的 XML 文件中)到 Classpath 资源的映射。需要此文件以防止 Spring 绝对必须使用默认的`EntityResolver`,该默认`EntityResolver`需要互联网访问才能检索模式文件。如果在此属性文件中指定了映射, Spring 将在 Classpath 上搜索模式(在本例中,在`org.springframework.samples.xml`包中搜索 `myns.XSD`)。下面的代码片段显示了我们需要为自定义模式添加的行: + +``` +http\://www.mycompany.example/schema/myns/myns.xsd=org/springframework/samples/xml/myns.xsd +``` + +(请记住,`:`字符必须转义。 + +鼓励你在 Classpath 上的`NamespaceHandler`和`BeanDefinitionParser`类旁边部署你的 XSD 文件(或多个文件)。 + +#### 10.2.5.在 Spring XML 配置中使用自定义扩展 + +使用你自己实现的自定义扩展与使用 Spring 提供的“自定义”扩展之一没有什么不同。下面的示例在 Spring XML 配置文件中使用了在前面的步骤中开发的自定义``元素: + +``` + + + + + (1) + + + + + + + + + +``` + +|**1**|我们的习惯 Bean。| +|-----|----------------| + +#### 10.2.6.更详细的例子 + +本节介绍了定制 XML 扩展的一些更详细的示例。 + +##### 在自定义元素中嵌套自定义元素 + +本节中展示的示例展示了如何编写满足以下配置的目标所需的各种工件: + +``` + + + + + + + + + + + + +``` + +前面的配置将自定义扩展嵌套在彼此内部。由``元素实际配置的类是`Component`类(如下一个示例所示)。请注意`Component`类不会公开`components`属性的 setter 方法。这使得使用 setter injection 为`Component`类配置 Bean 定义变得困难(或者说是不可能的)。下面的清单显示了`Component`类: + +爪哇 + +``` +package com.foo; + +import java.util.ArrayList; +import java.util.List; + +public class Component { + + private String name; + private List components = new ArrayList (); + + // mmm, there is no setter method for the 'components' + public void addComponent(Component component) { + this.components.add(component); + } + + public List getComponents() { + return components; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} +``` + +Kotlin + +``` +package com.foo + +import java.util.ArrayList + +class Component { + + var name: String? = null + private val components = ArrayList() + + // mmm, there is no setter method for the 'components' + fun addComponent(component: Component) { + this.components.add(component) + } + + fun getComponents(): List { + return components + } +} +``` + +这个问题的典型解决方案是创建一个自定义的`FactoryBean`,它为`components`属性公开一个 setter 属性。下面的清单显示了这样一个自定义的“FactoryBean”: + +爪哇 + +``` +package com.foo; + +import org.springframework.beans.factory.FactoryBean; + +import java.util.List; + +public class ComponentFactoryBean implements FactoryBean { + + private Component parent; + private List children; + + public void setParent(Component parent) { + this.parent = parent; + } + + public void setChildren(List children) { + this.children = children; + } + + public Component getObject() throws Exception { + if (this.children != null && this.children.size() > 0) { + for (Component child : children) { + this.parent.addComponent(child); + } + } + return this.parent; + } + + public Class getObjectType() { + return Component.class; + } + + public boolean isSingleton() { + return true; + } +} +``` + +Kotlin + +``` +package com.foo + +import org.springframework.beans.factory.FactoryBean +import org.springframework.stereotype.Component + +class ComponentFactoryBean : FactoryBean { + + private var parent: Component? = null + private var children: List? = null + + fun setParent(parent: Component) { + this.parent = parent + } + + fun setChildren(children: List) { + this.children = children + } + + override fun getObject(): Component? { + if (this.children != null && this.children!!.isNotEmpty()) { + for (child in children!!) { + this.parent!!.addComponent(child) + } + } + return this.parent + } + + override fun getObjectType(): Class? { + return Component::class.java + } + + override fun isSingleton(): Boolean { + return true + } +} +``` + +这很好地工作,但它向最终用户暴露了大量的管道系统。我们要做的是编写一个自定义扩展,以隐藏所有这些管道。如果我们坚持使用[前面描述的步骤](#xsd-custom-introduction),那么我们首先创建 XSD 模式来定义自定义标记的结构,如下面的清单所示: + +``` + + + + + + + + + + + + + + + +``` + +同样在[前面描述的过程](#xsd-custom-introduction)之后,我们将创建一个自定义`NamespaceHandler`: + +爪哇 + +``` +package com.foo; + +import org.springframework.beans.factory.xml.NamespaceHandlerSupport; + +public class ComponentNamespaceHandler extends NamespaceHandlerSupport { + + public void init() { + registerBeanDefinitionParser("component", new ComponentBeanDefinitionParser()); + } +} +``` + +Kotlin + +``` +package com.foo + +import org.springframework.beans.factory.xml.NamespaceHandlerSupport + +class ComponentNamespaceHandler : NamespaceHandlerSupport() { + + override fun init() { + registerBeanDefinitionParser("component", ComponentBeanDefinitionParser()) + } +} +``` + +接下来是自定义`BeanDefinitionParser`。请记住,我们正在创建一个`BeanDefinition`来描述`ComponentFactoryBean`。下面的清单显示了我们的自定义`BeanDefinitionParser`实现: + +爪哇 + +``` +package com.foo; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.util.xml.DomUtils; +import org.w3c.dom.Element; + +import java.util.List; + +public class ComponentBeanDefinitionParser extends AbstractBeanDefinitionParser { + + protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) { + return parseComponentElement(element); + } + + private static AbstractBeanDefinition parseComponentElement(Element element) { + BeanDefinitionBuilder factory = BeanDefinitionBuilder.rootBeanDefinition(ComponentFactoryBean.class); + factory.addPropertyValue("parent", parseComponent(element)); + + List childElements = DomUtils.getChildElementsByTagName(element, "component"); + if (childElements != null && childElements.size() > 0) { + parseChildComponents(childElements, factory); + } + + return factory.getBeanDefinition(); + } + + private static BeanDefinition parseComponent(Element element) { + BeanDefinitionBuilder component = BeanDefinitionBuilder.rootBeanDefinition(Component.class); + component.addPropertyValue("name", element.getAttribute("name")); + return component.getBeanDefinition(); + } + + private static void parseChildComponents(List childElements, BeanDefinitionBuilder factory) { + ManagedList children = new ManagedList(childElements.size()); + for (Element element : childElements) { + children.add(parseComponentElement(element)); + } + factory.addPropertyValue("children", children); + } +} +``` + +Kotlin + +``` +package com.foo + +import org.springframework.beans.factory.config.BeanDefinition +import org.springframework.beans.factory.support.AbstractBeanDefinition +import org.springframework.beans.factory.support.BeanDefinitionBuilder +import org.springframework.beans.factory.support.ManagedList +import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser +import org.springframework.beans.factory.xml.ParserContext +import org.springframework.util.xml.DomUtils +import org.w3c.dom.Element + +import java.util.List + +class ComponentBeanDefinitionParser : AbstractBeanDefinitionParser() { + + override fun parseInternal(element: Element, parserContext: ParserContext): AbstractBeanDefinition? { + return parseComponentElement(element) + } + + private fun parseComponentElement(element: Element): AbstractBeanDefinition { + val factory = BeanDefinitionBuilder.rootBeanDefinition(ComponentFactoryBean::class.java) + factory.addPropertyValue("parent", parseComponent(element)) + + val childElements = DomUtils.getChildElementsByTagName(element, "component") + if (childElements != null && childElements.size > 0) { + parseChildComponents(childElements, factory) + } + + return factory.getBeanDefinition() + } + + private fun parseComponent(element: Element): BeanDefinition { + val component = BeanDefinitionBuilder.rootBeanDefinition(Component::class.java) + component.addPropertyValue("name", element.getAttribute("name")) + return component.beanDefinition + } + + private fun parseChildComponents(childElements: List, factory: BeanDefinitionBuilder) { + val children = ManagedList(childElements.size) + for (element in childElements) { + children.add(parseComponentElement(element)) + } + factory.addPropertyValue("children", children) + } +} +``` + +最后,需要通过修改`META-INF/spring.handlers`和`META-INF/spring.schemas`文件,将各种工件注册到 Spring XML 基础结构中,如下所示: + +``` +# in 'META-INF/spring.handlers' +http\://www.foo.example/schema/component=com.foo.ComponentNamespaceHandler +``` + +``` +# in 'META-INF/spring.schemas' +http\://www.foo.example/schema/component/component.xsd=com/foo/component.xsd +``` + +##### “正常”元素上的自定义属性 + +编写自己的自定义解析器和相关的工件并不难。然而,这有时并不是正确的做法。考虑这样一个场景,你需要将元数据添加到已经存在的 Bean 定义中。在这种情况下,你当然不希望不得不编写自己的整个自定义扩展。相反,你只是希望向现有的 Bean 定义元素添加一个附加属性。 + +通过另一个示例,假设你为访问集群[JCache](https://jcp.org/en/jsr/detail?id=107)的服务对象定义了 Bean 定义,并且你希望确保命名的 JCache 实例在周围的集群中急切地启动。下面的清单显示了这样的定义: + +``` + + + +``` + +然后,我们可以在解析“jcache:cache-name”属性时创建另一个`BeanDefinition`。这`BeanDefinition`然后为我们初始化命名的 JCache。我们还可以修改“checkingAccountService”` 的现有,使其对这个新的 JCache 具有依赖性-初始化。下面的清单显示了我们的`JCacheInitializer`: + +爪哇 + +``` +package com.foo; + +public class JCacheInitializer { + + private String name; + + public JCacheInitializer(String name) { + this.name = name; + } + + public void initialize() { + // lots of JCache API calls to initialize the named cache... + } +} +``` + +Kotlin + +``` +package com.foo + +class JCacheInitializer(private val name: String) { + + fun initialize() { + // lots of JCache API calls to initialize the named cache... + } +} +``` + +现在,我们可以进入自定义扩展。首先,我们需要编写描述自定义属性的 XSD 模式,如下所示: + +``` + + + + + + + +``` + +接下来,我们需要创建关联的`NamespaceHandler`,如下所示: + +Java + +``` +package com.foo; + +import org.springframework.beans.factory.xml.NamespaceHandlerSupport; + +public class JCacheNamespaceHandler extends NamespaceHandlerSupport { + + public void init() { + super.registerBeanDefinitionDecoratorForAttribute("cache-name", + new JCacheInitializingBeanDefinitionDecorator()); + } + +} +``` + +Kotlin + +``` +package com.foo + +import org.springframework.beans.factory.xml.NamespaceHandlerSupport + +class JCacheNamespaceHandler : NamespaceHandlerSupport() { + + override fun init() { + super.registerBeanDefinitionDecoratorForAttribute("cache-name", + JCacheInitializingBeanDefinitionDecorator()) + } + +} +``` + +接下来,我们需要创建解析器。请注意,在本例中,因为我们要解析一个 XML 属性,所以我们写一个`BeanDefinitionDecorator`,而不是`BeanDefinitionParser`。下面的清单显示了我们的`BeanDefinitionDecorator`实现: + +Java + +``` +package com.foo; + +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.BeanDefinitionDecorator; +import org.springframework.beans.factory.xml.ParserContext; +import org.w3c.dom.Attr; +import org.w3c.dom.Node; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class JCacheInitializingBeanDefinitionDecorator implements BeanDefinitionDecorator { + + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + + public BeanDefinitionHolder decorate(Node source, BeanDefinitionHolder holder, + ParserContext ctx) { + String initializerBeanName = registerJCacheInitializer(source, ctx); + createDependencyOnJCacheInitializer(holder, initializerBeanName); + return holder; + } + + private void createDependencyOnJCacheInitializer(BeanDefinitionHolder holder, + String initializerBeanName) { + AbstractBeanDefinition definition = ((AbstractBeanDefinition) holder.getBeanDefinition()); + String[] dependsOn = definition.getDependsOn(); + if (dependsOn == null) { + dependsOn = new String[]{initializerBeanName}; + } else { + List dependencies = new ArrayList(Arrays.asList(dependsOn)); + dependencies.add(initializerBeanName); + dependsOn = (String[]) dependencies.toArray(EMPTY_STRING_ARRAY); + } + definition.setDependsOn(dependsOn); + } + + private String registerJCacheInitializer(Node source, ParserContext ctx) { + String cacheName = ((Attr) source).getValue(); + String beanName = cacheName + "-initializer"; + if (!ctx.getRegistry().containsBeanDefinition(beanName)) { + BeanDefinitionBuilder initializer = BeanDefinitionBuilder.rootBeanDefinition(JCacheInitializer.class); + initializer.addConstructorArg(cacheName); + ctx.getRegistry().registerBeanDefinition(beanName, initializer.getBeanDefinition()); + } + return beanName; + } +} +``` + +Kotlin + +``` +package com.foo + +import org.springframework.beans.factory.config.BeanDefinitionHolder +import org.springframework.beans.factory.support.AbstractBeanDefinition +import org.springframework.beans.factory.support.BeanDefinitionBuilder +import org.springframework.beans.factory.xml.BeanDefinitionDecorator +import org.springframework.beans.factory.xml.ParserContext +import org.w3c.dom.Attr +import org.w3c.dom.Node + +import java.util.ArrayList + +class JCacheInitializingBeanDefinitionDecorator : BeanDefinitionDecorator { + + override fun decorate(source: Node, holder: BeanDefinitionHolder, + ctx: ParserContext): BeanDefinitionHolder { + val initializerBeanName = registerJCacheInitializer(source, ctx) + createDependencyOnJCacheInitializer(holder, initializerBeanName) + return holder + } + + private fun createDependencyOnJCacheInitializer(holder: BeanDefinitionHolder, + initializerBeanName: String) { + val definition = holder.beanDefinition as AbstractBeanDefinition + var dependsOn = definition.dependsOn + dependsOn = if (dependsOn == null) { + arrayOf(initializerBeanName) + } else { + val dependencies = ArrayList(listOf(*dependsOn)) + dependencies.add(initializerBeanName) + dependencies.toTypedArray() + } + definition.setDependsOn(*dependsOn) + } + + private fun registerJCacheInitializer(source: Node, ctx: ParserContext): String { + val cacheName = (source as Attr).value + val beanName = "$cacheName-initializer" + if (!ctx.registry.containsBeanDefinition(beanName)) { + val initializer = BeanDefinitionBuilder.rootBeanDefinition(JCacheInitializer::class.java) + initializer.addConstructorArg(cacheName) + ctx.registry.registerBeanDefinition(beanName, initializer.getBeanDefinition()) + } + return beanName + } +} +``` + +最后,我们需要通过修改`META-INF/spring.handlers`和`META-INF/spring.schemas`文件,在 Spring XML 基础结构中注册各种工件,如下所示: + +``` +# in 'META-INF/spring.handlers' +http\://www.foo.example/schema/jcache=com.foo.JCacheNamespaceHandler +``` + +``` +# in 'META-INF/spring.schemas' +http\://www.foo.example/schema/jcache/jcache.xsd=com/foo/jcache.xsd +``` + +### 10.3.应用程序启动步骤 + +附录的这一部分列出了核心容器所使用的现有`StartupSteps`。 + +| |关于每个启动步骤的名称和详细信息不是公共契约的一部分,并且
可能会发生更改;这被认为是核心容器的实现细节,并且将随着
其行为的改变。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| Name |说明| Tags | +|----------------------------------------------|----------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------| +| `spring.beans.instantiate` |Bean 及其依赖关系的实例化。|`beanName` the name of the bean, `beanType` the type required at the injection point.| +| `spring.beans.smart-initialize` |初始化`SmartInitializingSingleton`bean。| `beanName` the name of the bean. | +|`spring.context.annotated-bean-reader.create` |创建`AnnotatedBeanDefinitionReader`。| | +| `spring.context.base-packages.scan` |扫描基本包装。| `packages` array of base packages for scanning. | +| `spring.context.beans.post-process` |beans 后处理阶段。| | +| `spring.context.bean-factory.post-process` |调用`BeanFactoryPostProcessor`bean。| `postProcessor` the current post-processor. | +|`spring.context.beandef-registry.post-process`|调用`BeanDefinitionRegistryPostProcessor`bean。| `postProcessor` the current post-processor. | +| `spring.context.component-classes.register` |通过`AnnotationConfigApplicationContext#register`注册组件类。| `classes` array of given classes for registration. | +| `spring.context.config-classes.enhance` |使用 CGlib 代理增强配置类。| `classCount` count of enhanced classes. | +| `spring.context.config-classes.parse` |配置类解析阶段使用`ConfigurationClassPostProcessor`。| `classCount` count of processed classes. | +| `spring.context.refresh` |应用程序上下文刷新阶段。| | diff --git a/docs/spring-framework/data-access.md b/docs/spring-framework/data-access.md new file mode 100644 index 0000000000000000000000000000000000000000..2d0e43c824720ff608c9ff7aac1b31847f37cee8 --- /dev/null +++ b/docs/spring-framework/data-access.md @@ -0,0 +1,6705 @@ +# 数据访问 + +引用文档的这一部分涉及数据访问以及数据访问层和业务或服务层之间的交互。 + +Spring 的全面事务管理支持进行了一些详细的介绍,随后是对 Spring 框架与之集成的各种数据访问框架和技术的全面介绍。 + +## 1. 事务管理 + +全面的事务支持是使用 Spring 框架的最有说服力的理由之一。 Spring 框架为事务管理提供了一个一致的抽象,它提供了以下好处: + +* 跨不同事务 API 的一致的编程模型,例如 爪哇 事务 API、JDBC、 Hibernate 和 爪哇 持久性 API( JPA)。 + +* 支持[声明式事务管理](#transaction-declarative)。 + +* 用于[programmatic](#transaction-programmatic)事务管理的比复杂事务 API(如 JTA)更简单的 API。 + +* 与 Spring 的数据访问抽象进行了出色的集成。 + +以下章节描述了 Spring 框架的事务特性和技术: + +* [Advantages of the Spring Framework’s transaction support model](#transaction-motivation)描述了为什么要使用 Spring 框架的事务抽象,而不是使用 EJB 容器管理事务(CMT),或者选择通过专有 API(如 Hibernate)来驱动本地事务。 + +* [Understanding the Spring Framework transaction abstraction](#transaction-strategies)概述了核心类,并描述了如何从各种源配置和获取`DataSource`实例。 + +* [将资源与事务同步](#tx-resource-synchronization)描述了应用程序代码如何确保正确地创建、重用和清理资源。 + +* [声明式事务管理](#transaction-declarative)描述了对声明式事务管理的支持。 + +* [程序化事务管理](#transaction-programmatic)涵盖了对程序化(即显式编码)事务管理的支持。 + +* [事务绑定事件](#transaction-event)描述如何在事务中使用应用程序事件。 + +本章还讨论了最佳实践,[应用服务器集成](#transaction-application-server-integration)和[常见问题的解决方案](#transaction-solutions-to-common-problems)。 + +### 1.1. Spring 框架的事务支持模型的优点 + +传统上,爪哇 EE 开发人员在事务管理方面有两种选择:全局事务或本地事务,这两种选择都有很大的局限性。在接下来的两节中,将介绍全局和本地事务管理,然后讨论 Spring 框架的事务管理支持如何解决全局和本地事务模型的局限性。 + +#### 1.1.1.全球交易 + +全局事务允许你使用多个事务资源,通常是关系数据库和消息队列。应用程序服务器通过 JTA 管理全局事务,JTA 是一个繁琐的 API(部分原因是其异常模型)。此外,JTA`UserTransaction`通常需要来自 JNDI,这意味着你还需要使用 JNDI 才能使用 JTA。全局事务的使用限制了应用程序代码的任何潜在重用,因为 JTA 通常仅在应用程序服务器环境中可用。 + +以前,使用全局事务的首选方式是通过 EJB CMT(容器管理事务)。CMT 是声明式事务管理的一种形式(区别于程序化事务管理)。EJB CMT 消除了对与事务相关的 JNDI 查找的需求,尽管 EJB 本身的使用需要使用 JNDI。它消除了编写 爪哇 代码来控制事务的大部分(但不是全部)需求。最大的缺点是,CMT 与 JTA 和应用程序服务器环境绑定在一起。而且,只有当你选择在 EJB 中实现业务逻辑(或者至少在事务性 EJB facade 的后面)时,它才可用。总的来说,EJB 的负面影响是如此之大,以至于这不是一个有吸引力的提议,尤其是在面对声明式事务管理的令人信服的替代方案时。 + +#### 1.1.2.本地交易 + +本地事务是特定于资源的,例如与 JDBC 连接关联的事务。本地事务可能更容易使用,但有一个明显的缺点:它们不能跨多个事务资源工作。例如,通过使用 JDBC 连接来管理事务的代码不能在全局 JTA 事务中运行。由于应用程序服务器不参与事务管理,因此它无法帮助确保跨多个资源的正确性。(值得注意的是,大多数应用程序使用单个事务资源。)另一个缺点是,本地事务对编程模型具有侵入性。 + +#### 1.1.3. Spring 框架的一致性编程模型 + +Spring 解决了全局和本地事务的缺点。它允许应用程序开发人员在任何环境中使用一致的编程模型。你只需编写一次代码,就可以从不同环境中的不同事务管理策略中受益。 Spring 框架提供了声明式和程序化的事务管理。大多数用户更喜欢声明式事务管理,我们在大多数情况下都推荐这种方法。 + +对于程序化事务管理,开发人员使用 Spring 框架事务抽象,它可以运行在任何底层事务基础设施上。对于优选的声明式模型,开发人员通常很少或根本不编写与事务管理相关的代码,因此,不依赖于 Spring Framework Transaction API 或任何其他事务 API。 + +你需要用于事务管理的应用程序服务器吗? + +Spring 框架的事务管理支持改变了 Enterprise爪哇 应用程序何时需要应用服务器的传统规则。 + +特别是,你不需要通过 EJB 进行完全用于声明式事务的应用程序服务器。实际上,即使你的应用程序服务器具有强大的 JTA 功能,你也可能认为 Spring 框架的声明式事务提供了比 EJB CMT 更强大的功能和更高效的编程模型。 + +通常,只有当你的应用程序需要跨多个资源处理事务时,你才需要应用程序服务器的 JTA 功能,而这对许多应用程序来说并不是必需的。许多高端应用程序使用单一的、高度可扩展的数据库(如 Oracle RAC)。独立事务管理器(例如[Atomikos 交易](https://www.atomikos.com/)和[JOTM](http://jotm.objectweb.org/))是其他选项。当然,你可能需要其他应用程序服务器功能,例如 爪哇 消息服务和 爪哇 EE 连接器架构。 + +Spring 框架为你提供了何时将应用程序扩展到全负载应用程序服务器的选择。使用 EJB CMT 或 JTA 的唯一替代方法是使用本地事务(例如 JDBC 连接上的事务)编写代码,如果你需要在全局的、容器管理的事务中运行这些代码,那么你将面临大量的返工,这种情况已经一去不复返了。在 Spring 框架中,只有配置文件中的 Bean 定义中的一些需要更改(而不是代码)。 + +### 1.2.理解 Spring 框架事务抽象 + +Spring 事务抽象的关键是事务策略的概念。事务策略由`TransactionManager`定义,特别是用于强制事务管理的 `org.springframework.transactionmanager’接口和用于反应式事务管理的 `org.springframework.transactionmanager’接口。下面的列表显示了“Platform TransactionManager”API 的定义: + +``` +public interface PlatformTransactionManager extends TransactionManager { + + TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException; + + void commit(TransactionStatus status) throws TransactionException; + + void rollback(TransactionStatus status) throws TransactionException; +} +``` + +这主要是一个服务提供程序接口,尽管你可以从应用程序代码中使用它[程式设计](#transaction-programmatic-ptm)。由于“Platform TransactionManager”是一个界面,因此可以在必要时轻松地对其进行模拟或删除。它不绑定到查找策略,例如 JNDI。“Platform TransactionManager”实现的定义与 Spring Framework IoC 容器中的任何其他对象(或 Bean)一样。仅这一点就使 Spring 框架事务成为一个有价值的抽象,即使在使用 JTA 的情况下也是如此。与直接使用 JTA 相比,你可以更容易地测试事务性代码。 + +同样,与 Spring 的哲学一致,可以由`PlatformTransactionManager`接口的任何方法抛出的`TransactionException`不受检查(也就是说,它扩展了`java.lang.RuntimeException`类)。事务基础设施故障几乎总是致命的。在应用程序代码实际上可以从事务失败中恢复的极少数情况下,应用程序开发人员仍然可以选择捕获和处理`TransactionException`。最重要的一点是,开发商并不是被迫这么做的。 + +`getTransaction(..)`方法返回一个`TransactionStatus`对象,这取决于一个 `transactiondefinition’参数。如果当前调用堆栈中存在匹配的事务,则返回的`TransactionStatus`可能表示一个新事务,或者可以表示一个现有事务。后一种情况的含义是,与 爪哇 EE 事务上下文一样,`TransactionStatus`与执行线程相关联。 + +在 Spring 框架 5.2 中, Spring 还为使用反应性类型或 Kotlin 协程的反应性应用程序提供了事务管理抽象。下面的列表显示了由 `org.springframework.transaction.reactiveTransactionManager’定义的事务策略: + +``` +public interface ReactiveTransactionManager extends TransactionManager { + + Mono getReactiveTransaction(TransactionDefinition definition) throws TransactionException; + + Mono commit(ReactiveTransaction status) throws TransactionException; + + Mono rollback(ReactiveTransaction status) throws TransactionException; +} +``` + +反应式事务管理器主要是一个服务提供程序接口,尽管你可以从应用程序代码中使用它[程式设计](#transaction-programmatic-rtm)。因为`ReactiveTransactionManager`是一个接口,所以可以根据需要轻松地对其进行模拟或截断。 + +`TransactionDefinition`接口指定: + +* 传播:通常,事务范围内的所有代码都在该事务中运行。但是,如果事务方法在事务上下文已经存在的情况下运行,则可以指定该行为。例如,代码可以在现有的事务中继续运行(常见的情况),或者可以暂停现有的事务并创建新的事务。 Spring 提供了从 EJB CMT 中熟悉的所有事务传播选项。要了解 Spring 中事务传播的语义,请参见[事务传播](#tx-propagation)。 + +* 隔离:此事务与其他事务的工作隔离的程度。例如,这个事务可以看到来自其他事务的未提交的写吗? + +* 超时:此事务在超时和被底层事务基础设施自动回滚之前运行了多长时间。 + +* 只读状态:当你的代码读取但不修改数据时,你可以使用只读事务。只读事务在某些情况下可以是一种有用的优化,例如当你使用 Hibernate 时。 + +这些设置反映了标准的事务概念。如果有必要,请参考讨论事务隔离级别和其他核心事务概念的资源。理解这些概念对于使用 Spring 框架或任何事务管理解决方案是至关重要的。 + +`TransactionStatus`接口为事务代码控制事务执行和查询事务状态提供了一种简单的方法。这些概念应该是熟悉的,因为它们是所有事务 API 所共有的。下面的列表显示了“TransactionStatus”接口: + +``` +public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable { + + @Override + boolean isNewTransaction(); + + boolean hasSavepoint(); + + @Override + void setRollbackOnly(); + + @Override + boolean isRollbackOnly(); + + void flush(); + + @Override + boolean isCompleted(); +} +``` + +无论你是否 OPT 用于 Spring 中的声明式或程序化事务管理,定义正确的`TransactionManager`实现是绝对必要的。你通常通过依赖注入来定义这个实现。 + +`TransactionManager`实现通常需要它们工作的环境的知识:JDBC、JTA、 Hibernate 等等。下面的示例展示了如何定义本地`PlatformTransactionManager`实现(在本例中,使用普通的 JDBC)。 + +你可以通过创建类似于以下的 Bean 来定义 JDBC`DataSource`: + +``` + + + + + + +``` + +然后,相关的`PlatformTransactionManager` Bean 定义引用了 ` 数据源’定义。它应该类似于以下示例: + +``` + + + +``` + +如果在 爪哇 EE 容器中使用 JTA,则使用通过 JNDI 获得的容器`DataSource`,并结合 Spring 的`JtaTransactionManager`。下面的示例显示了 JTA 和 JNDI 查找版本的外观: + +``` + + + + + + + + + + +``` + +`JtaTransactionManager`不需要了解`DataSource`(或任何其他特定资源),因为它使用容器的全局事务管理基础架构。 + +| |前面的`dataSource` Bean 定义使用了``名称空间中的
标记
。有关更多信息,请参见[The JEE Schema](integration.html#xsd-schemas-jee)。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |如果你使用 JTA,那么你的事务管理器定义看起来应该是相同的,无论你使用的是什么数据访问技术,无论是 JDBC、 Hibernate JPA,还是任何其他受支持的技术。这是因为 JTA 事务是全局事务,
可以获取任何事务资源。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在所有 Spring 事务设置中,应用程序代码不需要更改。你可以仅通过更改配置来更改事务的管理方式,即使这种更改意味着从本地事务转移到全局事务,或者反之亦然。 + +#### 1.2.1. Hibernate 交易设置 + +还可以轻松地使用 Hibernate 本地事务,如以下示例所示。在这种情况下,你需要定义一个 Hibernate `LocalSessionFactoryBean`,你的应用程序代码可以使用它来获得 Hibernate `Session`实例。 + +`DataSource` Bean 的定义类似于前面显示的本地 JDBC 示例,因此在下面的示例中没有显示。 + +| |如果`DataSource`(由任何非 JTA 事务管理器使用)通过
JNDI 查找并由 爪哇 EE 容器管理,则它应该是非事务性的,因为
Spring 框架(而不是 爪哇 EE 容器)管理事务。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在这种情况下,`txManager` Bean 是`HibernateTransactionManager`类型的。就像`DataSourceTransactionManager`需要引用`DataSource`一样,“HibernateTransactionManager”需要引用`SessionFactory`。下面的示例声明`sessionFactory`和`txManager`bean: + +``` + + + + + org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml + + + + + hibernate.dialect=${hibernate.dialect} + + + + + + + +``` + +如果使用 Hibernate 和 爪哇 EE 容器管理的 JTA 事务,则应该使用与上一个 JTA 示例中的 JDBC 相同的`JtaTransactionManager`,如下例所示。另外,建议 Hibernate 通过 JTA 的事务协调器以及可能的连接释放模式配置来了解 JTA: + +``` + + + + + org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml + + + + + hibernate.dialect=${hibernate.dialect} + hibernate.transaction.coordinator_class=jta + hibernate.connection.handling_mode=DELAYED_ACQUISITION_AND_RELEASE_AFTER_STATEMENT + + + + + +``` + +或者,你可以将`JtaTransactionManager`传递到`LocalSessionFactoryBean`中,以执行相同的默认值: + +``` + + + + + org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml + + + + + hibernate.dialect=${hibernate.dialect} + + + + + + +``` + +### 1.3.将资源与事务同步 + +如何创建不同的事务管理器,以及它们如何链接到需要同步到事务的相关资源(例如`DataSourceTransactionManager`到一个 JDBC`DataSource`,`HibernateTransactionManager`到一个 Hibernate `SessionFactory`,等等),现在应该已经很清楚了。本节描述了应用程序代码如何(通过使用持久性 API(如 JDBC、 Hibernate 或 JPA)直接或间接地)确保正确地创建、重用和清理这些资源。本节还讨论了如何(可选地)通过相关的`TransactionManager`触发事务同步。 + +#### 1.3.1.高层同步方法 + +首选的方法是使用 Spring 最高级别的基于模板的持久性集成 API,或者使用带有事务感知工厂 bean 或代理的本机 ORM API 来管理本机资源工厂。这些事务感知解决方案在内部处理资源的创建和重用、清理、资源的可选事务同步以及异常映射。因此,用户数据访问代码不必处理这些任务,而可以完全关注于非样板持久性逻辑。通常,你使用本机 ORM API,或者通过使用`JdbcTemplate`对 JDBC 访问采用模板方法。这些解决方案将在本参考文档的后续部分中详细介绍。 + +#### 1.3.2.低水平同步方法 + +诸如`DataSourceUtils`(对于 JDBC)、`EntityManagerFactoryUtils`(对于 JPA)、`sessionFactoryutils’(对于 Hibernate)等类存在于较低的级别。当你希望应用程序代码直接处理本机持久性 API 的资源类型时,可以使用这些类来确保获得适当的 Spring 框架管理的实例,(可选地)同步事务,以及将过程中发生的异常正确地映射到一致的 API。 + +例如,在 JDBC 的情况下,你可以使用 Spring 的 `org.springframework.jdbc.datasource.datasourceutils’类,而不是在`DataSource`上调用`getConnection()`方法的传统 JDBC 方法,如下所示: + +``` +Connection conn = DataSourceUtils.getConnection(dataSource); +``` + +如果现有事务已经有一个与其同步(链接)的连接,则返回该实例。否则,方法调用将触发新连接的创建,该连接(可选地)与任何现有事务同步,并可用于该相同事务的后续重用。如前所述,任何“sqlexception”都包装在 Spring 框架`CannotGetJdbcConnectionException`中,这是 Spring 框架的未检查`DataAccessException`类型的层次结构之一。这种方法为你提供了比从`SQLException`更容易获得的信息,并确保了跨数据库甚至跨不同持久性技术的可移植性。 + +该方法也可以在不使用 Spring 事务管理的情况下工作(事务同步是可选的),因此无论是否使用 Spring 用于事务管理,都可以使用它。 + +当然,一旦使用了 Spring 的 JDBC 支持、 JPA 支持或 Hibernate 支持,通常不会使用`DataSourceUtils`或其他助手类,因为与直接使用相关的 API 相比,通过 Spring 抽象进行工作要快乐得多。例如,如果你使用 Spring `JdbcTemplate`或 `jdbc.object’包来简化对 JDBC 的使用,那么正确的连接检索就会在幕后发生,并且你不需要编写任何特殊的代码。 + +#### 1.3.3.`TransactionAwareDataSourceProxy` + +在最底层存在`TransactionAwareDataSourceProxy`类。这是目标`DataSource`的代理,该代理封装了目标`DataSource`,以添加对 Spring 管理的事务的感知。在这方面,它类似于由 爪哇 EE 服务器提供的事务性 JNDI“数据源”。 + +你几乎不需要或不想使用这个类,除非必须调用现有代码并传递标准的 JDBC`DataSource`接口实现。在这种情况下,该代码可能是可用的,但它参与了 Spring 管理的事务。你可以使用前面提到的更高级别的抽象来编写新代码。 + +### 1.4.声明式事务管理 + +| |Spring 大多数框架用户选择声明式事务管理。此选项对应用程序代码的影响最小,因此最符合
非侵入式轻量级容器的理想。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +Spring 框架的声明性事务管理通过 Spring 面向方面的编程( AOP)而成为可能。然而,由于事务性方面的代码来自 Spring 框架分布并且可以以样板方式使用, AOP 概念通常不必被理解以有效地使用该代码。 + +Spring 框架的声明式事务管理类似于 EJB CMT,因为你可以指定事务行为(或缺少事务行为),直至单个方法级别。如果有必要,可以在事务上下文中进行`setRollbackOnly()`调用。这两种类型的事务管理的区别在于: + +* 与绑定到 JTA 的 EJB CMT 不同, Spring 框架的声明式事务管理可以在任何环境中工作。它可以通过使用 JDBC、 JPA 或 Hibernate 调整配置文件来处理 JTA 事务或本地事务。 + +* 你可以将 Spring 框架声明式事务管理应用于任何类,而不仅仅是 EJB 等特殊类。 + +* Spring 框架提供了声明性[rollback rules](#transaction-declarative-rolling-back),这是一个没有 EJB 等价物的特性。提供了对回滚规则的编程支持和声明式支持。 + +* Spring 框架允许你通过使用 AOP 自定义事务行为。例如,你可以在事务回滚的情况下插入自定义行为。你还可以添加任意的建议以及事务性建议。使用 EJB CMT,你不能影响容器的事务管理,除非使用“setRollBackOnly()”。 + +* Spring 框架不支持跨远程调用传播事务上下文,就像高端应用服务器所做的那样。如果你需要此功能,我们建议你使用 EJB。然而,在使用这样的特性之前要仔细考虑,因为通常情况下,人们不希望事务跨越远程调用。 + +回滚规则的概念很重要。它们允许你指定哪些异常(和 throwable)应该导致自动回滚。你可以在配置中,而不是在 爪哇 代码中,以声明式的方式指定此项。因此,尽管你仍然可以在`TransactionStatus`对象上调用`setRollbackOnly()`来回滚当前事务,但大多数情况下,你可以指定一条规则,即`MyApplicationException`必须始终导致回滚。此选项的显著优点是业务对象不依赖于事务基础设施。例如,它们通常不需要导入 Spring 事务 API 或其他 Spring API。 + +虽然 EJB 容器的默认行为会在系统异常(通常是运行时异常)上自动回滚事务,但 EJB CMT 不会在应用程序异常(即除`java.rmi.RemoteException`以外的检查异常)上自动回滚事务。虽然 Spring 声明性事务管理的默认行为遵循 EJB 约定(回滚仅在未检查的异常情况下是自动的),但定制这种行为通常是有用的。 + +#### 1.4.1.理解 Spring 框架的声明式事务实现 + +仅仅告诉你使用“@Transactional”注释对类进行注释,在配置中添加`@EnableTransactionManagement`,并期望你了解它是如何工作的,这是不够的。为了提供更深入的理解,本节将在与事务相关的问题的上下文中解释 Spring 框架的声明式事务基础结构的内部工作方式。 + +关于 Spring 框架的声明性事务支持,最重要的概念是启用了[via AOP proxies](core.html#aop-understanding-aop-proxies),并且事务建议由元数据(目前是基于 XML 或注释)驱动。 AOP 与事务性元数据的组合产生了 AOP 代理,该代理使用`TransactionInterceptor`并结合适当的`TransactionManager`实现来驱动围绕方法调用的事务。 + +| |Spring AOP 在[the AOP section](core.html#aop)中涵盖。| +|---|----------------------------------------------------------| + +Spring Framework 的`TransactionInterceptor`为命令式和反应式编程模型提供事务管理。拦截器通过检查方法返回类型来检测所需的事务管理风格。返回诸如`Publisher`或 Kotlin `Flow`之类的反应性类型的方法(或其中的一个子类型)符合进行反应性事务管理的条件。包括`void`在内的所有其他返回类型都使用强制事务管理的代码路径。 + +事务管理风格会影响所需的事务管理器。命令式事务需要`PlatformTransactionManager`,而反应式事务使用 `reactiveTransactionManager’实现。 + +| |`@Transactional`通常与由 `Platform TransactionManager’管理的线程绑定事务一起工作,将事务暴露于
当前执行线程内的所有数据访问操作。注意:这将*不是*传播到方法内新启动的线程


由`ReactiveTransactionManager`管理的反应式事务使用反应器上下文
而不是线程本地属性。因此,所有参与的数据访问
操作都需要在相同的反应性管道中的相同的反应器上下文中执行。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +下图显示了在事务代理上调用方法的概念视图: + +![tx](images/tx.png) + +#### 1.4.2.声明式事务实现示例 + +考虑下面的接口及其附带的实现。这个示例使用 `foo’和`Bar`类作为占位符,这样你就可以专注于事务使用,而不必关注特定的域模型。对于本例的目的,`DefaultFooService`类在每个实现的方法的主体中抛出`UnsupportedOperationException`实例的事实是好的。这种行为允许你看到正在创建的事务,然后响应“unsupportedOperationException”实例进行回滚。下面的清单显示了`FooService`接口: + +爪哇 + +``` +// the service interface that we want to make transactional + +package x.y.service; + +public interface FooService { + + Foo getFoo(String fooName); + + Foo getFoo(String fooName, String barName); + + void insertFoo(Foo foo); + + void updateFoo(Foo foo); + +} +``` + +Kotlin + +``` +// the service interface that we want to make transactional + +package x.y.service + +interface FooService { + + fun getFoo(fooName: String): Foo + + fun getFoo(fooName: String, barName: String): Foo + + fun insertFoo(foo: Foo) + + fun updateFoo(foo: Foo) +} +``` + +下面的示例展示了前面接口的一个实现: + +爪哇 + +``` +package x.y.service; + +public class DefaultFooService implements FooService { + + @Override + public Foo getFoo(String fooName) { + // ... + } + + @Override + public Foo getFoo(String fooName, String barName) { + // ... + } + + @Override + public void insertFoo(Foo foo) { + // ... + } + + @Override + public void updateFoo(Foo foo) { + // ... + } +} +``` + +Kotlin + +``` +package x.y.service + +class DefaultFooService : FooService { + + override fun getFoo(fooName: String): Foo { + // ... + } + + override fun getFoo(fooName: String, barName: String): Foo { + // ... + } + + override fun insertFoo(foo: Foo) { + // ... + } + + override fun updateFoo(foo: Foo) { + // ... + } +} +``` + +假设`FooService`接口的前两个方法`getFoo(String)`和 `getfoo(字符串,字符串)’必须在具有只读语义的事务的上下文中运行,而其他方法`insertFoo(Foo)`和`updateFoo(Foo)`必须在具有读写语义的事务的上下文中运行。下面的几段将详细介绍以下配置: + +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +检查前面的配置。它假定你希望创建一个事务性的服务对象`fooService` Bean。要应用的事务语义封装在``定义中。``定义为“所有以`get`开头的方法都将在只读事务的上下文中运行,所有其他方法都将使用默认事务语义运行”。将``标记的 `transactionmanager’属性设置为将驱动事务的 `transactionmanager’ Bean 的名称(在本例中,为 `txmanager’ Bean)。 + +| |如果要
中的`TransactionManager`线的 Bean 名称为`transactionManager`,则可以在事务通知
(``)中省略`transaction-manager`属性。如果要连接的`TransactionManager` Bean 中的
具有任何其他名称,则必须显式地使用`transaction-manager`属性,如前面的示例所示。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +``定义确保由 txadvice Bean 定义的事务通知在程序中的适当位置运行。首先,定义一个切入点,该切入点与`FooService`接口中定义的任何操作的执行相匹配。然后使用 advisor 将切入点与`txAdvice`关联起来。结果表明,在执行`fooServiceOperation`时,运行由`txAdvice`定义的通知。 + +在``元素中定义的表达式是 AspectJ 切入点表达式。有关 Spring 中的切入点表达式的更多详细信息,请参见[the AOP section](core.html#aop)。 + +一个常见的需求是使整个服务层具有事务性。实现此目的的最佳方法是更改切入点表达式,以匹配服务层中的任何操作。下面的示例展示了如何做到这一点: + +``` + + + + +``` + +| |在前面的示例中,假设你的所有服务接口都是在`x.y.service`包中定义的
。有关更多详细信息,请参见[the AOP section](core.html#aop)。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +既然我们已经分析了配置,那么你可能会问自己:“所有这些配置实际上做了什么?” + +前面显示的配置用于围绕根据`fooService` Bean 定义创建的对象创建事务代理。该代理配置了事务通知,以便当在该代理上调用适当的方法时,根据与该方法关联的事务配置,启动、挂起、标记为只读等等。考虑以下测试驱动前面显示的配置的程序: + +爪哇 + +``` +public final class Boot { + + public static void main(final String[] args) throws Exception { + ApplicationContext ctx = new ClassPathXmlApplicationContext("context.xml"); + FooService fooService = ctx.getBean(FooService.class); + fooService.insertFoo(new Foo()); + } +} +``` + +Kotlin + +``` +import org.springframework.beans.factory.getBean + +fun main() { + val ctx = ClassPathXmlApplicationContext("context.xml") + val fooService = ctx.getBean("fooService") + fooService.insertFoo(Foo()) +} +``` + +运行前一个程序的输出应该类似于以下内容(为清楚起见,对`DefaultFooService`类的 `insertfoo(..)’方法抛出的`UnsupportedOperationException`的 log4j 输出和堆栈跟踪进行了截断): + +``` + +[AspectJInvocationContextExposingAdvisorAutoProxyCreator] - Creating implicit proxy for bean 'fooService' with 0 common interceptors and 1 specific interceptors + + +[JdkDynamicAopProxy] - Creating JDK dynamic proxy for [x.y.service.DefaultFooService] + + +[TransactionInterceptor] - Getting transaction for x.y.service.FooService.insertFoo + + +[DataSourceTransactionManager] - Creating new transaction with name [x.y.service.FooService.insertFoo] +[DataSourceTransactionManager] - Acquired Connection [[email protected]] for JDBC transaction + + +[RuleBasedTransactionAttribute] - Applying rules to determine whether transaction should rollback on java.lang.UnsupportedOperationException +[TransactionInterceptor] - Invoking rollback for transaction on x.y.service.FooService.insertFoo due to throwable [java.lang.UnsupportedOperationException] + + +[DataSourceTransactionManager] - Rolling back JDBC transaction on Connection [[email protected]] +[DataSourceTransactionManager] - Releasing JDBC Connection after transaction +[DataSourceUtils] - Returning JDBC Connection to DataSource + +Exception in thread "main" java.lang.UnsupportedOperationException at x.y.service.DefaultFooService.insertFoo(DefaultFooService.java:14) + +at $Proxy0.insertFoo(Unknown Source) +at Boot.main(Boot.java:11) +``` + +要使用反应式事务管理,代码必须使用反应式类型。 + +| |Spring 框架使用`ReactiveAdapterRegistry`来确定方法
的返回类型是否是反应性的。| +|---|--------------------------------------------------------------------------------------------------------------| + +下面的清单显示了以前使用的`FooService`的修改版本,但这次代码使用了反应类型: + +爪哇 + +``` +// the reactive service interface that we want to make transactional + +package x.y.service; + +public interface FooService { + + Flux getFoo(String fooName); + + Publisher getFoo(String fooName, String barName); + + Mono insertFoo(Foo foo); + + Mono updateFoo(Foo foo); + +} +``` + +Kotlin + +``` +// the reactive service interface that we want to make transactional + +package x.y.service + +interface FooService { + + fun getFoo(fooName: String): Flow + + fun getFoo(fooName: String, barName: String): Publisher + + fun insertFoo(foo: Foo) : Mono + + fun updateFoo(foo: Foo) : Mono +} +``` + +下面的示例展示了前面接口的一个实现: + +爪哇 + +``` +package x.y.service; + +public class DefaultFooService implements FooService { + + @Override + public Flux getFoo(String fooName) { + // ... + } + + @Override + public Publisher getFoo(String fooName, String barName) { + // ... + } + + @Override + public Mono insertFoo(Foo foo) { + // ... + } + + @Override + public Mono updateFoo(Foo foo) { + // ... + } +} +``` + +Kotlin + +``` +package x.y.service + +class DefaultFooService : FooService { + + override fun getFoo(fooName: String): Flow { + // ... + } + + override fun getFoo(fooName: String, barName: String): Publisher { + // ... + } + + override fun insertFoo(foo: Foo): Mono { + // ... + } + + override fun updateFoo(foo: Foo): Mono { + // ... + } +} +``` + +对于事务边界和事务属性定义,命令式事务管理和反应式事务管理共享相同的语义。命令式事务和反应式事务之间的主要区别在于后者的延迟性。`TransactionInterceptor`使用事务操作符装饰返回的反应类型,以开始并清理事务。因此,调用事务性反应方法将实际事务管理推迟到激活反应类型处理的订阅类型。 + +反应式事务管理的另一个方面涉及数据转义,这是编程模型的自然结果。 + +命令式事务的方法返回值在方法成功终止时从事务性方法返回,以便部分计算的结果不会逃脱方法闭包。 + +Active Transaction 方法返回一个 Active 包装器类型,该类型表示一个计算序列以及开始和完成计算的承诺。 + +`Publisher`可以在事务正在进行但不一定完成时发出数据。因此,依赖于成功完成整个事务的方法需要确保调用代码中的完成和缓冲结果。 + +#### 1.4.3.回滚声明性事务 + +上一节概述了如何在应用程序中以声明方式为类(通常是服务层类)指定事务设置的基础知识。本节介绍如何以简单的声明式方式控制事务的回滚。 + +向 Spring 框架的事务基础结构指示要回滚事务工作的推荐方法是,从当前在事务上下文中执行的代码中抛出`Exception`。 Spring 框架的事务基础设施代码捕获任何未处理的`Exception`,因为它在调用堆栈中冒泡,并决定是否将事务标记为回滚。 + +在其默认配置中, Spring 框架的事务基础设施代码仅在运行时未检查异常的情况下标记用于回滚的事务。也就是说,当抛出的异常是`RuntimeException`的实例或子类时。(默认情况下,“错误”实例也会导致回滚)。从事务方法抛出的已检查异常不会在默认配置中导致回滚。 + +你可以准确地配置哪些`Exception`类型标记了回滚事务,包括选中的异常。下面的 XML 片段演示了如何为选中的、特定于应用程序的`Exception`类型配置回滚: + +``` + + + + + + +``` + +如果你不希望在抛出异常时回滚事务,也可以指定“没有回滚规则”。下面的示例告诉 Spring 框架的事务基础结构,即使面对未处理的`InstrumentNotFoundException`,也要提交伴随的事务: + +``` + + + + + + +``` + +Spring 框架的事务基础设施捕捉到异常并参考配置的回滚规则以确定是否将事务标记为回滚时,最强的匹配规则胜出。因此,在以下配置的情况下,除了`InstrumentNotFoundException`以外的任何异常都会导致附带事务的回滚: + +``` + + + + + +``` + +你还可以通过编程的方式指示所需的回滚。尽管很简单,但这个过程是非常具有侵入性的,并且将你的代码与 Spring 框架的事务基础结构紧密地结合在一起。下面的示例展示了如何以编程方式指示所需的回滚: + +爪哇 + +``` +public void resolvePosition() { + try { + // some business logic... + } catch (NoProductInStockException ex) { + // trigger rollback programmatically + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + } +} +``` + +Kotlin + +``` +fun resolvePosition() { + try { + // some business logic... + } catch (ex: NoProductInStockException) { + // trigger rollback programmatically + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + } +} +``` + +如果可能的话,我们强烈建议你使用声明式方法进行回滚。如果你绝对需要编程回滚,它是可用的,但是它的使用与实现一个干净的基于 POJO 的体系结构是背道而驰的。 + +#### 1.4.4.为不同的 bean 配置不同的事务语义 ### + +考虑这样的场景:你有许多服务层对象,并且希望对每个对象应用完全不同的事务配置。可以通过使用不同的`pointcut`和 `advice-ref’属性值定义不同的``元素来实现。 + +作为比较,首先假设你的所有服务层类都是在根`x.y.service`包中定义的。要使在该包(或子包)中定义的类的实例以及名称以`Service`结尾的所有 bean 具有默认的事务配置,你可以编写以下内容: + +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +下面的示例展示了如何配置具有完全不同的事务设置的两个不同的 bean: + +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +#### 1.4.5.\设置 + +本节总结了你可以通过使用``标记来指定的各种事务设置。默认的``设置是: + +* [传播设定](#tx-propagation)是`REQUIRED.` + +* 隔离级别为`DEFAULT.` + +* 事务是可读写的。 + +* 事务超时默认为基础事务系统的默认超时,如果不支持超时,则无超时。 + +* 任何`RuntimeException`都会触发回滚,而任何选中的`Exception`都不会。 + +你可以更改这些默认设置。下表总结了嵌套在``和``标签中的``标签的各种属性: + +| Attribute |Required?| Default |说明| +|-----------------|---------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `name` | Yes | |将事务属性与之关联的方法名。可以使用
通配符(\*)字符将相同的事务属性
设置与许多方法关联(例如,`get*`,`handle*`,`on*Event`,以及
forth)。| +| `propagation` | No |`REQUIRED`|事务传播行为。| +| `isolation` | No |`DEFAULT` |事务隔离级别。仅适用于`REQUIRED`或`REQUIRES_NEW`的传播设置。| +| `timeout` | No | \-1 |事务超时(秒)。仅适用于传播`REQUIRED`或`REQUIRES_NEW`。| +| `read-only` | No | false |读写与只读事务。仅适用于`REQUIRED`或`REQUIRES_NEW`。| +| `rollback-for` | No | |触发回滚的`Exception`实例的逗号分隔列表。例如,“com.foo.mybusinessexception,servletexception”。| +|`no-rollback-for`| No | |不触发回滚的`Exception`实例的逗号分隔列表。例如,“com.foo.mybusinessexception,servletexception”。| + +#### 1.4.6.使用`@Transactional` + +除了事务配置的基于 XML 的声明式方法之外,还可以使用基于注释的方法。直接在 爪哇 源代码中声明事务语义会使声明更接近受影响的代码。不存在太大的过度耦合的危险,因为本来打算在交易中使用的代码几乎总是以这种方式部署的。 + +| |标准的`javax.transaction.Transactional`注释也被支持为 Spring 自己的注释的
插入替换。有关更多详细信息,请参阅 JTA1.2 文档
。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +使用`@Transactional`注释所提供的易用性最好用一个示例来说明,下面的文本对此进行了解释。考虑以下类定义: + +爪哇 + +``` +// the service class that we want to make transactional +@Transactional +public class DefaultFooService implements FooService { + + @Override + public Foo getFoo(String fooName) { + // ... + } + + @Override + public Foo getFoo(String fooName, String barName) { + // ... + } + + @Override + public void insertFoo(Foo foo) { + // ... + } + + @Override + public void updateFoo(Foo foo) { + // ... + } +} +``` + +Kotlin + +``` +// the service class that we want to make transactional +@Transactional +class DefaultFooService : FooService { + + override fun getFoo(fooName: String): Foo { + // ... + } + + override fun getFoo(fooName: String, barName: String): Foo { + // ... + } + + override fun insertFoo(foo: Foo) { + // ... + } + + override fun updateFoo(foo: Foo) { + // ... + } +} +``` + +在类级别上使用,如上面所述,该注释表示声明类的所有方法(以及其子类)的默认值。或者,每个方法都可以单独注释。有关 Spring 考虑事务性的方法的更多详细信息,请参见[Method visibility and `@Transactional`](#transaction-declarative-annotations-method-visibility)。请注意,类级注释不适用于类层次结构中的祖先类;在这种情况下,继承的方法需要在本地重新声明才能参与子类级注释。 + +当像上面这样的 POJO 类在 Spring 上下文中被定义为 Bean 时,可以在`@Configuration`类中通过`@EnableTransactionManagement`注释使 Bean 实例具有事务性。有关详细信息,请参见[javadoc](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/transaction/annotation/EnableTransactionManagement.html)。 + +在 XML 配置中,``标记提供了类似的便利: + +``` + + + + + + + + + + (1) + + + + + + + + + +``` + +|**1**|使 Bean 实例具有事务性的行。| +|-----|----------------------------------------------------| + +| |如果要连接的`TransactionManager`的 Bean 名称为 `TransactionManager’,则可以省略`transaction-manager`标记中的`transaction-manager`属性。如果要依赖注入的`TransactionManager` Bean 中的
具有任何其他名称,则必须使用`transaction-manager`属性,如在
前面的示例中所示。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +反应式事务方法使用反应式返回类型,而不是命令式编程安排,如下所示: + +爪哇 + +``` +// the reactive service class that we want to make transactional +@Transactional +public class DefaultFooService implements FooService { + + @Override + public Publisher getFoo(String fooName) { + // ... + } + + @Override + public Mono getFoo(String fooName, String barName) { + // ... + } + + @Override + public Mono insertFoo(Foo foo) { + // ... + } + + @Override + public Mono updateFoo(Foo foo) { + // ... + } +} +``` + +Kotlin + +``` +// the reactive service class that we want to make transactional +@Transactional +class DefaultFooService : FooService { + + override fun getFoo(fooName: String): Flow { + // ... + } + + override fun getFoo(fooName: String, barName: String): Mono { + // ... + } + + override fun insertFoo(foo: Foo): Mono { + // ... + } + + override fun updateFoo(foo: Foo): Mono { + // ... + } +} +``` + +注意,对于返回的`Publisher`有关于无功流抵消信号的特殊考虑因素。有关更多详细信息,请参见“使用 TransactionOperator”下的[取消信号](#tx-prog-operator-cancel)小节。 + +| |方法可见性和`@Transactional`

当你使用具有 Spring 的标准配置的事务代理时,你应该只对具有
可见性的方法应用`@Transactional`注释。如果你使用
注释`protected`、`private`或使用`@Transactional`注释的包-可见方法,则不会引发错误,但是注释的方法不显示已配置的
事务设置。如果需要对非公共方法进行注释,请考虑以下段落中的技巧,用于基于类的代理,或者考虑使用 AspectJ 编译时或加载时编织(稍后将进行说明)。在类中使用时,`protected`或
包-visible 方法也可以通过
注册自定义的`transactionAttributeSource` Bean 来使基于类的代理具有事务性,就像下面的示例一样。
注意,基于接口的代理中的事务方法必须始终是“公共的”并在 proxied 接口中进行了定义。

```
/**
* Register a custom AnnotationTransactionAttributeSource with the
*PublicMethodsOnly 标志设置为 false,以启用对
* protected and package-private @Transactional methods in
*基于类的代理的支持。
@see yTransactionConfiguration#TransactioneManageR=”()“/>527”/><<503>>>[proxtActeConteFramework=527"/>[参见[事务管理](testing.html#testcontext-tx)中的测试
章节中的示例。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +你可以将`@Transactional`注释应用于接口定义、接口上的方法、类定义或类上的方法。然而,仅仅存在`@Transactional`注释并不足以激活事务行为。`@Transactional`注释仅仅是元数据,它可以被一些运行时基础设施(`@Transactional`)使用,并且可以使用元数据来配置具有事务行为的适当 bean。在前面的示例中,``元素切换事务行为。 + +| |Spring 团队建议你只使用`@Transactional`注释来注释具体的类(和
具体类的方法),而不是注释接口,
你当然可以将`@Transactional`注释放在接口(或接口
方法)上,但是,只有当你使用基于接口的
代理时,这才能正常工作。爪哇 注释不是从接口继承而来的,这意味着,如果使用基于类的代理(`proxy-target-class=“true”`)或基于编织的
方面(`mode=“aspectj”`),代理
和编织基础设施将无法识别事务设置,并且该对象不包装在事务性代理中。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |在代理模式(这是默认模式)中,只有通过
代理进入的外部方法调用才会被拦截。这意味着自我调用(实际上,在
中的一个方法调用目标对象的另一个方法)在运行时不会导致实际的
事务,即使调用的方法被标记为`@Transactional`。此外,
代理必须完全初始化以提供预期的行为,因此你不应该在初始化代码中依赖
这个特性——例如,在`@PostConstruct`方法中。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +如果你希望使用事务来包装自调用,请考虑使用 AspectJ 模式(参见下表中的`mode`属性)。在这种情况下,首先不存在代理。相反,目标类被编织(也就是说,它的字节码被修改)以支持在任何类型的方法上的`@Transactional`运行时行为。 + +| XML Attribute | Annotation Attribute | Default |说明| +|---------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|`transaction-manager`|N/A (see [`TransactionManagementConfigurer`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/transaction/annotation/TransactionManagementConfigurer.html) javadoc)| `transactionManager` |要使用的事务管理器的名称。仅当事务
管理器的名称不是`transactionManager`时才需要,如前面的示例所示。| +| `mode` | `mode` | `proxy` |缺省模式处理要通过使用 Spring 的 AOP
框架来代理的带注释的 bean(遵循代理语义,如前所述,应用于仅通过代理进入的方法调用
)。替代模式使用 Spring 的 AspectJ 事务方面来编织
受影响的类,修改目标类
字节码以应用于任何类型的方法调用。AspectJ 的编织需要 Spring-Aspects. jar 中的 Classpath 以及具有启用的加载时编织(或编译时
编织)。(有关如何设置加载时编织的详细信息,请参见[Spring configuration](core.html#aop-aj-ltw-spring)。| +|`proxy-target-class` | `proxyTargetClass` | `false` |仅适用于`proxy`模式。控制为使用`@Transactional`注释的类创建的事务代理类型
。如果“proxy-target-class”属性设置为`true`,则创建基于类的代理。
如果`proxy-target-class`是`false`,或者如果省略了该属性,则创建标准的 JDK
基于接口的代理。(有关不同代理类型的详细检查,请参见[代理机制](core.html#aop-proxying)。| +| `order` | `order` |`Ordered.LOWEST_PRECEDENCE`|定义应用于带“@transactional”注释的 bean 的事务通知的顺序。(有关与 AOP 通知的排序相关的规则的更多信息,请参见。)没有指定的排序意味着由 AOP 子系统确定通知的顺序。| + +| |处理`@Transactional`注释的默认通知模式是`proxy`,
,这仅允许通过代理拦截调用。在
相同的类内的本地调用不能以这种方式被拦截。对于更高级的拦截模式,
可以考虑结合编译时或加载时编织切换到`aspectj`模式。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |`proxy-target-class`属性控制为使用`@Transactional`注释的类创建的事务代理类型
。如果将“proxy-target-class”设置为`true`,则创建基于类的代理。如果“proxy-target-class”是`false`,或者省略了该属性,则创建标准的基于接口的 JDK
代理。(有关不同代理类型的讨论,请参见[代理机制](core.html#aop-proxying)。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |`@EnableTransactionManagement`和``仅在定义它们的相同应用程序上下文中的 bean 上查找 `@transactional’。
这意味着,如果将注释驱动的配置放在`WebApplicationContext`的`DispatcherServlet`中,它只在你的控制器`@Transactional`中检查`@Transactional`bean,而不是在你的服务中。有关更多信息,请参见[MVC](web.html#mvc-servlet)。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在计算方法的事务设置时,派生最多的位置优先。在下面的示例中,`DefaultFooService`类在类级别上使用只读事务的设置进行注释,但是在同一个类中的`updateFoo(Foo)`方法上的 `@Transactional’注释优先于在类级别上定义的事务设置。 + +爪哇 + +``` +@Transactional(readOnly = true) +public class DefaultFooService implements FooService { + + public Foo getFoo(String fooName) { + // ... + } + + // these settings have precedence for this method + @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW) + public void updateFoo(Foo foo) { + // ... + } +} +``` + +Kotlin + +``` +@Transactional(readOnly = true) +class DefaultFooService : FooService { + + override fun getFoo(fooName: String): Foo { + // ... + } + + // these settings have precedence for this method + @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW) + override fun updateFoo(foo: Foo) { + // ... + } +} +``` + +##### `@Transactional`设置 + +`@Transactional`注释是元数据,它指定接口、类或方法必须具有事务语义(例如,“在调用该方法时启动一个全新的只读事务,挂起任何现有事务”)。默认的`@Transactional`设置如下: + +* 传播设置为`PROPAGATION_REQUIRED.` + +* 隔离级别`ISOLATION_DEFAULT.` + +* 事务是可读写的。 + +* 事务超时默认为基础事务系统的默认超时,如果不支持超时,则为无超时。 + +* 任何`RuntimeException`都会触发回滚,而任何选中的`Exception`都不会。 + +你可以更改这些默认设置。下表总结了`@Transactional`注释的各种属性: + +| Property | Type |说明| +|--------------------------------------------------|-----------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------| +|[value](#tx-multiple-tx-mgrs-with-attransactional)| `String` |指定要使用的事务管理器的可选限定符。| +| [propagation](#tx-propagation) | `enum`: `Propagation` |可选的传播设置。| +| `isolation` | `enum`: `Isolation` |可选隔离级别。仅适用于`REQUIRED`或`REQUIRES_NEW`的传播值。| +| `timeout` | `int` (in seconds of granularity) |可选事务超时。仅适用于`REQUIRED`或`REQUIRES_NEW`的传播值。| +| `readOnly` | `boolean` |读写与只读事务。仅适用于`REQUIRED`或`REQUIRES_NEW`的值。| +| `rollbackFor` | Array of `Class` objects, which must be derived from `Throwable.` |必须引起回滚的异常类的可选数组.| +| `rollbackForClassName` | Array of class names. The classes must be derived from `Throwable.` |必须引起回滚的异常类名称的可选数组。| +| `noRollbackFor` | Array of `Class` objects, which must be derived from `Throwable.` |不能导致回滚的异常类的可选数组.| +| `noRollbackForClassName` | Array of `String` class names, which must be derived from `Throwable.` |不能导致回滚的异常类名称的可选数组。| +| `label` |Array of `String` labels to add an expressive description to the transaction.|标签可以由事务管理器进行评估,以将
实现特定的行为与实际事务相关联。| + +目前,你无法对事务的名称进行显式控制,其中“name”是指事务监视器(如果适用)(例如,WebLogic 的事务监视器)和日志输出中出现的事务名称。对于声明式事务,事务名称始终是完全限定类名称 +`.`+ 事务建议类的方法名称。例如,如果`BusinessService`类的 `handlePayment(..)’方法启动了一个事务,那么事务的名称将是:`com.example.BusinessService.handlePayment`。 + +##### 具有`@Transactional`的多个事务管理器 + +Spring 大多数应用程序只需要一个事务管理器,但是可能存在希望在单个应用程序中有多个独立事务管理器的情况。你可以使用“@Transactional”注释的`value`或`transactionManager`属性来指定要使用的“TransactionManager”的标识。这可以是 Bean 名称,也可以是事务管理器 Bean 的限定符值。例如,使用限定符表示法,你可以在应用程序上下文中将以下 爪哇 代码与以下事务管理器 Bean 声明结合起来: + +爪哇 + +``` +public class TransactionalService { + + @Transactional("order") + public void setSomething(String name) { ... } + + @Transactional("account") + public void doSomething() { ... } + + @Transactional("reactive-account") + public Mono doSomethingReactive() { ... } +} +``` + +Kotlin + +``` +class TransactionalService { + + @Transactional("order") + fun setSomething(name: String) { + // ... + } + + @Transactional("account") + fun doSomething() { + // ... + } + + @Transactional("reactive-account") + fun doSomethingReactive(): Mono { + // ... + } +} +``` + +下面的清单显示了 Bean 项声明: + +``` + + + + ... + + + + + ... + + + + + ... + + +``` + +在这种情况下,`TransactionalService`上的各个方法在单独的事务管理器下运行,由`order`、`account`和`reactive-account`限定符区分。如果没有找到特别限定的`TransactionManager` Bean,则仍然使用默认的``目标 Bean 名称`transactionManager`。 + +##### 自定义合成注释 + +如果你发现在许多不同的方法上重复使用与`@Transactional`相同的属性,[Spring’s meta-annotation support](core.html#beans-meta-annotations)允许你为特定的用例定义自定义组合注释。例如,考虑以下注释定义: + +爪哇 + +``` +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Transactional(transactionManager = "order", label = "causal-consistency") +public @interface OrderTx { +} + +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Transactional(transactionManager = "account", label = "retryable") +public @interface AccountTx { +} +``` + +Kotlin + +``` +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.TYPE) +@Retention(AnnotationRetention.RUNTIME) +@Transactional(transactionManager = "order", label = ["causal-consistency"]) +annotation class OrderTx + +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.TYPE) +@Retention(AnnotationRetention.RUNTIME) +@Transactional(transactionManager = "account", label = ["retryable"]) +annotation class AccountTx +``` + +前面的注释让我们将上一节的示例写如下: + +爪哇 + +``` +public class TransactionalService { + + @OrderTx + public void setSomething(String name) { + // ... + } + + @AccountTx + public void doSomething() { + // ... + } +} +``` + +Kotlin + +``` +class TransactionalService { + + @OrderTx + fun setSomething(name: String) { + // ... + } + + @AccountTx + fun doSomething() { + // ... + } +} +``` + +在前面的示例中,我们使用语法来定义事务管理器限定符和事务标签,但也可以包括传播行为、回滚规则、超时和其他功能。 + +#### 1.4.7.事务传播 + +本节描述 Spring 中事务传播的一些语义。请注意,本节不是事务传播的适当介绍。相反,它详细介绍了 Spring 中有关事务传播的一些语义。 + +在 Spring-管理事务中,要注意物理事务和逻辑事务之间的区别,以及传播设置如何应用于这种区别。 + +##### 理解`PROPAGATION_REQUIRED` + +![需要 TX 道具](images/tx_prop_required.png) + +`PROPAGATION_REQUIRED`强制执行一个物理事务,如果当前范围还不存在事务,则在本地执行,或者参与为更大范围定义的现有“外部”事务。这是相同线程内的公共调用堆栈安排中的一个很好的默认设置(例如,一个服务 facade,它将委托给几个存储库方法,在这些存储库中,所有底层资源都必须参与服务级事务)。 + +| |默认情况下,参与事务将加入外部作用域的特性,
默默地忽略本地隔离级别超时值,或只读标志(如果有的话)。
如果你希望在参与
具有不同隔离级别的现有事务时拒绝隔离级别声明,请考虑在事务`validateExistingTransactions`管理器上将`true`标志切换为`true`。这种不宽松的模式还
拒绝只读不匹配(即,试图参与
只读外部作用域的内部读写事务)。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +当传播设置`PROPAGATION_REQUIRED`时,将为应用该设置的每个方法创建一个逻辑事务作用域。每个这样的逻辑事务作用域可以单独确定仅回滚状态,外部事务作用域在逻辑上独立于内部事务作用域。在标准`PROPAGATION_REQUIRED`行为的情况下,所有这些作用域都映射到相同的物理事务。因此,内部事务范围中设置的只回滚标记确实会影响外部事务实际提交的机会。 + +但是,在内部事务作用域设置仅回滚标记的情况下,外部事务本身并未决定回滚,因此回滚(由内部事务作用域静默触发)是意外的。这时会抛出相应的“unexpectedRollbackException”。这是一种预期的行为,这样事务的调用者就永远不会被误导,以为提交是在实际上没有执行的情况下执行的。因此,如果内部事务(外部调用方不知道该事务)静默地将事务标记为只回滚,则外部调用方仍然调用 commit。外部调用者需要接收`UnexpectedRollbackException`,以清楚地表明执行了回滚。 + +##### 理解`PROPAGATION_REQUIRES_NEW` + +![TX Prop 需要新的](images/tx_prop_requires_new.png) + +`PROPAGATION_REQUIRES_NEW`,与`PROPAGATION_REQUIRED`相反,总是对每个受影响的事务范围使用独立的物理事务,而不是参与外部范围的现有事务。在这种安排中,底层资源事务是不同的,因此可以独立地提交或回滚,外部事务不受内部事务的回滚状态的影响,内部事务的锁在完成后立即释放。这种独立的内部事务还可以声明自己的隔离级别、超时和只读设置,而不会继承外部事务的特征。 + +##### 理解`PROPAGATION_NESTED` + +`PROPAGATION_NESTED`使用一个具有多个保存点的单个物理事务,它可以回滚到这些保存点。这样的部分回滚让内部事务作用域触发其作用域的回滚,外部事务能够继续物理事务,尽管有些操作已经回滚。该设置通常映射到 JDBC 保存点上,因此它仅适用于 JDBC 资源事务。参见 Spring 的[“DataSourceTransactionManager”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jdbc/datasource/DataSourceTransactionManager.html)。 + +#### 1.4.8.为交易业务提供咨询 + +假设你希望同时运行事务操作和一些基本的分析建议。在``的上下文中,如何实现这一点? + +调用`updateFoo(Foo)`方法时,你希望看到以下操作: + +* 配置的分析方面开始。 + +* 事务性建议运行。 + +* 被建议对象上的方法运行。 + +* 事务提交。 + +* 分析方面报告了整个事务方法调用的确切持续时间。 + +| |本章不涉及对 AOP 进行任何详细的解释(除非它
适用于交易)。关于 AOP
配置和 AOP 一般的详细覆盖,请参见[AOP](core.html#aop)。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +下面的代码展示了前面讨论的简单分析方面: + +爪哇 + +``` +package x.y; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.springframework.util.StopWatch; +import org.springframework.core.Ordered; + +public class SimpleProfiler implements Ordered { + + private int order; + + // allows us to control the ordering of advice + public int getOrder() { + return this.order; + } + + public void setOrder(int order) { + this.order = order; + } + + // this method is the around advice + public Object profile(ProceedingJoinPoint call) throws Throwable { + Object returnValue; + StopWatch clock = new StopWatch(getClass().getName()); + try { + clock.start(call.toShortString()); + returnValue = call.proceed(); + } finally { + clock.stop(); + System.out.println(clock.prettyPrint()); + } + return returnValue; + } +} +``` + +Kotlin + +``` +class SimpleProfiler : Ordered { + + private var order: Int = 0 + + // allows us to control the ordering of advice + override fun getOrder(): Int { + return this.order + } + + fun setOrder(order: Int) { + this.order = order + } + + // this method is the around advice + fun profile(call: ProceedingJoinPoint): Any { + var returnValue: Any + val clock = StopWatch(javaClass.name) + try { + clock.start(call.toShortString()) + returnValue = call.proceed() + } finally { + clock.stop() + println(clock.prettyPrint()) + } + return returnValue + } +} +``` + +建议的排序是通过`Ordered`接口控制的。有关建议订购的详细信息,请参见[Advice ordering](core.html#aop-ataspectj-advice-ordering)。 + +以下配置创建了一个`fooService` Bean,该配置具有按所需顺序应用于它的分析和事务方面: + +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +你可以以类似的方式配置任意数量的附加方面。 + +下面的示例创建了与前两个示例相同的设置,但使用了纯 XML 声明式方法: + +``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +上述配置的结果是一个`fooService` Bean,它具有按该顺序应用于它的分析和事务方面。如果你希望在进入事务建议之后和退出事务建议之前运行分析建议,则可以交换分析方面 Bean 的`order`属性的值,使其高于事务建议的订单值。 + +你可以以类似的方式配置其他方面。 + +#### 1.4.9.使用`@Transactional`与 AspectJ + +你还可以通过 AspectJ 方面在 Spring 容器之外使用 Spring 框架的`@Transactional`支持。为此,首先使用`@Transactional`注释对类(以及可选的类的方法)进行注释,然后使用在 ` Spring-Aspects. jar ` 文件中定义的 `org.SpringFramework.Transaction.AspectJ.AnnotationTransactionAspect’链接(编织)应用程序。你还必须使用事务管理器配置方面。你可以使用 Spring 框架的 IOC 容器来处理依赖关系-注入方面。配置事务管理方面的最简单方法是使用``元素,并将`mode`属性指定为`aspectj`,如[Using `@Transactional`](#transaction-declarative-annotations)中所述。因为我们在这里关注的是在 Spring 容器之外运行的应用程序,所以我们向你展示了如何以编程方式完成它。 + +| |在继续之前,你可能需要分别阅读[Using `@Transactional`](#transaction-declarative-annotations)和[AOP](core.html#aop)。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------| + +下面的示例展示了如何创建事务管理器并配置“AnnotationTransactionAspect”来使用它: + +爪哇 + +``` +// construct an appropriate transaction manager +DataSourceTransactionManager txManager = new DataSourceTransactionManager(getDataSource()); + +// configure the AnnotationTransactionAspect to use it; this must be done before executing any transactional methods +AnnotationTransactionAspect.aspectOf().setTransactionManager(txManager); +``` + +Kotlin + +``` +// construct an appropriate transaction manager +val txManager = DataSourceTransactionManager(getDataSource()) + +// configure the AnnotationTransactionAspect to use it; this must be done before executing any transactional methods +AnnotationTransactionAspect.aspectOf().transactionManager = txManager +``` + +| |当你使用这个方面时,你必须注释实现类(或该类中的方法
或两者),而不是该类实现的接口(如果有的话)。AspectJ
遵循 爪哇 的规则,即接口上的注释不会被继承。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +类上的`@Transactional`注释指定了类中任何公共方法的执行的默认事务语义。 + +类中方法上的`@Transactional`注释重写了类注释给出的默认事务语义(如果存在的话)。无论可见性如何,你都可以对任何方法进行注释。 + +要使用`AnnotationTransactionAspect`编织应用程序,你必须使用 AspectJ 构建应用程序(参见[AspectJ 开发指南](https://www.eclipse.org/aspectj/doc/released/devguide/index.html))或使用加载时编织。请参阅[Load-time weaving with AspectJ in the Spring Framework](core.html#aop-aj-ltw),以了解有关使用 AspectJ 进行加载时编织的讨论。 + +### 1.5.程序化事务管理 + +Spring 框架提供了两种程序化事务管理的方法,通过使用: + +* `TransactionTemplate`或`TransactionalOperator`。 + +* 直接实现`TransactionManager`。 + +Spring 团队通常推荐`TransactionTemplate`用于命令流中的程序化事务管理,而`TransactionalOperator`用于反应性代码。第二种方法类似于使用 JTA`UserTransaction`API,尽管异常处理不那么麻烦。 + +#### 1.5.1.使用`TransactionTemplate` + +`TransactionTemplate`采用了与其他 Spring 模板相同的方法,例如`JdbcTemplate`。它使用一种回调方法(将应用程序代码从必须执行样板获取和释放事务资源的过程中解放出来),并生成意图驱动的代码,因为你的代码只关注你想要做的事情。 + +| |正如下面的示例所示,使用`TransactionTemplate`绝对
将你与 Spring 的事务基础设施和 API 结合在一起。程序化的
事务管理是否适合你的开发需求是你自己必须做出的决定。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +必须在事务上下文中运行并显式使用“TransactionTemplate”的应用程序代码类似于下一个示例。作为应用程序开发人员,你可以编写`TransactionCallback`实现(通常表示为匿名内部类),其中包含你需要在事务上下文中运行的代码。然后,你可以将自定义`TransactionCallback`的实例传递给在`TransactionTemplate`上公开的 `execute(..)’方法。下面的示例展示了如何做到这一点: + +爪哇 + +``` +public class SimpleService implements Service { + + // single TransactionTemplate shared amongst all methods in this instance + private final TransactionTemplate transactionTemplate; + + // use constructor-injection to supply the PlatformTransactionManager + public SimpleService(PlatformTransactionManager transactionManager) { + this.transactionTemplate = new TransactionTemplate(transactionManager); + } + + public Object someServiceMethod() { + return transactionTemplate.execute(new TransactionCallback() { + // the code in this method runs in a transactional context + public Object doInTransaction(TransactionStatus status) { + updateOperation1(); + return resultOfUpdateOperation2(); + } + }); + } +} +``` + +Kotlin + +``` +// use constructor-injection to supply the PlatformTransactionManager +class SimpleService(transactionManager: PlatformTransactionManager) : Service { + + // single TransactionTemplate shared amongst all methods in this instance + private val transactionTemplate = TransactionTemplate(transactionManager) + + fun someServiceMethod() = transactionTemplate.execute { + updateOperation1() + resultOfUpdateOperation2() + } +} +``` + +如果没有返回值,你可以使用带有匿名类的方便的`TransactionCallbackWithoutResult`类,如下所示: + +爪哇 + +``` +transactionTemplate.execute(new TransactionCallbackWithoutResult() { + protected void doInTransactionWithoutResult(TransactionStatus status) { + updateOperation1(); + updateOperation2(); + } +}); +``` + +Kotlin + +``` +transactionTemplate.execute(object : TransactionCallbackWithoutResult() { + override fun doInTransactionWithoutResult(status: TransactionStatus) { + updateOperation1() + updateOperation2() + } +}) +``` + +回调中的代码可以通过调用所提供的`TransactionStatus`对象上的 `setRollBackOnly()’方法回滚事务,如下所示: + +爪哇 + +``` +transactionTemplate.execute(new TransactionCallbackWithoutResult() { + + protected void doInTransactionWithoutResult(TransactionStatus status) { + try { + updateOperation1(); + updateOperation2(); + } catch (SomeBusinessException ex) { + status.setRollbackOnly(); + } + } +}); +``` + +Kotlin + +``` +transactionTemplate.execute(object : TransactionCallbackWithoutResult() { + + override fun doInTransactionWithoutResult(status: TransactionStatus) { + try { + updateOperation1() + updateOperation2() + } catch (ex: SomeBusinessException) { + status.setRollbackOnly() + } + } +}) +``` + +##### 指定事务设置 + +你可以在`TransactionTemplate`上以编程方式或在配置中指定事务设置(例如传播模式、隔离级别、超时等等)。默认情况下,`TransactionTemplate`实例具有[默认事务设置](#transaction-declarative-txadvice-settings)。下面的示例显示了对特定`TransactionTemplate:`的事务设置的程序化定制。 + +爪哇 + +``` +public class SimpleService implements Service { + + private final TransactionTemplate transactionTemplate; + + public SimpleService(PlatformTransactionManager transactionManager) { + this.transactionTemplate = new TransactionTemplate(transactionManager); + + // the transaction settings can be set here explicitly if so desired + this.transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED); + this.transactionTemplate.setTimeout(30); // 30 seconds + // and so forth... + } +} +``` + +Kotlin + +``` +class SimpleService(transactionManager: PlatformTransactionManager) : Service { + + private val transactionTemplate = TransactionTemplate(transactionManager).apply { + // the transaction settings can be set here explicitly if so desired + isolationLevel = TransactionDefinition.ISOLATION_READ_UNCOMMITTED + timeout = 30 // 30 seconds + // and so forth... + } +} +``` + +下面的示例通过使用 Spring XML 配置定义带有一些自定义事务设置的`TransactionTemplate`: + +``` + + + + +``` + +然后,你可以将`sharedTransactionTemplate`注入到所需的尽可能多的服务中。 + +最后,`TransactionTemplate`类的实例是线程安全的,因为实例不维护任何会话状态。`TransactionTemplate`实例确实保持配置状态。因此,虽然许多类可能共享`TransactionTemplate`的单个实例,但是如果一个类需要使用具有不同设置(例如,不同的隔离级别)的`TransactionTemplate`,则需要创建两个不同的`TransactionTemplate`实例。 + +#### 1.5.2.使用`TransactionOperator` + +`TransactionOperator`遵循类似于其他无功运算符的运算符设计。它使用一种回调方法(将应用程序代码从必须执行样板获取和释放事务资源的过程中解放出来),并生成意图驱动的代码,因为你的代码只关注你想要做的事情。 + +| |正如下面的示例所示,使用`TransactionOperator`绝对
将你与 Spring 的事务基础设施和 API 结合在一起。程序化的
事务管理是否适合你的开发需求是你自己必须做出的决定。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +必须在事务上下文中运行并显式使用`TransactionOperator`的应用程序代码类似于下一个示例: + +爪哇 + +``` +public class SimpleService implements Service { + + // single TransactionOperator shared amongst all methods in this instance + private final TransactionalOperator transactionalOperator; + + // use constructor-injection to supply the ReactiveTransactionManager + public SimpleService(ReactiveTransactionManager transactionManager) { + this.transactionOperator = TransactionalOperator.create(transactionManager); + } + + public Mono someServiceMethod() { + + // the code in this method runs in a transactional context + + Mono update = updateOperation1(); + + return update.then(resultOfUpdateOperation2).as(transactionalOperator::transactional); + } +} +``` + +Kotlin + +``` +// use constructor-injection to supply the ReactiveTransactionManager +class SimpleService(transactionManager: ReactiveTransactionManager) : Service { + + // single TransactionalOperator shared amongst all methods in this instance + private val transactionalOperator = TransactionalOperator.create(transactionManager) + + suspend fun someServiceMethod() = transactionalOperator.executeAndAwait { + updateOperation1() + resultOfUpdateOperation2() + } +} +``` + +`TransactionalOperator`可以通过两种方式使用: + +* 操作员风格,使用项目反应器类型(“mono.as”) + +* 其他情况的回调样式(`transactionaloperator.execute(transactioncallback)`) + +回调中的代码可以通过在提供的`ReactiveTransaction`对象上调用`setRollbackOnly()`方法回滚事务,如下所示: + +爪哇 + +``` +transactionalOperator.execute(new TransactionCallback<>() { + + public Mono doInTransaction(ReactiveTransaction status) { + return updateOperation1().then(updateOperation2) + .doOnError(SomeBusinessException.class, e -> status.setRollbackOnly()); + } + } +}); +``` + +Kotlin + +``` +transactionalOperator.execute(object : TransactionCallback() { + + override fun doInTransactionWithoutResult(status: ReactiveTransaction) { + updateOperation1().then(updateOperation2) + .doOnError(SomeBusinessException.class, e -> status.setRollbackOnly()) + } +}) +``` + +##### Cancel Signals + +在反应流中,`Subscriber`可以取消其`Subscription`并停止其 `publisher’。Project Reactor 中的操作员以及其他库中的操作员,例如`next()`、`take(long)’、`timeout(Duration)`,以及其他库中的操作员可以发出取消通知。没有办法知道取消的原因,无论是由于错误还是仅仅是缺乏进一步消费的兴趣。由于版本 5.3 取消信号导致回滚。因此,重要的是要考虑事务“发布者”下游使用的操作符。特别是在 a`Flux`或其它多值`Publisher`的情况下,必须消耗完整的输出以允许事务完成。 + +##### 指定事务设置 + +你可以为`TransactionalOperator`指定事务设置(例如传播模式、隔离级别、超时等等)。默认情况下,“TransactionalOperator”实例具有[默认事务设置](#transaction-declarative-txadvice-settings)。下面的示例显示了针对特定的“TransactionalOperator”的事务设置的定制: + +Java + +``` +public class SimpleService implements Service { + + private final TransactionalOperator transactionalOperator; + + public SimpleService(ReactiveTransactionManager transactionManager) { + DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); + + // the transaction settings can be set here explicitly if so desired + definition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED); + definition.setTimeout(30); // 30 seconds + // and so forth... + + this.transactionalOperator = TransactionalOperator.create(transactionManager, definition); + } +} +``` + +Kotlin + +``` +class SimpleService(transactionManager: ReactiveTransactionManager) : Service { + + private val definition = DefaultTransactionDefinition().apply { + // the transaction settings can be set here explicitly if so desired + isolationLevel = TransactionDefinition.ISOLATION_READ_UNCOMMITTED + timeout = 30 // 30 seconds + // and so forth... + } + private val transactionalOperator = TransactionalOperator(transactionManager, definition) +} +``` + +#### 1.5.3.使用`TransactionManager` + +下面的部分解释命令式事务管理器和反应式事务管理器的编程使用。 + +##### 使用`PlatformTransactionManager` + +对于命令式事务,你可以直接使用“org.springframework.transactionmanager.platform transactionmanager”来管理你的事务。要做到这一点,通过 Bean 引用将你使用的`PlatformTransactionManager`的实现传递给你的 Bean。然后,通过使用`TransactionDefinition`和 `TransactionStatus’对象,你可以发起事务、回滚和提交。下面的示例展示了如何做到这一点: + +Java + +``` +DefaultTransactionDefinition def = new DefaultTransactionDefinition(); +// explicitly setting the transaction name is something that can be done only programmatically +def.setName("SomeTxName"); +def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); + +TransactionStatus status = txManager.getTransaction(def); +try { + // put your business logic here +} catch (MyException ex) { + txManager.rollback(status); + throw ex; +} +txManager.commit(status); +``` + +Kotlin + +``` +val def = DefaultTransactionDefinition() +// explicitly setting the transaction name is something that can be done only programmatically +def.setName("SomeTxName") +def.propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRED + +val status = txManager.getTransaction(def) +try { + // put your business logic here +} catch (ex: MyException) { + txManager.rollback(status) + throw ex +} + +txManager.commit(status) +``` + +##### 使用`ReactiveTransactionManager` + +在处理反应性事务时,可以直接使用 `org.springframework.transaction.reactiveTransactionManager’来管理事务。要做到这一点,通过 Bean 引用将你使用的`ReactiveTransactionManager`的实现传递给你的 Bean。然后,通过使用`TransactionDefinition`和 `reactiveTransaction’对象,你可以发起事务、回滚和提交。下面的示例展示了如何做到这一点: + +Java + +``` +DefaultTransactionDefinition def = new DefaultTransactionDefinition(); +// explicitly setting the transaction name is something that can be done only programmatically +def.setName("SomeTxName"); +def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); + +Mono reactiveTx = txManager.getReactiveTransaction(def); + +reactiveTx.flatMap(status -> { + + Mono tx = ...; // put your business logic here + + return tx.then(txManager.commit(status)) + .onErrorResume(ex -> txManager.rollback(status).then(Mono.error(ex))); +}); +``` + +Kotlin + +``` +val def = DefaultTransactionDefinition() +// explicitly setting the transaction name is something that can be done only programmatically +def.setName("SomeTxName") +def.propagationBehavior = TransactionDefinition.PROPAGATION_REQUIRED + +val reactiveTx = txManager.getReactiveTransaction(def) +reactiveTx.flatMap { status -> + + val tx = ... // put your business logic here + + tx.then(txManager.commit(status)) + .onErrorResume { ex -> txManager.rollback(status).then(Mono.error(ex)) } +} +``` + +### 1.6.在程序化事务管理和声明式事务管理之间进行选择 + +只有当你有少量的事务操作时,程序化事务管理通常才是一个好主意。例如,如果你有一个 Web 应用程序,该应用程序仅需要用于某些更新操作的事务,那么你可能不希望通过使用 Spring 或任何其他技术来设置事务代理。在这种情况下,使用“交易模板”可能是一种很好的方法。能够显式地设置事务名称也只能通过使用程序化的事务管理方法来完成。 + +另一方面,如果你的应用程序有许多事务操作,声明式事务管理通常是值得的。它使事务管理脱离了业务逻辑,并且不难配置。 Spring 当使用框架而不是 EJB CMT 时,声明式事务管理的配置成本大大降低。 + +### 1.7.事务绑定事件 + +根据 Spring 4.2,事件的侦听器可以绑定到事务的一个阶段。典型的示例是在事务成功完成时处理事件。当当前事务的结果对侦听器确实很重要时,这样做可以更灵活地使用事件。 + +你可以使用`@EventListener`注释来注册一个常规的事件侦听器。如果需要将其绑定到事务,请使用`@TransactionalEventListener`。这样做时,默认情况下侦听器将绑定到事务的提交阶段。 + +下一个示例展示了这个概念。假设组件发布了一个命令创建的事件,并且我们希望定义一个侦听器,该侦听器只应在成功提交了它所发布的事务之后才处理该事件。下面的示例设置了这样的事件侦听器: + +Java + +``` +@Component +public class MyComponent { + + @TransactionalEventListener + public void handleOrderCreatedEvent(CreationEvent creationEvent) { + // ... + } +} +``` + +Kotlin + +``` +@Component +class MyComponent { + + @TransactionalEventListener + fun handleOrderCreatedEvent(creationEvent: CreationEvent) { + // ... + } +} +``` + +`@TransactionalEventListener`注释公开了一个`phase`属性,该属性允许你定制侦听器应该绑定到的事务的阶段。有效的阶段是`BEFORE_COMMIT`,`AFTER_COMMIT`(默认),`AFTER_ROLLBACK`,以及汇总事务完成(无论是提交还是回滚)的 `after_complete’。 + +如果没有事务正在运行,则根本不会调用侦听器,因为我们无法遵守所需的语义。但是,你可以通过将注释的`fallbackExecution`属性设置为`true`来重写该行为。 + +| |`@TransactionalEventListener`仅适用于由“Platform TransactionManager”管理的线程绑定事务。由`ReactiveTransactionManager`管理的反应性事务使用反应器上下文而不是线程本地属性,因此从
事件侦听器的角度来看,它不能参与兼容的活动事务。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 1.8.特定于应用服务器的集成 + +Spring 的事务抽象通常与应用服务器无关。此外, Spring 的`JtaTransactionManager`类(它可以选择性地为 JTA`UserTransaction`和`TransactionManager`对象执行 JNDI 查找)自动检测后一个对象的位置,该位置因应用程序服务器的不同而不同。访问 JTA 的“TransactionManager”允许增强事务语义——特别是支持事务暂停。有关详细信息,请参见[“JTATRANSACTIONMANER”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/transaction/jta/JtaTransactionManager.html)Javadoc。 + +Spring 的`JtaTransactionManager`是在 Java EE 应用程序服务器上运行的标准选择,并且已知可在所有公共服务器上运行。高级功能(如事务挂起)也可以在许多服务器上运行(包括 GlassFish、JBoss 和 Geronimo),而不需要任何特殊配置。然而,对于完全支持的事务挂起和进一步的高级集成, Spring 包括用于 WebLogic Server 和 WebSphere 的特殊适配器。这些适配器将在下面的小节中进行讨论。 + +对于包括 WebLogic Server 和 WebSphere 在内的标准场景,可以考虑使用方便的``配置元素。在进行配置时,此元素会自动检测基础服务器,并为平台选择最佳的事务管理器。这意味着你不需要显式地配置特定于服务器的适配器类(如以下小节中讨论的那样)。相反,它们是自动选择的,标准的“jtaTransactionManager”是默认的后备选项。 + +#### 1.8.1.IBMWebSphere + +在 WebSphere6.1.0.9 及以上版本中,推荐使用的 Spring JTA 事务管理器是“WebSphereOwTransactionManager”。这个特殊的适配器使用 IBM 的`UOWManager`API,它在 WebSphereApplicationServer6.1.0.9 及更高版本中可用。有了这个适配器, Spring 驱动的事务暂停(由 `pragation_requires_new’发起的暂停和恢复)得到了 IBM 的正式支持。 + +#### 1.8.2.Oracle WebLogic 服务器 + +在 WebLogic Server9.0 或更高版本中,你通常会使用“WebLogicJTatRansactionManager”,而不是 stock`JtaTransactionManager`类。普通`JtaTransactionManager`的这个特殊的特定于 WebLogic 的子类在 WebLogic 管理的事务环境中支持 Spring 事务定义的全部功能,而不是标准的 JTA 语义。功能包括事务名称、每个事务隔离级别,以及在所有情况下正确地恢复事务。 + +### 1.9.常见问题的解决方案 + +这一节描述了一些常见问题的解决方案。 + +#### 1.9.1.对特定的`DataSource`#### 使用错误的事务管理器 + +根据事务技术和需求的选择,使用正确的`PlatformTransactionManager`实现。 Spring 框架使用得当,仅提供了一种直接的、可移植的抽象。如果使用全局事务,则必须使用 `org.springframework.transaction.jta.jtatransactionmanager’类(或它的[特定于应用服务器的子类](#transaction-application-server-integration))执行所有事务操作。否则,事务基础结构将尝试在容器`DataSource`实例等资源上执行本地事务。这样的本地事务是没有意义的,一个好的应用程序服务器会将它们视为错误。 + +### 1.10.更多资源 + +有关 Spring 框架的事务支持的更多信息,请参见: + +* [Distributed transactions in Spring, with and without XA](https://www.javaworld.com/javaworld/jw-01-2009/jw-01-spring-transactions.html)是一个 JavaWorld 演示文稿,其中 Spring 的 David Syer 指导你了解 Spring 应用程序中分布式事务的七种模式,其中三种有 XA,四种没有 XA。 + +* [*Java 事务设计策略 *](https://www.infoq.com/minibooks/JTDS)是一本可从[InfoQ](https://www.infoq.com/)获得的书,它提供了关于 Java 事务的快节奏的介绍。它还包括关于如何配置和使用 Spring 框架和 EJB3 的事务的并排示例。 + +## 2. DAO 支持 + +Spring 中的数据访问对象支持旨在使其易于以一致的方式与数据访问技术(例如 JDBC、 Hibernate 或 JPA)一起工作。这使你可以相当容易地在上述持久性技术之间进行切换,并且还允许你编写代码,而无需担心捕获特定于每种技术的异常。 + +### 2.1.一致的异常层次结构 + +Spring 提供了一种从技术特定的异常(例如 SQLException)到其自身的异常类层次结构的方便转换,其具有将`DataAccessException`作为根异常的特性。这些异常包装了原始异常,这样就不会有任何风险,你可能会丢失任何有关可能出了什么问题的信息。 + +除了 JDBC 异常之外, Spring 还可以包装 JPA 和 Hibernate 特定的异常,将它们转换为一组重点运行时异常。这样,你就可以只在适当的层中处理大多数不可恢复的持久性异常,而不需要在 DAO 中有烦人的样板抓取和抛出块和异常声明。正如上面提到的,JDBC 异常(包括特定于数据库的方言)也被转换为相同的层次结构,这意味着你可以在一致的编程模型中使用 JDBC 执行一些操作。 + +前面的讨论适用于 Spring 对各种 ORM 框架的支持中的各种模板类。如果使用基于拦截器的类,则应用程序必须关心处理`HibernateExceptions`和`PersistenceExceptions`本身,最好是通过分别将`convertHibernateAccessException(..)`或`convertJpaAccessException(..)`方法委托给`SessionFactoryUtils`。这些方法将异常转换为与`org.springframework.dao`异常层次结构中的异常兼容的异常。由于`PersistenceExceptions`未被选中,它们也可能被抛出(尽管在异常方面牺牲了泛型 DAO 抽象)。 + +下面的图像显示了 Spring 提供的异常层次结构。(请注意,图片中详细介绍的类层次结构只显示了整个“DataAccessException”层次结构的一个子集。 + +![DataAccessCeption](images/DataAccessException.png) + +### 2.2.用于配置 DAO 或存储库类的注释 + +保证你的数据访问对象或存储库提供异常转换的最佳方法是使用`@Repository`注释。这种注释还允许组件扫描支持查找和配置你的 DAO 和存储库,而无需为它们提供 XML 配置条目。下面的示例展示了如何使用`@Repository`注释: + +Java + +``` +@Repository (1) +public class SomeMovieFinder implements MovieFinder { + // ... +} +``` + +|**1**|`@Repository`注释。| +|-----|-----------------------------| + +Kotlin + +``` +@Repository (1) +class SomeMovieFinder : MovieFinder { + // ... +} +``` + +|**1**|`@Repository`注释。| +|-----|-----------------------------| + +任何 DAO 或存储库实现都需要访问持久性资源,这取决于所使用的持久性技术。例如,基于 JDBC 的存储库需要访问 JDBC`DataSource`,而基于 JPA 的存储库需要访问 `EntityManager’。实现这一点的最简单方法是通过使用`@Autowired`、`@Inject`、`@Resource`或`@PersistenceContext`注释中的一个来注入这种资源依赖关系。下面的示例适用于 JPA 存储库: + +Java + +``` +@Repository +public class JpaMovieFinder implements MovieFinder { + + @PersistenceContext + private EntityManager entityManager; + + // ... +} +``` + +Kotlin + +``` +@Repository +class JpaMovieFinder : MovieFinder { + + @PersistenceContext + private lateinit var entityManager: EntityManager + + // ... +} +``` + +如果使用经典的 Hibernate API,则可以插入`SessionFactory`,如下例所示: + +Java + +``` +@Repository +public class HibernateMovieFinder implements MovieFinder { + + private SessionFactory sessionFactory; + + @Autowired + public void setSessionFactory(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + } + + // ... +} +``` + +Kotlin + +``` +@Repository +class HibernateMovieFinder(private val sessionFactory: SessionFactory) : MovieFinder { + // ... +} +``` + +我们在这里展示的最后一个示例是典型的 JDBC 支持。你可以将“DataSource”注入到初始化方法或构造函数中,通过使用`DataSource`,可以在其中创建“JDBcTemplate”和其他数据访问支持类(例如`SimpleJdbcCall`和其他类)。以下示例自动连接`DataSource`: + +Java + +``` +@Repository +public class JdbcMovieFinder implements MovieFinder { + + private JdbcTemplate jdbcTemplate; + + @Autowired + public void init(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + // ... +} +``` + +Kotlin + +``` +@Repository +class JdbcMovieFinder(dataSource: DataSource) : MovieFinder { + + private val jdbcTemplate = JdbcTemplate(dataSource) + + // ... +} +``` + +| |有关如何
配置应用程序上下文以利用这些注释的详细信息,请参见每个持久性技术的具体覆盖范围。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## 3. 使用 JDBC 进行数据访问 + +Spring 框架 JDBC 抽象所提供的值可能最好通过下表中概述的操作序列来显示。该表显示了 Spring 处理哪些行为,以及哪些行为是你的责任。 + +|行动|Spring|You| +|--------------------------------------------------------|------|---| +|定义连接参数。| | X | +|打开连接。| X | | +|指定 SQL 语句。| | X | +|声明参数并提供参数值| | X | +|准备并运行该语句。| X | | +|设置循环以遍历结果(如果有的话)。| X | | +|完成每个迭代的工作。| | X | +|处理任何异常。| X | | +|处理交易。| X | | +|关闭连接、语句和结果集。| X | | + +Spring 框架负责处理所有可能使 JDBC 成为如此乏味的 API 的底层细节。 + +### 3.1.一种 JDBC 数据库访问方法的选择 + +你可以在几种方法中进行选择,以形成你的 JDBC 数据库访问的基础。除了三种类型的`JdbcTemplate`之外,新的`SimpleJdbcInsert`和“SimpleJDBCCall”方法优化了数据库元数据,并且 RDBMS 对象风格采用了一种类似于 JDO 查询设计的更面向对象的方法。一旦你开始使用这些方法之一,你仍然可以进行混合和匹配,以包含来自不同方法的功能。所有的方法都需要一个符合 JDBC2.0 的驱动程序,而一些高级特性则需要一个 JDBC3.0 驱动程序。 + +* `JdbcTemplate`是经典且最流行的 Spring JDBC 方法。这种“最底层”的方法和所有其他方法都使用了 JDBCtemplate。 + +* `NamedParameterJdbcTemplate`封装一个`JdbcTemplate`以提供命名参数,而不是传统的 JDBC`?`占位符。当你对 SQL 语句有多个参数时,这种方法提供了更好的文档和易用性。 + +* `SimpleJdbcInsert`和`SimpleJdbcCall`优化数据库元数据,以限制所需配置的数量。这种方法简化了编码,因此只需要提供表或过程的名称,并提供匹配列名称的参数映射。只有当数据库提供了足够的元数据时,这种方法才会起作用。如果数据库不提供此元数据,则必须提供参数的显式配置。 + +* RDBMS 对象——包括`MappingSqlQuery`、`SqlUpdate`和`StoredProcedure`——要求你在数据访问层的初始化过程中创建可重用和线程安全的对象。这种方法是仿照 JDO 查询建模的,在 JDO 查询中,你定义查询字符串、声明参数并编译查询。一旦这样做了,`execute(…)`、`update(…​)`和`findObject(…​)`方法就可以用各种参数值多次调用。 + +### 3.2.包层次结构 + +Spring 框架的 JDBC 抽象框架由四个不同的包组成: + +* `core`:`org.springframework.jdbc.core`包包含`JdbcTemplate`类及其各种回调接口,以及各种相关的类。名为 `org.springframework.jdbc.core.simple’的子包包含`SimpleJdbcInsert`和 `SimpleJDBCCall’类。另一个名为 `org.springframework.jdbc.core.namedparam’的子包包含`NamedParameterJdbcTemplate`类和相关的支持类。见[使用 JDBC 核心类来控制基本的 JDBC 处理和错误处理](#jdbc-core),[JDBC 批处理操作](#jdbc-advanced-jdbc),和[Simplifying JDBC Operations with the `SimpleJdbc` Classes](#jdbc-simple-jdbc)。 + +* `datasource`:`org.springframework.jdbc.datasource`包包含一个用于轻松访问 ` 数据源’的实用程序类和各种简单的`DataSource`实现,你可以使用这些实现在 Java EE 容器之外测试和运行未经修改的 JDBC 代码。一个名为`org.springfamework.jdbc.datasource.embedded`的子包提供了通过使用 Java 数据库引擎创建嵌入式数据库的支持,例如 HSQL、H2 和 Derby。见[控制数据库连接](#jdbc-connections)和[嵌入式数据库支持](#jdbc-embedded-database-support)。 + +* `object`:`org.springframework.jdbc.object`包包含表示 RDBMS 查询、更新和存储过程的类,这些类是线程安全的、可重用的对象。见[将 JDBC 操作建模为 Java 对象](#jdbc-object)。这种方法是由 JDO 建模的,尽管查询返回的对象自然地与数据库断开连接。这个较高级别的 JDBC 抽象依赖于`org.springframework.jdbc.core`包中的较低级别抽象。 + +* `support`:`org.springframework.jdbc.support`包提供了`SQLException`翻译功能和一些实用程序类。在 JDBC 处理过程中抛出的异常被转换为`org.springframework.dao`包中定义的异常。这意味着使用 Spring JDBC 抽象层的代码不需要实现 JDBC 或 RDBMS 特定的错误处理。所有转换后的异常都未被选中,这使你可以选择捕获异常,以便在允许将其他异常传播给调用方的同时恢复这些异常。见[Using `SQLExceptionTranslator`](#jdbc-SQLExceptionTranslator)。 + +### 3.3.使用 JDBC 核心类来控制基本的 JDBC 处理和错误处理 + +本节介绍如何使用 JDBC 核心类来控制基本的 JDBC 处理,包括错误处理。它包括以下主题: + +* [Using `JdbcTemplate`](#jdbc-JdbcTemplate) + +* [Using `NamedParameterJdbcTemplate`](#jdbc-NamedParameterJdbcTemplate) + +* [Using `SQLExceptionTranslator`](#jdbc-SQLExceptionTranslator) + +* [正在运行的语句](#jdbc-statements-executing) + +* [Running Queries](#jdbc-statements-querying) + +* [更新数据库](#jdbc-updates) + +* [检索自动生成的密钥](#jdbc-auto-generated-keys) + +#### 3.3.1.使用`JdbcTemplate` + +`JdbcTemplate`是 JDBC 核心包中的中心类。它处理资源的创建和发布,这有助于你避免常见的错误,例如忘记关闭连接。它执行核心 JDBC 工作流的基本任务(例如语句的创建和执行),留下应用程序代码来提供 SQL 和提取结果。`JdbcTemplate`类: + +* 运行 SQL 查询 + +* 更新语句和存储过程调用 + +* 在`ResultSet`实例上执行迭代,并提取返回的参数值。 + +* 捕获 JDBC 异常,并将它们转换为在`org.springframework.dao`包中定义的通用的、信息量更大的异常层次结构。(见[一致的异常层次结构](#dao-exceptions)。 + +当你为代码使用`JdbcTemplate`时,你只需要实现回调接口,并为它们提供一个明确定义的契约。给定一个由 `JDBcTemplate’类提供的`Connection`,`PreparedStatementCreator`回调接口将创建一个准备好的语句,提供 SQL 和任何必要的参数。“CallableStatementCreator”接口也是如此,它创建了可调用语句。“rowcallbackhandler”接口从`ResultSet`的每一行提取值。 + +你可以通过使用`DataSource`引用的直接实例化在 DAO 实现中使用`JdbcTemplate`,或者可以在 Spring IoC 容器中配置它,并将其作为 Bean 引用提供给 DAO。 + +| |`DataSource`应该始终配置为 Spring IOC 容器中的 Bean。在
中,第一种情况将 Bean 直接赋予服务;在第二种情况下,将
赋予准备好的模板。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +这个类发布的所有 SQL 都记录在`DEBUG`级别的目录下,该目录对应于模板实例的完全限定类名称(通常是 `jdbctemplate’,但如果使用 `jdbctemplate’类的自定义子类,则可能会有所不同)。 + +下面的部分提供了`JdbcTemplate`用法的一些示例。这些示例并不是`JdbcTemplate`公开的所有功能的详尽列表。关于这一点,请参见乘务员[javadoc](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jdbc/core/JdbcTemplate.html)。 + +##### 查询(“选择”) + +下面的查询获取关系中的行数: + +Java + +``` +int rowCount = this.jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class); +``` + +Kotlin + +``` +val rowCount = jdbcTemplate.queryForObject("select count(*) from t_actor")!! +``` + +以下查询使用了一个绑定变量: + +Java + +``` +int countOfActorsNamedJoe = this.jdbcTemplate.queryForObject( + "select count(*) from t_actor where first_name = ?", Integer.class, "Joe"); +``` + +Kotlin + +``` +val countOfActorsNamedJoe = jdbcTemplate.queryForObject( + "select count(*) from t_actor where first_name = ?", arrayOf("Joe"))!! +``` + +下面的查询查找`String`: + +Java + +``` +String lastName = this.jdbcTemplate.queryForObject( + "select last_name from t_actor where id = ?", + String.class, 1212L); +``` + +Kotlin + +``` +val lastName = this.jdbcTemplate.queryForObject( + "select last_name from t_actor where id = ?", + arrayOf(1212L))!! +``` + +下面的查询查找并填充一个域对象: + +Java + +``` +Actor actor = jdbcTemplate.queryForObject( + "select first_name, last_name from t_actor where id = ?", + (resultSet, rowNum) -> { + Actor newActor = new Actor(); + newActor.setFirstName(resultSet.getString("first_name")); + newActor.setLastName(resultSet.getString("last_name")); + return newActor; + }, + 1212L); +``` + +Kotlin + +``` +val actor = jdbcTemplate.queryForObject( + "select first_name, last_name from t_actor where id = ?", + arrayOf(1212L)) { rs, _ -> + Actor(rs.getString("first_name"), rs.getString("last_name")) + } +``` + +以下查询查找并填充域对象列表: + +Java + +``` +List actors = this.jdbcTemplate.query( + "select first_name, last_name from t_actor", + (resultSet, rowNum) -> { + Actor actor = new Actor(); + actor.setFirstName(resultSet.getString("first_name")); + actor.setLastName(resultSet.getString("last_name")); + return actor; + }); +``` + +Kotlin + +``` +val actors = jdbcTemplate.query("select first_name, last_name from t_actor") { rs, _ -> + Actor(rs.getString("first_name"), rs.getString("last_name")) +``` + +如果最后两个代码片段实际上存在于同一个应用程序中,那么删除两个`RowMapper`lambda 表达式中存在的重复并将它们提取到一个字段中是有意义的,然后可以根据需要由 DAO 方法引用。例如,将前面的代码片段编写如下可能更好: + +Java + +``` +private final RowMapper actorRowMapper = (resultSet, rowNum) -> { + Actor actor = new Actor(); + actor.setFirstName(resultSet.getString("first_name")); + actor.setLastName(resultSet.getString("last_name")); + return actor; +}; + +public List findAllActors() { + return this.jdbcTemplate.query("select first_name, last_name from t_actor", actorRowMapper); +} +``` + +Kotlin + +``` +val actorMapper = RowMapper { rs: ResultSet, rowNum: Int -> + Actor(rs.getString("first_name"), rs.getString("last_name")) +} + +fun findAllActors(): List { + return jdbcTemplate.query("select first_name, last_name from t_actor", actorMapper) +} +``` + +##### 用`JdbcTemplate`更新(` 插入 `,`UPDATE`,和`DELETE`) + +可以使用`update(..)`方法执行插入、更新和删除操作。参数值通常以变量参数或对象数组的形式提供。 + +下面的示例插入一个新条目: + +Java + +``` +this.jdbcTemplate.update( + "insert into t_actor (first_name, last_name) values (?, ?)", + "Leonor", "Watling"); +``` + +Kotlin + +``` +jdbcTemplate.update( + "insert into t_actor (first_name, last_name) values (?, ?)", + "Leonor", "Watling") +``` + +下面的示例更新了一个现有条目: + +Java + +``` +this.jdbcTemplate.update( + "update t_actor set last_name = ? where id = ?", + "Banjo", 5276L); +``` + +Kotlin + +``` +jdbcTemplate.update( + "update t_actor set last_name = ? where id = ?", + "Banjo", 5276L) +``` + +下面的示例删除一个条目: + +Java + +``` +this.jdbcTemplate.update( + "delete from t_actor where id = ?", + Long.valueOf(actorId)); +``` + +Kotlin + +``` +jdbcTemplate.update("delete from t_actor where id = ?", actorId.toLong()) +``` + +##### 其它`JdbcTemplate`操作 + +你可以使用`execute(..)`方法来运行任意 SQL。因此,该方法通常用于 DDL 语句。它被接受回调接口、绑定变量数组等的变量严重超载。下面的示例创建了一个表: + +Java + +``` +this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))"); +``` + +Kotlin + +``` +jdbcTemplate.execute("create table mytable (id integer, name varchar(100))") +``` + +下面的示例调用一个存储过程: + +爪哇 + +``` +this.jdbcTemplate.update( + "call SUPPORT.REFRESH_ACTORS_SUMMARY(?)", + Long.valueOf(unionId)); +``` + +Kotlin + +``` +jdbcTemplate.update( + "call SUPPORT.REFRESH_ACTORS_SUMMARY(?)", + unionId.toLong()) +``` + +更复杂的存储过程支持是[covered later](#jdbc-StoredProcedure)。 + +##### `JdbcTemplate`最佳实践 + +一旦配置好,`JdbcTemplate`类的实例是线程安全的。这很重要,因为这意味着你可以配置`JdbcTemplate`的单个实例,然后将这个共享引用安全地注入到多个 DAO(或存储库)中。`JdbcTemplate`是有状态的,因为它保持了对`DataSource`的引用,但是这个状态不是会话状态。 + +在使用`JdbcTemplate`类(以及相关的[NamedParameterJDBcTemplate](#jdbc-NamedParameterJdbcTemplate)类)时,一种常见的做法是在 Spring 配置文件中配置`DataSource`,然后在依赖项-将共享的`DataSource` Bean 注入到 DAO 类中。在 setter 中为`DataSource`创建了`JdbcTemplate`。这导致了类似于以下的 DAO: + +爪哇 + +``` +public class JdbcCorporateEventDao implements CorporateEventDao { + + private JdbcTemplate jdbcTemplate; + + public void setDataSource(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + // JDBC-backed implementations of the methods on the CorporateEventDao follow... +} +``` + +Kotlin + +``` +class JdbcCorporateEventDao(dataSource: DataSource) : CorporateEventDao { + + private val jdbcTemplate = JdbcTemplate(dataSource) + + // JDBC-backed implementations of the methods on the CorporateEventDao follow... +} +``` + +下面的示例展示了相应的 XML 配置: + +``` + + + + + + + + + + + + + + + + + +``` + +显式配置的一种替代方法是使用组件扫描和注释支持进行依赖注入。在这种情况下,你可以用`@Repository`注释类(这使它成为组件扫描的候选),并用`DataSource`setter 方法注解`@Autowired`。下面的示例展示了如何做到这一点: + +爪哇 + +``` +@Repository (1) +public class JdbcCorporateEventDao implements CorporateEventDao { + + private JdbcTemplate jdbcTemplate; + + @Autowired (2) + public void setDataSource(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); (3) + } + + // JDBC-backed implementations of the methods on the CorporateEventDao follow... +} +``` + +|**1**|用`@Repository`注释类。| +|-----|----------------------------------------------------------| +|**2**|用`@Autowired`注释`DataSource`setter 方法。| +|**3**|用`DataSource`创建一个新的`JdbcTemplate`。| + +Kotlin + +``` +@Repository (1) +class JdbcCorporateEventDao(dataSource: DataSource) : CorporateEventDao { (2) + + private val jdbcTemplate = JdbcTemplate(dataSource) (3) + + // JDBC-backed implementations of the methods on the CorporateEventDao follow... +} +``` + +|**1**|用`@Repository`注释类。| +|-----|--------------------------------------------------| +|**2**|`DataSource`的构造函数注入。| +|**3**|用`DataSource`创建一个新的`JdbcTemplate`。| + +下面的示例展示了相应的 XML 配置: + +``` + + + + + + + + + + + + + + + + +``` + +如果你使用 Spring 的`JdbcDaoSupport`类,并且你的各种支持 JDBC 的 DAO 类从中扩展,那么你的子类将从 `JDBCDAOSupport’类继承一个`setDataSource(..)`方法。你可以选择是否从这个类继承。提供“JDBCDAOSupport”类只是为了方便。 + +无论你选择使用(或不使用)上述哪种模板初始化样式,每次要运行 SQL 时,都很少需要创建`JdbcTemplate`类的新实例。一旦配置好,`JdbcTemplate`实例就是线程安全的。如果你的应用程序访问多个数据库,你可能需要多个`JdbcTemplate`实例,这需要多个`DataSources`实例,然后需要多个配置不同的`JdbcTemplate`实例。 + +#### 3.3.2.使用`NamedParameterJdbcTemplate` + +`NamedParameterJdbcTemplate`类增加了对使用命名参数编程 JDBC 语句的支持,而不是仅使用经典占位符(`'?'`)参数编程 JDBC 语句。`NamedParameterJdbcTemplate`类包装了一个 `JDBcTemplate’,并将其委托给包装好的`JdbcTemplate`,以完成其大部分工作。本节仅描述`NamedParameterJdbcTemplate`类中与`JdbcTemplate`本身不同的区域——即通过使用命名参数编程 JDBC 语句。下面的示例展示了如何使用`NamedParameterJdbcTemplate`: + +爪哇 + +``` +// some JDBC-backed DAO class... +private NamedParameterJdbcTemplate namedParameterJdbcTemplate; + +public void setDataSource(DataSource dataSource) { + this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource); +} + +public int countOfActorsByFirstName(String firstName) { + + String sql = "select count(*) from T_ACTOR where first_name = :first_name"; + + SqlParameterSource namedParameters = new MapSqlParameterSource("first_name", firstName); + + return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class); +} +``` + +Kotlin + +``` +private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource) + +fun countOfActorsByFirstName(firstName: String): Int { + val sql = "select count(*) from T_ACTOR where first_name = :first_name" + val namedParameters = MapSqlParameterSource("first_name", firstName) + return namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Int::class.java)!! +} +``` + +请注意,在分配给`sql`变量的值和插入`namedParameters`变量(类型`MapSqlParameterSource`)的对应值中使用了命名参数表示法。 + +或者,你可以使用基于`Map`的样式,将命名参数及其对应值传递给 `NamedParameterJDbcTemplate’实例。由`NamedParameterJdbcOperations`公开并由 `NamedParameterJDBcTemplate’类实现的其余方法遵循类似的模式,在此不涉及。 + +下面的示例展示了基于`Map`的样式的使用: + +爪哇 + +``` +// some JDBC-backed DAO class... +private NamedParameterJdbcTemplate namedParameterJdbcTemplate; + +public void setDataSource(DataSource dataSource) { + this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource); +} + +public int countOfActorsByFirstName(String firstName) { + + String sql = "select count(*) from T_ACTOR where first_name = :first_name"; + + Map namedParameters = Collections.singletonMap("first_name", firstName); + + return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class); +} +``` + +Kotlin + +``` +// some JDBC-backed DAO class... +private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource) + +fun countOfActorsByFirstName(firstName: String): Int { + val sql = "select count(*) from T_ACTOR where first_name = :first_name" + val namedParameters = mapOf("first_name" to firstName) + return namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Int::class.java)!! +} +``` + +与`NamedParameterJdbcTemplate`(存在于同一个 爪哇 包中)相关的一个不错的特性是`SqlParameterSource`接口。你已经在前面的代码片段(“mapsqlparameterSource”类)中看到了这个接口实现的示例。`SqlParameterSource`是`NamedParameterJdbcTemplate`的命名参数值的来源。`MapSqlParameterSource`类是一个简单的实现,它是一个围绕`java.util.Map`的适配器,其中键是参数名称,值是参数值。 + +另一个`SqlParameterSource`实现是`BeanPropertySqlParameterSource`类。这个类包装一个任意的 爪哇Bean(即一个坚持[爪哇Bean 公约](https://www.oracle.com/technetwork/java/javase/documentation/spec-136004.html)的类的实例),并使用包装好的 爪哇Bean 的属性作为命名参数值的源。 + +下面的示例展示了一个典型的 爪哇Bean: + +爪哇 + +``` +public class Actor { + + private Long id; + private String firstName; + private String lastName; + + public String getFirstName() { + return this.firstName; + } + + public String getLastName() { + return this.lastName; + } + + public Long getId() { + return this.id; + } + + // setters omitted... + +} +``` + +Kotlin + +``` +data class Actor(val id: Long, val firstName: String, val lastName: String) +``` + +下面的示例使用`NamedParameterJdbcTemplate`返回前面示例中所示的类的成员数: + +爪哇 + +``` +// some JDBC-backed DAO class... +private NamedParameterJdbcTemplate namedParameterJdbcTemplate; + +public void setDataSource(DataSource dataSource) { + this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource); +} + +public int countOfActors(Actor exampleActor) { + + // notice how the named parameters match the properties of the above 'Actor' class + String sql = "select count(*) from T_ACTOR where first_name = :firstName and last_name = :lastName"; + + SqlParameterSource namedParameters = new BeanPropertySqlParameterSource(exampleActor); + + return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class); +} +``` + +Kotlin + +``` +// some JDBC-backed DAO class... +private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource) + +private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource) + +fun countOfActors(exampleActor: Actor): Int { + // notice how the named parameters match the properties of the above 'Actor' class + val sql = "select count(*) from T_ACTOR where first_name = :firstName and last_name = :lastName" + val namedParameters = BeanPropertySqlParameterSource(exampleActor) + return namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Int::class.java)!! +} +``` + +请记住,`NamedParameterJdbcTemplate`类封装了一个经典的`JdbcTemplate`模板。如果需要访问打包的`JdbcTemplate`实例以访问仅在`JdbcTemplate`类中存在的功能,则可以使用 `getjdbcoperations()’方法通过 `jdbcoperations’接口访问打包的`JdbcTemplate`。 + +另请参见[“JDBcTemplate”最佳做法](#jdbc-JdbcTemplate-idioms)关于在应用程序上下文中使用 `NamedParameterJDBcTemplate’类的指导方针。 + +#### 3.3.3.使用`SQLExceptionTranslator` + +`SQLExceptionTranslator`是一个由类实现的接口,它可以在`SQLException`s 和 Spring 自己的`org.springframework.dao.DataAccessException`之间转换,这在数据访问策略方面是不可知的。实现可以是通用的(例如,为 JDBC 使用 SQLState 代码),也可以是专有的(例如,使用 Oracle 错误代码),以获得更高的精度。 + +`SQLErrorCodeSQLExceptionTranslator`是默认情况下使用的`SQLExceptionTranslator`的实现。该实现使用特定的供应商代码。它比`SQLState`实现更精确。错误代码转换是基于在一个名为`SQLErrorCodes`的 爪哇Bean 类型类中保存的代码。这个类由`SQLErrorCodesFactory`创建和填充,它(顾名思义)是一个工厂,用于基于名为 `sql-error-codes.xml’的配置文件的内容创建`SQLErrorCodes`。该文件填充了供应商代码,并基于`DatabaseMetaData`中的 `DatabaseProductName’。使用了你正在使用的实际数据库的代码。 + +`SQLErrorCodeSQLExceptionTranslator`按以下顺序应用匹配规则: + +1. 由子类实现的任何自定义转换。通常情况下,使用提供的具体“sqlerrorcodesqlexceptiontranslator”,因此此规则不适用。它仅在你实际提供了一个子类实现的情况下才适用。 + +2. 作为`SQLErrorCodes`类的`customSqlExceptionTranslator`属性提供的`SQLExceptionTranslator`接口的任何自定义实现。 + +3. 搜索`CustomSQLErrorCodesTranslation`类的实例列表(为`SQLErrorCodes`类的 `CustomTranslations’属性提供)以查找匹配项。 + +4. 应用了错误码匹配。 + +5. 使用后备翻译程序。`SQLExceptionSubclassTranslator`是默认的后备翻译程序。如果此翻译不可用,则下一个后备翻译程序是`SQLStateSQLExceptionTranslator`。 + +| |默认情况下,`SQLErrorCodesFactory`用于定义`Error`代码和自定义异常
翻译。它们是在一个名为`sql-error-codes.xml`的文件中从
Classpath 中查找的,并且匹配的`SQLErrorCodes`实例是位于基于数据库
名称的数据库中使用的数据库元数据。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +你可以扩展`SQLErrorCodeSQLExceptionTranslator`,如下例所示: + +爪哇 + +``` +public class CustomSQLErrorCodesTranslator extends SQLErrorCodeSQLExceptionTranslator { + + protected DataAccessException customTranslate(String task, String sql, SQLException sqlEx) { + if (sqlEx.getErrorCode() == -12345) { + return new DeadlockLoserDataAccessException(task, sqlEx); + } + return null; + } +} +``` + +Kotlin + +``` +class CustomSQLErrorCodesTranslator : SQLErrorCodeSQLExceptionTranslator() { + + override fun customTranslate(task: String, sql: String?, sqlEx: SQLException): DataAccessException? { + if (sqlEx.errorCode == -12345) { + return DeadlockLoserDataAccessException(task, sqlEx) + } + return null + } +} +``` + +在前面的示例中,将翻译特定的错误代码(“-12345”),而其他错误将由默认的翻译实现来翻译。要使用此自定义转换器,你必须通过方法“setExceptionTranslator”将其传递给`JdbcTemplate`,并且你必须在需要此转换器的所有数据访问处理中使用此`JdbcTemplate`。下面的示例展示了如何使用这个自定义转换器: + +爪哇 + +``` +private JdbcTemplate jdbcTemplate; + +public void setDataSource(DataSource dataSource) { + + // create a JdbcTemplate and set data source + this.jdbcTemplate = new JdbcTemplate(); + this.jdbcTemplate.setDataSource(dataSource); + + // create a custom translator and set the DataSource for the default translation lookup + CustomSQLErrorCodesTranslator tr = new CustomSQLErrorCodesTranslator(); + tr.setDataSource(dataSource); + this.jdbcTemplate.setExceptionTranslator(tr); + +} + +public void updateShippingCharge(long orderId, long pct) { + // use the prepared JdbcTemplate for this update + this.jdbcTemplate.update("update orders" + + " set shipping_charge = shipping_charge * ? / 100" + + " where id = ?", pct, orderId); +} +``` + +Kotlin + +``` +// create a JdbcTemplate and set data source +private val jdbcTemplate = JdbcTemplate(dataSource).apply { + // create a custom translator and set the DataSource for the default translation lookup + exceptionTranslator = CustomSQLErrorCodesTranslator().apply { + this.dataSource = dataSource + } +} + +fun updateShippingCharge(orderId: Long, pct: Long) { + // use the prepared JdbcTemplate for this update + this.jdbcTemplate!!.update("update orders" + + " set shipping_charge = shipping_charge * ? / 100" + + " where id = ?", pct, orderId) +} +``` + +自定义翻译器被传递给一个数据源,以便在“sql-error-codes.xml”中查找错误代码。 + +#### 3.3.4.正在运行的语句 + +运行 SQL 语句只需要很少的代码。你需要`DataSource`和 `JDBcTemplate’,包括 `JDBcTemplate’提供的方便方法。下面的示例展示了创建一个新表的最小但功能齐全的类需要包括哪些内容: + +爪哇 + +``` +import javax.sql.DataSource; +import org.springframework.jdbc.core.JdbcTemplate; + +public class ExecuteAStatement { + + private JdbcTemplate jdbcTemplate; + + public void setDataSource(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + public void doExecute() { + this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))"); + } +} +``` + +Kotlin + +``` +import javax.sql.DataSource +import org.springframework.jdbc.core.JdbcTemplate + +class ExecuteAStatement(dataSource: DataSource) { + + private val jdbcTemplate = JdbcTemplate(dataSource) + + fun doExecute() { + jdbcTemplate.execute("create table mytable (id integer, name varchar(100))") + } +} +``` + +#### 3.3.5.正在运行的查询 + +一些查询方法返回一个值。要从一行中检索计数或特定值,请使用`queryForObject(..)`。后者将返回的 JDBC`Type`转换为作为参数传入的 爪哇 类。如果类型转换无效,将抛出“InvalidDataAccessiusAgeException”。下面的示例包含两个查询方法,一个用于查询`int`,另一个用于查询`String`: + +爪哇 + +``` +import javax.sql.DataSource; +import org.springframework.jdbc.core.JdbcTemplate; + +public class RunAQuery { + + private JdbcTemplate jdbcTemplate; + + public void setDataSource(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + public int getCount() { + return this.jdbcTemplate.queryForObject("select count(*) from mytable", Integer.class); + } + + public String getName() { + return this.jdbcTemplate.queryForObject("select name from mytable", String.class); + } +} +``` + +Kotlin + +``` +import javax.sql.DataSource +import org.springframework.jdbc.core.JdbcTemplate + +class RunAQuery(dataSource: DataSource) { + + private val jdbcTemplate = JdbcTemplate(dataSource) + + val count: Int + get() = jdbcTemplate.queryForObject("select count(*) from mytable")!! + + val name: String? + get() = jdbcTemplate.queryForObject("select name from mytable") +} +``` + +除了单个结果查询方法外,还有几个方法返回一个列表,其中包含查询返回的每一行的条目。最通用的方法是`queryForList(..)`,它返回一个`List`,其中每个元素都是`Map`,包含每个列的一个条目,并使用列名作为键。如果在前面的示例中添加一个方法来检索所有行的列表,它可能如下: + +爪哇 + +``` +private JdbcTemplate jdbcTemplate; + +public void setDataSource(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); +} + +public List> getList() { + return this.jdbcTemplate.queryForList("select * from mytable"); +} +``` + +Kotlin + +``` +private val jdbcTemplate = JdbcTemplate(dataSource) + +fun getList(): List> { + return jdbcTemplate.queryForList("select * from mytable") +} +``` + +返回的列表类似于以下内容: + +``` +[{name=Bob, id=1}, {name=Mary, id=2}] +``` + +#### 3.3.6.更新数据库 + +下面的示例更新了某个主键的列: + +爪哇 + +``` +import javax.sql.DataSource; +import org.springframework.jdbc.core.JdbcTemplate; + +public class ExecuteAnUpdate { + + private JdbcTemplate jdbcTemplate; + + public void setDataSource(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + public void setName(int id, String name) { + this.jdbcTemplate.update("update mytable set name = ? where id = ?", name, id); + } +} +``` + +Kotlin + +``` +import javax.sql.DataSource +import org.springframework.jdbc.core.JdbcTemplate + +class ExecuteAnUpdate(dataSource: DataSource) { + + private val jdbcTemplate = JdbcTemplate(dataSource) + + fun setName(id: Int, name: String) { + jdbcTemplate.update("update mytable set name = ? where id = ?", name, id) + } +} +``` + +在前面的示例中,SQL 语句具有行参数的占位符。你可以将参数值以 varargs 的形式传递,也可以以对象数组的形式传递。因此,你应该在原语包装器类中显式地包装原语,或者应该使用自动装箱。 + +#### 3.3.7.检索自动生成的密钥 + +一种`update()`方便的方法支持对数据库生成的主键进行检索。这种支持是 JDBC3.0 标准的一部分。有关详细信息,请参见说明书第 13.6 章。该方法以`PreparedStatementCreator`作为其第一个参数,这就是指定所需 INSERT 语句的方式。另一个参数是`KeyHolder`,它包含更新成功返回时生成的键。没有标准的单一方法来创建适当的`PreparedStatement`(这解释了为什么方法签名是这样的)。以下示例可以在 Oracle 上运行,但可能无法在其他平台上运行: + +爪哇 + +``` +final String INSERT_SQL = "insert into my_test (name) values(?)"; +final String name = "Rob"; + +KeyHolder keyHolder = new GeneratedKeyHolder(); +jdbcTemplate.update(connection -> { + PreparedStatement ps = connection.prepareStatement(INSERT_SQL, new String[] { "id" }); + ps.setString(1, name); + return ps; +}, keyHolder); + +// keyHolder.getKey() now contains the generated key +``` + +Kotlin + +``` +val INSERT_SQL = "insert into my_test (name) values(?)" +val name = "Rob" + +val keyHolder = GeneratedKeyHolder() +jdbcTemplate.update({ + it.prepareStatement (INSERT_SQL, arrayOf("id")).apply { setString(1, name) } +}, keyHolder) + +// keyHolder.getKey() now contains the generated key +``` + +### 3.4.控制数据库连接 + +本节内容包括: + +* [Using `DataSource`](#jdbc-datasource) + +* [Using `DataSourceUtils`](#jdbc-DataSourceUtils) + +* [Implementing `SmartDataSource`](#jdbc-SmartDataSource) + +* [Extending `AbstractDataSource`](#jdbc-AbstractDataSource) + +* [Using `SingleConnectionDataSource`](#jdbc-SingleConnectionDataSource) + +* [Using `DriverManagerDataSource`](#jdbc-DriverManagerDataSource) + +* [Using `TransactionAwareDataSourceProxy`](#jdbc-TransactionAwareDataSourceProxy) + +* [Using `DataSourceTransactionManager`](#jdbc-DataSourceTransactionManager) + +#### 3.4.1.使用`DataSource` + +Spring 通过`DataSource`获得到数据库的连接。a`DataSource`是 JDBC 规范的一部分,是一个通用的连接工厂。它允许容器或框架从应用程序代码中隐藏连接池和事务管理问题。作为开发人员,你不需要了解有关如何连接到数据库的详细信息。这是设置数据源的管理员的责任。在开发和测试代码时,你最有可能同时填充这两个角色,但是你并不一定要知道生产数据源是如何配置的。 + +当你使用 Spring 的 JDBC 层时,你可以从 JNDI 获得数据源,也可以使用第三方提供的连接池实现来配置你自己的数据源。传统的选择是带有 Bean 风格`DataSource`类的 ApacheCommonsDBCP 和 C3P0;对于现代的 JDBC 连接池,请考虑使用其 Builder 风格的 API 的 Hikaricp。 + +| |你应该使用`DriverManagerDataSource`和`SimpleDriverDataSource`类
(包含在 Spring 发行版中)仅用于测试目的!这些变体不
提供池,并且在发出多个连接请求时性能很差。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +下面的部分使用 Spring 的`DriverManagerDataSource`实现。其他几个`DataSource`变体将在后面介绍。 + +要配置`DriverManagerDataSource`: + +1. 获得与`DriverManagerDataSource`的连接,就像你通常获得 JDBC 连接一样。 + +2. 指定 JDBC 驱动程序的完全限定类名,以便`DriverManager`可以加载驱动程序类。 + +3. 提供不同于 JDBC 驱动程序的 URL。(请参阅驱动程序的文档以获得正确的值。 + +4. 提供连接到数据库的用户名和密码。 + +下面的示例展示了如何在 爪哇 中配置`DriverManagerDataSource`: + +爪哇 + +``` +DriverManagerDataSource dataSource = new DriverManagerDataSource(); +dataSource.setDriverClassName("org.hsqldb.jdbcDriver"); +dataSource.setUrl("jdbc:hsqldb:hsql://localhost:"); +dataSource.setUsername("sa"); +dataSource.setPassword(""); +``` + +Kotlin + +``` +val dataSource = DriverManagerDataSource().apply { + setDriverClassName("org.hsqldb.jdbcDriver") + url = "jdbc:hsqldb:hsql://localhost:" + username = "sa" + password = "" +} +``` + +下面的示例展示了相应的 XML 配置: + +``` + + + + + + + + +``` + +接下来的两个示例展示了 DBCP 和 C3P0 的基本连接和配置。要了解有助于控制池特性的更多选项,请参阅相应的连接池实现的产品文档。 + +下面的示例展示了 DBCP 配置: + +``` + + + + + + + + +``` + +下面的示例展示了 C3P0 配置: + +``` + + + + + + + + +``` + +#### 3.4.2.使用`DataSourceUtils` + +`DataSourceUtils`类是一个方便且功能强大的助手类,它提供 ` 静态’方法来从 JNDI 获得连接,并在必要时关闭连接。它支持线程绑定连接,例如,`DataSourceTransactionManager`。 + +#### 3.4.3.实现`SmartDataSource` + +`SmartDataSource`接口应该由能够提供到关系数据库的连接的类来实现。它扩展了`DataSource`接口,让使用它的类查询在给定的操作之后是否应该关闭连接。当你知道需要重用某个连接时,这种用法是有效的。 + +#### 3.4.4.扩展`AbstractDataSource` + +`AbstractDataSource`是 Spring 的`abstract`实现的一个`DataSource`基类。它实现了所有`DataSource`实现所共有的代码。如果你编写自己的`DataSource`实现,则应该扩展`AbstractDataSource`类。 + +#### 3.4.5.使用`SingleConnectionDataSource` + +`SingleConnectionDataSource`类是`SmartDataSource`接口的实现,该接口封装单个`Connection`,该接口在每次使用后都不会关闭。这不是多线程能力。 + +如果任何客户机代码在假定池连接的情况下调用`close`(如使用持久性工具时),则应将`Map`属性设置为`true`。此设置返回一个封装物理连接的关闭抑制代理。请注意,你不能再将其强制转换为原生 Oracle`Connection`或类似的对象。 + +`SingleConnectionDataSource`主要是一个测试类。它通常支持在应用程序服务器之外,结合简单的 JNDI 环境,对代码进行简单的测试。与`DriverManagerDataSource`相反,它始终重用相同的连接,避免了过多地创建物理连接。 + +#### 3.4.6.使用`DriverManagerDataSource` + +`DriverManagerDataSource`类是标准`DataSource`接口的一种实现,该接口通过 Bean 属性配置一个普通的 JDBC 驱动程序,并每次返回一个新的 ` 连接’。 + +这种实现对于 爪哇 EE 容器之外的测试和独立环境非常有用,可以作为 Spring IoC 容器中的`DataSource` Bean,也可以与简单的 JNDI 环境结合使用。池-假设`Connection.close()`调用关闭连接,那么任何`DataSource`可感知的持久性代码都应该工作。然而,即使在测试环境中,使用 爪哇Bean 风格的连接池(例如`commons-dbcp`)也非常容易,因此在 `DriverManagerDataSource’上使用这样的连接池几乎总是更好。 + +#### 3.4.7.使用`TransactionAwareDataSourceProxy` + +`TransactionAwareDataSourceProxy`是目标`DataSource`的代理。代理封装了目标`DataSource`,以添加对 Spring 管理的事务的感知。在这方面,它类似于事务 JNDI`DataSource`,由 爪哇 EE 服务器提供。 + +| |很少希望使用这个类,除非已经存在的代码必须被
调用并通过标准的 JDBC`DataSource`接口实现。在这种情况下,
仍然可以使该代码可用,同时,使该代码
参与 Spring 托管事务。通常情况下,最好使用用于资源管理的高级抽象来编写你的
自己的新代码,例如 `jdbctemplate’或`DataSourceUtils`。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +有关更多详细信息,请参见[“TransactionAwaredataSourceProxy”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jdbc/datasource/TransactionAwareDataSourceProxy.html)爪哇doc。 + +#### 3.4.8.使用`DataSourceTransactionManager` + +`DataSourceTransactionManager`类是单个 JDBC 数据源的`PlatformTransactionManager`实现。它将来自指定数据源的 JDBC 连接绑定到当前正在执行的线程,可能允许每个数据源有一个线程连接。 + +应用程序代码需要通过“datasourceutils.getConnection(数据源)”而不是 爪哇 EE 的标准“datasource.getConnection”来检索 JDBC 连接。它抛出未选中的`org.springframework.dao`异常,而不是选中的`LobHandler`。所有框架类(如`JdbcTemplate`)都隐式地使用此策略。如果不与此事务管理器一起使用,则查找策略的行为与常见策略完全相同。因此,它可以在任何情况下使用。 + +`DataSourceTransactionManager`类支持自定义隔离级别和超时,这些级别和超时是根据适当的 JDBC 语句查询超时应用的。为了支持后者,应用程序代码必须使用`JdbcTemplate`,或者为每个创建的语句调用 `datasourceutils.applyTransactionTimeout’方法。 + +在单资源情况下,你可以使用这个实现,而不是`JtaTransactionManager`,因为它不需要容器支持 JTA。在两者之间切换只是一个配置问题,只要你坚持所需的连接查找模式。JTA 不支持自定义隔离级别。 + +### 3.5.JDBC 批处理操作 + +如果对同一条准备好的语句进行多个调用的批处理,大多数 JDBC 驱动程序都会提供更好的性能。通过将更新分组为批,可以限制往返数据库的次数。 + +#### 3.5.1.带有`JdbcTemplate`的基本批处理操作 + +通过实现特殊接口的两种方法`JdbcTemplate`,并将该实现作为`batchUpdate`方法调用中的第二个参数传入,可以完成`JdbcTemplate`批处理。你可以使用`getBatchSize`方法来提供当前批处理的大小。你可以使用`setValues`方法来设置 prepared 语句的参数的值。这个方法被称为在`getBatchSize`调用中指定的次数。下面的示例基于列表中的条目更新`t_actor`表,并将整个列表用作批处理: + +爪哇 + +``` +public class JdbcActorDao implements ActorDao { + + private JdbcTemplate jdbcTemplate; + + public void setDataSource(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + public int[] batchUpdate(final List actors) { + return this.jdbcTemplate.batchUpdate( + "update t_actor set first_name = ?, last_name = ? where id = ?", + new BatchPreparedStatementSetter() { + public void setValues(PreparedStatement ps, int i) throws SQLException { + Actor actor = actors.get(i); + ps.setString(1, actor.getFirstName()); + ps.setString(2, actor.getLastName()); + ps.setLong(3, actor.getId().longValue()); + } + public int getBatchSize() { + return actors.size(); + } + }); + } + + // ... additional methods +} +``` + +Kotlin + +``` +class JdbcActorDao(dataSource: DataSource) : ActorDao { + + private val jdbcTemplate = JdbcTemplate(dataSource) + + fun batchUpdate(actors: List): IntArray { + return jdbcTemplate.batchUpdate( + "update t_actor set first_name = ?, last_name = ? where id = ?", + object: BatchPreparedStatementSetter { + override fun setValues(ps: PreparedStatement, i: Int) { + ps.setString(1, actors[i].firstName) + ps.setString(2, actors[i].lastName) + ps.setLong(3, actors[i].id) + } + + override fun getBatchSize() = actors.size + }) + } + + // ... additional methods +} +``` + +如果你处理一个更新流或从一个文件中读取数据,那么你可能有一个首选批处理大小,但是最后一个批处理可能没有那么多条目。在这种情况下,你可以使用`InterruptibleBatchPreparedStatementSetter`接口,它允许你在输入源耗尽时中断批处理。`isBatchExhausted`方法允许你发出批处理结束的信号。 + +#### 3.5.2.具有对象列表的批处理操作 + +`JdbcTemplate`和`NamedParameterJdbcTemplate`都提供了一种提供批更新的替代方式。你不是实现一个特殊的批处理接口,而是以列表的形式提供调用中的所有参数值。框架在这些值上循环,并使用内部准备的语句设置器。API 会有所不同,这取决于你是否使用命名参数。对于已命名的参数,你提供一个“SQLParameterSource”数组,这是批处理的每个成员的一个条目。你可以使用 `SQLParameterSourceUtils.CreateBatch’便利方法来创建这个数组,传入一个由 Bean 样式的对象组成的数组(getter 方法对应于参数)、`string’-keyed`Map`实例(包含相应的参数作为值),或者两者的混合。 + +下面的示例显示了使用命名参数的批处理更新: + +爪哇 + +``` +public class JdbcActorDao implements ActorDao { + + private NamedParameterTemplate namedParameterJdbcTemplate; + + public void setDataSource(DataSource dataSource) { + this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource); + } + + public int[] batchUpdate(List actors) { + return this.namedParameterJdbcTemplate.batchUpdate( + "update t_actor set first_name = :firstName, last_name = :lastName where id = :id", + SqlParameterSourceUtils.createBatch(actors)); + } + + // ... additional methods +} +``` + +Kotlin + +``` +class JdbcActorDao(dataSource: DataSource) : ActorDao { + + private val namedParameterJdbcTemplate = NamedParameterJdbcTemplate(dataSource) + + fun batchUpdate(actors: List): IntArray { + return this.namedParameterJdbcTemplate.batchUpdate( + "update t_actor set first_name = :firstName, last_name = :lastName where id = :id", + SqlParameterSourceUtils.createBatch(actors)); + } + + // ... additional methods +} +``` + +对于使用经典`?`占位符的 SQL 语句,你将传入一个包含带更新值的对象数组的列表。这个对象数组必须为 SQL 语句中的每个占位符有一个条目,并且它们的顺序必须与 SQL 语句中定义的顺序相同。 + +下面的示例与前面的示例相同,只是使用了经典的 JDBC`?`占位符: + +爪哇 + +``` +public class JdbcActorDao implements ActorDao { + + private JdbcTemplate jdbcTemplate; + + public void setDataSource(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + public int[] batchUpdate(final List actors) { + List batch = new ArrayList(); + for (Actor actor : actors) { + Object[] values = new Object[] { + actor.getFirstName(), actor.getLastName(), actor.getId()}; + batch.add(values); + } + return this.jdbcTemplate.batchUpdate( + "update t_actor set first_name = ?, last_name = ? where id = ?", + batch); + } + + // ... additional methods +} +``` + +Kotlin + +``` +class JdbcActorDao(dataSource: DataSource) : ActorDao { + + private val jdbcTemplate = JdbcTemplate(dataSource) + + fun batchUpdate(actors: List): IntArray { + val batch = mutableListOf>() + for (actor in actors) { + batch.add(arrayOf(actor.firstName, actor.lastName, actor.id)) + } + return jdbcTemplate.batchUpdate( + "update t_actor set first_name = ?, last_name = ? where id = ?", batch) + } + + // ... additional methods +} +``` + +我们前面描述的所有批更新方法都返回一个`int`数组,该数组包含每个批处理条目的受影响行数。这个计数是由 JDBC 驱动程序报告的。如果计数不可用,则 JDBC 驱动程序返回一个`-2`的值。 + +| |在这样的场景中,在底层`PreparedStatement`上自动设定值,
每个值对应的 JDBC 类型需要从给定的 爪哇 类型派生。
虽然这通常工作得很好,但可能会出现问题(例如,对于包含 map 的 `null’值)。 Spring,默认情况下,在这样的
情况下调用
,这在使用你的 JDBC 驱动程序时可能是昂贵的。你应该使用最近的驱动程序
版本,并考虑将`spring.jdbc.getParameterType.ignore`属性设置为`true`(作为 JVM 系统属性或通过`out`机制),如果你遇到
性能问题(如 Oracle12c、JBoss 和 PostgreSQL 上报告的那样)。

或者,你可以考虑通过`BatchPreparedStatementSetter`(如前面所示),通过显式类型
给出一个基于`List`的调用的显式类型
数组,显式地指定相应的 JDBC 类型,通过`registerSqlType`调用
自定义`MapSqlParameterSource`实例,或者通过`List`从 爪哇 声明的属性类型派生出 SQL 类型,即使对于空值也是如此。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 3.5.3.具有多批处理的批处理操作 + +前面的批更新示例处理的批是如此之大,以至于你希望将它们分解为几个较小的批。你可以通过多次调用`batchUpdate`方法来使用前面提到的方法来实现这一点,但是现在有了一个更方便的方法。除了 SQL 语句之外,该方法还需要一个包含参数的对象“集合”,每个批处理要进行的更新的数量,以及一个`ParameterizedPreparedStatementSetter`来为准备好的语句的参数值设置值。框架在提供的值上循环,并将更新调用分成指定大小的批。 + +下面的示例显示了一个使用批处理大小为 100 的批处理更新: + +爪哇 + +``` +public class JdbcActorDao implements ActorDao { + + private JdbcTemplate jdbcTemplate; + + public void setDataSource(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + public int[][] batchUpdate(final Collection actors) { + int[][] updateCounts = jdbcTemplate.batchUpdate( + "update t_actor set first_name = ?, last_name = ? where id = ?", + actors, + 100, + (PreparedStatement ps, Actor actor) -> { + ps.setString(1, actor.getFirstName()); + ps.setString(2, actor.getLastName()); + ps.setLong(3, actor.getId().longValue()); + }); + return updateCounts; + } + + // ... additional methods +} +``` + +Kotlin + +``` +class JdbcActorDao(dataSource: DataSource) : ActorDao { + + private val jdbcTemplate = JdbcTemplate(dataSource) + + fun batchUpdate(actors: List): Array { + return jdbcTemplate.batchUpdate( + "update t_actor set first_name = ?, last_name = ? where id = ?", + actors, 100) { ps, argument -> + ps.setString(1, argument.firstName) + ps.setString(2, argument.lastName) + ps.setLong(3, argument.id) + } + } + + // ... additional methods +} +``` + +此调用的批更新方法返回一个`int`数组的数组,其中包含每个批处理的数组条目,以及每个更新的受影响行数组。顶级数组的长度表示运行的批处理的数量,而二级数组的长度表示该批处理中的更新数量。每个批处理中的更新数量应该是为所有批处理提供的批处理大小(除了最后一个可能更小),这取决于所提供的更新对象的总数。每个更新语句的更新计数是 JDBC 驱动程序报告的。如果计数不可用,则 JDBC 驱动程序返回一个`-2`的值。 + +### 3.6.使用`SimpleJdbc`类简化 JDBC 操作 + +`SimpleJdbcInsert`和`SimpleJdbcCall`类利用可以通过 JDBC 驱动程序检索的数据库元数据,提供了一种简化的配置。这意味着你需要预先配置的内容较少,但是如果你希望在代码中提供所有细节,则可以重写或关闭元数据处理。 + +#### 3.6.1.使用`SimpleJdbcInsert`插入数据 + +我们首先查看具有最少配置选项的`isBatchExhausted`类。你应该在数据访问层的初始化方法中实例化`SimpleJdbcInsert`。对于这个示例,初始化方法是“setDataSource”方法。你不需要子类`SimpleJdbcInsert`类。相反,你可以使用`withTableName`方法创建一个新实例并设置表名。该类的配置方法遵循`SimpleJdbcInsert`样式,该样式返回`SimpleJdbcInsert`的实例,它允许你链接所有配置方法。下面的示例只使用一种配置方法(稍后我们将展示多个方法的示例): + +爪哇 + +``` +public class JdbcActorDao implements ActorDao { + + private SimpleJdbcInsert insertActor; + + public void setDataSource(DataSource dataSource) { + this.insertActor = new SimpleJdbcInsert(dataSource).withTableName("t_actor"); + } + + public void add(Actor actor) { + Map parameters = new HashMap(3); + parameters.put("id", actor.getId()); + parameters.put("first_name", actor.getFirstName()); + parameters.put("last_name", actor.getLastName()); + insertActor.execute(parameters); + } + + // ... additional methods +} +``` + +Kotlin + +``` +class JdbcActorDao(dataSource: DataSource) : ActorDao { + + private val insertActor = SimpleJdbcInsert(dataSource).withTableName("t_actor") + + fun add(actor: Actor) { + val parameters = mutableMapOf() + parameters["id"] = actor.id + parameters["first_name"] = actor.firstName + parameters["last_name"] = actor.lastName + insertActor.execute(parameters) + } + + // ... additional methods +} +``` + +这里使用的`execute`方法将一个普通的`java.util.Map`作为其唯一参数。这里需要注意的重要事项是,用于`Map`的键必须与数据库中定义的表的列名匹配。这是因为我们读取元数据来构造实际的 INSERT 语句。 + +#### 3.6.2.使用`java.util.Map`检索自动生成的密钥 + +下一个示例使用与前一个示例相同的 INSERT,但是,它检索自动生成的键并将其设置在新的`Actor`对象上,而不是传入`id`。当它创建`SimpleJdbcInsert`时,除了指定表名外,它还使用`usingGeneratedKeyColumns`方法指定生成的键列的名称。下面的清单展示了它的工作原理: + +爪哇 + +``` +public class JdbcActorDao implements ActorDao { + + private SimpleJdbcInsert insertActor; + + public void setDataSource(DataSource dataSource) { + this.insertActor = new SimpleJdbcInsert(dataSource) + .withTableName("t_actor") + .usingGeneratedKeyColumns("id"); + } + + public void add(Actor actor) { + Map parameters = new HashMap(2); + parameters.put("first_name", actor.getFirstName()); + parameters.put("last_name", actor.getLastName()); + Number newId = insertActor.executeAndReturnKey(parameters); + actor.setId(newId.longValue()); + } + + // ... additional methods +} +``` + +Kotlin + +``` +class JdbcActorDao(dataSource: DataSource) : ActorDao { + + private val insertActor = SimpleJdbcInsert(dataSource) + .withTableName("t_actor").usingGeneratedKeyColumns("id") + + fun add(actor: Actor): Actor { + val parameters = mapOf( + "first_name" to actor.firstName, + "last_name" to actor.lastName) + val newId = insertActor.executeAndReturnKey(parameters); + return actor.copy(id = newId.toLong()) + } + + // ... additional methods +} +``` + +在使用第二种方法运行 INSERT 时,主要的区别是你没有将`id`添加到`Map`中,而是调用`executeAndReturnKey`方法。这返回一个’java.lang.number’对象,你可以使用它创建域类中使用的数值类型的实例。在这里,你不能依赖所有的数据库来返回特定的 爪哇 类。`java.lang.Number`是你可以依赖的基类。如果有多个自动生成的列,或者生成的值是非数字的,则可以使用从`executeAndReturnKeyHolder`方法返回的`KeyHolder`。 + +#### 3.6.3.指定`SimpleJdbcInsert`的列 + +可以通过使用“usingcolumns”方法指定列名列表来限制插入的列,如下例所示: + +爪哇 + +``` +public class JdbcActorDao implements ActorDao { + + private SimpleJdbcInsert insertActor; + + public void setDataSource(DataSource dataSource) { + this.insertActor = new SimpleJdbcInsert(dataSource) + .withTableName("t_actor") + .usingColumns("first_name", "last_name") + .usingGeneratedKeyColumns("id"); + } + + public void add(Actor actor) { + Map parameters = new HashMap(2); + parameters.put("first_name", actor.getFirstName()); + parameters.put("last_name", actor.getLastName()); + Number newId = insertActor.executeAndReturnKey(parameters); + actor.setId(newId.longValue()); + } + + // ... additional methods +} +``` + +Kotlin + +``` +class JdbcActorDao(dataSource: DataSource) : ActorDao { + + private val insertActor = SimpleJdbcInsert(dataSource) + .withTableName("t_actor") + .usingColumns("first_name", "last_name") + .usingGeneratedKeyColumns("id") + + fun add(actor: Actor): Actor { + val parameters = mapOf( + "first_name" to actor.firstName, + "last_name" to actor.lastName) + val newId = insertActor.executeAndReturnKey(parameters); + return actor.copy(id = newId.toLong()) + } + + // ... additional methods +} +``` + +INSERT 的执行与依赖元数据来确定要使用哪些列的情况相同。 + +#### 3.6.4.使用`SqlParameterSource`提供参数值 + +使用`Map`提供参数值可以很好地工作,但它不是使用最方便的类。 Spring 提供了[Understanding `SqlQuery`](#jdbc-SqlQuery)接口的两个实现方式,你可以使用这些实现方式来代替。第一个是`BeanPropertySqlParameterSource`,如果你有一个包含你的值的符合 爪哇Bean 的类,那么它是一个非常方便的类。它采用相应的 getter 方法提取参数值。下面的示例展示了如何使用`BeanPropertySqlParameterSource`: + +爪哇 + +``` +public class JdbcActorDao implements ActorDao { + + private SimpleJdbcInsert insertActor; + + public void setDataSource(DataSource dataSource) { + this.insertActor = new SimpleJdbcInsert(dataSource) + .withTableName("t_actor") + .usingGeneratedKeyColumns("id"); + } + + public void add(Actor actor) { + SqlParameterSource parameters = new BeanPropertySqlParameterSource(actor); + Number newId = insertActor.executeAndReturnKey(parameters); + actor.setId(newId.longValue()); + } + + // ... additional methods +} +``` + +Kotlin + +``` +class JdbcActorDao(dataSource: DataSource) : ActorDao { + + private val insertActor = SimpleJdbcInsert(dataSource) + .withTableName("t_actor") + .usingGeneratedKeyColumns("id") + + fun add(actor: Actor): Actor { + val parameters = BeanPropertySqlParameterSource(actor) + val newId = insertActor.executeAndReturnKey(parameters) + return actor.copy(id = newId.toLong()) + } + + // ... additional methods +} +``` + +另一个选项是`MapSqlParameterSource`,它类似于`Map`,但提供了一个可以链接的更方便的`addValue`方法。下面的示例展示了如何使用它: + +爪哇 + +``` +public class JdbcActorDao implements ActorDao { + + private SimpleJdbcInsert insertActor; + + public void setDataSource(DataSource dataSource) { + this.insertActor = new SimpleJdbcInsert(dataSource) + .withTableName("t_actor") + .usingGeneratedKeyColumns("id"); + } + + public void add(Actor actor) { + SqlParameterSource parameters = new MapSqlParameterSource() + .addValue("first_name", actor.getFirstName()) + .addValue("last_name", actor.getLastName()); + Number newId = insertActor.executeAndReturnKey(parameters); + actor.setId(newId.longValue()); + } + + // ... additional methods +} +``` + +Kotlin + +``` +class JdbcActorDao(dataSource: DataSource) : ActorDao { + + private val insertActor = SimpleJdbcInsert(dataSource) + .withTableName("t_actor") + .usingGeneratedKeyColumns("id") + + fun add(actor: Actor): Actor { + val parameters = MapSqlParameterSource() + .addValue("first_name", actor.firstName) + .addValue("last_name", actor.lastName) + val newId = insertActor.executeAndReturnKey(parameters) + return actor.copy(id = newId.toLong()) + } + + // ... additional methods +} +``` + +正如你所看到的,配置是相同的。只有执行代码必须更改才能使用这些可选的输入类。 + +#### 3.6.5.用`SimpleJdbcCall`调用存储过程 + +`SimpleJdbcCall`类使用数据库中的元数据查找`in`和`in`参数的名称,这样你就不必显式地声明它们。如果你愿意这样做,或者如果你的参数(例如`ARRAY`或`STRUCT`)没有自动映射到 爪哇 类,则可以声明参数。第一个示例显示了一个简单的过程,该过程仅从 MySQL 数据库返回`VARCHAR`和`DATE`格式中的标量值。示例过程读取指定的参与者条目,并以`SqlQuery`参数的形式返回 `first_name’、`last_name`和`birth_date`列。下面的清单展示了第一个示例: + +``` +CREATE PROCEDURE read_actor ( + IN in_id INTEGER, + OUT out_first_name VARCHAR(100), + OUT out_last_name VARCHAR(100), + OUT out_birth_date DATE) +BEGIN + SELECT first_name, last_name, birth_date + INTO out_first_name, out_last_name, out_birth_date + FROM t_actor where id = in_id; +END; +``` + +参数`in_id`包含你正在查找的参与者的`id`。`out`参数返回从表中读取的数据。 + +你可以以类似于声明`SimpleJdbcCall`的方式声明`SimpleJdbcCall`。你应该在数据访问层的初始化方法中实例化和配置类。与`StoredProcedure`类相比,不需要创建子类,也不需要声明可以在数据库元数据中查找的参数。下面的`SimpleJdbcCall`配置示例使用了前面的存储过程(除了`DataSource`之外,唯一的配置选项是存储过程的名称): + +爪哇 + +``` +public class JdbcActorDao implements ActorDao { + + private SimpleJdbcCall procReadActor; + + public void setDataSource(DataSource dataSource) { + this.procReadActor = new SimpleJdbcCall(dataSource) + .withProcedureName("read_actor"); + } + + public Actor readActor(Long id) { + SqlParameterSource in = new MapSqlParameterSource() + .addValue("in_id", id); + Map out = procReadActor.execute(in); + Actor actor = new Actor(); + actor.setId(id); + actor.setFirstName((String) out.get("out_first_name")); + actor.setLastName((String) out.get("out_last_name")); + actor.setBirthDate((Date) out.get("out_birth_date")); + return actor; + } + + // ... additional methods +} +``` + +Kotlin + +``` +class JdbcActorDao(dataSource: DataSource) : ActorDao { + + private val procReadActor = SimpleJdbcCall(dataSource) + .withProcedureName("read_actor") + + fun readActor(id: Long): Actor { + val source = MapSqlParameterSource().addValue("in_id", id) + val output = procReadActor.execute(source) + return Actor( + id, + output["out_first_name"] as String, + output["out_last_name"] as String, + output["out_birth_date"] as Date) + } + + // ... additional methods +} +``` + +你为执行调用编写的代码涉及创建一个包含 IN 参数的`SqlParameterSource`。你必须将为输入值提供的名称与存储过程中声明的参数名称的名称匹配。这种情况不一定要匹配,因为你使用元数据来确定在存储过程中应该如何引用数据库对象。源文件中为存储过程指定的内容不一定是存储在数据库中的方式。一些数据库将名称转换为所有大写,而另一些数据库则使用小写或使用指定的大小写。 + +`execute`方法接受 IN 参数并返回一个`Map`,该参数包含由名称键入的任何`out`参数,如在存储过程中指定的那样。在这种情况下,它们是 `out_first_name`,`out_last_name`和`out_birth_date`。 + +`execute`方法的最后一部分将创建一个`Actor`实例,用于返回检索到的数据。同样,使用在存储过程中声明的`out`参数的名称也很重要。而且,存储在结果映射中的`out`参数的名称与数据库中的`out`参数名称的情况匹配,这在不同的数据库中可能会有所不同。为了使代码更具可移植性,你应该进行不区分大小写的查找,或者指示 Spring 使用`LinkedCaseInsensitiveMap`。要实现后者,你可以创建自己的`JdbcTemplate`,并将`setResultsMapCaseInsensitive`属性设置为`true`。然后,你可以将这个定制的`JdbcTemplate`实例传递到你的`SimpleJdbcCall`的构造函数中。下面的示例展示了这种配置: + +爪哇 + +``` +public class JdbcActorDao implements ActorDao { + + private SimpleJdbcCall procReadActor; + + public void setDataSource(DataSource dataSource) { + JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); + jdbcTemplate.setResultsMapCaseInsensitive(true); + this.procReadActor = new SimpleJdbcCall(jdbcTemplate) + .withProcedureName("read_actor"); + } + + // ... additional methods +} +``` + +Kotlin + +``` +class JdbcActorDao(dataSource: DataSource) : ActorDao { + + private var procReadActor = SimpleJdbcCall(JdbcTemplate(dataSource).apply { + isResultsMapCaseInsensitive = true + }).withProcedureName("read_actor") + + // ... additional methods +} +``` + +通过执行此操作,可以避免在用于返回的`out`参数的名称的情况下发生冲突。 + +#### 3.6.6.显式声明用于`SimpleJdbcCall`的参数 + +在本章的前面,我们描述了参数是如何从元数据推导出来的,但是如果你愿意,你可以显式地声明它们。你可以通过使用`declareParameters`方法创建和配置`SimpleJdbcCall`来实现这一点,该方法接受数量可变的`SqlParameter`对象作为输入。有关如何定义`SqlParameter`的详细信息,请参见[next section](#jdbc-params)。 + +| |如果你使用的数据库不是 Spring 支持的
数据库,则需要显式声明。目前, Spring 支持
以下数据库的存储过程调用的元数据查找:Apache Derby、DB2、MySQL、Microsoft SQL Server、Oracle 和 Sybase。
我们还支持 MySQL、Microsoft SQL Server、
和 Oracle 的存储函数的元数据查找。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +你可以 OPT 显式地声明一个、一些或所有参数。在未显式声明参数的情况下,仍使用参数元数据。要绕过对潜在参数的元数据查找的所有处理,只使用声明的参数,可以调用方法`withoutProcedureColumnMetaDataAccess`作为声明的一部分。假设你为一个数据库函数声明了两个或多个不同的调用签名。在这种情况下,调用`useInParameterNames`来指定给定签名要包含的 IN 参数名列表。 + +下面的示例展示了一个完全声明的过程调用,并使用了前面示例中的信息: + +爪哇 + +``` +public class JdbcActorDao implements ActorDao { + + private SimpleJdbcCall procReadActor; + + public void setDataSource(DataSource dataSource) { + JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); + jdbcTemplate.setResultsMapCaseInsensitive(true); + this.procReadActor = new SimpleJdbcCall(jdbcTemplate) + .withProcedureName("read_actor") + .withoutProcedureColumnMetaDataAccess() + .useInParameterNames("in_id") + .declareParameters( + new SqlParameter("in_id", Types.NUMERIC), + new SqlOutParameter("out_first_name", Types.VARCHAR), + new SqlOutParameter("out_last_name", Types.VARCHAR), + new SqlOutParameter("out_birth_date", Types.DATE) + ); + } + + // ... additional methods +} +``` + +Kotlin + +``` +class JdbcActorDao(dataSource: DataSource) : ActorDao { + + private val procReadActor = SimpleJdbcCall(JdbcTemplate(dataSource).apply { + isResultsMapCaseInsensitive = true + }).withProcedureName("read_actor") + .withoutProcedureColumnMetaDataAccess() + .useInParameterNames("in_id") + .declareParameters( + SqlParameter("in_id", Types.NUMERIC), + SqlOutParameter("out_first_name", Types.VARCHAR), + SqlOutParameter("out_last_name", Types.VARCHAR), + SqlOutParameter("out_birth_date", Types.DATE) + ) + + // ... additional methods +} +``` + +这两个示例的执行和最终结果是相同的。第二个示例显式地指定了所有细节,而不是依赖于元数据。 + +#### 3.6.7.如何定义`SqlParameters` + +要为`SimpleJdbc`类和 RDBMS 操作类(在[将 JDBC 操作建模为 爪哇 对象](#jdbc-object)中涉及)定义参数,你可以使用`SqlParameter`或它的一个子类。为此,你通常在构造函数中指定参数名和 SQL 类型。SQL 类型是通过使用`java.sql.Types`常量指定的。在本章的前面,我们看到了类似的声明: + +爪哇 + +``` +new SqlParameter("in_id", Types.NUMERIC), +new SqlOutParameter("out_first_name", Types.VARCHAR), +``` + +Kotlin + +``` +SqlParameter("in_id", Types.NUMERIC), +SqlOutParameter("out_first_name", Types.VARCHAR), +``` + +带有`SqlParameter`的第一行声明了一个 IN 参数。通过使用`SqlQuery`及其子类(在[Understanding `SqlQuery`](#jdbc-SqlQuery)中涵盖),可以在参数中用于存储过程调用和查询。 + +第二行(带有`SqlOutParameter`)声明一个`out`参数,该参数将在存储过程调用中使用。对于`InOut`参数也有`SqlInOutParameter`(为过程提供 in 值并返回值的参数)。 + +| |只有声明为`SqlParameter`和`SqlInOutParameter`的参数用于
提供输入值。这与`StoredProcedure`类不同,后者(出于
向后兼容性的原因)允许为参数
提供输入值,声明为`SqlOutParameter`。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +对于 IN 参数,除了名称和 SQL 类型之外,你还可以为数字数据指定一个比例,或者为自定义数据库类型指定一个类型名称。对于`out`参数,可以提供`RowMapper`来处理从`REF`游标返回的行的映射。另一种选择是指定`SqlReturnType`,它提供了定义对返回值的定制处理的机会。 + +#### 3.6.8.使用`SimpleJdbcCall`调用存储函数 + +你可以以调用存储过程的几乎相同的方式调用存储函数,只是提供了一个函数名而不是过程名。使用 `WithFunctionName’方法作为配置的一部分,以指示你要对函数进行调用,并生成函数调用的相应字符串。一个专门的调用(“executefunction”)用于运行函数,它将返回函数的返回值作为指定类型的对象,这意味着你不必从结果映射中检索返回值。对于只有一个`out`参数的存储过程,也可以使用类似的方便方法(名为`executeObject`)。下面的示例(对于 MySQL)基于一个名为`get_actor_name`的存储函数,该函数返回参与者的全名: + +``` +CREATE FUNCTION get_actor_name (in_id INTEGER) +RETURNS VARCHAR(200) READS SQL DATA +BEGIN + DECLARE out_name VARCHAR(200); + SELECT concat(first_name, ' ', last_name) + INTO out_name + FROM t_actor where id = in_id; + RETURN out_name; +END; +``` + +要调用这个函数,我们再次在初始化方法中创建`SimpleJdbcCall`,如下例所示: + +爪哇 + +``` +public class JdbcActorDao implements ActorDao { + + private SimpleJdbcCall funcGetActorName; + + public void setDataSource(DataSource dataSource) { + JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); + jdbcTemplate.setResultsMapCaseInsensitive(true); + this.funcGetActorName = new SimpleJdbcCall(jdbcTemplate) + .withFunctionName("get_actor_name"); + } + + public String getActorName(Long id) { + SqlParameterSource in = new MapSqlParameterSource() + .addValue("in_id", id); + String name = funcGetActorName.executeFunction(String.class, in); + return name; + } + + // ... additional methods +} +``` + +Kotlin + +``` +class JdbcActorDao(dataSource: DataSource) : ActorDao { + + private val jdbcTemplate = JdbcTemplate(dataSource).apply { + isResultsMapCaseInsensitive = true + } + private val funcGetActorName = SimpleJdbcCall(jdbcTemplate) + .withFunctionName("get_actor_name") + + fun getActorName(id: Long): String { + val source = MapSqlParameterSource().addValue("in_id", id) + return funcGetActorName.executeFunction(String::class.java, source) + } + + // ... additional methods +} +``` + +所使用的`executeFunction`方法返回一个`SimpleJdbcCall`,它包含函数调用的返回值。 + +#### 3.6.9.从`SimpleJdbcCall`返回`ResultSet`或 ref 光标 + +调用返回结果集的存储过程或函数有点困难。一些数据库在 JDBC 结果处理过程中返回结果集,而另一些数据库则需要显式注册特定类型的`out`参数。这两种方法都需要额外的处理来循环结果集并处理返回的行。使用`SimpleJdbcCall`,你可以使用`returningResultSet`方法并声明用于特定参数的`SimpleJdbcCall`实现。如果在结果处理过程中返回了结果集,则没有定义名称,因此返回的结果必须与声明`RowMapper`实现的顺序匹配。指定的名称仍用于将已处理的结果列表存储在从`execute`语句返回的结果映射中。 + +下一个示例(对于 MySQL)使用一个存储过程,该过程接受 no in 参数并返回`t_actor`表中的所有行: + +``` +CREATE PROCEDURE read_all_actors() +BEGIN + SELECT a.id, a.first_name, a.last_name, a.birth_date FROM t_actor a; +END; +``` + +要调用这个过程,你可以声明`RowMapper`。因为要映射到的类遵循 爪哇Bean 规则,所以可以使用`BeanPropertyRowMapper`,它是通过在`newInstance`方法中传递要映射到的所需类来创建的。下面的示例展示了如何做到这一点: + +爪哇 + +``` +public class JdbcActorDao implements ActorDao { + + private SimpleJdbcCall procReadAllActors; + + public void setDataSource(DataSource dataSource) { + JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); + jdbcTemplate.setResultsMapCaseInsensitive(true); + this.procReadAllActors = new SimpleJdbcCall(jdbcTemplate) + .withProcedureName("read_all_actors") + .returningResultSet("actors", + BeanPropertyRowMapper.newInstance(Actor.class)); + } + + public List getActorsList() { + Map m = procReadAllActors.execute(new HashMap(0)); + return (List) m.get("actors"); + } + + // ... additional methods +} +``` + +Kotlin + +``` +class JdbcActorDao(dataSource: DataSource) : ActorDao { + + private val procReadAllActors = SimpleJdbcCall(JdbcTemplate(dataSource).apply { + isResultsMapCaseInsensitive = true + }).withProcedureName("read_all_actors") + .returningResultSet("actors", + BeanPropertyRowMapper.newInstance(Actor::class.java)) + + fun getActorsList(): List { + val m = procReadAllActors.execute(mapOf()) + return m["actors"] as List + } + + // ... additional methods +} +``` + +`execute`调用传入一个空的`Map`,因为这个调用不接受任何参数。然后从结果映射中检索参与者列表,并将其返回给调用者。 + +### 3.7.将 JDBC 操作建模为 爪哇 对象 + +`org.springframework.jdbc.object`包包含允许你以更面向对象的方式访问数据库的类。例如,你可以运行查询并以列表的形式获得结果,该列表包含业务对象,并将关系列数据映射到业务对象的属性。你还可以运行存储过程和运行更新、删除和插入语句。 + +| |Spring 许多开发人员认为,下面描述的各种 RDBMS 操作类(类除外)通常可以将替换为直接的调用。通常,编写一个直接在`JdbcTemplate`上调用方法的 DAO
方法更简单(而不是
将查询封装为一个成熟的类)。
但是,如果你要从使用 RDBMS 操作类中获得可测量的值,
你应该继续使用这些类。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 3.7.1.理解`SqlQuery` + +`SqlQuery`是一个可重用的、线程安全的类,它封装了一个 SQL 查询。子类必须实现`newRowMapper(..)`方法,以提供一个
实例,该实例可以通过在执行查询期间创建的`ResultSet`上进行迭代,为每行创建一个对象。很少直接使用`SqlQuery`类,因为`MappingSqlQuery`子类为将行映射到 Java 类提供了一个更方便的实现。扩展`SqlQuery`的其他实现是 `mappingsqlquerywithparameters’和`UpdatableSqlQuery`。 + +#### 3.7.2.使用`MappingSqlQuery` + +`MappingSqlQuery`是一个可重用的查询,在该查询中,具体的子类必须实现抽象`mapRow(..)`方法,以将所提供的`ResultSet`中的每一行转换为指定类型的对象。下面的示例显示了一个自定义查询,该查询将来自`t_actor`关系的数据映射到`getClobAsCharacterStream`类的实例: + +Java + +``` +public class ActorMappingQuery extends MappingSqlQuery { + + public ActorMappingQuery(DataSource ds) { + super(ds, "select id, first_name, last_name from t_actor where id = ?"); + declareParameter(new SqlParameter("id", Types.INTEGER)); + compile(); + } + + @Override + protected Actor mapRow(ResultSet rs, int rowNumber) throws SQLException { + Actor actor = new Actor(); + actor.setId(rs.getLong("id")); + actor.setFirstName(rs.getString("first_name")); + actor.setLastName(rs.getString("last_name")); + return actor; + } +} +``` + +Kotlin + +``` +class ActorMappingQuery(ds: DataSource) : MappingSqlQuery(ds, "select id, first_name, last_name from t_actor where id = ?") { + + init { + declareParameter(SqlParameter("id", Types.INTEGER)) + compile() + } + + override fun mapRow(rs: ResultSet, rowNumber: Int) = Actor( + rs.getLong("id"), + rs.getString("first_name"), + rs.getString("last_name") + ) +} +``` + +该类扩展了`MappingSqlQuery`参数化的`Actor`类型。此客户查询的构造函数将`DataSource`作为唯一的参数。在此构造函数中,你可以使用`DataSource`调用超类上的构造函数,并调用应该运行的 SQL 来检索此查询的行。此 SQL 用于创建`PreparedStatement`,因此它可能包含用于在执行过程中传递的任何参数的占位符。你必须使用`declareParameter`方法声明每个参数,并传入`SqlParameter`。`SqlParameter`接受一个名称和在`java.sql.Types`中定义的 JDBC 类型。在定义了所有参数之后,你可以调用“compile()”方法,这样就可以准备语句并在以后运行它。这个类在编译后是线程安全的,因此,只要在初始化 DAO 时创建了这些实例,它们就可以保留为实例变量并被重用。下面的示例展示了如何定义这样的类: + +Java + +``` +private ActorMappingQuery actorMappingQuery; + +@Autowired +public void setDataSource(DataSource dataSource) { + this.actorMappingQuery = new ActorMappingQuery(dataSource); +} + +public Customer getCustomer(Long id) { + return actorMappingQuery.findObject(id); +} +``` + +Kotlin + +``` +private val actorMappingQuery = ActorMappingQuery(dataSource) + +fun getCustomer(id: Long) = actorMappingQuery.findObject(id) +``` + +前面示例中的方法使用`id`检索作为唯一参数传入的客户。由于我们只希望返回一个对象,所以我们调用`findObject`便利方法,并以`id`作为参数。如果我们有一个返回对象列表并获取附加参数的查询,那么我们将使用`execute`方法中的一个,该方法接受以 vargs 形式传递的参数值数组。下面的示例展示了这样的方法: + +Java + +``` +public List searchForActors(int age, String namePattern) { + List actors = actorSearchMappingQuery.execute(age, namePattern); + return actors; +} +``` + +Kotlin + +``` +fun searchForActors(age: Int, namePattern: String) = + actorSearchMappingQuery.execute(age, namePattern) +``` + +#### 3.7.3.使用`SqlUpdate` + +`SqlUpdate`类封装了一个 SQL 更新。与查询一样,更新对象是可重用的,并且,与所有`RdbmsOperation`类一样,更新可以有参数,并在 SQL 中定义。这个类提供了许多`out`方法,类似于查询对象的 `execute’方法。`SqlUpdate`类是具体的。它可以被子类——例如,添加一个自定义更新方法。但是,你不必对`SqlUpdate`类进行子类,因为可以通过设置 SQL 和声明参数来轻松地对它进行参数化。下面的示例创建了一个名为`execute`的自定义更新方法: + +Java + +``` +import java.sql.Types; +import javax.sql.DataSource; +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.jdbc.object.SqlUpdate; + +public class UpdateCreditRating extends SqlUpdate { + + public UpdateCreditRating(DataSource ds) { + setDataSource(ds); + setSql("update customer set credit_rating = ? where id = ?"); + declareParameter(new SqlParameter("creditRating", Types.NUMERIC)); + declareParameter(new SqlParameter("id", Types.NUMERIC)); + compile(); + } + + /** + * @param id for the Customer to be updated + * @param rating the new value for credit rating + * @return number of rows updated + */ + public int execute(int id, int rating) { + return update(rating, id); + } +} +``` + +Kotlin + +``` +import java.sql.Types +import javax.sql.DataSource +import org.springframework.jdbc.core.SqlParameter +import org.springframework.jdbc.object.SqlUpdate + +class UpdateCreditRating(ds: DataSource) : SqlUpdate() { + + init { + setDataSource(ds) + sql = "update customer set credit_rating = ? where id = ?" + declareParameter(SqlParameter("creditRating", Types.NUMERIC)) + declareParameter(SqlParameter("id", Types.NUMERIC)) + compile() + } + + /** + * @param id for the Customer to be updated + * @param rating the new value for credit rating + * @return number of rows updated + */ + fun execute(id: Int, rating: Int): Int { + return update(rating, id) + } +} +``` + +#### 3.7.4.使用`StoredProcedure` + +`StoredProcedure`类是用于 RDBMS 存储过程的对象抽象的`abstract`超类。 + +继承的`sql`属性是 RDBMS 中存储过程的名称。 + +要为`StoredProcedure`类定义参数,可以使用`SqlParameter`或它的一个子类。你必须在构造函数中指定参数名和 SQL 类型,如下列代码片段所示: + +Java + +``` +new SqlParameter("in_id", Types.NUMERIC), +new SqlOutParameter("out_first_name", Types.VARCHAR), +``` + +Kotlin + +``` +SqlParameter("in_id", Types.NUMERIC), +SqlOutParameter("out_first_name", Types.VARCHAR), +``` + +SQL 类型是使用`java.sql.Types`常量指定的。 + +第一行(带有`SqlParameter`)声明一个 IN 参数。你可以在参数中使用`SqlQuery`及其子类的存储过程调用和查询(在[Understanding `SqlQuery`](#jdbc-SqlQuery)中涵盖)。 + +第二行(带有`SqlOutParameter`)声明一个`out`参数,该参数将在存储过程调用中使用。对于`InOut`参数也有`SqlInOutParameter`(这些参数为过程提供了`in`值,并且还返回了一个值)。 + +对于`in`参数,除了名称和 SQL 类型之外,还可以为数字数据指定一个比例,或者为自定义数据库类型指定一个类型名称。对于`out`参数,可以提供`RowMapper`来处理从`REF`游标返回的行的映射。另一个选项是指定`SqlReturnType`,它允许你定义对返回值的定制处理。 + +简单 DAO 的下一个示例使用`StoredProcedure`调用函数 `),该函数随任何 Oracle 数据库一起提供。要使用存储过程功能,你必须创建一个扩展`StoredProcedure`的类。在本例中,`StoredProcedure`类是一个内部类。但是,如果需要重用“storedprocedure”,则可以将其声明为顶级类。这个示例没有输入参数,但是通过使用“sqloutParameter”类将输出参数声明为日期类型。`execute()`方法运行过程,并从结果`Map`中提取返回的日期。通过使用参数名称作为键,结果`Map`为每个声明的输出参数(在本例中只有一个)有一个条目。下面的清单显示了我们的自定义存储过程类: + +Java + +``` +import java.sql.Types; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import javax.sql.DataSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.SqlOutParameter; +import org.springframework.jdbc.object.StoredProcedure; + +public class StoredProcedureDao { + + private GetSysdateProcedure getSysdate; + + @Autowired + public void init(DataSource dataSource) { + this.getSysdate = new GetSysdateProcedure(dataSource); + } + + public Date getSysdate() { + return getSysdate.execute(); + } + + private class GetSysdateProcedure extends StoredProcedure { + + private static final String SQL = "sysdate"; + + public GetSysdateProcedure(DataSource dataSource) { + setDataSource(dataSource); + setFunction(true); + setSql(SQL); + declareParameter(new SqlOutParameter("date", Types.DATE)); + compile(); + } + + public Date execute() { + // the 'sysdate' sproc has no input parameters, so an empty Map is supplied... + Map results = execute(new HashMap()); + Date sysdate = (Date) results.get("date"); + return sysdate; + } + } + +} +``` + +Kotlin + +``` +import java.sql.Types +import java.util.Date +import java.util.Map +import javax.sql.DataSource +import org.springframework.jdbc.core.SqlOutParameter +import org.springframework.jdbc.object.StoredProcedure + +class StoredProcedureDao(dataSource: DataSource) { + + private val SQL = "sysdate" + + private val getSysdate = GetSysdateProcedure(dataSource) + + val sysdate: Date + get() = getSysdate.execute() + + private inner class GetSysdateProcedure(dataSource: DataSource) : StoredProcedure() { + + init { + setDataSource(dataSource) + isFunction = true + sql = SQL + declareParameter(SqlOutParameter("date", Types.DATE)) + compile() + } + + fun execute(): Date { + // the 'sysdate' sproc has no input parameters, so an empty Map is supplied... + val results = execute(mutableMapOf()) + return results["date"] as Date + } + } +} +``` + +下面的`StoredProcedure`示例有两个输出参数(在本例中是 Oracle Ref 游标): + +Java + +``` +import java.util.HashMap; +import java.util.Map; +import javax.sql.DataSource; +import oracle.jdbc.OracleTypes; +import org.springframework.jdbc.core.SqlOutParameter; +import org.springframework.jdbc.object.StoredProcedure; + +public class TitlesAndGenresStoredProcedure extends StoredProcedure { + + private static final String SPROC_NAME = "AllTitlesAndGenres"; + + public TitlesAndGenresStoredProcedure(DataSource dataSource) { + super(dataSource, SPROC_NAME); + declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper())); + declareParameter(new SqlOutParameter("genres", OracleTypes.CURSOR, new GenreMapper())); + compile(); + } + + public Map execute() { + // again, this sproc has no input parameters, so an empty Map is supplied + return super.execute(new HashMap()); + } +} +``` + +Kotlin + +``` +import java.util.HashMap +import javax.sql.DataSource +import oracle.jdbc.OracleTypes +import org.springframework.jdbc.core.SqlOutParameter +import org.springframework.jdbc.object.StoredProcedure + +class TitlesAndGenresStoredProcedure(dataSource: DataSource) : StoredProcedure(dataSource, SPROC_NAME) { + + companion object { + private const val SPROC_NAME = "AllTitlesAndGenres" + } + + init { + declareParameter(SqlOutParameter("titles", OracleTypes.CURSOR, TitleMapper())) + declareParameter(SqlOutParameter("genres", OracleTypes.CURSOR, GenreMapper())) + compile() + } + + fun execute(): Map { + // again, this sproc has no input parameters, so an empty Map is supplied + return super.execute(HashMap()) + } +} +``` + +请注意,在`TitlesAndGenresStoredProcedure`构造函数中使用的`declareParameter(..)`方法的重载变量是如何传递`RowMapper`实现实例的。这是一种非常方便且功能强大的重用现有功能的方法。接下来的两个示例提供了两个`RowMapper`实现的代码。 + +对于所提供的`ResultSet`中的每一行,`TitleMapper`类将`ResultSet`映射到`Title`域对象,如下所示: + +Java + +``` +import java.sql.ResultSet; +import java.sql.SQLException; +import com.foo.domain.Title; +import org.springframework.jdbc.core.RowMapper; + +public final class TitleMapper implements RowMapper { + + public Title mapRow(ResultSet rs, int rowNum) throws SQLException { + Title title = new Title(); + title.setId(rs.getLong("id")); + title.setName(rs.getString("name")); + return title; + } +} +``` + +Kotlin + +``` +import java.sql.ResultSet +import com.foo.domain.Title +import org.springframework.jdbc.core.RowMapper + +class TitleMapper : RowMapper<Title> { + + override fun mapRow(rs: ResultSet, rowNum: Int) = + Title(rs.getLong("id"), rs.getString("name")) +} +``` + +对于所提供的`ResultSet`中的每一行,`SqlParameterValue`类将`ResultSet`映射到`Genre`域对象,如下所示: + +Java + +``` +import java.sql.ResultSet; +import java.sql.SQLException; +import com.foo.domain.Genre; +import org.springframework.jdbc.core.RowMapper; + +public final class GenreMapper implements RowMapper<Genre> { + + public Genre mapRow(ResultSet rs, int rowNum) throws SQLException { + return new Genre(rs.getString("name")); + } +} +``` + +Kotlin + +``` +import java.sql.ResultSet +import com.foo.domain.Genre +import org.springframework.jdbc.core.RowMapper + +class GenreMapper : RowMapper<Genre> { + + override fun mapRow(rs: ResultSet, rowNum: Int): Genre { + return Genre(rs.getString("name")) + } +} +``` + +要将参数传递给在 RDBMS 中的定义中具有一个或多个输入参数的存储过程,可以对强类型`execute(..)`方法进行编码,该方法将委托给超类中的非类型`execute(Map)`方法,如下例所示: + +Java + +``` +import java.sql.Types; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import javax.sql.DataSource; +import oracle.jdbc.OracleTypes; +import org.springframework.jdbc.core.SqlOutParameter; +import org.springframework.jdbc.core.SqlParameter; +import org.springframework.jdbc.object.StoredProcedure; + +public class TitlesAfterDateStoredProcedure extends StoredProcedure { + + private static final String SPROC_NAME = "TitlesAfterDate"; + private static final String CUTOFF_DATE_PARAM = "cutoffDate"; + + public TitlesAfterDateStoredProcedure(DataSource dataSource) { + super(dataSource, SPROC_NAME); + declareParameter(new SqlParameter(CUTOFF_DATE_PARAM, Types.DATE); + declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper())); + compile(); + } + + public Map<String, Object> execute(Date cutoffDate) { + Map<String, Object> inputs = new HashMap<String, Object>(); + inputs.put(CUTOFF_DATE_PARAM, cutoffDate); + return super.execute(inputs); + } +} +``` + +Kotlin + +``` +import java.sql.Types +import java.util.Date +import javax.sql.DataSource +import oracle.jdbc.OracleTypes +import org.springframework.jdbc.core.SqlOutParameter +import org.springframework.jdbc.core.SqlParameter +import org.springframework.jdbc.object.StoredProcedure + +class TitlesAfterDateStoredProcedure(dataSource: DataSource) : StoredProcedure(dataSource, SPROC_NAME) { + + companion object { + private const val SPROC_NAME = "TitlesAfterDate" + private const val CUTOFF_DATE_PARAM = "cutoffDate" + } + + init { + declareParameter(SqlParameter(CUTOFF_DATE_PARAM, Types.DATE)) + declareParameter(SqlOutParameter("titles", OracleTypes.CURSOR, TitleMapper())) + compile() + } + + fun execute(cutoffDate: Date) = super.execute( + mapOf<String, Any>(CUTOFF_DATE_PARAM to cutoffDate)) +} +``` + +### 3.8.参数和数据值处理的常见问题 + +Spring Framework 的 JDBC 支持所提供的不同方法中存在参数和数据值的常见问题。本节介绍如何解决这些问题。 + +#### 3.8.1.为参数提供 SQL 类型信息 + +通常, Spring 根据传入的参数的类型来确定参数的 SQL 类型。在设置参数值时,可以显式地提供要使用的 SQL 类型。这有时是正确设置`NULL`值所必需的。 + +你可以通过以下几种方式提供 SQL 类型信息: + +* `JdbcTemplate`的许多更新和查询方法以`int`数组的形式接受一个额外的参数。此数组用于通过使用来自`java.sql.Types`类的常量值来指示相应参数的 SQL 类型。为每个参数提供一个条目。 + +* 你可以使用`SqlParameterValue`类来包装需要此附加信息的参数值。为此,为每个值创建一个新实例,并在构造函数中传入 SQL 类型和参数值。你还可以为数值提供一个可选的缩放参数。 + +* 对于使用命名参数的方法,可以使用`SqlParameterSource`类,`BeanPropertySQLParameterSource’或`getBlobAsBytes`。它们都有为任何命名参数值注册 SQL 类型的方法。 + +#### 3.8.2.处理 BLOB 和 CLOB 对象 + +你可以在数据库中存储图像、其他二进制数据和大量文本。这些大对象被称为 BLOBS(二进制大对象),用于二进制数据,而 CLOBS(字符大对象),用于字符数据。在 Spring 中,可以通过直接使用`JdbcTemplate`处理这些大型对象,也可以在使用 RDBMS 对象和`SimpleJdbc`类提供的更高抽象时处理这些对象。所有这些方法都使用`LobHandler`接口的实现,用于实际管理 LOB(大对象)数据。`LOB 处理程序 ` 通过`LobCreator`方法提供对`LobCreator`类的访问,即用于创建要插入的新的 LOB 对象。 + +`LobCreator`和`LobHandler`为 LOB 输入和输出提供以下支持: + +* BLOB + + * `byte[]`:`getBlobAsBytes`和`setBlobAsBytes` + + * `InputStream`:`getBlobAsBinaryStream`和`setBlobAsBinaryStream` + +* CLOB + + * `String`:`getClobAsString`和`setClobAsString` + + * `InputStream`:`getClobAsAsciiStream`和`setClobAsAsciiStream` + + * `Reader`:`getClobAsCharacterStream`和`setClobAsCharacterStream` + +下一个示例展示了如何创建和插入 BLOB。稍后,我们将展示如何从数据库中读回它。 + +这个示例使用`JdbcTemplate`和 `AbstractLobcreatingPreparedStatementCallback’的实现。它实现了一个方法“setvalues”。这个方法提供了`LobCreator`,我们使用它来设置 SQL INSERT 语句中 LOB 列的值。 + +对于这个示例,我们假设有一个变量`lobHandler`,它已经被设置为`DefaultLobHandler`的实例。你通常通过依赖项注入来设置该值。 + +下面的示例展示了如何创建和插入 BLOB: + +Java + +``` +final File blobIn = new File("spring2004.jpg"); +final InputStream blobIs = new FileInputStream(blobIn); +final File clobIn = new File("large.txt"); +final InputStream clobIs = new FileInputStream(clobIn); +final InputStreamReader clobReader = new InputStreamReader(clobIs); + +jdbcTemplate.execute( + "INSERT INTO lob_table (id, a_clob, a_blob) VALUES (?, ?, ?)", + new AbstractLobCreatingPreparedStatementCallback(lobHandler) { (1) + protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException { + ps.setLong(1, 1L); + lobCreator.setClobAsCharacterStream(ps, 2, clobReader, (int)clobIn.length()); (2) + lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, (int)blobIn.length()); (3) + } + } +); + +blobIs.close(); +clobReader.close(); +``` + +|**1**|在`declareParameter`中传递(在本例中)是一个普通的`DefaultLobHandler`。| +|-----|--------------------------------------------------------------------------------| +|**2**|使用`setClobAsCharacterStream`的方法来传入 CLOB 的内容。| +|**3**|使用`setBlobAsBinaryStream`方法传入 BLOB 的内容。| + +Kotlin + +``` +val blobIn = File("spring2004.jpg") +val blobIs = FileInputStream(blobIn) +val clobIn = File("large.txt") +val clobIs = FileInputStream(clobIn) +val clobReader = InputStreamReader(clobIs) + +jdbcTemplate.execute( + "INSERT INTO lob_table (id, a_clob, a_blob) VALUES (?, ?, ?)", + object: AbstractLobCreatingPreparedStatementCallback(lobHandler) { (1) + override fun setValues(ps: PreparedStatement, lobCreator: LobCreator) { + ps.setLong(1, 1L) + lobCreator.setClobAsCharacterStream(ps, 2, clobReader, clobIn.length().toInt()) (2) + lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, blobIn.length().toInt()) (3) + } + } +) +blobIs.close() +clobReader.close() +``` + +|**1**|在`lobHandler`中传递(在本例中)是一个普通的`DefaultLobHandler`。| +|-----|--------------------------------------------------------------------------------| +|**2**|使用`setClobAsCharacterStream`方法传入 CLOB 的内容。| +|**3**|使用`setBlobAsBinaryStream`方法传入 BLOB 的内容。| + +| |如果在从 `defaultLobhandler.getlobCreator()’返回的<gtr="1338"/>上调用<gtr="1336"/>、<gtr="1337"/>或 `setClobasCharacterStream’方法,则可以为 `ContentLength’参数指定一个负值。如果指定的内容长度为负,“DefaultLobHandler”将使用 set-stream 方法的 JDBC4.0 变体,而不使用<br/>长度参数。否则,它会将指定的长度传递给驱动程序。<br/><br/>查看用于验证它是否支持流<br/>LOB 而不提供内容长度的 JDBC 驱动程序的文档。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +现在是从数据库中读取 LOB 数据的时候了。同样,使用带有相同实例变量`JdbcTemplate`的`lobHandler`和对`DefaultLobHandler`的引用。下面的示例展示了如何做到这一点: + +Java + +``` +List<Map<String, Object>> l = jdbcTemplate.query("select id, a_clob, a_blob from lob_table", + new RowMapper<Map<String, Object>>() { + public Map<String, Object> mapRow(ResultSet rs, int i) throws SQLException { + Map<String, Object> results = new HashMap<String, Object>(); + String clobText = lobHandler.getClobAsString(rs, "a_clob"); (1) + results.put("CLOB", clobText); + byte[] blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob"); (2) + results.put("BLOB", blobBytes); + return results; + } + }); +``` + +|**1**|使用`getClobAsString`方法检索 CLOB 的内容。| +|-----|------------------------------------------------------------------------| +|**2**|使用方法`getBlobAsBytes`检索 blob 的内容。| + +Kotlin + +``` +val l = jdbcTemplate.query("select id, a_clob, a_blob from lob_table") { rs, _ -> + val clobText = lobHandler.getClobAsString(rs, "a_clob") (1) + val blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob") (2) + mapOf("CLOB" to clobText, "BLOB" to blobBytes) +} +``` + +|**1**|使用方法`getClobAsString`检索 CLOB 的内容。| +|-----|------------------------------------------------------------------------| +|**2**|使用方法`getBlobAsBytes`检索 blob 的内容。| + +#### 3.8.3.传入 IN 子句的值列表 + +SQL 标准允许基于包含变量值列表的表达式选择行。一个典型的例子是`select * from T_ACTOR where id in (1, 2, 3)`。JDBC 标准对准备好的语句不直接支持此变量列表。不能声明数量可变的占位符。你需要使用所需数量的占位符来进行一些更改,或者,一旦知道需要多少个占位符,就需要动态地生成 SQL 字符串。在`NamedParameterJdbcTemplate`和`JdbcTemplate`中提供的命名参数支持采用后一种方法。可以将这些值作为基元对象的`java.util.List`传入。这个列表用于插入所需的占位符,并在语句执行期间传入这些值。 + +| |在传递许多值时要小心。JDBC 标准并不保证<br/>表达式列表可以使用超过 100 个值。各种数据库都超过了<br/>的值,但是它们通常对允许的值有一个严格的限制。例如,甲骨文的<br/>极限是 1000。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +除了 value 列表中的基本值之外,你还可以创建对象数组的`java.util.List`。这个列表可以支持为`in`子句定义的多个表达式,例如`select * from T_ACTOR where (id, last_name) in ((1, 'Johnson'), (2, 'Harrop'))`。当然,这需要你的数据库支持这种语法。 + +#### 3.8.4.处理存储过程调用的复杂类型 + +当调用存储过程时,有时可以使用特定于数据库的复杂类型。为了适应这些类型, Spring 提供了一个`SqlReturnType`,用于在从存储过程调用返回它们时处理它们,以及在将它们作为参数传入存储过程时处理`SqlTypeValue`。 + +`SqlReturnType`接口有一个必须实现的方法(名为`getTypeValue`)。该接口用作`SqlOutParameter`声明的一部分。下面的示例显示返回用户声明类型`ITEM_TYPE`的 Oracle`STRUCT`对象的值: + +Java + +``` +public class TestItemStoredProcedure extends StoredProcedure { + + public TestItemStoredProcedure(DataSource dataSource) { + // ... + declareParameter(new SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE", + (CallableStatement cs, int colIndx, int sqlType, String typeName) -> { + STRUCT struct = (STRUCT) cs.getObject(colIndx); + Object[] attr = struct.getAttributes(); + TestItem item = new TestItem(); + item.setId(((Number) attr[0]).longValue()); + item.setDescription((String) attr[1]); + item.setExpirationDate((java.util.Date) attr[2]); + return item; + })); + // ... + } +``` + +Kotlin + +``` +class TestItemStoredProcedure(dataSource: DataSource) : StoredProcedure() { + + init { + // ... + declareParameter(SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE") { cs, colIndx, sqlType, typeName -> + val struct = cs.getObject(colIndx) as STRUCT + val attr = struct.getAttributes() + TestItem((attr[0] as Long, attr[1] as String, attr[2] as Date) + }) + // ... + } +} +``` + +可以使用`SqlTypeValue`将 Java 对象的值(例如`TestItem`)传递给存储过程。`SqlTypeValue`接口有一个必须实现的方法(名为 `createtypeValue’)。活动连接被传入,你可以使用它创建特定于数据库的对象,例如`StructDescriptor`实例或`ArrayDescriptor`实例。下面的示例创建了`StructDescriptor`实例: + +Java + +``` +final TestItem testItem = new TestItem(123L, "A test item", + new SimpleDateFormat("yyyy-M-d").parse("2010-12-31")); + +SqlTypeValue value = new AbstractSqlTypeValue() { + protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException { + StructDescriptor itemDescriptor = new StructDescriptor(typeName, conn); + Struct item = new STRUCT(itemDescriptor, conn, + new Object[] { + testItem.getId(), + testItem.getDescription(), + new java.sql.Date(testItem.getExpirationDate().getTime()) + }); + return item; + } +}; +``` + +Kotlin + +``` +val (id, description, expirationDate) = TestItem(123L, "A test item", + SimpleDateFormat("yyyy-M-d").parse("2010-12-31")) + +val value = object : AbstractSqlTypeValue() { + override fun createTypeValue(conn: Connection, sqlType: Int, typeName: String?): Any { + val itemDescriptor = StructDescriptor(typeName, conn) + return STRUCT(itemDescriptor, conn, + arrayOf(id, description, java.sql.Date(expirationDate.time))) + } +} +``` + +现在可以将这个`SqlTypeValue`添加到`Map`中,该参数包含存储过程的 `execute’调用的输入参数。 + +`SqlTypeValue`的另一种用法是将一组值传递给 Oracle 存储过程。Oracle 有自己的内部`ARRAY`类,在这种情况下必须使用它,你可以使用`SqlTypeValue`来创建 Oracle`ARRAY`的实例,并用来自 Java`ARRAY`的值填充它,如下例所示: + +Java + +``` +final Long[] ids = new Long[] {1L, 2L}; + +SqlTypeValue value = new AbstractSqlTypeValue() { + protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException { + ArrayDescriptor arrayDescriptor = new ArrayDescriptor(typeName, conn); + ARRAY idArray = new ARRAY(arrayDescriptor, conn, ids); + return idArray; + } +}; +``` + +Kotlin + +``` +class TestItemStoredProcedure(dataSource: DataSource) : StoredProcedure() { + + init { + val ids = arrayOf(1L, 2L) + val value = object : AbstractSqlTypeValue() { + override fun createTypeValue(conn: Connection, sqlType: Int, typeName: String?): Any { + val arrayDescriptor = ArrayDescriptor(typeName, conn) + return ARRAY(arrayDescriptor, conn, ids) + } + } + } +} +``` + +### 3.9.嵌入式数据库支持 + +`org.springframework.jdbc.datasource.embedded`包提供了对嵌入式 Java 数据库引擎的支持。本地提供了对[HSQL](http://www.hsqldb.org)、[H2](https://www.h2database.com)和[Derby](https://db.apache.org/derby)的支持。你还可以使用可扩展的 API 来插入新的嵌入式数据库类型和“数据源”实现。 + +#### 3.9.1.为什么要使用嵌入式数据库? + +由于嵌入式数据库的轻量级特性,它在项目的开发阶段非常有用。好处包括易于配置、快速启动时间、可测试性以及在开发过程中快速改进 SQL 的能力。 + +#### 3.9.2.使用 Spring XML 创建嵌入式数据库 + +如果希望在 Spring `ApplicationContext’中以 Bean 的形式公开嵌入式数据库实例,则可以在`embedded-database`名称空间中使用`spring-jdbc`标记: + +``` +<jdbc:embedded-database id="dataSource" generate-name="true"> + <jdbc:script location="classpath:schema.sql"/> + <jdbc:script location="classpath:test-data.sql"/> +</jdbc:embedded-database> +``` + +前面的配置创建了一个嵌入式 HSQL 数据库,该数据库由来自 Classpath 根中`schema.sql`和`test-data.sql`资源的 SQL 填充。此外,作为一种最佳实践,嵌入式数据库被分配一个唯一生成的名称。将嵌入式数据库作为类型为“javax.sql.datasource”的 Bean 容器提供给 Spring 容器,然后可以根据需要将其注入到数据访问对象中。 + +#### 3.9.3.以编程方式创建嵌入式数据库 + +`EmbeddedDatabaseBuilder`类为以编程方式构建嵌入式数据库提供了一个 Fluent API。当你需要在独立环境或独立集成测试中创建嵌入式数据库时,可以使用此方法,如下例所示: + +Java + +``` +EmbeddedDatabase db = new EmbeddedDatabaseBuilder() + .generateUniqueName(true) + .setType(H2) + .setScriptEncoding("UTF-8") + .ignoreFailedDrops(true) + .addScript("schema.sql") + .addScripts("user_data.sql", "country_data.sql") + .build(); + +// perform actions against the db (EmbeddedDatabase extends javax.sql.DataSource) + +db.shutdown() +``` + +Kotlin + +``` +val db = EmbeddedDatabaseBuilder() + .generateUniqueName(true) + .setType(H2) + .setScriptEncoding("UTF-8") + .ignoreFailedDrops(true) + .addScript("schema.sql") + .addScripts("user_data.sql", "country_data.sql") + .build() + +// perform actions against the db (EmbeddedDatabase extends javax.sql.DataSource) + +db.shutdown() +``` + +有关所有支持的选项的更多详细信息,请参见[javadoc for `EmbeddedDatabaseBuilder`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.html)。 + +你还可以使用`EmbeddedDatabaseBuilder`通过使用 Java 配置来创建嵌入式数据库,如下例所示: + +Java + +``` +@Configuration +public class DataSourceConfig { + + @Bean + public DataSource dataSource() { + return new EmbeddedDatabaseBuilder() + .generateUniqueName(true) + .setType(H2) + .setScriptEncoding("UTF-8") + .ignoreFailedDrops(true) + .addScript("schema.sql") + .addScripts("user_data.sql", "country_data.sql") + .build(); + } +} +``` + +Kotlin + +``` +@Configuration +class DataSourceConfig { + + @Bean + fun dataSource(): DataSource { + return EmbeddedDatabaseBuilder() + .generateUniqueName(true) + .setType(H2) + .setScriptEncoding("UTF-8") + .ignoreFailedDrops(true) + .addScript("schema.sql") + .addScripts("user_data.sql", "country_data.sql") + .build() + } +} +``` + +#### 3.9.4.选择嵌入式数据库类型 + +本节介绍如何在 Spring 支持的三个嵌入式数据库中选择一个。它包括以下主题: + +* [使用 HSQL](#jdbc-embedded-database-using-HSQL) + +* [使用 H2](#jdbc-embedded-database-using-H2) + +* [使用 Derby](#jdbc-embedded-database-using-Derby) + +##### Using HSQL + +Spring 支持 HSQL1.8.0 及以上版本。如果没有显式指定类型,则 HSQL 是默认的嵌入式数据库。要显式地指定 HSQL,请将 `embedded-database’标记的`type`属性设置为`HSQL`。如果使用 Builder API,则使用`EmbeddedDatabaseType.HSQL`调用“settype”方法。 + +##### Using H2 + +Spring 支持 H2 数据库。要启用 H2,请将 `embedded-database’标记的`type`属性设置为`H2`。如果使用 Builder API,则使用`EmbeddedDatabaseType.H2`调用“settype”方法。 + +##### Using Derby + +Spring 支持 Apache Derby10.5 及以上版本。要启用 Derby,请将`embedded-database`标记的`type`属性设置为`DERBY`。如果使用 Builder API,则使用`EmbeddedDatabaseType.DERBY`调用`setType(EmbeddedDatabaseType)`方法。 + +#### 3.9.5.用嵌入式数据库测试数据访问逻辑 + +嵌入式数据库提供了一种轻量级的方式来测试数据访问代码。下一个示例是一个使用嵌入式数据库的数据访问集成测试模板。当嵌入式数据库不需要跨测试类重用时,使用这样的模板对于一次性操作很有用。但是,如果你希望创建在测试套件中共享的嵌入式数据库,可以考虑使用[Spring TestContext Framework](testing.html#testcontext-framework),并将嵌入式数据库配置为 Bean 中的 Spring `ApplicationContext`中的 Bean,如[Creating an Embedded Database by Using Spring XML](#jdbc-embedded-database-xml)和[以编程方式创建嵌入式数据库](#jdbc-embedded-database-java)中所述。下面的清单显示了测试模板: + +Java + +``` +public class DataAccessIntegrationTestTemplate { + + private EmbeddedDatabase db; + + @BeforeEach + public void setUp() { + // creates an HSQL in-memory database populated from default scripts + // classpath:schema.sql and classpath:data.sql + db = new EmbeddedDatabaseBuilder() + .generateUniqueName(true) + .addDefaultScripts() + .build(); + } + + @Test + public void testDataAccess() { + JdbcTemplate template = new JdbcTemplate(db); + template.query( /* ... */ ); + } + + @AfterEach + public void tearDown() { + db.shutdown(); + } + +} +``` + +Kotlin + +``` +class DataAccessIntegrationTestTemplate { + + private lateinit var db: EmbeddedDatabase + + @BeforeEach + fun setUp() { + // creates an HSQL in-memory database populated from default scripts + // classpath:schema.sql and classpath:data.sql + db = EmbeddedDatabaseBuilder() + .generateUniqueName(true) + .addDefaultScripts() + .build() + } + + @Test + fun testDataAccess() { + val template = JdbcTemplate(db) + template.query( /* ... */) + } + + @AfterEach + fun tearDown() { + db.shutdown() + } +} +``` + +#### 3.9.6.为嵌入式数据库生成唯一的名称 + +如果开发团队的测试套件无意中试图重新创建同一数据库的其他实例,那么在使用嵌入式数据库时,开发团队经常会遇到错误。如果一个 XML 配置文件或`@Configuration`类负责创建嵌入式数据库,并且相应的配置随后在相同的测试套件(即在相同的 JVM 流程中)的多个测试场景中被重用,那么这种情况就很容易发生,例如,针对嵌入式数据库的集成测试,其“ApplicationContext”配置仅在 Bean 定义配置文件处于活动状态时有所不同。 + +此类错误的根本原因是 Spring 的`EmbeddedDatabaseFactory`(`<jdbc:embedded-database>`XML 名称空间元素和用于 Java 配置的 `EmbedDedatabaseBuilder’内部都使用)将嵌入式数据库的名称设置为 `TestDB’,如果没有另外指定的话。对于`<jdbc:embedded-database>`的情况,嵌入式数据库通常被分配一个等于 Bean 的`id`的名称(通常,类似于`dataSource`)。因此,随后创建嵌入式数据库的尝试不会产生新的数据库。相反,相同的 JDBC 连接 URL 被重用,并且试图创建新的嵌入式数据库实际上指向从相同配置创建的现有嵌入式数据库。 + +为了解决这个常见的问题, Spring Framework4.2 提供了为嵌入式数据库生成唯一名称的支持。要启用生成的名称,请使用以下选项之一。 + +* `EmbeddedDatabaseFactory.setGenerateUniqueDatabaseName()` + +* `EmbeddedDatabaseBuilder.generateUniqueName()` + +* `<jdbc:embedded-database generate-name="true" …​ >` + +#### 3.9.7.扩展嵌入式数据库支持 + +你可以通过两种方式扩展 Spring JDBC 嵌入式数据库支持: + +* 实现`EmbeddedDatabaseConfigurer`以支持新的嵌入式数据库类型。 + +* 实现`DataSourceFactory`以支持新的`DataSource`实现,例如连接池来管理嵌入式数据库连接。 + +我们鼓励你在[GitHub Issues](https://github.com/spring-projects/spring-framework/issues)上为 Spring 社区提供扩展。 + +### 3.10.初始化`DataSource` + +`org.springframework.jdbc.datasource.init`包支持初始化现有的`DataSource`。嵌入式数据库支持提供了一个选项,用于为应用程序创建和初始化`DataSource`。然而,有时你可能需要初始化在某个服务器上运行的实例。 + +#### 3.10.1.使用 Spring XML 初始化数据库 + +如果要初始化数据库,并且可以提供对`DataSource` Bean 的引用,则可以在`initialize-database`名称空间中使用`initialize-database`标记: + +``` +<jdbc:initialize-database data-source="dataSource"> + <jdbc:script location="classpath:com/foo/sql/db-schema.sql"/> + <jdbc:script location="classpath:com/foo/sql/db-test-data.sql"/> +</jdbc:initialize-database> +``` + +前面的示例针对数据库运行两个指定的脚本。第一个脚本创建一个模式,第二个脚本使用测试数据集填充表。脚本位置也可以是带有通配符的模式,通配符是用于 Spring 中的资源的常见 Ant 样式(例如,` Classpath *:/com/foo/**/SQL/*-data.sql`)。如果使用模式,那么脚本将按照其 URL 或文件名的词法顺序运行。 + +数据库初始化器的默认行为是无条件地运行所提供的脚本。这可能并不总是你想要的——例如,如果你针对已经包含测试数据的数据库运行脚本。通过遵循先创建表,然后插入数据的常见模式(如前面所示),可以减少意外删除数据的可能性。如果表已经存在,则第一步失败。 + +然而,为了获得对现有数据的创建和删除的更多控制,XML 名称空间提供了一些附加选项。第一个是用来打开和关闭初始化的标志。你可以根据环境来设置这个值(例如,从系统属性或环境中提取布尔值 Bean)。下面的示例从系统属性获得一个值: + +``` +<jdbc:initialize-database data-source="dataSource" + enabled="#{systemProperties.INITIALIZE_DATABASE}"> (1) + <jdbc:script location="..."/> +</jdbc:initialize-database> +``` + +|**1**|从一个名为`INITIALIZE_DATABASE`的系统属性获取`enabled`的值。| +|-----|--------------------------------------------------------------------------------| + +控制现有数据的第二个选择是更宽容地对待失败。为此,你可以控制初始化器的能力,以忽略它从脚本运行的 SQL 中的某些错误,如下例所示: + +``` +<jdbc:initialize-database data-source="dataSource" ignore-failures="DROPS"> + <jdbc:script location="..."/> +</jdbc:initialize-database> +``` + +在前面的示例中,我们认为,有时脚本是针对一个空数据库运行的,因此脚本中的一些`DROP`语句将会失败。因此失败的 SQL`DROP`语句将被忽略,但其他失败将导致异常。如果你的 SQL 方言不支持`DROP …​ IF EXISTS`(或类似),但你希望在重新创建测试数据之前无条件地删除所有测试数据,那么这是非常有用的。在这种情况下,第一个脚本通常是一组`DROP`语句,然后是一组`CREATE`语句。 + +`ignore-failures`选项可以设置为`NONE`(默认值),`DROPS`(忽略失败的下降),或`ALL`(忽略所有失败)。 + +如果脚本中根本不存在`;`字符,则每个语句都应该用`;`分隔,或者用新的一行分隔。你可以全局控制它,也可以通过脚本控制它,如下例所示: + +``` +<jdbc:initialize-database data-source="dataSource" separator="@@"> (1) + <jdbc:script location="classpath:com/myapp/sql/db-schema.sql" separator=";"/> (2) + <jdbc:script location="classpath:com/myapp/sql/db-test-data-1.sql"/> + <jdbc:script location="classpath:com/myapp/sql/db-test-data-2.sql"/> +</jdbc:initialize-database> +``` + +|**1**|将分隔符脚本设置为`@@`。| +|-----|---------------------------------------------| +|**2**|将`db-schema.sql`的分隔符设置为`;`。| + +在这个示例中,两个`test-data`脚本使用`@@`作为语句分隔符,只有`db-schema.sql`使用`;`。此配置指定了`@@`的默认分隔符,并覆盖了`db-schema`脚本的默认分隔符。 + +如果你需要的控制比从 XML 命名空间获得的更多,那么你可以直接使用“DataSourceInitializer”并将其定义为应用程序中的组件。 + +##### 依赖于数据库的其他组件的初始化 + +一大类应用程序(那些在 Spring 上下文启动后才使用数据库的应用程序)可以使用数据库初始化器,而不会有更多的麻烦。如果你的应用程序不是其中之一,那么你可能需要阅读本节的其余部分。 + +数据库初始化器依赖于`DataSource`实例,并运行其初始化回调中提供的脚本(类似于 XML Bean 定义中的`init-method`,组件中的`@PostConstruct`方法,或实现`InitializingBean`的组件中的`afterPropertiesSet()`方法)。如果其他 bean 依赖于相同的数据源,并在初始化回调中使用数据源,则可能存在问题,因为数据尚未初始化。这方面的一个常见示例是一个缓存,它在应用程序启动时急切地初始化并从数据库加载数据。 + +要解决这个问题,你有两种选择:将缓存初始化策略更改到稍后的阶段,或者确保首先初始化数据库初始化器。 + +如果应用程序在你的控制范围内,而不是在其他情况下,更改缓存初始化策略可能很容易。关于如何实现这一点的一些建议包括: + +* 使缓存在第一次使用时就进行惰性初始化,从而提高了应用程序的启动时间。 + +* 拥有你的缓存或初始化缓存实现“生命周期”或`SmartLifecycle`的独立组件。当应用程序上下文启动时,可以通过设置其`autoStartup`标志自动启动`SmartLifecycle`,也可以通过在封闭的上下文上调用`ConfigurableApplicationContext.start()`手动启动`Lifecycle`。 + +* 使用 Spring `ApplicationEvent`或类似的自定义观察者机制来触发缓存初始化。`ContextRefreshedEvent`总是在上下文准备好使用时发布(在所有 bean 都已初始化之后),因此这通常是一个有用的钩子(这是`SmartLifecycle`默认情况下的工作方式)。 + +确保首先初始化数据库初始化器也很容易。关于如何实现这一点的一些建议包括: + +* 依赖于 Spring `BeanFactory`的默认行为,即以注册顺序初始化 bean。通过在 XML 配置中采用一组`<import/>`元素的常见做法来安排这一点,这些元素对应用程序模块进行了排序,并确保首先列出了数据库和数据库初始化。 + +* 分离`DataSource`和使用它的业务组件,并通过将它们放在单独的`ApplicationContext`实例中来控制它们的启动顺序(例如,父上下文包含`DataSource`,子上下文包含业务组件)。这种结构在 Spring Web 应用程序中很常见,但可以更普遍地应用。 + +## 4. 使用 R2DBC 进行数据访问 + +[R2DBC](https://r2dbc.io)(“反应式关系数据库连接”)是一种社区驱动的规范工作,用于使用反应式模式标准化对 SQL 数据库的访问。 + +### 4.1.包层次结构 + +Spring 框架的 R2DBC 抽象框架由两个不同的包组成: + +* `core`:`org.springframework.r2dbc.core`包包含`DatabaseClient`类和各种相关的类。见[使用 R2DBC 核心类来控制基本的 R2DBC 处理和错误处理](#r2dbc-core)。 + +* `connection`:`org.springframework.r2dbc.connection`包包含一个用于 easy`ConnectionFactory`访问的实用程序类和各种简单的`ConnectionFactory`实现,你可以使用这些实现来测试和运行未修改的 R2DBC。见[控制数据库连接](#r2dbc-connections)。 + +### 4.2.使用 R2DBC 核心类来控制基本的 R2DBC 处理和错误处理 + +本节介绍如何使用 R2DBC 核心类来控制基本的 R2DBC 处理,包括错误处理。它包括以下主题: + +* [Using `DatabaseClient`](#r2dbc-DatabaseClient) + +* [执行语句](#r2dbc-DatabaseClient-examples-statement) + +* [查询(“选择”)](#r2dbc-DatabaseClient-examples-query) + +* [Updating (`INSERT`, `UPDATE`, and `DELETE`) with `DatabaseClient`](#r2dbc-DatabaseClient-examples-update) + +* [语句过滤器](#r2dbc-DatabaseClient-filter) + +* [检索自动生成的密钥](#r2dbc-auto-generated-keys) + +#### 4.2.1.使用`DatabaseClient` + +`DatabaseClient`是 R2DBC 核心包中的中心类。它处理资源的创建和释放,这有助于避免常见的错误,例如忘记关闭连接。它执行核心 R2DBC 工作流的基本任务(例如语句创建和执行),将应用程序代码留给提供 SQL 和提取结果。`DatabaseClient`类: + +* 运行 SQL 查询 + +* 更新语句和存储过程调用 + +* 在`Result`实例上执行迭代 + +* 捕获 R2DBC 异常,并将其转换为在`org.springframework.dao`包中定义的通用的、信息量更大的异常层次结构。(见[一致的异常层次结构](#dao-exceptions)。 + +客户机有一个功能强大、流畅的 API,它使用了用于声明性组合的反应性类型。 + +当你使用`DatabaseClient`作为你的代码时,你只需要实现 `java.util.function’接口,并为它们提供一个明确定义的契约。给定一个由`DatabaseClient`类提供的`Connection`,一个`Function`回调将创建一个`Publisher`。对于提取`Row`结果的映射函数也是如此。 + +你可以通过使用`ConnectionFactory`引用的直接实例化在 DAO 实现中使用`DatabaseClient`,或者可以在 Spring IoC 容器中配置它,并将其作为 Bean 引用提供给 DAO。 + +创建`DatabaseClient`对象的最简单方法是通过静态工厂方法,如下所示: + +Java + +``` +DatabaseClient client = DatabaseClient.create(connectionFactory); +``` + +Kotlin + +``` +val client = DatabaseClient.create(connectionFactory) +``` + +| |在 Spring IOC<br/>容器中,应该始终将`ConnectionFactory`配置为 Bean。| +|---|----------------------------------------------------------------------------------------------| + +前面的方法使用默认设置创建`DatabaseClient`。 + +你还可以从`DatabaseClient.builder()`获得`Builder`实例。你可以通过调用以下方法来定制客户机: + +* `….bindMarkers(…)`:提供一个特定的`BindMarkersFactory`以将命名参数配置为数据库绑定标记转换。 + +* `….executeFunction(…)`:设置`ExecuteFunction`如何运行`Statement`对象。 + +* `….namedParameters(false)`:禁用命名参数展开。默认情况下启用。 + +| |方言是由[BindMarkersFactoryResolver](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/r2dbc/core/binding/BindMarkersFactoryResolver.html)从`ConnectionFactory`解析的,通常是通过检查`ConnectionFactoryMetadata`。<br/>你可以通过注册一个<br/>实现`org.springframework.r2dbc.core.binding.BindMarkersFactoryResolver$BindMarkerFactoryProvider`的类,让 Spring 自动发现你的`BindMarkersFactory`。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +目前支持的数据库有: + +* H2 + +* 马里亚布 + +* Microsoft SQL Server + +* MySQL + +* Postgres + +这个类发布的所有 SQL 都记录在`DEBUG`级别下,该类别对应于客户端实例的完全限定类名称(通常为 `DefaultDatabaseClient’)。此外,每个执行都在响应序列中注册一个检查点,以帮助调试。 + +下面的部分提供了`DatabaseClient`用法的一些示例。这些示例并不是`DatabaseClient`公开的所有功能的详尽列表。参见乘务员[javadoc](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/r2dbc/core/DatabaseClient.html)。 + +##### 执行语句 + +`DatabaseClient`提供了运行语句的基本功能。下面的示例展示了创建新表的最小但功能齐全的代码需要包括哪些内容: + +Java + +``` +Mono<Void> completion = client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);") + .then(); +``` + +Kotlin + +``` +client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);") + .await() +``` + +`DatabaseClient`是为方便、流畅的使用而设计的。它在执行规范的每个阶段公开中间方法、延续方法和终端方法。上面的示例使用`then()`返回一个完成的 `publisher’,该’publisher’在查询(或查询,如果 SQL 查询包含多个语句)完成后立即完成。 + +| |`execute(…)`接受 SQL 查询字符串或查询`Supplier<String>`,以将实际的查询创建推迟到执行时。| +|---|---------------------------------------------------------------------------------------------------------------------------------| + +##### 查询(“选择”) + +SQL 查询可以通过`Row`对象或受影响的行数返回值。`DatabaseClient’可以返回更新的行数或行本身,具体取决于发出的查询。 + +下面的查询从表中获取`id`和`name`列: + +Java + +``` +Mono<Map<String, Object>> first = client.sql("SELECT id, name FROM person") + .fetch().first(); +``` + +Kotlin + +``` +val first = client.sql("SELECT id, name FROM person") + .fetch().awaitSingle() +``` + +以下查询使用了一个绑定变量: + +爪哇 + +``` +Mono<Map<String, Object>> first = client.sql("SELECT id, name FROM person WHERE first_name = :fn") + .bind("fn", "Joe") + .fetch().first(); +``` + +Kotlin + +``` +val first = client.sql("SELECT id, name FROM person WHERE WHERE first_name = :fn") + .bind("fn", "Joe") + .fetch().awaitSingle() +``` + +你可能已经注意到在上面的示例中使用了`fetch()`。`fetch()`是一个延续运算符,它允许你指定要消耗多少数据。 + +调用`first()`返回结果中的第一行,并丢弃剩余的行。你可以使用以下操作符来使用数据: + +* `first()`返回整个结果的第一行。它的 Kotlin 协程变量名为`awaitSingle()`,用于表示不可空的返回值,如果该值是可选的,则名为`awaitSingleOrNull()`。 + +* `one()`只返回一个结果,如果结果包含更多行,则该结果将失败。使用 Kotlin 协程,`awaitOne()`正好代表一个值,或者`awaitOneOrNull()`如果该值可能是`null`。 + +* `all()`返回结果的所有行。当使用 Kotlin 协程时,使用`flow()`。 + +* `rowsUpdated()`返回受影响的行数。其 Kotlin 协程变体被命名为`awaitRowsUpdated()`。 + +在不指定更多映射细节的情况下,查询以`Map`的形式返回表式结果,其键是不区分大小写的列名,这些列名映射到它们的列值。 + +你可以通过提供一个`Function<Row, T>`来控制结果映射,每个`Row`都会被调用,这样它就可以返回任意值(奇异值、集合和映射以及对象)。 + +下面的示例提取`name`列并发出其值: + +爪哇 + +``` +Flux<String> names = client.sql("SELECT name FROM person") + .map(row -> row.get("name", String.class)) + .all(); +``` + +Kotlin + +``` +val names = client.sql("SELECT name FROM person") + .map{ row: Row -> row.get("name", String.class) } + .flow() +``` + +那么`null`呢? + +关系数据库结果可以包含`null`值。反应流规范禁止`null`值的发射。该需求要求在提取器函数中进行适当的`null`处理。虽然可以从`Row`中获得`null`值,但不能发出`null`值。你必须包装对象中的任何`null`值(例如,对于奇异值,`Optional`),以确保提取函数永远不会直接返回`null`值。 + +##### 用`DatabaseClient`更新(` 插入 `,`UPDATE`,和`DELETE`)##### + +修改语句的唯一区别是,这些语句通常不返回表格数据,因此你可以使用`rowsUpdated()`来使用结果。 + +下面的示例显示了返回更新行数的`UPDATE`语句: + +爪哇 + +``` +Mono<Integer> affectedRows = client.sql("UPDATE person SET first_name = :fn") + .bind("fn", "Joe") + .fetch().rowsUpdated(); +``` + +Kotlin + +``` +val affectedRows = client.sql("UPDATE person SET first_name = :fn") + .bind("fn", "Joe") + .fetch().awaitRowsUpdated() +``` + +##### 将值绑定到查询 + +典型的应用程序需要参数化的 SQL 语句来根据某些输入选择或更新行。这些语句通常是`SELECT`语句,受`WHERE`子句或`INSERT`和`UPDATE`语句的约束,这些语句接受输入参数。如果参数没有正确转义,参数化语句将承担 SQL 注入的风险。`DatabaseClient`利用 R2DBC 的 `bind’API 来消除查询参数的 SQL 注入风险。你可以使用`execute(…)`操作符提供参数化 SQL 语句,并将参数绑定到实际的`Statement`。然后,你的 R2DBC 驱动程序通过使用准备好的语句和参数替换来运行该语句。 + +参数绑定支持两种绑定策略: + +* 通过索引,使用零基参数索引。 + +* 按名称,使用占位符名称。 + +下面的示例展示了查询的参数绑定: + +``` +db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") + .bind("id", "joe") + .bind("name", "Joe") + .bind("age", 34); +``` + +R2DBC 原生结合标记 + +R2DBC 使用依赖于实际数据库供应商的数据库本地绑定标记。例如,Postgres 使用索引标记,例如`$1`,`$2`,`$n`。另一个例子是 SQL Server,它使用带有`@`前缀的命名绑定标记。 + +这与 JDBC 不同,后者需要`?`作为绑定标记。在 JDBC 中,实际的驱动程序将`?`绑定标记转换为数据库本地标记,作为其语句执行的一部分。 + +Spring Framework 的 R2DBC 支持允许你使用本机绑定标记或带有`:name`语法的命名绑定标记。 + +命名参数支持利用`BindMarkersFactory`实例在执行查询时将命名参数扩展为本机绑定标记,这使你在不同的数据库供应商之间具有一定程度的查询可移植性。 + +查询预处理程序将名为`Collection`的参数展开到一系列绑定标记中,以消除基于参数数量的动态查询创建的需要。嵌套对象数组被扩展以允许使用(例如)选择列表。 + +考虑以下查询: + +``` +SELECT id, name, state FROM table WHERE (name, age) IN (('John', 35), ('Ann', 50)) +``` + +前面的查询可以参数化并按以下方式运行: + +爪哇 + +``` +List<Object[]> tuples = new ArrayList<>(); +tuples.add(new Object[] {"John", 35}); +tuples.add(new Object[] {"Ann", 50}); + +client.sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)") + .bind("tuples", tuples); +``` + +Kotlin + +``` +val tuples: MutableList<Array<Any>> = ArrayList() +tuples.add(arrayOf("John", 35)) +tuples.add(arrayOf("Ann", 50)) + +client.sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)") + .bind("tuples", tuples) +``` + +| |选择列表的使用依赖于供应商。| +|---|------------------------------------------| + +下面的示例显示了一个使用`IN`谓词的更简单的变体: + +爪哇 + +``` +client.sql("SELECT id, name, state FROM table WHERE age IN (:ages)") + .bind("ages", Arrays.asList(35, 50)); +``` + +Kotlin + +``` +val tuples: MutableList<Array<Any>> = ArrayList() +tuples.add(arrayOf("John", 35)) +tuples.add(arrayOf("Ann", 50)) + +client.sql("SELECT id, name, state FROM table WHERE age IN (:ages)") + .bind("tuples", arrayOf(35, 50)) +``` + +| |R2DBC 本身不支持类集合的值。尽管如此,<br/>在上面的示例中扩展给定的`List`对于 Spring 的 R2DBC 支持中的命名参数<br/>有效,例如,如上面所示,用于`IN`子句。<br/>但是,插入或更新数组类型的列(例如在 postgres 中)<br/>需要底层 R2DBC 驱动程序支持的数组类型:<br/>通常是一个 爪哇 数组,例如。`String[]`以更新`text[]`列。<br/>不要将`Collection<String>`等作为数组参数传递。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 语句过滤器 + +有时,你需要在运行前对实际`Statement`上的选项进行微调。通过`DatabaseClient`注册一个`Statement`过滤器(`statementfilterfunction`),以便在语句的执行中拦截和修改语句,如下例所示: + +爪哇 + +``` +client.sql("INSERT INTO table (name, state) VALUES(:name, :state)") + .filter((s, next) -> next.execute(s.returnGeneratedValues("id"))) + .bind("name", …) + .bind("state", …); +``` + +Kotlin + +``` +client.sql("INSERT INTO table (name, state) VALUES(:name, :state)") + .filter { s: Statement, next: ExecuteFunction -> next.execute(s.returnGeneratedValues("id")) } + .bind("name", …) + .bind("state", …) +``` + +`DatabaseClient`公开还简化了`filter(…)`接受`Function<Statement, Statement>`的过载: + +爪哇 + +``` +client.sql("INSERT INTO table (name, state) VALUES(:name, :state)") + .filter(statement -> s.returnGeneratedValues("id")); + +client.sql("SELECT id, name, state FROM table") + .filter(statement -> s.fetchSize(25)); +``` + +Kotlin + +``` +client.sql("INSERT INTO table (name, state) VALUES(:name, :state)") + .filter { statement -> s.returnGeneratedValues("id") } + +client.sql("SELECT id, name, state FROM table") + .filter { statement -> s.fetchSize(25) } +``` + +`StatementFilterFunction`实现允许过滤 ` 语句’和过滤`Result`对象。 + +##### `DatabaseClient`最佳实践 + +一旦配置好,`DatabaseClient`类的实例是线程安全的。这很重要,因为这意味着你可以配置`DatabaseClient`的单个实例,然后将这个共享引用安全地注入到多个 DAO(或存储库)中。`DatabaseClient`是有状态的,因为它保持了对`ConnectionFactory`的引用,但是这个状态不是会话状态。 + +在使用`DatabaseClient`类时,一种常见的做法是在 Spring 配置文件中配置`ConnectionFactory`,然后在依赖项中注入共享的`ConnectionFactory` Bean 到 DAO 类中。在 setter 中为`ConnectionFactory`创建了`DatabaseClient`。这导致 DAO 类似于以下内容: + +爪哇 + +``` +public class R2dbcCorporateEventDao implements CorporateEventDao { + + private DatabaseClient databaseClient; + + public void setConnectionFactory(ConnectionFactory connectionFactory) { + this.databaseClient = DatabaseClient.create(connectionFactory); + } + + // R2DBC-backed implementations of the methods on the CorporateEventDao follow... +} +``` + +Kotlin + +``` +class R2dbcCorporateEventDao(connectionFactory: ConnectionFactory) : CorporateEventDao { + + private val databaseClient = DatabaseClient.create(connectionFactory) + + // R2DBC-backed implementations of the methods on the CorporateEventDao follow... +} +``` + +显式配置的一种替代方法是使用组件扫描和注释支持进行依赖注入。在这种情况下,你可以使用`@Component`对类进行注释(这使它成为组件扫描的候选),并使用`ConnectionFactory`setter 方法对`@Autowired`进行注释。下面的示例展示了如何做到这一点: + +爪哇 + +``` +@Component (1) +public class R2dbcCorporateEventDao implements CorporateEventDao { + + private DatabaseClient databaseClient; + + @Autowired (2) + public void setConnectionFactory(ConnectionFactory connectionFactory) { + this.databaseClient = DatabaseClient.create(connectionFactory); (3) + } + + // R2DBC-backed implementations of the methods on the CorporateEventDao follow... +} +``` + +|**1**|用`@Component`注释类。| +|-----|-----------------------------------------------------------------| +|**2**|用`@Autowired`注释`ConnectionFactory`setter 方法。| +|**3**|用`ConnectionFactory`创建一个新的`DatabaseClient`。| + +Kotlin + +``` +@Component (1) +class R2dbcCorporateEventDao(connectionFactory: ConnectionFactory) : CorporateEventDao { (2) + + private val databaseClient = DatabaseClient(connectionFactory) (3) + + // R2DBC-backed implementations of the methods on the CorporateEventDao follow... +} +``` + +|**1**|用`@Component`注释类。| +|-----|-----------------------------------------------------------| +|**2**|`ConnectionFactory`的构造函数注入。| +|**3**|用`ConnectionFactory`创建一个新的`DatabaseClient`。| + +无论你选择使用(或不使用)上述哪种模板初始化样式,每次要运行 SQL 时,都很少需要创建`DatabaseClient`类的新实例。一旦配置好,`DatabaseClient`实例就是线程安全的。如果你的应用程序访问多个数据库,你可能需要多个`DatabaseClient`实例,这需要多个 `ConnectionFactory’实例,然后需要多个配置不同的`DatabaseClient`实例。 + +### 4.3.检索自动生成的密钥 + +`INSERT`语句在向定义自动增量或标识列的表中插入行时可能会生成键。要获得对要生成的列名的完全控制,只需注册一个`StatementFilterFunction`,它为所需的列请求生成的键。 + +爪哇 + +``` +Mono<Integer> generatedId = client.sql("INSERT INTO table (name, state) VALUES(:name, :state)") + .filter(statement -> s.returnGeneratedValues("id")) + .map(row -> row.get("id", Integer.class)) + .first(); + +// generatedId emits the generated key once the INSERT statement has finished +``` + +Kotlin + +``` +val generatedId = client.sql("INSERT INTO table (name, state) VALUES(:name, :state)") + .filter { statement -> s.returnGeneratedValues("id") } + .map { row -> row.get("id", Integer.class) } + .awaitOne() + +// generatedId emits the generated key once the INSERT statement has finished +``` + +### 4.4.控制数据库连接 + +本节内容包括: + +* [Using `ConnectionFactory`](#r2dbc-ConnectionFactory) + +* [Using `ConnectionFactoryUtils`](#r2dbc-ConnectionFactoryUtils) + +* [Using `SingleConnectionFactory`](#r2dbc-SingleConnectionFactory) + +* [Using `TransactionAwareConnectionFactoryProxy`](#r2dbc-TransactionAwareConnectionFactoryProxy) + +* [Using `R2dbcTransactionManager`](#r2dbc-R2dbcTransactionManager) + +#### 4.4.1.使用`ConnectionFactory` + +Spring 通过`ConnectionFactory`获得到数据库的 R2DBC 连接。a`ConnectionFactory`是 R2DBC 规范的一部分,是驱动程序的一个常见入口点。它允许容器或框架从应用程序代码中隐藏连接池和事务管理问题。作为开发人员,你不需要了解有关如何连接到数据库的详细信息。这是设置`ConnectionFactory`的管理员的责任。在开发和测试代码时,你最有可能同时填充这两个角色,但是你并不一定要知道生产数据源是如何配置的。 + +当你使用 Spring 的 R2DBC 层时,你可以使用第三方提供的连接池实现来配置你自己的连接池。一个流行的实现是 R2DBC 池(“R2DBC-Pool”)。 Spring 发行版中的实现仅用于测试目的,并不提供池。 + +要配置`ConnectionFactory`: + +1. 获得与`ConnectionFactory`的连接,就像你通常获得 R2dbc`ConnectionFactory`一样。 + +2. 提供一个 R2DBC URL(请参阅驱动程序的文档以获得正确的值)。 + +下面的示例展示了如何配置`ConnectionFactory`: + +爪哇 + +``` +ConnectionFactory factory = ConnectionFactories.get("r2dbc:h2:mem:///test?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"); +``` + +Kotlin + +``` +val factory = ConnectionFactories.get("r2dbc:h2:mem:///test?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"); +``` + +#### 4.4.2.使用`ConnectionFactoryUtils` + +`ConnectionFactoryUtils`类是一个方便且功能强大的助手类,它提供`static`方法来从`ConnectionFactory`获得连接和紧密连接(如果需要)。 + +它支持订阅者`Context`绑定的连接,例如“R2DBcTransactionManager”。 + +#### 4.4.3.使用`SingleConnectionFactory` + +`SingleConnectionFactory`类是`DelegatingConnectionFactory`接口的一种实现,该接口封装单个`Connection`,该接口在每次使用后都不会关闭。 + +如果任何客户机代码在假定池连接的情况下调用`close`(如使用持久性工具时),则应将`suppressClose`属性设置为`true`。此设置返回一个封装物理连接的关闭抑制代理。请注意,你不能再将其强制转换为本机`Connection`或类似的对象。 + +`SingleConnectionFactory`主要是一个测试类,如果你的 R2DBC 驱动程序允许这样的使用,它可以用于特定的需求,例如流水线。与池`ConnectionFactory`相反,它始终重用相同的连接,避免了过多地创建物理连接。 + +#### 4.4.4.使用`TransactionAwareConnectionFactoryProxy` + +`TransactionAwareConnectionFactoryProxy`是目标`ConnectionFactory`的代理。代理封装了目标`ConnectionFactory`,以添加对 Spring 管理的事务的感知。 + +| |如果你使用的 R2DBC 客户机不是集成的,则需要使用这个类,否则将支持 Spring 的 R2DBC。在这种情况下,你仍然可以使用这个客户机,并且在<br/>的同时,让这个客户机参与 Spring 托管事务。通常情况下,<br/>最好是将具有对`ConnectionFactoryUtils`的正确访问的 R2DBC 客户机集成在一起,以进行资源管理。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +有关更多详细信息,请参见[“TransactionAwareConnectionFactoryProxy”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/r2dbc/connection/TransactionAwareConnectionFactoryProxy.html)爪哇doc。 + +#### 4.4.5.使用`R2dbcTransactionManager` + +`R2dbcTransactionManager`类是单个 R2DBC 数据源的`ReactiveTransactionManager`实现。它将从指定的连接工厂到订阅服务器`Context`的 R2DBC 连接绑定,可能允许每个连接工厂使用一个订阅服务器连接。 + +应用程序代码需要通过“ConnectionFactoryUtils.getConnection”来检索 R2DBC 连接,而不是 R2DBC 的标准“ConnectionFactory.Create()”。 + +所有框架类(如`DatabaseClient`)都隐式地使用此策略。如果不与此事务管理器一起使用,则查找策略的行为与常见策略完全相同。因此,它可以在任何情况下使用。 + +`R2dbcTransactionManager`类支持应用于连接的自定义隔离级别。 + +## 5. 对象关系映射数据访问 + +本节介绍使用对象关系映射时的数据访问。 + +### 5.1.ORM 简介 Spring + +Spring 框架支持与 爪哇 持久性 API( JPA)的集成,并支持用于资源管理、数据访问对象实现和事务策略的本机 Hibernate。例如,对于 Hibernate,具有几个方便的 IOC 特性的一流支持,这些特性解决了许多典型的 Hibernate 集成问题。你可以通过依赖项注入来配置 OR(对象关系)映射工具的所有支持的特性。它们可以参与 Spring 的资源和事务管理,并且符合 Spring 的通用事务和 DAO 异常层次结构。推荐的集成风格是针对普通的 Hibernate 或 JPA API 对 DAO 进行编码。 + +Spring 在创建数据访问应用程序时,为你选择的 ORM 层增加了显著的增强。你可以根据需要利用尽可能多的集成支持,并且应该将这种集成工作与内部构建类似基础设施的成本和风险进行比较。无论采用何种技术,你都可以像使用库一样使用大量的 ORM 支持,因为所有的东西都被设计为一组可重用的 爪哇Bean。 Spring IOC 容器中的 ORM 促进了配置和部署。因此,本节中的大多数示例都显示了 Spring 容器内的配置。 + +使用 Spring 框架创建 ORM DAO 的好处包括: + +* **更容易的测试。** Spring 的 IOC 方法使得很容易交换 Hibernate `SessionFactory`实例、JDBC`DataSource`实例、事务管理器和映射对象实现(如果需要)的实现和配置位置。这进而使得隔离地测试与持久性相关的每一段代码变得容易得多。 + +* **常见的数据访问异常。** Spring 可以从你的 ORM 工具中包装异常,将它们从专有的(可能经过检查的)异常转换为通用的运行时“DataAccessException”层次结构。这个特性允许你仅在适当的层中处理大多数持久性异常(这些异常是不可恢复的),而不需要烦人的样板文件捕获、抛出和异常声明。你仍然可以根据需要捕获和处理异常。请记住,JDBC 异常(包括特定于 DB 的方言)也被转换为相同的层次结构,这意味着你可以在一致的编程模型中使用 JDBC 执行一些操作。 + +* **一般资源管理。** Spring 应用程序上下文可以处理 Hibernate `SessionFactory`实例、 JPA `EntityManagerFactory`实例、JDBC`DataSource`实例的位置和配置,以及其他相关资源。这使得这些价值观易于管理和改变。 Spring 提供了对持久性资源的高效、简单和安全的处理。例如,使用 Hibernate 的相关代码通常需要使用相同的 Hibernate `Session`,以确保效率和适当的事务处理。 Spring 通过将当前的`Session`通过 Hibernate `SessionFactory`公开,使得创建`Session`并将其透明地绑定到当前线程变得容易。因此, Spring 解决了 Hibernate 典型使用的许多慢性问题,适用于任何本地或 JTA 事务环境。 + +* **综合事务管理。**你可以使用声明性的、面向方面的编程( AOP)风格的方法拦截器包装你的 ORM 代码,或者通过 `@Transactional’注释,或者通过在 XML 配置文件中显式地配置事务 AOP 通知。在这两种情况下,事务语义和异常处理(回滚等)都是为你处理的。正如[资源和事务管理](#orm-resource-mngmnt)中所讨论的,你还可以交换各种事务管理器,而不会影响与 ORM 相关的代码。例如,你可以在本地事务和 JTA 之间进行交换,在这两个场景中都可以使用相同的完整服务(例如声明式事务)。此外,与 JDBC 相关的代码可以完全集成到用于执行 ORM 的代码中。这对于不适合 ORM 的数据访问(例如批处理和 BLOB 流)很有用,但仍然需要与 ORM 操作共享公共事务。 + +| |有关更全面的 ORM 支持,包括对替代数据库<br/>技术(如 MongoDB)的支持,你可能想查看[Spring Data](https://projects.spring.io/spring-data/)项目套件。如果你是<br/> JPA 用户,[Getting Started Accessing<br/>Data with JPA](https://spring.io/guides/gs/accessing-data-jpa/)中的[https://spring.io](https://spring.io)指南提供了一个很好的介绍。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 5.2.ORM 集成的一般考虑因素 + +本节重点介绍了适用于所有 ORM 技术的注意事项。[Hibernate](#orm-hibernate)部分提供了更多细节,并在一个具体的上下文中显示了这些特性和配置。 + +Spring 的 ORM 集成的主要目标是清晰的应用程序分层(使用任何数据访问和事务技术),并实现应用程序对象的松耦合——不再需要业务服务依赖于数据访问或事务策略,不再需要硬编码的资源查找,不再需要难以替换的单件件,没有更多的自定义服务注册中心。我们的目标是使用一种简单且一致的方法来连接应用程序对象,使它们尽可能地可重用且不依赖于容器。所有单独的数据访问功能都可以单独使用,但与 Spring 的应用程序上下文概念很好地集成在一起,提供了基于 XML 的配置和对普通 爪哇Bean 实例的交叉引用,而这些实例不需要 Spring 知道。 Spring 在典型的应用程序中,许多重要的对象是 爪哇Bean:数据访问模板、数据访问对象、事务管理器、使用数据访问对象的业务服务和事务管理器、Web 视图解析器、使用业务服务的 Web 控制器,等等。 + +#### 5.2.1.资源和事务管理 + +典型的业务应用程序充斥着重复的资源管理代码。许多项目试图发明自己的解决方案,有时为了编程方便而牺牲了对故障的正确处理。 Spring 提倡用于适当的资源处理的简单解决方案,即在 JDBC 的情况下通过模板化来实现 IOC,并为 ORM 技术应用 AOP 拦截器。 + +基础架构提供了正确的资源处理,并将特定的 API 异常适当地转换为未经检查的基础架构异常层次结构。 Spring 引入了一种 DAO 异常层次结构,适用于任何数据访问策略。对于直接 JDBC,`JdbcTemplate`中提到的`JdbcTemplate`类提供连接处理和`SQLException`到 `DataAccessException’层次结构的正确转换,包括将数据库特定的 SQL 错误代码转换为有意义的异常类。对于 ORM 技术,请参见[next section](#orm-exception-translation),了解如何获得相同的异常转换好处。 + +当涉及到事务管理时,`JdbcTemplate`类连接到 Spring 事务支持,并通过相应的 Spring 事务管理器支持 JTA 和 JDBC 事务。对于所支持的 ORM 技术, Spring 通过 Hibernate 和 JPA 事务管理器以及 JTA 支持提供 Hibernate 和 JPA 支持。有关事务支持的详细信息,请参见[事务管理](#transaction)章节。 + +#### 5.2.2.异常转换 + +在 DAO 中使用 Hibernate 或 JPA 时,必须决定如何处理持久性技术的本机异常类。根据技术的不同,DAO 抛出了`HibernateException`或`PersistenceException`的子类。这些异常都是运行时异常,不需要声明或捕获。你可能还需要处理“非法论题”和`IllegalStateException`。这意味着调用者只能将异常视为通常是致命的,除非他们希望依赖于持久性技术自身的异常结构。如果不将调用方与实现策略绑定,就不可能捕获特定的原因(例如乐观锁定失败)。这种权衡对于基于强 ORM 或不需要任何特殊异常处理(或两者兼而有之)的应用程序来说可能是可以接受的。然而, Spring 允许通过`@Repository`注释透明地应用异常翻译。下面的示例(一个用于 爪哇 配置,一个用于 XML 配置)展示了如何这样做: + +爪哇 + +``` +@Repository +public class ProductDaoImpl implements ProductDao { + + // class body here... + +} +``` + +Kotlin + +``` +@Repository +class ProductDaoImpl : ProductDao { + + // class body here... + +} +``` + +``` +<beans> + + <!-- Exception translation bean post processor --> + <bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor"/> + + <bean id="myProductDao" class="product.ProductDaoImpl"/> + +</beans> +``` + +后处理器会自动查找所有异常翻译器(`PersistenceExceptionTranslator`接口的实现),并建议所有标有 `@Repository’注释的 bean,以便发现的翻译器可以拦截并在抛出的异常上应用适当的翻译。 + +总之,你可以基于纯持久性技术的 API 和注释实现 DAO,同时仍然受益于 Spring 管理的事务、依赖注入和透明的异常转换(如果需要的话)到 Spring 的自定义异常层次结构。 + +### 5.3. Hibernate + +我们从在 Spring 环境中覆盖[Hibernate 5](https://hibernate.org/)开始,使用它来演示 Spring 对集成或映射程序所采用的方法。本节详细介绍了许多问题,并展示了 DAO 实现和事务划分的不同变体。这些模式中的大多数可以直接转换为所有其他受支持的 ORM 工具。本章后面的部分将介绍其他 ORM 技术,并展示简要的示例。 + +| |在 Spring Framework5.3 中, Spring 对于 Spring 的“HibernateJPavendorAdapter”以及对于原生的 Hibernate `SessionFactory`setup.<br/>对于新启动的应用程序,强烈建议使用 Hibernate orm5.4。,<br/>用于`HibernateJpaVendorAdapter`, Hibernate 搜索需要升级到 5.1 1.6。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 5.3.1.`SessionFactory` Spring 容器中的设置 + +为了避免将应用程序对象绑定到硬编码资源查找,可以将资源(例如 JDBC或 Hibernate )定义为 Spring 容器中的 bean。需要访问资源的应用程序对象通过 Bean 引用接收对此类预定义实例的引用,如[next section](#orm-hibernate-straight)中的 DAO 定义所示。 + +以下摘录自 XML 应用程序上下文定义,展示了如何在它上面设置一个 JDBC`DataSource`和一个 Hibernate `SessionFactory`: + +``` +<beans> + + <bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> + <property name="driverClassName" value="org.hsqldb.jdbcDriver"/> + <property name="url" value="jdbc:hsqldb:hsql://localhost:9001"/> + <property name="username" value="sa"/> + <property name="password" value=""/> + </bean> + + <bean id="mySessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean"> + <property name="dataSource" ref="myDataSource"/> + <property name="mappingResources"> + <list> + <value>product.hbm.xml</value> + </list> + </property> + <property name="hibernateProperties"> + <value> + hibernate.dialect=org.hibernate.dialect.HSQLDialect + </value> + </property> + </bean> + +</beans> +``` + +从本地的 Jakarta Commons DBCP`BasicDataSource`切换到位于 JNDI 的 `DataSource’(通常由应用程序服务器管理)只是一个配置问题,如下例所示: + +``` +<beans> + <jee:jndi-lookup id="myDataSource" jndi-name="java:comp/env/jdbc/myds"/> +</beans> +``` + +你还可以访问位于`SessionFactory`的 JNDI,使用 Spring 的 `jndiObjectFactoryBean`/`<jee:jndi-lookup>`来检索和公开它。然而,在 EJB 上下文之外,这种情况通常并不常见。 + +| |Spring 还提供了一个`LocalSessionFactoryBuilder`变体,将<br/>与`@Bean`样式配置和编程设置无缝集成在一起(不涉及`FactoryBean`)。<br/><br/>既`LocalSessionFactoryBean`又`LocalSessionFactoryBuilder`支持后台<br/>引导, Hibernate 初始化与给定的引导程序执行器上的应用程序<br/>引导程序线程并行运行(例如`SimpleAsyncTaskExecutor`)。<br/>在`LocalSessionFactoryBean`上,这可以通过`bootstrapExecutor`属性获得。在程序化的`LocalSessionFactoryBuilder`上,有一个重载的 `buildsessionFactory’方法,它接受一个 Bootstrap 执行器参数。<br/><br/>从 Spring Framework5.1 开始,这样的本机 Hibernate 设置还可以将用于标准 JPA 交互的[EntityManagerFactory]公开到本机 Hibernate 访问。<br/>详见[Native Hibernate Setup for JPA](#orm-jpa-hibernate)。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 5.3.2.基于普通 Hibernate API 实现 DAOS + +Hibernate 具有称为上下文会话的功能,其中 Hibernate 本身管理每个事务的当前`Session`。这大致相当于 Spring 的每笔交易同步一次 Hibernate `Session`。基于普通的 Hibernate API,相应的 DAO 实现类似于以下示例: + +Java + +``` +public class ProductDaoImpl implements ProductDao { + + private SessionFactory sessionFactory; + + public void setSessionFactory(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + } + + public Collection loadProductsByCategory(String category) { + return this.sessionFactory.getCurrentSession() + .createQuery("from test.Product product where product.category=?") + .setParameter(0, category) + .list(); + } +} +``` + +Kotlin + +``` +class ProductDaoImpl(private val sessionFactory: SessionFactory) : ProductDao { + + fun loadProductsByCategory(category: String): Collection<*> { + return sessionFactory.currentSession + .createQuery("from test.Product product where product.category=?") + .setParameter(0, category) + .list() + } +} +``` + +这种样式类似于 Hibernate 引用文档和示例的样式,只是在实例变量中持有`SessionFactory`。在 Hibernate 的 CaveaTemptor 示例应用程序中的老式`static``HibernateUtil`类之上,我们强烈推荐这样一种基于实例的设置。(一般来说,除非绝对必要,否则不要将任何资源保留在“静态”变量中。) + +前面的 DAO 示例遵循依赖注入模式。它非常适合 Spring IOC 容器,就像它与 Spring 的`HibernateTemplate`编码一样。你也可以在普通 Java 中设置这样的 DAO(例如,在单元测试中)。要这样做,实例化它,并使用所需的工厂引用调用`setSessionFactory(..)`。作为 Spring Bean 的定义,DAO 类似于以下内容: + +``` +<beans> + + <bean id="myProductDao" class="product.ProductDaoImpl"> + <property name="sessionFactory" ref="mySessionFactory"/> + </bean> + +</beans> +``` + +这种 DAO 样式的主要优点是它仅依赖于 Hibernate API。不需要任何 Spring 类的进口。从非侵犯性的角度来看,这是有吸引力的,并且对于开发人员来说可能感觉更自然。 + +然而,DAO 抛出了普通的`HibernateException`(未选中,因此不必声明或捕获),这意味着调用者只能将异常视为通常是致命的——除非他们希望依赖 Hibernate 自己的异常层次结构。如果不将调用方与实现策略绑定,就不可能捕获特定的原因(例如乐观锁定失败)。这种权衡对于基于强 Hibernate 的应用程序、不需要任何特殊的异常处理,或者两者兼而有之的应用程序来说都是可以接受的。 + +幸运的是, Spring 的`LocalSessionFactoryBean`支持 Hibernate 的 `SessionFactory.getCurrentSession()’方法用于任何 Spring 事务策略,返回当前 Spring-管理的事务`Session`,甚至使用 `HibernateTransactionManager’。该方法的标准行为仍然是返回当前与正在进行的 JTA 事务关联的`Session`(如果有的话)。无论你是使用 Spring 的“JTATRansactionManager”、EJB 容器管理事务,还是使用 JTA,这种行为都适用。 + +总之,你可以基于普通的 Hibernate API 实现 DAO,同时仍然能够参与 Spring 管理的事务。 + +#### 5.3.3.声明式事务划分 + +我们建议你使用 Spring 的声明式事务支持,它允许你用 AOP 事务拦截器替换 Java 代码中的显式事务划分 API 调用。你可以使用 Java 注释或 XML 在 Spring 容器中配置此事务拦截器。这种声明性事务功能使你能够使业务服务免于重复的事务划分代码,并专注于添加业务逻辑,这是应用程序的真正价值所在。 + +| |在你继续之前,我们强烈建议你阅读[声明式事务管理](#transaction-declarative),如果你还没有这样做。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------| + +你可以使用`@Transactional`注释对服务层进行注释,并指示 Spring 容器查找这些注释,并为这些注释的方法提供事务语义。下面的示例展示了如何做到这一点: + +Java + +``` +public class ProductServiceImpl implements ProductService { + + private ProductDao productDao; + + public void setProductDao(ProductDao productDao) { + this.productDao = productDao; + } + + @Transactional + public void increasePriceOfAllProductsInCategory(final String category) { + List productsToChange = this.productDao.loadProductsByCategory(category); + // ... + } + + @Transactional(readOnly = true) + public List<Product> findAllProducts() { + return this.productDao.findAllProducts(); + } +} +``` + +Kotlin + +``` +class ProductServiceImpl(private val productDao: ProductDao) : ProductService { + + @Transactional + fun increasePriceOfAllProductsInCategory(category: String) { + val productsToChange = productDao.loadProductsByCategory(category) + // ... + } + + @Transactional(readOnly = true) + fun findAllProducts() = productDao.findAllProducts() +} +``` + +在容器中,你需要设置`PlatformTransactionManager`实现(作为 Bean)和`<tx:annotation-driven/>`条目,在运行时选择进入`@Transactional`处理。下面的示例展示了如何做到这一点: + +``` +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:aop="http://www.springframework.org/schema/aop" + xmlns:tx="http://www.springframework.org/schema/tx" + xsi:schemaLocation=" + http://www.springframework.org/schema/beans + https://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/tx + https://www.springframework.org/schema/tx/spring-tx.xsd + http://www.springframework.org/schema/aop + https://www.springframework.org/schema/aop/spring-aop.xsd"> + + <!-- SessionFactory, DataSource, etc. omitted --> + + <bean id="transactionManager" + class="org.springframework.orm.hibernate5.HibernateTransactionManager"> + <property name="sessionFactory" ref="sessionFactory"/> + </bean> + + <tx:annotation-driven/> + + <bean id="myProductService" class="product.SimpleProductService"> + <property name="productDao" ref="myProductDao"/> + </bean> + +</beans> +``` + +#### 5.3.4.程序化事务划分 + +你可以在应用程序的较高级别上,在跨越任意数量操作的较低级别的数据访问服务之上,对事务进行划分。对周围业务服务的实现也不存在限制。它只需要一个 Spring“平台交易管理器”。同样,后者可以来自任何地方,但优选地通过`setTransactionManager(..)`方法作为 Bean 参考。此外,“productDAO”应该由`setProductDao(..)`方法设置。以下一对片段显示了 Spring 应用程序上下文中的事务管理器和业务服务定义,以及业务方法实现的示例: + +``` +<beans> + + <bean id="myTxManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager"> + <property name="sessionFactory" ref="mySessionFactory"/> + </bean> + + <bean id="myProductService" class="product.ProductServiceImpl"> + <property name="transactionManager" ref="myTxManager"/> + <property name="productDao" ref="myProductDao"/> + </bean> + +</beans> +``` + +Java + +``` +public class ProductServiceImpl implements ProductService { + + private TransactionTemplate transactionTemplate; + private ProductDao productDao; + + public void setTransactionManager(PlatformTransactionManager transactionManager) { + this.transactionTemplate = new TransactionTemplate(transactionManager); + } + + public void setProductDao(ProductDao productDao) { + this.productDao = productDao; + } + + public void increasePriceOfAllProductsInCategory(final String category) { + this.transactionTemplate.execute(new TransactionCallbackWithoutResult() { + public void doInTransactionWithoutResult(TransactionStatus status) { + List productsToChange = this.productDao.loadProductsByCategory(category); + // do the price increase... + } + }); + } +} +``` + +Kotlin + +``` +class ProductServiceImpl(transactionManager: PlatformTransactionManager, + private val productDao: ProductDao) : ProductService { + + private val transactionTemplate = TransactionTemplate(transactionManager) + + fun increasePriceOfAllProductsInCategory(category: String) { + transactionTemplate.execute { + val productsToChange = productDao.loadProductsByCategory(category) + // do the price increase... + } + } +} +``` + +Spring 的`TransactionInterceptor`允许使用回调码抛出任何选中的应用程序异常,而`TransactionTemplate`仅限于回调中未选中的异常。`TransactionTemplate`在未检查应用程序异常的情况下,或者如果应用程序将事务标记为只回滚(通过设置`TransactionStatus`),则触发回滚。默认情况下,`TransactionInterceptor`的行为与此相同,但每个方法都允许可配置的回滚策略。 + +#### 5.3.5.事务管理策略 + +`TransactionTemplate`和`TransactionInterceptor`都将实际的事务处理委托给一个`PlatformTransactionManager`实例(它可以是一个 `HibernateTransactionManager’(对于单个 Hibernate `SessionFactory`),通过使用一个 `threadlocal’`Session`在引擎盖下)或一个`JtaTransactionManager`(委托给容器的 JTA 子系统)用于 Hibernate 应用程序。你甚至可以使用自定义的“Platform TransactionManager”实现。 Hibernate 从本机事务管理切换到 JTA(例如,当你的应用程序的某些部署面临分布式事务需求时)只是一个配置问题。你可以用 Spring 的 JTA 事务实现替换 Hibernate 事务管理器。事务划分和数据访问代码都可以在不做任何更改的情况下工作,因为它们使用了通用事务管理 API。 + +对于跨多个 Hibernate 会话工厂的分布式事务,你可以将“JTATRansactionManager”作为事务策略与多个“localSessionFactoryBean”定义结合起来。然后,每个 DAO 获得一个特定的`SessionFactory`引用,该引用被传递到其相应的 Bean 属性中。如果所有底层 JDBC 数据源都是事务容器数据源,那么业务服务可以在不需要特别注意的情况下在任意数量的 DAO 和任意数量的会话工厂之间划分事务,只要它使用`JtaTransactionManager`作为策略。 + +`HibernateTransactionManager`和`JtaTransactionManager`都允许使用 Hibernate 进行适当的 JVM 级缓存处理,而不需要容器特定的事务管理器查找或 JCA 连接器(如果不使用 EJB 发起事务)。 + +`HibernateTransactionManager`可以将 Hibernate JDBC`Connection`导出为特定`DataSource`的普通 JDBC 访问代码。如果只访问一个数据库,这种能力允许在完全不使用 JTA 的情况下使用混合 Hibernate 和 JDBC 数据访问的高级事务划分。`HibernateTransactionManager`如果你通过 `localSessionFactoryBean’类的`dataSource`属性设置了带有`DataSource`的传入的 `SessionFactory’事务,则自动将 Hibernate 事务公开为 JDBC 事务。或者,你可以通过`HibernateTransactionManager`类的 `DataSource’属性显式地指定事务应该公开的 `DataSource’。 + +#### 5.3.6.比较容器管理的资源和本地定义的资源 + +你可以在容器管理的 JNDI`SessionFactory`和本地定义的 JNDI 之间进行切换,而无需更改一行应用程序代码。是将资源定义保留在容器中,还是保留在应用程序的本地中,主要取决于你使用的事务策略。与 Spring 定义的本地“SessionFactory”相比,手动注册的 JNDI不提供任何好处。通过 Hibernate 的 JCA 连接器部署`SessionFactory`提供了参与 Java EE 服务器的管理基础架构的附加价值,但不会在此之外增加实际价值。 + +Spring 的事务支持不绑定到容器。当使用 JTA 以外的任何策略进行配置时,事务支持也可以在独立或测试环境中工作。特别是在单数据库事务的典型情况下, Spring 的单资源本地事务支持是 JTA 的一种轻量级和强大的替代方案。当使用本地 EJB 无状态会话 bean 来驱动事务时,你既依赖于 EJB 容器,也依赖于 JTA,即使你只访问一个数据库,并且仅使用无状态会话 bean 通过容器管理的事务来提供声明性事务。以编程方式直接使用 JTA 还需要一个 Java EE 环境。JTA 并不只涉及 JTA 本身和 JNDI`DataSource`实例方面的容器依赖关系。对于非 Spring、JTA 驱动的 Hibernate 事务,你必须使用 Hibernate JCA 连接器或额外的 Hibernate 事务代码,并配置用于适当的 JVM 级缓存的 `TransactionManagerLookup’。 + +Spring-驱动的事务可以与本地定义的 Hibernate `SessionFactory’一起工作,就像它们与本地 JDBC一起工作一样,只要它们访问单个数据库。因此,当你有分布式事务需求时,你只需要使用 Spring 的 JTA 事务策略。JCA 连接器需要特定于容器的部署步骤,并且(显然)首先需要 JCA 支持。与部署具有本地资源定义和 Spring 驱动事务的简单 Web 应用程序相比,这种配置需要更多的工作。此外,如果你使用 WebLogic Express(它不提供 JCA),则通常需要容器的 Enterprise 版本。 Spring 具有跨越单个数据库的本地资源和事务的应用程序在任何 Java EE Web 容器(不包括 JTA、JCA 或 EJB)中工作,例如 Tomcat、Resin,甚至是普通的 Jetty。此外,你可以轻松地在桌面应用程序或测试套件中重用这样的中间层。 + +考虑到所有因素,如果不使用 EJB,请坚持使用本地`SessionFactory`设置和 Spring 的`HibernateTransactionManager`或`JtaTransactionManager`。你获得了所有的好处,包括适当的事务 JVM 级缓存和分布式事务,而不会带来容器部署的不便。 Hibernate `SessionFactory`通过 JCA 连接器进行的 JNDI 注册仅在与 EJB 结合使用时才会增加价值。 + +#### 5.3.7.带有 Hibernate 的虚假应用程序服务器警告 + +在一些具有非常严格的`XADataSource`实现的 JTA 环境中(目前是一些 WebLogic Server 和 WebSphere 版本),当 Hibernate 在不考虑该环境的 JTA 事务管理器的情况下进行配置时,可能会在应用程序服务器日志中显示虚假的警告或异常。这些警告或异常表示正在访问的连接不再有效或 JDBC 访问不再有效,这可能是因为事务不再活动。举个例子,这里有一个来自 WebLogic 的实际例外: + +``` +java.sql.SQLException: The transaction is no longer active - status: 'Committed'. No +further JDBC access is allowed within this transaction. +``` + +另一个常见的问题是 JTA 事务之后的连接泄漏, Hibernate 会话(以及潜在的底层 JDBC 连接)无法正确关闭。 + +你可以通过使 Hibernate 了解 JTA 事务管理器来解决此类问题,JTA 事务管理器与 Spring 同步到该事务管理器。你有两个选择: + +* 将你的 Spring `JtaTransactionManager` Bean 传递到你的 Hibernate 设置中。最简单的方法是 Bean 引用`jtaTransactionManager`属性中的“localSessionFactoryBean” Bean(参见[Hibernate Transaction Setup](#transaction-strategies-hibernate))。 Spring 然后使相应的 JTA 策略可用于 Hibernate。 + +* 你还可以在`LocalSessionFactoryBean`上的“HibernateProperties”中显式地配置 Hibernate 的 JTA 相关属性,特别是“ Hibernate.transaction.coordinator\_class”、“ Hibernate.connection.handling\_mode”和潜在的“ Hibernate.transaction.jta.platform”(有关这些属性的详细信息,请参见 Hibernate 的手册)。 + +本节的其余部分描述了在 Hibernate 意识到 JTA的情况下发生的事件的顺序。 + +Hibernate 未配置任何对 JTA 事务管理器的了解时,当 JTA 事务提交时发生以下事件: + +* 提交 JTA 事务。 + +* Spring 的`JtaTransactionManager`与 JTA 事务同步,因此 JTA 事务管理器通过`afterCompletion`回调回调它。 + +* 在其他活动中,这种同步可以通过 Hibernate 的`afterTransactionCompletion`回调(用于清除 Hibernate 缓存)触发 Spring 到 Hibernate 的回调,然后在 Hibernate 会话上执行显式的`close()`调用,这会导致 Hibernate 尝试`close()`JDBC 连接。 + +* 在某些环境中,这个`Connection.close()`调用会触发警告或错误,因为应用程序服务器不再认为`Connection`是可用的,因为事务已经提交。 + +Hibernate 配置了对 JTA 事务管理器的感知时,当 JTA 事务提交时发生以下事件: + +* JTA 事务已准备好提交。 + +* Spring 的`JtaTransactionManager`与 JTA 事务同步,因此 JTA 事务管理器通过`beforeCompletion`回调回调该事务。 + +* Spring 意识到 Hibernate 本身与 JTA 事务同步,并且其行为与在前面的场景中不同。特别是,它与 Hibernate 的事务性资源管理保持一致。 + +* 提交 JTA 事务。 + +* Hibernate 是同步到 JTA 事务的,因此该事务通过 JTA 事务管理器的`afterCompletion`回调被调回,并且可以正确地清除其缓存。 + +### 5.4. JPA + +Spring JPA,在包下可用,以类似于与 Hibernate 集成的方式提供对的全面支持,同时意识到底层实现,以便提供额外的特征。 + +#### 5.4.1.在 Spring 环境中用于 JPA 设置的三个选项 + +Spring JPA 支持提供了设置 JPA 的三种方式,其被应用程序用于获得实体管理器。 + +* [使用`LocalEntityManagerFactoryBean`](#orm-jpa-setup-lemfb) + +* [从 JNDI 获得 EntityManagerFactory](#orm-jpa-setup-jndi) + +* [使用`LocalContainerEntityManagerFactoryBean`](#orm-jpa-setup-lcemfb) + +##### Using `LocalEntityManagerFactoryBean` + +你只能在简单的部署环境中使用此选项,例如独立应用程序和集成测试。 + +`LocalEntityManagerFactoryBean`创建了一个`EntityManagerFactory`,适用于应用程序仅使用 JPA 进行数据访问的简单部署环境。工厂 Bean 使用 JPA `PersistenceProvider`自动检测机制(根据 JPA 的 Java SE 引导),并且在大多数情况下,只需要指定持久性单元名称。下面的 XML 示例配置了这样的 Bean: + +``` +<beans> + <bean id="myEmf" class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean"> + <property name="persistenceUnitName" value="myPersistenceUnit"/> + </bean> +</beans> +``` + +JPA 部署的这种形式是最简单和最有限的。你无法引用现有的 JDBC`DataSource` Bean 定义,并且不存在对全局事务的支持。此外,持久类的编织(字节代码转换)是特定于提供者的,通常需要在启动时指定特定的 JVM 代理。此选项仅适用于 JPA 规范为其设计的独立应用程序和测试环境。 + +##### 从 JNDI 获得 EntityManagerFactory + +在部署到 Java EE 服务器时可以使用此选项。检查你的服务器的文档,了解如何将自定义 JPA 提供程序部署到你的服务器中,从而允许使用不同于服务器默认值的提供程序。 + +从 JNDI(例如在 Java EE 环境中)获得`EntityManagerFactory`是更改 XML 配置的问题,如下例所示: + +``` +<beans> + <jee:jndi-lookup id="myEmf" jndi-name="persistence/myPersistenceUnit"/> +</beans> +``` + +此操作假定了标准的 Java EE 引导。Java EE 服务器自动检测持久化单元(实际上是应用程序 JAR 中的`META-INF/persistence.xml`文件)和 Java EE 部署描述符中的“持久化-单元-ref”条目(例如,“web.xml”),并为这些持久化单元定义环境命名上下文位置。 + +在这样的场景中,整个持久性单元部署,包括持久性类的编织(字节代码转换),由 Java EE 服务器决定。JDBC“数据源”是通过`META-INF/persistence.xml`文件中的一个 JNDI 位置定义的。“EntityManager”事务与服务器的 JTA 子系统集成在一起。 Spring 仅使用获得的`EntityManagerFactory`,通过依赖注入将其传递给应用程序对象,并为持久性单元管理事务(通常通过`JtaTransactionManager`)。 + +如果在同一个应用程序中使用多个持久化单元,则此类 JNDI 检索的持久化单元的 Bean 名称应与应用程序用于引用它们的持久化单元名称相匹配(例如,在`@PersistenceUnit`和 `@persistentencecontext’注释中)。 + +##### Using `LocalContainerEntityManagerFactoryBean` + +你可以在基于 Spring 的应用程序环境中使用此选项来获得完整的 JPA 功能。这包括诸如 Tomcat 之类的 Web 容器、独立应用程序以及具有复杂持久性需求的集成测试。 + +| |如果你想要具体地配置一个 Hibernate 设置,那么一个直接的替代方案<br/>是设置一个本机 Hibernate `LocalSessionFactoryBean`,而不是一个普通的 JPA `localContaineRentityManageryBean’,让它与 JPA 访问代码<br/>以及本机 Hibernate 访问代码进行交互。<br/>有关详细信息,请参见[Native Hibernate setup for JPA interaction](#orm-jpa-hibernate)。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +`LocalContainerEntityManagerFactoryBean`提供了对“EntityManagerFactory”配置的完全控制,适用于需要细粒度定制的环境。`LocalContainerEntityManagerFactoryBean`基于`persistence.xml`文件、提供的`dataSourceLookup`策略和指定的`loadTimeWeaver`创建一个`PersistenceUnitInfo`实例。因此,可以使用 JNDI 之外的自定义数据源并控制编织过程。下面的示例显示了“localContainerentyManagerFactoryBean”的典型定义 Bean: + +``` +<beans> + <bean id="myEmf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> + <property name="dataSource" ref="someDataSource"/> + <property name="loadTimeWeaver"> + <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver"/> + </property> + </bean> +</beans> +``` + +下面的示例显示了一个典型的`persistence.xml`文件: + +``` +<persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0"> + <persistence-unit name="myUnit" transaction-type="RESOURCE_LOCAL"> + <mapping-file>META-INF/orm.xml</mapping-file> + <exclude-unlisted-classes/> + </persistence-unit> +</persistence> +``` + +| |`<exclude-unlisted-classes/>`快捷方式表示不应该对<br/>带注释的实体类进行扫描。显式的’true’值<br/>(`<exclude-unlisted-classes>true</exclude-unlisted-classes/>`)也表示没有扫描。`<exclude-unlisted-classes>false</exclude-unlisted-classes/>` 确实会触发扫描。<br/>但是,如果你希望发生实体类扫描,我们建议省略`exclude-unlisted-classes`元素<br/>。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +JPA 使用`LocalContainerEntityManagerFactoryBean`是最强大的设置选项,允许在应用程序中进行灵活的本地配置。它支持到现有 JDBC`DataSource`的链接,支持本地和全局事务,以此类推。然而,它也对运行时环境提出了要求,例如,如果持久性提供程序要求进行字节码转换,则需要一个具有编织能力的类装入器。 + +此选项可能与 Java EE 服务器的内置 JPA 功能相冲突。在完整的 Java EE 环境中,考虑从 JNDI 获得`EntityManagerFactory`。或者,在你的“localContaineRentyManagerFactoryBean”定义(例如,meta-inf/my-Persistence.xml)上指定一个自定义<gtr="1857"/>,并在应用程序 jar 文件中仅包含带有该名称的描述符。因为 Java EE 服务器只查找默认的 `meta-inf/persistence.xml’文件,所以它忽略了这些自定义的持久性单元,因此避免了与 Spring 驱动的 JPA 预先设置的冲突。(例如,这适用于树脂 3.1) + +什么时候需要进行负载-时间编织? + +并不是所有 JPA 提供者都需要 JVM 代理。 Hibernate 是一个不这样做的例子。如果你的提供者不需要代理,或者你有其他的选择,例如在构建时通过自定义编译器或 Ant 任务应用增强,那么你不应该使用加载时 Weaver。 + +`LoadTimeWeaver`接口是 Spring 提供的类,它允许以特定的方式插入 JPA `classtransformer’实例,这取决于环境是 Web 容器还是应用服务器。将`ClassTransformers`通过[agent](https://docs.oracle.com/javase/6/docs/api/java/lang/instrument/package-summary.html)挂钩通常效率不高。代理针对整个虚拟机工作,并检查加载的每个类,这在生产服务器环境中通常是不希望的。 + +Spring 为各种环境提供了许多`LoadTimeWeaver`实现,使得`ClassTransformer`实例仅适用于每个类装入器,而不适用于每个 VM。 + +有关`LoadTimeWeaver`实现及其设置的更多了解,请参见 AOP 章中的[Spring configuration](core.html#aop-aj-ltw-spring),这些实现及其设置可以是通用的,也可以是针对各种平台(例如 Tomcat、JBoss 和 WebSphere)定制的。 + +如[Spring configuration](core.html#aop-aj-ltw-spring)中所述,你可以通过使用`@EnableLoadTimeWeaving`注释或 `context:load-time-weaver`xml 元素来配置上下文范围的`LoadTimeWeaver`。所有 JPA `LocalContainerEntityManagerFactoryBean`实例都会自动拾取这样的全局织布器。下面的示例展示了设置加载时 Weaver、交付平台的自动检测(例如 Tomcat 的可编织的类装入器或 Spring 的 JVM 代理)以及将 Weaver 自动传播到所有可感知 Weaver 的 bean 的首选方法: + +``` +<context:load-time-weaver/> +<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> + ... +</bean> +``` + +但是,如果需要,你可以通过“loadTimeWeaver”属性手动指定一个专用的 Weaver,如下例所示: + +``` +<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> + <property name="loadTimeWeaver"> + <bean class="org.springframework.instrument.classloading.ReflectiveLoadTimeWeaver"/> + </property> +</bean> +``` + +无论 LTW 如何配置,通过使用该技术, JPA 依赖于检测的应用程序可以在目标平台(例如 Tomcat)中运行,而不需要代理。当托管应用程序依赖于不同的实现方式时,这一点尤其重要,因为 JPA 变压器仅在类装入器级别上应用,并且因此彼此隔离。 + +##### 处理多个持久性单元 + +对于依赖于多个持久性单元位置的应用程序(例如,存储在 Classpath 中的各种 JAR 中), Spring 提供了`PersistenceUnitManager`来充当中心存储库并避免持久性单元的发现过程,这可能是昂贵的。默认实现允许指定多个位置。这些位置会被解析,然后通过持久性单元名称检索。(默认情况下,在 Classpath 中搜索`META-INF/persistence.xml`文件。)以下示例配置了多个位置: + +``` +<bean id="pum" class="org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager"> + <property name="persistenceXmlLocations"> + <list> + <value>org/springframework/orm/jpa/domain/persistence-multi.xml</value> + <value>classpath:/my/package/**/custom-persistence.xml</value> + <value>classpath*:META-INF/persistence.xml</value> + </list> + </property> + <property name="dataSources"> + <map> + <entry key="localDataSource" value-ref="local-db"/> + <entry key="remoteDataSource" value-ref="remote-db"/> + </map> + </property> + <!-- if no datasource is specified, use this one --> + <property name="defaultDataSource" ref="remoteDataSource"/> +</bean> + +<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> + <property name="persistenceUnitManager" ref="pum"/> + <property name="persistenceUnitName" value="myCustomUnit"/> +</bean> +``` + +默认的实现允许对`PersistenceUnitInfo`实例(在它们被馈送到 JPA 提供程序之前)进行定制,可以是声明式的(通过其影响所有托管单元的属性),也可以是程序化的(通过允许持久性单元选择的 `PersistenceUnitPostProcessor’)。如果没有指定“PersistenceUnitManager”,则由“LocalContaineRentyManagerFactoryBean”在内部创建和使用。 + +##### 背景引导 + +`LocalContainerEntityManagerFactoryBean`支持通过`bootstrapExecutor`属性进行后台引导,如下例所示: + +``` +<bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> + <property name="bootstrapExecutor"> + <bean class="org.springframework.core.task.SimpleAsyncTaskExecutor"/> + </property> +</bean> +``` + +JPA 提供程序的实际引导将交给指定的执行器,然后并行地运行给应用程序引导线程。公开的`EntityManagerFactory`代理可以被注入到其他应用程序组件中,甚至能够响应“EntityManagerFactoryInfo”配置检查。然而,一旦实际的 JPA 提供程序被其他组件访问(例如,调用`createEntityManager`),这些调用就会阻塞,直到后台引导完成为止。特别是,当你使用 Spring 数据 JPA 时,请确保也为其存储库设置延迟引导。 + +#### 5.4.2.基于 JPA 实现 DAO:`EntityManagerFactory`和`EntityManager` + +| |虽然`EntityManagerFactory`实例是线程安全的,但`EntityManager`实例不是。注入的 JPA `EntityManager`的行为类似于从`EntityManager`应用服务器的 JNDI 环境中获取的`EntityManager`,如 JPA 规范所定义的。它将<br/>所有调用委托给当前事务`EntityManager`(如果有的话)。否则,它将返回<br/>到每个操作新创建的`EntityManager`,实际上使其使用线程安全。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +通过使用注入的`EntityManagerFactory`或`EntityManager`,可以在没有任何 Spring 依赖关系的情况下针对普通 JPA 编写代码。 Spring 如果启用了`PersistenceAnnotationBeanPostProcessor`,则可以在字段和方法级别上理解 @persistenceUnit 和`@PersistenceContext`注释。下面的示例显示了使用`@PersistenceUnit`注释的普通 JPA DAO 实现: + +Java + +``` +public class ProductDaoImpl implements ProductDao { + + private EntityManagerFactory emf; + + @PersistenceUnit + public void setEntityManagerFactory(EntityManagerFactory emf) { + this.emf = emf; + } + + public Collection loadProductsByCategory(String category) { + EntityManager em = this.emf.createEntityManager(); + try { + Query query = em.createQuery("from Product as p where p.category = ?1"); + query.setParameter(1, category); + return query.getResultList(); + } + finally { + if (em != null) { + em.close(); + } + } + } +} +``` + +Kotlin + +``` +class ProductDaoImpl : ProductDao { + + private lateinit var emf: EntityManagerFactory + + @PersistenceUnit + fun setEntityManagerFactory(emf: EntityManagerFactory) { + this.emf = emf + } + + fun loadProductsByCategory(category: String): Collection<*> { + val em = this.emf.createEntityManager() + val query = em.createQuery("from Product as p where p.category = ?1"); + query.setParameter(1, category); + return query.resultList; + } +} +``` + +前面的 DAO 对 Spring 没有依赖性,并且仍然很好地适合 Spring 应用程序上下文。此外,DAO 利用注释的优点来要求注入缺省的`EntityManagerFactory`,如下面的示例 Bean 中的定义所示: + +``` +<beans> + + <!-- bean post-processor for JPA annotations --> + <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor"/> + + <bean id="myProductDao" class="product.ProductDaoImpl"/> + +</beans> +``` + +作为显式定义`PersistenceAnnotationBeanPostProcessor`的替代方法,可以考虑在应用程序上下文配置中使用 Spring `context:annotation-config`XML 元素。这样做会自动注册用于基于注释的配置的所有 Spring 标准后处理程序,包括“CommonAnnotationBeanPostProcessor”等。 + +考虑以下示例: + +``` +<beans> + + <!-- post-processors for all standard config annotations --> + <context:annotation-config/> + + <bean id="myProductDao" class="product.ProductDaoImpl"/> + +</beans> +``` + +这种 DAO 的主要问题是,它总是通过工厂创建一个新的`EntityManager`。可以通过请求注入事务`EntityManager`(也称为“共享 EntityManager”,因为它是实际事务 EntityManager 的共享、线程安全代理)而不是工厂来避免这种情况。下面的示例展示了如何做到这一点: + +Java + +``` +public class ProductDaoImpl implements ProductDao { + + @PersistenceContext + private EntityManager em; + + public Collection loadProductsByCategory(String category) { + Query query = em.createQuery("from Product as p where p.category = :category"); + query.setParameter("category", category); + return query.getResultList(); + } +} +``` + +Kotlin + +``` +class ProductDaoImpl : ProductDao { + + @PersistenceContext + private lateinit var em: EntityManager + + fun loadProductsByCategory(category: String): Collection<*> { + val query = em.createQuery("from Product as p where p.category = :category") + query.setParameter("category", category) + return query.resultList + } +} +``` + +`@PersistenceContext`注释具有一个名为`type`的可选属性,该属性缺省为 `persistentecontextype.transaction’。你可以使用此默认值来接收共享的“EntityManager”代理。另一种选择,`PersistenceContextType.EXTENDED`,则是完全不同的事情。这导致了所谓的扩展`EntityManager`,它不是线程安全的,因此,不能在并发访问的组件中使用,例如 Spring 管理的单例 Bean。扩展的`EntityManager`实例只应该在有状态组件中使用,例如,这些组件驻留在会话中,而 `entityManager’的生命周期不绑定到当前事务,而是完全由应用程序决定。 + +方法和场级注入 + +你可以在类中的字段或方法上应用指示依赖注入的注释(例如`@PersistenceUnit`和 `@PersistenceConText`)——因此产生了表达式“方法级别的注入”和“字段级别的注入”。字段级别的注释简洁且易于使用,而方法级别的注释允许对注入的依赖项进行进一步处理。在这两种情况下,成员可见性(公共的、受保护的或私有的)并不重要。 + +那么类级别的注释呢? + +在 Java EE 平台上,它们用于依赖性声明,而不是用于资源注入。 + +注入的`EntityManager`是 Spring-管理的(了解正在进行的事务)。尽管新的 DAO 实现使用了方法级注入的`EntityManager`而不是`EntityManagerFactory`,但由于注释的使用,不需要对应用程序上下文 XML 进行更改。 + +这种 DAO 风格的主要优点是它仅依赖于 Java 持久性 API。不需要输入任何 Spring 类。此外,如 JPA 注释所理解的,注入由 Spring 容器自动应用。从非侵犯性的角度来看,这是很有吸引力的,并且对于 JPA 开发人员来说可能会感觉更自然。 + +#### 5.4.3. Spring-驱动的 JPA 交易 + +| |如果你还没有<br/>,我们强烈建议你阅读[声明式事务管理](#transaction-declarative),以获得 Spring 的声明性事务支持的更详细的内容。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +JPA 的推荐策略是通过 JPA 的本机事务支持进行本地事务。 Spring 的`JpaTransactionManager`针对任何常规的 JDBC 连接池(不需要 XA),提供了从本地 JDBC 事务中已知的许多功能(例如特定于事务的隔离级别和资源级别的只读优化)。 + +Spring JPA 还允许配置的`JpaTransactionManager`将 JPA 事务公开到访问相同`DataSource`的 JDBC 访问代码,前提是注册的 `JPADianquence’支持对底层 JDBC`Connection`的检索。 Spring 为 EclipseLink 和 Hibernate JPA 实现方式提供了方言。有关`JpaDialect`机制的详细信息,请参见[next section](#orm-jpa-dialect)。 + +| |作为一种直接的替代方案, Spring 的本机`HibernateTransactionManager`能够<br/>与 JPA 访问代码进行交互,适应多个 Hibernate 细节并提供<br/>JDBC 交互。这与`LocalSessionFactoryBean`设置结合在一起特别有意义。详见[Native Hibernate Setup for JPA Interaction](#orm-jpa-hibernate)。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 5.4.4.理解`JpaDialect`和`JpaVendorAdapter` + +作为一项高级功能,`JpaTransactionManager`和 `AbstractEntityManagerFactoryBean’的子类允许将一个自定义`JpaDialect`传递到 `JPADianquence’ Bean 属性中。一个`JpaDialect`实现可以实现 Spring 支持的以下高级特性,通常是以特定于供应商的方式实现的: + +* 应用特定的事务语义(例如自定义隔离级别或事务超时) + +* 检索事务性 JDBC`Connection`(用于暴露于基于 JDBC 的 DAO) + +* 将`PersistenceExceptions`高级翻译为 Spring `DataAccessExceptions` + +这对于特殊的事务语义和异常的高级转换特别有价值。默认实现不提供任何特殊功能,如果需要前面列出的功能,则必须指定适当的方言。 + +| |作为主要针对 Spring 的全功能“LocalContaineRentyManagerFactoryBean”设置的更广泛的提供者适配工具,`JpaVendorAdapter`将<br/>的<br/>功能与其他提供者特定的默认值结合在一起。指定一个 HibernateJPavendorAdapter 或`EclipseLinkJpaVendorAdapter`是分别为 Hibernate 或 EclipseLink、<br/>自动配置`EntityManagerFactory`设置的最方便的<br/>方式。请注意,这些提供程序适配器主要设计用于<br/> Spring 驱动的事务管理(即,用于`JpaTransactionManager`)。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +参见[`JpaDialect`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/orm/jpa/JpaDialect.html)和[JPavendorAdapter](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/orm/jpa/JpaVendorAdapter.html)Javadoc,以了解其操作的更多细节,以及在 Spring 的 JPA 支持中如何使用它们。 + +#### 5.4.5.使用 JTA 事务管理设置 JPA + +作为`JpaTransactionManager`的替代方案, Spring 还允许通过 JTA 进行多资源事务协调,可以是在 Java EE 环境中,也可以是使用独立的事务协调器,例如 Atomikos。除了选择 Spring 的“jtaTransactionManager”而不是`JpaTransactionManager`,你还需要采取一些进一步的步骤: + +* 底层的 JDBC 连接池需要具有 XA 功能,并与事务协调器集成在一起。在 Java EE 环境中,这通常很简单,通过 JNDI 公开另一种`DataSource`。有关详细信息,请参阅你的应用程序服务器文档。类似地,独立的事务协调器通常带有特殊的 XA 集成`DataSource`变体。再次,检查其文档。 + +* 需要为 JTA 配置 JPA `EntityManagerFactory`设置。这是特定于提供者的,通常通过在`LocalContainerEntityManagerFactoryBean`上指定为`jpaProperties`的特殊属性。在 Hibernate 的情况下,这些属性甚至是版本特定的。有关详细信息,请参阅你的 Hibernate 文档。 + +* Spring 的`HibernateJpaVendorAdapter`强制执行某些面向 Spring 的默认值,例如连接释放模式,`on-close`,该模式与 Hibernate 自己在 Hibernate 5.0 中的默认值相匹配,但在 Hibernate 5.1+ 中不再匹配。对于 JTA 设置,请确保将你的持久性单元事务类型声明为“JTA”。或者,将 Hibernate 5.2 的 ` Hibernate.connection.handling_mode` 属性设置为 `delayed_acquisition_and_release_after_statement’,以恢复 Hibernate 自己的默认值。相关注解见[Spurious Application Server Warnings with Hibernate](#orm-hibernate-invalid-jdbc-access-error)。 + +* 或者,考虑从应用程序服务器本身获得`EntityManagerFactory`(即通过 JNDI 查找,而不是本地声明的 localContainerentyManagerFactoryBean`)。服务器提供的`EntityManagerFactory`在你的服务器配置中可能需要特殊的定义(使得部署不那么可移植),但它是为服务器的 JTA 环境设置的。 + +#### 5.4.6.用于 JPA 交互的本机 Hibernate 设置和本机 Hibernate 事务 + +结合`HibernateTransactionManager`的本机`LocalSessionFactoryBean`设置允许与`@PersistenceContext`和其他 JPA 访问代码进行交互。一个 Hibernate `SessionFactory’本地实现了 JPA 的`EntityManagerFactory`接口,而一个 Hibernate `Session`句柄本地实现了一个 JPA `EntityManager`。 Spring 的 JPA 支持设施自动检测本机 Hibernate 会话。 + +因此,在许多场景中,这样的本机 Hibernate 设置可以替代标准的 JPA `localContaineRentyManagerFactoryBean’和<gtR="1957"/>组合,允许在相同的本地事务中与<gtR="1958"/>(以及<gtR="1959"/>)旁边的<gtR="1960"/>进行交互。这样的设置还提供了更强的 Hibernate 集成和更多的配置灵活性,因为它不受 JPA 引导契约的约束。 + +在这样的场景中,你不需要`HibernateJpaVendorAdapter`配置,因为 Spring 的本机 Hibernate 设置提供了更多的功能(例如,自定义 Hibernate Integrator 设置、 Hibernate 5.3 Bean 容器集成,以及针对只读事务的更强优化)。最后但并非最不重要的是,你还可以通过`LocalSessionFactoryBuilder`表示本机 Hibernate 设置,与`@Bean`样式配置无缝集成(不涉及`FactoryBean`)。 + +| |`LocalSessionFactoryBean`和`LocalSessionFactoryBuilder`支持背景<br/>引导,就像 JPA `LocalContainerEntityManagerFactoryBean`所做的那样。<br/>参见[背景引导](#orm-jpa-setup-background)介绍。<br/><br/>on`LocalSessionFactoryBean`,这可以通过`bootstrapExecutor`属性获得。在程序化的`LocalSessionFactoryBuilder`上,重载的 `buildsessionFactory’方法需要一个引导程序执行器参数。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## 6. 使用对象-XML 映射器对 XML 进行编组 + +### 6.1.导言 + +本章介绍 Spring 的对象-XML 映射支持。对象-XML 映射(Object-XML mapping,简称 O-X 映射)是将 XML 文档转换为对象和从对象转换的行为。这种转换过程也称为 XML 编组,或 XML 序列化。本章交替使用这些术语。 + +在 O-X 映射领域中,编组器负责将对象(图)序列化为 XML。以类似的方式,反编组器将 XML 反序列化为一个对象图。这个 XML 可以采取 DOM 文档、输入或输出流或 SAX 处理程序的形式。 + +针对你的 O/X 映射需求使用 Spring 的一些好处是: + +* [易于配置](#oxm-ease-of-configuration) + +* [一致的接口](#oxm-consistent-interfaces) + +* [一致的异常层次结构](#oxm-consistent-exception-hierarchy) + +#### 6.1.1.易于配置 + +Spring 的 Bean 工厂使得很容易配置编组器,而不需要构建 JAXB 上下文、JiBX 绑定工厂等。你可以像在你的应用程序上下文中的任何其他 Bean 一样配置编组器。此外,基于 XML 名称空间的配置可用于许多编组器,这使得配置更加简单。 + +#### 6.1.2.一致的接口 + +Spring 的 O-X 映射通过两个全局接口进行操作:[`Marshaller`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/oxm/Marshaller.html)和[`Unmarshaller`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/oxm/Unmarshaller.html)。这些抽象允许你相对轻松地切换 O-X 映射框架,只需对进行编组的类进行很少或根本不需要更改。这种方法还有一个额外的好处,那就是可以使用混合匹配的方法(例如,使用 JAXB 和 XStream 执行一些编组)以非侵入性的方式进行 XML 编组,从而使你能够利用每种技术的优势。 + +#### 6.1.3.一致的异常层次结构 + +Spring 提供了从底层 O-X 映射工具的异常到其自身的异常层次结构的转换,并将`XmlMappingException`作为根异常。这些运行时异常会对原始异常进行包装,这样就不会丢失任何信息。 + +### 6.2.`Marshaller`和`Unmarshaller` + +正如[introduction](#oxm-introduction)中所述,编组器将对象序列化为 XML,而解组器将 XML 流反序列化为对象。本节描述了用于此目的的两个 Spring 接口。 + +#### 6.2.1.理解`Marshaller` + +Spring 抽象了 `org.SpringFramework.OXM.Marshaller’接口背后的所有编组操作,其主要方法如下: + +``` +public interface Marshaller { + + /** + * Marshal the object graph with the given root into the provided Result. + */ + void marshal(Object graph, Result result) throws XmlMappingException, IOException; +} +``` + +`Marshaller`接口有一个主要方法,它将给定对象封送到给定的`javax.xml.transform.Result`。其结果是一个标记接口,它基本上表示一个 XML 输出抽象。具体的实现封装了各种 XML 表示,如下表所示: + +|Result implementation|包装 XML 表示| +|---------------------|-----------------------------------------------------------| +| `DOMResult` |`org.w3c.dom.Node`| +| `SAXResult` |`org.xml.sax.ContentHandler`| +| `StreamResult` |`java.io.File`,`java.io.OutputStream`,或`java.io.Writer`| + +| |尽管`marshal()`方法接受一个普通对象作为其第一个参数,但大多数 `Marshaller’实现不能处理任意对象。相反,对象类<br/>必须映射到映射文件中,用注释标记,用<br/>编组器注册,或者有一个公共的基类。请参阅本章<br/>中后面的部分,以确定你的 O-X 技术如何管理这一点。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 6.2.2.理解`Unmarshaller` + +与`Marshaller`类似,我们有`org.springframework.oxm.Unmarshaller`接口,下面的列表显示了该接口: + +``` +public interface Unmarshaller { + + /** + * Unmarshal the given provided Source into an object graph. + */ + Object unmarshal(Source source) throws XmlMappingException, IOException; +} +``` + +这个接口也有一个方法,它从给定的“javax.xml.transform.source”(一种 XML 输入抽象)读取对象,并返回读取的对象。与`Result`一样,`Source`是一个标记接口,具有三个具体的实现。每一种都包装了不同的 XML 表示形式,如下表所示: + +|Source implementation|包装 XML 表示| +|---------------------|----------------------------------------------------------| +| `DOMSource` |`org.w3c.dom.Node`| +| `SAXSource` |`org.xml.sax.InputSource`,和`org.xml.sax.XMLReader`| +| `StreamSource` |`java.io.File`,`java.io.InputStream`,or`java.io.Reader`| + +即使有两个独立的编组接口(“Marshaller”和“Unmarshaller”), Spring-WS 中的所有实现都在一个类中实现这两个接口。这意味着你可以连接一个 Marshaller 类,并在`applicationContext.xml`中同时将其称为一个 Marshaller 和一个 Unshaller。 + +#### 6.2.3.理解`XmlMappingException` + +Spring 将来自底层 O-X 映射工具的异常转换为其自身的异常层次结构,并将`XmlMappingException`作为根异常。这些运行时异常将包装原始异常,这样就不会丢失任何信息。 + +此外,`MarshallingFailureException`和`UnmarshallingFailureException`提供了编组和解组操作之间的区别,即使底层的 O-X 映射工具不这样做。 + +O-X 映射异常层次结构如下图所示: + +![oxm exceptions](images/oxm-exceptions.png) + +### 6.3.使用`Marshaller`和`Unmarshaller` + +你可以在各种各样的情况下使用 Spring 的 OXM。在下面的示例中,我们使用它将 Spring 管理的应用程序的设置编组为 XML 文件。在下面的示例中,我们使用一个简单的 JavaBean 来表示设置: + +Java + +``` +public class Settings { + + private boolean fooEnabled; + + public boolean isFooEnabled() { + return fooEnabled; + } + + public void setFooEnabled(boolean fooEnabled) { + this.fooEnabled = fooEnabled; + } +} +``` + +Kotlin + +``` +class Settings { + var isFooEnabled: Boolean = false +} +``` + +应用程序类使用此 Bean 来存储其设置。除了一个主方法之外,这个类还有两个方法:`saveSettings()`将设置保存到一个名为 `settings.xml’的文件中,`loadSettings()`再次加载这些设置。下面的`main()`方法构造了一个 Spring 应用程序上下文,并调用这两个方法: + +Java + +``` +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.oxm.Marshaller; +import org.springframework.oxm.Unmarshaller; + +public class Application { + + private static final String FILE_NAME = "settings.xml"; + private Settings settings = new Settings(); + private Marshaller marshaller; + private Unmarshaller unmarshaller; + + public void setMarshaller(Marshaller marshaller) { + this.marshaller = marshaller; + } + + public void setUnmarshaller(Unmarshaller unmarshaller) { + this.unmarshaller = unmarshaller; + } + + public void saveSettings() throws IOException { + try (FileOutputStream os = new FileOutputStream(FILE_NAME)) { + this.marshaller.marshal(settings, new StreamResult(os)); + } + } + + public void loadSettings() throws IOException { + try (FileInputStream is = new FileInputStream(FILE_NAME)) { + this.settings = (Settings) this.unmarshaller.unmarshal(new StreamSource(is)); + } + } + + public static void main(String[] args) throws IOException { + ApplicationContext appContext = + new ClassPathXmlApplicationContext("applicationContext.xml"); + Application application = (Application) appContext.getBean("application"); + application.saveSettings(); + application.loadSettings(); + } +} +``` + +Kotlin + +``` +class Application { + + lateinit var marshaller: Marshaller + + lateinit var unmarshaller: Unmarshaller + + fun saveSettings() { + FileOutputStream(FILE_NAME).use { outputStream -> marshaller.marshal(settings, StreamResult(outputStream)) } + } + + fun loadSettings() { + FileInputStream(FILE_NAME).use { inputStream -> settings = unmarshaller.unmarshal(StreamSource(inputStream)) as Settings } + } +} + +private const val FILE_NAME = "settings.xml" + +fun main(args: Array<String>) { + val appContext = ClassPathXmlApplicationContext("applicationContext.xml") + val application = appContext.getBean("application") as Application + application.saveSettings() + application.loadSettings() +} +``` + +`Application`需要同时设置`marshaller`和`unmarshaller`属性。我们可以通过使用以下`applicationContext.xml`来做到这一点: + +``` +<beans> + <bean id="application" class="Application"> + <property name="marshaller" ref="xstreamMarshaller" /> + <property name="unmarshaller" ref="xstreamMarshaller" /> + </bean> + <bean id="xstreamMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller"/> +</beans> +``` + +这个应用程序上下文使用 XStream,但我们可以使用本章后面描述的任何其他 Marshaller 实例。请注意,默认情况下,XStream 不需要任何进一步的配置,因此 Bean 的定义相当简单。还要注意,“xStreamMarshaller”同时实现了`Marshaller`和`Unmarshaller`,因此我们可以在应用程序的`marshaller`和`unmarshaller`属性中引用“xStreamMarshaller" Bean。 + +此示例应用程序生成以下`settings.xml`文件: + +``` +<?xml version="1.0" encoding="UTF-8"?> +<settings foo-enabled="false"/> +``` + +### 6.4.XML 配置命名空间 + +通过使用 OXM 名称空间中的标记,可以更简洁地配置编组器。要使这些标记可用,你必须首先在 XML 配置文件的前导码中引用适当的模式。下面的示例展示了如何做到这一点: + +``` +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:oxm="http://www.springframework.org/schema/oxm" (1) +xsi:schemaLocation="http://www.springframework.org/schema/beans + https://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/oxm https://www.springframework.org/schema/oxm/spring-oxm.xsd"> (2) +``` + +|**1**|引用`oxm`模式。| +|-----|----------------------------------| +|**2**|指定`oxm`模式位置。| + +该模式使以下元素可用: + +* [JAXB2-编组器](#oxm-jaxb2-xsd) + +* [JiBX-Marshaller’](#oxm-jibx-xsd) + +每个标记在其各自的编组器部分中进行了解释。但是,作为示例,JAXB2 封送编组器的配置可能类似于以下内容: + +``` +<oxm:jaxb2-marshaller id="marshaller" contextPath="org.springframework.ws.samples.airline.schema"/> +``` + +### 6.5.JAXB + +JAXB 绑定编译器将 W3CXML 模式转换为一个或多个 Java 类、一个“jaxb.properties”文件,可能还有一些资源文件。JAXB 还提供了一种从带注释的 Java 类生成模式的方法。 + +Spring 支持 JAXB2.0API 作为 XML 编组策略,遵循[`Marshaller` and `Unmarshaller`](#oxm-marshaller-unmarshaller)中描述的 `marshaller’和`Unmarshaller`接口。相应的集成类驻留在`org.springframework.oxm.jaxb`包中。 + +#### 6.5.1.使用`Jaxb2Marshaller` + +`Jaxb2Marshaller`类实现了 Spring 的`Marshaller`和`Unmarshaller`接口。它需要一个上下文路径来操作。你可以通过设置“ContextPath”属性来设置上下文路径。上下文路径是包含模式派生类的冒号分隔的 Java 包名称的列表。它还提供了一个`classesToBeBound`属性,它允许你设置一个由 Marshaller 支持的类数组。模式验证是通过向 Bean 指定一个或多个模式资源来执行的,如以下示例所示: + +``` +<beans> + <bean id="jaxb2Marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller"> + <property name="classesToBeBound"> + <list> + <value>org.springframework.oxm.jaxb.Flight</value> + <value>org.springframework.oxm.jaxb.Flights</value> + </list> + </property> + <property name="schema" value="classpath:org/springframework/oxm/schema.xsd"/> + </bean> + + ... + +</beans> +``` + +##### XML 配置命名空间 + +`jaxb2-marshaller`元素配置了`org.springframework.oxm.jaxb.Jaxb2Marshaller`,如下例所示: + +``` +<oxm:jaxb2-marshaller id="marshaller" contextPath="org.springframework.ws.samples.airline.schema"/> +``` + +或者,你可以通过使用“类-be-bound”子元素来提供要绑定到编组器的类的列表: + +``` +<oxm:jaxb2-marshaller id="marshaller"> + <oxm:class-to-be-bound name="org.springframework.ws.samples.airline.schema.Airport"/> + <oxm:class-to-be-bound name="org.springframework.ws.samples.airline.schema.Flight"/> + ... +</oxm:jaxb2-marshaller> +``` + +下表描述了可用的属性: + +| Attribute |说明|Required| +|-------------|------------------------|--------| +| `id` |编组员的身份| No | +|`contextPath`|JAXB 上下文路径| No | + +### 6.6.JiBX + +JiBX 框架提供了一种类似于 Hibernate 为 ORM 提供的解决方案:绑定定义定义了如何将 Java 对象转换为 XML 或从 XML 转换的规则。在准备好绑定和编译类之后,JiBX 绑定编译器将增强类文件,并添加代码来处理将类的实例从 XML 转换为 XML。 + +有关 JiBX 的更多信息,请参见[JiBX web site](http://jibx.sourceforge.net/)。 Spring 集成类驻留在`org.springframework.oxm.jibx`包中。 + +#### 6.6.1.使用`JibxMarshaller` + +`JibxMarshaller`类实现了`Marshaller`和`Unmarshaller`接口。要进行操作,它需要封送一个类的名称,你可以使用`targetClass`属性对其进行设置。也可以通过设置“bindingname”属性来设置绑定名称。在下面的示例中,我们绑定`Flights`类: + +``` +<beans> + <bean id="jibxFlightsMarshaller" class="org.springframework.oxm.jibx.JibxMarshaller"> + <property name="targetClass">org.springframework.oxm.jibx.Flights</property> + </bean> + ... +</beans> +``` + +为单个类配置了`JibxMarshaller`。如果要封送多个类,则必须配置具有不同`JibxMarshaller`属性值的多个`targetClass`实例。 + +##### XML 配置命名空间 + +`jibx-marshaller`标记配置了`org.springframework.oxm.jibx.JibxMarshaller`,如下例所示: + +``` +<oxm:jibx-marshaller id="marshaller" target-class="org.springframework.ws.samples.airline.schema.Flight"/> +``` + +下表描述了可用的属性: + +| Attribute |说明|Required| +|--------------|----------------------------------------|--------| +| `id` |编组员的身份| No | +|`target-class`|这个编组器的目标类| Yes | +|`bindingName` |这个编组器使用的绑定名称| No | + +### 6.7.XStream + +XStream 是一个简单的库,用于将对象序列化为 XML,然后再将对象序列化。它不需要任何映射,并生成干净的 XML。 + +有关 XStream 的更多信息,请参见[XStream 网站](https://x-stream.github.io/)。 Spring 集成类驻留在 `org.SpringFramework.OXM.xStream’包中。 + +#### 6.7.1.使用`XStreamMarshaller` + +`XStreamMarshaller`不需要任何配置,可以直接在应用程序上下文中进行配置。为了进一步定制 XML,你可以设置一个别名映射,它由映射到类的字符串别名组成,如下例所示: + +``` +<beans> + <bean id="xstreamMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller"> + <property name="aliases"> + <props> + <prop key="Flight">org.springframework.oxm.xstream.Flight</prop> + </props> + </property> + </bean> + ... +</beans> +``` + +| |默认情况下,XStream 允许对任意类进行解编,这可能导致<br/>不安全的 Java 序列化效果。因此,我们不建议使用“XStreamMarshaller”来从外部来源(即 Web)分解 XML,因为这可能会<br/>导致安全漏洞。<br/><br/>如果你选择使用`XStreamMarshaller`从外部源解编 XML,<br/>在`XStreamMarshaller`上设置`supportedClasses`属性,如下例所示:<br/><br/>``<br/><bean id="xstreamMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller"><br/><property name="supportedClasses" value="org.springframework.oxm.xstream.Flight"/><br/></bean><br/>``<<<<2079"/>这样做可确保只有已注册的类才有资格进行 unmarshalling。<81"/>”R=“2081”/>“R=”,“R=”=“2082”R=“的类只能确保你另外注册的类得到支持,”gt=“R=”<20 除了显式支持应该支持的域类的<br/>转换器之外,你可能希望在列表中添加一个`CatchAllConverter`作为最后一个转换器。作为<br/>结果,不会调用具有较低优先级和可能的安全性<br/>漏洞的默认 XStream 转换器。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |请注意,XStream 是一个 XML 序列化库,而不是一个数据绑定库。<br/>因此,它对名称空间的支持有限。因此,它非常不适合在 Web 服务中使用<br/>。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## 7. 附录 + +### 7.1.XML 模式 + +附录的这一部分列出了用于数据访问的 XML 模式,包括以下内容: + +* [The `tx` Schema](#xsd-schemas-tx) + +* [The `jdbc` Schema](#xsd-schemas-jdbc) + +#### 7.1.1.`tx`模式 + +`tx`标记处理在 Spring 的事务全面支持中配置所有这些 bean。这些标记在标题为[事务管理](#transaction)的章节中进行了介绍。 + +| |我们强烈建议你查看带有<br/> Spring 发行版的`'spring-tx.xsd'`文件。该文件包含 Spring 事务<br/>配置的 XML 模式,并涵盖`tx`命名空间中的所有不同元素,包括<br/>属性默认值和类似信息。这个文件是内联文档化的,因此,<br/>这里不重复信息是为了遵守 dry(不要<br/>重复自己)原则。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +为了完整起见,要使用`tx`模式中的元素,你需要在 Spring XML 配置文件的顶部具有以下前导码。以下代码片段中的文本引用了正确的模式,因此`tx`名称空间中的标记对你是可用的: + +``` +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:aop="http://www.springframework.org/schema/aop" + xmlns:tx="http://www.springframework.org/schema/tx" (1) + xsi:schemaLocation=" + http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx.xsd (2) + http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd"> + + <!-- bean definitions here --> + +</beans> +``` + +|**1**|声明`tx`名称空间的用法。| +|-----|---------------------------------------------------| +|**2**|指定位置(与其他架构位置一起)。| + +| |通常,当你使用`tx`命名空间中的元素时,你还使用<br/>命名空间中的<br/>元素(因为 Spring 中的声明性事务支持是通过使用 AOP 实现的<br/>)。前面的 XML 片段包含引用<br/>模式所需的相关行,以便`aop`名称空间中的元素对你可用<br/>。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 7.1.2.`jdbc`模式 + +`jdbc`元素允许你快速配置嵌入式数据库或初始化现有数据源。这些元素分别记录在[嵌入式数据库支持](#jdbc-embedded-database-support)和[初始化数据源](#jdbc-initializing-datasource)中。 + +要使用`jdbc`模式中的元素,你需要在 Spring XML 配置文件的顶部具有以下前导码。以下代码片段中的文本引用了正确的模式,因此`jdbc`名称空间中的元素对你是可用的: + +``` +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:jdbc="http://www.springframework.org/schema/jdbc" (1) + xsi:schemaLocation=" + http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/jdbc https://www.springframework.org/schema/jdbc/spring-jdbc.xsd"> (2) + + <!-- bean definitions here --> + +</beans> +``` diff --git a/docs/spring-framework/integration.md b/docs/spring-framework/integration.md new file mode 100644 index 0000000000000000000000000000000000000000..5fb6c2c827da0d7cd459f58c4d48f698312be737 --- /dev/null +++ b/docs/spring-framework/integration.md @@ -0,0 +1,4344 @@ +# 整合 + +参考文档的这一部分涵盖了 Spring 框架与许多技术的集成。 + +## 1. REST 端点 + +Spring 框架为调用 REST 端点提供了两种选择: + +* [`RestTemplate`](#rest-resttemplate):原始的 Spring REST 客户机具有同步的、模板的方法 API。 + +* [WebClient](web-reactive.html#webflux-client):一种非阻塞、反应性的替代方案,支持同步、异步以及流媒体场景。 + +| |截至 5.0,`RestTemplate`处于维护模式,只有少量的<br/>更改请求和 bug 被接受。请考虑使用[WebClient](web-reactive.html#webflux-client),它提供了一个更现代的 API,<br/>支持同步、异步和流场景。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 1.1.`RestTemplate` + +`RestTemplate`在 HTTP 客户库上提供了更高级别的 API。它使得在单行中调用 REST 端点变得很容易。它公开了以下几组重载方法: + +| Method group |说明| +|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `getForObject` |通过 get 检索表示。| +| `getForEntity` |使用 get 检索`ResponseEntity`(即状态、标题和正文)。| +|`headFor标头` |通过使用 head 检索资源的所有 header。| +|`postForLocation`|通过使用 POST 创建一个新资源,并从响应返回`Location`头。| +| `postForObject` |通过使用 POST 创建一个新资源,并从响应返回表示。| +| `postForEntity` |通过使用 POST 创建一个新资源,并从响应返回表示。| +| `put` |使用 PUT 创建或更新资源。| +|`patchForObject` |使用补丁更新资源并返回响应的表示。<br/>注意,JDK`HttpURLConnection`不支持`PATCH`,但是 Apache<br/>HttpComponents 和其他组件支持。| +| `delete` |使用 DELETE 删除指定 URI 上的资源。| +|`optionsForAllow`|通过使用 allow 检索资源的允许的 HTTP 方法。| +| `exchange` |在需要时提供额外的<br/>灵活性的上述方法的更通用(且不那么固执己见)版本。它接受`RequestEntity`(包括 HTTP 方法、URL、headers、<br/>和正文作为输入)并返回`ResponseEntity`。<br/><br/>这些方法允许使用`ParameterizedTypeReference`而不是`Class`来指定带有泛型的响应类型。| +| `execute` |最通用的执行请求的方式,通过回调接口完全控制请求<br/>准备和响应提取。| + +#### 1.1.1.初始化 + +默认构造函数使用`java.net.HttpURLConnection`执行请求。你可以使用`ClientHttpRequestFactory`的实现切换到不同的 HTTP 库。以下是内置的支持: + +* Apache HttpComponents + +* 内蒂 + +* OKHTTP + +例如,要切换到 Apache HttpComponents,你可以使用以下方法: + +``` +RestTemplate template = new RestTemplate(new HttpComponentsClientHttpRequestFactory()); +``` + +每个`ClientHttpRequestFactory`都公开了特定于底层 HTTP 客户库的配置选项——例如,用于凭据、连接池和其他详细信息。 + +| |请注意,当<br/>访问表示错误的响应的状态(例如 401)时,用于 HTTP 请求的`java.net`实现可能会引发异常。如果这是<br/>问题,请切换到另一个 HTTP 客户库。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 乌里斯 + +许多`RestTemplate`方法接受 URI 模板和 URI 模板变量,或者作为`String`变量参数,或者作为`Map<String,String>`变量。 + +下面的示例使用`String`变量参数: + +``` +String result = restTemplate.getForObject( + "https://example.com/hotels/{hotel}/bookings/{booking}", String.class, "42", "21"); +``` + +下面的示例使用`Map<String, String>`: + +``` +Map<String, String> vars = Collections.singletonMap("hotel", "42"); + +String result = restTemplate.getForObject( + "https://example.com/hotels/{hotel}/rooms/{hotel}", String.class, vars); +``` + +请记住,URI 模板是自动编码的,如下例所示: + +``` +restTemplate.getForObject("https://example.com/hotel list", String.class); + +// Results in request to "https://example.com/hotel%20list" +``` + +可以使用`RestTemplate`的`uriTemplateHandler`属性来定制 URI 的编码方式。或者,你可以准备一个`java.net.URI`并将其传递到一个`RestTemplate`方法,该方法接受`URI`。 + +有关使用和编码 URI 的更多详细信息,请参见[URI Links](web.html#mvc-uri-building)。 + +##### Headers + +你可以使用`exchange()`方法来指定请求头,如下例所示: + +``` +String uriTemplate = "https://example.com/hotels/{hotel}"; +URI uri = UriComponentsBuilder.fromUriString(uriTemplate).build(42); + +RequestEntity<Void> requestEntity = RequestEntity.get(uri) + .header("MyRequestHeader", "MyValue") + .build(); + +ResponseEntity<String> response = template.exchange(requestEntity, String.class); + +String responseHeader = response.getHeaders().getFirst("MyResponseHeader"); +String body = response.getBody(); +``` + +你可以通过许多返回“responseEntity”的`RestTemplate`方法变量获得响应头。 + +#### 1.1.2.身体 + +传入`RestTemplate`方法并从其返回的对象将在`HttpMessageConverter`的帮助下转换为 RAW 内容并从 RAW 内容转换。 + +在 POST 上,输入对象被序列化到请求主体,如下例所示: + +``` +URI location = template.postForLocation("https://example.com/people", person); +``` + +你不需要显式地设置请求的内容类型标头。在大多数情况下,可以找到基于源`Object`类型的兼容消息转换器,并且所选择的消息转换器相应地设置内容类型。如果有必要,可以使用“exchange”方法显式地提供`Content-Type`请求头,这反过来会影响选择什么消息转换器。 + +在 get 上,响应的主体被反序列化为输出`Object`,如下例所示: + +``` +Person person = restTemplate.getForObject("https://example.com/people/{id}", Person.class, 42); +``` + +请求的`Accept`头不需要显式设置。在大多数情况下,可以根据预期的响应类型找到兼容的消息转换器,这将有助于填充`Accept`头。如果有必要,可以使用`exchange`方法显式地提供`Accept`头。 + +默认情况下,`RestTemplate`注册了所有内置的[消息转换器](#rest-message-conversion),这取决于有助于确定存在哪些可选转换库的 Classpath 检查。你还可以将消息转换器设置为显式使用。 + +#### 1.1.3.消息转换 + +[WebFlux](web-reactive.html#webflux-codecs) + +`spring-web`模块包含`HttpMessageConverter`契约,用于通过`InputStream`和`OutputStream`读取和写入 HTTP 请求和响应的主体。在 Spring MVC REST 控制器中)。 + +MIME 类型的具体实现在框架中提供,并且默认情况下,在客户端用`RestTemplate`注册,在服务器端用<requestMethodHandlerAdapter>注册(参见[配置消息转换器](web.html#mvc-config-message-converters))。 + +`HttpMessageConverter`的实现方式在以下各节中进行了描述。对于所有转换器,都使用默认的媒体类型,但是你可以通过设置“supportedmediatypes” Bean 属性来覆盖它。下表描述了每种实现方式: + +| MessageConverter |说明| +|----------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `StringHttpMessageConverter` |一个`HttpMessageConverter`实现,它可以从 http<br/>请求和响应中读取和写入`String`实例。默认情况下,此转换器支持所有文本媒体类型<br/>,并使用`Content-Type`的`text/plain`进行写入。| +| `FormHttpMessageConverter` |一个`HttpMessageConverter`实现,它可以从 HTTP<br/>请求和响应中读写表单数据。默认情况下,此转换器读取和写入“应用程序/X-WWW-表单-URLENCODED”媒体类型。表单数据被读取并写入“multivalueMap<String, String>”。转换器还可以写入(但不读取)多部分<br/>从`MultiValueMap<String, Object>`中读取的数据。默认情况下,`multipart/form-data`是<br/>支持的。在 Spring Framework5.2 中,对于<br/>写入表单数据,可以支持额外的多部分子类型。有关更多详细信息,请咨询`FormHttpMessageConverter`的 Javadoc。| +| `ByteArrayHttpMessageConverter` |一个`HttpMessageConverter`实现,它可以从<br/>HTTP 请求和响应中读写字节数组。默认情况下,此转换器支持所有媒体类型<br/>,并使用`Content-Type`的`application/octet-stream`写。通过设置`supportedMediaTypes`属性并重写`getContentType(byte[])`,可以重写此<br/>。| +| `MarshallingHttpMessageConverter` |一个`HttpMessageConverter`实现,它可以通过使用 Spring 的 `marshaller’和`Unmarshaller`包中的抽象来读写 XML。<br/>此转换器需要`Marshaller`和`Unmarshaller`才能使用。你可以通过构造函数或 Bean 属性注入这些<br/>。默认情况下,这个转换器支持“text/xml”和`application/xml`。| +| `MappingJackson2HttpMessageConverter` |一个`HttpMessageConverter`实现,它可以通过使用 Jackson 的“ObjectMapper”来读写 JSON。你可以根据需要使用 Jackson 的<br/>提供的注释来定制 JSON 映射。当需要进一步控制时(对于需要为特定类型提供自定义 JSON序列化器/反序列化器的情况),可以通过属性注入自定义。默认情况下,这个<br/>转换器支持`application/json`。| +|`MappingJackson2XmlHttpMessageConverter`|一个`HttpMessageConverter`实现,它可以通过使用[Jackson XML](https://github.com/FasterXML/jackson-dataformat-xml)扩展的 `xmlmapper’来读写 XML。你可以根据需要通过使用 JAXB<br/>或 Jackson 提供的注释来定制 XML 映射。当需要进一步控制时(对于需要为特定类型提供自定义 XML序列化器/反序列化器的情况),可以通过属性注入自定义。默认情况下,这个<br/>转换器支持`application/xml`。| +| `SourceHttpMessageConverter` |一个`HttpMessageConverter`实现,它可以从 HTTP 请求和响应中读写 `javax.xml.transform.source’。只支持`DOMSource`、`saxsource’和`StreamSource`。默认情况下,这个转换器支持“text/xml”和`application/xml`。| +| `BufferedImageHttpMessageConverter` |一个`HttpMessageConverter`实现,可以从 HTTP 请求和响应中读写 `java.awt.image.BufferidImage’。这个转换器读取<br/>并写入 Java I/O API 支持的媒体类型。| + +#### 1.1.4.JacksonJSON 视图 + +你可以指定[JacksonJSON 视图](https://www.baeldung.com/jackson-json-view-annotation)来序列化对象属性的一个子集,如下例所示: + +``` +MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23")); +value.setSerializationView(User.WithoutPasswordView.class); + +RequestEntity<MappingJacksonValue> requestEntity = + RequestEntity.post(new URI("https://example.com/user")).body(value); + +ResponseEntity<String> response = template.exchange(requestEntity, String.class); +``` + +##### 多部分 + +要发送多部分数据,你需要提供一个`MultiValueMap<String, Object>`,其值对于部分内容可以是`Object`,对于文件部分可以是`Resource`,对于带有标题的部分内容可以是`HttpEntity`。例如: + +``` +MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>(); + +parts.add("fieldPart", "fieldValue"); +parts.add("filePart", new FileSystemResource("...logo.png")); +parts.add("jsonPart", new Person("Jason")); + +HttpHeaders headers = new HttpHeaders(); +headers.setContentType(MediaType.APPLICATION_XML); +parts.add("xmlPart", new HttpEntity<>(myBean, headers)); +``` + +在大多数情况下,你不必为每个部分指定`Content-Type`。内容类型是基于所选择的`HttpMessageConverter`自动确定的,以序列化它,或者,在`Resource`的情况下,基于文件扩展名。如果有必要,可以显式地为`MediaType`提供一个`HttpEntity`包装器。 + +一旦`MultiValueMap`准备好了,就可以将其传递给`RestTemplate`,如下所示: + +``` +MultiValueMap<String, Object> parts = ...; +template.postForObject("https://example.com/upload", parts, Void.class); +``` + +如果`MultiValueMap`至少包含一个非 ` 字符串’值,则`Content-Type`由`FormHttpMessageConverter`设置为`multipart/form-data`。如果`MultiValueMap`具有 `string` 值,则`Content-Type`默认为`application/x-www-form-urlencoded`。如果有必要,`Content-Type`也可以显式地设置。 + +### 1.2.使用`AsyncRestTemplate`(不推荐) + +`AsyncRestTemplate`已被弃用。对于可能考虑使用“AsyncrestTemplate”的所有用例,请使用[WebClient](web-reactive.html#webflux-client)。 + +## 2. 远程和 Web 服务 + +Spring 通过各种技术为远程控制提供支持。远程支持简化了支持远程的服务的开发,这些服务是通过 Java 接口和对象作为输入和输出来实现的。目前, Spring 支持以下远程处理技术: + +* [Java Web 服务](#remoting-web-services): Spring 通过 JAX-WS 为 Web 服务提供远程支持。 + +* [AMQP](#remoting-amqp):单独的 Spring AMQP 项目支持通过 AMQP 作为底层协议进行远程处理。 + +| |从 Spring Framework5.3 开始,出于安全原因和更广泛的行业支持,现在不赞成对几种远程技术的支持<br/>。支持基础设施将从 Spring 框架中删除<br/>,用于其下一个主要版本。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +以下远程处理技术现已弃用,不会被替换: + +* [RMI](#remoting-rmi):通过使用`RmiProxyFactoryBean`和 `RMIServiceExporter’, Spring 既支持传统的 RMI(带有`java.rmi.Remote`接口和`java.rmi.RemoteException`接口),也支持通过 RMI 调用程序(带有任何 Java 接口)进行透明的远程处理。 + +* [Spring HTTP Invoker (Deprecated)](#remoting-httpinvoker): Spring 提供了一种特殊的远程策略,该策略允许通过 HTTP 进行 Java 序列化,支持任何 Java 接口(就像 RMI 调用程序所做的那样)。对应的支持类是`HttpInvokerProxyFactoryBean`和`HttpInvokerServiceExporter`。 + +* [Hessian](#remoting-caucho-protocols-hessian):通过使用 Spring 的`HessianProxyFactoryBean`和 `HessianServiceExporter’,你可以通过 Caucho 提供的基于 HTTP 的轻量级二进制协议透明地公开你的服务。 + +* [JMS(已弃用)](#remoting-jms):通过 JMS 作为基础协议的远程处理,通过 ` Spring-JMS` 模块中的 `jmsinvokerServiceExporter’和`JmsInvokerProxyFactoryBean`类得到支持。 + +在讨论 Spring 的远程功能时,我们使用了以下领域模型和相应的服务: + +``` +public class Account implements Serializable { + + private String name; + + public String getName(){ + return name; + } + + public void setName(String name) { + this.name = name; + } +} +``` + +``` +public interface AccountService { + + public void insertAccount(Account account); + + public List<Account> getAccounts(String name); +} +``` + +``` +// the implementation doing nothing at the moment +public class AccountServiceImpl implements AccountService { + + public void insertAccount(Account acc) { + // do something... + } + + public List<Account> getAccounts(String name) { + // do something... + } +} +``` + +本节首先通过使用 RMI 将服务公开给远程客户机,并稍微讨论一下使用 RMI 的缺点。然后继续以使用 Hessian 作为协议的示例进行说明。 + +### 2.1.AMQP + +Spring AMQP 项目支持通过 AMQP 作为底层协议的远程处理。欲了解更多详情,请访问 Spring AMQP 参考文献的[Spring Remoting](https://docs.spring.io/spring-amqp/docs/current/reference/html/#remoting)部分。 + +| |远程接口未实现自动检测。<br/><br/>远程<br/>接口未实现自动检测的主要原因是避免为远程调用者打开太多的门。目标对象可以<br/>实现内部回调接口,例如`InitializingBean`或`DisposableBean`谁不想向调用者公开。<br/><br/>在本地情况下,提供由目标实现的所有接口的代理通常并不重要<br/>。但是,在导出远程服务时,应该公开一个特定的<br/>服务接口,其中包含用于远程使用的特定操作。除了内部<br/>回调接口外,目标可能实现多个业务接口,其中只有<br/>一个用于远程公开。由于这些原因,我们要求这样的<br/>服务接口被指定。<br/><br/>这是在配置便利和意外<br/>暴露内部方法的风险之间的权衡。始终指定一个服务接口并不需要太多的<br/>工作,并且对于控制特定方法的公开,这会使你处于安全的一边。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 2.2.选择技术时的考虑因素 + +这里介绍的每一种技术都有其缺陷。在选择一种技术时,你应该仔细考虑你的需求、你公开的服务以及通过网络发送的对象。 + +当使用 RMI 时,你无法通过 HTTP 协议访问这些对象,除非你对 RMI 通信量进行了隧道处理。RMI 是一种非常重要的协议,因为它支持全对象序列化,当你使用需要在线序列化的复杂数据模型时,这一点非常重要。然而,RMI-JRMP 与 Java 客户机绑定在一起。这是一种从 Java 到 Java 的远程解决方案。 + +Spring 的 HTTP Invoker 是一个很好的选择,如果你需要基于 HTTP 的远程处理,但也需要依赖 Java 序列化。它与 RMI 调用程序共享基本的基础设施,但使用 HTTP 作为传输。请注意,HTTP 调用程序不仅限于 Java-to-Java 远程操作,而且还限于客户端和服务器端的 Spring。(后者也适用于 Spring 的非 RMI 接口的 RMI 调用程序。) + +Hessian 在异构环境中操作时可能会提供重要的价值,因为它们明确地允许非 Java 客户机。然而,对非 Java 的支持仍然有限。已知的问题包括 Hibernate 对象的序列化与延迟初始化的集合的组合。如果你有这样的数据模型,可以考虑使用 RMI 或 HTTP 调用程序,而不是 Hessian。 + +JMS 可以用于提供服务集群,并让 JMS 代理负责负载平衡、发现和自动故障转移。默认情况下,Java 序列化用于 JMS 远程处理,但 JMS 提供者可以使用不同的机制来进行线接格式处理,例如 XStream,以使服务器能够在其他技术中实现。 + +最后但并非最不重要的一点是,EJB 比 RMI 具有优势,因为它支持标准的基于角色的身份验证和授权以及远程事务传播。也可以获得 RMI 调用程序或 HTTP 调用程序来支持安全上下文传播,尽管 Core Spring 没有提供这一点。 Spring 仅提供用于插入第三方或自定义解决方案的适当挂钩。 + +### 2.3.Java Web 服务 + +Spring 提供对标准 Java Web 服务 API 的完全支持: + +* 使用 JAX-WS 公开 Web 服务 + +* 使用 JAX-WS 访问 Web 服务 + +除了对 Spring Core 中的 JAX-WS 的股票支持外, Spring 投资组合还具有[Spring Web Services](https://projects.spring.io/spring-ws),这是一种契约优先、文档驱动的 Web 服务的解决方案——强烈推荐用于构建现代的、面向未来的 Web 服务。 + +#### 2.3.1.使用 JAX-WS 公开基于 Servlet 的 Web 服务 + +Spring 为 JAX-WS Servlet 端点实现提供了一个方便的基类:“SpringBeanAutoWiringSupport”。为了公开我们的`AccountService`,我们扩展了 Spring 的“SpringBeanAutoWiringSupport”类,并在这里实现了我们的业务逻辑,通常将调用委派给业务层。我们使用 Spring 的`@Autowired`注释来表示对 Spring 管理的 bean 的依赖关系。下面的示例展示了扩展`SpringBeanAutowiringSupport`的类: + +``` +/** + * JAX-WS compliant AccountService implementation that simply delegates + * to the AccountService implementation in the root web application context. + * + * This wrapper class is necessary because JAX-WS requires working with dedicated + * endpoint classes. If an existing service needs to be exported, a wrapper that + * extends SpringBeanAutowiringSupport for simple Spring bean autowiring (through + * the @Autowired annotation) is the simplest JAX-WS compliant way. + * + * This is the class registered with the server-side JAX-WS implementation. + * In the case of a Java EE server, this would simply be defined as a servlet + * in web.xml, with the server detecting that this is a JAX-WS endpoint and reacting + * accordingly. The servlet name usually needs to match the specified WS service name. + * + * The web service engine manages the lifecycle of instances of this class. + * Spring bean references will just be wired in here. + */ +import org.springframework.web.context.support.SpringBeanAutowiringSupport; + +@WebService(serviceName="AccountService") +public class AccountServiceEndpoint extends SpringBeanAutowiringSupport { + + @Autowired + private AccountService biz; + + @WebMethod + public void insertAccount(Account acc) { + biz.insertAccount(acc); + } + + @WebMethod + public Account[] getAccounts(String name) { + return biz.getAccounts(name); + } +} +``` + +我们的`AccountServiceEndpoint`需要在与 Spring 上下文相同的 Web 应用程序中运行,以允许访问 Spring 的设施。默认情况下,在 Java EE 环境中就是这样,它使用 JAX-WS Servlet 端点部署的标准契约。有关详细信息,请参见各种 Java EE Web 服务教程。 + +#### 2.3.2.使用 JAX-WS 导出独立的 Web 服务 + +甲骨文 JDK 附带的内置 JAX-WS 提供程序通过使用 JDK 中也包含的内置 HTTP 服务器支持公开 Web 服务。 Spring 的“SimpleJaxWsServiceExporter”检测 Spring 应用程序上下文中的所有`@WebService`-注释 bean,并通过默认的 JAX-WS 服务器(JDK HTTP 服务器)将它们导出。 + +在这个场景中,端点实例被定义为 Spring bean 本身并作为 bean 进行管理。它们在 JAX-WS 引擎中注册,但它们的生命周期取决于 Spring 应用程序上下文。这意味着你可以将 Spring 功能(例如显式依赖注入)应用到端点实例。通过`@Autowired`的注解驱动注入也可以工作。下面的示例展示了如何定义这些 bean: + +``` +<bean class="org.springframework.remoting.jaxws.SimpleJaxWsServiceExporter"> + <property name="baseAddress" value="http://localhost:8080/"/> +</bean> + +<bean id="accountServiceEndpoint" class="example.AccountServiceEndpoint"> + ... +</bean> + +... +``` + +`AccountServiceEndpoint`可以但不必从 Spring 的`SpringBeanAutowiringSupport`派生,因为本例中的端点是完全由 Spring 管理的 Bean。这意味着端点实现可以如下(不声明任何超类——并且 Spring 的`@Autowired`配置注释仍然受到尊重): + +``` +@WebService(serviceName="AccountService") +public class AccountServiceEndpoint { + + @Autowired + private AccountService biz; + + @WebMethod + public void insertAccount(Account acc) { + biz.insertAccount(acc); + } + + @WebMethod + public List<Account> getAccounts(String name) { + return biz.getAccounts(name); + } +} +``` + +#### 2.3.3.通过使用 JAX-WS RI 的 Spring 支持 ### 来导出 Web 服务 + +作为 GlassFish 项目的一部分开发的 Oracle 的 JAX-WS RI,作为其 JAX-WS Commons 项目的一部分提供了 Spring 支持。这允许将 JAX-WS 端点定义为 Spring-managed bean,类似于[上一节](#remoting-web-services-jaxws-export-standalone)中讨论的独立模式——但这次是在 Servlet 环境中。 + +| |这在 Java EE 环境中是不可移植的。它主要用于非 EE<br/>环境,例如 Tomcat,这些环境将 JAX-WS RI 嵌入为 Web 应用程序的一部分。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +与导出基于 Servlet 的端点的标准样式的不同之处在于,端点实例本身的生命周期由 Spring 管理,并且在`web.xml`中只定义了一个 JAX-WS Servlet。使用标准的 Java EE 样式(如前面所示),每个服务端点有一个 Servlet 定义,每个端点通常委派给 Spring bean(通过使用`@Autowired`,如前面所示)。 + +有关设置和使用方式的详细信息,请参见[https://jax-ws-commons.java.net/spring/](https://jax-ws-commons.java.net/spring/)。 + +#### 2.3.4.使用 JAX-WS 访问 Web 服务 + +Spring 提供了两个工厂 bean 来创建 JAX-WS Web 服务代理,即 localJaxWsServiceFactoryBean` 和<gtr="391"/>。前者只能返回一个 JAX-WS 服务类供我们使用。后者是成熟的版本,可以返回实现我们的业务服务接口的代理。在下面的示例中,我们使用`JaxWsPortProxyFactoryBean`为“AccountService”端点创建代理(再次): + +``` +<bean id="accountWebService" class="org.springframework.remoting.jaxws.JaxWsPortProxyFactoryBean"> + <property name="serviceInterface" value="example.AccountService"/> (1) + <property name="wsdlDocumentUrl" value="http://localhost:8888/AccountServiceEndpoint?WSDL"/> + <property name="namespaceUri" value="https://example/"/> + <property name="serviceName" value="AccountService"/> + <property name="portName" value="AccountServiceEndpointPort"/> +</bean> +``` + +|**1**|其中`serviceInterface`是客户机使用的业务接口。| +|-----|------------------------------------------------------------------------| + +`wsdlDocumentUrl`是 WSDL 文件的 URL。 Spring 在启动时需要这个来创建 JAX-WS 服务。`namespaceUri`对应于.wsdl 文件中的`targetNamespace`。`serviceName`对应于.wsdl 文件中的服务名称。`portName`对应于.wsdl 文件中的端口号。 + +访问 Web 服务很容易,因为我们有一个 Bean 工厂将其公开为一个名为`AccountService`的接口。下面的示例展示了我们如何在 Spring 中将其连接起来: + +``` +<bean id="client" class="example.AccountClientImpl"> + ... + <property name="service" ref="accountWebService"/> +</bean> +``` + +从客户机代码中,我们可以像访问普通类一样访问 Web 服务,如下例所示: + +``` +public class AccountClientImpl { + + private AccountService service; + + public void setService(AccountService service) { + this.service = service; + } + + public void foo() { + service.insertAccount(...); + } +} +``` + +| |上面稍微简化了一下,因为 JAX-WS 要求端点接口<br/>和实现类使用`@WebService`、`@SOAPBinding`等注释<br/>注释。这意味着你不能(轻松地)使用普通的 Java 接口和<br/>实现类作为 JAX-WS 端点工件;你需要首先对它们<br/>进行相应的注释。查看 JAX-WS 文档,了解有关这些需求的详细信息。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 2.4.RMI(已弃用) + +| |截至 Spring 框架 5.3,RMI 支持是不受欢迎的,并且不会被替换。| +|---|-------------------------------------------------------------------------------| + +通过使用 Spring 对 RMI 的支持,你可以通过 RMI 基础设施透明地公开你的服务。在进行了此设置之后,你基本上拥有了类似于远程 EJB 的配置,除了没有对安全上下文传播或远程事务传播的标准支持这一事实。 Spring 在使用 RMI 调用程序时确实为这样的附加调用上下文提供了挂钩,因此可以例如插入安全框架或自定义安全凭据。 + +#### 2.4.1.使用`RmiServiceExporter`导出服务 + +使用`RmiServiceExporter`,我们可以将 AccountService 对象的接口公开为 RMI 对象。该接口可以通过使用`RmiProxyFactoryBean`进行访问,或者在传统的 RMI 服务的情况下通过普通 RMI 进行访问。`RmiServiceExporter`显式地支持通过 RMI 调用程序公开任何非 RMI 服务。 + +我们首先必须在 Spring 容器中设置我们的服务。下面的示例展示了如何做到这一点: + +``` +<bean id="accountService" class="example.AccountServiceImpl"> + <!-- any additional properties, maybe a DAO? --> +</bean> +``` + +接下来,我们必须使用`RmiServiceExporter`公开我们的服务。下面的示例展示了如何做到这一点: + +``` +<bean class="org.springframework.remoting.rmi.RmiServiceExporter"> + <!-- does not necessarily have to be the same name as the bean to be exported --> + <property name="serviceName" value="AccountService"/> + <property name="service" ref="accountService"/> + <property name="serviceInterface" value="example.AccountService"/> + <!-- defaults to 1099 --> + <property name="registryPort" value="1199"/> +</bean> +``` + +在前面的示例中,我们重写了 RMI 注册中心的端口。通常,你的应用程序服务器还维护一个 RMI 注册中心,因此不干预该注册中心是明智的。此外,服务名称用于绑定服务。因此,在前面的示例中,服务绑定在`'rmi://HOST:1199/AccountService'`。稍后,我们将使用此 URL 在客户端的服务中进行链接。 + +| |省略了`servicePort`属性(默认为 0)。这意味着使用<br/>匿名端口与服务通信。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------| + +#### 2.4.2.在客户端的服务中链接 + +我们的客户机是一个简单的对象,它使用`AccountService`来管理帐户,如下例所示: + +``` +public class SimpleObject { + + private AccountService accountService; + + public void setAccountService(AccountService accountService) { + this.accountService = accountService; + } + + // additional methods using the accountService +} +``` + +为了在客户端上链接服务,我们创建了一个单独的 Spring 容器,以包含以下简单的对象和服务连接配置位: + +``` +<bean class="example.SimpleObject"> + <property name="accountService" ref="accountService"/> +</bean> + +<bean id="accountService" class="org.springframework.remoting.rmi.RmiProxyFactoryBean"> + <property name="serviceUrl" value="rmi://HOST:1199/AccountService"/> + <property name="serviceInterface" value="example.AccountService"/> +</bean> +``` + +这就是我们在客户端上支持远程帐户服务所需要做的全部工作。 Spring 透明地创建调用程序,并通过“rmiserviceexporter”远程启用帐户服务。在客户端,我们使用`RmiProxyFactoryBean`将其连接进来。 + +### 2.5.使用 Hessian 通过 HTTP 远程调用服务(已弃用) + +| |截至 Spring 框架 5.3,Hessian 支持已被弃用,并且不会被替换。| +|---|-----------------------------------------------------------------------------------| + +Hessian 提供了一种基于二进制 HTTP 的远程处理协议。它是由 Caucho 开发的,你可以在[https://www.caucho.com/](https://www.caucho.com/)上找到有关黑森本身的更多信息。 + +#### 2.5.1.黑森 + +Hessian 通过 HTTP 进行通信,并通过使用自定义 Servlet 进行通信。通过使用 Spring 的“DispatcherServlet”原则(参见[webmvc.html](webmvc.html#mvc-servlet)),我们可以连接这样的 Servlet 来公开你的服务。首先,我们必须在我们的应用程序中创建一个新的 Servlet,如以下`web.xml`的节选所示: + +``` +<servlet> + <servlet-name>remoting</servlet-name> + <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> + <load-on-startup>1</load-on-startup> +</servlet> + +<servlet-mapping> + <servlet-name>remoting</servlet-name> + <url-pattern>/remoting/*</url-pattern> +</servlet-mapping> +``` + +如果你熟悉 Spring 的`DispatcherServlet`原则,那么你可能知道,现在你必须在`WEB-INF`目录中创建一个名为 `remoting- Servlet.xml` 的 Spring 容器配置资源(以你的 Servlet 的名称命名)。应用程序上下文将在下一节中使用。 + +或者,考虑使用 Spring 的更简单的`HttpRequestHandlerServlet`。这样,你就可以在根应用程序上下文(默认情况下,在`WEB-INF/applicationContext.xml`中)中嵌入远程导出定义,并使用单独的 Servlet 定义指向特定的导出 bean。在这种情况下,每个 Servlet 名称需要匹配其目标输出器的 Bean 名称。 + +#### 2.5.2.使用`HessianServiceExporter`暴露 bean + +在新创建的名为`remoting-servlet.xml`的应用程序上下文中,我们创建一个 `HessianServiceExporter’来导出我们的服务,如下例所示: + +``` +<bean id="accountService" class="example.AccountServiceImpl"> + <!-- any additional properties, maybe a DAO? --> +</bean> + +<bean name="/AccountService" class="org.springframework.remoting.caucho.HessianServiceExporter"> + <property name="service" ref="accountService"/> + <property name="serviceInterface" value="example.AccountService"/> +</bean> +``` + +现在,我们已经准备好在客户端的服务链接。没有指定显式的处理程序映射(以将请求 URL 映射到服务上),因此我们使用`BeanNameUrlHandlerMapping`。因此,该服务在包含`DispatcherServlet`实例映射(如前面定义的)中的 Bean 名称所指示的 URL 处导出:`[https://host:8080/remoting/accountservice](https://HOST:8080/remoting/AccountService)`。 + +或者,你可以在根应用程序上下文中创建`HessianServiceExporter`(例如,在`WEB-INF/applicationContext.xml`中),如下例所示: + +``` +<bean name="accountExporter" class="org.springframework.remoting.caucho.HessianServiceExporter"> + <property name="service" ref="accountService"/> + <property name="serviceInterface" value="example.AccountService"/> +</bean> +``` + +在后一种情况下,你应该在`web.xml`中为这个输出器定义一个相应的 Servlet,其最终结果是相同的:输出器被映射到位于 `/remoting/accountService` 处的请求路径。请注意, Servlet 名称需要与目标输出器的 Bean 名称匹配。下面的示例展示了如何做到这一点: + +``` +<servlet> + <servlet-name>accountExporter</servlet-name> + <servlet-class>org.springframework.web.context.support.HttpRequestHandlerServlet</servlet-class> +</servlet> + +<servlet-mapping> + <servlet-name>accountExporter</servlet-name> + <url-pattern>/remoting/AccountService</url-pattern> +</servlet-mapping> +``` + +#### 2.5.3.在客户端的服务中链接 + +通过使用`HessianProxyFactoryBean`,我们可以在客户端的服务中进行链接。同样的原则也适用于 RMI 的例子。我们创建一个单独的 Bean 工厂或应用程序上下文,并提到以下 bean,其中`SimpleObject`是通过使用`AccountService`来管理帐户的,如下例所示: + +``` +<bean class="example.SimpleObject"> + <property name="accountService" ref="accountService"/> +</bean> + +<bean id="accountService" class="org.springframework.remoting.caucho.HessianProxyFactoryBean"> + <property name="serviceUrl" value="https://remotehost:8080/remoting/AccountService"/> + <property name="serviceInterface" value="example.AccountService"/> +</bean> +``` + +#### 2.5.4.将 HTTP 基本身份验证应用于通过 Hessian 公开的服务 #### + +Hessian 的优点之一是我们可以轻松地应用 HTTP 基本身份验证,因为这两个协议都是基于 HTTP 的。例如,可以通过使用`web.xml`安全特性来应用正常的 HTTP 服务器安全机制。通常,在此不需要使用每个用户的安全凭据。相反,你可以使用在`HessianProxyFactoryBean`级别定义的共享凭据(类似于 JDBC`DataSource`),如下例所示: + +``` +<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"> + <property name="interceptors" ref="authorizationInterceptor"/> +</bean> + +<bean id="authorizationInterceptor" + class="org.springframework.web.servlet.handler.UserRoleAuthorizationInterceptor"> + <property name="authorizedRoles" value="administrator,operator"/> +</bean> +``` + +在前面的示例中,我们明确地提到了`BeanNameUrlHandlerMapping`并设置了一个拦截器,以便只让管理员和操作员调用在此应用程序上下文中提到的 bean。 + +| |前面的示例并未展示一种灵活的安全基础架构。对于<br/>关于安全性的更多选项,请查看 Spring 安全性项目<br/>at[https://projects.spring.io/spring-security/](https://projects.spring.io/spring-security/)。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 2.6. Spring HTTP Invoker(不推荐) + +| |在 Spring Framework5.3 中,HTTP Invoker 支持已被弃用,不会被替换。| +|---|----------------------------------------------------------------------------------------| + +Spring HTTP 调用程序与 Hessian 相反,都是轻量级协议,它们使用自己的 Slim 序列化机制,并使用标准的 Java 序列化机制通过 HTTP 公开服务。如果你的参数和返回类型是复杂的类型,无法通过使用 Hessian 使用的序列化机制进行序列化,那么这将具有巨大的优势(在选择远程技术时,请参阅下一节以了解更多的考虑因素)。 + +在这种情况下, Spring 使用 JDK 或 Apache`HttpComponents`提供的标准工具来执行 HTTP 调用。如果你需要更高级、更易用的功能,请使用后者。有关更多信息,请参见[hc.apache.org/httpcomponents-client-ga/](https://hc.apache.org/httpcomponents-client-ga/)。 + +| |注意由不安全的 Java 反序列化引起的漏洞:<br/>在反序列化步骤期间,被操纵的输入流可能导致服务器上执行不需要的代码<br/>。因此,不要将 HTTP Invoker<br/>端点公开给不受信任的客户端。相反,只在你自己的服务之间公开它们。<br/>总的来说,我们强烈建议使用任何其他消息格式(例如 JSON),<br/><br/>如果你担心 Java 序列化带来的安全漏洞,<br/>请考虑核心 JVM 级别的通用序列化过滤机制,<br/>最初是为 JDK9 开发的,但后来移植到了 JDK8,同时是 7 号和 6 号。见[https://blogs.oracle.com/java-platform-group/entry/incoming\_filter\_serialization\_data\_a](https://blogs.oracle.com/java-platform-group/entry/incoming_filter_serialization_data_a)和[https://openjdk.java.net/jeps/290](https://openjdk.java.net/jeps/290)。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 2.6.1.公开服务对象 + +为服务对象设置 HTTP 调用程序基础结构与使用 Hessian 进行相同设置的方式非常相似。由于 Hessian 支持提供了“HessianServiceExporter”, Spring 的 Httpinvoker 支持提供了“org.SpringFramework.Remoting.Httpinvoker.HttpinvokerServiceExporter”。 + +要在 Spring Web MVC`DispatcherServlet’中公开`AccountService`(前面提到过),需要在 Dispatcher 的应用程序上下文中设置以下配置,如下例所示: + +``` +<bean name="/AccountService" class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter"> + <property name="service" ref="accountService"/> + <property name="serviceInterface" value="example.AccountService"/> +</bean> +``` + +这样的导出定义通过`DispatcherServlet`实例的标准映射工具公开,如[关于黑森的章节](#remoting-caucho-protocols)中所解释的那样。 + +或者,你可以在根应用程序上下文中创建`HttpInvokerServiceExporter`(例如,在`'WEB-INF/applicationContext.xml'`中),如下例所示: + +``` +<bean name="accountExporter" class="org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter"> + <property name="service" ref="accountService"/> + <property name="serviceInterface" value="example.AccountService"/> +</bean> +``` + +此外,可以在`web.xml`中为该输出器定义相应的 Servlet,其 Servlet 名称与目标输出器的 Bean 名称匹配,如下例所示: + +``` +<servlet> + <servlet-name>accountExporter</servlet-name> + <servlet-class>org.springframework.web.context.support.HttpRequestHandlerServlet</servlet-class> +</servlet> + +<servlet-mapping> + <servlet-name>accountExporter</servlet-name> + <url-pattern>/remoting/AccountService</url-pattern> +</servlet-mapping> +``` + +#### 2.6.2.在客户端的服务中链接 + +同样,从客户机在服务中进行链接与使用 Hessian 时的方式非常相似。通过使用代理, Spring 可以将对 HTTP POST 请求的调用转换为指向导出服务的 URL。下面的示例展示了如何配置这种安排: + +``` +<bean id="httpInvokerProxy" class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean"> + <property name="serviceUrl" value="https://remotehost:8080/remoting/AccountService"/> + <property name="serviceInterface" value="example.AccountService"/> +</bean> +``` + +如前所述,你可以选择要使用的 HTTP 客户机。默认情况下,“HttpinVokerProxy”使用 JDK 的 HTTP 功能,但你也可以通过设置`httpInvokerRequestExecutor`属性来使用 Apache“HttpComponents”客户端。下面的示例展示了如何做到这一点: + +``` +<property name="httpInvokerRequestExecutor"> + <bean class="org.springframework.remoting.httpinvoker.HttpComponentsHttpInvokerRequestExecutor"/> +</property> +``` + +### 2.7.JMS(已弃用) + +| |从 Spring Framework5.3 开始,JMS 远程支持已被弃用,不会被替换。| +|---|----------------------------------------------------------------------------------------| + +你还可以通过使用 JMS 作为底层通信协议来透明地公开服务。 Spring 框架中的 JMS 远程支持非常基本。它在`same thread`上发送和接收,并在相同的非事务性`Session`上发送和接收。因此,吞吐量是依赖于实现的。请注意,这些单线程和非事务约束仅适用于 Spring 的 JMS 远程支持。有关 Spring 对基于 JMS 的消息传递的丰富支持的信息,请参见[JMS(Java 消息服务)](#jms)。 + +服务器端和客户端都使用以下接口: + +``` +package com.foo; + +public interface CheckingAccountService { + + public void cancelAccount(Long accountId); +} +``` + +在服务器端使用了前面接口的以下简单实现: + +``` +package com.foo; + +public class SimpleCheckingAccountService implements CheckingAccountService { + + public void cancelAccount(Long accountId) { + System.out.println("Cancelling account [" + accountId + "]"); + } +} +``` + +以下配置文件包含在客户机和服务器上共享的 JMS-Infrastructure bean: + +``` +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans + https://www.springframework.org/schema/beans/spring-beans.xsd"> + + <bean id="connectionFactory" class="org.apache.activemq.ActiveMQConnectionFactory"> + <property name="brokerURL" value="tcp://ep-t43:61616"/> + </bean> + + <bean id="queue" class="org.apache.activemq.command.ActiveMQQueue"> + <constructor-arg value="mmm"/> + </bean> + +</beans> +``` + +#### 2.7.1.服务器端配置 + +在服务器上,你需要公开使用“jmsinVokerServiceExporter”的服务对象,如下例所示: + +``` +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans + https://www.springframework.org/schema/beans/spring-beans.xsd"> + + <bean id="checkingAccountService" + class="org.springframework.jms.remoting.JmsInvokerServiceExporter"> + <property name="serviceInterface" value="com.foo.CheckingAccountService"/> + <property name="service"> + <bean class="com.foo.SimpleCheckingAccountService"/> + </property> + </bean> + + <bean class="org.springframework.jms.listener.SimpleMessageListenerContainer"> + <property name="connectionFactory" ref="connectionFactory"/> + <property name="destination" ref="queue"/> + <property name="concurrentConsumers" value="3"/> + <property name="messageListener" ref="checkingAccountService"/> + </bean> + +</beans> +``` + +``` +package com.foo; + +import org.springframework.context.support.ClassPathXmlApplicationContext; + +public class Server { + + public static void main(String[] args) throws Exception { + new ClassPathXmlApplicationContext("com/foo/server.xml", "com/foo/jms.xml"); + } +} +``` + +#### 2.7.2.客户端配置 + +客户机只需要创建一个实现约定接口的客户端代理(“checkingAccountService”)。 + +下面的示例定义了可以注入到其他客户端对象中的 bean(代理负责通过 JMS 将调用转发到服务器端对象): + +``` +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans + https://www.springframework.org/schema/beans/spring-beans.xsd"> + + <bean id="checkingAccountService" + class="org.springframework.jms.remoting.JmsInvokerProxyFactoryBean"> + <property name="serviceInterface" value="com.foo.CheckingAccountService"/> + <property name="connectionFactory" ref="connectionFactory"/> + <property name="queue" ref="queue"/> + </bean> + +</beans> +``` + +``` +package com.foo; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +public class Client { + + public static void main(String[] args) throws Exception { + ApplicationContext ctx = new ClassPathXmlApplicationContext("com/foo/client.xml", "com/foo/jms.xml"); + CheckingAccountService service = (CheckingAccountService) ctx.getBean("checkingAccountService"); + service.cancelAccount(new Long(10)); + } +} +``` + +## 3. EnterpriseJavaBeans 集成 + +Spring 作为一种轻量级容器,通常被认为是 EJB 的替代品。我们确实认为,对于许多(如果不是大多数的话)应用程序和用例, Spring 作为一个容器,结合其在事务、ORM 和 JDBC 访问领域的丰富支持功能,是比通过 EJB 容器和 EJB 实现等效功能更好的选择。 + +然而,需要注意的是,使用 Spring 并不妨碍你使用 EJB。实际上, Spring 使得访问 EJB 和在其中实现 EJB 和功能变得更加容易。另外,使用 Spring 来访问由 EJB 提供的服务允许这些服务的实现稍后在本地 EJB、远程 EJB 或 POJO(纯旧的 Java 对象)变体之间透明地进行切换,而无需更改客户端代码。 + +在本章中,我们将研究 Spring 如何帮助你访问和实现 EJB。 Spring 在访问无状态会话 bean 时提供了特定的值,因此我们首先讨论这个主题。 + +### 3.1.访问 EJB + +本节介绍如何访问 EJB。 + +#### 3.1.1.概念 + +Bean 要在本地或远程无状态会话上调用方法,客户端代码通常必须执行 JNDI 查找以获得(本地或远程)EJB 主对象,然后在该对象上使用`create`方法调用以获得实际的(本地或远程)EJB 对象。然后在 EJB 上调用一个或多个方法。 + +为了避免重复的低级代码,许多 EJB 应用程序使用服务定位器和业务委托模式。这些比在整个客户机代码中进行大量的 JNDI 查找更好,但是它们通常的实现有很大的缺点: + +* 通常,使用 EJB 的代码依赖于服务定位器或业务委托单例,这使得很难进行测试。 + +* 在没有业务委托的情况下使用服务定位器模式的情况下,应用程序代码仍然必须在 EJB 主页上调用`create()`方法并处理由此产生的异常。因此,它仍然与 EJB API 和 EJB 编程模型的复杂性联系在一起。 + +* 实现业务委托模式通常会导致大量的代码重复,在这种情况下,我们必须编写许多在 EJB 上调用相同方法的方法。 + +Spring 方法是允许创建和使用代理对象(通常在 Spring 容器内配置),这些代理对象充当无码业务委托。你不需要在手工编码的业务委托中编写另一个服务定位器、另一个 JNDI 查找或重复的方法,除非你实际上在这样的代码中添加了真正的价值。 + +#### 3.1.2.访问本地 SLSB + +假设我们有一个需要使用本地 EJB 的 Web 控制器。我们遵循最佳实践并使用 EJB 业务方法接口模式,这样 EJB 的本地接口扩展了一个非 EJB 特定的业务方法接口。我们称这种业务方法接口`MyComponent`。下面的示例展示了这样的接口: + +``` +public interface MyComponent { + ... +} +``` + +Bean 使用业务方法接口模式的主要原因之一是确保本地接口中的方法签名和实现类之间的同步是自动的。另一个原因是,如果切换到服务的 POJO(纯旧的 Java 对象)实现是有意义的,那么以后切换到 POJO(纯旧的 Java 对象)实现会更容易。我们还需要实现本地 Home 接口,并提供一个实现`SessionBean`和`MyComponent`业务方法接口的实现类。现在,要将 Web 层控制器连接到 EJB 实现,我们需要做的唯一 Java 编码是在控制器上公开一个类型`MyComponent`的 setter 方法。这将引用保存为控制器中的实例变量。下面的示例展示了如何做到这一点: + +``` +private MyComponent myComponent; + +public void setMyComponent(MyComponent myComponent) { + this.myComponent = myComponent; +} +``` + +随后,我们可以在控制器中的任何业务方法中使用此实例变量。现在,假设我们从 Spring 容器中获得控制器对象,我们可以(在相同的上下文中)配置`LocalStatelessSessionProxyFactoryBean`实例,它是 EJB 代理对象。我们配置代理,并使用以下配置条目设置控制器的“mycomponent”属性: + +``` +<bean id="myComponent" + class="org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean"> + <property name="jndiName" value="ejb/myBean"/> + <property name="businessInterface" value="com.mycom.MyComponent"/> +</bean> + +<bean id="myController" class="com.mycom.myController"> + <property name="myComponent" ref="myComponent"/> +</bean> +``` + +由于 Spring AOP 框架,大量的工作在幕后进行,尽管你并不是被迫使用 AOP 概念来享受结果。 Bean“MyComponent”定义为 EJB 创建了一个代理,该代理实现了业务方法接口。EJB 本地主页是在启动时缓存的,因此只有一个 JNDI 查找。每次调用 EJB 时,代理都调用本地 EJB 上的`classname`方法,并在 EJB 上调用相应的业务方法。 + +`myController` Bean 定义将控制器类的`myComponent`属性设置为 EJB 代理。 + +或者(最好是在许多这样的代理定义的情况下),考虑在 Spring 的“jee”命名空间中使用`<jee:local-slsb>`配置元素。下面的示例展示了如何做到这一点: + +``` +<jee:local-slsb id="myComponent" jndi-name="ejb/myBean" + business-interface="com.mycom.MyComponent"/> + +<bean id="myController" class="com.mycom.myController"> + <property name="myComponent" ref="myComponent"/> +</bean> +``` + +这种 EJB 访问机制极大地简化了应用程序代码。Web 层代码(或其他 EJB 客户机代码)不依赖于 EJB 的使用。要用 POJO 或模拟对象或其他测试存根替换此 EJB 引用,我们可以在不更改一行 Java 代码的情况下更改`myComponent` Bean 定义。此外,我们不需要编写一行 JNDI 查找或其他 EJB 管道代码作为应用程序的一部分。 + +实际应用程序中的基准测试和经验表明,这种方法(涉及目标 EJB 的反射调用)的性能开销很小,并且在典型使用中是无法检测到的。请记住,我们无论如何都不想对 EJB 进行细粒度的调用,因为在应用程序服务器中存在与 EJB 基础设施相关的成本。 + +有一个关于 JNDI 查找的警告。在 Bean 容器中,这个类通常最好作为单例使用(没有理由将其作为原型)。但是,如果 Bean 容器预先实例化了单例(就像各种 XML`ApplicationContext’变体一样),那么如果 Bean 容器是在 EJB 容器加载目标 EJB 之前加载的,那么你可能会遇到问题。这是因为 JNDI 查找是在该类的`init()`方法中执行的,然后进行缓存,但是 EJB 还没有被绑定到目标位置。解决方案是不预先实例化这个工厂对象,而是让它在第一次使用时就被创建。在 XML 容器中,你可以通过使用`lazy-init`属性来控制这一点。 + +虽然大多数 Spring 用户不感兴趣,但是那些使用 EJB 进行编程工作的用户可能希望查看`LocalSlsbInvokerInterceptor`。 + +#### 3.1.3.访问远程 SLSB + +访问远程 EJB 本质上与访问本地 EJB 相同,只是使用了“SimpleRemoteStatelessionProxyFactoryBean”或<gtr="478"/>配置元素。当然,不管有没有 Spring,远程调用语义都适用:对另一台计算机中另一 VM 中的对象上的方法的调用,有时确实必须在使用场景和故障处理方面进行不同的处理。 + +Spring 的 EJB 客户机支持比非 Spring 方法增加了一个优势。通常情况下,EJB 客户机代码在本地或远程调用 EJB 之间容易地来回切换是有问题的。这是因为远程接口方法必须声明它们抛出`RemoteException`,而客户端代码必须处理这个问题,而本地接口方法则不需要。为本地 EJB 编写的客户端代码需要迁移到远程 EJB,通常需要对其进行修改,以添加对远程异常的处理,而为远程 EJB 编写的、需要转移到本地 EJB 的客户机代码可以保持不变,但可以对远程异常进行大量不必要的处理,或者进行修改以删除该代码。使用 Spring 远程 EJB 代理,你可以不声明在你的业务方法接口和实现 EJB 代码中抛出的任何,而是具有相同的远程接口(除了它确实抛出),并依赖代理来动态地对待这两个接口,就像它们是相同的一样。即,客户端代码不必处理选中的`RemoteException`类。在 EJB 调用期间抛出的任何实际`RemoteException`都将被重新抛出为未选中的`RemoteAccessException`类,它是`RuntimeException`的子类。然后,你可以在本地 EJB 或远程 EJB(甚至是普通的 Java 对象)实现之间随意切换目标服务,而不需要客户机代码知道或关心。当然,这是可选的:没有什么可以阻止你在业务接口中声明`RemoteException`。 + +#### 3.1.4.访问 EJB2.x slsbs 与 EJB3slsbs + +通过 Spring 访问 EJB2.x 会话 bean 和 EJB3 会话 bean 在很大程度上是透明的。 Spring 的 EJB 访问器,包括`<jee:local-slsb>`和<jee:remote-slsb>’设施,在运行时透明地适应实际组件。如果找到了 home 接口(EJB2.x 样式),他们将处理该接口;如果没有 home 接口,他们将执行直接的组件调用(EJB3 样式)。 + +注意:对于 EJB3 会话 bean,你也可以有效地使用`JndiObjectFactoryBean`/<jee:jndi-lookup>`,因为完全可用的组件引用在那里公开用于普通的 JNDI 查找。定义显式`<jee:local-slsb>`或`<jee:remote-slsb>`查找提供了一致和更显式的 EJB 访问配置。 + +## 4. JMS(Java 消息服务) + +Spring 提供了一种 JMS 集成框架,该框架简化了 JMS API 的使用,其方式与 Spring 的集成为 JDBC API 提供的方式大致相同。 + +JMS 可以大致分为两个功能领域,即消息的产生和使用。`JmsTemplate`类用于消息产生和同步消息接收。对于类似于 Java EE 的消息驱动 Bean 风格的异步接收, Spring 提供了许多消息侦听器容器,你可以使用这些容器来创建消息驱动的 POJO。 Spring 还提供了一种声明性的方式来创建消息侦听器。 + +`org.springframework.jms.core`包提供了使用 JMS 的核心功能。它包含 JMS 模板类,这些类通过处理资源的创建和发布来简化 JMS 的使用,就像`JdbcTemplate`为 JDBC 所做的那样。 Spring 模板类的共同设计原则是提供辅助方法来执行公共操作,并且为了更复杂的使用,将处理任务的本质委托给用户实现的回调接口。JMS 模板遵循相同的设计。这些类为发送消息、同步消费消息以及向用户公开 JMS 会话和消息生成器提供了各种方便的方法。 + +`org.springframework.jms.support`包提供了`JMSException`翻译功能。转换将选中的`JMSException`层次结构转换为未选中异常的镜像层次结构。如果选中`javax.jms.JMSException`的任何特定于提供者的子类存在,则该异常将被包装在未选中的`UncategorizedJmsException`中。 + +`org.springframework.jms.support.converter`包提供了一个`MessageConverter`抽象,用于在 Java 对象和 JMS 消息之间进行转换。 + +`org.springframework.jms.support.destination`包提供了用于管理 JMS 目的地的各种策略,例如为存储在 JNDI 中的目的地提供服务定位器。 + +`org.springframework.jms.annotation`包提供了必要的基础设施,通过使用`@JmsListener`支持注释驱动的侦听器端点。 + +`org.springframework.jms.config`包提供了 `JMS’名称空间的解析器实现,以及用于配置侦听器容器和创建侦听器端点的 Java Config 支持。 + +最后,`org.springframework.jms.connection`包提供了适合于在独立应用程序中使用的`ConnectionFactory`的实现。它还包含 Spring 的`PlatformTransactionManager`for JMS 的实现(巧妙地命名为“jmstransactionManager”)。这允许将 JMS 作为事务资源无缝地集成到 Spring 的事务管理机制中。 + +| |在 Spring Framework5 中, Spring 的 JMS 包完全支持 JMS2.0,并且要求在运行时存在<br/>JMS2.0API。我们建议使用与 JMS2.0 兼容的提供者。<br/><br/>如果你的系统中使用了较旧的消息代理,则可以尝试为你现有的代理生成升级到与<br/>JMS2.0 兼容的驱动程序。或者,你也可以<br/>尝试在基于 JMS1.1 的驱动程序上运行,只需将 JMS2.0API jar 放在<br/> Classpath 上,但仅对你的驱动程序使用与 JMS1.1 兼容的 API。 Spring 的 JMS 支持<br/>默认情况下遵循 JMS1.1 约定,因此通过相应的配置,它确实<br/>支持这样的场景。但是,请仅在转换场景中考虑这一点。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 4.1.使用 Spring JMS + +本节描述了如何使用 Spring 的 JMS 组件。 + +#### 4.1.1.使用`JmsTemplate` + +`JmsTemplate`类是 JMS 核心包中的中心类。它简化了 JMS 的使用,因为它在发送或同步接收消息时处理资源的创建和释放。 + +使用`JmsTemplate`的代码只需要实现回调接口,从而为它们提供一个明确定义的高级契约。当`MessageCreator`调用代码在`JmsTemplate`中提供了`Session`时,回调接口将创建一条消息。为了允许更复杂地使用 JMS API,`SessionCallback`提供了 JMS 会话,而`ProducerCallback`公开了一个`Session`和 `MessageProducer’对。 + +JMS API 公开了两种类型的发送方法,一种是将交付模式、优先级和实时作为服务质量参数,另一种是不接受 QoS 参数并使用默认值。由于`JmsTemplate`有许多发送方法,所以将 QoS 参数设置为 Bean 属性已公开,以避免发送方法数量的重复。类似地,同步接收调用的超时值是通过使用`setReceiveTimeout`属性设置的。 + +一些 JMS 提供程序允许通过`ConnectionFactory`的配置在管理上设置默认的 QoS 值。这样做的结果是,调用 `MessageProducer’实例的`send`方法(`Send(DestinateDestination,MessageMessage Message)’)所使用的 QoS 默认值与 JMS 规范中指定的值不同。因此,为了提供一致的 QoS 值管理,必须通过将布尔属性 `isexplicitqosenabled’设置为<gtr="532"/>,明确地使<gtr="531"/>能够使用其自己的 QoS 值。 + +为了方便起见,`JmsTemplate`还公开了一个基本的请求-答复操作,该操作允许在作为操作的一部分而创建的临时队列上发送消息并等待答复。 + +| |一旦配置好,`JmsTemplate`类的实例是线程安全的。这是<br/>重要的,因为这意味着你可以配置`JmsTemplate`的单个实例,然后安全地将此共享引用注入多个协作者。要使<br/>清晰,`JmsTemplate`是有状态的,因为它保持了对 `connectionFactory’的引用,但该状态不是会话状态。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在 Spring Framework4.1 中,`JmsMessagingTemplate`构建在`JmsTemplate`之上,并提供了与消息抽象的集成——即 `org.springframework.messaging.message`。这使你能够以通用的方式创建要发送的消息。 + +#### 4.1.2.连接 + +`JmsTemplate`需要引用`ConnectionFactory`。`ConnectionFactory`是 JMS 规范的一部分,是使用 JMS 的入口点。客户机应用程序将其用作工厂,以创建与 JMS 提供者的连接,并封装各种配置参数,其中许多是特定于供应商的,例如 SSL 配置选项。 + +当在 EJB 中使用 JMS 时,供应商提供 JMS 接口的实现,以便他们可以参与声明式事务管理并执行连接和会话的池。为了使用此实现,Java EE 容器通常要求你在 EJB 或 Servlet 部署描述符中将 JMS 连接工厂声明为`resource-ref`。为了确保在 EJB 中的“JMStemplate”中使用这些特性,客户端应用程序应该确保它引用`ConnectionFactory`的托管实现。 + +##### 缓存消息传递资源 + +标准 API 涉及创建许多中间对象。要发送消息,需要执行以下“API”遍历: + +``` +ConnectionFactory->Connection->Session->MessageProducer->send +``` + +在`ConnectionFactory`和`Send`操作之间,创建并销毁了三个中间对象。为了优化资源使用并提高性能, Spring 提供了`ConnectionFactory`的两种实现方式。 + +##### 使用`SingleConnectionFactory` + +Spring 提供了`ConnectionFactory`接口 `SingleConnectionFactory’的实现,该实现在所有 `createConnection()’调用上返回相同的`Connection`,并忽略对`close()`的调用。这对于测试和独立环境非常有用,因此同一个连接可以用于多个“JMStemplate”调用,这些调用可以跨越任意数量的事务。`SingleConnectionFactory`引用了通常来自 JNDI 的标准`ConnectionFactory`。 + +##### 使用`CachingConnectionFactory` + +`CachingConnectionFactory`扩展了`SingleConnectionFactory`的功能,并添加了`Session`、`MessageProducer`和`MessageConsumer`实例的缓存。初始缓存大小设置为`1`。你可以使用`sessionCacheSize`属性来增加缓存的会话的数量。请注意,实际缓存的会话的数量要多于这个数量,因为会话是基于它们的确认模式进行缓存的,因此当`sessionCacheSize`设置为 1 时,最多可以有 4 个缓存的会话实例(每个确认模式一个)。`MessageProducer`和`MessageConsumer`实例在其所属会话中进行缓存,并且在缓存时还考虑到生产者和消费者的独特属性。消息生成器是根据它们的目的地进行缓存的。MessageConsumer 是基于一个由 Destination、Selector、noLocal Delivery 标志和持久订阅名称(如果创建持久消费者的话)组成的键来缓存的。 + +#### 4.1.3.目的地管理 + +Destinations 作为`ConnectionFactory`实例,是可以在 JNDI 中存储和检索的 JMS 管理的对象。在配置 Spring 应用程序上下文时,可以使用 JNDI工厂类或对对象对 JMS 目的地的引用执行依赖项注入。但是,如果应用程序中有大量的目的地,或者如果有 JMS 提供程序独有的高级目的地管理功能,那么这种策略通常会很麻烦。这种高级目的地管理的示例包括创建动态目的地或支持目的地的分级命名空间。`JmsTemplate`将目标名称的解析委托给实现“destinationResolver”接口的 JMS Destination 对象。`DynamicDestinationResolver`是`JmsTemplate`使用的默认实现,并适应解析动态目的地。还提供了一个“JNDestInationResolver”,作为 JNDI 中包含的目的地的服务定位器,并可选地退回到“DynamicDestinationResolver”中包含的行为。 + +通常情况下,JMS 应用程序中使用的目标仅在运行时是已知的,因此,在部署应用程序时不能以管理方式创建目标。这通常是因为在交互的系统组件之间存在共享的应用程序逻辑,这些组件根据众所周知的命名约定在运行时创建目标。尽管动态目的地的创建不是 JMS 规范的一部分,但大多数供应商都提供了这种功能。动态目的地是用用户定义的名称创建的,这将它们与临时目的地区分开来,并且通常不会在 JNDI 中注册。用于创建动态目的地的 API 因提供者而异,因为与目的地关联的属性是特定于供应商的。然而,供应商有时会做出一个简单的实现选择,那就是忽略 JMS 规范中的警告,并使用方法`TopicSession`createtopic(字符串 topicname)` 或`QueueSession``createQueue(String queueName)`方法创建具有默认目标属性的新目标。根据供应商的实现,`DynamicDestinationResolver`还可以创建一个物理目的地,而不是只解决一个。 + +布尔属性`pubSubDomain`用于配置`JmsTemplate`,以了解正在使用的 JMS 域。默认情况下,此属性的值为 false,表示要使用点对点域`Queues`。此属性(由`JmsTemplate`使用)通过`DestinationResolver`接口的实现来确定动态目标解析的行为。 + +你还可以通过属性`defaultDestination`配置带有默认目标的`JmsTemplate`。默认的目标是不引用特定目标的发送和接收操作。 + +#### 4.1.4.消息监听器容器 + +在 EJB 世界中,JMS 消息最常见的用途之一是驱动消息驱动 Bean。 Spring 提供了一种解决方案,以不将用户绑定到 EJB 容器的方式创建消息驱动的 POJO。(关于 Spring 的 MDP 支持的详细介绍,请参见[异步接收:消息驱动的 POJO](#jms-receiving-async)。)自 Spring Framework4.1 以来,端点方法可以使用`@JmsListener`进行注释——有关更多详细信息,请参见[注释驱动的监听器端点](#jms-annotated)。 + +消息侦听器容器用于接收来自 JMS 消息队列的消息,并驱动注入其中的`MessageListener`。侦听器容器负责消息接收的所有线程处理,并将消息发送到侦听器中进行处理。消息侦听器容器是 MDP 和消息提供程序之间的中介,负责注册以接收消息、参与事务、资源获取和释放、异常转换等。这使你能够编写与接收消息(并可能对消息做出响应)相关的(可能复杂的)业务逻辑,并将 JMS 基础设施关注的样板委托给框架。 + +有两个打包了 Spring 的标准 JMS 消息侦听器容器,每个容器都有其专门的功能集。 + +* [SimpleMessageListenerContainer’](#jms-mdp-simple) + +* [“DefaultMessagelistenerContainer”](#jms-mdp-default) + +##### 使用`SimpleMessageListenerContainer` + +此消息侦听器容器是两种标准类型中较简单的一种。它在启动时创建固定数量的 JMS 会话和使用者,通过使用标准的 JMS`MessageConsumer.setMessageListener()`方法注册侦听器,并将其留给 JMS 提供者来执行侦听器回调。这种变体不允许对运行时需求进行动态调整,也不允许参与外部管理的事务。在兼容性方面,它非常接近独立的 JMS 规范的精神,但通常与 Java EE 的 JMS 限制不兼容。 + +| |虽然`SimpleMessageListenerContainer`不允许参与外部<br/>托管事务,但它确实支持本机 JMS 事务。要启用此功能,<br/>你可以将`sessionTransacted`标志切换到`true`,或者在 XML 命名空间中,将 `concount’属性设置为`transacted`。从侦听器抛出的异常将导致<br/>回滚,并重新传递消息。或者,考虑使用“Client_Account”模式,该模式在出现异常的情况下也提供重新交付,但是<br/>不使用已处理的`Session`实例,因此不包括事务协议中的任何其他“会话”操作(例如发送响应消息)。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |默认的`AUTO_ACKNOWLEDGE`模式不能提供适当的可靠性保证,当监听器执行失败时,<br/>消息可能会丢失(因为提供者在监听器调用后自动<br/>确认每条消息,没有异常情况要传播到<br/>提供程序)或当侦听器容器关闭时(你可以通过设置<br/>`acceptMessagesWhileStopping`标志来配置这一点)。确保在<br/>可靠性需要的情况下使用事务会话(例如,用于可靠的队列处理和持久的主题订阅)。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 使用`DefaultMessageListenerContainer` + +此消息侦听器容器在大多数情况下都被使用。与“SimpleMessageListenerContainer”相反,这种容器变体允许对运行时需求进行动态调整,并且能够参与外部管理的事务。当使用“JTATRANSACTIONMANAGER”配置时,每个接收到的消息都会在 XA 事务中注册。因此,处理可以利用 XA 事务语义。这个侦听器容器在对 JMS 提供者的低要求、高级功能(例如参与外部管理的事务)以及与 Java EE 环境的兼容性之间取得了良好的平衡。 + +你可以自定义容器的缓存级别。请注意,当不启用缓存时,将为每个消息接收创建一个新的连接和一个新的会话。将此与具有高负载的非持久性订阅结合在一起可能会导致消息丢失。在这种情况下,请确保使用适当的缓存级别。 + +当代理发生故障时,这个容器还具有可恢复的功能。默认情况下,一个简单的`BackOff`实现每五秒重试一次。你可以为更细粒度的恢复选项指定一个自定义`BackOff`实现。有关示例,请参见[“指数后退”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/util/backoff/ExponentialBackOff.html)。 + +| |和它的兄弟([SimpleMessageListenerContainer’](#jms-mdp-simple))一样,DefaultMessageListenerContainer 支持原生 JMS 事务,并允许<br/>自定义确认模式。如果对于你的场景是可行的,那么在外部管理的事务上强烈推荐<br/>—也就是说,如果你可以在 JVM 失效的情况下使用<br/>偶尔重复的消息。业务逻辑中的自定义重复消息<br/>检测步骤可以覆盖此类情况—例如,<br/>以业务实体存在检查或协议表检查的形式进行。<br/>任何这样的安排都比替代方案的效率高得多:<br/>用 XA 事务(通过配置你的 `defaultMessageListenerContainer` 和`JtaTransactionManager`)来覆盖<br/>接收 JMS 消息以及在你的<br/>消息侦听器中执行业务逻辑(包括数据库操作等)。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |默认的`AUTO_ACKNOWLEDGE`模式不能提供适当的可靠性保证,当监听器执行失败时,<br/>消息可能会丢失(因为提供者在监听器调用后自动<br/>确认每条消息,没有异常情况要传播到<br/>提供程序)或当侦听器容器关闭时(你可以通过设置<br/>`acceptMessagesWhileStopping`标志来配置此项)。确保在<br/>可靠性需要的情况下使用事务会话(例如,用于可靠的队列处理和持久的主题订阅)。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 4.1.5.事务管理 + +Spring 提供了一个`JmsTransactionManager`,它为单个 JMS`ConnectionFactory’管理事务。这使得 JMS 应用程序能够利用 Spring 的托管事务特性,如[数据访问章节的事务管理部分](data-access.html#transaction)中所述。`JmsTransactionManager`执行本地资源事务,将指定的`ConnectionFactory`中的一个 JMS 连接/会话对绑定到线程。 + +在 Java EE 环境中,`ConnectionFactory`池连接和会话实例,因此这些资源可以在事务之间有效地重用。在独立环境中,使用 Spring 的`SingleConnectionFactory`会导致共享的 JMS`Connection`,每个事务都有自己独立的`Session`。或者,考虑使用特定于提供程序的池适配器,例如 ActiveMQ 的`PooledConnectionFactory`类。 + +你还可以使用`JmsTemplate`和`JtaTransactionManager`以及支持 XA 的 JMS`ConnectionFactory’来执行分布式事务。请注意,这需要使用 JTA 事务管理器以及正确配置 XA 的 ConnectionFactory。(检查你的 Java EE 服务器或 JMS 提供者的文档。 + +当使用 JMS API 从`Connection`创建`Session`时,跨托管和非托管事务环境重用代码可能会引起混淆。这是因为 JMS API 只有一个工厂方法来创建`Session`,并且它需要事务和确认模式的值。在托管环境中,设置这些值是环境的事务基础设施的责任,因此供应商对 JMS 连接的包装器忽略了这些值。在非托管环境中使用`JmsTemplate`时,可以通过使用属性`sessionTransacted`和`sessionAcknowledgeMode`来指定这些值。当你使用带有`JmsTemplate`的 `Platform TransactionManager’时,模板总是被赋予一个事务性 JMS`Session`。 + +### 4.2.发送消息 + +`JmsTemplate`包含许多发送消息的方便方法。Send 方法通过使用`javax.jms.Destination`对象来指定目的地,而其他方法则通过在 JNDI 查找中使用`String`来指定目的地。不接受目标参数的`send`方法使用默认的目标。 + +下面的示例使用`MessageCreator`回调从提供的`Session`对象创建文本消息: + +``` +import javax.jms.ConnectionFactory; +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.Queue; +import javax.jms.Session; + +import org.springframework.jms.core.MessageCreator; +import org.springframework.jms.core.JmsTemplate; + +public class JmsQueueSender { + + private JmsTemplate jmsTemplate; + private Queue queue; + + public void setConnectionFactory(ConnectionFactory cf) { + this.jmsTemplate = new JmsTemplate(cf); + } + + public void setQueue(Queue queue) { + this.queue = queue; + } + + public void simpleSend() { + this.jmsTemplate.send(this.queue, new MessageCreator() { + public Message createMessage(Session session) throws JMSException { + return session.createTextMessage("hello queue world"); + } + }); + } +} +``` + +在前面的示例中,`JmsTemplate`是通过传递对“ConnectionFactory”的引用来构造的。作为一种替代方案,提供了一个零参数构造函数和“ConnectionFactory”,可以用来构造 JavaBean 风格的实例(使用`BeanFactory`或纯 Java 代码)。或者,考虑从 Spring 的`JmsGatewaySupport`便利基类派生,它为 JMS 配置提供了预先构建的 Bean 属性。 + +`send(String destinationName, MessageCreator creator)`方法允许你通过使用目标的字符串名称发送消息。如果这些名称是在 JNDI 中注册的,则应该将模板的`destinationResolver`属性设置为 `jndidestinationResolver’的实例。 + +如果你创建了`JmsTemplate`并指定了一个默认的目的地,那么“发送”将向该目的地发送一条消息。 + +#### 4.2.1.使用消息转换器 + +为了促进域模型对象的发送,`JmsTemplate`有各种发送方法,这些方法将 Java 对象作为消息数据内容的参数。jmstemplate 中的重载方法`convertAndSend()`和`receiveAndConvert()`方法将转换过程委托给`MessageConverter`接口的实例。这个接口定义了一个简单的契约,用于在 Java 对象和 JMS 消息之间进行转换。默认实现支持`String`和`TextMessage`、`byte[]`和`BytesMessage`之间的转换,以及`java.util.Map`和`MapMessage`之间的转换。通过使用转换器,你和你的应用程序代码可以专注于通过 JMS 发送或接收的业务对象,而不必关注如何将其表示为 JMS 消息的详细信息。 + +沙盒目前包括`MapMessageConverter`,它使用反射在 JavaBean 和`MapMessage`之间进行转换。你可能自己实现的其他流行的实现选择是转换器,它们使用现有的 XML 编组包(例如 JAXB 或 XStream)来创建表示对象的`TextMessage`。 + +为了适应消息的属性、标头和主体的设置,而这些属性、标头和主体不能通用地封装在转换器类中,`MessagePostProcessor`接口允许你在消息被转换之后但在消息被发送之前访问该消息。下面的示例展示了如何在将 `java.util.map’转换为消息后修改消息头和属性: + +``` +public void sendWithConversion() { + Map map = new HashMap(); + map.put("Name", "Mark"); + map.put("Age", new Integer(47)); + jmsTemplate.convertAndSend("testQueue", map, new MessagePostProcessor() { + public Message postProcessMessage(Message message) throws JMSException { + message.setIntProperty("AccountID", 1234); + message.setJMSCorrelationID("123-00001"); + return message; + } + }); +} +``` + +这将产生以下形式的消息: + +``` +MapMessage={ + Header={ + ... standard headers ... + CorrelationID={123-00001} + } + Properties={ + AccountID={Integer:1234} + } + Fields={ + Name={String:Mark} + Age={Integer:47} + } +} +``` + +#### 4.2.2.使用`SessionCallback`和`ProducerCallback` + +虽然发送操作涵盖了许多常见的使用场景,但有时你可能希望在 JMS`Session`或`MessageProducer`上执行多个操作。“sessionCallback”和`ProducerCallback`分别公开 JMS`Session`和`Session`/“MessageProducer”对。在`JmsTemplate`上的`execute()`方法运行这些回调方法。 + +### 4.3.接收消息 + +这描述了如何使用 Spring 中的 JMS 接收消息。 + +#### 4.3.1.同步接收 + +虽然 JMS 通常与异步处理相关联,但你可以同步地使用消息。重载的`receive(..)`方法提供了此功能。在同步接收期间,调用线程会阻塞消息,直到消息变得可用。这可能是一个危险的操作,因为调用线程可能会无限期地被阻塞。`receiveTimeout`属性指定接收者在放弃等待消息之前应该等待多长时间。 + +#### 4.3.2.异步接收:消息驱动的 POJO + +| |Spring 还通过使用`@JmsListener`注释来支持带注释的侦听器端点,并提供一种以编程方式注册端点的开放基础设施。<br/>这是迄今为止设置异步接收器的最方便的方式。<br/>有关更多详细信息,请参见[启用监听器端点注释](#jms-annotated-support)。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +Bean(MDB)在 EJB 世界中,以类似于消息驱动的方式,消息驱动的 POJO 充当 JMS 消息的接收者。MDP 上的一个限制(但请参见[Using `MessageListenerAdapter`](#jms-receiving-async-message-listener-adapter))是它必须实现`javax.jms.MessageListener`接口。请注意,如果你的 POJO 在多个线程上接收消息,那么确保你的实现是线程安全的非常重要。 + +下面的示例展示了 MDP 的一个简单实现: + +``` +import javax.jms.JMSException; +import javax.jms.Message; +import javax.jms.MessageListener; +import javax.jms.TextMessage; + +public class ExampleListener implements MessageListener { + + public void onMessage(Message message) { + if (message instanceof TextMessage) { + try { + System.out.println(((TextMessage) message).getText()); + } + catch (JMSException ex) { + throw new RuntimeException(ex); + } + } + else { + throw new IllegalArgumentException("Message must be of type TextMessage"); + } + } +} +``` + +一旦实现了`MessageListener`,就该创建消息侦听器容器了。 + +下面的示例展示了如何定义和配置带有 Spring(在本例中,`DefaultMessageListenerContainer`)的消息侦听器容器之一: + +``` +<!-- this is the Message Driven POJO (MDP) --> +<bean id="messageListener" class="jmsexample.ExampleListener"/> + +<!-- and this is the message listener container --> +<bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer"> + <property name="connectionFactory" ref="connectionFactory"/> + <property name="destination" ref="destination"/> + <property name="messageListener" ref="messageListener"/> +</bean> +``` + +参见各种消息侦听器容器(所有这些容器实现[MessagelistenerContainer](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jms/listener/MessageListenerContainer.html))的 Spring Javadoc,以获得每个实现所支持的特性的完整描述。 + +#### 4.3.3.使用`SessionAwareMessageListener`接口 #### + +`SessionAwareMessageListener`接口是一个 Spring 特定的接口,它提供了与 JMS`MessageListener`接口类似的契约,但也使消息处理方法能够访问 JMS`Session`,从该接口接收`Message`。下面的清单显示了`SessionAwareMessageListener`接口的定义: + +``` +package org.springframework.jms.listener; + +public interface SessionAwareMessageListener { + + void onMessage(Message message, Session session) throws JMSException; +} +``` + +如果你希望你的 MDP 能够响应任何接收到的消息(通过使用`onMessage(Message, Session)`方法中提供的`Session`方法),则可以选择让你的 MDP 实现这个接口(优先于标准的 JMS`MessageListener`接口)。 Spring 附带的所有消息侦听器容器实现都支持实现`MessageListener`或 `SessionAwareMessageListener’接口的 MDP。实现“SessionAwareMessageListener”的类需要注意的是,它们随后会通过接口绑定到 Spring。是否使用它的选择完全取决于作为应用程序开发人员或架构师的你。 + +注意,`onMessage(..)`接口的`SessionAwareMessageListener`方法抛出`JMSException`。与标准的 JMS`MessageListener`接口相反,当使用`SessionAwareMessageListener`接口时,客户端代码负责处理任何抛出的异常。 + +#### 4.3.4.使用`MessageListenerAdapter` + +`MessageListenerAdapter`类是 Spring 异步消息传递支持中的最后一个组件。简而言之,它允许你将几乎任何类公开为 MDP(尽管有一些约束)。 + +考虑以下接口定义: + +``` +public interface MessageDelegate { + + void handleMessage(String message); + + void handleMessage(Map message); + + void handleMessage(byte[] message); + + void handleMessage(Serializable message); +} +``` + +请注意,尽管接口既不扩展`MessageListener`也不扩展 `SessionAwareMessageListener’接口,但你仍然可以通过使用 `MessageListenerAdapter’类将其用作 MDP。还请注意,各种消息处理方法是如何根据它们可以接收和处理的各种`Message`类型的内容强类型的。 + +现在考虑`MessageDelegate`接口的以下实现: + +``` +public class DefaultMessageDelegate implements MessageDelegate { + // implementation elided for clarity... +} +``` + +特别是,请注意`MessageDelegate`接口(“defaultMessageDelegate”类)的前面的实现是如何完全没有 JMS 依赖关系的。这确实是一个 POJO,我们可以通过以下配置将其转换为 MDP: + +``` +<!-- this is the Message Driven POJO (MDP) --> +<bean id="messageListener" class="org.springframework.jms.listener.adapter.MessageListenerAdapter"> + <constructor-arg> + <bean class="jmsexample.DefaultMessageDelegate"/> + </constructor-arg> +</bean> + +<!-- and this is the message listener container... --> +<bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer"> + <property name="connectionFactory" ref="connectionFactory"/> + <property name="destination" ref="destination"/> + <property name="messageListener" ref="messageListener"/> +</bean> +``` + +下一个示例展示了另一个只能处理接收 JMS`TextMessage’消息的 MDP。请注意消息处理方法实际上是如何被称为“receive”(在`MessageListenerAdapter`中消息处理方法的名称默认为`handleMessage`)的,但是它是可配置的(正如你在本节后面看到的)。还请注意`receive(..)`方法是如何强类型的,以便仅接收和响应 JMS 的“textmessage”消息。下面的清单显示了`TextMessageDelegate`接口的定义: + +``` +public interface TextMessageDelegate { + + void receive(TextMessage message); +} +``` + +下面的清单显示了一个实现`TextMessageDelegate`接口的类: + +``` +public class DefaultTextMessageDelegate implements TextMessageDelegate { + // implementation elided for clarity... +} +``` + +然后,伴随函数`MessageListenerAdapter`的配置如下: + +``` +<bean id="messageListener" class="org.springframework.jms.listener.adapter.MessageListenerAdapter"> + <constructor-arg> + <bean class="jmsexample.DefaultTextMessageDelegate"/> + </constructor-arg> + <property name="defaultListenerMethod" value="receive"/> + <!-- we don't want automatic message context extraction --> + <property name="messageConverter"> + <null/> + </property> +</bean> +``` + +请注意,如果`messageListener`接收到不是`TextMessage`的类型的 JMS`Message`,则抛出一个`IllegalStateException`(并随后吞下)。`MessageListenerAdapter`类的另一个功能是,如果处理程序方法返回一个非 void 值,则能够自动发送回响应`Message`。考虑以下接口和类: + +``` +public interface ResponsiveTextMessageDelegate { + + // notice the return type... + String receive(TextMessage message); +} +``` + +``` +public class DefaultResponsiveTextMessageDelegate implements ResponsiveTextMessageDelegate { + // implementation elided for clarity... +} +``` + +如果将`DefaultResponsiveTextMessageDelegate`与 `MessageListenerAdapter’一起使用,则(在默认配置中)将执行`'receive(..)'`方法返回的任何非空值转换为 `textmessage’。然后将结果`TextMessage`发送到在原始`Message`的 JMS`Reply-To`属性中定义的`Destination`(如果存在)或在`MessageListenerAdapter`上设置的默认`Destination`(如果已经配置了)。如果没有找到`Destination`,则抛出一个`InvalidDestinationException`(请注意,此异常不会被吞没并向上传播调用堆栈)。 + +#### 4.3.5.处理事务中的消息 + +在事务中调用消息侦听器只需要重新配置侦听器容器。 + +你可以通过侦听器容器定义上的`sessionTransacted`标志激活本地资源事务。然后,每个消息侦听器调用都在一个活动的 JMS 事务中进行操作,在侦听器执行失败的情况下,消息接收将被回滚。发送响应消息(通过`SessionAwareMessageListener`)是同一本地事务的一部分,但任何其他资源操作(例如数据库访问)都是独立操作的。这通常需要在侦听器实现中进行重复消息检测,以覆盖数据库处理已提交但消息处理未提交的情况。 + +考虑以下 Bean 定义: + +``` +<bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer"> + <property name="connectionFactory" ref="connectionFactory"/> + <property name="destination" ref="destination"/> + <property name="messageListener" ref="messageListener"/> + <property name="sessionTransacted" value="true"/> +</bean> +``` + +要参与外部管理的事务,你需要配置一个事务管理器,并使用一个支持外部管理事务的侦听器容器(通常是`DefaultMessageListenerContainer`)。 + +要为 XA 事务参与配置消息侦听器容器,你需要配置`JtaTransactionManager`(默认情况下,它将委托给 Java EE 服务器的事务子系统)。请注意,底层 JMS`ConnectionFactory`需要具有 XA 功能,并正确地向你的 JTA 事务协调器注册。(检查你的 Java EE 服务器对 JNDI 资源的配置。)这使得消息接收以及(例如)数据库访问成为同一事务的一部分(使用统一的提交语义,以 XA 事务日志开销为代价)。 + +Bean 以下定义创建了事务管理器: + +``` +<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"/> +``` + +然后,我们需要将其添加到我们先前的容器配置中。剩下的就交给集装箱了。下面的示例展示了如何做到这一点: + +``` +<bean id="jmsContainer" class="org.springframework.jms.listener.DefaultMessageListenerContainer"> + <property name="connectionFactory" ref="connectionFactory"/> + <property name="destination" ref="destination"/> + <property name="messageListener" ref="messageListener"/> + <property name="transactionManager" ref="transactionManager"/> (1) +</bean> +``` + +|**1**|我们的交易经理。| +|-----|------------------------| + +### 4.4.对 JCA 消息端点的支持 + +从版本 2.5 开始, Spring 还提供了对基于 JCA 的“MessageListener”容器的支持。`JmsMessageEndpointManager`尝试从提供者的“ResourceAdapter”类名中自动确定`ActivationSpec`类名。因此,通常可以提供 Spring 的泛型`JmsActivationSpecConfig`,如下例所示: + +``` +<bean class="org.springframework.jms.listener.endpoint.JmsMessageEndpointManager"> + <property name="resourceAdapter" ref="resourceAdapter"/> + <property name="activationSpecConfig"> + <bean class="org.springframework.jms.listener.endpoint.JmsActivationSpecConfig"> + <property name="destinationName" value="myQueue"/> + </bean> + </property> + <property name="messageListener" ref="myMessageListener"/> +</bean> +``` + +或者,你可以使用给定的“ActivationSpec”对象设置`JmsMessageEndpointManager`。`ActivationSpec`对象也可以来自 JNDI 查找(使用`<jee:jndi-lookup>`)。下面的示例展示了如何做到这一点: + +``` +<bean class="org.springframework.jms.listener.endpoint.JmsMessageEndpointManager"> + <property name="resourceAdapter" ref="resourceAdapter"/> + <property name="activationSpec"> + <bean class="org.apache.activemq.ra.ActiveMQActivationSpec"> + <property name="destination" value="myQueue"/> + <property name="destinationType" value="javax.jms.Queue"/> + </bean> + </property> + <property name="messageListener" ref="myMessageListener"/> +</bean> +``` + +使用 Spring 的`ResourceAdapterFactoryBean`,可以在本地配置目标`ResourceAdapter`,如下例所示: + +``` +<bean id="resourceAdapter" class="org.springframework.jca.support.ResourceAdapterFactoryBean"> + <property name="resourceAdapter"> + <bean class="org.apache.activemq.ra.ActiveMQResourceAdapter"> + <property name="serverUrl" value="tcp://localhost:61616"/> + </bean> + </property> + <property name="workManager"> + <bean class="org.springframework.jca.work.SimpleTaskWorkManager"/> + </property> +</bean> +``` + +指定的`WorkManager`还可以指向特定于环境的线程池——通常通过`SimpleTaskWorkManager`实例的`asyncTaskExecutor`属性。考虑为所有`ResourceAdapter`实例定义一个共享线程池,如果你碰巧使用了多个适配器。 + +在某些环境中(例如 WebLogic9 或更高),你可以从 JNDI 获得整个`ResourceAdapter`对象(通过使用`<jee:jndi-lookup>`)。然后,基于 Spring 的消息侦听器可以与服务器托管的`ResourceAdapter`进行交互,该服务器还使用服务器内置的`WorkManager`。 + +有关更多详细信息,请参见[JMSMessageEndPointManager’](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jms/listener/endpoint/JmsMessageEndpointManager.html)、[JMSActivationSpecconfig](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jms/listener/endpoint/JmsActivationSpecConfig.html)和[“资源适应性工厂”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jca/support/ResourceAdapterFactoryBean.html)的 Javadoc。 + +Spring 还提供了不绑定到 JMS 的通用 JCA 消息端点管理器:`org.SpringFramework.jca.endpoint.GenericMessageEndpointManager`。该组件允许使用任何消息侦听器类型(例如 JMS`MessageListener`)和任何特定于提供者的`ActivationSpec`对象。查看你的 JCA 提供者的文档以了解连接器的实际功能,并查看[GenericMessageEndPointManager](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jca/endpoint/GenericMessageEndpointManager.html)Javadoc 以获得 Spring 特定的配置详细信息。 + +| |基于 JCA 的消息端点管理非常类似于 EJB2.1 消息驱动的 bean。<br/>它使用相同的底层资源提供者契约。与 EJB2.1MDB 一样,你也可以在 Spring 上下文中使用你的 JCA 提供程序支持的任何<br/>消息侦听器接口。尽管如此,<br/> Spring 仍然为 JMS 提供了明确的“便利”支持,因为 JMS 是 JCA 端点管理合同中使用的最常见的端点 API<br/>。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 4.5.注释驱动的监听器端点 + +异步接收消息的最简单方法是使用带注释的侦听器端点基础结构。简而言之,它允许你将托管 Bean 方法公开为 JMS 侦听器端点。下面的示例展示了如何使用它: + +``` +@Component +public class MyService { + + @JmsListener(destination = "myDestination") + public void processOrder(String data) { ... } +} +``` + +前面示例的思想是,每当消息在 `javax.jms.destination``myDestination`上可用时,都会相应地调用`processOrder`方法(在这种情况下,使用 JMS 消息的内容,类似于[“MessagelistenerAdapter”](#jms-receiving-async-message-listener-adapter)提供的内容)。 + +通过使用`JmsListenerContainerFactory`,带注释的端点基础设施在幕后为每个带注释的方法创建了一个消息侦听器容器。这样的容器不是针对应用程序上下文注册的,而是可以通过使用`JmsListenerEndpointRegistry` Bean 为管理目的而容易地定位。 + +| |`@JmsListener`是 Java8 上的一个可重复注释,因此你可以通过向它添加额外的`@JmsListener`声明,将<br/>多个 JMS 目标与相同的方法关联起来。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 4.5.1.启用监听器端点注释 + +要启用对`@JmsListener`注释的支持,你可以将`@EnableJms`添加到你的一个`@Configuration`类中,如下例所示: + +``` +@Configuration +@EnableJms +public class AppConfig { + + @Bean + public DefaultJmsListenerContainerFactory jmsListenerContainerFactory() { + DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory(); + factory.setConnectionFactory(connectionFactory()); + factory.setDestinationResolver(destinationResolver()); + factory.setSessionTransacted(true); + factory.setConcurrency("3-10"); + return factory; + } +} +``` + +默认情况下,基础结构会寻找一个名为`jmsListenerContainerFactory`的 Bean 源,以供工厂用来创建消息侦听器容器。在这种情况下(忽略 JMS 基础设施设置),你可以调用`processOrder`方法,其核心轮询大小为 3 个线程,最大池大小为 10 个线程。 + +你可以自定义用于每个注释的侦听器容器工厂,也可以通过实现`JmsListenerConfigurer`接口来配置显式默认值。只有当至少有一个端点在没有特定容器工厂的情况下注册时,才需要默认设置。有关详细信息和示例,请参见实现[jmslistenerconfigurer’](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jms/annotation/JmsListenerConfigurer.html)的类的 Javadoc。 + +如果你更喜欢[XML 配置](#jms-namespace),那么可以使用`<jms:annotation-driven>`元素,如下例所示: + +``` +<jms:annotation-driven/> + +<bean id="jmsListenerContainerFactory" + class="org.springframework.jms.config.DefaultJmsListenerContainerFactory"> + <property name="connectionFactory" ref="connectionFactory"/> + <property name="destinationResolver" ref="destinationResolver"/> + <property name="sessionTransacted" value="true"/> + <property name="concurrency" value="3-10"/> +</bean> +``` + +#### 4.5.2.程序化端点注册 + +`JmsListenerEndpoint`提供了一个 JMS 端点模型,并负责为该模型配置容器。除了通过`JmsListener`注释检测到的端点之外,该基础结构还允许你以编程方式配置端点。下面的示例展示了如何做到这一点: + +``` +@Configuration +@EnableJms +public class AppConfig implements JmsListenerConfigurer { + + @Override + public void configureJmsListeners(JmsListenerEndpointRegistrar registrar) { + SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint(); + endpoint.setId("myJmsEndpoint"); + endpoint.setDestination("anotherQueue"); + endpoint.setMessageListener(message -> { + // processing + }); + registrar.registerEndpoint(endpoint); + } +} +``` + +在前面的示例中,我们使用了`SimpleJmsListenerEndpoint`,它提供了要调用的实际“MessageListener”。但是,你也可以构建自己的端点变体来描述自定义调用机制。 + +请注意,你可以完全跳过`@JmsListener`的使用,并通过`JmsListenerConfigurer`以编程方式只注册你的端点。 + +#### 4.5.3.带注释的端点方法签名 + +到目前为止,我们已经在我们的端点中注入了一个简单的`String`,但是它实际上可以有一个非常灵活的方法签名。在下面的示例中,我们重写它,以使用自定义标头注入`Order`: + +``` +@Component +public class MyService { + + @JmsListener(destination = "myDestination") + public void processOrder(Order order, @Header("order_type") String orderType) { + ... + } +} +``` + +可以在 JMS Listener 端点中注入的主要元素如下: + +* RAW`javax.jms.Message`或其任一子类(前提是它匹配传入的消息类型)。 + +* `javax.jms.Session`用于对本机 JMS API 的可选访问(例如,用于发送自定义回复)。 + +* 表示传入的 JMS 消息的`org.springframework.messaging.Message`。请注意,此消息同时包含自定义标题和标准标题(由`JmsHeaders`定义)。 + +* `@Header`-带注释的方法参数,以提取特定的标头值,包括标准的 JMS 标头。 + +* 一个`@Headers`-带注释的参数,该参数也必须分配给`java.util.Map`,以获得对所有标题的访问权限。 + +* 不属于受支持类型之一(“消息”或“会话”)的未注释元素被视为有效负载。你可以通过使用`@Payload`对参数进行注释来使其显式。你还可以通过添加一个额外的“@valid”来打开验证。 + +注入 Spring 的`Message`抽象的能力对于受益于存储在特定传输消息中的所有信息而不依赖特定传输 API 特别有用。下面的示例展示了如何做到这一点: + +``` +@JmsListener(destination = "myDestination") +public void processOrder(Message<Order> order) { ... } +``` + +方法参数的处理由`DefaultMessageHandlerMethodFactory`提供,你可以进一步自定义该参数以支持其他方法参数。你也可以在那里自定义转换和验证支持。 + +例如,如果我们希望在处理它之前确保`Order`是有效的,那么我们可以用`@Valid`对有效负载进行注释,并配置必要的验证器,如下例所示: + +``` +@Configuration +@EnableJms +public class AppConfig implements JmsListenerConfigurer { + + @Override + public void configureJmsListeners(JmsListenerEndpointRegistrar registrar) { + registrar.setMessageHandlerMethodFactory(myJmsHandlerMethodFactory()); + } + + @Bean + public DefaultMessageHandlerMethodFactory myHandlerMethodFactory() { + DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory(); + factory.setValidator(myValidator()); + return factory; + } +} +``` + +#### 4.5.4.反应管理 + +[“MessagelistenerAdapter”](#jms-receiving-async-message-listener-adapter)中现有的支持已经允许你的方法具有非 `void’返回类型。在这种情况下,调用的结果被封装在`javax.jms.Message`中,在原始消息的`JMSReplyTo`头中指定的目标中或在侦听器上配置的默认目标中发送。现在,你可以使用消息传递抽象的`@SendTo`注释来设置默认的目标。 + +假设我们的`processOrder`方法现在应该返回一个`OrderStatus`,我们可以编写它来自动发送响应,如下例所示: + +``` +@JmsListener(destination = "myDestination") +@SendTo("status") +public OrderStatus processOrder(Order order) { + // order processing + return status; +} +``` + +| |如果你有几个`@JmsListener`-注释的方法,你还可以在类级别上放置`@SendTo`注释,以共享默认的回复目标。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------| + +如果需要以与传输无关的方式设置额外的头,则可以使用类似于以下方法的方法返回“message”: + +``` +@JmsListener(destination = "myDestination") +@SendTo("status") +public Message<OrderStatus> processOrder(Order order) { + // order processing + return MessageBuilder + .withPayload(status) + .setHeader("code", 1234) + .build(); +} +``` + +如果需要在运行时计算响应目的地,可以将响应封装在`JmsResponse`实例中,该实例还提供了在运行时使用的目的地。我们可以将前面的示例改写如下: + +``` +@JmsListener(destination = "myDestination") +public JmsResponse<Message<OrderStatus>> processOrder(Order order) { + // order processing + Message<OrderStatus> response = MessageBuilder + .withPayload(status) + .setHeader("code", 1234) + .build(); + return JmsResponse.forQueue(response, "status"); +} +``` + +最后,如果需要为响应指定一些 QoS 值,例如优先级或生存时间,则可以相应地配置`JmsListenerContainerFactory`,如下例所示: + +``` +@Configuration +@EnableJms +public class AppConfig { + + @Bean + public DefaultJmsListenerContainerFactory jmsListenerContainerFactory() { + DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory(); + factory.setConnectionFactory(connectionFactory()); + QosSettings replyQosSettings = new QosSettings(); + replyQosSettings.setPriority(2); + replyQosSettings.setTimeToLive(10000); + factory.setReplyQosSettings(replyQosSettings); + return factory; + } +} +``` + +### 4.6.JMS 名称空间支持 + +Spring 提供了用于简化 JMS 配置的 XML 命名空间。要使用 JMS 名称空间元素,你需要引用 JMS 模式,如下例所示: + +``` +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:jms="http://www.springframework.org/schema/jms" (1) + xsi:schemaLocation=" + http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/jms https://www.springframework.org/schema/jms/spring-jms.xsd"> + + <!-- bean definitions here --> + +</beans> +``` + +|**1**|引用 JMS 模式。| +|-----|---------------------------| + +名称空间由三个顶级元素组成:`<annotation-driven/>`、`<listener-container/>`和`<jca-listener-container/>`。`<annotation-driven/>`启用[注释驱动的监听器端点](#jms-annotated)。`<listener-container/>`和`<jca-listener-container/>`定义共享侦听器容器配置,并可以包含`<listener/>`子元素。下面的示例展示了两个侦听器的基本配置: + +``` +<jms:listener-container> + + <jms:listener destination="queue.orders" ref="orderService" method="placeOrder"/> + + <jms:listener destination="queue.confirmations" ref="confirmationLogger" method="log"/> + +</jms:listener-container> +``` + +前面的示例相当于创建两个不同的侦听器容器 Bean 定义和两个不同的`MessageListenerAdapter` Bean 定义,如[Using `MessageListenerAdapter`](#jms-receiving-async-message-listener-adapter)所示。除了前面示例中显示的属性外,`listener`元素还可以包含几个可选的属性。下表描述了所有可用的属性: + +| Attribute |说明| +|------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `id` |Bean 主机侦听器容器的名称。如果没有指定,则自动生成一个 Bean 名称<br/>。| +|`destination` (required)|此侦听器的目标名称,通过`DestinationResolver`策略解析。| +| `ref` (required) |处理程序对象的 Bean 名称。| +| `method` |要调用的处理程序方法的名称。如果`ref`属性指向`MessageListener`或 Spring `SessionAwareMessageListener`,则可以省略该属性。| +| `response-destination` |要向其发送响应消息的默认响应目的地的名称。这是<br/>应用于不带有`JMSReplyTo`字段的请求消息的情况。此目的地的<br/>类型由侦听器-容器的 `response-destination-type’属性确定。请注意,这仅适用于具有<br/>返回值的侦听器方法,为此,每个结果对象都转换为响应消息。| +| `subscription` |持久订阅的名称(如果有的话)。| +| `selector` |此侦听器的可选消息选择器。| +| `concurrency` |此侦听器要启动的并发会话或消费者的数量。这个值可以是<br/>表示最大值的简单数字(例如,`5`),也可以是表示较低值<br/>以及上限的范围(例如,`3-5`)。请注意,指定的最小值只是一个提示<br/>,在运行时可能会被忽略。默认值是容器提供的值。| + +`<listener-container/>`元素还接受几个可选属性。这允许定制各种策略(例如,`taskExecutor`和 `DestinationResolver’)以及基本的 JMS 设置和资源引用。通过使用这些属性,你可以定义高度定制的侦听器容器,同时仍然受益于名称空间的便利。 + +你可以通过指定 Bean 的`id`来通过`factory-id`属性自动将此类设置公开为`JmsListenerContainerFactory`,如下例所示: + +``` +<jms:listener-container connection-factory="myConnectionFactory" + task-executor="myTaskExecutor" + destination-resolver="myDestinationResolver" + transaction-manager="myTransactionManager" + concurrency="10"> + + <jms:listener destination="queue.orders" ref="orderService" method="placeOrder"/> + + <jms:listener destination="queue.confirmations" ref="confirmationLogger" method="log"/> + +</jms:listener-container> +``` + +下表描述了所有可用的属性。有关单个属性的更多详细信息,请参见[`AbstractMessagelistenerContainer’](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jms/listener/AbstractMessageListenerContainer.html)的类级 Javadoc 及其具体的子类。Javadoc 还提供了对事务选择和消息重新交付场景的讨论。 + +| Attribute |说明| +|---------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `container-type` |此侦听器容器的类型。可用的选项是`default`,`simple`,`default102’,或`simple102`(默认选项是`default`)。| +| `container-class` |根据`container-type`属性,默认值是 Spring 的标准`DefaultMessageListenerContainer`或 `SimpleMessageListenerContainer’。| +| `factory-id` |使用指定的`id`将此元素定义的设置公开为`JmsListenerContainerFactory`,以便它们可以在其他端点上重用。| +| `connection-factory` |引用 JMS`ConnectionFactory` Bean(缺省的 Bean 名称是“ConnectionFactory”)。| +| `task-executor` |对 Spring `TaskExecutor`的引用,用于 JMS 侦听器调用程序。| +| `destination-resolver` |引用`DestinationResolver`解决 JMS`Destination`实例的策略。| +| `message-converter` |引用用于将 JMS 消息转换为侦听器<br/>方法参数的`MessageConverter`策略。默认值是`SimpleMessageConverter`。| +| `error-handler` |对`ErrorHandler`策略的引用,该策略用于处理在<br/>执行过程中可能发生的`MessageListener`未捕获的异常。| +| `destination-type` |此侦听器的 JMS 目标类型:`queue`,`topic`,`durableTopic`,`sharedTopic`,<br/>或`sharedDurableTopic`。这可能启用容器的`pubSubDomain`、`subscriptionDurable`和`subscriptionShared`属性。默认值是`queue`(禁用<br/>这三个属性)。| +|`response-destination-type`|响应的 JMS 目标类型:`queue`或`topic`。缺省值是“destination-type”属性的值。| +| `client-id` |此侦听器容器的 JMS 客户机 ID。在使用<br/>持久订阅时,必须指定它。| +| `cache` |JMS 资源的缓存级别:`none`,`connection`,`session`,`consumer`,或 `auto’。默认情况下,缓存级别实际上是`consumer`,除非已经指定了外部事务管理器——在这种情况下,有效的<br/>默认值将是`none`(假设 Java EE 风格的事务管理,其中给定的<br/>ConnectionFactory 是一个 XA-aware 池)。| +| `acknowledge` |本机 JMS 确认模式:`auto`,`client`,`dups-ok`,或`transacted`。值<br/>的`transacted`激活局部交易的`Session`。作为一种选择,你可以指定<br/>`transaction-manager`属性,稍后将在表中进行说明。默认值为`auto`。| +| `transaction-manager` |对外部`PlatformTransactionManager`的引用(通常是基于 XA 的<br/>事务协调器,例如 Spring 的`JtaTransactionManager`)。如果未指定,则使用<br/>本机确认(请参见`acknowledge`属性)。| +| `concurrency` |为每个侦听器启动的并发会话或消费者的数量。它可以是<br/>表示最大值的简单数字(例如,`5`),也可以是表示<br/>的下限和上限的范围(例如,`3-5`)。请注意,指定的最小值只是一个<br/>提示,在运行时可能会被忽略。默认值为`1`。在<br/>主题侦听器或队列排序很重要的情况下,你应该将并发性限制为`1`。考虑为<br/>一般队列提高它。| +| `prefetch` |要加载到单个会话中的消息的最大数量。请注意,提高这个<br/>数可能会导致并发消费者的饥饿。| +| `receive-timeout` |用于接收呼叫的超时(以毫秒为单位)。默认值是`1000`(一<br/>秒)。`-1`表示没有超时。| +| `back-off` |指定用于计算恢复<br/>尝试之间的间隔的`BackOff`实例。如果`BackOffExecution`实现返回`BackOffExecution#STOP`,则<br/>侦听器容器不会进一步尝试恢复。设置此属性时,将忽略`recovery-interval`值。默认值是`FixedBackOff`和<br/>之间的间隔为 5000 毫秒(即 5 秒)。| +| `recovery-interval` |指定恢复尝试之间的间隔(以毫秒为单位)。它提供了一种方便的<br/>方法来创建具有指定间隔的`FixedBackOff`。对于更多的恢复<br/>选项,可以考虑指定一个`BackOff`实例。默认值是 5000 毫秒<br/>(即 5 秒)。| +| `phase` |此容器应在其中启动和停止的生命周期阶段。<br/>值越低,这个容器启动得越早,停止得越晚。默认值是 `integer.max_value’,这意味着容器尽可能晚地启动,并尽可能快地以<br/>的形式停止。| + +使用`jms`模式支持配置基于 JCA 的侦听器容器非常相似,如下例所示: + +``` +<jms:jca-listener-container resource-adapter="myResourceAdapter" + destination-resolver="myDestinationResolver" + transaction-manager="myTransactionManager" + concurrency="10"> + + <jms:listener destination="queue.orders" ref="myMessageListener"/> + +</jms:jca-listener-container> +``` + +下表描述了 JCA 变体的可用配置选项: + +| Attribute |说明| +|---------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `factory-id` |使用指定的`id`将此元素定义的设置公开为`JmsListenerContainerFactory`,以便它们可以在其他端点上重用。| +| `resource-adapter` |引用 JCA`ResourceAdapter` Bean(默认的 Bean 名称是 `ResourceAdapter’)。| +| `activation-spec-factory` |引用`JmsActivationSpecFactory`。默认值是自动检测 JMS<br/>提供程序及其`ActivationSpec`类(参见[“DefaultJMSActivationSpecFactory”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jms/listener/endpoint/DefaultJmsActivationSpecFactory.html))。| +| `destination-resolver` |引用`DestinationResolver`解析 JMS 的策略`Destinations`。| +| `message-converter` |引用用于将 JMS 消息转换为侦听器<br/>方法参数的`MessageConverter`策略。默认值为`SimpleMessageConverter`。| +| `destination-type` |此侦听器的 JMS 目标类型:`queue`,`topic`,`durableTopic`,`sharedTopic`。<br/>或`sharedDurableTopic`。这可能启用容器的`pubSubDomain`、`subscriptionDurable`、<br/>和`subscriptionShared`属性。默认值是`queue`(禁用<br/>这三个属性)。| +|`response-destination-type`|响应的 JMS 目标类型:`queue`或`MBeanExporter`。缺省值是“destination-type”属性的值。| +| `client-id` |此侦听器容器的 JMS 客户机 ID。在使用<br/>持久订阅时需要指定它。| +| `acknowledge` |本机 JMS 确认模式:`auto`,`client`,`dups-ok`,或`transacted`。值<br/>的`transacted`激活了局部交易的`Session`。作为一种选择,你可以指定<br/>后面描述的`transaction-manager`属性。默认值为`auto`。| +| `transaction-manager` |对 Spring `JtaTransactionManager`或 `javax.transactionmanager’的引用,用于为每个<br/>传入消息启动 XA 事务。如果未指定,则使用本机确认(请参见“确认”属性)。| +| `concurrency` |为每个侦听器启动的并发会话或消费者的数量。它可以是<br/>表示最大值的简单数字(例如`5`),也可以是表示<br/>的下限和上限的范围(例如,`3-5`)。请注意,指定的最小值只是一个<br/>提示,并且在使用 JCA 侦听器容器时,该提示通常在运行时被忽略。<br/>默认值为 1。| +| `prefetch` |要加载到单个会话中的消息的最大数量。请注意,提高这个<br/>值可能会导致并发消费者的饥饿。| + +## 5. JMX + +Spring 中的 JMX(Java 管理扩展)支持提供了一些特性,这些特性使你能够轻松且透明地将你的 Spring 应用程序集成到 JMX 基础设施中。 + +JMX? + +本章不是对 JMX 的介绍。它并不试图解释为什么你可能想要使用 JMX。如果你是 JMX 的新手,请参阅本章末尾的[更多资源](#jmx-resources)。 + +具体来说, Spring 的 JMX 支持提供了四个核心特性: + +* 将任何 Spring Bean 自动注册为 JMX MBean。 + +* 一种灵活的机制,用于控制你的 bean 的管理界面。 + +* MBean 在远程 JSR-160 连接器上的声明性公开。 + +* 本地和远程 MBean 资源的简单代理。 + +这些特性被设计成在不将应用程序组件耦合到 Spring 或 JMX 接口和类的情况下工作。实际上,在大多数情况下,你的应用程序类不需要了解 Spring 或 JMX,就可以利用 Spring JMX 特性。 + +### 5.1.将你的 bean 导出到 JMX + +Spring 的 JMX 框架中的核心类是`MBeanExporter`。这个类负责获取你的 Spring bean 并将它们注册到一个 JMX`MBeanServer`。例如,考虑以下类: + +``` +package org.springframework.jmx; + +public class JmxTestBean implements IJmxTestBean { + + private String name; + private int age; + private boolean isSuperman; + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public int add(int x, int y) { + return x + y; + } + + public void dontExposeMe() { + throw new RuntimeException(); + } +} +``` + +要将此 Bean 的属性和方法公开为 MBean 的属性和操作,可以在配置文件中配置`MBeanExporter`类的实例,并在 Bean 中传递,如下例所示: + +``` +<beans> + <!-- this bean must not be lazily initialized if the exporting is to happen --> + <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter" lazy-init="false"> + <property name="beans"> + <map> + <entry key="bean:name=testBean1" value-ref="testBean"/> + </map> + </property> + </bean> + <bean id="testBean" class="org.springframework.jmx.JmxTestBean"> + <property name="name" value="TEST"/> + <property name="age" value="100"/> + </bean> +</beans> +``` + +来自前面的配置片段的相关 Bean 定义是`exporter` Bean。`beans`属性告诉`MBeanExporter`确切地说,哪些 bean 必须导出到 JMX`MBeanServer`。在默认配置中,`beans``Map`中的每个条目的键被用作对应的条目所引用的 Bean 的[gt r=“989”/>的键。你可以更改此行为,如[Controlling `ObjectName` Instances for Your Beans](#jmx-naming)中所述。 + +通过这种配置,`testBean` Bean 将作为一个 MBean 在 `objectName``bean:name=testBean1`下公开。默认情况下, Bean 的所有`public`属性作为属性公开,所有`public`方法(从 `object’类继承的方法除外)作为操作公开。 + +| |`MBeanExporter`是`Lifecycle` Bean(见[启动和关闭回调](core.html#beans-factory-lifecycle-processor))。默认情况下,在<br/>应用程序生命周期期间,MBean 会尽可能晚地导出。你可以配置`phase`的<br/>导出,或者通过设置`autoStartup`标志禁用自动注册。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 5.1.1.创建一个 mBeanServer + +`@ManagedOperationParameter`中显示的配置假定应用程序运行在一个已经运行`MBeanServer`的环境中。在这种情况下, Spring 尝试定位正在运行的`MBeanServer`,并向该服务器注册你的 bean(如果有的话)。当你的应用程序运行在具有自己的`MBeanServer`的容器(例如 Tomcat 或 IBMWebSphere)内时,这种行为非常有用。 + +但是,这种方法在独立环境中或在不提供`MBeanServer`的容器中运行时没有用。为了解决这个问题,你可以通过在配置中添加 `org.springframework.jmx.support.mbeanserverfactorybean’类的实例来声明性地创建一个 `mBeanServer’实例。通过将 `mBeanExporter’实例的`server`属性的值设置为 `mBeanServerFactoryBean’返回的`MBeanServer`值,还可以确保使用特定的`MBeanServer`,如下例所示: + +``` +<beans> + + <bean id="mbeanServer" class="org.springframework.jmx.support.MBeanServerFactoryBean"/> + + <!-- + this bean needs to be eagerly pre-instantiated in order for the exporting to occur; + this means that it must not be marked as lazily initialized + --> + <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter"> + <property name="beans"> + <map> + <entry key="bean:name=testBean1" value-ref="testBean"/> + </map> + </property> + <property name="server" ref="mbeanServer"/> + </bean> + + <bean id="testBean" class="org.springframework.jmx.JmxTestBean"> + <property name="name" value="TEST"/> + <property name="age" value="100"/> + </bean> + +</beans> +``` + +在前面的示例中,`MBeanServer`的实例由`MBeanServerFactoryBean`创建,并通过`MBeanExporter`属性提供给`MBeanExporter`。当你提供自己的“mBeanServer”实例时,`MBeanExporter`不会尝试定位正在运行的“mBeanServer”,而是使用提供的`MBeanServer`实例。为了正确地实现这一点,你必须在 Classpath 上有一个 JMX 实现。 + +#### 5.1.2.重用现有的`MBeanServer` + +如果没有指定服务器,`MBeanExporter`将尝试自动检测正在运行的 `mBeanServer’。这在大多数环境中都有效,其中只使用了一个`MBeanServer`实例。但是,当存在多个实例时,输出者可能会选择错误的服务器。在这种情况下,你应该使用`MBeanServer``agentId`来指示要使用哪个实例,如下例所示: + +``` +<beans> + <bean id="mbeanServer" class="org.springframework.jmx.support.MBeanServerFactoryBean"> + <!-- indicate to first look for a server --> + <property name="locateExistingServerIfPossible" value="true"/> + <!-- search for the MBeanServer instance with the given agentId --> + <property name="agentId" value="MBeanServer_instance_agentId>"/> + </bean> + <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter"> + <property name="server" ref="mbeanServer"/> + ... + </bean> +</beans> +``` + +对于现有的`MBeanServer`具有通过查找方法检索的动态(或未知)`agentid’的平台或情况,你应该使用[factory-method](core.html#beans-factory-class-static-factory-method),如下例所示: + +``` +<beans> + <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter"> + <property name="server"> + <!-- Custom MBeanServerLocator --> + <bean class="platform.package.MBeanServerLocator" factory-method="locateMBeanServer"/> + </property> + </bean> + + <!-- other beans here --> + +</beans> +``` + +#### 5.1.3.惰性初始化的 MBean + +如果你配置的 Bean 带有`MBeanExporter`,该配置也用于延迟初始化,则`MBeanExporter`不会破坏此契约,并避免实例化 Bean。相反,它使用`MBeanServer`注册一个代理,并将从容器获得 Bean 的时间推迟到对代理进行第一次调用时。 + +#### 5.1.4.MBeans 的自动注册 + +通过`MBeanExporter`导出并且已经是有效的 MBean 的任何 bean 都以原样在`MBeanServer`中注册,而不需要 Spring 的进一步干预。通过将`autodetect`属性设置为`true`,可以使`MBeanExporter`自动检测到 MBean,如下例所示: + +``` +<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter"> + <property name="autodetect" value="true"/> +</bean> + +<bean name="spring:mbean=true" class="org.springframework.jmx.export.TestDynamicMBean"/> +``` + +在前面的示例中,被称为`spring:mbean=true`的 Bean 已经是一个有效的 JMX MBean,并且由 Spring 自动注册。默认情况下,自动检测 JMX 注册的 Bean 的 Bean 名称用作`ObjectName`。你可以重写此行为,详见[Controlling `ObjectName` Instances for Your Beans](#jmx-naming)。 + +#### 5.1.5.控制注册行为 + +考虑 Spring `MBeanExporter`通过使用`ObjectName``autodetect`尝试用`MBeanServer`注册`MBean`的场景。如果`server`实例已在同一个`ObjectName`下注册,则默认行为是失败(并抛出`InstanceAlreadyExistsException`)。 + +你可以精确地控制当`MBean`被注册为`MBeanServer`时会发生什么。 Spring 的 JMX 支持允许三种不同的注册行为,以在注册过程发现`MBean`已经在相同的`ObjectName`下注册时控制注册行为。下表总结了这些注册行为: + +|Registration behavior|解释| +|---------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `FAIL_ON_EXISTING` |这是默认的注册行为。如果`MBean`实例已经在同一个<br/>下注册了,则正在注册的`MBean`不是<br/>注册的,则抛出一个`InstanceAlreadyExistsException`。现有的“MBean”不受影响。| +| `IGNORE_EXISTING` |如果`MBean`实例已在同一个`ObjectName`下注册,则未注册正在注册的 `mbean’。现有的`MBean`是<br/>不受影响的,并且不会抛出`Exception`。这在设置<br/>多个应用程序希望在共享的`MBean`中共享一个公共`MBean`时很有用。| +| `REPLACE_EXISTING` |如果`MBean`实例已经在同一个`ObjectName`下注册,则先前注册的<br/>现有的<br/>未注册,并且新的 `mbean’已在其位置注册(新的`Notification`有效地替换了<br/>以前的实例)。| + +上表中的值被定义为`JmxTestBean`类上的枚举。如果要更改默认的注册行为,则需要将`MBeanExporter`定义中的 `registrationPolicy’属性的值设置为其中一个值。 + +下面的示例展示了如何从默认注册行为更改为`REPLACE_EXISTING`行为: + +``` +<beans> + + <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter"> + <property name="beans"> + <map> + <entry key="bean:name=testBean1" value-ref="testBean"/> + </map> + </property> + <property name="registrationPolicy" value="REPLACE_EXISTING"/> + </bean> + + <bean id="testBean" class="org.springframework.jmx.JmxTestBean"> + <property name="name" value="TEST"/> + <property name="age" value="100"/> + </bean> + +</beans> +``` + +### 5.2.控制 bean 的管理界面 + +在[前一节](#jmx-exporting-registration-behavior)中的示例中,你几乎无法控制 Bean 的管理接口。 Bean 每个导出的`public`属性和方法都分别作为 JMX 属性和操作公开。 Spring JMX 提供了一种全面的、可扩展的机制,用于控制你的 bean 的管理接口,从而实现对你导出的 bean 的哪些属性和方法作为 JMX 属性和操作实际公开的更精细的控制。 + +#### 5.2.1.使用`MBeanInfoAssembler`接口 + +在幕后,`MBeanExporter`委托给 `org.springframework.jmx.export.assembler.mbeaninfoAssembler’接口的一个实现,该接口负责定义公开的每个 Bean 的管理接口。默认实现 `org.springframework.jmx.export.assembler.SimpleReflectiveMBeanInfoAssembler’定义了一个管理接口,该接口公开所有公共属性和方法(如你在前面几节的示例中所看到的)。 Spring 提供了`MBeanInfoAssembler`接口的两个附加实现,它们允许你通过使用源级元数据或任何任意接口来控制生成的管理接口。 + +#### 5.2.2.使用源级元数据:Java 注释 + +通过使用`MetadataMBeanInfoAssembler`,你可以通过使用源级元数据来定义 bean 的管理接口。元数据的读取由`org.springframework.jmx.export.metadata.JmxAttributeSource`接口封装。 Spring JMX 提供了一个使用 Java 注释的默认实现,即 `org.springframework.jmx.export.annotation.annotationJMXAttributeSource`。你必须使用`JmxAttributeSource`接口的实现实例来配置`MetadataMBeanInfoAssembler`才能使其正常工作(没有默认值)。 + +要将 Bean 标记为导出到 JMX,你应该使用“ManagedResource”注释对 Bean 类进行注释。你必须用`ManagedOperation`注释将希望公开的每个方法标记为一个操作,并用`ManagedAttribute`注释标记希望公开的每个属性。在标记属性时,可以省略 getter 或 setter 的注释,以分别创建只写或只读属性。 + +| |带有注释的 Bean 的`ManagedResource`必须是公共的,就像公开<br/>操作或属性的方法一样。| +|---|-----------------------------------------------------------------------------------------------------------------| + +下面的示例显示了我们在[创建一个 mBeanServer](#jmx-exporting-mbeanserver)中使用的`MBean`类的注释版本: + +``` +package org.springframework.jmx; + +import org.springframework.jmx.export.annotation.ManagedResource; +import org.springframework.jmx.export.annotation.ManagedOperation; +import org.springframework.jmx.export.annotation.ManagedAttribute; + +@ManagedResource( + objectName="bean:name=testBean4", + description="My Managed Bean", + log=true, + logFile="jmx.log", + currencyTimeLimit=15, + persistPolicy="OnUpdate", + persistPeriod=200, + persistLocation="foo", + persistName="bar") +public class AnnotationTestBean implements IJmxTestBean { + + private String name; + private int age; + + @ManagedAttribute(description="The Age Attribute", currencyTimeLimit=15) + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + @ManagedAttribute(description="The Name Attribute", + currencyTimeLimit=20, + defaultValue="bar", + persistPolicy="OnUpdate") + public void setName(String name) { + this.name = name; + } + + @ManagedAttribute(defaultValue="foo", persistPeriod=300) + public String getName() { + return name; + } + + @ManagedOperation(description="Add two numbers") + @ManagedOperationParameters({ + @ManagedOperationParameter(name = "x", description = "The first number"), + @ManagedOperationParameter(name = "y", description = "The second number")}) + public int add(int x, int y) { + return x + y; + } + + public void dontExposeMe() { + throw new RuntimeException(); + } + +} +``` + +在前面的示例中,你可以看到`JmxTestBean`类被标记为 `ManagedResource’注释,并且`ManagedResource`注释配置了一组属性。这些属性可用于配置由[源级元数据类型](#jmx-interface-metadata-types)生成的 MBean 的各个方面,并在后面的[源级元数据类型](#jmx-interface-metadata-types)中进行更详细的说明。 + +`age`和`name`属性都使用`ManagedAttribute`注释,但是,在`name`属性的情况下,只标记 getter。这导致这两个属性都作为属性包含在管理接口中,但是`age`属性是只读的。 + +最后,`add(int, int)`方法被标记为`ManagedOperation`属性,而`dontExposeMe()`方法则不是。当你使用`MetadataMBeanInfoAssembler`时,这将导致管理 INT 面只包含一个操作(`add(INT,int)`)。 + +下面的配置显示了如何配置`MBeanExporter`以使用 `MetadatambeanInfoAssembler’: + +``` +<beans> + <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter"> + <property name="assembler" ref="assembler"/> + <property name="namingStrategy" ref="namingStrategy"/> + <property name="autodetect" value="true"/> + </bean> + + <bean id="jmxAttributeSource" + class="org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource"/> + + <!-- will create management interface using annotation metadata --> + <bean id="assembler" + class="org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler"> + <property name="attributeSource" ref="jmxAttributeSource"/> + </bean> + + <!-- will pick up the ObjectName from the annotation --> + <bean id="namingStrategy" + class="org.springframework.jmx.export.naming.MetadataNamingStrategy"> + <property name="attributeSource" ref="jmxAttributeSource"/> + </bean> + + <bean id="testBean" class="org.springframework.jmx.AnnotationTestBean"> + <property name="name" value="TEST"/> + <property name="age" value="100"/> + </bean> +</beans> +``` + +在前面的示例中,`MetadataMBeanInfoAssembler` Bean 已配置了`AnnotationJmxAttributeSource`类的实例,并通过 Assembler 属性传递给`MBeanExporter`。这就是为暴露在 Spring 中的 MBean 利用元数据驱动的管理接口所需的全部内容。 + +#### 5.2.3.源级元数据类型 + +下表描述了可在 Spring JMX 中使用的源级元数据类型: + +| Purpose |注释| Annotation Type | +|---------------------------------------------------------|--------------------------------------------------------------|---------------------------------| +|Mark all instances of a `Class` as JMX managed resources.|`@ManagedResource`| Class | +| Mark a method as a JMX operation. |`@ManagedOperation`| Method | +| Mark a getter or setter as one half of a JMX attribute. |`@ManagedAttribute`|Method (only getters and setters)| +| Define descriptions for operation parameters. |`@ManagedOperationParameter`和[MetadatanamingStrategy](#jmx-naming-metadata)| Method | + +下表描述了可用于这些源级元数据类型的配置参数: + +| Parameter | Description |适用于| +|-------------------|-------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------| +| `ObjectName` |Used by `MetadataNamingStrategy` to determine the `ObjectName` of a managed resource.|`ManagedResource`| +| `description` | Sets the friendly description of the resource, attribute or operation. |`ManagedResource`,`ManagedAttribute`,`ManagedOperation`,或`ManagedOperationParameter`| +|`currencyTimeLimit`| Sets the value of the `currencyTimeLimit` descriptor field. |`ManagedResource`或`ManagedAttribute`| +| `defaultValue` | Sets the value of the `defaultValue` descriptor field. |`ManagedAttribute`| +| `log` | Sets the value of the `log` descriptor field. |`ManagedResource`| +| `logFile` | Sets the value of the `logFile` descriptor field. |`ManagedResource`| +| `persistPolicy` | Sets the value of the `persistPolicy` descriptor field. |`ManagedResource`| +| `persistPeriod` | Sets the value of the `persistPeriod` descriptor field. |`ManagedResource`| +| `persistLocation` | Sets the value of the `persistLocation` descriptor field. |`ManagedResource`| +| `persistName` | Sets the value of the `persistName` descriptor field. |`ManagedResource`| +| `name` | Sets the display name of an operation parameter. |`ManagedOperationParameter`| +| `index` | Sets the index of an operation parameter. |`ManagedOperationParameter`| + +#### 5.2.4.使用`AutodetectCapableMBeanInfoAssembler`接口 + +Spring 为了进一步简化配置,包括 `AutodetectCapableMBeanInfoAssembler’接口,该接口扩展了`MBeanInfoAssembler`接口,以添加对 MBean 资源的自动检测的支持。如果你使用`AutodetectCapableMBeanInfoAssembler`实例配置 `mbeanexporter’,则允许它对包含用于暴露于 JMX 的 bean 进行“投票”。 + +`AutodetectCapableMBeanInfo`接口的唯一实现是`serverConnector`,它投票支持包含标记有`ManagedResource`属性的任何 Bean。在这种情况下,默认的方法是使用 Bean 名称作为`ObjectName`,这将导致类似于以下配置的结果: + +``` +<beans> + + <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter"> + <!-- notice how no 'beans' are explicitly configured here --> + <property name="autodetect" value="true"/> + <property name="assembler" ref="assembler"/> + </bean> + + <bean id="testBean" class="org.springframework.jmx.JmxTestBean"> + <property name="name" value="TEST"/> + <property name="age" value="100"/> + </bean> + + <bean id="assembler" class="org.springframework.jmx.export.assembler.MetadataMBeanInfoAssembler"> + <property name="attributeSource"> + <bean class="org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource"/> + </property> + </bean> + +</beans> +``` + +注意,在前面的配置中,没有向`MBeanExporter`传递任何 bean。但是,`JmxTestBean`仍然是注册的,因为它被标记为`ManagedResource`属性,并且`MetadataMBeanInfoAssembler`检测到并投票包含它。这种方法的唯一问题是`JmxTestBean`的名称现在具有业务含义。你可以通过更改`ObjectName`中定义的[Controlling `ObjectName` Instances for Your Beans](#jmx-naming)创建的默认行为来解决此问题。 + +#### 5.2.5.使用 Java 接口定义管理接口 + +除了`MetadataMBeanInfoAssembler`之外, Spring 还包括“InterfaceBasedMBeanInfoAssembler”,它允许你约束基于接口集合中定义的一组方法公开的方法和属性。 + +尽管公开 MBean 的标准机制是使用接口和一个简单的命名方案,但`InterfaceBasedMBeanInfoAssembler`通过消除对命名约定的需求、允许你使用多个接口以及消除对你的 Bean 实现 MBean 接口的需求,扩展了这一功能。 + +考虑一下下面的接口,它用于为我们前面展示的“JMXTestBean”类定义一个管理接口: + +``` +public interface IJmxTestBean { + + public int add(int x, int y); + + public long myOperation(); + + public int getAge(); + + public void setAge(int age); + + public void setName(String name); + + public String getName(); + +} +``` + +该接口定义了作为 JMX MBean 上的操作和属性公开的方法和属性。下面的代码展示了如何配置 Spring JMX 以使用此接口作为管理接口的定义: + +``` +<beans> + + <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter"> + <property name="beans"> + <map> + <entry key="bean:name=testBean5" value-ref="testBean"/> + </map> + </property> + <property name="assembler"> + <bean class="org.springframework.jmx.export.assembler.InterfaceBasedMBeanInfoAssembler"> + <property name="managedInterfaces"> + <value>org.springframework.jmx.IJmxTestBean</value> + </property> + </bean> + </property> + </bean> + + <bean id="testBean" class="org.springframework.jmx.JmxTestBean"> + <property name="name" value="TEST"/> + <property name="age" value="100"/> + </bean> + +</beans> +``` + +在前面的示例中,`InterfaceBasedMBeanInfoAssembler`被配置为在为任何 Bean 构建管理接口时使用 `IJMXTestBean’接口。理解由`InterfaceBasedMBeanInfoAssembler`处理的 bean 并不是实现用于生成 JMX 管理接口的接口所必需的,这一点很重要。 + +在前面的例子中,`IJmxTestBean`接口用于为所有 bean 构造所有管理接口。在许多情况下,这不是所需的行为,你可能希望为不同的 bean 使用不同的接口。在这种情况下,可以通过`interfaceMappings`属性传递 `InterfaceBasedMBeanInfoAssembler`a`Properties`实例,其中每个条目的键是 Bean 名称,每个条目的值是一个逗号分隔的接口名称列表,用于 Bean。 + +如果没有通过`managedInterfaces`或 `interfaceppings’属性指定管理接口,则`InterfaceBasedMBeanInfoAssembler`将反映在 Bean 上,并使用由 Bean 实现的所有接口来创建管理接口。 + +#### 5.2.6.使用`MethodNameBasedMBeanInfoAssembler` + +`MethodNameBasedMBeanInfoAssembler`允许你指定作为属性和操作公开给 JMX 的方法名列表。下面的代码展示了一个示例配置: + +``` +<bean id="exporter" class="org.springframework.jmx.export.MBeanExporter"> + <property name="beans"> + <map> + <entry key="bean:name=testBean5" value-ref="testBean"/> + </map> + </property> + <property name="assembler"> + <bean class="org.springframework.jmx.export.assembler.MethodNameBasedMBeanInfoAssembler"> + <property name="managedMethods"> + <value>add,myOperation,getName,setName,getAge</value> + </property> + </bean> + </property> +</bean> +``` + +在前面的示例中,可以看到`add`和`myOperation`方法作为 JMX 操作公开,`getName()`、`setName(String)`和`getAge()`作为 JMX 属性的适当部分公开。在前面的代码中,方法映射应用于公开给 JMX 的 bean。要在 Bean-by- Bean 的基础上控制方法曝光,可以使用`methodMappings`的`MethodNameMBeanInfoAssembler`属性将 Bean 名称映射到方法名称列表。 + +### 5.3.控制 bean 的`ObjectName`实例 + +在幕后,`MBeanExporter`将委托给 `ObjectNamingStrategy’的一个实现,以获得它所注册的每个 bean 的`ObjectName`实例。默认情况下,默认实现`KeyNamingStrategy`使用 `bean``Map`的键作为`ObjectName`。此外,`KeyNamingStrategy`可以将`beans``Map`的键映射到`Properties`文件(或文件)中的一个条目,以解析 `objectName’。除了`KeyNamingStrategy`之外, Spring 还提供了两个额外的 `ObjectNamingStrategy’实现:`IdentityNamingStrategy`(基于 Bean 的 JVM 标识构建 `ObjectName’)和`MetadataNamingStrategy`(使用源级元数据获得`ObjectName`)。 + +#### 5.3.1.从属性读取`ObjectName`实例 + +你可以配置自己的`KeyNamingStrategy`实例,并将其配置为从`Properties`实例中读取 `ObjectName’实例,而不是使用 Bean 键。keynamingstrategy 试图在`Properties`中使用与 Bean 键对应的键来定位一个条目。如果没有找到条目,或者`Properties`实例是 `null’,则使用 Bean 键本身。 + +下面的代码显示了`KeyNamingStrategy`的示例配置: + +``` +<beans> + + <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter"> + <property name="beans"> + <map> + <entry key="testBean" value-ref="testBean"/> + </map> + </property> + <property name="namingStrategy" ref="namingStrategy"/> + </bean> + + <bean id="testBean" class="org.springframework.jmx.JmxTestBean"> + <property name="name" value="TEST"/> + <property name="age" value="100"/> + </bean> + + <bean id="namingStrategy" class="org.springframework.jmx.export.naming.KeyNamingStrategy"> + <property name="mappings"> + <props> + <prop key="testBean">bean:name=testBean1</prop> + </props> + </property> + <property name="mappingLocations"> + <value>names1.properties,names2.properties</value> + </property> + </bean> + +</beans> +``` + +前面的示例使用`KeyNamingStrategy`实例配置`Properties`实例,该实例是从映射属性定义的`Properties`实例和映射属性定义的路径中的属性文件合并而来的。在这种配置中,`testBean` Bean 给定了`ObjectName`的`bean:name=testBean1`,因为这是`Properties`实例中的条目,该实例具有与 Bean 键对应的键。 + +如果在`Properties`实例中找不到条目,则 Bean 键名被用作`ObjectName`。 + +#### 5.3.2.使用`MetadataNamingStrategy` + +`MetadataNamingStrategy`在每个 Bean 上使用`objectName`属性的`objectName`属性来创建`ObjectName`。下面的代码显示了`MetadataNamingStrategy`的配置: + +``` +<beans> + + <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter"> + <property name="beans"> + <map> + <entry key="testBean" value-ref="testBean"/> + </map> + </property> + <property name="namingStrategy" ref="namingStrategy"/> + </bean> + + <bean id="testBean" class="org.springframework.jmx.JmxTestBean"> + <property name="name" value="TEST"/> + <property name="age" value="100"/> + </bean> + + <bean id="namingStrategy" class="org.springframework.jmx.export.naming.MetadataNamingStrategy"> + <property name="attributeSource" ref="attributeSource"/> + </bean> + + <bean id="attributeSource" + class="org.springframework.jmx.export.annotation.AnnotationJmxAttributeSource"/> + +</beans> +``` + +如果没有为`objectName`属性提供`ManagedResource`,则使用以下格式创建一个 `ObjectName’:`ObjectName`。例如,以下 Bean 生成的`ObjectName`将是 `com。例如:type=myclass,name=mybean`: + +``` +<bean id="myBean" class="com.example.MyClass"/> +``` + +#### 5.3.3.配置基于注释的 MBean 导出 + +如果你喜欢使用[基于注释的方法](#jmx-interface-metadata)来定义你的管理接口,那么可以使用`MBeanExporter`的一个方便的子类:`AnnotationmBeanExporter’。在定义这个子类的实例时,你不再需要 `namingstrategy’、`assembler`和`ObjectName`配置,因为它总是使用标准的基于 Java 注释的元数据(也总是启用自动检测)。实际上,与定义`MBeanExporter` Bean 不同的是,`@EnableMBeanExport``@Configuration`注释支持更简单的语法,如下例所示: + +``` +@Configuration +@EnableMBeanExport +public class AppConfig { + +} +``` + +如果你更喜欢基于 XML 的配置,则`<context:mbean-export/>`元素具有相同的目的,如以下清单所示: + +``` +<context:mbean-export/> +``` + +如果有必要,可以提供对特定 MBean`server`的引用,并且 `defaultDomain’属性(属性为`AnnotationMBeanExporter`)接受生成的 MBean`ObjectName`域的替换值。正如下面的示例所示,正如在[MetadatanamingStrategy](#jmx-naming-metadata)上一节中所描述的那样,这将代替完全限定的包名: + +``` +@EnableMBeanExport(server="myMBeanServer", defaultDomain="myDomain") +@Configuration +ContextConfiguration { + +} +``` + +下面的示例展示了前面的基于注释的示例的 XML 等价物: + +``` +<context:mbean-export server="myMBeanServer" default-domain="myDomain"/> +``` + +| |不要在 Bean 类中结合自动检测 JMX<br/>注释使用基于接口的 AOP 代理。基于接口的代理“隐藏”目标类,它<br/>还隐藏 JMX 管理的资源注释。因此,你应该在<br/>的情况下使用目标类代理(通过在[MetadatanamingStrategy](#jmx-naming-metadata)、<tx:annotation-driven/>“上设置”proxy-target-class" 标志,以此类推)。否则,在<br/>启动时,你的 JMX Bean 可能会被忽略。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 5.4.使用 JSR-160 连接器 + +对于远程访问, Spring JMX 模块在 `org.springframework.jmx.support` 包中提供了两个`FactoryBean`实现,用于创建服务器端和客户端连接器。 + +#### 5.4.1.服务器端连接器 + +要让 Spring JMX 创建、启动和公开一个 JSR-160`JMXConnectorServer`,你可以使用以下配置: + +``` +<bean id="serverConnector" class="org.springframework.jmx.support.ConnectorServerFactoryBean"/> +``` + +默认情况下,`ConnectorServerFactoryBean`创建一个`JMXConnectorServer`绑定到 `service:jmx:jmxmp://localhost:9875’的`JMXConnectorServer`。因此,`serverConnector` Bean 通过 LocalHost 上的 JMXMP 协议(端口 9875)将本地`MBeanServer`公开给客户端。请注意,JMXMP 协议在 JSR160 规范中被标记为可选的。目前,主要的开源 JMX 实现 MX4J 和 JDK 提供的 JDJ 都不支持 JMXMP。 + +要指定另一个 URL 并将`JMXConnectorServer`本身注册到 `mBeanServer’中,你可以分别使用<tx:annotation-driven/>和`ObjectName`属性,如下例所示: + +``` +<bean id="serverConnector" + class="org.springframework.jmx.support.ConnectorServerFactoryBean"> + <property name="objectName" value="connector:name=rmi"/> + <property name="serviceUrl" + value="service:jmx:rmi://localhost/jndi/rmi://localhost:1099/myconnector"/> +</bean> +``` + +如果设置了`ObjectName`属性, Spring 会自动在`ObjectName`下用`MBeanServer`注册连接器。下面的示例显示了在创建 `JMXConnector’时可以传递给`ConnectorServerFactoryBean`的完整参数集: + +``` +<bean id="serverConnector" + class="org.springframework.jmx.support.ConnectorServerFactoryBean"> + <property name="objectName" value="connector:name=iiop"/> + <property name="serviceUrl" + value="service:jmx:iiop://localhost/jndi/iiop://localhost:900/myconnector"/> + <property name="threaded" value="true"/> + <property name="daemon" value="true"/> + <property name="environment"> + <map> + <entry key="someKey" value="someValue"/> + </map> + </property> +</bean> +``` + +请注意,当你使用基于 RMI 的连接器时,需要启动查找服务(“tnameserv”或“rmiRegistry”)才能完成名称注册。如果使用 Spring 通过 RMI 为你导出远程服务,则 Spring 已经构建了 RMI 注册中心。如果没有,你可以通过使用以下配置片段轻松启动注册表: + +``` +<bean id="registry" class="org.springframework.remoting.rmi.RmiRegistryFactoryBean"> + <property name="port" value="1099"/> +</bean> +``` + +#### 5.4.2.客户端连接器 + +要为远程 JSR-160 启用的`MBeanServerConnection`创建`MBeanServer`,可以使用 `mBeanServerConnectionFactoryBean`,如下例所示: + +``` +<bean id="clientConnector" class="org.springframework.jmx.support.MBeanServerConnectionFactoryBean"> + <property name="serviceUrl" value="service:jmx:rmi://localhost/jndi/rmi://localhost:1099/jmxrmi"/> +</bean> +``` + +#### 5.4.3.JMX 超过 Hessian 或 SOAP + +JSR-160 允许扩展客户机和服务器之间的通信方式。前面几节中所示的示例使用了 JSR-160 规范(IIOP 和 JRMP)和(可选的)JMXMP 所要求的基于 RMI 的强制实现。通过使用其他提供程序或 JMX 实现(例如[MX4J](http://mx4j.sourceforge.net)),你可以利用 SOAP 或 Hessian 等协议而不是简单的 HTTP 或 SSL 等协议,如下例所示: + +``` +<bean id="serverConnector" class="org.springframework.jmx.support.ConnectorServerFactoryBean"> + <property name="objectName" value="connector:name=burlap"/> + <property name="serviceUrl" value="service:jmx:burlap://localhost:9874"/> +</bean> +``` + +在前面的示例中,我们使用了 MX4J3.0.0。有关更多信息,请参见官方的 MX4J 文档。 + +### 5.5.通过代理访问 MBean + +Spring JMX 允许你创建将调用重新路由到注册在本地或远程`MBeanServer`中的 MBean 的代理。这些代理为你提供了一个标准的 Java 接口,你可以通过该接口与 MBean 进行交互。下面的代码展示了如何为在本地`MBeanServer`中运行的 MBean 配置代理: + +``` +<bean id="proxy" class="org.springframework.jmx.access.MBeanProxyFactoryBean"> + <property name="objectName" value="bean:name=testBean"/> + <property name="proxyInterface" value="org.springframework.jmx.IJmxTestBean"/> +</bean> +``` + +在前面的示例中,你可以看到为在`MimeMessage`的 `ObjectName’下注册的 MBean 创建了一个代理。代理实现的接口集由`proxyInterfaces`属性控制,将这些接口上的方法和属性映射到 MBean 上的操作和属性的规则与`InterfaceBasedMBeanInfoAssembler`使用的规则相同。 + +`MBeanProxyFactoryBean`可以创建一个代理,该代理可以通过“mBeanServerConnection”访问任何 MBean。默认情况下,定位并使用了本地`MBeanServer`,但你可以重写此内容,并提供指向远程 `mBeanServer’的`MBeanServerConnection`,以满足指向远程 mBean 的代理的需要: + +``` +<bean id="clientConnector" + class="org.springframework.jmx.support.MBeanServerConnectionFactoryBean"> + <property name="serviceUrl" value="service:jmx:rmi://remotehost:9875"/> +</bean> + +<bean id="proxy" class="org.springframework.jmx.access.MBeanProxyFactoryBean"> + <property name="objectName" value="bean:name=testBean"/> + <property name="proxyInterface" value="org.springframework.jmx.IJmxTestBean"/> + <property name="server" ref="clientConnector"/> +</bean> +``` + +在前面的示例中,我们创建一个`MBeanServerConnection`,它指向使用`MBeanServerConnectionFactoryBean`的远程计算机。然后通过`server`属性将此`MBeanServerConnection`传递给`MBeanProxyFactoryBean`。创建的代理通过这个“mBeanServerConnection”将所有调用转发到`MBeanServer`。 + +### 5.6.通知 + +Spring 的 JMX 产品包括对 JMX 通知的全面支持。 + +#### 5.6.1.为通知注册监听器 + +Spring 的 JMX 支持使得在任意数量的 MBean 中注册任意数量的“notificationListener”变得很容易(这包括 Spring 的`MBeanExporter`导出的 MBean 和通过某些其他机制注册的 MBean)。例如,考虑这样的场景:每当目标 MBean 的属性发生变化时,都希望(通过“通知”)获得通知。以下示例将通知写入控制台: + +``` +package com.example; + +import javax.management.AttributeChangeNotification; +import javax.management.Notification; +import javax.management.NotificationFilter; +import javax.management.NotificationListener; + +public class ConsoleLoggingNotificationListener + implements NotificationListener, NotificationFilter { + + public void handleNotification(Notification notification, Object handback) { + System.out.println(notification); + System.out.println(handback); + } + + public boolean isNotificationEnabled(Notification notification) { + return AttributeChangeNotification.class.isAssignableFrom(notification.getClass()); + } + +} +``` + +下面的示例将`ConsoleLoggingNotificationListener`(在前面的示例中定义)添加到`notificationListenerMappings`: + +``` +<beans> + + <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter"> + <property name="beans"> + <map> + <entry key="bean:name=testBean1" value-ref="testBean"/> + </map> + </property> + <property name="notificationListenerMappings"> + <map> + <entry key="bean:name=testBean1"> + <bean class="com.example.ConsoleLoggingNotificationListener"/> + </entry> + </map> + </property> + </bean> + + <bean id="testBean" class="org.springframework.jmx.JmxTestBean"> + <property name="name" value="TEST"/> + <property name="age" value="100"/> + </bean> + +</beans> +``` + +有了前面的配置,每次从目标 MBean(` Bean:name=TestBean1`)广播一个 JMX时,就会通知通过属性注册为侦听器的 Bean。然后,`ConsoleLoggingNotificationListener` Bean 可以对`Notification`采取它认为适当的任何操作。 + +还可以使用 Straight Bean names 作为导出的 bean 和侦听器之间的链接,如下例所示: + +``` +<beans> + + <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter"> + <property name="beans"> + <map> + <entry key="bean:name=testBean1" value-ref="testBean"/> + </map> + </property> + <property name="notificationListenerMappings"> + <map> + <entry key="testBean"> + <bean class="com.example.ConsoleLoggingNotificationListener"/> + </entry> + </map> + </property> + </bean> + + <bean id="testBean" class="org.springframework.jmx.JmxTestBean"> + <property name="name" value="TEST"/> + <property name="age" value="100"/> + </bean> + +</beans> +``` + +如果你想为所包含的`MBeanExporter`导出的所有 bean 注册一个`NotificationListener`实例,则可以使用特殊通配符作为`notificationListenerMappings`属性映射中一个条目的键,如下例所示: + +``` +<property name="notificationListenerMappings"> + <map> + <entry key="*"> + <bean class="com.example.ConsoleLoggingNotificationListener"/> + </entry> + </map> +</property> +``` + +如果需要执行逆操作(即针对 MBean 注册多个不同的侦听器),则必须使用`notificationListeners`list 属性(优先于`notificationListenerMappings`属性)。这一次,我们不再为单个 MBean 配置`NotificationListener`,而是配置 `NotificationListenerBean’实例。a`NotificationListenerBean`封装了一个 `notificationListener’和将在`MBeanServer`中注册的`ObjectName`(或`ObjectNames`)。`NotificationListenerBean`还封装了许多其他属性,例如`NotificationFilter`和可以在高级 JMX 通知场景中使用的任意 handback 对象。 + +使用`NotificationListenerBean`实例时的配置与前面介绍的配置没有很大不同,如下例所示: + +``` +<beans> + + <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter"> + <property name="beans"> + <map> + <entry key="bean:name=testBean1" value-ref="testBean"/> + </map> + </property> + <property name="notificationListeners"> + <list> + <bean class="org.springframework.jmx.export.NotificationListenerBean"> + <constructor-arg> + <bean class="com.example.ConsoleLoggingNotificationListener"/> + </constructor-arg> + <property name="mappedObjectNames"> + <list> + <value>bean:name=testBean1</value> + </list> + </property> + </bean> + </list> + </property> + </bean> + + <bean id="testBean" class="org.springframework.jmx.JmxTestBean"> + <property name="name" value="TEST"/> + <property name="age" value="100"/> + </bean> + +</beans> +``` + +前面的示例与第一个通知示例等价。然后,假设我们希望在每次提出`Notification`时都给出一个 handback 对象,并且我们还希望通过提供 `notificationfilter’过滤掉无关的`Notifications`。下面的示例实现了这些目标: + +``` +<beans> + + <bean id="exporter" class="org.springframework.jmx.export.MBeanExporter"> + <property name="beans"> + <map> + <entry key="bean:name=testBean1" value-ref="testBean1"/> + <entry key="bean:name=testBean2" value-ref="testBean2"/> + </map> + </property> + <property name="notificationListeners"> + <list> + <bean class="org.springframework.jmx.export.NotificationListenerBean"> + <constructor-arg ref="customerNotificationListener"/> + <property name="mappedObjectNames"> + <list> + <!-- handles notifications from two distinct MBeans --> + <value>bean:name=testBean1</value> + <value>bean:name=testBean2</value> + </list> + </property> + <property name="handback"> + <bean class="java.lang.String"> + <constructor-arg value="This could be anything..."/> + </bean> + </property> + <property name="notificationFilter" ref="customerNotificationListener"/> + </bean> + </list> + </property> + </bean> + + <!-- implements both the NotificationListener and NotificationFilter interfaces --> + <bean id="customerNotificationListener" class="com.example.ConsoleLoggingNotificationListener"/> + + <bean id="testBean1" class="org.springframework.jmx.JmxTestBean"> + <property name="name" value="TEST"/> + <property name="age" value="100"/> + </bean> + + <bean id="testBean2" class="org.springframework.jmx.JmxTestBean"> + <property name="name" value="ANOTHER TEST"/> + <property name="age" value="200"/> + </bean> + +</beans> +``` + +(关于什么是 handback 对象以及`NotificationFilter`是什么的详细讨论,请参见 JMX 规范(1.2)中题为“JMX 通知模型”的部分。 + +#### 5.6.2.发布通知 + +Spring 不仅提供了对注册来接收`Notifications`的支持,而且还提供了对发布`Notifications`的支持。 + +| |这一部分实际上只与 Spring 管理的 bean 相关,这些 bean 的<br/>通过`MBeanExporter`被暴露为 MBean。任何现有的用户定义的 MBean 都应该<br/>使用标准的 JMXAPI 进行通知发布。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +Spring 的 JMX 通知发布支持中的关键接口是“NotificationPublisher”接口(在“org.springframework.jmx.export.Notification”包中定义)。将通过`MBeanExporter`实例导出为 MBean 的任何 Bean 都可以实现相关的 `NotificationPublisherAware’接口,以访问`NotificationPublisher`实例。`NotificationPublisherAware`接口通过一个简单的 setter 方法向实现 Bean 提供一个 `NotificationPublisher’的实例, Bean 然后可以使用该方法来发布`Notifications`。 + +正如[NotificationPublisher’](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jmx/export/notification/NotificationPublisher.html)接口的 Javadoc 中所述,通过`NotificationPublisher`机制发布事件的托管 bean 不负责通知侦听器的状态管理。 Spring 的 JMX 支持负责处理所有的 JMX 基础设施问题。作为应用程序开发人员,你所需要做的就是实现“NotificationPublisherAware”接口,并通过使用提供的`NotificationPublisher`实例开始发布事件。请注意,`NotificationPublisher`是在托管 Bean 已注册为`MBeanServer`之后设置的。 + +使用`NotificationPublisher`实例非常简单。你创建一个 JMX`Notification’实例(或一个适当的`Notification`子类的实例),用与将要发布的事件相关的数据填充通知,并在 `NotificationPublisher’实例上调用`sendNotification(Notification)`,传入`Notification`。 + +在下面的示例中,每当调用`add(int, int)`操作时,导出的`JmxTestBean`实例都会发布一个 `NotificationEvent’: + +``` +package org.springframework.jmx; + +import org.springframework.jmx.export.notification.NotificationPublisherAware; +import org.springframework.jmx.export.notification.NotificationPublisher; +import javax.management.Notification; + +public class JmxTestBean implements IJmxTestBean, NotificationPublisherAware { + + private String name; + private int age; + private boolean isSuperman; + private NotificationPublisher publisher; + + // other getters and setters omitted for clarity + + public int add(int x, int y) { + int answer = x + y; + this.publisher.sendNotification(new Notification("add", this, 0)); + return answer; + } + + public void dontExposeMe() { + throw new RuntimeException(); + } + + public void setNotificationPublisher(NotificationPublisher notificationPublisher) { + this.publisher = notificationPublisher; + } + +} +``` + +`NotificationPublisher`接口和使其全部工作的机制是 Spring 的 JMX 支持的更好的功能之一。然而,它确实带来了将类与 Spring 和 JMX 耦合的代价。和往常一样,这里的建议是要务实。如果你需要`Notifications`提供的功能,并且可以接受 Spring 和 JMX 的耦合,那么就这样做。 + +### 5.7.更多资源 + +本节包含指向有关 JMX 的更多资源的链接: + +* 甲骨文的[JMX homepage](https://www.oracle.com/technetwork/java/javase/tech/javamanagement-140525.html)。 + +* [JMX 规范](https://jcp.org/aboutJava/communityprocess/final/jsr003/index3.html)(jsr-000003)。 + +* [JMX 远程 API 规范](https://jcp.org/aboutJava/communityprocess/final/jsr160/index.html)(jsr-000160)。 + +* [MX4J homepage](http://mx4j.sourceforge.net/)。(MX4J 是各种 JMX 规范的开放源代码实现。 + +## 6. 电子邮件 + +本节描述了如何使用 Spring 框架发送电子邮件。 + +库依赖项 + +为了使用 Spring 框架的电子邮件库,需要在你的应用程序的 Classpath 上设置以下 jar: + +* [Javamail/Jakarta Mail1.6](https://eclipse-ee4j.github.io/mail/)库 + +该图书馆可在网上免费查阅——例如,在 Maven Central 以 `com.sun.mail:jakarta.mail’的形式提供。请确保使用最新的 1.6.x 版本,而不是 Jakarta Mail2.0(它带有不同的包名称空间)。 + +Spring 框架提供了用于发送电子邮件的有用的实用库,该实用库保护你不受底层邮件系统的详细信息的影响,并代表客户机负责低级别的资源处理。 + +`org.springframework.mail`包是 Spring 框架电子邮件支持的根级别包。发送电子邮件的中心接口是`MailSender`接口。一个简单的值对象封装了一个简单邮件的属性,如`from`和`to`(加上许多其他的)是`SimpleMailMessage`类。这个包还包含一个检查异常的层次结构,它们提供了比较低级别邮件系统异常更高级别的抽象,根异常是“MailException”。有关 Rich Mail 异常层次结构的更多信息,请参见[javadoc](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/mail/MailException.html)。 + +`MailSender`接口增加了专门的 JavaMail 功能,例如对`MailSender`接口(它从该接口继承而来)的 MIME 消息支持。`JavaMailSender`还提供了一个名为 `org.springframework.mail.javamail.mimeMessagePreparator` 的回调接口,用于准备`MimeMessage`。 + +### 6.1.用法 + +假设我们有一个名为`OrderManager`的业务接口,如下例所示: + +``` +public interface OrderManager { + + void placeOrder(Order order); + +} +``` + +进一步假设我们有一个要求,说明需要生成带有订单号的电子邮件消息,并将其发送给下了相关订单的客户。 + +#### 6.1.1.基本`MailSender`和`SimpleMailMessage`用法 + +下面的示例展示了如何在有人下订单时使用`MailSender`和`SimpleMailMessage`发送电子邮件: + +``` +import org.springframework.mail.MailException; +import org.springframework.mail.MailSender; +import org.springframework.mail.SimpleMailMessage; + +public class SimpleOrderManager implements OrderManager { + + private MailSender mailSender; + private SimpleMailMessage templateMessage; + + public void setMailSender(MailSender mailSender) { + this.mailSender = mailSender; + } + + public void setTemplateMessage(SimpleMailMessage templateMessage) { + this.templateMessage = templateMessage; + } + + public void placeOrder(Order order) { + + // Do the business calculations... + + // Call the collaborators to persist the order... + + // Create a thread safe "copy" of the template message and customize it + SimpleMailMessage msg = new SimpleMailMessage(this.templateMessage); + msg.setTo(order.getCustomer().getEmailAddress()); + msg.setText( + "Dear " + order.getCustomer().getFirstName() + + order.getCustomer().getLastName() + + ", thank you for placing order. Your order number is " + + order.getOrderNumber()); + try { + this.mailSender.send(msg); + } + catch (MailException ex) { + // simply log it and go on... + System.err.println(ex.getMessage()); + } + } + +} +``` + +下面的示例显示了 Bean 上述代码的定义: + +``` +<bean id="mailSender" class="org.springframework.mail.javamail.JavaMailSenderImpl"> + <property name="host" value="mail.mycompany.example"/> +</bean> + +<!-- this is a template message that we can pre-load with default state --> +<bean id="templateMessage" class="org.springframework.mail.SimpleMailMessage"> + <property name="from" value="[email protected]"/> + <property name="subject" value="Your order"/> +</bean> + +<bean id="orderManager" class="com.mycompany.businessapp.support.SimpleOrderManager"> + <property name="mailSender" ref="mailSender"/> + <property name="templateMessage" ref="templateMessage"/> +</bean> +``` + +#### 6.1.2.使用`JavaMailSender`和`MimeMessagePreparator` + +本节描述`OrderManager`的另一个实现,它使用`MimeMessagePreparator`回调接口。在下面的示例中,`mailSender`属性的类型为 `JavaMailSender’,因此我们能够使用 JavaMail`MimeMessage`类: + +``` +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; + +import javax.mail.internet.MimeMessage; +import org.springframework.mail.MailException; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessagePreparator; + +public class SimpleOrderManager implements OrderManager { + + private JavaMailSender mailSender; + + public void setMailSender(JavaMailSender mailSender) { + this.mailSender = mailSender; + } + + public void placeOrder(final Order order) { + // Do the business calculations... + // Call the collaborators to persist the order... + + MimeMessagePreparator preparator = new MimeMessagePreparator() { + public void prepare(MimeMessage mimeMessage) throws Exception { + mimeMessage.setRecipient(Message.RecipientType.TO, + new InternetAddress(order.getCustomer().getEmailAddress())); + mimeMessage.setFrom(new InternetAddress("[email protected]")); + mimeMessage.setText("Dear " + order.getCustomer().getFirstName() + " " + + order.getCustomer().getLastName() + ", thanks for your order. " + + "Your order number is " + order.getOrderNumber() + "."); + } + }; + + try { + this.mailSender.send(preparator); + } + catch (MailException ex) { + // simply log it and go on... + System.err.println(ex.getMessage()); + } + } + +} +``` + +| |邮件代码是一个横切关注点,很可能是<br/>重构为[custom Spring AOP aspect](core.html#aop)的候选项,然后在<br/>目标上的适当接入点上运行`OrderManager`。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +Spring 框架的邮件支持附带标准的 JavaMail 实现。有关更多信息,请参见相关的 Javadoc。 + +### 6.2.使用 JavaMail`MimeMessageHelper` + +在处理 JavaMail 消息时,一个非常有用的类是 `org.springframework.mail.javamail.mimeMessageHelper’,它使你不必使用冗长的 JavaMail API。使用`MimeMessageHelper`,很容易创建`MimeMessage`,如下例所示: + +``` +// of course you would use DI in any real-world cases +JavaMailSenderImpl sender = new JavaMailSenderImpl(); +sender.setHost("mail.host.com"); + +MimeMessage message = sender.createMimeMessage(); +MimeMessageHelper helper = new MimeMessageHelper(message); +helper.setTo("[email protected]"); +helper.setText("Thank you for ordering!"); + +sender.send(message); +``` + +#### 6.2.1.发送附件和内联资源 + +多部分电子邮件消息允许使用附件和内联资源。内联资源的示例包括希望在消息中使用但不希望显示为附件的图像或样式表。 + +##### 附件 + +下面的示例向你展示了如何使用`MimeMessageHelper`发送带有单个 JPEG 图像附件的电子邮件: + +``` +JavaMailSenderImpl sender = new JavaMailSenderImpl(); +sender.setHost("mail.host.com"); + +MimeMessage message = sender.createMimeMessage(); + +// use the true flag to indicate you need a multipart message +MimeMessageHelper helper = new MimeMessageHelper(message, true); +helper.setTo("[email protected]"); + +helper.setText("Check out this image!"); + +// let's attach the infamous windows Sample file (this time copied to c:/) +FileSystemResource file = new FileSystemResource(new File("c:/Sample.jpg")); +helper.addAttachment("CoolImage.jpg", file); + +sender.send(message); +``` + +##### 内联资源 + +下面的示例向你展示了如何使用`MimeMessageHelper`发送带有内联图像的电子邮件: + +``` +JavaMailSenderImpl sender = new JavaMailSenderImpl(); +sender.setHost("mail.host.com"); + +MimeMessage message = sender.createMimeMessage(); + +// use the true flag to indicate you need a multipart message +MimeMessageHelper helper = new MimeMessageHelper(message, true); +helper.setTo("[email protected]"); + +// use the true flag to indicate the text included is HTML +helper.setText("<html><body><img src='cid:identifier1234'></body></html>", true); + +// let's include the infamous windows Sample file (this time copied to c:/) +FileSystemResource res = new FileSystemResource(new File("c:/Sample.jpg")); +helper.addInline("identifier1234", res); + +sender.send(message); +``` + +| |通过使用指定的`Content-ID`(在上面的示例中为 `identifier1234’),将内联资源添加到`MimeMessage`中。添加文本<br/>的顺序和资源是非常重要的。一定要先添加文本,然后`MBeanServerConnection`资源。如果你是在做相反的方式,它是不起作用的。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 6.2.2.使用模板库创建电子邮件内容 + +前面几节所示示例中的代码通过使用`message.setText(..)`等方法调用,显式地创建了电子邮件消息的内容。这对于简单的情况很好,在前面提到的示例的上下文中也是可以的,其目的是向你展示 API 的基本知识。 + +然而,在典型的 Enterprise 应用程序中,开发人员通常不会使用前面所示的方法来创建电子邮件消息的内容,原因有很多: + +* 在 Java 代码中创建基于 HTML 的电子邮件内容是乏味且容易出错的。 + +* 显示逻辑和业务逻辑之间没有明确的区分。 + +* 更改电子邮件内容的显示结构需要编写 Java 代码、重新编译、重新部署等等。 + +通常,解决这些问题的方法是使用模板库(例如 Freemarker)来定义电子邮件内容的显示结构。这使得你的代码只负责创建要在电子邮件模板中呈现的数据并发送电子邮件。当你的电子邮件内容变得相当复杂时,这绝对是一种最佳实践,而且,有了 Spring 框架对 Freemarker 的支持类,这变得非常容易做到。 + +## 7. 任务执行和调度 + +Spring 框架提供了用于分别使用`TaskExecutor`和`TaskScheduler`接口的任务的异步执行和调度的抽象。 Spring 还具有在应用服务器环境中支持线程池或委托给 CommonJ 的那些接口的实现的特征。最终,在公共接口后面使用这些实现,可以抽象出 Java SE5、Java SE6 和 Java EE 环境之间的差异。 + +Spring 还具有集成类以支持与`Timer`(自 1.3 起的 JDK 的一部分)和 Quartz 调度器([https://www.quartz-scheduler.org/](https://www.quartz-scheduler.org/))的调度。你可以通过使用`FactoryBean`和可选引用“计时器”或`Trigger`实例来分别设置这两个调度程序。此外,Quartz 调度器和`Timer`都有一个方便的类,它允许你调用现有目标对象的方法(类似于正常的`MethodInvokingFactoryBean`操作)。 + +### 7.1. Spring `TaskExecutor`抽象 + +执行器是线程池概念的 JDK 名称。之所以命名为“executor”,是因为不能保证底层实现实际上是一个池。执行器可以是单线程的,甚至可以是同步的。 Spring 的抽象隐藏了 Java SE 和 Java EE 环境之间的实现细节。 + +Spring 的`TaskExecutor`接口与`java.util.concurrent.Executor`接口相同。实际上,最初,它存在的主要原因是在使用线程池时抽象出对 Java5 的需求。该接口只有一个方法(“execute(Runnable Task)”),该方法根据线程池的语义和配置接受要执行的任务。 + +创建`TaskExecutor`最初是为了在需要时为其他 Spring 组件提供一个用于线程池的抽象。诸如`ApplicationEventMulticaster`、JMS 的`AbstractMessageListenerContainer`和 Quartz Integration 等组件都使用 `taskexecutor’抽象来池线程。但是,如果你的 bean 需要线程池行为,你也可以根据自己的需要使用此抽象。 + +#### 7.1.1.`TaskExecutor`类型 + +Spring 包括许多预构建`TaskExecutor`的实现方式。十有八九,你应该永远不需要实现你自己的。 Spring 提供的备选案文如下: + +* `SyncTaskExecutor`:此实现不异步运行调用。相反,每次调用都发生在调用线程中。它主要用于不需要多线程的情况,例如在简单的测试用例中。 + +* `SimpleAsyncTaskExecutor`:此实现不重用任何线程。相反,它为每个调用启动一个新线程。但是,它确实支持一个并发限制,该限制可以阻止任何超出限制的调用,直到释放了一个插槽。如果你正在寻找真正的池,请参阅下面的列表中的`ThreadPoolTaskExecutor`。 + +* `ConcurrentTaskExecutor`:此实现是用于`java.util.concurrent.Executor`实例的适配器。有一种替代方法将`Executor`配置参数公开为 Bean 属性。很少需要直接使用“concurrenttaskexecutor”。但是,如果`ThreadPoolTaskExecutor`不够灵活以满足你的需要,`ConcurrentTaskExecutor`是一种替代方案。 + +* `ThreadPoolTaskExecutor`:这种实现是最常用的。它公开用于配置`java.util.concurrent.ThreadPoolExecutor`的 Bean 属性,并将其包装在`TaskExecutor`中。如果你需要适应不同类型的`java.util.concurrent.Executor`,我们建议你使用`ConcurrentTaskExecutor`代替。 + +* `WorkManagerTaskExecutor`:该实现使用 CommonJ`WorkManager`作为其支持服务提供者,并且是在 Spring 应用程序上下文中在 WebLogic 或 WebSphere 上设置基于 CommonJ 的线程池集成的中心便利类。 + +* `DefaultManagedTaskExecutor`:此实现在 JSR-236 兼容的运行时环境(例如 Java EE7+ 应用程序服务器)中使用 JNDI 获得的`ManagedExecutorService`,为此替换 CommonJ WorkManager。 + +#### 7.1.2.使用`TaskExecutor` + +Spring 的`TaskExecutor`实现被用作简单的 JavaBean。在下面的示例中,我们定义了一个 Bean,它使用`ThreadPoolTaskExecutor`异步打印出一组消息: + +``` +import org.springframework.core.task.TaskExecutor; + +public class TaskExecutorExample { + + private class MessagePrinterTask implements Runnable { + + private String message; + + public MessagePrinterTask(String message) { + this.message = message; + } + + public void run() { + System.out.println(message); + } + } + + private TaskExecutor taskExecutor; + + public TaskExecutorExample(TaskExecutor taskExecutor) { + this.taskExecutor = taskExecutor; + } + + public void printMessages() { + for(int i = 0; i < 25; i++) { + taskExecutor.execute(new MessagePrinterTask("Message" + i)); + } + } +} +``` + +正如你所看到的,不是从池中检索线程并自己执行它,而是将`Runnable`添加到队列中。然后`TaskExecutor`使用其内部规则来决定何时运行任务。 + +为了配置`TaskExecutor`使用的规则,我们公开了简单的 Bean 属性: + +``` +<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor"> + <property name="corePoolSize" value="5"/> + <property name="maxPoolSize" value="10"/> + <property name="queueCapacity" value="25"/> +</bean> + +<bean id="taskExecutorExample" class="TaskExecutorExample"> + <constructor-arg ref="taskExecutor"/> +</bean> +``` + +### 7.2. Spring `TaskScheduler`抽象 + +除了`TaskExecutor`的抽象之外, Spring 3.0 还引入了`TaskScheduler`的各种方法,用于调度在将来的某个时刻运行的任务。下面的清单显示了`TaskScheduler`接口定义: + +``` +public interface TaskScheduler { + + ScheduledFuture schedule(Runnable task, Trigger trigger); + + ScheduledFuture schedule(Runnable task, Instant startTime); + + ScheduledFuture schedule(Runnable task, Date startTime); + + ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period); + + ScheduledFuture scheduleAtFixedRate(Runnable task, Date startTime, long period); + + ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period); + + ScheduledFuture scheduleAtFixedRate(Runnable task, long period); + + ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay); + + ScheduledFuture scheduleWithFixedDelay(Runnable task, Date startTime, long delay); + + ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay); + + ScheduledFuture scheduleWithFixedDelay(Runnable task, long delay); +} +``` + +最简单的方法是一个名为`schedule`的方法,它只需要一个`Runnable`和一个`Date`。这会导致任务在指定的时间之后运行一次。所有其他方法都能够调度任务以重复运行。固定速率和固定延迟方法用于简单的周期性执行,但是接受`Trigger`的方法要灵活得多。 + +#### 7.2.1.`Trigger`接口 + +`Trigger`接口本质上是受 JSR-236 的启发,该接口在 Spring 3.0 时尚未正式实现。`Trigger`的基本思想是,执行时间可以基于过去的执行结果或甚至任意的条件来确定。如果这些确定确实考虑了前面执行的结果,则该信息在`TriggerContext`中可用。`Trigger`接口本身非常简单,如下所示: + +``` +public interface Trigger { + + Date nextExecutionTime(TriggerContext triggerContext); +} +``` + +`TriggerContext`是最重要的部分。它封装了所有相关的数据,如果有必要,将来还可以进行扩展。triggerContext 是一个接口(默认情况下使用`SimpleTriggerContext`实现)。下面的清单显示了`Trigger`实现的可用方法。 + +``` +public interface TriggerContext { + + Date lastScheduledExecutionTime(); + + Date lastActualExecutionTime(); + + Date lastCompletionTime(); +} +``` + +#### 7.2.2.`Trigger`实现 + +Spring 提供了`Trigger`接口的两种实现方式。最有趣的是`CronTrigger`。它支持基于[CRON 表达式](#scheduling-cron-expression)的任务调度。例如,以下任务被安排在每小时 15 分钟后运行,但仅在工作日的 9 到 5 个“营业时间”内运行: + +``` +scheduler.schedule(task, new CronTrigger("0 15 9-17 * * MON-FRI")); +``` + +另一种实现是`PeriodicTrigger`,它接受一个固定的周期、一个可选的初始延迟值和一个布尔值,以指示该周期应该被解释为固定速率还是固定延迟。由于`TaskScheduler`接口已经定义了以固定速率或固定延迟调度任务的方法,因此只要有可能,就应该直接使用这些方法。“periodictrigger”实现的价值在于,你可以在依赖`Trigger`抽象的组件中使用它。例如,可以方便地允许周期性触发器、基于 CRON 的触发器、甚至定制的触发器实现可互换地使用。这样的组件可以利用依赖注入,这样你就可以在外部配置这样的`Triggers`,因此,可以轻松地修改或扩展它们。 + +#### 7.2.3.`TaskScheduler`实现 + +与 Spring 的`TaskExecutor`抽象一样,`TaskScheduler`安排的主要好处是,应用程序的调度需求与部署环境分离。当部署到不应由应用程序本身直接创建线程的应用程序服务器环境时,这种抽象级别特别相关。对于这样的场景, Spring 提供了一个`TimerManagerTaskScheduler`,它在 WebLogic 或 WebSphere 上委托给 Commonj`TimerManager`,以及一个更新的 `DefaultManagedTaskScheduler’,它在 Java EE7+ 环境中委托给一个 JSR-236<gtr="1398"/>。两者通常都配置了 JNDI 查找。 + +每当不需要外部线程管理时,一种更简单的选择是在应用程序内进行本地`ScheduledExecutorService`设置,可以通过 Spring 的`ConcurrentTaskScheduler`进行调整。 Spring 还提供了一个“ThreadPoolTaskScheduler”,它在内部委托给`ScheduledExecutorService`,以便沿着`ThreadPoolTaskExecutor`的行提供公共 Bean 样式的配置。这些变体在宽松的应用程序服务器环境中(尤其是在 Tomcat 和 Jetty 上)对于本地嵌入线程池设置也非常适合。 + +### 7.3.对调度和异步执行的注释支持 + +Spring 为任务调度和异步方法执行提供注释支持。 + +#### 7.3.1.启用调度注释 + +要启用对`@Scheduled`和`@Async`注释的支持,你可以在你的`@EnableScheduling`类中添加`@EnableScheduling`和 `@enableasync`,如下例所示: + +``` +@Configuration +@EnableAsync +@EnableScheduling +public class AppConfig { +} +``` + +你可以为你的应用程序选择相关的注释。例如,如果只需要支持`@Scheduled`,则可以省略`@EnableAsync`。对于更细粒度的控件,你可以另外实现`SchedulingConfigurer`接口、`AsyncConfigurer`接口,或者两者兼而有之。有关详细信息,请参见[“调度配置器”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/scheduling/annotation/SchedulingConfigurer.html)和[“AsyncConfigurer”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/scheduling/annotation/AsyncConfigurer.html)Javadoc。 + +如果你更喜欢 XML 配置,那么可以使用`<task:annotation-driven>`元素,如下例所示: + +``` +<task:annotation-driven executor="myExecutor" scheduler="myScheduler"/> +<task:executor id="myExecutor" pool-size="5"/> +<task:scheduler id="myScheduler" pool-size="10"/> +``` + +注意,对于前面的 XML,提供了一个 Executor 引用来处理那些与`@Async`注释的方法对应的任务,并且提供了一个 Scheduler 引用来管理那些用`@Scheduled`注释的方法。 + +| |用于处理`@Async`注释的默认通知模式是`proxy`,它仅允许<br/>通过代理拦截调用。同一类<br/>中的本地调用不能以这种方式被拦截。对于更高级的拦截模式,可以考虑将<br/>转换为`aspectj`模式,并结合编译时或加载时编织。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 7.3.2.`@Scheduled`注释 + +你可以将`@Scheduled`注释以及触发器元数据添加到方法中。例如,以下方法每 5 秒(5000 毫秒)以固定的延迟被调用一次,这意味着该周期是从之前每次调用的完成时间开始计算的。 + +``` +@Scheduled(fixedDelay = 5000) +public void doSomething() { + // something that should run periodically +} +``` + +| |默认情况下,毫秒将被用作固定延迟、固定速率和<br/>初始延迟值的时间单位。如果你想使用一个不同的时间单位,例如秒或<br/>分钟,你可以通过`timeUnit`中的<gt r=“1424”属性来配置它。<br/>例如,前面的示例也可以写成如下。<br/><br/>``<br/>预定的(fixeddelay=5,timeUnit=timeUnit.seconds)<br/>public void dosomething(){<br/>//应该周期性运行的东西<br/>}<br/>```| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +如果需要固定速率执行,可以在注释中使用`fixedRate`属性。以下方法每五秒调用一次(在每次调用的连续启动时间之间进行度量)。 + +``` +@Scheduled(fixedRate = 5, timeUnit = TimeUnit.SECONDS) +public void doSomething() { + // something that should run periodically +} +``` + +对于固定延迟和固定速率任务,你可以通过指示在第一次执行该方法之前等待的时间量来指定初始延迟,如下面的“fixedrate”示例所示。 + +``` +@Scheduled(initialDelay = 1000, fixedRate = 5000) +public void doSomething() { + // something that should run periodically +} +``` + +如果简单的周期性调度不够表达,则可以提供[cron expression](#scheduling-cron-expression)。以下示例仅在工作日运行: + +``` +@Scheduled(cron="*/5 * * * * MON-FRI") +public void doSomething() { + // something that should run on weekdays only +} +``` + +| |还可以使用`zone`属性指定解析 CRON<br/>表达式的时区。| +|---|------------------------------------------------------------------------------------------------------------| + +请注意,要调度的方法必须具有 void 返回,并且不能接受任何参数。如果方法需要与来自应用程序上下文的其他对象交互,那么这些对象通常是通过依赖注入提供的。 + +| |从 Spring Framework4.3 开始,`@Scheduled`方法在任何作用域的 bean 上都是受支持的。<br/><br/>请确保你不是在运行时初始化同一个`@Scheduled`注释类的多个实例,除非你确实希望将回调安排到每个这样的<br/>实例。与此相关,请确保在 Bean <br/>上不使用`@Configurable`用`@Scheduled`注释并在容器中注册为常规 Spring bean<br/>的类。否则,你将获得双重初始化(一次通过<br/>容器,一次通过`@Configurable`方面),其结果是每个 `@schedule’方法被调用两次。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 7.3.3.`@Async`注释 + +你可以在方法上提供`@Async`注释,以便异步地调用该方法。换句话说,调用者在调用后立即返回,而方法的实际执行发生在已提交给 Spring `TaskExecutor`的任务中。在最简单的情况下,你可以将注释应用于返回`void`的方法,如下例所示: + +``` +@Async +void doSomething() { + // this will be run asynchronously +} +``` + +与使用`@Scheduled`注释的方法不同,这些方法可以预期参数,因为它们在运行时由调用者以“正常”方式调用,而不是从容器管理的计划任务中调用。例如,以下代码是`@Async`注释的合法应用程序: + +``` +@Async +void doSomething(String s) { + // this will be run asynchronously +} +``` + +甚至返回值的方法也可以异步调用。但是,这样的方法需要具有`Future`类型的返回值。这仍然提供了异步执行的好处,以便调用者可以在`Future`上调用 `get()’之前执行其他任务。下面的示例展示了如何在返回值的方法上使用`@Async`: + +``` +@Async +Future<String> returnSomething(int i) { + // this will be run asynchronously +} +``` + +| |`@Async`方法不仅可以声明一个常规的`java.util.concurrent.Future`返回类型<br/>,还可以声明 Spring 的`org.springframework.util.concurrent.ListenableFuture`或者,如 Spring <br/>4.2,JDK8 的`java.util.concurrent.CompletableFuture`,用于与<br/>异步任务进行更丰富的交互,并用于与进一步的处理步骤立即复合。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +不能将`@Async`与“@postConstruct”之类的生命周期回调结合使用。要异步初始化 Spring bean,你目前必须使用一个单独的初始化 Spring Bean,然后调用目标上的`@Async`注释方法,如下例所示: + +``` +public class SampleBeanImpl implements SampleBean { + + @Async + void doSomething() { + // ... + } + +} + +public class SampleBeanInitializer { + + private final SampleBean bean; + + public SampleBeanInitializer(SampleBean bean) { + this.bean = bean; + } + + @PostConstruct + public void initialize() { + bean.doSomething(); + } + +} +``` + +| |对于`@Async`没有直接的 XML 等价物,因为这样的方法首先应该为异步执行而设计<br/>,而不是在外部重新声明为异步。<br/>但是,你可以手动设置 Spring 的`AsyncExecutionInterceptor`与 Spring AOP,<br/>结合自定义切入点。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 7.3.4.遗嘱执行人资格(`@Async`) + +默认情况下,当在方法上指定`@Async`时,使用的执行器是[启用异步支持时进行配置](#scheduling-enable-annotation-support),即“注释驱动”元素(如果你使用 XML 或`AsyncConfigurer`实现)。但是,当需要指示在执行给定方法时应该使用默认值以外的执行器时,可以使用`value`注释的`@Async`属性。下面的示例展示了如何做到这一点: + +``` +@Async("otherExecutor") +void doSomething(String s) { + // this will be run asynchronously by "otherExecutor" +} +``` + +在这种情况下,`"otherExecutor"`可以是 Spring 容器中任何`Executor` Bean 的名称,也可以是与任何`Executor`相关联的限定符的名称(例如,与`<qualifier>`元素或 Spring 的`@Qualifier`注释一起指定)。 + +#### 7.3.5.异常管理与`@Async` + +当`@Async`方法具有`Future`类型的返回值时,很容易管理在方法执行期间抛出的异常,因为在`Future`结果上调用`get`时将抛出该异常。但是,对于`void`返回类型,异常是未捕获的,因此无法传输。你可以提供一个“AsyncuncaughtExceptionHandler”来处理此类异常。下面的示例展示了如何做到这一点: + +``` +public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler { + + @Override + public void handleUncaughtException(Throwable ex, Method method, Object... params) { + // handle exception + } +} +``` + +默认情况下,异常只会被记录。你可以使用`AsyncConfigurer`或`<task:annotation-driven/>`XML 元素来定义自定义`AsyncUncaughtExceptionHandler`。 + +### 7.4.`task`名称空间 + +在版本 3.0 中, Spring 包括一个用于配置`TaskExecutor`和 `taskscheduler’实例的 XML 命名空间。它还提供了一种方便的方式来配置要用触发器调度的任务。 + +#### 7.4.1.“调度程序”元素 + +以下元素创建具有指定线程池大小的`ThreadPoolTaskScheduler`实例: + +``` +<task:scheduler id="scheduler" pool-size="10"/> +``` + +为`id`属性提供的值被用作池中线程名称的前缀。`scheduler`元素相对简单。如果不提供`pool-size`属性,则默认线程池只有一个线程。调度程序没有其他配置选项。 + +#### 7.4.2.`executor`元素 + +下面创建一个`ThreadPoolTaskExecutor`实例: + +``` +<task:executor id="executor" pool-size="10"/> +``` + +与[上一节](#scheduling-task-namespace-scheduler)中显示的调度程序一样,为`id`属性提供的值被用作池中线程名称的前缀。就池大小而言,`executor`元素比`scheduler`元素支持更多的配置选项。首先,`ThreadPoolTaskExecutor`的线程池本身更可配置。执行器的线程池可以为核心和最大大小提供不同的值,而不是只有一个大小。如果你只提供一个值,那么执行器有一个固定大小的线程池(核心和最大大小是相同的)。但是,`executor`元素的`pool-size`属性也接受`min-max`形式的范围。以下示例设置的最小值为 `5’,最大值为`25`: + +``` +<task:executor + id="executorWithPoolSizeRange" + pool-size="5-25" + queue-capacity="100"/> +``` + +在前面的配置中,还提供了一个`queue-capacity`值。线程池的配置也应该根据执行器的队列容量来考虑。有关池大小和队列容量之间关系的完整说明,请参见[“Threadpoolexecutor”](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ThreadPoolExecutor.html)的文档。其主要思想是,当提交任务时,如果当前活动线程的数量小于核心大小,则执行器首先尝试使用空闲线程。如果已达到核心大小,则将该任务添加到队列中,只要该任务的容量尚未达到。只有这样,如果队列的容量已经达到,执行器才会创建超出核心大小的新线程。如果也达到了最大大小,那么执行器将拒绝该任务。 + +默认情况下,队列是无界的,但这很少是所需的配置,因为如果在所有池线程都忙的时候向队列中添加了足够多的任务,则可能导致`OutOfMemoryErrors`。此外,如果队列是无界的,则最大大小完全没有影响。由于执行器总是在创建超出核心大小的新线程之前尝试队列,因此队列必须具有有限的容量,以使线程池的容量超出核心大小(这就是为什么在使用无界队列时,固定大小的线程池是唯一明智的情况)。 + +考虑一下上面提到的当任务被拒绝时的情况。默认情况下,当任务被拒绝时,线程池执行器抛出一个`TaskRejectedException`。然而,拒绝策略实际上是可配置的。当使用默认的拒绝策略(即`AbortPolicy`实现)时,将引发异常。对于在重负载下可以跳过某些任务的应用程序,可以配置`DiscardPolicy`或`DiscardOldestPolicy`。对于需要在重负载下控制提交任务的应用程序,另一个很好的选项是`CallerRunsPolicy`。该策略强制调用提交方法的线程运行任务本身,而不是抛出异常或丢弃任务。其思想是,这样的调用者在运行该任务时很忙,无法立即提交其他任务。因此,它提供了一种简单的方法来限制传入的负载,同时保持线程池和队列的限制。通常,这允许执行器“赶上”它正在处理的任务,从而释放队列中、池中或两者中的一些容量。你可以从`executor`元素上`rejection-policy`属性可用的值的枚举中选择这些选项中的任何一个。 + +下面的示例显示了一个`executor`元素,该元素具有用于指定各种行为的多个属性: + +``` +<task:executor + id="executorWithCallerRunsPolicy" + pool-size="5-25" + queue-capacity="100" + rejection-policy="CALLER_RUNS"/> +``` + +最后,`keep-alive`设置决定了线程在停止之前可以保持空闲的时间限制(以秒为单位)。如果池中的线程数量超过了当前的核心数量,那么在不处理任务的情况下等待了这么长的时间之后,多余的线程将被停止。时间值为零会导致多余的线程在执行任务后立即停止,而不会在任务队列中保留后续工作。下面的示例将`keep-alive`值设置为两分钟: + +``` +<task:executor + id="executorWithKeepAlive" + pool-size="5-25" + keep-alive="120"/> +``` + +#### 7.4.3.“计划任务”元素 + +Spring 的任务命名空间最强大的功能是支持配置要在 Spring 应用程序上下文中调度的任务。这遵循了类似于 Spring 中的其他“方法调用程序”的方法,例如由 JMS 名称空间提供的用于配置消息驱动的 POJO 的方法。基本上,`ref`属性可以指向任何 Spring 管理的对象,而`method`属性提供了要在该对象上调用的方法的名称。下面的清单展示了一个简单的示例: + +``` +<task:scheduled-tasks scheduler="myScheduler"> + <task:scheduled ref="beanA" method="methodA" fixed-delay="5000"/> +</task:scheduled-tasks> + +<task:scheduler id="myScheduler" pool-size="10"/> +``` + +调度程序由外部元素引用,每个单独的任务都包括其触发器元数据的配置。在前面的示例中,该元数据定义了一个具有固定延迟的周期性触发器,该延迟指示每个任务执行完成后要等待的毫秒数。另一个选项是“fixed-rate”,表示无论之前的执行需要多长时间,该方法应该运行多长时间。此外,对于`fixed-delay`和`fixed-rate`任务,你都可以指定一个“initial-delay”参数,该参数指示在第一次执行该方法之前需要等待的毫秒数。对于更多的控制,你可以提供`cron`属性来提供[cron expression](#scheduling-cron-expression)。下面的示例展示了这些其他选项: + +``` +<task:scheduled-tasks scheduler="myScheduler"> + <task:scheduled ref="beanA" method="methodA" fixed-delay="5000" initial-delay="1000"/> + <task:scheduled ref="beanB" method="methodB" fixed-rate="5000"/> + <task:scheduled ref="beanC" method="methodC" cron="*/5 * * * * MON-FRI"/> +</task:scheduled-tasks> + +<task:scheduler id="myScheduler" pool-size="10"/> +``` + +### 7.5.CRON 表达式 + +Spring 所有 CRON 表达式都必须符合相同的格式,无论你是在[“@ 已排定的”注释](#scheduling-annotation-support-scheduled)、[“任务:排定的任务”要素](#scheduling-task-namespace-scheduled-tasks)中使用它们,还是在其他地方使用它们。格式良好的 CRON 表达式,如`* * * * * *`,由六个以空间分隔的时间和日期字段组成,每个字段都有自己的有效值范围: + +``` + ┌───────────── second (0-59) + │ ┌───────────── minute (0 - 59) + │ │ ┌───────────── hour (0 - 23) + │ │ │ ┌───────────── day of the month (1 - 31) + │ │ │ │ ┌───────────── month (1 - 12) (or JAN-DEC) + │ │ │ │ │ ┌───────────── day of the week (0 - 7) + │ │ │ │ │ │ (0 or 7 is Sunday, or MON-SUN) + │ │ │ │ │ │ + * * * * * * +``` + +有一些规则是适用的: + +* 一个域可能是一个星号,它总是代表“first-last”。对于月中日或周中日字段,可以使用问号(“?”)来代替星号。 + +* 逗号是用来分隔列表中的项目的。 + +* 用连字符分隔的两个数字表示一系列数字。指定的范围是包含的。 + +* 在`/`的范围(或`*`)之后,用`/`指定该数字的值在该范围内的间隔时间。 + +* 英文名称也可以用于月日和周日域。使用特定日期或月份的前三个字母(大小写无关紧要)。 + +* 月中日和周中日字段可以包含`L`字符,这具有不同的含义 + + * 在 day-of-month 字段中,`L`表示*这个月的最后一天*。如果后接一个负偏移量(即`L-n`),则表示*这个月的最后一天*。 + + * 在一周一天的字段中,`L`代表*一周的最后一天*。如果前缀是数字或三个字母的名称(`dl’或`DDDL`),则表示*the last day of week (`d`or `DDD`) in the month*。 + +* Day-of-Month 字段可以是`nW`,它代表*the nearest weekday to day of the month `n`*。如果`n`在周六下跌,这将产生它之前的周五。如果`n`在周日下跌,这将产生之后的星期一,如果`n`是`1`并在星期六下跌,也会发生这种情况(即:`1W`代表*这个月的第一个工作日。*)。 + +* 如果月中日字段是`LW`,则表示*这个月的最后一个工作日*。 + +* 一周一天的字段可以是`d#n`(或`DDD#n`),代表*the `n`th day of week `d`(or `DDD`) in the month*。 + +以下是一些例子: + +| Cron Expression |意义| +|----------------------|-------------------------------------------------| +| `0 0 * * * *` |每天的每个小时都是最重要的| +| `*/10 * * * * *` |每十秒钟| +| `0 0 8-10 * * *` |每天 8 点、9 点和 10 点| +| `0 0 6,19 * * *` |每天早上 6 点和晚上 7 点| +| `0 0/30 8-10 * * *` |每天 8:00、8:30、9:00、9:30、10:00 和 10:30| +|`0 0 9-17 * * MON-FRI`|工作日朝九晚五的时候| +| `0 0 0 25 DEC ?` |每个圣诞节的午夜| +| `0 0 0 L * *` |每月的最后一天午夜| +| `0 0 0 L-3 * *` |本月倒数第三天午夜| +| `0 0 0 * * 5L` |每月的最后一个星期五午夜| +| `0 0 0 * * THUL` |每月的最后一个星期四午夜| +| `0 0 0 1W * *` |每月的第一个工作日午夜| +| `0 0 0 LW * *` |每月最后一个工作日的午夜| +| `0 0 0 ? * 5#2` |这个月的第二个星期五午夜。| +| `0 0 0 ? * MON#1` |这个月的第一个星期一午夜。| + +#### 7.5.1.宏 + +对于人类来说,`0 0 * * * *`这样的表达式很难解析,因此在出现错误的情况下很难修复。 Spring 为了提高可读性,支持以下宏,它们表示常用的序列。你可以使用这些宏而不是六位数的值,因此:`@Scheduled(cron = "@hourly")`。 + +| Macro |意义| +|--------------------------|------------------------------| +|`@yearly` (or `@annually`)|一年一次(`00101*`)| +| `@monthly` |每月一次(`0001* *`)| +| `@weekly` |每周一次(`00* *0`)| +|`@daily` (or `@midnight`) |一天一次(`00* **`),或| +| `@hourly` |一小时一次,(`0* ** *`)| + +### 7.6.使用 Quartz 调度器 + +Quartz 使用`Trigger`、`Job`、`JobDetail`对象来实现对各类作业的调度。有关 Quartz 背后的基本概念,请参见[https://www.quartz-scheduler.org/](https://www.quartz-scheduler.org/)。出于方便的目的, Spring 提供了几个类,这些类简化了基于 Spring 的应用程序中使用 Quartz 的过程。 + +#### 7.6.1.使用`JobDetailFactoryBean` + +Quartz`JobDetail`对象包含运行作业所需的所有信息。 Spring 提供了“JobDetailFactoryBean”,它为 XML 配置目的提供了 Bean 样式的属性。考虑以下示例: + +``` +<bean name="exampleJob" class="org.springframework.scheduling.quartz.JobDetailFactoryBean"> + <property name="jobClass" value="example.ExampleJob"/> + <property name="jobDataAsMap"> + <map> + <entry key="timeout" value="5"/> + </map> + </property> +</bean> +``` + +作业细节配置具有运行作业所需的所有信息(“examplejob”)。超时在作业数据图中指定。作业数据映射可以通过“JobExecutionContext”(在执行时传递给你)获得,但是`JobDetail`还可以从映射到作业实例的属性的作业数据获取其属性。因此,在下面的示例中,`ExampleJob`包含一个名为`timeout`的 Bean 属性,而`JobDetail`已自动应用它: + +``` +package example; + +public class ExampleJob extends QuartzJobBean { + + private int timeout; + + /** + * Setter called after the ExampleJob is instantiated + * with the value from the JobDetailFactoryBean (5) + */ + public void setTimeout(int timeout) { + this.timeout = timeout; + } + + protected void executeInternal(JobExecutionContext ctx) throws JobExecutionException { + // do the actual work + } +} +``` + +你也可以使用作业数据图中的所有附加属性。 + +| |通过使用`name`和`group`属性,可以分别修改作业的名称和组<br/>。默认情况下,作业的名称与`JobDetailFactoryBean`的 Bean 名称<br/>相匹配(在上面的示例中为 `examplejob’)。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 7.6.2.使用`MethodInvokingJobDetailFactoryBean` + +通常,你只需要在特定对象上调用一个方法。通过使用“MethodinkingJobDetailFactoryBean”,你可以做到这一点,如下例所示: + +``` +<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean"> + <property name="targetObject" ref="exampleBusinessObject"/> + <property name="targetMethod" value="doIt"/> +</bean> +``` + +前面的示例导致`doIt`方法在 `ExampleBusinessObject’方法上被调用,如下例所示: + +``` +public class ExampleBusinessObject { + + // properties and collaborators + + public void doIt() { + // do the actual work + } +} +``` + +``` +<bean id="exampleBusinessObject" class="examples.ExampleBusinessObject"/> +``` + +通过使用`MethodInvokingJobDetailFactoryBean`,你不需要创建仅调用方法的单行作业。你只需要创建实际的业务对象并连接详细的对象。 + +默认情况下,Quartz 作业是无状态的,这导致了作业相互干扰的可能性。如果为同一个`JobDetail`指定两个触发器,则可能在第一个作业完成之前,第二个作业就开始了。如果“JobDetail”类实现`Stateful`接口,则不会发生这种情况。第二项工作在第一项工作完成之前不会开始。要使“MethodinkingJobDetailFactoryBean”生成的作业非并发,请将<gtr="1590"/>标志设置为“false”,如下例所示: + +``` +<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean"> + <property name="targetObject" ref="exampleBusinessObject"/> + <property name="targetMethod" value="doIt"/> + <property name="concurrent" value="false"/> +</bean> +``` + +| |默认情况下,作业将以并发方式运行。| +|---|--------------------------------------------------| + +#### 7.6.3.使用触发器和`SchedulerFactoryBean`连接作业 + +我们创造了工作细节和工作岗位。 Bean 我们还介绍了使你能够在特定对象上调用方法的便利性。当然,我们仍然需要自己安排工作。这是通过使用触发器和`SchedulerFactoryBean`来完成的。Quartz 中有几个触发器可用, Spring 提供了两个 Quartz`FactoryBean`实现,它们具有方便的默认值:`CronTriggerFactoryBean`和 `SimpleTriggerFactoryBean’。 + +需要对触发器进行计划。 Spring 提供了一个`SchedulerFactoryBean`,它公开了要设置为属性的触发器。`SchedulerFactoryBean`使用这些触发器来调度实际的作业。 + +下面的列表使用了`SimpleTriggerFactoryBean`和`CronTriggerFactoryBean`: + +``` +<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean"> + <!-- see the example of method invoking job above --> + <property name="jobDetail" ref="jobDetail"/> + <!-- 10 seconds --> + <property name="startDelay" value="10000"/> + <!-- repeat every 50 seconds --> + <property name="repeatInterval" value="50000"/> +</bean> + +<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean"> + <property name="jobDetail" ref="exampleJob"/> + <!-- run every morning at 6 AM --> + <property name="cronExpression" value="0 0 6 * * ?"/> +</bean> +``` + +前面的示例设置了两个触发器,一个是每 50 秒运行一次,启动延迟 10 秒,另一个是每天早上 6 点运行。要最终确定所有内容,我们需要设置“SchedulerFactoryBean”,如下例所示: + +``` +<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> + <property name="triggers"> + <list> + <ref bean="cronTrigger"/> + <ref bean="simpleTrigger"/> + </list> + </property> +</bean> +``` + +对于`SchedulerFactoryBean`,有更多的属性可用,例如作业详细信息使用的日历、用来自定义 Quartz 的属性,以及 Spring 提供的 JDBC 数据源。有关更多信息,请参见[“SchedulerFactoryBean”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/scheduling/quartz/SchedulerFactoryBean.html)Javadoc。 + +| |`SchedulerFactoryBean`还可以识别 Classpath 中的`quartz.properties`文件,<br/>基于石英属性键,就像常规的石英配置一样。请注意,许多“SchedulerFactoryBean”设置与属性文件中的常见石英设置交互;因此,不建议在这两个级别上指定值。例如,如果你打算依赖 Spring 提供的数据源,则不要设置<br/>一个“org.quartz.jobstore.class”属性,<br/>或指定一个`org.springframework.scheduling.quartz.LocalDataSourceJobStore`变体,其<br/>是标准`org.quartz.impl.jdbcjobstore.JobStoreTX`的完全替代。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## 8. 缓存抽象 + +自版本 3.1 以来, Spring 框架提供了对向现有 Spring 应用程序透明地添加缓存的支持。与[transaction](data-access.html#transaction)支持类似,缓存抽象允许一致地使用各种缓存解决方案,并且对代码的影响最小。 + +在 Spring Framework4.1 中,缓存抽象得到了显著扩展,支持[JSR-107 注释](#cache-jsr-107)和更多的自定义选项。 + +### 8.1.理解缓存抽象 + +缓存 VS 缓冲区 + +“缓冲”和“缓存”这两个词往往可以互换使用。然而,请注意,它们代表的是不同的东西。传统上,缓冲区是在快实体和慢实体之间作为数据的中间临时存储区。由于一方将不得不等待另一方(这会影响性能),缓冲区通过允许整个数据块同时移动而不是在小块中移动来缓解这种情况。数据只从缓冲区写入和读取一次。此外,至少有一方意识到缓冲区是可见的。 + +另一方面,根据定义,缓存是隐藏的,并且双方都不知道缓存的发生。它也提高了性能,但这是通过让相同的数据以快速的方式被多次读取来实现的。 + +你可以找到对缓冲区和缓存之间的差异的进一步解释[here](https://en.wikipedia.org/wiki/Cache_(computing)#the_difference_between_buffer_and_cache)。 + +在其核心部分,缓存抽象将缓存应用于 Java 方法,从而减少了基于缓存中可用信息的执行次数。也就是说,每次调用目标方法时,抽象都会应用一种缓存行为,该行为将检查该方法是否已经针对给定参数被调用。如果它已被调用,则将返回缓存的结果,而无需调用实际的方法。如果该方法未被调用,则调用该方法,并将结果缓存并返回给用户,这样,下一次调用该方法时,将返回缓存的结果。通过这种方式,对于给定的一组参数,昂贵的方法(不管是 CPU-还是 IO-bound)只能调用一次,并且结果可以重用,而无需再次实际调用该方法。缓存逻辑是透明地应用的,不会对调用者造成任何干扰。 + +| |这种方法仅适用于那些无论调用多少次都能保证为给定输入(或参数)返回相同的<br/>输出(结果)的方法。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +缓存抽象提供了其他与缓存相关的操作,例如更新缓存内容或删除一个或所有条目的能力。如果缓存处理的是在应用程序运行过程中可能发生变化的数据,那么这些就很有用。 + +与 Spring 框架中的其他服务一样,缓存服务是一种抽象(而不是缓存实现),需要使用实际存储来存储缓存数据——也就是说,该抽象使你不必编写缓存逻辑,但并不提供实际的数据存储。这个抽象是通过“org.springframework.cache.cache”和`org.springframework.cache.CacheManager`接口实现的。 + +Spring 提供了该抽象的[几个实现](#cache-store-configuration):基于 JDK`java.util.concurrent.ConcurrentMap`的缓存、[Ehcache 2.x](https://www.ehcache.org/)、Gemfire 缓存、[Caffeine](https://github.com/ben-manes/caffeine/wiki),以及与 JSR-107 兼容的缓存(例如 EHCache3.x)。有关插入其他缓存存储和提供程序的更多信息,请参见[插入不同的后端缓存](#cache-plug)。 + +| |对于多线程和<br/>多进程环境,缓存抽象没有特殊的处理,因为这些特性由缓存实现处理。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------| + +如果你有一个多进程环境(即部署在多个节点上的应用程序),则需要相应地配置你的缓存提供程序。根据你的用例,在几个节点上复制相同的数据就足够了。但是,如果在应用程序运行过程中更改了数据,则可能需要启用其他传播机制。 + +缓存特定的项目直接等同于在编程缓存交互中发现的典型的“如果没有找到,就继续进行并最终放置”的代码块。没有应用锁,并且多个线程可能会尝试并发加载相同的项。这同样适用于驱逐。如果多个线程试图同时更新或删除数据,则可能会使用过时的数据。某些缓存提供商在该领域提供了高级功能。有关更多详细信息,请参见缓存提供程序的文档。 + +要使用缓存抽象,你需要注意两个方面: + +* 缓存声明:确定需要缓存的方法及其策略。 + +* 缓存配置:存储数据并从中读取数据的备份缓存。 + +### 8.2.基于声明性注释的缓存 + +对于缓存声明, Spring 的缓存抽象提供了一组 Java 注释: + +* `@Cacheable`:触发缓存填充。 + +* `@CacheEvict`:触发缓存驱逐。 + +* `@CachePut`:在不干扰方法执行的情况下更新缓存。 + +* `@Caching`:重新组合要在方法上应用的多个缓存操作。 + +* `@CacheConfig`:在类级别共享一些常见的缓存相关设置。 + +#### 8.2.1.`@Cacheable`注释 + +顾名思义,你可以使用`@Cacheable`来划分可缓存的方法——即,将结果存储在缓存中的方法,以便在随后的调用中(使用相同的参数),将返回缓存中的值,而无需实际调用该方法。在最简单的形式中,注释声明需要与注释方法关联的缓存的名称,如下例所示: + +``` +@Cacheable("books") +public Book findBook(ISBN isbn) {...} +``` + +在前面的代码片段中,`findBook`方法与名为`books`的缓存相关联。每次调用该方法时,都会检查缓存,以查看调用是否已经运行,并且不需要重复调用。虽然在大多数情况下,只声明一个缓存,但该注释允许指定多个名称,以便使用多个缓存。在这种情况下,每个缓存都会在调用方法之前进行检查——如果至少有一个缓存被命中,则会返回相关值。 + +| |所有其他不包含该值的缓存也会被更新,即使<br/>缓存的方法实际上没有被调用。| +|---|--------------------------------------------------------------------------------------------------------------------------------| + +下面的示例在带有多个缓存的`findBook`方法上使用`@Cacheable`: + +``` +@Cacheable({"books", "isbns"}) +public Book findBook(ISBN isbn) {...} +``` + +##### 默认密钥生成 + +由于缓存本质上是键值存储,因此缓存方法的每次调用都需要转换为用于缓存访问的合适的键。缓存抽象使用基于以下算法的简单`KeyGenerator`: + +* 如果没有给出参数,则返回`SimpleKey.EMPTY`。 + +* 如果只给出一个参数,则返回该实例。 + +* 如果给出了一个以上的参数,则返回一个包含所有参数的`SimpleKey`。 + +只要参数具有自然键,并且实现有效的`hashCode()`和`equals()`方法,这种方法在大多数用例中都能很好地工作。如果不是这样,你就需要改变策略。 + +要提供不同的默认密钥生成器,你需要实现 `org.springframework.cache.interceptor.keygenerator` 接口。 + +| |Spring 4.0 版本的发布改变了默认的密钥生成策略。 Spring 的早期<br/>版本使用了一种密钥生成策略,对于多个密钥参数,<br/>只考虑参数的`hashCode()`,而不是`equals()`。这可能会导致<br/>意外的密钥冲突(有关背景信息,请参见[SPR-10237](https://jira.spring.io/browse/SPR-10237))。新的`SimpleKeyGenerator`在这种情况下使用复合键。<br/><br/>如果你想继续使用以前的键策略,可以配置不受欢迎的 `org.springframework.cache.interceptor.defaultkeygenerator’类或创建一个自定义的<br/>基于散列的`KeyGenerator`实现。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 自定义密钥生成声明 + +由于缓存是通用的,目标方法很可能具有各种签名,而这些签名不能很容易地映射到缓存结构的顶部。当目标方法有多个参数时,这一点往往变得很明显,其中只有一些参数适合缓存(而其余的参数仅由方法逻辑使用)。考虑以下示例: + +``` +@Cacheable("books") +public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) +``` + +乍一看,虽然两个`boolean`参数会影响找到这本书的方式,但它们对缓存没有用。此外,如果这两个因素中只有一个是重要的,而另一个不是重要的,那该怎么办? + +对于这种情况,`@Cacheable`注释允许你指定如何通过其`key`属性生成密钥。你可以使用[SpEL](core.html#expressions)来选择感兴趣的参数(或它们的嵌套属性),执行操作,甚至调用任意方法,而无需编写任何代码或实现任何接口。这是在[默认生成器](#cache-annotations-cacheable-default-key)上推荐的方法,因为随着代码库的增长,签名中的方法往往会有很大的不同。虽然默认策略可能对某些方法有效,但它很少对所有方法有效。 + +以下示例使用了各种 SPEL 声明(如果你不熟悉 SPEL,请帮自己一个忙,并阅读[Spring Expression Language](core.html#expressions)): + +``` +@Cacheable(cacheNames="books", key="#isbn") +public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) + +@Cacheable(cacheNames="books", key="#isbn.rawNumber") +public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) + +@Cacheable(cacheNames="books", key="T(someType).hash(#isbn)") +public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) +``` + +前面的代码片段显示了选择某个参数、它的一个属性,甚至是任意(静态)方法是多么容易。 + +如果负责生成密钥的算法过于具体,或者如果需要共享密钥,则可以在操作上定义自定义`keyGenerator`。为此,请指定要使用的`KeyGenerator` Bean 实现的名称,如下例所示: + +``` +@Cacheable(cacheNames="books", keyGenerator="myKeyGenerator") +public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) +``` + +| |`key`和`keyGenerator`参数是互斥的,指定这两个参数的操作<br/>会导致异常。| +|---|--------------------------------------------------------------------------------------------------------------------------------| + +##### 默认缓存分辨率 + +缓存抽象使用一个简单的`CacheResolver`,该抽象使用配置的“CacheManager”检索在操作级别定义的缓存。 + +要提供不同的默认缓存解析器,你需要实现 `org.SpringFramework.cache.Interceptor.CacheResolver’接口。 + +##### 自定义缓存分辨率 + +默认的缓存分辨率非常适合使用单个`CacheManager`且没有复杂的缓存分辨率要求的应用程序。 + +对于使用多个缓存管理器的应用程序,可以设置用于每个操作的“CacheManager”,如下例所示: + +``` +@Cacheable(cacheNames="books", cacheManager="anotherCacheManager") (1) +public Book findBook(ISBN isbn) {...} +``` + +|**1**|指定`anotherCacheManager`。| +|-----|---------------------------------| + +你还可以完全以类似于替换[key generation](#cache-annotations-cacheable-key)的方式替换`CacheResolver`。对每个缓存操作都请求解析,让实现根据运行时参数实际解析要使用的缓存。下面的示例展示了如何指定`CacheResolver`: + +``` +@Cacheable(cacheResolver="runtimeCacheResolver") (1) +public Book findBook(ISBN isbn) {...} +``` + +|**1**|指定`CacheResolver`。| +|-----|-------------------------------| + +| |自 Spring 4.1 起,`value`属性的缓存注释不再是<br/>强制的,因为该特定信息可以由`CacheResolver`提供,而与注释的内容无关。<br/>`key`与`keyGenerator`类似,`cacheManager`和`cacheResolver`的参数是互斥的,并且指定<br/>的操作会导致异常,因为自定义的`CacheManager`被 cachesolver 实现忽略。这可能不是你所期望的。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 同步缓存 + +在多线程环境中,某些操作可能会为相同的参数并发调用(通常是在启动时)。默认情况下,缓存抽象不会锁定任何内容,并且相同的值可能会被多次计算,从而破坏了缓存的目的。 + +对于这些特殊情况,可以使用`sync`属性来指示底层缓存提供程序在计算值时锁定缓存条目。结果,只有一个线程在忙着计算值,而其他线程则被阻塞,直到在缓存中更新条目。下面的示例展示了如何使用`sync`属性: + +``` +@Cacheable(cacheNames="foos", sync=true) (1) +public Foo executeExpensiveOperation(String id) {...} +``` + +|**1**|使用`sync`属性。| +|-----|---------------------------| + +| |这是一个可选的特性,并且你最喜欢的缓存库可能不支持它。<br/>所有由核心框架提供的`CacheManager`实现都支持它。有关更多详细信息,请参见缓存提供程序的<br/>文档。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 条件缓存 + +有时,一个方法可能不适合始终进行缓存(例如,它可能取决于给定的参数)。缓存注释通过“条件”参数支持这样的用例,该参数接受一个`SpEL`表达式,该表达式被求值为`true`或`false`。如果`true`,则该方法被缓存。如果不是,则表现为该方法没有被缓存(也就是说,无论缓存中有什么值或使用了什么参数,每次都调用该方法)。例如,只有当参数`name`的长度小于 32 时,才会缓存以下方法: + +``` +@Cacheable(cacheNames="book", condition="#name.length() < 32") (1) +public Book findBook(String name) +``` + +|**1**|在`@Cacheable`上设置条件。| +|-----|------------------------------------| + +除了`condition`参数外,你还可以使用`unless`参数来否决向缓存添加值的行为。与`condition`不同,`unless`表达式是在调用方法之后求值的。为了扩展前面的示例,我们可能只想缓存平装书,如下例所示: + +``` +@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result.hardback") (1) +public Book findBook(String name) +``` + +|**1**|使用`unless`属性来阻止精装本。| +|-----|------------------------------------------------| + +缓存抽象支持`java.util.Optional`返回类型。如果一个`Optional`值是*礼物*,它将被存储在关联的缓存中。如果不存在`Optional`值,则`null`将存储在关联的缓存中。`#result`总是指业务实体,而不是支持的包装器,因此前面的示例可以重写如下: + +``` +@Cacheable(cacheNames="book", condition="#name.length() < 32", unless="#result?.hardback") +public Optional<Book> findBook(String name) +``` + +注意,`#result`仍然是指`Book`,而不是`Optional<Book>`。因为它可能是“null”,所以我们使用 spel 的[安全导航操作员](core.html#expressions-operator-safe-navigation)。 + +##### 可用的缓存 SPEL 评估上下文 + +每个`SpEL`表达式相对于一个专用的[`context`](core.html#expressions-language-ref)计算。除了内置参数外,该框架还提供了专用的与缓存相关的元数据,例如参数名称。下表描述了可用于上下文的项,以便你可以将它们用于键和条件计算: + +| Name | Location |说明| Example | +|-------------|------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------| +|`methodName` | Root object |调用的方法的名称| `#root.methodName` | +| `method` | Root object |正在调用的方法| `#root.method.name` | +| `target` | Root object |正在调用的目标对象| `#root.target` | +|`targetClass`| Root object |被调用的目标的类| `#root.targetClass` | +| `args` | Root object |用于调用目标的参数(如数组)| `#root.args[0]` | +| `caches` | Root object |运行当前方法所针对的缓存的集合| `#root.caches[0].name` | +|Argument name|Evaluation context|任何方法参数的名称。如果名称不可用<br/>(可能是由于没有调试信息),则参数名称也可以在`#a<#arg>`下使用,其中`#arg`代表参数索引(从`0`开始)。|`#iban` or `#a0` (you can also use `#p0` or `#p<#arg>` notation as an alias).| +| `result` |Evaluation context|方法调用的结果(要缓存的值)。仅在`unless`表达式、`cache put`表达式(用于计算`key`)或`cache evict`表达式(当`beforeInvocation`为`false`时)中可用。对于受支持的包装器(例如“可选的”),`#result`指的是实际对象,而不是包装器。| `#result` | + +#### 8.2.2.`@CachePut`注释 + +当需要在不干扰方法执行的情况下更新缓存时,可以使用`@CachePut`注释。也就是说,总是调用该方法,并将其结果放入缓存中(根据`@CachePut`选项)。它支持与`@Cacheable`相同的选项,并且应该用于缓存填充,而不是方法流优化。下面的示例使用`@CachePut`注释: + +``` +@CachePut(cacheNames="book", key="#isbn") +public Book updateBook(ISBN isbn, BookDescriptor descriptor) +``` + +| |在相同的方法上使用`@CachePut`和`@Cacheable`注释通常不鼓励<br/>,因为它们具有不同的行为。虽然后者导致使用缓存跳过<br/>方法调用,但前者强制执行<br/>命令中的调用,以运行缓存更新。这会导致意想不到的行为,并且,除了<br/>的特定角格(例如注释具有将它们排除在每个<br/>之外的条件)之外,应该避免此类声明。还需要注意的是,这样的条件不应该依赖于<br/>上的结果对象(即`#result`变量),因为这些条件已经在<br/>之前验证了排除。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 8.2.3.`@CacheEvict`注释 + +缓存抽象不仅允许缓存存储的人口,还允许驱逐。此过程对于从缓存中删除过期或未使用的数据非常有用。与“@cacheable”相反,`@CacheEvict`定义了执行缓存驱逐的方法(即充当从缓存中删除数据的触发器的方法)。与它的同类类似,`@CacheEvict`要求指定一个或多个受该操作影响的缓存,允许自定义缓存和密钥解析,或者指定一个条件,并提供了一个额外的参数(“Allentries”),该参数指示是否需要执行缓存范围内的驱逐,而不仅仅是(基于密钥的)条目驱逐。下面的示例从`books`缓存中删除所有条目: + +``` +@CacheEvict(cacheNames="books", allEntries=true) (1) +public void loadBooks(InputStream batch) +``` + +|**1**|使用`allEntries`属性从缓存中清除所有条目。| +|-----|---------------------------------------------------------------------| + +当需要清除整个缓存区域时,此选项非常有用。正如前面的示例所示,不是逐出每个条目(这将花费很长时间,因为它效率不高),而是在一个操作中删除所有条目。请注意,框架会忽略此场景中指定的任何键,因为它不适用(整个缓存都会被移除,而不仅仅是一个条目)。 + +你还可以使用`beforeInvocation`属性指示是在调用方法之后(默认值)还是在调用方法之前进行驱逐。前者提供了与其余注释相同的语义:一旦方法成功完成,就会在缓存上运行一个操作(在本例中是驱逐)。如果方法不运行(因为它可能被缓存)或抛出异常,则不会发生驱逐。后一种方法(“beforeInvocation=true”)总是在调用方法之前发生驱逐。在驱逐不需要与方法结果挂钩的情况下,这是有用的。 + +请注意,`void`方法可以与`@CacheEvict`一起使用-当这些方法充当触发器时,返回值将被忽略(因为它们不与缓存交互)。`@Cacheable`的情况不是这样的,它将数据添加到缓存或更新缓存中的数据,因此需要一个结果。 + +#### 8.2.4.`@Caching`注释 + +有时,需要指定同一类型的多个注释(例如`@CacheEvict`或 `@cacheput`)——例如,因为不同的缓存之间的条件或密钥表达式是不同的。`@Caching`在相同的方法上使用多个嵌套的 `@cacheable’,`@CachePut`和`@CacheEvict`注释。下面的示例使用了两个`@CacheEvict`注释: + +``` +@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") }) +public Book importBooks(String deposit, Date date) +``` + +#### 8.2.5.`@CacheConfig`注释 + +到目前为止,我们已经看到缓存操作提供了许多定制选项,你可以为每个操作设置这些选项。然而,如果某些定制选项应用于类的所有操作,那么配置它们可能会很繁琐。例如,指定用于类的每个缓存操作的缓存的名称可以由一个类级定义代替。这就是`@CacheConfig`发挥作用的地方。以下示例使用`@CacheConfig`设置缓存的名称: + +``` +@CacheConfig("books") (1) +public class BookRepositoryImpl implements BookRepository { + + @Cacheable + public Book findBook(ISBN isbn) {...} +} +``` + +|**1**|使用`@CacheConfig`设置缓存的名称。| +|-----|--------------------------------------------------| + +`@CacheConfig`是一种类级注释,它允许共享缓存名称、自定义`KeyGenerator`、自定义`CacheManager`和自定义`CacheResolver`。在类上放置此注释不会启动任何缓存操作。 + +操作级定制总是覆盖`@CacheConfig`上的定制集。因此,这为每个缓存操作提供了三个级别的自定义: + +* 全局配置,可用于`CacheManager`,`KeyGenerator`。 + +* 在类级别上,使用`@CacheConfig`。 + +* 在操作层面。 + +#### 8.2.6.启用缓存注释 + +重要的是要注意,即使声明缓存注释并不会自动触发它们的动作--就像 Spring 中的许多事情一样,该功能必须以声明方式启用(这意味着,如果你怀疑这是缓存造成的,你可以通过只删除一个配置行而不是代码中的所有注释来禁用它)。 + +要启用缓存注释,请将注释`@EnableCaching`添加到一个 `@configuration’类中: + +``` +@Configuration +@EnableCaching +public class AppConfig { +} +``` + +或者,对于 XML 配置,你可以使用`cache:annotation-driven`元素: + +``` +<beans xmlns="http://www.springframework.org/schema/beans" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:cache="http://www.springframework.org/schema/cache" + xsi:schemaLocation=" + http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/cache https://www.springframework.org/schema/cache/spring-cache.xsd"> + + <cache:annotation-driven/> +</beans> +``` + +`cache:annotation-driven`元素和`@EnableCaching`注释都允许你指定各种选项,这些选项会影响通过 AOP 将缓存行为添加到应用程序的方式。该配置有意地与[@transactional`](data-access.html#tx-annotation-driven-settings)的配置相似。 + +| |处理缓存注释的默认通知模式是`proxy`,它允许<br/>仅通过代理拦截调用。同一类<br/>中的本地调用不能以这种方式被拦截。对于更高级的拦截模式,可以考虑将<br/>转换为`aspectj`模式,并结合编译时或加载时编织。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |有关实现`CachingConfigurer`所需的<br/>的高级定制(使用 Java 配置)的更多详细信息,请参见[javadoc](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/cache/annotation/CachingConfigurer.html)。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| XML Attribute | Annotation Attribute | Default |说明| +|--------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `cache-manager` |N/A (see the [`CachingConfigurer`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/cache/annotation/CachingConfigurer.html) javadoc)| `cacheManager` |要使用的缓存管理器的名称。默认的`CacheResolver`在<br/>使用此缓存管理器的场景后面初始化(如果未设置`cacheManager`)。要了解更多<br/>对缓存解析度的细粒度管理,请考虑设置“cache-resolver”<br/>属性。| +| `cache-resolver` |N/A (see the [`CachingConfigurer`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/cache/annotation/CachingConfigurer.html) javadoc)|A `SimpleCacheResolver` using the configured `cacheManager`.|Bean 用于解析备份缓存的 CacheResolver 的名称。<br/>此属性不是必需的,只需要指定为<br/>“cache-manager”属性的替代项。| +| `key-generator` |N/A (see the [`CachingConfigurer`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/cache/annotation/CachingConfigurer.html) javadoc)| `SimpleKeyGenerator` |要使用的自定义密钥生成器的名称。| +| `error-handler` |N/A (see the [`CachingConfigurer`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/cache/annotation/CachingConfigurer.html) javadoc)| `SimpleCacheErrorHandler` |要使用的自定义缓存错误处理程序的名称。默认情况下,在<br/>缓存相关操作期间抛出的任何异常都会在客户端被抛回。| +| `mode` | `mode` | `proxy` |默认模式处理要通过使用 Spring 的 AOP <br/>框架(遵循代理语义,如前面讨论的那样,应用于仅通过代理进入的方法调用<br/>)来代理的注释 bean。替代模式使用 Spring 的 AspectJ 缓存方面来编织<br/>受影响的类,修改目标类字节<br/>代码,以应用于任何类型的方法调用。AspectJ 编织需要在 Classpath 中`spring-aspects.jar`以及启用加载时编织(或编译时编织)。(有关如何设置<br/>加载时编织的详细信息,请参见[Spring configuration](core.html#aop-aj-ltw-spring)。| +|`proxy-target-class`| `proxyTargetClass` | `false` |仅适用于代理模式。控制为使用`@Cacheable`或`@CacheEvict`注释的<br/>类创建的缓存代理类型。如果“proxy-target-class”属性设置为`true`,则创建基于类的代理。<br/>如果`proxy-target-class`是`false`,或者如果省略了该属性,则创建标准的基于接口的代理。(有关不同代理类型的详细检查,请参见[代理机制](core.html#aop-proxying)。| +| `order` | `order` | Ordered.LOWEST\_PRECEDENCE |定义应用于带“@cacheable”或`@CacheEvict`注释的 bean 的缓存通知的顺序。(有关<br/>排序 AOP 通知的规则的更多信息,请参见[Advice Ordering](core.html#aop-ataspectj-advice-ordering)。)<br/>没有指定的排序意味着 AOP 子系统确定通知的顺序。| + +| |`<cache:annotation-driven/>`仅在与其定义相同的应用程序上下文中的 bean 上查找`@Cacheable/@CachePut/@CacheEvict/@Caching`。这意味着,<br/>如果将`<cache:annotation-driven/>`放在`WebApplicationContext`中的 `dispatcherservlet’,它只会检查控制器中的 bean,而不是你的服务。<br/>有关更多信息,请参见[the MVC section](web.html#mvc-servlet)。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +方法可见性和缓存注释 + +当你使用代理时,你应该只将缓存注释应用于具有公共可见性的方法。如果使用这些注释对受保护的、私有的或包可见的方法进行注释,则不会产生错误,但是注释的方法不显示已配置的缓存设置。如果需要对非公共方法进行注释,请考虑使用 AspectJ(请参阅本节的其余部分),因为它会更改字节码本身。 + +| |Spring 建议你只使用`@Cache*`注释来注释具体的类(和具体的<br/>类的方法),而不是注释接口。<br/>你当然可以在接口(或接口<br/>方法)上放置`@Cache*`注释,但这仅在使用代理模式(`mode=“proxy”`)的情况下才有效。如果使用<br/>基于编织的方面(`mode=“AspectJ”`),则在<br/>接口级别声明中,编织基础设施不会识别缓存设置。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |在代理模式(默认)中,只有通过<br/>代理进入的外部方法调用才会被拦截。这意味着,自我调用(实际上,<br/>目标对象中的方法调用了目标对象的另一个方法)在运行时不会导致实际的<br/>缓存,即使调用的方法被标记为`@Cacheable`。在这种情况下,使用`aspectj`模式考虑<br/>。此外,代理必须完全初始化为<br/>提供预期的行为,因此你不应该在<br/>初始化代码(即`@PostConstruct`)中依赖此功能。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 8.2.7.使用自定义注释 + +自定义注释和 AspectJ + +该功能仅适用于基于代理的方法,但可以通过使用 AspectJ 进行一些额外的工作来启用。 + +`spring-aspects`模块仅为标准注释定义了一个方面。如果你已经定义了自己的注释,那么还需要为这些注释定义一个方面。检查`AnnotationCacheAspect`以获取示例。 + +缓存抽象允许你使用自己的注释来识别触发缓存填充或驱逐的方法。作为一种模板机制,这非常方便,因为它消除了重复缓存注释声明的需要,如果指定了键或条件,或者你的代码库中不允许外国导入,这一点尤其有用。与[stereotype](core.html#beans-stereotype-annotations)注释的其余部分类似,你可以使用`@Cacheable`、`@CachePut`、`@CacheEvict`和`@CacheConfig`作为[元注释](core.html#beans-meta-annotations)(即可以注释其他注释的注释)。在下面的示例中,我们用自己的自定义注释替换了一个常见的“@cacheable”声明: + +``` +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +@Cacheable(cacheNames="books", key="#isbn") +public @interface SlowService { +} +``` + +在前面的示例中,我们定义了我们自己的`SlowService`注释,它本身用`@Cacheable`注释。现在我们可以替换以下代码: + +``` +@Cacheable(cacheNames="books", key="#isbn") +public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) +``` + +下面的示例显示了我们可以用来替换前面代码的自定义注释: + +``` +@SlowService +public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) +``` + +即使`@SlowService`不是 Spring 注释,容器也会在运行时自动获取其声明并理解其含义。注意,正如[earlier](#cache-annotation-enable)中提到的,需要启用注释驱动的行为。 + +### 注解 + +从版本 4.1 开始, Spring 的缓存抽象完全支持 JCache 标准(JSR-107)注释:`@CacheResult`,`@CachePut`,`@CacheRemove`,和`@CacheRemoveAll`,以及`@CacheDefaults`,`@CacheKey`伙伴关系。即使不将缓存存储迁移到 JSR-107,也可以使用这些注释。内部实现使用 Spring 的缓存抽象,并提供符合规范的缺省“CacheResolver”和实现。换句话说,如果你已经在使用 Spring 的缓存抽象,那么你可以在不更改缓存存储(或配置)的情况下切换到这些标准注释。 + +#### 8.3.1.功能摘要 + +对于那些熟悉 Spring 的缓存注释的人,下表描述了 Spring 注释与其 JSR-107 对应注释之间的主要区别: + +| Spring | JSR-107 |备注| +|------------------------------|-----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `@Cacheable` | `@CacheResult` |非常相似。`@CacheResult`可以缓存特定的异常并强制执行<br/>方法,而不管缓存的内容如何。| +| `@CachePut` | `@CachePut` |Spring 虽然使用方法调用的结果更新缓存,但 JCache<br/>要求将其作为参数传递,该参数用`@CacheValue`注释。<br/>由于这种不同,JCache 允许在<br/>实际方法调用之前或之后更新缓存。| +| `@CacheEvict` | `@CacheRemove` |非常相似。当<br/>方法调用导致异常时,`@CacheRemove`支持条件驱逐。| +|`@CacheEvict(allEntries=true)`|`@CacheRemoveAll`|见`@CacheRemove`。| +| `@CacheConfig` |`@CacheDefaults` |让你以类似的方式配置相同的概念。| + +JCache 有`javax.cache.annotation.CacheResolver`的概念,它与 Spring 的`CacheResolver`接口相同,只是 JCache 只支持一个缓存。默认情况下,一个简单的实现基于注释上声明的名称检索要使用的缓存。应该注意的是,如果在注释上没有指定缓存名称,则会自动生成默认值。有关更多信息,请参见`@CacheResult#cacheName()`的 javadoc。 + +`CacheResolver`实例由`CacheResolverFactory`检索。可以为每个缓存操作定制工厂,如下例所示: + +``` +@CacheResult(cacheNames="books", cacheResolverFactory=MyCacheResolverFactory.class) (1) +public Book findBook(ISBN isbn) +``` + +|**1**|为此操作定制工厂。| +|-----|-------------------------------------------| + +| |对于所有引用的类, Spring 尝试定位具有给定类型的 Bean。<br/>如果存在多个匹配,则创建一个新实例,并可以使用常规的<br/> Bean 生命周期回调,例如依赖注入。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +键是由`javax.cache.annotation.CacheKeyGenerator`生成的,其作用与 Spring 的`KeyGenerator`相同。默认情况下,所有的方法参数都会被考虑在内,除非至少有一个参数是用`@CacheKey`注释的。这类似于 Spring 的[自定义密钥生成声明](#cache-annotations-cacheable-key)。例如,以下是相同的操作,一个使用 Spring 的抽象,另一个使用 JCache: + +``` +@Cacheable(cacheNames="books", key="#isbn") +public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed) + +@CacheResult(cacheName="books") +public Book findBook(@CacheKey ISBN isbn, boolean checkWarehouse, boolean includeUsed) +``` + +你还可以在操作中指定`CacheKeyResolver`,这与你指定`CacheResolverFactory`的方式类似。 + +JCache 可以管理由带注释的方法引发的异常。这可以防止缓存的更新,但也可以缓存异常作为失败的指示器,而不是再次调用方法。假设如果 ISBN 的结构无效,则抛出`InvalidIsbnNotFoundException`。这是一个永久性的失败(用这样的参数无法检索到任何一本书)。以下缓存异常,以便使用相同的、无效的 ISBN 的进一步调用直接抛出缓存的异常,而不是再次调用该方法: + +``` +@CacheResult(cacheName="books", exceptionCacheName="failures" + cachedExceptions = InvalidIsbnNotFoundException.class) +public Book findBook(ISBN isbn) +``` + +#### 8.3.2.启用 JSR-107 支持 + +除了 Spring 的声明性注释支持外,你不需要做任何特定的操作来启用 JSR-107 支持。如果 Classpath 中同时存在 JSR-107API 和 ` Spring-上下文支持 ` 模块,则`@EnableCaching`和`cache:annotation-driven`XML 元素都会自动启用 JCache 支持。 + +| |根据你的用例,选择基本上是你的。你甚至可以混合和<br/>匹配服务,方法是在某些服务上使用 JSR-107API,并在<br/>其他服务上使用 Spring 自己的注释。但是,如果这些服务影响相同的缓存,则应该使用一致的<br/>和相同的密钥生成实现。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 8.4.声明式基于 XML 的缓存 + +如果注释不是一种选择(可能是由于无法访问源代码或没有外部代码),则可以使用 XML 进行声明式缓存。因此,你可以在外部指定目标方法和缓存指令,而不是注释用于缓存的方法(类似于声明性事务管理[advice](data-access.html#transaction-declarative-first-example))。上一节的示例可以转换为以下示例: + +``` +<!-- the service we want to make cacheable --> +<bean id="bookService" class="x.y.service.DefaultBookService"/> + +<!-- cache definitions --> +<cache:advice id="cacheAdvice" cache-manager="cacheManager"> + <cache:caching cache="books"> + <cache:cacheable method="findBook" key="#isbn"/> + <cache:cache-evict method="loadBooks" all-entries="true"/> + </cache:caching> +</cache:advice> + +<!-- apply the cacheable behavior to all BookService interfaces --> +<aop:config> + <aop:advisor advice-ref="cacheAdvice" pointcut="execution(* x.y.BookService.*(..))"/> +</aop:config> + +<!-- cache manager definition omitted --> +``` + +在前面的配置中,`bookService`是可缓存的。要应用的缓存语义封装在`cache:advice`定义中,这导致`findBooks`方法用于将数据放入缓存,而`loadBooks`方法用于驱逐数据。这两个定义都针对`books`缓存。 + +`aop:config`定义通过使用 AspectJ PointCut 表达式将缓存通知应用到程序中的适当点(更多信息可在[Aspect Oriented Programming with Spring](core.html#aop)中获得)。在前面的示例中,将考虑来自`BookService`的所有方法,并将缓存通知应用于它们。 + +声明式 XML 缓存支持所有基于注释的模型,因此在两者之间移动应该非常容易。此外,两者都可以在同一个应用程序中使用。基于 XML 的方法不会触及目标代码。然而,它本质上更加冗长。当处理具有针对缓存的重载方法的类时,确定正确的方法确实需要额外的努力,因为`method`参数不是一个好的鉴别器。在这些情况下,你可以使用 AspectJ 切入点来挑选目标方法并应用适当的缓存功能。然而,通过 XML,应用包或组或接口范围的缓存(同样由于 AspectJ 切入点)和创建模板类定义(就像我们在前面的示例中所做的那样,通过`cache:definitions``cache` 属性定义目标缓存)更容易。 + +### 8.5.配置缓存存储 + +缓存抽象提供了几个存储集成选项。要使用它们,你需要声明一个适当的`CacheManager`(一个控制和管理`Cache`实例的实体,该实例可用于检索这些实例以进行存储)。 + +#### 8.5.1.基于 jdk`ConcurrentMap`的缓存 + +基于 JDK 的`Cache`实现位于 `org.springframework.cache.concurrent`package 下。它允许你使用`ConcurrentHashMap`作为备份`Cache`存储。下面的示例展示了如何配置两个缓存: + +``` +<!-- simple cache manager --> +<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager"> + <property name="caches"> + <set> + <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="default"/> + <bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="books"/> + </set> + </property> +</bean> +``` + +前面的代码片段使用`SimpleCacheManager`为两个名为`ConcurrentMapCache`和`books`的嵌套实例创建`CacheManager`。请注意,名称是直接为每个缓存配置的。 + +由于缓存是由应用程序创建的,因此它与其生命周期绑定在一起,这使得它适合于基本的用例、测试或简单的应用程序。该缓存可以很好地扩展并且非常快,但是它不提供任何管理、持久性功能或驱逐契约。 + +#### 8.5.2.基于 eHcache 的缓存 + +| |Ehcache3.x 完全兼容 JSR-107,不需要专门的支持。| +|---|-----------------------------------------------------------------------------------| + +EhCache2.x 实现位于`org.springframework.cache.ehcache`包中。同样,要使用它,你需要声明适当的`CacheManager`。下面的示例展示了如何做到这一点: + +``` +<bean id="cacheManager" + class="org.springframework.cache.ehcache.EhCacheCacheManager" p:cache-manager-ref="ehcache"/> + +<!-- EhCache library setup --> +<bean id="ehcache" + class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" p:config-location="ehcache.xml"/> +``` + +此设置引导 Spring IoC 中的 EHCache 库(通过`ehcache` Bean),然后将其连接到专用的`CacheManager`实现中。请注意,整个 EHCache 特定的配置是从`ehcache.xml`读取的。 + +#### 8.5.3.咖啡因缓存 + +咖啡因是对芭乐缓存的 Java8 重写,其实现位于“org.springframework.cache.caffeine”包中,并提供了对咖啡因的几个功能的访问。 + +下面的示例配置了一个`CacheManager`,它会按需创建缓存: + +``` +<bean id="cacheManager" + class="org.springframework.cache.caffeine.CaffeineCacheManager"/> +``` + +你还可以提供要显式使用的缓存。在这种情况下,只有那些是由经理提供的。下面的示例展示了如何做到这一点: + +``` +<bean id="cacheManager" class="org.springframework.cache.caffeine.CaffeineCacheManager"> + <property name="cacheNames"> + <set> + <value>default</value> + <value>books</value> + </set> + </property> +</bean> +``` + +咖啡因`CacheManager`还支持自定义`Caffeine`和`CacheLoader`。有关这些问题的更多信息,请参见[咖啡因文档](https://github.com/ben-manes/caffeine/wiki)。 + +#### 8.5.4.基于 Gemfire 的高速缓存 + +Gemfire 是一个面向内存、磁盘支持、弹性可伸缩、持续可用、活动的(具有内置的基于模式的订阅通知)、全局复制的数据库,并提供功能齐全的边缘缓存。有关如何使用 Gemfire 作为`CacheManager`(以及更多)的更多信息,请参见[Spring Data GemFire reference documentation](https://docs.spring.io/spring-gemfire/docs/current/reference/html/)。 + +#### 8.5.5.JSR-107 高速缓存 + +Spring 的缓存抽象还可以使用兼容 JSR-107 的缓存。JCache 实现位于`org.springframework.cache.jcache`包中。 + +同样,要使用它,你需要声明适当的`CacheManager`。下面的示例展示了如何做到这一点: + +``` +<bean id="cacheManager" + class="org.springframework.cache.jcache.JCacheCacheManager" + p:cache-manager-ref="jCacheManager"/> + +<!-- JSR-107 cache manager setup --> +<bean id="jCacheManager" .../> +``` + +#### 8.5.6.处理没有后台存储的缓存 + +有时,在切换环境或进行测试时,你可能会有缓存声明,而没有配置实际的备份缓存。由于这是一个无效的配置,在运行时会引发一个异常,因为缓存基础设施无法找到合适的存储。在这种情况下,你可以连接到一个不执行缓存的简单虚拟缓存,而不是删除缓存声明(这可能会很乏味)——也就是说,它强制每次调用缓存的方法。下面的示例展示了如何做到这一点: + +``` +<bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager"> + <property name="cacheManagers"> + <list> + <ref bean="jdkCache"/> + <ref bean="gemfireCache"/> + </list> + </property> + <property name="fallbackToNoOpCache" value="true"/> +</bean> +``` + +在前面的链中的`CompositeCacheManager`多个`CacheManager`实例,并通过`fallbackToNoOpCache`标志,为所有未由配置的缓存管理器处理的定义添加了一个无操作缓存。也就是说,在`jdkCache`或`gemfireCache`(在示例的前面进行了配置)中找不到的每个缓存定义都由非 OP 缓存处理,该缓存不存储任何信息,导致每次都调用目标方法。 + +### 8.6.插入不同的后端缓存 + +显然,有很多缓存产品可以用作后台商店。对于那些不支持 JSR-107 的,你需要提供`CacheManager`和 `cache’实现。这听起来可能比实际情况更难,因为在实践中,类往往是简单的[adapters](https://en.wikipedia.org/wiki/Adapter_pattern),它将缓存抽象框架映射到存储 API 之上,就像`ehcache`类那样。大多数`CacheManager`类可以使用 `org.springframework.cache.support’包中的类(例如`AbstractCacheManager`,它负责锅炉板代码,只留下实际的映射要完成)。 + +### 8.7.我如何设置 TTL/TTI/驱逐策略/XXX 功能? + +直接通过你的缓存提供程序。缓存抽象是一个抽象,而不是一个缓存实现。你使用的解决方案可能支持其他解决方案不支持的各种数据策略和不同的拓扑(例如,JDK`ConcurrentHashMap`——在缓存抽象中暴露这一点将是无用的,因为没有支持)。这样的功能应该直接通过后台缓存(在配置时)或通过其本地 API 进行控制。 + +## 9. 附录 + +### 9.1.XML 模式 + +附录的这一部分列出了与集成技术相关的 XML 模式。 + +#### 9.1.1.`jee`模式 + +`jee`元素处理与 Java EE(Java Enterprise 版本)配置有关的问题,例如查找 JNDI 对象和定义 EJB 引用。 + +要使用`jee`模式中的元素,你需要在 Spring XML 配置文件的顶部有以下序言。以下代码片段中的文本引用了正确的模式,因此`jee`名称空间中的元素对你是可用的: + +``` +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:jee="http://www.springframework.org/schema/jee" + xsi:schemaLocation=" + http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/jee https://www.springframework.org/schema/jee/spring-jee.xsd"> + + <!-- bean definitions here --> + +</beans> +``` + +##### \<jee:jndi-lookup/\>(简单) + +下面的示例展示了如何使用 JNDI 在没有`jee`模式的情况下查找数据源: + +``` +<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean"> + <property name="jndiName" value="jdbc/MyDataSource"/> +</bean> +<bean id="userDao" class="com.foo.JdbcUserDao"> + <!-- Spring will do the cast automatically (as usual) --> + <property name="dataSource" ref="dataSource"/> +</bean> +``` + +下面的示例展示了如何使用 JNDI 查找带有`jee`模式的数据源: + +``` +<jee:jndi-lookup id="dataSource" jndi-name="jdbc/MyDataSource"/> + +<bean id="userDao" class="com.foo.JdbcUserDao"> + <!-- Spring will do the cast automatically (as usual) --> + <property name="dataSource" ref="dataSource"/> +</bean> +``` + +##### `<jee:jndi-lookup/>`(带有单个 JNDI 环境设置) + +下面的示例展示了如何使用 JNDI 查找不带“JEE”的环境变量: + +``` +<bean id="simple" class="org.springframework.jndi.JndiObjectFactoryBean"> + <property name="jndiName" value="jdbc/MyDataSource"/> + <property name="jndiEnvironment"> + <props> + <prop key="ping">pong</prop> + </props> + </property> +</bean> +``` + +下面的示例展示了如何使用 JNDI 查找带有`jee`的环境变量: + +``` +<jee:jndi-lookup id="simple" jndi-name="jdbc/MyDataSource"> + <jee:environment>ping=pong</jee:environment> +</jee:jndi-lookup> +``` + +##### `<jee:jndi-lookup/>`(具有多个 JNDI 环境设置) + +下面的示例展示了如何使用 JNDI 在不`jee`的情况下查找多个环境变量: + +``` +<bean id="simple" class="org.springframework.jndi.JndiObjectFactoryBean"> + <property name="jndiName" value="jdbc/MyDataSource"/> + <property name="jndiEnvironment"> + <props> + <prop key="sing">song</prop> + <prop key="ping">pong</prop> + </props> + </property> +</bean> +``` + +下面的示例展示了如何使用 JNDI 用“JEE”查找多个环境变量: + +``` +<jee:jndi-lookup id="simple" jndi-name="jdbc/MyDataSource"> + <!-- newline-separated, key-value pairs for the environment (standard Properties format) --> + <jee:environment> + sing=song + ping=pong + </jee:environment> +</jee:jndi-lookup> +``` + +##### `<jee:jndi-lookup/>`(复合) + +下面的示例展示了如何使用 JNDI 在没有`jee`的情况下查找数据源和许多不同的属性: + +``` +<bean id="simple" class="org.springframework.jndi.JndiObjectFactoryBean"> + <property name="jndiName" value="jdbc/MyDataSource"/> + <property name="cache" value="true"/> + <property name="resourceRef" value="true"/> + <property name="lookupOnStartup" value="false"/> + <property name="expectedType" value="com.myapp.DefaultThing"/> + <property name="proxyInterface" value="com.myapp.Thing"/> +</bean> +``` + +下面的示例展示了如何使用 JNDI 用`jee`查找数据源和许多不同的属性: + +``` +<jee:jndi-lookup id="simple" + jndi-name="jdbc/MyDataSource" + cache="true" + resource-ref="true" + lookup-on-startup="false" + expected-type="com.myapp.DefaultThing" + proxy-interface="com.myapp.Thing"/> +``` + +##### `<jee:local-slsb/>`(简单) + +Bean `<jee:local-slsb/>`元素配置了对本地 EJB 无状态会话的引用。 + +下面的示例展示了如何在没有`jee`的情况下配置对本地 EJB 无状态会话的引用 Bean: + +``` +<bean id="simple" + class="org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean"> + <property name="jndiName" value="ejb/RentalServiceBean"/> + <property name="businessInterface" value="com.foo.service.RentalService"/> +</bean> +``` + +下面的示例展示了如何使用`jee`配置对本地 EJB 无状态会话 Bean 的引用: + +``` +<jee:local-slsb id="simpleSlsb" jndi-name="ejb/RentalServiceBean" + business-interface="com.foo.service.RentalService"/> +``` + +##### `<jee:local-slsb/>`(复数) + +`<jee:local-slsb/>`元素配置了对本地 EJB 无状态会话的引用 Bean。 + +下面的示例展示了如何配置对本地 EJB 无状态会话 Bean 的引用和一些不带`jee`的属性: + +``` +<bean id="complexLocalEjb" + class="org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean"> + <property name="jndiName" value="ejb/RentalServiceBean"/> + <property name="businessInterface" value="com.example.service.RentalService"/> + <property name="cacheHome" value="true"/> + <property name="lookupHomeOnStartup" value="true"/> + <property name="resourceRef" value="true"/> +</bean> +``` + +下面的示例展示了如何配置对本地 EJB 无状态会话 Bean 的引用,以及使用`jee`的许多属性: + +``` +<jee:local-slsb id="complexLocalEjb" + jndi-name="ejb/RentalServiceBean" + business-interface="com.foo.service.RentalService" + cache-home="true" + lookup-home-on-startup="true" + resource-ref="true"> +``` + +##### \<jee:remote-slsb/\> + +`<jee:remote-slsb/>`元素配置了对`remote`EJB 无状态会话 Bean 的引用。 + +下面的示例展示了如何在没有`jee`的情况下配置对远程 EJB 无状态会话 Bean 的引用: + +``` +<bean id="complexRemoteEjb" + class="org.springframework.ejb.access.SimpleRemoteStatelessSessionProxyFactoryBean"> + <property name="jndiName" value="ejb/MyRemoteBean"/> + <property name="businessInterface" value="com.foo.service.RentalService"/> + <property name="cacheHome" value="true"/> + <property name="lookupHomeOnStartup" value="true"/> + <property name="resourceRef" value="true"/> + <property name="homeInterface" value="com.foo.service.RentalService"/> + <property name="refreshHomeOnConnectFailure" value="true"/> +</bean> +``` + +下面的示例展示了如何使用`jee`配置对远程 EJB 无状态会话 Bean 的引用: + +``` +<jee:remote-slsb id="complexRemoteEjb" + jndi-name="ejb/MyRemoteBean" + business-interface="com.foo.service.RentalService" + cache-home="true" + lookup-home-on-startup="true" + resource-ref="true" + home-interface="com.foo.service.RentalService" + refresh-home-on-connect-failure="true"> +``` + +#### 9.1.2.`jms`模式 + +`jms`元素处理与 JMS 相关的 bean 的配置,例如 Spring 的[消息监听器容器](#jms-mdp)。这些要素在[JMS chapter](#jms)题为[JMS 名称空间支持](#jms-namespace)的一节中有详细说明。有关此支持和`jms`元素本身的详细信息,请参见该章。 + +为了完整起见,要使用`jms`模式中的元素,你需要在 Spring XML 配置文件的顶部有以下序言。以下代码片段中的文本引用了正确的模式,因此`jms`名称空间中的元素对你是可用的: + +``` +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:jms="http://www.springframework.org/schema/jms" + xsi:schemaLocation=" + http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/jms https://www.springframework.org/schema/jms/spring-jms.xsd"> + + <!-- bean definitions here --> + +</beans> +``` + +#### 9.1.3.使用`<context:mbean-export/>` + +这个元素在[配置基于注释的 MBean 导出](#jmx-context-mbeanexport)中有详细说明。 + +#### 9.1.4.`cache`模式 + +你可以使用`cache`元素来支持 Spring 的`@CacheEvict`、`@CachePut`和`@Caching`注释。它还支持声明式的基于 XML 的缓存。详见[启用缓存注释](#cache-annotation-enable)和[声明式基于 XML 的缓存](#cache-declarative-xml)。 + +要使用`cache`模式中的元素,你需要在 Spring XML 配置文件的顶部有以下序言。以下代码片段中的文本引用了正确的模式,因此`cache`名称空间中的元素对你是可用的: + +``` +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:cache="http://www.springframework.org/schema/cache" + xsi:schemaLocation=" + http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/cache https://www.springframework.org/schema/cache/spring-cache.xsd"> + + <!-- bean definitions here --> + +</beans> +``` diff --git a/docs/spring-framework/languages.md b/docs/spring-framework/languages.md new file mode 100644 index 0000000000000000000000000000000000000000..131a34b41da51186cbbf2f47c62ab5d137c68e27 --- /dev/null +++ b/docs/spring-framework/languages.md @@ -0,0 +1,3333 @@ +# 语言支持 + +## 1. Kotlin + +[Kotlin](https://kotlinlang.org)是一种以 JVM(和其他平台)为目标的静态类型语言,它允许编写简洁和优雅的代码,同时使用用 Java 编写的现有库提供非常好的[互操作性](https://kotlinlang.org/docs/reference/java-interop.html)。 + +Spring 框架为 Kotlin 提供了一流的支持,并允许开发人员编写 Kotlin 应用程序,就好像 Spring 框架是一个原生 Kotlin 框架一样。除了 Java 之外,参考文档的大多数代码示例都是在 Kotlin 中提供的。 + +用 Kotlin 构建 Spring 应用程序的最简单方法是利用 Spring 引导和它的[dedicated Kotlin support](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-kotlin.html)。[这个全面的教程](https://spring.io/guides/tutorials/spring-boot-kotlin/)将教你如何使用[start.spring.io](https://start.spring.io/#!language=kotlin&type=gradle-project)用 Kotlin 构建 Spring 引导应用程序。 + +如果你需要支持,可以随时加入[Kotlin Slack](https://slack.kotlinlang.org/)的 # Spring 通道,或者使用`spring`和`kotlin`作为标记在[Stackoverflow](https://stackoverflow.com/questions/tagged/spring+kotlin)上提问。 + +### 1.1.所需经费 + +Spring 框架支持 Kotlin 1.3+,并且需要[`kotlin-stdlib`](https://search.maven.org/artifact/org.jetbrains.kotlin/kotlin-stdlib)(或其变体之一,例如[`kotlin-stdlib-jdk8`](https://search.maven.org/artifact/org.jetbrains.kotlin/kotlin-stdlib-jdk8))和[`kotlin-reflect`](https://search.maven.org/artifact/org.jetbrains.kotlin/kotlin-reflect)才能在 Classpath 上存在。如果你在[start.spring.io](https://start.spring.io/#!language=kotlin&type=gradle-project)上引导一个 Kotlin 项目,则默认情况下会提供它们。 + +| |对于使用 Jackson 对 Kotlin 类的 JSON 数据进行序列化或反序列化,需要[Jackson Kotlin module](https://github.com/FasterXML/jackson-module-kotlin)<br/>,因此,如果你有此需要,请确保将 `com.fasterxml.Jackson.module:Jackson-module- Kotlin `dependency 添加到你的项目中。<br/>在 Classpath 中找到时,它会自动注册。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 1.2.扩展 + +Kotlin [extensions](https://kotlinlang.org/docs/reference/extensions.html)提供了扩展具有附加功能的现有类的能力。 Spring 框架 Kotlin API 使用这些扩展来向现有 Spring API 添加新的 Kotlin 特定的便利。 + +[Spring 框架 KDoc API](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/)列表和文档都是可用的 Kotlin 扩展和 DSL。 + +| |请记住, Kotlin 扩展需要导入才能使用。这意味着,<br/>例如,`GenericApplicationContext.registerBean` Kotlin 扩展<br/>只有当`org.springframework.context.support.registerBean`被导入时才可用。<br/>也就是说,与静态导入类似,IDE 在大多数情况下应该自动建议导入。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +例如,[Kotlin reified type parameters](https://kotlinlang.org/docs/reference/inline-functions.html#reified-type-parameters)为 JVM[泛型擦除](https://docs.oracle.com/javase/tutorial/java/generics/erasure.html)提供了一种变通方法,而 Spring 框架提供了一些扩展来利用这一特性。这允许更好的 Kotlin API`RestTemplate`、 Spring WebFlux 中的新`WebClient`以及其他各种 API。 + +| |其他库,例如 Reactor 和 Spring Data,也为它们的 API 提供了 Kotlin 扩展,因此总体上提供了更好的 Kotlin 开发体验。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------| + +要在 Java 中检索`User`对象的列表,通常需要编写以下内容: + +``` +Flux<User> users = client.get().retrieve().bodyToFlux(User.class) +``` + +对于 Kotlin 和 Spring 框架扩展,你可以改为编写以下内容: + +``` +val users = client.get().retrieve().bodyToFlux<User>() +// or (both are equivalent) +val users : Flux<User> = client.get().retrieve().bodyToFlux() +``` + +正如在 Java 中一样, Kotlin 中的`users`是强类型的,但是 Kotlin 聪明的类型推断允许更短的语法。 + +### 1.3.零安全 + +Kotlin 的关键特性之一是[null-safety](https://kotlinlang.org/docs/reference/null-safety.html),它在编译时干净地处理`null`值,而不是在运行时遇到著名的 `nullPointerexception’。这使得应用程序通过可否定性声明和表达“值或无值”语义而更安全,而不需要支付包装器的费用,例如`Optional`。( Kotlin 允许使用具有可空的值的函数结构。参见[comprehensive guide to Kotlin null-safety](https://www.baeldung.com/kotlin-null-safety)。) + +虽然 Java 不允许在其类型系统中表示空安全,但 Spring 框架通过在`org.springframework.lang`包中声明的对工具友好的注释提供了[null-safety of the whole Spring Framework API](core.html#null-safety)。默认情况下,来自 Kotlin 中使用的 Java API 的类型被识别为[platform types](https://kotlinlang.org/docs/reference/java-interop.html#null-safety-and-platform-types),对此可以放松空值检查。[Kotlin support for JSR-305 annotations](https://kotlinlang.org/docs/reference/java-interop.html#jsr-305-support)和 Spring 空值注释为 Kotlin 开发人员提供了整个 Spring Framework API 的空安全性,并具有在编译时处理`null`相关问题的优点。 + +| |诸如 Reactor 或 Spring Data 之类的库提供了空安全的 API 来利用此功能。| +|---|-----------------------------------------------------------------------------------------| + +你可以通过添加`-Xjsr305`编译器标志和以下选项来配置 JSR-305 检查:`-Xjsr305={strict|warn|ignore}`。 + +对于 Kotlin 版本 1.1+,默认行为与`-Xjsr305=warn`相同。在从 Spring API 推断出的 Kotlin 类型中,`strict`值需要将 Spring FrameworkAPI 的空安全性考虑在内,但在使用该值时,应该知道 Spring API 的零性声明即使在较小的版本之间也可以发展,并且将来可能会添加更多的检查。 + +| |目前还不支持泛型类型参数、varargs 和数组元素的可空性,<br/>,但应该在即将发布的版本中支持。有关最新信息,请参见[this discussion](https://github.com/Kotlin/KEEP/issues/79)。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 1.4.类和接口 + +Spring 框架支持各种 Kotlin 构造,例如通过主构造函数实例化 Kotlin 类、不可变类数据绑定和具有默认值的函数可选参数。 + +Kotlin 通过专用的`KotlinReflectionParameterNameDiscoverer`来识别参数名称,该参数名称允许查找接口方法参数名称,而不需要在编译过程中启用 Java8`-parameters`编译器标志。 + +你可以将配置类声明为[顶层或嵌套的,但不是内部的](https://kotlinlang.org/docs/reference/nested-classes.html),因为后者需要对外部类的引用。 + +### 1.5.注解 + +Spring 框架还利用[Kotlin null-safety](https://kotlinlang.org/docs/reference/null-safety.html)来确定是否需要 HTTP 参数,而无需显式地定义`required`属性。这意味着`@RequestParam name: String?`被视为不需要,相反,`@RequestParam name: String`被视为需要。 Spring 消息传递`@Header`注释上也支持此功能。 + +以类似的方式, Spring Bean 带有`@Autowired`、`@Bean`或`@Inject`的注入使用该信息来确定是否需要 Bean。 + +例如,`@Autowired lateinit var thing: Thing`意味着必须在应用程序上下文中注册类型`Thing`的 Bean,而`@Autowired lateinit var thing: Thing?`如果不存在这样的 Bean,则不会引发错误。 + +遵循相同的原则,`@Bean fun play(toy: Toy, car: Car?) = Baz(toy, Car)`意味着类型`Toy`的 Bean 必须在应用程序上下文中注册,而类型`Car`的 Bean 可能存在,也可能不存在。同样的行为也适用于自动连线构造函数参数。 + +| |如果在具有属性或主构造函数<br/>参数的类上使用 Bean 验证,则可能需要使用[注释使用-站点目标](https://kotlinlang.org/docs/reference/annotations.html#annotation-use-site-targets),<br/>,例如`@field:NotNull`或`@get:Size(min=5, max=15)`,如[这个堆栈溢出响应](https://stackoverflow.com/a/35853200/1092077)中所述。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 1.6. Bean 定义 DSL + +Spring Framework 支持通过使用 lambdas 作为 XML 或 Java 配置的替代方案(`@configuration` 和`@Bean`)以功能方式注册 bean。简而言之,它允许你用 lambda 注册 bean,lambda 充当`FactoryBean`。这种机制非常有效,因为它不需要任何反射或 CGlib 代理。 + +例如,在 Java 中,你可以编写以下内容: + +``` +class Foo {} + +class Bar { + private final Foo foo; + public Bar(Foo foo) { + this.foo = foo; + } +} + +GenericApplicationContext context = new GenericApplicationContext(); +context.registerBean(Foo.class); +context.registerBean(Bar.class, () -> new Bar(context.getBean(Foo.class))); +``` + +在 Kotlin 中,使用具体化的类型参数和`GenericApplicationContext` Kotlin 扩展,你可以改为编写以下内容: + +``` +class Foo + +class Bar(private val foo: Foo) + +val context = GenericApplicationContext().apply { + registerBean<Foo>() + registerBean { Bar(it.getBean()) } +} +``` + +当类`Bar`只有一个构造函数时,你甚至可以只指定 Bean 类,构造函数参数将根据类型自动连线: + +``` +val context = GenericApplicationContext().apply { + registerBean<Foo>() + registerBean<Bar>() +} +``` + +为了允许更多的声明性方法和更干净的语法, Spring Framework 提供了[Kotlin bean definition DSL](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.context.support/-bean-definition-dsl/)它通过一个干净的声明性 API 声明`ApplicationContextInitializer`,它允许你处理配置文件和`Environment`,以自定义如何注册 bean。 + +在下面的示例中,请注意: + +* 类型推理通常允许避免为 Bean 引用指定类型,如`ref("bazBean")` + +* 在本例中,可以使用 Kotlin 顶级函数使用`bean(::myRouter)`之类的可调用引用来声明 bean。 + +* 当指定`bean<Bar>()`或`bean(::myRouter)`时,参数将按类型自动连线。 + +* 只有在`foobar`配置文件处于活动状态时,才会注册`FooBar` Bean + +``` +class Foo +class Bar(private val foo: Foo) +class Baz(var message: String = "") +class FooBar(private val baz: Baz) + +val myBeans = beans { + bean<Foo>() + bean<Bar>() + bean("bazBean") { + Baz().apply { + message = "Hello world" + } + } + profile("foobar") { + bean { FooBar(ref("bazBean")) } + } + bean(::myRouter) +} + +fun myRouter(foo: Foo, bar: Bar, baz: Baz) = router { + // ... +} +``` + +| |此 DSL 是可编程的,这意味着它允许通过`if`表达式、`for`循环或任何其他 Kotlin 构造对 bean<br/>进行自定义注册逻辑。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------| + +然后,你可以使用这个`beans()`函数在应用程序上下文中注册 bean,如下例所示: + +``` +val context = GenericApplicationContext().apply { + myBeans.initialize(this) + refresh() +} +``` + +| |Spring boot 是基于 javaconfig 和[does not yet provide specific support for functional bean definition](https://github.com/spring-projects/spring-boot/issues/8115),<br/>,但可以通过 Spring boot 的`ApplicationContextInitializer`支持实验性地使用函数 Bean 定义。<br/>查看[这个堆栈溢出回答](https://stackoverflow.com/questions/45935931/how-to-use-functional-bean-definition-kotlin-dsl-with-spring-boot-and-spring-w/46033685#46033685)以获得更多详细信息和最新信息。另见[Spring Fu incubator](https://github.com/spring-projects/spring-fu)中开发的实验 Kofu DSL。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 1.7.万维网 + +#### 1.7.1.路由器 DSL + +Spring Framework 自带的路由器 DSL 有 3 种类型: + +* WebMVC.FNDSL with[router { }](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.web.servlet.function/router.html) + +* webflux.FN[Reactive](web-reactive.html#webflux-fn)dsl with[router { }](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/router.html) + +* webflux.FN[Coroutines](#coroutines)dsl with[coRouter { }](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/co-router.html) + +这些 DSL 允许你编写干净且惯用的 Kotlin 代码来构建`RouterFunction`实例,如下例所示: + +``` +@Configuration +class RouterRouterConfiguration { + + @Bean + fun mainRouter(userHandler: UserHandler) = router { + accept(TEXT_HTML).nest { + GET("/") { ok().render("index") } + GET("/sse") { ok().render("sse") } + GET("/users", userHandler::findAllView) + } + "/api".nest { + accept(APPLICATION_JSON).nest { + GET("/users", userHandler::findAll) + } + accept(TEXT_EVENT_STREAM).nest { + GET("/users", userHandler::stream) + } + } + resources("/**", ClassPathResource("static/")) + } +} +``` + +| |此 DSL 是可编程的,这意味着它允许通过`if`表达式、`for`循环或任何其他 Kotlin 构造对 bean<br/>进行自定义注册逻辑。当你需要根据动态数据(例如,来自数据库)注册路由时,这可能是有用的<br/>。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +具体例子见[MiXiT project](https://github.com/mixitconf/mixit/)。 + +#### 1.7.2.MockMVC DSL + +Kotlin DSL 通过 Kotlin 扩展提供,以便提供更惯用的 Kotlin API 并允许更好的可发现性(不使用静态方法)。 + +``` +val mockMvc: MockMvc = ... +mockMvc.get("/person/{name}", "Lee") { + secure = true + accept = APPLICATION_JSON + headers { + contentLanguage = Locale.FRANCE + } + principal = Principal { "foo" } +}.andExpect { + status { isOk } + content { contentType(APPLICATION_JSON) } + jsonPath("$.name") { value("Lee") } + content { json("""{"someBoolean": false}""", false) } +}.andDo { + print() +} +``` + +#### 1.7.3. Kotlin 脚本模板 + +Spring Framework 提供了一个[“ScriptemplateView”](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/view/script/ScriptTemplateView.html),它支持[JSR-223](https://www.jcp.org/en/jsr/detail?id=223)通过使用脚本引擎来呈现模板。 + +通过利用`scripting-jsr223`依赖关系,可以使用这样的特性来呈现带有[kotlinx.html](https://github.com/Kotlin/kotlinx.html)DSL 或 Kotlin 多行插值`String`的基于 Kotlin 的模板。 + +`build.gradle.kts` + +``` +dependencies { + runtime("org.jetbrains.kotlin:kotlin-scripting-jsr223:${kotlinVersion}") +} +``` + +配置通常使用`ScriptTemplateConfigurer`和`ScriptTemplateViewResolver`bean。 + +`KotlinScriptConfiguration.kt` + +``` +@Configuration +class KotlinScriptConfiguration { + + @Bean + fun kotlinScriptConfigurer() = ScriptTemplateConfigurer().apply { + engineName = "kotlin" + setScripts("scripts/render.kts") + renderFunction = "render" + isSharedEngine = false + } + + @Bean + fun kotlinScriptViewResolver() = ScriptTemplateViewResolver().apply { + setPrefix("templates/") + setSuffix(".kts") + } +} +``` + +有关更多详细信息,请参见[kotlin-script-templating](https://github.com/sdeleuze/kotlin-script-templating)示例项目。 + +#### 1.7.4. Kotlin 多平台序列化 + +在 Spring Framework5.3 中,[Kotlin multiplatform serialization](https://github.com/Kotlin/kotlinx.serialization)在 Spring MVC、 Spring WebFlux 和 Spring Messaging 中得到了支持。内置支持目前只针对 JSON 格式。 + +要启用它,请按照[那些指示](https://github.com/Kotlin/kotlinx.serialization#setup)添加相关的依赖项和插件。对于 Spring MVC 和 WebFlux,如果 Kotlin 序列化和 Jackson 都在 Classpath 中,那么它们将默认地被配置,因为 Kotlin 序列化被设计为仅序列化 Kotlin 用`@Serializable`注释的类。使用 Spring 消息传递,如果你想要自动配置,则确保 Jackson、GSON 或 JSONB 都不在 Classpath 中,如果需要配置 Jackson,则手动配置`KotlinSerializationJsonMessageConverter`。 + +### 1.8.协理 + +Kotlin [Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html)是 Kotlin 轻量级线程,允许以强制方式编写非阻塞代码。在语言端,挂起函数为异步操作提供了抽象,而在库端[Kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines)提供了[`async { }`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html)之类的函数和[`Flow`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html)之类的类型。 + +Spring 框架在以下范围上为协程提供支持: + +* [Deferred](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html)和[Flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html)返回 Spring MVC 和 WebFlux 中支持的值,注释`@Controller` + +* Spring MVC 和 WebFlux 注释`@Controller`中的挂起功能支持 + +* WebFlux[client](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.web.reactive.function.client/index.html)和[server](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/index.html)函数 API 的扩展。 + +* WebFlux.FN[coRouter { }](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/co-router.html)DSL + +* 挂起函数和`Flow`在 RSocket 中支持`@MessageMapping`注释方法 + +* [rsocketrequester’](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.messaging.rsocket/index.html)的扩展 + +#### 1.8.1.依赖关系 + +当`kotlinx-coroutines-core`和`kotlinx-coroutines-reactor`依赖项位于 Classpath 中时,将启用协程支持: + +`build.gradle.kts` + +``` +dependencies { + + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:${coroutinesVersion}") +} +``` + +支持`1.4.0`及以上版本。 + +#### 1.8.2.反应性如何转化为协程? + +对于返回值,从 Active 到 CoroutineAPI 的转换如下: + +* `fun handler(): Mono<Void>`变为`suspend fun handler()` + +* `fun handler(): Mono<T>`变为`suspend fun handler(): T`或`suspend fun handler(): T?`取决于`Mono`是否为空(具有更静态类型的优点) + +* `fun handler(): Flux<T>`变为`fun handler(): Flow<T>` + +输入参数: + +* 如果不需要惰性,则`fun handler(mono: Mono<T>)`变为`fun handler(value: T)`,因为可以调用挂起的函数来获得值参数。 + +* 如果需要惰性,则`fun handler(mono: Mono<T>)`变为`fun handler(supplier: suspend () → T)`或`fun handler(supplier: suspend () → T?)` + +[`Flow`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html)在协程世界中是`Flux`等价的,适用于冷热气流、有限气流或无限气流,有以下主要区别: + +* `Flow`是推拉式的,而`Flux`是推拉式的混合动力 + +* 背压是通过悬挂功能实现的。 + +* `Flow`只有一个[single suspending `collect` method](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/collect.html),运算符实现为[extensions](https://kotlinlang.org/docs/reference/extensions.html) + +* [运营商很容易实现。](https://github.com/Kotlin/kotlinx.coroutines/tree/master/kotlinx-coroutines-core/common/src/flow/operators)感谢协程 + +* 扩展允许将自定义运算符添加到`Flow` + +* 收集操作是挂起的功能 + +* [`map` operator](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/map.html)支持异步操作(不需要`flatMap`),因为它需要一个挂起的函数参数 + +阅读这篇关于[Going Reactive with Spring, Coroutines and Kotlin Flow](https://spring.io/blog/2019/04/12/going-reactive-with-spring-coroutines-and-kotlin-flow)的博客文章,了解更多详细信息,包括如何与协程同时运行代码。 + +#### 1.8.3.控制器 + +下面是一个协程`@RestController`的例子。 + +``` +@RestController +class CoroutinesRestController(client: WebClient, banner: Banner) { + + @GetMapping("/suspend") + suspend fun suspendingEndpoint(): Banner { + delay(10) + return banner + } + + @GetMapping("/flow") + fun flowEndpoint() = flow { + delay(10) + emit(banner) + delay(10) + emit(banner) + } + + @GetMapping("/deferred") + fun deferredEndpoint() = GlobalScope.async { + delay(10) + banner + } + + @GetMapping("/sequential") + suspend fun sequential(): List<Banner> { + val banner1 = client + .get() + .uri("/suspend") + .accept(MediaType.APPLICATION_JSON) + .awaitExchange() + .awaitBody<Banner>() + val banner2 = client + .get() + .uri("/suspend") + .accept(MediaType.APPLICATION_JSON) + .awaitExchange() + .awaitBody<Banner>() + return listOf(banner1, banner2) + } + + @GetMapping("/parallel") + suspend fun parallel(): List<Banner> = coroutineScope { + val deferredBanner1: Deferred<Banner> = async { + client + .get() + .uri("/suspend") + .accept(MediaType.APPLICATION_JSON) + .awaitExchange() + .awaitBody<Banner>() + } + val deferredBanner2: Deferred<Banner> = async { + client + .get() + .uri("/suspend") + .accept(MediaType.APPLICATION_JSON) + .awaitExchange() + .awaitBody<Banner>() + } + listOf(deferredBanner1.await(), deferredBanner2.await()) + } + + @GetMapping("/error") + suspend fun error() { + throw IllegalStateException() + } + + @GetMapping("/cancel") + suspend fun cancel() { + throw CancellationException() + } + +} +``` + +还支持`@Controller`的视图呈现。 + +``` +@Controller +class CoroutinesViewController(banner: Banner) { + + @GetMapping("/") + suspend fun render(model: Model): String { + delay(10) + model["banner"] = banner + return "index" + } +} +``` + +#### 1.8.4.WebFlux.FN + +下面是通过[coRouter { }](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/co-router.html)DSL 和相关处理程序定义的协程路由器的示例。 + +``` +@Configuration +class RouterConfiguration { + + @Bean + fun mainRouter(userHandler: UserHandler) = coRouter { + GET("/", userHandler::listView) + GET("/api/user", userHandler::listApi) + } +} +``` + +``` +class UserHandler(builder: WebClient.Builder) { + + private val client = builder.baseUrl("...").build() + + suspend fun listView(request: ServerRequest): ServerResponse = + ServerResponse.ok().renderAndAwait("users", mapOf("users" to + client.get().uri("...").awaitExchange().awaitBody<User>())) + + suspend fun listApi(request: ServerRequest): ServerResponse = + ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyAndAwait( + client.get().uri("...").awaitExchange().awaitBody<User>()) +} +``` + +#### 1.8.5.交易 + +通过 Spring 框架 5.2 提供的反应式事务管理的编程变体,支持协程上的事务。 + +对于挂起的函数,提供了`TransactionalOperator.executeAndAwait`扩展。 + +``` +import org.springframework.transaction.reactive.executeAndAwait + +class PersonRepository(private val operator: TransactionalOperator) { + + suspend fun initDatabase() = operator.executeAndAwait { + insertPerson1() + insertPerson2() + } + + private suspend fun insertPerson1() { + // INSERT SQL statement + } + + private suspend fun insertPerson2() { + // INSERT SQL statement + } +} +``` + +对于 Kotlin `Flow`,提供了一个`Flow<T>.transactional`扩展。 + +``` +import org.springframework.transaction.reactive.transactional + +class PersonRepository(private val operator: TransactionalOperator) { + + fun updatePeople() = findPeople().map(::updatePerson).transactional(operator) + + private fun findPeople(): Flow<Person> { + // SELECT SQL statement + } + + private suspend fun updatePerson(person: Person): Person { + // UPDATE SQL statement + } +} +``` + +### 1.9. Spring Kotlin 中的项目 + +本节提供了一些值得在 Kotlin 中开发 Spring 项目的具体提示和建议。 + +#### 1.9.1.默认情况下为 final + +默认情况下,[all classes in Kotlin are `final`](https://discuss.kotlinlang.org/t/classes-final-by-default/166)。类上的`open`修饰符与 Java 的`final`相反:它允许其他人继承这个类。这也适用于成员函数,因为它们需要标记为`open`才能被重写。 + +虽然 Kotlin 的 JVM 友好设计通常与 Spring 无摩擦,但如果不考虑这一事实,则此特定的 Kotlin 特性可以阻止应用程序启动。这是因为 Spring bean(例如`@Configuration`注释类,由于技术原因,默认情况下需要在运行时进行扩展)通常是由 CGLIB 代理的。解决方法是在 Spring bean 的每个类和成员函数上添加一个`open`关键字,这些类和成员函数是由 CGlib 代理的,这可能很快会变得很痛苦,并且违反 Kotlin 保持代码简洁和可预测的原则。 + +| |也可以通过使用`@Configuration(proxyBeanMethods = false)`来避免配置类的 CGLIB 代理。<br/>查看[“ProxyBeanMethods”Javadoc](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/annotation/Configuration.html#proxyBeanMethods--)以获得更多详细信息。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +幸运的是, Kotlin 提供了一个[`kotlin-spring`](https://kotlinlang.org/docs/reference/compiler-plugins.html#kotlin-spring-compiler-plugin)插件(`kotlin-allopen`插件的预先配置版本),该插件自动为使用以下注释之一进行注释或元注释的类型打开类及其成员函数: + +* `@Component` + +* `@Async` + +* `@Transactional` + +* `@Cacheable` + +元注释支持意味着使用`@Configuration`、`@Controller`、<restcontroller`、或`@Repository`进行注释的类型将自动打开,因为这些注释是用`@Component`进行元注释的。 + +[start.spring.io](https://start.spring.io/#!language=kotlin&type=gradle-project)默认情况下启用`kotlin-spring`插件。因此,在实践中,你可以在不添加任何`open`关键字的情况下编写 Kotlin bean,就像在 Java 中一样。 + +| |Spring 框架文档中的 Kotlin 代码示例并未在类及其成员函数上明确指定“open”。示例是使用`kotlin-allopen`插件为<br/>项目编写的,因为这是最常用的设置。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.9.2.使用不可变类实例实现持久性 + +在 Kotlin 中,在主构造函数中声明只读属性是很方便的,并且被认为是一种最佳实践,如下例所示: + +``` +class Person(val name: String, val age: Int) +``` + +你可以选择添加[the `data` keyword](https://kotlinlang.org/docs/reference/data-classes.html),以使编译器自动从主构造函数中声明的所有属性派生出以下成员: + +* `equals()`和`hashCode()` + +* `toString()`的形式`"User(name=John, age=42)"` + +* `componentN()`与其声明顺序中的属性对应的函数 + +* `copy()`函数 + +正如下面的示例所示,这允许对单个属性进行简单的更改,即使`Person`属性是只读的: + +``` +data class Person(val name: String, val age: Int) + +val jack = Person(name = "Jack", age = 1) +val olderJack = jack.copy(age = 2) +``` + +常见的持久性技术(例如 JPA)需要一个默认的构造函数,从而阻止了这种设计。幸运的是,对于这个[“默认构造函数地狱”](https://stackoverflow.com/questions/32038177/kotlin-with-jpa-default-constructor-hell)有一种解决方法,因为 Kotlin 提供了一个[`kotlin-jpa`](https://kotlinlang.org/docs/reference/compiler-plugins.html#kotlin-jpa-compiler-plugin)插件,该插件为带有 JPA 注释的类生成合成的无 arg 构造函数。 + +如果需要为其他持久性技术利用这种机制,可以配置[`kotlin-noarg`](https://kotlinlang.org/docs/reference/compiler-plugins.html#how-to-use-no-arg-plugin)插件。 + +| |在 Kay Release Train 中, Spring 数据支持 Kotlin 不可变类实例,并且如果模块使用 Spring 数据对象映射`kotlin-noarg`(例如 MongoDB、Redis、Cassandra 和其他),则`kotlin-noarg`不需要<gt r=“261”插件。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.9.3.注入依赖项 + +我们的建议是尝试使用`val`只读(并且在可能的情况下不可为空)[properties](https://kotlinlang.org/docs/reference/properties.html)的构造函数注入,如下例所示: + +``` +@Component +class YourBean( + private val mongoTemplate: MongoTemplate, + private val solrClient: SolrClient +) +``` + +| |带有单个构造函数的类的参数自动自动连线。<br/>这就是为什么在上面的`@Autowired constructor`示例中不需要显式的`@Autowired constructor`的原因。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +如果确实需要使用字段注入,可以使用`lateinit var`构造,如下例所示: + +``` +@Component +class YourBean { + + @Autowired + lateinit var mongoTemplate: MongoTemplate + + @Autowired + lateinit var solrClient: SolrClient +} +``` + +#### 1.9.4.注入配置属性 + +在 Java 中,你可以通过使用注释(例如`@Value("${property}")`)注入配置属性。然而,在 Kotlin 中,`# 语言支持 + +## 1. Kotlin + +[Kotlin](https://kotlinlang.org)是一种以 JVM(和其他平台)为目标的静态类型语言,它允许编写简洁和优雅的代码,同时使用用 Java 编写的现有库提供非常好的[互操作性](https://kotlinlang.org/docs/reference/java-interop.html)。 + +Spring 框架为 Kotlin 提供了一流的支持,并允许开发人员编写 Kotlin 应用程序,就好像 Spring 框架是一个原生 Kotlin 框架一样。除了 Java 之外,参考文档的大多数代码示例都是在 Kotlin 中提供的。 + +用 Kotlin 构建 Spring 应用程序的最简单方法是利用 Spring 引导和它的[dedicated Kotlin support](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-kotlin.html)。[这个全面的教程](https://spring.io/guides/tutorials/spring-boot-kotlin/)将教你如何使用[start.spring.io](https://start.spring.io/#!language=kotlin&type=gradle-project)用 Kotlin 构建 Spring 引导应用程序。 + +如果你需要支持,可以随时加入[Kotlin Slack](https://slack.kotlinlang.org/)的 # Spring 通道,或者使用`spring`和`kotlin`作为标记在[Stackoverflow](https://stackoverflow.com/questions/tagged/spring+kotlin)上提问。 + +### 1.1.所需经费 + +Spring 框架支持 Kotlin 1.3+,并且需要[`kotlin-stdlib`](https://search.maven.org/artifact/org.jetbrains.kotlin/kotlin-stdlib)(或其变体之一,例如[`kotlin-stdlib-jdk8`](https://search.maven.org/artifact/org.jetbrains.kotlin/kotlin-stdlib-jdk8))和[`kotlin-reflect`](https://search.maven.org/artifact/org.jetbrains.kotlin/kotlin-reflect)才能在 Classpath 上存在。如果你在[start.spring.io](https://start.spring.io/#!language=kotlin&type=gradle-project)上引导一个 Kotlin 项目,则默认情况下会提供它们。 + +| |对于使用 Jackson 对 Kotlin 类的 JSON 数据进行序列化或反序列化,需要[Jackson Kotlin module](https://github.com/FasterXML/jackson-module-kotlin)<br/>,因此,如果你有此需要,请确保将 `com.fasterxml.Jackson.module:Jackson-module- Kotlin `dependency 添加到你的项目中。<br/>在 Classpath 中找到时,它会自动注册。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 1.2.扩展 + +Kotlin [extensions](https://kotlinlang.org/docs/reference/extensions.html)提供了扩展具有附加功能的现有类的能力。 Spring 框架 Kotlin API 使用这些扩展来向现有 Spring API 添加新的 Kotlin 特定的便利。 + +[Spring Framework KDoc API](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/)列表和文档都是可用的 Kotlin 扩展和 DSL。 + +| |请记住, Kotlin 扩展需要导入才能使用。这意味着,<br/>例如,`GenericApplicationContext.registerBean` Kotlin 扩展<br/>只有当`org.springframework.context.support.registerBean`被导入时才可用。<br/>也就是说,与静态导入类似,IDE 在大多数情况下应该自动建议导入。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +例如,[Kotlin reified type parameters](https://kotlinlang.org/docs/reference/inline-functions.html#reified-type-parameters)为 JVM[泛型擦除](https://docs.oracle.com/javase/tutorial/java/generics/erasure.html)提供了一种变通方法,而 Spring 框架提供了一些扩展来利用这一特性。这允许更好的 Kotlin API`RestTemplate`、 Spring WebFlux 中的新`WebClient`以及其他各种 API。 + +| |其他库,例如 Reactor 和 Spring Data,也为它们的 API 提供了 Kotlin 扩展,因此总体上提供了更好的 Kotlin 开发体验。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------| + +要在 Java 中检索`User`对象的列表,通常需要编写以下内容: + +``` +Flux<User> users = client.get().retrieve().bodyToFlux(User.class) +``` + +对于 Kotlin 和 Spring 框架扩展,你可以改为编写以下内容: + +``` +val users = client.get().retrieve().bodyToFlux<User>() +// or (both are equivalent) +val users : Flux<User> = client.get().retrieve().bodyToFlux() +``` + +正如在 Java 中一样, Kotlin 中的`users`是强类型的,但是 Kotlin 聪明的类型推断允许更短的语法。 + +### 1.3.零安全 + +Kotlin 的关键特性之一是[null-safety](https://kotlinlang.org/docs/reference/null-safety.html),它在编译时干净地处理`null`值,而不是在运行时遇到著名的 `nullPointerexception’。这使得应用程序通过可否定性声明和表达“值或无值”语义而更安全,而不需要支付包装器的费用,例如`Optional`。( Kotlin 允许使用具有可空的值的函数结构。参见[comprehensive guide to Kotlin null-safety](https://www.baeldung.com/kotlin-null-safety)。) + +虽然 Java 不允许在其类型系统中表示空安全,但 Spring 框架通过在`org.springframework.lang`包中声明的对工具友好的注释提供了[null-safety of the whole Spring Framework API](core.html#null-safety)。默认情况下,来自 Kotlin 中使用的 Java API 的类型被识别为[platform types](https://kotlinlang.org/docs/reference/java-interop.html#null-safety-and-platform-types),对此可以放松空值检查。[Kotlin support for JSR-305 annotations](https://kotlinlang.org/docs/reference/java-interop.html#jsr-305-support)和 Spring 空值注释为 Kotlin 开发人员提供了整个 Spring Framework API 的空安全性,并具有在编译时处理`null`相关问题的优点。 + +| |诸如 Reactor 或 Spring Data 之类的库提供了空安全的 API 来利用此功能。| +|---|-----------------------------------------------------------------------------------------| + +你可以通过添加`-Xjsr305`编译器标志和以下选项来配置 JSR-305 检查:`-Xjsr305={strict|warn|ignore}`。 + +对于 Kotlin 版本 1.1+,默认行为与`-Xjsr305=warn`相同。在从 Spring API 推断出的 Kotlin 类型中,`strict`值需要将 Spring FrameworkAPI 的空安全性考虑在内,但在使用该值时,应该知道 Spring API 的零性声明即使在较小的版本之间也可以发展,并且将来可能会添加更多的检查。 + +| |目前还不支持泛型类型参数、varargs 和数组元素的可空性,<br/>,但应该在即将发布的版本中支持。有关最新信息,请参见[this discussion](https://github.com/Kotlin/KEEP/issues/79)。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 1.4.类和接口 + +Spring 框架支持各种 Kotlin 构造,例如通过主构造函数实例化 Kotlin 类、不可变类数据绑定和具有默认值的函数可选参数。 + +Kotlin 通过专用的`KotlinReflectionParameterNameDiscoverer`来识别参数名称,该参数名称允许查找接口方法参数名称,而不需要在编译过程中启用 Java8`-parameters`编译器标志。 + +你可以将配置类声明为[顶层或嵌套的,但不是内部的](https://kotlinlang.org/docs/reference/nested-classes.html),因为后者需要对外部类的引用。 + +### 1.5.注解 + +Spring 框架还利用[Kotlin null-safety](https://kotlinlang.org/docs/reference/null-safety.html)来确定是否需要 HTTP 参数,而无需显式地定义`required`属性。这意味着`@RequestParam name: String?`被视为不需要,相反,`@RequestParam name: String`被视为需要。 Spring 消息传递`@Header`注释上也支持此功能。 + +以类似的方式, Spring Bean 带有`@Autowired`、`@Bean`或`@Inject`的注入使用该信息来确定是否需要 Bean。 + +例如,`@Autowired lateinit var thing: Thing`意味着必须在应用程序上下文中注册类型`Thing`的 Bean,而`@Autowired lateinit var thing: Thing?`如果不存在这样的 Bean,则不会引发错误。 + +遵循相同的原则,`@Bean fun play(toy: Toy, car: Car?) = Baz(toy, Car)`意味着类型`Toy`的 Bean 必须在应用程序上下文中注册,而类型`Car`的 Bean 可能存在,也可能不存在。同样的行为也适用于自动连线构造函数参数。 + +| |如果在具有属性或主构造函数<br/>参数的类上使用 Bean 验证,则可能需要使用[注释使用-站点目标](https://kotlinlang.org/docs/reference/annotations.html#annotation-use-site-targets),<br/>,例如`@field:NotNull`或`@get:Size(min=5, max=15)`,如[这个堆栈溢出响应](https://stackoverflow.com/a/35853200/1092077)中所述。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 1.6. Bean 定义 DSL + +Spring Framework 支持通过使用 lambdas 作为 XML 或 Java 配置的替代方案(`@configuration` 和`@Bean`)以功能方式注册 bean。简而言之,它允许你用 lambda 注册 bean,lambda 充当`FactoryBean`。这种机制非常有效,因为它不需要任何反射或 CGlib 代理。 + +例如,在 Java 中,你可以编写以下内容: + +``` +class Foo {} + +class Bar { + private final Foo foo; + public Bar(Foo foo) { + this.foo = foo; + } +} + +GenericApplicationContext context = new GenericApplicationContext(); +context.registerBean(Foo.class); +context.registerBean(Bar.class, () -> new Bar(context.getBean(Foo.class))); +``` + +在 Kotlin 中,使用具体化的类型参数和`GenericApplicationContext` Kotlin 扩展,你可以改为编写以下内容: + +``` +class Foo + +class Bar(private val foo: Foo) + +val context = GenericApplicationContext().apply { + registerBean<Foo>() + registerBean { Bar(it.getBean()) } +} +``` + +当类`Bar`只有一个构造函数时,你甚至可以只指定 Bean 类,构造函数参数将根据类型自动连线: + +``` +val context = GenericApplicationContext().apply { + registerBean<Foo>() + registerBean<Bar>() +} +``` + +为了允许更多的声明性方法和更干净的语法, Spring Framework 提供了[Kotlin bean definition DSL](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.context.support/-bean-definition-dsl/)它通过一个干净的声明性 API 声明`ApplicationContextInitializer`,它允许你处理配置文件和`Environment`,以自定义如何注册 bean。 + +在下面的示例中,请注意: + +* 类型推理通常允许避免为 Bean 引用指定类型,如`ref("bazBean")` + +* 在本例中,可以使用 Kotlin 顶级函数使用`bean(::myRouter)`之类的可调用引用来声明 bean。 + +* 当指定`bean<Bar>()`或`bean(::myRouter)`时,参数将按类型自动连线。 + +* 只有在`foobar`配置文件处于活动状态时,才会注册`FooBar` Bean + +``` +class Foo +class Bar(private val foo: Foo) +class Baz(var message: String = "") +class FooBar(private val baz: Baz) + +val myBeans = beans { + bean<Foo>() + bean<Bar>() + bean("bazBean") { + Baz().apply { + message = "Hello world" + } + } + profile("foobar") { + bean { FooBar(ref("bazBean")) } + } + bean(::myRouter) +} + +fun myRouter(foo: Foo, bar: Bar, baz: Baz) = router { + // ... +} +``` + +| |此 DSL 是可编程的,这意味着它允许通过`if`表达式、`for`循环或任何其他 Kotlin 构造对 bean<br/>进行自定义注册逻辑。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------| + +然后,你可以使用这个`beans()`函数在应用程序上下文中注册 bean,如下例所示: + +``` +val context = GenericApplicationContext().apply { + myBeans.initialize(this) + refresh() +} +``` + +| |Spring boot 是基于 javaconfig 和[does not yet provide specific support for functional bean definition](https://github.com/spring-projects/spring-boot/issues/8115),<br/>,但可以通过 Spring boot 的`ApplicationContextInitializer`支持实验性地使用函数 Bean 定义。<br/>查看[这个堆栈溢出回答](https://stackoverflow.com/questions/45935931/how-to-use-functional-bean-definition-kotlin-dsl-with-spring-boot-and-spring-w/46033685#46033685)以获得更多详细信息和最新信息。另见[Spring Fu incubator](https://github.com/spring-projects/spring-fu)中开发的实验 Kofu DSL。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 1.7.万维网 + +#### 1.7.1.路由器 DSL + +Spring Framework 自带的路由器 DSL 有 3 种类型: + +* WebMVC.FNDSL with[router { }](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.web.servlet.function/router.html) + +* webflux.FN[Reactive](web-reactive.html#webflux-fn)dsl with[router { }](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/router.html) + +* webflux.FN[Coroutines](#coroutines)dsl with[coRouter { }](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/co-router.html) + +这些 DSL 允许你编写干净且惯用的 Kotlin 代码来构建`RouterFunction`实例,如下例所示: + +``` +@Configuration +class RouterRouterConfiguration { + + @Bean + fun mainRouter(userHandler: UserHandler) = router { + accept(TEXT_HTML).nest { + GET("/") { ok().render("index") } + GET("/sse") { ok().render("sse") } + GET("/users", userHandler::findAllView) + } + "/api".nest { + accept(APPLICATION_JSON).nest { + GET("/users", userHandler::findAll) + } + accept(TEXT_EVENT_STREAM).nest { + GET("/users", userHandler::stream) + } + } + resources("/**", ClassPathResource("static/")) + } +} +``` + +| |此 DSL 是可编程的,这意味着它允许通过`if`表达式、`for`循环或任何其他 Kotlin 构造对 bean<br/>进行自定义注册逻辑。当你需要根据动态数据(例如,来自数据库)注册路由时,这可能是有用的<br/>。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +具体例子见[MiXiT project](https://github.com/mixitconf/mixit/)。 + +#### 1.7.2.MockMVC DSL + +Kotlin DSL 通过 Kotlin 扩展提供,以便提供更惯用的 Kotlin API 并允许更好的可发现性(不使用静态方法)。 + +``` +val mockMvc: MockMvc = ... +mockMvc.get("/person/{name}", "Lee") { + secure = true + accept = APPLICATION_JSON + headers { + contentLanguage = Locale.FRANCE + } + principal = Principal { "foo" } +}.andExpect { + status { isOk } + content { contentType(APPLICATION_JSON) } + jsonPath("$.name") { value("Lee") } + content { json("""{"someBoolean": false}""", false) } +}.andDo { + print() +} +``` + +#### 1.7.3. Kotlin 脚本模板 + +Spring Framework 提供了一个[“ScriptemplateView”](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/view/script/ScriptTemplateView.html),它支持[JSR-223](https://www.jcp.org/en/jsr/detail?id=223)通过使用脚本引擎来呈现模板。 + +通过利用`scripting-jsr223`依赖关系,可以使用这样的特性来呈现带有[kotlinx.html](https://github.com/Kotlin/kotlinx.html)DSL 或 Kotlin 多行插值`String`的基于 Kotlin 的模板。 + +`build.gradle.kts` + +``` +dependencies { + runtime("org.jetbrains.kotlin:kotlin-scripting-jsr223:${kotlinVersion}") +} +``` + +配置通常使用`ScriptTemplateConfigurer`和`ScriptTemplateViewResolver`bean。 + +`KotlinScriptConfiguration.kt` + +``` +@Configuration +class KotlinScriptConfiguration { + + @Bean + fun kotlinScriptConfigurer() = ScriptTemplateConfigurer().apply { + engineName = "kotlin" + setScripts("scripts/render.kts") + renderFunction = "render" + isSharedEngine = false + } + + @Bean + fun kotlinScriptViewResolver() = ScriptTemplateViewResolver().apply { + setPrefix("templates/") + setSuffix(".kts") + } +} +``` + +有关更多详细信息,请参见[kotlin-script-templating](https://github.com/sdeleuze/kotlin-script-templating)示例项目。 + +#### 1.7.4. Kotlin 多平台序列化 + +在 Spring Framework5.3 中,[Kotlin multiplatform serialization](https://github.com/Kotlin/kotlinx.serialization)在 Spring MVC、 Spring WebFlux 和 Spring Messaging 中得到了支持。内置支持目前只针对 JSON 格式。 + +要启用它,请按照[那些指示](https://github.com/Kotlin/kotlinx.serialization#setup)添加相关的依赖项和插件。对于 Spring MVC 和 WebFlux,如果 Kotlin 序列化和 Jackson 都在 Classpath 中,那么它们将默认地被配置,因为 Kotlin 序列化被设计为仅序列化 Kotlin 用`@Serializable`注释的类。使用 Spring 消息传递,如果你想要自动配置,则确保 Jackson、GSON 或 JSONB 都不在 Classpath 中,如果需要配置 Jackson,则手动配置`KotlinSerializationJsonMessageConverter`。 + +### 1.8.协理 + +Kotlin [Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html)是 Kotlin 轻量级线程,允许以强制方式编写非阻塞代码。在语言端,挂起函数为异步操作提供了抽象,而在库端[Kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines)提供了[`async { }`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html)之类的函数和[`Flow`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html)之类的类型。 + +Spring 框架在以下范围上为协程提供支持: + +* [Deferred](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html)和[Flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html)返回 Spring MVC 和 WebFlux 中支持的值,注释`@Controller` + +* Spring MVC 和 WebFlux 注释`@Controller`中的挂起功能支持 + +* WebFlux[client](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.web.reactive.function.client/index.html)和[server](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/index.html)函数 API 的扩展。 + +* WebFlux.FN[coRouter { }](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/co-router.html)DSL + +* 挂起函数和`Flow`在 RSocket 中支持`@MessageMapping`注释方法 + +* [rsocketrequester’](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.messaging.rsocket/index.html)的扩展 + +#### 1.8.1.依赖关系 + +当`kotlinx-coroutines-core`和`kotlinx-coroutines-reactor`依赖项位于 Classpath 中时,将启用协程支持: + +`build.gradle.kts` + +``` +dependencies { + + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:${coroutinesVersion}") +} +``` + +支持`1.4.0`及以上版本。 + +#### 1.8.2.反应性如何转化为协程? + +对于返回值,从 Active 到 CoroutineAPI 的转换如下: + +* `fun handler(): Mono<Void>`变为`suspend fun handler()` + +* `fun handler(): Mono<T>`变为`suspend fun handler(): T`或`suspend fun handler(): T?`取决于`Mono`是否为空(具有更静态类型的优点) + +* `fun handler(): Flux<T>`变为`fun handler(): Flow<T>` + +输入参数: + +* 如果不需要惰性,则`fun handler(mono: Mono<T>)`变为`fun handler(value: T)`,因为可以调用挂起的函数来获得值参数。 + +* 如果需要惰性,则`fun handler(mono: Mono<T>)`变为`fun handler(supplier: suspend () → T)`或`fun handler(supplier: suspend () → T?)` + +[`Flow`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html)在协程世界中是`Flux`等价的,适用于冷热气流、有限气流或无限气流,有以下主要区别: + +* `Flow`是推拉式的,而`Flux`是推拉式的混合动力 + +* 背压是通过悬挂功能实现的。 + +* `Flow`只有一个[single suspending `collect` method](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/collect.html),运算符实现为[extensions](https://kotlinlang.org/docs/reference/extensions.html) + +* [运营商很容易实现。](https://github.com/Kotlin/kotlinx.coroutines/tree/master/kotlinx-coroutines-core/common/src/flow/operators)感谢协程 + +* 扩展允许将自定义运算符添加到`Flow` + +* 收集操作是挂起的功能 + +* [`map` operator](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/map.html)支持异步操作(不需要`flatMap`),因为它需要一个挂起的函数参数 + +阅读这篇关于[Going Reactive with Spring, Coroutines and Kotlin Flow](https://spring.io/blog/2019/04/12/going-reactive-with-spring-coroutines-and-kotlin-flow)的博客文章,了解更多详细信息,包括如何与协程同时运行代码。 + +#### 1.8.3.控制器 + +下面是一个协程`@RestController`的例子。 + +``` +@RestController +class CoroutinesRestController(client: WebClient, banner: Banner) { + + @GetMapping("/suspend") + suspend fun suspendingEndpoint(): Banner { + delay(10) + return banner + } + + @GetMapping("/flow") + fun flowEndpoint() = flow { + delay(10) + emit(banner) + delay(10) + emit(banner) + } + + @GetMapping("/deferred") + fun deferredEndpoint() = GlobalScope.async { + delay(10) + banner + } + + @GetMapping("/sequential") + suspend fun sequential(): List<Banner> { + val banner1 = client + .get() + .uri("/suspend") + .accept(MediaType.APPLICATION_JSON) + .awaitExchange() + .awaitBody<Banner>() + val banner2 = client + .get() + .uri("/suspend") + .accept(MediaType.APPLICATION_JSON) + .awaitExchange() + .awaitBody<Banner>() + return listOf(banner1, banner2) + } + + @GetMapping("/parallel") + suspend fun parallel(): List<Banner> = coroutineScope { + val deferredBanner1: Deferred<Banner> = async { + client + .get() + .uri("/suspend") + .accept(MediaType.APPLICATION_JSON) + .awaitExchange() + .awaitBody<Banner>() + } + val deferredBanner2: Deferred<Banner> = async { + client + .get() + .uri("/suspend") + .accept(MediaType.APPLICATION_JSON) + .awaitExchange() + .awaitBody<Banner>() + } + listOf(deferredBanner1.await(), deferredBanner2.await()) + } + + @GetMapping("/error") + suspend fun error() { + throw IllegalStateException() + } + + @GetMapping("/cancel") + suspend fun cancel() { + throw CancellationException() + } + +} +``` + +还支持`@Controller`的视图呈现。 + +``` +@Controller +class CoroutinesViewController(banner: Banner) { + + @GetMapping("/") + suspend fun render(model: Model): String { + delay(10) + model["banner"] = banner + return "index" + } +} +``` + +#### 1.8.4.WebFlux.FN + +下面是通过[coRouter { }](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/co-router.html)DSL 和相关处理程序定义的协程路由器的示例。 + +``` +@Configuration +class RouterConfiguration { + + @Bean + fun mainRouter(userHandler: UserHandler) = coRouter { + GET("/", userHandler::listView) + GET("/api/user", userHandler::listApi) + } +} +``` + +``` +class UserHandler(builder: WebClient.Builder) { + + private val client = builder.baseUrl("...").build() + + suspend fun listView(request: ServerRequest): ServerResponse = + ServerResponse.ok().renderAndAwait("users", mapOf("users" to + client.get().uri("...").awaitExchange().awaitBody<User>())) + + suspend fun listApi(request: ServerRequest): ServerResponse = + ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyAndAwait( + client.get().uri("...").awaitExchange().awaitBody<User>()) +} +``` + +#### 1.8.5.交易 + +通过 Spring 框架 5.2 提供的反应式事务管理的编程变体,支持协程上的事务。 + +对于挂起的函数,提供了`TransactionalOperator.executeAndAwait`扩展。 + +``` +import org.springframework.transaction.reactive.executeAndAwait + +class PersonRepository(private val operator: TransactionalOperator) { + + suspend fun initDatabase() = operator.executeAndAwait { + insertPerson1() + insertPerson2() + } + + private suspend fun insertPerson1() { + // INSERT SQL statement + } + + private suspend fun insertPerson2() { + // INSERT SQL statement + } +} +``` + +对于 Kotlin `Flow`,提供了一个`Flow<T>.transactional`扩展。 + +``` +import org.springframework.transaction.reactive.transactional + +class PersonRepository(private val operator: TransactionalOperator) { + + fun updatePeople() = findPeople().map(::updatePerson).transactional(operator) + + private fun findPeople(): Flow<Person> { + // SELECT SQL statement + } + + private suspend fun updatePerson(person: Person): Person { + // UPDATE SQL statement + } +} +``` + +### 1.9. Spring Kotlin 中的项目 + +本节提供了一些值得在 Kotlin 中开发 Spring 项目的具体提示和建议。 + +#### 1.9.1.默认情况下为 final + +默认情况下,[all classes in Kotlin are `final`](https://discuss.kotlinlang.org/t/classes-final-by-default/166)。类上的`open`修饰符与 Java 的`final`相反:它允许其他人继承这个类。这也适用于成员函数,因为它们需要标记为`open`才能被重写。 + +虽然 Kotlin 的 JVM 友好设计通常与 Spring 无摩擦,但如果不考虑这一事实,则此特定的 Kotlin 特性可以阻止应用程序启动。这是因为 Spring bean(例如`@Configuration`注释类,由于技术原因,默认情况下需要在运行时进行扩展)通常是由 CGLIB 代理的。解决方法是在 Spring bean 的每个类和成员函数上添加一个`open`关键字,这些类和成员函数是由 CGlib 代理的,这可能很快会变得很痛苦,并且违反 Kotlin 保持代码简洁和可预测的原则。 + +| |也可以通过使用`@Configuration(proxyBeanMethods = false)`来避免配置类的 CGLIB 代理。<br/>查看[“ProxyBeanMethods”Javadoc](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/annotation/Configuration.html#proxyBeanMethods--)以获得更多详细信息。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +幸运的是, Kotlin 提供了一个[`kotlin-spring`](https://kotlinlang.org/docs/reference/compiler-plugins.html#kotlin-spring-compiler-plugin)插件(`kotlin-allopen`插件的预先配置版本),该插件自动为使用以下注释之一进行注释或元注释的类型打开类及其成员函数: + +* `@Component` + +* `@Async` + +* `@Transactional` + +* `@Cacheable` + +元注释支持意味着使用`@Configuration`、`@Controller`、<restcontroller`、或`@Repository`进行注释的类型将自动打开,因为这些注释是用`@Component`进行元注释的。 + +[start.spring.io](https://start.spring.io/#!language=kotlin&type=gradle-project)默认情况下启用`kotlin-spring`插件。因此,在实践中,你可以在不添加任何`open`关键字的情况下编写 Kotlin bean,就像在 Java 中一样。 + +| |Spring 框架文档中的 Kotlin 代码示例并未在类及其成员函数上明确指定“open”。示例是使用`kotlin-allopen`插件为<br/>项目编写的,因为这是最常用的设置。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.9.2.使用不可变类实例实现持久性 + +在 Kotlin 中,在主构造函数中声明只读属性是很方便的,并且被认为是一种最佳实践,如下例所示: + +``` +class Person(val name: String, val age: Int) +``` + +你可以选择添加[the `data` keyword](https://kotlinlang.org/docs/reference/data-classes.html),以使编译器自动从主构造函数中声明的所有属性派生出以下成员: + +* `equals()`和`hashCode()` + +* `toString()`的形式`"User(name=John, age=42)"` + +* `componentN()`与其声明顺序中的属性对应的函数 + +* `copy()`函数 + +正如下面的示例所示,这允许对单个属性进行简单的更改,即使`Person`属性是只读的: + +``` +data class Person(val name: String, val age: Int) + +val jack = Person(name = "Jack", age = 1) +val olderJack = jack.copy(age = 2) +``` + +常见的持久性技术(例如 JPA)需要一个默认的构造函数,从而阻止了这种设计。幸运的是,对于这个[“默认构造函数地狱”](https://stackoverflow.com/questions/32038177/kotlin-with-jpa-default-constructor-hell)有一种解决方法,因为 Kotlin 提供了一个[`kotlin-jpa`](https://kotlinlang.org/docs/reference/compiler-plugins.html#kotlin-jpa-compiler-plugin)插件,该插件为带有 JPA 注释的类生成合成的无 arg 构造函数。 + +如果需要为其他持久性技术利用这种机制,可以配置[`kotlin-noarg`](https://kotlinlang.org/docs/reference/compiler-plugins.html#how-to-use-no-arg-plugin)插件。 + +| |在 Kay Release Train 中, Spring 数据支持 Kotlin 不可变类实例,并且如果模块使用 Spring 数据对象映射`kotlin-noarg`(例如 MongoDB、Redis、Cassandra 和其他),则`kotlin-noarg`不需要<gt r=“261”插件。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.9.3.注入依赖项 + +我们的建议是尝试使用`val`只读(并且在可能的情况下不可为空)[properties](https://kotlinlang.org/docs/reference/properties.html)的构造函数注入,如下例所示: + +``` +@Component +class YourBean( + private val mongoTemplate: MongoTemplate, + private val solrClient: SolrClient +) +``` + +| |带有单个构造函数的类的参数自动自动连线。<br/>这就是为什么在上面的`@Autowired constructor`示例中不需要显式的`@Autowired constructor`的原因。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +如果确实需要使用字段注入,可以使用`lateinit var`构造,如下例所示: + +``` +@Component +class YourBean { + + @Autowired + lateinit var mongoTemplate: MongoTemplate + + @Autowired + lateinit var solrClient: SolrClient +} +``` + +#### 1.9.4.注入配置属性 + +是用于[字符串插值](https://kotlinlang.org/docs/reference/idioms.html#string-interpolation)的保留字符。 + +因此,如果希望在 Kotlin 中使用`@Value`注释,则需要通过写`@Value("\${property}")`来转义`# 语言支持 + +## 1. Kotlin + +[Kotlin](https://kotlinlang.org)是一种以 JVM(和其他平台)为目标的静态类型语言,它允许编写简洁和优雅的代码,同时使用用 Java 编写的现有库提供非常好的[互操作性](https://kotlinlang.org/docs/reference/java-interop.html)。 + +Spring 框架为 Kotlin 提供了一流的支持,并允许开发人员编写 Kotlin 应用程序,就好像 Spring 框架是一个原生 Kotlin 框架一样。除了 Java 之外,参考文档的大多数代码示例都是在 Kotlin 中提供的。 + +用 Kotlin 构建 Spring 应用程序的最简单方法是利用 Spring 引导和它的[dedicated Kotlin support](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-kotlin.html)。[这个全面的教程](https://spring.io/guides/tutorials/spring-boot-kotlin/)将教你如何使用[start.spring.io](https://start.spring.io/#!language=kotlin&type=gradle-project)用 Kotlin 构建 Spring 引导应用程序。 + +如果你需要支持,可以随时加入[Kotlin Slack](https://slack.kotlinlang.org/)的 # Spring 通道,或者使用`spring`和`kotlin`作为标记在[Stackoverflow](https://stackoverflow.com/questions/tagged/spring+kotlin)上提问。 + +### 1.1.所需经费 + +Spring 框架支持 Kotlin 1.3+,并且需要[`kotlin-stdlib`](https://search.maven.org/artifact/org.jetbrains.kotlin/kotlin-stdlib)(或其变体之一,例如[`kotlin-stdlib-jdk8`](https://search.maven.org/artifact/org.jetbrains.kotlin/kotlin-stdlib-jdk8))和[`kotlin-reflect`](https://search.maven.org/artifact/org.jetbrains.kotlin/kotlin-reflect)才能在 Classpath 上存在。如果你在[start.spring.io](https://start.spring.io/#!language=kotlin&type=gradle-project)上引导一个 Kotlin 项目,则默认情况下会提供它们。 + +| |对于使用 Jackson 对 Kotlin 类的 JSON 数据进行序列化或反序列化,需要[Jackson Kotlin module](https://github.com/FasterXML/jackson-module-kotlin)<br/>,因此,如果你有此需要,请确保将 `com.fasterxml.Jackson.module:Jackson-module- Kotlin `dependency 添加到你的项目中。<br/>在 Classpath 中找到时,它会自动注册。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 1.2.扩展 + +Kotlin [extensions](https://kotlinlang.org/docs/reference/extensions.html)提供了扩展具有附加功能的现有类的能力。 Spring 框架 Kotlin API 使用这些扩展来向现有 Spring API 添加新的 Kotlin 特定的便利。 + +[Spring Framework KDoc API](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/)列表和文档都是可用的 Kotlin 扩展和 DSL。 + +| |请记住, Kotlin 扩展需要导入才能使用。这意味着,<br/>例如,`GenericApplicationContext.registerBean` Kotlin 扩展<br/>只有当`org.springframework.context.support.registerBean`被导入时才可用。<br/>也就是说,与静态导入类似,IDE 在大多数情况下应该自动建议导入。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +例如,[Kotlin reified type parameters](https://kotlinlang.org/docs/reference/inline-functions.html#reified-type-parameters)为 JVM[泛型擦除](https://docs.oracle.com/javase/tutorial/java/generics/erasure.html)提供了一种变通方法,而 Spring 框架提供了一些扩展来利用这一特性。这允许更好的 Kotlin API`RestTemplate`、 Spring WebFlux 中的新`WebClient`以及其他各种 API。 + +| |其他库,例如 Reactor 和 Spring Data,也为它们的 API 提供了 Kotlin 扩展,因此总体上提供了更好的 Kotlin 开发体验。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------| + +要在 Java 中检索`User`对象的列表,通常需要编写以下内容: + +``` +Flux<User> users = client.get().retrieve().bodyToFlux(User.class) +``` + +对于 Kotlin 和 Spring 框架扩展,你可以改为编写以下内容: + +``` +val users = client.get().retrieve().bodyToFlux<User>() +// or (both are equivalent) +val users : Flux<User> = client.get().retrieve().bodyToFlux() +``` + +正如在 Java 中一样, Kotlin 中的`users`是强类型的,但是 Kotlin 聪明的类型推断允许更短的语法。 + +### 1.3.零安全 + +Kotlin 的关键特性之一是[null-safety](https://kotlinlang.org/docs/reference/null-safety.html),它在编译时干净地处理`null`值,而不是在运行时遇到著名的 `nullPointerexception’。这使得应用程序通过可否定性声明和表达“值或无值”语义而更安全,而不需要支付包装器的费用,例如`Optional`。( Kotlin 允许使用具有可空的值的函数结构。参见[comprehensive guide to Kotlin null-safety](https://www.baeldung.com/kotlin-null-safety)。) + +虽然 Java 不允许在其类型系统中表示空安全,但 Spring 框架通过在`org.springframework.lang`包中声明的对工具友好的注释提供了[null-safety of the whole Spring Framework API](core.html#null-safety)。默认情况下,来自 Kotlin 中使用的 Java API 的类型被识别为[platform types](https://kotlinlang.org/docs/reference/java-interop.html#null-safety-and-platform-types),对此可以放松空值检查。[Kotlin support for JSR-305 annotations](https://kotlinlang.org/docs/reference/java-interop.html#jsr-305-support)和 Spring 空值注释为 Kotlin 开发人员提供了整个 Spring Framework API 的空安全性,并具有在编译时处理`null`相关问题的优点。 + +| |诸如 Reactor 或 Spring Data 之类的库提供了空安全的 API 来利用此功能。| +|---|-----------------------------------------------------------------------------------------| + +你可以通过添加`-Xjsr305`编译器标志和以下选项来配置 JSR-305 检查:`-Xjsr305={strict|warn|ignore}`。 + +对于 Kotlin 版本 1.1+,默认行为与`-Xjsr305=warn`相同。在从 Spring API 推断出的 Kotlin 类型中,`strict`值需要将 Spring FrameworkAPI 的空安全性考虑在内,但在使用该值时,应该知道 Spring API 的零性声明即使在较小的版本之间也可以发展,并且将来可能会添加更多的检查。 + +| |目前还不支持泛型类型参数、varargs 和数组元素的可空性,<br/>,但应该在即将发布的版本中支持。有关最新信息,请参见[this discussion](https://github.com/Kotlin/KEEP/issues/79)。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 1.4.类和接口 + +Spring 框架支持各种 Kotlin 构造,例如通过主构造函数实例化 Kotlin 类、不可变类数据绑定和具有默认值的函数可选参数。 + +Kotlin 通过专用的`KotlinReflectionParameterNameDiscoverer`来识别参数名称,该参数名称允许查找接口方法参数名称,而不需要在编译过程中启用 Java8`-parameters`编译器标志。 + +你可以将配置类声明为[顶层或嵌套的,但不是内部的](https://kotlinlang.org/docs/reference/nested-classes.html),因为后者需要对外部类的引用。 + +### 1.5.注解 + +Spring 框架还利用[Kotlin null-safety](https://kotlinlang.org/docs/reference/null-safety.html)来确定是否需要 HTTP 参数,而无需显式地定义`required`属性。这意味着`@RequestParam name: String?`被视为不需要,相反,`@RequestParam name: String`被视为需要。 Spring 消息传递`@Header`注释上也支持此功能。 + +以类似的方式, Spring Bean 带有`@Autowired`、`@Bean`或`@Inject`的注入使用该信息来确定是否需要 Bean。 + +例如,`@Autowired lateinit var thing: Thing`意味着必须在应用程序上下文中注册类型`Thing`的 Bean,而`@Autowired lateinit var thing: Thing?`如果不存在这样的 Bean,则不会引发错误。 + +遵循相同的原则,`@Bean fun play(toy: Toy, car: Car?) = Baz(toy, Car)`意味着类型`Toy`的 Bean 必须在应用程序上下文中注册,而类型`Car`的 Bean 可能存在,也可能不存在。同样的行为也适用于自动连线构造函数参数。 + +| |如果在具有属性或主构造函数<br/>参数的类上使用 Bean 验证,则可能需要使用[注释使用-站点目标](https://kotlinlang.org/docs/reference/annotations.html#annotation-use-site-targets),<br/>,例如`@field:NotNull`或`@get:Size(min=5, max=15)`,如[这个堆栈溢出响应](https://stackoverflow.com/a/35853200/1092077)中所述。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 1.6. Bean 定义 DSL + +Spring Framework 支持通过使用 lambdas 作为 XML 或 Java 配置的替代方案(`@configuration` 和`@Bean`)以功能方式注册 bean。简而言之,它允许你用 lambda 注册 bean,lambda 充当`FactoryBean`。这种机制非常有效,因为它不需要任何反射或 CGlib 代理。 + +例如,在 Java 中,你可以编写以下内容: + +``` +class Foo {} + +class Bar { + private final Foo foo; + public Bar(Foo foo) { + this.foo = foo; + } +} + +GenericApplicationContext context = new GenericApplicationContext(); +context.registerBean(Foo.class); +context.registerBean(Bar.class, () -> new Bar(context.getBean(Foo.class))); +``` + +在 Kotlin 中,使用具体化的类型参数和`GenericApplicationContext` Kotlin 扩展,你可以改为编写以下内容: + +``` +class Foo + +class Bar(private val foo: Foo) + +val context = GenericApplicationContext().apply { + registerBean<Foo>() + registerBean { Bar(it.getBean()) } +} +``` + +当类`Bar`只有一个构造函数时,你甚至可以只指定 Bean 类,构造函数参数将根据类型自动连线: + +``` +val context = GenericApplicationContext().apply { + registerBean<Foo>() + registerBean<Bar>() +} +``` + +为了允许更多的声明性方法和更干净的语法, Spring Framework 提供了[Kotlin bean definition DSL](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.context.support/-bean-definition-dsl/)它通过一个干净的声明性 API 声明`ApplicationContextInitializer`,它允许你处理配置文件和`Environment`,以自定义如何注册 bean。 + +在下面的示例中,请注意: + +* 类型推理通常允许避免为 Bean 引用指定类型,如`ref("bazBean")` + +* 在本例中,可以使用 Kotlin 顶级函数使用`bean(::myRouter)`之类的可调用引用来声明 bean。 + +* 当指定`bean<Bar>()`或`bean(::myRouter)`时,参数将按类型自动连线。 + +* 只有在`foobar`配置文件处于活动状态时,才会注册`FooBar` Bean + +``` +class Foo +class Bar(private val foo: Foo) +class Baz(var message: String = "") +class FooBar(private val baz: Baz) + +val myBeans = beans { + bean<Foo>() + bean<Bar>() + bean("bazBean") { + Baz().apply { + message = "Hello world" + } + } + profile("foobar") { + bean { FooBar(ref("bazBean")) } + } + bean(::myRouter) +} + +fun myRouter(foo: Foo, bar: Bar, baz: Baz) = router { + // ... +} +``` + +| |此 DSL 是可编程的,这意味着它允许通过`if`表达式、`for`循环或任何其他 Kotlin 构造对 bean<br/>进行自定义注册逻辑。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------| + +然后,你可以使用这个`beans()`函数在应用程序上下文中注册 bean,如下例所示: + +``` +val context = GenericApplicationContext().apply { + myBeans.initialize(this) + refresh() +} +``` + +| |Spring boot 是基于 javaconfig 和[does not yet provide specific support for functional bean definition](https://github.com/spring-projects/spring-boot/issues/8115),<br/>,但可以通过 Spring boot 的`ApplicationContextInitializer`支持实验性地使用函数 Bean 定义。<br/>查看[这个堆栈溢出回答](https://stackoverflow.com/questions/45935931/how-to-use-functional-bean-definition-kotlin-dsl-with-spring-boot-and-spring-w/46033685#46033685)以获得更多详细信息和最新信息。另见[Spring Fu incubator](https://github.com/spring-projects/spring-fu)中开发的实验 Kofu DSL。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 1.7.万维网 + +#### 1.7.1.路由器 DSL + +Spring Framework 自带的路由器 DSL 有 3 种类型: + +* WebMVC.FNDSL with[router { }](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.web.servlet.function/router.html) + +* webflux.FN[Reactive](web-reactive.html#webflux-fn)dsl with[router { }](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/router.html) + +* webflux.FN[Coroutines](#coroutines)dsl with[coRouter { }](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/co-router.html) + +这些 DSL 允许你编写干净且惯用的 Kotlin 代码来构建`RouterFunction`实例,如下例所示: + +``` +@Configuration +class RouterRouterConfiguration { + + @Bean + fun mainRouter(userHandler: UserHandler) = router { + accept(TEXT_HTML).nest { + GET("/") { ok().render("index") } + GET("/sse") { ok().render("sse") } + GET("/users", userHandler::findAllView) + } + "/api".nest { + accept(APPLICATION_JSON).nest { + GET("/users", userHandler::findAll) + } + accept(TEXT_EVENT_STREAM).nest { + GET("/users", userHandler::stream) + } + } + resources("/**", ClassPathResource("static/")) + } +} +``` + +| |此 DSL 是可编程的,这意味着它允许通过`if`表达式、`for`循环或任何其他 Kotlin 构造对 bean<br/>进行自定义注册逻辑。当你需要根据动态数据(例如,来自数据库)注册路由时,这可能是有用的<br/>。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +具体例子见[MiXiT project](https://github.com/mixitconf/mixit/)。 + +#### 1.7.2.MockMVC DSL + +Kotlin DSL 通过 Kotlin 扩展提供,以便提供更惯用的 Kotlin API 并允许更好的可发现性(不使用静态方法)。 + +``` +val mockMvc: MockMvc = ... +mockMvc.get("/person/{name}", "Lee") { + secure = true + accept = APPLICATION_JSON + headers { + contentLanguage = Locale.FRANCE + } + principal = Principal { "foo" } +}.andExpect { + status { isOk } + content { contentType(APPLICATION_JSON) } + jsonPath("$.name") { value("Lee") } + content { json("""{"someBoolean": false}""", false) } +}.andDo { + print() +} +``` + +#### 1.7.3. Kotlin 脚本模板 + +Spring Framework 提供了一个[“ScriptemplateView”](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/view/script/ScriptTemplateView.html),它支持[JSR-223](https://www.jcp.org/en/jsr/detail?id=223)通过使用脚本引擎来呈现模板。 + +通过利用`scripting-jsr223`依赖关系,可以使用这样的特性来呈现带有[kotlinx.html](https://github.com/Kotlin/kotlinx.html)DSL 或 Kotlin 多行插值`String`的基于 Kotlin 的模板。 + +`build.gradle.kts` + +``` +dependencies { + runtime("org.jetbrains.kotlin:kotlin-scripting-jsr223:${kotlinVersion}") +} +``` + +配置通常使用`ScriptTemplateConfigurer`和`ScriptTemplateViewResolver`bean。 + +`KotlinScriptConfiguration.kt` + +``` +@Configuration +class KotlinScriptConfiguration { + + @Bean + fun kotlinScriptConfigurer() = ScriptTemplateConfigurer().apply { + engineName = "kotlin" + setScripts("scripts/render.kts") + renderFunction = "render" + isSharedEngine = false + } + + @Bean + fun kotlinScriptViewResolver() = ScriptTemplateViewResolver().apply { + setPrefix("templates/") + setSuffix(".kts") + } +} +``` + +有关更多详细信息,请参见[kotlin-script-templating](https://github.com/sdeleuze/kotlin-script-templating)示例项目。 + +#### 1.7.4. Kotlin 多平台序列化 + +在 Spring Framework5.3 中,[Kotlin multiplatform serialization](https://github.com/Kotlin/kotlinx.serialization)在 Spring MVC、 Spring WebFlux 和 Spring Messaging 中得到了支持。内置支持目前只针对 JSON 格式。 + +要启用它,请按照[那些指示](https://github.com/Kotlin/kotlinx.serialization#setup)添加相关的依赖项和插件。对于 Spring MVC 和 WebFlux,如果 Kotlin 序列化和 Jackson 都在 Classpath 中,那么它们将默认地被配置,因为 Kotlin 序列化被设计为仅序列化 Kotlin 用`@Serializable`注释的类。使用 Spring 消息传递,如果你想要自动配置,则确保 Jackson、GSON 或 JSONB 都不在 Classpath 中,如果需要配置 Jackson,则手动配置`KotlinSerializationJsonMessageConverter`。 + +### 1.8.协理 + +Kotlin [Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html)是 Kotlin 轻量级线程,允许以强制方式编写非阻塞代码。在语言端,挂起函数为异步操作提供了抽象,而在库端[Kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines)提供了[`async { }`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html)之类的函数和[`Flow`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html)之类的类型。 + +Spring 框架在以下范围上为协程提供支持: + +* [Deferred](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html)和[Flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html)返回 Spring MVC 和 WebFlux 中支持的值,注释`@Controller` + +* Spring MVC 和 WebFlux 注释`@Controller`中的挂起功能支持 + +* WebFlux[client](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.web.reactive.function.client/index.html)和[server](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/index.html)函数 API 的扩展。 + +* WebFlux.FN[coRouter { }](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/co-router.html)DSL + +* 挂起函数和`Flow`在 RSocket 中支持`@MessageMapping`注释方法 + +* [rsocketrequester’](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.messaging.rsocket/index.html)的扩展 + +#### 1.8.1.依赖关系 + +当`kotlinx-coroutines-core`和`kotlinx-coroutines-reactor`依赖项位于 Classpath 中时,将启用协程支持: + +`build.gradle.kts` + +``` +dependencies { + + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:${coroutinesVersion}") +} +``` + +支持`1.4.0`及以上版本。 + +#### 1.8.2.反应性如何转化为协程? + +对于返回值,从 Active 到 CoroutineAPI 的转换如下: + +* `fun handler(): Mono<Void>`变为`suspend fun handler()` + +* `fun handler(): Mono<T>`变为`suspend fun handler(): T`或`suspend fun handler(): T?`取决于`Mono`是否为空(具有更静态类型的优点) + +* `fun handler(): Flux<T>`变为`fun handler(): Flow<T>` + +输入参数: + +* 如果不需要惰性,则`fun handler(mono: Mono<T>)`变为`fun handler(value: T)`,因为可以调用挂起的函数来获得值参数。 + +* 如果需要惰性,则`fun handler(mono: Mono<T>)`变为`fun handler(supplier: suspend () → T)`或`fun handler(supplier: suspend () → T?)` + +[`Flow`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html)在协程世界中是`Flux`等价的,适用于冷热气流、有限气流或无限气流,有以下主要区别: + +* `Flow`是推拉式的,而`Flux`是推拉式的混合动力 + +* 背压是通过悬挂功能实现的。 + +* `Flow`只有一个[single suspending `collect` method](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/collect.html),运算符实现为[extensions](https://kotlinlang.org/docs/reference/extensions.html) + +* [运营商很容易实现。](https://github.com/Kotlin/kotlinx.coroutines/tree/master/kotlinx-coroutines-core/common/src/flow/operators)感谢协程 + +* 扩展允许将自定义运算符添加到`Flow` + +* 收集操作是挂起的功能 + +* [`map` operator](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/map.html)支持异步操作(不需要`flatMap`),因为它需要一个挂起的函数参数 + +阅读这篇关于[Going Reactive with Spring, Coroutines and Kotlin Flow](https://spring.io/blog/2019/04/12/going-reactive-with-spring-coroutines-and-kotlin-flow)的博客文章,了解更多详细信息,包括如何与协程同时运行代码。 + +#### 1.8.3.控制器 + +下面是一个协程`@RestController`的例子。 + +``` +@RestController +class CoroutinesRestController(client: WebClient, banner: Banner) { + + @GetMapping("/suspend") + suspend fun suspendingEndpoint(): Banner { + delay(10) + return banner + } + + @GetMapping("/flow") + fun flowEndpoint() = flow { + delay(10) + emit(banner) + delay(10) + emit(banner) + } + + @GetMapping("/deferred") + fun deferredEndpoint() = GlobalScope.async { + delay(10) + banner + } + + @GetMapping("/sequential") + suspend fun sequential(): List<Banner> { + val banner1 = client + .get() + .uri("/suspend") + .accept(MediaType.APPLICATION_JSON) + .awaitExchange() + .awaitBody<Banner>() + val banner2 = client + .get() + .uri("/suspend") + .accept(MediaType.APPLICATION_JSON) + .awaitExchange() + .awaitBody<Banner>() + return listOf(banner1, banner2) + } + + @GetMapping("/parallel") + suspend fun parallel(): List<Banner> = coroutineScope { + val deferredBanner1: Deferred<Banner> = async { + client + .get() + .uri("/suspend") + .accept(MediaType.APPLICATION_JSON) + .awaitExchange() + .awaitBody<Banner>() + } + val deferredBanner2: Deferred<Banner> = async { + client + .get() + .uri("/suspend") + .accept(MediaType.APPLICATION_JSON) + .awaitExchange() + .awaitBody<Banner>() + } + listOf(deferredBanner1.await(), deferredBanner2.await()) + } + + @GetMapping("/error") + suspend fun error() { + throw IllegalStateException() + } + + @GetMapping("/cancel") + suspend fun cancel() { + throw CancellationException() + } + +} +``` + +还支持`@Controller`的视图呈现。 + +``` +@Controller +class CoroutinesViewController(banner: Banner) { + + @GetMapping("/") + suspend fun render(model: Model): String { + delay(10) + model["banner"] = banner + return "index" + } +} +``` + +#### 1.8.4.WebFlux.FN + +下面是通过[coRouter { }](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/co-router.html)DSL 和相关处理程序定义的协程路由器的示例。 + +``` +@Configuration +class RouterConfiguration { + + @Bean + fun mainRouter(userHandler: UserHandler) = coRouter { + GET("/", userHandler::listView) + GET("/api/user", userHandler::listApi) + } +} +``` + +``` +class UserHandler(builder: WebClient.Builder) { + + private val client = builder.baseUrl("...").build() + + suspend fun listView(request: ServerRequest): ServerResponse = + ServerResponse.ok().renderAndAwait("users", mapOf("users" to + client.get().uri("...").awaitExchange().awaitBody<User>())) + + suspend fun listApi(request: ServerRequest): ServerResponse = + ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyAndAwait( + client.get().uri("...").awaitExchange().awaitBody<User>()) +} +``` + +#### 1.8.5.交易 + +通过 Spring 框架 5.2 提供的反应式事务管理的编程变体,支持协程上的事务。 + +对于挂起的函数,提供了`TransactionalOperator.executeAndAwait`扩展。 + +``` +import org.springframework.transaction.reactive.executeAndAwait + +class PersonRepository(private val operator: TransactionalOperator) { + + suspend fun initDatabase() = operator.executeAndAwait { + insertPerson1() + insertPerson2() + } + + private suspend fun insertPerson1() { + // INSERT SQL statement + } + + private suspend fun insertPerson2() { + // INSERT SQL statement + } +} +``` + +对于 Kotlin `Flow`,提供了一个`Flow<T>.transactional`扩展。 + +``` +import org.springframework.transaction.reactive.transactional + +class PersonRepository(private val operator: TransactionalOperator) { + + fun updatePeople() = findPeople().map(::updatePerson).transactional(operator) + + private fun findPeople(): Flow<Person> { + // SELECT SQL statement + } + + private suspend fun updatePerson(person: Person): Person { + // UPDATE SQL statement + } +} +``` + +### 1.9. Spring Kotlin 中的项目 + +本节提供了一些值得在 Kotlin 中开发 Spring 项目的具体提示和建议。 + +#### 1.9.1.默认情况下为 final + +默认情况下,[all classes in Kotlin are `final`](https://discuss.kotlinlang.org/t/classes-final-by-default/166)。类上的`open`修饰符与 Java 的`final`相反:它允许其他人继承这个类。这也适用于成员函数,因为它们需要标记为`open`才能被重写。 + +虽然 Kotlin 的 JVM 友好设计通常与 Spring 无摩擦,但如果不考虑这一事实,则此特定的 Kotlin 特性可以阻止应用程序启动。这是因为 Spring bean(例如`@Configuration`注释类,由于技术原因,默认情况下需要在运行时进行扩展)通常是由 CGLIB 代理的。解决方法是在 Spring bean 的每个类和成员函数上添加一个`open`关键字,这些类和成员函数是由 CGlib 代理的,这可能很快会变得很痛苦,并且违反 Kotlin 保持代码简洁和可预测的原则。 + +| |也可以通过使用`@Configuration(proxyBeanMethods = false)`来避免配置类的 CGLIB 代理。<br/>查看[“ProxyBeanMethods”Javadoc](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/annotation/Configuration.html#proxyBeanMethods--)以获得更多详细信息。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +幸运的是, Kotlin 提供了一个[`kotlin-spring`](https://kotlinlang.org/docs/reference/compiler-plugins.html#kotlin-spring-compiler-plugin)插件(`kotlin-allopen`插件的预先配置版本),该插件自动为使用以下注释之一进行注释或元注释的类型打开类及其成员函数: + +* `@Component` + +* `@Async` + +* `@Transactional` + +* `@Cacheable` + +元注释支持意味着使用`@Configuration`、`@Controller`、<restcontroller`、或`@Repository`进行注释的类型将自动打开,因为这些注释是用`@Component`进行元注释的。 + +[start.spring.io](https://start.spring.io/#!language=kotlin&type=gradle-project)默认情况下启用`kotlin-spring`插件。因此,在实践中,你可以在不添加任何`open`关键字的情况下编写 Kotlin bean,就像在 Java 中一样。 + +| |Spring 框架文档中的 Kotlin 代码示例并未在类及其成员函数上明确指定“open”。示例是使用`kotlin-allopen`插件为<br/>项目编写的,因为这是最常用的设置。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.9.2.使用不可变类实例实现持久性 + +在 Kotlin 中,在主构造函数中声明只读属性是很方便的,并且被认为是一种最佳实践,如下例所示: + +``` +class Person(val name: String, val age: Int) +``` + +你可以选择添加[the `data` keyword](https://kotlinlang.org/docs/reference/data-classes.html),以使编译器自动从主构造函数中声明的所有属性派生出以下成员: + +* `equals()`和`hashCode()` + +* `toString()`的形式`"User(name=John, age=42)"` + +* `componentN()`与其声明顺序中的属性对应的函数 + +* `copy()`函数 + +正如下面的示例所示,这允许对单个属性进行简单的更改,即使`Person`属性是只读的: + +``` +data class Person(val name: String, val age: Int) + +val jack = Person(name = "Jack", age = 1) +val olderJack = jack.copy(age = 2) +``` + +常见的持久性技术(例如 JPA)需要一个默认的构造函数,从而阻止了这种设计。幸运的是,对于这个[“默认构造函数地狱”](https://stackoverflow.com/questions/32038177/kotlin-with-jpa-default-constructor-hell)有一种解决方法,因为 Kotlin 提供了一个[`kotlin-jpa`](https://kotlinlang.org/docs/reference/compiler-plugins.html#kotlin-jpa-compiler-plugin)插件,该插件为带有 JPA 注释的类生成合成的无 arg 构造函数。 + +如果需要为其他持久性技术利用这种机制,可以配置[`kotlin-noarg`](https://kotlinlang.org/docs/reference/compiler-plugins.html#how-to-use-no-arg-plugin)插件。 + +| |在 Kay Release Train 中, Spring 数据支持 Kotlin 不可变类实例,并且如果模块使用 Spring 数据对象映射`kotlin-noarg`(例如 MongoDB、Redis、Cassandra 和其他),则`kotlin-noarg`不需要<gt r=“261”插件。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.9.3.注入依赖项 + +我们的建议是尝试使用`val`只读(并且在可能的情况下不可为空)[properties](https://kotlinlang.org/docs/reference/properties.html)的构造函数注入,如下例所示: + +``` +@Component +class YourBean( + private val mongoTemplate: MongoTemplate, + private val solrClient: SolrClient +) +``` + +| |带有单个构造函数的类的参数自动自动连线。<br/>这就是为什么在上面的`@Autowired constructor`示例中不需要显式的`@Autowired constructor`的原因。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +如果确实需要使用字段注入,可以使用`lateinit var`构造,如下例所示: + +``` +@Component +class YourBean { + + @Autowired + lateinit var mongoTemplate: MongoTemplate + + @Autowired + lateinit var solrClient: SolrClient +} +``` + +#### 1.9.4.注入配置属性 + +在 Java 中,你可以通过使用注释(例如`@Value("${property}")`)注入配置属性。然而,在 Kotlin 中,`# 语言支持 + +## 1. Kotlin + +[Kotlin](https://kotlinlang.org)是一种以 JVM(和其他平台)为目标的静态类型语言,它允许编写简洁和优雅的代码,同时使用用 Java 编写的现有库提供非常好的[互操作性](https://kotlinlang.org/docs/reference/java-interop.html)。 + +Spring 框架为 Kotlin 提供了一流的支持,并允许开发人员编写 Kotlin 应用程序,就好像 Spring 框架是一个原生 Kotlin 框架一样。除了 Java 之外,参考文档的大多数代码示例都是在 Kotlin 中提供的。 + +用 Kotlin 构建 Spring 应用程序的最简单方法是利用 Spring 引导和它的[dedicated Kotlin support](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-kotlin.html)。[这个全面的教程](https://spring.io/guides/tutorials/spring-boot-kotlin/)将教你如何使用[start.spring.io](https://start.spring.io/#!language=kotlin&type=gradle-project)用 Kotlin 构建 Spring 引导应用程序。 + +如果你需要支持,可以随时加入[Kotlin Slack](https://slack.kotlinlang.org/)的 # Spring 通道,或者使用`spring`和`kotlin`作为标记在[Stackoverflow](https://stackoverflow.com/questions/tagged/spring+kotlin)上提问。 + +### 1.1.所需经费 + +Spring 框架支持 Kotlin 1.3+,并且需要[`kotlin-stdlib`](https://search.maven.org/artifact/org.jetbrains.kotlin/kotlin-stdlib)(或其变体之一,例如[`kotlin-stdlib-jdk8`](https://search.maven.org/artifact/org.jetbrains.kotlin/kotlin-stdlib-jdk8))和[`kotlin-reflect`](https://search.maven.org/artifact/org.jetbrains.kotlin/kotlin-reflect)才能在 Classpath 上存在。如果你在[start.spring.io](https://start.spring.io/#!language=kotlin&type=gradle-project)上引导一个 Kotlin 项目,则默认情况下会提供它们。 + +| |对于使用 Jackson 对 Kotlin 类的 JSON 数据进行序列化或反序列化,需要[Jackson Kotlin module](https://github.com/FasterXML/jackson-module-kotlin)<br/>,因此,如果你有此需要,请确保将 `com.fasterxml.Jackson.module:Jackson-module- Kotlin `dependency 添加到你的项目中。<br/>在 Classpath 中找到时,它会自动注册。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 1.2.扩展 + +Kotlin [extensions](https://kotlinlang.org/docs/reference/extensions.html)提供了扩展具有附加功能的现有类的能力。 Spring 框架 Kotlin API 使用这些扩展来向现有 Spring API 添加新的 Kotlin 特定的便利。 + +[Spring Framework KDoc API](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/)列表和文档都是可用的 Kotlin 扩展和 DSL。 + +| |请记住, Kotlin 扩展需要导入才能使用。这意味着,<br/>例如,`GenericApplicationContext.registerBean` Kotlin 扩展<br/>只有当`org.springframework.context.support.registerBean`被导入时才可用。<br/>也就是说,与静态导入类似,IDE 在大多数情况下应该自动建议导入。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +例如,[Kotlin reified type parameters](https://kotlinlang.org/docs/reference/inline-functions.html#reified-type-parameters)为 JVM[泛型擦除](https://docs.oracle.com/javase/tutorial/java/generics/erasure.html)提供了一种变通方法,而 Spring 框架提供了一些扩展来利用这一特性。这允许更好的 Kotlin API`RestTemplate`、 Spring WebFlux 中的新`WebClient`以及其他各种 API。 + +| |其他库,例如 Reactor 和 Spring Data,也为它们的 API 提供了 Kotlin 扩展,因此总体上提供了更好的 Kotlin 开发体验。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------| + +要在 Java 中检索`User`对象的列表,通常需要编写以下内容: + +``` +Flux<User> users = client.get().retrieve().bodyToFlux(User.class) +``` + +对于 Kotlin 和 Spring 框架扩展,你可以改为编写以下内容: + +``` +val users = client.get().retrieve().bodyToFlux<User>() +// or (both are equivalent) +val users : Flux<User> = client.get().retrieve().bodyToFlux() +``` + +正如在 Java 中一样, Kotlin 中的`users`是强类型的,但是 Kotlin 聪明的类型推断允许更短的语法。 + +### 1.3.零安全 + +Kotlin 的关键特性之一是[null-safety](https://kotlinlang.org/docs/reference/null-safety.html),它在编译时干净地处理`null`值,而不是在运行时遇到著名的 `nullPointerexception’。这使得应用程序通过可否定性声明和表达“值或无值”语义而更安全,而不需要支付包装器的费用,例如`Optional`。( Kotlin 允许使用具有可空的值的函数结构。参见[comprehensive guide to Kotlin null-safety](https://www.baeldung.com/kotlin-null-safety)。) + +虽然 Java 不允许在其类型系统中表示空安全,但 Spring 框架通过在`org.springframework.lang`包中声明的对工具友好的注释提供了[null-safety of the whole Spring Framework API](core.html#null-safety)。默认情况下,来自 Kotlin 中使用的 Java API 的类型被识别为[platform types](https://kotlinlang.org/docs/reference/java-interop.html#null-safety-and-platform-types),对此可以放松空值检查。[Kotlin support for JSR-305 annotations](https://kotlinlang.org/docs/reference/java-interop.html#jsr-305-support)和 Spring 空值注释为 Kotlin 开发人员提供了整个 Spring Framework API 的空安全性,并具有在编译时处理`null`相关问题的优点。 + +| |诸如 Reactor 或 Spring Data 之类的库提供了空安全的 API 来利用此功能。| +|---|-----------------------------------------------------------------------------------------| + +你可以通过添加`-Xjsr305`编译器标志和以下选项来配置 JSR-305 检查:`-Xjsr305={strict|warn|ignore}`。 + +对于 Kotlin 版本 1.1+,默认行为与`-Xjsr305=warn`相同。在从 Spring API 推断出的 Kotlin 类型中,`strict`值需要将 Spring FrameworkAPI 的空安全性考虑在内,但在使用该值时,应该知道 Spring API 的零性声明即使在较小的版本之间也可以发展,并且将来可能会添加更多的检查。 + +| |目前还不支持泛型类型参数、varargs 和数组元素的可空性,<br/>,但应该在即将发布的版本中支持。有关最新信息,请参见[this discussion](https://github.com/Kotlin/KEEP/issues/79)。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 1.4.类和接口 + +Spring 框架支持各种 Kotlin 构造,例如通过主构造函数实例化 Kotlin 类、不可变类数据绑定和具有默认值的函数可选参数。 + +Kotlin 通过专用的`KotlinReflectionParameterNameDiscoverer`来识别参数名称,该参数名称允许查找接口方法参数名称,而不需要在编译过程中启用 Java8`-parameters`编译器标志。 + +你可以将配置类声明为[顶层或嵌套的,但不是内部的](https://kotlinlang.org/docs/reference/nested-classes.html),因为后者需要对外部类的引用。 + +### 1.5.注解 + +Spring 框架还利用[Kotlin null-safety](https://kotlinlang.org/docs/reference/null-safety.html)来确定是否需要 HTTP 参数,而无需显式地定义`required`属性。这意味着`@RequestParam name: String?`被视为不需要,相反,`@RequestParam name: String`被视为需要。 Spring 消息传递`@Header`注释上也支持此功能。 + +以类似的方式, Spring Bean 带有`@Autowired`、`@Bean`或`@Inject`的注入使用该信息来确定是否需要 Bean。 + +例如,`@Autowired lateinit var thing: Thing`意味着必须在应用程序上下文中注册类型`Thing`的 Bean,而`@Autowired lateinit var thing: Thing?`如果不存在这样的 Bean,则不会引发错误。 + +遵循相同的原则,`@Bean fun play(toy: Toy, car: Car?) = Baz(toy, Car)`意味着类型`Toy`的 Bean 必须在应用程序上下文中注册,而类型`Car`的 Bean 可能存在,也可能不存在。同样的行为也适用于自动连线构造函数参数。 + +| |如果在具有属性或主构造函数<br/>参数的类上使用 Bean 验证,则可能需要使用[注释使用-站点目标](https://kotlinlang.org/docs/reference/annotations.html#annotation-use-site-targets),<br/>,例如`@field:NotNull`或`@get:Size(min=5, max=15)`,如[这个堆栈溢出响应](https://stackoverflow.com/a/35853200/1092077)中所述。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 1.6. Bean 定义 DSL + +Spring Framework 支持通过使用 lambdas 作为 XML 或 Java 配置的替代方案(`@configuration` 和`@Bean`)以功能方式注册 bean。简而言之,它允许你用 lambda 注册 bean,lambda 充当`FactoryBean`。这种机制非常有效,因为它不需要任何反射或 CGlib 代理。 + +例如,在 Java 中,你可以编写以下内容: + +``` +class Foo {} + +class Bar { + private final Foo foo; + public Bar(Foo foo) { + this.foo = foo; + } +} + +GenericApplicationContext context = new GenericApplicationContext(); +context.registerBean(Foo.class); +context.registerBean(Bar.class, () -> new Bar(context.getBean(Foo.class))); +``` + +在 Kotlin 中,使用具体化的类型参数和`GenericApplicationContext` Kotlin 扩展,你可以改为编写以下内容: + +``` +class Foo + +class Bar(private val foo: Foo) + +val context = GenericApplicationContext().apply { + registerBean<Foo>() + registerBean { Bar(it.getBean()) } +} +``` + +当类`Bar`只有一个构造函数时,你甚至可以只指定 Bean 类,构造函数参数将根据类型自动连线: + +``` +val context = GenericApplicationContext().apply { + registerBean<Foo>() + registerBean<Bar>() +} +``` + +为了允许更多的声明性方法和更干净的语法, Spring Framework 提供了[Kotlin bean definition DSL](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.context.support/-bean-definition-dsl/)它通过一个干净的声明性 API 声明`ApplicationContextInitializer`,它允许你处理配置文件和`Environment`,以自定义如何注册 bean。 + +在下面的示例中,请注意: + +* 类型推理通常允许避免为 Bean 引用指定类型,如`ref("bazBean")` + +* 在本例中,可以使用 Kotlin 顶级函数使用`bean(::myRouter)`之类的可调用引用来声明 bean。 + +* 当指定`bean<Bar>()`或`bean(::myRouter)`时,参数将按类型自动连线。 + +* 只有在`foobar`配置文件处于活动状态时,才会注册`FooBar` Bean + +``` +class Foo +class Bar(private val foo: Foo) +class Baz(var message: String = "") +class FooBar(private val baz: Baz) + +val myBeans = beans { + bean<Foo>() + bean<Bar>() + bean("bazBean") { + Baz().apply { + message = "Hello world" + } + } + profile("foobar") { + bean { FooBar(ref("bazBean")) } + } + bean(::myRouter) +} + +fun myRouter(foo: Foo, bar: Bar, baz: Baz) = router { + // ... +} +``` + +| |此 DSL 是可编程的,这意味着它允许通过`if`表达式、`for`循环或任何其他 Kotlin 构造对 bean<br/>进行自定义注册逻辑。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------| + +然后,你可以使用这个`beans()`函数在应用程序上下文中注册 bean,如下例所示: + +``` +val context = GenericApplicationContext().apply { + myBeans.initialize(this) + refresh() +} +``` + +| |Spring boot 是基于 javaconfig 和[does not yet provide specific support for functional bean definition](https://github.com/spring-projects/spring-boot/issues/8115),<br/>,但可以通过 Spring boot 的`ApplicationContextInitializer`支持实验性地使用函数 Bean 定义。<br/>查看[这个堆栈溢出回答](https://stackoverflow.com/questions/45935931/how-to-use-functional-bean-definition-kotlin-dsl-with-spring-boot-and-spring-w/46033685#46033685)以获得更多详细信息和最新信息。另见[Spring Fu incubator](https://github.com/spring-projects/spring-fu)中开发的实验 Kofu DSL。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 1.7.万维网 + +#### 1.7.1.路由器 DSL + +Spring Framework 自带的路由器 DSL 有 3 种类型: + +* WebMVC.FNDSL with[router { }](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.web.servlet.function/router.html) + +* webflux.FN[Reactive](web-reactive.html#webflux-fn)dsl with[router { }](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/router.html) + +* webflux.FN[Coroutines](#coroutines)dsl with[coRouter { }](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/co-router.html) + +这些 DSL 允许你编写干净且惯用的 Kotlin 代码来构建`RouterFunction`实例,如下例所示: + +``` +@Configuration +class RouterRouterConfiguration { + + @Bean + fun mainRouter(userHandler: UserHandler) = router { + accept(TEXT_HTML).nest { + GET("/") { ok().render("index") } + GET("/sse") { ok().render("sse") } + GET("/users", userHandler::findAllView) + } + "/api".nest { + accept(APPLICATION_JSON).nest { + GET("/users", userHandler::findAll) + } + accept(TEXT_EVENT_STREAM).nest { + GET("/users", userHandler::stream) + } + } + resources("/**", ClassPathResource("static/")) + } +} +``` + +| |此 DSL 是可编程的,这意味着它允许通过`if`表达式、`for`循环或任何其他 Kotlin 构造对 bean<br/>进行自定义注册逻辑。当你需要根据动态数据(例如,来自数据库)注册路由时,这可能是有用的<br/>。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +具体例子见[MiXiT project](https://github.com/mixitconf/mixit/)。 + +#### 1.7.2.MockMVC DSL + +Kotlin DSL 通过 Kotlin 扩展提供,以便提供更惯用的 Kotlin API 并允许更好的可发现性(不使用静态方法)。 + +``` +val mockMvc: MockMvc = ... +mockMvc.get("/person/{name}", "Lee") { + secure = true + accept = APPLICATION_JSON + headers { + contentLanguage = Locale.FRANCE + } + principal = Principal { "foo" } +}.andExpect { + status { isOk } + content { contentType(APPLICATION_JSON) } + jsonPath("$.name") { value("Lee") } + content { json("""{"someBoolean": false}""", false) } +}.andDo { + print() +} +``` + +#### 1.7.3. Kotlin 脚本模板 + +Spring Framework 提供了一个[“ScriptemplateView”](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/view/script/ScriptTemplateView.html),它支持[JSR-223](https://www.jcp.org/en/jsr/detail?id=223)通过使用脚本引擎来呈现模板。 + +通过利用`scripting-jsr223`依赖关系,可以使用这样的特性来呈现带有[kotlinx.html](https://github.com/Kotlin/kotlinx.html)DSL 或 Kotlin 多行插值`String`的基于 Kotlin 的模板。 + +`build.gradle.kts` + +``` +dependencies { + runtime("org.jetbrains.kotlin:kotlin-scripting-jsr223:${kotlinVersion}") +} +``` + +配置通常使用`ScriptTemplateConfigurer`和`ScriptTemplateViewResolver`bean。 + +`KotlinScriptConfiguration.kt` + +``` +@Configuration +class KotlinScriptConfiguration { + + @Bean + fun kotlinScriptConfigurer() = ScriptTemplateConfigurer().apply { + engineName = "kotlin" + setScripts("scripts/render.kts") + renderFunction = "render" + isSharedEngine = false + } + + @Bean + fun kotlinScriptViewResolver() = ScriptTemplateViewResolver().apply { + setPrefix("templates/") + setSuffix(".kts") + } +} +``` + +有关更多详细信息,请参见[kotlin-script-templating](https://github.com/sdeleuze/kotlin-script-templating)示例项目。 + +#### 1.7.4. Kotlin 多平台序列化 + +在 Spring Framework5.3 中,[Kotlin multiplatform serialization](https://github.com/Kotlin/kotlinx.serialization)在 Spring MVC、 Spring WebFlux 和 Spring Messaging 中得到了支持。内置支持目前只针对 JSON 格式。 + +要启用它,请按照[那些指示](https://github.com/Kotlin/kotlinx.serialization#setup)添加相关的依赖项和插件。对于 Spring MVC 和 WebFlux,如果 Kotlin 序列化和 Jackson 都在 Classpath 中,那么它们将默认地被配置,因为 Kotlin 序列化被设计为仅序列化 Kotlin 用`@Serializable`注释的类。使用 Spring 消息传递,如果你想要自动配置,则确保 Jackson、GSON 或 JSONB 都不在 Classpath 中,如果需要配置 Jackson,则手动配置`KotlinSerializationJsonMessageConverter`。 + +### 1.8.协理 + +Kotlin [Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html)是 Kotlin 轻量级线程,允许以强制方式编写非阻塞代码。在语言端,挂起函数为异步操作提供了抽象,而在库端[Kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines)提供了[`async { }`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html)之类的函数和[`Flow`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html)之类的类型。 + +Spring 框架在以下范围上为协程提供支持: + +* [Deferred](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html)和[Flow](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html)返回 Spring MVC 和 WebFlux 中支持的值,注释`@Controller` + +* Spring MVC 和 WebFlux 注释`@Controller`中的挂起功能支持 + +* WebFlux[client](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.web.reactive.function.client/index.html)和[server](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/index.html)函数 API 的扩展。 + +* WebFlux.FN[coRouter { }](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/co-router.html)DSL + +* 挂起函数和`Flow`在 RSocket 中支持`@MessageMapping`注释方法 + +* [rsocketrequester’](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.messaging.rsocket/index.html)的扩展 + +#### 1.8.1.依赖关系 + +当`kotlinx-coroutines-core`和`kotlinx-coroutines-reactor`依赖项位于 Classpath 中时,将启用协程支持: + +`build.gradle.kts` + +``` +dependencies { + + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:${coroutinesVersion}") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:${coroutinesVersion}") +} +``` + +支持`1.4.0`及以上版本。 + +#### 1.8.2.反应性如何转化为协程? + +对于返回值,从 Active 到 CoroutineAPI 的转换如下: + +* `fun handler(): Mono<Void>`变为`suspend fun handler()` + +* `fun handler(): Mono<T>`变为`suspend fun handler(): T`或`suspend fun handler(): T?`取决于`Mono`是否为空(具有更静态类型的优点) + +* `fun handler(): Flux<T>`变为`fun handler(): Flow<T>` + +输入参数: + +* 如果不需要惰性,则`fun handler(mono: Mono<T>)`变为`fun handler(value: T)`,因为可以调用挂起的函数来获得值参数。 + +* 如果需要惰性,则`fun handler(mono: Mono<T>)`变为`fun handler(supplier: suspend () → T)`或`fun handler(supplier: suspend () → T?)` + +[`Flow`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/index.html)在协程世界中是`Flux`等价的,适用于冷热气流、有限气流或无限气流,有以下主要区别: + +* `Flow`是推拉式的,而`Flux`是推拉式的混合动力 + +* 背压是通过悬挂功能实现的。 + +* `Flow`只有一个[single suspending `collect` method](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/collect.html),运算符实现为[extensions](https://kotlinlang.org/docs/reference/extensions.html) + +* [运营商很容易实现。](https://github.com/Kotlin/kotlinx.coroutines/tree/master/kotlinx-coroutines-core/common/src/flow/operators)感谢协程 + +* 扩展允许将自定义运算符添加到`Flow` + +* 收集操作是挂起的功能 + +* [`map` operator](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/map.html)支持异步操作(不需要`flatMap`),因为它需要一个挂起的函数参数 + +阅读这篇关于[Going Reactive with Spring, Coroutines and Kotlin Flow](https://spring.io/blog/2019/04/12/going-reactive-with-spring-coroutines-and-kotlin-flow)的博客文章,了解更多详细信息,包括如何与协程同时运行代码。 + +#### 1.8.3.控制器 + +下面是一个协程`@RestController`的例子。 + +``` +@RestController +class CoroutinesRestController(client: WebClient, banner: Banner) { + + @GetMapping("/suspend") + suspend fun suspendingEndpoint(): Banner { + delay(10) + return banner + } + + @GetMapping("/flow") + fun flowEndpoint() = flow { + delay(10) + emit(banner) + delay(10) + emit(banner) + } + + @GetMapping("/deferred") + fun deferredEndpoint() = GlobalScope.async { + delay(10) + banner + } + + @GetMapping("/sequential") + suspend fun sequential(): List<Banner> { + val banner1 = client + .get() + .uri("/suspend") + .accept(MediaType.APPLICATION_JSON) + .awaitExchange() + .awaitBody<Banner>() + val banner2 = client + .get() + .uri("/suspend") + .accept(MediaType.APPLICATION_JSON) + .awaitExchange() + .awaitBody<Banner>() + return listOf(banner1, banner2) + } + + @GetMapping("/parallel") + suspend fun parallel(): List<Banner> = coroutineScope { + val deferredBanner1: Deferred<Banner> = async { + client + .get() + .uri("/suspend") + .accept(MediaType.APPLICATION_JSON) + .awaitExchange() + .awaitBody<Banner>() + } + val deferredBanner2: Deferred<Banner> = async { + client + .get() + .uri("/suspend") + .accept(MediaType.APPLICATION_JSON) + .awaitExchange() + .awaitBody<Banner>() + } + listOf(deferredBanner1.await(), deferredBanner2.await()) + } + + @GetMapping("/error") + suspend fun error() { + throw IllegalStateException() + } + + @GetMapping("/cancel") + suspend fun cancel() { + throw CancellationException() + } + +} +``` + +还支持`@Controller`的视图呈现。 + +``` +@Controller +class CoroutinesViewController(banner: Banner) { + + @GetMapping("/") + suspend fun render(model: Model): String { + delay(10) + model["banner"] = banner + return "index" + } +} +``` + +#### 1.8.4.WebFlux.FN + +下面是通过[coRouter { }](https://docs.spring.io/spring-framework/docs/5.3.16/kdoc-api/spring-framework/org.springframework.web.reactive.function.server/co-router.html)DSL 和相关处理程序定义的协程路由器的示例。 + +``` +@Configuration +class RouterConfiguration { + + @Bean + fun mainRouter(userHandler: UserHandler) = coRouter { + GET("/", userHandler::listView) + GET("/api/user", userHandler::listApi) + } +} +``` + +``` +class UserHandler(builder: WebClient.Builder) { + + private val client = builder.baseUrl("...").build() + + suspend fun listView(request: ServerRequest): ServerResponse = + ServerResponse.ok().renderAndAwait("users", mapOf("users" to + client.get().uri("...").awaitExchange().awaitBody<User>())) + + suspend fun listApi(request: ServerRequest): ServerResponse = + ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyAndAwait( + client.get().uri("...").awaitExchange().awaitBody<User>()) +} +``` + +#### 1.8.5.交易 + +通过 Spring 框架 5.2 提供的反应式事务管理的编程变体,支持协程上的事务。 + +对于挂起的函数,提供了`TransactionalOperator.executeAndAwait`扩展。 + +``` +import org.springframework.transaction.reactive.executeAndAwait + +class PersonRepository(private val operator: TransactionalOperator) { + + suspend fun initDatabase() = operator.executeAndAwait { + insertPerson1() + insertPerson2() + } + + private suspend fun insertPerson1() { + // INSERT SQL statement + } + + private suspend fun insertPerson2() { + // INSERT SQL statement + } +} +``` + +对于 Kotlin `Flow`,提供了一个`Flow<T>.transactional`扩展。 + +``` +import org.springframework.transaction.reactive.transactional + +class PersonRepository(private val operator: TransactionalOperator) { + + fun updatePeople() = findPeople().map(::updatePerson).transactional(operator) + + private fun findPeople(): Flow<Person> { + // SELECT SQL statement + } + + private suspend fun updatePerson(person: Person): Person { + // UPDATE SQL statement + } +} +``` + +### 1.9. Spring Kotlin 中的项目 + +本节提供了一些值得在 Kotlin 中开发 Spring 项目的具体提示和建议。 + +#### 1.9.1.默认情况下为 final + +默认情况下,[all classes in Kotlin are `final`](https://discuss.kotlinlang.org/t/classes-final-by-default/166)。类上的`open`修饰符与 Java 的`final`相反:它允许其他人继承这个类。这也适用于成员函数,因为它们需要标记为`open`才能被重写。 + +虽然 Kotlin 的 JVM 友好设计通常与 Spring 无摩擦,但如果不考虑这一事实,则此特定的 Kotlin 特性可以阻止应用程序启动。这是因为 Spring bean(例如`@Configuration`注释类,由于技术原因,默认情况下需要在运行时进行扩展)通常是由 CGLIB 代理的。解决方法是在 Spring bean 的每个类和成员函数上添加一个`open`关键字,这些类和成员函数是由 CGlib 代理的,这可能很快会变得很痛苦,并且违反 Kotlin 保持代码简洁和可预测的原则。 + +| |也可以通过使用`@Configuration(proxyBeanMethods = false)`来避免配置类的 CGLIB 代理。<br/>查看[“ProxyBeanMethods”Javadoc](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/annotation/Configuration.html#proxyBeanMethods--)以获得更多详细信息。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +幸运的是, Kotlin 提供了一个[`kotlin-spring`](https://kotlinlang.org/docs/reference/compiler-plugins.html#kotlin-spring-compiler-plugin)插件(`kotlin-allopen`插件的预先配置版本),该插件自动为使用以下注释之一进行注释或元注释的类型打开类及其成员函数: + +* `@Component` + +* `@Async` + +* `@Transactional` + +* `@Cacheable` + +元注释支持意味着使用`@Configuration`、`@Controller`、<restcontroller`、或`@Repository`进行注释的类型将自动打开,因为这些注释是用`@Component`进行元注释的。 + +[start.spring.io](https://start.spring.io/#!language=kotlin&type=gradle-project)默认情况下启用`kotlin-spring`插件。因此,在实践中,你可以在不添加任何`open`关键字的情况下编写 Kotlin bean,就像在 Java 中一样。 + +| |Spring 框架文档中的 Kotlin 代码示例并未在类及其成员函数上明确指定“open”。示例是使用`kotlin-allopen`插件为<br/>项目编写的,因为这是最常用的设置。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.9.2.使用不可变类实例实现持久性 + +在 Kotlin 中,在主构造函数中声明只读属性是很方便的,并且被认为是一种最佳实践,如下例所示: + +``` +class Person(val name: String, val age: Int) +``` + +你可以选择添加[the `data` keyword](https://kotlinlang.org/docs/reference/data-classes.html),以使编译器自动从主构造函数中声明的所有属性派生出以下成员: + +* `equals()`和`hashCode()` + +* `toString()`的形式`"User(name=John, age=42)"` + +* `componentN()`与其声明顺序中的属性对应的函数 + +* `copy()`函数 + +正如下面的示例所示,这允许对单个属性进行简单的更改,即使`Person`属性是只读的: + +``` +data class Person(val name: String, val age: Int) + +val jack = Person(name = "Jack", age = 1) +val olderJack = jack.copy(age = 2) +``` + +常见的持久性技术(例如 JPA)需要一个默认的构造函数,从而阻止了这种设计。幸运的是,对于这个[“默认构造函数地狱”](https://stackoverflow.com/questions/32038177/kotlin-with-jpa-default-constructor-hell)有一种解决方法,因为 Kotlin 提供了一个[`kotlin-jpa`](https://kotlinlang.org/docs/reference/compiler-plugins.html#kotlin-jpa-compiler-plugin)插件,该插件为带有 JPA 注释的类生成合成的无 arg 构造函数。 + +如果需要为其他持久性技术利用这种机制,可以配置[`kotlin-noarg`](https://kotlinlang.org/docs/reference/compiler-plugins.html#how-to-use-no-arg-plugin)插件。 + +| |在 Kay Release Train 中, Spring 数据支持 Kotlin 不可变类实例,并且如果模块使用 Spring 数据对象映射`kotlin-noarg`(例如 MongoDB、Redis、Cassandra 和其他),则`kotlin-noarg`不需要<gt r=“261”插件。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.9.3.注入依赖项 + +我们的建议是尝试使用`val`只读(并且在可能的情况下不可为空)[properties](https://kotlinlang.org/docs/reference/properties.html)的构造函数注入,如下例所示: + +``` +@Component +class YourBean( + private val mongoTemplate: MongoTemplate, + private val solrClient: SolrClient +) +``` + +| |带有单个构造函数的类的参数自动自动连线。<br/>这就是为什么在上面的`@Autowired constructor`示例中不需要显式的`@Autowired constructor`的原因。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +如果确实需要使用字段注入,可以使用`lateinit var`构造,如下例所示: + +``` +@Component +class YourBean { + + @Autowired + lateinit var mongoTemplate: MongoTemplate + + @Autowired + lateinit var solrClient: SolrClient +} +``` + +#### 1.9.4.注入配置属性 + +是用于[字符串插值](https://kotlinlang.org/docs/reference/idioms.html#string-interpolation)的保留字符。 + +字符。 + +| |如果使用 Spring boot,则可能应该使用[@configrationProperties](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html#boot-features-external-config-typesafe-configuration-properties)而不是`@Value`注释。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +作为一种选择,你可以通过声明以下配置 bean 来定制属性占位符前缀: + +``` +@Bean +fun propertyConfigurer() = PropertySourcesPlaceholderConfigurer().apply { + setPlaceholderPrefix("%{") +} +``` + +你可以使用配置 bean 自定义使用`${…​}`语法的现有代码(例如 Spring 引导执行器或`@LocalServerPort`),如下例所示: + +``` +@Bean +fun kotlinPropertyConfigurer() = PropertySourcesPlaceholderConfigurer().apply { + setPlaceholderPrefix("%{") + setIgnoreUnresolvablePlaceholders(true) +} + +@Bean +fun defaultPropertyConfigurer() = PropertySourcesPlaceholderConfigurer() +``` + +#### 1.9.5.已检查的异常 + +Java 和[Kotlin exception handling](https://kotlinlang.org/docs/reference/exceptions.html)非常接近,主要区别在于 Kotlin 将所有异常都视为未经检查的异常。但是,当使用代理对象(例如,用`@Transactional`注释的类或方法)时,抛出的选中异常将默认包装在`UndeclaredThrowableException`中。 + +要像在 Java 中一样获得原始抛出的异常,应该使用[`@Throws`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.jvm/-throws/index.html)对方法进行注释,以显式地指定抛出的检查过的异常(例如`@Throws(IOException::class)`)。 + +#### 1.9.6.注释数组属性 + +Kotlin 注释大多类似于 Java 注释,但是数组属性(在 Spring 中广泛使用)的行为是不同的。正如在[Kotlin documentation](https://kotlinlang.org/docs/reference/annotations.html)中所解释的,与其他属性不同,你可以省略`value`属性名称,并将其指定为`vararg`参数。 + +要理解这意味着什么,可以将`@RequestMapping`(这是使用最广泛的 Spring 注释之一)作为示例。此 Java 注释声明如下: + +``` +public @interface RequestMapping { + + @AliasFor("path") + String[] value() default {}; + + @AliasFor("value") + String[] path() default {}; + + RequestMethod[] method() default {}; + + // ... +} +``` + +`@RequestMapping`的典型用例是将处理程序方法映射到特定的路径和方法。在 Java 中,你可以为 Annotation Array 属性指定一个值,然后将其自动转换为一个数组。 + +这就是为什么我们可以写“@requestmapping(value=“/toys”,method=requestmethod.get)”或“@requestmapping(path=“/toys”,method=requestmethod.get)”。 + +但是,在 Kotlin 中,你必须写`@RequestMapping("/toys", method = [RequestMethod.GET])`或`@RequestMapping(path = ["/toys"], method = [RequestMethod.GET])`(需要用命名的数组属性指定方括号)。 + +对于这个特定的`method`属性(最常见的一种),另一种选择是使用快捷方式注释,例如`@GetMapping`,`@PostMapping`等。 + +| |如果`@RequestMapping``method`属性未指定,则所有 HTTP 方法都将<br/>进行匹配,而不仅仅是`GET`方法。| +|---|------------------------------------------------------------------------------------------------------------------------------| + +#### 1.9.7.测试 + +本节讨论使用 Kotlin 和 Spring 框架的组合进行的测试。推荐的测试框架是[JUnit 5](https://junit.org/junit5/)以及用于模拟的[Mockk](https://mockk.io/)。 + +| |如果你正在使用 Spring 引导,请参见[这个相关的文档](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-kotlin-testing)。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 构造函数注入 + +正如在[专用部分](testing.html#testcontext-junit-jupiter-di#spring-web-reactive)中所描述的,JUnit5 允许构造函数注入 bean,这在 Kotlin 中非常有用,以便使用`val`而不是`lateinit var`。你可以使用[@TestConstructor](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/context/TestConstructor.html)为所有参数启用自动布线。 + +``` +@SpringJUnitConfig(TestConfig::class) +@TestConstructor(autowireMode = AutowireMode.ALL) +class OrderServiceIntegrationTests(val orderService: OrderService, + val customerService: CustomerService) { + + // tests that use the injected OrderService and CustomerService +} +``` + +##### `PER_CLASS`生命周期 + +Kotlin 允许你在 backticks 之间指定有意义的测试函数名。在 JUnit5 中, Kotlin 测试类可以使用`@TestInstance(TestInstance.Lifecycle.PER_CLASS)`注释来启用测试类的单实例,这允许在非静态方法上使用`@BeforeAll`和`@AfterAll`注释,这很好地适合 Kotlin。 + +你还可以将默认行为更改为`PER_CLASS`,这要感谢带有`junit-platform.properties`属性的`junit.jupiter.testinstance.lifecycle.default = per_class`文件。 + +下面的示例演示了关于非静态方法的`@BeforeAll`和`@AfterAll`注释: + +``` +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class IntegrationTests { + + val application = Application(8181) + val client = WebClient.create("http://localhost:8181") + + @BeforeAll + fun beforeAll() { + application.start() + } + + @Test + fun `Find all users on HTML page`() { + client.get().uri("/users") + .accept(TEXT_HTML) + .retrieve() + .bodyToMono<String>() + .test() + .expectNextMatches { it.contains("Foo") } + .verifyComplete() + } + + @AfterAll + fun afterAll() { + application.stop() + } +} +``` + +##### 类似规范的测试 + +你可以使用 JUnit5 和 Kotlin 创建类似于规范的测试。下面的示例展示了如何做到这一点: + +``` +class SpecificationLikeTests { + + @Nested + @DisplayName("a calculator") + inner class Calculator { + val calculator = SampleCalculator() + + @Test + fun `should return the result of adding the first number to the second number`() { + val sum = calculator.sum(2, 4) + assertEquals(6, sum) + } + + @Test + fun `should return the result of subtracting the second number from the first number`() { + val subtract = calculator.subtract(4, 2) + assertEquals(2, subtract) + } + } +} +``` + +##### `WebTestClient` Kotlin 中的类型推断问题 + +由于[类型推断问题](https://youtrack.jetbrains.com/issue/KT-5464),你必须使用 Kotlin `expectBody`扩展(例如`.expectBody<String>().isEqualTo("toys")`),因为它为 Java API 的 Kotlin 问题提供了解决方法。 + +另见相关的[SPR-16057](https://jira.spring.io/browse/SPR-16057)问题。 + +### 1.10.开始 + +学习如何使用 Kotlin 构建 Spring 应用程序的最简单的方法是遵循[专门的教程](https://spring.io/guides/tutorials/spring-boot-kotlin/)。 + +#### 1.10.1.`start.spring.io` + +在 Kotlin 中启动新 Spring 框架项目的最简单的方法是在[start.spring.io](https://start.spring.io/#!language=kotlin&type=gradle-project)上创建新的 Spring Boot2 项目。 + +#### 1.10.2.选择网络口味 + +Spring Framework 现在带有两个不同的 Web 栈:[Spring MVC](web.html#mvc)和[Spring WebFlux](web-reactive.html#spring-web-reactive)。 + +Spring 如果你希望创建将处理延迟、长寿命连接、流场景的应用程序,或者如果你希望使用具有 Web 功能的 DSL,则建议使用 WebFlux Kotlin。 + +对于其他用例,特别是如果正在使用诸如 JPA 这样的阻塞技术, Spring MVC 及其基于注释的编程模型是推荐的选择。 + +### 1.11.资源 + +我们向学习如何使用 Kotlin 和 Spring 框架构建应用程序的人们推荐以下资源: + +* [Kotlin language reference](https://kotlinlang.org/docs/reference/) + +* [Kotlin Slack](https://slack.kotlinlang.org/)(带专用 # Spring 频道) + +* [Stackoverflow, with `spring` and `kotlin` tags](https://stackoverflow.com/questions/tagged/spring+kotlin) + +* [Try Kotlin in your browser](https://play.kotlinlang.org/) + +* [Kotlin blog](https://blog.jetbrains.com/kotlin/) + +* [Awesome Kotlin](https://kotlin.link/) + +#### 1.11.1.例子 + +以下 GitHub 项目提供了你可以学习甚至扩展的示例: + +* [spring-boot-kotlin-demo](https://github.com/sdeleuze/spring-boot-kotlin-demo):常规 Spring 引导和 Spring 数据 JPA 项目 + +* [mixit](https://github.com/mixitconf/mixit): Spring Boot2,WebFlux,和 Reactive Spring Data MongoDB + +* [spring-kotlin-functional](https://github.com/sdeleuze/spring-kotlin-functional):独立的 WebFlux 和 Functional Bean 定义 DSL + +* [spring-kotlin-fullstack](https://github.com/sdeleuze/spring-kotlin-fullstack):使用 Kotlin2js 代替 JavaScript 或 TypeScript 作为前端的 webflux Kotlin fullstack 示例 + +* [spring-petclinic-kotlin](https://github.com/spring-petclinic/spring-petclinic-kotlin): Spring PetClinic 样本申请的 Kotlin 版本 + +* [spring-kotlin-deepdive](https://github.com/sdeleuze/spring-kotlin-deepdive):引导 1.0 和 Java 到引导 2.0 和 Kotlin 的逐步迁移指南 + +* [spring-cloud-gcp-kotlin-app-sample](https://github.com/spring-cloud/spring-cloud-gcp/tree/master/spring-cloud-gcp-kotlin-samples/spring-cloud-gcp-kotlin-app-sample): Spring 启动与谷歌云平台集成 + +#### 1.11.2.问题 + +以下清单对与 Spring 和 Kotlin 支持有关的未决问题进行了分类: + +* Spring Framework + + * [Unable to use WebTestClient with mock server in Kotlin](https://github.com/spring-projects/spring-framework/issues/20606) + + * [在泛型、变量和数组元素级别支持零安全](https://github.com/spring-projects/spring-framework/issues/20496) + +* Kotlin + + * [Parent issue for Spring Framework support](https://youtrack.jetbrains.com/issue/KT-6380) + + * [Kotlin requires type inference where Java doesn’t](https://youtrack.jetbrains.com/issue/KT-5464) + + * [具有开放类的智能强制回归](https://youtrack.jetbrains.com/issue/KT-20283) + + * [不可能传递不是所有的 SAM 参数作为函数](https://youtrack.jetbrains.com/issue/KT-14984) + + * [通过脚本变量直接支持 JSR223 绑定](https://youtrack.jetbrains.com/issue/KT-15125) + + * [Kotlin properties do not override Java-style getters and setters](https://youtrack.jetbrains.com/issue/KT-6653) + +## 2. Apache Groovy + +Groovy 是一种功能强大的、可选类型的动态语言,具有静态类型和静态编译功能。它提供了简洁的语法,并与任何现有的 Java 应用程序平滑地集成在一起。 + +Spring 框架提供了一个专用的`ApplicationContext`,该框架支持基于 Groovy Bean 定义的 DSL。有关更多详细信息,请参见[The Groovy Bean Definition DSL](core.html#groovy-bean-definition-dsl)。 + +对 Groovy 的进一步支持,包括用 Groovy 编写的 bean、可刷新的脚本 bean,以及[动态语言支持](#dynamic-language)中提供的更多支持。 + +## 3. 动态语言支持 + +Spring 为使用已经通过使用动态语言(例如 Groovy)定义的类和对象提供了全面的支持。这种支持使你能够在受支持的动态语言中编写任意数量的类,并让 Spring 容器透明地实例化、配置和依赖注入结果对象。 + +Spring 的脚本支持主要针对 Groovy 和 BeanShell。除了那些特别支持的语言之外,JSR-223 脚本机制还支持与任何具有 JSR-223 功能的语言提供程序的集成(从 Spring 4.2 开始),例如 JRuby。 + +你可以在[Scenarios](#dynamic-language-scenarios)中找到充分工作的示例,说明这种动态语言支持可以立即发挥作用。 + +### 3.1.第一个例子 + +本章的主要内容是详细描述动态语言支持。在深入研究动态语言支持的所有细节和细节之前,我们先看一个在动态语言中定义的 Bean 的快速示例。这第一个 Bean 的动态语言是 Groovy。(这个示例的基础取自 Spring 测试套件。如果你想在任何其他受支持的语言中看到等效的示例,请查看源代码)。 + +下一个示例显示了 Groovy Bean 将要实现的`Messenger`接口。请注意,这个接口是用纯 Java 定义的。通过引用`Messenger`注入的依赖对象不知道底层实现是 Groovy 脚本。下面的清单显示了`Messenger`接口: + +``` +package org.springframework.scripting; + +public interface Messenger { + + String getMessage(); +} +``` + +下面的示例定义了一个对`Messenger`接口具有依赖关系的类: + +``` +package org.springframework.scripting; + +public class DefaultBookingService implements BookingService { + + private Messenger messenger; + + public void setMessenger(Messenger messenger) { + this.messenger = messenger; + } + + public void processBooking() { + // use the injected Messenger object... + } +} +``` + +下面的示例在 Groovy 中实现了`Messenger`接口: + +``` +// from the file 'Messenger.groovy' +package org.springframework.scripting.groovy; + +// import the Messenger interface (written in Java) that is to be implemented +import org.springframework.scripting.Messenger + +// define the implementation in Groovy +class GroovyMessenger implements Messenger { + + String message +} +``` + +| |要使用自定义的动态语言标记来定义动态语言支持的 bean,你需要在 Spring XML 配置文件的顶部具有 XML 模式序言。<br/>还需要使用 Spring `ApplicationContext`实现作为你的 IOC<br/>容器。支持使用带有普通`BeanFactory`实现的动态语言支持的 bean,但是你必须管理 Spring 内部<br/>的管道才能这样做。<br/><br/>有关基于模式的配置的更多信息,请参见[基于 XML 模式的配置](#xsd-schemas-lang)。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +最后,下面的示例展示了 Bean 定义,这些定义会将 Groovy 定义的`Messenger`实现注入到“DefaultBookingService”类的实例中: + +``` +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:lang="http://www.springframework.org/schema/lang" + xsi:schemaLocation=" + http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/lang https://www.springframework.org/schema/lang/spring-lang.xsd"> + + <!-- this is the bean definition for the Groovy-backed Messenger implementation --> + <lang:groovy id="messenger" script-source="classpath:Messenger.groovy"> + <lang:property name="message" value="I Can Do The Frug" /> + </lang:groovy> + + <!-- an otherwise normal bean that will be injected by the Groovy-backed Messenger --> + <bean id="bookingService" class="x.y.DefaultBookingService"> + <property name="messenger" ref="messenger" /> + </bean> + +</beans> +``` + +`bookingService` Bean(a`DefaultBookingService`)现在可以正常使用其私有`messenger`成员变量,因为注入到它的`Messenger`实例是`Messenger`实例。这里没有什么特别的——只有普通的 Java 和普通的 Groovy。 + +希望前面的 XML 片段是不言自明的,但如果不是,也不要过分担心。继续阅读关于前面配置的原因和原因的深入细节。 + +### 3.2.定义由动态语言支持的 bean + +本节详细描述了如何在任何受支持的动态语言中定义 Spring-managed bean。 + +请注意,本章并不试图解释所支持的动态语言的语法和习语。例如,如果你想使用 Groovy 在应用程序中编写某些类,我们假设你已经了解 Groovy。如果你需要更多关于动态语言本身的详细信息,请参阅本章末尾的[更多资源](#dynamic-language-resources)。 + +#### 3.2.1.共同概念 + +使用支持动态语言的 bean 所涉及的步骤如下: + +1. 为动态语言编写测试源代码(自然). + +2. 然后编写动态语言的源代码本身. + +3. 通过在 XML 配置中使用适当的`<lang:language/>`元素来定义你的动态语言支持的 bean(你可以通过使用 Spring API 以编程方式定义这样的 bean,尽管你将不得不查询源代码以了解如何做到这一点,因为本章不涉及这种类型的高级配置)。请注意,这是一个迭代步骤。对于每个动态语言源文件,至少需要一个 Bean 定义(尽管多个 Bean 定义可以引用相同的源文件)。 + +前两个步骤(测试和编写动态语言源文件)超出了本章的范围。请参阅你所选择的动态语言的语言规范和参考手册,并继续开发你的动态语言源文件。不过,你首先想阅读本章的其余部分,因为 Spring 的动态语言支持确实对动态语言源文件的内容进行了一些(小)假设。 + +##### \<lang:language/\>元素 + +在[前一节](#dynamic-language-beans-concepts)列表中的最后一步涉及定义动态语言支持的 Bean 定义,对于每个要配置的 Bean 定义一个(这与普通的 JavaBean 配置没有什么不同)。但是,你可以使用`<lang:language/>`元素来定义支持动态语言的 Bean,而不是指定要由容器实例化和配置的类的完全限定类名称。 + +每个受支持的语言都有一个对应的`<lang:language/>`元素: + +* `<lang:groovy/>` + +* `<lang:bsh/>` + +* `<lang:std/>`(JSR-223,例如使用 JRuby) + +可用于配置的确切属性和子元素取决于 Bean 在哪种语言中定义的准确(本章后面的语言特定部分详细介绍了这一点)。 + +##### 可刷新的咖啡豆 + +Spring 中动态语言支持的最引人注目的增值之一(或许也是唯一的)是“可刷新 Bean”特性。 + +可刷新的 Bean 是动态语言支持的 Bean。 Bean 通过少量配置,支持动态语言的动态文件可以监视其底层源文件资源中的更改,然后在动态语言源文件发生更改时重新加载自身(例如,当你编辑和保存对文件系统上的文件的更改时)。 + +这使你能够将任意数量的动态语言源文件部署为应用程序的一部分,配置 Spring 容器以创建由动态语言源文件支持的 bean(使用本章中描述的机制),以及(稍后, Bean 随着需求的变化或其他一些外部因素起作用),编辑动态语言源文件,并将它们所做的任何更改反映在由所更改的动态语言源文件支持的 Bean 中。没有必要关闭正在运行的应用程序(或者在 Web 应用程序的情况下重新部署)。 Bean 修改后的动态语言支持的 Bean 从更改后的动态语言源文件中获取新的状态和逻辑。 + +| |默认情况下,此功能是关闭的。| +|---|-------------------------------| + +现在,我们可以看看一个示例,看看开始使用可刷新 bean 是多么容易。要打开可刷新的 bean 特性,你必须在 Bean 定义的`<lang:language/>`元素上精确地指定一个附加属性。因此,如果我们坚持使用本章前面的[the example](#dynamic-language-a-first-example),下面的示例展示了我们将在 Spring XML 配置中进行哪些更改以实现可刷新的 bean: + +``` +<beans> + + <!-- this bean is now 'refreshable' due to the presence of the 'refresh-check-delay' attribute --> + <lang:groovy id="messenger" + refresh-check-delay="5000" <!-- switches refreshing on with 5 seconds between checks --> + script-source="classpath:Messenger.groovy"> + <lang:property name="message" value="I Can Do The Frug" /> + </lang:groovy> + + <bean id="bookingService" class="x.y.DefaultBookingService"> + <property name="messenger" ref="messenger" /> + </bean> + +</beans> +``` + +这真的是你所要做的一切。在“Messenger” Bean 定义中定义的`refresh-check-delay`属性是对底层动态语言源文件进行任何更改后刷新 Bean 的毫秒数。你可以通过为“刷新-检查-延迟”属性分配一个负值来关闭刷新行为。请记住,默认情况下,刷新行为是禁用的。如果不想要刷新行为,请不要定义属性。 + +如果我们接着运行下面的应用程序,我们就可以使用这个可刷新的特性。(请原谅下一段代码中的“跳过环到暂停执行”的恶作剧。)`System.in.read()`调用仅存在于此,以便当你(此场景中的开发人员)关闭并编辑底层动态语言源文件时,程序的执行暂停当程序恢复执行时,在动态语言支持的 Bean 上触发刷新。 + +下面的清单显示了这个示例应用程序: + +``` +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.scripting.Messenger; + +public final class Boot { + + public static void main(final String[] args) throws Exception { + ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml"); + Messenger messenger = (Messenger) ctx.getBean("messenger"); + System.out.println(messenger.getMessage()); + // pause execution while I go off and make changes to the source file... + System.in.read(); + System.out.println(messenger.getMessage()); + } +} +``` + +然后,为了本例的目的,假设对`Messenger`方法的所有调用都必须进行更改,以便消息被引号包围。下面的清单显示了在暂停执行程序时,你(开发人员)应该对`Messenger.groovy`源文件进行的更改: + +``` +package org.springframework.scripting + +class GroovyMessenger implements Messenger { + + private String message = "Bingo" + + public String getMessage() { + // change the implementation to surround the message in quotes + return "'" + this.message + "'" + } + + public void setMessage(String message) { + this.message = message + } +} +``` + +当程序运行时,输入暂停前的输出将是`I Can Do The Frug`。在对源文件进行更改并将其保存并且程序恢复执行之后,在动态语言支持的 `Messenger’实现上调用`getMessage()`方法的结果是`'I Can Do The Frug'`(请注意,其中包含了附加的引号)。 + +如果对脚本的更改发生在`refresh-check-delay`值的窗口内,则不会触发刷新。 Bean 在调用支持动态语言的方法之前,实际上不会接收到对脚本的更改。 Bean 只有当在支持动态语言的方法上调用方法时,它才会检查其底层脚本源是否已更改。与刷新脚本有关的任何异常(例如遇到编译错误或发现脚本文件已被删除)都会导致将致命异常传播到调用代码中。 + +Bean 前面描述的可刷新行为不适用于使用`<lang:inline-script/>`元素符号定义的动态语言源文件(参见[内联动态语言源文件](#dynamic-language-beans-inline))。此外,它仅适用于实际可以检测到对基础源文件的更改的 bean(例如,通过检查文件系统上存在的动态语言源文件的最后修改日期的代码)。 + +##### 内联动态语言源文件 + +动态语言支持还可以迎合直接嵌入在 Spring Bean 定义中的动态语言源文件。更具体地说,`<lang:inline-script/>` 元素允许你在 Spring 配置文件中立即定义动态语言源。一个示例可能会说明内联脚本功能是如何工作的: + +``` +<lang:groovy id="messenger"> + <lang:inline-script> + +package org.springframework.scripting.groovy; + +import org.springframework.scripting.Messenger + +class GroovyMessenger implements Messenger { + String message +} + + </lang:inline-script> + <lang:property name="message" value="I Can Do The Frug" /> +</lang:groovy> +``` + +如果我们不去考虑在 Spring 配置文件中定义动态语言源是否是一种好的实践的问题,`<lang:inline-script/>`元素在某些场景中可能是有用的。例如,我们可能希望快速地将 Spring `Validator`实现添加到 Spring MVC`Controller`中。这只是一个时刻的工作,使用内联资源。(例如,见[脚本验证器](#dynamic-language-scenarios-validators)。 + +##### 在支持动态语言的 bean 上下文中理解构造函数注入 + +关于 Spring 的动态语言支持,有一件非常重要的事情需要注意。也就是说,你不能(当前)向支持动态语言的 bean 提供构造函数参数(因此,对于支持动态语言的 bean,构造函数注入是不可用的)。为了使构造函数和属性的这种特殊处理 100% 清晰,以下代码和配置的混合不起作用: + +一种行不通的方法 + +``` +// from the file 'Messenger.groovy' +package org.springframework.scripting.groovy; + +import org.springframework.scripting.Messenger + +class GroovyMessenger implements Messenger { + + GroovyMessenger() {} + + // this constructor is not available for Constructor Injection + GroovyMessenger(String message) { + this.message = message; + } + + String message + + String anotherMessage +} +``` + +``` +<lang:groovy id="badMessenger" + script-source="classpath:Messenger.groovy"> + <!-- this next constructor argument will not be injected into the GroovyMessenger --> + <!-- in fact, this isn't even allowed according to the schema --> + <constructor-arg value="This will not work" /> + + <!-- only property values are injected into the dynamic-language-backed object --> + <lang:property name="anotherMessage" value="Passed straight through to the dynamic-language-backed object" /> + +</lang> +``` + +在实践中,这种限制并不像最初出现的那样重要,因为 Setter 注入是绝大多数开发人员所青睐的注入风格(关于这是否是一件好事的讨论,我们将留待以后讨论)。 + +#### 3.2.2.groovy beans + +本节描述如何使用 Spring 中在 Groovy 中定义的 bean。 + +Groovy 主页包括以下描述: + +Groovy 是一种面向 Java2 平台的敏捷动态语言,它具有许多人们非常喜欢的 Python、Ruby 和 Smalltalk 等语言的特性,使 Java 开发人员可以使用类似 Java 的语法来使用这些特性。” + +如果你直接从顶部阅读了这一章,那么你已经获得了支持 Groovy-Dynamic-Language 的[seen an example](#dynamic-language-a-first-example) Bean。现在考虑另一个示例(再次使用 Spring 测试套件中的示例): + +``` +package org.springframework.scripting; + +public interface Calculator { + + int add(int x, int y); +} +``` + +下面的示例在 Groovy 中实现了`Calculator`接口: + +``` +// from the file 'calculator.groovy' +package org.springframework.scripting.groovy + +class GroovyCalculator implements Calculator { + + int add(int x, int y) { + x + y + } +} +``` + +Bean 以下定义使用了在 Groovy 中定义的计算器: + +``` +<!-- from the file 'beans.xml' --> +<beans> + <lang:groovy id="calculator" script-source="classpath:calculator.groovy"/> +</beans> +``` + +最后,下面的小应用程序执行前面的配置: + +``` +package org.springframework.scripting; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +public class Main { + + public static void main(String[] args) { + ApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml"); + Calculator calc = ctx.getBean("calculator", Calculator.class); + System.out.println(calc.add(2, 8)); + } +} +``` + +运行上述程序的结果输出是(不出所料)`10`。(有关更多有趣的示例,请参见 Dynamic Language Showcase 项目,以获得更复杂的示例,或者参见本章后面的示例[Scenarios](#dynamic-language-scenarios))。 + +每个 Groovy 源文件不能定义多个类。虽然这在 Groovy 中完全合法,但(可以说)这是一种糟糕的做法。为了采用一致的方法,你应该(在 Spring 团队的意见中)尊重每个源文件一个(公共)类的标准 Java 约定。 + +##### 通过使用回调自定义 Groovy 对象 + +Bean `GroovyObjectCustomizer`接口是一个回调,它允许你将额外的创建逻辑连接到创建支持 Groovy 的过程中。例如,该接口的实现可以调用任何必需的初始化方法,设置一些默认属性值,或者指定一个自定义`MetaClass`。下面的清单显示了`GroovyObjectCustomizer`接口定义: + +``` +public interface GroovyObjectCustomizer { + + void customize(GroovyObject goo); +} +``` + +Spring 框架实例化支持 Groovy 的 Bean 的实例,然后将创建的`GroovyObject`传递给指定的`GroovyObjectCustomizer`(如果已经定义了一个)。你可以使用提供的`GroovyObject`引用执行任何你喜欢的操作。我们预计大多数人都希望通过这个回调设置一个自定义`MetaClass`,下面的示例展示了如何这样做: + +``` +public final class SimpleMethodTracingCustomizer implements GroovyObjectCustomizer { + + public void customize(GroovyObject goo) { + DelegatingMetaClass metaClass = new DelegatingMetaClass(goo.getMetaClass()) { + + public Object invokeMethod(Object object, String methodName, Object[] arguments) { + System.out.println("Invoking '" + methodName + "'."); + return super.invokeMethod(object, methodName, arguments); + } + }; + metaClass.initialize(); + goo.setMetaClass(metaClass); + } + +} +``` + +在 Groovy 中对元编程的全面讨论超出了 Spring 参考手册的范围。请参阅 Groovy 参考手册的相关部分或在线搜索。很多文章都谈到了这个话题。实际上,如果使用 Spring 名称空间支持,那么使用“GroovyObjectCustomizer”很容易,如下例所示: + +``` +<!-- define the GroovyObjectCustomizer just like any other bean --> +<bean id="tracingCustomizer" class="example.SimpleMethodTracingCustomizer"/> + + <!-- ... and plug it into the desired Groovy bean via the 'customizer-ref' attribute --> + <lang:groovy id="calculator" + script-source="classpath:org/springframework/scripting/groovy/Calculator.groovy" + customizer-ref="tracingCustomizer"/> +``` + +如果不使用 Spring 名称空间支持,你仍然可以使用“GroovyObjectCustomizer”功能,如下例所示: + +``` +<bean id="calculator" class="org.springframework.scripting.groovy.GroovyScriptFactory"> + <constructor-arg value="classpath:org/springframework/scripting/groovy/Calculator.groovy"/> + <!-- define the GroovyObjectCustomizer (as an inner bean) --> + <constructor-arg> + <bean id="tracingCustomizer" class="example.SimpleMethodTracingCustomizer"/> + </constructor-arg> +</bean> + +<bean class="org.springframework.scripting.support.ScriptFactoryPostProcessor"/> +``` + +| |你还可以在 Spring 的“GroovyObjectCustomizer”的相同位置指定一个 Groovy`CompilationCustomizer`(例如`ImportCustomizer`)<br/>或者一个完整的 Groovy`CompilerConfiguration`对象。此外,你可以在`ConfigurableApplicationContext.setClassLoader`级别上为你的 bean 设置一个带有自定义<br/>配置的通用`GroovyClassLoader`配置;<br/>这还会导致共享`GroovyClassLoader`使用,因此在<br/>大量脚本 bean 的情况下是值得推荐的(避免每个 Bean 使用一个孤立的`GroovyClassLoader`实例)。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 3.2.3.豆壳豆 + +这一部分描述了如何在 Spring 中使用豆壳豆。 + +[Beanshell 主页](https://beanshell.github.io/intro.html)包括以下描述: + +``` +BeanShell is a small, free, embeddable Java source interpreter with dynamic language +features, written in Java. BeanShell dynamically runs standard Java syntax and +extends it with common scripting conveniences such as loose types, commands, and method +closures like those in Perl and JavaScript. +``` + +Bean 与 Groovy 相反,支持 BeanShell 的定义需要一些(小的)附加配置。 Spring 中的 BeanShell 动态语言支持的实现是有趣的,因为 Spring 创建了一个 JDK 动态代理,该代理实现了在<`script-interfaces`元素的属性值<lang:bsh>中指定的所有接口(这就是为什么必须在属性的值中至少提供一个接口,因此,当你使用 beanshell 支持的 bean 时,将程序转换为接口)。这意味着对 BeanShell 支持的对象的每个方法调用都要通过 JDK 动态代理调用机制。 + +现在,我们可以展示一个使用基于 BeanShell 的 Bean 的完全工作的示例,该示例实现了本章前面定义的`Messenger`接口。我们再次展示`Messenger`接口的定义: + +``` +package org.springframework.scripting; + +public interface Messenger { + + String getMessage(); +} +``` + +下面的示例展示了`Messenger`接口的 BeanShell“实现”(我们在这里粗略地使用术语): + +``` +String message; + +String getMessage() { + return message; +} + +void setMessage(String aMessage) { + message = aMessage; +} +``` + +下面的示例展示了 Spring XML,它定义了上述“类”的一个“实例”(同样,我们在这里非常松散地使用这些术语): + +``` +<lang:bsh id="messageService" script-source="classpath:BshMessenger.bsh" + script-interfaces="org.springframework.scripting.Messenger"> + + <lang:property name="message" value="Hello World!" /> +</lang:bsh> +``` + +有关可能需要使用基于 BeanShell 的 bean 的一些场景,请参见[Scenarios](#dynamic-language-scenarios)。 + +### 3.3.场景 + +在脚本语言中定义 Spring 托管 bean 将是有益的,这种可能的场景有很多种。本节描述了 Spring 中动态语言支持的两个可能的用例。 + +#### 3.3.1.脚本 Spring MVC 控制器 + +可以从使用动态语言支持的 bean 中受益的一组类是 Spring MVC 控制器。在纯 Spring MVC 应用程序中,在很大程度上,通过 Web 应用程序的导航流是由封装在你的 Spring MVC 控制器中的代码决定的。由于 Web 应用程序的导航流和其他表示层逻辑需要更新,以响应支持问题或更改的业务需求,通过编辑一个或多个动态语言源文件,并看到这些更改立即反映在正在运行的应用程序的状态中,很可能更容易实现任何此类所需的更改。 + +请记住,在 Spring 这样的项目所支持的轻量级架构模型中,你通常的目标是拥有一个非常薄的表示层,应用程序的所有重要业务逻辑都包含在域和服务层类中。 Spring 将 MVC 控制器开发为动态语言支持的 bean,可以通过编辑和保存文本文件来更改表示层逻辑。对这种动态语言源文件的任何更改(取决于配置)都会自动反映在由动态语言源文件支持的 bean 中。 + +| |要实现对动态语言支持的<br/>bean 的任何更改的自动“拾取”,你必须启用“刷新 bean”功能。有关此功能的完整处理,请参见[可刷新的咖啡豆](#dynamic-language-refreshable-beans)。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +下面的示例显示了使用 Groovy 动态语言实现的`org.springframework.web.servlet.mvc.Controller`: + +``` +// from the file '/WEB-INF/groovy/FortuneController.groovy' +package org.springframework.showcase.fortune.web + +import org.springframework.showcase.fortune.service.FortuneService +import org.springframework.showcase.fortune.domain.Fortune +import org.springframework.web.servlet.ModelAndView +import org.springframework.web.servlet.mvc.Controller + +import javax.servlet.http.HttpServletRequest +import javax.servlet.http.HttpServletResponse + +class FortuneController implements Controller { + + @Property FortuneService fortuneService + + ModelAndView handleRequest(HttpServletRequest request, + HttpServletResponse httpServletResponse) { + return new ModelAndView("tell", "fortune", this.fortuneService.tellFortune()) + } +} +``` + +``` +<lang:groovy id="fortune" + refresh-check-delay="3000" + script-source="/WEB-INF/groovy/FortuneController.groovy"> + <lang:property name="fortuneService" ref="fortuneService"/> +</lang:groovy> +``` + +#### 3.3.2.脚本验证器 + +Spring 应用程序开发的另一个领域是验证领域,该领域可能受益于动态语言支持的 bean 所提供的灵活性。相对于正则 Java,使用松散类型的动态语言(也可能支持内联正则表达式)可以更容易地表示复杂的验证逻辑。 + +同样,将验证器开发为支持动态语言的 bean,可以通过编辑和保存一个简单的文本文件来更改验证逻辑。任何此类更改(取决于配置)都会自动反映在正在运行的应用程序的执行中,并且不需要重新启动应用程序。 + +| |要实现对动态语言支持的<br/>bean 的任何更改的自动“拾取”,你必须启用“可刷新 bean”功能。有关此功能的完整和详细处理,请参见[可刷新的咖啡豆](#dynamic-language-refreshable-beans)。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +下面的示例显示了通过使用 Groovy 动态语言实现的 Spring `org.springframework.validation.Validator`(有关 `validator’接口的讨论,请参见[Validation using Spring’s Validator interface](core.html#validator)): + +``` +import org.springframework.validation.Validator +import org.springframework.validation.Errors +import org.springframework.beans.TestBean + +class TestBeanValidator implements Validator { + + boolean supports(Class clazz) { + return TestBean.class.isAssignableFrom(clazz) + } + + void validate(Object bean, Errors errors) { + if(bean.name?.trim()?.size() > 0) { + return + } + errors.reject("whitespace", "Cannot be composed wholly of whitespace.") + } +} +``` + +### 3.4.附加细节 + +最后一节包含了一些与动态语言支持相关的附加细节。 + +#### 3.4.1. AOP——为脚本 bean 提供建议 + +你可以使用 Spring AOP 框架来建议脚本化的 bean。 Spring AOP 框架实际上并不知道正在被建议的 Bean 可能是脚本化的 Bean,因此你使用(或旨在使用)的所有 AOP 用例和功能都与脚本化的 bean 一起工作。当你建议使用脚本 bean 时,不能使用基于类的代理。你必须使用[基于接口的代理](core.html#aop-proxying)。 + +你不限于为脚本 bean 提供建议。还可以用受支持的动态语言编写方面本身,并使用这样的 bean 来建议其他 Spring bean。不过,这确实是对动态语言支持的高级使用。 + +#### 3.4.2.范围界定 + +在不是立即显而易见的情况下,脚本 bean 可以以与任何其他 bean 相同的方式进行作用域 Bean。各种`<lang:language/>`元素上的`scope`属性允许你控制底层脚本 Bean 的作用域,就像它对常规 Bean 所做的那样。(默认作用域是[singleton](core.html#beans-factory-scopes-singleton),就像“常规”bean 一样。 + +下面的示例使用`scope`属性将 groovy Bean 的作用域定义为[prototype](core.html#beans-factory-scopes-prototype): + +``` +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:lang="http://www.springframework.org/schema/lang" + xsi:schemaLocation=" + http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/lang https://www.springframework.org/schema/lang/spring-lang.xsd"> + + <lang:groovy id="messenger" script-source="classpath:Messenger.groovy" scope="prototype"> + <lang:property name="message" value="I Can Do The RoboCop" /> + </lang:groovy> + + <bean id="bookingService" class="x.y.DefaultBookingService"> + <property name="messenger" ref="messenger" /> + </bean> + +</beans> +``` + +有关 Spring 框架中范围支持的完整讨论,请参见[Bean Scopes](core.html#beans-factory-scopes)中的[IOC 容器](core.html#beans)。 + +#### 3.4.3.`lang`XML 模式 + +Spring XML 配置中的`lang`元素将用动态语言(如 Groovy 或 BeanShell)编写的对象作为 Spring 容器中的 bean 公开。 + +在[动态语言支持](#dynamic-language)中全面介绍了这些元素(以及动态语言支持)。有关此支持和`lang`元素的详细信息,请参见该部分。 + +要使用`lang`模式中的元素,你需要在 Spring XML 配置文件的顶部具有以下前导码。以下代码片段中的文本引用了正确的模式,因此你可以使用`lang`名称空间中的标记: + +``` +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:lang="http://www.springframework.org/schema/lang" + xsi:schemaLocation=" + http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/lang https://www.springframework.org/schema/lang/spring-lang.xsd"> + + <!-- bean definitions here --> + +</beans> +``` + +### 3.5.更多资源 + +下面的链接指向关于本章中引用的各种动态语言的更多参考资料: + +* [Groovy](https://www.groovy-lang.org/)主页 + +* [BeanShell](https://beanshell.github.io/intro.html)主页 + +* [JRuby](https://www.jruby.org)主页 diff --git a/docs/spring-framework/overview.md b/docs/spring-framework/overview.md new file mode 100644 index 0000000000000000000000000000000000000000..fc4768cc47fc17c297739248fd4cc34b9a67f235 --- /dev/null +++ b/docs/spring-framework/overview.md @@ -0,0 +1,71 @@ +# Spring 框架概述 + +Spring 使创建 Java Enterprise 应用程序变得容易。它提供了在 Enterprise 环境中接受 Java 语言所需的一切,支持 Groovy 和 Kotlin 作为 JVM 上的替代语言,并具有根据应用程序的需求创建多种架构的灵活性。在 Spring Framework5.1 中, Spring 需要 JDK8+(Java SE8+)并为 JDK11LTS 提供开箱即用的支持。Java SE8Update60 被建议作为 Java8 的最小补丁版本,但通常建议使用最近发布的补丁。 + +Spring 支持广泛的应用场景。在大型 Enterprise 中,应用程序通常存在很长一段时间,并且必须在 JDK 和应用程序服务器上运行,其升级周期超出了开发人员的控制范围。其他的可能作为一个单独的服务器运行 jar,嵌入式服务器,可能是在云环境中。还有一些可能是不需要服务器的独立应用程序(例如批处理或集成工作负载)。 + +Spring 是开源的。它拥有一个庞大而活跃的社区,可以根据各种不同的实际用例提供持续的反馈。这在很长一段时间内帮助了 Spring 成功的进化。 + +## 1. 我们所说的“ Spring”是什么意思? + +术语“ Spring”在不同的上下文中表示不同的事物。它可以用来指 Spring 框架项目本身,这就是它开始的地方。随着时间的推移,其他 Spring 个项目已经建立在 Spring 个框架之上。大多数情况下,当人们说“ Spring”时,他们指的是整个项目家族。这篇参考文献关注的是基础: Spring 框架本身。 + +Spring 框架被划分为多个模块。应用程序可以选择他们需要的模块。核心是核心容器的模块,包括配置模型和依赖注入机制。除此之外, Spring 框架为不同的应用程序架构提供了基础支持,包括消息传递、事务数据和持久性以及 Web。它还包括基于 Servlet 的 Spring MVC Web 框架,以及并行的 Spring WebFlux 反应性 Web 框架。 + +关于模块的说明: Spring 的框架 JAR 允许部署到 JDK9 的模块路径(“Jigsaw”)。为了在支持拼图的应用程序中使用, Spring Framework5JAR 附带了“automatic-module-name”清单条目,这些条目定义了独立于 jar 工件名称的稳定语言级别的模块名称(“ Spring.core”、“ Spring.context”等)(这些 JAR 遵循与“-”而不是“.”相同的命名模式,例如“ Spring-core”和“ Spring-context”)。当然, Spring 的框架 JAR 在 JDK8 和 9+ 上的 Classpath 上运行良好。 + +## 2. Spring 和 Spring 框架的历史 + +Spring 作为对早期[J2EE](https://en.wikipedia.org/wiki/Java_Platform,_Enterprise_Edition)规范的复杂性的响应,于 2003 年出现。虽然有些人认为 Java EE 和 Spring 是竞争关系,但 Spring 实际上是对 Java EE 的补充。 Spring 编程模型不包含 Java EE 平台规范;相反,它集成了从 EE 系统中精心选择的各个规范: + +* Servlet API([JSR 340](https://jcp.org/en/jsr/detail?id=340)) + +* WebSocket API([JSR 356](https://www.jcp.org/en/jsr/detail?id=356)) + +* 并发实用程序([JSR 236](https://www.jcp.org/en/jsr/detail?id=236)) + +* JSON 绑定 API([JSR 367](https://jcp.org/en/jsr/detail?id=367)) + +* Bean 验证([JSR 303](https://jcp.org/en/jsr/detail?id=303)) + +* JPA([JSR 338](https://jcp.org/en/jsr/detail?id=338)) + +* JMS([JSR 914](https://jcp.org/en/jsr/detail?id=914)) + +* 以及用于事务协调的 JTA/JCA 设置(如果有必要)。 + +Spring 框架还支持依赖注入([JSR 330](https://www.jcp.org/en/jsr/detail?id=330))和公共注释([JSR 250](https://jcp.org/en/jsr/detail?id=250))规范,应用程序开发人员可以选择使用这些规范,而不是 Spring 框架提供的 Spring 特定机制。 + +在 Spring Framework5.0 中, Spring 至少需要 Java EE7 级别(例如 Servlet 3.1+, JPA 2.1+),同时在运行时遇到 Java EE8 级别的较新 API(例如 Servlet 4.0,JSON Binding API)时提供开箱即用的集成。这使得 Spring 完全兼容例如 Tomcat 8 和 9、WebSphere9 和 JBossEAP7。 + +随着时间的推移,Java EE 在应用程序开发中的角色已经发生了变化。在 Java EE 和 Spring 的早期,创建应用程序是为了将其部署到应用程序服务器上。今天,在 Spring Boot 的帮助下,应用程序是以 DevOps 和云友好的方式创建的, Servlet 容器是嵌入式的,需要进行更改。在 Spring Framework5 中,WebFlux 应用程序甚至不直接使用 Servlet API,并且可以在不是 Servlet 容器的服务器(例如 Netty)上运行。 + +Spring 继续创新和发展。在 Spring 框架之外,还有其他项目,例如 Spring 引导、 Spring 安全、 Spring 数据、 Spring 云、 Spring 批处理等。重要的是要记住,每个项目都有自己的源代码存储库、问题跟踪器和发布 Cadence。有关 Spring 项目的完整列表,请参见[spring.io/projects](https://spring.io/projects)。 + +## 3. 设计哲学 + +当你了解一个框架时,重要的是不仅要知道它做了什么,还要知道它遵循了什么原则。以下是 Spring 框架的指导原则: + +* 在各个层面提供选择。 Spring 让你尽可能推迟设计决策。例如,你可以在不更改代码的情况下通过配置切换持久性提供程序。对于许多其他基础设施问题以及与第三方 API 的集成也是如此。 + +* 容纳不同的视角。 Spring 支持灵活性,对事情应该如何做并不固执己见。它以不同的视角支持广泛的应用需求。 + +* 保持强大的向后兼容性。 Spring 的演变过程经过了精心的管理,几乎没有在不同版本之间进行任何突破性的改变。 Spring 支持精心选择的一系列 JDK 版本和第三方库,以便于维护依赖于 Spring 的应用程序和库。 + +* 关心 API 设计。 Spring 团队投入了大量的思想和时间来制作直观的 API,并且可以在许多版本和许多年中使用。 + +* 为代码质量设定高标准。 Spring 框架非常强调有意义的、当前的和准确的 Javadoc。它是极少数几个可以声称没有包之间循环依赖的干净代码结构的项目之一。 + +## 4. 反馈和贡献 + +对于操作问题或诊断或调试问题,我们建议使用堆栈溢出。单击[here](https://stackoverflow.com/questions/tagged/spring+or+spring-mvc+or+spring-aop+or+spring-jdbc+or+spring-r2dbc+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+or+spring-rsocket?tab=Newest)以获得在堆栈溢出上使用的建议标记列表。如果你相当确定 Spring 框架中存在问题,或者想推荐一个功能,请使用[GitHub Issues](https://github.com/spring-projects/spring-framework/issues)。 + +如果你有一个解决方案或建议的修补程序,你可以在[Github](https://github.com/spring-projects/spring-framework)上提交一个拉请求。但是,请记住,对于所有的问题,除了最琐碎的问题,我们希望在问题追踪器中归档一张票,在那里进行讨论,并留下一个记录供将来参考。 + +有关更多详细信息,请参见[CONTRIBUTING](https://github.com/spring-projects/spring-framework/tree/main/CONTRIBUTING.md)顶层项目页面中的指南。 + +## 5. 开始 + +如果你刚刚开始使用 Spring,那么你可能希望通过创建一个基于[Spring Boot](https://projects.spring.io/spring-boot/)的应用程序来开始使用 Spring 框架。 Spring Boot 提供了一种快速的(且自以为是的)方法来创建基于 Spring 的可生产应用程序。它基于 Spring 框架,更倾向于约定而不是配置,并且旨在使你尽快启动和运行。 + +你可以使用[start.spring.io](https://start.spring.io/)来生成一个基本项目,或者遵循[“入门”指南](https://spring.io/guides)中的一个,例如[开始构建 RESTful Web 服务](https://spring.io/guides/gs/rest-service/)。除了更容易理解之外,这些指南还非常注重任务,并且大多数指南都是基于 Spring 引导的。它们还涵盖了 Spring 投资组合中你在解决特定问题时可能要考虑的其他项目。 diff --git a/docs/spring-framework/testing.md b/docs/spring-framework/testing.md new file mode 100644 index 0000000000000000000000000000000000000000..d7538f3b51c10e16d044f8fa68c66abd5e5aaf0f --- /dev/null +++ b/docs/spring-framework/testing.md @@ -0,0 +1,7440 @@ +# 测试 + +本章介绍 Spring 对集成测试的支持和单元测试的最佳实践。 Spring 团队提倡测试驱动开发。 Spring 团队已经发现,控制反转的正确使用确实使单元测试和集成测试变得更容易(在在类上存在 setter 方法和适当的构造函数,使得它们更容易在测试中连接在一起,而无需设置服务定位器注册中心和类似的结构)。 + +## 1. Spring 测试简介 + +测试是 Enterprise 软件开发中不可缺少的一部分。本章重点讨论了 IOC 原则对[unit testing](#unit-testing)的增值,以及 Spring 框架支持[集成测试](#integration-testing)的好处。(在 Enterprise 中对测试的彻底处理超出了本参考手册的范围。) + +## 2. 单元测试 + +与传统的 爪哇 EE 开发相比,依赖注入应该使你的代码更少地依赖于容器。组成应用程序的 POJO 应该在 JUnit 或 TestNG 测试中是可测试的,使用`new`操作符实例化对象,而不需要 Spring 或任何其他容器。你可以使用[mock objects](#mock-objects)(与其他有价值的测试技术一起使用)来孤立地测试你的代码。如果你遵循 Spring 的体系结构建议,那么你的代码库的干净分层和组件化将促进更容易的单元测试。例如,你可以通过截断或模拟 DAO 或存储库接口来测试服务层对象,而不需要在运行单元测试时访问持久性数据。 + +真正的单元测试通常运行得非常快,因为没有要设置的运行时基础设施。强调真正的单元测试作为开发方法的一部分,可以提高你的工作效率。你可能不需要测试章节的这一部分来帮助你为基于 IOC 的应用程序编写有效的单元测试。然而,对于某些单元测试场景, Spring 框架提供了模拟对象和测试支持类,这在本章中进行了描述。 + +### 2.1.模拟对象 + +Spring 包括一些专门用于嘲弄的软件包: + +* [Environment](#mock-objects-env) + +* [JNDI](#mock-objects-jndi) + +* [Servlet API](#mock-objects-servlet) + +* [Spring Web Reactive](#mock-objects-web-reactive) + +#### 2.1.1.环境 + +`org.springframework.mock.env`包包含 `environment’和`PropertySource`抽象的模拟实现(参见[Bean Definition Profiles](core.html#beans-definition-profiles)和[“PropertySource”抽象](core.html#beans-property-source-abstraction))。`mockenvironment’和`MockPropertySource`对于为依赖于环境特定属性的代码开发容器外测试非常有用。 + +#### 2.1.2.JNDI + +`org.springframework.mock.jndi`包包含 JNDI SPI 的部分实现,你可以使用它为测试套件或独立应用程序设置一个简单的 JNDI 环境。例如,如果 JDBC`DataSource`实例在测试代码中与在 爪哇 EE 容器中绑定到相同的 JNDI 名称,则可以在测试场景中重用应用程序代码和配置,而无需进行修改。 + +| |在`org.springframework.mock.jndi`包中的模拟 JNDI 支持是<br/>在 Spring Framework5.2 中正式反对的,以支持来自第三方<br/>的完整解决方案,例如[Simple-JNDI](https://github.com/h-thurow/Simple-JNDI)。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 2.1.3. Servlet 空气污染指数 + +`org.springframework.mock.web`包包含一组完整的 API 模拟对象,这些对象对于测试 Web 上下文、控制器和过滤器非常有用。这些模拟对象针对 Spring 的 Web MVC 框架的使用,并且通常比动态模拟对象(例如[EasyMock](http://easymock.org/))或可选的 Servlet API 模拟对象(例如[MockObjects](http://www.mockobjects.com))更方便地使用。 + +| |自 Spring Framework5.0 以来,`org.springframework.mock.web`中的模拟对象是基于 Servlet 4.0API 的<br/>。| +|---|--------------------------------------------------------------------------------------------------------------------| + +Spring MVC 测试框架构建在模拟 Servlet API 对象上,以提供 Spring MVC 的集成测试框架。见[MockMvc](#spring-mvc-test-framework)。 + +#### 2.1.4. Spring 网络反应 + +`org.springframework.mock.http.server.reactive`包包含用于 WebFlux 应用程序的`ServerHttpRequest`和`ServerHttpResponse`的模拟实现。“org.springframework.mock.web.server”包包含一个 mock`ServerWebExchange`,它依赖于这些 mock 请求和响应对象。 + +`MockServerHttpRequest`和`MockServerHttpResponse`都从相同的抽象基类扩展为特定于服务器的实现,并与它们共享行为。例如,一旦创建了一个模拟请求,它是不可变的,但是你可以使用`ServerHttpRequest`中的`mutate()`方法来创建一个修改过的实例。 + +为了让模拟响应正确地实现写契约并返回一个写完成句柄(即`Mono<Void>`),它默认情况下使用带有 `cache().then()’的`Flux`,这将缓冲数据并使其可用于测试中的断言。应用程序可以设置一个自定义的写函数(例如,测试一个无限的流)。 + +[WebTestClient](#webtestclient)构建在模拟请求和响应的基础上,为在没有 HTTP 服务器的情况下测试 WebFlux 应用程序提供支持。客户机还可以用于与正在运行的服务器进行端到端测试。 + +### 2.2.单元测试支持类 + +Spring 包括许多可以帮助单元测试的类。它们可分为两类: + +* [通用测试工具](#unit-testing-utilities) + +* [Spring MVC Testing Utilities](#unit-testing-spring-mvc) + +#### 2.2.1.通用测试工具 + +`org.springframework.test.util`包包含几个用于单元和集成测试的通用实用程序。 + +`ReflectionTestUtils`是一组基于反射的实用方法。你可以在测试场景中使用这些方法,在这些场景中,你需要更改常量的值、设置一个非“公共”字段、调用一个非“公共”setter 方法,或者在测试应用程序代码时调用一个非“公共”配置或生命周期回调方法,例如以下情况: + +* 允许`private`或`protected`字段访问的 ORM 框架(例如 JPA 和 Hibernate),而不是用于域实体中属性的`public`setter 方法。 + +* Spring 对注释的支持(例如`@Autowired`、`@Inject`和`@Resource`),它们为`private`或`protected`字段、setter 方法和配置方法提供依赖注入。 + +* 对于生命周期回调方法,使用`@PostConstruct`和`@PreDestroy`之类的注释。 + +[`AopTestUtils`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/util/AopTestUtils.html)是 AOP 相关实用程序方法的集合。可以使用这些方法获得隐藏在一个或多个 Spring 代理后面的底层目标对象的引用。例如,如果你已经通过使用 EasyMock 或 Mockito 等库将 Bean 配置为动态模拟,并且该模拟被包装在 Spring 代理中,那么你可能需要直接访问底层模拟以在其上配置期望并执行验证。关于 Spring 的核心 AOP 实用程序,请参见[`AopUtils`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/aop/support/AopUtils.html)和[`AopProxyUtils`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/aop/framework/AopProxyUtils.html)。 + +#### 2.2.2. Spring MVC 测试工具 + +`org.springframework.test.web`包包含[ModelandViewAssert](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/web/ModelAndViewAssert.html),你可以将其与 JUnit、TestNG 或用于处理 Spring MVC`ModelAndView`对象的单元测试的任何其他测试框架结合使用。 + +| |单元测试 Spring MVC 控制器<br/><br/>以单元测试你的 Spring MVC`Controller`类作为 POJO,使用`ModelAndViewAssert`与`MockHttpServletRequest`、`MockHttpSession`结合,从 Spring 的[Servlet API mocks](#mock-objects-servlet)以此类推。要对你的<br/> Spring MVC 和 REST`Controller`类以及 Spring MVC 的`WebApplicationContext`配置进行彻底的集成测试,请使用[Spring MVC Test Framework](#spring-mvc-test-framework)代替。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +## 3. 集成测试 + +本节(本章其余部分的大部分内容)涵盖了 Spring 应用程序的集成测试。它包括以下主题: + +* [Overview](#integration-testing-overview) + +* [集成测试的目标](#integration-testing-goals) + +* [JDBC 测试支持](#integration-testing-support-jdbc) + +* [Annotations](#integration-testing-annotations) + +* [Spring TestContext Framework](#testcontext-framework) + +* [MockMvc](#spring-mvc-test-framework) + +### 3.1.概述 + +在不需要部署到应用程序服务器或连接到其他 Enterprise 基础设施的情况下,能够执行某些集成测试是很重要的。这样做可以让你测试以下内容: + +* Spring IoC 容器上下文的正确接线。 + +* 使用 JDBC 或 ORM 工具进行数据访问。这可以包括诸如 SQL 语句的正确性、 Hibernate 查询、 JPA 实体映射等等。 + +Spring 框架为“ Spring-test”模块中的集成测试提供了一流的支持。实际 jar 文件的名称可能包括发布版本,也可能是长`org.springframework.test`格式,这取决于你从哪里获得它(有关解释,请参见[抚养管理一节](core.html#dependency-management))。这个库包括`org.springframework.test`包,其中包含用于使用 Spring 容器进行集成测试的有价值的类。此测试不依赖于应用程序服务器或其他部署环境。这样的测试比单元测试运行得慢,但比同等的 Selenium 测试或依赖于部署到应用程序服务器的远程测试快得多。 + +单元和集成测试支持是以注释驱动的[Spring TestContext Framework](#testcontext-framework)的形式提供的。TestContext 框架与实际使用的测试框架无关,该框架允许在各种环境中测试,包括 JUnit、TestNG 和其他环境。 + +### 3.2.集成测试的目标 + +Spring 的集成测试支持具有以下主要目标: + +* 管理测试之间的[Spring IoC container caching](#testing-ctx-management)。 + +* 提供[测试夹具实例的依赖注入](#testing-fixture-di)。 + +* 提供适合于集成测试的[事务管理](#testing-tx)。 + +* 提供[Spring-specific base classes](#testing-support-classes),以帮助开发人员编写集成测试。 + +接下来的几节描述了每个目标,并提供了实现和配置细节的链接。 + +#### 3.2.1.上下文管理和缓存 + +Spring TestContext 框架提供了 Spring `ApplicationContext’实例和`WebApplicationContext`实例的一致加载以及这些上下文的缓存。对加载上下文的缓存的支持很重要,因为启动时间可能会成为一个问题——这不是因为 Spring 本身的开销,而是因为 Spring 容器实例化的对象需要时间来实例化。例如,一个包含 50 到 100 个映射文件的项目可能需要 10 到 20 秒的时间来加载映射文件,而在每个测试装置中运行每个测试之前产生的成本会导致总体测试运行速度变慢,从而降低开发人员的工作效率。 + +测试类通常声明 XML 的资源位置数组或 Groovy 配置元数据(通常在 Classpath 中),或者用于配置应用程序的组件类数组。这些位置或类与`web.xml`或用于生产部署的其他配置文件中指定的位置或类相同或相似。 + +默认情况下,一旦加载,配置的`ApplicationContext`将在每个测试中重用。因此,每个测试套件只产生一次安装成本,并且随后的测试执行要快得多。在这种情况下,术语“测试套件”是指在相同的 JVM 中运行的所有测试——例如,针对给定项目或模块的 Ant、 Maven 或 Gradle 构建运行的所有测试。在不太可能的情况下,测试会破坏应用程序上下文并需要重新加载(例如,通过修改 Bean 定义或应用程序对象的状态),可以将 TestContext 框架配置为重新加载配置并在执行下一个测试之前重新构建应用程序上下文。 + +参见[上下文管理](#testcontext-ctx-management)和[Context Caching](#testcontext-ctx-management-caching)与 TestContext 框架。 + +#### 3.2.2.测试夹具的依赖注入 + +当 TestContext 框架加载应用程序上下文时,它可以通过使用依赖项注入来配置测试类的实例。这提供了一种方便的机制,可以通过使用应用程序上下文中预先配置的 bean 来设置测试装置。这里的一个很大的好处是,你可以跨各种测试场景重用应用程序上下文(例如,用于配置 Spring-托管对象图、事务代理、`DataSource`实例等),从而避免了为单个测试用例重复复杂的测试 fixture 设置的需要。 + +例如,考虑一个场景,其中有一个类实现了`Title`域实体的数据访问逻辑。我们希望编写测试以下领域的集成测试: + +* Spring 配置:基本上,一切都与“HibernatetitleRepository” Bean 的配置相关吗? + +* Hibernate 映射文件配置:是否所有映射都正确,以及是否存在正确的延迟加载设置? + +* `HibernateTitleRepository`的逻辑:该类的配置实例是否如预期的那样执行? + +参见使用[TestContext 框架](#testcontext-fixture-di)的测试固定件的依赖注入。 + +#### 3.2.3.事务管理 + +在访问真实数据库的测试中,一个常见的问题是它们对持久性存储状态的影响。即使在使用开发数据库时,对状态的更改也可能会影响将来的测试。此外,许多操作——例如插入或修改持久数据——无法在事务之外执行(或验证)。 + +TestContext 框架解决了这个问题。默认情况下,框架为每个测试创建并回滚一个事务。你可以编写可以假设存在事务的代码。如果你在测试中调用事务性代理对象,那么根据它们配置的事务语义,它们的行为是正确的。此外,如果一个测试方法在为测试而管理的事务中运行时删除了选定的表的内容,则默认情况下事务会回滚,并且数据库会返回到执行测试之前的状态。通过使用在测试的应用程序上下文中定义的`PlatformTransactionManager` Bean 向测试提供事务支持。 + +如果你想要提交一个事务(这是不寻常的,但在你想要填充或修改数据库的特定测试时偶尔会有用),你可以通过使用[`@Commit`](#integration-testing-annotations)注释,告诉 TestContext 框架使事务提交,而不是回滚。 + +参见事务管理[TestContext 框架](#testcontext-tx)。 + +#### 3.2.4.集成测试的支持类 + +Spring TestContext 框架提供了几个`abstract`支持类,这些类简化了集成测试的编写。这些基本测试类为测试框架提供了定义良好的挂钩,以及方便的实例变量和方法,使你能够访问: + +* `ApplicationContext`,用于执行显式 Bean 查找或测试整个上下文的状态。 + +* a`JdbcTemplate`,用于执行查询数据库的 SQL 语句。可以使用这样的查询来确认与数据库相关的应用程序代码执行之前和之后的数据库状态,并且 Spring 确保这样的查询在与应用程序代码相同的事务的范围内运行。当与 ORM 工具一起使用时,请务必避免[false positives](#testcontext-tx-false-positives)。 + +此外,你可能希望创建你自己的定制的、应用程序范围的超类,其中包含特定于你的项目的实例变量和方法。 + +参见[TestContext 框架](#testcontext-support-classes)的支持类。 + +### 3.3.JDBC 测试支持 + +`org.springframework.test.jdbc`包包含`JdbcTestUtils`,这是一个与 JDBC 相关的实用程序函数的集合,旨在简化标准数据库测试场景。具体地说,`JdbcTestUtils`提供了以下静态实用方法。 + +* `countRowsInTable(..)`:计算给定表中的行数。 + +* `countRowsInTableWhere(..)`:使用提供的`WHERE`子句计算给定表中的行数。 + +* `deleteFromTables(..)`:从指定的表中删除所有行。 + +* `deleteFromTableWhere(..)`:使用提供的 `where’子句从给定的表中删除行。 + +* `dropTables(..)`:删除指定的表。 + +| |[`AbstractTransactionalJunit4SpringContextTests’](#testcontext-support-classes-junit4)和[`AbstractTransactionalTestNgSpringContextTexttTests’](#testcontext-support-classes-testng)在 `jdbctestutils` 中提供了委派给上述方法的方便方法。<br/><br/>`spring-jdbc`模块提供了对配置和启动嵌入式<br/>数据库的支持,你可以在与数据库交互的集成测试中使用该数据库。<br/>有关详细信息,请参见[Embedded Database<br/>Support](data-access.html#jdbc-embedded-database-support)和<483"r="484"/>。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 3.4.注解 + +本节介绍了在测试 Spring 应用程序时可以使用的注释。它包括以下主题: + +* [Spring Testing Annotations](#integration-testing-annotations-spring) + +* [标准注释支持](#integration-testing-annotations-standard) + +* [Spring JUnit 4 Testing Annotations](#integration-testing-annotations-junit4) + +* [Spring JUnit Jupiter Testing Annotations](#integration-testing-annotations-junit-jupiter) + +* [测试的元注释支持](#integration-testing-annotations-meta) + +#### 3.4.1. Spring 测试注释 + +Spring 框架提供了以下一组 Spring 特定的注释,你可以在与 TestContext 框架结合的单元和集成测试中使用这些注释。有关更多信息,请参见相应的 爪哇doc,包括默认属性值、属性别名和其他详细信息。 + +Spring 的测试注释包括以下内容: + +* [@bootstrapwith](#spring-testing-annotation-bootstrapwith) + +* [@contextconfiguration](#spring-testing-annotation-contextconfiguration) + +* [@webappconfiguration](#spring-testing-annotation-webappconfiguration) + +* [@contexthierarchy](#spring-testing-annotation-contexthierarchy) + +* [@ActiveProfiles](#spring-testing-annotation-activeprofiles) + +* [@TestPropertySource](#spring-testing-annotation-testpropertysource) + +* [@DynamicPropertySource](#spring-testing-annotation-dynamicpropertysource) + +* [@dirtiescontext](#spring-testing-annotation-dirtiescontext) + +* [@TestexecutionListeners](#spring-testing-annotation-testexecutionlisteners) + +* [@RecordApplicationEvents](#spring-testing-annotation-recordapplicationevents) + +* [`@Commit`](#spring-testing-annotation-commit) + +* [`@Rollback`](#spring-testing-annotation-rollback) + +* [@BeForeTransaction’](#spring-testing-annotation-beforetransaction) + +* [@aftertransaction](#spring-testing-annotation-aftertransaction) + +* [`@Sql`](#spring-testing-annotation-sql) + +* [`@SqlConfig`](#spring-testing-annotation-sqlconfig) + +* [`@SqlMergeMode`](#spring-testing-annotation-sqlmergemode) + +* [`@SqlGroup`](#spring-testing-annotation-sqlgroup) + +##### `@BootstrapWith` + +`@BootstrapWith`是一个类级注释,你可以使用它来配置如何引导 Spring TestContext 框架。具体地说,你可以使用`@BootstrapWith`来指定自定义的`TestContextBootstrapper`。有关更多详细信息,请参见[引导 TestContext 框架](#testcontext-bootstrapping)一节。 + +##### `@ContextConfiguration` + +`@ContextConfiguration`定义了类级元数据,用于确定如何加载和配置用于集成测试的`ApplicationContext`。具体地说,“@ContextConfiguration”声明应用程序上下文资源`locations`或用于加载上下文的组件`classes`。 + +资源位置通常是位于 Classpath 中的 XML 配置文件或 Groovy 脚本,而组件类通常是`@Configuration`类。然而,资源位置也可以引用文件系统中的文件和脚本,并且组件类可以是`@Component`类、`@Service`类等等。有关更多详细信息,请参见[组件类](#testcontext-ctx-management-javaconfig-component-classes)。 + +下面的示例显示了引用 XML 文件的`@ContextConfiguration`注释: + +爪哇 + +``` +@ContextConfiguration("/test-config.xml") (1) +class XmlApplicationContextTests { + // class body... +} +``` + +|**1**|指一个 XML 文件。| +|-----|-------------------------| + +Kotlin + +``` +@ContextConfiguration("/test-config.xml") (1) +class XmlApplicationContextTests { + // class body... +} +``` + +|**1**|指一个 XML 文件。| +|-----|-------------------------| + +下面的示例显示了引用一个类的`@ContextConfiguration`注释: + +爪哇 + +``` +@ContextConfiguration(classes = TestConfig.class) (1) +class ConfigClassApplicationContextTests { + // class body... +} +``` + +|**1**|指的是一门课。| +|-----|---------------------| + +Kotlin + +``` +@ContextConfiguration(classes = [TestConfig::class]) (1) +class ConfigClassApplicationContextTests { + // class body... +} +``` + +|**1**|指的是一门课。| +|-----|---------------------| + +作为一种替代方法,或者除了声明资源位置或组件类之外,还可以使用`@ContextConfiguration`声明`ApplicationContextInitializer`类。下面的示例展示了这样一个案例: + +爪哇 + +``` +@ContextConfiguration(initializers = CustomContextIntializer.class) (1) +class ContextInitializerTests { + // class body... +} +``` + +|**1**|声明初始化程序类。| +|-----|-------------------------------| + +Kotlin + +``` +@ContextConfiguration(initializers = [CustomContextIntializer::class]) (1) +class ContextInitializerTests { + // class body... +} +``` + +|**1**|声明初始化程序类。| +|-----|-------------------------------| + +你也可以选择使用`@ContextConfiguration`来声明`ContextLoader`策略。但是,请注意,你通常不需要显式地配置加载器,因为默认加载器支持`initializers`和资源`locations`或组件`classes`。 + +下面的示例既使用了位置,也使用了加载器: + +爪哇 + +``` +@ContextConfiguration(locations = "/test-context.xml", loader = CustomContextLoader.class) (1) +class CustomLoaderXmlApplicationContextTests { + // class body... +} +``` + +|**1**|配置位置和自定义加载器。| +|-----|------------------------------------------------| + +Kotlin + +``` +@ContextConfiguration("/test-context.xml", loader = CustomContextLoader::class) (1) +class CustomLoaderXmlApplicationContextTests { + // class body... +} +``` + +|**1**|配置位置和自定义加载器。| +|-----|------------------------------------------------| + +| |`@ContextConfiguration`提供了对继承资源位置或<br/>配置类的支持,以及由超类<br/>声明的上下文初始化器或包含类。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +有关更多详细信息,请参见[上下文管理](#testcontext-ctx-management),[@nested 测试类配置](#testcontext-junit-jupiter-nested-test-configuration)和`@ContextConfiguration`爪哇docs。 + +##### `@WebAppConfiguration` + +`@WebAppConfiguration`是一个类级注释,你可以使用它来声明为集成测试加载的 `ApplicationContext’应该是`WebApplicationContext`。在测试类上仅存在`@WebAppConfiguration`就可以确保为测试加载一个“WebApplicationContext”,使用缺省值“file:SRC/main/webapp”` 作为 Web 应用程序根目录的路径(即资源库路径)。后台使用资源库路径创建一个“MockServletContext”,它充当测试的“WebApplicationContext”的`ServletContext`。 + +下面的示例展示了如何使用`@WebAppConfiguration`注释: + +爪哇 + +``` +@ContextConfiguration +@WebAppConfiguration (1) +class WebAppTests { + // class body... +} +``` + +Kotlin + +``` +@ContextConfiguration +@WebAppConfiguration (1) +class WebAppTests { + // class body... +} +``` + +|**1**|`@WebAppConfiguration`注释。| +|-----|--------------------------------------| + +要覆盖默认值,可以使用隐式`value`属性指定不同的基本资源路径。同时支持`classpath:`和`file:`资源前缀。如果没有提供资源前缀,则假定路径是一个文件系统资源。下面的示例展示了如何指定 Classpath 资源: + +爪哇 + +``` +@ContextConfiguration +@WebAppConfiguration("classpath:test-web-resources") (1) +class WebAppTests { + // class body... +} +``` + +|**1**|指定 Classpath 资源。| +|-----|--------------------------------| + +Kotlin + +``` +@ContextConfiguration +@WebAppConfiguration("classpath:test-web-resources") (1) +class WebAppTests { + // class body... +} +``` + +|**1**|指定 Classpath 资源。| +|-----|--------------------------------| + +请注意,`@WebAppConfiguration`必须与 `@contextconfiguration’一起使用,可以在单个测试类中使用,也可以在测试类层次结构中使用。有关更多详细信息,请参见[@webappconfiguration](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/context/web/WebAppConfiguration.html)爪哇doc。 + +##### `@ContextHierarchy` + +`@ContextHierarchy`是一种类级注释,用于为集成测试定义“ApplicationContext”实例的层次结构。`@ContextHierarchy`应该使用一个或多个`@ContextConfiguration`实例的列表来声明,每个实例在上下文层次结构中定义一个级别。下面的示例演示了在单个测试类中使用“@contexthierarchy”(“@contexthierarchy”也可以在测试类层次结构中使用): + +爪哇 + +``` +@ContextHierarchy({ + @ContextConfiguration("/parent-config.xml"), + @ContextConfiguration("/child-config.xml") +}) +class ContextHierarchyTests { + // class body... +} +``` + +Kotlin + +``` +@ContextHierarchy( + ContextConfiguration("/parent-config.xml"), + ContextConfiguration("/child-config.xml")) +class ContextHierarchyTests { + // class body... +} +``` + +爪哇 + +``` +@WebAppConfiguration +@ContextHierarchy({ + @ContextConfiguration(classes = AppConfig.class), + @ContextConfiguration(classes = WebConfig.class) +}) +class WebIntegrationTests { + // class body... +} +``` + +Kotlin + +``` +@WebAppConfiguration +@ContextHierarchy( + ContextConfiguration(classes = [AppConfig::class]), + ContextConfiguration(classes = [WebConfig::class])) +class WebIntegrationTests { + // class body... +} +``` + +如果需要合并或覆盖测试类层次结构中上下文层次结构的给定级别的配置,则必须在类层次结构中的每个对应级别上,通过向`@ContextConfiguration`中的`name`属性提供相同的值,显式地命名该级别。有关更多示例,请参见[上下文层次结构](#testcontext-ctx-management-ctx-hierarchies)和[@contexthierarchy](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/context/ContextHierarchy.html)爪哇doc。 + +##### `@ActiveProfiles` + +`@ActiveProfiles`是一种类级注释,用于声明在为集成测试加载`ApplicationContext`时哪个 Bean 定义配置文件应该处于活动状态。 + +以下示例表明`dev`配置文件应该处于活动状态: + +爪哇 + +``` +@ContextConfiguration +@ActiveProfiles("dev") (1) +class DeveloperTests { + // class body... +} +``` + +|**1**|表示`dev`配置文件应该处于活动状态。| +|-----|-------------------------------------------------| + +Kotlin + +``` +@ContextConfiguration +@ActiveProfiles("dev") (1) +class DeveloperTests { + // class body... +} +``` + +|**1**|指示`dev`配置文件应该处于活动状态。| +|-----|-------------------------------------------------| + +下面的示例表明,`dev`和`integration`配置文件都应该处于活动状态: + +爪哇 + +``` +@ContextConfiguration +@ActiveProfiles({"dev", "integration"}) (1) +class DeveloperIntegrationTests { + // class body... +} +``` + +|**1**|指示`dev`和`integration`配置文件应该处于活动状态。| +|-----|--------------------------------------------------------------------| + +Kotlin + +``` +@ContextConfiguration +@ActiveProfiles(["dev", "integration"]) (1) +class DeveloperIntegrationTests { + // class body... +} +``` + +|**1**|指示`dev`和`integration`配置文件应该处于活动状态。| +|-----|--------------------------------------------------------------------| + +| |`@ActiveProfiles`提供了对继承活动 Bean 定义配置文件<br/>的支持,该配置文件由超类声明并默认包含类。还可以通过实现自定义的[“ActiveProfilesResolver”](#testcontext-ctx-management-env-profiles-ActiveProfilesResolver)并使用`@ActiveProfiles`的`resolver`属性对其进行注册来以编程方式解析活动的<br/> Bean 定义配置文件。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +有关示例和更多详细信息,请参见[具有环境配置文件的上下文配置](#testcontext-ctx-management-env-profiles),[@nested 测试类配置](#testcontext-junit-jupiter-nested-test-configuration)和[@ActiveProfiles](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/context/ActiveProfiles.html)爪哇doc。 + +##### `@TestPropertySource` + +`@TestPropertySource`是一种类级注释,你可以使用它来配置要添加到`Environment`中的 `PropertySources’集合中的属性文件和内联属性的位置,以便为集成测试加载`ApplicationContext`。 + +下面的示例演示了如何从 Classpath 声明属性文件: + +爪哇 + +``` +@ContextConfiguration +@TestPropertySource("/test.properties") (1) +class MyIntegrationTests { + // class body... +} +``` + +|**1**|从 Classpath 的根中的`test.properties`获取属性。| +|-----|-------------------------------------------------------------------| + +Kotlin + +``` +@ContextConfiguration +@TestPropertySource("/test.properties") (1) +class MyIntegrationTests { + // class body... +} +``` + +|**1**|从 Classpath 根中的`test.properties`获取属性。| +|-----|-------------------------------------------------------------------| + +下面的示例演示了如何声明内联属性: + +爪哇 + +``` +@ContextConfiguration +@TestPropertySource(properties = { "timezone = GMT", "port: 4242" }) (1) +class MyIntegrationTests { + // class body... +} +``` + +|**1**|声明`timezone`和`port`属性。| +|-----|-----------------------------------------| + +Kotlin + +``` +@ContextConfiguration +@TestPropertySource(properties = ["timezone = GMT", "port: 4242"]) (1) +class MyIntegrationTests { + // class body... +} +``` + +|**1**|声明`timezone`和`port`属性。| +|-----|-----------------------------------------| + +有关示例和更多详细信息,请参见[具有测试属性源的上下文配置](#testcontext-ctx-management-property-sources)。 + +##### `@DynamicPropertySource` + +`@DynamicPropertySource`是一种方法级别的注释,你可以使用它来注册要添加到`Environment`中的`PropertySources`集合中的 *dynamic* 属性,用于为集成测试加载`ApplicationContext`。当你不知道属性的初始值时,动态属性是有用的——例如,如果属性是由外部资源管理的,比如由[Testcontainers](https://www.testcontainers.org/)项目管理的容器。 + +下面的示例演示了如何注册动态属性: + +爪哇 + +``` +@ContextConfiguration +class MyIntegrationTests { + + static MyExternalServer server = // ... + + @DynamicPropertySource (1) + static void dynamicProperties(DynamicPropertyRegistry registry) { (2) + registry.add("server.port", server::getPort); (3) + } + + // tests ... +} +``` + +|**1**|用`@DynamicPropertySource`注释`static`方法。| +|-----|---------------------------------------------------------------------------------| +|**2**|接受`DynamicPropertyRegistry`作为参数。| +|**3**|注册一个动态`server.port`属性,以便从服务器上懒洋洋地检索。| + +Kotlin + +``` +@ContextConfiguration +class MyIntegrationTests { + + companion object { + + @JvmStatic + val server: MyExternalServer = // ... + + @DynamicPropertySource (1) + @JvmStatic + fun dynamicProperties(registry: DynamicPropertyRegistry) { (2) + registry.add("server.port", server::getPort) (3) + } + } + + // tests ... +} +``` + +|**1**|用`@DynamicPropertySource`注释`static`方法。| +|-----|---------------------------------------------------------------------------------| +|**2**|接受`DynamicPropertyRegistry`作为参数。| +|**3**|注册一个动态`server.port`属性,以便从服务器上懒洋洋地检索。| + +有关更多详细信息,请参见[具有动态属性源的上下文配置](#testcontext-ctx-management-dynamic-property-sources)。 + +##### `@DirtiesContext` + +`@DirtiesContext`表示底层 Spring `ApplicationContext`在测试的执行过程中被弄脏了(也就是说,测试以某种方式修改或损坏了它——例如,通过更改单例 Bean 的状态),并且应该关闭。当一个应用程序上下文被标记为 dirty 时,它将从测试框架的缓存中删除并关闭。因此,基础 Spring 容器将被重建,以用于需要具有相同配置元数据的上下文的任何后续测试。 + +可以在同一个类或类层次结构中同时使用`@DirtiesContext`作为类级和方法级的注释。在这种情况下,根据配置的`methodMode`和`classMode`,在任何此类注释方法之前或之后以及在当前测试类之前或之后,`ApplicationContext`被标记为 dirty。 + +下面的示例解释了各种配置场景的上下文何时会被弄脏: + +* 在当前测试类之前,当在类模式设置为“before_class”的类上声明时。 + + 爪哇 + + ``` + @DirtiesContext(classMode = BEFORE_CLASS) (1) + class FreshContextTests { + // some tests that require a new Spring container + } + ``` + + |**1**|在当前测试类之前弄脏上下文。| + |-----|------------------------------------------------| + + Kotlin + + ``` + @DirtiesContext(classMode = BEFORE_CLASS) (1) + class FreshContextTests { + // some tests that require a new Spring container + } + ``` + + |**1**|在当前测试类之前弄脏上下文。| + |-----|------------------------------------------------| + +* 在当前测试类之后,当在类上声明时,其类模式设置为“after_class”(即默认的类模式)。 + + 爪哇 + + ``` + @DirtiesContext (1) + class ContextDirtyingTests { + // some tests that result in the Spring container being dirtied + } + ``` + + |**1**|在当前测试类之后,将上下文弄脏。| + |-----|-----------------------------------------------| + + Kotlin + + ``` + @DirtiesContext (1) + class ContextDirtyingTests { + // some tests that result in the Spring container being dirtied + } + ``` + + |**1**|在当前测试类之后,将上下文弄脏。| + |-----|-----------------------------------------------| + +* 在当前测试类中的每个测试方法之前,当在类的模式设置为`BEFORE_EACH_TEST_METHOD.`的类上声明时 + + 爪哇 + + ``` + @DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD) (1) + class FreshContextTests { + // some tests that require a new Spring container + } + ``` + + |**1**|在每个测试方法之前弄脏上下文。| + |-----|------------------------------------------| + + Kotlin + + ``` + @DirtiesContext(classMode = BEFORE_EACH_TEST_METHOD) (1) + class FreshContextTests { + // some tests that require a new Spring container + } + ``` + + |**1**|在每个测试方法之前弄脏上下文。| + |-----|------------------------------------------| + +* 在当前测试类中的每个测试方法之后,当在类上声明时,将类模式设置为`AFTER_EACH_TEST_METHOD.` + + 爪哇 + + ``` + @DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) (1) + class ContextDirtyingTests { + // some tests that result in the Spring container being dirtied + } + ``` + + |**1**|在每种测试方法之后都要弄脏上下文。| + |-----|-----------------------------------------| + + Kotlin + + ``` + @DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) (1) + class ContextDirtyingTests { + // some tests that result in the Spring container being dirtied + } + ``` + + |**1**|在每种测试方法之后都要弄脏上下文。| + |-----|-----------------------------------------| + +* 在当前测试之前,当对方法进行声明时,方法模式设置为“before_method”。 + + 爪哇 + + ``` + @DirtiesContext(methodMode = BEFORE_METHOD) (1) + @Test + void testProcessWhichRequiresFreshAppCtx() { + // some logic that requires a new Spring container + } + ``` + + |**1**|在当前测试方法之前弄脏上下文。| + |-----|-------------------------------------------------| + + Kotlin + + ``` + @DirtiesContext(methodMode = BEFORE_METHOD) (1) + @Test + fun testProcessWhichRequiresFreshAppCtx() { + // some logic that requires a new Spring container + } + ``` + + |**1**|在当前测试方法之前弄脏上下文。| + |-----|-------------------------------------------------| + +* 在当前测试之后,当对方法进行声明时,方法模式设置为“after_method”(即默认的方法模式)。 + + 爪哇 + + ``` + @DirtiesContext (1) + @Test + void testProcessWhichDirtiesAppCtx() { + // some logic that results in the Spring container being dirtied + } + ``` + + |**1**|在当前测试方法之后,将上下文弄脏。| + |-----|------------------------------------------------| + + Kotlin + + ``` + @DirtiesContext (1) + @Test + fun testProcessWhichDirtiesAppCtx() { + // some logic that results in the Spring container being dirtied + } + ``` + + |**1**|在当前测试方法之后,将上下文弄脏。| + |-----|------------------------------------------------| + +如果在一个测试中使用`@DirtiesContext`,该测试的上下文被配置为带有`@ContextHierarchy`的上下文层次结构的一部分,则可以使用`hierarchyMode`标志来控制如何清除上下文缓存。默认情况下,使用穷举算法来清除上下文缓存,不仅包括当前级别,还包括共享当前测试共有的祖先上下文的所有其他上下文层次结构。所有驻留在公共祖先上下文的子层次结构中的“ApplicationContext”实例都将从上下文缓存中删除并关闭。如果穷举算法对特定的用例来说过于强大,那么你可以指定更简单的当前级别算法,如下例所示。 + +爪哇 + +``` +@ContextHierarchy({ + @ContextConfiguration("/parent-config.xml"), + @ContextConfiguration("/child-config.xml") +}) +class BaseTests { + // class body... +} + +class ExtendedTests extends BaseTests { + + @Test + @DirtiesContext(hierarchyMode = CURRENT_LEVEL) (1) + void test() { + // some logic that results in the child context being dirtied + } +} +``` + +|**1**|使用当前级别的算法。| +|-----|--------------------------------| + +Kotlin + +``` +@ContextHierarchy( + ContextConfiguration("/parent-config.xml"), + ContextConfiguration("/child-config.xml")) +open class BaseTests { + // class body... +} + +class ExtendedTests : BaseTests() { + + @Test + @DirtiesContext(hierarchyMode = CURRENT_LEVEL) (1) + fun test() { + // some logic that results in the child context being dirtied + } +} +``` + +|**1**|使用当前级别的算法。| +|-----|--------------------------------| + +有关`EXHAUSTIVE`和`CURRENT_LEVEL`算法的更多详细信息,请参见[`DirtiesContext.HieraryMode’](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/annotation/DirtiesContext.HierarchyMode.html)爪哇doc。 + +##### `@TestExecutionListeners` + +`@TestExecutionListeners`定义了用于配置应在 `TestContextManager’中注册的 `TestExcutionListener’实现的类级元数据。通常,`@TestExecutionListeners`与“@contextconfiguration”一起使用。 + +下面的示例显示了如何注册两个`TestExecutionListener`实现: + +爪哇 + +``` +@ContextConfiguration +@TestExecutionListeners({CustomTestExecutionListener.class, AnotherTestExecutionListener.class}) (1) +class CustomTestExecutionListenerTests { + // class body... +} +``` + +|**1**|注册两个`TestExecutionListener`实现。| +|-----|-----------------------------------------------------| + +Kotlin + +``` +@ContextConfiguration +@TestExecutionListeners(CustomTestExecutionListener::class, AnotherTestExecutionListener::class) (1) +class CustomTestExecutionListenerTests { + // class body... +} +``` + +|**1**|注册两个`TestExecutionListener`实现。| +|-----|-----------------------------------------------------| + +默认情况下,`@TestExecutionListeners`支持从超类或封闭类继承侦听器。有关示例和更多详细信息,请参见[@nested 测试类配置](#testcontext-junit-jupiter-nested-test-configuration)和[@TestexecutionListeners’爪哇doc](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/context/TestExecutionListeners.html)。 + +##### `@RecordApplicationEvents` + +`@RecordApplicationEvents`是一种类级注释,用于指示 * Spring TestContext 框架 * 记录在执行单个测试期间在 `ApplicationContext’中发布的所有应用程序事件。 + +可以通过测试中的`ApplicationEvents`API 访问记录的事件。 + +有关示例和更多详细信息,请参见[应用程序事件](#testcontext-application-events)和[@RecordApplicationEvents](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/context/event/RecordApplicationEvents.html)。 + +##### `@Commit` + +`@Commit`表示事务性测试方法的事务应在测试方法完成后提交。你可以使用`@Commit`作为`@Rollback(false)`的直接替换,以更明确地传达代码的意图。类似于`@Rollback`,`@Commit`也可以声明为类级或方法级注释。 + +下面的示例展示了如何使用`@Commit`注释: + +爪哇 + +``` +@Commit (1) +@Test +void testProcessWithoutRollback() { + // ... +} +``` + +|**1**|将测试结果提交给数据库。| +|-----|----------------------------------------------| + +Kotlin + +``` +@Commit (1) +@Test +fun testProcessWithoutRollback() { + // ... +} +``` + +|**1**|将测试结果提交给数据库。| +|-----|----------------------------------------------| + +##### `@Rollback` + +`@Rollback`指示在测试方法完成后是否应该回滚事务测试方法的事务。如果`true`,则回滚事务。否则,事务将被提交(另请参见[`@Commit`](#spring-testing-annotation-commit))。 Spring TestContext 框架中集成测试的回滚默认为`true`,即使没有显式声明`@Rollback`。 + +当声明为类级注释时,`@Rollback`为测试类层次结构中的所有测试方法定义了默认的回滚语义。当声明为方法级别的注释时,`@Rollback`为特定的测试方法定义了回滚语义,可能会覆盖类级别的`@Rollback`或`@Commit`语义。 + +下面的示例导致测试方法的结果不会被回滚(即,结果被提交到数据库中): + +爪哇 + +``` +@Rollback(false) (1) +@Test +void testProcessWithoutRollback() { + // ... +} +``` + +|**1**|不要回滚结果。| +|-----|----------------------------| + +Kotlin + +``` +@Rollback(false) (1) +@Test +fun testProcessWithoutRollback() { + // ... +} +``` + +|**1**|不要回滚结果。| +|-----|----------------------------| + +##### `@BeforeTransaction` + +`@BeforeTransaction`表示在事务启动之前应该运行带注释的`void`方法,用于通过使用 Spring 的`@Transactional`注释配置为在事务中运行的测试方法。`@BeforeTransaction`方法不需要是`public`,并且可以在基于 爪哇8 的接口默认方法上声明。 + +下面的示例展示了如何使用`@BeforeTransaction`注释: + +爪哇 + +``` +@BeforeTransaction (1) +void beforeTransaction() { + // logic to be run before a transaction is started +} +``` + +|**1**|在事务之前运行此方法。| +|-----|-------------------------------------| + +Kotlin + +``` +@BeforeTransaction (1) +fun beforeTransaction() { + // logic to be run before a transaction is started +} +``` + +|**1**|在事务之前运行此方法。| +|-----|-------------------------------------| + +##### `@AfterTransaction` + +`@AfterTransaction`表示在事务结束后应该运行带注释的`void`方法,用于通过使用 Spring 的`@Transactional`注释配置为在事务中运行的测试方法。`@AfterTransaction`方法不需要是`public`,并且可以在基于 爪哇8 的接口默认方法上声明。 + +爪哇 + +``` +@AfterTransaction (1) +void afterTransaction() { + // logic to be run after a transaction has ended +} +``` + +|**1**|在事务之后运行此方法。| +|-----|------------------------------------| + +Kotlin + +``` +@AfterTransaction (1) +fun afterTransaction() { + // logic to be run after a transaction has ended +} +``` + +|**1**|在事务之后运行此方法。| +|-----|------------------------------------| + +##### `@Sql` + +`@Sql`用于对测试类或测试方法进行注释,以配置在集成测试期间针对给定数据库运行的 SQL 脚本。下面的示例展示了如何使用它: + +爪哇 + +``` +@Test +@Sql({"/test-schema.sql", "/test-user-data.sql"}) (1) +void userTest() { + // run code that relies on the test schema and test data +} +``` + +|**1**|为此测试运行两个脚本。| +|-----|------------------------------| + +Kotlin + +``` +@Test +@Sql("/test-schema.sql", "/test-user-data.sql") (1) +fun userTest() { + // run code that relies on the test schema and test data +} +``` + +|**1**|为此测试运行两个脚本。| +|-----|------------------------------| + +有关更多详细信息,请参见[使用 @sql 声明式执行 SQL 脚本](#testcontext-executing-sql-declaratively)。 + +##### `@SqlConfig` + +`@SqlConfig`定义了元数据,用于确定如何解析和运行配置有`@Sql`注释的 SQL 脚本。下面的示例展示了如何使用它: + +爪哇 + +``` +@Test +@Sql( + scripts = "/test-user-data.sql", + config = @SqlConfig(commentPrefix = "`", separator = "@@") (1) +) +void userTest() { + // run code that relies on the test data +} +``` + +|**1**|在 SQL 脚本中设置注释前缀和分隔符。| +|-----|--------------------------------------------------------| + +Kotlin + +``` +@Test +@Sql("/test-user-data.sql", config = SqlConfig(commentPrefix = "`", separator = "@@")) (1) +fun userTest() { + // run code that relies on the test data +} +``` + +|**1**|在 SQL 脚本中设置注释前缀和分隔符。| +|-----|--------------------------------------------------------| + +##### `@SqlMergeMode` + +`@SqlMergeMode`用于对测试类或测试方法进行注释,以配置方法级`@Sql`声明是否与类级`@Sql`声明合并。如果在测试类或测试方法上未声明 `@sqlmergemode`,则默认情况下将使用`OVERRIDE`合并模式。在`OVERRIDE`模式下,方法级别`@Sql`声明将有效地覆盖类级别`@Sql`声明。 + +注意,方法级`@SqlMergeMode`声明覆盖了类级声明。 + +下面的示例展示了如何在类级别上使用`@SqlMergeMode`。 + +爪哇 + +``` +@SpringJUnitConfig(TestConfig.class) +@Sql("/test-schema.sql") +@SqlMergeMode(MERGE) (1) +class UserTests { + + @Test + @Sql("/user-test-data-001.sql") + void standardUserProfile() { + // run code that relies on test data set 001 + } +} +``` + +|**1**|对于类中的所有测试方法,将`@Sql`合并模式设置为`MERGE`。| +|-----|-----------------------------------------------------------------------| + +Kotlin + +``` +@SpringJUnitConfig(TestConfig::class) +@Sql("/test-schema.sql") +@SqlMergeMode(MERGE) (1) +class UserTests { + + @Test + @Sql("/user-test-data-001.sql") + fun standardUserProfile() { + // run code that relies on test data set 001 + } +} +``` + +|**1**|对于类中的所有测试方法,将`@Sql`合并模式设置为`MERGE`。| +|-----|-----------------------------------------------------------------------| + +下面的示例展示了如何在方法级别上使用`@SqlMergeMode`。 + +Java + +``` +@SpringJUnitConfig(TestConfig.class) +@Sql("/test-schema.sql") +class UserTests { + + @Test + @Sql("/user-test-data-001.sql") + @SqlMergeMode(MERGE) (1) + void standardUserProfile() { + // run code that relies on test data set 001 + } +} +``` + +|**1**|对于特定的测试方法,将`@Sql`合并模式设置为`MERGE`。| +|-----|----------------------------------------------------------------| + +Kotlin + +``` +@SpringJUnitConfig(TestConfig::class) +@Sql("/test-schema.sql") +class UserTests { + + @Test + @Sql("/user-test-data-001.sql") + @SqlMergeMode(MERGE) (1) + fun standardUserProfile() { + // run code that relies on test data set 001 + } +} +``` + +|**1**|为特定的测试方法将`@Sql`合并模式设置为`MERGE`。| +|-----|----------------------------------------------------------------| + +##### `@SqlGroup` + +`@SqlGroup`是一个容器注释,它聚合了几个`@Sql`注释。你可以本地使用`@SqlGroup`来声明几个嵌套的`@Sql`注释,或者可以将其与 Java8 对可重复注释的支持结合使用,其中`@Sql`可以在同一个类或方法上声明几次,隐式地生成这个容器注释。下面的示例展示了如何声明 SQL 组: + +Java + +``` +@Test +@SqlGroup({ (1) + @Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")), + @Sql("/test-user-data.sql") +)} +void userTest() { + // run code that uses the test schema and test data +} +``` + +|**1**|声明一组 SQL 脚本。| +|-----|-------------------------------| + +Kotlin + +``` +@Test +@SqlGroup( (1) + Sql("/test-schema.sql", config = SqlConfig(commentPrefix = "`")), + Sql("/test-user-data.sql")) +fun userTest() { + // run code that uses the test schema and test data +} +``` + +|**1**|声明一组 SQL 脚本。| +|-----|-------------------------------| + +#### 3.4.2.标准注释支持 + +Spring TestContext 框架的所有配置都支持以下注释的标准语义。请注意,这些注释不是特定于测试的,并且可以在 Spring 框架中的任何地方使用。 + +* `@Autowired` + +* `@Qualifier` + +* `@Value` + +* `@Resource`(javax.annotation)如果存在 JSR-250 + +* `@ManagedBean`(javax.annotation)如果存在 JSR-250 + +* `@Inject`如果存在 JSR-330 + +* `@Named`如果存在 JSR-330 + +* `@PersistenceContext`(javax.persistence)如果存在 JPA + +* `@PersistenceUnit`(javax.persistence)如果存在 JPA + +* `@Required` + +* `@Transactional`*with[有限的属性支持](#testcontext-tx-attribute-support)* + +| |JSR-250 生命周期注释<br/><br/>在 Spring TestContext 框架中,你可以在`@PostConstruct`和`@PreDestroy`中配置的任何应用程序组件上使用<br/>标准语义。<br/>但是,在实际的测试类中,这些生命周期注释的使用是有限的。<br/><br/>如果测试类中的方法使用`@PostConstruct`进行注释,则该方法在底层测试框架的任何方法之前都会运行<br/>(例如,方法<br/>使用 JUnit Jupiter 的`@BeforeEach`进行注释),这适用于<br/>测试类中的每个测试方法。另一方面,如果测试类中的方法被注释为“@predestroy”,则该方法永远不会运行。因此,在测试类中,我们建议<br/>使用来自底层测试框架的测试生命周期回调,而不是 `@PostConstruct’和`@PreDestroy`。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 3.4.3. Spring JUnit4 测试注释 + +以下注释仅在与[SpringRunner](#testcontext-junit4-runner)、[Spring’s JUnit 4 rules](#testcontext-junit4-rules)或[Spring’s JUnit 4 support classes](#testcontext-support-classes-junit4)结合使用时才受支持: + +* [@ifprofileValue](#integration-testing-annotations-junit4-ifprofilevalue) + +* [@ProfilEvaluesOurceConfiguration](#integration-testing-annotations-junit4-profilevaluesourceconfiguration) + +* [`@Timed`](#integration-testing-annotations-junit4-timed) + +* [`@Repeat`](#integration-testing-annotations-junit4-repeat) + +##### `@IfProfileValue` + +`@IfProfileValue`表示针对特定的测试环境启用了带注释的测试。如果配置的`ProfileValueSource`返回所提供的`name`的匹配`value`,则启用测试。否则,该测试将被禁用,并实际上被忽略。 + +你可以在类级别、方法级别或两者都应用`@IfProfileValue`。对于该类或其子类中的任何方法,`@IfProfileValue`的类级用法优先于方法级用法。具体地说,如果在类级别和方法级别都启用了测试,则将启用该测试。缺少`@IfProfileValue`意味着隐式启用了测试。这类似于 JUnit4 的“@ignore”注释的语义,只是存在`@Ignore`总是会禁用测试。 + +下面的示例显示了一个带有`@IfProfileValue`注释的测试: + +Java + +``` +@IfProfileValue(name="java.vendor", value="Oracle Corporation") (1) +@Test +public void testProcessWhichRunsOnlyOnOracleJvm() { + // some logic that should run only on Java VMs from Oracle Corporation +} +``` + +|**1**|仅当 Java 供应商“甲骨文股份有限公司”时才运行此测试。| +|-----|----------------------------------------------------------------| + +Kotlin + +``` +@IfProfileValue(name="java.vendor", value="Oracle Corporation") (1) +@Test +fun testProcessWhichRunsOnlyOnOracleJvm() { + // some logic that should run only on Java VMs from Oracle Corporation +} +``` + +|**1**|仅当 Java 供应商“甲骨文股份有限公司”时才运行此测试。| +|-----|----------------------------------------------------------------| + +或者,你可以使用`@IfProfileValue`的列表配置`values`(带有`OR`语义),以在 JUnit4 环境中实现对测试组的类似 TestNG 的支持。考虑以下示例: + +Java + +``` +@IfProfileValue(name="test-groups", values={"unit-tests", "integration-tests"}) (1) +@Test +public void testProcessWhichRunsForUnitOrIntegrationTestGroups() { + // some logic that should run only for unit and integration test groups +} +``` + +|**1**|为单元测试和集成测试运行此测试。| +|-----|---------------------------------------------------| + +Kotlin + +``` +@IfProfileValue(name="test-groups", values=["unit-tests", "integration-tests"]) (1) +@Test +fun testProcessWhichRunsForUnitOrIntegrationTestGroups() { + // some logic that should run only for unit and integration test groups +} +``` + +|**1**|为单元测试和集成测试运行此测试。| +|-----|---------------------------------------------------| + +##### `@ProfileValueSourceConfiguration` + +`@ProfileValueSourceConfiguration`是一种类级注释,它指定在检索通过 `@ifprofileValue’注释配置的配置文件值时使用哪种类型的`ProfileValueSource`。如果`@ProfileValueSourceConfiguration`未声明用于测试,则默认情况下使用`SystemProfileValueSource`。下面的示例展示了如何使用`@ProfileValueSourceConfiguration`: + +Java + +``` +@ProfileValueSourceConfiguration(CustomProfileValueSource.class) (1) +public class CustomProfileValueSourceTests { + // class body... +} +``` + +|**1**|使用自定义配置文件的值源。| +|-----|----------------------------------| + +Kotlin + +``` +@ProfileValueSourceConfiguration(CustomProfileValueSource::class) (1) +class CustomProfileValueSourceTests { + // class body... +} +``` + +|**1**|使用自定义配置文件的值源。| +|-----|----------------------------------| + +##### `@Timed` + +`@Timed`表示带注释的测试方法必须在指定的时间内(以毫秒为单位)完成执行。如果文本执行时间超过了指定的时间段,则测试失败。 + +这段时间包括运行测试方法本身、测试的任何重复(参见“@repeat”),以及测试夹具的任何设置或拆除。下面的示例展示了如何使用它: + +Java + +``` +@Timed(millis = 1000) (1) +public void testProcessWithOneSecondTimeout() { + // some logic that should not take longer than 1 second to run +} +``` + +|**1**|将测试的时间段设置为一秒。| +|-----|-----------------------------------------------| + +Kotlin + +``` +@Timed(millis = 1000) (1) +fun testProcessWithOneSecondTimeout() { + // some logic that should not take longer than 1 second to run +} +``` + +|**1**|将测试的时间段设置为一秒。| +|-----|-----------------------------------------------| + +Spring 的`@Timed`注释与 JUnit4 的`@Test(timeout=…​)`支持具有不同的语义。具体地说,由于 JUnit4 处理测试执行超时的方式(即通过在单独的`Thread`中执行测试方法),如果测试时间过长,`@Test(timeout=…​)`会抢先失败测试。 Spring 的`@Timed`,在另一方面,不会先发制人地使测试失败,而是在失败之前等待测试完成。 + +##### `@Repeat` + +`@Repeat`表示必须重复运行带注释的测试方法。在注释中指定了要运行测试方法的次数。 + +要重复的执行范围包括测试方法本身的执行以及测试夹具的任何设置或拆除。当与[SpringMethodrule’](#testcontext-junit4-rules)一起使用时,该作用域还包括由`TestExecutionListener`实现的测试实例的准备。下面的示例展示了如何使用`@Repeat`注释: + +Java + +``` +@Repeat(10) (1) +@Test +public void testProcessRepeatedly() { + // ... +} +``` + +|**1**|把这个测验重复十次。| +|-----|---------------------------| + +Kotlin + +``` +@Repeat(10) (1) +@Test +fun testProcessRepeatedly() { + // ... +} +``` + +|**1**|把这个测验重复十次。| +|-----|---------------------------| + +#### 3.4.4. Spring Junit Jupiter 测试注释 + +当与[“SpringExtension”](#testcontext-junit-jupiter-extension)和 JUnit Jupiter(即 JUnit5 中的编程模型)一起使用时,支持以下注释: + +* [@SpringJunitConfig](#integration-testing-annotations-junit-jupiter-springjunitconfig) + +* [@SpringJunitWebConfig](#integration-testing-annotations-junit-jupiter-springjunitwebconfig) + +* [@testconstructor](#integration-testing-annotations-testconstructor) + +* [@nestedTestConfiguration](#integration-testing-annotations-nestedtestconfiguration) + +* [`@EnabledIf`](#integration-testing-annotations-junit-jupiter-enabledif) + +* [`@DisabledIf`](#integration-testing-annotations-junit-jupiter-disabledif) + +##### `@SpringJUnitConfig` + +`@SpringJUnitConfig`是一个组合注释,它结合了来自 JUnit Jupiter 的 `@extendwith` 和来自 Spring TestContext 框架的`@ContextConfiguration`。它可以在类级别上用作`@ContextConfiguration`的插入替换。关于配置选项,`@ContextConfiguration`和`@SpringJUnitConfig`之间的唯一区别是,组件类可以用`value`中的`value`属性声明。 + +下面的示例展示了如何使用`@SpringJUnitConfig`注释来指定配置类: + +Java + +``` +@SpringJUnitConfig(TestConfig.class) (1) +class ConfigurationClassJUnitJupiterSpringTests { + // class body... +} +``` + +|**1**|指定配置类。| +|-----|--------------------------------| + +Kotlin + +``` +@SpringJUnitConfig(TestConfig::class) (1) +class ConfigurationClassJUnitJupiterSpringTests { + // class body... +} +``` + +|**1**|指定配置类。| +|-----|--------------------------------| + +下面的示例展示了如何使用`@SpringJUnitConfig`注释来指定配置文件的位置: + +Java + +``` +@SpringJUnitConfig(locations = "/test-config.xml") (1) +class XmlJUnitJupiterSpringTests { + // class body... +} +``` + +|**1**|指定配置文件的位置。| +|-----|---------------------------------------------| + +Kotlin + +``` +@SpringJUnitConfig(locations = ["/test-config.xml"]) (1) +class XmlJUnitJupiterSpringTests { + // class body... +} +``` + +|**1**|指定配置文件的位置。| +|-----|---------------------------------------------| + +有关更多详细信息,请参见[上下文管理](#testcontext-ctx-management)以及[@SpringJunitConfig](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/context/junit/jupiter/SpringJUnitConfig.html)和`@ContextConfiguration`的 Javadoc。 + +##### `@SpringJUnitWebConfig` + +`@SpringJUnitWebConfig`是一个组合注释,它结合了来自 JUnit Jupiter 的 `@extendwith` 与`@ContextConfiguration`和来自 Spring TestContext 框架的 `@WebAppConfiguration’。你可以在类级别上使用它作为`@ContextConfiguration`和`@WebAppConfiguration`的插入替换。关于配置选项,`@ContextConfiguration`和`@SpringJUnitWebConfig`之间的唯一区别是,你可以通过使用`@SpringJUnitWebConfig`中的 `value’属性来声明组件类。此外,只需使用 @SpringJunitWebConfig` 中的`resourcePath`属性,就可以覆盖`value`中的`value`属性。 + +下面的示例展示了如何使用`@SpringJUnitWebConfig`注释来指定一个配置类: + +Java + +``` +@SpringJUnitWebConfig(TestConfig.class) (1) +class ConfigurationClassJUnitJupiterSpringWebTests { + // class body... +} +``` + +|**1**|指定配置类。| +|-----|--------------------------------| + +Kotlin + +``` +@SpringJUnitWebConfig(TestConfig::class) (1) +class ConfigurationClassJUnitJupiterSpringWebTests { + // class body... +} +``` + +|**1**|指定配置类。| +|-----|--------------------------------| + +下面的示例展示了如何使用`@SpringJUnitWebConfig`注释来指定配置文件的位置: + +Java + +``` +@SpringJUnitWebConfig(locations = "/test-config.xml") (1) +class XmlJUnitJupiterSpringWebTests { + // class body... +} +``` + +|**1**|指定配置文件的位置。| +|-----|---------------------------------------------| + +Kotlin + +``` +@SpringJUnitWebConfig(locations = ["/test-config.xml"]) (1) +class XmlJUnitJupiterSpringWebTests { + // class body... +} +``` + +|**1**|指定配置文件的位置。| +|-----|---------------------------------------------| + +有关更多详细信息,请参见[上下文管理](#testcontext-ctx-management)以及[@SpringJunitWebConfig](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/context/junit/jupiter/web/SpringJUnitWebConfig.html)、[@contextconfiguration](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/context/ContextConfiguration.html)和[@webappconfiguration](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/context/web/WebAppConfiguration.html)的 Javadoc。 + +##### `@TestConstructor` + +`@TestConstructor`是一种类型级别的注释,用于配置测试类构造函数的参数如何从测试的“应用上下文”中的组件自动连线。 + +如果`@TestConstructor`在测试类上不存在或元存在,则将使用默认的*测试构造函数 AutoWire 模式*。有关如何更改默认模式的详细信息,请参见下面的技巧。但是,请注意,构造函数上的`@Autowired`的本地声明优先于`@TestConstructor`和默认模式。 + +| |更改默认的测试构造函数 autowire 模式<br/><br/>可以通过将 ` Spring.test.constructor.autowire.mode`jvm 系统属性设置为`all`来更改默认的*测试构造函数 AutoWire 模式*。或者,<br/>默认模式可以通过[“SpringProperties”](appendix.html#appendix-spring-properties)机制设置。<br/><br/>在 Spring 框架 5.3 中,默认模式也可以配置为[JUnit 平台配置参数](https://junit.org/junit5/docs/current/user-guide/#running-tests-config-params)。<br/>如果`spring.test.constructor.autowire.mode`属性未设置,则测试类<br/>不会自动连线构造函数。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |在 Spring Framework5.2 中,`@TestConstructor`仅支持与<br/>结合使用的`SpringExtension`用于 Junit Jupiter。请注意,`SpringExtension`是<br/>通常会自动为你注册-例如,当使用诸如 `@SpringJunitConfig` 和`@SpringJUnitWebConfig`之类的注释或来自<br/> Spring 引导测试的各种与测试相关的注释时。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### `@NestedTestConfiguration` + +`@NestedTestConfiguration`是一种类型级别的注释,用于配置 Spring 测试配置注释在为内部测试类封装的类层次结构中的处理方式。 + +如果`@NestedTestConfiguration`在测试类中不存在或不存在,则在其超级类型层次结构中或在其封闭的类层次结构中,将使用默认的*封闭配置继承模式*。有关如何更改默认模式的详细信息,请参见下面的技巧。 + +| |更改默认的封闭配置继承模式<br/><br/>默认的*封闭配置继承模式*是`INHERIT`,但是可以通过将<br/>JVM 系统属性设置为 `override’来更改。或者,可以通过[“SpringProperties”](appendix.html#appendix-spring-properties)机制设置默认模式。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +[Spring TestContext Framework](#testcontext-framework)为下面的注释提供了`@NestedTestConfiguration`语义。 + +* [@bootstrapwith](#spring-testing-annotation-bootstrapwith) + +* [@contextconfiguration](#spring-testing-annotation-contextconfiguration) + +* [@webappconfiguration](#spring-testing-annotation-webappconfiguration) + +* [@contexthierarchy](#spring-testing-annotation-contexthierarchy) + +* [@ActiveProfiles](#spring-testing-annotation-activeprofiles) + +* [@TestPropertySource](#spring-testing-annotation-testpropertysource) + +* [@DynamicPropertySource](#spring-testing-annotation-dynamicpropertysource) + +* [@dirtiescontext](#spring-testing-annotation-dirtiescontext) + +* [@TestexecutionListeners](#spring-testing-annotation-testexecutionlisteners) + +* [@RecordApplicationEvents](#spring-testing-annotation-recordapplicationevents) + +* [@transactional`](#testcontext-tx) + +* [`@Commit`](#spring-testing-annotation-commit) + +* [`@Rollback`](#spring-testing-annotation-rollback) + +* [`@Sql`](#spring-testing-annotation-sql) + +* [`@SqlConfig`](#spring-testing-annotation-sqlconfig) + +* [`@SqlMergeMode`](#spring-testing-annotation-sqlmergemode) + +* [@testconstructor](#integration-testing-annotations-testconstructor) + +| |在 JUnit Jupiter 中,使用`@NestedTestConfiguration`通常仅在<br/>与`@Nested`测试类结合时才有意义;但是,可能存在其他支持 Spring 的测试<br/>框架和使用这种<br/>注释的嵌套测试类。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +有关示例和更多详细信息,请参见[@nested 测试类配置](#testcontext-junit-jupiter-nested-test-configuration)。 + +##### `@EnabledIf` + +`@EnabledIf`用于表示启用了注释的 JUnit Jupiter 测试类或测试方法,并且如果提供的`expression`计算为`true`,则应该运行该方法。具体地说,如果表达式的求值为`Boolean.TRUE`或`String`等于`true`(忽略情况),则启用测试。当应用于类级别时,该类中的所有测试方法在默认情况下也会自动启用。 + +表达式可以是以下任何一种: + +* [Spring Expression Language](core.html#expressions)表达式。例如:@enableDIF(“#{systemProperties[’os.name’].tolowercase().contains}”) + +* Spring [`Environment`](core.html#beans-environment)中可用的属性的占位符。例如:`@EnabledIf("${smoke.tests.enabled}")` + +* 文字文字。例如:`@EnabledIf("true")` + +然而,请注意,不是动态解析属性占位符的结果的文本文字是零实用价值的,因为`@EnabledIf("false")`等价于`@Disabled`和`@EnabledIf("true")`在逻辑上是没有意义的。 + +你可以使用`@EnabledIf`作为元注释来创建自定义组合注释。例如,你可以创建一个自定义`@EnabledOnMac`注释,如下所示: + +爪哇 + +``` +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@EnabledIf( + expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}", + reason = "Enabled on Mac OS" +) +public @interface EnabledOnMac {} +``` + +Kotlin + +``` +@Target(AnnotationTarget.TYPE, AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +@EnabledIf( + expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}", + reason = "Enabled on Mac OS" +) +annotation class EnabledOnMac {} +``` + +##### `@DisabledIf` + +`@DisabledIf`用于表示注释的 JUnit Jupiter 测试类或测试方法被禁用,并且如果提供的`expression`计算为 `true’,则不应运行该测试方法。具体地说,如果表达式的求值为`Boolean.TRUE`或`String`等于`true`(忽略情况),则禁用测试。当应用于类级别时,该类中的所有测试方法也会自动禁用。 + +表达式可以是以下任何一种: + +* [Spring Expression Language](core.html#expressions)表达式。例如:`@disabledif(“#{systemproperties[’os.name’].tolowercase().contains}”) + +* Spring [`Environment`](core.html#beans-environment)中可用的属性的占位符。例如:`@DisabledIf("${smoke.tests.disabled}")` + +* 文字文字。例如:`@DisabledIf("true")` + +然而,请注意,不是动态解析属性占位符的结果的文本文字是零实用价值的,因为`@DisabledIf("true")`等价于`@Disabled`和`@DisabledIf("false")`在逻辑上是没有意义的。 + +你可以使用`@DisabledIf`作为元注释来创建自定义组合注释。例如,你可以创建一个自定义`@DisabledOnMac`注释,如下所示: + +爪哇 + +``` +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@DisabledIf( + expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}", + reason = "Disabled on Mac OS" +) +public @interface DisabledOnMac {} +``` + +Kotlin + +``` +@Target(AnnotationTarget.TYPE, AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +@DisabledIf( + expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}", + reason = "Disabled on Mac OS" +) +annotation class DisabledOnMac {} +``` + +#### 3.4.5.测试的元注释支持 + +你可以使用大多数与测试相关的注释[元注释](core.html#beans-meta-annotations)来创建定制的组合注释,并在测试套件中减少配置重复。 + +你可以结合[TestContext 框架](#testcontext-framework)使用以下每一项作为元注释。 + +* `@BootstrapWith` + +* `@ContextConfiguration` + +* `@ContextHierarchy` + +* `@ActiveProfiles` + +* `@TestPropertySource` + +* `@DirtiesContext` + +* `@WebAppConfiguration` + +* `@TestExecutionListeners` + +* `@Transactional` + +* `@BeforeTransaction` + +* `@AfterTransaction` + +* `@Commit` + +* `@Rollback` + +* `@Sql` + +* `@SqlConfig` + +* `@SqlMergeMode` + +* `@SqlGroup` + +* `@Repeat` *(仅在 JUnit4 上支持)* + +* `@Timed` *(仅在 JUnit4 上支持)* + +* `@IfProfileValue` *(仅在 JUnit4 上支持)* + +* `@ProfileValueSourceConfiguration` *(仅在 JUnit4 上支持)* + +* `@SpringJUnitConfig` *(仅在 Junit Jupiter 上支持)* + +* `@SpringJUnitWebConfig` *(仅在 Junit Jupiter 上支持)* + +* `@TestConstructor` *(仅在 Junit Jupiter 上支持)* + +* `@NestedTestConfiguration` *(仅在 Junit Jupiter 上支持)* + +* `@EnabledIf` *(仅在 Junit Jupiter 上支持)* + +* `@DisabledIf` *(仅在 Junit Jupiter 上支持)* + +考虑以下示例: + +爪哇 + +``` +@RunWith(SpringRunner.class) +@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"}) +@ActiveProfiles("dev") +@Transactional +public class OrderRepositoryTests { } + +@RunWith(SpringRunner.class) +@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"}) +@ActiveProfiles("dev") +@Transactional +public class UserRepositoryTests { } +``` + +Kotlin + +``` +@RunWith(SpringRunner::class) +@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml") +@ActiveProfiles("dev") +@Transactional +class OrderRepositoryTests { } + +@RunWith(SpringRunner::class) +@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml") +@ActiveProfiles("dev") +@Transactional +class UserRepositoryTests { } +``` + +如果我们发现我们在基于 JUnit4 的测试套件中重复了前面的配置,那么我们可以通过引入一个自定义组合注释来减少重复,该注释集中了 Spring 的公共测试配置,如下所示: + +爪哇 + +``` +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"}) +@ActiveProfiles("dev") +@Transactional +public @interface TransactionalDevTestConfig { } +``` + +Kotlin + +``` +@Target(AnnotationTarget.TYPE) +@Retention(AnnotationRetention.RUNTIME) +@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml") +@ActiveProfiles("dev") +@Transactional +annotation class TransactionalDevTestConfig { } +``` + +然后,我们可以使用自定义的`@TransactionalDevTestConfig`注释来简化基于 JUnit4 的测试类的配置,如下所示: + +爪哇 + +``` +@RunWith(SpringRunner.class) +@TransactionalDevTestConfig +public class OrderRepositoryTests { } + +@RunWith(SpringRunner.class) +@TransactionalDevTestConfig +public class UserRepositoryTests { } +``` + +Kotlin + +``` +@RunWith(SpringRunner::class) +@TransactionalDevTestConfig +class OrderRepositoryTests + +@RunWith(SpringRunner::class) +@TransactionalDevTestConfig +class UserRepositoryTests +``` + +如果我们编写使用 JUnit Jupiter 的测试,我们可以进一步减少代码重复,因为 JUnit5 中的注释也可以用作元注释。考虑以下示例: + +爪哇 + +``` +@ExtendWith(SpringExtension.class) +@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"}) +@ActiveProfiles("dev") +@Transactional +class OrderRepositoryTests { } + +@ExtendWith(SpringExtension.class) +@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"}) +@ActiveProfiles("dev") +@Transactional +class UserRepositoryTests { } +``` + +Kotlin + +``` +@ExtendWith(SpringExtension::class) +@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml") +@ActiveProfiles("dev") +@Transactional +class OrderRepositoryTests { } + +@ExtendWith(SpringExtension::class) +@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml") +@ActiveProfiles("dev") +@Transactional +class UserRepositoryTests { } +``` + +如果我们发现我们在基于 JUnit Jupiter 的测试套件中重复前面的配置,那么我们可以通过引入一个自定义的组合注释来减少重复,该注释集中了 Spring 和 JUnit Jupiter 的公共测试配置,如下所示: + +爪哇 + +``` +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(SpringExtension.class) +@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"}) +@ActiveProfiles("dev") +@Transactional +public @interface TransactionalDevTestConfig { } +``` + +Kotlin + +``` +@Target(AnnotationTarget.TYPE) +@Retention(AnnotationRetention.RUNTIME) +@ExtendWith(SpringExtension::class) +@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml") +@ActiveProfiles("dev") +@Transactional +annotation class TransactionalDevTestConfig { } +``` + +然后,我们可以使用自定义`@TransactionalDevTestConfig`注释来简化基于 JUnit Jupiter 的各个测试类的配置,如下所示: + +爪哇 + +``` +@TransactionalDevTestConfig +class OrderRepositoryTests { } + +@TransactionalDevTestConfig +class UserRepositoryTests { } +``` + +Kotlin + +``` +@TransactionalDevTestConfig +class OrderRepositoryTests { } + +@TransactionalDevTestConfig +class UserRepositoryTests { } +``` + +由于 JUnit Jupiter 支持使用`@Test`、`@RepeatedTest`、`ParameterizedTest`和其他作为元注释的方法,因此你还可以在测试方法级别上创建自定义组合注释。例如,如果我们希望创建一个组合注释,将来自 Junit Jupiter 的`@Test`和`@Tag`注释与来自 Spring 的`@Transactional`注释结合在一起,那么我们可以创建一个`@TransactionalIntegrationTest`注释,如下所示: + +爪哇 + +``` +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Transactional +@Tag("integration-test") // org.junit.jupiter.api.Tag +@Test // org.junit.jupiter.api.Test +public @interface TransactionalIntegrationTest { } +``` + +Kotlin + +``` +@Target(AnnotationTarget.TYPE) +@Retention(AnnotationRetention.RUNTIME) +@Transactional +@Tag("integration-test") // org.junit.jupiter.api.Tag +@Test // org.junit.jupiter.api.Test +annotation class TransactionalIntegrationTest { } +``` + +然后我们可以使用我们的自定义`@TransactionalIntegrationTest`注释来简化基于 JUnit Jupiter 的单个测试方法的配置,如下所示: + +爪哇 + +``` +@TransactionalIntegrationTest +void saveOrder() { } + +@TransactionalIntegrationTest +void deleteOrder() { } +``` + +Kotlin + +``` +@TransactionalIntegrationTest +fun saveOrder() { } + +@TransactionalIntegrationTest +fun deleteOrder() { } +``` + +有关更多详细信息,请参见[Spring Annotation Programming Model](https://github.com/spring-projects/spring-framework/wiki/Spring-Annotation-Programming-Model)维基页面。 + +### 3.5. Spring TestContext 框架 + +Spring TestContext 框架(位于`org.springframework.test.context`包中)提供了通用的、注释驱动的单元和集成测试支持,这与正在使用的测试框架无关。TestContext 框架还非常重视约定而不是配置,使用合理的默认值,你可以通过基于注释的配置来覆盖这些默认值。 + +除了通用的测试基础设施之外,TestContext 框架还为 JUnit4、JUnit Jupiter(AKA 为 JUnit5)和 TestNG 提供了明确的支持。对于 JUnit4 和 TestNG, Spring 提供了`abstract`支持类。此外, Spring 为 JUnit4 提供了自定义的 JUnit`Runner`和自定义的 JUnit`Rules`,为 JUnit Jupiter 提供了自定义的`Extension`,允许你编写所谓的 POJO 测试类。扩展特定的类层次结构不需要 POJO 测试类,例如`abstract`支持类。 + +下一节将概述 TestContext 框架的内部内容。如果你只对使用框架感兴趣,而对使用自己的自定义侦听器或自定义加载器扩展框架不感兴趣,可以直接访问配置([上下文管理](#testcontext-ctx-management)、[依赖注入](#testcontext-fixture-di)、[事务管理](#testcontext-tx))、[support classes](#testcontext-support-classes)和[注释支持](#integration-testing-annotations)部分。 + +#### 3.5.1.关键抽象 + +框架的核心包括`TestContextManager`类和 `TestContext’、`TestExecutionListener`和`SmartContextLoader`接口。为每个测试类创建一个“TestContextManager”(例如,用于在 JUnitJupiter 中的单个测试类中执行所有测试方法)。而`TestContext`则管理保存当前测试上下文的`TestContext`。随着测试的进行,“TestContextManager”还会更新`TestContext`的状态,并将其委托给`TestExecutionListener`实现,该实现通过提供依赖注入、管理事务等来实现实际的测试执行。SmartContextLoader 负责为给定的测试类加载`ApplicationContext`。有关更多信息和各种实现的示例,请参见[javadoc](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/context/package-summary.html)和 Spring 测试套件。 + +##### `TestContext` + +`TestContext`封装了运行测试的上下文(与实际使用的测试框架无关),并为它负责的测试实例提供了上下文管理和缓存支持。`TestContext`还将委托给 `smartcontextloader’,以便在需要时加载`ApplicationContext`。 + +##### `TestContextManager` + +`TestContextManager`是 Spring TestContext 框架的主要入口点,负责管理单个`TestContext`,并在定义良好的测试执行点向每个注册的 `TestexecutionListener’发送事件信号: + +* 在特定测试框架的任何“类之前”或“所有之前”方法之前。 + +* 测试实例后处理。 + +* 在特定测试框架的任何“before”或“before each”方法之前。 + +* 在测试方法执行之前,但在测试设置之后. + +* 在测试方法执行后但在测试前立即拆除。 + +* 在特定测试框架的任何“之后”或“之后”方法之后。 + +* 在特定测试框架的任何“课后”或“毕竟”方法之后。 + +##### `TestExecutionListener` + +`TestExecutionListener`定义了对注册侦听器的`TestContextManager`发布的测试执行事件做出反应的 API。见[“TestexecutionListener”配置](#testcontext-tel-config)。 + +##### 上下文加载程序 + +`ContextLoader`是用于为 Spring TestContext 框架管理的集成测试加载`ApplicationContext`的策略接口。你应该实现“SmartContextLoader”而不是这个接口,以提供对组件类、活动 Bean 定义配置文件、测试属性源、上下文层次结构和“WebApplicationContext”支持的支持。 + +`SmartContextLoader`是`ContextLoader`接口的扩展,它取代了原来的极小值`ContextLoader`SPI。具体地说,`SmartContextLoader`可以选择处理资源位置、组件类或上下文初始化器。此外,“SmartContextLoader”可以在其加载的上下文中设置活动的 Bean 定义配置文件并测试属性源。 + +Spring 提供了以下实现方式: + +* `DelegatingSmartContextLoader`:两个默认加载器之一,它在内部委托给一个`AnnotationConfigContextLoader`、一个`GenericXmlContextLoader`或一个 `GenericGroovyxmlContextLoader’,这取决于为测试类声明的配置,或者取决于缺省位置或缺省配置类的存在。仅当 Groovy 位于 Classpath 上时,才启用 Groovy 支持。 + +* `WebDelegatingSmartContextLoader`:两个默认加载器之一,它在内部委托给一个`AnnotationConfigWebContextLoader`、一个`GenericXmlWebContextLoader`或一个 `GenericGroovyXMLWebContextLoader’,这取决于为测试类声明的配置,或者取决于缺省位置或缺省配置类的存在。只有当测试类上存在`@WebAppConfiguration`时,才使用 Web`ContextLoader`。仅当 Groovy 位于 Classpath 上时,才启用 Groovy 支持。 + +* `AnnotationConfigContextLoader`:从组件类加载标准的`ApplicationContext`。 + +* `AnnotationConfigWebContextLoader`:从组件类加载`WebApplicationContext`。 + +* `GenericGroovyXmlContextLoader`:从 Groovy 脚本或 XML 配置文件的资源位置加载标准的`ApplicationContext`。 + +* `GenericGroovyXmlWebContextLoader`:从 Groovy 脚本或 XML 配置文件的资源位置加载`WebApplicationContext`。 + +* `GenericXmlContextLoader`:从 XML 资源位置加载标准的`ApplicationContext`。 + +* `GenericXmlWebContextLoader`:从 XML 资源位置加载`WebApplicationContext`。 + +#### 3.5.2.引导 TestContext 框架 + +Spring TestContext 框架内部的默认配置对于所有常见的用例都足够了。但是,有时开发团队或第三方框架希望更改默认的`ContextLoader`,实现自定义的`TestContext`或`ContextCache`,增加 `contextcustomizerFactory’和<gtr="1004"/>实现的默认集,等等。对于这种对 TestContext 框架如何操作的低级控制, Spring 提供了一种引导策略。 + +`TestContextBootstrapper`定义了引导 TestContext 框架的 SPI。“TestContextBootstrapper”由`AfterTestClassEvent`用于加载当前测试的“TestexecutionListener”实现,并构建其管理的“TestContext”。你可以直接使用`@BootstrapWith`或作为元注释,为测试类(或测试类层次结构)配置自定义引导策略。如果未使用 @bootstrapwith 显式配置引导程序,则根据`@WebAppConfiguration`的存在,使用`DefaultTestContextBootstrapper`或 `webtestContextBootstrapper’。 + +由于`TestContextBootstrapper`SPI 在未来可能会发生变化(以适应新的需求),因此我们强烈鼓励实现者不要直接实现这个接口,而是扩展`AbstractTestContextBootstrapper`或它的一个具体子类。 + +#### 3.5.3.`TestExecutionListener`配置 + +Spring 提供了以下`TestExecutionListener`默认情况下注册的实现,其顺序完全如下: + +* `ServletTestExecutionListener`:为“WebApplicationContext”配置 Servlet API 模拟。 + +* `DirtiesContextBeforeModesTestExecutionListener`:处理“before”模式的`@DirtiesContext`注释。 + +* `ApplicationEventsTestExecutionListener`:提供对[` 应用事件’](#testcontext-application-events)的支持。 + +* `DependencyInjectionTestExecutionListener`:为测试实例提供依赖注入。 + +* `DirtiesContextTestExecutionListener`:处理“after”模式的`@DirtiesContext`注释。 + +* `TransactionalTestExecutionListener`:提供带有默认回滚语义的事务测试执行。 + +* `SqlScriptsTestExecutionListener`:运行使用`@Sql`注释配置的 SQL 脚本。 + +* `EventPublishingTestExecutionListener`:将测试执行事件发布到测试的 `ApplicationContext’(参见[测试执行事件](#testcontext-test-execution-events))。 + +##### 注册`TestExecutionListener`实现 + +你可以使用`@TestExecutionListeners`注释来注册测试类及其子类的`TestExecutionListener`实现。有关详细信息和示例,请参见[注释支持](#integration-testing-annotations)和[@TestexecutionListeners](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/context/TestExecutionListeners.html)的 爪哇doc。 + +##### 自动发现默认`TestExecutionListener`实现 + +通过使用`TestExecutionListener`注册`TestExecutionListener`实现适合于在有限的测试场景中使用的自定义侦听器。但是,如果需要在整个测试套件中使用自定义侦听器,那么它可能会变得很麻烦。这个问题通过支持通过`SpringFactoriesLoader`机制自动发现默认的 `TestexecutionListener’实现来解决。 + +具体地说,`spring-test`模块在其`META-INF/spring.factories`属性文件中的`org.springframework.test.context.TestExecutionListener`键下声明所有核心默认`TestExecutionListener`实现。第三方框架和开发人员可以通过他们自己的`ServletTestExecutionListener`属性文件以同样的方式向默认侦听器列表贡献他们自己的`TestExecutionListener`实现。 + +##### 排序`TestExecutionListener`实现 + +当 TestContext 框架通过[aforementioned](#testcontext-tel-config-automatic-discovery)“SpringFactoriesLoader”机制发现默认的`TestExecutionListener`实现时,实例化的侦听器将通过使用 Spring 的`AnnotationAwareOrderComparator`进行排序,该接口尊重 Spring 的`Ordered`接口和用于排序的 `@order’注释。`AbstractTestExecutionListener`和 Spring 提供的所有默认的 `TestexecutionListener’实现用适当的值实现`Ordered`。因此,第三方框架和开发人员应该通过实现`Ordered`或声明`@Order`来确保其默认的`TestExecutionListener`实现是按正确的顺序注册的。关于分配给每个核心侦听器的值的详细信息,请参见 爪哇doc 获取核心默认`getOrder()`实现的`TestExecutionListener`方法。 + +##### 合并`TestExecutionListener`实现 + +如果通过`@TestExecutionListeners`注册了自定义`TestExecutionListener`,则不会注册默认侦听器。在大多数常见的测试场景中,这会有效地迫使开发人员手动声明除自定义侦听器之外的所有默认侦听器。下面的清单演示了这种配置风格: + +爪哇 + +``` +@ContextConfiguration +@TestExecutionListeners({ + MyCustomTestExecutionListener.class, + ServletTestExecutionListener.class, + DirtiesContextBeforeModesTestExecutionListener.class, + DependencyInjectionTestExecutionListener.class, + DirtiesContextTestExecutionListener.class, + TransactionalTestExecutionListener.class, + SqlScriptsTestExecutionListener.class +}) +class MyTest { + // class body... +} +``` + +Kotlin + +``` +@ContextConfiguration +@TestExecutionListeners( + MyCustomTestExecutionListener::class, + ServletTestExecutionListener::class, + DirtiesContextBeforeModesTestExecutionListener::class, + DependencyInjectionTestExecutionListener::class, + DirtiesContextTestExecutionListener::class, + TransactionalTestExecutionListener::class, + SqlScriptsTestExecutionListener::class +) +class MyTest { + // class body... +} +``` + +这种方法的挑战在于,它要求开发人员准确地知道默认注册了哪些侦听器。此外,缺省侦听器集可以从一个版本更改到另一个版本——例如,`SqlScriptsTestExecutionListener`在 Spring Framework4.1 中引入,`DirtiesContextBeforeModesTestExecutionListener`在 Spring Framework4.2 中引入。此外,像 Spring 引导和 Spring 安全之类的第三方框架通过使用前述的[自动发现机制](#testcontext-tel-config-automatic-discovery)注册它们自己的默认`TestExecutionListener`实现。 + +为了避免必须意识到并重新声明所有默认侦听器,你可以将`@TestExecutionListeners`的 `mergemode’属性设置为`MergeMode.MERGE_WITH_DEFAULTS`。`merge_with_defaults’表示本地声明的侦听器应该与默认侦听器合并。合并算法确保从列表中删除重复项,并根据`AnnotationAwareOrderComparator`的语义对合并的侦听器集进行排序,如[Ordering `TestExecutionListener` Implementations](#testcontext-tel-config-ordering)中所述。如果一个侦听器实现了`@TestExecutionListeners`,或者用`@Order`进行了注释,那么它可能会影响它与默认值合并的位置。否则,在合并时,本地声明的侦听器将被追加到默认侦听器列表中。 + +例如,如果上一个示例中的[AssertJ](https://assertj.github.io/doc/)类将其`order`值(例如,`500`)配置为小于“servletteStexecutionListener”的顺序(恰好是`1000`),然后,可以将“myCustomTestexecutionListener”与`ServletTestExecutionListener`前面的默认值列表自动合并,并且可以用以下示例替换前面的示例: + +爪哇 + +``` +@ContextConfiguration +@TestExecutionListeners( + listeners = MyCustomTestExecutionListener.class, + mergeMode = MERGE_WITH_DEFAULTS +) +class MyTest { + // class body... +} +``` + +Kotlin + +``` +@ContextConfiguration +@TestExecutionListeners( + listeners = [MyCustomTestExecutionListener::class], + mergeMode = MERGE_WITH_DEFAULTS +) +class MyTest { + // class body... +} +``` + +#### 3.5.4.应用程序事件 + +Spring 框架 5.3.3 以来,TestContext 框架提供了对在 `ApplicationContext’中发布的[应用程序事件](core.html#context-functionality-events)的记录的支持,以便可以针对测试中的那些事件执行断言。在执行单个测试期间发布的所有事件都可以通过`ApplicationEvents`API 获得,该 API 允许你以 `java.util.stream’的形式处理这些事件。 + +要在测试中使用`ApplicationEvents`,请执行以下操作。 + +* 确保你的测试类是用[@RecordApplicationEvents](#spring-testing-annotation-recordapplicationevents)进行注释或元注释的。 + +* 确保`ApplicationEventsTestExecutionListener`已注册。但是,请注意,`ApplicationEventsTestExecutionListener`是默认注册的,只有当你通过不包括默认侦听器的 `@TestexecutionListeners’进行自定义配置时,才需要手动注册。 + +* 在`ApplicationEvents`类型的字段中使用`@Autowired`进行注释,并在你的测试和生命周期方法中使用“ApplicationEvents”的实例(例如 JUnit Jupiter 中的`@BeforeEach`和“@afteReach”方法)。 + + * 当使用`@Autowired`时,可以在测试或生命周期方法中声明类型为`ApplicationEvents`的方法参数,作为测试类中`@Autowired`字段的替代。 + +下面的测试类使用`SpringExtension`for JUnit Jupiter 和[AssertJ](https://assertj.github.io/doc/)来断言在 Spring 管理的组件中调用方法时发布的应用程序事件的类型: + +爪哇 + +``` +@SpringJUnitConfig(/* ... */) +@RecordApplicationEvents (1) +class OrderServiceTests { + + @Autowired + OrderService orderService; + + @Autowired + ApplicationEvents events; (2) + + @Test + void submitOrder() { + // Invoke method in OrderService that publishes an event + orderService.submitOrder(new Order(/* ... */)); + // Verify that an OrderSubmitted event was published + long numEvents = events.stream(OrderSubmitted.class).count(); (3) + assertThat(numEvents).isEqualTo(1); + } +} +``` + +|**1**|用`@RecordApplicationEvents`注释测试类。| +|-----|-----------------------------------------------------------------------------------------| +|**2**|为当前测试注入`@RecordApplicationEvents`实例。| +|**3**|使用`ApplicationEvents`API 来计算发布了多少`OrderSubmitted`事件。| + +Kotlin + +``` +@SpringJUnitConfig(/* ... */) +@RecordApplicationEvents (1) +class OrderServiceTests { + + @Autowired + lateinit var orderService: OrderService + + @Autowired + lateinit var events: ApplicationEvents (2) + + @Test + fun submitOrder() { + // Invoke method in OrderService that publishes an event + orderService.submitOrder(Order(/* ... */)) + // Verify that an OrderSubmitted event was published + val numEvents = events.stream(OrderSubmitted::class).count() (3) + assertThat(numEvents).isEqualTo(1) + } +} +``` + +|**1**|用`@RecordApplicationEvents`注释测试类。| +|-----|-----------------------------------------------------------------------------------------| +|**2**|为当前测试注入`ApplicationEvents`实例。| +|**3**|使用`@RecordApplicationEvents`API 来计算发布了多少`OrderSubmitted`事件。| + +有关`ApplicationEvents`API 的更多详细信息,请参见[“ApplicationEvents”爪哇doc](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/context/event/ApplicationEvents.html)。 + +#### 3.5.5.测试执行事件 + +Spring Framework5.2 中引入的`EventPublishingTestExecutionListener`提供了一种实现自定义`TestExecutionListener`的替代方法。测试的`ApplicationContext`中的组件可以监听“EventPublishingTestexeCutionListener”发布的以下事件,每个事件对应于“TestexeCutionListener”API 中的一个方法。 + +* `BeforeTestClassEvent` + +* `PrepareTestInstanceEvent` + +* `BeforeTestMethodEvent` + +* `BeforeTestExecutionEvent` + +* `AfterTestExecutionEvent` + +* `AfterTestMethodEvent` + +* `AfterTestClassEvent` + +| |只有在[上下文配置继承](#testcontext-ctx-management-inheritance)已经加载的情况下,这些事件才会发布。| +|---|------------------------------------------------------------------------------------| + +这些事件可能由于各种原因而被使用,例如重置模拟 bean 或跟踪测试执行。使用测试执行事件而不是实现自定义`TestExecutionListener`的一个优点是,测试执行事件可以被注册在测试`ApplicationContext`中的任何 Spring Bean 消耗,并且这样的 bean 可以直接受益于依赖注入和`ApplicationContext`的其他特征。与此相反,`TestExecutionListener`不是`ApplicationContext`中的 Bean。 + +为了侦听测试执行事件, Spring Bean 可以选择实现 `org.SpringFramework.Context.ApplicationListener’接口。或者,侦听器方法可以使用`@EventListener`进行注释,并配置为侦听上面列出的特定事件类型之一(参见[基于注释的事件侦听器](core.html#context-functionality-events-annotation))。由于这种方法的流行, Spring 提供了以下专用的 `@EventListener’注释,以简化测试执行事件侦听器的注册。这些注释驻留在`org.springframework.test.context.event.annotation`包中。 + +* `@BeforeTestClass` + +* `@PrepareTestInstance` + +* `@BeforeTestMethod` + +* `@BeforeTestExecution` + +* `@AfterTestExecution` + +* `@AfterTestMethod` + +* `@AfterTestClass` + +##### 异常处理 + +默认情况下,如果测试执行事件侦听器在使用事件时抛出异常,则该异常将传播到使用中的底层测试框架(例如 JUnit 或 TestNG)。例如,如果消耗`@EventListener`导致异常,则相应的测试方法将作为异常的结果而失败。相反,如果异步测试执行事件侦听器抛出异常,则异常将不会传播到底层测试框架。有关异步异常处理的更多详细信息,请参阅类级 爪哇doc for`@EventListener`。 + +##### 异步侦听器 + +如果你希望一个特定的测试执行事件侦听器异步地处理事件,那么你可以使用 Spring 的[常规的“@async”支持](integration.html#scheduling-annotation-support-async)。有关更多详细信息,请咨询类级 爪哇doc 中的“@EventListener”。 + +#### 3.5.6.上下文管理 + +每个`TestContext`都为其负责的测试实例提供了上下文管理和缓存支持。测试实例不会自动接收对配置的`ApplicationContext`的访问。但是,如果测试类实现了“ApplicationContextAware”接口,那么将向测试实例提供对`@BeforeTestMethod`的引用。请注意,`AbstractJUnit4SpringContextTests`和 `AbstractTestngSpringContexttTests’实现`ApplicationContextAware`,因此,自动提供对`ApplicationContext`的访问。 + +| |@Autowired ApplicationContext<br/><br/>作为实现`ApplicationContextAware`接口的替代方案,你可以通过`@Autowired`a 字段或 setter 方法上的`@Autowired`注释,为你的测试类注入应用程序上下文,如下例所示:<br/><br/>java<br/><br/>`<br/>@springjunitconfig<1144"/>class mytest{<br/><br/><br/>上下文<1147"/>>>><gt="1147"/>gt=“gt=”主体“./>><Application="/><1150">>>>><Application=">>>>><151<gt=">>>>>>>>><|**1**|Injecting the `ApplicationContext`.|<br/>|-----|-----------------------------------|<br/><br/>Kotlin<br/><br/>```<br/>@SpringJUnitConfig<br/>class MyTest {<br/><br/> @Autowired (1)<br/> lateinit var applicationContext: ApplicationContext<br/><br/> // class body...<br/>}<br/>```<br/><br/>|**1**|Injecting the `ApplicationContext`.|<br/>|-----|-----------------------------------|<br/><br/>Similarly, if your test is configured to load a `WebApplicationContext`, you can inject<br/>the web application context into your test, as follows:<br/><br/>爪哇<br/><br/>```<br/>@SpringJUnitWebConfig (1)<br/>class MyWebAppTest {<br/><br/> @Autowired (2)<br/> WebApplicationContext wac;<br/><br/> // class body...<br/>}<br/>```<br/><br/>|**1**|Configuring the `WebApplicationContext`.|<br/>|-----|----------------------------------------|<br/>|**2**| Injecting the `WebApplicationContext`. |<br/><br/>Kotlin<br/><br/>```<br/>@SpringJUnitWebConfig (1)<br/>class MyWebAppTest {<br/><br/> @Autowired (2)<br/> lateinit var wac: WebApplicationContext<br/> // class body...<br/>}<br/>```<br/><br/>|**1**|Configuring the `WebApplicationContext`.|<br/>|-----|----------------------------------------|<br/>|**2**| Injecting the `WebApplicationContext`. |<br/><br/>Dependency injection by using `@Autowired` is provided by the`DependencyInjectionTestExecutionListener`, which is configured by default<br/>(see [Dependency Injection of Test Fixtures](#testcontext-fixture-di)).| +|-----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**1**|注入`ApplicationContext`。| +|**1**|注入`ApplicationContext`。| +|**1**|配置`WebApplicationContext`。| +|**2**|注入`WebApplicationContext`。| +|**1**|配置`WebApplicationContext`。| +|**2**|注入`WebApplicationContext`。| + +使用 TestContext 框架的测试类不需要扩展任何特定的类或实现特定的接口来配置它们的应用程序上下文。相反,配置是通过在类级别声明`@ContextConfiguration`注释来实现的。如果你的测试类没有显式地声明应用程序上下文资源位置或组件类,那么配置的`ContextLoader`将决定如何从默认位置或默认配置类加载上下文。除了上下文资源位置和组件类之外,还可以通过应用程序上下文初始化器来配置应用程序上下文。 + +下面的部分解释了如何使用 Spring 的`@ContextConfiguration`注释来通过使用 XML 配置文件、Groovy 脚本、组件类(通常是`@Configuration`类)或上下文初始化器来配置测试`ApplicationContext`。或者,你可以为高级用例实现和配置你自己的自定义`SmartContextLoader`。 + +* [使用 XML 资源的上下文配置](#testcontext-ctx-management-xml) + +* [使用 Groovy 脚本的上下文配置](#testcontext-ctx-management-groovy) + +* [具有组件类的上下文配置](#testcontext-ctx-management-javaconfig) + +* [混合 XML、Groovy 脚本和组件类](#testcontext-ctx-management-mixed-config) + +* [带有上下文初始化器的上下文配置](#testcontext-ctx-management-initializers) + +* [上下文配置继承](#testcontext-ctx-management-inheritance) + +* [具有环境配置文件的上下文配置](#testcontext-ctx-management-env-profiles) + +* [具有测试属性源的上下文配置](#testcontext-ctx-management-property-sources) + +* [具有动态属性源的上下文配置](#testcontext-ctx-management-dynamic-property-sources) + +* [装入`WebApplicationContext`](#testcontext-ctx-management-web) + +* [上下文缓存](#testcontext-ctx-management-caching) + +* [上下文层次结构](#testcontext-ctx-management-ctx-hierarchies) + +##### 使用 XML 资源的上下文配置 + +要通过使用 XML 配置文件为测试加载`ApplicationContext`,请用`@ContextConfiguration`注释测试类,并使用包含 XML 配置元数据资源位置的数组配置`locations`属性。普通或相对路径(例如,`context.xml`)被视为相对于定义测试类的包的 Classpath 资源。以斜杠开头的路径被视为绝对 Classpath 位置(例如,`/org/example/config.xml`)。表示资源 URL 的路径(即带有`classpath:`、`file:`、<http:` 等前缀的路径)被使用。 + +爪哇 + +``` +@ExtendWith(SpringExtension.class) +// ApplicationContext will be loaded from "/app-config.xml" and +// "/test-config.xml" in the root of the classpath +@ContextConfiguration(locations={"/app-config.xml", "/test-config.xml"}) (1) +class MyTest { + // class body... +} +``` + +|**1**|将 locations 属性设置为 XML 文件列表。| +|-----|-------------------------------------------------------| + +Kotlin + +``` +@ExtendWith(SpringExtension::class) +// ApplicationContext will be loaded from "/app-config.xml" and +// "/test-config.xml" in the root of the classpath +@ContextConfiguration("/app-config.xml", "/test-config.xml") (1) +class MyTest { + // class body... +} +``` + +|**1**|将 locations 属性设置为 XML 文件列表。| +|-----|-------------------------------------------------------| + +`@ContextConfiguration`通过标准的 爪哇`value`属性支持`locations`属性的别名。因此,如果不需要在`@ContextConfiguration`中声明其他属性,则可以省略`locations`属性名的声明,并使用以下示例中演示的速记格式声明资源位置: + +爪哇 + +``` +@ExtendWith(SpringExtension.class) +@ContextConfiguration({"/app-config.xml", "/test-config.xml"}) (1) +class MyTest { + // class body... +} +``` + +|**1**|指定 XML 文件而不使用`location`属性。| +|-----|------------------------------------------------------------| + +Kotlin + +``` +@ExtendWith(SpringExtension::class) +@ContextConfiguration("/app-config.xml", "/test-config.xml") (1) +class MyTest { + // class body... +} +``` + +|**1**|指定 XML 文件而不使用`locations`属性。| +|-----|------------------------------------------------------------| + +如果省略 @contextConfiguration 注释中的<gtr="1193"/>和<gtr="1194"/>属性,TestContextFramework 将尝试检测默认的 XML 资源位置。具体地说,`GenericXmlContextLoader`和 `GenericXMLWebContextLoader’根据测试类的名称检测缺省位置。如果你的类名为`com.example.MyTest`,则`GenericXmlContextLoader`从`locations`加载应用程序上下文。下面的示例展示了如何做到这一点: + +爪哇 + +``` +@ExtendWith(SpringExtension.class) +// ApplicationContext will be loaded from +// "classpath:com/example/MyTest-context.xml" +@ContextConfiguration (1) +class MyTest { + // class body... +} +``` + +|**1**|从默认位置加载配置。| +|-----|------------------------------------------------| + +Kotlin + +``` +@ExtendWith(SpringExtension::class) +// ApplicationContext will be loaded from +// "classpath:com/example/MyTest-context.xml" +@ContextConfiguration (1) +class MyTest { + // class body... +} +``` + +|**1**|从默认位置加载配置。| +|-----|------------------------------------------------| + +##### 使用 Groovy 脚本的上下文配置 + +要使用使用使用[Groovy Bean Definition DSL](core.html#groovy-bean-definition-dsl)的 Groovy 脚本为测试加载`ApplicationContext`,你可以使用`@ContextConfiguration`注释测试类,并配置`locations`或`value`属性,并使用一个包含 Groovy 脚本资源位置的数组。Groovy 脚本的资源查找语义与[XML 配置文件](#testcontext-ctx-management-xml)的资源查找语义相同。 + +| |启用 Groovy 脚本支持<br/><br/>支持使用 Groovy 脚本在 Spring `ApplicationContext`中加载`ApplicationContext`如果 Groovy 位于 Classpath 上,则 TestContext 框架将自动启用。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +下面的示例展示了如何指定 Groovy 配置文件: + +爪哇 + +``` +@ExtendWith(SpringExtension.class) +// ApplicationContext will be loaded from "/AppConfig.groovy" and +// "/TestConfig.groovy" in the root of the classpath +@ContextConfiguration({"/AppConfig.groovy", "/TestConfig.Groovy"}) (1) +class MyTest { + // class body... +} +``` + +Kotlin + +``` +@ExtendWith(SpringExtension::class) +// ApplicationContext will be loaded from "/AppConfig.groovy" and +// "/TestConfig.groovy" in the root of the classpath +@ContextConfiguration("/AppConfig.groovy", "/TestConfig.Groovy") (1) +class MyTest { + // class body... +} +``` + +|**1**|指定 Groovy 配置文件的位置。| +|-----|------------------------------------------------------| + +如果从`@ContextConfiguration`注释中省略`locations`和`value`属性,TestContext 框架将尝试检测默认的 Groovy 脚本。具体地说,`GenericGroovyXmlContextLoader`和`GenericGroovyXmlWebContextLoader`根据测试类的名称检测缺省位置。如果你的类被命名为“com.example.mytest”,那么 Groovy 上下文加载程序将从“ Classpath:com/example/mytestcontext.groovy”中加载你的应用程序上下文。下面的示例展示了如何使用默认值: + +爪哇 + +``` +@ExtendWith(SpringExtension.class) +// ApplicationContext will be loaded from +// "classpath:com/example/MyTestContext.groovy" +@ContextConfiguration (1) +class MyTest { + // class body... +} +``` + +|**1**|从默认位置加载配置。| +|-----|------------------------------------------------| + +Kotlin + +``` +@ExtendWith(SpringExtension::class) +// ApplicationContext will be loaded from +// "classpath:com/example/MyTestContext.groovy" +@ContextConfiguration (1) +class MyTest { + // class body... +} +``` + +|**1**|从默认位置加载配置。| +|-----|------------------------------------------------| + +| |同时声明 XML 配置和 Groovy 脚本<br/><br/>你可以使用<br/>的`locations`或`value`属性同时声明 XML 配置文件和 Groovy 脚本。如果到`GenericGroovyXmlContextLoader`配置资源位置的路径以`.xml`结束,则使用 `xmlBeanDefinitionReader’加载它。否则,将使用 `GroovyBeanDefinitionReader`.<br/><br/>下面的列表显示了如何在集成测试中结合这两个部分:<br/><br/>java<br/><br/>@dwith<br/>/////gt/app context=“和”/“xml.tconfiguration”(<xtregt=“xtreadwith=”xml.“xtreadvy”,<<<xtexptcension.class)<gtr="<gt=“/testconfig.groovy”})<br/>class mytest{<br/>//class body....<br/><br/><br/><br/><gt="<class="/>/gt=”gt=“gt=”/>/12 41“/>将从”AppContextContextR=“///”AppContextR=“.”xtR=“和”."xt| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 具有组件类的上下文配置 + +要通过使用组件类为你的测试加载`ApplicationContext`(参见[基于 爪哇 的容器配置](core.html#beans-java)),你可以用<br/>注释你的测试类,并用一个包含对组件类的引用的数组配置`classes`属性。下面的示例展示了如何做到这一点: + +爪哇 + +``` +@ExtendWith(SpringExtension.class) +// ApplicationContext will be loaded from AppConfig and TestConfig +@ContextConfiguration(classes = {AppConfig.class, TestConfig.class}) (1) +class MyTest { + // class body... +} +``` + +|**1**|指定组件类。| +|-----|-----------------------------| + +Kotlin + +``` +@ExtendWith(SpringExtension::class) +// ApplicationContext will be loaded from AppConfig and TestConfig +@ContextConfiguration(classes = [AppConfig::class, TestConfig::class]) (1) +class MyTest { + // class body... +} +``` + +|**1**|指定组件类。| +|-----|-----------------------------| + +| |组件类<br/><br/>术语“组件类”可以指以下任何一种:<br/><br/>* 用`@Configuration`注释的类。<br/><br/>* 一个组件(即用`@Component`、`@Service`注释的类,或其他原型注释)。<br/><br/>* 一个用`javax.inject`注释的 JSR-330 兼容的类。<br/><br/>* 任何包含`@Bean`-methods 的类。<br/><br/>* 任何打算注册为 Spring 组件的其他类(即,gt r=“1274”/> Spring,有可能在不使用 Spring 注释的情况下利用单个构造函数的自动接线<br/>。<br/><br/>参见[@configuration](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/annotation/Configuration.html)和[`@Bean`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/annotation/Bean.html)的 javadoc 有关组件类的配置和语义的更多信息<br/>,请特别注意<br/>对`@Bean`lite mode 的讨论。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +如果从`@ContextConfiguration`注释中省略`classes`属性,TestContext 框架将尝试检测缺省配置类的存在。具体地说,`AnnotationConfigContextLoader`和`AnnotationConfigWebContextLoader`检测满足配置类实现要求的测试类的所有`static`嵌套类,如[@configuration](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/context/annotation/Configuration.html)爪哇doc 中所指定的。请注意,配置类的名称是任意的。此外,如果需要,一个测试类可以包含多个`static`嵌套配置类。在下面的示例中,`OrderServiceTest`类声明了一个名为`static`的嵌套配置类,该配置类用于自动加载测试类的`Config`: + +爪哇 + +``` +@SpringJUnitConfig (1) +// ApplicationContext will be loaded from the +// static nested Config class +class OrderServiceTest { + + @Configuration + static class Config { + + // this bean will be injected into the OrderServiceTest class + @Bean + OrderService orderService() { + OrderService orderService = new OrderServiceImpl(); + // set properties, etc. + return orderService; + } + } + + @Autowired + OrderService orderService; + + @Test + void testOrderService() { + // test the orderService + } + +} +``` + +|**1**|从嵌套的`Config`类加载配置信息。| +|-----|-----------------------------------------------------------------| + +Kotlin + +``` +@SpringJUnitConfig (1) +// ApplicationContext will be loaded from the nested Config class +class OrderServiceTest { + + @Autowired + lateinit var orderService: OrderService + + @Configuration + class Config { + + // this bean will be injected into the OrderServiceTest class + @Bean + fun orderService(): OrderService { + // set properties, etc. + return OrderServiceImpl() + } + } + + @Test + fun testOrderService() { + // test the orderService + } +} +``` + +|**1**|从嵌套的`Config`类加载配置信息。| +|-----|-----------------------------------------------------------------| + +##### 混合 XML、Groovy 脚本和组件类 + +有时可能需要混合使用 XML 配置文件、Groovy 脚本和组件类(通常是`@Configuration`类)来为测试配置“ApplicationContext”。例如,如果你在生产中使用 XML 配置,那么你可能会决定使用`@Configuration`类来为你的测试配置特定的由 Spring 管理的组件,反之亦然。 + +此外,一些第三方框架(例如 Spring Boot)提供了一流的支持,用于同时从不同类型的资源(例如,XML 配置文件、Groovy 脚本和 @configuration’类)加载`ApplicationContext`。 Spring 框架在历史上并不支持这一标准部署。因此, Spring 框架在`spring-test`模块中交付的大多数`SmartContextLoader`实现只为每个测试上下文支持一种资源类型。然而,这并不意味着你不能同时使用这两种方法。一般规则的一个例外是,`GenericGroovyXmlContextLoader`和 `GenericGroovyXMLWebContextLoader’同时支持 XML 配置文件和 Groovy 脚本。此外,第三方框架可以选择通过`ConfigurableApplicationContext`来支持`locations`和`classes`的声明,并且,在 TestContext 框架中的标准测试支持下,你有以下选项。 + +如果你想使用资源位置(例如,XML 或 Groovy)和`@Configuration`类来配置你的测试,你必须选择一个作为入口点,并且一个必须包含或导入另一个。例如,在 XML 或 Groovy 脚本中,可以通过使用组件扫描或将它们定义为正常的 Spring bean 来包含 `@Configuration’类,而在`@Configuration`类中,可以使用`@ImportResource`来导入 XML 配置文件或 Groovy 脚本。请注意,这种行为在语义上等同于在生产环境中配置应用程序的方式:在生产环境配置中,你可以定义一组 XML 或 Groovy 资源位置,也可以定义一组`@Configuration`类,从这些类加载你的生产环境`ApplicationContext`,但是,你仍然可以自由地包含或导入其他类型的配置。 + +##### 带有上下文初始化器的上下文配置 + +要使用上下文初始化器为测试配置`ApplicationContext`,请用`@ContextConfiguration`注释测试类,并使用一个数组配置`initializers`属性,该数组包含对实现 `applicationContextInitializer’的类的引用。然后使用声明的上下文初始化器初始化为测试加载的`ConfigurableApplicationContext`。请注意,每个声明的初始化器支持的具体`ConfigurableApplicationContext`类型必须与使用中的 `SmartContextLoader’创建的`ApplicationContext`类型兼容(通常是`GenericApplicationContext`)。此外,初始化器被调用的顺序取决于它们是实现 Spring 的“有序”接口,还是使用 Spring 的`@Order`注释或使用标准的“@priority”注释。下面的示例展示了如何使用初始化器: + +爪哇 + +``` +@ExtendWith(SpringExtension.class) +// ApplicationContext will be loaded from TestConfig +// and initialized by TestAppCtxInitializer +@ContextConfiguration( + classes = TestConfig.class, + initializers = TestAppCtxInitializer.class) (1) +class MyTest { + // class body... +} +``` + +|**1**|通过使用配置类和初始化器指定配置。| +|-----|---------------------------------------------------------------------------| + +Kotlin + +``` +@ExtendWith(SpringExtension::class) +// ApplicationContext will be loaded from TestConfig +// and initialized by TestAppCtxInitializer +@ContextConfiguration( + classes = [TestConfig::class], + initializers = [TestAppCtxInitializer::class]) (1) +class MyTest { + // class body... +} +``` + +|**1**|通过使用配置类和初始化器指定配置。| +|-----|---------------------------------------------------------------------------| + +你还可以在`@ContextConfiguration`中完全省略 XML 配置文件、Groovy 脚本或组件类的声明,而只声明 `ApplicationContextInitializer’类,这些类随后负责在上下文中注册 bean——例如,通过编程方式从 XML 文件或配置类中加载 Bean 定义。下面的示例展示了如何做到这一点: + +爪哇 + +``` +@ExtendWith(SpringExtension.class) +// ApplicationContext will be initialized by EntireAppInitializer +// which presumably registers beans in the context +@ContextConfiguration(initializers = EntireAppInitializer.class) (1) +class MyTest { + // class body... +} +``` + +|**1**|仅使用初始化器指定配置。| +|-----|------------------------------------------------------| + +Kotlin + +``` +@ExtendWith(SpringExtension::class) +// ApplicationContext will be initialized by EntireAppInitializer +// which presumably registers beans in the context +@ContextConfiguration(initializers = [EntireAppInitializer::class]) (1) +class MyTest { + // class body... +} +``` + +|**1**|仅使用初始化器指定配置。| +|-----|------------------------------------------------------| + +##### 上下文配置继承 + +`@ContextConfiguration`支持布尔`inheritLocations`和`inheritInitializers`属性,这些属性表示是否应该继承由超类声明的资源位置或组件类和上下文初始化器。这两个标志的默认值都是`true`。这意味着测试类继承了资源位置或组件类,以及由任何超类声明的上下文初始化器。具体地说,测试类的资源位置或组件类被追加到由超类声明的资源位置或注释类的列表中。类似地,给定测试类的初始化器被添加到由测试超类定义的初始化器集合中。因此,子类可以选择扩展资源位置、组件类或上下文初始化器。 + +如果`inheritLocations`或`inheritInitializers`中的`inheritInitializers`属性被设置为`false`,则资源位置或组件类和上下文初始化器分别用于测试类的影子和有效替换由超类定义的配置。 + +| |在 Spring Framework5.3 中,测试配置也可以从包含<br/>类继承。详见[@nested 测试类配置](#testcontext-junit-jupiter-nested-test-configuration)。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在下一个使用 XML 资源位置的示例中,“ExtendedTest”的`ApplicationContext`按照这个顺序从`base-config.xml`和<br/>加载。因此,在`extended-config.xml`中定义的 bean 可以覆盖(即替换)在`base-config.xml`中定义的 bean。下面的示例展示了一个类如何扩展另一个类,并使用它自己的配置文件和超类的配置文件: + +爪哇 + +``` +@ExtendWith(SpringExtension.class) +// ApplicationContext will be loaded from "/base-config.xml" +// in the root of the classpath +@ContextConfiguration("/base-config.xml") (1) +class BaseTest { + // class body... +} + +// ApplicationContext will be loaded from "/base-config.xml" and +// "/extended-config.xml" in the root of the classpath +@ContextConfiguration("/extended-config.xml") (2) +class ExtendedTest extends BaseTest { + // class body... +} +``` + +|**1**|在超类中定义的配置文件。| +|-----|---------------------------------------------| +|**2**|在子类中定义的配置文件。| + +Kotlin + +``` +@ExtendWith(SpringExtension::class) +// ApplicationContext will be loaded from "/base-config.xml" +// in the root of the classpath +@ContextConfiguration("/base-config.xml") (1) +open class BaseTest { + // class body... +} + +// ApplicationContext will be loaded from "/base-config.xml" and +// "/extended-config.xml" in the root of the classpath +@ContextConfiguration("/extended-config.xml") (2) +class ExtendedTest : BaseTest() { + // class body... +} +``` + +|**1**|在超类中定义的配置文件。| +|-----|---------------------------------------------| +|**2**|在子类中定义的配置文件。| + +类似地,在下一个使用组件类的示例中,`ApplicationContext`的`ExtendedTest`是从`BaseConfig`和`ExtendedConfig`类按此顺序加载的。因此,在`ExtendedConfig`中定义的 bean 可以覆盖(即替换)在`BaseConfig`中定义的 bean。下面的示例展示了一个类如何扩展另一个类,并同时使用自己的配置类和超类的配置类: + +爪哇 + +``` +// ApplicationContext will be loaded from BaseConfig +@SpringJUnitConfig(BaseConfig.class) (1) +class BaseTest { + // class body... +} + +// ApplicationContext will be loaded from BaseConfig and ExtendedConfig +@SpringJUnitConfig(ExtendedConfig.class) (2) +class ExtendedTest extends BaseTest { + // class body... +} +``` + +|**1**|在超类中定义的配置类。| +|-----|----------------------------------------------| +|**2**|在子类中定义的配置类。| + +Kotlin + +``` +// ApplicationContext will be loaded from BaseConfig +@SpringJUnitConfig(BaseConfig::class) (1) +open class BaseTest { + // class body... +} + +// ApplicationContext will be loaded from BaseConfig and ExtendedConfig +@SpringJUnitConfig(ExtendedConfig::class) (2) +class ExtendedTest : BaseTest() { + // class body... +} +``` + +|**1**|在超类中定义的配置类。| +|-----|----------------------------------------------| +|**2**|在子类中定义的配置类。| + +在下一个使用上下文初始化器的示例中,使用`ApplicationContext`和`ExtendedInitializer`初始化 `ExtendedTest’的`ApplicationContext`。然而,请注意,初始化器被调用的顺序取决于它们是实现 Spring 的`Ordered`接口,还是使用 Spring 的`@Order`注释或标准的`@Priority`注释。下面的示例展示了一个类如何扩展另一个类,并同时使用自己的初始化器和超类的初始化器: + +爪哇 + +``` +// ApplicationContext will be initialized by BaseInitializer +@SpringJUnitConfig(initializers = BaseInitializer.class) (1) +class BaseTest { + // class body... +} + +// ApplicationContext will be initialized by BaseInitializer +// and ExtendedInitializer +@SpringJUnitConfig(initializers = ExtendedInitializer.class) (2) +class ExtendedTest extends BaseTest { + // class body... +} +``` + +|**1**|在超类中定义的初始化器。| +|-----|--------------------------------------| +|**2**|在子类中定义的初始化器。| + +Kotlin + +``` +// ApplicationContext will be initialized by BaseInitializer +@SpringJUnitConfig(initializers = [BaseInitializer::class]) (1) +open class BaseTest { + // class body... +} + +// ApplicationContext will be initialized by BaseInitializer +// and ExtendedInitializer +@SpringJUnitConfig(initializers = [ExtendedInitializer::class]) (2) +class ExtendedTest : BaseTest() { + // class body... +} +``` + +|**1**|在超类中定义的初始化器。| +|-----|--------------------------------------| +|**2**|在子类中定义的初始化器。| + +##### 具有环境配置文件的上下文配置 + +Spring 框架具有对环境和配置文件(AKA“ Bean 定义配置文件”)的概念的一流支持,并且集成测试可以被配置为针对各种测试场景激活特定的 Bean 定义配置文件。这是通过用`@ActiveProfiles`注释一个测试类,并提供一个配置文件列表来实现的,这些配置文件在为测试加载`ApplicationContext`时应该被激活。 + +| |你可以在`@ActiveProfiles`SPI 的任何实现中使用`SmartContextLoader`,但是`@ActiveProfiles`在较早的 `ContextLoader’SPI 的实现中不支持。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +考虑使用 XML 配置和`@Configuration`类的两个示例: + +``` +<!-- app-config.xml --> +<beans xmlns="http://www.springframework.org/schema/beans" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:jdbc="http://www.springframework.org/schema/jdbc" + xmlns:jee="http://www.springframework.org/schema/jee" + xsi:schemaLocation="..."> + + <bean id="transferService" + class="com.bank.service.internal.DefaultTransferService"> + <constructor-arg ref="accountRepository"/> + <constructor-arg ref="feePolicy"/> + </bean> + + <bean id="accountRepository" + class="com.bank.repository.internal.JdbcAccountRepository"> + <constructor-arg ref="dataSource"/> + </bean> + + <bean id="feePolicy" + class="com.bank.service.internal.ZeroFeePolicy"/> + + <beans profile="dev"> + <jdbc:embedded-database id="dataSource"> + <jdbc:script + location="classpath:com/bank/config/sql/schema.sql"/> + <jdbc:script + location="classpath:com/bank/config/sql/test-data.sql"/> + </jdbc:embedded-database> + </beans> + + <beans profile="production"> + <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/> + </beans> + + <beans profile="default"> + <jdbc:embedded-database id="dataSource"> + <jdbc:script + location="classpath:com/bank/config/sql/schema.sql"/> + </jdbc:embedded-database> + </beans> + +</beans> +``` + +爪哇 + +``` +@ExtendWith(SpringExtension.class) +// ApplicationContext will be loaded from "classpath:/app-config.xml" +@ContextConfiguration("/app-config.xml") +@ActiveProfiles("dev") +class TransferServiceTest { + + @Autowired + TransferService transferService; + + @Test + void testTransferService() { + // test the transferService + } +} +``` + +Kotlin + +``` +@ExtendWith(SpringExtension::class) +// ApplicationContext will be loaded from "classpath:/app-config.xml" +@ContextConfiguration("/app-config.xml") +@ActiveProfiles("dev") +class TransferServiceTest { + + @Autowired + lateinit var transferService: TransferService + + @Test + fun testTransferService() { + // test the transferService + } +} +``` + +当`TransferServiceTest`运行时,其`ApplicationContext`将从 Classpath 根目录中的 `app-config.xml` 配置文件加载。如果检查“app-config.xml”,可以看到`accountRepository` Bean 对“数据源” Bean 具有依赖性。然而,`dataSource`并未被定义为顶级 Bean。相反,“数据源”被定义了三次:在`production`配置文件中,在`dev`配置文件中,以及在`default`配置文件中。 + +通过用`TransferServiceTest`注释`@ActiveProfiles("dev")`,我们指示 Spring TestContext 框架加载`ApplicationContext`,并将活动配置文件设置为 `{“dev”}`。结果,嵌入式数据库被创建并填充了测试数据,并且`accountRepository` Bean 与开发`DataSource`的引用连接在一起。这很可能是我们在集成测试中想要的。 + +有时,将 bean 分配到`default`配置文件中是有用的。只有当没有其他配置文件被特别激活时,默认配置文件中的 bean 才会被包含。你可以使用它来定义要在应用程序的默认状态下使用的“fallback”bean。例如,你可以显式地为`dev`和`production`配置文件提供数据源,但在这两个配置文件都不是活动的情况下,将内存中数据源定义为默认值。 + +下面的代码清单演示了如何使用`@Configuration`类而不是 XML 来实现相同的配置和集成测试: + +爪哇 + +``` +@Configuration +@Profile("dev") +public class StandaloneDataConfig { + + @Bean + public DataSource dataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.HSQL) + .addScript("classpath:com/bank/config/sql/schema.sql") + .addScript("classpath:com/bank/config/sql/test-data.sql") + .build(); + } +} +``` + +Kotlin + +``` +@Configuration +@Profile("dev") +class StandaloneDataConfig { + + @Bean + fun dataSource(): DataSource { + return EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.HSQL) + .addScript("classpath:com/bank/config/sql/schema.sql") + .addScript("classpath:com/bank/config/sql/test-data.sql") + .build() + } +} +``` + +爪哇 + +``` +@Configuration +@Profile("production") +public class JndiDataConfig { + + @Bean(destroyMethod="") + public DataSource dataSource() throws Exception { + Context ctx = new InitialContext(); + return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource"); + } +} +``` + +Kotlin + +``` +@Configuration +@Profile("production") +class JndiDataConfig { + + @Bean(destroyMethod = "") + fun dataSource(): DataSource { + val ctx = InitialContext() + return ctx.lookup("java:comp/env/jdbc/datasource") as DataSource + } +} +``` + +爪哇 + +``` +@Configuration +@Profile("default") +public class DefaultDataConfig { + + @Bean + public DataSource dataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.HSQL) + .addScript("classpath:com/bank/config/sql/schema.sql") + .build(); + } +} +``` + +Kotlin + +``` +@Configuration +@Profile("default") +class DefaultDataConfig { + + @Bean + fun dataSource(): DataSource { + return EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.HSQL) + .addScript("classpath:com/bank/config/sql/schema.sql") + .build() + } +} +``` + +爪哇 + +``` +@Configuration +public class TransferServiceConfig { + + @Autowired DataSource dataSource; + + @Bean + public TransferService transferService() { + return new DefaultTransferService(accountRepository(), feePolicy()); + } + + @Bean + public AccountRepository accountRepository() { + return new JdbcAccountRepository(dataSource); + } + + @Bean + public FeePolicy feePolicy() { + return new ZeroFeePolicy(); + } +} +``` + +Kotlin + +``` +@Configuration +class TransferServiceConfig { + + @Autowired + lateinit var dataSource: DataSource + + @Bean + fun transferService(): TransferService { + return DefaultTransferService(accountRepository(), feePolicy()) + } + + @Bean + fun accountRepository(): AccountRepository { + return JdbcAccountRepository(dataSource) + } + + @Bean + fun feePolicy(): FeePolicy { + return ZeroFeePolicy() + } +} +``` + +爪哇 + +``` +@SpringJUnitConfig({ + TransferServiceConfig.class, + StandaloneDataConfig.class, + JndiDataConfig.class, + DefaultDataConfig.class}) +@ActiveProfiles("dev") +class TransferServiceTest { + + @Autowired + TransferService transferService; + + @Test + void testTransferService() { + // test the transferService + } +} +``` + +Kotlin + +``` +@SpringJUnitConfig( + TransferServiceConfig::class, + StandaloneDataConfig::class, + JndiDataConfig::class, + DefaultDataConfig::class) +@ActiveProfiles("dev") +class TransferServiceTest { + + @Autowired + lateinit var transferService: TransferService + + @Test + fun testTransferService() { + // test the transferService + } +} +``` + +在这个变体中,我们将 XML 配置拆分为四个独立的 `@Configuration’类: + +* `TransferServiceConfig`:通过使用 `@autowired’通过依赖注入获得`dataSource`。 + +* `StandaloneDataConfig`:为适合于开发人员测试的嵌入式数据库定义`dataSource`。 + +* `JndiDataConfig`:定义在生产环境中从 JNDI 检索的`dataSource`。 + +* `DefaultDataConfig`:为默认的嵌入式数据库定义`dataSource`,以防配置文件不是活动的。 + +与基于 XML 的配置示例一样,我们仍然使用 @ActiveProfiles’对`TransferServiceTest`进行注释,但是这一次我们使用`@ContextConfiguration`注释来指定所有四个配置类。测试类本身的主体完全保持不变。 + +通常的情况是,在一个给定的项目中,跨多个测试类使用一组配置文件。因此,为了避免重复`@ActiveProfiles`注释的声明,你可以在基类上声明`@ActiveProfiles`一次,并且子类自动从基类继承`@ActiveProfiles`配置。在下面的示例中,`@ActiveProfiles`的声明(以及其他注释)已移动到一个抽象超类`AbstractIntegrationTest`: + +| |在 Spring Framework5.3 中,测试配置也可以从包含<br/>类中继承。详见[@nested 测试类配置](#testcontext-junit-jupiter-nested-test-configuration)。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +爪哇 + +``` +@SpringJUnitConfig({ + TransferServiceConfig.class, + StandaloneDataConfig.class, + JndiDataConfig.class, + DefaultDataConfig.class}) +@ActiveProfiles("dev") +abstract class AbstractIntegrationTest { +} +``` + +Kotlin + +``` +@SpringJUnitConfig( + TransferServiceConfig::class, + StandaloneDataConfig::class, + JndiDataConfig::class, + DefaultDataConfig::class) +@ActiveProfiles("dev") +abstract class AbstractIntegrationTest { +} +``` + +爪哇 + +``` +// "dev" profile inherited from superclass +class TransferServiceTest extends AbstractIntegrationTest { + + @Autowired + TransferService transferService; + + @Test + void testTransferService() { + // test the transferService + } +} +``` + +Kotlin + +``` +// "dev" profile inherited from superclass +class TransferServiceTest : AbstractIntegrationTest() { + + @Autowired + lateinit var transferService: TransferService + + @Test + fun testTransferService() { + // test the transferService + } +} +``` + +`@ActiveProfiles`还支持一个`inheritProfiles`属性,该属性可用于禁用活动配置文件的继承,如下例所示: + +爪哇 + +``` +// "dev" profile overridden with "production" +@ActiveProfiles(profiles = "production", inheritProfiles = false) +class ProductionTransferServiceTest extends AbstractIntegrationTest { + // test body +} +``` + +Kotlin + +``` +// "dev" profile overridden with "production" +@ActiveProfiles("production", inheritProfiles = false) +class ProductionTransferServiceTest : AbstractIntegrationTest() { + // test body +} +``` + +此外,有时需要以编程方式而不是声明式地解决测试的活动配置文件——例如,基于: + +* 当前的操作系统。 + +* 测试是否在持续集成构建服务器上运行。 + +* 某些环境变量的存在。 + +* 自定义类级注释的存在。 + +* 其他问题。 + +要以编程方式解析活动的 Bean 定义配置文件,可以实现一个自定义的`ActiveProfilesResolver`,并使用`resolver`的`resolver`属性对其进行注册。有关更多信息,请参见相应的[javadoc](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/context/ActiveProfilesResolver.html)。下面的示例演示如何实现和注册自定义的“OperatingSystemActiveProfilesResolver”: + +爪哇 + +``` +// "dev" profile overridden programmatically via a custom resolver +@ActiveProfiles( + resolver = OperatingSystemActiveProfilesResolver.class, + inheritProfiles = false) +class TransferServiceTest extends AbstractIntegrationTest { + // test body +} +``` + +Kotlin + +``` +// "dev" profile overridden programmatically via a custom resolver +@ActiveProfiles( + resolver = OperatingSystemActiveProfilesResolver::class, + inheritProfiles = false) +class TransferServiceTest : AbstractIntegrationTest() { + // test body +} +``` + +爪哇 + +``` +public class OperatingSystemActiveProfilesResolver implements ActiveProfilesResolver { + + @Override + public String[] resolve(Class<?> testClass) { + String profile = ...; + // determine the value of profile based on the operating system + return new String[] {profile}; + } +} +``` + +Kotlin + +``` +class OperatingSystemActiveProfilesResolver : ActiveProfilesResolver { + + override fun resolve(testClass: Class<*>): Array<String> { + val profile: String = ... + // determine the value of profile based on the operating system + return arrayOf(profile) + } +} +``` + +##### 具有测试属性源的上下文配置 + +Spring 框架对具有属性源层次结构的环境的概念有一流的支持,并且你可以使用特定于测试的属性源来配置集成测试。与在 `@Configuration’类上使用的`@PropertySource`注释不同,你可以在测试类上声明`@TestPropertySource`注释,以声明测试属性文件或内联属性的资源位置。这些测试属性源被添加到“Environment”中的`PropertySources`集合中,用于为带注释的集成测试加载`ApplicationContext`。 + +| |你可以使用`@TestPropertySource`与`SmartContextLoader`SPI 的任何实现一起使用`@TestPropertySource`,但是`@TestPropertySource`不支持与旧的 `contextloader`SPI 的实现一起使用。<gtr="1398"/><gtr="1399"/><gtr=“textsource 值<gtr=”/>的实现通过<mergedr="1396"/>配置中的<gtr=“1397”方法。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +###### 声明测试属性源 + +你可以使用 @testPropertySource 的`locations`或`value`属性来配置测试属性文件。 + +支持传统的和基于 XML 的属性文件格式——例如,“ Classpath:/com/example/test.properties”或`"file:///path/to/file.xml"`。 + +每个路径被解释为 Spring `Resource`。普通路径(例如,`“test.properties”`)被视为与定义测试类的包有关的 Classpath 资源。以斜杠开头的路径被视为绝对 Classpath 资源(例如:`"/org/example/test.xml"`)。通过使用指定的资源协议加载引用 URL 的路径(例如,带有`classpath:`、`file:`或`http:`前缀的路径)。不允许使用资源位置通配符(例如 `***/**.properties`):每个位置必须精确地求值到一个 `.properties’或`.xml`资源。 + +下面的示例使用了一个测试属性文件: + +爪哇 + +``` +@ContextConfiguration +@TestPropertySource("/test.properties") (1) +class MyIntegrationTests { + // class body... +} +``` + +|**1**|指定具有绝对路径的属性文件。| +|-----|---------------------------------------------------| + +Kotlin + +``` +@ContextConfiguration +@TestPropertySource("/test.properties") (1) +class MyIntegrationTests { + // class body... +} +``` + +|**1**|指定具有绝对路径的属性文件。| +|-----|---------------------------------------------------| + +你可以使用`@TestPropertySource`的 `properties’属性以键-值对的形式配置内联属性,如下一个示例所示。将所有键值对添加到附件`Environment`中,作为具有最高优先级的单个测试“PropertySource”。 + +所支持的键-值对语法与为 爪哇 属性文件中的条目定义的语法相同: + +* `key=value` + +* `key:value` + +* `key value` + +下面的示例设置了两个内联属性: + +Java + +``` +@ContextConfiguration +@TestPropertySource(properties = {"timezone = GMT", "port: 4242"}) (1) +class MyIntegrationTests { + // class body... +} +``` + +|**1**|使用键值语法的两种变体设置两个属性。| +|-----|-----------------------------------------------------------------------| + +Kotlin + +``` +@ContextConfiguration +@TestPropertySource(properties = ["timezone = GMT", "port: 4242"]) (1) +class MyIntegrationTests { + // class body... +} +``` + +|**1**|使用键值语法的两种变体设置两个属性。| +|-----|-----------------------------------------------------------------------| + +| |在 Spring Framework5.2 中,`@TestPropertySource`可以用作*可重复注释*。<br/>这意味着可以在单个<br/>测试类上有多个`@TestPropertySource`的声明,随着`locations`和`properties`后面的`@TestPropertySource`注释覆盖了前面的`@TestPropertySource`注释,<br/>此外,你可以在一个测试类上声明多个组合注释,每个组合注释<br/>元注释为`@TestPropertySource`,并且所有那些`@TestPropertySource`声明都将贡献给你的测试属性源。<br/><br/>直接呈现`@TestPropertySource`注释总是优先于<br/>meta-present`@TestPropertySource`注释。换句话说,来自直接存在的`locations`和 `properties’的注释将覆盖来自`@TestPropertySource`的 `locations’和`properties`用作`@TestPropertySource`元注释的注释。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +###### 默认属性文件检测 + +如果`@TestPropertySource`被声明为空注释(即,对于`locations`或`properties`属性没有显式的值),则尝试检测相对于声明注释的类的默认属性文件。例如,如果带注释的测试类是`com.example.MyTest`,则对应的默认属性文件是`classpath:com/example/MyTest.properties`。如果无法检测到默认值,则抛出“非法状态异常”。 + +###### 优先权 + +测试属性的优先级高于在操作系统环境、Java 系统属性或应用程序通过使用`@PropertySource`或编程方式声明性地添加的属性源中定义的属性。因此,可以使用测试属性来选择性地覆盖从系统和应用程序属性源加载的属性。此外,与从资源位置加载的属性相比,内联属性具有更高的优先级。但是,请注意,通过[@DynamicPropertySource](#testcontext-ctx-management-dynamic-property-sources)注册的属性比通过`@TestPropertySource`加载的属性具有更高的优先级。 + +在下一个示例中,`timezone`和`port`属性以及在 `“/test.properties”` 中定义的任何属性覆盖了在系统和应用程序属性源中定义的同名属性。此外,如果`"/test.properties"`文件定义了`timezone`和`port`属性的条目,那么这些条目将被使用`properties`属性声明的内联属性覆盖。下面的示例展示了如何在文件和内联中指定属性: + +Java + +``` +@ContextConfiguration +@TestPropertySource( + locations = "/test.properties", + properties = {"timezone = GMT", "port: 4242"} +) +class MyIntegrationTests { + // class body... +} +``` + +Kotlin + +``` +@ContextConfiguration +@TestPropertySource("/test.properties", + properties = ["timezone = GMT", "port: 4242"] +) +class MyIntegrationTests { + // class body... +} +``` + +###### 继承和重写测试属性源 + +`@TestPropertySource`支持布尔`inheritLocations`和`inheritProperties`属性,这些属性表示超类声明的属性文件和内联属性的资源位置是否应该被继承。这两个标志的默认值都是`true`。这意味着测试类继承了由任何超类声明的位置和内联属性。具体地说,测试类的位置和内联属性被追加到超类声明的位置和内联属性之后。因此,子类可以选择扩展位置和内联属性。请注意,后面出现的属性是与前面出现的相同名称的 shadow(即覆盖)属性。此外,前面提到的优先规则也适用于继承的测试属性源。 + +如果`inheritLocations`中的`inheritLocations`或`inheritProperties`属性被设置为`false`,则位置或内联属性分别用于测试类的影子和有效地替换由超类定义的配置。 + +| |在 Spring Framework5.3 中,测试配置也可以从包含<br/>类中继承。详见[@nested 测试类配置](#testcontext-junit-jupiter-nested-test-configuration)。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在下一个示例中,`ApplicationContext`的`BaseTest`通过仅使用 `base.properties’文件作为测试属性源加载。相比之下,`ApplicationContext`的`ExtendedTest`是通过使用`base.properties`和`extended.properties`文件作为测试属性源位置加载的。下面的示例展示了如何通过使用`properties`文件在子类和其超类中定义属性: + +Java + +``` +@TestPropertySource("base.properties") +@ContextConfiguration +class BaseTest { + // ... +} + +@TestPropertySource("extended.properties") +@ContextConfiguration +class ExtendedTest extends BaseTest { + // ... +} +``` + +Kotlin + +``` +@TestPropertySource("base.properties") +@ContextConfiguration +open class BaseTest { + // ... +} + +@TestPropertySource("extended.properties") +@ContextConfiguration +class ExtendedTest : BaseTest() { + // ... +} +``` + +在下一个示例中,只使用内联的`key1`属性加载`ApplicationContext`for`BaseTest`。相比之下,`ApplicationContext`的`ExtendedTest`是通过使用内联`key1`和`key2`属性加载的。下面的示例展示了如何通过使用内联属性在子类和其超类中定义属性: + +Java + +``` +@TestPropertySource(properties = "key1 = value1") +@ContextConfiguration +class BaseTest { + // ... +} + +@TestPropertySource(properties = "key2 = value2") +@ContextConfiguration +class ExtendedTest extends BaseTest { + // ... +} +``` + +Kotlin + +``` +@TestPropertySource(properties = ["key1 = value1"]) +@ContextConfiguration +open class BaseTest { + // ... +} + +@TestPropertySource(properties = ["key2 = value2"]) +@ContextConfiguration +class ExtendedTest : BaseTest() { + // ... +} +``` + +##### 具有动态属性源的上下文配置 + +在 Spring Framework5.2.5 中,TestContext 框架通过`@DynamicPropertySource`注释为*动态*属性提供支持。此注释可用于集成测试,这些测试需要在`Environment`中为集成测试加载的`ApplicationContext`中的 `PropertySources’集合中添加具有动态值的属性。 + +| |`@DynamicPropertySource`注释及其支持的基础结构是<br/>,最初的设计目的是允许基于[Testcontainers](https://www.testcontainers.org/)的测试中的属性很容易地暴露在<br/> Spring 集成测试中。然而,这个特性也可以用于任何形式的<br/>外部资源,其生命周期被维护在测试的`ApplicationContext`之外。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +与应用于类级别的[@TestPropertySource](#testcontext-ctx-management-property-sources)注释不同,`@DynamicPropertySource`必须应用于一个`static`方法,该方法接受一个`DynamicPropertyRegistry`参数,该参数用于将*名称-值*对添加到`Environment`。值是动态的,并通过`Supplier`提供,该参数仅在解析属性时调用。通常,方法引用用于提供值,如以下示例中所示,该示例使用 TestContainers 项目来管理 Spring `ApplicationContext’之外的 Redis 容器。托管 Redis 容器的 IP 地址和端口通过`redis.host`和 `redis.port’属性提供给测试`ApplicationContext`中的组件。这些属性可以通过 Spring 的`Environment`抽象进行访问,也可以直接注入到 Spring 管理的组件中——例如,分别通过 `@value(“${redis.host}”)` 和`@Value("${redis.port}")`。 + +| |如果在基类中使用`@DynamicPropertySource`并发现子类<br/>中的测试失败是因为子类之间的动态属性发生了变化,那么你可能需要用<br/>将你的基类注释为[@dirtiescontext](#spring-testing-annotation-dirtiescontext)到<br/>,以确保每个子类都获得其自己的`ApplicationContext`具有正确的动态<br/>属性。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +Java + +``` +@SpringJUnitConfig(/* ... */) +@Testcontainers +class ExampleIntegrationTests { + + @Container + static RedisContainer redis = new RedisContainer(); + + @DynamicPropertySource + static void redisProperties(DynamicPropertyRegistry registry) { + registry.add("redis.host", redis::getContainerIpAddress); + registry.add("redis.port", redis::getMappedPort); + } + + // tests ... + +} +``` + +Kotlin + +``` +@SpringJUnitConfig(/* ... */) +@Testcontainers +class ExampleIntegrationTests { + + companion object { + + @Container + @JvmStatic + val redis: RedisContainer = RedisContainer() + + @DynamicPropertySource + @JvmStatic + fun redisProperties(registry: DynamicPropertyRegistry) { + registry.add("redis.host", redis::getContainerIpAddress) + registry.add("redis.port", redis::getMappedPort) + } + } + + // tests ... + +} +``` + +###### 优先权 + +动态属性的优先级高于从`@TestPropertySource`、操作系统的环境、Java 系统属性或应用程序通过使用`@PropertySource`或编程方式声明性地添加的属性源加载的属性。因此,可以使用动态属性来选择性地覆盖通过 @testPropertySource、系统属性源和应用程序属性源加载的属性。 + +##### Loading a `WebApplicationContext` + +要指示 TestContext 框架加载`WebApplicationContext`而不是标准的`ApplicationContext`,你可以用 `@webappconfiguration` 注释相应的测试类。 + +测试类上的`@WebAppConfiguration`指示 TestContext Framework 应该为集成测试加载`WebApplicationContext`。在后台,TCF 确保创建了`MockServletContext`并将其提供给测试的 WAC。默认情况下,“MockServletContext”的基本资源路径设置为`src/main/webapp`。这被解释为与你的 JVM 的根相关的路径(通常是你的项目的路径)。如果你熟悉 Maven 项目中 Web 应用程序的目录结构,那么你就知道“SRC/main/webapp”是你的 WAR 的根目录的默认位置。如果需要重写此默认值,则可以提供`@WebAppConfiguration`注释的替代路径(例如,`@WebAppConfiguration("src/test/webapp")`)。如果希望从 Classpath 而不是文件系统引用基本资源路径,则可以使用 Spring 的`classpath:`前缀。 + +请注意, Spring 对`WebApplicationContext`实现的测试支持与其对标准`ApplicationContext`实现的支持是相同的。在使用“WebApplicationContext”进行测试时,可以使用`@ContextConfiguration`声明 XML 配置文件、Groovy 脚本或`@Configuration`类。你还可以自由地使用任何其他测试注释,例如`@ActiveProfiles`、`@TestExecutionListeners`、`@Sql`、`@rollback’和其他注释。 + +本节中的其余示例显示了用于加载`WebApplicationContext`的各种配置选项中的一些。下面的示例展示了 TestContext 框架对配置之上的约定的支持: + +Java + +``` +@ExtendWith(SpringExtension.class) + +// defaults to "file:src/main/webapp" +@WebAppConfiguration + +// detects "WacTests-context.xml" in the same package +// or static nested @Configuration classes +@ContextConfiguration +class WacTests { + //... +} +``` + +Kotlin + +``` +@ExtendWith(SpringExtension::class) + +// defaults to "file:src/main/webapp" +@WebAppConfiguration + +// detects "WacTests-context.xml" in the same package +// or static nested @Configuration classes +@ContextConfiguration +class WacTests { + //... +} +``` + +如果使用`@WebAppConfiguration`注释测试类而不指定资源基路径,则资源路径实际上默认为`file:src/main/webapp`。类似地,如果在不指定资源`locations`、组件 `classes’或上下文`initializers`的情况下声明`@ContextConfiguration`, Spring 将尝试通过使用约定(即在与`WacTests`类或静态嵌套`@Configuration`类相同的包中检测配置的存在)来尝试。 + +下面的示例展示了如何显式地声明带有“@WebAppConfiguration”的资源库路径和带有`@ContextConfiguration`的 XML 资源位置: + +Java + +``` +@ExtendWith(SpringExtension.class) + +// file system resource +@WebAppConfiguration("webapp") + +// classpath resource +@ContextConfiguration("/spring/test-servlet-config.xml") +class WacTests { + //... +} +``` + +Kotlin + +``` +@ExtendWith(SpringExtension::class) + +// file system resource +@WebAppConfiguration("webapp") + +// classpath resource +@ContextConfiguration("/spring/test-servlet-config.xml") +class WacTests { + //... +} +``` + +这里需要注意的重要一点是,这两种注释的路径具有不同的语义。默认情况下,`@WebAppConfiguration`资源路径是基于文件系统的,而`@ContextConfiguration`资源位置是基于 Classpath 的。 + +下面的示例表明,我们可以通过指定 Spring 资源前缀来覆盖这两种注释的默认资源语义: + +Java + +``` +@ExtendWith(SpringExtension.class) + +// classpath resource +@WebAppConfiguration("classpath:test-web-resources") + +// file system resource +@ContextConfiguration("file:src/main/webapp/WEB-INF/servlet-config.xml") +class WacTests { + //... +} +``` + +Kotlin + +``` +@ExtendWith(SpringExtension::class) + +// classpath resource +@WebAppConfiguration("classpath:test-web-resources") + +// file system resource +@ContextConfiguration("file:src/main/webapp/WEB-INF/servlet-config.xml") +class WacTests { + //... +} +``` + +将本例中的注释与前面的示例进行对比。 + +[]()使用 Web 模拟 + +为了提供全面的 Web 测试支持,TestContext 框架有一个默认启用的“ServletTestexecutionListener”。当针对“WebApplicationContext”进行测试时,此[“TestexecutionListener”](#testcontext-key-abstractions)在每个测试方法之前使用 Spring Web 的`RequestContextHolder`设置默认的线程本地状态,并基于配置为 `@WebAppConfiguration’的基本资源路径创建`MockHttpServletRequest`、`MockHttpServletResponse`和`ServletWebRequest`。`ServletTestExecutionListener`还确保可以将 `MockHttpServletResponse’和`ServletWebRequest`注入到测试实例中,并且,一旦测试完成,它将清除线程本地状态。 + +一旦为测试加载了`WebApplicationContext`,你可能会发现需要与 Web 模拟交互——例如,在调用 Web 组件后设置测试 fixture 或执行断言。下面的示例展示了哪些模拟可以自动连接到你的测试实例中。请注意,`WebApplicationContext`和 `MockServletContext’都是跨测试套件缓存的,而其他 mock 是由`ServletTestExecutionListener`根据每个测试方法管理的。 + +Java + +``` +@SpringJUnitWebConfig +class WacTests { + + @Autowired + WebApplicationContext wac; // cached + + @Autowired + MockServletContext servletContext; // cached + + @Autowired + MockHttpSession session; + + @Autowired + MockHttpServletRequest request; + + @Autowired + MockHttpServletResponse response; + + @Autowired + ServletWebRequest webRequest; + + //... +} +``` + +Kotlin + +``` +@SpringJUnitWebConfig +class WacTests { + + @Autowired + lateinit var wac: WebApplicationContext // cached + + @Autowired + lateinit var servletContext: MockServletContext // cached + + @Autowired + lateinit var session: MockHttpSession + + @Autowired + lateinit var request: MockHttpServletRequest + + @Autowired + lateinit var response: MockHttpServletResponse + + @Autowired + lateinit var webRequest: ServletWebRequest + + //... +} +``` + +##### Context Caching + +一旦 TestContext Framework 为测试加载`ApplicationContext`(或`WebApplicationContext`),该上下文将被缓存,并在随后的所有测试中重用,这些测试在相同的测试套件中声明相同的唯一上下文配置。要理解缓存的工作原理,重要的是要理解“唯一”和“测试套件”的含义。 + +`ApplicationContext`可以通过用于加载它的配置参数的组合来唯一标识。因此,配置参数的唯一组合被用来生成一个关键字,在该关键字下缓存上下文。TestContext 框架使用以下配置参数来构建上下文缓存键: + +* `locations`(来自`@ContextConfiguration`) + +* `classes`(来自`@ContextConfiguration`) + +* `contextInitializerClasses`(来自`@ContextConfiguration`) + +* `contextCustomizers`(来自`ContextCustomizerFactory`)–这包括 @@dynamicPropertySource 方法以及 Spring Boot 的测试支持中的各种功能,例如`@MockBean`和`@SpyBean`。 + +* `contextLoader`(来自`@ContextConfiguration`) + +* `parent`(来自`@ContextHierarchy`) + +* `activeProfiles`(来自`@ActiveProfiles`) + +* `propertySourceLocations`(来自`@TestPropertySource`) + +* `propertySourceProperties`(来自`@TestPropertySource`) + +* `resourceBasePath`(来自`@WebAppConfiguration`) + +例如,如果`TestClassA`为`@ContextConfiguration`的“locations”(或`value`)属性指定了`{"app-config.xml", "test-config.xml"}`,则 TestContext 框架将加载相应的`ApplicationContext`,并将其存储在`static`上下文缓存中的一个键下,该键仅基于这些位置。因此,如果`TestClassB`还为其位置定义了 `{“app-config.xml”,“test-config.xml”}`(通过继承显式或隐式地),但没有定义`@WebAppConfiguration`,则不同的 `contextloader’、不同的活动配置文件、不同的上下文初始化器、不同的测试属性源或不同的父上下文,那么相同的`ApplicationContext`由两个测试类共享。这意味着加载应用程序上下文的设置成本仅发生一次(每个测试套件),并且随后的测试执行速度要快得多。 + +| |测试套件和分叉进程<br/><br/> Spring TestContext 框架将应用程序上下文存储在静态缓存中。这<br/>意味着上下文实际上存储在`static`变量中。换句话说,如果<br/>测试在单独的进程中运行,则在每个测试<br/>执行之间清除静态缓存,这有效地禁用了缓存机制。<br/><br/>要受益于缓存机制,所有测试都必须在相同的进程或测试<br/>套件中运行。这可以通过在 IDE 中作为一个组执行所有测试来实现。类似地,<br/>在使用 Ant、 Maven 或 Gradle 之类的构建框架执行测试时,确保构建框架不在测试之间分叉是很重要的。例如,<br/>如果用于 Maven surefire 插件的[`forkMode`](https://maven.apache.org/plugins/maven-surefire-plugin/test-mojo.html#forkMode)被设置为`always`或`pertest`,则 TestContext 框架<br/>不能在测试类之间缓存应用程序上下文,结果是构建过程运行<br/>要慢得多。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +上下文缓存的大小是有界的,默认的最大大小为 32。每当达到最大大小时,就会使用最近使用的驱逐策略来驱逐和关闭陈旧的上下文。通过设置名为`spring.test.context.cache.maxSize`的 JVM 系统属性,可以从命令行或构建脚本配置最大大小。作为一种选择,你可以通过[“SpringProperties”](appendix.html#appendix-spring-properties)机制设置相同的属性。 + +由于在给定的测试套件中加载大量的应用程序上下文可能会导致该套件花费不必要的长时间运行,因此准确地知道加载和缓存了多少上下文通常是有益的。要查看底层上下文缓存的统计信息,可以将 `org.springframework.test.context.cache’日志类别的日志级别设置为`DEBUG`。 + +在不太可能的情况下,测试会破坏应用程序上下文并需要重新加载(例如,通过修改 Bean 定义或应用程序对象的状态),你可以使用`@DirtiesContext`对测试类或测试方法进行注释(请参见[Spring Testing Annotations](#spring-testing-annotation-dirtiescontext)中对 `@dirtiesContext` 的讨论)。这指示 Spring 在运行需要相同应用程序上下文的下一次测试之前,从缓存中删除上下文并重建应用程序上下文。请注意,对`@DirtiesContext`注释的支持是由“DirtiesContextBeForeModesteXeCustionListener”和“DirtiesContexteXeCustionListener”提供的,它们在默认情况下启用。 + +| |ApplicationContext 生命周期和控制台日志<br/><br/>当需要调试使用 Spring TestContext 框架执行的测试时,可以对<br/>控制台输出进行有用的分析(即输出到`SYSOUT`和`SYSERR`流)。一些构建工具和 IDE 能够将控制台输出与给定的<br/>测试相关联;但是,某些控制台输出不能很容易地与给定的测试相关联。<br/><br/>关于由 Spring 框架本身或由`ApplicationContext`中注册的组件<br/>触发的控制台日志,理解 Spring TestContext 框架在<br/>测试套件中加载的 `ApplicationContext’的生命周期非常重要。<br/><br/>用于测试的`ApplicationContext`通常是在准备测试<br/>类的实例时加载的,例如,要在测试实例的`@Autowired`字段中执行依赖项注入。这意味着在<br/>初始化`ApplicationContext`期间触发的任何控制台日志记录通常不能与<br/>单独的测试方法关联。但是,如果在<br/>执行根据[@dirtiescontext](#spring-testing-annotation-dirtiescontext)语义的测试方法之前立即关闭了上下文,则将在执行<br/>测试方法之前加载一个新的上下文实例。在后一种情况下,IDE 或构建工具可能会将<br/>控制台日志记录与单独的测试方法关联起来。<br/><br/>`ApplicationContext`对于可以通过以下场景之一关闭的测试。<br/><br/>* 上下文是根据`@DirtiesContext`语义关闭的。<br/><br/>* 上下文是关闭的因为根据 LRU 驱逐策略,它已经从缓存<br/>中自动驱逐了。<br/><br/>* 当 JVM 关闭钩子时,上下文将通过 JVM 关闭钩子关闭。对于测试套件<br/>终止。<br/><br/>如果上下文是根据`@DirtiesContext`语义关闭的,在特定的测试<br/>方法之后,IDE 或构建工具可能会将控制台日志记录与<br/>单独的测试方法相关联。如果在测试类之后根据`@DirtiesContext`语义<br/>关闭了上下文,则在关闭 `ApplicationContext’期间触发的任何控制台日志记录都不能与单个测试方法关联。类似地,在关闭阶段期间通过 JVM 关闭钩子触发的任何<br/>控制台日志记录都不能与单独的测试方法相关联。<br/><br/>当 Spring `ApplicationContext`通过 JVM 关闭钩子关闭时,在关闭阶段执行的回调<br/>在名为`SpringContextShutdownHook`的线程上执行。因此,<br/>如果你希望禁用在`ApplicationContext`关闭<br/>时触发的控制台日志记录,则可以通过 JVM 关机钩子在日志<br/>框架中注册一个自定义过滤器,该框架允许你忽略由该线程发起的任何日志记录。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 上下文层次结构 + +在编写依赖加载的 Spring `ApplicationContext`的集成测试时,通常只需针对单个上下文进行测试。然而,有时对`ApplicationContext`实例的层次结构进行测试是有益的,甚至是必要的。例如,如果你正在开发 Spring MVC Web 应用程序,则通常有一个根`WebApplicationContext`由 Spring 的`ContextLoaderListener`加载,而一个子`WebApplicationContext`由 Spring 的`DispatcherServlet`加载。这将导致一个父子上下文层次结构,其中共享组件和基础设施配置在根上下文中声明,并由特定于 Web 的组件在子上下文中使用。另一个用例可以在 Spring 批处理应用程序中找到,在这种情况下,你通常具有为共享批处理基础设施提供配置的父上下文和为特定批处理作业的配置提供配置的子上下文。 + +可以在单个测试类上或在测试类层次结构中,通过使用`@ContextHierarchy`注释声明上下文配置来编写使用上下文层次结构的集成测试。如果在测试类层次结构中的多个类上声明了上下文层次结构,则还可以合并或覆盖上下文层次结构中特定的命名级别的上下文配置。当合并层次结构中给定级别的配置时,配置资源类型(即 XML 配置文件或组件类)必须是一致的。否则,在上下文层次结构中使用不同的资源类型配置不同的级别是完全可以接受的。 + +本节中剩余的基于 JUnit Jupiter 的示例展示了需要使用上下文层次结构的集成测试的常见配置场景。 + +具有上下文层次结构的单个测试类 + +`ControllerIntegrationTests`通过声明由两个级别组成的上下文层次结构,表示 Spring MVC Web 应用程序的典型集成测试场景,一个用于根`WebApplicationContext`(通过使用`TestAppConfig``@configuration` 类加载),一个用于调度器 Servlet `WebApplicationContext`(通过使用`WebConfig`类加载)。自动连接到测试实例的`WebApplicationContext`是子上下文(即层次结构中最低的上下文)。下面的清单展示了这个配置场景: + +Java + +``` +@ExtendWith(SpringExtension.class) +@WebAppConfiguration +@ContextHierarchy({ + @ContextConfiguration(classes = TestAppConfig.class), + @ContextConfiguration(classes = WebConfig.class) +}) +class ControllerIntegrationTests { + + @Autowired + WebApplicationContext wac; + + // ... +} +``` + +Kotlin + +``` +@ExtendWith(SpringExtension::class) +@WebAppConfiguration +@ContextHierarchy( + ContextConfiguration(classes = [TestAppConfig::class]), + ContextConfiguration(classes = [WebConfig::class])) +class ControllerIntegrationTests { + + @Autowired + lateinit var wac: WebApplicationContext + + // ... +} +``` + +具有隐式父上下文的类层次结构 + +本例中的测试类在测试类层次结构中定义了上下文层次结构。`AbstractWebTests`在 Spring 驱动的 Web 应用程序中声明根用户“WebApplicationContext”的配置。但是,请注意,“AbstractWebTests”并不声明`@ContextHierarchy`。因此,“AbstractWebTests”的子类可以选择性地参与上下文层次结构或遵循`@ContextConfiguration`的标准语义。`SoapWebServiceTests`和 `restWebServiceTests’都扩展了`AbstractWebTests`,并通过使用`@ContextHierarchy`定义了上下文层次结构。结果是加载了三个应用程序上下文(每个`@ContextConfiguration`的声明有一个),并且基于`AbstractWebTests`中的配置加载的应用程序上下文被设置为为为具体子类加载的每个上下文的父上下文。下面的清单展示了这个配置场景: + +爪哇 + +``` +@ExtendWith(SpringExtension.class) +@WebAppConfiguration +@ContextConfiguration("file:src/main/webapp/WEB-INF/applicationContext.xml") +public abstract class AbstractWebTests {} + +@ContextHierarchy(@ContextConfiguration("/spring/soap-ws-config.xml")) +public class SoapWebServiceTests extends AbstractWebTests {} + +@ContextHierarchy(@ContextConfiguration("/spring/rest-ws-config.xml")) +public class RestWebServiceTests extends AbstractWebTests {} +``` + +Kotlin + +``` +@ExtendWith(SpringExtension::class) +@WebAppConfiguration +@ContextConfiguration("file:src/main/webapp/WEB-INF/applicationContext.xml") +abstract class AbstractWebTests + +@ContextHierarchy(ContextConfiguration("/spring/soap-ws-config.xml")) +class SoapWebServiceTests : AbstractWebTests() + +@ContextHierarchy(ContextConfiguration("/spring/rest-ws-config.xml")) +class RestWebServiceTests : AbstractWebTests() +``` + +具有合并上下文层次结构配置的类层次结构 + +本例中的类展示了命名层次结构级别的使用,以便合并上下文层次结构中特定级别的配置。`BaseTests`定义了层次结构中的两个级别,`parent`和`child`。`ExtendedTests`扩展了`BaseTests`,并指示 Spring TestContext 框架合并`child`层次结构级别的上下文配置,方法是确保在 `@contextConfiguration’中的<gtr="1685"/>属性中声明的名称都是<gtr="1686"/>。结果是加载了三个应用程序上下文:一个用于`/app-config.xml`,一个用于`/user-config.xml`,一个用于 `{“/user-config.xml”,“/order-config.xml”}`。与前面的示例一样,从`/app-config.xml`加载的应用程序上下文被设置为从`/user-config.xml`和`{"/user-config.xml", "/order-config.xml"}`加载的上下文的父上下文。下面的清单展示了这个配置场景: + +爪哇 + +``` +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(name = "parent", locations = "/app-config.xml"), + @ContextConfiguration(name = "child", locations = "/user-config.xml") +}) +class BaseTests {} + +@ContextHierarchy( + @ContextConfiguration(name = "child", locations = "/order-config.xml") +) +class ExtendedTests extends BaseTests {} +``` + +Kotlin + +``` +@ExtendWith(SpringExtension::class) +@ContextHierarchy( + ContextConfiguration(name = "parent", locations = ["/app-config.xml"]), + ContextConfiguration(name = "child", locations = ["/user-config.xml"])) +open class BaseTests {} + +@ContextHierarchy( + ContextConfiguration(name = "child", locations = ["/order-config.xml"]) +) +class ExtendedTests : BaseTests() {} +``` + +具有重写的上下文层次结构配置的类层次结构 + +与前面的示例相反,这个示例演示了如何通过将`@ContextConfiguration`中的 ` 继承位置’标志设置为`false`来覆盖上下文层次结构中给定命名级别的配置。因此,`ExtendedTests`的应用程序上下文仅从`/test-user-config.xml`加载,并将其父集设置为从`/app-config.xml`加载的上下文。下面的清单展示了这个配置场景: + +爪哇 + +``` +@ExtendWith(SpringExtension.class) +@ContextHierarchy({ + @ContextConfiguration(name = "parent", locations = "/app-config.xml"), + @ContextConfiguration(name = "child", locations = "/user-config.xml") +}) +class BaseTests {} + +@ContextHierarchy( + @ContextConfiguration( + name = "child", + locations = "/test-user-config.xml", + inheritLocations = false +)) +class ExtendedTests extends BaseTests {} +``` + +Kotlin + +``` +@ExtendWith(SpringExtension::class) +@ContextHierarchy( + ContextConfiguration(name = "parent", locations = ["/app-config.xml"]), + ContextConfiguration(name = "child", locations = ["/user-config.xml"])) +open class BaseTests {} + +@ContextHierarchy( + ContextConfiguration( + name = "child", + locations = ["/test-user-config.xml"], + inheritLocations = false + )) +class ExtendedTests : BaseTests() {} +``` + +| |在上下文层次结构<br/><br/>中删除上下文如果在一个测试中使用`@DirtiesContext`,该测试的上下文被配置为<br/>上下文层次结构的一部分,则可以使用`hierarchyMode`标志来控制如何清除上下文缓存<br/>。有关更多详细信息,请参见`@DirtiesContext`中[Spring Testing Annotations](#spring-testing-annotation-dirtiescontext)和[@dirtiescontext](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/annotation/DirtiesContext.html)爪哇doc 中的讨论。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 3.5.7.测试夹具的依赖注入 + +当使用`DependencyInjectionTestExecutionListener`(默认情况下进行了配置)时,测试实例的依赖项将从你配置`@ContextConfiguration`或相关注释的应用程序上下文中的 bean 中注入。你可以使用 setter 注入、字段注入,或者两者兼而有之,这取决于你选择了哪些注释,以及是否将它们放置在 setter 方法或字段上。如果你正在使用 JUnit Jupiter,你也可以选择使用构造函数注入(参见[依赖注入与`SpringExtension`](#testcontext-junit-jupiter-di))。为了与 Spring 的基于注释的注入支持保持一致,还可以使用 Spring 的`@Autowired`注释或来自 JSR-330 的`@Inject`注释进行字段和 setter 注入。 + +| |对于 JUnit Jupiter 以外的测试框架,TestContext 框架不<br/>参与测试类的实例化。因此,对于构造函数使用`@Autowired`或 `@inject’对测试类没有影响。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |尽管在生产代码中不鼓励现场注入,但在测试代码中,现场注入实际上是<br/>非常自然的。这种差异的基本原理是,你将永远不会直接实例化你的测试类<br/>。因此,不需要能够在测试类上调用<br/>构造函数或 setter 方法。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +因为`@Autowired`用于执行[按类型自动布线](core.html#beans-factory-autowire),所以如果你有相同类型的多个 Bean 定义,那么对于那些特定的 bean,你不能依赖这种方法。在这种情况下,可以将`@Autowired`与`@Qualifier`结合使用。你还可以选择将`@Inject`与“@named”结合使用。或者,如果你的测试类具有对其`ApplicationContext`的访问权限,那么你可以通过(例如)调用 `applicationContext.getBean(“titleRepository”,titleRepository.class)’来执行显式查找。 + +如果不希望将依赖项注入应用于测试实例,请不要使用`@Autowired`或`@Inject`注释域或 setter 方法。或者,你可以通过显式地将类配置为“@testexecutionListeners”并从侦听器列表中省略`DependencyInjectionTestExecutionListener.class`来完全禁用依赖注入。 + +考虑测试`HibernateTitleRepository`类的场景,如[Goals](#integration-testing-goals)部分所概述的那样。接下来的两个代码清单演示了`@Autowired`在字段和 setter 方法上的使用。在列出所有示例代码之后,将显示应用程序上下文配置。 + +| |以下代码列表中的依赖注入行为不是 JUnit<br/>Jupiter 特有的。相同的 DI 技术可以与任何支持的测试<br/>框架结合使用。<br/><br/>以下示例调用静态断言方法,例如`assertNotNull()`,<br/>,但不预先处理与`Assertions`的调用。在这种情况下,假设<br/>方法是通过`import static`声明正确导入的,该声明在<br/>示例中未显示。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +第一个代码清单显示了一个基于 JUnit Jupiter 的测试类实现,它使用`@Autowired`进行字段注入: + +爪哇 + +``` +@ExtendWith(SpringExtension.class) +// specifies the Spring configuration to load for this test fixture +@ContextConfiguration("repository-config.xml") +class HibernateTitleRepositoryTests { + + // this instance will be dependency injected by type + @Autowired + HibernateTitleRepository titleRepository; + + @Test + void findById() { + Title title = titleRepository.findById(new Long(10)); + assertNotNull(title); + } +} +``` + +Kotlin + +``` +@ExtendWith(SpringExtension::class) +// specifies the Spring configuration to load for this test fixture +@ContextConfiguration("repository-config.xml") +class HibernateTitleRepositoryTests { + + // this instance will be dependency injected by type + @Autowired + lateinit var titleRepository: HibernateTitleRepository + + @Test + fun findById() { + val title = titleRepository.findById(10) + assertNotNull(title) + } +} +``` + +或者,你可以将类配置为使用`@Autowired`进行 setter 注入,如下所示: + +爪哇 + +``` +@ExtendWith(SpringExtension.class) +// specifies the Spring configuration to load for this test fixture +@ContextConfiguration("repository-config.xml") +class HibernateTitleRepositoryTests { + + // this instance will be dependency injected by type + HibernateTitleRepository titleRepository; + + @Autowired + void setTitleRepository(HibernateTitleRepository titleRepository) { + this.titleRepository = titleRepository; + } + + @Test + void findById() { + Title title = titleRepository.findById(new Long(10)); + assertNotNull(title); + } +} +``` + +Kotlin + +``` +@ExtendWith(SpringExtension::class) +// specifies the Spring configuration to load for this test fixture +@ContextConfiguration("repository-config.xml") +class HibernateTitleRepositoryTests { + + // this instance will be dependency injected by type + lateinit var titleRepository: HibernateTitleRepository + + @Autowired + fun setTitleRepository(titleRepository: HibernateTitleRepository) { + this.titleRepository = titleRepository + } + + @Test + fun findById() { + val title = titleRepository.findById(10) + assertNotNull(title) + } +} +``` + +前面的代码列表使用了由“@contextconfiguration”注释(即<gtr="1742"/>)引用的相同的 XML 上下文文件。以下显示了此配置: + +``` +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.springframework.org/schema/beans + https://www.springframework.org/schema/beans/spring-beans.xsd"> + + <!-- this bean will be injected into the HibernateTitleRepositoryTests class --> + <bean id="titleRepository" class="com.foo.repository.hibernate.HibernateTitleRepository"> + <property name="sessionFactory" ref="sessionFactory"/> + </bean> + + <bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean"> + <!-- configuration elided for brevity --> + </bean> + +</beans> +``` + +| |如果你是从 Spring 提供的测试基类进行扩展的,而测试基类恰好在其 setter 方法之一上使用 `@autowired’,那么你可能在应用程序上下文中定义了多个受影响的<br/>类型的 bean(例如,多个`DataSource`bean)。在<br/>这样的情况下,你可以重写 setter 方法,并使用`@Qualifier`注释来表示特定的目标 Bean,如下所示(但要确保委托给超类中重写的<br/>方法)也):<br/><br/>爪哇<br/><br/>``<br/>/...<br/><br/>@autwired<br/>@override<br/>public void datasasasasasasce(@setdatasasasasce(@@taFilifier(<“mygtdatasource”("mygtdatasceGtasceDatasceDatasource....<br/>``<br/><br/>指定的限定符值指示要注入的特定`DataSource` Bean,<br/>将类型匹配的集合缩小到特定的 Bean。其值与相应的`<bean>`定义中的 `<qualifier>` 声明相匹配。 Bean name<br/>被用作回退限定符值,因此你也可以有效地通过此处的名称指向特定的<br/> Bean(如前面所示,假设`myDataSource`是 Bean `id`)。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 3.5.8.测试请求和会话范围的 bean + +Spring 从早期开始就支持[请求和会话范围的 bean](core.html#beans-factory-scopes-other),你可以通过以下步骤来测试你的请求范围和会话范围的 bean: + +* 通过使用`@WebAppConfiguration`注释测试类,确保为测试加载了`WebApplicationContext`。 + +* 将模拟请求或会话注入到你的测试实例中,并根据需要准备测试装置。 + +* 调用从配置的“WebApplicationContext”(使用依赖项注入)中检索到的 Web 组件。 + +* 对模拟执行断言。 + +下一个代码片段显示了登录用例的 XML 配置。请注意,“userservice” Bean 依赖于请求范围`loginAction` Bean。此外,通过使用[Spel 表达式](core.html#expressions)从当前 HTTP 请求中检索用户名和密码来实例化“LoginAction”。在测试中,我们希望通过 TestContext 框架管理的模拟来配置这些请求参数。下面的清单显示了这个用例的配置: + +请求范围 Bean 配置 + +``` +<beans> + + <bean id="userService" class="com.example.SimpleUserService" + c:loginAction-ref="loginAction"/> + + <bean id="loginAction" class="com.example.LoginAction" + c:username="#{request.getParameter('user')}" + c:password="#{request.getParameter('pswd')}" + scope="request"> + <aop:scoped-proxy/> + </bean> + +</beans> +``` + +在`RequestScopedBeanTests`中,我们将`UserService`(即被测试的对象)和`MockHttpServletRequest`注入到我们的测试实例中。在我们的“requestScope()”测试方法中,我们通过在提供的`MockHttpServletRequest`中设置请求参数来设置测试夹具。当在我们的“UserService”上调用`loginUser()`方法时,我们可以保证用户服务可以访问当前`MockHttpServletRequest`(即我们只设置参数的那个)的请求范围的“loginAction”。然后,我们可以根据用户名和密码的已知输入,针对结果执行断言。下面的清单展示了如何做到这一点: + +爪哇 + +``` +@SpringJUnitWebConfig +class RequestScopedBeanTests { + + @Autowired UserService userService; + @Autowired MockHttpServletRequest request; + + @Test + void requestScope() { + request.setParameter("user", "enigma"); + request.setParameter("pswd", "$pr!ng"); + + LoginResults results = userService.loginUser(); + // assert results + } +} +``` + +Kotlin + +``` +@SpringJUnitWebConfig +class RequestScopedBeanTests { + + @Autowired lateinit var userService: UserService + @Autowired lateinit var request: MockHttpServletRequest + + @Test + fun requestScope() { + request.setParameter("user", "enigma") + request.setParameter("pswd", "\$pr!ng") + + val results = userService.loginUser() + // assert results + } +} +``` + +下面的代码片段与我们前面看到的请求范围 Bean 的代码片段类似。然而,这一次,`userService` Bean 对会话范围的“用户偏好” Bean 具有依赖性。请注意,`UserPreferences` Bean 是通过使用 SPEL 表达式实例化的,该表达式从当前 HTTP 会话中检索主题。在测试中,我们需要在由 TestContext 框架管理的模拟会话中配置一个主题。下面的示例展示了如何做到这一点: + +会话范围 Bean 配置 + +``` +<beans> + + <bean id="userService" class="com.example.SimpleUserService" + c:userPreferences-ref="userPreferences" /> + + <bean id="userPreferences" class="com.example.UserPreferences" + c:theme="#{session.getAttribute('theme')}" + scope="session"> + <aop:scoped-proxy/> + </bean> + +</beans> +``` + +在`SessionScopedBeanTests`中,我们将`UserService`和`MockHttpSession`注入到我们的测试实例中。在我们的`sessionScope()`测试方法中,我们通过在提供的`MockHttpSession`中设置预期的`theme`属性来设置测试夹具。当在我们的`userService`上调用 `processuserPreferences()’方法时,我们可以保证用户服务可以访问当前 `mockHttpSession’的会话范围`userPreferences`,并且我们可以根据配置的主题对结果执行断言。下面的示例展示了如何做到这一点: + +爪哇 + +``` +@SpringJUnitWebConfig +class SessionScopedBeanTests { + + @Autowired UserService userService; + @Autowired MockHttpSession session; + + @Test + void sessionScope() throws Exception { + session.setAttribute("theme", "blue"); + + Results results = userService.processUserPreferences(); + // assert results + } +} +``` + +Kotlin + +``` +@SpringJUnitWebConfig +class SessionScopedBeanTests { + + @Autowired lateinit var userService: UserService + @Autowired lateinit var session: MockHttpSession + + @Test + fun sessionScope() { + session.setAttribute("theme", "blue") + + val results = userService.processUserPreferences() + // assert results + } +} +``` + +#### 3.5.9.事务管理 + +在 TestContext 框架中,事务由默认配置的“TransactionalTestexecutionListener”管理,即使你没有在测试类上显式声明`@TestExecutionListeners`。但是,要启用对事务的支持,你必须在加载了`@ContextConfiguration`语义的 `ApplicationContext’中配置`PlatformTransactionManager` Bean(稍后将提供更多详细信息)。此外,你必须在测试的类级别或方法级别声明 Spring 的`@Transactional`注释。 + +##### 测试管理事务 + +测试管理的事务是通过使用“TransactionalTestexecutionListener”或通过使用`TestTransaction`(稍后介绍)以声明式方式管理的事务。你不应该将这样的事务与 Spring 管理的事务(由 Spring 在`ApplicationContext`加载用于测试的事务中直接管理的事务)或应用程序管理的事务(在由测试调用的应用程序代码中以编程方式管理的事务)混淆。 Spring-管理事务和应用程序管理事务通常参与测试管理事务。但是,如果 Spring-managed 或 application-managed 事务被配置为除`REQUIRED`或`SUPPORTS`以外的任何传播类型,则应谨慎使用(有关详细信息,请参见[事务传播](data-access.html#tx-propagation)上的讨论)。 + +| |在结合 Spring 的测试管理事务使用来自测试框架<br/>的任何形式的抢占超时时时时时<br/>时,必须谨慎。<br/><br/>具体来说, Spring 的测试支持将事务状态绑定到当前线程(通过<br/>a`java.lang.ThreadLocal`变量)*在此之前*调用当前测试方法。如果<br/>测试框架在新线程中调用当前测试方法以支持<br/>抢占超时,则在当前测试方法内执行的任何操作都将*不是*在测试管理事务中调用<br/>。因此,任何此类操作<br/>的结果都不会被测试管理事务回滚。相反,这样的动作<br/>将被承诺到持久性存储中——例如,关系数据库——甚至<br/>,尽管测试管理事务被 Spring 适当回滚。<br/><br/>可能发生这种情况的情况包括但不限于以下情况。<br/><br/>*Junit4 的`@Test(timeout = …​)`支持和`TimeOut`规则<br/><br/>*Junit Jupiter 的`assertTimeoutPreemptively(…​)`方法在 `org.junit.jupiter.api.assertions’class<br/><br/>*testng 的`@Test(timeOut = …​)`支持| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 启用和禁用事务 + +用`@Transactional`注释测试方法会导致测试在事务中运行,默认情况下,该事务在测试完成后会自动回滚。如果一个测试类使用`@Transactional`进行注释,则该类层次结构中的每个测试方法都在一个事务中运行。未使用“@transactional”(在类或方法级别)进行注释的测试方法不在事务中运行。注意,`@Transactional`在测试生命周期方法上不受支持——例如,用 JUnit Jupiter 的`@BeforeAll`、`@BeforeEach`注释的方法,等等。此外,用`@Transactional`注释但将`propagation`属性设置为 `not_supported’或`NEVER`的测试不会在事务中运行。 + +| Attribute |支持测试管理的事务| +|--------------------------------------------|----------------------------------------------------------------------| +| `value` and `transactionManager` |是的| +| `propagation` |只支持`Propagation.NOT_SUPPORTED`和`Propagation.NEVER`| +| `isolation` |无| +| `timeout` |无| +| `readOnly` |无| +| `rollbackFor` and `rollbackForClassName` |否:用`TestTransaction.flagForRollback()`代替| +|`noRollbackFor` and `noRollbackForClassName`|否:用`TestTransaction.flagForCommit()`代替| + +| |方法级别的生命周期方法——例如,在测试管理的事务中运行带有 JUnit Jupiter 的“@beforeach”或`@AfterEach`注释的方法。在另一种<br/>方法上,使用组件级和类级生命周期方法——例如,使用<br/>Junit Jupiter 的`@BeforeAll`或`@AfterAll`注释的方法,以及使用 Testng 的 `@beforesuite`,`@AfterSuite`注释的方法,或者`@AfterClass`—是*不是*在<br/>测试管理事务中运行。<br/><br/>如果你需要在<br/>事务中的组件级或类级生命周期方法中运行代码,你可能希望将相应的`PlatformTransactionManager`注入到<br/>你的测试类中,然后将其与`TransactionTemplate`一起用于程序化的<br/>事务管理。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +请注意,[`AbstractTransactionalJunit4SpringContextTests’](#testcontext-support-classes-junit4)和[`AbstractTransactionalTestNgSpringContextTexttTests’](#testcontext-support-classes-testng)是在类级别上为事务支持而预先配置的。 + +下面的示例演示了为基于 Hibernate 的`UserRepository`编写集成测试的常见场景: + +爪哇 + +``` +@SpringJUnitConfig(TestConfig.class) +@Transactional +class HibernateUserRepositoryTests { + + @Autowired + HibernateUserRepository repository; + + @Autowired + SessionFactory sessionFactory; + + JdbcTemplate jdbcTemplate; + + @Autowired + void setDataSource(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + @Test + void createUser() { + // track initial state in test database: + final int count = countRowsInTable("user"); + + User user = new User(...); + repository.save(user); + + // Manual flush is required to avoid false positive in test + sessionFactory.getCurrentSession().flush(); + assertNumUsers(count + 1); + } + + private int countRowsInTable(String tableName) { + return JdbcTestUtils.countRowsInTable(this.jdbcTemplate, tableName); + } + + private void assertNumUsers(int expected) { + assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user")); + } +} +``` + +Kotlin + +``` +@SpringJUnitConfig(TestConfig::class) +@Transactional +class HibernateUserRepositoryTests { + + @Autowired + lateinit var repository: HibernateUserRepository + + @Autowired + lateinit var sessionFactory: SessionFactory + + lateinit var jdbcTemplate: JdbcTemplate + + @Autowired + fun setDataSource(dataSource: DataSource) { + this.jdbcTemplate = JdbcTemplate(dataSource) + } + + @Test + fun createUser() { + // track initial state in test database: + val count = countRowsInTable("user") + + val user = User() + repository.save(user) + + // Manual flush is required to avoid false positive in test + sessionFactory.getCurrentSession().flush() + assertNumUsers(count + 1) + } + + private fun countRowsInTable(tableName: String): Int { + return JdbcTestUtils.countRowsInTable(jdbcTemplate, tableName) + } + + private fun assertNumUsers(expected: Int) { + assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user")) + } +} +``` + +正如在[事务回滚和提交行为](#testcontext-tx-rollback-and-commit-behavior)中所解释的那样,在`createUser()`方法运行后不需要清理数据库,因为对数据库所做的任何更改都会由`TransactionalTestExecutionListener`自动回滚。 + +##### 事务回滚和提交行为 + +默认情况下,测试事务将在测试完成后自动回滚;但是,事务提交和回滚行为可以通过`@Commit`和`@Rollback`注释进行声明性配置。有关更多详细信息,请参见[注释支持](#integration-testing-annotations)部分中的相应条目。 + +##### 程序化事务管理 + +可以通过使用`TestTransaction`中的静态方法以编程方式与测试管理的事务交互。例如,可以在测试方法中、方法之前和方法之后使用`TestTransaction`来启动或结束当前的测试管理事务,或者为回滚或提交配置当前的测试管理事务。只要启用了“TransactionAlteStexecutionListener”,就会自动获得对`TestTransaction`的支持。 + +下面的示例演示了`TestTransaction`的一些特性。有关更多详细信息,请参见[“TestTransaction”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/context/transaction/TestTransaction.html)的 爪哇doc。 + +爪哇 + +``` +@ContextConfiguration(classes = TestConfig.class) +public class ProgrammaticTransactionManagementTests extends + AbstractTransactionalJUnit4SpringContextTests { + + @Test + public void transactionalTest() { + // assert initial state in test database: + assertNumUsers(2); + + deleteFromTables("user"); + + // changes to the database will be committed! + TestTransaction.flagForCommit(); + TestTransaction.end(); + assertFalse(TestTransaction.isActive()); + assertNumUsers(0); + + TestTransaction.start(); + // perform other actions against the database that will + // be automatically rolled back after the test completes... + } + + protected void assertNumUsers(int expected) { + assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user")); + } +} +``` + +Kotlin + +``` +@ContextConfiguration(classes = [TestConfig::class]) +class ProgrammaticTransactionManagementTests : AbstractTransactionalJUnit4SpringContextTests() { + + @Test + fun transactionalTest() { + // assert initial state in test database: + assertNumUsers(2) + + deleteFromTables("user") + + // changes to the database will be committed! + TestTransaction.flagForCommit() + TestTransaction.end() + assertFalse(TestTransaction.isActive()) + assertNumUsers(0) + + TestTransaction.start() + // perform other actions against the database that will + // be automatically rolled back after the test completes... + } + + protected fun assertNumUsers(expected: Int) { + assertEquals("Number of rows in the [user] table.", expected, countRowsInTable("user")) + } +} +``` + +##### 在事务之外运行代码 + +有时,你可能需要在事务性测试方法之前或之后运行特定的代码,但要在事务性上下文之外运行,例如,在运行测试之前验证初始数据库状态,或者在测试运行之后验证预期的事务提交行为(如果测试被配置为提交事务)。你可以使用这些注释之一对测试类中的任何`void`方法或测试接口中的任何`void`默认方法进行注释,并且`TransactionalTestExecutionListener`确保你的 before transaction 方法或 after transaction 方法在适当的时间运行。 + +| |任何之前的方法(例如用 JUnit Jupiter 的`@BeforeEach`注释的方法)<br/>和任何之后的方法(例如用 JUnit Jupiter 的`@AfterEach`注释的方法)都是<br/>在事务中运行的。此外,对于未配置为在<br/>事务中运行的测试方法,不运行带有`@BeforeTransaction`或 `@aftertransaction’注释的方法。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 配置事务管理器 + +`TransactionalTestExecutionListener`期望在 Spring `PlatformTransactionManager` Bean 中定义一个`ApplicationContext`用于测试。如果在测试的`PlatformTransactionManager`中有多个`PlatformTransactionManager`实例,则可以使用`@Transactional("myTxMgr")`或`@Transactional(transactionManager = "myTxMgr")`声明限定符,或者`TransactionManagementConfigurer`可以由 `@configuration’类实现。有关在测试的`ApplicationContext`中查找事务管理器所使用的算法的详细信息,请参阅[javadoc for `TestContextTransactionUtils.retrieveTransactionManager()`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/context/transaction/TestContextTransactionUtils.html#retrieveTransactionManager-org.springframework.test.context.TestContext-java.lang.String-)。 + +##### 演示所有与事务相关的注释 + +下面的基于 JUnit Jupiter 的示例显示了一个虚拟的集成测试场景,该场景突出显示了所有与事务相关的注释。该示例的目的不是演示最佳实践,而是演示如何使用这些注释。有关更多信息和配置示例,请参见[注释支持](#integration-testing-annotations)部分。[Transaction management for `@Sql`](#testcontext-executing-sql-declaratively-tx)包含一个附加示例,该示例使用`@Sql`执行带有默认事务回滚语义的声明性 SQL 脚本。下面的示例显示了相关的注释: + +爪哇 + +``` +@SpringJUnitConfig +@Transactional(transactionManager = "txMgr") +@Commit +class FictitiousTransactionalTest { + + @BeforeTransaction + void verifyInitialDatabaseState() { + // logic to verify the initial state before a transaction is started + } + + @BeforeEach + void setUpTestDataWithinTransaction() { + // set up test data within the transaction + } + + @Test + // overrides the class-level @Commit setting + @Rollback + void modifyDatabaseWithinTransaction() { + // logic which uses the test data and modifies database state + } + + @AfterEach + void tearDownWithinTransaction() { + // run "tear down" logic within the transaction + } + + @AfterTransaction + void verifyFinalDatabaseState() { + // logic to verify the final state after transaction has rolled back + } + +} +``` + +Kotlin + +``` +@SpringJUnitConfig +@Transactional(transactionManager = "txMgr") +@Commit +class FictitiousTransactionalTest { + + @BeforeTransaction + fun verifyInitialDatabaseState() { + // logic to verify the initial state before a transaction is started + } + + @BeforeEach + fun setUpTestDataWithinTransaction() { + // set up test data within the transaction + } + + @Test + // overrides the class-level @Commit setting + @Rollback + fun modifyDatabaseWithinTransaction() { + // logic which uses the test data and modifies database state + } + + @AfterEach + fun tearDownWithinTransaction() { + // run "tear down" logic within the transaction + } + + @AfterTransaction + fun verifyFinalDatabaseState() { + // logic to verify the final state after transaction has rolled back + } + +} +``` + +| |在测试 ORM 代码<br/><br/>时避免误报当你测试处理 Hibernate 会话状态或 JPA <br/>持久性上下文的应用程序代码时,请确保刷新运行该代码的测试方法<br/>中的底层工作单元。未能刷新底层工作单元可能会产生错误的<br/>正:你的测试通过了,但是相同的代码在动态的生产<br/>环境中抛出了异常。请注意,这适用于任何维护<br/>工作的内存单元的 ORM 框架。在以下基于 Hibernate 的示例测试用例中,一种方法演示了<br/>假阳性,而另一种方法则正确地公开了刷新<br/>会话的结果:<br/><br/>java<br/><br/>`<br/><br/><br/>@autowired<br/>SessionFactory;<<gt="/>sessionFactory=”/>vionR=“//>voidualtest=”//“没有”tvidentytretttttest“(未预期”/“1932”publetepsessionalpresent= 一旦 Hibernate <br/>//会话最终被刷新(即,在生产代码中)<br/>}<br/><br/>@transactional<br/>@test(预期 =...)<br/>public void updatewithessionflush(){<br/>updateentyinhibernatesession();<br/>/>////gt r=“gt=”gt=1942.getsession().flush().</>><1946"currunescurryr="/"."."<1946">"<gt=//没有预期的异常!<br/>fun falsePositive(){<br/>updateentyinhibernatesession()<br/>//误报:一旦 Hibernate <gt r=“1961”///session 最终刷新(即,在生产代码中)<br/>}<br/><br/>@transactional<br/>@test(预期 =....)<br/>fun updatewithessionflush()(){<br/>updateentyinhibernatesession()<br/>/>///////需要手动刷新,以避免测试中出现假阳性。flush<1970"><<<<<">>>>>>对于 JPA:<br/><br/>爪哇<br/><br/>``<br/>//...<br/><br/>@persistentenceontext<gt="1983"/>EntyManager EntityManager;<br/><gt"<gt"/><br/>transactional<falsevoid</>r=“/>不预期 public void</><test/>perseptyposittext=”/在生产代码中)<br/>}<br/><br/>@transactional<br/>@test<br/>public void updatewithentymanagerflush()(){<br/>updateentyinjpastencecontext()();<br/>/>/////Entygt managetrR=“1999”<>gt=“2000”/>r=“。”<<<<<<<>>>R=2002.“>>R=”。“。”R=<<<没有预期的异常!<br/>fun falsePositive(){<br/>updateentyinjpasepersistencecontext()<br/>/误报:一旦 JPA <br/>/entyManager 最终刷新(即,在生产代码中)<br/>}<br/><br/>@transactional<br/>@test(预期 =...)<br/>void updatewithentyflush()(){<br/>updateentyinjpastencecontext()<br/>/>//>tuntext=“tenmanager.flush<gt=”flush(gt“/>>tenetyr=”2028“/>>”ttR="2029。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 3.5.10.执行 SQL 脚本 + +在针对关系数据库编写集成测试时,运行 SQL 脚本来修改数据库模式或将测试数据插入到表中通常是有益的。 Spring-JDBC 模块通过在 Spring `ApplicationContext`加载时执行 SQL 脚本,为*初始化*嵌入式或现有数据库提供支持。详见[嵌入式数据库支持](data-access.html#jdbc-embedded-database-support)和[用嵌入式数据库测试数据访问逻辑](data-access.html#jdbc-embedded-database-dao-testing)。 + +虽然在加载 `ApplicationContext’时初始化一个用于测试*曾经*的数据库非常有用,但有时能够修改数据库*期间*集成测试是必不可少的。下面的部分解释了如何在集成测试期间以编程方式和声明式方式运行 SQL 脚本。 + +##### 以编程方式执行 SQL 脚本 + +Spring 提供了以下用于在集成测试方法中以编程方式执行 SQL 脚本的选项。 + +* `org.springframework.jdbc.datasource.init.ScriptUtils` + +* `org.springframework.jdbc.datasource.init.ResourceDatabasePopulator` + +* `org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests` + +* `org.springframework.test.context.testng.AbstractTransactionalTestNGSpringContextTests` + +`ScriptUtils`提供了一组用于处理 SQL 脚本的静态实用程序方法,主要用于框架内的内部使用。但是,如果你需要完全控制 SQL 脚本的解析和运行方式,`ScriptUtils`可能比后面介绍的其他一些替代方案更适合你的需要。有关更多详细信息,请参见[javadoc](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jdbc/datasource/init/ScriptUtils.html)中的`ScriptUtils`中的单个方法。 + +`ResourceDatabasePopulator`提供了一个基于对象的 API,用于通过使用在外部资源中定义的 SQL 脚本以编程方式填充、初始化或清理数据库。`ResourceDatabasePopulator`提供了用于配置字符编码、语句分隔符、注释分隔符和解析和运行脚本时使用的错误处理标志的选项。每个配置选项都有一个合理的默认值。有关默认值的详细信息,请参见[javadoc](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/jdbc/datasource/init/ResourceDatabasePopulator.html)。要运行在“ResourceDatabasePopulator”中配置的脚本,你可以调用`populate(Connection)`方法来针对`java.sql.Connection`运行 Populator,也可以调用`execute(DataSource)`方法来针对`javax.sql.DataSource`运行 Populator。下面的示例为测试模式和测试数据指定 SQL 脚本,将语句分隔符设置为“@@”,并针对`DataSource`运行脚本: + +爪哇 + +``` +@Test +void databaseTest() { + ResourceDatabasePopulator populator = new ResourceDatabasePopulator(); + populator.addScripts( + new ClassPathResource("test-schema.sql"), + new ClassPathResource("test-data.sql")); + populator.setSeparator("@@"); + populator.execute(this.dataSource); + // run code that uses the test schema and data +} +``` + +Kotlin + +``` +@Test +fun databaseTest() { + val populator = ResourceDatabasePopulator() + populator.addScripts( + ClassPathResource("test-schema.sql"), + ClassPathResource("test-data.sql")) + populator.setSeparator("@@") + populator.execute(dataSource) + // run code that uses the test schema and data +} +``` + +请注意,`ResourceDatabasePopulator`内部委托给`ScriptUtils`,用于解析和运行 SQL 脚本。类似地,`executeSqlScript(..)`和[`AbstractTransactionalTestNgSpringContextTexttTests’](#testcontext-support-classes-testng)中的`executeSqlScript(..)`方法在内部使用`ResourceDatabasePopulator`来运行 SQL 脚本。有关更多详细信息,请参见 爪哇doc 以获得各种`executeSqlScript(..)`方法。 + +##### 使用 @sql 声明式执行 SQL 脚本 + +除了上述以编程方式运行 SQL 脚本的机制外,还可以在 Spring TestContext 框架中声明性地配置 SQL 脚本。具体地说,你可以在测试类或测试方法上声明`@Sql`注释,以配置应在集成测试方法之前或之后针对给定数据库运行的各个 SQL 语句或 SQL 脚本的资源路径。对“@SQL”的支持由`SqlScriptsTestExecutionListener`提供,默认情况下启用。 + +| |方法级别`@Sql`声明默认重写类级别声明。然而,作为 Spring Framework5.2 的<br/>,该行为可以通过`@SqlMergeMode`配置为每个测试类或每个<br/>测试方法。有关更多详细信息,请参见[使用`@SqlMergeMode`合并和覆盖配置](#testcontext-executing-sql-declaratively-script-merging)。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +###### 路径资源语义 + +每个路径被解释为 Spring `Resource`。普通路径(例如,`“schema.sql”`)被视为 Classpath 相对于定义测试类的包的资源。以斜杠开头的路径被视为绝对 Classpath 资源(例如,`"/org/example/schema.sql"`)。通过使用指定的资源协议加载引用 URL 的路径(例如,带有`classpath:`、`file:`、`http:`前缀的路径)。 + +下面的示例展示了如何在类级别和基于 JUnit Jupiter 的集成测试类中的方法级别上使用`@Sql`: + +爪哇 + +``` +@SpringJUnitConfig +@Sql("/test-schema.sql") +class DatabaseTests { + + @Test + void emptySchemaTest() { + // run code that uses the test schema without any test data + } + + @Test + @Sql({"/test-schema.sql", "/test-user-data.sql"}) + void userTest() { + // run code that uses the test schema and test data + } +} +``` + +Kotlin + +``` +@SpringJUnitConfig +@Sql("/test-schema.sql") +class DatabaseTests { + + @Test + fun emptySchemaTest() { + // run code that uses the test schema without any test data + } + + @Test + @Sql("/test-schema.sql", "/test-user-data.sql") + fun userTest() { + // run code that uses the test schema and test data + } +} +``` + +###### 默认脚本检测 + +如果没有指定 SQL 脚本或语句,则尝试检测`default`脚本,这取决于声明`@Sql`的位置。如果无法检测到默认值,则抛出“非法状态异常”。 + +* 类级声明:如果带注释的测试类是`com.example.MyTest`,则对应的默认脚本是`classpath:com/example/MyTest.sql`。 + +* 方法级声明:如果带注释的测试方法名为`testMethod()`,并且在类`com.example.MyTest`中定义,那么对应的默认脚本是 ` Classpath:com/example/mytest.testmethod.sql`。 + +###### 声明多个`@Sql`集 + +如果需要为给定的测试类或测试方法配置多组 SQL 脚本,但语法配置不同,错误处理规则不同,或者每组执行阶段不同,则可以声明`@Sql`的多个实例。对于 爪哇8,你可以使用`@Sql`作为可重复的注释。否则,你可以使用“@sqlgroup”注释作为显式容器来声明“@SQL”的多个实例。 + +下面的示例展示了如何使用`@Sql`作为 爪哇8 的可重复注释: + +爪哇 + +``` +@Test +@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")) +@Sql("/test-user-data.sql") +void userTest() { + // run code that uses the test schema and test data +} +``` + +Kotlin + +``` +// Repeatable annotations with non-SOURCE retention are not yet supported by Kotlin +``` + +在前面示例中介绍的场景中,`test-schema.sql`脚本对单行注释使用了不同的语法。 + +下面的示例与前面的示例相同,只是`@Sql`声明在`@SqlGroup`中组合在一起。对于 爪哇8 及以上版本,使用“@sqlgroup”是可选的,但是你可能需要使用`@SqlGroup`来与其他 JVM 语言(如 Kotlin)兼容。 + +爪哇 + +``` +@Test +@SqlGroup({ + @Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")), + @Sql("/test-user-data.sql") +)} +void userTest() { + // run code that uses the test schema and test data +} +``` + +Kotlin + +``` +@Test +@SqlGroup( + Sql("/test-schema.sql", config = SqlConfig(commentPrefix = "`")), + Sql("/test-user-data.sql")) +fun userTest() { + // Run code that uses the test schema and test data +} +``` + +###### 脚本执行阶段 + +默认情况下,SQL 脚本在相应的测试方法之前运行。但是,如果需要在测试方法之后运行一组特定的脚本(例如,清理数据库状态),则可以在`executionPhase`中使用`@Sql`属性,如下例所示: + +爪哇 + +``` +@Test +@Sql( + scripts = "create-test-data.sql", + config = @SqlConfig(transactionMode = ISOLATED) +) +@Sql( + scripts = "delete-test-data.sql", + config = @SqlConfig(transactionMode = ISOLATED), + executionPhase = AFTER_TEST_METHOD +) +void userTest() { + // run code that needs the test data to be committed + // to the database outside of the test's transaction +} +``` + +Kotlin + +``` +@Test +@SqlGroup( + Sql("create-test-data.sql", + config = SqlConfig(transactionMode = ISOLATED)), + Sql("delete-test-data.sql", + config = SqlConfig(transactionMode = ISOLATED), + executionPhase = AFTER_TEST_METHOD)) +fun userTest() { + // run code that needs the test data to be committed + // to the database outside of the test's transaction +} +``` + +注意,`ISOLATED`和`AFTER_TEST_METHOD`分别从 `sql.transactionmode’和`Sql.ExecutionPhase`静态导入。 + +###### 带有`@SqlConfig`的脚本配置 + +你可以使用`@SqlConfig`注释来配置脚本解析和错误处理。当在集成测试类上声明为类级注释时,`@SqlConfig`充当测试类层次结构中所有 SQL 脚本的全局配置。当使用`@Sql`注释的`config`属性直接声明时,`@SqlConfig`充当在附件`@Sql`注释中声明的 SQL 脚本的本地配置。`@SqlConfig`中的每个属性都有一个隐含的默认值,该默认值在相应属性的 爪哇doc 中有文档说明。由于 爪哇 语言规范中为注释属性定义了规则,遗憾的是,不可能为注释属性赋值`null`。因此,为了支持对继承的全局配置的重写,`@SqlConfig`属性具有显式默认值`""`(用于字符串),`{}`(用于数组),或`DEFAULT`(用于枚举)。这种方法允许`@SqlConfig`的本地声明通过提供`""`、`{}`或`DEFAULT`以外的值,选择性地覆盖`@SqlConfig`的全局声明中的单个属性。当局部`@SqlConfig`属性不提供除`""`、`{}`或 `default’以外的显式值时,全局`@SqlConfig`属性将被继承。因此,显式的局部配置重写全局配置。 + +由`@Sql`和`@SqlConfig`提供的配置选项与`ScriptUtils`和`ResourceDatabasePopulator`所支持的配置选项等价,但它们是由`<jdbc:initialize-database/>`XML 名称空间元素提供的那些选项的超集。有关详细信息,请参见[`@Sql`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/context/jdbc/Sql.html)和[`@SqlConfig`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/context/jdbc/SqlConfig.html)中的单个属性的 爪哇doc。 + +**Transaction management for `@Sql`** + +默认情况下,`SqlScriptsTestExecutionListener`为使用`@Sql`配置的脚本推断所需的事务语义。具体地说,SQL 脚本在没有事务的情况下运行,在现有 Spring 管理的事务(例如,由`TransactionalTestExecutionListener`管理的事务,用于带有 `@transactional’注释的测试)中,或在独立的事务中,取决于`@SqlConfig`中`transactionMode`属性的配置值,以及测试的`ApplicationContext`中是否存在 `PlatformTransactionManager’。然而,作为最低要求,测试的`javax.sql.DataSource`中必须存在`ApplicationContext`。 + +如果`SqlScriptsTestExecutionListener`用于检测`DataSource`和 `Platform TransactionManager’并推断事务语义的算法不适合你的需要,则可以通过设置`dataSource`和`transactionManager`的属性来指定显式名称。此外,你可以通过设置`@SqlConfig`的`transactionMode`属性来控制事务传播行为(例如,是否应该在独立的事务中运行脚本)。虽然对使用`@Sql`的事务管理的所有支持选项的详细讨论超出了本参考手册的范围,但是[`@SqlConfig`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/context/jdbc/SqlConfig.html)和[SQLScriptSteXecutionListener’](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.html)的 爪哇doc 提供了详细的信息,下面的示例展示了使用 JUnit Jupiter 和使用`@Sql`的事务测试的典型测试场景: + +爪哇 + +``` +@SpringJUnitConfig(TestDatabaseConfig.class) +@Transactional +class TransactionalSqlScriptsTests { + + final JdbcTemplate jdbcTemplate; + + @Autowired + TransactionalSqlScriptsTests(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + @Test + @Sql("/test-data.sql") + void usersTest() { + // verify state in test database: + assertNumUsers(2); + // run code that uses the test data... + } + + int countRowsInTable(String tableName) { + return JdbcTestUtils.countRowsInTable(this.jdbcTemplate, tableName); + } + + void assertNumUsers(int expected) { + assertEquals(expected, countRowsInTable("user"), + "Number of rows in the [user] table."); + } +} +``` + +Kotlin + +``` +@SpringJUnitConfig(TestDatabaseConfig::class) +@Transactional +class TransactionalSqlScriptsTests @Autowired constructor(dataSource: DataSource) { + + val jdbcTemplate: JdbcTemplate = JdbcTemplate(dataSource) + + @Test + @Sql("/test-data.sql") + fun usersTest() { + // verify state in test database: + assertNumUsers(2) + // run code that uses the test data... + } + + fun countRowsInTable(tableName: String): Int { + return JdbcTestUtils.countRowsInTable(jdbcTemplate, tableName) + } + + fun assertNumUsers(expected: Int) { + assertEquals(expected, countRowsInTable("user"), + "Number of rows in the [user] table.") + } +} +``` + +请注意,运行`usersTest()`方法后不需要清理数据库,因为对数据库的任何更改(在测试方法内或在 `/test-data.sql` 脚本内)都会由 `transactionaltestexecutionlistener’自动回滚(详细信息请参见[事务管理](#testcontext-tx))。 + +###### Merging and Overriding Configuration with `@SqlMergeMode` + +在 Spring Framework5.2 中,可以将方法级`@Sql`声明与类级声明合并。例如,这允许你为每个测试类提供一次数据库模式或一些公共测试数据的配置,然后为每个测试方法提供额外的、特定于用例的测试数据。要启用`@Sql`合并,请用`@SqlMergeMode(MERGE)`注释你的测试类或测试方法。要禁用特定测试方法(或特定测试子类)的合并,可以通过`@SqlMergeMode(OVERRIDE)`切换回默认模式。有关示例和更多详细信息,请参阅[`@sqlmergemode` 注释文档部分](#spring-testing-annotation-sqlmergemode)。 + +#### 3.5.11.并行测试执行 + +Spring Framework5.0 引入了在使用 Spring TestContext 框架时在单个 JVM 内并行执行测试的基本支持。通常,这意味着大多数测试类或测试方法可以并行运行,而不需要对测试代码或配置进行任何更改。 + +| |有关如何设置并行测试执行的详细信息,请参阅<br/>测试框架、构建工具或 IDE 的文档。| +|---|-------------------------------------------------------------------------------------------------------------------------------| + +请记住,在测试套件中引入并发可能会导致意外的副作用、奇怪的运行时行为,以及间歇性或似乎随机地失败的测试。 Spring 因此,团队为何时不并行运行测试提供了以下一般指导方针。 + +如果测试: + +* 使用 Spring 框架的`@DirtiesContext`支持。 + +* 使用 Spring boot 的`@MockBean`或`@SpyBean`支持。 + +* 使用 JUnit4 的`@FixMethodOrder`支持或任何旨在确保测试方法以特定顺序运行的测试框架功能。但是,请注意,如果整个测试类并行运行,这不适用。 + +* 更改共享服务或系统(如数据库、消息代理、文件系统等)的状态。这既适用于嵌入式系统,也适用于外部系统。 + +| |如果并行测试执行失败,并且异常地声明当前测试的`ApplicationContext`不再活动,这通常意味着在另一个线程中从`ContextCache`中删除了 `ApplicationContext’。<br/><br/>这可能是由于使用了`@DirtiesContext`或由于从 `contextcache’中自动驱逐。如果`@DirtiesContext`是罪魁祸首,则需要找到一种方法来<br/>避免使用`@DirtiesContext`,或者将此类测试排除在并行执行之外。如果<br/>已超过`ContextCache`的最大大小,则可以增加缓存的最大大小<br/>。详见[context caching](#testcontext-ctx-management-caching)讨论。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |Spring TestContext 框架中的并行测试执行只有当<br/>底层`TestContext`实现提供了一个复制构造函数时才是可能的,如<br/>中所解释的[`TestContext`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/context/TestContext.html)的 爪哇doc。 Spring 中使用的“defaultTestContext”提供了这样的构造函数。但是,如果使用提供自定义<br/>实现的`TestContext`第三方库,则需要<br/>验证它是否适合并行测试执行。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 3.5.12.TestContext 框架支持类 + +本节描述了支持 Spring TestContext 框架的各种类。 + +##### Spring JUnit4Runner + +Spring TestContext 框架通过自定义运行器(在 JUnit4.12 或更高版本上支持)提供与 JUnit4 的完全集成。通过使用 @runwith(springjunit4classrunner.class)或更短的`@RunWith(SpringRunner.class)`变体对测试类进行注释,开发人员可以实现标准的基于 JUnit4 的单元和集成测试,并同时获得 TestContext 框架的好处,例如对加载应用程序上下文、测试实例的依赖注入、事务性测试方法执行的支持,等等。如果希望将 Spring TestContext 框架与可选的运行器(例如 JUnit4 的Runner)或第三方运行器(例如)一起使用,则可以选择使用。 + +下面的代码清单显示了配置使用自定义 Spring `Runner`运行的测试类的最低要求: + +爪哇 + +``` +@RunWith(SpringRunner.class) +@TestExecutionListeners({}) +public class SimpleTest { + + @Test + public void testMethod() { + // test logic... + } +} +``` + +Kotlin + +``` +@RunWith(SpringRunner::class) +@TestExecutionListeners +class SimpleTest { + + @Test + fun testMethod() { + // test logic... + } +} +``` + +在前面的示例中,`@TestExecutionListeners`被配置为空列表,以禁用默认侦听器,否则将需要通过`ApplicationContext`配置`@ContextConfiguration`。 + +##### Spring JUnit4 规则 + +`org.springframework.test.context.junit4.rules`包提供了以下 JUnit4 规则(JUnit4.12 或更高版本支持): + +* `SpringClassRule` + +* `SpringMethodRule` + +`SpringClassRule`是支持 Spring TestContext 框架的类级特性的 JUnit`TestRule`,而`SpringMethodRule`是支持 Spring TestContext 框架的实例级和方法级特性的 JUnit`MethodRule`。 + +与`SpringRunner`相反, Spring 的基于规则的 JUnit 支持具有独立于任何`org.junit.runner.Runner`实现的优点,因此可以与现有的替代运行器(例如 JUnit4 的`Parameterized`)或第三方运行器(例如`MockitoJUnitRunner`)组合。 + +要支持 TestContext 框架的全部功能,你必须将“SpringClassRule”与`SpringMethodRule`结合在一起。下面的示例展示了在集成测试中声明这些规则的正确方法: + +爪哇 + +``` +// Optionally specify a non-Spring Runner via @RunWith(...) +@ContextConfiguration +public class IntegrationTest { + + @ClassRule + public static final SpringClassRule springClassRule = new SpringClassRule(); + + @Rule + public final SpringMethodRule springMethodRule = new SpringMethodRule(); + + @Test + public void testMethod() { + // test logic... + } +} +``` + +Kotlin + +``` +// Optionally specify a non-Spring Runner via @RunWith(...) +@ContextConfiguration +class IntegrationTest { + + @Rule + val springMethodRule = SpringMethodRule() + + @Test + fun testMethod() { + // test logic... + } + + companion object { + @ClassRule + val springClassRule = SpringClassRule() + } +} +``` + +##### JUnit4 支持类 + +`org.springframework.test.context.junit4`包为基于 JUnit4 的测试用例(JUnit4.12 或更高版本支持)提供了以下支持类: + +* `AbstractJUnit4SpringContextTests` + +* `AbstractTransactionalJUnit4SpringContextTests` + +`AbstractJUnit4SpringContextTests`是一个抽象基测试类,它将 Spring TestContext 框架与 JUnit4 环境中显式的`ApplicationContext`测试支持集成在一起。扩展`AbstractJUnit4SpringContextTests`时,可以访问一个 `protected``applicationContext`实例变量,你可以使用该变量执行显式 Bean 查找或测试整个上下文的状态。 + +`AbstractTransactionalJUnit4SpringContextTests`是 `AbstractJunit4SpringContextTests’的抽象事务扩展,为 JDBC 访问添加了一些方便的功能。这个类期望在`javax.sql.DataSource` Bean 中定义一个`ApplicationContext`和一个 `platformatform transactionmanager’ Bean。扩展`AbstractTransactionalJUnit4SpringContextTests`时,可以访问`protected``jdbctemplate` 实例变量,你可以使用该变量运行 SQL 语句来查询数据库。可以使用这样的查询来确认在运行数据库相关的应用程序代码之前和之后的数据库状态,并且 Spring 确保这样的查询在与应用程序代码相同的事务范围内运行。当与 ORM 工具一起使用时,请务必避免[false positives](#testcontext-tx-false-positives)。正如[JDBC 测试支持](#integration-testing-support-jdbc)中提到的,`AbstractTransactionalJunit4SpringContextTests’还提供了方便的方法,通过使用前面提到的`JdbcTestUtils`将这些方法委托给`jdbcTemplate`中的方法。此外,`AbstractTransactionalJUnit4SpringContextTests`提供了一个 `executesqlscript(..)’方法,用于针对配置的`DataSource`运行 SQL 脚本。 + +| |这些类为扩展提供了方便。如果不希望你的测试类<br/>绑定到 Spring 特定的类层次结构,则可以使用<br/>或[Spring’s<br/>JUnit rules](#testcontext-junit4-rules)配置你自己的定制测试类。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 朱尼特木星的 SpringExtension + +Spring TestContext 框架提供了与 JUnit Jupiter 测试框架的完全集成,JUnit5 中介绍了该框架。通过使用“@extendwith”对测试类进行注释,你可以实现标准的基于 JUnit Jupiter 的单元和集成测试,并同时获得 TestContext 框架的好处,例如对加载应用程序上下文的支持、测试实例的依赖注入、事务性测试方法的执行,等等。 + +此外,由于 JUnit Jupiter 中丰富的扩展 API, Spring 在 Spring 支持 JUnit4 和 TestNG 的功能集之外还提供了以下功能: + +* 用于测试构造函数、测试方法和测试生命周期回调方法的依赖注入。有关更多详细信息,请参见[Dependency Injection with `SpringExtension`](#testcontext-junit-jupiter-di)。 + +* 基于 SPEL 表达式、环境变量、系统属性等对[条件测试执行](https://junit.org/junit5/docs/current/user-guide/#extensions-conditions)的强大支持。有关更多详细信息和示例,请参见`@EnabledIf`和`@DisabledIf`中的文档。 + +* 自定义合成的注释结合了来自 Spring 和 JUnit Jupiter 的注释。有关更多详细信息,请参见`@TransactionalDevTestConfig`和`@TransactionalIntegrationTest`中的示例。 + +下面的代码清单显示了如何配置一个测试类,以便将“SpringExtension”与`@ContextConfiguration`一起使用: + +爪哇 + +``` +// Instructs JUnit Jupiter to extend the test with Spring support. +@ExtendWith(SpringExtension.class) +// Instructs Spring to load an ApplicationContext from TestConfig.class +@ContextConfiguration(classes = TestConfig.class) +class SimpleTests { + + @Test + void testMethod() { + // test logic... + } +} +``` + +Kotlin + +``` +// Instructs JUnit Jupiter to extend the test with Spring support. +@ExtendWith(SpringExtension::class) +// Instructs Spring to load an ApplicationContext from TestConfig::class +@ContextConfiguration(classes = [TestConfig::class]) +class SimpleTests { + + @Test + fun testMethod() { + // test logic... + } +} +``` + +由于还可以使用 JUnit5 中的注释作为元注释, Spring 提供了 `@SpringJunitConfig` 和`@SpringJUnitWebConfig`组合注释,以简化测试`ApplicationContext`和 JUnitJupiter 的配置。 + +下面的示例使用`@SpringJUnitConfig`来减少前面示例中使用的配置量: + +爪哇 + +``` +// Instructs Spring to register the SpringExtension with JUnit +// Jupiter and load an ApplicationContext from TestConfig.class +@SpringJUnitConfig(TestConfig.class) +class SimpleTests { + + @Test + void testMethod() { + // test logic... + } +} +``` + +Kotlin + +``` +// Instructs Spring to register the SpringExtension with JUnit +// Jupiter and load an ApplicationContext from TestConfig.class +@SpringJUnitConfig(TestConfig::class) +class SimpleTests { + + @Test + fun testMethod() { + // test logic... + } +} +``` + +类似地,下面的示例使用`@SpringJUnitWebConfig`创建用于 JUnit Jupiter 的“WebApplicationContext”: + +爪哇 + +``` +// Instructs Spring to register the SpringExtension with JUnit +// Jupiter and load a WebApplicationContext from TestWebConfig.class +@SpringJUnitWebConfig(TestWebConfig.class) +class SimpleWebTests { + + @Test + void testMethod() { + // test logic... + } +} +``` + +Kotlin + +``` +// Instructs Spring to register the SpringExtension with JUnit +// Jupiter and load a WebApplicationContext from TestWebConfig::class +@SpringJUnitWebConfig(TestWebConfig::class) +class SimpleWebTests { + + @Test + fun testMethod() { + // test logic... + } +} +``` + +有关更多详细信息,请参见`@SpringJUnitConfig`和`@SpringJUnitWebConfig`中的文档。 + +##### Dependency Injection with `SpringExtension` + +`SpringExtension`实现了来自 JUnit Jupiter 的[“参数解析器”](https://junit.org/junit5/docs/current/user-guide/#extensions-parameter-resolution)扩展 API,它允许 Spring 为测试构造函数、测试方法和测试生命周期回调方法提供依赖注入。 + +具体地说,`SpringExtension`可以将来自测试的 `applicationContext’的依赖关系注入到使用 `@beforeall’、`@AfterAll`、`@BeforeEach`、`@AfterEach`、`@Test`、`@RepeatedTest`、`@parameterizedtest’等注释的测试构造函数和方法中。 + +###### 构造函数注入 + +如果 JUnit Jupiter 测试类的构造函数中的某个特定参数的类型为 `ApplicationContext’(或其子类型),或者被注释或元注释为 `@autowired’,<gtr="2242"/>,或<gtr="2243"/>, Spring 将该特定参数的值注入相应的 Bean 或来自测试的<gtr="2244"/>的值。 + +Spring 还可以被配置为在一个测试类构造函数被认为是*可自动连接*的情况下自动连接用于该构造函数的所有参数。如果满足以下条件之一(按优先顺序),则构造函数被认为是可自动连接的。 + +* 构造函数用`@Autowired`注释。 + +* `@TestConstructor`在测试类上存在或元存在,并且`autowireMode`属性设置为`ALL`。 + +* 默认的*测试构造函数 AutoWire 模式*已更改为`ALL`。 + +有关 `@TestConstructor’的使用以及如何更改全局*测试构造函数 AutoWire 模式*的详细信息,请参见[@testconstructor](#integration-testing-annotations-testconstructor)。 + +| |如果一个测试类的构造函数被认为是*可自动连接*,则 Spring <br/>承担解决构造函数中所有参数的参数的责任。<br/>因此,在 JUnit Jupiter 注册的其他`ParameterResolver`都不能为这样的构造函数解析<br/>参数。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |测试类的构造函数注入不能与 JUnit<br/>Jupiter 的`@TestInstance(PER_CLASS)`支持一起使用如果`@DirtiesContext`被用来关闭<br/>测试的`ApplicationContext`在测试方法之前或之后。<br/><br/>原因是`@TestInstance(PER_CLASS)`指示 JUnit Jupiter 在测试方法调用之间缓存测试<br/>实例。因此,测试实例将保留对 bean 的<br/>引用,这些 bean 最初是从`ApplicationContext`注入的,而<br/>随后被关闭。由于在这种情况下测试类的构造函数只会被调用<br/>一次,因此依赖项注入将不会再次发生,而后续的测试<br/>将与来自闭合的`ApplicationContext`的 bean 进行交互,这可能会导致错误。<br/><br/>将`@DirtiesContext`与“测试方法之前”或“测试方法之后”的模式在<br/>中与`@TestInstance(PER_CLASS)`结合,必须将 Spring <br/>中的依赖项配置为通过字段或 setter 注入提供,以便它们可以在测试<br/>方法调用之间重新注入。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在下面的示例中, Spring 将从`TestConfig.class`加载的 `ApplicationContext’中的`OrderService` Bean 注入到 `OrderServiceIntegrationTests’构造函数中。 + +爪哇 + +``` +@SpringJUnitConfig(TestConfig.class) +class OrderServiceIntegrationTests { + + private final OrderService orderService; + + @Autowired + OrderServiceIntegrationTests(OrderService orderService) { + this.orderService = orderService; + } + + // tests that use the injected OrderService +} +``` + +Kotlin + +``` +@SpringJUnitConfig(TestConfig::class) +class OrderServiceIntegrationTests @Autowired constructor(private val orderService: OrderService){ + // tests that use the injected OrderService +} +``` + +请注意,这个特性允许测试依赖项`final`,因此是不可变的。 + +如果`spring.test.constructor.autowire.mode`属性是`all`(参见[@testconstructor](#integration-testing-annotations-testconstructor)),我们可以在前面的示例中省略构造函数上的 `@autowired’的声明,结果如下。 + +爪哇 + +``` +@SpringJUnitConfig(TestConfig.class) +class OrderServiceIntegrationTests { + + private final OrderService orderService; + + OrderServiceIntegrationTests(OrderService orderService) { + this.orderService = orderService; + } + + // tests that use the injected OrderService +} +``` + +Kotlin + +``` +@SpringJUnitConfig(TestConfig::class) +class OrderServiceIntegrationTests(val orderService:OrderService) { + // tests that use the injected OrderService +} +``` + +###### 方法注入 + +如果 JUnit Jupiter 测试方法或测试生命周期回调方法中的参数类型为`ApplicationContext`(或其子类型),或被注释或元注释为 `@autowired’,`@Qualifier`,或`@Value`, Spring 用来自测试的`ApplicationContext`的相应的 Bean 注入该特定参数的值。 + +在下面的示例中, Spring 将来自`OrderService`的`ApplicationContext`加载自`TestConfig.class`的`deleteOrder()`注入到`deleteOrder()`的测试方法中: + +爪哇 + +``` +@SpringJUnitConfig(TestConfig.class) +class OrderServiceIntegrationTests { + + @Test + void deleteOrder(@Autowired OrderService orderService) { + // use orderService from the test's ApplicationContext + } +} +``` + +Kotlin + +``` +@SpringJUnitConfig(TestConfig::class) +class OrderServiceIntegrationTests { + + @Test + fun deleteOrder(@Autowired orderService: OrderService) { + // use orderService from the test's ApplicationContext + } +} +``` + +由于 JUnit Jupiter 中`ParameterResolver`支持的健壮性,你还可以将多个依赖注入到单个方法中,不仅来自 Spring,还来自 JUnit Jupiter 本身或其他第三方扩展。 + +下面的示例展示了如何让 Spring 和 JUnit Jupiter 同时向`placeOrderRepeatedly()`测试方法中注入依赖项。 + +爪哇 + +``` +@SpringJUnitConfig(TestConfig.class) +class OrderServiceIntegrationTests { + + @RepeatedTest(10) + void placeOrderRepeatedly(RepetitionInfo repetitionInfo, + @Autowired OrderService orderService) { + + // use orderService from the test's ApplicationContext + // and repetitionInfo from JUnit Jupiter + } +} +``` + +Kotlin + +``` +@SpringJUnitConfig(TestConfig::class) +class OrderServiceIntegrationTests { + + @RepeatedTest(10) + fun placeOrderRepeatedly(repetitionInfo:RepetitionInfo, @Autowired orderService:OrderService) { + + // use orderService from the test's ApplicationContext + // and repetitionInfo from JUnit Jupiter + } +} +``` + +注意,使用来自 Junit Jupiter 的`@RepeatedTest`可以让测试方法访问`RepetitionInfo`。 + +##### `@Nested`测试类配置 + +自 Spring Framework5.0 以来,*Spring TestContext Framework*一直支持在 JUnit Jupiter 中的 `@nested’测试类上使用与测试相关的注释;然而,直到 Spring Framework5.3 类级测试配置注释都不像来自超类那样来自包含类的*继承*。 + +Spring Framework5.3 引入了用于从封闭类继承测试类配置的第一类支持,并且这样的配置将在默认情况下被继承。要将默认的`INHERIT`模式更改为`OVERRIDE`模式,你可以使用 `@nestedTestConfiguration’’对单个`@Nested`测试类进行注释。显式的“@nestedTestConfiguration”声明将应用于带注释的测试类及其任何子类和嵌套类。因此,你可以用`@NestedTestConfiguration`注释顶级测试类,这将递归地应用于它的所有嵌套测试类。 + +为了允许开发团队将缺省模式更改为`OVERRIDE`——例如,为了与 Spring Framework5.0 到 5.2 兼容——缺省模式可以通过 JVM 系统属性或 Classpath 根中的`spring.properties`文件进行全局更改。有关详细信息,请参见[“更改默认的封闭配置继承模式”](#integration-testing-annotations-nestedtestconfiguration)注释。 + +尽管下面的“Hello World”示例非常简单,但它展示了如何在顶级类上声明公共配置,该类由其`@Nested`测试类继承。在这个特定的示例中,只继承了`TestConfig`配置类。每个嵌套测试类提供自己的一组活动配置文件,从而为每个嵌套测试类提供一个不同的`ApplicationContext`(有关详细信息,请参见[Context Caching](#testcontext-ctx-management-caching))。请参阅[支持的注释](#integration-testing-annotations-nestedtestconfiguration)列表,以查看哪些注释可以在`@Nested`测试类中继承。 + +爪哇 + +``` +@SpringJUnitConfig(TestConfig.class) +class GreetingServiceTests { + + @Nested + @ActiveProfiles("lang_en") + class EnglishGreetings { + + @Test + void hello(@Autowired GreetingService service) { + assertThat(service.greetWorld()).isEqualTo("Hello World"); + } + } + + @Nested + @ActiveProfiles("lang_de") + class GermanGreetings { + + @Test + void hello(@Autowired GreetingService service) { + assertThat(service.greetWorld()).isEqualTo("Hallo Welt"); + } + } +} +``` + +Kotlin + +``` +@SpringJUnitConfig(TestConfig::class) +class GreetingServiceTests { + + @Nested + @ActiveProfiles("lang_en") + inner class EnglishGreetings { + + @Test + fun hello(@Autowired service:GreetingService) { + assertThat(service.greetWorld()).isEqualTo("Hello World") + } + } + + @Nested + @ActiveProfiles("lang_de") + inner class GermanGreetings { + + @Test + fun hello(@Autowired service:GreetingService) { + assertThat(service.greetWorld()).isEqualTo("Hallo Welt") + } + } +} +``` + +##### TestNG 支持类 + +`org.springframework.test.context.testng`包为基于 TestNG 的测试用例提供了以下支持类: + +* `AbstractTestNGSpringContextTests` + +* `AbstractTransactionalTestNGSpringContextTests` + +`AbstractTestNGSpringContextTests`是一个抽象基测试类,它将 Spring TestContext 框架与 TestNG 环境中显式的`ApplicationContext`测试支持集成在一起。当扩展`AbstractTestNGSpringContextTests`时,你可以访问一个 `protected``applicationContext`实例变量,你可以使用该变量执行显式 Bean 查找或测试整个上下文的状态。 + +`AbstractTransactionalTestNGSpringContextTests`是 `AbstractTestngSpringContextTests’的抽象事务扩展,为 JDBC 访问添加了一些方便的功能。该类期望在`ApplicationContext` Bean 中定义一个`javax.sql.DataSource`和一个 `platformattransactionmanager’ Bean。当你扩展`AbstractTransactionalTestNGSpringContextTests`时,你可以访问`protected`jdbctemplate` 实例变量,你可以使用该变量运行 SQL 语句来查询数据库。可以使用这样的查询来确认在运行数据库相关的应用程序代码之前和之后的数据库状态,并且 Spring 确保这样的查询在与应用程序代码相同的事务范围内运行。当与 ORM 工具一起使用时,请务必避免[false positives](#testcontext-tx-false-positives)。正如[JDBC 测试支持](#integration-testing-support-jdbc)中提到的,`AbstractTransactionalTestngSpringContextTests’还提供了方便的方法,通过使用前述的`JdbcTestUtils`将这些方法委托给`jdbcTemplate`中的方法。此外,`AbstractTransactionalTestNGSpringContextTests`提供了一个 `executesqlscript(..)’方法,用于针对配置的`DataSource`运行 SQL 脚本。 + +| |这些类为扩展提供了方便。如果不希望你的测试类<br/>绑定到 Spring 特定的类层次结构,则可以通过使用`@ContextConfiguration`、`@TestExecutionListeners`以此类推,并通过<br/>手动使用`TestContextManager`来配置你自己的自定义测试类。请参阅源代码<br/>的`AbstractTestNGSpringContextTests`,以获得如何测试类的示例。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 3.6.WebTestClient + +`WebTestClient`是一种用于测试服务器应用程序的 HTTP 客户机。它封装了 Spring 的[WebClient](web-reactive.html#webflux-client),并使用它来执行请求,但公开了一个用于验证响应的测试 facade。`WebTestClient`可用于执行端到端 HTTP 测试。 Spring MVC 和 Spring WebFlux 应用程序也可用于在没有运行服务器的情况下通过模拟服务器来测试请求和响应对象。 + +| |Kotlin 用户:参见与[this section](languages.html#kotlin-webtestclient-issue)相关的`WebTestClient`的使用。| +|---|-----------------------------------------------------------------------------------------------------------------| + +#### 3.6.1.设置 + +要设置`WebTestClient`,你需要选择要绑定到的服务器设置。这可以是几个模拟服务器设置选项中的一个,也可以是与实时服务器的连接。 + +##### 绑定到控制器 + +此设置允许你通过模拟请求和响应对象测试特定的控制器,而无需运行服务器。 + +对于 WebFlux 应用程序,使用以下方法加载与[WebFlux 爪哇 配置](web-reactive.html#webflux-config)等价的基础设施,注册给定的控制器,并创建[Webhandler 链](web-reactive.html#webflux-web-handler-api)来处理请求: + +爪哇 + +``` +WebTestClient client = + WebTestClient.bindToController(new TestController()).build(); +``` + +Kotlin + +``` +val client = WebTestClient.bindToController(TestController()).build() +``` + +对于 Spring MVC,使用以下方法委托给[StandalOneMockmvcBuilder](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.html)以加载等价于[WebMVC 爪哇 配置](web.html#mvc-config)的基础设施,注册给定的控制器,并创建[MockMvc](#spring-mvc-test-framework)的实例来处理请求: + +爪哇 + +``` +WebTestClient client = + MockMvcWebTestClient.bindToController(new TestController()).build(); +``` + +Kotlin + +``` +val client = MockMvcWebTestClient.bindToController(TestController()).build() +``` + +##### 绑定到`ApplicationContext` + +这种设置允许你使用 Spring MVC 或 Spring WebFlux 基础设施和控制器声明加载 Spring 配置,并使用它通过模拟请求和响应对象来处理请求,而无需运行服务器。 + +对于 WebFlux,使用以下方法将 Spring `ApplicationContext`传递到[WebHttphandlerBuilder](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/server/adapter/WebHttpHandlerBuilder.html#applicationContext-org.springframework.context.ApplicationContext-),以创建[Webhandler 链](web-reactive.html#webflux-web-handler-api)来处理请求: + +爪哇 + +``` +@SpringJUnitConfig(WebConfig.class) (1) +class MyTests { + + WebTestClient client; + + @BeforeEach + void setUp(ApplicationContext context) { (2) + client = WebTestClient.bindToApplicationContext(context).build(); (3) + } +} +``` + +|**1**|指定要加载的配置| +|-----|---------------------------------| +|**2**|注入配置| +|**3**|创建`WebTestClient`| + +Kotlin + +``` +@SpringJUnitConfig(WebConfig::class) (1) +class MyTests { + + lateinit var client: WebTestClient + + @BeforeEach + fun setUp(context: ApplicationContext) { (2) + client = WebTestClient.bindToApplicationContext(context).build() (3) + } +} +``` + +|**1**|指定要加载的配置| +|-----|---------------------------------| +|**2**|注入配置| +|**3**|创建`WebTestClient`| + +对于 Spring MVC,使用以下方法,其中将 Spring `ApplicationContext`传递到[mockmvcbuilders.webappcontextsetup](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/web/servlet/setup/MockMvcBuilders.html#webAppContextSetup-org.springframework.web.context.WebApplicationContext-),以创建[MockMvc](#spring-mvc-test-framework)实例来处理请求: + +爪哇 + +``` +@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**|指定要加载的配置| +|-----|---------------------------------| +|**2**|注入配置| +|**3**|创建`WebTestClient`| + +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**|指定要加载的配置| +|-----|---------------------------------| +|**2**|注入配置| +|**3**|创建`WebTestClient`| + +##### 绑定到路由器功能 + +这种设置允许你通过模拟请求和响应对象来测试[功能端点](web-reactive.html#webflux-fn),而不需要运行服务器。 + +对于 WebFlux,使用以下方法委托`RouterFunctions.toWebHandler`来创建服务器设置以处理请求: + +爪哇 + +``` +RouterFunction<?> route = ... +client = WebTestClient.bindToRouterFunction(route).build(); +``` + +Kotlin + +``` +val route: RouterFunction<*> = ... +val client = WebTestClient.bindToRouterFunction(route).build() +``` + +对于 Spring MVC,目前没有测试[WebMVC 功能端点](web.html#webmvc-fn)的选项。 + +##### 绑定到服务器 + +此设置连接到正在运行的服务器,以执行完整的端到端 HTTP 测试: + +爪哇 + +``` +client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build(); +``` + +Kotlin + +``` +client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build() +``` + +##### 客户端配置 + +除了前面描述的服务器设置选项外,还可以配置客户端选项,包括基本 URL、默认标头、客户端过滤器和其他选项。这些选项在`bindToServer()`之后很容易获得。对于所有其他配置选项,你需要使用`configureClient()`来从服务器配置转换到客户机配置,如下所示: + +爪哇 + +``` +client = WebTestClient.bindToController(new TestController()) + .configureClient() + .baseUrl("/test") + .build(); +``` + +Kotlin + +``` +client = WebTestClient.bindToController(TestController()) + .configureClient() + .baseUrl("/test") + .build() +``` + +#### 3.6.2.写作测试 + +`WebTestClient`提供与[WebClient](web-reactive.html#webflux-client)相同的 API,直到使用`exchange()`执行请求为止。请参阅[WebClient](web-reactive.html#webflux-client-body)文档中的示例,以了解如何准备包含表单数据、多部分数据等任何内容的请求。 + +在调用`exchange()`之后,`WebTestClient`偏离了`WebClient`,而是继续使用工作流来验证响应。 + +要断言响应状态和头,请使用以下方法: + +爪哇 + +``` +client.get().uri("/persons/1") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isOk() + .expectHeader().contentType(MediaType.APPLICATION_JSON); +``` + +Kotlin + +``` +client.get().uri("/persons/1") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectStatus().isOk() + .expectHeader().contentType(MediaType.APPLICATION_JSON) +``` + +如果你希望所有的期望都被断言,即使其中一个失败了,你可以使用`expectAll(..)`,而不是使用多个链接的`expect*(..)`调用。此功能类似于 AssertJ 中的*软断言*支持和 JUnit Jupiter 中的`assertAll()`支持。 + +爪哇 + +``` +client.get().uri("/persons/1") + .accept(MediaType.APPLICATION_JSON) + .exchange() + .expectAll( + spec -> spec.expectStatus().isOk(), + spec -> spec.expectHeader().contentType(MediaType.APPLICATION_JSON) + ); +``` + +然后,你可以选择通过以下方式之一对响应体进行解码: + +* `expectBody(Class<T>)`:解码为单个对象。 + +* `expectBodyList(Class<T>)`:将对象解码并收集到`List<T>`。 + +* `expectBody()`:将`byte[]`或空体解码为[JSON 内容](#webtestclient-json)。 + +并在生成的较高级别对象上执行断言: + +爪哇 + +``` +client.get().uri("/persons") + .exchange() + .expectStatus().isOk() + .expectBodyList(Person.class).hasSize(3).contains(person); +``` + +Kotlin + +``` +import org.springframework.test.web.reactive.server.expectBodyList + +client.get().uri("/persons") + .exchange() + .expectStatus().isOk() + .expectBodyList<Person>().hasSize(3).contains(person) +``` + +如果内置断言不足,则可以使用该对象并执行任何其他断言: + +爪哇 + +``` +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)... + }); +``` + +Kotlin + +``` +client.get().uri("/persons/1") + .exchange() + .expectStatus().isOk() + .expectBody<Person>() + .consumeWith { + // custom assertions (e.g. AssertJ)... + } +``` + +或者,你可以退出工作流并获得`EntityExchangeResult`: + +爪哇 + +``` +EntityExchangeResult<Person> result = client.get().uri("/persons/1") + .exchange() + .expectStatus().isOk() + .expectBody(Person.class) + .returnResult(); +``` + +Kotlin + +``` +import org.springframework.test.web.reactive.server.expectBody + +val result = client.get().uri("/persons/1") + .exchange() + .expectStatus().isOk + .expectBody<Person>() + .returnResult() +``` + +| |当需要用泛型解码为目标类型时,请查找接受<br/>而不是`Class<T>`的重载方法<br/>。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 无内容 + +如果预期响应不包含内容,则可以断言如下: + +爪哇 + +``` +client.post().uri("/persons") + .body(personMono, Person.class) + .exchange() + .expectStatus().isCreated() + .expectBody().isEmpty(); +``` + +Kotlin + +``` +client.post().uri("/persons") + .bodyValue(person) + .exchange() + .expectStatus().isCreated() + .expectBody().isEmpty() +``` + +如果你想忽略响应内容,那么下面的内容将在没有任何断言的情况下发布: + +爪哇 + +``` +client.get().uri("/persons/123") + .exchange() + .expectStatus().isNotFound() + .expectBody(Void.class); +``` + +Kotlin + +``` +client.get().uri("/persons/123") + .exchange() + .expectStatus().isNotFound + .expectBody<Unit>() +``` + +##### JSON Content + +你可以在没有目标类型的情况下使用`expectBody()`对原始内容执行断言,而不是通过更高级别的对象。 + +要用[JSONAssert](https://jsonassert.skyscreamer.org)验证完整的 JSON 内容: + +爪哇 + +``` +client.get().uri("/persons/1") + .exchange() + .expectStatus().isOk() + .expectBody() + .json("{\"name\":\"Jane\"}") +``` + +Kotlin + +``` +client.get().uri("/persons/1") + .exchange() + .expectStatus().isOk() + .expectBody() + .json("{\"name\":\"Jane\"}") +``` + +要用[JSONPath](https://github.com/jayway/JsonPath)验证 JSON 内容: + +爪哇 + +``` +client.get().uri("/persons") + .exchange() + .expectStatus().isOk() + .expectBody() + .jsonPath("$[0].name").isEqualTo("Jane") + .jsonPath("$[1].name").isEqualTo("Jason"); +``` + +Kotlin + +``` +client.get().uri("/persons") + .exchange() + .expectStatus().isOk() + .expectBody() + .jsonPath("$[0].name").isEqualTo("Jane") + .jsonPath("$[1].name").isEqualTo("Jason") +``` + +##### 流式响应 + +要测试可能无限的流,例如`"text/event-stream"`或 `“application/x-ndjson”`,首先要验证响应状态和头,然后获得`FluxExchangeResult`: + +爪哇 + +``` +FluxExchangeResult<MyEvent> result = client.get().uri("/events") + .accept(TEXT_EVENT_STREAM) + .exchange() + .expectStatus().isOk() + .returnResult(MyEvent.class); +``` + +Kotlin + +``` +import org.springframework.test.web.reactive.server.returnResult + +val result = client.get().uri("/events") + .accept(TEXT_EVENT_STREAM) + .exchange() + .expectStatus().isOk() + .returnResult<MyEvent>() +``` + +现在,你可以使用`StepVerifier`中的`reactor-test`来使用响应流了: + +爪哇 + +``` +Flux<Event> eventFlux = result.getResponseBody(); + +StepVerifier.create(eventFlux) + .expectNext(person) + .expectNextCount(4) + .consumeNextWith(p -> ...) + .thenCancel() + .verify(); +``` + +Kotlin + +``` +val eventFlux = result.getResponseBody() + +StepVerifier.create(eventFlux) + .expectNext(person) + .expectNextCount(4) + .consumeNextWith { p -> ... } + .thenCancel() + .verify() +``` + +##### MockMVC 断言 + +`WebTestClient`是一个 HTTP 客户机,因此它只能验证客户机响应中的内容,包括状态、头和主体。 + +在使用 mockMVC 服务器设置测试 Spring MVC 应用程序时,你有额外的选择来对服务器响应执行进一步的断言。要做到这一点,首先要在断言主体之后获得`ExchangeResult`: + +爪哇 + +``` +// 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(); +``` + +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(); +``` + +然后切换到 MockMVC 服务器响应断言: + +爪哇 + +``` +MockMvcWebTestClient.resultActionsFor(result) + .andExpect(model().attribute("integer", 3)) + .andExpect(model().attribute("string", "a string value")); +``` + +Kotlin + +``` +MockMvcWebTestClient.resultActionsFor(result) + .andExpect(model().attribute("integer", 3)) + .andExpect(model().attribute("string", "a string value")); +``` + +### 3.7.MockMVC + +Spring MVC 测试框架,也称为 MockMVC,为测试 Spring MVC 应用程序提供了支持。它执行完整的 MVC 请求处理,但通过模拟请求和响应对象,而不是运行中的服务器。 + +MockMVC 可以单独用于执行请求和验证响应。它也可以通过[WebTestClient](#webtestclient)使用,其中 MockMVC 作为服务器插入以处理请求。`WebTestClient`的优点是可以选择使用更高级别的对象而不是原始数据,并且可以针对实时服务器切换到完整的端到端 HTTP 测试,并使用相同的测试 API。 + +#### 3.7.1.概述 + +你可以通过实例化控制器、向其注入依赖项并调用其方法来编写 Spring MVC 的普通单元测试。然而,这样的测试不会验证请求映射、数据绑定、消息转换、类型转换、验证,也不涉及任何支持`@InitBinder`、`@ModelAttribute`或 `@ExceptionHandler’的方法。 + +Spring MVC 测试框架,也称为,旨在为 Spring MVC 控制器提供更完整的测试,而无需运行服务器。它通过调用`DispacherServlet`并从 ` Spring-test` 模块传递[“mock” implementations of the Servlet API](#mock-objects-servlet)来实现这一点,该模块在不运行服务器的情况下复制完整的 Spring MVC 请求处理。 + +MockMVC 是一个服务器端测试框架,它允许你使用轻量级和有针对性的测试来验证 Spring MVC 应用程序的大部分功能。你可以单独使用它来执行请求和验证响应,或者你也可以通过[WebTestClient](#webtestclient)API 使用它,并将 MockMVC 插入其中作为处理请求的服务器。 + +##### 静态导入 + +当直接使用 MockMVC 来执行请求时,你将需要用于以下方面的静态导入: + +* `MockMvcBuilders.*` + +* `MockMvcRequestBuilders.*` + +* `MockMvcResultMatchers.*` + +* `MockMvcResultHandlers.*` + +记住这一点的一个简单方法是搜索`MockMvc*`。如果使用 Eclipse,请确保在 Eclipse 首选项中也添加上面的“最喜欢的静态成员”。 + +当通过[WebTestClient](#webtestclient)使用 mockmvc 时,不需要静态导入。`WebTestClient`提供了一个没有静态导入的 Fluent API。 + +##### 设置选项 + +MockMVC 可以通过以下两种方式之一设置。一种方法是直接指向你想要测试的控制器,并以编程方式配置 Spring MVC 基础设施。第二个是指向具有 Spring MVC 和控制器基础设施的 Spring 配置。 + +要设置用于测试特定控制器的 mockmvc,请使用以下方法: + +爪哇 + +``` +class MyWebTests { + + MockMvc mockMvc; + + @BeforeEach + void setup() { + this.mockMvc = MockMvcBuilders.standaloneSetup(new AccountController()).build(); + } + + // ... + +} +``` + +Kotlin + +``` +class MyWebTests { + + lateinit var mockMvc : MockMvc + + @BeforeEach + fun setup() { + mockMvc = MockMvcBuilders.standaloneSetup(AccountController()).build() + } + + // ... + +} +``` + +或者,你也可以在通过[WebTestClient](#webtestclient-controller-config)进行测试时使用此设置,该设置将委托给与上面所示相同的构建器。 + +要通过 Spring 配置设置 mockmvc,请使用以下方法: + +爪哇 + +``` +@SpringJUnitWebConfig(locations = "my-servlet-context.xml") +class MyWebTests { + + MockMvc mockMvc; + + @BeforeEach + void setup(WebApplicationContext wac) { + this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); + } + + // ... + +} +``` + +Kotlin + +``` +@SpringJUnitWebConfig(locations = ["my-servlet-context.xml"]) +class MyWebTests { + + lateinit var mockMvc: MockMvc + + @BeforeEach + fun setup(wac: WebApplicationContext) { + mockMvc = MockMvcBuilders.webAppContextSetup(wac).build() + } + + // ... + +} +``` + +或者,你也可以在通过[WebTestClient](#webtestclient-context-config)进行测试时使用此设置,该设置将委托给与上面所示相同的构建器。 + +你应该使用哪个设置选项? + +`webAppContextSetup`加载你实际的 Spring MVC 配置,从而产生一个更完整的集成测试。由于 TestContext 框架缓存了加载的 Spring 配置,因此它有助于保持测试快速运行,即使你在测试套件中引入了更多的测试。此外,还可以通过 Spring 配置将模拟服务注入控制器,以保持对 Web 层的重点测试。下面的示例使用 Mockito 声明了一个模拟服务: + +``` +<bean id="accountService" class="org.mockito.Mockito" factory-method="mock"> + <constructor-arg value="org.example.AccountService"/> +</bean> +``` + +然后,你可以将模拟服务注入到测试中,以设置和验证你的期望,如下例所示: + +爪哇 + +``` +@SpringJUnitWebConfig(locations = "test-servlet-context.xml") +class AccountTests { + + @Autowired + AccountService accountService; + + MockMvc mockMvc; + + @BeforeEach + void setup(WebApplicationContext wac) { + this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build(); + } + + // ... + +} +``` + +Kotlin + +``` +@SpringJUnitWebConfig(locations = ["test-servlet-context.xml"]) +class AccountTests { + + @Autowired + lateinit var accountService: AccountService + + lateinit mockMvc: MockMvc + + @BeforeEach + fun setup(wac: WebApplicationContext) { + mockMvc = MockMvcBuilders.webAppContextSetup(wac).build() + } + + // ... + +} +``` + +另一方面,`standaloneSetup`更接近于单元测试。它一次测试一个控制器。你可以手动为控制器注入模拟依赖项,并且它不涉及加载 Spring 配置。这样的测试更多地关注于样式,并使其更容易地看到正在测试的控制器,是否需要任何特定的 MVC 配置来工作,等等。`standaloneSetup`也是编写临时测试以验证特定行为或调试问题的一种非常方便的方法。 + +与大多数“集成与单元测试”的辩论一样,没有正确或错误的答案。然而,使用`standaloneSetup`确实意味着需要进行额外的 `WebAppContextSetup’测试,以验证你的 Spring MVC 配置。或者,你可以使用`webAppContextSetup`编写所有测试,以便始终根据实际的 Spring MVC 配置进行测试。 + +##### 设置功能 + +无论你使用哪个 MockMVC Builder,所有`MockMvcBuilder`实现都提供了一些常见且非常有用的特性。例如,你可以为所有请求声明一个`Accept`头,并期望在所有响应中的状态为 200,以及`Content-Type`头,如下所示: + +爪哇 + +``` +// static import of MockMvcBuilders.standaloneSetup + +MockMvc mockMvc = standaloneSetup(new MusicController()) + .defaultRequest(get("/").accept(MediaType.APPLICATION_JSON)) + .alwaysExpect(status().isOk()) + .alwaysExpect(content().contentType("application/json;charset=UTF-8")) + .build(); +``` + +Kotlin + +``` +// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed +``` + +此外,第三方框架(和应用程序)可以预先打包设置指令,例如`MockMvcConfigurer`中的设置指令。 Spring 框架有一个这样的内置实现,它有助于跨请求保存和重用 HTTP 会话。你可以按以下方式使用它: + +爪哇 + +``` +// static import of SharedHttpSessionConfigurer.sharedHttpSession + +MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new TestController()) + .apply(sharedHttpSession()) + .build(); + +// Use mockMvc to perform requests... +``` + +Kotlin + +``` +// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed +``` + +查看[`ConfigurableMockMVCBuilder’](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/test/web/servlet/setup/ConfigurableMockMvcBuilder.html)的 Javadoc 以获得所有 MockMVC Builder 特性的列表,或者使用 IDE 来探索可用的选项。 + +##### 执行请求 + +本节展示了如何单独使用 MockMVC 来执行请求和验证响应。如果通过`WebTestClient`使用 mockmvc,请查看[Writing Tests](#webtestclient-tests)上的相应部分。 + +要执行使用任何 HTTP 方法的请求,如以下示例所示: + +Java + +``` +// static import of MockMvcRequestBuilders.* + +mockMvc.perform(post("/hotels/{id}", 42).accept(MediaType.APPLICATION_JSON)); +``` + +Kotlin + +``` +import org.springframework.test.web.servlet.post + +mockMvc.post("/hotels/{id}", 42) { + accept = MediaType.APPLICATION_JSON +} +``` + +你还可以执行内部使用“MockMultiparthpServletRequest”的文件上传请求,这样就不需要实际解析多部分请求。相反,你必须将其设置为类似于以下示例: + +Java + +``` +mockMvc.perform(multipart("/doc").file("a1", "ABC".getBytes("UTF-8"))); +``` + +Kotlin + +``` +import org.springframework.test.web.servlet.multipart + +mockMvc.multipart("/doc") { + file("a1", "ABC".toByteArray(charset("UTF8"))) +} +``` + +你可以用 URI 模板样式指定查询参数,如下例所示: + +Java + +``` +mockMvc.perform(get("/hotels?thing={thing}", "somewhere")); +``` + +Kotlin + +``` +mockMvc.get("/hotels?thing={thing}", "somewhere") +``` + +Servlet 还可以添加表示查询或表单参数的请求参数,如下例所示: + +Java + +``` +mockMvc.perform(get("/hotels").param("thing", "somewhere")); +``` + +Kotlin + +``` +import org.springframework.test.web.servlet.get + +mockMvc.get("/hotels") { + param("thing", "somewhere") +} +``` + +如果应用程序代码依赖于 Servlet 请求参数,并且没有显式地检查查询字符串(通常是这种情况),那么使用哪个选项并不重要。但是,请记住,与 URI 模板一起提供的查询参数已经被解码,而通过`param(…​)`方法提供的请求参数预计已经被解码。 + +在大多数情况下,最好是将上下文路径和 Servlet 路径留在请求 URI 之外。如果必须使用完整的请求 URI 进行测试,请确保相应地设置`contextPath`和`servletPath`,以使请求映射工作,如下例所示: + +Java + +``` +mockMvc.perform(get("/app/main/hotels/{id}").contextPath("/app").servletPath("/main")) +``` + +Kotlin + +``` +import org.springframework.test.web.servlet.get + +mockMvc.get("/app/main/hotels/{id}") { + contextPath = "/app" + servletPath = "/main" +} +``` + +在前面的示例中,每次执行请求时都要设置`contextPath`和 `servletPath’,这会很麻烦。相反,你可以设置默认的请求属性,如下例所示: + +Java + +``` +class MyWebTests { + + MockMvc mockMvc; + + @BeforeEach + void setup() { + mockMvc = standaloneSetup(new AccountController()) + .defaultRequest(get("/") + .contextPath("/app").servletPath("/main") + .accept(MediaType.APPLICATION_JSON)).build(); + } +} +``` + +Kotlin + +``` +// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed +``` + +前面的属性会影响通过`MockMvc`实例执行的每个请求。如果在给定的请求中也指定了相同的属性,那么它将重写默认值。这就是为什么默认请求中的 HTTP 方法和 URI 无关紧要的原因,因为它们必须在每个请求中指定。 + +##### 定义期望 + +可以通过在执行请求后追加一个或多个`andExpect(..)`调用来定义期望,如下例所示。一旦一种预期落空,就不会再有其他预期了。 + +Java + +``` +// static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.* + +mockMvc.perform(get("/accounts/1")).andExpect(status().isOk()); +``` + +Kotlin + +``` +import org.springframework.test.web.servlet.get + +mockMvc.get("/accounts/1").andExpect { + status().isOk() +} +``` + +可以通过在执行请求后追加`andExpectAll(..)`来定义多个期望,如下例所示。与`andExpect(..)`相反,“ANDEXPECTALL(..)”保证将断言所有提供的期望,并将跟踪和报告所有故障。 + +Java + +``` +// static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.* + +mockMvc.perform(get("/accounts/1")).andExpectAll( + status().isOk(), + content().contentType("application/json;charset=UTF-8")); +``` + +`MockMvcResultMatchers.*`提供了许多期望,其中一些还进一步嵌套了更详细的期望。 + +预期分为两大类。第一类断言验证响应的属性(例如,响应状态、标题和内容)。这些是可以断言的最重要的结果。 + +第二类断言超出了响应范围。 Spring 这些断言允许你检查 MVC 的特定方面,例如哪个控制器方法处理了请求,是否引发并处理了异常,模型的内容是什么,选择了什么视图,添加了什么 flash 属性,等等。它们还允许你检查 Servlet 特定的方面,例如请求和会话属性。 + +以下测试断言绑定或验证失败: + +Java + +``` +mockMvc.perform(post("/persons")) + .andExpect(status().isOk()) + .andExpect(model().attributeHasErrors("person")); +``` + +Kotlin + +``` +import org.springframework.test.web.servlet.post + +mockMvc.post("/persons").andExpect { + status().isOk() + model { + attributeHasErrors("person") + } +} +``` + +很多时候,在编写测试时,转储执行的请求的结果是有用的。你可以这样做,其中`print()`是来自“mockmvcresulthandlers”的静态导入: + +Java + +``` +mockMvc.perform(post("/persons")) + .andDo(print()) + .andExpect(status().isOk()) + .andExpect(model().attributeHasErrors("person")); +``` + +Kotlin + +``` +import org.springframework.test.web.servlet.post + +mockMvc.post("/persons").andDo { + print() + }.andExpect { + status().isOk() + model { + attributeHasErrors("person") + } + } +``` + +只要请求处理不会导致未处理的异常,`print()`方法就会将所有可用的结果数据打印到`System.out`。还有一个`log()`方法和`print()`方法的两个附加变体,一个接受`OutputStream`,另一个接受`Writer`。例如,调用`print(System.err)`将结果数据打印到`System.err`,而调用`print(myWriter)`将结果数据打印到自定义写入器。如果希望记录结果数据而不是打印结果,则可以调用 `log()’方法,该方法将结果数据记录为 `org.springframework.test.web. Servlet.result`logging 类别下的单个`DEBUG`消息。 + +在某些情况下,你可能希望获得对结果的直接访问,并验证某些在其他情况下无法验证的内容。这可以通过在所有其他期望之后附加`.andReturn()`来实现,如下例所示: + +Java + +``` +MvcResult mvcResult = mockMvc.perform(post("/persons")).andExpect(status().isOk()).andReturn(); +// ... +``` + +Kotlin + +``` +var mvcResult = mockMvc.post("/persons").andExpect { status().isOk() }.andReturn() +// ... +``` + +如果所有测试都重复相同的期望,那么在构建`MockMvc`实例时,可以设置一次公共期望,如下例所示: + +Java + +``` +standaloneSetup(new SimpleController()) + .alwaysExpect(status().isOk()) + .alwaysExpect(content().contentType("application/json;charset=UTF-8")) + .build() +``` + +Kotlin + +``` +// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed +``` + +请注意,通常的期望总是被应用的,并且在不创建单独的`MockMvc`实例的情况下不能被重写。 + +当 JSON 响应内容包含用[Spring HATEOAS](https://github.com/spring-projects/spring-hateoas)创建的超媒体链接时,可以使用 JSONPath 表达式来验证结果链接,如下例所示: + +Java + +``` +mockMvc.perform(get("/people").accept(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.links[?(@.rel == 'self')].href").value("http://localhost:8080/people")); +``` + +Kotlin + +``` +mockMvc.get("/people") { + accept(MediaType.APPLICATION_JSON) +}.andExpect { + jsonPath("$.links[?(@.rel == 'self')].href") { + value("http://localhost:8080/people") + } +} +``` + +当 XML 响应内容包含使用[Spring HATEOAS](https://github.com/spring-projects/spring-hateoas)创建的超媒体链接时,可以使用 XPath 表达式来验证生成的链接: + +Java + +``` +Map<String, String> ns = Collections.singletonMap("ns", "http://www.w3.org/2005/Atom"); +mockMvc.perform(get("/handle").accept(MediaType.APPLICATION_XML)) + .andExpect(xpath("/person/ns:link[@rel='self']/@href", ns).string("http://localhost:8080/people")); +``` + +Kotlin + +``` +val ns = mapOf("ns" to "http://www.w3.org/2005/Atom") +mockMvc.get("/handle") { + accept(MediaType.APPLICATION_XML) +}.andExpect { + xpath("/person/ns:link[@rel='self']/@href", ns) { + string("http://localhost:8080/people") + } +} +``` + +##### 异步请求 + +本节展示了如何单独使用 MockMVC 来测试异步请求处理。如果通过[WebTestClient](#webtestclient)使用 MockMVC,则没有什么特别的事情可以使异步请求工作,因为`WebTestClient`会自动执行本节中描述的操作。 + +Servlet 3.0 异步请求[supported in Spring MVC](web.html#mvc-ann-async)通过退出 Servlet 容器线程并允许应用程序异步计算响应来工作,在此之后进行异步分派以在 Servlet 容器线程上完成处理。 + +Spring 在 MVC 测试中,异步请求可以通过首先断言产生的异步值,然后手动执行异步调度,最后验证响应来进行测试。下面是返回`DeferredResult`、`Callable`或反应类型(例如反应器`Mono`)的控制器方法的示例测试: + +Java + +``` +// static import of MockMvcRequestBuilders.* and MockMvcResultMatchers.* + +@Test +void test() throws Exception { + MvcResult mvcResult = this.mockMvc.perform(get("/path")) + .andExpect(status().isOk()) (1) + .andExpect(request().asyncStarted()) (2) + .andExpect(request().asyncResult("body")) (3) + .andReturn(); + + this.mockMvc.perform(asyncDispatch(mvcResult)) (4) + .andExpect(status().isOk()) (5) + .andExpect(content().string("body")); +} +``` + +|**1**|检查响应状态仍未更改| +|-----|---------------------------------------------------------------------| +|**2**|异步处理肯定已经启动了。| +|**3**|等待并断言异步结果| +|**4**|手动执行异步分派(因为没有正在运行的容器)| +|**5**|验证最终响应| + +Kotlin + +``` +@Test +fun test() { + var mvcResult = mockMvc.get("/path").andExpect { + status().isOk() (1) + request { asyncStarted() } (2) + // TODO Remove unused generic parameter + request { asyncResult<Nothing>("body") } (3) + }.andReturn() + + mockMvc.perform(asyncDispatch(mvcResult)) (4) + .andExpect { + status().isOk() (5) + content().string("body") + } +} +``` + +|**1**|检查响应状态仍未更改| +|-----|---------------------------------------------------------------------| +|**2**|异步处理肯定已经启动了。| +|**3**|等待并断言异步结果| +|**4**|手动执行异步分派(因为没有正在运行的容器)| +|**5**|验证最终响应| + +##### 流式响应 + +测试流响应(例如服务器发送的事件)的最佳方法是通过[WebTestClient](#webtestclient),它可以用作测试客户端,以连接到`MockMvc`实例,从而在 Spring MVC 控制器上执行测试,而无需运行服务器。例如: + +爪哇 + +``` +WebTestClient client = MockMvcWebTestClient.bindToController(new SseController()).build(); + +FluxExchangeResult<Person> exchangeResult = client.get() + .uri("/persons") + .exchange() + .expectStatus().isOk() + .expectHeader().contentType("text/event-stream") + .returnResult(Person.class); + +// Use StepVerifier from Project Reactor to test the streaming response + +StepVerifier.create(exchangeResult.getResponseBody()) + .expectNext(new Person("N0"), new Person("N1"), new Person("N2")) + .expectNextCount(4) + .consumeNextWith(person -> assertThat(person.getName()).endsWith("7")) + .thenCancel() + .verify(); +``` + +`WebTestClient`还可以连接到实时服务器并执行完整的端到端集成测试。这在 Spring boot 中也是支持的,在这里你可以[测试正在运行的服务器](https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-with-running-server)。 + +##### 过滤注册 + +在设置`MockMvc`实例时,可以注册一个或多个 Servlet `Filter`实例,如下例所示: + +爪哇 + +``` +mockMvc = standaloneSetup(new PersonController()).addFilters(new CharacterEncodingFilter()).build(); +``` + +Kotlin + +``` +// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed +``` + +通过`spring-test`中的`MockFilterChain`调用注册过滤器,最后一个过滤器将委托给`DispatcherServlet`。 + +##### MockMVC 与端到端测试 + +MockMVC 建立在“ Spring-test”模块的 Servlet API 模拟实现之上,并且不依赖于正在运行的容器。因此,与实际运行客户机和实时服务器的完整端到端集成测试相比,存在一些差异。 + +思考这个问题最简单的方法是从空白`MockHttpServletRequest`开始。无论你向它添加什么,请求都会变成什么。可能会让你感到惊讶的是,默认情况下没有上下文路径;没有`jsessionid`cookie;没有转发、错误或异步分派;因此,没有实际的 JSP 呈现。相反,“转发”和“重定向”URL 保存在`MockHttpServletResponse`中,并且可以使用期望断言。 + +这意味着,如果使用 JSP,你可以验证请求被转发到的 JSP 页面,但不呈现 HTML。换句话说,不会调用 JSP。但是,请注意,所有不依赖于转发的其他呈现技术,例如 ThymeLeaf 和 FreeMarker,都会按照预期的方式将 HTML 呈现给响应主体。对于通过`@ResponseBody`方法呈现 JSON、XML 和其他格式,也是如此。 + +或者,你可以考虑使用`@SpringBootTest`从 Spring 启动的完整的端到端集成测试支持。参见[Spring Boot Reference Guide](https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-testing)。 + +每种方法都有优点和缺点。 Spring MVC 测试中提供的选项是从经典单元测试到完全集成测试的不同规模的停止。可以肯定的是, Spring MVC 测试中的所有选项都不属于经典单元测试的范畴,但它们更接近于经典单元测试。例如,你可以通过将模拟的服务注入控制器来隔离 Web 层,在这种情况下,你只能通过`DispatcherServlet`来测试 Web 层,但要使用实际的 Spring 配置,因为你可能会在与上面的层隔离的情况下测试数据访问层。此外,你还可以使用独立设置,一次只关注一个控制器,并手动提供使其工作所需的配置。 + +使用 Spring MVC 测试时的另一个重要区别是,从概念上讲,这样的测试是服务器端的,因此你可以检查使用了什么处理程序,如果异常是用 HandleRexCeptionResolver 处理的,模型的内容是什么,有哪些绑定错误,以及其他细节。这意味着更容易编写预期值,因为服务器不是一个不透明的框,就像通过实际的 HTTP 客户机进行测试时一样。这通常是经典单元测试的一个优势:它更容易编写、推理和调试,但不会取代对完全集成测试的需求。同时,重要的是不要忽视这样一个事实,即反应是最重要的检查事项。简而言之,即使在同一个项目中,这里也有多种测试风格和策略的空间。 + +##### 进一步的例子 + +该框架自己的测试包括[许多样本测试](https://github.com/spring-projects/spring-framework/tree/main/spring-test/src/test/java/org/springframework/test/web/servlet/samples),旨在展示如何单独或通过[WebTestClient](https://github.com/spring-projects/spring-framework/tree/main/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client)使用 MockMVC。浏览这些示例以获得更多的想法。 + +#### 3.7.2.htmlunit 集成 + +Spring 提供[MockMvc](#spring-mvc-test-server)和[HtmlUnit](http://htmlunit.sourceforge.net/)之间的集成。这简化了在使用基于 HTML 的视图时执行端到端测试的过程。这种集成使你能够: + +* 使用[HtmlUnit](http://htmlunit.sourceforge.net/)、[WebDriver](https://www.seleniumhq.org)和[Geb](http://www.gebish.org/manual/current/#spock-junit-testng)等工具轻松测试 HTML 页面,而无需部署到 Servlet 容器。 + +* 在页面中测试 爪哇Script。 + +* 还可以选择使用模拟服务进行测试,以加快测试速度。 + +* 在容器内端到端测试和容器外集成测试之间共享逻辑。 + +| |MockMVC 使用不依赖于 Servlet 容器<br/>的模板化技术(例如,ThymeLeaf、FreeMarker 和其他),但不适用于 JSP,因为<br/>它们依赖于 Servlet 容器。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 为什么要进行 HTMLUnit 集成? + +我脑海中浮现的最明显的问题是“我为什么需要这个?”最好通过探索一个非常基本的示例应用程序来找到答案。假设你有一个 Spring MVC Web 应用程序,该应用程序在`Message`对象上支持增删改查操作。该应用程序还支持对所有消息进行分页。你会如何去测试它呢? + +通过 Spring MVC 测试,我们可以很容易地测试是否能够创建`Message`,如下所示: + +爪哇 + +``` +MockHttpServletRequestBuilder createMessage = post("/messages/") + .param("summary", "Spring Rocks") + .param("text", "In case you didn't know, Spring Rocks!"); + +mockMvc.perform(createMessage) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/messages/123")); +``` + +Kotlin + +``` +@Test +fun test() { + mockMvc.post("/messages/") { + param("summary", "Spring Rocks") + param("text", "In case you didn't know, Spring Rocks!") + }.andExpect { + status().is3xxRedirection() + redirectedUrl("/messages/123") + } +} +``` + +如果我们想测试允许我们创建消息的窗体视图,该怎么办?例如,假设我们的表单看起来像以下片段: + +``` +<form id="messageForm" action="/messages/" method="post"> + <div class="pull-right"><a href="/messages/">Messages</a></div> + + <label for="summary">Summary</label> + <input type="text" class="required" id="summary" name="summary" value="" /> + + <label for="text">Message</label> + <textarea id="text" name="text"></textarea> + + <div class="form-actions"> + <input type="submit" value="Create" /> + </div> +</form> +``` + +我们如何确保我们的表单产生正确的请求来创建新消息?一次幼稚的尝试可能类似于以下几点: + +爪哇 + +``` +mockMvc.perform(get("/messages/form")) + .andExpect(xpath("//input[@name='summary']").exists()) + .andExpect(xpath("//textarea[@name='text']").exists()); +``` + +Kotlin + +``` +mockMvc.get("/messages/form").andExpect { + xpath("//input[@name='summary']") { exists() } + xpath("//textarea[@name='text']") { exists() } +} +``` + +这种测试有一些明显的缺点。如果我们更新控制器以使用参数 `message’而不是`text`,那么我们的表单测试将继续通过,即使 HTML 表单与控制器不同步。为了解决这个问题,我们可以将两个测试结合起来,如下所示: + +爪哇 + +``` +String summaryParamName = "summary"; +String textParamName = "text"; +mockMvc.perform(get("/messages/form")) + .andExpect(xpath("//input[@name='" + summaryParamName + "']").exists()) + .andExpect(xpath("//textarea[@name='" + textParamName + "']").exists()); + +MockHttpServletRequestBuilder createMessage = post("/messages/") + .param(summaryParamName, "Spring Rocks") + .param(textParamName, "In case you didn't know, Spring Rocks!"); + +mockMvc.perform(createMessage) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/messages/123")); +``` + +Kotlin + +``` +val summaryParamName = "summary"; +val textParamName = "text"; +mockMvc.get("/messages/form").andExpect { + xpath("//input[@name='$summaryParamName']") { exists() } + xpath("//textarea[@name='$textParamName']") { exists() } +} +mockMvc.post("/messages/") { + param(summaryParamName, "Spring Rocks") + param(textParamName, "In case you didn't know, Spring Rocks!") +}.andExpect { + status().is3xxRedirection() + redirectedUrl("/messages/123") +} +``` + +这将减少我们的测试不正确通过的风险,但仍然存在一些问题: + +* 如果我们的页面上有多个表单怎么办?诚然,我们可以更新 XPath 表达式,但是随着我们考虑更多因素,它们变得更加复杂:字段是正确的类型吗?字段启用了吗?以此类推。 + +* 另一个问题是,我们正在做的工作是预期的两倍。我们必须首先验证视图,然后用刚刚验证过的相同参数提交视图。在理想情况下,这可以一次完成。 + +* 最后,我们仍然无法解释某些事情。例如,如果表单也有我们希望测试的 爪哇Script 验证呢? + +总的问题是,测试一个 Web 页面并不涉及一个单独的交互。相反,它是用户如何与 Web 页面交互以及该 Web 页面如何与其他资源交互的组合。例如,表单视图的结果被用作用户创建消息的输入。此外,我们的表单视图可能会使用影响页面行为的额外资源,例如 爪哇Script 验证。 + +###### 整合测试的拯救? + +为了解决前面提到的问题,我们可以执行端到端集成测试,但这有一些缺点。考虑测试这个视图,它可以让我们通过页面查看消息。我们可能需要进行以下测试: + +* 我们的页面是否向用户显示通知,以表明当消息为空时没有可用的结果? + +* 我们的页面是否正确地显示了一条消息? + +* 我们的页面是否适当地支持分页? + +要设置这些测试,我们需要确保我们的数据库包含正确的消息。这导致了一些额外的挑战: + +* 确保数据库中有正确的消息是很乏味的。(考虑一下国外的关键限制。 + +* 测试可能会变得很慢,因为每个测试都需要确保数据库处于正确的状态。 + +* 由于我们的数据库需要处于特定的状态,因此我们不能并行运行测试。 + +* 对自动生成的 ID、时间戳等项执行断言可能很困难。 + +这些挑战并不意味着我们应该完全放弃端到端集成测试。相反,我们可以通过重构我们的详细测试,使用运行得更快、更可靠且没有副作用的模拟服务,来减少端到端集成测试的数量。然后,我们可以实现少量真正的端到端集成测试,这些测试验证简单的工作流,以确保所有工作都正确地结合在一起。 + +###### 进入 HTMLUnit 集成 + +那么,我们如何在测试页面的交互和在测试套件中保持良好性能之间实现平衡呢?答案是:“通过将 MockMVC 与 HTMLUnit 集成在一起。” + +###### htmlunit 集成选项 + +当你想要将 mockmvc 与 htmlunit 集成在一起时,你有许多选项: + +* [mockmvc 和 htmlunit](#spring-mvc-test-server-htmlunit-mah):如果你想使用原始的 htmlUnit 库,请使用此选项。 + +* [MockMVC 和 WebDriver](#spring-mvc-test-server-htmlunit-webdriver):使用此选项可以简化开发,并在集成和端到端测试之间重用代码。 + +* [MOCKMVC 和 GEB](#spring-mvc-test-server-htmlunit-geb):如果你希望使用 Groovy 进行测试、简化开发以及在集成和端到端测试之间重用代码,请使用此选项。 + +##### mockmvc 和 htmlunit + +本节介绍如何集成 MockMVC 和 HTMLUnit。如果你想使用原始的 HTMLUnit 库,请使用此选项。 + +###### mockmvc 和 htmlunit 设置 + +首先,确保你包含了对“net.sourceforge.htmlunit:htmlunit”的测试依赖项。为了在 Apache HttpComponents4.5+ 中使用 HTMLUnit,你需要使用 HTMLUnit2.18 或更高版本。 + +我们可以使用“mockmvcwebclientbuilder”轻松创建一个与 mockmvvc 集成的 htmlunit<gtr="2489"/>,具体如下: + +爪哇 + +``` +WebClient webClient; + +@BeforeEach +void setup(WebApplicationContext context) { + webClient = MockMvcWebClientBuilder + .webAppContextSetup(context) + .build(); +} +``` + +Kotlin + +``` +lateinit var webClient: WebClient + +@BeforeEach +fun setup(context: WebApplicationContext) { + webClient = MockMvcWebClientBuilder + .webAppContextSetup(context) + .build() +} +``` + +| |这是使用`MockMvcWebClientBuilder`的一个简单示例。高级用法,<br/>见[高级`MockMvcWebClientBuilder`](#spring-mvc-test-server-htmlunit-mah-advanced-builder)。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +这确保了将`localhost`引用为服务器的任何 URL 都指向我们的“mockmvc”实例,而不需要真正的 HTTP 连接。正常情况下,通过使用网络连接来请求任何其他 URL。这让我们可以很容易地测试 CDNS 的使用情况。 + +###### mockmvc 和 htmlunit 的使用 + +现在,我们可以像通常那样使用 HTMLUnit,但不需要将我们的应用程序部署到 Servlet 容器中。例如,我们可以请求该视图创建带有以下内容的消息: + +爪哇 + +``` +HtmlPage createMsgFormPage = webClient.getPage("http://localhost/messages/form"); +``` + +Kotlin + +``` +val createMsgFormPage = webClient.getPage("http://localhost/messages/form") +``` + +| |默认的上下文路径是`""`。或者,我们可以指定上下文路径<br/>,如[Advanced `MockMvcWebClientBuilder`](#spring-mvc-test-server-htmlunit-mah-advanced-builder)中所述。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +一旦我们有了对`HtmlPage`的引用,我们就可以填写表单并提交它来创建消息,如下例所示: + +爪哇 + +``` +HtmlForm form = createMsgFormPage.getHtmlElementById("messageForm"); +HtmlTextInput summaryInput = createMsgFormPage.getHtmlElementById("summary"); +summaryInput.setValueAttribute("Spring Rocks"); +HtmlTextArea textInput = createMsgFormPage.getHtmlElementById("text"); +textInput.setText("In case you didn't know, Spring Rocks!"); +HtmlSubmitInput submit = form.getOneHtmlElementByAttribute("input", "type", "submit"); +HtmlPage newMessagePage = submit.click(); +``` + +Kotlin + +``` +val form = createMsgFormPage.getHtmlElementById("messageForm") +val summaryInput = createMsgFormPage.getHtmlElementById("summary") +summaryInput.setValueAttribute("Spring Rocks") +val textInput = createMsgFormPage.getHtmlElementById("text") +textInput.setText("In case you didn't know, Spring Rocks!") +val submit = form.getOneHtmlElementByAttribute("input", "type", "submit") +val newMessagePage = submit.click() +``` + +最后,我们可以验证新消息是否已成功创建。以下断言使用[AssertJ](https://assertj.github.io/doc/)库: + +爪哇 + +``` +assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123"); +String id = newMessagePage.getHtmlElementById("id").getTextContent(); +assertThat(id).isEqualTo("123"); +String summary = newMessagePage.getHtmlElementById("summary").getTextContent(); +assertThat(summary).isEqualTo("Spring Rocks"); +String text = newMessagePage.getHtmlElementById("text").getTextContent(); +assertThat(text).isEqualTo("In case you didn't know, Spring Rocks!"); +``` + +Kotlin + +``` +assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123") +val id = newMessagePage.getHtmlElementById("id").getTextContent() +assertThat(id).isEqualTo("123") +val summary = newMessagePage.getHtmlElementById("summary").getTextContent() +assertThat(summary).isEqualTo("Spring Rocks") +val text = newMessagePage.getHtmlElementById("text").getTextContent() +assertThat(text).isEqualTo("In case you didn't know, Spring Rocks!") +``` + +前面的代码在许多方面改进了我们的[MockMvc test](#spring-mvc-test-server-htmlunit-mock-mvc-test)。首先,我们不再需要显式地验证表单,然后创建一个看起来像表单的请求。相反,我们请求表单,填写并提交它,从而大大减少了开销。 + +另一个重要的因素是[HTMLUnit 使用 Mozilla Rhino 引擎](http://htmlunit.sourceforge.net/javascript.html)来评估 爪哇Script。这意味着我们还可以在页面中测试 爪哇Script 的行为。 + +有关使用 htmlunit 的更多信息,请参见[htmlunit 文档](http://htmlunit.sourceforge.net/gettingStarted.html)。 + +###### Advanced `MockMvcWebClientBuilder` + +在到目前为止的示例中,我们已经尽可能以最简单的方式使用`MockMvcWebClientBuilder`,通过基于 Spring TestContext 框架为我们加载的`WebApplicationContext`构建`WebClient`。下面的示例重复了这种方法: + +爪哇 + +``` +WebClient webClient; + +@BeforeEach +void setup(WebApplicationContext context) { + webClient = MockMvcWebClientBuilder + .webAppContextSetup(context) + .build(); +} +``` + +Kotlin + +``` +lateinit var webClient: WebClient + +@BeforeEach +fun setup(context: WebApplicationContext) { + webClient = MockMvcWebClientBuilder + .webAppContextSetup(context) + .build() +} +``` + +我们还可以指定其他配置选项,如下例所示: + +爪哇 + +``` +WebClient webClient; + +@BeforeEach +void setup() { + webClient = MockMvcWebClientBuilder + // demonstrates applying a MockMvcConfigurer (Spring Security) + .webAppContextSetup(context, springSecurity()) + // for illustration only - defaults to "" + .contextPath("") + // By default MockMvc is used for localhost only; + // the following will use MockMvc for example.com and example.org as well + .useMockMvcForHosts("example.com","example.org") + .build(); +} +``` + +Kotlin + +``` +lateinit var webClient: WebClient + +@BeforeEach +fun setup() { + webClient = MockMvcWebClientBuilder + // demonstrates applying a MockMvcConfigurer (Spring Security) + .webAppContextSetup(context, springSecurity()) + // for illustration only - defaults to "" + .contextPath("") + // By default MockMvc is used for localhost only; + // the following will use MockMvc for example.com and example.org as well + .useMockMvcForHosts("example.com","example.org") + .build() +} +``` + +作为一种选择,我们可以通过分别配置`MockMvc`实例并将其提供给`MockMvcWebClientBuilder`来执行完全相同的设置,如下所示: + +爪哇 + +``` +MockMvc mockMvc = MockMvcBuilders + .webAppContextSetup(context) + .apply(springSecurity()) + .build(); + +webClient = MockMvcWebClientBuilder + .mockMvcSetup(mockMvc) + // for illustration only - defaults to "" + .contextPath("") + // By default MockMvc is used for localhost only; + // the following will use MockMvc for example.com and example.org as well + .useMockMvcForHosts("example.com","example.org") + .build(); +``` + +Kotlin + +``` +// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed +``` + +这更详细,但是,通过使用`MockMvc`实例构建`WebClient`,我们可以在指尖获得 MockMVC 的全部功能。 + +| |有关创建`MockMvc`实例的更多信息,请参见[Setup Choices](#spring-mvc-test-server-setup-options)。| +|---|-----------------------------------------------------------------------------------------------------------------------| + +##### MockMVC 和 WebDriver + +在前面的部分中,我们已经了解了如何将 MockMVC 与原始的 HTMLUnitAPI 结合使用。在本节中,我们在 Selenium[WebDriver](https://docs.seleniumhq.org/projects/webdriver/)中使用了额外的抽象,以使事情变得更简单。 + +###### 为什么是 Webdriver 和 MockMVC? + +我们已经可以使用 HTMLUnit 和 MockMVC 了,那么为什么我们要使用 WebDriver 呢?Selenium WebDriver 提供了一个非常优雅的 API,可以让我们轻松地组织代码。为了更好地展示它是如何工作的,我们将在本节中探讨一个示例。 + +| |尽管是[Selenium](https://docs.seleniumhq.org/)的一部分,WebDriver 并不需要<br/>Selenium 服务器来运行测试。| +|---|-------------------------------------------------------------------------------------------------------------------------------------| + +假设我们需要确保消息是正确创建的。测试包括查找 HTML 表单输入元素,填写它们,并做出各种断言。 + +这种方法会导致许多单独的测试,因为我们也希望测试错误条件。例如,如果我们只填写表单的一部分,我们希望确保得到一个错误。如果我们填写了整个表单,那么新创建的消息将在之后显示。 + +如果其中一个字段被命名为“Summary”,那么我们可能会在测试中的多个地方重复类似于以下内容的内容: + +爪哇 + +``` +HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary"); +summaryInput.setValueAttribute(summary); +``` + +Kotlin + +``` +val summaryInput = currentPage.getHtmlElementById("summary") +summaryInput.setValueAttribute(summary) +``` + +那么,如果我们将`id`更改为`smmry`会发生什么呢?这样做将迫使我们更新所有的测试,以纳入这一变化。这违反了 dry 原则,因此我们最好将该代码提取到它自己的方法中,如下所示: + +爪哇 + +``` +public HtmlPage createMessage(HtmlPage currentPage, String summary, String text) { + setSummary(currentPage, summary); + // ... +} + +public void setSummary(HtmlPage currentPage, String summary) { + HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary"); + summaryInput.setValueAttribute(summary); +} +``` + +Kotlin + +``` +fun createMessage(currentPage: HtmlPage, summary:String, text:String) :HtmlPage{ + setSummary(currentPage, summary); + // ... +} + +fun setSummary(currentPage:HtmlPage , summary: String) { + val summaryInput = currentPage.getHtmlElementById("summary") + summaryInput.setValueAttribute(summary) +} +``` + +这样做可以确保在更改 UI 时不需要更新所有的测试。 + +我们甚至可以更进一步,将这个逻辑放在一个`Object`中,该逻辑表示我们当前所在的`HtmlPage`,如下例所示: + +爪哇 + +``` +public class CreateMessagePage { + + final HtmlPage currentPage; + + final HtmlTextInput summaryInput; + + final HtmlSubmitInput submit; + + public CreateMessagePage(HtmlPage currentPage) { + this.currentPage = currentPage; + this.summaryInput = currentPage.getHtmlElementById("summary"); + this.submit = currentPage.getHtmlElementById("submit"); + } + + public <T> T createMessage(String summary, String text) throws Exception { + setSummary(summary); + + HtmlPage result = submit.click(); + boolean error = CreateMessagePage.at(result); + + return (T) (error ? new CreateMessagePage(result) : new ViewMessagePage(result)); + } + + public void setSummary(String summary) throws Exception { + summaryInput.setValueAttribute(summary); + } + + public static boolean at(HtmlPage page) { + return "Create Message".equals(page.getTitleText()); + } +} +``` + +Kotlin + +``` + class CreateMessagePage(private val currentPage: HtmlPage) { + + val summaryInput: HtmlTextInput = currentPage.getHtmlElementById("summary") + + val submit: HtmlSubmitInput = currentPage.getHtmlElementById("submit") + + fun <T> createMessage(summary: String, text: String): T { + setSummary(summary) + + val result = submit.click() + val error = at(result) + + return (if (error) CreateMessagePage(result) else ViewMessagePage(result)) as T + } + + fun setSummary(summary: String) { + summaryInput.setValueAttribute(summary) + } + + fun at(page: HtmlPage): Boolean { + return "Create Message" == page.getTitleText() + } + } +} +``` + +以前,这种模式被称为[页面对象模式](https://github.com/SeleniumHQ/selenium/wiki/PageObjects)。虽然我们可以通过 HTMLUnit 实现这一点,但 WebDriver 提供了一些工具,我们将在下面的小节中对这些工具进行探讨,以使这种模式更容易实现。 + +###### MockMVC 和 WebDriver 设置 + +要在 Spring MVC 测试框架中使用 Selenium WebDriver,请确保你的项目包含对`org.seleniumhq.selenium:selenium-htmlunit-driver`的测试依赖关系。 + +我们可以通过使用“MockmvChtmLunitDriverBuilder”轻松地创建一个 Selenium WebDriver,该 WebDriver 与 MockMVC 集成在一起,如下例所示: + +爪哇 + +``` +WebDriver driver; + +@BeforeEach +void setup(WebApplicationContext context) { + driver = MockMvcHtmlUnitDriverBuilder + .webAppContextSetup(context) + .build(); +} +``` + +Kotlin + +``` +lateinit var driver: WebDriver + +@BeforeEach +fun setup(context: WebApplicationContext) { + driver = MockMvcHtmlUnitDriverBuilder + .webAppContextSetup(context) + .build() +} +``` + +| |这是使用`MockMvcHtmlUnitDriverBuilder`的一个简单示例。有关更高级的<br/>用法,请参见[高级`MockMvcHtmlUnitDriverBuilder`](#spring-mvc-test-server-htmlunit-webdriver-advanced-builder)。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +前面的示例确保将引用`localhost`作为服务器的任何 URL 都指向我们的`MockMvc`实例,而不需要真正的 HTTP 连接。正常情况下,通过使用网络连接来请求任何其他 URL。这让我们可以很容易地测试 CDNS 的使用情况。 + +###### MockMVC 和 WebDriver 的使用 + +现在,我们可以像通常那样使用 WebDriver,但不需要将我们的应用程序部署到 Servlet 容器中。例如,我们可以请求该视图创建带有以下内容的消息: + +爪哇 + +``` +CreateMessagePage page = CreateMessagePage.to(driver); +``` + +Kotlin + +``` +val page = CreateMessagePage.to(driver) +``` + +然后,我们可以填写表单并提交它来创建一条消息,如下所示: + +爪哇 + +``` +ViewMessagePage viewMessagePage = + page.createMessage(ViewMessagePage.class, expectedSummary, expectedText); +``` + +Kotlin + +``` +val viewMessagePage = + page.createMessage(ViewMessagePage::class, expectedSummary, expectedText) +``` + +通过利用页面对象模式,这改进了[HtmlUnit test](#spring-mvc-test-server-htmlunit-mah-usage)的设计。正如我们在[为什么是 Webdriver 和 MockMVC?](#spring-mvc-test-server-htmlunit-webdriver-why)中提到的,我们可以在 HTMLUnit 中使用 Page 对象模式,但是使用 WebDriver 要容易得多。考虑以下“创建应用程序”的实施: + +爪哇 + +``` +public class CreateMessagePage + extends AbstractPage { (1) + + (2) + private WebElement summary; + private WebElement text; + + (3) + @FindBy(css = "input[type=submit]") + private WebElement submit; + + public CreateMessagePage(WebDriver driver) { + super(driver); + } + + public <T> T createMessage(Class<T> resultPage, String summary, String details) { + this.summary.sendKeys(summary); + this.text.sendKeys(details); + this.submit.click(); + return PageFactory.initElements(driver, resultPage); + } + + public static CreateMessagePage to(WebDriver driver) { + driver.get("http://localhost:9990/mail/messages/form"); + return PageFactory.initElements(driver, CreateMessagePage.class); + } +} +``` + +|**1**|`CreateMessagePage`扩展了`AbstractPage`。我们没有详细讨论 `AbstractPage’的细节,但总的来说,它包含了我们所有页面的通用功能。<br/>例如,如果我们的应用程序具有导航栏、全局错误消息和其他<br/>功能,我们可以将此逻辑放置在共享位置。| +|-----|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|对于 HTML 页面中我们感兴趣的每个部分,我们都有一个成员变量<br/>。这些是`WebElement`型。Webdriver 的[`PageFactory`](https://github.com/SeleniumHQ/selenium/wiki/PageFactory)让我们通过自动解析<br/>每个`WebElement`,从`CreateMessagePage`的 htmlunit 版本中删除大量代码。[`PageFactory#initElements(WebDriver,Class<T>)`](https://seleniumhq.github.io/selenium/docs/api/java/org/openqa/selenium/support/PageFactory.html#initElements-org.openqa.selenium.WebDriver-java.lang.Class-)方法通过使用字段名称自动解析每个`WebElement`,并通过 HTML 页面中元素的<br/>或`name`查找它。| +|**3**|我们可以使用[@findby’注释](https://github.com/SeleniumHQ/selenium/wiki/PageFactory#making-the-example-work-using-annotations)来覆盖默认的查找行为。我们的示例展示了如何使用`@FindBy`注释来使用`css`选择器(**input[type=submit]**)查找我们的 Submit 按钮。| + +Kotlin + +``` +class CreateMessagePage(private val driver: WebDriver) : AbstractPage(driver) { (1) + + (2) + private lateinit var summary: WebElement + private lateinit var text: WebElement + + (3) + @FindBy(css = "input[type=submit]") + private lateinit var submit: WebElement + + fun <T> createMessage(resultPage: Class<T>, summary: String, details: String): T { + this.summary.sendKeys(summary) + text.sendKeys(details) + submit.click() + return PageFactory.initElements(driver, resultPage) + } + companion object { + fun to(driver: WebDriver): CreateMessagePage { + driver.get("http://localhost:9990/mail/messages/form") + return PageFactory.initElements(driver, CreateMessagePage::class.java) + } + } +} +``` + +|**1**|`CreateMessagePage`扩展了`AbstractPage`。我们没有详细讨论 `AbstractPage’的细节,但总的来说,它包含了我们所有页面的通用功能。<br/>例如,如果我们的应用程序具有导航栏、全局错误消息和其他<br/>功能,我们可以将此逻辑放置在共享位置。| +|-----|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|对于 HTML 页面中我们感兴趣的每个部分,我们都有一个成员变量<br/>。它们是`WebElement`型。WebDriver 的[`PageFactory`](https://github.com/SeleniumHQ/selenium/wiki/PageFactory)让我们通过自动解析`WebElement`每个`WebElement`,从`CreateMessagePage`的 htmlunit 版本中删除大量代码。[`PageFactory#initElements(WebDriver,Class<T>)`](https://seleniumhq.github.io/selenium/docs/api/java/org/openqa/selenium/support/PageFactory.html#initElements-org.openqa.selenium.WebDriver-java.lang.Class-)方法通过使用字段名称自动解析每个`WebElement`,并通过 HTML 页面中元素的<br/>或`name`查找它。| +|**3**|我们可以使用[@findby’注释](https://github.com/SeleniumHQ/selenium/wiki/PageFactory#making-the-example-work-using-annotations)来覆盖默认的查找行为。我们的示例展示了如何使用`@FindBy`注释来使用`css`选择器(**input[type=submit]**)查找我们的 Submit 按钮。| + +最后,我们可以验证新消息是否已成功创建。以下断言使用[AssertJ](https://assertj.github.io/doc/)断言程序库: + +爪哇 + +``` +assertThat(viewMessagePage.getMessage()).isEqualTo(expectedMessage); +assertThat(viewMessagePage.getSuccess()).isEqualTo("Successfully created a new message"); +``` + +Kotlin + +``` +assertThat(viewMessagePage.message).isEqualTo(expectedMessage) +assertThat(viewMessagePage.success).isEqualTo("Successfully created a new message") +``` + +我们可以看到,我们的`ViewMessagePage`允许我们与自定义域模型进行交互。例如,它公开了一个返回`Message`对象的方法: + +爪哇 + +``` +public Message getMessage() throws ParseException { + Message message = new Message(); + message.setId(getId()); + message.setCreated(getCreated()); + message.setSummary(getSummary()); + message.setText(getText()); + return message; +} +``` + +Kotlin + +``` +fun getMessage() = Message(getId(), getCreated(), getSummary(), getText()) +``` + +然后,我们可以在断言中使用富域对象。 + +最后,当测试完成时,我们不能忘记关闭`WebDriver`实例,如下所示: + +爪哇 + +``` +@AfterEach +void destroy() { + if (driver != null) { + driver.close(); + } +} +``` + +Kotlin + +``` +@AfterEach +fun destroy() { + if (driver != null) { + driver.close() + } +} +``` + +有关使用 WebDriver 的更多信息,请参见 Selenium[WebDriver 文档](https://github.com/SeleniumHQ/selenium/wiki/Getting-Started)。 + +###### Advanced `MockMvcHtmlUnitDriverBuilder` + +在到目前为止的示例中,我们已经尽可能以最简单的方式使用`MockMvcHtmlUnitDriverBuilder`,通过基于 Spring TestContext 框架为我们加载的`WebApplicationContext`构建`WebDriver`。这种方法在此重复如下: + +爪哇 + +``` +WebDriver driver; + +@BeforeEach +void setup(WebApplicationContext context) { + driver = MockMvcHtmlUnitDriverBuilder + .webAppContextSetup(context) + .build(); +} +``` + +Kotlin + +``` +lateinit var driver: WebDriver + +@BeforeEach +fun setup(context: WebApplicationContext) { + driver = MockMvcHtmlUnitDriverBuilder + .webAppContextSetup(context) + .build() +} +``` + +我们还可以指定其他配置选项,如下所示: + +爪哇 + +``` +WebDriver driver; + +@BeforeEach +void setup() { + driver = MockMvcHtmlUnitDriverBuilder + // demonstrates applying a MockMvcConfigurer (Spring Security) + .webAppContextSetup(context, springSecurity()) + // for illustration only - defaults to "" + .contextPath("") + // By default MockMvc is used for localhost only; + // the following will use MockMvc for example.com and example.org as well + .useMockMvcForHosts("example.com","example.org") + .build(); +} +``` + +Kotlin + +``` +lateinit var driver: WebDriver + +@BeforeEach +fun setup() { + driver = MockMvcHtmlUnitDriverBuilder + // demonstrates applying a MockMvcConfigurer (Spring Security) + .webAppContextSetup(context, springSecurity()) + // for illustration only - defaults to "" + .contextPath("") + // By default MockMvc is used for localhost only; + // the following will use MockMvc for example.com and example.org as well + .useMockMvcForHosts("example.com","example.org") + .build() +} +``` + +作为一种选择,我们可以通过分别配置`MockMvc`实例并将其提供给`MockMvcHtmlUnitDriverBuilder`来执行完全相同的设置,如下所示: + +Java + +``` +MockMvc mockMvc = MockMvcBuilders + .webAppContextSetup(context) + .apply(springSecurity()) + .build(); + +driver = MockMvcHtmlUnitDriverBuilder + .mockMvcSetup(mockMvc) + // for illustration only - defaults to "" + .contextPath("") + // By default MockMvc is used for localhost only; + // the following will use MockMvc for example.com and example.org as well + .useMockMvcForHosts("example.com","example.org") + .build(); +``` + +Kotlin + +``` +// Not possible in Kotlin until https://youtrack.jetbrains.com/issue/KT-22208 is fixed +``` + +这更详细,但是,通过构建带有`MockMvc`实例的`WebDriver`,我们可以在指尖获得 MockMVC 的全部功能。 + +| |有关创建`MockMvc`实例的更多信息,请参见[Setup Choices](#spring-mvc-test-server-setup-options)。| +|---|-----------------------------------------------------------------------------------------------------------------------| + +##### MockMvc and Geb + +在上一节中,我们了解了如何在 WebDriver 中使用 MockMVC。在这一节中,我们使用[Geb](http://www.gebish.org/)使我们的测试更加 Groovy-er。 + +###### 为什么是 GEB 和 MOCKMVC? + +GEB 由 WebDriver 支持,因此它提供了许多我们从 WebDriver 获得的[same benefits](#spring-mvc-test-server-htmlunit-webdriver-why)。然而,通过为我们处理一些样板代码,GEB 使事情变得更加简单。 + +###### mockmvc 和 geb 设置 + +我们可以使用使用使用 MockMVC 的 Selenium WebDriver 轻松地初始化 GEB`Browser`,如下所示: + +``` +def setup() { + browser.driver = MockMvcHtmlUnitDriverBuilder + .webAppContextSetup(context) + .build() +} +``` + +| |这是使用`MockMvcHtmlUnitDriverBuilder`的一个简单示例。有关更高级的<br/>用法,请参见[Advanced `MockMvcHtmlUnitDriverBuilder`](#spring-mvc-test-server-htmlunit-webdriver-advanced-builder)。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +这确保了将引用`localhost`作为服务器的任何 URL 指向我们的“mockmvc”实例,而不需要真正的 HTTP 连接。正常情况下,通过使用网络连接来请求任何其他 URL。这让我们可以很容易地测试 CDNS 的使用情况。 + +###### MockMVC 和 GEB 的使用 + +现在,我们可以像通常那样使用 GEB,但不需要将我们的应用程序部署到 Servlet 容器中。例如,我们可以请求该视图创建带有以下内容的消息: + +``` +to CreateMessagePage +``` + +然后,我们可以填写表单并提交它来创建一条消息,如下所示: + +``` +when: +form.summary = expectedSummary +form.text = expectedMessage +submit.click(ViewMessagePage) +``` + +未找到的任何未识别的方法调用或属性访问或引用都将转发到当前页对象。这删除了我们在直接使用 WebDriver 时所需的大量样板代码。 + +与直接使用 WebDriver 一样,通过使用页面对象模式,这改进了[HtmlUnit test](#spring-mvc-test-server-htmlunit-mah-usage)的设计。正如前面提到的,我们可以在 HTMLUnit 和 WebDriver 中使用页面对象模式,但是使用 GEB 则更容易。考虑一下我们新的基于 Groovy 的“CreateMessagePage”实现: + +``` +class CreateMessagePage extends Page { + static url = 'messages/form' + static at = { assert title == 'Messages : Create'; true } + static content = { + submit { $('input[type=submit]') } + form { $('form') } + errors(required:false) { $('label.error, .alert-error')?.text() } + } +} +``` + +我们的`CreateMessagePage`扩展了`Page`。我们不讨论`Page`的详细信息,但总而言之,它包含了我们所有页面的通用功能。我们定义了一个可以在其中找到此页面的 URL。这让我们可以导航到该页面,如下所示: + +``` +to CreateMessagePage +``` + +我们还有一个`at`闭包,它确定我们是否在指定的页面上。如果我们在正确的页面上,它应该返回`true`。这就是为什么我们可以断言我们在正确的页面上,如下所示: + +``` +then: +at CreateMessagePage +errors.contains('This field is required.') +``` + +| |我们在闭包中使用断言,这样我们就可以确定问题出在哪里<br/>,如果我们在错误的页面上。| +|---|---------------------------------------------------------------------------------------------------------------------| + +接下来,我们创建一个`content`闭包,该闭包指定页面中所有感兴趣的区域。我们可以使用[jQuery-ish Navigator API](http://www.gebish.org/manual/current/#the-jquery-ish-navigator-api)来选择我们感兴趣的内容。 + +最后,我们可以验证新消息是否已成功创建,如下所示: + +``` +then: +at ViewMessagePage +success == 'Successfully created a new message' +id +date +summary == expectedSummary +message == expectedMessage +``` + +有关如何最大限度地利用 GEB 的更多详细信息,请参见[The Book of Geb](http://www.gebish.org/manual/current/)用户手册。 + +### 3.8.测试客户端应用程序 + +你可以使用客户端测试来测试内部使用`RestTemplate`的代码。其思想是声明预期的请求并提供“存根”响应,这样你就可以专注于孤立地测试代码(也就是说,在不运行服务器的情况下)。下面的示例展示了如何做到这一点: + +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(); +``` + +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() +``` + +在前面的示例中,`MockRestServiceServer`(客户端 REST 测试的中心类)使用自定义的`RestTemplate`配置`ClientHttpRequestFactory`,该自定义配置根据预期断言实际请求并返回“存根”响应。在这种情况下,我们期望请求`/greeting`,并希望返回带有 `text/plain’内容的 200 响应。我们可以根据需要定义额外的期望请求和存根响应。当我们定义期望的请求和存根响应时,`RestTemplate`可以像往常一样在客户端代码中使用。在测试结束时,可以使用`mockServer.verify()`来验证所有的期望都已满足。 + +默认情况下,请求的预期顺序是声明期望的顺序。在构建服务器时,可以设置`ignoreExpectOrder`选项,在这种情况下,将检查所有期望(按顺序)以找到给定请求的匹配项。这意味着请求可以按任何顺序提出。下面的示例使用`ignoreExpectOrder`: + +Java + +``` +server = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build(); +``` + +Kotlin + +``` +server = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build() +``` + +即使默认情况下是无序的请求,每个请求也只允许运行一次。`expect`方法提供了一个重载变量,该变量接受一个指定计数范围的`ExpectedCount`参数(例如,`once`,`manyTimes`,`max`,`min`,`between’,等等)。下面的示例使用`times`: + +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(); +``` + +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() +``` + +请注意,当`ignoreExpectOrder`未设置(默认值),因此,请求是按照声明的顺序进行的,那么该顺序仅适用于任何预期请求中的第一个。例如,如果“/something”预期两次,然后是“/somewhere”三次,那么在向“/somewhere”发出请求之前,应该有一个对“/something”的请求,但是,除了随后的“/something”和“/somewhere”之外,请求可以随时出现。 + +作为上述所有功能的替代,客户端测试支持还提供了一个“ClienthtPrequestFactory”实现,你可以将其配置为`RestTemplate`,以将其绑定到`MockMvc`实例。这允许使用实际的服务器端逻辑处理请求,但不需要运行服务器。下面的示例展示了如何做到这一点: + +Java + +``` +MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build(); +this.restTemplate = new RestTemplate(new MockMvcClientHttpRequestFactory(mockMvc)); + +// Test code that uses the above RestTemplate ... +``` + +Kotlin + +``` +val mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build() +restTemplate = RestTemplate(MockMvcClientHttpRequestFactory(mockMvc)) + +// Test code that uses the above RestTemplate ... +``` + +#### 3.8.1.静态导入 + +与服务器端测试一样,客户端测试的 Fluent API 需要一些静态导入。搜索`MockRest*`很容易找到。Eclipse 用户应该在 Java Editor Content Assist Favoritions 下的 Eclipse 首选项中添加“MockRestRequestMatchers.*”和`MockRestResponseCreators.*`作为“最喜欢的静态成员”。这允许在输入静态方法名的第一个字符后使用内容辅助。其他 IDE(例如 IntelliJ)可能不需要任何额外的配置。检查对静态成员的代码完成的支持。 + +#### 3.8.2.客户端 REST 测试的进一步示例 + +Spring MVC 测试自己的测试包括[example tests](https://github.com/spring-projects/spring-framework/tree/main/spring-test/src/test/java/org/springframework/test/web/client/samples)客户端 REST 测试。 + +## 4. 更多资源 + +有关测试的更多信息,请参见以下参考资料: + +* [JUnit](https://www.junit.org/):“一个程序员友好的 Java 测试框架”。由 Spring 框架在其测试套件中使用,并在[Spring TestContext Framework](#testcontext-framework)中得到支持。 + +* [TestNG](https://testng.org/):受 JUnit 启发的测试框架,增加了对测试组、数据驱动测试、分布式测试和其他特性的支持。在[Spring TestContext Framework](#testcontext-framework)中支持 + +* [AssertJ](https://assertj.github.io/doc/):“Fluent Assertions for Java”,包括对 Java8Lambdas、Streams 和其他功能的支持。 + +* [Mock Objects](https://en.wikipedia.org/wiki/Mock_Object):维基百科条目。 + +* [MockObjects.com](http://www.mockobjects.com/):专门用于模拟对象的网站,这是一种在测试驱动开发中改进代码设计的技术。 + +* [Mockito](https://mockito.github.io):基于[Test Spy](http://xunitpatterns.com/Test%20Spy.html)模式的 Java 模拟库。 Spring 框架在其测试套件中使用。 + +* [EasyMock](https://easymock.org/):Java 库“通过使用 Java 的代理机制动态生成接口的模拟对象(以及通过类扩展的对象),为接口提供模拟对象。” + +* [JMock](https://jmock.org/):支持使用模拟对象对 Java 代码进行测试驱动开发的库。 + +* [DbUnit](https://www.dbunit.org/):JUnit 扩展(也可用于 Ant 和 Maven),它的目标是数据库驱动的项目,其中包括在测试运行之间使数据库处于已知状态。 + +* [Testcontainers](https://www.testcontainers.org/):支持 JUnit 测试的 Java 库,提供通用数据库、Selenium Web 浏览器或任何其他可以在 Docker 容器中运行的轻量级一次性实例。 + +* [The Grinder](https://sourceforge.net/projects/grinder/):Java 负载测试框架。 + +* [SpringMockK](https://github.com/Ninja-Squad/springmockk):支持在 Kotlin 中使用[MockK](https://mockk.io/)而不是 mockito 编写的 Spring 引导集成测试。 + diff --git a/docs/spring-framework/web-reactive.md b/docs/spring-framework/web-reactive.md new file mode 100644 index 0000000000000000000000000000000000000000..dd7cef83781f7c7b305c7a793087beec65951ab0 --- /dev/null +++ b/docs/spring-framework/web-reactive.md @@ -0,0 +1,7285 @@ +# 反应式堆栈上的 Web + +文档的这一部分涵盖了对构建在[反应流](https://www.reactive-streams.org/)API 上的反应式堆栈 Web 应用程序的支持,该应用程序可在非阻塞服务器上运行,例如 Netty、 Undertow 和 Servlet 3.1+ 容器。单独的章节涵盖[Spring WebFlux](webflux.html#webflux)框架、反应式[`WebClient`](#webflux-client)、对[testing](#webflux-test)的支持以及[反应库](#webflux-reactive-libraries)。对于 Servlet-stack Web 应用程序,请参见[Web on Servlet Stack](web.html#spring-web)。 + +## 1. Spring WebFlux + +Spring 框架中包含的原始 Web 框架 Spring Web MVC 是专门为 Servlet API 和 Servlet 容器构建的。反应式堆栈 Web 框架 Spring WebFlux 是后来在 5.0 版本中添加的。它是完全非阻塞的,支持[反应流](https://www.reactive-streams.org/)背压,并在 Netty、 Undertow 和 Servlet 3.1+ 容器等服务器上运行。 + +这两个 Web 框架都反映了它们的源模块的名称([spring-webmvc](https://github.com/spring-projects/spring-framework/tree/main/spring-webmvc)和[spring-webflux](https://github.com/spring-projects/spring-framework/tree/main/spring-webflux)),并在 Spring 框架中并存。每个模块都是可选的。应用程序可以使用一个或另一个模块,或者在某些情况下,同时使用这两个模块—例如,具有 Spring MVC 控制器的反应性。 + +### 1.1.概述 + +为什么要创建 WebFlux? + +部分解决方案是需要一个非阻塞的 Web 堆栈来处理少量线程的并发性,并以更少的硬件资源进行扩展。 Servlet 3.1 确实为非阻塞 I/O 提供了一个 API。但是,使用它会偏离 Servlet API 的其余部分,其中契约是同步的(`filter’,`Servlet`)或阻塞的(`getParameter’,`getPart’)。这就是一个新的通用 API 的动机,它可以作为跨任何非阻塞运行时的基础。这一点很重要,因为服务器(如 Netty)在异步、非阻塞空间中已经很好地建立了。 + +答案的另一部分是函数式编程。正如 爪哇5 中添加的注释创造了机会(例如注释的 REST 控制器或单元测试)一样,爪哇8 中添加的 lambda 表达式为 爪哇 中的功能 API 创造了机会。这对于允许异步逻辑的声明式组合的非阻塞应用程序和延续风格 API(由`CompletableFuture`和[ReactiveX](http://reactivex.io/)推广)来说是一个福音。在编程模型级别,爪哇8 使 Spring WebFlux 能够在带注释的控制器之外提供功能性的 Web 端点。 + +#### 1.1.1.定义“反应性” + +我们谈到了“非阻塞”和“功能性”,但是反应性是什么意思呢? + +术语“反应性”指的是围绕对变化做出反应而构建的编程模型——网络组件对 I/O 事件做出反应,UI 控制器对鼠标事件做出反应,等等。从这个意义上说,非阻塞是反应性的,因为我们现在不是被阻塞,而是在操作完成或数据可用时对通知做出反应。 + +在 Spring 团队中,还有另一个与“反应性”相关的重要机制,那就是无阻塞背压。在同步的命令式代码中,阻塞调用作为一种自然的反压形式,迫使调用者等待。在非阻塞代码中,控制事件的速率变得很重要,这样快速生成器就不会淹没其目标。 + +反应流是一个[small spec](https://github.com/reactive-streams/reactive-streams-jvm/blob/master/README.md#specification)(在 爪哇9 中也是[adopted](https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/Flow.html)),它定义了具有背压的异步组件之间的交互。例如,数据存储库(充当[Publisher](https://www.reactive-streams.org/reactive-streams-1.0.1-javadoc/org/reactivestreams/Publisher.html))可以生成 HTTP 服务器(充当[Subscriber](https://www.reactive-streams.org/reactive-streams-1.0.1-javadoc/org/reactivestreams/Subscriber.html))随后可以写入响应的数据。反应流的主要目的是让订阅者控制发布者生成数据的速度或速度。 + +| |**常见问题:如果出版商不能放慢速度怎么办?**<br/>反应流的目的只是为了建立机制和边界。<br/>如果一个发布者不能减速,它就必须决定是缓冲、下降还是失败。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.1.2.反应性 API + +反应流在互操作性中起着重要的作用。它是库和基础设施组件感兴趣的,但作为应用程序 API 用处不大,因为它的级别太低。应用程序需要一个更高层次和更丰富的功能 API 来组成异步逻辑——类似于 爪哇8`Stream`API,但不仅仅是用于集合。这就是反应库所扮演的角色。 + +[Reactor](https://github.com/reactor/reactor)是 Spring WebFlux 选择的反应库。它提供了[`Mono`](https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Mono.html)和[`Flux`](https://projectreactor.io/docs/core/release/api/reactor/core/publisher/Flux.html)API 类型,以便通过与 reactivex[运算符词汇表](http://reactivex.io/documentation/operators.html)对齐的一组丰富的运算符处理 0..1 和 0..n 的数据序列。反应器是一个反应库,因此,它的所有操作人员都支持无阻塞背压。Reactor 非常关注服务器端 爪哇。它是与 Spring 密切合作开发的。 + +WebFlux 需要将 Reactor 作为核心依赖项,但它可以通过反应流与其他反应库进行互操作。作为一般规则,WebFlux API 接受普通的`Publisher`作为输入,在内部将其调整为反应器类型,并使用该类型,然后返回 `flux’或`Mono`作为输出。因此,你可以将任何`Publisher`作为输入传递,并且可以对输出应用操作,但是你需要调整输出以与另一个反应库一起使用。只要可行(例如,带注释的控制器),WebFlux 就会透明地适应 RX爪哇 或其他反应库的使用。有关更多详细信息,请参见[反应库](#webflux-reactive-libraries)。 + +| |除了反应性 API 之外,WebFlux 还可以与[Coroutines](languages.html#coroutines)API 一起使用 Kotlin,这提供了一种更必要的编程风格。<br/>下面的 Kotlin 代码示例将与协程 API 一起提供。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.1.3.程序设计模型 + +`spring-web`模块包含支撑 Spring WebFlux 的反应性基础,包括 HTTP 抽象、支持服务器的反应性流[adapters](#webflux-httphandler)、[codecs](#webflux-codecs),以及与 Servlet API 类似但具有非阻塞契约的核心[“WebHandler”API](#webflux-web-handler-api)。 + +在此基础上, Spring WebFlux 提供了两种编程模型的选择: + +* [带注释的控制器](#webflux-controller):与 Spring MVC 一致,并且基于来自`spring-web`模块的相同注释。 Spring MVC 和 WebFlux 控制器都支持反应(反应器和 RX爪哇)返回类型,因此,很难将它们区分开来。一个值得注意的区别是,WebFlux 还支持活性的`@RequestBody`参数。 + +* [功能端点](#webflux-fn):基于 lambda 的、轻量级的和函数式的编程模型。你可以将其视为一个小型的库或一组实用程序,应用程序可以使用它们来路由和处理请求。与注解控制器的最大区别在于,应用程序负责从头到尾处理请求,而不是通过注解声明意图并被回调。 + +#### 1.1.4.适用性 + +Spring MVC 还是 WebFlux? + +这是个自然的问题,但会造成一种不合理的二分法。实际上,两者共同作用来扩大可供选择的范围。这两种设计是为了彼此之间的连续性和一致性,它们可以并排使用,并且来自每一方的反馈对双方都有利。下面的图表显示了这两者之间的关系,它们的共同点以及各自的独特支持: + +![spring mvc and webflux venn](images/spring-mvc-and-webflux-venn.png) + +我们建议你考虑以下几点: + +* 如果你的 Spring MVC 应用程序运行良好,则无需更改。命令式编程是编写、理解和调试代码的最简单的方法。你可以选择最多的库,因为从历史上看,大多数库都是阻塞的。 + +* 如果你已经在寻找一个非阻塞的 Web 堆栈, Spring WebFlux 提供了与该空间中的其他程序相同的执行模型的优点,并且还提供了服务器(Netty、 Tomcat、 Jetty、 Undertow 和 Servlet 3.1+ 容器)的选择,以及编程模型的选择(带注释的控制器和功能的 Web 端点),以及反应库的选择(反应器、RX爪哇 或其他)。 + +* 如果你对与 爪哇8Lambdas 或 Kotlin 一起使用的轻量级、功能性 Web 框架感兴趣,那么可以使用 Spring WebFlux Functional Web Endpoints。对于较小的应用程序或需求不那么复杂的微服务来说,这也是一个很好的选择,它们可以受益于更高的透明度和控制。 + +* 在微服务架构中,你可以混合使用具有 Spring MVC 或 Spring WebFlux 控制器的应用程序,或者具有 Spring WebFlux 功能端点的应用程序。在两个框架中都支持相同的基于注释的编程模型,这使得在为正确的工作选择正确的工具的同时更容易重用知识。 + +* 评估应用程序的一种简单方法是检查其依赖关系。如果你有阻塞持久性 API( JPA、JDBC)或网络 API 可供使用, Spring MVC 至少是通用架构的最佳选择。对于 Reactor 和 Rx爪哇 来说,在单独的线程上执行阻塞调用在技术上是可行的,但是你不会充分利用非阻塞的 Web 堆栈。 + +* 如果你有一个 Spring MVC 应用程序调用到远程服务,请尝试 reactive`WebClient`。你可以直接从 Spring MVC 控制器方法返回反应类型(reactor,rxjava,[or other](#webflux-reactive-libraries))。每次调用的延迟越大或调用之间的相互依赖性越大,其好处就越显著。 Spring MVC 控制器也可以调用其他无功分量。 + +* 如果你有一个庞大的团队,请记住,在向非阻塞、函数式和声明式编程的转变中,学习曲线很陡。在没有全开关的情况下,一种实用的启动方式是使用反应式`WebClient`。除此之外,从小处着手,衡量收益。我们预计,对于广泛的应用而言,这种转变是不必要的。如果你不确定要寻找哪些好处,那么可以从了解非阻塞 I/O 的工作方式(例如,在单线程 node.js 上的并发性)及其效果开始。 + +#### 1.1.5.服务器 + +Spring WebFlux 在 Tomcat、 Jetty、 Servlet 3.1+ 容器上以及在诸如 Netty 和 Undertow 等非 Servlet 运行时上得到支持。所有服务器都适应于低级别的[common API](#webflux-httphandler),以便可以跨服务器支持更高级别的[程序设计模型](#webflux-programming-models)。 + +Spring WebFlux 不具有启动或停止服务器的内置支持。然而,很容易用几行代码来[assemble](#webflux-web-handler-api)从 Spring 配置和[WebFlux 基础设施](#webflux-config)和[run it](#webflux-httphandler)的应用程序。 + +Spring Boot 有一个 WebFlux 启动器,可以自动执行这些步骤。默认情况下,启动器使用 Netty,但通过更改 Maven 或 Gradle 依赖关系,很容易切换到 Tomcat、 Jetty 或 Undertow。 Spring 启动默认为 netty,因为它在异步、非阻塞空间中被更广泛地使用,并且允许客户机和服务器共享资源。 + +Tomcat 和 Jetty 可以与 Spring MVC 和 WebFlux 一起使用。然而,请记住,它们的使用方式是非常不同的。 Spring MVC 依赖于 Servlet 阻塞 I/O,并允许应用程序在需要时直接使用 Servlet API。 Spring WebFlux 依赖于 Servlet 3.1 非阻塞 I/O,并使用 Servlet 底层适配器后面的 API。它不会直接暴露在外使用。 + +对于 Undertow, Spring WebFlux 直接使用 Undertow API 而不使用 Servlet API。 + +#### 1.1.6.表现 + +表演有许多特点和意义。反应性和非阻塞通常不会使应用程序运行得更快。在某些情况下,它们可以(例如,如果使用“WebClient”并行运行远程调用)。总的来说,它需要更多的工作来做事情的非阻塞的方式,这可以稍微增加所需的处理时间。 + +反应性和非阻塞的主要预期好处是能够以较小的、固定的线程数量和较少的内存进行扩展。这使得应用程序在负载下更具弹性,因为它们以更可预测的方式扩展。然而,为了观察这些好处,你需要有一些延迟(包括缓慢和不可预测的网络 I/O 的混合)。这就是反应性堆栈开始显示其优势的地方,差异可能是巨大的。 + +#### 1.1.7.并发模型 + +Spring MVC 和 Spring WebFlux 都支持带注释的控制器,但是在并发模型和用于阻塞和线程的默认假设中存在一个关键的区别。 + +在 Spring MVC(和 Servlet 一般的应用程序)中,假定应用程序可以阻止当前线程,(例如,用于远程调用)。出于这个原因, Servlet 容器使用一个大的线程池来吸收请求处理过程中的潜在阻塞。 + +Spring 在 WebFlux(以及一般的非阻塞服务器)中,假定应用程序不会阻塞。因此,非阻塞服务器使用一个小的、固定大小的线程池(事件循环工作者)来处理请求。 + +| |“可伸缩”和“少量线程”听起来可能是矛盾的,但永远不要阻塞<br/>当前线程(而是依赖回调)意味着你不需要额外的线程,因为<br/>没有要吸收的阻塞调用。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +调用阻塞 API + +如果你确实需要使用阻塞库,该怎么办?Reactor 和 Rx爪哇 都提供了“Publishon”操作符,以便在不同的线程上继续处理。这意味着有一个很容易逃脱的舱口。然而,请记住,阻塞 API 并不适合这种并发模型。 + +可变状态 + +在 Reactor 和 RX爪哇 中,你通过运算符声明逻辑。在运行时,会形成一个反应性管道,在该管道中,数据会在不同的阶段中按顺序进行处理。这样做的一个主要好处是,它使应用程序不必保护可变状态,因为该管道中的应用程序代码永远不会并发调用。 + +线程模型 + +在运行 Spring WebFlux 的服务器上,你应该看到哪些线程? + +* 在“vanilla” Spring WebFlux 服务器上(例如,没有数据访问或其他可选的依赖关系),你可以期望为服务器提供一个线程,并为请求处理提供其他几个线程(通常与 CPU 内核的数量一样多)。 Servlet 然而,容器可以以更多线程(例如, Tomcat 上的 10)开始,以支持 Servlet(阻塞)I/O 和 Servlet 3.1(非阻塞)I/O 的使用。 + +* 反应式`WebClient`以事件循环方式进行操作。因此,你可以看到与此相关的处理线程的数量很少且是固定的(例如,`reactor-http-nio-`与 Reactor Netty Connector)。但是,如果 Reactor Netty 同时用于客户机和服务器,则默认情况下这两个服务器共享事件循环资源。 + +* Reactor 和 Rx爪哇 提供线程池抽象,称为调度器,与用于将处理切换到不同线程池的“publishon”操作符一起使用。调度程序的名称建议了一种特定的并发策略——例如,“并行”(用于线程数量有限的 CPU 绑定工作)或“弹性”(用于具有大量线程的 I/O 绑定工作)。如果你看到这样的线程,这意味着某些代码正在使用特定的线程池`Scheduler`策略。 + +* 数据访问库和其他第三方依赖项也可以创建和使用自己的线程。 + +配置 + +Spring 框架不提供对启动和停止[servers](#webflux-server-choice)的支持。要为服务器配置线程模型,你需要使用特定于服务器的配置 API,或者,如果你使用 Spring 引导,请检查每个服务器的 Spring 引导配置选项。你可以直接[configure](#webflux-client-builder)`WebClient`。对于所有其他库,请参阅它们各自的文档。 + +### 1.2.反应核 + +`spring-web`模块包含对反应式 Web 应用程序的以下基本支持: + +* 对于服务器请求处理,有两个级别的支持。 + + * [HttpHandler](#webflux-httphandler):使用非阻塞 I/O 和反应流反压处理 HTTP 请求的基本契约,以及用于反应堆网络、 Undertow、 Tomcat、 Jetty 和任何 Servlet 3.1+ 容器的适配器。 + + * [“WebHandler”API](#webflux-web-handler-api):略高的级别,用于请求处理的通用 Web API,在此基础上构建了具体的编程模型,例如带注释的控制器和功能端点。 + +* 对于客户端,有一个基本的`ClientHttpConnector`契约来执行具有非阻塞 I/O 和反应性流反压的 HTTP 请求,以及用于[Reactor Netty](https://github.com/reactor/reactor-netty)、反应性[Jetty HttpClient](https://github.com/jetty-project/jetty-reactive-httpclient)和[Apache HttpComponents](https://hc.apache.org/)的适配器。应用程序中使用的更高级别[WebClient](#webflux-client)建立在此基本契约上。 + +* 对于客户机和服务器,[codecs](#webflux-codecs)用于序列化和反序列化 HTTP 请求和响应内容。 + +#### 1.2.1.`HttpHandler` + +[HttpHandler](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/http/server/reactive/HttpHandler.html)是一个简单的契约,它只有一个方法来处理请求和响应。它是故意最小化的,它的主要目的也是唯一的目的是在不同的 HTTP 服务器 API 上进行最小化抽象。 + +下表描述了受支持的服务器 API: + +| Server name |使用的服务器 API| Reactive Streams support | +|---------------------|--------------------------------------------------------------------------------|-------------------------------------------------------------------| +| Netty |Netty API| [Reactor Netty](https://github.com/reactor/reactor-netty) | +| Undertow |Undertow 空气污染指数| spring-web: Undertow to Reactive Streams bridge | +| Tomcat |Servlet 3.1 非阻塞 I/O; Tomcat 读写字节缓冲器 VS 字节的 API[]|spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge| +| Jetty |Servlet 3.1 非阻塞 I/O; Jetty 写字节缓冲器 VS 字节的 API[]|spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge| +|Servlet 3.1 container|Servlet 3.1 非阻塞 I/O|spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge| + +下表描述了服务器的依赖关系(另请参见[支持的版本](https://github.com/spring-projects/spring-framework/wiki/What%27s-New-in-the-Spring-Framework)): + +| Server name | Group id |工件名称| +|-------------|-----------------------|---------------------------| +|Reactor Netty|io.projectreactor.netty|反应堆网状结构| +| Undertow | io.undertow |Undertow-核心| +| Tomcat |org.apache.tomcat.embed|Tomcat-嵌入-核心| +| Jetty | org.eclipse.jetty |Jetty-服务器, Jetty- Servlet| + +下面的代码片段显示了在每个服务器 API 中使用`HttpHandler`适配器的情况: + +**反应堆网状结构** + +爪哇 + +``` +HttpHandler handler = ... +ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler); +HttpServer.create().host(host).port(port).handle(adapter).bind().block(); +``` + +Kotlin + +``` +val handler: HttpHandler = ... +val adapter = ReactorHttpHandlerAdapter(handler) +HttpServer.create().host(host).port(port).handle(adapter).bind().block() +``` + +**Undertow** + +爪哇 + +``` +HttpHandler handler = ... +UndertowHttpHandlerAdapter adapter = new UndertowHttpHandlerAdapter(handler); +Undertow server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build(); +server.start(); +``` + +Kotlin + +``` +val handler: HttpHandler = ... +val adapter = UndertowHttpHandlerAdapter(handler) +val server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build() +server.start() +``` + +**Tomcat** + +爪哇 + +``` +HttpHandler handler = ... +Servlet servlet = new TomcatHttpHandlerAdapter(handler); + +Tomcat server = new Tomcat(); +File base = new File(System.getProperty("java.io.tmpdir")); +Context rootContext = server.addContext("", base.getAbsolutePath()); +Tomcat.addServlet(rootContext, "main", servlet); +rootContext.addServletMappingDecoded("/", "main"); +server.setHost(host); +server.setPort(port); +server.start(); +``` + +Kotlin + +``` +val handler: HttpHandler = ... +val servlet = TomcatHttpHandlerAdapter(handler) + +val server = Tomcat() +val base = File(System.getProperty("java.io.tmpdir")) +val rootContext = server.addContext("", base.absolutePath) +Tomcat.addServlet(rootContext, "main", servlet) +rootContext.addServletMappingDecoded("/", "main") +server.host = host +server.setPort(port) +server.start() +``` + +**Jetty** + +爪哇 + +``` +HttpHandler handler = ... +Servlet servlet = new JettyHttpHandlerAdapter(handler); + +Server server = new Server(); +ServletContextHandler contextHandler = new ServletContextHandler(server, ""); +contextHandler.addServlet(new ServletHolder(servlet), "/"); +contextHandler.start(); + +ServerConnector connector = new ServerConnector(server); +connector.setHost(host); +connector.setPort(port); +server.addConnector(connector); +server.start(); +``` + +Kotlin + +``` +val handler: HttpHandler = ... +val servlet = JettyHttpHandlerAdapter(handler) + +val server = Server() +val contextHandler = ServletContextHandler(server, "") +contextHandler.addServlet(ServletHolder(servlet), "/") +contextHandler.start(); + +val connector = ServerConnector(server) +connector.host = host +connector.port = port +server.addConnector(connector) +server.start() +``` + +**Servlet 3.1+ Container** + +要作为 WAR 部署到任何 Servlet 3.1+ 容器,可以在 WAR 中扩展并包括[`AbstractreActiveWebInitializer’](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/server/adapter/AbstractReactiveWebInitializer.html)。该类将`HttpHandler`与`ServletHttpHandlerAdapter`包装在一起,并将其注册为`Servlet`。 + +#### 1.2.2.`WebHandler`api + +`org.springframework.web.server`包以[`HttpHandler`](#webflux-httphandler)契约为基础,通过多个[“WebExceptionHandler”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/server/WebExceptionHandler.html)、多个[`WebFilter`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/server/WebFilter.html)和一个[`WebHandler`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/server/WebHandler.html)组件的链来提供一个通用的 Web API 来处理请求。通过简单地指向 Spring `ApplicationContext’,其中组件是,和/或通过向构建器注册组件,可以将链与放在一起。 + +虽然`HttpHandler`的一个简单目标是抽象不同 HTTP 服务器的使用,但 `WebHandler’API 旨在提供 Web 应用程序中常用的一组更广泛的功能,例如: + +* 具有属性的用户会话。 + +* 请求属性。 + +* 已为请求解析`Locale`或`Principal`。 + +* 访问解析和缓存的表单数据。 + +* 多部分数据的抽象。 + +* 还有更多.. + +##### 特殊类型 Bean + +下表列出了`WebHttpHandlerBuilder`可以在 Spring ApplicationContext 中自动检测的组件,或者可以直接向其注册的组件: + +| Bean name | Bean type |Count|说明| +|----------------------------|----------------------------|-----|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| \<any\> | `WebExceptionHandler` |0..N |为`WebFilter`实例和目标“WebHandler”的链中的异常提供处理。有关更多详细信息,请参见[Exceptions](#webflux-exception-handler)。| +| \<any\> | `WebFilter` |0..N |将截取样式逻辑应用于过滤器链的其余部分之前和之后,以及<br/>目标`WebHandler`。有关更多详细信息,请参见[Filters](#webflux-filters)。| +| `webHandler` | `WebHandler` | 1 |请求的处理程序。| +| `webSessionManager` | `WebSessionManager` |0..1 |默认情况下,`WebSession`实例的管理器通过`ServerWebExchange`上的方法公开。| +| `serverCodecConfigurer` | `ServerCodecConfigurer` |0..1 |用于访问`HttpMessageReader`实例以解析表单数据和多部分数据,然后<br/>通过`ServerWebExchange`上的方法公开。默认情况下`ServerCodecConfigurer.create()`。| +| `localeContextResolver` | `LocaleContextResolver` |0..1 |默认情况下,`LocaleContext`的解析器通过`ServerWebExchange`上的方法公开。| +|`forwardedHeaderTransformer`|`ForwardedHeaderTransformer`|0..1 |对于处理转发的类型头,可以通过提取和删除它们,也可以只删除它们。<br/>默认情况下不使用。| + +##### 表单数据 + +`ServerWebExchange`公开了以下访问表单数据的方法: + +爪哇 + +``` +Mono<MultiValueMap<String, String>> getFormData(); +``` + +Kotlin + +``` +suspend fun getFormData(): MultiValueMap<String, String> +``` + +`DefaultServerWebExchange`使用配置的`HttpMessageReader`将表单数据(`application/x-WWW-form-urlencoded`)解析为`MultiValueMap`。默认情况下,“FormHttpMessageReader”被配置为由`ServerCodecConfigurer` Bean 使用(参见[Web Handler API](#webflux-web-handler-api))。 + +##### 多部分数据 + +[Web MVC](web.html#mvc-multipart) + +`ServerWebExchange`公开了以下访问多部分数据的方法: + +爪哇 + +``` +Mono<MultiValueMap<String, Part>> getMultipartData(); +``` + +Kotlin + +``` +suspend fun getMultipartData(): MultiValueMap<String, Part> +``` + +`DefaultServerWebExchange`使用配置的 `HttpMessageReader<multivalueMap<String, Part>>` 将`multipart/form-data`内容解析为`MultiValueMap`。默认情况下,这是`DefaultPartHttpMessageReader`,它没有任何第三方依赖关系。或者,可以使用`SynchronossPartHttpMessageReader`,这是基于[Synchronoss 多部件蔚来](https://github.com/synchronoss/nio-multipart)库的。这两个参数都是通过`ServerCodecConfigurer` Bean 配置的(参见[Web Handler API](#webflux-web-handler-api))。 + +要以流媒体方式解析多部分数据,可以使用从 `HttpMessageReader<Part>’返回的`Flux<Part>`。例如,在带注释的控制器中,使用“@requestPart”意味着`Map`-通过名称访问各个部分,因此需要完整地解析多部分数据。相比之下,你可以使用`@RequestBody`将内容解码为`Flux<Part>`,而无需收集到`MultiValueMap`。 + +##### 转发头 + +[Web MVC](web.html#filters-forwarded-headers) + +当请求通过代理(例如负载均衡器)时,主机、端口和方案可能会发生变化。从客户机的角度来看,这使得创建指向正确的主机、端口和方案的链接成为一项挑战。 + +[RFC 7239](https://tools.ietf.org/html/rfc7239)定义了`Forwarded`HTTP 报头,代理可以使用该报头来提供有关原始请求的信息。也有其他非标准标题,包括`X-Forwarded-Host`,`X-Forwarded-Port`,`x-forward-proto`,`X-Forwarded-Ssl`和`X-Forwarded-Prefix`。 + +`ForwardedHeaderTransformer`是一个组件,它基于转发的标头修改请求的主机、端口和方案,然后删除这些标头。如果将其声明为 Bean,并使用`forwardedHeaderTransformer`的名称,则将其声明为[detected](#webflux-web-handler-api-special-beans)并使用。 + +转发头的安全性需要考虑,因为应用程序不能知道头是由代理添加的,还是由恶意客户机添加的。这就是为什么在信任边界上的代理应该被配置为删除来自外部的不受信任的转发流量。你还可以将`ForwardedHeaderTransformer`配置为 `removeonly=true’,在这种情况下,它会删除但不使用头。 + +| |在 5.1 中,`ForwardedHeaderFilter`被弃用,并被 `forwardedHeaderTransformer’取代,因此,在创建<br/>交换之前,可以更早地处理转发的报头。如果无论如何都配置了过滤器,则将其从<br/>过滤器列表中取出,并使用`ForwardedHeaderTransformer`代替。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.2.3.过滤器 + +[Web MVC](web.html#filters) + +在[“WebHandler”API](#webflux-web-handler-api)中,你可以使用`WebFilter`在过滤器和目标 `WebHandler’的处理链的其余部分之前和之后应用拦截风格的逻辑。当使用[WebFlux Config](#webflux-config)时,注册`WebFilter`就像将其声明为 Spring Bean 一样简单,并且(可选地)通过在 Bean 声明上使用`@Order`或通过实现`Ordered`来表示优先级。 + +##### CORS + +[Web MVC](web.html#filters-cors) + +Spring WebFlux 通过控制器上的注释为 CORS 配置提供了细粒度的支持。然而,当你在 Spring 安全性的情况下使用它时,我们建议使用内置的“CorsFilter”,它必须在 Spring 安全性的过滤器链之前订购。 + +有关更多详细信息,请参见[CORS](#webflux-cors)和[webflux-cors.html](webflux-cors.html#webflux-cors-webfilter)一节。 + +#### 1.2.4.例外 + +[Web MVC](web.html#mvc-ann-customer-servlet-container-error-page) + +在[“WebHandler”API](#webflux-web-handler-api)中,你可以使用`WebExceptionHandler`来处理来自`WebFilter`实例和目标`WebHandler`的链中的异常。当使用[WebFlux Config](#webflux-config)时,注册`WebExceptionHandler`就像将其声明为 Spring Bean 一样简单,并且(可选地)通过在 Bean 声明上使用`@Order`或通过实现`Ordered`来表示优先级。 + +下表描述了可用的`WebExceptionHandler`实现: + +| Exception Handler |说明| +|---------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `ResponseStatusExceptionHandler` |通过将响应设置为异常的 HTTP 状态代码,为类型[` 责任-------------------------------------------------------](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/server/ResponseStatusException.html)的异常提供处理。| +|`WebFluxResponseStatusExceptionHandler`|`ResponseStatusExceptionHandler`的扩展,它还可以在任何异常情况下确定<br/>代码的 HTTP 状态`@ResponseStatus`注释。<br/><br/>此处理程序是在[WebFlux Config](#webflux-config)中声明的。| + +#### 1.2.5.编解码器 + +[Web MVC](integration.html#rest-message-conversion) + +`spring-web`和`spring-core`模块通过具有反应流反压的非阻塞 I/O,提供了对与高层对象之间的字节内容的序列化和反序列化的支持。以下介绍了这种支持: + +* [`Encoder`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/core/codec/Encoder.html)和[`Decoder`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/core/codec/Decoder.html)是独立于 HTTP 对内容进行编码和解码的低级契约。 + +* [HttpMessageReader](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/http/codec/HttpMessageReader.html)和[HttpMessageWriter](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/http/codec/HttpMessageWriter.html)是对 HTTP 消息内容进行编码和解码的契约。 + +* `Encoder`可以用`EncoderHttpMessageWriter`包装,以使其适合在 Web 应用程序中使用,而`Decoder`可以用`DecoderHttpMessageReader`包装。 + +* [`DataBuffer`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/core/io/buffer/DataBuffer.html)抽象了不同的字节缓冲区表示(例如,netty`ByteBuf`,`java.nio.ByteBuffer`,等等),并且是所有编解码器所工作的内容。有关此主题的更多信息,请参见“ Spring core”部分中的[数据缓冲区和编解码器](core.html#databuffers)。 + +`spring-core`模块提供`byte[]`、`ByteBuffer`、`DataBuffer`、`Resource`和 `String’编码器和解码器实现。`spring-web`模块提供了 Jackson 的 JSON、JacksonSmile、JAXB2、协议缓冲区和其他编码器和解码器,以及用于表单数据、多部分内容、服务器发送的事件和其他的仅用于 Web 的 HTTP 消息阅读器和编写器实现。 + +`ClientCodecConfigurer`和`ServerCodecConfigurer`通常用于配置和定制要在应用程序中使用的编解码器。参见关于配置[HTTP 消息编解码器](#webflux-config-message-codecs)的部分。 + +##### JacksonJSON + +当 Jackson 库存在时,都支持 JSON 和二进制 JSON([Smile](https://github.com/FasterXML/smile-format-specification))。 + +`Jackson2Decoder`的工作原理如下: + +* Jackson 的异步、非阻塞解析器用于将一个字节块流聚合到`TokenBuffer`中,每个字节块代表一个 JSON 对象。 + +* 每个`TokenBuffer`都传递给 Jackson 的`ObjectMapper`,以创建一个更高级别的对象。 + +* 当解码到单值发布者(例如`Mono`)时,存在一个`TokenBuffer`。 + +* 当解码到多值发布者(例如`Flux`)时,一旦接收到用于完全形成的对象的足够字节,每个`TokenBuffer`都会传递到`ObjectMapper`。输入内容可以是 JSON 数组,或者任何[线分隔的 JSON](https://en.wikipedia.org/wiki/JSON_streaming)格式,例如 NDJSON、JSON 行或 JSON 文本序列。 + +`Jackson2Encoder`的工作原理如下: + +* 对于单个值发布者(例如`Mono`),只需通过“ObjectMapper”序列化它。 + +* 对于使用`application/json`的多值发布者,默认情况下,使用 `flux#CollectToList()’收集这些值,然后序列化生成的集合。 + +* 对于具有流媒体类型(如 `application/x-ndjson’或`application/stream+x-jackson-smile`)的多值发布者,使用[线分隔的 JSON](https://en.wikipedia.org/wiki/JSON_streaming)格式对每个值分别进行编码、写入和刷新。其他流媒体类型可以在编码器中注册。 + +* 对于 SSE,每个事件都调用`Jackson2Encoder`,并刷新输出,以确保及时交付。 + +| |默认情况下,`Jackson2Encoder`和`Jackson2Decoder`都不支持类型为 `string’的元素。相反,默认的假设是字符串或字符串序列<br/>表示序列化的 JSON 内容,由`CharSequenceEncoder`呈现。如果<br/>需要的是从`Flux<String>`呈现一个 JSON 数组,那么使用`Flux#collectToList()`和<br/>编码一个`Mono<List<String>>`。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 表单数据 + +`FormHttpMessageReader`和`FormHttpMessageWriter`支持解码和编码 ` 应用程序/x-WWW-形式-URLencoded’内容。 + +在表单内容经常需要从多个地方访问的服务器端,“ServerWebExchange”提供了一个专用的`getFormData()`方法,该方法通过`FormHttpMessageReader`解析内容,然后缓存结果以进行重复访问。参见[Form Data](#webflux-form-data)中的[“WebHandler”API](#webflux-web-handler-api)一节。 + +一旦使用`getFormData()`,就不能再从请求主体中读取原始 RAW 内容。由于这个原因,应用程序需要始终通过`ServerWebExchange`来访问缓存的表单数据,而不是从原始请求主体读取。 + +##### 多部分 + +`MultipartHttpMessageReader`和`MultipartHttpMessageWriter`支持解码和编码“multipart/form-data”内容。反过来,`MultipartHttpMessageReader`将实际解析委托给另一个`HttpMessageReader`,然后简单地将部分收集到`MultiValueMap`中。默认情况下,使用`DefaultPartHttpMessageReader`,但这可以通过“servercodecconfigurer”进行更改。有关`DefaultPartHttpMessageReader`的更多信息,请参阅[javadoc of `DefaultPartHttpMessageReader`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/http/codec/multipart/DefaultPartHttpMessageReader.html)。 + +在可能需要从多个地方访问多部分表单内容的服务器端,`ServerWebExchange`提供了一个专用的`getMultipartData()`方法,该方法通过`MultipartHttpMessageReader`解析内容,然后缓存结果以进行重复访问。参见[Multipart Data](#webflux-multipart)中的[“WebHandler”API](#webflux-web-handler-api)部分。 + +一旦`getMultipartData()`被使用,原始的 RAW 内容就不能再从请求主体中读取。因此,应用程序必须始终使用`getMultipartData()`进行重复的、类似于地图的部分访问,或者以其他方式依赖 `SynchronosSparthPMessageReader’一次性访问`Flux<Part>`。 + +##### 限制 + +`Decoder`和`HttpMessageReader`实现了对部分或全部输入流进行缓冲,可以在内存中配置对缓冲区的最大字节数的限制。在某些情况下,发生缓冲是因为输入被聚合并表示为单个对象——例如,带有`@RequestBody byte[]`、`x-WWW-form-urlencoded’数据的控制器方法,等等。在分割输入流(例如,分隔的文本、JSON 对象流等)时,流也可以发生缓冲。对于那些流情况,限制应用于与流中的一个对象相关联的字节数。 + +要配置缓冲区大小,你可以检查给定的`Decoder`或`HttpMessageReader`是否公开了`maxInMemorySize`属性,如果是这样,爪哇doc 将提供有关默认值的详细信息。在服务器端,`ServerCodecConfigurer`提供了一个设置所有编解码器的位置,请参见[HTTP 消息编解码器](#webflux-config-message-codecs)。在客户端,所有编解码器的限制可以在[Webclient.builder](#webflux-client-builder-maxinmemorysize)中进行更改。 + +对于[多部分解析](#webflux-codecs-multipart),`maxInMemorySize`属性限制了非文件部分的大小。对于文件部件,它确定将部件写入磁盘的阈值。对于写入磁盘的文件部件,还有一个附加的“maxdiskusagepart”属性来限制每个部件的磁盘空间。还有一个`maxParts`属性来限制多部分请求中的部分总数。要在 WebFlux 中配置这三个选项,你需要提供一个预先配置的“multiparthtpMessageReader”实例到`ServerCodecConfigurer`。 + +##### 流媒体 + +[Web MVC](web.html#mvc-ann-async-http-streaming) + +当流到 HTTP 响应时(例如,`text/event-stream`,`application/x-ndjson`),定期发送数据是很重要的,这样可以更早而不是更晚地可靠地检测到断开连接的客户端。这样的发送可能是一个只有评论的、空的 SSE 事件,或者是任何其他可以有效充当心跳的“无操作”数据。 + +##### `DataBuffer` + +`DataBuffer`是 WebFlux 中字节缓冲区的表示形式。 Spring 该引用的核心部分在[数据缓冲区和编解码器](core.html#databuffers)一节中有更多关于该引用的内容。要理解的关键点是,在一些服务器(如 Netty)上,字节缓冲区是池的,引用也是计算的,并且必须在使用时释放,以避免内存泄漏。 + +WebFlux 应用程序通常不需要关注这些问题,除非它们直接使用或产生数据缓冲区,而不是依赖编解码器来转换到更高级别的对象,或者除非它们选择创建自定义编解码器。对于这种情况,请查阅[数据缓冲区和编解码器](core.html#databuffers)中的资料,特别是关于[使用 Databuffer](core.html#databuffers-using)的一节。 + +#### 1.2.6.伐木 + +[Web MVC](web.html#mvc-logging) + +Spring WebFlux 中的`DEBUG`级别日志被设计为紧凑、最小且对人类友好的。它关注的是一次又一次有用的高价值信息,而不是仅在调试特定问题时有用的其他信息。 + +`TRACE`级别日志记录通常遵循与`DEBUG`相同的原则(例如,也不应该是消防软管),但可以用于调试任何问题。此外,一些日志消息可能在`TRACE`与`DEBUG`处显示不同级别的详细信息。 + +良好的日志记录来自于使用日志的经验。如果你发现任何不符合规定的目标,请告诉我们。 + +##### 日志 ID + +在 WebFlux 中,单个请求可以在多个线程上运行,而线程 ID 对于关联属于特定请求的日志消息是没有用的。这就是为什么 WebFlux 日志消息在默认情况下使用特定于请求的 ID 作为前缀的原因。 + +在服务器端,日志 ID 存储在`ServerWebExchange`属性([`log_id_attribute’](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/server/ServerWebExchange.html#LOG_ID_ATTRIBUTE))中,而基于该 ID 的完全格式化的前缀可从“ServerWebExchange#getLogPrefix()”中获得。在`WebClient`端,日志 ID 存储在 `clientrequest’属性([`log_id_attribute’](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/reactive/function/client/ClientRequest.html#LOG_ID_ATTRIBUTE))中,而`ClientRequest#logPrefix()`中有一个完全格式化的前缀。 + +##### 敏感数据 + +[Web MVC](web.html#mvc-logging-sensitive-data) + +`DEBUG`和`TRACE`日志记录可以记录敏感信息。这就是为什么表单参数和标题在默认情况下是屏蔽的,并且你必须显式地完全启用它们的日志记录。 + +下面的示例展示了如何为服务器端请求执行此操作: + +爪哇 + +``` +@Configuration +@EnableWebFlux +class MyConfig implements WebFluxConfigurer { + + @Override + public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) { + configurer.defaultCodecs().enableLoggingRequestDetails(true); + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebFlux +class MyConfig : WebFluxConfigurer { + + override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) { + configurer.defaultCodecs().enableLoggingRequestDetails(true) + } +} +``` + +下面的示例展示了如何为客户端请求执行此操作: + +爪哇 + +``` +Consumer<ClientCodecConfigurer> consumer = configurer -> + configurer.defaultCodecs().enableLoggingRequestDetails(true); + +WebClient webClient = WebClient.builder() + .exchangeStrategies(strategies -> strategies.codecs(consumer)) + .build(); +``` + +Kotlin + +``` +val consumer: (ClientCodecConfigurer) -> Unit = { configurer -> configurer.defaultCodecs().enableLoggingRequestDetails(true) } + +val webClient = WebClient.builder() + .exchangeStrategies({ strategies -> strategies.codecs(consumer) }) + .build() +``` + +##### 附录 + +SLF4j 和 log4j2 等日志记录库提供了避免阻塞的异步记录器。尽管这些方法有其自身的缺点,比如可能会丢弃无法排队记录的消息,但它们是当前在反应性、非阻塞应用程序中使用的最佳可用选项。 + +##### 自定义编解码器 + +应用程序可以注册用于支持其他媒体类型的定制编解码器,或者默认编解码器不支持的特定行为。 + +开发人员表示的一些配置选项是在默认的编解码器上强制执行的。自定义编解码器可能希望有机会与这些首选项保持一致,比如[强制缓冲限制](#webflux-codecs-limits)或[记录敏感数据](#webflux-logging-sensitive-data)。 + +下面的示例展示了如何为客户端请求执行此操作: + +爪哇 + +``` +WebClient webClient = WebClient.builder() + .codecs(configurer -> { + CustomDecoder decoder = new CustomDecoder(); + configurer.customCodecs().registerWithDefaultConfig(decoder); + }) + .build(); +``` + +Kotlin + +``` +val webClient = WebClient.builder() + .codecs({ configurer -> + val decoder = CustomDecoder() + configurer.customCodecs().registerWithDefaultConfig(decoder) + }) + .build() +``` + +### 1.3.`DispatcherHandler` + +[Web MVC](web.html#mvc-servlet) + +Spring WebFlux,类似于 Spring MVC,是围绕前控制器模式设计的,其中中心,,提供用于请求处理的共享算法,而实际工作是通过可配置的、委托的组件来执行的。这个模型是灵活的,并支持不同的工作流程。 + +`DispatcherHandler`从 Spring 配置中发现它需要的委托组件。它本身也被设计为 Spring Bean 并且实现`ApplicationContextAware`以访问其运行的上下文。如果`DispatcherHandler`以 Bean 名`webHandler`声明,则它被[“WebHttphandlerBuilder”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/server/adapter/WebHttpHandlerBuilder.html)发现,该发现集合了一个请求处理链,如[“WebHandler”API](#webflux-web-handler-api)中所述。 + +Spring WebFlux 应用程序中的配置通常包括: + +* `DispatcherHandler`与 Bean 名称`webHandler` + +* `WebFilter`和`WebExceptionHandler`beans + +* [“DispatcherHandler”特殊豆](#webflux-special-bean-types) + +* 其他 + +将配置给`WebHttpHandlerBuilder`以构建处理链,如下例所示: + +爪哇 + +``` +ApplicationContext context = ... +HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context).build(); +``` + +Kotlin + +``` +val context: ApplicationContext = ... +val handler = WebHttpHandlerBuilder.applicationContext(context).build() +``` + +得到的`HttpHandler`可以与[server adapter](#webflux-httphandler)一起使用了。 + +#### 1.3.1.特殊类型 Bean + +[Web MVC](web.html#mvc-servlet-special-bean-types) + +`DispatcherHandler`将委托给特殊的 bean 来处理请求并呈现适当的响应。我们所说的“特殊 bean”是指实现 WebFlux 框架契约的 Spring-managed`Object`实例。这些通常带有内置契约,但你可以自定义它们的属性,扩展它们或替换它们。 + +下表列出了`DispatcherHandler`检测到的特殊 bean。请注意,在较低的级别上还检测到一些其他 bean(参见 Web 处理程序 API 中的[Special bean types](#webflux-web-handler-api-special-beans))。 + +| Bean type |解释| +|----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `HandlerMapping` |将请求映射到处理程序。该映射是基于一些标准的,其中<br/>的细节通过`HandlerMapping`实现—带注释的控制器,简单的<br/>URL 模式映射,以及其他方法而变化。<br/>`HandlerMapping`主要的`RequestMappingHandlerMapping`实现对于<@requestmapping` 带注释的方法,对于功能端点routes=”673",和`SimpleUrlHandlerMapping`用于显式注册 URI 路径模式<br/>和`WebHandler`实例。| +| `HandlerAdapter` |帮助`DispatcherHandler`调用映射到请求的处理程序,而不管<br/>实际调用处理程序的方式如何。例如,调用带注释的控制器<br/>需要解析注释。a`HandlerAdapter`的主要目的是保护“dispatcherhandler”不受这些细节的影响。| +|`HandlerResultHandler`|处理来自处理程序调用的结果并完成响应。<br/>参见[Result 处理](#webflux-resulthandling)。| + +#### 1.3.2.WebFlux 配置 + +[Web MVC](web.html#mvc-servlet-config) + +应用程序可以声明处理请求所需的基础设施 bean(在[Web Handler API](#webflux-web-handler-api-special-beans)和[DispatcherHandler’](#webflux-special-bean-types)下列出)。然而,在大多数情况下,[WebFlux Config](#webflux-config)是最好的起点。它声明所需的 bean,并提供一个更高级的配置回调 API 来定制它。 + +| |Spring 启动依赖于 WebFlux 配置来配置 Spring WebFlux,并且还提供了<br/>许多额外的方便选项。| +|---|-------------------------------------------------------------------------------------------------------------------------| + +#### 1.3.3.处理 + +[Web MVC](web.html#mvc-servlet-sequence) + +`DispatcherHandler`按以下方式处理请求: + +* 每个`HandlerMapping`都被要求找到一个匹配的处理程序,并使用第一个匹配。 + +* 如果找到了一个处理程序,则通过一个适当的`HandlerAdapter`运行该处理程序,该处理程序将执行时的返回值公开为`HandlerResult`。 + +* 将`HandlerResult`赋予适当的`HandlerResultHandler`,以通过直接写入响应或通过使用视图来呈现来完成处理。 + +#### 1.3.4.结果处理 + +通过`HandlerAdapter`调用处理程序的返回值被包装为`HandlerResult`,以及一些附加的上下文,并传递给声称支持它的第一个 `handlerResultHandler’。下表显示了可用的“HandlerResultHandler”实现,所有这些实现都在[WebFlux Config](#webflux-config)中声明: + +| Result Handler Type |返回值| Default Order | +|-----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------| +|`ResponseEntityResultHandler`|`ResponseEntity`,通常来自`@Controller`实例。| 0 | +|`ServerResponseResultHandler`|`ServerResponse`,通常来自功能端点。| 0 | +| `ResponseBodyResultHandler` |处理来自`@ResponseBody`方法或`@RestController`类的返回值。| 100 | +|`ViewResolutionResultHandler`|`CharSequence`,[`View`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/reactive/result/view/View.html),[Model](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/ui/Model.html),`Map`,[Rendering](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/reactive/result/view/Rendering.html),<br/>或任何其他`Object`都被视为模型属性。<br/><br/>参见[View Resolution](#webflux-viewresolution)。|`Integer.MAX_VALUE`| + +#### 1.3.5.例外 + +[Web MVC](web.html#mvc-exceptionhandlers) + +从`HandlerAdapter`返回的`HandlerResult`可以基于某些特定于处理程序的机制公开用于错误处理的函数。如果出现以下情况,则调用此错误函数: + +* 处理程序(例如,`@Controller`)调用失败。 + +* 通过`HandlerResultHandler`处理处理程序返回值失败。 + +错误函数可以更改响应(例如,到错误状态),只要在从处理程序返回的反应类型产生任何数据项之前发生错误信号。 + +这就是`@Controller`类中的`@ExceptionHandler`方法的支持方式。相比之下,对 Spring MVC 中相同内容的支持是建立在`HandlerExceptionResolver`上的。这一点一般不会有什么影响。但是,请记住,在 WebFlux 中,不能使用“@Controlleradvice”来处理在选择处理程序之前发生的异常。 + +另请参见“注释控制器”部分中的[管理异常](#webflux-ann-controller-exceptions)或 WebHandler API 部分中的[Exceptions](#webflux-exception-handler)。 + +#### 1.3.6.视图分辨率 + +[Web MVC](web.html#mvc-viewresolver) + +视图分辨率允许使用 HTML 模板和模型在浏览器上进行呈现,而无需将你绑定到特定的视图技术。在 Spring WebFlux 中,通过专用的[HandlerResultHandler](#webflux-resulthandling)支持视图解析,该实例使用 `ViewResolver’实例将字符串(代表逻辑视图名称)映射到`View`实例。然后使用`View`来呈现响应。 + +##### Handling + +[Web MVC](web.html#mvc-handling) + +传递到`ViewResolutionResultHandler`中的`HandlerResult`包含来自处理程序的返回值和包含在请求处理过程中添加的属性的模型。返回值被处理为以下内容之一: + +* `String`,`CharSequence`:要通过配置的`ViewResolver`实现的列表解析为`View`的逻辑视图名称。 + +* `void`:根据请求路径选择一个默认的视图名称,减去前导和后导斜杠,并将其解析为`View`。当未提供视图名称(例如,返回了 model 属性)或异步返回值(例如,`Mono`完全为空)时,也会发生相同的情况。 + +* [Rendering](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/reactive/result/view/Rendering.html):视图解析场景的 API。探索你的 IDE 中的代码补全选项。 + +* `Model`,`Map`:要为请求添加到模型中的额外模型属性。 + +* 任何其他:任何其他返回值(简单类型除外,由[Beanutils#IsSimpleProperty](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-)确定)被视为要添加到模型中的模型属性。属性名是通过使用[conventions](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/core/Conventions.html)从类名派生出来的,除非存在处理程序方法`@ModelAttribute`注释。 + +该模型可以包含异步的、反应性的类型(例如,来自 Reactor 或 RX爪哇)。在呈现之前,`AbstractView`将这些模型属性解析为具体的值并更新模型。单值活性类型被解析为单值或无值(如果为空),而多值活性类型(例如,`Flux<T>`)被收集并解析为`List<T>`。 + +要配置视图分辨率就像在 Spring 配置中添加`ViewResolutionResultHandler` Bean 一样简单。[WebFlux Config](#webflux-config-view-resolvers)为视图分辨率提供了专用的配置 API。 + +有关与 Spring WebFlux 集成的视图技术的更多信息,请参见[查看技术](#webflux-view)。 + +##### 重定向 + +[Web MVC](web.html#mvc-redirecting-redirect-prefix) + +视图名称中的特殊`redirect:`前缀允许你执行重定向。“urlbasedViewResolver”(和子类)将此视为需要重定向的指令。视图名称的其余部分是重定向 URL。 + +净效果与控制器返回`RedirectView`或 `rendering.redirectto(“abc”).build()` 相同,但现在控制器本身可以根据逻辑视图名称进行操作。一个视图名称,如“redirect:/some/resource”,是相对于当前应用程序的,而一个视图名称,如“redirect:https://example.com/Artunary/path”,则会重定向到一个绝对的 URL。 + +##### 内容协商 + +[Web MVC](web.html#mvc-multiple-representations) + +`ViewResolutionResultHandler`支持内容协商。它将请求媒体类型与每个选定的`View`所支持的媒体类型进行比较。使用了支持所请求的媒体类型的第一个`View`。 + +为了支持 JSON 和 XML 等媒体类型, Spring WebFlux 提供了 `HttpMessageWriterView’,这是一种特殊的`View`,它通过[HttpMessageWriter](#webflux-codecs)呈现。通常,你会通过[WebFlux 配置](#webflux-config-view-resolvers)将这些视图配置为默认视图。如果默认视图匹配所请求的媒体类型,则始终选择并使用它们。 + +### 1.4.带注释的控制器 + +[Web MVC](web.html#mvc-controller) + +Spring WebFlux 提供了一种基于注释的编程模型,其中`@Controller`和 `@RESTController’组件使用注释来表示请求映射、请求输入、处理异常等等。带注释的控制器具有灵活的方法签名,不需要扩展基类,也不需要实现特定的接口。 + +下面的清单展示了一个基本示例: + +Java + +``` +@RestController +public class HelloController { + + @GetMapping("/hello") + public String handle() { + return "Hello WebFlux"; + } +} +``` + +Kotlin + +``` +@RestController +class HelloController { + + @GetMapping("/hello") + fun handle() = "Hello WebFlux" +} +``` + +在前面的示例中,该方法返回要写入响应主体的`String`。 + +#### 1.4.1.`@Controller` + +[Web MVC](web.html#mvc-ann-controller) + +你可以使用标准的 Spring Bean 定义来定义控制器 bean。该原型允许自动检测并与 Spring 用于检测 Classpath 中的类的通用支持保持一致,并为它们自动注册 Bean 定义。它还充当带注释的类的原型,指示其作为 Web 组件的角色。 + +要启用对此类`@Controller`bean 的自动检测,可以将组件扫描添加到 Java 配置中,如下例所示: + +Java + +``` +@Configuration +@ComponentScan("org.example.web") (1) +public class WebConfig { + + // ... +} +``` + +|**1**|扫描`org.example.web`包。| +|-----|-----------------------------------| + +Kotlin + +``` +@Configuration +@ComponentScan("org.example.web") (1) +class WebConfig { + + // ... +} +``` + +|**1**|扫描`org.example.web`包。| +|-----|-----------------------------------| + +`@RestController`是一个[组合注释](core.html#beans-meta-annotations),它本身用`@Controller`和`@ResponseBody`进行了元注释,表示一个控制器,其每个方法都继承了类型级`@ResponseBody`注释,因此,它直接写到响应主体与视图解析之间,并使用 HTML 模板进行呈现。 + +#### 1.4.2.请求映射 + +[Web MVC](web.html#mvc-ann-requestmapping) + +`@RequestMapping`注释用于将请求映射到控制器方法。它具有各种属性,可以通过 URL、HTTP 方法、请求参数、标头和媒体类型进行匹配。你可以在类级别上使用它来表示共享映射,或者在方法级别上使用它来缩小到特定的端点映射。 + +还有`@RequestMapping`的特定于 HTTP 方法的快捷方式变体: + +* `@GetMapping` + +* `@PostMapping` + +* `@PutMapping` + +* `@DeleteMapping` + +* `@PatchMapping` + +前面的注释是[自定义注释](#webflux-ann-requestmapping-composed),之所以提供这些注释,是因为,可以说,大多数控制器方法都应该映射到特定的 HTTP 方法,而不是使用`@RequestMapping`,后者在默认情况下与所有 HTTP 方法匹配。同时,在类级别上仍然需要“@requestmapping”来表示共享映射。 + +下面的示例使用类型和方法级别映射: + +Java + +``` +@RestController +@RequestMapping("/persons") +class PersonController { + + @GetMapping("/{id}") + public Person getPerson(@PathVariable Long id) { + // ... + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public void add(@RequestBody Person person) { + // ... + } +} +``` + +Kotlin + +``` +@RestController +@RequestMapping("/persons") +class PersonController { + + @GetMapping("/{id}") + fun getPerson(@PathVariable id: Long): Person { + // ... + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + fun add(@RequestBody person: Person) { + // ... + } +} +``` + +##### URI 模式 + +[Web MVC](web.html#mvc-ann-requestmapping-uri-templates) + +你可以使用 GLOB 模式和通配符来映射请求: + +| Pattern | Description |例子| +|---------------|-------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `?` | Matches one character |`"/pages/t?st.html"`匹配`"/pages/test.html"`和`"/pages/t3st.html"`| +| `*` | Matches zero or more characters within a path segment |`"/resources/*.png"`匹配`"/resources/file.png"`<br/><br/>“/projects/*/versions”` 匹配`"/projects/spring/versions"`但不匹配`"/projects/spring/boot/versions"`| +| `**` | Matches zero or more path segments until the end of the path |`"/resources/**"`匹配`"/resources/file.png"`和`"/resources/images/file.png"`<br/><br/>“/resources/**/file.png”` 无效,因为`**`只允许在路径的末尾使用。| +| `{name}` | Matches a path segment and captures it as a variable named "name" |`"/projects/{project}/versions"`匹配`"/projects/spring/versions"`并捕获`project=spring`| +|`{name:[a-z]+}`| Matches the regexp `"[a-z]+"` as a path variable named "name" |`"/projects/{project:[a-z]+}/versions"`匹配`"/projects/spring/versions"`但不匹配`"/projects/spring1/versions"`| +| `{*path}` |Matches zero or more path segments until the end of the path and captures it as a variable named "path"|`"/resources/{*file}"`匹配`"/resources/images/file.png"`并捕获`file=/images/file.png`| + +可以使用`@PathVariable`访问捕获的 URI 变量,如下例所示: + +Java + +``` +@GetMapping("/owners/{ownerId}/pets/{petId}") +public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) { + // ... +} +``` + +Kotlin + +``` +@GetMapping("/owners/{ownerId}/pets/{petId}") +fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet { + // ... +} +``` + +可以在类和方法级别声明 URI 变量,如下例所示: + +Java + +``` +@Controller +@RequestMapping("/owners/{ownerId}") (1) +public class OwnerController { + + @GetMapping("/pets/{petId}") (2) + public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) { + // ... + } +} +``` + +|**1**|类级 URI 映射。| +|-----|-------------------------| +|**2**|方法级别的 URI 映射。| + +Kotlin + +``` +@Controller +@RequestMapping("/owners/{ownerId}") (1) +class OwnerController { + + @GetMapping("/pets/{petId}") (2) + fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet { + // ... + } +} +``` + +|**1**|类级 URI 映射。| +|-----|-------------------------| +|**2**|方法级别的 URI 映射。| + +URI 变量将自动转换为适当的类型,或者生成`TypeMismatchException`。默认情况下支持简单类型(`INT’,`long`,`Date`,等等),你可以注册对任何其他数据类型的支持。见[类型转换](#webflux-ann-typeconversion)和[`DataBinder`](#webflux-ann-initbinder)。 + +URI 变量可以显式地命名(例如,`@PathVariable("customId")`),但是如果名称相同,并且你可以使用调试信息或 Java8 上的`-parameters`编译器标志来编译代码,则可以忽略这些细节。 + +语法`{*varName}`声明一个 URI 变量,该变量匹配零个或多个剩余的路径段。例如,`/resources/{*path}`匹配`/resources/`下的所有文件,而 `“path”` 变量捕获`/resources`下的完整路径。 + +语法`{varName:regex}`声明一个 URI 变量,其正则表达式的语法为:`{varName:regex}`。例如,给定一个`/spring-web-3.0.5.jar`的 URL,下面的方法会提取名称、版本和文件扩展名: + +Java + +``` +@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}") +public void handle(@PathVariable String version, @PathVariable String ext) { + // ... +} +``` + +Kotlin + +``` +@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}") +fun handle(@PathVariable version: String, @PathVariable ext: String) { + // ... +} +``` + +URI 路径模式还可以嵌入`${…​}`占位符,这些占位符在启动时通过`PropertyPlaceHolderConfigurer`针对本地、系统、环境和其他属性源解析。例如,你可以使用它来基于某些外部配置参数化一个基本 URL。 + +| |Spring WebFlux 将`PathPattern`和`PathPatternParser`用于 URI 路径匹配支持。<br/>这两个类都位于`spring-web`中,并且明确地设计用于在 Web 应用程序中使用 http url<br/>路径,其中在运行时匹配了大量的 URI 路径模式。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +Spring WebFlux 不支持后缀模式匹配——不像 Spring MVC,其中像`/person`这样的映射也匹配到`/person.*`。对于基于 URL 的内容协商,如果需要,我们建议使用一个查询参数,该参数更简单,更明确,并且不易受到基于 URL 路径的攻击。 + +##### 模式比较 + +[Web MVC](web.html#mvc-ann-requestmapping-pattern-comparison) + +当多个模式匹配一个 URL 时,必须对它们进行比较以找到最佳匹配。这是用`PathPattern.SPECIFICITY_COMPARATOR`完成的,它寻找更具体的模式。 + +对于每个模式,都会根据 URI 变量和通配符的数量计算得分,其中 URI 变量的得分低于通配符。总分较低的模式获胜。如果两种模式得分相同,则选择较长的模式。 + +包罗万象的模式(例如,`**`,`{*varName}`)被排除在评分之外,并且总是排在最后。如果两种模式都是包罗万象的,则选择较长的模式。 + +##### 可消费媒体类型 + +[Web MVC](web.html#mvc-ann-requestmapping-consumes) + +你可以基于请求的`Content-Type`缩小请求映射,如下例所示: + +Java + +``` +@PostMapping(path = "/pets", consumes = "application/json") +public void addPet(@RequestBody Pet pet) { + // ... +} +``` + +Kotlin + +``` +@PostMapping("/pets", consumes = ["application/json"]) +fun addPet(@RequestBody pet: Pet) { + // ... +} +``` + +Consumes 属性还支持否定表达式——例如,`!text/plain`表示除`text/plain`以外的任何内容类型。 + +你可以在类级别声明一个共享的`consumes`属性。然而,与大多数其他请求映射属性不同的是,当在类级别使用时,方法级别`consumes`属性覆盖而不是扩展类级声明。 + +| |`MediaType`为常用的媒体类型提供常量——例如,`application_json_value` 和`APPLICATION_XML_VALUE`。| +|---|--------------------------------------------------------------------------------------------------------------------------------| + +##### 可生产媒体类型 + +[Web MVC](web.html#mvc-ann-requestmapping-produces) + +你可以基于`Accept`请求头和控制器方法产生的内容类型列表来缩小请求映射的范围,如下例所示: + +Java + +``` +@GetMapping(path = "/pets/{petId}", produces = "application/json") +@ResponseBody +public Pet getPet(@PathVariable String petId) { + // ... +} +``` + +Kotlin + +``` +@GetMapping("/pets/{petId}", produces = ["application/json"]) +@ResponseBody +fun getPet(@PathVariable String petId): Pet { + // ... +} +``` + +媒体类型可以指定字符集。支持否定表达式——例如,`!text/plain’表示除`text/plain`以外的任何内容类型。 + +你可以在类级别声明一个共享的`produces`属性。然而,与大多数其他请求映射属性不同的是,当在类级别使用时,方法级别`produces`属性覆盖而不是扩展类级别声明。 + +| |`MediaType`为常用的媒体类型提供常量,例如 `application_json_value`,`APPLICATION_XML_VALUE`。| +|---|---------------------------------------------------------------------------------------------------------------------| + +##### 参数和标题 + +[Web MVC](web.html#mvc-ann-requestmapping-params-and-headers) + +你可以根据查询参数条件缩小请求映射的范围。你可以测试一个查询参数的存在、它的不存在(“!MyParam”)或一个特定值(“MyParam=MyValue”)。下面的示例测试具有值的参数: + +Java + +``` +@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") (1) +public void findPet(@PathVariable String petId) { + // ... +} +``` + +|**1**|检查`myParam`等于`myValue`。| +|-----|--------------------------------------| + +Kotlin + +``` +@GetMapping("/pets/{petId}", params = ["myParam=myValue"]) (1) +fun findPet(@PathVariable petId: String) { + // ... +} +``` + +|**1**|检查`myParam`等于`myValue`。| +|-----|--------------------------------------| + +你也可以在请求头条件中使用相同的方法,如下面的示例所示: + +Java + +``` +@GetMapping(path = "/pets", headers = "myHeader=myValue") (1) +public void findPet(@PathVariable String petId) { + // ... +} +``` + +|**1**|检查`myHeader`等于`myValue`。| +|-----|---------------------------------------| + +Kotlin + +``` +@GetMapping("/pets", headers = ["myHeader=myValue"]) (1) +fun findPet(@PathVariable petId: String) { + // ... +} +``` + +|**1**|检查`myHeader`等于`myValue`。| +|-----|---------------------------------------| + +##### HTTP 头,选项 + +[Web MVC](web.html#mvc-ann-requestmapping-head-options) + +`@GetMapping`和`@RequestMapping(method=HttpMethod.GET)`透明地支持用于请求映射目的的 HTTP head。控制器的方法不需要改变。在`HttpHandler`服务器适配器中应用的响应包装器确保将`Content-Length`头设置为不实际写入响应的字节数。 + +默认情况下,HTTP 选项的处理方法是将`Allow`响应头设置为具有匹配的 URL 模式的所有`@RequestMapping`方法中列出的 HTTP 方法列表。 + +对于不带 HTTP 方法声明的`@RequestMapping`,`Allow`头将设置为 `get,head,post,put,patch,delete,options’。控制器方法应该总是声明受支持的 HTTP 方法(例如,通过使用 HTTP 方法特定的变体—,,以及其他)。 + +你可以显式地将`@RequestMapping`方法映射到 HTTPHead 和 HTTPOptions,但在常见的情况下,这是不必要的。 + +##### 自定义注释 + +[Web MVC](web.html#mvc-ann-requestmapping-composed) + +Spring WebFlux 支持使用[组合注释](core.html#beans-meta-annotations)进行请求映射。这些注释本身是用“@requestmapping”进行元注释的,其组成是为了重新声明`@RequestMapping`属性的一个子集(或全部),具有更窄、更具体的目的。 + +`@GetMapping`,`@PostMapping`,`@PutMapping`,`@DeleteMapping`,和`@PatchMapping`是合成注释的例子。提供它们是因为,可以说,大多数控制器方法应该映射到特定的 HTTP 方法,而不是使用`@RequestMapping`,后者默认情况下与所有 HTTP 方法匹配。如果你需要一个组合注释的示例,请查看这些注释是如何声明的。 + +Spring WebFlux 还支持具有自定义请求匹配逻辑的自定义请求映射属性。这是一个更高级的选项,它需要子类“requestmappinghandlermapping”并覆盖`getCustomMethodCondition`方法,在该方法中,你可以检查自定义属性并返回你自己的`RequestCondition`。 + +##### 显式注册 + +[Web MVC](web.html#mvc-ann-requestmapping-registration) + +你可以以编程方式注册处理程序方法,这些方法可以用于动态注册或高级情况,例如同一处理程序在不同 URL 下的不同实例。下面的示例展示了如何做到这一点: + +Java + +``` +@Configuration +public class MyConfig { + + @Autowired + public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler) (1) + throws NoSuchMethodException { + + RequestMappingInfo info = RequestMappingInfo + .paths("/user/{id}").methods(RequestMethod.GET).build(); (2) + + Method method = UserHandler.class.getMethod("getUser", Long.class); (3) + + mapping.registerMapping(info, handler, method); (4) + } + +} +``` + +|**1**|为控制器注入目标处理程序和处理程序映射。| +|-----|---------------------------------------------------------------| +|**2**|准备请求映射元数据。| +|**3**|获取 handler 方法。| +|**4**|添加注册。| + +Kotlin + +``` +@Configuration +class MyConfig { + + @Autowired + fun setHandlerMapping(mapping: RequestMappingHandlerMapping, handler: UserHandler) { (1) + + val info = RequestMappingInfo.paths("/user/{id}").methods(RequestMethod.GET).build() (2) + + val method = UserHandler::class.java.getMethod("getUser", Long::class.java) (3) + + mapping.registerMapping(info, handler, method) (4) + } +} +``` + +|**1**|为控制器注入目标处理程序和处理程序映射。| +|-----|---------------------------------------------------------------| +|**2**|准备请求映射元数据。| +|**3**|获取 handler 方法。| +|**4**|添加注册。| + +#### 1.4.3.处理程序方法 + +[Web MVC](web.html#mvc-ann-methods) + +`@RequestMapping`处理程序方法具有灵活的签名,并且可以从受支持的控制器方法参数和返回值的范围中进行选择。 + +##### 方法参数 + +[Web MVC](web.html#mvc-ann-arguments) + +下表显示了受支持的控制器方法参数。 + +对于需要解析阻塞 I/O(例如,读取请求主体)的参数,支持反应性类型(Reactor,RxJava,)。这一点在“描述”栏中进行了标记。在不需要阻塞的参数上,反应式类型是不被期望的。 + +JDK1.8 的`java.util.Optional`作为方法参数被支持,并与具有`required`属性(例如,`@RequestParam`,`@RequestHeader`,以及其他)的注释结合在一起,并且等价于`required=false`。 + +| Controller method argument |说明| +|---------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `ServerWebExchange` |访问完整的`ServerWebExchange`—用于 HTTP 请求和响应的容器、<br/>请求和会话属性、`checkNotModified`方法等。| +| `ServerHttpRequest`, `ServerHttpResponse` |访问 HTTP 请求或响应。| +| `WebSession` |访问会话。除非添加了属性<br/>,否则不强制开始新的会话。支持反应性类型。| +| `java.security.Principal` |当前经过身份验证的用户——如果已知的话,可能是特定的`Principal`实现类。<br/>支持反应性类型。| +| `org.springframework.http.HttpMethod` |请求的 HTTP 方法。| +| `java.util.Locale` |当前的请求区域设置,由可用的最特定的`LocaleResolver`确定—在<br/>效果中,配置的`LocaleResolver`/`localeContextResolver’。| +| `java.util.TimeZone` + `java.time.ZoneId` |与当前请求相关联的时区,由`LocaleContextResolver`确定。| +| `@PathVariable` |用于访问 URI 模板变量。见[URI Patterns](#webflux-ann-requestmapping-uri-templates)。| +| `@MatrixVariable` |用于访问 URI 路径段中的名称-值对。见[矩阵变量](#webflux-ann-matrix-variables)。| +| `@RequestParam` |用于访问 Servlet 请求参数。参数值被转换为声明的<br/>方法参数类型。参见[`@RequestParam`](#webflux-ann-requestparam)。<br/><br/>注意,`@RequestParam`的使用是可选的——例如,用于设置其属性。<br/>参见本表后面的“任何其他参数”。| +| `@RequestHeader` |用于访问请求头。标头值被转换为声明的方法参数<br/>type。见[@requestheader](#webflux-ann-requestheader)。| +| `@CookieValue` |获取 cookies 的权限。cookie 值被转换为声明的方法参数类型。<br/>参见[`@CookieValue`](#webflux-ann-cookievalue)。| +| `@RequestBody` |用于访问 HTTP 请求主体。通过使用`HttpMessageReader`实例,主体内容被转换为声明的方法<br/>参数类型。支持反应性类型。<br/>参见[`@RequestBody`](#webflux-ann-requestbody)。| +| `HttpEntity<B>` |用于访问请求头和主体。主体使用`HttpMessageReader`实例进行转换。<br/>支持反应式类型。见[`HttpEntity`](#webflux-ann-httpentity)。| +| `@RequestPart` |用于访问`multipart/form-data`请求中的部件。支持反应类型。<br/>参见[多部分内容](#webflux-multipart-forms)和[Multipart Data](#webflux-multipart)。| +|`java.util.Map`, `org.springframework.ui.Model`, and `org.springframework.ui.ModelMap`.|用于访问 HTML 控制器中使用的模型,并以<br/>视图呈现的一部分的形式暴露于模板中。| +| `@ModelAttribute` |用于访问模型中的现有属性(如果不存在则实例化),并应用<br/>数据绑定和验证。参见[@ModelAttribute](#webflux-ann-modelattrib-method-args)以及<br/>as[`Model`](#webflux-ann-modelattrib-methods)和[`DataBinder`](#webflux-ann-initbinder)。<br/><br/>注意,`@ModelAttribute`的使用是可选的,例如,用于设置其属性。<br/>参见本表后面的“任何其他参数”。| +| `Errors`, `BindingResult` |用于访问来自命令对象的验证和数据绑定的错误,即“@ModelAttribute”参数。一个`Errors`或`BindingResult`参数必须在验证方法参数之后立即声明<br/>。| +| `SessionStatus` + class-level `@SessionAttributes` |用于标记表单处理完成,这将触发通过类级`@SessionAttributes`注释声明的会话属性<br/>的清理。<br/>有关更多详细信息,请参见[@sessionAttributions’](#webflux-ann-sessionattributes)。| +| `UriComponentsBuilder` |用于准备相对于当前请求的主机、端口、方案和<br/>上下文路径的 URL。见[URI Links](#webflux-uri-building)。| +| `@SessionAttribute` |用于访问任何会话属性——与存储在会话<br/>中的模型属性形成对比,后者是类级别`@SessionAttributes`声明的结果。有关更多详细信息,请参见[@sessionAttribute](#webflux-ann-sessionattribute)。| +| `@RequestAttribute` |用于访问请求属性。有关更多详细信息,请参见[@requestAttribute](#webflux-ann-requestattrib)。| +| Any other argument |如果方法参数与上述任何一个参数不匹配,则默认情况下将其解析为<br/>a`@RequestParam`,如果它是一个简单类型,则通过[Beanutils#IsSimpleProperty](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-),<br/>或作为`@ModelAttribute`来确定,否则。| + +##### 返回值 + +[Web MVC](web.html#mvc-ann-return-types) + +下表显示了受支持的控制器方法的返回值。请注意,对于所有返回值,通常都支持来自诸如 reactor、rxjava、[or other](#webflux-reactive-libraries)等库的反应类型。 + +| Controller method return value |说明| +|------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `@ResponseBody` |返回值通过`HttpMessageWriter`实例进行编码并写入响应。<br/>参见[`@ResponseBody`](#webflux-ann-responsebody)。| +| `HttpEntity<B>`, `ResponseEntity<B>` |返回值指定完整的响应,包括 HTTP 头,并且通过`ServerHttpResponse`实例对主体进行编码<br/>,并将其写入响应。<br/>参见[` 负责实体’](#webflux-ann-responseentity)。| +| `HttpHeaders` |返回带有标题而没有正文的响应。| +| `String` |要用`ViewResolver`实例解析的视图名称,并与隐式<br/>模型一起使用——通过命令对象和`@ModelAttribute`方法确定。处理程序<br/>方法还可以通过声明一个`Model`参数<br/>(描述[earlier](#webflux-viewresolution-handling))以编程方式丰富模型。| +| `View` |一个`View`实例用于与隐式模型一起进行渲染——通过命令对象和`@ModelAttribute`方法确定<br/>。处理程序方法还可以通过声明`Model`参数<br/>(描述[earlier](#webflux-viewresolution-handling))以编程方式丰富模型。| +| `java.util.Map`, `org.springframework.ui.Model` |要添加到隐式模型中的属性,并根据请求路径隐式地确定视图名称<br/>。| +| `@ModelAttribute` |要添加到模型中的一个属性,其视图名称是基于请求路径隐式确定的<br/>。<br/><br/>注意,`@ModelAttribute`是可选的。请参阅下面的<br/>中的“任何其他返回值”。| +| `Rendering` |用于模型和视图呈现场景的 API。| +| `void` |如果方法具有`void`,可能是异步的(例如,`Mono<Void>`),返回类型(或`null`返回<br/>值),则认为该方法已完全处理了响应,如果它还具有`ServerHttpResponse`,<br/>`ServerWebExchange`参数,或`@ResponseStatus`注释。同样也是真的<br/>如果控制器进行了正的 ETag 或`lastModified`时间戳检查。<br/>//todo:详见[Controllers](#webflux-caching-etag-lastmodified)。<br/>如果上述各项都不是真的,`void`返回类型还可以表示<br/>REST 控制器的“无响应主体”或 HTML 控制器的默认视图名称选择。| +|`Flux<ServerSentEvent>`, `Observable<ServerSentEvent>`, or other reactive type|发出服务器发送的事件。当只需要<br/>写入数据时,`ServerSentEvent`包装器可以省略(但是,`text/event-stream`必须通过`produces`属性在映射<br/>中进行请求或声明)。| +| Any other return value |如果一个返回值与上述任一项不匹配,则默认情况下,它被视为一个视图<br/>名称,如果它是`String`或`void`(应用默认的视图名称选择),或者作为一个要添加到模型中的模型<br/>属性,除非是简单的类型,如由[Beanutils#IsSimpleProperty](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-)确定的,否则<br/>在这种情况下仍未解决。| + +##### Type Conversion + +[Web MVC](web.html#mvc-ann-typeconversion) + +一些表示基于字符串的请求输入的带注释的控制器方法参数(例如,`@requestParam`,`@RequestHeader`,`@PathVariable`,`@MatrixVariable`,和`@CookieValue`)可以要求类型转换,如果该参数被声明为`String`以外的内容。 + +对于这样的情况,类型转换是基于配置的转换器自动应用的。默认情况下,支持简单类型(如`int`、`long`、`Date`等)。类型转换可以通过`WebDataBinder`(参见[`DataBinder`](#webflux-ann-initbinder))或通过用`FormattingConversionService`注册 `formatter’(参见[Spring Field Formatting](core.html#format))进行定制。 + +类型转换中的一个实际问题是空字符串源值的处理。如果由于类型转换而使该值变为`null`,则将其视为缺失。这可能是`Long`、`UUID`和其他目标类型的情况。如果要允许注入`null`,可以在参数注释上使用`required`标志,或者将参数声明为`@Nullable`。 + +##### 矩阵变量 + +[Web MVC](web.html#mvc-ann-matrix-variables) + +[RFC 3986](https://tools.ietf.org/html/rfc3986#section-3.3)讨论路径段中的名称-值对。在 Spring WebFlux 中,我们将那些称为基于 Tim Berners-Lee 的[“old post”](https://www.w3.org/DesignIssues/MatrixURIs.html)的“矩阵变量”,但它们也可以称为 URI 路径参数。 + +矩阵变量可以出现在任何路径段中,每个变量用分号分隔,多个值用逗号分隔——例如,`"/cars;color=red,green;year=2012"`。还可以通过重复的变量名称指定多个值——例如,`“color=red;color=green;color=blue”`。 + +Spring 与 MVC 不同,在 WebFlux 中,URL 中是否存在矩阵变量并不影响请求映射。换句话说,你不需要使用 URI 变量来屏蔽变量内容。也就是说,如果你想从控制器方法访问矩阵变量,则需要在期望矩阵变量的路径段中添加一个 URI 变量。下面的示例展示了如何做到这一点: + +Java + +``` +// GET /pets/42;q=11;r=22 + +@GetMapping("/pets/{petId}") +public void findPet(@PathVariable String petId, @MatrixVariable int q) { + + // petId == 42 + // q == 11 +} +``` + +Kotlin + +``` +// GET /pets/42;q=11;r=22 + +@GetMapping("/pets/{petId}") +fun findPet(@PathVariable petId: String, @MatrixVariable q: Int) { + + // petId == 42 + // q == 11 +} +``` + +鉴于所有的路径段都可以包含矩阵变量,因此有时你可能需要消除矩阵变量预期在哪个路径变量中的歧义,如下例所示: + +Java + +``` +// GET /owners/42;q=11/pets/21;q=22 + +@GetMapping("/owners/{ownerId}/pets/{petId}") +public void findPet( + @MatrixVariable(name="q", pathVar="ownerId") int q1, + @MatrixVariable(name="q", pathVar="petId") int q2) { + + // q1 == 11 + // q2 == 22 +} +``` + +Kotlin + +``` +@GetMapping("/owners/{ownerId}/pets/{petId}") +fun findPet( + @MatrixVariable(name = "q", pathVar = "ownerId") q1: Int, + @MatrixVariable(name = "q", pathVar = "petId") q2: Int) { + + // q1 == 11 + // q2 == 22 +} +``` + +你可以定义一个可以定义为可选的矩阵变量,并指定一个默认值,如下例所示: + +Java + +``` +// GET /pets/42 + +@GetMapping("/pets/{petId}") +public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) { + + // q == 1 +} +``` + +Kotlin + +``` +// GET /pets/42 + +@GetMapping("/pets/{petId}") +fun findPet(@MatrixVariable(required = false, defaultValue = "1") q: Int) { + + // q == 1 +} +``` + +要获取所有矩阵变量,请使用`MultiValueMap`,如下例所示: + +Java + +``` +// GET /owners/42;q=11;r=12/pets/21;q=22;s=23 + +@GetMapping("/owners/{ownerId}/pets/{petId}") +public void findPet( + @MatrixVariable MultiValueMap<String, String> matrixVars, + @MatrixVariable(pathVar="petId") MultiValueMap<String, String> petMatrixVars) { + + // matrixVars: ["q" : [11,22], "r" : 12, "s" : 23] + // petMatrixVars: ["q" : 22, "s" : 23] +} +``` + +Kotlin + +``` +// GET /owners/42;q=11;r=12/pets/21;q=22;s=23 + +@GetMapping("/owners/{ownerId}/pets/{petId}") +fun findPet( + @MatrixVariable matrixVars: MultiValueMap<String, String>, + @MatrixVariable(pathVar="petId") petMatrixVars: MultiValueMap<String, String>) { + + // matrixVars: ["q" : [11,22], "r" : 12, "s" : 23] + // petMatrixVars: ["q" : 22, "s" : 23] +} +``` + +##### `@RequestParam` + +[Web MVC](web.html#mvc-ann-requestparam) + +可以使用`@RequestParam`注释将查询参数绑定到控制器中的方法参数。下面的代码片段显示了该用法: + +Java + +``` +@Controller +@RequestMapping("/pets") +public class EditPetForm { + + // ... + + @GetMapping + public String setupForm(@RequestParam("petId") int petId, Model model) { (1) + Pet pet = this.clinic.loadPet(petId); + model.addAttribute("pet", pet); + return "petForm"; + } + + // ... +} +``` + +|**1**|使用`@RequestParam`。| +|-----|----------------------| + +Kotlin + +``` +import org.springframework.ui.set + +@Controller +@RequestMapping("/pets") +class EditPetForm { + + // ... + + @GetMapping + fun setupForm(@RequestParam("petId") petId: Int, model: Model): String { (1) + val pet = clinic.loadPet(petId) + model["pet"] = pet + return "petForm" + } + + // ... +} +``` + +|**1**|使用`@RequestParam`。| +|-----|----------------------| + +| |Servlet API“Request Parameter”概念将查询参数、表单<br/>数据和多个部分合并为一个。然而,在 WebFlux 中,每个都可以通过“ServerWebExchange”单独访问。虽然`@RequestParam`仅绑定到查询参数,但你可以使用`String`数据绑定来将查询参数、表单数据和多个部分应用到[command object](#webflux-ann-modelattrib-method-args)。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +默认情况下需要使用`@RequestParam`注释的方法参数,但是可以通过将`@RequestParam`的所需标志设置为`false`,或者通过使用`java.util.Optional`包装器声明参数来指定方法参数是可选的。 + +如果目标方法参数类型不是“字符串”,则自动应用类型转换。见[Type Conversion](#webflux-ann-typeconversion)。 + +当在`Map<String, String>`或 `multivalueMap<String, String>` 参数上声明`@RequestParam`注释时,映射将填充所有查询参数。 + +请注意,`@RequestParam`的使用是可选的——例如,用于设置其属性。默认情况下,任何是简单值类型(由[Beanutils#IsSimpleProperty](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-)确定)且未由任何其他参数解析器解析的参数都将被视为已用`@RequestParam`注释。 + +##### `@RequestHeader` + +[Web MVC](web.html#mvc-ann-requestheader) + +可以使用`@RequestHeader`注释将请求头绑定到控制器中的方法参数。 + +下面的示例展示了一个带有标题的请求: + +``` +Host localhost:8080 +Accept text/html,application/xhtml+xml,application/xml;q=0.9 +Accept-Language fr,en-gb;q=0.7,en;q=0.3 +Accept-Encoding gzip,deflate +Accept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive 300 +``` + +下面的示例获取`Accept-Encoding`和`Keep-Alive`标题的值: + +Java + +``` +@GetMapping("/demo") +public void handle( + @RequestHeader("Accept-Encoding") String encoding, (1) + @RequestHeader("Keep-Alive") long keepAlive) { (2) + //... +} +``` + +|**1**|获取`Accept-Encoging`标头的值。| +|-----|----------------------------------------------| +|**2**|获取`Keep-Alive`标头的值。| + +Kotlin + +``` +@GetMapping("/demo") +fun handle( + @RequestHeader("Accept-Encoding") encoding: String, (1) + @RequestHeader("Keep-Alive") keepAlive: Long) { (2) + //... +} +``` + +|**1**|获取`Accept-Encoging`标头的值。| +|-----|----------------------------------------------| +|**2**|获取`Keep-Alive`标头的值。| + +如果目标方法参数类型不是“字符串”,则自动应用类型转换。见[Type Conversion](#webflux-ann-typeconversion)。 + +当在`@RequestHeader`、`multivalueMap<String, String>` 或`void`参数上使用`@RequestHeader`注释时,映射将填充所有头值。 + +| |内置支持用于将逗号分隔的字符串转换为<br/>数组或字符串集合或类型转换系统已知的其他类型。对于<br/>示例,用`@RequestHeader("Accept")`注释的方法参数可以是类型为 `string’,但也可以是类型为[Validation](core.html#validation)或`List<String>`。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### `@CookieValue` + +[Web MVC](web.html#mvc-ann-cookievalue) + +可以使用`@CookieValue`注释将 HTTP cookie 的值绑定到控制器中的方法参数。 + +下面的示例显示了一个带有 cookie 的请求: + +``` +JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84 +``` + +下面的代码示例演示了如何获得 Cookie 值: + +爪哇 + +``` +@GetMapping("/demo") +public void handle(@CookieValue("JSESSIONID") String cookie) { (1) + //... +} +``` + +|**1**|获取 cookie 值。| +|-----|---------------------| + +Kotlin + +``` +@GetMapping("/demo") +fun handle(@CookieValue("JSESSIONID") cookie: String) { (1) + //... +} +``` + +|**1**|获取 cookie 值。| +|-----|---------------------| + +如果目标方法参数类型不是“字符串”,则自动应用类型转换。见[Type Conversion](#webflux-ann-typeconversion)。 + +##### `@ModelAttribute` + +[Web MVC](web.html#mvc-ann-modelattrib-method-args) + +你可以在方法参数上使用`@ModelAttribute`注释来访问模型中的一个属性,或者如果不存在,则将其实例化。model 属性还覆盖了查询参数和表单字段的值,这些字段的名称与字段名称匹配。这被称为数据绑定,它使你不必处理解析和转换单个查询参数和窗体字段的问题。下面的示例绑定`Pet`的实例: + +爪哇 + +``` +@PostMapping("/owners/{ownerId}/pets/{petId}/edit") +public String processSubmit(@ModelAttribute Pet pet) { } (1) +``` + +|**1**|绑定`Pet`的实例。| +|-----|--------------------------| + +Kotlin + +``` +@PostMapping("/owners/{ownerId}/pets/{petId}/edit") +fun processSubmit(@ModelAttribute pet: Pet): String { } (1) +``` + +|**1**|绑定`Pet`的实例。| +|-----|--------------------------| + +前面示例中的`Pet`实例解析如下: + +* 如果已经添加到[`Model`](#webflux-ann-modelattrib-methods),则从模型开始。 + +* 从 HTTP 会话到`Map<String, String>`。 + +* 从默认构造函数的调用。 + +* 调用带有匹配查询参数或表单字段的参数的“主构造函数”。参数名称是通过 爪哇Beans@ConstructorProperties 或通过字节码中的运行时保留参数名称确定的。 + +在获得模型属性实例之后,再进行数据绑定。“WebExchangeDatabinder”类将查询参数和表单字段的名称与目标`Object`上的字段名称匹配。在必要时应用类型转换后,将填充匹配字段。有关数据绑定(和验证)的更多信息,请参见[验证](core.html#validation)。有关自定义数据绑定的更多信息,请参见[`DataBinder`](#webflux-ann-initbinder)。 + +数据绑定可能会导致错误。默认情况下,会引发`BindingResult`,但是,要检查控制器方法中的此类错误,可以在`BindingResult`旁边立即添加一个`BindingResult`参数,如下例所示: + +爪哇 + +``` +@PostMapping("/owners/{ownerId}/pets/{petId}/edit") +public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { (1) + if (result.hasErrors()) { + return "petForm"; + } + // ... +} +``` + +|**1**|添加`BindingResult`。| +|-----|-------------------------| + +Kotlin + +``` +@PostMapping("/owners/{ownerId}/pets/{petId}/edit") +fun processSubmit(@ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1) + if (result.hasErrors()) { + return "petForm" + } + // ... +} +``` + +|**1**|添加`BindingResult`。| +|-----|-------------------------| + +你可以在数据绑定后通过添加 javax.validation.validate 注释或 Spring 的`BindingResult`注释来自动应用验证(另请参见[Bean Validation](core.html#validation-beanvalidation)和[Spring validation](core.html#validation))。下面的示例使用`@Valid`注释: + +爪哇 + +``` +@PostMapping("/owners/{ownerId}/pets/{petId}/edit") +public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { (1) + if (result.hasErrors()) { + return "petForm"; + } + // ... +} +``` + +|**1**|在模型属性参数上使用`@Valid`。| +|-----|---------------------------------------------| + +Kotlin + +``` +@PostMapping("/owners/{ownerId}/pets/{petId}/edit") +fun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1) + if (result.hasErrors()) { + return "petForm" + } + // ... +} +``` + +|**1**|在模型属性参数上使用`@Valid`。| +|-----|---------------------------------------------| + +Spring 与 Spring MVC 不同,WebFlux 在模型中支持反应性类型——例如,`mono<Account>` 或`io.reactivex.Single<Account>`。你可以声明一个`@ModelAttribute`参数,带或不带反应性类型包装器,如果需要,它将相应地解析为实际值。但是,请注意,要使用`BindingResult`参数,你必须在不使用反应式类型包装器的情况下声明`@ModelAttribute`参数,如前面所示。或者,你也可以通过 reactive 类型来处理任何错误,如下例所示: + +爪哇 + +``` +@PostMapping("/owners/{ownerId}/pets/{petId}/edit") +public Mono<String> processSubmit(@Valid @ModelAttribute("pet") Mono<Pet> petMono) { + return petMono + .flatMap(pet -> { + // ... + }) + .onErrorResume(ex -> { + // ... + }); +} +``` + +Kotlin + +``` +@PostMapping("/owners/{ownerId}/pets/{petId}/edit") +fun processSubmit(@Valid @ModelAttribute("pet") petMono: Mono<Pet>): Mono<String> { + return petMono + .flatMap { pet -> + // ... + } + .onErrorResume{ ex -> + // ... + } +} +``` + +请注意,`@ModelAttribute`的使用是可选的——例如,用于设置其属性。默认情况下,任何不是简单值类型(由[Beanutils#IsSimpleProperty](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-)确定)且未由任何其他参数解析器解析的参数都将被视为已用`@ModelAttribute`注释。 + +##### `@SessionAttributes` + +[Web MVC](web.html#mvc-ann-sessionattributes) + +`@SessionAttributes`用于在请求之间的`WebSession`中存储模型属性。它是一种类型级别的注释,用于声明特定控制器使用的会话属性。这通常会列出模型属性的名称或模型属性的类型,这些属性应该透明地存储在会话中,以供后续的访问请求使用。 + +考虑以下示例: + +爪哇 + +``` +@Controller +@SessionAttributes("pet") (1) +public class EditPetForm { + // ... +} +``` + +|**1**|使用`@SessionAttributes`注释。| +|-----|------------------------------------------| + +Kotlin + +``` +@Controller +@SessionAttributes("pet") (1) +class EditPetForm { + // ... +} +``` + +|**1**|使用`@SessionAttributes`注释。| +|-----|------------------------------------------| + +在第一个请求中,当将名称为`pet`的 model 属性添加到 model 时,它会自动升级到`WebSession`并保存在`WebSession`中。在另一个控制器方法使用`SessionStatus`方法参数清除存储之前,它一直保持不变,如下例所示: + +爪哇 + +``` +@Controller +@SessionAttributes("pet") (1) +public class EditPetForm { + + // ... + + @PostMapping("/pets/{id}") + public String handle(Pet pet, BindingResult errors, SessionStatus status) { (2) + if (errors.hasErrors()) { + // ... + } + status.setComplete(); + // ... + } + } +} +``` + +|**1**|使用`long`注释。| +|-----|------------------------------------------| +|**2**|使用`SessionStatus`变量。| + +Kotlin + +``` +@Controller +@SessionAttributes("pet") (1) +class EditPetForm { + + // ... + + @PostMapping("/pets/{id}") + fun handle(pet: Pet, errors: BindingResult, status: SessionStatus): String { (2) + if (errors.hasErrors()) { + // ... + } + status.setComplete() + // ... + } +} +``` + +|**1**|使用`@SessionAttributes`注释。| +|-----|------------------------------------------| +|**2**|使用`SessionStatus`变量。| + +##### `@SessionAttribute` + +[Web MVC](web.html#mvc-ann-sessionattribute) + +如果你需要访问已存在的会话属性,这些属性是全局管理的(也就是说,在控制器之外——例如,由过滤器管理),并且可能存在,也可能不存在,那么你可以在方法参数上使用`@SessionAttribute`注释,如下例所示: + +爪哇 + +``` +@GetMapping("/") +public String handle(@SessionAttribute User user) { (1) + // ... +} +``` + +|**1**|使用`@RequestMapping`。| +|-----|--------------------------| + +Kotlin + +``` +@GetMapping("/") +fun handle(@SessionAttribute user: User): String { (1) + // ... +} +``` + +|**1**|使用`@SessionAttribute`。| +|-----|--------------------------| + +对于需要添加或删除会话属性的用例,可以考虑将“WebSession”注入到 Controller 方法中。 + +对于将会话中的模型属性临时存储为控制器工作流的一部分,可以考虑使用`SessionAttributes`,如[@sessionAttributions’](#webflux-ann-sessionattributes)中所述。 + +##### `@RequestAttribute` + +[Web MVC](web.html#mvc-ann-requestattrib) + +与`@SessionAttribute`类似,你可以使用`@RequestAttribute`注释来访问先前创建的预先存在的请求属性(例如,通过`WebFilter`),如下例所示: + +爪哇 + +``` +@GetMapping("/") +public String handle(@RequestAttribute Client client) { (1) + // ... +} +``` + +|**1**|使用`@RequestAttribute`。| +|-----|--------------------------| + +Kotlin + +``` +@GetMapping("/") +fun handle(@RequestAttribute client: Client): String { (1) + // ... +} +``` + +|**1**|使用`@RequestAttribute`。| +|-----|--------------------------| + +##### 多部分内容 + +[Web MVC](web.html#mvc-multipart-forms) + +正如[Multipart Data](#webflux-multipart)中所解释的,`ServerWebExchange`提供了对多部分内容的访问。在控制器中处理文件上载表单(例如,从浏览器)的最佳方法是通过数据绑定到[command object](#webflux-ann-modelattrib-method-args),如下例所示: + +爪哇 + +``` +class MyForm { + + private String name; + + private MultipartFile file; + + // ... + +} + +@Controller +public class FileUploadController { + + @PostMapping("/form") + public String handleFormUpload(MyForm form, BindingResult errors) { + // ... + } + +} +``` + +Kotlin + +``` +class MyForm( + val name: String, + val file: MultipartFile) + +@Controller +class FileUploadController { + + @PostMapping("/form") + fun handleFormUpload(form: MyForm, errors: BindingResult): String { + // ... + } + +} +``` + +你还可以在 RESTful 服务场景中提交来自非浏览器客户端的多部分请求。下面的示例与 JSON 一起使用一个文件: + +``` +POST /someUrl +Content-Type: multipart/mixed + +--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp +Content-Disposition: form-data; name="meta-data" +Content-Type: application/json; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +{ + "name": "value" +} +--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp +Content-Disposition: form-data; name="file-data"; filename="file.properties" +Content-Type: text/xml +Content-Transfer-Encoding: 8bit +... File Data ... +``` + +你可以使用`@RequestPart`访问单个部件,如下例所示: + +爪哇 + +``` +@PostMapping("/") +public String handle(@RequestPart("meta-data") Part metadata, (1) + @RequestPart("file-data") FilePart file) { (2) + // ... +} +``` + +|**1**|使用`@RequestPart`获取元数据。| +|-----|-----------------------------------------| +|**2**|使用`@RequestPart`获取文件。| + +Kotlin + +``` +@PostMapping("/") +fun handle(@RequestPart("meta-data") Part metadata, (1) + @RequestPart("file-data") FilePart file): String { (2) + // ... +} +``` + +|**1**|使用`@RequestPart`获取元数据。| +|-----|-----------------------------------------| +|**2**|使用`@RequestPart`获取文件。| + +要反序列化 RAW Part 内容(例如,到 JSON——类似于`@RequestBody`),你可以声明一个具体的目标`Object`,而不是`Part`,如下例所示: + +爪哇 + +``` +@PostMapping("/") +public String handle(@RequestPart("meta-data") MetaData metadata) { (1) + // ... +} +``` + +|**1**|使用`@RequestPart`获取元数据。| +|-----|-----------------------------------------| + +Kotlin + +``` +@PostMapping("/") +fun handle(@RequestPart("meta-data") metadata: MetaData): String { (1) + // ... +} +``` + +|**1**|使用`@RequestPart`获取元数据。| +|-----|-----------------------------------------| + +你可以将`@RequestPart`与`javax.validation.Valid`或 Spring 的 `@validated’注释结合使用,这会导致应用标准 Bean 验证。验证错误会导致`WebExchangeBindException`,从而导致 400(bad\_request)响应。异常包含带有错误详细信息的`BindingResult`,也可以在 Controller 方法中通过使用异步包装器声明参数并使用与错误相关的操作符来处理: + +爪哇 + +``` +@PostMapping("/") +public String handle(@Valid @RequestPart("meta-data") Mono<MetaData> metadata) { + // use one of the onError* operators... +} +``` + +Kotlin + +``` +@PostMapping("/") +fun handle(@Valid @RequestPart("meta-data") metadata: MetaData): String { + // ... +} +``` + +要以`MultiValueMap`的形式访问所有多部分数据,可以使用`@RequestBody`,如下例所示: + +爪哇 + +``` +@PostMapping("/") +public String handle(@RequestBody Mono<MultiValueMap<String, Part>> parts) { (1) + // ... +} +``` + +|**1**|使用`@RequestBody`。| +|-----|---------------------| + +Kotlin + +``` +@PostMapping("/") +fun handle(@RequestBody parts: MultiValueMap<String, Part>): String { (1) + // ... +} +``` + +|**1**|使用`@RequestBody`。| +|-----|---------------------| + +要按顺序访问多部分数据,在流式方式中,可以使用`@RequestBody`与 `flux<Part>(或`Flow<Part>`在 Kotlin 中)代替,如下例所示: + +爪哇 + +``` +@PostMapping("/") +public String handle(@RequestBody Flux<Part> parts) { (1) + // ... +} +``` + +|**1**|使用`@RequestBody`。| +|-----|---------------------| + +Kotlin + +``` +@PostMapping("/") +fun handle(@RequestBody parts: Flow<Part>): String { (1) + // ... +} +``` + +|**1**|使用`@RequestBody`。| +|-----|---------------------| + +##### `@RequestBody` + +[Web MVC](web.html#mvc-ann-requestbody) + +你可以使用`@RequestBody`注释,通过[HttpMessageReader](#webflux-codecs)将请求主体读取并反序列化为 ` 对象’。下面的示例使用`@RequestBody`参数: + +爪哇 + +``` +@PostMapping("/accounts") +public void handle(@RequestBody Account account) { + // ... +} +``` + +Kotlin + +``` +@PostMapping("/accounts") +fun handle(@RequestBody account: Account) { + // ... +} +``` + +与 Spring MVC 不同,在 WebFlux 中,`@RequestBody`方法参数支持反应性类型和完全非阻塞的读取和(客户机到服务器)流。 + +爪哇 + +``` +@PostMapping("/accounts") +public void handle(@RequestBody Mono<Account> account) { + // ... +} +``` + +Kotlin + +``` +@PostMapping("/accounts") +fun handle(@RequestBody accounts: Flow<Account>) { + // ... +} +``` + +可以使用[WebFlux Config](#webflux-config)的[HTTP 消息编解码器](#webflux-config-message-codecs)选项来配置或自定义消息阅读器。 + +你可以将`@RequestBody`与`javax.validation.Valid`或 Spring 的 `@validated’注释结合使用,这会导致应用标准 Bean 验证。验证错误会导致`WebExchangeBindException`,从而导致 400(bad\_request)响应。异常包含带有错误详细信息的`BindingResult`,可以通过使用异步包装器声明参数,然后使用与错误相关的操作符,在 Controller 方法中进行处理: + +爪哇 + +``` +@PostMapping("/accounts") +public void handle(@Valid @RequestBody Mono<Account> account) { + // use one of the onError* operators... +} +``` + +Kotlin + +``` +@PostMapping("/accounts") +fun handle(@Valid @RequestBody account: Mono<Account>) { + // ... +} +``` + +##### `HttpEntity` + +[Web MVC](web.html#mvc-ann-httpentity) + +`HttpEntity`或多或少与使用[`@RequestBody`](#webflux-ann-requestbody)相同,但它基于一个容器对象,该对象公开了请求头和主体。下面的示例使用了“HttpEntity”: + +爪哇 + +``` +@PostMapping("/accounts") +public void handle(HttpEntity<Account> entity) { + // ... +} +``` + +Kotlin + +``` +@PostMapping("/accounts") +fun handle(entity: HttpEntity<Account>) { + // ... +} +``` + +##### `@ResponseBody` + +[Web MVC](web.html#mvc-ann-responsebody) + +你可以在方法上使用`@ResponseBody`注释,通过[HttpMessageWriter](#webflux-codecs)将返回序列化到响应主体。下面的示例展示了如何做到这一点: + +爪哇 + +``` +@GetMapping("/accounts/{id}") +@ResponseBody +public Account handle() { + // ... +} +``` + +Kotlin + +``` +@GetMapping("/accounts/{id}") +@ResponseBody +fun handle(): Account { + // ... +} +``` + +`@ResponseBody`在类级别上也受到支持,在这种情况下,所有控制器方法都会继承它。这是`@RestController`的效果,它不过是一个标记为`@Controller`和`@ResponseBody`的元注释。 + +`@ResponseBody`支持反应类型,这意味着你可以返回 reactor 或 rxjava 类型,并将它们产生的异步值呈现给响应。有关更多详细信息,请参见[Streaming](#webflux-codecs-streaming)和[JSON rendering](#webflux-codecs-jackson)。 + +你可以将`@ResponseBody`方法与 JSON 序列化视图结合起来。详见[JacksonJSON](#webflux-ann-jackson)。 + +可以使用[WebFlux Config](#webflux-config)的[HTTP 消息编解码器](#webflux-config-message-codecs)选项来配置或自定义消息写入。 + +##### `ResponseEntity` + +[Web MVC](web.html#mvc-ann-responseentity) + +`ResponseEntity`类似于[`@ResponseBody`](#webflux-ann-responsebody),但带有状态和标题。例如: + +爪哇 + +``` +@GetMapping("/something") +public ResponseEntity<String> handle() { + String body = ... ; + String etag = ... ; + return ResponseEntity.ok().eTag(etag).build(body); +} +``` + +Kotlin + +``` +@GetMapping("/something") +fun handle(): ResponseEntity<String> { + val body: String = ... + val etag: String = ... + return ResponseEntity.ok().eTag(etag).build(body) +} +``` + +WebFlux 支持使用单个值`ResponseEntity`异步地生成`ResponseEntity`,和/或为主体生成单个值和多值的反应类型。这允许使用`ResponseEntity`的各种异步响应,如下所示: + +* `ResponseEntity<Mono<T>>`或`ResponseEntity<Flux<T>>`在稍后异步提供主体时,立即使响应状态和头为已知。如果主体由 0.1 个值组成,则使用`Mono`;如果可以产生多个值,则使用`Flux`。 + +* `Mono<ResponseEntity<T>>`在稍后的时间点异步提供了所有这三个方面——响应状态、头和主体。这允许响应状态和头根据异步请求处理的结果而变化。 + +* `Mono<ResponseEntity<Mono<T>>>`或`Mono<ResponseEntity<Flux<T>>>`是另一种可能的选择,尽管不太常见。它们首先异步地提供响应状态和报头,然后是响应主体,也是异步地提供响应主体。 + +##### Jackson JSON + +Spring 提供对 JacksonJSON 库的支持。 + +##### JSON 视图 # + +[Web MVC](web.html#mvc-ann-jackson) + +Spring WebFlux 提供了对[Jackson 的序列化视图](https://www.baeldung.com/jackson-json-view-annotation)的内置支持,其仅允许呈现`Object`中所有字段的一个子集。要将其与 `@responsebody’或`Object`控制器方法一起使用,你可以使用 Jackson 的 `@jsonview’注释来激活序列化视图类,如下例所示: + +爪哇 + +``` +@RestController +public class UserController { + + @GetMapping("/user") + @JsonView(User.WithoutPasswordView.class) + public User getUser() { + return new User("eric", "7!jd#h23"); + } +} + +public class User { + + public interface WithoutPasswordView {}; + public interface WithPasswordView extends WithoutPasswordView {}; + + private String username; + private String password; + + public User() { + } + + public User(String username, String password) { + this.username = username; + this.password = password; + } + + @JsonView(WithoutPasswordView.class) + public String getUsername() { + return this.username; + } + + @JsonView(WithPasswordView.class) + public String getPassword() { + return this.password; + } +} +``` + +Kotlin + +``` +@RestController +class UserController { + + @GetMapping("/user") + @JsonView(User.WithoutPasswordView::class) + fun getUser(): User { + return User("eric", "7!jd#h23") + } +} + +class User( + @JsonView(WithoutPasswordView::class) val username: String, + @JsonView(WithPasswordView::class) val password: String +) { + interface WithoutPasswordView + interface WithPasswordView : WithoutPasswordView +} +``` + +| |`@JsonView`允许一个视图类的数组,但是每个<br/>控制器方法只能指定一个。如果需要激活多个视图,请使用复合接口。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.4.4.`Model` + +[Web MVC](web.html#mvc-ann-modelattrib-methods) + +你可以使用`@ModelAttribute`注释: + +* 在[method argument](#webflux-ann-modelattrib-method-args)中的`@RequestMapping`方法上创建或访问模型中的对象,并通过 `WebDatabinder’将其绑定到请求。 + +* 作为`@Controller`或`@ControllerAdvice`类中的方法级注释,在任何`@RequestMapping`方法调用之前帮助初始化模型。 + +* 在`@RequestMapping`方法上将其返回值标记为模型属性。 + +本节讨论`Model`方法,或者前面列表中的第二个项。控制器可以有任意数量的`@ModelAttribute`方法。所有这些方法都是在同一个控制器中的`@RequestMapping`方法之前调用的。还可以通过`@ControllerAdvice`在控制器之间共享`@ModelAttribute`方法。有关更多详细信息,请参见[财务总监建议](#webflux-ann-controller-advice)一节。 + +`@ModelAttribute`方法具有灵活的方法签名。它们支持许多与`@RequestMapping`方法相同的参数(除了`@ModelAttribute`本身和与请求主体相关的任何参数)。 + +下面的示例使用`@ModelAttribute`方法: + +爪哇 + +``` +@ModelAttribute +public void populateModel(@RequestParam String number, Model model) { + model.addAttribute(accountRepository.findAccount(number)); + // add more ... +} +``` + +Kotlin + +``` +@ModelAttribute +fun populateModel(@RequestParam number: String, model: Model) { + model.addAttribute(accountRepository.findAccount(number)) + // add more ... +} +``` + +下面的示例只添加了一个属性: + +爪哇 + +``` +@ModelAttribute +public Account addAccount(@RequestParam String number) { + return accountRepository.findAccount(number); +} +``` + +Kotlin + +``` +@ModelAttribute +fun addAccount(@RequestParam number: String): Account { + return accountRepository.findAccount(number); +} +``` + +| |当未显式指定名称时,将根据类型<br/>选择缺省名称,正如在 爪哇doc 中对[`Conventions`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/core/Conventions.html)的解释。<br/>通过重载的`addAttribute`方法或<br/>上的 name 属性(用于返回值),始终可以分配显式名称。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +Spring WebFlux 与 Spring MVC 不同,在模型中明确支持反应性类型(例如,或)。这样的异步模型属性可以在调用`@RequestMapping`时透明地解析(并更新模型)到它们的实际值,只要不使用包装器就声明`@ModelAttribute`参数,如下例所示: + +爪哇 + +``` +@ModelAttribute +public void addAccount(@RequestParam String number) { + Mono<Account> accountMono = accountRepository.findAccount(number); + model.addAttribute("account", accountMono); +} + +@PostMapping("/accounts") +public String handle(@ModelAttribute Account account, BindingResult errors) { + // ... +} +``` + +Kotlin + +``` +import org.springframework.ui.set + +@ModelAttribute +fun addAccount(@RequestParam number: String) { + val accountMono: Mono<Account> = accountRepository.findAccount(number) + model["account"] = accountMono +} + +@PostMapping("/accounts") +fun handle(@ModelAttribute account: Account, errors: BindingResult): String { + // ... +} +``` + +此外,在视图呈现之前,具有反应性类型包装器的任何模型属性都将被解析为它们的实际值(以及模型更新)。 + +你也可以使用`@ModelAttribute`作为`@RequestMapping`方法的方法级别注释,在这种情况下,`@RequestMapping`方法的返回值被解释为一个模型属性。这通常不是必需的,因为这是 HTML 控制器中的默认行为,除非返回值是`String`,否则该返回值将被解释为视图名称。`@ModelAttribute`还可以帮助自定义模型属性名,如下例所示: + +爪哇 + +``` +@GetMapping("/accounts/{id}") +@ModelAttribute("myAccount") +public Account handle() { + // ... + return account; +} +``` + +Kotlin + +``` +@GetMapping("/accounts/{id}") +@ModelAttribute("myAccount") +fun handle(): Account { + // ... + return account +} +``` + +#### 1.4.5.`DataBinder` + +[Web MVC](web.html#mvc-ann-initbinder) + +`@Controller`或`javax.validation.Valid`类可以具有`@InitBinder`方法,以初始化`WebDataBinder`的实例。这些反过来又被用来: + +* 将请求参数(即表单数据或查询)绑定到模型对象。 + +* 将基于`String`的请求值(例如请求参数、路径变量、头、cookie 和其他)转换为控制器方法参数的目标类型。 + +* 在呈现 HTML 窗体时,将模型对象值格式化为`String`值。 + +`@InitBinder`方法可以注册控制器特定的`java.beans.PropertyEditor`或 Spring `Converter`和`Formatter`组件。此外,可以使用[WebFlux 爪哇 配置](#webflux-config-conversion)在全局共享的`FormattingConversionService`中注册`Converter`和 `formatter’类型。 + +`@InitBinder`方法支持许多与`@RequestMapping`方法相同的参数,但`@ModelAttribute`(命令对象)参数除外。通常,它们的声明使用`WebDataBinder`参数(用于注册)和`void`返回值。下面的示例使用`@InitBinder`注释: + +爪哇 + +``` +@Controller +public class FormController { + + @InitBinder (1) + public void initBinder(WebDataBinder binder) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); + dateFormat.setLenient(false); + binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false)); + } + + // ... +} +``` + +|**1**|使用`@InitBinder`注释。| +|-----|-----------------------------------| + +Kotlin + +``` +@Controller +class FormController { + + @InitBinder (1) + fun initBinder(binder: WebDataBinder) { + val dateFormat = SimpleDateFormat("yyyy-MM-dd") + dateFormat.isLenient = false + binder.registerCustomEditor(Date::class.java, CustomDateEditor(dateFormat, false)) + } + + // ... +} +``` + +或者,当通过共享的“formattingConversionService”使用基于`Formatter`的设置时,你可以重复使用相同的方法并注册特定于控制器的`Formatter`实例,如下例所示: + +爪哇 + +``` +@Controller +public class FormController { + + @InitBinder + protected void initBinder(WebDataBinder binder) { + binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd")); (1) + } + + // ... +} +``` + +|**1**|添加自定义格式化程序(在本例中为`DateFormatter`)。| +|-----|------------------------------------------------------------| + +Kotlin + +``` +@Controller +class FormController { + + @InitBinder + fun initBinder(binder: WebDataBinder) { + binder.addCustomFormatter(DateFormatter("yyyy-MM-dd")) (1) + } + + // ... +} +``` + +|**1**|添加自定义格式化程序(在本例中为`DateFormatter`)。| +|-----|------------------------------------------------------------| + +#### 1.4.6.管理异常 + +[Web MVC](web.html#mvc-ann-exceptionhandler) + +`@Controller`和[@controlleradvice](#webflux-ann-controller-advice)类可以使用 `@ExceptionHandler’方法来处理来自控制器方法的异常。以下示例包括这样的处理程序方法: + +爪哇 + +``` +@Controller +public class SimpleController { + + // ... + + @ExceptionHandler (1) + public ResponseEntity<String> handle(IOException ex) { + // ... + } +} +``` + +|**1**|声明`@ExceptionHandler`。| +|-----|---------------------------------| + +Kotlin + +``` +@Controller +class SimpleController { + + // ... + + @ExceptionHandler (1) + fun handle(ex: IOException): ResponseEntity<String> { + // ... + } +} +``` + +|**1**|声明`@ExceptionHandler`。| +|-----|---------------------------------| + +异常可以与正在传播的顶级异常(即抛出直接的 `ioException’)匹配,也可以与顶级包装器异常中的直接原因匹配(例如,在`IOException`中包装的`IllegalStateException`)。 + +为了匹配异常类型,最好将目标异常声明为方法参数,如前面的示例所示。或者,注释声明可以缩小异常类型以进行匹配。我们通常建议在参数签名中尽可能具体,并在“@ControllerAdvisory”上声明主根异常映射,并按相应的顺序进行优先排序。详见[the MVC section](web.html#mvc-ann-exceptionhandler)。 + +| |WebFlux 中的`@ExceptionHandler`方法支持与<br/>方法相同的方法参数和返回值,但请求主体-<br/>和`@ModelAttribute`-相关的方法参数除外。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +Spring WebFlux 中对`@ExceptionHandler`方法的支持是由`@RequestMapping`方法的“HandlerAdapter”提供的。有关更多详细信息,请参见[DispatcherHandler’](#webflux-dispatcher-handler)。 + +##### REST API 异常 + +[Web MVC](web.html#mvc-ann-rest-exceptions) + +REST 服务的一个常见要求是在响应主体中包含错误详细信息。 Spring 框架不会自动这样做,因为响应主体中的错误细节的表示是特定于应用程序的。但是,`@RESTController` 可以使用`Formatter`方法和`ResponseEntity`返回值来设置响应的状态和主体。这样的方法也可以在`@ControllerAdvice`类中声明,以在全局范围内应用它们。 + +| |请注意, Spring WebFlux 对于 Spring MVC`ResponseEntyExceptionHandler’没有一个等价物,因为 WebFlux 只会产生`ResponseStatusException`(或其子类),并且这些不需要被翻译为<br/>一个 HTTP 状态代码。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.4.7.财务总监建议 + +[Web MVC](web.html#mvc-ann-controller-advice) + +通常,`@ExceptionHandler`、`@InitBinder`和`@ModelAttribute`方法应用于声明它们的`@Controller`类(或类层次结构)中。如果你希望这样的方法在全局范围内(跨控制器)更多地应用,那么可以在一个用`@ControllerAdvice`或`@RestControllerAdvice`注释的类中声明它们。 + +`@ExceptionHandler`注释为`@Component`,这意味着这样的类可以通过[组件扫描](core.html#beans-java-instantiating-container-scan)注册为 Spring bean。`@RestControllerAdvice`是一个组合注释,它同时使用`@ControllerAdvice`和`@ResponseBody`进行注释,其本质上意味着 `@ExceptionHandler’方法通过消息转换(与视图解析或模板呈现相比)呈现到响应主体。 + +在启动时,`@RequestMapping`和`@ExceptionHandler`方法的基础设施类检测 Spring 带有`@ControllerAdvice`注释的 bean,然后在运行时应用它们的方法。全局`@ExceptionHandler`方法(来自`@ControllerAdvice`)应用于*之后*局部方法(来自`@Controller`)。相比之下,全局`@ModelAttribute`和`@InitBinder`方法则应用于局部方法*在此之前*。 + +默认情况下,`@ControllerAdvice`方法适用于每个请求(即所有控制器),但你可以通过使用注释上的属性将其缩小到控制器的子集,如下例所示: + +爪哇 + +``` +// Target all Controllers annotated with @RestController +@ControllerAdvice(annotations = RestController.class) +public class ExampleAdvice1 {} + +// Target all Controllers within specific packages +@ControllerAdvice("org.example.controllers") +public class ExampleAdvice2 {} + +// Target all Controllers assignable to specific classes +@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class}) +public class ExampleAdvice3 {} +``` + +Kotlin + +``` +// Target all Controllers annotated with @RestController +@ControllerAdvice(annotations = [RestController::class]) +public class ExampleAdvice1 {} + +// Target all Controllers within specific packages +@ControllerAdvice("org.example.controllers") +public class ExampleAdvice2 {} + +// Target all Controllers assignable to specific classes +@ControllerAdvice(assignableTypes = [ControllerInterface::class, AbstractController::class]) +public class ExampleAdvice3 {} +``` + +前面示例中的选择器是在运行时进行评估的,如果广泛使用,可能会对性能产生负面影响。有关更多详细信息,请参见[@controlleradvice](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/bind/annotation/ControllerAdvice.html)爪哇doc。 + +### 1.5.功能端点 + +[Web MVC](web.html#webmvc-fn) + +Spring WebFlux 包括 WebFlux.FN,这是一种轻量级函数编程模型,在该模型中,函数被用于路由和处理请求和契约,并被设计为具有不可变性。它是基于注释的编程模型的一种替代方案,但在其他情况下运行在相同的[Reactive Core](#webflux-reactive-spring-web)基础上。 + +#### 1.5.1.概述 + +[Web MVC](web.html#webmvc-fn-overview) + +在 WebFlux.FN 中,HTTP 请求是用`HandlerFunction`处理的:一个函数接受 `serverrequest’并返回一个延迟的`ServerResponse`(即`Mono<ServerResponse>`)。请求和响应对象都具有不可更改的契约,这些契约提供了对 HTTP 请求和响应的 JDK8 友好访问。在基于注释的编程模型中,“handlerfunction”相当于`@RequestMapping`方法的主体。 + +传入的请求被路由到带有`RouterFunction`的处理程序函数:该函数接受`ServerRequest`并返回延迟的`io.reactivex.Single<Account>`(即`Mono<HandlerFunction>`)。当路由器函数匹配时,将返回一个处理程序函数;否则将返回一个空的 mono。“routerfunction”相当于`@RequestMapping`注释,但与此的主要区别是,路由器函数不仅提供数据,还提供行为。 + +`RouterFunctions.route()`提供了一个路由器构建器,可以促进路由器的创建,如下例所示: + +爪哇 + +``` +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() + .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) { + // ... + } +} +``` + +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**|使用协程路由器 DSL 创建路由器,还可以通过`router { }`提供反应式替代方案。| +|-----|-----------------------------------------------------------------------------------------------------| + +运行`RouterFunction`的一种方法是将其转换为`HttpHandler`,并通过一个内置的[server adapters](#webflux-httphandler)安装它: + +* `RouterFunctions.toHttpHandler(RouterFunction)` + +* `RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)` + +大多数应用程序都可以通过 WebFlux 爪哇 配置运行,参见[运行服务器](#webflux-fn-running)。 + +#### 1.5.2.handlerfunction + +[Web MVC](web.html#webmvc-fn-handler-functions) + +`ServerRequest`和`ServerResponse`是不可变的接口,它们提供对 HTTP 请求和响应的 JDK8 友好访问。请求和响应都针对体流提供[反应流](https://www.reactive-streams.org)反压。请求主体用反应器`Flux`或`Mono`表示。响应体用任何反应流`Publisher`表示,包括`Flux`和`Mono`。有关该问题的更多信息,请参见[反应库](#webflux-reactive-libraries)。 + +##### ServerRequest + +`ServerRequest`提供对 HTTP 方法、URI、标头和查询参数的访问,而对主体的访问是通过`body`方法提供的。 + +下面的示例将请求主体提取到`Mono<String>`: + +爪哇 + +``` +Mono<String> string = request.bodyToMono(String.class); +``` + +Kotlin + +``` +val string = request.awaitBody<String>() +``` + +下面的示例将主体提取到`Flux<Person>`(或 Kotlin 中的`Flow<Person>`),其中`Person`对象是从形式化的形式(例如 JSON 或 XML)中解码的: + +爪哇 + +``` +Flux<Person> people = request.bodyToFlux(Person.class); +``` + +Kotlin + +``` +val people = request.bodyToFlow<Person>() +``` + +前面的示例是使用更通用的`ServerRequest.body(BodyExtractor)`的快捷方式,它接受`BodyExtractor`功能策略接口。实用程序类“BodyExtractors”提供了对许多实例的访问。例如,前面的示例也可以写如下: + +爪哇 + +``` +Mono<String> string = request.body(BodyExtractors.toMono(String.class)); +Flux<Person> people = request.body(BodyExtractors.toFlux(Person.class)); +``` + +Kotlin + +``` + val string = request.body(BodyExtractors.toMono(String::class.java)).awaitSingle() + val people = request.body(BodyExtractors.toFlux(Person::class.java)).asFlow() +``` + +下面的示例展示了如何访问表单数据: + +爪哇 + +``` +Mono<MultiValueMap<String, String>> map = request.formData(); +``` + +Kotlin + +``` +val map = request.awaitFormData() +``` + +下面的示例展示了如何以地图的形式访问多部分数据: + +爪哇 + +``` +Mono<MultiValueMap<String, Part>> map = request.multipartData(); +``` + +Kotlin + +``` +val map = request.awaitMultipartData() +``` + +下面的示例展示了如何以流媒体方式一次访问多个部分: + +爪哇 + +``` +Flux<Part> parts = request.body(BodyExtractors.toParts()); +``` + +Kotlin + +``` +val parts = request.body(BodyExtractors.toParts()).asFlow() +``` + +##### ServerResponse + +`ServerResponse`提供对 HTTP 响应的访问,由于它是不可变的,你可以使用`build`方法来创建它。你可以使用构建器设置响应状态、添加响应头或提供主体。下面的示例使用 JSON 内容创建一个 200(OK)响应: + +爪哇 + +``` +Mono<Person> person = ... +ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class); +``` + +Kotlin + +``` +val person: Person = ... +ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(person) +``` + +下面的示例展示了如何使用`Location`标头构建 201(已创建)响应,而不使用正文: + +爪哇 + +``` +URI location = ... +ServerResponse.created(location).build(); +``` + +Kotlin + +``` +val location: URI = ... +ServerResponse.created(location).build() +``` + +根据使用的编解码器,可以传递提示参数来自定义如何序列化或反序列化主体。例如,要指定[JacksonJSON 视图](https://www.baeldung.com/jackson-json-view-annotation): + +爪哇 + +``` +ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...); +``` + +Kotlin + +``` +ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...) +``` + +##### 处理程序类 + +我们可以将处理程序函数编写为 lambda,如下例所示: + +爪哇 + +``` +HandlerFunction<ServerResponse> helloWorld = + request -> ServerResponse.ok().bodyValue("Hello World"); +``` + +Kotlin + +``` +val helloWorld = HandlerFunction<ServerResponse> { ServerResponse.ok().bodyValue("Hello World") } +``` + +这很方便,但在一个应用程序中,我们需要多个功能,而多个内联 lambda 可能会变得混乱。因此,将相关的处理程序函数组合成一个处理程序类是有用的,该处理程序类在基于注释的应用程序中具有与`@Controller`类似的作用。例如,下面的类公开了一个反应性`Person`存储库: + +爪哇 + +``` +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`是一个处理函数,它将存储库中找到的所有`Person`对象作为<br/>json 返回。| +|-----|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|`createPerson`是一个处理函数,它存储了一个包含在请求主体中的新`Person`。<br/>注意,`PersonRepository.savePerson(Person)`返回`Mono<Void>`:一个空的`Mono`,当该人已从请求中读取并存储时,它会发出<br/>完成信号。因此,我们使用“build(publisher<Void>)”方法在收到完成信号时(即<br/>`Person`已保存时)发送响应。| +|**3**|`getPerson`是一个处理函数,它返回一个人,由`id`路径<br/>变量标识。我们从存储库中检索`Person`并创建一个 JSON 响应,如果找到了<br/>。如果没有找到它,我们使用`switchIfEmpty(Mono<T>)`返回 404Not Found 响应。| + +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`是一个处理函数,它以<br/>json 的形式返回在存储库中找到的所有`Person`对象。| +|-----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|`createPerson`是一个处理函数,它存储了一个包含在请求主体中的新`Person`。<br/>注意,`PersonRepository.savePerson(Person)`是一个没有返回类型的挂起函数。| +|**3**|`getPerson`是一个处理函数,它返回一个人,由`id`路径<br/>变量标识。我们从存储库中检索`Person`并创建一个 JSON 响应,如果找到了<br/>。如果没有找到它,我们将返回 404Not Found 响应。| + +##### Validation + +功能端点可以使用 Spring 的[验证设施](core.html#validation)将验证应用于请求主体。例如,给定一个针对`Person`的自定义 Spring [Validator](core.html#validation)实现: + +爪哇 + +``` +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**|创建`Validator`实例。| +|-----|-----------------------------------| +|**2**|应用验证。| +|**3**|提出 400 响应的例外情况。| + +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**|创建`Validator`实例。| +|-----|-----------------------------------| +|**2**|应用验证。| +|**3**|提出 400 响应的例外情况。| + +处理程序还可以通过基于`LocalValidatorFactoryBean`创建和注入一个全局`Validator`实例来使用标准 Bean 验证 API(JSR-303)。见[Spring Validation](core.html#validation-beanvalidation)。 + +#### 1.5.3.`RouterFunction` + +[Web MVC](web.html#webmvc-fn-router-functions) + +路由器函数用于将请求路由到相应的`HandlerFunction`。通常,你不会自己编写路由器函数,而是使用“RouterFunctions”实用程序类上的一种方法来创建一个。“RouterFunctions.Route()”(无参数)为你提供了一个用于创建路由器函数的 Fluent 构建器,而`RouterFunctions.route(RequestPredicate, HandlerFunction)`提供了一种直接创建路由器的方法。 + +通常,建议使用`route()`Builder,因为它为典型的映射场景提供了方便的快捷方式,而不需要很难发现的静态导入。例如,Router Function Builder 提供了方法`GET(String, HandlerFunction)`来创建 GET 请求的映射;以及`POST(String, HandlerFunction)`用于 POST。 + +除了基于 HTTP 方法的映射,Route Builder 还提供了一种在映射到请求时引入额外谓词的方法。对于每个 HTTP 方法,都有一个重载变量,该变量将`RequestPredicate`作为参数,尽管可以表示该参数的附加约束。 + +##### 谓词 + +你可以编写自己的`RequestPredicate`,但是`RequestPredicates`实用程序类提供了基于请求路径、HTTP 方法、Content-type 等的常用实现。下面的示例使用一个请求谓词来基于`Accept`头创建一个约束: + +爪哇 + +``` +RouterFunction<ServerResponse> route = RouterFunctions.route() + .GET("/hello-world", accept(MediaType.TEXT_PLAIN), + request -> ServerResponse.ok().bodyValue("Hello World")).build(); +``` + +Kotlin + +``` +val route = coRouter { + GET("/hello-world", accept(TEXT_PLAIN)) { + ServerResponse.ok().bodyValueAndAwait("Hello World") + } +} +``` + +你可以使用以下方法将多个请求谓词组合在一起: + +* `RequestPredicate.and(RequestPredicate)`—两者必须匹配。 + +* `RequestPredicate.or(RequestPredicate)`—两者都可以匹配。 + +来自`RequestPredicates`的许多谓词都是组成的。例如,`RequestPredicates.GET(String)`是由`RequestPredicates.method(HttpMethod)`和`RequestPredicates.path(String)`组成的。上面显示的示例还使用了两个请求谓词,因为构建器在内部使用 `requestPredicates.Get’,并用`accept`谓词将其组合起来。 + +##### 路线 + +对路由器的功能按顺序进行评估:如果第一条路由不匹配,则对第二条路由进行评估,依此类推。因此,在一般路线之前声明更具体的路线是有意义的。当将路由器功能注册为 Spring bean 时,这一点也很重要,后面将对此进行说明。请注意,这种行为与基于注释的编程模型不同,在该模型中,“最特定的”控制器方法是自动选择的。 + +当使用 Router Function Builder 时,所有定义的路由都被组合成一个“routerfunction”,该“routerfunction”从`build()`返回。还有其他方法可以将多个路由器功能组合在一起: + +* `add(RouterFunction)`上的`RouterFunctions.route()`构建器 + +* `RouterFunction.and(RouterFunction)` + +* `RouterFunction.andRoute(RequestPredicate, HandlerFunction)`—带嵌套`RouterFunctions.route()`的 `routerfunction.and()’的快捷方式。 + +下面的示例显示了四条路线的组成: + +爪哇 + +``` +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**|带有与 JSON 匹配的`GET /person/{id}`头的`Accept`被路由到 `personhandler.getperson’| +|-----|--------------------------------------------------------------------------------------------------| +|**2**|带有与 JSON 匹配的`GET /person`头的`Accept`被路由到 `personhandler.listpeople’| +|**3**|没有附加谓词的`POST /person`映射到 `personhandler.createPerson’,并且| +|**4**|`otherRoute`是在其他地方创建的路由器功能,并将其添加到构建的路由中。| + +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**|带有与 JSON 匹配的`GET /person/{id}`头的`Accept`被路由到 `personhandler.getperson’| +|-----|--------------------------------------------------------------------------------------------------| +|**2**|带有与 JSON 匹配的`GET /person`头的`Accept`被路由到 `personhandler.listpeople’| +|**3**|没有附加谓词的`POST /person`映射到 `personhandler.createPerson’,并且| +|**4**|`otherRoute`是在其他地方创建的路由器功能,并将其添加到构建的路由中。| + +##### 嵌套路线 + +一组路由器函数通常有一个共享谓词,例如共享路径。在上面的示例中,共享谓词将是一个匹配`/person`的路径谓词,由三个路由使用。在使用注释时,你可以使用映射到 `/person’的类型级别`@RequestMapping`注释来删除这种重复。在 WebFlux.FN 中,路径谓词可以通过 Router Function Builder 上的`path`方法共享。例如,通过使用嵌套路由,可以通过以下方式改进上面示例的最后几行: + +爪哇 + +``` +RouterFunction<ServerResponse> route = route() + .path("/person", builder -> builder (1) + .GET("/{id}", accept(APPLICATION_JSON), handler::getPerson) + .GET(accept(APPLICATION_JSON), handler::listPeople) + .POST("/person", handler::createPerson)) + .build(); +``` + +|**1**|请注意,`path`的第二个参数是接受路由器生成器的消费者。| +|-----|-----------------------------------------------------------------------------------| + +Kotlin + +``` +val route = coRouter { + "/person".nest { + GET("/{id}", accept(APPLICATION_JSON), handler::getPerson) + GET(accept(APPLICATION_JSON), handler::listPeople) + POST("/person", handler::createPerson) + } +} +``` + +尽管基于路径的嵌套是最常见的,但你可以通过在 Builder 上使用`nest`方法在任何类型的谓词上进行嵌套。上面仍然包含一些以共享`Accept`-header 谓词形式出现的重复。我们可以通过使用`nest`方法和`accept`方法来进一步改进: + +爪哇 + +``` +RouterFunction<ServerResponse> route = route() + .path("/person", b1 -> b1 + .nest(accept(APPLICATION_JSON), b2 -> b2 + .GET("/{id}", handler::getPerson) + .GET(handler::listPeople)) + .POST("/person", handler::createPerson)) + .build(); +``` + +Kotlin + +``` +val route = coRouter { + "/person".nest { + accept(APPLICATION_JSON).nest { + GET("/{id}", handler::getPerson) + GET(handler::listPeople) + POST("/person", handler::createPerson) + } + } +} +``` + +#### 1.5.4.运行服务器 + +[Web MVC](web.html#webmvc-fn-running) + +如何在 HTTP 服务器中运行路由器功能?一个简单的选择是使用以下方法之一将路由器函数转换为`HttpHandler`: + +* `RouterFunctions.toHttpHandler(RouterFunction)` + +* `RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)` + +然后,你可以将返回的`HttpHandler`与多个服务器适配器一起使用,方法是按照[HttpHandler](#webflux-httphandler)执行特定于服务器的指令。 + +Spring 引导也使用的一个更典型的选项是,通过[WebFlux Config](#webflux-config)使用基于[DispatcherHandler’](#webflux-dispatcher-handler)的设置运行,该设置使用 Spring 配置来声明处理请求所需的组件。WebFlux 爪哇 配置声明了以下支持功能端点的基础设施组件: + +* `RouterFunctionMapping`:在 Spring 配置中检测一个或多个`RouterFunction<?>`bean,[orders them](core.html#beans-factory-ordered),通过 `routerfunction.andother’将它们组合,并将请求路由到结果组合的`RouterFunction`。 + +* `HandlerFunctionAdapter`:允许`DispatcherHandler`调用映射到请求的`HandlerFunction`的简单适配器。 + +* `ServerResponseResultHandler`:通过调用`ServerResponse`的`writeTo`方法来处理调用 `handlerfunction’的结果。 + +前面的组件让功能端点适合`DispatcherHandler`请求处理生命周期,并且(可能)与带注释的控制器(如果声明了任何控制器的话)并排运行。这也是 Spring 引导 WebFlux 启动器启用功能端点的方式。 + +下面的示例展示了一个 WebFlux 爪哇 配置(有关如何运行它,请参见[DispatcherHandler](#webflux-dispatcher-handler)): + +爪哇 + +``` +@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... + } +} +``` + +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... + } +} +``` + +#### 1.5.5.过滤处理程序函数 + +[Web MVC](web.html#webmvc-fn-handler-filter-function) + +你可以通过使用路由函数生成器上的`before`、`after`或`filter`方法来过滤处理程序函数。对于注释,你可以通过使用`@ControllerAdvice`、`ServletFilter`或同时使用这两种方法来实现类似的功能。筛选器将应用于由构建器构建的所有路由。这意味着嵌套路由中定义的筛选器不适用于“顶层”路由。例如,考虑以下示例: + +爪哇 + +``` +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("/person", handler::createPerson)) + .after((request, response) -> logResponse(response)) (2) + .build(); +``` + +|**1**|添加自定义请求头的`before`过滤器仅应用于两个 GET 路由。| +|-----|----------------------------------------------------------------------------------------------| +|**2**|记录响应的`after`过滤器应用于所有路由,包括嵌套的路由。| + +Kotlin + +``` +val route = router { + "/person".nest { + GET("/{id}", handler::getPerson) + GET("", handler::listPeople) + before { (1) + ServerRequest.from(it) + .header("X-RequestHeader", "Value").build() + } + POST("/person", handler::createPerson) + after { _, response -> (2) + logResponse(response) + } + } +} +``` + +|**1**|添加自定义请求头的`before`过滤器仅应用于两个 GET 路由。| +|-----|----------------------------------------------------------------------------------------------| +|**2**|记录响应的`after`过滤器应用于所有路由,包括嵌套的路由。| + +路由器构建器上的`filter`方法接受`HandlerFilterFunction`:一个函数接受`ServerRequest`和`HandlerFunction`并返回`ServerResponse`。处理程序函数参数表示链中的下一个元素。这通常是路由到的处理程序,但是如果应用了多个,它也可以是另一个过滤器。 + +现在我们可以向我们的路由添加一个简单的安全过滤器,假设我们有一个`SecurityManager`,它可以确定是否允许特定的路径。下面的示例展示了如何做到这一点: + +爪哇 + +``` +SecurityManager securityManager = ... + +RouterFunction<ServerResponse> route = route() + .path("/person", b1 -> b1 + .nest(accept(APPLICATION_JSON), b2 -> b2 + .GET("/{id}", handler::getPerson) + .GET(handler::listPeople)) + .POST("/person", handler::createPerson)) + .filter((request, next) -> { + if (securityManager.allowAccessTo(request.path())) { + return next.handle(request); + } + else { + return ServerResponse.status(UNAUTHORIZED).build(); + } + }) + .build(); +``` + +Kotlin + +``` +val securityManager: SecurityManager = ... + +val route = router { + ("/person" and accept(APPLICATION_JSON)).nest { + GET("/{id}", handler::getPerson) + GET("", handler::listPeople) + POST("/person", handler::createPerson) + filter { request, next -> + if (securityManager.allowAccessTo(request.path())) { + next(request) + } + else { + status(UNAUTHORIZED).build(); + } + } + } + } +``` + +前面的示例演示了调用`next.handle(ServerRequest)`是可选的。我们只允许在允许访问的情况下运行处理程序函数。 + +除了在路由器功能构建器上使用`filter`方法外,还可以通过`RouterFunction.filter(HandlerFilterFunction)`对现有的路由器功能应用过滤器。 + +| |CORS 对功能端点的支持是通过专用的[`CorsWebFilter`](webflux-cors.html#webflux-cors-webfilter)提供的。| +|---|---------------------------------------------------------------------------------------------------------------------------------| + +### 1.6.URI 链接 + +[Web MVC](web.html#mvc-uri-building) + +本节描述了在 Spring 框架中可用来准备 URI 的各种选项。 + +#### 1.6.1.尿酸成分 + +Spring MVC 和 Spring WebFlux + +`UriComponentsBuilder`有助于从具有变量的 URI 模板构建 URI,如下例所示: + +爪哇 + +``` +UriComponents uriComponents = UriComponentsBuilder + .fromUriString("https://example.com/hotels/{hotel}") (1) + .queryParam("q", "{q}") (2) + .encode() (3) + .build(); (4) + +URI uri = uriComponents.expand("Westin", "123").toUri(); (5) +``` + +|**1**|带有 URI 模板的静态工厂方法。| +|-----|-----------------------------------------------------------| +|**2**|添加或替换 URI 组件。| +|**3**|请求对 URI 模板和 URI 变量进行编码。| +|**4**|构建`UriComponents`。| +|**5**|展开变量并获得`URI`。| + +Kotlin + +``` +val uriComponents = UriComponentsBuilder + .fromUriString("https://example.com/hotels/{hotel}") (1) + .queryParam("q", "{q}") (2) + .encode() (3) + .build() (4) + +val uri = uriComponents.expand("Westin", "123").toUri() (5) +``` + +|**1**|带有 URI 模板的静态工厂方法。| +|-----|-----------------------------------------------------------| +|**2**|添加或替换 URI 组件。| +|**3**|请求对 URI 模板和 URI 变量进行编码。| +|**4**|构建`UriComponents`。| +|**5**|展开变量并获得`URI`。| + +前面的示例可以合并为一个链,并用`buildAndExpand`将其缩短,如下例所示: + +爪哇 + +``` +URI uri = UriComponentsBuilder + .fromUriString("https://example.com/hotels/{hotel}") + .queryParam("q", "{q}") + .encode() + .buildAndExpand("Westin", "123") + .toUri(); +``` + +Kotlin + +``` +val uri = UriComponentsBuilder + .fromUriString("https://example.com/hotels/{hotel}") + .queryParam("q", "{q}") + .encode() + .buildAndExpand("Westin", "123") + .toUri() +``` + +你可以通过直接访问一个 URI(这意味着编码)来进一步缩短它,如下例所示: + +爪哇 + +``` +URI uri = UriComponentsBuilder + .fromUriString("https://example.com/hotels/{hotel}") + .queryParam("q", "{q}") + .build("Westin", "123"); +``` + +Kotlin + +``` +val uri = UriComponentsBuilder + .fromUriString("https://example.com/hotels/{hotel}") + .queryParam("q", "{q}") + .build("Westin", "123") +``` + +可以使用完整的 URI 模板进一步缩短它,如下例所示: + +爪哇 + +``` +URI uri = UriComponentsBuilder + .fromUriString("https://example.com/hotels/{hotel}?q={q}") + .build("Westin", "123"); +``` + +Kotlin + +``` +val uri = UriComponentsBuilder + .fromUriString("https://example.com/hotels/{hotel}?q={q}") + .build("Westin", "123") +``` + +#### 1.6.2.UriBuilder + +Spring MVC 和 Spring WebFlux + +[“uricomponentsbuilder”](#web-uricomponents)实现`UriBuilder`。你可以创建一个“uribuilder”,然后使用`UriBuilderFactory`。同时,`UriBuilderFactory`和 `uribuilder’提供了一种基于共享配置(如基本 URL、编码首选项和其他详细信息)的可插入机制,用于从 URI 模板构建 URI。 + +你可以使用`UriBuilderFactory`配置`RestTemplate`和`WebClient`来定制 URI 的准备。`DefaultUriBuilderFactory`是`UriBuilderFactory`的默认实现,它在内部使用`UriComponentsBuilder`并公开共享配置选项。 + +下面的示例展示了如何配置`RestTemplate`: + +Java + +``` +// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode; + +String baseUrl = "https://example.org"; +DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl); +factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES); + +RestTemplate restTemplate = new RestTemplate(); +restTemplate.setUriTemplateHandler(factory); +``` + +Kotlin + +``` +// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode + +val baseUrl = "https://example.org" +val factory = DefaultUriBuilderFactory(baseUrl) +factory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES + +val restTemplate = RestTemplate() +restTemplate.uriTemplateHandler = factory +``` + +下面的示例配置`WebClient`: + +Java + +``` +// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode; + +String baseUrl = "https://example.org"; +DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl); +factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES); + +WebClient client = WebClient.builder().uriBuilderFactory(factory).build(); +``` + +Kotlin + +``` +// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode + +val baseUrl = "https://example.org" +val factory = DefaultUriBuilderFactory(baseUrl) +factory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES + +val client = WebClient.builder().uriBuilderFactory(factory).build() +``` + +此外,还可以直接使用`DefaultUriBuilderFactory`。它类似于使用“uriComponentsBuilder”,但它不是静态的工厂方法,而是保存配置和首选项的实际实例,如下例所示: + +Java + +``` +String baseUrl = "https://example.com"; +DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl); + +URI uri = uriBuilderFactory.uriString("/hotels/{hotel}") + .queryParam("q", "{q}") + .build("Westin", "123"); +``` + +Kotlin + +``` +val baseUrl = "https://example.com" +val uriBuilderFactory = DefaultUriBuilderFactory(baseUrl) + +val uri = uriBuilderFactory.uriString("/hotels/{hotel}") + .queryParam("q", "{q}") + .build("Westin", "123") +``` + +#### 1.6.3.URI 编码 + +Spring MVC 和 Spring WebFlux + +`UriComponentsBuilder`在两个级别上公开编码选项: + +* [uricomponentsbuilder#encode()](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/util/UriComponentsBuilder.html#encode--):先对 URI 模板进行预编码,然后在展开时对 URI 变量进行严格编码。 + +* [uricomponents#encode()](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/util/UriComponents.html#encode--):编码 URI 组件*之后*URI 变量被展开。 + +这两个选项都用转义的八进制替换非 ASCII 和非法字符。然而,第一个选项也用 URI 变量中出现的保留意义替换字符。 + +| |考虑一下“;”,它在某种程度上是合法的,但具有保留的含义。第一个选项在 URI 变量中用“%3b”替换<br/>;;",而不是在 URI 模板中。相比之下,第二个选项永远不会<br/>替换“;”,因为它是路径中的法律字符。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在大多数情况下,第一个选项可能会给出预期的结果,因为它将 URI 变量视为不透明的数据来进行完全编码,而如果 URI 变量故意包含保留字符,则第二个选项是有用的。当完全不展开 URI 变量时,第二个选项也很有用,因为这也会对任何看起来像 URI 变量的内容进行编码。 + +下面的示例使用了第一个选项: + +Java + +``` +URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}") + .queryParam("q", "{q}") + .encode() + .buildAndExpand("New York", "foo+bar") + .toUri(); + +// Result is "/hotel%20list/New%20York?q=foo%2Bbar" +``` + +Kotlin + +``` +val uri = UriComponentsBuilder.fromPath("/hotel list/{city}") + .queryParam("q", "{q}") + .encode() + .buildAndExpand("New York", "foo+bar") + .toUri() + +// Result is "/hotel%20list/New%20York?q=foo%2Bbar" +``` + +可以通过直接访问 URI(这意味着编码)来缩短前面的示例,如下例所示: + +Java + +``` +URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}") + .queryParam("q", "{q}") + .build("New York", "foo+bar"); +``` + +Kotlin + +``` +val uri = UriComponentsBuilder.fromPath("/hotel list/{city}") + .queryParam("q", "{q}") + .build("New York", "foo+bar") +``` + +可以使用完整的 URI 模板进一步缩短它,如下例所示: + +Java + +``` +URI uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}") + .build("New York", "foo+bar"); +``` + +Kotlin + +``` +val uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}") + .build("New York", "foo+bar") +``` + +`WebClient`和`RestTemplate`通过`UriBuilderFactory`策略在内部扩展和编码 URI 模板。两者都可以使用自定义策略进行配置,如下例所示: + +Java + +``` +String baseUrl = "https://example.com"; +DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl) +factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES); + +// Customize the RestTemplate.. +RestTemplate restTemplate = new RestTemplate(); +restTemplate.setUriTemplateHandler(factory); + +// Customize the WebClient.. +WebClient client = WebClient.builder().uriBuilderFactory(factory).build(); +``` + +Kotlin + +``` +val baseUrl = "https://example.com" +val factory = DefaultUriBuilderFactory(baseUrl).apply { + encodingMode = EncodingMode.TEMPLATE_AND_VALUES +} + +// Customize the RestTemplate.. +val restTemplate = RestTemplate().apply { + uriTemplateHandler = factory +} + +// Customize the WebClient.. +val client = WebClient.builder().uriBuilderFactory(factory).build() +``` + +`DefaultUriBuilderFactory`实现在内部使用`UriComponentsBuilder`来扩展和编码 URI 模板。作为工厂,它提供了一个单独的位置来配置编码方法,该方法基于以下编码模式之一: + +* `TEMPLATE_AND_VALUES`:使用`UriComponentsBuilder#encode()`,对应于前面列表中的第一个选项,对 URI 模板进行预编码,并在展开时对 URI 变量进行严格编码。 + +* `VALUES_ONLY`:不对 URI 模板进行编码,而是在将 URI 变量扩展到模板之前,通过`UriUtils#encodeUriVariables`对 URI 变量进行严格编码。 + +* `URI_COMPONENT`:使用`UriComponents#encode()`,对应于前面列表中的第二个选项,来对 URI 组件值的编码*之后*URI 变量进行扩展。 + +* `NONE`:不应用任何编码。 + +由于历史原因和向后兼容,`RestTemplate`被设置为`EncodingMode.URI_COMPONENT`。`WebClient`依赖于`DefaultUriBuilderFactory`中的默认值,该默认值从 5.0.x 中的`EncodingMode.URI_COMPONENT`更改为 5.1 中的`EncodingMode.TEMPLATE_AND_VALUES`。 + +### 1.7.科尔斯 + +[Web MVC](web.html#mvc-cors) + +Spring WebFlux 允许你处理 CORS(跨源资源共享)。这一节描述了如何做到这一点。 + +#### 1.7.1.导言 + +[Web MVC](web.html#mvc-cors-intro) + +出于安全原因,浏览器禁止对当前来源以外的资源进行 Ajax 调用。例如,你可以在一个标签中设置你的银行帐户,而在另一个标签中设置 Evil.com。来自 Evil.com 的脚本不应该能够使用你的凭据向你的银行 API 发出 Ajax 请求——例如,从你的帐户中取款! + +跨源资源共享是由[most browsers](https://caniuse.com/#feat=cors)实现的[W3C 规范](https://www.w3.org/TR/cors/),它允许你指定授权哪种类型的跨域请求,而不是使用基于 iframe 或 jsonp 的安全性较低、功能较弱的解决方案。 + +#### 1.7.2.处理 + +[Web MVC](web.html#mvc-cors-processing) + +CORS 规范区分了飞行前、简单和实际请求。要了解 CORS 的工作原理,你可以阅读[this article](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)等,或者查看规范以获得更多详细信息。 + +Spring WebFlux`HandlerMapping`实现为 CORS 提供了内置支持。在成功地将一个请求映射到一个处理程序之后,`HandlerMapping`检查 CORS 配置中给定的请求和处理程序,并采取进一步的操作。前置请求是直接处理的,而简单和实际的 CORS 请求是截获、验证的,并设置了所需的 CORS 响应头。 + +为了启用跨源请求(即存在`Origin`头并与请求的主机不同),你需要有一些显式声明的 CORS 配置。如果没有找到匹配的 CORS 配置,则拒绝预航前请求。没有 CORS 头被添加到简单的和实际的 CORS 请求的响应中,因此,浏览器会拒绝它们。 + +每个`HandlerMapping`都可以单独使用基于 URL 模式的[configured](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/reactive/handler/AbstractHandlerMapping.html#setCorsConfigurations-java.util.Map-)映射`CorsConfiguration`。在大多数情况下,应用程序使用 WebFlux Java 配置来声明这样的映射,这将导致一个单一的全局映射传递给所有`HandlerMapping`实现。 + +你可以将`HandlerMapping`级别的全局 CORS 配置与更细粒度的、处理程序级别的 CORS 配置结合起来。例如,带注释的控制器可以使用类或方法级别的`@CrossOrigin`注释(其他处理程序可以实现 `CorsConfigurationSource’)。 + +结合全局和局部配置的规则通常是累加的——例如,所有全局配置和所有局部配置。对于那些只能接受单个值的属性,例如`allowCredentials`和`maxAge`,本地重写全局值。有关更多详细信息,请参见[“CorsConfiguration#Combine”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/cors/CorsConfiguration.html#combine-org.springframework.web.cors.CorsConfiguration-)。 + +| |要从源代码中了解更多信息或进行高级定制,请参见:<br/><br/>*`CorsConfiguration`<br/><br/>*`AbstractHandlerMapping`和<br/><br/>| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.7.3.`@CrossOrigin` + +[Web MVC](web.html#mvc-cors-controller) + +[`@CrossOrigin`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/bind/annotation/CrossOrigin.html)注释允许对带注释的控制器方法进行跨源请求,如下例所示: + +爪哇 + +``` +@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) { + // ... + } +} +``` + +Kotlin + +``` +@RestController +@RequestMapping("/account") +class AccountController { + + @CrossOrigin + @GetMapping("/{id}") + suspend fun retrieve(@PathVariable id: Long): Account { + // ... + } + + @DeleteMapping("/{id}") + suspend fun remove(@PathVariable id: Long) { + // ... + } +} +``` + +默认情况下,`@CrossOrigin`允许: + +* 所有的起源。 + +* 所有标题。 + +* 将控制器方法映射到的所有 HTTP 方法。 + +`allowCredentials`默认情况下不启用,因为这建立了一个信任级别,该级别公开敏感的特定于用户的信息(例如 Cookie 和 CSRF 令牌),并且只应在适当的情况下使用。当启用`allowOrigins`时,要么必须将`allowOrigins`设置为一个或多个特定的域(但不是特殊值`"*"`),要么将`allowOriginPatterns`属性用于匹配到源集的动态。 + +`maxAge`设置为 30 分钟。 + +`@CrossOrigin`在类级别上也受到支持,并被所有方法继承。下面的示例指定了一个特定的域,并将`maxAge`设置为一个小时: + +爪哇 + +``` +@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) { + // ... + } +} +``` + +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) { + // ... + } +} +``` + +可以在类和方法级别上使用`@CrossOrigin`,如下例所示: + +爪哇 + +``` +@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**|在类级别上使用`@CrossOrigin`。| +|-----|-----------------------------------------| +|**2**|在方法级别使用`@CrossOrigin`。| + +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**|在类级别上使用`@CrossOrigin`。| +|-----|-----------------------------------------| +|**2**|在方法级别使用`@CrossOrigin`。| + +#### 1.7.4.全局配置 + +[Web MVC](web.html#mvc-cors-global) + +除了细粒度的控制器方法级配置外,你可能还需要定义一些全局 CORS 配置。你可以在任何`HandlerMapping`上单独设置基于 URL 的`CorsConfiguration`映射。然而,大多数应用程序都使用 WebFlux 爪哇 配置来实现这一点。 + +默认情况下,全局配置启用以下功能: + +* 所有的起源。 + +* 所有标题。 + +* `GET`,`HEAD`,和`POST`方法。 + +`allowedCredentials`默认情况下不启用,因为这建立了一个信任级别,该级别公开敏感的特定于用户的信息(例如 Cookie 和 CSRF 令牌),并且只应在适当的情况下使用。当启用`allowOrigins`时,要么必须将`allowOrigins`设置为一个或多个特定的域(但不是特殊值`"*"`),要么将`allowOriginPatterns`属性可用于匹配到源集的动态。 + +`maxAge`设置为 30 分钟。 + +要在 WebFlux 爪哇 配置中启用 CORS,可以使用`CorsRegistry`回调,如下例所示: + +爪哇 + +``` +@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... + } +} +``` + +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... + } +} +``` + +#### 1.7.5.CORS`WebFilter` + +[Web MVC](web.html#mvc-cors-filter) + +你可以通过内置的[`CorsWebFilter`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/cors/reactive/CorsWebFilter.html)应用 CORS 支持,这非常适合[功能端点](#webflux-fn)。 + +| |如果你试图将`CorsFilter`与 Spring 安全性一起使用,请记住,对于 CORS, Spring <br/>安全性具有[内置支持](https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#cors)。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +要配置过滤器,你可以声明一个`CorsWebFilter` Bean,并将一个 `CorsConfigurationSource’传递给它的构造函数,如下例所示: + +爪哇 + +``` +@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); +} +``` + +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.8.网络安全 + +[Web MVC](web.html#mvc-web-security) + +[Spring Security](https://projects.spring.io/spring-security/)项目提供了保护 Web 应用程序免受恶意攻击的支持。请参阅 Spring 安全参考文档,包括: + +* [WebFlux 安全性](https://docs.spring.io/spring-security/site/docs/current/reference/html5/#jc-webflux) + +* [WebFlux 测试支持](https://docs.spring.io/spring-security/site/docs/current/reference/html5/#test-webflux) + +* [CSRF Protection](https://docs.spring.io/spring-security/site/docs/current/reference/html5/#csrf) + +* [安全响应标头](https://docs.spring.io/spring-security/site/docs/current/reference/html5/#headers) + +### 1.9.查看技术 + +[Web MVC](web.html#mvc-view) + +Spring WebFlux 中对视图技术的使用是可插入的。是否决定使用 ThymeLeaf、FreeMarker 或其他一些视图技术主要是配置更改的问题。本章介绍与 Spring WebFlux 集成的视图技术。我们假设你已经熟悉[View Resolution](#webflux-viewresolution)。 + +#### 1.9.1.百里香叶 + +[Web MVC](web.html#mvc-view-thymeleaf) + +ThymeLeaf 是一个现代的服务器端 爪哇 模板引擎,强调自然的 HTML 模板,可以通过双击在浏览器中预览,这对于在 UI 模板上独立工作(例如,由设计师)非常有帮助,而不需要运行的服务器。Thymeleaf 提供了一套广泛的功能,并且它是积极开发和维护的。有关更完整的介绍,请参见[Thymeleaf](https://www.thymeleaf.org/)项目主页。 + +ThymeLeaf 与 Spring WebFlux 的集成由 ThymeLeaf 项目管理。配置涉及几个声明,例如“SpringResourceTemPlateResolver”、`SpringWebFluxTemplateEngine`和“ThymeLeafReactiveViewResolver”。有关更多详细信息,请参见[Thymeleaf+Spring](https://www.thymeleaf.org/documentation.html)和 WebFlux 集成[announcement](http://forum.thymeleaf.org/Thymeleaf-3-0-8-JUST-PUBLISHED-td4030687.html)。 + +#### 1.9.2.自由标记 + +[Web MVC](web.html#mvc-view-freemarker) + +[Apache Freemarker](https://freemarker.apache.org/)是一个模板引擎,用于生成从 HTML 到电子邮件等任何类型的文本输出。 Spring 框架具有用于使用 Spring WebFlux 和 Freemarker 模板的内置集成。 + +##### 视图配置 + +[Web MVC](web.html#mvc-view-freemarker-contextconfig) + +下面的示例展示了如何将 Freemarker 配置为一种视图技术: + +爪哇 + +``` +@Configuration +@EnableWebFlux +public class WebConfig implements WebFluxConfigurer { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.freeMarker(); + } + + // Configure FreeMarker... + + @Bean + public FreeMarkerConfigurer freeMarkerConfigurer() { + FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); + configurer.setTemplateLoaderPath("classpath:/templates/freemarker"); + return configurer; + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebFlux +class WebConfig : WebFluxConfigurer { + + override fun configureViewResolvers(registry: ViewResolverRegistry) { + registry.freeMarker() + } + + // Configure FreeMarker... + + @Bean + fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply { + setTemplateLoaderPath("classpath:/templates/freemarker") + } +} +``` + +你的模板需要存储在`FreeMarkerConfigurer`指定的目录中,如前面的示例所示。给定上述配置,如果你的控制器返回视图名`welcome`,则解析器将查找 ` Classpath:/templates/freemarker/welcome.ftl`template。 + +##### 自由标记配置 + +[Web MVC](web.html#mvc-views-freemarker) + +通过在`FreeMarkerConfigurer` Bean 上设置适当的 Bean 属性,可以将自由标记的“设置”和“SharedVariets”直接传递给自由标记的“配置”对象(由 Spring 管理)。`freemarkerSettings`属性需要一个`java.util.Properties`对象,而`freemarkerVariables`属性需要一个 `java.util.map’。下面的示例展示了如何使用`FreeMarkerConfigurer`: + +爪哇 + +``` +@Configuration +@EnableWebFlux +public class WebConfig implements WebFluxConfigurer { + + // ... + + @Bean + public FreeMarkerConfigurer freeMarkerConfigurer() { + Map<String, Object> variables = new HashMap<>(); + variables.put("xml_escape", new XmlEscape()); + + FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); + configurer.setTemplateLoaderPath("classpath:/templates"); + configurer.setFreemarkerVariables(variables); + return configurer; + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebFlux +class WebConfig : WebFluxConfigurer { + + // ... + + @Bean + fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply { + setTemplateLoaderPath("classpath:/templates") + setFreemarkerVariables(mapOf("xml_escape" to XmlEscape())) + } +} +``` + +有关应用于`Configuration`对象的设置和变量的详细信息,请参见 Freemarker 文档。 + +##### 表单处理 + +[Web MVC](web.html#mvc-view-freemarker-forms) + +Spring 提供了在 JSP 中使用的标记库,其中包括一个 `<spring:bind/>` 元素。这个元素主要允许表单显示来自表单支持对象的值,并显示来自 Web 或业务层中`Validator`的失败验证的结果。 Spring 在 Freemarker 中还具有对相同功能的支持,具有用于生成表单输入元素本身的附加方便宏。 + +##### BIND 宏 # + +[Web MVC](web.html#mvc-view-bind-macros) + +在`spring-webflux.jar`freemarker 文件中维护了一组标准的宏,因此对于适当配置的应用程序,它们总是可用的。 + +Spring 模板库中定义的一些宏被认为是内部的(私有的),但是在宏定义中不存在这样的范围定义,这使得所有的宏对于调用代码和用户模板都是可见的。下面的部分只关注你需要从模板中直接调用的宏。如果你希望直接查看宏代码,那么该文件名为`spring.ftl`,位于 `org.springframework.web.reactive.result.view.freemarker`package 中。 + +有关绑定支持的更多详细信息,请参见[Simple Binding](web.html#mvc-view-simple-binding)中的 Spring MVC。 + +##### 表格宏 # + +有关 Spring 对自由标记模板的表单宏支持的详细信息,请参阅 Spring MVC 文档的以下部分。 + +* [Input Macros](web.html#mvc-views-form-macros) + +* [Input Fields](web.html#mvc-views-form-macros-input) + +* [选择字段](web.html#mvc-views-form-macros-select) + +* [HTML Escaping](web.html#mvc-views-form-macros-html-escaping) + +#### 1.9.3.脚本视图 + +[Web MVC](web.html#mvc-view-script) + +Spring 框架有一个内置的集成,用于使用 Spring WebFlux 和任何模板库,这些模板库可以在[JSR-223](https://www.jcp.org/en/jsr/detail?id=223)爪哇 脚本引擎之上运行。下表显示了我们在不同的脚本引擎上测试过的模板库: + +|脚本库| Scripting Engine | +|----------------------------------------------------------------------------------|-----------------------------------------------------| +|[Handlebars](https://handlebarsjs.com/)|[Nashorn](https://openjdk.java.net/projects/nashorn/)| +|[Mustache](https://mustache.github.io/)|[Nashorn](https://openjdk.java.net/projects/nashorn/)| +|[React](https://facebook.github.io/react/)|[Nashorn](https://openjdk.java.net/projects/nashorn/)| +|[EJS](https://www.embeddedjs.com/)|[Nashorn](https://openjdk.java.net/projects/nashorn/)| +|[ERB](https://www.stuartellis.name/articles/erb/)| [JRuby](https://www.jruby.org) | +|[字符串模板](https://docs.python.org/2/library/string.html#template-strings)| [Jython](https://www.jython.org/) | +|[Kotlin Script templating](https://github.com/sdeleuze/kotlin-script-templating)| [Kotlin](https://kotlinlang.org/) | + +| |集成任何其他脚本引擎的基本规则是,它必须实现“ScriptEngine”和`Invocable`接口。| +|---|------------------------------------------------------------------------------------------------------------------------------| + +##### 所需经费 + +[Web MVC](web.html#mvc-view-script-dependencies) + +你需要在 Classpath 上安装脚本引擎,其细节因脚本引擎而异: + +* [Nashorn](https://openjdk.java.net/projects/nashorn/)爪哇Script 引擎由 爪哇8+ 提供。强烈推荐使用最新的可用更新版本。 + +* [JRuby](https://www.jruby.org)应该作为 Ruby 支持的依赖项添加。 + +* [Jython](https://www.jython.org)应该作为 Python 支持的依赖项添加。 + +* 对于 Kotlin 脚本支持,应该添加`org.jetbrains.kotlin:kotlin-script-util`依赖项和一个`META-INF/services/javax.script.ScriptEngineFactory`文件,该文件包含`org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory`行。有关更多详细信息,请参见[this example](https://github.com/sdeleuze/kotlin-script-templating)。 + +你需要有脚本模板库。实现 爪哇Script 的一种方法是通过[WebJars](https://www.webjars.org/)。 + +##### 脚本模板 + +[Web MVC](web.html#mvc-view-script-integrate) + +你可以声明一个`ScriptTemplateConfigurer` Bean 来指定要使用的脚本引擎、要加载的脚本文件、调用什么函数来呈现模板,等等。下面的示例使用了 Mustache 模板和 Nashorn 爪哇Script 引擎: + +爪哇 + +``` +@Configuration +@EnableWebFlux +public class WebConfig implements WebFluxConfigurer { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.scriptTemplate(); + } + + @Bean + public ScriptTemplateConfigurer configurer() { + ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer(); + configurer.setEngineName("nashorn"); + configurer.setScripts("mustache.js"); + configurer.setRenderObject("Mustache"); + configurer.setRenderFunction("render"); + return configurer; + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebFlux +class WebConfig : WebFluxConfigurer { + + override fun configureViewResolvers(registry: ViewResolverRegistry) { + registry.scriptTemplate() + } + + @Bean + fun configurer() = ScriptTemplateConfigurer().apply { + engineName = "nashorn" + setScripts("mustache.js") + renderObject = "Mustache" + renderFunction = "render" + } +} +``` + +使用以下参数调用`render`函数: + +* `String template`:模板内容 + +* `Map model`:视图模型 + +* `RenderingContext renderingContext`:[“渲染环境”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/servlet/view/script/RenderingContext.html)提供对应用程序上下文、区域设置、模板加载器和 URL 的访问权限(自 5.0 起) + +`Mustache.render()`与该签名在本机上兼容,因此你可以直接调用它。 + +如果模板技术需要进行一些定制,那么可以提供一个实现定制呈现功能的脚本。例如,[Handlerbars](https://handlebarsjs.com)在使用模板之前需要对其进行编译,并且需要[polyfill](https://en.wikipedia.org/wiki/Polyfill),以便模拟服务器端脚本引擎中不可用的一些浏览器功能。下面的示例展示了如何设置自定义呈现函数: + +爪哇 + +``` +@Configuration +@EnableWebFlux +public class WebConfig implements WebFluxConfigurer { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.scriptTemplate(); + } + + @Bean + public ScriptTemplateConfigurer configurer() { + ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer(); + configurer.setEngineName("nashorn"); + configurer.setScripts("polyfill.js", "handlebars.js", "render.js"); + configurer.setRenderFunction("render"); + configurer.setSharedEngine(false); + return configurer; + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebFlux +class WebConfig : WebFluxConfigurer { + + override fun configureViewResolvers(registry: ViewResolverRegistry) { + registry.scriptTemplate() + } + + @Bean + fun configurer() = ScriptTemplateConfigurer().apply { + engineName = "nashorn" + setScripts("polyfill.js", "handlebars.js", "render.js") + renderFunction = "render" + isSharedEngine = false + } +} +``` + +| |当使用非线程安全的<br/>脚本引擎时,需要将`sharedEngine`属性设置为`false`,该脚本引擎的模板库不是为并发而设计的,例如在 Nashorn 上运行的手柄或<br/>React。在那种情况下,由于[this bug](https://bugs.openjdk.java.net/browse/JDK-8076099),爪哇 SE8Update60 是必需的,但是一般情况下<br/>推荐在任何情况下使用最近发布的 爪哇 SE 补丁。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +`polyfill.js`只定义了处理栏正常运行所需的`window`对象,如以下代码片段所示: + +``` +var window = {}; +``` + +这个基本的`render.js`实现在使用模板之前对模板进行编译。生产就绪的实现还应该存储和重用缓存的模板或预编译的模板。这可以在脚本端完成,也可以在你需要的任何定制中完成(例如,管理模板引擎配置)。下面的示例展示了如何编译模板: + +``` +function render(template, model) { + var compiledTemplate = Handlebars.compile(template); + return compiledTemplate(model); +} +``` + +查看 Spring Framework Unit 测试,[爪哇](https://github.com/spring-projects/spring-framework/tree/main/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/script)和[resources](https://github.com/spring-projects/spring-framework/tree/main/spring-webflux/src/test/resources/org/springframework/web/reactive/result/view/script),以获得更多配置示例。 + +#### 1.9.4.JSON 和 XML + +[Web MVC](web.html#mvc-view-jackson) + +出于[内容协商](#webflux-multiple-representations)的目的,可以根据客户机请求的内容类型,在使用 HTML 模板或其他格式(例如 JSON 或 XML)呈现模型之间进行替换,这是非常有用的。为了支持这样做, Spring WebFlux 提供了`HttpMessageWriterView`,你可以使用它插入来自`spring-web`的任何可用的[Codecs](#webflux-codecs),例如`Jackson2JsonEncoder`,`Jackson2SmileEncoder`,或`Jaxb2XmlEncoder`。 + +与其他视图技术不同,`HttpMessageWriterView`不需要`ViewResolver`,而是将[configured](#webflux-config-view-resolvers)作为默认视图。你可以配置一个或多个这样的默认视图,包装不同的`HttpMessageWriter`实例或`Encoder`实例。在运行时使用与请求的内容类型匹配的内容类型。 + +在大多数情况下,一个模型包含多个属性。要确定要序列化哪个,你可以配置`HttpMessageWriterView`,并使用要用于呈现的 model 属性的名称。如果模型只包含一个属性,则使用该属性。 + +### 1.10.HTTP 缓存 + +[Web MVC](web.html#mvc-caching) + +HTTP 缓存可以显著提高 Web 应用程序的性能。HTTP 缓存围绕`Cache-Control`响应头和后续的条件请求头,例如`Last-Modified`和`ETag`。`Cache-Control`建议私有(例如,浏览器)和公共(例如,代理)缓存如何缓存和重用响应。`ETag`报头用于发出条件请求,如果内容没有更改,则该请求可能会导致没有正文的 304(未 \_modified)。`ETag`可以看作是`Last-Modified`标头的更复杂的继承者。 + +本节描述了 Spring WebFlux 中可用的 HTTP 缓存相关选项。 + +#### 1.10.1.`CacheControl` + +[Web MVC](web.html#mvc-caching-cachecontrol) + +[`CacheControl`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/http/CacheControl.html)提供了对配置与`Cache-Control`头相关的设置的支持,并在许多地方被接受为参数: + +* [Controllers](#webflux-caching-etag-lastmodified) + +* [静态资源](#webflux-caching-static-resources) + +虽然[RFC 7234](https://tools.ietf.org/html/rfc7234#section-5.2.2)描述了`Cache-Control`响应头的所有可能的指令,但`CacheControl`类型采用了一种面向用例的方法,该方法专注于常见的场景,如下例所示: + +爪哇 + +``` +// Cache for an hour - "Cache-Control: max-age=3600" +CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS); + +// Prevent caching - "Cache-Control: no-store" +CacheControl ccNoStore = CacheControl.noStore(); + +// Cache for ten days in public and private caches, +// public caches should not transform the response +// "Cache-Control: max-age=864000, public, no-transform" +CacheControl ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic(); +``` + +Kotlin + +``` +// Cache for an hour - "Cache-Control: max-age=3600" +val ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS) + +// Prevent caching - "Cache-Control: no-store" +val ccNoStore = CacheControl.noStore() + +// Cache for ten days in public and private caches, +// public caches should not transform the response +// "Cache-Control: max-age=864000, public, no-transform" +val ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic() +``` + +#### 1.10.2.控制器 + +[Web MVC](web.html#mvc-caching-etag-lastmodified) + +控制器可以添加对 HTTP 缓存的显式支持。我们建议这样做,因为资源的“lastmodified”或`ETag`值需要在与条件请求头进行比较之前进行计算。控制器可以将`ETag`和`Cache-Control`设置添加到`ResponseEntity`中,如下例所示: + +爪哇 + +``` +@GetMapping("/book/{id}") +public ResponseEntity<Book> showBook(@PathVariable Long id) { + + Book book = findBook(id); + String version = book.getVersion(); + + return ResponseEntity + .ok() + .cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS)) + .eTag(version) // lastModified is also available + .body(book); +} +``` + +Kotlin + +``` +@GetMapping("/book/{id}") +fun showBook(@PathVariable id: Long): ResponseEntity<Book> { + + val book = findBook(id) + val version = book.getVersion() + + return ResponseEntity + .ok() + .cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS)) + .eTag(version) // lastModified is also available + .body(book) +} +``` + +如果与条件请求标题的比较表明内容没有更改,则前面的示例发送带有空主体的 304(未 \_modified)响应。否则,`ETag’和`Cache-Control`头将被添加到响应中。 + +你还可以检查控制器中的条件请求头,如下例所示: + +爪哇 + +``` +@RequestMapping +public String myHandleMethod(ServerWebExchange exchange, Model model) { + + long eTag = ... (1) + + if (exchange.checkNotModified(eTag)) { + return null; (2) + } + + model.addAttribute(...); (3) + return "myViewName"; +} +``` + +|**1**|应用程序特定的计算。| +|-----|--------------------------------------------------------------------| +|**2**|响应已设置为 304(未修改)。没有进一步的处理。| +|**3**|继续处理请求。| + +Kotlin + +``` +@RequestMapping +fun myHandleMethod(exchange: ServerWebExchange, model: Model): String? { + + val eTag: Long = ... (1) + + if (exchange.checkNotModified(eTag)) { + return null(2) + } + + model.addAttribute(...) (3) + return "myViewName" +} +``` + +|**1**|应用程序特定的计算。| +|-----|--------------------------------------------------------------------| +|**2**|响应已设置为 304(未修改)。没有进一步的处理。| +|**3**|继续处理请求。| + +针对`eTag`值、`lastModified`值或两者检查条件请求有三种变体。对于条件`GET`和`HEAD`请求,可以将响应设置为 304(不是 \_modified)。对于条件`POST`、`PUT`和`DELETE`,可以将响应设置为 412(前提条件 \_ 失败),以防止并发修改。 + +#### 1.10.3.静态资源 + +[Web MVC](web.html#mvc-caching-static-resources) + +为了获得最佳性能,你应该使用`Cache-Control`和条件响应头来服务静态资源。参见关于配置[静态资源](#webflux-config-static-resources)的部分。 + +### 1.11.WebFlux 配置 + +[Web MVC](web.html#mvc-config) + +WebFlux 爪哇 配置声明了用带注释的控制器或功能端点处理请求所需的组件,并提供了一个 API 来定制配置。这意味着你不需要理解由 爪哇 配置创建的底层 bean。但是,如果你想了解它们,可以在`WebFluxConfigurationSupport`中看到它们,或者在[Special Bean Types](#webflux-special-bean-types)中阅读有关它们的更多信息。 + +对于配置 API 中没有的更高级的定制,你可以通过[高级配置模式](#webflux-config-advanced-java)获得对配置的完全控制。 + +#### 1.11.1.启用 WebFlux 配置 + +[Web MVC](web.html#mvc-config-enable) + +你可以在 爪哇 配置中使用`@EnableWebFlux`注释,如下例所示: + +爪哇 + +``` +@Configuration +@EnableWebFlux +public class WebConfig { +} +``` + +Kotlin + +``` +@Configuration +@EnableWebFlux +class WebConfig +``` + +前面的示例注册了 Spring WebFlux[基础设施 bean](#webflux-special-bean-types)的数量,并适应了 Classpath 上可用的依赖关系——对于 JSON、XML 和其他的依赖关系。 + +#### 1.11.2.WebFlux 配置 API + +[Web MVC](web.html#mvc-config-customize) + +在你的 爪哇 配置中,你可以实现`WebFluxConfigurer`接口,如下例所示: + +爪哇 + +``` +@Configuration +@EnableWebFlux +public class WebConfig implements WebFluxConfigurer { + + // Implement configuration methods... +} +``` + +Kotlin + +``` +@Configuration +@EnableWebFlux +class WebConfig : WebFluxConfigurer { + + // Implement configuration methods... +} +``` + +#### 1.11.3.转换、格式化 + +[Web MVC](web.html#mvc-config-conversion) + +默认情况下,安装了用于各种数字和日期类型的格式化程序,并支持在字段上通过`@NumberFormat`和`@DateTimeFormat`进行定制。 + +要在 爪哇 Config 中注册自定义格式化程序和转换器,请使用以下方法: + +爪哇 + +``` +@Configuration +@EnableWebFlux +public class WebConfig implements WebFluxConfigurer { + + @Override + public void addFormatters(FormatterRegistry registry) { + // ... + } + +} +``` + +Kotlin + +``` +@Configuration +@EnableWebFlux +class WebConfig : WebFluxConfigurer { + + override fun addFormatters(registry: FormatterRegistry) { + // ... + } +} +``` + +Spring 默认情况下,WebFlux 在解析和格式化日期值时会考虑请求区域设置。这适用于将日期表示为带有“输入”窗体字段的字符串的窗体。但是,对于“日期”和“时间”表单字段,浏览器使用 HTML 规范中定义的固定格式。对于这种情况,日期和时间格式可以定制如下: + +爪哇 + +``` +@Configuration +@EnableWebFlux +public class WebConfig implements WebFluxConfigurer { + + @Override + public void addFormatters(FormatterRegistry registry) { + DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); + registrar.setUseIsoFormat(true); + registrar.registerFormatters(registry); + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebFlux +class WebConfig : WebFluxConfigurer { + + override fun addFormatters(registry: FormatterRegistry) { + val registrar = DateTimeFormatterRegistrar() + registrar.setUseIsoFormat(true) + registrar.registerFormatters(registry) + } +} +``` + +| |有关何时<br/>使用`FormatterRegistrar`实现的更多信息,请参见[`FormatterRegistrator’SPI](core.html#format-FormatterRegistrar-SPI)和`FormattingConversionServiceFactoryBean`。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.11.4.验证 + +[Web MVC](web.html#mvc-config-validation) + +默认情况下,如果[Bean Validation](core.html#validation-beanvalidation-overview)存在于 Classpath(例如, Hibernate 验证器)上,则`LocalValidatorFactoryBean`注册为全局[validator](core.html#validator),用于`@Valid`和`@Controller`方法参数上的 `@validated’。 + +在你的 爪哇 配置中,你可以自定义全局`Validator`实例,如下例所示: + +爪哇 + +``` +@Configuration +@EnableWebFlux +public class WebConfig implements WebFluxConfigurer { + + @Override + public Validator getValidator() { + // ... + } + +} +``` + +Kotlin + +``` +@Configuration +@EnableWebFlux +class WebConfig : WebFluxConfigurer { + + override fun getValidator(): Validator { + // ... + } + +} +``` + +请注意,你也可以在本地注册`Validator`实现,如下例所示: + +爪哇 + +``` +@Controller +public class MyController { + + @InitBinder + protected void initBinder(WebDataBinder binder) { + binder.addValidators(new FooValidator()); + } + +} +``` + +Kotlin + +``` +@Controller +class MyController { + + @InitBinder + protected fun initBinder(binder: WebDataBinder) { + binder.addValidators(FooValidator()) + } +} +``` + +| |如果需要在某个地方注入一个`LocalValidatorFactoryBean`,请创建一个 Bean 和<br/>,将其标记为`@Primary`,以避免与 MVC 配置中声明的那个冲突。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.11.5.内容类型解析器 + +[Web MVC](web.html#mvc-config-content-negotiation) + +你可以配置 Spring WebFlux 如何从请求中确定“@Controller”实例所请求的媒体类型。默认情况下,只检查`Accept`头,但你也可以启用基于查询参数的策略。 + +下面的示例展示了如何自定义所请求的内容类型分辨率: + +爪哇 + +``` +@Configuration +@EnableWebFlux +public class WebConfig implements WebFluxConfigurer { + + @Override + public void configureContentTypeResolver(RequestedContentTypeResolverBuilder builder) { + // ... + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebFlux +class WebConfig : WebFluxConfigurer { + + override fun configureContentTypeResolver(builder: RequestedContentTypeResolverBuilder) { + // ... + } +} +``` + +#### 1.11.6.HTTP 消息编解码器 + +[Web MVC](web.html#mvc-config-message-converters) + +下面的示例展示了如何自定义如何读取和写入请求和响应主体: + +爪哇 + +``` +@Configuration +@EnableWebFlux +public class WebConfig implements WebFluxConfigurer { + + @Override + public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) { + configurer.defaultCodecs().maxInMemorySize(512 * 1024); + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebFlux +class WebConfig : WebFluxConfigurer { + + override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) { + // ... + } +} +``` + +`ServerCodecConfigurer`提供了一组默认的读取器和编写器。你可以使用它来添加更多的读取器和编写器,自定义缺省的读取器和编写器,或者完全替换缺省的读取器和编写器。 + +对于 Jackson 的 JSON 和 XML,可以考虑使用[“Jackson2ObjectMapPerBuilder”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.html),它使用以下属性定制 Jackson 的默认属性: + +* [反序列化 feature.fail_on_unknown_properties’](https://fasterxml.github.io/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/DeserializationFeature.html#FAIL_ON_UNKNOWN_PROPERTIES)已禁用。 + +* [mapperfeature.default_view_inclusion](https://fasterxml.github.io/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/MapperFeature.html#DEFAULT_VIEW_INCLUSION)已禁用。 + +如果在 Classpath 上检测到以下已知模块,它还会自动注册这些模块: + +* [Jackson-数据类型-Joda](https://github.com/FasterXML/jackson-datatype-joda):支持 Joda-time 类型。 + +* [Jackson-数据类型-JSR310](https://github.com/FasterXML/jackson-datatype-jsr310):支持 爪哇8 日期和时间 API 类型。 + +* [Jackson-数据类型-JDK8’](https://github.com/FasterXML/jackson-datatype-jdk8):支持其他 爪哇8 类型,例如`Optional`。 + +* [`jackson-module-kotlin`](https://github.com/FasterXML/jackson-module-kotlin):支持 Kotlin 类和数据类。 + +#### 1.11.7.视图解析器 + +[Web MVC](web.html#mvc-config-view-resolvers) + +下面的示例展示了如何配置视图分辨率: + +爪哇 + +``` +@Configuration +@EnableWebFlux +public class WebConfig implements WebFluxConfigurer { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + // ... + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebFlux +class WebConfig : WebFluxConfigurer { + + override fun configureViewResolvers(registry: ViewResolverRegistry) { + // ... + } +} +``` + +`ViewResolverRegistry`具有 Spring 框架与之集成的视图技术的快捷方式。下面的示例使用 Freemarker(这也需要配置底层的 Freemarker 视图技术): + +爪哇 + +``` +@Configuration +@EnableWebFlux +public class WebConfig implements WebFluxConfigurer { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.freeMarker(); + } + + // Configure Freemarker... + + @Bean + public FreeMarkerConfigurer freeMarkerConfigurer() { + FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); + configurer.setTemplateLoaderPath("classpath:/templates"); + return configurer; + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebFlux +class WebConfig : WebFluxConfigurer { + + override fun configureViewResolvers(registry: ViewResolverRegistry) { + registry.freeMarker() + } + + // Configure Freemarker... + + @Bean + fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply { + setTemplateLoaderPath("classpath:/templates") + } +} +``` + +你还可以插入任何`ViewResolver`实现,如下例所示: + +爪哇 + +``` +@Configuration +@EnableWebFlux +public class WebConfig implements WebFluxConfigurer { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + ViewResolver resolver = ... ; + registry.viewResolver(resolver); + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebFlux +class WebConfig : WebFluxConfigurer { + + override fun configureViewResolvers(registry: ViewResolverRegistry) { + val resolver: ViewResolver = ... + registry.viewResolver(resolver + } +} +``` + +为了支持[内容协商](#webflux-multiple-representations)和通过视图分辨率呈现其他格式(除了 HTML),你可以基于`HttpMessageWriterView`实现配置一个或多个默认视图,该实现接受来自`spring-web`的任何可用的[Codecs](#webflux-codecs)。下面的示例展示了如何做到这一点: + +爪哇 + +``` +@Configuration +@EnableWebFlux +public class WebConfig implements WebFluxConfigurer { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.freeMarker(); + + Jackson2JsonEncoder encoder = new Jackson2JsonEncoder(); + registry.defaultViews(new HttpMessageWriterView(encoder)); + } + + // ... +} +``` + +Kotlin + +``` +@Configuration +@EnableWebFlux +class WebConfig : WebFluxConfigurer { + + override fun configureViewResolvers(registry: ViewResolverRegistry) { + registry.freeMarker() + + val encoder = Jackson2JsonEncoder() + registry.defaultViews(HttpMessageWriterView(encoder)) + } + + // ... +} +``` + +有关与 Spring WebFlux 集成的视图技术的更多信息,请参见[查看技术](#webflux-view)。 + +#### 1.11.8.静态资源 + +[Web MVC](web.html#mvc-config-static-resources) + +此选项提供了一种方便的方式来从基于[`Resource`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/core/io/Resource.html)的位置列表中提供静态资源。 + +在下一个示例中,给定一个以`/resources`开头的请求,相对路径用于在 Classpath 上查找和服务相对于`/static`的静态资源。资源将在一年后到期,以确保最大程度地使用浏览器缓存并减少浏览器发出的 HTTP 请求。还计算`Last-Modified`头,如果存在,则返回`304`状态代码。下面的列表显示了该示例: + +爪哇 + +``` +@Configuration +@EnableWebFlux +public class WebConfig implements WebFluxConfigurer { + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/resources/**") + .addResourceLocations("/public", "classpath:/static/") + .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS)); + } + +} +``` + +Kotlin + +``` +@Configuration +@EnableWebFlux +class WebConfig : WebFluxConfigurer { + + override fun addResourceHandlers(registry: ResourceHandlerRegistry) { + registry.addResourceHandler("/resources/**") + .addResourceLocations("/public", "classpath:/static/") + .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS)) + } +} +``` + +资源处理程序还支持[“资源解决者”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/reactive/resource/ResourceResolver.html)实现和[“资源转换器”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/reactive/resource/ResourceTransformer.html)实现的链,它们可用于创建用于使用优化资源的工具链。 + +你可以使用`VersionResourceResolver`来实现基于内容、固定应用程序版本或其他信息计算的 MD5 散列的版本化资源 URL。“ContentVersionStrategy”(MD5 散列)是一个很好的选择,但有一些明显的例外(例如与模块加载程序一起使用的 爪哇Script 资源)。 + +下面的示例展示了如何在 爪哇 配置中使用`VersionResourceResolver`: + +爪哇 + +``` +@Configuration +@EnableWebFlux +public class WebConfig implements WebFluxConfigurer { + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/resources/**") + .addResourceLocations("/public/") + .resourceChain(true) + .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**")); + } + +} +``` + +Kotlin + +``` +@Configuration +@EnableWebFlux +class WebConfig : WebFluxConfigurer { + + override fun addResourceHandlers(registry: ResourceHandlerRegistry) { + registry.addResourceHandler("/resources/**") + .addResourceLocations("/public/") + .resourceChain(true) + .addResolver(VersionResourceResolver().addContentVersionStrategy("/**")) + } + +} +``` + +你可以使用`ResourceUrlProvider`重写 URL,并应用完整的解析器和变压器链(例如,用于插入版本)。WebFlux 配置提供了`ResourceUrlProvider`,以便可以将其注入到其他配置中。 + +Spring 与 MVC 不同,目前,在 WebFlux 中,没有透明地重写静态资源 URL 的方法,因为没有视图技术可以利用解析器和转换器的非阻塞链来实现。当只提供本地资源时,解决方法是直接使用“ResourceUrlProvider”(例如,通过自定义元素)和块。 + +请注意,当同时使用`EncodedResourceResolver`(例如,gzip,Brotli 编码)和 `VersionedResourceResolver’时,它们必须按该顺序注册,以确保始终根据未编码的文件可靠地计算基于内容的版本。 + +[WebJars](https://www.webjars.org/documentation)还通过 Classpath 上的 `org.webjars:webjars-locator-core’库自动注册的 `WebjarsResourceResolver’提供支持。该解析器可以重写 URL 以包括 jar 的版本,也可以匹配没有版本的传入 URL——例如,从`/jquery/jquery.min.js`到 `/jquery/1.2.0/jquery.min.js`。 + +| |基于`ResourceHandlerRegistry`的 爪哇 配置为细粒度控制提供了进一步的选项<br/>,例如,上次修改行为和优化的资源解析。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.11.9.路径匹配 + +[Web MVC](web.html#mvc-config-path-matching) + +你可以自定义与路径匹配相关的选项。有关单个选项的详细信息,请参见[“PathMatchMatchProfigurer”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/reactive/config/PathMatchConfigurer.html)爪哇doc。下面的示例展示了如何使用`PathMatchConfigurer`: + +爪哇 + +``` +@Configuration +@EnableWebFlux +public class WebConfig implements WebFluxConfigurer { + + @Override + public void configurePathMatch(PathMatchConfigurer configurer) { + configurer + .setUseCaseSensitiveMatch(true) + .setUseTrailingSlashMatch(false) + .addPathPrefix("/api", + HandlerTypePredicate.forAnnotation(RestController.class)); + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebFlux +class WebConfig : WebFluxConfigurer { + + @Override + fun configurePathMatch(configurer: PathMatchConfigurer) { + configurer + .setUseCaseSensitiveMatch(true) + .setUseTrailingSlashMatch(false) + .addPathPrefix("/api", + HandlerTypePredicate.forAnnotation(RestController::class.java)) + } +} +``` + +| |Spring WebFlux 依赖于被称为 `RequestPath’的请求路径的解析表示,用于访问已解码的路径段值,并删除分号内容<br/>(即路径或矩阵变量)。这意味着,与 Spring MVC 不同,你不需要指示<br/>是否要对请求路径进行解码,也不需要指示是否要出于<br/>路径匹配的目的删除分号内容。<br/><br/> Spring WebFlux 也不支持后缀模式匹配,这与 Spring MVC 不同,其中我们<br/>也是[recommend](web.html#mvc-ann-requestmapping-suffix-pattern-match)远离对它的依赖。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.11.10.WebSocketService + +WebFlux 爪哇 Config 声明了一个`WebSocketHandlerAdapter` Bean,它为 WebSocket 处理程序的调用提供了支持。这意味着,要处理 WebSocket 握手请求,剩下的所有工作就是通过`WebSocketHandler`将`SimpleUrlHandlerMapping`映射到一个 URL。 + +在某些情况下,可能有必要创建带有所提供的`WebSocketHandlerAdapter` Bean 的`WebSocketService`服务,该服务允许配置 WebSocket 服务器属性。例如: + +爪哇 + +``` +@Configuration +@EnableWebFlux +public class WebConfig implements WebFluxConfigurer { + + @Override + public WebSocketService getWebSocketService() { + TomcatRequestUpgradeStrategy strategy = new TomcatRequestUpgradeStrategy(); + strategy.setMaxSessionIdleTimeout(0L); + return new HandshakeWebSocketService(strategy); + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebFlux +class WebConfig : WebFluxConfigurer { + + @Override + fun webSocketService(): WebSocketService { + val strategy = TomcatRequestUpgradeStrategy().apply { + setMaxSessionIdleTimeout(0L) + } + return HandshakeWebSocketService(strategy) + } +} +``` + +#### 1.11.11.高级配置模式 + +[Web MVC](web.html#mvc-config-advanced-java) + +`@EnableWebFlux`imports`DelegatingWebFluxConfiguration`表示: + +* 为 WebFlux 应用程序提供默认的 Spring 配置 + +* 检测并委托`WebFluxConfigurer`实现来定制该配置。 + +对于高级模式,你可以删除`@EnableWebFlux`,并直接从 `delegatingwebfluxconfiguration’进行扩展,而不是实现`WebFluxConfigurer`,如下例所示: + +爪哇 + +``` +@Configuration +public class WebConfig extends DelegatingWebFluxConfiguration { + + // ... +} +``` + +Kotlin + +``` +@Configuration +class WebConfig : DelegatingWebFluxConfiguration { + + // ... +} +``` + +你可以在`WebConfig`中保留现有的方法,但是你现在也可以重写 Bean 来自基类的声明,并且在 Classpath 上仍然具有任何数量的其他`WebMvcConfigurer`实现。 + +### 1.12.http/2 + +[Web MVC](web.html#mvc-http2) + +Tomcat、 Jetty 和 Undertow 支持 HTTP/2。但是,有一些与服务器配置相关的考虑因素。有关更多详细信息,请参见[HTTP/2Wiki 页面](https://github.com/spring-projects/spring-framework/wiki/HTTP-2-support)。 + +## 2. WebClient + +Spring WebFlux 包括用于执行 HTTP 请求的客户端。`WebClient`具有功能强大的、基于 reactor 的 Fluent API,参见[反应库](#webflux-reactive-libraries),它实现了异步逻辑的声明式组合,而不需要处理线程或并发性。它是完全非阻塞的,它支持流媒体,并且依赖同样的[codecs](#webflux-codecs),这些也用于在服务器端对请求和响应内容进行编码和解码。 + +`WebClient`需要一个 HTTP 客户库来执行请求。以下是内置的支持: + +* [Reactor Netty](https://github.com/reactor/reactor-netty) + +* [Jetty Reactive HttpClient](https://github.com/jetty-project/jetty-reactive-httpclient) + +* [Apache HttpComponents](https://hc.apache.org/index.html) + +* 其他的可以通过`ClientHttpConnector`进行插接。 + +### 2.1.配置 + +创建`WebClient`的最简单方法是通过一种静态工厂方法: + +* `WebClient.create()` + +* `WebClient.create(String baseUrl)` + +你还可以使用`WebClient.builder()`和其他选项: + +* `uriBuilderFactory`:定制`UriBuilderFactory`用作基本 URL。 + +* `defaultUriVariables`:展开 URI 模板时要使用的默认值。 + +* `defaultHeader`:每个请求的标题。 + +* `defaultCookie`:每个请求都有 cookies。 + +* `defaultRequest`:`Consumer`来定制每个请求。 + +* `filter`:每个请求的客户端过滤器。 + +* `exchangeStrategies`:HTTP 消息阅读器/Writer 自定义。 + +* `clientConnector`:http 客户库设置。 + +例如: + +爪哇 + +``` +WebClient client = WebClient.builder() + .codecs(configurer -> ... ) + .build(); +``` + +Kotlin + +``` +val webClient = WebClient.builder() + .codecs { configurer -> ... } + .build() +``` + +一旦建立,`WebClient`是不可变的。但是,你可以复制它并构建一个修改后的副本,如下所示: + +爪哇 + +``` +WebClient client1 = WebClient.builder() + .filter(filterA).filter(filterB).build(); + +WebClient client2 = client1.mutate() + .filter(filterC).filter(filterD).build(); + +// client1 has filterA, filterB + +// client2 has filterA, filterB, filterC, filterD +``` + +Kotlin + +``` +val client1 = WebClient.builder() + .filter(filterA).filter(filterB).build() + +val client2 = client1.mutate() + .filter(filterC).filter(filterD).build() + +// client1 has filterA, filterB + +// client2 has filterA, filterB, filterC, filterD +``` + +#### 2.1.1.MaxInMemorySize + +编解码器有[limits](#webflux-codecs-limits)用于缓冲内存中的数据,以避免应用程序内存问题。默认情况下,这些被设置为 256KB。如果这还不够,那么你将得到以下错误: + +``` +org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer +``` + +要更改默认编解码器的限制,请使用以下方法: + +爪哇 + +``` +WebClient webClient = WebClient.builder() + .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024)) + .build(); +``` + +Kotlin + +``` +val webClient = WebClient.builder() + .codecs { configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024) } + .build() +``` + +#### 2.1.2.反应堆网状结构 + +要定制反应堆网络设置,请提供预先配置的`HttpClient`: + +爪哇 + +``` +HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...); + +WebClient webClient = WebClient.builder() + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .build(); +``` + +Kotlin + +``` +val httpClient = HttpClient.create().secure { ... } + +val webClient = WebClient.builder() + .clientConnector(ReactorClientHttpConnector(httpClient)) + .build() +``` + +##### 资源 + +默认情况下,`HttpClient`参与在 `reactor.netty.http.httpresources’中持有的全球反应堆网络资源,包括事件循环线程和连接池。这是推荐的模式,因为对于事件循环并发,首选的是固定的共享资源。在这种模式下,全局资源在流程退出之前一直处于活动状态。 + +如果服务器与进程同步,则通常不需要显式关机。然而,如果服务器可以在进程中启动或停止(例如, Spring MVC 应用程序部署为 WAR),你可以使用`globalResources=true`(默认)声明 Spring-managed Bean 类型的 `ReactorResourceFactory’,以确保在 Spring `ApplicationContext`关闭时关闭反应堆网络全局资源,如下例所示: + +爪哇 + +``` +@Bean +public ReactorResourceFactory reactorResourceFactory() { + return new ReactorResourceFactory(); +} +``` + +Kotlin + +``` +@Bean +fun reactorResourceFactory() = ReactorResourceFactory() +``` + +你也可以选择不参与全球反应堆网状资源。但是,在这种模式下,要确保所有 Reactor Netty 客户机和服务器实例都使用共享资源是你的责任,如下例所示: + +爪哇 + +``` +@Bean +public ReactorResourceFactory resourceFactory() { + ReactorResourceFactory factory = new ReactorResourceFactory(); + factory.setUseGlobalResources(false); (1) + return factory; +} + +@Bean +public WebClient webClient() { + + Function<HttpClient, HttpClient> mapper = client -> { + // Further customizations... + }; + + ClientHttpConnector connector = + new ReactorClientHttpConnector(resourceFactory(), mapper); (2) + + return WebClient.builder().clientConnector(connector).build(); (3) +} +``` + +|**1**|创造独立于全球资源的资源。| +|-----|-----------------------------------------------------------------------| +|**2**|在资源工厂中使用`ReactorClientHttpConnector`构造函数。| +|**3**|将连接器插入`WebClient.Builder`。| + +Kotlin + +``` +@Bean +fun resourceFactory() = ReactorResourceFactory().apply { + isUseGlobalResources = false (1) +} + +@Bean +fun webClient(): WebClient { + + val mapper: (HttpClient) -> HttpClient = { + // Further customizations... + } + + val connector = ReactorClientHttpConnector(resourceFactory(), mapper) (2) + + return WebClient.builder().clientConnector(connector).build() (3) +} +``` + +|**1**|创造独立于全球资源的资源。| +|-----|-----------------------------------------------------------------------| +|**2**|在资源工厂中使用`ReactorClientHttpConnector`构造函数。| +|**3**|将连接器插入`WebClient.Builder`。| + +##### 超时 + +要配置连接超时: + +爪哇 + +``` +import io.netty.channel.ChannelOption; + +HttpClient httpClient = HttpClient.create() + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000); + +WebClient webClient = WebClient.builder() + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .build(); +``` + +Kotlin + +``` +import io.netty.channel.ChannelOption + +val httpClient = HttpClient.create() + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000); + +val webClient = WebClient.builder() + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .build(); +``` + +要配置读或写超时: + +爪哇 + +``` +import io.netty.handler.timeout.ReadTimeoutHandler; +import io.netty.handler.timeout.WriteTimeoutHandler; + +HttpClient httpClient = HttpClient.create() + .doOnConnected(conn -> conn + .addHandlerLast(new ReadTimeoutHandler(10)) + .addHandlerLast(new WriteTimeoutHandler(10))); + +// Create WebClient... +``` + +Kotlin + +``` +import io.netty.handler.timeout.ReadTimeoutHandler +import io.netty.handler.timeout.WriteTimeoutHandler + +val httpClient = HttpClient.create() + .doOnConnected { conn -> conn + .addHandlerLast(new ReadTimeoutHandler(10)) + .addHandlerLast(new WriteTimeoutHandler(10)) + } + +// Create WebClient... +``` + +要为所有请求配置响应超时: + +爪哇 + +``` +HttpClient httpClient = HttpClient.create() + .responseTimeout(Duration.ofSeconds(2)); + +// Create WebClient... +``` + +Kotlin + +``` +val httpClient = HttpClient.create() + .responseTimeout(Duration.ofSeconds(2)); + +// Create WebClient... +``` + +要为特定请求配置响应超时: + +爪哇 + +``` +WebClient.create().get() + .uri("https://example.org/path") + .httpRequest(httpRequest -> { + HttpClientRequest reactorRequest = httpRequest.getNativeRequest(); + reactorRequest.responseTimeout(Duration.ofSeconds(2)); + }) + .retrieve() + .bodyToMono(String.class); +``` + +Kotlin + +``` +WebClient.create().get() + .uri("https://example.org/path") + .httpRequest { httpRequest: ClientHttpRequest -> + val reactorRequest = httpRequest.getNativeRequest<HttpClientRequest>() + reactorRequest.responseTimeout(Duration.ofSeconds(2)) + } + .retrieve() + .bodyToMono(String::class.java) +``` + +#### 2.1.3. Jetty + +下面的示例展示了如何自定义 Jetty `HttpClient`设置: + +Java + +``` +HttpClient httpClient = new HttpClient(); +httpClient.setCookieStore(...); + +WebClient webClient = WebClient.builder() + .clientConnector(new JettyClientHttpConnector(httpClient)) + .build(); +``` + +Kotlin + +``` +val httpClient = HttpClient() +httpClient.cookieStore = ... + +val webClient = WebClient.builder() + .clientConnector(new JettyClientHttpConnector(httpClient)) + .build(); +``` + +默认情况下,`HttpClient`创建自己的资源(`executor’,`ByteBufferPool`,`Scheduler`),这些资源在进程退出或调用`stop()`之前一直处于活动状态。 + +可以在 Jetty 客户机(和服务器)的多个实例之间共享资源,并通过声明类型为`JettyResourceFactory`的 Spring-managed Bean 来确保在 Spring `ApplicationContext`关闭时关闭资源,如下例所示: + +Java + +``` +@Bean +public JettyResourceFactory resourceFactory() { + return new JettyResourceFactory(); +} + +@Bean +public WebClient webClient() { + + HttpClient httpClient = new HttpClient(); + // Further customizations... + + ClientHttpConnector connector = + new JettyClientHttpConnector(httpClient, resourceFactory()); (1) + + return WebClient.builder().clientConnector(connector).build(); (2) +} +``` + +|**1**|在资源工厂中使用`JettyClientHttpConnector`构造函数。| +|-----|---------------------------------------------------------------------| +|**2**|将连接器插入`WebClient.Builder`。| + +Kotlin + +``` +@Bean +fun resourceFactory() = JettyResourceFactory() + +@Bean +fun webClient(): WebClient { + + val httpClient = HttpClient() + // Further customizations... + + val connector = JettyClientHttpConnector(httpClient, resourceFactory()) (1) + + return WebClient.builder().clientConnector(connector).build() (2) +} +``` + +|**1**|在资源工厂中使用`JettyClientHttpConnector`构造函数。| +|-----|---------------------------------------------------------------------| +|**2**|将连接器插入`WebClient.Builder`。| + +#### 2.1.4.HttpComponents + +下面的示例展示了如何自定义 Apache HttpComponents`HttpClient`设置: + +Java + +``` +HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom(); +clientBuilder.setDefaultRequestConfig(...); +CloseableHttpAsyncClient client = clientBuilder.build(); +ClientHttpConnector connector = new HttpComponentsClientHttpConnector(client); + +WebClient webClient = WebClient.builder().clientConnector(connector).build(); +``` + +Kotlin + +``` +val client = HttpAsyncClients.custom().apply { + setDefaultRequestConfig(...) +}.build() +val connector = HttpComponentsClientHttpConnector(client) +val webClient = WebClient.builder().clientConnector(connector).build() +``` + +### 2.2.`retrieve()` + +可以使用`retrieve()`方法声明如何提取响应。例如: + +Java + +``` +WebClient client = WebClient.create("https://example.org"); + +Mono<ResponseEntity<Person>> result = client.get() + .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) + .retrieve() + .toEntity(Person.class); +``` + +Kotlin + +``` +val client = WebClient.create("https://example.org") + +val result = client.get() + .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) + .retrieve() + .toEntity<Person>().awaitSingle() +``` + +或者只得到身体: + +Java + +``` +WebClient client = WebClient.create("https://example.org"); + +Mono<Person> result = client.get() + .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) + .retrieve() + .bodyToMono(Person.class); +``` + +Kotlin + +``` +val client = WebClient.create("https://example.org") + +val result = client.get() + .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) + .retrieve() + .awaitBody<Person>() +``` + +要获取已解码对象的流: + +Java + +``` +Flux<Quote> result = client.get() + .uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM) + .retrieve() + .bodyToFlux(Quote.class); +``` + +Kotlin + +``` +val result = client.get() + .uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM) + .retrieve() + .bodyToFlow<Quote>() +``` + +默认情况下,4xx 或 5xx 响应会导致`WebClientResponseException`,包括用于特定 HTTP 状态代码的子类。要自定义错误响应的处理,请使用`onStatus`处理程序,如下所示: + +Java + +``` +Mono<Person> result = client.get() + .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) + .retrieve() + .onStatus(HttpStatus::is4xxClientError, response -> ...) + .onStatus(HttpStatus::is5xxServerError, response -> ...) + .bodyToMono(Person.class); +``` + +Kotlin + +``` +val result = client.get() + .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON) + .retrieve() + .onStatus(HttpStatus::is4xxClientError) { ... } + .onStatus(HttpStatus::is5xxServerError) { ... } + .awaitBody<Person>() +``` + +### 2.3.交换 + +Kotlin 中的`exchangeToMono()`和`exchangeToFlux()`方法(或`awaitExchange { }`和`exchangeToFlow { }`)对于需要更多控制的更高级情况是有用的,例如根据响应状态对响应进行不同的解码: + +Java + +``` +Mono<Person> entityMono = client.get() + .uri("/persons/1") + .accept(MediaType.APPLICATION_JSON) + .exchangeToMono(response -> { + if (response.statusCode().equals(HttpStatus.OK)) { + return response.bodyToMono(Person.class); + } + else { + // Turn to error + return response.createException().flatMap(Mono::error); + } + }); +``` + +Kotlin + +``` +val entity = client.get() + .uri("/persons/1") + .accept(MediaType.APPLICATION_JSON) + .awaitExchange { + if (response.statusCode() == HttpStatus.OK) { + return response.awaitBody<Person>() + } + else { + throw response.createExceptionAndAwait() + } + } +``` + +当使用上述方法时,在返回的`Mono`或`Flux`完成后,将检查响应体,如果没有消耗它,则释放它,以防止内存和连接泄漏。因此,响应不能在更下游的地方被解码。如果需要,由提供的函数声明如何解码响应。 + +### 2.4.请求主体 + +请求主体可以从`ReactiveAdapterRegistry`处理的任何异步类型进行编码,例如`Mono`或 Kotlin 协程`Deferred`,如下例所示: + +Java + +``` +Mono<Person> personMono = ... ; + +Mono<Void> result = client.post() + .uri("/persons/{id}", id) + .contentType(MediaType.APPLICATION_JSON) + .body(personMono, Person.class) + .retrieve() + .bodyToMono(Void.class); +``` + +Kotlin + +``` +val personDeferred: Deferred<Person> = ... + +client.post() + .uri("/persons/{id}", id) + .contentType(MediaType.APPLICATION_JSON) + .body<Person>(personDeferred) + .retrieve() + .awaitBody<Unit>() +``` + +还可以对对象流进行编码,如下例所示: + +Java + +``` +Flux<Person> personFlux = ... ; + +Mono<Void> result = client.post() + .uri("/persons/{id}", id) + .contentType(MediaType.APPLICATION_STREAM_JSON) + .body(personFlux, Person.class) + .retrieve() + .bodyToMono(Void.class); +``` + +Kotlin + +``` +val people: Flow<Person> = ... + +client.post() + .uri("/persons/{id}", id) + .contentType(MediaType.APPLICATION_JSON) + .body(people) + .retrieve() + .awaitBody<Unit>() +``` + +或者,如果你有实际值,则可以使用`bodyValue`快捷方式,如下例所示: + +Java + +``` +Person person = ... ; + +Mono<Void> result = client.post() + .uri("/persons/{id}", id) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(person) + .retrieve() + .bodyToMono(Void.class); +``` + +Kotlin + +``` +val person: Person = ... + +client.post() + .uri("/persons/{id}", id) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(person) + .retrieve() + .awaitBody<Unit>() +``` + +#### 2.4.1.表单数据 + +要发送表单数据,可以提供一个`MultiValueMap<String, String>`作为主体。请注意,“FormHttpMessageWriter”自动将内容设置为`application/x-www-form-urlencoded`。下面的示例展示了如何使用`MultiValueMap<String, String>`: + +Java + +``` +MultiValueMap<String, String> formData = ... ; + +Mono<Void> result = client.post() + .uri("/path", id) + .bodyValue(formData) + .retrieve() + .bodyToMono(Void.class); +``` + +Kotlin + +``` +val formData: MultiValueMap<String, String> = ... + +client.post() + .uri("/path", id) + .bodyValue(formData) + .retrieve() + .awaitBody<Unit>() +``` + +还可以通过使用`BodyInserters`在线提供表单数据,如下例所示: + +Java + +``` +import static org.springframework.web.reactive.function.BodyInserters.*; + +Mono<Void> result = client.post() + .uri("/path", id) + .body(fromFormData("k1", "v1").with("k2", "v2")) + .retrieve() + .bodyToMono(Void.class); +``` + +Kotlin + +``` +import org.springframework.web.reactive.function.BodyInserters.* + +client.post() + .uri("/path", id) + .body(fromFormData("k1", "v1").with("k2", "v2")) + .retrieve() + .awaitBody<Unit>() +``` + +#### 2.4.2.多部分数据 + +要发送多部分数据,你需要提供一个`MultiValueMap<String, ?>`,其值要么是表示部分内容的`Object`实例,要么是表示部分内容和标题的`HttpEntity`实例。`MultipartBodyBuilder`提供了一个方便的 API 来准备多部分请求。下面的示例展示了如何创建`MultiValueMap<String, ?>`: + +Java + +``` +MultipartBodyBuilder builder = new MultipartBodyBuilder(); +builder.part("fieldPart", "fieldValue"); +builder.part("filePart1", new FileSystemResource("...logo.png")); +builder.part("jsonPart", new Person("Jason")); +builder.part("myPart", part); // Part from a server request + +MultiValueMap<String, HttpEntity<?>> parts = builder.build(); +``` + +Kotlin + +``` +val builder = MultipartBodyBuilder().apply { + part("fieldPart", "fieldValue") + part("filePart1", new FileSystemResource("...logo.png")) + part("jsonPart", new Person("Jason")) + part("myPart", part) // Part from a server request +} + +val parts = builder.build() +``` + +在大多数情况下,你不必为每个部分指定`Content-Type`。内容类型是基于所选的`HttpMessageWriter`自动确定的,以序列化它,或者,在`Resource`的情况下,基于文件扩展名自动确定的。如果有必要,可以通过重载的构建器`part`方法之一显式地为每个部分提供`MediaType`。 + +一旦准备好了`MultiValueMap`,将其传递给`WebClient`的最简单方法是通过`body`方法,如下例所示: + +Java + +``` +MultipartBodyBuilder builder = ...; + +Mono<Void> result = client.post() + .uri("/path", id) + .body(builder.build()) + .retrieve() + .bodyToMono(Void.class); +``` + +Kotlin + +``` +val builder: MultipartBodyBuilder = ... + +client.post() + .uri("/path", id) + .body(builder.build()) + .retrieve() + .awaitBody<Unit>() +``` + +如果`MultiValueMap`至少包含一个非 `string’值,该值也可以表示常规形式的数据(即`application/x-www-form-urlencoded`),则无需将`Content-Type`设置为`multipart/form-data`。在使用“MultipartBodybuilder”时总是这样,这确保了`HttpEntity`包装器。 + +作为`MultipartBodyBuilder`的替代方案,还可以通过内置的`BodyInserters`提供内联样式的多部分内容,如下例所示: + +Java + +``` +import static org.springframework.web.reactive.function.BodyInserters.*; + +Mono<Void> result = client.post() + .uri("/path", id) + .body(fromMultipartData("fieldPart", "value").with("filePart", resource)) + .retrieve() + .bodyToMono(Void.class); +``` + +Kotlin + +``` +import org.springframework.web.reactive.function.BodyInserters.* + +client.post() + .uri("/path", id) + .body(fromMultipartData("fieldPart", "value").with("filePart", resource)) + .retrieve() + .awaitBody<Unit>() +``` + +### 2.5.过滤器 + +你可以通过`WebClient.Builder`注册一个客户端过滤器,以便拦截和修改请求,如下例所示: + +Java + +``` +WebClient client = WebClient.builder() + .filter((request, next) -> { + + ClientRequest filtered = ClientRequest.from(request) + .header("foo", "bar") + .build(); + + return next.exchange(filtered); + }) + .build(); +``` + +Kotlin + +``` +val client = WebClient.builder() + .filter { request, next -> + + val filtered = ClientRequest.from(request) + .header("foo", "bar") + .build() + + next.exchange(filtered) + } + .build() +``` + +这可以用于跨领域的关注,例如身份验证。下面的示例使用一个过滤器通过静态工厂方法进行基本身份验证: + +Java + +``` +import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; + +WebClient client = WebClient.builder() + .filter(basicAuthentication("user", "password")) + .build(); +``` + +Kotlin + +``` +import org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication + +val client = WebClient.builder() + .filter(basicAuthentication("user", "password")) + .build() +``` + +可以通过改变现有的`WebClient`实例来添加或删除过滤器,从而产生一个不影响原始实例的新的`WebClient`实例。例如: + +Java + +``` +import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication; + +WebClient client = webClient.mutate() + .filters(filterList -> { + filterList.add(0, basicAuthentication("user", "password")); + }) + .build(); +``` + +Kotlin + +``` +val client = webClient.mutate() + .filters { it.add(0, basicAuthentication("user", "password")) } + .build() +``` + +`WebClient`是围绕过滤器链的一个薄薄的外观,后面是“交换函数”。它提供了一个工作流,用于发出请求,对来自更高级别的对象进行编码,并有助于确保始终使用响应内容。当过滤器以某种方式处理响应时,必须格外小心,始终使用其内容,或者以其他方式将其向下游传播到`WebClient`,这将确保相同的结果。下面是一个过滤器,它处理`UNAUTHORIZED`状态代码,但确保释放任何响应内容(无论是否期望): + +Java + +``` +public ExchangeFilterFunction renewTokenFilter() { + return (request, next) -> next.exchange(request).flatMap(response -> { + if (response.statusCode().value() == HttpStatus.UNAUTHORIZED.value()) { + return response.releaseBody() + .then(renewToken()) + .flatMap(token -> { + ClientRequest newRequest = ClientRequest.from(request).build(); + return next.exchange(newRequest); + }); + } else { + return Mono.just(response); + } + }); +} +``` + +Kotlin + +``` +fun renewTokenFilter(): ExchangeFilterFunction? { + return ExchangeFilterFunction { request: ClientRequest?, next: ExchangeFunction -> + next.exchange(request!!).flatMap { response: ClientResponse -> + if (response.statusCode().value() == HttpStatus.UNAUTHORIZED.value()) { + [email protected] response.releaseBody() + .then(renewToken()) + .flatMap { token: String? -> + val newRequest = ClientRequest.from(request).build() + next.exchange(newRequest) + } + } else { + [email protected] Mono.just(response) + } + } + } +} +``` + +### 2.6.属性 + +你可以向请求添加属性。如果你希望通过筛选链传递信息并影响给定请求的筛选器的行为,这是很方便的。例如: + +Java + +``` +WebClient client = WebClient.builder() + .filter((request, next) -> { + Optional<Object> usr = request.attribute("myAttribute"); + // ... + }) + .build(); + +client.get().uri("https://example.org/") + .attribute("myAttribute", "...") + .retrieve() + .bodyToMono(Void.class); + + } +``` + +Kotlin + +``` +val client = WebClient.builder() + .filter { request, _ -> + val usr = request.attributes()["myAttribute"]; + // ... + } + .build() + + client.get().uri("https://example.org/") + .attribute("myAttribute", "...") + .retrieve() + .awaitBody<Unit>() +``` + +请注意,你可以在“webclient.builder”级别全局配置`defaultRequest`回调,它允许你将属性插入到所有请求中,例如,在 Spring MVC 应用程序中可以使用它来基于`ThreadLocal`数据填充请求属性。 + +### 2.7.上下文 + +[Attributes](#webflux-client-attributes)提供了一种将信息传递到过滤器链的方便方式,但它们只会影响当前的请求。如果你想要传递传播到嵌套的其他请求的信息,例如通过`flatMap`,或者在之后执行,例如通过`concatMap`,那么你将需要使用反应器`Context`。 + +反应器`Context`需要在反应链的末端填充,以便适用于所有操作。例如: + +Java + +``` +WebClient client = WebClient.builder() + .filter((request, next) -> + Mono.deferContextual(contextView -> { + String value = contextView.get("foo"); + // ... + })) + .build(); + +client.get().uri("https://example.org/") + .retrieve() + .bodyToMono(String.class) + .flatMap(body -> { + // perform nested request (context propagates automatically)... + }) + .contextWrite(context -> context.put("foo", ...)); +``` + +### 2.8.同步使用 + +`WebClient`可以以同步方式使用,方法是在末尾阻塞结果: + +Java + +``` +Person person = client.get().uri("/person/{id}", i).retrieve() + .bodyToMono(Person.class) + .block(); + +List<Person> persons = client.get().uri("/persons").retrieve() + .bodyToFlux(Person.class) + .collectList() + .block(); +``` + +Kotlin + +``` +val person = runBlocking { + client.get().uri("/person/{id}", i).retrieve() + .awaitBody<Person>() +} + +val persons = runBlocking { + client.get().uri("/persons").retrieve() + .bodyToFlow<Person>() + .toList() +} +``` + +但是,如果需要进行多个调用,那么更有效的方法是避免单个地阻塞每个响应,而是等待合并的结果: + +Java + +``` +Mono<Person> personMono = client.get().uri("/person/{id}", personId) + .retrieve().bodyToMono(Person.class); + +Mono<List<Hobby>> hobbiesMono = client.get().uri("/person/{id}/hobbies", personId) + .retrieve().bodyToFlux(Hobby.class).collectList(); + +Map<String, Object> data = Mono.zip(personMono, hobbiesMono, (person, hobbies) -> { + Map<String, String> map = new LinkedHashMap<>(); + map.put("person", person); + map.put("hobbies", hobbies); + return map; + }) + .block(); +``` + +Kotlin + +``` +val data = runBlocking { + val personDeferred = async { + client.get().uri("/person/{id}", personId) + .retrieve().awaitBody<Person>() + } + + val hobbiesDeferred = async { + client.get().uri("/person/{id}/hobbies", personId) + .retrieve().bodyToFlow<Hobby>().toList() + } + + mapOf("person" to personDeferred.await(), "hobbies" to hobbiesDeferred.await()) + } +``` + +以上只是一个例子。还有很多其他的模式和操作人员来组装一个反应性管道,该管道可以进行许多远程调用,可能是一些嵌套的、相互依赖的调用,并且直到最后都不会阻塞。 + +| |使用`Flux`或`Mono`,你应该永远不需要在 Spring MVC 或 Spring WebFlux 控制器中进行阻塞。<br/>只需从控制器方法返回得到的反应性类型。同样的原理也适用于<br/> Kotlin 协程和 Spring WebFlux,只需在你的<br/>控制器方法中使用悬挂函数或返回`Flow`。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +### 2.9.测试 + +要测试使用`WebClient`的代码,可以使用模拟 Web 服务器,例如[OKHTTP MockWebServer](https://github.com/square/okhttp#mockwebserver)。要查看它的使用示例,请查看 Spring Framework 测试套件中的[“WebClientIntegrationTests”](https://github.com/spring-projects/spring-framework/tree/main/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java)或 OkHTTP 存储库中的[`static-server`](https://github.com/square/okhttp/tree/master/samples/static-server)示例。 + +## 3. WebSockets + +[Same as in the Servlet stack](web.html#websocket) + +参考文档的这一部分涵盖了对 Reactive-Stack WebSocket 消息传递的支持。 + +### 3.1. WebSocket 介绍 + +WebSocket 协议[RFC 6455](https://tools.ietf.org/html/rfc6455)提供了一种标准化的方式,通过单个 TCP 连接在客户机和服务器之间建立全双工、双向通信通道。它是一种与 HTTP 不同的 TCP 协议,但其设计是通过 HTTP 工作的,使用端口 80 和 443,并允许重用现有的防火墙规则。 + +WebSocket 交互以一个 HTTP 请求开始,该 HTTP 请求使用 HTTP头来升级或在这种情况下切换到 WebSocket 协议。下面的示例展示了这样的交互: + +``` +GET /spring-websocket-portfolio/portfolio HTTP/1.1 +Host: localhost:8080 +Upgrade: websocket (1) +Connection: Upgrade (2) +Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg== +Sec-WebSocket-Protocol: v10.stomp, v11.stomp +Sec-WebSocket-Version: 13 +Origin: http://localhost:8080 +``` + +|**1**|`Upgrade`标头。| +|-----|-------------------------------| +|**2**|使用`Upgrade`连接。| + +具有 WebSocket 支持的服务器将返回类似于以下内容的输出,而不是通常的 200 状态代码: + +``` +HTTP/1.1 101 Switching Protocols (1) +Upgrade: websocket +Connection: Upgrade +Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0= +Sec-WebSocket-Protocol: v10.stomp +``` + +|**1**|协议转换| +|-----|---------------| + +在成功握手之后,HTTP 升级请求中的 TCP 套接字仍然是开放的,以便客户机和服务器继续发送和接收消息。 + +关于 WebSockets 如何工作的完整介绍超出了本文的范围。参见 RFC6455,HTML5 的 WebSocket 章,或者 Web 上的许多介绍和教程中的任何一个。 + +请注意,如果 WebSocket 服务器运行在 Web 服务器(例如 Nginx)的后面,则可能需要将其配置为将 WebSocket 升级请求传递到 WebSocket 服务器。同样,如果应用程序在云环境中运行,则检查与 WebSocket 支持相关的云提供商的指令。 + +#### 3.1.1.HTTP 与 WebSocket + +尽管 WebSocket 的设计是与 HTTP 兼容的,并且以 HTTP 请求开始,但重要的是要理解这两个协议导致了非常不同的体系结构和应用程序编程模型。 + +在 HTTP 和 REST 中,应用程序被建模为许多 URL。为了与应用程序交互,客户端访问这些 URL,请求-响应样式。服务器根据 HTTP URL、方法和标头将请求路由到适当的处理程序。 + +相比之下,在 WebSockets 中,初始连接通常只有一个 URL。随后,所有应用程序消息都在相同的 TCP 连接上流动。这指向了一种完全不同的异步、事件驱动的消息传递体系结构。 + +WebSocket 也是一种低级传输协议,它与 HTTP 不同,不对消息的内容规定任何语义。这意味着,除非客户机和服务器在消息语义上达成一致,否则就没有路由或处理消息的方法。 + +WebSocket 客户端和服务器可以协商使用更高级别的消息传递协议(例如,STOMP),通过`Sec-WebSocket-Protocol`头上的 HTTP 握手请求。如果不能做到这一点,他们就需要拿出自己的惯例。 + +#### 3.1.2.何时使用 WebSockets + +WebSockets 可以使 Web 页面具有动态性和交互性。然而,在许多情况下,Ajax 和 HTTP 流或长轮询的组合可以提供简单有效的解决方案。 + +例如,新闻、邮件和社交提要需要动态更新,但每隔几分钟更新一次可能完全没问题。另一方面,协作、游戏和金融应用程序需要更接近实时。 + +延迟本身并不是一个决定因素。如果消息量相对较低(例如,监视网络故障),则 HTTP 流或轮询可以提供有效的解决方案。正是低延迟、高频率和高音量的组合为 WebSocket 的使用提供了最佳的条件。 + +还请记住,在 Internet 上,超出你控制范围的限制性代理可能会阻止 WebSocket 交互,这可能是因为它们未配置为传递“升级”头,也可能是因为它们关闭了似乎空闲的长期连接。这意味着对防火墙内的内部应用程序使用 WebSocket 比对面向公众的应用程序使用 WebSocket 是一个更直接的决定。 + +### 3.2. WebSocket API + +[Same as in the Servlet stack](web.html#websocket-server) + +Spring 框架提供了一个 WebSocket API,你可以使用它编写处理 WebSocket 消息的客户端和服务器端应用程序。 + +#### 3.2.1.服务器 + +[Same as in the Servlet stack](web.html#websocket-server-handler) + +要创建 WebSocket 服务器,你可以首先创建`WebSocketHandler`。下面的示例展示了如何做到这一点: + +爪哇 + +``` +import org.springframework.web.reactive.socket.WebSocketHandler; +import org.springframework.web.reactive.socket.WebSocketSession; + +public class MyWebSocketHandler implements WebSocketHandler { + + @Override + public Mono<Void> handle(WebSocketSession session) { + // ... + } +} +``` + +Kotlin + +``` +import org.springframework.web.reactive.socket.WebSocketHandler +import org.springframework.web.reactive.socket.WebSocketSession + +class MyWebSocketHandler : WebSocketHandler { + + override fun handle(session: WebSocketSession): Mono<Void> { + // ... + } +} +``` + +然后你可以将它映射到一个 URL: + +爪哇 + +``` +@Configuration +class WebConfig { + + @Bean + public HandlerMapping handlerMapping() { + Map<String, WebSocketHandler> map = new HashMap<>(); + map.put("/path", new MyWebSocketHandler()); + int order = -1; // before annotated controllers + + return new SimpleUrlHandlerMapping(map, order); + } +} +``` + +Kotlin + +``` +@Configuration +class WebConfig { + + @Bean + fun handlerMapping(): HandlerMapping { + val map = mapOf("/path" to MyWebSocketHandler()) + val order = -1 // before annotated controllers + + return SimpleUrlHandlerMapping(map, order) + } +} +``` + +如果使用[WebFlux Config](#webflux-config)则无需进一步操作,或者如果不使用 WebFlux 配置,则需要声明“WebSocketHandlerAdapter”,如下所示: + +爪哇 + +``` +@Configuration +class WebConfig { + + // ... + + @Bean + public WebSocketHandlerAdapter handlerAdapter() { + return new WebSocketHandlerAdapter(); + } +} +``` + +Kotlin + +``` +@Configuration +class WebConfig { + + // ... + + @Bean + fun handlerAdapter() = WebSocketHandlerAdapter() +} +``` + +#### 3.2.2.`WebSocketHandler` + +`handle`的`WebSocketHandler`方法接受`WebSocketSession`,并返回`Mono<Void>`,以指示何时完成对会话的应用程序处理。会话通过两个流处理,一个用于入站消息,另一个用于出站消息。下表描述了处理流的两种方法: + +| `WebSocketSession` method |说明| +|----------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Flux<WebSocketMessage> receive()` |提供对入站消息流的访问,并在连接关闭时完成。| +|`Mono<Void> send(Publisher<WebSocketMessage>)`|获取传出消息的源,写入消息,并返回一个`Mono<Void>`,当源完成并写入完成时,该<br/>完成。| + +a`WebSocketHandler`必须将入站和出站流组合成一个统一的流,并返回一个`Mono<Void>`,该流反映了该流的完成。根据应用程序的需求,统一流在以下情况下完成: + +* 入站消息流或出站消息流已完成。 + +* 入站流完成(即连接关闭),而出站流是无限的。 + +* 在选定的点上,通过`close`的方法`WebSocketSession`。 + +当入站和出站消息流组合在一起时,不需要检查连接是否打开,因为反应流信号结束活动。入站流接收完成或错误信号,出站流接收取消信号。 + +处理程序的最基本实现是处理入站流的实现。下面的示例展示了这样的实现: + +爪哇 + +``` +class ExampleHandler implements WebSocketHandler { + + @Override + public Mono<Void> handle(WebSocketSession session) { + return session.receive() (1) + .doOnNext(message -> { + // ... (2) + }) + .concatMap(message -> { + // ... (3) + }) + .then(); (4) + } +} +``` + +|**1**|访问入站消息流。| +|-----|--------------------------------------------------------------------| +|**2**|对每条信息都做些什么。| +|**3**|执行使用消息内容的嵌套异步操作。| +|**4**|返回在接收完成时完成的`Mono<Void>`。| + +Kotlin + +``` +class ExampleHandler : WebSocketHandler { + + override fun handle(session: WebSocketSession): Mono<Void> { + return session.receive() (1) + .doOnNext { + // ... (2) + } + .concatMap { + // ... (3) + } + .then() (4) + } +} +``` + +|**1**|访问入站消息流。| +|-----|--------------------------------------------------------------------| +|**2**|对每条信息都做些什么。| +|**3**|执行使用消息内容的嵌套异步操作。| +|**4**|返回在接收完成时完成的`Mono<Void>`。| + +| |对于嵌套的异步操作,你可能需要在使用池数据缓冲区(例如 Netty)的底层<br/>服务器上调用`message.retain()`。否则,数据缓冲区可能是<br/>在你有机会读取数据之前释放的。有关更多背景信息,请参见[数据缓冲区和编解码器](core.html#databuffers)。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +以下实现合并了入站和出站流: + +爪哇 + +``` +class ExampleHandler implements WebSocketHandler { + + @Override + public Mono<Void> handle(WebSocketSession session) { + + Flux<WebSocketMessage> output = session.receive() (1) + .doOnNext(message -> { + // ... + }) + .concatMap(message -> { + // ... + }) + .map(value -> session.textMessage("Echo " + value)); (2) + + return session.send(output); (3) + } +} +``` + +|**1**|处理入站消息流。| +|-----|--------------------------------------------------------------------------| +|**2**|创建出站消息,生成一个合并的流。| +|**3**|返回一个`Mono<Void>`,它在我们继续接收时不完成。| + +Kotlin + +``` +class ExampleHandler : WebSocketHandler { + + override fun handle(session: WebSocketSession): Mono<Void> { + + val output = session.receive() (1) + .doOnNext { + // ... + } + .concatMap { + // ... + } + .map { session.textMessage("Echo $it") } (2) + + return session.send(output) (3) + } +} +``` + +|**1**|处理入站消息流。| +|-----|--------------------------------------------------------------------------| +|**2**|创建出站消息,生成一个合并的流。| +|**3**|返回一个`Mono<Void>`,它在我们继续接收时不完成。| + +入站和出站流可以是独立的,并且仅在完成时才加入,如下例所示: + +爪哇 + +``` +class ExampleHandler implements WebSocketHandler { + + @Override + public Mono<Void> handle(WebSocketSession session) { + + Mono<Void> input = session.receive() (1) + .doOnNext(message -> { + // ... + }) + .concatMap(message -> { + // ... + }) + .then(); + + Flux<String> source = ... ; + Mono<Void> output = session.send(source.map(session::textMessage)); (2) + + return Mono.zip(input, output).then(); (3) + } +} +``` + +|**1**|处理入站消息流。| +|-----|----------------------------------------------------------------------------------| +|**2**|发送外发消息。| +|**3**|加入这些流并返回一个`Mono<Void>`,当任一流结束时完成。| + +Kotlin + +``` +class ExampleHandler : WebSocketHandler { + + override fun handle(session: WebSocketSession): Mono<Void> { + + val input = session.receive() (1) + .doOnNext { + // ... + } + .concatMap { + // ... + } + .then() + + val source: Flux<String> = ... + val output = session.send(source.map(session::textMessage)) (2) + + return Mono.zip(input, output).then() (3) + } +} +``` + +|**1**|处理入站消息流。| +|-----|----------------------------------------------------------------------------------| +|**2**|发送外发消息。| +|**3**|加入这些流并返回一个`Mono<Void>`,当任一流结束时完成。| + +#### 3.2.3.`DataBuffer` + +`DataBuffer`是 WebFlux 中字节缓冲区的表示形式。参考文献的 Spring 核心部分在[数据缓冲区和编解码器](core.html#databuffers)一节中有更多关于这一点的内容。要理解的关键点是,在一些服务器(如 Netty)上,字节缓冲区是池的,引用也是计算的,并且必须在使用时释放,以避免内存泄漏。 + +当在 Netty 上运行时,如果应用程序希望保留输入数据缓冲区,则必须使用`DataBufferUtils.retain(dataBuffer)`,以确保它们不会被释放,然后在使用缓冲区时使用`DataBufferUtils.release(dataBuffer)`。 + +#### 3.2.4.握手 + +[Same as in the Servlet stack](web.html#websocket-server-handshake) + +`WebSocketHandlerAdapter`委托给`WebSocketService`。默认情况下,这是`HandshakeWebSocketService`的一个实例,它对 WebSocket 请求执行基本检查,然后对正在使用的服务器使用`RequestUpgradeStrategy`。目前,有对反应堆网状物、 Tomcat、 Jetty 和 Undertow 的内置支持。 + +`HandshakeWebSocketService`公开了一个`sessionAttributePredicate`属性,该属性允许设置`Predicate<String>`以从`WebSession`中提取属性,并将它们插入到`WebSocketSession`的属性中。 + +#### 3.2.5.服务器配置 + +[Same as in the Servlet stack](web.html#websocket-server-runtime-configuration) + +每个服务器的`RequestUpgradeStrategy`公开了特定于底层 WebSocket 服务器引擎的配置。当使用 WebFlux 爪哇 Config 时,你可以自定义这些属性,如[WebFlux Config](#webflux-config-websocket-service)的相应部分所示,或者如果不使用 WebFlux Config,则使用以下方法: + +爪哇 + +``` +@Configuration +class WebConfig { + + @Bean + public WebSocketHandlerAdapter handlerAdapter() { + return new WebSocketHandlerAdapter(webSocketService()); + } + + @Bean + public WebSocketService webSocketService() { + TomcatRequestUpgradeStrategy strategy = new TomcatRequestUpgradeStrategy(); + strategy.setMaxSessionIdleTimeout(0L); + return new HandshakeWebSocketService(strategy); + } +} +``` + +Kotlin + +``` +@Configuration +class WebConfig { + + @Bean + fun handlerAdapter() = + WebSocketHandlerAdapter(webSocketService()) + + @Bean + fun webSocketService(): WebSocketService { + val strategy = TomcatRequestUpgradeStrategy().apply { + setMaxSessionIdleTimeout(0L) + } + return HandshakeWebSocketService(strategy) + } +} +``` + +检查你的服务器的升级策略,看看有哪些选项可用。目前,只有 Tomcat 和 Jetty 公开了这样的选项。 + +#### 3.2.6.科尔斯 + +[Same as in the Servlet stack](web.html#websocket-server-allowed-origins) + +配置 CORS 并限制对 WebSocket 端点的访问的最简单方法是让你的`WebSocketHandler`实现`CorsConfigurationSource`,并返回带有允许的起源、标题和其他详细信息的 `corsconfiguration’。如果无法做到这一点,还可以在`SimpleUrlHandler`上设置`corsConfigurations`属性,以通过 URL 模式指定 CORS 设置。如果两者都有规定,则使用`CorsConfiguration`上的“combine”方法对它们进行合并。 + +#### 3.2.7.客户 + +Spring WebFlux 提供了一个`WebSocketClient`的抽象,其实现涉及反应器 Netty、 Tomcat、 Jetty、 Undertow 和标准 爪哇(即 JSR-356)。 + +| |Tomcat 客户机实际上是标准 爪哇 One 的扩展,它在`WebSocketSession`处理中具有一些额外的<br/>功能,以利用 Tomcat 特定的<br/>API 来暂停接收用于回压的消息。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +要启动 WebSocket 会话,你可以创建客户机的实例,并使用其`execute`方法: + +爪哇 + +``` +WebSocketClient client = new ReactorNettyWebSocketClient(); + +URI url = new URI("ws://localhost:8080/path"); +client.execute(url, session -> + session.receive() + .doOnNext(System.out::println) + .then()); +``` + +Kotlin + +``` +val client = ReactorNettyWebSocketClient() + + val url = URI("ws://localhost:8080/path") + client.execute(url) { session -> + session.receive() + .doOnNext(::println) + .then() + } +``` + +一些客户机,例如 Jetty,实现`Lifecycle`,并且需要在可以使用它们之前停止并启动它们。所有客户端都具有与底层 WebSocket 客户端的配置相关的构造函数选项。 + +## 4. 测试 + +[Same in Spring MVC](web.html#testing) + +`spring-test`模块提供了`ServerHttpRequest`、`ServerHttpresponse’和`ServerWebExchange`的模拟实现。关于模拟对象的讨论,请参见[Spring Web Reactive](testing.html#mock-objects-web-reactive)。 + +[`WebTestClient`](testing.html#webtestclient)构建在这些模拟请求和响应对象的基础上,为在没有 HTTP 服务器的情况下测试 WebFlux 应用程序提供支持。对于端到端集成测试,也可以使用`WebTestClient`。 + +## 5. RSocket + +本节描述 Spring Framework 对 RSocket 协议的支持。 + +### 5.1.概述 + +WebSocket RSocket 是一种用于在 TCP、 WebSocket 和其他字节流传输上进行多路复用、双工通信的应用程序协议,它使用以下交互模型之一: + +* `Request-Response`—发一条信息,收一条。 + +* `Request-Stream`—发送一条消息并接收一系列消息。 + +* `Channel`——双向发送消息流。 + +* `Fire-and-Forget`——发送单向消息。 + +一旦建立了初始连接,“客户端”与“服务器”的区别就会丢失,因为双方变得对称,并且双方都可以启动上述交互之一。这就是为什么在协议中将参与方称为“请求者”和“响应者”,而上述交互称为“请求流”或简称“请求”。 + +这些是 RSocket 协议的关键特性和优点: + +* [反应流](https://www.reactive-streams.org/)跨网络边界语义——对于`Request-Stream`和`Channel`之类的流媒体请求,背压信号在请求者和响应者之间传输,允许请求者在源处减慢响应者的速度,从而减少对网络层拥塞控制的依赖,以及在网络级别或任何级别上对缓冲的需求。 + +* 请求节流——这个特性在`LEASE`帧之后被命名为“租赁”,该帧可以从每一端发送,以限制另一端在给定时间内允许的请求总数。租约定期续签。 + +* 会话恢复——这是为失去连接而设计的,并且需要保持某些状态。状态管理对于应用程序是透明的,并且与背压相结合工作得很好,背压可以在可能的情况下停止生成器并减少所需的状态量。 + +* 大消息的分片和重新组装。 + +* KeepAlive(心跳)。 + +RSocket 在多种语言中都有[implementations](https://github.com/rsocket)。[爪哇 library](https://github.com/rsocket/rsocket-java)是建立在[Project Reactor](https://projectreactor.io/)和[Reactor Netty](https://github.com/reactor/reactor-netty)上的。这意味着来自应用程序中的反应流发布者的信号通过 RSocket 在整个网络中透明地传播。 + +#### 5.1.1.《议定书》 + +RSocket 的优点之一是它在线路上有很好定义的行为,以及易于读取的[specification](https://rsocket.io/docs/Protocol)以及一些协议[extensions](https://github.com/rsocket/rsocket/tree/master/Extensions)。因此,阅读规范是一个好主意,独立于语言实现和更高级别的框架 API。本节提供了一个简明的概述,以建立一些上下文。 + +**连接** + +最初,客户端通过一些低级别的流媒体传输(例如 TCP 或 WebSocket)连接到服务器,并向服务器发送`SETUP`帧,以设置连接的参数。 + +服务器可能会拒绝`SETUP`帧,但是通常在它被发送(对于客户端)和接收(对于服务器)之后,双方都可以开始进行请求,除非`SETUP`表示使用租赁语义来限制请求的数量,在这种情况下,双方都必须等待来自另一端的`LEASE`帧以允许进行请求。 + +**提出要求** + +一旦建立了连接,双方可以通过`REQUEST_RESPONSE`、`REQUEST_STREAM`、`REQUEST_CHANNEL`或`REQUEST_FNF`中的一个帧发起请求。这些帧中的每一个都从请求者向响应者传送一条消息。 + +然后响应者可以返回带有响应消息的`PAYLOAD`帧,并且在`REQUEST_CHANNEL`的情况下,请求者还可以发送带有更多请求消息的`PAYLOAD`帧。 + +当请求涉及诸如`Request-Stream`和`Channel`之类的消息流时,响应者必须尊重来自请求者的需求信号。需求被表示为大量的消息。初始需求在`REQUEST_STREAM`和 `request_channel’框架中指定。随后的需求是通过`REQUEST_N`帧来表示的。 + +每一方也可以通过`METADATA_PUSH`帧发送元数据通知,这些通知与任何单个请求无关,而是与整个连接有关。 + +**消息格式** + +RSocket 消息包含数据和元数据。元数据可用于发送路由、安全令牌等。数据和元数据可以采用不同的格式。每个类型的 MIME 类型都在`SETUP`框架中声明,并应用于给定连接上的所有请求。 + +虽然所有消息都可以具有元数据,但通常的元数据(例如路由)是每个请求的,因此仅包含在请求的第一个消息中,即具有一个框架 `request_response’,`REQUEST_STREAM`,`REQUEST_CHANNEL`,或`REQUEST_FNF`。 + +协议扩展定义了应用程序中使用的通用元数据格式: + +* [复合元数据](https://github.com/rsocket/rsocket/blob/master/Extensions/CompositeMetadata.md)--多个独立格式化的元数据条目。 + +* [Routing](https://github.com/rsocket/rsocket/blob/master/Extensions/Routing.md)—请求的路径。 + +#### 5.1.2.爪哇 实现 + +RSocket 的[爪哇 实现](https://github.com/rsocket/rsocket-java)是建立在[Project Reactor](https://projectreactor.io/)之上的。TCP 和 WebSocket 的传输建立在[Reactor Netty](https://github.com/reactor/reactor-netty)上。作为一种反应流库,Reactor 简化了协议的实现工作。对于应用程序来说,使用`Flux`和`Mono`声明运算符和透明背压支持是很自然的选择。 + +RSocket 爪哇 中的 API 有意地是最小的和基本的。它专注于协议特性,并将应用程序编程模型(例如 RPC Codegen vs Other)作为更高级别的独立关注点。 + +主契约[io.rsocket.rsocket](https://github.com/rsocket/rsocket-java/blob/master/rsocket-core/src/main/java/io/rsocket/RSocket.java)用`Mono`表示对单个消息的承诺、`Flux`消息流和`io.rsocket.Payload`实际消息进行建模,并将对数据和元数据的访问作为字节缓冲区。`RSocket`契约是对称使用的。对于请求,应用程序被赋予一个`RSocket`来执行请求。对于响应,应用程序实现`RSocket`来处理请求。 + +这并不意味着要做一个全面的介绍。 Spring 在大多数情况下,应用程序将不必直接使用其 API。然而,独立于 Spring 来观察或实验 RSocket 可能是重要的。RSocket 爪哇 存储库包含许多[sample apps](https://github.com/rsocket/rsocket-java/tree/master/rsocket-examples),它们演示了它的 API 和协议特性。 + +#### 5.1.3. Spring 支持 + +`spring-messaging`模块包含以下内容: + +* [Rsocketrequester](#rsocket-requester)—Fluent API 可以通过`io.rsocket.RSocket`进行请求,其中包含数据和元数据的编码/解码。 + +* [附加注释的响应者](#rsocket-annot-responders)—`@MessageMapping`用于响应的注释处理程序方法。 + +`spring-web`模块包含`Encoder`和`Decoder`实现,例如 Jackson 的 cbor/json,以及 RSocket 应用程序可能需要的 Protobuf。它还包含“PathpatternParser”,可以插入该功能以进行有效的路线匹配。 + +Spring Boot2.2 支持在 TCP 或 WebSocket 上站立 RSocket 服务器,包括在 WebFlux 服务器中公开 RSocket over WebSocket 的选项。对于`RSocketRequester.Builder`和`RSocket战略`也有客户机支持和自动配置。有关更多详细信息,请参见 Spring 引导引用中的[RSocket section](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-rsocket)。 + +Spring 安全性 5.2 提供了 RSocket 支持。 + +Spring 集成 5.2 提供了入站和出站网关,以与 RSocket 客户端和服务器进行交互。有关更多详细信息,请参见 Spring 集成参考手册。 + +Spring 云网关支持 RSocket 连接。 + +### 5.2.Rsocketrequester + +`RSocketRequester`提供了一个 Fluent API 来执行 RSocket 请求,接受并返回数据和元数据的对象,而不是低级别的数据缓冲区。它可以被对称地使用,用于从客户机发出请求和从服务器发出请求。 + +#### 5.2.1.客户请求者 + +要在客户端获得`RSocketRequester`,就需要连接到一个服务器,该服务器需要发送一个带有连接设置的 RSocket`SETUP`帧。`RSocketRequester`提供了一个构建器,该构建器帮助准备`io.rsocket.core.RSocketConnector`,包括`SETUP`框架的连接设置。 + +这是连接默认设置的最基本方式: + +爪哇 + +``` +RSocketRequester requester = RSocketRequester.builder().tcp("localhost", 7000); + +URI url = URI.create("https://example.org:8080/rsocket"); +RSocketRequester requester = RSocketRequester.builder().webSocket(url); +``` + +Kotlin + +``` +val requester = RSocketRequester.builder().tcp("localhost", 7000) + +URI url = URI.create("https://example.org:8080/rsocket"); +val requester = RSocketRequester.builder().webSocket(url) +``` + +上面的连接不是立即的。当提出请求时,将透明地建立并使用共享连接。 + +##### 连接设置 + +`RSocketRequester.Builder`提供了以下自定义初始化`SETUP`框架的方法: + +* `dataMimeType(MimeType)`—为连接上的数据设置 MIME 类型。 + +* `metadataMimeType(MimeType)`—为连接上的元数据设置 MIME 类型。 + +* `setupData(Object)`—要包含在`SETUP`中的数据。 + +* `setupRoute(String, Object…​)`—将元数据中的路由包含在`SETUP`中。 + +* `setupMetadata(Object, MimeType)`—要包含在`SETUP`中的其他元数据。 + +对于数据,默认的 MIME 类型是从第一个配置的`Decoder`派生的。对于元数据,默认的 MIME 类型是[复合元数据](https://github.com/rsocket/rsocket/blob/master/Extensions/CompositeMetadata.md),它允许每个请求有多个元数据值和 MIME 类型对。通常情况下,这两种情况都不需要改变。 + +`SETUP`框架中的数据和元数据是可选的。在服务器端,[@ConnectMapping](#rsocket-annot-connectmapping)方法可用于处理连接的开始和`SETUP`框架的内容。元数据可用于连接级别的安全性。 + +##### Strategies + +`RSocketRequester.Builder`接受`RSocketStrategies`来配置请求者。你将需要使用它来为数据和元数据值的(反)序列化提供编码器和解码器。默认情况下,只注册来自`spring-core`的`String`、`byte[]` 和`ByteBuffer`的基本编解码器。添加`spring-web`可以访问更多可以按以下方式注册的内容: + +爪哇 + +``` +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); +``` + +Kotlin + +``` +val strategies = RSocketStrategies.builder() + .encoders { it.add(Jackson2CborEncoder()) } + .decoders { it.add(Jackson2CborDecoder()) } + .build() + +val requester = RSocketRequester.builder() + .rsocketStrategies(strategies) + .tcp("localhost", 7000) +``` + +`RSocketStrategies`是为重复使用而设计的。在一些场景中,例如客户端和服务器在相同的应用程序中,可以优选地在 Spring 配置中对其进行声明。 + +##### 客户响应者 + +`RSocketRequester.Builder`可用于配置来自服务器的请求的响应程序。 + +你可以使用带注释的处理程序进行客户端响应,该处理程序基于服务器上使用的相同基础设施,但以编程方式注册,如下所示: + +爪哇 + +``` +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**|如果存在`spring-web`,则使用`PathPatternRouteMatcher`,以进行有效的<br/>路由匹配。| +|-----|--------------------------------------------------------------------------------------------| +|**2**|从具有`@MessageMaping`和/或`@ConnectMapping`方法的类创建响应器。| +|**3**|登记响应者。| + +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**|如果存在`spring-web`,则使用`PathPatternRouteMatcher`,以进行有效的<br/>路由匹配。| +|-----|--------------------------------------------------------------------------------------------| +|**2**|从具有`@MessageMaping`和/或`@ConnectMapping`方法的类创建响应器。| +|**3**|登记响应者。| + +注意,上面只是为客户端响应者的程序化注册设计的一个快捷方式。对于客户机响应者处于 Spring 配置中的替代场景,你仍然可以将`RSocketMessageHandler`声明为 Spring Bean,然后按以下方式应用: + +爪哇 + +``` +ApplicationContext context = ... ; +RSocketMessageHandler handler = context.getBean(RSocketMessageHandler.class); + +RSocketRequester requester = RSocketRequester.builder() + .rsocketConnector(connector -> connector.acceptor(handler.responder())) + .tcp("localhost", 7000); +``` + +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) +``` + +对于上述情况,还可能需要使用`setHandlerPredicate`中的`RSocketMessageHandler`来切换到用于检测客户端响应者的不同策略,例如基于诸如`@RSocketClientResponder`VS 的默认`@Controller`的自定义注释。在使用客户机和服务器,或者在同一个应用程序中有多个客户机的情况下,这是必要的。 + +有关编程模型的更多信息,请参见[附加注释的响应者](#rsocket-annot-responders)。 + +##### 高级 + +`RSocketRequesterBuilder`提供了一个回调,以公开底层的 `io.rsocket.core.rsocketConnector`,以获取关于保持活动间隔、会话恢复、拦截器等的更多配置选项。你可以按以下方式配置该级别的选项: + +爪哇 + +``` +RSocketRequester requester = RSocketRequester.builder() + .rsocketConnector(connector -> { + // ... + }) + .tcp("localhost", 7000); +``` + +Kotlin + +``` +val requester = RSocketRequester.builder() + .rsocketConnector { + //... + } + .tcp("localhost", 7000) +``` + +#### 5.2.2.服务器请求者 + +要从服务器向连接的客户机发出请求,需要从服务器获得连接的客户机的请求者。 + +在[附加注释的响应者](#rsocket-annot-responders)中,`@ConnectMapping`和`@MessageMapping`方法支持 `rsocketRequester’参数。使用它来访问连接的请求者。请记住,`@ConnectMapping`方法本质上是`SETUP`框架的处理程序,在开始请求之前必须对其进行处理。因此,从一开始就必须将请求与处理分离开来。例如: + +爪哇 + +``` +@ConnectMapping +Mono<Void> handle(RSocketRequester requester) { + requester.route("status").data("5") + .retrieveFlux(StatusReport.class) + .subscribe(bar -> { (1) + // ... + }); + return ... (2) +} +``` + +|**1**|异步启动请求,与处理无关。| +|-----|------------------------------------------------------------| +|**2**|执行处理并返回完成`Mono<Void>`。| + +Kotlin + +``` +@ConnectMapping +suspend fun handle(requester: RSocketRequester) { + GlobalScope.launch { + requester.route("status").data("5").retrieveFlow<StatusReport>().collect { (1) + // ... + } + } + /// ... (2) +} +``` + +|**1**|异步启动请求,与处理无关。| +|-----|------------------------------------------------------------| +|**2**|在挂起功能中执行处理。| + +#### 5.2.3.请求 + +一旦有了[client](#rsocket-requester-client)或[server](#rsocket-requester-server)请求者,你可以按以下方式进行请求: + +爪哇 + +``` +ViewBox viewBox = ... ; + +Flux<AirportLocation> locations = requester.route("locate.radars.within") (1) + .data(viewBox) (2) + .retrieveFlux(AirportLocation.class); (3) +``` + +|**1**|指定要包含在请求消息的元数据中的路由。| +|-----|------------------------------------------------------------------| +|**2**|为请求消息提供数据。| +|**3**|声明预期的响应。| + +Kotlin + +``` +val viewBox: ViewBox = ... + +val locations = requester.route("locate.radars.within") (1) + .data(viewBox) (2) + .retrieveFlow<AirportLocation>() (3) +``` + +|**1**|指定要包含在请求消息的元数据中的路由。| +|-----|------------------------------------------------------------------| +|**2**|为请求消息提供数据。| +|**3**|声明预期的响应。| + +交互类型是由输入和输出的基数隐式确定的。上面的示例是`Request-Stream`,因为发送了一个值并接收了一个值流。在大多数情况下,只要输入和输出的选择与 RSocket 交互类型以及响应者期望的输入和输出类型相匹配,就不需要考虑这个问题。无效组合的唯一示例是多对一。 + +`data(Object)`方法还接受任何活性流`Publisher`,包括 `flux’和`Mono`,以及在 `reactiveAdapterRegistry’中注册的任何其他价值产生者。对于产生相同类型的值的多值`Publisher`,例如`Flux`,可以考虑使用重载的`data`方法之一,以避免对每个元素进行类型检查和`Encoder`查找: + +``` +data(Object producer, Class<?> elementClass); +data(Object producer, ParameterizedTypeReference<?> elementTypeRef); +``` + +`data(Object)`步骤是可选的。对于不发送数据的请求,跳过它: + +爪哇 + +``` +Mono<AirportLocation> location = requester.route("find.radar.EWR")) + .retrieveMono(AirportLocation.class); +``` + +Kotlin + +``` +import org.springframework.messaging.rsocket.retrieveAndAwait + +val location = requester.route("find.radar.EWR") + .retrieveAndAwait<AirportLocation>() +``` + +如果使用[复合元数据](https://github.com/rsocket/rsocket/blob/master/Extensions/CompositeMetadata.md)(默认值),并且如果已注册的`Encoder`支持这些值,则可以添加额外的元数据值。例如: + +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); +``` + +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>() +``` + +对于`Fire-and-Forget`,使用`send()`方法,返回`Mono<Void>`。请注意,`Mono`仅表示消息已成功发送,而不表示消息已被处理。 + +对于`Metadata-Push`,使用带有`Mono<Void>`返回值的`sendMetadata()`方法。 + +### 5.3.附加注释的响应者 + +RSocket 响应器可以实现为`@MessageMapping`和`@ConnectMapping`方法。@MessageMapping 方法处理单个请求,而`@ConnectMapping`方法处理连接级事件(设置和元数据推送)。带注释的响应器是对称支持的,用于从服务器端响应和从客户端响应。 + +#### 5.3.1.服务器响应者 + +要在服务器端使用带注释的响应器,将`RSocketMessageHandler`添加到你的 Spring 配置中,以检测`@Controller`带有`@MessageMapping`和`@ConnectMapping`的 bean 方法: + +Java + +``` +@Configuration +static class ServerConfig { + + @Bean + public RSocketMessageHandler rsocketMessageHandler() { + RSocketMessageHandler handler = new RSocketMessageHandler(); + handler.routeMatcher(new PathPatternRouteMatcher()); + return handler; + } +} +``` + +Kotlin + +``` +@Configuration +class ServerConfig { + + @Bean + fun rsocketMessageHandler() = RSocketMessageHandler().apply { + routeMatcher = PathPatternRouteMatcher() + } +} +``` + +然后通过 Java RSocket API 启动一个 RSocket 服务器,并为响应者插入“rsocketMessageHandler”,如下所示: + +Java + +``` +ApplicationContext context = ... ; +RSocketMessageHandler handler = context.getBean(RSocketMessageHandler.class); + +CloseableChannel server = + RSocketServer.create(handler.responder()) + .bind(TcpServerTransport.create("localhost", 7000)) + .block(); +``` + +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`默认支持[composite](https://github.com/rsocket/rsocket/blob/master/Extensions/CompositeMetadata.md)和[routing](https://github.com/rsocket/rsocket/blob/master/Extensions/Routing.md)元数据。如果需要切换到不同的 MIME 类型或注册其他元数据 MIME 类型,则可以设置其[MetadataExtractor](#rsocket-metadata-extractor)。 + +你需要设置元数据和数据格式所需支持的`Encoder`和`Decoder`实例。你可能需要`spring-web`模块来实现编解码。 + +默认情况下,`SimpleRouteMatcher`用于通过`AntPathMatcher`匹配路由。我们建议插入`PathPatternRouteMatcher`中的`spring-web`以进行有效的路线匹配。RSocket 路由可以是分层的,但不是 URL 路径。这两个路由匹配器都被配置为默认使用“.”作为分隔符,并且没有像 HTTP URL 那样的 URL 解码。 + +`RSocketMessageHandler`可以通过`RSocketStrategies`进行配置,如果你需要在同一进程中在客户机和服务器之间共享配置,这可能会很有用: + +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(); + } +} +``` + +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() +} +``` + +#### 5.3.2.客户响应者 + +需要在“rsocketrequester.builder”中配置客户端的带注释的响应程序。详见[客户响应者](#rsocket-requester-client-responder)。 + +#### 5.3.3.@MessageMapping + +一旦[server](#rsocket-annot-responders-server)或[client](#rsocket-annot-responders-client)响应者配置到位,`@MessageMapping’方法可按以下方式使用: + +Java + +``` +@Controller +public class RadarsController { + + @MessageMapping("locate.radars.within") + public Flux<AirportLocation> radars(MapRequest request) { + // ... + } +} +``` + +Kotlin + +``` +@Controller +class RadarsController { + + @MessageMapping("locate.radars.within") + fun radars(request: MapRequest): Flow<AirportLocation> { + // ... + } +} +``` + +上面的`@MessageMapping`方法响应具有“locate.radars.within”路由的请求-流交互。它支持灵活的方法签名,可以选择使用以下方法参数: + +| Method Argument |说明| +|------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `@Payload` |请求的有效载荷。这可以是异步类型的一个具体值,如 `mono’或`Flux`。<br/><br/>** 注:** 注释的使用是可选的。一个方法参数不是简单的类型<br/>,也不是任何其他受支持的参数,假定它是预期的有效负载。| +| `RSocketRequester` |向远端发出请求的请求者。| +| `@DestinationVariable` |根据映射模式中的变量从路线中提取的值,例如 `@MessageMapping(“find.radar.{id}”)`。| +| `@Header` |按照[MetadataExtractor](#rsocket-metadata-extractor)中所述,为提取而注册的元数据值。| +|`@Headers Map<String, Object>`|按照[MetadataExtractor](#rsocket-metadata-extractor)中所述,为提取而注册的所有元数据值。| + +返回值应该是一个或多个要序列化为响应有效负载的对象。这可以是异步类型,如`Mono`或`Flux`,具体值,或`void`或无值异步类型,如`Mono<Void>`。 + +`@MessageMapping`方法支持的 RSocket 交互类型是由输入的基数(即`@Payload`参数)和输出的值,其中基数表示如下: + +|Cardinality|说明| +|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 1 |要么是显式的值,要么是单值异步类型,如`Mono<T>`。| +| Many |一种多值异步类型,如`Flux<T>`。| +| 0 |对于输入,这意味着该方法没有`@Payload`参数。对于输出,这是<br/><br/>,这是`void`或无值异步类型,例如`Mono<Void>`。| + +下表显示了所有输入和输出基数组合以及相应的交互类型: + +|Input Cardinality|Output Cardinality|交互类型| +|-----------------|------------------|---------------------------------| +| 0, 1 | 0 |先开火后遗忘、请求-响应| +| 0, 1 | 1 |请求-响应| +| 0, 1 | Many |请求流| +| Many | 0, 1, Many |请求通道| + +#### 5.3.4.@ConnectMapping + +`@ConnectMapping`处理 RSocket 连接开始时的`SETUP`框架,以及通过`METADATA_PUSH`框架的任何后续元数据推送通知,即`io.rsocket.RSocket`中的 `MetadataPush(有效负载)’`。 + +`@ConnectMapping`方法支持与[@MessageMapping](#rsocket-annot-messagemapping)相同的参数,但基于元数据和来自`SETUP`和 `metadata_push’框架的数据。`@ConnectMapping`可以使用一种模式,将处理范围缩小到在元数据中具有路由的特定连接,或者如果没有声明任何模式,则所有连接都匹配。 + +`@ConnectMapping`方法不能返回数据,必须以`void`或 `mono<Void>` 作为返回值来声明。如果处理返回一个新连接的错误,那么该连接将被拒绝。在向`RSocketRequester`请求连接时,不能停止处理。详见[服务器请求者](#rsocket-requester-server)。 + +### 5.4.MetadataExtractor + +响应者必须解释元数据。[复合元数据](https://github.com/rsocket/rsocket/blob/master/Extensions/CompositeMetadata.md)允许独立格式化的元数据值(例如用于路由、安全性、跟踪),每个值都具有自己的 MIME 类型。应用程序需要一种方法来配置元数据来支持 MIME 类型,以及一种方法来访问提取的值。 + +`MetadataExtractor`是一种契约,它接受序列化的元数据并返回经过解码的名称-值对,然后可以像头一样通过名称进行访问,例如通过注释处理程序方法中的`@Header`。 + +`DefaultMetadataExtractor`可以给出`Decoder`实例来解码元数据。开箱即用,它内置了对[“message/x.rsocket.routing.v0”](https://github.com/rsocket/rsocket/blob/master/Extensions/Routing.md)的支持,它将其解码为 `string’,并保存在“route”键下。对于任何其他 MIME 类型,你需要提供`Decoder`,并按以下方式注册 MIME 类型: + +Java + +``` +DefaultMetadataExtractor extractor = new DefaultMetadataExtractor(metadataDecoders); +extractor.metadataToExtract(fooMimeType, Foo.class, "foo"); +``` + +Kotlin + +``` +import org.springframework.messaging.rsocket.metadataToExtract + +val extractor = DefaultMetadataExtractor(metadataDecoders) +extractor.metadataToExtract<Foo>(fooMimeType, "foo") +``` + +复合元数据可以很好地组合独立的元数据值。但是,请求者可能不支持复合元数据,或者可能选择不使用它。为此,“DefaultMetadataExtractor”可能需要自定义逻辑来将已解码的值映射到输出映射。下面是一个使用 JSON 进行元数据处理的示例: + +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); + }); +``` + +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) +} +``` + +当通过`MetadataExtractor`配置`RSocketStrategies`时,你可以让 `rsocketStrategies.Builder` 使用已配置的解码器创建提取器,然后简单地使用回调来定制注册,如下所示: + +Java + +``` +RSocketStrategies strategies = RSocketStrategies.builder() + .metadataExtractorRegistry(registry -> { + registry.metadataToExtract(fooMimeType, Foo.class, "foo"); + // ... + }) + .build(); +``` + +Kotlin + +``` +import org.springframework.messaging.rsocket.metadataToExtract + +val strategies = RSocketStrategies.builder() + .metadataExtractorRegistry { registry: MetadataExtractorRegistry -> + registry.metadataToExtract<Foo>(fooMimeType, "foo") + // ... + } + .build() +``` + +## 6. 反应库 + +`spring-webflux`依赖于`reactor-core`,并在内部使用它来组成异步逻辑并提供反应流支持。通常,WebFlux API 返回`Flux`或“mono”(因为这些 API 是内部使用的),并宽容地接受任何反应流“publisher”实现作为输入。使用`Flux`相对于`Mono`是很重要的,因为它有助于表示基数——例如,预期是单个还是多个异步值,这对于做出决策(例如,在编码或解码 HTTP 消息时)是必不可少的。 + +对于带注释的控制器,WebFlux 透明地适应应用程序选择的反应库。这是在[“ReactiveAdapterRegistry”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/core/ReactiveAdapterRegistry.html)的帮助下完成的,它为反应库和其他异步类型提供了可插入的支持。注册中心内置了对 RXJava3、 Kotlin 协程和 SmallRye Mutiny 的支持,但你也可以注册其他第三方适配器。 + +| |在 Spring Framework5.3.11 中,对 RXJava1 和 2 的支持是不受欢迎的,下面是<br/>RXJava 自己的 EOL 建议和对 RXJava3 的升级建议。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------| + +对于功能 API(例如[功能端点](#webflux-fn)、`WebClient`和其他),WebFlux API 的一般规则适用于-`Flux`和`Mono`作为返回值,并将反应流 `publisher’作为输入。当`Publisher`(无论是自定义的还是来自另一个反应库)被提供时,它只能被视为具有未知语义(0..n)的流。但是,如果语义是已知的,则可以用`Flux`或`Mono.from(Publisher)`来包装它,而不是传递 RAW`Publisher`。 + +例如,给定一个不是`Publisher`的`Mono`,JacksonJSON 消息编写器需要多个值。如果媒体类型意味着一个无限的流(例如,“Application/JSON+Stream”),则单独编写和刷新值。否则,值将被缓冲到列表中,并呈现为 JSON 数组。 diff --git a/docs/spring-framework/web-servlet.md b/docs/spring-framework/web-servlet.md new file mode 100644 index 0000000000000000000000000000000000000000..4c60e8f3099a5eaf29ce8cac178d428935ce4696 --- /dev/null +++ b/docs/spring-framework/web-servlet.md @@ -0,0 +1,9639 @@ +# Servlet 堆栈上的 Web + +文档的这一部分涵盖了对构建在 Servlet API 上并部署到 Servlet 容器上的 Servlet 堆栈 Web 应用程序的支持。个别章节包括[Spring MVC](#mvc)、[查看技术](#mvc-view)、[CORS Support](#mvc-cors)和[WebSocket Support](#websocket)。有关反应式堆栈 Web 应用程序,请参见[反应式堆栈上的 Web](web-reactive.html#spring-web-reactive)。 + +## 1. Spring Web MVC + +Spring Web MVC 是建立在 Servlet API 上的原始 Web 框架,并且从一开始就包含在 Spring 框架中。正式名称“ Spring Web MVC”来自其源模块的名称([`spring-webmvc`](https://github.com/spring-projects/spring-framework/tree/main/spring-webmvc)),但更常用的名称是“ Spring MVC”。 + +与 Spring Web MVC 并行, Spring Framework5.0 引入了一种反应式堆栈 Web 框架,其名称“ Spring WebFlux”也基于其源模块([`spring-webflux`](https://github.com/spring-projects/spring-framework/tree/main/spring-webflux))。本节涵盖 Spring Web MVC。[next section](web-reactive.html#spring-web-reactive)覆盖了 Spring WebFlux。 + +有关基线信息以及与 Servlet 容器和 爪哇 EE 版本范围的兼容性,请参见 Spring 框架[Wiki](https://github.com/spring-projects/spring-framework/wiki/Spring-Framework-Versions)。 + +### 1.1.DispatcherServlet + +[WebFlux](web-reactive.html#webflux-dispatcher-handler) + +Spring 与许多其他 Web 框架一样,MVC 是围绕前控制器模式设计的,其中中央`Servlet`、`DispatcherServlet`,提供用于请求处理的共享算法,而实际工作是通过可配置的委托组件来执行的。这个模型是灵活的,并支持不同的工作流程。 + +`DispatcherServlet`,就像任何`Servlet`一样,需要通过使用 爪哇 配置或在`web.xml`中根据 Servlet 规范进行声明和映射。反过来,`DispatcherServlet`使用 Spring 配置来发现它在请求映射、视图解析、异常处理、[and more](#mvc-servlet-special-bean-types)中需要的委托组件。 + +下面的 爪哇 配置示例注册并初始化了`DispatcherServlet`,这是由 Servlet 容器自动检测的(参见[Servlet Config](#mvc-container-config)): + +爪哇 + +``` +public class MyWebApplicationInitializer implements WebApplicationInitializer { + + @Override + public void onStartup(ServletContext servletContext) { + + // Load Spring web application configuration + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.register(AppConfig.class); + + // Create and register the DispatcherServlet + DispatcherServlet servlet = new DispatcherServlet(context); + ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet); + registration.setLoadOnStartup(1); + registration.addMapping("/app/*"); + } +} +``` + +Kotlin + +``` +class MyWebApplicationInitializer : WebApplicationInitializer { + + override fun onStartup(servletContext: ServletContext) { + + // Load Spring web application configuration + val context = AnnotationConfigWebApplicationContext() + context.register(AppConfig::class.java) + + // Create and register the DispatcherServlet + val servlet = DispatcherServlet(context) + val registration = servletContext.addServlet("app", servlet) + registration.setLoadOnStartup(1) + registration.addMapping("/app/*") + } +} +``` + +| |除了直接使用 ServletContext API 之外,你还可以扩展 `AbstractNotationConfigDispatcherServletInitializer’并覆盖特定的方法<gtr="447"/>(参见<gtr="446"/>下的示例)。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +| |对于编程用例,`GenericWebApplicationContext`可以用作<br/>的`AnnotationConfigWebApplicationContext`的替代项。有关详细信息,请参见[GenericWebApplicationContext](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/context/support/GenericWebApplicationContext.html)爪哇doc。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +以下`web.xml`配置寄存器和初始化`DispatcherServlet`的示例: + +``` +<web-app> + + <listener> + <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> + </listener> + + <context-param> + <param-name>contextConfigLocation</param-name> + <param-value>/WEB-INF/app-context.xml</param-value> + </context-param> + + <servlet> + <servlet-name>app</servlet-name> + <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> + <init-param> + <param-name>contextConfigLocation</param-name> + <param-value></param-value> + </init-param> + <load-on-startup>1</load-on-startup> + </servlet> + + <servlet-mapping> + <servlet-name>app</servlet-name> + <url-pattern>/app/*</url-pattern> + </servlet-mapping> + +</web-app> +``` + +| |Spring 启动遵循不同的初始化顺序。 Spring boot 使用 Spring 配置来<br/>bootstrap 本身和嵌入的 Servlet 容器,而不是连接到 Servlet 容器的生命周期<br/>。`Filter`和`Servlet`声明<br/>在 Spring 配置中检测到并在 Servlet 容器中注册。<br/>有关更多详细信息,请参见[Spring Boot documentation](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-embedded-container)。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.1.1.上下文层次结构 + +`DispatcherServlet`对其自身的配置期望`WebApplicationContext`(普通的 `ApplicationContext’的扩展)。`WebApplicationContext`有一个到“servletContext”的链接和与其关联的`Servlet`。它还绑定到`ServletContext`,使得应用程序可以在`RequestContextUtils`上使用静态方法来查找“WebApplicationContext”(如果需要访问它)。 + +对于许多应用程序来说,拥有一个`WebApplicationContext`是简单且足够的。还可以有一个上下文层次结构,其中一个根`WebApplicationContext`在多个`DispatcherServlet`(或其他`Servlet`)实例之间共享,每个实例都有自己的子实例`WebApplicationContext`配置。有关上下文层次结构特性的更多信息,请参见[Additional Capabilities of the `ApplicationContext`](core.html#context-introduction)。 + +根`WebApplicationContext`通常包含基础设施 bean,例如需要跨多个`Servlet`实例共享的数据存储库和业务服务。这些 bean 被有效地继承,并且可以在 Servlet 特定的子`WebApplicationContext`中被重写(即重新声明),该子`Servlet`通常包含给定的`Servlet`本地的 bean。下图显示了这种关系: + +![MVC 上下文层次结构](images/mvc-context-hierarchy.png) + +以下示例配置了`WebApplicationContext`层次结构: + +爪哇 + +``` +public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { + + @Override + protected Class<?>[] getRootConfigClasses() { + return new Class<?>[] { RootConfig.class }; + } + + @Override + protected Class<?>[] getServletConfigClasses() { + return new Class<?>[] { App1Config.class }; + } + + @Override + protected String[] getServletMappings() { + return new String[] { "/app1/*" }; + } +} +``` + +Kotlin + +``` +class MyWebAppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() { + + override fun getRootConfigClasses(): Array<Class<*>> { + return arrayOf(RootConfig::class.java) + } + + override fun getServletConfigClasses(): Array<Class<*>> { + return arrayOf(App1Config::class.java) + } + + override fun getServletMappings(): Array<String> { + return arrayOf("/app1/*") + } +} +``` + +| |如果不需要应用程序上下文层次结构,则应用程序可以通过`getRootConfigClasses()`和`null`从`getServletConfigClasses()`返回所有<br/>配置。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +下面的示例显示了`web.xml`的等价值: + +``` +<web-app> + + <listener> + <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> + </listener> + + <context-param> + <param-name>contextConfigLocation</param-name> + <param-value>/WEB-INF/root-context.xml</param-value> + </context-param> + + <servlet> + <servlet-name>app1</servlet-name> + <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> + <init-param> + <param-name>contextConfigLocation</param-name> + <param-value>/WEB-INF/app1-context.xml</param-value> + </init-param> + <load-on-startup>1</load-on-startup> + </servlet> + + <servlet-mapping> + <servlet-name>app1</servlet-name> + <url-pattern>/app1/*</url-pattern> + </servlet-mapping> + +</web-app> +``` + +| |如果不需要应用程序上下文层次结构,则应用程序可以仅配置<br/>“root”上下文,并将`contextConfigLocation` Servlet 参数保留为空。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.1.2.特殊 Bean 类型 + +[WebFlux](web-reactive.html#webflux-special-bean-types) + +`DispatcherServlet`将委托给特殊的 bean 来处理请求并呈现适当的响应。“特殊 bean”指的是实现框架契约的 Spring-managed`Object`实例。这些通常带有内置契约,但你可以自定义它们的属性并扩展或替换它们。 + +下表列出了`DispatcherServlet`检测到的特殊 bean: + +| Bean type |解释| +|-------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `HandlerMapping` |将请求与[interceptors](#mvc-handlermapping-interceptor)的列表一起映射到处理程序,以进行预处理和后处理,<br/>映射基于一些条件,其中的细节取决于`HandlerMapping`实现。<br/><br/>两个主要的`HandlerMapping`实现是`RequestMappingHandlerMapping`(它支持`@RequestMapping`注释的方法)和`SimpleUrlHandlerMapping`(它维护对处理程序的 URI 路径模式的显式注册)。| +| `HandlerAdapter` |帮助`DispatcherServlet`调用映射到请求的处理程序,而不考虑<br/>实际调用处理程序的方式。例如,调用带注释的控制器<br/>需要解析注释。a`HandlerAdapter`的主要目的是<br/>使`DispatcherServlet`不受这些细节的影响。| +| [`HandlerExceptionResolver`](#mvc-exceptionhandlers) |解决异常的策略,可能将异常映射到处理程序、HTML 错误<br/>视图或其他目标。见[Exceptions](#mvc-exceptionhandlers)。| +| [`ViewResolver`](#mvc-viewresolver) |将从处理程序返回的基于逻辑`String`的视图名称解析为实际的`View`,并将其呈现给响应。见[View Resolution](#mvc-viewresolver)和[查看技术](#mvc-view)。| +|[`LocaleResolver`](#mvc-localeresolver), [LocaleContextResolver](#mvc-timezone)|解析客户端正在使用的`Locale`以及可能的时区,以便能够<br/>提供国际化的视图。见[Locale](#mvc-localeresolver)。| +| [`ThemeResolver`](#mvc-themeresolver) |解析 Web 应用程序可以使用的主题——例如,提供个性化的布局。<br/>参见[Themes](#mvc-themeresolver)。| +| [`MultipartResolver`](#mvc-multipart) |用于解析具有<br/>的多部分请求(例如,浏览器表单文件上传)的抽象,需要借助一些多部分解析库。见[多部分旋转变压器](#mvc-multipart)。| +| [`FlashMapManager`](#mvc-flash-attributes) |存储和检索“输入”和“输出”`FlashMap`,它们可以用于将<br/>属性从一个请求传递到另一个请求,通常是通过重定向。<br/>参见[flash 属性](#mvc-flash-attributes)。| + +#### 1.1.3.Web MVC 配置 + +[WebFlux](web-reactive.html#webflux-framework-config) + +应用程序可以声明[Special Bean Types](#mvc-servlet-special-bean-types)中列出的处理请求所需的基础设施 bean。`DispatcherServlet`检查每个特殊的“WebApplicationContext” Bean。如果没有匹配的 Bean 类型,则返回到[`DispatcherServlet.properties’](https://github.com/spring-projects/spring-framework/tree/main/spring-webmvc/src/main/resources/org/springframework/web/servlet/DispatcherServlet.properties)中列出的默认类型。 + +在大多数情况下,[MVC Config](#mvc-config)是最好的起点。它用 爪哇 或 XML 声明所需的 bean,并提供一个更高级别的配置回调 API 来定制它。 + +| |Spring Boot 依赖于 MVC 爪哇 配置来配置 Spring MVC,并且<br/>提供了许多额外的方便选项。| +|---|------------------------------------------------------------------------------------------------------------------------| + +#### 1.1.4. Servlet 配置 + +在 Servlet 3.0+ 环境中,你可以选择以编程方式配置 Servlet 容器作为替代方案,或者与`web.xml`文件组合。下面的示例注册了`DispatcherServlet`: + +爪哇 + +``` +import org.springframework.web.WebApplicationInitializer; + +public class MyWebApplicationInitializer implements WebApplicationInitializer { + + @Override + public void onStartup(ServletContext container) { + XmlWebApplicationContext appContext = new XmlWebApplicationContext(); + appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml"); + + ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet(appContext)); + registration.setLoadOnStartup(1); + registration.addMapping("/"); + } +} +``` + +Kotlin + +``` +import org.springframework.web.WebApplicationInitializer + +class MyWebApplicationInitializer : WebApplicationInitializer { + + override fun onStartup(container: ServletContext) { + val appContext = XmlWebApplicationContext() + appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml") + + val registration = container.addServlet("dispatcher", DispatcherServlet(appContext)) + registration.setLoadOnStartup(1) + registration.addMapping("/") + } +} +``` + +`WebApplicationInitializer`是由 Spring MVC 提供的一个接口,该接口确保检测到你的实现并自动用于初始化任何 Servlet 3 容器。名为 `AbstractDispatcherServletInitializer’的<gtR="532"/>的抽象基类实现使得通过覆盖方法来指定 Servlet 映射和<gtR="533"/>配置的位置来注册 `DispatcherServlet’变得更加容易。 + +对于使用基于 爪哇 的 Spring 配置的应用程序,推荐这样做,如下例所示: + +爪哇 + +``` +public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { + + @Override + protected Class<?>[] getRootConfigClasses() { + return null; + } + + @Override + protected Class<?>[] getServletConfigClasses() { + return new Class<?>[] { MyWebConfig.class }; + } + + @Override + protected String[] getServletMappings() { + return new String[] { "/" }; + } +} +``` + +Kotlin + +``` +class MyWebAppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() { + + override fun getRootConfigClasses(): Array<Class<*>>? { + return null + } + + override fun getServletConfigClasses(): Array<Class<*>>? { + return arrayOf(MyWebConfig::class.java) + } + + override fun getServletMappings(): Array<String> { + return arrayOf("/") + } +} +``` + +如果使用基于 XML 的 Spring 配置,则应该直接从 `AbstractDispatcherServletInitializer’扩展,如下例所示: + +爪哇 + +``` +public class MyWebAppInitializer extends AbstractDispatcherServletInitializer { + + @Override + protected WebApplicationContext createRootApplicationContext() { + return null; + } + + @Override + protected WebApplicationContext createServletApplicationContext() { + XmlWebApplicationContext cxt = new XmlWebApplicationContext(); + cxt.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml"); + return cxt; + } + + @Override + protected String[] getServletMappings() { + return new String[] { "/" }; + } +} +``` + +Kotlin + +``` +class MyWebAppInitializer : AbstractDispatcherServletInitializer() { + + override fun createRootApplicationContext(): WebApplicationContext? { + return null + } + + override fun createServletApplicationContext(): WebApplicationContext { + return XmlWebApplicationContext().apply { + setConfigLocation("/WEB-INF/spring/dispatcher-config.xml") + } + } + + override fun getServletMappings(): Array<String> { + return arrayOf("/") + } +} +``` + +`AbstractDispatcherServletInitializer`还提供了一种方便的方式来添加`Filter`实例,并将它们自动映射到`DispatcherServlet`,如下例所示: + +爪哇 + +``` +public class MyWebAppInitializer extends AbstractDispatcherServletInitializer { + + // ... + + @Override + protected Filter[] getServletFilters() { + return new Filter[] { + new HiddenHttpMethodFilter(), new CharacterEncodingFilter() }; + } +} +``` + +Kotlin + +``` +class MyWebAppInitializer : AbstractDispatcherServletInitializer() { + + // ... + + override fun getServletFilters(): Array<Filter> { + return arrayOf(HiddenHttpMethodFilter(), CharacterEncodingFilter()) + } +} +``` + +每个过滤器都会根据其具体类型添加一个默认名称,并自动映射到`DispatcherServlet`。 + +`isAsyncSupported`的`AbstractDispatcherServletInitializer`保护方法提供了一个位置,可以在`DispatcherServlet`和映射到它的所有过滤器上启用异步支持。默认情况下,此标志设置为`true`。 + +最后,如果需要进一步自定义`DispatcherServlet`本身,则可以重写`createDispatcherServlet`方法。 + +#### 1.1.5.处理 + +[WebFlux](web-reactive.html#webflux-dispatcher-handler-sequence) + +`DispatcherServlet`按以下方式处理请求: + +* 在请求中搜索并绑定`WebApplicationContext`,将其作为控制器和流程中的其他元素可以使用的属性。默认情况下,它在`DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE`键下绑定。 + +* Locale 解析器绑定到请求,以让流程中的元素解析在处理请求(呈现视图、准备数据等)时使用的 Locale。如果不需要区域设置解析,则不需要区域设置解析程序。 + +* 主题解析程序绑定到请求,以便让视图等元素决定使用哪个主题。如果你不使用主题,你可以忽略它。 + +* 如果指定了多部分文件解析器,则会检查请求的多部分。如果发现了多个部分,则将请求包装在`MultipartHttpServletRequest`中,以便由流程中的其他元素进行进一步处理。有关多部件处理的更多信息,请参见[多部分旋转变压器](#mvc-multipart)。 + +* 将搜索一个合适的处理程序。如果找到了一个处理程序,则运行与该处理程序(预处理器、后处理器和控制器)相关联的执行链,以便为呈现准备一个模型。或者,对于带注释的控制器,可以呈现响应(在`HandlerAdapter`内),而不是返回视图。 + +* 如果返回了一个模型,则呈现该视图。如果没有返回模型(可能是由于预处理器或后处理器拦截了请求,可能是出于安全原因),则不呈现视图,因为该请求可能已经满足。 + +在`WebApplicationContext`中声明的`HandlerExceptionResolver`bean 用于解决请求处理过程中抛出的异常。这些异常解析器允许定制逻辑来处理异常。有关更多详细信息,请参见[Exceptions](#mvc-exceptionhandlers)。 + +对于 HTTP 缓存支持,处理程序可以使用`checkNotModified`的`WebRequest`方法,以及[控制器的 HTTP 缓存](#mvc-caching-etag-lastmodified)中描述的注释控制器的其他选项。 + +你可以通过在“web.xml”文件中的 Servlet 声明中添加 Servlet 初始化参数(“init-param”元素)来定制单个`DispatcherServlet`实例。下表列出了支持的参数: + +| Parameter |解释| +|--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `contextClass` |实现`ConfigurableWebApplicationContext`的类,将其实例化并由此 Servlet 本地配置<br/>。默认情况下,使用`XmlWebApplicationContext`。| +| `contextConfigLocation` |传递到上下文实例(由`contextClass`指定)到<br/>的字符串,指示可以在哪里找到上下文。该字符串可能由多个<br/>字符串组成(使用逗号作为分隔符),以支持多个上下文。在<br/>具有两次定义的 bean 的多个上下文位置的情况下,最新的位置<br/>优先。| +| `namespace` |`WebApplicationContext`的命名空间。默认为`[servlet-name]-servlet`。| +|`throwExceptionIfNoHandlerFound`|当没有为请求找到处理程序时,是否抛出`NoHandlerFoundException`。<br/>然后可以使用`HandlerExceptionResolver`(例如,通过使用 `@ExceptionHandler`Controller 方法)捕获异常,并将其作为其他方法处理。,<br/><br/>默认情况下,将其设置为`false`,在这种情况下,`DispatcherServlet`将<br/>响应状态设置为 404(不 \_found),而不会引发异常。<br/><br/>注意,如果[default servlet handling](#mvc-default-servlet-handler)也配置了<br/>,则未解决的请求总是被转发到默认的 Servlet <br/>,并且永远不会引发 404。| + +#### 1.1.6.路径匹配 + +Servlet API 将完整的请求路径公开为`requestURI`,并进一步将其细分为`contextPath`、`servletPath`和`pathInfo`,其值随 Servlet 映射的方式而变化。 Spring 从这些输入中,MVC 需要确定要用于处理程序映射的查找路径,这是`DispatcherServlet`本身的映射内的路径,不包括`contextPath`和任何`servletMapping`前缀(如果存在的话)。 + +对`servletPath`和`pathInfo`进行了解码,这使得它们不可能直接与完整的`requestURI`进行比较,从而得出查找路径,这使得有必要对`requestURI`进行解码。然而,这也带来了自身的问题,因为路径可能包含编码的保留字符,例如`"/"`或`";"`,这些字符在解码后可能会反过来改变路径的结构,这也可能导致安全问题。此外, Servlet 容器可以在不同程度上对`servletPath`进行规范化,这使得进一步不可能对`startsWith`进行`requestURI`的比较。 + +这就是为什么最好避免依赖基于前缀的`servletPath`映射类型所提供的`servletPath`。如果`DispatcherServlet`被映射为带有`"/"`的缺省 Servlet,或者以其他方式不使用带有`"/*"`的前缀,并且 Servlet 容器是 4.0+,那么 Spring MVC 能够检测 Servlet 映射类型并完全避免使用`servletPath`和`pathInfo`。在 3.1 Servlet 容器上,假设具有相同的 Servlet 映射类型,可以通过在 MVC 配置中通过[Path Matching](#mvc-config-path-matching)提供带有`alwaysUseFullPath=true`的`UrlPathHelper`来实现等效的映射类型。 + +幸运的是,默认的 Servlet 映射`"/"`是一个很好的选择。然而,仍然存在一个问题,即需要对`requestURI`进行解码,以便能够与控制器映射进行比较。这也是不希望的,因为有可能对保留的字符进行解码,从而改变路径结构。如果不需要这样的字符,那么你可以拒绝它们(比如 Spring Security HTTP 防火墙),或者可以使用`urlDecode=false`配置 `urlpathhelper’,但是控制器映射将需要与编码路径匹配,这可能并不总是很好地工作。此外,有时 `DispatcherServlet’需要与另一个 Servlet 共享 URL 空间,并且可能需要通过前缀进行映射。 + +上述问题可以通过从`PathMatcher`切换到解析的`PathPattern`在 5.3 或更高版本中可用,来更全面地解决,请参见[模式比较](#mvc-ann-requestmapping-pattern-comparison)。与`AntPathMatcher`(需要对查找路径进行解码或对控制器映射进行编码)不同,解析的`PathPattern`与解析的路径表示(称为`RequestPath`)匹配,一次只有一个路径段。这允许单独地对路径段值进行解码和消毒,而不存在更改路径结构的风险。解析的`PathPattern`还支持使用`servletPath`前缀映射,只要前缀保持简单,并且没有任何需要编码的字符。 + +#### 1.1.7.拦截 + +所有`HandlerMapping`实现都支持处理程序拦截器,当你想要将特定功能应用于某些请求时,这些截取程序非常有用——例如,检查主体。拦截器必须从 `org.springframework.web. Servlet ` 包中实现`HandlerInterceptor`,包中有三种方法,它们应该提供足够的灵活性来进行各种预处理和后处理: + +* `preHandle(..)`:在实际处理程序运行之前 + +* `postHandle(..)`:运行处理程序之后 + +* `afterCompletion(..)`:在完成完整的请求之后 + +`preHandle(..)`方法返回一个布尔值。你可以使用此方法来中断或继续执行链的处理。当此方法返回`true`时,处理程序执行链将继续。当它返回 false 时,`DispatcherServlet`假定拦截器本身已经处理了请求(并且,例如,呈现了一个适当的视图),并且不继续执行执行链中的其他拦截器和实际处理程序。 + +有关如何配置拦截器的示例,请参见 MVC 配置一节中的[Interceptors](#mvc-config-interceptors)。你还可以在单独的“handlermapping”实现上使用 setters 直接注册它们。 + +请注意,`postHandle`对于`@ResponseBody`和`ResponseEntity`方法不那么有用,因为它们的响应是在`HandlerAdapter`中和 `posthandle’之前编写和提交的。这意味着对响应进行任何更改都为时已晚,例如添加一个额外的标头。对于这样的场景,你可以实现`ResponseBodyAdvice`,并将其声明为[财务总监建议](#mvc-ann-controller-advice) Bean,或者直接在 `RequestMappinghandlerAdapter’上配置它。 + +#### 1.1.8.例外 + +[WebFlux](web-reactive.html#webflux-dispatcher-exceptions) + +如果在请求映射期间发生异常,或者从请求处理程序(例如`@Controller`)抛出异常,则`DispatcherServlet`将委托给`HandlerExceptionResolver`bean 的链来解决异常并提供替代处理,这通常是错误响应。 + +下表列出了可用的`HandlerExceptionResolver`实现: + +| `HandlerExceptionResolver` |说明| +|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `SimpleMappingExceptionResolver` |异常类名和错误视图名之间的映射。用于在浏览器应用程序中呈现<br/>错误页面。| +|[`DefaultHandlerExceptionResolver`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.html)|解析由 Spring MVC 引发的异常,并将它们映射到 HTTP 状态代码。<br/>另请参见备选方案`ResponseEntityExceptionHandler`和[REST API 异常](#mvc-ann-rest-exceptions)。| +| `ResponseStatusExceptionResolver` |用`@ResponseStatus`注释解决异常,并根据注释中的值将它们映射到 HTTP 状态<br/>代码。| +| `ExceptionHandlerExceptionResolver` |通过在`@Controller`或 `@controlleradvice’类中调用`@ExceptionHandler`方法来解决异常。见[@ExceptionHandler 方法](#mvc-ann-exceptionhandler)。| + +##### 解析器链 + +通过在 Spring 配置中声明多个`HandlerExceptionResolver`bean 并根据需要设置它们的`order`属性,可以形成异常解决器链。Order 属性越高,异常解析器的定位就越晚。 + +`HandlerExceptionResolver`的契约指定它可以返回: + +* 指向错误视图的`ModelAndView`。 + +* 如果异常是在解析程序中处理的,则为空`ModelAndView`。 + +* `null`如果异常仍然未解决,则供后续的解析器尝试,并且,如果异常仍然在最后,则允许将其冒泡到 Servlet 容器。 + +[MVC Config](#mvc-config)自动声明用于默认 Spring MVC 异常、`@ResponseStatus`注释异常和支持 `@ExceptionHandler’方法的内置解析器。你可以自定义该列表或替换它。 + +##### 容器错误页 + +Servlet 如果异常仍然被任何`HandlerExceptionResolver`未解决,因此被留作传播,或者如果响应状态被设置为错误状态(即,4xx,5xx), Servlet 容器可以在 HTML 中呈现默认的错误页。要定制容器的默认错误页,可以在`web.xml`中声明一个错误页映射。下面的示例展示了如何做到这一点: + +``` +<error-page> + <location>/error</location> +</error-page> +``` + +给出了前面的示例,当出现异常或者响应具有错误状态时, Servlet 容器在容器内对配置的 URL 进行错误分派(例如,`/error`)。然后由`DispatcherServlet`对此进行处理,可能将其映射到`@Controller`,该实现可用于返回带有模型的错误视图名称或呈现 JSON 响应,如下例所示: + +爪哇 + +``` +@RestController +public class ErrorController { + + @RequestMapping(path = "/error") + public Map<String, Object> handle(HttpServletRequest request) { + Map<String, Object> map = new HashMap<String, Object>(); + map.put("status", request.getAttribute("javax.servlet.error.status_code")); + map.put("reason", request.getAttribute("javax.servlet.error.message")); + return map; + } +} +``` + +Kotlin + +``` +@RestController +class ErrorController { + + @RequestMapping(path = ["/error"]) + fun handle(request: HttpServletRequest): Map<String, Any> { + val map = HashMap<String, Any>() + map["status"] = request.getAttribute("javax.servlet.error.status_code") + map["reason"] = request.getAttribute("javax.servlet.error.message") + return map + } +} +``` + +| |Servlet API 不提供在 爪哇 中创建错误页映射的方法。但是,你可以同时使用<br/>和极小值`web.xml`。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.1.9.视图分辨率 + +[WebFlux](web-reactive.html#webflux-viewresolution) + +Spring MVC 定义了`ViewResolver`和`View`接口,这些接口允许你在浏览器中呈现模型,而无需将你绑定到特定的视图技术。`ViewResolver`提供了视图名称和实际视图之间的映射。`View`处理在将数据移交给特定视图技术之前的准备工作。 + +下表提供了关于`ViewResolver`层次结构的更多详细信息: + +| ViewResolver |说明| +|--------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `AbstractCachingViewResolver` |它们解析的`AbstractCachingViewResolver`缓存视图实例的子类。<br/>缓存提高了某些视图技术的性能。通过将`cache`属性设置为`false`,可以关闭<br/>缓存。此外,如果你必须在运行时刷新<br/>某个视图(例如,当修改自由标记模板时),<br/>你可以使用`removeFromCache(String viewName, Locale loc)`方法。| +| `UrlBasedViewResolver` |`ViewResolver`接口的简单实现,它可以在没有显式映射定义的情况下直接将逻辑视图名称<br/>解析为 URL。<br/>如果你的逻辑名称以一种直接的方式匹配视图资源的名称<br/>,而不需要进行任意映射,那么这是合适的。| +| `InternalResourceViewResolver` |支持`InternalResourceView`(在<br/>effect 中,servlet 和 JSP)的`UrlBasedViewResolver`的方便子类和`TilesView`的子类。你可以<br/>通过使用`setViewClass(..)`为这个解析器生成的所有视图指定视图类。<br/>有关详细信息,请参见[UrlbasedViewResolver](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/reactive/result/view/UrlBasedViewResolver.html)爪哇doc。| +| `FreeMarkerViewResolver` |`UrlBasedViewResolver`的方便的子类,它支持`FreeMarkerView`和<br/>它们的自定义子类。| +|`ContentNegotiatingViewResolver`|实现`ViewResolver`接口,该接口根据<br/>请求文件名或`Accept`报头解析视图。见[内容协商](#mvc-multiple-representations)。| +| `BeanNameViewResolver` |`ViewResolver`接口的实现,该接口将视图名称解释为当前应用程序上下文中的<br/> Bean 名称。这是一个非常灵活的变体,其<br/>允许基于不同的视图名称混合和匹配不同的视图类型。<br/>每个这样的`View`都可以被定义为 Bean,例如在 XML 中或在配置类中。| + +##### 处理 + +[WebFlux](web-reactive.html#webflux-viewresolution-handling) + +你可以通过声明多个解析器 Bean 并在必要时通过设置`order`属性来指定顺序来链接视图解析器 Bean。请记住,Order 属性越高,视图解析器在链中的定位就越晚。 + +`ViewResolver`的契约指定它可以返回 null 以表示找不到视图。但是,在 JSP 和`InternalResourceViewResolver`的情况下,要确定 JSP 是否存在,唯一的方法是通过“RequestDispatcher”执行分派。因此,你必须始终将`InternalResourceViewResolver`配置为在视图解析程序的总顺序中的最后一个。 + +配置视图分辨率就像在 Spring 配置中添加`ViewResolver`bean 一样简单。[MVC Config](#mvc-config)为[View Resolvers](#mvc-config-view-resolvers)和添加无逻辑[视图控制器](#mvc-config-view-controller)提供了专用的配置 API,这对于没有控制器逻辑的 HTML 模板呈现非常有用。 + +##### 重定向 + +[WebFlux](web-reactive.html#webflux-redirecting-redirect-prefix) + +视图名称中的特殊`redirect:`前缀允许你执行重定向。“urlbasedViewResolver”(及其子类)将此视为需要重定向的指令。视图名称的其余部分是重定向 URL。 + +净效果与控制器返回`RedirectView`相同,但现在控制器本身可以根据逻辑视图名称进行操作。逻辑视图名称(例如`redirect:/myapp/some/resource`)相对于当前 Servlet 上下文重定向,而诸如`redirect:https://myhost.com/some/arbitrary/path`的名称重定向到一个绝对 URL。 + +请注意,如果控制器方法使用`@ResponseStatus`进行注释,则注释值优先于`RedirectView`设置的响应状态。 + +##### 转发 + +对于最终由`UrlBasedViewResolver`和子类解析的视图名称,也可以使用特殊的`forward:`前缀。这将创建一个“InternalResourceView”,它执行`RequestDispatcher.forward()`。因此,对于`InternalResourceViewResolver`和 `InternalResourceView’(用于 JSP),这个前缀是没有用的,但是如果你使用另一种视图技术,但是仍然希望强制由 Servlet/JSP 引擎处理一个资源的转发,那么这个前缀会很有帮助。请注意,你也可以链接多个视图解析程序。 + +##### 内容协商 + +[WebFlux](web-reactive.html#webflux-multiple-representations) + +[“ContentCongreatingViewResolver”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/servlet/view/ContentNegotiatingViewResolver.html)不解析视图本身,而是将其委托给其他视图解析程序,并选择与客户机请求的表示类似的视图。表示可以从`Accept`头或从查询参数(例如,`"/path?format=pdf"`)确定。 + +`ContentNegotiatingViewResolver`通过将请求媒体类型与`View`所支持的媒体类型(也称为 `content-type’)进行比较,选择一个适当的`View`来处理请求。列表中具有兼容的`Content-Type`的第一个`View`将表示形式返回给客户机。如果`ViewResolver`链不能提供兼容的视图,则会查阅通过`DefaultViews`属性指定的视图列表。后一种选项适用于单例`Views`,它可以呈现当前资源的适当表示形式,而不管逻辑视图名称是什么。`Accept`标头可以包括通配符(例如`text/*`),在这种情况下,其 `Content-Type’是`text/xml`的`View`是一个兼容的匹配。 + +有关配置细节,请参见[View Resolvers](#mvc-config-view-resolvers)下的[MVC Config](#mvc-config)。 + +#### 1.1.10.场所 + +Spring 体系结构的大多数部分都支持国际化,就像 Spring Web MVC 框架所做的那样。`DispatcherServlet`允许你通过使用客户机的区域设置自动解析消息。这是用`LocaleResolver`对象完成的。 + +当收到请求时,`DispatcherServlet`查找区域设置解析器,如果找到一个,则尝试使用它来设置区域设置。通过使用`RequestContext.getLocale()`方法,你始终可以检索由区域设置解析程序解析的区域设置。 + +除了自动解析语言环境外,还可以将拦截器附加到处理程序映射(有关处理程序映射拦截器的更多信息,请参见[Interception](#mvc-handlermapping-interceptor)),以在特定情况下(例如,基于请求中的参数)更改语言环境。 + +Locale 解析器和拦截器在 `org.springframework.web. Servlet.i18n` 包中定义,并以正常方式在应用程序上下文中进行配置。 Spring 中包含了以下对区域设置解析器的选择。 + +* [时区](#mvc-timezone) + +* [报头解析器](#mvc-localeresolver-acceptheader) + +* [Cookie 解析器](#mvc-localeresolver-cookie) + +* [会话解析器](#mvc-localeresolver-session) + +* [Locale 拦截器](#mvc-localeresolver-interceptor) + +##### Time Zone + +除了获取客户端的语言环境,了解它的时区通常是有用的。`LocaleContextResolver`接口提供了对`LocaleResolver`的扩展,使解析器提供更丰富的`LocaleContext`,其中可能包括时区信息。 + +当可用时,用户的`TimeZone`可以通过使用 `requestContext.getTimeZone()’方法获得。时区信息被注册在 Spring 的“ConversionService”中的任何日期/时间`Converter`和`Formatter`对象自动使用。 + +##### Header Resolver + +此 Locale 解析器检查客户机(例如,Web 浏览器)发送的请求中的`accept-language`头。通常,这个头字段包含客户端操作系统的区域设置。请注意,此解析器不支持时区信息。 + +##### Cookie Resolver + +此区域设置解析程序检查客户端上可能存在的`Cookie`,以查看是否指定了 `locale’或`TimeZone`。如果是,则使用指定的细节。通过使用此区域设置解析程序的属性,你可以指定 cookie 的名称以及最长期限。下面的示例定义了`CookieLocaleResolver`: + +``` +<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver"> + + <property name="cookieName" value="clientlanguage"/> + + <!-- in seconds. If set to -1, the cookie is not persisted (deleted when browser shuts down) --> + <property name="cookieMaxAge" value="100000"/> + +</bean> +``` + +下表描述了属性`CookieLocaleResolver`: + +| Property | Default |说明| +|--------------|-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `cookieName` | classname + LOCALE |饼干的名字| +|`cookieMaxAge`|Servlet container default|cookie 在客户机上持续存在的最长时间。如果指定了`-1`,则不会持久保存<br/>cookie。它仅在客户端关闭<br/>浏览器之前可用。| +| `cookiePath` | / |将 cookie 的可见性限制在网站的特定部分。当`cookiePath`被指定为<br/>时,cookie 仅对该路径及其下方的路径可见。| + +##### 会话解析器 + +`SessionLocaleResolver`允许你从可能与用户请求关联的会话中检索`Locale`和`TimeZone`。与“CookielocaleResolver”相反,该策略将本地选择的区域设置存储在 Servlet 容器的`HttpSession`中。因此,这些设置是每个会话的临时设置,因此在每个会话结束时都会丢失。 + +请注意,与外部会话管理机制没有直接关系,例如 Spring 会话项目。这个`SessionLocaleResolver`针对当前的`HttpServletRequest`计算并修改相应的`HttpSession`属性。 + +##### Locale 拦截器 + +你可以通过将`LocaleChangeInterceptor`添加到一个“handlermapping”定义中来启用更改区域设置。它在请求中检测一个参数,并相应地更改区域设置,在 Dispatcher 的应用程序上下文中调用`LocaleResolver`上的`setLocale`方法。下一个示例显示,对包含名为`siteLanguage`的参数的所有`*.view`资源的调用现在会更改区域设置。因此,例如,对 URL 的请求`[https://www.sf.net/home.view?siteLanguage=nl](https://www.sf.net/home.view?siteLanguage=nl)`将站点语言更改为荷兰语。下面的示例展示了如何截取区域设置: + +``` +<bean id="localeChangeInterceptor" + class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor"> + <property name="paramName" value="siteLanguage"/> +</bean> + +<bean id="localeResolver" + class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/> + +<bean id="urlMapping" + class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"> + <property name="interceptors"> + <list> + <ref bean="localeChangeInterceptor"/> + </list> + </property> + <property name="mappings"> + <value>/**/*.view=someController</value> + </property> +</bean> +``` + +#### 1.1.11.主题 + +Spring 可以应用 Web MVC 框架主题来设置应用程序的整体外观,从而增强用户体验。主题是静态资源的集合,通常是样式表和图像,它们会影响应用程序的视觉风格。 + +##### 定义主题 + +要在 Web 应用程序中使用主题,你必须设置 `org.springframework.ui.context.themesource’接口的实现。`WebApplicationContext`接口扩展了`ThemeSource`,但将其职责委托给一个专用的实现。默认情况下,委托是一个“org.springframework.ui.context.support.ResourceBundleThemesource”实现,它从 Classpath 的根目录加载属性文件。要使用自定义的`ThemeSource`实现或配置`ResourceBundleThemeSource`的基名前缀,你可以在应用程序上下文中使用保留的名称`themeSource`注册 Bean。Web 应用程序上下文自动检测具有该名称的 Bean 并使用它。 + +当你使用`ResourceBundleThemeSource`时,将在一个简单的属性文件中定义一个主题。Properties 文件列出了构成主题的资源,如下例所示: + +``` +styleSheet=/themes/cool/style.css +background=/themes/cool/img/coolBg.jpg +``` + +属性的键是从视图代码中引用主题元素的名称。对于 JSP,你通常使用`spring:theme`自定义标记来执行此操作,该标记与`spring:message`标记非常相似。下面的 JSP 片段使用上一个示例中定义的主题来定制外观和感觉: + +``` +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> +<html> + <head> + <link rel="stylesheet" href="<spring:theme code='styleSheet'/>" type="text/css"/> + </head> + <body style="background=<spring:theme code='background'/>"> + ... + </body> +</html> +``` + +默认情况下,`ResourceBundleThemeSource`使用一个空的基名前缀。因此,属性文件是从 Classpath 的根目录加载的。因此,你将把“cool.properties”主题定义放在 Classpath 根目录中(例如,在`/WEB-INF/classes`中)。`ResourceBundleThemeSource`使用标准的 爪哇 资源包加载机制,允许主题的完全国际化。例如,我们可以有一个`/WEB-INF/classes/cool_nl.properties`,它引用了一个特殊的背景图像,上面有荷兰语文本。 + +##### 解决主题 + +在定义主题(如[前一节](#mvc-themeresolver-defining)中所述)之后,你将决定使用哪个主题。`DispatcherServlet`查找名为`themeResolver`的 Bean,以找出要使用哪个`ThemeResolver`实现。主题解析器的工作方式与`LocaleResolver`几乎相同。它会检测用于特定请求的主题,还可以更改请求的主题。下表描述了 Spring 提供的主题解析程序: + +| Class |说明| +|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------| +| `FixedThemeResolver` |选择一个固定的主题,通过使用`defaultThemeName`属性进行设置。| +|`SessionThemeResolver`|该主题在用户的 HTTP 会话中进行维护。对于<br/>每个会话只需要设置一次,但在会话之间不会持久化。| +|`CookieThemeResolver` |选定的主题存储在客户端的 cookie 中。| + +Spring 还提供了一个`ThemeChangeInterceptor`,它允许使用一个简单的请求参数对每个请求进行主题更改。 + +#### 1.1.12.多部分旋转变压器 + +[WebFlux](web-reactive.html#webflux-multipart) + +来自`MultipartResolver`包的`org.springframework.web.multipart`是一种解析包括文件上传在内的多部分请求的策略。存在一种基于[Commons 文件上传](https://commons.apache.org/proper/commons-fileupload)的实现和另一种基于 Servlet 3.0 多部分请求解析的实现。 + +要启用多部分处理,你需要在“DispatcherServlet” Spring 配置中声明一个名为`multipartResolver`的`MultipartResolver` Bean。`DispatcherServlet`检测到它并将其应用到传入请求。当接收到内容类型为`multipart/form-data`的帖子时,解析器将当前`HttpServletRequest`的内容包装解析为`MultipartHttpServletRequest`,以提供对已解析文件的访问,此外还将部分作为请求参数公开。 + +##### Apache Commons`FileUpload` + +要使用 ApacheCommons<gtr="811"/>,你可以配置 Bean 类型的 `CommonsMultipartResolver’,其名称为<gtr="812"/>。你还需要将`commons-fileupload` jar 作为 Classpath 的依赖项。 + +这个解析器变体将委托给应用程序中的一个本地库,从而在 Servlet 容器中提供了最大的可移植性。作为一种选择,考虑通过容器自己的解析器实现标准 Servlet 多部分分辨率,如下所述。 + +| |Commons FileUpload 传统上只适用于 POST 请求,但接受任何“multipart/”内容类型。有关详细信息和配置选项,请参见[“CommonsMultipartResolver”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/multipart/commons/CommonsMultipartResolver.html)爪哇doc。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### Servlet 3.0 + +Servlet 3.0 需要通过 Servlet 容器配置来启用多部分解析。这样做: + +* 在 爪哇 中,在 Servlet 注册上设置`MultipartConfigElement`。 + +* 在`web.xml`中,在 Servlet 声明中添加`"<multipart-config>"`部分。 + +下面的示例显示了如何在 Servlet 注册上设置`MultipartConfigElement`: + +爪哇 + +``` +public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { + + // ... + + @Override + protected void customizeRegistration(ServletRegistration.Dynamic registration) { + + // Optionally also set maxFileSize, maxRequestSize, fileSizeThreshold + registration.setMultipartConfig(new MultipartConfigElement("/tmp")); + } + +} +``` + +Kotlin + +``` +class AppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() { + + // ... + + override fun customizeRegistration(registration: ServletRegistration.Dynamic) { + + // Optionally also set maxFileSize, maxRequestSize, fileSizeThreshold + registration.setMultipartConfig(MultipartConfigElement("/tmp")) + } + +} +``` + +一旦 Servlet 3.0 配置到位,你就可以添加 Bean 类型为 `StandardServletMultipartResolver’的名称为`multipartResolver`。 + +| |这个解析器变体按原样使用 Servlet 容器的多部分解析器,<br/>可能会使应用程序暴露于容器实现的差异中。<br/>默认情况下,它将尝试使用任何 HTTP`multipart/`方法解析任何<br/>内容类型,但这可能不会在所有 Servlet 容器中得到支持。有关详细信息和配置选项,请参见[“标准 ServletMultipartResolver”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/multipart/support/StandardServletMultipartResolver.html)爪哇doc。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.1.13.伐木 + +[WebFlux](web-reactive.html#webflux-logging) + +Spring MVC 中的调试级日志被设计为紧凑、最小且对人友好的。它关注的是一次又一次有用的高价值信息,而不是仅在调试特定问题时有用的其他信息。 + +跟踪级日志记录通常遵循与调试相同的原则(例如,也不应该是消防水龙带),但可以用于调试任何问题。此外,一些日志消息在跟踪和调试时可能会显示不同级别的详细信息。 + +良好的日志记录来自于使用日志的经验。如果你发现任何不符合规定的目标,请告诉我们。 + +##### 敏感数据 + +[WebFlux](web-reactive.html#webflux-logging-sensitive-data) + +调试和跟踪日志记录可能会记录敏感信息。这就是为什么请求参数和头在默认情况下被屏蔽,并且必须通过`DispatcherServlet`上的`enableLoggingRequestDetails`属性显式地启用它们的完整日志记录。 + +下面的示例展示了如何通过使用 爪哇 配置来实现这一点: + +爪哇 + +``` +public class MyInitializer + extends AbstractAnnotationConfigDispatcherServletInitializer { + + @Override + protected Class<?>[] getRootConfigClasses() { + return ... ; + } + + @Override + protected Class<?>[] getServletConfigClasses() { + return ... ; + } + + @Override + protected String[] getServletMappings() { + return ... ; + } + + @Override + protected void customizeRegistration(ServletRegistration.Dynamic registration) { + registration.setInitParameter("enableLoggingRequestDetails", "true"); + } + +} +``` + +Kotlin + +``` +class MyInitializer : AbstractAnnotationConfigDispatcherServletInitializer() { + + override fun getRootConfigClasses(): Array<Class<*>>? { + return ... + } + + override fun getServletConfigClasses(): Array<Class<*>>? { + return ... + } + + override fun getServletMappings(): Array<String> { + return ... + } + + override fun customizeRegistration(registration: ServletRegistration.Dynamic) { + registration.setInitParameter("enableLoggingRequestDetails", "true") + } +} +``` + +### 1.2.过滤器 + +[WebFlux](web-reactive.html#webflux-filters) + +`spring-web`模块提供了一些有用的过滤器: + +* [Form Data](#filters-http-put) + +* [转发头](#filters-forwarded-headers) + +* [Shallow ETag](#filters-shallow-etag) + +* [CORS](#filters-cors) + +#### 1.2.1.表单数据 + +浏览器只能通过 HTTP GET 或 HTTP POST 提交表单数据,但非浏览器客户端也可以使用 HTTP PUT、补丁和 DELETE。 Servlet API 要求`ServletRequest.getParameter*()`方法只支持用于 HTTP POST 的表单字段访问。 + +`spring-web`模块提供`FormContentFilter`来拦截内容类型为`application/x-www-form-urlencoded`的 HTTP PUT、修补和删除请求,从请求的主体中读取表单数据,并包装`ServletRequest`以使表单数据通过`ServletRequest.getParameter*()`系列方法可用。 + +#### 1.2.2.转发头 + +[WebFlux](web-reactive.html#webflux-forwarded-headers) + +当请求通过代理(例如负载均衡器)时,主机、端口和方案可能会发生变化,这使得创建从客户端角度指向正确的主机、端口和方案的链接成为一个挑战。 + +[RFC 7239](https://tools.ietf.org/html/rfc7239)定义了`Forwarded`HTTP 报头,代理可以使用该报头来提供有关原始请求的信息。还有其他非标准标题,包括`X-Forwarded-Host`,`X-Forwarded-Port`,`x-forwarded-proto`,`X-Forwarded-Ssl`和`X-Forwarded-Prefix`。 + +`ForwardedHeaderFilter`是一个 Servlet 过滤器,它修改请求,以便 a)基于`Forwarded`标题更改主机、端口和方案,以及 b)删除那些标题以消除进一步的影响。过滤器依赖于包装请求,因此它必须在其他过滤器(例如`RequestContextFilter`)之前进行排序,这些过滤器应该与修改后的请求(而不是原始请求)一起工作。 + +转发头的安全性需要考虑,因为应用程序不能知道头是由代理添加的,还是由恶意客户机添加的。这就是为什么在信任边界上的代理应该被配置为删除来自外部的不受信任的`Forwarded`头。你还可以将`ForwardedHeaderFilter`配置为`removeOnly=true`,在这种情况下,它会删除但不使用头。 + +为了支持[异步请求](#mvc-ann-async)和错误分派,此筛选器应该映射为`DispatcherType.ASYNC`和`DispatcherType.ERROR`。如果使用 Spring Framework 的`AbstractAnnotationConfigDispatcherServletInitializer`(参见[Servlet Config](#mvc-container-config)),则所有分派类型的所有过滤器都会自动注册。但是,如果通过`web.xml`或在 Spring 引导中通过 `filterregistrationbean’注册过滤器,请确保除`DispatcherType.REQUEST`外还包括`DispatcherType.ASYNC`和 `dispatchertype.error’。 + +#### 1.2.3.浅层 ETAG + +`ShallowEtagHeaderFilter`过滤器通过缓存写到响应的内容并从中计算 MD5 散列来创建一个“浅”ETag。下一次客户端发送时,它也会执行相同的操作,但是它也会将计算的值与`If-None-Match`请求头进行比较,如果两者相等,则返回 304(不是 \_modified)。 + +这种策略节省了网络带宽,但不节省 CPU,因为必须为每个请求计算完整的响应。前面描述的控制器级别的其他策略可以避免计算。见[HTTP Caching](#mvc-caching)。 + +这个过滤器有一个`writeWeakETag`参数,该参数将过滤器配置为写入类似于以下内容的弱 ETags:`W/"02a2d595e6ed9a0b24f027f2b63b134d6"`(在[RFC7232 第 2.3 节](https://tools.ietf.org/html/rfc7232#section-2.3)中定义)。 + +为了支持[异步请求](#mvc-ann-async),这个过滤器必须与`DispatcherType.ASYNC`映射,这样过滤器就可以延迟并成功地生成 ETag,直到最后一个异步调度结束。如果使用 Spring Framework 的 `AbstractNotationConfigDispatcherServletInitializer’’(参见[Servlet Config](#mvc-container-config)),所有过滤器都会自动为所有分派类型注册。但是,如果通过`web.xml`或在 Spring boot 中通过`FilterRegistrationBean`注册过滤器,请确保包含 `dispatchertype.async’。 + +#### 1.2.4.科尔斯 + +[WebFlux](web-reactive.html#webflux-filters-cors) + +Spring MVC 通过控制器上的注释为 CORS 配置提供了细粒度的支持。然而,当与 Spring Security 一起使用时,我们建议依赖于内置的“CorsFilter”,这必须在 Spring Security 的一系列过滤器之前订购。 + +有关更多详细信息,请参见[CORS](#mvc-cors)和[CORS Filter](#mvc-cors-filter)部分。 + +### 1.3.带注释的控制器 + +[WebFlux](web-reactive.html#webflux-controller) + +Spring MVC 提供了一种基于注释的编程模型,其中`@Controller`和 `@RESTController’组件使用注释来表示请求映射、请求输入、异常处理等。带注释的控制器具有灵活的方法签名,不需要扩展基类,也不需要实现特定的接口。下面的示例显示了由注释定义的控制器: + +爪哇 + +``` +@Controller +public class HelloController { + + @GetMapping("/hello") + public String handle(Model model) { + model.addAttribute("message", "Hello World!"); + return "index"; + } +} +``` + +Kotlin + +``` +import org.springframework.ui.set + +@Controller +class HelloController { + + @GetMapping("/hello") + fun handle(model: Model): String { + model["message"] = "Hello World!" + return "index" + } +} +``` + +在前面的示例中,该方法接受`Model`并将视图名返回为`String`,但是存在许多其他选项,将在本章后面进行解释。 + +| |[spring.io](https://spring.io/guides)上的指南和教程使用本节中描述的基于注释的<br/>编程模型。| +|---|---------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.3.1.声明 + +[WebFlux](web-reactive.html#webflux-ann-controller) + +你可以使用 Servlet 的`WebApplicationContext`中的标准 Spring Bean 定义来定义控制器 bean。该原型允许自动检测,与 Spring 用于检测 Classpath 中的类的通用支持保持一致并为它们自动注册 Bean 定义。它还充当带注释的类的原型,指示其作为 Web 组件的角色。 + +要启用对此类`@Controller`bean 的自动检测,可以将组件扫描添加到 爪哇 配置中,如下例所示: + +爪哇 + +``` +@Configuration +@ComponentScan("org.example.web") +public class WebConfig { + + // ... +} +``` + +Kotlin + +``` +@Configuration +@ComponentScan("org.example.web") +class WebConfig { + + // ... +} +``` + +下面的示例展示了与前面示例类似的 XML 配置: + +``` +<?xml version="1.0" encoding="UTF-8"?> +<beans xmlns="http://www.springframework.org/schema/beans" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:p="http://www.springframework.org/schema/p" + xmlns:context="http://www.springframework.org/schema/context" + xsi:schemaLocation=" + http://www.springframework.org/schema/beans + https://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/context + https://www.springframework.org/schema/context/spring-context.xsd"> + + <context:component-scan base-package="org.example.web"/> + + <!-- ... --> + +</beans> +``` + +`@RestController`是一个[组合注释](core.html#beans-meta-annotations),它本身用`@Controller`和`@ResponseBody`进行了元注释,以指示控制器,其每个方法都继承了类型级`@ResponseBody`注释,因此,直接写到响应主体与视图解析之间,并使用 HTML 模板进行呈现。 + +##### AOP 代理 + +在某些情况下,你可能需要在运行时使用 AOP 代理来装饰控制器。一个例子是,如果你选择在控制器上直接使用`@Transactional`注释。在这种情况下,特别是对于控制器,我们建议使用基于类的代理。这通常是控制器的默认选择。但是,如果控制器必须实现不是 Spring 上下文回调的接口(例如`InitializingBean`、`*Aware`等),则可能需要显式地配置基于类的代理。例如,对于`<tx:annotation-driven/>`,你可以更改为`<tx:annotation-driven proxy-target-class="true"/>`,对于 `@enableTransactionManagement’,你可以更改为 `@enableTransactionManagement’。 + +#### 1.3.2.请求映射 + +[WebFlux](web-reactive.html#webflux-ann-requestmapping) + +你可以使用`@RequestMapping`注释将请求映射到控制器方法。它具有各种属性,可以通过 URL、HTTP 方法、请求参数、标头和媒体类型进行匹配。你可以在类级别上使用它来表示共享映射,或者在方法级别上使用它来缩小到特定的端点映射。 + +还有`@RequestMapping`的特定于 HTTP 方法的快捷方式变体: + +* `@GetMapping` + +* `@PostMapping` + +* `@PutMapping` + +* `@DeleteMapping` + +* `@PatchMapping` + +提供的快捷方式是[自定义注释](#mvc-ann-requestmapping-composed),因为可以说,大多数控制器方法都应该映射到特定的 HTTP 方法,而不是使用`@RequestMapping`,后者默认情况下与所有 HTTP 方法匹配。在类级别上仍然需要一个`@RequestMapping`来表示共享映射。 + +下面的示例具有类型和方法级别的映射: + +爪哇 + +``` +@RestController +@RequestMapping("/persons") +class PersonController { + + @GetMapping("/{id}") + public Person getPerson(@PathVariable Long id) { + // ... + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public void add(@RequestBody Person person) { + // ... + } +} +``` + +Kotlin + +``` +@RestController +@RequestMapping("/persons") +class PersonController { + + @GetMapping("/{id}") + fun getPerson(@PathVariable id: Long): Person { + // ... + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + fun add(@RequestBody person: Person) { + // ... + } +} +``` + +##### URI 模式 + +[WebFlux](web-reactive.html#webflux-ann-requestmapping-uri-templates) + +`@RequestMapping`方法可以使用 URL 模式进行映射。有两种选择: + +* `PathPattern`—与 URL 路径匹配的预解析模式,也预解析为 `pathcontainer’。该解决方案是为网络应用而设计的,能够有效地处理编码和路径参数,并能有效地进行匹配。 + +* `AntPathMatcher`—将字符串模式与字符串路径匹配。这也是在 Spring 配置中使用的原始解决方案,以在 Classpath、文件系统上和其他位置上选择资源。它的效率较低,并且字符串路径输入对于有效地处理编码和 URL 的其他问题是一个挑战。 + +`PathPattern`是 Web 应用程序的推荐解决方案,也是 Spring WebFlux 中的唯一选择。在版本 5.3 之前,`AntPathMatcher`是 Spring MVC 中的唯一选择,并且仍然是默认的。但是`PathPattern`可以在[MVC config](#mvc-config-path-matching)中启用。 + +`PathPattern`支持与`AntPathMatcher`相同的模式语法。此外,它还支持捕获模式,例如`{*spring}`,用于在路径的末端匹配 0 个或更多个路径段。`PathPattern`还限制了`**`用于匹配多个路径段的使用,因此只允许在模式的末尾使用。在为给定的请求选择最佳匹配模式时,这消除了许多模棱两可的情况。有关完整的模式语法,请参阅[PathPattern](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/util/pattern/PathPattern.html)和[AntPathMatcher](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/util/AntPathMatcher.html)。 + +一些示例模式: + +* `"/resources/ima?e.png"`-匹配路径段中的一个字符 + +* `"/resources/*.png"`-匹配路径段中的零个或多个字符 + +* `"/resources/**"`-匹配多个路径段 + +* `"/projects/{project}/versions"`-匹配路径段并将其捕获为变量 + +* `"/projects/{project:[a-z]+}/versions"`-匹配并捕获带有正则表达式的变量 + +捕获的 URI 变量可以通过`@PathVariable`访问。例如: + +Java + +``` +@GetMapping("/owners/{ownerId}/pets/{petId}") +public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) { + // ... +} +``` + +Kotlin + +``` +@GetMapping("/owners/{ownerId}/pets/{petId}") +fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet { + // ... +} +``` + +可以在类和方法级别声明 URI 变量,如下例所示: + +Java + +``` +@Controller +@RequestMapping("/owners/{ownerId}") +public class OwnerController { + + @GetMapping("/pets/{petId}") + public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) { + // ... + } +} +``` + +Kotlin + +``` +@Controller +@RequestMapping("/owners/{ownerId}") +class OwnerController { + + @GetMapping("/pets/{petId}") + fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet { + // ... + } +} +``` + +URI 变量将自动转换为适当的类型,或者生成`TypeMismatchException`。默认情况下支持简单类型(`INT’,`long`,`Date`,等等),你可以注册对任何其他数据类型的支持。见[类型转换](#mvc-ann-typeconversion)和[`DataBinder`](#mvc-ann-initbinder)。 + +你可以显式地命名 URI 变量(例如,`@PathVariable("customId")`),但是如果名称相同,并且你的代码是使用调试信息或 Java8 上的`-parameters`编译器标志编译的,则可以忽略该细节。 + +语法`{varName:regex}`声明一个 URI 变量,其正则表达式的语法为`{varName:regex}`。例如,给定的 URL`"/spring-web-3.0.5.jar"`,下面的方法会提取名称、版本和文件扩展名: + +Java + +``` +@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}") +public void handle(@PathVariable String name, @PathVariable String version, @PathVariable String ext) { + // ... +} +``` + +Kotlin + +``` +@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}") +fun handle(@PathVariable name: String, @PathVariable version: String, @PathVariable ext: String) { + // ... +} +``` + +URI 路径模式还可以嵌入`${…​}`占位符,这些占位符在启动时通过针对本地、系统、环境和其他属性源使用`PropertyPlaceHolderConfigurer`进行解析。例如,你可以使用它来基于某些外部配置参数化基本 URL。 + +##### 模式比较 + +[WebFlux](web-reactive.html#webflux-ann-requestmapping-pattern-comparison) + +当多个模式匹配一个 URL 时,必须选择最佳匹配。根据解析的`PathPattern`的使用是否被启用,可以使用以下方法之一来完成此操作: + +* [`PathPattern.Speciality_Comparator’](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/util/pattern/PathPattern.html#SPECIFICITY_COMPARATOR) + +* [getPatternComparator(字符串路径)](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/util/AntPathMatcher.html#getPatternComparator-java.lang.String-) + +这两种方法都有助于在顶部使用更具体的模式对模式进行排序。如果一个模式的 URI 变量(计为 1)、单通配符(计为 1)和双通配符(计为 2)的数量较少,那么它就不那么具体。给定一个相等的分数,选择较长的模式。在相同的分数和长度下,将选择 URI 变量多于通配符的模式。 + +默认的映射模式被排除在评分之外,并且总是排在最后。另外,前缀模式(如`/public/**`)被认为不如其他没有双通配符的模式具体。 + +有关详细信息,请按照上面的链接查看模式比较器。 + +##### 后缀匹配 + +从 5.3 开始,默认情况下 Spring MVC 不再执行`.*`后缀模式匹配,其中映射到`/person`的控制器也隐式映射到 `/person.*`。因此,路径扩展不再用于解释响应所请求的内容类型——例如,`/person.pdf`,`/person.xml`,以此类推。 + +当浏览器过去发送`Accept`标题时,以这种方式使用文件扩展名是必要的,而这些标题很难一致地进行解释。目前,这已不再是必要的,使用`Accept`头应该是首选的选择。 + +随着时间的推移,文件扩展名的使用在很多方面都被证明是有问题的。当使用 URI 变量、路径参数和 URI 编码时,它可能会造成歧义。关于基于 URL 的授权和安全性的推理(更多详细信息请参见下一节)也变得更加困难。 + +要在 5.3 之前的版本中完全禁用路径扩展的使用,请设置以下内容: + +* `useSuffixPatternMatching(false)`,见[路径匹配配置器](#mvc-config-path-matching) + +* `favorPathExtension(false)`,见[内容协商配置器](#mvc-config-content-negotiation) + +通过`"Accept"`头以外的方式请求内容类型仍然是有用的,例如在浏览器中输入 URL 时。路径扩展的一种安全替代方法是使用查询参数策略。如果必须使用文件扩展名,请考虑将其限制为通过`mediaTypes`的`myValue`属性显式注册的扩展名列表。 + +##### 后缀匹配和 RFD + +反射文件下载攻击类似于 XSS,因为它依赖于响应中反映的请求输入(例如,查询参数和 URI 变量)。然而,RFD 攻击不是将 JavaScript 插入 HTML,而是依靠浏览器切换来执行下载,并在稍后双击时将响应视为可执行脚本。 + +Spring MVC 中,`@ResponseBody`和`ResponseEntity`方法存在风险,因为它们可以呈现不同的内容类型,客户端可以通过 URL 路径扩展来请求这些内容类型。禁用后缀模式匹配和使用路径扩展进行内容协商降低了风险,但不足以防止 RFD 攻击。 + +为了防止 RFD 攻击,在呈现响应主体之前, Spring MVC 添加了一个“Content-Disposition:Inline;Filename=F.TXT”头,以建议一个固定且安全的下载文件。只有当 URL 路径包含一个既不被允许为安全的文件扩展名,也没有显式地为内容协商注册的文件扩展名时,才会执行此操作。然而,当 URL 被直接输入到浏览器中时,它可能会产生副作用。 + +默认情况下,许多常见的路径扩展都是安全的。具有自定义“HttpMessageConverter”实现的应用程序可以显式地注册用于内容协商的文件扩展名,以避免为这些扩展名添加`Content-Disposition`头。见[Content Types](#mvc-config-content-negotiation)。 + +有关 RFD 的其他建议,见[CVE-2015-5211](https://pivotal.io/security/cve-2015-5211)。 + +##### 可消费媒体类型 + +[WebFlux](web-reactive.html#webflux-ann-requestmapping-consumes) + +你可以基于请求的`Content-Type`缩小请求映射,如下例所示: + +Java + +``` +@PostMapping(path = "/pets", consumes = "application/json") (1) +public void addPet(@RequestBody Pet pet) { + // ... +} +``` + +|**1**|使用`consumes`属性来缩小内容类型之间的映射。| +|-----|-----------------------------------------------------------------------| + +Kotlin + +``` +@PostMapping("/pets", consumes = ["application/json"]) (1) +fun addPet(@RequestBody pet: Pet) { + // ... +} +``` + +|**1**|使用`consumes`属性来缩小内容类型之间的映射。| +|-----|-----------------------------------------------------------------------| + +`consumes`属性还支持否定表达式——例如,`!text/plain`表示除`text/plain`以外的任何内容类型。 + +你可以在类级别声明一个共享的`consumes`属性。然而,与大多数其他请求映射属性不同的是,当在类级别使用时,方法级别`consumes`属性覆盖而不是扩展类级声明。 + +| |`MediaType`为常用的媒体类型提供常量,例如 `application_json_value` 和`APPLICATION_XML_VALUE`。| +|---|--------------------------------------------------------------------------------------------------------------------------| + +##### 可生产媒体类型 + +[WebFlux](web-reactive.html#webflux-ann-requestmapping-produces) + +你可以基于`Accept`请求头和控制器方法产生的内容类型列表来缩小请求映射的范围,如下例所示: + +Java + +``` +@GetMapping(path = "/pets/{petId}", produces = "application/json") (1) +@ResponseBody +public Pet getPet(@PathVariable String petId) { + // ... +} +``` + +|**1**|使用`produces`属性来缩小内容类型之间的映射。| +|-----|-----------------------------------------------------------------------| + +Kotlin + +``` +@GetMapping("/pets/{petId}", produces = ["application/json"]) (1) +@ResponseBody +fun getPet(@PathVariable petId: String): Pet { + // ... +} +``` + +|**1**|使用`produces`属性来缩小内容类型之间的映射。| +|-----|-----------------------------------------------------------------------| + +媒体类型可以指定字符集。支持否定表达式——例如,“!text/plain”表示除“text/plain”之外的任何内容类型。 + +你可以在类级别声明一个共享的`produces`属性。然而,与大多数其他请求映射属性不同的是,当在类级别使用时,方法级别`produces`属性覆盖而不是扩展类级声明。 + +| |`MediaType`为常用的媒体类型提供常量,例如 `application_json_value` 和`APPLICATION_XML_VALUE`。| +|---|--------------------------------------------------------------------------------------------------------------------------| + +##### 参数、标头 + +[WebFlux](web-reactive.html#webflux-ann-requestmapping-params-and-headers) + +你可以根据请求参数条件缩小请求映射的范围。你可以测试是否存在请求参数(“myparam”)、是否缺少请求参数(“!myparam”)或特定的值(“myparam=myvalue”)。下面的示例展示了如何测试一个特定值: + +Java + +``` +@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") (1) +public void findPet(@PathVariable String petId) { + // ... +} +``` + +|**1**|测试`myParam`是否等于`myValue`。| +|-----|-------------------------------------------| + +Kotlin + +``` +@GetMapping("/pets/{petId}", params = ["myParam=myValue"]) (1) +fun findPet(@PathVariable petId: String) { + // ... +} +``` + +|**1**|测试`myParam`是否等于`myValue`。| +|-----|-------------------------------------------| + +你也可以在请求头条件中使用相同的方法,如下例所示: + +Java + +``` +@GetMapping(path = "/pets", headers = "myHeader=myValue") (1) +public void findPet(@PathVariable String petId) { + // ... +} +``` + +|**1**|测试`myHeader`是否等于`myValue`。| +|-----|--------------------------------------------| + +Kotlin + +``` +@GetMapping("/pets", headers = ["myHeader=myValue"]) (1) +fun findPet(@PathVariable petId: String) { + // ... +} +``` + +| |你可以将`Content-Type`和`Accept`与 headers 条件匹配,但最好使用[consumes](#mvc-ann-requestmapping-consumes)和[produces](#mvc-ann-requestmapping-produces)。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### HTTP 头,选项 + +[WebFlux](web-reactive.html#webflux-ann-requestmapping-head-options) + +`@GetMapping`(和`@RequestMapping(method=HttpMethod.GET)`)透明地支持用于请求映射的 HTTP head。控制器的方法不需要更改。在`javax.servlet.http.HttpServlet`中应用的响应包装程序确保将`Content-Length`头设置为写入的字节数(不实际写入响应)。 + +`@GetMapping`(和`@RequestMapping(method=HttpMethod.GET)`)隐式映射到并支持 HTTPHEAD。HTTP head 请求的处理方式就像 HTTP GET 一样,除了不写正文,而是计算字节数并设置`Content-Length`头。 + +默认情况下,HTTP 选项的处理方法是将`Allow`响应头设置为具有匹配的 URL 模式的所有`@RequestMapping`方法中列出的 HTTP 方法列表。 + +对于不带 HTTP 方法声明的`@RequestMapping`,`Allow`头将设置为 `get,head,post,put,patch,delete,options’。控制器方法应该总是声明受支持的 HTTP 方法(例如,通过使用 HTTP 方法特定的变体:@getMapping`,`@SessionAttributes`,以及其他)。 + +你可以显式地将`@RequestMapping`方法映射到 HTTPHead 和 HTTPOptions,但在常见的情况下,这是不必要的。 + +##### 自定义注释 + +[WebFlux](web-reactive.html#mvc-ann-requestmapping-head-options) + +Spring MVC 支持使用[组合注释](core.html#beans-meta-annotations)进行请求映射。这些注释本身是用“@requestmapping”进行元注释的,其组成是为了重新声明`@RequestMapping`属性的一个子集(或全部),具有更窄、更具体的目的。 + +`@GetMapping`,`@PostMapping`,`@PutMapping`,`@DeleteMapping`,和`@PatchMapping`是复合注释的示例。提供它们是因为,可以说,大多数控制器方法都应该映射到特定的 HTTP 方法,而不是使用`@RequestMapping`,后者在默认情况下与所有 HTTP 方法匹配。如果你需要一个组合注释的示例,请查看这些注释是如何声明的。 + +Spring MVC 还支持具有自定义请求匹配逻辑的自定义请求映射属性。这是一个更高级的选项,它需要子类化 `requestmappinghandlermapping’并覆盖`getCustomMethodCondition`方法,在该方法中,你可以检查自定义属性并返回自己的`RequestCondition`。 + +##### 显式注册 + +[WebFlux](web-reactive.html#webflux-ann-requestmapping-registration) + +你可以以编程方式注册处理程序方法,你可以将其用于动态注册或高级情况,例如同一处理程序在不同 URL 下的不同实例。下面的示例注册了一个处理程序方法: + +Java + +``` +@Configuration +public class MyConfig { + + @Autowired + public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler) (1) + throws NoSuchMethodException { + + RequestMappingInfo info = RequestMappingInfo + .paths("/user/{id}").methods(RequestMethod.GET).build(); (2) + + Method method = UserHandler.class.getMethod("getUser", Long.class); (3) + + mapping.registerMapping(info, handler, method); (4) + } +} +``` + +|**1**|为控制器注入目标处理程序和处理程序映射。| +|-----|------------------------------------------------------------------| +|**2**|准备请求映射元数据。| +|**3**|获取 handler 方法。| +|**4**|添加注册。| + +Kotlin + +``` +@Configuration +class MyConfig { + + @Autowired + fun setHandlerMapping(mapping: RequestMappingHandlerMapping, handler: UserHandler) { (1) + val info = RequestMappingInfo.paths("/user/{id}").methods(RequestMethod.GET).build() (2) + val method = UserHandler::class.java.getMethod("getUser", Long::class.java) (3) + mapping.registerMapping(info, handler, method) (4) + } +} +``` + +|**1**|为控制器注入目标处理程序和处理程序映射。| +|-----|------------------------------------------------------------------| +|**2**|准备请求映射元数据。| +|**3**|获取 handler 方法。| +|**4**|添加注册。| + +#### 1.3.3.处理程序方法 + +[WebFlux](web-reactive.html#webflux-ann-methods) + +`@RequestMapping`处理程序方法具有灵活的签名,并且可以从受支持的控制器方法参数和返回值的范围中进行选择。 + +##### 方法参数 + +[WebFlux](web-reactive.html#webflux-ann-arguments) + +下一个表描述了受支持的控制器方法参数。任何参数都不支持反应式类型。 + +JDK8 的`java.util.Optional`作为方法参数与具有`required`属性(例如,`@RequestParam`,`@RequestHeader`,以及其他)并且与`required=false`等价的注释结合在一起,是受支持的。 + +| Controller method argument |说明| +|----------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `WebRequest`, `NativeWebRequest` |对请求参数以及请求和会话属性的通用访问,而不需要直接使用<br/>的 Servlet API。| +| `javax.servlet.ServletRequest`, `javax.servlet.ServletResponse` |选择任何特定的请求或响应类型——例如,`ServletRequest`,`HttpServletRequest`,<br/>或 Spring 的`MultipartRequest`,`MultipartHttpServletRequest`。| +| `javax.servlet.http.HttpSession` |强制执行会话的存在。因此,这样的参数永远不是`Views`。<br/>注意,会话访问不是线程安全的。如果允许多个<br/>请求并发访问会话,请考虑将 `requestmappinghandleradapter’实例的`synchronizeOnSession`标志设置为`true`。| +| `javax.servlet.http.PushBuilder` |Servlet 4.0Push Builder API 用于程序化的 HTTP/2 资源推送。注意,根据 Servlet 规范,如果客户机不支持该 HTTP/2 功能,则注入的实例可以为空。| +| `java.security.Principal` |当前经过身份验证的用户——如果已知的话,可能是一个特定的`Principal`实现类。<br/><br/>注意,如果为了允许自定义解析器在通过`HttpServletRequest#getUserPrincipal`恢复默认解析之前解析<br/>,则该参数不会立即解析,例如, Spring 安全性`Authentication`实现了`Principal`,并且将通过 `HttpServletRequest#getUserprincipal’作为这样的注入,除非它也用`@AuthenticationPrincipal`注释,在这种情况下,它<br/>由自定义 Spring 安全性解析器通过`Authentication#getPrincipal`解析。| +| `HttpMethod` |请求的 HTTP 方法。| +| `java.util.Locale` |当前的请求区域设置,由最具体的`LocaleResolver`确定(在<br/>效果中,配置的`LocaleResolver`或`LocaleContextResolver`)。| +| `java.util.TimeZone` + `java.time.ZoneId` |与当前请求相关联的时区,由`LocaleContextResolver`确定。| +| `java.io.InputStream`, `java.io.Reader` |用于访问由 Servlet API 公开的原始请求主体。| +| `java.io.OutputStream`, `java.io.Writer` |用于访问由 Servlet API 公开的原始响应体。| +| `@PathVariable` |用于访问 URI 模板变量。见[URI patterns](#mvc-ann-requestmapping-uri-templates)。| +| `@MatrixVariable` |用于访问 URI 路径段中的名称-值对。见[矩阵变量](#mvc-ann-matrix-variables)。| +| `@RequestParam` |用于访问 Servlet 请求参数,包括多部分文件。参数值<br/>被转换为声明的方法参数类型。参见[`@RequestParam`](#mvc-ann-requestparam)以及[`@RequestParam`](#mvc-ann-requestparam)as[Multipart](#mvc-multipart-forms)。<br/>注意,对于简单参数值,`@RequestParam`是可选的。<br/>参见本表末尾的“任何其他参数”。| +| `@RequestHeader` |用于访问请求头。标头值被转换为声明的方法参数<br/>type。见<br/>。| +| `@CookieValue` |获取 cookie 的权限。Cookie 值被转换为声明的方法参数<br/>type。见[`@CookieValue`](#mvc-ann-cookievalue)。| +| `@RequestBody` |用于访问 HTTP 请求主体。通过使用`HttpMessageConverter`实现,主体内容被转换为声明的方法<br/>参数类型。见[`@RequestBody`](#mvc-ann-requestbody)。| +| `HttpEntity<B>` |用于访问请求头和主体。体被转换为`HttpMessageConverter`。<br/>见[HttpEntity](#mvc-ann-httpentity)。| +| `@RequestPart` |为了访问`multipart/form-data`请求中的一个部件,将该部件的主体<br/>转换为`HttpMessageConverter`。见[Multipart](#mvc-multipart-forms)。| +|`java.util.Map`, `org.springframework.ui.Model`, `org.springframework.ui.ModelMap`|用于访问 HTML 控制器中使用的模型,并将其作为<br/>视图呈现的一部分暴露于模板中。| +| `RedirectAttributes` |指定在重定向情况下使用的属性(即要追加到查询<br/>字符串中)和要临时存储的 flash 属性,直到重定向后的请求为止。<br/>参见[重定向属性](#mvc-redirecting-passing-data)和<br/>。| +| `@ModelAttribute` |用于访问模型中的现有属性(如果不存在则实例化),并应用<br/>数据绑定和验证。参见`@ModelAttribute`以及[Model](#mvc-ann-modelattrib-methods)和[`DataBinder`](#mvc-ann-initbinder)。<br/><br/>注意,`@ModelAttribute`的使用是可选的(例如,用于设置其属性)。<br/>参见本表末尾的“任何其他参数”。| +| `Errors`, `BindingResult` |用于访问来自命令对象<br/>的验证和数据绑定的错误(即`@ModelAttribute`参数)或来自`@RequestBody`或 `@requestPart` 参数的验证的错误。你必须在验证方法参数之后立即声明`Errors`或`BindingResult`参数<br/>。| +| `SessionStatus` + class-level `@SessionAttributes` |用于标记表单处理完成,这将触发清除通过类级`@SessionAttributes`注释声明的会话属性<br/>。有关更多详细信息,请参见[@sessionAttributions’](#mvc-ann-sessionattributes)。| +| `UriComponentsBuilder` |用于准备相对于当前请求的主机、端口、方案、上下文路径的 URL,以及 Servlet 映射的文字部分<br/>。见[URI Links](#mvc-uri-building)。| +| `@SessionAttribute` |对于任何会话属性的访问,与存储在会话<br/>中的模型属性形成对比的是类级别`@SessionAttributes`声明的结果。有关更多详细信息,请参见[@sessionAttribute](#mvc-ann-sessionattribute)。| +| `@RequestAttribute` |用于访问请求属性。有关更多详细信息,请参见[@requestAttribute](#mvc-ann-requestattrib)。| +| Any other argument |如果方法参数不匹配到此表中的任何早期值,并且它是<br/>一个简单的类型(由[Beanutils#IsSimpleProperty](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-)确定,<br/>将解析为`@RequestParam`。否则,它将解析为`@ModelAttribute`。| + +##### 返回值 + +[WebFlux](web-reactive.html#webflux-ann-return-types) + +下一个表描述了受支持的控制器方法的返回值。所有返回值都支持反应性类型。 + +| Controller method return value |说明| +|-----------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `@ResponseBody` |返回值通过<br/>实现进行转换,并写入<br/>响应。见[`@ResponseBody`](#mvc-ann-responsebody)。| +| `HttpEntity<B>`, `ResponseEntity<B>` |指定完整响应(包括 HTTP 头和主体)的返回值将通过`HttpMessageConverter`实现进行转换<br/>并写入响应。<br/>参见[ResponseEntity](#mvc-ann-responseentity)。| +| `HttpHeaders` |返回带有标题而没有正文的响应。| +| `String` |要用<br/>实现来解析的视图名称,并与隐式<br/>模型一起使用——通过命令对象和`@ModelAttribute`方法确定。处理程序[显式注册](#mvc-ann-requestmapping-registration)方法还可以通过声明一个`Model`参数<br/>(参见[显式注册](#mvc-ann-requestmapping-registration))以编程方式丰富模型。| +| `View` |一个`View`实例用于与隐式模型一起进行渲染——通过命令对象和`@ModelAttribute`方法确定<br/>。处理程序方法还可以通过声明`Model`参数<br/>(参见[显式注册](#mvc-ann-requestmapping-registration))以编程方式丰富模型。| +| `java.util.Map`, `org.springframework.ui.Model` |要添加到隐式模型的属性,通过`RequestToViewNameTranslator`隐式确定视图名称<br/>。| +| `@ModelAttribute` |要添加到模型中的一个属性,其视图名称通过<br/>a`RequestToViewNameTranslator`隐式确定。<br/><br/>注意,`@ModelAttribute`是可选的。请参阅<br/>本表结尾处的“任何其他返回值”。| +| `ModelAndView` object |要使用的视图和模型属性,以及可选的响应状态。| +| `void` |具有`void`返回类型(或`null`返回值)的方法被认为具有完全的<br/>处理响应,如果它还具有`ServletResponse`,`OutputStream`参数,或<br/>`@ResponseStatus`注释。如果控制器进行了正的 `ETag’或`lastModified`时间戳检查(详见[Controllers](#mvc-caching-etag-lastmodified)),也是如此。<br/><br/>如果上述各项都不是真的,`void`返回类型还可以表示<br/>REST 控制器的“无响应体”或 HTML 控制器的默认视图名称选择。| +| `DeferredResult<V>` |从任何线程异步生成前面的任何返回值——例如,作为某个事件或回调的结果<br/>。见[异步请求](#mvc-ann-async)和[“推迟结果”](#mvc-ann-async-deferredresult)。| +| `Callable<V>` |在 Spring MVC 管理的线程中异步生成上述任一返回值。<br/>参见[异步请求](#mvc-ann-async)和[`Callable`](#mvc-ann-async-callable)。| +|`ListenableFuture<V>`,`java.util.concurrent.CompletionStage<V>`,`java.util.concurrent.CompletableFuture<V>`|替代`DeferredResult`,作为一种方便(例如,当底层服务<br/>返回其中之一时)。| +| `ResponseBodyEmitter`, `SseEmitter` |发出一个对象流,用“HttpMessageConverter”实现异步写入响应。也支持作为`ResponseEntity`的主体。<br/>参见[异步请求](#mvc-ann-async)和[HTTP Streaming](#mvc-ann-async-http-streaming)。| +| `StreamingResponseBody` |异步写入响应`OutputStream`。也支持作为一个“责任实体”的机构。见[异步请求](#mvc-ann-async)和[HTTP Streaming](#mvc-ann-async-http-streaming)。| +| Reactive types — Reactor, RxJava, or others through `ReactiveAdapterRegistry` |对于具有多值流的`DeferredResult`(例如,`Flux`,`Observable`)<br/>收集到`List`的替代方案。<br/><br/>用于流场景(例如,用`text/event-stream`,`application/json+stream`),用<sseemitter` 和<sgt r=“1179”/>,其中`ServletOutputStream`在 Spring MVC 管理的线程上执行阻塞 I/O,并且在每次写操作完成时施加背压<br/>。<br/><br/>参见[异步请求](#mvc-ann-async)和[Reactive Types](#mvc-ann-async-reactive-types)。| +| Any other return value |如果返回值与此表中的任何早期值不匹配,并且`void`是`String`或`void`,则将其视为一个视图名(通过 `requestToViewnameTranslator’进行默认视图名选择),前提是它不是一个简单的类型,由[Beanutils#IsSimpleProperty](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-)确定。<br/>是简单类型的值仍未解决。| + +##### Type Conversion + +[WebFlux](web-reactive.html#webflux-ann-typeconversion) + +一些表示的基于请求输入的带注释的控制器方法参数(例如 `@requestParam`,`@RequestHeader`,`@PathVariable`,`void`,以及`@CookieValue`)可以要求类型转换,如果参数被声明为`String`以外的内容。 + +对于这样的情况,类型转换是基于配置的转换器自动应用的。默认情况下,支持简单类型(`INT’、`long`、`Date`和其他类型)。你可以通过`WebDataBinder`(参见[`DataBinder`](#mvc-ann-initbinder))或通过在`FormattingConversionService`中注册 `formatters’来定制类型转换。见[Spring Field Formatting](core.html#format)。 + +类型转换中的一个实际问题是空字符串源值的处理。如果由于类型转换而变成`null`,那么这样的值将被视为丢失。这可能是`UUID`、`UUID`和其他目标类型的情况。如果要允许注入`null`,可以在参数注释上使用`required`标志,或者将参数声明为`@Nullable`。 + +| |从 5.3 开始,即使在类型转换之后,也将强制执行非空参数。如果你的处理程序<br/>方法也打算接受空值,那么可以将参数声明为`@Nullable`,或者在相应的`@RequestParam`注释中将其标记为`required=false`。这是<br/>对于在 5.3 升级中遇到的回归的最佳实践和推荐解决方案。`@Nullable`或者,你可以具体地处理例如在所需`MissingPathVariableException`的情况下产生的`@PathVariable`。转换后的空值将被处理为像<br/>一个空的原始值一样,因此相应的`Missing…​Exception`变量将被抛出。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### 矩阵变量 + +[WebFlux](web-reactive.html#webflux-ann-matrix-variables) + +[RFC 3986](https://tools.ietf.org/html/rfc3986#section-3.3)讨论路径段中的名称-值对。在 Spring MVC 中,我们将那些称为基于 Tim Berners-Lee 的[“old post”](https://www.w3.org/DesignIssues/MatrixURIs.html)的“矩阵变量”,但它们也可以称为 URI 路径参数。 + +矩阵变量可以出现在任何路径段中,每个变量用分号分隔,多个值用逗号分隔(例如,`/cars;color=red,green;year=2012`)。还可以通过重复的变量名指定多个值(例如,`color=red;color=green;color=blue`)。 + +如果一个 URL 预期包含矩阵变量,那么控制器方法的请求映射必须使用 URI 变量来掩盖该变量内容,并确保请求能够独立于矩阵变量的顺序和存在而成功匹配。下面的示例使用了一个矩阵变量: + +Java + +``` +// GET /pets/42;q=11;r=22 + +@GetMapping("/pets/{petId}") +public void findPet(@PathVariable String petId, @MatrixVariable int q) { + + // petId == 42 + // q == 11 +} +``` + +Kotlin + +``` +// GET /pets/42;q=11;r=22 + +@GetMapping("/pets/{petId}") +fun findPet(@PathVariable petId: String, @MatrixVariable q: Int) { + + // petId == 42 + // q == 11 +} +``` + +考虑到所有的路径段都可能包含矩阵变量,你有时可能需要消除矩阵变量预期在哪个路径变量中的歧义。下面的示例展示了如何做到这一点: + +Java + +``` +// GET /owners/42;q=11/pets/21;q=22 + +@GetMapping("/owners/{ownerId}/pets/{petId}") +public void findPet( + @MatrixVariable(name="q", pathVar="ownerId") int q1, + @MatrixVariable(name="q", pathVar="petId") int q2) { + + // q1 == 11 + // q2 == 22 +} +``` + +Kotlin + +``` +// GET /owners/42;q=11/pets/21;q=22 + +@GetMapping("/owners/{ownerId}/pets/{petId}") +fun findPet( + @MatrixVariable(name = "q", pathVar = "ownerId") q1: Int, + @MatrixVariable(name = "q", pathVar = "petId") q2: Int) { + + // q1 == 11 + // q2 == 22 +} +``` + +矩阵变量可以定义为可选的,并指定默认值,如下例所示: + +Java + +``` +// GET /pets/42 + +@GetMapping("/pets/{petId}") +public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) { + + // q == 1 +} +``` + +Kotlin + +``` +// GET /pets/42 + +@GetMapping("/pets/{petId}") +fun findPet(@MatrixVariable(required = false, defaultValue = "1") q: Int) { + + // q == 1 +} +``` + +要获得所有矩阵变量,可以使用`MultiValueMap`,如下例所示: + +Java + +``` +// GET /owners/42;q=11;r=12/pets/21;q=22;s=23 + +@GetMapping("/owners/{ownerId}/pets/{petId}") +public void findPet( + @MatrixVariable MultiValueMap<String, String> matrixVars, + @MatrixVariable(pathVar="petId") MultiValueMap<String, String> petMatrixVars) { + + // matrixVars: ["q" : [11,22], "r" : 12, "s" : 23] + // petMatrixVars: ["q" : 22, "s" : 23] +} +``` + +Kotlin + +``` +// GET /owners/42;q=11;r=12/pets/21;q=22;s=23 + +@GetMapping("/owners/{ownerId}/pets/{petId}") +fun findPet( + @MatrixVariable matrixVars: MultiValueMap<String, String>, + @MatrixVariable(pathVar="petId") petMatrixVars: MultiValueMap<String, String>) { + + // matrixVars: ["q" : [11,22], "r" : 12, "s" : 23] + // petMatrixVars: ["q" : 22, "s" : 23] +} +``` + +请注意,你需要启用矩阵变量的使用。在 MVC Java 配置中,你需要通过[Path Matching](#mvc-config-path-matching)设置`UrlPathHelper`和`removeSemicolonContent=false`。在 MVC XML 命名空间中,可以设置 ``Pet``。 + +##### `@RequestParam` + +[WebFlux](web-reactive.html#webflux-ann-requestparam) + +可以使用`@RequestParam`注释将 Servlet 请求参数(即查询参数或表单数据)绑定到控制器中的方法参数。 + +下面的示例展示了如何做到这一点: + +Java + +``` +@Controller +@RequestMapping("/pets") +public class EditPetForm { + + // ... + + @GetMapping + public String setupForm(@RequestParam("petId") int petId, Model model) { (1) + Pet pet = this.clinic.loadPet(petId); + model.addAttribute("pet", pet); + return "petForm"; + } + + // ... + +} +``` + +|**1**|使用`@RequestParam`绑定`petId`。| +|-----|--------------------------------------| + +Kotlin + +``` +import org.springframework.ui.set + +@Controller +@RequestMapping("/pets") +class EditPetForm { + + // ... + + @GetMapping + fun setupForm(@RequestParam("petId") petId: Int, model: Model): String { (1) + val pet = this.clinic.loadPet(petId); + model["pet"] = pet + return "petForm" + } + + // ... + +} +``` + +|**1**|使用`@RequestParam`绑定`petId`。| +|-----|--------------------------------------| + +默认情况下,使用此注释的方法参数是必需的,但是你可以通过将`@RequestParam`注释的`required`标志设置为 `false’或使用`java.util.Optional`包装器声明参数来指定方法参数是可选的。 + +如果目标方法参数类型不是“字符串”,则自动应用类型转换。见[Type Conversion](#mvc-ann-typeconversion)。 + +将参数类型声明为数组或列表,可以为相同的参数名称解析多个参数值。 + +当`@RequestParam`注释被声明为`Map<String, String>`或 `multivalueMap<String, String>` 时,如果注释中没有指定参数名称,那么映射将被填充为每个给定参数名称的请求参数值。 + +请注意,`@RequestParam`的使用是可选的(例如,用于设置其属性)。默认情况下,任何参数是简单的值类型(由`@ResponseStatus`确定)且不是由任何其他参数解析器解析的,都将被视为用`@RequestParam`进行了注释。 + +##### `@RequestHeader` + +[WebFlux](web-reactive.html#webflux-ann-requestheader) + +可以使用`@RequestHeader`注释将请求头绑定到控制器中的方法参数。 + +考虑以下带有标题的请求: + +``` +Host localhost:8080 +Accept text/html,application/xhtml+xml,application/xml;q=0.9 +Accept-Language fr,en-gb;q=0.7,en;q=0.3 +Accept-Encoding gzip,deflate +Accept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7 +Keep-Alive 300 +``` + +下面的示例获取`Accept-Encoding`和`Keep-Alive`标题的值: + +Java + +``` +@GetMapping("/demo") +public void handle( + @RequestHeader("Accept-Encoding") String encoding, (1) + @RequestHeader("Keep-Alive") long keepAlive) { (2) + //... +} +``` + +|**1**|获取`Accept-Encoding`标头的值。| +|-----|----------------------------------------------| +|**2**|获取`Keep-Alive`标头的值。| + +Kotlin + +``` +@GetMapping("/demo") +fun handle( + @RequestHeader("Accept-Encoding") encoding: String, (1) + @RequestHeader("Keep-Alive") keepAlive: Long) { (2) + //... +} +``` + +|**1**|获取`Accept-Encoding`标头的值。| +|-----|----------------------------------------------| +|**2**|获取`Keep-Alive`标头的值。| + +如果目标方法参数类型不是“String”,则自动应用类型转换。见[Type Conversion](#mvc-ann-typeconversion)。 + +当`@RequestHeader`在`Map<String, String>`、`multivalueMap<String, String>` 或`HttpHeaders`参数上使用`@RequestHeader`注释时,映射将填充所有头值。 + +| |内置的支持可用于将逗号分隔的字符串转换为<br/>数组或字符串集合或类型转换系统已知的其他类型。对于<br/>示例,用`@RequestHeader("Accept")`注释的方法参数可以是 `string’类型,但也可以是`String[]`或`List<String>`类型。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### `@CookieValue` + +[WebFlux](web-reactive.html#webflux-ann-cookievalue) + +可以使用`@CookieValue`注释将 HTTP cookie 的值绑定到控制器中的方法参数。 + +考虑使用以下 cookie 的请求: + +``` +JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84 +``` + +下面的示例展示了如何获得 Cookie 值: + +Java + +``` +@GetMapping("/demo") +public void handle(@CookieValue("JSESSIONID") String cookie) { (1) + //... +} +``` + +|**1**|获取`JSESSIONID`cookie 的值。| +|-----|-----------------------------------------| + +Kotlin + +``` +@GetMapping("/demo") +fun handle(@CookieValue("JSESSIONID") cookie: String) { (1) + //... +} +``` + +|**1**|获取`JSESSIONID`cookie 的值。| +|-----|-----------------------------------------| + +如果目标方法参数类型不是`String`,则自动应用类型转换。见[Type Conversion](#mvc-ann-typeconversion)。 + +##### `@ModelAttribute` + +[WebFlux](web-reactive.html#webflux-ann-modelattrib-method-args) + +你可以在方法参数上使用`@ModelAttribute`注释来访问模型中的一个属性,或者如果不存在该属性,则将其实例化。模型属性还覆盖了来自 HTTP Servlet 请求参数的值,这些参数的名称与字段名匹配。这被称为数据绑定,它使你不必处理解析和转换单个查询参数和窗体字段的问题。下面的示例展示了如何做到这一点: + +爪哇 + +``` +@PostMapping("/owners/{ownerId}/pets/{petId}/edit") +public String processSubmit(@ModelAttribute Pet pet) { + // method logic... +} +``` + +Kotlin + +``` +@PostMapping("/owners/{ownerId}/pets/{petId}/edit") +fun processSubmit(@ModelAttribute pet: Pet): String { + // method logic... +} +``` + +上面的`Pet`实例的来源如下: + +* 从可能由[@ModelAttribute 方法](#mvc-ann-modelattrib-methods)添加的模型中检索。 + +* 如果模型属性在类级[@sessionAttributions’](#mvc-ann-sessionattributes)注释中列出,则从 HTTP 会话中检索。 + +* 通过`Converter`获得,其中模型属性名与请求值(如路径变量或请求参数)的名称匹配(参见下一个示例)。 + +* 使用其默认构造函数实例化。 + +* 通过具有与 Servlet 请求参数匹配的参数的“主构造函数”实例化。参数名称是通过 爪哇Beans@ConstructorProperties 或通过字节码中的运行时保留参数名称确定的。 + +除了使用`@CookieValue`来提供它,或者依靠框架来创建模型属性之外,另一种选择是使用 `converter<String, T>` 来提供实例。当模型属性名与请求值(例如路径变量或请求参数)的名称匹配时,并且存在`Converter`到模型属性类型的`String`时,将应用此方法。在下面的示例中,模型属性名为`account`,它与 URI 路径变量`account`匹配,并且有一个已注册的`Converter<String, Account>`,它可以从数据存储加载`Account`: + +爪哇 + +``` +@PutMapping("/accounts/{account}") +public String save(@ModelAttribute("account") Account account) { + // ... +} +``` + +Kotlin + +``` +@PutMapping("/accounts/{account}") +fun save(@ModelAttribute("account") account: Account): String { + // ... +} +``` + +在获得模型属性实例之后,再进行数据绑定。“WebDatabinder”类将 Servlet 请求参数名称(查询参数和表单字段)与目标`Object`上的字段名称匹配。在应用了类型转换之后(如有必要)填充匹配字段。有关数据绑定(和验证)的更多信息,请参见[验证](core.html#validation)。有关自定义数据绑定的更多信息,请参见[`DataBinder`](#mvc-ann-initbinder)。 + +数据绑定可能会导致错误。默认情况下,会引发`BindException`。但是,要检查控制器方法中的此类错误,可以在`@ModelAttribute`旁边立即添加一个`BindingResult`参数,如下例所示: + +爪哇 + +``` +@PostMapping("/owners/{ownerId}/pets/{petId}/edit") +public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { (1) + if (result.hasErrors()) { + return "petForm"; + } + // ... +} +``` + +|**1**|在`@ModelAttribute`旁边添加一个`BindingResult`。| +|-----|-------------------------------------------------------| + +Kotlin + +``` +@PostMapping("/owners/{ownerId}/pets/{petId}/edit") +fun processSubmit(@ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1) + if (result.hasErrors()) { + return "petForm" + } + // ... +} +``` + +|**1**|在`@ModelAttribute`旁边添加一个`BindingResult`。| +|-----|-------------------------------------------------------| + +在某些情况下,你可能希望访问没有数据绑定的模型属性。对于这种情况,你可以将`@ModelAttribute(binding=false)`注入控制器并直接访问它,或者设置`@ModelAttribute(binding=false)`,如下例所示: + +爪哇 + +``` +@ModelAttribute +public AccountForm setUpForm() { + return new AccountForm(); +} + +@ModelAttribute +public Account findAccount(@PathVariable String accountId) { + return accountRepository.findOne(accountId); +} + +@PostMapping("update") +public String update(@Valid AccountForm form, BindingResult result, + @ModelAttribute(binding=false) Account account) { (1) + // ... +} +``` + +|**1**|设置`@ModelAttribute(binding=false)`。| +|-----|-----------------------------------------| + +Kotlin + +``` +@ModelAttribute +fun setUpForm(): AccountForm { + return AccountForm() +} + +@ModelAttribute +fun findAccount(@PathVariable accountId: String): Account { + return accountRepository.findOne(accountId) +} + +@PostMapping("update") +fun update(@Valid form: AccountForm, result: BindingResult, + @ModelAttribute(binding = false) account: Account): String { (1) + // ... +} +``` + +|**1**|设置`@ModelAttribute(binding=false)`。| +|-----|-----------------------------------------| + +你可以在数据绑定后通过添加 javax.validation.validate 注释或 Spring 的`@Validated`注释([Bean Validation](core.html#validation-beanvalidation)和[Spring validation](core.html#validation))自动应用验证。下面的示例展示了如何做到这一点: + +爪哇 + +``` +@PostMapping("/owners/{ownerId}/pets/{petId}/edit") +public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { (1) + if (result.hasErrors()) { + return "petForm"; + } + // ... +} +``` + +|**1**|验证`Pet`实例。| +|-----|----------------------------| + +Kotlin + +``` +@PostMapping("/owners/{ownerId}/pets/{petId}/edit") +fun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1) + if (result.hasErrors()) { + return "petForm" + } + // ... +} +``` + +请注意,使用`@ModelAttribute`是可选的(例如,用于设置其属性)。默认情况下,任何不是简单值类型(由[Beanutils#IsSimpleProperty](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-)确定)且未由任何其他参数解析器解析的参数都将被视为已用`@ModelAttribute`注释。 + +##### `@SessionAttributes` + +[WebFlux](web-reactive.html#webflux-ann-sessionattributes) + +`@SessionAttributes`用于在请求之间的 HTTP Servlet 会话中存储模型属性。它是一种类型级别的注释,用于声明特定控制器使用的会话属性。这通常会列出模型属性的名称或模型属性的类型,这些属性应该透明地存储在会话中,以供后续的访问请求使用。 + +下面的示例使用`@SessionAttributes`注释: + +爪哇 + +``` +@Controller +@SessionAttributes("pet") (1) +public class EditPetForm { + // ... +} +``` + +|**1**|使用`@SessionAttributes`注释。| +|-----|------------------------------------------| + +Kotlin + +``` +@Controller +@SessionAttributes("pet") (1) +class EditPetForm { + // ... +} +``` + +|**1**|使用`@SessionAttributes`注释。| +|-----|------------------------------------------| + +在第一个请求中,当将名称为`pet`的模型属性添加到模型中时,将自动将其提升到 HTTP Servlet 会话中并将其保存。在另一个控制器方法使用`SessionStatus`方法参数清除存储之前,它一直保持不变,如下例所示: + +爪哇 + +``` +@Controller +@SessionAttributes("pet") (1) +public class EditPetForm { + + // ... + + @PostMapping("/pets/{id}") + public String handle(Pet pet, BindingResult errors, SessionStatus status) { + if (errors.hasErrors) { + // ... + } + status.setComplete(); (2) + // ... + } +} +``` + +|**1**|在 Servlet 会话中存储`Pet`值。| +|-----|--------------------------------------------------| +|**2**|清除 Servlet 会话中的`Pet`值。| + +Kotlin + +``` +@Controller +@SessionAttributes("pet") (1) +class EditPetForm { + + // ... + + @PostMapping("/pets/{id}") + fun handle(pet: Pet, errors: BindingResult, status: SessionStatus): String { + if (errors.hasErrors()) { + // ... + } + status.setComplete() (2) + // ... + } +} +``` + +|**1**|在 Servlet 会话中存储`Pet`值。| +|-----|--------------------------------------------------| +|**2**|清除 Servlet 会话中的`Pet`值。| + +##### `@SessionAttribute` + +[WebFlux](web-reactive.html#webflux-ann-sessionattribute) + +如果你需要访问已存在的会话属性,这些属性是全局管理的(也就是说,在控制器之外——例如,由过滤器管理),并且可能存在,也可能不存在,那么你可以在方法参数上使用`@SessionAttribute`注释,如下例所示: + +爪哇 + +``` +@RequestMapping("/") +public String handle(@SessionAttribute User user) { (1) + // ... +} +``` + +|**1**|使用`@SessionAttribute`注释。| +|-----|---------------------------------------| + +Kotlin + +``` +@RequestMapping("/") +fun handle(@SessionAttribute user: User): String { (1) + // ... +} +``` + +对于需要添加或删除会话属性的用例,可以考虑将 `org.springframework.web.context.request.webrequest` 或 `javax. Servlet.http.httpsession` 注入控制器方法。 + +对于将会话中的模型属性临时存储为控制器工作流的一部分,可以考虑使用`@RequestParam`,如[@sessionAttributions’](#mvc-ann-sessionattributes)中所述。 + +##### `@RequestAttribute` + +[WebFlux](web-reactive.html#webflux-ann-requestattrib) + +与`@SessionAttribute`类似,你可以使用`@RequestAttribute`注释来访问先前创建的预先存在的请求属性(例如,通过 Servlet `Filter`或`HandlerInterceptor`): + +爪哇 + +``` +@GetMapping("/") +public String handle(@RequestAttribute Client client) { (1) + // ... +} +``` + +|**1**|使用`@RequestAttribute`注释。| +|-----|-----------------------------------------| + +Kotlin + +``` +@GetMapping("/") +fun handle(@RequestAttribute client: Client): String { (1) + // ... +} +``` + +|**1**|使用`@RequestAttribute`注释。| +|-----|-----------------------------------------| + +##### 重定向属性 + +默认情况下,所有模型属性都被认为是作为重定向 URL 中的 URI 模板变量公开的。在剩余的属性中,那些是基元类型或基元类型的集合或数组的属性将自动附加为查询参数。 + +如果模型实例是专门为重定向准备的,那么将原始类型属性追加为查询参数可能是期望的结果。然而,在带注释的控制器中,模型可以包含为呈现目的而添加的附加属性(例如,下拉字段值)。为了避免这种属性出现在 URL 中的可能性,`@RequestMapping`方法可以声明类型为`RedirectAttributes`的参数,并使用它来指定使`RedirectView`可用的确切属性。如果该方法确实重定向,则使用`RedirectAttributes`的内容。否则,将使用模型的内容。 + +`RequestMappingHandlerAdapter`提供了一个名为 `ignoreDefaultModelOnReDirect’的标志,你可以使用它来指示,如果控制器方法重定向,则永远不应该使用默认的 `model’的内容。相反,控制器方法应该声明类型为`RedirectAttributes`的属性,或者,如果不这样做,则不应该将任何属性传递到`RedirectView`。MVC 名称空间和 MVC 爪哇 配置都将此标志设置为`false`,以保持向后兼容性。但是,对于新的应用程序,我们建议将其设置为`true`。 + +请注意,当展开重定向 URL 时,当前请求中的 URI 模板变量将自动可用,并且你不需要通过`Model`或`RedirectAttributes`显式地添加它们。下面的示例展示了如何定义重定向: + +爪哇 + +``` +@PostMapping("/files/{path}") +public String upload(...) { + // ... + return "redirect:files/{path}"; +} +``` + +Kotlin + +``` +@PostMapping("/files/{path}") +fun upload(...): String { + // ... + return "redirect:files/{path}" +} +``` + +另一种将数据传递给重定向目标的方法是使用 flash 属性。与其他重定向属性不同,flash 属性保存在 HTTP 会话中(因此不会出现在 URL 中)。有关更多信息,请参见[flash 属性](#mvc-flash-attributes)。 + +##### flash 属性 + +Flash 属性为一个请求提供了一种方式,用于存储打算在另一个请求中使用的属性。这是重定向时最常见的需求——例如,后重定向-get 模式。在重定向之前(通常是在会话中)临时保存 flash 属性,以便在重定向之后使请求可用,并立即删除。 + +Spring MVC 有两个主要的抽象来支持 Flash 属性。`FlashMap`用于保存 flash 属性,而`FlashMapManager`用于存储、检索和管理 `flashmap’实例。 + +flash 属性支持始终是“打开”的,并且不需要显式地启用。但是,如果不使用,它永远不会导致 HTTP 会话的创建。在每个请求中,都有一个“input”`FlashMap`,其中包含从以前的请求(如果有的话)传递的属性,以及一个“output”`FlashMap`,其中包含用于保存后续请求的属性。这两个`FlashMap`实例都可以通过 `RequestContextutils’中的静态方法从 Spring MVC 中的任何地方访问。 + +带注释的控制器通常不需要直接使用`FlashMap`。相反,“@requestmapping”方法可以接受类型`RedirectAttributes`的参数,并使用它为重定向场景添加 flash 属性。通过“RedirectAttributes”添加的 flash 属性将自动传播到“输出”flashmap。类似地,在重定向之后,来自“input”`FlashMap`的属性将自动添加到服务于目标 URL 的控制器的“model”中。 + +将请求与 flash 属性匹配 + +flash 属性的概念存在于许多其他 Web 框架中,并已被证明有时会遇到并发性问题。这是因为,根据定义,flash 属性将被存储到下一个请求为止。然而,“下一个”请求可能不是预期的接收者,而是另一个异步请求(例如,轮询或资源请求),在这种情况下,flash 属性被过早地删除。 + +为了减少此类问题的可能性,`RedirectView`自动“标记”具有目标重定向 URL 的路径和查询参数的“flashmap”实例。反过来,当它查找“input”`FlashMap`时,默认的`FlashMapManager`将该信息与传入请求匹配。 + +这并不能完全消除并发问题的可能性,但可以通过重定向 URL 中已有的信息大大减少并发问题。因此,我们建议你主要在重定向场景中使用 Flash 属性。 + +##### 多部分 + +[WebFlux](web-reactive.html#webflux-multipart-forms) + +在`MultipartResolver`已[enabled](#mvc-multipart)之后,对带有`multipart/form-data`的 POST 请求的内容进行解析,并将其作为常规的请求参数进行访问。以下示例访问一个常规窗体字段和一个上载文件: + +爪哇 + +``` +@Controller +public class FileUploadController { + + @PostMapping("/form") + public String handleFormUpload(@RequestParam("name") String name, + @RequestParam("file") MultipartFile file) { + + if (!file.isEmpty()) { + byte[] bytes = file.getBytes(); + // store the bytes somewhere + return "redirect:uploadSuccess"; + } + return "redirect:uploadFailure"; + } +} +``` + +Kotlin + +``` +@Controller +class FileUploadController { + + @PostMapping("/form") + fun handleFormUpload(@RequestParam("name") name: String, + @RequestParam("file") file: MultipartFile): String { + + if (!file.isEmpty) { + val bytes = file.bytes + // store the bytes somewhere + return "redirect:uploadSuccess" + } + return "redirect:uploadFailure" + } +} +``` + +将参数类型声明为`List<MultipartFile>`可以为相同的参数名称解析多个文件。 + +当`@RequestParam`注释被声明为`Map<String, MultipartFile>`或 `multivalueMap<String, MultipartFile>` 时,如果注释中没有指定参数名称,则该映射将被填充每个给定参数名称的多部分文件。 + +| |通过 Servlet 3.0 多部分解析,你还可以声明`javax.servlet.http.Part`而不是 Spring 的`MultipartFile`,作为方法参数或集合值类型。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +还可以使用多部分内容作为与[command object](#mvc-ann-modelattrib-method-args)的数据绑定的一部分。例如,前面示例中的表单字段和文件可以是表单对象上的字段,如下例所示: + +爪哇 + +``` +class MyForm { + + private String name; + + private MultipartFile file; + + // ... +} + +@Controller +public class FileUploadController { + + @PostMapping("/form") + public String handleFormUpload(MyForm form, BindingResult errors) { + if (!form.getFile().isEmpty()) { + byte[] bytes = form.getFile().getBytes(); + // store the bytes somewhere + return "redirect:uploadSuccess"; + } + return "redirect:uploadFailure"; + } +} +``` + +Kotlin + +``` +class MyForm(val name: String, val file: MultipartFile, ...) + +@Controller +class FileUploadController { + + @PostMapping("/form") + fun handleFormUpload(form: MyForm, errors: BindingResult): String { + if (!form.file.isEmpty) { + val bytes = form.file.bytes + // store the bytes somewhere + return "redirect:uploadSuccess" + } + return "redirect:uploadFailure" + } +} +``` + +在 RESTful 服务场景中,还可以从非浏览器客户端提交多部分请求。下面的示例显示了一个使用 JSON 的文件: + +``` +POST /someUrl +Content-Type: multipart/mixed + +--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp +Content-Disposition: form-data; name="meta-data" +Content-Type: application/json; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +{ + "name": "value" +} +--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp +Content-Disposition: form-data; name="file-data"; filename="file.properties" +Content-Type: text/xml +Content-Transfer-Encoding: 8bit +... File Data ... +``` + +你可以使用`@RequestParam`作为`String`访问“元数据”部分,但你可能希望它从 JSON 反序列化(类似于`@RequestBody`)。在使用[HtpMessageConverter](integration.html#rest-message-conversion)转换多个部分之后,使用 @requestpart 注释访问多个部分: + +爪哇 + +``` +@PostMapping("/") +public String handle(@RequestPart("meta-data") MetaData metadata, + @RequestPart("file-data") MultipartFile file) { + // ... +} +``` + +Kotlin + +``` +@PostMapping("/") +fun handle(@RequestPart("meta-data") metadata: MetaData, + @RequestPart("file-data") file: MultipartFile): String { + // ... +} +``` + +你可以将`@RequestPart`与`javax.validation.Valid`结合使用,或者使用 Spring 的 `@validated’注释,这两种方法都会导致应用标准 Bean 验证。默认情况下,验证错误会导致`MethodArgumentNotValidException`,并将其转换为 400(bad\_request)响应。或者,你可以通过`Errors`或`BindingResult`参数在控制器内部本地处理验证错误,如下例所示: + +爪哇 + +``` +@PostMapping("/") +public String handle(@Valid @RequestPart("meta-data") MetaData metadata, + BindingResult result) { + // ... +} +``` + +Kotlin + +``` +@PostMapping("/") +fun handle(@Valid @RequestPart("meta-data") metadata: MetaData, + result: BindingResult): String { + // ... +} +``` + +##### `@RequestBody` + +[WebFlux](web-reactive.html#webflux-ann-requestbody) + +你可以使用`@RequestBody`注释,通过[`HttpMessageConverter’](integration.html#rest-message-conversion)将请求主体读取并反序列化为 ` 对象’。下面的示例使用了`@RequestBody`参数: + +爪哇 + +``` +@PostMapping("/accounts") +public void handle(@RequestBody Account account) { + // ... +} +``` + +Kotlin + +``` +@PostMapping("/accounts") +fun handle(@RequestBody account: Account) { + // ... +} +``` + +你可以使用[MVC Config](#mvc-config)的[消息转换器](#mvc-config-message-converters)选项来配置或自定义消息转换。 + +你可以将`@RequestBody`与`javax.validation.Valid`或 Spring 的 `@validated’注释结合使用,这两种方法都会导致应用标准 Bean 验证。默认情况下,验证错误会导致`MethodArgumentNotValidException`,并将其转换为 400(bad\_request)响应。或者,你可以通过`Errors`或`BindingResult`参数在控制器内部本地处理验证错误,如下例所示: + +爪哇 + +``` +@PostMapping("/accounts") +public void handle(@Valid @RequestBody Account account, BindingResult result) { + // ... +} +``` + +Kotlin + +``` +@PostMapping("/accounts") +fun handle(@Valid @RequestBody account: Account, result: BindingResult) { + // ... +} +``` + +##### HttpEntity + +[WebFlux](web-reactive.html#webflux-ann-httpentity) + +`HttpEntity`与使用[`@RequestBody`](#mvc-ann-requestbody)大致相同,但它基于一个容器对象,该对象公开了请求头和主体。下面的清单展示了一个示例: + +爪哇 + +``` +@PostMapping("/accounts") +public void handle(HttpEntity<Account> entity) { + // ... +} +``` + +Kotlin + +``` +@PostMapping("/accounts") +fun handle(entity: HttpEntity<Account>) { + // ... +} +``` + +##### `@ResponseBody` + +[WebFlux](web-reactive.html#webflux-ann-responsebody) + +你可以在方法上使用`@ResponseBody`注释,通过[HtpMessageConverter](integration.html#rest-message-conversion)将返回序列化到响应主体。下面的清单展示了一个示例: + +爪哇 + +``` +@GetMapping("/accounts/{id}") +@ResponseBody +public Account handle() { + // ... +} +``` + +Kotlin + +``` +@GetMapping("/accounts/{id}") +@ResponseBody +fun handle(): Account { + // ... +} +``` + +`@ResponseBody`在类级别上也受到支持,在这种情况下,所有控制器方法都会继承它。这是`@RestController`的效果,它不过是一个标记有`@Controller`和`@ResponseBody`的元注释。 + +你可以将`@ResponseBody`用于反应类型。有关更多详细信息,请参见[异步请求](#mvc-ann-async)和[Reactive Types](#mvc-ann-async-reactive-types)。 + +你可以使用[MVC Config](#mvc-config)的[消息转换器](#mvc-config-message-converters)选项来配置或自定义消息转换。 + +你可以将`@ResponseBody`方法与 JSON 序列化视图结合起来。详见[JacksonJSON](#mvc-ann-jackson)。 + +##### 负责实体 + +[WebFlux](web-reactive.html#webflux-ann-responseentity) + +`ResponseEntity`类似于[`@ResponseBody`](#mvc-ann-responsebody),但带有状态和标题。例如: + +爪哇 + +``` +@GetMapping("/something") +public ResponseEntity<String> handle() { + String body = ... ; + String etag = ... ; + return ResponseEntity.ok().eTag(etag).build(body); +} +``` + +Kotlin + +``` +@GetMapping("/something") +fun handle(): ResponseEntity<String> { + val body = ... + val etag = ... + return ResponseEntity.ok().eTag(etag).build(body) +} +``` + +Spring MVC 支持使用单个值[reactive type](#mvc-ann-async-reactive-types)来异步地产生`ResponseEntity`,和/或用于主体的单个值和多个值的反应类型。这允许以下类型的异步响应: + +* `ResponseEntity<Mono<T>>`或`ResponseEntity<Flux<T>>`在稍后异步提供主体时,立即使响应状态和头为已知。如果主体由 0.1 个值组成,则使用`Mono`;如果可以产生多个值,则使用`Flux`。 + +* `Mono<ResponseEntity<T>>`在稍后的时间点异步提供了所有这三个方面——响应状态、头和主体。这允许响应状态和头根据异步请求处理的结果而变化。 + +##### Jackson JSON + +Spring 提供对 JacksonJSON 库的支持。 + +###### JSON 视图 + +[WebFlux](web-reactive.html#webflux-ann-jsonview) + +Spring MVC 提供了对[Jackson 的序列化视图](https://www.baeldung.com/jackson-json-view-annotation)的内置支持,其仅允许呈现`Object`中所有字段的一个子集。要将其与 `@responsebody’或`ResponseEntity`控制器方法一起使用,你可以使用 Jackson 的 `@jsonview’注释来激活序列化视图类,如下例所示: + +爪哇 + +``` +@RestController +public class UserController { + + @GetMapping("/user") + @JsonView(User.WithoutPasswordView.class) + public User getUser() { + return new User("eric", "7!jd#h23"); + } +} + +public class User { + + public interface WithoutPasswordView {}; + public interface WithPasswordView extends WithoutPasswordView {}; + + private String username; + private String password; + + public User() { + } + + public User(String username, String password) { + this.username = username; + this.password = password; + } + + @JsonView(WithoutPasswordView.class) + public String getUsername() { + return this.username; + } + + @JsonView(WithPasswordView.class) + public String getPassword() { + return this.password; + } +} +``` + +Kotlin + +``` +@RestController +class UserController { + + @GetMapping("/user") + @JsonView(User.WithoutPasswordView::class) + fun getUser() = User("eric", "7!jd#h23") +} + +class User( + @JsonView(WithoutPasswordView::class) val username: String, + @JsonView(WithPasswordView::class) val password: String) { + + interface WithoutPasswordView + interface WithPasswordView : WithoutPasswordView +} +``` + +| |`@JsonView`允许一个视图类的数组,但是每个<br/>控制器方法只能指定一个。如果需要激活多个视图,可以使用复合接口。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +如果你希望以编程方式执行上述操作,而不是声明`@JsonView`注释,请将返回值包装为`MappingJacksonValue`,并使用它来提供序列化视图: + +爪哇 + +``` +@RestController +public class UserController { + + @GetMapping("/user") + public MappingJacksonValue getUser() { + User user = new User("eric", "7!jd#h23"); + MappingJacksonValue value = new MappingJacksonValue(user); + value.setSerializationView(User.WithoutPasswordView.class); + return value; + } +} +``` + +Kotlin + +``` +@RestController +class UserController { + + @GetMapping("/user") + fun getUser(): MappingJacksonValue { + val value = MappingJacksonValue(User("eric", "7!jd#h23")) + value.serializationView = User.WithoutPasswordView::class.java + return value + } +} +``` + +对于依赖于视图分辨率的控制器,可以将序列化视图类添加到模型中,如下例所示: + +爪哇 + +``` +@Controller +public class UserController extends AbstractController { + + @GetMapping("/user") + public String getUser(Model model) { + model.addAttribute("user", new User("eric", "7!jd#h23")); + model.addAttribute(JsonView.class.getName(), User.WithoutPasswordView.class); + return "userView"; + } +} +``` + +Kotlin + +``` +import org.springframework.ui.set + +@Controller +class UserController : AbstractController() { + + @GetMapping("/user") + fun getUser(model: Model): String { + model["user"] = User("eric", "7!jd#h23") + model[JsonView::class.qualifiedName] = User.WithoutPasswordView::class.java + return "userView" + } +} +``` + +#### 1.3.4.模型 + +[WebFlux](web-reactive.html#webflux-ann-modelattrib-methods) + +你可以使用`@ModelAttribute`注释: + +* 在[method argument](#mvc-ann-modelattrib-method-args)中的`@RequestMapping`方法上创建或访问模型中的`Object`,并通过 `WebDatabinder’将其绑定到请求。 + +* 作为`@Controller`或`@ControllerAdvice`类中的方法级注释,它有助于在任何`@RequestMapping`方法调用之前初始化模型。 + +* 在`@RequestMapping`方法上标记其返回值是一个模型属性。 + +本节讨论`@ModelAttribute`方法——前面列表中的第二项。控制器可以有任意数量的`@ModelAttribute`方法。所有这样的方法都是在同一个控制器中的`@RequestMapping`方法之前调用的。还可以通过`@ControllerAdvice`在控制器之间共享`@ModelAttribute`方法。有关更多详细信息,请参见[财务总监建议](#mvc-ann-controller-advice)一节。 + +`@ModelAttribute`方法具有灵活的方法签名。它们支持许多与`@RequestMapping`方法相同的参数,但`@ModelAttribute`本身或与请求主体相关的任何参数除外。 + +下面的示例显示了`@ModelAttribute`方法: + +爪哇 + +``` +@ModelAttribute +public void populateModel(@RequestParam String number, Model model) { + model.addAttribute(accountRepository.findAccount(number)); + // add more ... +} +``` + +Kotlin + +``` +@ModelAttribute +fun populateModel(@RequestParam number: String, model: Model) { + model.addAttribute(accountRepository.findAccount(number)) + // add more ... +} +``` + +下面的示例只添加了一个属性: + +爪哇 + +``` +@ModelAttribute +public Account addAccount(@RequestParam String number) { + return accountRepository.findAccount(number); +} +``` + +Kotlin + +``` +@ModelAttribute +fun addAccount(@RequestParam number: String): Account { + return accountRepository.findAccount(number) +} +``` + +| |当未显式指定名称时,将根据`Object`类型选择缺省名称,正如在[`Conventions`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/core/Conventions.html)的 爪哇doc 中所解释的,<br/>通过`@ModelAttribute`上的<br/>属性(对于返回值),始终可以通过重载的`name`方法或<br/>分配显式名称。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +你也可以在`@RequestMapping`方法上使用`@ModelAttribute`作为方法级别的注释,在这种情况下,`@RequestMapping`方法的返回值被解释为一个模型属性。这通常不是必需的,因为这是 HTML 控制器中的默认行为,除非返回值是`String`,否则将被解释为视图名称。@ModelAttribute` 还可以自定义模型属性名称,如下例所示: + +爪哇 + +``` +@GetMapping("/accounts/{id}") +@ModelAttribute("myAccount") +public Account handle() { + // ... + return account; +} +``` + +Kotlin + +``` +@GetMapping("/accounts/{id}") +@ModelAttribute("myAccount") +fun handle(): Account { + // ... + return account +} +``` + +#### 1.3.5.`DataBinder` + +[WebFlux](web-reactive.html#webflux-ann-initbinder) + +`@Controller`或`@ControllerAdvice`类可以具有初始化`@InitBinder`实例的`@InitBinder`方法,而这些方法可以: + +* 将请求参数(即窗体或查询数据)绑定到模型对象。 + +* 将基于字符串的请求值(例如请求参数、路径变量、头、cookie 和其他)转换为控制器方法参数的目标类型。 + +* 在呈现 HTML 窗体时,将模型对象值格式化为`String`值。 + +`@InitBinder`方法可以注册控制器特定的`java.beans.PropertyEditor`或 Spring `Converter`和`Formatter`组件。此外,可以使用[MVC config](#mvc-config-conversion)在全局共享的`FormattingConversionService`中注册`Converter`和`Formatter`类型。 + +`@InitBinder`方法支持许多与`@RequestMapping`方法相同的参数,但`@ModelAttribute`(命令对象)参数除外。通常,它们是用`WebDataBinder`参数(用于注册)和`void`返回值声明的。下面的清单展示了一个示例: + +爪哇 + +``` +@Controller +public class FormController { + + @InitBinder (1) + public void initBinder(WebDataBinder binder) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); + dateFormat.setLenient(false); + binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false)); + } + + // ... +} +``` + +|**1**|定义`@InitBinder`方法。| +|-----|---------------------------------| + +Kotlin + +``` +@Controller +class FormController { + + @InitBinder (1) + fun initBinder(binder: WebDataBinder) { + val dateFormat = SimpleDateFormat("yyyy-MM-dd") + dateFormat.isLenient = false + binder.registerCustomEditor(Date::class.java, CustomDateEditor(dateFormat, false)) + } + + // ... +} +``` + +|**1**|定义`@InitBinder`方法。| +|-----|---------------------------------| + +或者,当你通过共享的“formattingConversionService”使用基于`Formatter`的设置时,你可以重复使用相同的方法并注册特定于控制器的`Formatter`实现,如下例所示: + +爪哇 + +``` +@Controller +public class FormController { + + @InitBinder (1) + protected void initBinder(WebDataBinder binder) { + binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd")); + } + + // ... +} +``` + +|**1**|在自定义格式化程序上定义`@InitBinder`方法。| +|-----|-------------------------------------------------------| + +Kotlin + +``` +@Controller +class FormController { + + @InitBinder (1) + protected fun initBinder(binder: WebDataBinder) { + binder.addCustomFormatter(DateFormatter("yyyy-MM-dd")) + } + + // ... +} +``` + +|**1**|在自定义格式化程序上定义`@InitBinder`方法。| +|-----|-------------------------------------------------------| + +#### 1.3.6.例外 + +[WebFlux](web-reactive.html#webflux-ann-controller-exceptions) + +`@Controller`和[@controlleradvice](#mvc-ann-controller-advice)类可以使用 `@ExceptionHandler’方法来处理来自控制器方法的异常,如下例所示: + +爪哇 + +``` +@Controller +public class SimpleController { + + // ... + + @ExceptionHandler + public ResponseEntity<String> handle(IOException ex) { + // ... + } +} +``` + +Kotlin + +``` +@Controller +class SimpleController { + + // ... + + @ExceptionHandler + fun handle(ex: IOException): ResponseEntity<String> { + // ... + } +} +``` + +异常可能与正在传播的顶级异常(例如,抛出的直接“ioException”)匹配,也可能与包装异常中的嵌套原因匹配(例如,将`IOException`包装在`IllegalStateException`中)。截至 5.3,这可以匹配在任意的原因水平,而以前只考虑一个直接原因。 + +为了匹配异常类型,最好将目标异常声明为方法参数,如前面的示例所示。当多个异常方法匹配时,根异常匹配通常优于原因异常匹配。更具体地说,`ExceptionDepthComparator`用于根据抛出的异常类型的深度对异常进行排序。 + +或者,注释声明可以缩小异常类型的范围以进行匹配,如下例所示: + +爪哇 + +``` +@ExceptionHandler({FileSystemException.class, RemoteException.class}) +public ResponseEntity<String> handle(IOException ex) { + // ... +} +``` + +Kotlin + +``` +@ExceptionHandler(FileSystemException::class, RemoteException::class) +fun handle(ex: IOException): ResponseEntity<String> { + // ... +} +``` + +你甚至可以使用带有非常通用的参数签名的特定异常类型列表,如下例所示: + +爪哇 + +``` +@ExceptionHandler({FileSystemException.class, RemoteException.class}) +public ResponseEntity<String> handle(Exception ex) { + // ... +} +``` + +Kotlin + +``` +@ExceptionHandler(FileSystemException::class, RemoteException::class) +fun handle(ex: Exception): ResponseEntity<String> { + // ... +} +``` + +| |根和原因异常匹配之间的区别可能是令人惊讶的。<br/><br/>在前面显示的`IOException`变体中,该方法通常以<br/>实际调用的`FileSystemException`或`RemoteException`实例作为参数,<br/>因为它们都从`IOException`扩展。但是,如果任何这样的匹配<br/>异常是在包装异常中传播的,而包装异常本身是`IOException`,<br/>传递的异常实例是该包装异常。<br/><br/>变体中的行为甚至更简单。这是<br/>在包装场景中总是与包装异常一起调用,在这种情况下,<br/>实际上匹配了通过`ex.getCause()`发现的异常。<br/>传入的异常是实际的`FileSystemException`或 `remoteexception’实例,只有当这些异常作为顶级异常抛出时才是这样。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +我们通常建议你在参数签名中尽可能地具体,以减少根异常类型和原因异常类型之间不匹配的可能性。考虑将多个匹配方法分解为单独的`@ExceptionHandler`方法,每个方法通过其签名匹配单个特定的异常类型。 + +在多个“@controlleradvice”安排中,我们建议在`@ControllerAdvice`上声明主根异常映射,并按相应的顺序进行优先排序。虽然根异常匹配比原因更可取,但这是在给定控制器或`@ControllerAdvice`类的方法之间定义的。这意味着在较高优先级的“@Controlleradvice” Bean 上的原因匹配比在较低优先级的“@Controlleradvice” Bean 上的任何匹配(例如,根)都更可取。 + +最后但并非最不重要的一点是,`@ExceptionHandler`方法实现可以通过以其原始形式重抛给定的异常实例来选择退出处理该异常实例。这在只对根级别匹配感兴趣或对无法静态确定的特定上下文中的匹配感兴趣的场景中很有用。一个重新抛出的异常将通过剩余的解析链传播,就好像给定的`@ExceptionHandler`方法首先不会匹配一样。 + +Spring MVC 中对`@ExceptionHandler`方法的支持是建立在`DispatcherServlet`级别的[HandleRexCeptionResolver](#mvc-exceptionhandlers)机制上的。 + +##### 方法参数 + +`@ExceptionHandler`方法支持以下参数: + +| Method argument |说明| +|----------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Exception type |访问提出的异常。| +| `HandlerMethod` |用于访问引发异常的控制器方法。| +| `WebRequest`, `NativeWebRequest` |在不直接使用<br/> Servlet API 的情况下,对请求参数以及请求和会话属性进行通用访问。| +| `javax.servlet.ServletRequest`, `javax.servlet.ServletResponse` |选择任何特定的请求或响应类型(例如,`ServletRequest`或 `HttpServletRequest’或 Spring 的`MultipartRequest`或`MultipartHttpServletRequest`)。| +| `javax.servlet.http.HttpSession` |强制执行会话的存在。因此,这样的参数永远不是`null`。<br/>注意,会话访问不是线程安全的。如果允许多个<br/>请求同时访问会话,请考虑将 `requestmappinghandleradapter’实例的`synchronizeOnSession`标志设置为`true`。| +| `java.security.Principal` |当前经过身份验证的用户——如果已知的话,可能是特定的`Principal`实现类。| +| `HttpMethod` |请求的 HTTP 方法。| +| `java.util.Locale` |当前的请求区域设置,由可用的最特定的`LocaleResolver`确定—在<br/>效果中,配置的`LocaleResolver`或`LocaleContextResolver`。| +| `java.util.TimeZone`, `java.time.ZoneId` |与当前请求相关联的时区,由`LocaleContextResolver`确定。| +| `java.io.OutputStream`, `java.io.Writer` |用于访问原始响应体,如 Servlet API 所公开的那样。| +|`java.util.Map`, `org.springframework.ui.Model`, `org.springframework.ui.ModelMap`|用于访问模型以获得错误响应。总是空的。| +| `RedirectAttributes` |指定在重定向的情况下使用的属性—(即附加到查询<br/>字符串中)和要临时存储的 flash 属性,直到重定向后的请求为止。<br/>参见[重定向属性](#mvc-redirecting-passing-data)和[flash 属性](#mvc-flash-attributes)。| +| `@SessionAttribute` |对于任何会话属性的访问,与存储在<br/>会话中的模型属性形成对比的是,由于 class-level`@SessionAttributes`声明。<br/>参见[@sessionAttribute](#mvc-ann-sessionattribute)获取更多详细信息。| +| `@RequestAttribute` |用于访问请求属性。有关更多详细信息,请参见[@requestAttribute](#mvc-ann-requestattrib)。| + +##### 返回值 + +`@ExceptionHandler`方法支持以下返回值: + +| Return value |说明| +|-----------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `@ResponseBody` |返回值通过`HttpMessageConverter`实例进行转换,并写入<br/>响应。见[`@ResponseBody`](#mvc-ann-responsebody)。| +| `HttpEntity<B>`, `ResponseEntity<B>` |返回值指定通过`HttpMessageConverter`实例转换完整的响应(包括 HTTP 头和正文)<br/>并将其写入响应。<br/>参见[ResponseEntity](#mvc-ann-responseentity)。| +| `String` |要用`ViewResolver`实现来解析的视图名称,并与<br/>隐式模型一起使用——通过命令对象和`@ModelAttribute`方法确定。<br/>处理程序方法还可以通过声明`Model`参数(前面已经描述过)以编程方式丰富模型。| +| `View` |一个`View`实例用于与隐式模型一起进行渲染——通过命令对象和`@ModelAttribute`方法确定<br/>。处理程序方法还可以通过声明一个`Model`参数(前面已经描述过)以编程方式丰富模型。| +|`java.util.Map`, `org.springframework.ui.Model`|要添加到隐式模型的属性,其视图名称通过`RequestToViewNameTranslator`隐式确定<br/>。| +| `@ModelAttribute` |通过<br/>a`RequestToViewNameTranslator`隐式确定的视图名称要添加到模型中的属性。<br/><br/>注意,`@ModelAttribute`是可选的。请参阅<br/>本表末尾的“任何其他返回值”。| +| `ModelAndView` object |要使用的视图和模型属性,以及可选的响应状态。| +| `void` |具有`void`返回类型(或`null`返回值)的方法被认为具有完全的<br/>处理响应,如果它还具有`ServletResponse``OutputStream`参数,或<br/>`@ResponseStatus`注释。如果控制器进行了正的 `ETag’或`lastModified`时间戳检查(详见[Controllers](#mvc-caching-etag-lastmodified)),也是如此。<br/><br/>如果上述各项都不是真的,`void`返回类型还可以表示<br/>REST 控制器的“无响应主体”或 HTML 控制器的默认视图名称选择。| +| Any other return value |如果一个返回值与上述任一项不匹配且不是简单类型(由[Beanutils#IsSimpleProperty](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-)确定),则默认情况下<br/>将其视为要添加到模型中的模型属性。如果它是一个简单的类型,<br/>它仍然是未解决的。| + +##### REST API 异常 + +[WebFlux](web-reactive.html#webflux-ann-rest-exceptions) + +REST 服务的一个常见要求是在响应主体中包含错误详细信息。 Spring 框架不会自动做到这一点,因为响应主体中的错误详细信息的表示是特定于应用程序的。但是,`@restcontroller` 可以使用`@ExceptionHandler`方法和`ResponseEntity`返回值来设置响应的状态和主体。这样的方法也可以在`@ControllerAdvice`类中声明,以在全局范围内应用它们。 + +在响应体中实现带有错误详细信息的全局异常处理的应用程序应该考虑扩展[“ResponseentyExceptionHandler”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.html),它为 Spring MVC 引发的异常提供处理,并提供钩子来定制响应体。要利用这一点,创建一个“responseEntyExceptionHandler”的子类,用`@ControllerAdvice`对其进行注释,覆盖必要的方法,并将其声明为 Spring Bean。 + +#### 1.3.7.财务总监建议 + +[WebFlux](web-reactive.html#webflux-ann-controller-advice) + +`@ExceptionHandler`、`@InitBinder`和`@ModelAttribute`方法仅适用于在其中声明它们的 `@controller’类或类层次结构。如果它们是在`@ControllerAdvice`或`@RestControllerAdvice`类中声明的,则它们适用于任何控制器。此外,从 5.3 开始,`@ExceptionHandler`中的`@ControllerAdvice`方法可以用来处理来自任何`@Controller`或任何其他处理程序的异常。 + +`@ControllerAdvice`是用`@Component`注释的,因此可以通过[组件扫描](core.html#beans-java-instantiating-container-scan)注册为 Spring Bean。`@RestControllerAdvice`是用`@ControllerAdvice`和`@ResponseBody`进行元注释的,这意味着`@ExceptionHandler`方法将通过响应体消息转换而不是通过 HTML 视图来呈现其返回值。 + +在启动时,`RequestMappingHandlerMapping`和`ExceptionHandlerExceptionResolver`检测控制器建议 bean 并在运行时应用它们。全局`@ExceptionHandler`方法,来自于`@ControllerAdvice`,应用于*之后*局部方法,来自于`@Controller`。相比之下,全局`@ModelAttribute`和`@InitBinder`方法则应用于局部方法*在此之前*。 + +`@ControllerAdvice`注释的属性允许你缩小它们所应用的控制器和处理程序的集合。例如: + +爪哇 + +``` +// Target all Controllers annotated with @RestController +@ControllerAdvice(annotations = RestController.class) +public class ExampleAdvice1 {} + +// Target all Controllers within specific packages +@ControllerAdvice("org.example.controllers") +public class ExampleAdvice2 {} + +// Target all Controllers assignable to specific classes +@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class}) +public class ExampleAdvice3 {} +``` + +Kotlin + +``` +// Target all Controllers annotated with @RestController +@ControllerAdvice(annotations = [RestController::class]) +class ExampleAdvice1 + +// Target all Controllers within specific packages +@ControllerAdvice("org.example.controllers") +class ExampleAdvice2 + +// Target all Controllers assignable to specific classes +@ControllerAdvice(assignableTypes = [ControllerInterface::class, AbstractController::class]) +class ExampleAdvice3 +``` + +前面示例中的选择器是在运行时进行评估的,如果广泛使用,可能会对性能产生负面影响。有关更多详细信息,请参见[@controlleradvice](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/bind/annotation/ControllerAdvice.html)爪哇doc。 + +### 1.4.功能端点 + +[WebFlux](web-reactive.html#webflux-fn) + +Spring Web MVC 包括 WebMVC.FN,这是一种轻量级的函数编程模型,在该模型中,函数被用于路由和处理请求,并且契约被设计为具有不可变性。它是基于注释的编程模型的一种替代方案,但在其他情况下运行在相同的[DispatcherServlet](#mvc-servlet)上。 + +#### 1.4.1.概述 + +[WebFlux](web-reactive.html#webflux-fn-overview) + +在 Webmvc.FN 中,HTTP 请求是用`HandlerFunction`处理的:这个函数接受 `serverrequest’并返回`ServerResponse`。请求和响应对象都具有不可更改的契约,这些契约提供了对 HTTP 请求和响应的 JDK8 友好访问。“HandlerFunction”相当于基于注释的编程模型中的`@RequestMapping`方法的主体。 + +传入的请求被路由到带有`RouterFunction`的处理程序函数:该函数接受`ServerRequest`并返回可选的`HandlerFunction`(即`Optional<HandlerFunction>`)。当路由器函数匹配时,将返回一个处理程序函数;否则将返回一个空的可选函数。“Routerfunction”相当于`@RequestMapping`注释,但它的主要区别在于,路由器函数不仅提供数据,还提供行为。 + +`RouterFunctions.route()`提供了一个路由器构建器,可以促进路由器的创建,如下例所示: + +爪哇 + +``` +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) { + // ... + } +} +``` + +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**|使用路由器 DSL 创建路由器。| +|-----|-----------------------------------| + +如果你将`RouterFunction`注册为 Bean,例如,通过在 `@configuration’类中公开它,它将被 Servlet 自动检测,如[运行服务器](#webmvc-fn-running)中所解释的那样。 + +#### 1.4.2.handlerfunction + +[WebFlux](web-reactive.html#webflux-fn-handler-functions) + +`ServerRequest`和`ServerResponse`是不可变的接口,它们提供对 HTTP 请求和响应的 JDK8 友好访问,包括头、主体、方法和状态代码。 + +##### ServerRequest + +`ServerRequest`提供对 HTTP 方法、URI、标头和查询参数的访问,而对正文的访问是通过`body`方法提供的。 + +下面的示例将请求主体提取到`String`: + +爪哇 + +``` +String string = request.body(String.class); +``` + +Kotlin + +``` +val string = request.body<String>() +``` + +下面的示例将主体提取到`List<Person>`,其中`Person`对象是从序列化形式(例如 JSON 或 XML)中解码的: + +爪哇 + +``` +List<Person> people = request.body(new ParameterizedTypeReference<List<Person>>() {}); +``` + +Kotlin + +``` +val people = request.body<Person>() +``` + +下面的示例展示了如何访问参数: + +爪哇 + +``` +MultiValueMap<String, String> params = request.params(); +``` + +Kotlin + +``` +val map = request.params() +``` + +##### ServerResponse + +`ServerResponse`提供对 HTTP 响应的访问,由于它是不可变的,你可以使用`build`方法来创建它。你可以使用构建器设置响应状态、添加响应头或提供主体。下面的示例使用 JSON 内容创建一个 200(OK)响应: + +爪哇 + +``` +Person person = ... +ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person); +``` + +Kotlin + +``` +val person: Person = ... +ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person) +``` + +下面的示例展示了如何使用`Location`标头构建 201(已创建)响应,而不包含正文: + +爪哇 + +``` +URI location = ... +ServerResponse.created(location).build(); +``` + +Kotlin + +``` +val location: URI = ... +ServerResponse.created(location).build() +``` + +还可以使用异步结果作为主体,其形式为`CompletableFuture`、`publisher’或`ReactiveAdapterRegistry`支持的任何其他类型。例如: + +爪哇 + +``` +Mono<Person> person = webClient.get().retrieve().bodyToMono(Person.class); +ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person); +``` + +Kotlin + +``` +val person = webClient.get().retrieve().awaitBody<Person>() +ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person) +``` + +如果不只是主体,而且状态或头也是基于异步类型的,则可以在`ServerResponse`上使用静态`async`方法,该方法接受`CompletableFuture<ServerResponse>`、`Publisher<ServerResponse>`或`ReactiveAdapterRegistry`支持的任何其他异步类型。例如: + +爪哇 + +``` +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/)可以通过`ServerResponse`上的静态`sse`方法提供。该方法提供的构建器允许你以 JSON 的形式发送字符串或其他对象。例如: + +爪哇 + +``` +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(); +``` + +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() +``` + +##### 处理程序类 + +我们可以将处理程序函数编写为 lambda,如下例所示: + +爪哇 + +``` +HandlerFunction<ServerResponse> helloWorld = + request -> ServerResponse.ok().body("Hello World"); +``` + +Kotlin + +``` +val helloWorld: (ServerRequest) -> ServerResponse = + { ServerResponse.ok().body("Hello World") } +``` + +这很方便,但在一个应用程序中,我们需要多个功能,而多个内联 lambda 可能会变得混乱。因此,将相关的处理程序函数组合成一个处理程序类是有用的,该处理程序类在基于注释的应用程序中具有与`@Controller`类似的作用。例如,下面的类公开了一个反应性`Person`存储库: + +爪哇 + +``` +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`是一个处理函数,它将存储库中找到的所有`Person`对象作为<br/>json 返回。| +|-----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|`createPerson`是一个处理函数,它存储了一个包含在请求主体中的新`Person`。| +|**3**|`getPerson`是一个处理函数,它返回一个人,由`id`路径<br/>变量标识。我们从存储库中检索`Person`并创建一个 JSON 响应,如果找到了<br/>。如果没有找到它,我们将返回 404Not Found 响应。| + +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`是一个处理函数,它将存储库中找到的所有`Person`对象作为<br/>json 返回。| +|-----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|`createPerson`是一个处理函数,它存储了一个包含在请求主体中的新`Person`。| +|**3**|`getPerson`是一个处理函数,它返回一个人,由`id`路径<br/>变量标识。我们从存储库中检索`Person`并创建一个 JSON 响应,如果找到了<br/>。如果没有找到它,我们将返回 404Not Found 响应。| + +##### Validation + +功能端点可以使用 Spring 的[验证设施](core.html#validation)将验证应用于请求主体。例如,给定一个针对`Person`的自定义 Spring [Validator](core.html#validation)实现: + +爪哇 + +``` +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**|创建`Validator`实例。| +|-----|-----------------------------------| +|**2**|应用验证。| +|**3**|提出 400 响应的例外情况。| + +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**|创建`Validator`实例。| +|-----|-----------------------------------| +|**2**|应用验证。| +|**3**|提出 400 响应的例外情况。| + +处理程序还可以通过基于`LocalValidatorFactoryBean`创建和注入一个全局`Validator`实例来使用标准 Bean 验证 API(JSR-303)。见[Spring Validation](core.html#validation-beanvalidation)。 + +#### 1.4.3.`RouterFunction` + +[WebFlux](web-reactive.html#webflux-fn-router-functions) + +路由器函数用于将请求路由到相应的`HandlerFunction`。通常,你不会自己编写路由器函数,而是使用“RouterFunctions”实用程序类上的一种方法来创建一个。“RouterFunctions.Route()”(无参数)为你提供了一个用于创建路由器函数的 Fluent 构建器,而`RouterFunctions.route(RequestPredicate, HandlerFunction)`提供了一种直接创建路由器的方法。 + +通常,建议使用`route()`Builder,因为它为典型的映射场景提供了方便的快捷方式,而不需要很难发现的静态导入。例如,Router Function Builder 提供了方法`GET(String, HandlerFunction)`来创建 GET 请求的映射;以及`POST(String, HandlerFunction)`用于 POST。 + +除了基于 HTTP 方法的映射,Route Builder 还提供了一种在映射到请求时引入额外谓词的方法。对于每个 HTTP 方法,都有一个重载变量,它将`RequestPredicate`作为参数,通过该参数可以表示额外的约束。 + +##### 谓词 + +你可以编写自己的`RequestPredicate`,但是`RequestPredicates`实用程序类提供了基于请求路径、HTTP 方法、Content-type 等的常用实现。下面的示例使用一个请求谓词来基于`Accept`头创建一个约束: + +爪哇 + +``` +RouterFunction<ServerResponse> route = RouterFunctions.route() + .GET("/hello-world", accept(MediaType.TEXT_PLAIN), + request -> ServerResponse.ok().body("Hello World")).build(); +``` + +Kotlin + +``` +import org.springframework.web.servlet.function.router + +val route = router { + GET("/hello-world", accept(TEXT_PLAIN)) { + ServerResponse.ok().body("Hello World") + } +} +``` + +你可以使用以下方法将多个请求谓词组合在一起: + +* `RequestPredicate.and(RequestPredicate)`—两者必须匹配。 + +* `RequestPredicate.or(RequestPredicate)`—两者都可以匹配。 + +来自`RequestPredicates`的许多谓词都是组成的。例如,`RequestPredicates.GET(String)`是由`RequestPredicates.method(HttpMethod)`和`RequestPredicates.path(String)`组成的。上面显示的示例还使用了两个请求谓词,因为构建器在内部使用 `requestPredicates.Get’,并用`accept`谓词将其组合起来。 + +##### 路线 + +对路由器的功能按顺序进行评估:如果第一条路由不匹配,则对第二条路由进行评估,依此类推。因此,在一般路线之前声明更具体的路线是有意义的。当将路由器功能注册为 Spring bean 时,这一点也很重要,将在后面进行说明。请注意,这种行为与基于注释的编程模型不同,在该模型中,“最特定的”控制器方法是自动选择的。 + +当使用 Router Function Builder 时,所有定义的路由都被组合成一个“routerfunction”,该“routerfunction”从`build()`返回。还有其他方法可以将多个路由器功能组合在一起: + +* `add(RouterFunction)`上的`RouterFunctions.route()`构建器 + +* `RouterFunction.and(RouterFunction)` + +* `RouterFunction.andRoute(RequestPredicate, HandlerFunction)`—带有嵌套`RouterFunctions.route()`的 `routerfunction.and()’的快捷方式。 + +下面的示例显示了四条路线的组成: + +爪哇 + +``` +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**|带有与 JSON 匹配的`GET /person/{id}`头的`Accept`被路由到 `personhandler.getperson’| +|-----|--------------------------------------------------------------------------------------------------| +|**2**|带有与 JSON 匹配的`GET /person`头的`Accept`被路由到 `personhandler.listpeople’| +|**3**|没有附加谓词的`POST /person`映射到 `personhandler.createPerson’,并且| +|**4**|`otherRoute`是在其他地方创建的路由器功能,并将其添加到所构建的路由中。| + +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**|带有与 JSON 匹配的`GET /person/{id}`头的`Accept`被路由到 `personhandler.getperson’| +|-----|--------------------------------------------------------------------------------------------------| +|**2**|带有与 JSON 匹配的`GET /person`标头的`Accept`被路由到 `personhandler.listpeople’| +|**3**|没有附加谓词的`POST /person`映射到 `personhandler.createPerson’,和| +|**4**|`otherRoute`是在其他地方创建的路由器功能,并将其添加到所构建的路由中。| + +##### 嵌套路线 + +一组路由器函数通常有一个共享谓词,例如共享路径。在上面的示例中,共享谓词将是一个匹配`/person`的路径谓词,由三个路由使用。在使用注释时,可以使用映射到`/person`的类型级`@RequestMapping`注释来删除这种重复。在 WebMVC.FN 中,路径谓词可以通过 Router Function Builder 上的`path`方法进行共享。例如,通过使用嵌套路由,可以通过以下方式改进上面示例的最后几行: + +爪哇 + +``` +RouterFunction<ServerResponse> route = route() + .path("/person", builder -> builder (1) + .GET("/{id}", accept(APPLICATION_JSON), handler::getPerson) + .GET(accept(APPLICATION_JSON), handler::listPeople) + .POST("/person", handler::createPerson)) + .build(); +``` + +|**1**|请注意,`path`的第二个参数是接受路由器生成器的消费者。| +|-----|---------------------------------------------------------------------------------| + +Kotlin + +``` +import org.springframework.web.servlet.function.router + +val route = router { + "/person".nest { + GET("/{id}", accept(APPLICATION_JSON), handler::getPerson) + GET(accept(APPLICATION_JSON), handler::listPeople) + POST("/person", handler::createPerson) + } +} +``` + +尽管基于路径的嵌套是最常见的,但你可以在构建器上使用`nest`方法在任何类型的谓词上进行嵌套。上面仍然包含一些以共享`Accept`-header 谓词形式出现的重复。通过使用`nest`方法和`accept`方法,我们可以进一步改进: + +爪哇 + +``` +RouterFunction<ServerResponse> route = route() + .path("/person", b1 -> b1 + .nest(accept(APPLICATION_JSON), b2 -> b2 + .GET("/{id}", handler::getPerson) + .GET(handler::listPeople)) + .POST("/person", handler::createPerson)) + .build(); +``` + +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("/person", handler::createPerson) + } + } +} +``` + +#### 1.4.4.运行服务器 + +[WebFlux](web-reactive.html#webflux-fn-running) + +你通常通过[MVC Config](#mvc-config)在基于[DispatcherHandler’](#mvc-servlet)的设置中运行路由器函数,该设置使用 Spring 配置来声明处理请求所需的组件。MVC 爪哇 配置声明以下基础设施组件以支持功能端点: + +* `RouterFunctionMapping`:在 Spring 配置中检测一个或多个`RouterFunction<?>`bean,[orders them](core.html#beans-factory-ordered),通过 `routerfunction.andother’将它们组合,并将请求路由到结果组合的`RouterFunction`。 + +* `HandlerFunctionAdapter`:允许`DispatcherHandler`调用映射到请求的`HandlerFunction`的简单适配器。 + +前面的组件让功能端点适合`DispatcherServlet`请求处理生命周期,并且(可能)与带注释的控制器(如果声明了任何控制器的话)并排运行。这也是 Spring boot Web starter 启用功能端点的方式。 + +下面的示例展示了一个 WebFlux 爪哇 配置: + +爪哇 + +``` +@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... + } +} +``` + +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... + } +} +``` + +#### 1.4.5.过滤处理程序函数 + +[WebFlux](web-reactive.html#webflux-fn-handler-filter-function) + +你可以使用路由函数生成器上的`before`、`after`或`filter`方法来过滤处理程序函数。对于注释,你可以通过使用`@ControllerAdvice`、`ServletFilter`或同时使用这两种方法来实现类似的功能。筛选器将应用于由构建器构建的所有路由。这意味着嵌套路由中定义的筛选器不适用于“顶层”路由。例如,考虑以下示例: + +爪哇 + +``` +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("/person", handler::createPerson)) + .after((request, response) -> logResponse(response)) (2) + .build(); +``` + +|**1**|添加自定义请求头的`before`过滤器仅应用于两个 GET 路由。| +|-----|----------------------------------------------------------------------------------------------| +|**2**|记录响应的`after`过滤器应用于所有路由,包括嵌套的路由。| + +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("/person", handler::createPerson) + after { _, response -> (2) + logResponse(response) + } +} +``` + +|**1**|添加自定义请求头的`before`过滤器仅应用于两个 GET 路由。| +|-----|----------------------------------------------------------------------------------------------| +|**2**|记录响应的`after`过滤器应用于所有路由,包括嵌套的路由。| + +路由器构建器上的`filter`方法接受`HandlerFilterFunction`:一个函数接受`ServerRequest`和`HandlerFunction`并返回`ServerResponse`。处理程序函数参数表示链中的下一个元素。这通常是路由到的处理程序,但是如果应用了多个,它也可以是另一个过滤器。 + +现在,我们可以在路由中添加一个简单的安全过滤器,假设我们有一个`SecurityManager`,它可以确定是否允许特定的路径。下面的示例展示了如何做到这一点: + +爪哇 + +``` +SecurityManager securityManager = ... + +RouterFunction<ServerResponse> route = route() + .path("/person", b1 -> b1 + .nest(accept(APPLICATION_JSON), b2 -> b2 + .GET("/{id}", handler::getPerson) + .GET(handler::listPeople)) + .POST("/person", handler::createPerson)) + .filter((request, next) -> { + if (securityManager.allowAccessTo(request.path())) { + return next.handle(request); + } + else { + return ServerResponse.status(UNAUTHORIZED).build(); + } + }) + .build(); +``` + +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("/person", handler::createPerson) + filter { request, next -> + if (securityManager.allowAccessTo(request.path())) { + next(request) + } + else { + status(UNAUTHORIZED).build(); + } + } + } +} +``` + +前面的示例演示了调用`next.handle(ServerRequest)`是可选的。我们只允许在允许访问的情况下运行处理程序函数。 + +除了在路由器功能构建器上使用`filter`方法外,还可以通过`RouterFunction.filter(HandlerFilterFunction)`对现有的路由器功能应用过滤器。 + +| |CORS 对功能端点的支持是通过专用的[`CorsFilter`](webmvc-cors.html#mvc-cors-filter)提供的。| +|---|----------------------------------------------------------------------------------------------------------------------| + +### 1.5.URI 链接 + +[WebFlux](web-reactive.html#webflux-uri-building) + +这一节描述了 Spring 框架中可用来处理 URI 的各种选项。 + +#### 1.5.1.尿酸成分 + +Spring MVC 和 Spring WebFlux + +`UriComponentsBuilder`有助于从具有变量的 URI 模板构建 URI,如下例所示: + +爪哇 + +``` +UriComponents uriComponents = UriComponentsBuilder + .fromUriString("https://example.com/hotels/{hotel}") (1) + .queryParam("q", "{q}") (2) + .encode() (3) + .build(); (4) + +URI uri = uriComponents.expand("Westin", "123").toUri(); (5) +``` + +|**1**|带有 URI 模板的静态工厂方法。| +|-----|-----------------------------------------------------------| +|**2**|添加或替换 URI 组件。| +|**3**|请求对 URI 模板和 URI 变量进行编码。| +|**4**|构建`UriComponents`。| +|**5**|展开变量并获得`URI`。| + +Kotlin + +``` +val uriComponents = UriComponentsBuilder + .fromUriString("https://example.com/hotels/{hotel}") (1) + .queryParam("q", "{q}") (2) + .encode() (3) + .build() (4) + +val uri = uriComponents.expand("Westin", "123").toUri() (5) +``` + +|**1**|带有 URI 模板的静态工厂方法。| +|-----|-----------------------------------------------------------| +|**2**|添加或替换 URI 组件。| +|**3**|请求对 URI 模板和 URI 变量进行编码。| +|**4**|构建`UriComponents`。| +|**5**|展开变量并获得`URI`。| + +前面的示例可以合并为一个链,并用`buildAndExpand`将其缩短,如下例所示: + +爪哇 + +``` +URI uri = UriComponentsBuilder + .fromUriString("https://example.com/hotels/{hotel}") + .queryParam("q", "{q}") + .encode() + .buildAndExpand("Westin", "123") + .toUri(); +``` + +Kotlin + +``` +val uri = UriComponentsBuilder + .fromUriString("https://example.com/hotels/{hotel}") + .queryParam("q", "{q}") + .encode() + .buildAndExpand("Westin", "123") + .toUri() +``` + +你可以通过直接访问一个 URI(这意味着编码)来进一步缩短它,如下例所示: + +爪哇 + +``` +URI uri = UriComponentsBuilder + .fromUriString("https://example.com/hotels/{hotel}") + .queryParam("q", "{q}") + .build("Westin", "123"); +``` + +Kotlin + +``` +val uri = UriComponentsBuilder + .fromUriString("https://example.com/hotels/{hotel}") + .queryParam("q", "{q}") + .build("Westin", "123") +``` + +可以使用完整的 URI 模板进一步缩短它,如下例所示: + +爪哇 + +``` +URI uri = UriComponentsBuilder + .fromUriString("https://example.com/hotels/{hotel}?q={q}") + .build("Westin", "123"); +``` + +Kotlin + +``` +val uri = UriComponentsBuilder + .fromUriString("https://example.com/hotels/{hotel}?q={q}") + .build("Westin", "123") +``` + +#### 1.5.2.UriBuilder + +Spring MVC 和 Spring WebFlux + +[“uricomponentsbuilder”](#web-uricomponents)实现`UriBuilder`。你可以创建一个“uribuilder”,然后使用`UriBuilderFactory`。同时,`UriBuilderFactory`和 `uribuilder’提供了一种基于共享配置(例如基本 URL、编码首选项和其他详细信息)的可插入机制,用于从 URI 模板构建 URI。 + +你可以使用`UriBuilderFactory`配置`RestTemplate`和`WebClient`来定制 URI 的准备。`DefaultUriBuilderFactory`是`UriBuilderFactory`的默认实现,它在内部使用`UriComponentsBuilder`并公开共享配置选项。 + +下面的示例展示了如何配置`RestTemplate`: + +爪哇 + +``` +// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode; + +String baseUrl = "https://example.org"; +DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl); +factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES); + +RestTemplate restTemplate = new RestTemplate(); +restTemplate.setUriTemplateHandler(factory); +``` + +Kotlin + +``` +// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode + +val baseUrl = "https://example.org" +val factory = DefaultUriBuilderFactory(baseUrl) +factory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES + +val restTemplate = RestTemplate() +restTemplate.uriTemplateHandler = factory +``` + +下面的示例配置`WebClient`: + +爪哇 + +``` +// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode; + +String baseUrl = "https://example.org"; +DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl); +factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES); + +WebClient client = WebClient.builder().uriBuilderFactory(factory).build(); +``` + +Kotlin + +``` +// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode + +val baseUrl = "https://example.org" +val factory = DefaultUriBuilderFactory(baseUrl) +factory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES + +val client = WebClient.builder().uriBuilderFactory(factory).build() +``` + +此外,还可以直接使用`DefaultUriBuilderFactory`。它类似于使用“uriComponentsBuilder”,但它不是静态的工厂方法,而是保存配置和首选项的实际实例,如下例所示: + +爪哇 + +``` +String baseUrl = "https://example.com"; +DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl); + +URI uri = uriBuilderFactory.uriString("/hotels/{hotel}") + .queryParam("q", "{q}") + .build("Westin", "123"); +``` + +Kotlin + +``` +val baseUrl = "https://example.com" +val uriBuilderFactory = DefaultUriBuilderFactory(baseUrl) + +val uri = uriBuilderFactory.uriString("/hotels/{hotel}") + .queryParam("q", "{q}") + .build("Westin", "123") +``` + +#### 1.5.3.URI 编码 + +Spring MVC 和 Spring WebFlux + +`UriComponentsBuilder`在两个级别上公开编码选项: + +* [uricomponentsbuilder#encode()](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/util/UriComponentsBuilder.html#encode--):先对 URI 模板进行预编码,然后在展开时对 URI 变量进行严格编码。 + +* [uricomponents#encode()](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/util/UriComponents.html#encode--):编码 URI 组件*之后*URI 变量被展开。 + +这两个选项都用转义的八进制替换非 ASCII 和非法字符。然而,第一个选项也用 URI 变量中出现的保留意义替换字符。 + +| |考虑一下“;”,它在某种程度上是合法的,但具有保留的含义。第一个选项在 URI 变量中用“%3b”替换<br/>;;",但不在 URI 模板中。相比之下,第二个选项永远不会<br/>取代“;”,因为它是路径中的法律字符。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在大多数情况下,第一个选项可能会给出预期的结果,因为它将 URI 变量视为不透明的数据来进行完全编码,而如果 URI 变量故意包含保留字符,则第二个选项是有用的。当完全不展开 URI 变量时,第二个选项也很有用,因为这也会对任何看起来像 URI 变量的内容进行编码。 + +下面的示例使用了第一个选项: + +爪哇 + +``` +URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}") + .queryParam("q", "{q}") + .encode() + .buildAndExpand("New York", "foo+bar") + .toUri(); + +// Result is "/hotel%20list/New%20York?q=foo%2Bbar" +``` + +Kotlin + +``` +val uri = UriComponentsBuilder.fromPath("/hotel list/{city}") + .queryParam("q", "{q}") + .encode() + .buildAndExpand("New York", "foo+bar") + .toUri() + +// Result is "/hotel%20list/New%20York?q=foo%2Bbar" +``` + +可以通过直接访问 URI(这意味着编码)来缩短前面的示例,如下例所示: + +爪哇 + +``` +URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}") + .queryParam("q", "{q}") + .build("New York", "foo+bar"); +``` + +Kotlin + +``` +val uri = UriComponentsBuilder.fromPath("/hotel list/{city}") + .queryParam("q", "{q}") + .build("New York", "foo+bar") +``` + +可以使用完整的 URI 模板进一步缩短它,如下例所示: + +Java + +``` +URI uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}") + .build("New York", "foo+bar"); +``` + +Kotlin + +``` +val uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}") + .build("New York", "foo+bar") +``` + +`WebClient`和`RestTemplate`通过`UriBuilderFactory`策略在内部扩展和编码 URI 模板。两者都可以使用自定义策略进行配置,如下例所示: + +Java + +``` +String baseUrl = "https://example.com"; +DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl) +factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES); + +// Customize the RestTemplate.. +RestTemplate restTemplate = new RestTemplate(); +restTemplate.setUriTemplateHandler(factory); + +// Customize the WebClient.. +WebClient client = WebClient.builder().uriBuilderFactory(factory).build(); +``` + +Kotlin + +``` +val baseUrl = "https://example.com" +val factory = DefaultUriBuilderFactory(baseUrl).apply { + encodingMode = EncodingMode.TEMPLATE_AND_VALUES +} + +// Customize the RestTemplate.. +val restTemplate = RestTemplate().apply { + uriTemplateHandler = factory +} + +// Customize the WebClient.. +val client = WebClient.builder().uriBuilderFactory(factory).build() +``` + +`DefaultUriBuilderFactory`实现在内部使用`UriComponentsBuilder`来扩展和编码 URI 模板。作为一个工厂,它提供了一个单独的位置来配置编码方法,该方法基于以下编码模式之一: + +* `TEMPLATE_AND_VALUES`:使用`UriComponentsBuilder#encode()`,对应于前面列表中的第一个选项,对 URI 模板进行预编码,并在展开时对 URI 变量进行严格编码。 + +* `VALUES_ONLY`:不对 URI 模板进行编码,而是在将 URI 变量扩展到模板之前,通过`UriUtils#encodeUriVariables`对 URI 变量进行严格编码。 + +* `URI_COMPONENT`:使用`UriComponents#encode()`,对应于前面列表中的第二个选项,来对 URI 组件值的编码*之后*URI 变量进行展开。 + +* `NONE`:不应用编码。 + +由于历史原因和向后兼容性,`RestTemplate`被设置为`EncodingMode.URI_COMPONENT`。`WebClient`依赖于`DefaultUriBuilderFactory`中的默认值,该默认值从 5.0.x 中的`EncodingMode.URI_COMPONENT`更改为 5.1 中的`EncodingMode.TEMPLATE_AND_VALUES`。 + +#### 1.5.4.相对的 Servlet 请求 + +可以使用`ServletUriComponentsBuilder`创建相对于当前请求的 URI,如下例所示: + +Java + +``` +HttpServletRequest request = ... + +// Re-uses scheme, host, port, path, and query string... + +URI uri = ServletUriComponentsBuilder.fromRequest(request) + .replaceQueryParam("accountId", "{id}") + .build("123"); +``` + +Kotlin + +``` +val request: HttpServletRequest = ... + +// Re-uses scheme, host, port, path, and query string... + +val uri = ServletUriComponentsBuilder.fromRequest(request) + .replaceQueryParam("accountId", "{id}") + .build("123") +``` + +你可以创建相对于上下文路径的 URI,如下例所示: + +Java + +``` +HttpServletRequest request = ... + +// Re-uses scheme, host, port, and context path... + +URI uri = ServletUriComponentsBuilder.fromContextPath(request) + .path("/accounts") + .build() + .toUri(); +``` + +Kotlin + +``` +val request: HttpServletRequest = ... + +// Re-uses scheme, host, port, and context path... + +val uri = ServletUriComponentsBuilder.fromContextPath(request) + .path("/accounts") + .build() + .toUri() +``` + +你可以创建相对于 Servlet 的 URI(例如,`/main/*`),如下例所示: + +Java + +``` +HttpServletRequest request = ... + +// Re-uses scheme, host, port, context path, and Servlet mapping prefix... + +URI uri = ServletUriComponentsBuilder.fromServletMapping(request) + .path("/accounts") + .build() + .toUri(); +``` + +Kotlin + +``` +val request: HttpServletRequest = ... + +// Re-uses scheme, host, port, context path, and Servlet mapping prefix... + +val uri = ServletUriComponentsBuilder.fromServletMapping(request) + .path("/accounts") + .build() + .toUri() +``` + +| |截至 5.1,`ServletUriComponentsBuilder`忽略来自`Forwarded`和 `x-forward-*’头的信息,这些头指定了源自客户端的地址。考虑使用[“ForwardedHeaderFilter”](#filters-forwarded-headers)来提取和使用或丢弃<br/>这样的标题。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.5.5.与控制器的链接 + +Spring MVC 提供了一种用于准备连接到控制器的机制的方法。例如,下面的 MVC 控制器允许创建链接: + +Java + +``` +@Controller +@RequestMapping("/hotels/{hotel}") +public class BookingController { + + @GetMapping("/bookings/{booking}") + public ModelAndView getBooking(@PathVariable Long booking) { + // ... + } +} +``` + +Kotlin + +``` +@Controller +@RequestMapping("/hotels/{hotel}") +class BookingController { + + @GetMapping("/bookings/{booking}") + fun getBooking(@PathVariable booking: Long): ModelAndView { + // ... + } +} +``` + +你可以通过按名称引用方法来准备链接,如下例所示: + +爪哇 + +``` +UriComponents uriComponents = MvcUriComponentsBuilder + .fromMethodName(BookingController.class, "getBooking", 21).buildAndExpand(42); + +URI uri = uriComponents.encode().toUri(); +``` + +Kotlin + +``` +val uriComponents = MvcUriComponentsBuilder + .fromMethodName(BookingController::class.java, "getBooking", 21).buildAndExpand(42) + +val uri = uriComponents.encode().toUri() +``` + +在前面的示例中,我们提供了实际的方法参数值(在本例中,长值:`21`),将其用作路径变量并插入到 URL 中。此外,我们提供了值`42`来填充任何剩余的 URI 变量,例如从类型级请求映射继承的`hotel`变量。如果该方法有更多的参数,我们可以为 URL 不需要的参数提供 NULL。通常,只有`@PathVariable`和`@RequestParam`参数与构造 URL 有关。 + +还有其他使用`MvcUriComponentsBuilder`的方法。例如,你可以使用一种类似于通过代理模拟测试的技术,以避免按名称引用控制器方法,如下例所示(该示例假定静态导入`MvcUriComponentsBuilder.on`): + +爪哇 + +``` +UriComponents uriComponents = MvcUriComponentsBuilder + .fromMethodCall(on(BookingController.class).getBooking(21)).buildAndExpand(42); + +URI uri = uriComponents.encode().toUri(); +``` + +Kotlin + +``` +val uriComponents = MvcUriComponentsBuilder + .fromMethodCall(on(BookingController::class.java).getBooking(21)).buildAndExpand(42) + +val uri = uriComponents.encode().toUri() +``` + +| |控制器方法签名在其设计中受到限制,因为它们应该可用于<br/>与`fromMethodCall`的链接创建。除了需要一个适当的参数签名外,<br/>在返回类型上还有一个技术限制(即为链接生成器调用生成一个运行时代理<br/>),因此返回类型不能是`final`。特别是,<br/>视图名称的常见`String`返回类型在此不工作。你应该使用`ModelAndView`,甚至使用普通的`Object`(带有`String`返回值)。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +较早的示例使用`MvcUriComponentsBuilder`中的静态方法。在内部,它们依赖`ServletUriComponentsBuilder`从当前请求的方案、主机、端口、上下文路径和 Servlet 路径准备一个基本 URL。这在大多数情况下都很有效。然而,有时候,这可能是不够的。例如,你可能在请求的上下文之外(例如准备链接的批处理),或者你可能需要插入路径前缀(例如从请求路径中删除并需要重新插入到链接中的区域设置前缀)。 + +对于这种情况,可以使用静态`fromXxx`重载方法,该方法接受 `uriComponentsBuilder’来使用基本 URL。或者,你可以使用基本 URL 创建`MvcUriComponentsBuilder`的实例,然后使用基于实例的`withXxx`方法。例如,下面的列表使用`withMethodCall`: + +爪哇 + +``` +UriComponentsBuilder base = ServletUriComponentsBuilder.fromCurrentContextPath().path("/en"); +MvcUriComponentsBuilder builder = MvcUriComponentsBuilder.relativeTo(base); +builder.withMethodCall(on(BookingController.class).getBooking(21)).buildAndExpand(42); + +URI uri = uriComponents.encode().toUri(); +``` + +Kotlin + +``` +val base = ServletUriComponentsBuilder.fromCurrentContextPath().path("/en") +val builder = MvcUriComponentsBuilder.relativeTo(base) +builder.withMethodCall(on(BookingController::class.java).getBooking(21)).buildAndExpand(42) + +val uri = uriComponents.encode().toUri() +``` + +| |截至 5.1,`MvcUriComponentsBuilder`忽略来自`Forwarded`和 `x-forwarded-*’头的信息,这些头指定了客户机发起的地址。考虑使用[ForwardedHeaderFilter](#filters-forwarded-headers)来提取和使用或丢弃<br/>这样的标题。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.5.6.视图中的链接 + +在 ThymeLeaf、Freemarker 或 JSP 等视图中,你可以通过引用每个请求映射的隐式或显式分配的名称来构建到带注释的控制器的链接。 + +考虑以下示例: + +爪哇 + +``` +@RequestMapping("/people/{id}/addresses") +public class PersonAddressController { + + @RequestMapping("/{country}") + public HttpEntity<PersonAddress> getAddress(@PathVariable String country) { ... } +} +``` + +Kotlin + +``` +@RequestMapping("/people/{id}/addresses") +class PersonAddressController { + + @RequestMapping("/{country}") + fun getAddress(@PathVariable country: String): HttpEntity<PersonAddress> { ... } +} +``` + +给定上述控制器,你可以从 JSP 准备一个链接,如下所示: + +``` +<%@ taglib uri="http://www.springframework.org/tags" prefix="s" %> +... +<a href="${s:mvcUrl('PAC#getAddress').arg(0,'US').buildAndExpand('123')}">Get Address</a> +``` + +前面的示例依赖于 Spring 标记库(即 meta-inf/ Spring.tld)中声明的`mvcUrl`函数,但是很容易定义自己的函数或为其他模板技术准备类似的函数。 + +这就是它的工作原理。在启动时,每个`@RequestMapping`都通过`HandlerMethodMappingNamingStrategy`分配一个默认名称,其默认实现使用类的大写字母和方法名称(例如,`thingcontroller’中的`getThing`方法变成了“tc#getthing”)。如果存在名称冲突,你可以使用 @requestmapping(name=“.”)` 来指定一个显式的名称或实现你自己的 `HandlerMethodMappingNamingStrategy’。 + +### 1.6.异步请求 + +[与 WebFlux 相比](#mvc-ann-async-vs-webflux) + +Spring MVC 与 Servlet 3.0 异步请求[processing](#mvc-ann-async-processing)具有广泛的集成: + +* 控制器方法中的[“推迟结果”](#mvc-ann-async-deferredresult)和[`Callable`](#mvc-ann-async-callable)返回值为单个异步返回值提供了基本支持。 + +* 控制器可以[stream](#mvc-ann-async-http-streaming)多个值,包括[SSE](#mvc-ann-async-sse)和[raw data](#mvc-ann-async-output-stream)。 + +* 控制器可以使用反应式客户端并返回[reactive types](#mvc-ann-async-reactive-types)进行响应处理。 + +#### 1.6.1.`DeferredResult` + +[与 WebFlux 相比](#mvc-ann-async-vs-webflux) + +一旦异步请求处理特征是[enabled](#mvc-ann-async-configuration)在 Servlet 容器中,控制器方法就可以将任何受支持的控制器方法返回值包装为`DeferredResult`,如下例所示: + +爪哇 + +``` +@GetMapping("/quotes") +@ResponseBody +public DeferredResult<String> quotes() { + DeferredResult<String> deferredResult = new DeferredResult<String>(); + // Save the deferredResult somewhere.. + return deferredResult; +} + +// From some other thread... +deferredResult.setResult(result); +``` + +Kotlin + +``` +@GetMapping("/quotes") +@ResponseBody +fun quotes(): DeferredResult<String> { + val deferredResult = DeferredResult<String>() + // Save the deferredResult somewhere.. + return deferredResult +} + +// From some other thread... +deferredResult.setResult(result) +``` + +控制器可以异步地从不同的线程产生返回值——例如,响应外部事件(JMS 消息)、计划任务或其他事件。 + +#### 1.6.2.`Callable` + +[与 WebFlux 相比](#mvc-ann-async-vs-webflux) + +控制器可以用`java.util.concurrent.Callable`包装任何受支持的返回值,如下例所示: + +爪哇 + +``` +@PostMapping +public Callable<String> processUpload(final MultipartFile file) { + + return new Callable<String>() { + public String call() throws Exception { + // ... + return "someView"; + } + }; +} +``` + +Kotlin + +``` +@PostMapping +fun processUpload(file: MultipartFile) = Callable<String> { + // ... + "someView" +} +``` + +然后可以通过[configured](#mvc-ann-async-configuration-spring-mvc)`TaskExecutor`运行给定任务来获得返回值。 + +#### 1.6.3.处理 + +[与 WebFlux 相比](#mvc-ann-async-vs-webflux) + +下面是 Servlet 异步请求处理的一个非常简明的概述: + +* 可以通过调用`request.startAsync()`将`ServletRequest`置于异步模式。这样做的主要效果是, Servlet(以及任何过滤器)都可以退出,但响应仍然是开放的,以便稍后完成处理。 + +* 对`request.startAsync()`的调用返回`AsyncContext`,你可以使用它来进一步控制异步处理。例如,它提供了`dispatch`方法,它类似于来自 Servlet API 的转发,只是它允许应用程序在 Servlet 容器线程上恢复请求处理。 + +* `ServletRequest`提供对当前`DispatcherType`的访问,你可以使用它来区分处理初始请求、异步分派、转发和其他分派类型。 + +`DeferredResult`处理工作如下: + +* 控制器返回一个`DeferredResult`,并将其保存在内存队列或列表中,以便访问它。 + +* Spring MVC 调用`request.startAsync()`。 + +* 同时,`DispatcherServlet`和所有配置的过滤器退出请求处理线程,但响应保持打开状态。 + +* 应用程序设置来自某个线程的`DeferredResult`,然后 Spring MVC 将请求分派回 Servlet 容器。 + +* 再次调用`DispatcherServlet`,然后用异步产生的返回值恢复处理。 + +`Callable`处理工作如下: + +* 控制器返回`Callable`。 + +* Spring MVC 调用`request.startAsync()`,并将`Callable`提交给`TaskExecutor`,以便在单独的线程中进行处理。 + +* 同时,`DispatcherServlet`和所有过滤器都退出 Servlet 容器线程,但响应仍然是打开的。 + +* 最终,`Callable`产生一个结果, Spring MVC 将请求分派回 Servlet 容器以完成处理。 + +* 将再次调用`DispatcherServlet`,并使用来自`Callable`的异步产生的返回值恢复处理。 + +对于进一步的背景和上下文,你还可以阅读 Spring MVC3.2 中引入了异步请求处理支持的[the blog posts](https://spring.io/blog/2012/05/07/spring-mvc-3-2-preview-introducing-servlet-3-async-support)。 + +##### 异常处理 + +当你使用`DeferredResult`时,你可以选择调用`setResult`还是调用 `seTerrorResult’,但有一个例外。在这两种情况下, Spring MVC 将请求分派回 Servlet 容器以完成处理。然后将其视为控制器方法返回给定的值或产生给定的异常。然后,异常将通过常规的异常处理机制(例如,调用“@ExceptionHandler”方法)。 + +当使用`Callable`时,会出现类似的处理逻辑,主要的区别是从`Callable`返回结果,或者由它引发异常。 + +##### 拦截 + +`HandlerInterceptor`实例的类型可以是`AsyncHandlerInterceptor`,以便在启动异步处理的初始请求上接收 `afterconcurrythandlingstarted’回调(而不是`postHandle`和`afterCompletion`)。 + +`HandlerInterceptor`实现还可以注册`CallableProcessingInterceptor`或`DeferredResultProcessingInterceptor`,以更深入地与异步请求的生命周期集成(例如,处理超时事件)。有关更多详细信息,请参见[AsynchandlerInterceptor](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/servlet/AsyncHandlerInterceptor.html)。 + +`DeferredResult`提供`onTimeout(Runnable)`和`onCompletion(Runnable)`回调。有关更多详细信息,请参见[javadoc of `DeferredResult`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/context/request/async/DeferredResult.html)。`Callable`可以替换`WebAsyncTask`,后者公开了用于超时和完成回调的其他方法。 + +##### 与 WebFlux 相比 + +Servlet API 最初是为通过过滤器- Servlet 链而构建的。 Servlet 3.0 中添加的异步请求处理允许应用程序退出 Filter- Servlet 链,但允许响应开放以进行进一步的处理。 Spring MVC 异步支持是围绕该机制构建的。当控制器返回`DeferredResult`时,退出 filter- Servlet 链,并释放 Servlet 容器线程。稍后,当设置`DeferredResult`时,将执行`ASYNC`调度(发送到相同的 URL),在此期间将再次映射控制器,但是将使用`DeferredResult`值(就像控制器返回了它一样)来恢复处理。 + +相比之下, Spring WebFlux 既不是建立在 Servlet API 上的,也不需要这样的异步请求处理特性,因为它在设计上是异步的。异步处理被内置在所有框架契约中,并且在请求处理的所有阶段都得到了本质上的支持。 + +从编程模型的角度来看, Spring MVC 和 Spring WebFlux 都支持异步和作为控制器方法中的返回值。 Spring MVC 甚至支持流媒体,包括反压力反应。然而,对响应的单独写仍然是阻塞的(并且在单独的线程上执行),这与 WebFlux 不同,后者依赖于非阻塞的 I/O,并且不需要为每个写增加一个线程。 + +另一个根本的区别是 Spring MVC 不支持控制器方法参数中的异步或反应类型(例如,`@RequestBody`,`@RequestPart`,以及其他),也不明确支持异步和反应类型作为模型属性。 Spring WebFlux 确实支持这一切。 + +#### 1.6.4.HTTP 流媒体 + +[WebFlux](web-reactive.html#webflux-codecs-streaming) + +对于单个异步返回值,可以使用`DeferredResult`和`Callable`。如果你想要产生多个异步值并将这些值写入响应中,该怎么办?这一节描述了如何做到这一点。 + +##### 对象 + +你可以使用`ResponseBodyEmitter`返回值来生成一个对象流,其中每个对象都用[`HttpMessageConverter’](integration.html#rest-message-conversion)序列化并写入响应,如下例所示: + +爪哇 + +``` +@GetMapping("/events") +public ResponseBodyEmitter handle() { + ResponseBodyEmitter emitter = new ResponseBodyEmitter(); + // Save the emitter somewhere.. + return emitter; +} + +// In some other thread +emitter.send("Hello once"); + +// and again later on +emitter.send("Hello again"); + +// and done at some point +emitter.complete(); +``` + +Kotlin + +``` +@GetMapping("/events") +fun handle() = ResponseBodyEmitter().apply { + // Save the emitter somewhere.. +} + +// In some other thread +emitter.send("Hello once") + +// and again later on +emitter.send("Hello again") + +// and done at some point +emitter.complete() +``` + +你也可以使用`ResponseBodyEmitter`作为`ResponseEntity`中的主体,让你自定义响应的状态和标题。 + +当`emitter`抛出`IOException`时(例如,如果远程客户端消失了),应用程序不负责清理连接,并且不应调用`emitter.complete`或`emitter.completeWithError`。相反, Servlet 容器自动发起“Asynclistener”错误通知,其中 Spring MVC 进行`completeWithError`调用。然后,这个调用执行对应用程序的最后一次`ASYNC`分派,在此期间, Spring MVC 调用配置的异常解析器并完成请求。 + +##### SSE + +`SseEmitter`(`ResponseBodyEmitter`的一个子类)提供了对[服务器发送的事件](https://www.w3.org/TR/eventsource/)的支持,其中从服务器发送的事件是根据 W3C SSE 规范进行格式化的。要从控制器生成 SSE 流,请返回`SseEmitter`,如下例所示: + +爪哇 + +``` +@GetMapping(path="/events", produces=MediaType.TEXT_EVENT_STREAM_VALUE) +public SseEmitter handle() { + SseEmitter emitter = new SseEmitter(); + // Save the emitter somewhere.. + return emitter; +} + +// In some other thread +emitter.send("Hello once"); + +// and again later on +emitter.send("Hello again"); + +// and done at some point +emitter.complete(); +``` + +Kotlin + +``` +@GetMapping("/events", produces = [MediaType.TEXT_EVENT_STREAM_VALUE]) +fun handle() = SseEmitter().apply { + // Save the emitter somewhere.. +} + +// In some other thread +emitter.send("Hello once") + +// and again later on +emitter.send("Hello again") + +// and done at some point +emitter.complete() +``` + +虽然 SSE 是流媒体进入浏览器的主要选项,但请注意,Internet Explorer 不支持服务器发送的事件。考虑使用 Spring 的[WebSocket messaging](#websocket)和[SockJS fallback](#websocket-fallback)传输(包括 SSE),这些传输针对广泛的浏览器。 + +另请参阅[上一节](#mvc-ann-async-objects)有关异常处理的说明。 + +##### 原始数据 + +有时,绕过消息转换并直接流到响应“outputstream”(例如,对于文件下载)是有用的。你可以使用`StreamingResponseBody`返回值类型来这样做,如下例所示: + +爪哇 + +``` +@GetMapping("/download") +public StreamingResponseBody handle() { + return new StreamingResponseBody() { + @Override + public void writeTo(OutputStream outputStream) throws IOException { + // write... + } + }; +} +``` + +Kotlin + +``` +@GetMapping("/download") +fun handle() = StreamingResponseBody { + // write... +} +``` + +你可以使用`StreamingResponseBody`作为`ResponseEntity`中的主体来定制响应的状态和标题。 + +#### 1.6.5.反应类型 + +[WebFlux](web-reactive.html#webflux-codecs-streaming) + +Spring MVC 支持在控制器中使用反应式客户端库(在 WebFlux 部分中也可以读)。这包括来自`spring-webflux`的`WebClient`和其他的,例如 Spring 数据反应式数据存储库。在这样的场景中,能够从控制器方法返回无功类型是很方便的。 + +反应性返回值的处理如下: + +* 对单值 promise 进行了调整,类似于使用`DeferredResult`。例如`Mono`(反应堆)或`Single`。 + +* 具有流媒体类型的多值流(例如`application/x-ndjson`或`text/event-stream`)被适配,类似于使用`ResponseBodyEmitter`或 `sseemitter’。例如`Flux`(反应堆)或`Observable`。应用程序也可以返回`Flux<ServerSentEvent>`或`Observable<ServerSentEvent>`。 + +* 多值流与任何其他媒体类型(例如`application/json`)相适应,类似于使用`DeferredResult<List<?>>`。 + +| |Spring MVC 通过来自 ` Spring-core` 的[“ReactiveAdapterRegistry”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/core/ReactiveAdapterRegistry.html)来支持 Reactor 和 Rxjava,这使它能够适应多个反应库。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +对于流到响应,支持反应背压,但是写到响应仍然是阻塞的,并且是通过[configured](#mvc-ann-async-configuration-spring-mvc)`TaskExecutor`在单独的线程上运行的,以避免阻塞上游源(例如从`Flux`返回的`WebClient`)。默认情况下,`SimpleAsyncTaskExecutor`用于阻塞写操作,但这在加载时不适用。如果你计划使用反应类型的流,那么你应该使用[MVC 配置](#mvc-ann-async-configuration-spring-mvc)来配置任务执行器。 + +#### 1.6.6.断开连接 + +[WebFlux](web-reactive.html#webflux-codecs-streaming) + +Servlet 当远程客户端消失时,API 不提供任何通知。因此,在流到响应的同时,无论是通过[SseEmitter](#mvc-ann-async-sse)还是[reactive types](#mvc-ann-async-reactive-types),定期发送数据是重要的,因为如果客户端已断开连接,则写失败。发送可以采取空(仅限注释)SSE 事件的形式,或者任何其他数据,而另一方必须将其解释为心跳并忽略这些数据。 + +或者,考虑使用具有内置心跳机制的 Web 消息传递解决方案(例如[STOMP over WebSocket](#websocket-stomp)或 WebSocket with[SockJS](#websocket-fallback))。 + +#### 1.6.7.配置 + +[与 WebFlux 相比](#mvc-ann-async-vs-webflux) + +异步请求处理特性必须在 Servlet 容器级别上启用。MVC 配置还为异步请求公开了几个选项。 + +##### Servlet 集装箱 + +Filter 和 Servlet 声明具有一个`asyncSupported`标志,该标志需要设置为`true`,以启用异步请求处理。此外,应该声明过滤器映射来处理`ASYNC``javax.servlet.DispatchType`。 + +在 爪哇 配置中,当你使用`AbstractAnnotationConfigDispatcherServletInitializer`初始化 Servlet 容器时,这是自动完成的。 + +在`web.xml`configuration 中,可以将`<async-supported>true</async-supported>`添加到 `DispatcherServlet’和`Filter`声明中,并添加 `<dispatcher>async</dispatcher>` 以过滤映射。 + +##### Spring MVC + +MVC 配置公开了以下与异步请求处理相关的选项: + +* 爪哇 配置:在`WebMvcConfigurer`上使用`configureAsyncSupport`回调。 + +* XML 命名空间:在`<mvc:annotation-driven>`下使用`<async-support>`元素。 + +你可以配置以下内容: + +* 异步请求的默认超时值(如果未设置该超时值)取决于底层 Servlet 容器。 + +* `AsyncTaskExecutor`用于在使用[Reactive Types](#mvc-ann-async-reactive-types)进行流式传输时阻止写操作,并用于执行从控制器方法返回的`Callable`实例。我们强烈建议配置此属性,如果你使用无反应类型的流或具有返回`Callable`的控制器方法,因为默认情况下,它是`SimpleAsyncTaskExecutor`。 + +* `DeferredResultProcessingInterceptor`实现和`CallableProcessingInterceptor`实现。 + +请注意,你还可以在`DeferredResult`、`ResponseBodyEmitter`和`SseEmitter`上设置默认的超时值。对于`Callable`,可以使用 `webasynctask’来提供超时值。 + +### 1.7.科尔斯 + +[WebFlux](web-reactive.html#webflux-cors) + +Spring MVC 允许你处理 CORS(跨源资源共享)。这一节描述了如何做到这一点。 + +#### 1.7.1.导言 + +[WebFlux](web-reactive.html#webflux-cors-intro) + +出于安全原因,浏览器禁止对当前来源以外的资源进行 Ajax 调用。例如,你可以在一个标签中设置你的银行帐户,而在另一个标签中设置 Evil.com。来自 Evil.com 的脚本不应该能够使用你的凭据向你的银行 API 发出 Ajax 请求——例如,从你的帐户中取款! + +跨源资源共享是由[most browsers](https://caniuse.com/#feat=cors)实现的[W3C 规范](https://www.w3.org/TR/cors/),它允许你指定授权哪种类型的跨域请求,而不是使用基于 iframe 或 jsonp 的安全性较低、功能较弱的解决方案。 + +#### 1.7.2.处理 + +[WebFlux](web-reactive.html#webflux-cors-processing) + +CORS 规范区分了飞行前、简单和实际的请求。要了解 CORS 的工作原理,你可以阅读[this article](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)等,或者查看规范以获得更多详细信息。 + +Spring MVC实现为 CORS 提供了内置支持。在成功地将一个请求映射到一个处理程序之后,`HandlerMapping`实现将检查 CORS 配置中给定的请求和处理程序,并采取进一步的操作。前置请求是直接处理的,而简单和实际的 CORS 请求是截获、验证的,并且设置了所需的 CORS 响应头。 + +为了启用跨源请求(即存在`Origin`头,并且与请求的主机不同),你需要有一些显式声明的 CORS 配置。如果没有找到匹配的 CORS 配置,则拒绝预航前请求。没有 CORS 头被添加到简单的和实际的 CORS 请求的响应中,因此,浏览器会拒绝它们。 + +每个`HandlerMapping`都可以单独使用基于 URL 模式的`Cors配置`映射[configured](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/servlet/handler/AbstractHandlerMapping.html#setCors配置s-java.util.Map-)。在大多数情况下,应用程序使用 MVC 爪哇 配置或 XML 命名空间来声明这样的映射,这将导致将单个全局映射传递给所有`HandlerMapping`实例。 + +你可以将`HandlerMapping`级别的全局 CORS 配置与更细粒度的、处理程序级别的 CORS 配置结合起来。例如,带注释的控制器可以使用类或方法级别的`@CrossOrigin`注释(其他处理程序可以实现 `CorsConfigurationSource’)。 + +结合全局和局部配置的规则通常是累加的——例如,所有全局配置和所有局部配置。对于那些只能接受单个值的属性,例如。`allowCredentials`和`maxAge`,局部重写全局值。有关更多详细信息,请参见[“CorsConfiguration#Combine”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/cors/CorsConfiguration.html#combine-org.springframework.web.cors.CorsConfiguration-)。 + +| |要从源代码中了解更多信息或进行高级定制,请检查后面的代码:<br/><br/>*`CorsConfiguration`<br/><br/>*`CorsProcessor`,`DefaultCorsProcessor`<br/><br/>| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.7.3.`@CrossOrigin` + +[WebFlux](web-reactive.html#webflux-cors-controller) + +[`@CrossOrigin`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/bind/annotation/CrossOrigin.html)注释允许对带注释的控制器方法进行跨源请求,如下例所示: + +爪哇 + +``` +@RestController +@RequestMapping("/account") +public class AccountController { + + @CrossOrigin + @GetMapping("/{id}") + public Account retrieve(@PathVariable Long id) { + // ... + } + + @DeleteMapping("/{id}") + public void remove(@PathVariable Long id) { + // ... + } +} +``` + +Kotlin + +``` +@RestController +@RequestMapping("/account") +class AccountController { + + @CrossOrigin + @GetMapping("/{id}") + fun retrieve(@PathVariable id: Long): Account { + // ... + } + + @DeleteMapping("/{id}") + fun remove(@PathVariable id: Long) { + // ... + } +} +``` + +默认情况下,`@CrossOrigin`允许: + +* 所有的起源。 + +* 所有标题。 + +* 将控制器方法映射到的所有 HTTP 方法。 + +`allowCredentials`默认情况下不启用,因为这建立了一个信任级别,该级别公开敏感的特定于用户的信息(例如 Cookie 和 CSRF 令牌),并且只应在适当的情况下使用。启用`allowOrigins`时,必须将`allowOrigins`设置为一个或多个特定的域(但不是特殊值`"*"`),或者,`allowOriginPatterns`属性可用于匹配到源集的动态。 + +`maxAge`设置为 30 分钟。 + +`@CrossOrigin`在类级别也受到支持,并且所有方法都会继承它,如下例所示: + +爪哇 + +``` +@CrossOrigin(origins = "https://domain2.com", maxAge = 3600) +@RestController +@RequestMapping("/account") +public class AccountController { + + @GetMapping("/{id}") + public Account retrieve(@PathVariable Long id) { + // ... + } + + @DeleteMapping("/{id}") + public void remove(@PathVariable Long id) { + // ... + } +} +``` + +Kotlin + +``` +@CrossOrigin(origins = ["https://domain2.com"], maxAge = 3600) +@RestController +@RequestMapping("/account") +class AccountController { + + @GetMapping("/{id}") + fun retrieve(@PathVariable id: Long): Account { + // ... + } + + @DeleteMapping("/{id}") + fun remove(@PathVariable id: Long) { + // ... + } +``` + +可以在类级别和方法级别使用`@CrossOrigin`,如下例所示: + +爪哇 + +``` +@CrossOrigin(maxAge = 3600) +@RestController +@RequestMapping("/account") +public class AccountController { + + @CrossOrigin("https://domain2.com") + @GetMapping("/{id}") + public Account retrieve(@PathVariable Long id) { + // ... + } + + @DeleteMapping("/{id}") + public void remove(@PathVariable Long id) { + // ... + } +} +``` + +Kotlin + +``` +@CrossOrigin(maxAge = 3600) +@RestController +@RequestMapping("/account") +class AccountController { + + @CrossOrigin("https://domain2.com") + @GetMapping("/{id}") + fun retrieve(@PathVariable id: Long): Account { + // ... + } + + @DeleteMapping("/{id}") + fun remove(@PathVariable id: Long) { + // ... + } +} +``` + +#### 1.7.4.全局配置 + +[WebFlux](web-reactive.html#webflux-cors-global) + +除了细粒度的控制器方法级配置外,你可能还需要定义一些全局 CORS 配置。你可以在任何`HandlerMapping`上单独设置基于 URL 的`CorsConfiguration`映射。然而,大多数应用程序使用 MVC 爪哇 配置或 MVC XML 命名空间来实现这一点。 + +默认情况下,全局配置启用以下功能: + +* 所有的起源。 + +* 所有标题。 + +* `GET`,`HEAD`,和`POST`方法。 + +`allowCredentials`默认情况下不启用,因为这建立了一个信任级别,该级别公开敏感的特定于用户的信息(例如 Cookie 和 CSRF 令牌),并且只应在适当的情况下使用。当启用`allowOrigins`时,必须将其设置为一个或多个特定的域(但不是特殊值`"*"`),或者,`allowOriginPatterns`属性可用于与源集的动态匹配。 + +`maxAge`设置为 30 分钟。 + +##### 爪哇 配置 + +[WebFlux](web-reactive.html#webflux-cors-global) + +要在 MVC 爪哇 配置中启用 CORS,可以使用`CorsRegistry`回调,如下例所示: + +爪哇 + +``` +@Configuration +@EnableWebMvc +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + + registry.addMapping("/api/**") + .allowedOrigins("https://domain2.com") + .allowedMethods("PUT", "DELETE") + .allowedHeaders("header1", "header2", "header3") + .exposedHeaders("header1", "header2") + .allowCredentials(true).maxAge(3600); + + // Add more mappings... + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebMvc +class WebConfig : WebMvcConfigurer { + + override fun addCorsMappings(registry: CorsRegistry) { + + registry.addMapping("/api/**") + .allowedOrigins("https://domain2.com") + .allowedMethods("PUT", "DELETE") + .allowedHeaders("header1", "header2", "header3") + .exposedHeaders("header1", "header2") + .allowCredentials(true).maxAge(3600) + + // Add more mappings... + } +} +``` + +##### XML 配置 + +要在 XML 命名空间中启用 COR,可以使用`<mvc:cors>`元素,如下例所示: + +``` +<mvc:cors> + + <mvc:mapping path="/api/**" + allowed-origins="https://domain1.com, https://domain2.com" + allowed-methods="GET, PUT" + allowed-headers="header1, header2, header3" + exposed-headers="header1, header2" allow-credentials="true" + max-age="123" /> + + <mvc:mapping path="/resources/**" + allowed-origins="https://domain1.com" /> + +</mvc:cors> +``` + +#### 1.7.5.CORS 过滤器 + +[WebFlux](webflux-cors.html#webflux-cors-webfilter) + +你可以通过内置的[`CorsFilter`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/filter/CorsFilter.html)应用 CORS 支持。 + +| |如果你尝试使用带有 Spring 安全性的`CorsFilter`,请记住,对于 CORS,<br/> Spring 安全性具有[内置支持](https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#cors)。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +要配置过滤器,请将“corsconfigurationSource”传递到其构造函数,如下例所示: + +爪哇 + +``` +CorsConfiguration config = new CorsConfiguration(); + +// Possibly... +// config.applyPermitDefaultValues() + +config.setAllowCredentials(true); +config.addAllowedOrigin("https://domain1.com"); +config.addAllowedHeader("*"); +config.addAllowedMethod("*"); + +UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); +source.registerCorsConfiguration("/**", config); + +CorsFilter filter = new CorsFilter(source); +``` + +Kotlin + +``` +val config = CorsConfiguration() + +// Possibly... +// config.applyPermitDefaultValues() + +config.allowCredentials = true +config.addAllowedOrigin("https://domain1.com") +config.addAllowedHeader("*") +config.addAllowedMethod("*") + +val source = UrlBasedCorsConfigurationSource() +source.registerCorsConfiguration("/**", config) + +val filter = CorsFilter(source) +``` + +### 1.8.网络安全 + +[WebFlux](web-reactive.html#webflux-web-security) + +[Spring Security](https://projects.spring.io/spring-security/)项目提供了保护 Web 应用程序免受恶意攻击的支持。请参阅 Spring 安全参考文档,包括: + +* [Spring MVC Security](https://docs.spring.io/spring-security/site/docs/current/reference/html5/#mvc) + +* [Spring MVC Test Support](https://docs.spring.io/spring-security/site/docs/current/reference/html5/#test-mockmvc) + +* [CSRF protection](https://docs.spring.io/spring-security/site/docs/current/reference/html5/#csrf) + +* [安全响应标头](https://docs.spring.io/spring-security/site/docs/current/reference/html5/#headers) + +[HDIV](https://hdiv.org/)是与 Spring MVC 集成的另一种 Web 安全框架。 + +### 1.9.HTTP 缓存 + +[WebFlux](web-reactive.html#webflux-caching) + +HTTP 缓存可以显著提高 Web 应用程序的性能。HTTP 缓存围绕`Cache-Control`响应头,随后是条件请求头(例如`Last-Modified`和`ETag`)。`Cache-Control`建议私有(例如,浏览器)和公共(例如,代理)缓存如何缓存和重用响应。`ETag`报头用于发出条件请求,如果内容没有更改,则该请求可能导致没有正文的 304(未 \_modified)。`ETag`可以看作是`Last-Modified`标头的更复杂的继承者。 + +本节描述了 Spring Web MVC 中可用的与 HTTP 缓存相关的选项。 + +#### 1.9.1.`CacheControl` + +[WebFlux](web-reactive.html#webflux-caching-cachecontrol) + +[`CacheControl`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/http/CacheControl.html)提供了对配置与`Cache-Control`标头相关的设置的支持,并在许多地方被接受为参数: + +* [“WebContentInterceptor”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/servlet/mvc/WebContentInterceptor.html) + +* [“WebContentGenerator”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/servlet/support/WebContentGenerator.html) + +* [Controllers](#mvc-caching-etag-lastmodified) + +* [静态资源](#mvc-caching-static-resources) + +虽然[RFC 7234](https://tools.ietf.org/html/rfc7234#section-5.2.2)描述了`Cache-Control`响应头的所有可能的指令,但`CacheControl`类型采用了一种面向用例的方法,该方法关注于常见的场景: + +爪哇 + +``` +// Cache for an hour - "Cache-Control: max-age=3600" +CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS); + +// Prevent caching - "Cache-Control: no-store" +CacheControl ccNoStore = CacheControl.noStore(); + +// Cache for ten days in public and private caches, +// public caches should not transform the response +// "Cache-Control: max-age=864000, public, no-transform" +CacheControl ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic(); +``` + +Kotlin + +``` +// Cache for an hour - "Cache-Control: max-age=3600" +val ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS) + +// Prevent caching - "Cache-Control: no-store" +val ccNoStore = CacheControl.noStore() + +// Cache for ten days in public and private caches, +// public caches should not transform the response +// "Cache-Control: max-age=864000, public, no-transform" +val ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic() +``` + +`WebContentGenerator`还接受一个更简单的`cachePeriod`属性(以秒为单位定义),其工作方式如下: + +* `-1`值不会生成`Cache-Control`响应报头。 + +* 通过使用`'Cache-Control: no-store'`指令,`0`值可以防止缓存。 + +* 通过使用“cache-control:max-age=n”指令,`n > 0`值将给定的响应缓存为`n`秒。 + +#### 1.9.2.控制器 + +[WebFlux](web-reactive.html#webflux-caching-etag-lastmodified) + +控制器可以添加对 HTTP 缓存的显式支持。我们建议这样做,因为资源的“lastmodified”或`ETag`值需要在与条件请求头进行比较之前进行计算。控制器可以将`ETag`标头和`Cache-Control`设置添加到`ResponseEntity`中,如下例所示: + +爪哇 + +``` +@GetMapping("/book/{id}") +public ResponseEntity<Book> showBook(@PathVariable Long id) { + + Book book = findBook(id); + String version = book.getVersion(); + + return ResponseEntity + .ok() + .cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS)) + .eTag(version) // lastModified is also available + .body(book); +} +``` + +Kotlin + +``` +@GetMapping("/book/{id}") +fun showBook(@PathVariable id: Long): ResponseEntity<Book> { + + val book = findBook(id); + val version = book.getVersion() + + return ResponseEntity + .ok() + .cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS)) + .eTag(version) // lastModified is also available + .body(book) +} +``` + +如果与条件请求标题的比较表明内容没有更改,则前面的示例发送带有空主体的 304(not\_modified)响应。否则,`ETag’和`Cache-Control`头将被添加到响应中。 + +你还可以检查控制器中的条件请求头,如下例所示: + +爪哇 + +``` +@RequestMapping +public String myHandleMethod(WebRequest request, Model model) { + + long eTag = ... (1) + + if (request.checkNotModified(eTag)) { + return null; (2) + } + + model.addAttribute(...); (3) + return "myViewName"; +} +``` + +|**1**|应用程序特定的计算。| +|-----|-------------------------------------------------------------------------| +|**2**|响应已设置为 304(未修改)——没有进一步的处理。| +|**3**|继续处理请求。| + +Kotlin + +``` +@RequestMapping +fun myHandleMethod(request: WebRequest, model: Model): String? { + + val eTag: Long = ... (1) + + if (request.checkNotModified(eTag)) { + return null (2) + } + + model[...] = ... (3) + return "myViewName" +} +``` + +|**1**|应用程序特定的计算。| +|-----|-------------------------------------------------------------------------| +|**2**|响应已设置为 304(未修改)——没有进一步的处理。| +|**3**|继续处理请求。| + +针对`eTag`值、`lastModified`值或两者检查条件请求有三种变体。对于条件`GET`和`HEAD`请求,可以将响应设置为 304(不是 \_modified)。对于条件`POST`、`PUT`和`DELETE`,你可以将响应设置为 412(前提条件 \_ 失败),以防止并发修改。 + +#### 1.9.3.静态资源 + +[WebFlux](web-reactive.html#webflux-caching-static-resources) + +为了获得最佳性能,你应该使用`Cache-Control`和条件响应头来服务静态资源。参见关于配置[静态资源](#mvc-config-static-resources)的部分。 + +#### 1.9.4.`ETag`过滤器 + +你可以使用`ShallowEtagHeaderFilter`来添加“shallow”`eTag`值,这些值是从响应内容计算出来的,因此,可以节省带宽,但不会节省 CPU 时间。见[Shallow ETag](#filters-shallow-etag)。 + +### 1.10.查看技术 + +[WebFlux](web-reactive.html#webflux-view) + +Spring MVC 中的视图技术的使用是可插入的。是否决定使用 ThymeLeaf、Groovy 标记模板、JSP 或其他技术主要是配置更改的问题。本章介绍与 Spring MVC 集成的视图技术。我们假设你已经熟悉[View Resolution](#mvc-viewresolver)。 + +| |Spring MVC 应用程序的视图位于该应用程序的内部信任边界<br/>内。视图可以访问应用程序上下文中的所有 bean。由于<br/>这样,不建议在应用程序中使用 Spring MVC 的模板支持,因为在这种应用程序中,<br/>模板可以由外部源进行编辑,因为这可能具有安全含义。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.10.1.百里香叶 + +[WebFlux](web-reactive.html#webflux-view-thymeleaf) + +ThymeLeaf 是一个现代的服务器端 爪哇 模板引擎,强调自然的 HTML 模板,可以通过双击在浏览器中预览,这对于在 UI 模板上独立工作(例如,由设计师)非常有帮助,而不需要运行的服务器。如果你想要替换 JSP,ThymeLeaf 提供了最广泛的一组特性,可以使这种转换变得更容易。麝香草叶得到了积极的开发和维护。有关更完整的介绍,请参见[Thymeleaf](https://www.thymeleaf.org/)项目主页。 + +ThymeLeaf 与 Spring MVC 的集成由 ThymeLeaf 项目管理。该配置涉及一些声明,例如“servletContextemPlateResolver”、<gtr="2104"/>和<gtr="2105"/>。有关更多详细信息,请参见[Thymeleaf+Spring](https://www.thymeleaf.org/documentation.html)。 + +#### 1.10.2.自由标记 + +[WebFlux](web-reactive.html#webflux-view-freemarker) + +[Apache Freemarker](https://freemarker.apache.org/)是一个模板引擎,用于生成从 HTML 到电子邮件等任何类型的文本输出。 Spring 框架具有用于使用 Spring MVC 和 Freemarker 模板的内置集成。 + +##### 视图配置 + +[WebFlux](web-reactive.html#webflux-view-freemarker-contextconfig) + +下面的示例展示了如何将 Freemarker 配置为一种视图技术: + +爪哇 + +``` +@Configuration +@EnableWebMvc +public class WebConfig implements WebMvcConfigurer { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.freeMarker(); + } + + // Configure FreeMarker... + + @Bean + public FreeMarkerConfigurer freeMarkerConfigurer() { + FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); + configurer.setTemplateLoaderPath("/WEB-INF/freemarker"); + return configurer; + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebMvc +class WebConfig : WebMvcConfigurer { + + override fun configureViewResolvers(registry: ViewResolverRegistry) { + registry.freeMarker() + } + + // Configure FreeMarker... + + @Bean + fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply { + setTemplateLoaderPath("/WEB-INF/freemarker") + } +} +``` + +下面的示例展示了如何在 XML 中配置相同的内容: + +``` +<mvc:annotation-driven/> + +<mvc:view-resolvers> + <mvc:freemarker/> +</mvc:view-resolvers> + +<!-- Configure FreeMarker... --> +<mvc:freemarker-configurer> + <mvc:template-loader-path location="/WEB-INF/freemarker"/> +</mvc:freemarker-configurer> +``` + +或者,你也可以声明`FreeMarkerConfigurer` Bean 以完全控制所有属性,如下例所示: + +``` +<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer"> + <property name="templateLoaderPath" value="/WEB-INF/freemarker/"/> +</bean> +``` + +你的模板需要存储在前面示例中所示的`FreeMarkerConfigurer`指定的目录中。给定上述配置,如果控制器返回`welcome`的视图名,则解析器将查找 `/WEB-INF/freemarker/welcome.ftl` 模板。 + +##### 自由标记配置 + +[WebFlux](web-reactive.html#webflux-views-freemarker) + +通过在`FreeMarkerConfigurer` Bean 上设置适当的 Bean 属性,可以将自由标记“设置”和“共享变量”直接传递给自由标记“配置”对象(由 Spring 管理)。`freemarkerSettings`属性需要一个`java.util.Properties`对象,而`freemarkerVariables`属性需要一个 `java.util.map’。下面的示例展示了如何使用`FreeMarkerConfigurer`: + +``` +<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer"> + <property name="templateLoaderPath" value="/WEB-INF/freemarker/"/> + <property name="freemarkerVariables"> + <map> + <entry key="xml_escape" value-ref="fmXmlEscape"/> + </map> + </property> +</bean> + +<bean id="fmXmlEscape" class="freemarker.template.utility.XmlEscape"/> +``` + +有关应用于`Configuration`对象的设置和变量的详细信息,请参见 Freemarker 文档。 + +##### 表单处理 + +Spring 提供了一种用于 JSP 的标记库,该标记库包括一个 `<spring:bind/>` 元素。这个元素主要允许表单显示来自表单支持对象的值,并显示来自 Web 或业务层中`Validator`的失败验证的结果。 Spring 在 Freemarker 中还具有对相同功能的支持,具有用于生成表单输入元素本身的附加方便宏。 + +###### BIND 宏 + +[WebFlux](web-reactive.html#webflux-view-bind-macros) + +在`spring-webmvc.jar`freemarker 文件中维护了一组标准的宏,因此对于适当配置的应用程序,它们总是可用的。 + +Spring 模板库中定义的一些宏被认为是内部的(私有的),但是在宏定义中不存在这样的范围,这使得所有的宏对于调用代码和用户模板都是可见的。下面的部分只关注你需要从模板中直接调用的宏。如果你希望直接查看宏代码,那么该文件名为`spring.ftl`,位于 `org.springframework.web. Servlet.view.freemarker`package 中。 + +###### 简单绑定 + +在基于作为 Spring MVC 控制器的窗体视图的自由标记模板的 HTML 表单中,你可以使用类似于下一个示例的代码绑定到字段值,并以类似于 JSP 的方式显示每个输入字段的错误消息。下面的示例显示了`personForm`视图: + +``` +<!-- FreeMarker macros have to be imported into a namespace. + We strongly recommend sticking to 'spring'. --> +<#import "/spring.ftl" as spring/> +<html> + ... + <form action="" method="POST"> + Name: + <@spring.bind "personForm.name"/> + <input type="text" + name="${spring.status.expression}" + value="${spring.status.value?html}"/><br /> + <#list spring.status.errorMessages as error> <b>${error}</b> <br /> </#list> + <br /> + ... + <input type="submit" value="submit"/> + </form> + ... +</html> +``` + +`<@spring.bind>`需要一个’path’参数,该参数包括命令对象的名称(除非在控制器配置中更改了它,否则它是’command’),然后是一个句号和你希望绑定到的命令对象上的字段的名称。你也可以使用嵌套字段,例如`command.address.street`。`bind`宏在`ServletContext`参数 `defaulthmlescape` 中假定了`web.xml`中指定的默认 HTML 转义行为。 + +名为`<@spring.bindEscaped>`的宏的另一种形式接受第二个参数,该参数显式地指定在状态错误消息或值中是否应该使用 HTML 转义。你可以根据需要将其设置为`true`或`false`。额外的表单处理宏简化了 HTML 转义的使用,你应该尽可能使用这些宏。它们将在下一节中进行解释。 + +###### 输入宏 + +FreeMarker 的附加方便宏简化了绑定和表单生成(包括验证错误显示)。从来没有必要使用这些宏来生成表单输入字段,你可以将它们与简单的 HTML 或直接调用 Spring BIND 宏进行混合和匹配,我们在前面强调了这一点。 + +下面的可用宏的表格显示了 Freemarker Template 的定义和每个定义所接受的参数列表: + +|宏| FTL definition | +|------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------| +|`message`(根据代码参数从资源包输出字符串)| \<@spring.message code/\> | +|`messageText`(根据代码参数从资源包输出字符串,<br/>返回到默认参数的值)| \<@spring.messageText code, text/\> | +|`url`(用应用程序的上下文根作为相对 URL 的前缀)| \<@spring.url relativeUrl/\> | +|`formInput`(收集用户输入的标准输入字段)| \<@spring.formInput path, attributes, fieldType/\> | +|`formHiddenInput`(用于提交非用户输入的隐藏输入字段)| \<@spring.formHiddenInput path, attributes/\> | +|`formPasswordInput`(收集密码的标准输入字段。请注意,在这种类型的字段中没有填充<br/>值。)| \<@spring.formPasswordInput path, attributes/\> | +|`formTextarea`(用于收集长的、自由格式的文本输入的大型文本字段)| \<@spring.formTextarea path, attributes/\> | +|`formSingleSelect`(下拉框中的选项,可以让单个所需的值被<br/>选中)| \<@spring.formSingleSelect path, options, attributes/\> | +|`formMultiSelect`(允许用户选择 0 个或更多值的选项列表框)| \<@spring.formMultiSelect path, options, attributes/\> | +|`formRadioButtons`(一组单选按钮,可以从可用的选项中进行单个选择<br/>)|\<@spring.formRadioButtons path, options separator, attributes/\>| +|`formCheckboxes`(一组允许选择 0 个或更多值的复选框)|\<@spring.formCheckboxes path, options, separator, attributes/\> | +|`formCheckbox`(一个复选框)| \<@spring.formCheckbox path, attributes/\> | +|`showErrors`(简化绑定字段的验证错误显示)| \<@spring.showErrors separator, classOrStyle/\> | + +| |在自由标记模板中,`formHiddenInput`和`formPasswordInput`实际上不是<br/>所需的,因为你可以使用正常的`formInput`宏,指定`hidden`或`password`作为`fieldType`参数的值。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +上述任何宏的参数都具有一致的含义: + +* `path`:要绑定到的字段的名称(即“command.name”) + +* `options`:一个`Map`在输入字段中可以选择的所有可用值。映射的键表示从表单发回并绑定到命令对象的值。相对于键存储的映射对象是在表单上向用户显示的标签,并且可以不同于由表单发回的对应值。通常,这样的映射由控制器提供作为参考数据。你可以使用任何`Map`实现,这取决于所需的行为。对于严格排序的映射,可以使用带有合适的`Comparator`的`SortedMap`(例如`TreeMap`),对于应该以插入顺序返回值的任意映射,可以使用`LinkedHashMap`或`LinkedMap`。 + +* `separator`:当多个选项作为独立元素(单选按钮或复选框)可用时,用于分隔列表中每个元素的字符序列(例如`<br>`)。 + +* `attributes`:要包含在 HTML 标记本身中的任意标记或文本的附加字符串。这个字符串在宏的字面上得到了呼应。例如,在“textarea”字段中,你可以提供属性(例如“rows=“5”cols=“60””),也可以传递样式信息,例如“style=“border:1px solid silver”。 + +* `classOrStyle`:对于`showErrors`宏,表示封装每个错误的`span`元素所使用的 CSS 类的名称。如果没有提供任何信息(或者值是空的),则将错误包装在`<b></b>`标记中。 + +下面的小节概述了宏的示例。 + +## 输入字段 + +`formInput`宏接受`path`参数和一个额外的`attributes`参数(在接下来的示例中为空)。宏以及所有其他的窗体生成宏在路径参数上执行隐式 Spring 绑定。在发生新的绑定之前,绑定一直有效,因此`showErrors`宏不需要再次传递路径参数——它在上次创建绑定的字段上进行操作。 + +`showErrors`宏接受一个分隔符参数(用于分隔给定字段上的多个错误的字符),还接受第二个参数——这次是一个类名或样式属性。请注意,Freemarker 可以为属性参数指定默认值。下面的示例展示了如何使用`formInput`和`showErrors`宏: + +``` +<@spring.formInput "command.name"/> +<@spring.showErrors "<br>"/> +``` + +下一个示例显示了表单片段的输出,生成了 Name 字段,并在提交了该字段中没有值的表单之后显示了一个验证错误。验证通过 Spring 的验证框架进行。 + +生成的 HTML 类似于以下示例: + +``` +Name: +<input type="text" name="name" value=""> +<br> + <b>required</b> +<br> +<br> +``` + +`formTextarea`宏的工作方式与`formInput`宏相同,并接受相同的参数列表。通常,第二个参数用于为`textarea`传递样式信息或`rows`和`cols`属性。 + +## 选择字段 + +你可以使用四个选择字段宏在 HTML 表单中生成常见的 UI 值选择输入: + +* `formSingleSelect` + +* `formMultiSelect` + +* `formRadioButtons` + +* `formCheckboxes` + +这四个宏中的每一个都接受`Map`选项,这些选项包含表单字段的值和与该值对应的标签。值和标签可以是相同的。 + +下一个例子是 FTL 中的单选按钮。表单支持对象为该字段指定了默认值“london”,因此不需要验证。当呈现表单时,可供选择的整个城市列表将作为模型中的参考数据提供,名称为“CityMap”。下面的清单展示了这个示例: + +``` +... +Town: +<@spring.formRadioButtons "command.address.town", cityMap, ""/><br><br> +``` + +前面的列表呈现了一行单选按钮,在`cityMap`中每个值对应一个单选按钮,并使用`""`的分隔符。不提供其他属性(缺少宏的最后一个参数)。对于映射中的每个键值对,`cityMap`使用相同的`String`。映射的键是表单实际提交的`POST`请求参数。映射值是用户看到的标签。在前面的示例中,给出了三个著名城市的列表和表单支持对象中的默认值,HTML 类似于以下内容: + +``` +Town: +<input type="radio" name="address.town" value="London">London</input> +<input type="radio" name="address.town" value="Paris" checked="checked">Paris</input> +<input type="radio" name="address.town" value="New York">New York</input> +``` + +如果你的应用程序希望通过内部代码(例如)处理城市,则可以创建带有合适密钥的代码映射,如下例所示: + +爪哇 + +``` +protected Map<String, ?> referenceData(HttpServletRequest request) throws Exception { + Map<String, String> cityMap = new LinkedHashMap<>(); + cityMap.put("LDN", "London"); + cityMap.put("PRS", "Paris"); + cityMap.put("NYC", "New York"); + + Map<String, Object> model = new HashMap<>(); + model.put("cityMap", cityMap); + return model; +} +``` + +Kotlin + +``` +protected fun referenceData(request: HttpServletRequest): Map<String, *> { + val cityMap = linkedMapOf( + "LDN" to "London", + "PRS" to "Paris", + "NYC" to "New York" + ) + return hashMapOf("cityMap" to cityMap) +} +``` + +该代码现在产生输出,其中无线电值是相关的代码,但用户仍然可以看到更方便用户的城市名称,如下所示: + +``` +Town: +<input type="radio" name="address.town" value="LDN">London</input> +<input type="radio" name="address.town" value="PRS" checked="checked">Paris</input> +<input type="radio" name="address.town" value="NYC">New York</input> +``` + +###### HTML 转义 + +前面描述的表格宏的默认使用会导致 HTML 元素与 HTML4.01 兼容,并且使用在`web.xml`文件中定义的 HTML 转义的默认值,如 Spring 的 BIND 支持所使用的那样。要使元素与 XHTML 兼容或覆盖默认的 HTML 转义值,你可以在模板中指定两个变量(或者在模型中,它们对模板是可见的)。在模板中指定它们的优点是,它们可以在以后的模板处理中更改为不同的值,从而为表单中的不同字段提供不同的行为。 + +要为标记切换到 XHTML 遵从性,请为名为`xhtmlCompliant`的模型或上下文变量指定一个值`true`,如下例所示: + +``` +<#-- for FreeMarker --> +<#assign xhtmlCompliant = true> +``` + +在处理此指令之后,由 Spring 宏生成的任何元素现在都与 XHTML 兼容。 + +以类似的方式,你可以指定每个字段的 HTML 转义,如下例所示: + +``` +<#-- until this point, default HTML escaping is used --> + +<#assign htmlEscape = true> +<#-- next field will use HTML escaping --> +<@spring.formInput "command.name"/> + +<#assign htmlEscape = false in spring> +<#-- all future fields will be bound with HTML escaping off --> +``` + +#### 1.10.3.Groovy 标记 + +[Groovy 标记模板引擎](http://groovy-lang.org/templating.html#_the_markuptemplateengine)主要用于生成类似 XML 的标记(XML、XHTML、HTML5 和其他标记),但你可以使用它生成任何基于文本的内容。 Spring 框架具有用于使用带有 Groovy 标记的 Spring MVC 的内置集成。 + +| |Groovy 标记模板引擎需要 Groovy2.3.1+。| +|---|---------------------------------------------------------| + +##### Configuration + +下面的示例展示了如何配置 Groovy 标记模板引擎: + +Java + +``` +@Configuration +@EnableWebMvc +public class WebConfig implements WebMvcConfigurer { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.groovy(); + } + + // Configure the Groovy Markup Template Engine... + + @Bean + public GroovyMarkupConfigurer groovyMarkupConfigurer() { + GroovyMarkupConfigurer configurer = new GroovyMarkupConfigurer(); + configurer.setResourceLoaderPath("/WEB-INF/"); + return configurer; + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebMvc +class WebConfig : WebMvcConfigurer { + + override fun configureViewResolvers(registry: ViewResolverRegistry) { + registry.groovy() + } + + // Configure the Groovy Markup Template Engine... + + @Bean + fun groovyMarkupConfigurer() = GroovyMarkupConfigurer().apply { + resourceLoaderPath = "/WEB-INF/" + } +} +``` + +下面的示例展示了如何在 XML 中配置相同的内容: + +``` +<mvc:annotation-driven/> + +<mvc:view-resolvers> + <mvc:groovy/> +</mvc:view-resolvers> + +<!-- Configure the Groovy Markup Template Engine... --> +<mvc:groovy-configurer resource-loader-path="/WEB-INF/"/> +``` + +##### 例子 + +与传统的模板引擎不同,Groovy Markup 依赖于使用 Builder 语法的 DSL。下面的示例展示了 HTML 页面的示例模板: + +``` +yieldUnescaped '<!DOCTYPE html>' +html(lang:'en') { + head { + meta('http-equiv':'"Content-Type" content="text/html; charset=utf-8"') + title('My page') + } + body { + p('This is an example of HTML contents') + } +} +``` + +#### 1.10.4.脚本视图 + +[WebFlux](web-reactive.html#webflux-view-script) + +Spring 框架具有用于使用 Spring MVC 和任何模板库的内置集成,这些模板库可以在[JSR-223](https://www.jcp.org/en/jsr/detail?id=223)Java 脚本引擎之上运行。我们在不同的脚本引擎上测试了以下模板库: + +|脚本库| Scripting Engine | +|----------------------------------------------------------------------------------|-----------------------------------------------------| +|[Handlebars](https://handlebarsjs.com/)|[Nashorn](https://openjdk.java.net/projects/nashorn/)| +|[Mustache](https://mustache.github.io/)|[Nashorn](https://openjdk.java.net/projects/nashorn/)| +|[React](https://facebook.github.io/react/)|[Nashorn](https://openjdk.java.net/projects/nashorn/)| +|[EJS](https://www.embeddedjs.com/)|[Nashorn](https://openjdk.java.net/projects/nashorn/)| +|[ERB](https://www.stuartellis.name/articles/erb/)| [JRuby](https://www.jruby.org) | +|[字符串模板](https://docs.python.org/2/library/string.html#template-strings)| [Jython](https://www.jython.org/) | +|[Kotlin Script templating](https://github.com/sdeleuze/kotlin-script-templating)| [Kotlin](https://kotlinlang.org/) | + +| |集成任何其他脚本引擎的基本规则是,它必须实现“ScriptEngine”和`Invocable`接口。| +|---|------------------------------------------------------------------------------------------------------------------------------| + +##### 所需经费 + +[WebFlux](web-reactive.html#webflux-view-script-dependencies) + +你需要在 Classpath 上安装脚本引擎,其细节因脚本引擎而异: + +* [Nashorn](https://openjdk.java.net/projects/nashorn/)JavaScript 引擎由 Java8+ 提供。强烈推荐使用最新的可用更新版本。 + +* [JRuby](https://www.jruby.org)应该作为 Ruby 支持的依赖项添加。 + +* [Jython](https://www.jython.org)应该作为 Python 支持的依赖项添加。 + +* 对于 Kotlin 脚本支持,应该添加`org.jetbrains.kotlin:kotlin-script-util`依赖项和包含`META-INF/services/javax.script.ScriptEngineFactory`行的`org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory`文件。有关更多详细信息,请参见[this example](https://github.com/sdeleuze/kotlin-script-templating)。 + +你需要有脚本模板库。实现 JavaScript 的一种方法是通过[WebJars](https://www.webjars.org/)。 + +##### 脚本模板 + +[WebFlux](web-reactive.html#webflux-script-integrate) + +你可以声明一个`ScriptTemplateConfigurer` Bean 来指定要使用的脚本引擎、要加载的脚本文件、调用什么函数来呈现模板,等等。下面的示例使用了 Mustache 模板和 Nashorn JavaScript 引擎: + +Java + +``` +@Configuration +@EnableWebMvc +public class WebConfig implements WebMvcConfigurer { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.scriptTemplate(); + } + + @Bean + public ScriptTemplateConfigurer configurer() { + ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer(); + configurer.setEngineName("nashorn"); + configurer.setScripts("mustache.js"); + configurer.setRenderObject("Mustache"); + configurer.setRenderFunction("render"); + return configurer; + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebMvc +class WebConfig : WebMvcConfigurer { + + override fun configureViewResolvers(registry: ViewResolverRegistry) { + registry.scriptTemplate() + } + + @Bean + fun configurer() = ScriptTemplateConfigurer().apply { + engineName = "nashorn" + setScripts("mustache.js") + renderObject = "Mustache" + renderFunction = "render" + } +} +``` + +下面的示例用 XML 展示了相同的排列方式: + +``` +<mvc:annotation-driven/> + +<mvc:view-resolvers> + <mvc:script-template/> +</mvc:view-resolvers> + +<mvc:script-template-configurer engine-name="nashorn" render-object="Mustache" render-function="render"> + <mvc:script location="mustache.js"/> +</mvc:script-template-configurer> +``` + +对于 Java 和 XML 配置,控制器看起来没有什么不同,如下例所示: + +Java + +``` +@Controller +public class SampleController { + + @GetMapping("/sample") + public String test(Model model) { + model.addAttribute("title", "Sample title"); + model.addAttribute("body", "Sample body"); + return "template"; + } +} +``` + +Kotlin + +``` +@Controller +class SampleController { + + @GetMapping("/sample") + fun test(model: Model): String { + model["title"] = "Sample title" + model["body"] = "Sample body" + return "template" + } +} +``` + +下面的示例展示了小胡子模板: + +``` +<html> + <head> + <title>{{title}} + + +

{{body}}

+ + +``` + +使用以下参数调用呈现函数: + +* `String template`:模板内容 + +* `Map model`:视图模型 + +* `RenderingContext renderingContext`:[“渲染环境”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/servlet/view/script/RenderingContext.html),用于访问应用程序上下文、区域设置、模板加载器和 URL(自 5.0 起) + +`Mustache.render()`与此签名原生兼容,因此你可以直接调用它。 + +如果模板技术需要进行一些定制,那么可以提供一个实现定制呈现功能的脚本。例如,[Handlerbars](https://handlebarsjs.com)在使用模板之前需要对其进行编译,并且需要[polyfill](https://en.wikipedia.org/wiki/Polyfill)来模拟服务器端脚本引擎中不可用的一些浏览器功能。 + +下面的示例展示了如何做到这一点: + +Java + +``` +@Configuration +@EnableWebMvc +public class WebConfig implements WebMvcConfigurer { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.scriptTemplate(); + } + + @Bean + public ScriptTemplateConfigurer configurer() { + ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer(); + configurer.setEngineName("nashorn"); + configurer.setScripts("polyfill.js", "handlebars.js", "render.js"); + configurer.setRenderFunction("render"); + configurer.setSharedEngine(false); + return configurer; + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebMvc +class WebConfig : WebMvcConfigurer { + + override fun configureViewResolvers(registry: ViewResolverRegistry) { + registry.scriptTemplate() + } + + @Bean + fun configurer() = ScriptTemplateConfigurer().apply { + engineName = "nashorn" + setScripts("polyfill.js", "handlebars.js", "render.js") + renderFunction = "render" + isSharedEngine = false + } +} +``` + +| |当使用非线程安全的
脚本引擎时,需要将`sharedEngine`属性设置为`false`,该脚本引擎的模板库不是为并发而设计的,例如在 Nashorn 上运行的手柄或
React。在那种情况下,Java SE8Update60 是必需的,由于[this bug](https://bugs.openjdk.java.net/browse/JDK-8076099),但一般情况下
推荐在任何情况下使用最近发布的 Java SE 补丁。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +`polyfill.js`只定义了处理栏正常运行所需的`window`对象,如下所示: + +``` +var window = {}; +``` + +这个基本的`render.js`实现在使用模板之前对其进行编译。生产就绪的实现还应该存储任何重用的缓存模板或预编译模板。你可以在脚本端这样做(并处理你需要的任何定制——例如,管理模板引擎配置)。下面的示例展示了如何做到这一点: + +``` +function render(template, model) { + var compiledTemplate = Handlebars.compile(template); + return compiledTemplate(model); +} +``` + +查看 Spring Framework Unit 测试,[Java](https://github.com/spring-projects/spring-framework/tree/main/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script)和[resources](https://github.com/spring-projects/spring-framework/tree/main/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script),以获得更多配置示例。 + +#### 1.10.5.JSP 和 JSTL + +Spring 框架具有用于使用 Spring MVC 与 JSP 和 JSTL 的内置集成。 + +##### 视图解析器 + +在使用 JSP 进行开发时,通常声明`InternalResourceViewResolver` Bean。 + +`InternalResourceViewResolver`可用于向任何 Servlet 资源进行调度,但特别是用于 JSP。作为一种最佳实践,我们强烈建议将你的 JSP 文件放置在`'WEB-INF'`目录下的目录中,这样客户端就不能直接访问它。 + +``` + + + + + +``` + +##### JSP 与 JSTL + +当使用 JSP 标准标记库时,你必须使用一个特殊的视图类“JSTLVIEW”,因为 JSTL 需要做一些准备,才能使用诸如 i18n 特性之类的功能。 + +##### Spring 的 JSP 标记库 + +Spring 提供请求参数到命令对象的数据绑定,如前面几章所描述的那样。 Spring 为了促进结合那些数据绑定特性的 JSP 页面的开发,提供了一些使事情变得更容易的标记。 Spring 所有标记都具有 HTML 转义功能,以启用或禁用字符的转义。 + +`spring.tld`标记库描述符包含在`spring-webmvc.jar`中。有关单个标记的全面引用,请浏览[API reference](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/servlet/tags/package-summary.html#package.description)或查看标记库描述。 + +##### Spring 的表单标记库 + +在版本 2.0 中, Spring 提供了一组全面的数据绑定感知标记,用于在使用 JSP 和 Spring Web MVC 时处理表单元素。每个标记都提供了对其对应的 HTML 标记对应物的属性集的支持,使标记变得熟悉且使用起来直观。标记生成的 HTML 兼容 HTML4.01/XHTML1.0。 + +与其他表单/输入标记库不同, Spring 的表单标记库与 Spring Web MVC 集成在一起,使标记能够访问你的控制器处理的命令对象和引用数据。正如我们在下面的示例中所示,表单标记使 JSP 更易于开发、读取和维护。 + +我们通过表单标记,并查看每个标记如何使用的示例。我们已经包含了生成的 HTML 片段,其中某些标记需要进一步的注释。 + +###### Configuration + +表单标记库捆绑在`spring-webmvc.jar`中。库描述符称为`spring-form.tld`。 + +要使用这个库中的标记,请在 JSP 页面的顶部添加以下指令: + +``` +<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> +``` + +其中`form`是你要为这个库中的标记使用的标记名称前缀。 + +###### 表单标签 + +此标记呈现 HTML“Form”元素,并将绑定路径公开给内部标记以进行绑定。它将命令对象放在`PageContext`中,以便可以通过内部标记访问命令对象。这个库中的所有其他标记都是“form”标记的嵌套标记。 + +假设我们有一个名为`User`的域对象。它是一个 JavaBean,具有`firstName`和`lastName`等属性。我们可以使用它作为表单控制器的表单支持对象,它返回`form.jsp`。下面的示例显示了`form.jsp`可能是什么样子的: + +``` + + + + + + + + + + + + + +
First Name:
Last Name:
+ +
+
+``` + +由页面控制器从放置在`PageContext`中的命令对象检索`firstName`和`lastName`值。继续阅读,以查看更多关于内部标记如何与`form`标记一起使用的复杂示例。 + +下面的清单显示了生成的 HTML,它看起来像一个标准表单: + +``` +
+ + + + + + + + + + + + +
First Name:
Last Name:
+ +
+
+``` + +前面的 JSP 假设表单支持对象的变量名为“command”。如果你已将表单备份对象以另一个名称(肯定是最佳实践)放入模型中,则可以将表单绑定到已命名的变量,如下例所示: + +``` + + + + + + + + + + + + + +
First Name:
Last Name:
+ +
+
+``` + +###### `input`标签 + +默认情况下,此标记呈现带有绑定值和`type='text'`的 HTML`input`元素。有关此标记的示例,请参见[The Form Tag](#mvc-view-jsp-formtaglib-formtag)。也可以使用 HTML5 特定的类型,例如`email`、`tel`、`date`等。 + +###### `checkbox`标签 + +此标记呈现 HTML`input`标记,其`type`设置为`checkbox`。 + +假设我们的`User`具有首选项,例如订阅时事通讯和一系列爱好。下面的示例显示了`Preferences`类: + +Java + +``` +public class Preferences { + + private boolean receiveNewsletter; + private String[] interests; + private String favouriteWord; + + public boolean isReceiveNewsletter() { + return receiveNewsletter; + } + + public void setReceiveNewsletter(boolean receiveNewsletter) { + this.receiveNewsletter = receiveNewsletter; + } + + public String[] getInterests() { + return interests; + } + + public void setInterests(String[] interests) { + this.interests = interests; + } + + public String getFavouriteWord() { + return favouriteWord; + } + + public void setFavouriteWord(String favouriteWord) { + this.favouriteWord = favouriteWord; + } +} +``` + +Kotlin + +``` +class Preferences( + var receiveNewsletter: Boolean, + var interests: StringArray, + var favouriteWord: String +) +``` + +相应的`form.jsp`可能如下所示: + +``` + + + + + <%-- Approach 1: Property is of type java.lang.Boolean --%> + + + + + + <%-- Approach 2: Property is of an array or of type java.util.Collection --%> + + + + + + <%-- Approach 3: Property is of type java.lang.Object --%> + + +
Subscribe to newsletter?:
Interests: + Quidditch: + Herbology: + Defence Against the Dark Arts: +
Favourite Word: + Magic: +
+
+``` + +对于`checkbox`标记,有三种方法可以满足你的所有复选框需求。 + +* 方法一:当绑定值类型为`java.lang.Boolean`时,如果绑定值为`true`,则 `input(复选框)` 被标记为`checked`。`value`属性对应于`setValue(Object)`值属性的解析值。 + +* 方法二:当绑定值类型为`array`或`java.util.Collection`时,如果配置的`setValue(Object)`值存在于绑定值`Collection`中,则输入(复选框)` 标记为`checked`。 + +* 方法三:对于任何其他绑定值类型,如果配置的`setValue(Object)`等于绑定值,则将`input(checkbox)`标记为 `checked’。 + +请注意,无论采用哪种方法,都会生成相同的 HTML 结构。以下 HTML 片段定义了一些复选框: + +``` + + Interests: + + Quidditch: + + Herbology: + + Defence Against the Dark Arts: + + + +``` + +你可能不希望在每个复选框之后看到额外的隐藏字段。当 HTML 页面中的复选框未被选中时,其值不会在表单提交后作为 HTTP 请求参数的一部分发送到服务器,因此我们需要在 HTML 中解决此问题,以使 Spring 表单数据绑定工作。“复选框”标记遵循现有的 Spring 惯例,即为每个复选框包含一个以下划线为前缀的隐藏参数。通过这样做,你可以有效地告诉 Spring“复选框在表单中是可见的,并且我希望表单数据绑定到的对象能够反映复选框的状态,无论发生什么情况。” + +###### `checkboxes`标签 + +此标记将呈现多个 HTML`input`标记,并将`type`设置为`checkbox`。 + +本节以前面的`checkbox`标记部分的示例为基础。有时,你不希望在 JSP 页面中列出所有可能的爱好。你更愿意在运行时提供一个可用选项的列表,并将其传递给标记。这就是`checkboxes`标记的目的。你可以传入`Array`、`List`或`Map`属性中包含可用选项的`items`。通常,绑定属性是一个集合,以便它可以保存用户选择的多个值。下面的示例展示了使用此标记的 JSP: + +``` + + + + + + +
Interests: + <%-- Property is of an array or of type java.util.Collection --%> + +
+
+``` + +这个示例假设`interestList`是一个`List`,作为包含要从中选择的值的字符串的模型属性可用。如果使用`Map`,则使用 map entry 键作为值,并使用 map entry 的值作为要显示的标签。你还可以使用自定义对象,在该对象中,你可以通过使用`itemValue`提供该值的属性名称,并通过使用`itemLabel`提供标签。 + +###### tag`radiobutton` + +此标记呈现 HTML`input`元素,其`type`设置为`radio`。 + +典型的使用模式涉及绑定到相同属性但具有不同值的多个标记实例,如下例所示: + +``` + + Sex: + + Male:
+ Female: + + +``` + +###### tag`radiobuttons` + +此标记呈现多个 HTML`input`元素,并将`type`设置为`radio`。 + +与[“复选框”标签](#mvc-view-jsp-formtaglib-checkboxestag)一样,你可能希望将可用选项作为运行时变量传递。对于这种用法,你可以使用“RadioButtons”标签。你传入一个`Array`、一个`List`或一个`Map`,它包含`items`属性中的可用选项。如果使用`Map`,则使用 map entry 键作为值,并使用 map entry 的值作为要显示的标签。你还可以使用自定义对象,在该对象中,你可以通过使用`itemValue`提供该值的属性名称,并通过使用`itemLabel`提供标签,如下例所示: + +``` + + Sex: + + +``` + +###### tag`password` + +此标记呈现 HTML`input`标记,其类型设置为`password`,并具有绑定值。 + +``` + + Password: + + + + +``` + +请注意,默认情况下,不会显示密码值。如果确实希望显示密码值,可以将`showPassword`属性的值设置为 `true’,如下例所示: + +``` + + Password: + + + + +``` + +###### tag`select` + +此标记呈现 HTML“select”元素。它支持与所选选项的数据绑定,以及使用嵌套的`option`和`options`标记。 + +假设`User`有一个技能列表。相应的 HTML 可以如下所示: + +``` + + Skills: + + +``` + +如果`User’s`技能是草药学的,那么“技能”行的 HTML 源可以如下所示: + +``` + + Skills: + + + + +``` + +###### tag`option` + +此标记呈现 HTML`option`元素。它基于绑定值设置`selected`。下面的 HTML 显示了它的典型输出: + +``` + + House: + + + + + + + + + +``` + +如果`User’s`房子在格兰芬多,那么“房子”行的 HTML 源代码如下: + +``` + + House: + + + + +``` + +|**1**|注意添加了一个`selected`属性。| +|-----|--------------------------------------------| + +###### tag`options` + +此标记呈现 HTML`option`元素的列表。它基于绑定值设置`selected`属性。下面的 HTML 显示了它的典型输出: + +``` + + Country: + + + + + + + +``` + +如果`User`位于 UK 中,则“country”行的 HTML 源代码如下: + +``` + + Country: + + + + +``` + +|**1**|注意添加了一个`selected`属性。| +|-----|--------------------------------------------| + +正如前面的示例所示,将`option`标记与`options`标记合并使用,会生成相同的标准 HTML,但允许你在 JSP 中显式地指定一个值,该值仅用于显示(在它所属的位置),例如示例中的默认字符串:“--请选择”。 + +`items`属性通常填充有条目对象的集合或数组。`itemValue’和`itemLabel`引用这些条目对象的 Bean 属性(如果指定的话)。否则,项目对象本身就会变成字符串。或者,你可以指定项目的`Map`,在这种情况下,映射键被解释为选项值,映射值对应于选项标签。如果`itemValue`或`itemLabel`(或两者兼而有之)恰好也被指定,则 Item Value 属性将应用于 map 键,而 Item Label 属性将应用于 map 值。 + +###### tag`textarea` + +此标记呈现 HTML`textarea`元素。下面的 HTML 显示了它的典型输出: + +``` + + Notes: + + + +``` + +###### `hidden`标签 + +此标记将呈现一个 HTML`input`标记,其`type`设置为`hidden`,并具有绑定值。要提交未绑定的隐藏值,请使用 HTML`input`标记,并将`type`设置为`hidden`。下面的 HTML 显示了它的典型输出: + +``` + +``` + +如果我们选择将`house`值作为隐藏的值提交,则 HTML 将如下所示: + +``` + +``` + +###### tag`errors` + +此标记在 HTML`span`元素中呈现字段错误。它提供对在控制器中创建的错误或由与控制器关联的任何验证器创建的错误的访问。 + +假设我们希望在提交表单后显示`firstName`和`lastName`字段的所有错误消息。对于`User`类的实例,我们有一个名为`UserValidator`的验证器,如下例所示: + +Java + +``` +public class UserValidator implements Validator { + + public boolean supports(Class candidate) { + return User.class.isAssignableFrom(candidate); + } + + public void validate(Object obj, Errors errors) { + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "required", "Field is required."); + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName", "required", "Field is required."); + } +} +``` + +Kotlin + +``` +class UserValidator : Validator { + + override fun supports(candidate: Class<*>): Boolean { + return User::class.java.isAssignableFrom(candidate) + } + + override fun validate(obj: Any, errors: Errors) { + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "required", "Field is required.") + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "lastName", "required", "Field is required.") + } +} +``` + +`form.jsp`可以如下: + +``` + + + + + + <%-- Show errors for firstName field --%> + + + + + + + <%-- Show errors for lastName field --%> + + + + + +
First Name:
Last Name:
+ +
+
+``` + +如果我们提交一个在`firstName`和`lastName`字段中具有空值的表单,则 HTML 将如下所示: + +``` +
+ + + + + <%-- Associated errors to firstName field displayed --%> + + + + + + + <%-- Associated errors to lastName field displayed --%> + + + + + +
First Name:Field is required.
Last Name:Field is required.
+ +
+
+``` + +如果我们想要显示给定页面的整个错误列表,该怎么办?下一个示例显示`errors`标记还支持一些基本的通配符功能。 + +* `path="*"`:显示所有错误。 + +* `path="lastName"`:显示与`lastName`字段关联的所有错误。 + +* 如果省略`path`,则只显示对象错误。 + +下面的示例在页面顶部显示错误列表,然后在字段旁边显示特定于字段的错误: + +``` + + + + + + + + + + + + + + + + +
First Name:
Last Name:
+ +
+
+``` + +HTML 将如下所示: + +``` +
+ Field is required.
Field is required.
+ + + + + + + + + + + + + + + +
First Name:Field is required.
Last Name:Field is required.
+ +
+
+``` + +`spring-form.tld`标记库描述符包含在`spring-webmvc.jar`中。有关单个标记的全面引用,请浏览[API reference](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/servlet/tags/form/package-summary.html#package.description)或查看标记库描述。 + +###### HTTP 方法转换 + +REST 的一个关键原则是使用“统一接口”。这意味着所有资源(URL)都可以通过使用相同的四种 HTTP 方法进行操作:GET、PUT、POST 和 DELETE。对于每种方法,HTTP 规范都定义了确切的语义。例如,get 应该始终是一个安全的操作,这意味着它没有副作用,而 put 或 delete 应该是幂等的,这意味着你可以一遍又一遍地重复这些操作,但最终结果应该是相同的。虽然 HTTP 定义了这四种方法,但 HTML 只支持两种:GET 和 POST。幸运的是,有两种可能的解决方法:你可以使用 JavaScript 来执行 PUT 或 DELETE,或者你可以使用“Real”方法作为附加参数(以 HTML 形式的隐藏输入字段建模)来执行 POST。 Spring 的`HiddenHttpMethodFilter`使用了后一种技巧。 Servlet 该过滤器是一个普通的过滤器,因此,它可以与任何 Web 框架(而不仅仅是 Spring MVC)组合使用。将此筛选器添加到你的 web.xml 中,带有隐藏`method`参数的 POST 将被转换为相应的 HTTP 方法请求。 + +为了支持 HTTP 方法转换,更新了 Spring MVC 表单标记以支持设置 HTTP 方法。例如,以下片段来自宠物诊所样本: + +``` + +

+
+``` + +前面的示例执行 HTTP POST,在请求参数后面隐藏“real”delete 方法。下面的示例显示了在 web.xml 中定义的`HiddenHttpMethodFilter`来选择它: + +``` + + httpMethodFilter + org.springframework.web.filter.HiddenHttpMethodFilter + + + + httpMethodFilter + petclinic + +``` + +下面的示例显示了相应的`@Controller`方法: + +Java + +``` +@RequestMapping(method = RequestMethod.DELETE) +public String deletePet(@PathVariable int ownerId, @PathVariable int petId) { + this.clinic.deletePet(petId); + return "redirect:/owners/" + ownerId; +} +``` + +Kotlin + +``` +@RequestMapping(method = [RequestMethod.DELETE]) +fun deletePet(@PathVariable ownerId: Int, @PathVariable petId: Int): String { + clinic.deletePet(petId) + return "redirect:/owners/$ownerId" +} +``` + +###### HTML5 标签 + +Spring 表单标记库允许输入动态属性,这意味着你可以输入任何 HTML5 特定的属性。 + +表单`input`标记支持输入`text`以外的类型属性。这旨在允许呈现新的 HTML5 特定输入类型,例如`email`、`date`、`range’和其他类型。请注意,不需要输入`type='text'`,因为`text`是默认类型。 + +#### 1.10.6.瓦片 + +你可以在使用 Spring 的 Web 应用程序中集成图块——就像任何其他视图技术一样。这一节以一种宽泛的方式描述了如何做到这一点。 + +| |这一部分主要介绍 Spring 在 `org.springframework.web. Servlet.view.tiles3` 包中对 tiles 版本 3 的支持。| +|---|-------------------------------------------------------------------------------------------------------------------------| + +##### 依赖关系 + +为了能够使用磁贴,你必须在项目中添加对磁贴版本 3.0.1 或更高版本和[它的传递依赖关系](https://tiles.apache.org/framework/dependency-management.html)的依赖关系。 + +##### 配置 + +为了能够使用磁贴,你必须使用包含定义的文件来配置它(有关定义和其他磁贴概念的基本信息,请参见[https://tiles.apache.org](https://tiles.apache.org))。在 Spring 中,这是通过使用`TilesConfigurer`来完成的。下面的`ApplicationContext`配置示例展示了如何这样做: + +``` + + + + /WEB-INF/defs/general.xml + /WEB-INF/defs/widgets.xml + /WEB-INF/defs/administrator.xml + /WEB-INF/defs/customer.xml + /WEB-INF/defs/templates.xml + + + +``` + +前面的示例定义了五个包含定义的文件。这些文件都位于`WEB-INF/defs`目录中。在初始化`WebApplicationContext`时,文件被加载,定义工厂被初始化。完成此操作后,定义文件中包含的磁贴可以用作 Spring Web 应用程序中的视图。为了能够使用这些视图,与 Spring 中的任何其他视图技术一样,你必须有一个`ViewResolver`:通常是一个方便的`TilesViewResolver`。 + +你可以通过添加下划线和区域设置来指定特定于区域设置的磁贴定义,如下例所示: + +``` + + + + /WEB-INF/defs/tiles.xml + /WEB-INF/defs/tiles_fr_FR.xml + + + +``` + +在前面的配置中,`tiles_fr_FR.xml`用于使用`fr_FR`语言环境的请求,默认情况下使用`tiles.xml`。 + +| |由于下划线是用来指示区域设置的,因此我们建议不要在文件名中使用
,否则将其用于瓷砖定义。| +|---|------------------------------------------------------------------------------------------------------------------------------------| + +###### `UrlBasedViewResolver` + +`UrlBasedViewResolver`为它必须解析的每个视图实例化给定的`viewClass`。下面的 Bean 定义了`UrlBasedViewResolver`: + +``` + + + +``` + +###### `SimpleSpringPreparerFactory`和`SpringBeanPreparerFactory` + +作为一种高级特性, Spring 还支持两种特殊的贴片`PreparerFactory`实现方式。有关如何在瓷砖定义文件中使用“ViewPreparer”引用的详细信息,请参见瓷砖文档。 + +你可以指定`SimpleSpringPreparerFactory`以基于指定的准备程序类的 AutoWire`ViewPreparer`实例,应用 Spring 的容器回调以及应用已配置的 Spring BeanPostProcessors。如果 Spring 的上下文范围注释配置已被激活,则将自动检测并应用`ViewPreparer`类中的注释。请注意,这需要在磁贴定义文件中的准备程序类,就像默认的`PreparerFactory`所做的那样。 + +你可以指定`SpringBeanPreparerFactory`来对指定的编制者名称(而不是类)进行操作,从而从 DispatcherServlet 的应用程序上下文中获得相应的 Spring Bean。在这种情况下,完整的 Bean 创建过程处于 Spring 应用程序上下文的控制中,允许使用显式依赖注入配置、作用域 bean 等。请注意,你需要为每个准备者名称定义一个 Spring Bean 定义(如你的磁贴定义中所使用的)。下面的示例展示了如何在`TilesConfigurer` Bean 上定义`SpringBeanPreparerFactory`属性: + +``` + + + + /WEB-INF/defs/general.xml + /WEB-INF/defs/widgets.xml + /WEB-INF/defs/administrator.xml + /WEB-INF/defs/customer.xml + /WEB-INF/defs/templates.xml + + + + + + + +``` + +#### 1.10.7.RSS 和 Atom + +`AbstractAtomFeedView`和`AbstractRssFeedView`都继承自 `AbstractFeedView’基类,并分别用于提供 Atom 和 RSS 提要视图。它们基于[ROME](https://rometools.github.io/rome/)项目,位于包`org.springframework.web.servlet.view.feed`中。 + +`AbstractAtomFeedView`要求你实现`buildFeedEntries()`方法,并可选地覆盖`buildFeedMetadata()`方法(默认实现为空)。下面的示例展示了如何做到这一点: + +爪哇 + +``` +public class SampleContentAtomView extends AbstractAtomFeedView { + + @Override + protected void buildFeedMetadata(Map model, + Feed feed, HttpServletRequest request) { + // implementation omitted + } + + @Override + protected List buildFeedEntries(Map model, + HttpServletRequest request, HttpServletResponse response) throws Exception { + // implementation omitted + } +} +``` + +Kotlin + +``` +class SampleContentAtomView : AbstractAtomFeedView() { + + override fun buildFeedMetadata(model: Map, + feed: Feed, request: HttpServletRequest) { + // implementation omitted + } + + override fun buildFeedEntries(model: Map, + request: HttpServletRequest, response: HttpServletResponse): List { + // implementation omitted + } +} +``` + +类似的要求也适用于实现`AbstractRssFeedView`,如下例所示: + +爪哇 + +``` +public class SampleContentRssView extends AbstractRssFeedView { + + @Override + protected void buildFeedMetadata(Map model, + Channel feed, HttpServletRequest request) { + // implementation omitted + } + + @Override + protected List buildFeedItems(Map model, + HttpServletRequest request, HttpServletResponse response) throws Exception { + // implementation omitted + } +} +``` + +Kotlin + +``` +class SampleContentRssView : AbstractRssFeedView() { + + override fun buildFeedMetadata(model: Map, + feed: Channel, request: HttpServletRequest) { + // implementation omitted + } + + override fun buildFeedItems(model: Map, + request: HttpServletRequest, response: HttpServletResponse): List { + // implementation omitted + } +} +``` + +如果你需要访问区域设置,`buildFeedItems()`和`buildFeedEntries()`方法将传入 HTTP 请求。HTTP 响应仅在设置 Cookie 或其他 HTTP 头时传入。方法返回后,提要将自动写入响应对象。 + +有关创建 Atom 视图的示例,请参见 ALEFArendsen Spring 团队博客[entry](https://spring.io/blog/2009/03/16/adding-an-atom-view-to-an-application-using-spring-s-rest-support)。 + +#### 1.10.8.PDF 和 Excel + +Spring 提供了返回 HTML 以外的输出的方法,包括 PDF 和 Excel 电子表格。这一节描述了如何使用这些特性。 + +##### 文档视图介绍 + +HTML 页面并不总是用户查看模型输出的最佳方式, Spring 使得从模型数据动态生成 PDF 文档或 Excel 电子表格变得简单。该文档是视图,并从服务器以正确的内容类型进行流媒体传输,以(希望)使客户端 PC 能够运行其电子表格或 PDF 查看器应用程序作为响应。 + +为了使用 Excel 视图,你需要将 Apache POI 库添加到 Classpath 中。为了生成 PDF,你需要添加(最好是)OpenPDF 库。 + +| |如果可能的话,你应该使用底层文档生成库的最新版本
。特别是,我们强烈推荐 OpenPDF(例如,OpenPDF1.2.12)
而不是过时的原始 iText2.1.7,因为 OpenPDF 是积极维护的,
修复了不可信 PDF 内容的一个重要漏洞。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +##### PDF 视图 + +一个简单的 PDF 视图可以扩展“org.springframework.web. Servlet.view.document.abstractpdfview”并实现“buildpdfdocument()”方法,如下例所示: + +爪哇 + +``` +public class PdfWordList extends AbstractPdfView { + + protected void buildPdfDocument(Map model, Document doc, PdfWriter writer, + HttpServletRequest request, HttpServletResponse response) throws Exception { + + List words = (List) model.get("wordList"); + for (String word : words) { + doc.add(new Paragraph(word)); + } + } +} +``` + +Kotlin + +``` +class PdfWordList : AbstractPdfView() { + + override fun buildPdfDocument(model: Map, doc: Document, writer: PdfWriter, + request: HttpServletRequest, response: HttpServletResponse) { + + val words = model["wordList"] as List + for (word in words) { + doc.add(Paragraph(word)) + } + } +} +``` + +控制器可以从外部视图定义(通过名称引用它)返回这样的视图,也可以从处理程序方法返回`View`实例。 + +##### Excel 视图 + +自 Spring Framework4.2 以来,`org.springframework.web. Servlet.view.document.AbstractXlsView’被提供为 Excel 视图的基类。它以 Apache POI 为基础,有专门的子类(`AbstractXlsxView’和`AbstractXlsxStreamingView`)取代过时的`AbstractExcelView`类。 + +编程模型类似于`AbstractPdfView`,以`buildExcelDocument()`作为中心模板方法,控制器能够从外部定义(通过名称)返回这样的视图,或者作为处理程序方法的`View`实例。 + +#### 1.10.9.Jackson + +[WebFlux](web-reactive.html#webflux-view-httpmessagewriter) + +Spring 提供对 JacksonJSON 库的支持。 + +##### 基于 Jackson 的 JSON MVC 视图 + +[WebFlux](web-reactive.html#webflux-view-httpmessagewriter) + +`MappingJackson2JsonView`使用 Jackson 库的`ObjectMapper`将响应内容呈现为 JSON。默认情况下,模型映射的全部内容(除了特定于框架的类)都被编码为 JSON。对于需要对映射的内容进行筛选的情况,可以指定一组特定的模型属性来使用`modelKeys`属性进行编码。你还可以使用`extractValueFromSingleKeyModel`属性,将单键模型中的值直接提取和序列化,而不是作为模型属性的映射。 + +你可以根据需要使用 Jackson 提供的注释来定制 JSON 映射。当你需要进一步的控制时,你可以通过`ObjectMapper`属性注入一个自定义的`ObjectMapper`,用于需要为特定类型提供自定义 JSON 序列化器和反序列化器的情况。 + +##### 基于 Jackson 的 XML 视图 + +[WebFlux](web-reactive.html#webflux-view-httpmessagewriter) + +`MappingJackson2XmlView`使用[JacksonXML 扩展的](https://github.com/FasterXML/jackson-dataformat-xml)`XmlMapper`将响应内容呈现为 XML。如果模型包含多个条目,你应该使用`modelKey` Bean 属性显式地设置要序列化的对象。如果模型包含单个条目,则自动对其进行序列化。 + +你可以根据需要使用 JAXB 或 Jackson 提供的注释来定制 XML 映射。当需要进一步的控制时,可以通过`ObjectMapper`属性注入自定义`XmlMapper`,用于需要为特定类型提供序列化器和反序列化器的自定义 XML。 + +#### 1.10.10.XML 编组 + +`MarshallingView`使用 XML`Marshaller`(在`org.springframework.oxm`包中定义)将响应内容呈现为 XML。可以使用`MarshallingView`实例的`modelKey` Bean 属性显式地设置要编组的对象。或者,该视图对所有模型属性进行迭代,并封送`Marshaller`所支持的第一个类型。有关 `org.springframework.OXM’包中的功能的更多信息,请参见[使用 O/X 映射器编组 XML](data-access.html#oxm)。 + +#### 1.10.11.XSLT 视图 + +XSLT 是一种 XML 转换语言,在 Web 应用程序中作为一种视图技术很受欢迎。如果你的应用程序自然地处理 XML,或者你的模型可以很容易地转换为 XML,那么 XSLT 作为一种视图技术是一个不错的选择。下面的部分展示了如何生成 XML 文档作为模型数据,并在 Spring Web MVC 应用程序中使用 XSLT 对其进行转换。 + +Spring 这个示例是一个简单的应用程序,它在“controller”中创建一个单词列表,并将它们添加到模型映射中。将返回映射以及 XSLT 视图的视图名称。有关 Spring Web MVC 的“控制器”接口的详细信息,请参见[带注释的控制器](#mvc-controller)。XSLT 控制器将单词列表转换为一个简单的 XML 文档,以便进行转换。 + +##### 豆子 + +对于简单的 Spring Web 应用程序,配置是标准的:MVC 配置必须定义`XsltViewResolver` Bean 和常规的 MVC 注释配置。下面的示例展示了如何做到这一点: + +爪哇 + +``` +@EnableWebMvc +@ComponentScan +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Bean + public XsltViewResolver xsltViewResolver() { + XsltViewResolver viewResolver = new XsltViewResolver(); + viewResolver.setPrefix("/WEB-INF/xsl/"); + viewResolver.setSuffix(".xslt"); + return viewResolver; + } +} +``` + +Kotlin + +``` +@EnableWebMvc +@ComponentScan +@Configuration +class WebConfig : WebMvcConfigurer { + + @Bean + fun xsltViewResolver() = XsltViewResolver().apply { + setPrefix("/WEB-INF/xsl/") + setSuffix(".xslt") + } +} +``` + +##### 控制器 + +我们还需要一个封装我们的字生成逻辑的控制器。 + +控制器逻辑封装在`@Controller`类中,处理程序方法定义如下: + +爪哇 + +``` +@Controller +public class XsltController { + + @RequestMapping("/") + public String home(Model model) throws Exception { + Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); + Element root = document.createElement("wordList"); + + List words = Arrays.asList("Hello", "Spring", "Framework"); + for (String word : words) { + Element wordNode = document.createElement("word"); + Text textNode = document.createTextNode(word); + wordNode.appendChild(textNode); + root.appendChild(wordNode); + } + + model.addAttribute("wordList", root); + return "home"; + } +} +``` + +Kotlin + +``` +import org.springframework.ui.set + +@Controller +class XsltController { + + @RequestMapping("/") + fun home(model: Model): String { + val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument() + val root = document.createElement("wordList") + + val words = listOf("Hello", "Spring", "Framework") + for (word in words) { + val wordNode = document.createElement("word") + val textNode = document.createTextNode(word) + wordNode.appendChild(textNode) + root.appendChild(wordNode) + } + + model["wordList"] = root + return "home" + } +} +``` + +到目前为止,我们只创建了一个 DOM 文档并将其添加到模型映射中。请注意,你也可以将 XML 文件加载为`Resource`,并使用它来代替自定义 DOM 文档。 + +有一些软件包可以自动对对象图进行“domify”,但是,在 Spring 之内,你可以完全灵活地以你选择的任何方式从你的模型中创建 DOM。这可以防止 XML 转换在模型数据的结构中起到太大的作用,这在使用工具管理 Domification 过程时是一种危险。 + +##### 转换 + +最后,`XsltViewResolver`解析“home”XSLT 模板文件,并将 DOM 文档合并到其中以生成视图。如`XsltViewResolver`配置中所示,XSLT 模板位于`war`目录中的`WEB-INF/xsl`文件中,并以`xslt`文件扩展名结束。 + +下面的示例展示了一个 XSLT 转换: + +``` + + + + + + + + Hello! + +

My First Words

+
    + +
+ + +
+ + +
  • +
    + +
    +``` + +前面的转换呈现为以下 HTML: + +``` + + + + Hello! + + +

    My First Words

    +
      +
    • Hello
    • +
    • Spring
    • +
    • Framework
    • +
    + + +``` + +### 1.11.MVC 配置 + +[WebFlux](web-reactive.html#webflux-config) + +MVC 爪哇 配置和 MVC XML 命名空间提供了适用于大多数应用程序的默认配置,并提供了一个配置 API 来对其进行定制。 + +有关配置 API 中不提供的更高级的自定义,请参见[高级 爪哇 配置](#mvc-config-advanced-java)和[高级 XML 配置](#mvc-config-advanced-xml)。 + +你不需要理解由 MVC 爪哇 配置和 MVC 名称空间创建的底层 bean。如果你想了解更多信息,请参见[Special Bean Types](#mvc-servlet-special-bean-types)和[Web MVC Config](#mvc-servlet-config)。 + +#### 1.11.1.启用 MVC 配置 + +[WebFlux](web-reactive.html#webflux-config-enable) + +在 爪哇 配置中,可以使用`@EnableWebMvc`注释来启用 MVC 配置,如下例所示: + +爪哇 + +``` +@Configuration +@EnableWebMvc +public class WebConfig { +} +``` + +Kotlin + +``` +@Configuration +@EnableWebMvc +class WebConfig +``` + +在 XML 配置中,可以使用``元素来启用 MVC 配置,如下例所示: + +``` + + + + + + +``` + +前面的示例注册了许多 Spring MVC,并适应于 Classpath 上可用的依赖关系(例如,用于 JSON、XML 和其他的有效负载转换器)。 + +#### 1.11.2.MVC 配置 API + +[WebFlux](web-reactive.html#webflux-config-customize) + +在 爪哇 配置中,你可以实现`WebMvcConfigurer`接口,如下例所示: + +爪哇 + +``` +@Configuration +@EnableWebMvc +public class WebConfig implements WebMvcConfigurer { + + // Implement configuration methods... +} +``` + +Kotlin + +``` +@Configuration +@EnableWebMvc +class WebConfig : WebMvcConfigurer { + + // Implement configuration methods... +} +``` + +在 XML 中,你可以检查``的属性和子元素。你可以查看[Spring MVC XML schema](https://schema.spring.io/mvc/spring-mvc.xsd),或者使用 IDE 的代码完成功能来发现哪些属性和子元素是可用的。 + +#### 1.11.3.类型转换 + +[WebFlux](web-reactive.html#webflux-config-conversion) + +默认情况下,安装了各种数字和日期类型的格式化程序,并支持通过字段上的`@NumberFormat`和`@DateTimeFormat`进行定制。 + +要在 爪哇 Config 中注册自定义格式化程序和转换器,请使用以下方法: + +爪哇 + +``` +@Configuration +@EnableWebMvc +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addFormatters(FormatterRegistry registry) { + // ... + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebMvc +class WebConfig : WebMvcConfigurer { + + override fun addFormatters(registry: FormatterRegistry) { + // ... + } +} +``` + +要在 XML Config 中执行相同的操作,请使用以下方法: + +``` + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +Spring 默认情况下,MVC 在解析和格式化日期值时会考虑请求区域设置。这适用于将日期表示为带有“输入”窗体字段的字符串的窗体。但是,对于“日期”和“时间”表单字段,浏览器使用 HTML 规范中定义的固定格式。对于这种情况,日期和时间格式可以定制如下: + +爪哇 + +``` +@Configuration +@EnableWebMvc +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addFormatters(FormatterRegistry registry) { + DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar(); + registrar.setUseIsoFormat(true); + registrar.registerFormatters(registry); + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebMvc +class WebConfig : WebMvcConfigurer { + + override fun addFormatters(registry: FormatterRegistry) { + val registrar = DateTimeFormatterRegistrar() + registrar.setUseIsoFormat(true) + registrar.registerFormatters(registry) + } +} +``` + +| |有关何时使用
    FormatterRegistrar 实现的更多信息,请参见[the `FormatterRegistrar` SPI](core.html#format-FormatterRegistrar-SPI)和`FormattingConversionServiceFactoryBean`。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.11.4.验证 + +[WebFlux](web-reactive.html#webflux-config-validation) + +默认情况下,如果[Bean Validation](core.html#validation-beanvalidation-overview)存在于 Classpath(例如, Hibernate 验证器)上,则将`LocalValidatorFactoryBean`注册为全局[Validator](core.html#validator),以便在控制器方法参数上与`@Valid`和 `validated’一起使用。 + +在 爪哇 配置中,你可以自定义全局`Validator`实例,如下例所示: + +爪哇 + +``` +@Configuration +@EnableWebMvc +public class WebConfig implements WebMvcConfigurer { + + @Override + public Validator getValidator() { + // ... + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebMvc +class WebConfig : WebMvcConfigurer { + + override fun getValidator(): Validator { + // ... + } +} +``` + +下面的示例展示了如何用 XML 实现相同的配置: + +``` + + + + + + +``` + +请注意,你也可以在本地注册`Validator`实现,如下例所示: + +爪哇 + +``` +@Controller +public class MyController { + + @InitBinder + protected void initBinder(WebDataBinder binder) { + binder.addValidators(new FooValidator()); + } +} +``` + +Kotlin + +``` +@Controller +class MyController { + + @InitBinder + protected fun initBinder(binder: WebDataBinder) { + binder.addValidators(FooValidator()) + } +} +``` + +| |如果需要在某个地方注入`LocalValidatorFactoryBean`,请创建一个 Bean 和
    ,将其标记为`@Primary`,以避免与 MVC 配置中声明的值发生冲突。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.11.5.拦截器 + +在 爪哇 配置中,你可以注册拦截器以应用于传入的请求,如下例所示: + +爪哇 + +``` +@Configuration +@EnableWebMvc +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new LocaleChangeInterceptor()); + registry.addInterceptor(new ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**"); + registry.addInterceptor(new SecurityInterceptor()).addPathPatterns("/secure/*"); + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebMvc +class WebConfig : WebMvcConfigurer { + + override fun addInterceptors(registry: InterceptorRegistry) { + registry.addInterceptor(LocaleChangeInterceptor()) + registry.addInterceptor(ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**") + registry.addInterceptor(SecurityInterceptor()).addPathPatterns("/secure/*") + } +} +``` + +下面的示例展示了如何用 XML 实现相同的配置: + +``` + + + + + + + + + + + + +``` + +#### 1.11.6.内容类型 + +[WebFlux](web-reactive.html#webflux-config-content-negotiation) + +你可以配置 Spring MVC 如何从请求中确定所请求的媒体类型(例如,`Accept`头、URL 路径扩展、查询参数和其他)。 + +默认情况下,只勾选`Accept`标头。 + +如果必须使用基于 URL 的内容类型解析,请考虑在路径扩展上使用查询参数策略。有关更多详细信息,请参见[Suffix Match](#mvc-ann-requestmapping-suffix-pattern-match)和[后缀匹配和 RFD](#mvc-ann-requestmapping-rfd)。 + +在 爪哇 配置中,你可以自定义所请求的内容类型分辨率,如下例所示: + +爪哇 + +``` +@Configuration +@EnableWebMvc +public class WebConfig implements WebMvcConfigurer { + + @Override + public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { + configurer.mediaType("json", MediaType.APPLICATION_JSON); + configurer.mediaType("xml", MediaType.APPLICATION_XML); + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebMvc +class WebConfig : WebMvcConfigurer { + + override fun configureContentNegotiation(configurer: ContentNegotiationConfigurer) { + configurer.mediaType("json", MediaType.APPLICATION_JSON) + configurer.mediaType("xml", MediaType.APPLICATION_XML) + } +} +``` + +下面的示例展示了如何用 XML 实现相同的配置: + +``` + + + + + + json=application/json + xml=application/xml + + + +``` + +#### 1.11.7.消息转换器 + +[WebFlux](web-reactive.html#webflux-config-message-codecs) + +在 爪哇 配置中,你可以通过重写[“ConfigureMessageConverters()”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.html#configureMessageConverters-java.util.List-)(以替换由 Spring MVC 创建的默认转换器)或重写[ExtendMessageConverters()](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.html#extendMessageConverters-java.util.List-)(以定制默认转换器或向默认转换器添加额外转换器)来定制`HttpMessageConverter`。 + +下面的示例使用定制的“ObjectMapper”(而不是默认的)来添加 XML 和 JacksonJSON 转换器: + +爪哇 + +``` +@Configuration +@EnableWebMvc +public class WebConfiguration implements WebMvcConfigurer { + + @Override + public void configureMessageConverters(List> converters) { + Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder() + .indentOutput(true) + .dateFormat(new SimpleDateFormat("yyyy-MM-dd")) + .modulesToInstall(new ParameterNamesModule()); + converters.add(new MappingJackson2HttpMessageConverter(builder.build())); + converters.add(new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build())); + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebMvc +class WebConfiguration : WebMvcConfigurer { + + override fun configureMessageConverters(converters: MutableList>) { + val builder = Jackson2ObjectMapperBuilder() + .indentOutput(true) + .dateFormat(SimpleDateFormat("yyyy-MM-dd")) + .modulesToInstall(ParameterNamesModule()) + converters.add(MappingJackson2HttpMessageConverter(builder.build())) + converters.add(MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build())) +``` + +在前面的示例中,[“Jackson2ObjectMapPerBuilder”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.html)用于为`MappingJackson2HttpMessageConverter`和 `mappingjackson2xmlHttpMessageConverter’创建一个公共配置,该配置启用了缩进、自定义的日期格式和[“Jackson-模块-参数-名称”](https://github.com/FasterXML/jackson-module-parameter-names)的注册,从而增加了对访问参数名称的支持(Java8 中添加的一个功能)。 + +这个构建器自定义 Jackson 的默认属性如下: + +* [反序列化 feature.fail_on_unknown_properties’](https://fasterxml.github.io/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/DeserializationFeature.html#FAIL_ON_UNKNOWN_PROPERTIES)已禁用。 + +* [mapperfeature.default_view_inclusion](https://fasterxml.github.io/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/MapperFeature.html#DEFAULT_VIEW_INCLUSION)已禁用。 + +如果在 Classpath 上检测到以下已知模块,它还会自动注册这些模块: + +* [Jackson-数据类型-Joda](https://github.com/FasterXML/jackson-datatype-joda):支持 Joda-time 类型。 + +* [Jackson-数据类型-JSR310](https://github.com/FasterXML/jackson-datatype-jsr310):支持 Java8 日期和时间 API 类型。 + +* [Jackson-数据类型-JDK8](https://github.com/FasterXML/jackson-datatype-jdk8):支持其他 Java8 类型,例如`Optional`。 + +* [`jackson-module-kotlin`](https://github.com/FasterXML/jackson-module-kotlin):对 Kotlin 类和数据类的支持。 + +| |除了[Jackson-数据格式-XML](https://search.maven.org/#search%7Cga%7C1%7Ca%3A%22jackson-dataformat-xml%22)依赖关系外,启用具有 JacksonXML 支持的缩进还需要[“Woodstox-Core-ASL”](https://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22org.codehaus.woodstox%22%20AND%20a%3A%22woodstox-core-asl%22)依赖关系。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +还有其他有趣的 Jackson 模块可供选择: + +* [Jackson-数据类型-货币](https://github.com/zalando/jackson-datatype-money):支持`javax.money`类型(非官方模块)。 + +* [jackson-datatype-hibernate](https://github.com/FasterXML/jackson-datatype-hibernate):支持 Hibernate 特定的类型和属性(包括惰性加载方面)。 + +下面的示例展示了如何用 XML 实现相同的配置: + +``` + + + + + + + + + + + + + + +``` + +#### 1.11.8.视图控制器 + +这是一个用于定义`ParameterizableViewController`的快捷方式,该快捷方式在调用时立即转发到视图。如果在视图生成响应之前没有要运行的 Java 控制器逻辑,则可以在静态情况下使用它。 + +下面的 Java 配置示例将对`/`的请求转发到一个名为`home`的视图: + +Java + +``` +@Configuration +@EnableWebMvc +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addViewControllers(ViewControllerRegistry registry) { + registry.addViewController("/").setViewName("home"); + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebMvc +class WebConfig : WebMvcConfigurer { + + override fun addViewControllers(registry: ViewControllerRegistry) { + registry.addViewController("/").setViewName("home") + } +} +``` + +下面的示例通过使用``元素,实现了与前面示例相同的功能,但使用了 XML: + +``` + +``` + +如果`@RequestMapping`方法被映射到任何 HTTP 方法的 URL,则不能使用视图控制器来处理相同的 URL。这是因为 URL 与带注释的控制器的匹配被认为是对端点所有权的足够强的指示,因此可以将 405(方法 \_ 不允许)、415(不支持 \_media\_type)或类似的响应发送到客户机,以帮助进行调试。出于这个原因,建议避免在带注释的控制器和视图控制器之间分割 URL 处理。 + +#### 1.11.9.视图解析器 + +[WebFlux](web-reactive.html#webflux-config-view-resolvers) + +MVC 配置简化了视图解析程序的注册。 + +下面的 Java 配置示例通过使用 JSP 和 Jackson 作为 JSON 呈现的默认`View`配置内容协商视图解析: + +Java + +``` +@Configuration +@EnableWebMvc +public class WebConfig implements WebMvcConfigurer { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.enableContentNegotiation(new MappingJackson2JsonView()); + registry.jsp(); + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebMvc +class WebConfig : WebMvcConfigurer { + + override fun configureViewResolvers(registry: ViewResolverRegistry) { + registry.enableContentNegotiation(MappingJackson2JsonView()) + registry.jsp() + } +} +``` + +下面的示例展示了如何用 XML 实现相同的配置: + +``` + + + + + + + + +``` + +但是,请注意,自由标记、磁贴、Groovy 标记和脚本模板也需要配置底层视图技术。 + +MVC 命名空间提供了专用的元素。下面的示例与 Freemarker 一起工作: + +``` + + + + + + + + + + + + +``` + +在 Java 配置中,你可以添加相应的`Configurer` Bean,如下例所示: + +Java + +``` +@Configuration +@EnableWebMvc +public class WebConfig implements WebMvcConfigurer { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.enableContentNegotiation(new MappingJackson2JsonView()); + registry.freeMarker().cache(false); + } + + @Bean + public FreeMarkerConfigurer freeMarkerConfigurer() { + FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); + configurer.setTemplateLoaderPath("/freemarker"); + return configurer; + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebMvc +class WebConfig : WebMvcConfigurer { + + override fun configureViewResolvers(registry: ViewResolverRegistry) { + registry.enableContentNegotiation(MappingJackson2JsonView()) + registry.freeMarker().cache(false) + } + + @Bean + fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply { + setTemplateLoaderPath("/freemarker") + } +} +``` + +#### 1.11.10.静态资源 + +[WebFlux](web-reactive.html#webflux-config-static-resources) + +此选项提供了一种方便的方式来从基于[`Resource`](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/core/io/Resource.html)的位置列表中提供静态资源。 + +在下一个示例中,给定一个以`/resources`开头的请求,相对路径用于在 Web 应用程序根目录下或在`/static`下的 Classpath 上查找和服务相对于`/public`的静态资源。这些资源将在一年后到期,以确保最大程度地使用浏览器缓存,并减少浏览器发出的 HTTP 请求。`Last-Modified`信息是从`Resource#lastModified`推导出来的,因此`"Last-Modified"`头支持 HTTP 条件请求。 + +下面的清单展示了如何使用 Java 配置来实现这一点: + +Java + +``` +@Configuration +@EnableWebMvc +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/resources/**") + .addResourceLocations("/public", "classpath:/static/") + .setCacheControl(CacheControl.maxAge(Duration.ofDays(365))); + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebMvc +class WebConfig : WebMvcConfigurer { + + override fun addResourceHandlers(registry: ResourceHandlerRegistry) { + registry.addResourceHandler("/resources/**") + .addResourceLocations("/public", "classpath:/static/") + .setCacheControl(CacheControl.maxAge(Duration.ofDays(365))) + } +} +``` + +下面的示例展示了如何用 XML 实现相同的配置: + +``` + +``` + +另见[对静态资源的 HTTP 缓存支持](#mvc-caching-static-resources)。 + +资源处理程序还支持[“资源解决者”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/servlet/resource/ResourceResolver.html)实现和[“资源转换器”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/servlet/resource/ResourceTransformer.html)实现的链,你可以使用它们创建工具链来使用优化的资源。 + +你可以使用`VersionResourceResolver`来实现基于从内容、固定应用程序版本或其他版本计算的 MD5 散列的版本管理的资源 URL。“ContentVersionStrategy”(MD5 散列)是一个不错的选择——除了一些明显的例外,比如与模块加载程序一起使用的 JavaScript 资源。 + +下面的示例展示了如何在 Java 配置中使用`VersionResourceResolver`: + +Java + +``` +@Configuration +@EnableWebMvc +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/resources/**") + .addResourceLocations("/public/") + .resourceChain(true) + .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**")); + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebMvc +class WebConfig : WebMvcConfigurer { + + override fun addResourceHandlers(registry: ResourceHandlerRegistry) { + registry.addResourceHandler("/resources/**") + .addResourceLocations("/public/") + .resourceChain(true) + .addResolver(VersionResourceResolver().addContentVersionStrategy("/**")) + } +} +``` + +下面的示例展示了如何用 XML 实现相同的配置: + +``` + + + + + + + + + +``` + +然后,你可以使用`ResourceUrlProvider`重写 URL,并应用整个解析器和变压器链——例如,用于插入版本。MVC 配置提供了`ResourceUrlProvider` Bean,以便可以将其注入到其他配置中。你还可以使用 ThymeLeaf、JSP、Freemarker 和其他具有依赖`HttpServletResponse#encodeURL`的 URL 标记的“ResourceUrlenCodingFilter”来使重写透明。 + +请注意,当同时使用`EncodedResourceResolver`(例如,用于服务 gzipped 或 brotli 编码的资源)和`VersionResourceResolver`时,必须按此顺序注册它们。这确保了基于内容的版本总是基于未编码的文件进行可靠的计算。 + +[WebJars](https://www.webjars.org/documentation)还通过 Classpath 上的 `org.webjars:webjars-locator-core’库自动注册的 `WebjarsResourceResolver’进行支持。该解析器可以重写 URL 以包括 jar 的版本,也可以匹配没有版本的传入 URL——例如,从`/jquery/jquery.min.js`到 `/jquery/1.2.0/jquery.min.js`。 + +| |基于`ResourceHandlerRegistry`的 Java 配置为细粒度控制提供了进一步的选项
    ,例如,上次修改行为和优化的资源解析。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 1.11.11.默认值 Servlet + +Spring MVC 允许将`DispatcherServlet`映射到`/`(从而覆盖容器的默认值 Servlet 的映射),同时仍然允许由容器的默认值处理静态资源请求 Servlet。它配置了一个带有`/**`的 URL 映射和相对于其他 URL 映射的最低优先级的“defaultServlettPrequestHandler”。 + +此处理程序将所有请求转发到缺省 Servlet。因此,它必须以所有其他 URL`HandlerMappings`的顺序保持在最后。如果使用``,就是这种情况。或者,如果你设置了自己定制的`HandlerMapping`实例,请确保将其`order`属性设置为一个低于`DefaultServletHttpRequestHandler`的值,即`Integer.MAX_VALUE`。 + +下面的示例展示了如何通过使用默认设置来启用该功能: + +Java + +``` +@Configuration +@EnableWebMvc +public class WebConfig implements WebMvcConfigurer { + + @Override + public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { + configurer.enable(); + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebMvc +class WebConfig : WebMvcConfigurer { + + override fun configureDefaultServletHandling(configurer: DefaultServletHandlerConfigurer) { + configurer.enable() + } +} +``` + +下面的示例展示了如何用 XML 实现相同的配置: + +``` + +``` + +要重写`/` Servlet 映射的注意事项是,默认 Servlet 的`RequestDispatcher`必须通过名称而不是路径检索。DefaultServlethtPrequestHandler 尝试在启动时自动检测容器的默认 Servlet,使用大多数主要 Servlet 容器(包括 Tomcat、 Jetty、GlassFish、JBoss、Resin、WebLogic 和 WebSphere)的已知名称列表。如果默认值 Servlet 已被定制配置为不同的名称,或者在默认值 Servlet 名称未知的情况下正在使用不同的 Servlet 容器,那么你必须显式地提供默认值 Servlet 的名称,如下例所示: + +Java + +``` +@Configuration +@EnableWebMvc +public class WebConfig implements WebMvcConfigurer { + + @Override + public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { + configurer.enable("myCustomDefaultServlet"); + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebMvc +class WebConfig : WebMvcConfigurer { + + override fun configureDefaultServletHandling(configurer: DefaultServletHandlerConfigurer) { + configurer.enable("myCustomDefaultServlet") + } +} +``` + +下面的示例展示了如何用 XML 实现相同的配置: + +``` + +``` + +#### 1.11.12.路径匹配 + +[WebFlux](web-reactive.html#webflux-config-path-matching) + +你可以自定义与路径匹配和 URL 处理相关的选项。有关单个选项的详细信息,请参见[“PathMatchMatchProfigurer”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/servlet/config/annotation/PathMatchConfigurer.html)Javadoc。 + +下面的示例展示了如何在 Java 配置中定制路径匹配: + +Java + +``` +@Configuration +@EnableWebMvc +public class WebConfig implements WebMvcConfigurer { + + @Override + public void configurePathMatch(PathMatchConfigurer configurer) { + configurer + .setPatternParser(new PathPatternParser()) + .addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController.class)); + } + + private PathPatternParser patternParser() { + // ... + } +} +``` + +Kotlin + +``` +@Configuration +@EnableWebMvc +class WebConfig : WebMvcConfigurer { + + override fun configurePathMatch(configurer: PathMatchConfigurer) { + configurer + .setPatternParser(patternParser) + .addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController::class.java)) + } + + fun patternParser(): PathPatternParser { + //... + } +} +``` + +下面的示例展示了如何用 XML 实现相同的配置: + +``` + + + + + + +``` + +#### 1.11.13.高级 Java 配置 + +[WebFlux](web-reactive.html#webflux-config-advanced-java) + +`@EnableWebMvc`imports`DelegatingWebMvcConfiguration`,其中: + +* 为 Spring MVC 应用程序提供默认的 Spring 配置 + +* 检测并委托`WebMvcConfigurer`实现来定制该配置。 + +对于高级模式,你可以删除`@EnableWebMvc`,并直接从 `delegatingWebMVCConfiguration’进行扩展,而不是实现`WebMvcConfigurer`,如下例所示: + +Java + +``` +@Configuration +public class WebConfig extends DelegatingWebMvcConfiguration { + + // ... +} +``` + +Kotlin + +``` +@Configuration +class WebConfig : DelegatingWebMvcConfiguration() { + + // ... +} +``` + +你可以将现有的方法保留在`WebConfig`中,但是你现在也可以覆盖来自基类的 Bean 声明,并且你仍然可以在 Classpath 上拥有任何数量的其他`WebMvcConfigurer`实现。 + +#### 1.11.14.高级 XML 配置 + +MVC 命名空间没有高级模式。如果你需要在 Bean 上自定义一个你无法以其他方式更改的属性,那么你可以使用 Spring `BeanPostProcessor`生命周期钩子`ApplicationContext`,如下例所示: + +Java + +``` +@Component +public class MyPostProcessor implements BeanPostProcessor { + + public Object postProcessBeforeInitialization(Object bean, String name) throws BeansException { + // ... + } +} +``` + +Kotlin + +``` +@Component +class MyPostProcessor : BeanPostProcessor { + + override fun postProcessBeforeInitialization(bean: Any, name: String): Any { + // ... + } +} +``` + +请注意,你需要将`MyPostProcessor`声明为 Bean,可以在 XML 中显式地声明,也可以通过``声明来检测它。 + +### 1.12.http/2 + +[WebFlux](web-reactive.html#webflux-http2) + +Servlet 需要 4 个容器来支持 HTTP/2,并且 Spring Framework5 与 Servlet API4 兼容。从编程模型的角度来看,应用程序不需要做任何特定的事情。但是,有一些与服务器配置相关的考虑因素。有关更多详细信息,请参见[HTTP/2Wiki 页面](https://github.com/spring-projects/spring-framework/wiki/HTTP-2-support)。 + +Servlet API 确实公开了一个与 HTTP/2 相关的构造。你可以使用 javax. Servlet.http.pushbuilder’来主动地将资源推送到客户端,并且支持[method argument](#mvc-ann-arguments)到`@RequestMapping`方法。 + +## 2. REST 客户 + +本节描述客户端访问 REST 端点的选项。 + +### 2.1.`RestTemplate` + +`RestTemplate`是执行 HTTP 请求的同步客户端。它是原始的 Spring REST 客户机,在底层 HTTP 客户库上公开了一个简单的模板方法 API。 + +| |截至 5.0,`RestTemplate`处于维护模式,只有少量的
    更改请求和 bug 被接受。请考虑使用[WebClient](web-reactive.html#webflux-client),它提供了一个更现代的 API,并且
    支持同步、异步和流场景。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +详见[REST Endpoints](integration.html#rest-client-access)。 + +### 2.2.`WebClient` + +`WebClient`是执行 HTTP 请求的非阻塞、反应式客户端。它是在 5.0 中引入的,并提供了`RestTemplate`的现代替代方案,有效地支持同步和异步以及流场景。 + +与`RestTemplate`相反,`WebClient`支持以下内容: + +* 非阻塞 I/O。 + +* 反应性气流反压。 + +* 高并发性与较少的硬件资源. + +* 功能风格的、流畅的 API,充分利用了 Java8Lambdas。 + +* 同步和异步交互。 + +* 从服务器往上流或往下流。 + +有关更多详细信息,请参见[WebClient](web-reactive.html#webflux-client)。 + +## 3. 测试 + +[Same in Spring WebFlux](web-reactive.html#webflux-test) + +这一部分总结了在`spring-test`中可用于 Spring MVC 应用程序的选项。 + +* Servlet API 模拟: Servlet 用于单元测试控制器、过滤器和其他 Web 组件的 API 合同的模拟实现。有关更多详细信息,请参见[Servlet API](testing.html#mock-objects-servlet)模拟对象。 + +* TestContext Framework:支持在 JUnit 和 TestNG 测试中加载 Spring 配置,包括跨测试方法对加载的配置进行有效缓存,以及支持用`MockServletContext`加载`WebApplicationContext`。有关更多详细信息,请参见[TestContext 框架](testing.html#testcontext-framework)。 + +* Spring MVC 测试:一种框架,也称为,用于通过测试带注释的控制器(即支持注释),用 Spring MVC 基础设施完成,但没有 HTTP 服务器。有关更多详细信息,请参见[Spring MVC Test](testing.html#spring-mvc-test-framework)。 + +* 客户端 REST:`spring-test`提供了一个`MockRestServiceServer`,你可以将其用作模拟服务器,用于测试内部使用`RestTemplate`的客户端代码。有关更多详细信息,请参见[客户机 REST 测试](testing.html#spring-mvc-test-client)。 + +* `WebTestClient`:用于测试 WebFlux 应用程序,但也可以用于通过 HTTP 连接对任何服务器进行端到端集成测试。它是一个非阻塞的、反应性的客户机,非常适合于测试异步和流媒体场景。 + +## 4. WebSockets + +[WebFlux](web-reactive.html#webflux-websocket) + +参考文档的这一部分涵盖了对 Servlet 堆栈的支持、包括原始 WebSocket 交互的 WebSocket 消息传递、 WebSocket 通过 Sockjs 的模拟,以及通过作为 WebSocket 上的子协议的 STOMP 的发布-订阅消息传递。 + +### 4.1. WebSocket 介绍 + +WebSocket 协议[RFC 6455](https://tools.ietf.org/html/rfc6455)提供了一种标准化的方式,通过单个 TCP 连接在客户机和服务器之间建立全双工、双向通信通道。它是一种与 HTTP 不同的 TCP 协议,但其设计是通过 HTTP 工作的,使用端口 80 和 443,并允许重用现有的防火墙规则。 + +WebSocket 交互以一个 HTTP 请求开始,该 HTTP 请求使用 HTTP头来升级或在这种情况下切换到 WebSocket 协议。下面的示例展示了这样的交互: + +``` +GET /spring-websocket-portfolio/portfolio HTTP/1.1 +Host: localhost:8080 +Upgrade: websocket (1) +Connection: Upgrade (2) +Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg== +Sec-WebSocket-Protocol: v10.stomp, v11.stomp +Sec-WebSocket-Version: 13 +Origin: http://localhost:8080 +``` + +|**1**|`Upgrade`标头。| +|-----|-------------------------------| +|**2**|使用`Upgrade`连接。| + +具有 WebSocket 支持的服务器将返回类似于以下内容的输出,而不是通常的 200 状态代码: + +``` +HTTP/1.1 101 Switching Protocols (1) +Upgrade: websocket +Connection: Upgrade +Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0= +Sec-WebSocket-Protocol: v10.stomp +``` + +|**1**|协议转换| +|-----|---------------| + +在成功握手之后,HTTP 升级请求中的 TCP 套接字仍然是开放的,以便客户机和服务器继续发送和接收消息。 + +关于 WebSockets 如何工作的完整介绍超出了本文的范围。参见 RFC6455,HTML5 的 WebSocket 章,或者 Web 上的许多介绍和教程中的任何一个。 + +注意,如果 WebSocket 服务器在 Web 服务器(例如 Nginx)后面运行,则可能需要将其配置为将 WebSocket 升级请求传递到 WebSocket 服务器。同样,如果应用程序在云环境中运行,则检查与 WebSocket 支持相关的云提供商的指令。 + +#### 4.1.1.HTTP 与 WebSocket + +WebSocket 即使被设计为与 HTTP 兼容并且以 HTTP 请求开始,重要的是要理解这两个协议导致非常不同的体系结构和应用程序编程模型。 + +在 HTTP 和 REST 中,应用程序被建模为许多 URL。为了与应用程序交互,客户端访问这些 URL,请求-响应样式。服务器根据 HTTP URL、方法和标头将请求路由到适当的处理程序。 + +相比之下,在 WebSockets 中,初始连接通常只有一个 URL。随后,所有应用程序消息都在相同的 TCP 连接上流动。这指向了一种完全不同的异步、事件驱动的消息传递体系结构。 + +WebSocket 也是一种低级传输协议,它与 HTTP 不同,不对消息的内容规定任何语义。这意味着,除非客户机和服务器在消息语义上达成一致,否则就没有路由或处理消息的方法。 + +WebSocket 客户端和服务器可以协商使用更高级别的消息传递协议(例如,STOMP),通过头上的 HTTP 握手请求。如果不能做到这一点,他们就需要拿出自己的惯例。 + +#### 4.1.2.何时使用 WebSockets + +WebSockets 可以使 Web 页面具有动态性和交互性。然而,在许多情况下,Ajax 和 HTTP 流或长轮询的组合可以提供简单有效的解决方案。 + +例如,新闻、邮件和社交提要需要动态更新,但每隔几分钟更新一次可能完全没问题。另一方面,协作、游戏和金融应用程序需要更接近实时。 + +延迟本身并不是一个决定因素。如果消息量相对较低(例如,监视网络故障),则 HTTP 流或轮询可以提供有效的解决方案。 WebSocket 的最佳使用情况是低延迟、高频率和大容量的组合。 + +还请记住,在 Internet 上,超出你控制范围的限制性代理可能会阻止 WebSocket 交互,这是因为它们未被配置为传递“升级”头,或者是因为它们关闭了似乎空闲的长期连接。这意味着, WebSocket 对于防火墙内的内部应用程序的使用是一个比对于面向公众的应用程序更直接的决定。 + +### 4.2. WebSocket 空气污染指数 + +[WebFlux](web-reactive.html#webflux-websocket-server) + +Spring 框架提供了一个 WebSocket API,你可以使用该 API 来编写处理 WebSocket 消息的客户端和服务器端应用程序。 + +#### 4.2.1.`WebSocketHandler` + +[WebFlux](web-reactive.html#webflux-websocket-server-handler) + +WebSocket 创建服务器就像实现`WebSocketHandler`一样简单,或者更有可能的是,扩展`TextWebSocketHandler`或`BinaryWebSocketHandler`。下面的示例使用`TextWebSocketHandler`: + +``` +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.TextMessage; + +public class MyHandler extends TextWebSocketHandler { + + @Override + public void handleTextMessage(WebSocketSession session, TextMessage message) { + // ... + } + +} +``` + +有专门的 WebSocket Java 配置和 XML 命名空间支持,用于将前面的 WebSocket 处理程序映射到特定的 URL,如下例所示: + +``` +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; + +@Configuration +@EnableWebSocket +public class WebSocketConfig implements WebSocketConfigurer { + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(myHandler(), "/myHandler"); + } + + @Bean + public WebSocketHandler myHandler() { + return new MyHandler(); + } + +} +``` + +下面的示例展示了与前面示例类似的 XML 配置: + +``` + + + + + + + + + +``` + +前面的示例用于 Spring MVC 应用程序中,并且应该包括在[“Dispatcherservlet”](#mvc-servlet)的配置中。然而, Spring 的 WebSocket 支持并不依赖于 Spring MVC。在[“Websocketthprequesthandler”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/socket/server/support/WebSocketHttpRequestHandler.html)的帮助下,将`WebSocketHandler`集成到其他 HTTP 服务环境中是相对简单的。 + +当直接 VS 间接地使用`WebSocketHandler`API 时,例如通过[STOMP](#websocket-stomp)消息传递时,应用程序必须同步消息的发送,因为底层标准 WebSocket 会话(JSR-356)不允许并发。一个选项是将`WebSocketSession`换成[“ConcurrentWebSocketsessionDecorator”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/socket/handler/ConcurrentWebSocketSessionDecorator.html)。 + +#### 4.2.2. WebSocket 握手 + +[WebFlux](web-reactive.html#webflux-websocket-server-handshake) + +定制初始 HTTP WebSocket 握手请求的最简单方法是通过`HandshakeInterceptor`,该方法公开了握手之前和之后的方法。你可以使用这样的拦截器来阻止握手或使`WebSocketSession`的任何属性可用。下面的示例使用内置的拦截器将 HTTP 会话属性传递给 WebSocket 会话: + +``` +@Configuration +@EnableWebSocket +public class WebSocketConfig implements WebSocketConfigurer { + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(new MyHandler(), "/myHandler") + .addInterceptors(new HttpSessionHandshakeInterceptor()); + } + +} +``` + +下面的示例展示了与前面示例类似的 XML 配置: + +``` + + + + + + + + + + + + +``` + +一个更高级的选项是扩展执行 WebSocket 握手步骤的`DefaultHandshakeHandler`,包括验证客户端原点、协商子协议和其他细节。如果应用程序需要配置自定义`RequestUpgradeStrategy`以适应 WebSocket 服务器引擎和尚未支持的版本,则可能还需要使用此选项(有关此主题的更多信息,请参见[Deployment](#websocket-server-deployment))。Java 配置和 XML 命名空间都使配置自定义的“HandshakeHandler”成为可能。 + +| |Spring 提供了一个`WebSocketHandlerDecorator`基类,你可以使用它来使用附加的行为来装饰
    a`WebSocketHandler`。当使用 WebSocket Java 配置
    或 XML 命名空间时,默认提供并添加日志记录和异常处理
    实现。`ExceptionWebSocketHandlerDecorator`捕获由任何`WebSocketHandler`方法产生的所有未捕获的
    异常,并关闭带有状态
    的 WebSocket
    会话,这表示服务器错误。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 4.2.3.部署 + +Spring WebSocket API 很容易集成到 Spring MVC 应用程序中,其中`DispatcherServlet`同时服务 HTTP WebSocket 握手和其他 HTTP 请求。通过调用`WebSocketHttpRequestHandler`,也很容易集成到其他 HTTP 处理场景中。这很方便,也很容易理解。但是,对于 JSR-356 运行时,需要进行特殊的考虑。 + +Java WebSocket API(JSR-356)提供了两种部署机制。第一个涉及启动时的 Servlet 容器 Classpath 扫描( Servlet 3 特征)。另一种是在 Servlet 容器初始化时使用的注册 API。这两种机制都不可能对所有 HTTP 处理——包括 WebSocket 握手和所有其他 HTTP 请求——例如 Spring MVC 的—使用单个“前置控制器”。 + +这是 JSR-356 的一个重大限制,即 Spring 的 WebSocket 支持具有特定于服务器的`RequestUpgradeStrategy`实现的地址,即使在 JSR-356 运行时也是如此。此类策略目前存在于 Tomcat、 Jetty、GlassFish、WebLogic、WebSphere 和 Undertow(以及 Wildfly)。 + +| |一个克服前面的限制的请求在 Java WebSocket API 中已经被
    创建,并且可以在[eclipse-ee4j/websocket-api#211](https://github.com/eclipse-ee4j/websocket-api/issues/211)处被遵循。
    Tomcat、 Undertow 和 WebSphere 提供了它们自己的 API 替代方案,使得
    可以做到这一点,并且在 Jetty 中也是可能的。我们希望
    更多的服务器也能做到这一点。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +第二个考虑因素是,具有 JSR-356 支持的 Servlet 容器预计将执行`ServletContainerInitializer`扫描,这可能会大大减慢应用程序的启动速度——在某些情况下。如果在升级到具有 JSR-356 支持的 Servlet 容器版本后观察到重大影响,则应该可以通过使用``中的``元素选择性地启用或禁用 Web 片段(和 SCI 扫描),如下例所示: + +``` + + + + + +``` + +然后,你可以根据名称选择性地启用 Web 片段,例如 Spring 自己的“SpringServletContainerInitializer”,它为 Servlet 3Java 初始化 API 提供了支持。下面的示例展示了如何做到这一点: + +``` + + + + spring_web + + + +``` + +#### 4.2.4.服务器配置 + +[WebFlux](web-reactive.html#webflux-websocket-server-config) + +WebSocket 每个底层引擎都公开了控制运行时特性的配置属性,例如消息缓冲区大小、空闲超时等。 + +对于 Tomcat、Wildfly 和 GlassFish,可以在 WebSocket Java 配置中添加`ServletServerContainerFactoryBean`,如下例所示: + +``` +@Configuration +@EnableWebSocket +public class WebSocketConfig implements WebSocketConfigurer { + + @Bean + public ServletServerContainerFactoryBean createWebSocketContainer() { + ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean(); + container.setMaxTextMessageBufferSize(8192); + container.setMaxBinaryMessageBufferSize(8192); + return container; + } + +} +``` + +下面的示例展示了与前面示例类似的 XML 配置: + +``` + + + + + + + + +``` + +| |对于客户端 WebSocket 配置,你应该使用`WebSocketContainerFactoryBean`或`ContainerProvider.getWebSocketContainer()`(Java 配置)。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------| + +对于 Jetty,你需要提供一个预先配置的 Jetty `WebSocketServerFactory`,并通过 WebSocket Java 配置将其插入 Spring 的`DefaultHandshakeHandler`。下面的示例展示了如何做到这一点: + +``` +@Configuration +@EnableWebSocket +public class WebSocketConfig implements WebSocketConfigurer { + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(echoWebSocketHandler(), + "/echo").setHandshakeHandler(handshakeHandler()); + } + + @Bean + public DefaultHandshakeHandler handshakeHandler() { + + WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER); + policy.setInputBufferSize(8192); + policy.setIdleTimeout(600000); + + return new DefaultHandshakeHandler( + new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy))); + } + +} +``` + +下面的示例展示了与前面示例类似的 XML 配置: + +``` + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +#### 4.2.5.允许的来源 + +[WebFlux](web-reactive.html#webflux-websocket-server-cors) + +在 Spring Framework4.1.5 中, WebSocket 和 Sockjs 的默认行为是仅接受同源请求。也可以允许所有或指定的源列表。这种检查主要是为浏览器客户端设计的。没有什么可以阻止其他类型的客户机修改`Origin`标头值(有关更多详细信息,请参见[RFC6454:Web Origin 概念](https://tools.ietf.org/html/rfc6454))。 + +这三种可能的行为是: + +* 只允许同源请求(缺省):在这种模式下,当启用 Sockjs 时,IFRAME HTTP 响应头`X-Frame-Options`设置为`SAMEORIGIN`,并且禁用 JSONP 传输,因为它不允许检查请求的源。因此,当启用此模式时,IE6 和 IE7 将不受支持。 + +* 允许指定的源列表:每个允许的源列表必须以`http://`或`https://`开头。在这种模式下,当启用 Sockjs 时,iframe 传输将被禁用。因此,当启用此模式时,IE6 到 IE9 将不受支持。 + +* 允许所有原点:要启用此模式,你应该提供`*`作为允许的原点值。在这种模式下,所有的传输都是可用的。 + +你可以配置 WebSocket 和 Sockjs 允许的起源,如下例所示: + +``` +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; + +@Configuration +@EnableWebSocket +public class WebSocketConfig implements WebSocketConfigurer { + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(myHandler(), "/myHandler").setAllowedOrigins("https://mydomain.com"); + } + + @Bean + public WebSocketHandler myHandler() { + return new MyHandler(); + } + +} +``` + +下面的示例展示了与前面示例类似的 XML 配置: + +``` + + + + + + + + + +``` + +### 4.3.Sockjs 后援 + +在公共互联网上,不受你控制的限制性代理可能会阻止 WebSocket 交互,这是因为它们未被配置为传递`Upgrade`头,或者是因为它们关闭了似乎处于空闲状态的长期连接。 + +这个问题的解决方案是 WebSocket 仿真——即,首先尝试使用 WebSocket,然后退回基于 HTTP 的技术,该技术模拟 WebSocket 交互并公开相同的应用程序级 API。 + +在 Servlet 栈上, Spring 框架为 Sockjs 协议提供了服务器(以及客户端)支持。 + +#### 4.3.1.概述 + +SockJS 的目标是让应用程序使用 WebSocket API,但在运行时在必要时退回到非 WebSocket 替代方案,而不需要更改应用程序代码。 + +Sockjs 包括: + +* 将[SockJS protocol](https://github.com/sockjs/sockjs-protocol)定义为可执行文件[narrated tests](https://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html)的形式。 + +* [Sockjs JavaScript 客户端](https://github.com/sockjs/sockjs-client/)——用于浏览器的客户库。 + +* Sockjs 服务器实现,包括在 Spring 框架`spring-websocket`模块中的一个。 + +* `spring-websocket`模块中的 Sockjs Java 客户端(自版本 4.1 起)。 + +Sockjs 是为在浏览器中使用而设计的。它使用各种技术来支持各种浏览器版本。有关 Sockjs 传输类型和浏览器的完整列表,请参见[SockJS client](https://github.com/sockjs/sockjs-client/)页面。传输分为三大类: WebSocket、HTTP 流和 HTTP 长轮询。有关这些类别的概述,请参见[this blog post](https://spring.io/blog/2012/05/08/spring-mvc-3-2-preview-techniques-for-real-time-updates/)。 + +Sockjs 客户机通过发送`GET /info`开始从服务器获取基本信息。在那之后,它必须决定使用什么交通工具。如果可能,使用 WebSocket。如果没有,在大多数浏览器中,至少有一个 HTTP 流媒体选项。如果不是,则使用 HTTP(长)轮询。 + +所有传输请求都具有以下 URL 结构: + +``` +https://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport} +``` + +地点: + +* `{server-id}`对于群集中的路由请求很有用,但在其他情况下不会使用。 + +* `{session-id}`关联属于 Sockjs 会话的 HTTP 请求。 + +* `{transport}`表示传输类型(例如,`websocket`,`xhr-streaming`等)。 + +WebSocket 传输只需要一个 HTTP 请求就可以完成 WebSocket 握手。此后的所有消息都在该套接字上交换。 + +HTTP 传输需要更多的请求。例如,Ajax/XHR 流依赖于对服务器到客户端消息的一个长时间运行的请求,以及对客户端到服务器消息的额外 HTTP POST 请求。长轮询是类似的,只是它在每个服务器到客户端发送后结束当前请求。 + +Sockjs 添加了最小的消息框架。例如,服务器最初发送字母`o`(“打开”框架),消息被发送为`a["message1","message2"]`(JSON 编码的数组),如果在 25 秒内没有消息流(默认情况下),则发送字母`h`(“关闭”框架)以关闭会话。 + +要了解更多信息,请在浏览器中运行一个示例,并观察 HTTP 请求。Sockjs 客户机允许固定传输列表,因此可以一次查看每个传输。Sockjs 客户机还提供了一个调试标志,可以在浏览器控制台中启用有用的消息。在服务器端,你可以启用`org.springframework.web.socket`的 `trace’日志记录。有关更多详细信息,请参见 Sockjs 协议[narrated test](https://sockjs.github.io/sockjs-protocol/sockjs-protocol-0.3.3.html)。 + +#### 4.3.2.启用 Sockjs + +你可以通过 Java 配置启用 Sockjs,如下例所示: + +``` +@Configuration +@EnableWebSocket +public class WebSocketConfig implements WebSocketConfigurer { + + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(myHandler(), "/myHandler").withSockJS(); + } + + @Bean + public WebSocketHandler myHandler() { + return new MyHandler(); + } + +} +``` + +下面的示例展示了与前面示例类似的 XML 配置: + +``` + + + + + + + + + + +``` + +前面的示例用于 Spring MVC 应用程序中,并且应该包括在[“Dispatcherservlet”](#mvc-servlet)的配置中。然而, Spring 的 WebSocket 和 Sockjs 支持并不依赖于 Spring MVC。在[“Sockjshtprequesthandler”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/socket/sockjs/support/SockJsHttpRequestHandler.html)的帮助下,集成到其他 HTTP 服务环境中相对简单。 + +在浏览器方面,应用程序可以使用[`sockjs-client`](https://github.com/sockjs/sockjs-client/)(版本 1.0.x)。它模拟 W3C WebSocket API,并与服务器通信以选择最佳的传输选项,这取决于它在其中运行的浏览器。请参阅[sockjs-client](https://github.com/sockjs/sockjs-client/)页面和浏览器支持的传输类型列表。客户机还提供了几个配置选项——例如,指定要包含哪些传输。 + +#### 4.3.3.IE8 和 IE9 + +Internet Explorer8 和 9 仍在使用中。它们是拥有袜子的一个关键原因。本节介绍了在这些浏览器中运行的重要注意事项。 + +Sockjs 客户端通过使用 Microsoft 的[xdomainrequest](https://blogs.msdn.com/b/ieinternals/archive/2010/05/13/xdomainrequest-restrictions-limitations-and-workarounds.aspx)支持 IE8 和 IE9 中的 Ajax/XHR 流媒体。它可以跨域工作,但不支持发送 cookie。对于 Java 应用程序来说,Cookie 通常是必不可少的。然而,由于 Sockjs 客户机可以用于许多服务器类型(而不仅仅是 Java 类型),因此它需要知道 Cookie 是否重要。如果是这样的话,Sockjs 客户机更喜欢 Ajax/XHR。否则,它依赖于一种基于 iframe 的技术。 + +来自 Sockjs 客户机的第一个`/info`请求是对可能影响客户机选择传输方式的信息的请求。这些细节之一是服务器应用程序是否依赖 Cookie(例如,出于身份验证目的或使用粘性会话进行集群)。 Spring 的 Sockjs 支持包括一个名为`sessionCookieNeeded`的属性。默认情况下,它是启用的,因为大多数 Java 应用程序依赖于`JSESSIONID`cookie。如果你的应用程序不需要它,你可以关闭此选项,然后 Sockjs 客户机应该在 IE8 和 IE9 中选择`xdr-streaming`。 + +如果确实使用基于 iframe 的传输,请记住,可以通过将 HTTP 响应头`X-Frame-Options`设置为`DENY`、`SameOrigin’或`ALLOW-FROM `来指示浏览器阻止在给定页面上使用 iframes。这是用来防止[clickjacking](https://www.owasp.org/index.php/Clickjacking)。 + +| |Spring Security3.2+ 为在每个
    响应上设置`X-Frame-Options`提供了支持。默认情况下, Spring Security Java 配置将其设置为`DENY`。
    在 3.2 中, Spring Security XML 命名空间默认情况下不设置该标头
    ,但可以配置为这样做。在将来,它可能会默认设置它。

    有关如何配置[默认安全标头](https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#headers)头的`X-Frame-Options`设置的详细信息,请参见 Spring 安全文档的
    。你还可以查看[SEC-2501](https://jira.spring.io/browse/SEC-2501)以获取更多背景信息。| +|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +如果你的应用程序添加了`X-Frame-Options`响应报头(应该如此!)并依赖基于 iframe 的传输,则需要将报头值设置为 `SameOrigin’或`ALLOW-FROM `。 Spring Sockjs 支持还需要知道 Sockjs 客户机的位置,因为它是从 iframe 加载的。默认情况下,iframe 被设置为从 CDN 位置下载 Sockjs 客户端。将此选项配置为使用来自与应用程序相同来源的 URL 是一个好主意。 + +下面的示例展示了如何在 Java 配置中实现这一点: + +``` +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/portfolio").withSockJS() + .setClientLibraryUrl("http://localhost:8080/myapp/js/sockjs-client.js"); + } + + // ... + +} +``` + +XML 名称空间通过``元素提供了类似的选项。 + +| |在最初的开发过程中,启用 Sockjs 客户机`devel`模式,该模式可以防止
    浏览器缓存原本会缓存
    的 Sockjs 请求(如 iframe)。有关如何启用它的详细信息,请参见[SockJS client](https://github.com/sockjs/sockjs-client/)页面。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 4.3.4.心跳 + +Sockjs 协议要求服务器发送心跳消息,以防止代理得出连接已挂起的结论。 Spring Sockjs 配置具有一个名为`heartbeatTime`的属性,你可以使用该属性来定制频率。默认情况下,假设在该连接上没有发送其他消息,则会在 25 秒后发送心跳。对于公共互联网应用程序,这个 25 秒的值与下面的[IETF 推荐](https://tools.ietf.org/html/rfc6202)一致。 + +| |在使用 STOMP over WebSocket 和 Sockjs 时,如果 STOMP 客户机和服务器协商
    要交换的心跳,则禁用 Sockjs 心跳。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------| + +Spring Sockjs 支持还允许你配置`TaskScheduler`来调度心跳任务。任务计划程序由线程池支持,并根据可用处理器的数量进行默认设置。你应该考虑根据你的特定需求自定义设置。 + +#### 4.3.5.客户端断开连接 + +HTTP 流和 HTTP 长轮询 Sockjs 传输要求连接的打开时间比通常更长。有关这些技术的概述,请参见[this blog post](https://spring.io/blog/2012/05/08/spring-mvc-3-2-preview-techniques-for-real-time-updates/)。 + +在 Servlet 容器中,这是通过 Servlet 3 异步支持完成的,该异步支持允许退出 Servlet 容器线程,处理请求,并继续写入来自另一个线程的响应。 + +一个具体的问题是, Servlet API 不为已经消失的客户机提供通知。见[eclipse-ee4j/servlet-api#44](https://github.com/eclipse-ee4j/servlet-api/issues/44)。然而, Servlet 容器在随后尝试写入响应时会引发异常。由于 Spring 的 SockJS 服务支持服务器发送的心跳(默认情况下每 25 秒),这意味着通常会在该时间段内检测到客户端断开连接(如果发送消息的频率更高,则会更早)。 + +| |结果,由于客户端断开连接,可能会发生网络 I/O 故障,而
    会用不必要的堆栈跟踪填充日志。 Spring 尽最大努力通过使用专用日志类别来标识
    表示客户端断开连接(特定于每个服务器)和日志
    的这样的网络故障,`DISCONNECTED_CLIENT_LOG_CATEGORY`(在`AbstractSockJsSession`中定义)。如果需要查看堆栈跟踪,可以将
    日志类别设置为跟踪。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 4.3.6.Sockjs 和 CORS + +如果允许跨源请求(参见[Allowed Origins](#websocket-server-allowed-origins)),则 Sockjs 协议在 XHR 流和轮询传输中使用 CORS 提供跨域支持。因此,CORS 头是自动添加的,除非检测到响应中存在 CORS 头。因此,如果应用程序已经被配置为提供 CORS 支持(例如,通过 Servlet 过滤器),则 Spring 的`SockJsService`跳过了这一部分。 + +还可以通过在 Spring 的 SockJSService 中设置“subscors”属性来禁用这些 CORS 头的添加。 + +Sockjs 期望以下标题和值: + +* `Access-Control-Allow-Origin`:从`Origin`请求头的值初始化。 + +* `Access-Control-Allow-Credentials`:总是设置为`true`。 + +* `Access-Control-Request-Headers`:从等效请求头的值初始化。 + +* `Access-Control-Allow-Methods`:传输支持的 HTTP 方法(参见`TransportType`枚举)。 + +* `Access-Control-Max-Age`:设置为 31536000(1 年)。 + +有关确切的实现,请参见`addCorsHeaders`中的`AbstractSockJsService`和源代码中的`TransportType`枚举。 + +或者,如果 CORS 配置允许,可以考虑排除具有 SockJS 端点前缀的 URL,从而让 Spring 的`SockJsService`处理它。 + +#### 4.3.7.`SockJsClient` + +Spring 提供了一种 Sockjs Java 客户端,以在不使用浏览器的情况下连接到远程 Sockjs 端点。当需要在公共网络上的两个服务器之间进行双向通信时,这可能是特别有用的(即,在这种情况下,网络代理可以排除 WebSocket 协议的使用)。对于测试目的(例如,模拟大量并发用户),Sockjs Java 客户机也非常有用。 + +Sockjs Java 客户端支持`websocket`、`xhr-streaming`和`xhr-polling`传输。剩下的那些只有在浏览器中使用才有意义。 + +你可以将`WebSocketTransport`配置为: + +* `StandardWebSocketClient`在 JSR-356 运行时中。 + +* `JettyWebSocketClient`通过使用 Jetty 9+ 本机 WebSocket API。 + +* Spring 的`WebSocketClient`的任意实现。 + +根据定义,`XhrTransport`同时支持`xhr-streaming`和`xhr-polling`,因为从客户机的角度来看,除了用于连接到服务器的 URL 之外,没有其他区别。目前有两种实现方式: + +* `RestTemplateXhrTransport`将 Spring 的`RestTemplate`用于 HTTP 请求。 + +* `JettyXhrTransport`将 Jetty 的`HttpClient`用于 HTTP 请求。 + +下面的示例展示了如何创建 Sockjs 客户机并连接到 Sockjs 端点: + +``` +List transports = new ArrayList<>(2); +transports.add(new WebSocketTransport(new StandardWebSocketClient())); +transports.add(new RestTemplateXhrTransport()); + +SockJsClient sockJsClient = new SockJsClient(transports); +sockJsClient.doHandshake(new MyWebSocketHandler(), "ws://example.com:8080/sockjs"); +``` + +| |Sockjs 使用 JSON 格式化的数组来处理消息。默认情况下,使用 Jackson2 并且需要
    才能在 Classpath 上。或者,你可以配置“SockjSmessageCodec”的自定义实现,并在`SockJsClient`上配置它。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +要使用`SockJsClient`来模拟大量并发用户,你需要配置底层 HTTP 客户端(用于 XHR 传输)以允许足够数量的连接和线程。下面的示例展示了如何使用 Jetty 来实现这一点: + +``` +HttpClient jettyHttpClient = new HttpClient(); +jettyHttpClient.setMaxConnectionsPerDestination(1000); +jettyHttpClient.setExecutor(new QueuedThreadPool(1000)); +``` + +下面的示例显示了你还应该考虑定制的服务器端 Sockjs 相关属性(详细信息请参见 Javadoc): + +``` +@Configuration +public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport { + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/sockjs").withSockJS() + .setStreamBytesLimit(512 * 1024) (1) + .setHttpMessageCacheSize(1000) (2) + .setDisconnectDelay(30 * 1000); (3) + } + + // ... +} +``` + +|**1**|将`streamBytesLimit`属性设置为 512KB(默认值为 128KB—`128 * 1024`)。| +|-----|-----------------------------------------------------------------------------------------------------| +|**2**|将`httpMessageCacheSize`属性设置为 1,000(默认值为`100`)。| +|**3**|将`disconnectDelay`属性设置为 30 个属性秒(默认值为 5 秒—`5 * 1000`)。| + +### 4.4.跺脚 + +WebSocket 协议定义了两种类型的消息(文本和二进制),但它们的内容是未定义的。该协议定义了一种机制,用于客户机和服务器协商在 WebSocket 之上使用的子协议(即更高级别的消息传递协议)来定义各自可以发送什么样的消息、格式是什么、每个消息的内容,等等。子协议的使用是可选的,但无论哪种方式,客户机和服务器都需要在定义消息内容的某些协议上达成一致。 + +#### 4.4.1.概述 + +[STOMP](https://stomp.github.io/stomp-specification-1.2.html#Abstract)(简单的面向文本的消息传递协议)最初是为脚本语言(例如 Ruby、Python 和 Perl)创建的,用于连接到 Enterprise 消息代理。它旨在解决常用消息传递模式的最小子集。 WebSocket 可以在任何可靠的双向流网络协议上使用 STOMP,例如 TCP 和 WebSocket。尽管 STOMP 是一种面向文本的协议,但消息负载可以是文本的,也可以是二进制的。 + +STOMP 是一种基于帧的协议,其帧是以 HTTP 为模型的。下面的清单显示了 Stomp 框架的结构: + +``` +COMMAND +header1:value1 +header2:value2 + +Body^@ +``` + +客户端可以使用`SEND`或`SUBSCRIBE`命令发送或订阅消息,以及一个`destination`头,该头描述消息的内容以及应该由谁接收。这启用了一个简单的发布-订阅机制,你可以使用该机制通过代理向其他连接的客户机发送消息,或者向服务器发送消息,以请求执行某些工作。 + +当你使用 Spring 的 STOMP 支持时, Spring WebSocket 应用程序充当客户的 STOMP 经纪人。消息被路由到`@Controller`消息处理方法或简单的内存代理,该代理跟踪订阅并向订阅的用户广播消息。还可以配置 Spring 来使用专用的 Stomp 代理(例如 RabbitMQ、ActiveMQ 和其他代理)来实际广播消息。在这种情况下, Spring 维护到代理的 TCP 连接,将消息中继到代理,并将消息从代理向下传递到连接的 WebSocket 客户端。因此, Spring Web 应用程序可以依赖统一的基于 HTTP 的安全性、公共验证和熟悉的编程模型来进行消息处理。 + +下面的示例显示了订阅接收股票报价的客户机,服务器可能会周期性地发送该报价(例如,通过调度任务通过`SimpMessagingTemplate`向经纪人发送消息): + +``` +SUBSCRIBE +id:sub-1 +destination:/topic/price.stock.* + +^@ +``` + +下面的示例显示了一个发送交易请求的客户机,服务器可以通过`@MessageMapping`方法处理该请求: + +``` +SEND +destination:/queue/trade +content-type:application/json +content-length:44 + +{"action":"BUY","ticker":"MMM","shares",44}^@ +``` + +执行后,服务器可以向客户端广播交易确认消息和详细信息。 + +在 Stomp 规范中,目的地的含义是故意不透明的。它可以是任何字符串,完全由 Stomp 服务器来定义它们所支持的目标的语义和语法。然而,很常见的情况是,目标是类似路径的字符串,其中`/topic/..`表示发布-订阅(一对多),而`/queue/`表示点对点(一对一)消息交换。 + +Stomp 服务器可以使用`MESSAGE`命令向所有订阅者广播消息。下面的示例显示了一个服务器,该服务器将股票报价发送到一个已订阅的客户端: + +``` +MESSAGE +message-id:nxahklf6-1 +subscription:sub-1 +destination:/topic/price.stock.MMM + +{"ticker":"MMM","price":129.45}^@ +``` + +服务器不能发送未经请求的消息。来自服务器的所有消息必须响应于特定的客户端订阅,并且服务器消息的“subscription-id”头必须与客户端订阅的`id`头匹配。 + +前面的概述旨在提供对 STOMP 协议的最基本的理解。我们建议对[specification](https://stomp.github.io/stomp-specification-1.2.html)协议进行全面审查。 + +#### 4.4.2.福利 + +与使用原始 WebSockets 相比,使用 STOMP 作为子协议使 Spring 框架和 Spring 安全性提供了更丰富的编程模型。关于 HTTP 相对于原始 TCP 以及它如何让 Spring MVC 和其他 Web 框架提供丰富的功能,也可以提出同样的观点。以下是一系列好处: + +* 无需发明定制的消息传递协议和消息格式。 + +* Spring 框架中包括[Java client](#websocket-stomp-client)在内的 STOMP 客户机是可用的。 + +* 你可以(可选地)使用消息代理(例如 RabbitMQ、ActiveMQ 和其他代理)来管理订阅和广播消息。 + +* 应用程序逻辑可以在任意数量的`@Controller`实例中进行组织,并且可以基于 stomp 目标头将消息路由到它们,而不是针对给定连接使用单个`WebSocketHandler`处理原始消息。 + +* 你可以使用 Spring 安全性来保护基于 STOMP 目的地和消息类型的消息。 + +#### 4.4.3.启用 Stomp + +在`spring-messaging`和 ` Spring- WebSocket ` 模块中提供了对 WebSocket 的 stomp 支持。一旦有了这些依赖关系,就可以使用[SockJS Fallback](#websocket-fallback)在 WebSocket 上公开 Stomp 端点,如下例所示: + +``` +import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; +import org.springframework.web.socket.config.annotation.StompEndpointRegistry; + +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/portfolio").withSockJS(); (1) + } + + @Override + public void configureMessageBroker(MessageBrokerRegistry config) { + config.setApplicationDestinationPrefixes("/app"); (2) + config.enableSimpleBroker("/topic", "/queue"); (3) + } +} +``` + +|**1**|`/portfolio`是 WebSocket(或 Sockjs)
    客户端为 WebSocket 握手需要连接到的端点的 HTTP URL。| +|-----|------------------------------------------------------------------------------------------------------------------------------------------------------------------| +|**2**|目标头以`/app`开头的 stomp 消息被路由到`@Controller`类中的 `@MessageMapping’方法。| +|**3**|使用内置的消息代理进行订阅和广播,并将目标头以`/topic `或`/queue`开头的消息路由到代理。| + +下面的示例展示了与前面示例类似的 XML 配置: + +``` + + + + + + + + + + +``` + +| |对于内置的简单代理,`/topic`和`/queue`前缀没有任何特殊的
    含义。它们仅仅是区分发布订阅和点对点
    消息传递(即多个订阅者和一个消费者)的一种约定。当你使用外部代理时,
    检查代理的 stomp 页面,以了解它支持什么样的 stomp 目的地和
    前缀。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +要从浏览器连接,对于 Sockjs,你可以使用[`sockjs-client`](https://github.com/sockjs/sockjs-client)。对于 Stomp,许多应用程序使用[jmesnil/stomp-websocket](https://github.com/jmesnil/stomp-websocket)库(也称为 stomp.js),它是功能完备的,并已在生产中使用多年,但不再维护。目前,[JSteunou/WebStomp-客户端](https://github.com/JSteunou/webstomp-client)是该库最活跃的维护和不断发展的后继库。下面的示例代码是基于它的: + +``` +var socket = new SockJS("/spring-websocket-portfolio/portfolio"); +var stompClient = webstomp.over(socket); + +stompClient.connect({}, function(frame) { +} +``` + +或者,如果你通过 WebSocket(不使用 Sockjs)进行连接,则可以使用以下代码: + +``` +var socket = new WebSocket("/spring-websocket-portfolio/portfolio"); +var stompClient = Stomp.over(socket); + +stompClient.connect({}, function(frame) { +} +``` + +注意,在前面的示例中`stompClient`不需要指定`login`和`passcode`头。即使这样做了,它们也会在服务器端被忽略(或者更确切地说,被覆盖)。有关身份验证的更多信息,请参见[连接到代理](#websocket-stomp-handle-broker-relay-configure)和[Authentication](#websocket-stomp-authentication)。 + +有关更多示例代码,请参见: + +* [Using WebSocket to build an interactive web application](https://spring.io/guides/gs/messaging-stomp-websocket/)——入门指南。 + +* [Stock Portfolio](https://github.com/rstoyanchev/spring-websocket-portfolio)—一个示例应用程序。 + +#### 4.4.4. WebSocket 服务器 + +要配置底层 WebSocket 服务器,应用[服务器配置](#websocket-server-runtime-configuration)中的信息。然而,对于 Jetty,你需要通过`StompEndpointRegistry`设置`HandshakeHandler`和`WebSocketPolicy`: + +``` +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/portfolio").setHandshakeHandler(handshakeHandler()); + } + + @Bean + public DefaultHandshakeHandler handshakeHandler() { + + WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER); + policy.setInputBufferSize(8192); + policy.setIdleTimeout(600000); + + return new DefaultHandshakeHandler( + new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy))); + } +} +``` + +#### 4.4.5.消息流 + +Spring 一旦公开了一个 STOMP 端点,应用程序就成为连接客户端的 STOMP 代理。本节描述服务器端的消息流。 + +`spring-messaging`模块包含对起源于[Spring Integration](https://spring.io/spring-integration)的消息传递应用程序的基本支持,该支持后来被提取并合并到 Spring 框架中,以便在许多[Spring projects](https://spring.io/projects)和应用程序场景中更广泛地使用。下面的列表简要描述了一些可用的消息传递抽象: + +* [Message](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/messaging/Message.html):消息的简单表示,包括消息头和有效负载。 + +* [MessageHandler](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/messaging/MessageHandler.html):处理消息的契约。 + +* [MessageChannel](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/messaging/MessageChannel.html):用于发送消息的契约,该消息允许在生产者和消费者之间进行松散耦合。 + +* [下标 bablechannel](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/messaging/SubscribableChannel.html):与`MessageHandler`订阅者的“MessageChannel”。 + +* [执行者下标 bablechannel](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/messaging/support/ExecutorSubscribableChannel.html):“subscribblechannel”,它使用`Executor`来传递消息。 + +Java 配置(即`@EnableWebSocketMessageBroker`)和 XML 名称空间配置(即``)都使用前面的组件来组装消息工作流。下图显示了启用简单的内置消息代理时使用的组件: + +![消息流简单代理](images/message-flow-simple-broker.png) + +前面的图表显示了三个消息通道: + +* `clientInboundChannel`:用于传递从 WebSocket 客户端接收的消息。 + +* `clientOutboundChannel`:用于向 WebSocket 客户端发送服务器消息。 + +* `brokerChannel`:用于从服务器端应用程序代码中向 Message Broker 发送消息。 + +下一个关系图显示了当外部代理(例如 RabbitMQ)被配置为管理订阅和广播消息时所使用的组件: + +![消息流代理中继](images/message-flow-broker-relay.png) + +前面两个图之间的主要区别是使用“代理中继”通过 TCP 将消息传递到外部的 Stomp 代理,并将消息从代理传递到订阅的客户机。 + +当从 WebSocket 连接接收消息时,将它们解码为 Stomp 帧,转换为 Spring `Message`表示,并将其发送到 `ClientInboundChannel’以进行进一步处理。例如,目标标头以`/app`开头的 stomp 消息可以路由到带注释的控制器中的`@MessageMapping`方法,而`/topic`和`/queue`消息可以直接路由到消息代理。 + +处理来自客户端的 stomp 消息的带注释的`@Controller`可以通过`brokerChannel`向消息代理发送消息,并且代理通过`clientOutboundChannel`将消息广播给匹配的订阅者。相同的控制器也可以响应 HTTP 请求执行相同的操作,因此客户端可以执行 HTTP POST,然后使用`@PostMapping`方法将消息发送到消息代理以广播到订阅的客户端。 + +我们可以通过一个简单的例子来追踪这个流程。考虑以下设置服务器的示例: + +``` +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/portfolio"); + } + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.setApplicationDestinationPrefixes("/app"); + registry.enableSimpleBroker("/topic"); + } +} + +@Controller +public class GreetingController { + + @MessageMapping("/greeting") + public String handle(String greeting) { + return "[" + getTimestamp() + ": " + greeting; + } +} +``` + +前面的示例支持以下流程: + +1. 客户机连接到`[http://localhost:8080/portfolio](http://localhost:8080/portfolio)`,并且,一旦建立了 WebSocket 连接,Stomp 帧就开始在其上流动。 + +2. 客户机发送一个订阅帧,其目的标头为`/topic/greeting`。一旦接收并解码,消息将被发送到`clientInboundChannel`,然后路由到消息代理,该代理存储客户端订阅。 + +3. 客户端将发送帧发送到`/app/greeting`。`/app`前缀有助于将其路由到带注释的控制器。在去掉`/app`前缀之后,剩余的`/greeting`部分目的地将映射到`@MessageMapping`中的`GreetingController`方法。 + +4. 将从`GreetingController`返回的值转换为 Spring `Message`,其有效负载基于返回值和默认的目的标头 `/topic/greeting`(派生自输入目的标头,用`/app`替换为 `/topic`)。生成的消息被发送到`brokerChannel`,并由消息代理处理。 + +5. 消息代理找到所有匹配的订阅者,并通过`clientOutboundChannel`向每个订阅者发送消息帧,从这里消息被编码为 Stomp 帧并在 WebSocket 连接上发送。 + +下一节将提供更多有关带注释方法的详细信息,包括所支持的参数和返回值的类型。 + +#### 4.4.6.带注释的控制器 + +应用程序可以使用带注释的`@Controller`类来处理来自客户端的消息。这样的类可以声明`@MessageMapping`、`@SubscribeMapping`和`@ExceptionHandler`方法,如以下主题中所述: + +* [@MessageMapping](#websocket-stomp-message-mapping) + +* [@subscribmapping](#websocket-stomp-subscribe-mapping) + +* [@MessageExceptionHandler](#websocket-stomp-exception-handler) + +##### `@MessageMapping` + +你可以使用`@MessageMapping`对基于目的地路由消息的方法进行注释。它在方法级和类型级都受到支持。在类型级别,`@MessageMapping`用于表示控制器中所有方法之间的共享映射。 + +默认情况下,映射值是 Ant 样式的路径模式(例如`/thing*`,`/thing/**`),包括对模板变量的支持(例如,`/thing/{id}`)。这些值可以通过`@DestinationVariable`方法参数进行引用。应用程序还可以切换到用于映射的以点分隔的目标约定,如[作为分隔器的点](#websocket-stomp-destination-separator)中所解释的那样。 + +###### 支持的方法参数 + +下表描述了方法参数: + +| Method argument |说明| +|-------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Message` |以获取完整的消息。| +| `MessageHeaders` |用于访问`Message`中的标题。| +|`MessageHeaderAccessor`, `SimpMessageHeaderAccessor`, and `StompHeaderAccessor`|用于通过类型化访问器方法访问标头。| +| `@Payload` |为了访问消息的有效负载,通过配置的“MessageConverter”转换(例如,从 JSON)。

    不需要存在此注释,因为默认情况下是这样,假设没有
    其他参数匹配。

    你可以用`@javax.validation.Valid`或 Spring 的`@Validated`、
    注释有效载荷参数,以使有效载荷参数自动生效。| +| `@Header` |用于访问特定的标头值——如果需要,还可以使用“org.springframework.core.convert.converter.converter”进行类型转换。| +| `@Headers` |用于访问消息中的所有头。此参数必须可分配给 `java.util.map’。| +| `@DestinationVariable` |用于访问从消息目标提取的模板变量。
    值将根据需要转换为声明的方法参数类型。| +| `java.security.Principal` |反映在 WebSocket HTTP 握手时登录的用户。| + +###### 返回值 + +默认情况下,来自`@MessageMapping`方法的返回值通过匹配的`MessageConverter`序列化到有效负载,并作为`Message`发送到`brokerChannel`,从那里向订阅者广播。出站消息的目的地与入站消息的目的地相同,但前缀为`/topic`。 + +你可以使用`@SendTo`和`@SendToUser`注释来定制输出消息的目标。`@SendTo`用于自定义目标目的地或指定多个目的地。`@SendToUser`用于将输出消息引导到仅与输入消息关联的用户。见[用户目的地](#websocket-stomp-user-destination)。 + +你可以在同一个方法上同时使用`@SendTo`和`@SendToUser`,并且这两个方法在类级别上都是受支持的,在这种情况下,它们充当类中方法的默认值。但是,请记住,任何方法级别的`@SendTo`或`@SendToUser`注释都会覆盖类级别的任何此类注释。 + +消息可以异步处理,并且`@MessageMapping`方法可以返回 `ListenableFuture’、`CompletableFuture`或`CompletionStage`。 + +请注意,`@SendTo`和`@SendToUser`仅仅是一种便利,相当于使用“SimpMessagingTemplate”发送消息。如果有必要,对于更高级的场景,“@MessageMapping”方法可以直接使用`SimpMessagingTemplate`。可以这样做,而不是返回一个值,或者可能是另外返回一个值。见[发送消息](#websocket-stomp-handle-send)。 + +##### `@SubscribeMapping` + +`@SubscribeMapping`类似于`@MessageMapping`,但仅将映射范围缩小到订阅消息。它支持与`@MessageMapping`相同的[方法参数](#websocket-stomp-message-mapping)。但是,对于返回值,默认情况下,消息是直接发送给客户机的(响应订阅的“Clientoutboundchannel”),而不是直接发送给经纪人的(通过“BrokerChannel”,作为对匹配订阅的广播)。添加`@SendTo`或 `@sendtouser’将重写此行为并将其发送给代理。 + +这个什么时候有用?假设代理映射到`/topic`和`/queue`,而应用程序控制器映射到`/app`。在此设置中,代理存储所有用于重复广播的`/topic`和`/queue`的订阅,并且不需要应用程序参与其中。客户机还可以订阅某些`/app`目标,控制器可以响应该订阅返回一个值,而不涉及代理,而无需存储或再次使用订阅(实际上是一次性的请求-回复交换)。这样做的一个用例是在启动时用初始数据填充 UI。 + +这什么时候没用?不要尝试将代理和控制器映射到相同的目标前缀,除非出于某种原因希望两者独立处理消息(包括订阅)。入站消息是并行处理的。不能保证代理或控制器是否首先处理给定的消息。如果目标是在订阅被存储并准备好广播时得到通知,那么如果服务器支持该订阅,客户端应该要求提供收据(Simple Broker 不支持)。例如,使用 Java[STOMP client](#websocket-stomp-client),你可以执行以下操作来添加收据: + +``` +@Autowired +private TaskScheduler messageBrokerTaskScheduler; + +// During initialization.. +stompClient.setTaskScheduler(this.messageBrokerTaskScheduler); + +// When subscribing.. +StompHeaders headers = new StompHeaders(); +headers.setDestination("/topic/..."); +headers.setReceipt("r1"); +FrameHandler handler = ...; +stompSession.subscribe(headers, handler).addReceiptTask(() -> { + // Subscription ready... +}); +``` + +服务器端选项是[to register](#websocket-stomp-interceptors)`brokerChannel`上的 `ExecutorChannelInterceptor’,并实现`afterMessageHandled`方法,该方法在处理包括订阅在内的消息后调用。 + +##### `@MessageExceptionHandler` + +应用程序可以使用`@MessageExceptionHandler`方法来处理来自 `@MessageMapping’方法的异常。如果希望访问异常实例,可以在注释本身中声明异常,也可以通过方法参数声明异常。下面的示例通过方法参数声明一个异常: + +``` +@Controller +public class MyController { + + // ... + + @MessageExceptionHandler + public ApplicationError handleException(MyException exception) { + // ... + return appError; + } +} +``` + +`@MessageExceptionHandler`方法支持灵活的方法签名,并支持与[@MessageMapping](#websocket-stomp-message-mapping)方法相同的方法参数类型和返回值。 + +通常,`@MessageExceptionHandler`方法应用于声明它们的`@Controller`类(或类层次结构)中。如果你希望此类方法在全局范围内(在控制器之间)应用得更多,那么可以在一个标有“@controlleradvice”的类中声明它们。这类似于 Spring MVC 中可用的[similar support](#mvc-ann-controller-advice)。 + +#### 4.4.7.发送消息 + +如果你想要从应用程序的任何部分向连接的客户端发送消息,该怎么办?任何应用程序组件都可以向`brokerChannel`发送消息。这样做的最简单的方法是注入`SimpMessagingTemplate`并使用它发送消息。通常,你将按类型注入它,如下例所示: + +``` +@Controller +public class GreetingController { + + private SimpMessagingTemplate template; + + @Autowired + public GreetingController(SimpMessagingTemplate template) { + this.template = template; + } + + @RequestMapping(path="/greetings", method=POST) + public void greet(String greeting) { + String text = "[" + getTimestamp() + "]:" + greeting; + this.template.convertAndSend("/topic/greetings", text); + } + +} +``` + +但是,如果存在另一个相同类型的 Bean,你也可以通过它的名称(“BrokerMessagingTemplate”)对其进行限定。 + +#### 4.4.8.简单经纪人 + +内置的简单消息代理处理来自客户端的订阅请求,将它们存储在内存中,并将消息广播到具有匹配目标的连接客户端。代理支持类似路径的目标,包括对 Ant 风格的目标模式的订阅。 + +| |应用程序也可以使用点分隔(而不是斜杠分隔)的目的地。
    参见[作为分隔器的点](#websocket-stomp-destination-separator)。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------| + +如果配置了任务调度程序,那么简单代理支持[跺脚心跳](https://stomp.github.io/stomp-specification-1.2.html#Heart-beating)。要配置计划程序,你可以声明自己的`TaskScheduler` Bean,并通过`MessageBrokerRegistry`对其进行设置。或者,你可以使用在内置 WebSocket 配置中自动声明的配置,但是,你需要`@Lazy`来避免在内置 WebSocket 配置和你的“WebSocketMessageBrokerConfigrer”之间的循环。例如: + +``` +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + private TaskScheduler messageBrokerTaskScheduler; + + @Autowired + public void setMessageBrokerTaskScheduler(@Lazy TaskScheduler taskScheduler) { + this.messageBrokerTaskScheduler = taskScheduler; + } + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.enableSimpleBroker("/queue/", "/topic/") + .setHeartbeatValue(new long[] {10000, 20000}) + .setTaskScheduler(this.messageBrokerTaskScheduler); + + // ... + } +} +``` + +#### 4.4.9.外部经纪人 + +Simple Broker 非常适合入门,但只支持一组 STOMP 命令(它不支持 ACK、Receipts 和其他一些特性),依赖于一个简单的消息发送循环,并且不适合集群。作为替代方案,你可以升级应用程序以使用功能齐全的消息代理。 + +请参阅 STOMP 文档,了解你选择的消息代理(例如[RabbitMQ](https://www.rabbitmq.com/stomp.html),[ActiveMQ](https://activemq.apache.org/stomp.html)等),安装代理,并在启用了 STOMP 支持的情况下运行它。然后,你可以在 Spring 配置中启用 Stomp 代理中继(而不是简单的代理)。 + +下面的示例配置启用了功能齐全的代理: + +``` +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void registerStompEndpoints(StompEndpointRegistry registry) { + registry.addEndpoint("/portfolio").withSockJS(); + } + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.enableStompBrokerRelay("/topic", "/queue"); + registry.setApplicationDestinationPrefixes("/app"); + } + +} +``` + +下面的示例展示了与前面示例类似的 XML 配置: + +``` + + + + + + + + + + +``` + +前面配置中的 Stomp 代理中继是 Spring [“MessageHandler”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/messaging/MessageHandler.html),它通过将消息转发给外部消息代理来处理消息。为此,它建立到代理的 TCP 连接,将所有消息转发给它,然后将从代理收到的所有消息通过其 WebSocket 会话转发给客户机。从本质上讲,它充当了双向转发消息的“中继”。 + +| |为 TCP 连接管理将`io.projectreactor.netty:reactor-netty`和`io.netty:netty-all`依赖项添加到项目中。| +|---|-------------------------------------------------------------------------------------------------------------------------------| + +此外,应用程序组件(例如 HTTP 请求处理方法、业务服务和其他)也可以向代理中继发送消息,如[发送消息](#websocket-stomp-handle-send)中所述,以将消息广播到订阅的 WebSocket 客户端。 + +实际上,代理中继支持健壮和可伸缩的消息广播。 + +#### 4.4.10.连接到代理 + +Stomp 代理中继维护与代理的单个“系统”TCP 连接。此连接仅用于源自服务器端应用程序的消息,而不用于接收消息。可以为此连接配置 STOMP 凭据(即 STOMP 框架`login`和`passcode`标头)。这在 XML 名称空间和 Java 配置中都公开为`systemLogin`和 `SystemPasscode’属性,其默认值为`guest`和`guest`。 + +Stomp 代理中继还为每个连接的 WebSocket 客户端创建一个单独的 TCP 连接。你可以配置用于代表客户机创建的所有 TCP 连接的 STOMP 凭据。这在 XML 名称空间和 Java 配置中都公开为`clientLogin`和`clientPasscode`属性,其默认值为`guest`和`guest`。 + +| |Stomp 代理中继总是在它代表客户转发给代理的每个`CONNECT`框架上设置`login`和`passcode`头。因此, WebSocket 客户机
    不需要设置这些头。他们被忽视了。正如[Authentication](#websocket-stomp-authentication)部分所解释的那样, WebSocket 客户端应该依赖 HTTP 身份验证来保护
    WebSocket 端点并建立客户端标识。| +|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +Stomp 代理中继还通过“System”TCP 连接向消息代理发送和接收来自消息代理的心跳。你可以配置发送和接收心跳的间隔(默认情况下各为 10 秒)。如果失去了与代理的连接,代理中继将继续尝试每 5 秒重新连接一次,直到成功。 + +任何 Spring Bean 都可以实现`ApplicationListener`,以在与代理的“系统”连接丢失并重新建立时接收通知。例如,当没有活动的“系统”连接时,广播股票报价的股票报价服务可以停止尝试发送消息。 + +默认情况下,STOMP 代理中继总是连接到相同的主机和端口,如果连接丢失,则根据需要重新连接。如果你希望提供多个地址,那么在每次尝试连接时,你可以配置一个地址供应商,而不是一个固定的主机和端口。下面的示例展示了如何做到这一点: + +``` +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer { + + // ... + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.enableStompBrokerRelay("/queue/", "/topic/").setTcpClient(createTcpClient()); + registry.setApplicationDestinationPrefixes("/app"); + } + + private ReactorNettyTcpClient createTcpClient() { + return new ReactorNettyTcpClient<>( + client -> client.addressSupplier(() -> ... ), + new StompReactorNettyCodec()); + } +} +``` + +你还可以使用`virtualHost`属性配置 Stomp 代理中继。此属性的值被设置为每个`CONNECT`帧的`host`头,并且可以是有用的(例如,在云环境中,其中建立 TCP 连接的实际主机与提供基于云的 Stomp 服务的主机不同)。 + +#### 4.4.11.作为分隔器的点 + +当消息路由到`@MessageMapping`方法时,它们将与“Antpathmatcher”匹配。默认情况下,模式应该使用斜杠作为分隔符。这是 Web 应用程序中的一种很好的约定,类似于 HTTP URL。但是,如果你更习惯于消息传递约定,则可以切换到使用 dot 作为分隔符。 + +下面的示例展示了如何在 Java 配置中实现这一点: + +``` +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + // ... + + @Override + public void configureMessageBroker(MessageBrokerRegistry registry) { + registry.setPathMatcher(new AntPathMatcher(".")); + registry.enableStompBrokerRelay("/queue", "/topic"); + registry.setApplicationDestinationPrefixes("/app"); + } +} +``` + +下面的示例展示了与前面示例类似的 XML 配置: + +``` + + + + + + + + + + + + +``` + +在此之后,控制器可以在`@MessageMapping`方法中使用一个点作为分隔符,如下例所示: + +``` +@Controller +@MessageMapping("red") +public class RedController { + + @MessageMapping("blue.{green}") + public void handleGreen(@DestinationVariable String green) { + // ... + } +} +``` + +客户机现在可以向`/app/red.blue.green123`发送消息。 + +在前面的示例中,我们没有更改“代理中继”的前缀,因为这些前缀完全依赖于外部消息代理。请参阅你使用的代理的 STOMP 文档页,以了解它为目标头支持哪些约定。 + +另一方面,“简单代理”确实依赖于配置的`PathMatcher`,因此,如果你切换分隔符,该更改也适用于代理以及代理从消息到订阅模式的目标匹配方式。 + +#### 4.4.12.认证 + +在 WebSocket 消息传递会话中的每一次重击都是从一个 HTTP 请求开始的。这可以是一个升级到 WebSockets 的请求(即 WebSocket 握手),或者在 Sockjs 回退的情况下,是一系列 Sockjs HTTP 传输请求。 + +许多 Web 应用程序已经具有适当的身份验证和授权,以保护 HTTP 请求。通常,通过使用诸如登录页面、HTTP 基本身份验证或另一种方式的某种机制,通过 Spring 安全性对用户进行身份验证。经过身份验证的用户的安全上下文保存在 HTTP 会话中,并与同一基于 Cookie 的会话中的后续请求相关联。 + +因此,对于 WebSocket 握手或 Sockjs HTTP 传输请求,通常已经有一个经过身份验证的用户可以通过 `HttpServletRequest#getUserPrincipal()’访问。 Spring 自动地将该用户与为他们创建的 WebSocket 或 Sockjs 会话关联,随后,与通过该会话通过用户头传输的所有 Stomp 消息关联。 + +简而言之,一个典型的 Web 应用程序只需要做它在安全性方面已经做过的事情。通过基于 Cookie 的 HTTP 会话(该会话随后与为该用户创建的 WebSocket 或 Sockjs 会话相关联)维护安全上下文,在 HTTP 请求级别上对用户进行身份验证,并在流经该应用程序的每个`Message`上标记一个用户标头。 + +在`CONNECT`框架上,Stomp 协议确实有`login`和`passcode`头。它们最初是为 TCP 上的 Stomp 而设计的,现在也需要这样做。然而,对于 STOMP over WebSocket,默认情况下, Spring 忽略了 STOMP 协议级别上的身份验证头,并假定用户已经在 HTTP 传输级别上进行了身份验证。期望 WebSocket 或 Sockjs 会话包含经过身份验证的用户。 + +#### 4.4.13.令牌认证 + +[Spring Security OAuth](https://github.com/spring-projects/spring-security-oauth)提供了对基于令牌的安全性的支持,包括 JSON Web 令牌。你可以将其用作 Web 应用程序中的身份验证机制,包括对 WebSocket 交互的 stomp,如上一节所述(即,通过基于 Cookie 的会话来维护身份)。 + +同时,基于 Cookie 的会话并不总是最合适的(例如,在不维护服务器端会话的应用程序中,或者在通常使用头进行身份验证的移动应用程序中)。 + +[WebSocket protocol, RFC 6455](https://tools.ietf.org/html/rfc6455#section-10.5)“并没有规定服务器在 WebSocket 次握手过程中对客户端进行身份验证的任何特定方式。”然而,在实践中,浏览器客户机只能使用标准的身份验证头(即基本的 HTTP 身份验证)或 Cookie,并且不能(例如)提供自定义的头。同样,Sockjs JavaScript 客户机也不提供一种发送带有 Sockjs 传输请求的 HTTP 头的方法。见[Sockjs-客户端第 196 期](https://github.com/sockjs/sockjs-client/issues/196)。相反,它确实允许发送查询参数,你可以使用这些参数来发送令牌,但这有其自身的缺点(例如,令牌可能会无意中与服务器日志中的 URL 一起记录)。 + +| |上述限制适用于基于浏览器的客户机,并且不适用于
    Spring 基于 Java 的 Stomp 客户机,该客户机确实支持发送带有
    WebSocket 和 Sockjs 请求的头。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +因此,希望避免使用 Cookie 的应用程序可能没有任何好的 HTTP 协议级别的身份验证替代方案。与其使用 Cookie,他们可能更喜欢在 Stomp 消息传递协议级别使用头进行身份验证。这样做需要两个简单的步骤: + +1. 使用 STOMP 客户机在连接时传递身份验证头。 + +2. 用`ChannelInterceptor`处理身份验证头。 + +下一个示例使用服务器端配置来注册自定义身份验证拦截器。请注意,拦截器只需要验证和设置 Connect`Message`上的用户头。 Spring 记录并保存经过身份验证的用户,并将其与相同会话上的后续 Stomp 消息关联。下面的示例展示了如何注册自定义身份验证拦截器: + +``` +@Configuration +@EnableWebSocketMessageBroker +public class MyConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(new ChannelInterceptor() { + @Override + public Message preSend(Message message, MessageChannel channel) { + StompHeaderAccessor accessor = + MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); + if (StompCommand.CONNECT.equals(accessor.getCommand())) { + Authentication user = ... ; // access authentication header(s) + accessor.setUser(user); + } + return message; + } + }); + } +} +``` + +另外,请注意,当你对消息使用 Spring Security 的授权时,目前你需要确保身份验证`ChannelInterceptor`配置是在 Spring Security 的授权之前进行的。最好的方法是在其自己的`WebSocketMessageBrokerConfigurer`实现中声明自定义拦截器,该拦截器被标记为 `@Order(Order.Highest_Precision+99)’。 + +#### 4.4.14.授权 + +Spring 安全性提供了[WebSocket sub-protocol authorization](https://docs.spring.io/spring-security/reference/servlet/integrations/websocket.html#websocket-authorization),它使用`ChannelInterceptor`基于其中的用户头来授权消息。此外, Spring 会话提供了[WebSocket integration](https://docs.spring.io/spring-session/reference/web-socket.html),以确保在 WebSocket 会话仍然处于活动状态时用户的 HTTP 会话不会过期。 + +#### 4.4.15.用户目的地 + +应用程序可以发送针对特定用户的消息, Spring 的 STOMP 支持为此目的识别带有`/user/`前缀的目的地。例如,客户机可能订阅`/user/queue/position-updates`Destination。`UserDestInationMessageHandler` 处理此目的地并将其转换为用户会话所独有的目的地(例如`/queue/position-updates-user123`)。这提供了订阅一个通用命名的目的地的便利,同时,确保不与订阅相同目的地的其他用户发生冲突,以便每个用户都可以接收唯一的股票位置更新。 + +| |在使用用户目标时,配置代理和
    应用程序目标前缀是很重要的,如[Enable STOMP](#websocket-stomp-enable)中所示,否则
    代理将处理带“/user”前缀的消息,这些消息只应由 `userdestinationMessageHandler’处理。| +|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在发送端,消息可以被发送到诸如“/user/{username}/queue/position-updates”这样的目的地,而这又被`UserDestinationMessageHandler`翻译成一个或多个目的地,每个会话对应一个与用户关联的目的地。这使得应用程序中的任何组件都可以发送针对特定用户的消息,而不必知道他们的名字和通用目的地以外的任何信息。这也通过注释和消息传递模板得到了支持。 + +一种消息处理方法可以通过`@SendToUser`注释(也支持在类级上共享一个公共目的地)向与正在处理的消息相关联的用户发送消息,如下例所示: + +``` +@Controller +public class PortfolioController { + + @MessageMapping("/trade") + @SendToUser("/queue/position-updates") + public TradeResult executeTrade(Trade trade, Principal principal) { + // ... + return tradeResult; + } +} +``` + +如果用户有一个以上的会话,默认情况下,目标用户是订阅给定目标的所有会话。然而,有时可能需要只针对发送要处理的消息的会话。可以通过将`broadcast`属性设置为 false 来实现此目的,如下例所示: + +``` +@Controller +public class MyController { + + @MessageMapping("/action") + public void handleAction() throws Exception{ + // raise MyBusinessException here + } + + @MessageExceptionHandler + @SendToUser(destinations="/queue/errors", broadcast=false) + public ApplicationError handleException(MyBusinessException exception) { + // ... + return appError; + } +} +``` + +| |虽然用户目的地通常意味着经过身份验证的用户,但并不是严格要求的。
    与经过身份验证的用户不关联的 WebSocket 会话
    可以订阅用户目的地。在这种情况下,`@SendToUser`注释
    的行为与`broadcast=false`完全相同(即仅针对发送正在处理的消息的
    会话)。| +|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +你可以通过注入由 Java 配置或 XML 命名空间创建的`SimpMessagingTemplate`,从任何应用程序组件向用户目的地发送消息。(如果使用`@Qualifier`进行限定,则 Bean 名称为`brokerMessagingTemplate`。)下面的示例展示了如何这样做: + +``` +@Service +public class TradeServiceImpl implements TradeService { + + private final SimpMessagingTemplate messagingTemplate; + + @Autowired + public TradeServiceImpl(SimpMessagingTemplate messagingTemplate) { + this.messagingTemplate = messagingTemplate; + } + + // ... + + public void afterTradeExecuted(Trade trade) { + this.messagingTemplate.convertAndSendToUser( + trade.getUserName(), "/queue/position-updates", trade.getResult()); + } +} +``` + +| |当你使用带有外部消息代理的用户目的地时,你应该检查代理
    关于如何管理非活动队列的文档,这样,当用户会话
    结束时,所有唯一的用户队列都将被删除。例如,当你使用诸如`/exchange/amq.direct/position-updates`之类的目标时,RabbitMQ 会创建自动删除
    队列。
    因此,在这种情况下,客户端可以订阅`/user/exchange/amq.direct/position-updates`。
    类似地,ActiveMQ 也有[配置选项](https://activemq.apache.org/delete-inactive-destinations.html)用于清除不活动的目标。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在多应用服务器场景中,由于用户连接到不同的服务器,用户目的地可能仍未解决。在这种情况下,你可以配置一个目标来广播未解决的消息,以便其他服务器有机会尝试。这可以通过 Java 配置中“MessageBrokerRegistry”的`userDestinationBroadcast`属性和 XML 中`message-broker`元素的`user-destination-broadcast`属性来完成。 + +#### 4.4.16.消息顺序 + +来自代理的消息被发布到`clientOutboundChannel`,从那里它们被写到 WebSocket 会话。由于通道由`ThreadPoolExecutor`支持,消息在不同的线程中进行处理,客户端接收到的结果序列可能与发布的确切顺序不匹配。 + +如果这是一个问题,请启用`setPreservePublishOrder`标志,如下例所示: + +``` +@Configuration +@EnableWebSocketMessageBroker +public class MyConfig implements WebSocketMessageBrokerConfigurer { + + @Override + protected void configureMessageBroker(MessageBrokerRegistry registry) { + // ... + registry.setPreservePublishOrder(true); + } + +} +``` + +下面的示例展示了与前面示例类似的 XML 配置: + +``` + + + + + + + +``` + +设置该标志后,同一客户端会话中的消息将一次一个地发布到“ClientOutboundChannel”,以保证发布的顺序。请注意,这会带来很小的性能开销,因此你应该仅在需要时才启用它。 + +#### 4.4.17.事件 + +发布了几个`ApplicationContext`事件,并且可以通过实现 Spring 的`ApplicationListener`接口来接收这些事件: + +* `BrokerAvailabilityEvent`:表示代理何时变得可用或不可用。虽然“简单”代理在启动时立即可用,并且在应用程序运行时仍然可用,但 STOMP“代理中继”可能会失去与功能齐全的代理的连接(例如,如果代理被重新启动)。代理中继具有重新连接逻辑,并在代理恢复时重新建立与代理的“系统”连接。因此,每当状态从连接变为断开时,此事件就会发布,反之亦然。使用`SimpMessagingTemplate`的组件应该订阅此事件,并避免在代理不可用时发送消息。在任何情况下,他们都应该准备好在发送消息时处理`MessageDeliveryException`。 + +* `SessionConnectEvent`:当接收到新的 Stomp 连接时发布,以表示新客户端会话的开始。该事件包含表示连接的消息,包括会话 ID、用户信息(如果有的话)以及客户端发送的任何自定义标头。这对于跟踪客户端会话非常有用。订阅此事件的组件可以用`SimpMessageHeaderAccessor`或 `StompMessageHeaderAccessor’包装所包含的消息。 + +* `SessionConnectedEvent`:在`SessionConnectEvent`之后不久发布,此时代理已发送一个 stomp 连接帧以响应该连接。在这一点上,Stomp 会话可以被认为是完全成立的。 + +* `SessionSubscribeEvent`:在接收到新的 Stomp 订阅时发布。 + +* `SessionUnsubscribeEvent`:当收到新的 stomp 退订时发布。 + +* `SessionDisconnectEvent`:在 stomp 会话结束时发布。断开连接可以是从客户端发送的,也可以是在 WebSocket 会话关闭时自动生成的。在某些情况下,此事件在每个会话中发布不止一次。对于多个断开事件,组件应该是幂等的。 + +| |当你使用功能齐全的代理时,如果代理暂时不可用,则 STOMP“代理中继”会自动重新连接
    “系统”连接。但是,
    客户端连接不会自动重新连接。假设启用了心跳,客户机
    通常会注意到代理在 10 秒内没有响应。客户端需要
    实现自己的重新连接逻辑。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +#### 4.4.18.拦截 + +[Events](#websocket-stomp-appplication-context-events)为 stomp 连接的生命周期提供通知,但不是为每个客户机消息提供通知。应用程序还可以注册一个“通道拦截器”来拦截任何消息和处理链的任何部分。下面的示例展示了如何截获来自客户端的入站消息: + +``` +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + registration.interceptors(new MyChannelInterceptor()); + } +} +``` + +自定义`ChannelInterceptor`可以使用`StompHeaderAccessor`或`SimpMessageHeaderAccessor`来访问有关消息的信息,如下例所示: + +``` +public class MyChannelInterceptor implements ChannelInterceptor { + + @Override + public Message preSend(Message message, MessageChannel channel) { + StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message); + StompCommand command = accessor.getStompCommand(); + // ... + return message; + } +} +``` + +应用程序还可以实现`ExecutorChannelInterceptor`,这是`ChannelInterceptor`的子接口,在处理消息的线程中具有回调。对于发送到通道的每条消息,都会调用一次`ChannelInterceptor`,而 `ExecutorChannelInterceptor’在每个`MessageHandler`订阅通道消息的线程中提供钩子。 + +注意,与前面描述的`SessionDisconnectEvent`一样,断开连接消息可以是来自客户端的,或者也可以是在 WebSocket 会话关闭时自动生成的。在某些情况下,拦截器可能会在每个会话中多次拦截此消息。对于多个断开事件,组件应该是幂等的。 + +#### 4.4.19.STOMP 客户端 + +Spring 提供了在 WebSocket 客户端上的 stomp 和在 TCP 客户端上的 stomp。 + +首先,你可以创建和配置`WebSocketStompClient`,如下例所示: + +``` +WebSocketClient webSocketClient = new StandardWebSocketClient(); +WebSocketStompClient stompClient = new WebSocketStompClient(webSocketClient); +stompClient.setMessageConverter(new StringMessageConverter()); +stompClient.setTaskScheduler(taskScheduler); // for heartbeats +``` + +在前面的示例中,你可以将`StandardWebSocketClient`替换为`SockJsClient`,因为这也是`WebSocketClient`的实现。`SockJsClient`可以使用 WebSocket 或基于 HTTP 的传输作为后备。更多详情见[`SockJsClient`](#websocket-fallback-sockjs-client)。 + +接下来,你可以建立一个连接,并为 STOMP 会话提供一个处理程序,如下例所示: + +``` +String url = "ws://127.0.0.1:8080/endpoint"; +StompSessionHandler sessionHandler = new MyStompSessionHandler(); +stompClient.connect(url, sessionHandler); +``` + +当会话准备好使用时,将通知处理程序,如下例所示: + +``` +public class MyStompSessionHandler extends StompSessionHandlerAdapter { + + @Override + public void afterConnected(StompSession session, StompHeaders connectedHeaders) { + // ... + } +} +``` + +一旦建立了会话,就可以发送任何有效负载,并使用配置的`MessageConverter`进行序列化,如下例所示: + +``` +session.send("/topic/something", "payload"); +``` + +你也可以订阅目的地。`subscribe`方法需要一个订阅消息的处理程序,并返回一个`Subscription`句柄,你可以使用它来取消订阅。对于每条接收到的消息,处理程序可以指定有效负载应该反序列化到的目标“对象”类型,如下例所示: + +``` +session.subscribe("/topic/something", new StompFrameHandler() { + + @Override + public Type getPayloadType(StompHeaders headers) { + return String.class; + } + + @Override + public void handleFrame(StompHeaders headers, Object payload) { + // ... + } + +}); +``` + +要启用 Stomp heartbeat,你可以使用`WebSocketStompClient`配置`TaskScheduler`并可选地自定义心跳间隔(10 秒用于写不活动,这会导致发送心跳;10 秒用于读不活动,这会关闭连接)。 + +`WebSocketStompClient`仅在不活动的情况下发送心跳,即没有发送其他消息时。当使用外部代理时,这可能会带来挑战,因为具有非代理目的地的消息表示活动,但 AREN 并未实际转发给代理。在这种情况下,你可以在初始化`TaskScheduler`时配置`TaskScheduler`,从而确保仅在发送具有非代理目的地的消息时也将心跳转发到代理。 + +| |当你使用`WebSocketStompClient`进行性能测试以模拟来自同一台机器的数千个
    客户端时,请考虑关闭心跳,因为每个
    连接都调度自己的心跳任务,而这并不是针对
    在同一台机器上运行的大量客户端进行优化的。| +|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +STOMP 协议还支持收据,其中客户端必须添加`receipt`头,在处理发送或订阅后,服务器将用收据帧对其进行响应。为了支持这一点,`StompSession`提供了 `setAutoReceive(布尔)’’,该’setAutoReceive’’导致在随后的每个发送或订阅事件上添加`receipt`头。或者,你也可以手动将收据标题添加到`StompHeaders`中。send 和 subscribe 都返回`Receiptable`的实例,你可以使用该实例来注册接收成功和失败的回调。对于此功能,你必须为客户机配置`TaskScheduler`和收据过期前的时间(默认情况下为 15 秒)。 + +请注意,`StompSessionHandler`本身是`StompFrameHandler`,这使得它除了用于处理消息异常的`handleException`回调和用于处理包括`ConnectionLostException`在内的传输级别错误的`handleTransportError`回调外,还可以处理错误帧。 + +#### 4.4.20. WebSocket 范围 + +每个 WebSocket 会话都有一个属性映射。映射作为头附加到入站客户端消息,并且可以从控制器方法访问,如以下示例所示: + +``` +@Controller +public class MyController { + + @MessageMapping("/action") + public void handle(SimpMessageHeaderAccessor headerAccessor) { + Map attrs = headerAccessor.getSessionAttributes(); + // ... + } +} +``` + +你可以在`websocket`范围中声明一个 Spring 管理的 Bean。你可以将 WebSocket 范围的 bean 注入控制器和在`clientInboundChannel`上注册的任何通道拦截器。这些通常是单例,并且比任何单独的会话 WebSocket 活得更长。因此,你需要对 WebSocket 范围的 bean 使用范围代理模式,如下例所示: + +``` +@Component +@Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS) +public class MyBean { + + @PostConstruct + public void init() { + // Invoked after dependencies injected + } + + // ... + + @PreDestroy + public void destroy() { + // Invoked when the WebSocket session ends + } +} + +@Controller +public class MyController { + + private final MyBean myBean; + + @Autowired + public MyController(MyBean myBean) { + this.myBean = myBean; + } + + @MessageMapping("/action") + public void handle() { + // this.myBean from the current WebSocket session + } +} +``` + +与任何自定义作用域一样, Spring 在第一次从控制器访问新的`MyBean`实例时初始化该实例,并将该实例存储在 WebSocket 会话属性中。随后将返回相同的实例,直到会话结束。 WebSocket-作用域 bean 具有调用的所有 Spring 生命周期方法,如前面的示例中所示。 + +#### 4.4.21.表现 + +谈到业绩,没有灵丹妙药。许多因素都会影响它,包括消息的大小和数量,应用程序方法是否执行需要阻塞的工作,以及外部因素(例如网络速度和其他问题)。本节的目标是提供可用配置选项的概述,以及关于如何推理缩放的一些想法。 + +在消息传递应用程序中,消息通过通道传递,以进行由线程池支持的异步执行。配置这样的应用程序需要对通道和消息流有很好的了解。因此,建议复习[消息流](#websocket-stomp-message-flow)。 + +显而易见的开始是配置线程池,这些线程池支持“clientinboundchannel”和`clientOutboundChannel`。默认情况下,这两个处理器的配置都是可用处理器数量的两倍。 + +如果在带注释的方法中处理消息主要是 CPU 绑定的,则`clientInboundChannel`的线程数量应保持与处理器数量接近。如果他们所做的工作更受 IO 约束,并且需要阻塞或等待数据库或其他外部系统,则线程池的大小可能需要增加。 + +| |`ThreadPoolExecutor`有三个重要的属性:核心线程池大小,
    最大线程池大小,以及队列存储
    没有可用线程的任务的能力。

    一个常见的混淆之处是,配置核心池大小(例如,10)
    和最大线程池大小(例如,20)会导致线程池中包含 10 到 20 个线程,实际上,如果将容量保持在其默认值 integer.max\_value,
    ,则线程池永远不会超过核心池大小而增加,由于
    所有额外的任务都被排队。

    参见`ThreadPoolExecutor`的 Javadoc 来了解这些属性是如何工作的,以及
    了解各种排队策略。| +|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| + +在`clientOutboundChannel`方面,这完全是关于向 WebSocket 客户端发送消息。如果客户机在快速网络上,线程的数量应该与可用处理器的数量保持接近。如果它们速度较慢或带宽较低,则会花费更长的时间来消耗消息,并给线程池带来负担。因此,增加线程池的大小是必要的。 + +虽然`clientInboundChannel`的工作负载是可以预测的——毕竟,它是基于应用程序所做的工作——但如何配置“ClientoutboundChannel”比较困难,因为它基于应用程序无法控制的因素。因此,有两个额外的属性与消息的发送有关:`sendTimeLimit`和`sendBufferSizeLimit`。你可以使用这些方法来配置允许发送多长时间,以及在向客户机发送消息时可以缓冲多少数据。 + +一般的想法是,在任何给定的时间,只能使用单个线程发送到客户端。同时,所有附加的消息都会得到缓冲,你可以使用这些属性来决定允许发送消息需要多长时间,以及在此期间可以缓冲多少数据。有关重要的附加详细信息,请参见 XMLSchema 的 Javadoc 和文档。 + +下面的示例展示了一种可能的配置: + +``` +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void configureWebSocketTransport(WebSocketTransportRegistration registration) { + registration.setSendTimeLimit(15 * 1000).setSendBufferSizeLimit(512 * 1024); + } + + // ... + +} +``` + +下面的示例展示了与前面示例类似的 XML 配置: + +``` + + + + + + + + +``` + +还可以使用前面显示的 WebSocket 传输配置来配置传入的 STOMP 消息的最大允许大小。从理论上讲, WebSocket 条消息的大小几乎可以是无限的。在实践中, WebSocket 服务器施加了限制——例如,在 Tomcat 上施加 8K,在 Jetty 上施加 64K。出于这个原因,Stomp 客户机(例如 JavaScript[webstomp-client](https://github.com/JSteunou/webstomp-client)和其他)在 16K 边界分割较大的 Stomp 消息,并将它们作为多个消息发送 WebSocket,这需要服务器进行缓冲和重新组装。 + +Spring 的 Stomp-over- WebSocket 支持做到了这一点,因此应用程序可以为 Stomp 消息配置最大大小,而与 WebSocket 服务器特定的消息大小无关。请记住, WebSocket 消息大小是自动调整的,如果需要的话,以确保它们能够至少携带 16k WebSocket 消息。 + +下面的示例展示了一种可能的配置: + +``` +@Configuration +@EnableWebSocketMessageBroker +public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + + @Override + public void configureWebSocketTransport(WebSocketTransportRegistration registration) { + registration.setMessageSizeLimit(128 * 1024); + } + + // ... + +} +``` + +下面的示例展示了与前面示例类似的 XML 配置: + +``` + + + + + + + + +``` + +关于扩展的一个要点是使用多个应用程序实例。目前,你无法使用简单的代理来实现这一点。然而,当使用全功能代理(例如 RabbitMQ)时,每个应用程序实例都连接到代理,并且从一个应用程序实例广播的消息可以通过代理广播到通过任何其他应用程序实例连接的客户端 WebSocket。 + +#### 4.4.22.监测 + +当使用`@EnableWebSocketMessageBroker`或``时,关键基础设施组件会自动收集统计信息和计数器,它们为应用程序的内部状态提供了重要的见解。该配置还声明了类型`WebSocketMessageBrokerStats`的 Bean,该类型在一个地方收集所有可用的信息,并且默认情况下每 30 分钟将其记录在`INFO`级别。这个 Bean 可以通过 Spring 的 `mBeanExporter’导出到 JMX,以便在运行时查看(例如,通过 JDK 的`jconsole`)。以下清单总结了现有的信息: + +客户端 WebSocket 会话 + +当前 + +指示当前有多少个客户端会话,计数进一步细分为 WebSocket 与 HTTP 流和 Polling Sockjs 会话的比较。 + +合计 + +指示总共建立了多少个会话。 + +异常关闭 + +连接失败 + +建立了会话,但在 60 秒内没有收到任何消息后关闭。这通常表示代理或网络问题。 + +超过发送限制 + +会话在超过配置的发送超时或发送缓冲区限制后关闭,这可能发生在客户端速度较慢的情况下(请参见上一节)。 + +传输错误 + +会话在传输错误之后关闭,例如未能读或写到 WebSocket 连接或 HTTP 请求或响应。 + +Stomp 框架 + +处理的连接帧、连接帧和断开帧的总数,表示在 STOMP 级别上连接了多少个客户端。请注意,当会话异常关闭或当客户端关闭而不发送断开连接帧时,断开连接计数可能会更低。 + +Stomp 经纪商接力 + +TCP 连接 + +指示代表客户端 WebSocket 会话向代理建立了多少 TCP 连接。这应该等于客户端 WebSocket 会话的数量 + 用于从应用程序内发送消息的 1 个额外的共享“系统”连接。 + +Stomp 框架 + +代表客户端转发给代理或从代理接收的连接、已连接和断开连接帧的总数。请注意,不管客户端 WebSocket 会话是如何关闭的,断开连接帧都会被发送到代理。因此,较低的断开帧计数表示代理正在主动关闭连接(可能是由于心跳没有及时到达,输入帧无效或其他问题)。 + +客户端入站通道 + +来自线程池的统计数据支持`clientInboundChannel`,这些统计数据提供了对传入消息处理的健康状况的深入了解。在此排队的任务表明应用程序可能太慢而无法处理消息。如果存在 I/O 绑定任务(例如,缓慢的数据库查询、对第三方 REST API 的 HTTP 请求等),请考虑增加线程池大小。 + +客户端出站通道 + +来自线程池的统计数据支持`clientOutboundChannel`,该线程池提供了对向客户广播消息的健康状况的深入了解。在这里排队等待的任务表明客户端太慢,无法使用消息。解决这个问题的一种方法是增加线程池大小,以适应预期的并发慢客户端数量。另一种选择是减少发送超时和发送缓冲区大小限制(请参见上一节)。 + +Sockjs 任务调度程序 + +来自用于发送心跳的 SockJS 任务计划程序的线程池的统计信息。请注意,当心跳在 Stomp 级别协商时,Sockjs 心跳将被禁用。 + +#### 4.4.23.测试 + +在使用 Spring 的 Stomp-over- WebSocket 支持时,有两种主要的方法来测试应用程序。第一种方法是编写服务器端测试,以验证控制器的功能及其带注释的消息处理方法。第二种方法是编写完整的端到端测试,其中涉及运行客户机和服务器。 + +这两种方法并不相互排斥。相反,它们在总体测试策略中都有一席之地。服务器端测试更加集中,并且更容易编写和维护。另一方面,端到端集成测试更完整,测试更多,但它们也更多地参与编写和维护。 + +服务器端测试的最简单形式是编写控制器单元测试。然而,这是不够有用的,因为控制器所做的很大程度上取决于它的注释。纯粹的单元测试根本无法对此进行测试。 + +理想情况下,测试中的控制器应该像在运行时那样被调用,这很像通过使用 Spring MVC 测试框架来测试处理 HTTP 请求的控制器的方法——也就是说,不运行 Servlet 容器,而是依赖 Spring 框架来调用带注释的控制器。与 Spring MVC 测试一样,这里有两种可能的选择,要么使用“基于上下文”的设置,要么使用“独立”的设置: + +* 借助 Spring TestContext 框架加载实际的 Spring 配置,注入`clientInboundChannel`作为测试字段,并使用它发送要由控制器方法处理的消息。 + +* 手动设置调用控制器(即`SimpAnnotationMethodMessageHandler`)所需的最低 Spring 框架基础设施,并将控制器的消息直接传递给它。 + +这两种设置场景都在[股票投资组合的测试](https://github.com/rstoyanchev/spring-websocket-portfolio/tree/master/src/test/java/org/springframework/samples/portfolio/web)示例应用程序中进行了演示。 + +第二种方法是创建端到端集成测试。为此,你需要以嵌入式模式运行 WebSocket 服务器,并将其作为发送 WebSocket 包含 STOMP 帧的消息的 WebSocket 客户端连接到该服务器。[股票投资组合的测试](https://github.com/rstoyanchev/spring-websocket-portfolio/tree/master/src/test/java/org/springframework/samples/portfolio/web)示例应用程序还通过使用 Tomcat 作为嵌入式 WebSocket 服务器和用于测试目的的简单的 Stomp 客户机来演示这种方法。 + +## 5. 其他 Web 框架 + +本章详细介绍了 Spring 与第三方 Web 框架的集成。 + +Spring 框架的核心价值主张之一是使 * 选择 * 成为可能。在一般意义上, Spring 不会强迫你使用或购买任何特定的架构、技术或方法(尽管它肯定会推荐一些而不是其他)。这种选择与开发人员及其开发团队最相关的架构、技术或方法的自由,可以说在 Web 领域最为明显, Spring 提供了自己的 Web 框架([Spring MVC](#mvc)和[Spring WebFlux](webflux.html#webflux)),同时,支持与许多流行的第三方 Web 框架的集成。 + +### 5.1.公共配置 + +在深入了解每个受支持的 Web 框架的集成细节之前,让我们先来看看不特定于任何一个 Web 框架的常见配置 Spring。(本节同样适用于 Spring 自己的 Web 框架变体。) + +Spring 的轻量级应用程序模型支持的概念之一(因为没有更好的词)是分层架构。请记住,在“经典”的分层架构中,Web 层只是许多层中的一个。它充当服务器端应用程序的入口点之一,并将其委托给在服务层中定义的服务对象(面),以满足特定于业务的(与表示技术无关的)用例。在 Spring 中,这些服务对象、任何其他特定于业务的对象、数据访问对象和其他对象存在于不同的“业务上下文”中,其中不包含 Web 或表示层对象(表示对象,例如 Spring MVC 控制器,通常配置在不同的“表示上下文”中)。本节详细介绍了如何配置一个 Spring 容器(“WebApplicationContext”),该容器包含应用程序中的所有“业务 bean”。 + +接下来讨论细节,你所需要做的就是在 Web 应用程序的标准 Java EE Servlet 文件中声明一个,并添加一个 `contextconfiglocation`\节(在同一文件中),该节定义要加载哪组 Spring XML 配置文件。 + +考虑以下``配置: + +``` + + org.springframework.web.context.ContextLoaderListener + +``` + +进一步考虑以下``配置: + +``` + + contextConfigLocation + /WEB-INF/applicationContext*.xml + +``` + +如果你没有指定`contextConfigLocation`上下文参数,那么 `contextLoaderListener’将查找一个名为`/WEB-INF/applicationContext.xml`的文件来加载。一旦加载了上下文文件, Spring 将基于 Bean 定义创建一个[“WebApplication Context”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/context/WebApplicationContext.html)对象,并将其存储在 Web 应用程序的`ServletContext`中。 + +所有 Java Web 框架都建立在 Servlet API 之上,因此你可以使用以下代码片段来访问由`ApplicationContext`创建的“业务上下文”`ContextLoaderListener`。 + +下面的示例展示了如何获得`WebApplicationContext`: + +``` +WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext); +``` + +[“WebApplication Contextutils”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/context/support/WebApplicationContextUtils.html)类是为了方便,所以你不需要记住`ServletContext`属性的名称。如果在`WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE`键下不存在对象,则其`getWebApplicationContext()`方法返回`null`。与其冒险在应用程序中获得`NullPointerExceptions`,不如使用`getRequiredWebApplicationContext()`方法。当`ApplicationContext`不存在时,此方法抛出一个异常。 + +一旦有了对`WebApplicationContext`的引用,就可以通过 bean 的名称或类型来检索 bean。大多数开发人员按名称检索 bean,然后将它们强制转换到其实现的接口之一。 + +幸运的是,本节中的大多数框架都有更简单的查找 bean 的方法。它们不仅使从 Spring 容器获得 bean 变得容易,而且还允许你在它们的控制器上使用依赖注入。每个 Web Framework 部分都有关于其特定集成策略的更多详细信息。 + +### 5.2.JSF + +JavaServer Faces 是 JCP 的标准的基于组件的、事件驱动的 Web 用户界面框架。它是 Java EE 保护伞的正式部分,但也可以单独使用,例如通过在 Tomcat 中嵌入 Mojarra 或 MyFaces。 + +请注意,最近的 JSF 版本与应用程序服务器中的 CDI 基础架构紧密相关,一些新的 JSF 功能仅在这样的环境中工作。 Spring 的 JSF 支持不再是积极发展的,主要是为了在更新基于 JSF 的旧应用程序时的迁移目的而存在。 + +Spring 的 JSF 集成中的关键元素是 JSF`ELResolver`机制。 + +#### 5.2.1. Spring Bean 解析器 + +`SpringBeanFacesELResolver`是一个兼容 JSF 的`ELResolver`实现,与 JSF 和 JSP 使用的标准统一 EL 集成。它首先委托给 Spring 的“业务上下文”`WebApplicationContext`,然后委托给底层 JSF 实现的默认解析器。 + +在配置方面,你可以在你的 JSF`faces-context.xml’文件中定义`SpringBeanFacesELResolver`,如下例所示: + +``` + + + org.springframework.web.jsf.el.SpringBeanFacesELResolver + ... + + +``` + +#### 5.2.2.使用`FacesContextUtils` + +当将你的属性映射到 `faces-config.xml’中的 bean 时,自定义`ELResolver`很好用,但是,有时你可能需要显式地获取 Bean。[“面部背景图”](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/jsf/FacesContextUtils.html)类使这一点变得简单。它类似于`WebApplicationContextUtils`,只是它需要一个`FacesContext`参数,而不是`ServletContext`参数。 + +下面的示例展示了如何使用`FacesContextUtils`: + +``` +ApplicationContext ctx = FacesContextUtils.getWebApplicationContext(FacesContext.getCurrentInstance()); +``` + +### 5.3.Apache Struts2.x + +由 Craig McClanahan 发明的[Struts](https://struts.apache.org)是一个由 Apache 软件基金会主持的开源项目。当时,它极大地简化了 JSP/ Servlet 编程范式,并赢得了许多使用专有框架的开发人员的支持。它简化了编程模型,它是开源的(因此像 Beer 一样是免费的),并且它拥有一个庞大的社区,这让该项目得以发展并在 Java Web 开发人员中流行起来。 + +作为原始 Struts1.x 的后续版本,请查看 Struts2.x 和 Struts-提供的[Spring Plugin](https://struts.apache.org/release/2.3.x/docs/spring-plugin.html)用于内置 Spring 集成。 + +### 5.4.Apache Tapestry5.x + +[Tapestry](https://tapestry.apache.org/)是一种“面向组件的框架,用于在 Java 中创建动态的、健壮的、高度可扩展的 Web 应用程序。” + +Spring 虽然具有自己的[强大的 Web 层](#mvc),但是通过使用用于 Web 用户界面的 Tapestry 和用于较低层的 Spring 容器的组合来构建 Enterprise 的 Java 应用程序有许多独特的优点。 + +有关更多信息,请参见 Tapestry 的专用[integration module for Spring](https://tapestry.apache.org/integrating-with-spring-framework.html)。 + +### 5.5.更多资源 + +下面的链接指向关于本章中描述的各种 Web 框架的更多参考资料。 + +* [JSF](https://www.oracle.com/technetwork/java/javaee/javaserverfaces-139869.html)主页 + +* 主页 + +* [Tapestry](https://tapestry.apache.org/)主页 +