web-reactive.md 290.5 KB
Newer Older
茶陵後's avatar
茶陵後 已提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 2406 2407 2408 2409 2410 2411 2412 2413 2414 2415 2416 2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 2427 2428 2429 2430 2431 2432 2433 2434 2435 2436 2437 2438 2439 2440 2441 2442 2443 2444 2445 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502 2503 2504 2505 2506 2507 2508 2509 2510 2511 2512 2513 2514 2515 2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 2559 2560 2561 2562 2563 2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 2587 2588 2589 2590 2591 2592 2593 2594 2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 2630 2631 2632 2633 2634 2635 2636 2637 2638 2639 2640 2641 2642 2643 2644 2645 2646 2647 2648 2649 2650 2651 2652 2653 2654 2655 2656 2657 2658 2659 2660 2661 2662 2663 2664 2665 2666 2667 2668 2669 2670 2671 2672 2673 2674 2675 2676 2677 2678 2679 2680 2681 2682 2683 2684 2685 2686 2687 2688 2689 2690 2691 2692 2693 2694 2695 2696 2697 2698 2699 2700 2701 2702 2703 2704 2705 2706 2707 2708 2709 2710 2711 2712 2713 2714 2715 2716 2717 2718 2719 2720 2721 2722 2723 2724 2725 2726 2727 2728 2729 2730 2731 2732 2733 2734 2735 2736 2737 2738 2739 2740 2741 2742 2743 2744 2745 2746 2747 2748 2749 2750 2751 2752 2753 2754 2755 2756 2757 2758 2759 2760 2761 2762 2763 2764 2765 2766 2767 2768 2769 2770 2771 2772 2773 2774 2775 2776 2777 2778 2779 2780 2781 2782 2783 2784 2785 2786 2787 2788 2789 2790 2791 2792 2793 2794 2795 2796 2797 2798 2799 2800 2801 2802 2803 2804 2805 2806 2807 2808 2809 2810 2811 2812 2813 2814 2815 2816 2817 2818 2819 2820 2821 2822 2823 2824 2825 2826 2827 2828 2829 2830 2831 2832 2833 2834 2835 2836 2837 2838 2839 2840 2841 2842 2843 2844 2845 2846 2847 2848 2849 2850 2851 2852 2853 2854 2855 2856 2857 2858 2859 2860 2861 2862 2863 2864 2865 2866 2867 2868 2869 2870 2871 2872 2873 2874 2875 2876 2877 2878 2879 2880 2881 2882 2883 2884 2885 2886 2887 2888 2889 2890 2891 2892 2893 2894 2895 2896 2897 2898 2899 2900 2901 2902 2903 2904 2905 2906 2907 2908 2909 2910 2911 2912 2913 2914 2915 2916 2917 2918 2919 2920 2921 2922 2923 2924 2925 2926 2927 2928 2929 2930 2931 2932 2933 2934 2935 2936 2937 2938 2939 2940 2941 2942 2943 2944 2945 2946 2947 2948 2949 2950 2951 2952 2953 2954 2955 2956 2957 2958 2959 2960 2961 2962 2963 2964 2965 2966 2967 2968 2969 2970 2971 2972 2973 2974 2975 2976 2977 2978 2979 2980 2981 2982 2983 2984 2985 2986 2987 2988 2989 2990 2991 2992 2993 2994 2995 2996 2997 2998 2999 3000 3001 3002 3003 3004 3005 3006 3007 3008 3009 3010 3011 3012 3013 3014 3015 3016 3017 3018 3019 3020 3021 3022 3023 3024 3025 3026 3027 3028 3029 3030 3031 3032 3033 3034 3035 3036 3037 3038 3039 3040 3041 3042 3043 3044 3045 3046 3047 3048 3049 3050 3051 3052 3053 3054 3055 3056 3057 3058 3059 3060 3061 3062 3063 3064 3065 3066 3067 3068 3069 3070 3071 3072 3073 3074 3075 3076 3077 3078 3079 3080 3081 3082 3083 3084 3085 3086 3087 3088 3089 3090 3091 3092 3093 3094 3095 3096 3097 3098 3099 3100 3101 3102 3103 3104 3105 3106 3107 3108 3109 3110 3111 3112 3113 3114 3115 3116 3117 3118 3119 3120 3121 3122 3123 3124 3125 3126 3127 3128 3129 3130 3131 3132 3133 3134 3135 3136 3137 3138 3139 3140 3141 3142 3143 3144 3145 3146 3147 3148 3149 3150 3151 3152 3153 3154 3155 3156 3157 3158 3159 3160 3161 3162 3163 3164 3165 3166 3167 3168 3169 3170 3171 3172 3173 3174 3175 3176 3177 3178 3179 3180 3181 3182 3183 3184 3185 3186 3187 3188 3189 3190 3191 3192 3193 3194 3195 3196 3197 3198 3199 3200 3201 3202 3203 3204 3205 3206 3207 3208 3209 3210 3211 3212 3213 3214 3215 3216 3217 3218 3219 3220 3221 3222 3223 3224 3225 3226 3227 3228 3229 3230 3231 3232 3233 3234 3235 3236 3237 3238 3239 3240 3241 3242 3243 3244 3245 3246 3247 3248 3249 3250 3251 3252 3253 3254 3255 3256 3257 3258 3259 3260 3261 3262 3263 3264 3265 3266 3267 3268 3269 3270 3271 3272 3273 3274 3275 3276 3277 3278 3279 3280 3281 3282 3283 3284 3285 3286 3287 3288 3289 3290 3291 3292 3293 3294 3295 3296 3297 3298 3299 3300 3301 3302 3303 3304 3305 3306 3307 3308 3309 3310 3311 3312 3313 3314 3315 3316 3317 3318 3319 3320 3321 3322 3323 3324 3325 3326 3327 3328 3329 3330 3331 3332 3333 3334 3335 3336 3337 3338 3339 3340 3341 3342 3343 3344 3345 3346 3347 3348 3349 3350 3351 3352 3353 3354 3355 3356 3357 3358 3359 3360 3361 3362 3363 3364 3365 3366 3367 3368 3369 3370 3371 3372 3373 3374 3375 3376 3377 3378 3379 3380 3381 3382 3383 3384 3385 3386 3387 3388 3389 3390 3391 3392 3393 3394 3395 3396 3397 3398 3399 3400 3401 3402 3403 3404 3405 3406 3407 3408 3409 3410 3411 3412 3413 3414 3415 3416 3417 3418 3419 3420 3421 3422 3423 3424 3425 3426 3427 3428 3429 3430 3431 3432 3433 3434 3435 3436 3437 3438 3439 3440 3441 3442 3443 3444 3445 3446 3447 3448 3449 3450 3451 3452 3453 3454 3455 3456 3457 3458 3459 3460 3461 3462 3463 3464 3465 3466 3467 3468 3469 3470 3471 3472 3473 3474 3475 3476 3477 3478 3479 3480 3481 3482 3483 3484 3485 3486 3487 3488 3489 3490 3491 3492 3493 3494 3495 3496 3497 3498 3499 3500 3501 3502 3503 3504 3505 3506 3507 3508 3509 3510 3511 3512 3513 3514 3515 3516 3517 3518 3519 3520 3521 3522 3523 3524 3525 3526 3527 3528 3529 3530 3531 3532 3533 3534 3535 3536 3537 3538 3539 3540 3541 3542 3543 3544 3545 3546 3547 3548 3549 3550 3551 3552 3553 3554 3555 3556 3557 3558 3559 3560 3561 3562 3563 3564 3565 3566 3567 3568 3569 3570 3571 3572 3573 3574 3575 3576 3577 3578 3579 3580 3581 3582 3583 3584 3585 3586 3587 3588 3589 3590 3591 3592 3593 3594 3595 3596 3597 3598 3599 3600 3601 3602 3603 3604 3605 3606 3607 3608 3609 3610 3611 3612 3613 3614 3615 3616 3617 3618 3619 3620 3621 3622 3623 3624 3625 3626 3627 3628 3629 3630 3631 3632 3633 3634 3635 3636 3637 3638 3639 3640 3641 3642 3643 3644 3645 3646 3647 3648 3649 3650 3651 3652 3653 3654 3655 3656 3657 3658 3659 3660 3661 3662 3663 3664 3665 3666 3667 3668 3669 3670 3671 3672 3673 3674 3675 3676 3677 3678 3679 3680 3681 3682 3683 3684 3685 3686 3687 3688 3689 3690 3691 3692 3693 3694 3695 3696 3697 3698 3699 3700 3701 3702 3703 3704 3705 3706 3707 3708 3709 3710 3711 3712 3713 3714 3715 3716 3717 3718 3719 3720 3721 3722 3723 3724 3725 3726 3727 3728 3729 3730 3731 3732 3733 3734 3735 3736 3737 3738 3739 3740 3741 3742 3743 3744 3745 3746 3747 3748 3749 3750 3751 3752 3753 3754 3755 3756 3757 3758 3759 3760 3761 3762 3763 3764 3765 3766 3767 3768 3769 3770 3771 3772 3773 3774 3775 3776 3777 3778 3779 3780 3781 3782 3783 3784 3785 3786 3787 3788 3789 3790 3791 3792 3793 3794 3795 3796 3797 3798 3799 3800 3801 3802 3803 3804 3805 3806 3807 3808 3809 3810 3811 3812 3813 3814 3815 3816 3817 3818 3819 3820 3821 3822 3823 3824 3825 3826 3827 3828 3829 3830 3831 3832 3833 3834 3835 3836 3837 3838 3839 3840 3841 3842 3843 3844 3845 3846 3847 3848 3849 3850 3851 3852 3853 3854 3855 3856 3857 3858 3859 3860 3861 3862 3863 3864 3865 3866 3867 3868 3869 3870 3871 3872 3873 3874 3875 3876 3877 3878 3879 3880 3881 3882 3883 3884 3885 3886 3887 3888 3889 3890 3891 3892 3893 3894 3895 3896 3897 3898 3899 3900 3901 3902 3903 3904 3905 3906 3907 3908 3909 3910 3911 3912 3913 3914 3915 3916 3917 3918 3919 3920 3921 3922 3923 3924 3925 3926 3927 3928 3929 3930 3931 3932 3933 3934 3935 3936 3937 3938 3939 3940 3941 3942 3943 3944 3945 3946 3947 3948 3949 3950 3951 3952 3953 3954 3955 3956 3957 3958 3959 3960 3961 3962 3963 3964 3965 3966 3967 3968 3969 3970 3971 3972 3973 3974 3975 3976 3977 3978 3979 3980 3981 3982 3983 3984 3985 3986 3987 3988 3989 3990 3991 3992 3993 3994 3995 3996 3997 3998 3999 4000 4001 4002 4003 4004 4005 4006 4007 4008 4009 4010 4011 4012 4013 4014 4015 4016 4017 4018 4019 4020 4021 4022 4023 4024 4025 4026 4027 4028 4029 4030 4031 4032 4033 4034 4035 4036 4037 4038 4039 4040 4041 4042 4043 4044 4045 4046 4047 4048 4049 4050 4051 4052 4053 4054 4055 4056 4057 4058 4059 4060 4061 4062 4063 4064 4065 4066 4067 4068 4069 4070 4071 4072 4073 4074 4075 4076 4077 4078 4079 4080 4081 4082 4083 4084 4085 4086 4087 4088 4089 4090 4091 4092 4093 4094 4095 4096 4097 4098 4099 4100 4101 4102 4103 4104 4105 4106 4107 4108 4109 4110 4111 4112 4113 4114 4115 4116 4117 4118 4119 4120 4121 4122 4123 4124 4125 4126 4127 4128 4129 4130 4131 4132 4133 4134 4135 4136 4137 4138 4139 4140 4141 4142 4143 4144 4145 4146 4147 4148 4149 4150 4151 4152 4153 4154 4155 4156 4157 4158 4159 4160 4161 4162 4163 4164 4165 4166 4167 4168 4169 4170 4171 4172 4173 4174 4175 4176 4177 4178 4179 4180 4181 4182 4183 4184 4185 4186 4187 4188 4189 4190 4191 4192 4193 4194 4195 4196 4197 4198 4199 4200 4201 4202 4203 4204 4205 4206 4207 4208 4209 4210 4211 4212 4213 4214 4215 4216 4217 4218 4219 4220 4221 4222 4223 4224 4225 4226 4227 4228 4229 4230 4231 4232 4233 4234 4235 4236 4237 4238 4239 4240 4241 4242 4243 4244 4245 4246 4247 4248 4249 4250 4251 4252 4253 4254 4255 4256 4257 4258 4259 4260 4261 4262 4263 4264 4265 4266 4267 4268 4269 4270 4271 4272 4273 4274 4275 4276 4277 4278 4279 4280 4281 4282 4283 4284 4285 4286 4287 4288 4289 4290 4291 4292 4293 4294 4295 4296 4297 4298 4299 4300 4301 4302 4303 4304 4305 4306 4307 4308 4309 4310 4311 4312 4313 4314 4315 4316 4317 4318 4319 4320 4321 4322 4323 4324 4325 4326 4327 4328 4329 4330 4331 4332 4333 4334 4335 4336 4337 4338 4339 4340 4341 4342 4343 4344 4345 4346 4347 4348 4349 4350 4351 4352 4353 4354 4355 4356 4357 4358 4359 4360 4361 4362 4363 4364 4365 4366 4367 4368 4369 4370 4371 4372 4373 4374 4375 4376 4377 4378 4379 4380 4381 4382 4383 4384 4385 4386 4387 4388 4389 4390 4391 4392 4393 4394 4395 4396 4397 4398 4399 4400 4401 4402 4403 4404 4405 4406 4407 4408 4409 4410 4411 4412 4413 4414 4415 4416 4417 4418 4419 4420 4421 4422 4423 4424 4425 4426 4427 4428 4429 4430 4431 4432 4433 4434 4435 4436 4437 4438 4439 4440 4441 4442 4443 4444 4445 4446 4447 4448 4449 4450 4451 4452 4453 4454 4455 4456 4457 4458 4459 4460 4461 4462 4463 4464 4465 4466 4467 4468 4469 4470 4471 4472 4473 4474 4475 4476 4477 4478 4479 4480 4481 4482 4483 4484 4485 4486 4487 4488 4489 4490 4491 4492 4493 4494 4495 4496 4497 4498 4499 4500 4501 4502 4503 4504 4505 4506 4507 4508 4509 4510 4511 4512 4513 4514 4515 4516 4517 4518 4519 4520 4521 4522 4523 4524 4525 4526 4527 4528 4529 4530 4531 4532 4533 4534 4535 4536 4537 4538 4539 4540 4541 4542 4543 4544 4545 4546 4547 4548 4549 4550 4551 4552 4553 4554 4555 4556 4557 4558 4559 4560 4561 4562 4563 4564 4565 4566 4567 4568 4569 4570 4571 4572 4573 4574 4575 4576 4577 4578 4579 4580 4581 4582 4583 4584 4585 4586 4587 4588 4589 4590 4591 4592 4593 4594 4595 4596 4597 4598 4599 4600 4601 4602 4603 4604 4605 4606 4607 4608 4609 4610 4611 4612 4613 4614 4615 4616 4617 4618 4619 4620 4621 4622 4623 4624 4625 4626 4627 4628 4629 4630 4631 4632 4633 4634 4635 4636 4637 4638 4639 4640 4641 4642 4643 4644 4645 4646 4647 4648 4649 4650 4651 4652 4653 4654 4655 4656 4657 4658 4659 4660 4661 4662 4663 4664 4665 4666 4667 4668 4669 4670 4671 4672 4673 4674 4675 4676 4677 4678 4679 4680 4681 4682 4683 4684 4685 4686 4687 4688 4689 4690 4691 4692 4693 4694 4695 4696 4697 4698 4699 4700 4701 4702 4703 4704 4705 4706 4707 4708 4709 4710 4711 4712 4713 4714 4715 4716 4717 4718 4719 4720 4721 4722 4723 4724 4725 4726 4727 4728 4729 4730 4731 4732 4733 4734 4735 4736 4737 4738 4739 4740 4741 4742 4743 4744 4745 4746 4747 4748 4749 4750 4751 4752 4753 4754 4755 4756 4757 4758 4759 4760 4761 4762 4763 4764 4765 4766 4767 4768 4769 4770 4771 4772 4773 4774 4775 4776 4777 4778 4779 4780 4781 4782 4783 4784 4785 4786 4787 4788 4789 4790 4791 4792 4793 4794 4795 4796 4797 4798 4799 4800 4801 4802 4803 4804 4805 4806 4807 4808 4809 4810 4811 4812 4813 4814 4815 4816 4817 4818 4819 4820 4821 4822 4823 4824 4825 4826 4827 4828 4829 4830 4831 4832 4833 4834 4835 4836 4837 4838 4839 4840 4841 4842 4843 4844 4845 4846 4847 4848 4849 4850 4851 4852 4853 4854 4855 4856 4857 4858 4859 4860 4861 4862 4863 4864 4865 4866 4867 4868 4869 4870 4871 4872 4873 4874 4875 4876 4877 4878 4879 4880 4881 4882 4883 4884 4885 4886 4887 4888 4889 4890 4891 4892 4893 4894 4895 4896 4897 4898 4899 4900 4901 4902 4903 4904 4905 4906 4907 4908 4909 4910 4911 4912 4913 4914 4915 4916 4917 4918 4919 4920 4921 4922 4923 4924 4925 4926 4927 4928 4929 4930 4931 4932 4933 4934 4935 4936 4937 4938 4939 4940 4941 4942 4943 4944 4945 4946 4947 4948 4949 4950 4951 4952 4953 4954 4955 4956 4957 4958 4959 4960 4961 4962 4963 4964 4965 4966 4967 4968 4969 4970 4971 4972 4973 4974 4975 4976 4977 4978 4979 4980 4981 4982 4983 4984 4985 4986 4987 4988 4989 4990 4991 4992 4993 4994 4995 4996 4997 4998 4999 5000 5001 5002 5003 5004 5005 5006 5007 5008 5009 5010 5011 5012 5013 5014 5015 5016 5017 5018 5019 5020 5021 5022 5023 5024 5025 5026 5027 5028 5029 5030 5031 5032 5033 5034 5035 5036 5037 5038 5039 5040 5041 5042 5043 5044 5045 5046 5047 5048 5049 5050 5051 5052 5053 5054 5055 5056 5057 5058 5059 5060 5061 5062 5063 5064 5065 5066 5067 5068 5069 5070 5071 5072 5073 5074 5075 5076 5077 5078 5079 5080 5081 5082 5083 5084 5085 5086 5087 5088 5089 5090 5091 5092 5093 5094 5095 5096 5097 5098 5099 5100 5101 5102 5103 5104 5105 5106 5107 5108 5109 5110 5111 5112 5113 5114 5115 5116 5117 5118 5119 5120 5121 5122 5123 5124 5125 5126 5127 5128 5129 5130 5131 5132 5133 5134 5135 5136 5137 5138 5139 5140 5141 5142 5143 5144 5145 5146 5147 5148 5149 5150 5151 5152 5153 5154 5155 5156 5157 5158 5159 5160 5161 5162 5163 5164 5165 5166 5167 5168 5169 5170 5171 5172 5173 5174 5175 5176 5177 5178 5179 5180 5181 5182 5183 5184 5185 5186 5187 5188 5189 5190 5191 5192 5193 5194 5195 5196 5197 5198 5199 5200 5201 5202 5203 5204 5205 5206 5207 5208 5209 5210 5211 5212 5213 5214 5215 5216 5217 5218 5219 5220 5221 5222 5223 5224 5225 5226 5227 5228 5229 5230 5231 5232 5233 5234 5235 5236 5237 5238 5239 5240 5241 5242 5243 5244 5245 5246 5247 5248 5249 5250 5251 5252 5253 5254 5255 5256 5257 5258 5259 5260 5261 5262 5263 5264 5265 5266 5267 5268 5269 5270 5271 5272 5273 5274 5275 5276 5277 5278 5279 5280 5281 5282 5283 5284 5285 5286 5287 5288 5289 5290 5291 5292 5293 5294 5295 5296 5297 5298 5299 5300 5301 5302 5303 5304 5305 5306 5307 5308 5309 5310 5311 5312 5313 5314 5315 5316 5317 5318 5319 5320 5321 5322 5323 5324 5325 5326 5327 5328 5329 5330 5331 5332 5333 5334 5335 5336 5337 5338 5339 5340 5341 5342 5343 5344 5345 5346 5347 5348 5349 5350 5351 5352 5353 5354 5355 5356 5357 5358 5359 5360 5361 5362 5363 5364 5365 5366 5367 5368 5369 5370 5371 5372 5373 5374 5375 5376 5377 5378 5379 5380 5381 5382 5383 5384 5385 5386 5387 5388 5389 5390 5391 5392 5393 5394 5395 5396 5397 5398 5399 5400 5401 5402 5403 5404 5405 5406 5407 5408 5409 5410 5411 5412 5413 5414 5415 5416 5417 5418 5419 5420 5421 5422 5423 5424 5425 5426 5427 5428 5429 5430 5431 5432 5433 5434 5435 5436 5437 5438 5439 5440 5441 5442 5443 5444 5445 5446 5447 5448 5449 5450 5451 5452 5453 5454 5455 5456 5457 5458 5459 5460 5461 5462 5463 5464 5465 5466 5467 5468 5469 5470 5471 5472 5473 5474 5475 5476 5477 5478 5479 5480 5481 5482 5483 5484 5485 5486 5487 5488 5489 5490 5491 5492 5493 5494 5495 5496 5497 5498 5499 5500 5501 5502 5503 5504 5505 5506 5507 5508 5509 5510 5511 5512 5513 5514 5515 5516 5517 5518 5519 5520 5521 5522 5523 5524 5525 5526 5527 5528 5529 5530 5531 5532 5533 5534 5535 5536 5537 5538 5539 5540 5541 5542 5543 5544 5545 5546 5547 5548 5549 5550 5551 5552 5553 5554 5555 5556 5557 5558 5559 5560 5561 5562 5563 5564 5565 5566 5567 5568 5569 5570 5571 5572 5573 5574 5575 5576 5577 5578 5579 5580 5581 5582 5583 5584 5585 5586 5587 5588 5589 5590 5591 5592 5593 5594 5595 5596 5597 5598 5599 5600 5601 5602 5603 5604 5605 5606 5607 5608 5609 5610 5611 5612 5613 5614 5615 5616 5617 5618 5619 5620 5621 5622 5623 5624 5625 5626 5627 5628 5629 5630 5631 5632 5633 5634 5635 5636 5637 5638 5639 5640 5641 5642 5643 5644 5645 5646 5647 5648 5649 5650 5651 5652 5653 5654 5655 5656 5657 5658 5659 5660 5661 5662 5663 5664 5665 5666 5667 5668 5669 5670 5671 5672 5673 5674 5675 5676 5677 5678 5679 5680 5681 5682 5683 5684 5685 5686 5687 5688 5689 5690 5691 5692 5693 5694 5695 5696 5697 5698 5699 5700 5701 5702 5703 5704 5705 5706 5707 5708 5709 5710 5711 5712 5713 5714 5715 5716 5717 5718 5719 5720 5721 5722 5723 5724 5725 5726 5727 5728 5729 5730 5731 5732 5733 5734 5735 5736 5737 5738 5739 5740 5741 5742 5743 5744 5745 5746 5747 5748 5749 5750 5751 5752 5753 5754 5755 5756 5757 5758 5759 5760 5761 5762 5763 5764 5765 5766 5767 5768 5769 5770 5771 5772 5773 5774 5775 5776 5777 5778 5779 5780 5781 5782 5783 5784 5785 5786 5787 5788 5789 5790 5791 5792 5793 5794 5795 5796 5797 5798 5799 5800 5801 5802 5803 5804 5805 5806 5807 5808 5809 5810 5811 5812 5813 5814 5815 5816 5817 5818 5819 5820 5821 5822 5823 5824 5825 5826 5827 5828 5829 5830 5831 5832 5833 5834 5835 5836 5837 5838 5839 5840 5841 5842 5843 5844 5845 5846 5847 5848 5849 5850 5851 5852 5853 5854 5855 5856 5857 5858 5859 5860 5861 5862 5863 5864 5865 5866 5867 5868 5869 5870 5871 5872 5873 5874 5875 5876 5877 5878 5879 5880 5881 5882 5883 5884 5885 5886 5887 5888 5889 5890 5891 5892 5893 5894 5895 5896 5897 5898 5899 5900 5901 5902 5903 5904 5905 5906 5907 5908 5909 5910 5911 5912 5913 5914 5915 5916 5917 5918 5919 5920 5921 5922 5923 5924 5925 5926 5927 5928 5929 5930 5931 5932 5933 5934 5935 5936 5937 5938 5939 5940 5941 5942 5943 5944 5945 5946 5947 5948 5949 5950 5951 5952 5953 5954 5955 5956 5957 5958 5959 5960 5961 5962 5963 5964 5965 5966 5967 5968 5969 5970 5971 5972 5973 5974 5975 5976 5977 5978 5979 5980 5981 5982 5983 5984 5985 5986 5987 5988 5989 5990 5991 5992 5993 5994 5995 5996 5997 5998 5999 6000 6001 6002 6003 6004 6005 6006 6007 6008 6009 6010 6011 6012 6013 6014 6015 6016 6017 6018 6019 6020 6021 6022 6023 6024 6025 6026 6027 6028 6029 6030 6031 6032 6033 6034 6035 6036 6037 6038 6039 6040 6041 6042 6043 6044 6045 6046 6047 6048 6049 6050 6051 6052 6053 6054 6055 6056 6057 6058 6059 6060 6061 6062 6063 6064 6065 6066 6067 6068 6069 6070 6071 6072 6073 6074 6075 6076 6077 6078 6079 6080 6081 6082 6083 6084 6085 6086 6087 6088 6089 6090 6091 6092 6093 6094 6095 6096 6097 6098 6099 6100 6101 6102 6103 6104 6105 6106 6107 6108 6109 6110 6111 6112 6113 6114 6115 6116 6117 6118 6119 6120 6121 6122 6123 6124 6125 6126 6127 6128 6129 6130 6131 6132 6133 6134 6135 6136 6137 6138 6139 6140 6141 6142 6143 6144 6145 6146 6147 6148 6149 6150 6151 6152 6153 6154 6155 6156 6157 6158 6159 6160 6161 6162 6163 6164 6165 6166 6167 6168 6169 6170 6171 6172 6173 6174 6175 6176 6177 6178 6179 6180 6181 6182 6183 6184 6185 6186 6187 6188 6189 6190 6191 6192 6193 6194 6195 6196 6197 6198 6199 6200 6201 6202 6203 6204 6205 6206 6207 6208 6209 6210 6211 6212 6213 6214 6215 6216 6217 6218 6219 6220 6221 6222 6223 6224 6225 6226 6227 6228 6229 6230 6231 6232 6233 6234 6235 6236 6237 6238 6239 6240 6241 6242 6243 6244 6245 6246 6247 6248 6249 6250 6251 6252 6253 6254 6255 6256 6257 6258 6259 6260 6261 6262 6263 6264 6265 6266 6267 6268 6269 6270 6271 6272 6273 6274 6275 6276 6277 6278 6279 6280 6281 6282 6283 6284 6285 6286 6287 6288 6289 6290 6291 6292 6293 6294 6295 6296 6297 6298 6299 6300 6301 6302 6303 6304 6305 6306 6307 6308 6309 6310 6311 6312 6313 6314 6315 6316 6317 6318 6319 6320 6321 6322 6323 6324 6325 6326 6327 6328 6329 6330 6331 6332 6333 6334 6335 6336 6337 6338 6339 6340 6341 6342 6343 6344 6345 6346 6347 6348 6349 6350 6351 6352 6353 6354 6355 6356 6357 6358 6359 6360 6361 6362 6363 6364 6365 6366 6367 6368 6369 6370 6371 6372 6373 6374 6375 6376 6377 6378 6379 6380 6381 6382 6383 6384 6385 6386 6387 6388 6389 6390 6391 6392 6393 6394 6395 6396 6397 6398 6399 6400 6401 6402 6403 6404 6405 6406 6407 6408 6409 6410 6411 6412 6413 6414 6415 6416 6417 6418 6419 6420 6421 6422 6423 6424 6425 6426 6427 6428 6429 6430 6431 6432 6433 6434 6435 6436 6437 6438 6439 6440 6441 6442 6443 6444 6445 6446 6447 6448 6449 6450 6451 6452 6453 6454 6455 6456 6457 6458 6459 6460 6461 6462 6463 6464 6465 6466 6467 6468 6469 6470 6471 6472 6473 6474 6475 6476 6477 6478 6479 6480 6481 6482 6483 6484 6485 6486 6487 6488 6489 6490 6491 6492 6493 6494 6495 6496 6497 6498 6499 6500 6501 6502 6503 6504 6505 6506 6507 6508 6509 6510 6511 6512 6513 6514 6515 6516 6517 6518 6519 6520 6521 6522 6523 6524 6525 6526 6527 6528 6529 6530 6531 6532 6533 6534 6535 6536 6537 6538 6539 6540 6541 6542 6543 6544 6545 6546 6547 6548 6549 6550 6551 6552 6553 6554 6555 6556 6557 6558 6559 6560 6561 6562 6563 6564 6565 6566 6567 6568 6569 6570 6571 6572 6573 6574 6575 6576 6577 6578 6579 6580 6581 6582 6583 6584 6585 6586 6587 6588 6589 6590 6591 6592 6593 6594 6595 6596 6597 6598 6599 6600 6601 6602 6603 6604 6605 6606 6607 6608 6609 6610 6611 6612 6613 6614 6615 6616 6617 6618 6619 6620 6621 6622 6623 6624 6625 6626 6627 6628 6629 6630 6631 6632 6633 6634 6635 6636 6637 6638 6639 6640 6641 6642 6643 6644 6645 6646 6647 6648 6649 6650 6651 6652 6653 6654 6655 6656 6657 6658 6659 6660 6661 6662 6663 6664 6665 6666 6667 6668 6669 6670 6671 6672 6673 6674 6675 6676 6677 6678 6679 6680 6681 6682 6683 6684 6685 6686 6687 6688 6689 6690 6691 6692 6693 6694 6695 6696 6697 6698 6699 6700 6701 6702 6703 6704 6705 6706 6707 6708 6709 6710 6711 6712 6713 6714 6715 6716 6717 6718 6719 6720 6721 6722 6723 6724 6725 6726 6727 6728 6729 6730 6731 6732 6733 6734 6735 6736 6737 6738 6739 6740 6741 6742 6743 6744 6745 6746 6747 6748 6749 6750 6751 6752 6753 6754 6755 6756 6757 6758 6759 6760 6761 6762 6763 6764 6765 6766 6767 6768 6769 6770 6771 6772 6773 6774 6775 6776 6777 6778 6779 6780 6781 6782 6783 6784 6785 6786 6787 6788 6789 6790 6791 6792 6793 6794 6795 6796 6797 6798 6799 6800 6801 6802 6803 6804 6805 6806 6807 6808 6809 6810 6811 6812 6813 6814 6815 6816 6817 6818 6819 6820 6821 6822 6823 6824 6825 6826 6827 6828 6829 6830 6831 6832 6833 6834 6835 6836 6837 6838 6839 6840 6841 6842 6843 6844 6845 6846 6847 6848 6849 6850 6851 6852 6853 6854 6855 6856 6857 6858 6859 6860 6861 6862 6863 6864 6865 6866 6867 6868 6869 6870 6871 6872 6873 6874 6875 6876 6877 6878 6879 6880 6881 6882 6883 6884 6885 6886 6887 6888 6889 6890 6891 6892 6893 6894 6895 6896 6897 6898 6899 6900 6901 6902 6903 6904 6905 6906 6907 6908 6909 6910 6911 6912 6913 6914 6915 6916 6917 6918 6919 6920 6921 6922 6923 6924 6925 6926 6927 6928 6929 6930 6931 6932 6933 6934 6935 6936 6937 6938 6939 6940 6941 6942 6943 6944 6945 6946 6947 6948 6949 6950 6951 6952 6953 6954 6955 6956 6957 6958 6959 6960 6961 6962 6963 6964 6965 6966 6967 6968 6969 6970 6971 6972 6973 6974 6975 6976 6977 6978 6979 6980 6981 6982 6983 6984 6985 6986 6987 6988 6989 6990 6991 6992 6993 6994 6995 6996 6997 6998 6999 7000 7001 7002 7003 7004 7005 7006 7007 7008 7009 7010 7011 7012 7013 7014 7015 7016 7017 7018 7019 7020 7021 7022 7023 7024 7025 7026 7027 7028 7029 7030 7031 7032 7033 7034 7035 7036 7037 7038 7039 7040 7041 7042 7043 7044 7045 7046 7047 7048 7049 7050 7051 7052 7053 7054 7055 7056 7057 7058 7059 7060 7061 7062 7063 7064 7065 7066 7067 7068 7069 7070 7071 7072 7073 7074 7075 7076 7077 7078 7079 7080 7081 7082 7083 7084 7085 7086 7087 7088 7089 7090 7091 7092 7093 7094 7095 7096 7097 7098 7099 7100 7101 7102 7103 7104 7105 7106 7107 7108 7109 7110 7111 7112 7113 7114 7115 7116 7117 7118 7119 7120 7121 7122 7123 7124 7125 7126 7127 7128 7129 7130 7131 7132 7133 7134 7135 7136 7137 7138 7139 7140 7141 7142 7143 7144 7145 7146 7147 7148 7149 7150 7151 7152 7153 7154 7155 7156 7157 7158 7159 7160 7161 7162 7163 7164 7165 7166 7167 7168 7169 7170 7171 7172 7173 7174 7175 7176 7177 7178 7179 7180 7181 7182 7183 7184 7185 7186 7187 7188 7189 7190 7191 7192 7193 7194 7195 7196 7197 7198 7199 7200 7201 7202 7203 7204 7205 7206 7207 7208 7209 7210 7211 7212 7213 7214 7215 7216 7217 7218 7219 7220 7221 7222 7223 7224 7225 7226 7227 7228 7229 7230 7231 7232 7233 7234 7235 7236 7237 7238 7239 7240 7241 7242 7243 7244 7245 7246 7247 7248 7249 7250 7251 7252 7253 7254 7255 7256 7257 7258 7259 7260 7261 7262 7263 7264 7265 7266 7267 7268 7269 7270 7271 7272 7273 7274 7275 7276 7277 7278 7279 7280 7281 7282 7283 7284 7285
# 反应式堆栈上的 Web

