mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
565 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4424e1de30 | |||
| c88c9620b3 | |||
| 2ac55659c8 | |||
| 25ea1f4c0f | |||
| 33862d98ea | |||
| c504ac5a47 | |||
| 73c06347be | |||
| 4becce1c2b | |||
| 8a283e39ff | |||
| 0d8a8432d1 | |||
| 3a8b5111d9 | |||
| b375a061df | |||
| 0e9cd2a35f | |||
| d6230824cb | |||
| edba535c8b | |||
| 79cf532c2f | |||
| f6b8ee76cd | |||
| 635cd29599 | |||
| 6f733512b5 | |||
| d6e05ddf2d | |||
| a92bd42236 | |||
| cebda46469 | |||
| f01fb19318 | |||
| 5e31856aaa | |||
| 1058fed339 | |||
| bcd09d7ad8 | |||
| abc56cde3d | |||
| 889fca98ac | |||
| 351a200747 | |||
| 01ceee8de5 | |||
| 0611192dac | |||
| be8bfad3af | |||
| 63fe45d92a | |||
| 5ce8ffd197 | |||
| 56c7b4065d | |||
| 680769d770 | |||
| 70cf754a2f | |||
| e6d360c1c6 | |||
| 8629182822 | |||
| 37ac3d8764 | |||
| d61b9c6294 | |||
| d8854a2f3f | |||
| 806c83591c | |||
| fd17df91fd | |||
| 519927421e | |||
| 52c19272c6 | |||
| 0d5a7db238 | |||
| 064cd3b7af | |||
| 6957ac5324 | |||
| d24c131130 | |||
| 0ab94478fe | |||
| 4b80b0f490 | |||
| 793581ebde | |||
| cc7f3101b7 | |||
| 49c463b1d2 | |||
| 094eb3e236 | |||
| 3fed2ec3a1 | |||
| 357d5b4e6e | |||
| 4152034799 | |||
| 3c9cfa8a0f | |||
| b016f385e1 | |||
| f40d1f2329 | |||
| f73f1e0687 | |||
| 1b09ef5051 | |||
| c1932dd307 | |||
| 490ff0af5e | |||
| 16b9640af2 | |||
| 2e3fbac9a0 | |||
| 18c11e84f3 | |||
| a2da97bc4c | |||
| 2e809903ec | |||
| 6075629015 | |||
| c20a2e4763 | |||
| e048b093b5 | |||
| 57ed5bf34b | |||
| 8b77ed164d | |||
| 3a8c40fd2f | |||
| 3a481a7d7f | |||
| 74972fb751 | |||
| 15b6626a4c | |||
| c61d011076 | |||
| 8448f597b1 | |||
| 950c43b4e7 | |||
| 39e74d89e1 | |||
| dc4fcef884 | |||
| d04d7b2e57 | |||
| a3e37597aa | |||
| 3209cf5c7a | |||
| 22376c2efa | |||
| 47667ab990 | |||
| 068dc7db28 | |||
| 3be4c0a893 | |||
| 268f3c853e | |||
| bf82ed7186 | |||
| 1b27a7baab | |||
| d40c10e00f | |||
| 5d022d2a24 | |||
| 433b72d493 | |||
| 7bc731dfa6 | |||
| 5cb6454824 | |||
| 9a68b3e910 | |||
| 38f9cbe3de | |||
| 76f94e998e | |||
| 71bb45c87b | |||
| 0521cdda88 | |||
| 6b1fbc9fe1 | |||
| 5243c2262a | |||
| a0c80ffc06 | |||
| a8614531ab | |||
| f0fe58f0ec | |||
| 8fb412ea74 | |||
| bf99361abb | |||
| 464b676ec5 | |||
| ac11b03cd3 | |||
| df2617d575 | |||
| a03a14797f | |||
| 20dd66cd5a | |||
| d720d6be6b | |||
| cb0c5f5a7b | |||
| 8691173fd8 | |||
| ddc3cf301a | |||
| b12115b61f | |||
| 029a69f5c1 | |||
| 80d1f12324 | |||
| e906eac049 | |||
| 32aa9e4b63 | |||
| 7162faaa4d | |||
| 1901b708b2 | |||
| d86750372a | |||
| 3a0e5c8894 | |||
| cf008eb9b1 | |||
| 0bf85af8e9 | |||
| 78bb66b0f2 | |||
| 246135364c | |||
| 468ef7a618 | |||
| 68b5eedde1 | |||
| 3d2befc84a | |||
| 8e9528de13 | |||
| 0c8d3e70cf | |||
| 47e631d5ff | |||
| e86003b692 | |||
| f6218cadd7 | |||
| 368a917466 | |||
| a6c5692586 | |||
| d0a2820af4 | |||
| 299b86bae3 | |||
| 1218e65ca1 | |||
| 496155525c | |||
| 7c37f4bc51 | |||
| 6793edc349 | |||
| 1777e7f3b7 | |||
| 6fa09e1783 | |||
| ae23f5a594 | |||
| deaa493644 | |||
| 2a126faae7 | |||
| 35304435d0 | |||
| 420255373d | |||
| 834d22f866 | |||
| 2a77665be7 | |||
| 14857ae0da | |||
| 7156ea016e | |||
| 0820210c7c | |||
| 9eb1fbd5c3 | |||
| ea9c217827 | |||
| 1fc020cf92 | |||
| 592ab0f350 | |||
| 0da2241367 | |||
| 37e8ef1542 | |||
| e995033811 | |||
| 4c0329014f | |||
| 72301dc861 | |||
| 84e863f803 | |||
| 7a79da589a | |||
| a481c7649f | |||
| ba4d9a5230 | |||
| e83594a2a3 | |||
| a8ea472121 | |||
| 1393fac402 | |||
| 46f1849c2f | |||
| 9900575f9c | |||
| 2ed10f13e9 | |||
| a23c9cd53d | |||
| 9ac2443b78 | |||
| f075120675 | |||
| b8f091e2f6 | |||
| 41f8b6926f | |||
| b4ed3fbcd0 | |||
| 83ec9f4fe7 | |||
| 3f65b8506b | |||
| 50074f0e96 | |||
| 31a51cca4f | |||
| 654dee8cd6 | |||
| 7028de9dbd | |||
| e4751513a4 | |||
| bd7e2d2875 | |||
| 6061fdf231 | |||
| 9b662e8244 | |||
| f51a640309 | |||
| 48906afaf4 | |||
| ef4d93507d | |||
| d1d79babe7 | |||
| 2eb8efe83b | |||
| dd57ec9fa0 | |||
| 0033eb4ed6 | |||
| 2ca8dd2faa | |||
| c2e3fed8dc | |||
| 91eb2be44c | |||
| d4ac90dae0 | |||
| 11a416156b | |||
| d37d6688d8 | |||
| 801f01e23f | |||
| ea398d7b7e | |||
| 40bf923d7d | |||
| e69a1d22f9 | |||
| 3f40452511 | |||
| 2ccf4cab8b | |||
| feac983869 | |||
| 66b27e6dc8 | |||
| 6072a537d9 | |||
| aecebf7981 | |||
| 4cbf47834d | |||
| 854715d9a7 | |||
| 9127777c32 | |||
| 6c816c55d1 | |||
| 209e02cf29 | |||
| 19686adc01 | |||
| 35667e81ea | |||
| d881cd3346 | |||
| ca51b1422a | |||
| da00bbebbf | |||
| 171535f680 | |||
| f06cf21341 | |||
| 254fb39567 | |||
| 40596433f1 | |||
| 3be1216897 | |||
| b8a713fde3 | |||
| 06b6c4bcf9 | |||
| cd4ab23fa9 | |||
| 5598dd2c34 | |||
| f12d81f791 | |||
| 49547fc710 | |||
| ea8fdd17c8 | |||
| a6566bfd5f | |||
| abefc0aba0 | |||
| 03b304f05c | |||
| 54e25e2fa6 | |||
| 83447663b4 | |||
| b581a79eed | |||
| 6535614d78 | |||
| fa82683ce2 | |||
| 1a201cd180 | |||
| 24457660e3 | |||
| f1fb8cf03e | |||
| 18c6aceee7 | |||
| 24ddceea47 | |||
| 82ec28b369 | |||
| db19f6395d | |||
| b677ff20fe | |||
| b98c1ec36a | |||
| 37ff9792be | |||
| 3344ab909b | |||
| 828b39587f | |||
| 0605172d4d | |||
| 089d938e15 | |||
| c00508d6cf | |||
| 83b0f4f394 | |||
| 09cb844421 | |||
| 842569c9e5 | |||
| 3568f6c61a | |||
| 5bf213948c | |||
| 92c2fb45c7 | |||
| cd9c0e03e7 | |||
| 2dd34cf967 | |||
| a2072de391 | |||
| 526d9eae7f | |||
| 7681d12eb0 | |||
| b077ae9259 | |||
| 362e189aa5 | |||
| 96ae03b48f | |||
| c276e5b679 | |||
| f00a8cb3a3 | |||
| 220995b830 | |||
| e344f3f869 | |||
| 517a073f33 | |||
| dcba9475ba | |||
| 2e43412a82 | |||
| 48861b67dd | |||
| 61eaa9333b | |||
| 9c7b5cb3f5 | |||
| 53828cbfad | |||
| e3c602c43a | |||
| 02e9209e5c | |||
| 8a29bfba3f | |||
| 0c817f0441 | |||
| 326e27eab2 | |||
| 5547361d18 | |||
| 805d643347 | |||
| b20a71a0d3 | |||
| 3de4e931c7 | |||
| 93345de687 | |||
| bbf3c6ecac | |||
| ca4de8f191 | |||
| 858ea1a8c5 | |||
| dd50da314d | |||
| 1f068fcdb8 | |||
| 85d81024a4 | |||
| e7c3e1c516 | |||
| a268203832 | |||
| 9536dd14ef | |||
| c565df96f5 | |||
| 4b4a8af893 | |||
| 1e3161b161 | |||
| 6b50b7b72a | |||
| 96895c7759 | |||
| 96c494c6ad | |||
| 48548b29d6 | |||
| 55b3b28aec | |||
| 6a26db80b6 | |||
| ccf68878c7 | |||
| f7367ceae5 | |||
| 61c9cbc3f5 | |||
| 446b90172b | |||
| 6501176081 | |||
| ac35ffef9e | |||
| 6b89cf94a3 | |||
| bd054a4918 | |||
| cb04c3b335 | |||
| 5c5d8e61ae | |||
| 0fef380202 | |||
| cc8361cd93 | |||
| 8b53fecc06 | |||
| 425d5a94cb | |||
| 38abee00e2 | |||
| 082784e023 | |||
| 155a37d3a6 | |||
| f8a3253d12 | |||
| b4ba80b09e | |||
| f6045e8e11 | |||
| f0a3f776c2 | |||
| 74dc07e736 | |||
| c5eb4ed978 | |||
| bff81d3056 | |||
| c8bb7ded5d | |||
| 56f3bd86cf | |||
| 52ae97cffc | |||
| c7ca5c81c3 | |||
| ccb8db41f0 | |||
| 46fe7f5bea | |||
| 54904de77d | |||
| dbd32f8f11 | |||
| ef2b47e202 | |||
| 60e8dbf11d | |||
| 01414d0b3d | |||
| ad5bf2fac8 | |||
| a28ef22113 | |||
| fe55b49c7d | |||
| e32a92daa7 | |||
| d7970e4ab8 | |||
| 2f78b42133 | |||
| f4ef057e9e | |||
| 448786bfd0 | |||
| 0093d6e34d | |||
| 324c6057fd | |||
| 759ab23c45 | |||
| 1b62b6dd89 | |||
| b3597f3d99 | |||
| 06bb6dbcff | |||
| 73e9de7b5f | |||
| e535ef37c7 | |||
| dd926af5a8 | |||
| a726b5109a | |||
| 7c817dd34b | |||
| c9165470f2 | |||
| 61816d0076 | |||
| aa2a067489 | |||
| 714d380ec0 | |||
| df7223f39c | |||
| 7f6f47b97b | |||
| b9e972c248 | |||
| 66a1be2d86 | |||
| 717915e6ac | |||
| c8c8f5722b | |||
| cd610b3ed1 | |||
| 93deb0a584 | |||
| 05eab703cc | |||
| c0cd55a8fa | |||
| 35052f2113 | |||
| 53f891226e | |||
| 4fb4c95220 | |||
| b8b88e638b | |||
| 837327f530 | |||
| e224c71119 | |||
| 5bc80fc094 | |||
| 75466fee8d | |||
| bedf5f26fe | |||
| 2689bab652 | |||
| e82dd14f0f | |||
| 1dba8f6add | |||
| 25a1e8d414 | |||
| 8c6287ef7b | |||
| 322cbca0dc | |||
| 2685a35c3a | |||
| 8606d9b581 | |||
| 3d63cbf076 | |||
| 67f88482e6 | |||
| b3f5d20ad8 | |||
| 49d688f99c | |||
| 033bebf8cd | |||
| 1ce22bdcc1 | |||
| 4e678479b9 | |||
| f69db61147 | |||
| 655f5eb1e2 | |||
| e45edff4a4 | |||
| fc309ee314 | |||
| 8a5f655a5c | |||
| 3f14ebc4cf | |||
| 4d8f6c1b41 | |||
| 7150c23e93 | |||
| 009179fed7 | |||
| 6dbd446fc8 | |||
| a45d438f05 | |||
| cabc41bcd6 | |||
| 5977bb05ca | |||
| 9332b3f690 | |||
| 5f13b2b35b | |||
| 01abd521e2 | |||
| f91fae5dfb | |||
| 7169577f9c | |||
| 72152ff1f3 | |||
| 1d6246d700 | |||
| 5441796675 | |||
| 876e417eb4 | |||
| e6c812e2ab | |||
| 0e5e2f5d2e | |||
| 5bca25fe44 | |||
| ab253470f0 | |||
| 02f152c6e1 | |||
| 791802fdf7 | |||
| f4f8066877 | |||
| d95ecbe0fd | |||
| cba77c6909 | |||
| b4eb352e1d | |||
| 5fc937d558 | |||
| f926deda89 | |||
| c3e28728ce | |||
| f956d399ae | |||
| 7efd347da6 | |||
| 5e07141846 | |||
| 40543e0a58 | |||
| 097758baf3 | |||
| 96a429a561 | |||
| e5ee369e70 | |||
| f5bc084ce2 | |||
| d86cfc949d | |||
| 1d708510df | |||
| 1275b10d9c | |||
| 1f3dd3e2ee | |||
| c227fbfdf2 | |||
| f3f3dc693a | |||
| 5e1a4740d7 | |||
| b4866e51b2 | |||
| a9fa813a75 | |||
| d99c2cf31e | |||
| ffa431da7b | |||
| a91effcd86 | |||
| 07a5d8c91c | |||
| 0bd0578ff9 | |||
| 2f3a73a59e | |||
| 1e74d793a2 | |||
| 3d96e3f8c8 | |||
| d8215a62f6 | |||
| 841124af75 | |||
| dbc42f56b3 | |||
| 96fbcb26c9 | |||
| 6a6dd599a1 | |||
| 5e52259fb3 | |||
| e51c71bcd6 | |||
| 4d5e235166 | |||
| 5236d17ac4 | |||
| d9f659171b | |||
| 993a69d3a9 | |||
| 944305b9f1 | |||
| 6eb63f778e | |||
| eb240016ed | |||
| b674906e3a | |||
| bc7ba8cf2b | |||
| b4649137fc | |||
| c8b920a05d | |||
| c564725f46 | |||
| e599fec685 | |||
| 657fe1fa43 | |||
| 697d5e6247 | |||
| d8d7e0a762 | |||
| 73d30dd875 | |||
| 033548a760 | |||
| e416dfdbc0 | |||
| 592a3074e1 | |||
| b39e93d0d1 | |||
| 906c54faff | |||
| d3c3088c6b | |||
| f2c0b30641 | |||
| 601de66c03 | |||
| 91ea2d4c27 | |||
| b7884ddd02 | |||
| 9fec516560 | |||
| f469eff97b | |||
| 69cde11a51 | |||
| 7d2047cdbd | |||
| c76116b970 | |||
| 8d604350e4 | |||
| 4db724984e | |||
| 87942ed71d | |||
| dd871e0d8c | |||
| c1014f5989 | |||
| 6a2262969e | |||
| d39e44c209 | |||
| 4d15b58ca4 | |||
| 9342317291 | |||
| dfd74495bd | |||
| 90ffa95b6a | |||
| 825f160369 | |||
| 834e694e94 | |||
| ce3e9b0c29 | |||
| 1c7ceaa2ca | |||
| 7e0620a143 | |||
| fb039a3e0c | |||
| 05e4234525 | |||
| e19472568f | |||
| 845488af8d | |||
| b924b7b4c6 | |||
| b368fc03ea | |||
| 982a094646 | |||
| dfe320f172 | |||
| 2537c7c735 | |||
| 54853ee239 | |||
| b408cee29f | |||
| e63d89973a | |||
| b311088f5a | |||
| d9c534b13d | |||
| 95e311fd02 | |||
| 5b2aa0be81 | |||
| 33b4995547 | |||
| c16dab236a | |||
| 202c46035a | |||
| ea83d66fb5 | |||
| 9f85e397d4 | |||
| 7df2e2a8d2 | |||
| d446de62a4 | |||
| dd9f03d9b9 | |||
| 3c57d5518a | |||
| 5f98afc180 | |||
| 7492c0ea03 | |||
| 65a430bb42 | |||
| bd62f47afa | |||
| 3e0aa4c603 | |||
| 8b5bfdfa44 | |||
| da88ac8cca | |||
| a66f3e02f4 | |||
| 7f44861f32 | |||
| 63e20404a2 | |||
| 5c574b9878 | |||
| 9562a1c146 | |||
| ed4404f350 | |||
| bd55b647c7 | |||
| 19c7527376 |
+6
-6
@@ -123,13 +123,13 @@ define the source file coding standards we use along with some IDEA editor setti
|
||||
|
||||
### Reference Docs
|
||||
|
||||
The reference documentation is in the [framework-docs/src/docs/asciidoc](framework-docs/src/docs/asciidoc) directory, in
|
||||
[Asciidoctor](https://asciidoctor.org/) format. For trivial changes, you may be able to browse,
|
||||
edit source files, and submit directly from GitHub.
|
||||
The reference documentation is authored in [Asciidoctor](https://asciidoctor.org/) format
|
||||
using [Antora](https://docs.antora.org/antora/latest/). The source files for the documentation
|
||||
reside in the [framework-docs/modules/ROOT](framework-docs/modules/ROOT) directory. For
|
||||
trivial changes, you may be able to browse, edit source files, and submit directly from GitHub.
|
||||
|
||||
When making changes locally, execute `./gradlew :framework-docs:asciidoctor` and then browse the result under
|
||||
`framework-docs/build/docs/ref-docs/html5/index.html`.
|
||||
When making changes locally, execute `./gradlew antora` and then browse the results under
|
||||
`framework-docs/build/site/index.html`.
|
||||
|
||||
Asciidoctor also supports live editing. For more details see
|
||||
[AsciiDoc Tooling](https://docs.asciidoctor.org/asciidoctor/latest/tooling/).
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
This is the home of the Spring Framework: the foundation for all [Spring projects](https://spring.io/projects). Collectively the Spring Framework and the family of Spring projects are often referred to simply as "Spring".
|
||||
|
||||
Spring provides everything required beyond the Java programming language for creating enterprise applications for a wide range of scenarios and architectures. Please read the [Overview](https://docs.spring.io/spring/docs/current/spring-framework-reference/overview.html#spring-introduction) section as reference for a more complete introduction.
|
||||
Spring provides everything required beyond the Java programming language for creating enterprise applications for a wide range of scenarios and architectures. Please read the [Overview](https://docs.spring.io/spring-framework/reference/overview.html) section of the reference documentation for a more complete introduction.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
@@ -14,7 +14,7 @@ For access to artifacts or a distribution zip, see the [Spring Framework Artifac
|
||||
|
||||
## Documentation
|
||||
|
||||
The Spring Framework maintains reference documentation ([published](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/) and [source](framework-docs/src/docs/asciidoc)), GitHub [wiki pages](https://github.com/spring-projects/spring-framework/wiki), and an
|
||||
The Spring Framework maintains reference documentation ([published](https://docs.spring.io/spring-framework/reference/) and [source](framework-docs/modules/ROOT)), GitHub [wiki pages](https://github.com/spring-projects/spring-framework/wiki), and an
|
||||
[API reference](https://docs.spring.io/spring-framework/docs/current/javadoc-api/). There are also [guides and tutorials](https://spring.io/guides) across Spring projects.
|
||||
|
||||
## Micro-Benchmarks
|
||||
@@ -31,7 +31,7 @@ Information regarding CI builds can be found in the [Spring Framework Concourse
|
||||
|
||||
## Stay in Touch
|
||||
|
||||
Follow [@SpringCentral](https://twitter.com/springcentral), [@SpringFramework](https://twitter.com/springframework), and its [team members](https://twitter.com/springframework/lists/team/members) on Twitter. In-depth articles can be found at [The Spring Blog](https://spring.io/blog/), and releases are announced via our [news feed](https://spring.io/blog/category/news).
|
||||
Follow [@SpringCentral](https://twitter.com/springcentral), [@SpringFramework](https://twitter.com/springframework), and its [team members](https://twitter.com/springframework/lists/team/members) on Twitter. In-depth articles can be found at [The Spring Blog](https://spring.io/blog/), and releases are announced via our [releases feed](https://spring.io/blog/category/releases).
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+2
-54
@@ -1,14 +1,14 @@
|
||||
plugins {
|
||||
id 'io.spring.nohttp' version '0.0.11'
|
||||
id 'io.freefair.aspectj' version '8.0.1' apply false
|
||||
// kotlinVersion is managed in gradle.properties
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version "${kotlinVersion}" apply false
|
||||
id 'org.jetbrains.dokka' version '1.8.10'
|
||||
id 'org.jetbrains.dokka' version '1.8.20'
|
||||
id 'org.unbroken-dome.xjc' version '2.0.0' apply false
|
||||
id 'com.github.ben-manes.versions' version '0.46.0'
|
||||
id 'com.github.johnrengelman.shadow' version '8.1.1' apply false
|
||||
id 'de.undercouch.download' version '5.4.0'
|
||||
id 'me.champeau.jmh' version '0.7.1' apply false
|
||||
id 'me.champeau.mrjar' version '0.1.1'
|
||||
}
|
||||
|
||||
ext {
|
||||
@@ -46,7 +46,6 @@ configure([rootProject] + javaProjects) { project ->
|
||||
|
||||
apply plugin: "java"
|
||||
apply plugin: "java-test-fixtures"
|
||||
apply plugin: "checkstyle"
|
||||
apply plugin: 'org.springframework.build.conventions'
|
||||
apply from: "${rootDir}/gradle/toolchains.gradle"
|
||||
apply from: "${rootDir}/gradle/ide.gradle"
|
||||
@@ -60,33 +59,6 @@ configure([rootProject] + javaProjects) { project ->
|
||||
matching { it.name.endsWith("Classpath") }.all { it.extendsFrom(dependencyManagement) }
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
include(["**/*Tests.class", "**/*Test.class"])
|
||||
systemProperty("java.awt.headless", "true")
|
||||
systemProperty("testGroups", project.properties.get("testGroups"))
|
||||
systemProperty("io.netty.leakDetection.level", "paranoid")
|
||||
systemProperty("io.netty5.leakDetectionLevel", "paranoid")
|
||||
systemProperty("io.netty5.leakDetection.targetRecords", "32")
|
||||
systemProperty("io.netty5.buffer.lifecycleTracingEnabled", "true")
|
||||
systemProperty("io.netty5.buffer.leakDetectionEnabled", "true")
|
||||
jvmArgs(["--add-opens=java.base/java.lang=ALL-UNNAMED",
|
||||
"--add-opens=java.base/java.util=ALL-UNNAMED"])
|
||||
}
|
||||
|
||||
checkstyle {
|
||||
toolVersion = "10.12.1"
|
||||
configDirectory.set(rootProject.file("src/checkstyle"))
|
||||
}
|
||||
|
||||
tasks.named("checkstyleMain").configure {
|
||||
maxHeapSize = "1g"
|
||||
}
|
||||
|
||||
tasks.named("checkstyleTest").configure {
|
||||
maxHeapSize = "1g"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
dependencyManagement(enforcedPlatform(dependencies.project(path: ":framework-platform")))
|
||||
testImplementation("org.junit.jupiter:junit-jupiter-api")
|
||||
@@ -106,7 +78,6 @@ configure([rootProject] + javaProjects) { project ->
|
||||
// JSR-305 only used for non-required meta-annotations
|
||||
compileOnly("com.google.code.findbugs:jsr305")
|
||||
testCompileOnly("com.google.code.findbugs:jsr305")
|
||||
checkstyle("io.spring.javaformat:spring-javaformat-checkstyle:0.0.38")
|
||||
}
|
||||
|
||||
ext.javadocLinks = [
|
||||
@@ -144,28 +115,5 @@ configure(moduleProjects) { project ->
|
||||
|
||||
configure(rootProject) {
|
||||
description = "Spring Framework"
|
||||
|
||||
apply plugin: "io.spring.nohttp"
|
||||
apply plugin: 'org.springframework.build.api-diff'
|
||||
|
||||
nohttp {
|
||||
source.exclude "**/test-output/**"
|
||||
source.exclude "**/.gradle/**"
|
||||
allowlistFile = project.file("src/nohttp/allowlist.lines")
|
||||
def rootPath = file(rootDir).toPath()
|
||||
def projectDirs = allprojects.collect { it.projectDir } + "${rootDir}/buildSrc"
|
||||
projectDirs.forEach { dir ->
|
||||
[ 'bin', 'build', 'out', '.settings' ]
|
||||
.collect { rootPath.relativize(new File(dir, it).toPath()) }
|
||||
.forEach { source.exclude "$it/**" }
|
||||
[ '.classpath', '.project' ]
|
||||
.collect { rootPath.relativize(new File(dir, it).toPath()) }
|
||||
.forEach { source.exclude "$it" }
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("checkstyleNohttp").configure {
|
||||
maxHeapSize = "1g"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
plugins {
|
||||
id 'java-gradle-plugin'
|
||||
id 'checkstyle'
|
||||
id 'io.spring.javaformat' version "${javaFormatVersion}"
|
||||
}
|
||||
|
||||
repositories {
|
||||
@@ -17,10 +19,13 @@ ext {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
|
||||
implementation("org.jetbrains.kotlin:kotlin-compiler-embeddable:${kotlinVersion}")
|
||||
checkstyle "io.spring.javaformat:spring-javaformat-checkstyle:${javaFormatVersion}"
|
||||
implementation "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}"
|
||||
implementation "org.jetbrains.kotlin:kotlin-compiler-embeddable:${kotlinVersion}"
|
||||
implementation "me.champeau.gradle:japicmp-gradle-plugin:0.3.0"
|
||||
implementation "org.gradle:test-retry-gradle-plugin:1.4.1"
|
||||
implementation "io.spring.javaformat:spring-javaformat-gradle-plugin:${javaFormatVersion}"
|
||||
implementation "io.spring.nohttp:nohttp-gradle:0.0.11"
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
org.gradle.caching=true
|
||||
javaFormatVersion=0.0.38
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.build;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import io.spring.javaformat.gradle.SpringJavaFormatPlugin;
|
||||
import io.spring.nohttp.gradle.NoHttpExtension;
|
||||
import io.spring.nohttp.gradle.NoHttpPlugin;
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.artifacts.DependencySet;
|
||||
import org.gradle.api.plugins.JavaBasePlugin;
|
||||
import org.gradle.api.plugins.quality.Checkstyle;
|
||||
import org.gradle.api.plugins.quality.CheckstyleExtension;
|
||||
import org.gradle.api.plugins.quality.CheckstylePlugin;
|
||||
|
||||
/**
|
||||
* {@link Plugin} that applies conventions for checkstyle.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public class CheckstyleConventions {
|
||||
|
||||
/**
|
||||
* Applies the Spring Java Format and Checkstyle plugins with the project conventions.
|
||||
* @param project the current project
|
||||
*/
|
||||
public void apply(Project project) {
|
||||
project.getPlugins().withType(JavaBasePlugin.class, (java) -> {
|
||||
if (project.getRootProject() == project) {
|
||||
configureNoHttpPlugin(project);
|
||||
}
|
||||
project.getPlugins().apply(CheckstylePlugin.class);
|
||||
project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g"));
|
||||
CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class);
|
||||
checkstyle.setToolVersion("10.12.1");
|
||||
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
|
||||
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
|
||||
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
|
||||
checkstyleDependencies
|
||||
.add(project.getDependencies().create("io.spring.javaformat:spring-javaformat-checkstyle:" + version));
|
||||
});
|
||||
}
|
||||
|
||||
private static void configureNoHttpPlugin(Project project) {
|
||||
project.getPlugins().apply(NoHttpPlugin.class);
|
||||
NoHttpExtension noHttp = project.getExtensions().getByType(NoHttpExtension.class);
|
||||
noHttp.setAllowlistFile(project.file("src/nohttp/allowlist.lines"));
|
||||
noHttp.getSource().exclude("**/test-output/**", "**/.settings/**",
|
||||
"**/.classpath", "**/.project", "**/.gradle/**");
|
||||
List<String> buildFolders = List.of("bin", "build", "out");
|
||||
project.allprojects(subproject -> {
|
||||
Path rootPath = project.getRootDir().toPath();
|
||||
Path projectPath = rootPath.relativize(subproject.getProjectDir().toPath());
|
||||
for (String buildFolder : buildFolders) {
|
||||
Path innerBuildDir = projectPath.resolve(buildFolder);
|
||||
noHttp.getSource().exclude(innerBuildDir + File.separator + "**");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -25,10 +25,8 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinBasePlugin;
|
||||
* Plugin to apply conventions to projects that are part of Spring Framework's build.
|
||||
* Conventions are applied in response to various plugins being applied.
|
||||
*
|
||||
* When the {@link JavaBasePlugin} is applied, the conventions in {@link TestConventions}
|
||||
* are applied.
|
||||
* When the {@link JavaBasePlugin} is applied, the conventions in {@link JavaConventions}
|
||||
* are applied.
|
||||
* <p>When the {@link JavaBasePlugin} is applied, the conventions in {@link CheckstyleConventions},
|
||||
* {@link TestConventions} and {@link JavaConventions} are applied.
|
||||
* When the {@link KotlinBasePlugin} is applied, the conventions in {@link KotlinConventions}
|
||||
* are applied.
|
||||
*
|
||||
@@ -38,8 +36,10 @@ public class ConventionsPlugin implements Plugin<Project> {
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
new CheckstyleConventions().apply(project);
|
||||
new JavaConventions().apply(project);
|
||||
new KotlinConventions().apply(project);
|
||||
new TestConventions().apply(project);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -24,7 +24,9 @@ import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.plugins.JavaBasePlugin;
|
||||
import org.gradle.api.plugins.JavaPlugin;
|
||||
import org.gradle.api.plugins.JavaPluginExtension;
|
||||
import org.gradle.api.tasks.compile.JavaCompile;
|
||||
import org.gradle.jvm.toolchain.JavaLanguageVersion;
|
||||
|
||||
/**
|
||||
* {@link Plugin} that applies conventions for compiling Java sources in Spring Framework.
|
||||
@@ -68,6 +70,8 @@ public class JavaConventions {
|
||||
* @param project the current project
|
||||
*/
|
||||
private void applyJavaCompileConventions(Project project) {
|
||||
project.getExtensions().getByType(JavaPluginExtension.class)
|
||||
.getToolchain().getLanguageVersion().set(JavaLanguageVersion.of(17));
|
||||
project.getTasks().withType(JavaCompile.class)
|
||||
.matching(compileTask -> compileTask.getName().equals(JavaPlugin.COMPILE_JAVA_TASK_NAME))
|
||||
.forEach(compileTask -> {
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.build;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.plugins.JavaBasePlugin;
|
||||
import org.gradle.api.tasks.testing.Test;
|
||||
@@ -41,11 +43,36 @@ class TestConventions {
|
||||
|
||||
private void configureTestConventions(Project project) {
|
||||
project.getTasks().withType(Test.class,
|
||||
test -> project.getPlugins().withType(TestRetryPlugin.class, testRetryPlugin -> {
|
||||
TestRetryTaskExtension testRetry = test.getExtensions().getByType(TestRetryTaskExtension.class);
|
||||
testRetry.getFailOnPassedAfterRetry().set(true);
|
||||
testRetry.getMaxRetries().set(isCi() ? 3 : 0);
|
||||
}));
|
||||
test -> {
|
||||
configureTests(project, test);
|
||||
configureTestRetryPlugin(project, test);
|
||||
});
|
||||
}
|
||||
|
||||
private void configureTests(Project project, Test test) {
|
||||
test.useJUnitPlatform();
|
||||
test.include("**/*Tests.class", "**/*Test.class");
|
||||
test.setSystemProperties(Map.of(
|
||||
"java.awt.headless", "true",
|
||||
"io.netty.leakDetection.level", "paranoid",
|
||||
"io.netty5.leakDetectionLevel", "paranoid",
|
||||
"io.netty5.leakDetection.targetRecords", "32",
|
||||
"io.netty5.buffer.lifecycleTracingEnabled", "true"
|
||||
));
|
||||
if (project.hasProperty("testGroups")) {
|
||||
test.systemProperty("testGroups", project.getProperties().get("testGroups"));
|
||||
}
|
||||
test.jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED",
|
||||
"--add-opens=java.base/java.util=ALL-UNNAMED",
|
||||
"-Djava.locale.providers=COMPAT");
|
||||
}
|
||||
|
||||
private void configureTestRetryPlugin(Project project, Test test) {
|
||||
project.getPlugins().withType(TestRetryPlugin.class, testRetryPlugin -> {
|
||||
TestRetryTaskExtension testRetry = test.getExtensions().getByType(TestRetryTaskExtension.class);
|
||||
testRetry.getFailOnPassedAfterRetry().set(true);
|
||||
testRetry.getMaxRetries().set(isCi() ? 3 : 0);
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isCi() {
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.build.hint;
|
||||
|
||||
import org.gradle.api.file.ConfigurableFileCollection;
|
||||
import org.gradle.api.provider.SetProperty;
|
||||
import org.gradle.api.tasks.Classpath;
|
||||
import org.gradle.api.tasks.Input;
|
||||
import org.gradle.process.CommandLineArgumentProvider;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* Argument provider for registering the runtime hints agent with a Java process.
|
||||
*/
|
||||
public interface RuntimeHintsAgentArgumentProvider extends CommandLineArgumentProvider {
|
||||
|
||||
@Classpath
|
||||
ConfigurableFileCollection getAgentJar();
|
||||
|
||||
@Input
|
||||
SetProperty<String> getIncludedPackages();
|
||||
|
||||
@Input
|
||||
SetProperty<String> getExcludedPackages();
|
||||
|
||||
@Override
|
||||
default Iterable<String> asArguments() {
|
||||
StringBuilder packages = new StringBuilder();
|
||||
getIncludedPackages().get().forEach(packageName -> packages.append('+').append(packageName).append(','));
|
||||
getExcludedPackages().get().forEach(packageName -> packages.append('-').append(packageName).append(','));
|
||||
return Collections.singleton("-javaagent:" + getAgentJar().getSingleFile() + "=" + packages);
|
||||
}
|
||||
}
|
||||
+4
-27
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,38 +16,15 @@
|
||||
|
||||
package org.springframework.build.hint;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.gradle.api.model.ObjectFactory;
|
||||
import org.gradle.api.provider.SetProperty;
|
||||
|
||||
/**
|
||||
* Entry point to the DSL extension for the {@link RuntimeHintsAgentPlugin} Gradle plugin.
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public class RuntimeHintsAgentExtension {
|
||||
public interface RuntimeHintsAgentExtension {
|
||||
|
||||
private final SetProperty<String> includedPackages;
|
||||
SetProperty<String> getIncludedPackages();
|
||||
|
||||
private final SetProperty<String> excludedPackages;
|
||||
|
||||
public RuntimeHintsAgentExtension(ObjectFactory objectFactory) {
|
||||
this.includedPackages = objectFactory.setProperty(String.class).convention(Collections.singleton("org.springframework"));
|
||||
this.excludedPackages = objectFactory.setProperty(String.class).convention(Collections.emptySet());
|
||||
}
|
||||
|
||||
public SetProperty<String> getIncludedPackages() {
|
||||
return this.includedPackages;
|
||||
}
|
||||
|
||||
public SetProperty<String> getExcludedPackages() {
|
||||
return this.excludedPackages;
|
||||
}
|
||||
|
||||
String asJavaAgentArgument() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
this.includedPackages.get().forEach(packageName -> builder.append('+').append(packageName).append(','));
|
||||
this.excludedPackages.get().forEach(packageName -> builder.append('-').append(packageName).append(','));
|
||||
return builder.toString();
|
||||
}
|
||||
SetProperty<String> getExcludedPackages();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,41 +16,79 @@
|
||||
|
||||
package org.springframework.build.hint;
|
||||
|
||||
import org.gradle.api.JavaVersion;
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.artifacts.Configuration;
|
||||
import org.gradle.api.attributes.Bundling;
|
||||
import org.gradle.api.attributes.Category;
|
||||
import org.gradle.api.attributes.LibraryElements;
|
||||
import org.gradle.api.attributes.Usage;
|
||||
import org.gradle.api.attributes.java.TargetJvmVersion;
|
||||
import org.gradle.api.plugins.JavaPlugin;
|
||||
import org.gradle.api.tasks.bundling.Jar;
|
||||
import org.gradle.api.tasks.testing.Test;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* {@link Plugin} that configures the {@code RuntimeHints} Java agent to test tasks.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
public class RuntimeHintsAgentPlugin implements Plugin<Project> {
|
||||
|
||||
public static final String RUNTIMEHINTS_TEST_TASK = "runtimeHintsTest";
|
||||
private static final String EXTENSION_NAME = "runtimeHintsAgent";
|
||||
private static final String CONFIGURATION_NAME = "testRuntimeHintsAgentJar";
|
||||
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
|
||||
project.getPlugins().withType(JavaPlugin.class, javaPlugin -> {
|
||||
RuntimeHintsAgentExtension agentExtension = project.getExtensions().create(EXTENSION_NAME,
|
||||
RuntimeHintsAgentExtension.class, project.getObjects());
|
||||
RuntimeHintsAgentExtension agentExtension = createRuntimeHintsAgentExtension(project);
|
||||
Test agentTest = project.getTasks().create(RUNTIMEHINTS_TEST_TASK, Test.class, test -> {
|
||||
test.useJUnitPlatform(options -> {
|
||||
options.includeTags("RuntimeHintsTests");
|
||||
});
|
||||
test.include("**/*Tests.class", "**/*Test.class");
|
||||
test.systemProperty("java.awt.headless", "true");
|
||||
});
|
||||
project.afterEvaluate(p -> {
|
||||
Jar jar = project.getRootProject().project("spring-core-test").getTasks().withType(Jar.class).named("jar").get();
|
||||
agentTest.jvmArgs("-javaagent:" + jar.getArchiveFile().get().getAsFile() + "=" + agentExtension.asJavaAgentArgument());
|
||||
test.systemProperty("org.graalvm.nativeimage.imagecode", "runtime");
|
||||
test.getJvmArgumentProviders().add(createRuntimeHintsAgentArgumentProvider(project, agentExtension));
|
||||
});
|
||||
project.getTasks().getByName("check", task -> task.dependsOn(agentTest));
|
||||
project.getDependencies().add(CONFIGURATION_NAME, project.project(":spring-core-test"));
|
||||
});
|
||||
}
|
||||
|
||||
private static RuntimeHintsAgentExtension createRuntimeHintsAgentExtension(Project project) {
|
||||
RuntimeHintsAgentExtension agentExtension = project.getExtensions().create(EXTENSION_NAME, RuntimeHintsAgentExtension.class);
|
||||
agentExtension.getIncludedPackages().convention(Collections.singleton("org.springframework"));
|
||||
agentExtension.getExcludedPackages().convention(Collections.emptySet());
|
||||
return agentExtension;
|
||||
}
|
||||
|
||||
private static RuntimeHintsAgentArgumentProvider createRuntimeHintsAgentArgumentProvider(
|
||||
Project project, RuntimeHintsAgentExtension agentExtension) {
|
||||
RuntimeHintsAgentArgumentProvider agentArgumentProvider = project.getObjects().newInstance(RuntimeHintsAgentArgumentProvider.class);
|
||||
agentArgumentProvider.getAgentJar().from(createRuntimeHintsAgentConfiguration(project));
|
||||
agentArgumentProvider.getIncludedPackages().set(agentExtension.getIncludedPackages());
|
||||
agentArgumentProvider.getExcludedPackages().set(agentExtension.getExcludedPackages());
|
||||
return agentArgumentProvider;
|
||||
}
|
||||
|
||||
private static Configuration createRuntimeHintsAgentConfiguration(Project project) {
|
||||
return project.getConfigurations().create(CONFIGURATION_NAME, configuration -> {
|
||||
configuration.setCanBeConsumed(false);
|
||||
configuration.setTransitive(false); // Only the built artifact is required
|
||||
configuration.attributes(attributes -> {
|
||||
attributes.attribute(Bundling.BUNDLING_ATTRIBUTE, project.getObjects().named(Bundling.class, Bundling.EXTERNAL));
|
||||
attributes.attribute(Category.CATEGORY_ATTRIBUTE, project.getObjects().named(Category.class, Category.LIBRARY));
|
||||
attributes.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, project.getObjects().named(LibraryElements.class, LibraryElements.JAR));
|
||||
attributes.attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, Integer.valueOf(JavaVersion.current().getMajorVersion()));
|
||||
attributes.attribute(Usage.USAGE_ATTRIBUTE, project.getObjects().named(Usage.class, Usage.JAVA_RUNTIME));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@ RUN ./setup.sh
|
||||
|
||||
ENV JAVA_HOME /opt/openjdk/java17
|
||||
ENV JDK17 /opt/openjdk/java17
|
||||
ENV JDK20 /opt/openjdk/java20
|
||||
ENV JDK21 /opt/openjdk/java21
|
||||
|
||||
ENV PATH $JAVA_HOME/bin:$PATH
|
||||
|
||||
@@ -5,8 +5,8 @@ case "$1" in
|
||||
java17)
|
||||
echo "https://github.com/bell-sw/Liberica/releases/download/17.0.7+7/bellsoft-jdk17.0.7+7-linux-amd64.tar.gz"
|
||||
;;
|
||||
java20)
|
||||
echo "https://github.com/bell-sw/Liberica/releases/download/20.0.1+10/bellsoft-jdk20.0.1+10-linux-amd64.tar.gz"
|
||||
java21)
|
||||
echo "https://download.java.net/java/early_access/jdk21/31/GPL/openjdk-21-ea+31_linux-x64_bin.tar.gz"
|
||||
;;
|
||||
*)
|
||||
echo $"Unknown java version"
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ curl https://raw.githubusercontent.com/spring-io/concourse-java-scripts/v0.0.4/c
|
||||
|
||||
mkdir -p /opt/openjdk
|
||||
pushd /opt/openjdk > /dev/null
|
||||
for jdk in java17 java20
|
||||
for jdk in java17 java21
|
||||
do
|
||||
JDK_URL=$( /get-jdk-url.sh $jdk )
|
||||
mkdir $jdk
|
||||
|
||||
+2
-2
@@ -3,8 +3,8 @@ github-repo-name: "spring-projects/spring-framework"
|
||||
sonatype-staging-profile: "org.springframework"
|
||||
docker-hub-organization: "springci"
|
||||
artifactory-server: "https://repo.spring.io"
|
||||
branch: "6.0.x"
|
||||
milestone: "6.0.x"
|
||||
branch: "main"
|
||||
milestone: "6.1.x"
|
||||
build-name: "spring-framework"
|
||||
pipeline-name: "spring-framework"
|
||||
concourse-url: "https://ci.spring.io"
|
||||
|
||||
+10
-10
@@ -45,7 +45,7 @@ resource_types:
|
||||
source:
|
||||
<<: *docker-resource-source
|
||||
repository: concourse/registry-image-resource
|
||||
tag: 1.5.0
|
||||
tag: 1.8.0
|
||||
- name: artifactory-resource
|
||||
type: registry-image
|
||||
source:
|
||||
@@ -57,7 +57,7 @@ resource_types:
|
||||
source:
|
||||
<<: *docker-resource-source
|
||||
repository: concourse/github-release-resource
|
||||
tag: 1.5.5
|
||||
tag: 1.8.0
|
||||
- name: github-status-resource
|
||||
type: registry-image
|
||||
source:
|
||||
@@ -113,14 +113,14 @@ resources:
|
||||
access_token: ((github-ci-status-token))
|
||||
branch: ((branch))
|
||||
context: build
|
||||
- name: repo-status-jdk20-build
|
||||
- name: repo-status-jdk21-build
|
||||
type: github-status-resource
|
||||
icon: eye-check-outline
|
||||
source:
|
||||
repository: ((github-repo-name))
|
||||
access_token: ((github-ci-status-token))
|
||||
branch: ((branch))
|
||||
context: jdk20-build
|
||||
context: jdk21-build
|
||||
- name: slack-alert
|
||||
type: slack-notification
|
||||
icon: slack
|
||||
@@ -217,7 +217,7 @@ jobs:
|
||||
"zip.type": "schema"
|
||||
get_params:
|
||||
threads: 8
|
||||
- name: jdk20-build
|
||||
- name: jdk21-build
|
||||
serial: true
|
||||
public: true
|
||||
plan:
|
||||
@@ -225,7 +225,7 @@ jobs:
|
||||
- get: git-repo
|
||||
- get: every-morning
|
||||
trigger: true
|
||||
- put: repo-status-jdk20-build
|
||||
- put: repo-status-jdk21-build
|
||||
params: { state: "pending", commit: "git-repo" }
|
||||
- do:
|
||||
- task: check-project
|
||||
@@ -234,16 +234,16 @@ jobs:
|
||||
privileged: true
|
||||
timeout: ((task-timeout))
|
||||
params:
|
||||
TEST_TOOLCHAIN: 20
|
||||
TEST_TOOLCHAIN: 21
|
||||
<<: *build-project-task-params
|
||||
on_failure:
|
||||
do:
|
||||
- put: repo-status-jdk20-build
|
||||
- put: repo-status-jdk21-build
|
||||
params: { state: "failure", commit: "git-repo" }
|
||||
- put: slack-alert
|
||||
params:
|
||||
<<: *slack-fail-params
|
||||
- put: repo-status-jdk20-build
|
||||
- put: repo-status-jdk21-build
|
||||
params: { state: "success", commit: "git-repo" }
|
||||
- name: stage-milestone
|
||||
serial: true
|
||||
@@ -396,7 +396,7 @@ jobs:
|
||||
|
||||
groups:
|
||||
- name: "builds"
|
||||
jobs: ["build", "jdk20-build"]
|
||||
jobs: ["build", "jdk21-build"]
|
||||
- name: "releases"
|
||||
jobs: ["stage-milestone", "stage-rc", "stage-release", "promote-milestone", "promote-rc", "promote-release", "create-github-release"]
|
||||
- name: "ci-images"
|
||||
|
||||
@@ -4,5 +4,6 @@ set -e
|
||||
source $(dirname $0)/common.sh
|
||||
|
||||
pushd git-repo > /dev/null
|
||||
./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --max-workers=4 check
|
||||
./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false -Porg.gradle.java.installations.fromEnv=JDK17,JDK21 \
|
||||
--no-daemon --max-workers=4 check
|
||||
popd > /dev/null
|
||||
|
||||
@@ -5,5 +5,6 @@ source $(dirname $0)/common.sh
|
||||
repository=$(pwd)/distribution-repository
|
||||
|
||||
pushd git-repo > /dev/null
|
||||
./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --max-workers=4 -PdeploymentRepository=${repository} build publishAllPublicationsToDeploymentRepository
|
||||
./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false -Porg.gradle.java.installations.fromEnv=JDK17,JDK21 \
|
||||
--no-daemon --max-workers=4 -PdeploymentRepository=${repository} build publishAllPublicationsToDeploymentRepository
|
||||
popd > /dev/null
|
||||
|
||||
@@ -4,6 +4,6 @@ set -e
|
||||
source $(dirname $0)/common.sh
|
||||
|
||||
pushd git-repo > /dev/null
|
||||
./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false -Porg.gradle.java.installations.fromEnv=JDK17,JDK20 \
|
||||
./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false -Porg.gradle.java.installations.fromEnv=JDK17,JDK21 \
|
||||
-PmainToolchain=${MAIN_TOOLCHAIN} -PtestToolchain=${TEST_TOOLCHAIN} --no-daemon --max-workers=4 check antora
|
||||
popd > /dev/null
|
||||
|
||||
@@ -35,7 +35,8 @@ git add gradle.properties > /dev/null
|
||||
git commit -m"Release v$stageVersion" > /dev/null
|
||||
git tag -a "v$stageVersion" -m"Release v$stageVersion" > /dev/null
|
||||
|
||||
./gradlew --no-daemon --max-workers=4 -PdeploymentRepository=${repository} build publishAllPublicationsToDeploymentRepository
|
||||
./gradlew --no-daemon --max-workers=4 -PdeploymentRepository=${repository} -Porg.gradle.java.installations.fromEnv=JDK17,JDK21 \
|
||||
build publishAllPublicationsToDeploymentRepository
|
||||
|
||||
git reset --hard HEAD^ > /dev/null
|
||||
if [[ $nextVersion != $snapshotVersion ]]; then
|
||||
|
||||
@@ -45,12 +45,6 @@ tasks.create("generateAntoraResources") {
|
||||
dependsOn 'generateAntoraYml'
|
||||
}
|
||||
|
||||
// Commented out for now:
|
||||
// https://github.com/spring-projects/spring-framework/issues/30481
|
||||
// tasks.named("check") {
|
||||
// dependsOn 'antora'
|
||||
// }
|
||||
|
||||
jar {
|
||||
enabled = false
|
||||
}
|
||||
|
||||
@@ -130,6 +130,7 @@
|
||||
**** xref:testing/testcontext-framework/ctx-management/web.adoc[]
|
||||
**** xref:testing/testcontext-framework/ctx-management/web-mocks.adoc[]
|
||||
**** xref:testing/testcontext-framework/ctx-management/caching.adoc[]
|
||||
**** xref:testing/testcontext-framework/ctx-management/failure-threshold.adoc[]
|
||||
**** xref:testing/testcontext-framework/ctx-management/hierarchies.adoc[]
|
||||
*** xref:testing/testcontext-framework/fixture-di.adoc[]
|
||||
*** xref:testing/testcontext-framework/web-scoped-beans.adoc[]
|
||||
@@ -266,6 +267,7 @@
|
||||
***** xref:web/webmvc/mvc-controller/ann-methods/jackson.adoc[]
|
||||
**** xref:web/webmvc/mvc-controller/ann-modelattrib-methods.adoc[]
|
||||
**** xref:web/webmvc/mvc-controller/ann-initbinder.adoc[]
|
||||
**** xref:web/webmvc/mvc-controller/ann-validation.adoc[]
|
||||
**** xref:web/webmvc/mvc-controller/ann-exceptionhandler.adoc[]
|
||||
**** xref:web/webmvc/mvc-controller/ann-advice.adoc[]
|
||||
*** xref:web/webmvc-functional.adoc[]
|
||||
@@ -360,6 +362,7 @@
|
||||
***** xref:web/webflux/controller/ann-methods/jackson.adoc[]
|
||||
**** xref:web/webflux/controller/ann-modelattrib-methods.adoc[]
|
||||
**** xref:web/webflux/controller/ann-initbinder.adoc[]
|
||||
**** xref:web/webflux/controller/ann-validation.adoc[]
|
||||
**** xref:web/webflux/controller/ann-exceptions.adoc[]
|
||||
**** xref:web/webflux/controller/ann-advice.adoc[]
|
||||
*** xref:web/webflux-functional.adoc[]
|
||||
@@ -414,6 +417,7 @@
|
||||
*** xref:integration/cache/plug.adoc[]
|
||||
*** xref:integration/cache/specific-config.adoc[]
|
||||
** xref:integration/observability.adoc[]
|
||||
** xref:integration/checkpoint-restore.adoc[]
|
||||
** xref:integration/appendix.adoc[]
|
||||
* xref:languages.adoc[]
|
||||
** xref:languages/kotlin.adoc[]
|
||||
|
||||
@@ -39,11 +39,6 @@ resolvable otherwise. See
|
||||
{api-spring-framework}++/core/env/AbstractEnvironment.html#IGNORE_GETENV_PROPERTY_NAME++[`AbstractEnvironment`]
|
||||
for details.
|
||||
|
||||
| `spring.index.ignore`
|
||||
| Instructs Spring to ignore the components index located in
|
||||
`META-INF/spring.components`. See xref:core/beans/classpath-scanning.adoc#beans-scanning-index[Generating an Index of Candidate Components]
|
||||
.
|
||||
|
||||
| `spring.jdbc.getParameterType.ignore`
|
||||
| Instructs Spring to ignore `java.sql.ParameterMetaData.getParameterType` completely.
|
||||
See the note in xref:data-access/jdbc/advanced.adoc#jdbc-batch-list[Batch Operations with a List of Objects].
|
||||
@@ -69,6 +64,11 @@ on a test class. See xref:testing/annotations/integration-junit-jupiter.adoc#int
|
||||
| The maximum size of the context cache in the _Spring TestContext Framework_. See
|
||||
xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching].
|
||||
|
||||
| `spring.test.context.failure.threshold`
|
||||
| The failure threshold for errors encountered while attempting to load an `ApplicationContext`
|
||||
in the _Spring TestContext Framework_. See
|
||||
xref:testing/testcontext-framework/ctx-management/failure-threshold.adoc[Context Failure Threshold].
|
||||
|
||||
| `spring.test.enclosing.configuration`
|
||||
| The default _enclosing configuration inheritance mode_ to use if
|
||||
`@NestedTestConfiguration` is not present on a test class. See
|
||||
|
||||
@@ -728,11 +728,6 @@ of determining parameter names, an exception will be thrown.
|
||||
`StandardReflectionParameterNameDiscoverer` :: Uses the standard `java.lang.reflect.Parameter`
|
||||
API to determine parameter names. Requires that code be compiled with the `-parameters`
|
||||
flag for `javac`. Recommended approach on Java 8+.
|
||||
`LocalVariableTableParameterNameDiscoverer` :: Analyzes the local variable table available
|
||||
in the byte code of the advice class to determine parameter names from debug information.
|
||||
Requires that code be compiled with debug symbols (`-g:vars` at a minimum). Deprecated
|
||||
as of Spring Framework 6.0 for removal in Spring Framework 6.1 in favor of compiling
|
||||
code with `-parameters`. Not supported in a GraalVM native image.
|
||||
`AspectJAdviceParameterNameDiscoverer` :: Deduces parameter names from the pointcut
|
||||
expression, `returning`, and `throwing` clauses. See the
|
||||
{api-spring-framework}/aop/aspectj/AspectJAdviceParameterNameDiscoverer.html[javadoc]
|
||||
|
||||
@@ -989,68 +989,4 @@ metadata is provided per-instance rather than per-class.
|
||||
|
||||
|
||||
|
||||
[[beans-scanning-index]]
|
||||
== Generating an Index of Candidate Components
|
||||
|
||||
While classpath scanning is very fast, it is possible to improve the startup performance
|
||||
of large applications by creating a static list of candidates at compilation time. In this
|
||||
mode, all modules that are targets of component scanning must use this mechanism.
|
||||
|
||||
NOTE: Your existing `@ComponentScan` or `<context:component-scan/>` directives must remain
|
||||
unchanged to request the context to scan candidates in certain packages. When the
|
||||
`ApplicationContext` detects such an index, it automatically uses it rather than scanning
|
||||
the classpath.
|
||||
|
||||
To generate the index, add an additional dependency to each module that contains
|
||||
components that are targets for component scan directives. The following example shows
|
||||
how to do so with Maven:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes,attributes"]
|
||||
----
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context-indexer</artifactId>
|
||||
<version>{spring-version}</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
----
|
||||
|
||||
With Gradle 4.5 and earlier, the dependency should be declared in the `compileOnly`
|
||||
configuration, as shown in the following example:
|
||||
|
||||
[source,groovy,indent=0,subs="verbatim,quotes,attributes"]
|
||||
----
|
||||
dependencies {
|
||||
compileOnly "org.springframework:spring-context-indexer:{spring-version}"
|
||||
}
|
||||
----
|
||||
|
||||
With Gradle 4.6 and later, the dependency should be declared in the `annotationProcessor`
|
||||
configuration, as shown in the following example:
|
||||
|
||||
[source,groovy,indent=0,subs="verbatim,quotes,attributes"]
|
||||
----
|
||||
dependencies {
|
||||
annotationProcessor "org.springframework:spring-context-indexer:{spring-version}"
|
||||
}
|
||||
----
|
||||
|
||||
The `spring-context-indexer` artifact generates a `META-INF/spring.components` file that
|
||||
is included in the jar file.
|
||||
|
||||
NOTE: When working with this mode in your IDE, the `spring-context-indexer` must be
|
||||
registered as an annotation processor to make sure the index is up-to-date when
|
||||
candidate components are updated.
|
||||
|
||||
TIP: The index is enabled automatically when a `META-INF/spring.components` file is found
|
||||
on the classpath. If an index is partially available for some libraries (or use cases)
|
||||
but could not be built for the whole application, you can fall back to a regular classpath
|
||||
arrangement (as though no index were present at all) by setting `spring.index.ignore` to
|
||||
`true`, either as a JVM system property or via the
|
||||
xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -46,7 +46,9 @@ The expression language supports the following functionality:
|
||||
* Inline maps
|
||||
* Ternary operator
|
||||
* Variables
|
||||
* User-defined functions
|
||||
* User-defined functions added to the context
|
||||
* reflective invocation of `Method`
|
||||
* various cases of `MethodHandle`
|
||||
* Collection projection
|
||||
* Collection selection
|
||||
* Templated expressions
|
||||
|
||||
@@ -15,7 +15,7 @@ topics:
|
||||
* xref:core/expressions/language-ref/types.adoc[Types]
|
||||
* xref:core/expressions/language-ref/constructors.adoc[Constructors]
|
||||
* xref:core/expressions/language-ref/variables.adoc[Variables]
|
||||
* xref:core/expressions/language-ref/functions.adoc[Functions]
|
||||
* xref:core/expressions/language-ref/functions.adoc[User-Defined Functions]
|
||||
* xref:core/expressions/language-ref/bean-references.adoc[Bean References]
|
||||
* xref:core/expressions/language-ref/operator-ternary.adoc[Ternary Operator (If-Then-Else)]
|
||||
* xref:core/expressions/language-ref/operator-elvis.adoc[The Elvis Operator]
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
You can extend SpEL by registering user-defined functions that can be called within the
|
||||
expression string. The function is registered through the `EvaluationContext`. The
|
||||
following example shows how to register a user-defined function:
|
||||
following example shows how to register a user-defined function to be invoked via reflection
|
||||
(i.e. a `Method`):
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -94,5 +95,97 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
The use of `MethodHandle` is also supported. This enables potentially more efficient use
|
||||
cases if the `MethodHandle` target and parameters have been fully bound prior to
|
||||
registration, but partially bound handles are also supported.
|
||||
|
||||
Consider the `String#formatted(String, Object...)` instance method, which produces a
|
||||
message according to a template and a variable number of arguments.
|
||||
|
||||
You can register and use the `formatted` method as a `MethodHandle`, as the following
|
||||
example shows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
|
||||
|
||||
MethodHandle mh = MethodHandles.lookup().findVirtual(String.class, "formatted",
|
||||
MethodType.methodType(String.class, Object[].class));
|
||||
context.setVariable("message", mh);
|
||||
|
||||
String message = parser.parseExpression("#message('Simple message: <%s>', 'Hello World', 'ignored')")
|
||||
.getValue(context, String.class);
|
||||
//returns "Simple message: <Hello World>"
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
val parser = SpelExpressionParser()
|
||||
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()
|
||||
|
||||
val mh = MethodHandles.lookup().findVirtual(String::class.java, "formatted",
|
||||
MethodType.methodType(String::class.java, Array<Any>::class.java))
|
||||
context.setVariable("message", mh)
|
||||
|
||||
val message = parser.parseExpression("#message('Simple message: <%s>', 'Hello World', 'ignored')")
|
||||
.getValue(context, String::class.java)
|
||||
----
|
||||
======
|
||||
|
||||
As hinted above, binding a `MethodHandle` and registering the bound `MethodHandle` is also
|
||||
supported. This is likely to be more performant if both the target and all the arguments
|
||||
are bound. In that case no arguments are necessary in the SpEL expression, as the
|
||||
following example shows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
|
||||
|
||||
String template = "This is a %s message with %s words: <%s>";
|
||||
Object varargs = new Object[] { "prerecorded", 3, "Oh Hello World!", "ignored" };
|
||||
MethodHandle mh = MethodHandles.lookup().findVirtual(String.class, "formatted",
|
||||
MethodType.methodType(String.class, Object[].class))
|
||||
.bindTo(template)
|
||||
.bindTo(varargs); //here we have to provide arguments in a single array binding
|
||||
context.setVariable("message", mh);
|
||||
|
||||
String message = parser.parseExpression("#message()")
|
||||
.getValue(context, String.class);
|
||||
//returns "This is a prerecorded message with 3 words: <Oh Hello World!>"
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
val parser = SpelExpressionParser()
|
||||
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()
|
||||
|
||||
val template = "This is a %s message with %s words: <%s>"
|
||||
val varargs = arrayOf("prerecorded", 3, "Oh Hello World!", "ignored")
|
||||
|
||||
val mh = MethodHandles.lookup().findVirtual(String::class.java, "formatted",
|
||||
MethodType.methodType(String::class.java, Array<Any>::class.java))
|
||||
.bindTo(template)
|
||||
.bindTo(varargs) //here we have to provide arguments in a single array binding
|
||||
context.setVariable("message", mh)
|
||||
|
||||
val message = parser.parseExpression("#message()")
|
||||
.getValue(context, String::class.java)
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -38,6 +38,10 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
NOTE: The SpEL Elvis operator also checks for _empty_ Strings in addition to `null` objects.
|
||||
The original snippet is thus only close to emulating the semantics of the operator (it would need an
|
||||
additional `!name.isEmpty()` check).
|
||||
|
||||
The following listing shows a more complex example:
|
||||
|
||||
[tabs]
|
||||
@@ -53,7 +57,7 @@ Java::
|
||||
String name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String.class);
|
||||
System.out.println(name); // Nikola Tesla
|
||||
|
||||
tesla.setName(null);
|
||||
tesla.setName("");
|
||||
name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String.class);
|
||||
System.out.println(name); // Elvis Presley
|
||||
----
|
||||
@@ -69,7 +73,7 @@ Kotlin::
|
||||
var name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String::class.java)
|
||||
println(name) // Nikola Tesla
|
||||
|
||||
tesla.setName(null)
|
||||
tesla.setName("")
|
||||
name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String::class.java)
|
||||
println(name) // Elvis Presley
|
||||
----
|
||||
|
||||
@@ -123,15 +123,12 @@ Validator, is expected to be present in the classpath and is automatically detec
|
||||
|
||||
|
||||
[[validation-beanvalidation-spring-inject]]
|
||||
=== Injecting a Validator
|
||||
=== Inject Jakarta Validator
|
||||
|
||||
`LocalValidatorFactoryBean` implements both `jakarta.validation.ValidatorFactory` and
|
||||
`jakarta.validation.Validator`, as well as Spring's `org.springframework.validation.Validator`.
|
||||
You can inject a reference to either of these interfaces into beans that need to invoke
|
||||
validation logic.
|
||||
|
||||
You can inject a reference to `jakarta.validation.Validator` if you prefer to work with the Bean
|
||||
Validation API directly, as the following example shows:
|
||||
`jakarta.validation.Validator`, so you can inject a reference to the latter to
|
||||
apply validation logic if you prefer to work with the Bean Validation API directly,
|
||||
as the following example shows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -160,8 +157,15 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
You can inject a reference to `org.springframework.validation.Validator` if your bean
|
||||
requires the Spring Validation API, as the following example shows:
|
||||
|
||||
[[validation-beanvalidation-spring-inject-adapter]]
|
||||
=== Inject Spring Validator
|
||||
|
||||
In addition to implementing `jakarta.validation.Validator`, `LocalValidatorFactoryBean`
|
||||
also adapts to `org.springframework.validation.Validator`, so you can inject a reference
|
||||
to the latter if your bean requires the Spring Validation API.
|
||||
|
||||
For example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -190,9 +194,15 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
When used as `org.springframework.validation.Validator`, `LocalValidatorFactoryBean`
|
||||
invokes the underlying `jakarta.validation.Validator`, and then adapts
|
||||
``ContraintViolation``s to ``FieldError``s, and registers them with the `Errors` object
|
||||
passed into the `validate` method.
|
||||
|
||||
|
||||
|
||||
[[validation-beanvalidation-spring-constraints]]
|
||||
=== Configuring Custom Constraints
|
||||
=== Configure Custom Constraints
|
||||
|
||||
Each bean validation constraint consists of two parts:
|
||||
|
||||
@@ -274,9 +284,8 @@ As the preceding example shows, a `ConstraintValidator` implementation can have
|
||||
[[validation-beanvalidation-spring-method]]
|
||||
=== Spring-driven Method Validation
|
||||
|
||||
You can integrate the method validation feature supported by Bean Validation 1.1 (and, as
|
||||
a custom extension, also by Hibernate Validator 4.3) into a Spring context through a
|
||||
`MethodValidationPostProcessor` bean definition:
|
||||
You can integrate the method validation feature of Bean Validation into a
|
||||
Spring context through a `MethodValidationPostProcessor` bean definition:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -305,11 +314,11 @@ XML::
|
||||
----
|
||||
======
|
||||
|
||||
To be eligible for Spring-driven method validation, all target classes need to be annotated
|
||||
To be eligible for Spring-driven method validation, target classes need to be annotated
|
||||
with Spring's `@Validated` annotation, which can optionally also declare the validation
|
||||
groups to use. See
|
||||
{api-spring-framework}/validation/beanvalidation/MethodValidationPostProcessor.html[`MethodValidationPostProcessor`]
|
||||
for setup details with the Hibernate Validator and Bean Validation 1.1 providers.
|
||||
for setup details with the Hibernate Validator and Bean Validation providers.
|
||||
|
||||
[TIP]
|
||||
====
|
||||
@@ -320,8 +329,141 @@ xref:core/aop/proxying.adoc#aop-understanding-aop-proxies[Understanding AOP Prox
|
||||
to always use methods and accessors on proxied classes; direct field access will not work.
|
||||
====
|
||||
|
||||
NOTE: Spring MVC and WebFlux have built-in support for method validation, and therefore
|
||||
for web controller methods there is no need for a class level `@Validated` and an AOP proxy.
|
||||
See the Spring MVC xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation] section,
|
||||
the WebFlux xref:web/webflux/controller/ann-validation.adoc[Validation] section,
|
||||
and the xref:web/webmvc/mvc-controller/ann-validation.adoc[Error Responses] section.
|
||||
|
||||
|
||||
[[validation-beanvalidation-spring-method-exceptions]]
|
||||
==== Method Validation Exceptions
|
||||
|
||||
By default, `jakarta.validation.ConstraintViolationException` is raised with the set of
|
||||
``ConstraintViolation``s returned by `jakarata.validation.Validator`. As an alternative,
|
||||
you can have `MethodValidationException` raised instead with ``ConstraintViolation``s
|
||||
adapted to `MessageSourceResolvable` errors. To enable set the following flag:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
|
||||
|
||||
@Configuration
|
||||
public class AppConfig {
|
||||
|
||||
@Bean
|
||||
public MethodValidationPostProcessor validationPostProcessor() {
|
||||
MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
|
||||
processor.setAdaptConstraintViolations(true);
|
||||
return processor;
|
||||
}
|
||||
}
|
||||
|
||||
----
|
||||
|
||||
XML::
|
||||
+
|
||||
[source,xml,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor">
|
||||
<property name="adaptConstraintViolations" value="true"/>
|
||||
</bean>
|
||||
----
|
||||
======
|
||||
|
||||
`MethodValidationException` contains a list of ``ParameterValidationResult``s which
|
||||
group errors by method parameter, and each exposes a `MethodParameter`, the argument
|
||||
value, and a list of `MessageSourceResolvable` errors adapted from
|
||||
``ConstraintViolation``s. For `@Valid` method parameters with cascaded violations on
|
||||
fields and properties, the `ParameterValidationResult` is `ParameterErrors` which
|
||||
implements `org.springframework.validation.Errors` and exposes validation errors as
|
||||
``FieldError``s.
|
||||
|
||||
|
||||
[[validation-beanvalidation-spring-method-i18n]]
|
||||
==== Customizing Validation Errors
|
||||
|
||||
The adapted `MessageSourceResolvable` errors can be turned into error messages to
|
||||
display to users through the configured `MessageSource` with locale and language specific
|
||||
resource bundles. This section provides an example for illustration.
|
||||
|
||||
Given the following class declarations:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
record Person(@Size(min = 1, max = 10) String name) {
|
||||
}
|
||||
|
||||
@Validated
|
||||
public class MyService {
|
||||
|
||||
void addStudent(@Valid Person person, @Max(2) int degrees) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@JvmRecord
|
||||
internal data class Person(@Size(min = 1, max = 10) val name: String)
|
||||
|
||||
@Validated
|
||||
class MyService {
|
||||
|
||||
fun addStudent(person: @Valid Person?, degrees: @Max(2) Int) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
A `ConstraintViolation` on `Person.name()` is adapted to a `FieldErrro` with the following:
|
||||
|
||||
- Error codes `"Size.student.name"`, `"Size.name"`, `"Size.java.lang.String"`, and `"Size"`
|
||||
- Message arguments `"name"`, `10`, and `1` (the field name and the constraint attributes)
|
||||
- Default message "size must be between 1 and 10"
|
||||
|
||||
To customize the default message, you can add properties to
|
||||
xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource]
|
||||
resource bundles using any of the above errors codes and message arguments. Note also that the
|
||||
message argument `"name"` is itself a `MessagreSourceResolvable` with error codes
|
||||
`"student.name"` and `"name"` and can customized too. For example:
|
||||
|
||||
Properties::
|
||||
+
|
||||
[source,properties,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
Size.student.name=Please, provide a {0} that is between {2} and {1} characters long
|
||||
student.name=username
|
||||
----
|
||||
|
||||
A `ConstraintViolation` on the `degrees` method parameter is adapted to a
|
||||
`MessageSourceResolvable` with the following:
|
||||
|
||||
- Error codes `"Max.myService#addStudent.degrees"`, `"Max.degrees"`, `"Max.int"`, `"Max"`
|
||||
- Message arguments "degrees2 and 2 (the field name and the constraint attribute)
|
||||
- Default message "must be less than or equal to 2"
|
||||
|
||||
To customize the above default message, you can add a property such as:
|
||||
|
||||
Properties::
|
||||
+
|
||||
[source,properties,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
Max.degrees=You cannot provide more than {1} {0}
|
||||
----
|
||||
|
||||
|
||||
[[validation-beanvalidation-spring-other]]
|
||||
=== Additional Configuration Options
|
||||
|
||||
@@ -16,7 +16,7 @@ STOMP Messaging.
|
||||
xref:web-reactive.adoc[Web Reactive] :: Spring WebFlux, WebClient,
|
||||
WebSocket, RSocket.
|
||||
xref:integration.adoc[Integration] :: REST Clients, JMS, JCA, JMX,
|
||||
Email, Tasks, Scheduling, Caching, Observability.
|
||||
Email, Tasks, Scheduling, Caching, Observability, JVM Checkpoint Restore.
|
||||
xref:languages.adoc[Languages] :: Kotlin, Groovy, Dynamic Languages.
|
||||
xref:testing/appendix.adoc[Appendix] :: Spring properties.
|
||||
https://github.com/spring-projects/spring-framework/wiki[Wiki] :: What's New,
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
[[checkpoint-restore]]
|
||||
= JVM Checkpoint Restore
|
||||
|
||||
The Spring Framework integrates with checkpoint/restore as implemented by https://github.com/CRaC/docs[Project CRaC] in order to allow implementing systems capable to reduce the startup and warmup times of Spring-based Java applications with the JVM.
|
||||
|
||||
Using this feature requires:
|
||||
|
||||
* A checkpoint/restore enabled JVM (Linux only for now).
|
||||
* The presence in the classpath of the https://github.com/CRaC/org.crac[`org.crac:crac`] library.
|
||||
* Specifying the required `java` command line parameters like `-XX:CRaCCheckpointTo=PATH` or `-XX:CRaCRestoreFrom=PATH`.
|
||||
|
||||
WARNING: The files generated in the path specified by `-XX:CRaCCheckpointTo=PATH` when a checkpoint is requested contain a representation of the memory of the running JVM, which may contain secrets and other sensitive data. Using this feature should be done with the assumption that any value "seen" by the JVM, such as configuration properties coming from the environment, will be stored in those CRaC files. As a consequence, the security implications of where and how those files are generated, stored and accessed should be carefully assessed.
|
||||
|
||||
Conceptually, checkpoint and restore match with xref:core/beans/factory-nature.adoc#beans-factory-lifecycle-processor[Spring `Lifecycle` contract] for individual beans.
|
||||
|
||||
== On demand checkpoint/restore of a running application
|
||||
|
||||
A checkpoint can be created on demand, for example using a command like `jcmd application.jar JDK.checkpoint`. Before the creation of the checkpoint, Spring Framework
|
||||
stops all the running beans, giving them a chance to close resources if needed by implementing `Lifecycle.stop`. After restore, the same beans are restarted, with `Lifecycle.start` allowing to reopen resources when relevant. For libraries not depending on Spring, checkpoint/restore custom integration can be provided by implementing `org.crac.Resource` and registering the related instance.
|
||||
|
||||
WARNING: Leveraging checkpoint/restore of a running application typically requires additional lifecycle management to gracefully stop and start using resources like files or sockets and stop active threads.
|
||||
|
||||
NOTE: If the checkpoint is created on a warmed-up JVM, the restored JVM will be equally warmed-up, allowing potentially peak performance immediately. This method typically requires access to remote services, and thus requires some level of platform integration.
|
||||
|
||||
== Automatic checkpoint/restore at startup
|
||||
|
||||
When the `-Dspring.context.checkpoint=onRefresh` Java system property is set, a checkpoint is created automatically during the startup at `LifecycleProcessor.onRefresh` level. At this phase, all non-lazy initialized singletons are instantiated, `InitializingBean.afterPropertiesSet` callbacks have been invoked, but not `Lifecycle.start` ones and `ContextRefreshedEvent` has not yet been published.
|
||||
|
||||
WARNING: As mentioned above, and especially in use cases where the CRaC files are shipped as part of a deployable artifact (a container image for example), operate with the assumption that any sensitive data "seen" by the JVM ends up in the CRaC files, and assess carefully the related security implications.
|
||||
|
||||
NOTE: Here checkpoint/restore is a way to "fast-forward" the startup of the application to a phase where the application context is about to start, but does not allow to have a fully warmed-up JVM.
|
||||
@@ -21,11 +21,14 @@ As outlined xref:integration/observability.adoc[at the beginning of this section
|
||||
|===
|
||||
|Observation name |Description
|
||||
|
||||
|xref:integration/observability.adoc#http-client[`"http.client.requests"`]
|
||||
|xref:integration/observability.adoc#observability.http-client[`"http.client.requests"`]
|
||||
|Time spent for HTTP client exchanges
|
||||
|
||||
|xref:integration/observability.adoc#http-server[`"http.server.requests"`]
|
||||
|xref:integration/observability.adoc#observability.http-server[`"http.server.requests"`]
|
||||
|Processing time for HTTP server exchanges at the Framework level
|
||||
|
||||
|xref:integration/observability.adoc#observability.tasks-scheduled[`"tasks.scheduled.execution"`]
|
||||
|Processing time for an execution of a `@Scheduled` task
|
||||
|===
|
||||
|
||||
NOTE: Observations are using Micrometer's official naming convention, but Metrics names will be automatically converted
|
||||
@@ -79,6 +82,31 @@ include-code::./ServerRequestObservationFilter[]
|
||||
|
||||
You can configure `ObservationFilter` instances on the `ObservationRegistry`.
|
||||
|
||||
[[observability.tasks-scheduled]]
|
||||
== @Scheduled tasks instrumentation
|
||||
|
||||
An Observation is created for xref:integration/scheduling.adoc#scheduling-enable-annotation-support[each execution of an `@Scheduled` task].
|
||||
Applications need to configure the `ObservationRegistry` on the `ScheduledTaskRegistrar` to enable the recording of observations.
|
||||
This can be done by declaring a `SchedulingConfigurer` bean that sets the observation registry:
|
||||
|
||||
include-code::./ObservationSchedulingConfigurer[]
|
||||
|
||||
It is using the `org.springframework.scheduling.support.DefaultScheduledTaskObservationConvention` by default, backed by the `ScheduledTaskObservationContext`.
|
||||
You can configure a custom implementation on the `ObservationRegistry` directly.
|
||||
During the execution of the scheduled method, the current observation is restored in the `ThreadLocal` context or the Reactor context (if the scheduled method returns a `Mono` or `Flux` type).
|
||||
|
||||
By default, the following `KeyValues` are created:
|
||||
|
||||
.Low cardinality Keys
|
||||
[cols="a,a"]
|
||||
|===
|
||||
|Name | Description
|
||||
|`code.function` _(required)_|Name of Java `Method` that is scheduled for execution.
|
||||
|`code.namespace` _(required)_|Canonical name of the class of the bean instance that holds the scheduled method.
|
||||
|`exception` _(required)_|Name of the exception thrown during the execution, or `"none"` if no exception happened.
|
||||
|`outcome` _(required)_|Outcome of the method execution. Can be `"SUCCESS"`, `"ERROR"` or `"UNKNOWN"` (if for example the operation was cancelled during execution).
|
||||
|===
|
||||
|
||||
|
||||
[[observability.http-server]]
|
||||
== HTTP Server instrumentation
|
||||
@@ -107,7 +135,7 @@ By default, the following `KeyValues` are created:
|
||||
[cols="a,a"]
|
||||
|===
|
||||
|Name | Description
|
||||
|`exception` _(required)_|Name of the exception thrown during the exchange, or `KeyValue#NONE_VALUE`} if no exception happened.
|
||||
|`exception` _(required)_|Name of the exception thrown during the exchange, or `"none"` if no exception happened.
|
||||
|`method` _(required)_|Name of HTTP request method or `"none"` if the request was not received properly.
|
||||
|`outcome` _(required)_|Outcome of the HTTP server exchange.
|
||||
|`status` _(required)_|HTTP response raw status code, or `"UNKNOWN"` if no response was created.
|
||||
@@ -125,11 +153,15 @@ By default, the following `KeyValues` are created:
|
||||
[[observability.http-server.reactive]]
|
||||
=== Reactive applications
|
||||
|
||||
Applications need to configure the `org.springframework.web.filter.reactive.ServerHttpObservationFilter` reactive `WebFilter` in their application.
|
||||
It uses the `org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention` by default, backed by the `ServerRequestObservationContext`.
|
||||
Applications need to configure the `WebHttpHandlerBuilder` with a `MeterRegistry` to enable server instrumentation.
|
||||
This can be done on the `WebHttpHandlerBuilder`, as follows:
|
||||
|
||||
This will only record an observation as an error if the `Exception` has not been handled by the web framework and has bubbled up to the `WebFilter`.
|
||||
Typically, all exceptions handled by Spring WebFlux's `@ExceptionHandler` and xref:web/webflux/ann-rest-exceptions.adoc[`ProblemDetail` support] will not be recorded with the observation.
|
||||
include-code::./HttpHandlerConfiguration[]
|
||||
|
||||
It is using the `org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention` by default, backed by the `ServerRequestObservationContext`.
|
||||
|
||||
This will only record an observation as an error if the `Exception` has not been handled by an application Controller.
|
||||
Typically, all exceptions handled by Spring WebFlux's `@ExceptionHandler` and <<web.adoc#webflux-ann-rest-exceptions,`ProblemDetail` support>> will not be recorded with the observation.
|
||||
You can, at any point during request processing, set the error field on the `ObservationContext` yourself:
|
||||
|
||||
include-code::./UserController[]
|
||||
|
||||
@@ -3,11 +3,19 @@
|
||||
|
||||
The Spring Framework provides the following choices for making calls to REST endpoints:
|
||||
|
||||
* xref:integration/rest-clients.adoc#rest-restclient[`RestClient`] - synchronous client with a fluent API.
|
||||
* xref:integration/rest-clients.adoc#rest-webclient[`WebClient`] - non-blocking, reactive client with fluent API.
|
||||
* xref:integration/rest-clients.adoc#rest-resttemplate[`RestTemplate`] - synchronous client with template method API.
|
||||
* xref:integration/rest-clients.adoc#rest-http-interface[HTTP Interface] - annotated interface with generated, dynamic proxy implementation.
|
||||
|
||||
|
||||
[[rest-restclient]]
|
||||
== `RestClient`
|
||||
|
||||
Reference documentation is forthcoming.
|
||||
For now, please refer to the https://docs.spring.io/spring-framework/docs/6.1.0-M2/javadoc-api/org/springframework/web/client/RestClient.html[API documentation].
|
||||
|
||||
|
||||
[[rest-webclient]]
|
||||
== `WebClient`
|
||||
|
||||
@@ -36,9 +44,8 @@ The `RestTemplate` provides a higher level API over HTTP client libraries. It ma
|
||||
easy to invoke REST endpoints in a single line. It exposes the following groups of
|
||||
overloaded methods:
|
||||
|
||||
NOTE: `RestTemplate` is in maintenance mode, with only requests for minor
|
||||
changes and bugs to be accepted. Please, consider using the
|
||||
xref:web/webflux-webclient.adoc[WebClient] instead.
|
||||
NOTE: The xref:integration/rest-clients.adoc#rest-restclient[`RestClient`] offers a more modern API for synchronous HTTP access.
|
||||
For asynchronous and streaming scenarios, consider the reactive xref:web/webflux-webclient.adoc[WebClient].
|
||||
|
||||
[[rest-overview-of-resttemplate-methods-tbl]]
|
||||
.RestTemplate methods
|
||||
@@ -375,12 +382,37 @@ One, declare an interface with `@HttpExchange` methods:
|
||||
}
|
||||
----
|
||||
|
||||
Two, create a proxy that will perform the declared HTTP exchanges:
|
||||
Two, create a proxy that will perform the declared HTTP exchanges,
|
||||
either using `WebClient`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
WebClient client = WebClient.builder().baseUrl("https://api.github.com/").build();
|
||||
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder(WebClientAdapter.forClient(client)).build();
|
||||
WebClientAdapter adapter = WebClientAdapter.forClient(webClient)
|
||||
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
|
||||
|
||||
RepositoryService service = factory.createClient(RepositoryService.class);
|
||||
----
|
||||
|
||||
using `RestTemplate`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory("https://api.github.com/"));
|
||||
RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate);
|
||||
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
|
||||
|
||||
RepositoryService service = factory.createClient(RepositoryService.class);
|
||||
----
|
||||
|
||||
or using `RestClient`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
RestClient restClient = RestClient.builder().baseUrl("https://api.github.com/").build();
|
||||
RestClientAdapter adapter = RestClientAdapter.create(restClient);
|
||||
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
|
||||
|
||||
RepositoryService service = factory.createClient(RepositoryService.class);
|
||||
----
|
||||
@@ -448,6 +480,10 @@ method parameters:
|
||||
Object (entity to be encoded, e.g. as JSON), `HttpEntity` (part content and headers),
|
||||
a Spring `Part`, or Reactive Streams `Publisher` of any of the above.
|
||||
|
||||
| `MultipartFile`
|
||||
| Add a request part from a `MultipartFile`, typically used in a Spring MVC controller
|
||||
where it represents an uploaded file.
|
||||
|
||||
| `@CookieValue`
|
||||
| Add a cookie or multiple cookies. The argument may be a `Map<String, ?>` or
|
||||
`MultiValueMap<String, ?>` with multiple cookies, a `Collection<?>` of values, or an
|
||||
@@ -497,6 +533,13 @@ Annotated, HTTP exchange methods support the following return values:
|
||||
TIP: You can also use any other async or reactive types registered in the
|
||||
`ReactiveAdapterRegistry`.
|
||||
|
||||
By default, the behavior of HTTP service methods with synchronous (blocking) method
|
||||
signature depends on connection and timeout settings of the underlying HTTP client.
|
||||
`HttpServiceProxyFactory.Builder` does expose a `blockTimeout` option that also lets you
|
||||
configure the maximum time to block for a response, but we recommend configuring timeout
|
||||
values directly on the underlying HTTP client, which likely provides more control over
|
||||
such settings.
|
||||
|
||||
|
||||
[[rest-http-interface-exceptions]]
|
||||
=== Exception Handling
|
||||
|
||||
@@ -399,6 +399,120 @@ container and once through the `@Configurable` aspect), with the consequence of
|
||||
`@Scheduled` method being invoked twice.
|
||||
====
|
||||
|
||||
[[scheduling-annotation-support-scheduled-reactive]]
|
||||
=== The `@Scheduled` annotation on Reactive methods or Kotlin suspending functions
|
||||
|
||||
As of Spring Framework 6.1, `@Scheduled` methods are also supported on several types
|
||||
of reactive methods:
|
||||
|
||||
- methods with a `Publisher` return type (or any concrete implementation of `Publisher`)
|
||||
like in the following example:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Scheduled(fixedDelay = 500)
|
||||
public Publisher<Void> reactiveSomething() {
|
||||
// return an instance of Publisher
|
||||
}
|
||||
----
|
||||
|
||||
- methods with a return type that can be adapted to `Publisher` via the shared instance
|
||||
of the `ReactiveAdapterRegistry`, provided the type supports _deferred subscription_ like
|
||||
in the following example:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Scheduled(fixedDelay = 500)
|
||||
public Single<String> rxjavaNonPublisher() {
|
||||
return Single.just("example");
|
||||
}
|
||||
----
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
The `CompletableFuture` class is an example of a type that can typically be adapted
|
||||
to `Publisher` but doesn't support deferred subscription. Its `ReactiveAdapter` in the
|
||||
registry denotes that by having the `getDescriptor().isDeferred()` method return `false`.
|
||||
====
|
||||
|
||||
- Kotlin suspending functions, like in the following example:
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Scheduled(fixedDelay = 500)
|
||||
suspend fun something() {
|
||||
// do something asynchronous
|
||||
}
|
||||
----
|
||||
|
||||
- methods that return a Kotlin `Flow` or `Deferred` instance, like in the following example:
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Scheduled(fixedDelay = 500)
|
||||
fun something(): Flow<Void> {
|
||||
flow {
|
||||
// do something asynchronous
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
All these types of methods must be declared without any arguments. In the case of Kotlin
|
||||
suspending functions, the `kotlinx.coroutines.reactor` bridge must also be present to allow
|
||||
the framework to invoke a suspending function as a `Publisher`.
|
||||
|
||||
The Spring Framework will obtain a `Publisher` for the annotated method once and will
|
||||
schedule a `Runnable` in which it subscribes to said `Publisher`. These inner regular
|
||||
subscriptions occur according to the corresponding `cron`/fixedDelay`/`fixedRate` configuration.
|
||||
|
||||
If the `Publisher` emits `onNext` signal(s), these are ignored and discarded (the same way
|
||||
return values from synchronous `@Scheduled` methods are ignored).
|
||||
|
||||
In the following example, the `Flux` emits `onNext("Hello"), onNext("World")` every 5
|
||||
seconds, but these values are unused:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Scheduled(initialDelay = 5000, fixedRate = 5000)
|
||||
public Flux<String> reactiveSomething() {
|
||||
return Flux.just("Hello", "World");
|
||||
}
|
||||
----
|
||||
|
||||
If the `Publisher` emits an `onError` signal, it is logged at `WARN` level and recovered.
|
||||
Because of the asynchronous and lazy nature of `Publisher` instances, exceptions are
|
||||
not thrown from the `Runnable` task: this means that the `ErrorHandler` contract is not
|
||||
involved for reactive methods.
|
||||
|
||||
As a result, further scheduled subscription occurs despite the error.
|
||||
|
||||
In the following example, the `Mono` subscription fails twice in the first five seconds.
|
||||
Then subscriptions start succeeding, printing a message to the standard output every five
|
||||
seconds:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Scheduled(initialDelay = 0, fixedRate = 5000)
|
||||
public Mono<Void> reactiveSomething() {
|
||||
AtomicInteger countdown = new AtomicInteger(2);
|
||||
|
||||
return Mono.defer(() -> {
|
||||
if (countDown.get() == 0 || countDown.decrementAndGet() == 0) {
|
||||
return Mono.fromRunnable(() -> System.out.println("Message"));
|
||||
}
|
||||
return Mono.error(new IllegalStateException("Cannot deliver message"));
|
||||
})
|
||||
}
|
||||
----
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
When destroying the annotated bean or closing the application context, Spring Framework cancels
|
||||
scheduled tasks, which includes the next scheduled subscription to the `Publisher` as well
|
||||
as any past subscription that is still currently active (e.g. for long-running publishers
|
||||
or even infinite publishers).
|
||||
====
|
||||
|
||||
|
||||
[[scheduling-annotation-support-async]]
|
||||
=== The `@Async` annotation
|
||||
|
||||
@@ -1066,3 +1066,10 @@ method parameters:
|
||||
Annotated, RSocket exchange methods support return values that are concrete value(s), or
|
||||
any producer of value(s) that can be adapted to a Reactive Streams `Publisher` via
|
||||
`ReactiveAdapterRegistry`.
|
||||
|
||||
By default, the behavior of RSocket service methods with synchronous (blocking) method
|
||||
signature depends on response timeout settings of the underlying RSocket `ClientTransport`
|
||||
as well as RSocket keep-alive settings. `RSocketServiceProxyFactory.Builder` does expose a
|
||||
`blockTimeout` option that also lets you configure the maximum time to block for a response,
|
||||
but we recommend configuring timeout values at the RSocket level for more control.
|
||||
|
||||
|
||||
@@ -119,5 +119,6 @@ advanced use cases.
|
||||
* xref:testing/testcontext-framework/ctx-management/dynamic-property-sources.adoc[Context Configuration with Dynamic Property Sources]
|
||||
* xref:testing/testcontext-framework/ctx-management/web.adoc[Loading a `WebApplicationContext`]
|
||||
* xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching]
|
||||
* xref:testing/testcontext-framework/ctx-management/failure-threshold.adoc[Context Failure Threshold]
|
||||
* xref:testing/testcontext-framework/ctx-management/hierarchies.adoc[Context Hierarchies]
|
||||
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
[[testcontext-ctx-management-failure-threshold]]
|
||||
= Context Failure Threshold
|
||||
|
||||
As of Spring Framework 6.1, a context _failure threshold_ policy is in place which helps
|
||||
avoid repeated attempts to load a failing `ApplicationContext`. By default, the failure
|
||||
threshold is set to `1` which means that only one attempt will be made to load an
|
||||
`ApplicationContext` for a given context cache key (see
|
||||
xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching]). Any
|
||||
subsequent attempt to load the `ApplicationContext` for the same context cache key will
|
||||
result in an immediate `IllegalStateException` with an error message which explains that
|
||||
the attempt was preemptively skipped. This behavior allows individual test classes and
|
||||
test suites to fail faster by avoiding repeated attempts to load an `ApplicationContext`
|
||||
that will never successfully load -- for example, due to a configuration error or a missing
|
||||
external resource that prevents the context from loading in the current environment.
|
||||
|
||||
You can configure the context failure threshold from the command line or a build script
|
||||
by setting a JVM system property named `spring.test.context.failure.threshold` with a
|
||||
positive integer value. As an alternative, you can set the same property via the
|
||||
xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism.
|
||||
|
||||
NOTE: If you wish to effectively disable the context failure threshold, you can set the
|
||||
property to a very large value. For example, from the command line you could set the
|
||||
system property via `-Dspring.test.context.failure.threshold=1000000`.
|
||||
@@ -67,52 +67,44 @@ from an existing `ProblemDetail`. This could be done centrally, e.g. from an
|
||||
|
||||
|
||||
[[webflux-ann-rest-exceptions-i18n]]
|
||||
== Internationalization
|
||||
== Customization and i18n
|
||||
[.small]#xref:web/webmvc/mvc-ann-rest-exceptions.adoc#mvc-ann-rest-exceptions-i18n[See equivalent in the Servlet stack]#
|
||||
|
||||
It is a common requirement to internationalize error response details, and good practice
|
||||
to customize the problem details for Spring WebFlux exceptions. This is supported as follows:
|
||||
It is a common requirement to customize and internationalize error response details.
|
||||
It is also good practice to customize the problem details for Spring WebFlux exceptions
|
||||
to avoid revealing implementation details. This section describes the support for that.
|
||||
|
||||
- Each `ErrorResponse` exposes a message code and arguments to resolve the "detail" field
|
||||
through a xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource].
|
||||
The actual message code value is parameterized with placeholders, e.g.
|
||||
`+"HTTP method {0} not supported"+` to be expanded from the arguments.
|
||||
- Each `ErrorResponse` also exposes a message code to resolve the "title" field.
|
||||
- `ResponseEntityExceptionHandler` uses the message code and arguments to resolve the
|
||||
"detail" and the "title" fields.
|
||||
An `ErrorResponse` exposes message codes for "type", "title", and "detail", as well as
|
||||
message code arguments for the "detail" field. `ResponseEntityExceptionHandler` resolves
|
||||
these through a xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource]
|
||||
and updates the corresponding `ProblemDetail` fields accordingly.
|
||||
|
||||
By default, the message code for the "detail" field is "problemDetail." + the fully
|
||||
qualified exception class name. Some exceptions may expose additional message codes in
|
||||
which case a suffix is added to the default message code. The table below lists message
|
||||
arguments and codes for Spring WebFlux exceptions:
|
||||
The default strategy for message codes follows the pattern:
|
||||
|
||||
`problemDetail.[type|title|detail].[fully qualified exception class name]`
|
||||
|
||||
An `ErrorResponse` may expose more than one message code, typically adding a suffix
|
||||
to the default message code. The table below lists message codes, and arguments for
|
||||
Spring WebFlux exceptions:
|
||||
|
||||
[[webflux-ann-rest-exceptions-codes]]
|
||||
[cols="1,1,2", options="header"]
|
||||
|===
|
||||
| Exception | Message Code | Message Code Arguments
|
||||
|
||||
| `UnsupportedMediaTypeStatusException`
|
||||
| `HandlerMethodValidationException`
|
||||
| (default)
|
||||
| `+{0}+` the media type that is not supported, `+{1}+` list of supported media types
|
||||
| `+{0}+` list all validation errors.
|
||||
Message codes and arguments for each error are also resolved via `MessageSource`.
|
||||
|
||||
| `UnsupportedMediaTypeStatusException`
|
||||
| (default) + ".parseError"
|
||||
|
|
||||
| `MethodNotAllowedException`
|
||||
| (default)
|
||||
| `+{0}+` the current HTTP method, `+{1}+` the list of supported HTTP methods
|
||||
|
||||
| `MissingRequestValueException`
|
||||
| (default)
|
||||
| `+{0}+` a label for the value (e.g. "request header", "cookie value", ...), `+{1}+` the value name
|
||||
|
||||
| `UnsatisfiedRequestParameterException`
|
||||
| (default)
|
||||
| `+{0}+` the list of parameter conditions
|
||||
|
||||
| `WebExchangeBindException`
|
||||
| (default)
|
||||
| `+{0}+` the list of global errors, `+{1}+` the list of field errors.
|
||||
Message codes and arguments for each error within the `BindingResult` are also resolved
|
||||
via `MessageSource`.
|
||||
|
||||
| `NotAcceptableStatusException`
|
||||
| (default)
|
||||
| `+{0}+` list of supported media types
|
||||
@@ -125,14 +117,32 @@ via `MessageSource`.
|
||||
| (default)
|
||||
| `+{0}+` the failure reason provided to the class constructor
|
||||
|
||||
| `MethodNotAllowedException`
|
||||
| `UnsupportedMediaTypeStatusException`
|
||||
| (default)
|
||||
| `+{0}+` the current HTTP method, `+{1}+` the list of supported HTTP methods
|
||||
| `+{0}+` the media type that is not supported, `+{1}+` list of supported media types
|
||||
|
||||
| `UnsupportedMediaTypeStatusException`
|
||||
| (default) + ".parseError"
|
||||
|
|
||||
|
||||
| `UnsatisfiedRequestParameterException`
|
||||
| (default)
|
||||
| `+{0}+` the list of parameter conditions
|
||||
|
||||
| `WebExchangeBindException`
|
||||
| (default)
|
||||
| `+{0}+` the list of global errors, `+{1}+` the list of field errors.
|
||||
Message codes and arguments for each error are also resolved via `MessageSource`.
|
||||
|
||||
|===
|
||||
|
||||
By default, the message code for the "title" field is "problemDetail.title." + the fully
|
||||
qualified exception class name.
|
||||
NOTE: Unlike other exceptions, the message arguments for
|
||||
`WebExchangeBindException` and `HandlerMethodValidationException` are based on a list of
|
||||
`MessageSourceResolvable` errors that can also be customized through a
|
||||
xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource]
|
||||
resource bundle. See
|
||||
xref:core/validation/beanvalidation.adoc#validation-beanvalidation-spring-method-i18n[Customizing Validation Errors]
|
||||
for more details.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -700,9 +700,8 @@ Java::
|
||||
|
||||
@Override
|
||||
public void configurePathMatch(PathMatchConfigurer configurer) {
|
||||
configurer
|
||||
.setUseCaseSensitiveMatch(true)
|
||||
.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController.class));
|
||||
configurer.addPathPrefix(
|
||||
"/api", HandlerTypePredicate.forAnnotation(RestController.class));
|
||||
}
|
||||
}
|
||||
----
|
||||
@@ -717,9 +716,8 @@ Kotlin::
|
||||
|
||||
@Override
|
||||
fun configurePathMatch(configurer: PathMatchConfigurer) {
|
||||
configurer
|
||||
.setUseCaseSensitiveMatch(true)
|
||||
.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController::class.java))
|
||||
configurer.addPathPrefix(
|
||||
"/api", HandlerTypePredicate.forAnnotation(RestController::class.java))
|
||||
}
|
||||
}
|
||||
----
|
||||
@@ -740,6 +738,59 @@ reliance on it.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-config-blocking-execution]]
|
||||
== Blocking Execution
|
||||
|
||||
The WebFlux Java config allows you to customize blocking execution in WebFlux.
|
||||
|
||||
You can have blocking controller methods called on a separate thread by providing
|
||||
an `Executor` such as the
|
||||
{api-spring-framework}/core/task/VirtualThreadTaskExecutor.html[`VirtualThreadTaskExecutor`]
|
||||
as follows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
public class WebConfig implements WebFluxConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureBlockingExecution(BlockingExecutionConfigurer configurer) {
|
||||
Executor executor = ...
|
||||
configurer.setExecutor(executor);
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
class WebConfig : WebFluxConfigurer {
|
||||
|
||||
@Override
|
||||
fun configureBlockingExecution(configurer: BlockingExecutionConfigurer) {
|
||||
val executor = ...
|
||||
configurer.setExecutor(executor)
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
By default, controller methods whose return type is not recognized by the configured
|
||||
`ReactiveAdapterRegistry` are considered blocking, but you can set a custom controller
|
||||
method predicate via `BlockingExecutionConfigurer`.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-config-websocket-service]]
|
||||
== WebSocketService
|
||||
|
||||
|
||||
+8
-5
@@ -160,10 +160,13 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
Note that use of `@ModelAttribute` is optional -- for example, to set its attributes.
|
||||
By default, any argument that is not a simple value type (as determined by
|
||||
{api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty])
|
||||
and is not resolved by any other argument resolver is treated as if it were annotated
|
||||
with `@ModelAttribute`.
|
||||
If method validation applies because other parameters have `@Constraint` annotations,
|
||||
then `HandlerMethodValidationException` would be raised instead. See the section on
|
||||
controller method xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation].
|
||||
|
||||
TIP: Using `@ModelAttribute` is optional. By default, any argument that is not a simple
|
||||
value type as determined by
|
||||
{api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]
|
||||
_AND_ that is not resolved by any other argument resolver is treated as an `@ModelAttribute`.
|
||||
|
||||
|
||||
|
||||
+4
-2
@@ -176,6 +176,10 @@ Kotlin::
|
||||
======
|
||||
--
|
||||
|
||||
If method validation applies because other parameters have `@Constraint` annotations,
|
||||
then `HandlerMethodValidationException` is raised instead. See the section on
|
||||
xref:web/webflux/controller/ann-validation.adoc[Validation].
|
||||
|
||||
To access all multipart data as a `MultiValueMap`, you can use `@RequestBody`,
|
||||
as the following example shows:
|
||||
|
||||
@@ -306,5 +310,3 @@ file upload.
|
||||
|
||||
Received part events can also be relayed to another service by using the `WebClient`.
|
||||
See xref:web/webflux-webclient/client-body.adoc#webflux-client-body-multipart[Multipart Data].
|
||||
|
||||
|
||||
|
||||
@@ -89,4 +89,33 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
You can also declare an `Errors` parameter for access to validation errors, but in
|
||||
that case the request body must not be a `Mono`, and will be resolved first:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@PostMapping("/accounts")
|
||||
public void handle(@Valid @RequestBody Account account, Errors errors) {
|
||||
// use one of the onError* operators...
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@PostMapping("/accounts")
|
||||
fun handle(@Valid @RequestBody account: Mono<Account>) {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
If method validation applies because other parameters have `@Constraint` annotations,
|
||||
then `HandlerMethodValidationException` is raised instead. For more details, see the
|
||||
section on xref:web/webflux/controller/ann-validation.adoc[Validation].
|
||||
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
[[mvc-ann-validation]]
|
||||
= Validation
|
||||
|
||||
[.small]#xref:web/webmvc/mvc-controller/ann-validation.adoc[See equivalent in the Servlet stack]#
|
||||
|
||||
Spring WebFlux has built-in xref:core/validation/validator.adoc[Validation] support for
|
||||
`@RequestMapping` methods, including the option to use
|
||||
xref:core/validation/beanvalidation.adoc[Java Bean Validation].
|
||||
The validation support works on two levels.
|
||||
|
||||
First, method parameters such as
|
||||
xref:web/webflux/controller/ann-methods/modelattrib-method-args.adoc[@ModelAttribute],
|
||||
xref:web/webflux/controller/ann-methods/requestbody.adoc[@RequestBody], and
|
||||
xref:web/webflux/controller/ann-methods/multipart-forms.adoc[@RequestPart] do perform
|
||||
validation if annotated with Jakarta's `@Valid` or Spring's `@Validated` annotation, and
|
||||
raise `MethodArgumentNotValidException` in case of validation errors. If you want to handle
|
||||
the errors in the controller method instead, you can declare an `Errors` or `BindingResult`
|
||||
method parameter immediately after the validated parameter.
|
||||
|
||||
Second, if https://beanvalidation.org/[Java Bean Validation] is present _AND_ other method
|
||||
parameters, e.g. `@RequestHeader`, `@RequestParam`, `@PathVariable` have `@Constraint`
|
||||
annotations, then method validation is applied to all method arguments, raising
|
||||
`HandlerMethodValidationException` in case of validation errors. You can still declare an
|
||||
`Errors` or `BindingResult` after an `@Valid` method parameter, and handle validation
|
||||
errors within the controller method, as long as there are no validation errors on other
|
||||
method arguments.
|
||||
|
||||
You can configure a `Validator` globally through the
|
||||
xref:web/webflux/config.adoc#webflux-config-validation[WebMvc config], or locally
|
||||
through an xref:web/webflux/controller/ann-initbinder.adoc[@InitBinder] method in an
|
||||
`@Controller` or `@ControllerAdvice`. You can also use multiple validators.
|
||||
|
||||
NOTE: If a controller has a class level `@Validated`, then
|
||||
xref:core/validation/beanvalidation.adoc#validation-beanvalidation-spring-method[method validation is applied]
|
||||
through an AOP proxy. In order to take advantage of the Spring MVC built-in support for
|
||||
method validation added in Spring Framework 6.1, you need to remove the class level
|
||||
`@Validated` annotation from the controller.
|
||||
|
||||
The xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses] section provides further
|
||||
details on how `MethodArgumentNotValidException` and `HandlerMethodValidationException`
|
||||
are handled, and also how their rendering can be customized through a `MessageSource` and
|
||||
locale and language specific resource bundles.
|
||||
|
||||
For further custom handling of method validation errors, you can extend
|
||||
`ResponseEntityExceptionHandler` or use an `@ExceptionHandler` method in a controller
|
||||
or in a `@ControllerAdvice`, and handle `HandlerMethodValidationException` directly.
|
||||
The exception contains a list of``ParameterValidationResult``s that group validation errors
|
||||
by method parameter. You can either iterate over those, or provide a visitor with callback
|
||||
methods by controller method parameter type:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
HandlerMethodValidationException ex = ... ;
|
||||
|
||||
ex.visitResults(new HandlerMethodValidationException.Visitor() {
|
||||
|
||||
@Override
|
||||
public void requestHeader(RequestHeader requestHeader, ParameterValidationResult result) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestParam(@Nullable RequestParam requestParam, ParameterValidationResult result) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@Override
|
||||
public void modelAttribute(@Nullable ModelAttribute modelAttribute, ParameterErrors errors) {
|
||||
|
||||
// ...
|
||||
|
||||
@Override
|
||||
public void other(ParameterValidationResult result) {
|
||||
// ...
|
||||
}
|
||||
});
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
// HandlerMethodValidationException
|
||||
val ex
|
||||
|
||||
ex.visitResults(object : HandlerMethodValidationException.Visitor {
|
||||
|
||||
override fun requestHeader(requestHeader: RequestHeader, result: ParameterValidationResult) {
|
||||
// ...
|
||||
}
|
||||
|
||||
override fun requestParam(requestParam: RequestParam?, result: ParameterValidationResult) {
|
||||
// ...
|
||||
}
|
||||
|
||||
override fun modelAttribute(modelAttribute: ModelAttribute?, errors: ParameterErrors) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
override fun other(result: ParameterValidationResult) {
|
||||
// ...
|
||||
}
|
||||
})
|
||||
----
|
||||
======
|
||||
@@ -6,6 +6,26 @@ This section describes options for client-side access to REST endpoints.
|
||||
|
||||
|
||||
|
||||
[[webmvc-restclient]]
|
||||
== `RestClient`
|
||||
|
||||
`RestClient` is a synchronous HTTP client that exposes a modern, fluent API.
|
||||
|
||||
See xref:integration/rest-clients.adoc#rest-restclient[`RestClient`] for more details.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webmvc-webclient]]
|
||||
== `WebClient`
|
||||
|
||||
`WebClient` is a reactive client to perform HTTP requests with a fluent API.
|
||||
|
||||
See xref:web/webflux-webclient.adoc[WebClient] for more details.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webmvc-resttemplate]]
|
||||
== `RestTemplate`
|
||||
|
||||
@@ -13,37 +33,8 @@ This section describes options for client-side access to REST endpoints.
|
||||
Spring REST client and exposes a simple, template-method API over underlying HTTP client
|
||||
libraries.
|
||||
|
||||
NOTE: As of 5.0 the `RestTemplate` is in maintenance mode, with only requests for minor
|
||||
changes and bugs to be accepted. Please, consider using the
|
||||
xref:web/webflux-webclient.adoc[WebClient] which offers a more modern API and
|
||||
supports sync, async, and streaming scenarios.
|
||||
|
||||
See xref:integration/rest-clients.adoc[REST Endpoints] for details.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webmvc-webclient]]
|
||||
== `WebClient`
|
||||
|
||||
`WebClient` is a non-blocking, reactive client to perform HTTP requests. It was
|
||||
introduced in 5.0 and offers a modern alternative to the `RestTemplate`, with efficient
|
||||
support for both synchronous and asynchronous, as well as streaming scenarios.
|
||||
|
||||
In contrast to `RestTemplate`, `WebClient` supports the following:
|
||||
|
||||
* Non-blocking I/O.
|
||||
* Reactive Streams back pressure.
|
||||
* High concurrency with fewer hardware resources.
|
||||
* Functional-style, fluent API that takes advantage of Java 8 lambdas.
|
||||
* Synchronous and asynchronous interactions.
|
||||
* Streaming up to or streaming down from a server.
|
||||
|
||||
See xref:web/webflux-webclient.adoc[WebClient] for more details.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webmvc-http-interface]]
|
||||
== HTTP Interface
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ Kotlin::
|
||||
======
|
||||
|
||||
The return value can then be obtained by running the given task through the
|
||||
xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-configuration-spring-mvc[configured] `TaskExecutor`.
|
||||
xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-configuration-spring-mvc[configured] `AsyncTaskExecutor`.
|
||||
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ Here is a very concise overview of Servlet asynchronous request processing:
|
||||
|
||||
* The controller returns a `Callable`.
|
||||
* Spring MVC calls `request.startAsync()` and submits the `Callable` to
|
||||
a `TaskExecutor` for processing in a separate thread.
|
||||
an `AsyncTaskExecutor` for processing in a separate thread.
|
||||
* Meanwhile, the `DispatcherServlet` and all filters exit the Servlet container thread,
|
||||
but the response remains open.
|
||||
* Eventually the `Callable` produces a result, and Spring MVC dispatches the request back
|
||||
@@ -404,11 +404,10 @@ TIP: Spring MVC supports Reactor and RxJava through the
|
||||
|
||||
For streaming to the response, reactive back pressure is supported, but writes to the
|
||||
response are still blocking and are run on a separate thread through the
|
||||
xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-configuration-spring-mvc[configured] `TaskExecutor`, to avoid
|
||||
blocking the upstream source (such as a `Flux` returned from `WebClient`).
|
||||
By default, `SimpleAsyncTaskExecutor` is used for the blocking writes, but that is not
|
||||
suitable under load. If you plan to stream with a reactive type, you should use the
|
||||
xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-configuration-spring-mvc[MVC configuration] to configure a task executor.
|
||||
xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-configuration-spring-mvc[configured]
|
||||
`AsyncTaskExecutor`, to avoid blocking the upstream source such as a `Flux` returned
|
||||
from `WebClient`.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -494,7 +493,7 @@ In `web.xml` configuration, you can add `<async-supported>true</async-supported>
|
||||
[[mvc-ann-async-configuration-spring-mvc]]
|
||||
=== Spring MVC
|
||||
|
||||
The MVC configuration exposes the following options related to asynchronous request processing:
|
||||
The MVC configuration exposes the following options for asynchronous request processing:
|
||||
|
||||
* Java configuration: Use the `configureAsyncSupport` callback on `WebMvcConfigurer`.
|
||||
* XML namespace: Use the `<async-support>` element under `<mvc:annotation-driven>`.
|
||||
@@ -504,10 +503,9 @@ You can configure the following:
|
||||
* Default timeout value for async requests, which if not set, depends
|
||||
on the underlying Servlet container.
|
||||
* `AsyncTaskExecutor` to use for blocking writes when streaming with
|
||||
xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-reactive-types[Reactive Types] and for executing `Callable` instances returned from
|
||||
controller methods. We highly recommended configuring this property if you
|
||||
stream with reactive types or have controller methods that return `Callable`, since
|
||||
by default, it is a `SimpleAsyncTaskExecutor`.
|
||||
xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-reactive-types[Reactive Types] and for
|
||||
executing `Callable` instances returned from controller methods.
|
||||
The one used by default is not suitable for production under load.
|
||||
* `DeferredResultProcessingInterceptor` implementations and `CallableProcessingInterceptor` implementations.
|
||||
|
||||
Note that you can also set the default timeout value on a `DeferredResult`,
|
||||
|
||||
@@ -67,24 +67,25 @@ from an existing `ProblemDetail`. This could be done centrally, e.g. from an
|
||||
|
||||
|
||||
[[mvc-ann-rest-exceptions-i18n]]
|
||||
== Internationalization
|
||||
== Customization and i18n
|
||||
[.small]#xref:web/webflux/ann-rest-exceptions.adoc#webflux-ann-rest-exceptions-i18n[See equivalent in the Reactive stack]#
|
||||
|
||||
It is a common requirement to internationalize error response details, and good practice
|
||||
to customize the problem details for Spring MVC exceptions. This is supported as follows:
|
||||
It is a common requirement to customize and internationalize error response details.
|
||||
It is also good practice to customize the problem details for Spring MVC exceptions
|
||||
to avoid revealing implementation details. This section describes the support for that.
|
||||
|
||||
- Each `ErrorResponse` exposes a message code and arguments to resolve the "detail" field
|
||||
through a xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource].
|
||||
The actual message code value is parameterized with placeholders, e.g.
|
||||
`+"HTTP method {0} not supported"+` to be expanded from the arguments.
|
||||
- Each `ErrorResponse` also exposes a message code to resolve the "title" field.
|
||||
- `ResponseEntityExceptionHandler` uses the message code and arguments to resolve the
|
||||
"detail" and the "title" fields.
|
||||
An `ErrorResponse` exposes message codes for "type", "title", and "detail", as well as
|
||||
message code arguments for the "detail" field. `ResponseEntityExceptionHandler` resolves
|
||||
these through a xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource]
|
||||
and updates the corresponding `ProblemDetail` fields accordingly.
|
||||
|
||||
By default, the message code for the "detail" field is "problemDetail." + the fully
|
||||
qualified exception class name. Some exceptions may expose additional message codes in
|
||||
which case a suffix is added to the default message code. The table below lists message
|
||||
arguments and codes for Spring MVC exceptions:
|
||||
The default strategy for message codes follows the pattern:
|
||||
|
||||
`problemDetail.[type|title|detail].[fully qualified exception class name]`
|
||||
|
||||
An `ErrorResponse` may expose more than one message code, typically adding a suffix
|
||||
to the default message code. The table below lists message codes, and arguments for
|
||||
Spring MVC exceptions:
|
||||
|
||||
[[mvc-ann-rest-exceptions-codes]]
|
||||
[cols="1,1,2", options="header"]
|
||||
@@ -99,6 +100,11 @@ arguments and codes for Spring MVC exceptions:
|
||||
| (default)
|
||||
| `+{0}+` property name, `+{1}+` property value
|
||||
|
||||
| `HandlerMethodValidationException`
|
||||
| (default)
|
||||
| `+{0}+` list all validation errors.
|
||||
Message codes and arguments for each error are also resolved via `MessageSource`.
|
||||
|
||||
| `HttpMediaTypeNotAcceptableException`
|
||||
| (default)
|
||||
| `+{0}+` list of supported media types
|
||||
@@ -130,8 +136,7 @@ arguments and codes for Spring MVC exceptions:
|
||||
| `MethodArgumentNotValidException`
|
||||
| (default)
|
||||
| `+{0}+` the list of global errors, `+{1}+` the list of field errors.
|
||||
Message codes and arguments for each error within the `BindingResult` are also resolved
|
||||
via `MessageSource`.
|
||||
Message codes and arguments for each error are also resolvedvia `MessageSource`.
|
||||
|
||||
| `MissingRequestHeaderException`
|
||||
| (default)
|
||||
@@ -161,6 +166,10 @@ arguments and codes for Spring MVC exceptions:
|
||||
| (default)
|
||||
|
|
||||
|
||||
| `NoResourceFoundException`
|
||||
| (default)
|
||||
|
|
||||
|
||||
| `TypeMismatchException`
|
||||
| (default)
|
||||
| `+{0}+` property name, `+{1}+` property value
|
||||
@@ -171,8 +180,13 @@ arguments and codes for Spring MVC exceptions:
|
||||
|
||||
|===
|
||||
|
||||
By default, the message code for the "title" field is "problemDetail.title." + the fully
|
||||
qualified exception class name.
|
||||
NOTE: Unlike other exceptions, the message arguments for
|
||||
`MethodArgumentValidException` and `HandlerMethodValidationException` are baed on a list of
|
||||
`MessageSourceResolvable` errors that can also be customized through a
|
||||
xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource]
|
||||
resource bundle. See
|
||||
xref:core/validation/beanvalidation.adoc#validation-beanvalidation-spring-method-i18n[Customizing Validation Errors]
|
||||
for more details.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
By default, if xref:core/validation/beanvalidation.adoc#validation-beanvalidation-overview[Bean Validation] is present
|
||||
on the classpath (for example, Hibernate Validator), the `LocalValidatorFactoryBean` is
|
||||
registered as a global xref:core/validation/validator.adoc[Validator] for use with `@Valid` and
|
||||
`Validated` on controller method arguments.
|
||||
`@Validated` on controller method arguments.
|
||||
|
||||
In Java configuration, you can customize the global `Validator` instance, as the
|
||||
following example shows:
|
||||
|
||||
+12
-7
@@ -144,7 +144,7 @@ Java::
|
||||
}
|
||||
|
||||
@PostMapping("update")
|
||||
public String update(@Valid AccountForm form, BindingResult result,
|
||||
public String update(AccountForm form, BindingResult result,
|
||||
@ModelAttribute(binding=false) Account account) { // <1>
|
||||
// ...
|
||||
}
|
||||
@@ -166,7 +166,7 @@ Kotlin::
|
||||
}
|
||||
|
||||
@PostMapping("update")
|
||||
fun update(@Valid form: AccountForm, result: BindingResult,
|
||||
fun update(form: AccountForm, result: BindingResult,
|
||||
@ModelAttribute(binding = false) account: Account): String { // <1>
|
||||
// ...
|
||||
}
|
||||
@@ -210,10 +210,15 @@ Kotlin::
|
||||
<1> Validate the `Pet` instance.
|
||||
======
|
||||
|
||||
Note that using `@ModelAttribute` is optional (for example, to set its attributes).
|
||||
By default, any argument that is not a simple value type (as determined by
|
||||
{api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty])
|
||||
and is not resolved by any other argument resolver is treated as if it were annotated
|
||||
with `@ModelAttribute`.
|
||||
If an `@ModelAttribute` is declared without `BindingResult` parameter after it, then
|
||||
`MethodArgumentNotValueException` is raised. However, if method validation applies because
|
||||
other parameters have `@Constraint` annotations, then `HandlerMethodValidationException`
|
||||
is raised instead. For more details, see the section on
|
||||
xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation].
|
||||
|
||||
TIP: Using `@ModelAttribute` is optional. By default, any parameter that is not a simple
|
||||
value type as determined by
|
||||
{api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]
|
||||
_AND_ that is not resolved by any other argument resolver is treated as an `@ModelAttribute`.
|
||||
|
||||
|
||||
|
||||
+5
-4
@@ -188,8 +188,7 @@ Java::
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@PostMapping("/")
|
||||
public String handle(@Valid @RequestPart("meta-data") MetaData metadata,
|
||||
BindingResult result) {
|
||||
public String handle(@Valid @RequestPart("meta-data") MetaData metadata, Errors errors) {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
@@ -199,12 +198,14 @@ Kotlin::
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@PostMapping("/")
|
||||
fun handle(@Valid @RequestPart("meta-data") metadata: MetaData,
|
||||
result: BindingResult): String {
|
||||
fun handle(@Valid @RequestPart("meta-data") metadata: MetaData, errors: Errors): String {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
If method validation applies because other parameters have `@Constraint` annotations,
|
||||
then `HandlerMethodValidationException` is raised instead. For more details, see the
|
||||
section on xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation].
|
||||
|
||||
|
||||
|
||||
+5
-2
@@ -48,7 +48,7 @@ Java::
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@PostMapping("/accounts")
|
||||
public void handle(@Valid @RequestBody Account account, BindingResult result) {
|
||||
public void handle(@Valid @RequestBody Account account, Errors errors) {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
@@ -58,10 +58,13 @@ Kotlin::
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@PostMapping("/accounts")
|
||||
fun handle(@Valid @RequestBody account: Account, result: BindingResult) {
|
||||
fun handle(@Valid @RequestBody account: Account, errors: Errors) {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
If method validation applies because other parameters have `@Constraint` annotations,
|
||||
then `HandlerMethodValidationException` is raised instead. For more details, see the
|
||||
section on xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation].
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
[[mvc-ann-validation]]
|
||||
= Validation
|
||||
|
||||
[.small]#xref:web/webflux/controller/ann-validation.adoc[See equivalent in the Reactive stack]#
|
||||
|
||||
Spring MVC has built-in xref:core/validation/validator.adoc[Validation] support for
|
||||
`@RequestMapping` methods, including the option to use
|
||||
xref:core/validation/beanvalidation.adoc[Java Bean Validation].
|
||||
The validation support works on two levels.
|
||||
|
||||
First, method parameters such as
|
||||
xref:web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc[@ModelAttribute],
|
||||
xref:web/webmvc/mvc-controller/ann-methods/requestbody.adoc[@RequestBody], and
|
||||
xref:web/webmvc/mvc-controller/ann-methods/multipart-forms.adoc[@RequestPart] do perform
|
||||
validation if annotated with Jakarta's `@Valid` or Spring's `@Validated` annotation, and
|
||||
raise `MethodArgumentNotValidException` in case of validation errors. If you want to handle
|
||||
the errors in the controller method instead, you can declare an `Errors` or `BindingResult`
|
||||
method parameter immediately after the validated parameter.
|
||||
|
||||
Second, if https://beanvalidation.org/[Java Bean Validation] is present _AND_ other method
|
||||
parameters, e.g. `@RequestHeader`, `@RequestParam`, `@PathVariable` have `@Constraint`
|
||||
annotations, then method validation is applied to all method arguments, raising
|
||||
`HandlerMethodValidationException` in case of validation errors. You can still declare an
|
||||
`Errors` or `BindingResult` after an `@Valid` method parameter, and handle validation
|
||||
errors within the controller method, as long as there are no validation errors on other
|
||||
method arguments. Method validation is also applied to the return value if the method
|
||||
is annotated with `@Valid` or has other `@Constraint` annotations.
|
||||
|
||||
You can configure a `Validator` globally through the
|
||||
xref:web/webmvc/mvc-config/validation.adoc[WebMvc config], or locally through an
|
||||
xref:web/webmvc/mvc-controller/ann-initbinder.adoc[@InitBinder] method in an
|
||||
`@Controller` or `@ControllerAdvice`. You can also use multiple validators.
|
||||
|
||||
NOTE: If a controller has a class level `@Validated`, then
|
||||
xref:core/validation/beanvalidation.adoc#validation-beanvalidation-spring-method[method validation is applied]
|
||||
through an AOP proxy. In order to take advantage of the Spring MVC built-in support for
|
||||
method validation added in Spring Framework 6.1, you need to remove the class level
|
||||
`@Validated` annotation from the controller.
|
||||
|
||||
The xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses] section provides further
|
||||
details on how `MethodArgumentNotValidException` and `HandlerMethodValidationException`
|
||||
are handled, and also how their rendering can be customized through a `MessageSource` and
|
||||
locale and language specific resource bundles.
|
||||
|
||||
For further custom handling of method validation errors, you can extend
|
||||
`ResponseEntityExceptionHandler` or use an `@ExceptionHandler` method in a controller
|
||||
or in a `@ControllerAdvice`, and handle `HandlerMethodValidationException` directly.
|
||||
The exception contains a list of``ParameterValidationResult``s that group validation errors
|
||||
by method parameter. You can either iterate over those, or provide a visitor with callback
|
||||
methods by controller method parameter type:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
HandlerMethodValidationException ex = ... ;
|
||||
|
||||
ex.visitResults(new HandlerMethodValidationException.Visitor() {
|
||||
|
||||
@Override
|
||||
public void requestHeader(RequestHeader requestHeader, ParameterValidationResult result) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestParam(@Nullable RequestParam requestParam, ParameterValidationResult result) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@Override
|
||||
public void modelAttribute(@Nullable ModelAttribute modelAttribute, ParameterErrors errors) {
|
||||
|
||||
// ...
|
||||
|
||||
@Override
|
||||
public void other(ParameterValidationResult result) {
|
||||
// ...
|
||||
}
|
||||
});
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
// HandlerMethodValidationException
|
||||
val ex
|
||||
|
||||
ex.visitResults(object : HandlerMethodValidationException.Visitor {
|
||||
|
||||
override fun requestHeader(requestHeader: RequestHeader, result: ParameterValidationResult) {
|
||||
// ...
|
||||
}
|
||||
|
||||
override fun requestParam(requestParam: RequestParam?, result: ParameterValidationResult) {
|
||||
// ...
|
||||
}
|
||||
|
||||
override fun modelAttribute(modelAttribute: ModelAttribute?, errors: ParameterErrors) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
override fun other(result: ParameterValidationResult) {
|
||||
// ...
|
||||
}
|
||||
})
|
||||
----
|
||||
======
|
||||
@@ -62,8 +62,7 @@ initialization parameters (`init-param` elements) to the Servlet declaration in
|
||||
The exception can then be caught with a `HandlerExceptionResolver` (for example, by using an
|
||||
`@ExceptionHandler` controller method) and handled as any others.
|
||||
|
||||
By default, this is set to `false`, in which case the `DispatcherServlet` sets the
|
||||
response status to 404 (NOT_FOUND) without raising an exception.
|
||||
As of 6.1, this property is set to `true` and deprecated.
|
||||
|
||||
Note that, if xref:web/webmvc/mvc-config/default-servlet-handler.adoc[default servlet handling] is
|
||||
also configured, unresolved requests are always forwarded to the default servlet
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.integration.observability.httpserver.reactive;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class HttpHandlerConfiguration {
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
public HttpHandlerConfiguration(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public HttpHandler httpHandler(ObservationRegistry registry) {
|
||||
return WebHttpHandlerBuilder.applicationContext(this.applicationContext)
|
||||
.observationRegistry(registry)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -17,9 +17,9 @@
|
||||
package org.springframework.docs.integration.observability.httpserver.reactive;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.server.reactive.observation.ServerRequestObservationContext;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.filter.reactive.ServerHttpObservationFilter;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
@Controller
|
||||
@@ -28,7 +28,7 @@ public class UserController {
|
||||
@ExceptionHandler(MissingUserException.class)
|
||||
ResponseEntity<Void> handleMissingUser(ServerWebExchange exchange, MissingUserException exception) {
|
||||
// We want to record this exception with the observation
|
||||
ServerHttpObservationFilter.findObservationContext(exchange)
|
||||
ServerRequestObservationContext.findCurrent(exchange.getAttributes())
|
||||
.ifPresent(context -> context.setError(exception));
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.integration.observability.tasksscheduled;
|
||||
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
|
||||
import org.springframework.scheduling.annotation.SchedulingConfigurer;
|
||||
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
|
||||
|
||||
public class ObservationSchedulingConfigurer implements SchedulingConfigurer {
|
||||
|
||||
private final ObservationRegistry observationRegistry;
|
||||
|
||||
public ObservationSchedulingConfigurer(ObservationRegistry observationRegistry) {
|
||||
this.observationRegistry = observationRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
|
||||
taskRegistrar.setObservationRegistry(this.observationRegistry);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,17 +7,17 @@ javaPlatform {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.14.3"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.10.9"))
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.15.2"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.12.0-M1"))
|
||||
api(platform("io.netty:netty-bom:4.1.94.Final"))
|
||||
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
|
||||
api(platform("io.projectreactor:reactor-bom:2022.0.9"))
|
||||
api(platform("io.projectreactor:reactor-bom:2023.0.0-M1"))
|
||||
api(platform("io.rsocket:rsocket-bom:1.1.3"))
|
||||
api(platform("org.apache.groovy:groovy-bom:4.0.13"))
|
||||
api(platform("org.apache.logging.log4j:log4j-bom:2.20.0"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:11.0.15"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.4.0"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.2"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.5.1"))
|
||||
api(platform("org.junit:junit-bom:5.9.3"))
|
||||
api(platform("org.mockito:mockito-bom:5.4.0"))
|
||||
|
||||
@@ -46,7 +46,7 @@ dependencies {
|
||||
api("commons-io:commons-io:2.11.0")
|
||||
api("de.bechte.junit:junit-hierarchicalcontextrunner:4.12.2")
|
||||
api("info.picocli:picocli:4.7.4")
|
||||
api("io.micrometer:context-propagation:1.0.4")
|
||||
api("io.micrometer:context-propagation:1.1.0-M3")
|
||||
api("io.mockk:mockk:1.13.4")
|
||||
api("io.projectreactor.netty:reactor-netty5-http:2.0.0-M3")
|
||||
api("io.projectreactor.tools:blockhound:1.0.8.RELEASE")
|
||||
@@ -111,6 +111,7 @@ dependencies {
|
||||
api("org.awaitility:awaitility:4.2.0")
|
||||
api("org.bouncycastle:bcpkix-jdk18on:1.72")
|
||||
api("org.codehaus.jettison:jettison:1.5.4")
|
||||
api("org.crac:crac:1.3.0")
|
||||
api("org.dom4j:dom4j:2.1.4")
|
||||
api("org.eclipse.jetty:jetty-reactive-httpclient:3.0.8")
|
||||
api("org.eclipse.persistence:org.eclipse.persistence.jpa:3.0.3")
|
||||
@@ -144,6 +145,6 @@ dependencies {
|
||||
api("org.webjars:webjars-locator-core:0.53")
|
||||
api("org.xmlunit:xmlunit-assertj:2.9.1")
|
||||
api("org.xmlunit:xmlunit-matchers:2.9.1")
|
||||
api("org.yaml:snakeyaml:1.33")
|
||||
api("org.yaml:snakeyaml:2.0")
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -1,9 +1,10 @@
|
||||
version=6.0.12-SNAPSHOT
|
||||
version=6.1.0-M3
|
||||
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
org.gradle.parallel=true
|
||||
|
||||
kotlinVersion=1.7.21
|
||||
kotlinVersion=1.9.0
|
||||
|
||||
kotlin.jvm.target.validation.mode=ignore
|
||||
kotlin.stdlib.default.dependency=false
|
||||
|
||||
+14
-65
@@ -2,16 +2,14 @@
|
||||
* Apply the JVM Toolchain conventions
|
||||
* See https://docs.gradle.org/current/userguide/toolchains.html
|
||||
*
|
||||
* One can choose the toolchain to use for compiling the MAIN sources and/or compiling
|
||||
* and running the TEST sources. These options apply to Java, Kotlin and Groovy sources
|
||||
* when available.
|
||||
* {@code "./gradlew check -PmainToolchain=17 -PtestToolchain=20"} will use:
|
||||
* <ul>
|
||||
* <li>a JDK17 toolchain for compiling the main SourceSet
|
||||
* <li>a JDK20 toolchain for compiling and running the test SourceSet
|
||||
* </ul>
|
||||
* One can choose the toolchain to use for compiling and running the TEST sources.
|
||||
* These options apply to Java, Kotlin and Groovy test sources when available.
|
||||
* {@code "./gradlew check -PtestToolchain=22"} will use a JDK22
|
||||
* toolchain for compiling and running the test SourceSet.
|
||||
*
|
||||
* By default, the build will fall back to using the current JDK and 17 language level for all sourceSets.
|
||||
* By default, the main build will fall back to using the a JDK 17
|
||||
* toolchain (and 17 language level) for all main sourceSets.
|
||||
* See {@link org.springframework.build.JavaConventions}.
|
||||
*
|
||||
* Gradle will automatically detect JDK distributions in well-known locations.
|
||||
* The following command will list the detected JDKs on the host.
|
||||
@@ -23,52 +21,27 @@
|
||||
* {@code
|
||||
* $ echo JDK17
|
||||
* /opt/openjdk/java17
|
||||
* $ echo JDK20
|
||||
* /opt/openjdk/java20
|
||||
* $ ./gradlew -Porg.gradle.java.installations.fromEnv=JDK17,JDK20 check
|
||||
* $ echo JDK22
|
||||
* /opt/openjdk/java22
|
||||
* $ ./gradlew -Porg.gradle.java.installations.fromEnv=JDK17,JDK22 check
|
||||
* }
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
|
||||
def mainToolchainConfigured() {
|
||||
return project.hasProperty('mainToolchain') && project.mainToolchain
|
||||
}
|
||||
|
||||
def testToolchainConfigured() {
|
||||
return project.hasProperty('testToolchain') && project.testToolchain
|
||||
}
|
||||
|
||||
def mainToolchainLanguageVersion() {
|
||||
if (mainToolchainConfigured()) {
|
||||
return JavaLanguageVersion.of(project.mainToolchain.toString())
|
||||
}
|
||||
return JavaLanguageVersion.of(17)
|
||||
}
|
||||
|
||||
def testToolchainLanguageVersion() {
|
||||
if (testToolchainConfigured()) {
|
||||
return JavaLanguageVersion.of(project.testToolchain.toString())
|
||||
}
|
||||
return mainToolchainLanguageVersion()
|
||||
return JavaLanguageVersion.of(17)
|
||||
}
|
||||
|
||||
plugins.withType(JavaPlugin) {
|
||||
// Configure the Java Toolchain if the 'mainToolchain' is configured
|
||||
if (mainToolchainConfigured()) {
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = mainToolchainLanguageVersion()
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Fallback to JDK17
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
plugins.withType(JavaPlugin).configureEach {
|
||||
// Configure a specific Java Toolchain for compiling and running tests if the 'testToolchain' property is defined
|
||||
if (testToolchainConfigured()) {
|
||||
def testLanguageVersion = testToolchainLanguageVersion()
|
||||
@@ -81,37 +54,13 @@ plugins.withType(JavaPlugin) {
|
||||
javaLauncher = javaToolchains.launcherFor {
|
||||
languageVersion = testLanguageVersion
|
||||
}
|
||||
jvmArgs += ['-Djava.locale.providers=COMPAT']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plugins.withType(GroovyPlugin) {
|
||||
// Fallback to JDK17
|
||||
if (!mainToolchainConfigured()) {
|
||||
compileGroovy {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pluginManager.withPlugin("kotlin") {
|
||||
// Fallback to JDK17
|
||||
compileKotlin {
|
||||
kotlinOptions {
|
||||
jvmTarget = '17'
|
||||
}
|
||||
}
|
||||
compileTestKotlin {
|
||||
kotlinOptions {
|
||||
jvmTarget = '17'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Configure the JMH plugin to use the toolchain for generating and running JMH bytecode
|
||||
pluginManager.withPlugin("me.champeau.jmh") {
|
||||
if (mainToolchainConfigured() || testToolchainConfigured()) {
|
||||
if (testToolchainConfigured()) {
|
||||
tasks.matching { it.name.contains('jmh') && it.hasProperty('javaLauncher') }.configureEach {
|
||||
javaLauncher.set(javaToolchains.launcherFor {
|
||||
languageVersion.set(testToolchainLanguageVersion())
|
||||
@@ -131,7 +80,7 @@ rootProject.ext {
|
||||
resolvedTestToolchain = false
|
||||
}
|
||||
gradle.taskGraph.afterTask { Task task, TaskState state ->
|
||||
if (mainToolchainConfigured() && !resolvedMainToolchain && task instanceof JavaCompile && task.javaCompiler.isPresent()) {
|
||||
if (!resolvedMainToolchain && task instanceof JavaCompile && task.javaCompiler.isPresent()) {
|
||||
def metadata = task.javaCompiler.get().metadata
|
||||
task.project.buildScan.value('Main toolchain', "$metadata.vendor $metadata.languageVersion ($metadata.installationPath)")
|
||||
resolvedMainToolchain = true
|
||||
|
||||
+2
-2
@@ -25,8 +25,8 @@ import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* AspectJPointcutAdvisor that adapts an {@link AbstractAspectJAdvice}
|
||||
* to the {@link org.springframework.aop.PointcutAdvisor} interface.
|
||||
* {@code AspectJPointcutAdvisor} adapts an {@link AbstractAspectJAdvice} to the
|
||||
* {@link PointcutAdvisor} interface.
|
||||
*
|
||||
* @author Adrian Colyer
|
||||
* @author Juergen Hoeller
|
||||
|
||||
+39
-37
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -23,7 +23,6 @@ import java.util.regex.Pattern;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.apache.commons.logging.Log;
|
||||
|
||||
import org.springframework.core.Constants;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
@@ -62,6 +61,7 @@ import org.springframework.util.StringUtils;
|
||||
*
|
||||
* @author Rob Harrop
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 1.2
|
||||
* @see #setEnterMessage
|
||||
* @see #setExitMessage
|
||||
@@ -152,8 +152,15 @@ public class CustomizableTraceInterceptor extends AbstractTraceInterceptor {
|
||||
/**
|
||||
* The {@code Set} of allowed placeholders.
|
||||
*/
|
||||
private static final Set<Object> ALLOWED_PLACEHOLDERS =
|
||||
new Constants(CustomizableTraceInterceptor.class).getValues("PLACEHOLDER_");
|
||||
static final Set<String> ALLOWED_PLACEHOLDERS = Set.of(
|
||||
PLACEHOLDER_METHOD_NAME,
|
||||
PLACEHOLDER_TARGET_CLASS_NAME,
|
||||
PLACEHOLDER_TARGET_CLASS_SHORT_NAME,
|
||||
PLACEHOLDER_RETURN_VALUE,
|
||||
PLACEHOLDER_ARGUMENT_TYPES,
|
||||
PLACEHOLDER_ARGUMENTS,
|
||||
PLACEHOLDER_EXCEPTION,
|
||||
PLACEHOLDER_INVOCATION_TIME);
|
||||
|
||||
|
||||
/**
|
||||
@@ -295,43 +302,38 @@ public class CustomizableTraceInterceptor extends AbstractTraceInterceptor {
|
||||
protected String replacePlaceholders(String message, MethodInvocation methodInvocation,
|
||||
@Nullable Object returnValue, @Nullable Throwable throwable, long invocationTime) {
|
||||
|
||||
Matcher matcher = PATTERN.matcher(message);
|
||||
Object target = methodInvocation.getThis();
|
||||
Assert.state(target != null, "Target must not be null");
|
||||
|
||||
StringBuilder output = new StringBuilder();
|
||||
Matcher matcher = PATTERN.matcher(message);
|
||||
while (matcher.find()) {
|
||||
String match = matcher.group();
|
||||
if (PLACEHOLDER_METHOD_NAME.equals(match)) {
|
||||
matcher.appendReplacement(output, Matcher.quoteReplacement(methodInvocation.getMethod().getName()));
|
||||
}
|
||||
else if (PLACEHOLDER_TARGET_CLASS_NAME.equals(match)) {
|
||||
String className = getClassForLogging(target).getName();
|
||||
matcher.appendReplacement(output, Matcher.quoteReplacement(className));
|
||||
}
|
||||
else if (PLACEHOLDER_TARGET_CLASS_SHORT_NAME.equals(match)) {
|
||||
String shortName = ClassUtils.getShortName(getClassForLogging(target));
|
||||
matcher.appendReplacement(output, Matcher.quoteReplacement(shortName));
|
||||
}
|
||||
else if (PLACEHOLDER_ARGUMENTS.equals(match)) {
|
||||
matcher.appendReplacement(output,
|
||||
switch (match) {
|
||||
case PLACEHOLDER_METHOD_NAME -> matcher.appendReplacement(output,
|
||||
Matcher.quoteReplacement(methodInvocation.getMethod().getName()));
|
||||
case PLACEHOLDER_TARGET_CLASS_NAME -> {
|
||||
String className = getClassForLogging(target).getName();
|
||||
matcher.appendReplacement(output, Matcher.quoteReplacement(className));
|
||||
}
|
||||
case PLACEHOLDER_TARGET_CLASS_SHORT_NAME -> {
|
||||
String shortName = ClassUtils.getShortName(getClassForLogging(target));
|
||||
matcher.appendReplacement(output, Matcher.quoteReplacement(shortName));
|
||||
}
|
||||
case PLACEHOLDER_ARGUMENTS -> matcher.appendReplacement(output,
|
||||
Matcher.quoteReplacement(StringUtils.arrayToCommaDelimitedString(methodInvocation.getArguments())));
|
||||
}
|
||||
else if (PLACEHOLDER_ARGUMENT_TYPES.equals(match)) {
|
||||
appendArgumentTypes(methodInvocation, matcher, output);
|
||||
}
|
||||
else if (PLACEHOLDER_RETURN_VALUE.equals(match)) {
|
||||
appendReturnValue(methodInvocation, matcher, output, returnValue);
|
||||
}
|
||||
else if (throwable != null && PLACEHOLDER_EXCEPTION.equals(match)) {
|
||||
matcher.appendReplacement(output, Matcher.quoteReplacement(throwable.toString()));
|
||||
}
|
||||
else if (PLACEHOLDER_INVOCATION_TIME.equals(match)) {
|
||||
matcher.appendReplacement(output, Long.toString(invocationTime));
|
||||
}
|
||||
else {
|
||||
// Should not happen since placeholders are checked earlier.
|
||||
throw new IllegalArgumentException("Unknown placeholder [" + match + "]");
|
||||
case PLACEHOLDER_ARGUMENT_TYPES -> appendArgumentTypes(methodInvocation, matcher, output);
|
||||
case PLACEHOLDER_RETURN_VALUE -> appendReturnValue(methodInvocation, matcher, output, returnValue);
|
||||
case PLACEHOLDER_EXCEPTION -> {
|
||||
if (throwable != null) {
|
||||
matcher.appendReplacement(output, Matcher.quoteReplacement(throwable.toString()));
|
||||
}
|
||||
}
|
||||
case PLACEHOLDER_INVOCATION_TIME -> matcher.appendReplacement(output, Long.toString(invocationTime));
|
||||
default -> {
|
||||
// Should not happen since placeholders are checked earlier.
|
||||
throw new IllegalArgumentException("Unknown placeholder [" + match + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
matcher.appendTail(output);
|
||||
@@ -348,7 +350,7 @@ public class CustomizableTraceInterceptor extends AbstractTraceInterceptor {
|
||||
* @param output the {@code StringBuilder} to write output to
|
||||
* @param returnValue the value returned by the method invocation.
|
||||
*/
|
||||
private void appendReturnValue(
|
||||
private static void appendReturnValue(
|
||||
MethodInvocation methodInvocation, Matcher matcher, StringBuilder output, @Nullable Object returnValue) {
|
||||
|
||||
if (methodInvocation.getMethod().getReturnType() == void.class) {
|
||||
@@ -372,7 +374,7 @@ public class CustomizableTraceInterceptor extends AbstractTraceInterceptor {
|
||||
* @param matcher the {@code Matcher} containing the state of the output
|
||||
* @param output the {@code StringBuilder} containing the output
|
||||
*/
|
||||
private void appendArgumentTypes(MethodInvocation methodInvocation, Matcher matcher, StringBuilder output) {
|
||||
private static void appendArgumentTypes(MethodInvocation methodInvocation, Matcher matcher, StringBuilder output) {
|
||||
Class<?>[] argumentTypes = methodInvocation.getMethod().getParameterTypes();
|
||||
String[] argumentTypeShortNames = new String[argumentTypes.length];
|
||||
for (int i = 0; i < argumentTypeShortNames.length; i++) {
|
||||
@@ -387,7 +389,7 @@ public class CustomizableTraceInterceptor extends AbstractTraceInterceptor {
|
||||
* that are not specified as constants on this class and throws an
|
||||
* {@code IllegalArgumentException} if so.
|
||||
*/
|
||||
private void checkForInvalidPlaceholders(String message) throws IllegalArgumentException {
|
||||
private static void checkForInvalidPlaceholders(String message) throws IllegalArgumentException {
|
||||
Matcher matcher = PATTERN.matcher(message);
|
||||
while (matcher.find()) {
|
||||
String match = matcher.group();
|
||||
|
||||
+82
-41
@@ -16,10 +16,17 @@
|
||||
|
||||
package org.springframework.aop.interceptor;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
@@ -27,73 +34,80 @@ import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.springframework.aop.interceptor.CustomizableTraceInterceptor.ALLOWED_PLACEHOLDERS;
|
||||
import static org.springframework.aop.interceptor.CustomizableTraceInterceptor.PLACEHOLDER_ARGUMENTS;
|
||||
import static org.springframework.aop.interceptor.CustomizableTraceInterceptor.PLACEHOLDER_ARGUMENT_TYPES;
|
||||
import static org.springframework.aop.interceptor.CustomizableTraceInterceptor.PLACEHOLDER_EXCEPTION;
|
||||
import static org.springframework.aop.interceptor.CustomizableTraceInterceptor.PLACEHOLDER_INVOCATION_TIME;
|
||||
import static org.springframework.aop.interceptor.CustomizableTraceInterceptor.PLACEHOLDER_METHOD_NAME;
|
||||
import static org.springframework.aop.interceptor.CustomizableTraceInterceptor.PLACEHOLDER_RETURN_VALUE;
|
||||
import static org.springframework.aop.interceptor.CustomizableTraceInterceptor.PLACEHOLDER_TARGET_CLASS_NAME;
|
||||
import static org.springframework.aop.interceptor.CustomizableTraceInterceptor.PLACEHOLDER_TARGET_CLASS_SHORT_NAME;
|
||||
|
||||
/**
|
||||
* Tests for {@link CustomizableTraceInterceptor}.
|
||||
*
|
||||
* @author Rob Harrop
|
||||
* @author Rick Evans
|
||||
* @author Juergen Hoeller
|
||||
* @author Chris Beams
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
public class CustomizableTraceInterceptorTests {
|
||||
class CustomizableTraceInterceptorTests {
|
||||
|
||||
private final CustomizableTraceInterceptor interceptor = new CustomizableTraceInterceptor();
|
||||
|
||||
|
||||
@Test
|
||||
public void testSetEmptyEnterMessage() {
|
||||
void setEmptyEnterMessage() {
|
||||
// Must not be able to set empty enter message
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
new CustomizableTraceInterceptor().setEnterMessage(""));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> interceptor.setEnterMessage(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetEnterMessageWithReturnValuePlaceholder() {
|
||||
void setEnterMessageWithReturnValuePlaceholder() {
|
||||
// Must not be able to set enter message with return value placeholder
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
new CustomizableTraceInterceptor().setEnterMessage(CustomizableTraceInterceptor.PLACEHOLDER_RETURN_VALUE));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> interceptor.setEnterMessage(PLACEHOLDER_RETURN_VALUE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetEnterMessageWithExceptionPlaceholder() {
|
||||
void setEnterMessageWithExceptionPlaceholder() {
|
||||
// Must not be able to set enter message with exception placeholder
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
new CustomizableTraceInterceptor().setEnterMessage(CustomizableTraceInterceptor.PLACEHOLDER_EXCEPTION));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> interceptor.setEnterMessage(PLACEHOLDER_EXCEPTION));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetEnterMessageWithInvocationTimePlaceholder() {
|
||||
void setEnterMessageWithInvocationTimePlaceholder() {
|
||||
// Must not be able to set enter message with invocation time placeholder
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
new CustomizableTraceInterceptor().setEnterMessage(CustomizableTraceInterceptor.PLACEHOLDER_INVOCATION_TIME));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> interceptor.setEnterMessage(PLACEHOLDER_INVOCATION_TIME));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetEmptyExitMessage() {
|
||||
void setEmptyExitMessage() {
|
||||
// Must not be able to set empty exit message
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
new CustomizableTraceInterceptor().setExitMessage(""));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> interceptor.setExitMessage(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetExitMessageWithExceptionPlaceholder() {
|
||||
void setExitMessageWithExceptionPlaceholder() {
|
||||
// Must not be able to set exit message with exception placeholder
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
new CustomizableTraceInterceptor().setExitMessage(CustomizableTraceInterceptor.PLACEHOLDER_EXCEPTION));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> interceptor.setExitMessage(PLACEHOLDER_EXCEPTION));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetEmptyExceptionMessage() {
|
||||
void setEmptyExceptionMessage() {
|
||||
// Must not be able to set empty exception message
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
new CustomizableTraceInterceptor().setExceptionMessage(""));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> interceptor.setExceptionMessage(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetExceptionMethodWithReturnValuePlaceholder() {
|
||||
void setExceptionMethodWithReturnValuePlaceholder() {
|
||||
// Must not be able to set exception message with return value placeholder
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
new CustomizableTraceInterceptor().setExceptionMessage(CustomizableTraceInterceptor.PLACEHOLDER_RETURN_VALUE));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> interceptor.setExceptionMessage(PLACEHOLDER_RETURN_VALUE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSunnyDayPathLogsCorrectly() throws Throwable {
|
||||
void sunnyDayPathLogsCorrectly() throws Throwable {
|
||||
MethodInvocation methodInvocation = mock();
|
||||
given(methodInvocation.getMethod()).willReturn(String.class.getMethod("toString"));
|
||||
given(methodInvocation.getThis()).willReturn(this);
|
||||
@@ -108,7 +122,7 @@ public class CustomizableTraceInterceptorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExceptionPathLogsCorrectly() throws Throwable {
|
||||
void exceptionPathLogsCorrectly() throws Throwable {
|
||||
MethodInvocation methodInvocation = mock();
|
||||
|
||||
IllegalArgumentException exception = new IllegalArgumentException();
|
||||
@@ -120,15 +134,14 @@ public class CustomizableTraceInterceptorTests {
|
||||
given(log.isTraceEnabled()).willReturn(true);
|
||||
|
||||
CustomizableTraceInterceptor interceptor = new StubCustomizableTraceInterceptor(log);
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
interceptor.invoke(methodInvocation));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> interceptor.invoke(methodInvocation));
|
||||
|
||||
verify(log).trace(anyString());
|
||||
verify(log).trace(anyString(), eq(exception));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSunnyDayPathLogsCorrectlyWithPrettyMuchAllPlaceholdersMatching() throws Throwable {
|
||||
void sunnyDayPathLogsCorrectlyWithPrettyMuchAllPlaceholdersMatching() throws Throwable {
|
||||
MethodInvocation methodInvocation = mock();
|
||||
|
||||
given(methodInvocation.getMethod()).willReturn(String.class.getMethod("toString", new Class[0]));
|
||||
@@ -141,31 +154,59 @@ public class CustomizableTraceInterceptorTests {
|
||||
|
||||
CustomizableTraceInterceptor interceptor = new StubCustomizableTraceInterceptor(log);
|
||||
interceptor.setEnterMessage(new StringBuilder()
|
||||
.append("Entering the '").append(CustomizableTraceInterceptor.PLACEHOLDER_METHOD_NAME)
|
||||
.append("' method of the [").append(CustomizableTraceInterceptor.PLACEHOLDER_TARGET_CLASS_NAME)
|
||||
.append("] class with the following args (").append(CustomizableTraceInterceptor.PLACEHOLDER_ARGUMENTS)
|
||||
.append(") and arg types (").append(CustomizableTraceInterceptor.PLACEHOLDER_ARGUMENT_TYPES)
|
||||
.append("Entering the '").append(PLACEHOLDER_METHOD_NAME)
|
||||
.append("' method of the [").append(PLACEHOLDER_TARGET_CLASS_NAME)
|
||||
.append("] class with the following args (").append(PLACEHOLDER_ARGUMENTS)
|
||||
.append(") and arg types (").append(PLACEHOLDER_ARGUMENT_TYPES)
|
||||
.append(").").toString());
|
||||
interceptor.setExitMessage(new StringBuilder()
|
||||
.append("Exiting the '").append(CustomizableTraceInterceptor.PLACEHOLDER_METHOD_NAME)
|
||||
.append("' method of the [").append(CustomizableTraceInterceptor.PLACEHOLDER_TARGET_CLASS_SHORT_NAME)
|
||||
.append("] class with the following args (").append(CustomizableTraceInterceptor.PLACEHOLDER_ARGUMENTS)
|
||||
.append(") and arg types (").append(CustomizableTraceInterceptor.PLACEHOLDER_ARGUMENT_TYPES)
|
||||
.append("), returning '").append(CustomizableTraceInterceptor.PLACEHOLDER_RETURN_VALUE)
|
||||
.append("' and taking '").append(CustomizableTraceInterceptor.PLACEHOLDER_INVOCATION_TIME)
|
||||
.append("Exiting the '").append(PLACEHOLDER_METHOD_NAME)
|
||||
.append("' method of the [").append(PLACEHOLDER_TARGET_CLASS_SHORT_NAME)
|
||||
.append("] class with the following args (").append(PLACEHOLDER_ARGUMENTS)
|
||||
.append(") and arg types (").append(PLACEHOLDER_ARGUMENT_TYPES)
|
||||
.append("), returning '").append(PLACEHOLDER_RETURN_VALUE)
|
||||
.append("' and taking '").append(PLACEHOLDER_INVOCATION_TIME)
|
||||
.append("' this long.").toString());
|
||||
interceptor.invoke(methodInvocation);
|
||||
|
||||
verify(log, times(2)).trace(anyString());
|
||||
}
|
||||
|
||||
/**
|
||||
* This test effectively verifies that the internal ALLOWED_PLACEHOLDERS set
|
||||
* is properly configured in {@link CustomizableTraceInterceptor}.
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
void supportedPlaceholderValues() {
|
||||
assertThat(ALLOWED_PLACEHOLDERS).containsAll(getPlaceholderConstantValues());
|
||||
}
|
||||
|
||||
private List<String> getPlaceholderConstantValues() {
|
||||
return Arrays.stream(CustomizableTraceInterceptor.class.getFields())
|
||||
.filter(ReflectionUtils::isPublicStaticFinal)
|
||||
.filter(field -> field.getName().startsWith("PLACEHOLDER_"))
|
||||
.map(this::getFieldValue)
|
||||
.map(String.class::cast)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private Object getFieldValue(Field field) {
|
||||
try {
|
||||
return field.get(null);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private static class StubCustomizableTraceInterceptor extends CustomizableTraceInterceptor {
|
||||
|
||||
private final Log log;
|
||||
|
||||
public StubCustomizableTraceInterceptor(Log log) {
|
||||
StubCustomizableTraceInterceptor(Log log) {
|
||||
super.setUseDynamicLogger(false);
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ dependencies {
|
||||
optional("org.apache.groovy:groovy-xml")
|
||||
optional("org.jetbrains.kotlin:kotlin-reflect")
|
||||
optional("org.jetbrains.kotlin:kotlin-stdlib")
|
||||
optional("org.reactivestreams:reactive-streams")
|
||||
testImplementation(testFixtures(project(":spring-core")))
|
||||
testImplementation(project(":spring-core-test"))
|
||||
testImplementation("jakarta.annotation:jakarta.annotation-api")
|
||||
|
||||
+38
-28
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -30,7 +30,6 @@ import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -320,14 +319,14 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
}
|
||||
|
||||
else if (propValue instanceof List list) {
|
||||
Class<?> requiredType = ph.getCollectionType(tokens.keys.length);
|
||||
TypeDescriptor requiredType = ph.getCollectionType(tokens.keys.length);
|
||||
int index = Integer.parseInt(lastKey);
|
||||
Object oldValue = null;
|
||||
if (isExtractOldValueForEditor() && index < list.size()) {
|
||||
oldValue = list.get(index);
|
||||
}
|
||||
Object convertedValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(),
|
||||
requiredType, ph.nested(tokens.keys.length));
|
||||
requiredType.getResolvableType().resolve(), requiredType);
|
||||
int size = list.size();
|
||||
if (index >= size && index < this.autoGrowCollectionLimit) {
|
||||
for (int i = size; i < index; i++) {
|
||||
@@ -355,12 +354,12 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
}
|
||||
|
||||
else if (propValue instanceof Map map) {
|
||||
Class<?> mapKeyType = ph.getMapKeyType(tokens.keys.length);
|
||||
Class<?> mapValueType = ph.getMapValueType(tokens.keys.length);
|
||||
TypeDescriptor mapKeyType = ph.getMapKeyType(tokens.keys.length);
|
||||
TypeDescriptor mapValueType = ph.getMapValueType(tokens.keys.length);
|
||||
// IMPORTANT: Do not pass full property name in here - property editors
|
||||
// must not kick in for map keys but rather only for map values.
|
||||
TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType);
|
||||
Object convertedMapKey = convertIfNecessary(null, null, lastKey, mapKeyType, typeDescriptor);
|
||||
Object convertedMapKey = convertIfNecessary(null, null, lastKey,
|
||||
mapKeyType.getResolvableType().resolve(), mapKeyType);
|
||||
Object oldValue = null;
|
||||
if (isExtractOldValueForEditor()) {
|
||||
oldValue = map.get(convertedMapKey);
|
||||
@@ -368,7 +367,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
// Pass full property name and old value in here, since we want full
|
||||
// conversion ability for map values.
|
||||
Object convertedMapValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(),
|
||||
mapValueType, ph.nested(tokens.keys.length));
|
||||
mapValueType.getResolvableType().resolve(), mapValueType);
|
||||
map.put(convertedMapKey, convertedMapValue);
|
||||
}
|
||||
|
||||
@@ -657,22 +656,32 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
growCollectionIfNecessary(list, index, indexedPropertyName.toString(), ph, i + 1);
|
||||
value = list.get(index);
|
||||
}
|
||||
else if (value instanceof Set set) {
|
||||
// Apply index to Iterator in case of a Set.
|
||||
else if (value instanceof Iterable iterable) {
|
||||
// Apply index to Iterator in case of a Set/Collection/Iterable.
|
||||
int index = Integer.parseInt(key);
|
||||
if (index < 0 || index >= set.size()) {
|
||||
throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
|
||||
"Cannot get element with index " + index + " from Set of size " +
|
||||
set.size() + ", accessed using property path '" + propertyName + "'");
|
||||
if (value instanceof Collection<?> coll) {
|
||||
if (index < 0 || index >= coll.size()) {
|
||||
throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
|
||||
"Cannot get element with index " + index + " from Collection of size " +
|
||||
coll.size() + ", accessed using property path '" + propertyName + "'");
|
||||
}
|
||||
}
|
||||
Iterator<Object> it = set.iterator();
|
||||
for (int j = 0; it.hasNext(); j++) {
|
||||
Iterator<Object> it = iterable.iterator();
|
||||
boolean found = false;
|
||||
int currIndex = 0;
|
||||
for (; it.hasNext(); currIndex++) {
|
||||
Object elem = it.next();
|
||||
if (j == index) {
|
||||
if (currIndex == index) {
|
||||
value = elem;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
|
||||
"Cannot get element with index " + index + " from Iterable of size " +
|
||||
currIndex + ", accessed using property path '" + propertyName + "'");
|
||||
}
|
||||
}
|
||||
else if (value instanceof Map map) {
|
||||
Class<?> mapKeyType = ph.getResolvableType().getNested(i + 1).asMap().resolveGeneric(0);
|
||||
@@ -685,13 +694,17 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
else {
|
||||
throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
|
||||
"Property referenced in indexed property path '" + propertyName +
|
||||
"' is neither an array nor a List nor a Set nor a Map; returned value was [" + value + "]");
|
||||
"' is neither an array nor a List/Set/Collection/Iterable nor a Map; " +
|
||||
"returned value was [" + value + "]");
|
||||
}
|
||||
indexedPropertyName.append(PROPERTY_KEY_PREFIX).append(key).append(PROPERTY_KEY_SUFFIX);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
catch (InvalidPropertyException ex) {
|
||||
throw ex;
|
||||
}
|
||||
catch (IndexOutOfBoundsException ex) {
|
||||
throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
|
||||
"Index of out of bounds in property path '" + propertyName + "'", ex);
|
||||
@@ -1028,19 +1041,16 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
|
||||
public abstract ResolvableType getResolvableType();
|
||||
|
||||
@Nullable
|
||||
public Class<?> getMapKeyType(int nestingLevel) {
|
||||
return getResolvableType().getNested(nestingLevel).asMap().resolveGeneric(0);
|
||||
public TypeDescriptor getMapKeyType(int nestingLevel) {
|
||||
return TypeDescriptor.valueOf(getResolvableType().getNested(nestingLevel).asMap().resolveGeneric(0));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Class<?> getMapValueType(int nestingLevel) {
|
||||
return getResolvableType().getNested(nestingLevel).asMap().resolveGeneric(1);
|
||||
public TypeDescriptor getMapValueType(int nestingLevel) {
|
||||
return TypeDescriptor.valueOf(getResolvableType().getNested(nestingLevel).asMap().resolveGeneric(1));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Class<?> getCollectionType(int nestingLevel) {
|
||||
return getResolvableType().getNested(nestingLevel).asCollection().resolveGeneric();
|
||||
public TypeDescriptor getCollectionType(int nestingLevel) {
|
||||
return TypeDescriptor.valueOf(getResolvableType().getNested(nestingLevel).asCollection().resolveGeneric());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
||||
@@ -23,15 +23,10 @@ import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.time.temporal.Temporal;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -663,26 +658,22 @@ public abstract class BeanUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given type represents a "simple" value type: a primitive or
|
||||
* primitive wrapper, an enum, a String or other CharSequence, a Number, a
|
||||
* Date, a Temporal, a URI, a URL, a Locale, or a Class.
|
||||
* Check if the given type represents a "simple" value type for
|
||||
* bean property and data binding purposes:
|
||||
* a primitive or primitive wrapper, an {@code Enum}, a {@code String}
|
||||
* or other {@code CharSequence}, a {@code Number}, a {@code Date},
|
||||
* a {@code Temporal}, a {@code UUID}, a {@code URI}, a {@code URL},
|
||||
* a {@code Locale}, or a {@code Class}.
|
||||
* <p>{@code Void} and {@code void} are not considered simple value types.
|
||||
* <p>As of 6.1, this method delegates to {@link ClassUtils#isSimpleValueType}
|
||||
* as-is but could potentially add further rules for bean property purposes.
|
||||
* @param type the type to check
|
||||
* @return whether the given type represents a "simple" value type
|
||||
* @see #isSimpleProperty(Class)
|
||||
* @see ClassUtils#isSimpleValueType(Class)
|
||||
*/
|
||||
public static boolean isSimpleValueType(Class<?> type) {
|
||||
return (Void.class != type && void.class != type &&
|
||||
(ClassUtils.isPrimitiveOrWrapper(type) ||
|
||||
Enum.class.isAssignableFrom(type) ||
|
||||
CharSequence.class.isAssignableFrom(type) ||
|
||||
Number.class.isAssignableFrom(type) ||
|
||||
Date.class.isAssignableFrom(type) ||
|
||||
Temporal.class.isAssignableFrom(type) ||
|
||||
URI.class == type ||
|
||||
URL.class == type ||
|
||||
Locale.class == type ||
|
||||
Class.class == type));
|
||||
return ClassUtils.isSimpleValueType(type);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -236,19 +236,36 @@ public class BeanWrapperImpl extends AbstractNestablePropertyAccessor implements
|
||||
|
||||
private final PropertyDescriptor pd;
|
||||
|
||||
private final TypeDescriptor typeDescriptor;
|
||||
|
||||
public BeanPropertyHandler(PropertyDescriptor pd) {
|
||||
super(pd.getPropertyType(), pd.getReadMethod() != null, pd.getWriteMethod() != null);
|
||||
this.pd = pd;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolvableType getResolvableType() {
|
||||
return ResolvableType.forMethodReturnType(this.pd.getReadMethod());
|
||||
this.typeDescriptor = new TypeDescriptor(property(pd));
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeDescriptor toTypeDescriptor() {
|
||||
return new TypeDescriptor(property(this.pd));
|
||||
return this.typeDescriptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolvableType getResolvableType() {
|
||||
return this.typeDescriptor.getResolvableType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeDescriptor getMapValueType(int nestingLevel) {
|
||||
return new TypeDescriptor(
|
||||
this.typeDescriptor.getResolvableType().getNested(nestingLevel).asMap().getGeneric(1),
|
||||
null, this.typeDescriptor.getAnnotations());
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeDescriptor getCollectionType(int nestingLevel) {
|
||||
return new TypeDescriptor(
|
||||
this.typeDescriptor.getResolvableType().getNested(nestingLevel).asCollection().getGeneric(),
|
||||
null, this.typeDescriptor.getAnnotations());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -101,19 +101,34 @@ public class DirectFieldAccessor extends AbstractNestablePropertyAccessor {
|
||||
|
||||
private final Field field;
|
||||
|
||||
private final ResolvableType resolvableType;
|
||||
|
||||
public FieldPropertyHandler(Field field) {
|
||||
super(field.getType(), true, true);
|
||||
this.field = field;
|
||||
this.resolvableType = ResolvableType.forField(this.field);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeDescriptor toTypeDescriptor() {
|
||||
return new TypeDescriptor(this.field);
|
||||
return new TypeDescriptor(this.resolvableType, this.field.getType(), this.field.getAnnotations());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolvableType getResolvableType() {
|
||||
return ResolvableType.forField(this.field);
|
||||
return this.resolvableType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeDescriptor getMapValueType(int nestingLevel) {
|
||||
return new TypeDescriptor(this.resolvableType.getNested(nestingLevel).asMap().getGeneric(1),
|
||||
null, this.field.getAnnotations());
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeDescriptor getCollectionType(int nestingLevel) {
|
||||
return new TypeDescriptor(this.resolvableType.getNested(nestingLevel).asCollection().getGeneric(),
|
||||
null, this.field.getAnnotations());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -42,7 +42,7 @@ public abstract class TypeConverterSupport extends PropertyEditorRegistrySupport
|
||||
@Override
|
||||
@Nullable
|
||||
public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType) throws TypeMismatchException {
|
||||
return convertIfNecessary(value, requiredType, TypeDescriptor.valueOf(requiredType));
|
||||
return convertIfNecessary(null, value, requiredType, TypeDescriptor.valueOf(requiredType));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -50,7 +50,7 @@ public abstract class TypeConverterSupport extends PropertyEditorRegistrySupport
|
||||
public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
|
||||
@Nullable MethodParameter methodParam) throws TypeMismatchException {
|
||||
|
||||
return convertIfNecessary(value, requiredType,
|
||||
return convertIfNecessary((methodParam != null ? methodParam.getParameterName() : null), value, requiredType,
|
||||
(methodParam != null ? new TypeDescriptor(methodParam) : TypeDescriptor.valueOf(requiredType)));
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ public abstract class TypeConverterSupport extends PropertyEditorRegistrySupport
|
||||
public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType, @Nullable Field field)
|
||||
throws TypeMismatchException {
|
||||
|
||||
return convertIfNecessary(value, requiredType,
|
||||
return convertIfNecessary((field != null ? field.getName() : null), value, requiredType,
|
||||
(field != null ? new TypeDescriptor(field) : TypeDescriptor.valueOf(requiredType)));
|
||||
}
|
||||
|
||||
@@ -68,9 +68,17 @@ public abstract class TypeConverterSupport extends PropertyEditorRegistrySupport
|
||||
public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
|
||||
@Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException {
|
||||
|
||||
return convertIfNecessary(null, value, requiredType, typeDescriptor);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private <T> T convertIfNecessary(@Nullable String propertyName, @Nullable Object value,
|
||||
@Nullable Class<T> requiredType, @Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException {
|
||||
|
||||
Assert.state(this.typeConverterDelegate != null, "No TypeConverterDelegate");
|
||||
try {
|
||||
return this.typeConverterDelegate.convertIfNecessary(null, null, value, requiredType, typeDescriptor);
|
||||
return this.typeConverterDelegate.convertIfNecessary(
|
||||
propertyName, null, value, requiredType, typeDescriptor);
|
||||
}
|
||||
catch (ConverterNotFoundException | IllegalStateException ex) {
|
||||
throw new ConversionNotSupportedException(value, requiredType, ex);
|
||||
|
||||
+2
-2
@@ -27,6 +27,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.aot.hint.ExecutableMode;
|
||||
import org.springframework.beans.BeanInstantiationException;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.TypeConverter;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
@@ -345,8 +346,7 @@ public final class BeanInstanceSupplier<T> extends AutowiredElementResolver impl
|
||||
Object enclosingInstance = createInstance(declaringClass.getEnclosingClass());
|
||||
args = ObjectUtils.addObjectToArray(args, enclosingInstance, 0);
|
||||
}
|
||||
ReflectionUtils.makeAccessible(constructor);
|
||||
return constructor.newInstance(args);
|
||||
return BeanUtils.instantiateClass(constructor, args);
|
||||
}
|
||||
|
||||
private Object instantiate(ConfigurableBeanFactory beanFactory, Method method, Object[] args) throws Exception {
|
||||
|
||||
+40
-6
@@ -24,6 +24,11 @@ import java.lang.reflect.Modifier;
|
||||
import java.util.Arrays;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import kotlin.jvm.JvmClassMappingKt;
|
||||
import kotlin.reflect.KClass;
|
||||
import kotlin.reflect.KFunction;
|
||||
import kotlin.reflect.KParameter;
|
||||
|
||||
import org.springframework.aot.generate.AccessControl;
|
||||
import org.springframework.aot.generate.AccessControl.Visibility;
|
||||
import org.springframework.aot.generate.GeneratedMethod;
|
||||
@@ -31,8 +36,11 @@ import org.springframework.aot.generate.GeneratedMethods;
|
||||
import org.springframework.aot.generate.GenerationContext;
|
||||
import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator;
|
||||
import org.springframework.aot.hint.ExecutableMode;
|
||||
import org.springframework.aot.hint.MemberCategory;
|
||||
import org.springframework.aot.hint.ReflectionHints;
|
||||
import org.springframework.beans.factory.support.InstanceSupplier;
|
||||
import org.springframework.beans.factory.support.RegisteredBean;
|
||||
import org.springframework.core.KotlinDetector;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.javapoet.ClassName;
|
||||
import org.springframework.javapoet.CodeBlock;
|
||||
@@ -56,6 +64,7 @@ import org.springframework.util.function.ThrowingSupplier;
|
||||
* @author Phillip Webb
|
||||
* @author Stephane Nicoll
|
||||
* @author Juergen Hoeller
|
||||
* @author Sebastien Deleuze
|
||||
* @since 6.0
|
||||
*/
|
||||
class InstanceSupplierCodeGenerator {
|
||||
@@ -108,11 +117,16 @@ class InstanceSupplierCodeGenerator {
|
||||
boolean dependsOnBean = ClassUtils.isInnerClass(declaringClass);
|
||||
|
||||
Visibility accessVisibility = getAccessVisibility(registeredBean, constructor);
|
||||
if (accessVisibility != Visibility.PRIVATE) {
|
||||
if (KotlinDetector.isKotlinReflectPresent() && KotlinDelegate.hasConstructorWithOptionalParameter(beanClass)) {
|
||||
return generateCodeForInaccessibleConstructor(beanName, beanClass, constructor,
|
||||
dependsOnBean, hints -> hints.registerType(beanClass, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS));
|
||||
}
|
||||
else if (accessVisibility != Visibility.PRIVATE) {
|
||||
return generateCodeForAccessibleConstructor(beanName, beanClass, constructor,
|
||||
dependsOnBean, declaringClass);
|
||||
}
|
||||
return generateCodeForInaccessibleConstructor(beanName, beanClass, constructor, dependsOnBean);
|
||||
return generateCodeForInaccessibleConstructor(beanName, beanClass, constructor, dependsOnBean,
|
||||
hints -> hints.registerConstructor(constructor, ExecutableMode.INVOKE));
|
||||
}
|
||||
|
||||
private CodeBlock generateCodeForAccessibleConstructor(String beanName, Class<?> beanClass,
|
||||
@@ -137,11 +151,10 @@ class InstanceSupplierCodeGenerator {
|
||||
return generateReturnStatement(generatedMethod);
|
||||
}
|
||||
|
||||
private CodeBlock generateCodeForInaccessibleConstructor(String beanName,
|
||||
Class<?> beanClass, Constructor<?> constructor, boolean dependsOnBean) {
|
||||
private CodeBlock generateCodeForInaccessibleConstructor(String beanName, Class<?> beanClass,
|
||||
Constructor<?> constructor, boolean dependsOnBean, Consumer<ReflectionHints> hints) {
|
||||
|
||||
this.generationContext.getRuntimeHints().reflection()
|
||||
.registerConstructor(constructor, ExecutableMode.INVOKE);
|
||||
hints.accept(this.generationContext.getRuntimeHints().reflection());
|
||||
|
||||
GeneratedMethod generatedMethod = generateGetInstanceSupplierMethod(method -> {
|
||||
method.addJavadoc("Get the bean instance supplier for '$L'.", beanName);
|
||||
@@ -338,4 +351,25 @@ class InstanceSupplierCodeGenerator {
|
||||
.anyMatch(Exception.class::isAssignableFrom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inner class to avoid a hard dependency on Kotlin at runtime.
|
||||
*/
|
||||
private static class KotlinDelegate {
|
||||
|
||||
public static boolean hasConstructorWithOptionalParameter(Class<?> beanClass) {
|
||||
if (KotlinDetector.isKotlinType(beanClass)) {
|
||||
KClass<?> kClass = JvmClassMappingKt.getKotlinClass(beanClass);
|
||||
for (KFunction<?> constructor : kClass.getConstructors()) {
|
||||
for (KParameter parameter : constructor.getParameters()) {
|
||||
if (parameter.isOptional()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
-6
@@ -64,7 +64,6 @@ public interface AutowireCapableBeanFactory extends BeanFactory {
|
||||
/**
|
||||
* Constant that indicates no externally defined autowiring. Note that
|
||||
* BeanFactoryAware etc and annotation-driven injection will still be applied.
|
||||
* @see #createBean
|
||||
* @see #autowire
|
||||
* @see #autowireBeanProperties
|
||||
*/
|
||||
@@ -73,7 +72,6 @@ public interface AutowireCapableBeanFactory extends BeanFactory {
|
||||
/**
|
||||
* Constant that indicates autowiring bean properties by name
|
||||
* (applying to all bean property setters).
|
||||
* @see #createBean
|
||||
* @see #autowire
|
||||
* @see #autowireBeanProperties
|
||||
*/
|
||||
@@ -82,7 +80,6 @@ public interface AutowireCapableBeanFactory extends BeanFactory {
|
||||
/**
|
||||
* Constant that indicates autowiring bean properties by type
|
||||
* (applying to all bean property setters).
|
||||
* @see #createBean
|
||||
* @see #autowire
|
||||
* @see #autowireBeanProperties
|
||||
*/
|
||||
@@ -91,7 +88,6 @@ public interface AutowireCapableBeanFactory extends BeanFactory {
|
||||
/**
|
||||
* Constant that indicates autowiring the greediest constructor that
|
||||
* can be satisfied (involves resolving the appropriate constructor).
|
||||
* @see #createBean
|
||||
* @see #autowire
|
||||
*/
|
||||
int AUTOWIRE_CONSTRUCTOR = 3;
|
||||
@@ -99,7 +95,6 @@ public interface AutowireCapableBeanFactory extends BeanFactory {
|
||||
/**
|
||||
* Constant that indicates determining an appropriate autowire strategy
|
||||
* through introspection of the bean class.
|
||||
* @see #createBean
|
||||
* @see #autowire
|
||||
* @deprecated as of Spring 3.0: If you are using mixed autowiring strategies,
|
||||
* prefer annotation-based autowiring for clearer demarcation of autowiring needs.
|
||||
@@ -192,7 +187,9 @@ public interface AutowireCapableBeanFactory extends BeanFactory {
|
||||
* @see #AUTOWIRE_BY_NAME
|
||||
* @see #AUTOWIRE_BY_TYPE
|
||||
* @see #AUTOWIRE_CONSTRUCTOR
|
||||
* @deprecated as of 6.1, in favor of {@link #createBean(Class)}
|
||||
*/
|
||||
@Deprecated(since = "6.1")
|
||||
Object createBean(Class<?> beanClass, int autowireMode, boolean dependencyCheck) throws BeansException;
|
||||
|
||||
/**
|
||||
@@ -322,7 +319,7 @@ public interface AutowireCapableBeanFactory extends BeanFactory {
|
||||
throws BeansException;
|
||||
|
||||
/**
|
||||
* Destroy the given bean instance (typically coming from {@link #createBean}),
|
||||
* Destroy the given bean instance (typically coming from {@link #createBean(Class)}),
|
||||
* applying the {@link org.springframework.beans.factory.DisposableBean} contract as well as
|
||||
* registered {@link DestructionAwareBeanPostProcessor DestructionAwareBeanPostProcessors}.
|
||||
* <p>Any exception that arises during destruction should be caught
|
||||
|
||||
+18
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,13 +16,14 @@
|
||||
|
||||
package org.springframework.beans.factory.config;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.core.Constants;
|
||||
import org.springframework.core.SpringProperties;
|
||||
import org.springframework.core.env.AbstractEnvironment;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.PropertyPlaceholderHelper;
|
||||
import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
|
||||
import org.springframework.util.StringValueResolver;
|
||||
@@ -45,6 +46,7 @@ import org.springframework.util.StringValueResolver;
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Chris Beams
|
||||
* @author Sam Brannen
|
||||
* @since 02.10.2003
|
||||
* @see #setSystemPropertiesModeName
|
||||
* @see PlaceholderConfigurerSupport
|
||||
@@ -72,7 +74,16 @@ public class PropertyPlaceholderConfigurer extends PlaceholderConfigurerSupport
|
||||
public static final int SYSTEM_PROPERTIES_MODE_OVERRIDE = 2;
|
||||
|
||||
|
||||
private static final Constants constants = new Constants(PropertyPlaceholderConfigurer.class);
|
||||
/**
|
||||
* Map of constant names to constant values for the system properties mode
|
||||
* constants defined in this class.
|
||||
*/
|
||||
private static final Map<String, Integer> constants = Map.of(
|
||||
"SYSTEM_PROPERTIES_MODE_NEVER", SYSTEM_PROPERTIES_MODE_NEVER,
|
||||
"SYSTEM_PROPERTIES_MODE_FALLBACK", SYSTEM_PROPERTIES_MODE_FALLBACK,
|
||||
"SYSTEM_PROPERTIES_MODE_OVERRIDE", SYSTEM_PROPERTIES_MODE_OVERRIDE
|
||||
);
|
||||
|
||||
|
||||
private int systemPropertiesMode = SYSTEM_PROPERTIES_MODE_FALLBACK;
|
||||
|
||||
@@ -87,7 +98,10 @@ public class PropertyPlaceholderConfigurer extends PlaceholderConfigurerSupport
|
||||
* @see #setSystemPropertiesMode
|
||||
*/
|
||||
public void setSystemPropertiesModeName(String constantName) throws IllegalArgumentException {
|
||||
this.systemPropertiesMode = constants.asNumber(constantName).intValue();
|
||||
Assert.hasText(constantName, "'constantName' must not be null or blank");
|
||||
Integer mode = constants.get(constantName);
|
||||
Assert.notNull(mode, "Only system properties mode constants allowed");
|
||||
this.systemPropertiesMode = mode;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -64,7 +64,7 @@ import org.springframework.lang.Nullable;
|
||||
* Note that the value of "foo" in the first document is not simply replaced
|
||||
* with the value in the second, but its nested values are merged.
|
||||
*
|
||||
* <p>Requires SnakeYAML 1.18 or higher, as of Spring Framework 5.0.6.
|
||||
* <p>Requires SnakeYAML 2.0 or higher, as of Spring Framework 6.1.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Juergen Hoeller
|
||||
|
||||
+16
-24
@@ -33,7 +33,10 @@ import org.apache.commons.logging.LogFactory;
|
||||
import org.yaml.snakeyaml.DumperOptions;
|
||||
import org.yaml.snakeyaml.LoaderOptions;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
import org.yaml.snakeyaml.composer.ComposerException;
|
||||
import org.yaml.snakeyaml.constructor.Constructor;
|
||||
import org.yaml.snakeyaml.inspector.TagInspector;
|
||||
import org.yaml.snakeyaml.nodes.Tag;
|
||||
import org.yaml.snakeyaml.reader.UnicodeReader;
|
||||
import org.yaml.snakeyaml.representer.Representer;
|
||||
|
||||
@@ -47,7 +50,7 @@ import org.springframework.util.StringUtils;
|
||||
/**
|
||||
* Base class for YAML factories.
|
||||
*
|
||||
* <p>Requires SnakeYAML 1.18 or higher, as of Spring Framework 5.0.6.
|
||||
* <p>Requires SnakeYAML 2.0 or higher, as of Spring Framework 6.1.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Juergen Hoeller
|
||||
@@ -132,7 +135,7 @@ public abstract class YamlProcessor {
|
||||
* <p>If no supported types are configured, only Java standard classes
|
||||
* (as defined in {@link org.yaml.snakeyaml.constructor.SafeConstructor})
|
||||
* encountered in YAML documents will be supported.
|
||||
* If an unsupported type is encountered, an {@link IllegalStateException}
|
||||
* If an unsupported type is encountered, a {@link ComposerException}
|
||||
* will be thrown when the corresponding YAML node is processed.
|
||||
* @param supportedTypes the supported types, or an empty array to clear the
|
||||
* supported types
|
||||
@@ -173,19 +176,20 @@ public abstract class YamlProcessor {
|
||||
/**
|
||||
* Create the {@link Yaml} instance to use.
|
||||
* <p>The default implementation sets the "allowDuplicateKeys" flag to {@code false},
|
||||
* enabling built-in duplicate key handling in SnakeYAML 1.18+.
|
||||
* <p>As of Spring Framework 5.1.16, if custom {@linkplain #setSupportedTypes
|
||||
* supported types} have been configured, the default implementation creates
|
||||
* a {@code Yaml} instance that filters out unsupported types encountered in
|
||||
* YAML documents. If an unsupported type is encountered, an
|
||||
* {@link IllegalStateException} will be thrown when the node is processed.
|
||||
* enabling built-in duplicate key handling.
|
||||
* <p>If custom {@linkplain #setSupportedTypes supported types} have been configured,
|
||||
* the default implementation creates a {@code Yaml} instance that filters out
|
||||
* unsupported types encountered in YAML documents.
|
||||
* If an unsupported type is encountered, a {@link ComposerException} will be
|
||||
* thrown when the node is processed.
|
||||
* @see LoaderOptions#setAllowDuplicateKeys(boolean)
|
||||
*/
|
||||
protected Yaml createYaml() {
|
||||
LoaderOptions loaderOptions = new LoaderOptions();
|
||||
loaderOptions.setAllowDuplicateKeys(false);
|
||||
loaderOptions.setTagInspector(new SupportedTagInspector());
|
||||
DumperOptions dumperOptions = new DumperOptions();
|
||||
return new Yaml(new FilteringConstructor(loaderOptions), new Representer(dumperOptions),
|
||||
return new Yaml(new Constructor(loaderOptions), new Representer(dumperOptions),
|
||||
dumperOptions, loaderOptions);
|
||||
}
|
||||
|
||||
@@ -425,23 +429,11 @@ public abstract class YamlProcessor {
|
||||
FIRST_FOUND
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@link Constructor} that supports filtering of unsupported types.
|
||||
* <p>If an unsupported type is encountered in a YAML document, an
|
||||
* {@link IllegalStateException} will be thrown from {@link #getClassForName}.
|
||||
*/
|
||||
private class FilteringConstructor extends Constructor {
|
||||
|
||||
FilteringConstructor(LoaderOptions loaderOptions) {
|
||||
super(loaderOptions);
|
||||
}
|
||||
private class SupportedTagInspector implements TagInspector {
|
||||
|
||||
@Override
|
||||
protected Class<?> getClassForName(String name) throws ClassNotFoundException {
|
||||
Assert.state(YamlProcessor.this.supportedTypes.contains(name),
|
||||
() -> "Unsupported type encountered in YAML document: " + name);
|
||||
return super.getClassForName(name);
|
||||
public boolean isGlobalTagAllowed(Tag tag) {
|
||||
return supportedTypes.contains(tag.getClassName());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -74,7 +74,7 @@ import org.springframework.lang.Nullable;
|
||||
* servers[1]=foo.bar.com
|
||||
* </pre>
|
||||
*
|
||||
* <p>Requires SnakeYAML 1.18 or higher, as of Spring Framework 5.0.6.
|
||||
* <p>Requires SnakeYAML 2.0 or higher, as of Spring Framework 6.1.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Stephane Nicoll
|
||||
|
||||
+1
@@ -357,6 +357,7 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac
|
||||
// Specialized methods for fine-grained control over the bean lifecycle
|
||||
//-------------------------------------------------------------------------
|
||||
|
||||
@Deprecated
|
||||
@Override
|
||||
public Object createBean(Class<?> beanClass, int autowireMode, boolean dependencyCheck) throws BeansException {
|
||||
// Use non-singleton bean definition, to avoid registering bean as dependent bean.
|
||||
|
||||
+110
-15
@@ -21,13 +21,20 @@ import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.reactivestreams.Subscriber;
|
||||
import org.reactivestreams.Subscription;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor;
|
||||
import org.springframework.core.ReactiveAdapter;
|
||||
import org.springframework.core.ReactiveAdapterRegistry;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
@@ -68,6 +75,10 @@ class DisposableBeanAdapter implements DisposableBean, Runnable, Serializable {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(DisposableBeanAdapter.class);
|
||||
|
||||
private static final boolean reactiveStreamsPresent = ClassUtils.isPresent(
|
||||
"org.reactivestreams.Publisher", DisposableBeanAdapter.class.getClassLoader());
|
||||
|
||||
|
||||
private final Object bean;
|
||||
|
||||
private final String beanName;
|
||||
@@ -289,32 +300,40 @@ class DisposableBeanAdapter implements DisposableBean, Runnable, Serializable {
|
||||
* assuming a "force" parameter), else logging an error.
|
||||
*/
|
||||
private void invokeCustomDestroyMethod(Method destroyMethod) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Invoking custom destroy method '" + destroyMethod.getName() +
|
||||
"' on bean with name '" + this.beanName + "': " + destroyMethod);
|
||||
}
|
||||
|
||||
int paramCount = destroyMethod.getParameterCount();
|
||||
Object[] args = new Object[paramCount];
|
||||
if (paramCount == 1) {
|
||||
args[0] = Boolean.TRUE;
|
||||
}
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Invoking custom destroy method '" + destroyMethod.getName() +
|
||||
"' on bean with name '" + this.beanName + "'");
|
||||
}
|
||||
|
||||
try {
|
||||
ReflectionUtils.makeAccessible(destroyMethod);
|
||||
destroyMethod.invoke(this.bean, args);
|
||||
}
|
||||
catch (InvocationTargetException ex) {
|
||||
if (logger.isWarnEnabled()) {
|
||||
String msg = "Custom destroy method '" + destroyMethod.getName() + "' on bean with name '" +
|
||||
this.beanName + "' threw an exception";
|
||||
Object returnValue = destroyMethod.invoke(this.bean, args);
|
||||
|
||||
if (returnValue == null) {
|
||||
// Regular case: a void method
|
||||
logDestroyMethodCompletion(destroyMethod, false);
|
||||
}
|
||||
else if (returnValue instanceof Future<?> future) {
|
||||
// An async task: await its completion.
|
||||
future.get();
|
||||
logDestroyMethodCompletion(destroyMethod, true);
|
||||
}
|
||||
else if (!reactiveStreamsPresent || !new ReactiveDestroyMethodHandler().await(destroyMethod, returnValue)) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
// Log at warn level like below but add the exception stacktrace only with debug level
|
||||
logger.warn(msg, ex.getTargetException());
|
||||
}
|
||||
else {
|
||||
logger.warn(msg + ": " + ex.getTargetException());
|
||||
logger.debug("Unknown return value type from custom destroy method '" + destroyMethod.getName() +
|
||||
"' on bean with name '" + this.beanName + "': " + returnValue.getClass());
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (InvocationTargetException | ExecutionException ex) {
|
||||
logDestroyMethodException(destroyMethod, ex.getCause());
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
if (logger.isWarnEnabled()) {
|
||||
logger.warn("Failed to invoke custom destroy method '" + destroyMethod.getName() +
|
||||
@@ -323,6 +342,27 @@ class DisposableBeanAdapter implements DisposableBean, Runnable, Serializable {
|
||||
}
|
||||
}
|
||||
|
||||
void logDestroyMethodException(Method destroyMethod, Throwable ex) {
|
||||
if (logger.isWarnEnabled()) {
|
||||
String msg = "Custom destroy method '" + destroyMethod.getName() + "' on bean with name '" +
|
||||
this.beanName + "' propagated an exception";
|
||||
if (logger.isDebugEnabled()) {
|
||||
// Log at warn level like below but add the exception stacktrace only with debug level
|
||||
logger.warn(msg, ex);
|
||||
}
|
||||
else {
|
||||
logger.warn(msg + ": " + ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void logDestroyMethodCompletion(Method destroyMethod, boolean async) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Custom destroy method '" + destroyMethod.getName() +
|
||||
"' on bean with name '" + this.beanName + "' completed" + (async ? " asynchronously" : ""));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Serializes a copy of the state of this class,
|
||||
@@ -445,4 +485,59 @@ class DisposableBeanAdapter implements DisposableBean, Runnable, Serializable {
|
||||
return filteredPostProcessors;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Inner class to avoid a hard dependency on the Reactive Streams API at runtime.
|
||||
*/
|
||||
private class ReactiveDestroyMethodHandler {
|
||||
|
||||
public boolean await(Method destroyMethod, Object returnValue) throws InterruptedException {
|
||||
ReactiveAdapter adapter = ReactiveAdapterRegistry.getSharedInstance().getAdapter(returnValue.getClass());
|
||||
if (adapter != null) {
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
adapter.toPublisher(returnValue).subscribe(new DestroyMethodSubscriber(destroyMethod, latch));
|
||||
latch.await();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reactive Streams Subscriber for destroy method completion.
|
||||
*/
|
||||
private class DestroyMethodSubscriber implements Subscriber<Object> {
|
||||
|
||||
private final Method destroyMethod;
|
||||
|
||||
private final CountDownLatch latch;
|
||||
|
||||
public DestroyMethodSubscriber(Method destroyMethod, CountDownLatch latch) {
|
||||
this.destroyMethod = destroyMethod;
|
||||
this.latch = latch;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSubscribe(Subscription s) {
|
||||
s.request(Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNext(Object o) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable t) {
|
||||
this.latch.countDown();
|
||||
logDestroyMethodException(this.destroyMethod, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete() {
|
||||
this.latch.countDown();
|
||||
logDestroyMethodCompletion(this.destroyMethod, true);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+16
-4
@@ -19,6 +19,7 @@ package org.springframework.beans.factory.xml;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
@@ -40,7 +41,6 @@ import org.springframework.beans.factory.parsing.ReaderEventListener;
|
||||
import org.springframework.beans.factory.parsing.SourceExtractor;
|
||||
import org.springframework.beans.factory.support.AbstractBeanDefinitionReader;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.core.Constants;
|
||||
import org.springframework.core.NamedThreadLocal;
|
||||
import org.springframework.core.io.DescriptiveResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
@@ -68,6 +68,7 @@ import org.springframework.util.xml.XmlValidationModeDetector;
|
||||
* @author Juergen Hoeller
|
||||
* @author Rob Harrop
|
||||
* @author Chris Beams
|
||||
* @author Sam Brannen
|
||||
* @since 26.11.2003
|
||||
* @see #setDocumentReaderClass
|
||||
* @see BeanDefinitionDocumentReader
|
||||
@@ -99,8 +100,16 @@ public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {
|
||||
public static final int VALIDATION_XSD = XmlValidationModeDetector.VALIDATION_XSD;
|
||||
|
||||
|
||||
/** Constants instance for this class. */
|
||||
private static final Constants constants = new Constants(XmlBeanDefinitionReader.class);
|
||||
/**
|
||||
* Map of constant names to constant values for the validation constants defined
|
||||
* in this class.
|
||||
*/
|
||||
private static final Map<String, Integer> constants = Map.of(
|
||||
"VALIDATION_NONE", VALIDATION_NONE,
|
||||
"VALIDATION_AUTO", VALIDATION_AUTO,
|
||||
"VALIDATION_DTD", VALIDATION_DTD,
|
||||
"VALIDATION_XSD", VALIDATION_XSD
|
||||
);
|
||||
|
||||
private int validationMode = VALIDATION_AUTO;
|
||||
|
||||
@@ -163,7 +172,10 @@ public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {
|
||||
* @see #setValidationMode
|
||||
*/
|
||||
public void setValidationModeName(String validationModeName) {
|
||||
setValidationMode(constants.asNumber(validationModeName).intValue());
|
||||
Assert.hasText(validationModeName, "'validationModeName' must not be null or blank");
|
||||
Integer mode = constants.get(validationModeName);
|
||||
Assert.notNull(mode, "Only validation mode constants allowed");
|
||||
setValidationMode(mode);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+9
@@ -139,12 +139,14 @@ abstract class AbstractPropertyAccessorTests {
|
||||
assertThat(accessor.isReadableProperty("list")).isTrue();
|
||||
assertThat(accessor.isReadableProperty("set")).isTrue();
|
||||
assertThat(accessor.isReadableProperty("map")).isTrue();
|
||||
assertThat(accessor.isReadableProperty("myTestBeans")).isTrue();
|
||||
assertThat(accessor.isReadableProperty("xxx")).isFalse();
|
||||
|
||||
assertThat(accessor.isWritableProperty("array")).isTrue();
|
||||
assertThat(accessor.isWritableProperty("list")).isTrue();
|
||||
assertThat(accessor.isWritableProperty("set")).isTrue();
|
||||
assertThat(accessor.isWritableProperty("map")).isTrue();
|
||||
assertThat(accessor.isWritableProperty("myTestBeans")).isTrue();
|
||||
assertThat(accessor.isWritableProperty("xxx")).isFalse();
|
||||
|
||||
assertThat(accessor.isReadableProperty("array[0]")).isTrue();
|
||||
@@ -159,6 +161,8 @@ abstract class AbstractPropertyAccessorTests {
|
||||
assertThat(accessor.isReadableProperty("map[key4][0].name")).isTrue();
|
||||
assertThat(accessor.isReadableProperty("map[key4][1]")).isTrue();
|
||||
assertThat(accessor.isReadableProperty("map[key4][1].name")).isTrue();
|
||||
assertThat(accessor.isReadableProperty("myTestBeans[0]")).isTrue();
|
||||
assertThat(accessor.isReadableProperty("myTestBeans[1]")).isFalse();
|
||||
assertThat(accessor.isReadableProperty("array[key1]")).isFalse();
|
||||
|
||||
assertThat(accessor.isWritableProperty("array[0]")).isTrue();
|
||||
@@ -173,6 +177,8 @@ abstract class AbstractPropertyAccessorTests {
|
||||
assertThat(accessor.isWritableProperty("map[key4][0].name")).isTrue();
|
||||
assertThat(accessor.isWritableProperty("map[key4][1]")).isTrue();
|
||||
assertThat(accessor.isWritableProperty("map[key4][1].name")).isTrue();
|
||||
assertThat(accessor.isReadableProperty("myTestBeans[0]")).isTrue();
|
||||
assertThat(accessor.isReadableProperty("myTestBeans[1]")).isFalse();
|
||||
assertThat(accessor.isWritableProperty("array[key1]")).isFalse();
|
||||
}
|
||||
|
||||
@@ -1388,6 +1394,7 @@ abstract class AbstractPropertyAccessorTests {
|
||||
assertThat(accessor.getPropertyValue("map[key5[foo]].name")).isEqualTo("name8");
|
||||
assertThat(accessor.getPropertyValue("map['key5[foo]'].name")).isEqualTo("name8");
|
||||
assertThat(accessor.getPropertyValue("map[\"key5[foo]\"].name")).isEqualTo("name8");
|
||||
assertThat(accessor.getPropertyValue("myTestBeans[0].name")).isEqualTo("nameZ");
|
||||
|
||||
MutablePropertyValues pvs = new MutablePropertyValues();
|
||||
pvs.add("array[0].name", "name5");
|
||||
@@ -1401,6 +1408,7 @@ abstract class AbstractPropertyAccessorTests {
|
||||
pvs.add("map[key4][0].name", "nameA");
|
||||
pvs.add("map[key4][1].name", "nameB");
|
||||
pvs.add("map[key5[foo]].name", "name10");
|
||||
pvs.add("myTestBeans[0].name", "nameZZ");
|
||||
accessor.setPropertyValues(pvs);
|
||||
assertThat(tb0.getName()).isEqualTo("name5");
|
||||
assertThat(tb1.getName()).isEqualTo("name4");
|
||||
@@ -1419,6 +1427,7 @@ abstract class AbstractPropertyAccessorTests {
|
||||
assertThat(accessor.getPropertyValue("map[key4][0].name")).isEqualTo("nameA");
|
||||
assertThat(accessor.getPropertyValue("map[key4][1].name")).isEqualTo("nameB");
|
||||
assertThat(accessor.getPropertyValue("map[key5[foo]].name")).isEqualTo("name10");
|
||||
assertThat(accessor.getPropertyValue("myTestBeans[0].name")).isEqualTo("nameZZ");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -31,6 +31,7 @@ import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
@@ -470,7 +471,8 @@ class BeanUtilsTests {
|
||||
@ValueSource(classes = {
|
||||
boolean.class, char.class, byte.class, short.class, int.class, long.class, float.class, double.class,
|
||||
Boolean.class, Character.class, Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class,
|
||||
DayOfWeek.class, String.class, LocalDateTime.class, Date.class, URI.class, URL.class, Locale.class, Class.class
|
||||
DayOfWeek.class, String.class, LocalDateTime.class, Date.class, UUID.class, URI.class, URL.class,
|
||||
Locale.class, Class.class
|
||||
})
|
||||
void isSimpleValueType(Class<?> type) {
|
||||
assertThat(BeanUtils.isSimpleValueType(type)).as("Type [" + type.getName() + "] should be a simple value type").isTrue();
|
||||
@@ -486,8 +488,8 @@ class BeanUtilsTests {
|
||||
@ValueSource(classes = {
|
||||
boolean.class, char.class, byte.class, short.class, int.class, long.class, float.class, double.class,
|
||||
Boolean.class, Character.class, Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class,
|
||||
DayOfWeek.class, String.class, LocalDateTime.class, Date.class, URI.class, URL.class, Locale.class, Class.class,
|
||||
boolean[].class, Boolean[].class, LocalDateTime[].class, Date[].class
|
||||
DayOfWeek.class, String.class, LocalDateTime.class, Date.class, UUID.class, URI.class, URL.class,
|
||||
Locale.class, Class.class, boolean[].class, Boolean[].class, LocalDateTime[].class, Date[].class
|
||||
})
|
||||
void isSimpleProperty(Class<?> type) {
|
||||
assertThat(BeanUtils.isSimpleProperty(type)).as("Type [" + type.getName() + "] should be a simple property").isTrue();
|
||||
|
||||
@@ -36,6 +36,8 @@ import org.springframework.beans.testfixture.beans.GenericIntegerBean;
|
||||
import org.springframework.beans.testfixture.beans.GenericSetOfIntegerBean;
|
||||
import org.springframework.beans.testfixture.beans.TestBean;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
@@ -429,6 +431,18 @@ class BeanWrapperGenericsTests {
|
||||
assertThat(holder.getGenericIndexedMap().values().iterator().next().get(0)).isEqualTo(Long.valueOf(10));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testComplexGenericIndexedMapEntryWithPlainValue() {
|
||||
String inputValue = "10";
|
||||
|
||||
ComplexMapHolder holder = new ComplexMapHolder();
|
||||
BeanWrapper bw = new BeanWrapperImpl(holder);
|
||||
bw.setPropertyValue("genericIndexedMap[1]", inputValue);
|
||||
|
||||
assertThat(holder.getGenericIndexedMap().keySet().iterator().next()).isEqualTo(1);
|
||||
assertThat(holder.getGenericIndexedMap().values().iterator().next().get(0)).isEqualTo(Long.valueOf(10));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testComplexDerivedIndexedMapEntry() {
|
||||
List<String> inputValue = new ArrayList<>();
|
||||
@@ -455,6 +469,56 @@ class BeanWrapperGenericsTests {
|
||||
assertThat(holder.getDerivedIndexedMap().values().iterator().next().get(0)).isEqualTo(Long.valueOf(10));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testComplexDerivedIndexedMapEntryWithPlainValue() {
|
||||
String inputValue = "10";
|
||||
|
||||
ComplexMapHolder holder = new ComplexMapHolder();
|
||||
BeanWrapper bw = new BeanWrapperImpl(holder);
|
||||
bw.setPropertyValue("derivedIndexedMap[1]", inputValue);
|
||||
|
||||
assertThat(holder.getDerivedIndexedMap().keySet().iterator().next()).isEqualTo(1);
|
||||
assertThat(holder.getDerivedIndexedMap().values().iterator().next().get(0)).isEqualTo(Long.valueOf(10));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testComplexMultiValueMapEntry() {
|
||||
List<String> inputValue = new ArrayList<>();
|
||||
inputValue.add("10");
|
||||
|
||||
ComplexMapHolder holder = new ComplexMapHolder();
|
||||
BeanWrapper bw = new BeanWrapperImpl(holder);
|
||||
bw.setPropertyValue("multiValueMap[1]", inputValue);
|
||||
|
||||
assertThat(holder.getMultiValueMap().keySet().iterator().next()).isEqualTo(1);
|
||||
assertThat(holder.getMultiValueMap().values().iterator().next().get(0)).isEqualTo(Long.valueOf(10));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testComplexMultiValueMapEntryWithCollectionConversion() {
|
||||
Set<String> inputValue = new HashSet<>();
|
||||
inputValue.add("10");
|
||||
|
||||
ComplexMapHolder holder = new ComplexMapHolder();
|
||||
BeanWrapper bw = new BeanWrapperImpl(holder);
|
||||
bw.setPropertyValue("multiValueMap[1]", inputValue);
|
||||
|
||||
assertThat(holder.getMultiValueMap().keySet().iterator().next()).isEqualTo(1);
|
||||
assertThat(holder.getMultiValueMap().values().iterator().next().get(0)).isEqualTo(Long.valueOf(10));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testComplexMultiValueMapEntryWithPlainValue() {
|
||||
String inputValue = "10";
|
||||
|
||||
ComplexMapHolder holder = new ComplexMapHolder();
|
||||
BeanWrapper bw = new BeanWrapperImpl(holder);
|
||||
bw.setPropertyValue("multiValueMap[1]", inputValue);
|
||||
|
||||
assertThat(holder.getMultiValueMap().keySet().iterator().next()).isEqualTo(1);
|
||||
assertThat(holder.getMultiValueMap().values().iterator().next().get(0)).isEqualTo(Long.valueOf(10));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGenericallyTypedIntegerBean() {
|
||||
GenericIntegerBean gb = new GenericIntegerBean();
|
||||
@@ -585,6 +649,8 @@ class BeanWrapperGenericsTests {
|
||||
|
||||
private DerivedMap derivedIndexedMap = new DerivedMap();
|
||||
|
||||
private MultiValueMap<Integer, Long> multiValueMap = new LinkedMultiValueMap<>();
|
||||
|
||||
public void setGenericMap(Map<List<Integer>, List<Long>> genericMap) {
|
||||
this.genericMap = genericMap;
|
||||
}
|
||||
@@ -608,6 +674,14 @@ class BeanWrapperGenericsTests {
|
||||
public DerivedMap getDerivedIndexedMap() {
|
||||
return derivedIndexedMap;
|
||||
}
|
||||
|
||||
public void setMultiValueMap(MultiValueMap<Integer, Long> multiValueMap) {
|
||||
this.multiValueMap = multiValueMap;
|
||||
}
|
||||
|
||||
public MultiValueMap<Integer, Long> getMultiValueMap() {
|
||||
return multiValueMap;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+49
-37
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,7 +16,10 @@
|
||||
|
||||
package org.springframework.beans.factory.config;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.Properties;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -27,8 +30,10 @@ import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.beans.testfixture.beans.TestBean;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
import static org.springframework.beans.factory.support.BeanDefinitionBuilder.genericBeanDefinition;
|
||||
import static org.springframework.beans.factory.support.BeanDefinitionBuilder.rootBeanDefinition;
|
||||
import static org.springframework.beans.factory.support.BeanDefinitionReaderUtils.registerWithGeneratedName;
|
||||
@@ -37,46 +42,42 @@ import static org.springframework.beans.factory.support.BeanDefinitionReaderUtil
|
||||
* Unit tests for {@link PropertyPlaceholderConfigurer}.
|
||||
*
|
||||
* @author Chris Beams
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public class PropertyPlaceholderConfigurerTests {
|
||||
class PropertyPlaceholderConfigurerTests {
|
||||
|
||||
private static final String P1 = "p1";
|
||||
private static final String P1_LOCAL_PROPS_VAL = "p1LocalPropsVal";
|
||||
private static final String P1_SYSTEM_PROPS_VAL = "p1SystemPropsVal";
|
||||
|
||||
private DefaultListableBeanFactory bf;
|
||||
private PropertyPlaceholderConfigurer ppc;
|
||||
private Properties ppcProperties;
|
||||
private final DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||
|
||||
private AbstractBeanDefinition p1BeanDef;
|
||||
private final PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer();
|
||||
|
||||
private final Properties ppcProperties = new Properties();
|
||||
|
||||
private AbstractBeanDefinition p1BeanDef = rootBeanDefinition(TestBean.class)
|
||||
.addPropertyValue("name", "${" + P1 + "}")
|
||||
.getBeanDefinition();
|
||||
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
p1BeanDef = rootBeanDefinition(TestBean.class)
|
||||
.addPropertyValue("name", "${" + P1 + "}")
|
||||
.getBeanDefinition();
|
||||
|
||||
bf = new DefaultListableBeanFactory();
|
||||
|
||||
ppcProperties = new Properties();
|
||||
void setup() {
|
||||
ppcProperties.setProperty(P1, P1_LOCAL_PROPS_VAL);
|
||||
System.setProperty(P1, P1_SYSTEM_PROPS_VAL);
|
||||
ppc = new PropertyPlaceholderConfigurer();
|
||||
ppc.setProperties(ppcProperties);
|
||||
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void cleanup() {
|
||||
void cleanup() {
|
||||
System.clearProperty(P1);
|
||||
System.clearProperty(P1_SYSTEM_PROPS_VAL);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void localPropertiesViaResource() {
|
||||
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||
void localPropertiesViaResource() {
|
||||
bf.registerBeanDefinition("testBean",
|
||||
genericBeanDefinition(TestBean.class)
|
||||
.addPropertyValue("name", "${my.name}")
|
||||
@@ -89,7 +90,7 @@ public class PropertyPlaceholderConfigurerTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveFromSystemProperties() {
|
||||
void resolveFromSystemProperties() {
|
||||
System.setProperty("otherKey", "systemValue");
|
||||
p1BeanDef = rootBeanDefinition(TestBean.class)
|
||||
.addPropertyValue("name", "${" + P1 + "}")
|
||||
@@ -104,7 +105,7 @@ public class PropertyPlaceholderConfigurerTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveFromLocalProperties() {
|
||||
void resolveFromLocalProperties() {
|
||||
System.clearProperty(P1);
|
||||
registerWithGeneratedName(p1BeanDef, bf);
|
||||
ppc.postProcessBeanFactory(bf);
|
||||
@@ -113,7 +114,7 @@ public class PropertyPlaceholderConfigurerTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setSystemPropertiesMode_defaultIsFallback() {
|
||||
void setSystemPropertiesMode_defaultIsFallback() {
|
||||
registerWithGeneratedName(p1BeanDef, bf);
|
||||
ppc.postProcessBeanFactory(bf);
|
||||
TestBean bean = bf.getBean(TestBean.class);
|
||||
@@ -121,7 +122,7 @@ public class PropertyPlaceholderConfigurerTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setSystemSystemPropertiesMode_toOverride_andResolveFromSystemProperties() {
|
||||
void setSystemSystemPropertiesMode_toOverride_andResolveFromSystemProperties() {
|
||||
registerWithGeneratedName(p1BeanDef, bf);
|
||||
ppc.setSystemPropertiesMode(PropertyPlaceholderConfigurer.SYSTEM_PROPERTIES_MODE_OVERRIDE);
|
||||
ppc.postProcessBeanFactory(bf);
|
||||
@@ -130,7 +131,7 @@ public class PropertyPlaceholderConfigurerTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setSystemSystemPropertiesMode_toOverride_andSetSearchSystemEnvironment_toFalse() {
|
||||
void setSystemSystemPropertiesMode_toOverride_andSetSearchSystemEnvironment_toFalse() {
|
||||
registerWithGeneratedName(p1BeanDef, bf);
|
||||
System.clearProperty(P1); // will now fall all the way back to system environment
|
||||
ppc.setSearchSystemEnvironment(false);
|
||||
@@ -145,7 +146,7 @@ public class PropertyPlaceholderConfigurerTests {
|
||||
* settings regarding resolving properties from the environment.
|
||||
*/
|
||||
@Test
|
||||
public void twoPlaceholderConfigurers_withConflictingSettings() {
|
||||
void twoPlaceholderConfigurers_withConflictingSettings() {
|
||||
String P2 = "p2";
|
||||
String P2_LOCAL_PROPS_VAL = "p2LocalPropsVal";
|
||||
String P2_SYSTEM_PROPS_VAL = "p2SystemPropsVal";
|
||||
@@ -184,12 +185,10 @@ public class PropertyPlaceholderConfigurerTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void customPlaceholderPrefixAndSuffix() {
|
||||
PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer();
|
||||
void customPlaceholderPrefixAndSuffix() {
|
||||
ppc.setPlaceholderPrefix("@<");
|
||||
ppc.setPlaceholderSuffix(">");
|
||||
|
||||
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||
bf.registerBeanDefinition("testBean",
|
||||
rootBeanDefinition(TestBean.class)
|
||||
.addPropertyValue("name", "@<key1>")
|
||||
@@ -207,11 +206,9 @@ public class PropertyPlaceholderConfigurerTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nullValueIsPreserved() {
|
||||
PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer();
|
||||
void nullValueIsPreserved() {
|
||||
ppc.setNullValue("customNull");
|
||||
System.setProperty("my.name", "customNull");
|
||||
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||
bf.registerBeanDefinition("testBean", rootBeanDefinition(TestBean.class)
|
||||
.addPropertyValue("name", "${my.name}")
|
||||
.getBeanDefinition());
|
||||
@@ -221,10 +218,8 @@ public class PropertyPlaceholderConfigurerTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void trimValuesIsOffByDefault() {
|
||||
PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer();
|
||||
void trimValuesIsOffByDefault() {
|
||||
System.setProperty("my.name", " myValue ");
|
||||
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||
bf.registerBeanDefinition("testBean", rootBeanDefinition(TestBean.class)
|
||||
.addPropertyValue("name", "${my.name}")
|
||||
.getBeanDefinition());
|
||||
@@ -234,11 +229,9 @@ public class PropertyPlaceholderConfigurerTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void trimValuesIsApplied() {
|
||||
PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer();
|
||||
void trimValuesIsApplied() {
|
||||
ppc.setTrimValues(true);
|
||||
System.setProperty("my.name", " myValue ");
|
||||
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||
bf.registerBeanDefinition("testBean", rootBeanDefinition(TestBean.class)
|
||||
.addPropertyValue("name", "${my.name}")
|
||||
.getBeanDefinition());
|
||||
@@ -247,4 +240,23 @@ public class PropertyPlaceholderConfigurerTests {
|
||||
System.clearProperty("my.name");
|
||||
}
|
||||
|
||||
/**
|
||||
* This test effectively verifies that the internal 'constants' map is properly
|
||||
* configured for all SYSTEM_PROPERTIES_MODE_ constants defined in
|
||||
* {@link PropertyPlaceholderConfigurer}.
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
void setSystemPropertiesModeNameToAllSupportedValues() {
|
||||
streamSystemPropertiesModeConstants()
|
||||
.map(Field::getName)
|
||||
.forEach(name -> assertThatNoException().as(name).isThrownBy(() -> ppc.setSystemPropertiesModeName(name)));
|
||||
}
|
||||
|
||||
private static Stream<Field> streamSystemPropertiesModeConstants() {
|
||||
return Arrays.stream(PropertyPlaceholderConfigurer.class.getFields())
|
||||
.filter(ReflectionUtils::isPublicStaticFinal)
|
||||
.filter(field -> field.getName().startsWith("SYSTEM_PROPERTIES_MODE_"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -24,7 +24,7 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.yaml.snakeyaml.constructor.ConstructorException;
|
||||
import org.yaml.snakeyaml.composer.ComposerException;
|
||||
import org.yaml.snakeyaml.parser.ParserException;
|
||||
import org.yaml.snakeyaml.scanner.ScannerException;
|
||||
|
||||
@@ -156,9 +156,9 @@ class YamlProcessorTests {
|
||||
void customTypeNotSupportedByDefault() throws Exception {
|
||||
URL url = new URL("https://localhost:9000/");
|
||||
setYaml("value: !!java.net.URL [\"" + url + "\"]");
|
||||
assertThatExceptionOfType(ConstructorException.class)
|
||||
assertThatExceptionOfType(ComposerException.class)
|
||||
.isThrownBy(() -> this.processor.process((properties, map) -> {}))
|
||||
.withMessageContaining("Unsupported type encountered in YAML document: java.net.URL");
|
||||
.withMessageContaining("Global tag is not allowed: tag:yaml.org,2002:java.net.URL");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -180,9 +180,9 @@ class YamlProcessorTests {
|
||||
|
||||
setYaml("value: !!java.net.URL [\"https://localhost:9000/\"]");
|
||||
|
||||
assertThatExceptionOfType(ConstructorException.class)
|
||||
assertThatExceptionOfType(ComposerException.class)
|
||||
.isThrownBy(() -> this.processor.process((properties, map) -> {}))
|
||||
.withMessageContaining("Unsupported type encountered in YAML document: java.net.URL");
|
||||
.withMessageContaining("Global tag is not allowed: tag:yaml.org,2002:java.net.URL");
|
||||
}
|
||||
|
||||
private void setYaml(String yaml) {
|
||||
|
||||
+65
-45
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,7 +16,9 @@
|
||||
|
||||
package org.springframework.beans.factory.xml;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.xml.sax.InputSource;
|
||||
@@ -29,107 +31,102 @@ import org.springframework.beans.testfixture.beans.TestBean;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.InputStreamResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
|
||||
/**
|
||||
* Tests for {@link XmlBeanDefinitionReader}.
|
||||
*
|
||||
* @author Rick Evans
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
public class XmlBeanDefinitionReaderTests {
|
||||
class XmlBeanDefinitionReaderTests {
|
||||
|
||||
private final SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry();
|
||||
|
||||
private final XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(registry);
|
||||
|
||||
|
||||
@Test
|
||||
public void setParserClassSunnyDay() {
|
||||
SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry();
|
||||
new XmlBeanDefinitionReader(registry).setDocumentReaderClass(DefaultBeanDefinitionDocumentReader.class);
|
||||
void setReaderClass() {
|
||||
assertThatNoException().isThrownBy(() -> reader.setDocumentReaderClass(DefaultBeanDefinitionDocumentReader.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withOpenInputStream() {
|
||||
SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry();
|
||||
void withInputStreamResourceWithoutExplicitValidationMode() {
|
||||
Resource resource = new InputStreamResource(getClass().getResourceAsStream("test.xml"));
|
||||
assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() ->
|
||||
new XmlBeanDefinitionReader(registry).loadBeanDefinitions(resource));
|
||||
assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() -> reader.loadBeanDefinitions(resource));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withOpenInputStreamAndExplicitValidationMode() {
|
||||
SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry();
|
||||
void withInputStreamResourceAndExplicitValidationMode() {
|
||||
Resource resource = new InputStreamResource(getClass().getResourceAsStream("test.xml"));
|
||||
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(registry);
|
||||
reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_DTD);
|
||||
reader.loadBeanDefinitions(resource);
|
||||
testBeanDefinitions(registry);
|
||||
assertBeanDefinitions(registry);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withImport() {
|
||||
SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry();
|
||||
void withImport() {
|
||||
Resource resource = new ClassPathResource("import.xml", getClass());
|
||||
new XmlBeanDefinitionReader(registry).loadBeanDefinitions(resource);
|
||||
testBeanDefinitions(registry);
|
||||
reader.loadBeanDefinitions(resource);
|
||||
assertBeanDefinitions(registry);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withWildcardImport() {
|
||||
SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry();
|
||||
void withWildcardImport() {
|
||||
Resource resource = new ClassPathResource("importPattern.xml", getClass());
|
||||
new XmlBeanDefinitionReader(registry).loadBeanDefinitions(resource);
|
||||
testBeanDefinitions(registry);
|
||||
reader.loadBeanDefinitions(resource);
|
||||
assertBeanDefinitions(registry);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withInputSource() {
|
||||
SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry();
|
||||
void withInputSourceWithoutExplicitValidationMode() {
|
||||
InputSource resource = new InputSource(getClass().getResourceAsStream("test.xml"));
|
||||
assertThatExceptionOfType(BeanDefinitionStoreException.class).isThrownBy(() ->
|
||||
new XmlBeanDefinitionReader(registry).loadBeanDefinitions(resource));
|
||||
assertThatExceptionOfType(BeanDefinitionStoreException.class)
|
||||
.isThrownBy(() -> reader.loadBeanDefinitions(resource))
|
||||
.withMessageStartingWith("Unable to determine validation mode for [resource loaded through SAX InputSource]:");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withInputSourceAndExplicitValidationMode() {
|
||||
SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry();
|
||||
void withInputSourceAndExplicitValidationMode() {
|
||||
InputSource resource = new InputSource(getClass().getResourceAsStream("test.xml"));
|
||||
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(registry);
|
||||
reader.setValidationMode(XmlBeanDefinitionReader.VALIDATION_DTD);
|
||||
reader.loadBeanDefinitions(resource);
|
||||
testBeanDefinitions(registry);
|
||||
assertBeanDefinitions(registry);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withFreshInputStream() {
|
||||
SimpleBeanDefinitionRegistry registry = new SimpleBeanDefinitionRegistry();
|
||||
void withClassPathResource() {
|
||||
Resource resource = new ClassPathResource("test.xml", getClass());
|
||||
new XmlBeanDefinitionReader(registry).loadBeanDefinitions(resource);
|
||||
testBeanDefinitions(registry);
|
||||
reader.loadBeanDefinitions(resource);
|
||||
assertBeanDefinitions(registry);
|
||||
}
|
||||
|
||||
private void testBeanDefinitions(BeanDefinitionRegistry registry) {
|
||||
private void assertBeanDefinitions(BeanDefinitionRegistry registry) {
|
||||
assertThat(registry.getBeanDefinitionCount()).isEqualTo(24);
|
||||
assertThat(registry.getBeanDefinitionNames()).hasSize(24);
|
||||
assertThat(Arrays.asList(registry.getBeanDefinitionNames()).contains("rod")).isTrue();
|
||||
assertThat(Arrays.asList(registry.getBeanDefinitionNames()).contains("aliased")).isTrue();
|
||||
assertThat(registry.getBeanDefinitionNames()).contains("rod", "aliased");
|
||||
assertThat(registry.containsBeanDefinition("rod")).isTrue();
|
||||
assertThat(registry.containsBeanDefinition("aliased")).isTrue();
|
||||
assertThat(registry.getBeanDefinition("rod").getBeanClassName()).isEqualTo(TestBean.class.getName());
|
||||
assertThat(registry.getBeanDefinition("aliased").getBeanClassName()).isEqualTo(TestBean.class.getName());
|
||||
assertThat(registry.isAlias("youralias")).isTrue();
|
||||
String[] aliases = registry.getAliases("aliased");
|
||||
assertThat(aliases).hasSize(2);
|
||||
assertThat(ObjectUtils.containsElement(aliases, "myalias")).isTrue();
|
||||
assertThat(ObjectUtils.containsElement(aliases, "youralias")).isTrue();
|
||||
assertThat(registry.getAliases("aliased")).containsExactly("myalias", "youralias");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void dtdValidationAutodetect() {
|
||||
void dtdValidationAutodetect() {
|
||||
doTestValidation("validateWithDtd.xml");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void xsdValidationAutodetect() {
|
||||
void xsdValidationAutodetect() {
|
||||
doTestValidation("validateWithXsd.xml");
|
||||
}
|
||||
|
||||
@@ -137,8 +134,31 @@ public class XmlBeanDefinitionReaderTests {
|
||||
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
|
||||
Resource resource = new ClassPathResource(resourceName, getClass());
|
||||
new XmlBeanDefinitionReader(factory).loadBeanDefinitions(resource);
|
||||
TestBean bean = (TestBean) factory.getBean("testBean");
|
||||
assertThat(bean).isNotNull();
|
||||
assertThat((TestBean) factory.getBean("testBean")).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void setValidationModeNameToUnsupportedValues() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> reader.setValidationModeName(null));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> reader.setValidationModeName(" "));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> reader.setValidationModeName("bogus"));
|
||||
}
|
||||
|
||||
/**
|
||||
* This test effectively verifies that the internal 'constants' map is properly
|
||||
* configured for all VALIDATION_ constants defined in {@link XmlBeanDefinitionReader}.
|
||||
*/
|
||||
@Test
|
||||
void setValidationModeNameToAllSupportedValues() {
|
||||
streamValidationModeConstants()
|
||||
.map(Field::getName)
|
||||
.forEach(name -> assertThatNoException().as(name).isThrownBy(() -> reader.setValidationModeName(name)));
|
||||
}
|
||||
|
||||
private static Stream<Field> streamValidationModeConstants() {
|
||||
return Arrays.stream(XmlBeanDefinitionReader.class.getFields())
|
||||
.filter(ReflectionUtils::isPublicStaticFinal)
|
||||
.filter(field -> field.getName().startsWith("VALIDATION_"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.beans.factory.aot
|
||||
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.assertj.core.api.ThrowingConsumer
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.aot.hint.*
|
||||
import org.springframework.aot.test.generate.TestGenerationContext
|
||||
import org.springframework.beans.factory.config.BeanDefinition
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory
|
||||
import org.springframework.beans.factory.support.InstanceSupplier
|
||||
import org.springframework.beans.factory.support.RegisteredBean
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition
|
||||
import org.springframework.beans.testfixture.beans.KotlinTestBean
|
||||
import org.springframework.beans.testfixture.beans.KotlinTestBeanWithOptionalParameter
|
||||
import org.springframework.beans.testfixture.beans.factory.aot.DeferredTypeBuilder
|
||||
import org.springframework.core.test.tools.Compiled
|
||||
import org.springframework.core.test.tools.TestCompiler
|
||||
import org.springframework.javapoet.MethodSpec
|
||||
import org.springframework.javapoet.ParameterizedTypeName
|
||||
import org.springframework.javapoet.TypeSpec
|
||||
import java.util.function.BiConsumer
|
||||
import java.util.function.Supplier
|
||||
import javax.lang.model.element.Modifier
|
||||
|
||||
/**
|
||||
* Kotlin tests for [InstanceSupplierCodeGenerator].
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
class InstanceSupplierCodeGeneratorKotlinTests {
|
||||
|
||||
private val generationContext = TestGenerationContext()
|
||||
|
||||
@Test
|
||||
fun generateWhenHasDefaultConstructor() {
|
||||
val beanDefinition: BeanDefinition = RootBeanDefinition(KotlinTestBean::class.java)
|
||||
val beanFactory = DefaultListableBeanFactory()
|
||||
compile(beanFactory, beanDefinition) { instanceSupplier, compiled ->
|
||||
val bean = getBean<KotlinTestBean>(beanFactory, beanDefinition, instanceSupplier)
|
||||
Assertions.assertThat(bean).isInstanceOf(KotlinTestBean::class.java)
|
||||
Assertions.assertThat(compiled.sourceFile).contains("InstanceSupplier.using(KotlinTestBean::new)")
|
||||
}
|
||||
Assertions.assertThat(getReflectionHints().getTypeHint(KotlinTestBean::class.java))
|
||||
.satisfies(hasConstructorWithMode(ExecutableMode.INTROSPECT))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generateWhenConstructorHasOptionalParameter() {
|
||||
val beanDefinition: BeanDefinition = RootBeanDefinition(KotlinTestBeanWithOptionalParameter::class.java)
|
||||
val beanFactory = DefaultListableBeanFactory()
|
||||
compile(beanFactory, beanDefinition) { instanceSupplier, compiled ->
|
||||
val bean: KotlinTestBeanWithOptionalParameter = getBean(beanFactory, beanDefinition, instanceSupplier)
|
||||
Assertions.assertThat(bean).isInstanceOf(KotlinTestBeanWithOptionalParameter::class.java)
|
||||
Assertions.assertThat(compiled.sourceFile)
|
||||
.contains("return BeanInstanceSupplier.<KotlinTestBeanWithOptionalParameter>forConstructor();")
|
||||
}
|
||||
Assertions.assertThat<TypeHint>(getReflectionHints().getTypeHint(KotlinTestBeanWithOptionalParameter::class.java))
|
||||
.satisfies(hasMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS))
|
||||
}
|
||||
|
||||
private fun getReflectionHints(): ReflectionHints {
|
||||
return generationContext.runtimeHints.reflection()
|
||||
}
|
||||
|
||||
private fun hasConstructorWithMode(mode: ExecutableMode): ThrowingConsumer<TypeHint> {
|
||||
return ThrowingConsumer {
|
||||
Assertions.assertThat(it.constructors()).anySatisfy(hasMode(mode))
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasMemberCategory(category: MemberCategory): ThrowingConsumer<TypeHint> {
|
||||
return ThrowingConsumer {
|
||||
Assertions.assertThat(it.memberCategories).contains(category)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasMode(mode: ExecutableMode): ThrowingConsumer<ExecutableHint> {
|
||||
return ThrowingConsumer {
|
||||
Assertions.assertThat(it.mode).isEqualTo(mode)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <T> getBean(beanFactory: DefaultListableBeanFactory, beanDefinition: BeanDefinition,
|
||||
instanceSupplier: InstanceSupplier<*>): T {
|
||||
(beanDefinition as RootBeanDefinition).instanceSupplier = instanceSupplier
|
||||
beanFactory.registerBeanDefinition("testBean", beanDefinition)
|
||||
return beanFactory.getBean("testBean") as T
|
||||
}
|
||||
|
||||
private fun compile(beanFactory: DefaultListableBeanFactory, beanDefinition: BeanDefinition,
|
||||
result: BiConsumer<InstanceSupplier<*>, Compiled>) {
|
||||
|
||||
val freshBeanFactory = DefaultListableBeanFactory(beanFactory)
|
||||
freshBeanFactory.registerBeanDefinition("testBean", beanDefinition)
|
||||
val registeredBean = RegisteredBean.of(freshBeanFactory, "testBean")
|
||||
val typeBuilder = DeferredTypeBuilder()
|
||||
val generateClass = generationContext.generatedClasses.addForFeature("TestCode", typeBuilder)
|
||||
val generator = InstanceSupplierCodeGenerator(
|
||||
generationContext, generateClass.name,
|
||||
generateClass.methods, false
|
||||
)
|
||||
val constructorOrFactoryMethod = registeredBean.resolveConstructorOrFactoryMethod()
|
||||
Assertions.assertThat(constructorOrFactoryMethod).isNotNull()
|
||||
val generatedCode = generator.generateCode(registeredBean, constructorOrFactoryMethod)
|
||||
typeBuilder.set { type: TypeSpec.Builder ->
|
||||
type.addModifiers(Modifier.PUBLIC)
|
||||
type.addSuperinterface(
|
||||
ParameterizedTypeName.get(
|
||||
Supplier::class.java,
|
||||
InstanceSupplier::class.java
|
||||
)
|
||||
)
|
||||
type.addMethod(
|
||||
MethodSpec.methodBuilder("get")
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.returns(InstanceSupplier::class.java)
|
||||
.addStatement("return \$L", generatedCode).build()
|
||||
)
|
||||
}
|
||||
generationContext.writeGeneratedContent()
|
||||
TestCompiler.forSystem().with(generationContext).compile {
|
||||
result.accept(it.getInstance(Supplier::class.java).get() as InstanceSupplier<*>, it)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+30
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,8 +17,10 @@
|
||||
package org.springframework.beans.testfixture.beans;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -47,6 +49,8 @@ public class IndexedTestBean {
|
||||
|
||||
private SortedMap sortedMap;
|
||||
|
||||
private MyTestBeans myTestBeans;
|
||||
|
||||
|
||||
public IndexedTestBean() {
|
||||
this(true);
|
||||
@@ -71,6 +75,7 @@ public class IndexedTestBean {
|
||||
TestBean tb8 = new TestBean("name8", 0);
|
||||
TestBean tbX = new TestBean("nameX", 0);
|
||||
TestBean tbY = new TestBean("nameY", 0);
|
||||
TestBean tbZ = new TestBean("nameZ", 0);
|
||||
this.array = new TestBean[] {tb0, tb1};
|
||||
this.list = new ArrayList<>();
|
||||
this.list.add(tb2);
|
||||
@@ -87,6 +92,7 @@ public class IndexedTestBean {
|
||||
list.add(tbY);
|
||||
this.map.put("key4", list);
|
||||
this.map.put("key5[foo]", tb8);
|
||||
this.myTestBeans = new MyTestBeans(tbZ);
|
||||
}
|
||||
|
||||
|
||||
@@ -146,4 +152,27 @@ public class IndexedTestBean {
|
||||
this.sortedMap = sortedMap;
|
||||
}
|
||||
|
||||
public MyTestBeans getMyTestBeans() {
|
||||
return myTestBeans;
|
||||
}
|
||||
|
||||
public void setMyTestBeans(MyTestBeans myTestBeans) {
|
||||
this.myTestBeans = myTestBeans;
|
||||
}
|
||||
|
||||
|
||||
public static class MyTestBeans implements Iterable<TestBean> {
|
||||
|
||||
private final Collection<TestBean> testBeans;
|
||||
|
||||
public MyTestBeans(TestBean... testBeans) {
|
||||
this.testBeans = Arrays.asList(testBeans);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<TestBean> iterator() {
|
||||
return this.testBeans.iterator();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.beans.testfixture.beans
|
||||
|
||||
class KotlinTestBean
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.beans.testfixture.beans
|
||||
|
||||
class KotlinTestBeanWithOptionalParameter(private val other: KotlinTestBean = KotlinTestBean())
|
||||
+2
@@ -42,7 +42,9 @@ import javax.lang.model.element.TypeElement;
|
||||
* @author Stephane Nicoll
|
||||
* @author Juergen Hoeller
|
||||
* @since 5.0
|
||||
* @deprecated as of 6.1, in favor of the AOT engine.
|
||||
*/
|
||||
@Deprecated(since = "6.1", forRemoval = true)
|
||||
public class CandidateComponentsIndexer implements Processor {
|
||||
|
||||
private MetadataStore metadataStore;
|
||||
|
||||
+2
@@ -231,12 +231,14 @@ class CandidateComponentsIndexerTests {
|
||||
assertThat(metadata.getItems()).hasSize(1);
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
private CandidateComponentsMetadata compile(Class<?>... types) {
|
||||
CandidateComponentsIndexer processor = new CandidateComponentsIndexer();
|
||||
this.compiler.getTask(types).call(processor);
|
||||
return readGeneratedMetadata(this.compiler.getOutputLocation());
|
||||
}
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
private CandidateComponentsMetadata compile(String... types) {
|
||||
CandidateComponentsIndexer processor = new CandidateComponentsIndexer();
|
||||
this.compiler.getTask(types).call(processor);
|
||||
|
||||
+15
-4
@@ -30,7 +30,6 @@ import org.quartz.impl.triggers.CronTriggerImpl;
|
||||
import org.springframework.beans.factory.BeanNameAware;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.core.Constants;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -48,6 +47,7 @@ import org.springframework.util.Assert;
|
||||
* instead of registering the JobDetail separately.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 3.1
|
||||
* @see #setName
|
||||
* @see #setGroup
|
||||
@@ -58,8 +58,16 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public class CronTriggerFactoryBean implements FactoryBean<CronTrigger>, BeanNameAware, InitializingBean {
|
||||
|
||||
/** Constants for the CronTrigger class. */
|
||||
private static final Constants constants = new Constants(CronTrigger.class);
|
||||
/**
|
||||
* Map of constant names to constant values for the misfire instruction constants
|
||||
* defined in {@link org.quartz.Trigger} and {@link org.quartz.CronTrigger}.
|
||||
*/
|
||||
private static final Map<String, Integer> constants = Map.of(
|
||||
"MISFIRE_INSTRUCTION_SMART_POLICY", CronTrigger.MISFIRE_INSTRUCTION_SMART_POLICY,
|
||||
"MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY", CronTrigger.MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY,
|
||||
"MISFIRE_INSTRUCTION_FIRE_ONCE_NOW", CronTrigger.MISFIRE_INSTRUCTION_FIRE_ONCE_NOW,
|
||||
"MISFIRE_INSTRUCTION_DO_NOTHING", CronTrigger.MISFIRE_INSTRUCTION_DO_NOTHING
|
||||
);
|
||||
|
||||
|
||||
@Nullable
|
||||
@@ -213,7 +221,10 @@ public class CronTriggerFactoryBean implements FactoryBean<CronTrigger>, BeanNam
|
||||
* @see org.quartz.CronTrigger#MISFIRE_INSTRUCTION_DO_NOTHING
|
||||
*/
|
||||
public void setMisfireInstructionName(String constantName) {
|
||||
this.misfireInstruction = constants.asNumber(constantName).intValue();
|
||||
Assert.hasText(constantName, "'constantName' must not be null or blank");
|
||||
Integer misfireInstruction = constants.get(constantName);
|
||||
Assert.notNull(misfireInstruction, "Only misfire instruction constants allowed");
|
||||
this.misfireInstruction = misfireInstruction;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -77,12 +77,6 @@ public class SimpleThreadPoolTaskExecutor extends SimpleThreadPool
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Override
|
||||
public void execute(Runnable task, long startTimeout) {
|
||||
execute(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Future<?> submit(Runnable task) {
|
||||
FutureTask<Object> future = new FutureTask<>(task, null);
|
||||
|
||||
+25
-5
@@ -28,7 +28,6 @@ import org.quartz.impl.triggers.SimpleTriggerImpl;
|
||||
import org.springframework.beans.factory.BeanNameAware;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.core.Constants;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -46,6 +45,7 @@ import org.springframework.util.Assert;
|
||||
* instead of registering the JobDetail separately.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 3.1
|
||||
* @see #setName
|
||||
* @see #setGroup
|
||||
@@ -56,9 +56,26 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public class SimpleTriggerFactoryBean implements FactoryBean<SimpleTrigger>, BeanNameAware, InitializingBean {
|
||||
|
||||
/** Constants for the SimpleTrigger class. */
|
||||
private static final Constants constants = new Constants(SimpleTrigger.class);
|
||||
|
||||
/**
|
||||
* Map of constant names to constant values for the misfire instruction constants
|
||||
* defined in {@link org.quartz.Trigger} and {@link org.quartz.SimpleTrigger}.
|
||||
*/
|
||||
private static final Map<String, Integer> constants = Map.of(
|
||||
"MISFIRE_INSTRUCTION_SMART_POLICY",
|
||||
SimpleTrigger.MISFIRE_INSTRUCTION_SMART_POLICY,
|
||||
"MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY",
|
||||
SimpleTrigger.MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY,
|
||||
"MISFIRE_INSTRUCTION_FIRE_NOW",
|
||||
SimpleTrigger.MISFIRE_INSTRUCTION_FIRE_NOW,
|
||||
"MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT",
|
||||
SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT,
|
||||
"MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT",
|
||||
SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT,
|
||||
"MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT",
|
||||
SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT,
|
||||
"MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT",
|
||||
SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT
|
||||
);
|
||||
|
||||
@Nullable
|
||||
private String name;
|
||||
@@ -204,7 +221,10 @@ public class SimpleTriggerFactoryBean implements FactoryBean<SimpleTrigger>, Bea
|
||||
* @see org.quartz.SimpleTrigger#MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT
|
||||
*/
|
||||
public void setMisfireInstructionName(String constantName) {
|
||||
this.misfireInstruction = constants.asNumber(constantName).intValue();
|
||||
Assert.hasText(constantName, "'constantName' must not be null or blank");
|
||||
Integer misfireInstruction = constants.get(constantName);
|
||||
Assert.notNull(misfireInstruction, "Only misfire instruction constants allowed");
|
||||
this.misfireInstruction = misfireInstruction;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-4
@@ -22,7 +22,6 @@ import org.quartz.spi.TriggerFiredBundle;
|
||||
import org.springframework.beans.BeanWrapper;
|
||||
import org.springframework.beans.MutablePropertyValues;
|
||||
import org.springframework.beans.PropertyAccessorFactory;
|
||||
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -87,9 +86,7 @@ public class SpringBeanJobFactory extends AdaptableJobFactory
|
||||
@Override
|
||||
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
|
||||
Object job = (this.applicationContext != null ?
|
||||
// to be replaced with createBean(Class) in 6.1
|
||||
this.applicationContext.getAutowireCapableBeanFactory().createBean(
|
||||
bundle.getJobDetail().getJobClass(), AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false) :
|
||||
this.applicationContext.getAutowireCapableBeanFactory().createBean(bundle.getJobDetail().getJobClass()) :
|
||||
super.createJobInstance(bundle));
|
||||
|
||||
if (isEligibleForPropertyPopulation(job)) {
|
||||
|
||||
+40
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,21 +16,33 @@
|
||||
|
||||
package org.springframework.scheduling.quartz;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.text.ParseException;
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.quartz.CronTrigger;
|
||||
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
|
||||
/**
|
||||
* Tests for {@link CronTriggerFactoryBean}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
public class CronTriggerFactoryBeanTests {
|
||||
class CronTriggerFactoryBeanTests {
|
||||
|
||||
private final CronTriggerFactoryBean factory = new CronTriggerFactoryBean();
|
||||
|
||||
|
||||
@Test
|
||||
public void createWithoutJobDetail() throws ParseException {
|
||||
CronTriggerFactoryBean factory = new CronTriggerFactoryBean();
|
||||
void createWithoutJobDetail() throws ParseException {
|
||||
factory.setName("myTrigger");
|
||||
factory.setCronExpression("0 15 10 ? * *");
|
||||
factory.afterPropertiesSet();
|
||||
@@ -38,4 +50,28 @@ public class CronTriggerFactoryBeanTests {
|
||||
assertThat(trigger.getCronExpression()).isEqualTo("0 15 10 ? * *");
|
||||
}
|
||||
|
||||
@Test
|
||||
void setMisfireInstructionNameToUnsupportedValues() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> factory.setMisfireInstructionName(null));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> factory.setMisfireInstructionName(" "));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> factory.setMisfireInstructionName("bogus"));
|
||||
}
|
||||
|
||||
/**
|
||||
* This test effectively verifies that the internal 'constants' map is properly
|
||||
* configured for all MISFIRE_INSTRUCTION_ constants defined in {@link CronTrigger}.
|
||||
*/
|
||||
@Test
|
||||
void setMisfireInstructionNameToAllSupportedValues() {
|
||||
streamMisfireInstructionConstants()
|
||||
.map(Field::getName)
|
||||
.forEach(name -> assertThatNoException().as(name).isThrownBy(() -> factory.setMisfireInstructionName(name)));
|
||||
}
|
||||
|
||||
private static Stream<Field> streamMisfireInstructionConstants() {
|
||||
return Arrays.stream(CronTrigger.class.getFields())
|
||||
.filter(ReflectionUtils::isPublicStaticFinal)
|
||||
.filter(field -> field.getName().startsWith("MISFIRE_INSTRUCTION_"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+40
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,21 +16,32 @@
|
||||
|
||||
package org.springframework.scheduling.quartz;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.quartz.SimpleTrigger;
|
||||
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
|
||||
/**
|
||||
* Tests for {@link SimpleTriggerFactoryBean}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
public class SimpleTriggerFactoryBeanTests {
|
||||
class SimpleTriggerFactoryBeanTests {
|
||||
|
||||
private final SimpleTriggerFactoryBean factory = new SimpleTriggerFactoryBean();
|
||||
|
||||
|
||||
@Test
|
||||
public void createWithoutJobDetail() throws ParseException {
|
||||
SimpleTriggerFactoryBean factory = new SimpleTriggerFactoryBean();
|
||||
void createWithoutJobDetail() {
|
||||
factory.setName("myTrigger");
|
||||
factory.setRepeatCount(5);
|
||||
factory.setRepeatInterval(1000L);
|
||||
@@ -40,4 +51,28 @@ public class SimpleTriggerFactoryBeanTests {
|
||||
assertThat(trigger.getRepeatInterval()).isEqualTo(1000L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void setMisfireInstructionNameToUnsupportedValues() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> factory.setMisfireInstructionName(null));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> factory.setMisfireInstructionName(" "));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> factory.setMisfireInstructionName("bogus"));
|
||||
}
|
||||
|
||||
/**
|
||||
* This test effectively verifies that the internal 'constants' map is properly
|
||||
* configured for all MISFIRE_INSTRUCTION_ constants defined in {@link SimpleTrigger}.
|
||||
*/
|
||||
@Test
|
||||
void setMisfireInstructionNameToAllSupportedValues() {
|
||||
streamMisfireInstructionConstants()
|
||||
.map(Field::getName)
|
||||
.forEach(name -> assertThatNoException().as(name).isThrownBy(() -> factory.setMisfireInstructionName(name)));
|
||||
}
|
||||
|
||||
private static Stream<Field> streamMisfireInstructionConstants() {
|
||||
return Arrays.stream(SimpleTrigger.class.getFields())
|
||||
.filter(ReflectionUtils::isPublicStaticFinal)
|
||||
.filter(field -> field.getName().startsWith("MISFIRE_INSTRUCTION_"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ dependencies {
|
||||
api(project(":spring-beans"))
|
||||
api(project(":spring-core"))
|
||||
api(project(":spring-expression"))
|
||||
api("io.micrometer:micrometer-observation")
|
||||
optional(project(":spring-instrument"))
|
||||
optional("jakarta.annotation:jakarta.annotation-api")
|
||||
optional("jakarta.ejb:jakarta.ejb-api")
|
||||
@@ -23,10 +24,12 @@ dependencies {
|
||||
optional("org.aspectj:aspectjweaver")
|
||||
optional("org.apache.groovy:groovy")
|
||||
optional("org.apache-extras.beanshell:bsh")
|
||||
optional("org.crac:crac")
|
||||
optional("org.hibernate:hibernate-validator")
|
||||
optional("org.jetbrains.kotlin:kotlin-reflect")
|
||||
optional("org.jetbrains.kotlin:kotlin-stdlib")
|
||||
optional("org.reactivestreams:reactive-streams")
|
||||
optional("io.projectreactor:reactor-core")
|
||||
testImplementation(project(":spring-core-test"))
|
||||
testImplementation(testFixtures(project(":spring-aop")))
|
||||
testImplementation(testFixtures(project(":spring-beans")))
|
||||
@@ -38,6 +41,10 @@ dependencies {
|
||||
testImplementation("org.apache.commons:commons-pool2")
|
||||
testImplementation("org.awaitility:awaitility")
|
||||
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
|
||||
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
|
||||
testImplementation("io.reactivex.rxjava3:rxjava")
|
||||
testImplementation('io.micrometer:context-propagation')
|
||||
testImplementation("io.micrometer:micrometer-observation-test")
|
||||
testRuntimeOnly("jakarta.xml.bind:jakarta.xml.bind-api")
|
||||
testRuntimeOnly("org.glassfish:jakarta.el")
|
||||
// Substitute for javax.management:jmxremote_optional:1.0.1_04 (not available on Maven Central)
|
||||
|
||||
+9
-12
@@ -400,13 +400,6 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
|
||||
// Check if we have a cached item matching the conditions
|
||||
Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
|
||||
|
||||
// Collect puts from any @Cacheable miss, if no cached item is found
|
||||
List<CachePutRequest> cachePutRequests = new ArrayList<>();
|
||||
if (cacheHit == null) {
|
||||
collectPutRequests(contexts.get(CacheableOperation.class),
|
||||
CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
|
||||
}
|
||||
|
||||
Object cacheValue;
|
||||
Object returnValue;
|
||||
|
||||
@@ -421,6 +414,12 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
|
||||
cacheValue = unwrapReturnValue(returnValue);
|
||||
}
|
||||
|
||||
// Collect puts from any @Cacheable miss, if no cached item is found
|
||||
List<CachePutRequest> cachePutRequests = new ArrayList<>();
|
||||
if (cacheHit == null) {
|
||||
collectPutRequests(contexts.get(CacheableOperation.class), cacheValue, cachePutRequests);
|
||||
}
|
||||
|
||||
// Collect any explicit @CachePuts
|
||||
collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
|
||||
|
||||
@@ -557,7 +556,7 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
|
||||
@Nullable Object result, Collection<CachePutRequest> putRequests) {
|
||||
|
||||
for (CacheOperationContext context : contexts) {
|
||||
if (isConditionPassing(context, result)) {
|
||||
if (isConditionPassing(context, result) && context.canPutToCache(result)) {
|
||||
Object key = generateKey(context, result);
|
||||
putRequests.add(new CachePutRequest(context, key));
|
||||
}
|
||||
@@ -831,10 +830,8 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
|
||||
}
|
||||
|
||||
public void apply(@Nullable Object result) {
|
||||
if (this.context.canPutToCache(result)) {
|
||||
for (Cache cache : this.context.getCaches()) {
|
||||
doPut(cache, this.key, result);
|
||||
}
|
||||
for (Cache cache : this.context.getCaches()) {
|
||||
doPut(cache, this.key, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -86,6 +86,7 @@ import org.springframework.util.ClassUtils;
|
||||
* @see ScannedGenericBeanDefinition
|
||||
* @see CandidateComponentsIndex
|
||||
*/
|
||||
@SuppressWarnings("removal") // components index
|
||||
public class ClassPathScanningCandidateComponentProvider implements EnvironmentCapable, ResourceLoaderAware {
|
||||
|
||||
static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user