mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
1046 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 71db005f81 | |||
| 59961960dd | |||
| 727f69ae70 | |||
| 92b6c5df4f | |||
| e109c55192 | |||
| 8f21c228b8 | |||
| a220c545fc | |||
| 99a50e7cea | |||
| 68ba55bea9 | |||
| bb85d9b5e4 | |||
| a678be80ea | |||
| 550f05c9dc | |||
| 5bcbcb3049 | |||
| fe7355d5b0 | |||
| a271d5ec15 | |||
| af13967e2a | |||
| ae8a353041 | |||
| 8f6c56fe9a | |||
| e63e3a6d28 | |||
| fa1dd74aff | |||
| 5dacf688b2 | |||
| e5f18798ca | |||
| 01f717375b | |||
| f2e898d35d | |||
| bd4b3866fc | |||
| c23795a8a6 | |||
| c598f0565a | |||
| e7cf54d4e2 | |||
| 2cc1ee78c1 | |||
| 1f1f222b8b | |||
| 74182cd3d6 | |||
| 1f8913a96c | |||
| 29e3acc887 | |||
| 5d454390f0 | |||
| 6f9546722a | |||
| 6548cee20a | |||
| abb296c7d2 | |||
| b6fdfe8ee3 | |||
| 345910591a | |||
| 9df735b3ab | |||
| c5c843696b | |||
| 39786e4790 | |||
| ed172d6269 | |||
| f51838b6ba | |||
| 19588d413d | |||
| dfd631e662 | |||
| 3099710087 | |||
| a199654a62 | |||
| bf5ce82254 | |||
| 66a571fe27 | |||
| fceed9f3e5 | |||
| 8d4ea72e4e | |||
| 5c7bf0c2fe | |||
| 4aeeacb4c2 | |||
| 38a9c2ceeb | |||
| fe8e6d3d5a | |||
| f83c609436 | |||
| 6de0be16c2 | |||
| ed83461021 | |||
| 41904d46ad | |||
| 0ebdd8cb98 | |||
| b082f546ec | |||
| 9728b8cefd | |||
| 6ca01e15cf | |||
| 588b5a45f8 | |||
| 436c7136e9 | |||
| 7305692005 | |||
| 0f36c5fd6a | |||
| 60d05d4204 | |||
| dfea3d05aa | |||
| 8dd857a84d | |||
| 0f945873a3 | |||
| 88ee8fc92f | |||
| 154aec7d75 | |||
| a11b8039c5 | |||
| f0b1133b12 | |||
| 91f65e63b4 | |||
| 3e424e3ce7 | |||
| d1dfac4e5d | |||
| c9e13575ca | |||
| 762d903aad | |||
| 47d760845b | |||
| ab5d46411a | |||
| 4df3eb4670 | |||
| 5a87f555f8 | |||
| 5fd356e44f | |||
| 6f38e90048 | |||
| b0d4931d5d | |||
| 4c621826bd | |||
| e0c4347cc6 | |||
| 53133d949d | |||
| a1ce5dac0b | |||
| da7ad71b7f | |||
| 94a49c4ed3 | |||
| bc6954255b | |||
| c3a0022c9e | |||
| b02e1bb935 | |||
| ac3a466607 | |||
| fefbeeeb18 | |||
| 244c069ea8 | |||
| 3686c457bb | |||
| 7b5effecf3 | |||
| e42902b742 | |||
| 05790e36db | |||
| 578fdbe97a | |||
| ada76bd7bf | |||
| b0aa004d9d | |||
| 9d768a89d2 | |||
| ab2ad746b5 | |||
| 125b8e7418 | |||
| 2a916a3869 | |||
| 2bc93710f3 | |||
| b110a39e88 | |||
| 734d113b11 | |||
| 0a7166234d | |||
| 552e7fb1d5 | |||
| 884975e094 | |||
| adb73389a8 | |||
| 7dc0653f38 | |||
| d39034754f | |||
| 070590cb11 | |||
| 4c45f37166 | |||
| fad70aa252 | |||
| 0c714e3a78 | |||
| b068742ec8 | |||
| 6597727c86 | |||
| 210b42b7d8 | |||
| 5fb567a37e | |||
| e8fd29091c | |||
| 7882d265c6 | |||
| 759e3d4aa6 | |||
| 9b50e7ded6 | |||
| 855fe39b7f | |||
| 7595465c21 | |||
| d6e167d16e | |||
| 23a0903b5c | |||
| a96aeb69c1 | |||
| 03650e7d58 | |||
| 0aa4dc11f1 | |||
| 81607238ce | |||
| 63a20be502 | |||
| acd34df0d5 | |||
| 093d6a1bec | |||
| 1ee18e74fc | |||
| 0d7c9b7c93 | |||
| 32f128b6ba | |||
| dc7179c528 | |||
| da1d00be7a | |||
| c93ae250f9 | |||
| 9133e61687 | |||
| 1d7cbfa415 | |||
| 376a8989f5 | |||
| 38392233ba | |||
| 64ff37f42c | |||
| f5a356c9c6 | |||
| d47c7f9552 | |||
| bcf11e8919 | |||
| af03fa7657 | |||
| bfd918d16c | |||
| 2d377155ab | |||
| 1f6fd16dae | |||
| 13e62d0b8e | |||
| c91841883c | |||
| 9e4b95ef3c | |||
| 9b44413f2d | |||
| 76c032cc11 | |||
| 3ccbe3d1cb | |||
| 411b355f2c | |||
| 499f57ebc5 | |||
| 349674ef5e | |||
| 2f43f77dd1 | |||
| f054c2e804 | |||
| 95453a4427 | |||
| 4e2d9252e5 | |||
| e239753a1b | |||
| ff104b6de0 | |||
| d189e169cc | |||
| 7c508a4cd3 | |||
| 9d7bd9520c | |||
| ce4a5b1f06 | |||
| 5c691960a2 | |||
| b67a381fbe | |||
| e1826d2322 | |||
| aaa0a2be63 | |||
| 71ba7bc5e0 | |||
| e18f1f5d9c | |||
| f9cffcce05 | |||
| 2d7bf83d8d | |||
| a6b0c75cae | |||
| e0c3b1ae1d | |||
| dac481da43 | |||
| 31a07e6fbf | |||
| ef01a078d3 | |||
| 44167ecea4 | |||
| 962f4d20b4 | |||
| 609580bfb9 | |||
| f6a09f3fad | |||
| 48878619d2 | |||
| 16cb296b35 | |||
| fd18c00e50 | |||
| e737e6d9ba | |||
| 78d8fac05b | |||
| 3efbbc5994 | |||
| fe2ef2ea6e | |||
| 35fc2df948 | |||
| 22fd6df711 | |||
| 8432f777aa | |||
| cde476f90e | |||
| 178cb429a8 | |||
| 49cafe07ed | |||
| 33454a4007 | |||
| d0fc6dd06d | |||
| 8efc7a958e | |||
| 32c0540424 | |||
| c8169e5cad | |||
| 9b3f4567ee | |||
| 352c7cd8b3 | |||
| 325edbec7d | |||
| 2a37284c86 | |||
| 95c43bb0ae | |||
| b91179d1b8 | |||
| 21613eabf1 | |||
| c564f27647 | |||
| cf0a48692c | |||
| 1f437add0b | |||
| 86e02a47fb | |||
| c809fda4ed | |||
| e7071c329a | |||
| 3c5dc19da3 | |||
| ecc0a6d2db | |||
| 5878a0741e | |||
| e1d0176faa | |||
| b2a86cc42d | |||
| f161bc798e | |||
| 471e4d2b20 | |||
| a20a74817e | |||
| 06f6b45248 | |||
| bb14dfa747 | |||
| 8695fadbc0 | |||
| 6e141cc23a | |||
| 5f581a9a68 | |||
| 8f6d24efde | |||
| 5d5cd17d33 | |||
| 67e3b1b531 | |||
| 35ba53e918 | |||
| 942800e8ec | |||
| ff069de6d7 | |||
| c6c61d6485 | |||
| bf2cdcb1f2 | |||
| 74175c1117 | |||
| 4695bd332a | |||
| d9b8826142 | |||
| d529eee889 | |||
| 26485b0e7d | |||
| 18813de69f | |||
| f95a1f49df | |||
| ac9ca412c8 | |||
| 9c175608e5 | |||
| 2b76c4d847 | |||
| 6712c044b1 | |||
| a55b50b512 | |||
| 2935ff8f97 | |||
| aedd909ef6 | |||
| d8523cb033 | |||
| 5e171d3cf5 | |||
| 5f893cadc5 | |||
| 8f7d61650d | |||
| c2bdc23b5e | |||
| 26da0e49e4 | |||
| 583d92e69a | |||
| 758dddce3b | |||
| 430cc2edc0 | |||
| 1e3099759e | |||
| 0e0c298dcf | |||
| baf367831f | |||
| 8f71bea553 | |||
| 94d9a71bb5 | |||
| 11abd8b04f | |||
| 74130d007b | |||
| 285c92bb03 | |||
| ee6998ba52 | |||
| 9583d18896 | |||
| 334b1548b7 | |||
| 3dcac0cf80 | |||
| ca118ca4a0 | |||
| e0fb777325 | |||
| 48e94f535c | |||
| 77067d0e6b | |||
| 1a137c2e61 | |||
| f43f1e12f1 | |||
| 34477b4f03 | |||
| d41f546c43 | |||
| e685ff0416 | |||
| 4bfbf7d3c8 | |||
| 279e6eb423 | |||
| eb65939341 | |||
| 3f79b267b1 | |||
| 5641d87ce8 | |||
| 24dd484471 | |||
| b43972bb54 | |||
| 17109b2402 | |||
| dbfece6c31 | |||
| 86a101ac2b | |||
| 1f544f113a | |||
| 3a38bb48b5 | |||
| 02d3269dbb | |||
| d81ddcef34 | |||
| 701c39a325 | |||
| d77595bf2c | |||
| 0b99872704 | |||
| b8b3e6d20c | |||
| ae3bc378d6 | |||
| 2ab1c5b387 | |||
| a2f52db452 | |||
| 8c2a39b5af | |||
| 02d003127f | |||
| 57f675c537 | |||
| d89e305c87 | |||
| 443e3d5fa6 | |||
| cf75a09011 | |||
| 45c20e34e4 | |||
| 9c74c25961 | |||
| 3228502108 | |||
| 94fe1f4c63 | |||
| 20d27e4fb6 | |||
| fd5b0e144d | |||
| 2c895974b2 | |||
| bfeca4a0bf | |||
| 4979d8fded | |||
| f9b6aed6b6 | |||
| 156546ad05 | |||
| 1a05ba3215 | |||
| 3bda2b7124 | |||
| 0b902f32f6 | |||
| fb6c325cc0 | |||
| c52bfc0586 | |||
| a33b14338f | |||
| 2ede74fa9c | |||
| 86ca6fee16 | |||
| 9ede1d07a0 | |||
| c65b0a199e | |||
| 1e73439955 | |||
| 566621f7e3 | |||
| f516431260 | |||
| b4c61f20e7 | |||
| 0c477f14cc | |||
| c38f053905 | |||
| 88c2a25f12 | |||
| dd76ed7a0a | |||
| 837e8960c2 | |||
| 8a6c52b018 | |||
| 7f561fb53d | |||
| c4896aca9b | |||
| 3b093754c8 | |||
| 8e16e5ea35 | |||
| dedb58f7ed | |||
| f4b5738869 | |||
| 21a007bb15 | |||
| d3d414c3c7 | |||
| 3c34e69cc2 | |||
| 526fc391ee | |||
| 96fd3c10fb | |||
| fe5560400c | |||
| 6090eb0b42 | |||
| 3804b1c602 | |||
| 40b33bca59 | |||
| 3253d2de89 | |||
| eaf54b54c3 | |||
| c596ff5c38 | |||
| cc90a956f7 | |||
| 6dbd684279 | |||
| faf3c7831f | |||
| 59b78cc513 | |||
| 14911fb32f | |||
| 5115684baf | |||
| 6630b16771 | |||
| d1d5b54f12 | |||
| 646fd3edcc | |||
| 9908967954 | |||
| 5fd3456f1e | |||
| 3758e5155c | |||
| 376f13f8ef | |||
| 04cce0bafd | |||
| b80872b762 | |||
| 8feb8198fe | |||
| c97def0b98 | |||
| 4e863c5a75 | |||
| a6ff95a69c | |||
| f7c3e6480a | |||
| d868f58e6e | |||
| 84b3335e71 | |||
| c3e18bc173 | |||
| c942c04aa0 | |||
| ad80b94e14 | |||
| 8513ec7440 | |||
| 667eb42a63 | |||
| bd23798323 | |||
| 961084dfe0 | |||
| 3e5aa8d734 | |||
| 1bfcaecc9b | |||
| eed14214b5 | |||
| 4b7d1e3a2c | |||
| 08a99e275e | |||
| 44d14811d3 | |||
| 900ee11f3b | |||
| 89b85c81a7 | |||
| 51aaaae94e | |||
| aa10d23de4 | |||
| 10610a6f54 | |||
| 5e26786bab | |||
| 3b1af692cc | |||
| b9ae996dfc | |||
| 2d50b758c4 | |||
| 450cc212a2 | |||
| a9d100eeee | |||
| dde8f4489f | |||
| f9f7a7cd78 | |||
| 6de95a2b37 | |||
| f00756bc7c | |||
| b00d120514 | |||
| 2dc4eea62f | |||
| db8fa4d505 | |||
| 525621c4d8 | |||
| 01e90bbd0e | |||
| af1c06917d | |||
| bcff7d74cd | |||
| 170d6bfdad | |||
| 2f7046f572 | |||
| 3b8dd0a5ac | |||
| 9da318af96 | |||
| b45bfcafc2 | |||
| 81181c346a | |||
| 1378cce9fb | |||
| 12f765c133 | |||
| 06c6af9b0d | |||
| 181c814e69 | |||
| 0eda44186a | |||
| 48eb477755 | |||
| c43d2e2edc | |||
| 0d4010841e | |||
| cc9278666d | |||
| c87925cee7 | |||
| 5b6c127283 | |||
| 4cd9e2e9b0 | |||
| 376223c87d | |||
| ce80637891 | |||
| 78d0dbb519 | |||
| 85704c890e | |||
| 7681200ee7 | |||
| 5e4ed68fdd | |||
| 49c4b205e4 | |||
| cf1ba98d01 | |||
| cb4222d2c2 | |||
| 3437e61f98 | |||
| 67e3d86bd8 | |||
| 37eaded63d | |||
| ccaccda6ca | |||
| 5ebbb3ff3e | |||
| 3c02ab83ed | |||
| 021161ea38 | |||
| 019c34f480 | |||
| b9ba0fc572 | |||
| 4a319c3c33 | |||
| 52c77d89e9 | |||
| 4786e2bf53 | |||
| b53034fe62 | |||
| ba46d2bf21 | |||
| 27f9473422 | |||
| f99faac073 | |||
| d65d285378 | |||
| efb6abc43f | |||
| 3d57425dcb | |||
| d4caaebab0 | |||
| 10cb2322e9 | |||
| 9571aa1c68 | |||
| 97810c84a2 | |||
| d0076f5c14 | |||
| c110644107 | |||
| 05956d4028 | |||
| b2ca36f098 | |||
| 15253f3448 | |||
| 4e00c988c1 | |||
| 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
|
||||
|
||||
|
||||
+19
-67
@@ -1,22 +1,24 @@
|
||||
plugins {
|
||||
id 'io.spring.nohttp' version '0.0.11'
|
||||
id 'io.freefair.aspectj' version '8.0.1' apply false
|
||||
id 'io.freefair.aspectj' version '8.3' apply false
|
||||
// kotlinVersion is managed in gradle.properties
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version "${kotlinVersion}" apply false
|
||||
id 'org.jetbrains.dokka' version '1.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 {
|
||||
moduleProjects = subprojects.findAll { it.name.startsWith("spring-") }
|
||||
javaProjects = subprojects - project(":framework-bom") - project(":framework-platform")
|
||||
javaProjects = subprojects.findAll { !it.name.startsWith("framework-") }
|
||||
}
|
||||
|
||||
configure(allprojects) { project ->
|
||||
apply plugin: "org.springframework.build.localdev"
|
||||
group = "org.springframework"
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
@@ -41,16 +43,7 @@ configure(allprojects) { project ->
|
||||
}
|
||||
}
|
||||
|
||||
configure([rootProject] + javaProjects) { project ->
|
||||
group = "org.springframework"
|
||||
|
||||
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"
|
||||
|
||||
configure(allprojects - project(":framework-platform")) {
|
||||
configurations {
|
||||
dependencyManagement {
|
||||
canBeConsumed = false
|
||||
@@ -59,36 +52,19 @@ 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.3"
|
||||
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")))
|
||||
}
|
||||
}
|
||||
|
||||
configure([rootProject] + javaProjects) { project ->
|
||||
apply plugin: "java"
|
||||
apply plugin: "java-test-fixtures"
|
||||
apply plugin: 'org.springframework.build.conventions'
|
||||
apply from: "${rootDir}/gradle/toolchains.gradle"
|
||||
apply from: "${rootDir}/gradle/ide.gradle"
|
||||
|
||||
dependencies {
|
||||
testImplementation("org.junit.jupiter:junit-jupiter-api")
|
||||
testImplementation("org.junit.jupiter:junit-jupiter-params")
|
||||
testImplementation("org.junit.platform:junit-platform-suite-api")
|
||||
@@ -106,7 +82,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.39")
|
||||
}
|
||||
|
||||
ext.javadocLinks = [
|
||||
@@ -127,7 +102,7 @@ configure([rootProject] + javaProjects) { project ->
|
||||
// TODO Uncomment link to JUnit 5 docs once we execute Gradle with Java 18+.
|
||||
// See https://github.com/spring-projects/spring-framework/issues/27497
|
||||
//
|
||||
// "https://junit.org/junit5/docs/5.9.3/api/",
|
||||
// "https://junit.org/junit5/docs/5.10.0/api/",
|
||||
"https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/",
|
||||
"https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/",
|
||||
"https://r2dbc.io/spec/1.0.0.RELEASE/api/",
|
||||
@@ -144,28 +119,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"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+11
-2
@@ -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 {
|
||||
@@ -33,6 +38,10 @@ gradlePlugin {
|
||||
id = "org.springframework.build.conventions"
|
||||
implementationClass = "org.springframework.build.ConventionsPlugin"
|
||||
}
|
||||
localDevPlugin {
|
||||
id = "org.springframework.build.localdev"
|
||||
implementationClass = "org.springframework.build.dev.LocalDevelopmentPlugin"
|
||||
}
|
||||
optionalDependenciesPlugin {
|
||||
id = "org.springframework.build.optional-dependencies"
|
||||
implementationClass = "org.springframework.build.optional.OptionalDependenciesPlugin"
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
org.gradle.caching=true
|
||||
javaFormatVersion=0.0.39
|
||||
|
||||
@@ -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.3");
|
||||
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() {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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.dev;
|
||||
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.plugins.JavaBasePlugin;
|
||||
|
||||
/**
|
||||
* {@link Plugin} that skips documentation tasks when the {@code "-PskipDocs"} property is defined.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public class LocalDevelopmentPlugin implements Plugin<Project> {
|
||||
|
||||
private static final String SKIP_DOCS_PROPERTY = "skipDocs";
|
||||
|
||||
@Override
|
||||
public void apply(Project target) {
|
||||
if (target.hasProperty(SKIP_DOCS_PROPERTY)) {
|
||||
skipDocumentationTasks(target);
|
||||
target.subprojects(this::skipDocumentationTasks);
|
||||
}
|
||||
}
|
||||
|
||||
private void skipDocumentationTasks(Project project) {
|
||||
project.afterEvaluate(p -> {
|
||||
p.getTasks().matching(task -> {
|
||||
return JavaBasePlugin.DOCUMENTATION_GROUP.equals(task.getGroup())
|
||||
|| "distribution".equals(task.getGroup());
|
||||
})
|
||||
.forEach(task -> task.setEnabled(false));
|
||||
});
|
||||
}
|
||||
}
|
||||
+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));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -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.
|
||||
@@ -19,6 +19,7 @@ package org.springframework.build.optional;
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.artifacts.Configuration;
|
||||
import org.gradle.api.plugins.JavaBasePlugin;
|
||||
import org.gradle.api.plugins.JavaPlugin;
|
||||
import org.gradle.api.plugins.JavaPluginExtension;
|
||||
import org.gradle.api.tasks.SourceSetContainer;
|
||||
@@ -40,10 +41,10 @@ public class OptionalDependenciesPlugin implements Plugin<Project> {
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
Configuration optional = project.getConfigurations().create("optional");
|
||||
Configuration optional = project.getConfigurations().create(OPTIONAL_CONFIGURATION_NAME);
|
||||
optional.setCanBeConsumed(false);
|
||||
optional.setCanBeResolved(false);
|
||||
project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> {
|
||||
project.getPlugins().withType(JavaBasePlugin.class, (javaBasePlugin) -> {
|
||||
SourceSetContainer sourceSets = project.getExtensions().getByType(JavaPluginExtension.class)
|
||||
.getSourceSets();
|
||||
sourceSets.all((sourceSet) -> {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,10 +3,10 @@ set -e
|
||||
|
||||
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"
|
||||
echo "https://download.bell-sw.com/java/17.0.8.1+1/bellsoft-jdk17.0.8.1+1-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/GA/jdk21/fd2272bbf8e04c3dbaee13770090416c/35/GPL/openjdk-21_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"
|
||||
|
||||
+13
-17
@@ -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
|
||||
@@ -198,26 +198,22 @@ jobs:
|
||||
threads: 8
|
||||
artifact_set:
|
||||
- include:
|
||||
- "/**/framework-docs-*.zip"
|
||||
- "/**/framework-api-*.zip"
|
||||
properties:
|
||||
"zip.name": "spring-framework"
|
||||
"zip.displayname": "Spring Framework"
|
||||
"zip.deployed": "false"
|
||||
- include:
|
||||
- "/**/framework-docs-*-docs.zip"
|
||||
- "/**/framework-api-*-docs.zip"
|
||||
properties:
|
||||
"zip.type": "docs"
|
||||
- include:
|
||||
- "/**/framework-docs-*-dist.zip"
|
||||
properties:
|
||||
"zip.type": "dist"
|
||||
- include:
|
||||
- "/**/framework-docs-*-schema.zip"
|
||||
- "/**/framework-api-*-schema.zip"
|
||||
properties:
|
||||
"zip.type": "schema"
|
||||
get_params:
|
||||
threads: 8
|
||||
- name: jdk20-build
|
||||
- name: jdk21-build
|
||||
serial: true
|
||||
public: true
|
||||
plan:
|
||||
@@ -225,7 +221,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 +230,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 +392,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
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
plugins {
|
||||
id 'java-platform'
|
||||
id 'io.freefair.aggregate-javadoc' version '8.3'
|
||||
}
|
||||
|
||||
description = "Spring Framework API Docs"
|
||||
|
||||
apply from: "${rootDir}/gradle/publications.gradle"
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
url "https://repo.spring.io/release"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
moduleProjects.each { moduleProject ->
|
||||
javadoc moduleProject
|
||||
}
|
||||
}
|
||||
|
||||
javadoc {
|
||||
title = "${rootProject.description} ${version} API"
|
||||
options {
|
||||
encoding = "UTF-8"
|
||||
memberLevel = JavadocMemberLevel.PROTECTED
|
||||
author = true
|
||||
header = rootProject.description
|
||||
use = true
|
||||
overview = "framework-docs/src/docs/api/overview.html"
|
||||
splitIndex = true
|
||||
links(rootProject.ext.javadocLinks)
|
||||
addBooleanOption('Xdoclint:syntax,reference', true) // only check syntax and reference with doclint
|
||||
addBooleanOption('Werror', true) // fail build on Javadoc warnings
|
||||
}
|
||||
maxMemory = "1024m"
|
||||
doFirst {
|
||||
classpath += files(
|
||||
// ensure the javadoc process can resolve types compiled from .aj sources
|
||||
project(":spring-aspects").sourceSets.main.output
|
||||
)
|
||||
classpath += files(moduleProjects.collect { it.sourceSets.main.compileClasspath })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce KDoc for all Spring Framework modules in "build/docs/kdoc"
|
||||
*/
|
||||
rootProject.tasks.dokkaHtmlMultiModule.configure {
|
||||
dependsOn {
|
||||
tasks.named("javadoc")
|
||||
}
|
||||
moduleName.set("spring-framework")
|
||||
outputDirectory.set(project.file("$buildDir/docs/kdoc"))
|
||||
}
|
||||
|
||||
/**
|
||||
* Zip all Java docs (javadoc & kdoc) into a single archive
|
||||
*/
|
||||
tasks.register('docsZip', Zip) {
|
||||
dependsOn = ['javadoc', rootProject.tasks.dokkaHtmlMultiModule]
|
||||
group = "distribution"
|
||||
description = "Builds -${archiveClassifier} archive containing api and reference " +
|
||||
"for deployment at https://docs.spring.io/spring-framework/docs/."
|
||||
|
||||
archiveBaseName.set("spring-framework")
|
||||
archiveClassifier.set("docs")
|
||||
from("src/dist") {
|
||||
include "changelog.txt"
|
||||
}
|
||||
from(javadoc) {
|
||||
into "javadoc-api"
|
||||
}
|
||||
from(rootProject.tasks.dokkaHtmlMultiModule.outputDirectory) {
|
||||
into "kdoc-api"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zip all Spring Framework schemas into a single archive
|
||||
*/
|
||||
tasks.register('schemaZip', Zip) {
|
||||
group = "distribution"
|
||||
archiveBaseName.set("spring-framework")
|
||||
archiveClassifier.set("schema")
|
||||
description = "Builds -${archiveClassifier} archive containing all " +
|
||||
"XSDs for deployment at https://springframework.org/schema."
|
||||
duplicatesStrategy DuplicatesStrategy.EXCLUDE
|
||||
moduleProjects.each { module ->
|
||||
def Properties schemas = new Properties();
|
||||
|
||||
module.sourceSets.main.resources.find {
|
||||
(it.path.endsWith("META-INF/spring.schemas") || it.path.endsWith("META-INF\\spring.schemas"))
|
||||
}?.withInputStream { schemas.load(it) }
|
||||
|
||||
for (def key : schemas.keySet()) {
|
||||
def shortName = key.replaceAll(/http.*schema.(.*).spring-.*/, '$1')
|
||||
assert shortName != key
|
||||
File xsdFile = module.sourceSets.main.resources.find {
|
||||
(it.path.endsWith(schemas.get(key)) || it.path.endsWith(schemas.get(key).replaceAll('\\/', '\\\\')))
|
||||
}
|
||||
assert xsdFile != null
|
||||
into(shortName) {
|
||||
from xsdFile.path
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
mavenJava(MavenPublication) {
|
||||
artifact docsZip
|
||||
artifact schemaZip
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ description = "Spring Framework Docs"
|
||||
|
||||
apply from: "${rootDir}/gradle/publications.gradle"
|
||||
|
||||
|
||||
antora {
|
||||
version = '3.2.0-alpha.2'
|
||||
playbook = 'cached-antora-playbook.yml'
|
||||
@@ -34,23 +33,16 @@ antora {
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
tasks.named("generateAntoraYml") {
|
||||
asciidocAttributes = project.provider( {
|
||||
return ["spring-version": project.version ]
|
||||
} )
|
||||
}
|
||||
|
||||
tasks.create("generateAntoraResources") {
|
||||
tasks.register("generateAntoraResources") {
|
||||
dependsOn 'generateAntoraYml'
|
||||
}
|
||||
|
||||
// Commented out for now:
|
||||
// https://github.com/spring-projects/spring-framework/issues/30481
|
||||
// tasks.named("check") {
|
||||
// dependsOn 'antora'
|
||||
// }
|
||||
|
||||
jar {
|
||||
enabled = false
|
||||
}
|
||||
@@ -67,166 +59,11 @@ repositories {
|
||||
|
||||
dependencies {
|
||||
api(project(":spring-context"))
|
||||
api(project(":spring-jms"))
|
||||
api(project(":spring-web"))
|
||||
api("jakarta.jms:jakarta.jms-api")
|
||||
api("jakarta.servlet:jakarta.servlet-api")
|
||||
|
||||
implementation(project(":spring-core-test"))
|
||||
implementation("org.assertj:assertj-core")
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce Javadoc for all Spring Framework modules in "build/docs/javadoc"
|
||||
*/
|
||||
task api(type: Javadoc) {
|
||||
group = "Documentation"
|
||||
description = "Generates aggregated Javadoc API documentation."
|
||||
title = "${rootProject.description} ${version} API"
|
||||
|
||||
dependsOn {
|
||||
moduleProjects.collect {
|
||||
it.tasks.getByName("jar")
|
||||
}
|
||||
}
|
||||
doFirst {
|
||||
classpath = files(
|
||||
// ensure the javadoc process can resolve types compiled from .aj sources
|
||||
project(":spring-aspects").sourceSets.main.output
|
||||
)
|
||||
classpath += files(moduleProjects.collect { it.sourceSets.main.compileClasspath })
|
||||
}
|
||||
|
||||
options {
|
||||
encoding = "UTF-8"
|
||||
memberLevel = JavadocMemberLevel.PROTECTED
|
||||
author = true
|
||||
header = rootProject.description
|
||||
use = true
|
||||
overview = "framework-docs/src/docs/api/overview.html"
|
||||
splitIndex = true
|
||||
links(project.ext.javadocLinks)
|
||||
addBooleanOption('Xdoclint:syntax,reference', true) // only check syntax and reference with doclint
|
||||
addBooleanOption('Werror', true) // fail build on Javadoc warnings
|
||||
}
|
||||
source moduleProjects.collect { project ->
|
||||
project.sourceSets.main.allJava
|
||||
}
|
||||
maxMemory = "1024m"
|
||||
destinationDir = file("$buildDir/docs/javadoc")
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce KDoc for all Spring Framework modules in "build/docs/kdoc"
|
||||
*/
|
||||
rootProject.tasks.dokkaHtmlMultiModule.configure {
|
||||
dependsOn {
|
||||
tasks.getByName("api")
|
||||
}
|
||||
moduleName.set("spring-framework")
|
||||
outputDirectory.set(project.file("$buildDir/docs/kdoc"))
|
||||
}
|
||||
|
||||
/**
|
||||
* Zip all Java docs (javadoc & kdoc) into a single archive
|
||||
*/
|
||||
task docsZip(type: Zip, dependsOn: ['api', rootProject.tasks.dokkaHtmlMultiModule]) {
|
||||
group = "Distribution"
|
||||
description = "Builds -${archiveClassifier} archive containing api and reference " +
|
||||
"for deployment at https://docs.spring.io/spring-framework/docs/."
|
||||
|
||||
archiveBaseName.set("spring-framework")
|
||||
archiveClassifier.set("docs")
|
||||
from("src/dist") {
|
||||
include "changelog.txt"
|
||||
}
|
||||
from (api) {
|
||||
into "javadoc-api"
|
||||
}
|
||||
from (rootProject.tasks.dokkaHtmlMultiModule.outputDirectory) {
|
||||
into "kdoc-api"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zip all Spring Framework schemas into a single archive
|
||||
*/
|
||||
task schemaZip(type: Zip) {
|
||||
group = "Distribution"
|
||||
archiveBaseName.set("spring-framework")
|
||||
archiveClassifier.set("schema")
|
||||
description = "Builds -${archiveClassifier} archive containing all " +
|
||||
"XSDs for deployment at https://springframework.org/schema."
|
||||
duplicatesStrategy DuplicatesStrategy.EXCLUDE
|
||||
moduleProjects.each { module ->
|
||||
def Properties schemas = new Properties();
|
||||
|
||||
module.sourceSets.main.resources.find {
|
||||
(it.path.endsWith("META-INF/spring.schemas") || it.path.endsWith("META-INF\\spring.schemas"))
|
||||
}?.withInputStream { schemas.load(it) }
|
||||
|
||||
for (def key : schemas.keySet()) {
|
||||
def shortName = key.replaceAll(/http.*schema.(.*).spring-.*/, '$1')
|
||||
assert shortName != key
|
||||
File xsdFile = module.sourceSets.main.resources.find {
|
||||
(it.path.endsWith(schemas.get(key)) || it.path.endsWith(schemas.get(key).replaceAll('\\/','\\\\')))
|
||||
}
|
||||
assert xsdFile != null
|
||||
into (shortName) {
|
||||
from xsdFile.path
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a distribution zip with everything:
|
||||
* docs, schemas, jars, source jars, javadoc jars
|
||||
*/
|
||||
task distZip(type: Zip, dependsOn: [docsZip, schemaZip]) {
|
||||
group = "Distribution"
|
||||
archiveBaseName.set("spring-framework")
|
||||
archiveClassifier.set("dist")
|
||||
description = "Builds -${archiveClassifier} archive, containing all jars and docs, " +
|
||||
"suitable for community download page."
|
||||
|
||||
ext.baseDir = "spring-framework-${project.version}";
|
||||
|
||||
from("src/docs/dist") {
|
||||
include "readme.txt"
|
||||
include "license.txt"
|
||||
include "notice.txt"
|
||||
into "${baseDir}"
|
||||
expand(copyright: new Date().format("yyyy"), version: project.version)
|
||||
}
|
||||
|
||||
from(zipTree(docsZip.archiveFile)) {
|
||||
into "${baseDir}/docs"
|
||||
}
|
||||
|
||||
from(zipTree(schemaZip.archiveFile)) {
|
||||
into "${baseDir}/schema"
|
||||
}
|
||||
|
||||
moduleProjects.each { module ->
|
||||
into ("${baseDir}/libs") {
|
||||
from module.jar
|
||||
if (module.tasks.findByPath("sourcesJar")) {
|
||||
from module.sourcesJar
|
||||
}
|
||||
if (module.tasks.findByPath("javadocJar")) {
|
||||
from module.javadocJar
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
distZip.mustRunAfter moduleProjects.check
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
mavenJava(MavenPublication) {
|
||||
artifact docsZip
|
||||
artifact schemaZip
|
||||
artifact distZip
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -122,6 +122,7 @@
|
||||
**** xref:testing/testcontext-framework/ctx-management/groovy.adoc[]
|
||||
**** xref:testing/testcontext-framework/ctx-management/javaconfig.adoc[]
|
||||
**** xref:testing/testcontext-framework/ctx-management/mixed-config.adoc[]
|
||||
**** xref:testing/testcontext-framework/ctx-management/context-customizers.adoc[]
|
||||
**** xref:testing/testcontext-framework/ctx-management/initializers.adoc[]
|
||||
**** xref:testing/testcontext-framework/ctx-management/inheritance.adoc[]
|
||||
**** xref:testing/testcontext-framework/ctx-management/env-profiles.adoc[]
|
||||
@@ -130,6 +131,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[]
|
||||
@@ -165,6 +167,7 @@
|
||||
***** xref:testing/annotations/integration-spring/annotation-contextconfiguration.adoc[]
|
||||
***** xref:testing/annotations/integration-spring/annotation-webappconfiguration.adoc[]
|
||||
***** xref:testing/annotations/integration-spring/annotation-contexthierarchy.adoc[]
|
||||
***** xref:testing/annotations/integration-spring/annotation-contextcustomizerfactories.adoc[]
|
||||
***** xref:testing/annotations/integration-spring/annotation-activeprofiles.adoc[]
|
||||
***** xref:testing/annotations/integration-spring/annotation-testpropertysource.adoc[]
|
||||
***** xref:testing/annotations/integration-spring/annotation-dynamicpropertysource.adoc[]
|
||||
@@ -266,6 +269,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 +364,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 +419,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].
|
||||
@@ -60,19 +55,27 @@ for details.
|
||||
{api-spring-framework}++/objenesis/SpringObjenesis.html#IGNORE_OBJENESIS_PROPERTY_NAME++[`SpringObjenesis`]
|
||||
for details.
|
||||
|
||||
| `spring.test.aot.processing.failOnError`
|
||||
| A boolean flag that controls whether errors encountered during AOT processing in the
|
||||
_Spring TestContext Framework_ should result in an exception that fails the overall process.
|
||||
See xref:testing/testcontext-framework/aot.adoc[Ahead of Time Support for Tests].
|
||||
|
||||
| `spring.test.constructor.autowire.mode`
|
||||
| The default _test constructor autowire mode_ to use if `@TestConstructor` is not present
|
||||
on a test class. See xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[Changing the default test constructor autowire mode]
|
||||
.
|
||||
on a test class. See xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[Changing the default test constructor autowire mode].
|
||||
|
||||
| `spring.test.context.cache.maxSize`
|
||||
| The maximum size of the context cache in the _Spring TestContext Framework_. See
|
||||
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
|
||||
xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration[Changing the default enclosing configuration inheritance mode]
|
||||
.
|
||||
xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration[Changing the default enclosing configuration inheritance mode].
|
||||
|
||||
|===
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -31,7 +31,7 @@ However, it would be even more confusing if Spring used its own terminology.
|
||||
the "advised object". Since Spring AOP is implemented by using runtime proxies, this
|
||||
object is always a proxied object.
|
||||
* AOP proxy: An object created by the AOP framework in order to implement the aspect
|
||||
contracts (advise method executions and so on). In the Spring Framework, an AOP proxy
|
||||
contracts (advice method executions and so on). In the Spring Framework, an AOP proxy
|
||||
is a JDK dynamic proxy or a CGLIB proxy.
|
||||
* Weaving: linking aspects with other application types or objects to create an
|
||||
advised object. This can be done at compile time (using the AspectJ compiler, for
|
||||
|
||||
@@ -256,6 +256,13 @@ Java::
|
||||
|
||||
If you are registering bean definitions programmatically, consider using `RootBeanBefinition` as it allows to specify a `ResolvableType` that handles generics.
|
||||
|
||||
[[aot.bestpractices.constructors]]
|
||||
=== Avoid Multiple Constructors
|
||||
The container is able to choose the most appropriate constructor to use based on several candidates.
|
||||
However, this is not a best practice and flagging the preferred constructor with `@Autowired` if necessary is preferred.
|
||||
|
||||
In case you are working on a code base that you can't modify, you can set the {api-spring-framework}/beans/factory/support/AbstractBeanDefinition.html#PREFERRED_CONSTRUCTORS_ATTRIBUTE[`preferredConstructors` attribute] on the related bean definition to indicate which constructor should be used.
|
||||
|
||||
[[aot.bestpractices.factory-bean]]
|
||||
=== FactoryBean
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ use these features.
|
||||
== `@Component` and Further Stereotype Annotations
|
||||
|
||||
The `@Repository` annotation is a marker for any class that fulfills the role or
|
||||
stereotype of a repository (also known as Data Access Object or DAO). Among the uses
|
||||
_stereotype_ of a repository (also known as Data Access Object or DAO). Among the uses
|
||||
of this marker is the automatic translation of exceptions, as described in
|
||||
xref:data-access/orm/general.adoc#orm-exception-translation[Exception Translation].
|
||||
|
||||
@@ -39,7 +39,7 @@ layers, respectively). Therefore, you can annotate your component classes with
|
||||
`@Component`, but, by annotating them with `@Repository`, `@Service`, or `@Controller`
|
||||
instead, your classes are more properly suited for processing by tools or associating
|
||||
with aspects. For example, these stereotype annotations make ideal targets for
|
||||
pointcuts. `@Repository`, `@Service`, and `@Controller` can also
|
||||
pointcuts. `@Repository`, `@Service`, and `@Controller` may also
|
||||
carry additional semantics in future releases of the Spring Framework. Thus, if you are
|
||||
choosing between using `@Component` or `@Service` for your service layer, `@Service` is
|
||||
clearly the better choice. Similarly, as stated earlier, `@Repository` is already
|
||||
@@ -664,15 +664,36 @@ analogous to how the container selects between multiple `@Autowired` constructor
|
||||
== Naming Autodetected Components
|
||||
|
||||
When a component is autodetected as part of the scanning process, its bean name is
|
||||
generated by the `BeanNameGenerator` strategy known to that scanner. By default, any
|
||||
Spring stereotype annotation (`@Component`, `@Repository`, `@Service`, and
|
||||
`@Controller`) that contains a name `value` thereby provides that name to the
|
||||
corresponding bean definition.
|
||||
generated by the `BeanNameGenerator` strategy known to that scanner.
|
||||
|
||||
If such an annotation contains no name `value` or for any other detected component
|
||||
(such as those discovered by custom filters), the default bean name generator returns
|
||||
the uncapitalized non-qualified class name. For example, if the following component
|
||||
classes were detected, the names would be `myMovieLister` and `movieFinderImpl`:
|
||||
By default, the `AnnotationBeanNameGenerator` is used. For Spring
|
||||
xref:core/beans/classpath-scanning.adoc#beans-stereotype-annotations[stereotype annotations],
|
||||
if you supply a name via the annotation's `value` attribute that name will be used as
|
||||
the name in the corresponding bean definition. This convention also applies when the
|
||||
following JSR-250 and JSR-330 annotations are used instead of Spring stereotype
|
||||
annotations: `@jakarta.annotation.ManagedBean`, `@javax.annotation.ManagedBean`,
|
||||
`@jakarta.inject.Named`, and `@javax.inject.Named`.
|
||||
|
||||
As of Spring Framework 6.1, the name of the annotation attribute that is used to specify
|
||||
the bean name is no longer required to be `value`. Custom stereotype annotations can
|
||||
declare an attribute with a different name (such as `name`) and annotate that attribute
|
||||
with `@AliasFor(annotation = Component.class, attribute = "value")`. See the source code
|
||||
declaration of `ControllerAdvice#name()` for a concrete example.
|
||||
|
||||
[WARNING]
|
||||
====
|
||||
As of Spring Framework 6.1, support for convention-based stereotype names is deprecated
|
||||
and will be removed in a future version of the framework. Consequently, custom stereotype
|
||||
annotations must use `@AliasFor` to declare an explicit alias for the `value` attribute
|
||||
in `@Component`. See the source code declaration of `Repository#value()` and
|
||||
`ControllerAdvice#name()` for concrete examples.
|
||||
====
|
||||
|
||||
If an explicit bean name cannot be derived from such an annotation or for any other
|
||||
detected component (such as those discovered by custom filters), the default bean name
|
||||
generator returns the uncapitalized non-qualified class name. For example, if the
|
||||
following component classes were detected, the names would be `myMovieLister` and
|
||||
`movieFinderImpl`.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -989,68 +1010,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.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -478,20 +478,21 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
Notice that `ApplicationListener` is generically parameterized with the type of your
|
||||
custom event (`BlockedListEvent` in the preceding example). This means that the
|
||||
`onApplicationEvent()` method can remain type-safe, avoiding any need for downcasting.
|
||||
You can register as many event listeners as you wish, but note that, by default, event
|
||||
listeners receive events synchronously. This means that the `publishEvent()` method
|
||||
blocks until all listeners have finished processing the event. One advantage of this
|
||||
synchronous and single-threaded approach is that, when a listener receives an event,
|
||||
it operates inside the transaction context of the publisher if a transaction context
|
||||
is available. If another strategy for event publication becomes necessary, e.g.
|
||||
asynchronous event processing by default, see the javadoc for Spring's
|
||||
{api-spring-framework}/context/event/ApplicationEventMulticaster.html[`ApplicationEventMulticaster`] interface
|
||||
and {api-spring-framework}/context/event/SimpleApplicationEventMulticaster.html[`SimpleApplicationEventMulticaster`]
|
||||
implementation for configuration options which can be applied to a custom
|
||||
"applicationEventMulticaster" bean definition.
|
||||
Notice that `ApplicationListener` is generically parameterized with the type of your custom event (`BlockedListEvent` in the preceding example).
|
||||
This means that the `onApplicationEvent()` method can remain type-safe, avoiding any need for downcasting.
|
||||
You can register as many event listeners as you wish, but note that, by default, event listeners receive events synchronously.
|
||||
This means that the `publishEvent()` method blocks until all listeners have finished processing the event.
|
||||
One advantage of this synchronous and single-threaded approach is that, when a listener receives an event,
|
||||
it operates inside the transaction context of the publisher if a transaction context is available.
|
||||
If another strategy for event publication becomes necessary, e.g. asynchronous event processing by default,
|
||||
see the javadoc for Spring's {api-spring-framework}/context/event/ApplicationEventMulticaster.html[`ApplicationEventMulticaster`] interface
|
||||
and {api-spring-framework}/context/event/SimpleApplicationEventMulticaster.html[`SimpleApplicationEventMulticaster`] implementation
|
||||
for configuration options which can be applied to a custom "applicationEventMulticaster" bean definition.
|
||||
In these cases, ThreadLocals and logging context are not propagated for the event processing.
|
||||
See xref:integration/observability.adoc#observability.application-events[the `@EventListener` Observability section]
|
||||
for more information on Observability concerns.
|
||||
|
||||
|
||||
|
||||
The following example shows the bean definitions used to register and configure each of
|
||||
the classes above:
|
||||
@@ -747,6 +748,9 @@ Be aware of the following limitations when using asynchronous events:
|
||||
value. If you need to publish another event as the result of the processing, inject an
|
||||
{api-spring-framework}/context/ApplicationEventPublisher.html[`ApplicationEventPublisher`]
|
||||
to publish the event manually.
|
||||
* ThreadLocals and logging context are not propagated by default for the event processing.
|
||||
See xref:integration/observability.adoc#observability.application-events[the `@EventListener` Observability section]
|
||||
for more information on Observability concerns.
|
||||
|
||||
|
||||
[[context-functionality-events-order]]
|
||||
|
||||
@@ -771,11 +771,9 @@ resolved to the corresponding value. If not, then `default/path` is used
|
||||
as a default. If no default is specified and a property cannot be resolved, an
|
||||
`IllegalArgumentException` is thrown.
|
||||
|
||||
NOTE: The `@PropertySource` annotation is repeatable, according to Java 8 conventions.
|
||||
However, all such `@PropertySource` annotations need to be declared at the same
|
||||
level, either directly on the configuration class or as meta-annotations within the
|
||||
same custom annotation. Mixing direct annotations and meta-annotations is not
|
||||
recommended, since direct annotations effectively override meta-annotations.
|
||||
NOTE: `@PropertySource` can be used as a repeatable annotation. `@PropertySource`
|
||||
may also be used as a meta-annotation to create custom composed annotations with
|
||||
attribute overrides.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
----
|
||||
|
||||
@@ -1,5 +1,50 @@
|
||||
[[beans-binding]]
|
||||
= Data Binding
|
||||
|
||||
Data binding is useful for binding user input to a target object where user input is a map
|
||||
with property paths as keys, following xref:beans-beans-conventions[JavaBeans conventions].
|
||||
`DataBinder` is the main class that supports this, and it provides two ways to bind user
|
||||
input:
|
||||
|
||||
- xref:beans-constructor-binding[Constructor binding] - bind user input to a public data
|
||||
constructor, looking up constructor argument values in the user input.
|
||||
- xref:beans-beans[Property binding] - bind user input to setters, matching keys from the
|
||||
user input to properties of the target object structure.
|
||||
|
||||
You can apply both constructor and property binding or only one.
|
||||
|
||||
|
||||
[[beans-constructor-binding]]
|
||||
== Constructor Binding
|
||||
|
||||
To use constructor binding:
|
||||
|
||||
1. Create a `DataBinder` with `null` as the target object.
|
||||
2. Set `targetType` to the target class.
|
||||
3. Call `construct`.
|
||||
|
||||
The target class should have a single public constructor or a single non-public constructor
|
||||
with arguments. If there are multiple constructors, then a default constructor if present
|
||||
is used.
|
||||
|
||||
By default, constructor parameter names are used to look up argument values, but you can
|
||||
configure a `NameResolver`. Spring MVC and WebFlux both rely to allow customizing the name
|
||||
of the value to bind through an `@BindParam` annotation on constructor parameters.
|
||||
|
||||
xref:beans-beans-conventions[Type conversion] is applied as needed to convert user input.
|
||||
If the constructor parameter is an object, it is constructed recursively in the same
|
||||
manner, but through a nested property path. That means constructor binding creates both
|
||||
the target object and any objects it contains.
|
||||
|
||||
Binding and conversion errors are reflected in the `BindingResult` of the `DataBinder`.
|
||||
If the target is created successfully, then `target` is set to the created instance
|
||||
after the call to `construct`.
|
||||
|
||||
|
||||
|
||||
|
||||
[[beans-beans]]
|
||||
= Bean Manipulation and the `BeanWrapper`
|
||||
== Property Binding with `BeanWrapper`
|
||||
|
||||
The `org.springframework.beans` package adheres to the JavaBeans standard.
|
||||
A JavaBean is a class with a default no-argument constructor and that follows
|
||||
@@ -26,7 +71,7 @@ perform actions on that bean, such as setting and retrieving properties.
|
||||
|
||||
|
||||
[[beans-beans-conventions]]
|
||||
== Setting and Getting Basic and Nested Properties
|
||||
=== Setting and Getting Basic and Nested Properties
|
||||
|
||||
Setting and getting properties is done through the `setPropertyValue` and
|
||||
`getPropertyValue` overloaded method variants of `BeanWrapper`. See their Javadoc for
|
||||
@@ -192,7 +237,7 @@ Kotlin::
|
||||
|
||||
|
||||
[[beans-beans-conversion]]
|
||||
== Built-in `PropertyEditor` Implementations
|
||||
== ``PropertyEditor``'s
|
||||
|
||||
Spring uses the concept of a `PropertyEditor` to effect the conversion between an
|
||||
`Object` and a `String`. It can be handy
|
||||
@@ -378,7 +423,7 @@ Kotlin::
|
||||
|
||||
|
||||
[[beans-beans-conversion-customeditor-registration]]
|
||||
=== Registering Additional Custom `PropertyEditor` Implementations
|
||||
=== Custom ``PropertyEditor``'s
|
||||
|
||||
When setting bean properties as string values, a Spring IoC container ultimately uses
|
||||
standard JavaBeans `PropertyEditor` implementations to convert these strings to the complex type of the
|
||||
@@ -521,7 +566,7 @@ Finally, the following example shows how to use `CustomEditorConfigurer` to regi
|
||||
----
|
||||
|
||||
[[beans-beans-conversion-customeditor-registration-per]]
|
||||
==== Using `PropertyEditorRegistrar`
|
||||
=== `PropertyEditorRegistrar`
|
||||
|
||||
Another mechanism for registering property editors with the Spring container is to
|
||||
create and use a `PropertyEditorRegistrar`. This interface is particularly useful when
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -272,11 +282,10 @@ As the preceding example shows, a `ConstraintValidator` implementation can have
|
||||
|
||||
|
||||
[[validation-beanvalidation-spring-method]]
|
||||
=== Spring-driven Method Validation
|
||||
== 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,142 @@ 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.
|
||||
====
|
||||
|
||||
Spring MVC and WebFlux have built-in support for the same underlying method validation but without
|
||||
the need for AOP. Therefore, do check the rest of this section, and also see the Spring MVC
|
||||
xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation] and
|
||||
xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses] sections, and the WebFlux
|
||||
xref:web/webflux/controller/ann-validation.adoc[Validation] and
|
||||
xref:web/webflux/ann-rest-exceptions.adoc[Error Responses] sections.
|
||||
|
||||
|
||||
[[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
|
||||
|
||||
@@ -195,6 +195,12 @@ of Spring Web MVC, you can use the `<spring:bind/>` tag to inspect the error mes
|
||||
you can also inspect the `Errors` object yourself. More information about the
|
||||
methods it offers can be found in the {api-spring-framework}/validation/Errors.html[javadoc].
|
||||
|
||||
Validators may also get locally invoked for the immediate validation of a given object,
|
||||
not involving a binding process. As of 6.1, this has been simplified through a new
|
||||
`Validator.validateObject(Object)` method which is available by default now, returning
|
||||
a simple ´Errors` representation which can be inspected: typically calling `hasErrors()`
|
||||
or the new `failOnError` method for turning the error summary message into an exception
|
||||
(e.g. `validator.validateObject(myObject).failOnError(IllegalArgumentException::new)`).
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,30 +2,26 @@
|
||||
= Choosing an Approach for JDBC Database Access
|
||||
|
||||
You can choose among several approaches to form the basis for your JDBC database access.
|
||||
In addition to three flavors of `JdbcTemplate`, a new `SimpleJdbcInsert` and
|
||||
`SimpleJdbcCall` approach optimizes database metadata, and the RDBMS Object style takes a
|
||||
more object-oriented approach similar to that of JDO Query design. Once you start using
|
||||
one of these approaches, you can still mix and match to include a feature from a
|
||||
different approach. All approaches require a JDBC 2.0-compliant driver, and some
|
||||
advanced features require a JDBC 3.0 driver.
|
||||
In addition to three flavors of `JdbcTemplate`, a `SimpleJdbcInsert` and `SimpleJdbcCall`
|
||||
approach optimizes database metadata, and the RDBMS Object style results in a more
|
||||
object-oriented approach. Once you start using one of these approaches, you can still mix
|
||||
and match to include a feature from a different approach.
|
||||
|
||||
* `JdbcTemplate` is the classic and most popular Spring JDBC approach. This
|
||||
"`lowest-level`" approach and all others use a JdbcTemplate under the covers.
|
||||
"`lowest-level`" approach and all others use a `JdbcTemplate` under the covers.
|
||||
* `NamedParameterJdbcTemplate` wraps a `JdbcTemplate` to provide named parameters
|
||||
instead of the traditional JDBC `?` placeholders. This approach provides better
|
||||
documentation and ease of use when you have multiple parameters for an SQL statement.
|
||||
* `SimpleJdbcInsert` and `SimpleJdbcCall` optimize database metadata to limit the amount
|
||||
of necessary configuration. This approach simplifies coding so that you need to
|
||||
provide only the name of the table or procedure and provide a map of parameters matching
|
||||
the column names. This works only if the database provides adequate metadata. If the
|
||||
database does not provide this metadata, you have to provide explicit
|
||||
configuration of the parameters.
|
||||
of necessary configuration. This approach simplifies coding so that you only need to
|
||||
provide the name of the table or procedure and a map of parameters matching the column
|
||||
names. This works only if the database provides adequate metadata. If the database does
|
||||
not provide this metadata, you have to provide explicit configuration of the parameters.
|
||||
* RDBMS objects — including `MappingSqlQuery`, `SqlUpdate`, and `StoredProcedure` —
|
||||
require you to create reusable and thread-safe objects during initialization of your
|
||||
data-access layer. This approach is modeled after JDO Query, wherein you define your
|
||||
query string, declare parameters, and compile the query. Once you do that,
|
||||
`execute(...)`, `update(...)`, and `findObject(...)` methods can be called multiple
|
||||
times with various parameter values.
|
||||
data-access layer. This approach allows you to define your query string, declare
|
||||
parameters, and compile the query. Once you do that, `execute(...)`, `update(...)`, and
|
||||
`findObject(...)` methods can be called multiple times with various parameter values.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ including error handling. It includes the following topics:
|
||||
|
||||
* xref:data-access/jdbc/core.adoc#jdbc-JdbcTemplate[Using `JdbcTemplate`]
|
||||
* xref:data-access/jdbc/core.adoc#jdbc-NamedParameterJdbcTemplate[Using `NamedParameterJdbcTemplate`]
|
||||
* xref:data-access/jdbc/core.adoc#jdbc-JdbcClient[Unified JDBC Query/Update Operations: `JdbcClient`]
|
||||
* xref:data-access/jdbc/core.adoc#jdbc-SQLExceptionTranslator[Using `SQLExceptionTranslator`]
|
||||
* xref:data-access/jdbc/core.adoc#jdbc-statements-executing[Running Statements]
|
||||
* xref:data-access/jdbc/core.adoc#jdbc-statements-querying[Running Queries]
|
||||
@@ -701,6 +702,120 @@ See also xref:data-access/jdbc/core.adoc#jdbc-JdbcTemplate-idioms[`JdbcTemplate`
|
||||
for guidelines on using the `NamedParameterJdbcTemplate` class in the context of an application.
|
||||
|
||||
|
||||
[[jdbc-JdbcClient]]
|
||||
== Unified JDBC Query/Update Operations: `JdbcClient`
|
||||
|
||||
As of 6.1, the named parameter statements of `NamedParameterJdbcTemplate` and the positional
|
||||
parameter statements of a regular `JdbcTemplate` are available through a unified client API
|
||||
with a fluent interaction model.
|
||||
|
||||
For example, with positional parameters:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
private JdbcClient jdbcClient = JdbcClient.create(dataSource);
|
||||
|
||||
public int countOfActorsByFirstName(String firstName) {
|
||||
return this.jdbcClient.sql("select count(*) from t_actor where first_name = ?")
|
||||
.param(firstName);
|
||||
.query(Integer.class).single();
|
||||
}
|
||||
----
|
||||
|
||||
For example, with named parameters:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
private JdbcClient jdbcClient = JdbcClient.create(dataSource);
|
||||
|
||||
public int countOfActorsByFirstName(String firstName) {
|
||||
return this.jdbcClient.sql("select count(*) from t_actor where first_name = :firstName")
|
||||
.param("firstName", firstName);
|
||||
.query(Integer.class).single();
|
||||
}
|
||||
----
|
||||
|
||||
`RowMapper` capabilities are available as well, with flexible result resolution:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
List<Actor> actors = this.jdbcClient.sql("select first_name, last_name from t_actor")
|
||||
.query((rs, rowNum) -> new Actor(rs.getString("first_name"), rs.getString("last_name")))
|
||||
.list();
|
||||
----
|
||||
|
||||
Instead of a custom `RowMapper`, you may also specify a class to map to.
|
||||
For example, assuming that `Actor` has `firstName` and `lastName` properties
|
||||
as a record class, a custom constructor, bean properties, or plain fields:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
List<Actor> actors = this.jdbcClient.sql("select first_name, last_name from t_actor")
|
||||
.query(Actor.class)
|
||||
.list();
|
||||
----
|
||||
|
||||
With a required single object result:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
Actor actor = this.jdbcClient.sql("select first_name, last_name from t_actor where id = ?",
|
||||
.param(1212L);
|
||||
.query(Actor.class)
|
||||
.single();
|
||||
----
|
||||
|
||||
With a `java.util.Optional` result:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
Optional<Actor> actor = this.jdbcClient.sql("select first_name, last_name from t_actor where id = ?",
|
||||
.param(1212L);
|
||||
.query(Actor.class)
|
||||
.optional();
|
||||
----
|
||||
|
||||
And for an update statement:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (?, ?)")
|
||||
.param("Leonor").param("Watling");
|
||||
.update();
|
||||
----
|
||||
|
||||
Or an update statement with named parameters:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (:firstName, :lastName)")
|
||||
.param("firstName", "Leonor").param("lastName", "Watling");
|
||||
.update();
|
||||
----
|
||||
|
||||
Instead of individual named parameters, you may also specify a parameter source object –
|
||||
for example, a record class, a class with bean properties, or a plain field holder which
|
||||
provides `firstName` and `lastName` properties, such as the `Actor` class from above:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (:firstName, :lastName)")
|
||||
.paramSource(new Actor("Leonor", "Watling");
|
||||
.update();
|
||||
----
|
||||
|
||||
The automatic `Actor` class mapping for parameters as well as the query results above is
|
||||
provided through implicit `SimplePropertySqlParameterSource` and `SimplePropertyRowMapper`
|
||||
strategies which are also available for direct use. They can serve as a common replacement
|
||||
for `BeanPropertySqlParameterSource` and `BeanPropertyRowMapper`/`DataClassRowMapper`,
|
||||
also with `JdbcTemplate` and `NamedParameterJdbcTemplate` themselves.
|
||||
|
||||
NOTE: `JdbcClient` is a flexible but simplified facade for JDBC query/update statements.
|
||||
Advanced capabilities such as batch inserts and stored procedure calls typically require
|
||||
extra customization: consider Spring's `SimpleJdbcInsert` and `SimpleJdbcCall` classes or
|
||||
plain direct `JdbcTemplate` usage for any such capabilities not available in `JdbcClient`.
|
||||
|
||||
|
||||
[[jdbc-SQLExceptionTranslator]]
|
||||
== Using `SQLExceptionTranslator`
|
||||
|
||||
@@ -709,7 +824,7 @@ between ``SQLException``s and Spring's own `org.springframework.dao.DataAccessEx
|
||||
which is agnostic in regard to data access strategy. Implementations can be generic (for
|
||||
example, using SQLState codes for JDBC) or proprietary (for example, using Oracle error
|
||||
codes) for greater precision. This exception translation mechanism is used behind the
|
||||
the common `JdbcTemplate` and `JdbcTransactionManager` entry points which do not
|
||||
common `JdbcTemplate` and `JdbcTransactionManager` entry points which do not
|
||||
propagate `SQLException` but rather `DataAccessException`.
|
||||
|
||||
NOTE: As of 6.0, the default exception translator is `SQLExceptionSubclassTranslator`,
|
||||
|
||||
@@ -20,9 +20,10 @@ xref:data-access/jdbc/connections.adoc[Controlling Database Connections] and xre
|
||||
|
||||
* `object`: The `org.springframework.jdbc.object` package contains classes that represent
|
||||
RDBMS queries, updates, and stored procedures as thread-safe, reusable objects. See
|
||||
xref:data-access/jdbc/object.adoc[Modeling JDBC Operations as Java Objects]. This approach is modeled by JDO, although objects returned by queries
|
||||
are naturally disconnected from the database. This higher-level of JDBC abstraction
|
||||
depends on the lower-level abstraction in the `org.springframework.jdbc.core` package.
|
||||
xref:data-access/jdbc/object.adoc[Modeling JDBC Operations as Java Objects]. This style
|
||||
results in a more object-oriented approach, although objects returned by queries are
|
||||
naturally disconnected from the database. This higher-level of JDBC abstraction depends
|
||||
on the lower-level abstraction in the `org.springframework.jdbc.core` package.
|
||||
|
||||
* `support`: The `org.springframework.jdbc.support` package provides `SQLException`
|
||||
translation functionality and some utility classes. Exceptions thrown during JDBC processing
|
||||
|
||||
@@ -254,6 +254,25 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
Alternatively, there is a shortcut for mapping to a single value:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
Flux<String> names = client.sql("SELECT name FROM person")
|
||||
.mapValue(String.class)
|
||||
.all();
|
||||
----
|
||||
|
||||
Or you may map to a result object with bean properties or record components:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
// assuming a name property on Person
|
||||
Flux<Person> persons = client.sql("SELECT name FROM person")
|
||||
.mapProperties(Person.class)
|
||||
.all();
|
||||
----
|
||||
|
||||
[[r2dbc-DatabaseClient-mapping-null]]
|
||||
.What about `null`?
|
||||
****
|
||||
@@ -324,6 +343,27 @@ The following example shows parameter binding for a query:
|
||||
.bind("age", 34);
|
||||
----
|
||||
|
||||
Alternatively, you may pass in a map of names and values:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
Map<String, Object> params = new LinkedHashMap<>();
|
||||
params.put("id", "joe");
|
||||
params.put("name", "Joe");
|
||||
params.put("age", 34);
|
||||
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
|
||||
.bindValues(params);
|
||||
----
|
||||
|
||||
Or you may pass in a parameter object with bean properties or record components:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
// assuming id, name, age properties on Person
|
||||
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
|
||||
.bindProperties(new Person("joe", "Joe", 34);
|
||||
----
|
||||
|
||||
.R2DBC Native Bind Markers
|
||||
****
|
||||
R2DBC uses database-native bind markers that depend on the actual database vendor.
|
||||
|
||||
@@ -57,10 +57,14 @@ attribute of the annotation to `true`.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
`@TransactionalEventListener` only works with thread-bound transactions managed by
|
||||
`PlatformTransactionManager`. A reactive transaction managed by `ReactiveTransactionManager`
|
||||
uses the Reactor context instead of thread-local attributes, so from the perspective of
|
||||
an event listener, there is no compatible active transaction that it can participate in.
|
||||
As of 6.1, `@TransactionalEventListener` can work with thread-bound transactions managed by
|
||||
`PlatformTransactionManager` as well as reactive transactions managed by `ReactiveTransactionManager`.
|
||||
For the former, listeners are guaranteed to see the current thread-bound transaction.
|
||||
Since the latter uses the Reactor context instead of thread-local variables, the transaction
|
||||
context needs to be included in the published event instance as the event source.
|
||||
See the
|
||||
{api-spring-framework}/transaction/reactive/TransactionalEventPublisher.html[`TransactionalEventPublisher`]
|
||||
javadoc for details.
|
||||
====
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -208,6 +208,76 @@ NOTE: This is an optional feature, and your favorite cache library may not suppo
|
||||
All `CacheManager` implementations provided by the core framework support it. See the
|
||||
documentation of your cache provider for more details.
|
||||
|
||||
[[cache-annotations-cacheable-reactive]]
|
||||
=== Caching with CompletableFuture and Reactive Return Types
|
||||
|
||||
As of 6.1, cache annotations take `CompletableFuture` and reactive return types
|
||||
into account, automatically adapting the cache interaction accordingly.
|
||||
|
||||
For a method returning a `CompletableFuture`, the object produced by that future
|
||||
will be cached whenever it is complete, and the cache lookup for a cache hit will
|
||||
be retrieved via a `CompletableFuture`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Cacheable("books")
|
||||
public CompletableFuture<Book> findBook(ISBN isbn) {...}
|
||||
----
|
||||
|
||||
For a method returning a Reactor `Mono`, the object emitted by that Reactive Streams
|
||||
publisher will be cached whenever it is available, and the cache lookup for a cache
|
||||
hit will be retrieved as a `Mono` (backed by a `CompletableFuture`):
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Cacheable("books")
|
||||
public Mono<Book> findBook(ISBN isbn) {...}
|
||||
----
|
||||
|
||||
For a method returning a Reactor `Flux`, the objects emitted by that Reactive Streams
|
||||
publisher will be collected into a `List` and cached whenever that list is complete,
|
||||
and the cache lookup for a cache hit will be retrieved as a `Flux` (backed by a
|
||||
`CompletableFuture` for the cached `List` value):
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Cacheable("books")
|
||||
public Flux<Book> findBooks(String author) {...}
|
||||
----
|
||||
|
||||
Such `CompletableFuture` and reactive adaptation also works for synchronized caching,
|
||||
computing the value only once in case of a concurrent cache miss:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Cacheable(cacheNames="foos", sync=true) <1>
|
||||
public CompletableFuture<Foo> executeExpensiveOperation(String id) {...}
|
||||
----
|
||||
<1> Using the `sync` attribute.
|
||||
|
||||
NOTE: In order for such an arrangement to work at runtime, the configured cache
|
||||
needs to be capable of `CompletableFuture`-based retrieval. The Spring-provided
|
||||
`ConcurrentMapCacheManager` automatically adapts to that retrieval style, and
|
||||
`CaffeineCacheManager` natively supports it when its asynchronous cache mode is
|
||||
enabled: set `setAsyncCacheMode(true)` on your `CaffeineCacheManager` instance.
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Bean
|
||||
CacheManager cacheManager() {
|
||||
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
|
||||
cacheManager.setCacheSpecification(...);
|
||||
cacheManager.setAsyncCacheMode(true);
|
||||
return cacheManager;
|
||||
}
|
||||
----
|
||||
|
||||
Last but not least, be aware that annotation-driven caching is not appropriate
|
||||
for sophisticated reactive interactions involving composition and back pressure.
|
||||
If you choose to declare `@Cacheable` on specific reactive methods, consider the
|
||||
impact of the rather coarse-granular cache interaction which simply stores the
|
||||
emitted object for a `Mono` or even a pre-collected list of objects for a `Flux`.
|
||||
|
||||
[[cache-annotations-cacheable-condition]]
|
||||
=== Conditional Caching
|
||||
|
||||
@@ -337,6 +407,9 @@ other), such declarations should be avoided. Note also that such conditions shou
|
||||
on the result object (that is, the `#result` variable), as these are validated up-front to
|
||||
confirm the exclusion.
|
||||
|
||||
As of 6.1, `@CachePut` takes `CompletableFuture` and reactive return types into account,
|
||||
performing the put operation whenever the produced object is available.
|
||||
|
||||
|
||||
[[cache-annotations-evict]]
|
||||
== The `@CacheEvict` Annotation
|
||||
@@ -379,6 +452,9 @@ trigger, the return values are ignored (as they do not interact with the cache).
|
||||
not the case with `@Cacheable` which adds data to the cache or updates data in the cache
|
||||
and, thus, requires a result.
|
||||
|
||||
As of 6.1, `@CacheEvict` takes `CompletableFuture` and reactive return types into account,
|
||||
performing an after-invocation evict operation whenever processing has completed.
|
||||
|
||||
|
||||
[[cache-annotations-caching]]
|
||||
== The `@Caching` Annotation
|
||||
|
||||
@@ -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,20 @@ 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.jms.publish[`"jms.message.publish"`]
|
||||
|Time spent sending a JMS message to a destination by a message producer.
|
||||
|
||||
|xref:integration/observability.adoc#observability.jms.process[`"jms.message.process"`]
|
||||
|Processing time for a JMS message that was previously received by a message consumer.
|
||||
|
||||
|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 +88,97 @@ 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.jms]]
|
||||
== JMS messaging instrumentation
|
||||
|
||||
Spring Framework uses the Jakarta JMS instrumentation provided by Micrometer if the `io.micrometer:micrometer-core` dependency is on the classpath.
|
||||
The `io.micrometer.core.instrument.binder.jms.JmsInstrumentation` instruments `jakarta.jms.Session` and records the relevant observations.
|
||||
|
||||
This instrumentation will create 2 types of observations:
|
||||
|
||||
* `"jms.message.publish"` when a JMS message is sent to the broker, typically with `JmsTemplate`.
|
||||
* `"jms.message.process"` when a JMS message is processed by the application, typically with a `MessageListener` or a `@JmsListener` annotated method.
|
||||
|
||||
NOTE: currently there is no instrumentation for `"jms.message.receive"` observations as there is little value in measuring the time spent waiting for the reception of a message.
|
||||
Such an integration would typically instrument `MessageConsumer#receive` method calls. But once those return, the processing time is not measured and the trace scope cannot be propagated to the application.
|
||||
|
||||
By default, both observations share the same set of possible `KeyValues`:
|
||||
|
||||
.Low cardinality Keys
|
||||
[cols="a,a"]
|
||||
|===
|
||||
|Name | Description
|
||||
|`exception` |Class name of the exception thrown during the messaging operation (or "none").
|
||||
|`messaging.destination.temporary` _(required)_|Whether the destination is a `TemporaryQueue` or `TemporaryTopic` (values: `"true"` or `"false"`).
|
||||
|`messaging.operation` _(required)_|Name of JMS operation being performed (values: `"publish"` or `"process"`).
|
||||
|===
|
||||
|
||||
.High cardinality Keys
|
||||
[cols="a,a"]
|
||||
|===
|
||||
|Name | Description
|
||||
|`messaging.message.conversation_id` |The correlation ID of the JMS message.
|
||||
|`messaging.destination.name` |The name of destination the current message was sent to.
|
||||
|`messaging.message.id` |Value used by the messaging system as an identifier for the message.
|
||||
|===
|
||||
|
||||
[[observability.jms.publish]]
|
||||
=== JMS message Publication instrumentation
|
||||
|
||||
`"jms.message.publish"` observations are recorded when a JMS message is sent to the broker.
|
||||
They measure the time spent sending the message and propagate the tracing information with outgoing JMS message headers.
|
||||
|
||||
You will need to configure the `ObservationRegistry` on the `JmsTemplate` to enable observations:
|
||||
|
||||
include-code::./JmsTemplatePublish[]
|
||||
|
||||
It uses the `io.micrometer.core.instrument.binder.jms.DefaultJmsPublishObservationConvention` by default, backed by the `io.micrometer.core.instrument.binder.jms.JmsPublishObservationContext`.
|
||||
|
||||
[[observability.jms.process]]
|
||||
=== JMS message Processing instrumentation
|
||||
|
||||
`"jms.message.process"` observations are recorded when a JMS message is processed by the application.
|
||||
They measure the time spent processing the message and propagate the tracing context with incoming JMS message headers.
|
||||
|
||||
Most applications will use the xref:integration/jms/annotated.adoc#jms-annotated[`@JmsListener` annotated methods] mechanism to process incoming messages.
|
||||
You will need to ensure that the `ObservationRegistry` is configured on the dedicated `JmsListenerContainerFactory`:
|
||||
|
||||
include-code::./JmsConfiguration[]
|
||||
|
||||
A xref:integration/jms/annotated.adoc#jms-annotated-support[default container factory is required to enable the annotation support],
|
||||
but note that `@JmsListener` annotations can refer to specific container factory beans for specific purposes.
|
||||
In all cases, Observations are only recorded if the observation registry is configured on the container factory.
|
||||
|
||||
Similar observations are recorded with `JmsTemplate` when messages are processed by a `MessageListener`.
|
||||
Such listeners are set on a `MessageConsumer` within a session callback (see `JmsTemplate.execute(SessionCallback<T>)`).
|
||||
|
||||
This observation uses the `io.micrometer.core.instrument.binder.jms.DefaultJmsProcessObservationConvention` by default, backed by the `io.micrometer.core.instrument.binder.jms.JmsProcessObservationContext`.
|
||||
|
||||
[[observability.http-server]]
|
||||
== HTTP Server instrumentation
|
||||
@@ -107,7 +207,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 +225,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[]
|
||||
@@ -190,6 +294,32 @@ Instrumentation uses the `org.springframework.http.client.observation.ClientRequ
|
||||
|===
|
||||
|
||||
|
||||
[[observability.http-client.restclient]]
|
||||
=== RestClient
|
||||
|
||||
Applications must configure an `ObservationRegistry` on the `RestClient.Builder` to enable the instrumentation; without that, observations are "no-ops".
|
||||
|
||||
Instrumentation uses the `org.springframework.http.client.observation.ClientRequestObservationConvention` by default, backed by the `ClientRequestObservationContext`.
|
||||
|
||||
.Low cardinality Keys
|
||||
[cols="a,a"]
|
||||
|===
|
||||
|Name | Description
|
||||
|`method` _(required)_|Name of HTTP request method or `"none"` if the request could not be created.
|
||||
|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. Only the path part of the URI is considered.
|
||||
|`client.name` _(required)_|Client name derived from the request URI host.
|
||||
|`status` _(required)_|HTTP response raw status code, or `"IO_ERROR"` in case of `IOException`, or `"CLIENT_ERROR"` if no response was received.
|
||||
|`outcome` _(required)_|Outcome of the HTTP client exchange.
|
||||
|`exception` _(required)_|Name of the exception thrown during the exchange, or `"none"` if no exception happened.
|
||||
|===
|
||||
|
||||
.High cardinality Keys
|
||||
[cols="a,a"]
|
||||
|===
|
||||
|Name | Description
|
||||
|`http.url` _(required)_|HTTP request URI.
|
||||
|===
|
||||
|
||||
|
||||
[[observability.http-client.webclient]]
|
||||
=== WebClient
|
||||
@@ -219,3 +349,28 @@ Instrumentation uses the `org.springframework.web.reactive.function.client.Clien
|
||||
|===
|
||||
|
||||
|
||||
[[observability.application-events]]
|
||||
== Application Events and `@EventListener`
|
||||
|
||||
Spring Framework does not contribute Observations for xref:core/beans/context-introduction.adoc#context-functionality-events-annotation[`@EventListener` calls],
|
||||
as they don't have the right semantics for such instrumentation.
|
||||
By default, event publication and processing is done synchronously and on the same Thread.
|
||||
This means that during the execution of that task, the ThreadLocals and logging context will be the same as the event publisher.
|
||||
|
||||
If the application configures globally a custom `ApplicationEventMulticaster` with a strategy that schedules event processing on different threads, this is no longer true.
|
||||
All `@EventListener` methods will be processed on a different thread, outstide of the main event publication thread.
|
||||
In these cases, the https://micrometer.io/docs/contextPropagation[Micrometer Context Propagation library] can help propagating such values and better correlate the processing of the events.
|
||||
The application can configure the chosen `TaskExecutor` to use a `ContextPropagatingTaskDecorator` that decorates tasks and propagates context.
|
||||
For this to work, the `io.micrometer:context-propagation` library must be present on the classpath:
|
||||
|
||||
include-code::./ApplicationEventsConfiguration[]
|
||||
|
||||
Similarly, if that asynchronous choice is made locally for each `@EventListener` annotated method, by adding an `@Async` method to it,
|
||||
you can choose a `TaskExecutor` that propagates context by referring to it by its qualifier.
|
||||
Given the following `TaskExecutor` bean definition, configured with the dedicated task decorator:
|
||||
|
||||
include-code::./EventAsyncExecutionConfiguration[]
|
||||
|
||||
Annotating event listeners with `@Async` and the relevant qualifier will achieve similar context propagation results:
|
||||
|
||||
include-code::./EmailNotificationListener[]
|
||||
|
||||
@@ -3,11 +3,467 @@
|
||||
|
||||
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`
|
||||
|
||||
The `RestClient` is a synchronous HTTP client that offers a modern, fluent API.
|
||||
It offers an abstraction over HTTP libraries that allows for convenient conversion from Java object to HTTP request, and creation of objects from the HTTP response.
|
||||
|
||||
=== Creating a `RestClient`
|
||||
|
||||
The `RestClient` is created using one of the static `create` methods.
|
||||
You can also use `builder` to get a builder with further options, such as specifying which HTTP library to use (see <<rest-request-factories>>) and which message converters to use (see <<rest-message-conversion>>), setting a default URI, default path variables, a default request headers, or `uriBuilderFactory`, or registering interceptors and initializers.
|
||||
|
||||
Once created (or built), the `RestClient` can be used safely by multiple threads.
|
||||
|
||||
The following sample shows how to create a default `RestClient`, and how to build a custom one.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim",role="primary"]
|
||||
----
|
||||
RestClient defaultClient = RestClient.create();
|
||||
|
||||
RestClient customClient = RestClient.builder()
|
||||
.requestFactory(new HttpComponentsClientHttpRequestFactory())
|
||||
.messageConverters(converters -> converters.add(new MyCustomMessageConverter()))
|
||||
.baseUrl("https://example.com")
|
||||
.defaultUriVariables(Map.of("variable", "foo"))
|
||||
.defaultHeader("My-Header", "Foo")
|
||||
.requestInterceptor(myCustomInterceptor)
|
||||
.requestInitializer(myCustomInitializer)
|
||||
.build();
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim",role="secondary"]
|
||||
----
|
||||
val defaultClient = RestClient.create()
|
||||
|
||||
val customClient = RestClient.builder()
|
||||
.requestFactory(HttpComponentsClientHttpRequestFactory())
|
||||
.messageConverters(converters -> converters.add(MyCustomMessageConverter()))
|
||||
.baseUrl("https://example.com")
|
||||
.defaultUriVariables(Map.of("variable", "foo"))
|
||||
.defaultHeader("My-Header", "Foo")
|
||||
.requestInterceptor(myCustomInterceptor)
|
||||
.requestInitializer(myCustomInitializer)
|
||||
.build()
|
||||
----
|
||||
======
|
||||
|
||||
=== Using the `RestClient`
|
||||
|
||||
When making an HTTP request with the `RestClient`, the first thing to specify is which HTTP method to use.
|
||||
This can be done with `method(HttpMethod)`, or with the convenience methods `get()`, `head()`, `post()`, and so on.
|
||||
|
||||
==== Request URL
|
||||
|
||||
Next, the request URI can be specified with the `uri` methods
|
||||
This step is optional, and can be skipped if the `RestClient` is configured with a default URI.
|
||||
The URL is typically specified as `String`, with optional URI template variables.
|
||||
String URLs are encoded by default, but this can be changed by building a client with a custom `uriBuilderFactory`.
|
||||
|
||||
The URL can also be provided with a function, or as `java.net.URI`, both of which are not encoded.
|
||||
For more details on working with and encoding URIs, see xref:web/webmvc/mvc-uri-building.adoc[URI Links].
|
||||
|
||||
==== Request headers and body
|
||||
|
||||
If necessary, the HTTP request can be manipulated, by adding request headers with `header(String, String)`, `headers(Consumer<HttpHeaders>`, or with the convenience methods `accept(MediaType...)`, `acceptCharset(Charset...)` and so on.
|
||||
For HTTP request that can contain a body (`POST`, `PUT`, and `PATCH`), additional methods are available: `contentType(MediaType)`, and `contentLength(long)`.
|
||||
|
||||
The request body itself can be set by `body(Object)`, which internally uses <<rest-message-conversion>>.
|
||||
Alternatively, the request body can be set using a `ParameterizedTypeReference`, allowing you to use generics.
|
||||
Finally, the body can be set to a callback function that writes to an `OutputStream`.
|
||||
|
||||
==== Retrieving the response
|
||||
Once the request has been set up, the HTTP response is accessed by invoking `retrieve()`.
|
||||
The response body can be accessed by using `body(Class)`, or `body(ParameterizedTypeReference)` for parameterized types like lists.
|
||||
The `body` method converts the response contents into various types, for instance bytes can be converted into a `String`, JSON into objects using Jackson, and so on (see <<rest-message-conversion>>).
|
||||
|
||||
The response can also be converted into a `ResponseEntity`, giving access to the response headers as well as the body.
|
||||
|
||||
This sample shows how `RestClient` can be used to perform a simple GET request.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
String result = restClient.get() <1>
|
||||
.uri("https://example.com") <2>
|
||||
.retrieve() <3>
|
||||
.body(String.class); <4>
|
||||
|
||||
System.out.println(result); <5>
|
||||
----
|
||||
<1> Set up a GET request
|
||||
<2> Specify the URL to connect to
|
||||
<3> Retrieve the response
|
||||
<4> Convert the response into a string
|
||||
<5> Print the result
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
val result= restClient.get() <1>
|
||||
.uri("https://example.com") <2>
|
||||
.retrieve() <3>
|
||||
.body<String>() <4>
|
||||
|
||||
println(result) <5>
|
||||
----
|
||||
<1> Set up a GET request
|
||||
<2> Specify the URL to connect to
|
||||
<3> Retrieve the response
|
||||
<4> Convert the response into a string
|
||||
<5> Print the result
|
||||
======
|
||||
|
||||
Access to the response status code and headers is provided through `ResponseEntity`:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
ResponseEntity<String> result = restClient.get() <1>
|
||||
.uri("https://example.com") <1>
|
||||
.retrieve()
|
||||
.toEntity(String.class); <2>
|
||||
|
||||
System.out.println("Response status: " + result.getStatusCode()); <3>
|
||||
System.out.println("Response headers: " + result.getHeaders()); <3>
|
||||
System.out.println("Contents: " + result.getBody()); <3>
|
||||
----
|
||||
<1> Set up a GET request for the specified URL
|
||||
<2> Convert the response into a `ResponseEntity`
|
||||
<3> Print the result
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
val result = restClient.get() <1>
|
||||
.uri("https://example.com") <1>
|
||||
.retrieve()
|
||||
.toEntity<String>() <2>
|
||||
|
||||
println("Response status: " + result.statusCode) <3>
|
||||
println("Response headers: " + result.headers) <3>
|
||||
println("Contents: " + result.body) <3>
|
||||
----
|
||||
<1> Set up a GET request for the specified URL
|
||||
<2> Convert the response into a `ResponseEntity`
|
||||
<3> Print the result
|
||||
======
|
||||
|
||||
`RestClient` can convert JSON to objects, using the Jackson library.
|
||||
Note the usage of uri variables in this sample, and that the `Accept` header is set to JSON.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
int id = ...;
|
||||
Pet pet = restClient.get()
|
||||
.uri("https://petclinic.example.com/pets/{id}", id) <1>
|
||||
.accept(APPLICATION_JSON) <2>
|
||||
.retrieve()
|
||||
.body(Pet.class); <3>
|
||||
----
|
||||
<1> Using URI variables
|
||||
<2> Set the `Accept` header to `application/json`
|
||||
<3> Convert the JSON response into a `Pet` domain object
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
val id = ...
|
||||
val pet = restClient.get()
|
||||
.uri("https://petclinic.example.com/pets/{id}", id) <1>
|
||||
.accept(APPLICATION_JSON) <2>
|
||||
.retrieve()
|
||||
.body<Pet>() <3>
|
||||
----
|
||||
<1> Using URI variables
|
||||
<2> Set the `Accept` header to `application/json`
|
||||
<3> Convert the JSON response into a `Pet` domain object
|
||||
======
|
||||
|
||||
In the next sample, `RestClient` is used to perform a POST request that contains JSON, which again is converted using Jackson.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
Pet pet = ... <1>
|
||||
ResponseEntity<Void> response = restClient.post() <2>
|
||||
.uri("https://petclinic.example.com/pets/new") <2>
|
||||
.contentType(APPLICATION_JSON) <3>
|
||||
.body(pet) <4>
|
||||
.retrieve()
|
||||
.toBodilessEntity(); <5>
|
||||
----
|
||||
<1> Create a `Pet` domain object
|
||||
<2> Set up a POST request, and the URL to connect to
|
||||
<3> Set the `Content-Type` header to `application/json`
|
||||
<4> Use `pet` as the request body
|
||||
<5> Convert the response into a response entity with no body.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
val pet: Pet = ... <1>
|
||||
val response = restClient.post() <2>
|
||||
.uri("https://petclinic.example.com/pets/new") <2>
|
||||
.contentType(APPLICATION_JSON) <3>
|
||||
.body(pet) <4>
|
||||
.retrieve()
|
||||
.toBodilessEntity() <5>
|
||||
----
|
||||
<1> Create a `Pet` domain object
|
||||
<2> Set up a POST request, and the URL to connect to
|
||||
<3> Set the `Content-Type` header to `application/json`
|
||||
<4> Use `pet` as the request body
|
||||
<5> Convert the response into a response entity with no body.
|
||||
======
|
||||
|
||||
==== Error handling
|
||||
By default, `RestClient` throws a subclass of `RestClientException` when retrieving a response with a 4xx or 5xx status code.
|
||||
This behavior can be overriden using `onStatus`.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
String result = restClient.get() <1>
|
||||
.uri("https://example.com/this-url-does-not-exist") <1>
|
||||
.retrieve()
|
||||
.onStatus(HttpStatusCode::is4xxClientError, (request, response) -> { <2>
|
||||
throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) <3>
|
||||
})
|
||||
.body(String.class);
|
||||
----
|
||||
<1> Create a GET request for a URL that returns a 404 status code
|
||||
<2> Set up a status handler for all 4xx status codes
|
||||
<3> Throw a custom exception
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
val result = restClient.get() <1>
|
||||
.uri("https://example.com/this-url-does-not-exist") <1>
|
||||
.retrieve()
|
||||
.onStatus(HttpStatusCode::is4xxClientError) { _, response -> <2>
|
||||
throw MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) } <3>
|
||||
.body<String>()
|
||||
----
|
||||
<1> Create a GET request for a URL that returns a 404 status code
|
||||
<2> Set up a status handler for all 4xx status codes
|
||||
<3> Throw a custom exception
|
||||
======
|
||||
|
||||
==== Exchange
|
||||
For more advanced scenarios, the `RestClient` gives access to the underlying HTTP request and response through the `exchange` method, which can be used instead of `retrieve()`.
|
||||
Status handlers are not applied when you exchange, because the exchange function already provides access to the full response, allowing you to perform any error handling necessary.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
Pet result = restClient.get()
|
||||
.uri("https://petclinic.example.com/pets/{id}", id)
|
||||
.accept(APPLICATION_JSON)
|
||||
.exchange((request, response) -> { <1>
|
||||
if (response.getStatusCode().is4xxClientError()) { <2>
|
||||
throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); <2>
|
||||
}
|
||||
else {
|
||||
Pet pet = convertResponse(response); <3>
|
||||
return pet;
|
||||
}
|
||||
});
|
||||
----
|
||||
<1> `exchange` provides the request and response
|
||||
<2> Throw an exception when the response has a 4xx status code
|
||||
<3> Convert the response into a Pet domain object
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
val result = restClient.get()
|
||||
.uri("https://petclinic.example.com/pets/{id}", id)
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.exchange { request, response -> <1>
|
||||
if (response.getStatusCode().is4xxClientError()) { <2>
|
||||
throw MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) <2>
|
||||
} else {
|
||||
val pet: Pet = convertResponse(response) <3>
|
||||
pet
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> `exchange` provides the request and response
|
||||
<2> Throw an exception when the response has a 4xx status code
|
||||
<3> Convert the response into a Pet domain object
|
||||
======
|
||||
|
||||
|
||||
[[rest-message-conversion]]
|
||||
=== HTTP Message Conversion
|
||||
[.small]#xref:web/webflux/reactive-spring.adoc#webflux-codecs[See equivalent in the Reactive stack]#
|
||||
|
||||
The `spring-web` module contains the `HttpMessageConverter` interface for reading and writing the body of HTTP requests and responses through `InputStream` and `OutputStream`.
|
||||
`HttpMessageConverter` instances are used on the client side (for example, in the `RestClient`) and on the server side (for example, in Spring MVC REST controllers).
|
||||
|
||||
Concrete implementations for the main media (MIME) types are provided in the framework and are, by default, registered with the `RestClient` and `RestTemplate` on the client side and with `RequestMappingHandlerAdapter` on the server side (see xref:web/webmvc/mvc-config/message-converters.adoc[Configuring Message Converters]).
|
||||
|
||||
Several implementations of `HttpMessageConverter` are described below.
|
||||
Refer to the {api-spring-framework}/http/converter/HttpMessageConverter.html[`HttpMessageConverter` Javadoc] for the complete list.
|
||||
For all converters, a default media type is used, but you can override it by setting the `supportedMediaTypes` property.
|
||||
|
||||
[[rest-message-converters-tbl]]
|
||||
.HttpMessageConverter Implementations
|
||||
[cols="1,3"]
|
||||
|===
|
||||
| MessageConverter | Description
|
||||
|
||||
| `StringHttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write `String` instances from the HTTP request and response.
|
||||
By default, this converter supports all text media types(`text/{asterisk}`) and writes with a `Content-Type` of `text/plain`.
|
||||
|
||||
| `FormHttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write form data from the HTTP request and response.
|
||||
By default, this converter reads and writes the `application/x-www-form-urlencoded` media type.
|
||||
Form data is read from and written into a `MultiValueMap<String, String>`.
|
||||
The converter can also write (but not read) multipart data read from a `MultiValueMap<String, Object>`.
|
||||
By default, `multipart/form-data` is supported.
|
||||
Additional multipart subtypes can be supported for writing form data.
|
||||
Consult the javadoc for `FormHttpMessageConverter` for further details.
|
||||
|
||||
| `ByteArrayHttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write byte arrays from the HTTP request and response.
|
||||
By default, this converter supports all media types (`{asterisk}/{asterisk}`) and writes with a `Content-Type` of `application/octet-stream`.
|
||||
You can override this by setting the `supportedMediaTypes` property and overriding `getContentType(byte[])`.
|
||||
|
||||
| `MarshallingHttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write XML by using Spring's `Marshaller` and `Unmarshaller` abstractions from the `org.springframework.oxm` package.
|
||||
This converter requires a `Marshaller` and `Unmarshaller` before it can be used.
|
||||
You can inject these through constructor or bean properties.
|
||||
By default, this converter supports `text/xml` and `application/xml`.
|
||||
|
||||
| `MappingJackson2HttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write JSON by using Jackson's `ObjectMapper`.
|
||||
You can customize JSON mapping as needed through the use of Jackson's provided annotations.
|
||||
When you need further control (for cases where custom JSON serializers/deserializers need to be provided for specific types), you can inject a custom `ObjectMapper` through the `ObjectMapper` property.
|
||||
By default, this converter supports `application/json`.
|
||||
|
||||
| `MappingJackson2XmlHttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write XML by using https://github.com/FasterXML/jackson-dataformat-xml[Jackson XML] extension's `XmlMapper`.
|
||||
You can customize XML mapping as needed through the use of JAXB or Jackson's provided annotations.
|
||||
When you need further control (for cases where custom XML serializers/deserializers need to be provided for specific types), you can inject a custom `XmlMapper` through the `ObjectMapper` property.
|
||||
By default, this converter supports `application/xml`.
|
||||
|
||||
| `SourceHttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write `javax.xml.transform.Source` from the HTTP request and response.
|
||||
Only `DOMSource`, `SAXSource`, and `StreamSource` are supported.
|
||||
By default, this converter supports `text/xml` and `application/xml`.
|
||||
|
||||
|===
|
||||
|
||||
By default, `RestClient` and `RestTemplate` register all built-in message converters, depending on the availability of underlying libraries on the classpath.
|
||||
You can also set the message converters to use explicitly, by using `messageConverters` on the `RestClient` builder, or via the `messageConverters` property of `RestTemplate`.
|
||||
|
||||
==== Jackson JSON Views
|
||||
|
||||
To serialize only a subset of the object properties, you can specify a https://www.baeldung.com/jackson-json-view-annotation[Jackson JSON View], as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim"]
|
||||
----
|
||||
MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23"));
|
||||
value.setSerializationView(User.WithoutPasswordView.class);
|
||||
|
||||
ResponseEntity<Void> response = restClient.post() // or RestTemplate.postForEntity
|
||||
.contentType(APPLICATION_JSON)
|
||||
.body(value)
|
||||
.retrieve()
|
||||
.toBodilessEntity();
|
||||
|
||||
----
|
||||
|
||||
==== Multipart
|
||||
|
||||
To send multipart data, you need to provide a `MultiValueMap<String, Object>` whose values may be an `Object` for part content, a `Resource` for a file part, or an `HttpEntity` for part content with headers.
|
||||
For example:
|
||||
|
||||
[source,java,indent=0,subs="verbatim"]
|
||||
----
|
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
|
||||
parts.add("fieldPart", "fieldValue");
|
||||
parts.add("filePart", new FileSystemResource("...logo.png"));
|
||||
parts.add("jsonPart", new Person("Jason"));
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_XML);
|
||||
parts.add("xmlPart", new HttpEntity<>(myBean, headers));
|
||||
|
||||
// send using RestClient.post or RestTemplate.postForEntity
|
||||
----
|
||||
|
||||
In most cases, you do not have to specify the `Content-Type` for each part.
|
||||
The content type is determined automatically based on the `HttpMessageConverter` chosen to serialize it or, in the case of a `Resource` based on the file extension.
|
||||
If necessary, you can explicitly provide the `MediaType` with an `HttpEntity` wrapper.
|
||||
|
||||
Once the `MultiValueMap` is ready, you can use it as the body of a POST request, using `RestClient.post().body(parts)` (or `RestTemplate.postForObject`).
|
||||
|
||||
If the `MultiValueMap` contains at least one non-`String` value, the `Content-Type` is set to `multipart/form-data` by the `FormHttpMessageConverter`.
|
||||
If the `MultiValueMap` has `String` values the `Content-Type` defaults to `application/x-www-form-urlencoded`.
|
||||
If necessary the `Content-Type` may also be set explicitly.
|
||||
|
||||
[[rest-request-factories]]
|
||||
=== Client Request Factories
|
||||
|
||||
To execute the HTTP request, `RestClient` uses a client HTTP library.
|
||||
These libraries are adapted via the `ClientRequestFactory` interface.
|
||||
Various implementations are available:
|
||||
|
||||
* `JdkClientHttpRequestFactory` for Java's `HttpClient`,
|
||||
* `HttpComponentsClientHttpRequestFactory` for use with Apache HTTP Components `HttpClient`,
|
||||
* `JettyClientHttpRequestFactory` for Jetty's `HttpClient`,
|
||||
* `ReactorNettyClientRequestFactory` for Reactor Netty's `HttpClient`,
|
||||
* `SimpleClientHttpRequestFactory` as a simple default.
|
||||
|
||||
|
||||
If no request factory is specified when the `RestClient` was built, it will use the Apache or Jetty `HttpClient` if they are available on the classpath.
|
||||
Otherwise, if the `java.net.http` module is loaded, it will use Java's `HttpClient`.
|
||||
Finally, it will resort to the simple default.
|
||||
|
||||
[[rest-webclient]]
|
||||
== `WebClient`
|
||||
|
||||
@@ -32,13 +488,11 @@ See xref:web/webflux-webclient.adoc[WebClient] for more details.
|
||||
[[rest-resttemplate]]
|
||||
== `RestTemplate`
|
||||
|
||||
The `RestTemplate` provides a higher level API over HTTP client libraries. It makes it
|
||||
easy to invoke REST endpoints in a single line. It exposes the following groups of
|
||||
overloaded methods:
|
||||
The `RestTemplate` provides a high-level API over HTTP client libraries in the form of a classic Spring Template class.
|
||||
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
|
||||
@@ -69,8 +523,7 @@ xref:web/webflux-webclient.adoc[WebClient] instead.
|
||||
|
||||
| `patchForObject`
|
||||
| Updates a resource by using PATCH and returns the representation from the response.
|
||||
Note that the JDK `HttpURLConnection` does not support `PATCH`, but Apache
|
||||
HttpComponents and others do.
|
||||
Note that the JDK `HttpURLConnection` does not support `PATCH`, but Apache HttpComponents and others do.
|
||||
|
||||
| `delete`
|
||||
| Deletes the resources at the specified URI by using DELETE.
|
||||
@@ -79,9 +532,8 @@ HttpComponents and others do.
|
||||
| Retrieves allowed HTTP methods for a resource by using ALLOW.
|
||||
|
||||
| `exchange`
|
||||
| More generalized (and less opinionated) version of the preceding methods that provides extra
|
||||
flexibility when needed. It accepts a `RequestEntity` (including HTTP method, URL, headers,
|
||||
and body as input) and returns a `ResponseEntity`.
|
||||
| More generalized (and less opinionated) version of the preceding methods that provides extra flexibility when needed.
|
||||
It accepts a `RequestEntity` (including HTTP method, URL, headers, and body as input) and returns a `ResponseEntity`.
|
||||
|
||||
These methods allow the use of `ParameterizedTypeReference` instead of `Class` to specify
|
||||
a response type with generics.
|
||||
@@ -92,276 +544,30 @@ preparation and response extraction through callback interfaces.
|
||||
|
||||
|===
|
||||
|
||||
[[rest-resttemplate-create]]
|
||||
=== Initialization
|
||||
|
||||
The default constructor uses `java.net.HttpURLConnection` to perform requests. You can
|
||||
switch to a different HTTP library with an implementation of `ClientHttpRequestFactory`.
|
||||
There is built-in support for the following:
|
||||
|
||||
* Apache HttpComponents
|
||||
* Netty
|
||||
* OkHttp
|
||||
|
||||
For example, to switch to Apache HttpComponents, you can use the following:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
RestTemplate template = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
|
||||
----
|
||||
|
||||
Each `ClientHttpRequestFactory` exposes configuration options specific to the underlying
|
||||
HTTP client library -- for example, for credentials, connection pooling, and other details.
|
||||
|
||||
TIP: Note that the `java.net` implementation for HTTP requests can raise an exception when
|
||||
accessing the status of a response that represents an error (such as 401). If this is an
|
||||
issue, switch to another HTTP client library.
|
||||
`RestTemplate` uses the same HTTP library abstraction as `RestClient`.
|
||||
By default, it uses the `SimpleClientHttpRequestFactory`, but this can be changed via the constructor.
|
||||
See <<rest-request-factories>>.
|
||||
|
||||
NOTE: `RestTemplate` can be instrumented for observability, in order to produce metrics and traces.
|
||||
See the xref:integration/observability.adoc#http-client.resttemplate[RestTemplate Observability support] section.
|
||||
|
||||
[[rest-resttemplate-uri]]
|
||||
==== URIs
|
||||
|
||||
Many of the `RestTemplate` methods accept a URI template and URI template variables,
|
||||
either as a `String` variable argument, or as `Map<String,String>`.
|
||||
|
||||
The following example uses a `String` variable argument:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
String result = restTemplate.getForObject(
|
||||
"https://example.com/hotels/{hotel}/bookings/{booking}", String.class, "42", "21");
|
||||
----
|
||||
|
||||
The following example uses a `Map<String, String>`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
Map<String, String> vars = Collections.singletonMap("hotel", "42");
|
||||
|
||||
String result = restTemplate.getForObject(
|
||||
"https://example.com/hotels/{hotel}/rooms/{hotel}", String.class, vars);
|
||||
----
|
||||
|
||||
Keep in mind URI templates are automatically encoded, as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
restTemplate.getForObject("https://example.com/hotel list", String.class);
|
||||
|
||||
// Results in request to "https://example.com/hotel%20list"
|
||||
----
|
||||
|
||||
You can use the `uriTemplateHandler` property of `RestTemplate` to customize how URIs
|
||||
are encoded. Alternatively, you can prepare a `java.net.URI` and pass it into one of
|
||||
the `RestTemplate` methods that accepts a `URI`.
|
||||
|
||||
For more details on working with and encoding URIs, see xref:web/webmvc/mvc-uri-building.adoc[URI Links].
|
||||
|
||||
[[rest-template-headers]]
|
||||
==== Headers
|
||||
|
||||
You can use the `exchange()` methods to specify request headers, as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
String uriTemplate = "https://example.com/hotels/{hotel}";
|
||||
URI uri = UriComponentsBuilder.fromUriString(uriTemplate).build(42);
|
||||
|
||||
RequestEntity<Void> requestEntity = RequestEntity.get(uri)
|
||||
.header("MyRequestHeader", "MyValue")
|
||||
.build();
|
||||
|
||||
ResponseEntity<String> response = template.exchange(requestEntity, String.class);
|
||||
|
||||
String responseHeader = response.getHeaders().getFirst("MyResponseHeader");
|
||||
String body = response.getBody();
|
||||
----
|
||||
|
||||
You can obtain response headers through many `RestTemplate` method variants that return
|
||||
`ResponseEntity`.
|
||||
|
||||
[[rest-template-body]]
|
||||
=== Body
|
||||
|
||||
Objects passed into and returned from `RestTemplate` methods are converted to and from raw
|
||||
content with the help of an `HttpMessageConverter`.
|
||||
|
||||
On a POST, an input object is serialized to the request body, as the following example shows:
|
||||
|
||||
----
|
||||
URI location = template.postForLocation("https://example.com/people", person);
|
||||
----
|
||||
|
||||
You need not explicitly set the Content-Type header of the request. In most cases,
|
||||
you can find a compatible message converter based on the source `Object` type, and the chosen
|
||||
message converter sets the content type accordingly. If necessary, you can use the
|
||||
`exchange` methods to explicitly provide the `Content-Type` request header, and that, in
|
||||
turn, influences what message converter is selected.
|
||||
|
||||
On a GET, the body of the response is deserialized to an output `Object`, as the following example shows:
|
||||
|
||||
----
|
||||
Person person = restTemplate.getForObject("https://example.com/people/{id}", Person.class, 42);
|
||||
----
|
||||
|
||||
The `Accept` header of the request does not need to be explicitly set. In most cases,
|
||||
a compatible message converter can be found based on the expected response type, which
|
||||
then helps to populate the `Accept` header. If necessary, you can use the `exchange`
|
||||
methods to provide the `Accept` header explicitly.
|
||||
|
||||
By default, `RestTemplate` registers all built-in
|
||||
xref:integration/rest-clients.adoc#rest-message-conversion[message converters], depending on classpath checks that help
|
||||
to determine what optional conversion libraries are present. You can also set the message
|
||||
converters to use explicitly.
|
||||
|
||||
[[rest-message-conversion]]
|
||||
==== Message Conversion
|
||||
[.small]#xref:web/webflux/reactive-spring.adoc#webflux-codecs[See equivalent in the Reactive stack]#
|
||||
|
||||
The `spring-web` module contains the `HttpMessageConverter` contract for reading and
|
||||
writing the body of HTTP requests and responses through `InputStream` and `OutputStream`.
|
||||
`HttpMessageConverter` instances are used on the client side (for example, in the `RestTemplate`) and
|
||||
on the server side (for example, in Spring MVC REST controllers).
|
||||
|
||||
Concrete implementations for the main media (MIME) types are provided in the framework
|
||||
and are, by default, registered with the `RestTemplate` on the client side and with
|
||||
`RequestMappingHandlerAdapter` on the server side (see
|
||||
xref:web/webmvc/mvc-config/message-converters.adoc[Configuring Message Converters]).
|
||||
|
||||
The implementations of `HttpMessageConverter` are described in the following sections.
|
||||
For all converters, a default media type is used, but you can override it by setting the
|
||||
`supportedMediaTypes` bean property. The following table describes each implementation:
|
||||
|
||||
[[rest-message-converters-tbl]]
|
||||
.HttpMessageConverter Implementations
|
||||
[cols="1,3"]
|
||||
|===
|
||||
| MessageConverter | Description
|
||||
|
||||
| `StringHttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write `String` instances from the HTTP
|
||||
request and response. By default, this converter supports all text media types
|
||||
(`text/{asterisk}`) and writes with a `Content-Type` of `text/plain`.
|
||||
|
||||
| `FormHttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write form data from the HTTP
|
||||
request and response. By default, this converter reads and writes the
|
||||
`application/x-www-form-urlencoded` media type. Form data is read from and written into a
|
||||
`MultiValueMap<String, String>`. The converter can also write (but not read) multipart
|
||||
data read from a `MultiValueMap<String, Object>`. By default, `multipart/form-data` is
|
||||
supported. As of Spring Framework 5.2, additional multipart subtypes can be supported for
|
||||
writing form data. Consult the javadoc for `FormHttpMessageConverter` for further details.
|
||||
|
||||
| `ByteArrayHttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write byte arrays from the
|
||||
HTTP request and response. By default, this converter supports all media types (`{asterisk}/{asterisk}`)
|
||||
and writes with a `Content-Type` of `application/octet-stream`. You can override this
|
||||
by setting the `supportedMediaTypes` property and overriding `getContentType(byte[])`.
|
||||
|
||||
| `MarshallingHttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write XML by using Spring's
|
||||
`Marshaller` and `Unmarshaller` abstractions from the `org.springframework.oxm` package.
|
||||
This converter requires a `Marshaller` and `Unmarshaller` before it can be used. You can inject these
|
||||
through constructor or bean properties. By default, this converter supports
|
||||
`text/xml` and `application/xml`.
|
||||
|
||||
| `MappingJackson2HttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write JSON by using Jackson's
|
||||
`ObjectMapper`. You can customize JSON mapping as needed through the use of Jackson's
|
||||
provided annotations. When you need further control (for cases where custom JSON
|
||||
serializers/deserializers need to be provided for specific types), you can inject a custom `ObjectMapper`
|
||||
through the `ObjectMapper` property. By default, this
|
||||
converter supports `application/json`.
|
||||
|
||||
| `MappingJackson2XmlHttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write XML by using
|
||||
https://github.com/FasterXML/jackson-dataformat-xml[Jackson XML] extension's
|
||||
`XmlMapper`. You can customize XML mapping as needed through the use of JAXB
|
||||
or Jackson's provided annotations. When you need further control (for cases where custom XML
|
||||
serializers/deserializers need to be provided for specific types), you can inject a custom `XmlMapper`
|
||||
through the `ObjectMapper` property. By default, this
|
||||
converter supports `application/xml`.
|
||||
|
||||
| `SourceHttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write
|
||||
`javax.xml.transform.Source` from the HTTP request and response. Only `DOMSource`,
|
||||
`SAXSource`, and `StreamSource` are supported. By default, this converter supports
|
||||
`text/xml` and `application/xml`.
|
||||
|
||||
| `BufferedImageHttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write
|
||||
`java.awt.image.BufferedImage` from the HTTP request and response. This converter reads
|
||||
and writes the media type supported by the Java I/O API.
|
||||
|
||||
|===
|
||||
|
||||
[[rest-template-jsonview]]
|
||||
=== Jackson JSON Views
|
||||
|
||||
You can specify a https://www.baeldung.com/jackson-json-view-annotation[Jackson JSON View]
|
||||
to serialize only a subset of the object properties, as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23"));
|
||||
value.setSerializationView(User.WithoutPasswordView.class);
|
||||
|
||||
RequestEntity<MappingJacksonValue> requestEntity =
|
||||
RequestEntity.post(new URI("https://example.com/user")).body(value);
|
||||
|
||||
ResponseEntity<String> response = template.exchange(requestEntity, String.class);
|
||||
----
|
||||
|
||||
[[rest-template-multipart]]
|
||||
=== Multipart
|
||||
|
||||
To send multipart data, you need to provide a `MultiValueMap<String, Object>` whose values
|
||||
may be an `Object` for part content, a `Resource` for a file part, or an `HttpEntity` for
|
||||
part content with headers. For example:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
|
||||
parts.add("fieldPart", "fieldValue");
|
||||
parts.add("filePart", new FileSystemResource("...logo.png"));
|
||||
parts.add("jsonPart", new Person("Jason"));
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_XML);
|
||||
parts.add("xmlPart", new HttpEntity<>(myBean, headers));
|
||||
----
|
||||
|
||||
In most cases, you do not have to specify the `Content-Type` for each part. The content
|
||||
type is determined automatically based on the `HttpMessageConverter` chosen to serialize
|
||||
it or, in the case of a `Resource` based on the file extension. If necessary, you can
|
||||
explicitly provide the `MediaType` with an `HttpEntity` wrapper.
|
||||
|
||||
Once the `MultiValueMap` is ready, you can pass it to the `RestTemplate`, as show below:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
MultiValueMap<String, Object> parts = ...;
|
||||
template.postForObject("https://example.com/upload", parts, Void.class);
|
||||
----
|
||||
|
||||
If the `MultiValueMap` contains at least one non-`String` value, the `Content-Type` is set
|
||||
to `multipart/form-data` by the `FormHttpMessageConverter`. If the `MultiValueMap` has
|
||||
`String` values the `Content-Type` is defaulted to `application/x-www-form-urlencoded`.
|
||||
If necessary the `Content-Type` may also be set explicitly.
|
||||
|
||||
Objects passed into and returned from `RestTemplate` methods are converted to and from HTTP messages with the help of an `HttpMessageConverter`, see <<rest-message-conversion>>.
|
||||
|
||||
[[rest-http-interface]]
|
||||
== HTTP Interface
|
||||
|
||||
The Spring Framework lets you define an HTTP service as a Java interface with annotated
|
||||
methods for HTTP exchanges. You can then generate a proxy that implements this interface
|
||||
and performs the exchanges. This helps to simplify HTTP remote access which often
|
||||
involves a facade that wraps the details of using the underlying HTTP client.
|
||||
The Spring Framework lets you define an HTTP service as a Java interface with
|
||||
`@HttpExchange` methods. You can pass such an interface to `HttpServiceProxyFactory`
|
||||
to create a proxy which performs requests through an HTTP client such as `RestClient`
|
||||
or `WebClient`. You can also implement the interface from an `@Controller` for server
|
||||
request handling.
|
||||
|
||||
One, declare an interface with `@HttpExchange` methods:
|
||||
Start by creating the interface with `@HttpExchange` methods:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@@ -375,12 +581,38 @@ One, declare an interface with `@HttpExchange` methods:
|
||||
}
|
||||
----
|
||||
|
||||
Two, create a proxy that will perform the declared HTTP exchanges:
|
||||
Now you can create a proxy that performs requests when methods are called.
|
||||
|
||||
For `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);
|
||||
----
|
||||
|
||||
For `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);
|
||||
----
|
||||
|
||||
For `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);
|
||||
----
|
||||
@@ -448,6 +680,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
|
||||
@@ -459,43 +695,74 @@ method parameters:
|
||||
[[rest-http-interface-return-values]]
|
||||
=== Return Values
|
||||
|
||||
Annotated, HTTP exchange methods support the following return values:
|
||||
The supported return values depend on the underlying client.
|
||||
|
||||
Clients adapted to `HttpExchangeAdapter` such as `RestClient` and `RestTemplate`
|
||||
support synchronous return values:
|
||||
|
||||
[cols="1,2", options="header"]
|
||||
|===
|
||||
| Method return value | Description
|
||||
|
||||
| `void`, `Mono<Void>`
|
||||
| Perform the given request, and release the response content, if any.
|
||||
| `void`
|
||||
| Perform the given request.
|
||||
|
||||
| `HttpHeaders`, `Mono<HttpHeaders>`
|
||||
| Perform the given request, release the response content, if any, and return the
|
||||
response headers.
|
||||
| `HttpHeaders`
|
||||
| Perform the given request and return the response headers.
|
||||
|
||||
| `<T>`, `Mono<T>`
|
||||
| `<T>`
|
||||
| Perform the given request and decode the response content to the declared return type.
|
||||
|
||||
| `<T>`, `Flux<T>`
|
||||
| Perform the given request and decode the response content to a stream of the declared
|
||||
element type.
|
||||
| `ResponseEntity<Void>`
|
||||
| Perform the given request and return a `ResponseEntity` with the status and headers.
|
||||
|
||||
| `ResponseEntity<Void>`, `Mono<ResponseEntity<Void>>`
|
||||
| Perform the given request, and release the response content, if any, and return a
|
||||
`ResponseEntity` with the status and headers.
|
||||
|
||||
| `ResponseEntity<T>`, `Mono<ResponseEntity<T>>`
|
||||
| `ResponseEntity<T>`
|
||||
| Perform the given request, decode the response content to the declared return type, and
|
||||
return a `ResponseEntity` with the status, headers, and the decoded body.
|
||||
|
||||
|===
|
||||
|
||||
Clients adapted to `ReactorHttpExchangeAdapter` such as `WebClient`, support all of above
|
||||
as well as reactive variants. The table below shows Reactor types, but you can also use
|
||||
other reactive types that are supported through the `ReactiveAdapterRegistry`:
|
||||
|
||||
[cols="1,2", options="header"]
|
||||
|===
|
||||
| Method return value | Description
|
||||
|
||||
| `Mono<Void>`
|
||||
| Perform the given request, and release the response content, if any.
|
||||
|
||||
| `Mono<HttpHeaders>`
|
||||
| Perform the given request, release the response content, if any, and return the
|
||||
response headers.
|
||||
|
||||
| `Mono<T>`
|
||||
| Perform the given request and decode the response content to the declared return type.
|
||||
|
||||
| `Flux<T>`
|
||||
| Perform the given request and decode the response content to a stream of the declared
|
||||
element type.
|
||||
|
||||
| `Mono<ResponseEntity<Void>>`
|
||||
| Perform the given request, and release the response content, if any, and return a
|
||||
`ResponseEntity` with the status and headers.
|
||||
|
||||
| `Mono<ResponseEntity<T>>`
|
||||
| Perform the given request, decode the response content to the declared return type, and
|
||||
return a `ResponseEntity` with the status, headers, and the decoded body.
|
||||
|
||||
| `Mono<ResponseEntity<Flux<T>>`
|
||||
| Perform the given request, decode the response content to a stream of the declared
|
||||
element type, and return a `ResponseEntity` with the status, headers, and the decoded
|
||||
response body stream.
|
||||
element type, and return a `ResponseEntity` with the status, headers, and the decoded
|
||||
response body stream.
|
||||
|
||||
|===
|
||||
|
||||
TIP: You can also use any other async or reactive types registered in the
|
||||
`ReactiveAdapterRegistry`.
|
||||
By default, the timeout for synchronous return values with `ReactorHttpExchangeAdapter`
|
||||
depends on how the underlying HTTP client is configured. You can set a `blockTimeout`
|
||||
value on the adapter level as well, but we recommend relying on timeout settings of the
|
||||
underlying HTTP client, which operates at a lower level and provides more control.
|
||||
|
||||
|
||||
[[rest-http-interface-exceptions]]
|
||||
|
||||
@@ -66,6 +66,11 @@ The variants that Spring provides are as follows:
|
||||
compatible runtime environment (such as a Jakarta EE application server),
|
||||
replacing a CommonJ WorkManager for that purpose.
|
||||
|
||||
As of 6.1, `ThreadPoolTaskExecutor` provides a pause/resume capability and graceful
|
||||
shutdown through Spring's lifecycle management. There is also a new "virtualThreads"
|
||||
option on `SimpleAsyncTaskExecutor` which is aligned with JDK 21's Virtual Threads,
|
||||
as well as a graceful shutdown capability for `SimpleAsyncTaskExecutor` as well.
|
||||
|
||||
|
||||
[[scheduling-task-executor-usage]]
|
||||
=== Using a `TaskExecutor`
|
||||
@@ -244,6 +249,11 @@ to provide common bean-style configuration along the lines of `ThreadPoolTaskExe
|
||||
These variants work perfectly fine for locally embedded thread pool setups in lenient
|
||||
application server environments, as well -- in particular on Tomcat and Jetty.
|
||||
|
||||
As of 6.1, `ThreadPoolTaskScheduler` provides a pause/resume capability and graceful
|
||||
shutdown through Spring's lifecycle management. There is also a new option called
|
||||
`SimpleAsyncTaskScheduler` which is aligned with JDK 21's Virtual Threads, using a
|
||||
single scheduler thread but firing up a new thread for every scheduled task execution.
|
||||
|
||||
|
||||
|
||||
[[scheduling-annotation-support]]
|
||||
@@ -353,6 +363,17 @@ the amount of time to wait before the first execution of the method, as the foll
|
||||
}
|
||||
----
|
||||
|
||||
For one-time tasks, you can just specify an initial delay by indicating the amount
|
||||
of time to wait before the intended execution of the method:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Scheduled(initialDelay = 1000)
|
||||
public void doSomething() {
|
||||
// something that should run only once
|
||||
}
|
||||
----
|
||||
|
||||
If simple periodic scheduling is not expressive enough, you can provide a
|
||||
xref:integration/scheduling.adoc#scheduling-cron-expression[cron expression].
|
||||
The following example runs only on weekdays:
|
||||
@@ -392,6 +413,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
|
||||
|
||||
@@ -251,11 +251,14 @@ https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-featu
|
||||
=== Constructor injection
|
||||
|
||||
As described in the xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-di[dedicated section],
|
||||
JUnit 5 allows constructor injection of beans which is pretty useful with Kotlin
|
||||
JUnit Jupiter (JUnit 5) allows constructor injection of beans which is pretty useful with Kotlin
|
||||
in order to use `val` instead of `lateinit var`. You can use
|
||||
{api-spring-framework}/test/context/TestConstructor.html[`@TestConstructor(autowireMode = AutowireMode.ALL)`]
|
||||
to enable autowiring for all parameters.
|
||||
|
||||
NOTE: You can also change the default behavior to `ALL` in a `junit-platform.properties`
|
||||
file with a `spring.test.constructor.autowire.mode = all` property.
|
||||
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
@SpringJUnitConfig(TestConfig::class)
|
||||
@@ -272,11 +275,11 @@ class OrderServiceIntegrationTests(val orderService: OrderService,
|
||||
=== `PER_CLASS` Lifecycle
|
||||
|
||||
Kotlin lets you specify meaningful test function names between backticks (```).
|
||||
As of JUnit 5, Kotlin test classes can use the `@TestInstance(TestInstance.Lifecycle.PER_CLASS)`
|
||||
With JUnit Jupiter (JUnit 5), Kotlin test classes can use the `@TestInstance(TestInstance.Lifecycle.PER_CLASS)`
|
||||
annotation to enable single instantiation of test classes, which allows the use of `@BeforeAll`
|
||||
and `@AfterAll` annotations on non-static methods, which is a good fit for Kotlin.
|
||||
|
||||
You can also change the default behavior to `PER_CLASS` thanks to a `junit-platform.properties`
|
||||
NOTE: You can also change the default behavior to `PER_CLASS` in a `junit-platform.properties`
|
||||
file with a `junit.jupiter.testinstance.lifecycle.default = per_class` property.
|
||||
|
||||
The following example demonstrates `@BeforeAll` and `@AfterAll` annotations on non-static methods:
|
||||
|
||||
@@ -137,10 +137,12 @@ demonstrate its API and protocol features.
|
||||
|
||||
The `spring-messaging` module contains the following:
|
||||
|
||||
* xref:rsocket.adoc#rsocket-requester[RSocketRequester] -- fluent API to make requests through an `io.rsocket.RSocket`
|
||||
with data and metadata encoding/decoding.
|
||||
* xref:rsocket.adoc#rsocket-annot-responders[Annotated Responders] -- `@MessageMapping` annotated handler methods for
|
||||
responding.
|
||||
* xref:rsocket.adoc#rsocket-requester[RSocketRequester] -- fluent API to make requests
|
||||
through an `io.rsocket.RSocket` with data and metadata encoding/decoding.
|
||||
* xref:rsocket.adoc#rsocket-annot-responders[Annotated Responders] -- `@MessageMapping`
|
||||
and `@RSocketExchange` annotated handler methods for responding.
|
||||
* xref:rsocket.adoc#rsocket-interface[RSocket Interface] -- RSocket service declaration
|
||||
as Java interface with `@RSocketExchange` methods, for use as requester or responder.
|
||||
|
||||
The `spring-web` module contains `Encoder` and `Decoder` implementations such as Jackson
|
||||
CBOR/JSON, and Protobuf that RSocket applications will likely need. It also contains the
|
||||
@@ -863,6 +865,69 @@ interaction type(s):
|
||||
|
||||
|
||||
|
||||
[[rsocket-annot-rsocketexchange]]
|
||||
=== @RSocketExchange
|
||||
|
||||
As an alternative to `@MessageMapping`, you can also handle requests with
|
||||
`@RSocketExchange` methods. Such methods are declared on an
|
||||
xref:rsocket-interface[RSocket Interface] and can be used as a requester via
|
||||
`RSocketServiceProxyFactory` or implemented by a responder.
|
||||
|
||||
For example, to handle requests as a responder:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
public interface RadarsService {
|
||||
|
||||
@RSocketExchange("locate.radars.within")
|
||||
Flux<AirportLocation> radars(MapRequest request);
|
||||
}
|
||||
|
||||
@Controller
|
||||
public class RadarsController implements RadarsService {
|
||||
|
||||
public Flux<AirportLocation> radars(MapRequest request) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
interface RadarsService {
|
||||
|
||||
@RSocketExchange("locate.radars.within")
|
||||
fun radars(request: MapRequest): Flow<AirportLocation>
|
||||
}
|
||||
|
||||
@Controller
|
||||
class RadarsController : RadarsService {
|
||||
|
||||
override fun radars(request: MapRequest): Flow<AirportLocation> {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
There some differences between `@RSocketExhange` and `@MessageMapping` since the
|
||||
former needs to remain suitable for requester and responder use. For example, while
|
||||
`@MessageMapping` can be declared to handle any number of routes and each route can
|
||||
be a pattern, `@RSocketExchange` must be declared with a single, concrete route. There are
|
||||
also small differences in the supported method parameters related to metadata, see
|
||||
xref:rsocket-annot-messagemapping[@MessageMapping] and
|
||||
xref:rsocket-interface[RSocket Interface] for a list of supported parameters.
|
||||
|
||||
`@RSocketExchange` can be used at the type level to specify a common prefix for all routes
|
||||
for a given RSocket service interface.
|
||||
|
||||
|
||||
[[rsocket-annot-connectmapping]]
|
||||
=== @ConnectMapping
|
||||
|
||||
@@ -997,12 +1062,13 @@ Kotlin::
|
||||
[[rsocket-interface]]
|
||||
== RSocket Interface
|
||||
|
||||
The Spring Framework lets you define an RSocket service as a Java interface with annotated
|
||||
methods for RSocket exchanges. You can then generate a proxy that implements this interface
|
||||
and performs the exchanges. This helps to simplify RSocket remote access by wrapping the
|
||||
use of the underlying xref:rsocket.adoc#rsocket-requester[RSocketRequester].
|
||||
The Spring Framework lets you define an RSocket service as a Java interface with
|
||||
`@RSocketExchange` methods. You can pass such an interface to `RSocketServiceProxyFactory`
|
||||
to create a proxy which performs requests through an
|
||||
xref:rsocket.adoc#rsocket-requester[RSocketRequester]. You can also implement the
|
||||
interface as a responder that handles requests.
|
||||
|
||||
One, declare an interface with `@RSocketExchange` methods:
|
||||
Start by creating the interface with `@RSocketExchange` methods:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@@ -1016,7 +1082,7 @@ One, declare an interface with `@RSocketExchange` methods:
|
||||
}
|
||||
----
|
||||
|
||||
Two, create a proxy that will perform the declared RSocket exchanges:
|
||||
Now you can create a proxy that performs requests when methods are called:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@@ -1026,6 +1092,10 @@ Two, create a proxy that will perform the declared RSocket exchanges:
|
||||
RadarService service = factory.createClient(RadarService.class);
|
||||
----
|
||||
|
||||
You can also implement the interface to handle requests as a responder.
|
||||
See xref:rsocket.adoc#rsocket-annot-rsocketexchange[Annotated Responders].
|
||||
|
||||
|
||||
|
||||
[[rsocket-interface-method-parameters]]
|
||||
=== Method Parameters
|
||||
@@ -1066,3 +1136,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.
|
||||
|
||||
|
||||
+4
-2
@@ -170,8 +170,9 @@ of a test class constructor are autowired from components in the test's
|
||||
|
||||
If `@TestConstructor` is not present or meta-present on a test class, the default _test
|
||||
constructor autowire mode_ will be used. See the tip below for details on how to change
|
||||
the default mode. Note, however, that a local declaration of `@Autowired` on a
|
||||
constructor takes precedence over both `@TestConstructor` and the default mode.
|
||||
the default mode. Note, however, that a local declaration of `@Autowired`,
|
||||
`@jakarta.inject.Inject`, or `@javax.inject.Inject` on a constructor takes precedence
|
||||
over both `@TestConstructor` and the default mode.
|
||||
|
||||
.Changing the default test constructor autowire mode
|
||||
[TIP]
|
||||
@@ -222,6 +223,7 @@ following annotations.
|
||||
* xref:testing/annotations/integration-spring/annotation-contextconfiguration.adoc[`@ContextConfiguration`]
|
||||
* xref:testing/annotations/integration-spring/annotation-webappconfiguration.adoc[`@WebAppConfiguration`]
|
||||
* xref:testing/annotations/integration-spring/annotation-contexthierarchy.adoc[`@ContextHierarchy`]
|
||||
* xref:testing/annotations/integration-spring/annotation-contextcustomizerfactories.adoc[`@ContextCustomizerFactories`]
|
||||
* xref:testing/annotations/integration-spring/annotation-activeprofiles.adoc[`@ActiveProfiles`]
|
||||
* xref:testing/annotations/integration-spring/annotation-testpropertysource.adoc[`@TestPropertySource`]
|
||||
* xref:testing/annotations/integration-spring/annotation-dynamicpropertysource.adoc[`@DynamicPropertySource`]
|
||||
|
||||
@@ -11,6 +11,7 @@ xref:testing/testcontext-framework.adoc[TestContext framework].
|
||||
* `@BootstrapWith`
|
||||
* `@ContextConfiguration`
|
||||
* `@ContextHierarchy`
|
||||
* `@ContextCustomizerFactories`
|
||||
* `@ActiveProfiles`
|
||||
* `@TestPropertySource`
|
||||
* `@DirtiesContext`
|
||||
|
||||
@@ -12,6 +12,7 @@ Spring's testing annotations include the following:
|
||||
* xref:testing/annotations/integration-spring/annotation-contextconfiguration.adoc[`@ContextConfiguration`]
|
||||
* xref:testing/annotations/integration-spring/annotation-webappconfiguration.adoc[`@WebAppConfiguration`]
|
||||
* xref:testing/annotations/integration-spring/annotation-contexthierarchy.adoc[`@ContextHierarchy`]
|
||||
* xref:testing/annotations/integration-spring/annotation-contextcustomizerfactories.adoc[`@ContextCustomizerFactories`]
|
||||
* xref:testing/annotations/integration-spring/annotation-activeprofiles.adoc[`@ActiveProfiles`]
|
||||
* xref:testing/annotations/integration-spring/annotation-testpropertysource.adoc[`@TestPropertySource`]
|
||||
* xref:testing/annotations/integration-spring/annotation-dynamicpropertysource.adoc[`@DynamicPropertySource`]
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
[[spring-testing-annotation-contextcustomizerfactories]]
|
||||
= `@ContextCustomizerFactories`
|
||||
|
||||
`@ContextCustomizerFactories` is used to register `ContextCustomizerFactory`
|
||||
implementations for a particular test class, its subclasses, and its nested classes. If
|
||||
you wish to register a factory globally, you should register it via the automatic
|
||||
discovery mechanism described in
|
||||
xref:testing/testcontext-framework/ctx-management/context-customizers.adoc[`ContextCustomizerFactory` Configuration].
|
||||
|
||||
The following example shows how to register two `ContextCustomizerFactory` implementations:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@ContextConfiguration
|
||||
@ContextCustomizerFactories({CustomContextCustomizerFactory.class, AnotherContextCustomizerFactory.class}) // <1>
|
||||
class CustomContextCustomizerFactoryTests {
|
||||
// class body...
|
||||
}
|
||||
----
|
||||
<1> Register two `ContextCustomizerFactory` implementations.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@ContextConfiguration
|
||||
@ContextCustomizerFactories([CustomContextCustomizerFactory::class, AnotherContextCustomizerFactory::class]) // <1>
|
||||
class CustomContextCustomizerFactoryTests {
|
||||
// class body...
|
||||
}
|
||||
----
|
||||
<1> Register two `ContextCustomizerFactory` implementations.
|
||||
======
|
||||
|
||||
|
||||
By default, `@ContextCustomizerFactories` provides support for inheriting factories from
|
||||
superclasses or enclosing classes. See
|
||||
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-nested-test-configuration[`@Nested` test class configuration] and the
|
||||
{api-spring-framework}/test/context/ContextCustomizerFactories.html[`@ContextCustomizerFactories`
|
||||
javadoc] for an example and further details.
|
||||
|
||||
@@ -19,7 +19,22 @@ following features.
|
||||
use an AOT-optimized `ApplicationContext` that participates transparently with the
|
||||
xref:testing/testcontext-framework/ctx-management/caching.adoc[context cache].
|
||||
|
||||
[WARNING]
|
||||
[TIP]
|
||||
====
|
||||
By default, if an error is encountered during build-time AOT processing, an exception
|
||||
will be thrown, and the overall process will fail immediately.
|
||||
|
||||
If you would prefer that build-time AOT processing continue after errors are encountered,
|
||||
you can disable the `failOnError` mode which results in errors being logged at `WARN`
|
||||
level or with greater detail at `DEBUG` level.
|
||||
|
||||
The `failOnError` mode can be disabled from the command line or a build script by setting
|
||||
a JVM system property named `spring.test.aot.processing.failOnError` to `false`. As an
|
||||
alternative, you can set the same property via the
|
||||
xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism.
|
||||
====
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
The `@ContextHierarchy` annotation is currently not supported in AOT mode.
|
||||
====
|
||||
|
||||
@@ -112,6 +112,7 @@ advanced use cases.
|
||||
* xref:testing/testcontext-framework/ctx-management/groovy.adoc[Context Configuration with Groovy Scripts]
|
||||
* xref:testing/testcontext-framework/ctx-management/javaconfig.adoc[Context Configuration with Component Classes]
|
||||
* xref:testing/testcontext-framework/ctx-management/mixed-config.adoc[Mixing XML, Groovy Scripts, and Component Classes]
|
||||
* xref:testing/testcontext-framework/ctx-management/context-customizers.adoc[Context Configuration with Context Customizers]
|
||||
* xref:testing/testcontext-framework/ctx-management/initializers.adoc[Context Configuration with Context Initializers]
|
||||
* xref:testing/testcontext-framework/ctx-management/inheritance.adoc[Context Configuration Inheritance]
|
||||
* xref:testing/testcontext-framework/ctx-management/env-profiles.adoc[Context Configuration with Environment Profiles]
|
||||
@@ -119,5 +120,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]
|
||||
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ framework uses the following configuration parameters to build the context cache
|
||||
* `contextLoader` (from `@ContextConfiguration`)
|
||||
* `parent` (from `@ContextHierarchy`)
|
||||
* `activeProfiles` (from `@ActiveProfiles`)
|
||||
* `propertySourceLocations` (from `@TestPropertySource`)
|
||||
* `propertySourceDescriptors` (from `@TestPropertySource`)
|
||||
* `propertySourceProperties` (from `@TestPropertySource`)
|
||||
* `resourceBasePath` (from `@WebAppConfiguration`)
|
||||
|
||||
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
[[testcontext-context-customizers]]
|
||||
= Configuration Configuration with Context Customizers
|
||||
|
||||
A `ContextCustomizer` is responsible for customizing the supplied
|
||||
`ConfigurableApplicationContext` after bean definitions have been loaded into the context
|
||||
but before the context has been refreshed.
|
||||
|
||||
A `ContextCustomizerFactory` is responsible for creating a `ContextCustomizer`, based on
|
||||
some custom logic which determines if the `ContextCustomizer` is necessary for a given
|
||||
test class -- for example, based on the presence of a certain annotation. Factories are
|
||||
invoked after `ContextLoaders` have processed context configuration attributes for a test
|
||||
class but before the `MergedContextConfiguration` is created.
|
||||
|
||||
For example, Spring Framework provides the following `ContextCustomizerFactory`
|
||||
implementation which is registered by default:
|
||||
|
||||
`MockServerContainerContextCustomizerFactory`:: Creates a
|
||||
`MockServerContainerContextCustomizer` if WebSocket support is present in the classpath
|
||||
and the test class or one of its enclosing classes is annotated or meta-annotated with
|
||||
`@WebAppConfiguration`. `MockServerContainerContextCustomizer` instantiates a new
|
||||
`MockServerContainer` and stores it in the `ServletContext` under the attribute named
|
||||
`jakarta.websocket.server.ServerContainer`.
|
||||
|
||||
|
||||
[[testcontext-context-customizers-registration]]
|
||||
== Registering `ContextCustomizerFactory` Implementations
|
||||
|
||||
You can register `ContextCustomizerFactory` implementations explicitly for a test class, its
|
||||
subclasses, and its nested classes by using the `@ContextCustomizerFactories` annotation. See
|
||||
xref:testing/annotations/integration-spring/annotation-contextcustomizerfactories.adoc[annotation support]
|
||||
and the javadoc for
|
||||
{api-spring-framework}/test/context/ContextCustomizerFactories.html[`@ContextCustomizerFactories`]
|
||||
for details and examples.
|
||||
|
||||
|
||||
[[testcontext-context-customizers-automatic-discovery]]
|
||||
== Automatic Discovery of Default `ContextCustomizerFactory` Implementations
|
||||
|
||||
Registering `ContextCustomizerFactory` implementations by using `@ContextCustomizerFactories` is
|
||||
suitable for custom factories that are used in limited testing scenarios. However, it can
|
||||
become cumbersome if a custom factory needs to be used across an entire test suite. This
|
||||
issue is addressed through support for automatic discovery of default
|
||||
`ContextCustomizerFactory` implementations through the `SpringFactoriesLoader` mechanism.
|
||||
|
||||
Specifically, the modules that make up the testing support in Spring Framework and Spring
|
||||
Boot declare all core default `ContextCustomizerFactory` implementations under the
|
||||
`org.springframework.test.context.ContextCustomizerFactory` key in their
|
||||
`META-INF/spring.factories` properties files. Third-party frameworks and developers can
|
||||
contribute their own `ContextCustomizerFactory` implementations to the list of default
|
||||
factories in the same manner through their own `META-INF/spring.factories` properties
|
||||
files.
|
||||
|
||||
|
||||
[[testcontext-context-customizers-merging]]
|
||||
== Merging `ContextCustomizerFactory` Implementations
|
||||
|
||||
If a custom `ContextCustomizerFactory` is registered via `@ContextCustomizerFactories`, it
|
||||
will be _merged_ with the default factories that have been registered using the aforementioned
|
||||
xref:testing/testcontext-framework/ctx-management/context-customizers.adoc#testcontext-context-customizers-automatic-discovery[automatic discovery mechanism].
|
||||
|
||||
The merging algorithm ensures that duplicates are removed from the list and that locally
|
||||
declared factories are appended to the list of default factories when merged.
|
||||
|
||||
[TIP]
|
||||
====
|
||||
To replace the default factories for a test class, its subclasses, and its nested
|
||||
classes, you can set the `mergeMode` attribute of `@ContextCustomizerFactories` to
|
||||
`MergeMode.REPLACE_DEFAULTS`.
|
||||
====
|
||||
+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`.
|
||||
+69
-12
@@ -16,7 +16,7 @@ SPI, but `@TestPropertySource` is not supported with implementations of the olde
|
||||
`ContextLoader` SPI.
|
||||
|
||||
Implementations of `SmartContextLoader` gain access to merged test property source values
|
||||
through the `getPropertySourceLocations()` and `getPropertySourceProperties()` methods in
|
||||
through the `getPropertySourceDescriptors()` and `getPropertySourceProperties()` methods in
|
||||
`MergedContextConfiguration`.
|
||||
====
|
||||
|
||||
@@ -26,17 +26,23 @@ through the `getPropertySourceLocations()` and `getPropertySourceProperties()` m
|
||||
You can configure test properties files by using the `locations` or `value` attribute of
|
||||
`@TestPropertySource`.
|
||||
|
||||
Both traditional and XML-based properties file formats are supported -- for example,
|
||||
`"classpath:/com/example/test.properties"` or `"file:///path/to/file.xml"`.
|
||||
By default, both traditional and XML-based `java.util.Properties` file formats are
|
||||
supported -- for example, `"classpath:/com/example/test.properties"` or
|
||||
`"file:///path/to/file.xml"`. As of Spring Framework 6.1, you can configure a custom
|
||||
`PropertySourceFactory` via the `factory` attribute in `@TestPropertySource` in order to
|
||||
support a different file format such as JSON, YAML, etc.
|
||||
|
||||
Each path is interpreted as a Spring `Resource`. A plain path (for example,
|
||||
`"test.properties"`) is treated as a classpath resource that is relative to the package
|
||||
in which the test class is defined. A path starting with a slash is treated as an
|
||||
absolute classpath resource (for example: `"/org/example/test.xml"`). A path that
|
||||
references a URL (for example, a path prefixed with `classpath:`, `file:`, or `http:`) is
|
||||
loaded by using the specified resource protocol. Resource location wildcards (such as
|
||||
`{asterisk}{asterisk}/{asterisk}.properties`) are not permitted: Each location must
|
||||
evaluate to exactly one `.properties` or `.xml` resource.
|
||||
loaded by using the specified resource protocol.
|
||||
|
||||
Property placeholders in paths (such as `${...}`) will be resolved against the `Environment`.
|
||||
|
||||
As of Spring Framework 6.1, resource location patterns are also supported — for
|
||||
example, `"classpath*:/config/*.properties"`.
|
||||
|
||||
The following example uses a test properties file:
|
||||
|
||||
@@ -80,6 +86,20 @@ a Java properties file:
|
||||
* `key:value`
|
||||
* `key value`
|
||||
|
||||
[TIP]
|
||||
====
|
||||
Although properties can be defined using any of the above syntax variants and any number
|
||||
of spaces between the key and the value, it is recommended that you use one syntax
|
||||
variant and consistent spacing within your test suite — for example, consider always
|
||||
using `key = value` instead of `key= value`, `key=value`, etc. Similarly, if you define
|
||||
inlined properties using text blocks you should consistently use text blocks for inlined
|
||||
properties throughout your test suite.
|
||||
|
||||
The reason is that the exact strings you provide will be used to determine the key for
|
||||
the context cache. Consequently, to benefit from the context cache you must ensure that
|
||||
you define inlined properties consistently.
|
||||
====
|
||||
|
||||
The following example sets two inlined properties:
|
||||
|
||||
[tabs]
|
||||
@@ -89,24 +109,61 @@ Java::
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@ContextConfiguration
|
||||
@TestPropertySource(properties = {"timezone = GMT", "port: 4242"}) // <1>
|
||||
@TestPropertySource(properties = {"timezone = GMT", "port = 4242"}) // <1>
|
||||
class MyIntegrationTests {
|
||||
// class body...
|
||||
}
|
||||
----
|
||||
<1> Setting two properties by using two variations of the key-value syntax.
|
||||
<1> Setting two properties via an array of strings.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@ContextConfiguration
|
||||
@TestPropertySource(properties = ["timezone = GMT", "port: 4242"]) // <1>
|
||||
@TestPropertySource(properties = ["timezone = GMT", "port = 4242"]) // <1>
|
||||
class MyIntegrationTests {
|
||||
// class body...
|
||||
}
|
||||
----
|
||||
<1> Setting two properties by using two variations of the key-value syntax.
|
||||
<1> Setting two properties via an array of strings.
|
||||
======
|
||||
|
||||
As of Spring Framework 6.1, you can use _text blocks_ to define multiple inlined
|
||||
properties in a single `String`. The following example sets two inlined properties using
|
||||
a text block:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@ContextConfiguration
|
||||
@TestPropertySource(properties = """
|
||||
timezone = GMT
|
||||
port = 4242
|
||||
""") // <1>
|
||||
class MyIntegrationTests {
|
||||
// class body...
|
||||
}
|
||||
----
|
||||
<1> Setting two properties via a text block.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@ContextConfiguration
|
||||
@TestPropertySource(properties = ["""
|
||||
timezone = GMT
|
||||
port = 4242
|
||||
"""]) // <1>
|
||||
class MyIntegrationTests {
|
||||
// class body...
|
||||
}
|
||||
----
|
||||
<1> Setting two properties via a text block.
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
@@ -166,7 +223,7 @@ Java::
|
||||
@ContextConfiguration
|
||||
@TestPropertySource(
|
||||
locations = "/test.properties",
|
||||
properties = {"timezone = GMT", "port: 4242"}
|
||||
properties = {"timezone = GMT", "port = 4242"}
|
||||
)
|
||||
class MyIntegrationTests {
|
||||
// class body...
|
||||
@@ -179,7 +236,7 @@ Kotlin::
|
||||
----
|
||||
@ContextConfiguration
|
||||
@TestPropertySource("/test.properties",
|
||||
properties = ["timezone = GMT", "port: 4242"]
|
||||
properties = ["timezone = GMT", "port = 4242"]
|
||||
)
|
||||
class MyIntegrationTests {
|
||||
// class body...
|
||||
|
||||
+9
-7
@@ -177,7 +177,7 @@ following features above and beyond the feature set that Spring supports for JUn
|
||||
TestNG:
|
||||
|
||||
* Dependency injection for test constructors, test methods, and test lifecycle callback
|
||||
methods. See xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-di[Dependency Injection with `SpringExtension`] for further details.
|
||||
methods. See xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-di[Dependency Injection with the `SpringExtension`] for further details.
|
||||
* Powerful support for link:https://junit.org/junit5/docs/current/user-guide/#extensions-conditions[conditional
|
||||
test execution] based on SpEL expressions, environment variables, system properties,
|
||||
and so on. See the documentation for `@EnabledIf` and `@DisabledIf` in
|
||||
@@ -310,17 +310,19 @@ See the documentation for `@SpringJUnitConfig` and `@SpringJUnitWebConfig` in
|
||||
xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations] for further details.
|
||||
|
||||
[[testcontext-junit-jupiter-di]]
|
||||
=== Dependency Injection with `SpringExtension`
|
||||
=== Dependency Injection with the `SpringExtension`
|
||||
|
||||
`SpringExtension` implements the
|
||||
The `SpringExtension` implements the
|
||||
link:https://junit.org/junit5/docs/current/user-guide/#extensions-parameter-resolution[`ParameterResolver`]
|
||||
extension API from JUnit Jupiter, which lets Spring provide dependency injection for test
|
||||
constructors, test methods, and test lifecycle callback methods.
|
||||
|
||||
Specifically, `SpringExtension` can inject dependencies from the test's
|
||||
Specifically, the `SpringExtension` can inject dependencies from the test's
|
||||
`ApplicationContext` into test constructors and methods that are annotated with
|
||||
`@BeforeAll`, `@AfterAll`, `@BeforeEach`, `@AfterEach`, `@Test`, `@RepeatedTest`,
|
||||
`@ParameterizedTest`, and others.
|
||||
Spring's `@BeforeTransaction` and `@AfterTransaction` or JUnit's `@BeforeAll`,
|
||||
`@AfterAll`, `@BeforeEach`, `@AfterEach`, `@Test`, `@RepeatedTest`, `@ParameterizedTest`,
|
||||
and others.
|
||||
|
||||
|
||||
[[testcontext-junit-jupiter-di-constructor]]
|
||||
==== Constructor Injection
|
||||
@@ -534,7 +536,7 @@ The _Spring TestContext Framework_ has supported the use of test-related annotat
|
||||
Framework 5.3 class-level test configuration annotations were not _inherited_ from
|
||||
enclosing classes like they are from superclasses.
|
||||
|
||||
Spring Framework 5.3 introduces first-class support for inheriting test class
|
||||
Spring Framework 5.3 introduced first-class support for inheriting test class
|
||||
configuration from enclosing classes, and such configuration will be inherited by
|
||||
default. To change from the default `INHERIT` mode to `OVERRIDE` mode, you may annotate
|
||||
an individual `@Nested` test class with
|
||||
|
||||
@@ -295,14 +295,56 @@ behavior after your test runs (if the test was configured to commit the transact
|
||||
`TransactionalTestExecutionListener` supports the `@BeforeTransaction` and
|
||||
`@AfterTransaction` annotations for exactly such scenarios. You can annotate any `void`
|
||||
method in a test class or any `void` default method in a test interface with one of these
|
||||
annotations, and the `TransactionalTestExecutionListener` ensures that your before
|
||||
transaction method or after transaction method runs at the appropriate time.
|
||||
annotations, and the `TransactionalTestExecutionListener` ensures that your
|
||||
before-transaction method or after-transaction method runs at the appropriate time.
|
||||
|
||||
TIP: Any before methods (such as methods annotated with JUnit Jupiter's `@BeforeEach`)
|
||||
and any after methods (such as methods annotated with JUnit Jupiter's `@AfterEach`) are
|
||||
run within a transaction. In addition, methods annotated with `@BeforeTransaction` or
|
||||
`@AfterTransaction` are not run for test methods that are not configured to run within a
|
||||
transaction.
|
||||
[NOTE]
|
||||
====
|
||||
Generally speaking, `@BeforeTransaction` and `@AfterTransaction` methods must not accept
|
||||
any arguments.
|
||||
|
||||
However, as of Spring Framework 6.1, for tests using the
|
||||
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[`SpringExtension`]
|
||||
with JUnit Jupiter, `@BeforeTransaction` and `@AfterTransaction` methods may optionally
|
||||
accept arguments which will be resolved by any registered JUnit `ParameterResolver`
|
||||
extension such as the `SpringExtension`. This means that JUnit-specific arguments like
|
||||
`TestInfo` or beans from the test's `ApplicationContext` may be provided to
|
||||
`@BeforeTransaction` and `@AfterTransaction` methods, as demonstrated in the following
|
||||
example.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@BeforeTransaction
|
||||
void verifyInitialDatabaseState(@Autowired DataSource dataSource) {
|
||||
// Use the DataSource to verify the initial state before a transaction is started
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@BeforeTransaction
|
||||
fun verifyInitialDatabaseState(@Autowired dataSource: DataSource) {
|
||||
// Use the DataSource to verify the initial state before a transaction is started
|
||||
}
|
||||
----
|
||||
======
|
||||
====
|
||||
|
||||
[TIP]
|
||||
====
|
||||
Any before methods (such as methods annotated with JUnit Jupiter's `@BeforeEach`) and any
|
||||
after methods (such as methods annotated with JUnit Jupiter's `@AfterEach`) are run
|
||||
within the test-managed transaction for a transactional test method.
|
||||
|
||||
Similarly, methods annotated with `@BeforeTransaction` or `@AfterTransaction` are only
|
||||
run for transactional test methods.
|
||||
====
|
||||
|
||||
[[testcontext-tx-mgr-config]]
|
||||
== Configuring a Transaction Manager
|
||||
|
||||
@@ -148,5 +148,75 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
The example below demonstrates how to use the `ExchangeFilterFunction` interface to create
|
||||
a custom filter class that helps with computing a `Content-Length` header for `PUT` and `POST`
|
||||
`multipart/form-data` requests using buffering.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
public class MultipartExchangeFilterFunction implements ExchangeFilterFunction {
|
||||
|
||||
@Override
|
||||
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
|
||||
if (MediaType.MULTIPART_FORM_DATA.includes(request.headers().getContentType())
|
||||
&& (request.method() == HttpMethod.PUT || request.method() == HttpMethod.POST)) {
|
||||
return next.exchange(ClientRequest.from(request).body((outputMessage, context) ->
|
||||
request.body().insert(new BufferingDecorator(outputMessage), context)).build()
|
||||
);
|
||||
} else {
|
||||
return next.exchange(request);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class BufferingDecorator extends ClientHttpRequestDecorator {
|
||||
|
||||
private BufferingDecorator(ClientHttpRequest delegate) {
|
||||
super(delegate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
|
||||
return DataBufferUtils.join(body).flatMap(buffer -> {
|
||||
getHeaders().setContentLength(buffer.readableByteCount());
|
||||
return super.writeWith(Mono.just(buffer));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
class MultipartExchangeFilterFunction : ExchangeFilterFunction {
|
||||
|
||||
override fun filter(request: ClientRequest, next: ExchangeFunction): Mono<ClientResponse> {
|
||||
return if (MediaType.MULTIPART_FORM_DATA.includes(request.headers().getContentType())
|
||||
&& (request.method() == HttpMethod.PUT || request.method() == HttpMethod.POST)) {
|
||||
next.exchange(ClientRequest.from(request)
|
||||
.body { message, context -> request.body().insert(BufferingDecorator(message), context) }
|
||||
.build())
|
||||
}
|
||||
else {
|
||||
next.exchange(request)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class BufferingDecorator(delegate: ClientHttpRequest) : ClientHttpRequestDecorator(delegate) {
|
||||
override fun writeWith(body: Publisher<out DataBuffer>): Mono<Void> {
|
||||
return DataBufferUtils.join(body)
|
||||
.flatMap {
|
||||
headers.contentLength = it.readableByteCount().toLong()
|
||||
super.writeWith(Mono.just(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
@@ -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 `AsyncTaskExecutor` 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) {
|
||||
AsyncTaskExecutor 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
|
||||
|
||||
|
||||
@@ -3,23 +3,21 @@
|
||||
|
||||
[.small]#xref:web/webmvc/mvc-controller/ann-initbinder.adoc[See equivalent in the Servlet stack]#
|
||||
|
||||
`@Controller` or `@ControllerAdvice` classes can have `@InitBinder` methods, to
|
||||
initialize instances of `WebDataBinder`. Those, in turn, are used to:
|
||||
`@Controller` or `@ControllerAdvice` classes can have `@InitBinder` methods to
|
||||
initialize `WebDataBinder` instances that in turn can:
|
||||
|
||||
* Bind request parameters (that is, form data or query) to a model object.
|
||||
* Convert `String`-based request values (such as request parameters, path variables,
|
||||
headers, cookies, and others) to the target type of controller method arguments.
|
||||
* Format model object values as `String` values when rendering HTML forms.
|
||||
* Bind request parameters to a model object.
|
||||
* Convert request values from string to object property types.
|
||||
* Format model object properties as strings when rendering HTML forms.
|
||||
|
||||
`@InitBinder` methods can register controller-specific `java.beans.PropertyEditor` or
|
||||
Spring `Converter` and `Formatter` components. In addition, you can use the
|
||||
xref:web/webflux/config.adoc#webflux-config-conversion[WebFlux Java configuration] to register `Converter` and
|
||||
`Formatter` types in a globally shared `FormattingConversionService`.
|
||||
In an `@Controller`, `DataBinder` customizations apply locally within the controller,
|
||||
or even to a specific model attribute referenced by name through the annotation.
|
||||
In an `@ControllerAdvice` customizations can apply to all or a subset of controllers.
|
||||
|
||||
`@InitBinder` methods support many of the same arguments that `@RequestMapping` methods
|
||||
do, except for `@ModelAttribute` (command object) arguments. Typically, they are declared
|
||||
with a `WebDataBinder` argument, for registrations, and a `void` return value.
|
||||
The following example uses the `@InitBinder` annotation:
|
||||
You can register `PropertyEditor`, `Converter`, and `Formatter` components in the
|
||||
`DataBinder` for type conversion. Alternatively, you can use the
|
||||
xref:web/webflux/config.adoc#webflux-config-conversion[WebFlux config] to register
|
||||
`Converter` and `Formatter` components in a globally shared `FormattingConversionService`.
|
||||
|
||||
--
|
||||
[tabs]
|
||||
@@ -112,4 +110,5 @@ Kotlin::
|
||||
== Model Design
|
||||
[.small]#xref:web/webmvc/mvc-controller/ann-initbinder.adoc#mvc-ann-initbinder-model-design[See equivalent in the Servlet stack]#
|
||||
|
||||
include::partial$web/web-data-binding-model-design.adoc[]
|
||||
|
||||
|
||||
+105
-71
@@ -3,11 +3,8 @@
|
||||
|
||||
[.small]#xref:web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc[See equivalent in the Servlet stack]#
|
||||
|
||||
You can use the `@ModelAttribute` annotation on a method argument to access an attribute from the
|
||||
model or have it instantiated if not present. The model attribute is also overlaid with
|
||||
the values of query parameters and form fields whose names match to field names. This is
|
||||
referred to as data binding, and it saves you from having to deal with parsing and
|
||||
converting individual query parameters and form fields. The following example binds an instance of `Pet`:
|
||||
The `@ModelAttribute` method parameter annotation binds request parameters onto a model
|
||||
object. For example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -18,7 +15,7 @@ Java::
|
||||
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
|
||||
public String processSubmit(@ModelAttribute Pet pet) { } // <1>
|
||||
----
|
||||
<1> Bind an instance of `Pet`.
|
||||
<1> Bind to an instance of `Pet`.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
@@ -27,28 +24,66 @@ Kotlin::
|
||||
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
|
||||
fun processSubmit(@ModelAttribute pet: Pet): String { } // <1>
|
||||
----
|
||||
<1> Bind an instance of `Pet`.
|
||||
<1> Bind to an instance of `Pet`.
|
||||
======
|
||||
|
||||
The `Pet` instance in the preceding example is resolved as follows:
|
||||
The `Pet` instance may be:
|
||||
|
||||
* From the model if already added through xref:web/webflux/controller/ann-modelattrib-methods.adoc[`Model`].
|
||||
* From the HTTP session through xref:web/webflux/controller/ann-methods/sessionattributes.adoc[`@SessionAttributes`].
|
||||
* From the invocation of a default constructor.
|
||||
* From the invocation of a "`primary constructor`" with arguments that match query
|
||||
parameters or form fields. Argument names are determined through JavaBeans
|
||||
`@ConstructorProperties` or through runtime-retained parameter names in the bytecode.
|
||||
* Accessed from the model where it could have been added by a
|
||||
xref:web/webflux/controller/ann-modelattrib-methods.adoc[`Model`].
|
||||
* Accessed from the HTTP session if the model attribute was listed in
|
||||
the class-level xref:web/webflux/controller/ann-methods/sessionattributes.adoc[`@SessionAttributes`].
|
||||
* Instantiated through a default constructor.
|
||||
* Instantiated through a "`primary constructor`" with arguments that match to Servlet
|
||||
request parameters. Argument names are determined through runtime-retained parameter
|
||||
names in the bytecode.
|
||||
|
||||
After the model attribute instance is obtained, data binding is applied. The
|
||||
`WebExchangeDataBinder` class matches names of query parameters and form fields to field
|
||||
names on the target `Object`. Matching fields are populated after type conversion is applied
|
||||
where necessary. For more on data binding (and validation), see
|
||||
xref:web/webmvc/mvc-config/validation.adoc[Validation]. For more on customizing data binding, see
|
||||
xref:web/webflux/controller/ann-initbinder.adoc[`DataBinder`].
|
||||
By default, both constructor and property
|
||||
xref:core/validation/beans-beans.adoc#beans-binding[data binding] are applied. However,
|
||||
model object design requires careful consideration, and for security reasons it is
|
||||
recommended either to use an object tailored specifically for web binding, or to apply
|
||||
constructor binding only. If property binding must still be used, then _allowedFields_
|
||||
patterns should be set to limit which properties can be set. For further details on this
|
||||
and example configuration, see
|
||||
xref:web/webflux/controller/ann-initbinder.adoc#webflux-ann-initbinder-model-design[model design].
|
||||
|
||||
Data binding can result in errors. By default, a `WebExchangeBindException` is raised, but,
|
||||
to check for such errors in the controller method, you can add a `BindingResult` argument
|
||||
immediately next to the `@ModelAttribute`, as the following example shows:
|
||||
When using constructor binding, you can customize request parameter names through an
|
||||
`@BindParam` annotation. For example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
class Account {
|
||||
|
||||
private final String firstName;
|
||||
|
||||
public Account(@BindParam("first-name") String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
}
|
||||
----
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
class Account(@BindParam("first-name") val firstName: String)
|
||||
----
|
||||
======
|
||||
|
||||
NOTE: The `@BindParam` may also be placed on the fields that correspond to constructor
|
||||
parameters. While `@BindParam` is supported out of the box, you can also use a
|
||||
different annotation by setting a `DataBinder.NameResolver` on `DataBinder`
|
||||
|
||||
WebFlux, unlike Spring MVC, supports reactive types in the model, e.g. `Mono<Account>`.
|
||||
You can declare a `@ModelAttribute` argument with or without a reactive type wrapper, and
|
||||
it will be resolved accordingly to the actual value.
|
||||
|
||||
If data binding results in errors, by default a `WebExchangeBindException` is raised,
|
||||
but you can also add a `BindingResult` argument immediately next to the `@ModelAttribute`
|
||||
in order to handle such errors in the controller method. For example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -81,49 +116,9 @@ Kotlin::
|
||||
<1> Adding a `BindingResult`.
|
||||
======
|
||||
|
||||
You can automatically apply validation after data binding by adding the
|
||||
`jakarta.validation.Valid` annotation or Spring's `@Validated` annotation (see also
|
||||
xref:core/validation/beanvalidation.adoc[Bean Validation] and
|
||||
xref:web/webmvc/mvc-config/validation.adoc[Spring validation]). The following example uses the `@Valid` annotation:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
|
||||
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { // <1>
|
||||
if (result.hasErrors()) {
|
||||
return "petForm";
|
||||
}
|
||||
// ...
|
||||
}
|
||||
----
|
||||
<1> Using `@Valid` on a model attribute argument.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
|
||||
fun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { // <1>
|
||||
if (result.hasErrors()) {
|
||||
return "petForm"
|
||||
}
|
||||
// ...
|
||||
}
|
||||
----
|
||||
<1> Using `@Valid` on a model attribute argument.
|
||||
======
|
||||
|
||||
Spring WebFlux, unlike Spring MVC, supports reactive types in the model -- for example,
|
||||
`Mono<Account>` or `io.reactivex.Single<Account>`. You can declare a `@ModelAttribute` argument
|
||||
with or without a reactive type wrapper, and it will be resolved accordingly,
|
||||
to the actual value if necessary. However, note that, to use a `BindingResult`
|
||||
argument, you must declare the `@ModelAttribute` argument before it without a reactive
|
||||
type wrapper, as shown earlier. Alternatively, you can handle any errors through the
|
||||
reactive type, as the following example shows:
|
||||
To use a `BindingResult` argument, you must declare the `@ModelAttribute` argument before
|
||||
it without a reactive type wrapper. If you want to use the reactive, you can handle errors
|
||||
directly through it. For example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -160,10 +155,49 @@ 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`.
|
||||
You can automatically apply validation after data binding by adding the
|
||||
`jakarta.validation.Valid` annotation or Spring's `@Validated` annotation (see
|
||||
xref:core/validation/beanvalidation.adoc[Bean Validation] and
|
||||
xref:web/webmvc/mvc-config/validation.adoc[Spring validation]). For example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
|
||||
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { // <1>
|
||||
if (result.hasErrors()) {
|
||||
return "petForm";
|
||||
}
|
||||
// ...
|
||||
}
|
||||
----
|
||||
<1> Using `@Valid` on a model attribute argument.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
|
||||
fun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { // <1>
|
||||
if (result.hasErrors()) {
|
||||
return "petForm"
|
||||
}
|
||||
// ...
|
||||
}
|
||||
----
|
||||
<1> Using `@Valid` on a model attribute argument.
|
||||
======
|
||||
|
||||
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].
|
||||
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
[[webflux-ann-requestmapping]]
|
||||
= Request Mapping
|
||||
= Mapping Requests
|
||||
|
||||
[.small]#xref:web/webmvc/mvc-controller/ann-requestmapping.adoc[See equivalent in the Servlet stack]#
|
||||
|
||||
This section discusses request mapping for annotated controllers.
|
||||
|
||||
[[webflux-ann-requestmapping-annotation]]
|
||||
== `@RequestMapping`
|
||||
|
||||
[.small]#xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-annotation[See equivalent in the Servlet stack]#
|
||||
|
||||
The `@RequestMapping` annotation is used to map requests to controllers methods. It has
|
||||
various attributes to match by URL, HTTP method, request parameters, headers, and media
|
||||
types. You can use it at the class level to express shared mappings or at the method level
|
||||
@@ -500,3 +507,68 @@ Kotlin::
|
||||
|
||||
|
||||
|
||||
[[webflux-ann-httpexchange-annotation]]
|
||||
== `@HttpExchange`
|
||||
[.small]#xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-httpexchange-annotation[See equivalent in the Reactive stack]#
|
||||
|
||||
As an alternative to `@RequestMapping`, you can also handle requests with `@HttpExchange`
|
||||
methods. Such methods are declared on an
|
||||
xref:integration/rest-clients.adoc#rest-http-interface[HTTP Interface] and can be used as
|
||||
a client via `HttpServiceProxyFactory` or implemented by a server `@Controller`.
|
||||
|
||||
For example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@RestController
|
||||
@HttpExchange("/persons")
|
||||
class PersonController {
|
||||
|
||||
@GetExchange("/{id}")
|
||||
public Person getPerson(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@PostExchange
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
public void add(@RequestBody Person person) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@RestController
|
||||
@HttpExchange("/persons")
|
||||
class PersonController {
|
||||
|
||||
@GetExchange("/{id}")
|
||||
fun getPerson(@PathVariable id: Long): Person {
|
||||
// ...
|
||||
}
|
||||
|
||||
@PostExchange
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
fun add(@RequestBody person: Person) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
There some differences between `@HttpExchange` and `@RequestMapping` since the
|
||||
former needs to remain suitable for client and server use. For example, while
|
||||
`@RequestMapping` can be declared to handle any number of paths and each path can
|
||||
be a pattern, `@HttpExchange` must be declared with a single, concrete path. There are
|
||||
also differences in the supported method parameters. Generally, `@HttpExchange` supports
|
||||
a subset of method parameters that `@RequestMapping` does, excluding any parameters that
|
||||
are server side only. For details see the list of supported method parameters for
|
||||
xref:integration/rest-clients.adoc#rest-http-interface-method-parameters[HTTP interface] and for
|
||||
xref:web/webflux/controller/ann-methods/arguments.adoc[@RequestMapping].
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
[[mvc-ann-initbinder]]
|
||||
= `DataBinder`
|
||||
= `@InitBinder`
|
||||
|
||||
[.small]#xref:web/webflux/controller/ann-initbinder.adoc[See equivalent in the Reactive stack]#
|
||||
|
||||
`@Controller` or `@ControllerAdvice` classes can have `@InitBinder` methods that
|
||||
initialize instances of `WebDataBinder`, and those, in turn, can:
|
||||
`@Controller` or `@ControllerAdvice` classes can have `@InitBinder` methods to
|
||||
initialize `WebDataBinder` instances that in turn can:
|
||||
|
||||
* Bind request parameters (that is, form or query data) to a model object.
|
||||
* Convert String-based request values (such as request parameters, path variables,
|
||||
headers, cookies, and others) to the target type of controller method arguments.
|
||||
* Format model object values as `String` values when rendering HTML forms.
|
||||
* Bind request parameters to a model object.
|
||||
* Convert request values from string to object property types.
|
||||
* Format model object properties as strings when rendering HTML forms.
|
||||
|
||||
`@InitBinder` methods can register controller-specific `java.beans.PropertyEditor` or
|
||||
Spring `Converter` and `Formatter` components. In addition, you can use the
|
||||
xref:web/webmvc/mvc-config/conversion.adoc[MVC config] to register `Converter` and `Formatter`
|
||||
types in a globally shared `FormattingConversionService`.
|
||||
In an `@Controller`, `DataBinder` customizations apply locally within the controller,
|
||||
or even to a specific model attribute referenced by name through the annotation.
|
||||
In an `@ControllerAdvice` customizations can apply to all or a subset of controllers.
|
||||
|
||||
`@InitBinder` methods support many of the same arguments that `@RequestMapping` methods
|
||||
do, except for `@ModelAttribute` (command object) arguments. Typically, they are declared
|
||||
with a `WebDataBinder` argument (for registrations) and a `void` return value.
|
||||
The following listing shows an example:
|
||||
You can register `PropertyEditor`, `Converter`, and `Formatter` components in the
|
||||
`DataBinder` for type conversion. Alternatively, you can use the
|
||||
xref:web/webmvc/mvc-config/conversion.adoc[MVC config] to register `Converter` and
|
||||
`Formatter` components in a globally shared `FormattingConversionService`.
|
||||
|
||||
`@InitBinder` methods can have many of the same arguments that `@RequestMapping` methods
|
||||
have, with the notable exception of `@ModelAttribute`. Typically, such methods have a
|
||||
`WebDataBinder` argument (for registrations) and a `void` return value, for example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
|
||||
+121
-94
@@ -3,11 +3,8 @@
|
||||
|
||||
[.small]#xref:web/webflux/controller/ann-methods/modelattrib-method-args.adoc[See equivalent in the Reactive stack]#
|
||||
|
||||
You can use the `@ModelAttribute` annotation on a method argument to access an attribute from
|
||||
the model or have it be instantiated if not present. The model attribute is also overlain with
|
||||
values from HTTP Servlet request parameters whose names match to field names. This is referred
|
||||
to as data binding, and it saves you from having to deal with parsing and converting individual
|
||||
query parameters and form fields. The following example shows how to do so:
|
||||
The `@ModelAttribute` method parameter annotation binds request parameters onto a model
|
||||
object. For example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -20,7 +17,7 @@ Java::
|
||||
// method logic...
|
||||
}
|
||||
----
|
||||
<1> Bind an instance of `Pet`.
|
||||
<1> Bind to an instance of `Pet`.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
@@ -31,30 +28,27 @@ fun processSubmit(@ModelAttribute pet: Pet): String { // <1>
|
||||
// method logic...
|
||||
}
|
||||
----
|
||||
<1> Bind an instance of `Pet`.
|
||||
<1> Bind to an instance of `Pet`.
|
||||
======
|
||||
|
||||
The `Pet` instance above is sourced in one of the following ways:
|
||||
The `Pet` instance may be:
|
||||
|
||||
* Retrieved from the model where it may have been added by a
|
||||
* Accessed from the model where it could have been added by a
|
||||
xref:web/webmvc/mvc-controller/ann-modelattrib-methods.adoc[@ModelAttribute method].
|
||||
* Retrieved from the HTTP session if the model attribute was listed in
|
||||
* Accessed from the HTTP session if the model attribute was listed in
|
||||
the class-level xref:web/webmvc/mvc-controller/ann-methods/sessionattributes.adoc[`@SessionAttributes`] annotation.
|
||||
* Obtained through a `Converter` where the model attribute name matches the name of a
|
||||
request value such as a path variable or a request parameter (see next example).
|
||||
* Instantiated using its default constructor.
|
||||
* Obtained through a `Converter` if the model attribute name matches the name of a
|
||||
request value such as a path variable or a request parameter (example follows).
|
||||
* Instantiated through a default constructor.
|
||||
* Instantiated through a "`primary constructor`" with arguments that match to Servlet
|
||||
request parameters. Argument names are determined through JavaBeans
|
||||
`@ConstructorProperties` or through runtime-retained parameter names in the bytecode.
|
||||
request parameters. Argument names are determined through runtime-retained parameter
|
||||
names in the bytecode.
|
||||
|
||||
One alternative to using a xref:web/webmvc/mvc-controller/ann-modelattrib-methods.adoc[@ModelAttribute method] to
|
||||
supply it or relying on the framework to create the model attribute, is to have a
|
||||
`Converter<String, T>` to provide the instance. This is applied when the model attribute
|
||||
name matches to the name of a request value such as a path variable or a request
|
||||
parameter, and there is a `Converter` from `String` to the model attribute type.
|
||||
In the following example, the model attribute name is `account` which matches the URI
|
||||
path variable `account`, and there is a registered `Converter<String, Account>` which
|
||||
could load the `Account` from a data store:
|
||||
As mentioned above, a `Converter<String, T>` may be used to obtain the model object if
|
||||
the model attribute name matches to the name of a request value such as a path variable or a
|
||||
request parameter, _and_ there is a compatible `Converter<String, T>`. In the below example,
|
||||
the model attribute name `account` matches URI path variable `account`, and there is a
|
||||
registered `Converter<String, Account>` that perhaps retrieves it from a persistence store:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -67,7 +61,6 @@ Java::
|
||||
// ...
|
||||
}
|
||||
----
|
||||
<1> Bind an instance of `Account` using an explicit attribute name.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
@@ -78,19 +71,101 @@ Kotlin::
|
||||
// ...
|
||||
}
|
||||
----
|
||||
<1> Bind an instance of `Account` using an explicit attribute name.
|
||||
======
|
||||
|
||||
After the model attribute instance is obtained, data binding is applied. The
|
||||
`WebDataBinder` class matches Servlet request parameter names (query parameters and form
|
||||
fields) to field names on the target `Object`. Matching fields are populated after type
|
||||
conversion is applied, where necessary. For more on data binding (and validation), see
|
||||
xref:web/webmvc/mvc-config/validation.adoc[Validation]. For more on customizing data binding, see
|
||||
xref:web/webmvc/mvc-controller/ann-initbinder.adoc[`DataBinder`].
|
||||
By default, both constructor and property
|
||||
xref:core/validation/beans-beans.adoc#beans-binding[data binding] are applied. However,
|
||||
model object design requires careful consideration, and for security reasons it is
|
||||
recommended either to use an object tailored specifically for web binding, or to apply
|
||||
constructor binding only. If property binding must still be used, then _allowedFields_
|
||||
patterns should be set to limit which properties can be set. For further details on this
|
||||
and example configuration, see
|
||||
xref:web/webmvc/mvc-controller/ann-initbinder.adoc#mvc-ann-initbinder-model-design[model design].
|
||||
|
||||
Data binding can result in errors. By default, a `BindException` is raised. However, to check
|
||||
for such errors in the controller method, you can add a `BindingResult` argument immediately next
|
||||
to the `@ModelAttribute`, as the following example shows:
|
||||
When using constructor binding, you can customize request parameter names through an
|
||||
`@BindParam` annotation. For example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
class Account {
|
||||
|
||||
private final String firstName;
|
||||
|
||||
public Account(@BindParam("first-name") String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
}
|
||||
----
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
class Account(@BindParam("first-name") val firstName: String)
|
||||
----
|
||||
======
|
||||
|
||||
NOTE: The `@BindParam` may also be placed on the fields that correspond to constructor
|
||||
parameters. While `@BindParam` is supported out of the box, you can also use a
|
||||
different annotation by setting a `DataBinder.NameResolver` on `DataBinder`
|
||||
|
||||
In some cases, you may want access to a model attribute without data binding. For such
|
||||
cases, you can inject the `Model` into the controller and access it directly or,
|
||||
alternatively, set `@ModelAttribute(binding=false)`, as the following example shows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@ModelAttribute
|
||||
public AccountForm setUpForm() {
|
||||
return new AccountForm();
|
||||
}
|
||||
|
||||
@ModelAttribute
|
||||
public Account findAccount(@PathVariable String accountId) {
|
||||
return accountRepository.findOne(accountId);
|
||||
}
|
||||
|
||||
@PostMapping("update")
|
||||
public String update(AccountForm form, BindingResult result,
|
||||
@ModelAttribute(binding=false) Account account) { // <1>
|
||||
// ...
|
||||
}
|
||||
----
|
||||
<1> Setting `@ModelAttribute(binding=false)`.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@ModelAttribute
|
||||
fun setUpForm(): AccountForm {
|
||||
return AccountForm()
|
||||
}
|
||||
|
||||
@ModelAttribute
|
||||
fun findAccount(@PathVariable accountId: String): Account {
|
||||
return accountRepository.findOne(accountId)
|
||||
}
|
||||
|
||||
@PostMapping("update")
|
||||
fun update(form: AccountForm, result: BindingResult,
|
||||
@ModelAttribute(binding = false) account: Account): String { // <1>
|
||||
// ...
|
||||
}
|
||||
----
|
||||
<1> Setting `@ModelAt\tribute(binding=false)`.
|
||||
======
|
||||
|
||||
If data binding results in errors, by default a `MethodArgumentNotValidException` is raised,
|
||||
but you can also add a `BindingResult` argument immediately next to the `@ModelAttribute`
|
||||
in order to handle such errors in the controller method. For example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -123,61 +198,10 @@ Kotlin::
|
||||
<1> Adding a `BindingResult` next to the `@ModelAttribute`.
|
||||
======
|
||||
|
||||
In some cases, you may want access to a model attribute without data binding. For such
|
||||
cases, you can inject the `Model` into the controller and access it directly or,
|
||||
alternatively, set `@ModelAttribute(binding=false)`, as the following example shows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@ModelAttribute
|
||||
public AccountForm setUpForm() {
|
||||
return new AccountForm();
|
||||
}
|
||||
|
||||
@ModelAttribute
|
||||
public Account findAccount(@PathVariable String accountId) {
|
||||
return accountRepository.findOne(accountId);
|
||||
}
|
||||
|
||||
@PostMapping("update")
|
||||
public String update(@Valid AccountForm form, BindingResult result,
|
||||
@ModelAttribute(binding=false) Account account) { // <1>
|
||||
// ...
|
||||
}
|
||||
----
|
||||
<1> Setting `@ModelAttribute(binding=false)`.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@ModelAttribute
|
||||
fun setUpForm(): AccountForm {
|
||||
return AccountForm()
|
||||
}
|
||||
|
||||
@ModelAttribute
|
||||
fun findAccount(@PathVariable accountId: String): Account {
|
||||
return accountRepository.findOne(accountId)
|
||||
}
|
||||
|
||||
@PostMapping("update")
|
||||
fun update(@Valid form: AccountForm, result: BindingResult,
|
||||
@ModelAttribute(binding = false) account: Account): String { // <1>
|
||||
// ...
|
||||
}
|
||||
----
|
||||
<1> Setting `@ModelAttribute(binding=false)`.
|
||||
======
|
||||
|
||||
You can automatically apply validation after data binding by adding the
|
||||
`jakarta.validation.Valid` annotation or Spring's `@Validated` annotation
|
||||
(xref:core/validation/beanvalidation.adoc[Bean Validation] and
|
||||
xref:web/webmvc/mvc-config/validation.adoc[Spring validation]). The following example shows how to do so:
|
||||
`jakarta.validation.Valid` annotation or Spring's `@Validated` annotation.
|
||||
See xref:core/validation/beanvalidation.adoc[Bean Validation] and
|
||||
xref:web/webmvc/mvc-config/validation.adoc[Spring validation]. For example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -210,10 +234,13 @@ 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 there is no `BindingResult` parameter after the `@ModelAttribute`, then
|
||||
`MethodArgumentNotValueException` is raised with the validation errors. However, if method
|
||||
validation applies because other parameters have `@jakarta.validation.Constraint` annotations,
|
||||
then `HandlerMethodValidationException` is raised instead. For more details, see the section
|
||||
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].
|
||||
|
||||
|
||||
+75
-1
@@ -1,8 +1,17 @@
|
||||
[[mvc-ann-requestmapping]]
|
||||
= Request Mapping
|
||||
= Mapping Requests
|
||||
|
||||
[.small]#xref:web/webflux/controller/ann-requestmapping.adoc[See equivalent in the Reactive stack]#
|
||||
|
||||
This section discusses request mapping for annotated controllers.
|
||||
|
||||
|
||||
|
||||
[[mvc-ann-requestmapping-annotation]]
|
||||
== `@RequestMapping`
|
||||
|
||||
[.small]#xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-annotation[See equivalent in the Reactive stack]#
|
||||
|
||||
You can use the `@RequestMapping` annotation to map requests to controllers methods. It has
|
||||
various attributes to match by URL, HTTP method, request parameters, headers, and media
|
||||
types. You can use it at the class level to express shared mappings or at the method level
|
||||
@@ -549,3 +558,68 @@ Kotlin::
|
||||
|
||||
|
||||
|
||||
[[mvc-ann-httpexchange-annotation]]
|
||||
== `@HttpExchange`
|
||||
[.small]#xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-httpexchange-annotation[See equivalent in the Reactive stack]#
|
||||
|
||||
As an alternative to `@RequestMapping`, you can also handle requests with `@HttpExchange`
|
||||
methods. Such methods are declared on an
|
||||
xref:integration/rest-clients.adoc#rest-http-interface[HTTP Interface] and can be used as
|
||||
a client via `HttpServiceProxyFactory` or implemented by a server `@Controller`.
|
||||
|
||||
For example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@RestController
|
||||
@HttpExchange("/persons")
|
||||
class PersonController {
|
||||
|
||||
@GetExchange("/{id}")
|
||||
public Person getPerson(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@PostExchange
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
public void add(@RequestBody Person person) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@RestController
|
||||
@HttpExchange("/persons")
|
||||
class PersonController {
|
||||
|
||||
@GetExchange("/{id}")
|
||||
fun getPerson(@PathVariable id: Long): Person {
|
||||
// ...
|
||||
}
|
||||
|
||||
@PostExchange
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
fun add(@RequestBody person: Person) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
There some differences between `@HttpExchange` and `@RequestMapping` since the
|
||||
former needs to remain suitable for client and server use. For example, while
|
||||
`@RequestMapping` can be declared to handle any number of paths and each path can
|
||||
be a pattern, `@HttpExchange` must be declared with a single, concrete path. There are
|
||||
also differences in the supported method parameters. Generally, `@HttpExchange` supports
|
||||
a subset of method parameters that `@RequestMapping` does, excluding any parameters that
|
||||
are server side only. For details see the list of supported method parameters for
|
||||
xref:integration/rest-clients.adoc#rest-http-interface-method-parameters[HTTP interface] and for
|
||||
xref:web/webmvc/mvc-controller/ann-methods/arguments.adoc[@RequestMapping].
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -280,9 +280,7 @@ The following example shows the XML configuration equivalent of the preceding ex
|
||||
NOTE: For client-side WebSocket configuration, you should use `WebSocketContainerFactoryBean`
|
||||
(XML) or `ContainerProvider.getWebSocketContainer()` (Java configuration).
|
||||
|
||||
For Jetty, you need to supply a pre-configured Jetty `WebSocketServerFactory` and plug
|
||||
that into Spring's `DefaultHandshakeHandler` through your WebSocket Java config.
|
||||
The following example shows how to do so:
|
||||
For Jetty, you need to supply a `Consumer` callback to configure the WebSocket server. For example:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@@ -292,62 +290,20 @@ The following example shows how to do so:
|
||||
|
||||
@Override
|
||||
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
||||
|
||||
JettyRequestUpgradeStrategy upgradeStrategy = new JettyRequestUpgradeStrategy();
|
||||
upgradeStrategy.addWebSocketConfigurer(configurable -> {
|
||||
policy.setInputBufferSize(8192);
|
||||
policy.setIdleTimeout(600000);
|
||||
});
|
||||
|
||||
registry.addHandler(echoWebSocketHandler(),
|
||||
"/echo").setHandshakeHandler(handshakeHandler());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DefaultHandshakeHandler handshakeHandler() {
|
||||
|
||||
WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
|
||||
policy.setInputBufferSize(8192);
|
||||
policy.setIdleTimeout(600000);
|
||||
|
||||
return new DefaultHandshakeHandler(
|
||||
new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));
|
||||
"/echo").setHandshakeHandler(new DefaultHandshakeHandler(upgradeStrategy));
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
The following example shows the XML configuration equivalent of the preceding example:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes,attributes"]
|
||||
----
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:websocket="http://www.springframework.org/schema/websocket"
|
||||
xsi:schemaLocation="
|
||||
http://www.springframework.org/schema/beans
|
||||
https://www.springframework.org/schema/beans/spring-beans.xsd
|
||||
http://www.springframework.org/schema/websocket
|
||||
https://www.springframework.org/schema/websocket/spring-websocket.xsd">
|
||||
|
||||
<websocket:handlers>
|
||||
<websocket:mapping path="/echo" handler="echoHandler"/>
|
||||
<websocket:handshake-handler ref="handshakeHandler"/>
|
||||
</websocket:handlers>
|
||||
|
||||
<bean id="handshakeHandler" class="org.springframework...DefaultHandshakeHandler">
|
||||
<constructor-arg ref="upgradeStrategy"/>
|
||||
</bean>
|
||||
|
||||
<bean id="upgradeStrategy" class="org.springframework...JettyRequestUpgradeStrategy">
|
||||
<constructor-arg ref="serverFactory"/>
|
||||
</bean>
|
||||
|
||||
<bean id="serverFactory" class="org.eclipse.jetty...WebSocketServerFactory">
|
||||
<constructor-arg>
|
||||
<bean class="org.eclipse.jetty...WebSocketPolicy">
|
||||
<constructor-arg value="SERVER"/>
|
||||
<property name="inputBufferSize" value="8092"/>
|
||||
<property name="idleTimeout" value="600000"/>
|
||||
</bean>
|
||||
</constructor-arg>
|
||||
</bean>
|
||||
|
||||
</beans>
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,29 +1,16 @@
|
||||
In the context of web applications, _data binding_ involves the binding of HTTP request
|
||||
parameters (that is, form data or query parameters) to properties in a model object and
|
||||
its nested objects.
|
||||
xref:core/validation/beans-beans.adoc#beans-binding[Data binding] for web requests involves
|
||||
binding request parameters to a model object. By default, request parameters can be bound
|
||||
to any public property of the model object, which means malicious clients can provide
|
||||
extra values for properties that exist in the model object graph, but are not expected to
|
||||
be set. This is why model object design requires careful consideration.
|
||||
|
||||
Only `public` properties following the
|
||||
https://www.oracle.com/java/technologies/javase/javabeans-spec.html[JavaBeans naming conventions]
|
||||
are exposed for data binding — for example, `public String getFirstName()` and
|
||||
`public void setFirstName(String)` methods for a `firstName` property.
|
||||
|
||||
TIP: The model object, and its nested object graph, is also sometimes referred to as a
|
||||
TIP: The model object, and its nested object graph is also sometimes referred to as a
|
||||
_command object_, _form-backing object_, or _POJO_ (Plain Old Java Object).
|
||||
|
||||
By default, Spring permits binding to all public properties in the model object graph.
|
||||
This means you need to carefully consider what public properties the model has, since a
|
||||
client could target any public property path, even some that are not expected to be
|
||||
targeted for a given use case.
|
||||
|
||||
For example, given an HTTP form data endpoint, a malicious client could supply values for
|
||||
properties that exist in the model object graph but are not part of the HTML form
|
||||
presented in the browser. This could lead to data being set on the model object and any
|
||||
of its nested objects, that is not expected to be updated.
|
||||
|
||||
The recommended approach is to use a _dedicated model object_ that exposes only
|
||||
properties that are relevant for the form submission. For example, on a form for changing
|
||||
a user's email address, the model object should declare a minimum set of properties such
|
||||
as in the following `ChangeEmailForm`.
|
||||
A good practice is to use a _dedicated model object_ rather than exposing your domain
|
||||
model such as JPA or Hibernate entities for web data binding. For example, on a form to
|
||||
change an email address, create a `ChangeEmailForm` model object that declares only
|
||||
the properties required for the input:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@@ -51,13 +38,16 @@ as in the following `ChangeEmailForm`.
|
||||
}
|
||||
----
|
||||
|
||||
If you cannot or do not want to use a _dedicated model object_ for each data
|
||||
binding use case, you **must** limit the properties that are allowed for data binding.
|
||||
Ideally, you can achieve this by registering _allowed field patterns_ via the
|
||||
`setAllowedFields()` method on `WebDataBinder`.
|
||||
Another good practice is to apply
|
||||
xref:core/validation/beans-beans.adoc#beans-constructor-binding[constructor binding],
|
||||
which uses only the request parameters it needs for constructor arguments, and any other
|
||||
input is ignored. This is in contrast to property binding which by default binds every
|
||||
request parameter for which there is a matching property.
|
||||
|
||||
For example, to register allowed field patterns in your application, you can implement an
|
||||
`@InitBinder` method in a `@Controller` or `@ControllerAdvice` component as shown below:
|
||||
If neither a dedicated model object nor constructor binding is sufficient, and you must
|
||||
use property binding, we strongy recommend registering `allowedFields` patterns (case
|
||||
sensitive) on `WebDataBinder` in order to prevent unexpected properties from being set.
|
||||
For example:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@@ -74,22 +64,28 @@ For example, to register allowed field patterns in your application, you can imp
|
||||
}
|
||||
----
|
||||
|
||||
In addition to registering allowed patterns, it is also possible to register _disallowed
|
||||
field patterns_ via the `setDisallowedFields()` method in `DataBinder` and its subclasses.
|
||||
Please note, however, that an "allow list" is safer than a "deny list". Consequently,
|
||||
`setAllowedFields()` should be favored over `setDisallowedFields()`.
|
||||
You can also register `disallowedFields` patterns (case insensitive). However,
|
||||
"allowed" configuration is preferred over "disallowed" as it is more explicit and less
|
||||
prone to mistakes.
|
||||
|
||||
Note that matching against allowed field patterns is case-sensitive; whereas, matching
|
||||
against disallowed field patterns is case-insensitive. In addition, a field matching a
|
||||
disallowed pattern will not be accepted even if it also happens to match a pattern in the
|
||||
allowed list.
|
||||
By default, constructor and property binding are both used. If you want to use
|
||||
constructor binding only, you can set the `declarativeBinding` flag on `WebDataBinder`
|
||||
through an `@InitBinder` method either locally within a controller or globally through an
|
||||
`@ControllerAdvice`. Turning this flag on ensures that only constructor binding is used
|
||||
and that property binding is not used unless `allowedFields` patterns are configured.
|
||||
For example:
|
||||
|
||||
[WARNING]
|
||||
====
|
||||
It is extremely important to properly configure allowed and disallowed field patterns
|
||||
when exposing your domain model directly for data binding purposes. Otherwise, it is a
|
||||
big security risk.
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Controller
|
||||
public class MyController {
|
||||
|
||||
Furthermore, it is strongly recommended that you do **not** use types from your domain
|
||||
model such as JPA or Hibernate entities as the model object in data binding scenarios.
|
||||
====
|
||||
@InitBinder
|
||||
void initBinder(WebDataBinder binder) {
|
||||
binder.setDeclarativeBinding(true);
|
||||
}
|
||||
|
||||
// @RequestMapping methods, etc.
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
+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.applicationevents;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.event.SimpleApplicationEventMulticaster;
|
||||
import org.springframework.core.task.SimpleAsyncTaskExecutor;
|
||||
import org.springframework.core.task.support.ContextPropagatingTaskDecorator;
|
||||
|
||||
@Configuration
|
||||
public class ApplicationEventsConfiguration {
|
||||
|
||||
@Bean(name = "applicationEventMulticaster")
|
||||
public SimpleApplicationEventMulticaster simpleApplicationEventMulticaster() {
|
||||
SimpleApplicationEventMulticaster eventMulticaster = new SimpleApplicationEventMulticaster();
|
||||
SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor();
|
||||
// decorate task execution with a decorator that supports context propagation
|
||||
taskExecutor.setTaskDecorator(new ContextPropagatingTaskDecorator());
|
||||
eventMulticaster.setTaskExecutor(taskExecutor);
|
||||
return eventMulticaster;
|
||||
}
|
||||
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.applicationevents;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class EmailNotificationListener {
|
||||
|
||||
private final Log logger = LogFactory.getLog(EmailNotificationListener.class);
|
||||
|
||||
@EventListener(EmailReceivedEvent.class)
|
||||
@Async("propagatingContextExecutor")
|
||||
public void emailReceived(EmailReceivedEvent event) {
|
||||
// asynchronously process the received event
|
||||
// this logging statement will contain the expected MDC entries from the propagated context
|
||||
logger.info("email has been received");
|
||||
}
|
||||
|
||||
}
|
||||
+8
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2013 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.
|
||||
@@ -14,16 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.http.client;
|
||||
package org.springframework.docs.integration.observability.applicationevents;
|
||||
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
public class NoOutputStreamingStreamingSimpleHttpRequestFactoryTests extends AbstractHttpRequestFactoryTests {
|
||||
@SuppressWarnings("serial")
|
||||
public class EmailReceivedEvent extends ApplicationEvent {
|
||||
|
||||
@Override
|
||||
protected ClientHttpRequestFactory createRequestFactory() {
|
||||
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
||||
factory.setBufferRequestBody(false);
|
||||
factory.setOutputStreaming(false);
|
||||
return factory;
|
||||
public EmailReceivedEvent(Object source) {
|
||||
super(source);
|
||||
}
|
||||
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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.applicationevents;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.task.SimpleAsyncTaskExecutor;
|
||||
import org.springframework.core.task.TaskExecutor;
|
||||
import org.springframework.core.task.support.ContextPropagatingTaskDecorator;
|
||||
|
||||
@Configuration
|
||||
public class EventAsyncExecutionConfiguration {
|
||||
|
||||
@Bean(name = "propagatingContextExecutor")
|
||||
public TaskExecutor propagatingContextExecutor() {
|
||||
SimpleAsyncTaskExecutor taskExecutor = new SimpleAsyncTaskExecutor();
|
||||
// decorate task execution with a decorator that supports context propagation
|
||||
taskExecutor.setTaskDecorator(new ContextPropagatingTaskDecorator());
|
||||
return taskExecutor;
|
||||
}
|
||||
|
||||
}
|
||||
+2
-2
@@ -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.
|
||||
@@ -59,7 +59,7 @@ public class CustomServerRequestObservationConvention implements ServerRequestOb
|
||||
}
|
||||
|
||||
private KeyValue exception(ServerRequestObservationContext context) {
|
||||
String exception = (context.getError() != null) ? context.getError().getClass().getSimpleName() : KeyValue.NONE_VALUE;
|
||||
String exception = (context.getError() != null ? context.getError().getClass().getSimpleName() : KeyValue.NONE_VALUE);
|
||||
return KeyValue.of(ServerHttpObservationDocumentation.LowCardinalityKeyNames.EXCEPTION, exception);
|
||||
}
|
||||
|
||||
|
||||
+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();
|
||||
}
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.jms.process;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import jakarta.jms.ConnectionFactory;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.jms.annotation.EnableJms;
|
||||
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
|
||||
|
||||
@Configuration
|
||||
@EnableJms
|
||||
public class JmsConfiguration {
|
||||
|
||||
@Bean
|
||||
public DefaultJmsListenerContainerFactory jmsListenerContainerFactory(ConnectionFactory connectionFactory, ObservationRegistry observationRegistry) {
|
||||
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
|
||||
factory.setConnectionFactory(connectionFactory);
|
||||
factory.setObservationRegistry(observationRegistry);
|
||||
return factory;
|
||||
}
|
||||
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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.jms.publish;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import jakarta.jms.ConnectionFactory;
|
||||
|
||||
import org.springframework.jms.core.JmsMessagingTemplate;
|
||||
import org.springframework.jms.core.JmsTemplate;
|
||||
|
||||
public class JmsTemplatePublish {
|
||||
|
||||
private final JmsTemplate jmsTemplate;
|
||||
|
||||
private final JmsMessagingTemplate jmsMessagingTemplate;
|
||||
|
||||
public JmsTemplatePublish(ObservationRegistry observationRegistry, ConnectionFactory connectionFactory) {
|
||||
this.jmsTemplate = new JmsTemplate(connectionFactory);
|
||||
// configure the observation registry
|
||||
this.jmsTemplate.setObservationRegistry(observationRegistry);
|
||||
|
||||
// For JmsMessagingTemplate, instantiate it with a JMS template that has a configured registry
|
||||
this.jmsMessagingTemplate = new JmsMessagingTemplate(this.jmsTemplate);
|
||||
}
|
||||
|
||||
public void sendMessages() {
|
||||
this.jmsTemplate.convertAndSend("spring.observation.test", "test message");
|
||||
}
|
||||
|
||||
}
|
||||
+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,18 +7,19 @@ javaPlatform {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.14.3"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.10.11"))
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.15.2"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.12.0-M3"))
|
||||
api(platform("io.netty:netty-bom:4.1.97.Final"))
|
||||
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
|
||||
api(platform("io.projectreactor:reactor-bom:2022.0.11"))
|
||||
api(platform("io.projectreactor:reactor-bom:2023.0.0-M3"))
|
||||
api(platform("io.rsocket:rsocket-bom:1.1.3"))
|
||||
api(platform("org.apache.groovy:groovy-bom:4.0.14"))
|
||||
api(platform("org.apache.logging.log4j:log4j-bom:2.20.0"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:11.0.16"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.4.0"))
|
||||
api(platform("org.junit:junit-bom:5.9.3"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.0.1"))
|
||||
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.1"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.0"))
|
||||
api(platform("org.junit:junit-bom:5.10.0"))
|
||||
api(platform("org.mockito:mockito-bom:5.5.0"))
|
||||
|
||||
constraints {
|
||||
@@ -45,7 +46,7 @@ dependencies {
|
||||
api("com.thoughtworks.xstream:xstream:1.4.20")
|
||||
api("commons-io:commons-io:2.11.0")
|
||||
api("de.bechte.junit:junit-hierarchicalcontextrunner:4.12.2")
|
||||
api("io.micrometer:context-propagation:1.0.5")
|
||||
api("io.micrometer:context-propagation:1.1.0-M4")
|
||||
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")
|
||||
@@ -83,6 +84,7 @@ dependencies {
|
||||
api("jakarta.xml.bind:jakarta.xml.bind-api:3.0.1")
|
||||
api("javax.annotation:javax.annotation-api:1.3.2")
|
||||
api("javax.cache:cache-api:1.1.1")
|
||||
api("javax.inject:javax.inject:1")
|
||||
api("javax.money:money-api:1.1")
|
||||
api("jaxen:jaxen:1.2.0")
|
||||
api("junit:junit:4.13.2")
|
||||
@@ -92,6 +94,8 @@ dependencies {
|
||||
api("org.apache.activemq:activemq-broker:5.17.4")
|
||||
api("org.apache.activemq:activemq-kahadb-store:5.17.4")
|
||||
api("org.apache.activemq:activemq-stomp:5.17.4")
|
||||
api("org.apache.activemq:artemis-junit-5:2.29.0")
|
||||
api("org.apache.activemq:artemis-jakarta-client:2.29.0")
|
||||
api("org.apache.commons:commons-pool2:2.9.0")
|
||||
api("org.apache.derby:derby:10.16.1.1")
|
||||
api("org.apache.derby:derbyclient:10.16.1.1")
|
||||
@@ -110,8 +114,9 @@ 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.4.0")
|
||||
api("org.dom4j:dom4j:2.1.4")
|
||||
api("org.eclipse.jetty:jetty-reactive-httpclient:3.0.8")
|
||||
api("org.eclipse.jetty:jetty-reactive-httpclient:4.0.0")
|
||||
api("org.eclipse.persistence:org.eclipse.persistence.jpa:3.0.3")
|
||||
api("org.eclipse:yasson:2.0.4")
|
||||
api("org.ehcache:ehcache:3.10.8")
|
||||
@@ -143,6 +148,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-M5
|
||||
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
org.gradle.parallel=true
|
||||
|
||||
kotlinVersion=1.7.21
|
||||
kotlinVersion=1.9.10
|
||||
|
||||
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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user