mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-17 16:39:29 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d055fca80 | ||
|
|
d41f546c43 | ||
|
|
e685ff0416 | ||
|
|
4bfbf7d3c8 | ||
|
|
279e6eb423 | ||
|
|
eb65939341 | ||
|
|
3f79b267b1 | ||
|
|
5641d87ce8 | ||
|
|
24dd484471 | ||
|
|
b43972bb54 | ||
|
|
17109b2402 | ||
|
|
dbfece6c31 | ||
|
|
86a101ac2b | ||
|
|
1f544f113a | ||
|
|
3a38bb48b5 | ||
|
|
02d3269dbb | ||
|
|
d81ddcef34 | ||
|
|
701c39a325 | ||
|
|
d77595bf2c | ||
|
|
0b99872704 | ||
|
|
b8b3e6d20c | ||
|
|
ae3bc378d6 | ||
|
|
2ab1c5b387 | ||
|
|
a2f52db452 | ||
|
|
8c2a39b5af | ||
|
|
02d003127f | ||
|
|
57f675c537 | ||
|
|
d89e305c87 | ||
|
|
443e3d5fa6 | ||
|
|
cf75a09011 | ||
|
|
45c20e34e4 | ||
|
|
9c74c25961 | ||
|
|
3228502108 | ||
|
|
94fe1f4c63 | ||
|
|
20d27e4fb6 | ||
|
|
fd5b0e144d | ||
|
|
2c895974b2 | ||
|
|
bfeca4a0bf | ||
|
|
4979d8fded | ||
|
|
f9b6aed6b6 | ||
|
|
156546ad05 | ||
|
|
1a05ba3215 | ||
|
|
3bda2b7124 | ||
|
|
0b902f32f6 | ||
|
|
fb6c325cc0 | ||
|
|
c52bfc0586 | ||
|
|
a33b14338f | ||
|
|
2ede74fa9c | ||
|
|
86ca6fee16 | ||
|
|
9ede1d07a0 | ||
|
|
c65b0a199e | ||
|
|
1e73439955 | ||
|
|
566621f7e3 | ||
|
|
f516431260 | ||
|
|
b4c61f20e7 | ||
|
|
0c477f14cc | ||
|
|
c38f053905 | ||
|
|
88c2a25f12 | ||
|
|
dd76ed7a0a | ||
|
|
837e8960c2 | ||
|
|
8a6c52b018 | ||
|
|
7f561fb53d | ||
|
|
c4896aca9b | ||
|
|
3b093754c8 | ||
|
|
8e16e5ea35 | ||
|
|
dedb58f7ed | ||
|
|
f4b5738869 | ||
|
|
21a007bb15 | ||
|
|
d3d414c3c7 | ||
|
|
3c34e69cc2 | ||
|
|
526fc391ee | ||
|
|
96fd3c10fb | ||
|
|
fe5560400c | ||
|
|
6090eb0b42 | ||
|
|
3804b1c602 | ||
|
|
40b33bca59 | ||
|
|
3253d2de89 | ||
|
|
eaf54b54c3 | ||
|
|
c596ff5c38 | ||
|
|
cc90a956f7 | ||
|
|
6dbd684279 | ||
|
|
faf3c7831f | ||
|
|
59b78cc513 | ||
|
|
14911fb32f | ||
|
|
5115684baf | ||
|
|
6630b16771 | ||
|
|
d1d5b54f12 | ||
|
|
646fd3edcc | ||
|
|
9908967954 | ||
|
|
5fd3456f1e | ||
|
|
3758e5155c | ||
|
|
376f13f8ef | ||
|
|
04cce0bafd | ||
|
|
b80872b762 | ||
|
|
8feb8198fe | ||
|
|
c97def0b98 | ||
|
|
4e863c5a75 | ||
|
|
a6ff95a69c | ||
|
|
f7c3e6480a | ||
|
|
d868f58e6e | ||
|
|
84b3335e71 | ||
|
|
c3e18bc173 | ||
|
|
c942c04aa0 | ||
|
|
ad80b94e14 | ||
|
|
8513ec7440 | ||
|
|
667eb42a63 | ||
|
|
bd23798323 | ||
|
|
961084dfe0 | ||
|
|
3e5aa8d734 | ||
|
|
1bfcaecc9b | ||
|
|
eed14214b5 | ||
|
|
4b7d1e3a2c | ||
|
|
08a99e275e | ||
|
|
44d14811d3 | ||
|
|
900ee11f3b | ||
|
|
89b85c81a7 | ||
|
|
51aaaae94e | ||
|
|
aa10d23de4 | ||
|
|
10610a6f54 | ||
|
|
5e26786bab | ||
|
|
3b1af692cc | ||
|
|
b9ae996dfc | ||
|
|
2d50b758c4 | ||
|
|
450cc212a2 | ||
|
|
a9d100eeee | ||
|
|
dde8f4489f | ||
|
|
f9f7a7cd78 | ||
|
|
6de95a2b37 | ||
|
|
f00756bc7c | ||
|
|
b00d120514 | ||
|
|
2dc4eea62f | ||
|
|
db8fa4d505 | ||
|
|
525621c4d8 | ||
|
|
01e90bbd0e | ||
|
|
af1c06917d | ||
|
|
bcff7d74cd | ||
|
|
170d6bfdad | ||
|
|
2f7046f572 | ||
|
|
3b8dd0a5ac | ||
|
|
9da318af96 | ||
|
|
b45bfcafc2 | ||
|
|
81181c346a | ||
|
|
1378cce9fb | ||
|
|
12f765c133 | ||
|
|
06c6af9b0d | ||
|
|
181c814e69 | ||
|
|
0eda44186a | ||
|
|
48eb477755 | ||
|
|
c43d2e2edc | ||
|
|
0d4010841e | ||
|
|
cc9278666d | ||
|
|
c87925cee7 | ||
|
|
5b6c127283 | ||
|
|
4cd9e2e9b0 | ||
|
|
376223c87d | ||
|
|
ce80637891 | ||
|
|
78d0dbb519 | ||
|
|
85704c890e | ||
|
|
7681200ee7 | ||
|
|
5e4ed68fdd | ||
|
|
49c4b205e4 | ||
|
|
cf1ba98d01 | ||
|
|
cb4222d2c2 | ||
|
|
3437e61f98 | ||
|
|
67e3d86bd8 | ||
|
|
37eaded63d | ||
|
|
ccaccda6ca | ||
|
|
5ebbb3ff3e | ||
|
|
3c02ab83ed | ||
|
|
021161ea38 | ||
|
|
019c34f480 | ||
|
|
b9ba0fc572 | ||
|
|
4a319c3c33 | ||
|
|
52c77d89e9 | ||
|
|
4786e2bf53 | ||
|
|
b53034fe62 | ||
|
|
ba46d2bf21 | ||
|
|
27f9473422 | ||
|
|
f99faac073 | ||
|
|
d65d285378 | ||
|
|
efb6abc43f | ||
|
|
3d57425dcb | ||
|
|
d4caaebab0 | ||
|
|
10cb2322e9 | ||
|
|
9571aa1c68 | ||
|
|
97810c84a2 | ||
|
|
d0076f5c14 | ||
|
|
c110644107 | ||
|
|
05956d4028 | ||
|
|
b2ca36f098 | ||
|
|
15253f3448 | ||
|
|
4e00c988c1 | ||
|
|
c88c9620b3 | ||
|
|
2ac55659c8 | ||
|
|
25ea1f4c0f | ||
|
|
33862d98ea | ||
|
|
c504ac5a47 | ||
|
|
73c06347be | ||
|
|
4becce1c2b | ||
|
|
8a283e39ff | ||
|
|
0d8a8432d1 | ||
|
|
3a8b5111d9 | ||
|
|
b375a061df | ||
|
|
0e9cd2a35f | ||
|
|
d6230824cb | ||
|
|
edba535c8b | ||
|
|
79cf532c2f | ||
|
|
f6b8ee76cd | ||
|
|
635cd29599 | ||
|
|
6f733512b5 | ||
|
|
d6e05ddf2d | ||
|
|
a92bd42236 | ||
|
|
cebda46469 | ||
|
|
f01fb19318 | ||
|
|
5e31856aaa | ||
|
|
1058fed339 | ||
|
|
bcd09d7ad8 | ||
|
|
abc56cde3d | ||
|
|
889fca98ac | ||
|
|
351a200747 | ||
|
|
01ceee8de5 | ||
|
|
0611192dac | ||
|
|
be8bfad3af | ||
|
|
63fe45d92a | ||
|
|
5ce8ffd197 | ||
|
|
56c7b4065d | ||
|
|
680769d770 | ||
|
|
70cf754a2f | ||
|
|
e6d360c1c6 | ||
|
|
8629182822 | ||
|
|
37ac3d8764 | ||
|
|
d61b9c6294 | ||
|
|
d8854a2f3f | ||
|
|
806c83591c | ||
|
|
fd17df91fd | ||
|
|
519927421e | ||
|
|
52c19272c6 | ||
|
|
0d5a7db238 | ||
|
|
064cd3b7af | ||
|
|
6957ac5324 | ||
|
|
d24c131130 | ||
|
|
0ab94478fe | ||
|
|
4b80b0f490 | ||
|
|
793581ebde | ||
|
|
cc7f3101b7 | ||
|
|
49c463b1d2 | ||
|
|
094eb3e236 | ||
|
|
3fed2ec3a1 | ||
|
|
357d5b4e6e | ||
|
|
4152034799 | ||
|
|
3c9cfa8a0f | ||
|
|
b016f385e1 | ||
|
|
f40d1f2329 | ||
|
|
f73f1e0687 | ||
|
|
1b09ef5051 | ||
|
|
c1932dd307 | ||
|
|
490ff0af5e | ||
|
|
16b9640af2 | ||
|
|
2e3fbac9a0 | ||
|
|
18c11e84f3 | ||
|
|
a2da97bc4c | ||
|
|
2e809903ec | ||
|
|
6075629015 | ||
|
|
c20a2e4763 | ||
|
|
e048b093b5 | ||
|
|
57ed5bf34b | ||
|
|
8b77ed164d | ||
|
|
3a8c40fd2f | ||
|
|
3a481a7d7f | ||
|
|
74972fb751 | ||
|
|
15b6626a4c | ||
|
|
c61d011076 | ||
|
|
8448f597b1 | ||
|
|
950c43b4e7 | ||
|
|
39e74d89e1 | ||
|
|
dc4fcef884 | ||
|
|
d04d7b2e57 | ||
|
|
a3e37597aa | ||
|
|
3209cf5c7a | ||
|
|
22376c2efa | ||
|
|
47667ab990 | ||
|
|
068dc7db28 | ||
|
|
3be4c0a893 | ||
|
|
268f3c853e | ||
|
|
bf82ed7186 | ||
|
|
1b27a7baab | ||
|
|
d40c10e00f | ||
|
|
5d022d2a24 | ||
|
|
433b72d493 | ||
|
|
7bc731dfa6 | ||
|
|
5cb6454824 | ||
|
|
9a68b3e910 | ||
|
|
38f9cbe3de | ||
|
|
76f94e998e | ||
|
|
71bb45c87b | ||
|
|
0521cdda88 | ||
|
|
6b1fbc9fe1 | ||
|
|
5243c2262a | ||
|
|
a0c80ffc06 | ||
|
|
a8614531ab | ||
|
|
f0fe58f0ec | ||
|
|
8fb412ea74 | ||
|
|
bf99361abb | ||
|
|
464b676ec5 | ||
|
|
ac11b03cd3 | ||
|
|
df2617d575 | ||
|
|
a03a14797f | ||
|
|
20dd66cd5a | ||
|
|
d720d6be6b | ||
|
|
cb0c5f5a7b | ||
|
|
8691173fd8 | ||
|
|
ddc3cf301a | ||
|
|
b12115b61f | ||
|
|
029a69f5c1 | ||
|
|
80d1f12324 | ||
|
|
e906eac049 | ||
|
|
32aa9e4b63 | ||
|
|
7162faaa4d | ||
|
|
1901b708b2 | ||
|
|
d86750372a | ||
|
|
3a0e5c8894 | ||
|
|
cf008eb9b1 | ||
|
|
0bf85af8e9 | ||
|
|
78bb66b0f2 | ||
|
|
246135364c | ||
|
|
468ef7a618 | ||
|
|
68b5eedde1 | ||
|
|
3d2befc84a | ||
|
|
8e9528de13 | ||
|
|
0c8d3e70cf | ||
|
|
47e631d5ff | ||
|
|
e86003b692 | ||
|
|
f6218cadd7 | ||
|
|
368a917466 | ||
|
|
a6c5692586 | ||
|
|
d0a2820af4 | ||
|
|
299b86bae3 | ||
|
|
1218e65ca1 | ||
|
|
496155525c | ||
|
|
7c37f4bc51 | ||
|
|
6793edc349 | ||
|
|
1777e7f3b7 | ||
|
|
6fa09e1783 | ||
|
|
ae23f5a594 | ||
|
|
deaa493644 | ||
|
|
2a126faae7 | ||
|
|
35304435d0 | ||
|
|
420255373d | ||
|
|
834d22f866 | ||
|
|
2a77665be7 | ||
|
|
14857ae0da | ||
|
|
7156ea016e | ||
|
|
0820210c7c | ||
|
|
9eb1fbd5c3 | ||
|
|
ea9c217827 | ||
|
|
1fc020cf92 | ||
|
|
592ab0f350 | ||
|
|
0da2241367 | ||
|
|
37e8ef1542 | ||
|
|
e995033811 | ||
|
|
4c0329014f | ||
|
|
72301dc861 | ||
|
|
84e863f803 | ||
|
|
7a79da589a | ||
|
|
a481c7649f | ||
|
|
ba4d9a5230 | ||
|
|
e83594a2a3 | ||
|
|
a8ea472121 | ||
|
|
1393fac402 | ||
|
|
46f1849c2f | ||
|
|
9900575f9c | ||
|
|
2ed10f13e9 | ||
|
|
a23c9cd53d | ||
|
|
9ac2443b78 | ||
|
|
f075120675 | ||
|
|
b8f091e2f6 | ||
|
|
41f8b6926f | ||
|
|
b4ed3fbcd0 | ||
|
|
83ec9f4fe7 | ||
|
|
3f65b8506b | ||
|
|
50074f0e96 | ||
|
|
31a51cca4f | ||
|
|
654dee8cd6 | ||
|
|
7028de9dbd | ||
|
|
e4751513a4 | ||
|
|
bd7e2d2875 | ||
|
|
6061fdf231 | ||
|
|
9b662e8244 | ||
|
|
f51a640309 | ||
|
|
48906afaf4 | ||
|
|
ef4d93507d | ||
|
|
d1d79babe7 | ||
|
|
2eb8efe83b | ||
|
|
dd57ec9fa0 | ||
|
|
0033eb4ed6 | ||
|
|
2ca8dd2faa | ||
|
|
c2e3fed8dc | ||
|
|
91eb2be44c | ||
|
|
d4ac90dae0 | ||
|
|
11a416156b | ||
|
|
d37d6688d8 | ||
|
|
801f01e23f | ||
|
|
ea398d7b7e | ||
|
|
40bf923d7d | ||
|
|
e69a1d22f9 | ||
|
|
3f40452511 | ||
|
|
2ccf4cab8b | ||
|
|
feac983869 | ||
|
|
66b27e6dc8 | ||
|
|
6072a537d9 | ||
|
|
aecebf7981 | ||
|
|
4cbf47834d | ||
|
|
854715d9a7 | ||
|
|
9127777c32 | ||
|
|
6c816c55d1 | ||
|
|
209e02cf29 | ||
|
|
19686adc01 | ||
|
|
35667e81ea | ||
|
|
d881cd3346 | ||
|
|
ca51b1422a | ||
|
|
da00bbebbf | ||
|
|
171535f680 | ||
|
|
f06cf21341 | ||
|
|
254fb39567 | ||
|
|
40596433f1 | ||
|
|
3be1216897 | ||
|
|
b8a713fde3 | ||
|
|
06b6c4bcf9 | ||
|
|
cd4ab23fa9 | ||
|
|
5598dd2c34 | ||
|
|
f12d81f791 | ||
|
|
49547fc710 | ||
|
|
ea8fdd17c8 | ||
|
|
a6566bfd5f | ||
|
|
abefc0aba0 | ||
|
|
03b304f05c | ||
|
|
54e25e2fa6 | ||
|
|
83447663b4 | ||
|
|
b581a79eed | ||
|
|
6535614d78 | ||
|
|
fa82683ce2 | ||
|
|
1a201cd180 | ||
|
|
24457660e3 | ||
|
|
f1fb8cf03e | ||
|
|
18c6aceee7 | ||
|
|
24ddceea47 | ||
|
|
82ec28b369 | ||
|
|
db19f6395d | ||
|
|
b677ff20fe | ||
|
|
b98c1ec36a | ||
|
|
37ff9792be | ||
|
|
3344ab909b | ||
|
|
828b39587f | ||
|
|
0605172d4d | ||
|
|
089d938e15 | ||
|
|
c00508d6cf | ||
|
|
83b0f4f394 | ||
|
|
09cb844421 | ||
|
|
842569c9e5 | ||
|
|
3568f6c61a | ||
|
|
5bf213948c | ||
|
|
92c2fb45c7 | ||
|
|
cd9c0e03e7 | ||
|
|
2dd34cf967 | ||
|
|
a2072de391 | ||
|
|
526d9eae7f | ||
|
|
7681d12eb0 | ||
|
|
b077ae9259 | ||
|
|
362e189aa5 | ||
|
|
96ae03b48f | ||
|
|
c276e5b679 | ||
|
|
f00a8cb3a3 | ||
|
|
220995b830 | ||
|
|
e344f3f869 | ||
|
|
517a073f33 | ||
|
|
dcba9475ba | ||
|
|
2e43412a82 | ||
|
|
48861b67dd | ||
|
|
61eaa9333b | ||
|
|
9c7b5cb3f5 | ||
|
|
53828cbfad | ||
|
|
e3c602c43a | ||
|
|
02e9209e5c | ||
|
|
8a29bfba3f | ||
|
|
0c817f0441 | ||
|
|
326e27eab2 | ||
|
|
5547361d18 | ||
|
|
805d643347 | ||
|
|
b20a71a0d3 | ||
|
|
3de4e931c7 | ||
|
|
93345de687 | ||
|
|
bbf3c6ecac | ||
|
|
ca4de8f191 | ||
|
|
858ea1a8c5 | ||
|
|
dd50da314d | ||
|
|
1f068fcdb8 | ||
|
|
85d81024a4 | ||
|
|
e7c3e1c516 | ||
|
|
a268203832 | ||
|
|
9536dd14ef | ||
|
|
c565df96f5 | ||
|
|
4b4a8af893 | ||
|
|
1e3161b161 | ||
|
|
6b50b7b72a | ||
|
|
96895c7759 | ||
|
|
96c494c6ad | ||
|
|
48548b29d6 | ||
|
|
55b3b28aec | ||
|
|
6a26db80b6 | ||
|
|
ccf68878c7 | ||
|
|
f7367ceae5 | ||
|
|
61c9cbc3f5 | ||
|
|
446b90172b | ||
|
|
6501176081 | ||
|
|
ac35ffef9e | ||
|
|
6b89cf94a3 | ||
|
|
bd054a4918 | ||
|
|
cb04c3b335 | ||
|
|
5c5d8e61ae | ||
|
|
0fef380202 | ||
|
|
cc8361cd93 | ||
|
|
8b53fecc06 | ||
|
|
425d5a94cb | ||
|
|
38abee00e2 | ||
|
|
082784e023 | ||
|
|
155a37d3a6 | ||
|
|
f8a3253d12 | ||
|
|
b4ba80b09e | ||
|
|
f6045e8e11 | ||
|
|
f0a3f776c2 | ||
|
|
74dc07e736 | ||
|
|
c5eb4ed978 | ||
|
|
bff81d3056 | ||
|
|
c8bb7ded5d | ||
|
|
56f3bd86cf | ||
|
|
52ae97cffc | ||
|
|
c7ca5c81c3 | ||
|
|
ccb8db41f0 | ||
|
|
46fe7f5bea | ||
|
|
54904de77d | ||
|
|
dbd32f8f11 | ||
|
|
ef2b47e202 | ||
|
|
60e8dbf11d | ||
|
|
01414d0b3d | ||
|
|
ad5bf2fac8 | ||
|
|
a28ef22113 | ||
|
|
fe55b49c7d | ||
|
|
e32a92daa7 | ||
|
|
d7970e4ab8 | ||
|
|
2f78b42133 | ||
|
|
f4ef057e9e | ||
|
|
448786bfd0 | ||
|
|
0093d6e34d | ||
|
|
324c6057fd | ||
|
|
759ab23c45 | ||
|
|
1b62b6dd89 | ||
|
|
b3597f3d99 | ||
|
|
06bb6dbcff | ||
|
|
73e9de7b5f | ||
|
|
e535ef37c7 | ||
|
|
dd926af5a8 | ||
|
|
a726b5109a | ||
|
|
7c817dd34b | ||
|
|
c9165470f2 | ||
|
|
61816d0076 | ||
|
|
aa2a067489 | ||
|
|
714d380ec0 | ||
|
|
df7223f39c | ||
|
|
7f6f47b97b | ||
|
|
b9e972c248 | ||
|
|
66a1be2d86 | ||
|
|
717915e6ac | ||
|
|
c8c8f5722b | ||
|
|
cd610b3ed1 | ||
|
|
93deb0a584 | ||
|
|
05eab703cc | ||
|
|
c0cd55a8fa | ||
|
|
35052f2113 | ||
|
|
53f891226e | ||
|
|
4fb4c95220 | ||
|
|
b8b88e638b | ||
|
|
837327f530 | ||
|
|
e224c71119 | ||
|
|
5bc80fc094 | ||
|
|
75466fee8d | ||
|
|
bedf5f26fe | ||
|
|
2689bab652 | ||
|
|
e82dd14f0f | ||
|
|
1dba8f6add | ||
|
|
25a1e8d414 | ||
|
|
8c6287ef7b | ||
|
|
322cbca0dc | ||
|
|
2685a35c3a | ||
|
|
8606d9b581 | ||
|
|
3d63cbf076 | ||
|
|
67f88482e6 | ||
|
|
b3f5d20ad8 | ||
|
|
49d688f99c | ||
|
|
033bebf8cd | ||
|
|
1ce22bdcc1 | ||
|
|
4e678479b9 | ||
|
|
f69db61147 | ||
|
|
655f5eb1e2 | ||
|
|
e45edff4a4 | ||
|
|
fc309ee314 | ||
|
|
8a5f655a5c | ||
|
|
3f14ebc4cf | ||
|
|
4d8f6c1b41 | ||
|
|
7150c23e93 | ||
|
|
009179fed7 | ||
|
|
6dbd446fc8 | ||
|
|
a45d438f05 | ||
|
|
cabc41bcd6 | ||
|
|
5977bb05ca | ||
|
|
9332b3f690 | ||
|
|
5f13b2b35b | ||
|
|
01abd521e2 | ||
|
|
f91fae5dfb | ||
|
|
7169577f9c | ||
|
|
72152ff1f3 | ||
|
|
1d6246d700 | ||
|
|
5441796675 | ||
|
|
876e417eb4 | ||
|
|
e6c812e2ab | ||
|
|
0e5e2f5d2e | ||
|
|
5bca25fe44 | ||
|
|
ab253470f0 | ||
|
|
02f152c6e1 | ||
|
|
791802fdf7 | ||
|
|
f4f8066877 | ||
|
|
d95ecbe0fd | ||
|
|
cba77c6909 | ||
|
|
b4eb352e1d | ||
|
|
5fc937d558 | ||
|
|
f926deda89 | ||
|
|
c3e28728ce | ||
|
|
f956d399ae | ||
|
|
7efd347da6 | ||
|
|
5e07141846 | ||
|
|
40543e0a58 | ||
|
|
097758baf3 | ||
|
|
96a429a561 | ||
|
|
e5ee369e70 | ||
|
|
f5bc084ce2 | ||
|
|
d86cfc949d | ||
|
|
1d708510df | ||
|
|
1275b10d9c | ||
|
|
1f3dd3e2ee | ||
|
|
c227fbfdf2 | ||
|
|
f3f3dc693a | ||
|
|
5e1a4740d7 | ||
|
|
b4866e51b2 | ||
|
|
a9fa813a75 | ||
|
|
d99c2cf31e | ||
|
|
ffa431da7b | ||
|
|
a91effcd86 | ||
|
|
07a5d8c91c | ||
|
|
0bd0578ff9 | ||
|
|
2f3a73a59e | ||
|
|
1e74d793a2 | ||
|
|
3d96e3f8c8 | ||
|
|
d8215a62f6 | ||
|
|
841124af75 | ||
|
|
dbc42f56b3 | ||
|
|
96fbcb26c9 | ||
|
|
6a6dd599a1 | ||
|
|
5e52259fb3 | ||
|
|
e51c71bcd6 | ||
|
|
4d5e235166 | ||
|
|
5236d17ac4 | ||
|
|
d9f659171b | ||
|
|
993a69d3a9 | ||
|
|
944305b9f1 | ||
|
|
6eb63f778e | ||
|
|
eb240016ed | ||
|
|
b674906e3a | ||
|
|
bc7ba8cf2b | ||
|
|
b4649137fc | ||
|
|
c8b920a05d | ||
|
|
c564725f46 | ||
|
|
e599fec685 | ||
|
|
657fe1fa43 | ||
|
|
697d5e6247 | ||
|
|
d8d7e0a762 | ||
|
|
73d30dd875 | ||
|
|
033548a760 | ||
|
|
e416dfdbc0 | ||
|
|
592a3074e1 | ||
|
|
b39e93d0d1 | ||
|
|
906c54faff | ||
|
|
d3c3088c6b | ||
|
|
f2c0b30641 | ||
|
|
601de66c03 | ||
|
|
91ea2d4c27 | ||
|
|
b7884ddd02 | ||
|
|
9fec516560 | ||
|
|
f469eff97b | ||
|
|
69cde11a51 | ||
|
|
7d2047cdbd | ||
|
|
c76116b970 | ||
|
|
8d604350e4 | ||
|
|
4db724984e | ||
|
|
87942ed71d | ||
|
|
dd871e0d8c | ||
|
|
c1014f5989 | ||
|
|
6a2262969e | ||
|
|
d39e44c209 | ||
|
|
4d15b58ca4 | ||
|
|
9342317291 | ||
|
|
dfd74495bd | ||
|
|
90ffa95b6a | ||
|
|
825f160369 | ||
|
|
834e694e94 | ||
|
|
ce3e9b0c29 | ||
|
|
1c7ceaa2ca | ||
|
|
7e0620a143 | ||
|
|
fb039a3e0c | ||
|
|
05e4234525 | ||
|
|
e19472568f | ||
|
|
845488af8d | ||
|
|
b924b7b4c6 | ||
|
|
b368fc03ea | ||
|
|
982a094646 | ||
|
|
dfe320f172 | ||
|
|
2537c7c735 | ||
|
|
54853ee239 | ||
|
|
b408cee29f | ||
|
|
e63d89973a | ||
|
|
b311088f5a | ||
|
|
d9c534b13d | ||
|
|
95e311fd02 | ||
|
|
5b2aa0be81 | ||
|
|
33b4995547 | ||
|
|
c16dab236a | ||
|
|
202c46035a | ||
|
|
ea83d66fb5 | ||
|
|
9f85e397d4 | ||
|
|
7df2e2a8d2 | ||
|
|
d446de62a4 | ||
|
|
dd9f03d9b9 | ||
|
|
3c57d5518a | ||
|
|
5f98afc180 | ||
|
|
7492c0ea03 | ||
|
|
65a430bb42 | ||
|
|
bd62f47afa | ||
|
|
3e0aa4c603 | ||
|
|
8b5bfdfa44 | ||
|
|
da88ac8cca | ||
|
|
a66f3e02f4 | ||
|
|
7f44861f32 | ||
|
|
63e20404a2 | ||
|
|
5c574b9878 | ||
|
|
9562a1c146 | ||
|
|
ed4404f350 | ||
|
|
bd55b647c7 | ||
|
|
19c7527376 |
+6
-6
@@ -123,13 +123,13 @@ define the source file coding standards we use along with some IDEA editor setti
|
||||
|
||||
### Reference Docs
|
||||
|
||||
The reference documentation is in the [framework-docs/src/docs/asciidoc](framework-docs/src/docs/asciidoc) directory, in
|
||||
[Asciidoctor](https://asciidoctor.org/) format. For trivial changes, you may be able to browse,
|
||||
edit source files, and submit directly from GitHub.
|
||||
The reference documentation is authored in [Asciidoctor](https://asciidoctor.org/) format
|
||||
using [Antora](https://docs.antora.org/antora/latest/). The source files for the documentation
|
||||
reside in the [framework-docs/modules/ROOT](framework-docs/modules/ROOT) directory. For
|
||||
trivial changes, you may be able to browse, edit source files, and submit directly from GitHub.
|
||||
|
||||
When making changes locally, execute `./gradlew :framework-docs:asciidoctor` and then browse the result under
|
||||
`framework-docs/build/docs/ref-docs/html5/index.html`.
|
||||
When making changes locally, execute `./gradlew antora` and then browse the results under
|
||||
`framework-docs/build/site/index.html`.
|
||||
|
||||
Asciidoctor also supports live editing. For more details see
|
||||
[AsciiDoc Tooling](https://docs.asciidoctor.org/asciidoctor/latest/tooling/).
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
This is the home of the Spring Framework: the foundation for all [Spring projects](https://spring.io/projects). Collectively the Spring Framework and the family of Spring projects are often referred to simply as "Spring".
|
||||
|
||||
Spring provides everything required beyond the Java programming language for creating enterprise applications for a wide range of scenarios and architectures. Please read the [Overview](https://docs.spring.io/spring/docs/current/spring-framework-reference/overview.html#spring-introduction) section as reference for a more complete introduction.
|
||||
Spring provides everything required beyond the Java programming language for creating enterprise applications for a wide range of scenarios and architectures. Please read the [Overview](https://docs.spring.io/spring-framework/reference/overview.html) section of the reference documentation for a more complete introduction.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
@@ -14,7 +14,7 @@ For access to artifacts or a distribution zip, see the [Spring Framework Artifac
|
||||
|
||||
## Documentation
|
||||
|
||||
The Spring Framework maintains reference documentation ([published](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/) and [source](framework-docs/src/docs/asciidoc)), GitHub [wiki pages](https://github.com/spring-projects/spring-framework/wiki), and an
|
||||
The Spring Framework maintains reference documentation ([published](https://docs.spring.io/spring-framework/reference/) and [source](framework-docs/modules/ROOT)), GitHub [wiki pages](https://github.com/spring-projects/spring-framework/wiki), and an
|
||||
[API reference](https://docs.spring.io/spring-framework/docs/current/javadoc-api/). There are also [guides and tutorials](https://spring.io/guides) across Spring projects.
|
||||
|
||||
## Micro-Benchmarks
|
||||
@@ -31,7 +31,7 @@ Information regarding CI builds can be found in the [Spring Framework Concourse
|
||||
|
||||
## Stay in Touch
|
||||
|
||||
Follow [@SpringCentral](https://twitter.com/springcentral), [@SpringFramework](https://twitter.com/springframework), and its [team members](https://twitter.com/springframework/lists/team/members) on Twitter. In-depth articles can be found at [The Spring Blog](https://spring.io/blog/), and releases are announced via our [news feed](https://spring.io/blog/category/news).
|
||||
Follow [@SpringCentral](https://twitter.com/springcentral), [@SpringFramework](https://twitter.com/springframework), and its [team members](https://twitter.com/springframework/lists/team/members) on Twitter. In-depth articles can be found at [The Spring Blog](https://spring.io/blog/), and releases are announced via our [releases feed](https://spring.io/blog/category/releases).
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+3
-55
@@ -1,14 +1,14 @@
|
||||
plugins {
|
||||
id 'io.spring.nohttp' version '0.0.11'
|
||||
id 'io.freefair.aspectj' version '8.0.1' apply false
|
||||
// kotlinVersion is managed in gradle.properties
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version "${kotlinVersion}" apply false
|
||||
id 'org.jetbrains.dokka' version '1.8.10'
|
||||
id 'org.jetbrains.dokka' version '1.8.20'
|
||||
id 'org.unbroken-dome.xjc' version '2.0.0' apply false
|
||||
id 'com.github.ben-manes.versions' version '0.46.0'
|
||||
id 'com.github.johnrengelman.shadow' version '8.1.1' apply false
|
||||
id 'de.undercouch.download' version '5.4.0'
|
||||
id 'me.champeau.jmh' version '0.7.1' apply false
|
||||
id 'me.champeau.mrjar' version '0.1.1'
|
||||
}
|
||||
|
||||
ext {
|
||||
@@ -46,7 +46,6 @@ configure([rootProject] + javaProjects) { project ->
|
||||
|
||||
apply plugin: "java"
|
||||
apply plugin: "java-test-fixtures"
|
||||
apply plugin: "checkstyle"
|
||||
apply plugin: 'org.springframework.build.conventions'
|
||||
apply from: "${rootDir}/gradle/toolchains.gradle"
|
||||
apply from: "${rootDir}/gradle/ide.gradle"
|
||||
@@ -60,33 +59,6 @@ configure([rootProject] + javaProjects) { project ->
|
||||
matching { it.name.endsWith("Classpath") }.all { it.extendsFrom(dependencyManagement) }
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
include(["**/*Tests.class", "**/*Test.class"])
|
||||
systemProperty("java.awt.headless", "true")
|
||||
systemProperty("testGroups", project.properties.get("testGroups"))
|
||||
systemProperty("io.netty.leakDetection.level", "paranoid")
|
||||
systemProperty("io.netty5.leakDetectionLevel", "paranoid")
|
||||
systemProperty("io.netty5.leakDetection.targetRecords", "32")
|
||||
systemProperty("io.netty5.buffer.lifecycleTracingEnabled", "true")
|
||||
systemProperty("io.netty5.buffer.leakDetectionEnabled", "true")
|
||||
jvmArgs(["--add-opens=java.base/java.lang=ALL-UNNAMED",
|
||||
"--add-opens=java.base/java.util=ALL-UNNAMED"])
|
||||
}
|
||||
|
||||
checkstyle {
|
||||
toolVersion = "10.12.1"
|
||||
configDirectory.set(rootProject.file("src/checkstyle"))
|
||||
}
|
||||
|
||||
tasks.named("checkstyleMain").configure {
|
||||
maxHeapSize = "1g"
|
||||
}
|
||||
|
||||
tasks.named("checkstyleTest").configure {
|
||||
maxHeapSize = "1g"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
dependencyManagement(enforcedPlatform(dependencies.project(path: ":framework-platform")))
|
||||
testImplementation("org.junit.jupiter:junit-jupiter-api")
|
||||
@@ -106,7 +78,6 @@ configure([rootProject] + javaProjects) { project ->
|
||||
// JSR-305 only used for non-required meta-annotations
|
||||
compileOnly("com.google.code.findbugs:jsr305")
|
||||
testCompileOnly("com.google.code.findbugs:jsr305")
|
||||
checkstyle("io.spring.javaformat:spring-javaformat-checkstyle:0.0.38")
|
||||
}
|
||||
|
||||
ext.javadocLinks = [
|
||||
@@ -127,7 +98,7 @@ configure([rootProject] + javaProjects) { project ->
|
||||
// TODO Uncomment link to JUnit 5 docs once we execute Gradle with Java 18+.
|
||||
// See https://github.com/spring-projects/spring-framework/issues/27497
|
||||
//
|
||||
// "https://junit.org/junit5/docs/5.9.3/api/",
|
||||
// "https://junit.org/junit5/docs/5.10.0/api/",
|
||||
"https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/",
|
||||
"https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/",
|
||||
"https://r2dbc.io/spec/1.0.0.RELEASE/api/",
|
||||
@@ -144,28 +115,5 @@ configure(moduleProjects) { project ->
|
||||
|
||||
configure(rootProject) {
|
||||
description = "Spring Framework"
|
||||
|
||||
apply plugin: "io.spring.nohttp"
|
||||
apply plugin: 'org.springframework.build.api-diff'
|
||||
|
||||
nohttp {
|
||||
source.exclude "**/test-output/**"
|
||||
source.exclude "**/.gradle/**"
|
||||
allowlistFile = project.file("src/nohttp/allowlist.lines")
|
||||
def rootPath = file(rootDir).toPath()
|
||||
def projectDirs = allprojects.collect { it.projectDir } + "${rootDir}/buildSrc"
|
||||
projectDirs.forEach { dir ->
|
||||
[ 'bin', 'build', 'out', '.settings' ]
|
||||
.collect { rootPath.relativize(new File(dir, it).toPath()) }
|
||||
.forEach { source.exclude "$it/**" }
|
||||
[ '.classpath', '.project' ]
|
||||
.collect { rootPath.relativize(new File(dir, it).toPath()) }
|
||||
.forEach { source.exclude "$it" }
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("checkstyleNohttp").configure {
|
||||
maxHeapSize = "1g"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
plugins {
|
||||
id 'java-gradle-plugin'
|
||||
id 'checkstyle'
|
||||
id 'io.spring.javaformat' version "${javaFormatVersion}"
|
||||
}
|
||||
|
||||
repositories {
|
||||
@@ -17,10 +19,13 @@ ext {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
|
||||
implementation("org.jetbrains.kotlin:kotlin-compiler-embeddable:${kotlinVersion}")
|
||||
checkstyle "io.spring.javaformat:spring-javaformat-checkstyle:${javaFormatVersion}"
|
||||
implementation "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}"
|
||||
implementation "org.jetbrains.kotlin:kotlin-compiler-embeddable:${kotlinVersion}"
|
||||
implementation "me.champeau.gradle:japicmp-gradle-plugin:0.3.0"
|
||||
implementation "org.gradle:test-retry-gradle-plugin:1.4.1"
|
||||
implementation "io.spring.javaformat:spring-javaformat-gradle-plugin:${javaFormatVersion}"
|
||||
implementation "io.spring.nohttp:nohttp-gradle:0.0.11"
|
||||
}
|
||||
|
||||
gradlePlugin {
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
org.gradle.caching=true
|
||||
javaFormatVersion=0.0.38
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.build;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import io.spring.javaformat.gradle.SpringJavaFormatPlugin;
|
||||
import io.spring.nohttp.gradle.NoHttpExtension;
|
||||
import io.spring.nohttp.gradle.NoHttpPlugin;
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.artifacts.DependencySet;
|
||||
import org.gradle.api.plugins.JavaBasePlugin;
|
||||
import org.gradle.api.plugins.quality.Checkstyle;
|
||||
import org.gradle.api.plugins.quality.CheckstyleExtension;
|
||||
import org.gradle.api.plugins.quality.CheckstylePlugin;
|
||||
|
||||
/**
|
||||
* {@link Plugin} that applies conventions for checkstyle.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public class CheckstyleConventions {
|
||||
|
||||
/**
|
||||
* Applies the Spring Java Format and Checkstyle plugins with the project conventions.
|
||||
* @param project the current project
|
||||
*/
|
||||
public void apply(Project project) {
|
||||
project.getPlugins().withType(JavaBasePlugin.class, (java) -> {
|
||||
if (project.getRootProject() == project) {
|
||||
configureNoHttpPlugin(project);
|
||||
}
|
||||
project.getPlugins().apply(CheckstylePlugin.class);
|
||||
project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g"));
|
||||
CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class);
|
||||
checkstyle.setToolVersion("10.12.1");
|
||||
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
|
||||
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
|
||||
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
|
||||
checkstyleDependencies
|
||||
.add(project.getDependencies().create("io.spring.javaformat:spring-javaformat-checkstyle:" + version));
|
||||
});
|
||||
}
|
||||
|
||||
private static void configureNoHttpPlugin(Project project) {
|
||||
project.getPlugins().apply(NoHttpPlugin.class);
|
||||
NoHttpExtension noHttp = project.getExtensions().getByType(NoHttpExtension.class);
|
||||
noHttp.setAllowlistFile(project.file("src/nohttp/allowlist.lines"));
|
||||
noHttp.getSource().exclude("**/test-output/**", "**/.settings/**",
|
||||
"**/.classpath", "**/.project", "**/.gradle/**");
|
||||
List<String> buildFolders = List.of("bin", "build", "out");
|
||||
project.allprojects(subproject -> {
|
||||
Path rootPath = project.getRootDir().toPath();
|
||||
Path projectPath = rootPath.relativize(subproject.getProjectDir().toPath());
|
||||
for (String buildFolder : buildFolders) {
|
||||
Path innerBuildDir = projectPath.resolve(buildFolder);
|
||||
noHttp.getSource().exclude(innerBuildDir + File.separator + "**");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -25,10 +25,8 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinBasePlugin;
|
||||
* Plugin to apply conventions to projects that are part of Spring Framework's build.
|
||||
* Conventions are applied in response to various plugins being applied.
|
||||
*
|
||||
* When the {@link JavaBasePlugin} is applied, the conventions in {@link TestConventions}
|
||||
* are applied.
|
||||
* When the {@link JavaBasePlugin} is applied, the conventions in {@link JavaConventions}
|
||||
* are applied.
|
||||
* <p>When the {@link JavaBasePlugin} is applied, the conventions in {@link CheckstyleConventions},
|
||||
* {@link TestConventions} and {@link JavaConventions} are applied.
|
||||
* When the {@link KotlinBasePlugin} is applied, the conventions in {@link KotlinConventions}
|
||||
* are applied.
|
||||
*
|
||||
@@ -38,8 +36,10 @@ public class ConventionsPlugin implements Plugin<Project> {
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
new CheckstyleConventions().apply(project);
|
||||
new JavaConventions().apply(project);
|
||||
new KotlinConventions().apply(project);
|
||||
new TestConventions().apply(project);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -24,7 +24,9 @@ import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.plugins.JavaBasePlugin;
|
||||
import org.gradle.api.plugins.JavaPlugin;
|
||||
import org.gradle.api.plugins.JavaPluginExtension;
|
||||
import org.gradle.api.tasks.compile.JavaCompile;
|
||||
import org.gradle.jvm.toolchain.JavaLanguageVersion;
|
||||
|
||||
/**
|
||||
* {@link Plugin} that applies conventions for compiling Java sources in Spring Framework.
|
||||
@@ -68,6 +70,8 @@ public class JavaConventions {
|
||||
* @param project the current project
|
||||
*/
|
||||
private void applyJavaCompileConventions(Project project) {
|
||||
project.getExtensions().getByType(JavaPluginExtension.class)
|
||||
.getToolchain().getLanguageVersion().set(JavaLanguageVersion.of(17));
|
||||
project.getTasks().withType(JavaCompile.class)
|
||||
.matching(compileTask -> compileTask.getName().equals(JavaPlugin.COMPILE_JAVA_TASK_NAME))
|
||||
.forEach(compileTask -> {
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.build;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.plugins.JavaBasePlugin;
|
||||
import org.gradle.api.tasks.testing.Test;
|
||||
@@ -41,11 +43,36 @@ class TestConventions {
|
||||
|
||||
private void configureTestConventions(Project project) {
|
||||
project.getTasks().withType(Test.class,
|
||||
test -> project.getPlugins().withType(TestRetryPlugin.class, testRetryPlugin -> {
|
||||
TestRetryTaskExtension testRetry = test.getExtensions().getByType(TestRetryTaskExtension.class);
|
||||
testRetry.getFailOnPassedAfterRetry().set(true);
|
||||
testRetry.getMaxRetries().set(isCi() ? 3 : 0);
|
||||
}));
|
||||
test -> {
|
||||
configureTests(project, test);
|
||||
configureTestRetryPlugin(project, test);
|
||||
});
|
||||
}
|
||||
|
||||
private void configureTests(Project project, Test test) {
|
||||
test.useJUnitPlatform();
|
||||
test.include("**/*Tests.class", "**/*Test.class");
|
||||
test.setSystemProperties(Map.of(
|
||||
"java.awt.headless", "true",
|
||||
"io.netty.leakDetection.level", "paranoid",
|
||||
"io.netty5.leakDetectionLevel", "paranoid",
|
||||
"io.netty5.leakDetection.targetRecords", "32",
|
||||
"io.netty5.buffer.lifecycleTracingEnabled", "true"
|
||||
));
|
||||
if (project.hasProperty("testGroups")) {
|
||||
test.systemProperty("testGroups", project.getProperties().get("testGroups"));
|
||||
}
|
||||
test.jvmArgs("--add-opens=java.base/java.lang=ALL-UNNAMED",
|
||||
"--add-opens=java.base/java.util=ALL-UNNAMED",
|
||||
"-Djava.locale.providers=COMPAT");
|
||||
}
|
||||
|
||||
private void configureTestRetryPlugin(Project project, Test test) {
|
||||
project.getPlugins().withType(TestRetryPlugin.class, testRetryPlugin -> {
|
||||
TestRetryTaskExtension testRetry = test.getExtensions().getByType(TestRetryTaskExtension.class);
|
||||
testRetry.getFailOnPassedAfterRetry().set(true);
|
||||
testRetry.getMaxRetries().set(isCi() ? 3 : 0);
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isCi() {
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.build.hint;
|
||||
|
||||
import org.gradle.api.file.ConfigurableFileCollection;
|
||||
import org.gradle.api.provider.SetProperty;
|
||||
import org.gradle.api.tasks.Classpath;
|
||||
import org.gradle.api.tasks.Input;
|
||||
import org.gradle.process.CommandLineArgumentProvider;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* Argument provider for registering the runtime hints agent with a Java process.
|
||||
*/
|
||||
public interface RuntimeHintsAgentArgumentProvider extends CommandLineArgumentProvider {
|
||||
|
||||
@Classpath
|
||||
ConfigurableFileCollection getAgentJar();
|
||||
|
||||
@Input
|
||||
SetProperty<String> getIncludedPackages();
|
||||
|
||||
@Input
|
||||
SetProperty<String> getExcludedPackages();
|
||||
|
||||
@Override
|
||||
default Iterable<String> asArguments() {
|
||||
StringBuilder packages = new StringBuilder();
|
||||
getIncludedPackages().get().forEach(packageName -> packages.append('+').append(packageName).append(','));
|
||||
getExcludedPackages().get().forEach(packageName -> packages.append('-').append(packageName).append(','));
|
||||
return Collections.singleton("-javaagent:" + getAgentJar().getSingleFile() + "=" + packages);
|
||||
}
|
||||
}
|
||||
+4
-27
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,38 +16,15 @@
|
||||
|
||||
package org.springframework.build.hint;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.gradle.api.model.ObjectFactory;
|
||||
import org.gradle.api.provider.SetProperty;
|
||||
|
||||
/**
|
||||
* Entry point to the DSL extension for the {@link RuntimeHintsAgentPlugin} Gradle plugin.
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public class RuntimeHintsAgentExtension {
|
||||
public interface RuntimeHintsAgentExtension {
|
||||
|
||||
private final SetProperty<String> includedPackages;
|
||||
SetProperty<String> getIncludedPackages();
|
||||
|
||||
private final SetProperty<String> excludedPackages;
|
||||
|
||||
public RuntimeHintsAgentExtension(ObjectFactory objectFactory) {
|
||||
this.includedPackages = objectFactory.setProperty(String.class).convention(Collections.singleton("org.springframework"));
|
||||
this.excludedPackages = objectFactory.setProperty(String.class).convention(Collections.emptySet());
|
||||
}
|
||||
|
||||
public SetProperty<String> getIncludedPackages() {
|
||||
return this.includedPackages;
|
||||
}
|
||||
|
||||
public SetProperty<String> getExcludedPackages() {
|
||||
return this.excludedPackages;
|
||||
}
|
||||
|
||||
String asJavaAgentArgument() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
this.includedPackages.get().forEach(packageName -> builder.append('+').append(packageName).append(','));
|
||||
this.excludedPackages.get().forEach(packageName -> builder.append('-').append(packageName).append(','));
|
||||
return builder.toString();
|
||||
}
|
||||
SetProperty<String> getExcludedPackages();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,41 +16,79 @@
|
||||
|
||||
package org.springframework.build.hint;
|
||||
|
||||
import org.gradle.api.JavaVersion;
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.artifacts.Configuration;
|
||||
import org.gradle.api.attributes.Bundling;
|
||||
import org.gradle.api.attributes.Category;
|
||||
import org.gradle.api.attributes.LibraryElements;
|
||||
import org.gradle.api.attributes.Usage;
|
||||
import org.gradle.api.attributes.java.TargetJvmVersion;
|
||||
import org.gradle.api.plugins.JavaPlugin;
|
||||
import org.gradle.api.tasks.bundling.Jar;
|
||||
import org.gradle.api.tasks.testing.Test;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* {@link Plugin} that configures the {@code RuntimeHints} Java agent to test tasks.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
public class RuntimeHintsAgentPlugin implements Plugin<Project> {
|
||||
|
||||
public static final String RUNTIMEHINTS_TEST_TASK = "runtimeHintsTest";
|
||||
private static final String EXTENSION_NAME = "runtimeHintsAgent";
|
||||
private static final String CONFIGURATION_NAME = "testRuntimeHintsAgentJar";
|
||||
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
|
||||
project.getPlugins().withType(JavaPlugin.class, javaPlugin -> {
|
||||
RuntimeHintsAgentExtension agentExtension = project.getExtensions().create(EXTENSION_NAME,
|
||||
RuntimeHintsAgentExtension.class, project.getObjects());
|
||||
RuntimeHintsAgentExtension agentExtension = createRuntimeHintsAgentExtension(project);
|
||||
Test agentTest = project.getTasks().create(RUNTIMEHINTS_TEST_TASK, Test.class, test -> {
|
||||
test.useJUnitPlatform(options -> {
|
||||
options.includeTags("RuntimeHintsTests");
|
||||
});
|
||||
test.include("**/*Tests.class", "**/*Test.class");
|
||||
test.systemProperty("java.awt.headless", "true");
|
||||
});
|
||||
project.afterEvaluate(p -> {
|
||||
Jar jar = project.getRootProject().project("spring-core-test").getTasks().withType(Jar.class).named("jar").get();
|
||||
agentTest.jvmArgs("-javaagent:" + jar.getArchiveFile().get().getAsFile() + "=" + agentExtension.asJavaAgentArgument());
|
||||
test.systemProperty("org.graalvm.nativeimage.imagecode", "runtime");
|
||||
test.getJvmArgumentProviders().add(createRuntimeHintsAgentArgumentProvider(project, agentExtension));
|
||||
});
|
||||
project.getTasks().getByName("check", task -> task.dependsOn(agentTest));
|
||||
project.getDependencies().add(CONFIGURATION_NAME, project.project(":spring-core-test"));
|
||||
});
|
||||
}
|
||||
|
||||
private static RuntimeHintsAgentExtension createRuntimeHintsAgentExtension(Project project) {
|
||||
RuntimeHintsAgentExtension agentExtension = project.getExtensions().create(EXTENSION_NAME, RuntimeHintsAgentExtension.class);
|
||||
agentExtension.getIncludedPackages().convention(Collections.singleton("org.springframework"));
|
||||
agentExtension.getExcludedPackages().convention(Collections.emptySet());
|
||||
return agentExtension;
|
||||
}
|
||||
|
||||
private static RuntimeHintsAgentArgumentProvider createRuntimeHintsAgentArgumentProvider(
|
||||
Project project, RuntimeHintsAgentExtension agentExtension) {
|
||||
RuntimeHintsAgentArgumentProvider agentArgumentProvider = project.getObjects().newInstance(RuntimeHintsAgentArgumentProvider.class);
|
||||
agentArgumentProvider.getAgentJar().from(createRuntimeHintsAgentConfiguration(project));
|
||||
agentArgumentProvider.getIncludedPackages().set(agentExtension.getIncludedPackages());
|
||||
agentArgumentProvider.getExcludedPackages().set(agentExtension.getExcludedPackages());
|
||||
return agentArgumentProvider;
|
||||
}
|
||||
|
||||
private static Configuration createRuntimeHintsAgentConfiguration(Project project) {
|
||||
return project.getConfigurations().create(CONFIGURATION_NAME, configuration -> {
|
||||
configuration.setCanBeConsumed(false);
|
||||
configuration.setTransitive(false); // Only the built artifact is required
|
||||
configuration.attributes(attributes -> {
|
||||
attributes.attribute(Bundling.BUNDLING_ATTRIBUTE, project.getObjects().named(Bundling.class, Bundling.EXTERNAL));
|
||||
attributes.attribute(Category.CATEGORY_ATTRIBUTE, project.getObjects().named(Category.class, Category.LIBRARY));
|
||||
attributes.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, project.getObjects().named(LibraryElements.class, LibraryElements.JAR));
|
||||
attributes.attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, Integer.valueOf(JavaVersion.current().getMajorVersion()));
|
||||
attributes.attribute(Usage.USAGE_ATTRIBUTE, project.getObjects().named(Usage.class, Usage.JAVA_RUNTIME));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@ RUN ./setup.sh
|
||||
|
||||
ENV JAVA_HOME /opt/openjdk/java17
|
||||
ENV JDK17 /opt/openjdk/java17
|
||||
ENV JDK20 /opt/openjdk/java20
|
||||
ENV JDK21 /opt/openjdk/java21
|
||||
|
||||
ENV PATH $JAVA_HOME/bin:$PATH
|
||||
|
||||
@@ -5,8 +5,8 @@ case "$1" in
|
||||
java17)
|
||||
echo "https://github.com/bell-sw/Liberica/releases/download/17.0.7+7/bellsoft-jdk17.0.7+7-linux-amd64.tar.gz"
|
||||
;;
|
||||
java20)
|
||||
echo "https://github.com/bell-sw/Liberica/releases/download/20.0.1+10/bellsoft-jdk20.0.1+10-linux-amd64.tar.gz"
|
||||
java21)
|
||||
echo "https://download.java.net/java/early_access/jdk21/31/GPL/openjdk-21-ea+31_linux-x64_bin.tar.gz"
|
||||
;;
|
||||
*)
|
||||
echo $"Unknown java version"
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ curl https://raw.githubusercontent.com/spring-io/concourse-java-scripts/v0.0.4/c
|
||||
|
||||
mkdir -p /opt/openjdk
|
||||
pushd /opt/openjdk > /dev/null
|
||||
for jdk in java17 java20
|
||||
for jdk in java17 java21
|
||||
do
|
||||
JDK_URL=$( /get-jdk-url.sh $jdk )
|
||||
mkdir $jdk
|
||||
|
||||
+2
-2
@@ -3,8 +3,8 @@ github-repo-name: "spring-projects/spring-framework"
|
||||
sonatype-staging-profile: "org.springframework"
|
||||
docker-hub-organization: "springci"
|
||||
artifactory-server: "https://repo.spring.io"
|
||||
branch: "6.0.x"
|
||||
milestone: "6.0.x"
|
||||
branch: "main"
|
||||
milestone: "6.1.x"
|
||||
build-name: "spring-framework"
|
||||
pipeline-name: "spring-framework"
|
||||
concourse-url: "https://ci.spring.io"
|
||||
|
||||
+10
-10
@@ -45,7 +45,7 @@ resource_types:
|
||||
source:
|
||||
<<: *docker-resource-source
|
||||
repository: concourse/registry-image-resource
|
||||
tag: 1.5.0
|
||||
tag: 1.8.0
|
||||
- name: artifactory-resource
|
||||
type: registry-image
|
||||
source:
|
||||
@@ -57,7 +57,7 @@ resource_types:
|
||||
source:
|
||||
<<: *docker-resource-source
|
||||
repository: concourse/github-release-resource
|
||||
tag: 1.5.5
|
||||
tag: 1.8.0
|
||||
- name: github-status-resource
|
||||
type: registry-image
|
||||
source:
|
||||
@@ -113,14 +113,14 @@ resources:
|
||||
access_token: ((github-ci-status-token))
|
||||
branch: ((branch))
|
||||
context: build
|
||||
- name: repo-status-jdk20-build
|
||||
- name: repo-status-jdk21-build
|
||||
type: github-status-resource
|
||||
icon: eye-check-outline
|
||||
source:
|
||||
repository: ((github-repo-name))
|
||||
access_token: ((github-ci-status-token))
|
||||
branch: ((branch))
|
||||
context: jdk20-build
|
||||
context: jdk21-build
|
||||
- name: slack-alert
|
||||
type: slack-notification
|
||||
icon: slack
|
||||
@@ -217,7 +217,7 @@ jobs:
|
||||
"zip.type": "schema"
|
||||
get_params:
|
||||
threads: 8
|
||||
- name: jdk20-build
|
||||
- name: jdk21-build
|
||||
serial: true
|
||||
public: true
|
||||
plan:
|
||||
@@ -225,7 +225,7 @@ jobs:
|
||||
- get: git-repo
|
||||
- get: every-morning
|
||||
trigger: true
|
||||
- put: repo-status-jdk20-build
|
||||
- put: repo-status-jdk21-build
|
||||
params: { state: "pending", commit: "git-repo" }
|
||||
- do:
|
||||
- task: check-project
|
||||
@@ -234,16 +234,16 @@ jobs:
|
||||
privileged: true
|
||||
timeout: ((task-timeout))
|
||||
params:
|
||||
TEST_TOOLCHAIN: 20
|
||||
TEST_TOOLCHAIN: 21
|
||||
<<: *build-project-task-params
|
||||
on_failure:
|
||||
do:
|
||||
- put: repo-status-jdk20-build
|
||||
- put: repo-status-jdk21-build
|
||||
params: { state: "failure", commit: "git-repo" }
|
||||
- put: slack-alert
|
||||
params:
|
||||
<<: *slack-fail-params
|
||||
- put: repo-status-jdk20-build
|
||||
- put: repo-status-jdk21-build
|
||||
params: { state: "success", commit: "git-repo" }
|
||||
- name: stage-milestone
|
||||
serial: true
|
||||
@@ -396,7 +396,7 @@ jobs:
|
||||
|
||||
groups:
|
||||
- name: "builds"
|
||||
jobs: ["build", "jdk20-build"]
|
||||
jobs: ["build", "jdk21-build"]
|
||||
- name: "releases"
|
||||
jobs: ["stage-milestone", "stage-rc", "stage-release", "promote-milestone", "promote-rc", "promote-release", "create-github-release"]
|
||||
- name: "ci-images"
|
||||
|
||||
@@ -4,5 +4,6 @@ set -e
|
||||
source $(dirname $0)/common.sh
|
||||
|
||||
pushd git-repo > /dev/null
|
||||
./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --max-workers=4 check
|
||||
./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false -Porg.gradle.java.installations.fromEnv=JDK17,JDK21 \
|
||||
--no-daemon --max-workers=4 check
|
||||
popd > /dev/null
|
||||
|
||||
@@ -5,5 +5,6 @@ source $(dirname $0)/common.sh
|
||||
repository=$(pwd)/distribution-repository
|
||||
|
||||
pushd git-repo > /dev/null
|
||||
./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --max-workers=4 -PdeploymentRepository=${repository} build publishAllPublicationsToDeploymentRepository
|
||||
./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false -Porg.gradle.java.installations.fromEnv=JDK17,JDK21 \
|
||||
--no-daemon --max-workers=4 -PdeploymentRepository=${repository} build publishAllPublicationsToDeploymentRepository
|
||||
popd > /dev/null
|
||||
|
||||
@@ -4,6 +4,6 @@ set -e
|
||||
source $(dirname $0)/common.sh
|
||||
|
||||
pushd git-repo > /dev/null
|
||||
./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false -Porg.gradle.java.installations.fromEnv=JDK17,JDK20 \
|
||||
./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false -Porg.gradle.java.installations.fromEnv=JDK17,JDK21 \
|
||||
-PmainToolchain=${MAIN_TOOLCHAIN} -PtestToolchain=${TEST_TOOLCHAIN} --no-daemon --max-workers=4 check antora
|
||||
popd > /dev/null
|
||||
|
||||
@@ -35,7 +35,8 @@ git add gradle.properties > /dev/null
|
||||
git commit -m"Release v$stageVersion" > /dev/null
|
||||
git tag -a "v$stageVersion" -m"Release v$stageVersion" > /dev/null
|
||||
|
||||
./gradlew --no-daemon --max-workers=4 -PdeploymentRepository=${repository} build publishAllPublicationsToDeploymentRepository
|
||||
./gradlew --no-daemon --max-workers=4 -PdeploymentRepository=${repository} -Porg.gradle.java.installations.fromEnv=JDK17,JDK21 \
|
||||
build publishAllPublicationsToDeploymentRepository
|
||||
|
||||
git reset --hard HEAD^ > /dev/null
|
||||
if [[ $nextVersion != $snapshotVersion ]]; then
|
||||
|
||||
@@ -45,12 +45,6 @@ tasks.create("generateAntoraResources") {
|
||||
dependsOn 'generateAntoraYml'
|
||||
}
|
||||
|
||||
// Commented out for now:
|
||||
// https://github.com/spring-projects/spring-framework/issues/30481
|
||||
// tasks.named("check") {
|
||||
// dependsOn 'antora'
|
||||
// }
|
||||
|
||||
jar {
|
||||
enabled = false
|
||||
}
|
||||
@@ -67,7 +61,9 @@ repositories {
|
||||
|
||||
dependencies {
|
||||
api(project(":spring-context"))
|
||||
api(project(":spring-jms"))
|
||||
api(project(":spring-web"))
|
||||
api("jakarta.jms:jakarta.jms-api")
|
||||
api("jakarta.servlet:jakarta.servlet-api")
|
||||
|
||||
implementation(project(":spring-core-test"))
|
||||
|
||||
@@ -130,6 +130,7 @@
|
||||
**** xref:testing/testcontext-framework/ctx-management/web.adoc[]
|
||||
**** xref:testing/testcontext-framework/ctx-management/web-mocks.adoc[]
|
||||
**** xref:testing/testcontext-framework/ctx-management/caching.adoc[]
|
||||
**** xref:testing/testcontext-framework/ctx-management/failure-threshold.adoc[]
|
||||
**** xref:testing/testcontext-framework/ctx-management/hierarchies.adoc[]
|
||||
*** xref:testing/testcontext-framework/fixture-di.adoc[]
|
||||
*** xref:testing/testcontext-framework/web-scoped-beans.adoc[]
|
||||
@@ -266,6 +267,7 @@
|
||||
***** xref:web/webmvc/mvc-controller/ann-methods/jackson.adoc[]
|
||||
**** xref:web/webmvc/mvc-controller/ann-modelattrib-methods.adoc[]
|
||||
**** xref:web/webmvc/mvc-controller/ann-initbinder.adoc[]
|
||||
**** xref:web/webmvc/mvc-controller/ann-validation.adoc[]
|
||||
**** xref:web/webmvc/mvc-controller/ann-exceptionhandler.adoc[]
|
||||
**** xref:web/webmvc/mvc-controller/ann-advice.adoc[]
|
||||
*** xref:web/webmvc-functional.adoc[]
|
||||
@@ -360,6 +362,7 @@
|
||||
***** xref:web/webflux/controller/ann-methods/jackson.adoc[]
|
||||
**** xref:web/webflux/controller/ann-modelattrib-methods.adoc[]
|
||||
**** xref:web/webflux/controller/ann-initbinder.adoc[]
|
||||
**** xref:web/webflux/controller/ann-validation.adoc[]
|
||||
**** xref:web/webflux/controller/ann-exceptions.adoc[]
|
||||
**** xref:web/webflux/controller/ann-advice.adoc[]
|
||||
*** xref:web/webflux-functional.adoc[]
|
||||
@@ -414,6 +417,7 @@
|
||||
*** xref:integration/cache/plug.adoc[]
|
||||
*** xref:integration/cache/specific-config.adoc[]
|
||||
** xref:integration/observability.adoc[]
|
||||
** xref:integration/checkpoint-restore.adoc[]
|
||||
** xref:integration/appendix.adoc[]
|
||||
* xref:languages.adoc[]
|
||||
** xref:languages/kotlin.adoc[]
|
||||
|
||||
@@ -39,11 +39,6 @@ resolvable otherwise. See
|
||||
{api-spring-framework}++/core/env/AbstractEnvironment.html#IGNORE_GETENV_PROPERTY_NAME++[`AbstractEnvironment`]
|
||||
for details.
|
||||
|
||||
| `spring.index.ignore`
|
||||
| Instructs Spring to ignore the components index located in
|
||||
`META-INF/spring.components`. See xref:core/beans/classpath-scanning.adoc#beans-scanning-index[Generating an Index of Candidate Components]
|
||||
.
|
||||
|
||||
| `spring.jdbc.getParameterType.ignore`
|
||||
| Instructs Spring to ignore `java.sql.ParameterMetaData.getParameterType` completely.
|
||||
See the note in xref:data-access/jdbc/advanced.adoc#jdbc-batch-list[Batch Operations with a List of Objects].
|
||||
@@ -60,19 +55,27 @@ for details.
|
||||
{api-spring-framework}++/objenesis/SpringObjenesis.html#IGNORE_OBJENESIS_PROPERTY_NAME++[`SpringObjenesis`]
|
||||
for details.
|
||||
|
||||
| `spring.test.aot.processing.failOnError`
|
||||
| A boolean flag that controls whether errors encountered during AOT processing in the
|
||||
_Spring TestContext Framework_ should result in an exception that fails the overall process.
|
||||
See xref:testing/testcontext-framework/aot.adoc[Ahead of Time Support for Tests].
|
||||
|
||||
| `spring.test.constructor.autowire.mode`
|
||||
| The default _test constructor autowire mode_ to use if `@TestConstructor` is not present
|
||||
on a test class. See xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[Changing the default test constructor autowire mode]
|
||||
.
|
||||
on a test class. See xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[Changing the default test constructor autowire mode].
|
||||
|
||||
| `spring.test.context.cache.maxSize`
|
||||
| The maximum size of the context cache in the _Spring TestContext Framework_. See
|
||||
xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching].
|
||||
|
||||
| `spring.test.context.failure.threshold`
|
||||
| The failure threshold for errors encountered while attempting to load an `ApplicationContext`
|
||||
in the _Spring TestContext Framework_. See
|
||||
xref:testing/testcontext-framework/ctx-management/failure-threshold.adoc[Context Failure Threshold].
|
||||
|
||||
| `spring.test.enclosing.configuration`
|
||||
| The default _enclosing configuration inheritance mode_ to use if
|
||||
`@NestedTestConfiguration` is not present on a test class. See
|
||||
xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration[Changing the default enclosing configuration inheritance mode]
|
||||
.
|
||||
xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration[Changing the default enclosing configuration inheritance mode].
|
||||
|
||||
|===
|
||||
|
||||
@@ -728,11 +728,6 @@ of determining parameter names, an exception will be thrown.
|
||||
`StandardReflectionParameterNameDiscoverer` :: Uses the standard `java.lang.reflect.Parameter`
|
||||
API to determine parameter names. Requires that code be compiled with the `-parameters`
|
||||
flag for `javac`. Recommended approach on Java 8+.
|
||||
`LocalVariableTableParameterNameDiscoverer` :: Analyzes the local variable table available
|
||||
in the byte code of the advice class to determine parameter names from debug information.
|
||||
Requires that code be compiled with debug symbols (`-g:vars` at a minimum). Deprecated
|
||||
as of Spring Framework 6.0 for removal in Spring Framework 6.1 in favor of compiling
|
||||
code with `-parameters`. Not supported in a GraalVM native image.
|
||||
`AspectJAdviceParameterNameDiscoverer` :: Deduces parameter names from the pointcut
|
||||
expression, `returning`, and `throwing` clauses. See the
|
||||
{api-spring-framework}/aop/aspectj/AspectJAdviceParameterNameDiscoverer.html[javadoc]
|
||||
|
||||
@@ -256,6 +256,13 @@ Java::
|
||||
|
||||
If you are registering bean definitions programmatically, consider using `RootBeanBefinition` as it allows to specify a `ResolvableType` that handles generics.
|
||||
|
||||
[[aot.bestpractices.constructors]]
|
||||
=== Avoid Multiple Constructors
|
||||
The container is able to choose the most appropriate constructor to use based on several candidates.
|
||||
However, this is not a best practice and flagging the preferred constructor with `@Autowired` if necessary is preferred.
|
||||
|
||||
In case you are working on a code base that you can't modify, you can set the {api-spring-framework}/beans/factory/support/AbstractBeanDefinition.html#PREFERRED_CONSTRUCTORS_ATTRIBUTE[`preferredConstructors` attribute] on the related bean definition to indicate which constructor should be used.
|
||||
|
||||
[[aot.bestpractices.factory-bean]]
|
||||
=== FactoryBean
|
||||
|
||||
|
||||
@@ -989,68 +989,4 @@ metadata is provided per-instance rather than per-class.
|
||||
|
||||
|
||||
|
||||
[[beans-scanning-index]]
|
||||
== Generating an Index of Candidate Components
|
||||
|
||||
While classpath scanning is very fast, it is possible to improve the startup performance
|
||||
of large applications by creating a static list of candidates at compilation time. In this
|
||||
mode, all modules that are targets of component scanning must use this mechanism.
|
||||
|
||||
NOTE: Your existing `@ComponentScan` or `<context:component-scan/>` directives must remain
|
||||
unchanged to request the context to scan candidates in certain packages. When the
|
||||
`ApplicationContext` detects such an index, it automatically uses it rather than scanning
|
||||
the classpath.
|
||||
|
||||
To generate the index, add an additional dependency to each module that contains
|
||||
components that are targets for component scan directives. The following example shows
|
||||
how to do so with Maven:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes,attributes"]
|
||||
----
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-context-indexer</artifactId>
|
||||
<version>{spring-version}</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
----
|
||||
|
||||
With Gradle 4.5 and earlier, the dependency should be declared in the `compileOnly`
|
||||
configuration, as shown in the following example:
|
||||
|
||||
[source,groovy,indent=0,subs="verbatim,quotes,attributes"]
|
||||
----
|
||||
dependencies {
|
||||
compileOnly "org.springframework:spring-context-indexer:{spring-version}"
|
||||
}
|
||||
----
|
||||
|
||||
With Gradle 4.6 and later, the dependency should be declared in the `annotationProcessor`
|
||||
configuration, as shown in the following example:
|
||||
|
||||
[source,groovy,indent=0,subs="verbatim,quotes,attributes"]
|
||||
----
|
||||
dependencies {
|
||||
annotationProcessor "org.springframework:spring-context-indexer:{spring-version}"
|
||||
}
|
||||
----
|
||||
|
||||
The `spring-context-indexer` artifact generates a `META-INF/spring.components` file that
|
||||
is included in the jar file.
|
||||
|
||||
NOTE: When working with this mode in your IDE, the `spring-context-indexer` must be
|
||||
registered as an annotation processor to make sure the index is up-to-date when
|
||||
candidate components are updated.
|
||||
|
||||
TIP: The index is enabled automatically when a `META-INF/spring.components` file is found
|
||||
on the classpath. If an index is partially available for some libraries (or use cases)
|
||||
but could not be built for the whole application, you can fall back to a regular classpath
|
||||
arrangement (as though no index were present at all) by setting `spring.index.ignore` to
|
||||
`true`, either as a JVM system property or via the
|
||||
xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -771,11 +771,9 @@ resolved to the corresponding value. If not, then `default/path` is used
|
||||
as a default. If no default is specified and a property cannot be resolved, an
|
||||
`IllegalArgumentException` is thrown.
|
||||
|
||||
NOTE: The `@PropertySource` annotation is repeatable, according to Java 8 conventions.
|
||||
However, all such `@PropertySource` annotations need to be declared at the same
|
||||
level, either directly on the configuration class or as meta-annotations within the
|
||||
same custom annotation. Mixing direct annotations and meta-annotations is not
|
||||
recommended, since direct annotations effectively override meta-annotations.
|
||||
NOTE: `@PropertySource` can be used as a repeatable annotation. `@PropertySource`
|
||||
may also be used as a meta-annotation to create custom composed annotations with
|
||||
attribute overrides.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -46,7 +46,9 @@ The expression language supports the following functionality:
|
||||
* Inline maps
|
||||
* Ternary operator
|
||||
* Variables
|
||||
* User-defined functions
|
||||
* User-defined functions added to the context
|
||||
* reflective invocation of `Method`
|
||||
* various cases of `MethodHandle`
|
||||
* Collection projection
|
||||
* Collection selection
|
||||
* Templated expressions
|
||||
|
||||
@@ -15,7 +15,7 @@ topics:
|
||||
* xref:core/expressions/language-ref/types.adoc[Types]
|
||||
* xref:core/expressions/language-ref/constructors.adoc[Constructors]
|
||||
* xref:core/expressions/language-ref/variables.adoc[Variables]
|
||||
* xref:core/expressions/language-ref/functions.adoc[Functions]
|
||||
* xref:core/expressions/language-ref/functions.adoc[User-Defined Functions]
|
||||
* xref:core/expressions/language-ref/bean-references.adoc[Bean References]
|
||||
* xref:core/expressions/language-ref/operator-ternary.adoc[Ternary Operator (If-Then-Else)]
|
||||
* xref:core/expressions/language-ref/operator-elvis.adoc[The Elvis Operator]
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
You can extend SpEL by registering user-defined functions that can be called within the
|
||||
expression string. The function is registered through the `EvaluationContext`. The
|
||||
following example shows how to register a user-defined function:
|
||||
following example shows how to register a user-defined function to be invoked via reflection
|
||||
(i.e. a `Method`):
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -94,5 +95,97 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
The use of `MethodHandle` is also supported. This enables potentially more efficient use
|
||||
cases if the `MethodHandle` target and parameters have been fully bound prior to
|
||||
registration, but partially bound handles are also supported.
|
||||
|
||||
Consider the `String#formatted(String, Object...)` instance method, which produces a
|
||||
message according to a template and a variable number of arguments.
|
||||
|
||||
You can register and use the `formatted` method as a `MethodHandle`, as the following
|
||||
example shows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
|
||||
|
||||
MethodHandle mh = MethodHandles.lookup().findVirtual(String.class, "formatted",
|
||||
MethodType.methodType(String.class, Object[].class));
|
||||
context.setVariable("message", mh);
|
||||
|
||||
String message = parser.parseExpression("#message('Simple message: <%s>', 'Hello World', 'ignored')")
|
||||
.getValue(context, String.class);
|
||||
//returns "Simple message: <Hello World>"
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
val parser = SpelExpressionParser()
|
||||
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()
|
||||
|
||||
val mh = MethodHandles.lookup().findVirtual(String::class.java, "formatted",
|
||||
MethodType.methodType(String::class.java, Array<Any>::class.java))
|
||||
context.setVariable("message", mh)
|
||||
|
||||
val message = parser.parseExpression("#message('Simple message: <%s>', 'Hello World', 'ignored')")
|
||||
.getValue(context, String::class.java)
|
||||
----
|
||||
======
|
||||
|
||||
As hinted above, binding a `MethodHandle` and registering the bound `MethodHandle` is also
|
||||
supported. This is likely to be more performant if both the target and all the arguments
|
||||
are bound. In that case no arguments are necessary in the SpEL expression, as the
|
||||
following example shows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
|
||||
|
||||
String template = "This is a %s message with %s words: <%s>";
|
||||
Object varargs = new Object[] { "prerecorded", 3, "Oh Hello World!", "ignored" };
|
||||
MethodHandle mh = MethodHandles.lookup().findVirtual(String.class, "formatted",
|
||||
MethodType.methodType(String.class, Object[].class))
|
||||
.bindTo(template)
|
||||
.bindTo(varargs); //here we have to provide arguments in a single array binding
|
||||
context.setVariable("message", mh);
|
||||
|
||||
String message = parser.parseExpression("#message()")
|
||||
.getValue(context, String.class);
|
||||
//returns "This is a prerecorded message with 3 words: <Oh Hello World!>"
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
val parser = SpelExpressionParser()
|
||||
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()
|
||||
|
||||
val template = "This is a %s message with %s words: <%s>"
|
||||
val varargs = arrayOf("prerecorded", 3, "Oh Hello World!", "ignored")
|
||||
|
||||
val mh = MethodHandles.lookup().findVirtual(String::class.java, "formatted",
|
||||
MethodType.methodType(String::class.java, Array<Any>::class.java))
|
||||
.bindTo(template)
|
||||
.bindTo(varargs) //here we have to provide arguments in a single array binding
|
||||
context.setVariable("message", mh)
|
||||
|
||||
val message = parser.parseExpression("#message()")
|
||||
.getValue(context, String::class.java)
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -38,6 +38,10 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
NOTE: The SpEL Elvis operator also checks for _empty_ Strings in addition to `null` objects.
|
||||
The original snippet is thus only close to emulating the semantics of the operator (it would need an
|
||||
additional `!name.isEmpty()` check).
|
||||
|
||||
The following listing shows a more complex example:
|
||||
|
||||
[tabs]
|
||||
@@ -53,7 +57,7 @@ Java::
|
||||
String name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String.class);
|
||||
System.out.println(name); // Nikola Tesla
|
||||
|
||||
tesla.setName(null);
|
||||
tesla.setName("");
|
||||
name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String.class);
|
||||
System.out.println(name); // Elvis Presley
|
||||
----
|
||||
@@ -69,7 +73,7 @@ Kotlin::
|
||||
var name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String::class.java)
|
||||
println(name) // Nikola Tesla
|
||||
|
||||
tesla.setName(null)
|
||||
tesla.setName("")
|
||||
name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String::class.java)
|
||||
println(name) // Elvis Presley
|
||||
----
|
||||
|
||||
@@ -1,5 +1,50 @@
|
||||
[[beans-binding]]
|
||||
= Data Binding
|
||||
|
||||
Data binding is useful for binding user input to a target object where user input is a map
|
||||
with property paths as keys, following xref:beans-beans-conventions[JavaBeans conventions].
|
||||
`DataBinder` is the main class that supports this, and it provides two ways to bind user
|
||||
input:
|
||||
|
||||
- xref:beans-constructor-binding[Constructor binding] - bind user input to a public data
|
||||
constructor, looking up constructor argument values in the user input.
|
||||
- xref:beans-beans[Property binding] - bind user input to setters, matching keys from the
|
||||
the user input to properties of the target object structure.
|
||||
|
||||
You can apply both constructor and property binding or only one.
|
||||
|
||||
|
||||
[[beans-constructor-binding]]
|
||||
== Constructor Binding
|
||||
|
||||
To use constructor binding:
|
||||
|
||||
1. Create a `DataBinder` with `null` as the target object.
|
||||
2. Set `targetType` to the target class.
|
||||
3. Call `construct`.
|
||||
|
||||
The target class should have a single public constructor or a single non-public constructor
|
||||
with arguments. If there are multiple constructors, then a default constructor if present
|
||||
is used.
|
||||
|
||||
By default, constructor parameter names are used to look up argument values, but you can
|
||||
configure a `NameResolver`. Spring MVC and WebFlux both rely to allow customizing the name
|
||||
of the value to bind through an `@BindParam` annotation on constructor parameters.
|
||||
|
||||
xref:beans-beans-conventions[Type conversion] is applied as needed to convert user input.
|
||||
If the constructor parameter is an object, it is constructed recursively in the same
|
||||
manner, but through a nested property path. That means constructor binding creates both
|
||||
the target object and any objects it contains.
|
||||
|
||||
Binding and conversion errors are reflected in the `BindingResult` of the `DataBinder`.
|
||||
If the target is created successfully, then `target` is set to the created instance
|
||||
after the call to `construct`.
|
||||
|
||||
|
||||
|
||||
|
||||
[[beans-beans]]
|
||||
= Bean Manipulation and the `BeanWrapper`
|
||||
== Property Binding with `BeanWrapper`
|
||||
|
||||
The `org.springframework.beans` package adheres to the JavaBeans standard.
|
||||
A JavaBean is a class with a default no-argument constructor and that follows
|
||||
@@ -26,7 +71,7 @@ perform actions on that bean, such as setting and retrieving properties.
|
||||
|
||||
|
||||
[[beans-beans-conventions]]
|
||||
== Setting and Getting Basic and Nested Properties
|
||||
=== Setting and Getting Basic and Nested Properties
|
||||
|
||||
Setting and getting properties is done through the `setPropertyValue` and
|
||||
`getPropertyValue` overloaded method variants of `BeanWrapper`. See their Javadoc for
|
||||
@@ -192,7 +237,7 @@ Kotlin::
|
||||
|
||||
|
||||
[[beans-beans-conversion]]
|
||||
== Built-in `PropertyEditor` Implementations
|
||||
== ``PropertyEditor``'s
|
||||
|
||||
Spring uses the concept of a `PropertyEditor` to effect the conversion between an
|
||||
`Object` and a `String`. It can be handy
|
||||
@@ -378,7 +423,7 @@ Kotlin::
|
||||
|
||||
|
||||
[[beans-beans-conversion-customeditor-registration]]
|
||||
=== Registering Additional Custom `PropertyEditor` Implementations
|
||||
=== Custom ``PropertyEditor``'s
|
||||
|
||||
When setting bean properties as string values, a Spring IoC container ultimately uses
|
||||
standard JavaBeans `PropertyEditor` implementations to convert these strings to the complex type of the
|
||||
@@ -521,7 +566,7 @@ Finally, the following example shows how to use `CustomEditorConfigurer` to regi
|
||||
----
|
||||
|
||||
[[beans-beans-conversion-customeditor-registration-per]]
|
||||
==== Using `PropertyEditorRegistrar`
|
||||
=== `PropertyEditorRegistrar`
|
||||
|
||||
Another mechanism for registering property editors with the Spring container is to
|
||||
create and use a `PropertyEditorRegistrar`. This interface is particularly useful when
|
||||
|
||||
@@ -123,15 +123,12 @@ Validator, is expected to be present in the classpath and is automatically detec
|
||||
|
||||
|
||||
[[validation-beanvalidation-spring-inject]]
|
||||
=== Injecting a Validator
|
||||
=== Inject Jakarta Validator
|
||||
|
||||
`LocalValidatorFactoryBean` implements both `jakarta.validation.ValidatorFactory` and
|
||||
`jakarta.validation.Validator`, as well as Spring's `org.springframework.validation.Validator`.
|
||||
You can inject a reference to either of these interfaces into beans that need to invoke
|
||||
validation logic.
|
||||
|
||||
You can inject a reference to `jakarta.validation.Validator` if you prefer to work with the Bean
|
||||
Validation API directly, as the following example shows:
|
||||
`jakarta.validation.Validator`, so you can inject a reference to the latter to
|
||||
apply validation logic if you prefer to work with the Bean Validation API directly,
|
||||
as the following example shows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -160,8 +157,15 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
You can inject a reference to `org.springframework.validation.Validator` if your bean
|
||||
requires the Spring Validation API, as the following example shows:
|
||||
|
||||
[[validation-beanvalidation-spring-inject-adapter]]
|
||||
=== Inject Spring Validator
|
||||
|
||||
In addition to implementing `jakarta.validation.Validator`, `LocalValidatorFactoryBean`
|
||||
also adapts to `org.springframework.validation.Validator`, so you can inject a reference
|
||||
to the latter if your bean requires the Spring Validation API.
|
||||
|
||||
For example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -190,9 +194,15 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
When used as `org.springframework.validation.Validator`, `LocalValidatorFactoryBean`
|
||||
invokes the underlying `jakarta.validation.Validator`, and then adapts
|
||||
``ContraintViolation``s to ``FieldError``s, and registers them with the `Errors` object
|
||||
passed into the `validate` method.
|
||||
|
||||
|
||||
|
||||
[[validation-beanvalidation-spring-constraints]]
|
||||
=== Configuring Custom Constraints
|
||||
=== Configure Custom Constraints
|
||||
|
||||
Each bean validation constraint consists of two parts:
|
||||
|
||||
@@ -274,9 +284,8 @@ As the preceding example shows, a `ConstraintValidator` implementation can have
|
||||
[[validation-beanvalidation-spring-method]]
|
||||
=== Spring-driven Method Validation
|
||||
|
||||
You can integrate the method validation feature supported by Bean Validation 1.1 (and, as
|
||||
a custom extension, also by Hibernate Validator 4.3) into a Spring context through a
|
||||
`MethodValidationPostProcessor` bean definition:
|
||||
You can integrate the method validation feature of Bean Validation into a
|
||||
Spring context through a `MethodValidationPostProcessor` bean definition:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -305,11 +314,11 @@ XML::
|
||||
----
|
||||
======
|
||||
|
||||
To be eligible for Spring-driven method validation, all target classes need to be annotated
|
||||
To be eligible for Spring-driven method validation, target classes need to be annotated
|
||||
with Spring's `@Validated` annotation, which can optionally also declare the validation
|
||||
groups to use. See
|
||||
{api-spring-framework}/validation/beanvalidation/MethodValidationPostProcessor.html[`MethodValidationPostProcessor`]
|
||||
for setup details with the Hibernate Validator and Bean Validation 1.1 providers.
|
||||
for setup details with the Hibernate Validator and Bean Validation providers.
|
||||
|
||||
[TIP]
|
||||
====
|
||||
@@ -320,8 +329,141 @@ xref:core/aop/proxying.adoc#aop-understanding-aop-proxies[Understanding AOP Prox
|
||||
to always use methods and accessors on proxied classes; direct field access will not work.
|
||||
====
|
||||
|
||||
NOTE: Spring MVC and WebFlux have built-in support for method validation, and therefore
|
||||
for web controller methods there is no need for a class level `@Validated` and an AOP proxy.
|
||||
See the Spring MVC xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation] section,
|
||||
the WebFlux xref:web/webflux/controller/ann-validation.adoc[Validation] section,
|
||||
and the xref:web/webmvc/mvc-controller/ann-validation.adoc[Error Responses] section.
|
||||
|
||||
|
||||
[[validation-beanvalidation-spring-method-exceptions]]
|
||||
==== Method Validation Exceptions
|
||||
|
||||
By default, `jakarta.validation.ConstraintViolationException` is raised with the set of
|
||||
``ConstraintViolation``s returned by `jakarata.validation.Validator`. As an alternative,
|
||||
you can have `MethodValidationException` raised instead with ``ConstraintViolation``s
|
||||
adapted to `MessageSourceResolvable` errors. To enable set the following flag:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
|
||||
|
||||
@Configuration
|
||||
public class AppConfig {
|
||||
|
||||
@Bean
|
||||
public MethodValidationPostProcessor validationPostProcessor() {
|
||||
MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
|
||||
processor.setAdaptConstraintViolations(true);
|
||||
return processor;
|
||||
}
|
||||
}
|
||||
|
||||
----
|
||||
|
||||
XML::
|
||||
+
|
||||
[source,xml,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor">
|
||||
<property name="adaptConstraintViolations" value="true"/>
|
||||
</bean>
|
||||
----
|
||||
======
|
||||
|
||||
`MethodValidationException` contains a list of ``ParameterValidationResult``s which
|
||||
group errors by method parameter, and each exposes a `MethodParameter`, the argument
|
||||
value, and a list of `MessageSourceResolvable` errors adapted from
|
||||
``ConstraintViolation``s. For `@Valid` method parameters with cascaded violations on
|
||||
fields and properties, the `ParameterValidationResult` is `ParameterErrors` which
|
||||
implements `org.springframework.validation.Errors` and exposes validation errors as
|
||||
``FieldError``s.
|
||||
|
||||
|
||||
[[validation-beanvalidation-spring-method-i18n]]
|
||||
==== Customizing Validation Errors
|
||||
|
||||
The adapted `MessageSourceResolvable` errors can be turned into error messages to
|
||||
display to users through the configured `MessageSource` with locale and language specific
|
||||
resource bundles. This section provides an example for illustration.
|
||||
|
||||
Given the following class declarations:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
record Person(@Size(min = 1, max = 10) String name) {
|
||||
}
|
||||
|
||||
@Validated
|
||||
public class MyService {
|
||||
|
||||
void addStudent(@Valid Person person, @Max(2) int degrees) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@JvmRecord
|
||||
internal data class Person(@Size(min = 1, max = 10) val name: String)
|
||||
|
||||
@Validated
|
||||
class MyService {
|
||||
|
||||
fun addStudent(person: @Valid Person?, degrees: @Max(2) Int) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
A `ConstraintViolation` on `Person.name()` is adapted to a `FieldErrro` with the following:
|
||||
|
||||
- Error codes `"Size.student.name"`, `"Size.name"`, `"Size.java.lang.String"`, and `"Size"`
|
||||
- Message arguments `"name"`, `10`, and `1` (the field name and the constraint attributes)
|
||||
- Default message "size must be between 1 and 10"
|
||||
|
||||
To customize the default message, you can add properties to
|
||||
xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource]
|
||||
resource bundles using any of the above errors codes and message arguments. Note also that the
|
||||
message argument `"name"` is itself a `MessagreSourceResolvable` with error codes
|
||||
`"student.name"` and `"name"` and can customized too. For example:
|
||||
|
||||
Properties::
|
||||
+
|
||||
[source,properties,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
Size.student.name=Please, provide a {0} that is between {2} and {1} characters long
|
||||
student.name=username
|
||||
----
|
||||
|
||||
A `ConstraintViolation` on the `degrees` method parameter is adapted to a
|
||||
`MessageSourceResolvable` with the following:
|
||||
|
||||
- Error codes `"Max.myService#addStudent.degrees"`, `"Max.degrees"`, `"Max.int"`, `"Max"`
|
||||
- Message arguments "degrees2 and 2 (the field name and the constraint attribute)
|
||||
- Default message "must be less than or equal to 2"
|
||||
|
||||
To customize the above default message, you can add a property such as:
|
||||
|
||||
Properties::
|
||||
+
|
||||
[source,properties,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
Max.degrees=You cannot provide more than {1} {0}
|
||||
----
|
||||
|
||||
|
||||
[[validation-beanvalidation-spring-other]]
|
||||
=== Additional Configuration Options
|
||||
|
||||
@@ -195,6 +195,12 @@ of Spring Web MVC, you can use the `<spring:bind/>` tag to inspect the error mes
|
||||
you can also inspect the `Errors` object yourself. More information about the
|
||||
methods it offers can be found in the {api-spring-framework}/validation/Errors.html[javadoc].
|
||||
|
||||
Validators may also get locally invoked for the immediate validation of a given object,
|
||||
not involving a binding process. As of 6.1, this has been simplified through a new
|
||||
`Validator.validateObject(Object)` method which is available by default now, returning
|
||||
a simple ´Errors` representation which can be inspected: typically calling `hasErrors()`
|
||||
or the new `failOnError` method for turning the error summary message into an exception
|
||||
(e.g. `validator.validateObject(myObject).failOnError(IllegalArgumentException::new)`).
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ including error handling. It includes the following topics:
|
||||
|
||||
* xref:data-access/jdbc/core.adoc#jdbc-JdbcTemplate[Using `JdbcTemplate`]
|
||||
* xref:data-access/jdbc/core.adoc#jdbc-NamedParameterJdbcTemplate[Using `NamedParameterJdbcTemplate`]
|
||||
* xref:data-access/jdbc/core.adoc#jdbc-JdbcClient[Unified JDBC Query/Update Operations: `JdbcClient`]
|
||||
* xref:data-access/jdbc/core.adoc#jdbc-SQLExceptionTranslator[Using `SQLExceptionTranslator`]
|
||||
* xref:data-access/jdbc/core.adoc#jdbc-statements-executing[Running Statements]
|
||||
* xref:data-access/jdbc/core.adoc#jdbc-statements-querying[Running Queries]
|
||||
@@ -701,6 +702,120 @@ See also xref:data-access/jdbc/core.adoc#jdbc-JdbcTemplate-idioms[`JdbcTemplate`
|
||||
for guidelines on using the `NamedParameterJdbcTemplate` class in the context of an application.
|
||||
|
||||
|
||||
[[jdbc-JdbcClient]]
|
||||
== Unified JDBC Query/Update Operations: `JdbcClient`
|
||||
|
||||
As of 6.1, the named parameter statements of `NamedParameterJdbcTemplate` and the positional
|
||||
parameter statements of a regular `JdbcTemplate` are available through a unified client API
|
||||
with a fluent interaction model.
|
||||
|
||||
E.g. with positional parameters:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
private JdbcClient jdbcClient = JdbcClient.create(dataSource);
|
||||
|
||||
public int countOfActorsByFirstName(String firstName) {
|
||||
return this.jdbcClient.sql("select count(*) from t_actor where first_name = ?")
|
||||
.param(firstName);
|
||||
.query(Integer.class).single();
|
||||
}
|
||||
----
|
||||
|
||||
E.g. with named parameters:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
private JdbcClient jdbcClient = JdbcClient.create(dataSource);
|
||||
|
||||
public int countOfActorsByFirstName(String firstName) {
|
||||
return this.jdbcClient.sql("select count(*) from t_actor where first_name = :firstName")
|
||||
.param("firstName", firstName);
|
||||
.query(Integer.class).single();
|
||||
}
|
||||
----
|
||||
|
||||
`RowMapper` capabilities are available as well, with flexible result resolution:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
List<Actor> actors = this.jdbcClient.sql("select first_name, last_name from t_actor")
|
||||
.query((rs, rowNum) -> new Actor(rs.getString("first_name"), rs.getString("last_name")))
|
||||
.list();
|
||||
----
|
||||
|
||||
Instead of a custom `RowMapper`, you may also specify a class to map to.
|
||||
E.g. assuming that `Actor` has `firstName` and `lastName` properties
|
||||
as a record class, a custom constructor, bean properties, or plain fields:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
List<Actor> actors = this.jdbcClient.sql("select first_name, last_name from t_actor")
|
||||
.query(Actor.class)
|
||||
.list();
|
||||
----
|
||||
|
||||
With a required single object result:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
Actor actor = this.jdbcClient.sql("select first_name, last_name from t_actor where id = ?",
|
||||
.param(1212L);
|
||||
.query(Actor.class)
|
||||
.single();
|
||||
----
|
||||
|
||||
With a `java.util.Optional` result:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
Optional<Actor> actor = this.jdbcClient.sql("select first_name, last_name from t_actor where id = ?",
|
||||
.param(1212L);
|
||||
.query(Actor.class)
|
||||
.optional();
|
||||
----
|
||||
|
||||
And for an update statement:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (?, ?)")
|
||||
.param("Leonor").param("Watling");
|
||||
.update();
|
||||
----
|
||||
|
||||
Or an update statement with named parameters:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (:firstName, :lastName)")
|
||||
.param("firstName", "Leonor").param("lastName", "Watling");
|
||||
.update();
|
||||
----
|
||||
|
||||
Instead of individual named parameters, you may also specify a parameter source object,
|
||||
e.g. a record class or a class with bean properties or a plain field holder which
|
||||
provides `firstName` and `lastName` properties, such as the `Actor` class from above:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (:firstName, :lastName)")
|
||||
.paramSource(new Actor("Leonor", "Watling");
|
||||
.update();
|
||||
----
|
||||
|
||||
The automatic `Actor` class mapping for parameters as well as the query results above is
|
||||
provided through implicit `SimplePropertySqlParameterSource` and `SimplePropertyRowMapper`
|
||||
strategies which are also available for direct use. They can serve as a common replacement
|
||||
for `BeanPropertySqlParameterSource` and `BeanPropertyRowMapper`/`DataClassRowMapper`,
|
||||
also with `JdbcTemplate` and `NamedParameterJdbcTemplate` themselves.
|
||||
|
||||
NOTE: `JdbcClient` is a flexible but simplified facade for JDBC query/update statements.
|
||||
Advanced capabilities such as batch inserts and stored procedure calls typically require
|
||||
extra customization: consider Spring's `SimpleJdbcInsert` and `SimpleJdbcCall` classes or
|
||||
plain direct `JdbcTemplate` usage for any such capabilities not available on `JdbcClient`.
|
||||
|
||||
|
||||
[[jdbc-SQLExceptionTranslator]]
|
||||
== Using `SQLExceptionTranslator`
|
||||
|
||||
|
||||
@@ -254,6 +254,25 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
Alternatively, there is a shortcut for mapping to a single value:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
Flux<String> names = client.sql("SELECT name FROM person")
|
||||
.mapValue(String.class)
|
||||
.all();
|
||||
----
|
||||
|
||||
Or you may map to a result object with bean properties or record components:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
// assuming a name property on Person
|
||||
Flux<Person> persons = client.sql("SELECT name FROM person")
|
||||
.mapProperties(Person.class)
|
||||
.all();
|
||||
----
|
||||
|
||||
[[r2dbc-DatabaseClient-mapping-null]]
|
||||
.What about `null`?
|
||||
****
|
||||
@@ -324,6 +343,27 @@ The following example shows parameter binding for a query:
|
||||
.bind("age", 34);
|
||||
----
|
||||
|
||||
Alternatively, you may pass in a map of names and values:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
Map<String, Object> params = new LinkedHashMap<>();
|
||||
params.put("id", "joe");
|
||||
params.put("name", "Joe");
|
||||
params.put("age", 34);
|
||||
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
|
||||
.bindValues(params);
|
||||
----
|
||||
|
||||
Or you may pass in a parameter object with bean properties or record components:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
// assuming id, name, age properties on Person
|
||||
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
|
||||
.bindProperties(new Person("joe", "Joe", 34);
|
||||
----
|
||||
|
||||
.R2DBC Native Bind Markers
|
||||
****
|
||||
R2DBC uses database-native bind markers that depend on the actual database vendor.
|
||||
|
||||
@@ -57,10 +57,14 @@ attribute of the annotation to `true`.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
`@TransactionalEventListener` only works with thread-bound transactions managed by
|
||||
`PlatformTransactionManager`. A reactive transaction managed by `ReactiveTransactionManager`
|
||||
uses the Reactor context instead of thread-local attributes, so from the perspective of
|
||||
an event listener, there is no compatible active transaction that it can participate in.
|
||||
As of 6.1, `@TransactionalEventListener` can work with thread-bound transactions managed by
|
||||
`PlatformTransactionManager` as well as reactive transactions managed by `ReactiveTransactionManager`.
|
||||
For the former, listeners are guaranteed to see the current thread-bound transaction.
|
||||
Since the latter uses the Reactor context instead of thread-local variables, the transaction
|
||||
context needs to be included in the published event instance as the event source.
|
||||
See the
|
||||
{api-spring-framework}/transaction/reactive/TransactionalEventPublisher.html[`TransactionalEventPublisher`]
|
||||
javadoc for details.
|
||||
====
|
||||
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ STOMP Messaging.
|
||||
xref:web-reactive.adoc[Web Reactive] :: Spring WebFlux, WebClient,
|
||||
WebSocket, RSocket.
|
||||
xref:integration.adoc[Integration] :: REST Clients, JMS, JCA, JMX,
|
||||
Email, Tasks, Scheduling, Caching, Observability.
|
||||
Email, Tasks, Scheduling, Caching, Observability, JVM Checkpoint Restore.
|
||||
xref:languages.adoc[Languages] :: Kotlin, Groovy, Dynamic Languages.
|
||||
xref:testing/appendix.adoc[Appendix] :: Spring properties.
|
||||
https://github.com/spring-projects/spring-framework/wiki[Wiki] :: What's New,
|
||||
|
||||
@@ -208,6 +208,65 @@ NOTE: This is an optional feature, and your favorite cache library may not suppo
|
||||
All `CacheManager` implementations provided by the core framework support it. See the
|
||||
documentation of your cache provider for more details.
|
||||
|
||||
[[cache-annotations-cacheable-reactive]]
|
||||
=== Caching with CompletableFuture and Reactive Return Types
|
||||
|
||||
As of 6.1, cache annotations take `CompletableFuture` and reactive return types
|
||||
into account, automatically adapting the cache interaction accordingly.
|
||||
|
||||
For a method returning a `CompletableFuture`, the object produced by that future
|
||||
will be cached whenever it is complete, and the cache lookup for a cache hit will
|
||||
be retrieved via a `CompletableFuture`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Cacheable("books")
|
||||
public CompletableFuture<Book> findBook(ISBN isbn) {...}
|
||||
----
|
||||
|
||||
For a method returning a Reactor `Mono`, the object emitted by that Reactive Streams
|
||||
publisher will be cached whenever it is available, and the cache lookup for a cache
|
||||
hit will be retrieved as a `Mono` (backed by a `CompletableFuture`):
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Cacheable("books")
|
||||
public Mono<Book> findBook(ISBN isbn) {...}
|
||||
----
|
||||
|
||||
For a method returning a Reactor `Flux`, the objects emitted by that Reactive Streams
|
||||
publisher will be collected into a `List` and cached whenever that list is complete,
|
||||
and the cache lookup for a cache hit will be retrieved as a `Flux` (backed by a
|
||||
`CompletableFuture` for the cached `List` value):
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Cacheable("books")
|
||||
public Flux<Book> findBooks(String author) {...}
|
||||
----
|
||||
|
||||
Such `CompletableFuture` and reactive adaptation also works for synchronized caching,
|
||||
computing the value only once in case of a concurrent cache miss:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Cacheable(cacheNames="foos", sync=true) <1>
|
||||
public CompletableFuture<Foo> executeExpensiveOperation(String id) {...}
|
||||
----
|
||||
<1> Using the `sync` attribute.
|
||||
|
||||
NOTE: In order for such an arrangement to work at runtime, the configured cache
|
||||
needs to be capable of `CompletableFuture`-based retrieval. The Spring-provided
|
||||
`ConcurrentMapCacheManager` automatically adapts to that retrieval style, and
|
||||
`CaffeineCacheManager` natively supports it when its asynchronous cache mode is
|
||||
enabled: set `setAsyncCacheMode(true)` on your `CaffeineCacheManager` instance.
|
||||
|
||||
Last but not least, be aware that annotation-driven caching is not appropriate
|
||||
for sophisticated reactive interactions involving composition and back pressure.
|
||||
If you choose to declare `@Cacheable` on specific reactive methods, consider the
|
||||
impact of the rather coarse-granular cache interaction which simply stores the
|
||||
emitted object for a `Mono` or even a pre-collected list of objects for a `Flux`.
|
||||
|
||||
[[cache-annotations-cacheable-condition]]
|
||||
=== Conditional Caching
|
||||
|
||||
@@ -337,6 +396,9 @@ other), such declarations should be avoided. Note also that such conditions shou
|
||||
on the result object (that is, the `#result` variable), as these are validated up-front to
|
||||
confirm the exclusion.
|
||||
|
||||
As of 6.1, `@CachePut` takes `CompletableFuture` and reactive return types into account,
|
||||
performing the put operation whenever the produced object is available.
|
||||
|
||||
|
||||
[[cache-annotations-evict]]
|
||||
== The `@CacheEvict` Annotation
|
||||
@@ -379,6 +441,9 @@ trigger, the return values are ignored (as they do not interact with the cache).
|
||||
not the case with `@Cacheable` which adds data to the cache or updates data in the cache
|
||||
and, thus, requires a result.
|
||||
|
||||
As of 6.1, `@CacheEvict` takes `CompletableFuture` and reactive return types into account,
|
||||
performing an after-invocation evict operation whenever processing has completed.
|
||||
|
||||
|
||||
[[cache-annotations-caching]]
|
||||
== The `@Caching` Annotation
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
[[checkpoint-restore]]
|
||||
= JVM Checkpoint Restore
|
||||
|
||||
The Spring Framework integrates with checkpoint/restore as implemented by https://github.com/CRaC/docs[Project CRaC] in order to allow implementing systems capable to reduce the startup and warmup times of Spring-based Java applications with the JVM.
|
||||
|
||||
Using this feature requires:
|
||||
|
||||
* A checkpoint/restore enabled JVM (Linux only for now).
|
||||
* The presence in the classpath of the https://github.com/CRaC/org.crac[`org.crac:crac`] library.
|
||||
* Specifying the required `java` command line parameters like `-XX:CRaCCheckpointTo=PATH` or `-XX:CRaCRestoreFrom=PATH`.
|
||||
|
||||
WARNING: The files generated in the path specified by `-XX:CRaCCheckpointTo=PATH` when a checkpoint is requested contain a representation of the memory of the running JVM, which may contain secrets and other sensitive data. Using this feature should be done with the assumption that any value "seen" by the JVM, such as configuration properties coming from the environment, will be stored in those CRaC files. As a consequence, the security implications of where and how those files are generated, stored and accessed should be carefully assessed.
|
||||
|
||||
Conceptually, checkpoint and restore match with xref:core/beans/factory-nature.adoc#beans-factory-lifecycle-processor[Spring `Lifecycle` contract] for individual beans.
|
||||
|
||||
== On demand checkpoint/restore of a running application
|
||||
|
||||
A checkpoint can be created on demand, for example using a command like `jcmd application.jar JDK.checkpoint`. Before the creation of the checkpoint, Spring Framework
|
||||
stops all the running beans, giving them a chance to close resources if needed by implementing `Lifecycle.stop`. After restore, the same beans are restarted, with `Lifecycle.start` allowing to reopen resources when relevant. For libraries not depending on Spring, checkpoint/restore custom integration can be provided by implementing `org.crac.Resource` and registering the related instance.
|
||||
|
||||
WARNING: Leveraging checkpoint/restore of a running application typically requires additional lifecycle management to gracefully stop and start using resources like files or sockets and stop active threads.
|
||||
|
||||
NOTE: If the checkpoint is created on a warmed-up JVM, the restored JVM will be equally warmed-up, allowing potentially peak performance immediately. This method typically requires access to remote services, and thus requires some level of platform integration.
|
||||
|
||||
== Automatic checkpoint/restore at startup
|
||||
|
||||
When the `-Dspring.context.checkpoint=onRefresh` Java system property is set, a checkpoint is created automatically during the startup at `LifecycleProcessor.onRefresh` level. At this phase, all non-lazy initialized singletons are instantiated, `InitializingBean.afterPropertiesSet` callbacks have been invoked, but not `Lifecycle.start` ones and `ContextRefreshedEvent` has not yet been published.
|
||||
|
||||
WARNING: As mentioned above, and especially in use cases where the CRaC files are shipped as part of a deployable artifact (a container image for example), operate with the assumption that any sensitive data "seen" by the JVM ends up in the CRaC files, and assess carefully the related security implications.
|
||||
|
||||
NOTE: Here checkpoint/restore is a way to "fast-forward" the startup of the application to a phase where the application context is about to start, but does not allow to have a fully warmed-up JVM.
|
||||
@@ -21,11 +21,20 @@ As outlined xref:integration/observability.adoc[at the beginning of this section
|
||||
|===
|
||||
|Observation name |Description
|
||||
|
||||
|xref:integration/observability.adoc#http-client[`"http.client.requests"`]
|
||||
|xref:integration/observability.adoc#observability.http-client[`"http.client.requests"`]
|
||||
|Time spent for HTTP client exchanges
|
||||
|
||||
|xref:integration/observability.adoc#http-server[`"http.server.requests"`]
|
||||
|xref:integration/observability.adoc#observability.http-server[`"http.server.requests"`]
|
||||
|Processing time for HTTP server exchanges at the Framework level
|
||||
|
||||
|xref:integration/observability.adoc#observability.jms.publish[`"jms.message.publish"`]
|
||||
|Time spent sending a JMS message to a destination by a message producer.
|
||||
|
||||
|xref:integration/observability.adoc#observability.jms.process[`"jms.message.process"`]
|
||||
|Processing time for a JMS message that was previously received by a message consumer.
|
||||
|
||||
|xref:integration/observability.adoc#observability.tasks-scheduled[`"tasks.scheduled.execution"`]
|
||||
|Processing time for an execution of a `@Scheduled` task
|
||||
|===
|
||||
|
||||
NOTE: Observations are using Micrometer's official naming convention, but Metrics names will be automatically converted
|
||||
@@ -79,6 +88,97 @@ include-code::./ServerRequestObservationFilter[]
|
||||
|
||||
You can configure `ObservationFilter` instances on the `ObservationRegistry`.
|
||||
|
||||
[[observability.tasks-scheduled]]
|
||||
== @Scheduled tasks instrumentation
|
||||
|
||||
An Observation is created for xref:integration/scheduling.adoc#scheduling-enable-annotation-support[each execution of an `@Scheduled` task].
|
||||
Applications need to configure the `ObservationRegistry` on the `ScheduledTaskRegistrar` to enable the recording of observations.
|
||||
This can be done by declaring a `SchedulingConfigurer` bean that sets the observation registry:
|
||||
|
||||
include-code::./ObservationSchedulingConfigurer[]
|
||||
|
||||
It is using the `org.springframework.scheduling.support.DefaultScheduledTaskObservationConvention` by default, backed by the `ScheduledTaskObservationContext`.
|
||||
You can configure a custom implementation on the `ObservationRegistry` directly.
|
||||
During the execution of the scheduled method, the current observation is restored in the `ThreadLocal` context or the Reactor context (if the scheduled method returns a `Mono` or `Flux` type).
|
||||
|
||||
By default, the following `KeyValues` are created:
|
||||
|
||||
.Low cardinality Keys
|
||||
[cols="a,a"]
|
||||
|===
|
||||
|Name | Description
|
||||
|`code.function` _(required)_|Name of Java `Method` that is scheduled for execution.
|
||||
|`code.namespace` _(required)_|Canonical name of the class of the bean instance that holds the scheduled method.
|
||||
|`exception` _(required)_|Name of the exception thrown during the execution, or `"none"` if no exception happened.
|
||||
|`outcome` _(required)_|Outcome of the method execution. Can be `"SUCCESS"`, `"ERROR"` or `"UNKNOWN"` (if for example the operation was cancelled during execution).
|
||||
|===
|
||||
|
||||
|
||||
[[observability.jms]]
|
||||
== JMS messaging instrumentation
|
||||
|
||||
Spring Framework uses the Jakarta JMS instrumentation provided by Micrometer if the `io.micrometer:micrometer-core` dependency is on the classpath.
|
||||
The `io.micrometer.core.instrument.binder.jms.JmsInstrumentation` instruments `jakarta.jms.Session` and records the relevant observations.
|
||||
|
||||
This instrumentation will create 2 types of observations:
|
||||
|
||||
* `"jms.message.publish"` when a JMS message is sent to the broker, typically with `JmsTemplate`.
|
||||
* `"jms.message.process"` when a JMS message is processed by the application, typically with a `MessageListener` or a `@JmsListener` annotated method.
|
||||
|
||||
NOTE: currently there is no instrumentation for `"jms.message.receive"` observations as there is little value in measuring the time spent waiting for the reception of a message.
|
||||
Such an integration would typically instrument `MessageConsumer#receive` method calls. But once those return, the processing time is not measured and the trace scope cannot be propagated to the application.
|
||||
|
||||
By default, both observations share the same set of possible `KeyValues`:
|
||||
|
||||
.Low cardinality Keys
|
||||
[cols="a,a"]
|
||||
|===
|
||||
|Name | Description
|
||||
|`exception` |Class name of the exception thrown during the messaging operation (or "none").
|
||||
|`messaging.destination.temporary` _(required)_|Whether the destination is a `TemporaryQueue` or `TemporaryTopic` (values: `"true"` or `"false"`).
|
||||
|`messaging.operation` _(required)_|Name of JMS operation being performed (values: `"publish"` or `"process"`).
|
||||
|===
|
||||
|
||||
.High cardinality Keys
|
||||
[cols="a,a"]
|
||||
|===
|
||||
|Name | Description
|
||||
|`messaging.message.conversation_id` |The correlation ID of the JMS message.
|
||||
|`messaging.destination.name` |The name of destination the current message was sent to.
|
||||
|`messaging.message.id` |Value used by the messaging system as an identifier for the message.
|
||||
|===
|
||||
|
||||
[[observability.jms.publish]]
|
||||
=== JMS message Publication instrumentation
|
||||
|
||||
`"jms.message.publish"` observations are recorded when a JMS message is sent to the broker.
|
||||
They measure the time spent sending the message and propagate the tracing information with outgoing JMS message headers.
|
||||
|
||||
You will need to configure the `ObservationRegistry` on the `JmsTemplate` to enable observations:
|
||||
|
||||
include-code::./JmsTemplatePublish[]
|
||||
|
||||
It uses the `io.micrometer.core.instrument.binder.jms.DefaultJmsPublishObservationConvention` by default, backed by the `io.micrometer.core.instrument.binder.jms.JmsPublishObservationContext`.
|
||||
|
||||
[[observability.jms.process]]
|
||||
=== JMS message Processing instrumentation
|
||||
|
||||
`"jms.message.process"` observations are recorded when a JMS message is processed by the application.
|
||||
They measure the time spent processing the message and propagate the tracing context with incoming JMS message headers.
|
||||
|
||||
Most applications will use the xref:integration/jms/annotated.adoc#jms-annotated[`@JmsListener` annotated methods] mechanism to process incoming messages.
|
||||
You will need to ensure that the `ObservationRegistry` is configured on the dedicated `JmsListenerContainerFactory`:
|
||||
|
||||
include-code::./JmsConfiguration[]
|
||||
|
||||
A xref:integration/jms/annotated.adoc#jms-annotated-support[default container factory is required to enable the annotation support],
|
||||
but note that `@JmsListener` annotations can refer to specific container factory beans for specific purposes.
|
||||
In all cases, Observations are only recorded if the observation registry is configured on the container factory.
|
||||
|
||||
Similar observations are recorded with `JmsTemplate` when messages are processed by a `MessageListener`.
|
||||
Such listeners are set on a `MessageConsumer` within a session callback (see `JmsTemplate.execute(SessionCallback<T>)`).
|
||||
|
||||
This observation uses the `io.micrometer.core.instrument.binder.jms.DefaultJmsProcessObservationConvention` by default, backed by the `io.micrometer.core.instrument.binder.jms.JmsProcessObservationContext`.
|
||||
|
||||
[[observability.http-server]]
|
||||
== HTTP Server instrumentation
|
||||
@@ -107,7 +207,7 @@ By default, the following `KeyValues` are created:
|
||||
[cols="a,a"]
|
||||
|===
|
||||
|Name | Description
|
||||
|`exception` _(required)_|Name of the exception thrown during the exchange, or `KeyValue#NONE_VALUE`} if no exception happened.
|
||||
|`exception` _(required)_|Name of the exception thrown during the exchange, or `"none"` if no exception happened.
|
||||
|`method` _(required)_|Name of HTTP request method or `"none"` if the request was not received properly.
|
||||
|`outcome` _(required)_|Outcome of the HTTP server exchange.
|
||||
|`status` _(required)_|HTTP response raw status code, or `"UNKNOWN"` if no response was created.
|
||||
@@ -125,11 +225,15 @@ By default, the following `KeyValues` are created:
|
||||
[[observability.http-server.reactive]]
|
||||
=== Reactive applications
|
||||
|
||||
Applications need to configure the `org.springframework.web.filter.reactive.ServerHttpObservationFilter` reactive `WebFilter` in their application.
|
||||
It uses the `org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention` by default, backed by the `ServerRequestObservationContext`.
|
||||
Applications need to configure the `WebHttpHandlerBuilder` with a `MeterRegistry` to enable server instrumentation.
|
||||
This can be done on the `WebHttpHandlerBuilder`, as follows:
|
||||
|
||||
This will only record an observation as an error if the `Exception` has not been handled by the web framework and has bubbled up to the `WebFilter`.
|
||||
Typically, all exceptions handled by Spring WebFlux's `@ExceptionHandler` and xref:web/webflux/ann-rest-exceptions.adoc[`ProblemDetail` support] will not be recorded with the observation.
|
||||
include-code::./HttpHandlerConfiguration[]
|
||||
|
||||
It is using the `org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention` by default, backed by the `ServerRequestObservationContext`.
|
||||
|
||||
This will only record an observation as an error if the `Exception` has not been handled by an application Controller.
|
||||
Typically, all exceptions handled by Spring WebFlux's `@ExceptionHandler` and <<web.adoc#webflux-ann-rest-exceptions,`ProblemDetail` support>> will not be recorded with the observation.
|
||||
You can, at any point during request processing, set the error field on the `ObservationContext` yourself:
|
||||
|
||||
include-code::./UserController[]
|
||||
|
||||
@@ -3,11 +3,19 @@
|
||||
|
||||
The Spring Framework provides the following choices for making calls to REST endpoints:
|
||||
|
||||
* xref:integration/rest-clients.adoc#rest-restclient[`RestClient`] - synchronous client with a fluent API.
|
||||
* xref:integration/rest-clients.adoc#rest-webclient[`WebClient`] - non-blocking, reactive client with fluent API.
|
||||
* xref:integration/rest-clients.adoc#rest-resttemplate[`RestTemplate`] - synchronous client with template method API.
|
||||
* xref:integration/rest-clients.adoc#rest-http-interface[HTTP Interface] - annotated interface with generated, dynamic proxy implementation.
|
||||
|
||||
|
||||
[[rest-restclient]]
|
||||
== `RestClient`
|
||||
|
||||
Reference documentation is forthcoming.
|
||||
For now, please refer to the https://docs.spring.io/spring-framework/docs/6.1.0-M2/javadoc-api/org/springframework/web/client/RestClient.html[API documentation].
|
||||
|
||||
|
||||
[[rest-webclient]]
|
||||
== `WebClient`
|
||||
|
||||
@@ -36,9 +44,8 @@ The `RestTemplate` provides a higher level API over HTTP client libraries. It ma
|
||||
easy to invoke REST endpoints in a single line. It exposes the following groups of
|
||||
overloaded methods:
|
||||
|
||||
NOTE: `RestTemplate` is in maintenance mode, with only requests for minor
|
||||
changes and bugs to be accepted. Please, consider using the
|
||||
xref:web/webflux-webclient.adoc[WebClient] instead.
|
||||
NOTE: The xref:integration/rest-clients.adoc#rest-restclient[`RestClient`] offers a more modern API for synchronous HTTP access.
|
||||
For asynchronous and streaming scenarios, consider the reactive xref:web/webflux-webclient.adoc[WebClient].
|
||||
|
||||
[[rest-overview-of-resttemplate-methods-tbl]]
|
||||
.RestTemplate methods
|
||||
@@ -356,12 +363,13 @@ If necessary the `Content-Type` may also be set explicitly.
|
||||
[[rest-http-interface]]
|
||||
== HTTP Interface
|
||||
|
||||
The Spring Framework lets you define an HTTP service as a Java interface with annotated
|
||||
methods for HTTP exchanges. You can then generate a proxy that implements this interface
|
||||
and performs the exchanges. This helps to simplify HTTP remote access which often
|
||||
involves a facade that wraps the details of using the underlying HTTP client.
|
||||
The Spring Framework lets you define an HTTP service as a Java interface with
|
||||
`@HttpExchange` methods. You can pass such an interface to `HttpServiceProxyFactory`
|
||||
to create a proxy which performs requests through an HTTP client such as `RestClient`
|
||||
or `WebClient`. You can also implement the interface from an `@Controller` for server
|
||||
request handling.
|
||||
|
||||
One, declare an interface with `@HttpExchange` methods:
|
||||
Start by creating the interface with `@HttpExchange` methods:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@@ -375,12 +383,38 @@ One, declare an interface with `@HttpExchange` methods:
|
||||
}
|
||||
----
|
||||
|
||||
Two, create a proxy that will perform the declared HTTP exchanges:
|
||||
Now you can create a proxy that performs requests when methods are called.
|
||||
|
||||
For `RestClient`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
RestClient restClient = RestClient.builder().baseUrl("https://api.github.com/").build();
|
||||
RestClientAdapter adapter = RestClientAdapter.create(restClient);
|
||||
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
|
||||
|
||||
RepositoryService service = factory.createClient(RepositoryService.class);
|
||||
----
|
||||
|
||||
For `WebClient`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
WebClient client = WebClient.builder().baseUrl("https://api.github.com/").build();
|
||||
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder(WebClientAdapter.forClient(client)).build();
|
||||
WebClientAdapter adapter = WebClientAdapter.forClient(webClient)
|
||||
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
|
||||
|
||||
RepositoryService service = factory.createClient(RepositoryService.class);
|
||||
----
|
||||
|
||||
For `RestTemplate`:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory("https://api.github.com/"));
|
||||
RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate);
|
||||
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
|
||||
|
||||
RepositoryService service = factory.createClient(RepositoryService.class);
|
||||
----
|
||||
@@ -448,6 +482,10 @@ method parameters:
|
||||
Object (entity to be encoded, e.g. as JSON), `HttpEntity` (part content and headers),
|
||||
a Spring `Part`, or Reactive Streams `Publisher` of any of the above.
|
||||
|
||||
| `MultipartFile`
|
||||
| Add a request part from a `MultipartFile`, typically used in a Spring MVC controller
|
||||
where it represents an uploaded file.
|
||||
|
||||
| `@CookieValue`
|
||||
| Add a cookie or multiple cookies. The argument may be a `Map<String, ?>` or
|
||||
`MultiValueMap<String, ?>` with multiple cookies, a `Collection<?>` of values, or an
|
||||
@@ -459,43 +497,74 @@ method parameters:
|
||||
[[rest-http-interface-return-values]]
|
||||
=== Return Values
|
||||
|
||||
Annotated, HTTP exchange methods support the following return values:
|
||||
The supported return values depend on the underlying client.
|
||||
|
||||
Clients adapted to `HttpExchangeAdapter` such as `RestClient` and `RestTemplate`
|
||||
support synchronous return values:
|
||||
|
||||
[cols="1,2", options="header"]
|
||||
|===
|
||||
| Method return value | Description
|
||||
|
||||
| `void`, `Mono<Void>`
|
||||
| Perform the given request, and release the response content, if any.
|
||||
| `void`
|
||||
| Perform the given request.
|
||||
|
||||
| `HttpHeaders`, `Mono<HttpHeaders>`
|
||||
| Perform the given request, release the response content, if any, and return the
|
||||
response headers.
|
||||
| `HttpHeaders`
|
||||
| Perform the given request and return the response headers.
|
||||
|
||||
| `<T>`, `Mono<T>`
|
||||
| `<T>`
|
||||
| Perform the given request and decode the response content to the declared return type.
|
||||
|
||||
| `<T>`, `Flux<T>`
|
||||
| Perform the given request and decode the response content to a stream of the declared
|
||||
element type.
|
||||
| `ResponseEntity<Void>`
|
||||
| Perform the given request and return a `ResponseEntity` with the status and headers.
|
||||
|
||||
| `ResponseEntity<Void>`, `Mono<ResponseEntity<Void>>`
|
||||
| Perform the given request, and release the response content, if any, and return a
|
||||
`ResponseEntity` with the status and headers.
|
||||
|
||||
| `ResponseEntity<T>`, `Mono<ResponseEntity<T>>`
|
||||
| `ResponseEntity<T>`
|
||||
| Perform the given request, decode the response content to the declared return type, and
|
||||
return a `ResponseEntity` with the status, headers, and the decoded body.
|
||||
|
||||
|===
|
||||
|
||||
Clients adapted to `ReactorHttpExchangeAdapter` such as `WebClient`, support all of above
|
||||
as well as reactive variants. The table below shows Reactor types, but you can also use
|
||||
other reactive types that are supported through the `ReactiveAdapterRegistry`:
|
||||
|
||||
[cols="1,2", options="header"]
|
||||
|===
|
||||
| Method return value | Description
|
||||
|
||||
| `Mono<Void>`
|
||||
| Perform the given request, and release the response content, if any.
|
||||
|
||||
| `Mono<HttpHeaders>`
|
||||
| Perform the given request, release the response content, if any, and return the
|
||||
response headers.
|
||||
|
||||
| `Mono<T>`
|
||||
| Perform the given request and decode the response content to the declared return type.
|
||||
|
||||
| `Flux<T>`
|
||||
| Perform the given request and decode the response content to a stream of the declared
|
||||
element type.
|
||||
|
||||
| `Mono<ResponseEntity<Void>>`
|
||||
| Perform the given request, and release the response content, if any, and return a
|
||||
`ResponseEntity` with the status and headers.
|
||||
|
||||
| `Mono<ResponseEntity<T>>`
|
||||
| Perform the given request, decode the response content to the declared return type, and
|
||||
return a `ResponseEntity` with the status, headers, and the decoded body.
|
||||
|
||||
| `Mono<ResponseEntity<Flux<T>>`
|
||||
| Perform the given request, decode the response content to a stream of the declared
|
||||
element type, and return a `ResponseEntity` with the status, headers, and the decoded
|
||||
response body stream.
|
||||
element type, and return a `ResponseEntity` with the status, headers, and the decoded
|
||||
response body stream.
|
||||
|
||||
|===
|
||||
|
||||
TIP: You can also use any other async or reactive types registered in the
|
||||
`ReactiveAdapterRegistry`.
|
||||
By default, the timeout for synchronous return values with `ReactorHttpExchangeAdapter`
|
||||
depends on how the underlying HTTP client is configured. You can set a `blockTimeout`
|
||||
value on the adapter level as well, but we recommend relying on timeout settings of the
|
||||
underlying HTTP client, which operates at a lower level and provides more control.
|
||||
|
||||
|
||||
[[rest-http-interface-exceptions]]
|
||||
|
||||
@@ -71,6 +71,11 @@ The variants that Spring provides are as follows:
|
||||
compatible runtime environment (such as a Jakarta EE application server),
|
||||
replacing a CommonJ WorkManager for that purpose.
|
||||
|
||||
As of 6.1, `ThreadPoolTaskExecutor` provides a pause/resume capability and graceful
|
||||
shutdown through Spring's lifecycle management. There is also a new "virtualThreads"
|
||||
option on `SimpleAsyncTaskExecutor` which is aligned with JDK 21's Virtual Threads,
|
||||
as well as a graceful shutdown capability for `SimpleAsyncTaskExecutor` as well.
|
||||
|
||||
|
||||
[[scheduling-task-executor-usage]]
|
||||
=== Using a `TaskExecutor`
|
||||
@@ -249,6 +254,11 @@ to provide common bean-style configuration along the lines of `ThreadPoolTaskExe
|
||||
These variants work perfectly fine for locally embedded thread pool setups in lenient
|
||||
application server environments, as well -- in particular on Tomcat and Jetty.
|
||||
|
||||
As of 6.1, `ThreadPoolTaskScheduler` provides a pause/resume capability and graceful
|
||||
shutdown through Spring's lifecycle management. There is also a new option called
|
||||
`SimpleAsyncTaskScheduler` which is aligned with JDK 21's Virtual Threads, using a
|
||||
single scheduler thread but firing up a new thread for every scheduled task execution.
|
||||
|
||||
|
||||
|
||||
[[scheduling-annotation-support]]
|
||||
@@ -397,6 +407,120 @@ container and once through the `@Configurable` aspect), with the consequence of
|
||||
`@Scheduled` method being invoked twice.
|
||||
====
|
||||
|
||||
[[scheduling-annotation-support-scheduled-reactive]]
|
||||
=== The `@Scheduled` annotation on Reactive methods or Kotlin suspending functions
|
||||
|
||||
As of Spring Framework 6.1, `@Scheduled` methods are also supported on several types
|
||||
of reactive methods:
|
||||
|
||||
- methods with a `Publisher` return type (or any concrete implementation of `Publisher`)
|
||||
like in the following example:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Scheduled(fixedDelay = 500)
|
||||
public Publisher<Void> reactiveSomething() {
|
||||
// return an instance of Publisher
|
||||
}
|
||||
----
|
||||
|
||||
- methods with a return type that can be adapted to `Publisher` via the shared instance
|
||||
of the `ReactiveAdapterRegistry`, provided the type supports _deferred subscription_ like
|
||||
in the following example:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Scheduled(fixedDelay = 500)
|
||||
public Single<String> rxjavaNonPublisher() {
|
||||
return Single.just("example");
|
||||
}
|
||||
----
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
The `CompletableFuture` class is an example of a type that can typically be adapted
|
||||
to `Publisher` but doesn't support deferred subscription. Its `ReactiveAdapter` in the
|
||||
registry denotes that by having the `getDescriptor().isDeferred()` method return `false`.
|
||||
====
|
||||
|
||||
- Kotlin suspending functions, like in the following example:
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Scheduled(fixedDelay = 500)
|
||||
suspend fun something() {
|
||||
// do something asynchronous
|
||||
}
|
||||
----
|
||||
|
||||
- methods that return a Kotlin `Flow` or `Deferred` instance, like in the following example:
|
||||
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Scheduled(fixedDelay = 500)
|
||||
fun something(): Flow<Void> {
|
||||
flow {
|
||||
// do something asynchronous
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
All these types of methods must be declared without any arguments. In the case of Kotlin
|
||||
suspending functions, the `kotlinx.coroutines.reactor` bridge must also be present to allow
|
||||
the framework to invoke a suspending function as a `Publisher`.
|
||||
|
||||
The Spring Framework will obtain a `Publisher` for the annotated method once and will
|
||||
schedule a `Runnable` in which it subscribes to said `Publisher`. These inner regular
|
||||
subscriptions occur according to the corresponding `cron`/fixedDelay`/`fixedRate` configuration.
|
||||
|
||||
If the `Publisher` emits `onNext` signal(s), these are ignored and discarded (the same way
|
||||
return values from synchronous `@Scheduled` methods are ignored).
|
||||
|
||||
In the following example, the `Flux` emits `onNext("Hello"), onNext("World")` every 5
|
||||
seconds, but these values are unused:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Scheduled(initialDelay = 5000, fixedRate = 5000)
|
||||
public Flux<String> reactiveSomething() {
|
||||
return Flux.just("Hello", "World");
|
||||
}
|
||||
----
|
||||
|
||||
If the `Publisher` emits an `onError` signal, it is logged at `WARN` level and recovered.
|
||||
Because of the asynchronous and lazy nature of `Publisher` instances, exceptions are
|
||||
not thrown from the `Runnable` task: this means that the `ErrorHandler` contract is not
|
||||
involved for reactive methods.
|
||||
|
||||
As a result, further scheduled subscription occurs despite the error.
|
||||
|
||||
In the following example, the `Mono` subscription fails twice in the first five seconds.
|
||||
Then subscriptions start succeeding, printing a message to the standard output every five
|
||||
seconds:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Scheduled(initialDelay = 0, fixedRate = 5000)
|
||||
public Mono<Void> reactiveSomething() {
|
||||
AtomicInteger countdown = new AtomicInteger(2);
|
||||
|
||||
return Mono.defer(() -> {
|
||||
if (countDown.get() == 0 || countDown.decrementAndGet() == 0) {
|
||||
return Mono.fromRunnable(() -> System.out.println("Message"));
|
||||
}
|
||||
return Mono.error(new IllegalStateException("Cannot deliver message"));
|
||||
})
|
||||
}
|
||||
----
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
When destroying the annotated bean or closing the application context, Spring Framework cancels
|
||||
scheduled tasks, which includes the next scheduled subscription to the `Publisher` as well
|
||||
as any past subscription that is still currently active (e.g. for long-running publishers
|
||||
or even infinite publishers).
|
||||
====
|
||||
|
||||
|
||||
[[scheduling-annotation-support-async]]
|
||||
=== The `@Async` annotation
|
||||
|
||||
@@ -137,10 +137,12 @@ demonstrate its API and protocol features.
|
||||
|
||||
The `spring-messaging` module contains the following:
|
||||
|
||||
* xref:rsocket.adoc#rsocket-requester[RSocketRequester] -- fluent API to make requests through an `io.rsocket.RSocket`
|
||||
with data and metadata encoding/decoding.
|
||||
* xref:rsocket.adoc#rsocket-annot-responders[Annotated Responders] -- `@MessageMapping` annotated handler methods for
|
||||
responding.
|
||||
* xref:rsocket.adoc#rsocket-requester[RSocketRequester] -- fluent API to make requests
|
||||
through an `io.rsocket.RSocket` with data and metadata encoding/decoding.
|
||||
* xref:rsocket.adoc#rsocket-annot-responders[Annotated Responders] -- `@MessageMapping`
|
||||
and `@RSocketExchange` annotated handler methods for responding.
|
||||
* xref:rsocket.adoc#rsocket-interface[RSocket Interface] -- RSocket service declaration
|
||||
as Java interface with `@RSocketExchange` methods, for use as requester or responder.
|
||||
|
||||
The `spring-web` module contains `Encoder` and `Decoder` implementations such as Jackson
|
||||
CBOR/JSON, and Protobuf that RSocket applications will likely need. It also contains the
|
||||
@@ -863,6 +865,69 @@ interaction type(s):
|
||||
|
||||
|
||||
|
||||
[[rsocket-annot-rsocketexchange]]
|
||||
=== @RSocketExchange
|
||||
|
||||
As an alternative to `@MessageMapping`, you can also handle requests with
|
||||
`@RSocketExchange` methods. Such methods are declared on an
|
||||
xref:rsocket-interface[RSocket Interface] and can be used as a requester via
|
||||
`RSocketServiceProxyFactory` or implemented by a responder.
|
||||
|
||||
For example, to handle requests as a responder:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
public interface RadarsService {
|
||||
|
||||
@RSocketExchange("locate.radars.within")
|
||||
Flux<AirportLocation> radars(MapRequest request);
|
||||
}
|
||||
|
||||
@Controller
|
||||
public class RadarsController implements RadarsService {
|
||||
|
||||
public Flux<AirportLocation> radars(MapRequest request) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
interface RadarsService {
|
||||
|
||||
@RSocketExchange("locate.radars.within")
|
||||
fun radars(request: MapRequest): Flow<AirportLocation>
|
||||
}
|
||||
|
||||
@Controller
|
||||
class RadarsController : RadarsService {
|
||||
|
||||
override fun radars(request: MapRequest): Flow<AirportLocation> {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
There some differences between `@RSocketExhange` and `@MessageMapping` since the
|
||||
former needs to remain suitable for requester and responder use. For example, while
|
||||
`@MessageMapping` can be declared to handle any number of routes and each route can
|
||||
be a pattern, `@RSocketExchange` must be declared with a single, concrete route. There are
|
||||
also small differences in the supported method parameters related to metadata, see
|
||||
xref:rsocket-annot-messagemapping[@MessageMapping] and
|
||||
xref:rsocket-interface[RSocket Interface] for a list of supported parameters.
|
||||
|
||||
`@RSocketExchange` can be used at the type level to specify a common prefix for all routes
|
||||
for a given RSocket service interface.
|
||||
|
||||
|
||||
[[rsocket-annot-connectmapping]]
|
||||
=== @ConnectMapping
|
||||
|
||||
@@ -997,12 +1062,13 @@ Kotlin::
|
||||
[[rsocket-interface]]
|
||||
== RSocket Interface
|
||||
|
||||
The Spring Framework lets you define an RSocket service as a Java interface with annotated
|
||||
methods for RSocket exchanges. You can then generate a proxy that implements this interface
|
||||
and performs the exchanges. This helps to simplify RSocket remote access by wrapping the
|
||||
use of the underlying xref:rsocket.adoc#rsocket-requester[RSocketRequester].
|
||||
The Spring Framework lets you define an RSocket service as a Java interface with
|
||||
`@RSocketExchange` methods. You can pass such an interface to `RSocketServiceProxyFactory`
|
||||
to create a proxy which performs requests through an
|
||||
xref:rsocket.adoc#rsocket-requester[RSocketRequester]. You can also implement the
|
||||
interface as a responder that handles requests.
|
||||
|
||||
One, declare an interface with `@RSocketExchange` methods:
|
||||
Start by creating the interface with `@RSocketExchange` methods:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@@ -1016,7 +1082,7 @@ One, declare an interface with `@RSocketExchange` methods:
|
||||
}
|
||||
----
|
||||
|
||||
Two, create a proxy that will perform the declared RSocket exchanges:
|
||||
Now you can create a proxy that performs requests when methods are called:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@@ -1026,6 +1092,10 @@ Two, create a proxy that will perform the declared RSocket exchanges:
|
||||
RepositoryService service = factory.createClient(RadarService.class);
|
||||
----
|
||||
|
||||
You can also implement the interface to handle requests as a responder.
|
||||
See xref:rsocket.adoc#rsocket-annot-rsocketexchange[Annotated Responders].
|
||||
|
||||
|
||||
|
||||
[[rsocket-interface-method-parameters]]
|
||||
=== Method Parameters
|
||||
@@ -1066,3 +1136,10 @@ method parameters:
|
||||
Annotated, RSocket exchange methods support return values that are concrete value(s), or
|
||||
any producer of value(s) that can be adapted to a Reactive Streams `Publisher` via
|
||||
`ReactiveAdapterRegistry`.
|
||||
|
||||
By default, the behavior of RSocket service methods with synchronous (blocking) method
|
||||
signature depends on response timeout settings of the underlying RSocket `ClientTransport`
|
||||
as well as RSocket keep-alive settings. `RSocketServiceProxyFactory.Builder` does expose a
|
||||
`blockTimeout` option that also lets you configure the maximum time to block for a response,
|
||||
but we recommend configuring timeout values at the RSocket level for more control.
|
||||
|
||||
|
||||
@@ -19,7 +19,22 @@ following features.
|
||||
use an AOT-optimized `ApplicationContext` that participates transparently with the
|
||||
xref:testing/testcontext-framework/ctx-management/caching.adoc[context cache].
|
||||
|
||||
[WARNING]
|
||||
[TIP]
|
||||
====
|
||||
By default, if an error is encountered during build-time AOT processing, an exception
|
||||
will be thrown, and the overall process will fail immediately.
|
||||
|
||||
If you would prefer that build-time AOT processing continue after errors are encountered,
|
||||
you can disable the `failOnError` mode which results in errors being logged at `WARN`
|
||||
level or with greater detail at `DEBUG` level.
|
||||
|
||||
The `failOnError` mode can be disabled from the command line or a build script by setting
|
||||
a JVM system property named `spring.test.aot.processing.failOnError` to `false`. As an
|
||||
alternative, you can set the same property via the
|
||||
xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism.
|
||||
====
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
The `@ContextHierarchy` annotation is currently not supported in AOT mode.
|
||||
====
|
||||
|
||||
@@ -119,5 +119,6 @@ advanced use cases.
|
||||
* xref:testing/testcontext-framework/ctx-management/dynamic-property-sources.adoc[Context Configuration with Dynamic Property Sources]
|
||||
* xref:testing/testcontext-framework/ctx-management/web.adoc[Loading a `WebApplicationContext`]
|
||||
* xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching]
|
||||
* xref:testing/testcontext-framework/ctx-management/failure-threshold.adoc[Context Failure Threshold]
|
||||
* xref:testing/testcontext-framework/ctx-management/hierarchies.adoc[Context Hierarchies]
|
||||
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ framework uses the following configuration parameters to build the context cache
|
||||
* `contextLoader` (from `@ContextConfiguration`)
|
||||
* `parent` (from `@ContextHierarchy`)
|
||||
* `activeProfiles` (from `@ActiveProfiles`)
|
||||
* `propertySourceLocations` (from `@TestPropertySource`)
|
||||
* `propertySourceDescriptors` (from `@TestPropertySource`)
|
||||
* `propertySourceProperties` (from `@TestPropertySource`)
|
||||
* `resourceBasePath` (from `@WebAppConfiguration`)
|
||||
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
[[testcontext-ctx-management-failure-threshold]]
|
||||
= Context Failure Threshold
|
||||
|
||||
As of Spring Framework 6.1, a context _failure threshold_ policy is in place which helps
|
||||
avoid repeated attempts to load a failing `ApplicationContext`. By default, the failure
|
||||
threshold is set to `1` which means that only one attempt will be made to load an
|
||||
`ApplicationContext` for a given context cache key (see
|
||||
xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching]). Any
|
||||
subsequent attempt to load the `ApplicationContext` for the same context cache key will
|
||||
result in an immediate `IllegalStateException` with an error message which explains that
|
||||
the attempt was preemptively skipped. This behavior allows individual test classes and
|
||||
test suites to fail faster by avoiding repeated attempts to load an `ApplicationContext`
|
||||
that will never successfully load -- for example, due to a configuration error or a missing
|
||||
external resource that prevents the context from loading in the current environment.
|
||||
|
||||
You can configure the context failure threshold from the command line or a build script
|
||||
by setting a JVM system property named `spring.test.context.failure.threshold` with a
|
||||
positive integer value. As an alternative, you can set the same property via the
|
||||
xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism.
|
||||
|
||||
NOTE: If you wish to effectively disable the context failure threshold, you can set the
|
||||
property to a very large value. For example, from the command line you could set the
|
||||
system property via `-Dspring.test.context.failure.threshold=1000000`.
|
||||
+69
-12
@@ -16,7 +16,7 @@ SPI, but `@TestPropertySource` is not supported with implementations of the olde
|
||||
`ContextLoader` SPI.
|
||||
|
||||
Implementations of `SmartContextLoader` gain access to merged test property source values
|
||||
through the `getPropertySourceLocations()` and `getPropertySourceProperties()` methods in
|
||||
through the `getPropertySourceDescriptors()` and `getPropertySourceProperties()` methods in
|
||||
`MergedContextConfiguration`.
|
||||
====
|
||||
|
||||
@@ -26,17 +26,23 @@ through the `getPropertySourceLocations()` and `getPropertySourceProperties()` m
|
||||
You can configure test properties files by using the `locations` or `value` attribute of
|
||||
`@TestPropertySource`.
|
||||
|
||||
Both traditional and XML-based properties file formats are supported -- for example,
|
||||
`"classpath:/com/example/test.properties"` or `"file:///path/to/file.xml"`.
|
||||
By default, both traditional and XML-based `java.util.Properties` file formats are
|
||||
supported -- for example, `"classpath:/com/example/test.properties"` or
|
||||
`"file:///path/to/file.xml"`. As of Spring Framework 6.1, you can configure a custom
|
||||
`PropertySourceFactory` via the `factory` attribute in `@TestPropertySource` in order to
|
||||
support a different file format such as JSON, YAML, etc.
|
||||
|
||||
Each path is interpreted as a Spring `Resource`. A plain path (for example,
|
||||
`"test.properties"`) is treated as a classpath resource that is relative to the package
|
||||
in which the test class is defined. A path starting with a slash is treated as an
|
||||
absolute classpath resource (for example: `"/org/example/test.xml"`). A path that
|
||||
references a URL (for example, a path prefixed with `classpath:`, `file:`, or `http:`) is
|
||||
loaded by using the specified resource protocol. Resource location wildcards (such as
|
||||
`{asterisk}{asterisk}/{asterisk}.properties`) are not permitted: Each location must
|
||||
evaluate to exactly one `.properties` or `.xml` resource.
|
||||
loaded by using the specified resource protocol.
|
||||
|
||||
Property placeholders in paths (such as `${...}`) will be resolved against the `Environment`.
|
||||
|
||||
As of Spring Framework 6.1, resource location patterns are also supported — for
|
||||
example, `"classpath*:/config/*.properties"`.
|
||||
|
||||
The following example uses a test properties file:
|
||||
|
||||
@@ -80,6 +86,20 @@ a Java properties file:
|
||||
* `key:value`
|
||||
* `key value`
|
||||
|
||||
[TIP]
|
||||
====
|
||||
Although properties can be defined using any of the above syntax variants and any number
|
||||
of spaces between the key and the value, it is recommended that you use one syntax
|
||||
variant and consistent spacing within your test suite — for example, consider always
|
||||
using `key = value` instead of `key= value`, `key=value`, etc. Similarly, if you define
|
||||
inlined properties using text blocks you should consistently use text blocks for inlined
|
||||
properties throughout your test suite.
|
||||
|
||||
The reason is that the exact strings you provide will be used to determine the key for
|
||||
the context cache. Consequently, to benefit from the context cache you must ensure that
|
||||
you define inlined properties consistently.
|
||||
====
|
||||
|
||||
The following example sets two inlined properties:
|
||||
|
||||
[tabs]
|
||||
@@ -89,24 +109,61 @@ Java::
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@ContextConfiguration
|
||||
@TestPropertySource(properties = {"timezone = GMT", "port: 4242"}) // <1>
|
||||
@TestPropertySource(properties = {"timezone = GMT", "port = 4242"}) // <1>
|
||||
class MyIntegrationTests {
|
||||
// class body...
|
||||
}
|
||||
----
|
||||
<1> Setting two properties by using two variations of the key-value syntax.
|
||||
<1> Setting two properties via an array of strings.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@ContextConfiguration
|
||||
@TestPropertySource(properties = ["timezone = GMT", "port: 4242"]) // <1>
|
||||
@TestPropertySource(properties = ["timezone = GMT", "port = 4242"]) // <1>
|
||||
class MyIntegrationTests {
|
||||
// class body...
|
||||
}
|
||||
----
|
||||
<1> Setting two properties by using two variations of the key-value syntax.
|
||||
<1> Setting two properties via an array of strings.
|
||||
======
|
||||
|
||||
As of Spring Framework 6.1, you can use _text blocks_ to define multiple inlined
|
||||
properties in a single `String`. The following example sets two inlined properties using
|
||||
a text block:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@ContextConfiguration
|
||||
@TestPropertySource(properties = """
|
||||
timezone = GMT
|
||||
port = 4242
|
||||
""") // <1>
|
||||
class MyIntegrationTests {
|
||||
// class body...
|
||||
}
|
||||
----
|
||||
<1> Setting two properties via a text block.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@ContextConfiguration
|
||||
@TestPropertySource(properties = ["""
|
||||
timezone = GMT
|
||||
port = 4242
|
||||
"""]) // <1>
|
||||
class MyIntegrationTests {
|
||||
// class body...
|
||||
}
|
||||
----
|
||||
<1> Setting two properties via a text block.
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
@@ -166,7 +223,7 @@ Java::
|
||||
@ContextConfiguration
|
||||
@TestPropertySource(
|
||||
locations = "/test.properties",
|
||||
properties = {"timezone = GMT", "port: 4242"}
|
||||
properties = {"timezone = GMT", "port = 4242"}
|
||||
)
|
||||
class MyIntegrationTests {
|
||||
// class body...
|
||||
@@ -179,7 +236,7 @@ Kotlin::
|
||||
----
|
||||
@ContextConfiguration
|
||||
@TestPropertySource("/test.properties",
|
||||
properties = ["timezone = GMT", "port: 4242"]
|
||||
properties = ["timezone = GMT", "port = 4242"]
|
||||
)
|
||||
class MyIntegrationTests {
|
||||
// class body...
|
||||
|
||||
@@ -67,52 +67,44 @@ from an existing `ProblemDetail`. This could be done centrally, e.g. from an
|
||||
|
||||
|
||||
[[webflux-ann-rest-exceptions-i18n]]
|
||||
== Internationalization
|
||||
== Customization and i18n
|
||||
[.small]#xref:web/webmvc/mvc-ann-rest-exceptions.adoc#mvc-ann-rest-exceptions-i18n[See equivalent in the Servlet stack]#
|
||||
|
||||
It is a common requirement to internationalize error response details, and good practice
|
||||
to customize the problem details for Spring WebFlux exceptions. This is supported as follows:
|
||||
It is a common requirement to customize and internationalize error response details.
|
||||
It is also good practice to customize the problem details for Spring WebFlux exceptions
|
||||
to avoid revealing implementation details. This section describes the support for that.
|
||||
|
||||
- Each `ErrorResponse` exposes a message code and arguments to resolve the "detail" field
|
||||
through a xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource].
|
||||
The actual message code value is parameterized with placeholders, e.g.
|
||||
`+"HTTP method {0} not supported"+` to be expanded from the arguments.
|
||||
- Each `ErrorResponse` also exposes a message code to resolve the "title" field.
|
||||
- `ResponseEntityExceptionHandler` uses the message code and arguments to resolve the
|
||||
"detail" and the "title" fields.
|
||||
An `ErrorResponse` exposes message codes for "type", "title", and "detail", as well as
|
||||
message code arguments for the "detail" field. `ResponseEntityExceptionHandler` resolves
|
||||
these through a xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource]
|
||||
and updates the corresponding `ProblemDetail` fields accordingly.
|
||||
|
||||
By default, the message code for the "detail" field is "problemDetail." + the fully
|
||||
qualified exception class name. Some exceptions may expose additional message codes in
|
||||
which case a suffix is added to the default message code. The table below lists message
|
||||
arguments and codes for Spring WebFlux exceptions:
|
||||
The default strategy for message codes follows the pattern:
|
||||
|
||||
`problemDetail.[type|title|detail].[fully qualified exception class name]`
|
||||
|
||||
An `ErrorResponse` may expose more than one message code, typically adding a suffix
|
||||
to the default message code. The table below lists message codes, and arguments for
|
||||
Spring WebFlux exceptions:
|
||||
|
||||
[[webflux-ann-rest-exceptions-codes]]
|
||||
[cols="1,1,2", options="header"]
|
||||
|===
|
||||
| Exception | Message Code | Message Code Arguments
|
||||
|
||||
| `UnsupportedMediaTypeStatusException`
|
||||
| `HandlerMethodValidationException`
|
||||
| (default)
|
||||
| `+{0}+` the media type that is not supported, `+{1}+` list of supported media types
|
||||
| `+{0}+` list all validation errors.
|
||||
Message codes and arguments for each error are also resolved via `MessageSource`.
|
||||
|
||||
| `UnsupportedMediaTypeStatusException`
|
||||
| (default) + ".parseError"
|
||||
|
|
||||
| `MethodNotAllowedException`
|
||||
| (default)
|
||||
| `+{0}+` the current HTTP method, `+{1}+` the list of supported HTTP methods
|
||||
|
||||
| `MissingRequestValueException`
|
||||
| (default)
|
||||
| `+{0}+` a label for the value (e.g. "request header", "cookie value", ...), `+{1}+` the value name
|
||||
|
||||
| `UnsatisfiedRequestParameterException`
|
||||
| (default)
|
||||
| `+{0}+` the list of parameter conditions
|
||||
|
||||
| `WebExchangeBindException`
|
||||
| (default)
|
||||
| `+{0}+` the list of global errors, `+{1}+` the list of field errors.
|
||||
Message codes and arguments for each error within the `BindingResult` are also resolved
|
||||
via `MessageSource`.
|
||||
|
||||
| `NotAcceptableStatusException`
|
||||
| (default)
|
||||
| `+{0}+` list of supported media types
|
||||
@@ -125,14 +117,32 @@ via `MessageSource`.
|
||||
| (default)
|
||||
| `+{0}+` the failure reason provided to the class constructor
|
||||
|
||||
| `MethodNotAllowedException`
|
||||
| `UnsupportedMediaTypeStatusException`
|
||||
| (default)
|
||||
| `+{0}+` the current HTTP method, `+{1}+` the list of supported HTTP methods
|
||||
| `+{0}+` the media type that is not supported, `+{1}+` list of supported media types
|
||||
|
||||
| `UnsupportedMediaTypeStatusException`
|
||||
| (default) + ".parseError"
|
||||
|
|
||||
|
||||
| `UnsatisfiedRequestParameterException`
|
||||
| (default)
|
||||
| `+{0}+` the list of parameter conditions
|
||||
|
||||
| `WebExchangeBindException`
|
||||
| (default)
|
||||
| `+{0}+` the list of global errors, `+{1}+` the list of field errors.
|
||||
Message codes and arguments for each error are also resolved via `MessageSource`.
|
||||
|
||||
|===
|
||||
|
||||
By default, the message code for the "title" field is "problemDetail.title." + the fully
|
||||
qualified exception class name.
|
||||
NOTE: Unlike other exceptions, the message arguments for
|
||||
`WebExchangeBindException` and `HandlerMethodValidationException` are based on a list of
|
||||
`MessageSourceResolvable` errors that can also be customized through a
|
||||
xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource]
|
||||
resource bundle. See
|
||||
xref:core/validation/beanvalidation.adoc#validation-beanvalidation-spring-method-i18n[Customizing Validation Errors]
|
||||
for more details.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -700,9 +700,8 @@ Java::
|
||||
|
||||
@Override
|
||||
public void configurePathMatch(PathMatchConfigurer configurer) {
|
||||
configurer
|
||||
.setUseCaseSensitiveMatch(true)
|
||||
.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController.class));
|
||||
configurer.addPathPrefix(
|
||||
"/api", HandlerTypePredicate.forAnnotation(RestController.class));
|
||||
}
|
||||
}
|
||||
----
|
||||
@@ -717,9 +716,8 @@ Kotlin::
|
||||
|
||||
@Override
|
||||
fun configurePathMatch(configurer: PathMatchConfigurer) {
|
||||
configurer
|
||||
.setUseCaseSensitiveMatch(true)
|
||||
.addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController::class.java))
|
||||
configurer.addPathPrefix(
|
||||
"/api", HandlerTypePredicate.forAnnotation(RestController::class.java))
|
||||
}
|
||||
}
|
||||
----
|
||||
@@ -740,6 +738,59 @@ reliance on it.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-config-blocking-execution]]
|
||||
== Blocking Execution
|
||||
|
||||
The WebFlux Java config allows you to customize blocking execution in WebFlux.
|
||||
|
||||
You can have blocking controller methods called on a separate thread by providing
|
||||
an `Executor` such as the
|
||||
{api-spring-framework}/core/task/VirtualThreadTaskExecutor.html[`VirtualThreadTaskExecutor`]
|
||||
as follows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
public class WebConfig implements WebFluxConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureBlockingExecution(BlockingExecutionConfigurer configurer) {
|
||||
Executor executor = ...
|
||||
configurer.setExecutor(executor);
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
class WebConfig : WebFluxConfigurer {
|
||||
|
||||
@Override
|
||||
fun configureBlockingExecution(configurer: BlockingExecutionConfigurer) {
|
||||
val executor = ...
|
||||
configurer.setExecutor(executor)
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
By default, controller methods whose return type is not recognized by the configured
|
||||
`ReactiveAdapterRegistry` are considered blocking, but you can set a custom controller
|
||||
method predicate via `BlockingExecutionConfigurer`.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webflux-config-websocket-service]]
|
||||
== WebSocketService
|
||||
|
||||
|
||||
@@ -3,23 +3,21 @@
|
||||
|
||||
[.small]#xref:web/webmvc/mvc-controller/ann-initbinder.adoc[See equivalent in the Servlet stack]#
|
||||
|
||||
`@Controller` or `@ControllerAdvice` classes can have `@InitBinder` methods, to
|
||||
initialize instances of `WebDataBinder`. Those, in turn, are used to:
|
||||
`@Controller` or `@ControllerAdvice` classes can have `@InitBinder` methods to
|
||||
initialize `WebDataBinder` instances that in turn can:
|
||||
|
||||
* Bind request parameters (that is, form data or query) to a model object.
|
||||
* Convert `String`-based request values (such as request parameters, path variables,
|
||||
headers, cookies, and others) to the target type of controller method arguments.
|
||||
* Format model object values as `String` values when rendering HTML forms.
|
||||
* Bind request parameters to a model object.
|
||||
* Convert request values from string to object property types.
|
||||
* Format model object properties as strings when rendering HTML forms.
|
||||
|
||||
`@InitBinder` methods can register controller-specific `java.beans.PropertyEditor` or
|
||||
Spring `Converter` and `Formatter` components. In addition, you can use the
|
||||
xref:web/webflux/config.adoc#webflux-config-conversion[WebFlux Java configuration] to register `Converter` and
|
||||
`Formatter` types in a globally shared `FormattingConversionService`.
|
||||
In an `@Controller`, `DataBinder` customizations apply locally within the controller,
|
||||
or even to a specific model attribute referenced by name through the annotation.
|
||||
In an `@ControllerAdvice` customizations can apply to all or a subset of controllers.
|
||||
|
||||
`@InitBinder` methods support many of the same arguments that `@RequestMapping` methods
|
||||
do, except for `@ModelAttribute` (command object) arguments. Typically, they are declared
|
||||
with a `WebDataBinder` argument, for registrations, and a `void` return value.
|
||||
The following example uses the `@InitBinder` annotation:
|
||||
You can register `PropertyEditor`, `Converter`, and `Formatter` components in the
|
||||
`DataBinder` for type conversion. Alternatively, you can use the
|
||||
xref:web/webflux/config.adoc#webflux-config-conversion[WebFlux config] to register
|
||||
`Converter` and `Formatter` components in a globally shared `FormattingConversionService`.
|
||||
|
||||
--
|
||||
[tabs]
|
||||
@@ -112,4 +110,5 @@ Kotlin::
|
||||
== Model Design
|
||||
[.small]#xref:web/webmvc/mvc-controller/ann-initbinder.adoc#mvc-ann-initbinder-model-design[See equivalent in the Servlet stack]#
|
||||
|
||||
include::partial$web/web-data-binding-model-design.adoc[]
|
||||
|
||||
|
||||
+105
-71
@@ -3,11 +3,8 @@
|
||||
|
||||
[.small]#xref:web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc[See equivalent in the Servlet stack]#
|
||||
|
||||
You can use the `@ModelAttribute` annotation on a method argument to access an attribute from the
|
||||
model or have it instantiated if not present. The model attribute is also overlaid with
|
||||
the values of query parameters and form fields whose names match to field names. This is
|
||||
referred to as data binding, and it saves you from having to deal with parsing and
|
||||
converting individual query parameters and form fields. The following example binds an instance of `Pet`:
|
||||
The `@ModelAttribute` method parameter annotation binds request parameters onto a model
|
||||
object. For example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -18,7 +15,7 @@ Java::
|
||||
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
|
||||
public String processSubmit(@ModelAttribute Pet pet) { } // <1>
|
||||
----
|
||||
<1> Bind an instance of `Pet`.
|
||||
<1> Bind to an instance of `Pet`.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
@@ -27,28 +24,66 @@ Kotlin::
|
||||
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
|
||||
fun processSubmit(@ModelAttribute pet: Pet): String { } // <1>
|
||||
----
|
||||
<1> Bind an instance of `Pet`.
|
||||
<1> Bind to an instance of `Pet`.
|
||||
======
|
||||
|
||||
The `Pet` instance in the preceding example is resolved as follows:
|
||||
The `Pet` instance may be:
|
||||
|
||||
* From the model if already added through xref:web/webflux/controller/ann-modelattrib-methods.adoc[`Model`].
|
||||
* From the HTTP session through xref:web/webflux/controller/ann-methods/sessionattributes.adoc[`@SessionAttributes`].
|
||||
* From the invocation of a default constructor.
|
||||
* From the invocation of a "`primary constructor`" with arguments that match query
|
||||
parameters or form fields. Argument names are determined through JavaBeans
|
||||
`@ConstructorProperties` or through runtime-retained parameter names in the bytecode.
|
||||
* Accessed from the model where it could have been added by a
|
||||
xref:web/webflux/controller/ann-modelattrib-methods.adoc[`Model`].
|
||||
* Accessed from the HTTP session if the model attribute was listed in
|
||||
the class-level xref:web/webflux/controller/ann-methods/sessionattributes.adoc[`@SessionAttributes`].
|
||||
* Instantiated through a default constructor.
|
||||
* Instantiated through a "`primary constructor`" with arguments that match to Servlet
|
||||
request parameters. Argument names are determined through runtime-retained parameter
|
||||
names in the bytecode.
|
||||
|
||||
After the model attribute instance is obtained, data binding is applied. The
|
||||
`WebExchangeDataBinder` class matches names of query parameters and form fields to field
|
||||
names on the target `Object`. Matching fields are populated after type conversion is applied
|
||||
where necessary. For more on data binding (and validation), see
|
||||
xref:web/webmvc/mvc-config/validation.adoc[Validation]. For more on customizing data binding, see
|
||||
xref:web/webflux/controller/ann-initbinder.adoc[`DataBinder`].
|
||||
By default, both constructor and property
|
||||
xref:core/validation/beans-beans.adoc#beans-binding[data binding] are applied. However,
|
||||
model object design requires careful consideration, and for security reasons it is
|
||||
recommended either to use an object tailored specifically for web binding, or to apply
|
||||
constructor binding only. If property binding must still be used, then _allowedFields_
|
||||
patterns should be set to limit which properties can be set. For further details on this
|
||||
and example configuration, see
|
||||
xref:web/webflux/controller/ann-initbinder.adoc#webflux-ann-initbinder-model-design[model design].
|
||||
|
||||
Data binding can result in errors. By default, a `WebExchangeBindException` is raised, but,
|
||||
to check for such errors in the controller method, you can add a `BindingResult` argument
|
||||
immediately next to the `@ModelAttribute`, as the following example shows:
|
||||
When using constructor binding, you can customize request parameter names through an
|
||||
`@BindParam` annotation. For example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
class Account {
|
||||
|
||||
private final String firstName;
|
||||
|
||||
public Account(@BindParam("first-name") String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
}
|
||||
----
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
class Account(@BindParam("first-name") val firstName: String)
|
||||
----
|
||||
======
|
||||
|
||||
NOTE: The `@BindParam` may also be placed on the fields that correspond to constructor
|
||||
parameters. While `@BindParam` is supported out of the box, you can also use a
|
||||
different annotation by setting a `DataBinder.NameResolver` on `DataBinder`
|
||||
|
||||
WebFlux, unlike Spring MVC, supports reactive types in the model, e.g. `Mono<Account>`.
|
||||
You can declare a `@ModelAttribute` argument with or without a reactive type wrapper, and
|
||||
it will be resolved accordingly to the actual value.
|
||||
|
||||
If data binding results in errors, by default a `WebExchangeBindException` is raised,
|
||||
but you can also add a `BindingResult` argument immediately next to the `@ModelAttribute`
|
||||
in order to handle such errors in the controller method. For example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -81,49 +116,9 @@ Kotlin::
|
||||
<1> Adding a `BindingResult`.
|
||||
======
|
||||
|
||||
You can automatically apply validation after data binding by adding the
|
||||
`jakarta.validation.Valid` annotation or Spring's `@Validated` annotation (see also
|
||||
xref:core/validation/beanvalidation.adoc[Bean Validation] and
|
||||
xref:web/webmvc/mvc-config/validation.adoc[Spring validation]). The following example uses the `@Valid` annotation:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
|
||||
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { // <1>
|
||||
if (result.hasErrors()) {
|
||||
return "petForm";
|
||||
}
|
||||
// ...
|
||||
}
|
||||
----
|
||||
<1> Using `@Valid` on a model attribute argument.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
|
||||
fun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { // <1>
|
||||
if (result.hasErrors()) {
|
||||
return "petForm"
|
||||
}
|
||||
// ...
|
||||
}
|
||||
----
|
||||
<1> Using `@Valid` on a model attribute argument.
|
||||
======
|
||||
|
||||
Spring WebFlux, unlike Spring MVC, supports reactive types in the model -- for example,
|
||||
`Mono<Account>` or `io.reactivex.Single<Account>`. You can declare a `@ModelAttribute` argument
|
||||
with or without a reactive type wrapper, and it will be resolved accordingly,
|
||||
to the actual value if necessary. However, note that, to use a `BindingResult`
|
||||
argument, you must declare the `@ModelAttribute` argument before it without a reactive
|
||||
type wrapper, as shown earlier. Alternatively, you can handle any errors through the
|
||||
reactive type, as the following example shows:
|
||||
To use a `BindingResult` argument, you must declare the `@ModelAttribute` argument before
|
||||
it without a reactive type wrapper. If you want to use the reactive, you can handle errors
|
||||
directly through it. For example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -160,10 +155,49 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
Note that use of `@ModelAttribute` is optional -- for example, to set its attributes.
|
||||
By default, any argument that is not a simple value type (as determined by
|
||||
{api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty])
|
||||
and is not resolved by any other argument resolver is treated as if it were annotated
|
||||
with `@ModelAttribute`.
|
||||
You can automatically apply validation after data binding by adding the
|
||||
`jakarta.validation.Valid` annotation or Spring's `@Validated` annotation (see
|
||||
xref:core/validation/beanvalidation.adoc[Bean Validation] and
|
||||
xref:web/webmvc/mvc-config/validation.adoc[Spring validation]). For example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
|
||||
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { // <1>
|
||||
if (result.hasErrors()) {
|
||||
return "petForm";
|
||||
}
|
||||
// ...
|
||||
}
|
||||
----
|
||||
<1> Using `@Valid` on a model attribute argument.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
|
||||
fun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { // <1>
|
||||
if (result.hasErrors()) {
|
||||
return "petForm"
|
||||
}
|
||||
// ...
|
||||
}
|
||||
----
|
||||
<1> Using `@Valid` on a model attribute argument.
|
||||
======
|
||||
|
||||
If method validation applies because other parameters have `@Constraint` annotations,
|
||||
then `HandlerMethodValidationException` would be raised instead. See the section on
|
||||
controller method xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation].
|
||||
|
||||
TIP: Using `@ModelAttribute` is optional. By default, any argument that is not a simple
|
||||
value type as determined by
|
||||
{api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]
|
||||
_AND_ that is not resolved by any other argument resolver is treated as an `@ModelAttribute`.
|
||||
|
||||
|
||||
|
||||
+4
-2
@@ -176,6 +176,10 @@ Kotlin::
|
||||
======
|
||||
--
|
||||
|
||||
If method validation applies because other parameters have `@Constraint` annotations,
|
||||
then `HandlerMethodValidationException` is raised instead. See the section on
|
||||
xref:web/webflux/controller/ann-validation.adoc[Validation].
|
||||
|
||||
To access all multipart data as a `MultiValueMap`, you can use `@RequestBody`,
|
||||
as the following example shows:
|
||||
|
||||
@@ -306,5 +310,3 @@ file upload.
|
||||
|
||||
Received part events can also be relayed to another service by using the `WebClient`.
|
||||
See xref:web/webflux-webclient/client-body.adoc#webflux-client-body-multipart[Multipart Data].
|
||||
|
||||
|
||||
|
||||
@@ -89,4 +89,33 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
You can also declare an `Errors` parameter for access to validation errors, but in
|
||||
that case the request body must not be a `Mono`, and will be resolved first:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@PostMapping("/accounts")
|
||||
public void handle(@Valid @RequestBody Account account, Errors errors) {
|
||||
// use one of the onError* operators...
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@PostMapping("/accounts")
|
||||
fun handle(@Valid @RequestBody account: Mono<Account>) {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
If method validation applies because other parameters have `@Constraint` annotations,
|
||||
then `HandlerMethodValidationException` is raised instead. For more details, see the
|
||||
section on xref:web/webflux/controller/ann-validation.adoc[Validation].
|
||||
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
[[webflux-ann-requestmapping]]
|
||||
= Request Mapping
|
||||
= Mapping Requests
|
||||
|
||||
[.small]#xref:web/webmvc/mvc-controller/ann-requestmapping.adoc[See equivalent in the Servlet stack]#
|
||||
|
||||
This section discusses request mapping for annotated controllers.
|
||||
|
||||
[[webflux-ann-requestmapping-annotation]]
|
||||
== `@RequestMapping`
|
||||
|
||||
[.small]#xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-annotation[See equivalent in the Servlet stack]#
|
||||
|
||||
The `@RequestMapping` annotation is used to map requests to controllers methods. It has
|
||||
various attributes to match by URL, HTTP method, request parameters, headers, and media
|
||||
types. You can use it at the class level to express shared mappings or at the method level
|
||||
@@ -500,3 +507,68 @@ Kotlin::
|
||||
|
||||
|
||||
|
||||
[[webflux-ann-httpexchange-annotation]]
|
||||
== `@HttpExchange`
|
||||
[.small]#xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-httpexchange-annotation[See equivalent in the Reactive stack]#
|
||||
|
||||
As an alternative to `@RequestMapping`, you can also handle requests with `@HttpExchange`
|
||||
methods. Such methods are declared on an
|
||||
xref:integration/rest-clients.adoc#rest-http-interface[HTTP Interface] and can be used as
|
||||
a client via `HttpServiceProxyFactory` or implemented by a server `@Controller`.
|
||||
|
||||
For example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@RestController
|
||||
@HttpExchange("/persons")
|
||||
class PersonController {
|
||||
|
||||
@GetExchange("/{id}")
|
||||
public Person getPerson(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@PostExchange
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
public void add(@RequestBody Person person) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@RestController
|
||||
@HttpExchange("/persons")
|
||||
class PersonController {
|
||||
|
||||
@GetExchange("/{id}")
|
||||
fun getPerson(@PathVariable id: Long): Person {
|
||||
// ...
|
||||
}
|
||||
|
||||
@PostExchange
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
fun add(@RequestBody person: Person) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
There some differences between `@HttpExchange` and `@RequestMapping` since the
|
||||
former needs to remain suitable for client and server use. For example, while
|
||||
`@RequestMapping` can be declared to handle any number of paths and each path can
|
||||
be a pattern, `@HttpExchange` must be declared with a single, concrete path. There are
|
||||
also differences in the supported method parameters. Generally, `@HttpExchange` supports
|
||||
a subset of method parameters that `@RequestMapping` does, excluding any parameters that
|
||||
are server side only. For details see the list of supported method parameters for
|
||||
xref:integration/rest-clients.adoc#rest-http-interface-method-parameters[HTTP interface] and for
|
||||
xref:web/webflux/controller/ann-methods/arguments.adoc[@RequestMapping].
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
[[mvc-ann-validation]]
|
||||
= Validation
|
||||
|
||||
[.small]#xref:web/webmvc/mvc-controller/ann-validation.adoc[See equivalent in the Servlet stack]#
|
||||
|
||||
Spring WebFlux has built-in xref:core/validation/validator.adoc[Validation] support for
|
||||
`@RequestMapping` methods, including the option to use
|
||||
xref:core/validation/beanvalidation.adoc[Java Bean Validation].
|
||||
The validation support works on two levels.
|
||||
|
||||
First, method parameters such as
|
||||
xref:web/webflux/controller/ann-methods/modelattrib-method-args.adoc[@ModelAttribute],
|
||||
xref:web/webflux/controller/ann-methods/requestbody.adoc[@RequestBody], and
|
||||
xref:web/webflux/controller/ann-methods/multipart-forms.adoc[@RequestPart] do perform
|
||||
validation if annotated with Jakarta's `@Valid` or Spring's `@Validated` annotation, and
|
||||
raise `MethodArgumentNotValidException` in case of validation errors. If you want to handle
|
||||
the errors in the controller method instead, you can declare an `Errors` or `BindingResult`
|
||||
method parameter immediately after the validated parameter.
|
||||
|
||||
Second, if https://beanvalidation.org/[Java Bean Validation] is present _AND_ other method
|
||||
parameters, e.g. `@RequestHeader`, `@RequestParam`, `@PathVariable` have `@Constraint`
|
||||
annotations, then method validation is applied to all method arguments, raising
|
||||
`HandlerMethodValidationException` in case of validation errors. You can still declare an
|
||||
`Errors` or `BindingResult` after an `@Valid` method parameter, and handle validation
|
||||
errors within the controller method, as long as there are no validation errors on other
|
||||
method arguments.
|
||||
|
||||
You can configure a `Validator` globally through the
|
||||
xref:web/webflux/config.adoc#webflux-config-validation[WebMvc config], or locally
|
||||
through an xref:web/webflux/controller/ann-initbinder.adoc[@InitBinder] method in an
|
||||
`@Controller` or `@ControllerAdvice`. You can also use multiple validators.
|
||||
|
||||
NOTE: If a controller has a class level `@Validated`, then
|
||||
xref:core/validation/beanvalidation.adoc#validation-beanvalidation-spring-method[method validation is applied]
|
||||
through an AOP proxy. In order to take advantage of the Spring MVC built-in support for
|
||||
method validation added in Spring Framework 6.1, you need to remove the class level
|
||||
`@Validated` annotation from the controller.
|
||||
|
||||
The xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses] section provides further
|
||||
details on how `MethodArgumentNotValidException` and `HandlerMethodValidationException`
|
||||
are handled, and also how their rendering can be customized through a `MessageSource` and
|
||||
locale and language specific resource bundles.
|
||||
|
||||
For further custom handling of method validation errors, you can extend
|
||||
`ResponseEntityExceptionHandler` or use an `@ExceptionHandler` method in a controller
|
||||
or in a `@ControllerAdvice`, and handle `HandlerMethodValidationException` directly.
|
||||
The exception contains a list of``ParameterValidationResult``s that group validation errors
|
||||
by method parameter. You can either iterate over those, or provide a visitor with callback
|
||||
methods by controller method parameter type:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
HandlerMethodValidationException ex = ... ;
|
||||
|
||||
ex.visitResults(new HandlerMethodValidationException.Visitor() {
|
||||
|
||||
@Override
|
||||
public void requestHeader(RequestHeader requestHeader, ParameterValidationResult result) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestParam(@Nullable RequestParam requestParam, ParameterValidationResult result) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@Override
|
||||
public void modelAttribute(@Nullable ModelAttribute modelAttribute, ParameterErrors errors) {
|
||||
|
||||
// ...
|
||||
|
||||
@Override
|
||||
public void other(ParameterValidationResult result) {
|
||||
// ...
|
||||
}
|
||||
});
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
// HandlerMethodValidationException
|
||||
val ex
|
||||
|
||||
ex.visitResults(object : HandlerMethodValidationException.Visitor {
|
||||
|
||||
override fun requestHeader(requestHeader: RequestHeader, result: ParameterValidationResult) {
|
||||
// ...
|
||||
}
|
||||
|
||||
override fun requestParam(requestParam: RequestParam?, result: ParameterValidationResult) {
|
||||
// ...
|
||||
}
|
||||
|
||||
override fun modelAttribute(modelAttribute: ModelAttribute?, errors: ParameterErrors) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
override fun other(result: ParameterValidationResult) {
|
||||
// ...
|
||||
}
|
||||
})
|
||||
----
|
||||
======
|
||||
@@ -6,6 +6,26 @@ This section describes options for client-side access to REST endpoints.
|
||||
|
||||
|
||||
|
||||
[[webmvc-restclient]]
|
||||
== `RestClient`
|
||||
|
||||
`RestClient` is a synchronous HTTP client that exposes a modern, fluent API.
|
||||
|
||||
See xref:integration/rest-clients.adoc#rest-restclient[`RestClient`] for more details.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webmvc-webclient]]
|
||||
== `WebClient`
|
||||
|
||||
`WebClient` is a reactive client to perform HTTP requests with a fluent API.
|
||||
|
||||
See xref:web/webflux-webclient.adoc[WebClient] for more details.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webmvc-resttemplate]]
|
||||
== `RestTemplate`
|
||||
|
||||
@@ -13,37 +33,8 @@ This section describes options for client-side access to REST endpoints.
|
||||
Spring REST client and exposes a simple, template-method API over underlying HTTP client
|
||||
libraries.
|
||||
|
||||
NOTE: As of 5.0 the `RestTemplate` is in maintenance mode, with only requests for minor
|
||||
changes and bugs to be accepted. Please, consider using the
|
||||
xref:web/webflux-webclient.adoc[WebClient] which offers a more modern API and
|
||||
supports sync, async, and streaming scenarios.
|
||||
|
||||
See xref:integration/rest-clients.adoc[REST Endpoints] for details.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webmvc-webclient]]
|
||||
== `WebClient`
|
||||
|
||||
`WebClient` is a non-blocking, reactive client to perform HTTP requests. It was
|
||||
introduced in 5.0 and offers a modern alternative to the `RestTemplate`, with efficient
|
||||
support for both synchronous and asynchronous, as well as streaming scenarios.
|
||||
|
||||
In contrast to `RestTemplate`, `WebClient` supports the following:
|
||||
|
||||
* Non-blocking I/O.
|
||||
* Reactive Streams back pressure.
|
||||
* High concurrency with fewer hardware resources.
|
||||
* Functional-style, fluent API that takes advantage of Java 8 lambdas.
|
||||
* Synchronous and asynchronous interactions.
|
||||
* Streaming up to or streaming down from a server.
|
||||
|
||||
See xref:web/webflux-webclient.adoc[WebClient] for more details.
|
||||
|
||||
|
||||
|
||||
|
||||
[[webmvc-http-interface]]
|
||||
== HTTP Interface
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ Kotlin::
|
||||
======
|
||||
|
||||
The return value can then be obtained by running the given task through the
|
||||
xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-configuration-spring-mvc[configured] `TaskExecutor`.
|
||||
xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-configuration-spring-mvc[configured] `AsyncTaskExecutor`.
|
||||
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ Here is a very concise overview of Servlet asynchronous request processing:
|
||||
|
||||
* The controller returns a `Callable`.
|
||||
* Spring MVC calls `request.startAsync()` and submits the `Callable` to
|
||||
a `TaskExecutor` for processing in a separate thread.
|
||||
an `AsyncTaskExecutor` for processing in a separate thread.
|
||||
* Meanwhile, the `DispatcherServlet` and all filters exit the Servlet container thread,
|
||||
but the response remains open.
|
||||
* Eventually the `Callable` produces a result, and Spring MVC dispatches the request back
|
||||
@@ -404,11 +404,10 @@ TIP: Spring MVC supports Reactor and RxJava through the
|
||||
|
||||
For streaming to the response, reactive back pressure is supported, but writes to the
|
||||
response are still blocking and are run on a separate thread through the
|
||||
xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-configuration-spring-mvc[configured] `TaskExecutor`, to avoid
|
||||
blocking the upstream source (such as a `Flux` returned from `WebClient`).
|
||||
By default, `SimpleAsyncTaskExecutor` is used for the blocking writes, but that is not
|
||||
suitable under load. If you plan to stream with a reactive type, you should use the
|
||||
xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-configuration-spring-mvc[MVC configuration] to configure a task executor.
|
||||
xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-configuration-spring-mvc[configured]
|
||||
`AsyncTaskExecutor`, to avoid blocking the upstream source such as a `Flux` returned
|
||||
from `WebClient`.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -494,7 +493,7 @@ In `web.xml` configuration, you can add `<async-supported>true</async-supported>
|
||||
[[mvc-ann-async-configuration-spring-mvc]]
|
||||
=== Spring MVC
|
||||
|
||||
The MVC configuration exposes the following options related to asynchronous request processing:
|
||||
The MVC configuration exposes the following options for asynchronous request processing:
|
||||
|
||||
* Java configuration: Use the `configureAsyncSupport` callback on `WebMvcConfigurer`.
|
||||
* XML namespace: Use the `<async-support>` element under `<mvc:annotation-driven>`.
|
||||
@@ -504,10 +503,9 @@ You can configure the following:
|
||||
* Default timeout value for async requests, which if not set, depends
|
||||
on the underlying Servlet container.
|
||||
* `AsyncTaskExecutor` to use for blocking writes when streaming with
|
||||
xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-reactive-types[Reactive Types] and for executing `Callable` instances returned from
|
||||
controller methods. We highly recommended configuring this property if you
|
||||
stream with reactive types or have controller methods that return `Callable`, since
|
||||
by default, it is a `SimpleAsyncTaskExecutor`.
|
||||
xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-reactive-types[Reactive Types] and for
|
||||
executing `Callable` instances returned from controller methods.
|
||||
The one used by default is not suitable for production under load.
|
||||
* `DeferredResultProcessingInterceptor` implementations and `CallableProcessingInterceptor` implementations.
|
||||
|
||||
Note that you can also set the default timeout value on a `DeferredResult`,
|
||||
|
||||
@@ -67,24 +67,25 @@ from an existing `ProblemDetail`. This could be done centrally, e.g. from an
|
||||
|
||||
|
||||
[[mvc-ann-rest-exceptions-i18n]]
|
||||
== Internationalization
|
||||
== Customization and i18n
|
||||
[.small]#xref:web/webflux/ann-rest-exceptions.adoc#webflux-ann-rest-exceptions-i18n[See equivalent in the Reactive stack]#
|
||||
|
||||
It is a common requirement to internationalize error response details, and good practice
|
||||
to customize the problem details for Spring MVC exceptions. This is supported as follows:
|
||||
It is a common requirement to customize and internationalize error response details.
|
||||
It is also good practice to customize the problem details for Spring MVC exceptions
|
||||
to avoid revealing implementation details. This section describes the support for that.
|
||||
|
||||
- Each `ErrorResponse` exposes a message code and arguments to resolve the "detail" field
|
||||
through a xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource].
|
||||
The actual message code value is parameterized with placeholders, e.g.
|
||||
`+"HTTP method {0} not supported"+` to be expanded from the arguments.
|
||||
- Each `ErrorResponse` also exposes a message code to resolve the "title" field.
|
||||
- `ResponseEntityExceptionHandler` uses the message code and arguments to resolve the
|
||||
"detail" and the "title" fields.
|
||||
An `ErrorResponse` exposes message codes for "type", "title", and "detail", as well as
|
||||
message code arguments for the "detail" field. `ResponseEntityExceptionHandler` resolves
|
||||
these through a xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource]
|
||||
and updates the corresponding `ProblemDetail` fields accordingly.
|
||||
|
||||
By default, the message code for the "detail" field is "problemDetail." + the fully
|
||||
qualified exception class name. Some exceptions may expose additional message codes in
|
||||
which case a suffix is added to the default message code. The table below lists message
|
||||
arguments and codes for Spring MVC exceptions:
|
||||
The default strategy for message codes follows the pattern:
|
||||
|
||||
`problemDetail.[type|title|detail].[fully qualified exception class name]`
|
||||
|
||||
An `ErrorResponse` may expose more than one message code, typically adding a suffix
|
||||
to the default message code. The table below lists message codes, and arguments for
|
||||
Spring MVC exceptions:
|
||||
|
||||
[[mvc-ann-rest-exceptions-codes]]
|
||||
[cols="1,1,2", options="header"]
|
||||
@@ -99,6 +100,11 @@ arguments and codes for Spring MVC exceptions:
|
||||
| (default)
|
||||
| `+{0}+` property name, `+{1}+` property value
|
||||
|
||||
| `HandlerMethodValidationException`
|
||||
| (default)
|
||||
| `+{0}+` list all validation errors.
|
||||
Message codes and arguments for each error are also resolved via `MessageSource`.
|
||||
|
||||
| `HttpMediaTypeNotAcceptableException`
|
||||
| (default)
|
||||
| `+{0}+` list of supported media types
|
||||
@@ -130,8 +136,7 @@ arguments and codes for Spring MVC exceptions:
|
||||
| `MethodArgumentNotValidException`
|
||||
| (default)
|
||||
| `+{0}+` the list of global errors, `+{1}+` the list of field errors.
|
||||
Message codes and arguments for each error within the `BindingResult` are also resolved
|
||||
via `MessageSource`.
|
||||
Message codes and arguments for each error are also resolvedvia `MessageSource`.
|
||||
|
||||
| `MissingRequestHeaderException`
|
||||
| (default)
|
||||
@@ -161,6 +166,10 @@ arguments and codes for Spring MVC exceptions:
|
||||
| (default)
|
||||
|
|
||||
|
||||
| `NoResourceFoundException`
|
||||
| (default)
|
||||
|
|
||||
|
||||
| `TypeMismatchException`
|
||||
| (default)
|
||||
| `+{0}+` property name, `+{1}+` property value
|
||||
@@ -171,8 +180,13 @@ arguments and codes for Spring MVC exceptions:
|
||||
|
||||
|===
|
||||
|
||||
By default, the message code for the "title" field is "problemDetail.title." + the fully
|
||||
qualified exception class name.
|
||||
NOTE: Unlike other exceptions, the message arguments for
|
||||
`MethodArgumentValidException` and `HandlerMethodValidationException` are baed on a list of
|
||||
`MessageSourceResolvable` errors that can also be customized through a
|
||||
xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource]
|
||||
resource bundle. See
|
||||
xref:core/validation/beanvalidation.adoc#validation-beanvalidation-spring-method-i18n[Customizing Validation Errors]
|
||||
for more details.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
By default, if xref:core/validation/beanvalidation.adoc#validation-beanvalidation-overview[Bean Validation] is present
|
||||
on the classpath (for example, Hibernate Validator), the `LocalValidatorFactoryBean` is
|
||||
registered as a global xref:core/validation/validator.adoc[Validator] for use with `@Valid` and
|
||||
`Validated` on controller method arguments.
|
||||
`@Validated` on controller method arguments.
|
||||
|
||||
In Java configuration, you can customize the global `Validator` instance, as the
|
||||
following example shows:
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
[[mvc-ann-initbinder]]
|
||||
= `DataBinder`
|
||||
= `@InitBinder`
|
||||
|
||||
[.small]#xref:web/webflux/controller/ann-initbinder.adoc[See equivalent in the Reactive stack]#
|
||||
|
||||
`@Controller` or `@ControllerAdvice` classes can have `@InitBinder` methods that
|
||||
initialize instances of `WebDataBinder`, and those, in turn, can:
|
||||
`@Controller` or `@ControllerAdvice` classes can have `@InitBinder` methods to
|
||||
initialize `WebDataBinder` instances that in turn can:
|
||||
|
||||
* Bind request parameters (that is, form or query data) to a model object.
|
||||
* Convert String-based request values (such as request parameters, path variables,
|
||||
headers, cookies, and others) to the target type of controller method arguments.
|
||||
* Format model object values as `String` values when rendering HTML forms.
|
||||
* Bind request parameters to a model object.
|
||||
* Convert request values from string to object property types.
|
||||
* Format model object properties as strings when rendering HTML forms.
|
||||
|
||||
`@InitBinder` methods can register controller-specific `java.beans.PropertyEditor` or
|
||||
Spring `Converter` and `Formatter` components. In addition, you can use the
|
||||
xref:web/webmvc/mvc-config/conversion.adoc[MVC config] to register `Converter` and `Formatter`
|
||||
types in a globally shared `FormattingConversionService`.
|
||||
In an `@Controller`, `DataBinder` customizations apply locally within the controller,
|
||||
or even to a specific model attribute referenced by name through the annotation.
|
||||
In an `@ControllerAdvice` customizations can apply to all or a subset of controllers.
|
||||
|
||||
`@InitBinder` methods support many of the same arguments that `@RequestMapping` methods
|
||||
do, except for `@ModelAttribute` (command object) arguments. Typically, they are declared
|
||||
with a `WebDataBinder` argument (for registrations) and a `void` return value.
|
||||
The following listing shows an example:
|
||||
You can register `PropertyEditor`, `Converter`, and `Formatter` components in the
|
||||
`DataBinder` for type conversion. Alternatively, you can use the
|
||||
xref:web/webmvc/mvc-config/conversion.adoc[MVC config] to register `Converter` and
|
||||
`Formatter` components in a globally shared `FormattingConversionService`.
|
||||
|
||||
`@InitBinder` methods can have many of the same arguments that `@RequestMapping` methods
|
||||
have, with the notable exception of `@ModelAttribute`. Typically, such methods have a
|
||||
`WebDataBinder` argument (for registrations) and a `void` return value, for example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
|
||||
+121
-94
@@ -3,11 +3,8 @@
|
||||
|
||||
[.small]#xref:web/webflux/controller/ann-methods/modelattrib-method-args.adoc[See equivalent in the Reactive stack]#
|
||||
|
||||
You can use the `@ModelAttribute` annotation on a method argument to access an attribute from
|
||||
the model or have it be instantiated if not present. The model attribute is also overlain with
|
||||
values from HTTP Servlet request parameters whose names match to field names. This is referred
|
||||
to as data binding, and it saves you from having to deal with parsing and converting individual
|
||||
query parameters and form fields. The following example shows how to do so:
|
||||
The `@ModelAttribute` method parameter annotation binds request parameters onto a model
|
||||
object. For example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -20,7 +17,7 @@ Java::
|
||||
// method logic...
|
||||
}
|
||||
----
|
||||
<1> Bind an instance of `Pet`.
|
||||
<1> Bind to an instance of `Pet`.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
@@ -31,30 +28,27 @@ fun processSubmit(@ModelAttribute pet: Pet): String { // <1>
|
||||
// method logic...
|
||||
}
|
||||
----
|
||||
<1> Bind an instance of `Pet`.
|
||||
<1> Bind to an instance of `Pet`.
|
||||
======
|
||||
|
||||
The `Pet` instance above is sourced in one of the following ways:
|
||||
The `Pet` instance may be:
|
||||
|
||||
* Retrieved from the model where it may have been added by a
|
||||
* Accessed from the model where it could have been added by a
|
||||
xref:web/webmvc/mvc-controller/ann-modelattrib-methods.adoc[@ModelAttribute method].
|
||||
* Retrieved from the HTTP session if the model attribute was listed in
|
||||
* Accessed from the HTTP session if the model attribute was listed in
|
||||
the class-level xref:web/webmvc/mvc-controller/ann-methods/sessionattributes.adoc[`@SessionAttributes`] annotation.
|
||||
* Obtained through a `Converter` where the model attribute name matches the name of a
|
||||
request value such as a path variable or a request parameter (see next example).
|
||||
* Instantiated using its default constructor.
|
||||
* Obtained through a `Converter` if the model attribute name matches the name of a
|
||||
request value such as a path variable or a request parameter (example follows).
|
||||
* Instantiated through a default constructor.
|
||||
* Instantiated through a "`primary constructor`" with arguments that match to Servlet
|
||||
request parameters. Argument names are determined through JavaBeans
|
||||
`@ConstructorProperties` or through runtime-retained parameter names in the bytecode.
|
||||
request parameters. Argument names are determined through runtime-retained parameter
|
||||
names in the bytecode.
|
||||
|
||||
One alternative to using a xref:web/webmvc/mvc-controller/ann-modelattrib-methods.adoc[@ModelAttribute method] to
|
||||
supply it or relying on the framework to create the model attribute, is to have a
|
||||
`Converter<String, T>` to provide the instance. This is applied when the model attribute
|
||||
name matches to the name of a request value such as a path variable or a request
|
||||
parameter, and there is a `Converter` from `String` to the model attribute type.
|
||||
In the following example, the model attribute name is `account` which matches the URI
|
||||
path variable `account`, and there is a registered `Converter<String, Account>` which
|
||||
could load the `Account` from a data store:
|
||||
As mentioned above, a `Converter<String, T>` may be used to obtain the model object if
|
||||
the model attribute name matches to the name of a request value such as a path variable or a
|
||||
request parameter, _and_ there is a compatible `Converter<String, T>`. In the below example,
|
||||
the model attribute name `account` matches URI path variable `account`, and there is a
|
||||
registered `Converter<String, Account>` that perhaps retrieves it from a persistence store:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -67,7 +61,6 @@ Java::
|
||||
// ...
|
||||
}
|
||||
----
|
||||
<1> Bind an instance of `Account` using an explicit attribute name.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
@@ -78,19 +71,101 @@ Kotlin::
|
||||
// ...
|
||||
}
|
||||
----
|
||||
<1> Bind an instance of `Account` using an explicit attribute name.
|
||||
======
|
||||
|
||||
After the model attribute instance is obtained, data binding is applied. The
|
||||
`WebDataBinder` class matches Servlet request parameter names (query parameters and form
|
||||
fields) to field names on the target `Object`. Matching fields are populated after type
|
||||
conversion is applied, where necessary. For more on data binding (and validation), see
|
||||
xref:web/webmvc/mvc-config/validation.adoc[Validation]. For more on customizing data binding, see
|
||||
xref:web/webmvc/mvc-controller/ann-initbinder.adoc[`DataBinder`].
|
||||
By default, both constructor and property
|
||||
xref:core/validation/beans-beans.adoc#beans-binding[data binding] are applied. However,
|
||||
model object design requires careful consideration, and for security reasons it is
|
||||
recommended either to use an object tailored specifically for web binding, or to apply
|
||||
constructor binding only. If property binding must still be used, then _allowedFields_
|
||||
patterns should be set to limit which properties can be set. For further details on this
|
||||
and example configuration, see
|
||||
xref:web/webmvc/mvc-controller/ann-initbinder.adoc#mvc-ann-initbinder-model-design[model design].
|
||||
|
||||
Data binding can result in errors. By default, a `BindException` is raised. However, to check
|
||||
for such errors in the controller method, you can add a `BindingResult` argument immediately next
|
||||
to the `@ModelAttribute`, as the following example shows:
|
||||
When using constructor binding, you can customize request parameter names through an
|
||||
`@BindParam` annotation. For example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
class Account {
|
||||
|
||||
private final String firstName;
|
||||
|
||||
public Account(@BindParam("first-name") String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
}
|
||||
----
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
class Account(@BindParam("first-name") val firstName: String)
|
||||
----
|
||||
======
|
||||
|
||||
NOTE: The `@BindParam` may also be placed on the fields that correspond to constructor
|
||||
parameters. While `@BindParam` is supported out of the box, you can also use a
|
||||
different annotation by setting a `DataBinder.NameResolver` on `DataBinder`
|
||||
|
||||
In some cases, you may want access to a model attribute without data binding. For such
|
||||
cases, you can inject the `Model` into the controller and access it directly or,
|
||||
alternatively, set `@ModelAttribute(binding=false)`, as the following example shows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@ModelAttribute
|
||||
public AccountForm setUpForm() {
|
||||
return new AccountForm();
|
||||
}
|
||||
|
||||
@ModelAttribute
|
||||
public Account findAccount(@PathVariable String accountId) {
|
||||
return accountRepository.findOne(accountId);
|
||||
}
|
||||
|
||||
@PostMapping("update")
|
||||
public String update(AccountForm form, BindingResult result,
|
||||
@ModelAttribute(binding=false) Account account) { // <1>
|
||||
// ...
|
||||
}
|
||||
----
|
||||
<1> Setting `@ModelAttribute(binding=false)`.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@ModelAttribute
|
||||
fun setUpForm(): AccountForm {
|
||||
return AccountForm()
|
||||
}
|
||||
|
||||
@ModelAttribute
|
||||
fun findAccount(@PathVariable accountId: String): Account {
|
||||
return accountRepository.findOne(accountId)
|
||||
}
|
||||
|
||||
@PostMapping("update")
|
||||
fun update(form: AccountForm, result: BindingResult,
|
||||
@ModelAttribute(binding = false) account: Account): String { // <1>
|
||||
// ...
|
||||
}
|
||||
----
|
||||
<1> Setting `@ModelAt\tribute(binding=false)`.
|
||||
======
|
||||
|
||||
If data binding results in errors, by default a `MethodArgumentNotValidException` is raised,
|
||||
but you can also add a `BindingResult` argument immediately next to the `@ModelAttribute`
|
||||
in order to handle such errors in the controller method. For example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -123,61 +198,10 @@ Kotlin::
|
||||
<1> Adding a `BindingResult` next to the `@ModelAttribute`.
|
||||
======
|
||||
|
||||
In some cases, you may want access to a model attribute without data binding. For such
|
||||
cases, you can inject the `Model` into the controller and access it directly or,
|
||||
alternatively, set `@ModelAttribute(binding=false)`, as the following example shows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@ModelAttribute
|
||||
public AccountForm setUpForm() {
|
||||
return new AccountForm();
|
||||
}
|
||||
|
||||
@ModelAttribute
|
||||
public Account findAccount(@PathVariable String accountId) {
|
||||
return accountRepository.findOne(accountId);
|
||||
}
|
||||
|
||||
@PostMapping("update")
|
||||
public String update(@Valid AccountForm form, BindingResult result,
|
||||
@ModelAttribute(binding=false) Account account) { // <1>
|
||||
// ...
|
||||
}
|
||||
----
|
||||
<1> Setting `@ModelAttribute(binding=false)`.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@ModelAttribute
|
||||
fun setUpForm(): AccountForm {
|
||||
return AccountForm()
|
||||
}
|
||||
|
||||
@ModelAttribute
|
||||
fun findAccount(@PathVariable accountId: String): Account {
|
||||
return accountRepository.findOne(accountId)
|
||||
}
|
||||
|
||||
@PostMapping("update")
|
||||
fun update(@Valid form: AccountForm, result: BindingResult,
|
||||
@ModelAttribute(binding = false) account: Account): String { // <1>
|
||||
// ...
|
||||
}
|
||||
----
|
||||
<1> Setting `@ModelAttribute(binding=false)`.
|
||||
======
|
||||
|
||||
You can automatically apply validation after data binding by adding the
|
||||
`jakarta.validation.Valid` annotation or Spring's `@Validated` annotation
|
||||
(xref:core/validation/beanvalidation.adoc[Bean Validation] and
|
||||
xref:web/webmvc/mvc-config/validation.adoc[Spring validation]). The following example shows how to do so:
|
||||
`jakarta.validation.Valid` annotation or Spring's `@Validated` annotation.
|
||||
See xref:core/validation/beanvalidation.adoc[Bean Validation] and
|
||||
xref:web/webmvc/mvc-config/validation.adoc[Spring validation]. For example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -210,10 +234,13 @@ Kotlin::
|
||||
<1> Validate the `Pet` instance.
|
||||
======
|
||||
|
||||
Note that using `@ModelAttribute` is optional (for example, to set its attributes).
|
||||
By default, any argument that is not a simple value type (as determined by
|
||||
{api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty])
|
||||
and is not resolved by any other argument resolver is treated as if it were annotated
|
||||
with `@ModelAttribute`.
|
||||
|
||||
If there is no `BindingResult` parameter after the `@ModelAttribute`, then
|
||||
`MethodArgumentNotValueException` is raised with the validation errors. However, if method
|
||||
validation applies because other parameters have `@jakarta.validation.Constraint` annotations,
|
||||
then `HandlerMethodValidationException` is raised instead. For more details, see the section
|
||||
xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation].
|
||||
|
||||
TIP: Using `@ModelAttribute` is optional. By default, any parameter that is not a simple
|
||||
value type as determined by
|
||||
{api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]
|
||||
_AND_ that is not resolved by any other argument resolver is treated as an `@ModelAttribute`.
|
||||
|
||||
+5
-4
@@ -188,8 +188,7 @@ Java::
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@PostMapping("/")
|
||||
public String handle(@Valid @RequestPart("meta-data") MetaData metadata,
|
||||
BindingResult result) {
|
||||
public String handle(@Valid @RequestPart("meta-data") MetaData metadata, Errors errors) {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
@@ -199,12 +198,14 @@ Kotlin::
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@PostMapping("/")
|
||||
fun handle(@Valid @RequestPart("meta-data") metadata: MetaData,
|
||||
result: BindingResult): String {
|
||||
fun handle(@Valid @RequestPart("meta-data") metadata: MetaData, errors: Errors): String {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
If method validation applies because other parameters have `@Constraint` annotations,
|
||||
then `HandlerMethodValidationException` is raised instead. For more details, see the
|
||||
section on xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation].
|
||||
|
||||
|
||||
|
||||
+5
-2
@@ -48,7 +48,7 @@ Java::
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@PostMapping("/accounts")
|
||||
public void handle(@Valid @RequestBody Account account, BindingResult result) {
|
||||
public void handle(@Valid @RequestBody Account account, Errors errors) {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
@@ -58,10 +58,13 @@ Kotlin::
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@PostMapping("/accounts")
|
||||
fun handle(@Valid @RequestBody account: Account, result: BindingResult) {
|
||||
fun handle(@Valid @RequestBody account: Account, errors: Errors) {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
If method validation applies because other parameters have `@Constraint` annotations,
|
||||
then `HandlerMethodValidationException` is raised instead. For more details, see the
|
||||
section on xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation].
|
||||
|
||||
|
||||
+75
-1
@@ -1,8 +1,17 @@
|
||||
[[mvc-ann-requestmapping]]
|
||||
= Request Mapping
|
||||
= Mapping Requests
|
||||
|
||||
[.small]#xref:web/webflux/controller/ann-requestmapping.adoc[See equivalent in the Reactive stack]#
|
||||
|
||||
This section discusses request mapping for annotated controllers.
|
||||
|
||||
|
||||
|
||||
[[mvc-ann-requestmapping-annotation]]
|
||||
== `@RequestMapping`
|
||||
|
||||
[.small]#xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-annotation[See equivalent in the Reactive stack]#
|
||||
|
||||
You can use the `@RequestMapping` annotation to map requests to controllers methods. It has
|
||||
various attributes to match by URL, HTTP method, request parameters, headers, and media
|
||||
types. You can use it at the class level to express shared mappings or at the method level
|
||||
@@ -549,3 +558,68 @@ Kotlin::
|
||||
|
||||
|
||||
|
||||
[[mvc-ann-httpexchange-annotation]]
|
||||
== `@HttpExchange`
|
||||
[.small]#xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-httpexchange-annotation[See equivalent in the Reactive stack]#
|
||||
|
||||
As an alternative to `@RequestMapping`, you can also handle requests with `@HttpExchange`
|
||||
methods. Such methods are declared on an
|
||||
xref:integration/rest-clients.adoc#rest-http-interface[HTTP Interface] and can be used as
|
||||
a client via `HttpServiceProxyFactory` or implemented by a server `@Controller`.
|
||||
|
||||
For example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@RestController
|
||||
@HttpExchange("/persons")
|
||||
class PersonController {
|
||||
|
||||
@GetExchange("/{id}")
|
||||
public Person getPerson(@PathVariable Long id) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@PostExchange
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
public void add(@RequestBody Person person) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@RestController
|
||||
@HttpExchange("/persons")
|
||||
class PersonController {
|
||||
|
||||
@GetExchange("/{id}")
|
||||
fun getPerson(@PathVariable id: Long): Person {
|
||||
// ...
|
||||
}
|
||||
|
||||
@PostExchange
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
fun add(@RequestBody person: Person) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
There some differences between `@HttpExchange` and `@RequestMapping` since the
|
||||
former needs to remain suitable for client and server use. For example, while
|
||||
`@RequestMapping` can be declared to handle any number of paths and each path can
|
||||
be a pattern, `@HttpExchange` must be declared with a single, concrete path. There are
|
||||
also differences in the supported method parameters. Generally, `@HttpExchange` supports
|
||||
a subset of method parameters that `@RequestMapping` does, excluding any parameters that
|
||||
are server side only. For details see the list of supported method parameters for
|
||||
xref:integration/rest-clients.adoc#rest-http-interface-method-parameters[HTTP interface] and for
|
||||
xref:web/webmvc/mvc-controller/ann-methods/arguments.adoc[@RequestMapping].
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
[[mvc-ann-validation]]
|
||||
= Validation
|
||||
|
||||
[.small]#xref:web/webflux/controller/ann-validation.adoc[See equivalent in the Reactive stack]#
|
||||
|
||||
Spring MVC has built-in xref:core/validation/validator.adoc[Validation] support for
|
||||
`@RequestMapping` methods, including the option to use
|
||||
xref:core/validation/beanvalidation.adoc[Java Bean Validation].
|
||||
The validation support works on two levels.
|
||||
|
||||
First, method parameters such as
|
||||
xref:web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc[@ModelAttribute],
|
||||
xref:web/webmvc/mvc-controller/ann-methods/requestbody.adoc[@RequestBody], and
|
||||
xref:web/webmvc/mvc-controller/ann-methods/multipart-forms.adoc[@RequestPart] do perform
|
||||
validation if annotated with Jakarta's `@Valid` or Spring's `@Validated` annotation, and
|
||||
raise `MethodArgumentNotValidException` in case of validation errors. If you want to handle
|
||||
the errors in the controller method instead, you can declare an `Errors` or `BindingResult`
|
||||
method parameter immediately after the validated parameter.
|
||||
|
||||
Second, if https://beanvalidation.org/[Java Bean Validation] is present _AND_ other method
|
||||
parameters, e.g. `@RequestHeader`, `@RequestParam`, `@PathVariable` have `@Constraint`
|
||||
annotations, then method validation is applied to all method arguments, raising
|
||||
`HandlerMethodValidationException` in case of validation errors. You can still declare an
|
||||
`Errors` or `BindingResult` after an `@Valid` method parameter, and handle validation
|
||||
errors within the controller method, as long as there are no validation errors on other
|
||||
method arguments. Method validation is also applied to the return value if the method
|
||||
is annotated with `@Valid` or has other `@Constraint` annotations.
|
||||
|
||||
You can configure a `Validator` globally through the
|
||||
xref:web/webmvc/mvc-config/validation.adoc[WebMvc config], or locally through an
|
||||
xref:web/webmvc/mvc-controller/ann-initbinder.adoc[@InitBinder] method in an
|
||||
`@Controller` or `@ControllerAdvice`. You can also use multiple validators.
|
||||
|
||||
NOTE: If a controller has a class level `@Validated`, then
|
||||
xref:core/validation/beanvalidation.adoc#validation-beanvalidation-spring-method[method validation is applied]
|
||||
through an AOP proxy. In order to take advantage of the Spring MVC built-in support for
|
||||
method validation added in Spring Framework 6.1, you need to remove the class level
|
||||
`@Validated` annotation from the controller.
|
||||
|
||||
The xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses] section provides further
|
||||
details on how `MethodArgumentNotValidException` and `HandlerMethodValidationException`
|
||||
are handled, and also how their rendering can be customized through a `MessageSource` and
|
||||
locale and language specific resource bundles.
|
||||
|
||||
For further custom handling of method validation errors, you can extend
|
||||
`ResponseEntityExceptionHandler` or use an `@ExceptionHandler` method in a controller
|
||||
or in a `@ControllerAdvice`, and handle `HandlerMethodValidationException` directly.
|
||||
The exception contains a list of``ParameterValidationResult``s that group validation errors
|
||||
by method parameter. You can either iterate over those, or provide a visitor with callback
|
||||
methods by controller method parameter type:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
HandlerMethodValidationException ex = ... ;
|
||||
|
||||
ex.visitResults(new HandlerMethodValidationException.Visitor() {
|
||||
|
||||
@Override
|
||||
public void requestHeader(RequestHeader requestHeader, ParameterValidationResult result) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestParam(@Nullable RequestParam requestParam, ParameterValidationResult result) {
|
||||
// ...
|
||||
}
|
||||
|
||||
@Override
|
||||
public void modelAttribute(@Nullable ModelAttribute modelAttribute, ParameterErrors errors) {
|
||||
|
||||
// ...
|
||||
|
||||
@Override
|
||||
public void other(ParameterValidationResult result) {
|
||||
// ...
|
||||
}
|
||||
});
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
// HandlerMethodValidationException
|
||||
val ex
|
||||
|
||||
ex.visitResults(object : HandlerMethodValidationException.Visitor {
|
||||
|
||||
override fun requestHeader(requestHeader: RequestHeader, result: ParameterValidationResult) {
|
||||
// ...
|
||||
}
|
||||
|
||||
override fun requestParam(requestParam: RequestParam?, result: ParameterValidationResult) {
|
||||
// ...
|
||||
}
|
||||
|
||||
override fun modelAttribute(modelAttribute: ModelAttribute?, errors: ParameterErrors) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
override fun other(result: ParameterValidationResult) {
|
||||
// ...
|
||||
}
|
||||
})
|
||||
----
|
||||
======
|
||||
@@ -62,8 +62,7 @@ initialization parameters (`init-param` elements) to the Servlet declaration in
|
||||
The exception can then be caught with a `HandlerExceptionResolver` (for example, by using an
|
||||
`@ExceptionHandler` controller method) and handled as any others.
|
||||
|
||||
By default, this is set to `false`, in which case the `DispatcherServlet` sets the
|
||||
response status to 404 (NOT_FOUND) without raising an exception.
|
||||
As of 6.1, this property is set to `true` and deprecated.
|
||||
|
||||
Note that, if xref:web/webmvc/mvc-config/default-servlet-handler.adoc[default servlet handling] is
|
||||
also configured, unresolved requests are always forwarded to the default servlet
|
||||
|
||||
@@ -1,29 +1,16 @@
|
||||
In the context of web applications, _data binding_ involves the binding of HTTP request
|
||||
parameters (that is, form data or query parameters) to properties in a model object and
|
||||
its nested objects.
|
||||
xref:core/validation/beans-beans.adoc#beans-binding[Data binding] for web requests involves
|
||||
binding request parameters to a model object. By default, request parameters can be bound
|
||||
to any public property of the model object, which means malicious clients can provide
|
||||
extra values for properties that exist in the model object graph, but are not expected to
|
||||
be set. This is why model object design requires careful consideration.
|
||||
|
||||
Only `public` properties following the
|
||||
https://www.oracle.com/java/technologies/javase/javabeans-spec.html[JavaBeans naming conventions]
|
||||
are exposed for data binding — for example, `public String getFirstName()` and
|
||||
`public void setFirstName(String)` methods for a `firstName` property.
|
||||
|
||||
TIP: The model object, and its nested object graph, is also sometimes referred to as a
|
||||
TIP: The model object, and its nested object graph is also sometimes referred to as a
|
||||
_command object_, _form-backing object_, or _POJO_ (Plain Old Java Object).
|
||||
|
||||
By default, Spring permits binding to all public properties in the model object graph.
|
||||
This means you need to carefully consider what public properties the model has, since a
|
||||
client could target any public property path, even some that are not expected to be
|
||||
targeted for a given use case.
|
||||
|
||||
For example, given an HTTP form data endpoint, a malicious client could supply values for
|
||||
properties that exist in the model object graph but are not part of the HTML form
|
||||
presented in the browser. This could lead to data being set on the model object and any
|
||||
of its nested objects, that is not expected to be updated.
|
||||
|
||||
The recommended approach is to use a _dedicated model object_ that exposes only
|
||||
properties that are relevant for the form submission. For example, on a form for changing
|
||||
a user's email address, the model object should declare a minimum set of properties such
|
||||
as in the following `ChangeEmailForm`.
|
||||
A good practice is to use a _dedicated model object_ rather than exposing your domain
|
||||
model such as JPA or Hibernate entities for web data binding. For example, on a form to
|
||||
change an email address, create a `ChangeEmailForm` model object that declares only
|
||||
the properties required for the input:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@@ -51,13 +38,16 @@ as in the following `ChangeEmailForm`.
|
||||
}
|
||||
----
|
||||
|
||||
If you cannot or do not want to use a _dedicated model object_ for each data
|
||||
binding use case, you **must** limit the properties that are allowed for data binding.
|
||||
Ideally, you can achieve this by registering _allowed field patterns_ via the
|
||||
`setAllowedFields()` method on `WebDataBinder`.
|
||||
Another good practice is to apply
|
||||
xref:core/validation/beans-beans.adoc#beans-constructor-binding[constructor binding],
|
||||
which uses only the request parameters it needs for constructor arguments, and any other
|
||||
input is ignored. This is in contrast to property binding which by default binds every
|
||||
request parameter for which there is a matching property.
|
||||
|
||||
For example, to register allowed field patterns in your application, you can implement an
|
||||
`@InitBinder` method in a `@Controller` or `@ControllerAdvice` component as shown below:
|
||||
If neither a dedicated model object nor constructor binding is sufficient, and you must
|
||||
use property binding, we strongy recommend registering `allowedFields` patterns (case
|
||||
sensitive) on `WebDataBinder` in order to prevent unexpected properties from being set.
|
||||
For example:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@@ -74,22 +64,28 @@ For example, to register allowed field patterns in your application, you can imp
|
||||
}
|
||||
----
|
||||
|
||||
In addition to registering allowed patterns, it is also possible to register _disallowed
|
||||
field patterns_ via the `setDisallowedFields()` method in `DataBinder` and its subclasses.
|
||||
Please note, however, that an "allow list" is safer than a "deny list". Consequently,
|
||||
`setAllowedFields()` should be favored over `setDisallowedFields()`.
|
||||
You can also register `disallowedFields` patterns (case insensitive). However,
|
||||
"allowed" configuration is preferred over "disallowed" as it is more explicit and less
|
||||
prone to mistakes.
|
||||
|
||||
Note that matching against allowed field patterns is case-sensitive; whereas, matching
|
||||
against disallowed field patterns is case-insensitive. In addition, a field matching a
|
||||
disallowed pattern will not be accepted even if it also happens to match a pattern in the
|
||||
allowed list.
|
||||
By default, constructor and property binding are both used. If you want to use
|
||||
constructor binding only, you can set the `declarativeBinding` flag on `WebDataBinder`
|
||||
through an `@InitBinder` method either locally within a controller or globally through an
|
||||
`@ControllerAdvice`. Turning this flag on ensures that only constructor binding is used
|
||||
and that property binding is not used unless `allowedFields` patterns are configured.
|
||||
For example:
|
||||
|
||||
[WARNING]
|
||||
====
|
||||
It is extremely important to properly configure allowed and disallowed field patterns
|
||||
when exposing your domain model directly for data binding purposes. Otherwise, it is a
|
||||
big security risk.
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Controller
|
||||
public class MyController {
|
||||
|
||||
Furthermore, it is strongly recommended that you do **not** use types from your domain
|
||||
model such as JPA or Hibernate entities as the model object in data binding scenarios.
|
||||
====
|
||||
@InitBinder
|
||||
void initBinder(WebDataBinder binder) {
|
||||
binder.setDeclarativeBinding(true);
|
||||
}
|
||||
|
||||
// @RequestMapping methods, etc.
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.integration.observability.httpserver.reactive;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class HttpHandlerConfiguration {
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
public HttpHandlerConfiguration(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public HttpHandler httpHandler(ObservationRegistry registry) {
|
||||
return WebHttpHandlerBuilder.applicationContext(this.applicationContext)
|
||||
.observationRegistry(registry)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -17,9 +17,9 @@
|
||||
package org.springframework.docs.integration.observability.httpserver.reactive;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.server.reactive.observation.ServerRequestObservationContext;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.filter.reactive.ServerHttpObservationFilter;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
@Controller
|
||||
@@ -28,7 +28,7 @@ public class UserController {
|
||||
@ExceptionHandler(MissingUserException.class)
|
||||
ResponseEntity<Void> handleMissingUser(ServerWebExchange exchange, MissingUserException exception) {
|
||||
// We want to record this exception with the observation
|
||||
ServerHttpObservationFilter.findObservationContext(exchange)
|
||||
ServerRequestObservationContext.findCurrent(exchange.getAttributes())
|
||||
.ifPresent(context -> context.setError(exception));
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.integration.observability.jms.process;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import jakarta.jms.ConnectionFactory;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.jms.annotation.EnableJms;
|
||||
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
|
||||
|
||||
@Configuration
|
||||
@EnableJms
|
||||
public class JmsConfiguration {
|
||||
|
||||
@Bean
|
||||
public DefaultJmsListenerContainerFactory jmsListenerContainerFactory(ConnectionFactory connectionFactory, ObservationRegistry observationRegistry) {
|
||||
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
|
||||
factory.setConnectionFactory(connectionFactory);
|
||||
factory.setObservationRegistry(observationRegistry);
|
||||
return factory;
|
||||
}
|
||||
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.integration.observability.jms.publish;
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
import jakarta.jms.ConnectionFactory;
|
||||
|
||||
import org.springframework.jms.core.JmsMessagingTemplate;
|
||||
import org.springframework.jms.core.JmsTemplate;
|
||||
|
||||
public class JmsTemplatePublish {
|
||||
|
||||
private final JmsTemplate jmsTemplate;
|
||||
|
||||
private final JmsMessagingTemplate jmsMessagingTemplate;
|
||||
|
||||
public JmsTemplatePublish(ObservationRegistry observationRegistry, ConnectionFactory connectionFactory) {
|
||||
this.jmsTemplate = new JmsTemplate(connectionFactory);
|
||||
// configure the observation registry
|
||||
this.jmsTemplate.setObservationRegistry(observationRegistry);
|
||||
|
||||
// For JmsMessagingTemplate, instantiate it with a JMS template that has a configured registry
|
||||
this.jmsMessagingTemplate = new JmsMessagingTemplate(this.jmsTemplate);
|
||||
}
|
||||
|
||||
public void sendMessages() {
|
||||
this.jmsTemplate.convertAndSend("spring.observation.test", "test message");
|
||||
}
|
||||
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.integration.observability.tasksscheduled;
|
||||
|
||||
|
||||
import io.micrometer.observation.ObservationRegistry;
|
||||
|
||||
import org.springframework.scheduling.annotation.SchedulingConfigurer;
|
||||
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
|
||||
|
||||
public class ObservationSchedulingConfigurer implements SchedulingConfigurer {
|
||||
|
||||
private final ObservationRegistry observationRegistry;
|
||||
|
||||
public ObservationSchedulingConfigurer(ObservationRegistry observationRegistry) {
|
||||
this.observationRegistry = observationRegistry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
|
||||
taskRegistrar.setObservationRegistry(this.observationRegistry);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,18 +7,18 @@ javaPlatform {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.14.3"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.10.9"))
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.15.2"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.12.0-M2"))
|
||||
api(platform("io.netty:netty-bom:4.1.96.Final"))
|
||||
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
|
||||
api(platform("io.projectreactor:reactor-bom:2022.0.9"))
|
||||
api(platform("io.projectreactor:reactor-bom:2023.0.0-M2"))
|
||||
api(platform("io.rsocket:rsocket-bom:1.1.3"))
|
||||
api(platform("org.apache.groovy:groovy-bom:4.0.13"))
|
||||
api(platform("org.apache.logging.log4j:log4j-bom:2.20.0"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:11.0.15"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.4.0"))
|
||||
api(platform("org.junit:junit-bom:5.9.3"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.2"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.5.1"))
|
||||
api(platform("org.junit:junit-bom:5.10.0"))
|
||||
api(platform("org.mockito:mockito-bom:5.4.0"))
|
||||
|
||||
constraints {
|
||||
@@ -45,7 +45,7 @@ dependencies {
|
||||
api("com.thoughtworks.xstream:xstream:1.4.20")
|
||||
api("commons-io:commons-io:2.11.0")
|
||||
api("de.bechte.junit:junit-hierarchicalcontextrunner:4.12.2")
|
||||
api("io.micrometer:context-propagation:1.0.4")
|
||||
api("io.micrometer:context-propagation:1.1.0-M3")
|
||||
api("io.mockk:mockk:1.13.4")
|
||||
api("io.projectreactor.netty:reactor-netty5-http:2.0.0-M3")
|
||||
api("io.projectreactor.tools:blockhound:1.0.8.RELEASE")
|
||||
@@ -92,6 +92,8 @@ dependencies {
|
||||
api("org.apache.activemq:activemq-broker:5.17.4")
|
||||
api("org.apache.activemq:activemq-kahadb-store:5.17.4")
|
||||
api("org.apache.activemq:activemq-stomp:5.17.4")
|
||||
api("org.apache.activemq:artemis-junit-5:2.29.0")
|
||||
api("org.apache.activemq:artemis-jakarta-client:2.29.0")
|
||||
api("org.apache.commons:commons-pool2:2.9.0")
|
||||
api("org.apache.derby:derby:10.16.1.1")
|
||||
api("org.apache.derby:derbyclient:10.16.1.1")
|
||||
@@ -110,6 +112,7 @@ dependencies {
|
||||
api("org.awaitility:awaitility:4.2.0")
|
||||
api("org.bouncycastle:bcpkix-jdk18on:1.72")
|
||||
api("org.codehaus.jettison:jettison:1.5.4")
|
||||
api("org.crac:crac:1.3.0")
|
||||
api("org.dom4j:dom4j:2.1.4")
|
||||
api("org.eclipse.jetty:jetty-reactive-httpclient:3.0.8")
|
||||
api("org.eclipse.persistence:org.eclipse.persistence.jpa:3.0.3")
|
||||
@@ -143,6 +146,6 @@ dependencies {
|
||||
api("org.webjars:webjars-locator-core:0.53")
|
||||
api("org.xmlunit:xmlunit-assertj:2.9.1")
|
||||
api("org.xmlunit:xmlunit-matchers:2.9.1")
|
||||
api("org.yaml:snakeyaml:1.33")
|
||||
api("org.yaml:snakeyaml:2.0")
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -1,9 +1,10 @@
|
||||
version=6.0.12-SNAPSHOT
|
||||
version=6.1.0-M4
|
||||
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
org.gradle.parallel=true
|
||||
|
||||
kotlinVersion=1.7.21
|
||||
kotlinVersion=1.9.0
|
||||
|
||||
kotlin.jvm.target.validation.mode=ignore
|
||||
kotlin.stdlib.default.dependency=false
|
||||
|
||||
+14
-65
@@ -2,16 +2,14 @@
|
||||
* Apply the JVM Toolchain conventions
|
||||
* See https://docs.gradle.org/current/userguide/toolchains.html
|
||||
*
|
||||
* One can choose the toolchain to use for compiling the MAIN sources and/or compiling
|
||||
* and running the TEST sources. These options apply to Java, Kotlin and Groovy sources
|
||||
* when available.
|
||||
* {@code "./gradlew check -PmainToolchain=17 -PtestToolchain=20"} will use:
|
||||
* <ul>
|
||||
* <li>a JDK17 toolchain for compiling the main SourceSet
|
||||
* <li>a JDK20 toolchain for compiling and running the test SourceSet
|
||||
* </ul>
|
||||
* One can choose the toolchain to use for compiling and running the TEST sources.
|
||||
* These options apply to Java, Kotlin and Groovy test sources when available.
|
||||
* {@code "./gradlew check -PtestToolchain=22"} will use a JDK22
|
||||
* toolchain for compiling and running the test SourceSet.
|
||||
*
|
||||
* By default, the build will fall back to using the current JDK and 17 language level for all sourceSets.
|
||||
* By default, the main build will fall back to using the a JDK 17
|
||||
* toolchain (and 17 language level) for all main sourceSets.
|
||||
* See {@link org.springframework.build.JavaConventions}.
|
||||
*
|
||||
* Gradle will automatically detect JDK distributions in well-known locations.
|
||||
* The following command will list the detected JDKs on the host.
|
||||
@@ -23,52 +21,27 @@
|
||||
* {@code
|
||||
* $ echo JDK17
|
||||
* /opt/openjdk/java17
|
||||
* $ echo JDK20
|
||||
* /opt/openjdk/java20
|
||||
* $ ./gradlew -Porg.gradle.java.installations.fromEnv=JDK17,JDK20 check
|
||||
* $ echo JDK22
|
||||
* /opt/openjdk/java22
|
||||
* $ ./gradlew -Porg.gradle.java.installations.fromEnv=JDK17,JDK22 check
|
||||
* }
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
|
||||
def mainToolchainConfigured() {
|
||||
return project.hasProperty('mainToolchain') && project.mainToolchain
|
||||
}
|
||||
|
||||
def testToolchainConfigured() {
|
||||
return project.hasProperty('testToolchain') && project.testToolchain
|
||||
}
|
||||
|
||||
def mainToolchainLanguageVersion() {
|
||||
if (mainToolchainConfigured()) {
|
||||
return JavaLanguageVersion.of(project.mainToolchain.toString())
|
||||
}
|
||||
return JavaLanguageVersion.of(17)
|
||||
}
|
||||
|
||||
def testToolchainLanguageVersion() {
|
||||
if (testToolchainConfigured()) {
|
||||
return JavaLanguageVersion.of(project.testToolchain.toString())
|
||||
}
|
||||
return mainToolchainLanguageVersion()
|
||||
return JavaLanguageVersion.of(17)
|
||||
}
|
||||
|
||||
plugins.withType(JavaPlugin) {
|
||||
// Configure the Java Toolchain if the 'mainToolchain' is configured
|
||||
if (mainToolchainConfigured()) {
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = mainToolchainLanguageVersion()
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Fallback to JDK17
|
||||
java {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
plugins.withType(JavaPlugin).configureEach {
|
||||
// Configure a specific Java Toolchain for compiling and running tests if the 'testToolchain' property is defined
|
||||
if (testToolchainConfigured()) {
|
||||
def testLanguageVersion = testToolchainLanguageVersion()
|
||||
@@ -81,37 +54,13 @@ plugins.withType(JavaPlugin) {
|
||||
javaLauncher = javaToolchains.launcherFor {
|
||||
languageVersion = testLanguageVersion
|
||||
}
|
||||
jvmArgs += ['-Djava.locale.providers=COMPAT']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plugins.withType(GroovyPlugin) {
|
||||
// Fallback to JDK17
|
||||
if (!mainToolchainConfigured()) {
|
||||
compileGroovy {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pluginManager.withPlugin("kotlin") {
|
||||
// Fallback to JDK17
|
||||
compileKotlin {
|
||||
kotlinOptions {
|
||||
jvmTarget = '17'
|
||||
}
|
||||
}
|
||||
compileTestKotlin {
|
||||
kotlinOptions {
|
||||
jvmTarget = '17'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Configure the JMH plugin to use the toolchain for generating and running JMH bytecode
|
||||
pluginManager.withPlugin("me.champeau.jmh") {
|
||||
if (mainToolchainConfigured() || testToolchainConfigured()) {
|
||||
if (testToolchainConfigured()) {
|
||||
tasks.matching { it.name.contains('jmh') && it.hasProperty('javaLauncher') }.configureEach {
|
||||
javaLauncher.set(javaToolchains.launcherFor {
|
||||
languageVersion.set(testToolchainLanguageVersion())
|
||||
@@ -131,7 +80,7 @@ rootProject.ext {
|
||||
resolvedTestToolchain = false
|
||||
}
|
||||
gradle.taskGraph.afterTask { Task task, TaskState state ->
|
||||
if (mainToolchainConfigured() && !resolvedMainToolchain && task instanceof JavaCompile && task.javaCompiler.isPresent()) {
|
||||
if (!resolvedMainToolchain && task instanceof JavaCompile && task.javaCompiler.isPresent()) {
|
||||
def metadata = task.javaCompiler.get().metadata
|
||||
task.project.buildScan.value('Main toolchain', "$metadata.vendor $metadata.languageVersion ($metadata.installationPath)")
|
||||
resolvedMainToolchain = true
|
||||
|
||||
+2
-2
@@ -25,8 +25,8 @@ import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* AspectJPointcutAdvisor that adapts an {@link AbstractAspectJAdvice}
|
||||
* to the {@link org.springframework.aop.PointcutAdvisor} interface.
|
||||
* {@code AspectJPointcutAdvisor} adapts an {@link AbstractAspectJAdvice} to the
|
||||
* {@link PointcutAdvisor} interface.
|
||||
*
|
||||
* @author Adrian Colyer
|
||||
* @author Juergen Hoeller
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -291,7 +291,7 @@ public class MethodInvocationProceedingJoinPoint implements ProceedingJoinPoint,
|
||||
|
||||
private void appendType(StringBuilder sb, Class<?> type, boolean useLongTypeName) {
|
||||
if (type.isArray()) {
|
||||
appendType(sb, type.getComponentType(), useLongTypeName);
|
||||
appendType(sb, type.componentType(), useLongTypeName);
|
||||
sb.append("[]");
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -266,7 +266,7 @@ public abstract class AopProxyUtils {
|
||||
if (varargArray instanceof Object[] && !varargType.isInstance(varargArray)) {
|
||||
Object[] newArguments = new Object[arguments.length];
|
||||
System.arraycopy(arguments, 0, newArguments, 0, varargIndex);
|
||||
Class<?> targetElementType = varargType.getComponentType();
|
||||
Class<?> targetElementType = varargType.componentType();
|
||||
int varargLength = Array.getLength(varargArray);
|
||||
Object newVarargArray = Array.newInstance(targetElementType, varargLength);
|
||||
System.arraycopy(varargArray, 0, newVarargArray, 0, varargLength);
|
||||
|
||||
+34
-10
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -27,6 +27,7 @@ import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.aop.AfterAdvice;
|
||||
import org.springframework.aop.framework.AopConfigException;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -78,21 +79,44 @@ public class ThrowsAdviceInterceptor implements MethodInterceptor, AfterAdvice {
|
||||
|
||||
Method[] methods = throwsAdvice.getClass().getMethods();
|
||||
for (Method method : methods) {
|
||||
if (method.getName().equals(AFTER_THROWING) &&
|
||||
(method.getParameterCount() == 1 || method.getParameterCount() == 4)) {
|
||||
Class<?> throwableParam = method.getParameterTypes()[method.getParameterCount() - 1];
|
||||
if (Throwable.class.isAssignableFrom(throwableParam)) {
|
||||
// An exception handler to register...
|
||||
this.exceptionHandlerMap.put(throwableParam, method);
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Found exception handler method on throws advice: " + method);
|
||||
if (method.getName().equals(AFTER_THROWING)) {
|
||||
Class<?> throwableParam = null;
|
||||
if (method.getParameterCount() == 1) {
|
||||
// just a Throwable parameter
|
||||
throwableParam = method.getParameterTypes()[0];
|
||||
if (!Throwable.class.isAssignableFrom(throwableParam)) {
|
||||
throw new AopConfigException("Invalid afterThrowing signature: " +
|
||||
"single argument must be a Throwable subclass");
|
||||
}
|
||||
}
|
||||
else if (method.getParameterCount() == 4) {
|
||||
// Method, Object[], target, throwable
|
||||
Class<?>[] paramTypes = method.getParameterTypes();
|
||||
if (!Method.class.equals(paramTypes[0]) || !Object[].class.equals(paramTypes[1]) ||
|
||||
Throwable.class.equals(paramTypes[2]) || !Throwable.class.isAssignableFrom(paramTypes[3])) {
|
||||
throw new AopConfigException("Invalid afterThrowing signature: " +
|
||||
"four arguments must be Method, Object[], target, throwable: " + method);
|
||||
}
|
||||
throwableParam = paramTypes[3];
|
||||
}
|
||||
if (throwableParam == null) {
|
||||
throw new AopConfigException("Unsupported afterThrowing signature: single throwable argument " +
|
||||
"or four arguments Method, Object[], target, throwable expected: " + method);
|
||||
}
|
||||
// An exception handler to register...
|
||||
Method existingMethod = this.exceptionHandlerMap.put(throwableParam, method);
|
||||
if (existingMethod != null) {
|
||||
throw new AopConfigException("Only one afterThrowing method per specific Throwable subclass " +
|
||||
"allowed: " + method + " / " + existingMethod);
|
||||
}
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Found exception handler method on throws advice: " + method);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.exceptionHandlerMap.isEmpty()) {
|
||||
throw new IllegalArgumentException(
|
||||
throw new AopConfigException(
|
||||
"At least one handler method must be found in class [" + throwsAdvice.getClass() + "]");
|
||||
}
|
||||
}
|
||||
|
||||
+39
-37
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -23,7 +23,6 @@ import java.util.regex.Pattern;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.apache.commons.logging.Log;
|
||||
|
||||
import org.springframework.core.Constants;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
@@ -62,6 +61,7 @@ import org.springframework.util.StringUtils;
|
||||
*
|
||||
* @author Rob Harrop
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 1.2
|
||||
* @see #setEnterMessage
|
||||
* @see #setExitMessage
|
||||
@@ -152,8 +152,15 @@ public class CustomizableTraceInterceptor extends AbstractTraceInterceptor {
|
||||
/**
|
||||
* The {@code Set} of allowed placeholders.
|
||||
*/
|
||||
private static final Set<Object> ALLOWED_PLACEHOLDERS =
|
||||
new Constants(CustomizableTraceInterceptor.class).getValues("PLACEHOLDER_");
|
||||
static final Set<String> ALLOWED_PLACEHOLDERS = Set.of(
|
||||
PLACEHOLDER_METHOD_NAME,
|
||||
PLACEHOLDER_TARGET_CLASS_NAME,
|
||||
PLACEHOLDER_TARGET_CLASS_SHORT_NAME,
|
||||
PLACEHOLDER_RETURN_VALUE,
|
||||
PLACEHOLDER_ARGUMENT_TYPES,
|
||||
PLACEHOLDER_ARGUMENTS,
|
||||
PLACEHOLDER_EXCEPTION,
|
||||
PLACEHOLDER_INVOCATION_TIME);
|
||||
|
||||
|
||||
/**
|
||||
@@ -295,43 +302,38 @@ public class CustomizableTraceInterceptor extends AbstractTraceInterceptor {
|
||||
protected String replacePlaceholders(String message, MethodInvocation methodInvocation,
|
||||
@Nullable Object returnValue, @Nullable Throwable throwable, long invocationTime) {
|
||||
|
||||
Matcher matcher = PATTERN.matcher(message);
|
||||
Object target = methodInvocation.getThis();
|
||||
Assert.state(target != null, "Target must not be null");
|
||||
|
||||
StringBuilder output = new StringBuilder();
|
||||
Matcher matcher = PATTERN.matcher(message);
|
||||
while (matcher.find()) {
|
||||
String match = matcher.group();
|
||||
if (PLACEHOLDER_METHOD_NAME.equals(match)) {
|
||||
matcher.appendReplacement(output, Matcher.quoteReplacement(methodInvocation.getMethod().getName()));
|
||||
}
|
||||
else if (PLACEHOLDER_TARGET_CLASS_NAME.equals(match)) {
|
||||
String className = getClassForLogging(target).getName();
|
||||
matcher.appendReplacement(output, Matcher.quoteReplacement(className));
|
||||
}
|
||||
else if (PLACEHOLDER_TARGET_CLASS_SHORT_NAME.equals(match)) {
|
||||
String shortName = ClassUtils.getShortName(getClassForLogging(target));
|
||||
matcher.appendReplacement(output, Matcher.quoteReplacement(shortName));
|
||||
}
|
||||
else if (PLACEHOLDER_ARGUMENTS.equals(match)) {
|
||||
matcher.appendReplacement(output,
|
||||
switch (match) {
|
||||
case PLACEHOLDER_METHOD_NAME -> matcher.appendReplacement(output,
|
||||
Matcher.quoteReplacement(methodInvocation.getMethod().getName()));
|
||||
case PLACEHOLDER_TARGET_CLASS_NAME -> {
|
||||
String className = getClassForLogging(target).getName();
|
||||
matcher.appendReplacement(output, Matcher.quoteReplacement(className));
|
||||
}
|
||||
case PLACEHOLDER_TARGET_CLASS_SHORT_NAME -> {
|
||||
String shortName = ClassUtils.getShortName(getClassForLogging(target));
|
||||
matcher.appendReplacement(output, Matcher.quoteReplacement(shortName));
|
||||
}
|
||||
case PLACEHOLDER_ARGUMENTS -> matcher.appendReplacement(output,
|
||||
Matcher.quoteReplacement(StringUtils.arrayToCommaDelimitedString(methodInvocation.getArguments())));
|
||||
}
|
||||
else if (PLACEHOLDER_ARGUMENT_TYPES.equals(match)) {
|
||||
appendArgumentTypes(methodInvocation, matcher, output);
|
||||
}
|
||||
else if (PLACEHOLDER_RETURN_VALUE.equals(match)) {
|
||||
appendReturnValue(methodInvocation, matcher, output, returnValue);
|
||||
}
|
||||
else if (throwable != null && PLACEHOLDER_EXCEPTION.equals(match)) {
|
||||
matcher.appendReplacement(output, Matcher.quoteReplacement(throwable.toString()));
|
||||
}
|
||||
else if (PLACEHOLDER_INVOCATION_TIME.equals(match)) {
|
||||
matcher.appendReplacement(output, Long.toString(invocationTime));
|
||||
}
|
||||
else {
|
||||
// Should not happen since placeholders are checked earlier.
|
||||
throw new IllegalArgumentException("Unknown placeholder [" + match + "]");
|
||||
case PLACEHOLDER_ARGUMENT_TYPES -> appendArgumentTypes(methodInvocation, matcher, output);
|
||||
case PLACEHOLDER_RETURN_VALUE -> appendReturnValue(methodInvocation, matcher, output, returnValue);
|
||||
case PLACEHOLDER_EXCEPTION -> {
|
||||
if (throwable != null) {
|
||||
matcher.appendReplacement(output, Matcher.quoteReplacement(throwable.toString()));
|
||||
}
|
||||
}
|
||||
case PLACEHOLDER_INVOCATION_TIME -> matcher.appendReplacement(output, Long.toString(invocationTime));
|
||||
default -> {
|
||||
// Should not happen since placeholders are checked earlier.
|
||||
throw new IllegalArgumentException("Unknown placeholder [" + match + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
matcher.appendTail(output);
|
||||
@@ -348,7 +350,7 @@ public class CustomizableTraceInterceptor extends AbstractTraceInterceptor {
|
||||
* @param output the {@code StringBuilder} to write output to
|
||||
* @param returnValue the value returned by the method invocation.
|
||||
*/
|
||||
private void appendReturnValue(
|
||||
private static void appendReturnValue(
|
||||
MethodInvocation methodInvocation, Matcher matcher, StringBuilder output, @Nullable Object returnValue) {
|
||||
|
||||
if (methodInvocation.getMethod().getReturnType() == void.class) {
|
||||
@@ -372,7 +374,7 @@ public class CustomizableTraceInterceptor extends AbstractTraceInterceptor {
|
||||
* @param matcher the {@code Matcher} containing the state of the output
|
||||
* @param output the {@code StringBuilder} containing the output
|
||||
*/
|
||||
private void appendArgumentTypes(MethodInvocation methodInvocation, Matcher matcher, StringBuilder output) {
|
||||
private static void appendArgumentTypes(MethodInvocation methodInvocation, Matcher matcher, StringBuilder output) {
|
||||
Class<?>[] argumentTypes = methodInvocation.getMethod().getParameterTypes();
|
||||
String[] argumentTypeShortNames = new String[argumentTypes.length];
|
||||
for (int i = 0; i < argumentTypeShortNames.length; i++) {
|
||||
@@ -387,7 +389,7 @@ public class CustomizableTraceInterceptor extends AbstractTraceInterceptor {
|
||||
* that are not specified as constants on this class and throws an
|
||||
* {@code IllegalArgumentException} if so.
|
||||
*/
|
||||
private void checkForInvalidPlaceholders(String message) throws IllegalArgumentException {
|
||||
private static void checkForInvalidPlaceholders(String message) throws IllegalArgumentException {
|
||||
Matcher matcher = PATTERN.matcher(message);
|
||||
while (matcher.find()) {
|
||||
String match = matcher.group();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
||||
+3
-2
@@ -23,25 +23,26 @@ import java.rmi.RemoteException;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.aop.framework.AopConfigException;
|
||||
import org.springframework.aop.testfixture.advice.MyThrowsHandler;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatException;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* @author Rod Johnson
|
||||
* @author Chris Beams
|
||||
* @author Juergen Hoeller
|
||||
*/
|
||||
public class ThrowsAdviceInterceptorTests {
|
||||
|
||||
@Test
|
||||
public void testNoHandlerMethods() {
|
||||
// should require one handler method at least
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
assertThatExceptionOfType(AopConfigException.class).isThrownBy(() ->
|
||||
new ThrowsAdviceInterceptor(new Object()));
|
||||
}
|
||||
|
||||
|
||||
+82
-41
@@ -16,10 +16,17 @@
|
||||
|
||||
package org.springframework.aop.interceptor;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
@@ -27,73 +34,80 @@ import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.springframework.aop.interceptor.CustomizableTraceInterceptor.ALLOWED_PLACEHOLDERS;
|
||||
import static org.springframework.aop.interceptor.CustomizableTraceInterceptor.PLACEHOLDER_ARGUMENTS;
|
||||
import static org.springframework.aop.interceptor.CustomizableTraceInterceptor.PLACEHOLDER_ARGUMENT_TYPES;
|
||||
import static org.springframework.aop.interceptor.CustomizableTraceInterceptor.PLACEHOLDER_EXCEPTION;
|
||||
import static org.springframework.aop.interceptor.CustomizableTraceInterceptor.PLACEHOLDER_INVOCATION_TIME;
|
||||
import static org.springframework.aop.interceptor.CustomizableTraceInterceptor.PLACEHOLDER_METHOD_NAME;
|
||||
import static org.springframework.aop.interceptor.CustomizableTraceInterceptor.PLACEHOLDER_RETURN_VALUE;
|
||||
import static org.springframework.aop.interceptor.CustomizableTraceInterceptor.PLACEHOLDER_TARGET_CLASS_NAME;
|
||||
import static org.springframework.aop.interceptor.CustomizableTraceInterceptor.PLACEHOLDER_TARGET_CLASS_SHORT_NAME;
|
||||
|
||||
/**
|
||||
* Tests for {@link CustomizableTraceInterceptor}.
|
||||
*
|
||||
* @author Rob Harrop
|
||||
* @author Rick Evans
|
||||
* @author Juergen Hoeller
|
||||
* @author Chris Beams
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
public class CustomizableTraceInterceptorTests {
|
||||
class CustomizableTraceInterceptorTests {
|
||||
|
||||
private final CustomizableTraceInterceptor interceptor = new CustomizableTraceInterceptor();
|
||||
|
||||
|
||||
@Test
|
||||
public void testSetEmptyEnterMessage() {
|
||||
void setEmptyEnterMessage() {
|
||||
// Must not be able to set empty enter message
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
new CustomizableTraceInterceptor().setEnterMessage(""));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> interceptor.setEnterMessage(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetEnterMessageWithReturnValuePlaceholder() {
|
||||
void setEnterMessageWithReturnValuePlaceholder() {
|
||||
// Must not be able to set enter message with return value placeholder
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
new CustomizableTraceInterceptor().setEnterMessage(CustomizableTraceInterceptor.PLACEHOLDER_RETURN_VALUE));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> interceptor.setEnterMessage(PLACEHOLDER_RETURN_VALUE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetEnterMessageWithExceptionPlaceholder() {
|
||||
void setEnterMessageWithExceptionPlaceholder() {
|
||||
// Must not be able to set enter message with exception placeholder
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
new CustomizableTraceInterceptor().setEnterMessage(CustomizableTraceInterceptor.PLACEHOLDER_EXCEPTION));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> interceptor.setEnterMessage(PLACEHOLDER_EXCEPTION));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetEnterMessageWithInvocationTimePlaceholder() {
|
||||
void setEnterMessageWithInvocationTimePlaceholder() {
|
||||
// Must not be able to set enter message with invocation time placeholder
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
new CustomizableTraceInterceptor().setEnterMessage(CustomizableTraceInterceptor.PLACEHOLDER_INVOCATION_TIME));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> interceptor.setEnterMessage(PLACEHOLDER_INVOCATION_TIME));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetEmptyExitMessage() {
|
||||
void setEmptyExitMessage() {
|
||||
// Must not be able to set empty exit message
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
new CustomizableTraceInterceptor().setExitMessage(""));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> interceptor.setExitMessage(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetExitMessageWithExceptionPlaceholder() {
|
||||
void setExitMessageWithExceptionPlaceholder() {
|
||||
// Must not be able to set exit message with exception placeholder
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
new CustomizableTraceInterceptor().setExitMessage(CustomizableTraceInterceptor.PLACEHOLDER_EXCEPTION));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> interceptor.setExitMessage(PLACEHOLDER_EXCEPTION));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetEmptyExceptionMessage() {
|
||||
void setEmptyExceptionMessage() {
|
||||
// Must not be able to set empty exception message
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
new CustomizableTraceInterceptor().setExceptionMessage(""));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> interceptor.setExceptionMessage(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetExceptionMethodWithReturnValuePlaceholder() {
|
||||
void setExceptionMethodWithReturnValuePlaceholder() {
|
||||
// Must not be able to set exception message with return value placeholder
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
new CustomizableTraceInterceptor().setExceptionMessage(CustomizableTraceInterceptor.PLACEHOLDER_RETURN_VALUE));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> interceptor.setExceptionMessage(PLACEHOLDER_RETURN_VALUE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSunnyDayPathLogsCorrectly() throws Throwable {
|
||||
void sunnyDayPathLogsCorrectly() throws Throwable {
|
||||
MethodInvocation methodInvocation = mock();
|
||||
given(methodInvocation.getMethod()).willReturn(String.class.getMethod("toString"));
|
||||
given(methodInvocation.getThis()).willReturn(this);
|
||||
@@ -108,7 +122,7 @@ public class CustomizableTraceInterceptorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExceptionPathLogsCorrectly() throws Throwable {
|
||||
void exceptionPathLogsCorrectly() throws Throwable {
|
||||
MethodInvocation methodInvocation = mock();
|
||||
|
||||
IllegalArgumentException exception = new IllegalArgumentException();
|
||||
@@ -120,15 +134,14 @@ public class CustomizableTraceInterceptorTests {
|
||||
given(log.isTraceEnabled()).willReturn(true);
|
||||
|
||||
CustomizableTraceInterceptor interceptor = new StubCustomizableTraceInterceptor(log);
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
interceptor.invoke(methodInvocation));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> interceptor.invoke(methodInvocation));
|
||||
|
||||
verify(log).trace(anyString());
|
||||
verify(log).trace(anyString(), eq(exception));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSunnyDayPathLogsCorrectlyWithPrettyMuchAllPlaceholdersMatching() throws Throwable {
|
||||
void sunnyDayPathLogsCorrectlyWithPrettyMuchAllPlaceholdersMatching() throws Throwable {
|
||||
MethodInvocation methodInvocation = mock();
|
||||
|
||||
given(methodInvocation.getMethod()).willReturn(String.class.getMethod("toString", new Class[0]));
|
||||
@@ -141,31 +154,59 @@ public class CustomizableTraceInterceptorTests {
|
||||
|
||||
CustomizableTraceInterceptor interceptor = new StubCustomizableTraceInterceptor(log);
|
||||
interceptor.setEnterMessage(new StringBuilder()
|
||||
.append("Entering the '").append(CustomizableTraceInterceptor.PLACEHOLDER_METHOD_NAME)
|
||||
.append("' method of the [").append(CustomizableTraceInterceptor.PLACEHOLDER_TARGET_CLASS_NAME)
|
||||
.append("] class with the following args (").append(CustomizableTraceInterceptor.PLACEHOLDER_ARGUMENTS)
|
||||
.append(") and arg types (").append(CustomizableTraceInterceptor.PLACEHOLDER_ARGUMENT_TYPES)
|
||||
.append("Entering the '").append(PLACEHOLDER_METHOD_NAME)
|
||||
.append("' method of the [").append(PLACEHOLDER_TARGET_CLASS_NAME)
|
||||
.append("] class with the following args (").append(PLACEHOLDER_ARGUMENTS)
|
||||
.append(") and arg types (").append(PLACEHOLDER_ARGUMENT_TYPES)
|
||||
.append(").").toString());
|
||||
interceptor.setExitMessage(new StringBuilder()
|
||||
.append("Exiting the '").append(CustomizableTraceInterceptor.PLACEHOLDER_METHOD_NAME)
|
||||
.append("' method of the [").append(CustomizableTraceInterceptor.PLACEHOLDER_TARGET_CLASS_SHORT_NAME)
|
||||
.append("] class with the following args (").append(CustomizableTraceInterceptor.PLACEHOLDER_ARGUMENTS)
|
||||
.append(") and arg types (").append(CustomizableTraceInterceptor.PLACEHOLDER_ARGUMENT_TYPES)
|
||||
.append("), returning '").append(CustomizableTraceInterceptor.PLACEHOLDER_RETURN_VALUE)
|
||||
.append("' and taking '").append(CustomizableTraceInterceptor.PLACEHOLDER_INVOCATION_TIME)
|
||||
.append("Exiting the '").append(PLACEHOLDER_METHOD_NAME)
|
||||
.append("' method of the [").append(PLACEHOLDER_TARGET_CLASS_SHORT_NAME)
|
||||
.append("] class with the following args (").append(PLACEHOLDER_ARGUMENTS)
|
||||
.append(") and arg types (").append(PLACEHOLDER_ARGUMENT_TYPES)
|
||||
.append("), returning '").append(PLACEHOLDER_RETURN_VALUE)
|
||||
.append("' and taking '").append(PLACEHOLDER_INVOCATION_TIME)
|
||||
.append("' this long.").toString());
|
||||
interceptor.invoke(methodInvocation);
|
||||
|
||||
verify(log, times(2)).trace(anyString());
|
||||
}
|
||||
|
||||
/**
|
||||
* This test effectively verifies that the internal ALLOWED_PLACEHOLDERS set
|
||||
* is properly configured in {@link CustomizableTraceInterceptor}.
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
void supportedPlaceholderValues() {
|
||||
assertThat(ALLOWED_PLACEHOLDERS).containsExactlyInAnyOrderElementsOf(getPlaceholderConstantValues());
|
||||
}
|
||||
|
||||
private List<String> getPlaceholderConstantValues() {
|
||||
return Arrays.stream(CustomizableTraceInterceptor.class.getFields())
|
||||
.filter(ReflectionUtils::isPublicStaticFinal)
|
||||
.filter(field -> field.getName().startsWith("PLACEHOLDER_"))
|
||||
.map(this::getFieldValue)
|
||||
.map(String.class::cast)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private Object getFieldValue(Field field) {
|
||||
try {
|
||||
return field.get(null);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private static class StubCustomizableTraceInterceptor extends CustomizableTraceInterceptor {
|
||||
|
||||
private final Log log;
|
||||
|
||||
public StubCustomizableTraceInterceptor(Log log) {
|
||||
StubCustomizableTraceInterceptor(Log log) {
|
||||
super.setUseDynamicLogger(false);
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
+1
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -34,9 +34,4 @@ public class MyThrowsHandler extends MethodCounter implements ThrowsAdvice {
|
||||
count("remoteException");
|
||||
}
|
||||
|
||||
/** Not valid, wrong number of arguments */
|
||||
public void afterThrowing(Method m, Exception ex) throws Throwable {
|
||||
throw new UnsupportedOperationException("Shouldn't be called");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ dependencies {
|
||||
optional("org.apache.groovy:groovy-xml")
|
||||
optional("org.jetbrains.kotlin:kotlin-reflect")
|
||||
optional("org.jetbrains.kotlin:kotlin-stdlib")
|
||||
optional("org.reactivestreams:reactive-streams")
|
||||
optional("org.yaml:snakeyaml")
|
||||
testFixturesApi("org.junit.jupiter:junit-jupiter-api")
|
||||
testFixturesImplementation("com.google.code.findbugs:jsr305")
|
||||
|
||||
+42
-32
@@ -30,7 +30,6 @@ import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -292,7 +291,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
String lastKey = tokens.keys[tokens.keys.length - 1];
|
||||
|
||||
if (propValue.getClass().isArray()) {
|
||||
Class<?> requiredType = propValue.getClass().getComponentType();
|
||||
Class<?> requiredType = propValue.getClass().componentType();
|
||||
int arrayIndex = Integer.parseInt(lastKey);
|
||||
Object oldValue = null;
|
||||
try {
|
||||
@@ -303,7 +302,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
requiredType, ph.nested(tokens.keys.length));
|
||||
int length = Array.getLength(propValue);
|
||||
if (arrayIndex >= length && arrayIndex < this.autoGrowCollectionLimit) {
|
||||
Class<?> componentType = propValue.getClass().getComponentType();
|
||||
Class<?> componentType = propValue.getClass().componentType();
|
||||
Object newArray = Array.newInstance(componentType, arrayIndex + 1);
|
||||
System.arraycopy(propValue, 0, newArray, 0, length);
|
||||
int lastKeyIndex = tokens.canonicalName.lastIndexOf('[');
|
||||
@@ -320,14 +319,14 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
}
|
||||
|
||||
else if (propValue instanceof List list) {
|
||||
Class<?> requiredType = ph.getCollectionType(tokens.keys.length);
|
||||
TypeDescriptor requiredType = ph.getCollectionType(tokens.keys.length);
|
||||
int index = Integer.parseInt(lastKey);
|
||||
Object oldValue = null;
|
||||
if (isExtractOldValueForEditor() && index < list.size()) {
|
||||
oldValue = list.get(index);
|
||||
}
|
||||
Object convertedValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(),
|
||||
requiredType, ph.nested(tokens.keys.length));
|
||||
requiredType.getResolvableType().resolve(), requiredType);
|
||||
int size = list.size();
|
||||
if (index >= size && index < this.autoGrowCollectionLimit) {
|
||||
for (int i = size; i < index; i++) {
|
||||
@@ -355,12 +354,12 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
}
|
||||
|
||||
else if (propValue instanceof Map map) {
|
||||
Class<?> mapKeyType = ph.getMapKeyType(tokens.keys.length);
|
||||
Class<?> mapValueType = ph.getMapValueType(tokens.keys.length);
|
||||
TypeDescriptor mapKeyType = ph.getMapKeyType(tokens.keys.length);
|
||||
TypeDescriptor mapValueType = ph.getMapValueType(tokens.keys.length);
|
||||
// IMPORTANT: Do not pass full property name in here - property editors
|
||||
// must not kick in for map keys but rather only for map values.
|
||||
TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType);
|
||||
Object convertedMapKey = convertIfNecessary(null, null, lastKey, mapKeyType, typeDescriptor);
|
||||
Object convertedMapKey = convertIfNecessary(null, null, lastKey,
|
||||
mapKeyType.getResolvableType().resolve(), mapKeyType);
|
||||
Object oldValue = null;
|
||||
if (isExtractOldValueForEditor()) {
|
||||
oldValue = map.get(convertedMapKey);
|
||||
@@ -368,7 +367,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
// Pass full property name and old value in here, since we want full
|
||||
// conversion ability for map values.
|
||||
Object convertedMapValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(),
|
||||
mapValueType, ph.nested(tokens.keys.length));
|
||||
mapValueType.getResolvableType().resolve(), mapValueType);
|
||||
map.put(convertedMapKey, convertedMapValue);
|
||||
}
|
||||
|
||||
@@ -657,22 +656,32 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
growCollectionIfNecessary(list, index, indexedPropertyName.toString(), ph, i + 1);
|
||||
value = list.get(index);
|
||||
}
|
||||
else if (value instanceof Set set) {
|
||||
// Apply index to Iterator in case of a Set.
|
||||
else if (value instanceof Iterable iterable) {
|
||||
// Apply index to Iterator in case of a Set/Collection/Iterable.
|
||||
int index = Integer.parseInt(key);
|
||||
if (index < 0 || index >= set.size()) {
|
||||
throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
|
||||
"Cannot get element with index " + index + " from Set of size " +
|
||||
set.size() + ", accessed using property path '" + propertyName + "'");
|
||||
if (value instanceof Collection<?> coll) {
|
||||
if (index < 0 || index >= coll.size()) {
|
||||
throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
|
||||
"Cannot get element with index " + index + " from Collection of size " +
|
||||
coll.size() + ", accessed using property path '" + propertyName + "'");
|
||||
}
|
||||
}
|
||||
Iterator<Object> it = set.iterator();
|
||||
for (int j = 0; it.hasNext(); j++) {
|
||||
Iterator<Object> it = iterable.iterator();
|
||||
boolean found = false;
|
||||
int currIndex = 0;
|
||||
for (; it.hasNext(); currIndex++) {
|
||||
Object elem = it.next();
|
||||
if (j == index) {
|
||||
if (currIndex == index) {
|
||||
value = elem;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
|
||||
"Cannot get element with index " + index + " from Iterable of size " +
|
||||
currIndex + ", accessed using property path '" + propertyName + "'");
|
||||
}
|
||||
}
|
||||
else if (value instanceof Map map) {
|
||||
Class<?> mapKeyType = ph.getResolvableType().getNested(i + 1).asMap().resolveGeneric(0);
|
||||
@@ -685,13 +694,17 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
else {
|
||||
throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
|
||||
"Property referenced in indexed property path '" + propertyName +
|
||||
"' is neither an array nor a List nor a Set nor a Map; returned value was [" + value + "]");
|
||||
"' is neither an array nor a List/Set/Collection/Iterable nor a Map; " +
|
||||
"returned value was [" + value + "]");
|
||||
}
|
||||
indexedPropertyName.append(PROPERTY_KEY_PREFIX).append(key).append(PROPERTY_KEY_SUFFIX);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
catch (InvalidPropertyException ex) {
|
||||
throw ex;
|
||||
}
|
||||
catch (IndexOutOfBoundsException ex) {
|
||||
throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
|
||||
"Index of out of bounds in property path '" + propertyName + "'", ex);
|
||||
@@ -756,7 +769,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
}
|
||||
int length = Array.getLength(array);
|
||||
if (index >= length && index < this.autoGrowCollectionLimit) {
|
||||
Class<?> componentType = array.getClass().getComponentType();
|
||||
Class<?> componentType = array.getClass().componentType();
|
||||
Object newArray = Array.newInstance(componentType, index + 1);
|
||||
System.arraycopy(array, 0, newArray, 0, length);
|
||||
for (int i = length; i < Array.getLength(newArray); i++) {
|
||||
@@ -887,11 +900,11 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
private Object newValue(Class<?> type, @Nullable TypeDescriptor desc, String name) {
|
||||
try {
|
||||
if (type.isArray()) {
|
||||
Class<?> componentType = type.getComponentType();
|
||||
Class<?> componentType = type.componentType();
|
||||
// TODO - only handles 2-dimensional arrays
|
||||
if (componentType.isArray()) {
|
||||
Object array = Array.newInstance(componentType, 1);
|
||||
Array.set(array, 0, Array.newInstance(componentType.getComponentType(), 0));
|
||||
Array.set(array, 0, Array.newInstance(componentType.componentType(), 0));
|
||||
return array;
|
||||
}
|
||||
else {
|
||||
@@ -1028,19 +1041,16 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
|
||||
public abstract ResolvableType getResolvableType();
|
||||
|
||||
@Nullable
|
||||
public Class<?> getMapKeyType(int nestingLevel) {
|
||||
return getResolvableType().getNested(nestingLevel).asMap().resolveGeneric(0);
|
||||
public TypeDescriptor getMapKeyType(int nestingLevel) {
|
||||
return TypeDescriptor.valueOf(getResolvableType().getNested(nestingLevel).asMap().resolveGeneric(0));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Class<?> getMapValueType(int nestingLevel) {
|
||||
return getResolvableType().getNested(nestingLevel).asMap().resolveGeneric(1);
|
||||
public TypeDescriptor getMapValueType(int nestingLevel) {
|
||||
return TypeDescriptor.valueOf(getResolvableType().getNested(nestingLevel).asMap().resolveGeneric(1));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Class<?> getCollectionType(int nestingLevel) {
|
||||
return getResolvableType().getNested(nestingLevel).asCollection().resolveGeneric();
|
||||
public TypeDescriptor getCollectionType(int nestingLevel) {
|
||||
return TypeDescriptor.valueOf(getResolvableType().getNested(nestingLevel).asCollection().resolveGeneric());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
||||
@@ -23,21 +23,19 @@ import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.time.temporal.Temporal;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import kotlin.jvm.JvmClassMappingKt;
|
||||
import kotlin.jvm.JvmInline;
|
||||
import kotlin.reflect.KClass;
|
||||
import kotlin.reflect.KFunction;
|
||||
import kotlin.reflect.KParameter;
|
||||
import kotlin.reflect.full.KAnnotatedElements;
|
||||
import kotlin.reflect.full.KClasses;
|
||||
import kotlin.reflect.jvm.KCallablesJvm;
|
||||
import kotlin.reflect.jvm.ReflectJvmMapping;
|
||||
@@ -659,30 +657,26 @@ public abstract class BeanUtils {
|
||||
*/
|
||||
public static boolean isSimpleProperty(Class<?> type) {
|
||||
Assert.notNull(type, "'type' must not be null");
|
||||
return isSimpleValueType(type) || (type.isArray() && isSimpleValueType(type.getComponentType()));
|
||||
return isSimpleValueType(type) || (type.isArray() && isSimpleValueType(type.componentType()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given type represents a "simple" value type: a primitive or
|
||||
* primitive wrapper, an enum, a String or other CharSequence, a Number, a
|
||||
* Date, a Temporal, a URI, a URL, a Locale, or a Class.
|
||||
* Check if the given type represents a "simple" value type for
|
||||
* bean property and data binding purposes:
|
||||
* a primitive or primitive wrapper, an {@code Enum}, a {@code String}
|
||||
* or other {@code CharSequence}, a {@code Number}, a {@code Date},
|
||||
* a {@code Temporal}, a {@code UUID}, a {@code URI}, a {@code URL},
|
||||
* a {@code Locale}, or a {@code Class}.
|
||||
* <p>{@code Void} and {@code void} are not considered simple value types.
|
||||
* <p>As of 6.1, this method delegates to {@link ClassUtils#isSimpleValueType}
|
||||
* as-is but could potentially add further rules for bean property purposes.
|
||||
* @param type the type to check
|
||||
* @return whether the given type represents a "simple" value type
|
||||
* @see #isSimpleProperty(Class)
|
||||
* @see ClassUtils#isSimpleValueType(Class)
|
||||
*/
|
||||
public static boolean isSimpleValueType(Class<?> type) {
|
||||
return (Void.class != type && void.class != type &&
|
||||
(ClassUtils.isPrimitiveOrWrapper(type) ||
|
||||
Enum.class.isAssignableFrom(type) ||
|
||||
CharSequence.class.isAssignableFrom(type) ||
|
||||
Number.class.isAssignableFrom(type) ||
|
||||
Date.class.isAssignableFrom(type) ||
|
||||
Temporal.class.isAssignableFrom(type) ||
|
||||
URI.class == type ||
|
||||
URL.class == type ||
|
||||
Locale.class == type ||
|
||||
Class.class == type));
|
||||
return ClassUtils.isSimpleValueType(type);
|
||||
}
|
||||
|
||||
|
||||
@@ -844,13 +838,22 @@ public abstract class BeanUtils {
|
||||
* @see <a href="https://kotlinlang.org/docs/reference/classes.html#constructors">
|
||||
* https://kotlinlang.org/docs/reference/classes.html#constructors</a>
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@Nullable
|
||||
public static <T> Constructor<T> findPrimaryConstructor(Class<T> clazz) {
|
||||
try {
|
||||
KFunction<T> primaryCtor = KClasses.getPrimaryConstructor(JvmClassMappingKt.getKotlinClass(clazz));
|
||||
KClass<T> kClass = JvmClassMappingKt.getKotlinClass(clazz);
|
||||
KFunction<T> primaryCtor = KClasses.getPrimaryConstructor(kClass);
|
||||
if (primaryCtor == null) {
|
||||
return null;
|
||||
}
|
||||
if (kClass.isValue() && !KAnnotatedElements
|
||||
.findAnnotations(kClass, JvmClassMappingKt.getKotlinClass(JvmInline.class)).isEmpty()) {
|
||||
Constructor<?>[] constructors = clazz.getDeclaredConstructors();
|
||||
Assert.state(constructors.length == 1,
|
||||
"Kotlin value classes annotated with @JvmInline are expected to have a single JVM constructor");
|
||||
return (Constructor<T>) constructors[0];
|
||||
}
|
||||
Constructor<T> constructor = ReflectJvmMapping.getJavaConstructor(primaryCtor);
|
||||
if (constructor == null) {
|
||||
throw new IllegalStateException(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -236,19 +236,36 @@ public class BeanWrapperImpl extends AbstractNestablePropertyAccessor implements
|
||||
|
||||
private final PropertyDescriptor pd;
|
||||
|
||||
private final TypeDescriptor typeDescriptor;
|
||||
|
||||
public BeanPropertyHandler(PropertyDescriptor pd) {
|
||||
super(pd.getPropertyType(), pd.getReadMethod() != null, pd.getWriteMethod() != null);
|
||||
this.pd = pd;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolvableType getResolvableType() {
|
||||
return ResolvableType.forMethodReturnType(this.pd.getReadMethod());
|
||||
this.typeDescriptor = new TypeDescriptor(property(pd));
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeDescriptor toTypeDescriptor() {
|
||||
return new TypeDescriptor(property(this.pd));
|
||||
return this.typeDescriptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolvableType getResolvableType() {
|
||||
return this.typeDescriptor.getResolvableType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeDescriptor getMapValueType(int nestingLevel) {
|
||||
return new TypeDescriptor(
|
||||
this.typeDescriptor.getResolvableType().getNested(nestingLevel).asMap().getGeneric(1),
|
||||
null, this.typeDescriptor.getAnnotations());
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeDescriptor getCollectionType(int nestingLevel) {
|
||||
return new TypeDescriptor(
|
||||
this.typeDescriptor.getResolvableType().getNested(nestingLevel).asCollection().getGeneric(),
|
||||
null, this.typeDescriptor.getAnnotations());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -101,19 +101,34 @@ public class DirectFieldAccessor extends AbstractNestablePropertyAccessor {
|
||||
|
||||
private final Field field;
|
||||
|
||||
private final ResolvableType resolvableType;
|
||||
|
||||
public FieldPropertyHandler(Field field) {
|
||||
super(field.getType(), true, true);
|
||||
this.field = field;
|
||||
this.resolvableType = ResolvableType.forField(this.field);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeDescriptor toTypeDescriptor() {
|
||||
return new TypeDescriptor(this.field);
|
||||
return new TypeDescriptor(this.resolvableType, this.field.getType(), this.field.getAnnotations());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResolvableType getResolvableType() {
|
||||
return ResolvableType.forField(this.field);
|
||||
return this.resolvableType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeDescriptor getMapValueType(int nestingLevel) {
|
||||
return new TypeDescriptor(this.resolvableType.getNested(nestingLevel).asMap().getGeneric(1),
|
||||
null, this.field.getAnnotations());
|
||||
}
|
||||
|
||||
@Override
|
||||
public TypeDescriptor getCollectionType(int nestingLevel) {
|
||||
return new TypeDescriptor(this.resolvableType.getNested(nestingLevel).asCollection().getGeneric(),
|
||||
null, this.field.getAnnotations());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -192,14 +192,14 @@ class ExtendedBeanInfo implements BeanInfo {
|
||||
if (pd instanceof IndexedPropertyDescriptor indexedPd) {
|
||||
candidateType = indexedPd.getIndexedPropertyType();
|
||||
if (candidateName.equals(propertyName) &&
|
||||
(candidateType.equals(propertyType) || candidateType.equals(propertyType.getComponentType()))) {
|
||||
(candidateType.equals(propertyType) || candidateType.equals(propertyType.componentType()))) {
|
||||
return pd;
|
||||
}
|
||||
}
|
||||
else {
|
||||
candidateType = pd.getPropertyType();
|
||||
if (candidateName.equals(propertyName) &&
|
||||
(candidateType.equals(propertyType) || propertyType.equals(candidateType.getComponentType()))) {
|
||||
(candidateType.equals(propertyType) || propertyType.equals(candidateType.componentType()))) {
|
||||
return pd;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,7 +233,7 @@ abstract class PropertyDescriptorUtils {
|
||||
}
|
||||
|
||||
if (propertyType != null && (!propertyType.isArray() ||
|
||||
propertyType.getComponentType() != indexedPropertyType)) {
|
||||
propertyType.componentType() != indexedPropertyType)) {
|
||||
throw new IntrospectionException("Type mismatch between indexed and non-indexed methods: " +
|
||||
indexedReadMethod + " - " + indexedWriteMethod);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.NumberUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -139,13 +140,17 @@ class TypeConverterDelegate {
|
||||
|
||||
// Value not of required type?
|
||||
if (editor != null || (requiredType != null && !ClassUtils.isAssignableValue(requiredType, convertedValue))) {
|
||||
if (typeDescriptor != null && requiredType != null && Collection.class.isAssignableFrom(requiredType) &&
|
||||
convertedValue instanceof String text) {
|
||||
if (typeDescriptor != null && requiredType != null && Collection.class.isAssignableFrom(requiredType)) {
|
||||
TypeDescriptor elementTypeDesc = typeDescriptor.getElementTypeDescriptor();
|
||||
if (elementTypeDesc != null) {
|
||||
Class<?> elementType = elementTypeDesc.getType();
|
||||
if (Class.class == elementType || Enum.class.isAssignableFrom(elementType)) {
|
||||
convertedValue = StringUtils.commaDelimitedListToStringArray(text);
|
||||
if (convertedValue instanceof String text) {
|
||||
if (Class.class == elementType || Enum.class.isAssignableFrom(elementType)) {
|
||||
convertedValue = StringUtils.commaDelimitedListToStringArray(text);
|
||||
}
|
||||
if (editor == null && String.class != elementType) {
|
||||
editor = findDefaultEditor(elementType.arrayType());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,10 +172,21 @@ class TypeConverterDelegate {
|
||||
else if (requiredType.isArray()) {
|
||||
// Array required -> apply appropriate conversion of elements.
|
||||
if (convertedValue instanceof String text &&
|
||||
Enum.class.isAssignableFrom(requiredType.getComponentType())) {
|
||||
Enum.class.isAssignableFrom(requiredType.componentType())) {
|
||||
convertedValue = StringUtils.commaDelimitedListToStringArray(text);
|
||||
}
|
||||
return (T) convertToTypedArray(convertedValue, propertyName, requiredType.getComponentType());
|
||||
return (T) convertToTypedArray(convertedValue, propertyName, requiredType.componentType());
|
||||
}
|
||||
else if (convertedValue.getClass().isArray()) {
|
||||
if (Array.getLength(convertedValue) == 1) {
|
||||
convertedValue = Array.get(convertedValue, 0);
|
||||
standardConversion = true;
|
||||
}
|
||||
else if (Collection.class.isAssignableFrom(requiredType)) {
|
||||
convertedValue = convertToTypedCollection(CollectionUtils.arrayToList(convertedValue),
|
||||
propertyName, requiredType, typeDescriptor);
|
||||
standardConversion = true;
|
||||
}
|
||||
}
|
||||
else if (convertedValue instanceof Collection<?> coll) {
|
||||
// Convert elements to target type, if determined.
|
||||
@@ -182,10 +198,6 @@ class TypeConverterDelegate {
|
||||
convertedValue = convertToTypedMap(map, propertyName, requiredType, typeDescriptor);
|
||||
standardConversion = true;
|
||||
}
|
||||
if (convertedValue.getClass().isArray() && Array.getLength(convertedValue) == 1) {
|
||||
convertedValue = Array.get(convertedValue, 0);
|
||||
standardConversion = true;
|
||||
}
|
||||
if (String.class == requiredType && ClassUtils.isPrimitiveOrWrapper(convertedValue.getClass())) {
|
||||
// We can stringify any primitive value...
|
||||
return (T) convertedValue.toString();
|
||||
@@ -441,7 +453,7 @@ class TypeConverterDelegate {
|
||||
}
|
||||
else if (input.getClass().isArray()) {
|
||||
// Convert array elements, if necessary.
|
||||
if (componentType.equals(input.getClass().getComponentType()) &&
|
||||
if (componentType.equals(input.getClass().componentType()) &&
|
||||
!this.propertyEditorRegistry.hasCustomEditorForElement(componentType, propertyName)) {
|
||||
return input;
|
||||
}
|
||||
@@ -502,12 +514,11 @@ class TypeConverterDelegate {
|
||||
|
||||
Collection<Object> convertedCopy;
|
||||
try {
|
||||
if (approximable) {
|
||||
if (approximable && requiredType.isInstance(original)) {
|
||||
convertedCopy = CollectionFactory.createApproximateCollection(original, original.size());
|
||||
}
|
||||
else {
|
||||
convertedCopy = (Collection<Object>)
|
||||
ReflectionUtils.accessibleConstructor(requiredType).newInstance();
|
||||
convertedCopy = CollectionFactory.createCollection(requiredType, original.size());
|
||||
}
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
@@ -577,12 +588,11 @@ class TypeConverterDelegate {
|
||||
|
||||
Map<Object, Object> convertedCopy;
|
||||
try {
|
||||
if (approximable) {
|
||||
if (approximable && requiredType.isInstance(original)) {
|
||||
convertedCopy = CollectionFactory.createApproximateMap(original, original.size());
|
||||
}
|
||||
else {
|
||||
convertedCopy = (Map<Object, Object>)
|
||||
ReflectionUtils.accessibleConstructor(requiredType).newInstance();
|
||||
convertedCopy = CollectionFactory.createMap(requiredType, original.size());
|
||||
}
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
|
||||
@@ -42,7 +42,7 @@ public abstract class TypeConverterSupport extends PropertyEditorRegistrySupport
|
||||
@Override
|
||||
@Nullable
|
||||
public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType) throws TypeMismatchException {
|
||||
return convertIfNecessary(value, requiredType, TypeDescriptor.valueOf(requiredType));
|
||||
return convertIfNecessary(null, value, requiredType, TypeDescriptor.valueOf(requiredType));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -50,7 +50,7 @@ public abstract class TypeConverterSupport extends PropertyEditorRegistrySupport
|
||||
public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
|
||||
@Nullable MethodParameter methodParam) throws TypeMismatchException {
|
||||
|
||||
return convertIfNecessary(value, requiredType,
|
||||
return convertIfNecessary((methodParam != null ? methodParam.getParameterName() : null), value, requiredType,
|
||||
(methodParam != null ? new TypeDescriptor(methodParam) : TypeDescriptor.valueOf(requiredType)));
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ public abstract class TypeConverterSupport extends PropertyEditorRegistrySupport
|
||||
public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType, @Nullable Field field)
|
||||
throws TypeMismatchException {
|
||||
|
||||
return convertIfNecessary(value, requiredType,
|
||||
return convertIfNecessary((field != null ? field.getName() : null), value, requiredType,
|
||||
(field != null ? new TypeDescriptor(field) : TypeDescriptor.valueOf(requiredType)));
|
||||
}
|
||||
|
||||
@@ -68,9 +68,17 @@ public abstract class TypeConverterSupport extends PropertyEditorRegistrySupport
|
||||
public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
|
||||
@Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException {
|
||||
|
||||
return convertIfNecessary(null, value, requiredType, typeDescriptor);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private <T> T convertIfNecessary(@Nullable String propertyName, @Nullable Object value,
|
||||
@Nullable Class<T> requiredType, @Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException {
|
||||
|
||||
Assert.state(this.typeConverterDelegate != null, "No TypeConverterDelegate");
|
||||
try {
|
||||
return this.typeConverterDelegate.convertIfNecessary(null, null, value, requiredType, typeDescriptor);
|
||||
return this.typeConverterDelegate.convertIfNecessary(
|
||||
propertyName, null, value, requiredType, typeDescriptor);
|
||||
}
|
||||
catch (ConverterNotFoundException | IllegalStateException ex) {
|
||||
throw new ConversionNotSupportedException(value, requiredType, ex);
|
||||
|
||||
+2
-2
@@ -27,6 +27,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.aot.hint.ExecutableMode;
|
||||
import org.springframework.beans.BeanInstantiationException;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.TypeConverter;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
@@ -345,8 +346,7 @@ public final class BeanInstanceSupplier<T> extends AutowiredElementResolver impl
|
||||
Object enclosingInstance = createInstance(declaringClass.getEnclosingClass());
|
||||
args = ObjectUtils.addObjectToArray(args, enclosingInstance, 0);
|
||||
}
|
||||
ReflectionUtils.makeAccessible(constructor);
|
||||
return constructor.newInstance(args);
|
||||
return BeanUtils.instantiateClass(constructor, args);
|
||||
}
|
||||
|
||||
private Object instantiate(ConfigurableBeanFactory beanFactory, Method method, Object[] args) throws Exception {
|
||||
|
||||
+40
-6
@@ -24,6 +24,11 @@ import java.lang.reflect.Modifier;
|
||||
import java.util.Arrays;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import kotlin.jvm.JvmClassMappingKt;
|
||||
import kotlin.reflect.KClass;
|
||||
import kotlin.reflect.KFunction;
|
||||
import kotlin.reflect.KParameter;
|
||||
|
||||
import org.springframework.aot.generate.AccessControl;
|
||||
import org.springframework.aot.generate.AccessControl.Visibility;
|
||||
import org.springframework.aot.generate.GeneratedMethod;
|
||||
@@ -31,8 +36,11 @@ import org.springframework.aot.generate.GeneratedMethods;
|
||||
import org.springframework.aot.generate.GenerationContext;
|
||||
import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator;
|
||||
import org.springframework.aot.hint.ExecutableMode;
|
||||
import org.springframework.aot.hint.MemberCategory;
|
||||
import org.springframework.aot.hint.ReflectionHints;
|
||||
import org.springframework.beans.factory.support.InstanceSupplier;
|
||||
import org.springframework.beans.factory.support.RegisteredBean;
|
||||
import org.springframework.core.KotlinDetector;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.javapoet.ClassName;
|
||||
import org.springframework.javapoet.CodeBlock;
|
||||
@@ -56,6 +64,7 @@ import org.springframework.util.function.ThrowingSupplier;
|
||||
* @author Phillip Webb
|
||||
* @author Stephane Nicoll
|
||||
* @author Juergen Hoeller
|
||||
* @author Sebastien Deleuze
|
||||
* @since 6.0
|
||||
*/
|
||||
class InstanceSupplierCodeGenerator {
|
||||
@@ -108,11 +117,16 @@ class InstanceSupplierCodeGenerator {
|
||||
boolean dependsOnBean = ClassUtils.isInnerClass(declaringClass);
|
||||
|
||||
Visibility accessVisibility = getAccessVisibility(registeredBean, constructor);
|
||||
if (accessVisibility != Visibility.PRIVATE) {
|
||||
if (KotlinDetector.isKotlinReflectPresent() && KotlinDelegate.hasConstructorWithOptionalParameter(beanClass)) {
|
||||
return generateCodeForInaccessibleConstructor(beanName, beanClass, constructor,
|
||||
dependsOnBean, hints -> hints.registerType(beanClass, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS));
|
||||
}
|
||||
else if (accessVisibility != Visibility.PRIVATE) {
|
||||
return generateCodeForAccessibleConstructor(beanName, beanClass, constructor,
|
||||
dependsOnBean, declaringClass);
|
||||
}
|
||||
return generateCodeForInaccessibleConstructor(beanName, beanClass, constructor, dependsOnBean);
|
||||
return generateCodeForInaccessibleConstructor(beanName, beanClass, constructor, dependsOnBean,
|
||||
hints -> hints.registerConstructor(constructor, ExecutableMode.INVOKE));
|
||||
}
|
||||
|
||||
private CodeBlock generateCodeForAccessibleConstructor(String beanName, Class<?> beanClass,
|
||||
@@ -137,11 +151,10 @@ class InstanceSupplierCodeGenerator {
|
||||
return generateReturnStatement(generatedMethod);
|
||||
}
|
||||
|
||||
private CodeBlock generateCodeForInaccessibleConstructor(String beanName,
|
||||
Class<?> beanClass, Constructor<?> constructor, boolean dependsOnBean) {
|
||||
private CodeBlock generateCodeForInaccessibleConstructor(String beanName, Class<?> beanClass,
|
||||
Constructor<?> constructor, boolean dependsOnBean, Consumer<ReflectionHints> hints) {
|
||||
|
||||
this.generationContext.getRuntimeHints().reflection()
|
||||
.registerConstructor(constructor, ExecutableMode.INVOKE);
|
||||
hints.accept(this.generationContext.getRuntimeHints().reflection());
|
||||
|
||||
GeneratedMethod generatedMethod = generateGetInstanceSupplierMethod(method -> {
|
||||
method.addJavadoc("Get the bean instance supplier for '$L'.", beanName);
|
||||
@@ -338,4 +351,25 @@ class InstanceSupplierCodeGenerator {
|
||||
.anyMatch(Exception.class::isAssignableFrom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inner class to avoid a hard dependency on Kotlin at runtime.
|
||||
*/
|
||||
private static class KotlinDelegate {
|
||||
|
||||
public static boolean hasConstructorWithOptionalParameter(Class<?> beanClass) {
|
||||
if (KotlinDetector.isKotlinType(beanClass)) {
|
||||
KClass<?> kClass = JvmClassMappingKt.getKotlinClass(beanClass);
|
||||
for (KFunction<?> constructor : kClass.getConstructors()) {
|
||||
for (KParameter parameter : constructor.getParameters()) {
|
||||
if (parameter.isOptional()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+9
-6
@@ -64,7 +64,6 @@ public interface AutowireCapableBeanFactory extends BeanFactory {
|
||||
/**
|
||||
* Constant that indicates no externally defined autowiring. Note that
|
||||
* BeanFactoryAware etc and annotation-driven injection will still be applied.
|
||||
* @see #createBean
|
||||
* @see #autowire
|
||||
* @see #autowireBeanProperties
|
||||
*/
|
||||
@@ -73,7 +72,6 @@ public interface AutowireCapableBeanFactory extends BeanFactory {
|
||||
/**
|
||||
* Constant that indicates autowiring bean properties by name
|
||||
* (applying to all bean property setters).
|
||||
* @see #createBean
|
||||
* @see #autowire
|
||||
* @see #autowireBeanProperties
|
||||
*/
|
||||
@@ -82,7 +80,6 @@ public interface AutowireCapableBeanFactory extends BeanFactory {
|
||||
/**
|
||||
* Constant that indicates autowiring bean properties by type
|
||||
* (applying to all bean property setters).
|
||||
* @see #createBean
|
||||
* @see #autowire
|
||||
* @see #autowireBeanProperties
|
||||
*/
|
||||
@@ -91,7 +88,6 @@ public interface AutowireCapableBeanFactory extends BeanFactory {
|
||||
/**
|
||||
* Constant that indicates autowiring the greediest constructor that
|
||||
* can be satisfied (involves resolving the appropriate constructor).
|
||||
* @see #createBean
|
||||
* @see #autowire
|
||||
*/
|
||||
int AUTOWIRE_CONSTRUCTOR = 3;
|
||||
@@ -99,7 +95,6 @@ public interface AutowireCapableBeanFactory extends BeanFactory {
|
||||
/**
|
||||
* Constant that indicates determining an appropriate autowire strategy
|
||||
* through introspection of the bean class.
|
||||
* @see #createBean
|
||||
* @see #autowire
|
||||
* @deprecated as of Spring 3.0: If you are using mixed autowiring strategies,
|
||||
* prefer annotation-based autowiring for clearer demarcation of autowiring needs.
|
||||
@@ -192,7 +187,9 @@ public interface AutowireCapableBeanFactory extends BeanFactory {
|
||||
* @see #AUTOWIRE_BY_NAME
|
||||
* @see #AUTOWIRE_BY_TYPE
|
||||
* @see #AUTOWIRE_CONSTRUCTOR
|
||||
* @deprecated as of 6.1, in favor of {@link #createBean(Class)}
|
||||
*/
|
||||
@Deprecated(since = "6.1")
|
||||
Object createBean(Class<?> beanClass, int autowireMode, boolean dependencyCheck) throws BeansException;
|
||||
|
||||
/**
|
||||
@@ -300,7 +297,10 @@ public interface AutowireCapableBeanFactory extends BeanFactory {
|
||||
* @throws BeansException if any post-processing failed
|
||||
* @see BeanPostProcessor#postProcessBeforeInitialization
|
||||
* @see #ORIGINAL_INSTANCE_SUFFIX
|
||||
* @deprecated as of 6.1, in favor of implicit post-processing through
|
||||
* {@link #initializeBean(Object, String)}
|
||||
*/
|
||||
@Deprecated(since = "6.1")
|
||||
Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName)
|
||||
throws BeansException;
|
||||
|
||||
@@ -317,12 +317,15 @@ public interface AutowireCapableBeanFactory extends BeanFactory {
|
||||
* @throws BeansException if any post-processing failed
|
||||
* @see BeanPostProcessor#postProcessAfterInitialization
|
||||
* @see #ORIGINAL_INSTANCE_SUFFIX
|
||||
* @deprecated as of 6.1, in favor of implicit post-processing through
|
||||
* {@link #initializeBean(Object, String)}
|
||||
*/
|
||||
@Deprecated(since = "6.1")
|
||||
Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
|
||||
throws BeansException;
|
||||
|
||||
/**
|
||||
* Destroy the given bean instance (typically coming from {@link #createBean}),
|
||||
* Destroy the given bean instance (typically coming from {@link #createBean(Class)}),
|
||||
* applying the {@link org.springframework.beans.factory.DisposableBean} contract as well as
|
||||
* registered {@link DestructionAwareBeanPostProcessor DestructionAwareBeanPostProcessors}.
|
||||
* <p>Any exception that arises during destruction should be caught
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user