mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-18 18:22:23 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e6e620ee1 | ||
|
|
f6e53b76ae | ||
|
|
f9f9186478 | ||
|
|
12cf9b7d54 | ||
|
|
96cbca1db5 | ||
|
|
6f4021ad87 | ||
|
|
bd1616b0aa | ||
|
|
71965c1c85 | ||
|
|
3d023b3ef5 | ||
|
|
00291a5172 | ||
|
|
c1ba31c40f | ||
|
|
4cdd314850 | ||
|
|
884975b7eb | ||
|
|
e8eb2b6751 | ||
|
|
496ed729a0 | ||
|
|
7ca362a003 | ||
|
|
c3360ba7da | ||
|
|
3222c1b3a8 | ||
|
|
cd110ad14e | ||
|
|
0d079ea435 | ||
|
|
e2cc271e0c | ||
|
|
8ab525f6f0 | ||
|
|
33988a4621 | ||
|
|
5b424c0431 | ||
|
|
6504e75669 | ||
|
|
566887d573 | ||
|
|
0148c4ccde | ||
|
|
dabd63770f | ||
|
|
fe2617582a | ||
|
|
c4376c04b1 | ||
|
|
74a6c1c328 | ||
|
|
05a1075b69 | ||
|
|
9f8a476f94 | ||
|
|
afdbebee70 | ||
|
|
a099debca9 | ||
|
|
c1d4a76692 | ||
|
|
8d50a5b724 | ||
|
|
b40ed77e94 | ||
|
|
d1402d3c7f | ||
|
|
adbc8ceeab | ||
|
|
60b9f4cd3a | ||
|
|
aa655718a8 | ||
|
|
06f2fba5ed | ||
|
|
e8f5e31219 | ||
|
|
572850bdcf | ||
|
|
1bc5bb0f90 | ||
|
|
30a07ed551 | ||
|
|
4c8c6409a2 | ||
|
|
8f4fcb6cbc | ||
|
|
2028c54d01 | ||
|
|
e06482ad51 | ||
|
|
99f2ccc2ec | ||
|
|
1aedfa5d5a | ||
|
|
93528f096a | ||
|
|
e74054be0a | ||
|
|
42fe29218b | ||
|
|
e92bf76055 | ||
|
|
19fdc0b77c | ||
|
|
82018e1510 | ||
|
|
9bedb06b9b | ||
|
|
54b3a8c868 | ||
|
|
59df1a7031 | ||
|
|
c21ea9e249 | ||
|
|
acbae80205 | ||
|
|
60e5abff7f | ||
|
|
21bb726934 | ||
|
|
658c263cf6 | ||
|
|
34b9815215 | ||
|
|
21ee87448c | ||
|
|
c0519b9bbf | ||
|
|
c39d48a261 | ||
|
|
130b0ec50c | ||
|
|
815911b39c | ||
|
|
d130601a74 | ||
|
|
4f0b8e4205 | ||
|
|
42cffc2a55 | ||
|
|
d798317a2b | ||
|
|
d550ab1313 | ||
|
|
136dddb67d | ||
|
|
0de56c41df | ||
|
|
29602e95b6 | ||
|
|
94458c05e0 | ||
|
|
351a6d8f61 | ||
|
|
ff9192fa8f | ||
|
|
3be70836ce | ||
|
|
1911647ab5 | ||
|
|
2ce9563b3c | ||
|
|
e2235b702a | ||
|
|
558e8955f0 | ||
|
|
46913bca91 | ||
|
|
5ab70bb036 | ||
|
|
ce00aac748 | ||
|
|
990d1a8370 | ||
|
|
5df0c2dc0c | ||
|
|
5c4bf226bb | ||
|
|
2028eb3694 | ||
|
|
ee19b96ee9 | ||
|
|
bc66622342 | ||
|
|
dd110d4604 | ||
|
|
513ca9d9e5 | ||
|
|
4b5afaff53 | ||
|
|
97067f9c9c | ||
|
|
5524fc6a55 | ||
|
|
49d955650a | ||
|
|
b020aca2eb | ||
|
|
354ade9f40 | ||
|
|
1b56f58999 | ||
|
|
3eb93a4873 | ||
|
|
09548c0874 | ||
|
|
491859b5ce | ||
|
|
5d6a56fe4a | ||
|
|
d9e240b37d | ||
|
|
2588fb078e | ||
|
|
752193fca9 | ||
|
|
17e0daf8a1 | ||
|
|
d883602919 | ||
|
|
1b354f2705 | ||
|
|
7daf1013aa | ||
|
|
6e9534df4d | ||
|
|
a00fb1b5ae | ||
|
|
b28569119f | ||
|
|
79a75a5762 | ||
|
|
ac95b96c21 | ||
|
|
91eb42645e | ||
|
|
aee39843f8 | ||
|
|
9dabfe98e8 | ||
|
|
fb240829b3 | ||
|
|
68d438c9ff | ||
|
|
8473ec3e25 | ||
|
|
89047909ea | ||
|
|
df72838ce1 | ||
|
|
6ee3ef6af5 | ||
|
|
59a784cb7e | ||
|
|
af466ccf63 | ||
|
|
bcfa6c3c4f | ||
|
|
74b6a5ba3e | ||
|
|
2730d77f82 | ||
|
|
4a64537ac6 | ||
|
|
555ac3768d | ||
|
|
e9e00e3257 | ||
|
|
526c706d1c | ||
|
|
e8e293a706 | ||
|
|
f067d40f0a | ||
|
|
fd95ab16ba | ||
|
|
ba13cae6cc | ||
|
|
ee3b666a5c | ||
|
|
a165094470 | ||
|
|
04e5c92162 | ||
|
|
b20d31a5cd | ||
|
|
daa8c10031 | ||
|
|
7da47f8c47 | ||
|
|
a947f9bf79 | ||
|
|
a13056f6af | ||
|
|
d823822dd6 | ||
|
|
14927a959f | ||
|
|
efbae85324 | ||
|
|
e12f0761f3 | ||
|
|
30e3a5719e | ||
|
|
d31f7a5a80 | ||
|
|
1c77e241e6 | ||
|
|
9312e25e24 | ||
|
|
d5dbc9b310 | ||
|
|
a69fe71630 | ||
|
|
d6f5356db1 | ||
|
|
b4d9b514f5 | ||
|
|
ea3e61fd2f | ||
|
|
8a92c19e4d | ||
|
|
ee1874ac52 | ||
|
|
8cb1151375 | ||
|
|
bb65d819a4 | ||
|
|
5abe6d3e5f | ||
|
|
999f428987 | ||
|
|
fd50270b8d | ||
|
|
b10179ffdf | ||
|
|
f8974ad620 | ||
|
|
c7712052ce | ||
|
|
e78d3df566 | ||
|
|
951c1f306a | ||
|
|
27a85be4e0 | ||
|
|
8b894933ae | ||
|
|
68e6acd37e | ||
|
|
88b383a153 | ||
|
|
00b9063e7d | ||
|
|
cc751e61a2 | ||
|
|
69bf83ad71 | ||
|
|
da4b31c82b | ||
|
|
b6cb9a5f87 | ||
|
|
7f01cd0a5b | ||
|
|
51c4539bb6 | ||
|
|
595c246cce | ||
|
|
4b5c92703c | ||
|
|
c17b4ad787 | ||
|
|
e8729d0438 | ||
|
|
d2d7fd36da | ||
|
|
0abf59feee | ||
|
|
11da74d51b | ||
|
|
eceebb3077 | ||
|
|
f3e202e1b5 | ||
|
|
17002a26cc | ||
|
|
0376dd9a78 | ||
|
|
7c2fdcc1fb | ||
|
|
abe33703b4 | ||
|
|
badddeb0dc | ||
|
|
50f923ace4 | ||
|
|
317eae88d0 | ||
|
|
2aaeef7190 | ||
|
|
8bc5e11ec3 | ||
|
|
f53674d582 | ||
|
|
67d54dccd2 | ||
|
|
4bcae5305d | ||
|
|
3f632382d6 | ||
|
|
cde75754bc | ||
|
|
dddd237449 | ||
|
|
56f7cc2dab | ||
|
|
91c6851f29 | ||
|
|
16d9965fe3 | ||
|
|
1d1aac3674 | ||
|
|
d5acf5bceb | ||
|
|
4c192bf58f | ||
|
|
cb6226c98a | ||
|
|
ffcf37468f | ||
|
|
e24f5f2ca7 | ||
|
|
079992021c | ||
|
|
9bdeadcfbd | ||
|
|
31c37d4f2c | ||
|
|
68862530fb | ||
|
|
7b71df72e5 | ||
|
|
224522244f | ||
|
|
1502ab0b20 | ||
|
|
304f8eb27e | ||
|
|
3dfb4ef754 | ||
|
|
08a4844288 | ||
|
|
38e1bf5970 | ||
|
|
5ac20a8104 | ||
|
|
12d71c9a9b | ||
|
|
0791d9a6e3 | ||
|
|
734c7ed4a7 | ||
|
|
40f7d56ed4 | ||
|
|
c4c0a84f83 | ||
|
|
99b991b6f3 | ||
|
|
9024d5ffbd | ||
|
|
6dd2aa3988 | ||
|
|
28bf619887 | ||
|
|
1700fad16d | ||
|
|
d71643c347 | ||
|
|
257687d44b | ||
|
|
9a82d107c0 | ||
|
|
45b4d00d9f | ||
|
|
4bcb6cc081 | ||
|
|
df0ec74107 | ||
|
|
c5d7908a78 | ||
|
|
ce718cf699 | ||
|
|
efaa53b488 | ||
|
|
bb34bf6dc6 | ||
|
|
8f539b6e23 | ||
|
|
62eea96151 | ||
|
|
3cf19aabcd | ||
|
|
fc0ad3d367 | ||
|
|
69839889c4 | ||
|
|
2b7ec43571 | ||
|
|
787f9e1cbb | ||
|
|
ed0919b92e | ||
|
|
7e9da44e18 | ||
|
|
a077324670 | ||
|
|
c35e17b038 | ||
|
|
0cdb0cfae4 | ||
|
|
bd405756ab | ||
|
|
9130ded96f | ||
|
|
0dc2d03093 | ||
|
|
8a2e4a9e0a | ||
|
|
0fbe714bd1 | ||
|
|
9c64be98e4 | ||
|
|
8e6b6c5ac3 | ||
|
|
7b31e0c2dc | ||
|
|
233e7b91f9 | ||
|
|
077bfaf095 | ||
|
|
175f551d91 | ||
|
|
2bc0ee7ec1 | ||
|
|
65fe0f1d2f | ||
|
|
d5dee4ef1c | ||
|
|
97db213d41 | ||
|
|
7565c51dc5 | ||
|
|
03d80feed0 | ||
|
|
b611fcf114 | ||
|
|
472e610c4a | ||
|
|
2723847917 | ||
|
|
94db4f7f7a | ||
|
|
8cfe90c4c0 | ||
|
|
30287d789c | ||
|
|
c26029c26b | ||
|
|
0bfe82b315 | ||
|
|
cdc3c52640 | ||
|
|
0c60266986 | ||
|
|
99f6f1a77a | ||
|
|
1bd3ad2050 | ||
|
|
2b08e6e1d3 | ||
|
|
4e39c28b92 | ||
|
|
381fa10477 | ||
|
|
14f4deef3a | ||
|
|
e23971efb9 | ||
|
|
2469aae672 | ||
|
|
83e29382b3 | ||
|
|
ee5c82a2f8 | ||
|
|
ae7891e797 | ||
|
|
98ed1dfcf8 | ||
|
|
9b8a851969 | ||
|
|
4c6194a2ad | ||
|
|
2fc99eb12f | ||
|
|
783388e9f8 | ||
|
|
de4de02a1d | ||
|
|
e6ce2a3c36 | ||
|
|
481a5743b3 | ||
|
|
85a8868bae | ||
|
|
dd72b9eb4b | ||
|
|
2e65c1dfe6 | ||
|
|
ca72cd66e3 | ||
|
|
a3f3ba5685 | ||
|
|
79f4f76bd2 | ||
|
|
744d136cf7 | ||
|
|
00ca23859e | ||
|
|
facc7c5371 | ||
|
|
bd1e7e16a9 | ||
|
|
25e8395df8 | ||
|
|
148b7fd8f3 | ||
|
|
68338aa818 | ||
|
|
fbbc0f487c | ||
|
|
611c390417 | ||
|
|
cf3196e8de | ||
|
|
80e9e4f6f2 | ||
|
|
91655be0db | ||
|
|
3b030e0431 | ||
|
|
9870ce1844 | ||
|
|
d87c03a6be | ||
|
|
1d91982f83 | ||
|
|
0c25d817bd | ||
|
|
17ed3e8370 | ||
|
|
2f458f9093 | ||
|
|
16e0ed0463 | ||
|
|
e24448118c | ||
|
|
a0ec6656b4 | ||
|
|
5ab4c5c5d9 | ||
|
|
c3baa01535 | ||
|
|
c0b749479a | ||
|
|
5bf58cfe05 | ||
|
|
238a24cb6d | ||
|
|
9a54f75d6d | ||
|
|
9db16e4c15 | ||
|
|
ec8f0ca6b2 | ||
|
|
1a0bd12c34 | ||
|
|
e6590aa1a8 | ||
|
|
051b09694e | ||
|
|
39ff8e46ab | ||
|
|
27bdf24482 | ||
|
|
d9ecf945cc | ||
|
|
b35da5b140 | ||
|
|
367a62018d | ||
|
|
1b26f5d1e6 | ||
|
|
6ff2d187cf | ||
|
|
72cf389754 | ||
|
|
3184eb3acc | ||
|
|
9e4c127eda | ||
|
|
1f642b973b | ||
|
|
80c9efd638 | ||
|
|
0e1a2b4f87 | ||
|
|
5e6e223686 | ||
|
|
7a5844f1ce | ||
|
|
0d14aa5856 | ||
|
|
c6096ff6e5 | ||
|
|
6ff758391c | ||
|
|
ca9b26eb8f | ||
|
|
279409acce | ||
|
|
4f1b4b36bf | ||
|
|
680f4ee482 | ||
|
|
59cd577fd1 | ||
|
|
9f92183710 | ||
|
|
680854d1f3 | ||
|
|
1a7161c85e | ||
|
|
8bce51267a | ||
|
|
d169eb547e | ||
|
|
9c8535f5e4 | ||
|
|
829add3aa9 | ||
|
|
20e57608ba | ||
|
|
6275f46a66 | ||
|
|
9a17b5c453 | ||
|
|
1787d3e885 | ||
|
|
1f5af8f364 | ||
|
|
a4b33b98df | ||
|
|
59f9cf8645 | ||
|
|
fb34264169 | ||
|
|
c2cf5e065d | ||
|
|
e0e78257d6 | ||
|
|
227ddf817d | ||
|
|
07aa952bed | ||
|
|
598f0b64f0 | ||
|
|
d4e3d6be58 | ||
|
|
8fe0eec5bf | ||
|
|
f78264d158 | ||
|
|
4b0101a9dc | ||
|
|
e940a38014 | ||
|
|
623bdfb677 | ||
|
|
8566e7bf55 | ||
|
|
c17f25f939 | ||
|
|
9da22ecb46 | ||
|
|
f602967dbc | ||
|
|
f7d3556b8c | ||
|
|
813e113ea9 | ||
|
|
306a1f6c99 | ||
|
|
62c6d67615 | ||
|
|
c4c0aca69b | ||
|
|
6f08c0b473 | ||
|
|
d37d7abb17 | ||
|
|
2a1678f246 | ||
|
|
3806315e31 | ||
|
|
6062363738 | ||
|
|
31d9fe5f41 | ||
|
|
2ee4c3a363 | ||
|
|
82b179f238 | ||
|
|
f993e9710d | ||
|
|
822001c6a4 | ||
|
|
f2d4d59f5a | ||
|
|
4709f68446 | ||
|
|
afba74c516 | ||
|
|
fd50c0841c | ||
|
|
cbe8b148b1 | ||
|
|
2086508924 | ||
|
|
759b173b1a | ||
|
|
7590c4c92e | ||
|
|
596c0df826 | ||
|
|
d5b6f4a7ee | ||
|
|
5dc4a3a7a0 | ||
|
|
21f8b6d2f3 | ||
|
|
6ebaeba1c2 | ||
|
|
3bf31e45dd | ||
|
|
5e16e25109 | ||
|
|
6b5f0e92b1 | ||
|
|
2c973d3034 | ||
|
|
94e2f49e9f | ||
|
|
8b62ea13a1 | ||
|
|
76ad7c9cc0 | ||
|
|
8586095370 | ||
|
|
32970fe10d | ||
|
|
83c2afb643 | ||
|
|
7473cd5fbc | ||
|
|
8de3683ccb | ||
|
|
208f62ba07 | ||
|
|
a0d51c8f7d | ||
|
|
f4cefb6ec5 | ||
|
|
e7fdbb8339 | ||
|
|
a64b001f6a | ||
|
|
c68470d0fd | ||
|
|
182e6b744a | ||
|
|
d46e96f1ca | ||
|
|
51a5489438 | ||
|
|
f9523a785b | ||
|
|
955f9d3ea9 | ||
|
|
7502b92392 | ||
|
|
99e7543a7f | ||
|
|
82a9d66079 | ||
|
|
5708b73ea9 | ||
|
|
5e975f51dd | ||
|
|
ccf0cae18c | ||
|
|
de0dfe5d93 | ||
|
|
aac521c116 | ||
|
|
ab8de8ec4b | ||
|
|
abc3cfc7be | ||
|
|
44302aca93 | ||
|
|
ac86dc1264 | ||
|
|
051219c3c9 | ||
|
|
d8916a326a | ||
|
|
3be51b1c77 | ||
|
|
a3118f276f | ||
|
|
3d70074089 | ||
|
|
bcc9e27dd0 | ||
|
|
0f4ee906de | ||
|
|
5f9dd9e135 | ||
|
|
5335dbf802 | ||
|
|
4c1b4f33a8 | ||
|
|
f1a60f1664 | ||
|
|
d66e6571c1 | ||
|
|
dc8b9fff57 | ||
|
|
0eba6f0da3 | ||
|
|
a5579614d9 | ||
|
|
eca6d91532 | ||
|
|
354c315f55 | ||
|
|
a302ad88f3 | ||
|
|
447e1ee0cd | ||
|
|
bef2f4488f | ||
|
|
d5adfd7980 | ||
|
|
3777a9fcbd | ||
|
|
7c834224a2 | ||
|
|
c1e90d51ca | ||
|
|
a0dba60fa6 | ||
|
|
63e01cf6b3 | ||
|
|
5d45036c09 | ||
|
|
b31f78de50 | ||
|
|
967fd099f9 | ||
|
|
e655edafec | ||
|
|
6ec2455e24 | ||
|
|
003d8b2f80 | ||
|
|
b846a29b17 | ||
|
|
8b994be381 | ||
|
|
b246fc881b | ||
|
|
5785923c0e | ||
|
|
66607cb145 | ||
|
|
4637da1f36 | ||
|
|
22e4d84993 | ||
|
|
e2b9b19970 | ||
|
|
75f964657f | ||
|
|
23b73168a0 | ||
|
|
391dd90e84 | ||
|
|
1345760087 | ||
|
|
51fccf226f | ||
|
|
a14a20b61b | ||
|
|
fadbd0fa31 | ||
|
|
40ef084e7f | ||
|
|
a07033f3fb | ||
|
|
293a9c0ee3 | ||
|
|
32d1b83f62 | ||
|
|
227bc196b1 | ||
|
|
2fcef050e0 |
@@ -2,7 +2,7 @@ name: Build and Deploy Snapshot
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 7.0.x
|
||||
- 'main'
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
|
||||
/**/framework-api-*-docs.zip::zip.type=docs
|
||||
/**/framework-api-*-schema.zip::zip.type=schema
|
||||
build-name: ${{ vars.COMMERCIAL && format('spring-framework-commercial-{0}', '7.0.x') || format('spring-framework-{0}', '7.0.x') }}
|
||||
build-name: ${{ vars.COMMERCIAL && format('spring-framework-commercial-{0}', '7.1.x') || format('spring-framework-{0}', '7.1.x') }}
|
||||
folder: 'deployment-repository'
|
||||
project: ${{ vars.COMMERCIAL && 'spring' }}
|
||||
repository: ${{ vars.COMMERCIAL && 'spring-enterprise-maven-dev-local' || 'libs-snapshot-local' }}
|
||||
|
||||
@@ -22,7 +22,7 @@ jobs:
|
||||
toolchain: true
|
||||
- version: 25
|
||||
toolchain: false
|
||||
- version: 26
|
||||
- version: 27
|
||||
toolchain: true
|
||||
exclude:
|
||||
- os:
|
||||
|
||||
@@ -2,8 +2,8 @@ name: Release Milestone
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v7.0.0-M[1-9]
|
||||
- v7.0.0-RC[1-9]
|
||||
- v7.1.0-M[1-9]
|
||||
- v7.1.0-RC[1-9]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
|
||||
@@ -2,7 +2,7 @@ name: Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v7.0.[0-9]+
|
||||
- v7.1.[0-9]+
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Contributing to the Spring Framework
|
||||
# Contributing to the Spring Framework
|
||||
|
||||
First off, thank you for taking the time to contribute! :+1: :tada:
|
||||
|
||||
|
||||
+3
-3
@@ -3,7 +3,7 @@ plugins {
|
||||
// kotlinVersion is managed in gradle.properties
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version "${kotlinVersion}" apply false
|
||||
id 'org.jetbrains.dokka'
|
||||
id 'com.github.bjornvester.xjc' version '1.8.2' apply false
|
||||
id 'com.github.bjornvester.xjc' version '1.9.1' apply false
|
||||
id 'com.gradleup.shadow' version "9.2.2" apply false
|
||||
id 'me.champeau.jmh' version '0.7.2' apply false
|
||||
id 'io.spring.nullability' version '0.0.15' apply false
|
||||
@@ -64,13 +64,13 @@ configure([rootProject] + javaProjects) { project ->
|
||||
ext.javadocLinks = [
|
||||
"https://docs.oracle.com/en/java/javase/17/docs/api/",
|
||||
//"https://jakarta.ee/specifications/platform/11/apidocs/",
|
||||
"https://docs.hibernate.org/orm/7.2/javadocs/",
|
||||
"https://docs.hibernate.org/orm/7.4/javadocs/",
|
||||
"https://www.quartz-scheduler.org/api/2.3.0/",
|
||||
"https://hc.apache.org/httpcomponents-client-5.6.x/5.6/httpclient5/apidocs/",
|
||||
"https://projectreactor.io/docs/core/release/api/",
|
||||
"https://projectreactor.io/docs/test/release/api/",
|
||||
"https://junit.org/junit4/javadoc/4.13.2/",
|
||||
"https://docs.junit.org/6.0.3/api/",
|
||||
"https://docs.junit.org/6.1.2/api/",
|
||||
"https://www.reactive-streams.org/reactive-streams-1.0.4-javadoc/",
|
||||
"https://r2dbc.io/spec/1.0.0.RELEASE/api/",
|
||||
"https://jspecify.dev/docs/api/"
|
||||
|
||||
@@ -20,7 +20,7 @@ ext {
|
||||
dependencies {
|
||||
checkstyle "io.spring.javaformat:spring-javaformat-checkstyle:${javaFormatVersion}"
|
||||
implementation "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}"
|
||||
implementation "org.jetbrains.dokka:dokka-gradle-plugin:2.1.0"
|
||||
implementation "org.jetbrains.dokka:dokka-gradle-plugin:2.2.0"
|
||||
implementation "com.tngtech.archunit:archunit:1.4.1"
|
||||
implementation "org.gradle:test-retry-gradle-plugin:1.6.2"
|
||||
implementation "io.spring.javaformat:spring-javaformat-gradle-plugin:${javaFormatVersion}"
|
||||
|
||||
@@ -37,7 +37,8 @@ public class KotlinConventions {
|
||||
if (project.getLayout().getProjectDirectory().dir("src/main/kotlin").getAsFile().exists()) {
|
||||
project.getPlugins().apply(DokkaPlugin.class);
|
||||
project.getExtensions().configure(DokkaExtension.class, dokka -> configure(project, dokka));
|
||||
project.project(":framework-api").getDependencies().add("dokka", project);
|
||||
project.project(":framework-api").getDependencies()
|
||||
.add("dokka", project.getDependencyFactory().createProjectDependency());
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -54,7 +55,7 @@ public class KotlinConventions {
|
||||
"-Xjsr305=strict", // For dependencies using JSR 305
|
||||
"-opt-in=kotlin.RequiresOptIn",
|
||||
"-Xjdk-release=17", // Needed due to https://youtrack.jetbrains.com/issue/KT-49746
|
||||
"-Xannotation-default-target=param-property" // Upcoming default, see https://youtrack.jetbrains.com/issue/KT-73255
|
||||
"-Xannotation-default-target=param-property" // Preferred behavior, default with Kotlin language version set to 2.4+, see https://youtrack.jetbrains.com/issue/KT-73255
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
+8
-20
@@ -18,7 +18,6 @@ package org.springframework.build.architecture;
|
||||
|
||||
import com.tngtech.archunit.core.domain.JavaClasses;
|
||||
import com.tngtech.archunit.core.importer.ClassFileImporter;
|
||||
import com.tngtech.archunit.lang.ArchRule;
|
||||
import com.tngtech.archunit.lang.EvaluationResult;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -44,12 +43,6 @@ import org.gradle.api.tasks.PathSensitivity;
|
||||
import org.gradle.api.tasks.SkipWhenEmpty;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
|
||||
import static org.springframework.build.architecture.ArchitectureRules.allPackagesShouldBeFreeOfTangles;
|
||||
import static org.springframework.build.architecture.ArchitectureRules.classesShouldNotImportForbiddenTypes;
|
||||
import static org.springframework.build.architecture.ArchitectureRules.javaClassesShouldNotImportKotlinAnnotations;
|
||||
import static org.springframework.build.architecture.ArchitectureRules.noClassesShouldCallStringToLowerCaseWithoutLocale;
|
||||
import static org.springframework.build.architecture.ArchitectureRules.noClassesShouldCallStringToUpperCaseWithoutLocale;
|
||||
|
||||
/**
|
||||
* {@link Task} that checks for architecture problems.
|
||||
*
|
||||
@@ -63,12 +56,11 @@ public abstract class ArchitectureCheck extends DefaultTask {
|
||||
public ArchitectureCheck() {
|
||||
getOutputDirectory().convention(getProject().getLayout().getBuildDirectory().dir(getName()));
|
||||
getProhibitObjectsRequireNonNull().convention(true);
|
||||
getRules().addAll(classesShouldNotImportForbiddenTypes(),
|
||||
javaClassesShouldNotImportKotlinAnnotations(),
|
||||
allPackagesShouldBeFreeOfTangles(),
|
||||
noClassesShouldCallStringToLowerCaseWithoutLocale(),
|
||||
noClassesShouldCallStringToUpperCaseWithoutLocale());
|
||||
getRuleDescriptions().set(getRules().map((rules) -> rules.stream().map(ArchRule::getDescription).toList()));
|
||||
getRules().addAll(ArchitectureRules.CLASSES_SHOULD_NOT_IMPORT_FORBIDDEN_TYPES,
|
||||
ArchitectureRules.JAVA_CLASSES_SHOULD_NOT_IMPORT_KOTLIN_ANNOTATIONS,
|
||||
ArchitectureRules.ALL_PACKAGES_SHOULD_BE_FREE_OF_TANGLES,
|
||||
ArchitectureRules.NO_CLASSES_SHOULD_CALL_STRING_TO_LOWER_CASE_WITHOUT_LOCALE,
|
||||
ArchitectureRules.NO_CLASSES_SHOULD_CALL_STRING_TO_UPPER_CASE_WITHOUT_LOCALE);
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
@@ -77,6 +69,7 @@ public abstract class ArchitectureCheck extends DefaultTask {
|
||||
.importPaths(this.classes.getFiles().stream().map(File::toPath).toList());
|
||||
List<EvaluationResult> violations = getRules().get()
|
||||
.stream()
|
||||
.map(ArchitectureRules::archRule)
|
||||
.map((rule) -> rule.evaluate(javaClasses))
|
||||
.filter(EvaluationResult::hasViolation)
|
||||
.toList();
|
||||
@@ -122,14 +115,9 @@ public abstract class ArchitectureCheck extends DefaultTask {
|
||||
@OutputDirectory
|
||||
public abstract DirectoryProperty getOutputDirectory();
|
||||
|
||||
@Internal
|
||||
public abstract ListProperty<ArchRule> getRules();
|
||||
@Input
|
||||
public abstract ListProperty<ArchitectureRules> getRules();
|
||||
|
||||
@Internal
|
||||
public abstract Property<Boolean> getProhibitObjectsRequireNonNull();
|
||||
|
||||
@Input
|
||||
// The rules themselves can't be an input as they aren't serializable so we use
|
||||
// their descriptions instead
|
||||
abstract ListProperty<String> getRuleDescriptions();
|
||||
}
|
||||
|
||||
+22
-5
@@ -25,7 +25,24 @@ import com.tngtech.archunit.library.dependencies.SliceIdentifier;
|
||||
import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition;
|
||||
import java.util.List;
|
||||
|
||||
abstract class ArchitectureRules {
|
||||
public enum ArchitectureRules {
|
||||
|
||||
ALL_PACKAGES_SHOULD_BE_FREE_OF_TANGLES(allPackagesShouldBeFreeOfTangles()),
|
||||
NO_CLASSES_SHOULD_CALL_STRING_TO_LOWER_CASE_WITHOUT_LOCALE(noClassesShouldCallStringToLowerCaseWithoutLocale()),
|
||||
NO_CLASSES_SHOULD_CALL_STRING_TO_UPPER_CASE_WITHOUT_LOCALE(noClassesShouldCallStringToUpperCaseWithoutLocale()),
|
||||
CLASSES_SHOULD_NOT_IMPORT_FORBIDDEN_TYPES(classesShouldNotImportForbiddenTypes()),
|
||||
JAVA_CLASSES_SHOULD_NOT_IMPORT_KOTLIN_ANNOTATIONS(javaClassesShouldNotImportKotlinAnnotations())
|
||||
;
|
||||
|
||||
private final ArchRule archRule;
|
||||
|
||||
public ArchRule archRule() {
|
||||
return this.archRule;
|
||||
}
|
||||
|
||||
private ArchitectureRules(ArchRule archRule) {
|
||||
this.archRule = archRule;
|
||||
}
|
||||
|
||||
static ArchRule allPackagesShouldBeFreeOfTangles() {
|
||||
return SlicesRuleDefinition.slices()
|
||||
@@ -61,10 +78,10 @@ abstract class ArchitectureRules {
|
||||
static ArchRule javaClassesShouldNotImportKotlinAnnotations() {
|
||||
return ArchRuleDefinition.noClasses()
|
||||
.that(new DescribedPredicate<JavaClass>("is not a Kotlin class") {
|
||||
@Override
|
||||
public boolean test(JavaClass javaClass) {
|
||||
return javaClass.getSourceCodeLocation()
|
||||
.getSourceFileName().endsWith(".java");
|
||||
@Override
|
||||
public boolean test(JavaClass javaClass) {
|
||||
return javaClass.getSourceCodeLocation()
|
||||
.getSourceFileName().endsWith(".java");
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -65,7 +65,8 @@ public class RuntimeHintsAgentPlugin implements Plugin<Project> {
|
||||
test.getJvmArgumentProviders().add(createRuntimeHintsAgentArgumentProvider(project, agentExtension));
|
||||
});
|
||||
project.getTasks().named("check", task -> task.dependsOn(agentTest));
|
||||
project.getDependencies().add(CONFIGURATION_NAME, project.project(":spring-core-test"));
|
||||
project.getDependencies().add(CONFIGURATION_NAME,
|
||||
project.getDependencyFactory().createProjectDependency(":spring-core-test"));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ asciidoc:
|
||||
spring-org: 'spring-projects'
|
||||
spring-github-org: "https://github.com/{spring-org}"
|
||||
spring-framework-github: "https://github.com/{spring-org}/spring-framework"
|
||||
spring-framework-code: '{spring-framework-github}/tree/7.0.x'
|
||||
spring-framework-code: '{spring-framework-github}/tree/main'
|
||||
spring-framework-issues: '{spring-framework-github}/issues'
|
||||
spring-framework-wiki: '{spring-framework-github}/wiki'
|
||||
# Docs
|
||||
|
||||
@@ -560,6 +560,14 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
When collection auto-growing is enabled, a collection cannot automatically grow beyond
|
||||
256 elements by default; however, the `maximumAutoGrowSize` value is configurable. This
|
||||
default is aligned with `DataBinder.DEFAULT_AUTO_GROW_COLLECTION_LIMIT`, for consistency
|
||||
with the auto-grow limit used for data binding in Spring MVC and Spring WebFlux. If you
|
||||
create a `SpelExpressionParser` programmatically, you can specify a custom
|
||||
`maximumAutoGrowSize` when creating the `SpelParserConfiguration` that you provide to the
|
||||
`SpelExpressionParser`.
|
||||
|
||||
By default, a SpEL expression cannot contain more than 10,000 characters; however, the
|
||||
`maxExpressionLength` is configurable. If you create a `SpelExpressionParser`
|
||||
programmatically, you can specify a custom `maxExpressionLength` via
|
||||
@@ -598,6 +606,32 @@ not able to configure an explicit value for `maximumBigPowerBits` via
|
||||
`spring.expression.maxBigPowerBits` to the maximum result size in bits (see
|
||||
xref:appendix.adoc#appendix-spring-properties[Supported Spring Properties]).
|
||||
|
||||
Likewise, the structural nesting depth of a SpEL expression -- for example, the depth of
|
||||
nested inline lists or maps, parenthesized expressions, ternary or Elvis expressions, or
|
||||
chained unary operators -- cannot exceed 1,000 by default; however, the
|
||||
`maximumNestingDepth` value is configurable. If you create a `SpelExpressionParser`
|
||||
programmatically, you can specify a custom `maximumNestingDepth` value via
|
||||
`SpelParserConfiguration.builder().maximumNestingDepth(...)` when creating the
|
||||
`SpelParserConfiguration` that you provide to the `SpelExpressionParser`. Unlike
|
||||
`maxExpressionLength` and `maxOperations`, there is currently no JVM system property or
|
||||
Spring property available for configuring `maximumNestingDepth` globally.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Without such a limit, a sufficiently deeply nested expression can drive SpEL's
|
||||
recursive-descent parser to exhaust the current thread's call stack, resulting in a
|
||||
`StackOverflowError` instead of a descriptive exception.
|
||||
|
||||
The `maximumNestingDepth` limit improves diagnostics for that common case by converting
|
||||
it into a clear `SpelParseException`; however, it is not a guaranteed defense against
|
||||
`StackOverflowError` under every possible JVM thread stack size configuration, since the
|
||||
amount of stack space consumed per level of nesting depends on the JVM, its JIT
|
||||
compilation state, and the platform. Applications and frameworks that evaluate SpEL
|
||||
expressions from an untrusted source should not rely on `maximumNestingDepth` alone and
|
||||
should instead heed the <<expressions-evaluation-context-security,security
|
||||
considerations>> discussed previously in this chapter.
|
||||
====
|
||||
|
||||
[[expressions-spel-compilation]]
|
||||
== SpEL Compilation
|
||||
|
||||
@@ -737,7 +771,6 @@ following kinds of expressions cannot be compiled.
|
||||
* Expressions relying on the conversion service
|
||||
* Expressions using custom resolvers
|
||||
* Expressions using overloaded operators
|
||||
* Expressions using `Optional` with the null-safe or Elvis operator
|
||||
* Expressions using array construction syntax
|
||||
* Expressions using selection or projection
|
||||
* Expressions using bean references
|
||||
|
||||
@@ -61,7 +61,8 @@ corresponding implementation (`BeanWrapperImpl`). As quoted from the javadoc, th
|
||||
`BeanWrapper` offers functionality to set and get property values (individually or in
|
||||
bulk), get property descriptors, and query properties to determine if they are
|
||||
readable or writable. Also, the `BeanWrapper` offers support for nested properties,
|
||||
enabling the setting of properties on sub-properties to an unlimited depth. The
|
||||
enabling the setting of properties on sub-properties up to a
|
||||
<<data-binding-nested-path-depth,configurable maximum nesting depth>>. The
|
||||
`BeanWrapper` also supports the ability to add standard JavaBeans `PropertyChangeListeners`
|
||||
and `VetoableChangeListeners`, without the need for supporting code in the target class.
|
||||
Last but not least, the `BeanWrapper` provides support for setting indexed properties.
|
||||
@@ -236,6 +237,48 @@ Kotlin::
|
||||
======
|
||||
|
||||
|
||||
[[data-binding-nested-path-depth]]
|
||||
=== Maximum Nesting Depth for Nested Property Paths
|
||||
|
||||
A nested property path such as `managingDirector.salary` is resolved recursively, one
|
||||
level per nested property. The nesting depth of a property path therefore corresponds to
|
||||
the number of intermediate properties that must be traversed in order to reach the final
|
||||
property -- for example, `address.country.name` has a nesting depth of 2, since the
|
||||
`address` and `country` properties must be traversed in order to reach the `name`
|
||||
property.
|
||||
|
||||
The nesting depth of a property path cannot exceed 100 by default; however, the
|
||||
`maxNestedPathDepth` value is configurable. You can specify a custom value via
|
||||
`setMaxNestedPathDepth(...)` on a `ConfigurablePropertyAccessor` such as
|
||||
`BeanWrapperImpl`, or on a `DataBinder` -- and therefore also on a `WebDataBinder`, for
|
||||
example within an `@InitBinder` method. If a property path exceeds the configured limit,
|
||||
an `InvalidPropertyException` is thrown. Specify `0` to disable support for nested
|
||||
property paths altogether, while continuing to allow simple, indexed, and mapped property
|
||||
access -- for example, `name`, `accounts[2]`, or `accounts[KEY]`.
|
||||
|
||||
Note that this limit applies to property binding as well as to
|
||||
<<data-binding-constructor-binding,constructor binding>> via `DataBinder.construct`,
|
||||
since a constructor parameter which is itself an object is constructed recursively
|
||||
through a nested property path.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Without such a limit, a sufficiently deeply nested property path can drive the recursive
|
||||
resolution of nested property paths to exhaust the current thread's call stack, resulting
|
||||
in a `StackOverflowError` instead of a descriptive exception.
|
||||
|
||||
The `maxNestedPathDepth` limit improves diagnostics for that common case by converting
|
||||
it into a clear `InvalidPropertyException`; however, it is not a guaranteed defense against
|
||||
`StackOverflowError` under every possible JVM thread stack size configuration, since the
|
||||
amount of stack space consumed per level of nesting depends on the JVM, its JIT
|
||||
compilation state, and the platform.
|
||||
|
||||
When binding untrusted input, you should additionally constrain binding to the expected
|
||||
input as described in
|
||||
xref:web/webmvc/mvc-data-binding.adoc#mvc-data-binding-design[Model Design].
|
||||
====
|
||||
|
||||
|
||||
[[data-binding-conversion]]
|
||||
== ``PropertyEditor``s
|
||||
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
|
||||
The `org.springframework.jdbc.datasource.embedded` package provides support for embedded
|
||||
Java database engines. Support for https://www.hsqldb.org[HSQL],
|
||||
https://www.h2database.com[H2], and https://db.apache.org/derby[Derby] is provided
|
||||
natively. You can also use an extensible API to plug in new embedded database types and
|
||||
`DataSource` implementations.
|
||||
and https://www.h2database.com[H2] is provided natively.
|
||||
You can also use an extensible API to plug in new embedded database types and `DataSource` implementations.
|
||||
|
||||
|
||||
[[jdbc-why-embedded-database]]
|
||||
@@ -41,7 +40,6 @@ supports. It includes the following topics:
|
||||
|
||||
* <<jdbc-embedded-database-using-HSQL,Using HSQL>>
|
||||
* <<jdbc-embedded-database-using-H2,Using H2>>
|
||||
* <<jdbc-embedded-database-using-Derby,Using Derby>>
|
||||
|
||||
[[jdbc-embedded-database-using-HSQL]]
|
||||
=== Using HSQL
|
||||
@@ -58,13 +56,6 @@ Spring supports the H2 database. To enable H2, set the `type` attribute of the
|
||||
`embedded-database` tag to `H2`. If you use the builder API, call the
|
||||
`setType(EmbeddedDatabaseType)` method with `EmbeddedDatabaseType.H2`.
|
||||
|
||||
[[jdbc-embedded-database-using-Derby]]
|
||||
=== Using Derby
|
||||
|
||||
Spring supports Apache Derby 10.5 and above. To enable Derby, set the `type`
|
||||
attribute of the `embedded-database` tag to `DERBY`. If you use the builder API,
|
||||
call the `setType(EmbeddedDatabaseType)` method with `EmbeddedDatabaseType.DERBY`.
|
||||
|
||||
|
||||
[[jdbc-embedded-database-types-custom]]
|
||||
== Customizing the Embedded Database Type
|
||||
|
||||
@@ -17,7 +17,7 @@ xref:data-access/jdbc/simple.adoc[Simplifying JDBC Operations with the `SimpleJd
|
||||
for easy `DataSource` access and various simple `DataSource` implementations that you can
|
||||
use for testing and running unmodified JDBC code outside of a Jakarta EE container. A subpackage
|
||||
named `org.springframework.jdbc.datasource.embedded` provides support for creating
|
||||
embedded databases by using Java database engines, such as HSQL, H2, and Derby. See
|
||||
embedded databases by using Java database engines, such as HSQL and H2. See
|
||||
xref:data-access/jdbc/connections.adoc[Controlling Database Connections] and
|
||||
xref:data-access/jdbc/embedded-database-support.adoc[Embedded Database Support].
|
||||
|
||||
|
||||
@@ -483,7 +483,7 @@ as input. See the <<jdbc-params,next section>> for details on how to define an `
|
||||
|
||||
NOTE: Explicit declarations are necessary if the database you use is not a Spring-supported
|
||||
database. Currently, Spring supports metadata lookup of stored procedure calls for the
|
||||
following databases: Apache Derby, DB2, MySQL, Microsoft SQL Server, Oracle, and Sybase.
|
||||
following databases: DB2, MySQL, Microsoft SQL Server, Oracle, and Sybase.
|
||||
We also support metadata lookup of stored functions for MySQL, Microsoft SQL Server,
|
||||
and Oracle.
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ example writes notifications to the console:
|
||||
}
|
||||
|
||||
public boolean isNotificationEnabled(Notification notification) {
|
||||
return AttributeChangeNotification.class.isAssignableFrom(notification.getClass());
|
||||
return (notification instanceof AttributeChangeNotification);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ The Spring Framework provides the following choices for making calls to REST end
|
||||
`RestClient` is a synchronous HTTP client that provides a fluent API to perform requests.
|
||||
It serves as an abstraction over HTTP libraries, and handles conversion of HTTP request and response content to and from higher level Java objects.
|
||||
|
||||
[[rest-restclient.create]]
|
||||
=== Create a `RestClient`
|
||||
|
||||
`RestClient` has static `create` shortcut methods.
|
||||
@@ -32,48 +33,7 @@ Once created, a `RestClient` is safe to use in multiple threads.
|
||||
|
||||
The below shows how to create or build a `RestClient`:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim"]
|
||||
----
|
||||
RestClient defaultClient = RestClient.create();
|
||||
|
||||
RestClient customClient = RestClient.builder()
|
||||
.requestFactory(new HttpComponentsClientHttpRequestFactory())
|
||||
.messageConverters(converters -> converters.add(new MyCustomMessageConverter()))
|
||||
.baseUrl("https://example.com")
|
||||
.defaultUriVariables(Map.of("variable", "foo"))
|
||||
.defaultHeader("My-Header", "Foo")
|
||||
.defaultCookie("My-Cookie", "Bar")
|
||||
.defaultVersion("1.2")
|
||||
.apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build())
|
||||
.requestInterceptor(myCustomInterceptor)
|
||||
.requestInitializer(myCustomInitializer)
|
||||
.build();
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim"]
|
||||
----
|
||||
val defaultClient = RestClient.create()
|
||||
|
||||
val customClient = RestClient.builder()
|
||||
.requestFactory(HttpComponentsClientHttpRequestFactory())
|
||||
.messageConverters { converters -> converters.add(MyCustomMessageConverter()) }
|
||||
.baseUrl("https://example.com")
|
||||
.defaultUriVariables(mapOf("variable" to "foo"))
|
||||
.defaultHeader("My-Header", "Foo")
|
||||
.defaultCookie("My-Cookie", "Bar")
|
||||
.defaultVersion("1.2")
|
||||
.apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build())
|
||||
.requestInterceptor(myCustomInterceptor)
|
||||
.requestInitializer(myCustomInitializer)
|
||||
.build()
|
||||
----
|
||||
======
|
||||
include-code::./RestClientCreation[tag=snippet,indent=0]
|
||||
|
||||
=== Use the `RestClient`
|
||||
|
||||
@@ -390,47 +350,42 @@ xref:web/webmvc/message-converters.adoc#message-converters[See the supported HTT
|
||||
|
||||
To serialize only a subset of the object properties, you can specify a {baeldung-blog}/jackson-json-view-annotation[Jackson JSON View], as the following example shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim"]
|
||||
----
|
||||
MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23"));
|
||||
value.setSerializationView(User.WithoutPasswordView.class);
|
||||
|
||||
ResponseEntity<Void> response = restClient.post() // or RestTemplate.postForEntity
|
||||
.contentType(APPLICATION_JSON)
|
||||
.body(value)
|
||||
.retrieve()
|
||||
.toBodilessEntity();
|
||||
----
|
||||
include-code::./../restmessageconversion/RestClientMessageConversion[tag=jsonview,indent=0]
|
||||
|
||||
==== URL encoded Forms
|
||||
|
||||
URL encoded forms, using the `"application/x-www-form-urlencoded"` media type, are useful for sending String key/values over the wire.
|
||||
This is supported by the `FormHttpMessageConverter`, if the application uses a `MultiValueMap<String, String>` as source instance
|
||||
or a target type.
|
||||
|
||||
For example:
|
||||
|
||||
include-code::./../restmessageconversion/RestClientMessageConversion[tag=urlencodedform,indent=0]
|
||||
|
||||
|
||||
==== Multipart
|
||||
|
||||
To send multipart data, you need to provide a `MultiValueMap<String, Object>` whose values may be an `Object` for part content, a `Resource` for a file part, or an `HttpEntity` for part content with headers.
|
||||
For example:
|
||||
|
||||
[source,java,indent=0,subs="verbatim"]
|
||||
----
|
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
|
||||
parts.add("fieldPart", "fieldValue");
|
||||
parts.add("filePart", new FileSystemResource("...logo.png"));
|
||||
parts.add("jsonPart", new Person("Jason"));
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_XML);
|
||||
parts.add("xmlPart", new HttpEntity<>(myBean, headers));
|
||||
|
||||
// send using RestClient.post or RestTemplate.postForEntity
|
||||
----
|
||||
include-code::./../restmessageconversion/RestClientMessageConversion[tag=multipartrequest,indent=0]
|
||||
|
||||
In most cases, you do not have to specify the `Content-Type` for each part.
|
||||
The content type is determined automatically based on the `HttpMessageConverter` chosen to serialize it or, in the case of a `Resource`, based on the file extension.
|
||||
If necessary, you can explicitly provide the `MediaType` with an `HttpEntity` wrapper.
|
||||
|
||||
Once the `MultiValueMap` is ready, you can use it as the body of a `POST` request, using `RestClient.post().body(parts)` (or `RestTemplate.postForObject`).
|
||||
The `Content-Type` is set to `multipart/form-data` by the `MultipartHttpMessageConverter`.
|
||||
As seen in the previous section, `MultiValueMap` types can also be used for URL encoded forms.
|
||||
It is preferable to explicitly set the media type in the `Content-Type` or `Accept` HTTP request headers to ensure that the expected
|
||||
message converter is used.
|
||||
|
||||
`RestClient` can also receive multipart responses.
|
||||
To decode a multipart response body, use a `ParameterizedTypeReference<MultiValueMap<String, Part>>`.
|
||||
The decoded map contains `Part` instances where `FormFieldPart` represents form field values
|
||||
and `FilePart` represents file parts with a `filename()` and a `transferTo()` method.
|
||||
|
||||
include-code::./../restmessageconversion/RestClientMessageConversion[tag=multipartresponse,indent=0]
|
||||
|
||||
If the `MultiValueMap` contains at least one non-`String` value, the `Content-Type` is set to `multipart/form-data` by the `FormHttpMessageConverter`.
|
||||
If the `MultiValueMap` has `String` values, the `Content-Type` defaults to `application/x-www-form-urlencoded`.
|
||||
If necessary the `Content-Type` may also be set explicitly.
|
||||
|
||||
[[rest-request-factories]]
|
||||
=== Client Request Factories
|
||||
|
||||
+158
-6
@@ -12,18 +12,20 @@ The annotations can be applied in the following ways.
|
||||
* On a non-static field in a test class or any of its superclasses.
|
||||
* On a non-static field in an enclosing class for a `@Nested` test class or in any class
|
||||
in the type hierarchy or enclosing class hierarchy above the `@Nested` test class.
|
||||
* On a parameter in the constructor for a test class.
|
||||
* At the type level on a test class or any superclass or implemented interface in the
|
||||
type hierarchy above the test class.
|
||||
* At the type level on an enclosing class for a `@Nested` test class or on any class or
|
||||
interface in the type hierarchy or enclosing class hierarchy above the `@Nested` test
|
||||
class.
|
||||
|
||||
When `@MockitoBean` or `@MockitoSpyBean` is declared on a field, the bean to mock or spy
|
||||
is inferred from the type of the annotated field. If multiple candidates exist in the
|
||||
`ApplicationContext`, a `@Qualifier` annotation can be declared on the field to help
|
||||
disambiguate. In the absence of a `@Qualifier` annotation, the name of the annotated
|
||||
field will be used as a _fallback qualifier_. Alternatively, you can explicitly specify a
|
||||
bean name to mock or spy by setting the `value` or `name` attribute in the annotation.
|
||||
When `@MockitoBean` or `@MockitoSpyBean` is declared on a field or constructor parameter,
|
||||
the bean to mock or spy is inferred from the type of the annotated field or parameter. If
|
||||
multiple candidates exist in the `ApplicationContext`, a `@Qualifier` annotation can be
|
||||
declared on the field or parameter to help disambiguate. In the absence of a `@Qualifier`
|
||||
annotation, the name of the annotated field or parameter will be used as a _fallback
|
||||
qualifier_. Alternatively, you can explicitly specify a bean name to mock or spy by
|
||||
setting the `value` or `name` attribute in the annotation.
|
||||
|
||||
When `@MockitoBean` or `@MockitoSpyBean` is declared at the type level, the type of bean
|
||||
(or beans) to mock or spy must be supplied via the `types` attribute in the annotation –
|
||||
@@ -211,6 +213,82 @@ Kotlin::
|
||||
<1> Replace the bean named `service` with a Mockito mock.
|
||||
======
|
||||
|
||||
The following example shows how to use `@MockitoBean` on a constructor parameter for a
|
||||
by-type lookup.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig(TestConfig.class)
|
||||
class BeanOverrideTests {
|
||||
|
||||
private final CustomService customService;
|
||||
|
||||
BeanOverrideTests(@MockitoBean CustomService customService) { // <1>
|
||||
this.customService = customService;
|
||||
}
|
||||
|
||||
// tests...
|
||||
}
|
||||
----
|
||||
<1> Replace the bean with type `CustomService` with a Mockito mock and inject it into
|
||||
the constructor.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig(TestConfig::class)
|
||||
class BeanOverrideTests(@MockitoBean val customService: CustomService) { // <1>
|
||||
|
||||
// tests...
|
||||
}
|
||||
----
|
||||
<1> Replace the bean with type `CustomService` with a Mockito mock and inject it into
|
||||
the constructor.
|
||||
======
|
||||
|
||||
The following example shows how to use `@MockitoBean` on a constructor parameter for a
|
||||
by-name lookup.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig(TestConfig.class)
|
||||
class BeanOverrideTests {
|
||||
|
||||
private final CustomService customService;
|
||||
|
||||
BeanOverrideTests(@MockitoBean("service") CustomService customService) { // <1>
|
||||
this.customService = customService;
|
||||
}
|
||||
|
||||
// tests...
|
||||
}
|
||||
----
|
||||
<1> Replace the bean named `service` with a Mockito mock and inject it into the
|
||||
constructor.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig(TestConfig::class)
|
||||
class BeanOverrideTests(@MockitoBean("service") val customService: CustomService) { // <1>
|
||||
|
||||
// tests...
|
||||
}
|
||||
----
|
||||
<1> Replace the bean named `service` with a Mockito mock and inject it into the
|
||||
constructor.
|
||||
======
|
||||
|
||||
The following `@SharedMocks` annotation registers two mocks by-type and one mock by-name.
|
||||
|
||||
[tabs]
|
||||
@@ -385,6 +463,80 @@ Kotlin::
|
||||
<1> Wrap the bean named `service` with a Mockito spy.
|
||||
======
|
||||
|
||||
The following example shows how to use `@MockitoSpyBean` on a constructor parameter for
|
||||
a by-type lookup.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig(TestConfig.class)
|
||||
class BeanOverrideTests {
|
||||
|
||||
private final CustomService customService;
|
||||
|
||||
BeanOverrideTests(@MockitoSpyBean CustomService customService) { // <1>
|
||||
this.customService = customService;
|
||||
}
|
||||
|
||||
// tests...
|
||||
}
|
||||
----
|
||||
<1> Wrap the bean with type `CustomService` with a Mockito spy and inject it into the
|
||||
constructor.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig(TestConfig::class)
|
||||
class BeanOverrideTests(@MockitoSpyBean val customService: CustomService) { // <1>
|
||||
|
||||
// tests...
|
||||
}
|
||||
----
|
||||
<1> Wrap the bean with type `CustomService` with a Mockito spy and inject it into the
|
||||
constructor.
|
||||
======
|
||||
|
||||
The following example shows how to use `@MockitoSpyBean` on a constructor parameter for
|
||||
a by-name lookup.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig(TestConfig.class)
|
||||
class BeanOverrideTests {
|
||||
|
||||
private final CustomService customService;
|
||||
|
||||
BeanOverrideTests(@MockitoSpyBean("service") CustomService customService) { // <1>
|
||||
this.customService = customService;
|
||||
}
|
||||
|
||||
// tests...
|
||||
}
|
||||
----
|
||||
<1> Wrap the bean named `service` with a Mockito spy and inject it into the constructor.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig(TestConfig::class)
|
||||
class BeanOverrideTests(@MockitoSpyBean("service") val customService: CustomService) { // <1>
|
||||
|
||||
// tests...
|
||||
}
|
||||
----
|
||||
<1> Wrap the bean named `service` with a Mockito spy and inject it into the constructor.
|
||||
======
|
||||
|
||||
The following `@SharedSpies` annotation registers two spies by-type and one spy by-name.
|
||||
|
||||
[tabs]
|
||||
|
||||
@@ -142,6 +142,9 @@ provides two alternative ways to verify the response:
|
||||
1. <<resttestclient.workflow,Built-in Assertions>> extend the request workflow with a chain of expectations
|
||||
2. <<resttestclient.assertj,AssertJ Integration>> to verify the response via `assertThat()` statements
|
||||
|
||||
TIP: See the xref:integration/rest-clients.adoc#rest-message-conversion[HTTP Message Conversion]
|
||||
section for examples on how to prepare a request with any content, including form data and multipart data.
|
||||
|
||||
|
||||
|
||||
[[resttestclient.workflow]]
|
||||
@@ -213,6 +216,16 @@ To verify JSON content with https://github.com/jayway/JsonPath[JSONPath]:
|
||||
include-code::./JsonTests[tag=jsonPath,indent=0]
|
||||
|
||||
|
||||
[[resttestclient.multipart]]
|
||||
==== Multipart Content
|
||||
|
||||
When testing endpoints that return multipart responses, you can decode the body to a
|
||||
`MultiValueMap<String, Part>` and assert individual parts using the `FormFieldPart`
|
||||
and `FilePart` subtypes.
|
||||
|
||||
include-code::./MultipartTests[tag=multipart,indent=0]
|
||||
|
||||
|
||||
[[resttestclient.assertj]]
|
||||
=== AssertJ Integration
|
||||
|
||||
|
||||
+6
-5
@@ -2,8 +2,9 @@
|
||||
= Bean Overriding in Tests
|
||||
|
||||
Bean overriding in tests refers to the ability to override specific beans in the
|
||||
`ApplicationContext` for a test class, by annotating the test class or one or more
|
||||
non-static fields in the test class.
|
||||
`ApplicationContext` for a test class, by annotating the test class, one or more
|
||||
non-static fields in the test class, or one or more parameters in the constructor for the
|
||||
test class.
|
||||
|
||||
NOTE: This feature is intended as a less risky alternative to the practice of registering
|
||||
a bean via `@Bean` with the `DefaultListableBeanFactory`
|
||||
@@ -42,9 +43,9 @@ The `spring-test` module registers implementations of the latter two
|
||||
{spring-framework-code}/spring-test/src/main/resources/META-INF/spring.factories[`META-INF/spring.factories`
|
||||
properties file].
|
||||
|
||||
The bean overriding infrastructure searches for annotations on test classes as well as
|
||||
annotations on non-static fields in test classes that are meta-annotated with
|
||||
`@BeanOverride` and instantiates the corresponding `BeanOverrideProcessor` which is
|
||||
The bean overriding infrastructure searches for annotations on test classes, non-static
|
||||
fields in test classes, and parameters in test class constructors that are meta-annotated
|
||||
with `@BeanOverride`, and instantiates the corresponding `BeanOverrideProcessor` which is
|
||||
responsible for creating an appropriate `BeanOverrideHandler`.
|
||||
|
||||
The internal `BeanOverrideBeanFactoryPostProcessor` then uses bean override handlers to
|
||||
|
||||
@@ -179,6 +179,10 @@ If a specific parameter in a constructor for a JUnit Jupiter test class is of ty
|
||||
`ApplicationContext` (or a sub-type thereof) or is annotated or meta-annotated with
|
||||
`@Autowired`, `@Qualifier`, or `@Value`, Spring injects the value for that specific
|
||||
parameter with the corresponding bean or value from the test's `ApplicationContext`.
|
||||
Similarly, if a specific parameter is annotated with `@MockitoBean` or `@MockitoSpyBean`,
|
||||
Spring will inject a Mockito mock or spy, respectively — see
|
||||
xref:testing/annotations/integration-spring/annotation-mockitobean.adoc[`@MockitoBean` and `@MockitoSpyBean`]
|
||||
for details.
|
||||
|
||||
Spring can also be configured to autowire all arguments for a test class constructor if
|
||||
the constructor is considered to be _autowirable_. A constructor is considered to be
|
||||
|
||||
@@ -580,8 +580,8 @@ Kotlin::
|
||||
[[webtestclient-stream]]
|
||||
==== Streaming Responses
|
||||
|
||||
To test potentially infinite streams such as `"text/event-stream"` or
|
||||
`"application/x-ndjson"`, start by verifying the response status and headers, and then
|
||||
To test potentially infinite streams such as `"text/event-stream"`,
|
||||
`"application/jsonl"` or `"application/x-ndjson"`, start by verifying the response status and headers, and then
|
||||
obtain a `FluxExchangeResult`:
|
||||
|
||||
[tabs]
|
||||
|
||||
@@ -65,64 +65,4 @@ The exception contains a list of ``ParameterValidationResult``s that group valid
|
||||
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"]
|
||||
----
|
||||
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"]
|
||||
----
|
||||
// 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) {
|
||||
// ...
|
||||
}
|
||||
})
|
||||
----
|
||||
======
|
||||
include-code::./HandlerMethodValidationExceptionVisitor[tag=snippet,indent=0]
|
||||
@@ -352,9 +352,9 @@ outside. A proxy at the edge of trust must remove forwarded headers including bo
|
||||
standard `"Forwarded"` and `"X-Forwarded"` headers, regardless of which one they use,
|
||||
to protect applications which may check both.
|
||||
|
||||
When creating `ForwardedHeaderTransformer` you can specify whether to use the
|
||||
standard `"Forwarded"` or `"X-Forwarded"` headers. A separate property on the transformer
|
||||
lets you turn use of `"X-Forwarded-Prefix"` on and off.
|
||||
When creating `ForwardedHeaderTransformer` you need to specify whether it should use the
|
||||
standard `"Forwarded"` or `"X-Forwarded"` headers. If needed `"X-Forwarded-Prefix"`
|
||||
must be enabled separately through a property on the transformer.
|
||||
|
||||
`ForwardedHeaderTransformer` can be configured in `removeOnly` mode, in which case it removes
|
||||
forwarded headers from the request without using them.
|
||||
@@ -490,8 +490,8 @@ The `JacksonJsonEncoder` works as follows:
|
||||
* For a multi-value publisher with `application/json`, by default collect the values with
|
||||
`Flux#collectToList()` and then serialize the resulting collection.
|
||||
* For a multi-value publisher with a streaming media type such as
|
||||
`application/x-ndjson` or `application/stream+x-jackson-smile`, encode, write, and
|
||||
flush each value individually using a
|
||||
`application/jsonl`, `application/x-ndjson` or `application/stream+x-jackson-smile`,
|
||||
encode, write, and flush each value individually using a
|
||||
https://en.wikipedia.org/wiki/JSON_streaming[line-delimited JSON] format. Other
|
||||
streaming media types may be registered with the encoder.
|
||||
* For SSE the `JacksonJsonEncoder` is invoked per event and the output is flushed to ensure
|
||||
@@ -603,7 +603,7 @@ To configure all three in WebFlux, you'll need to supply a pre-configured instan
|
||||
[.small]#xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-http-streaming[See equivalent in the Servlet stack]#
|
||||
|
||||
When streaming to the HTTP response (for example, `text/event-stream`,
|
||||
`application/x-ndjson`), it is important to send data periodically, in order to
|
||||
`application/jsonl`, `application/x-ndjson`), it is important to send data periodically, in order to
|
||||
reliably detect a disconnected client sooner rather than later. Such a send could be a
|
||||
comment-only, empty SSE event or any other "no-op" data that would effectively serve as
|
||||
a heartbeat.
|
||||
|
||||
@@ -66,9 +66,9 @@ outside. A proxy at the edge of trust must remove forwarded headers including bo
|
||||
standard `"Forwarded"` and `"X-Forwarded"` headers, regardless of which one they use,
|
||||
to protect applications which may check both.
|
||||
|
||||
When creating `ForwardedHeaderFilter` you can specify whether to use the
|
||||
standard `"Forwarded"` or `"X-Forwarded"` headers. A separate property on the filter
|
||||
lets you turn use of `"X-Forwarded-Prefix"` on and off.
|
||||
When creating `ForwardedHeaderFilter` you need to specify whether it should use the
|
||||
standard `"Forwarded"` or `"X-Forwarded"` headers. If needed `"X-Forwarded-Prefix"`
|
||||
must be enabled separately through a property on the filter.
|
||||
|
||||
`ForwardedHeaderFilter` can be configured in `removeOnly` mode, in which case it removes
|
||||
forwarded headers from the request without using them.
|
||||
|
||||
@@ -23,13 +23,17 @@ For all converters, a default media type is used, but you can override it by set
|
||||
By default, this converter supports all text media types(`text/{asterisk}`) and writes with a `Content-Type` of `text/plain`.
|
||||
|
||||
| `FormHttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write form data from the HTTP request and response.
|
||||
| An `HttpMessageConverter` implementation that can read and write URL encoded forms.
|
||||
By default, this converter reads and writes the `application/x-www-form-urlencoded` media type.
|
||||
Form data is read from and written into a `MultiValueMap<String, String>`.
|
||||
The converter can also write (but not read) multipart data read from a `MultiValueMap<String, Object>`.
|
||||
By default, `multipart/form-data` is supported.
|
||||
Additional multipart subtypes can be supported for writing form data.
|
||||
Consult the javadoc for `FormHttpMessageConverter` for further details.
|
||||
`Map<String, String>` is also supported, but multiple values under the same key will be ignored.
|
||||
|
||||
| `MultipartHttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write multipart messages.
|
||||
`MultiValueMap<String, Object>` can be written to multipart messages, converting each part independently using
|
||||
the configured message converters. Multipart messages can be read into `MultiValueMap<String, Part>`, each value
|
||||
being a `Part` or one of its subtypes (`FormFieldPart` and `FilePart`).
|
||||
By default, `multipart/form-data` is supported. Additional multipart subtypes can be supported for writing form data.
|
||||
|
||||
| `ByteArrayHttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write byte arrays from the HTTP request and response.
|
||||
|
||||
@@ -423,8 +423,8 @@ Reactive return values are handled as follows:
|
||||
|
||||
* A single-value promise is adapted to, similar to using `DeferredResult`. Examples
|
||||
include `CompletionStage` (JDK), `Mono` (Reactor), and `Single` (RxJava).
|
||||
* A multi-value stream with a streaming media type (such as `application/x-ndjson`
|
||||
or `text/event-stream`) is adapted to, similar to using `ResponseBodyEmitter` or
|
||||
* A multi-value stream with a streaming media type (such as `application/jsonl`,
|
||||
`application/x-ndjson` or `text/event-stream`) is adapted to, similar to using `ResponseBodyEmitter` or
|
||||
`SseEmitter`. Examples include `Flux` (Reactor) or `Observable` (RxJava).
|
||||
Applications can also return `Flux<ServerSentEvent>` or `Observable<ServerSentEvent>`.
|
||||
* A multi-value stream with any other media type (such as `application/json`) is adapted
|
||||
|
||||
@@ -65,64 +65,4 @@ The exception contains a list of ``ParameterValidationResult``s that group valid
|
||||
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"]
|
||||
----
|
||||
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"]
|
||||
----
|
||||
// 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) {
|
||||
// ...
|
||||
}
|
||||
})
|
||||
----
|
||||
======
|
||||
include-code::./HandlerMethodValidationExceptionVisitor[tag=snippet,indent=0]
|
||||
@@ -28,9 +28,9 @@ For example:
|
||||
}
|
||||
----
|
||||
|
||||
NOTE: It is also possible to configure `disallowedFields`, but that's fragile, and
|
||||
due to be https://github.com/spring-projects/spring-framework/issues/36802[deprecated] in Spring Framework 7.1.
|
||||
It is easy to overlook fields or introduce additional fields over time that should also be excluded.
|
||||
NOTE: The `disallowedFields` property has been
|
||||
https://github.com/spring-projects/spring-framework/issues/36802[deprecated in Spring Framework 7.1]
|
||||
because it is fragile and easy to get out of sync with the actual properties over time.
|
||||
|
||||
The patterns given to `allowedFields` and `disallowedFields` are not limited to top-level
|
||||
field names. They are property paths, using the same syntax supported for reading and
|
||||
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Copyright 2002-present 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.restmessageconversion;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonView;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.converter.multipart.FilePart;
|
||||
import org.springframework.http.converter.multipart.FormFieldPart;
|
||||
import org.springframework.http.converter.multipart.Part;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON;
|
||||
|
||||
public class RestClientMessageConversion {
|
||||
|
||||
private final RestClient restClient = RestClient.create();
|
||||
|
||||
private final Object myBean = new Object();
|
||||
|
||||
void useJsonView() {
|
||||
// tag::jsonview[]
|
||||
User user = new User("eric", "7!jd#h23");
|
||||
|
||||
ResponseEntity<Void> response = this.restClient.post()
|
||||
.contentType(APPLICATION_JSON)
|
||||
.body(user)
|
||||
.hint(JsonView.class.getName(), User.WithoutPasswordView.class)
|
||||
.retrieve()
|
||||
.toBodilessEntity();
|
||||
// end::jsonview[]
|
||||
}
|
||||
|
||||
void sendUrlEncodedForm() {
|
||||
// tag::urlencodedform[]
|
||||
MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
|
||||
form.add("project", "Spring Framework");
|
||||
form.add("module", "spring-web");
|
||||
ResponseEntity<Void> response = this.restClient.post()
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.body(form)
|
||||
.retrieve()
|
||||
.toBodilessEntity();
|
||||
// end::urlencodedform[]
|
||||
}
|
||||
|
||||
void sendMultipartData() {
|
||||
// tag::multipartrequest[]
|
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
|
||||
parts.add("fieldPart", "fieldValue");
|
||||
parts.add("filePart", new FileSystemResource("...logo.png"));
|
||||
parts.add("jsonPart", new Person("Jason"));
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_XML);
|
||||
parts.add("xmlPart", new HttpEntity<>(this.myBean, headers));
|
||||
|
||||
ResponseEntity<Void> response = this.restClient.post()
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA)
|
||||
.body(parts)
|
||||
.retrieve()
|
||||
.toBodilessEntity();
|
||||
// end::multipartrequest[]
|
||||
}
|
||||
|
||||
void receiveMultipartData() throws IOException {
|
||||
// tag::multipartresponse[]
|
||||
MultiValueMap<String, Part> result = this.restClient.get()
|
||||
.uri("https://example.com/upload")
|
||||
.accept(MediaType.MULTIPART_FORM_DATA)
|
||||
.retrieve()
|
||||
.body(new ParameterizedTypeReference<>() {});
|
||||
|
||||
Part field = result.getFirst("fieldPart");
|
||||
if (field instanceof FormFieldPart formField) {
|
||||
String fieldValue = formField.value();
|
||||
}
|
||||
Part file = result.getFirst("filePart");
|
||||
if (file instanceof FilePart filePart) {
|
||||
filePart.transferTo(Path.of("/tmp/" + filePart.filename()));
|
||||
}
|
||||
// end::multipartresponse[]
|
||||
}
|
||||
|
||||
public static class User {
|
||||
|
||||
private String username;
|
||||
private String password;
|
||||
|
||||
public User() {
|
||||
}
|
||||
|
||||
public User(String username, String password) {
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
@JsonView(WithoutPasswordView.class)
|
||||
public String getUsername() {
|
||||
return this.username;
|
||||
}
|
||||
|
||||
@JsonView(WithPasswordView.class)
|
||||
public String getPassword() {
|
||||
return this.password;
|
||||
}
|
||||
|
||||
public interface WithoutPasswordView {
|
||||
}
|
||||
|
||||
public interface WithPasswordView extends WithoutPasswordView {
|
||||
}
|
||||
}
|
||||
|
||||
private record Person(String name) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2002-present 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.restrestclient.create;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.http.HttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequest;
|
||||
import org.springframework.http.client.ClientHttpRequestExecution;
|
||||
import org.springframework.http.client.ClientHttpRequestInitializer;
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||
import org.springframework.web.client.ApiVersionInserter;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
public class RestClientCreation {
|
||||
|
||||
void createRestClient() {
|
||||
// tag::snippet[]
|
||||
RestClient defaultClient = RestClient.create();
|
||||
|
||||
RestClient customClient = RestClient.builder()
|
||||
.requestFactory(new HttpComponentsClientHttpRequestFactory())
|
||||
.configureMessageConverters(converters -> converters.addCustomConverter(new MyCustomMessageConverter()))
|
||||
.baseUrl("https://example.com")
|
||||
.defaultUriVariables(Map.of("variable", "foo"))
|
||||
.defaultHeader("My-Header", "Foo")
|
||||
.defaultCookie("My-Cookie", "Bar")
|
||||
.defaultApiVersion("1.2")
|
||||
.apiVersionInserter(ApiVersionInserter.useHeader("API-Version"))
|
||||
.requestInterceptor(new MyCustomInterceptor())
|
||||
.requestInitializer(new MyCustomInitializer())
|
||||
.build();
|
||||
// end::snippet[]
|
||||
}
|
||||
|
||||
private static class MyCustomMessageConverter extends StringHttpMessageConverter {
|
||||
}
|
||||
|
||||
private static class MyCustomInterceptor implements ClientHttpRequestInterceptor {
|
||||
|
||||
@Override
|
||||
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
|
||||
ClientHttpRequestExecution execution) throws IOException {
|
||||
|
||||
return execution.execute(request, body);
|
||||
}
|
||||
}
|
||||
|
||||
private static class MyCustomInitializer implements ClientHttpRequestInitializer {
|
||||
|
||||
@Override
|
||||
public void initialize(ClientHttpRequest request) {
|
||||
request.getHeaders().add("My-Header", "My-Value");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2025-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.testing.resttestclient.multipart;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.multipart.FilePart;
|
||||
import org.springframework.http.converter.multipart.FormFieldPart;
|
||||
import org.springframework.http.converter.multipart.Part;
|
||||
import org.springframework.test.web.servlet.client.RestTestClient;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class MultipartTests {
|
||||
|
||||
RestTestClient client;
|
||||
|
||||
@Test
|
||||
void multipart() {
|
||||
// tag::multipart[]
|
||||
client.get().uri("/upload")
|
||||
.accept(MediaType.MULTIPART_FORM_DATA)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(new ParameterizedTypeReference<MultiValueMap<String, Part>>() {})
|
||||
.value(result -> {
|
||||
Part field = result.getFirst("fieldPart");
|
||||
assertThat(field).isInstanceOfSatisfying(FormFieldPart.class,
|
||||
formField -> assertThat(formField.value()).isEqualTo("fieldValue"));
|
||||
Part file = result.getFirst("filePart");
|
||||
assertThat(file).isInstanceOfSatisfying(FilePart.class,
|
||||
filePart -> assertThat(filePart.filename()).isEqualTo("logo.png"));
|
||||
});
|
||||
// end::multipart[]
|
||||
}
|
||||
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.web.webflux.controller.mvcannvalidation;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.springframework.validation.method.ParameterErrors;
|
||||
import org.springframework.validation.method.ParameterValidationResult;
|
||||
import org.springframework.web.bind.annotation.CookieValue;
|
||||
import org.springframework.web.bind.annotation.MatrixVariable;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.method.annotation.HandlerMethodValidationException;
|
||||
|
||||
public class HandlerMethodValidationExceptionVisitor {
|
||||
|
||||
static void main() {
|
||||
// tag::snippet[]
|
||||
HandlerMethodValidationException ex = /**/ new HandlerMethodValidationException(null);
|
||||
|
||||
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) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// @fold:on // ...
|
||||
@Override
|
||||
public void requestPart(RequestPart requestPart, ParameterErrors errors) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cookieValue(CookieValue cookieValue, ParameterValidationResult result) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void matrixVariable(MatrixVariable matrixVariable, ParameterValidationResult result) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pathVariable(PathVariable pathVariable, ParameterValidationResult result) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestBody(RequestBody requestBody, ParameterErrors errors) {
|
||||
|
||||
}
|
||||
// @fold:off
|
||||
|
||||
@Override
|
||||
public void other(ParameterValidationResult result) {
|
||||
// ...
|
||||
}
|
||||
});
|
||||
// end::snippet[]
|
||||
}
|
||||
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.web.webmvc.mvccontroller.mvcannvalidation;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.springframework.validation.method.ParameterErrors;
|
||||
import org.springframework.validation.method.ParameterValidationResult;
|
||||
import org.springframework.web.bind.annotation.CookieValue;
|
||||
import org.springframework.web.bind.annotation.MatrixVariable;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.method.annotation.HandlerMethodValidationException;
|
||||
|
||||
public class HandlerMethodValidationExceptionVisitor {
|
||||
|
||||
static void main() {
|
||||
// tag::snippet[]
|
||||
HandlerMethodValidationException ex = /**/ new HandlerMethodValidationException(null);
|
||||
|
||||
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) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// @fold:on // ...
|
||||
@Override
|
||||
public void requestPart(RequestPart requestPart, ParameterErrors errors) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cookieValue(CookieValue cookieValue, ParameterValidationResult result) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void matrixVariable(MatrixVariable matrixVariable, ParameterValidationResult result) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pathVariable(PathVariable pathVariable, ParameterValidationResult result) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestBody(RequestBody requestBody, ParameterErrors errors) {
|
||||
|
||||
}
|
||||
// @fold:off
|
||||
|
||||
@Override
|
||||
public void other(ParameterValidationResult result) {
|
||||
// ...
|
||||
}
|
||||
});
|
||||
// end::snippet[]
|
||||
}
|
||||
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright 2002-present 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.restmessageconversion
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonView
|
||||
import org.springframework.core.ParameterizedTypeReference
|
||||
import org.springframework.core.io.FileSystemResource
|
||||
import org.springframework.http.HttpEntity
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.MediaType.APPLICATION_JSON
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.http.converter.multipart.FilePart
|
||||
import org.springframework.http.converter.multipart.FormFieldPart
|
||||
import org.springframework.http.converter.multipart.Part
|
||||
import org.springframework.util.LinkedMultiValueMap
|
||||
import org.springframework.util.MultiValueMap
|
||||
import org.springframework.web.client.RestClient
|
||||
import java.nio.file.Path
|
||||
|
||||
class RestClientMessageConversion {
|
||||
|
||||
private val restClient = RestClient.create()
|
||||
|
||||
private val myBean = Any()
|
||||
|
||||
fun useJsonView() {
|
||||
// tag::jsonview[]
|
||||
val user = User("eric", "7!jd#h23")
|
||||
|
||||
val response: ResponseEntity<Void> = restClient.post()
|
||||
.contentType(APPLICATION_JSON)
|
||||
.body(user)
|
||||
.hint(JsonView::class.java.name, User.WithoutPasswordView::class.java)
|
||||
.retrieve()
|
||||
.toBodilessEntity()
|
||||
// end::jsonview[]
|
||||
}
|
||||
|
||||
fun sendUrlEncodedForm() {
|
||||
// tag::urlencodedform[]
|
||||
val form: MultiValueMap<String, String> = LinkedMultiValueMap()
|
||||
form.add("project", "Spring Framework")
|
||||
form.add("module", "spring-web")
|
||||
val response: ResponseEntity<Void> = this.restClient.post()
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.body(form)
|
||||
.retrieve()
|
||||
.toBodilessEntity()
|
||||
// end::urlencodedform[]
|
||||
}
|
||||
|
||||
fun sendMultipartData() {
|
||||
// tag::multipartrequest[]
|
||||
val parts: MultiValueMap<String, Any> = LinkedMultiValueMap()
|
||||
|
||||
parts.add("fieldPart", "fieldValue")
|
||||
parts.add("filePart", FileSystemResource("...logo.png"))
|
||||
parts.add("jsonPart", Person("Jason"))
|
||||
|
||||
val headers = HttpHeaders()
|
||||
headers.contentType = MediaType.APPLICATION_XML
|
||||
parts.add("xmlPart", HttpEntity(myBean, headers))
|
||||
|
||||
val response: ResponseEntity<Void> = this.restClient.post()
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA)
|
||||
.body(parts)
|
||||
.retrieve()
|
||||
.toBodilessEntity()
|
||||
// end::multipartrequest[]
|
||||
}
|
||||
|
||||
fun receiveMultipartData() {
|
||||
// tag::multipartresponse[]
|
||||
val result = this.restClient.get()
|
||||
.uri("https://example.com/upload")
|
||||
.accept(MediaType.MULTIPART_FORM_DATA)
|
||||
.retrieve()
|
||||
.body(object : ParameterizedTypeReference<MultiValueMap<String, Part>>() {})
|
||||
|
||||
val field = result?.getFirst("fieldPart")
|
||||
if (field is FormFieldPart) {
|
||||
val fieldValue = field.value()
|
||||
}
|
||||
val file = result?.getFirst("filePart")
|
||||
if (file is FilePart) {
|
||||
file.transferTo(Path.of("/tmp/" + file.filename()))
|
||||
}
|
||||
// end::multipartresponse[]
|
||||
}
|
||||
|
||||
class User(
|
||||
@JsonView(WithoutPasswordView::class) val username: String,
|
||||
@JsonView(WithPasswordView::class) val password: String) {
|
||||
|
||||
interface WithoutPasswordView
|
||||
interface WithPasswordView : WithoutPasswordView
|
||||
}
|
||||
|
||||
data class Person(val name: String)
|
||||
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2002-present 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.restrestclient.create
|
||||
|
||||
import org.springframework.http.HttpRequest
|
||||
import org.springframework.http.client.ClientHttpRequest
|
||||
import org.springframework.http.client.ClientHttpRequestExecution
|
||||
import org.springframework.http.client.ClientHttpRequestInitializer
|
||||
import org.springframework.http.client.ClientHttpRequestInterceptor
|
||||
import org.springframework.http.client.ClientHttpResponse
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory
|
||||
import org.springframework.http.converter.StringHttpMessageConverter
|
||||
import org.springframework.web.client.ApiVersionInserter
|
||||
import org.springframework.web.client.RestClient
|
||||
|
||||
class RestClientCreation {
|
||||
|
||||
fun createRestClient() {
|
||||
// tag::snippet[]
|
||||
val defaultClient = RestClient.create()
|
||||
|
||||
val customClient = RestClient.builder()
|
||||
.requestFactory(HttpComponentsClientHttpRequestFactory())
|
||||
.configureMessageConverters { converters -> converters.addCustomConverter(MyCustomMessageConverter()) }
|
||||
.baseUrl("https://example.com")
|
||||
.defaultUriVariables(mapOf("variable" to "foo"))
|
||||
.defaultHeader("My-Header", "Foo")
|
||||
.defaultCookie("My-Cookie", "Bar")
|
||||
.defaultApiVersion("1.2")
|
||||
.apiVersionInserter(ApiVersionInserter.useHeader("API-Version"))
|
||||
.requestInterceptor(MyCustomInterceptor())
|
||||
.requestInitializer(MyCustomInitializer())
|
||||
.build()
|
||||
// end::snippet[]
|
||||
}
|
||||
|
||||
private class MyCustomMessageConverter : StringHttpMessageConverter()
|
||||
|
||||
private class MyCustomInterceptor : ClientHttpRequestInterceptor {
|
||||
|
||||
override fun intercept(
|
||||
request: HttpRequest,
|
||||
body: ByteArray,
|
||||
execution: ClientHttpRequestExecution
|
||||
): ClientHttpResponse {
|
||||
return execution.execute(request, body)
|
||||
}
|
||||
}
|
||||
|
||||
private class MyCustomInitializer : ClientHttpRequestInitializer {
|
||||
|
||||
override fun initialize(request: ClientHttpRequest) {
|
||||
request.headers.add("My-Header", "My-Value")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.web.webflux.controller.mvcannvalidation
|
||||
|
||||
import org.springframework.context.MessageSourceResolvable
|
||||
import org.springframework.validation.method.MethodValidationResult
|
||||
import org.springframework.validation.method.ParameterErrors
|
||||
import org.springframework.validation.method.ParameterValidationResult
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import org.springframework.web.method.annotation.HandlerMethodValidationException
|
||||
import java.lang.reflect.Method
|
||||
|
||||
class HandlerMethodValidationExceptionVisitor {
|
||||
|
||||
fun main() {
|
||||
// tag::snippet[]
|
||||
val ex: HandlerMethodValidationException = /**/ HandlerMethodValidationException(EmptyMethodValidationResult())
|
||||
|
||||
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) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// @fold:on // ...
|
||||
override fun requestPart(requestPart: RequestPart, errors: ParameterErrors) {
|
||||
}
|
||||
|
||||
override fun cookieValue(cookieValue: CookieValue, result: ParameterValidationResult) {
|
||||
}
|
||||
|
||||
override fun matrixVariable(matrixVariable: MatrixVariable, result: ParameterValidationResult) {
|
||||
}
|
||||
|
||||
override fun pathVariable(pathVariable: PathVariable, result: ParameterValidationResult) {
|
||||
}
|
||||
|
||||
override fun requestBody(requestBody: RequestBody, errors: ParameterErrors) {
|
||||
}
|
||||
// @fold:off
|
||||
|
||||
override fun other(result: ParameterValidationResult) {
|
||||
// ...
|
||||
}
|
||||
})
|
||||
// end::snippet[]
|
||||
}
|
||||
|
||||
internal class EmptyMethodValidationResult : MethodValidationResult {
|
||||
override fun getTarget(): Any {
|
||||
TODO()
|
||||
}
|
||||
|
||||
override fun getMethod(): Method {
|
||||
TODO()
|
||||
}
|
||||
|
||||
override fun isForReturnValue(): Boolean {
|
||||
TODO()
|
||||
}
|
||||
|
||||
override fun getParameterValidationResults(): List<ParameterValidationResult> {
|
||||
TODO()
|
||||
}
|
||||
|
||||
override fun getCrossParameterValidationResults(): List<MessageSourceResolvable> {
|
||||
TODO()
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.web.webmvc.mvccontroller.mvcannvalidation
|
||||
|
||||
import org.springframework.context.MessageSourceResolvable
|
||||
import org.springframework.validation.method.MethodValidationResult
|
||||
import org.springframework.validation.method.ParameterErrors
|
||||
import org.springframework.validation.method.ParameterValidationResult
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import org.springframework.web.method.annotation.HandlerMethodValidationException
|
||||
import java.lang.reflect.Method
|
||||
|
||||
class HandlerMethodValidationExceptionVisitor {
|
||||
|
||||
fun main() {
|
||||
// tag::snippet[]
|
||||
val ex: HandlerMethodValidationException = /**/ HandlerMethodValidationException(EmptyMethodValidationResult())
|
||||
|
||||
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) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// @fold:on // ...
|
||||
override fun requestPart(requestPart: RequestPart, errors: ParameterErrors) {
|
||||
}
|
||||
|
||||
override fun cookieValue(cookieValue: CookieValue, result: ParameterValidationResult) {
|
||||
}
|
||||
|
||||
override fun matrixVariable(matrixVariable: MatrixVariable, result: ParameterValidationResult) {
|
||||
}
|
||||
|
||||
override fun pathVariable(pathVariable: PathVariable, result: ParameterValidationResult) {
|
||||
}
|
||||
|
||||
override fun requestBody(requestBody: RequestBody, errors: ParameterErrors) {
|
||||
}
|
||||
// @fold:off
|
||||
|
||||
override fun other(result: ParameterValidationResult) {
|
||||
// ...
|
||||
}
|
||||
})
|
||||
// end::snippet[]
|
||||
}
|
||||
|
||||
internal class EmptyMethodValidationResult : MethodValidationResult {
|
||||
override fun getTarget(): Any {
|
||||
TODO()
|
||||
}
|
||||
|
||||
override fun getMethod(): Method {
|
||||
TODO()
|
||||
}
|
||||
|
||||
override fun isForReturnValue(): Boolean {
|
||||
TODO()
|
||||
}
|
||||
|
||||
override fun getParameterValidationResults(): List<ParameterValidationResult> {
|
||||
TODO()
|
||||
}
|
||||
|
||||
override fun getCrossParameterValidationResults(): List<MessageSourceResolvable> {
|
||||
TODO()
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,30 +7,30 @@ javaPlatform {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.20.2"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.16.7"))
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.21.6"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.18.0-SNAPSHOT"))
|
||||
api(platform("io.netty:netty-bom:4.2.18.Final"))
|
||||
api(platform("io.projectreactor:reactor-bom:2025.0.7"))
|
||||
api(platform("io.projectreactor:reactor-bom:2026.0.0-SNAPSHOT"))
|
||||
api(platform("io.rsocket:rsocket-bom:1.1.5"))
|
||||
api(platform("org.apache.groovy:groovy-bom:5.0.8"))
|
||||
api(platform("org.apache.groovy:groovy-bom:5.1.2"))
|
||||
api(platform("org.apache.logging.log4j:log4j-bom:2.26.1"))
|
||||
api(platform("org.assertj:assertj-bom:3.27.7"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.1.13"))
|
||||
api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.13"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.2"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.9.0"))
|
||||
api(platform("org.junit:junit-bom:6.0.3"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.11.0"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.11.0"))
|
||||
api(platform("org.junit:junit-bom:6.1.2"))
|
||||
api(platform("org.mockito:mockito-bom:5.23.0"))
|
||||
api(platform("tools.jackson:jackson-bom:3.0.4"))
|
||||
api(platform("tools.jackson:jackson-bom:3.1.6"))
|
||||
|
||||
constraints {
|
||||
api("com.fasterxml:aalto-xml:1.3.4")
|
||||
api("com.fasterxml.woodstox:woodstox-core:6.7.0")
|
||||
api("com.fasterxml:aalto-xml:1.4.0")
|
||||
api("com.fasterxml.woodstox:woodstox-core:7.2.2")
|
||||
api("com.github.ben-manes.caffeine:caffeine:3.2.4")
|
||||
api("com.github.librepdf:openpdf:1.3.43")
|
||||
api("com.google.code.findbugs:findbugs:3.0.1")
|
||||
api("com.google.code.findbugs:jsr305:3.0.2")
|
||||
api("com.google.code.gson:gson:2.13.2")
|
||||
api("com.google.code.gson:gson:2.14.0")
|
||||
api("com.google.protobuf:protobuf-java-util:4.36.1")
|
||||
api("com.h2database:h2:2.4.240")
|
||||
api("com.jayway.jsonpath:json-path:2.10.0")
|
||||
@@ -120,10 +120,10 @@ dependencies {
|
||||
api("org.glassfish:jakarta.el:4.0.2")
|
||||
api("org.graalvm.sdk:graal-sdk:22.3.1")
|
||||
api("org.hamcrest:hamcrest:3.0")
|
||||
api("org.hibernate.orm:hibernate-core:7.2.24.Final")
|
||||
api("org.hibernate.orm:hibernate-core:7.4.8.Final")
|
||||
api("org.hibernate.validator:hibernate-validator:9.1.3.Final")
|
||||
api("org.hsqldb:hsqldb:2.7.4")
|
||||
api("org.htmlunit:htmlunit:4.21.0")
|
||||
api("org.htmlunit:htmlunit:5.4.0")
|
||||
api("org.javamoney:moneta:1.4.4")
|
||||
api("org.jboss.logging:jboss-logging:3.6.1.Final")
|
||||
api("org.jruby:jruby:10.0.2.0")
|
||||
@@ -134,8 +134,8 @@ dependencies {
|
||||
api("org.python:jython-standalone:2.7.4")
|
||||
api("org.quartz-scheduler:quartz:2.3.2")
|
||||
api("org.reactivestreams:reactive-streams:1.0.4")
|
||||
api("org.seleniumhq.selenium:htmlunit3-driver:4.41.0")
|
||||
api("org.seleniumhq.selenium:selenium-java:4.41.0")
|
||||
api("org.seleniumhq.selenium:htmlunit3-driver:4.47.0")
|
||||
api("org.seleniumhq.selenium:selenium-java:4.47.0")
|
||||
api("org.skyscreamer:jsonassert:1.5.3")
|
||||
api("org.testng:testng:7.12.0")
|
||||
api("org.webjars:webjars-locator-lite:1.1.0")
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
version=7.0.10-SNAPSHOT
|
||||
version=7.1.0-SNAPSHOT
|
||||
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
org.gradle.parallel=true
|
||||
|
||||
kotlinVersion=2.2.21
|
||||
kotlinVersion=2.4.20
|
||||
byteBuddyVersion=1.17.6
|
||||
|
||||
kotlin.jvm.target.validation.mode=ignore
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ public class ComponentFactoryBean implements FactoryBean<Component> {
|
||||
|
||||
@Override
|
||||
public Component getObject() {
|
||||
if (this.children != null && this.children.size() > 0) {
|
||||
if (this.children != null && !this.children.isEmpty()) {
|
||||
for (Component child : children) {
|
||||
this.parent.addComponent(child);
|
||||
}
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ public class AspectJAfterThrowingAdvice extends AbstractAspectJAdvice
|
||||
* is only invoked if the thrown exception is a subtype of the given throwing type.
|
||||
*/
|
||||
private boolean shouldInvokeOnThrowing(Throwable ex) {
|
||||
return getDiscoveredThrowingType().isAssignableFrom(ex.getClass());
|
||||
return getDiscoveredThrowingType().isInstance(ex);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+4
-6
@@ -75,11 +75,9 @@ final class InstantiationModelAwarePointcutAdvisorImpl
|
||||
|
||||
private @Nullable Advice instantiatedAdvice;
|
||||
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
private Boolean isBeforeAdvice;
|
||||
private @Nullable Boolean isBeforeAdvice;
|
||||
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
private Boolean isAfterAdvice;
|
||||
private @Nullable Boolean isAfterAdvice;
|
||||
|
||||
|
||||
public InstantiationModelAwarePointcutAdvisorImpl(AspectJExpressionPointcut declaredPointcut,
|
||||
@@ -198,7 +196,7 @@ final class InstantiationModelAwarePointcutAdvisorImpl
|
||||
if (this.isBeforeAdvice == null) {
|
||||
determineAdviceType();
|
||||
}
|
||||
return this.isBeforeAdvice;
|
||||
return (this.isBeforeAdvice == Boolean.TRUE);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -206,7 +204,7 @@ final class InstantiationModelAwarePointcutAdvisorImpl
|
||||
if (this.isAfterAdvice == null) {
|
||||
determineAdviceType();
|
||||
}
|
||||
return this.isAfterAdvice;
|
||||
return (this.isAfterAdvice == Boolean.TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.aop.framework;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
@@ -30,6 +31,9 @@ import org.springframework.aop.TargetClassAware;
|
||||
import org.springframework.aop.TargetSource;
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.aop.target.SingletonTargetSource;
|
||||
import org.springframework.beans.factory.Aware;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.core.DecoratingProxy;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
@@ -301,4 +305,16 @@ public abstract class AopProxyUtils {
|
||||
return arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the given interface is a Spring configuration callback
|
||||
* interface (i.e. {@link InitializingBean}, {@link DisposableBean},
|
||||
* {@link Closeable}/{@link AutoCloseable}, or an {@link Aware} sub-interface).
|
||||
* @since 7.1
|
||||
*/
|
||||
static boolean isConfigurationCallbackInterface(Class<?> ifc) {
|
||||
return (InitializingBean.class == ifc || DisposableBean.class == ifc ||
|
||||
Closeable.class == ifc || AutoCloseable.class == ifc ||
|
||||
ObjectUtils.containsElement(ifc.getInterfaces(), Aware.class));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -292,8 +292,13 @@ class CglibAopProxy implements AopProxy, Serializable {
|
||||
if (Modifier.isFinal(mod)) {
|
||||
if (logger.isWarnEnabled() && Modifier.isPublic(mod)) {
|
||||
if (implementsInterface(method, ifcs)) {
|
||||
logger.warn("Unable to proxy interface-implementing method [" + method + "] because " +
|
||||
"it is marked as final, consider using interface-based JDK proxies instead.");
|
||||
// Final methods inherited from configuration callback interfaces are
|
||||
// typically driven by the container itself rather than by user code, so
|
||||
// logging a warning about CGLIB being unable to advise them is misleading noise.
|
||||
if (!implementsOnlyConfigurationCallbackInterfaces(method, ifcs)) {
|
||||
logger.warn("Unable to proxy interface-implementing method [" + method + "] because " +
|
||||
"it is marked as final, consider using interface-based JDK proxies instead.");
|
||||
}
|
||||
}
|
||||
else {
|
||||
logger.warn("Public final method [" + method + "] cannot get proxied via CGLIB, " +
|
||||
@@ -415,6 +420,25 @@ class CglibAopProxy implements AopProxy, Serializable {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether every interface that declares the given method is a
|
||||
* configuration callback interface.
|
||||
* @since 7.1
|
||||
* @see AopProxyUtils#isConfigurationCallbackInterface(Class)
|
||||
*/
|
||||
static boolean implementsOnlyConfigurationCallbackInterfaces(Method method, Set<Class<?>> ifcs) {
|
||||
boolean matched = false;
|
||||
for (Class<?> ifc : ifcs) {
|
||||
if (ClassUtils.hasMethod(ifc, method)) {
|
||||
if (!AopProxyUtils.isConfigurationCallbackInterface(ifc)) {
|
||||
return false;
|
||||
}
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a return value. Wraps a return of {@code this} if necessary to be the
|
||||
* {@code proxy} and also verifies that {@code null} is not returned as a primitive.
|
||||
|
||||
+1
-8
@@ -16,17 +16,11 @@
|
||||
|
||||
package org.springframework.aop.framework;
|
||||
|
||||
import java.io.Closeable;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.beans.factory.Aware;
|
||||
import org.springframework.beans.factory.BeanClassLoaderAware;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Base class with common functionality for proxy processors, in particular
|
||||
@@ -130,8 +124,7 @@ public class ProxyProcessorSupport extends ProxyConfig implements Ordered, BeanC
|
||||
* @return whether the given interface is just a container callback
|
||||
*/
|
||||
protected boolean isConfigurationCallbackInterface(Class<?> ifc) {
|
||||
return (InitializingBean.class == ifc || DisposableBean.class == ifc || Closeable.class == ifc ||
|
||||
AutoCloseable.class == ifc || ObjectUtils.containsElement(ifc.getInterfaces(), Aware.class));
|
||||
return AopProxyUtils.isConfigurationCallbackInterface(ifc);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+3
-4
@@ -42,8 +42,7 @@ public abstract class AbstractRefreshableTargetSource implements TargetSource, R
|
||||
/** Logger available to subclasses. */
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
protected Object targetObject;
|
||||
protected @Nullable Object targetObject;
|
||||
|
||||
private long refreshCheckDelay = -1;
|
||||
|
||||
@@ -66,11 +65,11 @@ public abstract class AbstractRefreshableTargetSource implements TargetSource, R
|
||||
|
||||
|
||||
@Override
|
||||
public synchronized Class<?> getTargetClass() {
|
||||
public synchronized @Nullable Class<?> getTargetClass() {
|
||||
if (this.targetObject == null) {
|
||||
refresh();
|
||||
}
|
||||
return this.targetObject.getClass();
|
||||
return (this.targetObject != null ? this.targetObject.getClass() : null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* Copyright 2002-present 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.aop.framework;
|
||||
|
||||
import java.io.Closeable;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link CglibAopProxy#implementsOnlyConfigurationCallbackInterfaces}.
|
||||
*
|
||||
* <p>Verifies that final methods inherited from Spring's configuration callback
|
||||
* interfaces (InitializingBean, DisposableBean, Aware sub-interfaces,
|
||||
* Closeable/AutoCloseable) are recognized so that the CGLIB validation warning
|
||||
* can be suppressed for those container-driven methods.
|
||||
*
|
||||
* @since 7.1
|
||||
* @see <a href="https://github.com/spring-projects/spring-framework/pull/36935>gh-36935</a>
|
||||
*/
|
||||
class CglibAopProxyConfigurationCallbackTests {
|
||||
|
||||
@Test
|
||||
void finalAfterPropertiesSetIsRecognisedAsCallback() {
|
||||
var method = ClassUtils.getMethod(WithFinalAfterPropertiesSet.class, "afterPropertiesSet");
|
||||
var interfaces = ClassUtils.getAllInterfacesForClassAsSet(WithFinalAfterPropertiesSet.class);
|
||||
|
||||
assertThat(CglibAopProxy.implementsOnlyConfigurationCallbackInterfaces(method, interfaces)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void finalDestroyIsRecognisedAsCallback() {
|
||||
var method = ClassUtils.getMethod(WithFinalDestroy.class, "destroy");
|
||||
var interfaces = ClassUtils.getAllInterfacesForClassAsSet(WithFinalDestroy.class);
|
||||
|
||||
assertThat(CglibAopProxy.implementsOnlyConfigurationCallbackInterfaces(method, interfaces)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void finalAwareCallbackIsRecognisedAsCallback() {
|
||||
var method = ClassUtils.getMethod(WithFinalBeanFactoryAware.class, "setBeanFactory", BeanFactory.class);
|
||||
var interfaces = ClassUtils.getAllInterfacesForClassAsSet(WithFinalBeanFactoryAware.class);
|
||||
|
||||
assertThat(CglibAopProxy.implementsOnlyConfigurationCallbackInterfaces(method, interfaces)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void finalCloseableCloseIsRecognisedAsCallback() {
|
||||
var method = ClassUtils.getMethod(WithFinalCloseableClose.class, "close");
|
||||
var interfaces = ClassUtils.getAllInterfacesForClassAsSet(WithFinalCloseableClose.class);
|
||||
|
||||
assertThat(CglibAopProxy.implementsOnlyConfigurationCallbackInterfaces(method, interfaces)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void finalAutoCloseableCloseIsRecognisedAsCallback() {
|
||||
var method = ClassUtils.getMethod(WithFinalAutoCloseableClose.class, "close");
|
||||
var interfaces = ClassUtils.getAllInterfacesForClassAsSet(WithFinalAutoCloseableClose.class);
|
||||
|
||||
assertThat(CglibAopProxy.implementsOnlyConfigurationCallbackInterfaces(method, interfaces)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void finalUserInterfaceMethodIsNotSuppressed() {
|
||||
var method = ClassUtils.getMethod(WithFinalUserApi.class, "execute");
|
||||
var interfaces = ClassUtils.getAllInterfacesForClassAsSet(WithFinalUserApi.class);
|
||||
|
||||
assertThat(CglibAopProxy.implementsOnlyConfigurationCallbackInterfaces(method, interfaces)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void methodSharedBetweenCallbackAndUserInterfaceIsNotSuppressed() {
|
||||
var method = ClassUtils.getMethod(WithSharedSignature.class, "afterPropertiesSet");
|
||||
var interfaces = ClassUtils.getAllInterfacesForClassAsSet(WithSharedSignature.class);
|
||||
|
||||
// Even though InitializingBean declares afterPropertiesSet(), a user
|
||||
// interface (CustomLifecycle) declares the same signature, so the
|
||||
// warning must still fire.
|
||||
assertThat(CglibAopProxy.implementsOnlyConfigurationCallbackInterfaces(method, interfaces)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void finalMethodWithoutInterfaceMatchIsNotSuppressed() {
|
||||
var method = ClassUtils.getMethod(WithStandaloneFinal.class, "doSomething");
|
||||
var interfaces = ClassUtils.getAllInterfacesForClassAsSet(WithStandaloneFinal.class);
|
||||
|
||||
assertThat(CglibAopProxy.implementsOnlyConfigurationCallbackInterfaces(method, interfaces)).isFalse();
|
||||
}
|
||||
|
||||
|
||||
static class WithFinalAfterPropertiesSet implements InitializingBean {
|
||||
|
||||
@Override
|
||||
public final void afterPropertiesSet() {
|
||||
}
|
||||
}
|
||||
|
||||
static class WithFinalDestroy implements DisposableBean {
|
||||
|
||||
@Override
|
||||
public final void destroy() {
|
||||
}
|
||||
}
|
||||
|
||||
static class WithFinalBeanFactoryAware implements BeanFactoryAware {
|
||||
|
||||
@Override
|
||||
public final void setBeanFactory(BeanFactory beanFactory) {
|
||||
}
|
||||
}
|
||||
|
||||
static class WithFinalCloseableClose implements Closeable {
|
||||
|
||||
@Override
|
||||
public final void close() {
|
||||
}
|
||||
}
|
||||
|
||||
static class WithFinalAutoCloseableClose implements AutoCloseable {
|
||||
|
||||
@Override
|
||||
public final void close() {
|
||||
}
|
||||
}
|
||||
|
||||
interface UserApi {
|
||||
|
||||
void execute();
|
||||
}
|
||||
|
||||
static class WithFinalUserApi implements UserApi {
|
||||
|
||||
@Override
|
||||
public final void execute() {
|
||||
}
|
||||
}
|
||||
|
||||
interface CustomLifecycle {
|
||||
|
||||
void afterPropertiesSet();
|
||||
}
|
||||
|
||||
static class WithSharedSignature implements InitializingBean, CustomLifecycle {
|
||||
|
||||
@Override
|
||||
public final void afterPropertiesSet() {
|
||||
}
|
||||
}
|
||||
|
||||
static class WithStandaloneFinal {
|
||||
|
||||
public final void doSomething() {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+171
-236
@@ -23,7 +23,6 @@ import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.lang.reflect.UndeclaredThrowableException;
|
||||
import java.security.PrivilegedActionException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
@@ -42,7 +41,6 @@ import org.springframework.core.convert.ConverterNotFoundException;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A basic {@link ConfigurablePropertyAccessor} that provides the necessary
|
||||
@@ -60,6 +58,7 @@ import org.springframework.util.StringUtils;
|
||||
* @author Rod Johnson
|
||||
* @author Rob Harrop
|
||||
* @author Sam Brannen
|
||||
* @author Brian Clozel
|
||||
* @since 4.2
|
||||
* @see #registerCustomEditor
|
||||
* @see #setPropertyValues
|
||||
@@ -76,16 +75,14 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
*/
|
||||
private static final Log logger = LogFactory.getLog(AbstractNestablePropertyAccessor.class);
|
||||
|
||||
private int autoGrowCollectionLimit = Integer.MAX_VALUE;
|
||||
|
||||
@Nullable Object wrappedObject;
|
||||
|
||||
private String nestedPath = "";
|
||||
|
||||
@Nullable Object rootObject;
|
||||
|
||||
/** Map with cached nested Accessors: nested path -> Accessor instance. */
|
||||
private @Nullable Map<String, AbstractNestablePropertyAccessor> nestedPropertyAccessors;
|
||||
/** Map with cached nested Accessors: path segment -> Accessor instance. */
|
||||
private @Nullable Map<PropertyPath.Segment, AbstractNestablePropertyAccessor> nestedPropertyAccessors;
|
||||
|
||||
|
||||
/**
|
||||
@@ -152,25 +149,11 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
setExtractOldValueForEditor(parent.isExtractOldValueForEditor());
|
||||
setAutoGrowNestedPaths(parent.isAutoGrowNestedPaths());
|
||||
setAutoGrowCollectionLimit(parent.getAutoGrowCollectionLimit());
|
||||
setMaxNestedPathDepth(parent.getMaxNestedPathDepth());
|
||||
setConversionService(parent.getConversionService());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Specify a limit for array and collection auto-growing.
|
||||
* <p>Default is unlimited on a plain accessor.
|
||||
*/
|
||||
public void setAutoGrowCollectionLimit(int autoGrowCollectionLimit) {
|
||||
this.autoGrowCollectionLimit = autoGrowCollectionLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the limit for array and collection auto-growing.
|
||||
*/
|
||||
public int getAutoGrowCollectionLimit() {
|
||||
return this.autoGrowCollectionLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch the target object, replacing the cached introspection results only
|
||||
* if the class of the new object is different to that of the replaced object.
|
||||
@@ -231,61 +214,70 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
|
||||
@Override
|
||||
public void setPropertyValue(String propertyName, @Nullable Object value) throws BeansException {
|
||||
AbstractNestablePropertyAccessor nestedPa;
|
||||
ResolvedProperty resolved;
|
||||
try {
|
||||
nestedPa = getPropertyAccessorForPropertyPath(propertyName);
|
||||
resolved = resolvePropertyPath(propertyName);
|
||||
}
|
||||
catch (InvalidPropertyPathException ex) {
|
||||
// A malformed path is a syntax error, distinct from a syntactically
|
||||
// valid path whose intermediate segment genuinely does not exist
|
||||
// (caught below and reported as "not writable" instead).
|
||||
throw new InvalidPropertyPathException(getRootInstance(), this.nestedPath + propertyName, value, ex);
|
||||
}
|
||||
catch (NotReadablePropertyException ex) {
|
||||
throw new NotWritablePropertyException(getRootClass(), this.nestedPath + propertyName,
|
||||
"Nested property in path '" + propertyName + "' does not exist", ex);
|
||||
}
|
||||
PropertyTokenHolder tokens = getPropertyNameTokens(getFinalPath(nestedPa, propertyName));
|
||||
nestedPa.setPropertyValue(tokens, new PropertyValue(propertyName, value));
|
||||
resolved.accessor().setPropertyValue(resolved.segment(), new PropertyValue(propertyName, value));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPropertyValue(PropertyValue pv) throws BeansException {
|
||||
PropertyTokenHolder tokens = (PropertyTokenHolder) pv.resolvedTokens;
|
||||
if (tokens == null) {
|
||||
PropertyPath.Segment segment = (PropertyPath.Segment) pv.resolvedTokens;
|
||||
if (segment == null) {
|
||||
String propertyName = pv.getName();
|
||||
AbstractNestablePropertyAccessor nestedPa;
|
||||
ResolvedProperty resolved;
|
||||
try {
|
||||
nestedPa = getPropertyAccessorForPropertyPath(propertyName);
|
||||
resolved = resolvePropertyPath(propertyName);
|
||||
}
|
||||
catch (InvalidPropertyPathException ex) {
|
||||
throw new InvalidPropertyPathException(getRootInstance(), this.nestedPath + propertyName, pv.getValue(), ex);
|
||||
}
|
||||
catch (NotReadablePropertyException ex) {
|
||||
throw new NotWritablePropertyException(getRootClass(), this.nestedPath + propertyName,
|
||||
"Nested property in path '" + propertyName + "' does not exist", ex);
|
||||
}
|
||||
tokens = getPropertyNameTokens(getFinalPath(nestedPa, propertyName));
|
||||
if (nestedPa == this) {
|
||||
pv.getOriginalPropertyValue().resolvedTokens = tokens;
|
||||
segment = resolved.segment();
|
||||
if (resolved.accessor() == this) {
|
||||
pv.getOriginalPropertyValue().resolvedTokens = segment;
|
||||
}
|
||||
nestedPa.setPropertyValue(tokens, pv);
|
||||
resolved.accessor().setPropertyValue(segment, pv);
|
||||
}
|
||||
else {
|
||||
setPropertyValue(tokens, pv);
|
||||
setPropertyValue(segment, pv);
|
||||
}
|
||||
}
|
||||
|
||||
protected void setPropertyValue(PropertyTokenHolder tokens, PropertyValue pv) throws BeansException {
|
||||
if (tokens.keys != null) {
|
||||
processKeyedProperty(tokens, pv);
|
||||
protected void setPropertyValue(PropertyPath.Segment segment, PropertyValue pv) throws BeansException {
|
||||
if (!segment.keys().isEmpty()) {
|
||||
processKeyedProperty(segment, pv);
|
||||
}
|
||||
else {
|
||||
processLocalProperty(tokens, pv);
|
||||
processLocalProperty(segment, pv);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
private void processKeyedProperty(PropertyTokenHolder tokens, PropertyValue pv) {
|
||||
Object propValue = getPropertyHoldingValue(tokens);
|
||||
PropertyHandler ph = getLocalPropertyHandler(tokens.actualName);
|
||||
private void processKeyedProperty(PropertyPath.Segment segment, PropertyValue pv) {
|
||||
Object propValue = getPropertyHoldingValue(segment);
|
||||
PropertyHandler ph = getLocalPropertyHandler(segment.name());
|
||||
if (ph == null) {
|
||||
throw new InvalidPropertyException(
|
||||
getRootClass(), this.nestedPath + tokens.actualName, "No property handler found");
|
||||
getRootClass(), this.nestedPath + segment.name(), "No property handler found");
|
||||
}
|
||||
Assert.state(tokens.keys != null, "No token keys");
|
||||
String lastKey = tokens.keys[tokens.keys.length - 1];
|
||||
List<String> keys = segment.keys();
|
||||
String lastKey = keys.get(keys.size() - 1);
|
||||
String canonicalName = segment.toCanonicalName();
|
||||
|
||||
if (propValue.getClass().isArray()) {
|
||||
Class<?> componentType = propValue.getClass().componentType();
|
||||
@@ -295,44 +287,43 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
if (isExtractOldValueForEditor() && arrayIndex < Array.getLength(propValue)) {
|
||||
oldValue = Array.get(propValue, arrayIndex);
|
||||
}
|
||||
Object convertedValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(),
|
||||
componentType, ph.nested(tokens.keys.length));
|
||||
Object convertedValue = convertIfNecessary(canonicalName, oldValue, pv.getValue(),
|
||||
componentType, ph.nested(keys.size()));
|
||||
int length = Array.getLength(propValue);
|
||||
if (arrayIndex >= length && arrayIndex < this.autoGrowCollectionLimit) {
|
||||
if (arrayIndex >= length && arrayIndex < getAutoGrowCollectionLimit()) {
|
||||
Object newArray = Array.newInstance(componentType, arrayIndex + 1);
|
||||
System.arraycopy(propValue, 0, newArray, 0, length);
|
||||
int lastKeyIndex = tokens.canonicalName.lastIndexOf('[');
|
||||
String propName = tokens.canonicalName.substring(0, lastKeyIndex);
|
||||
String propName = segment.withoutLastKey().toCanonicalName();
|
||||
setPropertyValue(propName, newArray);
|
||||
propValue = getPropertyValue(propName);
|
||||
}
|
||||
Array.set(propValue, arrayIndex, convertedValue);
|
||||
}
|
||||
catch (IndexOutOfBoundsException ex) {
|
||||
throw new InvalidPropertyException(getRootClass(), this.nestedPath + tokens.canonicalName,
|
||||
"Invalid array index in property path '" + tokens.canonicalName + "'", ex);
|
||||
throw new InvalidPropertyException(getRootClass(), this.nestedPath + canonicalName,
|
||||
"Invalid array index in property path '" + canonicalName + "'", ex);
|
||||
}
|
||||
}
|
||||
|
||||
else if (propValue instanceof List list) {
|
||||
TypeDescriptor requiredType = ph.getCollectionType(tokens.keys.length);
|
||||
TypeDescriptor requiredType = ph.getCollectionType(keys.size());
|
||||
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(),
|
||||
Object convertedValue = convertIfNecessary(canonicalName, oldValue, pv.getValue(),
|
||||
requiredType.getResolvableType().resolve(), requiredType);
|
||||
int size = list.size();
|
||||
if (index >= size && index < this.autoGrowCollectionLimit) {
|
||||
if (index >= size && index < getAutoGrowCollectionLimit()) {
|
||||
for (int i = size; i < index; i++) {
|
||||
try {
|
||||
list.add(null);
|
||||
}
|
||||
catch (NullPointerException ex) {
|
||||
throw new InvalidPropertyException(getRootClass(), this.nestedPath + tokens.canonicalName,
|
||||
throw new InvalidPropertyException(getRootClass(), this.nestedPath + canonicalName,
|
||||
"Cannot set element with index " + index + " in List of size " +
|
||||
size + ", accessed using property path '" + tokens.canonicalName +
|
||||
size + ", accessed using property path '" + canonicalName +
|
||||
"': List does not support filling up gaps with null elements");
|
||||
}
|
||||
}
|
||||
@@ -343,15 +334,15 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
list.set(index, convertedValue);
|
||||
}
|
||||
catch (IndexOutOfBoundsException ex) {
|
||||
throw new InvalidPropertyException(getRootClass(), this.nestedPath + tokens.canonicalName,
|
||||
"Invalid list index in property path '" + tokens.canonicalName + "'", ex);
|
||||
throw new InvalidPropertyException(getRootClass(), this.nestedPath + canonicalName,
|
||||
"Invalid list index in property path '" + canonicalName + "'", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else if (propValue instanceof Map map) {
|
||||
TypeDescriptor mapKeyType = ph.getMapKeyType(tokens.keys.length);
|
||||
TypeDescriptor mapValueType = ph.getMapValueType(tokens.keys.length);
|
||||
TypeDescriptor mapKeyType = ph.getMapKeyType(keys.size());
|
||||
TypeDescriptor mapValueType = ph.getMapValueType(keys.size());
|
||||
// IMPORTANT: Do not pass full property name in here - property editors
|
||||
// must not kick in for map keys but rather only for map values.
|
||||
Object convertedMapKey = convertIfNecessary(null, null, lastKey,
|
||||
@@ -362,58 +353,54 @@ 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(),
|
||||
Object convertedMapValue = convertIfNecessary(canonicalName, oldValue, pv.getValue(),
|
||||
mapValueType.getResolvableType().resolve(), mapValueType);
|
||||
map.put(convertedMapKey, convertedMapValue);
|
||||
}
|
||||
|
||||
else {
|
||||
throw new InvalidPropertyException(getRootClass(), this.nestedPath + tokens.canonicalName,
|
||||
"Property referenced in indexed property path '" + tokens.canonicalName +
|
||||
throw new InvalidPropertyException(getRootClass(), this.nestedPath + canonicalName,
|
||||
"Property referenced in indexed property path '" + canonicalName +
|
||||
"' is neither an array nor a List nor a Map; returned value was [" + propValue + "]");
|
||||
}
|
||||
}
|
||||
|
||||
private Object getPropertyHoldingValue(PropertyTokenHolder tokens) {
|
||||
private Object getPropertyHoldingValue(PropertyPath.Segment segment) {
|
||||
// Apply indexes and map keys: fetch value for all keys but the last one.
|
||||
Assert.state(tokens.keys != null, "No token keys");
|
||||
PropertyTokenHolder getterTokens = new PropertyTokenHolder(tokens.actualName);
|
||||
getterTokens.canonicalName = tokens.canonicalName;
|
||||
getterTokens.keys = new String[tokens.keys.length - 1];
|
||||
System.arraycopy(tokens.keys, 0, getterTokens.keys, 0, tokens.keys.length - 1);
|
||||
|
||||
PropertyPath.Segment getterSegment = segment.withoutLastKey();
|
||||
Object propValue;
|
||||
try {
|
||||
propValue = getPropertyValue(getterTokens);
|
||||
propValue = getPropertyValue(getterSegment);
|
||||
}
|
||||
catch (NotReadablePropertyException ex) {
|
||||
throw new NotWritablePropertyException(getRootClass(), this.nestedPath + tokens.canonicalName,
|
||||
throw new NotWritablePropertyException(getRootClass(), this.nestedPath + segment.toCanonicalName(),
|
||||
"Cannot access indexed value in property referenced " +
|
||||
"in indexed property path '" + tokens.canonicalName + "'", ex);
|
||||
"in indexed property path '" + segment.toCanonicalName() + "'", ex);
|
||||
}
|
||||
|
||||
if (propValue == null) {
|
||||
// null map value case
|
||||
if (isAutoGrowNestedPaths()) {
|
||||
int lastKeyIndex = tokens.canonicalName.lastIndexOf('[');
|
||||
getterTokens.canonicalName = tokens.canonicalName.substring(0, lastKeyIndex);
|
||||
propValue = setDefaultValue(getterTokens);
|
||||
propValue = setDefaultValue(getterSegment);
|
||||
}
|
||||
else {
|
||||
throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + tokens.canonicalName,
|
||||
throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + segment.toCanonicalName(),
|
||||
"Cannot access indexed value in property referenced " +
|
||||
"in indexed property path '" + tokens.canonicalName + "': returned null");
|
||||
"in indexed property path '" + segment.toCanonicalName() + "': returned null");
|
||||
}
|
||||
}
|
||||
return propValue;
|
||||
}
|
||||
|
||||
private void processLocalProperty(PropertyTokenHolder tokens, PropertyValue pv) {
|
||||
PropertyHandler ph = getLocalPropertyHandler(tokens.actualName);
|
||||
private void processLocalProperty(PropertyPath.Segment segment, PropertyValue pv) {
|
||||
// segment.keys() is always empty here (see setPropertyValue(Segment, PropertyValue)
|
||||
// above), so the segment's canonical name is always just its raw name.
|
||||
String name = segment.name();
|
||||
PropertyHandler ph = getLocalPropertyHandler(name);
|
||||
if (ph == null || !ph.isWritable()) {
|
||||
if (pv.isOptional()) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Ignoring optional value for property '" + tokens.actualName +
|
||||
logger.debug("Ignoring optional value for property '" + name +
|
||||
"' - property not found on bean class [" + getRootClass().getName() + "]");
|
||||
}
|
||||
return;
|
||||
@@ -423,7 +410,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
// exception would be caught and swallowed higher up anyway...
|
||||
return;
|
||||
}
|
||||
throw createNotWritablePropertyException(tokens.canonicalName);
|
||||
throw createNotWritablePropertyException(name);
|
||||
}
|
||||
|
||||
Object oldValue = null;
|
||||
@@ -445,12 +432,11 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
}
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Could not read previous value of property '" +
|
||||
this.nestedPath + tokens.canonicalName + "'", ex);
|
||||
this.nestedPath + name + "'", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
valueToApply = convertForProperty(
|
||||
tokens.canonicalName, oldValue, originalValue, ph.toTypeDescriptor());
|
||||
valueToApply = convertForProperty(name, oldValue, originalValue, ph.toTypeDescriptor());
|
||||
}
|
||||
pv.getOriginalPropertyValue().conversionNecessary = (valueToApply != originalValue);
|
||||
}
|
||||
@@ -463,7 +449,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
}
|
||||
catch (InvocationTargetException ex) {
|
||||
PropertyChangeEvent propertyChangeEvent = new PropertyChangeEvent(
|
||||
getRootInstance(), this.nestedPath + tokens.canonicalName, oldValue, pv.getValue());
|
||||
getRootInstance(), this.nestedPath + name, oldValue, pv.getValue());
|
||||
if (ex.getTargetException() instanceof ClassCastException) {
|
||||
throw new TypeMismatchException(propertyChangeEvent, ph.getPropertyType(), ex.getTargetException());
|
||||
}
|
||||
@@ -478,7 +464,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
}
|
||||
catch (Exception ex) {
|
||||
PropertyChangeEvent pce = new PropertyChangeEvent(
|
||||
getRootInstance(), this.nestedPath + tokens.canonicalName, oldValue, pv.getValue());
|
||||
getRootInstance(), this.nestedPath + name, oldValue, pv.getValue());
|
||||
throw new MethodInvocationException(pce, ex);
|
||||
}
|
||||
}
|
||||
@@ -507,7 +493,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (InvalidPropertyException ex) {
|
||||
catch (InvalidPropertyException | InvalidPropertyPathException ex) {
|
||||
// Consider as not determinable.
|
||||
}
|
||||
return null;
|
||||
@@ -516,14 +502,14 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
@Override
|
||||
public @Nullable TypeDescriptor getPropertyTypeDescriptor(String propertyName) throws BeansException {
|
||||
try {
|
||||
AbstractNestablePropertyAccessor nestedPa = getPropertyAccessorForPropertyPath(propertyName);
|
||||
String finalPath = getFinalPath(nestedPa, propertyName);
|
||||
PropertyTokenHolder tokens = getPropertyNameTokens(finalPath);
|
||||
PropertyHandler ph = nestedPa.getLocalPropertyHandler(tokens.actualName);
|
||||
ResolvedProperty resolved = resolvePropertyPath(propertyName);
|
||||
PropertyPath.Segment segment = resolved.segment();
|
||||
PropertyHandler ph = resolved.accessor().getLocalPropertyHandler(segment.name());
|
||||
if (ph != null) {
|
||||
if (tokens.keys != null) {
|
||||
List<String> keys = segment.keys();
|
||||
if (!keys.isEmpty()) {
|
||||
if (ph.isReadable() || ph.isWritable()) {
|
||||
return ph.nested(tokens.keys.length);
|
||||
return ph.nested(keys.size());
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -533,7 +519,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (InvalidPropertyException ex) {
|
||||
catch (InvalidPropertyException | InvalidPropertyPathException ex) {
|
||||
// Consider as not determinable.
|
||||
}
|
||||
return null;
|
||||
@@ -552,7 +538,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (InvalidPropertyException ex) {
|
||||
catch (InvalidPropertyException | InvalidPropertyPathException ex) {
|
||||
// Cannot be evaluated, so can't be readable.
|
||||
}
|
||||
return false;
|
||||
@@ -571,7 +557,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (InvalidPropertyException ex) {
|
||||
catch (InvalidPropertyException | InvalidPropertyPathException ex) {
|
||||
// Cannot be evaluated, so can't be writable.
|
||||
}
|
||||
return false;
|
||||
@@ -606,25 +592,25 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
|
||||
@Override
|
||||
public @Nullable Object getPropertyValue(String propertyName) throws BeansException {
|
||||
AbstractNestablePropertyAccessor nestedPa = getPropertyAccessorForPropertyPath(propertyName);
|
||||
PropertyTokenHolder tokens = getPropertyNameTokens(getFinalPath(nestedPa, propertyName));
|
||||
return nestedPa.getPropertyValue(tokens);
|
||||
ResolvedProperty resolved = resolvePropertyPath(propertyName);
|
||||
return resolved.accessor().getPropertyValue(resolved.segment());
|
||||
}
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
protected @Nullable Object getPropertyValue(PropertyTokenHolder tokens) throws BeansException {
|
||||
String propertyName = tokens.canonicalName;
|
||||
String actualName = tokens.actualName;
|
||||
protected @Nullable Object getPropertyValue(PropertyPath.Segment segment) throws BeansException {
|
||||
String propertyName = segment.toCanonicalName();
|
||||
String actualName = segment.name();
|
||||
PropertyHandler ph = getLocalPropertyHandler(actualName);
|
||||
if (ph == null || !ph.isReadable()) {
|
||||
throw new NotReadablePropertyException(getRootClass(), this.nestedPath + propertyName);
|
||||
}
|
||||
try {
|
||||
Object value = ph.getValue();
|
||||
if (tokens.keys != null) {
|
||||
List<String> keys = segment.keys();
|
||||
if (!keys.isEmpty()) {
|
||||
if (value == null) {
|
||||
if (isAutoGrowNestedPaths()) {
|
||||
value = setDefaultValue(new PropertyTokenHolder(tokens.actualName));
|
||||
value = setDefaultValue(new PropertyPath.Segment(actualName, List.of()));
|
||||
}
|
||||
else {
|
||||
throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + propertyName,
|
||||
@@ -632,10 +618,10 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
"property path '" + propertyName + "': returned null");
|
||||
}
|
||||
}
|
||||
StringBuilder indexedPropertyName = new StringBuilder(tokens.actualName);
|
||||
StringBuilder indexedPropertyName = new StringBuilder(actualName);
|
||||
// apply indexes and map keys
|
||||
for (int i = 0; i < tokens.keys.length; i++) {
|
||||
String key = tokens.keys[i];
|
||||
for (int i = 0; i < keys.size(); i++) {
|
||||
String key = keys.get(i);
|
||||
if (value == null) {
|
||||
throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + propertyName,
|
||||
"Cannot access indexed value of property referenced in indexed " +
|
||||
@@ -734,8 +720,8 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
*/
|
||||
protected @Nullable PropertyHandler getPropertyHandler(String propertyName) throws BeansException {
|
||||
Assert.notNull(propertyName, "Property name must not be null");
|
||||
AbstractNestablePropertyAccessor nestedPa = getPropertyAccessorForPropertyPath(propertyName);
|
||||
return nestedPa.getLocalPropertyHandler(getFinalPath(nestedPa, propertyName));
|
||||
ResolvedProperty resolved = resolvePropertyPath(propertyName);
|
||||
return resolved.accessor().getLocalPropertyHandler(resolved.segment().toCanonicalName());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -766,7 +752,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
return array;
|
||||
}
|
||||
int length = Array.getLength(array);
|
||||
if (index >= length && index < this.autoGrowCollectionLimit) {
|
||||
if (index >= length && index < getAutoGrowCollectionLimit()) {
|
||||
Class<?> componentType = array.getClass().componentType();
|
||||
Object newArray = Array.newInstance(componentType, index + 1);
|
||||
System.arraycopy(array, 0, newArray, 0, length);
|
||||
@@ -790,7 +776,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
return;
|
||||
}
|
||||
int size = collection.size();
|
||||
if (index >= size && index < this.autoGrowCollectionLimit) {
|
||||
if (index >= size && index < getAutoGrowCollectionLimit()) {
|
||||
Class<?> elementType = ph.getResolvableType().getNested(nestingLevel).asCollection().resolveGeneric();
|
||||
if (elementType != null) {
|
||||
for (int i = collection.size(); i < index + 1; i++) {
|
||||
@@ -801,35 +787,55 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last component of the path. Also works if not nested.
|
||||
* @param pa property accessor to work on
|
||||
* @param nestedPath property path we know is nested
|
||||
* @return last component of the path (the property on the target bean)
|
||||
* Resolve a property path to the accessor owning its final segment and
|
||||
* that segment itself, parsing the path exactly once.
|
||||
* @param propertyPath the property path, which may be nested
|
||||
* @return the accessor for the target bean, paired with the final segment
|
||||
* @throws InvalidPropertyPathException if the given path is not a
|
||||
* well-formed property path, or if its nesting depth exceeds
|
||||
* {@link #getMaxNestedPathDepth()}
|
||||
* @since 7.1
|
||||
*/
|
||||
protected String getFinalPath(AbstractNestablePropertyAccessor pa, String nestedPath) {
|
||||
if (pa == this) {
|
||||
return nestedPath;
|
||||
}
|
||||
return nestedPath.substring(PropertyAccessorUtils.getLastNestedPropertySeparatorIndex(nestedPath) + 1);
|
||||
protected ResolvedProperty resolvePropertyPath(String propertyPath) {
|
||||
PropertyPath.Options options = PropertyPath.Options.withMaxNestedPathDepth(getMaxNestedPathDepth());
|
||||
List<PropertyPath.Segment> segments = PropertyPath.parse(propertyPath, options).segments();
|
||||
AbstractNestablePropertyAccessor accessor = getPropertyAccessorForSegments(segments, 0);
|
||||
PropertyPath.Segment segment = finalSegment(propertyPath, segments);
|
||||
return new ResolvedProperty(accessor, segment);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively navigate to return a property accessor for the nested property path.
|
||||
* @param propertyPath property path, which may be nested
|
||||
* @return a property accessor for the target bean
|
||||
* The final segment of an already-parsed property path: the property to
|
||||
* actually get or set. Handles the one case {@link PropertyPath} itself
|
||||
* does not produce a segment for: the empty path ({@code ""}), a valid
|
||||
* property path with zero segments, treated the same as a single segment
|
||||
* with an empty name and no keys — a shape {@link PropertyPath.Segment}'s
|
||||
* own constructor allows even though {@link PropertyPath#parse}'s grammar
|
||||
* validation never produces it.
|
||||
*/
|
||||
protected AbstractNestablePropertyAccessor getPropertyAccessorForPropertyPath(String propertyPath) {
|
||||
int pos = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(propertyPath);
|
||||
// Handle nested properties recursively.
|
||||
if (pos > -1) {
|
||||
String nestedProperty = propertyPath.substring(0, pos);
|
||||
String nestedPath = propertyPath.substring(pos + 1);
|
||||
AbstractNestablePropertyAccessor nestedPa = getNestedPropertyAccessor(nestedProperty);
|
||||
return nestedPa.getPropertyAccessorForPropertyPath(nestedPath);
|
||||
}
|
||||
else {
|
||||
private static PropertyPath.Segment finalSegment(String propertyPath, List<PropertyPath.Segment> segments) {
|
||||
return (segments.isEmpty() ? new PropertyPath.Segment(propertyPath, List.of()) :
|
||||
segments.get(segments.size() - 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively navigate to return a property accessor for the given,
|
||||
* already-parsed property path segments, peeling one segment off at a
|
||||
* time until only the final segment (the property to actually get or
|
||||
* set on the returned accessor) is left.
|
||||
* @param segments the segments of the full property path, parsed exactly
|
||||
* once by the caller
|
||||
* @param fromIndex the index of the first segment not yet consumed
|
||||
* @return a property accessor for the bean holding the final segment
|
||||
*/
|
||||
private AbstractNestablePropertyAccessor getPropertyAccessorForSegments(
|
||||
List<PropertyPath.Segment> segments, int fromIndex) {
|
||||
|
||||
if (segments.isEmpty() || fromIndex >= segments.size() - 1) {
|
||||
return this;
|
||||
}
|
||||
AbstractNestablePropertyAccessor nestedPa = getNestedPropertyAccessor(segments.get(fromIndex));
|
||||
return nestedPa.getPropertyAccessorForSegments(segments, fromIndex + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -837,31 +843,30 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
* Create a new one if not found in the cache.
|
||||
* <p>Note: Caching nested PropertyAccessors is necessary now,
|
||||
* to keep registered custom editors for nested properties.
|
||||
* @param nestedProperty property to create the PropertyAccessor for
|
||||
* @param segment the already-parsed segment to create the PropertyAccessor for
|
||||
* @return the PropertyAccessor instance, either cached or newly created
|
||||
*/
|
||||
private AbstractNestablePropertyAccessor getNestedPropertyAccessor(String nestedProperty) {
|
||||
Map<String, AbstractNestablePropertyAccessor> nestedAccessors = this.nestedPropertyAccessors;
|
||||
private AbstractNestablePropertyAccessor getNestedPropertyAccessor(PropertyPath.Segment segment) {
|
||||
Map<PropertyPath.Segment, AbstractNestablePropertyAccessor> nestedAccessors = this.nestedPropertyAccessors;
|
||||
if (nestedAccessors == null) {
|
||||
nestedAccessors = new HashMap<>();
|
||||
this.nestedPropertyAccessors = nestedAccessors;
|
||||
}
|
||||
// Get value of bean property.
|
||||
PropertyTokenHolder tokens = getPropertyNameTokens(nestedProperty);
|
||||
String canonicalName = tokens.canonicalName;
|
||||
Object value = getPropertyValue(tokens);
|
||||
Object value = getPropertyValue(segment);
|
||||
if (value == null || (value instanceof Optional<?> optional && optional.isEmpty())) {
|
||||
if (isAutoGrowNestedPaths()) {
|
||||
value = setDefaultValue(tokens);
|
||||
value = setDefaultValue(segment);
|
||||
}
|
||||
else {
|
||||
throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + canonicalName);
|
||||
throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + segment.toCanonicalName());
|
||||
}
|
||||
}
|
||||
|
||||
// Lookup cached sub-PropertyAccessor, create new one if not found.
|
||||
AbstractNestablePropertyAccessor nestedPa = nestedAccessors.get(canonicalName);
|
||||
AbstractNestablePropertyAccessor nestedPa = nestedAccessors.get(segment);
|
||||
if (nestedPa == null || nestedPa.getWrappedInstance() != ObjectUtils.unwrapOptional(value)) {
|
||||
String canonicalName = segment.toCanonicalName();
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Creating new nested " + getClass().getSimpleName() + " for property '" + canonicalName + "'");
|
||||
}
|
||||
@@ -869,32 +874,33 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
// Inherit all type-specific PropertyEditors.
|
||||
copyDefaultEditorsTo(nestedPa);
|
||||
copyCustomEditorsTo(nestedPa, canonicalName);
|
||||
nestedAccessors.put(canonicalName, nestedPa);
|
||||
nestedAccessors.put(segment, nestedPa);
|
||||
}
|
||||
else {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Using cached nested property accessor for property '" + canonicalName + "'");
|
||||
logger.trace("Using cached nested property accessor for property '" + segment.toCanonicalName() + "'");
|
||||
}
|
||||
}
|
||||
return nestedPa;
|
||||
}
|
||||
|
||||
private Object setDefaultValue(PropertyTokenHolder tokens) {
|
||||
PropertyValue pv = createDefaultPropertyValue(tokens);
|
||||
setPropertyValue(tokens, pv);
|
||||
Object defaultValue = getPropertyValue(tokens);
|
||||
private Object setDefaultValue(PropertyPath.Segment segment) {
|
||||
PropertyValue pv = createDefaultPropertyValue(segment);
|
||||
setPropertyValue(segment, pv);
|
||||
Object defaultValue = getPropertyValue(segment);
|
||||
Assert.state(defaultValue != null, "Default value must not be null");
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private PropertyValue createDefaultPropertyValue(PropertyTokenHolder tokens) {
|
||||
TypeDescriptor desc = getPropertyTypeDescriptor(tokens.canonicalName);
|
||||
private PropertyValue createDefaultPropertyValue(PropertyPath.Segment segment) {
|
||||
String canonicalName = segment.toCanonicalName();
|
||||
TypeDescriptor desc = getPropertyTypeDescriptor(canonicalName);
|
||||
if (desc == null) {
|
||||
throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + tokens.canonicalName,
|
||||
throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + canonicalName,
|
||||
"Could not determine property type for auto-growing a default value");
|
||||
}
|
||||
Object defaultValue = newValue(desc.getType(), desc, tokens.canonicalName);
|
||||
return new PropertyValue(tokens.canonicalName, defaultValue);
|
||||
Object defaultValue = newValue(desc.getType(), desc, canonicalName);
|
||||
return new PropertyValue(canonicalName, defaultValue);
|
||||
}
|
||||
|
||||
private Object newValue(Class<?> type, @Nullable TypeDescriptor desc, String name) {
|
||||
@@ -942,70 +948,6 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the given property name into the corresponding property name tokens.
|
||||
* @param propertyName the property name to parse
|
||||
* @return representation of the parsed property tokens
|
||||
*/
|
||||
private PropertyTokenHolder getPropertyNameTokens(String propertyName) {
|
||||
String actualName = null;
|
||||
List<String> keys = new ArrayList<>(2);
|
||||
int searchIndex = 0;
|
||||
while (searchIndex != -1) {
|
||||
int keyStart = propertyName.indexOf(PROPERTY_KEY_PREFIX, searchIndex);
|
||||
searchIndex = -1;
|
||||
if (keyStart != -1) {
|
||||
int keyEnd = getPropertyNameKeyEnd(propertyName, keyStart + PROPERTY_KEY_PREFIX.length());
|
||||
if (keyEnd != -1) {
|
||||
if (actualName == null) {
|
||||
actualName = propertyName.substring(0, keyStart);
|
||||
}
|
||||
String key = propertyName.substring(keyStart + PROPERTY_KEY_PREFIX.length(), keyEnd);
|
||||
if (key.length() > 1 && ((key.startsWith("'") && key.endsWith("'")) ||
|
||||
(key.startsWith("\"") && key.endsWith("\"")))) {
|
||||
key = key.substring(1, key.length() - 1);
|
||||
}
|
||||
keys.add(key);
|
||||
searchIndex = keyEnd + PROPERTY_KEY_SUFFIX.length();
|
||||
}
|
||||
}
|
||||
}
|
||||
PropertyTokenHolder tokens = new PropertyTokenHolder(actualName != null ? actualName : propertyName);
|
||||
if (!keys.isEmpty()) {
|
||||
tokens.canonicalName += PROPERTY_KEY_PREFIX +
|
||||
StringUtils.collectionToDelimitedString(keys, PROPERTY_KEY_SUFFIX + PROPERTY_KEY_PREFIX) +
|
||||
PROPERTY_KEY_SUFFIX;
|
||||
tokens.keys = StringUtils.toStringArray(keys);
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private int getPropertyNameKeyEnd(String propertyName, int startIndex) {
|
||||
int unclosedPrefixes = 0;
|
||||
int length = propertyName.length();
|
||||
for (int i = startIndex; i < length; i++) {
|
||||
switch (propertyName.charAt(i)) {
|
||||
case PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR -> {
|
||||
// The property name contains opening prefix(es)...
|
||||
unclosedPrefixes++;
|
||||
}
|
||||
case PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR -> {
|
||||
if (unclosedPrefixes == 0) {
|
||||
// No unclosed prefix(es) in the property name (left) ->
|
||||
// this is the suffix we are looking for.
|
||||
return i;
|
||||
}
|
||||
else {
|
||||
// This suffix does not close the initial prefix but rather
|
||||
// just one that occurred within the property name.
|
||||
unclosedPrefixes--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
String className = getClass().getName();
|
||||
@@ -1016,6 +958,17 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The result of resolving a property path: the accessor owning its final
|
||||
* segment, and that segment itself.
|
||||
* @since 7.1
|
||||
* @param accessor the accessor for the target bean
|
||||
* @param segment the final segment of the resolved path (the property to
|
||||
* actually get or set on {@code accessor})
|
||||
*/
|
||||
protected record ResolvedProperty(AbstractNestablePropertyAccessor accessor, PropertyPath.Segment segment) {}
|
||||
|
||||
|
||||
/**
|
||||
* A handler for a specific property.
|
||||
*/
|
||||
@@ -1072,22 +1025,4 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Holder class used to store property tokens.
|
||||
*/
|
||||
protected static class PropertyTokenHolder {
|
||||
|
||||
public PropertyTokenHolder(String name) {
|
||||
this.actualName = name;
|
||||
this.canonicalName = name;
|
||||
}
|
||||
|
||||
public String actualName;
|
||||
|
||||
public String canonicalName;
|
||||
|
||||
public String @Nullable [] keys;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,13 +23,17 @@ import java.util.Map;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Abstract implementation of the {@link PropertyAccessor} interface.
|
||||
* Provides base implementations of all convenience methods, with the
|
||||
*
|
||||
* <p>Provides base implementations of all convenience methods, with the
|
||||
* implementation of actual property access left to subclasses.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Stephane Nicoll
|
||||
* @author Sam Brannen
|
||||
* @since 2.0
|
||||
* @see #getPropertyValue
|
||||
* @see #setPropertyValue
|
||||
@@ -40,6 +44,10 @@ public abstract class AbstractPropertyAccessor extends TypeConverterSupport impl
|
||||
|
||||
private boolean autoGrowNestedPaths = false;
|
||||
|
||||
private int autoGrowCollectionLimit = Integer.MAX_VALUE;
|
||||
|
||||
private int maxNestedPathDepth = DEFAULT_MAX_NESTED_PATH_DEPTH;
|
||||
|
||||
boolean suppressNotWritablePropertyException = false;
|
||||
|
||||
|
||||
@@ -63,6 +71,27 @@ public abstract class AbstractPropertyAccessor extends TypeConverterSupport impl
|
||||
return this.autoGrowNestedPaths;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAutoGrowCollectionLimit(int autoGrowCollectionLimit) {
|
||||
this.autoGrowCollectionLimit = autoGrowCollectionLimit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAutoGrowCollectionLimit() {
|
||||
return this.autoGrowCollectionLimit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMaxNestedPathDepth(int maxNestedPathDepth) {
|
||||
Assert.isTrue(maxNestedPathDepth >= 0, "'maxNestedPathDepth' must not be negative");
|
||||
this.maxNestedPathDepth = maxNestedPathDepth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxNestedPathDepth() {
|
||||
return this.maxNestedPathDepth;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void setPropertyValue(PropertyValue pv) throws BeansException {
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.beans;
|
||||
|
||||
import java.beans.PropertyDescriptor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
@@ -213,9 +214,17 @@ public class BeanWrapperImpl extends AbstractNestablePropertyAccessor implements
|
||||
|
||||
@Override
|
||||
public PropertyDescriptor getPropertyDescriptor(String propertyName) throws InvalidPropertyException {
|
||||
BeanWrapperImpl nestedBw = (BeanWrapperImpl) getPropertyAccessorForPropertyPath(propertyName);
|
||||
String finalPath = getFinalPath(nestedBw, propertyName);
|
||||
PropertyDescriptor pd = nestedBw.getCachedIntrospectionResults().getPropertyDescriptor(finalPath);
|
||||
ResolvedProperty resolved;
|
||||
try {
|
||||
resolved = resolvePropertyPath(propertyName);
|
||||
}
|
||||
catch (InvalidPropertyPathException ex) {
|
||||
throw new InvalidPropertyException(
|
||||
getRootClass(), getNestedPath() + propertyName, Objects.requireNonNull(ex.getMessage()), ex);
|
||||
}
|
||||
BeanWrapperImpl nestedBw = (BeanWrapperImpl) resolved.accessor();
|
||||
PropertyDescriptor pd = nestedBw.getCachedIntrospectionResults()
|
||||
.getPropertyDescriptor(resolved.segment().toCanonicalName());
|
||||
if (pd == null) {
|
||||
throw new InvalidPropertyException(getRootClass(), getNestedPath() + propertyName,
|
||||
"No property '" + propertyName + "' found");
|
||||
|
||||
+55
-3
@@ -21,19 +21,39 @@ import org.jspecify.annotations.Nullable;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
|
||||
/**
|
||||
* Interface that encapsulates configuration methods for a PropertyAccessor.
|
||||
* Also extends the PropertyEditorRegistry interface, which defines methods
|
||||
* for PropertyEditor management.
|
||||
* Interface that encapsulates configuration methods for a {@link PropertyAccessor}.
|
||||
*
|
||||
* <p>Also extends the {@link PropertyEditorRegistry} interface, which defines methods
|
||||
* for {@link java.beans.PropertyEditor} management.
|
||||
*
|
||||
* <p>Serves as base interface for {@link BeanWrapper}.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Stephane Nicoll
|
||||
* @author Sam Brannen
|
||||
* @since 2.0
|
||||
* @see BeanWrapper
|
||||
*/
|
||||
public interface ConfigurablePropertyAccessor extends PropertyAccessor, PropertyEditorRegistry, TypeConverter {
|
||||
|
||||
/**
|
||||
* Default maximum nesting depth permitted for a nested property path: {@value}.
|
||||
* <p>This limit guards against deeply nested property paths that could otherwise
|
||||
* drive the recursive resolution of a nested property path to exhaust the current
|
||||
* thread's call stack.
|
||||
* <p><strong>NOTE</strong>: This limit improves diagnostics for the common case
|
||||
* by converting what would otherwise be an opaque {@link StackOverflowError}
|
||||
* into a descriptive {@link InvalidPropertyException}, but it is <em>not</em>
|
||||
* a guaranteed defense against {@code StackOverflowError} under every possible
|
||||
* JVM thread stack size configuration. The amount of stack space consumed per
|
||||
* level of nesting depends on the JVM, its current JIT compilation state, and
|
||||
* the platform.
|
||||
* @since 7.1
|
||||
* @see #setMaxNestedPathDepth(int)
|
||||
*/
|
||||
int DEFAULT_MAX_NESTED_PATH_DEPTH = 100;
|
||||
|
||||
|
||||
/**
|
||||
* Specify a {@link ConversionService} to use for converting
|
||||
* property values, as an alternative to JavaBeans PropertyEditors.
|
||||
@@ -74,4 +94,36 @@ public interface ConfigurablePropertyAccessor extends PropertyAccessor, Property
|
||||
*/
|
||||
boolean isAutoGrowNestedPaths();
|
||||
|
||||
/**
|
||||
* Specify a limit for array and collection auto-growing.
|
||||
* <p>Default is unlimited on a plain accessor.
|
||||
* @since 7.1
|
||||
*/
|
||||
void setAutoGrowCollectionLimit(int autoGrowCollectionLimit);
|
||||
|
||||
/**
|
||||
* Return the limit for array and collection auto-growing.
|
||||
* @since 7.1
|
||||
*/
|
||||
int getAutoGrowCollectionLimit();
|
||||
|
||||
/**
|
||||
* Specify the maximum nesting depth permitted for a nested property path.
|
||||
* <p>The nesting depth corresponds to the number of intermediate properties
|
||||
* traversed to reach the final property — for example,
|
||||
* {@code "address.country.name"} has a nesting depth of 2.
|
||||
* <p>Specify {@code 0} to disable nested property paths altogether, while
|
||||
* still allowing simple, indexed, and mapped property access.
|
||||
* <p>Default is {@link #DEFAULT_MAX_NESTED_PATH_DEPTH}.
|
||||
* @param maxNestedPathDepth the maximum nesting depth; must not be negative
|
||||
* @since 7.1
|
||||
*/
|
||||
void setMaxNestedPathDepth(int maxNestedPathDepth);
|
||||
|
||||
/**
|
||||
* Return the maximum nesting depth permitted for a nested property path.
|
||||
* @since 7.1
|
||||
*/
|
||||
int getMaxNestedPathDepth();
|
||||
|
||||
}
|
||||
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.beans;
|
||||
|
||||
import java.beans.PropertyChangeEvent;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Exception thrown when a property path is not a well-formed property path
|
||||
* according to the grammar implemented by {@link PropertyPath}.
|
||||
*
|
||||
* <p>This signals a syntactically invalid path, as opposed to a
|
||||
* syntactically valid path that happens not to resolve against a particular
|
||||
* target object. The latter cases are reported as {@link NotReadablePropertyException}
|
||||
* or {@link NotWritablePropertyException} instead.
|
||||
*
|
||||
* <p>Extends {@link PropertyAccessException} so that a malformed path
|
||||
* encountered while binding a single property value can be collected into a
|
||||
* {@link PropertyBatchUpdateException} alongside other per-property failures,
|
||||
* rather than aborting the whole binding operation.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 7.1
|
||||
* @see PropertyPath#parse(String)
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class InvalidPropertyPathException extends PropertyAccessException {
|
||||
|
||||
/**
|
||||
* Error code that an {@code InvalidPropertyPathException} is registered with.
|
||||
*/
|
||||
public static final String ERROR_CODE = "invalidPropertyPath";
|
||||
|
||||
|
||||
private final String propertyPath;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new {@code InvalidPropertyPathException}.
|
||||
* @param propertyPath the offending property path
|
||||
* @param reason a description of the grammar rule that was violated
|
||||
*/
|
||||
public InvalidPropertyPathException(String propertyPath, String reason) {
|
||||
super("Invalid property path '" + propertyPath + "': " + reason, null);
|
||||
this.propertyPath = propertyPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@code InvalidPropertyPathException}.
|
||||
* @param propertyChangeEvent the event for the property
|
||||
* @param cause the original parsing exception
|
||||
*/
|
||||
public InvalidPropertyPathException(PropertyChangeEvent propertyChangeEvent, InvalidPropertyPathException cause) {
|
||||
super(propertyChangeEvent, Objects.requireNonNull(cause.getMessage()), cause);
|
||||
this.propertyPath = cause.propertyPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@code InvalidPropertyPathException}.
|
||||
* @param source the bean that fired the event
|
||||
* @param propertyName the programmatic name of the property that was changed
|
||||
* @param newValue the new value of the property
|
||||
* @param cause the original parsing exception
|
||||
*/
|
||||
public InvalidPropertyPathException(Object source, String propertyName, @Nullable Object newValue, InvalidPropertyPathException cause) {
|
||||
this(new PropertyChangeEvent(source, propertyName, null, newValue), cause);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the offending property path.
|
||||
*/
|
||||
public String getPropertyPath() {
|
||||
return this.propertyPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getErrorCode() {
|
||||
return ERROR_CODE;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -23,8 +23,19 @@ import org.jspecify.annotations.Nullable;
|
||||
* according to the {@link PropertyAccessor} interface.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 1.2.6
|
||||
* @deprecated as of 7.1, in favor of {@link PropertyPath}. Use
|
||||
* {@link PropertyPath#parse(String)} and {@link PropertyPath#canonicalName()}
|
||||
* in place of {@link #canonicalPropertyName} and {@link #canonicalPropertyNames},
|
||||
* and match a registered path against a property by comparing canonical names
|
||||
* in place of {@link #matchesProperty}. There is no direct replacement for
|
||||
* {@link #getPropertyName}, {@link #isNestedOrIndexedProperty},
|
||||
* {@link #getFirstNestedPropertySeparatorIndex}, or
|
||||
* {@link #getLastNestedPropertySeparatorIndex}, which operate on raw,
|
||||
* unparsed property path text.
|
||||
*/
|
||||
@Deprecated(since = "7.1", forRemoval = true)
|
||||
public abstract class PropertyAccessorUtils {
|
||||
|
||||
/**
|
||||
@@ -81,21 +92,23 @@ public abstract class PropertyAccessorUtils {
|
||||
/**
|
||||
* Determine the first (or last) nested property separator in the
|
||||
* given property path, ignoring dots in keys (like "map[my.key]").
|
||||
* <p>Also tracks bracket nesting depth so that keys containing an unbalanced
|
||||
* number of {@code [} or {@code ]} characters do not interfere with the
|
||||
* detection of separators that precede or follow the key.
|
||||
* @param propertyPath the property path to check
|
||||
* @param last whether to return the last separator rather than the first
|
||||
* @return the index of the nested property separator, or -1 if none
|
||||
*/
|
||||
private static int getNestedPropertySeparatorIndex(String propertyPath, boolean last) {
|
||||
boolean inKey = false;
|
||||
int depth = 0;
|
||||
int length = propertyPath.length();
|
||||
int i = (last ? length - 1 : 0);
|
||||
while (last ? i >= 0 : i < length) {
|
||||
switch (propertyPath.charAt(i)) {
|
||||
case PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR, PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR -> {
|
||||
inKey = !inKey;
|
||||
}
|
||||
case PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR -> depth += (last ? -1 : 1);
|
||||
case PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR -> depth += (last ? 1 : -1);
|
||||
case PropertyAccessor.NESTED_PROPERTY_SEPARATOR_CHAR -> {
|
||||
if (!inKey) {
|
||||
if (depth <= 0) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
@@ -118,17 +131,21 @@ public abstract class PropertyAccessorUtils {
|
||||
* @return whether the paths match
|
||||
*/
|
||||
public static boolean matchesProperty(String registeredPath, String propertyPath) {
|
||||
if (!registeredPath.startsWith(propertyPath)) {
|
||||
// canonicalPropertyName, not PropertyPath.parse directly: this method's
|
||||
// long-standing contract is non-throwing, even for a malformed path.
|
||||
String registered = canonicalPropertyName(registeredPath);
|
||||
String property = canonicalPropertyName(propertyPath);
|
||||
if (!registered.startsWith(property)) {
|
||||
return false;
|
||||
}
|
||||
if (registeredPath.length() == propertyPath.length()) {
|
||||
if (registered.length() == property.length()) {
|
||||
return true;
|
||||
}
|
||||
if (registeredPath.charAt(propertyPath.length()) != PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR) {
|
||||
if (registered.charAt(property.length()) != PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR) {
|
||||
return false;
|
||||
}
|
||||
return (registeredPath.indexOf(PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR, propertyPath.length() + 1) ==
|
||||
registeredPath.length() - 1);
|
||||
return (registered.indexOf(PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR, property.length() + 1) ==
|
||||
registered.length() - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,31 +157,7 @@ public abstract class PropertyAccessorUtils {
|
||||
* @return the canonical representation of the property path
|
||||
*/
|
||||
public static String canonicalPropertyName(@Nullable String propertyName) {
|
||||
if (propertyName == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
StringBuilder sb = new StringBuilder(propertyName);
|
||||
int searchIndex = 0;
|
||||
while (searchIndex != -1) {
|
||||
int keyStart = sb.indexOf(PropertyAccessor.PROPERTY_KEY_PREFIX, searchIndex);
|
||||
searchIndex = -1;
|
||||
if (keyStart != -1) {
|
||||
int keyEnd = sb.indexOf(
|
||||
PropertyAccessor.PROPERTY_KEY_SUFFIX, keyStart + PropertyAccessor.PROPERTY_KEY_PREFIX.length());
|
||||
if (keyEnd != -1) {
|
||||
String key = sb.substring(keyStart + PropertyAccessor.PROPERTY_KEY_PREFIX.length(), keyEnd);
|
||||
if (key.length() > 1 && ((key.startsWith("'") && key.endsWith("'")) ||
|
||||
(key.startsWith("\"") && key.endsWith("\"")))) {
|
||||
sb.delete(keyStart + 1, keyStart + 2);
|
||||
sb.delete(keyEnd - 2, keyEnd - 1);
|
||||
keyEnd = keyEnd - 2;
|
||||
}
|
||||
searchIndex = keyEnd + PropertyAccessor.PROPERTY_KEY_SUFFIX.length();
|
||||
}
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
return PropertyPath.canonicalNameOrOriginal(propertyName);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.springframework.beans;
|
||||
import java.beans.IntrospectionException;
|
||||
import java.beans.PropertyDescriptor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Enumeration;
|
||||
@@ -50,6 +51,12 @@ abstract class PropertyDescriptorUtils {
|
||||
* <p>This just supports the basic JavaBeans conventions, without indexed
|
||||
* properties or any customizers, and without other BeanInfo metadata.
|
||||
* For standard JavaBeans introspection, use the JavaBeans Introspector.
|
||||
* <p>Note that, in contrast to the standard {@link java.beans.Introspector},
|
||||
* this method does support a static {@code set} method as the write method
|
||||
* for a property, resulting in a write-only property if no corresponding
|
||||
* instance {@code get}/{@code is} method is present. Static {@code get} and
|
||||
* {@code is} methods, on the other hand, are never considered read methods
|
||||
* for a property, aligning with standard JavaBeans introspection.
|
||||
* @param beanClass the target class to introspect
|
||||
* @return a collection of property descriptors
|
||||
* @throws IntrospectionException from introspecting the given bean class
|
||||
@@ -71,11 +78,13 @@ abstract class PropertyDescriptorUtils {
|
||||
setter = true;
|
||||
nameIndex = 3;
|
||||
}
|
||||
else if (methodName.startsWith("get") && method.getParameterCount() == 0 && method.getReturnType() != void.class) {
|
||||
else if (methodName.startsWith("get") && method.getParameterCount() == 0 &&
|
||||
method.getReturnType() != void.class && !Modifier.isStatic(method.getModifiers())) {
|
||||
setter = false;
|
||||
nameIndex = 3;
|
||||
}
|
||||
else if (methodName.startsWith("is") && method.getParameterCount() == 0 && method.getReturnType() == boolean.class) {
|
||||
else if (methodName.startsWith("is") && method.getParameterCount() == 0 &&
|
||||
method.getReturnType() == boolean.class && !Modifier.isStatic(method.getModifiers())) {
|
||||
setter = false;
|
||||
nameIndex = 2;
|
||||
}
|
||||
|
||||
+129
-64
@@ -79,12 +79,15 @@ import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Base implementation of the {@link PropertyEditorRegistry} interface.
|
||||
* Provides management of default editors and custom editors.
|
||||
* Mainly serves as base class for {@link BeanWrapperImpl}.
|
||||
*
|
||||
* <p>Provides management of default editors and custom editors.
|
||||
*
|
||||
* <p>Mainly serves as base class for {@link BeanWrapperImpl}.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Rob Harrop
|
||||
* @author Sebastien Deleuze
|
||||
* @author Sam Brannen
|
||||
* @since 1.2.6
|
||||
* @see java.beans.PropertyEditorManager
|
||||
* @see java.beans.PropertyEditorSupport#setAsText
|
||||
@@ -92,6 +95,15 @@ import org.springframework.util.ClassUtils;
|
||||
*/
|
||||
public class PropertyEditorRegistrySupport implements PropertyEditorRegistry {
|
||||
|
||||
/**
|
||||
* The maximum number of {@code [key]} segments that {@link #addStrippedPropertyPaths}
|
||||
* will process in a single property path, to avoid excessive recursion and the
|
||||
* resulting exponential blowup in the number of generated stripped paths for
|
||||
* property paths with a large number of {@code [key]} segments.
|
||||
* @since 7.1
|
||||
*/
|
||||
private static final int MAX_STRIPPED_PROPERTY_PATH_DEPTH = 8;
|
||||
|
||||
private @Nullable ConversionService conversionService;
|
||||
|
||||
private boolean defaultEditorsActive = false;
|
||||
@@ -100,8 +112,7 @@ public class PropertyEditorRegistrySupport implements PropertyEditorRegistry {
|
||||
|
||||
private @Nullable PropertyEditorRegistrar defaultEditorRegistrar;
|
||||
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
private Map<Class<?>, PropertyEditor> defaultEditors;
|
||||
private @Nullable Map<Class<?>, PropertyEditor> defaultEditors;
|
||||
|
||||
private @Nullable Map<Class<?>, PropertyEditor> overriddenDefaultEditors;
|
||||
|
||||
@@ -201,7 +212,7 @@ public class PropertyEditorRegistrySupport implements PropertyEditorRegistry {
|
||||
}
|
||||
}
|
||||
if (this.defaultEditors == null) {
|
||||
createDefaultEditors();
|
||||
this.defaultEditors = createDefaultEditors();
|
||||
}
|
||||
return this.defaultEditors.get(requiredType);
|
||||
}
|
||||
@@ -209,75 +220,77 @@ public class PropertyEditorRegistrySupport implements PropertyEditorRegistry {
|
||||
/**
|
||||
* Actually register the default editors for this registry instance.
|
||||
*/
|
||||
private void createDefaultEditors() {
|
||||
this.defaultEditors = new HashMap<>(64);
|
||||
private Map<Class<?>, PropertyEditor> createDefaultEditors() {
|
||||
Map<Class<?>, PropertyEditor> defaultEditors = new HashMap<>(64);
|
||||
|
||||
// Simple editors, without parameterization capabilities.
|
||||
// The JDK does not contain a default editor for any of these target types.
|
||||
this.defaultEditors.put(Charset.class, new CharsetEditor());
|
||||
this.defaultEditors.put(Class.class, new ClassEditor());
|
||||
this.defaultEditors.put(Class[].class, new ClassArrayEditor());
|
||||
this.defaultEditors.put(Currency.class, new CurrencyEditor());
|
||||
this.defaultEditors.put(File.class, new FileEditor());
|
||||
this.defaultEditors.put(InputStream.class, new InputStreamEditor());
|
||||
this.defaultEditors.put(InputSource.class, new InputSourceEditor());
|
||||
this.defaultEditors.put(Locale.class, new LocaleEditor());
|
||||
this.defaultEditors.put(Path.class, new PathEditor());
|
||||
this.defaultEditors.put(Pattern.class, new PatternEditor());
|
||||
this.defaultEditors.put(Properties.class, new PropertiesEditor());
|
||||
this.defaultEditors.put(Reader.class, new ReaderEditor());
|
||||
this.defaultEditors.put(Resource[].class, new ResourceArrayPropertyEditor());
|
||||
this.defaultEditors.put(TimeZone.class, new TimeZoneEditor());
|
||||
this.defaultEditors.put(URI.class, new URIEditor());
|
||||
this.defaultEditors.put(URL.class, new URLEditor());
|
||||
this.defaultEditors.put(UUID.class, new UUIDEditor());
|
||||
this.defaultEditors.put(ZoneId.class, new ZoneIdEditor());
|
||||
defaultEditors.put(Charset.class, new CharsetEditor());
|
||||
defaultEditors.put(Class.class, new ClassEditor());
|
||||
defaultEditors.put(Class[].class, new ClassArrayEditor());
|
||||
defaultEditors.put(Currency.class, new CurrencyEditor());
|
||||
defaultEditors.put(File.class, new FileEditor());
|
||||
defaultEditors.put(InputStream.class, new InputStreamEditor());
|
||||
defaultEditors.put(InputSource.class, new InputSourceEditor());
|
||||
defaultEditors.put(Locale.class, new LocaleEditor());
|
||||
defaultEditors.put(Path.class, new PathEditor());
|
||||
defaultEditors.put(Pattern.class, new PatternEditor());
|
||||
defaultEditors.put(Properties.class, new PropertiesEditor());
|
||||
defaultEditors.put(Reader.class, new ReaderEditor());
|
||||
defaultEditors.put(Resource[].class, new ResourceArrayPropertyEditor());
|
||||
defaultEditors.put(TimeZone.class, new TimeZoneEditor());
|
||||
defaultEditors.put(URI.class, new URIEditor());
|
||||
defaultEditors.put(URL.class, new URLEditor());
|
||||
defaultEditors.put(UUID.class, new UUIDEditor());
|
||||
defaultEditors.put(ZoneId.class, new ZoneIdEditor());
|
||||
|
||||
// Default instances of collection editors.
|
||||
// Can be overridden by registering custom instances of those as custom editors.
|
||||
this.defaultEditors.put(Collection.class, new CustomCollectionEditor(Collection.class));
|
||||
this.defaultEditors.put(Set.class, new CustomCollectionEditor(Set.class));
|
||||
this.defaultEditors.put(SortedSet.class, new CustomCollectionEditor(SortedSet.class));
|
||||
this.defaultEditors.put(List.class, new CustomCollectionEditor(List.class));
|
||||
this.defaultEditors.put(SortedMap.class, new CustomMapEditor(SortedMap.class));
|
||||
defaultEditors.put(Collection.class, new CustomCollectionEditor(Collection.class));
|
||||
defaultEditors.put(Set.class, new CustomCollectionEditor(Set.class));
|
||||
defaultEditors.put(SortedSet.class, new CustomCollectionEditor(SortedSet.class));
|
||||
defaultEditors.put(List.class, new CustomCollectionEditor(List.class));
|
||||
defaultEditors.put(SortedMap.class, new CustomMapEditor(SortedMap.class));
|
||||
|
||||
// Default editors for primitive arrays.
|
||||
this.defaultEditors.put(byte[].class, new ByteArrayPropertyEditor());
|
||||
this.defaultEditors.put(char[].class, new CharArrayPropertyEditor());
|
||||
defaultEditors.put(byte[].class, new ByteArrayPropertyEditor());
|
||||
defaultEditors.put(char[].class, new CharArrayPropertyEditor());
|
||||
|
||||
// The JDK does not contain a default editor for char!
|
||||
this.defaultEditors.put(char.class, new CharacterEditor(false));
|
||||
this.defaultEditors.put(Character.class, new CharacterEditor(true));
|
||||
defaultEditors.put(char.class, new CharacterEditor(false));
|
||||
defaultEditors.put(Character.class, new CharacterEditor(true));
|
||||
|
||||
// Spring's CustomBooleanEditor accepts more flag values than the JDK's default editor.
|
||||
this.defaultEditors.put(boolean.class, new CustomBooleanEditor(false));
|
||||
this.defaultEditors.put(Boolean.class, new CustomBooleanEditor(true));
|
||||
defaultEditors.put(boolean.class, new CustomBooleanEditor(false));
|
||||
defaultEditors.put(Boolean.class, new CustomBooleanEditor(true));
|
||||
|
||||
// The JDK does not contain default editors for number wrapper types!
|
||||
// Override JDK primitive number editors with our own CustomNumberEditor.
|
||||
this.defaultEditors.put(byte.class, new CustomNumberEditor(Byte.class, false));
|
||||
this.defaultEditors.put(Byte.class, new CustomNumberEditor(Byte.class, true));
|
||||
this.defaultEditors.put(short.class, new CustomNumberEditor(Short.class, false));
|
||||
this.defaultEditors.put(Short.class, new CustomNumberEditor(Short.class, true));
|
||||
this.defaultEditors.put(int.class, new CustomNumberEditor(Integer.class, false));
|
||||
this.defaultEditors.put(Integer.class, new CustomNumberEditor(Integer.class, true));
|
||||
this.defaultEditors.put(long.class, new CustomNumberEditor(Long.class, false));
|
||||
this.defaultEditors.put(Long.class, new CustomNumberEditor(Long.class, true));
|
||||
this.defaultEditors.put(float.class, new CustomNumberEditor(Float.class, false));
|
||||
this.defaultEditors.put(Float.class, new CustomNumberEditor(Float.class, true));
|
||||
this.defaultEditors.put(double.class, new CustomNumberEditor(Double.class, false));
|
||||
this.defaultEditors.put(Double.class, new CustomNumberEditor(Double.class, true));
|
||||
this.defaultEditors.put(BigDecimal.class, new CustomNumberEditor(BigDecimal.class, true));
|
||||
this.defaultEditors.put(BigInteger.class, new CustomNumberEditor(BigInteger.class, true));
|
||||
defaultEditors.put(byte.class, new CustomNumberEditor(Byte.class, false));
|
||||
defaultEditors.put(Byte.class, new CustomNumberEditor(Byte.class, true));
|
||||
defaultEditors.put(short.class, new CustomNumberEditor(Short.class, false));
|
||||
defaultEditors.put(Short.class, new CustomNumberEditor(Short.class, true));
|
||||
defaultEditors.put(int.class, new CustomNumberEditor(Integer.class, false));
|
||||
defaultEditors.put(Integer.class, new CustomNumberEditor(Integer.class, true));
|
||||
defaultEditors.put(long.class, new CustomNumberEditor(Long.class, false));
|
||||
defaultEditors.put(Long.class, new CustomNumberEditor(Long.class, true));
|
||||
defaultEditors.put(float.class, new CustomNumberEditor(Float.class, false));
|
||||
defaultEditors.put(Float.class, new CustomNumberEditor(Float.class, true));
|
||||
defaultEditors.put(double.class, new CustomNumberEditor(Double.class, false));
|
||||
defaultEditors.put(Double.class, new CustomNumberEditor(Double.class, true));
|
||||
defaultEditors.put(BigDecimal.class, new CustomNumberEditor(BigDecimal.class, true));
|
||||
defaultEditors.put(BigInteger.class, new CustomNumberEditor(BigInteger.class, true));
|
||||
|
||||
// Only register config value editors if explicitly requested.
|
||||
if (this.configValueEditorsActive) {
|
||||
StringArrayPropertyEditor sae = new StringArrayPropertyEditor();
|
||||
this.defaultEditors.put(String[].class, sae);
|
||||
this.defaultEditors.put(short[].class, sae);
|
||||
this.defaultEditors.put(int[].class, sae);
|
||||
this.defaultEditors.put(long[].class, sae);
|
||||
defaultEditors.put(String[].class, sae);
|
||||
defaultEditors.put(short[].class, sae);
|
||||
defaultEditors.put(int[].class, sae);
|
||||
defaultEditors.put(long[].class, sae);
|
||||
}
|
||||
|
||||
return defaultEditors;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -360,7 +373,7 @@ public class PropertyEditorRegistrySupport implements PropertyEditorRegistry {
|
||||
public boolean hasCustomEditorForElement(@Nullable Class<?> elementType, @Nullable String propertyPath) {
|
||||
if (propertyPath != null && this.customEditorsForPath != null) {
|
||||
for (Map.Entry<String, CustomEditorHolder> entry : this.customEditorsForPath.entrySet()) {
|
||||
if (PropertyAccessorUtils.matchesProperty(entry.getKey(), propertyPath) &&
|
||||
if (matchesProperty(entry.getKey(), propertyPath) &&
|
||||
entry.getValue().getPropertyEditor(elementType) != null) {
|
||||
return true;
|
||||
}
|
||||
@@ -370,6 +383,26 @@ public class PropertyEditorRegistrySupport implements PropertyEditorRegistry {
|
||||
return (elementType != null && this.customEditors != null && this.customEditors.containsKey(elementType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether {@code registeredPath} is {@code propertyPath} itself or one
|
||||
* indexed element of it.
|
||||
*/
|
||||
private static boolean matchesProperty(String registeredPath, String propertyPath) {
|
||||
String canonicalRegisteredPath = PropertyPath.canonicalNameOrOriginal(registeredPath);
|
||||
String canonicalPropertyPath = PropertyPath.canonicalNameOrOriginal(propertyPath);
|
||||
if (!canonicalRegisteredPath.startsWith(canonicalPropertyPath)) {
|
||||
return false;
|
||||
}
|
||||
if (canonicalRegisteredPath.length() == canonicalPropertyPath.length()) {
|
||||
return true;
|
||||
}
|
||||
if (canonicalRegisteredPath.charAt(canonicalPropertyPath.length()) != PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR) {
|
||||
return false;
|
||||
}
|
||||
return (canonicalRegisteredPath.indexOf(PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR, canonicalPropertyPath.length() + 1) ==
|
||||
canonicalRegisteredPath.length() - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the property type for the given property path.
|
||||
* <p>Called by {@link #findCustomEditor} if no required type has been specified,
|
||||
@@ -470,18 +503,25 @@ public class PropertyEditorRegistrySupport implements PropertyEditorRegistry {
|
||||
* will be copied. If this is null, all editors will be copied.
|
||||
*/
|
||||
protected void copyCustomEditorsTo(PropertyEditorRegistry target, @Nullable String nestedProperty) {
|
||||
String actualPropertyName =
|
||||
(nestedProperty != null ? PropertyAccessorUtils.getPropertyName(nestedProperty) : null);
|
||||
String actualPropertyName = (nestedProperty != null ? actualPropertyNameOf(nestedProperty) : null);
|
||||
if (this.customEditors != null) {
|
||||
this.customEditors.forEach(target::registerCustomEditor);
|
||||
}
|
||||
if (this.customEditorsForPath != null) {
|
||||
this.customEditorsForPath.forEach((editorPath, editorHolder) -> {
|
||||
if (nestedProperty != null) {
|
||||
int pos = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(editorPath);
|
||||
if (pos != -1) {
|
||||
String editorNestedProperty = editorPath.substring(0, pos);
|
||||
String editorNestedPath = editorPath.substring(pos + 1);
|
||||
PropertyPath editorPropertyPath;
|
||||
try {
|
||||
editorPropertyPath = PropertyPath.parse(editorPath);
|
||||
}
|
||||
catch (InvalidPropertyPathException ex) {
|
||||
// Not a well-formed path; nothing to nest into.
|
||||
return;
|
||||
}
|
||||
List<PropertyPath.Segment> editorSegments = editorPropertyPath.segments();
|
||||
if (editorSegments.size() > 1) {
|
||||
String editorNestedProperty = editorSegments.get(0).toCanonicalName();
|
||||
String editorNestedPath = editorPropertyPath.subPath(1).canonicalName();
|
||||
if (editorNestedProperty.equals(nestedProperty) || editorNestedProperty.equals(actualPropertyName)) {
|
||||
target.registerCustomEditor(
|
||||
editorHolder.getRegisteredType(), editorNestedPath, editorHolder.getPropertyEditor());
|
||||
@@ -496,15 +536,40 @@ public class PropertyEditorRegistrySupport implements PropertyEditorRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The raw name of {@code nestedProperty}'s single segment with its keys stripped,
|
||||
* or {@code null} if malformed.
|
||||
*/
|
||||
private static @Nullable String actualPropertyNameOf(String nestedProperty) {
|
||||
try {
|
||||
List<PropertyPath.Segment> segments = PropertyPath.parse(nestedProperty).segments();
|
||||
return (segments.size() == 1 ? segments.get(0).name() : null);
|
||||
}
|
||||
catch (InvalidPropertyPathException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add property paths with all variations of stripped keys and/or indexes.
|
||||
* Invokes itself recursively with nested paths.
|
||||
* <p>Invokes itself recursively with nested paths, bounded to a nesting depth
|
||||
* of {@link #MAX_STRIPPED_PROPERTY_PATH_DEPTH}.
|
||||
* @param strippedPaths the result list to add to
|
||||
* @param nestedPath the current nested path
|
||||
* @param propertyPath the property path to check for keys/indexes to strip
|
||||
*/
|
||||
private void addStrippedPropertyPaths(List<String> strippedPaths, String nestedPath, String propertyPath) {
|
||||
addStrippedPropertyPaths(strippedPaths, nestedPath, propertyPath, 0);
|
||||
}
|
||||
|
||||
private void addStrippedPropertyPaths(
|
||||
List<String> strippedPaths, String nestedPath, String propertyPath, int depth) {
|
||||
|
||||
if (depth >= MAX_STRIPPED_PROPERTY_PATH_DEPTH) {
|
||||
// Avoid excessive recursion.
|
||||
return;
|
||||
}
|
||||
int startIndex = propertyPath.indexOf(PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR);
|
||||
if (startIndex != -1) {
|
||||
int endIndex = propertyPath.indexOf(PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR);
|
||||
@@ -515,9 +580,9 @@ public class PropertyEditorRegistrySupport implements PropertyEditorRegistry {
|
||||
// Strip the first key.
|
||||
strippedPaths.add(nestedPath + prefix + suffix);
|
||||
// Search for further keys to strip, with the first key stripped.
|
||||
addStrippedPropertyPaths(strippedPaths, nestedPath + prefix, suffix);
|
||||
addStrippedPropertyPaths(strippedPaths, nestedPath + prefix, suffix, depth + 1);
|
||||
// Search for further keys to strip, with the first key not stripped.
|
||||
addStrippedPropertyPaths(strippedPaths, nestedPath + prefix + key, suffix);
|
||||
addStrippedPropertyPaths(strippedPaths, nestedPath + prefix + key, suffix, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,539 @@
|
||||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.beans;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A parsed bean property path, such as {@code "person.addresses[1].city"}.
|
||||
*
|
||||
* <p>Parses bean property paths for property access: it decides whether a given
|
||||
* string is a well-formed property path (see grammar) and where each path
|
||||
* segment begins and ends. The {@linkplain #canonicalName() canonical form}
|
||||
* is used for policy matching and error reporting.
|
||||
* The {@linkplain #segments() structured segment list} is used for property
|
||||
* navigation within beans.
|
||||
*
|
||||
* <p>The grammar is:
|
||||
* <pre>
|
||||
* PropertyPath := Segment ('.' Segment)*
|
||||
* Segment := Name Index* -- Name may be empty only if at least one Index follows
|
||||
* Name := char* excluding '.', '[', ']'
|
||||
* Index := '[' Key ']'
|
||||
* Key := QuotedKey | RawKey
|
||||
* QuotedKey := "'" [^']* "'" | '"' [^"]* '"'
|
||||
* RawKey := char* excluding quote characters, with balanced '[' / ']' nesting
|
||||
* </pre>
|
||||
*
|
||||
* <p>Invalid property paths are rejected with {@link InvalidPropertyPathException}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 7.1
|
||||
*/
|
||||
public final class PropertyPath {
|
||||
|
||||
|
||||
private final String canonicalName;
|
||||
|
||||
private final List<Segment> segments;
|
||||
|
||||
|
||||
private PropertyPath(String canonicalName, List<Segment> segments) {
|
||||
this.canonicalName = canonicalName;
|
||||
this.segments = segments;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parse the given property path.
|
||||
* @param path the property path to parse; an empty string is a valid path
|
||||
* with no segments
|
||||
* @return the parsed property path
|
||||
* @throws InvalidPropertyPathException if the given path is not a
|
||||
* well-formed property path
|
||||
*/
|
||||
public static PropertyPath parse(String path) throws InvalidPropertyPathException {
|
||||
return parse(path, Options.UNLIMITED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the given property path, rejecting it if it exceeds the given {@code options}.
|
||||
* @param path the property path to parse; an empty string is a valid path
|
||||
* with no segments
|
||||
* @param options the parsing options to apply
|
||||
* @return the parsed property path
|
||||
* @throws InvalidPropertyPathException if the given path is not a
|
||||
* well-formed property path, or if it exceeds the given options
|
||||
*/
|
||||
public static PropertyPath parse(String path, Options options) throws InvalidPropertyPathException {
|
||||
Assert.notNull(path, "Property path must not be null");
|
||||
Assert.notNull(options, "Options must not be null");
|
||||
if (path.isEmpty()) {
|
||||
return new PropertyPath("", Collections.emptyList());
|
||||
}
|
||||
PropertyPath parsed = new Parser(path).parse();
|
||||
int nestingDepth = parsed.segments.size() - 1;
|
||||
if (nestingDepth > options.maxNestedPathDepth) {
|
||||
throw new InvalidPropertyPathException(path,
|
||||
"nesting depth exceeds the maximum of " + options.maxNestedPathDepth);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code path}'s canonical form, or {@code path} itself if it is not a
|
||||
* well-formed property path (including a {@code null} path, for which
|
||||
* this returns an empty string).
|
||||
* <p>A convenience for callers with nothing better to fall back to than
|
||||
* the original string, such as canonicalizing a user-supplied field name
|
||||
* for display, comparison, or configuration matching, as opposed to
|
||||
* {@link #parse(String)} itself, whose non-throwing behavior would be the
|
||||
* wrong default for a caller that is about to navigate an object graph.
|
||||
* @param path the property path to canonicalize, possibly {@code null}
|
||||
* @return the canonical form of {@code path}, or {@code path} unchanged
|
||||
* (or an empty string, if {@code path} is {@code null}) if it is not a
|
||||
* well-formed property path
|
||||
* @since 7.1
|
||||
*/
|
||||
public static String canonicalNameOrOriginal(@Nullable String path) {
|
||||
if (path == null) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return parse(path).canonicalName();
|
||||
}
|
||||
catch (InvalidPropertyPathException ex) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the canonical string form of this path.
|
||||
* <p>Unnecessary surrounding quotes are removed from keys:
|
||||
* {@code map['key'].name} → {@code map[key].name}.
|
||||
*/
|
||||
public String canonicalName() {
|
||||
return this.canonicalName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the segments of this path, in order, as an unmodifiable list.
|
||||
*/
|
||||
public List<Segment> segments() {
|
||||
return this.segments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the sub-path made up of this path's segments from the given
|
||||
* index to the end, such as the {@code "country.name"} sub-path of
|
||||
* {@code "address.country.name"} from index 1.
|
||||
* @param fromIndex the index of the first segment to include (inclusive)
|
||||
* @return the sub-path starting at {@code fromIndex}
|
||||
* @throws IndexOutOfBoundsException if {@code fromIndex} is negative
|
||||
* @throws IllegalArgumentException if {@code fromIndex} is greater than
|
||||
* {@link #segments()}{@code .size()}
|
||||
*/
|
||||
public PropertyPath subPath(int fromIndex) {
|
||||
if (fromIndex == 0) {
|
||||
return this;
|
||||
}
|
||||
List<Segment> subSegments = this.segments.subList(fromIndex, this.segments.size());
|
||||
StringBuilder subCanonicalName = new StringBuilder();
|
||||
for (int i = 0; i < subSegments.size(); i++) {
|
||||
if (i > 0) {
|
||||
subCanonicalName.append(PropertyAccessor.NESTED_PROPERTY_SEPARATOR_CHAR);
|
||||
}
|
||||
subCanonicalName.append(subSegments.get(i).toCanonicalName());
|
||||
}
|
||||
return new PropertyPath(subCanonicalName.toString(), subSegments);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
return (this == other || (other instanceof PropertyPath that &&
|
||||
this.canonicalName.equals(that.canonicalName)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.canonicalName.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.canonicalName;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A dot-separated segment of a property path. It consists of a
|
||||
* property name and the keys of any indexes applied to it, if the
|
||||
* target property is an indexed collection.
|
||||
* <p>For example, the path {@code "map[key].name"} has two segments:
|
||||
* {@code Segment["map", ["key"]]} and {@code Segment["name", []]}.
|
||||
* @param name the property name, which is empty only for a root-level
|
||||
* indexed access such as {@code "[user]"}
|
||||
* @param keys the keys of the indexes applied to the property, with any
|
||||
* surrounding quotes removed; never {@code null}, possibly empty
|
||||
*/
|
||||
public record Segment(String name, List<String> keys) {
|
||||
|
||||
public Segment(String name, List<String> keys) {
|
||||
Assert.notNull(name, "Segment name must not be null");
|
||||
Assert.notNull(keys, "Segment keys must not be null");
|
||||
this.name = name;
|
||||
this.keys = List.copyOf(keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* The canonical format of this segment alone: its name, followed by each
|
||||
* key wrapped in brackets, quoted only when necessary.
|
||||
*/
|
||||
public String toCanonicalName() {
|
||||
StringBuilder canonicalNameBuilder = new StringBuilder();
|
||||
canonicalNameBuilder.append(this.name);
|
||||
for (String key : this.keys) {
|
||||
appendCanonicalKey(canonicalNameBuilder, key);
|
||||
}
|
||||
return canonicalNameBuilder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new segment instance with the same name, but dropping the last key.
|
||||
*/
|
||||
public Segment withoutLastKey() {
|
||||
if (this.keys.isEmpty()) {
|
||||
return this;
|
||||
}
|
||||
return new Segment(this.name, this.keys.subList(0, this.keys.size() - 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Append the canonical form of the given key, which is the key
|
||||
* unquoted, or the key re-quoted if it contains illegal raw chars,
|
||||
* such as {@code map['a]b']}.
|
||||
*/
|
||||
private void appendCanonicalKey(StringBuilder canonicalNameBuilder, String key) {
|
||||
char quoteChar = canonicalQuoteChar(key);
|
||||
canonicalNameBuilder.append(PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR);
|
||||
if (quoteChar != 0) {
|
||||
canonicalNameBuilder.append(quoteChar);
|
||||
}
|
||||
canonicalNameBuilder.append(key);
|
||||
if (quoteChar != 0) {
|
||||
canonicalNameBuilder.append(quoteChar);
|
||||
}
|
||||
canonicalNameBuilder.append(PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the quote character to surround the given key with in the
|
||||
* canonical name, or {@code 0} if the key needs no quoting.
|
||||
*/
|
||||
private static char canonicalQuoteChar(String key) {
|
||||
int depth = 0;
|
||||
boolean balanced = true;
|
||||
boolean containsSingleQuote = false;
|
||||
boolean containsDoubleQuote = false;
|
||||
for (int i = 0; i < key.length(); i++) {
|
||||
switch (key.charAt(i)) {
|
||||
case PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR -> depth++;
|
||||
case PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR -> {
|
||||
if (--depth < 0) {
|
||||
balanced = false;
|
||||
}
|
||||
}
|
||||
case '\'' -> containsSingleQuote = true;
|
||||
case '"' -> containsDoubleQuote = true;
|
||||
default -> {
|
||||
// Ordinary key character.
|
||||
}
|
||||
}
|
||||
}
|
||||
if (balanced && depth == 0 && !containsSingleQuote && !containsDoubleQuote) {
|
||||
return 0;
|
||||
}
|
||||
return (containsSingleQuote && !containsDoubleQuote ? '"' : '\'');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Options to customize the parsing for {@link PropertyPath}.
|
||||
*/
|
||||
public static final class Options {
|
||||
|
||||
/**
|
||||
* Options with no limit on nesting depth.
|
||||
*/
|
||||
public static final Options UNLIMITED = new Options(Integer.MAX_VALUE);
|
||||
|
||||
private final int maxNestedPathDepth;
|
||||
|
||||
private Options(int maxNestedPathDepth) {
|
||||
this.maxNestedPathDepth = maxNestedPathDepth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an {@link Options} instance that rejects a path whose nesting
|
||||
* depth exceeds the given maximum.
|
||||
* @param maxNestedPathDepth the maximum nesting depth
|
||||
*/
|
||||
public static Options withMaxNestedPathDepth(int maxNestedPathDepth) {
|
||||
Assert.isTrue(maxNestedPathDepth >= 0, "'maxNestedPathDepth' must not be negative");
|
||||
return new Options(maxNestedPathDepth);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parser for a property path string.
|
||||
* <p>Each stage of the enforced grammar is modeled by a separate {@link State}.
|
||||
*/
|
||||
private static final class Parser {
|
||||
|
||||
private final String path;
|
||||
|
||||
private final List<Segment> segments = new ArrayList<>(2);
|
||||
|
||||
private final StringBuilder canonicalName;
|
||||
|
||||
// Offset of the character currently being processed.
|
||||
private int pos;
|
||||
|
||||
// Offset at which the current segment's name starts.
|
||||
private int segmentStart;
|
||||
|
||||
// Offset at which the current segment's name ends, or -1 if not yet known.
|
||||
private int segmentNameEnd = -1;
|
||||
|
||||
// Keys collected for the current segment
|
||||
private @Nullable List<String> keys;
|
||||
|
||||
// Offset at which the current key starts.
|
||||
private int keyStart;
|
||||
|
||||
// Bracket nesting depth within the current raw key.
|
||||
private int depth;
|
||||
|
||||
// The quote character that opened the current quoted key.
|
||||
private char quoteChar;
|
||||
|
||||
Parser(String path) {
|
||||
this.path = path;
|
||||
this.canonicalName = new StringBuilder(path.length());
|
||||
}
|
||||
|
||||
PropertyPath parse() {
|
||||
State state = State.NAME;
|
||||
for (; this.pos < this.path.length(); this.pos++) {
|
||||
state = state.process(this.path.charAt(this.pos), this);
|
||||
}
|
||||
state.onEof(this);
|
||||
return new PropertyPath(this.canonicalName.toString(), Collections.unmodifiableList(this.segments));
|
||||
}
|
||||
|
||||
private void addKey(String key) {
|
||||
List<String> keys = this.keys;
|
||||
if (keys == null) {
|
||||
keys = new ArrayList<>(2);
|
||||
this.keys = keys;
|
||||
}
|
||||
keys.add(key);
|
||||
}
|
||||
|
||||
private void endSegment() {
|
||||
String name = this.path.substring(this.segmentStart, this.segmentNameEnd);
|
||||
List<String> keys = (this.keys != null ? this.keys : Collections.emptyList());
|
||||
if (name.isEmpty() && keys.isEmpty()) {
|
||||
throw new InvalidPropertyPathException(this.path,
|
||||
"empty path segment (at position " + this.segmentStart + ")");
|
||||
}
|
||||
if (!this.segments.isEmpty()) {
|
||||
this.canonicalName.append(PropertyAccessor.NESTED_PROPERTY_SEPARATOR_CHAR);
|
||||
}
|
||||
Segment segment = new Segment(name, keys);
|
||||
this.segments.add(segment);
|
||||
this.canonicalName.append(segment.toCanonicalName());
|
||||
this.keys = null;
|
||||
this.segmentNameEnd = -1;
|
||||
}
|
||||
|
||||
private InvalidPropertyPathException error(String reason) {
|
||||
return new InvalidPropertyPathException(this.path, reason + " (at position " + this.pos + ")");
|
||||
}
|
||||
|
||||
private InvalidPropertyPathException unclosedIndex() {
|
||||
return error("unclosed '" + PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR + "'");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private enum State {
|
||||
|
||||
// Reading a segment name, before any index of that segment
|
||||
NAME {
|
||||
@Override
|
||||
State process(char ch, Parser parser) {
|
||||
if (ch == PropertyAccessor.NESTED_PROPERTY_SEPARATOR_CHAR) {
|
||||
parser.segmentNameEnd = parser.pos;
|
||||
parser.endSegment();
|
||||
parser.segmentStart = parser.pos + 1;
|
||||
return this;
|
||||
}
|
||||
if (ch == PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR) {
|
||||
parser.segmentNameEnd = parser.pos;
|
||||
return INDEX_OPEN;
|
||||
}
|
||||
if (ch == PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR) {
|
||||
throw parser.error("unexpected '" + PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR +
|
||||
"' without a matching '" + PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR + "'");
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
void onEof(Parser parser) {
|
||||
parser.segmentNameEnd = parser.path.length();
|
||||
parser.endSegment();
|
||||
}
|
||||
},
|
||||
|
||||
// Immediately after the "[" that opens an index
|
||||
INDEX_OPEN {
|
||||
@Override
|
||||
State process(char ch, Parser parser) {
|
||||
if (ch == PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR) {
|
||||
// An empty key, as in "map[]".
|
||||
parser.addKey("");
|
||||
return AFTER_INDEX;
|
||||
}
|
||||
if (ch == '\'' || ch == '"') {
|
||||
parser.quoteChar = ch;
|
||||
parser.keyStart = parser.pos + 1;
|
||||
return QUOTED_KEY;
|
||||
}
|
||||
parser.keyStart = parser.pos;
|
||||
// A key that itself opens a bracket level, as in "map[[a]]"
|
||||
parser.depth = (ch == PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR ? 1 : 0);
|
||||
return RAW_KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
void onEof(Parser parser) {
|
||||
throw parser.unclosedIndex();
|
||||
}
|
||||
},
|
||||
|
||||
// Inside an unquoted key, tracking the depth of bracket nesting.
|
||||
RAW_KEY {
|
||||
@Override
|
||||
State process(char ch, Parser parser) {
|
||||
if (ch == PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR) {
|
||||
parser.depth++;
|
||||
return this;
|
||||
}
|
||||
if (ch == PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR) {
|
||||
if (parser.depth == 0) {
|
||||
parser.addKey(parser.path.substring(parser.keyStart, parser.pos));
|
||||
return AFTER_INDEX;
|
||||
}
|
||||
parser.depth--;
|
||||
return this;
|
||||
}
|
||||
if (ch == '\'' || ch == '"') {
|
||||
throw parser.error("unexpected quote '" + ch + "' in an unquoted key; " +
|
||||
"quote the whole key to use quote characters within it");
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
void onEof(Parser parser) {
|
||||
throw parser.unclosedIndex();
|
||||
}
|
||||
},
|
||||
|
||||
// Inside a quoted key, looking for the closing quote.
|
||||
QUOTED_KEY {
|
||||
@Override
|
||||
State process(char ch, Parser parser) {
|
||||
if (ch == parser.quoteChar) {
|
||||
parser.addKey(parser.path.substring(parser.keyStart, parser.pos));
|
||||
return QUOTE_CLOSED;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
void onEof(Parser parser) {
|
||||
throw parser.error("unterminated quote '" + parser.quoteChar + "'");
|
||||
}
|
||||
},
|
||||
|
||||
// Immediately after a closing quote, only "]" is legal.
|
||||
QUOTE_CLOSED {
|
||||
@Override
|
||||
State process(char ch, Parser parser) {
|
||||
if (ch != PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR) {
|
||||
throw parser.error("unexpected '" + ch + "' after a closing quote; expected '" +
|
||||
PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR + "'");
|
||||
}
|
||||
return AFTER_INDEX;
|
||||
}
|
||||
|
||||
@Override
|
||||
void onEof(Parser parser) {
|
||||
throw parser.unclosedIndex();
|
||||
}
|
||||
},
|
||||
|
||||
// Immediately after an index's "]" , only "." or "[" are legal.
|
||||
AFTER_INDEX {
|
||||
@Override
|
||||
State process(char ch, Parser parser) {
|
||||
if (ch == PropertyAccessor.NESTED_PROPERTY_SEPARATOR_CHAR) {
|
||||
parser.endSegment();
|
||||
parser.segmentStart = parser.pos + 1;
|
||||
return NAME;
|
||||
}
|
||||
if (ch == PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR) {
|
||||
return INDEX_OPEN;
|
||||
}
|
||||
throw parser.error("unexpected '" + ch + "' after an index; expected '" +
|
||||
PropertyAccessor.NESTED_PROPERTY_SEPARATOR_CHAR + "', '" +
|
||||
PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR + "', or the end of the path");
|
||||
}
|
||||
|
||||
@Override
|
||||
void onEof(Parser parser) {
|
||||
parser.endSegment();
|
||||
}
|
||||
};
|
||||
|
||||
abstract State process(char ch, Parser parser);
|
||||
|
||||
abstract void onEof(Parser parser);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -100,6 +100,7 @@ import org.springframework.core.ResolvableType;
|
||||
* @author Rod Johnson
|
||||
* @author Juergen Hoeller
|
||||
* @author Chris Beams
|
||||
* @author Yanming Zhou
|
||||
* @since 13 April 2001
|
||||
* @see BeanNameAware#setBeanName
|
||||
* @see BeanClassLoaderAware#setBeanClassLoader
|
||||
@@ -175,6 +176,29 @@ public interface BeanFactory {
|
||||
*/
|
||||
<T> T getBean(String name, Class<T> requiredType) throws BeansException;
|
||||
|
||||
/**
|
||||
* Return an instance, which may be shared or independent, of the specified bean.
|
||||
* <p>Behaves the same as {@link #getBean(String)}, but provides a measure of type
|
||||
* safety by throwing a BeanNotOfRequiredTypeException if the bean is not of the
|
||||
* required type. This means that ClassCastException can't be thrown on casting
|
||||
* the result correctly, as can happen with {@link #getBean(String)}.
|
||||
* <p>Translates aliases back to the corresponding canonical bean name.
|
||||
* <p>Will ask the parent factory if the bean cannot be found in this factory instance.
|
||||
* @param name the name of the bean to retrieve
|
||||
* @param typeReference the reference to obtain type the bean must match
|
||||
* @return an instance of the bean.
|
||||
* Note that the return value will never be {@code null}. In case of a stub for
|
||||
* {@code null} from a factory method having been resolved for the requested bean, a
|
||||
* {@code BeanNotOfRequiredTypeException} against the NullBean stub will be raised.
|
||||
* Consider using {@link #getBeanProvider(Class)} for resolving optional dependencies.
|
||||
* @throws NoSuchBeanDefinitionException if there is no such bean definition
|
||||
* @throws BeanNotOfRequiredTypeException if the bean is not of the required type
|
||||
* @throws BeansException if the bean could not be created
|
||||
* @since 7.1
|
||||
* @see #getBean(String, Class)
|
||||
*/
|
||||
<T> T getBean(String name, ParameterizedTypeReference<T> typeReference) throws BeansException;
|
||||
|
||||
/**
|
||||
* Return an instance, which may be shared or independent, of the specified bean.
|
||||
* <p>Allows for specifying explicit constructor arguments / factory method arguments,
|
||||
|
||||
+29
-6
@@ -16,14 +16,17 @@
|
||||
|
||||
package org.springframework.beans.factory;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.core.ResolvableType;
|
||||
|
||||
/**
|
||||
* Thrown when a bean doesn't match the expected type.
|
||||
*
|
||||
* @author Rod Johnson
|
||||
* @author Juergen Hoeller
|
||||
* @author Yanming Zhou
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class BeanNotOfRequiredTypeException extends BeansException {
|
||||
@@ -32,7 +35,7 @@ public class BeanNotOfRequiredTypeException extends BeansException {
|
||||
private final String beanName;
|
||||
|
||||
/** The required type. */
|
||||
private final Class<?> requiredType;
|
||||
private final Type genericRequiredType;
|
||||
|
||||
/** The offending type. */
|
||||
private final Class<?> actualType;
|
||||
@@ -46,10 +49,22 @@ public class BeanNotOfRequiredTypeException extends BeansException {
|
||||
* the expected type
|
||||
*/
|
||||
public BeanNotOfRequiredTypeException(String beanName, Class<?> requiredType, Class<?> actualType) {
|
||||
super("Bean named '" + beanName + "' is expected to be of type '" + ClassUtils.getQualifiedName(requiredType) +
|
||||
"' but was actually of type '" + ClassUtils.getQualifiedName(actualType) + "'");
|
||||
this(beanName, (Type) requiredType, actualType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new BeanNotOfRequiredTypeException.
|
||||
* @param beanName the name of the bean requested
|
||||
* @param requiredType the required type
|
||||
* @param actualType the actual type returned, which did not match
|
||||
* the expected type
|
||||
* @since 7.1
|
||||
*/
|
||||
public BeanNotOfRequiredTypeException(String beanName, Type requiredType, Class<?> actualType) {
|
||||
super("Bean named '" + beanName + "' is expected to be of type '" + requiredType.getTypeName() +
|
||||
"' but was actually of type '" + actualType.getTypeName() + "'");
|
||||
this.beanName = beanName;
|
||||
this.requiredType = requiredType;
|
||||
this.genericRequiredType = requiredType;
|
||||
this.actualType = actualType;
|
||||
}
|
||||
|
||||
@@ -65,7 +80,15 @@ public class BeanNotOfRequiredTypeException extends BeansException {
|
||||
* Return the expected type for the bean.
|
||||
*/
|
||||
public Class<?> getRequiredType() {
|
||||
return this.requiredType;
|
||||
return (this.genericRequiredType instanceof Class<?> clazz ? clazz : ResolvableType.forType(this.genericRequiredType).toClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the expected generic type for the bean.
|
||||
* @since 7.1
|
||||
*/
|
||||
public Type getGenericRequiredType() {
|
||||
return this.genericRequiredType;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,21 +19,9 @@ package org.springframework.beans.factory;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
/**
|
||||
* Contract for registering beans programmatically, typically imported with an
|
||||
* {@link org.springframework.context.annotation.Import @Import} annotation on
|
||||
* a {@link org.springframework.context.annotation.Configuration @Configuration}
|
||||
* class.
|
||||
* <pre class="code">
|
||||
* @Configuration
|
||||
* @Import(MyBeanRegistrar.class)
|
||||
* class MyConfiguration {
|
||||
* }</pre>
|
||||
* Can also be applied to an application context via
|
||||
* {@link org.springframework.context.support.GenericApplicationContext#register(BeanRegistrar...)}.
|
||||
* Contract for registering beans programmatically. Implementations use the
|
||||
* {@link BeanRegistry} and {@link Environment} to register beans:
|
||||
*
|
||||
*
|
||||
* <p>Bean registrar implementations use {@link BeanRegistry} and {@link Environment}
|
||||
* APIs to register beans programmatically in a concise and flexible way.
|
||||
* <pre class="code">
|
||||
* class MyBeanRegistrar implements BeanRegistrar {
|
||||
*
|
||||
@@ -52,9 +40,55 @@ import org.springframework.core.env.Environment;
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* <p>{@code BeanRegistrar} implementations are not Spring components: they must have
|
||||
* a no-arg constructor and cannot rely on dependency injection or any other
|
||||
* component-model feature. They can be used in two distinct ways depending on the
|
||||
* application context setup.
|
||||
*
|
||||
* <h3>With the {@code @Configuration} model</h3>
|
||||
*
|
||||
* <p>A {@code BeanRegistrar} must be imported via
|
||||
* {@link org.springframework.context.annotation.Import @Import} on a
|
||||
* {@link org.springframework.context.annotation.Configuration @Configuration} class:
|
||||
*
|
||||
* <pre class="code">
|
||||
* @Configuration
|
||||
* @Import(MyBeanRegistrar.class)
|
||||
* class MyConfiguration {
|
||||
* }</pre>
|
||||
*
|
||||
* <p>This is the only mechanism that triggers bean registration in the annotation-based
|
||||
* configuration model. Annotating an implementation with {@code @Configuration} or
|
||||
* {@code @Component}, or returning an instance from a {@code @Bean} method, registers
|
||||
* it as a bean but does <strong>not</strong> invoke its
|
||||
* {@link #register(BeanRegistry, Environment) register} method.
|
||||
*
|
||||
* <p>When imported, the registrar is invoked in the order it is encountered during
|
||||
* configuration class processing. It can therefore check for and build on beans that
|
||||
* have already been defined, but has no visibility into beans that will be registered
|
||||
* by classes processed later.
|
||||
*
|
||||
* <h3>Programmatic usage</h3>
|
||||
*
|
||||
* <p>A {@code BeanRegistrar} can also be applied directly to a
|
||||
* {@link org.springframework.context.support.GenericApplicationContext}:
|
||||
*
|
||||
* <pre class="code">
|
||||
* GenericApplicationContext context = new GenericApplicationContext();
|
||||
* context.register(new MyBeanRegistrar());
|
||||
* context.registerBean("myBean", MyBean.class);
|
||||
* context.refresh();</pre>
|
||||
*
|
||||
* <p>This mode is primarily intended for fully programmatic application context setups.
|
||||
* Registrars applied this way are invoked before any {@code @Configuration} class is
|
||||
* processed. They can therefore observe beans registered programmatically (e.g., via
|
||||
* one of the {@code GenericApplicationContext#registerBean} methods), but will
|
||||
* <strong>not</strong> see any beans defined in {@code @Configuration} classes also
|
||||
* registered with the context.
|
||||
*
|
||||
* <p>A {@code BeanRegistrar} implementing {@link org.springframework.context.annotation.ImportAware}
|
||||
* can optionally introspect import metadata when used in an import scenario, otherwise the
|
||||
* {@code setImportMetadata} method is simply not being called.
|
||||
* can optionally introspect import metadata when used in an import scenario; otherwise
|
||||
* the {@code setImportMetadata} method is not called.
|
||||
*
|
||||
* <p>In Kotlin, it is recommended to use {@code BeanRegistrarDsl} instead of
|
||||
* implementing {@code BeanRegistrar}.
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.springframework.core.env.Environment;
|
||||
* programmatic bean registration capabilities.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @author Juergen Hoeller
|
||||
* @since 7.0
|
||||
*/
|
||||
public interface BeanRegistry {
|
||||
@@ -140,6 +141,28 @@ public interface BeanRegistry {
|
||||
*/
|
||||
<T> void registerBean(String name, ParameterizedTypeReference<T> beanType, Consumer<Spec<T>> customizer);
|
||||
|
||||
/**
|
||||
* Determine whether a bean of the given name is already registered.
|
||||
* @param name the name of the bean
|
||||
* @since 7.1
|
||||
*/
|
||||
boolean containsBean(String name);
|
||||
|
||||
/**
|
||||
* Determine whether a bean of the given type is already registered.
|
||||
* @param beanType the type of the bean
|
||||
* @since 7.1
|
||||
*/
|
||||
boolean containsBean(Class<?> beanType);
|
||||
|
||||
/**
|
||||
* Determine whether a bean of the given generics-containing type is
|
||||
* already registered.
|
||||
* @param beanType the generics-containing type of the bean
|
||||
* @since 7.1
|
||||
*/
|
||||
<T> boolean containsBean(ParameterizedTypeReference<T> beanType);
|
||||
|
||||
|
||||
/**
|
||||
* Specification for customizing a bean.
|
||||
|
||||
+63
-13
@@ -92,16 +92,8 @@ public final class ParameterResolutionDelegate {
|
||||
/**
|
||||
* Resolve the dependency for the supplied {@link Parameter} from the
|
||||
* supplied {@link AutowireCapableBeanFactory}.
|
||||
* <p>Provides comprehensive autowiring support for individual method parameters
|
||||
* on par with Spring's dependency injection facilities for autowired fields and
|
||||
* methods, including support for {@link Autowired @Autowired},
|
||||
* {@link Qualifier @Qualifier}, and {@link Value @Value} with support for property
|
||||
* placeholders and SpEL expressions in {@code @Value} declarations.
|
||||
* <p>The dependency is required unless the parameter is annotated or meta-annotated
|
||||
* with {@link Autowired @Autowired} with the {@link Autowired#required required}
|
||||
* flag set to {@code false}.
|
||||
* <p>If an explicit <em>qualifier</em> is not declared, the name of the parameter
|
||||
* will be used as the qualifier for resolving ambiguities.
|
||||
* <p>See {@link #resolveDependency(Parameter, int, String, Class, AutowireCapableBeanFactory)}
|
||||
* for details.
|
||||
* @param parameter the parameter whose dependency should be resolved (must not be
|
||||
* {@code null})
|
||||
* @param parameterIndex the index of the parameter in the constructor or method
|
||||
@@ -113,13 +105,49 @@ public final class ParameterResolutionDelegate {
|
||||
* the dependency (must not be {@code null})
|
||||
* @return the resolved object, or {@code null} if none found
|
||||
* @throws BeansException if dependency resolution failed
|
||||
* @see #resolveDependency(Parameter, int, String, Class, AutowireCapableBeanFactory)
|
||||
*/
|
||||
public static @Nullable Object resolveDependency(
|
||||
Parameter parameter, int parameterIndex, Class<?> containingClass, AutowireCapableBeanFactory beanFactory)
|
||||
throws BeansException {
|
||||
|
||||
return resolveDependency(parameter, parameterIndex, null, containingClass, beanFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the dependency for the supplied {@link Parameter} from the
|
||||
* supplied {@link AutowireCapableBeanFactory}.
|
||||
* <p>Provides comprehensive autowiring support for individual method parameters
|
||||
* on par with Spring's dependency injection facilities for autowired fields and
|
||||
* methods, including support for {@link Autowired @Autowired},
|
||||
* {@link Qualifier @Qualifier}, and {@link Value @Value} with support for property
|
||||
* placeholders and SpEL expressions in {@code @Value} declarations.
|
||||
* <p>The dependency is required unless the parameter is annotated or meta-annotated
|
||||
* with {@link Autowired @Autowired} with the {@link Autowired#required required}
|
||||
* flag set to {@code false}.
|
||||
* <p>If an explicit <em>qualifier</em> is not declared, the name of the parameter
|
||||
* (or a supplied custom name) will be used as the qualifier for resolving ambiguities.
|
||||
* @param parameter the parameter whose dependency should be resolved (must not be
|
||||
* {@code null})
|
||||
* @param parameterIndex the index of the parameter in the constructor or method
|
||||
* that declares the parameter
|
||||
* @param parameterName a custom name for the parameter; or {@code null} to use
|
||||
* the default parameter name discovery logic
|
||||
* @param containingClass the concrete class that contains the parameter; this may
|
||||
* differ from the class that declares the parameter in that it may be a subclass
|
||||
* thereof, potentially substituting type variables (must not be {@code null})
|
||||
* @param beanFactory the {@code AutowireCapableBeanFactory} from which to resolve
|
||||
* the dependency (must not be {@code null})
|
||||
* @return the resolved object, or {@code null} if none found
|
||||
* @throws BeansException if dependency resolution failed
|
||||
* @since 7.1
|
||||
* @see #isAutowirable
|
||||
* @see Autowired#required
|
||||
* @see SynthesizingMethodParameter#forExecutable(Executable, int)
|
||||
* @see AutowireCapableBeanFactory#resolveDependency(DependencyDescriptor, String)
|
||||
*/
|
||||
public static @Nullable Object resolveDependency(
|
||||
Parameter parameter, int parameterIndex, Class<?> containingClass, AutowireCapableBeanFactory beanFactory)
|
||||
public static @Nullable Object resolveDependency(Parameter parameter, int parameterIndex,
|
||||
@Nullable String parameterName, Class<?> containingClass, AutowireCapableBeanFactory beanFactory)
|
||||
throws BeansException {
|
||||
|
||||
Assert.notNull(parameter, "Parameter must not be null");
|
||||
@@ -132,7 +160,7 @@ public final class ParameterResolutionDelegate {
|
||||
|
||||
MethodParameter methodParameter = SynthesizingMethodParameter.forExecutable(
|
||||
parameter.getDeclaringExecutable(), parameterIndex);
|
||||
DependencyDescriptor descriptor = new DependencyDescriptor(methodParameter, required);
|
||||
DependencyDescriptor descriptor = new NamedParameterDependencyDescriptor(methodParameter, required, parameterName);
|
||||
descriptor.setContainingClass(containingClass);
|
||||
return beanFactory.resolveDependency(descriptor, null);
|
||||
}
|
||||
@@ -171,4 +199,26 @@ public final class ParameterResolutionDelegate {
|
||||
return parameter;
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private static class NamedParameterDependencyDescriptor extends DependencyDescriptor {
|
||||
|
||||
private final @Nullable String parameterName;
|
||||
|
||||
NamedParameterDependencyDescriptor(MethodParameter methodParameter, boolean required, @Nullable String parameterName) {
|
||||
super(methodParameter, required);
|
||||
this.parameterName = parameterName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getDependencyName() {
|
||||
return (this.parameterName != null ? this.parameterName : super.getDependencyName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean usesStandardBeanLookup() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ public interface AutowiredArguments {
|
||||
Object value = getObject(index);
|
||||
if (!ClassUtils.isAssignableValue(requiredType, value)) {
|
||||
throw new IllegalArgumentException("Argument type mismatch: expected '" +
|
||||
ClassUtils.getQualifiedName(requiredType) + "' for value [" + value + "]");
|
||||
requiredType.getTypeName() + "' for value [" + value + "]");
|
||||
}
|
||||
return (T) value;
|
||||
}
|
||||
|
||||
+14
-13
@@ -72,10 +72,7 @@ public abstract class AbstractFactoryBean<T>
|
||||
|
||||
private @Nullable BeanFactory beanFactory;
|
||||
|
||||
private boolean initialized = false;
|
||||
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
private T singletonInstance;
|
||||
private @Nullable T singletonInstance;
|
||||
|
||||
private @Nullable T earlySingletonInstance;
|
||||
|
||||
@@ -134,7 +131,6 @@ public abstract class AbstractFactoryBean<T>
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
if (isSingleton()) {
|
||||
this.initialized = true;
|
||||
this.singletonInstance = createInstance();
|
||||
this.earlySingletonInstance = null;
|
||||
}
|
||||
@@ -149,7 +145,8 @@ public abstract class AbstractFactoryBean<T>
|
||||
@Override
|
||||
public final T getObject() throws Exception {
|
||||
if (isSingleton()) {
|
||||
return (this.initialized ? this.singletonInstance : getEarlySingletonInstance());
|
||||
T instance = this.singletonInstance;
|
||||
return (instance != null ? instance : getEarlySingletonInstance());
|
||||
}
|
||||
else {
|
||||
return createInstance();
|
||||
@@ -161,7 +158,7 @@ public abstract class AbstractFactoryBean<T>
|
||||
* circular reference. Not called in a non-circular scenario.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private T getEarlySingletonInstance() throws Exception {
|
||||
private T getEarlySingletonInstance() {
|
||||
Class<?>[] ifcs = getEarlySingletonInterfaces();
|
||||
if (ifcs == null) {
|
||||
throw new FactoryBeanNotInitializedException(
|
||||
@@ -179,9 +176,10 @@ public abstract class AbstractFactoryBean<T>
|
||||
* @return the singleton instance that this FactoryBean holds
|
||||
* @throws IllegalStateException if the singleton instance is not initialized
|
||||
*/
|
||||
private @Nullable T getSingletonInstance() throws IllegalStateException {
|
||||
Assert.state(this.initialized, "Singleton instance not initialized yet");
|
||||
return this.singletonInstance;
|
||||
private T getSingletonInstance() throws IllegalStateException {
|
||||
T instance = this.singletonInstance;
|
||||
Assert.state(instance != null, "Singleton instance not initialized yet");
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -191,7 +189,10 @@ public abstract class AbstractFactoryBean<T>
|
||||
@Override
|
||||
public void destroy() throws Exception {
|
||||
if (isSingleton()) {
|
||||
destroyInstance(this.singletonInstance);
|
||||
T instance = this.singletonInstance;
|
||||
if (instance != null) {
|
||||
destroyInstance(instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,7 +242,7 @@ public abstract class AbstractFactoryBean<T>
|
||||
* @throws Exception in case of shutdown errors
|
||||
* @see #createInstance()
|
||||
*/
|
||||
protected void destroyInstance(@Nullable T instance) throws Exception {
|
||||
protected void destroyInstance(T instance) throws Exception {
|
||||
}
|
||||
|
||||
|
||||
@@ -260,7 +261,7 @@ public abstract class AbstractFactoryBean<T>
|
||||
// Use hashCode of reference proxy.
|
||||
return System.identityHashCode(proxy);
|
||||
}
|
||||
else if (!initialized && ReflectionUtils.isToStringMethod(method)) {
|
||||
else if (ReflectionUtils.isToStringMethod(method) && singletonInstance == null) {
|
||||
return "Early singleton proxy for interfaces " +
|
||||
ObjectUtils.nullSafeToString(getEarlySingletonInterfaces());
|
||||
}
|
||||
|
||||
+12
-11
@@ -89,15 +89,13 @@ public class PropertyPathFactoryBean implements FactoryBean<Object>, BeanNameAwa
|
||||
|
||||
private @Nullable BeanWrapper targetBeanWrapper;
|
||||
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
private String targetBeanName;
|
||||
private @Nullable String targetBeanName;
|
||||
|
||||
private @Nullable String propertyPath;
|
||||
|
||||
private @Nullable Class<?> resultType;
|
||||
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
private String beanName;
|
||||
private @Nullable String beanName;
|
||||
|
||||
private @Nullable BeanFactory beanFactory;
|
||||
|
||||
@@ -160,25 +158,27 @@ public class PropertyPathFactoryBean implements FactoryBean<Object>, BeanNameAwa
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) {
|
||||
this.beanFactory = beanFactory;
|
||||
String targetBeanName = this.targetBeanName;
|
||||
|
||||
if (this.targetBeanWrapper != null && this.targetBeanName != null) {
|
||||
if (this.targetBeanWrapper != null && targetBeanName != null) {
|
||||
throw new IllegalArgumentException("Specify either 'targetObject' or 'targetBeanName', not both");
|
||||
}
|
||||
|
||||
if (this.targetBeanWrapper == null && this.targetBeanName == null) {
|
||||
if (this.targetBeanWrapper == null && targetBeanName == null) {
|
||||
if (this.propertyPath != null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Specify 'targetObject' or 'targetBeanName' in combination with 'propertyPath'");
|
||||
}
|
||||
|
||||
// No other properties specified: check bean name.
|
||||
int dotIndex = (this.beanName != null ? this.beanName.indexOf('.') : -1);
|
||||
if (dotIndex == -1) {
|
||||
int dotIndex;
|
||||
if (this.beanName == null || (dotIndex = this.beanName.indexOf('.')) <= 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Neither 'targetObject' nor 'targetBeanName' specified, and PropertyPathFactoryBean " +
|
||||
"bean name '" + this.beanName + "' does not follow 'beanName.property' syntax");
|
||||
}
|
||||
this.targetBeanName = this.beanName.substring(0, dotIndex);
|
||||
targetBeanName = this.beanName.substring(0, dotIndex);
|
||||
this.targetBeanName = targetBeanName;
|
||||
this.propertyPath = this.beanName.substring(dotIndex + 1);
|
||||
}
|
||||
|
||||
@@ -187,9 +187,10 @@ public class PropertyPathFactoryBean implements FactoryBean<Object>, BeanNameAwa
|
||||
throw new IllegalArgumentException("'propertyPath' is required");
|
||||
}
|
||||
|
||||
if (this.targetBeanWrapper == null && this.beanFactory.isSingleton(this.targetBeanName)) {
|
||||
if (this.targetBeanWrapper == null && StringUtils.hasLength(targetBeanName) &&
|
||||
this.beanFactory.isSingleton(targetBeanName)) {
|
||||
// Eagerly fetch singleton target bean, and determine result type.
|
||||
Object bean = this.beanFactory.getBean(this.targetBeanName);
|
||||
Object bean = this.beanFactory.getBean(targetBeanName);
|
||||
this.targetBeanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(bean);
|
||||
this.resultType = this.targetBeanWrapper.getPropertyType(this.propertyPath);
|
||||
}
|
||||
|
||||
+8
-6
@@ -26,6 +26,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
@@ -194,30 +195,31 @@ public abstract class YamlProcessor {
|
||||
}
|
||||
|
||||
private boolean process(MatchCallback callback, Yaml yaml, Resource resource) {
|
||||
int count = 0;
|
||||
AtomicInteger count = new AtomicInteger();
|
||||
try {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Loading from YAML: " + resource);
|
||||
}
|
||||
try (Reader reader = new UnicodeReader(resource.getInputStream())) {
|
||||
resource.consumeContent(inputStream -> {
|
||||
Reader reader = new UnicodeReader(inputStream);
|
||||
for (Object object : yaml.loadAll(reader)) {
|
||||
if (object != null && process(asMap(object), callback)) {
|
||||
count++;
|
||||
count.incrementAndGet();
|
||||
if (this.resolutionMethod == ResolutionMethod.FIRST_FOUND) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Loaded " + count + " document" + (count > 1 ? "s" : "") +
|
||||
logger.debug("Loaded " + count + " document" + (count.get() > 1 ? "s" : "") +
|
||||
" from YAML resource: " + resource);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (IOException ex) {
|
||||
handleProcessError(resource, ex);
|
||||
}
|
||||
return (count > 0);
|
||||
return (count.get() > 0);
|
||||
}
|
||||
|
||||
private void handleProcessError(Resource resource, IOException ex) {
|
||||
|
||||
+15
-2
@@ -42,7 +42,7 @@ import org.springframework.beans.BeanWrapperImpl;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.InvalidPropertyException;
|
||||
import org.springframework.beans.MutablePropertyValues;
|
||||
import org.springframework.beans.PropertyAccessorUtils;
|
||||
import org.springframework.beans.PropertyAccessor;
|
||||
import org.springframework.beans.PropertyValue;
|
||||
import org.springframework.beans.PropertyValues;
|
||||
import org.springframework.beans.TypeConverter;
|
||||
@@ -1751,7 +1751,7 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac
|
||||
*/
|
||||
private boolean isConvertibleProperty(String propertyName, BeanWrapper bw) {
|
||||
try {
|
||||
return !PropertyAccessorUtils.isNestedOrIndexedProperty(propertyName) &&
|
||||
return !isNestedOrIndexedProperty(propertyName) &&
|
||||
BeanUtils.hasUniqueWriteMethod(bw.getPropertyDescriptor(propertyName));
|
||||
}
|
||||
catch (InvalidPropertyException ex) {
|
||||
@@ -1759,6 +1759,19 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given property path indicates an indexed or nested property.
|
||||
*/
|
||||
private static boolean isNestedOrIndexedProperty(String propertyName) {
|
||||
for (int i = 0; i < propertyName.length(); i++) {
|
||||
char ch = propertyName.charAt(i);
|
||||
if (ch == PropertyAccessor.NESTED_PROPERTY_SEPARATOR_CHAR || ch == PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given value for the specified target property.
|
||||
*/
|
||||
|
||||
+14
-1
@@ -17,6 +17,7 @@
|
||||
package org.springframework.beans.factory.support;
|
||||
|
||||
import java.beans.PropertyEditor;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
@@ -66,6 +67,7 @@ import org.springframework.beans.factory.config.Scope;
|
||||
import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor;
|
||||
import org.springframework.core.DecoratingClassLoader;
|
||||
import org.springframework.core.NamedThreadLocal;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.log.LogMessage;
|
||||
@@ -201,6 +203,17 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
|
||||
return doGetBean(name, requiredType, null, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T getBean(String name, ParameterizedTypeReference<T> typeReference) throws BeansException {
|
||||
Object bean = getBean(name);
|
||||
Type requiredType = typeReference.getType();
|
||||
if (!isTypeMatch(name, ResolvableType.forType(requiredType), true)) {
|
||||
throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
|
||||
}
|
||||
return (T) bean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBean(String name, @Nullable Object @Nullable ... args) throws BeansException {
|
||||
return doGetBean(name, null, args, false);
|
||||
@@ -413,7 +426,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
|
||||
catch (TypeMismatchException ex) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Failed to convert bean '" + name + "' to required type '" +
|
||||
ClassUtils.getQualifiedName(requiredType) + "'", ex);
|
||||
requiredType.getTypeName() + "'", ex);
|
||||
}
|
||||
throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
|
||||
}
|
||||
|
||||
+17
@@ -26,6 +26,7 @@ import org.jspecify.annotations.Nullable;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.beans.factory.BeanRegistrar;
|
||||
import org.springframework.beans.factory.BeanRegistry;
|
||||
import org.springframework.beans.factory.ListableBeanFactory;
|
||||
@@ -176,6 +177,22 @@ public class BeanRegistryAdapter implements BeanRegistry {
|
||||
this.beanRegistry.registerBeanDefinition(name, beanDefinition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsBean(String name) {
|
||||
return this.beanFactory.containsBean(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsBean(Class<?> beanType) {
|
||||
return BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, beanType).length > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> boolean containsBean(ParameterizedTypeReference<T> beanType) {
|
||||
ResolvableType resolvableType = ResolvableType.forType(beanType);
|
||||
return BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, resolvableType).length > 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@link RootBeanDefinition} subclass for {@code #registerBean} based
|
||||
|
||||
+2
-3
@@ -17,7 +17,6 @@
|
||||
package org.springframework.beans.factory.support;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
@@ -256,14 +255,14 @@ public class PropertiesBeanDefinitionReader extends AbstractBeanDefinitionReader
|
||||
|
||||
Properties props = new Properties();
|
||||
try {
|
||||
try (InputStream is = encodedResource.getResource().getInputStream()) {
|
||||
encodedResource.getResource().consumeContent(is -> {
|
||||
if (encodedResource.getEncoding() != null) {
|
||||
getPropertiesPersister().load(props, new InputStreamReader(is, encodedResource.getEncoding()));
|
||||
}
|
||||
else {
|
||||
getPropertiesPersister().load(props, is);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
int count = registerBeanDefinitions(props, prefix, encodedResource.getResource().getDescription());
|
||||
if (logger.isDebugEnabled()) {
|
||||
|
||||
+1
-1
@@ -159,7 +159,7 @@ public class SimpleInstantiationStrategy implements InstantiationStrategy {
|
||||
return result;
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
if (factoryBean != null && !factoryMethod.getDeclaringClass().isAssignableFrom(factoryBean.getClass())) {
|
||||
if (factoryBean != null && !factoryMethod.getDeclaringClass().isInstance(factoryBean)) {
|
||||
throw new BeanInstantiationException(factoryMethod,
|
||||
"Illegal factory instance for factory method '" + factoryMethod.getName() + "'; " +
|
||||
"instance: " + factoryBean.getClass().getName(), ex);
|
||||
|
||||
+13
@@ -17,6 +17,7 @@
|
||||
package org.springframework.beans.factory.support;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@@ -64,6 +65,7 @@ import org.springframework.util.StringUtils;
|
||||
* @author Rod Johnson
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @author Yanming Zhou
|
||||
* @since 06.01.2003
|
||||
* @see DefaultListableBeanFactory
|
||||
*/
|
||||
@@ -149,6 +151,17 @@ public class StaticListableBeanFactory implements ListableBeanFactory {
|
||||
return (T) bean;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T getBean(String name, ParameterizedTypeReference<T> typeReference) throws BeansException {
|
||||
Object bean = getBean(name);
|
||||
Type requiredType = typeReference.getType();
|
||||
if (!ResolvableType.forType(requiredType).isInstance(bean)) {
|
||||
throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
|
||||
}
|
||||
return (T) bean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBean(String name, @Nullable Object @Nullable ... args) throws BeansException {
|
||||
if (!ObjectUtils.isEmpty(args)) {
|
||||
|
||||
+11
-6
@@ -21,6 +21,7 @@ import java.io.InputStream;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
|
||||
@@ -337,12 +338,16 @@ public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {
|
||||
"Detected cyclic loading of " + encodedResource + " - check your import definitions!");
|
||||
}
|
||||
|
||||
try (InputStream inputStream = encodedResource.getResource().getInputStream()) {
|
||||
InputSource inputSource = new InputSource(inputStream);
|
||||
if (encodedResource.getEncoding() != null) {
|
||||
inputSource.setEncoding(encodedResource.getEncoding());
|
||||
}
|
||||
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
|
||||
try {
|
||||
AtomicInteger count = new AtomicInteger();
|
||||
encodedResource.getResource().consumeContent(inputStream -> {
|
||||
InputSource inputSource = new InputSource(inputStream);
|
||||
if (encodedResource.getEncoding() != null) {
|
||||
inputSource.setEncoding(encodedResource.getEncoding());
|
||||
}
|
||||
count.addAndGet(doLoadBeanDefinitions(inputSource, encodedResource.getResource()));
|
||||
});
|
||||
return count.get();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new BeanDefinitionStoreException(
|
||||
|
||||
+2
-2
@@ -84,8 +84,8 @@ public class ClassArrayEditor extends PropertyEditorSupport {
|
||||
return "";
|
||||
}
|
||||
StringJoiner sj = new StringJoiner(",");
|
||||
for (Class<?> klass : classes) {
|
||||
sj.add(ClassUtils.getQualifiedName(klass));
|
||||
for (Class<?> clazz : classes) {
|
||||
sj.add(clazz.getTypeName());
|
||||
}
|
||||
return sj.toString();
|
||||
}
|
||||
|
||||
+1
-6
@@ -72,12 +72,7 @@ public class ClassEditor extends PropertyEditorSupport {
|
||||
@Override
|
||||
public String getAsText() {
|
||||
Class<?> clazz = (Class<?>) getValue();
|
||||
if (clazz != null) {
|
||||
return ClassUtils.getQualifiedName(clazz);
|
||||
}
|
||||
else {
|
||||
return "";
|
||||
}
|
||||
return (clazz != null ? clazz.getTypeName() : "");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
-2
@@ -24,6 +24,7 @@ import org.springframework.core.ResolvableType
|
||||
* This extension is not subject to type erasure and retains actual generic type arguments.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @author Yanming Zhou
|
||||
* @since 5.0
|
||||
*/
|
||||
inline fun <reified T : Any> BeanFactory.getBean(): T =
|
||||
@@ -31,14 +32,14 @@ inline fun <reified T : Any> BeanFactory.getBean(): T =
|
||||
|
||||
/**
|
||||
* Extension for [BeanFactory.getBean] providing a `getBean<Foo>("foo")` variant.
|
||||
* Like the original Java method, this extension is subject to type erasure.
|
||||
* This extension is not subject to type erasure and retains actual generic type arguments.
|
||||
*
|
||||
* @see BeanFactory.getBean(String, Class<T>)
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.0
|
||||
*/
|
||||
inline fun <reified T : Any> BeanFactory.getBean(name: String): T =
|
||||
getBean(name, T::class.java)
|
||||
getBean(name, (object : ParameterizedTypeReference<T>() {}))
|
||||
|
||||
/**
|
||||
* Extension for [BeanFactory.getBean] providing a `getBean<Foo>(arg1, arg2)` variant.
|
||||
|
||||
@@ -18,8 +18,8 @@ package org.springframework.beans.factory
|
||||
|
||||
import org.springframework.beans.factory.BeanRegistry.SupplierContext
|
||||
import org.springframework.core.ParameterizedTypeReference
|
||||
import org.springframework.core.ResolvableType
|
||||
import org.springframework.core.env.Environment
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* Contract for registering programmatically beans.
|
||||
@@ -364,6 +364,28 @@ open class BeanRegistrarDsl(private val init: BeanRegistrarDsl.() -> Unit): Bean
|
||||
return registry.registerBean(object: ParameterizedTypeReference<T>() {}, customizer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a bean of the given name is already registered.
|
||||
* @param name the name of the bean
|
||||
* @since 7.1
|
||||
*/
|
||||
fun containsBean(name: String): Boolean = registry.containsBean(name)
|
||||
|
||||
/**
|
||||
* Determine whether a bean of the given type is already registered.
|
||||
* @param beanType the type of the bean
|
||||
* @since 7.1
|
||||
*/
|
||||
fun containsBean(beanType: KClass<*>): Boolean = registry.containsBean(beanType.java)
|
||||
|
||||
/**
|
||||
* Determine whether a bean of the given type is already registered.
|
||||
* @param T the type of the bean
|
||||
* @since 7.1
|
||||
*/
|
||||
inline fun <reified T : Any> containsBean(): Boolean =
|
||||
registry.containsBean(object: ParameterizedTypeReference<T>() {})
|
||||
|
||||
|
||||
/**
|
||||
* Context available from the bean instance supplier designed to give access
|
||||
|
||||
+150
-6
@@ -34,8 +34,12 @@ import java.util.SortedSet;
|
||||
import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowire;
|
||||
import org.springframework.beans.propertyeditors.StringArrayPropertyEditor;
|
||||
@@ -56,6 +60,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.within;
|
||||
import static org.springframework.beans.ConfigurablePropertyAccessor.DEFAULT_MAX_NESTED_PATH_DEPTH;
|
||||
|
||||
/**
|
||||
* Shared tests for property accessors.
|
||||
@@ -67,6 +72,7 @@ import static org.assertj.core.api.Assertions.within;
|
||||
* @author Chris Beams
|
||||
* @author Dave Syer
|
||||
* @author Stephane Nicoll
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
abstract class AbstractPropertyAccessorTests {
|
||||
|
||||
@@ -223,10 +229,7 @@ abstract class AbstractPropertyAccessorTests {
|
||||
|
||||
@Test
|
||||
void getAnotherNestedDeepProperty() {
|
||||
ITestBean target = new TestBean("rod", 31);
|
||||
ITestBean kerry = new TestBean("kerry", 35);
|
||||
target.setSpouse(kerry);
|
||||
kerry.setSpouse(target);
|
||||
ITestBean target = createSpouseCycle();
|
||||
AbstractPropertyAccessor accessor = createAccessor(target);
|
||||
Integer KA = (Integer) accessor.getPropertyValue("spouse.age");
|
||||
assertThat(KA).as("kerry is 35").isEqualTo(35);
|
||||
@@ -291,6 +294,18 @@ abstract class AbstractPropertyAccessorTests {
|
||||
accessor.getPropertyValue("address.bar"));
|
||||
}
|
||||
|
||||
@ParameterizedTest // gh-36999
|
||||
@ValueSource(strings = {"address.[.city", "address.].city", "address.[[.city",
|
||||
"address.]].city", "address.][.city", "address.[X.city", "address.X[.city"})
|
||||
void getNestedPropertyWithUnbalancedBracket(String propertyPath) {
|
||||
Person target = createPerson("John", "London", "UK");
|
||||
AbstractPropertyAccessor accessor = createAccessor(target);
|
||||
|
||||
assertThatExceptionOfType(InvalidPropertyPathException.class)
|
||||
.isThrownBy(() -> accessor.getPropertyValue(propertyPath))
|
||||
.withMessageContaining("Invalid property path '" + propertyPath + "'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void setSimpleProperty() {
|
||||
Simple target = new Simple("John", 2);
|
||||
@@ -1362,6 +1377,19 @@ abstract class AbstractPropertyAccessorTests {
|
||||
accessor.setPropertyValue("address.bar", "value"));
|
||||
}
|
||||
|
||||
@ParameterizedTest // gh-36999
|
||||
@ValueSource(strings = {"address.[.city", "address.].city", "address.[[.city",
|
||||
"address.]].city", "address.][.city", "address.[X.city", "address.X[.city"})
|
||||
void setNestedPropertyWithUnbalancedBracket(String propertyPath) {
|
||||
Person target = createPerson("John", "Paris", "FR");
|
||||
AbstractPropertyAccessor accessor = createAccessor(target);
|
||||
|
||||
assertThatExceptionOfType(InvalidPropertyPathException.class)
|
||||
.isThrownBy(() -> accessor.setPropertyValue(propertyPath, "Zürich"))
|
||||
.withMessageContaining("Invalid property path '" + propertyPath + "'");
|
||||
assertThat(target.getAddress().getCity()).isEqualTo("Paris");
|
||||
}
|
||||
|
||||
@Test
|
||||
void setPropertyValuesIgnoresInvalidNestedOnRequest() {
|
||||
ITestBean target = new TestBean();
|
||||
@@ -1413,8 +1441,6 @@ abstract class AbstractPropertyAccessorTests {
|
||||
assertThat(accessor.getPropertyValue("map[key5[foo]].name")).isEqualTo("name8");
|
||||
assertThat(accessor.getPropertyValue("map['key5[foo]'].name")).isEqualTo("name8");
|
||||
assertThat(accessor.getPropertyValue("map[\"key5[foo]\"].name")).isEqualTo("name8");
|
||||
assertThat(accessor.getPropertyValue("map['].name")).isEqualTo("name9");
|
||||
assertThat(accessor.getPropertyValue("map[\"].name")).isEqualTo("name9");
|
||||
assertThat(accessor.getPropertyValue("iterableMap[key1].name")).isEqualTo("nameC");
|
||||
assertThat(accessor.getPropertyValue("iterableMap[key2][0].name")).isEqualTo("nameA");
|
||||
assertThat(accessor.getPropertyValue("iterableMap[key2][1].name")).isEqualTo("nameB");
|
||||
@@ -1583,10 +1609,128 @@ abstract class AbstractPropertyAccessorTests {
|
||||
}
|
||||
|
||||
|
||||
@Nested // gh-37252
|
||||
class MaxNestedPathDepthTests {
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(ints = {-1, Integer.MIN_VALUE})
|
||||
void setMaxNestedPathDepthPreconditions(int depth) {
|
||||
AbstractPropertyAccessor accessor = createAccessor(createSpouseCycle());
|
||||
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> accessor.setMaxNestedPathDepth(depth))
|
||||
.withMessage("'maxNestedPathDepth' must not be negative");
|
||||
}
|
||||
|
||||
@Test
|
||||
void maxNestedPathDepthOfZeroOnlyDisablesNestedPropertyPaths() {
|
||||
AbstractPropertyAccessor accessor = createAccessor(createSpouseCycle());
|
||||
accessor.setMaxNestedPathDepth(0);
|
||||
|
||||
// Simple property access is still supported.
|
||||
assertThat(accessor.getPropertyValue("name")).isEqualTo("rod");
|
||||
accessor.setPropertyValue("name", "ROD");
|
||||
assertThat(accessor.getPropertyValue("name")).isEqualTo("ROD");
|
||||
|
||||
// As is indexed property access.
|
||||
accessor.setPropertyValue("stringArray", new String[] {"a", "b"});
|
||||
assertThat(accessor.getPropertyValue("stringArray[1]")).isEqualTo("b");
|
||||
accessor.setPropertyValue("stringArray[1]", "B");
|
||||
assertThat(accessor.getPropertyValue("stringArray[1]")).isEqualTo("B");
|
||||
|
||||
// As is mapped property access.
|
||||
accessor.setPropertyValue("someMap[key]", "value");
|
||||
assertThat(accessor.getPropertyValue("someMap[key]")).isEqualTo("value");
|
||||
|
||||
// Nested property paths, however, are not.
|
||||
assertNestedPathDepthExceeded(() -> accessor.getPropertyValue(nestedSpousePath(1)), 0);
|
||||
assertNestedPathDepthExceeded(() -> accessor.getPropertyValue(nestedSpousePath(2)), 0);
|
||||
assertNestedPathDepthExceeded(() -> accessor.setPropertyValue(nestedSpousePath(1), "Joe"), 0);
|
||||
assertNestedPathDepthExceeded(() -> accessor.setPropertyValue(nestedSpousePath(2), "Joe"), 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultMaxNestedPathDepth() {
|
||||
AbstractPropertyAccessor accessor = createAccessor(createSpouseCycle());
|
||||
|
||||
assertThat(accessor.getMaxNestedPathDepth()).isEqualTo(DEFAULT_MAX_NESTED_PATH_DEPTH);
|
||||
|
||||
// depth == max
|
||||
assertThat(accessor.getPropertyValue(nestedSpousePath(DEFAULT_MAX_NESTED_PATH_DEPTH))).isEqualTo("rod");
|
||||
|
||||
// depth > max
|
||||
assertNestedPathDepthExceeded(
|
||||
() -> accessor.getPropertyValue(nestedSpousePath(DEFAULT_MAX_NESTED_PATH_DEPTH + 1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNestedPropertyWithCustomMaxNestedPathDepth() {
|
||||
AbstractPropertyAccessor accessor = createAccessor(createSpouseCycle());
|
||||
accessor.setMaxNestedPathDepth(10);
|
||||
|
||||
// depth < max
|
||||
assertThat(accessor.getPropertyValue(nestedSpousePath(9))).isEqualTo("kerry");
|
||||
|
||||
// depth == max
|
||||
assertThat(accessor.getPropertyValue(nestedSpousePath(10))).isEqualTo("rod");
|
||||
|
||||
// depth > max
|
||||
assertNestedPathDepthExceeded(() -> accessor.getPropertyValue(nestedSpousePath(11)), 10);
|
||||
assertNestedPathDepthExceeded(() -> accessor.getPropertyValue(nestedSpousePath(100)), 10);
|
||||
}
|
||||
|
||||
@Test
|
||||
void setNestedPropertyExceedingMaxNestedPathDepth() {
|
||||
AbstractPropertyAccessor accessor = createAccessor(createSpouseCycle());
|
||||
accessor.setMaxNestedPathDepth(10);
|
||||
|
||||
assertNestedPathDepthExceeded(() -> accessor.setPropertyValue(nestedSpousePath(11), "Jane"), 10);
|
||||
}
|
||||
|
||||
@Test
|
||||
void maxNestedPathDepthProtectsAgainstStackOverflow() {
|
||||
AbstractPropertyAccessor accessor = createAccessor(createSpouseCycle());
|
||||
accessor.setAutoGrowNestedPaths(true);
|
||||
|
||||
assertNestedPathDepthExceeded(() -> accessor.getPropertyValue(nestedSpousePath(100_000)));
|
||||
assertNestedPathDepthExceeded(() -> accessor.setPropertyValue(nestedSpousePath(100_000), "Jane"));
|
||||
}
|
||||
|
||||
|
||||
private static String nestedSpousePath(int depth) {
|
||||
return "spouse.".repeat(depth) + "name";
|
||||
}
|
||||
|
||||
private static void assertNestedPathDepthExceeded(ThrowingCallable throwingCallable) {
|
||||
assertNestedPathDepthExceeded(throwingCallable, DEFAULT_MAX_NESTED_PATH_DEPTH);
|
||||
}
|
||||
|
||||
private static void assertNestedPathDepthExceeded(ThrowingCallable throwingCallable, int maxDepth) {
|
||||
assertThatExceptionOfType(InvalidPropertyPathException.class)
|
||||
.isThrownBy(throwingCallable)
|
||||
.withMessageEndingWith("nesting depth exceeds the maximum of " + maxDepth);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private Person createPerson(String name, String city, String country) {
|
||||
return new Person(name, new Address(city, country));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create two beans that are each other's spouse, so that a nested property
|
||||
* path consisting of any number of {@code spouse} segments can be traversed.
|
||||
* @return a {@code "rod"} bean, whose spouse is a {@code "kerry"} bean
|
||||
*/
|
||||
private static ITestBean createSpouseCycle() {
|
||||
ITestBean rod = new TestBean("rod", 31);
|
||||
ITestBean kerry = new TestBean("kerry", 35);
|
||||
rod.setSpouse(kerry);
|
||||
kerry.setSpouse(rod);
|
||||
return rod;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class Simple {
|
||||
|
||||
@@ -294,9 +294,18 @@ class BeanWrapperTests extends AbstractPropertyAccessorTests {
|
||||
void incompletelyQuotedKeyLeadsToPropertyException() {
|
||||
TestBean target = new TestBean();
|
||||
BeanWrapper accessor = createAccessor(target);
|
||||
assertThatExceptionOfType(NotWritablePropertyException.class)
|
||||
assertThatExceptionOfType(InvalidPropertyPathException.class)
|
||||
.isThrownBy(() -> accessor.setPropertyValue("[']", "foobar"))
|
||||
.satisfies(ex -> assertThat(ex.getPossibleMatches()).isNull());
|
||||
.withMessageContaining("unterminated quote");
|
||||
}
|
||||
|
||||
@Test // gh-37275
|
||||
void getPropertyDescriptorForMalformedPathThrowsInvalidPropertyException() {
|
||||
TestBean target = new TestBean();
|
||||
BeanWrapper accessor = createAccessor(target);
|
||||
assertThatExceptionOfType(InvalidPropertyException.class)
|
||||
.isThrownBy(() -> accessor.getPropertyDescriptor("map[unterminated"))
|
||||
.withCauseInstanceOf(InvalidPropertyPathException.class);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -25,7 +25,9 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Chris Beams
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
class PropertyAccessorUtilsTests {
|
||||
|
||||
@Test
|
||||
@@ -49,12 +51,24 @@ class PropertyAccessorUtilsTests {
|
||||
void getFirstNestedPropertySeparatorIndex() {
|
||||
assertThat(PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex("[user]")).isEqualTo(-1);
|
||||
assertThat(PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex("user.name")).isEqualTo(4);
|
||||
assertThat(PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex("map[key[0]].name")).isEqualTo(11);
|
||||
|
||||
// A dot nested two (unclosed) bracket levels deep must not be mistaken
|
||||
// for a top-level separator, even though the net number of brackets
|
||||
// seen so far is even.
|
||||
assertThat(PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex("a[b[c.d]]")).isEqualTo(-1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getLastNestedPropertySeparatorIndex() {
|
||||
assertThat(PropertyAccessorUtils.getLastNestedPropertySeparatorIndex("[user]")).isEqualTo(-1);
|
||||
assertThat(PropertyAccessorUtils.getLastNestedPropertySeparatorIndex("user.address.street")).isEqualTo(12);
|
||||
assertThat(PropertyAccessorUtils.getLastNestedPropertySeparatorIndex("map[key[0]].name")).isEqualTo(11);
|
||||
|
||||
// Symmetric case to the one above, but scanning from the end: a dot
|
||||
// preceded (from the right) by two unmatched closing brackets must
|
||||
// not be mistaken for a top-level separator.
|
||||
assertThat(PropertyAccessorUtils.getLastNestedPropertySeparatorIndex("d.c]b]a")).isEqualTo(-1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -67,6 +81,13 @@ class PropertyAccessorUtilsTests {
|
||||
assertThat(PropertyAccessorUtils.matchesProperty("user[name]", "user")).isTrue();
|
||||
}
|
||||
|
||||
@Test // gh-37275
|
||||
void matchesPropertyNeverThrowsForMalformedInput() {
|
||||
// Non-throwing, best-effort contract, same as canonicalPropertyName.
|
||||
assertThat(PropertyAccessorUtils.matchesProperty("map[key1]other", "map")).isFalse();
|
||||
assertThat(PropertyAccessorUtils.matchesProperty("map", "map[key1]other")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void canonicalPropertyName() {
|
||||
assertThat(PropertyAccessorUtils.canonicalPropertyName(null)).isEmpty();
|
||||
@@ -83,6 +104,13 @@ class PropertyAccessorUtilsTests {
|
||||
assertThat(PropertyAccessorUtils.canonicalPropertyName("map[\"key1]")).isEqualTo("map[\"key1]");
|
||||
assertThat(PropertyAccessorUtils.canonicalPropertyName("map[']")).isEqualTo("map[']");
|
||||
assertThat(PropertyAccessorUtils.canonicalPropertyName("map[\"]")).isEqualTo("map[\"]");
|
||||
|
||||
// Keys that themselves contain bracket characters must be resolved
|
||||
// using depth-aware parsing, consistent with how the property
|
||||
// accessor resolves the same paths during actual data binding.
|
||||
assertThat(PropertyAccessorUtils.canonicalPropertyName("map[\"key[0]\"]")).isEqualTo("map[key[0]]");
|
||||
assertThat(PropertyAccessorUtils.canonicalPropertyName("map['key[0]'].name")).isEqualTo("map[key[0]].name");
|
||||
assertThat(PropertyAccessorUtils.canonicalPropertyName("users['admin[0]']")).isEqualTo("users[admin[0]]");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+18
@@ -69,6 +69,13 @@ class PropertyDescriptorUtilsPropertyResolutionTests {
|
||||
assertReadAndWriteMethodsForClassAndId(pdMap, Number.class, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void classWithStaticGetters() {
|
||||
var pdMap = resolver.resolve(ClassWithStaticGetters.class);
|
||||
|
||||
assertThat(pdMap).containsOnlyKeys("class");
|
||||
}
|
||||
|
||||
@Test
|
||||
void classWithOnlySetter() {
|
||||
var pdMap = resolver.resolve(ClassWithOnlySetter.class);
|
||||
@@ -134,6 +141,17 @@ class PropertyDescriptorUtilsPropertyResolutionTests {
|
||||
}
|
||||
}
|
||||
|
||||
static class ClassWithStaticGetters {
|
||||
|
||||
public static Long getId() {
|
||||
return 42L;
|
||||
}
|
||||
|
||||
public static boolean isActive() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static class ClassWithOnlySetter {
|
||||
|
||||
public void setId(Long id) {
|
||||
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.beans;
|
||||
|
||||
import java.beans.PropertyEditor;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.Timeout;
|
||||
|
||||
import org.springframework.beans.propertyeditors.CustomNumberEditor;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link PropertyEditorRegistrySupport}.
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @since 7.1
|
||||
*/
|
||||
class PropertyEditorRegistrySupportTests {
|
||||
|
||||
/**
|
||||
* Matches the private {@code MAX_STRIPPED_PROPERTY_PATH_DEPTH} constant in
|
||||
* {@link PropertyEditorRegistrySupport}.
|
||||
*/
|
||||
private static final int MAX_DEPTH = 8;
|
||||
|
||||
private final PropertyEditorRegistrySupport registry = new PropertyEditorRegistrySupport();
|
||||
|
||||
private final PropertyEditor editor = new CustomNumberEditor(Integer.class, true);
|
||||
|
||||
|
||||
@Test
|
||||
void findCustomEditorMatchesStrippedPathAtMaxSupportedNestingDepth() {
|
||||
registry.registerCustomEditor(null, "list", this.editor);
|
||||
|
||||
String propertyPath = "list" + "[0]".repeat(MAX_DEPTH);
|
||||
assertThat(registry.findCustomEditor(null, propertyPath)).isSameAs(this.editor);
|
||||
}
|
||||
|
||||
@Test
|
||||
void findCustomEditorDoesNotMatchStrippedPathBeyondMaxSupportedNestingDepth() {
|
||||
registry.registerCustomEditor(null, "list", this.editor);
|
||||
|
||||
String propertyPath = "list" + "[0]".repeat(MAX_DEPTH + 1);
|
||||
assertThat(registry.findCustomEditor(null, propertyPath)).isNull();
|
||||
}
|
||||
|
||||
@Test // gh-37020
|
||||
@Timeout(5)
|
||||
void findCustomEditorWithExcessivelyNestedPropertyPathDoesNotHang() {
|
||||
registry.registerCustomEditor(null, "attrs", this.editor);
|
||||
|
||||
// A property path with a bracket-nesting depth (40) that would previously
|
||||
// have caused addStrippedPropertyPaths() to recursively enumerate 2^40 - 1
|
||||
// stripped path variants. With the depth limit in place, this should return
|
||||
// promptly.
|
||||
String propertyPath = "attrs" + "[k]".repeat(40);
|
||||
|
||||
assertThat(registry.findCustomEditor(null, propertyPath)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void guessPropertyTypeFromEditorsMatchesStrippedPathAtMaxSupportedNestingDepth() {
|
||||
registry.registerCustomEditor(Integer.class, "list", this.editor);
|
||||
|
||||
String propertyPath = "list" + "[0]".repeat(MAX_DEPTH);
|
||||
assertThat(registry.guessPropertyTypeFromEditors(propertyPath)).isEqualTo(Integer.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void guessPropertyTypeFromEditorsDoesNotMatchStrippedPathBeyondMaxSupportedNestingDepth() {
|
||||
registry.registerCustomEditor(Integer.class, "list", this.editor);
|
||||
|
||||
String propertyPath = "list" + "[0]".repeat(MAX_DEPTH + 1);
|
||||
assertThat(registry.guessPropertyTypeFromEditors(propertyPath)).isNull();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.beans;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import org.springframework.beans.PropertyPath.Segment;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link PropertyPath}.
|
||||
*/
|
||||
class PropertyPathTests {
|
||||
|
||||
@Test
|
||||
void canonicalNameOfWellFormedPath() {
|
||||
assertThat(canonicalName("name")).isEqualTo("name");
|
||||
assertThat(canonicalName("person.name")).isEqualTo("person.name");
|
||||
assertThat(canonicalName("person.addresses[1].city")).isEqualTo("person.addresses[1].city");
|
||||
assertThat(canonicalName("map[key1]")).isEqualTo("map[key1]");
|
||||
assertThat(canonicalName("map['key1']")).isEqualTo("map[key1]");
|
||||
assertThat(canonicalName("map[\"key1\"]")).isEqualTo("map[key1]");
|
||||
assertThat(canonicalName("map['key1'].name")).isEqualTo("map[key1].name");
|
||||
assertThat(canonicalName("map[key1][key2]")).isEqualTo("map[key1][key2]");
|
||||
assertThat(canonicalName("map['key1'][\"key2\"]")).isEqualTo("map[key1][key2]");
|
||||
assertThat(canonicalName("map[key[0]]")).isEqualTo("map[key[0]]");
|
||||
assertThat(canonicalName("map['key[0]']")).isEqualTo("map[key[0]]");
|
||||
assertThat(canonicalName("map['key[0]'].name")).isEqualTo("map[key[0]].name");
|
||||
assertThat(canonicalName("users['admin[0]']")).isEqualTo("users[admin[0]]");
|
||||
assertThat(canonicalName("map[]")).isEqualTo("map[]");
|
||||
assertThat(canonicalName("map['']")).isEqualTo("map[]");
|
||||
assertThat(canonicalName("map[\"\"]")).isEqualTo("map[]");
|
||||
assertThat(canonicalName("map[my.key]")).isEqualTo("map[my.key]");
|
||||
assertThat(canonicalName("map[[a]]")).isEqualTo("map[[a]]");
|
||||
assertThat(canonicalName("[user]")).isEqualTo("[user]");
|
||||
assertThat(canonicalName("[user].name")).isEqualTo("[user].name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void canonicalNameKeepsQuotesWhenRequired() {
|
||||
assertThat(canonicalName("map['a]b']")).isEqualTo("map['a]b']");
|
||||
assertThat(canonicalName("map['a[b']")).isEqualTo("map['a[b']");
|
||||
assertThat(canonicalName("map[\"a'b\"]")).isEqualTo("map[\"a'b\"]");
|
||||
assertThat(canonicalName("map['a\"b']")).isEqualTo("map['a\"b']");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseEmptyPath() {
|
||||
PropertyPath path = PropertyPath.parse("");
|
||||
|
||||
assertThat(path.canonicalName()).isEmpty();
|
||||
assertThat(path.segments()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseSimplePath() {
|
||||
assertThat(PropertyPath.parse("name").segments())
|
||||
.containsExactly(new Segment("name", List.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseNestedPath() {
|
||||
assertThat(PropertyPath.parse("person.addresses[1].city").segments())
|
||||
.containsExactly(
|
||||
new Segment("person", List.of()),
|
||||
new Segment("addresses", List.of("1")),
|
||||
new Segment("city", List.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseMultipleIndexesInOneSegment() {
|
||||
assertThat(PropertyPath.parse("map[key1][key2]").segments())
|
||||
.containsExactly(new Segment("map", List.of("key1", "key2")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseRootLevelIndexedAccess() {
|
||||
assertThat(PropertyPath.parse("[user]").segments())
|
||||
.containsExactly(new Segment("", List.of("user")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseStripsQuotesFromKeys() {
|
||||
assertThat(PropertyPath.parse("map['key1'][\"key2\"]").segments())
|
||||
.containsExactly(new Segment("map", List.of("key1", "key2")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseKeepsDotsInsideKeysOpaque() {
|
||||
assertThat(PropertyPath.parse("map[my.key].name").segments())
|
||||
.containsExactly(
|
||||
new Segment("map", List.of("my.key")),
|
||||
new Segment("name", List.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseKeepsNestedBracketsInRawKey() {
|
||||
assertThat(PropertyPath.parse("map[key[0]]").segments())
|
||||
.containsExactly(new Segment("map", List.of("key[0]")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseKeepsUnbalancedBracketInQuotedKey() {
|
||||
assertThat(PropertyPath.parse("map['a]b']").segments())
|
||||
.containsExactly(new Segment("map", List.of("a]b")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseEmptyKey() {
|
||||
assertThat(PropertyPath.parse("map[]").segments())
|
||||
.containsExactly(new Segment("map", List.of("")));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"", "name", "person.name", "person.addresses[1].city", "map[key1]",
|
||||
"map['key1']", "map[\"key1\"]", "map[key1][key2]", "map[key[0]]", "map['key[0]']",
|
||||
"map[]", "map['']", "map[my.key]", "map[[a]]", "[user]", "[user].name",
|
||||
"map['a]b']", "map['a[b']", "map[\"a'b\"]", "map['a\"b']", "map['']['a]b']"})
|
||||
void canonicalNameIdempotent(String path) {
|
||||
PropertyPath parsed = PropertyPath.parse(path);
|
||||
|
||||
PropertyPath reparsed = PropertyPath.parse(parsed.canonicalName());
|
||||
|
||||
assertThat(reparsed.segments()).isEqualTo(parsed.segments());
|
||||
assertThat(reparsed.canonicalName()).isEqualTo(parsed.canonicalName());
|
||||
assertThat(reparsed).isEqualTo(parsed);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"name", "person.name", "person.addresses[1].city", "person.map[key1]",
|
||||
"person.map['key1']", "person.map[\"key1\"]", "map[key1].map[key2]", "map['key[0]']",
|
||||
"person.map['a]b']", "person.map['a[b']", "person.map[\"a'b\"]", "person.map['a\"b']", "person.map['']['a]b']"})
|
||||
void canonicalPathAndSegmentsAreEquivalent(String path) {
|
||||
PropertyPath parsed = PropertyPath.parse(path);
|
||||
assertThat(parsed.segments().stream()
|
||||
.map(Segment::toCanonicalName)
|
||||
.reduce((a, b) -> a + "." + b))
|
||||
.hasValue(parsed.canonicalName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void segmentsCannotBeModified() {
|
||||
PropertyPath path = PropertyPath.parse("map[key1]");
|
||||
|
||||
assertThat(path.segments()).hasSize(1);
|
||||
assertThatExceptionOfType(UnsupportedOperationException.class)
|
||||
.isThrownBy(() -> path.segments().clear());
|
||||
assertThatExceptionOfType(UnsupportedOperationException.class)
|
||||
.isThrownBy(() -> path.segments().get(0).keys().clear());
|
||||
}
|
||||
|
||||
@Test
|
||||
void equalsAndHashCodeUseCanonicalName() {
|
||||
assertThat(PropertyPath.parse("map['key1'].name"))
|
||||
.isEqualTo(PropertyPath.parse("map[key1].name"))
|
||||
.hasSameHashCodeAs(PropertyPath.parse("map[key1].name"));
|
||||
assertThat(PropertyPath.parse("map[key1]")).isNotEqualTo(PropertyPath.parse("map[key2]"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void toStringReturnsCanonicalName() {
|
||||
assertThat(PropertyPath.parse("map['key1'].name")).hasToString("map[key1].name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseRejectsNull() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> PropertyPath.parse(null));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"map[key1]other", "map[key1]other.name", "map[key1]IGNORED[key2]",
|
||||
"options[priority]X", "options[security]IGNORED[role]", "map[key1]]", "[user]x"})
|
||||
void parseRejectsTextAfterIndex(String path) {
|
||||
assertThatExceptionOfType(InvalidPropertyPathException.class)
|
||||
.isThrownBy(() -> PropertyPath.parse(path))
|
||||
.withMessageContaining("after an index")
|
||||
.satisfies(ex -> assertThat(ex.getPropertyPath()).isEqualTo(path));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"map[key1", "map[", "map[[a]", "map[a][", "map['a'"})
|
||||
void parseRejectsUnclosedIndex(String path) {
|
||||
assertThatExceptionOfType(InvalidPropertyPathException.class)
|
||||
.isThrownBy(() -> PropertyPath.parse(path))
|
||||
.withMessageContaining("unclosed '['");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"map]", "map]name", "publication.].published", "address.].city", "]"})
|
||||
void parseRejectsUnmatchedIndexSuffix(String path) {
|
||||
assertThatExceptionOfType(InvalidPropertyPathException.class)
|
||||
.isThrownBy(() -> PropertyPath.parse(path))
|
||||
.withMessageContaining("without a matching '['");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {".name", "person.", "person..name", ".", "..", "map[key1]."})
|
||||
void parseRejectsEmptySegment(String path) {
|
||||
assertThatExceptionOfType(InvalidPropertyPathException.class)
|
||||
.isThrownBy(() -> PropertyPath.parse(path))
|
||||
.withMessageContaining("empty path segment");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"map[']", "map[\"]", "map['key1]", "map[\"key1]", "map['a"})
|
||||
void parseRejectsUnterminatedQuote(String path) {
|
||||
assertThatExceptionOfType(InvalidPropertyPathException.class)
|
||||
.isThrownBy(() -> PropertyPath.parse(path))
|
||||
.withMessageContaining("unterminated quote");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"map[don't]", "map[a'b]", "map[a\"b]"})
|
||||
void parseRejectsQuoteInRawKey(String path) {
|
||||
assertThatExceptionOfType(InvalidPropertyPathException.class)
|
||||
.isThrownBy(() -> PropertyPath.parse(path))
|
||||
.withMessageContaining("in an unquoted key");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"map['a'b']", "map['a'b]", "map[\"a\"b\"]"})
|
||||
void parseRejectsTextAfterClosingQuote(String path) {
|
||||
assertThatExceptionOfType(InvalidPropertyPathException.class)
|
||||
.isThrownBy(() -> PropertyPath.parse(path))
|
||||
.withMessageContaining("after a closing quote");
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidPropertyPathExceptionIsAPropertyAccessException() {
|
||||
assertThatExceptionOfType(InvalidPropertyPathException.class)
|
||||
.isThrownBy(() -> PropertyPath.parse("map[key1]other"))
|
||||
.isInstanceOf(PropertyAccessException.class)
|
||||
.withMessageContaining("Invalid property path 'map[key1]other'")
|
||||
.satisfies(ex -> {
|
||||
assertThat(ex.getErrorCode()).isEqualTo(InvalidPropertyPathException.ERROR_CODE);
|
||||
assertThat(ex.getPropertyPath()).isEqualTo("map[key1]other");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void failsWhenExceedsNestingDepth() {
|
||||
assertThatExceptionOfType(InvalidPropertyPathException.class)
|
||||
.isThrownBy(() ->
|
||||
PropertyPath.parse("one.two.three[1].four", PropertyPath.Options.withMaxNestedPathDepth(2)))
|
||||
.withMessageContaining("nesting depth exceeds the maximum of 2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void subPathFromZeroReturnsSamePath() {
|
||||
PropertyPath path = PropertyPath.parse("address.country.name");
|
||||
assertThat(path.subPath(0)).isSameAs(path);
|
||||
}
|
||||
|
||||
@Test
|
||||
void subPathDropsLeadingSegments() {
|
||||
PropertyPath subPath = PropertyPath.parse("address.country[0].name").subPath(1);
|
||||
assertThat(subPath.canonicalName()).isEqualTo("country[0].name");
|
||||
assertThat(subPath.segments()).containsExactly(
|
||||
new Segment("country", List.of("0")),
|
||||
new Segment("name", List.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void subPathAtLastIndexIsEmpty() {
|
||||
PropertyPath subPath = PropertyPath.parse("address.name").subPath(2);
|
||||
assertThat(subPath.canonicalName()).isEmpty();
|
||||
assertThat(subPath.segments()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void subPathRejectsOutOfBoundsIndex() {
|
||||
PropertyPath path = PropertyPath.parse("address.name");
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> path.subPath(3));
|
||||
}
|
||||
|
||||
|
||||
private static String canonicalName(String path) {
|
||||
return PropertyPath.parse(path).canonicalName();
|
||||
}
|
||||
|
||||
}
|
||||
+60
-1
@@ -19,6 +19,7 @@ package org.springframework.beans.factory;
|
||||
import java.io.Closeable;
|
||||
import java.io.Serializable;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.net.MalformedURLException;
|
||||
import java.text.NumberFormat;
|
||||
import java.text.ParseException;
|
||||
@@ -79,6 +80,7 @@ import org.springframework.beans.testfixture.beans.TestBean;
|
||||
import org.springframework.beans.testfixture.beans.factory.DummyFactory;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.core.annotation.Order;
|
||||
@@ -1236,11 +1238,13 @@ class DefaultListableBeanFactoryTests {
|
||||
RootBeanDefinition bd1 = new RootBeanDefinition(TestBean.class);
|
||||
bd1.setScope(BeanDefinition.SCOPE_PROTOTYPE);
|
||||
lbf.registerBeanDefinition("testBean", bd1);
|
||||
assertThat(lbf.getBeanDefinition("testBean").getScope()).isEqualTo(BeanDefinition.SCOPE_PROTOTYPE);
|
||||
assertThat(lbf.getBean("testBean")).isInstanceOf(TestBean.class);
|
||||
|
||||
RootBeanDefinition bd2 = new RootBeanDefinition(NestedTestBean.class);
|
||||
bd2.setScope(BeanDefinition.SCOPE_PROTOTYPE);
|
||||
bd2.setScope(BeanDefinition.SCOPE_SINGLETON);
|
||||
lbf.registerBeanDefinition("testBean", bd2);
|
||||
assertThat(lbf.getBeanDefinition("testBean").getScope()).isEqualTo(BeanDefinition.SCOPE_SINGLETON);
|
||||
assertThat(lbf.getBean("testBean")).isInstanceOf(NestedTestBean.class);
|
||||
}
|
||||
|
||||
@@ -1682,6 +1686,46 @@ class DefaultListableBeanFactoryTests {
|
||||
lbf.getBean(TestBean.class));
|
||||
}
|
||||
|
||||
@Test // gh-34687
|
||||
void getBeanByNameWithTypeReference() {
|
||||
RootBeanDefinition bd1 = new RootBeanDefinition(StringTemplate.class);
|
||||
RootBeanDefinition bd2 = new RootBeanDefinition(NumberTemplate.class);
|
||||
lbf.registerBeanDefinition("bd1", bd1);
|
||||
lbf.registerBeanDefinition("bd2", bd2);
|
||||
|
||||
Template<String> stringTemplate = lbf.getBean("bd1", new ParameterizedTypeReference<>() {});
|
||||
Template<Number> numberTemplate = lbf.getBean("bd2", new ParameterizedTypeReference<>() {});
|
||||
|
||||
assertThat(stringTemplate).isInstanceOf(StringTemplate.class);
|
||||
assertThat(numberTemplate).isInstanceOf(NumberTemplate.class);
|
||||
|
||||
assertThatExceptionOfType(BeanNotOfRequiredTypeException.class)
|
||||
.isThrownBy(() -> lbf.getBean("bd2", new ParameterizedTypeReference<Template<String>>() {}))
|
||||
.satisfies(ex -> {
|
||||
assertThat(ex.getBeanName()).isEqualTo("bd2");
|
||||
assertThat(ex.getRequiredType()).isEqualTo(Template.class);
|
||||
assertThat(ex.getActualType()).isEqualTo(NumberTemplate.class);
|
||||
assertThat(ex.getGenericRequiredType().toString()).endsWith("Template<java.lang.String>");
|
||||
});
|
||||
}
|
||||
|
||||
@Test // gh-37047
|
||||
void getBeanByNameWithTypeReferenceMatchingGenericTypeOnAopProxy() {
|
||||
RootBeanDefinition bd = new RootBeanDefinition(ProxiedStringTemplate.class);
|
||||
bd.setTargetType(ResolvableType.forClass(ProxiedStringTemplate.class));
|
||||
lbf.registerBeanDefinition("proxiedTemplate", bd);
|
||||
|
||||
// Simulate a JDK dynamic AOP proxy (as created for proxyTargetClass=false)
|
||||
// which only exposes the raw ProxiedTemplate interface, erasing the
|
||||
// generic type information that is available on the target class.
|
||||
Object proxy = Proxy.newProxyInstance(getClass().getClassLoader(),
|
||||
new Class<?>[] {ProxiedTemplate.class}, (target, method, args) -> null);
|
||||
lbf.registerSingleton("proxiedTemplate", proxy);
|
||||
|
||||
ProxiedTemplate<String> template = lbf.getBean("proxiedTemplate", new ParameterizedTypeReference<>() {});
|
||||
assertThat(template).isSameAs(proxy);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBeanByTypeWithPrimary() {
|
||||
RootBeanDefinition bd1 = new RootBeanDefinition(TestBean.class);
|
||||
@@ -3872,4 +3916,19 @@ class DefaultListableBeanFactoryTests {
|
||||
}
|
||||
}
|
||||
|
||||
private static class Template<T> {
|
||||
}
|
||||
|
||||
private static class StringTemplate extends Template<String> {
|
||||
}
|
||||
|
||||
private static class NumberTemplate extends Template<Number> {
|
||||
}
|
||||
|
||||
private interface ProxiedTemplate<T> {
|
||||
}
|
||||
|
||||
private static class ProxiedStringTemplate implements ProxiedTemplate<String> {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+68
-12
@@ -45,9 +45,9 @@ class ParameterResolutionTests {
|
||||
|
||||
@Test
|
||||
void isAutowirablePreconditions() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
ParameterResolutionDelegate.isAutowirable(null, 0))
|
||||
.withMessageContaining("Parameter must not be null");
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> ParameterResolutionDelegate.isAutowirable(null, 0))
|
||||
.withMessageContaining("Parameter must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -87,29 +87,30 @@ class ParameterResolutionTests {
|
||||
Parameter[] parameters = notAutowirableConstructor.getParameters();
|
||||
for (int parameterIndex = 0; parameterIndex < parameters.length; parameterIndex++) {
|
||||
Parameter parameter = parameters[parameterIndex];
|
||||
assertThat(ParameterResolutionDelegate.isAutowirable(parameter, parameterIndex)).as("Parameter " + parameter + " must not be autowirable").isFalse();
|
||||
assertThat(ParameterResolutionDelegate.isAutowirable(parameter, parameterIndex))
|
||||
.as("Parameter " + parameter + " must not be autowirable").isFalse();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveDependencyPreconditionsForParameter() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> ParameterResolutionDelegate.resolveDependency(null, 0, null, mock()))
|
||||
.withMessageContaining("Parameter must not be null");
|
||||
.isThrownBy(() -> ParameterResolutionDelegate.resolveDependency(null, 0, null, mock()))
|
||||
.withMessageContaining("Parameter must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveDependencyPreconditionsForContainingClass() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
ParameterResolutionDelegate.resolveDependency(getParameter(), 0, null, null))
|
||||
.withMessageContaining("Containing class must not be null");
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> ParameterResolutionDelegate.resolveDependency(getParameter(), 0, null, null))
|
||||
.withMessageContaining("Containing class must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveDependencyPreconditionsForBeanFactory() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
ParameterResolutionDelegate.resolveDependency(getParameter(), 0, getClass(), null))
|
||||
.withMessageContaining("AutowireCapableBeanFactory must not be null");
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> ParameterResolutionDelegate.resolveDependency(getParameter(), 0, getClass(), null))
|
||||
.withMessageContaining("AutowireCapableBeanFactory must not be null");
|
||||
}
|
||||
|
||||
private Parameter getParameter() throws NoSuchMethodException {
|
||||
@@ -133,9 +134,64 @@ class ParameterResolutionTests {
|
||||
parameter, parameterIndex, AutowirableClass.class, beanFactory);
|
||||
assertThat(intermediateDependencyDescriptor.getAnnotatedElement()).isEqualTo(constructor);
|
||||
assertThat(intermediateDependencyDescriptor.getMethodParameter().getParameter()).isEqualTo(parameter);
|
||||
assertThat(intermediateDependencyDescriptor.usesStandardBeanLookup()).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveDependencyWithCustomParameterNamePreconditionsForParameter() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> ParameterResolutionDelegate.resolveDependency(null, 0, "customName", getClass(), mock()))
|
||||
.withMessageContaining("Parameter must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveDependencyWithCustomParameterNamePreconditionsForContainingClass() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> ParameterResolutionDelegate.resolveDependency(getParameter(), 0, "customName", null, mock()))
|
||||
.withMessageContaining("Containing class must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveDependencyWithCustomParameterNamePreconditionsForBeanFactory() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> ParameterResolutionDelegate.resolveDependency(getParameter(), 0, "customName", getClass(), null))
|
||||
.withMessageContaining("AutowireCapableBeanFactory must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveDependencyWithNullCustomParameterNameFallsBackToDefaultParameterNameDiscovery() throws Exception {
|
||||
Constructor<?> constructor = AutowirableClass.class.getConstructor(String.class, String.class, String.class, String.class);
|
||||
AutowireCapableBeanFactory beanFactory = mock();
|
||||
given(beanFactory.resolveDependency(any(), isNull())).willAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
Parameter[] parameters = constructor.getParameters();
|
||||
for (int parameterIndex = 0; parameterIndex < parameters.length; parameterIndex++) {
|
||||
Parameter parameter = parameters[parameterIndex];
|
||||
DependencyDescriptor via4ArgMethod = (DependencyDescriptor) ParameterResolutionDelegate.resolveDependency(
|
||||
parameter, parameterIndex, AutowirableClass.class, beanFactory);
|
||||
DependencyDescriptor via5ArgMethod = (DependencyDescriptor) ParameterResolutionDelegate.resolveDependency(
|
||||
parameter, parameterIndex, null, AutowirableClass.class, beanFactory);
|
||||
assertThat(via5ArgMethod.getDependencyName()).isEqualTo(via4ArgMethod.getDependencyName());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveDependencyWithCustomParameterName() throws Exception {
|
||||
Constructor<?> constructor = AutowirableClass.class.getConstructor(String.class, String.class, String.class, String.class);
|
||||
AutowireCapableBeanFactory beanFactory = mock();
|
||||
given(beanFactory.resolveDependency(any(), isNull())).willAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
Parameter parameter = constructor.getParameters()[0];
|
||||
DependencyDescriptor descriptor = (DependencyDescriptor) ParameterResolutionDelegate.resolveDependency(
|
||||
parameter, 0, "customBeanName", AutowirableClass.class, beanFactory);
|
||||
|
||||
assertThat(descriptor.getAnnotatedElement()).isEqualTo(constructor);
|
||||
assertThat(descriptor.getMethodParameter().getParameter()).isEqualTo(parameter);
|
||||
assertThat(descriptor.getDependencyName()).isEqualTo("customBeanName");
|
||||
assertThat(descriptor.usesStandardBeanLookup()).isTrue();
|
||||
}
|
||||
|
||||
|
||||
void autowirableMethod(
|
||||
@Autowired String firstParameter,
|
||||
|
||||
+2
-2
@@ -27,10 +27,10 @@ import javax.lang.model.element.Modifier;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.aot.generate.ClassNameGenerator;
|
||||
import org.springframework.aot.generate.GenerationContext;
|
||||
import org.springframework.aot.generate.MethodReference;
|
||||
import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator;
|
||||
import org.springframework.aot.generate.NameGenerator;
|
||||
import org.springframework.aot.generate.ValueCodeGenerationException;
|
||||
import org.springframework.aot.test.generate.TestGenerationContext;
|
||||
import org.springframework.beans.factory.aot.BeanRegistrationsAotContribution.Registration;
|
||||
@@ -109,7 +109,7 @@ class BeanRegistrationsAotContributionTests {
|
||||
@Test
|
||||
void applyToWhenHasNameGeneratesPrefixedFeatureName() {
|
||||
this.generationContext = new TestGenerationContext(
|
||||
new ClassNameGenerator(TestGenerationContext.TEST_TARGET, "Management"));
|
||||
new NameGenerator(TestGenerationContext.TEST_TARGET, "Management"));
|
||||
this.beanFactoryInitializationCode = new MockBeanFactoryInitializationCode(this.generationContext);
|
||||
RegisteredBean registeredBean = registerBean(new RootBeanDefinition(TestBean.class));
|
||||
BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator(this.methodGeneratorFactory,
|
||||
|
||||
+11
-1
@@ -21,6 +21,7 @@ import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.core.ParameterizedTypeReference
|
||||
import org.springframework.core.ResolvableType
|
||||
|
||||
/**
|
||||
@@ -53,7 +54,16 @@ class BeanFactoryExtensionsTests {
|
||||
fun `getBean with String and reified type parameters`() {
|
||||
val name = "foo"
|
||||
bf.getBean<Foo>(name)
|
||||
verify { bf.getBean(name, Foo::class.java) }
|
||||
verify { bf.getBean(name, ofType<ParameterizedTypeReference<Foo>>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getBean with String and reified generic type parameters`() {
|
||||
val name = "foo"
|
||||
val foo = listOf(Foo())
|
||||
every { bf.getBean(name, ofType<ParameterizedTypeReference<List<Foo>>>()) } returns foo
|
||||
assertThat(bf.getBean<List<Foo>>("foo")).isSameAs(foo)
|
||||
verify { bf.getBean(name, ofType<ParameterizedTypeReference<List<Foo>>>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
-3
@@ -76,7 +76,6 @@ public class IndexedTestBean {
|
||||
TestBean tb6 = new TestBean("name6", 0);
|
||||
TestBean tb7 = new TestBean("name7", 0);
|
||||
TestBean tb8 = new TestBean("name8", 0);
|
||||
TestBean tb9 = new TestBean("name9", 0);
|
||||
TestBean tbA = new TestBean("nameA", 0);
|
||||
TestBean tbB = new TestBean("nameB", 0);
|
||||
TestBean tbC = new TestBean("nameC", 0);
|
||||
@@ -105,8 +104,6 @@ public class IndexedTestBean {
|
||||
list.add(tbY);
|
||||
this.map.put("key4", list);
|
||||
this.map.put("key5[foo]", tb8);
|
||||
this.map.put("'", tb9);
|
||||
this.map.put("\"", tb9);
|
||||
this.myTestBeans = new MyTestBeans(tbZ);
|
||||
}
|
||||
|
||||
|
||||
+1
-2
@@ -41,8 +41,7 @@ import org.springframework.context.annotation.Role;
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public abstract class AbstractJCacheConfiguration extends AbstractCachingConfiguration {
|
||||
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
protected Supplier<@Nullable CacheResolver> exceptionCacheResolver;
|
||||
protected @Nullable Supplier<@Nullable CacheResolver> exceptionCacheResolver;
|
||||
|
||||
|
||||
@Override
|
||||
|
||||
+9
-5
@@ -79,7 +79,7 @@ public class SimpleMailMessage implements MailMessage, Serializable {
|
||||
this.to = copyOrNull(original.getTo());
|
||||
this.cc = copyOrNull(original.getCc());
|
||||
this.bcc = copyOrNull(original.getBcc());
|
||||
this.sentDate = original.getSentDate();
|
||||
this.sentDate = copyOrNull(original.sentDate);
|
||||
this.subject = original.getSubject();
|
||||
this.text = original.getText();
|
||||
}
|
||||
@@ -147,11 +147,11 @@ public class SimpleMailMessage implements MailMessage, Serializable {
|
||||
|
||||
@Override
|
||||
public void setSentDate(@Nullable Date sentDate) {
|
||||
this.sentDate = sentDate;
|
||||
this.sentDate = copyOrNull(sentDate);
|
||||
}
|
||||
|
||||
public @Nullable Date getSentDate() {
|
||||
return this.sentDate;
|
||||
return copyOrNull(this.sentDate);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -194,8 +194,8 @@ public class SimpleMailMessage implements MailMessage, Serializable {
|
||||
if (getBcc() != null) {
|
||||
target.setBcc(copy(getBcc()));
|
||||
}
|
||||
if (getSentDate() != null) {
|
||||
target.setSentDate(getSentDate());
|
||||
if (this.sentDate != null) {
|
||||
target.setSentDate((Date) this.sentDate.clone());
|
||||
}
|
||||
if (getSubject() != null) {
|
||||
target.setSubject(getSubject());
|
||||
@@ -247,6 +247,10 @@ public class SimpleMailMessage implements MailMessage, Serializable {
|
||||
return copy(state);
|
||||
}
|
||||
|
||||
private static @Nullable Date copyOrNull(@Nullable Date date) {
|
||||
return (date != null ? (Date) date.clone() : null);
|
||||
}
|
||||
|
||||
private static String[] copy(String[] state) {
|
||||
return state.clone();
|
||||
}
|
||||
|
||||
+63
@@ -21,11 +21,16 @@ import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link SimpleMailMessage}.
|
||||
*
|
||||
* @author Dmitriy Kopylenko
|
||||
* @author Juergen Hoeller
|
||||
* @author Rick Evans
|
||||
@@ -98,6 +103,64 @@ class SimpleMailMessageTests {
|
||||
assertThat(copy.getBcc()[0]).isEqualTo("us@mail.org");
|
||||
}
|
||||
|
||||
@Test // gh-36626
|
||||
void setSentDateStoresACopy() {
|
||||
SimpleMailMessage message = new SimpleMailMessage();
|
||||
Date sentDate = new Date(1234L);
|
||||
|
||||
message.setSentDate(sentDate);
|
||||
sentDate.setTime(0L);
|
||||
|
||||
assertThat(message.getSentDate()).isEqualTo(new Date(1234L));
|
||||
}
|
||||
|
||||
@Test // gh-36626
|
||||
void getSentDateReturnsACopy() {
|
||||
SimpleMailMessage message = new SimpleMailMessage();
|
||||
Date sentDate = new Date(1234L);
|
||||
message.setSentDate(sentDate);
|
||||
|
||||
Date exportedDate = message.getSentDate();
|
||||
exportedDate.setTime(0L);
|
||||
|
||||
assertThat(message.getSentDate()).isEqualTo(new Date(1234L));
|
||||
}
|
||||
|
||||
@Test // gh-36626
|
||||
void copyConstructorCopiesSentDate() {
|
||||
Date sentDate = new Date(1234L);
|
||||
SimpleMailMessage original = new SimpleMailMessage();
|
||||
original.setSentDate(sentDate);
|
||||
|
||||
SimpleMailMessage copy = new SimpleMailMessage(original);
|
||||
sentDate.setTime(0L);
|
||||
|
||||
Date copiedDate = copy.getSentDate();
|
||||
assertThat(copiedDate).isNotNull();
|
||||
copiedDate.setTime(1L);
|
||||
|
||||
assertThat(original.getSentDate()).isEqualTo(new Date(1234L));
|
||||
assertThat(copy.getSentDate()).isEqualTo(new Date(1234L));
|
||||
}
|
||||
|
||||
@Test // gh-36626
|
||||
void copyToCopiesSentDate() {
|
||||
SimpleMailMessage source = new SimpleMailMessage();
|
||||
source.setSentDate(new Date(1234L));
|
||||
|
||||
MailMessage target = mock();
|
||||
source.copyTo(target);
|
||||
|
||||
ArgumentCaptor<Date> dateCaptor = ArgumentCaptor.forClass(Date.class);
|
||||
verify(target).setSentDate(dateCaptor.capture());
|
||||
|
||||
Date copiedDate = dateCaptor.getValue();
|
||||
assertThat(copiedDate).isNotNull();
|
||||
copiedDate.setTime(0L);
|
||||
|
||||
assertThat(source.getSentDate()).isEqualTo(new Date(1234L));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that two equal SimpleMailMessages have equal hash codes.
|
||||
*/
|
||||
|
||||
Vendored
+4
-8
@@ -50,17 +50,13 @@ public abstract class AbstractCachingConfiguration implements ImportAware {
|
||||
|
||||
protected @Nullable AnnotationAttributes enableCaching;
|
||||
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
protected Supplier<@Nullable CacheManager> cacheManager;
|
||||
protected @Nullable Supplier<@Nullable CacheManager> cacheManager;
|
||||
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
protected Supplier<@Nullable CacheResolver> cacheResolver;
|
||||
protected @Nullable Supplier<@Nullable CacheResolver> cacheResolver;
|
||||
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
protected Supplier<@Nullable KeyGenerator> keyGenerator;
|
||||
protected @Nullable Supplier<@Nullable KeyGenerator> keyGenerator;
|
||||
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
protected Supplier<@Nullable CacheErrorHandler> errorHandler;
|
||||
protected @Nullable Supplier<@Nullable CacheErrorHandler> errorHandler;
|
||||
|
||||
|
||||
@Override
|
||||
|
||||
-17
@@ -291,23 +291,6 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convenience method to return a String representation of this Method
|
||||
* for use in logging. Can be overridden in subclasses to provide a
|
||||
* different identifier for the given method.
|
||||
* @param method the method we're interested in
|
||||
* @param targetClass class the method is on
|
||||
* @return log message identifying this method
|
||||
* @see org.springframework.util.ClassUtils#getQualifiedMethodName
|
||||
* @deprecated since 6.2.18 with no replacement, for removal in 7.1
|
||||
*/
|
||||
@Deprecated(since = "6.2.18", forRemoval = true)
|
||||
protected String methodIdentification(Method method, Class<?> targetClass) {
|
||||
Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
|
||||
return ClassUtils.getQualifiedMethodName(specificMethod);
|
||||
}
|
||||
|
||||
protected Collection<? extends Cache> getCaches(
|
||||
CacheOperationInvocationContext<CacheOperation> context, CacheResolver cacheResolver) {
|
||||
|
||||
|
||||
+4
-16
@@ -152,12 +152,12 @@ public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBean
|
||||
private static final Set<Class<? extends Annotation>> resourceAnnotationTypes = CollectionUtils.newLinkedHashSet(2);
|
||||
|
||||
static {
|
||||
JAKARTA_RESOURCE_TYPE = loadAnnotationType("jakarta.annotation.Resource");
|
||||
JAKARTA_RESOURCE_TYPE = AnnotationUtils.loadAnnotationType("jakarta.annotation.Resource");
|
||||
if (JAKARTA_RESOURCE_TYPE != null) {
|
||||
resourceAnnotationTypes.add(JAKARTA_RESOURCE_TYPE);
|
||||
}
|
||||
|
||||
EJB_ANNOTATION_TYPE = loadAnnotationType("jakarta.ejb.EJB");
|
||||
EJB_ANNOTATION_TYPE = AnnotationUtils.loadAnnotationType("jakarta.ejb.EJB");
|
||||
if (EJB_ANNOTATION_TYPE != null) {
|
||||
resourceAnnotationTypes.add(EJB_ANNOTATION_TYPE);
|
||||
}
|
||||
@@ -191,8 +191,8 @@ public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBean
|
||||
setOrder(Ordered.LOWEST_PRECEDENCE - 3);
|
||||
|
||||
// Jakarta EE 9 set of annotations in jakarta.annotation package
|
||||
addInitAnnotationType(loadAnnotationType("jakarta.annotation.PostConstruct"));
|
||||
addDestroyAnnotationType(loadAnnotationType("jakarta.annotation.PreDestroy"));
|
||||
addInitAnnotationType(AnnotationUtils.loadAnnotationType("jakarta.annotation.PostConstruct"));
|
||||
addDestroyAnnotationType(AnnotationUtils.loadAnnotationType("jakarta.annotation.PreDestroy"));
|
||||
|
||||
// java.naming module present on JDK 9+?
|
||||
if (JNDI_PRESENT) {
|
||||
@@ -575,18 +575,6 @@ public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBean
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static @Nullable Class<? extends Annotation> loadAnnotationType(String name) {
|
||||
try {
|
||||
return (Class<? extends Annotation>)
|
||||
ClassUtils.forName(name, CommonAnnotationBeanPostProcessor.class.getClassLoader());
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Class representing generic injection information about an annotated field
|
||||
* or setter method, supporting @Resource and related annotations.
|
||||
|
||||
+1
-1
@@ -276,7 +276,7 @@ public class ComponentScanBeanDefinitionParser implements BeanDefinitionParser {
|
||||
strategyType.getName() + "]: a zero-argument constructor is required", ex);
|
||||
}
|
||||
|
||||
if (!strategyType.isAssignableFrom(result.getClass())) {
|
||||
if (!strategyType.isInstance(result)) {
|
||||
throw new IllegalArgumentException("Provided class name must be an implementation of " + strategyType);
|
||||
}
|
||||
return result;
|
||||
|
||||
+4
-2
@@ -38,9 +38,11 @@ public interface ConfigurationBeanNameGenerator extends BeanNameGenerator {
|
||||
/**
|
||||
* Derive a default bean name for the given {@link Bean @Bean} method,
|
||||
* taking into account the specified {@link Bean#name() name} attribute.
|
||||
* <p>As of 7.1, the original {@code @Bean} name (typically the method name)
|
||||
* will be registered as an alias if that name has not been taken already.
|
||||
* @param beanMethod the method metadata for the {@link Bean @Bean} method
|
||||
* @param beanName the {@link Bean#name() name} attribute or {@code null} if
|
||||
* none is specified
|
||||
* @param beanName the {@link Bean#name() name} attribute or {@code null}
|
||||
* if none is specified
|
||||
* @return the default bean name to use
|
||||
*/
|
||||
String deriveBeanName(MethodMetadata beanMethod, @Nullable String beanName);
|
||||
|
||||
+13
-6
@@ -210,6 +210,11 @@ class ConfigurationClassBeanDefinitionReader {
|
||||
String localBeanName = defaultBeanName(beanName, methodName);
|
||||
beanName = (this.importBeanNameGenerator instanceof ConfigurationBeanNameGenerator cbng ?
|
||||
cbng.deriveBeanName(metadata, beanName) : localBeanName);
|
||||
if (!localBeanName.equals(beanName) && !this.registry.containsBeanDefinition(localBeanName) &&
|
||||
!this.registry.isAlias(localBeanName)) {
|
||||
// Register original name as alias unless registered already.
|
||||
this.registry.registerAlias(beanName, localBeanName);
|
||||
}
|
||||
if (explicitNames.length > 0) {
|
||||
// Register aliases even when overridden below.
|
||||
for (int i = 1; i < explicitNames.length; i++) {
|
||||
@@ -423,12 +428,14 @@ class ConfigurationClassBeanDefinitionReader {
|
||||
}
|
||||
|
||||
private void loadBeanDefinitionsFromBeanRegistrars(MultiValueMap<String, BeanRegistrar> registrars) {
|
||||
if (!(this.registry instanceof ListableBeanFactory beanFactory)) {
|
||||
throw new IllegalStateException("Cannot support bean registrars since " +
|
||||
this.registry.getClass().getName() + " does not implement ListableBeanFactory");
|
||||
}
|
||||
registrars.values().forEach(registrarList -> registrarList.forEach(registrar -> registrar.register(new BeanRegistryAdapter(
|
||||
this.registry, beanFactory, this.environment, registrar.getClass()), this.environment)));
|
||||
registrars.values().forEach(registrarList -> registrarList.forEach(registrar -> {
|
||||
if (!(this.registry instanceof ListableBeanFactory beanFactory)) {
|
||||
throw new IllegalStateException("Cannot support bean registrars since " +
|
||||
this.registry.getClass().getName() + " does not implement ListableBeanFactory");
|
||||
}
|
||||
registrar.register(new BeanRegistryAdapter(
|
||||
this.registry, beanFactory, this.environment, registrar.getClass()), this.environment);
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
|
||||
+7
-4
@@ -32,10 +32,13 @@ import org.springframework.core.type.MethodMetadata;
|
||||
* {@code @Bean} methods (which uses the plain method name), primarily for use
|
||||
* in large applications with potential bean name overlaps. Favor this bean
|
||||
* naming strategy over {@code FullyQualifiedAnnotationBeanNameGenerator} if
|
||||
* you expect such naming conflicts for {@code @Bean} methods, as long as the
|
||||
* application does not depend on {@code @Bean} method names as bean names.
|
||||
* Where the name does matter, make sure to declare {@code @Bean("myBeanName")}
|
||||
* in such a scenario, even if it repeats the method name as the bean name.
|
||||
* you expect such naming conflicts for {@code @Bean} methods.
|
||||
*
|
||||
* <p>As of 7.1, the original {@code @Bean} method name will be registered
|
||||
* as an alias if that name has not been taken already: effectively on first
|
||||
* occurrence, whereas any later {@code @Bean} methods of the same name will
|
||||
* not have aliases applied. This preserves the availability of common beans
|
||||
* under the original bean names for retrieval and injection purposes.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @since 7.0
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user