文档的这一部分涵盖了对构建在[反应流](https://www.reactive-streams.org/)API 上的反应式堆栈 Web 应用程序的支持,该应用程序可在非阻塞服务器上运行,例如 Netty、 Undertow 和 Servlet 3.1+ 容器。个别章节涵盖了[Spring WebFlux](webflux.html#webflux)框架、反应性[`WebClient`](#WebFlux-client)、对[testing](#webflux-test)[反应库](#webflux-reactive-libraries)的支持。对于 Servlet-stack Web 应用程序,请参见[Web on Servlet Stack](web.html#spring-web)

## 1. Spring WebFlux

Spring 框架中包含的原始 Web 框架 Spring Web MVC 是专门为 Servlet API 和 Servlet 容器构建的。反应式堆栈 Web 框架 Spring WebFlux 是后来在 5.0 版本中添加的。它是完全非阻塞的,支持[反应流](https://www.reactive-streams.org/)背压,并在 Netty、 Undertow 和 Servlet 3.1+ 容器等服务器上运行。

这两个 Web 框架都反映了它们的源模块的名称([spring-webmvc](https://github.com/spring-projects/spring-framework/tree/main/spring-webmvc)[spring-webflux](https://github.com/spring-projects/spring-framework/tree/main/spring-webflux)),并在 Spring 框架中并存。每个模块都是可选的。应用程序可以使用一个或另一个模块,或者在某些情况下,同时使用这两个模块—例如,具有 Spring MVC 控制器的反应性。

### 1.1.概述

为什么要创建 WebFlux?

部分解决方案是需要一个非阻塞的 Web 堆栈来处理少量线程的并发性,并以更少的硬件资源进行扩展。 Servlet 3.1 确实为非阻塞 I/O 提供了一个 API。但是,使用它会导致远离 Servlet API 的其余部分,其中契约是同步的(`Filter``Servlet`)或阻塞的(`getParameter``getPart`)。这就是一个新的通用 API 的动机,它可以作为跨任何非阻塞运行时的基础。这一点很重要,因为服务器(如 Netty)在异步、非阻塞空间中已经很好地建立了。

答案的另一部分是函数式编程。正如 爪哇5 中添加的注释创造了机会(例如注释的 REST 控制器或单元测试)一样,爪哇8 中添加的 lambda 表达式为 爪哇 中的功能 API 创造了机会。这对于允许异步逻辑的声明式组合的非阻塞应用程序和延续风格 API(由`CompletableFuture`[ReactiveX](http://reactivex.io/)推广)是一个福音。在编程模型级别,爪哇8 使 Spring WebFlux 能够在带注释的控制器之外提供功能性的 Web 端点。

#### 1.1.1.定义“反应性”

我们谈到了“非阻塞”和“功能性”,但是反应性是什么意思呢?

术语“反应性”指的是围绕对变化做出反应而构建的编程模型——网络组件对 I/O 事件做出反应,UI 控制器对鼠标事件做出反应,等等。从这个意义上说,非阻塞是反应性的,因为我们现在不是被阻塞,而是在操作完成或数据可用时对通知做出反应。

我们团队中还有另一个与“反应性”相关的重要机制,那就是无阻塞背压。在同步的命令式代码中,阻塞调用作为一种自然的反压形式,迫使调用者等待。在非阻塞代码中,控制事件的速率变得很重要,这样快速生成器就不会淹没其目标。

反应流是一个[small spec](https://github.com/reactive-streams/reactive-streams-jvm/blob/master/README.md#specification)(在 爪哇9 中也是[adopted](https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/Flow.html)),它定义了具有背压的异步组件之间的交互。例如,数据存储库(充当[Publisher](https://www.reactive-streams.org/reactive-streams-1.0.1-javadoc/org/reactivestreams/Publisher.html))可以生成 HTTP 服务器(充当[Subscriber](https://www.reactive-streams.org/reactive-streams-1.0.1-javadoc/org/reactivestreams/Subscriber.html))随后可以写入响应的数据。反应流的主要目的是让订阅者控制发布者生成数据的速度或速度。

|   |**常见问题:如果出版商不能放慢速度怎么办?**<br/>反应流的目的只是为了建立机制和边界。<br/>如果一个发布者不能减速,它就必须决定是缓冲、下降还是失败。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

#### 1.1.2.反应性 API

反应流在互操作性中起着重要的作用。它是库和基础设施组件感兴趣的,但作为应用程序 API 用处不大,因为它的级别太低。应用程序需要一个更高层次和更丰富的功能 API 来组成异步逻辑——类似于 爪哇8`Stream`API,但不仅仅是用于集合。这就是反应库所扮演的角色。

[Reactor](https://github.com/reactor/reactor)是 Spring WebFlux 选择的反应库。它提供了[`Mono`](https://projectreactor.io/DOCS/core/release/api/reactor/core/publisher/mono.html)和[`Flux`](https://projectreactor.io/core/core/release/api/reactor/reactor/publisher.html)API 类型,以便通过与 reactivex=“388”/>对齐的一组丰富的运算符对 0..1(`Mono`)和 0.n(<gt r=“386”)的数据序列进行工作。反应器是一个反应库,因此,它的所有操作人员都支持无阻塞背压。Reactor 非常关注服务器端 爪哇。它是与 Spring 密切合作开发的。

WebFlux 需要将 Reactor 作为核心依赖项,但它可以通过反应流与其他反应库进行互操作。作为一般规则,WebFlux API 接受普通的`Publisher`作为输入,在内部将其调整为反应器类型,并使用该类型,并返回`Flux``Mono`作为输出。因此,你可以将任何`Publisher`作为输入传递,并且可以在输出上应用操作,但是你需要调整输出以与另一个反应库一起使用。只要可行(例如,带注释的控制器),WebFlux 就会透明地适应 RX爪哇 或其他反应库的使用。有关更多详细信息,请参见[反应库](#webflux-reactive-libraries)

|   |除了反应性 API,WebFlux 还可以与[Coroutines](languages.html#coroutines) Kotlin 中的 API 一起使用,这提供了一种更必要的编程风格。<br/>下面的 Kotlin 代码示例将与协程 API 一起提供。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

#### 1.1.3.程序设计模型

`spring-web`模块包含支撑 Spring WebFlux 的反应性基础,包括 HTTP 抽象、支持服务器的反应性流[adapters](#webflux-httphandler)[codecs](#webflux-codecs),以及与 Servlet API 类似但具有非阻塞契约的核心[`WebHandler`API](#WebFlux-web-handler-API)。

在此基础上, Spring WebFlux 提供了两种编程模型的选择:

* [带注释的控制器](#webflux-controller):与 Spring MVC 一致,并且基于来自`spring-web`模块的相同注释。 Spring MVC 和 WebFlux 控制器都支持反应(反应器和 RX爪哇)返回类型,因此,很难将它们区分开来。一个值得注意的区别是,WebFlux 还支持活性的`@RequestBody`参数。

* [功能端点](#webflux-fn):基于 lambda 的、轻量级的和函数式的编程模型。你可以将其视为一个小型的库或一组实用程序,应用程序可以使用它们来路由和处理请求。与注解控制器的最大区别在于,应用程序负责从头到尾处理请求,而不是通过注解声明意图并被回调。

#### 1.1.4.适用性

Spring MVC 还是 WebFlux?

这是个自然的问题,但会造成一种不合理的二分法。实际上,两者共同作用来扩大可供选择的范围。这两种设计是为了彼此之间的连续性和一致性,它们可以并排使用,并且来自每一方的反馈对双方都有利。下面的图表显示了这两者之间的关系,它们的共同点,以及各自的独特支持:

![spring mvc and webflux venn](images/spring-mvc-and-webflux-venn.png)

我们建议你考虑以下几点:

* 如果你的 Spring MVC 应用程序运行良好,则无需更改。命令式编程是编写、理解和调试代码的最简单的方法。你有最多的库可供选择,因为从历史上看,大多数库都是阻塞的。

* 如果你已经在寻找一个非阻塞的 Web 堆栈, Spring WebFlux 提供了与该空间中的其他程序相同的执行模型的优点,并且还提供了服务器(Netty、 Tomcat、 Jetty、 Undertow 和 Servlet 3.1+ 容器)的选择,以及编程模型的选择(带注释的控制器和功能的 Web 端点),以及反应库的选择(反应器、RX爪哇 或其他)。

* 如果你对与 爪哇8Lambdas 或 Kotlin 一起使用的轻量级、功能性 Web 框架感兴趣,那么可以使用 Spring WebFlux Functional Web Endpoints。对于较小的应用程序或需求不那么复杂的微服务来说,这也是一个很好的选择,它们可以受益于更高的透明度和控制。

* 在微服务架构中,你可以混合使用具有 Spring MVC 或 Spring WebFlux 控制器的应用程序,或者具有 Spring WebFlux 功能端点的应用程序。在两个框架中都支持相同的基于注释的编程模型,这使得在为正确的工作选择正确的工具的同时更容易重用知识。

* 评估应用程序的一种简单方法是检查其依赖关系。如果你有阻塞持久性 API( JPA、JDBC)或网络 API 可供使用, Spring MVC 至少是通用架构的最佳选择。对于 Reactor 和 Rx爪哇 来说,在单独的线程上执行阻塞调用在技术上是可行的,但是你不会充分利用非阻塞的 Web 堆栈。

* 如果你有一个 Spring MVC 应用程序,其中调用了远程服务,请尝试 reactive`WebClient`。你可以直接从 Spring MVC 控制器方法返回反应类型(reactor,rxjava,[or other](#webflux-reactive-libraries))。每次调用的延迟越大或调用之间的相互依赖性越大,其好处就越显著。 Spring MVC 控制器也可以调用其他无功分量。

* 如果你有一个庞大的团队,请记住,在向非阻塞、函数式和声明式编程的转变中,学习曲线很陡。在没有全开关的情况下,一种实用的启动方式是使用反应式`WebClient`。除此之外,从小处着手,衡量收益。我们预计,对于广泛的应用而言,这种转变是不必要的。如果你不确定要寻找哪些好处,那么可以从了解非阻塞 I/O 的工作方式(例如,在单线程 node.js 上的并发性)及其效果开始。

#### 1.1.5.服务器

Spring WebFlux 在 Tomcat、 Jetty、 Servlet 3.1+ 容器上以及在诸如 Netty 和 Undertow 等非 Servlet 运行时上得到支持。所有服务器都适应于低级别的[common API](#webflux-httphandler),以便可以跨服务器支持更高级别的[程序设计模型](#webflux-programming-models)

Spring WebFlux 不具有启动或停止服务器的内置支持。然而,很容易从 Spring 配置和[WebFlux 基础设施](#webflux-config)[run it](#webflux-httphandler)的应用程序中使用几行代码。

Spring Boot 有一个 WebFlux 启动器,可以自动执行这些步骤。默认情况下,启动器使用 Netty,但通过更改 Maven 或 Gradle 依赖关系,很容易切换到 Tomcat、 Jetty 或 Undertow。 Spring 启动默认为 netty,因为它在异步、非阻塞空间中被更广泛地使用,并且允许客户机和服务器共享资源。

Tomcat 和 Jetty 可以与 Spring MVC 和 WebFlux 一起使用。然而,请记住,它们的使用方式是非常不同的。 Spring MVC 依赖于 Servlet 阻塞 I/O,并允许应用程序在需要时直接使用 Servlet API。 Spring WebFlux 依赖于 Servlet 3.1 非阻塞 I/O,并使用 Servlet 底层适配器后面的 API。它不会直接暴露在外使用。

对于 Undertow, Spring WebFlux 直接使用 Undertow API 而不使用 Servlet API。

#### 1.1.6.表现

表演有许多特点和意义。反应性和非阻塞通常不会使应用程序运行得更快。在某些情况下,它们可以(例如,如果使用`WebClient`并行运行远程调用)。总的来说,它需要更多的工作来做事情的非阻塞的方式,这可以稍微增加所需的处理时间。

反应性和非阻塞的主要预期好处是能够以较小的、固定的线程数量和较少的内存进行扩展。这使得应用程序在负载下更具弹性,因为它们以更可预测的方式扩展。然而,为了观察这些好处,你需要有一些延迟(包括缓慢和不可预测的网络 I/O 的混合)。这就是反应性堆栈开始显示其优势的地方,差异可能是巨大的。

#### 1.1.7.并发模型

Spring MVC 和 Spring WebFlux 都支持带注释的控制器,但在并发模型和用于阻塞和线程的默认假设中存在关键差异。

在 Spring MVC(和 Servlet 一般的应用程序)中,假定应用程序可以阻止当前线程,(例如,用于远程调用)。出于这个原因, Servlet 容器使用一个大的线程池来吸收请求处理过程中的潜在阻塞。

Spring 在 WebFlux(以及一般的非阻塞服务器)中,假定应用程序不会阻塞。因此,非阻塞服务器使用一个小的、固定大小的线程池(事件循环工作者)来处理请求。

|   |“可伸缩”和“少量线程”听起来可能是矛盾的,但永远不要阻塞<br/>当前线程(而是依赖回调)意味着你不需要额外的线程,因为<br/>没有要吸收的阻塞调用。|
|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

调用阻塞 API

如果你确实需要使用阻塞库,该怎么办?Reactor 和 Rx爪哇 都提供`publishOn`操作符,以便在不同的线程上继续处理。这意味着有一个很容易逃脱的舱口。然而,请记住,阻塞 API 并不适合这种并发模型。

可变状态

在 Reactor 和 RX爪哇 中,你通过运算符声明逻辑。在运行时,会形成一个反应性管道,在该管道中,数据会在不同的阶段中按顺序进行处理。这样做的一个主要好处是,它使应用程序不必保护可变状态,因为该管道中的应用程序代码永远不会并发调用。

线程模型

在运行 Spring WebFlux 的服务器上,你应该看到哪些线程?

* 在“vanilla” Spring WebFlux 服务器上(例如,没有数据访问或其他可选的依赖关系),你可以期望为服务器提供一个线程,为请求处理提供几个线程(通常与 CPU 内核的数量一样多)。 Servlet 然而,容器可以以更多线程(例如, Tomcat 上的 10)开始,以支持 Servlet(阻塞)I/O 和 Servlet 3.1(非阻塞)I/O 的使用。

* 反应式`WebClient`以事件循环方式进行操作。因此,你可以看到与此相关的处理线程的数量很少且是固定的(例如,`reactor-http-nio-`与 Reactor Netty 连接器)。但是,如果 Reactor Netty 同时用于客户机和服务器,则默认情况下这两个服务器共享事件循环资源。

* Reactor 和 Rx爪哇 提供线程池抽象(称为调度器),与`publishOn`操作符一起使用,该操作符用于将处理切换到不同的线程池。调度程序的名称建议了一种特定的并发策略——例如,“并行”(用于线程数量有限的 CPU 绑定工作)或“弹性”(用于具有大量线程的 I/O 绑定工作)。如果你看到这样的线程,这意味着某些代码正在使用特定的线程池`Scheduler`策略。

* 数据访问库和其他第三方依赖项也可以创建和使用自己的线程。

配置

Spring 框架不提供对启动和停止[servers](#webflux-server-choice)的支持。要为服务器配置线程模型,你需要使用特定于服务器的配置 API,或者,如果你使用 Spring 引导,请检查每个服务器的 Spring 引导配置选项。你可以直接[configure](#webflux-client-builder)`WebClient`。对于所有其他库,请参阅它们各自的文档。

### 1.2.反应核

`spring-web`模块包含对反应式 Web 应用程序的以下基本支持:

* 对于服务器请求处理,有两个级别的支持。

  * [Httphandler](#webflux-httphandler):使用非阻塞 I/O 和反应流反压处理 HTTP 请求的基本契约,以及用于反应堆网络、 Undertow、 Tomcat、 Jetty 和任何 Servlet 3.1+ 容器的适配器。

  * [`WebHandler`API](#WebFlux-Web-Handler-API):略高级别的、用于请求处理的通用 Web API,在此基础上构建了具体的编程模型,如带注释的控制器和功能端点。

* 对于客户端,有一个基本的`ClientHttpConnector`契约来执行具有非阻塞 I/O 和反应性流反压的 HTTP 请求,以及用于[反应堆网状结构](https://github.com/reactor/reactor-netty)、反应性[Jetty HttpClient](https://github.com/jetty-project/jetty-reactive-httpclient)[Apache HttpComponents](https://hc.apache.org/)的适配器。应用程序中使用的较高级别[WebClient](#webflux-client)建立在此基本契约上。

* 对于客户机和服务器,[codecs](#webflux-codecs)用于序列化和反序列化 HTTP 请求和响应内容。

#### 1.2.1.`HttpHandler`

[Httphandler](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/http/server/reactive/HttpHandler.html)是一个简单的契约,它只有一个方法来处理请求和响应。它是故意最小化的,它的主要目的也是唯一的目的是在不同的 HTTP 服务器 API 上进行最小化抽象。

下表描述了受支持的服务器 API:

|     Server name     |使用的服务器 API|                     Reactive Streams support                      |
|---------------------|--------------------------------------------------------------------------------|-------------------------------------------------------------------|
|        Netty        |Netty API|     [Reactor Netty](https://github.com/reactor/reactor-netty)     |
|      Undertow       |Undertow 空气污染指数|          spring-web: Undertow to Reactive Streams bridge          |
|       Tomcat        |Servlet 3.1 非阻塞 I/O; Tomcat 读写字节缓冲器 VS 字节的 API[]|spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge|
|        Jetty        |Servlet 3.1 非阻塞 I/O; Jetty 写字节缓冲器 VS 字节的 API[]|spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge|
|Servlet 3.1 container|Servlet 3.1 非阻塞 I/O|spring-web: Servlet 3.1 non-blocking I/O to Reactive Streams bridge|

下表描述了服务器的依赖关系(另请参见[支持的版本](https://github.com/spring-projects/spring-framework/wiki/What%27s-New-in-the-Spring-Framework)):

| Server name |       Group id        |工件名称|
|-------------|-----------------------|---------------------------|
|Reactor Netty|io.projectreactor.netty|反应堆网状结构|
|  Undertow   |      io.undertow      |Undertow-核心|
|   Tomcat    |org.apache.tomcat.embed|Tomcat-嵌入-核心|
|    Jetty    |   org.eclipse.jetty   |Jetty-服务器, Jetty- Servlet|

下面的代码片段显示了在每个服务器 API 中使用`HttpHandler`适配器的情况:

**反应堆网状结构**

爪哇

```
HttpHandler handler = ...
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(handler);
HttpServer.create().host(host).port(port).handle(adapter).bind().block();
```

Kotlin

```
val handler: HttpHandler = ...
val adapter = ReactorHttpHandlerAdapter(handler)
HttpServer.create().host(host).port(port).handle(adapter).bind().block()
```

**Undertow**

爪哇

```
HttpHandler handler = ...
UndertowHttpHandlerAdapter adapter = new UndertowHttpHandlerAdapter(handler);
Undertow server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build();
server.start();
```

Kotlin

```
val handler: HttpHandler = ...
val adapter = UndertowHttpHandlerAdapter(handler)
val server = Undertow.builder().addHttpListener(port, host).setHandler(adapter).build()
server.start()
```

**Tomcat**

爪哇

```
HttpHandler handler = ...
Servlet servlet = new TomcatHttpHandlerAdapter(handler);

Tomcat server = new Tomcat();
File base = new File(System.getProperty("java.io.tmpdir"));
Context rootContext = server.addContext("", base.getAbsolutePath());
Tomcat.addServlet(rootContext, "main", servlet);
rootContext.addServletMappingDecoded("/", "main");
server.setHost(host);
server.setPort(port);
server.start();
```

Kotlin

```
val handler: HttpHandler = ...
val servlet = TomcatHttpHandlerAdapter(handler)

val server = Tomcat()
val base = File(System.getProperty("java.io.tmpdir"))
val rootContext = server.addContext("", base.absolutePath)
Tomcat.addServlet(rootContext, "main", servlet)
rootContext.addServletMappingDecoded("/", "main")
server.host = host
server.setPort(port)
server.start()
```

**Jetty**

爪哇

```
HttpHandler handler = ...
Servlet servlet = new JettyHttpHandlerAdapter(handler);

Server server = new Server();
ServletContextHandler contextHandler = new ServletContextHandler(server, "");
contextHandler.addServlet(new ServletHolder(servlet), "/");
contextHandler.start();

ServerConnector connector = new ServerConnector(server);
connector.setHost(host);
connector.setPort(port);
server.addConnector(connector);
server.start();
```

Kotlin

```
val handler: HttpHandler = ...
val servlet = JettyHttpHandlerAdapter(handler)

val server = Server()
val contextHandler = ServletContextHandler(server, "")
contextHandler.addServlet(ServletHolder(servlet), "/")
contextHandler.start();

val connector = ServerConnector(server)
connector.host = host
connector.port = port
server.addConnector(connector)
server.start()
```

**Servlet 3.1+ Container**

要将 WAR 部署到任何 Servlet 3.1+ 容器上,你可以在 WAR 中扩展并包括[`AbstractReactiveWebInitializer`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/server/adapter/abstractreactivewebinitializer.html)。该类将`HttpHandler``ServletHttpHandlerAdapter`包装在一起,并将其注册为`Servlet`

#### 1.2.2.`WebHandler`api

`org.springframework.web.server`包构建在[`HttpHandler`](#WebFlux-Httphandler)合同的基础上,通过多个[`WebExceptionHandler`](https://DOCS. Spring.io/ Spring-Framework/DOCS/5.3.16/javadoc-api/org/org/org/SpringFramework/Web/Server/WebExceptionHandler.html)的链提供一个通用的 Web API,用于处理请求(https:///gt://gt/DOCS. Spring.3.3.org/16.org/16.org/javloverFramework.org/jumerfilter 通过简单地指向一个 Spring `ApplicationContext`,其中组件是[自动检测](#webflux-web-handler-api-special-beans),和/或通过向构建器注册组件,可以将链与`WebHttpHandlerBuilder`放在一起。

虽然`HttpHandler`的一个简单目标是抽象不同 HTTP 服务器的使用,但`WebHandler`API 的目标是提供 Web 应用程序中常用的一组更广泛的功能,例如:

* 具有属性的用户会话。

* 请求属性。

* 已为请求解析`Locale``Principal`

* 访问解析和缓存的表单数据。

* 多部分数据的抽象。

* 还有更多..

##### 特殊 Bean 类型

下表列出了`WebHttpHandlerBuilder`可以在 Spring ApplicationContext 中自动检测的组件,或者可以直接向其注册的组件:

|         Bean name          |         Bean type          |Count|说明|
|----------------------------|----------------------------|-----|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|          \<any\>           |   `WebExceptionHandler`    |0..N |为来自`WebFilter`实例和目标`WebHandler`的链中的异常提供处理。有关更多详细信息,请参见[Exceptions](#webflux-exception-handler)。|
|          \<any\>           |        `WebFilter`         |0..N |将截取样式逻辑应用于过滤器链的其余部分之前和之后,以及<br/>目标`WebHandler`。有关更多详细信息,请参见[Filters](#webflux-filters)。|
|        `webHandler`        |        `WebHandler`        |  1  |请求的处理程序。|
|    `webSessionManager`     |    `WebSessionManager`     |0..1 |默认情况下,`WebSession`实例的管理器通过`ServerWebExchange`上的方法公开。`DefaultWebSessionManager`实例。|
|  `serverCodecConfigurer`   |  `ServerCodecConfigurer`   |0..1 |用于访问`HttpMessageReader`实例以解析表单数据和多部分数据,然后<br/>通过`ServerWebExchange`上的方法公开。默认情况下`ServerCodecConfigurer.create()`。|
|  `localeContextResolver`   |  `LocaleContextResolver`   |0..1 |默认情况下,`LocaleContext`的解析器通过`ServerWebExchange`上的方法公开。`AcceptHeaderLocaleContextResolver`。|
|`forwardedHeaderTransformer`|`ForwardedHeaderTransformer`|0..1 |对于处理转发的类型头,可以通过提取和删除它们,也可以只删除它们。<br/>默认情况下不使用。|

##### 表单数据

`ServerWebExchange`公开了以下访问表单数据的方法:

爪哇

```
Mono<MultiValueMap<String, String>> getFormData();
```

Kotlin

```
suspend fun getFormData(): MultiValueMap<String, String>
```

`DefaultServerWebExchange`使用配置的`HttpMessageReader`将表单数据(`application/x-www-form-urlencoded`)解析为`MultiValueMap`。默认情况下,`FormHttpMessageReader`被配置为由`ServerCodecConfigurer` Bean 使用(请参见[Web 处理程序 API](#webflux-web-handler-api))。

##### 多部分数据

[Web MVC](web.html#mvc-multipart)

`ServerWebExchange`公开了以下访问多部分数据的方法:

爪哇

```
Mono<MultiValueMap<String, Part>> getMultipartData();
```

Kotlin

```
suspend fun getMultipartData(): MultiValueMap<String, Part>
```

`DefaultServerWebExchange`使用配置的`HttpMessageReader<MultiValueMap<String, Part>>``multipart/form-data`内容解析为`MultiValueMap`。默认情况下,这是`DefaultPartHttpMessageReader`,它没有任何第三方依赖关系。或者,可以使用`SynchronossPartHttpMessageReader`,这是基于[Synchronoss 多部件蔚来](https://github.com/synchronoss/nio-multipart)库的。这两个参数都是通过`ServerCodecConfigurer` Bean 配置的(参见[Web 处理程序 API](#webflux-web-handler-api))。

要以流式方式解析多部分数据,可以使用从`HttpMessageReader<Part>`返回的`Flux<Part>`代替。例如,在带注释的控制器中,使用`@RequestPart`意味着通过名称对各个部分进行类似`Map`的访问,因此需要完整地解析多部分数据。相比之下,你可以使用`@RequestBody`将内容解码为`Flux<Part>`,而无需收集到`MultiValueMap`

##### 转发头

[Web MVC](web.html#filters-forwarded-headers)

当请求通过代理(例如负载均衡器)时,主机、端口和方案可能会发生变化。从客户机的角度来看,这使得创建指向正确的主机、端口和方案的链接成为一项挑战。

[RFC 7239](https://tools.ietf.org/html/rfc7239)定义了`Forwarded`HTTP 报头,代理可以使用该报头来提供有关原始请求的信息。也有其他非标准标题,包括`X-Forwarded-Host``X-Forwarded-Port``X-Forwarded-Proto``X-Forwarded-Ssl``X-Forwarded-Prefix`

`ForwardedHeaderTransformer`是一个组件,它基于转发的标头修改请求的主机、端口和方案,然后删除这些标头。如果将其声明为 Bean,并使用名称`forwardedHeaderTransformer`,则将其声明为[detected](#webflux-web-handler-api-special-beans)

转发头的安全性需要考虑,因为应用程序不能知道头是由代理添加的,还是由恶意客户机添加的。这就是为什么在信任边界上的代理应该被配置为删除来自外部的不受信任的转发流量。你还可以将`ForwardedHeaderTransformer`配置为`removeOnly=true`,在这种情况下,它会删除但不使用头。

|   |在 5.1 中,`ForwardedHeaderFilter`被弃用,并被`ForwardedHeaderTransformer`取代,因此在创建<br/>交换之前,可以更早地处理转发头。如果无论如何都配置了过滤器,则将其从<br/>过滤器列表中取出,并使用`ForwardedHeaderTransformer`代替。|
|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

#### 1.2.3.过滤器

[Web MVC](web.html#filters)

[`WebHandler`API](#WebFlux-Web-Handler-API)中,可以使用`WebFilter`在过滤器和目标`WebHandler`的处理链的其余部分之前和之后应用拦截风格的逻辑。当使用[WebFlux 配置](#webflux-config)时,注册`WebFilter`就像将其声明为 Spring  Bean 一样简单,并且(可选地)通过在 Bean 声明上使用`@Order`或通过实现`Ordered`来表示优先级。

##### CORS

[Web MVC](web.html#filters-cors)

Spring WebFlux 通过控制器上的注释为 CORS 配置提供了细粒度的支持。然而,当你将其与 Spring 安全性一起使用时,我们建议依赖于内置的`CorsFilter`,这必须在 Spring 安全性的过滤器链之前订购。

有关更多详细信息,请参见[CORS](#webflux-cors)[webflux-cors.html](webflux-cors.html#webflux-cors-webfilter)一节。

#### 1.2.4.例外

[Web MVC](web.html#mvc-ann-customer-servlet-container-error-page)

[`WebHandler`API]中,可以使用`WebExceptionHandler`来处理来自`WebFilter`实例和目标`WebHandler`的链中的异常。当使用[WebFlux 配置](#webflux-config)时,注册`WebExceptionHandler`就像将其声明为 Spring  Bean 一样简单,并且(可选地)通过在 Bean 声明上使用`@Order`或通过实现`Ordered`来表示优先级。

下表描述了可用的`WebExceptionHandler`实现:

|           Exception Handler           |说明|
|---------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|   `ResponseStatusExceptionHandler`    |通过将响应设置为异常的 HTTP 状态代码,为类型[`ResponseStatusException`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/server/responsestatusception.html)的异常提供处理。|
|`WebFluxResponseStatusExceptionHandler`|`ResponseStatusExceptionHandler`的扩展,该扩展还可以在任何异常情况下确定<br/>代码的 HTTP 状态`@ResponseStatus`注释。<br/><br/>[WebFlux 配置](#webflux-config)中声明此处理程序。|

#### 1.2.5.编解码器

[Web MVC](integration.html#rest-message-conversion)

`spring-web``spring-core`模块通过具有反应流反压力的非阻塞 I/O,提供对与高层对象之间的字节内容的序列化和反序列化的支持。以下介绍了这种支持:

* [`Encoder`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/core/codec/encoder.html)和[`Decoder`(https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/core/core/core/codecoder.html)是独立于 http 的编码和解码内容的低级合同。

* [`HttpMessageReader`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/http/codec/httpMessageReader.html)和[`HttpMessageWriter`(https://DOCS. Spring.io/ Spring/ Spring-framework/DOCS/5.3.16/javadoc-api/org/spramework/http/codec/httpmessagewriter.html)是对 HTTPMessageWriter 内容进行编码和解码的合同。

* `Encoder`可以用`EncoderHttpMessageWriter`包装,以使其适合在 Web 应用程序中使用,而`Decoder`可以用`DecoderHttpMessageReader`包装。

* [`DataBuffer`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/core/io/io/buffer/databuffer.html)抽象出不同的字节缓冲区表示(例如 netty`ByteBuf`,`java.nio.ByteBuffer`等),这也是所有编解码器的工作内容。有关此主题的更多信息,请参见“ Spring core”部分中的[数据缓冲区和编解码器](core.html#databuffers)

`spring-core`模块提供`byte[]``ByteBuffer``DataBuffer``Resource``String`编码器和解码器实现方式。`spring-web`模块提供了 Jackson 的 JSON、JacksonSmile、JAXB2、协议缓冲区和其他编码器和解码器,以及用于表单数据、多部分内容、服务器发送的事件等的仅用于 Web 的 HTTP 消息阅读器和编写器实现。

`ClientCodecConfigurer``ServerCodecConfigurer`通常用于配置和定制要在应用程序中使用的编解码器。参见关于配置[HTTP 消息编解码器](#webflux-config-message-codecs)的部分。

##### JacksonJSON

当 Jackson 库存在时,都支持 JSON 和二进制 JSON([Smile](https://github.com/FasterXML/smile-format-specification))。

`Jackson2Decoder`的工作原理如下:

* Jackson 的异步、非阻塞解析器用于将一个字节块流聚合到`TokenBuffer`中,每个字节块代表一个 JSON 对象。

* 每个`TokenBuffer`都传递给 Jackson 的`ObjectMapper`,以创建一个更高级别的对象。

* 当解码到单值发布器(例如`Mono`)时,存在一个`TokenBuffer`

* 当解码到多值发布者(例如`Flux`)时,一旦接收到用于完全形成的对象的足够字节,每个`TokenBuffer`都会传递到`ObjectMapper`。输入内容可以是 JSON 数组,或任何[线分隔的 JSON](https://en.wikipedia.org/wiki/JSON_streaming)格式,例如 NDJSON、JSON 行或 JSON 文本序列。

`Jackson2Encoder`的工作原理如下:

* 对于单个值发布者(例如`Mono`),只需通过`ObjectMapper`序列化它。

* 对于具有`application/json`的多值发布者,默认情况下,使用`Flux#collectToList()`收集这些值,然后序列化生成的集合。

* 对于具有流媒体类型(如`application/x-ndjson``application/stream+x-jackson-smile`)的多值发布者,使用[线分隔的 JSON](https://en.wikipedia.org/wiki/JSON_streaming)格式对每个值分别进行编码、写入和刷新。其他流媒体类型可以在编码器中注册。

* 对于 SSE,每个事件都调用`Jackson2Encoder`,并刷新输出,以确保及时交付。

|   |默认情况下,`Jackson2Encoder``Jackson2Decoder`都不支持类型`String`的元素。相反,默认的假设是字符串或字符串序列<br/>表示序列化的 JSON 内容,由`CharSequenceEncoder`呈现。如果<br/>需要的是从`Flux<String>`呈现一个 JSON 数组,则使用`Flux#collectToList()`<br/>编码`Mono<List<String>>`。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

##### 表单数据

`FormHttpMessageReader``FormHttpMessageWriter`支持解码和编码`application/x-www-form-urlencoded`内容。

在表单内容经常需要从多个地方访问的服务器端,`ServerWebExchange`提供了一个专用的`getFormData()`方法,该方法通过`FormHttpMessageReader`解析内容,然后缓存结果以进行重复访问。参见[`WebHandler`API]部分中的[Form Data](#webflux-form-data)

一旦使用`getFormData()`,就不能再从请求主体中读取原始 RAW 内容。出于这个原因,应用程序需要始终通过`ServerWebExchange`来访问缓存的表单数据,而不是从原始请求主体读取数据。

##### 多部分

`MultipartHttpMessageReader``MultipartHttpMessageWriter`支持解码和编码“multipart/form-data”内容。反过来,`MultipartHttpMessageReader`将实际解析委托给另一个`HttpMessageReader`,然后简单地将部分收集到`MultiValueMap`中。默认情况下,使用`DefaultPartHttpMessageReader`,但这可以通过`ServerCodecConfigurer`进行更改。有关`DefaultPartHttpMessageReader`的更多信息,请参阅`DefaultPartHttpMessageReader`的[javadoc](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/http/codec/multipart/defaultparthpmessagereader.html)。

在可能需要从多个地方访问多部分表单内容的服务器端,`ServerWebExchange`提供了一个专用的`getMultipartData()`方法,该方法通过`MultipartHttpMessageReader`解析内容,然后缓存结果以进行重复访问。参见[`WebHandler`API]部分中的[多部分数据](#webflux-multipart)

一旦使用`getMultipartData()`,就不能再从请求主体中读取原始 RAW 内容。由于这个原因,应用程序必须始终使用`getMultipartData()`进行重复的、类似于 MAP 的部分访问,或者以其他方式依赖于`SynchronossPartHttpMessageReader`进行一次性访问`Flux<Part>`

##### 限制

`Decoder``HttpMessageReader`实现了对部分或全部输入流进行缓冲,可以在内存中配置缓冲的最大字节数的限制。在某些情况下,发生缓冲是因为输入被聚合并表示为单个对象——例如,带有`@RequestBody byte[]``x-www-form-urlencoded`数据的控制器方法,以此类推。在分割输入流(例如,分隔的文本、JSON 对象流等)时,流也可以发生缓冲。对于那些流情况,限制应用于与流中的一个对象相关联的字节数。

要配置缓冲区大小,你可以检查给定的`Decoder``HttpMessageReader`是否公开了`maxInMemorySize`属性,如果是这样,爪哇doc 将提供有关默认值的详细信息。在服务器端,`ServerCodecConfigurer`提供了一个设置所有编解码器的位置,请参见[HTTP 消息编解码器](#webflux-config-message-codecs)。在客户端,所有编解码器的限制可以在[Webclient.builder](#webflux-client-builder-maxinmemorysize)中进行更改。

对于[多部分解析](#webflux-codecs-multipart)`maxInMemorySize`属性限制了非文件部分的大小。对于文件部件,它确定将部件写入磁盘的阈值。对于写入磁盘的文件部件,有一个额外的`maxDiskUsagePerPart`属性来限制每个部件的磁盘空间。还有一个`maxParts`属性来限制多部分请求中的部分总数。要在 WebFlux 中配置这三个变量,你需要提供一个`MultipartHttpMessageReader``ServerCodecConfigurer`的预配置实例。

##### 流媒体

[Web MVC](web.html#mvc-ann-async-http-streaming)

当流到 HTTP 响应时(例如,`text/event-stream``application/x-ndjson`),定期发送数据是很重要的,以便可靠地检测断开连接的客户端,越早越好。这样的发送可能是一个只有评论的、空的 SSE 事件,或者是任何其他可以有效充当心跳的“无操作”数据。

##### `DataBuffer`

`DataBuffer`是 WebFlux 中字节缓冲区的表示形式。 Spring 该引用的核心部分在[数据缓冲区和编解码器](core.html#databuffers)一节中有更多关于该引用的内容。要理解的关键点是,在一些服务器(如 Netty)上,字节缓冲区是池的,引用也是计算的,并且必须在使用时释放,以避免内存泄漏。

WebFlux 应用程序通常不需要关注这些问题,除非它们直接使用或产生数据缓冲区,而不是依赖编解码器来转换到更高级别的对象,或者除非它们选择创建自定义编解码器。对于这种情况,请参阅[数据缓冲区和编解码器](core.html#databuffers)中的信息,特别是关于[使用 Databuffer](core.html#databuffers-using)的部分。

#### 1.2.6.伐木

[Web MVC](web.html#mvc-logging)

Spring WebFlux 中的`DEBUG`级别日志被设计为紧凑、最小且对人类友好的。它关注的是一次又一次有用的高价值信息,而不是仅在调试特定问题时有用的其他信息。

`TRACE`级别日志记录通常遵循与`DEBUG`相同的原则(例如,也不应该是消防软管),但可以用于调试任何问题。此外,一些日志消息可能在`TRACE``DEBUG`处显示不同级别的详细信息。

良好的日志记录来自于使用日志的经验。如果你发现任何不符合规定的目标,请告诉我们。

##### 日志 ID

在 WebFlux 中,单个请求可以在多个线程上运行,而线程 ID 对于关联属于特定请求的日志消息是没有用的。这就是为什么 WebFlux 日志消息在默认情况下使用特定于请求的 ID 作为前缀的原因。

在服务器端,日志 ID 存储在`ServerWebExchange`属性中([`LOG_ID_ATTRIBUTE`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/server/serverwebexchange.html#log_id_attribute)),而基于该 ID 的完全格式化的前缀可从`ServerWebExchange#getLogPrefix()`获得。在`WebClient`端,日志 ID 存储在`ClientRequest`属性([`LOG_ID_ATTRIBUTE`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/active/function/client/clientrequest.html#log_id_attribute)中),而完全格式化的前缀可从`ClientRequest#logPrefix()`获得。

##### 敏感数据

[Web MVC](web.html#mvc-logging-sensitive-data)

`DEBUG``TRACE`日志记录可以记录敏感信息。这就是为什么表单参数和标题在默认情况下是屏蔽的,并且你必须显式地完全启用它们的日志记录。

下面的示例展示了如何为服务器端请求执行此操作:

爪哇

```
@Configuration
@EnableWebFlux
class MyConfig implements WebFluxConfigurer {

    @Override
    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
        configurer.defaultCodecs().enableLoggingRequestDetails(true);
    }
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class MyConfig : WebFluxConfigurer {

    override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
        configurer.defaultCodecs().enableLoggingRequestDetails(true)
    }
}
```

下面的示例展示了如何为客户端请求执行此操作:

爪哇

```
Consumer<ClientCodecConfigurer> consumer = configurer ->
        configurer.defaultCodecs().enableLoggingRequestDetails(true);

WebClient webClient = WebClient.builder()
        .exchangeStrategies(strategies -> strategies.codecs(consumer))
        .build();
```

Kotlin

```
val consumer: (ClientCodecConfigurer) -> Unit  = { configurer -> configurer.defaultCodecs().enableLoggingRequestDetails(true) }

val webClient = WebClient.builder()
        .exchangeStrategies({ strategies -> strategies.codecs(consumer) })
        .build()
```

##### 附录

SLF4j 和 log4j2 等日志记录库提供了避免阻塞的异步记录器。尽管这些方法有其自身的缺点,比如可能会丢弃无法排队记录的消息,但它们是当前在反应性、非阻塞应用程序中使用的最佳可用选项。

##### 自定义编解码器

应用程序可以注册用于支持其他媒体类型的定制编解码器,或者默认编解码器不支持的特定行为。

开发人员表示的一些配置选项是在默认的编解码器上强制执行的。自定义编解码器可能希望有机会与这些首选项保持一致,比如[强制缓冲限制](#webflux-codecs-limits)[记录敏感数据](#webflux-logging-sensitive-data)

下面的示例展示了如何为客户端请求执行此操作:

爪哇

```
WebClient webClient = WebClient.builder()
        .codecs(configurer -> {
                CustomDecoder decoder = new CustomDecoder();
                configurer.customCodecs().registerWithDefaultConfig(decoder);
        })
        .build();
```

Kotlin

```
val webClient = WebClient.builder()
        .codecs({ configurer ->
                val decoder = CustomDecoder()
                configurer.customCodecs().registerWithDefaultConfig(decoder)
         })
        .build()
```

### 1.3.`DispatcherHandler`

[Web MVC](web.html#mvc-servlet)

Spring WebFlux,类似于 Spring MVC,是围绕前控制器模式设计的,其中中心,,提供用于请求处理的共享算法,而实际工作是通过可配置的、委托的组件来执行的。这个模型是灵活的,并支持不同的工作流程。

`DispatcherHandler`从 Spring 配置中发现它需要的委托组件。它本身也被设计为 Spring  Bean 并且实现`ApplicationContextAware`以访问它运行的上下文。如果`DispatcherHandler`是以 Bean 名`webHandler`声明的,那么它又是由[`WebHttpHandlerBuilder`](https:/DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/server/adapter/webhtphandlerbuilder.html)发现的,它组合了一个请求-处理链,如[`WebHandler`r="webpi-api-handler.html](#-flux)中所述。

Spring WebFlux 应用程序中的配置通常包括:

* `DispatcherHandler`与 Bean 名称`webHandler`

* `WebFilter``WebExceptionHandler`beans

* [`DispatcherHandler`Special Beans](#WebFlux-Special- Bean-types)

* 其他

将配置给`WebHttpHandlerBuilder`以构建处理链,如下例所示:

爪哇

```
ApplicationContext context = ...
HttpHandler handler = WebHttpHandlerBuilder.applicationContext(context).build();
```

Kotlin

```
val context: ApplicationContext = ...
val handler = WebHttpHandlerBuilder.applicationContext(context).build()
```

得到的`HttpHandler`可以与[服务器适配器](#webflux-httphandler)一起使用了。

#### 1.3.1.特殊类型 Bean

[Web MVC](web.html#mvc-servlet-special-bean-types)

`DispatcherHandler`将委托给特殊的 bean 来处理请求并呈现适当的响应。我们所说的“特殊 bean”是指实现 WebFlux 框架契约的 Spring-managed`Object`实例。这些通常带有内置契约,但你可以自定义它们的属性,扩展它们或替换它们。

下表列出了`DispatcherHandler`检测到的特殊 bean。请注意,在较低的级别上还检测到一些其他 bean(请参见 Web 处理程序 API 中的[Special bean types](#webflux-web-handler-api-special-beans))。

|      Bean type       |解释|
|----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|   `HandlerMapping`   |将请求映射到处理程序。该映射是基于一些条件,其中<br/>的细节通过`HandlerMapping`实现—注释控制器,简单<br/>URL 模式映射,以及其他不同的实现。<br/>`HandlerMapping`主要的`RequestMappingHandlerMapping`实现为`@RequestMapping`注释方法,`RouterFunctionMapping`为功能端点<710"/>r= 路由,和`SimpleUrlHandlerMapping`用于显式注册 URI 路径模式<br/>`WebHandler`实例。|
|   `HandlerAdapter`   |帮助`DispatcherHandler`调用映射到请求的处理程序,而不管<br/>实际调用处理程序的方式如何。例如,调用带注释的控制器<br/>需要解析注释。a`HandlerAdapter`的主要目的是保护`DispatcherHandler`不受这些细节的影响。|
|`HandlerResultHandler`|处理来自处理程序调用的结果并最终确定响应。<br/>参见[结果处理](#webflux-resulthandling)。|

#### 1.3.2.WebFlux 配置

[Web MVC](web.html#mvc-servlet-config)

应用程序可以声明处理请求所需的基础设施 bean(在[Web 处理程序 API](#webflux-web-handler-api-special-beans)[`DispatcherHandler`](#Webflux-special- Bean-types)下列出)。然而,在大多数情况下,[WebFlux 配置](#webflux-config)是最好的起点。它声明所需的 bean,并提供一个更高级的配置回调 API 来定制它。

|   |Spring 启动依赖于 WebFlux 配置来配置 Spring WebFlux,并且还提供了<br/>许多额外的方便选项。|
|---|-------------------------------------------------------------------------------------------------------------------------|

#### 1.3.3.处理

[Web MVC](web.html#mvc-servlet-sequence)

`DispatcherHandler`按以下方式处理请求:

* 每个`HandlerMapping`都被要求找到一个匹配的处理程序,并使用第一个匹配。

* 如果找到了一个处理程序,则通过一个适当的`HandlerAdapter`运行该处理程序,该处理程序将执行时的返回值公开为`HandlerResult`

*`HandlerResult`赋予适当的`HandlerResultHandler`,以通过直接写入响应或通过使用视图来呈现来完成处理。

#### 1.3.4.结果处理

通过`HandlerAdapter`调用处理程序的返回值被包装为`HandlerResult`,以及一些附加的上下文,并传递给声称支持它的第一个`HandlerResultHandler`。下表显示了可用的`HandlerResultHandler`实现,所有这些实现都在[WebFlux 配置](#webflux-config)中声明:

|     Result Handler Type     |返回值|   Default Order   |
|-----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------|
|`ResponseEntityResultHandler`|`ResponseEntity`,通常来自`@Controller`实例。|         0         |
|`ServerResponseResultHandler`|`ServerResponse`,通常来自功能端点。|         0         |
| `ResponseBodyResultHandler` |处理来自`@ResponseBody`方法或`@RestController`类的返回值。|        100        |
|`ViewResolutionResultHandler`|`CharSequence`[`View`](https://DOCS. Spring.io/ Spring-framework/5.3.16/javadoc-api/org/springframework/web/result/result/view/view.html),[Model](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/ui/Model.html)`Map`[Rendering](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/reactive/result/view/Rendering.html),或其他任何`Object`均作为模型属性处理。<br/><>[视图分辨率](#webflux-viewresolution)<<>r=">><<|`Integer.MAX_VALUE`|

#### 1.3.5.例外

[Web MVC](web.html#mvc-exceptionhandlers)

`HandlerAdapter`返回的`HandlerResult`可以基于某些特定于处理程序的机制公开用于错误处理的函数。如果出现以下情况,则调用此错误函数:

* 处理程序(例如,`@Controller`)调用失败。

* 通过`HandlerResultHandler`处理处理程序返回值失败。

错误函数可以更改响应(例如,到错误状态),只要在从处理程序返回的反应类型产生任何数据项之前发生错误信号。

这就是`@Controller`类中的`@ExceptionHandler`方法的支持方式。相比之下,对 Spring MVC 中相同内容的支持是建立在`HandlerExceptionResolver`上的。这一点一般不会有什么影响。但是,请记住,在 WebFlux 中,不能使用`@ControllerAdvice`来处理在选择处理程序之前发生的异常。

另请参见“注释控制器”部分中的[管理异常](#webflux-ann-controller-exceptions)或 WebHandler API 部分中的[Exceptions](#webflux-exception-handler)

#### 1.3.6.视图分辨率

[Web MVC](web.html#mvc-viewresolver)

视图分辨率允许使用 HTML 模板和模型在浏览器上进行呈现,而无需将你绑定到特定的视图技术。在 Spring WebFlux 中,通过专用的[HandlerResultHandler](#webflux-resulthandling)支持视图解析,该实例使用`ViewResolver`实例将字符串(代表逻辑视图名称)映射到`View`实例。然后使用`View`来呈现响应。

##### 处理

[Web MVC](web.html#mvc-handling)

传递到`ViewResolutionResultHandler`中的`HandlerResult`包含来自处理程序的返回值和包含在请求处理过程中添加的属性的模型。返回值被处理为以下内容之一:

* `String``CharSequence`:要通过配置的`ViewResolver`实现的列表解析为`View`的逻辑视图名称。

* `void`:根据请求路径选择一个默认的视图名称,减去前导和后导斜杠,并将其解析为`View`。当未提供视图名称(例如,返回了 model 属性)或异步返回值(例如,`Mono`完全为空)时,也会发生同样的情况。

* [Rendering](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/reactive/result/view/Rendering.html):视图解析场景的 API。探索你的 IDE 中的代码补全选项。

* `Model``Map`:要为请求添加到模型中的额外模型属性。

* 任何其他:任何其他返回值(简单类型除外,由[Beanutils#IsSimpleProperty](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-)确定)被视为要添加到模型中的模型属性。属性名是通过使用[惯例](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/core/Conventions.html)从类名派生出来的,除非存在处理程序方法`@ModelAttribute`注释。

该模型可以包含异步的、反应性的类型(例如,来自 Reactor 或 RX爪哇)。在呈现之前,`AbstractView`将这些模型属性解析为具体的值并更新模型。单值活性类型被解析为单值或无值(如果为空),而多值活性类型(例如,`Flux<T>`)被收集并解析为`List<T>`

要配置视图分辨率就像在 Spring 配置中添加`ViewResolutionResultHandler` Bean 一样简单。[WebFlux 配置](#webflux-config-view-resolvers)为视图分辨率提供了专用的配置 API。

有关与 Spring WebFlux 集成的视图技术的更多信息,请参见[查看技术](#webflux-view)

##### 重定向

[Web MVC](web.html#mvc-redirecting-redirect-prefix)

视图名称中的特殊`redirect:`前缀允许你执行重定向。`UrlBasedViewResolver`(和子类)将其视为需要重定向的指令。视图名称的其余部分是重定向 URL。

净效果与控制器返回`RedirectView``Rendering.redirectTo("abc").build()`相同,但现在控制器本身可以根据逻辑视图名称进行操作。像`redirect:/some/resource`这样的视图名称是相对于当前应用程序的,而像`redirect:https://example.com/arbitrary/path`这样的视图名称会重定向到一个绝对 URL。

##### 内容协商

[Web MVC](web.html#mvc-multiple-representations)

`ViewResolutionResultHandler`支持内容协商。它将请求媒体类型与每个选定的`View`所支持的媒体类型进行比较。使用了支持所请求的媒体类型的第一个`View`

为了支持诸如 JSON 和 XML 等媒体类型, Spring WebFlux 提供了`HttpMessageWriterView`,这是一个特殊的`View`,它通过[HttpMessageWriter](#webflux-codecs)呈现。通常,你会通过[WebFlux 配置](#webflux-config-view-resolvers)将这些视图配置为默认视图。如果默认视图匹配所请求的媒体类型,则始终选择并使用它们。

### 1.4.带注释的控制器

[Web MVC](web.html#mvc-controller)

Spring WebFlux 提供了一种基于注释的编程模型,其中`@Controller``@RestController`组件使用注释来表示请求映射、请求输入、处理异常等。带注释的控制器具有灵活的方法签名,不需要扩展基类,也不需要实现特定的接口。

下面的清单展示了一个基本示例:

Java

```
@RestController
public class HelloController {

    @GetMapping("/hello")
    public String handle() {
        return "Hello WebFlux";
    }
}
```

Kotlin

```
@RestController
class HelloController {

    @GetMapping("/hello")
    fun handle() = "Hello WebFlux"
}
```

在前面的示例中,该方法返回要写入响应主体的`String`

#### 1.4.1.`@Controller`

[Web MVC](web.html#mvc-ann-controller)

你可以通过使用标准的 Spring  Bean 定义来定义控制器 bean。该原型允许自动检测并与 Spring 用于检测 Classpath 中的类的通用支持保持一致,并为它们自动注册 Bean 定义。它还充当带注释的类的原型,指示其作为 Web 组件的角色。

要启用对此类`@Controller`bean 的自动检测,可以将组件扫描添加到 Java 配置中,如下例所示:

Java

```
@Configuration
@ComponentScan("org.example.web") (1)
public class WebConfig {

    // ...
}
```

|**1**|扫描`org.example.web`包。|
|-----|-----------------------------------|

Kotlin

```
@Configuration
@ComponentScan("org.example.web") (1)
class WebConfig {

    // ...
}
```

|**1**|扫描`org.example.web`包。|
|-----|-----------------------------------|

`@RestController`是一个[组合注释](core.html#beans-meta-annotations),它本身用`@Controller``@ResponseBody`进行了元注释,表示一个控制器,其每个方法都继承了类型级`@ResponseBody`注释,因此,它直接写到响应主体与视图解析之间,并使用 HTML 模板进行呈现。

#### 1.4.2.请求映射

[Web MVC](web.html#mvc-ann-requestmapping)

`@RequestMapping`注释用于将请求映射到控制器方法。它具有各种属性,可以通过 URL、HTTP 方法、请求参数、标头和媒体类型进行匹配。你可以在类级别上使用它来表示共享映射,或者在方法级别上使用它来缩小到特定的端点映射。

还有`@RequestMapping`的特定于 HTTP 方法的快捷方式变体:

* `@GetMapping`

* `@PostMapping`

* `@PutMapping`

* `@DeleteMapping`

* `@PatchMapping`

提供前面的注释是[自定义注释](#webflux-ann-requestmapping-composed),因为可以说,大多数控制器方法应该映射到特定的 HTTP 方法,而不是使用`@RequestMapping`,默认情况下,该方法匹配所有的 HTTP 方法。同时,在类级别上仍然需要一个`@RequestMapping`来表示共享映射。

下面的示例使用类型和方法级别映射:

Java

```
@RestController
@RequestMapping("/persons")
class PersonController {

    @GetMapping("/{id}")
    public Person getPerson(@PathVariable Long id) {
        // ...
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public void add(@RequestBody Person person) {
        // ...
    }
}
```

Kotlin

```
@RestController
@RequestMapping("/persons")
class PersonController {

    @GetMapping("/{id}")
    fun getPerson(@PathVariable id: Long): Person {
        // ...
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    fun add(@RequestBody person: Person) {
        // ...
    }
}
```

##### URI 模式

[Web MVC](web.html#mvc-ann-requestmapping-uri-templates)

你可以使用 GLOB 模式和通配符来映射请求:

|    Pattern    |                                              Description                                              |例子|
|---------------|-------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|      `?`      |                                         Matches one character                                         |`"/pages/t?st.html"`匹配`"/pages/test.html"``"/pages/t3st.html"`|
|      `*`      |                         Matches zero or more characters within a path segment                         |`"/resources/*.png"`匹配`"/resources/file.png"`<br/><br/>匹配`"/projects/*/versions"`但不匹配`"/projects/spring/versions"`|
|     `**`      |                     Matches zero or more path segments until the end of the path                      |`"/resources/**"`匹配`"/resources/file.png"``"/resources/images/file.png"`<br/><br/>`"/resources/**/file.png"`是无效的,因为`**`只允许在路径的末尾。|
|   `{name}`    |                   Matches a path segment and captures it as a variable named "name"                   |`"/projects/{project}/versions"`匹配`"/projects/spring/versions"`并捕获`project=spring`|
|`{name:[a-z]+}`|                     Matches the regexp `"[a-z]+"` as a path variable named "name"                     |`"/projects/{project:[a-z]+}/versions"`匹配`"/projects/spring/versions"`但不匹配`"/projects/spring1/versions"`|
|   `{*path}`   |Matches zero or more path segments until the end of the path and captures it as a variable named "path"|`"/resources/{*file}"`匹配`"/resources/images/file.png"`并捕获`file=/images/file.png`|

可以使用`@PathVariable`访问捕获的 URI 变量,如下例所示:

Java

```
@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
    // ...
}
```

Kotlin

```
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet {
    // ...
}
```

可以在类和方法级别声明 URI 变量,如下例所示:

Java

```
@Controller
@RequestMapping("/owners/{ownerId}") (1)
public class OwnerController {

    @GetMapping("/pets/{petId}") (2)
    public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
        // ...
    }
}
```

|**1**|类级 URI 映射。|
|-----|-------------------------|
|**2**|方法级别的 URI 映射。|

Kotlin

```
@Controller
@RequestMapping("/owners/{ownerId}") (1)
class OwnerController {

    @GetMapping("/pets/{petId}") (2)
    fun findPet(@PathVariable ownerId: Long, @PathVariable petId: Long): Pet {
        // ...
    }
}
```

|**1**|类级 URI 映射。|
|-----|-------------------------|
|**2**|方法级别的 URI 映射。|

URI 变量将自动转换为适当的类型,或者生成`TypeMismatchException`。默认情况下支持简单类型(`int``long``Date`,等等),你可以注册对任何其他数据类型的支持。参见[类型转换](#webflux-ann-typeconversion)和[`DataBinder`]。

URI 变量可以显式地命名(例如,`@PathVariable("customId")`),但是如果名称相同,并且你可以使用调试信息或 Java8 上的`-parameters`编译器标志来编译代码,则可以忽略这些细节。

语法`{*varName}`声明一个 URI 变量,该变量匹配零个或多个剩余的路径段。例如,`/resources/{*path}`匹配`/resources/`下的所有文件,并且`"path"`变量捕获`/resources`下的完整路径。

语法`{varName:regex}`声明一个 URI 变量,其正则表达式的语法为:`{varName:regex}`。例如,给定一个`/spring-web-3.0.5.jar`的 URL,下面的方法会提取名称、版本和文件扩展名:

Java

```
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String version, @PathVariable String ext) {
    // ...
}
```

Kotlin

```
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
fun handle(@PathVariable version: String, @PathVariable ext: String) {
    // ...
}
```

URI 路径模式还可以嵌入`${…​}`占位符,这些占位符在启动时通过`PropertyPlaceHolderConfigurer`针对本地、系统、环境和其他属性源解析。例如,你可以使用它来基于某些外部配置参数化一个基本 URL。

|   |Spring WebFlux 将`PathPattern``PathPatternParser`用于 URI 路径匹配支持。<br/>这两个类都位于`spring-web`中,并且明确地设计用于在 Web 应用程序中使用 http url<br/>路径,其中在运行时匹配了大量的 URI 路径模式。|
|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

Spring WebFlux 不支持后缀模式匹配——不像 Spring MVC,其中像`/person`这样的映射也匹配到`/person.*`。对于基于 URL 的内容协商,如果需要,我们建议使用一个查询参数,该参数更简单,更明确,并且不易受到基于 URL 路径的攻击。

##### 模式比较

[Web MVC](web.html#mvc-ann-requestmapping-pattern-comparison)

当多个模式匹配一个 URL 时,必须对它们进行比较以找到最佳匹配。这是用`PathPattern.SPECIFICITY_COMPARATOR`完成的,它寻找更具体的模式。

对于每个模式,都会根据 URI 变量和通配符的数量计算得分,其中 URI 变量的得分低于通配符。总分较低的模式获胜。如果两种模式得分相同,则选择较长的模式。

包罗万象的模式(例如,`**``{*varName}`)被排除在评分之外,并且总是排在最后。如果两种模式都是包罗万象的,则选择较长的模式。

##### 可消费媒体类型

[Web MVC](web.html#mvc-ann-requestmapping-consumes)

你可以基于请求的`Content-Type`缩小请求映射,如下例所示:

Java

```
@PostMapping(path = "/pets", consumes = "application/json")
public void addPet(@RequestBody Pet pet) {
    // ...
}
```

Kotlin

```
@PostMapping("/pets", consumes = ["application/json"])
fun addPet(@RequestBody pet: Pet) {
    // ...
}
```

Consumes 属性还支持否定表达式——例如,`!text/plain`表示除`text/plain`以外的任何内容类型。

你可以在类级别声明一个共享的`consumes`属性。然而,与大多数其他请求映射属性不同的是,当在类级使用时,方法级`consumes`属性覆盖而不是扩展类级声明。

|   |`MediaType`提供了常用媒体类型的常量——例如,`APPLICATION_JSON_VALUE``APPLICATION_XML_VALUE`。|
|---|--------------------------------------------------------------------------------------------------------------------------------|

##### 可生产媒体类型

[Web MVC](web.html#mvc-ann-requestmapping-produces)

你可以基于`Accept`请求头和控制器方法产生的内容类型列表来缩小请求映射的范围,如下例所示:

Java

```
@GetMapping(path = "/pets/{petId}", produces = "application/json")
@ResponseBody
public Pet getPet(@PathVariable String petId) {
    // ...
}
```

Kotlin

```
@GetMapping("/pets/{petId}", produces = ["application/json"])
@ResponseBody
fun getPet(@PathVariable String petId): Pet {
    // ...
}
```

媒体类型可以指定字符集。支持否定表达式——例如,`!text/plain`表示除`text/plain`以外的任何内容类型。

你可以在类级别声明一个共享的`produces`属性。然而,与大多数其他请求映射属性不同的是,当在类级别使用时,方法级别`produces`属性覆盖而不是扩展类级别声明。

|   |`MediaType`提供了常用媒体类型的常量,例如`APPLICATION_JSON_VALUE``APPLICATION_XML_VALUE`。|
|---|---------------------------------------------------------------------------------------------------------------------|

##### 参数和标题

[Web MVC](web.html#mvc-ann-requestmapping-params-and-headers)

你可以根据查询参数条件缩小请求映射的范围。你可以测试查询参数的存在(`myParam`)、它的不存在(`!myParam`)或特定值(`myParam=myValue`)。下面的示例测试具有值的参数:

Java

```
@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") (1)
public void findPet(@PathVariable String petId) {
    // ...
}
```

|**1**|检查`myParam`等于`myValue`。|
|-----|--------------------------------------|

Kotlin

```
@GetMapping("/pets/{petId}", params = ["myParam=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
    // ...
}
```

|**1**|检查`myParam`等于`myValue`。|
|-----|--------------------------------------|

你也可以在请求头条件中使用相同的方法,如下面的示例所示:

Java

```
@GetMapping(path = "/pets", headers = "myHeader=myValue") (1)
public void findPet(@PathVariable String petId) {
    // ...
}
```

|**1**|检查`myHeader`等于`myValue`。|
|-----|---------------------------------------|

Kotlin

```
@GetMapping("/pets", headers = ["myHeader=myValue"]) (1)
fun findPet(@PathVariable petId: String) {
    // ...
}
```

|**1**|检查`myHeader`等于`myValue`。|
|-----|---------------------------------------|

##### HTTP 头,选项

[Web MVC](web.html#mvc-ann-requestmapping-head-options)

`@GetMapping``@RequestMapping(method=HttpMethod.GET)`透明地支持用于请求映射目的的 HTTP head。控制器的方法不需要改变。在`HttpHandler`服务器适配器中应用的响应包装器确保将`Content-Length`头设置为不实际写入响应的字节数。

默认情况下,通过将`Allow`响应头设置为所有`@RequestMapping`方法中列出的具有匹配 URL 模式的 HTTP 方法列表来处理 HTTP 选项。

对于不带 HTTP 方法声明的`@RequestMapping``Allow`头被设置为`GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS`。控制器方法应该总是声明受支持的 HTTP 方法(例如,通过使用 HTTP 方法特定的变体—`@GetMapping``@PostMapping`,以及其他)。

你可以显式地将`@RequestMapping`方法映射到 HTTPHead 和 HTTPOptions,但在常见情况下这不是必需的。

##### 自定义注释

[Web MVC](web.html#mvc-ann-requestmapping-composed)

Spring WebFlux 支持使用[组合注释](core.html#beans-meta-annotations)进行请求映射。这些注释本身是用`@RequestMapping`进行元注释的,其组成是为了重新声明`@RequestMapping`属性的一个子集(或全部),具有更窄、更具体的目的。

`@GetMapping``@PostMapping``@PutMapping``@DeleteMapping`,和`@PatchMapping`是合成注释的例子。提供它们是因为,可以说,大多数控制器方法应该映射到特定的 HTTP 方法,而不是使用`@RequestMapping`,后者默认情况下与所有 HTTP 方法匹配。如果你需要一个组合注释的示例,请查看这些注释是如何声明的。

Spring WebFlux 还支持具有自定义请求匹配逻辑的自定义请求映射属性。这是一个更高级的选项,它需要子类化`RequestMappingHandlerMapping`并覆盖`getCustomMethodCondition`方法,在该方法中,你可以检查自定义属性并返回你自己的`RequestCondition`

##### 显式注册

[Web MVC](web.html#mvc-ann-requestmapping-registration)

你可以以编程方式注册处理程序方法,这些方法可以用于动态注册或高级情况,例如同一处理程序在不同 URL 下的不同实例。下面的示例展示了如何做到这一点:

Java

```
@Configuration
public class MyConfig {

    @Autowired
    public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler) (1)
            throws NoSuchMethodException {

        RequestMappingInfo info = RequestMappingInfo
                .paths("/user/{id}").methods(RequestMethod.GET).build(); (2)

        Method method = UserHandler.class.getMethod("getUser", Long.class); (3)

        mapping.registerMapping(info, handler, method); (4)
    }

}
```

|**1**|为控制器注入目标处理程序和处理程序映射。|
|-----|---------------------------------------------------------------|
|**2**|准备请求映射元数据。|
|**3**|获取 handler 方法。|
|**4**|添加注册。|

Kotlin

```
@Configuration
class MyConfig {

    @Autowired
    fun setHandlerMapping(mapping: RequestMappingHandlerMapping, handler: UserHandler) { (1)

        val info = RequestMappingInfo.paths("/user/{id}").methods(RequestMethod.GET).build() (2)

        val method = UserHandler::class.java.getMethod("getUser", Long::class.java) (3)

        mapping.registerMapping(info, handler, method) (4)
    }
}
```

|**1**|为控制器注入目标处理程序和处理程序映射。|
|-----|---------------------------------------------------------------|
|**2**|准备请求映射元数据。|
|**3**|获取 handler 方法。|
|**4**|添加注册。|

#### 1.4.3.处理程序方法

[Web MVC](web.html#mvc-ann-methods)

`@RequestMapping`处理程序方法具有灵活的签名,并且可以从受支持的控制器方法参数和返回值的范围中进行选择。

##### 方法参数

[Web MVC](web.html#mvc-ann-arguments)

下表显示了受支持的控制器方法参数。

对于需要解析阻塞 I/O(例如,读取请求主体)的参数,支持反应性类型(reactor,RxJava,[or other](#webflux-reactive-libraries))。这一点在“描述”栏中进行了标记。在不需要阻塞的参数上,反应式类型是不被期望的。

JDK1.8 的`java.util.Optional`作为方法参数被支持,并与具有`required`属性(例如,`@RequestParam``@RequestHeader`,以及其他)的注释相结合,并且与`required=false`等价。

|                              Controller method argument                               |说明|
|---------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|                                  `ServerWebExchange`                                  |访问完整的`ServerWebExchange`—用于 HTTP 请求和响应的容器、<br/>请求和会话属性、`checkNotModified`方法等。|
|                       `ServerHttpRequest`, `ServerHttpResponse`                       |访问 HTTP 请求或响应。|
|                                     `WebSession`                                      |访问会话。除非添加了属性<br/>,否则不强制开始新的会话。支持反应性类型。|
|                               `java.security.Principal`                               |当前经过身份验证的用户——如果已知的话,可能是特定的<br/>实现类。<br/>支持反应性类型。|
|                         `org.springframework.http.HttpMethod`                         |请求的 HTTP 方法。|
|                                  `java.util.Locale`                                   |当前的请求区域设置,由可用的最特定的`LocaleResolver`确定—在<br/>效果中,配置的`LocaleResolver`/`LocaleContextResolver`。|
|                       `java.util.TimeZone` + `java.time.ZoneId`                       |与当前请求相关联的时区,由`LocaleContextResolver`确定。|
|                                    `@PathVariable`                                    |用于访问 URI 模板变量。见[URI 模式](#webflux-ann-requestmapping-uri-templates)。|
|                                   `@MatrixVariable`                                   |用于访问 URI 路径段中的名称-值对。见[矩阵变量](#webflux-ann-matrix-variables)。|
|                                    `@RequestParam`                                    |用于访问 Servlet 请求参数。参数值被转换为声明的<br/>方法参数类型。参见[`@RequestParam`]。<br/><br/>注意,`@RequestParam`的使用是可选的——例如,用于设置其属性。<br/>参见本表后面的“任何其他参数”。|
|                                   `@RequestHeader`                                    |用于访问请求头。标头值被转换为声明的方法参数<br/>type。参见[`@RequestHeader`]。|
|                                    `@CookieValue`                                     |获取 cookie 的权限。cookie 值被转换为声明的方法参数类型。<br/>参见[`@CookieValue`]。|
|                                    `@RequestBody`                                     |用于访问 HTTP 请求主体。通过使用`HttpMessageReader`实例,主体内容被转换为声明的方法<br/>参数类型。支持反应性类型。<br/>参见[`@RequestBody`]。|
|                                    `HttpEntity<B>`                                    |用于访问请求头和主体。主体使用`HttpMessageReader`实例进行转换。<br/>实例支持反应类型。参见[`HttpEntity`]。|
|                                    `@RequestPart`                                     |用于访问`multipart/form-data`请求中的部件。支持反应类型。<br/>参见[多部分内容](#webflux-multipart-forms)[多部分数据](#webflux-multipart)。|
|`java.util.Map`, `org.springframework.ui.Model`, and `org.springframework.ui.ModelMap`.|用于访问 HTML 控制器中使用的模型,并作为<br/>视图呈现的一部分暴露于模板中。|
|                                   `@ModelAttribute`                                   |用于访问模型中的现有属性(如果不存在则实例化),并应用<br/>数据绑定和验证。参见[`@ModelAttribute`](#Webflux-ann-modelattrib-method-args)以及<br/>as[`Model`](#Webflux-ann-modelattrib-methods)和[<`DataBinder`(#Webflux-ann-initbinder)。<br/><br/>注意,使用<br/>是可选的,例如,用于设置其属性。|
|                               `Errors`, `BindingResult`                               |用于访问来自命令对象的验证和数据绑定的错误,即`@ModelAttribute`参数。一个`Errors``BindingResult`参数必须在经过验证的方法参数之后立即声明<br/>。|
|                  `SessionStatus` + class-level `@SessionAttributes`                   |要标记表单处理完成,这将触发清除通过类级`@SessionAttributes`注释声明的会话属性<br/><br/>有关更多详细信息,请参见[`@SessionAttributes`]。|
|                                `UriComponentsBuilder`                                 |用于准备相对于当前请求的主机、端口、方案和<br/>上下文路径的 URL。见`@RequestParam`。|
|                                  `@SessionAttribute`                                  |用于访问任何会话属性——与存储在会话<br/>中的模型属性形成对比,后者是类级别`@SessionAttributes`声明的结果。有关更多详细信息,请参见[`@SessionAttribute`]。|
|                                  `@RequestAttribute`                                  |用于访问请求属性。有关更多详细信息,请参见[`@RequestAttribute`]。|
|                                  Any other argument                                   |如果方法参数与上述任何一个参数不匹配,则默认情况下将其解析为<br/>a`@RequestParam`如果它是一个简单类型,则由[Beanutils#IsSimpleProperty](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-)、<br/>或 a`@ModelAttribute`确定,否则。|

##### 返回值

[Web MVC](web.html#mvc-ann-return-types)

下表显示了受支持的控制器方法的返回值。请注意,对于所有返回值,通常都支持来自诸如 reactor、rxjava、[or other](#webflux-reactive-libraries)等库的反应类型。

|                        Controller method return value                        |说明|
|------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|                               `@ResponseBody`                                |返回值通过`HttpMessageWriter`实例进行编码,并写入响应。<br/>参见[`@ResponseBody`]。|
|                     `HttpEntity<B>`, `ResponseEntity<B>`                     |返回值指定了完整的响应,包括 HTTP 头,并且通过`HttpMessageWriter`实例对主体进行编码<br/>,并将其写入响应。<br/>参见[`ResponseEntity`](#WebFlux-Ann-ResponseEntity)。|
|                                `HttpHeaders`                                 |返回带有标题而没有正文的响应。|
|                                   `String`                                   |要用`ViewResolver`实例解析的视图名称,并与隐式<br/>模型一起使用——通过命令对象和`@ModelAttribute`方法确定。处理程序<br/>方法还可以通过声明一个`Model`参数<br/>(描述[earlier](#webflux-viewresolution-handling))以编程方式丰富模型。|
|                                    `View`                                    |一个`View`实例用于与隐式模型一起进行渲染——通过命令对象和<br/>方法确定<br/>。处理程序方法还可以通过声明一个`Model`参数<br/>(描述[earlier](#webflux-viewresolution-handling))以编程方式丰富模型。|
|               `java.util.Map`, `org.springframework.ui.Model`                |要添加到隐式模型中的属性,并根据请求路径隐式地确定视图名称<br/>。|
|                              `@ModelAttribute`                               |要添加到模型中的一个属性,其视图名称是基于请求路径隐式确定的<br/><br/><br/>注意,[Beanutils#IsSimpleProperty](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-)是可选的。请参阅下面的<br/>中的“任何其他返回值”。|
|                                 `Rendering`                                  |用于模型和视图呈现场景的 API。|
|                                    `void`                                    |如果方法具有`void`,可能是异步的(例如`Mono<Void>`),返回类型(或`null`返回<br/>值),则认为该方法已完全处理了响应,如果该方法还具有`ServerHttpResponse`<br/>参数,或`ServerWebExchange`注释。同样也是真的<br/>如果控制器进行了正的 ETag 或`lastModified`时间戳检查。<br/>//todo:详情请参见[控制器](#webflux-caching-etag-lastmodified)<br/><br/>如果以上都不是真的,`void`返回类型还可以表示<br/>REST 控制器的“无响应体”或 HTML 控制器的默认视图名称选择。|
|`Flux<ServerSentEvent>`, `Observable<ServerSentEvent>`, or other reactive type|发出服务器发送的事件。当只需要<br/>写入数据时(但是,`text/event-stream`必须通过<br/>属性在映射中请求或声明`text/event-stream`),可以省略`ServerSentEvent`包装器。|
|                            Any other return value                            |如果一个返回值与上述任一项不匹配,则默认情况下,它被视为一个视图<br/>名称,如果它是`String`或(应用默认的视图名称选择),或者作为一个要添加到模型中的模型<br/>属性,除非是简单的类型,如由[Beanutils#IsSimpleProperty](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-)确定的,否则<br/>在这种情况下仍未解决。|

##### 类型转换

[Web MVC](web.html#mvc-ann-typeconversion)

一些表示基于字符串的请求输入的带注释的控制器方法参数(例如,`@RequestParam``@RequestHeader``@PathVariable``@MatrixVariable`,和`@CookieValue`)可以要求类型转换,如果参数被声明为`String`以外的内容。

对于这样的情况,类型转换是基于配置的转换器自动应用的。默认情况下,支持简单类型(如`int``long``Date`等)。可以通过`WebDataBinder`(参见[`null`](#WebFlux-Ann-initbinder))或通过使用`FormattingConversionService`注册`Formatters`来定制类型转换(参见[Spring Field Formatting](core.html#format))。

类型转换中的一个实际问题是空字符串源值的处理。如果由于类型转换而使该值变为[Spring Field Formatting](core.html#format),则将其视为缺失。这可能是`Long``UUID`和其他目标类型的情况。如果要允许注入`null`,可以在参数注释上使用`required`标志,或者将参数声明为`null`

##### 矩阵变量

[Web MVC](web.html#mvc-ann-matrix-variables)

[RFC 3986](https://tools.ietf.org/html/rfc3986#section-3.3)讨论路径段中的名称-值对。在 Spring WebFlux 中,我们将那些称为基于 Tim Berners-Lee 的[“old post”](https://www.w3.org/DesignIssues/MatrixURIs.html)的“矩阵变量”,但它们也可以称为 URI 路径参数。

矩阵变量可以出现在任何路径段中,每个变量用分号分隔,多个值用逗号分隔——例如,`"/cars;color=red,green;year=2012"`。还可以通过重复的变量名指定多个值——例如,`"color=red;color=green;color=blue"`

Spring 与 MVC 不同,在 WebFlux 中,URL 中是否存在矩阵变量并不影响请求映射。换句话说,你不需要使用 URI 变量来屏蔽变量内容。也就是说,如果你想从控制器方法访问矩阵变量,则需要在期望矩阵变量的路径段中添加一个 URI 变量。下面的示例展示了如何做到这一点:

Java

```
// GET /pets/42;q=11;r=22

@GetMapping("/pets/{petId}")
public void findPet(@PathVariable String petId, @MatrixVariable int q) {

    // petId == 42
    // q == 11
}
```

Kotlin

```
// GET /pets/42;q=11;r=22

@GetMapping("/pets/{petId}")
fun findPet(@PathVariable petId: String, @MatrixVariable q: Int) {

    // petId == 42
    // q == 11
}
```

鉴于所有的路径段都可以包含矩阵变量,因此有时你可能需要消除矩阵变量预期在哪个路径变量中的歧义,如下例所示:

Java

```
// GET /owners/42;q=11/pets/21;q=22

@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
        @MatrixVariable(name="q", pathVar="ownerId") int q1,
        @MatrixVariable(name="q", pathVar="petId") int q2) {

    // q1 == 11
    // q2 == 22
}
```

Kotlin

```
@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(
        @MatrixVariable(name = "q", pathVar = "ownerId") q1: Int,
        @MatrixVariable(name = "q", pathVar = "petId") q2: Int) {

    // q1 == 11
    // q2 == 22
}
```

你可以定义一个可以定义为可选的矩阵变量,并指定一个默认值,如下例所示:

Java

```
// GET /pets/42

@GetMapping("/pets/{petId}")
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {

    // q == 1
}
```

Kotlin

```
// GET /pets/42

@GetMapping("/pets/{petId}")
fun findPet(@MatrixVariable(required = false, defaultValue = "1") q: Int) {

    // q == 1
}
```

要获取所有矩阵变量,请使用`MultiValueMap`,如下例所示:

Java

```
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23

@GetMapping("/owners/{ownerId}/pets/{petId}")
public void findPet(
        @MatrixVariable MultiValueMap<String, String> matrixVars,
        @MatrixVariable(pathVar="petId") MultiValueMap<String, String> petMatrixVars) {

    // matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
    // petMatrixVars: ["q" : 22, "s" : 23]
}
```

Kotlin

```
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23

@GetMapping("/owners/{ownerId}/pets/{petId}")
fun findPet(
        @MatrixVariable matrixVars: MultiValueMap<String, String>,
        @MatrixVariable(pathVar="petId") petMatrixVars: MultiValueMap<String, String>) {

    // matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
    // petMatrixVars: ["q" : 22, "s" : 23]
}
```

##### `@RequestParam`

[Web MVC](web.html#mvc-ann-requestparam)

可以使用`@RequestParam`注释将查询参数绑定到控制器中的方法参数。下面的代码片段显示了该用法:

Java

```
@Controller
@RequestMapping("/pets")
public class EditPetForm {

    // ...

    @GetMapping
    public String setupForm(@RequestParam("petId") int petId, Model model) { (1)
        Pet pet = this.clinic.loadPet(petId);
        model.addAttribute("pet", pet);
        return "petForm";
    }

    // ...
}
```

|**1**|使用`@RequestParam`。|
|-----|----------------------|

Kotlin

```
import org.springframework.ui.set

@Controller
@RequestMapping("/pets")
class EditPetForm {

    // ...

    @GetMapping
    fun setupForm(@RequestParam("petId") petId: Int, model: Model): String { (1)
        val pet = clinic.loadPet(petId)
        model["pet"] = pet
        return "petForm"
    }

    // ...
}
```

|**1**|使用`@RequestParam`。|
|-----|----------------------|

|   |Servlet API“Request Parameter”概念将查询参数、表单<br/>数据和多个部分合并为一个。然而,在 WebFlux 中,每个都可以通过`ServerWebExchange`单独访问。虽然`@RequestParam`仅绑定到查询参数,但你可以使用<br/>数据绑定来将查询参数、表单数据和多个部分应用到`@RequestParam`。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

默认情况下需要使用`@RequestParam`注释的方法参数,但是可以通过将`@RequestParam`的所需标志设置为`false`,或者通过使用`java.util.Optional`包装器声明参数来指定方法参数是可选的。

如果目标方法参数类型不是`String`,则自动应用类型转换。见[类型转换](#webflux-ann-typeconversion)

当在`Map<String, String>``MultiValueMap<String, String>`参数上声明<br/>注释时,映射将填充所有查询参数。

请注意,`@RequestParam`的使用是可选的——例如,用于设置其属性。默认情况下,任何参数是一个简单的值类型(由[Beanutils#IsSimpleProperty](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-)确定)且不是由任何其他参数解析器解析的,都将被视为用`@RequestParam`进行了注释。

##### `@RequestHeader`

[Web MVC](web.html#mvc-ann-requestheader)

可以使用`@RequestHeader`注释将请求头绑定到控制器中的方法参数。

下面的示例展示了一个带有标题的请求:

```
Host                    localhost:8080
Accept                  text/html,application/xhtml+xml,application/xml;q=0.9
Accept-Language         fr,en-gb;q=0.7,en;q=0.3
Accept-Encoding         gzip,deflate
Accept-Charset          ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive              300
```

下面的示例获取`Accept-Encoding``Keep-Alive`标题的值:

Java

```
@GetMapping("/demo")
public void handle(
        @RequestHeader("Accept-Encoding") String encoding, (1)
        @RequestHeader("Keep-Alive") long keepAlive) { (2)
    //...
}
```

|**1**|获取`Accept-Encoging`标头的值。|
|-----|----------------------------------------------|
|**2**|获取`Model`标头的值。|

Kotlin

```
@GetMapping("/demo")
fun handle(
        @RequestHeader("Accept-Encoding") encoding: String, (1)
        @RequestHeader("Keep-Alive") keepAlive: Long) { (2)
    //...
}
```

|**1**|获取`Accept-Encoging`标头的值。|
|-----|----------------------------------------------|
|**2**|获取`Keep-Alive`标头的值。|

如果目标方法参数类型不是`String`,则自动应用类型转换。见[类型转换](#webflux-ann-typeconversion)

当在`@RequestHeader``MultiValueMap<String, String>``HttpHeaders`参数上使用`@RequestHeader`注释时,映射将填充所有头值。

|   |内置支持用于将逗号分隔的字符串转换为<br/>数组或字符串集合或类型转换系统已知的其他类型。对于<br/>示例,用`@RequestHeader("Accept")`注释的方法参数可以是类型`String`但也可以是类型`String[]``List<String>`。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

##### `@CookieValue`

[Web MVC](web.html#mvc-ann-cookievalue)

可以使用`@CookieValue`注释将 HTTP cookie 的值绑定到控制器中的方法参数。

下面的示例显示了一个带有 cookie 的请求:

```
JSESSIONID=415A4AC178C59DACE0B2C9CA727CDD84
```

下面的代码示例演示了如何获得 Cookie 值:

爪哇

```
@GetMapping("/demo")
public void handle(@CookieValue("JSESSIONID") String cookie) { (1)
    //...
}
```

|**1**|获取 cookie 值。|
|-----|---------------------|

Kotlin

```
@GetMapping("/demo")
fun handle(@CookieValue("JSESSIONID") cookie: String) { (1)
    //...
}
```

|**1**|获取 cookie 值。|
|-----|---------------------|

如果目标方法参数类型不是`String`,则自动应用类型转换。见[类型转换](#webflux-ann-typeconversion)

##### `@ModelAttribute`

[Web MVC](web.html#mvc-ann-modelattrib-method-args)

你可以在方法参数上使用`@ModelAttribute`注释来访问模型中的一个属性,如果不存在,也可以实例化它。model 属性还覆盖了查询参数和表单字段的值,这些字段的名称与字段名称匹配。这被称为数据绑定,它使你不必处理解析和转换单个查询参数和窗体字段的问题。下面的示例绑定`Pet`的实例:

爪哇

```
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) { } (1)
```

|**1**|绑定`Pet`的实例。|
|-----|--------------------------|

Kotlin

```
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute pet: Pet): String { } (1)
```

|**1**|绑定`Pet`的实例。|
|-----|--------------------------|

前面示例中的`Pet`实例解析如下:

* 如果已经通过[`Model`](#Webflux-ann-modelattrib-methods)添加了该模型。

* 从 HTTP 会话到[`@SessionAttributes`](#WebFlux-Ann-SessionAttributes)。

* 从默认构造函数的调用。

* 调用带有匹配查询参数或表单字段的参数的“主构造函数”。参数名称是通过 爪哇Beans`@ConstructorProperties`或通过字节码中的运行时保留参数名称确定的。

在获得模型属性实例之后,再进行数据绑定。`WebExchangeDataBinder`类将查询参数和窗体字段的名称与目标`Object`上的字段名称匹配。在必要时应用类型转换后,将填充匹配字段。有关数据绑定(和验证)的更多信息,请参见[验证](core.html#validation)。有关自定义数据绑定的更多信息,请参见[`DataBinder`]。

数据绑定可能会导致错误。默认情况下,会引发`WebExchangeBindException`,但是,要检查控制器方法中的此类错误,可以在`BindingResult`旁边立即添加一个`BindingResult`参数,如下例所示:

爪哇

```
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { (1)
    if (result.hasErrors()) {
        return "petForm";
    }
    // ...
}
```

|**1**|添加`BindingResult`。|
|-----|-------------------------|

Kotlin

```
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
    if (result.hasErrors()) {
        return "petForm"
    }
    // ...
}
```

|**1**|添加`BindingResult`。|
|-----|-------------------------|

通过添加`javax.validation.Valid`注释或 Spring 的`@Validated`注释,可以在数据绑定后自动应用验证(另请参见[Bean Validation](core.html#validation-beanvalidation)[Spring validation](core.html#validation))。下面的示例使用`@Valid`注释:

爪哇

```
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { (1)
    if (result.hasErrors()) {
        return "petForm";
    }
    // ...
}
```

|**1**|在模型属性参数上使用`@Valid`。|
|-----|---------------------------------------------|

Kotlin

```
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { (1)
    if (result.hasErrors()) {
        return "petForm"
    }
    // ...
}
```

|**1**|在模型属性参数上使用`@Valid`。|
|-----|---------------------------------------------|

Spring 与 Spring MVC 不同,WebFlux 在模型中支持反应性类型——例如,`Mono<Account>``io.reactivex.Single<Account>`。你可以声明一个`@ModelAttribute`参数,带或不带反应性类型包装器,如果有必要,它将相应地解析为实际值。但是,请注意,要使用`BindingResult`参数,你必须在不使用反应式类型包装器的情况下声明`@ModelAttribute`参数,如前面所示。或者,你也可以通过 reactive 类型来处理任何错误,如下例所示:

爪哇

```
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public Mono<String> processSubmit(@Valid @ModelAttribute("pet") Mono<Pet> petMono) {
    return petMono
        .flatMap(pet -> {
            // ...
        })
        .onErrorResume(ex -> {
            // ...
        });
}
```

Kotlin

```
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@Valid @ModelAttribute("pet") petMono: Mono<Pet>): Mono<String> {
    return petMono
            .flatMap { pet ->
                // ...
            }
            .onErrorResume{ ex ->
                // ...
            }
}
```

请注意,`@ModelAttribute`的使用是可选的——例如,用于设置其属性。默认情况下,任何不是简单值类型(由[Beanutils#IsSimpleProperty](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-)确定)且未由任何其他参数解析器解析的参数都将被视为已用`@ModelAttribute`注释。

##### `@SessionAttributes`

[Web MVC](web.html#mvc-ann-sessionattributes)

`@SessionAttributes`用于在请求之间的`WebSession`中存储模型属性。它是一种类型级别的注释,用于声明特定控制器使用的会话属性。这通常会列出模型属性的名称或模型属性的类型,这些属性应该透明地存储在会话中,以供后续的访问请求使用。

考虑以下示例:

爪哇

```
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {
    // ...
}
```

|**1**|使用`@SessionAttributes`注释。|
|-----|------------------------------------------|

Kotlin

```
@Controller
@SessionAttributes("pet") (1)
class EditPetForm {
    // ...
}
```

|**1**|使用`@SessionAttributes`注释。|
|-----|------------------------------------------|

在第一个请求中,当将名称为`pet`的 model 属性添加到 model 时,它会自动升级到`WebSession`并保存在`WebSession`中。在另一个控制器方法使用`SessionStatus`方法参数来清除存储之前,它一直保持不变,如下例所示:

爪哇

```
@Controller
@SessionAttributes("pet") (1)
public class EditPetForm {

    // ...

    @PostMapping("/pets/{id}")
    public String handle(Pet pet, BindingResult errors, SessionStatus status) { (2)
        if (errors.hasErrors()) {
            // ...
        }
            status.setComplete();
            // ...
        }
    }
}
```

|**1**|使用`@SessionAttributes`注释。|
|-----|------------------------------------------|
|**2**|使用`SessionStatus`变量。|

Kotlin

```
@Controller
@SessionAttributes("pet") (1)
class EditPetForm {

    // ...

    @PostMapping("/pets/{id}")
    fun handle(pet: Pet, errors: BindingResult, status: SessionStatus): String { (2)
        if (errors.hasErrors()) {
            // ...
        }
        status.setComplete()
        // ...
    }
}
```

|**1**|使用`@SessionAttributes`注释。|
|-----|------------------------------------------|
|**2**|使用`SessionStatus`变量。|

##### `@SessionAttribute`

[Web MVC](web.html#mvc-ann-sessionattribute)

如果你需要访问已存在的会话属性,这些属性是全局管理的(也就是说,在控制器之外——例如,由过滤器管理),并且可能存在,也可能不存在,那么你可以在方法参数上使用`@SessionAttribute`注释,如下例所示:

爪哇

```
@GetMapping("/")
public String handle(@SessionAttribute User user) { (1)
    // ...
}
```

|**1**|使用`SessionStatus`。|
|-----|--------------------------|

Kotlin

```
@GetMapping("/")
fun handle(@SessionAttribute user: User): String { (1)
    // ...
}
```

|**1**|使用`@SessionAttribute`。|
|-----|--------------------------|

对于需要添加或删除会话属性的用例,可以考虑将`WebSession`注入到控制器方法中。

对于将会话中的模型属性临时存储为控制器工作流的一部分,可以考虑使用`SessionAttributes`,如[`@SessionAttributes`]中所述。

##### `@RequestAttribute`

[Web MVC](web.html#mvc-ann-requestattrib)

`@SessionAttribute`类似,你可以使用`@RequestAttribute`注释来访问先前创建的预先存在的请求属性(例如,通过`WebFilter`),如下例所示:

爪哇

```
@GetMapping("/")
public String handle(@RequestAttribute Client client) { (1)
    // ...
}
```

|**1**|使用`WebFilter`。|
|-----|--------------------------|

Kotlin

```
@GetMapping("/")
fun handle(@RequestAttribute client: Client): String { (1)
    // ...
}
```

|**1**|使用`@RequestAttribute`。|
|-----|--------------------------|

##### 多部分内容

[Web MVC](web.html#mvc-multipart-forms)

正如[多部分数据](#webflux-multipart)中所解释的,`ServerWebExchange`提供了对多部分内容的访问。在控制器中处理文件上载表单(例如,从浏览器)的最佳方法是通过数据绑定到[命令对象](#webflux-ann-modelattrib-method-args),如下例所示:

爪哇

```
class MyForm {

    private String name;

    private MultipartFile file;

    // ...

}

@Controller
public class FileUploadController {

    @PostMapping("/form")
    public String handleFormUpload(MyForm form, BindingResult errors) {
        // ...
    }

}
```

Kotlin

```
class MyForm(
        val name: String,
        val file: MultipartFile)

@Controller
class FileUploadController {

    @PostMapping("/form")
    fun handleFormUpload(form: MyForm, errors: BindingResult): String {
        // ...
    }

}
```

你还可以在 RESTful 服务场景中提交来自非浏览器客户端的多部分请求。下面的示例与 JSON 一起使用一个文件:

```
POST /someUrl
Content-Type: multipart/mixed

--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="meta-data"
Content-Type: application/json; charset=UTF-8
Content-Transfer-Encoding: 8bit

{
    "name": "value"
}
--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp
Content-Disposition: form-data; name="file-data"; filename="file.properties"
Content-Type: text/xml
Content-Transfer-Encoding: 8bit
... File Data ...
```

你可以使用`@RequestPart`访问单个部件,如下例所示:

爪哇

```
@PostMapping("/")
public String handle(@RequestPart("meta-data") Part metadata, (1)
        @RequestPart("file-data") FilePart file) { (2)
    // ...
}
```

|**1**|使用`@RequestPart`获取元数据。|
|-----|-----------------------------------------|
|**2**|使用`@RequestPart`获取文件。|

Kotlin

```
@PostMapping("/")
fun handle(@RequestPart("meta-data") Part metadata, (1)
        @RequestPart("file-data") FilePart file): String { (2)
    // ...
}
```

|**1**|使用`@RequestPart`获取元数据。|
|-----|-----------------------------------------|
|**2**|使用`@RequestPart`获取文件。|

要反序列化 RAW Part 内容(例如,到 JSON——类似于`@RequestBody`),你可以声明一个具体的目标`Object`,而不是[多部分数据](#webflux-multipart),如下例所示:

爪哇

```
@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata) { (1)
    // ...
}
```

|**1**|使用`@RequestPart`获取元数据。|
|-----|-----------------------------------------|

Kotlin

```
@PostMapping("/")
fun handle(@RequestPart("meta-data") metadata: MetaData): String { (1)
    // ...
}
```

|**1**|使用`@RequestPart`获取元数据。|
|-----|-----------------------------------------|

你可以将`@RequestPart``javax.validation.Valid`或 Spring 的`@Validated`注释结合使用,这将导致应用标准 Bean 验证。验证错误会导致`WebExchangeBindException`,从而导致 400(bad\_request)响应。异常包含带有错误详细信息的`BindingResult`,也可以通过使用异步包装器声明参数并使用与错误相关的操作符来在 Controller 方法中进行处理:

爪哇

```
@PostMapping("/")
public String handle(@Valid @RequestPart("meta-data") Mono<MetaData> metadata) {
    // use one of the onError* operators...
}
```

Kotlin

```
@PostMapping("/")
fun handle(@Valid @RequestPart("meta-data") metadata: MetaData): String {
    // ...
}
```

要以`MultiValueMap`的形式访问所有多部分数据,可以使用`@RequestBody`,如下例所示:

爪哇

```
@PostMapping("/")
public String handle(@RequestBody Mono<MultiValueMap<String, Part>> parts) { (1)
    // ...
}
```

|**1**|使用`@RequestBody`。|
|-----|---------------------|

Kotlin

```
@PostMapping("/")
fun handle(@RequestBody parts: MultiValueMap<String, Part>): String { (1)
    // ...
}
```

|**1**|使用`@RequestMapping`。|
|-----|---------------------|

要按顺序访问多部分数据,在流式方式中,你可以使用`@RequestBody``Flux<Part>`(或`Flow<Part>`在 Kotlin 中)代替,如下例所示:

爪哇

```
@PostMapping("/")
public String handle(@RequestBody Flux<Part> parts) { (1)
    // ...
}
```

|**1**|使用`@RequestBody`。|
|-----|---------------------|

Kotlin

```
@PostMapping("/")
fun handle(@RequestBody parts: Flow<Part>): String { (1)
    // ...
}
```

|**1**|使用`@RequestBody`。|
|-----|---------------------|

##### `@RequestBody`

[Web MVC](web.html#mvc-ann-requestbody)

你可以使用`@RequestBody`注释,通过[HttpMessageReader](#webflux-codecs)将请求主体读取并反序列化为`Object`。下面的示例使用了`@RequestBody`参数:

爪哇

```
@PostMapping("/accounts")
public void handle(@RequestBody Account account) {
    // ...
}
```

Kotlin

```
@PostMapping("/accounts")
fun handle(@RequestBody account: Account) {
    // ...
}
```

与 Spring MVC 不同,在 WebFlux 中,`@RequestBody`方法参数支持反应性类型和完全非阻塞的读取和(客户机到服务器)流。

爪哇

```
@PostMapping("/accounts")
public void handle(@RequestBody Mono<Account> account) {
    // ...
}
```

Kotlin

```
@PostMapping("/accounts")
fun handle(@RequestBody accounts: Flow<Account>) {
    // ...
}
```

你可以使用[WebFlux 配置](#webflux-config)[HTTP 消息编解码器](#webflux-config-message-codecs)选项来配置或自定义消息阅读器。

你可以将`@RequestBody``javax.validation.Valid`或 Spring 的[Web MVC](web.html#mvc-ann-modelattrib-method-args)注释结合使用,这将导致应用标准 Bean 验证。验证错误会导致`WebExchangeBindException`,从而导致 400(bad\_request)响应。异常包含带有错误详细信息的`BindingResult`,可以通过使用异步包装器声明参数,然后使用与错误相关的操作符,在 Controller 方法中进行处理:

爪哇

```
@PostMapping("/accounts")
public void handle(@Valid @RequestBody Mono<Account> account) {
    // use one of the onError* operators...
}
```

Kotlin

```
@PostMapping("/accounts")
fun handle(@Valid @RequestBody account: Mono<Account>) {
    // ...
}
```

##### `HttpEntity`

[Web MVC](web.html#mvc-ann-httpentity)

`HttpEntity`与使用[`@RequestBody`](#WebFlux-Ann-RequestBody)或多或少相同,但它基于一个容器对象,该对象公开了请求头和主体。下面的示例使用`HttpEntity`:

爪哇

```
@PostMapping("/accounts")
public void handle(HttpEntity<Account> entity) {
    // ...
}
```

Kotlin

```
@PostMapping("/accounts")
fun handle(entity: HttpEntity<Account>) {
    // ...
}
```

##### `@ResponseBody`

[Web MVC](web.html#mvc-ann-responsebody)

你可以在方法上使用`@ResponseBody`注释,通过[HttpMessageWriter](#webflux-codecs)将返回序列化到响应主体。下面的示例展示了如何做到这一点:

爪哇

```
@GetMapping("/accounts/{id}")
@ResponseBody
public Account handle() {
    // ...
}
```

Kotlin

```
@GetMapping("/accounts/{id}")
@ResponseBody
fun handle(): Account {
    // ...
}
```

`@ResponseBody`在类级别上也受到支持,在这种情况下,所有控制器方法都会继承它。这是`@RestController`的效果,它不过是一个标记有`@Controller``@ResponseBody`的元注释。

`@ResponseBody`支持反应类型,这意味着你可以返回 reactor 或 rxjava 类型,并将它们产生的异步值呈现给响应。有关更多详细信息,请参见[Streaming](#webflux-codecs-streaming)[JSON 渲染](#webflux-codecs-jackson)

你可以将`@ResponseBody`方法与 JSON 序列化视图合并。详见[JacksonJSON](#webflux-ann-jackson)

可以使用[WebFlux 配置](#webflux-config)[HTTP 消息编解码器](#webflux-config-message-codecs)选项来配置或自定义消息写入。

##### `ResponseEntity`

[Web MVC](web.html#mvc-ann-responseentity)

`ResponseEntity`类似于[`@ResponseBody`](#WebFlux-Ann-ResponseBody),但具有状态和标题。例如:

爪哇

```
@GetMapping("/something")
public ResponseEntity<String> handle() {
    String body = ... ;
    String etag = ... ;
    return ResponseEntity.ok().eTag(etag).build(body);
}
```

Kotlin

```
@GetMapping("/something")
fun handle(): ResponseEntity<String> {
    val body: String = ...
    val etag: String = ...
    return ResponseEntity.ok().eTag(etag).build(body)
}
```

WebFlux 支持使用单个值[反应型](#webflux-reactive-libraries)异步地生成`ResponseEntity`,和/或为主体生成单个值和多值的反应类型。这允许使用`ResponseEntity`的各种异步响应,如下所示:

* `ResponseEntity<Mono<T>>``ResponseEntity<Flux<T>>`在稍后异步提供主体时,立即使响应状态和头为已知。如果主体由 0.1 个值组成,则使用`Mono`;如果可以产生多个值,则使用`Flux`

* `Mono<ResponseEntity<T>>`在稍后的时间点异步提供了所有这三个方面——响应状态、头和主体。这允许响应状态和头根据异步请求处理的结果而变化。

* `Mono<ResponseEntity<Mono<T>>>``Mono<ResponseEntity<Flux<T>>>`是另一种可能的选择,尽管不太常见。它们首先异步地提供响应状态和报头,然后是响应主体,也是异步地提供响应主体。

##### JacksonJSON

Spring 提供对 JacksonJSON 库的支持。

##### JSON 视图 #

[Web MVC](web.html#mvc-ann-jackson)

Spring WebFlux 提供了对[Jackson 的序列化视图](https://www.baeldung.com/jackson-json-view-annotation)的内置支持,它只允许呈现`Object`中所有字段的一个子集。要与`@ResponseBody``@ResponseBody`控制器方法一起使用它,你可以使用 Jackson 的`@JsonView`注释来激活序列化视图类,如下例所示:

爪哇

```
@RestController
public class UserController {

    @GetMapping("/user")
    @JsonView(User.WithoutPasswordView.class)
    public User getUser() {
        return new User("eric", "7!jd#h23");
    }
}

public class User {

    public interface WithoutPasswordView {};
    public interface WithPasswordView extends WithoutPasswordView {};

    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;
    }
}
```

Kotlin

```
@RestController
class UserController {

    @GetMapping("/user")
    @JsonView(User.WithoutPasswordView::class)
    fun getUser(): User {
        return User("eric", "7!jd#h23")
    }
}

class User(
        @JsonView(WithoutPasswordView::class) val username: String,
        @JsonView(WithPasswordView::class) val password: String
) {
    interface WithoutPasswordView
    interface WithPasswordView : WithoutPasswordView
}
```

|   |<br/>允许一个视图类的数组,但是每个<br/>控制器方法只能指定一个。如果需要激活多个视图,请使用复合接口。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

#### 1.4.4.`Model`

[Web MVC](web.html#mvc-ann-modelattrib-methods)

你可以使用`@ModelAttribute`注释:

*[方法参数](#webflux-ann-modelattrib-method-args)中的`@RequestMapping`方法上创建或访问模型中的对象,并通过`WebDataBinder`将其绑定到请求。

* 作为`@Controller``@ModelAttribute`类中的方法级注释,在任何`@RequestMapping`方法调用之前帮助初始化模型。

*`@RequestMapping`方法上将其返回值标记为模型属性。

本节讨论`@ModelAttribute`方法,或者前面列表中的第二个项。控制器可以有任意数量的`@ModelAttribute`方法。所有这些方法都是在同一个控制器中的`@RequestMapping`方法之前调用的。还可以通过`@ControllerAdvice`在控制器之间共享`@ModelAttribute`方法。有关更多详细信息,请参见[财务总监建议](#webflux-ann-controller-advice)一节。

`@ModelAttribute`方法具有灵活的方法签名。它们支持许多与`@RequestMapping`方法相同的参数(除了`@ModelAttribute`本身和与请求主体相关的任何参数)。

下面的示例使用`@ModelAttribute`方法:

爪哇

```
@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
    model.addAttribute(accountRepository.findAccount(number));
    // add more ...
}
```

Kotlin

```
@ModelAttribute
fun populateModel(@RequestParam number: String, model: Model) {
    model.addAttribute(accountRepository.findAccount(number))
    // add more ...
}
```

下面的示例只添加了一个属性:

爪哇

```
@ModelAttribute
public Account addAccount(@RequestParam String number) {
    return accountRepository.findAccount(number);
}
```

Kotlin

```
@ModelAttribute
fun addAccount(@RequestParam number: String): Account {
    return accountRepository.findAccount(number);
}
```

|   |当未显式指定名称时,默认的名称是基于类型选择的,<br/>,正如在 爪哇doc 中对[`Conventions`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/core/core/conventions.html)所解释的那样。<br/>通过重载的`addAttribute`方法或`addAttribute`方法或<br/>(用于返回值)上的 name 属性,始终可以指定一个显式名称。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

Spring WebFlux 与 Spring MVC 不同,在模型中显式地支持反应类型(例如,或)。这样的异步模型属性可以在`@RequestMapping`调用时透明地解析(并更新模型)到它们的实际值,只要不使用包装器声明`@ModelAttribute`参数,如下例所示:

爪哇

```
@ModelAttribute
public void addAccount(@RequestParam String number) {
    Mono<Account> accountMono = accountRepository.findAccount(number);
    model.addAttribute("account", accountMono);
}

@PostMapping("/accounts")
public String handle(@ModelAttribute Account account, BindingResult errors) {
    // ...
}
```

Kotlin

```
import org.springframework.ui.set

@ModelAttribute
fun addAccount(@RequestParam number: String) {
    val accountMono: Mono<Account> = accountRepository.findAccount(number)
    model["account"] = accountMono
}

@PostMapping("/accounts")
fun handle(@ModelAttribute account: Account, errors: BindingResult): String {
    // ...
}
```

此外,在视图呈现之前,具有反应性类型包装器的任何模型属性都将被解析为它们的实际值(以及模型更新)。

你还可以使用`@ModelAttribute`作为`@RequestMapping`方法的方法级注释,在这种情况下,`@RequestMapping`方法的返回值被解释为一个模型属性。这通常不是必需的,因为这是 HTML 控制器中的默认行为,除非返回值是`String`,否则该返回值将被解释为视图名称。`@RequestBody`还可以帮助自定义模型属性名,如下例所示:

爪哇

```
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
public Account handle() {
    // ...
    return account;
}
```

Kotlin

```
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
fun handle(): Account {
    // ...
    return account
}
```

#### 1.4.5.`DataBinder`

[Web MVC](web.html#mvc-ann-initbinder)

`@Controller``@ControllerAdvice`类可以具有`@InitBinder`方法,以初始化`WebDataBinder`的实例。这些反过来又被用来:

* 将请求参数(即表单数据或查询)绑定到模型对象。

* 将基于`String`的请求值(例如请求参数、路径变量、头、cookie 和其他)转换为控制器方法参数的目标类型。

* 在呈现 HTML 窗体时,将模型对象值格式化为`String`值。

`@InitBinder`方法可以注册控制器特定的`java.beans.PropertyEditor`或 Spring `Converter``Formatter`组件。此外,可以使用[WebFlux 爪哇 配置](#webflux-config-conversion)在全局共享的`FormattingConversionService`中注册`Converter``Formatter`类型。

`@InitBinder`方法支持许多与`@RequestMapping`方法相同的参数,但`@ModelAttribute`(命令对象)参数除外。通常,它们是用`WebDataBinder`参数声明的,用于注册,并使用`void`返回值。下面的示例使用`@InitBinder`注释:

爪哇

```
@Controller
public class FormController {

    @InitBinder (1)
    public void initBinder(WebDataBinder binder) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        dateFormat.setLenient(false);
        binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
    }

    // ...
}
```

|**1**|使用`@InitBinder`注释。|
|-----|-----------------------------------|

Kotlin

```
@Controller
class FormController {

    @InitBinder (1)
    fun initBinder(binder: WebDataBinder) {
        val dateFormat = SimpleDateFormat("yyyy-MM-dd")
        dateFormat.isLenient = false
        binder.registerCustomEditor(Date::class.java, CustomDateEditor(dateFormat, false))
    }

    // ...
}
```

或者,当通过共享的`FormattingConversionService`使用基于`Formatter`的设置时,你可以重新使用相同的方法并注册特定于控制器的`Formatter`实例,如下例所示:

爪哇

```
@Controller
public class FormController {

    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd")); (1)
    }

    // ...
}
```

|**1**|添加自定义格式化程序(在本例中为`DateFormatter`)。|
|-----|------------------------------------------------------------|

Kotlin

```
@Controller
class FormController {

    @InitBinder
    fun initBinder(binder: WebDataBinder) {
        binder.addCustomFormatter(DateFormatter("yyyy-MM-dd")) (1)
    }

    // ...
}
```

|**1**|添加自定义格式化程序(在本例中为`DateFormatter`)。|
|-----|------------------------------------------------------------|

#### 1.4.6.管理异常

[Web MVC](web.html#mvc-ann-exceptionhandler)

`@Controller`[@controlleradvice](#webflux-ann-controller-advice)类可以使用`@ExceptionHandler`方法来处理来自控制器方法的异常。以下示例包括这样的处理程序方法:

爪哇

```
@Controller
public class SimpleController {

    // ...

    @ExceptionHandler (1)
    public ResponseEntity<String> handle(IOException ex) {
        // ...
    }
}
```

|**1**|声明`@ExceptionHandler`。|
|-----|---------------------------------|

Kotlin

```
@Controller
class SimpleController {

    // ...

    @ExceptionHandler (1)
    fun handle(ex: IOException): ResponseEntity<String> {
        // ...
    }
}
```

|**1**|声明`@ExceptionHandler`。|
|-----|---------------------------------|

异常可以与正在传播的顶级异常(即抛出的直接`IOException`)匹配,也可以与顶级包装异常中的直接原因匹配(例如,在`IOException`中包装的`IllegalStateException`)。

为了匹配异常类型,最好将目标异常声明为方法参数,如前面的示例所示。或者,注释声明可以缩小异常类型以进行匹配。我们通常建议在参数签名中尽可能具体,并在`@ControllerAdvice`上以相应的顺序优先声明主根异常映射。详见[MVC 部门](web.html#mvc-ann-exceptionhandler)

|   |WebFlux 中的`@ExceptionHandler`方法支持与<br/>方法相同的方法参数和<br/>返回值,但请求主体-<br/>`@ModelAttribute`-相关的方法参数除外。|
|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

Spring WebFlux 中对`@ExceptionHandler`方法的支持是由`HandlerAdapter`方法提供的。有关更多详细信息,请参见[`DispatcherHandler`]。

##### REST API 异常

[Web MVC](web.html#mvc-ann-rest-exceptions)

REST 服务的一个常见要求是在响应主体中包含错误详细信息。 Spring 框架不会自动这样做,因为响应主体中的错误细节的表示是特定于应用程序的。但是,`@RestController`可以使用带有`ResponseEntity`返回值的`@ExceptionHandler`方法来设置响应的状态和主体。这样的方法也可以在`@ControllerAdvice`类中声明,以在全局范围内应用它们。

|   |请注意, Spring WebFlux 对于 Spring MVC没有一个等价的,因为 WebFlux 仅引发(或其子类),并且这些不需要被翻译为一个 HTTP 状态代码。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

#### 1.4.7.财务总监建议

[Web MVC](web.html#mvc-ann-controller-advice)

通常,`@ExceptionHandler``@InitBinder``@ModelAttribute`方法应用于声明它们的`@Controller`类(或类层次结构)中。如果你希望这样的方法在全局范围内(在控制器之间)应用得更多,那么可以在一个用`@ControllerAdvice``@RestControllerAdvice`注释的类中声明它们。

`@ControllerAdvice`注释为`@Component`,这意味着这样的类可以通过[组件扫描](core.html#beans-java-instantiating-container-scan)注册为 Spring bean。`@RestControllerAdvice`是一种组合注释,它同时使用`@ControllerAdvice``@ResponseBody`进行注释,其本质上意味着`@ExceptionHandler`方法通过消息转换(与视图解析或模板呈现)呈现到响应主体。

在启动时,`@RequestMapping``@ExceptionHandler`方法的基础设施类检测用`@ControllerAdvice`注释的 Spring bean,然后在运行时应用它们的方法。全局`@ExceptionHandler`方法(来自`@ControllerAdvice`)应用于*之后*局部方法(来自`@Controller`)。相比之下,全局`@ModelAttribute``@InitBinder`方法应用于局部方法*在此之前*

默认情况下,`@ControllerAdvice`方法适用于每个请求(即所有控制器),但你可以通过使用注释上的属性将其缩小到控制器的子集,如下例所示:

爪哇

```
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}

// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}

// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}
```

Kotlin

```
// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = [RestController::class])
public class ExampleAdvice1 {}

// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}

// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = [ControllerInterface::class, AbstractController::class])
public class ExampleAdvice3 {}
```

前面示例中的选择器是在运行时进行评估的,如果广泛使用,可能会对性能产生负面影响。有关更多详细信息,请参见[`@ControllerAdvice`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/bind/annotation/controlleradvice.html)爪哇doc。

### 1.5.功能端点

[Web MVC](web.html#webmvc-fn)

Spring WebFlux 包括 WebFlux.FN,这是一种轻量级函数式编程模型,其中函数被用于路由和处理请求,并且契约被设计为具有不可变性。它是基于注释的编程模型的一种替代方案,但在其他情况下运行在相同的[反应核](#webflux-reactive-spring-web)基础上。

#### 1.5.1.概述

[Web MVC](web.html#webmvc-fn-overview)

在 WebFlux.FN 中,一个 HTTP 请求是用`HandlerFunction`处理的:一个函数接受`ServerRequest`并返回一个延迟的`ServerResponse`(即`Mono<ServerResponse>`)。请求和响应对象都具有不可更改的契约,这些契约提供对 HTTP 请求和响应的 JDK8 友好访问。`HandlerFunction`相当于基于注释的编程模型中的`@RequestMapping`方法的主体。

传入的请求被路由到带有`RouterFunction`的处理程序函数:该函数接受`ServerRequest`并返回延迟的`HandlerFunction`(即`Mono<HandlerFunction>`)。当路由器函数匹配时,将返回一个处理程序函数;否则将返回一个空的 mono。`RouterFunction`相当于`@RequestMapping`注释,但与此的主要区别是,路由器函数不仅提供数据,还提供行为。

`RouterFunctions.route()`提供了一个路由器构建器,可以促进路由器的创建,如下例所示:

爪哇

```
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);

RouterFunction<ServerResponse> route = route()
    .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson)
    .GET("/person", accept(APPLICATION_JSON), handler::listPeople)
    .POST("/person", handler::createPerson)
    .build();

public class PersonHandler {

    // ...

    public Mono<ServerResponse> listPeople(ServerRequest request) {
        // ...
    }

    public Mono<ServerResponse> createPerson(ServerRequest request) {
        // ...
    }

    public Mono<ServerResponse> getPerson(ServerRequest request) {
        // ...
    }
}
```

Kotlin

```
val repository: PersonRepository = ...
val handler = PersonHandler(repository)

val route = coRouter { (1)
    accept(APPLICATION_JSON).nest {
        GET("/person/{id}", handler::getPerson)
        GET("/person", handler::listPeople)
    }
    POST("/person", handler::createPerson)
}

class PersonHandler(private val repository: PersonRepository) {

    // ...

    suspend fun listPeople(request: ServerRequest): ServerResponse {
        // ...
    }

    suspend fun createPerson(request: ServerRequest): ServerResponse {
        // ...
    }

    suspend fun getPerson(request: ServerRequest): ServerResponse {
        // ...
    }
}
```

|**1**|使用协程路由器 DSL 创建路由器,还可以通过`router { }`提供反应式替代方案。|
|-----|-----------------------------------------------------------------------------------------------------|

运行`RouterFunction`的一种方法是将其转换为`HttpHandler`,并通过一个内置的[服务器适配器](#webflux-httphandler)安装它:

* `RouterFunctions.toHttpHandler(RouterFunction)`

* `RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)`

大多数应用程序都可以通过 WebFlux 爪哇 配置运行,参见[运行服务器](#webflux-fn-running)

#### 1.5.2.handlerfunction

[Web MVC](web.html#webmvc-fn-handler-functions)

`ServerRequest``ServerResponse`是不可变的接口,它们提供对 HTTP 请求和响应的 JDK8 友好访问。请求和响应都针对体流提供[反应流](https://www.reactive-streams.org)反压。请求主体用反应器`Flux``Mono`表示。响应体用任何反应流`Publisher`表示,包括`Flux``Mono`。有关该问题的更多信息,请参见[反应库](#webflux-reactive-libraries)

##### ServerRequest

`ServerRequest`提供对 HTTP 方法、URI、标头和查询参数的访问,而对正文的访问是通过`body`方法提供的。

下面的示例将请求主体提取到`Mono<String>`:

爪哇

```
Mono<String> string = request.bodyToMono(String.class);
```

Kotlin

```
val string = request.awaitBody<String>()
```

下面的示例将主体提取到`Flux<Person>`(或 Kotlin 中的`Flow<Person>`),其中`Person`对象是从形式化的形式(例如 JSON 或 XML)中解码的:

爪哇

```
Flux<Person> people = request.bodyToFlux(Person.class);
```

Kotlin

```
val people = request.bodyToFlow<Person>()
```

前面的示例是使用更通用的`ServerRequest.body(BodyExtractor)`的快捷方式,它接受`BodyExtractor`功能策略接口。实用程序类`BodyExtractors`提供了对许多实例的访问。例如,前面的示例也可以写如下:

爪哇

```
Mono<String> string = request.body(BodyExtractors.toMono(String.class));
Flux<Person> people = request.body(BodyExtractors.toFlux(Person.class));
```

Kotlin

```
    val string = request.body(BodyExtractors.toMono(String::class.java)).awaitSingle()
    val people = request.body(BodyExtractors.toFlux(Person::class.java)).asFlow()
```

下面的示例展示了如何访问表单数据:

爪哇

```
Mono<MultiValueMap<String, String>> map = request.formData();
```

Kotlin

```
val map = request.awaitFormData()
```

下面的示例展示了如何以地图的形式访问多部分数据:

爪哇

```
Mono<MultiValueMap<String, Part>> map = request.multipartData();
```

Kotlin

```
val map = request.awaitMultipartData()
```

下面的示例展示了如何以流媒体方式一次访问多个部分:

爪哇

```
Flux<Part> parts = request.body(BodyExtractors.toParts());
```

Kotlin

```
val parts = request.body(BodyExtractors.toParts()).asFlow()
```

##### ServerResponse

`ServerResponse`提供对 HTTP 响应的访问,由于它是不可变的,你可以使用`build`方法来创建它。你可以使用构建器设置响应状态、添加响应头或提供主体。下面的示例使用 JSON 内容创建一个 200(OK)响应:

爪哇

```
Mono<Person> person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class);
```

Kotlin

```
val person: Person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(person)
```

下面的示例展示了如何使用`Location`标头构建 201(已创建)响应,而不使用正文:

爪哇

```
URI location = ...
ServerResponse.created(location).build();
```

Kotlin

```
val location: URI = ...
ServerResponse.created(location).build()
```

根据使用的编解码器,可以传递提示参数来自定义如何序列化或反序列化主体。例如,要指定[JacksonJSON 视图](https://www.baeldung.com/jackson-json-view-annotation):

爪哇

```
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
```

Kotlin

```
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...)
```

##### 处理程序类

我们可以将处理程序函数编写为 lambda,如下例所示:

爪哇

```
HandlerFunction<ServerResponse> helloWorld =
  request -> ServerResponse.ok().bodyValue("Hello World");
```

Kotlin

```
val helloWorld = HandlerFunction<ServerResponse> { ServerResponse.ok().bodyValue("Hello World") }
```

这很方便,但在一个应用程序中,我们需要多个功能,而多个内联 lambda 可能会变得混乱。因此,将相关的处理程序函数组合成一个处理程序类是有用的,该处理程序类在基于注释的应用程序中具有与`@Controller`类似的作用。例如,下面的类公开了一个反应性`Person`存储库:

爪哇

```
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;

public class PersonHandler {

    private final PersonRepository repository;

    public PersonHandler(PersonRepository repository) {
        this.repository = repository;
    }

    public Mono<ServerResponse> listPeople(ServerRequest request) { (1)
        Flux<Person> people = repository.allPeople();
        return ok().contentType(APPLICATION_JSON).body(people, Person.class);
    }

    public Mono<ServerResponse> createPerson(ServerRequest request) { (2)
        Mono<Person> person = request.bodyToMono(Person.class);
        return ok().build(repository.savePerson(person));
    }

    public Mono<ServerResponse> getPerson(ServerRequest request) { (3)
        int personId = Integer.valueOf(request.pathVariable("id"));
        return repository.getPerson(personId)
            .flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person))
            .switchIfEmpty(ServerResponse.notFound().build());
    }
}
```

|**1**|`listPeople`是一个处理函数,它将存储库中找到的所有`Person`对象作为<br/>json 返回。|
|-----|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|**2**|`createPerson`是一个处理函数,它存储了一个包含在请求主体中的新`Person`<br/>注意,`PersonRepository.savePerson(Person)`返回`Mono<Void>`:一个空的`Mono`,当该人已从请求中读取并存储时,它会发出<br/>完成信号。因此,我们使用`build(Publisher<Void>)`方法在接收到完成信号时(即<br/>保存了`Person`时)发送响应。|
|**3**|`getPerson`是一个处理函数,它返回一个人,由`id`路径<br/>变量标识。我们从存储库中检索`Person`并创建一个 JSON 响应,如果找到了<br/>。如果没有找到它,我们使用`switchIfEmpty(Mono<T>)`返回 404Not Found 响应。|

Kotlin

```
class PersonHandler(private val repository: PersonRepository) {

    suspend fun listPeople(request: ServerRequest): ServerResponse { (1)
        val people: Flow<Person> = repository.allPeople()
        return ok().contentType(APPLICATION_JSON).bodyAndAwait(people);
    }

    suspend fun createPerson(request: ServerRequest): ServerResponse { (2)
        val person = request.awaitBody<Person>()
        repository.savePerson(person)
        return ok().buildAndAwait()
    }

    suspend fun getPerson(request: ServerRequest): ServerResponse { (3)
        val personId = request.pathVariable("id").toInt()
        return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).bodyValueAndAwait(it) }
                ?: ServerResponse.notFound().buildAndAwait()

    }
}
```

|**1**|`listPeople`是一个处理函数,它将存储库中找到的所有`Person`对象作为<br/>json 返回。|
|-----|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|**2**|`createPerson`是一个处理函数,它存储了一个包含在请求主体中的新`Person`<br/>注意,`PersonRepository.savePerson(Person)`是一个没有返回类型的挂起函数。|
|**3**|`getPerson`是一个处理函数,它返回一个人,由`id`路径<br/>变量标识。我们从存储库中检索`Person`并创建一个 JSON 响应,如果找到了<br/>。如果没有找到它,我们将返回 404Not Found 响应。|

##### Validation

功能端点可以使用 Spring 的[验证设施](core.html#validation)将验证应用于请求主体。例如,给定用于`Person`的自定义 Spring [Validator](core.html#validation)实现:

爪哇

```
public class PersonHandler {

    private final Validator validator = new PersonValidator(); (1)

    // ...

    public Mono<ServerResponse> createPerson(ServerRequest request) {
        Mono<Person> person = request.bodyToMono(Person.class).doOnNext(this::validate); (2)
        return ok().build(repository.savePerson(person));
    }

    private void validate(Person person) {
        Errors errors = new BeanPropertyBindingResult(person, "person");
        validator.validate(person, errors);
        if (errors.hasErrors()) {
            throw new ServerWebInputException(errors.toString()); (3)
        }
    }
}
```

|**1**|创建`Validator`实例。|
|-----|-----------------------------------|
|**2**|应用验证。|
|**3**|提出 400 响应的例外情况。|

Kotlin

```
class PersonHandler(private val repository: PersonRepository) {

    private val validator = PersonValidator() (1)

    // ...

    suspend fun createPerson(request: ServerRequest): ServerResponse {
        val person = request.awaitBody<Person>()
        validate(person) (2)
        repository.savePerson(person)
        return ok().buildAndAwait()
    }

    private fun validate(person: Person) {
        val errors: Errors = BeanPropertyBindingResult(person, "person");
        validator.validate(person, errors);
        if (errors.hasErrors()) {
            throw ServerWebInputException(errors.toString()) (3)
        }
    }
}
```

|**1**|创建`Validator`实例。|
|-----|-----------------------------------|
|**2**|应用验证。|
|**3**|提出 400 响应的例外情况。|

处理程序还可以通过基于`LocalValidatorFactoryBean`创建和注入一个全局`Validator`实例来使用标准 Bean 验证 API(JSR-303)。见[Spring Validation](core.html#validation-beanvalidation)

#### 1.5.3.`RouterFunction`

[Web MVC](web.html#webmvc-fn-router-functions)

路由器函数用于将请求路由到相应的`HandlerFunction`。通常,你不会自己编写路由器函数,而是使用`RouterFunctions`实用程序类上的一个方法来创建一个。`RouterFunctions.route()`(无参数)为你提供了用于创建路由器函数的 Fluent 构建器,而`RouterFunctions.route(RequestPredicate, HandlerFunction)`提供了一种直接创建路由器的方法。

通常,建议使用`route()`Builder,因为它为典型的映射场景提供了方便的快捷方式,而不需要很难发现的静态导入。例如,Router Function Builder 提供了方法`GET(String, HandlerFunction)`来创建 GET 请求的映射;以及`POST(String, HandlerFunction)`用于 POST。

除了基于 HTTP 方法的映射,Route Builder 还提供了一种在映射到请求时引入额外谓词的方法。对于每个 HTTP 方法,都有一个重载变量,该变量将`RequestPredicate`作为参数,尽管可以表示该参数的附加约束。

##### 谓词

你可以编写自己的`RequestPredicate`,但是`RequestPredicates`实用程序类提供了基于请求路径、HTTP 方法、Content-type 等的常用实现。下面的示例使用一个请求谓词来基于`Accept`头创建一个约束:

爪哇

```
RouterFunction<ServerResponse> route = RouterFunctions.route()
    .GET("/hello-world", accept(MediaType.TEXT_PLAIN),
        request -> ServerResponse.ok().bodyValue("Hello World")).build();
```

Kotlin

```
val route = coRouter {
    GET("/hello-world", accept(TEXT_PLAIN)) {
        ServerResponse.ok().bodyValueAndAwait("Hello World")
    }
}
```

你可以使用以下方法将多个请求谓词组合在一起:

* `RequestPredicate.and(RequestPredicate)`—两者必须匹配。

* `RequestPredicate.or(RequestPredicate)`—两者都可以匹配。

来自`RequestPredicates`的许多谓词都是组成的。例如,`RequestPredicates.GET(String)`是由`RequestPredicates.method(HttpMethod)``RequestPredicates.path(String)`组成的。上面显示的示例还使用两个请求谓词,因为构建器在内部使用`RequestPredicates.GET`,并将其与`accept`谓词组合在一起。

##### 路线

对路由器的功能按顺序进行评估:如果第一条路由不匹配,则对第二条路由进行评估,依此类推。因此,在一般路线之前声明更具体的路线是有意义的。当将路由器功能注册为 Spring bean 时,这一点也很重要,后面将对此进行说明。请注意,这种行为与基于注释的编程模型不同,在该模型中,“最特定的”控制器方法是自动选择的。

当使用 Router 函数 builder 时,所有定义的路由都被组合成一个`RouterFunction`,从`build()`返回。还有其他方法可以将多个路由器功能组合在一起:

* `add(RouterFunction)`上的`RouterFunctions.route()`构建器

* `RouterFunction.and(RouterFunction)`

* `RouterFunction.andRoute(RequestPredicate, HandlerFunction)`—带有嵌套`RouterFunctions.route()``RouterFunction.and()`的快捷方式。

下面的示例显示了四条路线的组成:

爪哇

```
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;

PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);

RouterFunction<ServerResponse> otherRoute = ...

RouterFunction<ServerResponse> route = route()
    .GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)
    .GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)
    .POST("/person", handler::createPerson) (3)
    .add(otherRoute) (4)
    .build();
```

|**1**|带有与 JSON 匹配的`Accept`标头的`GET /person/{id}`被路由到`PersonHandler.getPerson`|
|-----|--------------------------------------------------------------------------------------------------|
|**2**|带有与 JSON 匹配的`Accept`头的`GET /person`被路由到`PersonHandler.listPeople`|
|**3**|没有附加谓词的`POST /person`映射到`PersonHandler.createPerson`,并且|
|**4**|`otherRoute`是在其他地方创建的路由器功能,并将其添加到构建的路由中。|

Kotlin

```
import org.springframework.http.MediaType.APPLICATION_JSON

val repository: PersonRepository = ...
val handler = PersonHandler(repository);

val otherRoute: RouterFunction<ServerResponse> = coRouter {  }

val route = coRouter {
    GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) (1)
    GET("/person", accept(APPLICATION_JSON), handler::listPeople) (2)
    POST("/person", handler::createPerson) (3)
}.and(otherRoute) (4)
```

|**1**|带有与 JSON 匹配的`GET /person/{id}`头的`Accept`被路由到`PersonHandler.getPerson`|
|-----|--------------------------------------------------------------------------------------------------|
|**2**|带有与 JSON 匹配的`GET /person`头的`Accept`被路由到`PersonHandler.listPeople`|
|**3**|没有附加谓词的`POST /person`映射到`PersonHandler.createPerson`,并且|
|**4**|`otherRoute`是在其他地方创建的路由器功能,并将其添加到构建的路由中。|

##### 嵌套路线

一组路由器函数通常有一个共享谓词,例如共享路径。在上面的示例中,共享谓词将是一个匹配`/person`的路径谓词,由三个路由使用。在使用注释时,可以使用映射到`/person`的类型级`@RequestMapping`注释来删除这种重复。在 WebFlux.FN 中,路径谓词可以通过 Router Function Builder 上的`path`方法共享。例如,通过使用嵌套路由,可以通过以下方式改进上面示例的最后几行:

爪哇

```
RouterFunction<ServerResponse> route = route()
    .path("/person", builder -> builder (1)
        .GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
        .GET(accept(APPLICATION_JSON), handler::listPeople)
        .POST("/person", handler::createPerson))
    .build();
```

|**1**|请注意,`path`的第二个参数是接受路由器生成器的使用者。|
|-----|-----------------------------------------------------------------------------------|

Kotlin

```
val route = coRouter {
    "/person".nest {
        GET("/{id}", accept(APPLICATION_JSON), handler::getPerson)
        GET(accept(APPLICATION_JSON), handler::listPeople)
        POST("/person", handler::createPerson)
    }
}
```

尽管基于路径的嵌套是最常见的,但你可以通过在 Builder 上使用`nest`方法在任何类型的谓词上进行嵌套。上面仍然包含一些以共享`Accept`-header 谓词形式出现的重复。我们可以通过使用`nest`方法和`accept`方法来进一步改进:

爪哇

```
RouterFunction<ServerResponse> route = route()
    .path("/person", b1 -> b1
        .nest(accept(APPLICATION_JSON), b2 -> b2
            .GET("/{id}", handler::getPerson)
            .GET(handler::listPeople))
        .POST("/person", handler::createPerson))
    .build();
```

Kotlin

```
val route = coRouter {
    "/person".nest {
        accept(APPLICATION_JSON).nest {
            GET("/{id}", handler::getPerson)
            GET(handler::listPeople)
            POST("/person", handler::createPerson)
        }
    }
}
```

#### 1.5.4.运行服务器

[Web MVC](web.html#webmvc-fn-running)

如何在 HTTP 服务器中运行路由器功能?一个简单的选择是使用以下方法之一将路由器函数转换为`HttpHandler`:

* `RouterFunctions.toHttpHandler(RouterFunction)`

* `RouterFunctions.toHttpHandler(RouterFunction, HandlerStrategies)`

然后,你可以将返回的`HttpHandler`与多个服务器适配器一起使用,方法是按照[Httphandler](#webflux-httphandler)执行特定于服务器的指令。

一个更典型的选项(也被 Spring boot 使用)是通过[WebFlux 配置](#webflux-config)使用基于[`DispatcherHandler`](#WebFlux-Dispatcher-Handler)的设置运行,该设置使用 Spring 配置来声明处理请求所需的组件。WebFlux 爪哇 配置声明了以下支持功能端点的基础设施组件:

* `RouterFunctionMapping`:在 Spring 配置中检测一个或多个`RouterFunction<?>`bean,[命令他们](core.html#beans-factory-ordered),通过`RouterFunction.andOther`对它们进行组合,并将请求路由到结果组合的`RouterFunction`

* `HandlerFunctionAdapter`:允许`DispatcherHandler`调用映射到请求的`HandlerFunction`的简单适配器。

* `ServerResponseResultHandler`:通过调用`ServerResponse``writeTo`方法来处理调用`HandlerFunction`的结果。

前面的组件让功能端点适合`DispatcherHandler`请求处理生命周期,并且(可能)与带注释的控制器(如果声明了任何控制器的话)并排运行。这也是 Spring 引导 WebFlux 启动器启用功能端点的方式。

下面的示例显示了一个 WebFlux 爪哇 配置(有关如何运行它,请参见[DispatcherHandler](#webflux-dispatcher-handler)):

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Bean
    public RouterFunction<?> routerFunctionA() {
        // ...
    }

    @Bean
    public RouterFunction<?> routerFunctionB() {
        // ...
    }

    // ...

    @Override
    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
        // configure message conversion...
    }

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // configure CORS...
    }

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        // configure view resolution for HTML rendering...
    }
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    @Bean
    fun routerFunctionA(): RouterFunction<*> {
        // ...
    }

    @Bean
    fun routerFunctionB(): RouterFunction<*> {
        // ...
    }

    // ...

    override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
        // configure message conversion...
    }

    override fun addCorsMappings(registry: CorsRegistry) {
        // configure CORS...
    }

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        // configure view resolution for HTML rendering...
    }
}
```

#### 1.5.5.过滤处理程序函数

[Web MVC](web.html#webmvc-fn-handler-filter-function)

你可以使用路由函数生成器上的`before``after``filter`方法来过滤处理程序函数。对于注释,你可以通过使用`@ControllerAdvice``ServletFilter`或同时使用这两种方法来实现类似的功能。筛选器将应用于由构建器构建的所有路由。这意味着嵌套路由中定义的筛选器不适用于“顶层”路由。例如,考虑以下示例:

爪哇

```
RouterFunction<ServerResponse> route = route()
    .path("/person", b1 -> b1
        .nest(accept(APPLICATION_JSON), b2 -> b2
            .GET("/{id}", handler::getPerson)
            .GET(handler::listPeople)
            .before(request -> ServerRequest.from(request) (1)
                .header("X-RequestHeader", "Value")
                .build()))
        .POST("/person", handler::createPerson))
    .after((request, response) -> logResponse(response)) (2)
    .build();
```

|**1**|添加自定义请求头的`before`过滤器仅应用于两个 GET 路由。|
|-----|----------------------------------------------------------------------------------------------|
|**2**|记录响应的`after`过滤器应用于所有路由,包括嵌套的路由。|

Kotlin

```
val route = router {
    "/person".nest {
        GET("/{id}", handler::getPerson)
        GET("", handler::listPeople)
        before { (1)
            ServerRequest.from(it)
                    .header("X-RequestHeader", "Value").build()
        }
        POST("/person", handler::createPerson)
        after { _, response -> (2)
            logResponse(response)
        }
    }
}
```

|**1**|添加自定义请求头的`before`过滤器仅应用于两个 GET 路由。|
|-----|----------------------------------------------------------------------------------------------|
|**2**|记录响应的`after`过滤器应用于所有路由,包括嵌套的路由。|

路由器构建器上的`filter`方法接受`HandlerFilterFunction`:一个函数接受`ServerRequest``HandlerFunction`并返回`ServerResponse`。处理程序函数参数表示链中的下一个元素。这通常是路由到的处理程序,但是如果应用了多个,它也可以是另一个过滤器。

现在,我们可以在路由中添加一个简单的安全过滤器,假设我们有一个`SecurityManager`,它可以确定是否允许特定的路径。下面的示例展示了如何做到这一点:

爪哇

```
SecurityManager securityManager = ...

RouterFunction<ServerResponse> route = route()
    .path("/person", b1 -> b1
        .nest(accept(APPLICATION_JSON), b2 -> b2
            .GET("/{id}", handler::getPerson)
            .GET(handler::listPeople))
        .POST("/person", handler::createPerson))
    .filter((request, next) -> {
        if (securityManager.allowAccessTo(request.path())) {
            return next.handle(request);
        }
        else {
            return ServerResponse.status(UNAUTHORIZED).build();
        }
    })
    .build();
```

Kotlin

```
val securityManager: SecurityManager = ...

val route = router {
        ("/person" and accept(APPLICATION_JSON)).nest {
            GET("/{id}", handler::getPerson)
            GET("", handler::listPeople)
            POST("/person", handler::createPerson)
            filter { request, next ->
                if (securityManager.allowAccessTo(request.path())) {
                    next(request)
                }
                else {
                    status(UNAUTHORIZED).build();
                }
            }
        }
    }
```

前面的示例演示了调用`next.handle(ServerRequest)`是可选的。我们只允许在允许访问的情况下运行处理程序函数。

除了在路由器功能构建器上使用`filter`方法外,还可以通过`RouterFunction.filter(HandlerFilterFunction)`对现有的路由器功能应用过滤器。

|   |CORS 对功能端点的支持是通过专用的[`CorsWebFilter`]提供的。|
|---|---------------------------------------------------------------------------------------------------------------------------------|

### 1.6.URI 链接

[Web MVC](web.html#mvc-uri-building)

本节描述了在 Spring 框架中可用来准备 URI 的各种选项。

#### 1.6.1.尿酸成分

Spring MVC 和 Spring WebFlux

`UriComponentsBuilder`有助于从具有变量的 URI 模板构建 URI,如下例所示:

爪哇

```
UriComponents uriComponents = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")  (1)
        .queryParam("q", "{q}")  (2)
        .encode() (3)
        .build(); (4)

URI uri = uriComponents.expand("Westin", "123").toUri();  (5)
```

|**1**|带有 URI 模板的静态工厂方法。|
|-----|-----------------------------------------------------------|
|**2**|添加或替换 URI 组件。|
|**3**|请求对 URI 模板和 URI 变量进行编码。|
|**4**|构建`UriComponents`。|
|**5**|展开变量并获得`URI`。|

Kotlin

```
val uriComponents = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")  (1)
        .queryParam("q", "{q}")  (2)
        .encode() (3)
        .build() (4)

val uri = uriComponents.expand("Westin", "123").toUri()  (5)
```

|**1**|带有 URI 模板的静态工厂方法。|
|-----|-----------------------------------------------------------|
|**2**|添加或替换 URI 组件。|
|**3**|请求对 URI 模板和 URI 变量进行编码。|
|**4**|构建`UriComponents`。|
|**5**|展开变量并获得`URI`。|

前面的示例可以合并为一个链,并用`buildAndExpand`将其缩短,如下例所示:

爪哇

```
URI uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")
        .queryParam("q", "{q}")
        .encode()
        .buildAndExpand("Westin", "123")
        .toUri();
```

Kotlin

```
val uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")
        .queryParam("q", "{q}")
        .encode()
        .buildAndExpand("Westin", "123")
        .toUri()
```

你可以通过直接访问一个 URI(这意味着编码)来进一步缩短它,如下例所示:

爪哇

```
URI uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")
        .queryParam("q", "{q}")
        .build("Westin", "123");
```

Kotlin

```
val uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}")
        .queryParam("q", "{q}")
        .build("Westin", "123")
```

可以使用完整的 URI 模板进一步缩短它,如下例所示:

爪哇

```
URI uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}?q={q}")
        .build("Westin", "123");
```

Kotlin

```
val uri = UriComponentsBuilder
        .fromUriString("https://example.com/hotels/{hotel}?q={q}")
        .build("Westin", "123")
```

#### 1.6.2.UriBuilder

Spring MVC 和 Spring WebFlux

[`UriComponentsBuilder`]实现`UriBuilder`。你可以创建`UriBuilder`,然后使用`UriBuilderFactory`。同时,`UriBuilderFactory``UriBuilder`提供了一种基于共享配置(例如基本 URL、编码首选项和其他细节)的可插入机制,用于从 URI 模板构建 URI。

你可以使用`UriBuilderFactory`配置`RestTemplate``WebClient`来定制 URI 的准备。`DefaultUriBuilderFactory``UriBuilderFactory`的默认实现,它在内部使用`UriComponentsBuilder`并公开共享配置选项。

下面的示例展示了如何配置`RestTemplate`:

Java

```
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;

String baseUrl = "https://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);

RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);
```

Kotlin

```
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode

val baseUrl = "https://example.org"
val factory = DefaultUriBuilderFactory(baseUrl)
factory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES

val restTemplate = RestTemplate()
restTemplate.uriTemplateHandler = factory
```

下面的示例配置`WebClient`:

Java

```
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode;

String baseUrl = "https://example.org";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);

WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
```

Kotlin

```
// import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode

val baseUrl = "https://example.org"
val factory = DefaultUriBuilderFactory(baseUrl)
factory.encodingMode = EncodingMode.TEMPLATE_AND_VALUES

val client = WebClient.builder().uriBuilderFactory(factory).build()
```

此外,还可以直接使用`DefaultUriBuilderFactory`。它类似于使用`UriComponentsBuilder`,但它是一个实际的实例,它保存配置和首选项,而不是静态工厂方法,如下例所示:

Java

```
String baseUrl = "https://example.com";
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(baseUrl);

URI uri = uriBuilderFactory.uriString("/hotels/{hotel}")
        .queryParam("q", "{q}")
        .build("Westin", "123");
```

Kotlin

```
val baseUrl = "https://example.com"
val uriBuilderFactory = DefaultUriBuilderFactory(baseUrl)

val uri = uriBuilderFactory.uriString("/hotels/{hotel}")
        .queryParam("q", "{q}")
        .build("Westin", "123")
```

#### 1.6.3.URI 编码

Spring MVC 和 Spring WebFlux

`UriComponentsBuilder`在两个级别上公开编码选项:

* [uricomponentsbuilder#encode()](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/util/UriComponentsBuilder.html#encode--):先对 URI 模板进行预编码,然后在展开时对 URI 变量进行严格编码。

* [uricomponents#encode()](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/util/UriComponents.html#encode--):编码 URI 组件*之后*URI 变量被展开。

这两个选项都用转义的八进制替换非 ASCII 和非法字符。然而,第一个选项也用 URI 变量中出现的保留意义替换字符。

|   |考虑一下“;”,它在某种程度上是合法的,但具有保留的含义。第一个选项在 URI 变量中用“%3b”替换<br/>;;",但不在 URI 模板中。相比之下,第二个选项永远不会<br/>取代“;”,因为它是路径中的法律字符。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

在大多数情况下,第一个选项可能会给出预期的结果,因为它将 URI 变量视为不透明的数据来进行完全编码,而如果 URI 变量故意包含保留字符,则第二个选项是有用的。当完全不展开 URI 变量时,第二个选项也很有用,因为这也会对任何看起来像 URI 变量的内容进行编码。

下面的示例使用了第一个选项:

Java

```
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
        .queryParam("q", "{q}")
        .encode()
        .buildAndExpand("New York", "foo+bar")
        .toUri();

// Result is "/hotel%20list/New%20York?q=foo%2Bbar"
```

Kotlin

```
val uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
        .queryParam("q", "{q}")
        .encode()
        .buildAndExpand("New York", "foo+bar")
        .toUri()

// Result is "/hotel%20list/New%20York?q=foo%2Bbar"
```

可以通过直接访问 URI(这意味着编码)来缩短前面的示例,如下例所示:

Java

```
URI uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
        .queryParam("q", "{q}")
        .build("New York", "foo+bar");
```

Kotlin

```
val uri = UriComponentsBuilder.fromPath("/hotel list/{city}")
        .queryParam("q", "{q}")
        .build("New York", "foo+bar")
```

可以使用完整的 URI 模板进一步缩短它,如下例所示:

Java

```
URI uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
        .build("New York", "foo+bar");
```

Kotlin

```
val uri = UriComponentsBuilder.fromUriString("/hotel list/{city}?q={q}")
        .build("New York", "foo+bar")
```

`WebClient``RestTemplate`通过`UriBuilderFactory`策略在内部扩展和编码 URI 模板。两者都可以使用自定义策略进行配置,如下例所示:

Java

```
String baseUrl = "https://example.com";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl)
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);

// Customize the RestTemplate..
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);

// Customize the WebClient..
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
```

Kotlin

```
val baseUrl = "https://example.com"
val factory = DefaultUriBuilderFactory(baseUrl).apply {
    encodingMode = EncodingMode.TEMPLATE_AND_VALUES
}

// Customize the RestTemplate..
val restTemplate = RestTemplate().apply {
    uriTemplateHandler = factory
}

// Customize the WebClient..
val client = WebClient.builder().uriBuilderFactory(factory).build()
```

`DefaultUriBuilderFactory`实现在内部使用`UriComponentsBuilder`来扩展和编码 URI 模板。作为工厂,它提供了一个单独的位置来配置编码方法,该方法基于以下编码模式之一:

* `TEMPLATE_AND_VALUES`:使用`UriComponentsBuilder#encode()`(对应于前面列表中的第一个选项)对 URI 模板进行预编码,并在展开时对 URI 变量进行严格编码。

* `VALUES_ONLY`:不对 URI 模板进行编码,而是在将 URI 变量扩展到模板之前,通过`UriUtils#encodeUriVariables`对 URI 变量进行严格编码。

* `URI_COMPONENT`:使用`UriComponents#encode()`,对应于前面列表中的第二个选项,来对 URI 组件值的编码*之后*URI 变量进行扩展。

* `NONE`:不应用编码。

由于历史原因和向后兼容,`RestTemplate`被设置为`EncodingMode.URI_COMPONENT``WebClient`依赖于`DefaultUriBuilderFactory`中的默认值,该默认值从 5.0.x 中的`EncodingMode.URI_COMPONENT`更改为 5.1 中的`EncodingMode.TEMPLATE_AND_VALUES`

### 1.7.科尔斯

[Web MVC](web.html#mvc-cors)

Spring WebFlux 允许你处理 CORS(跨源资源共享)。这一节描述了如何做到这一点。

#### 1.7.1.导言

[Web MVC](web.html#mvc-cors-intro)

出于安全原因,浏览器禁止对当前来源以外的资源进行 Ajax 调用。例如,你可以在一个标签中设置你的银行帐户,而在另一个标签中设置 Evil.com。来自 Evil.com 的脚本不应该能够使用你的凭据向你的银行 API 发出 Ajax 请求——例如,从你的帐户中取款!

跨源资源共享是由[大多数浏览器](https://caniuse.com/#feat=cors)实现的[W3C 规范](https://www.w3.org/TR/cors/),它允许你指定授权哪种类型的跨域请求,而不是使用基于 iframe 或 JSONP 的安全性较低、功能较弱的解决方案。

#### 1.7.2.处理

[Web MVC](web.html#mvc-cors-processing)

CORS 规范区分了飞行前、简单和实际请求。要了解 CORS 的工作原理,你可以阅读[这篇文章](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)等,或者查看规范以获得更多详细信息。

Spring WebFlux`HandlerMapping`实现为 CORS 提供了内置支持。在成功地将一个请求映射到一个处理程序之后,`HandlerMapping`检查 CORS 配置中给定的请求和处理程序,并采取进一步的操作。前置请求是直接处理的,而简单和实际的 CORS 请求是截获、验证的,并设置了所需的 CORS 响应头。

为了启用跨源请求(即存在`Origin`头并与请求的主机不同),你需要有一些显式声明的 CORS 配置。如果没有找到匹配的 CORS 配置,则拒绝预航前请求。没有 CORS 头被添加到简单的和实际的 CORS 请求的响应中,因此,浏览器会拒绝它们。

每个`HandlerMapping`都可以单独使用基于 URL 模式的[configured](https://docs.spring.io/spring-framework/docs/5.3.16/javadoc-api/org/springframework/web/reactive/handler/AbstractHandlerMapping.html#setCorsConfigurations-java.util.Map-)映射`CorsConfiguration`。在大多数情况下,应用程序使用 WebFlux Java 配置来声明这样的映射,这将导致一个单一的全局映射传递给所有`HandlerMapping`实现。

你可以将`HandlerMapping`级别的全局 CORS 配置与更细粒度的、处理程序级别的 CORS 配置结合起来。例如,带注释的控制器可以使用类或方法级别的`@CrossOrigin`注释(其他处理程序可以实现`CorsConfigurationSource`)。

结合全局和局部配置的规则通常是累加的——例如,所有全局配置和所有局部配置。对于那些只能接受单个值的属性,例如`allowCredentials``maxAge`,本地重写全局值。详见[`CorsConfiguration#combine(CorsConfiguration)`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/cors/corsconfiguration.html#combine-org.springframework.web.cors.corsconfiguration-)。

|   |要从源代码中了解更多信息或进行高级定制,请参见:<br/><br/>*`CorsConfiguration`<br/><br/>*`AbstractHandlerMapping`<br/><br/>|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

#### 1.7.3.`@CrossOrigin`

[Web MVC](web.html#mvc-cors-controller)

[`@CrossOrigin`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/bind/annotation/crossorigin.html)注解可以在带注释的控制器方法上实现跨源请求,如下例所示:

爪哇

```
@RestController
@RequestMapping("/account")
public class AccountController {

    @CrossOrigin
    @GetMapping("/{id}")
    public Mono<Account> retrieve(@PathVariable Long id) {
        // ...
    }

    @DeleteMapping("/{id}")
    public Mono<Void> remove(@PathVariable Long id) {
        // ...
    }
}
```

Kotlin

```
@RestController
@RequestMapping("/account")
class AccountController {

    @CrossOrigin
    @GetMapping("/{id}")
    suspend fun retrieve(@PathVariable id: Long): Account {
        // ...
    }

    @DeleteMapping("/{id}")
    suspend fun remove(@PathVariable id: Long) {
        // ...
    }
}
```

默认情况下,`@CrossOrigin`允许:

* 所有的起源。

* 所有标题。

* 将控制器方法映射到的所有 HTTP 方法。

`allowCredentials`默认情况下不启用,因为这建立了一个信任级别,该级别公开敏感的特定于用户的信息(例如 Cookie 和 CSRF 令牌),并且只应在适当的情况下使用。当启用`allowOrigins`时,要么必须将`allowOrigins`设置为一个或多个特定域(但不是特定值`"*"`),要么可选择将`allowOriginPatterns`属性用于匹配到源集的动态。

`maxAge`设置为 30 分钟。

`@CrossOrigin`在类级别上也受到支持,并被所有方法继承。下面的示例指定了一个特定的域,并将`maxAge`设置为一个小时:

爪哇

```
@CrossOrigin(origins = "https://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {

    @GetMapping("/{id}")
    public Mono<Account> retrieve(@PathVariable Long id) {
        // ...
    }

    @DeleteMapping("/{id}")
    public Mono<Void> remove(@PathVariable Long id) {
        // ...
    }
}
```

Kotlin

```
@CrossOrigin("https://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
class AccountController {

    @GetMapping("/{id}")
    suspend fun retrieve(@PathVariable id: Long): Account {
        // ...
    }

    @DeleteMapping("/{id}")
    suspend fun remove(@PathVariable id: Long) {
        // ...
    }
}
```

可以在类和方法级别上使用`@CrossOrigin`,如下例所示:

爪哇

```
@CrossOrigin(maxAge = 3600) (1)
@RestController
@RequestMapping("/account")
public class AccountController {

    @CrossOrigin("https://domain2.com") (2)
    @GetMapping("/{id}")
    public Mono<Account> retrieve(@PathVariable Long id) {
        // ...
    }

    @DeleteMapping("/{id}")
    public Mono<Void> remove(@PathVariable Long id) {
        // ...
    }
}
```

|**1**|在类级别上使用`@CrossOrigin`。|
|-----|-----------------------------------------|
|**2**|在方法级别使用`@CrossOrigin`。|

Kotlin

```
@CrossOrigin(maxAge = 3600) (1)
@RestController
@RequestMapping("/account")
class AccountController {

    @CrossOrigin("https://domain2.com") (2)
    @GetMapping("/{id}")
    suspend fun retrieve(@PathVariable id: Long): Account {
        // ...
    }

    @DeleteMapping("/{id}")
    suspend fun remove(@PathVariable id: Long) {
        // ...
    }
}
```

|**1**|在类级别上使用`@CrossOrigin`。|
|-----|-----------------------------------------|
|**2**|在方法级别使用`@CrossOrigin`。|

#### 1.7.4.全局配置

[Web MVC](web.html#mvc-cors-global)

除了细粒度的控制器方法级配置外,你可能还需要定义一些全局 CORS 配置。你可以在任何`HandlerMapping`上单独设置基于 URL 的`CorsConfiguration`映射。然而,大多数应用程序都使用 WebFlux 爪哇 配置来实现这一点。

默认情况下,全局配置启用以下功能:

* 所有的起源。

* 所有标题。

* `GET``HEAD`,和`POST`方法。

`allowedCredentials`默认情况下不启用,因为这建立了一个信任级别,该级别公开敏感的特定于用户的信息(例如 Cookie 和 CSRF 令牌),并且只应在适当的情况下使用。当启用`allowOrigins`时,要么必须将`allowOrigins`设置为一个或多个特定的域(但不是特殊值`"*"`),要么将`allowOriginPatterns`属性用于匹配到源集的动态。

`maxAge`设置为 30 分钟。

要在 WebFlux 爪哇 配置中启用 CORS,可以使用`CorsRegistry`回调,如下例所示:

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {

        registry.addMapping("/api/**")
            .allowedOrigins("https://domain2.com")
            .allowedMethods("PUT", "DELETE")
            .allowedHeaders("header1", "header2", "header3")
            .exposedHeaders("header1", "header2")
            .allowCredentials(true).maxAge(3600);

        // Add more mappings...
    }
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun addCorsMappings(registry: CorsRegistry) {

        registry.addMapping("/api/**")
                .allowedOrigins("https://domain2.com")
                .allowedMethods("PUT", "DELETE")
                .allowedHeaders("header1", "header2", "header3")
                .exposedHeaders("header1", "header2")
                .allowCredentials(true).maxAge(3600)

        // Add more mappings...
    }
}
```

#### 1.7.5.CORS`WebFilter`

[Web MVC](web.html#mvc-cors-filter)

你可以通过内置的[`CorsWebFilter`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/cors/active/corswebfilter.html)应用 CORS 支持,这与[功能端点](#webflux-fn)很好地匹配。

|   |如果你试图将`CorsFilter`与 Spring 安全性一起使用,请记住,对于 CORS, Spring <br/>安全性具有[内置支持](https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#cors)。|
|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

要配置过滤器,你可以声明一个`CorsWebFilter` Bean,并将一个`CorsConfigurationSource`传递给它的构造函数,如下例所示:

爪哇

```
@Bean
CorsWebFilter corsFilter() {

    CorsConfiguration config = new CorsConfiguration();

    // Possibly...
    // config.applyPermitDefaultValues()

    config.setAllowCredentials(true);
    config.addAllowedOrigin("https://domain1.com");
    config.addAllowedHeader("*");
    config.addAllowedMethod("*");

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", config);

    return new CorsWebFilter(source);
}
```

Kotlin

```
@Bean
fun corsFilter(): CorsWebFilter {

    val config = CorsConfiguration()

    // Possibly...
    // config.applyPermitDefaultValues()

    config.allowCredentials = true
    config.addAllowedOrigin("https://domain1.com")
    config.addAllowedHeader("*")
    config.addAllowedMethod("*")

    val source = UrlBasedCorsConfigurationSource().apply {
        registerCorsConfiguration("/**", config)
    }
    return CorsWebFilter(source)
}
```

### 1.8.网络安全

[Web MVC](web.html#mvc-web-security)

[Spring Security](https://projects.spring.io/spring-security/)项目提供了保护 Web 应用程序免受恶意攻击的支持。请参阅 Spring 安全参考文档,包括:

* [WebFlux 安全性](https://docs.spring.io/spring-security/site/docs/current/reference/html5/#jc-webflux)

* [WebFlux 测试支持](https://docs.spring.io/spring-security/site/docs/current/reference/html5/#test-webflux)

* [CSRF 保护](https://docs.spring.io/spring-security/site/docs/current/reference/html5/#csrf)

* [安全响应标头](https://docs.spring.io/spring-security/site/docs/current/reference/html5/#headers)

### 1.9.查看技术

[Web MVC](web.html#mvc-view)

Spring WebFlux 中对视图技术的使用是可插入的。是否决定使用 ThymeLeaf、FreeMarker 或其他一些视图技术主要是配置更改的问题。本章介绍与 Spring WebFlux 集成的视图技术。我们假设你已经熟悉[视图分辨率](#webflux-viewresolution)

#### 1.9.1.百里香叶

[Web MVC](web.html#mvc-view-thymeleaf)

ThymeLeaf 是一个现代的服务器端 爪哇 模板引擎,强调自然的 HTML 模板,可以通过双击在浏览器中预览,这对于在 UI 模板上独立工作(例如,由设计师)非常有帮助,而不需要运行的服务器。Thymeleaf 提供了一套广泛的功能,并且它是积极开发和维护的。有关更完整的介绍,请参见[Thymeleaf](https://www.thymeleaf.org/)项目主页。

ThymeLeaf 与 Spring WebFlux 的集成由 ThymeLeaf 项目管理。配置涉及一些 Bean 声明,例如`SpringResourceTemplateResolver``SpringWebFluxTemplateEngine``ThymeleafReactiveViewResolver`。有关更多详细信息,请参见[Thymeleaf+Spring](https://www.thymeleaf.org/documentation.html)和 WebFlux 集成[公告](http://forum.thymeleaf.org/Thymeleaf-3-0-8-JUST-PUBLISHED-td4030687.html)

#### 1.9.2.自由标记

[Web MVC](web.html#mvc-view-freemarker)

[Apache Freemarker](https://freemarker.apache.org/)是一个模板引擎,用于生成从 HTML 到电子邮件等任何类型的文本输出。 Spring 框架具有用于使用 Spring WebFlux 和 Freemarker 模板的内置集成。

##### 视图配置

[Web MVC](web.html#mvc-view-freemarker-contextconfig)

下面的示例展示了如何将 Freemarker 配置为一种视图技术:

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.freeMarker();
    }

    // Configure FreeMarker...

    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer() {
        FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
        configurer.setTemplateLoaderPath("classpath:/templates/freemarker");
        return configurer;
    }
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.freeMarker()
    }

    // Configure FreeMarker...

    @Bean
    fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
        setTemplateLoaderPath("classpath:/templates/freemarker")
    }
}
```

你的模板需要存储在`FreeMarkerConfigurer`指定的目录中,如前面的示例所示。给定上述配置,如果控制器返回视图名`welcome`,则解析器将查找`classpath:/templates/freemarker/welcome.ftl`模板。

##### 自由标记配置

[Web MVC](web.html#mvc-views-freemarker)

通过在`FreeMarkerConfigurer` Bean 上设置适当的 Bean 属性,可以将自由标记’settings’和’sharedvariables’直接传递给自由标记`Configuration`对象(由 Spring 管理)。`freemarkerSettings`属性需要一个`java.util.Properties`对象,而`freemarkerVariables`属性需要一个`java.util.Map`。下面的示例展示了如何使用`FreeMarkerConfigurer`:

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    // ...

    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer() {
        Map<String, Object> variables = new HashMap<>();
        variables.put("xml_escape", new XmlEscape());

        FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
        configurer.setTemplateLoaderPath("classpath:/templates");
        configurer.setFreemarkerVariables(variables);
        return configurer;
    }
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    // ...

    @Bean
    fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
        setTemplateLoaderPath("classpath:/templates")
        setFreemarkerVariables(mapOf("xml_escape" to XmlEscape()))
    }
}
```

有关应用于`Configuration`对象的设置和变量的详细信息,请参见 Freemarker 文档。

##### 表单处理

[Web MVC](web.html#mvc-view-freemarker-forms)

Spring 提供了用于 JSP 的标记库,该标记库包括`<spring:bind/>`元素。这个元素主要允许表单显示来自表单支持对象的值,并显示来自 Web 或业务层中`Validator`的失败验证的结果。 Spring 在 Freemarker 中还具有对相同功能的支持,具有用于生成表单输入元素本身的附加方便宏。

##### BIND 宏 #

[Web MVC](web.html#mvc-view-bind-macros)

`spring-webflux.jar`freemarker 文件中维护了一组标准的宏,因此对于适当配置的应用程序,它们总是可用的。

Spring 模板库中定义的一些宏被认为是内部的(私有的),但是在宏定义中不存在这样的范围,这使得所有的宏对于调用代码和用户模板都是可见的。下面的部分只关注你需要从模板中直接调用的宏。如果你希望直接查看宏代码,那么该文件被称为`spring.ftl`,并且位于`org.springframework.web.reactive.result.view.freemarker`包中。

有关绑定支持的更多详细信息,请参见[简单绑定](web.html#mvc-view-simple-binding)中的 Spring MVC。

##### 表格宏 #

有关 Spring 对自由标记模板的表单宏支持的详细信息,请参阅 Spring MVC 文档的以下部分。

* [输入宏](web.html#mvc-views-form-macros)

* [输入字段](web.html#mvc-views-form-macros-input)

* [选择字段](web.html#mvc-views-form-macros-select)

* [HTML 转义](web.html#mvc-views-form-macros-html-escaping)

#### 1.9.3.脚本视图

[Web MVC](web.html#mvc-view-script)

Spring 框架有一个内置的集成,用于使用 Spring WebFlux 和任何模板库,这些模板库可以在[JSR-223](https://www.jcp.org/en/jsr/detail?id=223)爪哇 脚本引擎之上运行。下表显示了我们在不同的脚本引擎上测试过的模板库:

|脚本库|                  Scripting Engine                   |
|----------------------------------------------------------------------------------|-----------------------------------------------------|
|[Handlebars](https://handlebarsjs.com/)|[Nashorn](https://openjdk.java.net/projects/nashorn/)|
|[Mustache](https://mustache.github.io/)|[Nashorn](https://openjdk.java.net/projects/nashorn/)|
|[React](https://facebook.github.io/react/)|[Nashorn](https://openjdk.java.net/projects/nashorn/)|
|[EJS](https://www.embeddedjs.com/)|[Nashorn](https://openjdk.java.net/projects/nashorn/)|
|[ERB](https://www.stuartellis.name/articles/erb/)|           [JRuby](https://www.jruby.org)            |
|[字符串模板](https://docs.python.org/2/library/string.html#template-strings)|          [Jython](https://www.jython.org/)          |
|[Kotlin Script templating](https://github.com/sdeleuze/kotlin-script-templating)|          [Kotlin](https://kotlinlang.org/)          |

|   |集成任何其他脚本引擎的基本规则是,它必须实现`ScriptEngine``Invocable`接口。|
|---|------------------------------------------------------------------------------------------------------------------------------|

##### 所需经费

[Web MVC](web.html#mvc-view-script-dependencies)

你需要在 Classpath 上安装脚本引擎,其细节因脚本引擎而异:

* [Nashorn](https://openjdk.java.net/projects/nashorn/)爪哇Script 引擎由 爪哇8+ 提供。强烈推荐使用最新的可用更新版本。

* [JRuby](https://www.jruby.org)应该作为 Ruby 支持的依赖项添加。

* [Jython](https://www.jython.org)应该作为 Python 支持的依赖项添加。

* 对于 Kotlin 脚本支持,应该添加`org.jetbrains.kotlin:kotlin-script-util`依赖项和包含`META-INF/services/javax.script.ScriptEngineFactory`行的`org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory`文件。有关更多详细信息,请参见[这个例子](https://github.com/sdeleuze/kotlin-script-templating)

你需要有脚本模板库。实现 爪哇Script 的一种方法是通过[WebJars](https://www.webjars.org/)

##### 脚本模板

[Web MVC](web.html#mvc-view-script-integrate)

你可以声明一个`ScriptTemplateConfigurer` Bean 来指定要使用的脚本引擎、要加载的脚本文件、调用什么函数来呈现模板,等等。下面的示例使用了 Mustache 模板和 Nashorn 爪哇Script 引擎:

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.scriptTemplate();
    }

    @Bean
    public ScriptTemplateConfigurer configurer() {
        ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
        configurer.setEngineName("nashorn");
        configurer.setScripts("mustache.js");
        configurer.setRenderObject("Mustache");
        configurer.setRenderFunction("render");
        return configurer;
    }
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.scriptTemplate()
    }

    @Bean
    fun configurer() = ScriptTemplateConfigurer().apply {
        engineName = "nashorn"
        setScripts("mustache.js")
        renderObject = "Mustache"
        renderFunction = "render"
    }
}
```

使用以下参数调用`render`函数:

* `String template`:模板内容

* `Map model`:视图模型

* `RenderingContext renderingContext`:[`RenderingContext`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/ Servlet/view/script/renderingcontext.html),它提供了对应用程序上下文、区域设置、模板装入器和 URL(自 5.0 起)的访问权限

`Mustache.render()`与该签名在本机上兼容,因此你可以直接调用它。

如果模板技术需要进行一些定制,那么可以提供一个实现定制呈现功能的脚本。例如,[Handlerbars](https://handlebarsjs.com)在使用模板之前需要对其进行编译,并且需要[polyfill](https://en.wikipedia.org/wiki/Polyfill),以便模拟服务器端脚本引擎中不可用的一些浏览器功能。下面的示例展示了如何设置自定义呈现函数:

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.scriptTemplate();
    }

    @Bean
    public ScriptTemplateConfigurer configurer() {
        ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
        configurer.setEngineName("nashorn");
        configurer.setScripts("polyfill.js", "handlebars.js", "render.js");
        configurer.setRenderFunction("render");
        configurer.setSharedEngine(false);
        return configurer;
    }
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.scriptTemplate()
    }

    @Bean
    fun configurer() = ScriptTemplateConfigurer().apply {
        engineName = "nashorn"
        setScripts("polyfill.js", "handlebars.js", "render.js")
        renderFunction = "render"
        isSharedEngine = false
    }
}
```

|   |当使用非线程安全的<br/>脚本引擎时,需要将`sharedEngine`属性设置为`false`,该脚本引擎的模板库不是为并发而设计的,例如在 Nashorn 上运行的手柄或<br/>React。在那种情况下,由于[this bug](https://bugs.openjdk.java.net/browse/JDK-8076099),爪哇 SE8Update60 是必需的,但是一般情况下<br/>推荐在任何情况下使用最近发布的 爪哇 SE 补丁。|
|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

`polyfill.js`只定义了处理栏正常运行所需的`window`对象,如以下代码片段所示:

```
var window = {};
```

这个基本的`render.js`实现在使用模板之前对其进行编译。为生产准备好的实现还应该存储和重用缓存的模板或预编译的模板。这可以在脚本端完成,也可以在你需要的任何定制中完成(例如,管理模板引擎配置)。下面的示例展示了如何编译模板:

```
function render(template, model) {
    var compiledTemplate = Handlebars.compile(template);
    return compiledTemplate(model);
}
```

查看 Spring Framework Unit 测试,[爪哇](https://github.com/spring-projects/spring-framework/tree/main/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/script)[resources](https://github.com/spring-projects/spring-framework/tree/main/spring-webflux/src/test/resources/org/springframework/web/reactive/result/view/script),以获得更多配置示例。

#### 1.9.4.JSON 和 XML

[Web MVC](web.html#mvc-view-jackson)

出于[内容协商](#webflux-multiple-representations)的目的,可以根据客户机请求的内容类型,在使用 HTML 模板或其他格式(例如 JSON 或 XML)呈现模型之间进行替换,这是非常有用的。为了支持这样做, Spring WebFlux 提供了`HttpMessageWriterView`,你可以使用它来插入`spring-web`中的任何可用的[Codecs](#webflux-codecs),例如`Jackson2JsonEncoder``Jackson2SmileEncoder``Jaxb2XmlEncoder`

与其他视图技术不同,`HttpMessageWriterView`不需要`ViewResolver`,而是将[configured](#webflux-config-view-resolvers)作为默认视图。你可以配置一个或多个这样的默认视图,包装不同的`HttpMessageWriter`实例或`Encoder`实例。在运行时使用与请求的内容类型匹配的内容类型。

在大多数情况下,一个模型包含多个属性。要确定要序列化哪个,你可以配置`HttpMessageWriterView`,并使用要用于呈现的 model 属性的名称。如果模型只包含一个属性,则使用该属性。

### 1.10.HTTP 缓存

[Web MVC](web.html#mvc-caching)

HTTP 缓存可以显著提高 Web 应用程序的性能。HTTP 缓存围绕`Cache-Control`响应头和后续的条件请求头,例如`Last-Modified``ETag``Cache-Control`建议私有(例如,浏览器)和公共(例如,代理)缓存如何缓存和重用响应。`ETag`报头用于发出条件请求,如果内容没有更改,则该请求可能在没有正文的情况下导致 304(未 \_modified)。`ETag`可以看作是`Last-Modified`页眉的更复杂的继承者。

本节描述了 Spring WebFlux 中可用的 HTTP 缓存相关选项。

#### 1.10.1.`CacheControl`

[Web MVC](web.html#mvc-caching-cachecontrol)

[`CacheControl`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/http/cachecontrol.html)提供了对配置`Cache-Control`标头相关设置的支持,并在许多地方被接受为参数:

* [控制器](#webflux-caching-etag-lastmodified)

* [静态资源](#webflux-caching-static-resources)

虽然[RFC 7234](https://tools.ietf.org/html/rfc7234#section-5.2.2)描述了`Cache-Control`响应头的所有可能的指令,但`CacheControl`类型采用了一种面向用例的方法,该方法专注于常见的场景,如下例所示:

爪哇

```
// Cache for an hour - "Cache-Control: max-age=3600"
CacheControl ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS);

// Prevent caching - "Cache-Control: no-store"
CacheControl ccNoStore = CacheControl.noStore();

// Cache for ten days in public and private caches,
// public caches should not transform the response
// "Cache-Control: max-age=864000, public, no-transform"
CacheControl ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic();
```

Kotlin

```
// Cache for an hour - "Cache-Control: max-age=3600"
val ccCacheOneHour = CacheControl.maxAge(1, TimeUnit.HOURS)

// Prevent caching - "Cache-Control: no-store"
val ccNoStore = CacheControl.noStore()

// Cache for ten days in public and private caches,
// public caches should not transform the response
// "Cache-Control: max-age=864000, public, no-transform"
val ccCustom = CacheControl.maxAge(10, TimeUnit.DAYS).noTransform().cachePublic()
```

#### 1.10.2.控制器

[Web MVC](web.html#mvc-caching-etag-lastmodified)

控制器可以添加对 HTTP 缓存的显式支持。我们建议这样做,因为资源的`lastModified``ETag`值需要在与条件请求头进行比较之前进行计算。控制器可以将`ETag``Cache-Control`设置添加到`ResponseEntity`中,如下例所示:

爪哇

```
@GetMapping("/book/{id}")
public ResponseEntity<Book> showBook(@PathVariable Long id) {

    Book book = findBook(id);
    String version = book.getVersion();

    return ResponseEntity
            .ok()
            .cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
            .eTag(version) // lastModified is also available
            .body(book);
}
```

Kotlin

```
@GetMapping("/book/{id}")
fun showBook(@PathVariable id: Long): ResponseEntity<Book> {

    val book = findBook(id)
    val version = book.getVersion()

    return ResponseEntity
            .ok()
            .cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS))
            .eTag(version) // lastModified is also available
            .body(book)
}
```

如果与条件请求标题的比较表明内容没有更改,则前面的示例发送带有空主体的 304(未 \_modified)响应。否则,将`ETag``Cache-Control`标头添加到响应中。

你还可以检查控制器中的条件请求头,如下例所示:

爪哇

```
@RequestMapping
public String myHandleMethod(ServerWebExchange exchange, Model model) {

    long eTag = ... (1)

    if (exchange.checkNotModified(eTag)) {
        return null; (2)
    }

    model.addAttribute(...); (3)
    return "myViewName";
}
```

|**1**|应用程序特定的计算。|
|-----|--------------------------------------------------------------------|
|**2**|响应已设置为 304(未修改)。没有进一步的处理。|
|**3**|继续处理请求。|

Kotlin

```
@RequestMapping
fun myHandleMethod(exchange: ServerWebExchange, model: Model): String? {

    val eTag: Long = ... (1)

    if (exchange.checkNotModified(eTag)) {
        return null(2)
    }

    model.addAttribute(...) (3)
    return "myViewName"
}
```

|**1**|应用程序特定的计算。|
|-----|--------------------------------------------------------------------|
|**2**|响应已设置为 304(未修改)。没有进一步的处理。|
|**3**|继续处理请求。|

针对`eTag`值、`lastModified`值或两者检查条件请求有三种变体。对于条件`GET``HEAD`请求,可以将响应设置为 304(不是 \_modified)。对于条件`POST``PUT``DELETE`,可以将响应设置为 412(前提条件 \_ 失败),以防止并发修改。

#### 1.10.3.静态资源

[Web MVC](web.html#mvc-caching-static-resources)

为了获得最佳性能,你应该使用`Cache-Control`和条件响应头来服务静态资源。参见关于配置[静态资源](#webflux-config-static-resources)的部分。

### 1.11.WebFlux 配置

[Web MVC](web.html#mvc-config)

WebFlux 爪哇 配置声明了用带注释的控制器或功能端点处理请求所需的组件,并提供了一个 API 来定制配置。这意味着你不需要理解由 爪哇 配置创建的底层 bean。但是,如果你想了解它们,可以在`WebFluxConfigurationSupport`中看到它们,或者在[Special Bean Types](#webflux-special-bean-types)中阅读有关它们的更多信息。

对于配置 API 中没有的更高级的定制,你可以通过[高级配置模式](#webflux-config-advanced-java)获得对配置的完全控制。

#### 1.11.1.启用 WebFlux 配置

[Web MVC](web.html#mvc-config-enable)

你可以在 爪哇 配置中使用`@EnableWebFlux`注释,如下例所示:

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig {
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig
```

前面的示例注册了许多 Spring WebFlux[基础设施 bean](#webflux-special-bean-types),并适应了 Classpath 上可用的依赖关系——对于 JSON、XML 和其他的依赖关系。

#### 1.11.2.WebFlux 配置 API

[Web MVC](web.html#mvc-config-customize)

在你的 爪哇 配置中,你可以实现`WebFluxConfigurer`接口,如下例所示:

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    // Implement configuration methods...
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    // Implement configuration methods...
}
```

#### 1.11.3.转换、格式化

[Web MVC](web.html#mvc-config-conversion)

默认情况下,安装了用于各种数字和日期类型的格式化程序,并支持在字段上通过`@NumberFormat``@DateTimeFormat`进行定制。

要在 爪哇 Config 中注册自定义格式化程序和转换器,请使用以下方法:

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        // ...
    }

}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun addFormatters(registry: FormatterRegistry) {
        // ...
    }
}
```

Spring 默认情况下,WebFlux 在解析和格式化日期值时会考虑请求区域设置。这适用于将日期表示为带有“输入”窗体字段的字符串的窗体。但是,对于“日期”和“时间”表单字段,浏览器使用 HTML 规范中定义的固定格式。对于这种情况,日期和时间格式可以定制如下:

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
        registrar.setUseIsoFormat(true);
        registrar.registerFormatters(registry);
    }
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun addFormatters(registry: FormatterRegistry) {
        val registrar = DateTimeFormatterRegistrar()
        registrar.setUseIsoFormat(true)
        registrar.registerFormatters(registry)
    }
}
```

|   |参见[`FormatterRegistrar`SPI]和`FormattingConversionServiceFactoryBean`有关何时<br/>使用`FormatterRegistrar`实现的更多信息。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

#### 1.11.4.验证

[Web MVC](web.html#mvc-config-validation)

默认情况下,如果[Bean Validation](core.html#validation-beanvalidation-overview)存在于 Classpath(例如, Hibernate 验证器)上,则`LocalValidatorFactoryBean`注册为全局[validator](core.html#validator),用于`@Valid``@Validated`上的`@Controller`方法参数。

在你的 爪哇 配置中,你可以自定义全局`Validator`实例,如下例所示:

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public Validator getValidator() {
        // ...
    }

}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun getValidator(): Validator {
        // ...
    }

}
```

请注意,你也可以在本地注册`Validator`实现,如下例所示:

爪哇

```
@Controller
public class MyController {

    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.addValidators(new FooValidator());
    }

}
```

Kotlin

```
@Controller
class MyController {

    @InitBinder
    protected fun initBinder(binder: WebDataBinder) {
        binder.addValidators(FooValidator())
    }
}
```

|   |如果需要在某个地方注入一个`LocalValidatorFactoryBean`,请创建一个 Bean 并将<br/>标记为`@Primary`,以避免与 MVC 配置中声明的那个冲突。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

#### 1.11.5.内容类型解析器

[Web MVC](web.html#mvc-config-content-negotiation)

你可以配置 Spring WebFlux 如何从请求中确定`@Controller`实例所请求的媒体类型。默认情况下,只检查`Accept`头,但你也可以启用基于查询参数的策略。

下面的示例展示了如何自定义所请求的内容类型分辨率:

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureContentTypeResolver(RequestedContentTypeResolverBuilder builder) {
        // ...
    }
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureContentTypeResolver(builder: RequestedContentTypeResolverBuilder) {
        // ...
    }
}
```

#### 1.11.6.HTTP 消息编解码器

[Web MVC](web.html#mvc-config-message-converters)

下面的示例展示了如何自定义如何读取和写入请求和响应主体:

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
        configurer.defaultCodecs().maxInMemorySize(512 * 1024);
    }
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
        // ...
    }
}
```

`ServerCodecConfigurer`提供了一组默认的读取器和写入器。你可以使用它来添加更多的读取器和编写器,自定义缺省的读取器和编写器,或者完全替换缺省的读取器和编写器。

对于 Jackson 的 JSON 和 XML,可以考虑使用[`Jackson2ObjectMapperBuilder`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/http/converter/json/Jackson2objectmapperbuilder.html),它使用以下属性定制 Jackson 的默认属性:

* [`DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES`](https://fasterxml.github.io/Jackson-databind/javadoc/2.6/com/fasterxml/Jackson/databind/deserializationfeature.html#fail_on_unknown_properties)被禁用。

* [`MapperFeature.DEFAULT_VIEW_INCLUSION`](https://fasterxml.github.io/Jackson-databind/javadoc/2.6/com/fasterxml/Jackson/databind/mapperfeature.html#default_view_inclusion)被禁用。

如果在 Classpath 上检测到以下已知模块,它还会自动注册这些模块:

* [`jackson-datatype-joda`](https://github.com/fasterxml/Jackson-datatype-joda):支持 joda-time 类型。

* [`jackson-datatype-jsr310`](https://github.com/fasterxml/Jackson-datatype-jsr310):支持 爪哇8 日期和时间 API 类型。

* [`jackson-datatype-jdk8`](https://github.com/fasterxml/Jackson-datatype-jdk8):支持其他 爪哇8 类型,例如`Optional`

* [`jackson-module-kotlin`](https://github.com/fasterxml/Jackson-module- Kotlin):支持 Kotlin 类和数据类。

#### 1.11.7.视图解析器

[Web MVC](web.html#mvc-config-view-resolvers)

下面的示例展示了如何配置视图分辨率:

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        // ...
    }
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        // ...
    }
}
```

`ViewResolverRegistry`具有 Spring 框架与之集成的视图技术的快捷方式。下面的示例使用 Freemarker(这也需要配置底层的 Freemarker 视图技术):

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.freeMarker();
    }

    // Configure Freemarker...

    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer() {
        FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
        configurer.setTemplateLoaderPath("classpath:/templates");
        return configurer;
    }
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.freeMarker()
    }

    // Configure Freemarker...

    @Bean
    fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
        setTemplateLoaderPath("classpath:/templates")
    }
}
```

你还可以插入任何`ViewResolver`实现,如下例所示:

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        ViewResolver resolver = ... ;
        registry.viewResolver(resolver);
    }
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        val resolver: ViewResolver = ...
        registry.viewResolver(resolver
    }
}
```

为了支持[内容协商](#webflux-multiple-representations)和通过视图分辨率呈现其他格式(除了 HTML),你可以基于`HttpMessageWriterView`实现配置一个或多个默认视图,该实现接受来自`spring-web`的任何可用的[Codecs](#webflux-codecs)。下面的示例展示了如何做到这一点:

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.freeMarker();

        Jackson2JsonEncoder encoder = new Jackson2JsonEncoder();
        registry.defaultViews(new HttpMessageWriterView(encoder));
    }

    // ...
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun configureViewResolvers(registry: ViewResolverRegistry) {
        registry.freeMarker()

        val encoder = Jackson2JsonEncoder()
        registry.defaultViews(HttpMessageWriterView(encoder))
    }

    // ...
}
```

有关集成 Spring WebFlux 的视图技术的更多信息,请参见[查看技术](#webflux-view)

#### 1.11.8.静态资源

[Web MVC](web.html#mvc-config-static-resources)

此选项提供了一种方便的方式,可以从[`Resource`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/core/io/resource.html)-based 位置的列表中提供静态资源。

在下一个示例中,给定一个以`/resources`开头的请求,相对路径用于在 Classpath 上查找和服务相对于`/static`的静态资源。资源将在一年后到期,以确保最大程度地使用浏览器缓存并减少浏览器发出的 HTTP 请求。还计算`Last-Modified`头,如果存在,则返回`304`状态代码。下面的列表显示了该示例:

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/public", "classpath:/static/")
                .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS));
    }

}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/public", "classpath:/static/")
                .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS))
    }
}
```

资源处理程序还支持[`ResourceResolver`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/reactive/resource/资源olever.html)实现和[`ResourceTransformer`](https://DOCS. Spring.io/ Spring/ Spring-framework/DOCS/5.3.16/javoc-api-api-api-api-api/org/spractuframework/org/resource/resource/resource/resolver.html)

你可以使用`VersionResourceResolver`实现基于内容、固定应用程序版本或其他信息计算的 MD5 散列的版本管理的资源 URL。a`ContentVersionStrategy`(md5hash)是一个很好的选择,但有一些明显的例外(例如与模块装入器一起使用的 爪哇Script 资源)。

下面的示例展示了如何在 爪哇 配置中使用`VersionResourceResolver`:

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/public/")
                .resourceChain(true)
                .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
    }

}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/public/")
                .resourceChain(true)
                .addResolver(VersionResourceResolver().addContentVersionStrategy("/**"))
    }

}
```

你可以使用`ResourceUrlProvider`重写 URL,并应用完整的解析器和转换器(例如,用于插入版本)。WebFlux 配置提供了一个`ResourceUrlProvider`,以便可以将其注入到其他配置中。

Spring 与 MVC 不同的是,目前,在 WebFlux 中,还没有透明地重写静态资源 URL 的方法,因为还没有视图技术可以利用解析器和转换器的非阻塞链。当只提供本地资源时,解决方法是直接使用`ResourceUrlProvider`(例如,通过自定义元素)和块。

请注意,当同时使用`EncodedResourceResolver`(例如,gzip,Brotli 编码)和`VersionedResourceResolver`时,它们必须按该顺序进行注册,以确保始终基于未编码文件可靠地计算基于内容的版本。

[WebJars](https://www.webjars.org/documentation)也通过`WebJarsResourceResolver`来支持,这是在 Classpath 上存在`org.webjars:webjars-locator-core`库时自动注册的。解析器可以重写 URL 以包括 jar 的版本,也可以匹配没有版本的传入 URL——例如,从`/jquery/jquery.min.js``/jquery/1.2.0/jquery.min.js`

|   |基于`ResourceHandlerRegistry`的 爪哇 配置为细粒度控制提供了进一步的选项<br/>,例如,上次修改行为和优化的资源解析。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

#### 1.11.9.路径匹配

[Web MVC](web.html#mvc-config-path-matching)

你可以自定义与路径匹配相关的选项。有关单个选项的详细信息,请参见[`PathMatchConfigurer`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/web/active/config/pathmatchconfigrer.html)爪哇doc。下面的示例展示了如何使用`PathMatchConfigurer`:

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        configurer
            .setUseCaseSensitiveMatch(true)
            .setUseTrailingSlashMatch(false)
            .addPathPrefix("/api",
                    HandlerTypePredicate.forAnnotation(RestController.class));
    }
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    @Override
    fun configurePathMatch(configurer: PathMatchConfigurer) {
        configurer
            .setUseCaseSensitiveMatch(true)
            .setUseTrailingSlashMatch(false)
            .addPathPrefix("/api",
                    HandlerTypePredicate.forAnnotation(RestController::class.java))
    }
}
```

|   |Spring WebFlux 依赖于对名为`RequestPath`的请求路径的解析表示,用于访问已解码的路径段值,并删除带有分号内容的<br/>(即路径或矩阵变量)。这意味着,与 Spring MVC 不同,你不需要指示<br/>是否要对请求路径进行解码,也不需要指示是否要出于<br/>路径匹配的目的删除分号内容。<br/><br/> Spring WebFlux 也不支持后缀模式匹配,这与 Spring MVC 不同,其中,我们<br/>也是[recommend](web.html#mvc-ann-requestmapping-suffix-pattern-match)远离<br/>对它的依赖。|
|---|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

#### 1.11.10.WebSocketService

WebFlux 爪哇 Config 声明了一个`WebSocketHandlerAdapter` Bean,它为 WebSocket 处理程序的调用提供了支持。这意味着,要处理 WebSocket 握手请求,仅需通过`SimpleUrlHandlerMapping``WebSocketHandler`映射到一个 URL。

在某些情况下,可能需要创建带有所提供的`WebSocketHandlerAdapter` Bean 的`WebSocketService`服务,该服务允许配置 WebSocket 服务器属性。例如:

爪哇

```
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {

    @Override
    public WebSocketService getWebSocketService() {
        TomcatRequestUpgradeStrategy strategy = new TomcatRequestUpgradeStrategy();
        strategy.setMaxSessionIdleTimeout(0L);
        return new HandshakeWebSocketService(strategy);
    }
}
```

Kotlin

```
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {

    @Override
    fun webSocketService(): WebSocketService {
        val strategy = TomcatRequestUpgradeStrategy().apply {
            setMaxSessionIdleTimeout(0L)
        }
        return HandshakeWebSocketService(strategy)
    }
}
```

#### 1.11.11.高级配置模式

[Web MVC](web.html#mvc-config-advanced-java)

`@EnableWebFlux`Imports`DelegatingWebFluxConfiguration`表示:

* 为 WebFlux 应用程序提供默认的 Spring 配置

* 检测并委托`WebFluxConfigurer`实现来定制该配置。

对于高级模式,可以删除`@EnableWebFlux`并直接从`DelegatingWebFluxConfiguration`扩展,而不是实现`WebFluxConfigurer`,如下例所示:

爪哇

```
@Configuration
public class WebConfig extends DelegatingWebFluxConfiguration {

    // ...
}
```

Kotlin

```
@Configuration
class WebConfig : DelegatingWebFluxConfiguration {

    // ...
}
```

你可以在`WebConfig`中保留现有的方法,但是你现在也可以重写 Bean 来自基类的声明,并且在 Classpath 上仍然具有任何数量的其他`WebMvcConfigurer`实现。

### 1.12.http/2

[Web MVC](web.html#mvc-http2)

Tomcat、 Jetty 和 Undertow 支持 HTTP/2。但是,有一些与服务器配置相关的考虑因素。有关更多详细信息,请参见[HTTP/2Wiki 页面](https://github.com/spring-projects/spring-framework/wiki/HTTP-2-support)

## 2. WebClient

Spring WebFlux 包括用于执行 HTTP 请求的客户端。`WebClient`具有功能强大的、基于 Reactor 的 Fluent API,参见[反应库](#webflux-reactive-libraries),它使异步逻辑的声明式组合无需处理线程或并发性。它是完全非阻塞的,它支持流媒体,并且依赖同样的[codecs](#webflux-codecs),这些也用于在服务器端对请求和响应内容进行编码和解码。

`WebClient`需要一个 HTTP 客户库来执行请求。以下是内置的支持:

* [反应堆网状结构](https://github.com/reactor/reactor-netty)

* [Jetty Reactive HttpClient](https://github.com/jetty-project/jetty-reactive-httpclient)

* [Apache HttpComponents](https://hc.apache.org/index.html)

* 其他的可以通过`ClientHttpConnector`进行插接。

### 2.1.配置

创建`WebClient`的最简单方法是通过一种静态工厂方法:

* `WebClient.create()`

* `WebClient.create(String baseUrl)`

你还可以使用`WebClient.builder()`和其他选项:

* `uriBuilderFactory`:定制`UriBuilderFactory`用作基本 URL。

* `defaultUriVariables`:展开 URI 模板时要使用的默认值。

* `defaultHeader`:每个请求的标题。

* `defaultCookie`:每个请求都有 cookies。

* `defaultRequest`:`Consumer`来定制每个请求。

* `filter`:每个请求的客户端过滤器。

* `exchangeStrategies`:HTTP 消息阅读器/Writer 自定义。

* `clientConnector`:http 客户库设置。

例如:

爪哇

```
WebClient client = WebClient.builder()
        .codecs(configurer -> ... )
        .build();
```

Kotlin

```
val webClient = WebClient.builder()
        .codecs { configurer -> ... }
        .build()
```

一旦建立,`WebClient`是不变的。但是,你可以复制它并构建一个修改后的副本,如下所示:

爪哇

```
WebClient client1 = WebClient.builder()
        .filter(filterA).filter(filterB).build();

WebClient client2 = client1.mutate()
        .filter(filterC).filter(filterD).build();

// client1 has filterA, filterB

// client2 has filterA, filterB, filterC, filterD
```

Kotlin

```
val client1 = WebClient.builder()
        .filter(filterA).filter(filterB).build()

val client2 = client1.mutate()
        .filter(filterC).filter(filterD).build()

// client1 has filterA, filterB

// client2 has filterA, filterB, filterC, filterD
```

#### 2.1.1.MaxInMemorySize

编解码器有[limits](#webflux-codecs-limits)用于缓冲内存中的数据,以避免应用程序内存问题。默认情况下,这些被设置为 256KB。如果这还不够,那么你将得到以下错误:

```
org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer
```

要更改默认编解码器的限制,请使用以下方法:

爪哇

```
WebClient webClient = WebClient.builder()
        .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
        .build();
```

Kotlin

```
val webClient = WebClient.builder()
        .codecs { configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024) }
        .build()
```

#### 2.1.2.反应堆网状结构

要定制反应堆网络设置,请提供预先配置的`HttpClient`:

爪哇

```
HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...);

WebClient webClient = WebClient.builder()
        .clientConnector(new ReactorClientHttpConnector(httpClient))
        .build();
```

Kotlin

```
val httpClient = HttpClient.create().secure { ... }

val webClient = WebClient.builder()
    .clientConnector(ReactorClientHttpConnector(httpClient))
    .build()
```

##### Resources

默认情况下,`HttpClient`参与在`reactor.netty.http.HttpResources`中持有的全局反应器网络资源,包括事件循环线程和连接池。这是推荐的模式,因为对于事件循环并发,首选的是固定的共享资源。在这种模式下,全局资源在流程退出之前一直处于活动状态。

如果服务器与进程同步,则通常不需要显式关机。然而,如果服务器可以在进程中启动或停止(例如, Spring MVC 应用程序部署为 WAR),可以用`globalResources=true`(默认)声明类型`ReactorResourceFactory`的 Spring-管理的 Bean,以确保在 Spring `ApplicationContext`关闭时关闭反应堆网络全局资源,如下例所示:

爪哇

```
@Bean
public ReactorResourceFactory reactorResourceFactory() {
    return new ReactorResourceFactory();
}
```

Kotlin

```
@Bean
fun reactorResourceFactory() = ReactorResourceFactory()
```

你也可以选择不参与全球反应堆网状资源。但是,在这种模式下,要确保所有 Reactor Netty 客户机和服务器实例都使用共享资源是你的责任,如下例所示:

爪哇

```
@Bean
public ReactorResourceFactory resourceFactory() {
    ReactorResourceFactory factory = new ReactorResourceFactory();
    factory.setUseGlobalResources(false); (1)
    return factory;
}

@Bean
public WebClient webClient() {

    Function<HttpClient, HttpClient> mapper = client -> {
        // Further customizations...
    };

    ClientHttpConnector connector =
            new ReactorClientHttpConnector(resourceFactory(), mapper); (2)

    return WebClient.builder().clientConnector(connector).build(); (3)
}
```

|**1**|创造独立于全球资源的资源。|
|-----|-----------------------------------------------------------------------|
|**2**|在资源工厂中使用`ReactorClientHttpConnector`构造函数。|
|**3**|将连接器插入`WebClient.Builder`。|

Kotlin

```
@Bean
fun resourceFactory() = ReactorResourceFactory().apply {
    isUseGlobalResources = false (1)
}

@Bean
fun webClient(): WebClient {

    val mapper: (HttpClient) -> HttpClient = {
        // Further customizations...
    }

    val connector = ReactorClientHttpConnector(resourceFactory(), mapper) (2)

    return WebClient.builder().clientConnector(connector).build() (3)
}
```

|**1**|创造独立于全球资源的资源。|
|-----|-----------------------------------------------------------------------|
|**2**|在资源工厂中使用`ReactorClientHttpConnector`构造函数。|
|**3**|将连接器插入`WebClient.Builder`。|

##### 超时

要配置连接超时:

爪哇

```
import io.netty.channel.ChannelOption;

HttpClient httpClient = HttpClient.create()
        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);

WebClient webClient = WebClient.builder()
        .clientConnector(new ReactorClientHttpConnector(httpClient))
        .build();
```

Kotlin

```
import io.netty.channel.ChannelOption

val httpClient = HttpClient.create()
        .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);

val webClient = WebClient.builder()
        .clientConnector(new ReactorClientHttpConnector(httpClient))
        .build();
```

要配置读或写超时:

爪哇

```
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;

HttpClient httpClient = HttpClient.create()
        .doOnConnected(conn -> conn
                .addHandlerLast(new ReadTimeoutHandler(10))
                .addHandlerLast(new WriteTimeoutHandler(10)));

// Create WebClient...
```

Kotlin

```
import io.netty.handler.timeout.ReadTimeoutHandler
import io.netty.handler.timeout.WriteTimeoutHandler

val httpClient = HttpClient.create()
        .doOnConnected { conn -> conn
                .addHandlerLast(new ReadTimeoutHandler(10))
                .addHandlerLast(new WriteTimeoutHandler(10))
        }

// Create WebClient...
```

要为所有请求配置响应超时:

爪哇

```
HttpClient httpClient = HttpClient.create()
        .responseTimeout(Duration.ofSeconds(2));

// Create WebClient...
```

Kotlin

```
val httpClient = HttpClient.create()
        .responseTimeout(Duration.ofSeconds(2));

// Create WebClient...
```

要为特定请求配置响应超时:

爪哇

```
WebClient.create().get()
        .uri("https://example.org/path")
        .httpRequest(httpRequest -> {
            HttpClientRequest reactorRequest = httpRequest.getNativeRequest();
            reactorRequest.responseTimeout(Duration.ofSeconds(2));
        })
        .retrieve()
        .bodyToMono(String.class);
```

Kotlin

```
WebClient.create().get()
        .uri("https://example.org/path")
        .httpRequest { httpRequest: ClientHttpRequest ->
            val reactorRequest = httpRequest.getNativeRequest<HttpClientRequest>()
            reactorRequest.responseTimeout(Duration.ofSeconds(2))
        }
        .retrieve()
        .bodyToMono(String::class.java)
```

#### 2.1.3. Jetty

下面的示例展示了如何自定义 Jetty `HttpClient`设置:

Java

```
HttpClient httpClient = new HttpClient();
httpClient.setCookieStore(...);

WebClient webClient = WebClient.builder()
        .clientConnector(new JettyClientHttpConnector(httpClient))
        .build();
```

Kotlin

```
val httpClient = HttpClient()
httpClient.cookieStore = ...

val webClient = WebClient.builder()
        .clientConnector(new JettyClientHttpConnector(httpClient))
        .build();
```

默认情况下,`HttpClient`创建自己的资源(`Executor``ByteBufferPool``Scheduler`),这些资源在进程退出或调用`stop()`之前一直处于活动状态。

你可以在 Jetty 客户机(和服务器)的多个实例之间共享资源,并通过声明类型为`JettyResourceFactory`的 Spring 管理的 Bean 来确保在关闭 Spring `ApplicationContext`时关闭资源,如下例所示:

Java

```
@Bean
public JettyResourceFactory resourceFactory() {
    return new JettyResourceFactory();
}

@Bean
public WebClient webClient() {

    HttpClient httpClient = new HttpClient();
    // Further customizations...

    ClientHttpConnector connector =
            new JettyClientHttpConnector(httpClient, resourceFactory()); (1)

    return WebClient.builder().clientConnector(connector).build(); (2)
}
```

|**1**|在资源工厂中使用`JettyClientHttpConnector`构造函数。|
|-----|---------------------------------------------------------------------|
|**2**|将连接器插入`WebClient.Builder`。|

Kotlin

```
@Bean
fun resourceFactory() = JettyResourceFactory()

@Bean
fun webClient(): WebClient {

    val httpClient = HttpClient()
    // Further customizations...

    val connector = JettyClientHttpConnector(httpClient, resourceFactory()) (1)

    return WebClient.builder().clientConnector(connector).build() (2)
}
```

|**1**|在资源工厂中使用`JettyClientHttpConnector`构造函数。|
|-----|---------------------------------------------------------------------|
|**2**|将连接器插入`WebClient.Builder`。|

#### 2.1.4.HttpComponents

下面的示例展示了如何定制 Apache HttpComponents`HttpClient`设置:

Java

```
HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom();
clientBuilder.setDefaultRequestConfig(...);
CloseableHttpAsyncClient client = clientBuilder.build();
ClientHttpConnector connector = new HttpComponentsClientHttpConnector(client);

WebClient webClient = WebClient.builder().clientConnector(connector).build();
```

Kotlin

```
val client = HttpAsyncClients.custom().apply {
    setDefaultRequestConfig(...)
}.build()
val connector = HttpComponentsClientHttpConnector(client)
val webClient = WebClient.builder().clientConnector(connector).build()
```

### 2.2.`retrieve()`

`retrieve()`方法可用于声明如何提取响应。例如:

Java

```
WebClient client = WebClient.create("https://example.org");

Mono<ResponseEntity<Person>> result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .toEntity(Person.class);
```

Kotlin

```
val client = WebClient.create("https://example.org")

val result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .toEntity<Person>().awaitSingle()
```

或者只得到身体:

Java

```
WebClient client = WebClient.create("https://example.org");

Mono<Person> result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .bodyToMono(Person.class);
```

Kotlin

```
val client = WebClient.create("https://example.org")

val result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .awaitBody<Person>()
```

要获取已解码对象的流:

Java

```
Flux<Quote> result = client.get()
        .uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
        .retrieve()
        .bodyToFlux(Quote.class);
```

Kotlin

```
val result = client.get()
        .uri("/quotes").accept(MediaType.TEXT_EVENT_STREAM)
        .retrieve()
        .bodyToFlow<Quote>()
```

默认情况下,4xx 或 5xx 响应会导致`WebClientResponseException`,包括用于特定 HTTP 状态代码的子类。要自定义错误响应的处理,请使用`onStatus`处理程序,如下所示:

Java

```
Mono<Person> result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .onStatus(HttpStatus::is4xxClientError, response -> ...)
        .onStatus(HttpStatus::is5xxServerError, response -> ...)
        .bodyToMono(Person.class);
```

Kotlin

```
val result = client.get()
        .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
        .retrieve()
        .onStatus(HttpStatus::is4xxClientError) { ... }
        .onStatus(HttpStatus::is5xxServerError) { ... }
        .awaitBody<Person>()
```

### 2.3.交换

Kotlin 中的`exchangeToMono()``exchangeToFlux()`方法(或`awaitExchange { }``exchangeToFlow { }`)对于需要更多控制的更高级情况是有用的,例如根据响应状态对响应进行不同的解码:

Java

```
Mono<Person> entityMono = client.get()
        .uri("/persons/1")
        .accept(MediaType.APPLICATION_JSON)
        .exchangeToMono(response -> {
            if (response.statusCode().equals(HttpStatus.OK)) {
                return response.bodyToMono(Person.class);
            }
            else {
                // Turn to error
                return response.createException().flatMap(Mono::error);
            }
        });
```

Kotlin

```
val entity = client.get()
  .uri("/persons/1")
  .accept(MediaType.APPLICATION_JSON)
  .awaitExchange {
        if (response.statusCode() == HttpStatus.OK) {
             return response.awaitBody<Person>()
        }
        else {
             throw response.createExceptionAndAwait()
        }
  }
```

当使用上述方法时,在返回的`Mono``Flux`完成后,将检查响应体,如果没有使用它,则释放它,以防止内存和连接泄漏。因此,响应不能在更下游的地方被解码。如果需要,由提供的函数声明如何解码响应。

### 2.4.请求主体

请求主体可以从`ReactiveAdapterRegistry`处理的任何异步类型进行编码,例如`Mono`或 Kotlin 协程`Deferred`,如下例所示:

Java

```
Mono<Person> personMono = ... ;

Mono<Void> result = client.post()
        .uri("/persons/{id}", id)
        .contentType(MediaType.APPLICATION_JSON)
        .body(personMono, Person.class)
        .retrieve()
        .bodyToMono(Void.class);
```

Kotlin

```
val personDeferred: Deferred<Person> = ...

client.post()
        .uri("/persons/{id}", id)
        .contentType(MediaType.APPLICATION_JSON)
        .body<Person>(personDeferred)
        .retrieve()
        .awaitBody<Unit>()
```

还可以对对象流进行编码,如下例所示:

Java

```
Flux<Person> personFlux = ... ;

Mono<Void> result = client.post()
        .uri("/persons/{id}", id)
        .contentType(MediaType.APPLICATION_STREAM_JSON)
        .body(personFlux, Person.class)
        .retrieve()
        .bodyToMono(Void.class);
```

Kotlin

```
val people: Flow<Person> = ...

client.post()
        .uri("/persons/{id}", id)
        .contentType(MediaType.APPLICATION_JSON)
        .body(people)
        .retrieve()
        .awaitBody<Unit>()
```

或者,如果你有实际值,你可以使用`bodyValue`快捷方式,如下例所示:

Java

```
Person person = ... ;

Mono<Void> result = client.post()
        .uri("/persons/{id}", id)
        .contentType(MediaType.APPLICATION_JSON)
        .bodyValue(person)
        .retrieve()
        .bodyToMono(Void.class);
```

Kotlin

```
val person: Person = ...

client.post()
        .uri("/persons/{id}", id)
        .contentType(MediaType.APPLICATION_JSON)
        .bodyValue(person)
        .retrieve()
        .awaitBody<Unit>()
```

#### 2.4.1.表单数据

要发送表单数据,可以提供`MultiValueMap<String, String>`作为主体。请注意,内容由`FormHttpMessageWriter`自动设置为`application/x-www-form-urlencoded`。下面的示例展示了如何使用`MultiValueMap<String, String>`:

Java

```
MultiValueMap<String, String> formData = ... ;

Mono<Void> result = client.post()
        .uri("/path", id)
        .bodyValue(formData)
        .retrieve()
        .bodyToMono(Void.class);
```

Kotlin

```
val formData: MultiValueMap<String, String> = ...

client.post()
        .uri("/path", id)
        .bodyValue(formData)
        .retrieve()
        .awaitBody<Unit>()
```

还可以使用`BodyInserters`在线提供表单数据,如下例所示:

Java

```
import static org.springframework.web.reactive.function.BodyInserters.*;

Mono<Void> result = client.post()
        .uri("/path", id)
        .body(fromFormData("k1", "v1").with("k2", "v2"))
        .retrieve()
        .bodyToMono(Void.class);
```

Kotlin

```
import org.springframework.web.reactive.function.BodyInserters.*

client.post()
        .uri("/path", id)
        .body(fromFormData("k1", "v1").with("k2", "v2"))
        .retrieve()
        .awaitBody<Unit>()
```

#### 2.4.2.多部分数据

要发送多部分数据,你需要提供一个`MultiValueMap<String, ?>`,其值要么是表示部分内容的`Object`实例,要么是表示部分内容和标题的`HttpEntity`实例。`MultipartBodyBuilder`提供了一个方便的 API 来准备多部分请求。下面的示例展示了如何创建`MultiValueMap<String, ?>`:

Java

```
MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.part("fieldPart", "fieldValue");
builder.part("filePart1", new FileSystemResource("...logo.png"));
builder.part("jsonPart", new Person("Jason"));
builder.part("myPart", part); // Part from a server request

MultiValueMap<String, HttpEntity<?>> parts = builder.build();
```

Kotlin

```
val builder = MultipartBodyBuilder().apply {
    part("fieldPart", "fieldValue")
    part("filePart1", new FileSystemResource("...logo.png"))
    part("jsonPart", new Person("Jason"))
    part("myPart", part) // Part from a server request
}

val parts = builder.build()
```

在大多数情况下,你不必为每个部分指定`Content-Type`。内容类型是根据用于序列化它的`HttpMessageWriter`自动确定的,或者在`Resource`的情况下,根据文件扩展名自动确定的。如果有必要,你可以通过重载的构建器`part`方法之一,显式地为每个部分提供`MediaType`

一旦准备好`MultiValueMap`,将其传递给`WebClient`的最简单方法是通过`body`方法,如下例所示:

Java

```
MultipartBodyBuilder builder = ...;

Mono<Void> result = client.post()
        .uri("/path", id)
        .body(builder.build())
        .retrieve()
        .bodyToMono(Void.class);
```

Kotlin

```
val builder: MultipartBodyBuilder = ...

client.post()
        .uri("/path", id)
        .body(builder.build())
        .retrieve()
        .awaitBody<Unit>()
```

如果`MultiValueMap`包含至少一个非`String`值,该值也可以表示常规的表单数据(即`application/x-www-form-urlencoded`),则无需将`Content-Type`设置为`multipart/form-data`。当使用`MultipartBodyBuilder`时总是这样,这确保了`HttpEntity`包装器。

作为`MultipartBodyBuilder`的替代方案,你还可以通过内置的`BodyInserters`提供内联样式的多部分内容,如下例所示:

Java

```
import static org.springframework.web.reactive.function.BodyInserters.*;

Mono<Void> result = client.post()
        .uri("/path", id)
        .body(fromMultipartData("fieldPart", "value").with("filePart", resource))
        .retrieve()
        .bodyToMono(Void.class);
```

Kotlin

```
import org.springframework.web.reactive.function.BodyInserters.*

client.post()
        .uri("/path", id)
        .body(fromMultipartData("fieldPart", "value").with("filePart", resource))
        .retrieve()
        .awaitBody<Unit>()
```

### 2.5.过滤器

你可以通过`WebClient.Builder`注册一个客户端过滤器(`ExchangeFilterFunction`),以便拦截和修改请求,如下例所示:

Java

```
WebClient client = WebClient.builder()
        .filter((request, next) -> {

            ClientRequest filtered = ClientRequest.from(request)
                    .header("foo", "bar")
                    .build();

            return next.exchange(filtered);
        })
        .build();
```

Kotlin

```
val client = WebClient.builder()
        .filter { request, next ->

            val filtered = ClientRequest.from(request)
                    .header("foo", "bar")
                    .build()

            next.exchange(filtered)
        }
        .build()
```

这可以用于跨领域的关注,例如身份验证。下面的示例使用一个过滤器通过静态工厂方法进行基本身份验证:

Java

```
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;

WebClient client = WebClient.builder()
        .filter(basicAuthentication("user", "password"))
        .build();
```

Kotlin

```
import org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication

val client = WebClient.builder()
        .filter(basicAuthentication("user", "password"))
        .build()
```

可以通过更改现有的`WebClient`实例来添加或删除过滤器,从而生成一个不影响原始实例的新`WebClient`实例。例如:

Java

```
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;

WebClient client = webClient.mutate()
        .filters(filterList -> {
            filterList.add(0, basicAuthentication("user", "password"));
        })
        .build();
```

Kotlin

```
val client = webClient.mutate()
        .filters { it.add(0, basicAuthentication("user", "password")) }
        .build()
```

`WebClient`是围绕过滤器链的一个薄的外观,然后是`ExchangeFunction`。它提供了一个工作流,用于发出请求,对来自更高级别的对象进行编码,并有助于确保始终使用响应内容。当过滤器以某种方式处理响应时,必须格外小心,以始终使用其内容,或以其他方式将其向下游传播到`WebClient`,这将确保相同的结果。下面是一个过滤器,它处理`UNAUTHORIZED`状态代码,但确保释放任何响应内容(无论是否期望):

Java

```
public ExchangeFilterFunction renewTokenFilter() {
    return (request, next) -> next.exchange(request).flatMap(response -> {
        if (response.statusCode().value() == HttpStatus.UNAUTHORIZED.value()) {
            return response.releaseBody()
                    .then(renewToken())
                    .flatMap(token -> {
                        ClientRequest newRequest = ClientRequest.from(request).build();
                        return next.exchange(newRequest);
                    });
        } else {
            return Mono.just(response);
        }
    });
}
```

Kotlin

```
fun renewTokenFilter(): ExchangeFilterFunction? {
    return ExchangeFilterFunction { request: ClientRequest?, next: ExchangeFunction ->
        next.exchange(request!!).flatMap { response: ClientResponse ->
            if (response.statusCode().value() == HttpStatus.UNAUTHORIZED.value()) {
                [email protected] response.releaseBody()
                        .then(renewToken())
                        .flatMap { token: String? ->
                            val newRequest = ClientRequest.from(request).build()
                            next.exchange(newRequest)
                        }
            } else {
                [email protected] Mono.just(response)
            }
        }
    }
}
```

### 2.6.属性

你可以向请求添加属性。如果你希望通过筛选链传递信息并影响给定请求的筛选器的行为,这是很方便的。例如:

Java

```
WebClient client = WebClient.builder()
        .filter((request, next) -> {
            Optional<Object> usr = request.attribute("myAttribute");
            // ...
        })
        .build();

client.get().uri("https://example.org/")
        .attribute("myAttribute", "...")
        .retrieve()
        .bodyToMono(Void.class);

    }
```

Kotlin

```
val client = WebClient.builder()
        .filter { request, _ ->
            val usr = request.attributes()["myAttribute"];
            // ...
        }
        .build()

    client.get().uri("https://example.org/")
            .attribute("myAttribute", "...")
            .retrieve()
            .awaitBody<Unit>()
```

请注意,你可以在`WebClient.Builder`级别全局配置`defaultRequest`回调,它允许你将属性插入到所有请求中,例如,可以在 Spring MVC 应用程序中使用它来基于`ThreadLocal`数据填充请求属性。

### 2.7.上下文

[Attributes](#webflux-client-attributes)提供了一种将信息传递到过滤器链的方便方式,但它们只会影响当前的请求。如果你想要传递传播到嵌套的其他请求的信息,例如通过`flatMap`,或者在之后执行,例如通过`concatMap`,那么你将需要使用反应器`Context`

反应器`Context`需要在反应链的末端填充,以便应用于所有操作。例如:

Java

```
WebClient client = WebClient.builder()
        .filter((request, next) ->
                Mono.deferContextual(contextView -> {
                    String value = contextView.get("foo");
                    // ...
                }))
        .build();

client.get().uri("https://example.org/")
        .retrieve()
        .bodyToMono(String.class)
        .flatMap(body -> {
                // perform nested request (context propagates automatically)...
        })
        .contextWrite(context -> context.put("foo", ...));
```

### 2.8.同步使用

`WebClient`可以以同步方式使用,方法是在末尾对结果进行阻塞:

Java

```
Person person = client.get().uri("/person/{id}", i).retrieve()
    .bodyToMono(Person.class)
    .block();

List<Person> persons = client.get().uri("/persons").retrieve()
    .bodyToFlux(Person.class)
    .collectList()
    .block();
```

Kotlin

```
val person = runBlocking {
    client.get().uri("/person/{id}", i).retrieve()
            .awaitBody<Person>()
}

val persons = runBlocking {
    client.get().uri("/persons").retrieve()
            .bodyToFlow<Person>()
            .toList()
}
```

但是,如果需要进行多个调用,那么更有效的方法是避免单个地阻塞每个响应,而是等待合并的结果:

Java

```
Mono<Person> personMono = client.get().uri("/person/{id}", personId)
        .retrieve().bodyToMono(Person.class);

Mono<List<Hobby>> hobbiesMono = client.get().uri("/person/{id}/hobbies", personId)
        .retrieve().bodyToFlux(Hobby.class).collectList();

Map<String, Object> data = Mono.zip(personMono, hobbiesMono, (person, hobbies) -> {
            Map<String, String> map = new LinkedHashMap<>();
            map.put("person", person);
            map.put("hobbies", hobbies);
            return map;
        })
        .block();
```

Kotlin

```
val data = runBlocking {
        val personDeferred = async {
            client.get().uri("/person/{id}", personId)
                    .retrieve().awaitBody<Person>()
        }

        val hobbiesDeferred = async {
            client.get().uri("/person/{id}/hobbies", personId)
                    .retrieve().bodyToFlow<Hobby>().toList()
        }

        mapOf("person" to personDeferred.await(), "hobbies" to hobbiesDeferred.await())
    }
```

以上只是一个例子。还有很多其他模式和操作员可以将反应性管道组合在一起,从而进行许多远程调用,可能是一些嵌套的、相互依赖的调用,并且直到最后都不会阻塞。

|   |使用`Flux``Mono`,你应该永远不需要在 Spring MVC 或 Spring WebFlux 控制器中进行阻塞。<br/>只需从控制器方法返回得到的反应性类型。同样的原理也适用于<br/> Kotlin 协程和 Spring WebFlux,只需使用悬挂函数或在你的`Flow`控制器中返回<br/>方法。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

### 2.9.测试

要测试使用`WebClient`的代码,可以使用模拟 Web 服务器,例如[OKHTTP MockWebServer](https://github.com/square/okhttp#mockwebserver)。要查看它的使用示例,请查看 Spring Framework 测试套件中的[`WebClientIntegrationTests`](https://github.com/ Spring-projects/ Spring-framework/tree/main/ Spring-webflux/SRC/test/java/org/springframework/web/active/function/client/webclientintegrationtests.java)或[<gtr="2055"/>](https:/giaster/tsquare/kmaster/okthub/samples/static-server)存储库中的

## 3. WebSockets

[Same as in the Servlet stack](web.html#websocket)

参考文档的这一部分涵盖了对 Reactive-Stack WebSocket 消息传递的支持。

### 3.1. WebSocket 介绍

WebSocket 协议[RFC 6455](https://tools.ietf.org/html/rfc6455)提供了一种标准化的方式,通过单个 TCP 连接在客户机和服务器之间建立全双工、双向通信通道。它是一种与 HTTP 不同的 TCP 协议,但其设计是通过 HTTP 工作的,使用端口 80 和 443,并允许重用现有的防火墙规则。

WebSocket 交互以 HTTP 请求开始,该 HTTP 请求使用 HTTP头来升级或在这种情况下切换到 WebSocket 协议。下面的示例展示了这样的交互:

```
GET /spring-websocket-portfolio/portfolio HTTP/1.1
Host: localhost:8080
Upgrade: websocket (1)
Connection: Upgrade (2)
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13
Origin: http://localhost:8080
```

|**1**|`Upgrade`标头。|
|-----|-------------------------------|
|**2**|使用`Upgrade`连接。|

具有 WebSocket 支持的服务器将返回类似于以下内容的输出,而不是通常的 200 状态代码:

```
HTTP/1.1 101 Switching Protocols (1)
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp
```

|**1**|协议转换|
|-----|---------------|

在成功握手之后,HTTP 升级请求中的 TCP 套接字仍然是开放的,以便客户机和服务器继续发送和接收消息。

关于 WebSockets 如何工作的完整介绍超出了本文的范围。参见 RFC6455,HTML5 的 WebSocket 章,或者 Web 上的许多介绍和教程中的任何一个。

注意,如果 WebSocket 服务器运行在 Web 服务器(例如 Nginx)的后面,则可能需要将其配置为将 WebSocket 升级请求传递到 WebSocket 服务器。同样,如果应用程序在云环境中运行,则检查与 WebSocket 支持相关的云提供商的指令。

#### 3.1.1.HTTP 与 WebSocket

尽管 WebSocket 的设计是与 HTTP 兼容的,并且以 HTTP 请求开始,但重要的是要理解这两个协议导致了非常不同的体系结构和应用程序编程模型。

在 HTTP 和 REST 中,应用程序被建模为许多 URL。为了与应用程序交互,客户端访问这些 URL,请求-响应样式。服务器根据 HTTP URL、方法和标头将请求路由到适当的处理程序。

相比之下,在 WebSockets 中,初始连接通常只有一个 URL。随后,所有应用程序消息都在相同的 TCP 连接上流动。这指向了一种完全不同的异步、事件驱动的消息传递体系结构。

WebSocket 也是一种低级传输协议,它与 HTTP 不同,不对消息的内容规定任何语义。这意味着,除非客户机和服务器在消息语义上达成一致,否则就没有路由或处理消息的方法。

WebSocket 客户端和服务器可以协商使用更高级别的消息传递协议(例如,STOMP),通过Header 上的 HTTP 握手请求。如果不能做到这一点,他们就需要拿出自己的惯例。

#### 3.1.2.何时使用 WebSockets

WebSockets 可以使 Web 页面具有动态性和交互性。然而,在许多情况下,Ajax 和 HTTP 流或长轮询的组合可以提供简单有效的解决方案。

例如,新闻、邮件和社交提要需要动态更新,但每隔几分钟更新一次可能完全没问题。另一方面,协作、游戏和金融应用程序需要更接近实时。

延迟本身并不是一个决定因素。如果消息量相对较低(例如,监视网络故障),则 HTTP 流或轮询可以提供有效的解决方案。正是低延迟、高频率和高音量的组合为 WebSocket 的使用提供了最佳的条件。

还请记住,在 Internet 上,超出你控制范围的限制性代理可能会阻止 WebSocket 交互,这是因为它们未被配置为传递`Upgrade`头,或者是因为它们关闭了似乎空闲的长期连接。这意味着对防火墙内的内部应用程序使用 WebSocket 比对面向公众的应用程序使用 WebSocket 是一个更直接的决策。

### 3.2. WebSocket API

[Same as in the Servlet stack](web.html#websocket-server)

Spring 框架提供了一个 WebSocket API,你可以使用它编写处理 WebSocket 消息的客户端和服务器端应用程序。

#### 3.2.1.服务器

[Same as in the Servlet stack](web.html#websocket-server-handler)

要创建 WebSocket 服务器,你可以首先创建`WebSocketHandler`。下面的示例展示了如何做到这一点:

爪哇

```
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.WebSocketSession;

public class MyWebSocketHandler implements WebSocketHandler {

    @Override
    public Mono<Void> handle(WebSocketSession session) {
        // ...
    }
}
```

Kotlin

```
import org.springframework.web.reactive.socket.WebSocketHandler
import org.springframework.web.reactive.socket.WebSocketSession

class MyWebSocketHandler : WebSocketHandler {

    override fun handle(session: WebSocketSession): Mono<Void> {
        // ...
    }
}
```

然后你可以将它映射到一个 URL:

爪哇

```
@Configuration
class WebConfig {

    @Bean
    public HandlerMapping handlerMapping() {
        Map<String, WebSocketHandler> map = new HashMap<>();
        map.put("/path", new MyWebSocketHandler());
        int order = -1; // before annotated controllers

        return new SimpleUrlHandlerMapping(map, order);
    }
}
```

Kotlin

```
@Configuration
class WebConfig {

    @Bean
    fun handlerMapping(): HandlerMapping {
        val map = mapOf("/path" to MyWebSocketHandler())
        val order = -1 // before annotated controllers

        return SimpleUrlHandlerMapping(map, order)
    }
}
```

如果使用[WebFlux 配置](#webflux-config),则没有更多的事情要做,或者如果不使用 WebFlux 配置,则需要声明`WebSocketHandlerAdapter`,如下所示:

爪哇

```
@Configuration
class WebConfig {

    // ...

    @Bean
    public WebSocketHandlerAdapter handlerAdapter() {
        return new WebSocketHandlerAdapter();
    }
}
```

Kotlin

```
@Configuration
class WebConfig {

    // ...

    @Bean
    fun handlerAdapter() =  WebSocketHandlerAdapter()
}
```

#### 3.2.2.`WebSocketHandler`

`handle``WebSocketHandler`方法接受`WebSocketSession`并返回`Mono<Void>`,以指示会话的应用程序处理完成时。会话通过两个流处理,一个用于入站消息,另一个用于出站消息。下表描述了处理流的两种方法:

|          `WebSocketSession` method           |说明|
|----------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------|
|      `Flux<WebSocketMessage> receive()`      |提供对入站消息流的访问,并在连接关闭时完成。|
|`Mono<Void> send(Publisher<WebSocketMessage>)`|获取传出消息的源,写入消息,并返回一个`Mono<Void>`,当源完成并写入完成时,该<br/>完成。|

a`WebSocketHandler`必须将入站和出站流组合成一个统一的流,并返回一个`Mono<Void>`,该流反映了该流的完成。根据应用程序的需求,统一流在以下情况下完成:

* 入站消息流或出站消息流已完成。

* 入站流完成(即连接关闭),而出站流是无限的。

* 在选定的点上,通过`close``WebSocketSession`方法。

当入站和出站消息流组合在一起时,不需要检查连接是否打开,因为反应流信号结束活动。入站流接收完成或错误信号,出站流接收取消信号。

处理程序的最基本实现是处理入站流的实现。下面的示例展示了这样的实现:

爪哇

```
class ExampleHandler implements WebSocketHandler {

    @Override
    public Mono<Void> handle(WebSocketSession session) {
        return session.receive()            (1)
                .doOnNext(message -> {
                    // ...                  (2)
                })
                .concatMap(message -> {
                    // ...                  (3)
                })
                .then();                    (4)
    }
}
```

|**1**|访问入站消息流。|
|-----|--------------------------------------------------------------------|
|**2**|对每条信息都做些什么。|
|**3**|执行使用消息内容的嵌套异步操作。|
|**4**|返回在接收完成时完成的`Mono<Void>`。|

Kotlin

```
class ExampleHandler : WebSocketHandler {

    override fun handle(session: WebSocketSession): Mono<Void> {
        return session.receive()            (1)
                .doOnNext {
                    // ...                  (2)
                }
                .concatMap {
                    // ...                  (3)
                }
                .then()                     (4)
    }
}
```

|**1**|访问入站消息流。|
|-----|--------------------------------------------------------------------|
|**2**|对每条信息都做些什么。|
|**3**|执行使用消息内容的嵌套异步操作。|
|**4**|返回在接收完成时完成的`Mono<Void>`。|

|   |对于嵌套的异步操作,你可能需要在使用池数据缓冲区的底层<br/>服务器(例如 Netty)上调用`message.retain()`。否则,数据缓冲区可能会在有机会读取数据之前<br/>释放。有关更多背景信息,请参见[数据缓冲区和编解码器](core.html#databuffers)。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

以下实现合并了入站和出站流:

爪哇

```
class ExampleHandler implements WebSocketHandler {

    @Override
    public Mono<Void> handle(WebSocketSession session) {

        Flux<WebSocketMessage> output = session.receive()               (1)
                .doOnNext(message -> {
                    // ...
                })
                .concatMap(message -> {
                    // ...
                })
                .map(value -> session.textMessage("Echo " + value));    (2)

        return session.send(output);                                    (3)
    }
}
```

|**1**|处理入站消息流。|
|-----|--------------------------------------------------------------------------|
|**2**|创建出站消息,生成一个合并的流。|
|**3**|返回一个`Mono<Void>`,它在我们继续接收时不完成。|

Kotlin

```
class ExampleHandler : WebSocketHandler {

    override fun handle(session: WebSocketSession): Mono<Void> {

        val output = session.receive()                      (1)
                .doOnNext {
                    // ...
                }
                .concatMap {
                    // ...
                }
                .map { session.textMessage("Echo $it") }    (2)

        return session.send(output)                         (3)
    }
}
```

|**1**|处理入站消息流。|
|-----|--------------------------------------------------------------------------|
|**2**|创建出站消息,生成一个合并的流。|
|**3**|返回一个`Mono<Void>`,它在我们继续接收时不完成。|

入站和出站流可以是独立的,并且仅在完成时才加入,如下例所示:

爪哇

```
class ExampleHandler implements WebSocketHandler {

    @Override
    public Mono<Void> handle(WebSocketSession session) {

        Mono<Void> input = session.receive()                                (1)
                .doOnNext(message -> {
                    // ...
                })
                .concatMap(message -> {
                    // ...
                })
                .then();

        Flux<String> source = ... ;
        Mono<Void> output = session.send(source.map(session::textMessage)); (2)

        return Mono.zip(input, output).then();                              (3)
    }
}
```

|**1**|处理入站消息流。|
|-----|----------------------------------------------------------------------------------|
|**2**|发送外发消息。|
|**3**|加入这些流并返回一个`Mono<Void>`,当任一流结束时完成。|

Kotlin

```
class ExampleHandler : WebSocketHandler {

    override fun handle(session: WebSocketSession): Mono<Void> {

        val input = session.receive()                                   (1)
                .doOnNext {
                    // ...
                }
                .concatMap {
                    // ...
                }
                .then()

        val source: Flux<String> = ...
        val output = session.send(source.map(session::textMessage))     (2)

        return Mono.zip(input, output).then()                           (3)
    }
}
```

|**1**|处理入站消息流。|
|-----|----------------------------------------------------------------------------------|
|**2**|发送外发消息。|
|**3**|加入这些流并返回一个`Mono<Void>`,当任一流结束时完成。|

#### 3.2.3.`DataBuffer`

`DataBuffer`是 WebFlux 中字节缓冲区的表示形式。参考的 Spring 核心部分在[数据缓冲区和编解码器](core.html#databuffers)一节中对此有更多的说明。要理解的关键点是,在某些服务器(如 Netty)上,字节缓冲区是池的,引用也是计算的,并且必须在使用时释放,以避免内存泄漏。

在 Netty 上运行时,如果应用程序希望保留输入数据缓冲区,则必须使用`DataBufferUtils.retain(dataBuffer)`,以确保它们不会被释放,然后在使用缓冲区时使用`DataBufferUtils.release(dataBuffer)`

#### 3.2.4.握手

[Same as in the Servlet stack](web.html#websocket-server-handshake)

`WebSocketHandlerAdapter`委托给`WebSocketService`。默认情况下,这是`HandshakeWebSocketService`的一个实例,它对 WebSocket 请求执行基本检查,然后对正在使用的服务器使用`RequestUpgradeStrategy`。目前,有对反应堆网状物、 Tomcat、 Jetty 和 Undertow 的内置支持。

`HandshakeWebSocketService`公开了一个`sessionAttributePredicate`属性,该属性允许设置`Predicate<String>`以从`WebSession`中提取属性,并将它们插入到`WebSocketSession`的属性中。

#### 3.2.5.服务器配置

[Same as in the Servlet stack](web.html#websocket-server-runtime-configuration)

每个服务器的`RequestUpgradeStrategy`公开了特定于底层 WebSocket 服务器引擎的配置。当使用 WebFlux 爪哇 Config 时,你可以自定义这些属性,如[WebFlux 配置](#webflux-config-websocket-service)的相应部分所示,或者如果不使用 WebFlux Config,则使用以下方法:

爪哇

```
@Configuration
class WebConfig {

    @Bean
    public WebSocketHandlerAdapter handlerAdapter() {
        return new WebSocketHandlerAdapter(webSocketService());
    }

    @Bean
    public WebSocketService webSocketService() {
        TomcatRequestUpgradeStrategy strategy = new TomcatRequestUpgradeStrategy();
        strategy.setMaxSessionIdleTimeout(0L);
        return new HandshakeWebSocketService(strategy);
    }
}
```

Kotlin

```
@Configuration
class WebConfig {

    @Bean
    fun handlerAdapter() =
            WebSocketHandlerAdapter(webSocketService())

    @Bean
    fun webSocketService(): WebSocketService {
        val strategy = TomcatRequestUpgradeStrategy().apply {
            setMaxSessionIdleTimeout(0L)
        }
        return HandshakeWebSocketService(strategy)
    }
}
```

检查你的服务器的升级策略,看看有哪些选项可用。目前,只有 Tomcat 和 Jetty 公开了这样的选项。

#### 3.2.6.科尔斯

[Same as in the Servlet stack](web.html#websocket-server-allowed-origins)

配置 CORS 并限制对 WebSocket 端点的访问的最简单方法是让你的`WebSocketHandler`实现`CorsConfigurationSource`并返回一个`CorsConfiguration`,其中包含允许的源代码、标头和其他详细信息。如果无法做到这一点,还可以在`SimpleUrlHandler`上设置`corsConfigurations`属性,以通过 URL 模式指定 CORS 设置。如果指定了这两个参数,则使用`combine`上的`CorsConfiguration`方法将它们合并。

#### 3.2.7.客户

Spring WebFlux 提供了一个`WebSocketClient`抽象,其中包含用于反应器 Netty、 Tomcat、 Jetty、 Undertow 和标准 爪哇(即 JSR-356)的实现。

|   |Tomcat 客户机实际上是标准 爪哇 One 的扩展,它在`WebSocketSession`处理中具有一些额外的<br/>功能,以利用 Tomcat 特定的<br/>API 来暂停接收用于回压的消息。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

要启动 WebSocket 会话,你可以创建客户机的实例,并使用其`execute`方法:

爪哇

```
WebSocketClient client = new ReactorNettyWebSocketClient();

URI url = new URI("ws://localhost:8080/path");
client.execute(url, session ->
        session.receive()
                .doOnNext(System.out::println)
                .then());
```

Kotlin

```
val client = ReactorNettyWebSocketClient()

        val url = URI("ws://localhost:8080/path")
        client.execute(url) { session ->
            session.receive()
                    .doOnNext(::println)
            .then()
        }
```

一些客户机,例如 Jetty,实现了`Lifecycle`,并且需要在可以使用它们之前停止并启动它们。所有客户端都具有与底层 WebSocket 客户端的配置相关的构造函数选项。

## 4. 测试

[Same in Spring MVC](web.html#testing)

`spring-test`模块提供了`ServerHttpRequest``ServerHttpResponse``ServerWebExchange`的模拟实现。有关模拟对象的讨论,请参见[Spring Web Reactive](testing.html#mock-objects-web-reactive)

[`WebTestClient`](Testing.html#WebTestClient)构建在这些模拟请求和响应对象上,以提供对无需 HTTP 服务器的 WebFlux 应用程序的测试支持。对于端到端集成测试,也可以使用`WebTestClient`

## 5. RSocket

本节描述 Spring Framework 对 RSocket 协议的支持。

### 5.1.概述

WebSocket RSocket 是一种用于在 TCP、 WebSocket 和其他字节流传输上进行多路复用、双工通信的应用程序协议,它使用以下交互模型之一:

* `Request-Response`—发送一条消息,接收一条返回。

* `Request-Stream`—发送一条消息并接收一系列消息。

* `Channel`——双向发送消息流。

* `Fire-and-Forget`—发送单向消息。

一旦建立了初始连接,“客户端”与“服务器”的区别就会丢失,因为双方变得对称,并且双方都可以启动上述交互之一。这就是为什么在协议中将参与方称为“请求者”和“响应者”,而上述交互称为“请求流”或简称“请求”。

这些是 RSocket 协议的关键特性和优点:

* 跨越网络边界的[反应流](https://www.reactive-streams.org/)语义——对于`Request-Stream``Channel`之类的流媒体请求,背压信号在请求者和响应者之间传输,从而允许请求者在源处减慢响应者的速度,从而减少对网络层拥塞控制的依赖,以及在网络级别或任何级别上对缓冲的需求。

* 请求节流——这个特性在`LEASE`帧之后被命名为“租赁”,该帧可以从每一端发送,以限制另一端在给定时间内允许的请求总数。租约定期续签。

* 会话恢复——这是为失去连接而设计的,并且需要保持某些状态。状态管理对于应用程序是透明的,并且与背压相结合工作得很好,背压可以在可能的情况下停止生产者并减少所需的状态量。

* 大消息的分片和重新组装。

* KeepAlive(心跳)。

RSocket 在多种语言中都有[实现](https://github.com/rsocket)[爪哇 库](https://github.com/rsocket/rsocket-java)是建立在[项目反应堆](https://projectreactor.io/)上的,而[反应堆网状结构](https://github.com/reactor/reactor-netty)是用于传输的。这意味着来自应用程序中的反应流发布者的信号通过 RSocket 在整个网络中透明地传播。

#### 5.1.1.《议定书》

RSocket 的优点之一是它在有线上具有定义良好的行为,以及易于读取的[规格](https://rsocket.io/docs/Protocol)以及一些协议[extensions](https://github.com/rsocket/rsocket/tree/master/Extensions)。因此,阅读规范是一个好主意,独立于语言实现和更高级别的框架 API。本节提供了一个简明的概述,以建立一些上下文。

**连接**

最初,客户端通过一些低级别的流媒体传输(例如 TCP 或 WebSocket)连接到服务器,并向服务器发送`SETUP`帧,以设置连接的参数。

服务器可能会拒绝`SETUP`帧,但通常在它被发送(对于客户端)和接收(对于服务器)之后,双方都可以开始进行请求,除非`SETUP`表示使用租赁语义来限制请求的数量,在这种情况下,双方都必须等待来自另一端的`LEASE`帧以允许进行请求。

**提出要求**

一旦建立了连接,双方可以通过`REQUEST_RESPONSE``REQUEST_STREAM``REQUEST_CHANNEL``REQUEST_FNF`中的一个帧发起请求。这些帧中的每一个都从请求者向响应者传送一条消息。

然后响应者可以返回带有响应消息的`PAYLOAD`帧,并且在`REQUEST_CHANNEL`的情况下,请求者还可以发送带有更多请求消息的`PAYLOAD`帧。

当请求涉及诸如`Request-Stream``Channel`之类的消息流时,响应者必须尊重来自请求者的需求信号。需求被表示为大量的消息。初始需求在`REQUEST_STREAM``REQUEST_CHANNEL`框架中指定。后续的需求是通过`REQUEST_N`帧来表示的。

每一方也可以通过`METADATA_PUSH`帧发送元数据通知,这些通知与任何单独的请求无关,而是与整个连接有关。

**消息格式**

RSocket 消息包含数据和元数据。元数据可用于发送路由、安全令牌等。数据和元数据可以采用不同的格式。每个类型的 MIME 类型都在`SETUP`框架中声明,并应用于给定连接上的所有请求。

虽然所有消息都可以具有元数据,但通常的元数据例如路由是每个请求的,因此仅包含在请求的第一个消息中,即具有一个框架`REQUEST_RESPONSE``REQUEST_STREAM``REQUEST_CHANNEL`,或`REQUEST_FNF`

协议扩展定义了应用程序中使用的通用元数据格式:

* [复合元数据](https://github.com/rsocket/rsocket/blob/master/Extensions/CompositeMetadata.md)--多个独立格式化的元数据条目。

* [Routing](https://github.com/rsocket/rsocket/blob/master/Extensions/Routing.md)—请求的路径。

#### 5.1.2.爪哇 实现

RSocket 的[爪哇 实现](https://github.com/rsocket/rsocket-java)是建立在[项目反应堆](https://projectreactor.io/)之上的。TCP 和 WebSocket 的传输建立在[反应堆网状结构](https://github.com/reactor/reactor-netty)上。作为一种反应流库,Reactor 简化了协议的实现工作。对于应用程序来说,使用`Flux``Mono`声明运算符和透明背压支持是很自然的。

RSocket 爪哇 中的 API 有意地是最小的和基本的。它专注于协议特性,并将应用程序编程模型(例如 RPC Codegen vs Other)作为更高级别的独立关注点。

主契约[io.rsocket.rsocket](https://github.com/rsocket/rsocket-java/blob/master/rsocket-core/src/main/java/io/rsocket/RSocket.java)`Mono`表示对单个消息的承诺、`Flux`消息流和`io.rsocket.Payload`实际消息进行建模,并将对数据和元数据的访问作为字节缓冲区。`RSocket`契约是对称使用的。对于请求,给应用程序一个`RSocket`来执行请求。对于响应,应用程序实现`RSocket`来处理请求。

这并不意味着要做一个全面的介绍。 Spring 在大多数情况下,应用程序将不必直接使用其 API。然而,独立于 Spring 来观察或实验 RSocket 可能是重要的。RSocket 爪哇 存储库包含许多[示例应用程序](https://github.com/rsocket/rsocket-java/tree/master/rsocket-examples),它们演示了它的 API 和协议特性。

#### 5.1.3. Spring 支持

`spring-messaging`模块包含以下内容:

* [Rsocketrequester](#rsocket-requester)—Fluent API 通过带有数据和元数据编码/解码的`io.rsocket.RSocket`进行请求。

* [附加注释的响应者](#rsocket-annot-responders)`@MessageMapping`用于响应的注释处理程序方法。

`spring-web`模块包含`Encoder``Decoder`实现,例如 Jackson 的 cbor/json,以及 RSocket 应用程序可能需要的 Protobuf。它还包含`PathPatternParser`,可以插入该参数以进行有效的路由匹配。

Spring Boot2.2 支持在 TCP 或 WebSocket 上站立 RSocket 服务器,包括在 WebFlux 服务器中公开 RSocket over WebSocket 的选项。对于`RSocketRequester.Builder``RSocket战略`也有客户机支持和自动配置。有关更多详细信息,请参见 Spring 引导引用中的[RSocket 部分](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-rsocket)

Spring 安全性 5.2 提供了 RSocket 支持。

Spring 集成 5.2 提供了入站和出站网关,以与 RSocket 客户端和服务器进行交互。有关更多详细信息,请参见 Spring 集成参考手册。

Spring 云网关支持 RSocket 连接。

### 5.2.Rsocketrequester

`RSocketRequester`提供了一个 Fluent API 来执行 RSocket 请求,接受并返回数据和元数据的对象,而不是低级别的数据缓冲区。它可以被对称地使用,用于从客户机发出请求和从服务器发出请求。

#### 5.2.1.客户请求者

要在客户端获得`RSocketRequester`,就需要连接到一个服务器,该服务器需要发送一个带有连接设置的 RSocket`SETUP`帧。`RSocketRequester`提供了一个构建器,该构建器帮助准备一个`io.rsocket.core.RSocketConnector`,包括`SETUP`框架的连接设置。

这是连接默认设置的最基本方式:

爪哇

```
RSocketRequester requester = RSocketRequester.builder().tcp("localhost", 7000);

URI url = URI.create("https://example.org:8080/rsocket");
RSocketRequester requester = RSocketRequester.builder().webSocket(url);
```

Kotlin

```
val requester = RSocketRequester.builder().tcp("localhost", 7000)

URI url = URI.create("https://example.org:8080/rsocket");
val requester = RSocketRequester.builder().webSocket(url)
```

上面的连接不是立即的。当提出请求时,将透明地建立并使用共享连接。

##### 连接设置

`RSocketRequester.Builder`提供了以下自定义初始化`SETUP`框架的方法:

* `dataMimeType(MimeType)`—为连接上的数据设置 MIME 类型。

* `metadataMimeType(MimeType)`—为连接上的元数据设置 MIME 类型。

* `setupData(Object)`—要包含在`SETUP`中的数据。

* `setupRoute(String, Object…​)`—将元数据中的路由包含在`SETUP`中。

* `setupMetadata(Object, MimeType)`—要包含在`SETUP`中的其他元数据。

对于数据,默认的 MIME 类型是从第一个配置的`Decoder`派生的。对于元数据,默认的 MIME 类型是[复合元数据](https://github.com/rsocket/rsocket/blob/master/Extensions/CompositeMetadata.md),它允许每个请求有多个元数据值和 MIME 类型对。通常情况下,这两种情况都不需要改变。

`SETUP`框架中的数据和元数据是可选的。在服务器端,[@ConnectMapping](#rsocket-annot-connectmapping)方法可用于处理连接的开始和`SETUP`框架的内容。元数据可用于连接级别的安全性。

##### Strategies

`RSocketRequester.Builder`接受`RSocketStrategies`来配置请求者。你将需要使用它来为数据和元数据值的(反)序列化提供编码器和解码器。默认情况下,只注册来自`spring-core``String``byte[]``ByteBuffer`的基本编解码器。添加`spring-web`可以访问更多可以按以下方式注册的内容:

爪哇

```
RSocketStrategies strategies = RSocketStrategies.builder()
    .encoders(encoders -> encoders.add(new Jackson2CborEncoder()))
    .decoders(decoders -> decoders.add(new Jackson2CborDecoder()))
    .build();

RSocketRequester requester = RSocketRequester.builder()
    .rsocketStrategies(strategies)
    .tcp("localhost", 7000);
```

Kotlin

```
val strategies = RSocketStrategies.builder()
        .encoders { it.add(Jackson2CborEncoder()) }
        .decoders { it.add(Jackson2CborDecoder()) }
        .build()

val requester = RSocketRequester.builder()
        .rsocketStrategies(strategies)
        .tcp("localhost", 7000)
```

`RSocketStrategies`是为重复使用而设计的。在一些场景中,例如客户端和服务器在相同的应用程序中,可以优选地在 Spring 配置中对其进行声明。

##### 客户响应者

`RSocketRequester.Builder`可用于配置来自服务器的请求的响应程序。

你可以使用带注释的处理程序进行客户端响应,该处理程序基于服务器上使用的相同基础设施,但以编程方式注册,如下所示:

爪哇

```
RSocketStrategies strategies = RSocketStrategies.builder()
    .routeMatcher(new PathPatternRouteMatcher())  (1)
    .build();

SocketAcceptor responder =
    RSocketMessageHandler.responder(strategies, new ClientHandler()); (2)

RSocketRequester requester = RSocketRequester.builder()
    .rsocketConnector(connector -> connector.acceptor(responder)) (3)
    .tcp("localhost", 7000);
```

|**1**|如果存在`spring-web`,则使用`PathPatternRouteMatcher`,以进行有效的<br/>路由匹配。|
|-----|--------------------------------------------------------------------------------------------|
|**2**|从具有`@MessageMaping`和/或`@ConnectMapping`方法的类创建响应器。|
|**3**|登记响应者。|

Kotlin

```
val strategies = RSocketStrategies.builder()
        .routeMatcher(PathPatternRouteMatcher())  (1)
        .build()

val responder =
    RSocketMessageHandler.responder(strategies, new ClientHandler()); (2)

val requester = RSocketRequester.builder()
        .rsocketConnector { it.acceptor(responder) } (3)
        .tcp("localhost", 7000)
```

|**1**|如果存在`spring-web`,则使用`PathPatternRouteMatcher`,以进行有效的<br/>路由匹配。|
|-----|--------------------------------------------------------------------------------------------|
|**2**|从具有`@MessageMaping`和/或`@ConnectMapping`方法的类创建响应器。|
|**3**|登记响应者。|

注意,上面只是为客户端响应者的程序化注册设计的一个快捷方式。对于客户机响应者处于 Spring 配置中的替代场景,你仍然可以将`RSocketMessageHandler`声明为 Spring  Bean,然后按以下方式应用:

爪哇

```
ApplicationContext context = ... ;
RSocketMessageHandler handler = context.getBean(RSocketMessageHandler.class);

RSocketRequester requester = RSocketRequester.builder()
    .rsocketConnector(connector -> connector.acceptor(handler.responder()))
    .tcp("localhost", 7000);
```

Kotlin

```
import org.springframework.beans.factory.getBean

val context: ApplicationContext = ...
val handler = context.getBean<RSocketMessageHandler>()

val requester = RSocketRequester.builder()
        .rsocketConnector { it.acceptor(handler.responder()) }
        .tcp("localhost", 7000)
```

对于上述情况,还可能需要使用`setHandlerPredicate`中的`RSocketMessageHandler`来切换到用于检测客户端响应者的不同策略,例如基于诸如`@RSocketClientResponder`VS 的默认`@Controller`的自定义注释。在使用客户机和服务器,或者在同一个应用程序中有多个客户机的情况下,这是必要的。

有关编程模型的更多信息,请参见[附加注释的响应者](#rsocket-annot-responders)

##### 高级

`RSocketRequesterBuilder`提供了一个回调,以公开底层`io.rsocket.core.RSocketConnector`的更多配置选项,用于保持活动间隔、会话恢复、拦截器等。你可以按以下方式配置该级别的选项:

爪哇

```
RSocketRequester requester = RSocketRequester.builder()
    .rsocketConnector(connector -> {
        // ...
    })
    .tcp("localhost", 7000);
```

Kotlin

```
val requester = RSocketRequester.builder()
        .rsocketConnector {
            //...
        }
        .tcp("localhost", 7000)
```

#### 5.2.2.服务器请求者

要从服务器向连接的客户机发出请求,需要从服务器获得连接的客户机的请求者。

[附加注释的响应者](#rsocket-annot-responders)中,`@ConnectMapping``@MessageMapping`方法支持`RSocketRequester`参数。使用它来访问连接的请求者。请记住,`@ConnectMapping`方法本质上是`SETUP`框架的处理程序,在开始请求之前必须对其进行处理。因此,从一开始就必须将请求与处理分离开来。例如:

爪哇

```
@ConnectMapping
Mono<Void> handle(RSocketRequester requester) {
    requester.route("status").data("5")
        .retrieveFlux(StatusReport.class)
        .subscribe(bar -> { (1)
            // ...
        });
    return ... (2)
}
```

|**1**|异步启动请求,与处理无关。|
|-----|------------------------------------------------------------|
|**2**|执行处理并返回完成`Mono<Void>`。|

Kotlin

```
@ConnectMapping
suspend fun handle(requester: RSocketRequester) {
    GlobalScope.launch {
        requester.route("status").data("5").retrieveFlow<StatusReport>().collect { (1)
            // ...
        }
    }
    /// ... (2)
}
```

|**1**|异步启动请求,与处理无关。|
|-----|------------------------------------------------------------|
|**2**|在挂起功能中执行处理。|

#### 5.2.3.请求

一旦有了[client](#rsocket-requester-client)[server](#rsocket-requester-server)请求者,你可以按以下方式进行请求:

爪哇

```
ViewBox viewBox = ... ;

Flux<AirportLocation> locations = requester.route("locate.radars.within") (1)
        .data(viewBox) (2)
        .retrieveFlux(AirportLocation.class); (3)
```

|**1**|指定要包含在请求消息的元数据中的路由。|
|-----|------------------------------------------------------------------|
|**2**|为请求消息提供数据。|
|**3**|声明预期的响应。|

Kotlin

```
val viewBox: ViewBox = ...

val locations = requester.route("locate.radars.within") (1)
        .data(viewBox) (2)
        .retrieveFlow<AirportLocation>() (3)
```

|**1**|指定要包含在请求消息的元数据中的路由。|
|-----|------------------------------------------------------------------|
|**2**|为请求消息提供数据。|
|**3**|声明预期的响应。|

交互类型是由输入和输出的基数隐式确定的。上面的示例是`Request-Stream`,因为发送了一个值并接收了一个值流。在大多数情况下,只要输入和输出的选择与 RSocket 交互类型以及响应者期望的输入和输出类型相匹配,就不需要考虑这个问题。无效组合的唯一示例是多对一。

`data(Object)`方法还接受任何活性流`Publisher`,包括`Flux``Mono`,以及在`ReactiveAdapterRegistry`中注册的任何其他值的生成器。对于产生相同类型的值的多值`Publisher`,例如`Flux`,可以考虑使用重载的`data`方法之一,以避免对每个元素进行类型检查和`Encoder`查找:

```
data(Object producer, Class<?> elementClass);
data(Object producer, ParameterizedTypeReference<?> elementTypeRef);
```

`data(Object)`步骤是可选的。对于不发送数据的请求,跳过它:

爪哇

```
Mono<AirportLocation> location = requester.route("find.radar.EWR"))
    .retrieveMono(AirportLocation.class);
```

Kotlin

```
import org.springframework.messaging.rsocket.retrieveAndAwait

val location = requester.route("find.radar.EWR")
    .retrieveAndAwait<AirportLocation>()
```

如果使用[复合元数据](https://github.com/rsocket/rsocket/blob/master/Extensions/CompositeMetadata.md)(默认值),并且如果已注册的`Encoder`支持这些值,则可以添加额外的元数据值。例如:

Java

```
String securityToken = ... ;
ViewBox viewBox = ... ;
MimeType mimeType = MimeType.valueOf("message/x.rsocket.authentication.bearer.v0");

Flux<AirportLocation> locations = requester.route("locate.radars.within")
        .metadata(securityToken, mimeType)
        .data(viewBox)
        .retrieveFlux(AirportLocation.class);
```

Kotlin

```
import org.springframework.messaging.rsocket.retrieveFlow

val requester: RSocketRequester = ...

val securityToken: String = ...
val viewBox: ViewBox = ...
val mimeType = MimeType.valueOf("message/x.rsocket.authentication.bearer.v0")

val locations = requester.route("locate.radars.within")
        .metadata(securityToken, mimeType)
        .data(viewBox)
        .retrieveFlow<AirportLocation>()
```

对于`Fire-and-Forget`,使用`send()`方法,返回`Mono<Void>`。请注意,`Mono`仅表示消息已成功发送,而不表示消息已被处理。

对于`Metadata-Push`,使用带有`sendMetadata()`返回值的`Mono<Void>`方法。

### 5.3.附加注释的响应者

RSocket 响应器可以实现为`@MessageMapping``@ConnectMapping`方法。`@MessageMapping`方法处理单个请求,而`@ConnectMapping`方法处理连接级事件(设置和元数据推送)。带注释的响应器是对称支持的,用于从服务器端响应和从客户端响应。

#### 5.3.1.服务器响应者

要在服务器端使用带注释的响应器,将`RSocketMessageHandler`添加到你的 Spring 配置中,以检测`@Controller`带有`@MessageMapping``@ConnectMapping`的 bean 方法:

Java

```
@Configuration
static class ServerConfig {

    @Bean
    public RSocketMessageHandler rsocketMessageHandler() {
        RSocketMessageHandler handler = new RSocketMessageHandler();
        handler.routeMatcher(new PathPatternRouteMatcher());
        return handler;
    }
}
```

Kotlin

```
@Configuration
class ServerConfig {

    @Bean
    fun rsocketMessageHandler() = RSocketMessageHandler().apply {
        routeMatcher = PathPatternRouteMatcher()
    }
}
```

然后通过 Java RSocket API 启动一个 RSocket 服务器,并为响应者插入`RSocketMessageHandler`,如下所示:

Java

```
ApplicationContext context = ... ;
RSocketMessageHandler handler = context.getBean(RSocketMessageHandler.class);

CloseableChannel server =
    RSocketServer.create(handler.responder())
        .bind(TcpServerTransport.create("localhost", 7000))
        .block();
```

Kotlin

```
import org.springframework.beans.factory.getBean

val context: ApplicationContext = ...
val handler = context.getBean<RSocketMessageHandler>()

val server = RSocketServer.create(handler.responder())
        .bind(TcpServerTransport.create("localhost", 7000))
        .awaitSingle()
```

`RSocketMessageHandler`默认支持[composite](https://github.com/rsocket/rsocket/blob/master/Extensions/CompositeMetadata.md)[routing](https://github.com/rsocket/rsocket/blob/master/Extensions/Routing.md)元数据。如果需要切换到不同的 MIME 类型或注册其他元数据 MIME 类型,则可以设置其[MetadataExtractor](#rsocket-metadata-extractor)

你需要设置元数据和数据格式所需支持的`Encoder``Decoder`实例。你可能需要`spring-web`模块来实现编解码。

默认情况下,`SimpleRouteMatcher`用于通过`AntPathMatcher`匹配路由。我们建议插入`spring-web`中的`PathPatternRouteMatcher`以进行有效的路线匹配。RSocket 路由可以是分层的,但不是 URL 路径。这两个路由匹配器都被配置为默认使用“.”作为分隔符,并且没有像 HTTP URL 那样的 URL 解码。

`RSocketMessageHandler`可以通过`RSocketStrategies`进行配置,如果你需要在同一进程中在客户机和服务器之间共享配置,这可能会很有用:

Java

```
@Configuration
static class ServerConfig {

    @Bean
    public RSocketMessageHandler rsocketMessageHandler() {
        RSocketMessageHandler handler = new RSocketMessageHandler();
        handler.setRSocketStrategies(rsocketStrategies());
        return handler;
    }

    @Bean
    public RSocketStrategies rsocketStrategies() {
        return RSocketStrategies.builder()
            .encoders(encoders -> encoders.add(new Jackson2CborEncoder()))
            .decoders(decoders -> decoders.add(new Jackson2CborDecoder()))
            .routeMatcher(new PathPatternRouteMatcher())
            .build();
    }
}
```

Kotlin

```
@Configuration
class ServerConfig {

    @Bean
    fun rsocketMessageHandler() = RSocketMessageHandler().apply {
        rSocketStrategies = rsocketStrategies()
    }

    @Bean
    fun rsocketStrategies() = RSocketStrategies.builder()
            .encoders { it.add(Jackson2CborEncoder()) }
            .decoders { it.add(Jackson2CborDecoder()) }
            .routeMatcher(PathPatternRouteMatcher())
            .build()
}
```

#### 5.3.2.客户响应者

需要在`RSocketRequester.Builder`中配置带有注释的客户端响应程序。详见[客户响应者](#rsocket-requester-client-responder)

#### 5.3.3.@MessageMapping

一旦[server](#rsocket-annot-responders-server)[client](#rsocket-annot-responders-client)响应者配置到位,`@MessageMapping`方法可按以下方式使用:

Java

```
@Controller
public class RadarsController {

    @MessageMapping("locate.radars.within")
    public Flux<AirportLocation> radars(MapRequest request) {
        // ...
    }
}
```

Kotlin

```
@Controller
class RadarsController {

    @MessageMapping("locate.radars.within")
    fun radars(request: MapRequest): Flow<AirportLocation> {
        // ...
    }
}
```

上面的`@MessageMapping`方法响应具有“locate.radars.within”路由的请求-流交互。它支持灵活的方法签名,可以选择使用以下方法参数:

|       Method Argument        |说明|
|------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|          `@Payload`          |请求的有效载荷。这可以是异步类型的一个具体值,如`Mono``Flux`<br/><br/>** 注:** 注释的使用是可选的。一个方法参数不是简单的类型<br/>,也不是任何其他受支持的参数,假定它是预期的有效负载。|
|      `RSocketRequester`      |向远端发出请求的请求者。|
|    `@DestinationVariable`    |基于映射模式中的变量从路由中提取的值,例如`@MessageMapping("find.radar.{id}")`。|
|          `@Header`           |按照[MetadataExtractor](#rsocket-metadata-extractor)中所述,为提取而注册的元数据值。|
|`@Headers Map<String, Object>`|按照[MetadataExtractor](#rsocket-metadata-extractor)中所述,为提取而注册的所有元数据值。|

返回值应该是一个或多个要序列化为响应有效负载的对象。这可以是异步类型,如`Mono``Flux`,具体值,或`void`或无值异步类型,如`Mono<Void>`

`@MessageMapping`方法支持的 RSocket 交互类型是由输入的基数(即`@Payload`参数)的输出,其中基数表示如下:

|Cardinality|说明|
|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|
|     1     |要么是显式的值,要么是单值异步类型,如`Mono<T>`。|
|   Many    |一种多值异步类型,如`Flux<T>`。|
|     0     |对于输入,这意味着该方法没有`@Payload`参数。对于输出,这是<br/><br/>,这是`void`或无值异步类型,例如`Mono<Void>`。|

下表显示了所有输入和输出基数组合以及相应的交互类型:

|Input Cardinality|Output Cardinality|交互类型|
|-----------------|------------------|---------------------------------|
|      0, 1       |        0         |先开火后遗忘、请求-响应|
|      0, 1       |        1         |请求-响应|
|      0, 1       |       Many       |请求流|
|      Many       |    0, 1, Many    |请求通道|

#### 5.3.4.@ConnectMapping

`@ConnectMapping`处理 RSocket 连接开始时的`SETUP`框架,以及通过`METADATA_PUSH`框架的任何后续元数据推送通知,即`metadataPush(Payload)`中的`io.rsocket.RSocket`

`@ConnectMapping`方法支持与[@MessageMapping](#rsocket-annot-messagemapping)相同的参数,但基于来自`SETUP``METADATA_PUSH`框架的元数据和数据。`@ConnectMapping`可以使用一种模式,将处理范围缩小到在元数据中具有路由的特定连接,或者如果没有声明任何模式,则所有连接都匹配。

`@ConnectMapping`方法不能返回数据,必须以`void``Mono<Void>`作为返回值进行声明。如果处理返回一个新连接的错误,那么该连接将被拒绝。在向`RSocketRequester`请求连接时,不能停止处理。详见[服务器请求者](#rsocket-requester-server)

### 5.4.MetadataExtractor

响应者必须解释元数据。[复合元数据](https://github.com/rsocket/rsocket/blob/master/Extensions/CompositeMetadata.md)允许独立格式化的元数据值(例如用于路由、安全性、跟踪),每个值都具有自己的 MIME 类型。应用程序需要一种方法来配置元数据来支持 MIME 类型,以及一种方法来访问提取的值。

`MetadataExtractor`是一种契约,用于获取序列化的元数据并返回经过解码的名称-值对,然后可以通过名称像头一样访问这些对,例如通过注释处理程序方法中的`@Header`

`DefaultMetadataExtractor`可以给出`Decoder`实例来解码元数据。开箱即用,它内置了对[“message/x.rsocket.routing.v0”](https://github.com/rsocket/rsocket/blob/master/Extensions/Routing.md)的支持,并将其解码为`String`,并保存在“route”键下。对于任何其他 MIME 类型,你需要提供`Decoder`,并按照以下方式注册 MIME 类型:

Java

```
DefaultMetadataExtractor extractor = new DefaultMetadataExtractor(metadataDecoders);
extractor.metadataToExtract(fooMimeType, Foo.class, "foo");
```

Kotlin

```
import org.springframework.messaging.rsocket.metadataToExtract

val extractor = DefaultMetadataExtractor(metadataDecoders)
extractor.metadataToExtract<Foo>(fooMimeType, "foo")
```

复合元数据可以很好地组合独立的元数据值。但是,请求者可能不支持复合元数据,或者可能选择不使用它。为此,`DefaultMetadataExtractor`可能需要自定义逻辑来将解码后的值映射到输出映射。下面是一个使用 JSON 进行元数据处理的示例:

Java

```
DefaultMetadataExtractor extractor = new DefaultMetadataExtractor(metadataDecoders);
extractor.metadataToExtract(
    MimeType.valueOf("application/vnd.myapp.metadata+json"),
    new ParameterizedTypeReference<Map<String,String>>() {},
    (jsonMap, outputMap) -> {
        outputMap.putAll(jsonMap);
    });
```

Kotlin

```
import org.springframework.messaging.rsocket.metadataToExtract

val extractor = DefaultMetadataExtractor(metadataDecoders)
extractor.metadataToExtract<Map<String, String>>(MimeType.valueOf("application/vnd.myapp.metadata+json")) { jsonMap, outputMap ->
    outputMap.putAll(jsonMap)
}
```

当通过`MetadataExtractor`配置`RSocketStrategies`时,你可以让`RSocketStrategies.Builder`使用已配置的解码器创建提取器,并只需使用回调来定制注册,如下所示:

Java

```
RSocketStrategies strategies = RSocketStrategies.builder()
    .metadataExtractorRegistry(registry -> {
        registry.metadataToExtract(fooMimeType, Foo.class, "foo");
        // ...
    })
    .build();
```

Kotlin

```
import org.springframework.messaging.rsocket.metadataToExtract

val strategies = RSocketStrategies.builder()
        .metadataExtractorRegistry { registry: MetadataExtractorRegistry ->
            registry.metadataToExtract<Foo>(fooMimeType, "foo")
            // ...
        }
        .build()
```

## 6. 反应库

`spring-webflux`依赖于`reactor-core`,并在内部使用它来组成异步逻辑并提供反应流支持。通常,WebFlux API 返回`Flux``Mono`(因为这些 API 是内部使用的),并宽容地接受任何反应流`Publisher`实现作为输入。使用`Flux``Mono`是很重要的,因为它有助于表示基数——例如,预期是单个还是多个异步值,这对于做出决策(例如,在编码或解码 HTTP 消息时)是必不可少的。

对于带注释的控制器,WebFlux 透明地适应应用程序选择的反应库。这是在[`ReactiveAdapterRegistry`](https://DOCS. Spring.io/ Spring-framework/DOCS/5.3.16/javadoc-api/org/springframework/core/reactiveadapterregistry.html)的帮助下完成的。注册中心内置了对 RXJava3、 Kotlin 协程和 SmallRye Mutiny 的支持,但你也可以注册其他第三方适配器。

|   |在 Spring Framework5.3.11 中,对 RXJava1 和 2 的支持是不受欢迎的,下面是<br/>RXJava 自己的 EOL 建议和对 RXJava3 的升级建议。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------|

对于功能 API(例如[功能端点](#webflux-fn)`WebClient`和其他),WebFlux API 的一般规则适用于-`Flux``Mono`作为返回值,并将反应流`Publisher`作为输入。当`Publisher`(无论是自定义的还是来自另一个反应库)被提供时,它只能被视为具有未知语义(0..n)的流。但是,如果语义是已知的,则可以用`Flux``Mono.from(Publisher)`来包装它,而不是传递 RAW`Publisher`

例如,给定一个不是`Mono``Publisher`,JacksonJSON 消息编写器需要多个值。如果媒体类型意味着一个无限的流(例如,`application/json+stream`),则值是单独写入和刷新的。否则,值将被缓冲到列表中,并呈现为 JSON 数组。