mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
757 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d03ef343e | |||
| bd83fb7021 | |||
| e41fe16d37 | |||
| 1187bc2016 | |||
| 01fea5e7ed | |||
| ea340fbe69 | |||
| 807cf5e254 | |||
| 3b5acc270f | |||
| 88c92ab067 | |||
| 4956cb7b82 | |||
| e04de95ef2 | |||
| 5cb3ea228e | |||
| cce2771639 | |||
| bf78980925 | |||
| 2a29e16456 | |||
| 6ae89ea397 | |||
| 7acd4aec38 | |||
| 0ad4fcaeaf | |||
| ac3c1b8762 | |||
| a0ed3f052e | |||
| 3a0a755144 | |||
| d0cd7af7e6 | |||
| 71987a8713 | |||
| d4e4a9ae06 | |||
| 7a8db31dc3 | |||
| 5cb2f870d0 | |||
| 746679f7a7 | |||
| 46e6783593 | |||
| 0f2fbd1983 | |||
| 254a55d217 | |||
| 0df75ff75e | |||
| 261862076c | |||
| 680094d74e | |||
| 233eb7f0aa | |||
| 18756c0701 | |||
| 33aeb6ee9c | |||
| c7787b8383 | |||
| dde69c96b0 | |||
| 74f4a07eac | |||
| c28b22b465 | |||
| 958dab7c84 | |||
| 6878587a33 | |||
| 32e2c453b0 | |||
| 2018d933ac | |||
| 3fb9c2b507 | |||
| 5fbfdbc91c | |||
| 4f38c5bdfa | |||
| 13b09e6f3c | |||
| ead80ce2f5 | |||
| dbaba3d88a | |||
| 97d5f7f07d | |||
| 733e695e18 | |||
| e673345102 | |||
| 6c4651925e | |||
| d8b506e09a | |||
| e5d4d7c13c | |||
| 22e7f24731 | |||
| fef969145d | |||
| 964d67474c | |||
| 4a99fa8846 | |||
| ecdb63371e | |||
| f8e7bf7825 | |||
| 67f3ff574f | |||
| 42e854e5ff | |||
| d2246162c3 | |||
| abbee1a050 | |||
| 13f9fed4bc | |||
| d0a1ba5092 | |||
| b98ef1309b | |||
| 4d15c136b1 | |||
| bb513de224 | |||
| 86b5679516 | |||
| 8375962813 | |||
| 6d6f657858 | |||
| 0c6a26a381 | |||
| d0af5146e0 | |||
| 5d3170448a | |||
| d8c7c250e3 | |||
| 5bdec756b9 | |||
| 1234d29d78 | |||
| c74fc7ec90 | |||
| 2db1a63a92 | |||
| 4bf8e2f366 | |||
| d8503daa1f | |||
| fed6e9b3c3 | |||
| 98cdae6444 | |||
| cdee5c5b79 | |||
| 52265a5c65 | |||
| ead76b6317 | |||
| 838b4d67a5 | |||
| 2a3e7ea3f9 | |||
| 014a395aed | |||
| ffb32f4238 | |||
| 61a517ea3b | |||
| f2dd7b01cd | |||
| 78f901649e | |||
| d3a5aefdff | |||
| fc516cb39e | |||
| e7402bc365 | |||
| 49ef150c5d | |||
| c5e865494e | |||
| a185f15352 | |||
| 7a4bf1dcb2 | |||
| 16bb91e2e1 | |||
| cb76ebcf9d | |||
| 39e263fe5d | |||
| 3057de7746 | |||
| d8b05c7eba | |||
| b24f4edbec | |||
| 4599ad4168 | |||
| 40853825dc | |||
| 88e773ae24 | |||
| e3e99ac8a0 | |||
| 5fb37e3133 | |||
| aad742ded0 | |||
| 76ba02ec3e | |||
| fb94109c09 | |||
| c55bebab21 | |||
| af461fc0a6 | |||
| d5baab2aed | |||
| 7dfe0cc3bd | |||
| 23aea5c402 | |||
| 6ade8c5a41 | |||
| 6ab4cf49a8 | |||
| 75ed4be446 | |||
| 7bb7456686 | |||
| e35db3311f | |||
| df4868bf83 | |||
| 1795b234e4 | |||
| 24c44019a0 | |||
| 8bc99fa662 | |||
| 4283a34fa4 | |||
| 65cdbf7c07 | |||
| 024f421170 | |||
| 48df362dfc | |||
| 63daa7a0e4 | |||
| 343f613f7c | |||
| cfb7888044 | |||
| 555d17df14 | |||
| 8d48a1e34b | |||
| eee45c3583 | |||
| 76d335aa41 | |||
| 5b4511fbf7 | |||
| 907c1db7a6 | |||
| 98de16bc50 | |||
| d6e35cf1f0 | |||
| 18989123ac | |||
| a9cab2a3f1 | |||
| d10d8e98c2 | |||
| 6576c52ed3 | |||
| 71b6dcfa7e | |||
| 5b1c55252e | |||
| e9c4665a80 | |||
| b94ab90eb2 | |||
| dd888ed813 | |||
| 7f1bc20688 | |||
| 0b92a51650 | |||
| c35aac0c52 | |||
| a787088df2 | |||
| 533283a23e | |||
| 7970046f17 | |||
| d06b47818b | |||
| 5e2c16c30c | |||
| 7bf628c827 | |||
| 483abd96a4 | |||
| 5c93bb38c4 | |||
| 10e32c92e6 | |||
| a122dda596 | |||
| ebdebbbd06 | |||
| 779f1b080e | |||
| b2af1af50f | |||
| 18645022e9 | |||
| 87fa9a5acb | |||
| 85bc6350ce | |||
| d771d02119 | |||
| 302f04ecf3 | |||
| 1c0bcba587 | |||
| 42409e21fa | |||
| 49e24b7dfa | |||
| 9721cbad5c | |||
| 39c4cc537d | |||
| 7c3618de7c | |||
| 92b0eb7f8b | |||
| a63c5ad305 | |||
| 551f6c0c66 | |||
| 9956cc11bb | |||
| 8379ac772a | |||
| b8c2780bfe | |||
| ac7c7ff5b2 | |||
| 51c084ffb6 | |||
| 5be83e9223 | |||
| e41487492d | |||
| 8edda5c768 | |||
| 2fa7b30831 | |||
| 7ab108a321 | |||
| f55ca3058b | |||
| 7e9c33f50e | |||
| 00b88ec76f | |||
| 3872c1a762 | |||
| fb423d66e3 | |||
| 20b35f068a | |||
| 7d0cc6c83a | |||
| d1a1364231 | |||
| 02c7719eef | |||
| d5bfd7ff8b | |||
| 065692237c | |||
| 603e559397 | |||
| b8b279d3e0 | |||
| 274a689a10 | |||
| 7d5b3892c4 | |||
| 8819c749ce | |||
| b7a9bee020 | |||
| 58246ec5ec | |||
| 07ae1853ff | |||
| 5ce64f47b2 | |||
| 3e788e4ca1 | |||
| 66c94bb4a2 | |||
| 0e1422820f | |||
| a145f6205e | |||
| d710f73c47 | |||
| 6505c4b839 | |||
| cc9b39b53f | |||
| a376ef36e4 | |||
| cacc63da20 | |||
| 92ee20c896 | |||
| 8db1340263 | |||
| 2f8c5a580a | |||
| 2bc213d703 | |||
| 087d239148 | |||
| 73c4b46ed7 | |||
| 6ab87477ea | |||
| 84ba6b4b26 | |||
| e05d4f2f18 | |||
| 69ed984a81 | |||
| ce815006d2 | |||
| d6a7aefd1d | |||
| a0763d13c5 | |||
| 30a7f7be28 | |||
| 8ee09e5766 | |||
| de75b6e1a2 | |||
| b660d8c553 | |||
| 892a5cdca6 | |||
| 37a135447c | |||
| 13efc2205b | |||
| 6e1706a8a4 | |||
| f678af4626 | |||
| 666e2df0f3 | |||
| ae13eac86b | |||
| d2a8b56742 | |||
| 4724020174 | |||
| 90e32cadeb | |||
| d9bd168d54 | |||
| 68fce29ae9 | |||
| 1780e30a43 | |||
| c7b0550e43 | |||
| 71716e848d | |||
| 2c05e991b5 | |||
| d3d951e44b | |||
| f8a82b46c1 | |||
| 057742f27a | |||
| ea551cd9b3 | |||
| 1eec0382d7 | |||
| 762831e742 | |||
| 641b809d4b | |||
| 0d3f07fdd6 | |||
| eb7b26db24 | |||
| 86b21d9b5c | |||
| beb3a91847 | |||
| 4d955f9ed8 | |||
| f8f712dff8 | |||
| bb2174c260 | |||
| e73dc37513 | |||
| 51d34fff64 | |||
| e9701a9ce3 | |||
| 15b251dcd6 | |||
| 91d0ebe327 | |||
| 789791e186 | |||
| a0e2d3a221 | |||
| c74f897fac | |||
| 639af7befa | |||
| 86d81632c8 | |||
| 80df88bd4f | |||
| 15a71f94fb | |||
| 5bd03494d4 | |||
| fda9b9a696 | |||
| bedc235bab | |||
| 682e2d6d84 | |||
| 496be9ca98 | |||
| aeaf52ee96 | |||
| e09cdcd920 | |||
| d722b9434e | |||
| 2a98b3137d | |||
| 52e01bbb62 | |||
| cd4e73a4a3 | |||
| a605f07100 | |||
| 5a0848b579 | |||
| 2fbd54078b | |||
| ca7ebd60ed | |||
| 702c63a7e8 | |||
| 955cc2039b | |||
| 5ffd88cd89 | |||
| e9345f16dc | |||
| 3632c09348 | |||
| 84a57861d1 | |||
| 7bc712e304 | |||
| f7db4bf4f0 | |||
| 533ecf0244 | |||
| 99d5e90b5e | |||
| 788b66feac | |||
| 6a9783bb1a | |||
| b610711235 | |||
| fec0bfe6b3 | |||
| 03cce13937 | |||
| 7f020118eb | |||
| 72afd4b9bd | |||
| 33fef8df84 | |||
| 8a7d20f844 | |||
| f55fe79805 | |||
| 68e9460e9b | |||
| 382caac37c | |||
| 5f98db6ffe | |||
| fb77fcd922 | |||
| a1410d98de | |||
| dc580740c0 | |||
| 466ac6b703 | |||
| 8b14bf86ef | |||
| 75329e6d9d | |||
| 34129f3f89 | |||
| 7c4594c014 | |||
| 1aea698088 | |||
| db5505d55a | |||
| a08be368a2 | |||
| a4d3977692 | |||
| 4ffbc4ea18 | |||
| 5dc7425a8f | |||
| e34899c06f | |||
| 24cafbb0f5 | |||
| 67d2635947 | |||
| 63f6c33db6 | |||
| 48aa94be46 | |||
| 2e4fbd1ff3 | |||
| bc82077350 | |||
| 63ee6e6fe2 | |||
| 4dd83c814c | |||
| b8d9dee7be | |||
| eae09637a1 | |||
| 6bdb9bb950 | |||
| d9100917d1 | |||
| 21604d1a25 | |||
| fe41cd6d20 | |||
| e64243d8a1 | |||
| 6868ff7dbe | |||
| 9bc7229a1c | |||
| 4d6947d14f | |||
| 8df21109f2 | |||
| f42e886f03 | |||
| de4db3812a | |||
| 9eae0b6f6f | |||
| 8a53525209 | |||
| 1fd6ded7a0 | |||
| 667004e5fa | |||
| ba74de997a | |||
| 011e398355 | |||
| 295a9565a3 | |||
| b07ff1c2d4 | |||
| bb33a3efa3 | |||
| 3956a36837 | |||
| 5c09435816 | |||
| 7271358ea4 | |||
| 39bd530461 | |||
| dce3f71b7d | |||
| 7077809561 | |||
| 6ded25ba28 | |||
| e78e802647 | |||
| 18e31fcfba | |||
| 722701a451 | |||
| e9d16da633 | |||
| bae12e739d | |||
| 85855ec793 | |||
| 2f6d142ad7 | |||
| 7557967f9e | |||
| 4ba14ca58c | |||
| b78d371746 | |||
| b0a1a11612 | |||
| e49d2da443 | |||
| e6f2f86f9f | |||
| 1a29fbda48 | |||
| 6d63abd4e9 | |||
| e81fcc34de | |||
| 11a1858e79 | |||
| 68fcf81c4d | |||
| 3fff3b8a6d | |||
| bd9ae6bd2a | |||
| a09f454dfc | |||
| cf1d274baa | |||
| 55a090602b | |||
| 0c739016d4 | |||
| 262ce16bb4 | |||
| 66b3a94376 | |||
| e8f71ab5ef | |||
| 2a846c9594 | |||
| 7695be0079 | |||
| e44a3eb39a | |||
| 03984bacf4 | |||
| 49bd8333e8 | |||
| e997e16cd1 | |||
| fce0389e23 | |||
| 3c4d535723 | |||
| 0db2c7d40c | |||
| 35dea59ce4 | |||
| 8810913f30 | |||
| 83ab71725f | |||
| dfe6dc1ee5 | |||
| ecf75f2f40 | |||
| 088d53adb4 | |||
| ecc7ddd96b | |||
| 121be15004 | |||
| 7405e20690 | |||
| 4620d864dd | |||
| 042b78f609 | |||
| d4030a87e2 | |||
| 7a98e210da | |||
| 38bdf0cd27 | |||
| c9f12aa804 | |||
| a853e94810 | |||
| f5b5f9a639 | |||
| 83cdd58ac8 | |||
| 52c187bf64 | |||
| db3c2b39b1 | |||
| 36cd069c6e | |||
| 5574f45cb1 | |||
| 1167b30b3f | |||
| 86b04b7b69 | |||
| 673e2b0dd2 | |||
| 35fede1382 | |||
| 564eb77a52 | |||
| fc075d110e | |||
| 8a89248b16 | |||
| 7f21443a1b | |||
| 4189f8b477 | |||
| f85752a956 | |||
| 3302bc46f8 | |||
| 4763877259 | |||
| d888432dca | |||
| 1115be581a | |||
| d83be7c33a | |||
| b3e888279e | |||
| 92472a6b62 | |||
| a257697775 | |||
| 001ccca5e6 | |||
| 62405560af | |||
| a8de8ac0da | |||
| 7fedb9c1d4 | |||
| eda7af764b | |||
| 1763334180 | |||
| ffd7b93dde | |||
| 4a95b804e9 | |||
| 113c101bed | |||
| ef05b82920 | |||
| 863ccd81d5 | |||
| f6602c2f68 | |||
| 309f5bebdb | |||
| 1bd9848d42 | |||
| 1ab3d89f10 | |||
| 31578c4170 | |||
| 2ed281f6a8 | |||
| eb15b26abe | |||
| 5f6df35ec4 | |||
| 65df3097e0 | |||
| c6b060a2eb | |||
| 94e3b54581 | |||
| 80a84945ef | |||
| 6873427aa7 | |||
| 3b1d14a64d | |||
| 75e2e2c3c7 | |||
| c4fe18ca79 | |||
| 38a41b6d01 | |||
| b8c51a2767 | |||
| 9cfece0d54 | |||
| 4920086225 | |||
| 83c020eea5 | |||
| 9f77d5ff1f | |||
| 9ddbf800b9 | |||
| 00ee0ab99c | |||
| 6a81de95ab | |||
| 2c749ebdb7 | |||
| 7e71001896 | |||
| caf84ffe7d | |||
| 448d6c6ebf | |||
| 05d8604012 | |||
| 46327d6e9b | |||
| 19f647ab8c | |||
| c71bfc7aed | |||
| 2f59701148 | |||
| 69bfb64dfd | |||
| 380c9e318c | |||
| e2216ddc32 | |||
| 5be36ce8ce | |||
| 2f8ff7eb41 | |||
| 8b64e2735e | |||
| 8299a617b9 | |||
| 4c988146bc | |||
| 9d9383aaeb | |||
| 435cb0c7d6 | |||
| 928a3c7184 | |||
| f52f83349c | |||
| c0039fb624 | |||
| b9c1aec62f | |||
| c02251672e | |||
| 9b17928143 | |||
| ae9abc8545 | |||
| d28c0396c9 | |||
| 6be811119e | |||
| 87da67165a | |||
| 6888137cb6 | |||
| b8c3f03ec0 | |||
| 92ede84ea3 | |||
| 8c03d55905 | |||
| dc98a7960a | |||
| 4d0f1729cb | |||
| c85e56aac9 | |||
| f2a18e5920 | |||
| 2561a60968 | |||
| 39bb751546 | |||
| be5542a7a7 | |||
| ec48c47886 | |||
| bce7d87151 | |||
| 87f1961c3e | |||
| 52f325db82 | |||
| 262a3130d4 | |||
| df1ed0a4dc | |||
| 483985b173 | |||
| b85a76f983 | |||
| 63770ba516 | |||
| f55c234eb4 | |||
| a73a819c82 | |||
| 82941caa00 | |||
| 4b9be5aaf8 | |||
| 819a13f22d | |||
| 808d1b6413 | |||
| 27c4f0e29d | |||
| 6a0c5ddf68 | |||
| b9efa91ecc | |||
| 3f7d3cbbd3 | |||
| c970a6052a | |||
| 396f04a3b9 | |||
| 7a71d7c3a8 | |||
| 774231db1e | |||
| e8788c7e3e | |||
| 1b7ccbce2a | |||
| e184860ca4 | |||
| a3594e7207 | |||
| 96f7d50abf | |||
| 2697aea15f | |||
| 284f6057cc | |||
| 68833e43aa | |||
| 2cdc406e71 | |||
| e8e722fb59 | |||
| 292a3a4895 | |||
| 5e9b07b56d | |||
| e2ea87fe4f | |||
| 8bba4f641f | |||
| bc499df6ce | |||
| 2eede7ad1c | |||
| 4345acd7b9 | |||
| 7b1cdb5efb | |||
| 69ef885b68 | |||
| 57c0d95007 | |||
| f136e27ccf | |||
| 99bcd3a845 | |||
| e26c161ae9 | |||
| 087641c508 | |||
| bab8a8300b | |||
| b6de2b0611 | |||
| 69b7624629 | |||
| 333dfd1577 | |||
| 067bbce1d3 | |||
| 48308aee26 | |||
| e0521fc1a1 | |||
| 08d7a38546 | |||
| a2bc1ded73 | |||
| eb9ee9fafa | |||
| f025d60b92 | |||
| 5aab947b6e | |||
| 6c86914709 | |||
| b332920b17 | |||
| 5fba926ab6 | |||
| a442c180f1 | |||
| 7417bd0ac1 | |||
| 73b24b6f7b | |||
| abccba22f1 | |||
| 4fa33dfece | |||
| 9516d9b822 | |||
| be11e73d2c | |||
| 4747ab67e6 | |||
| c245e20e3f | |||
| 185537d469 | |||
| 92f1f55957 | |||
| b4a2cdf028 | |||
| 8b741f3a46 | |||
| 1c37e7092e | |||
| f24f9a4ba4 | |||
| cf49a33ea4 | |||
| f368147f9d | |||
| cf90beec0a | |||
| fbc759077c | |||
| 0c6f5d7d29 | |||
| 1e0ef99b0c | |||
| 528578d138 | |||
| 950d0b5164 | |||
| bccd9e9f11 | |||
| bf06d74879 | |||
| 5b8e436c98 | |||
| 373763723e | |||
| 41a9db376d | |||
| 62d9600cdd | |||
| ef06bf4e74 | |||
| 6d2d10cffe | |||
| b3586560c1 | |||
| 61545fabfc | |||
| 0e85d950b3 | |||
| bc5d771a06 | |||
| fcb8aed03f | |||
| 4b9f1732f1 | |||
| 2e6046a655 | |||
| 32db1a1680 | |||
| 4fd368b1af | |||
| f094c46172 | |||
| 7f4da52cc5 | |||
| e9946937e7 | |||
| ca909483f1 | |||
| c75887c20d | |||
| dd094b5a17 | |||
| af83a152dc | |||
| aa7793a593 | |||
| bf80485cc3 | |||
| 8a8df90a46 | |||
| c8009dc67d | |||
| 5cbb5d4d70 | |||
| 30d249c3a7 | |||
| 34f74026f7 | |||
| 65553f55d7 | |||
| 43ff6d9711 | |||
| 4773ffc72c | |||
| 2dbad8de41 | |||
| 5e549da75f | |||
| 3db1b94465 | |||
| c5eefaa5f3 | |||
| 7a6ba43402 | |||
| 01a15cf0e7 | |||
| 0ebbe024f2 | |||
| 429833cae6 | |||
| 2d5943352f | |||
| 9fe69e2cc4 | |||
| cb633832a9 | |||
| 88a5b4efd3 | |||
| 1ef7e70950 | |||
| d45e6ec197 | |||
| c4b100ac0c | |||
| fd8823819f | |||
| 3256bf436a | |||
| 11db31a8ee | |||
| 41f8ac6b59 | |||
| 3d19d78461 | |||
| 1c435c0f47 | |||
| 2d4baec2b1 | |||
| 83517d05d0 | |||
| 2546f63824 | |||
| cd25a64ac0 | |||
| 78f28fda54 | |||
| 810da11bb5 | |||
| 5044b70a40 | |||
| ff28d2d47a | |||
| 3edb256298 | |||
| 2015a93823 | |||
| b904753a25 | |||
| 0e50fe4f6a | |||
| f8fd6da10b | |||
| 6d28ac6b2c | |||
| 5e62ae19a9 | |||
| b8cdef6d29 | |||
| c2822a6b23 | |||
| 086d7081f8 | |||
| db196321a6 | |||
| 75e57475ca | |||
| 58020ff0eb | |||
| edce3029a2 | |||
| 3eb5b8263b | |||
| 5e939ccb4b | |||
| da75840712 | |||
| 162e533393 | |||
| 2b9010c2a2 | |||
| 078d683f47 | |||
| 3f3341bb1d | |||
| 342369355d | |||
| 5de03f6bb1 | |||
| d41d674c4f | |||
| 5ac0b8e22e | |||
| 949432ce8b | |||
| 15c6d3449b | |||
| f243abbce3 | |||
| ba312f6c7c | |||
| 0759129c14 | |||
| 71362c953c | |||
| fec2ed5540 | |||
| 989eb37fb7 | |||
| bd81abe58d | |||
| fa01e9c566 | |||
| 431d726dc6 | |||
| 89b2a6500e | |||
| a0cc6419f4 | |||
| 5fa9460bf6 | |||
| 5d492689b7 | |||
| 4f260a4511 | |||
| 28273b9309 | |||
| c213724a47 | |||
| b6e6b015e8 | |||
| ab33d715a2 | |||
| 80f63d89d1 | |||
| 81ea35c726 | |||
| 186f909c96 | |||
| 1fd0b8730b | |||
| 1164ac3079 | |||
| b027cf110d | |||
| 98676746f3 | |||
| 64f93d5755 | |||
| 102cc72fd3 | |||
| 5e08a88219 | |||
| 3b65506c13 | |||
| f5ff84ab7b | |||
| 50061dae35 | |||
| 74c48d8132 | |||
| ff5529bbae | |||
| 082a8cfae3 | |||
| ba4105d4f0 | |||
| f548da8a8d | |||
| 4b3e192ca1 | |||
| c28cbfd582 | |||
| 1fb1801285 | |||
| a7547cd311 | |||
| e69ae513b9 | |||
| 8d3d0e7ae5 | |||
| 8d4a8cbbe5 | |||
| 409bf5f889 | |||
| 00831a06ea | |||
| e81fe79e2b | |||
| e34189a793 | |||
| cea5d9e3d1 | |||
| def2d033a2 | |||
| 7f8102a4cd | |||
| e57464dac8 | |||
| 2528fbe3b2 | |||
| 600399cc96 | |||
| c0d16eafb3 | |||
| a4cea65ace | |||
| 6e17a05419 | |||
| 2522a73111 |
@@ -0,0 +1,3 @@
|
||||
require:
|
||||
members: false
|
||||
|
||||
@@ -2,7 +2,7 @@ name: Build and Deploy Snapshot
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 6.2.x
|
||||
- main
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
|
||||
/**/framework-api-*-docs.zip::zip.type=docs
|
||||
/**/framework-api-*-schema.zip::zip.type=schema
|
||||
build-name: 'spring-framework-6.2.x'
|
||||
build-name: 'spring-framework-7.0.x'
|
||||
folder: 'deployment-repository'
|
||||
password: ${{ secrets.ARTIFACTORY_PASSWORD }}
|
||||
repository: 'libs-snapshot-local'
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 6.2.x
|
||||
schedule:
|
||||
- cron: '30 9 * * *'
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
name: Release Milestone
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v7.0.0-M[1-9]
|
||||
- v7.0.0-RC[1-9]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
build-and-stage-release:
|
||||
name: Build and Stage Release
|
||||
if: ${{ github.repository == 'spring-projects/spring-framework' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v4
|
||||
- name: Build and Publish
|
||||
id: build-and-publish
|
||||
uses: ./.github/actions/build
|
||||
with:
|
||||
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
|
||||
publish: true
|
||||
- name: Stage Release
|
||||
uses: spring-io/artifactory-deploy-action@26bbe925a75f4f863e1e529e85be2d0093cac116 # v0.0.1
|
||||
with:
|
||||
artifact-properties: |
|
||||
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
|
||||
/**/framework-api-*-docs.zip::zip.type=docs
|
||||
/**/framework-api-*-schema.zip::zip.type=schema
|
||||
build-name: ${{ format('spring-framework-{0}', steps.build-and-publish.outputs.version)}}
|
||||
folder: 'deployment-repository'
|
||||
password: ${{ secrets.ARTIFACTORY_PASSWORD }}
|
||||
repository: 'libs-staging-local'
|
||||
signing-key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
signing-passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||
uri: 'https://repo.spring.io'
|
||||
username: ${{ secrets.ARTIFACTORY_USERNAME }}
|
||||
outputs:
|
||||
version: ${{ steps.build-and-publish.outputs.version }}
|
||||
verify:
|
||||
name: Verify
|
||||
needs: build-and-stage-release
|
||||
uses: ./.github/workflows/verify.yml
|
||||
secrets:
|
||||
google-chat-webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }}
|
||||
repository-password: ${{ secrets.ARTIFACTORY_PASSWORD }}
|
||||
repository-username: ${{ secrets.ARTIFACTORY_USERNAME }}
|
||||
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
with:
|
||||
staging: true
|
||||
version: ${{ needs.build-and-stage-release.outputs.version }}
|
||||
sync-to-maven-central:
|
||||
name: Sync to Maven Central
|
||||
needs:
|
||||
- build-and-stage-release
|
||||
- verify
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- name: Sync to Maven Central
|
||||
uses: ./.github/actions/sync-to-maven-central
|
||||
with:
|
||||
jfrog-cli-config-token: ${{ secrets.JF_ARTIFACTORY_SPRING }}
|
||||
ossrh-s01-staging-profile: ${{ secrets.OSSRH_S01_STAGING_PROFILE }}
|
||||
ossrh-s01-token-password: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }}
|
||||
ossrh-s01-token-username: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }}
|
||||
spring-framework-version: ${{ needs.build-and-stage-release.outputs.version }}
|
||||
promote-release:
|
||||
name: Promote Release
|
||||
needs:
|
||||
- build-and-stage-release
|
||||
- sync-to-maven-central
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up JFrog CLI
|
||||
uses: jfrog/setup-jfrog-cli@dff217c085c17666e8849ebdbf29c8fe5e3995e6 # v4.5.2
|
||||
env:
|
||||
JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }}
|
||||
- name: Promote build
|
||||
run: jfrog rt build-promote ${{ format('spring-framework-{0}', needs.build-and-stage-release.outputs.version)}} ${{ github.run_number }} libs-milestone-local
|
||||
create-github-release:
|
||||
name: Create GitHub Release
|
||||
needs:
|
||||
- build-and-stage-release
|
||||
- promote-release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- name: Create GitHub Release
|
||||
uses: ./.github/actions/create-github-release
|
||||
with:
|
||||
milestone: ${{ needs.build-and-stage-release.outputs.version }}
|
||||
pre-release: true
|
||||
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
@@ -2,7 +2,7 @@ name: Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v6.2.[0-9]+
|
||||
- v7.0.[0-9]+
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
|
||||
@@ -54,3 +54,4 @@ atlassian-ide-plugin.xml
|
||||
cached-antora-playbook.yml
|
||||
|
||||
node_modules
|
||||
/.kotlin/
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# Enable auto-env through the sdkman_auto_env config
|
||||
# Add key=value pairs of SDKs to use below
|
||||
java=17.0.13-librca
|
||||
java=24-librca
|
||||
|
||||
+7
-26
@@ -1,14 +1,11 @@
|
||||
plugins {
|
||||
id 'io.freefair.aspectj' version '8.4' apply false
|
||||
id 'io.freefair.aspectj' version '8.13.1' apply false
|
||||
// kotlinVersion is managed in gradle.properties
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version "${kotlinVersion}" apply false
|
||||
id 'org.jetbrains.dokka' version '1.9.20'
|
||||
id 'com.github.ben-manes.versions' version '0.51.0'
|
||||
id 'com.github.bjornvester.xjc' version '1.8.2' apply false
|
||||
id 'de.undercouch.download' version '5.4.0'
|
||||
id 'io.github.goooler.shadow' version '8.1.8' apply false
|
||||
id 'me.champeau.jmh' version '0.7.2' apply false
|
||||
id 'me.champeau.mrjar' version '0.1.1'
|
||||
id "net.ltgt.errorprone" version "4.1.0" apply false
|
||||
}
|
||||
|
||||
@@ -24,13 +21,6 @@ configure(allprojects) { project ->
|
||||
group = "org.springframework"
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
url = "https://repo.spring.io/milestone"
|
||||
content {
|
||||
// Netty 5 optional support
|
||||
includeGroup 'io.projectreactor.netty'
|
||||
}
|
||||
}
|
||||
if (version.contains('-')) {
|
||||
maven { url = "https://repo.spring.io/milestone" }
|
||||
}
|
||||
@@ -64,7 +54,6 @@ 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 {
|
||||
@@ -80,36 +69,28 @@ configure([rootProject] + javaProjects) { project ->
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-suite-engine")
|
||||
testRuntimeOnly("org.apache.logging.log4j:log4j-core")
|
||||
testRuntimeOnly("org.apache.logging.log4j:log4j-jul")
|
||||
testRuntimeOnly("org.apache.logging.log4j:log4j-slf4j2-impl")
|
||||
// JSR-305 only used for non-required meta-annotations
|
||||
compileOnly("com.google.code.findbugs:jsr305")
|
||||
testCompileOnly("com.google.code.findbugs:jsr305")
|
||||
}
|
||||
|
||||
ext.javadocLinks = [
|
||||
"https://docs.oracle.com/en/java/javase/17/docs/api/",
|
||||
"https://jakarta.ee/specifications/platform/9/apidocs/",
|
||||
"https://jakarta.ee/specifications/platform/11/apidocs/",
|
||||
"https://docs.jboss.org/hibernate/orm/5.6/javadocs/",
|
||||
"https://eclipse.dev/aspectj/doc/latest/runtime-api/",
|
||||
"https://www.quartz-scheduler.org/api/2.3.0/",
|
||||
"https://fasterxml.github.io/jackson-core/javadoc/2.14/",
|
||||
"https://fasterxml.github.io/jackson-databind/javadoc/2.14/",
|
||||
"https://fasterxml.github.io/jackson-dataformat-xml/javadoc/2.14/",
|
||||
"https://hc.apache.org/httpcomponents-client-5.4.x/current/httpclient5/apidocs/",
|
||||
"https://projectreactor.io/docs/test/release/api/",
|
||||
"https://junit.org/junit4/javadoc/4.13.2/",
|
||||
// TODO Uncomment link to JUnit 5 docs once we execute Gradle with Java 18+.
|
||||
// See https://github.com/spring-projects/spring-framework/issues/27497
|
||||
//
|
||||
// "https://junit.org/junit5/docs/5.12.2/api/",
|
||||
"https://junit.org/junit5/docs/5.12.2/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/",
|
||||
// Previously there could be a split-package issue between JSR250 and JSR305 javax.annotation packages,
|
||||
// but since 6.0 JSR 250 annotations such as @Resource and @PostConstruct have been replaced by their
|
||||
// JakartaEE equivalents in the jakarta.annotation package.
|
||||
//"https://www.javadoc.io/doc/com.google.code.findbugs/jsr305/3.0.2/"
|
||||
//"https://www.javadoc.io/doc/com.google.code.findbugs/jsr305/3.0.2/",
|
||||
"https://jspecify.dev/docs/api/",
|
||||
"https://www.javadoc.io/doc/tools.jackson.core/jackson-databind/3.0.0-rc4/"
|
||||
|
||||
] as String[]
|
||||
}
|
||||
|
||||
|
||||
+31
-1
@@ -9,7 +9,18 @@ The `org.springframework.build.conventions` plugin applies all conventions to th
|
||||
|
||||
* Configuring the Java compiler, see `JavaConventions`
|
||||
* Configuring the Kotlin compiler, see `KotlinConventions`
|
||||
* Configuring testing in the build with `TestConventions`
|
||||
* Configuring testing in the build with `TestConventions`
|
||||
* Configuring the ArchUnit rules for the project, see `org.springframework.build.architecture.ArchitectureRules`
|
||||
|
||||
This plugin also provides a DSL extension to optionally enable Java preview features for
|
||||
compiling and testing sources in a module. This can be applied with the following in a
|
||||
module build file:
|
||||
|
||||
```groovy
|
||||
springFramework {
|
||||
enableJavaPreviewFeatures = true
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Build Plugins
|
||||
@@ -22,6 +33,25 @@ but doesn't affect the classpath of dependent projects.
|
||||
This plugin does not provide a `provided` configuration, as the native `compileOnly` and `testCompileOnly`
|
||||
configurations are preferred.
|
||||
|
||||
### MultiRelease Jar
|
||||
|
||||
The `org.springframework.build.multiReleaseJar` plugin configures the project with MultiRelease JAR support.
|
||||
It creates a new SourceSet and dedicated tasks for each Java variant considered.
|
||||
This can be configured with the DSL, by setting a list of Java variants to configure:
|
||||
|
||||
```groovy
|
||||
plugins {
|
||||
id 'org.springframework.build.multiReleaseJar'
|
||||
}
|
||||
|
||||
multiRelease {
|
||||
releaseVersions 21, 24
|
||||
}
|
||||
```
|
||||
|
||||
Note, Java classes will be compiled with the toolchain pre-configured by the project, assuming that its
|
||||
Java language version is equal or higher than all variants we consider. Each compilation task will only
|
||||
set the "-release" compilation option accordingly to produce the expected bytecode version.
|
||||
|
||||
### RuntimeHints Java Agent
|
||||
|
||||
|
||||
+21
-2
@@ -20,14 +20,23 @@ ext {
|
||||
dependencies {
|
||||
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 "org.gradle:test-retry-gradle-plugin:1.5.6"
|
||||
implementation "com.tngtech.archunit:archunit:1.4.0"
|
||||
implementation "org.gradle:test-retry-gradle-plugin:1.6.2"
|
||||
implementation "io.spring.javaformat:spring-javaformat-gradle-plugin:${javaFormatVersion}"
|
||||
implementation "io.spring.nohttp:nohttp-gradle:0.0.11"
|
||||
|
||||
testImplementation("org.assertj:assertj-core:${assertjVersion}")
|
||||
testImplementation(platform("org.junit:junit-bom:${junitVersion}"))
|
||||
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
plugins {
|
||||
architecturePlugin {
|
||||
id = "org.springframework.architecture"
|
||||
implementationClass = "org.springframework.build.architecture.ArchitecturePlugin"
|
||||
}
|
||||
conventionsPlugin {
|
||||
id = "org.springframework.build.conventions"
|
||||
implementationClass = "org.springframework.build.ConventionsPlugin"
|
||||
@@ -36,6 +45,10 @@ gradlePlugin {
|
||||
id = "org.springframework.build.localdev"
|
||||
implementationClass = "org.springframework.build.dev.LocalDevelopmentPlugin"
|
||||
}
|
||||
multiReleasePlugin {
|
||||
id = "org.springframework.build.multiReleaseJar"
|
||||
implementationClass = "org.springframework.build.multirelease.MultiReleaseJarPlugin"
|
||||
}
|
||||
optionalDependenciesPlugin {
|
||||
id = "org.springframework.build.optional-dependencies"
|
||||
implementationClass = "org.springframework.build.optional.OptionalDependenciesPlugin"
|
||||
@@ -46,3 +59,9 @@ gradlePlugin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
jar.dependsOn check
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE module PUBLIC "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" "https://checkstyle.org/dtds/configuration_1_3.dtd">
|
||||
<module name="com.puppycrawl.tools.checkstyle.Checker">
|
||||
<module name="Checker">
|
||||
|
||||
<!-- Root Checks -->
|
||||
<module name="io.spring.javaformat.checkstyle.check.SpringHeaderCheck">
|
||||
@@ -12,16 +12,15 @@
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.NewlineAtEndOfFileCheck"/>
|
||||
|
||||
<!-- TreeWalker Checks -->
|
||||
<module name="com.puppycrawl.tools.checkstyle.TreeWalker">
|
||||
<module name="TreeWalker">
|
||||
|
||||
<!-- Imports -->
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.imports.UnusedImportsCheck">
|
||||
<property name="processJavadoc" value="true"/>
|
||||
</module>
|
||||
|
||||
<module name="AvoidStarImport"/>
|
||||
<module name="UnusedImports"/>
|
||||
<module name="RedundantImport"/>
|
||||
<!-- Modifiers -->
|
||||
<module name="com.puppycrawl.tools.checkstyle.checks.modifier.ModifierOrderCheck"/>
|
||||
|
||||
</module>
|
||||
|
||||
</module>
|
||||
</module>
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
org.gradle.caching=true
|
||||
assertjVersion=3.27.3
|
||||
javaFormatVersion=0.0.43
|
||||
junitVersion=5.12.2
|
||||
|
||||
@@ -63,8 +63,8 @@ public class CheckstyleConventions {
|
||||
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/**", "**/node_modules/**", "buildSrc/build/**");
|
||||
noHttp.getSource().exclude("**/test-output/**", "**/.settings/**", "**/.classpath",
|
||||
"**/.project", "**/.gradle/**", "**/node_modules/**", "**/spring-jcl/**", "buildSrc/build/**");
|
||||
List<String> buildFolders = List.of("bin", "build", "out");
|
||||
project.allprojects(subproject -> {
|
||||
Path rootPath = project.getRootDir().toPath();
|
||||
|
||||
@@ -21,12 +21,15 @@ import org.gradle.api.Project;
|
||||
import org.gradle.api.plugins.JavaBasePlugin;
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinBasePlugin;
|
||||
|
||||
import org.springframework.build.architecture.ArchitecturePlugin;
|
||||
|
||||
/**
|
||||
* Plugin to apply conventions to projects that are part of Spring Framework's build.
|
||||
* Conventions are applied in response to various plugins being applied.
|
||||
*
|
||||
* <p>When the {@link JavaBasePlugin} is applied, the conventions in {@link CheckstyleConventions},
|
||||
* {@link TestConventions} and {@link JavaConventions} are applied.
|
||||
* The {@link ArchitecturePlugin} plugin is also applied.
|
||||
* When the {@link KotlinBasePlugin} is applied, the conventions in {@link KotlinConventions}
|
||||
* are applied.
|
||||
*
|
||||
@@ -36,6 +39,8 @@ public class ConventionsPlugin implements Plugin<Project> {
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
project.getExtensions().create("springFramework", SpringFrameworkExtension.class);
|
||||
new ArchitecturePlugin().apply(project);
|
||||
new CheckstyleConventions().apply(project);
|
||||
new JavaConventions().apply(project);
|
||||
new KotlinConventions().apply(project);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,7 +17,6 @@
|
||||
package org.springframework.build;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.gradle.api.Plugin;
|
||||
@@ -42,8 +41,21 @@ public class JavaConventions {
|
||||
|
||||
private static final List<String> TEST_COMPILER_ARGS;
|
||||
|
||||
/**
|
||||
* The Java version we should use as the JVM baseline for building the project.
|
||||
* <p>NOTE: If you update this value, you should also update the value used in
|
||||
* the {@code javadoc} task in {@code framework-api.gradle}.
|
||||
*/
|
||||
private static final JavaLanguageVersion DEFAULT_LANGUAGE_VERSION = JavaLanguageVersion.of(24);
|
||||
|
||||
/**
|
||||
* The Java version we should use as the baseline for the compiled bytecode
|
||||
* (the "-release" compiler argument).
|
||||
*/
|
||||
private static final JavaLanguageVersion DEFAULT_RELEASE_VERSION = JavaLanguageVersion.of(17);
|
||||
|
||||
static {
|
||||
List<String> commonCompilerArgs = Arrays.asList(
|
||||
List<String> commonCompilerArgs = List.of(
|
||||
"-Xlint:serial", "-Xlint:cast", "-Xlint:classfile", "-Xlint:dep-ann",
|
||||
"-Xlint:divzero", "-Xlint:empty", "-Xlint:finally", "-Xlint:overrides",
|
||||
"-Xlint:path", "-Xlint:processing", "-Xlint:static", "-Xlint:try", "-Xlint:-options",
|
||||
@@ -51,43 +63,75 @@ public class JavaConventions {
|
||||
);
|
||||
COMPILER_ARGS = new ArrayList<>();
|
||||
COMPILER_ARGS.addAll(commonCompilerArgs);
|
||||
COMPILER_ARGS.addAll(Arrays.asList(
|
||||
COMPILER_ARGS.addAll(List.of(
|
||||
"-Xlint:varargs", "-Xlint:fallthrough", "-Xlint:rawtypes", "-Xlint:deprecation",
|
||||
"-Xlint:unchecked", "-Werror"
|
||||
));
|
||||
TEST_COMPILER_ARGS = new ArrayList<>();
|
||||
TEST_COMPILER_ARGS.addAll(commonCompilerArgs);
|
||||
TEST_COMPILER_ARGS.addAll(Arrays.asList("-Xlint:-varargs", "-Xlint:-fallthrough", "-Xlint:-rawtypes",
|
||||
TEST_COMPILER_ARGS.addAll(List.of("-Xlint:-varargs", "-Xlint:-fallthrough", "-Xlint:-rawtypes",
|
||||
"-Xlint:-deprecation", "-Xlint:-unchecked"));
|
||||
}
|
||||
|
||||
public void apply(Project project) {
|
||||
project.getPlugins().withType(JavaBasePlugin.class, javaPlugin -> applyJavaCompileConventions(project));
|
||||
project.getPlugins().withType(JavaBasePlugin.class, javaPlugin -> {
|
||||
applyToolchainConventions(project);
|
||||
applyJavaCompileConventions(project);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the common Java compiler options for main sources, test fixture sources, and
|
||||
* Configure the Toolchain support for the project.
|
||||
* @param project the current project
|
||||
*/
|
||||
private static void applyToolchainConventions(Project project) {
|
||||
project.getExtensions().getByType(JavaPluginExtension.class).toolchain(toolchain -> {
|
||||
toolchain.getVendor().set(JvmVendorSpec.BELLSOFT);
|
||||
toolchain.getLanguageVersion().set(DEFAULT_LANGUAGE_VERSION);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the common Java compiler options for main sources, test fixture sources, and
|
||||
* test sources.
|
||||
* @param project the current project
|
||||
*/
|
||||
private void applyJavaCompileConventions(Project project) {
|
||||
project.getExtensions().getByType(JavaPluginExtension.class).toolchain(toolchain -> {
|
||||
toolchain.getVendor().set(JvmVendorSpec.BELLSOFT);
|
||||
toolchain.getLanguageVersion().set(JavaLanguageVersion.of(17));
|
||||
project.afterEvaluate(p -> {
|
||||
p.getTasks().withType(JavaCompile.class)
|
||||
.matching(compileTask -> compileTask.getName().startsWith(JavaPlugin.COMPILE_JAVA_TASK_NAME))
|
||||
.forEach(compileTask -> {
|
||||
compileTask.getOptions().setCompilerArgs(COMPILER_ARGS);
|
||||
compileTask.getOptions().setEncoding("UTF-8");
|
||||
setJavaRelease(compileTask);
|
||||
});
|
||||
p.getTasks().withType(JavaCompile.class)
|
||||
.matching(compileTask -> compileTask.getName().startsWith(JavaPlugin.COMPILE_TEST_JAVA_TASK_NAME)
|
||||
|| compileTask.getName().equals("compileTestFixturesJava"))
|
||||
.forEach(compileTask -> {
|
||||
compileTask.getOptions().setCompilerArgs(TEST_COMPILER_ARGS);
|
||||
compileTask.getOptions().setEncoding("UTF-8");
|
||||
setJavaRelease(compileTask);
|
||||
});
|
||||
|
||||
});
|
||||
project.getTasks().withType(JavaCompile.class)
|
||||
.matching(compileTask -> compileTask.getName().equals(JavaPlugin.COMPILE_JAVA_TASK_NAME))
|
||||
.forEach(compileTask -> {
|
||||
compileTask.getOptions().setCompilerArgs(COMPILER_ARGS);
|
||||
compileTask.getOptions().setEncoding("UTF-8");
|
||||
});
|
||||
project.getTasks().withType(JavaCompile.class)
|
||||
.matching(compileTask -> compileTask.getName().equals(JavaPlugin.COMPILE_TEST_JAVA_TASK_NAME)
|
||||
|| compileTask.getName().equals("compileTestFixturesJava"))
|
||||
.forEach(compileTask -> {
|
||||
compileTask.getOptions().setCompilerArgs(TEST_COMPILER_ARGS);
|
||||
compileTask.getOptions().setEncoding("UTF-8");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* We should pick the {@link #DEFAULT_RELEASE_VERSION} for all compiled classes,
|
||||
* unless the current task is compiling multi-release JAR code with a higher version.
|
||||
*/
|
||||
private void setJavaRelease(JavaCompile task) {
|
||||
int defaultVersion = DEFAULT_RELEASE_VERSION.asInt();
|
||||
int releaseVersion = defaultVersion;
|
||||
int compilerVersion = task.getJavaCompiler().get().getMetadata().getLanguageVersion().asInt();
|
||||
for (int version = defaultVersion ; version <= compilerVersion ; version++) {
|
||||
if (task.getName().contains("Java" + version)) {
|
||||
releaseVersion = version;
|
||||
break;
|
||||
}
|
||||
}
|
||||
task.getOptions().getRelease().set(releaseVersion);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2025 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,15 +16,14 @@
|
||||
|
||||
package org.springframework.build;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.gradle.api.Project;
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions;
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget;
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion;
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile;
|
||||
|
||||
/**
|
||||
* @author Brian Clozel
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
public class KotlinConventions {
|
||||
|
||||
@@ -34,15 +33,19 @@ public class KotlinConventions {
|
||||
}
|
||||
|
||||
private void configure(KotlinCompile compile) {
|
||||
KotlinJvmOptions kotlinOptions = compile.getKotlinOptions();
|
||||
kotlinOptions.setApiVersion("1.7");
|
||||
kotlinOptions.setLanguageVersion("1.7");
|
||||
kotlinOptions.setJvmTarget("17");
|
||||
kotlinOptions.setJavaParameters(true);
|
||||
kotlinOptions.setAllWarningsAsErrors(true);
|
||||
List<String> freeCompilerArgs = new ArrayList<>(compile.getKotlinOptions().getFreeCompilerArgs());
|
||||
freeCompilerArgs.addAll(List.of("-Xsuppress-version-warnings", "-Xjsr305=strict", "-opt-in=kotlin.RequiresOptIn"));
|
||||
compile.getKotlinOptions().setFreeCompilerArgs(freeCompilerArgs);
|
||||
compile.compilerOptions(options -> {
|
||||
options.getApiVersion().set(KotlinVersion.KOTLIN_2_1);
|
||||
options.getLanguageVersion().set(KotlinVersion.KOTLIN_2_1);
|
||||
options.getJvmTarget().set(JvmTarget.JVM_17);
|
||||
options.getJavaParameters().set(true);
|
||||
options.getAllWarningsAsErrors().set(true);
|
||||
options.getFreeCompilerArgs().addAll(
|
||||
"-Xsuppress-version-warnings",
|
||||
"-Xjsr305=strict", // For dependencies using JSR 305
|
||||
"-opt-in=kotlin.RequiresOptIn",
|
||||
"-Xjdk-release=17" // Needed due to https://youtrack.jetbrains.com/issue/KT-49746
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.build;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.provider.Property;
|
||||
import org.gradle.api.tasks.compile.JavaCompile;
|
||||
import org.gradle.api.tasks.testing.Test;
|
||||
import org.gradle.process.CommandLineArgumentProvider;
|
||||
|
||||
public class SpringFrameworkExtension {
|
||||
|
||||
private final Property<Boolean> enableJavaPreviewFeatures;
|
||||
|
||||
public SpringFrameworkExtension(Project project) {
|
||||
this.enableJavaPreviewFeatures = project.getObjects().property(Boolean.class);
|
||||
project.getTasks().withType(JavaCompile.class).configureEach(javaCompile ->
|
||||
javaCompile.getOptions().getCompilerArgumentProviders().add(asArgumentProvider()));
|
||||
project.getTasks().withType(Test.class).configureEach(test ->
|
||||
test.getJvmArgumentProviders().add(asArgumentProvider()));
|
||||
|
||||
}
|
||||
|
||||
public Property<Boolean> getEnableJavaPreviewFeatures() {
|
||||
return this.enableJavaPreviewFeatures;
|
||||
}
|
||||
|
||||
private CommandLineArgumentProvider asArgumentProvider() {
|
||||
return () -> {
|
||||
if (getEnableJavaPreviewFeatures().getOrElse(false)) {
|
||||
return List.of("--enable-preview");
|
||||
}
|
||||
return Collections.emptyList();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -62,10 +62,7 @@ class TestConventions {
|
||||
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"
|
||||
"io.netty.leakDetection.level", "paranoid"
|
||||
));
|
||||
if (project.hasProperty("testGroups")) {
|
||||
test.systemProperty("testGroups", project.getProperties().get("testGroups"));
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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.architecture;
|
||||
|
||||
import com.tngtech.archunit.core.domain.JavaClasses;
|
||||
import com.tngtech.archunit.core.importer.ClassFileImporter;
|
||||
import com.tngtech.archunit.lang.ArchRule;
|
||||
import com.tngtech.archunit.lang.EvaluationResult;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.List;
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.GradleException;
|
||||
import org.gradle.api.Task;
|
||||
import org.gradle.api.file.DirectoryProperty;
|
||||
import org.gradle.api.file.FileCollection;
|
||||
import org.gradle.api.file.FileTree;
|
||||
import org.gradle.api.provider.ListProperty;
|
||||
import org.gradle.api.provider.Property;
|
||||
import org.gradle.api.tasks.IgnoreEmptyDirectories;
|
||||
import org.gradle.api.tasks.Input;
|
||||
import org.gradle.api.tasks.InputFiles;
|
||||
import org.gradle.api.tasks.Internal;
|
||||
import org.gradle.api.tasks.Optional;
|
||||
import org.gradle.api.tasks.OutputDirectory;
|
||||
import org.gradle.api.tasks.PathSensitive;
|
||||
import org.gradle.api.tasks.PathSensitivity;
|
||||
import org.gradle.api.tasks.SkipWhenEmpty;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
|
||||
import static org.springframework.build.architecture.ArchitectureRules.allPackagesShouldBeFreeOfTangles;
|
||||
import static org.springframework.build.architecture.ArchitectureRules.classesShouldNotImportForbiddenTypes;
|
||||
import static org.springframework.build.architecture.ArchitectureRules.javaClassesShouldNotImportKotlinAnnotations;
|
||||
import static org.springframework.build.architecture.ArchitectureRules.noClassesShouldCallStringToLowerCaseWithoutLocale;
|
||||
import static org.springframework.build.architecture.ArchitectureRules.noClassesShouldCallStringToUpperCaseWithoutLocale;
|
||||
import static org.springframework.build.architecture.ArchitectureRules.packageInfoShouldBeNullMarked;
|
||||
|
||||
/**
|
||||
* {@link Task} that checks for architecture problems.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
public abstract class ArchitectureCheck extends DefaultTask {
|
||||
|
||||
private FileCollection classes;
|
||||
|
||||
public ArchitectureCheck() {
|
||||
getOutputDirectory().convention(getProject().getLayout().getBuildDirectory().dir(getName()));
|
||||
getProhibitObjectsRequireNonNull().convention(true);
|
||||
getRules().addAll(packageInfoShouldBeNullMarked(),
|
||||
classesShouldNotImportForbiddenTypes(),
|
||||
javaClassesShouldNotImportKotlinAnnotations(),
|
||||
allPackagesShouldBeFreeOfTangles(),
|
||||
noClassesShouldCallStringToLowerCaseWithoutLocale(),
|
||||
noClassesShouldCallStringToUpperCaseWithoutLocale());
|
||||
getRuleDescriptions().set(getRules().map((rules) -> rules.stream().map(ArchRule::getDescription).toList()));
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
void checkArchitecture() throws IOException {
|
||||
JavaClasses javaClasses = new ClassFileImporter()
|
||||
.importPaths(this.classes.getFiles().stream().map(File::toPath).toList());
|
||||
List<EvaluationResult> violations = getRules().get()
|
||||
.stream()
|
||||
.map((rule) -> rule.evaluate(javaClasses))
|
||||
.filter(EvaluationResult::hasViolation)
|
||||
.toList();
|
||||
File outputFile = getOutputDirectory().file("failure-report.txt").get().getAsFile();
|
||||
outputFile.getParentFile().mkdirs();
|
||||
if (!violations.isEmpty()) {
|
||||
StringBuilder report = new StringBuilder();
|
||||
for (EvaluationResult violation : violations) {
|
||||
report.append(violation.getFailureReport());
|
||||
report.append(String.format("%n"));
|
||||
}
|
||||
Files.writeString(outputFile.toPath(), report.toString(), StandardOpenOption.CREATE,
|
||||
StandardOpenOption.TRUNCATE_EXISTING);
|
||||
throw new GradleException("Architecture check failed. See '" + outputFile + "' for details.");
|
||||
}
|
||||
else {
|
||||
outputFile.createNewFile();
|
||||
}
|
||||
}
|
||||
|
||||
public void setClasses(FileCollection classes) {
|
||||
this.classes = classes;
|
||||
}
|
||||
|
||||
@Internal
|
||||
public FileCollection getClasses() {
|
||||
return this.classes;
|
||||
}
|
||||
|
||||
@InputFiles
|
||||
@SkipWhenEmpty
|
||||
@IgnoreEmptyDirectories
|
||||
@PathSensitive(PathSensitivity.RELATIVE)
|
||||
final FileTree getInputClasses() {
|
||||
return this.classes.getAsFileTree();
|
||||
}
|
||||
|
||||
@Optional
|
||||
@InputFiles
|
||||
@PathSensitive(PathSensitivity.RELATIVE)
|
||||
public abstract DirectoryProperty getResourcesDirectory();
|
||||
|
||||
@OutputDirectory
|
||||
public abstract DirectoryProperty getOutputDirectory();
|
||||
|
||||
@Internal
|
||||
public abstract ListProperty<ArchRule> getRules();
|
||||
|
||||
@Internal
|
||||
public abstract Property<Boolean> getProhibitObjectsRequireNonNull();
|
||||
|
||||
@Input
|
||||
// The rules themselves can't be an input as they aren't serializable so we use
|
||||
// their descriptions instead
|
||||
abstract ListProperty<String> getRuleDescriptions();
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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.architecture;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.Task;
|
||||
import org.gradle.api.plugins.JavaPlugin;
|
||||
import org.gradle.api.plugins.JavaPluginExtension;
|
||||
import org.gradle.api.tasks.SourceSet;
|
||||
import org.gradle.api.tasks.TaskProvider;
|
||||
import org.gradle.language.base.plugins.LifecycleBasePlugin;
|
||||
|
||||
/**
|
||||
* {@link Plugin} for verifying a project's architecture.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class ArchitecturePlugin implements Plugin<Project> {
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> registerTasks(project));
|
||||
}
|
||||
|
||||
private void registerTasks(Project project) {
|
||||
JavaPluginExtension javaPluginExtension = project.getExtensions().getByType(JavaPluginExtension.class);
|
||||
List<TaskProvider<ArchitectureCheck>> architectureChecks = new ArrayList<>();
|
||||
for (SourceSet sourceSet : javaPluginExtension.getSourceSets()) {
|
||||
if (sourceSet.getName().contains("test")) {
|
||||
// skip test source sets.
|
||||
continue;
|
||||
}
|
||||
TaskProvider<ArchitectureCheck> checkArchitecture = project.getTasks()
|
||||
.register(taskName(sourceSet), ArchitectureCheck.class,
|
||||
(task) -> {
|
||||
task.setClasses(sourceSet.getOutput().getClassesDirs());
|
||||
task.getResourcesDirectory().set(sourceSet.getOutput().getResourcesDir());
|
||||
task.dependsOn(sourceSet.getProcessResourcesTaskName());
|
||||
task.setDescription("Checks the architecture of the classes of the " + sourceSet.getName()
|
||||
+ " source set.");
|
||||
task.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP);
|
||||
});
|
||||
architectureChecks.add(checkArchitecture);
|
||||
}
|
||||
if (!architectureChecks.isEmpty()) {
|
||||
TaskProvider<Task> checkTask = project.getTasks().named(LifecycleBasePlugin.CHECK_TASK_NAME);
|
||||
checkTask.configure((check) -> check.dependsOn(architectureChecks));
|
||||
}
|
||||
}
|
||||
|
||||
private static String taskName(SourceSet sourceSet) {
|
||||
return "checkArchitecture"
|
||||
+ sourceSet.getName().substring(0, 1).toUpperCase()
|
||||
+ sourceSet.getName().substring(1);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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.architecture;
|
||||
|
||||
import com.tngtech.archunit.base.DescribedPredicate;
|
||||
import com.tngtech.archunit.core.domain.JavaClass;
|
||||
import com.tngtech.archunit.lang.ArchRule;
|
||||
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
|
||||
import com.tngtech.archunit.library.dependencies.SliceAssignment;
|
||||
import com.tngtech.archunit.library.dependencies.SliceIdentifier;
|
||||
import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition;
|
||||
import java.util.List;
|
||||
|
||||
abstract class ArchitectureRules {
|
||||
|
||||
static ArchRule allPackagesShouldBeFreeOfTangles() {
|
||||
return SlicesRuleDefinition.slices()
|
||||
.assignedFrom(new SpringSlices()).should().beFreeOfCycles();
|
||||
}
|
||||
|
||||
static ArchRule noClassesShouldCallStringToLowerCaseWithoutLocale() {
|
||||
return ArchRuleDefinition.noClasses()
|
||||
.should()
|
||||
.callMethod(String.class, "toLowerCase")
|
||||
.because("String.toLowerCase(Locale.ROOT) should be used instead");
|
||||
}
|
||||
|
||||
static ArchRule noClassesShouldCallStringToUpperCaseWithoutLocale() {
|
||||
return ArchRuleDefinition.noClasses()
|
||||
.should()
|
||||
.callMethod(String.class, "toUpperCase")
|
||||
.because("String.toUpperCase(Locale.ROOT) should be used instead");
|
||||
}
|
||||
|
||||
static ArchRule packageInfoShouldBeNullMarked() {
|
||||
return ArchRuleDefinition.classes()
|
||||
.that().haveSimpleName("package-info")
|
||||
.should().beAnnotatedWith("org.jspecify.annotations.NullMarked")
|
||||
.allowEmptyShould(true);
|
||||
}
|
||||
|
||||
static ArchRule classesShouldNotImportForbiddenTypes() {
|
||||
return ArchRuleDefinition.noClasses()
|
||||
.should().dependOnClassesThat()
|
||||
.haveFullyQualifiedName("reactor.core.support.Assert")
|
||||
.orShould().dependOnClassesThat()
|
||||
.haveFullyQualifiedName("org.slf4j.LoggerFactory")
|
||||
.orShould().dependOnClassesThat()
|
||||
.haveFullyQualifiedName("org.springframework.lang.NonNull")
|
||||
.orShould().dependOnClassesThat()
|
||||
.haveFullyQualifiedName("org.springframework.lang.Nullable");
|
||||
}
|
||||
|
||||
static ArchRule javaClassesShouldNotImportKotlinAnnotations() {
|
||||
return ArchRuleDefinition.noClasses()
|
||||
.that(new DescribedPredicate<JavaClass>("is not a Kotlin class") {
|
||||
@Override
|
||||
public boolean test(JavaClass javaClass) {
|
||||
return javaClass.getSourceCodeLocation()
|
||||
.getSourceFileName().endsWith(".java");
|
||||
}
|
||||
}
|
||||
)
|
||||
.should().dependOnClassesThat()
|
||||
.resideInAnyPackage("org.jetbrains.annotations..")
|
||||
.allowEmptyShould(true);
|
||||
}
|
||||
|
||||
static class SpringSlices implements SliceAssignment {
|
||||
|
||||
private final List<String> ignoredPackages = List.of("org.springframework.asm",
|
||||
"org.springframework.cglib",
|
||||
"org.springframework.javapoet",
|
||||
"org.springframework.objenesis");
|
||||
|
||||
@Override
|
||||
public SliceIdentifier getIdentifierOf(JavaClass javaClass) {
|
||||
|
||||
String packageName = javaClass.getPackageName();
|
||||
for (String ignoredPackage : ignoredPackages) {
|
||||
if (packageName.startsWith(ignoredPackage)) {
|
||||
return SliceIdentifier.ignore();
|
||||
}
|
||||
}
|
||||
return SliceIdentifier.of("spring framework");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Spring Framework Slices";
|
||||
}
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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.multirelease;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import org.gradle.api.artifacts.Configuration;
|
||||
import org.gradle.api.artifacts.ConfigurationContainer;
|
||||
import org.gradle.api.artifacts.dsl.DependencyHandler;
|
||||
import org.gradle.api.attributes.LibraryElements;
|
||||
import org.gradle.api.file.ConfigurableFileCollection;
|
||||
import org.gradle.api.file.FileCollection;
|
||||
import org.gradle.api.java.archives.Attributes;
|
||||
import org.gradle.api.model.ObjectFactory;
|
||||
import org.gradle.api.tasks.SourceSet;
|
||||
import org.gradle.api.tasks.SourceSetContainer;
|
||||
import org.gradle.api.tasks.TaskContainer;
|
||||
import org.gradle.api.tasks.TaskProvider;
|
||||
import org.gradle.api.tasks.bundling.Jar;
|
||||
import org.gradle.api.tasks.compile.JavaCompile;
|
||||
import org.gradle.api.tasks.testing.Test;
|
||||
import org.gradle.language.base.plugins.LifecycleBasePlugin;
|
||||
|
||||
/**
|
||||
* @author Cedric Champeau
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public abstract class MultiReleaseExtension {
|
||||
private final TaskContainer tasks;
|
||||
private final SourceSetContainer sourceSets;
|
||||
private final DependencyHandler dependencies;
|
||||
private final ObjectFactory objects;
|
||||
private final ConfigurationContainer configurations;
|
||||
|
||||
@Inject
|
||||
public MultiReleaseExtension(SourceSetContainer sourceSets,
|
||||
ConfigurationContainer configurations,
|
||||
TaskContainer tasks,
|
||||
DependencyHandler dependencies,
|
||||
ObjectFactory objectFactory) {
|
||||
this.sourceSets = sourceSets;
|
||||
this.configurations = configurations;
|
||||
this.tasks = tasks;
|
||||
this.dependencies = dependencies;
|
||||
this.objects = objectFactory;
|
||||
}
|
||||
|
||||
public void releaseVersions(int... javaVersions) {
|
||||
releaseVersions("src/main/", "src/test/", javaVersions);
|
||||
}
|
||||
|
||||
private void releaseVersions(String mainSourceDirectory, String testSourceDirectory, int... javaVersions) {
|
||||
for (int javaVersion : javaVersions) {
|
||||
addLanguageVersion(javaVersion, mainSourceDirectory, testSourceDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
private void addLanguageVersion(int javaVersion, String mainSourceDirectory, String testSourceDirectory) {
|
||||
String javaN = "java" + javaVersion;
|
||||
|
||||
SourceSet langSourceSet = sourceSets.create(javaN, srcSet -> srcSet.getJava().srcDir(mainSourceDirectory + javaN));
|
||||
SourceSet testSourceSet = sourceSets.create(javaN + "Test", srcSet -> srcSet.getJava().srcDir(testSourceDirectory + javaN));
|
||||
SourceSet sharedSourceSet = sourceSets.findByName(SourceSet.MAIN_SOURCE_SET_NAME);
|
||||
SourceSet sharedTestSourceSet = sourceSets.findByName(SourceSet.TEST_SOURCE_SET_NAME);
|
||||
|
||||
FileCollection mainClasses = objects.fileCollection().from(sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME).getOutput().getClassesDirs());
|
||||
dependencies.add(javaN + "Implementation", mainClasses);
|
||||
|
||||
tasks.named(langSourceSet.getCompileJavaTaskName(), JavaCompile.class, task ->
|
||||
task.getOptions().getRelease().set(javaVersion)
|
||||
);
|
||||
tasks.named(testSourceSet.getCompileJavaTaskName(), JavaCompile.class, task ->
|
||||
task.getOptions().getRelease().set(javaVersion)
|
||||
);
|
||||
|
||||
TaskProvider<Test> testTask = createTestTask(javaVersion, testSourceSet, sharedTestSourceSet, langSourceSet, sharedSourceSet);
|
||||
tasks.named("check", task -> task.dependsOn(testTask));
|
||||
|
||||
configureMultiReleaseJar(javaVersion, langSourceSet);
|
||||
}
|
||||
|
||||
private TaskProvider<Test> createTestTask(int javaVersion, SourceSet testSourceSet, SourceSet sharedTestSourceSet, SourceSet langSourceSet, SourceSet sharedSourceSet) {
|
||||
Configuration testImplementation = configurations.getByName(testSourceSet.getImplementationConfigurationName());
|
||||
testImplementation.extendsFrom(configurations.getByName(sharedTestSourceSet.getImplementationConfigurationName()));
|
||||
Configuration testCompileOnly = configurations.getByName(testSourceSet.getCompileOnlyConfigurationName());
|
||||
testCompileOnly.extendsFrom(configurations.getByName(sharedTestSourceSet.getCompileOnlyConfigurationName()));
|
||||
testCompileOnly.getDependencies().add(dependencies.create(langSourceSet.getOutput().getClassesDirs()));
|
||||
testCompileOnly.getDependencies().add(dependencies.create(sharedSourceSet.getOutput().getClassesDirs()));
|
||||
|
||||
Configuration testRuntimeClasspath = configurations.getByName(testSourceSet.getRuntimeClasspathConfigurationName());
|
||||
// so here's the deal. MRjars are JARs! Which means that to execute tests, we need
|
||||
// the JAR on classpath, not just classes + resources as Gradle usually does
|
||||
testRuntimeClasspath.getAttributes()
|
||||
.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.class, LibraryElements.JAR));
|
||||
|
||||
TaskProvider<Test> testTask = tasks.register("java" + javaVersion + "Test", Test.class, test -> {
|
||||
test.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP);
|
||||
|
||||
ConfigurableFileCollection testClassesDirs = objects.fileCollection();
|
||||
testClassesDirs.from(testSourceSet.getOutput());
|
||||
testClassesDirs.from(sharedTestSourceSet.getOutput());
|
||||
test.setTestClassesDirs(testClassesDirs);
|
||||
ConfigurableFileCollection classpath = objects.fileCollection();
|
||||
// must put the MRJar first on classpath
|
||||
classpath.from(tasks.named("jar"));
|
||||
// then we put the specific test sourceset tests, so that we can override
|
||||
// the shared versions
|
||||
classpath.from(testSourceSet.getOutput());
|
||||
|
||||
// then we add the shared tests
|
||||
classpath.from(sharedTestSourceSet.getRuntimeClasspath());
|
||||
test.setClasspath(classpath);
|
||||
});
|
||||
return testTask;
|
||||
}
|
||||
|
||||
private void configureMultiReleaseJar(int version, SourceSet languageSourceSet) {
|
||||
tasks.named("jar", Jar.class, jar -> {
|
||||
jar.into("META-INF/versions/" + version, s -> s.from(languageSourceSet.getOutput()));
|
||||
Attributes attributes = jar.getManifest().getAttributes();
|
||||
attributes.put("Multi-Release", "true");
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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.multirelease;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.artifacts.ConfigurationContainer;
|
||||
import org.gradle.api.artifacts.dsl.DependencyHandler;
|
||||
import org.gradle.api.model.ObjectFactory;
|
||||
import org.gradle.api.plugins.ExtensionContainer;
|
||||
import org.gradle.api.plugins.JavaPlugin;
|
||||
import org.gradle.api.plugins.JavaPluginExtension;
|
||||
import org.gradle.api.tasks.TaskContainer;
|
||||
import org.gradle.jvm.toolchain.JavaToolchainService;
|
||||
|
||||
/**
|
||||
* A plugin which adds support for building multi-release jars
|
||||
* with Gradle.
|
||||
* @author Cedric Champeau
|
||||
* @author Brian Clozel
|
||||
* @see <a href="https://github.com/melix/mrjar-gradle-plugin">original project</a>
|
||||
*/
|
||||
public class MultiReleaseJarPlugin implements Plugin<Project> {
|
||||
|
||||
@Inject
|
||||
protected JavaToolchainService getToolchains() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
public void apply(Project project) {
|
||||
project.getPlugins().apply(JavaPlugin.class);
|
||||
ExtensionContainer extensions = project.getExtensions();
|
||||
JavaPluginExtension javaPluginExtension = extensions.getByType(JavaPluginExtension.class);
|
||||
ConfigurationContainer configurations = project.getConfigurations();
|
||||
TaskContainer tasks = project.getTasks();
|
||||
DependencyHandler dependencies = project.getDependencies();
|
||||
ObjectFactory objects = project.getObjects();
|
||||
extensions.create("multiRelease", MultiReleaseExtension.class,
|
||||
javaPluginExtension.getSourceSets(),
|
||||
configurations,
|
||||
tasks,
|
||||
dependencies,
|
||||
objects);
|
||||
}
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright 2002-2025 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.multirelease;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.JarFile;
|
||||
import org.gradle.testkit.runner.BuildResult;
|
||||
import org.gradle.testkit.runner.GradleRunner;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link MultiReleaseJarPlugin}
|
||||
*/
|
||||
public class MultiReleaseJarPluginTests {
|
||||
|
||||
private File projectDir;
|
||||
|
||||
private File buildFile;
|
||||
|
||||
@BeforeEach
|
||||
void setup(@TempDir File projectDir) {
|
||||
this.projectDir = projectDir;
|
||||
this.buildFile = new File(this.projectDir, "build.gradle");
|
||||
}
|
||||
|
||||
@Test
|
||||
void configureSourceSets() throws IOException {
|
||||
writeBuildFile("""
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.build.multiReleaseJar'
|
||||
}
|
||||
multiRelease { releaseVersions 21, 24 }
|
||||
task printSourceSets {
|
||||
doLast {
|
||||
sourceSets.all { println it.name }
|
||||
}
|
||||
}
|
||||
""");
|
||||
BuildResult buildResult = runGradle("printSourceSets");
|
||||
assertThat(buildResult.getOutput()).contains("main", "test", "java21", "java21Test", "java24", "java24Test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void configureToolchainReleaseVersion() throws IOException {
|
||||
writeBuildFile("""
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.build.multiReleaseJar'
|
||||
}
|
||||
multiRelease { releaseVersions 21 }
|
||||
task printReleaseVersion {
|
||||
doLast {
|
||||
tasks.all { println it.name }
|
||||
tasks.named("compileJava21Java") {
|
||||
println "compileJava21Java releaseVersion: ${it.options.release.get()}"
|
||||
}
|
||||
tasks.named("compileJava21TestJava") {
|
||||
println "compileJava21TestJava releaseVersion: ${it.options.release.get()}"
|
||||
}
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
BuildResult buildResult = runGradle("printReleaseVersion");
|
||||
assertThat(buildResult.getOutput()).contains("compileJava21Java releaseVersion: 21")
|
||||
.contains("compileJava21TestJava releaseVersion: 21");
|
||||
}
|
||||
|
||||
@Test
|
||||
void packageInJar() throws IOException {
|
||||
writeBuildFile("""
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.build.multiReleaseJar'
|
||||
}
|
||||
version = '1.2.3'
|
||||
multiRelease { releaseVersions 17 }
|
||||
""");
|
||||
writeClass("src/main/java17", "Main.java", """
|
||||
public class Main {}
|
||||
""");
|
||||
BuildResult buildResult = runGradle("assemble");
|
||||
File file = new File(this.projectDir, "/build/libs/" + this.projectDir.getName() + "-1.2.3.jar");
|
||||
assertThat(file).exists();
|
||||
try (JarFile jar = new JarFile(file)) {
|
||||
Attributes mainAttributes = jar.getManifest().getMainAttributes();
|
||||
assertThat(mainAttributes.getValue("Multi-Release")).isEqualTo("true");
|
||||
|
||||
assertThat(jar.entries().asIterator()).toIterable()
|
||||
.anyMatch(entry -> entry.getName().equals("META-INF/versions/17/Main.class"));
|
||||
}
|
||||
}
|
||||
|
||||
private void writeBuildFile(String buildContent) throws IOException {
|
||||
try (PrintWriter out = new PrintWriter(new FileWriter(this.buildFile))) {
|
||||
out.print(buildContent);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeClass(String path, String fileName, String fileContent) throws IOException {
|
||||
Path folder = this.projectDir.toPath().resolve(path);
|
||||
Files.createDirectories(folder);
|
||||
Path filePath = folder.resolve(fileName);
|
||||
Files.createFile(filePath);
|
||||
Files.writeString(filePath, fileContent);
|
||||
}
|
||||
|
||||
private BuildResult runGradle(String... args) {
|
||||
return GradleRunner.create().withProjectDir(this.projectDir).withArguments(args).withPluginClasspath().build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,6 +20,10 @@ dependencies {
|
||||
}
|
||||
|
||||
javadoc {
|
||||
javadocTool.set(javaToolchains.javadocToolFor({
|
||||
languageVersion = JavaLanguageVersion.of(24)
|
||||
}))
|
||||
|
||||
title = "${rootProject.description} ${version} API"
|
||||
failOnError = true
|
||||
options {
|
||||
@@ -34,7 +38,11 @@ javadoc {
|
||||
links(rootProject.ext.javadocLinks)
|
||||
// Check for 'syntax' and 'reference' during linting.
|
||||
addBooleanOption('Xdoclint:syntax,reference', true)
|
||||
addBooleanOption('Werror', true) // fail build on Javadoc warnings
|
||||
// Change modularity mismatch from warn to info.
|
||||
// See https://github.com/spring-projects/spring-framework/issues/27497
|
||||
addStringOption("-link-modularity-mismatch", "info")
|
||||
// Fail build on Javadoc warnings.
|
||||
addBooleanOption('Werror', true)
|
||||
}
|
||||
maxMemory = "1024m"
|
||||
doFirst {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
|
||||
|
||||
plugins {
|
||||
id 'kotlin'
|
||||
id 'io.spring.antora.generate-antora-yml' version '0.0.1'
|
||||
@@ -41,6 +43,11 @@ repositories {
|
||||
}
|
||||
}
|
||||
|
||||
// To avoid a redeclaration error with Kotlin compiler
|
||||
tasks.named('compileKotlin', KotlinCompilationTask.class) {
|
||||
javaSources.from = []
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":spring-aspects"))
|
||||
implementation(project(":spring-context"))
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
**** xref:core/beans/java/bean-annotation.adoc[]
|
||||
**** xref:core/beans/java/configuration-annotation.adoc[]
|
||||
**** xref:core/beans/java/composing-configuration-classes.adoc[]
|
||||
**** xref:core/beans/java/programmatic-bean-registration.adoc[]
|
||||
*** xref:core/beans/environment.adoc[]
|
||||
*** xref:core/beans/context-load-time-weaver.adoc[]
|
||||
*** xref:core/beans/context-introduction.adoc[]
|
||||
@@ -101,7 +102,6 @@
|
||||
*** xref:core/aop-api/extensibility.adoc[]
|
||||
** xref:core/null-safety.adoc[]
|
||||
** xref:core/databuffer-codec.adoc[]
|
||||
** xref:core/spring-jcl.adoc[]
|
||||
** xref:core/aot.adoc[]
|
||||
** xref:core/appendix.adoc[]
|
||||
*** xref:core/appendix/xsd-schemas.adoc[]
|
||||
@@ -161,7 +161,6 @@
|
||||
**** xref:web/webmvc/mvc-servlet/exceptionhandlers.adoc[]
|
||||
**** xref:web/webmvc/mvc-servlet/viewresolver.adoc[]
|
||||
**** xref:web/webmvc/mvc-servlet/localeresolver.adoc[]
|
||||
**** xref:web/webmvc/mvc-servlet/themeresolver.adoc[]
|
||||
**** xref:web/webmvc/mvc-servlet/multipart.adoc[]
|
||||
**** xref:web/webmvc/mvc-servlet/logging.adoc[]
|
||||
*** xref:web/webmvc/filters.adoc[]
|
||||
@@ -198,6 +197,7 @@
|
||||
*** xref:web/webmvc/mvc-uri-building.adoc[]
|
||||
*** xref:web/webmvc/mvc-ann-async.adoc[]
|
||||
*** xref:web/webmvc-cors.adoc[]
|
||||
*** xref:web/webmvc-versioning.adoc[]
|
||||
*** xref:web/webmvc/mvc-ann-rest-exceptions.adoc[]
|
||||
*** xref:web/webmvc/mvc-security.adoc[]
|
||||
*** xref:web/webmvc/mvc-caching.adoc[]
|
||||
@@ -226,6 +226,7 @@
|
||||
**** xref:web/webmvc/mvc-config/static-resources.adoc[]
|
||||
**** xref:web/webmvc/mvc-config/default-servlet-handler.adoc[]
|
||||
**** xref:web/webmvc/mvc-config/path-matching.adoc[]
|
||||
**** xref:web/webmvc/mvc-config/api-version.adoc[]
|
||||
**** xref:web/webmvc/mvc-config/advanced-java.adoc[]
|
||||
**** xref:web/webmvc/mvc-config/advanced-xml.adoc[]
|
||||
*** xref:web/webmvc/mvc-http2.adoc[]
|
||||
@@ -293,6 +294,7 @@
|
||||
*** xref:web/webflux-functional.adoc[]
|
||||
*** xref:web/webflux/uri-building.adoc[]
|
||||
*** xref:web/webflux-cors.adoc[]
|
||||
*** xref:web/webflux-versioning.adoc[]
|
||||
*** xref:web/webflux/ann-rest-exceptions.adoc[]
|
||||
*** xref:web/webflux/security.adoc[]
|
||||
*** xref:web/webflux/caching.adoc[]
|
||||
@@ -433,8 +435,8 @@
|
||||
*** xref:integration/cache/plug.adoc[]
|
||||
*** xref:integration/cache/specific-config.adoc[]
|
||||
** xref:integration/observability.adoc[]
|
||||
** xref:integration/aot-cache.adoc[]
|
||||
** xref:integration/checkpoint-restore.adoc[]
|
||||
** xref:integration/cds.adoc[]
|
||||
** xref:integration/appendix.adoc[]
|
||||
* xref:languages.adoc[]
|
||||
** xref:languages/kotlin.adoc[]
|
||||
|
||||
@@ -206,18 +206,17 @@ another file or files. The following example shows how to do so:
|
||||
<beans>
|
||||
<import resource="services.xml"/>
|
||||
<import resource="resources/messageSource.xml"/>
|
||||
<import resource="/resources/themeSource.xml"/>
|
||||
|
||||
<bean id="bean1" class="..."/>
|
||||
<bean id="bean2" class="..."/>
|
||||
</beans>
|
||||
----
|
||||
|
||||
In the preceding example, external bean definitions are loaded from three files:
|
||||
`services.xml`, `messageSource.xml`, and `themeSource.xml`. All location paths are
|
||||
In the preceding example, external bean definitions are loaded from the files
|
||||
`services.xml` and `messageSource.xml`. All location paths are
|
||||
relative to the definition file doing the importing, so `services.xml` must be in the
|
||||
same directory or classpath location as the file doing the importing, while
|
||||
`messageSource.xml` and `themeSource.xml` must be in a `resources` location below the
|
||||
`messageSource.xml` must be in a `resources` location below the
|
||||
location of the importing file. As you can see, a leading slash is ignored. However, given
|
||||
that these paths are relative, it is better form not to use the slash at all. The
|
||||
contents of the files being imported, including the top level `<beans/>` element, must
|
||||
|
||||
@@ -674,9 +674,7 @@ 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`.
|
||||
`@jakarta.inject.Named` annotation is used instead of Spring stereotype annotations.
|
||||
|
||||
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
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
[[beans-java-programmatic-registration]]
|
||||
= Programmatic Bean Registration
|
||||
|
||||
As of Spring Framework 7, a first-class support for programmatic bean registration is
|
||||
provided via the {spring-framework-api}/beans/factory/BeanRegistrar.html[`BeanRegistrar`]
|
||||
interface that can be implemented to register beans programmatically in a flexible and
|
||||
efficient way.
|
||||
|
||||
Those bean registrar implementations are typically imported with an `@Import` annotation
|
||||
on `@Configuration` classes.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration
|
||||
@Import(MyBeanRegistrar.class)
|
||||
class MyConfiguration {
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration
|
||||
@Import(MyBeanRegistrar::class)
|
||||
class MyConfiguration {
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
NOTE: You can leverage type-level conditional annotations ({spring-framework-api}/context/annotation/Conditional.html[`@Conditional`],
|
||||
but also other variants) to conditionally import the related bean registrars.
|
||||
|
||||
The bean registrar implementation uses {spring-framework-api}/beans/factory/BeanRegistry.html[`BeanRegistry`] and
|
||||
{spring-framework-api}/core/env/Environment.html[`Environment`] APIs to register beans programmatically in a concise
|
||||
and flexible way. For example, it allows custom registration through an `if` expression, a
|
||||
`for` loop, etc.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
class MyBeanRegistrar implements BeanRegistrar {
|
||||
|
||||
@Override
|
||||
public void register(BeanRegistry registry, Environment env) {
|
||||
registry.registerBean("foo", Foo.class);
|
||||
registry.registerBean("bar", Bar.class, spec -> spec
|
||||
.prototype()
|
||||
.lazyInit()
|
||||
.description("Custom description")
|
||||
.supplier(context -> new Bar(context.bean(Foo.class))));
|
||||
if (env.matchesProfiles("baz")) {
|
||||
registry.registerBean(Baz.class, spec -> spec
|
||||
.supplier(context -> new Baz("Hello World!")));
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
class MyBeanRegistrar : BeanRegistrarDsl({
|
||||
registerBean<Foo>()
|
||||
registerBean(
|
||||
name = "bar",
|
||||
prototype = true,
|
||||
lazyInit = true,
|
||||
description = "Custom description") {
|
||||
Bar(bean<Foo>())
|
||||
}
|
||||
profile("baz") {
|
||||
registerBean { Baz("Hello World!") }
|
||||
}
|
||||
})
|
||||
----
|
||||
======
|
||||
|
||||
NOTE: Bean registrars are supported with xref:core/aot.adoc[Ahead of Time Optimizations],
|
||||
either on the JVM or with GraalVM native images, including when instance suppliers are used.
|
||||
+33
-19
@@ -1,19 +1,19 @@
|
||||
[[expressions-operator-elvis]]
|
||||
= The Elvis Operator
|
||||
|
||||
The Elvis operator is a shortening of the ternary operator syntax and is used in the
|
||||
https://www.groovy-lang.org/operators.html#_elvis_operator[Groovy] language.
|
||||
With the ternary operator syntax, you usually have to repeat a variable twice, as the
|
||||
following example shows:
|
||||
The Elvis operator (`?:`) is a shortening of the ternary operator syntax and is used in
|
||||
the https://www.groovy-lang.org/operators.html#_elvis_operator[Groovy] language. With the
|
||||
ternary operator syntax, you often have to repeat a variable twice, as the following Java
|
||||
example shows:
|
||||
|
||||
[source,groovy,indent=0,subs="verbatim,quotes"]
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
String name = "Elvis Presley";
|
||||
String displayName = (name != null ? name : "Unknown");
|
||||
----
|
||||
|
||||
Instead, you can use the Elvis operator (named for the resemblance to Elvis' hair style).
|
||||
The following example shows how to use the Elvis operator:
|
||||
The following example shows how to use the Elvis operator in a SpEL expression:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -23,7 +23,7 @@ Java::
|
||||
----
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
|
||||
String name = parser.parseExpression("name?:'Unknown'").getValue(new Inventor(), String.class);
|
||||
String name = parser.parseExpression("name ?: 'Unknown'").getValue(new Inventor(), String.class);
|
||||
System.out.println(name); // 'Unknown'
|
||||
----
|
||||
|
||||
@@ -33,14 +33,29 @@ Kotlin::
|
||||
----
|
||||
val parser = SpelExpressionParser()
|
||||
|
||||
val name = parser.parseExpression("name?:'Unknown'").getValue(Inventor(), String::class.java)
|
||||
val name = parser.parseExpression("name ?: 'Unknown'").getValue(Inventor(), String::class.java)
|
||||
println(name) // 'Unknown'
|
||||
----
|
||||
======
|
||||
|
||||
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).
|
||||
[NOTE]
|
||||
====
|
||||
The SpEL Elvis operator also treats an _empty_ String like a `null` object. Thus, the
|
||||
original Java example is only close to emulating the semantics of the operator: it would
|
||||
need to use `name != null && !name.isEmpty()` as the predicate to be compatible with the
|
||||
semantics of the SpEL Elvis operator.
|
||||
====
|
||||
|
||||
[TIP]
|
||||
====
|
||||
As of Spring Framework 7.0, the SpEL Elvis operator supports `java.util.Optional` with
|
||||
transparent unwrapping semantics.
|
||||
|
||||
For example, given the expression `A ?: B`, if `A` is `null` or an _empty_ `Optional`,
|
||||
the expression evaluates to `B`. However, if `A` is a non-empty `Optional` the expression
|
||||
evaluates to the object contained in the `Optional`, thereby effectively unwrapping the
|
||||
`Optional` which correlates to `A.get()`.
|
||||
====
|
||||
|
||||
The following listing shows a more complex example:
|
||||
|
||||
@@ -54,11 +69,11 @@ Java::
|
||||
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
|
||||
|
||||
Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
|
||||
String name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String.class);
|
||||
String name = parser.parseExpression("name ?: 'Elvis Presley'").getValue(context, tesla, String.class);
|
||||
System.out.println(name); // Nikola Tesla
|
||||
|
||||
tesla.setName("");
|
||||
name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String.class);
|
||||
name = parser.parseExpression("name ?: 'Elvis Presley'").getValue(context, tesla, String.class);
|
||||
System.out.println(name); // Elvis Presley
|
||||
----
|
||||
|
||||
@@ -70,16 +85,16 @@ Kotlin::
|
||||
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()
|
||||
|
||||
val tesla = Inventor("Nikola Tesla", "Serbian")
|
||||
var name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String::class.java)
|
||||
var name = parser.parseExpression("name ?: 'Elvis Presley'").getValue(context, tesla, String::class.java)
|
||||
println(name) // Nikola Tesla
|
||||
|
||||
tesla.setName("")
|
||||
name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String::class.java)
|
||||
name = parser.parseExpression("name ?: 'Elvis Presley'").getValue(context, tesla, String::class.java)
|
||||
println(name) // Elvis Presley
|
||||
----
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
[TIP]
|
||||
=====
|
||||
You can use the Elvis operator to apply default values in expressions. The following
|
||||
example shows how to use the Elvis operator in a `@Value` expression:
|
||||
@@ -89,7 +104,6 @@ example shows how to use the Elvis operator in a `@Value` expression:
|
||||
@Value("#{systemProperties['pop3.port'] ?: 25}")
|
||||
----
|
||||
|
||||
This will inject a system property `pop3.port` if it is defined or 25 if not.
|
||||
This will inject the value of the system property named `pop3.port` if it is defined or
|
||||
`25` if the property is not defined.
|
||||
=====
|
||||
|
||||
|
||||
|
||||
+44
-1
@@ -252,7 +252,6 @@ Kotlin::
|
||||
<1> Use "null-safe select first" operator on potentially null `members` list
|
||||
======
|
||||
|
||||
|
||||
The following example shows how to use the "null-safe select last" operator for
|
||||
collections (`?.$`).
|
||||
|
||||
@@ -351,6 +350,50 @@ Kotlin::
|
||||
<2> Use null-safe projection operator on null `members` list
|
||||
======
|
||||
|
||||
[[expressions-operator-safe-navigation-optional]]
|
||||
== Null-safe Operations on `Optional`
|
||||
|
||||
As of Spring Framework 7.0, null-safe operations are supported on instances of
|
||||
`java.util.Optional` with transparent unwrapping semantics.
|
||||
|
||||
Specifically, when a null-safe operator is applied to an _empty_ `Optional`, it will be
|
||||
treated as if the `Optional` were `null`, and the subsequent operation will evaluate to
|
||||
`null`. However, if a null-safe operator is applied to a non-empty `Optional`, the
|
||||
subsequent operation will be applied to the object contained in the `Optional`, thereby
|
||||
effectively unwrapping the `Optional`.
|
||||
|
||||
For example, if `user` is of type `Optional<User>`, the expression `user?.name` will
|
||||
evaluate to `null` if `user` is either `null` or an _empty_ `Optional` and will otherwise
|
||||
evaluate to the `name` of the `user`, effectively `user.get().getName()` or
|
||||
`user.get().name` for property or field access, respectively.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Invocations of methods defined in the `Optional` API are still supported on an _empty_
|
||||
`Optional`. For example, if `name` is of type `Optional<String>`, the expression
|
||||
`name?.orElse('Unknown')` will evaluate to `"Unknown"` if `name` is an empty `Optional`
|
||||
and will otherwise evaluate to the `String` contained in the `Optional` if `name` is a
|
||||
non-empty `Optional`, effectively `name.get()`.
|
||||
====
|
||||
|
||||
// NOTE: ⁠ is the Unicode Character 'WORD JOINER', which prevents undesired line wraps.
|
||||
|
||||
Similarly, if `names` is of type `Optional<List<String>>`, the expression
|
||||
`names?.?⁠[#this.length > 5]` will evaluate to `null` if `names` is `null` or an _empty_
|
||||
`Optional` and will otherwise evaluate to a sequence containing the names whose lengths
|
||||
are greater than 5, effectively
|
||||
`names.get().stream().filter(s -> s.length() > 5).toList()`.
|
||||
|
||||
The same semantics apply to all of the null-safe operators mentioned previously in this
|
||||
chapter.
|
||||
|
||||
For further details and examples, consult the javadoc for the following operators.
|
||||
|
||||
* {spring-framework-api}/expression/spel/ast/PropertyOrFieldReference.html[`PropertyOrFieldReference`]
|
||||
* {spring-framework-api}/expression/spel/ast/MethodReference.html[`MethodReference`]
|
||||
* {spring-framework-api}/expression/spel/ast/Indexer.html[`Indexer`]
|
||||
* {spring-framework-api}/expression/spel/ast/Selection.html[`Selection`]
|
||||
* {spring-framework-api}/expression/spel/ast/Projection.html[`Projection`]
|
||||
|
||||
[[expressions-operator-safe-navigation-compound-expressions]]
|
||||
== Null-safe Operations in Compound Expressions
|
||||
|
||||
@@ -1,57 +1,184 @@
|
||||
[[null-safety]]
|
||||
= Null-safety
|
||||
|
||||
Although Java does not let you express null-safety with its type system, the Spring Framework
|
||||
provides the following annotations in the `org.springframework.lang` package to let you
|
||||
declare nullability of APIs and fields:
|
||||
Although Java does not let you express null-safety with its type system, the Spring Framework codebase is annotated with
|
||||
https://jspecify.dev/docs/start-here/[JSpecify] annotations to declare the nullness of APIs, fields and related type
|
||||
usages. Reading the https://jspecify.dev/docs/user-guide/[JSpecify user guide] is highly recommended in order to get
|
||||
familiar with those annotations and semantics.
|
||||
|
||||
* {spring-framework-api}/lang/Nullable.html[`@Nullable`]: Annotation to indicate that a
|
||||
specific parameter, return value, or field can be `null`.
|
||||
* {spring-framework-api}/lang/NonNull.html[`@NonNull`]: Annotation to indicate that a specific
|
||||
parameter, return value, or field cannot be `null` (not needed on parameters, return values,
|
||||
and fields where `@NonNullApi` and `@NonNullFields` apply, respectively).
|
||||
* {spring-framework-api}/lang/NonNullApi.html[`@NonNullApi`]: Annotation at the package level
|
||||
that declares non-null as the default semantics for parameters and return values.
|
||||
* {spring-framework-api}/lang/NonNullFields.html[`@NonNullFields`]: Annotation at the package
|
||||
level that declares non-null as the default semantics for fields.
|
||||
The primary goal of this explicit null-safety arrangement is to prevent `NullPointerException` to be thrown at runtime via
|
||||
build time checks and to turn explicit nullness into a way to express the possible absence of value. It is useful in
|
||||
both Java by leveraging some tooling (https://github.com/uber/NullAway[NullAway] or IDEs supporting null-safety
|
||||
annotations such as IntelliJ IDEA or Eclipse) and Kotlin where JSpecify annotations are automatically translated to
|
||||
{kotlin-docs}/null-safety.html[Kotlin's null safety].
|
||||
|
||||
The Spring Framework itself leverages these annotations, but they can also be used in any
|
||||
Spring-based Java project to declare null-safe APIs and optionally null-safe fields.
|
||||
Nullability declarations for generic type arguments, varargs, and array elements are not supported yet.
|
||||
Nullability declarations are expected to be fine-tuned between Spring Framework releases,
|
||||
including minor ones. Nullability of types used inside method bodies is outside the
|
||||
scope of this feature.
|
||||
The {spring-framework-api}/core/Nullness.html[`Nullness` Spring API] can be used at runtime to detect the nullness of a
|
||||
type usage, a field, a method return type or a parameter. It provides full support for JSpecify annotations,
|
||||
Kotlin null safety, Java primitive types, as well as a pragmatic check on any `@Nullable` annotation (regardless of the
|
||||
package).
|
||||
|
||||
NOTE: Other common libraries such as Reactor and Spring Data provide null-safe APIs that
|
||||
use a similar nullability arrangement, delivering a consistent overall experience for
|
||||
Spring application developers.
|
||||
[[null-safety-libraries]]
|
||||
== Annotating libraries with JSpecify annotations
|
||||
|
||||
As of Spring Framework 7, the Spring Framework codebase leverages JSpecify annotations to expose null-safe APIs and
|
||||
to check the consistency of those null-safety declarations with https://github.com/uber/NullAway[NullAway] as part of
|
||||
its build. It is recommended for each library depending on Spring Framework (Spring portfolio projects), as
|
||||
well as other libraries related to the Spring ecosystem (Reactor, Micrometer and Spring community projects), to do the
|
||||
same.
|
||||
|
||||
[[null-safety-applications]]
|
||||
== Leveraging JSpecify annotations in Spring applications
|
||||
|
||||
Developing applications with IDEs supporting null-safety annotations, such as IntelliJ IDEA or Eclipse, will provide
|
||||
warnings in Java and errors in Kotlin when the null-safety contracts are not honored, allowing Spring application
|
||||
developers to refine their null handling to prevent `NullPointerException` to be thrown at runtime.
|
||||
|
||||
Optionally, Spring application developers can annotate their codebase and use https://github.com/uber/NullAway[NullAway]
|
||||
to enforce null-safety during build time at application level.
|
||||
|
||||
[[null-safety-guidelines]]
|
||||
== Guidelines
|
||||
|
||||
The purpose of this section is to share some guidelines proposed for specifying explicitly the nullness of Spring-related
|
||||
libraries or applications.
|
||||
|
||||
|
||||
[[null-safety-guidelines-jpecify]]
|
||||
=== JSpecify
|
||||
|
||||
The key points to understand is that by default, the nullness of types is unknown in Java, and that non-null type
|
||||
usages are by far more frequent than nullable ones. In order to keep codebases readable, we typically want to define
|
||||
that by default, type usages are non-null unless marked as nullable for a specific scope. This is exactly the purpose of
|
||||
https://jspecify.dev/docs/api/org/jspecify/annotations/NullMarked.html[`@NullMarked`] that is typically set with Spring
|
||||
at package level via a `package-info.java` file, for example:
|
||||
|
||||
[source,java,subs="verbatim,quotes",chomp="-packages",fold="none"]
|
||||
----
|
||||
@NullMarked
|
||||
package org.springframework.core;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
----
|
||||
|
||||
In the various Java files belonging to the package, nullable type usages are defined explicitly with
|
||||
https://jspecify.dev/docs/api/org/jspecify/annotations/Nullable.html[`@Nullable`]. It is recommended that this
|
||||
annotation is specified just before the related type.
|
||||
|
||||
For example, for a field:
|
||||
|
||||
[source,java,subs="verbatim,quotes"]
|
||||
----
|
||||
private @Nullable String fileEncoding;
|
||||
----
|
||||
|
||||
Or for method parameters and return value:
|
||||
|
||||
[source,java,subs="verbatim,quotes"]
|
||||
----
|
||||
public static @Nullable String buildMessage(@Nullable String message,
|
||||
@Nullable Throwable cause) {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
|
||||
When overriding a method, nullness annotations are not inherited from the superclass method. That means those
|
||||
nullness annotations should be repeated if you just want to override the implementation and keep the same API
|
||||
nullness.
|
||||
|
||||
With arrays and varargs, you need to be able to differentiate the nullness of the elements from the nullness of
|
||||
the array itself. Pay attention to the syntax
|
||||
https://docs.oracle.com/javase/specs/jls/se17/html/jls-9.html#jls-9.7.4[defined by the Java specification] which may be
|
||||
initially surprising:
|
||||
|
||||
- `@Nullable Object[] array` means individual elements can be null but the array itself can't.
|
||||
- `Object @Nullable [] array` means individual elements can't be null but the array itself can.
|
||||
- `@Nullable Object @Nullable [] array` means both individual elements and the array can be null.
|
||||
|
||||
The Java specifications also enforces that annotations defined with `@Target(ElementType.TYPE_USE)` like JSpecify
|
||||
`@Nullable` should be specified after the last `.` with inner or fully qualified types:
|
||||
|
||||
- `Cache.@Nullable ValueWrapper`
|
||||
- `jakarta.validation.@Nullable Validator`
|
||||
|
||||
https://jspecify.dev/docs/api/org/jspecify/annotations/NonNull.html[`@NonNull`] and
|
||||
https://jspecify.dev/docs/api/org/jspecify/annotations/NullUnmarked.html[`@NullUnmarked`] should rarely be needed for
|
||||
typical use cases.
|
||||
|
||||
[[null-safety-guidelines-nullaway]]
|
||||
=== NullAway
|
||||
|
||||
==== Configuration
|
||||
|
||||
The recommended configuration is:
|
||||
|
||||
- `NullAway:OnlyNullMarked=true` in order to perform nullness checks only for packages annotated with `@NullMarked`.
|
||||
- `NullAway:CustomContractAnnotations=org.springframework.lang.Contract` which makes NullAway aware of the
|
||||
{spring-framework-api}/lang/Contract.html[@Contract] annotation in the `org.springframework.lang` package which
|
||||
can be used to express complementary semantics to avoid non-relevant null-safety warnings in your codebase.
|
||||
|
||||
A good example of `@Contract` benefits is
|
||||
{spring-framework-api}/util/Assert.html#notNull(java.lang.Object,java.lang.String)[`Assert#notnull`] which is annotated
|
||||
with `@Contract("null, _ -> fail")`. With the configuration above, NullAway will understand that after a successful
|
||||
invocation, the value passed as a parameter is not null.
|
||||
|
||||
Optionally, it is possible to set `NullAway:JSpecifyMode=true` to enable
|
||||
https://github.com/uber/NullAway/wiki/JSpecify-Support[checks on the full JSpecify semantics], including annotations on
|
||||
generic types. Be aware that this mode is
|
||||
https://github.com/uber/NullAway/issues?q=is%3Aissue+is%3Aopen+label%3Ajspecify[still under development] and requires
|
||||
using JDK 22 or later (typically combined with the `--release` Java compiler flag to configure the
|
||||
expected baseline). It is recommended to enable the JSpecify mode only as a second step, after making sure the codebase
|
||||
generates no warning with the recommended configuration mentioned above.
|
||||
|
||||
==== Warnings suppression
|
||||
|
||||
There are a few valid use cases where NullAway will wrongly detect nullness problems. In such case, it is recommended
|
||||
to suppress related warnings and to document the reason:
|
||||
|
||||
- `@SuppressWarnings("NullAway.Init")` at field, constructor or class level can be used to avoid unnecessary warnings
|
||||
due to the lazy initialization of fields, for example due to a class implementing
|
||||
{spring-framework-api}/beans/factory/InitializingBean.html[`InitializingBean`].
|
||||
- `@SuppressWarnings("NullAway") // Dataflow analysis limitation` can be used when NullAway dataflow analysis is not
|
||||
able to detect that the path involving a nullness problem will never happen.
|
||||
- `@SuppressWarnings("NullAway") // Lambda` can be used when NullAway does not take into account assertions performed
|
||||
outside of a lambda for the code path within the lambda.
|
||||
- `@SuppressWarnings("NullAway") // Reflection` can be used for some reflection operations that are known returning
|
||||
non-null values even if that can't be expressed by the API.
|
||||
- `@SuppressWarnings("NullAway") // Well-known map keys` can be used when `Map#get` invocations are done with keys known
|
||||
to be present and non-null related values inserted previously.
|
||||
- `@SuppressWarnings("NullAway") // Overridden method does not define nullness` can be used when the super class does
|
||||
not define nullness (typically when the super class is coming from a dependency).
|
||||
|
||||
|
||||
[[use-cases]]
|
||||
== Use cases
|
||||
[[null-safety-migrating]]
|
||||
== Migrating from Spring null-safety annotations
|
||||
|
||||
In addition to providing an explicit declaration for Spring Framework API nullability,
|
||||
these annotations can be used by an IDE (such as IDEA or Eclipse) to provide useful
|
||||
warnings related to null-safety in order to avoid `NullPointerException` at runtime.
|
||||
Spring null-safety annotations {spring-framework-api}/lang/Nullable.html[`@Nullable`],
|
||||
{spring-framework-api}/lang/NonNull.html[`@NonNull`],
|
||||
{spring-framework-api}/lang/NonNullApi.html[`@NonNullApi`], and
|
||||
{spring-framework-api}/lang/NonNullFields.html[`@NonNullFields`] in the `org.springframework.lang` package have been
|
||||
introduced in Spring Framework 5 when JSpecify did not exist and the best option was to leverage JSR 305 (a dormant
|
||||
but widespread JSR) meta-annotations. They are deprecated as of Spring Framework 7 in favor of
|
||||
https://jspecify.dev/docs/start-here/[JSpecify] annotations, which provide significant enhancements such as properly
|
||||
defined specifications, a canonical dependency with no split-package issue, better tooling, better Kotlin integration
|
||||
and the capability to specify the nullness more precisely for more use cases.
|
||||
|
||||
They are also used to make Spring APIs null-safe in Kotlin projects, since Kotlin natively
|
||||
supports {kotlin-docs}/null-safety.html[null-safety]. More details
|
||||
are available in the xref:languages/kotlin/null-safety.adoc[Kotlin support documentation].
|
||||
A key difference is that Spring null-safety annotations, following JSR 305 semantics, apply to fields,
|
||||
parameters and return values while JSpecify annotations apply to type usages. This subtle difference
|
||||
is in practice pretty significant, as it allows for example to differentiate the nullness of elements from the
|
||||
nullness of arrays/varargs as well as defining the nullness of generic types.
|
||||
|
||||
That means array and varargs null-safety declarations have to be updated to keep the same semantic. For example
|
||||
`@Nullable Object[] array` with Spring annotations needs to be changed to `Object @Nullable [] array` with JSpecify
|
||||
annotations. Same for varargs.
|
||||
|
||||
It is also recommended to move field and return value annotations closer to the type, for example:
|
||||
|
||||
- For fields, instead of `@Nullable private String field` with Spring annotations, use `private @Nullable String field`
|
||||
with JSpecify annotations.
|
||||
- For return values, instead of `@Nullable public String method()` with Spring annotations, use
|
||||
`public @Nullable String method()` with JSpecify annotations.
|
||||
|
||||
[[jsr-305-meta-annotations]]
|
||||
== JSR-305 meta-annotations
|
||||
Also, with JSpecify, you don't need to specify `@NonNull` when overriding a type usage annotated with `@Nullable` in the
|
||||
super method to "undo" the nullable declaration in null-marked code. Just declare it unannotated and the null-marked
|
||||
defaults (a type usage is considered non-null unless explicitly annotated as nullable) will apply.
|
||||
|
||||
Spring annotations are meta-annotated with {JSR}305[JSR 305]
|
||||
annotations (a dormant but widespread JSR). JSR-305 meta-annotations let tooling vendors
|
||||
like IDEA or Kotlin provide null-safety support in a generic way, without having to
|
||||
hard-code support for Spring annotations.
|
||||
|
||||
It is neither necessary nor recommended to add a JSR-305 dependency to the project classpath to
|
||||
take advantage of Spring's null-safe APIs. Only projects such as Spring-based libraries that use
|
||||
null-safety annotations in their codebase should add `com.google.code.findbugs:jsr305:3.0.2`
|
||||
with `compileOnly` Gradle configuration or Maven `provided` scope to avoid compiler warnings.
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
[[spring-jcl]]
|
||||
= Logging
|
||||
|
||||
Spring comes with its own Commons Logging bridge implemented
|
||||
in the `spring-jcl` module. The implementation checks for the presence of the Log4j 2.x
|
||||
API and the SLF4J 1.7 API in the classpath and uses the first one of those found as the
|
||||
logging implementation, falling back to the Java platform's core logging facilities (also
|
||||
known as _JUL_ or `java.util.logging`) if neither Log4j 2.x nor SLF4J is available.
|
||||
|
||||
Put Log4j 2.x or Logback (or another SLF4J provider) in your classpath, without any extra
|
||||
bridges, and let the framework auto-adapt to your choice. For further information see the
|
||||
{spring-boot-docs-ref}/features/logging.html[Spring
|
||||
Boot Logging Reference Documentation].
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Spring's Commons Logging variant is only meant to be used for infrastructure logging
|
||||
purposes in the core framework and in extensions.
|
||||
|
||||
For logging needs within application code, prefer direct use of Log4j 2.x, SLF4J, or JUL.
|
||||
====
|
||||
|
||||
A `Log` implementation may be retrieved via `org.apache.commons.logging.LogFactory` as in
|
||||
the following example.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
public class MyBean {
|
||||
private final Log log = LogFactory.getLog(getClass());
|
||||
// ...
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
class MyBean {
|
||||
private val log = LogFactory.getLog(javaClass)
|
||||
// ...
|
||||
}
|
||||
----
|
||||
======
|
||||
@@ -0,0 +1,105 @@
|
||||
[[aot-cache]]
|
||||
= JVM AOT Cache
|
||||
:page-aliases: integration/class-data-sharing.adoc
|
||||
:page-aliases: integration/cds.adoc
|
||||
|
||||
The ahead-of-time cache is a JVM feature introduced in Java 24 via the
|
||||
https://openjdk.org/jeps/483[JEP 483] that can help reduce the startup time and memory
|
||||
footprint of Java applications. AOT cache is a natural evolution of https://docs.oracle.com/en/java/javase/17/vm/class-data-sharing.html[Class Data Sharing (CDS)].
|
||||
Spring Framework supports both CDS and AOT cache, and it is recommended that you use the
|
||||
later if available in the JVM version your are using (Java 24+).
|
||||
|
||||
To use this feature, an AOT cache should be created for the particular classpath of the
|
||||
application. It is possible to create this cache on the deployed instance, or during a
|
||||
training run performed for example when packaging the application thanks to an hook-point
|
||||
provided by the Spring Framework to ease such use case. Once the cache is available, users
|
||||
should opt in to use it via a JVM flag.
|
||||
|
||||
NOTE: If you are using Spring Boot, it is highly recommended to leverage its
|
||||
{spring-boot-docs-ref}/packaging/efficient.html#packaging.efficient.unpacking[executable JAR unpacking support]
|
||||
which is designed to fulfill the class loading requirements of both AOT cache and CDS.
|
||||
|
||||
== Creating the cache
|
||||
|
||||
An AOT cache can typically be created when the application exits. The Spring Framework
|
||||
provides a mode of operation where the process can exit automatically once the
|
||||
`ApplicationContext` has refreshed. In this mode, all non-lazy initialized singletons
|
||||
have been instantiated, and `InitializingBean#afterPropertiesSet` callbacks have been
|
||||
invoked; but the lifecycle has not started, and the `ContextRefreshedEvent` has not yet
|
||||
been published.
|
||||
|
||||
To create the cache during the training run, it is possible to specify the `-Dspring.context.exit=onRefresh`
|
||||
JVM flag to start then exit your Spring application once the
|
||||
`ApplicationContext` has refreshed:
|
||||
|
||||
|
||||
--
|
||||
[tabs]
|
||||
======
|
||||
AOT cache::
|
||||
+
|
||||
[source,bash,subs="verbatim,quotes"]
|
||||
----
|
||||
# Both commands need to be run with the same classpath
|
||||
java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf -Dspring.context.exit=onRefresh ...
|
||||
java -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf -XX:AOTCache=app.aot ...
|
||||
----
|
||||
|
||||
CDS::
|
||||
+
|
||||
[source,bash,subs="verbatim,quotes"]
|
||||
----
|
||||
# To create a CDS archive, your JDK/JRE must have a base image
|
||||
java -XX:ArchiveClassesAtExit=app.jsa -Dspring.context.exit=onRefresh ...
|
||||
----
|
||||
======
|
||||
--
|
||||
|
||||
== Using the cache
|
||||
|
||||
Once the cache file has been created, you can use it to start your application faster:
|
||||
|
||||
--
|
||||
[tabs]
|
||||
======
|
||||
AOT cache::
|
||||
+
|
||||
[source,bash,subs="verbatim"]
|
||||
----
|
||||
# With the same classpath (or a superset) tan the training run
|
||||
java -XX:AOTCache=app.aot ...
|
||||
----
|
||||
|
||||
CDS::
|
||||
+
|
||||
[source,bash,subs="verbatim"]
|
||||
----
|
||||
# With the same classpath (or a superset) tan the training run
|
||||
java -XX:SharedArchiveFile=app.jsa ...
|
||||
----
|
||||
======
|
||||
--
|
||||
|
||||
Pay attention to the logs and the startup time to check if the AOT cache is used successfully.
|
||||
To figure out how effective the cache is, you can enable class loading logs by adding
|
||||
an extra attribute: `-Xlog:class+load:file=aot-cache.log`. This creates a `aot-cache.log` with
|
||||
every attempt to load a class and its source. Classes that are loaded from the cache should have
|
||||
a "shared objects file" source, as shown in the following example:
|
||||
|
||||
[source,shell,subs="verbatim"]
|
||||
----
|
||||
[0.151s][info][class,load] org.springframework.core.env.EnvironmentCapable source: shared objects file
|
||||
[0.151s][info][class,load] org.springframework.beans.factory.BeanFactory source: shared objects file
|
||||
[0.151s][info][class,load] org.springframework.beans.factory.ListableBeanFactory source: shared objects file
|
||||
[0.151s][info][class,load] org.springframework.beans.factory.HierarchicalBeanFactory source: shared objects file
|
||||
[0.151s][info][class,load] org.springframework.context.MessageSource source: shared objects file
|
||||
----
|
||||
|
||||
If the AOT cache can't be enabled or if you have a large number of classes that are not loaded from
|
||||
the cache, make sure that the following conditions are fulfilled when creating and using the cache:
|
||||
|
||||
- The very same JVM must be used.
|
||||
- The classpath must be specified as a JAR or a list of JARs, and avoid the usage of directories and `*` wildcard characters.
|
||||
- The timestamps of the JARs must be preserved.
|
||||
- When using the cache, the classpath must be the same than the one used to create it, in the same order.
|
||||
Additional JARs or directories can be specified *at the end* (but won't be cached).
|
||||
@@ -1,72 +0,0 @@
|
||||
[[cds]]
|
||||
= CDS
|
||||
:page-aliases: integration/class-data-sharing.adoc
|
||||
|
||||
Class Data Sharing (CDS) is a https://docs.oracle.com/en/java/javase/17/vm/class-data-sharing.html[JVM feature]
|
||||
that can help reduce the startup time and memory footprint of Java applications.
|
||||
|
||||
To use this feature, a CDS archive should be created for the particular classpath of the
|
||||
application. The Spring Framework provides a hook-point to ease the creation of the
|
||||
archive. Once the archive is available, users should opt in to use it via a JVM flag.
|
||||
|
||||
== Creating the CDS Archive
|
||||
|
||||
A CDS archive for an application can be created when the application exits. The Spring
|
||||
Framework provides a mode of operation where the process can exit automatically once the
|
||||
`ApplicationContext` has refreshed. In this mode, all non-lazy initialized singletons
|
||||
have been instantiated, and `InitializingBean#afterPropertiesSet` callbacks have been
|
||||
invoked; but the lifecycle has not started, and the `ContextRefreshedEvent` has not yet
|
||||
been published.
|
||||
|
||||
To create the archive, two additional JVM flags must be specified:
|
||||
|
||||
* `-XX:ArchiveClassesAtExit=application.jsa`: creates the CDS archive on exit
|
||||
* `-Dspring.context.exit=onRefresh`: starts and then immediately exits your Spring
|
||||
application as described above
|
||||
|
||||
To create a CDS archive, your JDK/JRE must have a base image. If you add the flags above to
|
||||
your startup script, you may get a warning that looks like this:
|
||||
|
||||
[source,shell,indent=0,subs="verbatim"]
|
||||
----
|
||||
-XX:ArchiveClassesAtExit is unsupported when base CDS archive is not loaded. Run with -Xlog:cds for more info.
|
||||
----
|
||||
|
||||
The base CDS archive is usually provided out-of-the-box, but can also be created if needed by issuing the following
|
||||
command:
|
||||
|
||||
[source,shell,indent=0,subs="verbatim"]
|
||||
----
|
||||
$ java -Xshare:dump
|
||||
----
|
||||
|
||||
== Using the Archive
|
||||
|
||||
Once the archive is available, add `-XX:SharedArchiveFile=application.jsa` to your startup
|
||||
script to use it, assuming an `application.jsa` file in the working directory.
|
||||
|
||||
To check if the CDS cache is effective, you can use (for testing purposes only, not in production) `-Xshare:on` which
|
||||
prints an error message and exits if CDS can't be enabled.
|
||||
|
||||
To figure out how effective the cache is, you can enable class loading logs by adding
|
||||
an extra attribute: `-Xlog:class+load:file=cds.log`. This creates a `cds.log` with every
|
||||
attempt to load a class and its source. Classes that are loaded from the cache should have
|
||||
a "shared objects file" source, as shown in the following example:
|
||||
|
||||
[source,shell,indent=0,subs="verbatim"]
|
||||
----
|
||||
[0.064s][info][class,load] org.springframework.core.env.EnvironmentCapable source: shared objects file (top)
|
||||
[0.064s][info][class,load] org.springframework.beans.factory.BeanFactory source: shared objects file (top)
|
||||
[0.064s][info][class,load] org.springframework.beans.factory.ListableBeanFactory source: shared objects file (top)
|
||||
[0.064s][info][class,load] org.springframework.beans.factory.HierarchicalBeanFactory source: shared objects file (top)
|
||||
[0.065s][info][class,load] org.springframework.context.MessageSource source: shared objects file (top)
|
||||
----
|
||||
|
||||
If CDS can't be enabled or if you have a large number of classes that are not loaded from the cache, make sure that
|
||||
the following conditions are fulfilled when creating and using the archive:
|
||||
|
||||
- The very same JVM must be used.
|
||||
- The classpath must be specified as a list of JARs, and avoid the usage of directories and `*` wildcard characters.
|
||||
- The timestamps of the JARs must be preserved.
|
||||
- When using the archive, the classpath must be the same than the one used to create the archive, in the same order.
|
||||
Additional JARs or directories can be specified *at the end* (but won't be cached).
|
||||
@@ -11,9 +11,7 @@ Spring Framework's email support:
|
||||
* The https://jakartaee.github.io/mail-api/[Jakarta Mail] library
|
||||
|
||||
This library is freely available on the web -- for example, in Maven Central as
|
||||
`com.sun.mail:jakarta.mail`. Please make sure to use the latest 2.x version (which uses
|
||||
the `jakarta.mail` package namespace) rather than Jakarta Mail 1.6.x (which uses the
|
||||
`javax.mail` package namespace).
|
||||
`org.eclipse.angus:angus-mail`.
|
||||
****
|
||||
|
||||
The Spring Framework provides a helpful utility library for sending email that shields
|
||||
|
||||
@@ -12,17 +12,25 @@ The Spring Framework provides the following choices for making calls to REST end
|
||||
[[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 a Java object to an HTTP request, and the creation of objects from an HTTP response.
|
||||
`RestClient` is a synchronous HTTP client that provides a fluent API to perform requests.
|
||||
It serves as an abstraction over HTTP libraries, and handles conversion of HTTP request and response content to and from higher level Java objects.
|
||||
|
||||
=== Creating a `RestClient`
|
||||
=== Create 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, default request headers, or `uriBuilderFactory`, or registering interceptors and initializers.
|
||||
`RestClient` has static `create` shortcut methods.
|
||||
It also exposes a `builder()` with further options:
|
||||
|
||||
Once created (or built), the `RestClient` can be used safely by multiple threads.
|
||||
- select the HTTP library to use, see <<rest-request-factories>>
|
||||
- configure message converters, see <<rest-message-conversion>>
|
||||
- set a baseUrl
|
||||
- set default request headers, cookies, path variables, API version
|
||||
- configure an `ApiVersionInserter`
|
||||
- register interceptors
|
||||
- register request initializers
|
||||
|
||||
The following sample shows how to create a default `RestClient`, and how to build a custom one.
|
||||
Once created, a `RestClient` is safe to use in multiple threads.
|
||||
|
||||
The below shows how to create or build a `RestClient`:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -39,6 +47,8 @@ Java::
|
||||
.defaultUriVariables(Map.of("variable", "foo"))
|
||||
.defaultHeader("My-Header", "Foo")
|
||||
.defaultCookie("My-Cookie", "Bar")
|
||||
.defaultVersion("1.2")
|
||||
.apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build())
|
||||
.requestInterceptor(myCustomInterceptor)
|
||||
.requestInitializer(myCustomInitializer)
|
||||
.build();
|
||||
@@ -57,23 +67,25 @@ Kotlin::
|
||||
.defaultUriVariables(mapOf("variable" to "foo"))
|
||||
.defaultHeader("My-Header", "Foo")
|
||||
.defaultCookie("My-Cookie", "Bar")
|
||||
.defaultVersion("1.2")
|
||||
.apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build())
|
||||
.requestInterceptor(myCustomInterceptor)
|
||||
.requestInitializer(myCustomInitializer)
|
||||
.build()
|
||||
----
|
||||
======
|
||||
|
||||
=== Using the `RestClient`
|
||||
=== Use 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.
|
||||
To perform an HTTP request, first specify the HTTP method to use.
|
||||
Use the convenience methods like `get()`, `head()`, `post()`, and others, or `method(HttpMethod)`.
|
||||
|
||||
==== 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.
|
||||
Next, specify the request URI with the `uri` methods.
|
||||
This is optional, and you can skip this step if you configured a baseUrl through the builder.
|
||||
The URL is typically specified as a `String`, with optional URI template variables.
|
||||
The following example configures a GET request to `https://example.com/orders/42`:
|
||||
The following shows how to perform a request:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -108,6 +120,7 @@ For more details on working with and encoding URIs, see xref:web/webmvc/mvc-uri-
|
||||
|
||||
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 requests that can contain a body (`POST`, `PUT`, and `PATCH`), additional methods are available: `contentType(MediaType)`, and `contentLength(long)`.
|
||||
You can set an API version for the request if the client is configured with `ApiVersionInserter`.
|
||||
|
||||
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.
|
||||
|
||||
@@ -1,113 +1,7 @@
|
||||
[[kotlin-bean-definition-dsl]]
|
||||
= Bean Definition DSL
|
||||
|
||||
Spring Framework supports registering beans in a functional way by using lambdas
|
||||
as an alternative to XML or Java configuration (`@Configuration` and `@Bean`). In a nutshell,
|
||||
it lets you register beans with a lambda that acts as a `FactoryBean`.
|
||||
This mechanism is very efficient, as it does not require any reflection or CGLIB proxies.
|
||||
|
||||
In Java, you can, for example, write the following:
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
class Foo {}
|
||||
|
||||
class Bar {
|
||||
private final Foo foo;
|
||||
public Bar(Foo foo) {
|
||||
this.foo = foo;
|
||||
}
|
||||
}
|
||||
|
||||
GenericApplicationContext context = new GenericApplicationContext();
|
||||
context.registerBean(Foo.class);
|
||||
context.registerBean(Bar.class, () -> new Bar(context.getBean(Foo.class)));
|
||||
----
|
||||
|
||||
In Kotlin, with reified type parameters and `GenericApplicationContext` Kotlin extensions,
|
||||
you can instead write the following:
|
||||
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
class Foo
|
||||
|
||||
class Bar(private val foo: Foo)
|
||||
|
||||
val context = GenericApplicationContext().apply {
|
||||
registerBean<Foo>()
|
||||
registerBean { Bar(it.getBean()) }
|
||||
}
|
||||
----
|
||||
|
||||
When the class `Bar` has a single constructor, you can even just specify the bean class,
|
||||
the constructor parameters will be autowired by type:
|
||||
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
val context = GenericApplicationContext().apply {
|
||||
registerBean<Foo>()
|
||||
registerBean<Bar>()
|
||||
}
|
||||
----
|
||||
|
||||
In order to allow a more declarative approach and cleaner syntax, Spring Framework provides
|
||||
a {spring-framework-api-kdoc}/spring-context/org.springframework.context.support/-bean-definition-dsl/index.html[Kotlin bean definition DSL]
|
||||
It declares an `ApplicationContextInitializer` through a clean declarative API,
|
||||
which lets you deal with profiles and `Environment` for customizing
|
||||
how beans are registered.
|
||||
|
||||
In the following example notice that:
|
||||
|
||||
* Type inference usually allows to avoid specifying the type for bean references like `ref("bazBean")`
|
||||
* It is possible to use Kotlin top level functions to declare beans using callable references like `bean(::myRouter)` in this example
|
||||
* When specifying `bean<Bar>()` or `bean(::myRouter)`, parameters are autowired by type
|
||||
* The `FooBar` bean will be registered only if the `foobar` profile is active
|
||||
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
class Foo
|
||||
class Bar(private val foo: Foo)
|
||||
class Baz(var message: String = "")
|
||||
class FooBar(private val baz: Baz)
|
||||
|
||||
val myBeans = beans {
|
||||
bean<Foo>()
|
||||
bean<Bar>()
|
||||
bean("bazBean") {
|
||||
Baz().apply {
|
||||
message = "Hello world"
|
||||
}
|
||||
}
|
||||
profile("foobar") {
|
||||
bean { FooBar(ref("bazBean")) }
|
||||
}
|
||||
bean(::myRouter)
|
||||
}
|
||||
|
||||
fun myRouter(foo: Foo, bar: Bar, baz: Baz) = router {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
|
||||
NOTE: This DSL is programmatic, meaning it allows custom registration logic of beans
|
||||
through an `if` expression, a `for` loop, or any other Kotlin constructs.
|
||||
|
||||
You can then use this `beans()` function to register beans on the application context,
|
||||
as the following example shows:
|
||||
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
val context = GenericApplicationContext().apply {
|
||||
myBeans.initialize(this)
|
||||
refresh()
|
||||
}
|
||||
----
|
||||
|
||||
NOTE: Spring Boot is based on JavaConfig and
|
||||
{spring-boot-issues}/8115[does not yet provide specific support for functional bean definition],
|
||||
but you can experimentally use functional bean definitions through Spring Boot's `ApplicationContextInitializer` support.
|
||||
See {stackoverflow-questions}/45935931/how-to-use-functional-bean-definition-kotlin-dsl-with-spring-boot-and-spring-w/46033685#46033685[this Stack Overflow answer]
|
||||
for more details and up-to-date information. See also the experimental Kofu DSL developed in {spring-github-org}-experimental/spring-fu[Spring Fu incubator].
|
||||
See xref:core/beans/java/programmatic-bean-registration.adoc[Programmatic Bean Registration].
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -5,34 +5,11 @@ One of Kotlin's key features is {kotlin-docs}/null-safety.html[null-safety],
|
||||
which cleanly deals with `null` values at compile time rather than bumping into the famous
|
||||
`NullPointerException` at runtime. This makes applications safer through nullability
|
||||
declarations and expressing "`value or no value`" semantics without paying the cost of wrappers, such as `Optional`.
|
||||
(Kotlin allows using functional constructs with nullable values. See this
|
||||
{baeldung-blog}/kotlin-null-safety[comprehensive guide to Kotlin null-safety].)
|
||||
Kotlin allows using functional constructs with nullable values. See this
|
||||
{baeldung-blog}/kotlin-null-safety[comprehensive guide to Kotlin null-safety].
|
||||
|
||||
Although Java does not let you express null-safety in its type-system, the Spring Framework
|
||||
provides xref:languages/kotlin/null-safety.adoc[null-safety of the whole Spring Framework API]
|
||||
via tooling-friendly annotations declared in the `org.springframework.lang` package.
|
||||
By default, types from Java APIs used in Kotlin are recognized as
|
||||
{kotlin-docs}/java-interop.html#null-safety-and-platform-types[platform types],
|
||||
for which null-checks are relaxed.
|
||||
{kotlin-docs}/java-interop.html#jsr-305-support[Kotlin support for JSR-305 annotations]
|
||||
and Spring nullability annotations provide null-safety for the whole Spring Framework API to Kotlin developers,
|
||||
with the advantage of dealing with `null`-related issues at compile time.
|
||||
|
||||
NOTE: Libraries such as Reactor or Spring Data provide null-safe APIs to leverage this feature.
|
||||
|
||||
You can configure JSR-305 checks by adding the `-Xjsr305` compiler flag with the following
|
||||
options: `-Xjsr305={strict|warn|ignore}`.
|
||||
|
||||
For kotlin versions 1.1+, the default behavior is the same as `-Xjsr305=warn`.
|
||||
The `strict` value is required to have Spring Framework API null-safety taken into account
|
||||
in Kotlin types inferred from Spring API but should be used with the knowledge that Spring
|
||||
API nullability declaration could evolve even between minor releases and that more checks may
|
||||
be added in the future.
|
||||
|
||||
NOTE: Generic type arguments, varargs, and array elements nullability are not supported yet,
|
||||
but should be in an upcoming release. See {kotlin-github-org}/KEEP/issues/79[this discussion]
|
||||
for up-to-date information.
|
||||
|
||||
|
||||
|
||||
provides xref:core/null-safety.adoc[null-safety of the whole Spring Framework API]
|
||||
via tooling-friendly https://jspecify.dev/[JSpecify] annotations.
|
||||
|
||||
As of Kotlin 2.1, Kotlin enforces strict handling of nullability annotations from `org.jspecify.annotations` package.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
= Requirements
|
||||
:page-section-summary-toc: 1
|
||||
|
||||
Spring Framework supports Kotlin 1.7+ and requires
|
||||
Spring Framework supports Kotlin 2.1+ and requires
|
||||
https://search.maven.org/artifact/org.jetbrains.kotlin/kotlin-stdlib[`kotlin-stdlib`]
|
||||
and https://search.maven.org/artifact/org.jetbrains.kotlin/kotlin-reflect[`kotlin-reflect`]
|
||||
to be present on the classpath. They are provided by default if you bootstrap a Kotlin project on
|
||||
|
||||
@@ -73,63 +73,13 @@ idiomatic Kotlin API and to allow better discoverability (no usage of static met
|
||||
|
||||
|
||||
|
||||
[[kotlin-script-templates]]
|
||||
== Kotlin Script Templates
|
||||
|
||||
Spring Framework provides a
|
||||
{spring-framework-api}/web/servlet/view/script/ScriptTemplateView.html[`ScriptTemplateView`]
|
||||
which supports {JSR}223[JSR-223] to render templates by using script engines.
|
||||
|
||||
By leveraging `scripting-jsr223` dependencies, it
|
||||
is possible to use such feature to render Kotlin-based templates with
|
||||
{kotlin-github-org}/kotlinx.html[kotlinx.html] DSL or Kotlin multiline interpolated `String`.
|
||||
|
||||
`build.gradle.kts`
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
dependencies {
|
||||
runtime("org.jetbrains.kotlin:kotlin-scripting-jsr223:${kotlinVersion}")
|
||||
}
|
||||
----
|
||||
|
||||
Configuration is usually done with `ScriptTemplateConfigurer` and `ScriptTemplateViewResolver` beans.
|
||||
|
||||
`KotlinScriptConfiguration.kt`
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
@Configuration
|
||||
class KotlinScriptConfiguration {
|
||||
|
||||
@Bean
|
||||
fun kotlinScriptConfigurer() = ScriptTemplateConfigurer().apply {
|
||||
engineName = "kotlin"
|
||||
setScripts("scripts/render.kts")
|
||||
renderFunction = "render"
|
||||
isSharedEngine = false
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun kotlinScriptViewResolver() = ScriptTemplateViewResolver().apply {
|
||||
setPrefix("templates/")
|
||||
setSuffix(".kts")
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
See the https://github.com/sdeleuze/kotlin-script-templating[kotlin-script-templating] example
|
||||
project for more details.
|
||||
|
||||
|
||||
|
||||
[[kotlin-multiplatform-serialization]]
|
||||
== Kotlin multiplatform serialization
|
||||
|
||||
{kotlin-github-org}/kotlinx.serialization[Kotlin multiplatform serialization] is
|
||||
supported in Spring MVC, Spring WebFlux and Spring Messaging (RSocket). The built-in support currently targets CBOR, JSON, and ProtoBuf formats.
|
||||
|
||||
To enable it, follow {kotlin-github-org}/kotlinx.serialization#setup[those instructions] to add the related dependency and plugin.
|
||||
With Spring MVC and WebFlux, both Kotlin serialization and Jackson will be configured by default if they are in the classpath since
|
||||
Kotlin serialization is designed to serialize only Kotlin classes annotated with `@Serializable`.
|
||||
With Spring Messaging (RSocket), make sure that neither Jackson, GSON or JSONB are in the classpath if you want automatic configuration,
|
||||
if Jackson is needed configure `KotlinSerializationJsonMessageConverter` manually.
|
||||
supported in Spring MVC, Spring WebFlux and Spring Messaging (RSocket). The builtin support currently targets CBOR, JSON,
|
||||
and ProtoBuf formats.
|
||||
|
||||
To enable it, follow {kotlin-github-org}/kotlinx.serialization#setup[those instructions] to add the related dependencies
|
||||
and plugin. With Spring MVC and WebFlux, Kotlin serialization is configured by default if it is in the classpath and
|
||||
other variants like Jackson are not. If needed, configure the converters or codecs manually.
|
||||
|
||||
+3
-3
@@ -171,9 +171,9 @@ the parameters of a test class constructor are autowired from components in the
|
||||
|
||||
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`,
|
||||
`@jakarta.inject.Inject`, or `@javax.inject.Inject` on a constructor takes precedence
|
||||
over both `@TestConstructor` and the default mode.
|
||||
the default mode. Note, however, that a local declaration of `@Autowired` or
|
||||
`@jakarta.inject.Inject` on a constructor takes precedence over both `@TestConstructor`
|
||||
and the default mode.
|
||||
|
||||
.Changing the default test constructor autowire mode
|
||||
[TIP]
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
[[integration-testing-annotations-junit4]]
|
||||
= Spring JUnit 4 Testing Annotations
|
||||
|
||||
[WARNING]
|
||||
====
|
||||
JUnit 4 support is deprecated since Spring Framework 7.0 in favor of the
|
||||
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[`SpringExtension`]
|
||||
and JUnit Jupiter.
|
||||
====
|
||||
|
||||
The following annotations are supported only when used in conjunction with the
|
||||
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-runner[SpringRunner],
|
||||
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's JUnit 4 rules], or
|
||||
@@ -206,6 +213,3 @@ Kotlin::
|
||||
----
|
||||
<1> Repeat this test ten times.
|
||||
======
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -483,6 +483,13 @@ Kotlin::
|
||||
[[testcontext-junit4-runner]]
|
||||
=== Spring JUnit 4 Runner
|
||||
|
||||
[WARNING]
|
||||
====
|
||||
JUnit 4 support is deprecated since Spring Framework 7.0 in favor of the
|
||||
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[`SpringExtension`]
|
||||
and JUnit Jupiter.
|
||||
====
|
||||
|
||||
The Spring TestContext Framework offers full integration with JUnit 4 through a custom
|
||||
runner (supported on JUnit 4.12 or higher). By annotating test classes with
|
||||
`@RunWith(SpringJUnit4ClassRunner.class)` or the shorter `@RunWith(SpringRunner.class)`
|
||||
@@ -538,6 +545,13 @@ be configured through `@ContextConfiguration`.
|
||||
[[testcontext-junit4-rules]]
|
||||
=== Spring JUnit 4 Rules
|
||||
|
||||
[WARNING]
|
||||
====
|
||||
JUnit 4 support is deprecated since Spring Framework 7.0 in favor of the
|
||||
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[`SpringExtension`]
|
||||
and JUnit Jupiter.
|
||||
====
|
||||
|
||||
The `org.springframework.test.context.junit4.rules` package provides the following JUnit
|
||||
4 rules (supported on JUnit 4.12 or higher):
|
||||
|
||||
@@ -607,6 +621,13 @@ Kotlin::
|
||||
[[testcontext-support-classes-junit4]]
|
||||
=== JUnit 4 Base Classes
|
||||
|
||||
[WARNING]
|
||||
====
|
||||
JUnit 4 support is deprecated since Spring Framework 7.0 in favor of the
|
||||
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[`SpringExtension`]
|
||||
for JUnit Jupiter.
|
||||
====
|
||||
|
||||
The `org.springframework.test.context.junit4` package provides the following support
|
||||
classes for JUnit 4-based test cases (supported on JUnit 4.12 or higher):
|
||||
|
||||
|
||||
+1
-3
@@ -96,9 +96,7 @@ Kotlin::
|
||||
The following code snippet is similar to the one we saw earlier for a request-scoped
|
||||
bean. However, this time, the `userService` bean has a dependency on a session-scoped
|
||||
`userPreferences` bean. Note that the `UserPreferences` bean is instantiated by using a
|
||||
SpEL expression that retrieves the theme from the current HTTP session. In our test, we
|
||||
need to configure a theme in the mock session managed by the TestContext framework. The
|
||||
following example shows how to do so:
|
||||
SpEL expression that retrieves an attribute from the current HTTP session.
|
||||
|
||||
.Session-scoped bean configuration
|
||||
[source,xml,indent=0,subs="verbatim,quotes"]
|
||||
|
||||
@@ -265,6 +265,7 @@ Java::
|
||||
client = WebTestClient.bindToController(new TestController())
|
||||
.configureClient()
|
||||
.baseUrl("/test")
|
||||
.apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build())
|
||||
.build();
|
||||
----
|
||||
|
||||
@@ -275,6 +276,7 @@ Kotlin::
|
||||
client = WebTestClient.bindToController(TestController())
|
||||
.configureClient()
|
||||
.baseUrl("/test")
|
||||
.apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build())
|
||||
.build()
|
||||
----
|
||||
======
|
||||
|
||||
@@ -804,8 +804,7 @@ Java::
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
ClassPathResource index = new ClassPathResource("static/index.html");
|
||||
List<String> extensions = List.of("js", "css", "ico", "png", "jpg", "gif");
|
||||
RequestPredicate spaPredicate = path("/api/**").or(path("/error")).or(pathExtension(extensions::contains)).negate();
|
||||
RequestPredicate spaPredicate = path("/api/**").or(path("/error")).negate();
|
||||
RouterFunction<ServerResponse> redirectToIndex = route()
|
||||
.resource(spaPredicate, index)
|
||||
.build();
|
||||
@@ -817,9 +816,7 @@ Kotlin::
|
||||
----
|
||||
val redirectToIndex = router {
|
||||
val index = ClassPathResource("static/index.html")
|
||||
val extensions = listOf("js", "css", "ico", "png", "jpg", "gif")
|
||||
val spaPredicate = !(path("/api/**") or path("/error") or
|
||||
pathExtension(extensions::contains))
|
||||
val spaPredicate = !(path("/api/**") or path("/error"))
|
||||
resource(spaPredicate, index)
|
||||
}
|
||||
----
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
[[webflux-versioning]]
|
||||
= API Versioning
|
||||
:page-section-summary-toc: 1
|
||||
|
||||
[.small]#xref:web/webmvc-versioning.adoc[See equivalent in the Servlet stack]#
|
||||
|
||||
Spring WebFlux supports API versioning. This section provides an overview of the support
|
||||
and underlying strategies.
|
||||
|
||||
Please, see also related content in:
|
||||
|
||||
- Use xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-version[API Versions]
|
||||
to map requests to annotated controller methods
|
||||
- Configure API versioning in xref:web/webflux/config.adoc#webflux-config-api-version[WebFlux Config]
|
||||
|
||||
TIP: API versioning is also supported on the client side in `RestClient`, `WebClient`, and
|
||||
xref:integration/rest-clients.adoc#rest-http-interface[HTTP Service] clients, as well as
|
||||
for testing with `WebTestClient`.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-versioning-strategy]]
|
||||
== ApiVersionStrategy
|
||||
[.small]#xref:web/webmvc-versioning.adoc#mvc-versioning-strategy[See equivalent in the Servlet stack]#
|
||||
|
||||
This strategy holds all application preferences about how to manage versioning.
|
||||
It delegates to xref:#webflux-versioning-resolver[ApiVersionResolver] to resolve versions
|
||||
from requests, and to xref:#webflux-versioning-parser[ApiVersionParser] to parse raw version
|
||||
values into `Comparable<?>`. It also helps to xref:#webflux-versioning-validation[validate]
|
||||
request versions.
|
||||
|
||||
NOTE: `ApiVersionStrategy` helps to map requests to `@RequestMapping` controller methods,
|
||||
and is initialized by the WebFlux config. Typically, applications do not interact directly with it.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-versioning-resolver]]
|
||||
== ApiVersionResolver
|
||||
[.small]#xref:web/webmvc-versioning.adoc#mvc-versioning-resolver[See equivalent in the Servlet stack]#
|
||||
|
||||
This strategy resolves the API version from a request. The WebFlux config provides built-in
|
||||
options to resolve from a header, a request parameter, or from the URL path.
|
||||
You can also use a custom `ApiVersionResolver`.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-versioning-parser]]
|
||||
== ApiVersionParser
|
||||
[.small]#xref:web/webmvc-versioning.adoc#mvc-versioning-parser[See equivalent in the Servlet stack]#
|
||||
|
||||
This strategy helps to parse raw version values into `Comparable<?>`, which helps to
|
||||
compare, sort, and select versions. By default, the built-in `SemanticApiVersionParser`
|
||||
parses a version into `major`, `minor`, and `patch` integer values. Minor and patch
|
||||
values are set to 0 if not present.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-versioning-validation]]
|
||||
== Validation
|
||||
[.small]#xref:web/webmvc-versioning.adoc#mvc-versioning-validation[See equivalent in the Servlet stack]#
|
||||
|
||||
If a request version is not supported, `InvalidApiVersionException` is raised resulting
|
||||
in a 400 response. By default, the list of supported versions is initialized from declared
|
||||
versions in annotated controller mappings. You can add to that list, or set it explicitly
|
||||
to a fixed set of versions (i.e. ignoring declared ones) through the MVC config.
|
||||
|
||||
By default, a version is required when API versioning is enabled, but you can turn that
|
||||
off in which case the highest available version is used. You can also specify a default
|
||||
version. `MissingApiVersionException` is raised resulting in a 400 response when a
|
||||
version is required but not present.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-versioning-mapping]]
|
||||
== Request Mapping
|
||||
[.small]#xref:web/webmvc-versioning.adoc#mvc-versioning-mapping[See equivalent in the Servlet stack]#
|
||||
|
||||
`ApiVersionStrategy` supports the mapping of requests to annotated controller methods.
|
||||
See xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-version[API Versions]
|
||||
for more details.
|
||||
@@ -467,7 +467,7 @@ Java::
|
||||
----
|
||||
@GetMapping
|
||||
FragmentsRendering handle() {
|
||||
return FragmentsRendering.with("posts").fragment("comments").build();
|
||||
return FragmentsRendering.fragment("posts").fragment("comments").build();
|
||||
}
|
||||
----
|
||||
|
||||
@@ -477,7 +477,7 @@ Kotlin::
|
||||
----
|
||||
@GetMapping
|
||||
fun handle(): FragmentsRendering {
|
||||
return FragmentsRendering.with("posts").fragment("comments").build()
|
||||
return FragmentsRendering.fragment("posts").fragment("comments").build()
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[[webflux-client-builder]]
|
||||
= Configuration
|
||||
|
||||
The simplest way to create a `WebClient` is through one of the static factory methods:
|
||||
The simplest way to create `WebClient` is through one of the static factory methods:
|
||||
|
||||
* `WebClient.create()`
|
||||
* `WebClient.create(String baseUrl)`
|
||||
@@ -12,10 +12,12 @@ You can also use `WebClient.builder()` with further options:
|
||||
* `defaultUriVariables`: default values to use when expanding URI templates.
|
||||
* `defaultHeader`: Headers for every request.
|
||||
* `defaultCookie`: Cookies for every request.
|
||||
* `defaultApiVersion`: API version for every request.
|
||||
* `defaultRequest`: `Consumer` to customize every request.
|
||||
* `filter`: Client filter for every request.
|
||||
* `exchangeStrategies`: HTTP message reader/writer customizations.
|
||||
* `clientConnector`: HTTP client library settings.
|
||||
* `apiVersionInserter`: to insert API version values in the request
|
||||
* `observationRegistry`: the registry to use for enabling xref:integration/observability.adoc#http-client.webclient[Observability support].
|
||||
* `observationConvention`: xref:integration/observability.adoc#config[an optional, custom convention to extract metadata] for recorded observations.
|
||||
|
||||
|
||||
@@ -647,13 +647,12 @@ For https://www.webjars.org/documentation[WebJars], versioned URLs like
|
||||
`/webjars/jquery/1.2.0/jquery.min.js` are the recommended and most efficient way to use them.
|
||||
The related resource location is configured out of the box with Spring Boot (or can be configured
|
||||
manually via `ResourceHandlerRegistry`) and does not require to add the
|
||||
`org.webjars:webjars-locator-core` dependency.
|
||||
`org.webjars:webjars-locator-lite` dependency.
|
||||
|
||||
Version-less URLs like `/webjars/jquery/jquery.min.js` are supported through the
|
||||
`WebJarsResourceResolver` which is automatically registered when the
|
||||
`org.webjars:webjars-locator-core` library is present on the classpath, at the cost of a
|
||||
classpath scanning that could slow down application startup. The resolver can re-write URLs to
|
||||
include the version of the jar and can also match against incoming URLs without versions
|
||||
`org.webjars:webjars-locator-lite` library is present on the classpath. The resolver can re-write
|
||||
URLs to include the version of the jar and can also match against incoming URLs without versions
|
||||
-- for example, from `/webjars/jquery/jquery.min.js` to `/webjars/jquery/1.2.0/jquery.min.js`.
|
||||
|
||||
TIP: The Java configuration based on `ResourceHandlerRegistry` provides further options
|
||||
@@ -687,6 +686,63 @@ reliance on it.
|
||||
|
||||
|
||||
|
||||
[[webflux-config-api-version]]
|
||||
== API Version
|
||||
[.small]#xref:web/webmvc/mvc-config/api-version.adoc[See equivalent in the Servlet stack]#
|
||||
|
||||
To enable API versioning with a request header, use the following:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim"]
|
||||
----
|
||||
@Configuration
|
||||
public class WebConfiguration implements WebFluxConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureApiVersioning(ApiVersionConfigurer configurer) {
|
||||
configurer.useRequestHeader("X-API-Version");
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim"]
|
||||
----
|
||||
@Configuration
|
||||
class WebConfiguration : WebMvcConfigurer {
|
||||
|
||||
override fun configureApiVersioning(configurer: ApiVersionConfigurer) {
|
||||
configurer.useRequestHeader("X-API-Version")
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
Alternatively, the version can be resolved from a request parameter, from a path segment,
|
||||
or through a custom `ApiVersionResolver`.
|
||||
|
||||
TIP: When resolving from a path segment, consider configuring a path prefix once in
|
||||
xref:web/webmvc/mvc-config/path-matching.adoc[Path Matching] options.
|
||||
|
||||
Raw version values are parsed with `SemanticVersionParser` by default, but you can use
|
||||
a custom xref:web/webflux-versioning.adoc#webflux-versioning-parser[ApiVersionParser].
|
||||
|
||||
"Supported" versions are transparently detected from versions declared in request mappings
|
||||
for convenience, but you can also set the list of supported versions explicitly, and
|
||||
ignore declared ones. Requests with a version that is not supported are rejected with an
|
||||
`InvalidApiVersionException` resulting in a 400 response.
|
||||
|
||||
Once API versioning is configured, you can begin to map requests to
|
||||
xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-version[controller methods]
|
||||
according to the request version.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-config-blocking-execution]]
|
||||
== Blocking Execution
|
||||
|
||||
|
||||
@@ -408,6 +408,85 @@ Kotlin::
|
||||
======
|
||||
|
||||
|
||||
[[webflux-ann-requestmapping-version]]
|
||||
== API Version
|
||||
[.small]#xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-version[See equivalent in the Servlet stack]#
|
||||
|
||||
There is no standard way to specify an API version, so you need to configure that first
|
||||
through the xref:web/webflux/config.adoc#webflux-config-api-version[WebFlux Config] along with other
|
||||
config options. This results in the creation of an
|
||||
xref:web/webflux-versioning.adoc#webflux-versioning-strategy[ApiVersionStrategy] that in
|
||||
supports request mapping.
|
||||
|
||||
Once API versioning is enabled, you can begin to map requests with versions.
|
||||
The `@RequestMapping` version attribute supports the following:
|
||||
|
||||
- No value -- match any version
|
||||
- Fixed version ("1.2") -- match the given version only
|
||||
- Baseline version ("1.2+") -- match the given version and above
|
||||
|
||||
If multiple controller methods have a version less than or equal to the request version,
|
||||
the one closest to the request version is considered for mapping purposes,
|
||||
in effect superseding the rest.
|
||||
|
||||
To illustrate this, consider the following controller mappings:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@RestController
|
||||
@RequestMapping("/account/{id}")
|
||||
public class AccountController {
|
||||
|
||||
@GetMapping // <1>
|
||||
public Account getAccount() {
|
||||
}
|
||||
|
||||
@GetMapping(version = "1.1") // <2>
|
||||
public Account getAccount1_1() {
|
||||
}
|
||||
|
||||
@GetMapping(version = "1.2+") // <3>
|
||||
public Account getAccount1_2() {
|
||||
}
|
||||
|
||||
@GetMapping(version = "1.5") // <4>
|
||||
public Account getAccount1_5() {
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> match any version
|
||||
<2> match version 1.1
|
||||
<3> match version 1.2 and above
|
||||
<4> match version 1.5
|
||||
======
|
||||
|
||||
For request with version `"1.3"`:
|
||||
|
||||
- (1) matches as it matches any version
|
||||
- (2) does not match
|
||||
- (3) matches as it matches 1.2 and above, and is *chosen* as the highest match
|
||||
- (4) is higher and does not match
|
||||
|
||||
For request with version `"1.5"`:
|
||||
|
||||
- (1) matches as it matches any version
|
||||
- (2) does not match
|
||||
- (3) matches as it matches 1.2 and above
|
||||
- (4) matches and is *chosen* as the highest match
|
||||
|
||||
A request with version `"1.6"` does not have a match. (1) and (3) do match, but are
|
||||
superseded by (4), which does not match. In this scenario, `NotAcceptableApiVersionException`
|
||||
is raised resulting in a 400 response.
|
||||
|
||||
NOTE: The above assumes the request version is a "supported" versions. If not it would
|
||||
fail xref:web/webflux-versioning.adoc#webflux-versioning-validation[Validation].
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-ann-requestmapping-head-options]]
|
||||
== HTTP HEAD, OPTIONS
|
||||
|
||||
@@ -783,7 +783,7 @@ Java::
|
||||
----
|
||||
ClassPathResource index = new ClassPathResource("static/index.html");
|
||||
List<String> extensions = List.of("js", "css", "ico", "png", "jpg", "gif");
|
||||
RequestPredicate spaPredicate = path("/api/**").or(path("/error")).or(pathExtension(extensions::contains)).negate();
|
||||
RequestPredicate spaPredicate = path("/api/**").or(path("/error")).negate();
|
||||
RouterFunction<ServerResponse> redirectToIndex = route()
|
||||
.resource(spaPredicate, index)
|
||||
.build();
|
||||
@@ -795,9 +795,7 @@ Kotlin::
|
||||
----
|
||||
val redirectToIndex = router {
|
||||
val index = ClassPathResource("static/index.html")
|
||||
val extensions = listOf("js", "css", "ico", "png", "jpg", "gif")
|
||||
val spaPredicate = !(path("/api/**") or path("/error") or
|
||||
pathExtension(extensions::contains))
|
||||
val spaPredicate = !(path("/api/**") or path("/error"))
|
||||
resource(spaPredicate, index)
|
||||
}
|
||||
----
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
[[mvc-versioning]]
|
||||
= API Versioning
|
||||
:page-section-summary-toc: 1
|
||||
|
||||
[.small]#xref:web/webflux-versioning.adoc[See equivalent in the Reactive stack]#
|
||||
|
||||
Spring MVC supports API versioning. This section provides an overview of the support
|
||||
and underlying strategies.
|
||||
|
||||
Please, see also related content in:
|
||||
|
||||
- Use xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-version[API Version]
|
||||
to map requests to annotated controller methods
|
||||
- Configure API versioning in xref:web/webmvc/mvc-config/api-version.adoc[MVC Config]
|
||||
|
||||
TIP: API versioning is also supported on the client side in `RestClient`, `WebClient`, and
|
||||
xref:integration/rest-clients.adoc#rest-http-interface[HTTP Service] clients, as well as
|
||||
for testing with `WebTestClient`.
|
||||
|
||||
|
||||
|
||||
|
||||
[[mvc-versioning-strategy]]
|
||||
== ApiVersionStrategy
|
||||
[.small]#xref:web/webflux-versioning.adoc#webflux-versioning-strategy[See equivalent in the Reactive stack]#
|
||||
|
||||
This strategy holds all application preferences about how to manage versioning.
|
||||
It delegates to xref:#mvc-versioning-resolver[ApiVersionResolver] to resolve versions
|
||||
from requests, and to xref:#mvc-versioning-parser[ApiVersionParser] to parse raw version
|
||||
values into `Comparable<?>`. It also helps to xref:#mvc-versioning-validation[validate]
|
||||
request versions.
|
||||
|
||||
NOTE: `ApiVersionStrategy` helps to map requests to `@RequestMapping` controller methods,
|
||||
and is initialized by the MVC config. Typically, applications do not interact directly with it.
|
||||
|
||||
|
||||
|
||||
|
||||
[[mvc-versioning-resolver]]
|
||||
== ApiVersionResolver
|
||||
[.small]#xref:web/webmvc-versioning.adoc#mvc-versioning-resolver[See equivalent in the Reactive stack]#
|
||||
|
||||
This strategy resolves the API version from a request. The MVC config provides built-in
|
||||
options to resolve from a header, from a request parameter, or from the URL path.
|
||||
You can also use a custom `ApiVersionResolver`.
|
||||
|
||||
|
||||
|
||||
|
||||
[[mvc-versioning-parser]]
|
||||
== ApiVersionParser
|
||||
[.small]#xref:web/webflux-versioning.adoc#webflux-versioning-parser[See equivalent in the Reactive stack]#
|
||||
|
||||
This strategy helps to parse raw version values into `Comparable<?>`, which helps to
|
||||
compare, sort, and select versions. By default, the built-in `SemanticApiVersionParser`
|
||||
parses a version into `major`, `minor`, and `patch` integer values. Minor and patch
|
||||
values are set to 0 if not present.
|
||||
|
||||
|
||||
|
||||
|
||||
[[mvc-versioning-validation]]
|
||||
== Validation
|
||||
[.small]#xref:web/webflux-versioning.adoc#webflux-versioning-validation[See equivalent in the Reactive stack]#
|
||||
|
||||
If a request version is not supported, `InvalidApiVersionException` is raised resulting
|
||||
in a 400 response. By default, the list of supported versions is initialized from declared
|
||||
versions in annotated controller mappings. You can add to that list, or set it explicitly
|
||||
to a fixed set of versions (i.e. ignoring declared ones) through the MVC config.
|
||||
|
||||
By default, a version is required when API versioning is enabled, but you can turn that
|
||||
off in which case the highest available version is used. You can also specify a default
|
||||
version. `MissingApiVersionException` is raised resulting in a 400 response when a
|
||||
version is required but not present.
|
||||
|
||||
|
||||
|
||||
|
||||
[[mvc-versioning-mapping]]
|
||||
== Request Mapping
|
||||
[.small]#xref:web/webflux-versioning.adoc#webflux-versioning-mapping[See equivalent in the Reactive stack]#
|
||||
|
||||
`ApiVersionStrategy` supports the mapping of requests to annotated controller methods.
|
||||
See xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-version[API Version]
|
||||
for more details.
|
||||
@@ -48,7 +48,7 @@ Java::
|
||||
----
|
||||
@GetMapping
|
||||
FragmentsRendering handle() {
|
||||
return FragmentsRendering.with("posts").fragment("comments").build();
|
||||
return FragmentsRendering.fragment("posts").fragment("comments").build();
|
||||
}
|
||||
----
|
||||
|
||||
@@ -58,7 +58,7 @@ Kotlin::
|
||||
----
|
||||
@GetMapping
|
||||
fun handle(): FragmentsRendering {
|
||||
return FragmentsRendering.with("posts").fragment("comments").build()
|
||||
return FragmentsRendering.fragment("posts").fragment("comments").build()
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
@@ -118,12 +118,12 @@ See the sections on xref:web/webmvc-cors.adoc[CORS] and the xref:web/webmvc-cors
|
||||
[.small]#xref:web/webflux/reactive-spring.adoc#filters.url-handler[See equivalent in the Reactive stack]#
|
||||
|
||||
In previous Spring Framework versions, Spring MVC could be configured to ignore trailing slashes in URL paths
|
||||
when mapping incoming requests on controller methods. This could be done by enabling the `setUseTrailingSlashMatch`
|
||||
option on the `PathMatchConfigurer`. This means that sending a "GET /home/" request would be handled by a controller
|
||||
method annotated with `@GetMapping("/home")`.
|
||||
when mapping incoming requests on controller methods. This means that sending a "GET /home/" request would be
|
||||
handled by a controller method annotated with `@GetMapping("/home")`.
|
||||
|
||||
This option has been retired, but applications are still expected to handle such requests in a safe way.
|
||||
The `UrlHandlerFilter` Servlet filter has been designed for this purpose. It can be configured to:
|
||||
This option was deprecated in 6.0 and removed in 7.0, but applications are still expected to handle such
|
||||
requests in a safe way. The `UrlHandlerFilter` Servlet filter has been designed for this purpose.
|
||||
It can be configured to:
|
||||
|
||||
* respond with an HTTP redirect status when receiving URLs with trailing slashes, sending browsers to the non-trailing slash URL variant.
|
||||
* wrap the request to act as if the request was sent without a trailing slash and continue the processing of the request.
|
||||
|
||||
@@ -54,6 +54,11 @@ You can customize XML mapping as needed through the use of JAXB or Jackson's pro
|
||||
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`. This requires the `com.fasterxml.jackson.dataformat:jackson-dataformat-xml` dependency.
|
||||
|
||||
| `KotlinSerializationJsonHttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write JSON using `kotlinx.serialization`.
|
||||
This converter is not configured by default, as this conflicts with Jackson.
|
||||
Developers must configure it as an additional converter ahead of the Jackson one.
|
||||
|
||||
| `MappingJackson2CborHttpMessageConverter`
|
||||
| `com.fasterxml.jackson.dataformat:jackson-dataformat-cbor`
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
[[mvc-config-api-version]]
|
||||
= API Version
|
||||
|
||||
[.small]#xref:web/webflux/config.adoc#webflux-config-api-version[See equivalent in the Reactive stack]#
|
||||
|
||||
To enable API versioning with a request header, use the following:
|
||||
|
||||
include-code::./WebConfiguration[tag=snippet,indent=0]
|
||||
|
||||
Alternatively, the version can be resolved from a request parameter, from a path segment,
|
||||
or through a custom `ApiVersionResolver`.
|
||||
|
||||
TIP: When resolving from a path segment, consider configuring a path prefix once in
|
||||
xref:web/webmvc/mvc-config/path-matching.adoc[Path Matching] options.
|
||||
|
||||
Raw version values are parsed with `SemanticVersionParser` by default, but you can use
|
||||
a custom xref:web/webmvc-versioning.adoc#mvc-versioning-parser[ApiVersionParser].
|
||||
|
||||
"Supported" versions are transparently detected from versions declared in request mappings
|
||||
for convenience, but you can also set the list of supported versions explicitly, and
|
||||
ignore declared ones. Requests with a version that is not supported are rejected with an
|
||||
`InvalidApiVersionException` resulting in a 400 response.
|
||||
|
||||
Once API versioning is configured, you can begin to map requests to
|
||||
xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-version[controller methods]
|
||||
according to the request version.
|
||||
@@ -7,6 +7,10 @@ You can use the `@EnableWebMvc` annotation to enable MVC configuration with prog
|
||||
|
||||
include-code::./WebConfiguration[tag=snippet,indent=0]
|
||||
|
||||
WARNING: As of 7.0, support for the XML configuration namespace for Spring MVC has been deprecated.
|
||||
There are no plans yet for removing it completely but XML configuration will not be updated to follow
|
||||
the Java configuration model.
|
||||
|
||||
NOTE: When using Spring Boot, you may want to use `@Configuration` classes of type `WebMvcConfigurer` but without `@EnableWebMvc` to keep Spring Boot MVC customizations. See more details in xref:web/webmvc/mvc-config/customize.adoc[the MVC Config API section] and in {spring-boot-docs-ref}/web/servlet.html#web.servlet.spring-mvc.auto-configuration[the dedicated Spring Boot documentation].
|
||||
|
||||
The preceding example registers a number of Spring MVC
|
||||
|
||||
@@ -48,13 +48,12 @@ For https://www.webjars.org/documentation[WebJars], versioned URLs like
|
||||
`/webjars/jquery/1.2.0/jquery.min.js` are the recommended and most efficient way to use them.
|
||||
The related resource location is configured out of the box with Spring Boot (or can be configured
|
||||
manually via `ResourceHandlerRegistry`) and does not require to add the
|
||||
`org.webjars:webjars-locator-core` dependency.
|
||||
`org.webjars:webjars-locator-lite` dependency.
|
||||
|
||||
Version-less URLs like `/webjars/jquery/jquery.min.js` are supported through the
|
||||
`WebJarsResourceResolver` which is automatically registered when the
|
||||
`org.webjars:webjars-locator-core` library is present on the classpath, at the cost of a
|
||||
classpath scanning that could slow down application startup. The resolver can re-write URLs to
|
||||
include the version of the jar and can also match against incoming URLs without versions
|
||||
`org.webjars:webjars-locator-lite` library is present on the classpath. The resolver can re-write
|
||||
URLs to include the version of the jar and can also match against incoming URLs without versions
|
||||
-- for example, from `/webjars/jquery/jquery.min.js` to `/webjars/jquery/1.2.0/jquery.min.js`.
|
||||
|
||||
TIP: The Java configuration based on `ResourceHandlerRegistry` provides further options
|
||||
|
||||
+1
-2
@@ -30,8 +30,7 @@ and others) and is equivalent to `required=false`.
|
||||
|
||||
| `jakarta.servlet.http.PushBuilder`
|
||||
| Servlet 4.0 push builder API for programmatic HTTP/2 resource pushes.
|
||||
Note that, per the Servlet specification, the injected `PushBuilder` instance can be null if the client
|
||||
does not support that HTTP/2 feature.
|
||||
Note that this API has been deprecated as of Servlet 6.1.
|
||||
|
||||
| `java.security.Principal`
|
||||
| Currently authenticated user -- possibly a specific `Principal` implementation class if known.
|
||||
|
||||
+1
-3
@@ -157,9 +157,7 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
Note that you need to enable the use of matrix variables. In the MVC Java configuration,
|
||||
you need to set a `UrlPathHelper` with `removeSemicolonContent=false` through
|
||||
xref:web/webmvc/mvc-config/path-matching.adoc[Path Matching]. In the MVC XML namespace, you can set
|
||||
Note that you need to enable the use of matrix variables. In the MVC XML namespace, you can set
|
||||
`<mvc:annotation-driven enable-matrix-variables="true"/>`.
|
||||
|
||||
|
||||
|
||||
+80
-30
@@ -248,36 +248,6 @@ specific than other pattern that do not have double wildcards.
|
||||
For the full details, follow the above links to the pattern Comparators.
|
||||
|
||||
|
||||
[[mvc-ann-requestmapping-suffix-pattern-match]]
|
||||
== Suffix Match
|
||||
|
||||
Starting in 5.3, by default Spring MVC no longer performs `.{asterisk}` suffix pattern
|
||||
matching where a controller mapped to `/person` is also implicitly mapped to
|
||||
`/person.{asterisk}`. As a consequence path extensions are no longer used to interpret
|
||||
the requested content type for the response -- for example, `/person.pdf`, `/person.xml`,
|
||||
and so on.
|
||||
|
||||
Using file extensions in this way was necessary when browsers used to send `Accept` headers
|
||||
that were hard to interpret consistently. At present, that is no longer a necessity and
|
||||
using the `Accept` header should be the preferred choice.
|
||||
|
||||
Over time, the use of file name extensions has proven problematic in a variety of ways.
|
||||
It can cause ambiguity when overlain with the use of URI variables, path parameters, and
|
||||
URI encoding. Reasoning about URL-based authorization
|
||||
and security (see next section for more details) also becomes more difficult.
|
||||
|
||||
To completely disable the use of path extensions in versions prior to 5.3, set the following:
|
||||
|
||||
* `useSuffixPatternMatching(false)`, see xref:web/webmvc/mvc-config/path-matching.adoc[PathMatchConfigurer]
|
||||
* `favorPathExtension(false)`, see xref:web/webmvc/mvc-config/content-negotiation.adoc[ContentNegotiationConfigurer]
|
||||
|
||||
Having a way to request content types other than through the `"Accept"` header can still
|
||||
be useful, for example, when typing a URL in a browser. A safe alternative to path extensions is
|
||||
to use the query parameter strategy. If you must use file extensions, consider restricting
|
||||
them to a list of explicitly registered extensions through the `mediaTypes` property of
|
||||
xref:web/webmvc/mvc-config/content-negotiation.adoc[ContentNegotiationConfigurer].
|
||||
|
||||
|
||||
[[mvc-ann-requestmapping-rfd]]
|
||||
== Suffix Match and RFD
|
||||
|
||||
@@ -459,6 +429,86 @@ xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-co
|
||||
instead.
|
||||
|
||||
|
||||
[[mvc-ann-requestmapping-version]]
|
||||
== API Version
|
||||
[.small]#xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-version[See equivalent in the Reactive stack]#
|
||||
|
||||
There is no standard way to specify an API version, so you need to configure that first
|
||||
through the xref:web/webmvc/mvc-config/api-version.adoc[MVC Config] along with other
|
||||
config options. This results in the creation of an
|
||||
xref:web/webmvc-versioning.adoc#mvc-versioning-strategy[ApiVersionStrategy] that in turn
|
||||
supports request mapping.
|
||||
|
||||
Once API versioning is enabled, you can begin to map requests with versions.
|
||||
The `@RequestMapping` version attribute supports the following:
|
||||
|
||||
- No value -- match any version
|
||||
- Fixed version ("1.2") -- match the given version only
|
||||
- Baseline version ("1.2+") -- match the given version and above
|
||||
|
||||
If multiple controller methods have a version less than or equal to the request version,
|
||||
the one closest to the request version is considered for mapping purposes,
|
||||
in effect superseding the rest.
|
||||
|
||||
To illustrate this, consider the following controller mappings:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@RestController
|
||||
@RequestMapping("/account/{id}")
|
||||
public class AccountController {
|
||||
|
||||
@GetMapping // <1>
|
||||
public Account getAccount() {
|
||||
}
|
||||
|
||||
@GetMapping(version = "1.1") // <2>
|
||||
public Account getAccount1_1() {
|
||||
}
|
||||
|
||||
@GetMapping(version = "1.2+") // <3>
|
||||
public Account getAccount1_2() {
|
||||
}
|
||||
|
||||
@GetMapping(version = "1.5") // <4>
|
||||
public Account getAccount1_5() {
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> match any version
|
||||
<2> match version 1.1
|
||||
<3> match version 1.2 and above
|
||||
<4> match version 1.5
|
||||
======
|
||||
|
||||
For request with version `"1.3"`:
|
||||
|
||||
- (1) matches as it matches any version
|
||||
- (2) does not match
|
||||
- (3) matches as it matches 1.2 and above, and is *chosen* as the highest match
|
||||
- (4) is higher and does not match
|
||||
|
||||
For request with version `"1.5"`:
|
||||
|
||||
- (1) matches as it matches any version
|
||||
- (2) does not match
|
||||
- (3) matches as it matches 1.2 and above
|
||||
- (4) matches and is *chosen* as the highest match
|
||||
|
||||
A request with version `"1.6"` does not have a match. (1) and (3) do match, but are
|
||||
superseded by (4), which does not match. In this scenario, `NotAcceptableApiVersionException`
|
||||
is raised resulting in a 400 response.
|
||||
|
||||
NOTE: The above assumes the request version is a "supported" versions. If not it would
|
||||
fail xref:web/webmvc-versioning.adoc#mvc-versioning-validation[Validation].
|
||||
|
||||
|
||||
|
||||
|
||||
[[mvc-ann-requestmapping-head-options]]
|
||||
== HTTP HEAD, OPTIONS
|
||||
[.small]#xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-head-options[See equivalent in the Reactive stack]#
|
||||
|
||||
@@ -4,12 +4,8 @@
|
||||
|
||||
[.small]#xref:web/webflux/http2.adoc[See equivalent in the Reactive stack]#
|
||||
|
||||
Servlet 4 containers are required to support HTTP/2, and Spring Framework 5 is compatible
|
||||
with Servlet API 4. From a programming model perspective, there is nothing specific that
|
||||
Servlet 4 containers are required to support HTTP/2, and Spring Framework requires
|
||||
Servlet API 6.1. From a programming model perspective, there is nothing specific that
|
||||
applications need to do. However, there are considerations related to server configuration.
|
||||
For more details, see the
|
||||
{spring-framework-wiki}/HTTP-2-support[HTTP/2 wiki page].
|
||||
|
||||
The Servlet API does expose one construct related to HTTP/2. You can use the
|
||||
`jakarta.servlet.http.PushBuilder` to proactively push resources to clients, and it
|
||||
is supported as a xref:web/webmvc/mvc-controller/ann-methods/arguments.adoc[method argument] to `@RequestMapping` methods.
|
||||
|
||||
@@ -11,8 +11,6 @@ The `DispatcherServlet` processes requests as follows:
|
||||
* The locale resolver is bound to the request to let elements in the process
|
||||
resolve the locale to use when processing the request (rendering the view, preparing
|
||||
data, and so on). If you do not need locale resolving, you do not need the locale resolver.
|
||||
* The theme resolver is bound to the request to let elements such as views determine
|
||||
which theme to use. If you do not use themes, you can ignore it.
|
||||
* If you specify a multipart file resolver, the request is inspected for multiparts. If
|
||||
multiparts are found, the request is wrapped in a `MultipartHttpServletRequest` for
|
||||
further processing by other elements in the process. See xref:web/webmvc/mvc-servlet/multipart.adoc[Multipart Resolver] for further
|
||||
|
||||
@@ -43,10 +43,6 @@ The following table lists the special beans detected by the `DispatcherServlet`:
|
||||
| Resolve the `Locale` a client is using and possibly their time zone, in order to be able
|
||||
to offer internationalized views. See xref:web/webmvc/mvc-servlet/localeresolver.adoc[Locale].
|
||||
|
||||
| xref:web/webmvc/mvc-servlet/themeresolver.adoc[`ThemeResolver`]
|
||||
| Resolve themes your web application can use -- for example, to offer personalized layouts.
|
||||
See xref:web/webmvc/mvc-servlet/themeresolver.adoc[Themes].
|
||||
|
||||
| xref:web/webmvc/mvc-servlet/multipart.adoc[`MultipartResolver`]
|
||||
| Abstraction for parsing a multi-part request (for example, browser form file upload) with
|
||||
the help of some multipart parsing library. See xref:web/webmvc/mvc-servlet/multipart.adoc[Multipart Resolver].
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
[[mvc-themeresolver]]
|
||||
= Themes
|
||||
|
||||
You can apply Spring Web MVC framework themes to set the overall look-and-feel of your
|
||||
application, thereby enhancing user experience. A theme is a collection of static
|
||||
resources, typically style sheets and images, that affect the visual style of the
|
||||
application.
|
||||
|
||||
WARNING: as of 6.0 support for themes has been deprecated theme in favor of using CSS,
|
||||
and without any special support on the server side.
|
||||
|
||||
|
||||
[[mvc-themeresolver-defining]]
|
||||
== Defining a theme
|
||||
|
||||
To use themes in your web application, you must set up an implementation of the
|
||||
`org.springframework.ui.context.ThemeSource` interface. The `WebApplicationContext`
|
||||
interface extends `ThemeSource` but delegates its responsibilities to a dedicated
|
||||
implementation. By default, the delegate is an
|
||||
`org.springframework.ui.context.support.ResourceBundleThemeSource` implementation that
|
||||
loads properties files from the root of the classpath. To use a custom `ThemeSource`
|
||||
implementation or to configure the base name prefix of the `ResourceBundleThemeSource`,
|
||||
you can register a bean in the application context with the reserved name, `themeSource`.
|
||||
The web application context automatically detects a bean with that name and uses it.
|
||||
|
||||
When you use the `ResourceBundleThemeSource`, a theme is defined in a simple properties
|
||||
file. The properties file lists the resources that make up the theme, as the following example shows:
|
||||
|
||||
[literal,subs="verbatim,quotes"]
|
||||
----
|
||||
styleSheet=/themes/cool/style.css
|
||||
background=/themes/cool/img/coolBg.jpg
|
||||
----
|
||||
|
||||
The keys of the properties are the names that refer to the themed elements from view
|
||||
code. For a JSP, you typically do this using the `spring:theme` custom tag, which is
|
||||
very similar to the `spring:message` tag. The following JSP fragment uses the theme
|
||||
defined in the previous example to customize the look and feel:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="<spring:theme code='styleSheet'/>" type="text/css"/>
|
||||
</head>
|
||||
<body style="background=<spring:theme code='background'/>">
|
||||
...
|
||||
</body>
|
||||
</html>
|
||||
----
|
||||
|
||||
By default, the `ResourceBundleThemeSource` uses an empty base name prefix. As a result,
|
||||
the properties files are loaded from the root of the classpath. Thus, you would put the
|
||||
`cool.properties` theme definition in a directory at the root of the classpath (for
|
||||
example, in `/WEB-INF/classes`). The `ResourceBundleThemeSource` uses the standard Java
|
||||
resource bundle loading mechanism, allowing for full internationalization of themes. For
|
||||
example, we could have a `/WEB-INF/classes/cool_nl.properties` that references a special
|
||||
background image with Dutch text on it.
|
||||
|
||||
|
||||
[[mvc-themeresolver-resolving]]
|
||||
== Resolving Themes
|
||||
|
||||
After you define themes, as described in the xref:web/webmvc/mvc-servlet/themeresolver.adoc#mvc-themeresolver-defining[preceding section],
|
||||
you decide which theme to use. The `DispatcherServlet` looks for a bean named `themeResolver`
|
||||
to find out which `ThemeResolver` implementation to use. A theme resolver works in much the same
|
||||
way as a `LocaleResolver`. It detects the theme to use for a particular request and can also
|
||||
alter the request's theme. The following table describes the theme resolvers provided by Spring:
|
||||
|
||||
[[mvc-theme-resolver-impls-tbl]]
|
||||
.ThemeResolver implementations
|
||||
[cols="1,4"]
|
||||
|===
|
||||
| Class | Description
|
||||
|
||||
| `FixedThemeResolver`
|
||||
| Selects a fixed theme, set by using the `defaultThemeName` property.
|
||||
|
||||
| `SessionThemeResolver`
|
||||
| The theme is maintained in the user's HTTP session. It needs to be set only once for
|
||||
each session but is not persisted between sessions.
|
||||
|
||||
| `CookieThemeResolver`
|
||||
| The selected theme is stored in a cookie on the client.
|
||||
|===
|
||||
|
||||
Spring also provides a `ThemeChangeInterceptor` that lets theme changes on every
|
||||
request with a simple request parameter.
|
||||
|
||||
|
||||
|
||||
@@ -85,10 +85,7 @@ for all HTTP processing -- including WebSocket handshake and all other HTTP
|
||||
requests -- such as Spring MVC's `DispatcherServlet`.
|
||||
|
||||
This is a significant limitation of JSR-356 that Spring's WebSocket support addresses with
|
||||
server-specific `RequestUpgradeStrategy` implementations even when running in a JSR-356 runtime.
|
||||
Such strategies currently exist for Tomcat, Jetty, GlassFish, WebLogic, WebSphere, and Undertow
|
||||
(and WildFly). As of Jakarta WebSocket 2.1, a standard request upgrade strategy is available
|
||||
which Spring chooses on Jakarta EE 10 based web containers such as Tomcat 10.1 and Jetty 12.
|
||||
a standard `RequestUpgradeStrategy` implementation when running in a WebSocket API 2.1+ runtime.
|
||||
|
||||
A secondary consideration is that Servlet containers with JSR-356 support are expected
|
||||
to perform a `ServletContainerInitializer` (SCI) scan that can slow down application
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 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,6 @@ import org.springframework.aot.hint.ExecutableMode;
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.test.agent.EnabledIfRuntimeHintsAgent;
|
||||
import org.springframework.aot.test.agent.RuntimeHintsInvocations;
|
||||
import org.springframework.aot.test.agent.RuntimeHintsRecorder;
|
||||
import org.springframework.core.SpringVersion;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -33,6 +32,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
// method is only enabled if the RuntimeHintsAgent is loaded on the current JVM.
|
||||
// It also tags tests with the "RuntimeHints" JUnit tag.
|
||||
@EnabledIfRuntimeHintsAgent
|
||||
@SuppressWarnings("removal")
|
||||
class SampleReflectionRuntimeHintsTests {
|
||||
|
||||
@Test
|
||||
@@ -43,7 +43,7 @@ class SampleReflectionRuntimeHintsTests {
|
||||
typeHint.withMethod("getVersion", List.of(), ExecutableMode.INVOKE));
|
||||
|
||||
// Invoke the relevant piece of code we want to test within a recording lambda
|
||||
RuntimeHintsInvocations invocations = RuntimeHintsRecorder.record(() -> {
|
||||
RuntimeHintsInvocations invocations = org.springframework.aot.test.agent.RuntimeHintsRecorder.record(() -> {
|
||||
SampleReflection sample = new SampleReflection();
|
||||
sample.performReflection();
|
||||
});
|
||||
|
||||
+1
@@ -25,6 +25,7 @@ import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig;
|
||||
import org.springframework.test.web.servlet.assertj.MockMvcTester;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
// tag::snippet[]
|
||||
@SpringJUnitWebConfig(ApplicationWebConfiguration.class)
|
||||
class AccountControllerIntegrationTests {
|
||||
|
||||
+10
-19
@@ -14,28 +14,19 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.http.client;
|
||||
package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigapiversion;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
|
||||
/**
|
||||
* @author Roy Clarkson
|
||||
*/
|
||||
class OkHttp3ClientHttpRequestFactoryTests extends AbstractHttpRequestFactoryTests {
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
@Override
|
||||
protected ClientHttpRequestFactory createRequestFactory() {
|
||||
return new OkHttp3ClientHttpRequestFactory();
|
||||
}
|
||||
// tag::snippet[]
|
||||
@Configuration
|
||||
public class WebConfiguration implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
@Test
|
||||
void httpMethods() throws Exception {
|
||||
super.httpMethods();
|
||||
assertHttpMethod("patch", HttpMethod.PATCH);
|
||||
public void configureApiVersioning(ApiVersionConfigurer configurer) {
|
||||
configurer.useRequestHeader("X-API-Version");
|
||||
}
|
||||
|
||||
}
|
||||
// end::snippet[]
|
||||
+1
@@ -28,6 +28,7 @@ import org.springframework.http.converter.json.MappingJackson2HttpMessageConvert
|
||||
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
// tag::snippet[]
|
||||
@Configuration
|
||||
public class WebConfiguration implements WebMvcConfigurer {
|
||||
|
||||
+1
@@ -23,6 +23,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
|
||||
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
// tag::snippet[]
|
||||
@Configuration
|
||||
public class FreeMarkerConfiguration implements WebMvcConfigurer {
|
||||
|
||||
+1
@@ -21,6 +21,7 @@ import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
// tag::snippet[]
|
||||
@Configuration
|
||||
public class WebConfiguration implements WebMvcConfigurer {
|
||||
|
||||
+4
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 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,11 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package example.indexed;
|
||||
package org.springframework.docs.core.beans.dependencies.beansfactorylazyinit
|
||||
|
||||
/**
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
@javax.annotation.ManagedBean
|
||||
public class IndexedJavaxManagedBeanComponent {
|
||||
}
|
||||
class AnotherBean {
|
||||
}
|
||||
+4
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 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,11 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package example.scannable;
|
||||
package org.springframework.docs.core.beans.dependencies.beansfactorylazyinit
|
||||
|
||||
/**
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
@javax.inject.Named("myJavaxNamedComponent")
|
||||
public class JavaxNamedComponent {
|
||||
}
|
||||
class ExpensiveToCreateBean {
|
||||
}
|
||||
+4
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 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,11 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package example.indexed;
|
||||
package org.springframework.docs.core.expressions.expressionsbeandef
|
||||
|
||||
/**
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
@jakarta.annotation.ManagedBean
|
||||
public class IndexedJakartaManagedBeanComponent {
|
||||
}
|
||||
class CustomerPreferenceDao {
|
||||
}
|
||||
+5
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 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,11 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package example.indexed;
|
||||
|
||||
/**
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
@javax.inject.Named("myIndexedJavaxNamedComponent")
|
||||
public class IndexedJavaxNamedComponent {
|
||||
}
|
||||
package org.springframework.docs.core.expressions.expressionsbeandef
|
||||
|
||||
class MovieFinder {
|
||||
}
|
||||
+3
-11
@@ -14,16 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.servlet.view;
|
||||
package org.springframework.docs.dataaccess.jdbc.jdbccomplextypes
|
||||
|
||||
/**
|
||||
* @author Rod Johnson
|
||||
*/
|
||||
class ResourceBundleViewResolverNoCacheTests extends ResourceBundleViewResolverTests {
|
||||
import java.util.Date
|
||||
|
||||
@Override
|
||||
protected boolean getCache() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
data class TestItem(val id: Long, val description: String, val expirationDate: Date)
|
||||
+5
-5
@@ -31,11 +31,11 @@ class TestItemStoredProcedure(dataSource: DataSource) : StoredProcedure(dataSour
|
||||
cs: CallableStatement, colIndx: Int, _: Int, _: String? ->
|
||||
val struct = cs.getObject(colIndx) as Struct
|
||||
val attr = struct.attributes
|
||||
val item = TestItem()
|
||||
item.id = (attr[0] as Number).toLong()
|
||||
item.description = attr[1] as String
|
||||
item.expirationDate = attr[2] as Date
|
||||
item
|
||||
TestItem(
|
||||
(attr[0] as Number).toLong(),
|
||||
attr[1] as String,
|
||||
attr[2] as Date
|
||||
)
|
||||
})
|
||||
// ...
|
||||
}
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.dataaccess.jdbc.jdbcjdbctemplateidioms
|
||||
|
||||
interface CorporateEventDao {
|
||||
}
|
||||
+1
-3
@@ -28,9 +28,7 @@ class CustomCacheConfiguration {
|
||||
// tag::snippet[]
|
||||
@Bean
|
||||
fun cacheManager(): CacheManager {
|
||||
return CaffeineCacheManager().apply {
|
||||
cacheNames = listOf("default", "books")
|
||||
}
|
||||
return CaffeineCacheManager("default", "books")
|
||||
}
|
||||
// end::snippet[]
|
||||
}
|
||||
+9
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 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,11 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package example.scannable;
|
||||
package org.springframework.docs.integration.jmx.jmxexporting
|
||||
|
||||
/**
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
@jakarta.annotation.ManagedBean("myJakartaManagedBeanComponent")
|
||||
public class JakartaManagedBeanComponent {
|
||||
}
|
||||
interface IJmxTestBean {
|
||||
|
||||
var name: String
|
||||
var age: Int
|
||||
fun add(x: Int, y: Int): Int
|
||||
fun dontExposeMe()
|
||||
}
|
||||
+2
-18
@@ -19,24 +19,8 @@ package org.springframework.docs.integration.jmx.jmxexporting
|
||||
// tag::snippet[]
|
||||
class JmxTestBean : IJmxTestBean {
|
||||
|
||||
private lateinit var name: String
|
||||
private var age = 0
|
||||
|
||||
override fun getAge(): Int {
|
||||
return age
|
||||
}
|
||||
|
||||
override fun setAge(age: Int) {
|
||||
this.age = age
|
||||
}
|
||||
|
||||
override fun setName(name: String) {
|
||||
this.name = name
|
||||
}
|
||||
|
||||
override fun getName(): String {
|
||||
return name
|
||||
}
|
||||
override lateinit var name: String
|
||||
override var age = 0
|
||||
|
||||
override fun add(x: Int, y: Int): Int {
|
||||
return x + y
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.mailusagesimple
|
||||
|
||||
data class Customer(
|
||||
val emailAddress: String,
|
||||
val firstName: String,
|
||||
val lastName: String
|
||||
)
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.mailusagesimple
|
||||
|
||||
data class Order(
|
||||
val customer: Customer,
|
||||
val orderNumber: String
|
||||
)
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.testing.mockmvc.assertj.mockmvctestersetup
|
||||
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@RestController
|
||||
class AccountController {
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.testing.mockmvc.assertj.mockmvctestersetup
|
||||
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableWebMvc
|
||||
class ApplicationWebConfiguration {
|
||||
}
|
||||
+3
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 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,6 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package org.springframework.docs.testing.mockmvc.assertj.mockmvctestersetup.converter
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
|
||||
+11
-11
@@ -14,18 +14,18 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.http.client;
|
||||
package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigapiversion
|
||||
|
||||
/**
|
||||
* Tests for {@link BufferingClientHttpRequestWrapper} for clients
|
||||
* not supporting non-null, empty request bodies for GET requests.
|
||||
*/
|
||||
class BufferingClientHttpRequestFactoryWithOkHttpTests extends AbstractHttpRequestFactoryTests {
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("removal")
|
||||
protected ClientHttpRequestFactory createRequestFactory() {
|
||||
return new BufferingClientHttpRequestFactory(new OkHttp3ClientHttpRequestFactory());
|
||||
// tag::snippet[]
|
||||
@Configuration
|
||||
class WebConfiguration : WebMvcConfigurer {
|
||||
|
||||
override fun configureApiVersioning(configurer: ApiVersionConfigurer) {
|
||||
configurer.useRequestHeader("X-API-Version")
|
||||
}
|
||||
|
||||
}
|
||||
// end::snippet[]
|
||||
+2
-3
@@ -14,14 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:Suppress("DEPRECATION")
|
||||
package org.springframework.docs.web.webmvc.mvcconfig.mvcconfiginterceptors
|
||||
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
|
||||
import org.springframework.web.servlet.handler.UserRoleAuthorizationInterceptor
|
||||
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor
|
||||
import org.springframework.web.servlet.theme.ThemeChangeInterceptor
|
||||
|
||||
// tag::snippet[]
|
||||
@Configuration
|
||||
@@ -29,7 +28,7 @@ class WebConfiguration : WebMvcConfigurer {
|
||||
|
||||
override fun addInterceptors(registry: InterceptorRegistry) {
|
||||
registry.addInterceptor(LocaleChangeInterceptor())
|
||||
registry.addInterceptor(ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**")
|
||||
registry.addInterceptor(UserRoleAuthorizationInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**")
|
||||
}
|
||||
}
|
||||
// end::snippet[]
|
||||
+2
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigmessageconverters
|
||||
|
||||
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.web.webmvc.mvcconfig.mvcconfigvalidation
|
||||
|
||||
import org.springframework.validation.Errors
|
||||
import org.springframework.validation.Validator
|
||||
|
||||
class FooValidator : Validator {
|
||||
override fun supports(clazz: Class<*>) = false
|
||||
|
||||
override fun validate(target: Any, errors: Errors) {
|
||||
}
|
||||
}
|
||||
+2
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigviewresolvers
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
|
||||
+2
@@ -14,6 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigviewresolvers
|
||||
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2002-2024 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.web.websocket.websocketserverruntimeconfiguration
|
||||
|
||||
import org.springframework.web.socket.handler.AbstractWebSocketHandler
|
||||
|
||||
class MyEchoHandler : AbstractWebSocketHandler() {
|
||||
}
|
||||
+3
-1
@@ -14,7 +14,9 @@
|
||||
<mvc:interceptor>
|
||||
<mvc:mapping path="/**"/>
|
||||
<mvc:exclude-mapping path="/admin/**"/>
|
||||
<bean class="org.springframework.web.servlet.theme.ThemeChangeInterceptor"/>
|
||||
<bean class="org.springframework.web.servlet.handler.UserRoleAuthorizationInterceptor">
|
||||
<property name="authorizedRoles" value="ROLE_USER"/>
|
||||
</bean>
|
||||
</mvc:interceptor>
|
||||
</mvc:interceptors>
|
||||
<!-- end::snippet[] -->
|
||||
|
||||
@@ -8,20 +8,20 @@ javaPlatform {
|
||||
|
||||
dependencies {
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.18.4"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.14.7"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.15.0"))
|
||||
api(platform("io.netty:netty-bom:4.1.121.Final"))
|
||||
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
|
||||
api(platform("io.projectreactor:reactor-bom:2024.0.6"))
|
||||
api(platform("io.projectreactor:reactor-bom:2025.0.0-M3"))
|
||||
api(platform("io.rsocket:rsocket-bom:1.1.5"))
|
||||
api(platform("org.apache.groovy:groovy-bom:4.0.26"))
|
||||
api(platform("org.apache.logging.log4j:log4j-bom:2.21.1"))
|
||||
api(platform("org.apache.logging.log4j:log4j-bom:3.0.0-beta3"))
|
||||
api(platform("org.assertj:assertj-bom:3.27.3"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.0.21"))
|
||||
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.21"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.3"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.1.0.alpha2"))
|
||||
api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.0.alpha2"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.2"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.8.1"))
|
||||
api(platform("org.junit:junit-bom:5.12.2"))
|
||||
api(platform("org.mockito:mockito-bom:5.17.0"))
|
||||
api(platform("tools.jackson:jackson-bom:3.0.0-rc4"))
|
||||
|
||||
constraints {
|
||||
api("com.fasterxml:aalto-xml:1.3.2")
|
||||
@@ -30,26 +30,25 @@ dependencies {
|
||||
api("com.github.librepdf:openpdf:1.3.43")
|
||||
api("com.google.code.findbugs:findbugs:3.0.1")
|
||||
api("com.google.code.findbugs:jsr305:3.0.2")
|
||||
api("com.google.code.gson:gson:2.12.1")
|
||||
api("com.google.code.gson:gson:2.13.0")
|
||||
api("com.google.protobuf:protobuf-java-util:4.30.2")
|
||||
api("com.h2database:h2:2.3.232")
|
||||
api("com.jayway.jsonpath:json-path:2.9.0")
|
||||
api("com.networknt:json-schema-validator:1.5.3")
|
||||
api("com.oracle.database.jdbc:ojdbc11:21.9.0.0")
|
||||
api("com.rometools:rome:1.19.0")
|
||||
api("com.squareup.okhttp3:mockwebserver:3.14.9")
|
||||
api("com.squareup.okhttp3:okhttp:3.14.9")
|
||||
api("com.sun.activation:jakarta.activation:2.0.1")
|
||||
api("com.sun.mail:jakarta.mail:2.0.1")
|
||||
api("com.sun.xml.bind:jaxb-core:3.0.2")
|
||||
api("com.sun.xml.bind:jaxb-impl:3.0.2")
|
||||
api("com.sun.xml.bind:jaxb-xjc:3.0.2")
|
||||
api("com.thoughtworks.qdox:qdox:2.2.0")
|
||||
api("com.thoughtworks.xstream:xstream:1.4.21")
|
||||
api("commons-io:commons-io:2.15.0")
|
||||
api("commons-logging:commons-logging:1.3.5")
|
||||
api("de.bechte.junit:junit-hierarchicalcontextrunner:4.12.2")
|
||||
api("io.micrometer:context-propagation:1.1.1")
|
||||
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")
|
||||
api("io.r2dbc:r2dbc-h2:1.0.0.RELEASE")
|
||||
api("io.r2dbc:r2dbc-spi-test:1.0.0.RELEASE")
|
||||
@@ -60,32 +59,31 @@ dependencies {
|
||||
api("io.undertow:undertow-servlet:2.3.18.Final")
|
||||
api("io.undertow:undertow-websockets-jsr:2.3.18.Final")
|
||||
api("io.vavr:vavr:0.10.4")
|
||||
api("jakarta.activation:jakarta.activation-api:2.0.1")
|
||||
api("jakarta.annotation:jakarta.annotation-api:2.0.0")
|
||||
api("jakarta.activation:jakarta.activation-api:2.1.3")
|
||||
api("jakarta.annotation:jakarta.annotation-api:3.0.0")
|
||||
api("jakarta.validation:jakarta.validation-api:3.1.0")
|
||||
api("jakarta.ejb:jakarta.ejb-api:4.0.1")
|
||||
api("jakarta.el:jakarta.el-api:4.0.0")
|
||||
api("jakarta.enterprise.concurrent:jakarta.enterprise.concurrent-api:2.0.0")
|
||||
api("jakarta.faces:jakarta.faces-api:3.0.0")
|
||||
api("jakarta.el:jakarta.el-api:6.0.1")
|
||||
api("jakarta.enterprise.concurrent:jakarta.enterprise.concurrent-api:3.1.1")
|
||||
api("jakarta.faces:jakarta.faces-api:4.1.2")
|
||||
api("jakarta.inject:jakarta.inject-api:2.0.1")
|
||||
api("jakarta.inject:jakarta.inject-tck:2.0.1")
|
||||
api("jakarta.interceptor:jakarta.interceptor-api:2.0.0")
|
||||
api("jakarta.jms:jakarta.jms-api:3.0.0")
|
||||
api("jakarta.json.bind:jakarta.json.bind-api:2.0.0")
|
||||
api("jakarta.json:jakarta.json-api:2.0.1")
|
||||
api("jakarta.mail:jakarta.mail-api:2.0.1")
|
||||
api("jakarta.persistence:jakarta.persistence-api:3.0.0")
|
||||
api("jakarta.resource:jakarta.resource-api:2.0.0")
|
||||
api("jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:3.0.0")
|
||||
api("jakarta.servlet.jsp:jakarta.servlet.jsp-api:3.1.1")
|
||||
api("jakarta.servlet:jakarta.servlet-api:6.0.0")
|
||||
api("jakarta.interceptor:jakarta.interceptor-api:2.2.0")
|
||||
api("jakarta.jms:jakarta.jms-api:3.1.0")
|
||||
api("jakarta.json.bind:jakarta.json.bind-api:3.0.1")
|
||||
api("jakarta.json:jakarta.json-api:2.1.3")
|
||||
api("jakarta.mail:jakarta.mail-api:2.1.3")
|
||||
api("jakarta.persistence:jakarta.persistence-api:3.2.0")
|
||||
api("jakarta.resource:jakarta.resource-api:2.1.0")
|
||||
api("jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:3.0.2")
|
||||
api("jakarta.servlet.jsp:jakarta.servlet.jsp-api:4.0.0")
|
||||
api("jakarta.servlet:jakarta.servlet-api:6.1.0")
|
||||
api("jakarta.transaction:jakarta.transaction-api:2.0.1")
|
||||
api("jakarta.validation:jakarta.validation-api:3.0.2")
|
||||
api("jakarta.websocket:jakarta.websocket-api:2.1.0")
|
||||
api("jakarta.websocket:jakarta.websocket-client-api:2.1.0")
|
||||
api("jakarta.validation:jakarta.validation-api:3.1.0")
|
||||
api("jakarta.websocket:jakarta.websocket-api:2.2.0")
|
||||
api("jakarta.websocket:jakarta.websocket-client-api:2.2.0")
|
||||
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")
|
||||
@@ -103,48 +101,49 @@ dependencies {
|
||||
api("org.apache.httpcomponents.client5:httpclient5:5.4.4")
|
||||
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3.4")
|
||||
api("org.apache.poi:poi-ooxml:5.2.5")
|
||||
api("org.apache.tomcat.embed:tomcat-embed-core:10.1.28")
|
||||
api("org.apache.tomcat.embed:tomcat-embed-websocket:10.1.28")
|
||||
api("org.apache.tomcat:tomcat-util:10.1.28")
|
||||
api("org.apache.tomcat:tomcat-websocket:10.1.28")
|
||||
api("org.aspectj:aspectjrt:1.9.22.1")
|
||||
api("org.aspectj:aspectjtools:1.9.22.1")
|
||||
api("org.aspectj:aspectjweaver:1.9.22.1")
|
||||
api("org.apache.tomcat.embed:tomcat-embed-core:11.0.7")
|
||||
api("org.apache.tomcat.embed:tomcat-embed-websocket:11.0.7")
|
||||
api("org.apache.tomcat:tomcat-util:11.0.7")
|
||||
api("org.apache.tomcat:tomcat-websocket:11.0.7")
|
||||
api("org.aspectj:aspectjrt:1.9.24")
|
||||
api("org.aspectj:aspectjtools:1.9.24")
|
||||
api("org.aspectj:aspectjweaver:1.9.24")
|
||||
api("org.awaitility:awaitility:4.3.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.easymock:easymock:5.5.0")
|
||||
api("org.eclipse.angus:angus-mail:2.0.3")
|
||||
api("org.eclipse.jetty:jetty-reactive-httpclient:4.0.9")
|
||||
api("org.eclipse.persistence:org.eclipse.persistence.jpa:3.0.4")
|
||||
api("org.eclipse:yasson:2.0.4")
|
||||
api("org.eclipse.persistence:org.eclipse.persistence.jpa:5.0.0-B04")
|
||||
api("org.eclipse:yasson:3.0.4")
|
||||
api("org.ehcache:ehcache:3.10.8")
|
||||
api("org.ehcache:jcache:1.0.1")
|
||||
api("org.freemarker:freemarker:2.3.34")
|
||||
api("org.glassfish.external:opendmk_jmxremote_optional_jar:1.0-b01-ea")
|
||||
api("org.glassfish:jakarta.el:4.0.2")
|
||||
api("org.glassfish.tyrus:tyrus-container-servlet:2.1.3")
|
||||
api("org.graalvm.sdk:graal-sdk:22.3.1")
|
||||
api("org.hamcrest:hamcrest:2.2")
|
||||
api("org.hibernate:hibernate-core-jakarta:5.6.15.Final")
|
||||
api("org.hibernate:hibernate-validator:7.0.5.Final")
|
||||
api("org.hamcrest:hamcrest:3.0")
|
||||
api("org.hibernate.orm:hibernate-core:7.0.0.CR1")
|
||||
api("org.hibernate.validator:hibernate-validator:9.0.0.CR1")
|
||||
api("org.hsqldb:hsqldb:2.7.4")
|
||||
api("org.htmlunit:htmlunit:4.10.0")
|
||||
api("org.javamoney:moneta:1.4.4")
|
||||
api("org.jboss.logging:jboss-logging:3.6.1.Final")
|
||||
api("org.jruby:jruby:9.4.12.0")
|
||||
api("org.jspecify:jspecify:1.0.0")
|
||||
api("org.junit.support:testng-engine:1.0.5")
|
||||
api("org.mozilla:rhino:1.7.15")
|
||||
api("org.ogce:xpp3:1.1.6")
|
||||
api("org.python:jython-standalone:2.7.4")
|
||||
api("org.quartz-scheduler:quartz:2.3.2")
|
||||
api("org.reactivestreams:reactive-streams:1.0.4")
|
||||
api("org.seleniumhq.selenium:htmlunit3-driver:4.29.0")
|
||||
api("org.seleniumhq.selenium:selenium-java:4.29.0")
|
||||
api("org.skyscreamer:jsonassert:1.5.3")
|
||||
api("org.slf4j:slf4j-api:2.0.17")
|
||||
api("org.skyscreamer:jsonassert:2.0-rc1")
|
||||
api("org.testng:testng:7.11.0")
|
||||
api("org.webjars:underscorejs:1.8.3")
|
||||
api("org.webjars:webjars-locator-core:0.59")
|
||||
api("org.webjars:webjars-locator-lite:1.1.0")
|
||||
api("org.xmlunit:xmlunit-assertj:2.10.0")
|
||||
api("org.xmlunit:xmlunit-matchers:2.10.0")
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
version=6.2.8-SNAPSHOT
|
||||
version=7.0.0-M5
|
||||
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
org.gradle.parallel=true
|
||||
|
||||
kotlinVersion=1.9.25
|
||||
kotlinVersion=2.1.21
|
||||
|
||||
kotlin.jvm.target.validation.mode=ignore
|
||||
kotlin.stdlib.default.dependency=false
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user