8.md 95.9 KB
Newer Older
W
wizardforcel 已提交
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
# 扩展我们的电子商务申请

在最后一章中,我们开始开发电子商务应用程序,我们创建了基于其 ID 查找产品的功能,以及在几个参数的基础上查找产品。 在本章中,我们将扩展此功能,以便我们还可以订购我们选择的产品。 在这样做的同时,我们将学习新技术,专注于 Java 中的功能规划以及在运行时期间的反射和注释处理等其他语言功能,以及脚本界面。

正如我们在先前的章节中所做的那样,我们将一步一步地开发申请。 当我们发现新学习的技术时,我们将重构代码以注册新工具和方法以产生更可读和有效的代码。 我们还将模仿现实生活项目的发展中的意义上,在开始,我们将有简单的要求,后来,新的要求将被设置为我们想象的业务发展和销售更多和更多的产品。 我们将成为想象的百万富翁。

我们将使用上一章的代码库,但我们将进一步开发新项目。 我们将使用 Spring,Gradle,Tomcat 和 Soapui,这不是新的,因为我们在上一章中熟悉这些时。 在本章中,您将了解以下主题:

*   注释处理
*   使用反思
*   Java 中的功能编程使用:
*   lambda 表达式
*   溪流
*   调用 java 的脚本

# MyBusiness 订购

订购过程比仅查找产品更复杂。 订单表单本身列出了产品和金额,并确定了该订单的客户是谁。 我们所要做的就是检查我们的商店中提供的产品,并可以将它们提供给给定客户。 这是最简单的方法; 但是,有一些产品,有更多的限制。 例如,当某人订购桌面灯时,我们将电源线另有交付。 原因是电源线特定于该国。 我们向英国和德国提供不同的电源线。 一种可能的方法可以是识别客户的国家。 但这种方法没有考虑到我们的客户是经销商的事实。 所有客户都可以位于英国,同时,他们可能希望用电力电缆向德国提供灯。 为避免这种情况和歧义,我们的客户将桌面灯和电源线顺序排列为同一订单。 在某些情况下,我们在没有电源线的情况下送桌面灯,但这是一个特殊情况。 我们需要一定程度的逻辑来识别这些特殊情况。 因此,我们必须实现逻辑,看看是否有桌面灯的电源线,如果没有自动处理订单,则拒绝。 这并不意味着我们不会提供产品。 我们将仅将订单放在队列中,并且运营商将不得不查看它。

这种方法的问题是桌面灯仅是一个需要配置支持的产品。 我们拥有的更多产品,他们可能拥有的专业越多,并且检查订单一致性的代码变得越来越复杂,直到它达到不可管理的复杂程度。 当类或方法变得太复杂时,程序员重构它,将方法或类分成较小的部分。 我们必须用产品检查来做同样的事情。 我们不应该尝试创建一个巨大的类,检查产品和所有可能的订单星座,而是我们应该有许多较小的检查,以便每个都只检查一个小集。

在某些情况下,检查一致性更简单。 检查灯具是否有电源线具有复杂性任何新手程序员可以编程。 我们在我们的代码中使用此示例,因为我们希望专注于代码的实际结构,而不是在检查本身的复杂性。 然而,在现实生活中,检查可能相当复杂。 想象一家卖电脑的商店。 它将配置放在一起:电源,显卡和主板,适当的 CPU 和内存。 有很多选择,其中一些可能无法一起工作。 在真实情况下,我们需要检查主板与所选的内存兼容,它与顺序一样多的银行,它们被适当配对(一些存储器只能成对安装),即 图形卡有一个兼容的插槽,电源有足够的瓦特来可靠地运行整个配置。 这是非常复杂的,最好不要与检查是否有灯具电源线的代码混淆。

# 设置项目

由于我们仍然使用春靴,因此构建文件不需要任何修改; 我们将使用与上一章相同的文件。 然而,包装结构有点不同。 这一次,我们做的事情比获得请求并响应向我们提供的后端服务做出更复杂的事情。 现在,我们必须实施复杂的业务逻辑,正如我们将看到的那样,需要许多课程。 当我们有超过 10 个课程时,给予或采取,在某个包装中,是时候考虑将它们放入单独的包裹。 应彼此相关并具有类似功能的类别放入一个包中。 这样,我们将有一个包裹:

*   控制器(虽然我们在这个例子中只有一个,但通常有更多)
*   数据存储除了存储数据以外的功能,因此,属性,字段,设置器和 getters 以外的功能
*   当订购桌面灯时,将帮助我们检查电源线的跳棋
*   为控制器执行不同服务的服务
*   我们程序的主要包包含`Application`类,`SpringConfiguration`和多个接口

# 订单控制器和 DTO

当请求到服务器订购一堆产品时,它进入了 HTTPS `POST`请求。 请求的主体在 JSON 中编码。 到目前为止,我们有控制器正在处理`GET`参数。 当我们依赖于春天的数据编制时,处理`POST`请求并不重要。 控制器代码本身很简单:

```
package packt.java11.bulkorder.controllers;

import ...

@RestController
public class OrderController {
    private static final Logger log = LoggerFactory.getLogger((OrderController.class));
    private final Checker checker;

    public OrderController(@Autowired Checker checker) {
        this.checker = checker;
    }

    @RequestMapping("/order")
    public Confirmation getProductInformation(@RequestBody Order order) {
        if (checker.isConsistent(order)) {
            return Confirmation.accepted(order);
        } else {
            return Confirmation.refused(order);
        }
    }
}
```

只有一个要求在此控制器中处理`order`。 这是映射到 URL,`/order`。 订单自动从 JSON 转换为请求主体的订单对象。 这就是`@RequestBody`注释要求春天为我们做的事情。 控制器的功能只需检查订单的一致性。 如果订单一致,那么我们接受订单; 否则,我们拒绝它。 现实生活例证还将检查订单不仅是一致的,而且还来自有资格购买这些产品的客户,并且在仓库中提供产品,或者至少可以根据承诺提供产品 和生产者的交付时间。

要检查订单的一致性,我们需要为我们做这项工作的东西。 正如我们所知,我们必须模块化代码而不是在单个类中实现太多的东西,我们需要一个检查器对象。 这是根据类上的注释自动提供的,也是通过`@Autowired`的控制器的构造函数。

`Order` class 是一个简单的 bean,只需列出项目:

```
package packt.java11.bulkorder.dtos;

import ...

public class Order {
    private String orderId;
    private List<OrderItem> items;
    private String customerId;

    // ... setters and getters ...
}
```

包的名称是`dtos`,它代表复数**数据传输对象****dto** )。 DTO 是用于在不同组件之间传输数据的对象,通常通过网络传输数据。 由于另一边可以以任何语言实现,因此元帅可以是 JSON,XML 或其他能够提供数据的其他格式。 这些类没有真正的方法。 DTO 通常只有字段,安置者和吸气器。

以下是包含订单的一个项目的类:

```
package packt.java11.bulkorder.dtos;

public class OrderItem {
    private double amount;
    private String unit;
    private String productId;

    // ... setters and getters ...
}
```

订单确认也在此包中,虽然这也是一个真正的 DTO,但它有一些简单的辅助方法:

```
package packt.java11.bulkorder.dtos;

public class Confirmation {
    private final Order order;
    private final boolean accepted;

    private Confirmation(Order order, boolean accepted) {
        this.order = order;
        this.accepted = accepted;
    }

    public static Confirmation accepted(Order order) {
        return new Confirmation(order, true);
    }

    public static Confirmation refused(Order order) {
        return new Confirmation(order, false);
    }

    public Order getOrder() {
        return order;
    }

    public boolean isAccepted() {
        return accepted;
    }
}
```

我们为班级提供两种工厂方法。 这有点违反了纯粹主义者讨厌的单一责任原则。 大多数情况下,当代码变得更复杂时,这样的快捷方式咬了回来,并且代码必须重新转换为清洁。 纯粹的解决方案是创建一个单独的工厂类。 从该类或来自分隔类的工厂方法的利用使控制器的代码更具可读。

我们拥有的主要任务是一致性检查。 截至目前的代码几乎是微不足道的。

# 一致性检查器

我们有一个一致性检查器类,并且将其实例注入到控制器中。 此类用于检查一致性,但实际上并不实际执行校验。 它只控制我们提供的不同验牌,并逐一地调用它们来完成真实的工作。

我们要求一致性检查器(例如检查订购桌面灯时)是否包含电源线的一致性检查器,实现`ConsistencyChecker`接口:

```
package packt.java11.bulkorder;

import packt.java11.bulkorder.dtos.Order;

public interface ConsistencyChecker {

    boolean isInconsistent(Order order);
}
```

如果订单不一致,则该方法`isInconsistent`应返回`true`。 它返回`false`如果它不知道订单是否不一致,而是从实际检查器检查顺序的方面,没有不一致。 有几个`ConsistencyChecker`类,我们必须在另一个之后调用一个,直到一个返回`true`或我们离开它们。 如果其中没有一个返回`true`,则可以安全地假设,至少从自动检查器的角度来看,顺序是一致的。

我们在开发开始时知道我们真的有很多一致性检查,而且并非所有的订单都是相关的。 我们希望避免每个订单的每个检查员的调用。 为此,我们实现了一些过滤。 我们让产品指定所需的检查类型。 这是一块产品信息,例如尺寸或描述。 为了适应这一点,我们需要扩展`ProductInformation`类。

我们将创建每个`ConsistencyChecker`界面,实现类为 Spring Bean(用`@Component`注释注释),同时,我们将使用注释向它们注释,该注释指定它们实现的检查类型类型。 同时,扩展`ProductInformation`,包含一组`Annotation`类对象,指定要调用的验询。 我们可以简单地列出 Checker 类而不是注释,但这为我们提供了在产品和注释之间配置映射的一些额外自由。 注释指定产品的性质,检查器类被注释。 桌面灯是`PoweredDevice`类型,HTG5 `NeedPowercord`,用`@PoweredDevice`注释注释。 如果有任何其他类型的产品也需要电源线,则应将该类型的注释添加到`NeedPowercord`类中,我们的代码将工作。 由于我们开始深入潜入注释和注释处理,因此我们必须先了解确实的注释。 我们已经使用了注释,[第 3 章](3.html)*优化排序 - 制作代码专业人员*,但我们所知道的只是如何使用它们,这通常是危险的,而不是理解我们所做的 。

# 注释

注释与它们前面的`@`字符一起使用,可以附加到包,类,接口,字段,方法,方法参数,通用类型声明和使用,以及最后到注释。 几乎可以使用注释,它们用于描述某些程序元信息。 例如,`@RestController`注释不会直接改变`OrderController`类的行为。 该类的行为由内部的 Java 代码描述。 注释有助于春天了解该类是什么以及应如何使用它。 当春季扫描所有包和类来发现不同的春季 bean 时,它会在课堂上看到注释并将其考虑在内。 春天不明白的课堂上有其他注释。 它们可以由其他一些框架或计划代码使用。 春天忽略了它们作为任何行为良好的框架。 例如,正如我们稍后会看到的那样,在我们的代码库中,我们拥有`NeedPowercord`类,它是一个春豆,因此,用`@Component`注释注释。 与此同时,它也用`@PoweredDevice`注释注释。 春天不知道一个有源设备是什么。 这是我们定义和使用的东西。 春天忽略了这个。

软件包,类,接口,字段等,可以附加许多注释。 这些注释应该简单地写在他们附加的句法单元的声明前面。

在包的情况下,注释必须在`package-info.java`文件中的包名称前面写入。 该文件可以放在包的目录中,可用于编辑包的 *javadoc* ,并为包添加注释。 此文件不能包含任何 Java 类,因为名称`package-info`不是有效的标识符。

我们不能只是在任何内容中写任何东西作为注释。 应宣布注释。 它们处于 Java 特殊接口的运行时。 例如,声明`@PoweredDevice`注释的 Java 文件如下所示:

```
package packt.java11.bulkorder.checkers;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface PoweredDevice {
}
```

`interface`关键字前面的`@`字符向我们展示了这是一个特殊的一个注释类型。 有一些特殊规则; 例如,注释界面不应扩展任何其他接口,甚至没有注释一个接口。 另一方面,编译器自动使注释界面使其扩展了 JDK 接口`java.lang.annotation.Annotation`

注释位于源代码中,因此,它们在编译过程中可用。 它们也可以由编译器保留并放入生成的类文件,并且当类加载器加载类文件时,它们也可以在运行时使用。 默认行为是编译器将注释与类文件中的注释元素一起存储,但类加载器不会保留运行时可用。

要处理编译过程中的注释,必须使用注释处理器扩展 Java 编译器。 这是一个相当高的主题,只有几个例子,您可以在使用 Java 时见面。 注释处理器是一种 java 类,它实现了一个特殊接口,并且当处理器被声明为兴趣的源文件中处理源文件中的注释时,由编译器调用。

# 答案保留

Spring 和其他框架通常在运行时处理注释。 必须指示编译器和类加载器指示在运行时在运行时保持注释。 为此,必须使用`@Retention`注释来注释注释界面本身。 该注释具有`RetentionPolicy`类型的一个参数,即`enum`。 我们很快就会讨论如何定义注释参数。

有趣的是,注释界面上的`@Retention`注释必须在类文件中使用; 否则,类加载器不会知道如何对待注释。 我们如何发出编译过程中的编译器保存注释? 我们注释了注释界面声明。 因此,`@Retention`的声明本身被注释并声明在运行时可用。

注释声明可以使用`@Retention(RetentionPolicy.SOURCE)``@Retention(RetentionPolicy.CLASS)``@Retention(RetentionPolicy.RUNTIME)`注释。

# 注释目标

最终的保留类型将是最常用的类型。 还有其他注释可以用于注释声明。 `@Target`注释可用于限制注释对某些位置的使用。 此注释的参数是单个`java.lang.annotation.ElementType`值或这些值的数组。 有充分的理由限制注释的使用。 当我们在运行时比在运行期间在错误的地方放入错误的地方时,可以更好地获得编译时间错误,因为框架是框架忽略我们的注释。

# 注释参数

注释,正如我们之前所看到的,可以具有参数。 要在`@interface`的注释声明中声明这些参数,我们使用方法。 这些方法具有名称和返回值,但它们不应该有一个参数。 您可以尝试声明一些参数,但 Java 编译器将严格,不会编译代码。

可以在使用方法的名称和`=`字符中使用注释和`=`字符的位置定义值,分配给它们与方法类型兼容的值。 例如,让我们假设我们修改`PoweredDevice`注释的声明到以下内容:

```
public @interface ParameteredPoweredDevice { 
    String myParameter(); 
}
```

在这种情况下,在使用注释时,我们应该为参数指定值,例如以下内容:

```
@Component 
@ParameteredPoweredDevice(myParameter = "1966") 
public class NeedPowercord implements ConsistencyChecker { 
...
```

如果参数的名称是一个值,并且在使用注释的位置,则没有其他参数定义,则可以跳过名称*值*。 例如,在我们只有一个参数时修改代码是一个方便的速记:

```
public @interface ParameteredPoweredDevice{ 
    String value(); 
} 
... 
@Component 
@ParameteredPoweredDevice("1966") 
public class NeedPowercord implements ConsistencyChecker { 
...
```

我们可以使用方法声明后使用`default`关键字来定义可选参数。 在这种情况下,我们必须为参数定义默认值。 修改我们进一步的示例注释,我们仍然可以,但不需要,指定值。 在后一种情况下,它将是一个空字符串:

```
public @interface ParameteredPoweredDevice { 
    String value() default ""; 
}
```

由于我们指定的值应该是恒定的,并且在编译时可以是可计算的,因此复杂类型的使用并不多。 注释参数通常是字符串,整数,以及偶尔,双打或其他原始类型。 语言规范给出的类型的确切列表如下:

*   原始(`double``int`等)
*   细绳
*   班级
*   枚举
*   另一个注释
*   任何上述类型的数组

我们已经了解了`String`的示例以及`enum``Retention``Target`都具有`enum`参数。 我们想要专注的有趣部分是前面列表中的最后两个项目。

当参数的值是数组时,可以将该值指定为`{``}`字符之间的逗号分隔值。 例如:

```
String[] value();
```

然后可以将其添加到`@interface`注释中,我们可以写入以下内容:

```
@ParameteredPoweredDevice({"1966","1967","1991"})
```

但是,如果只有一个值我们想要传递作为参数值,我们仍然可以使用格式:

```
@ParameteredPoweredDevice("1966")
```

在这种情况下,属性的值将是长度`1`的数组。 当注释的值是一个注释类型的数组时,事情会变得更加复杂。 我们创建一个`@interface`注释(请注意名称中的复数):

```
@Retention(RetentionPolicy.RUNTIME) 
public @interface PoweredDevices { 
ParameteredPoweredDevice[] value() default {}; 
}
```

使用此注释可以如下:

```
@PoweredDevices( 
        {@ParameteredPoweredDevice("1956"), @ParameteredPoweredDevice({"1968", "2018"})} 
)
```

请注意,这与具有三个参数的`ParameteredPoweredDevice`注释的不一样。 这是一个有两个参数的注释。 每个参数都是注释。 第一个有一个字符串参数,第二个有两个参数。

正如您所看到的,注释可能是相当复杂的,以及一些框架(或创建它们的程序员)使用它们 ran amok。 在开始撰写框架之前,进行研究,看看是否有可以使用的框架。 另外,检查是否有另一种方法来解决您的问题。 可以避免 99%的注释处理代码并更简单。 我们编写相同功能的代码越少,我们的越快乐。 美国程序员是懒惰的,这是它必须的方式。

借记的参数是一个注释数组的最后一个示例,了解如何创建可重复注释。

# 可重复的注释

`@Repeatable`向注释声明表示注释可以在一个地方多次应用。 此注释的参数是一个应该具有类型的参数的注释类型,这是该注释的数组。 不要试图了解! 我会举个例子。 事实上,我已经拥有了`@PoweredDevices`。 它有一个参数,它是`@ParameteredPoweredDevice`的数组。 考虑我们现在注释这`@interface`如下:

```
... 
@Repeatable(PoweredDevices.class) 
public @interface ParameteredPoweredDevice { 
...
```

然后,我们可以简化使用`@ParameteredPoweredDevice`。 我们可以多次重复注释,java 运行时将自动将其括在包装类中,在这种情况下,这是`@PoweredDevices`。 在这种情况下,以下两个将是等效的:

```
... 
@ParameteredPoweredDevice("1956") 
@ParameteredPoweredDevice({"1968", "2018"}) 
public class NeedPowercord implements ConsistencyChecker { 
... 

@PoweredDevices( 
        {@ParameteredPoweredDevice("1956"), @ParameteredPoweredDevice({"1968", "2018"})} 
) 
public class NeedPowercord implements ConsistencyChecker { 
...
```

这种复杂方法的原因再次成为 Java 严格遵循的后向兼容性的示例。 注释是在 Java 1.5 中引入的,并且只有 1.8 版自版本可重复注释。 我们很快就会谈论我们在运行时处理注释的反射 API。 `java.lang.reflect.AnnotatedElement`接口中的此 API 具有`getAnnotation(annotationClass)`方法,返回注释。 如果单个注释可以在类,方法等上出现多次,则无法调用此方法以获取所有不同参数的所有不同实例。 通过引入包装多个注释的包含类型来确保向后兼容性。

# 注释继承

注释,就像方法或字段一样,可以在类层次结构之间继承。 如果使用`@Inherited`标有注释声明,那么将另一个归纳延伸另一个类的类可以继承它。 如果儿童类有注释,可以覆盖注释。 由于 Java 中没有多重继承,因此无法继承界面上的注释。 即使归纳是继承的,检索某个元素注释的应用程序代码也可以区分继承的注释以及在实体本身上声明的注释。 有方法可以获取注释和单独的方法来获取在实际元素上声明的声明的注释,而不是继承。

# @documented 诠释

`@Documented`注释表示注释是实体合同的一部分,以这种方式,它必须进入文档。 这是一个注释: *Javadoc* 生成器在创建引用`@Documented`注释的元素的文档时查看。

# JDK 注释

除了用于定义注释的人之外,JDK 还定义了其他注释。 我们已经看到了其中一些。 最常使用的是`@Override`注释。 当编译器看到此注释时,它会检查该方法是否真正覆盖继承的方法。 未能这样做会导致错误,从悲惨的运行时调试中保存我们。

`@Deprecated`注释信号在方法,类或不使用元素的一些其他元素的文档中。 它仍然存在于代码中,因为某些用户可能仍然可以使用它,但在新的开发的情况下取决于包含元素的库的情况,新开发的代码不应使用它。 注释有两个参数。 一个参数是`since`,它可以具有字符串值,可以提供有关折旧方法或类或类的要长时间或何种版本的版本信息。 另一个参数是`forRemoval`,如果元素不会出现在图书馆的未来版本中,则应该是`true`。 某些方法可能被弃用,因为有更好的替代方案,但开发人员不打算从库中删除该方法。 在这种情况下,可以将`forRemoval`设置为`false`

`@SuppressWarning`注释也是一个常用的注释,但它的使用是可疑的。 它可用于抑制编译器的一些警告。 建议在可能会编写代码,可以在没有任何警告的情况下编译。

`@FunctionalInterface`注释声明接口旨在只有一种方法。 此类接口可以实现为 Lambda 表达式。 您将在本章后面了解 Lambda 表达式。 当在接口上应用此注释并且在接口中声明了多个方法时,编译器将发出编译错误。 这将防止任何开发人员早期添加另一个方法,以便与功能编程和 Lambda 表达式一起使用。

# 使用反思

既然您已经学会了如何宣布注释以及如何将它们附加到类和方法,我们可以返回我们的`ProductInformation`类。 您可能会记住,我们希望在此类中指定产品类型,并且每个产品类型由`@interface`注释表示。 我们已经在前几页中列出了它,我们将在我们的`@PoweredDevice`示例中实施。 我们将在稍后开发代码,稍后将有许多使用`@Component`和一个或多个注释的诸如批注告的此类注释,产品类型和一致性检查。

# 获取注释

我们将使用以下字段扩展`ProductInformation`类:

```
private List<Class<? extends Annotation>> check;
```

由于这是一个 DTO,春天需要定居者和吸气器,我们还将为它添加一个新的吸气器和 Setter。 此字段将包含每个类用于我们的注释和内置 JDK 接口`Annotation`的每个类实现的类列表,因为这是 Java 编译器生成它们的方式。 此时,这可能有点朦胧,但我保证黎明将破裂,隧道末端会有光线。

要获取产品信息,我们必须根据 ID 查找。 这是我们在最后一章中开发的界面和服务,除了这次,我们有另一个新字段。 事实上,这是一个显着的差异,尽管`ProductLookup`界面根本没有改变。 在最后一章中,我们开发了两个版本。 其中一个版本正在从属性文件读取数据,而另一个版本正在连接到 REST 服务。

属性文件是丑陋和旧技术,但如果您打算通过 Java 面试或在 21 世纪初开发的企业应用程序中的工作。 我必须在最后一章中包含它。 它坚持不懈地包含在本书中。 同时,在编码本章时,我没有胃部继续使用它。 我也想向您展示相同的内容,可以以 JSON 格式管理。

现在,我们将扩展`ResourceBasedProductLookup`的实现,从 JSON 格式资源文件中读取产品信息。 大多数代码在类中保持不变; 因此,我们只列出了以下区别:

```
package packt.java11.bulkorder.services;
import ...

@Service
public class ResourceBasedProductLookup implements ProductLookup {
    private static final Logger log =
        LoggerFactory.getLogger(ResourceBasedProductLookup.class);

    private ProductInformation fromJSON(InputStream jsonStream) throws IOException {
        final var mapper = new ObjectMapper();
        return mapper.readValue(jsonStream, ProductInformation.class);
    }

// ...
    private void loadProducts() {
        if (productsAreNotLoaded) {
            try {
                final var resources = new PathMatchingResourcePatternResolver().
                        getResources("classpath:products/*.json");
                for (final var resource : resources) {
                    loadResource(resource);
                }
                productsAreNotLoaded = false;
            } catch (IOException ex) {
                log.error("Test resources can not be read", ex);
            }
        }
    }

    private void loadResource(Resource resource) throws IOException {
        final var dotPos = resource.getFilename().lastIndexOf('.');
        final var id = resource.getFilename().substring(0, dotPos);
        final var pi = fromJSON(resource.getInputStream());
        pi.setId(id);
        products.put(id, pi);
        if( pi.getCheck() != null )
        log.info("Product {} check is {}",id,pi.getCheck().get(0));
    }
// ...
```

`project resources/products`目录中,我们有几个 json 文件。 其中一个包含桌面灯产品信息:

```
{ 
  "id" : "124", 
  "title": "Desk Lamp", 
  "check": [ 
    "packt.java11.bulkorder.checkers.PoweredDevice" 
  ], 
  "description": "this is a lamp that stands on my desk", 
  "weight": "600", 
  "size": [ "300", "20", "2" ] 
}
```

产品类型在 JSON 数组中指定。 在此示例中,此阵列仅具有一个元素,并且该元素是表示产品类型的注释界面的完全限定名称。 当 JSON MARSHALLER 将 JSON 转换为 Java 对象时,它识别出需要此信息的字段是`List`,因此它将数组转换为列表,并且,此外,来自`String``Class`对象的元素 代表注释界面。

既然我们有从 JSON 格式化的资源加载的资源,我们已经看到了在使用 Spring 时阅读 JSON 数据的容易,我们可以返回订单一致性检查。 `Checker`类实现逻辑以收集可插拔验询,并调用它们。 它还实现了基于注释的筛选,以免调用验查员,我们并不需要实际顺序实际产品:

```
package packt.java11.bulkorder.services;

import ...

@Component()
@RequestScope
public class Checker {
    private static final Logger log = LoggerFactory.getLogger(Checker.class);

    private final Collection<ConsistencyChecker> checkers;
    private final ProductInformationCollector piCollector;
    private final ProductsCheckerCollector pcCollector;

    public Checker(@Autowired Collection<ConsistencyChecker> checkers,
                   @Autowired ProductInformationCollector piCollector,
                   @Autowired ProductsCheckerCollector pcCollector
    ) {
        this.checkers = checkers;
        this.piCollector = piCollector;
        this.pcCollector = pcCollector;
    }

    public boolean isConsistent(Order order) {
        final var map = piCollector.collectProductInformation(order);
        if (map == null) {
            return false;
        }
        final var annotations = pcCollector.getProductAnnotations(order);
        for (final var checker : checkers) {
            for (final var annotation : checker.getClass().getAnnotations()) {
                if (annotations.contains(annotation.annotationType())) {
                    if (checker.isInconsistent(order)) {
                        return false;
                    }
                    break;
                }
            }
        }
        return true;
    }
}
```

有趣的事情之一是,春天自动控制非常聪明。 我们有一个与`Collection<ConsistencyChecker>`类型的字段。 通常,自动控制有效,如果恰好有一个类具有与电线资源相同的类型。 在我们的情况下,我们没有任何此类候选人,因为这是一个集合,但我们有许多`ConsistencyChecker`课程。 我们所有的检查员都实现了这个界面和 Spring 识别它,实例化所有,神奇地创建它们的集合,并将集合注入此字段。

通常,一个很好的框架逻辑上工作。 我没有意识到这个春天的这个功能,但我认为这将是逻辑的,神奇地,它的工作。 如果事情是逻辑且只是工作,则无需阅读并记住文档。 然而,有点小心不会造成任何伤害。 在我意识到这种功能这样的方式之后,我在文档中抬头看,看看这是春天的保证功能,而不是刚刚工作的东西,但可能在未来的未来版本中改变。 仅使用保证功能非常重要,但在我们的行业中经常被忽视。

调用`isConsistent()`方法时,首先将产品信息收集到`HashMap`中,为每个`OrderItem`分配`ProductInformation`实例。 这是在单独的课程中完成的。 在此之后,`ProductsCheckerCollector`收集一个或多个产品项所需的`ConsistencyChecker`实例。 当我们拥有此集时,我们只需要调用那些使用此集合中的一个注释注释的检查程序。 我们在循环中这样做。

在此代码中,我们使用反思。 我们循环每个检查员的注释。 要获取注释集合,请调用`checker.getClass().getAnnotations()`。 此调用返回对象的集合。 每个对象都是一些 JDK 运行时生成的类的实例,它实现了我们在其自己的源文件中声明为注释的接口。 但是,没有保证,动态创建的类仅实现我们的`@interface`而不是其他接口。 因此,要获取实际的注释类,我们必须调用`annotationType()`方法。

`ProductCheckerCollector``ProductInformationCollector`类非常简单,我们将在学习溪流时稍后讨论它们。 当我们使用循环实现它们时,它们将作为该点作为一个很好的例子。

拥有它们,我们最终可以创建实际的检查课程。 有助于我们看到我们灯有电源线的电源线是以下内容:

```
package packt.java11.bulkorder.checkers;

//SNIPPET SKIL TILL "import ..."

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import packt.java11.bulkorder.ConsistencyChecker;
import packt.java11.bulkorder.dtos.Order;

import ...
@Component
@PoweredDevice
public class NeedPowercord implements ConsistencyChecker {
    private static final Logger log = LoggerFactory.getLogger(NeedPowercord.class);

    @Override
    public boolean isInconsistent(Order order) {
        log.info("checking order {}", order);
        var helper = new CheckHelper(order);
        return !helper.containsOneOf("126", "127", "128");
    }
}
```

辅助类包含许多检查员将需要的简单方法,例如:

```
public boolean containsOneOf(String... ids) {
    for (final var item : order.getItems()) {
        for (final var id : ids) {
            if (item.getProductId().equals(id)) {
                return true;
            }
        }
    }
    return false;
}
```

# 调用方法

在此示例中,我们仅使用单个反射调用来获取附加到类的注释。 反思可以做更多的事情。 处理注释是这些呼叫最重要的使用,因为注释没有自己的功能,并且在运行时无法以任何其他方式处理。 然而,反思不会停止告诉我们课程或任何其他注释控股元素的注释。 反射可用于获取类的方法列表,作为字符串的方法的名称,类的实现接口,父类它扩展,字段,字段类型,等等。 反射通常提供通过以编程方式通过实际代码结构行走的方法和类。

此演练不仅允许读取类型和代码结构,而且还可以在编译时创新方法的名称,可以设置字段值和呼叫方法。 我们甚至可以设置`private`的字段,通常不能被外部世界访问。 还应注意,通过反射访问方法和字段通常比通过编译代码慢慢,因为它始终涉及根据代码中元素的名称查找。

拇指规则是,如果您发现必须使用反射创建代码,那么意识到您可能创建框架(或写一本关于 java 的书籍细节反射)。 这听起来很熟悉吗?

Spring 还使用反射来发现类,方法和字段,也可以注入对象。 它使用 URL 类加载器列出了类路径上的所有 JAR 文件和目录,加载它们,并检查类。

对于一个创意的示例,为了演示,让我们假设许多外部软件供应商的`ConsistencyChecker`实现是由许多外部软件供应商编写的,以及最初设计的程序结构的建筑师刚刚忘记接口中的`isConsistent()`方法。 (与此同时,为了保存我们的心理健康,我们也可以想象这个人不再在公司中工作。)因此,不同的供应商提供了“实施”这个界面的 Java 类,但我们 不能调用该方法,不仅是因为我们没有具有此方法的常见父界面,还因为刚刚发生的供应商使用不同的名称来获取其方法。

我们在这种情况下可以做些什么? 商业明智,要求所有供应商重写他们的跳棋,因为他们知道我们遇到麻烦将一个余性价格标签附加到任务。 我们的经理希望避免成本和美国开发人员也希望表明我们可以纠正这种情况并执行奇迹(我稍后会发表评论)。

我们可以只有一个阶级,了解每个检查员,以及如何以许多不同的方式调用它们。 每当向系统引入新的检查员时,这需要我们维护所述课程,我们希望避免这种情况。 我们正在使用的整个插件架构是为了本发明的目的而发明。

我们如何在我们知道只有一个声明的方法的对象上调用一种方法,它接受命令作为参数? 这就是反射进入图片的地方。 除了调用`checker.isInconsistent(order)`而不是调用`checker.isInconsistent(order)`,我们实现了一个呼叫该方法的`private`方法`isInconsistent()`,无论它的名称是什么,都通过反射:

```
private boolean isInconsistent(ConsistencyChecker checker, Order order) {
    final var methods = checker.getClass().getDeclaredMethods();
    if (methods.length != 1) {
        log.error("The checker {} has zero or more than one methods",
            checker.getClass());
        return false;
    }
    final var method = methods[0];
    final boolean inconsistent;
    try {
        inconsistent = (boolean) method.invoke(checker, order);
    } catch (InvocationTargetException |
        IllegalAccessException |
        ClassCastException e) {
        log.error("Calling the method {} on class {} threw exception",
            method, checker.getClass());
        log.error("The exception is ", e);
        return false;
    }
    return inconsistent;
}
```

我们可以通过调用`getClass()`方法来获取对象的类,并且在代表类本身的对象上,我们可以调用`getDeclaredMethods`。 幸运的是,检查器类不受许多方法乱丢,因此我们检查 Checker 类中只声明了一种方法。 请注意,反射库中还有一个`getMethods()`方法,但它将始终返回多种方法。 它返回声明的和继承方法。 因为每个类和每个类从`java.lang.Object`继承,至少在那里的`Object`类的方法。

在此之后,我们尝试使用表示反射类中的方法的`Method`对象来调用类。 请注意,此`Method`对象不直接连接到实例。 我们从类中检索了该方法,因此,当我们调用它时,我们应该传递它应该作为第一个参数工作的对象。 这样,`x.y(z)`变成`method.invoke(x,z)``invoke()`的最后一个参数是作为`Object`数组传递的变量数量。 在大多数情况下,当我们调用方法时,我们知道我们代码中的参数,即使我们不知道方法的名称,也必须使用反射。 当甚至参数尚未知道时,但作为计算问题,我们必须将其作为`Object`数组传递。

通过反射调用方法是一个冒险的呼叫。 如果我们尝试调用方法正常的方式,这是`private`,那么编译器将发出错误。 如果参数或类型的数量不合适,则编译器将再次给我们一个错误。 如果返回的值不是`boolean`,或者根本没有返回值,那么我们再次获得编译器错误。 在反射的情况下,编译器无能为力。 它不知道在代码正在执行时我们会调用哪种方法。 另一方面,`invoke()`方法可以在调用它时会注意到所有这些故障。 如果发生任何上述问题,那么我们将获得例外。 如果`invoke()`方法本身看到它无法执行我们的要求,那么它将抛出`InvocationTargetException``IllegalAccessException`。 如果从实际返回值转换为`boolean`是不可能的,那么我们将获得`ClassCastException`

关于表演魔法,我们觉得自己是一种自然的冲动,令人出色的东西。 当我们尝试某些东西时,这是可以的,以便有趣,但在我们正在努力工作的工作时绝对不可能。 普通程序员,不了解您的辉煌解决方案,将维护企业环境中的代码。 它们会在修复一些错误或实现一些次要的新功能时将您的良好的代码变为干草堆。 即使您是编程的莫扎特,它们也将是,最佳 No-Name Singers。 企业环境中的精彩代码可以是一个 Requiem,具有隐喻所带来的所有含义。

最后但并非最不重要的是,悲伤的现实是我们通常不是编程的莫镖。

请注意,如果原始值的返回值是原始的情况下,它将通过反射转换为对象,然后我们将其转换回原始值。 如果该方法没有返回值,换句话说,如果它是`void`,则反射将返回一个`java.lang.Void`对象。 `Void`对象只是占位符。 我们无法将其转换为任何原始值或任何其他类型的对象。 它是必要的,因为 Java 是严格的并且`invoke`必须返回`Object`,因此运行时需要它可以返回的东西。 我们所能做的就是检查返回的值类是否真的`Void`

让我们继续我们的故事和我们的解决方案。 我们提交了代码,它在生产中运行一段时间,直到软件供应商的新更新中断。 我们调试测试环境中的代码,并查看此类现在包含多个方法。 我们的文档清楚地说明他们应该只有一个`public`方法,他们提供了一个有...嗯......我们意识到其他方法是`private`。 他们是对的; 它们可以根据合同具有`private`方法,因此我们必须修改代码。 我们替换查找唯一方法的行:

```
final var methods = checker.getClass().getDeclaredMethods(); 
if (methods.length != 1) { 
... 
} 
final var method = methods[0];
```

新代码如下:

```
final var method = getSingleDeclaredPublicMethod(checker); 
if (method == null) { 
    log.error( 
            "The checker {} has zero or more than one methods", 
            checker.getClass()); 
    return false; 

}
```

我们编写待查找一个且唯一的`public`方法的新方法如下:

```
private Method getSingleDeclaredPublicMethod(
    ConsistencyChecker checker) {
    final var methods = checker.getClass().getDeclaredMethods();
    Method singleMethod = null;
    for (final var method : methods) {
        if (Modifier.isPublic(method.getModifiers())) {
            if (singleMethod != null) {
                return null;
            }
            singleMethod = method;
        }
    }
    return singleMethod;
}
```

要检查方法是否为`public`,我们使用`Modifier`类使用`static`方法。 有方法检查所有可能的修饰符。 `getModifiers()`方法返回的值是`int`比特字段。 不同的比特具有不同的修饰符,并且有常量定义这些。 永远不会设置只能用于其他类型的反射对象的比特。

有一个例外,它是`volatile`。 该位可重用到信号桥方法。 Bridge 方法由编译器自动创建,并且可以在本书中具有深刻和复杂的问题。 相同位的重用不会引起混淆,因为字段可以是`volatile`,但作为字段,它不能是桥接方法。 显然,一个字段是一个字段而不是一种方法。 以同样的方式,一种方法不能是`volatile`字段。 一般规则如下:不要在反射物体上使用方法,在那里他们没有意义; 或者,知道你在做什么。

使故事情节更复杂,一个新版本的检查器意外地将检查方法实现为`private`包。 程序员只是忘记使用`public`关键字。 为了简单起见,让我们假设类别再次声明一种方法,但它不公开。 我们如何使用反思解决这个问题?

显然,最简单的解决方案是要求供应商解决问题 - 这是他们的错。 但是,在某些情况下,我们必须在一些问题上创造一个解决方法。 在同一包中,还有另一个解决方案 - 在同一包中使用`public`方法,调用来自另一个类的`private`包方法,从而中继另一个类。 事实上,这种解决方案作为这种错误的解决方法,似乎更加逻辑和更清洁,但这一次,我们想要使用反思。

为避免`java.lang.IllegalAccessException`,我们必须将`method`对象设置为可访问。 为此,我们必须在调用前插入以下行:

```
method.setAccessible(true);
```

请注意,这不会将方法更改为`public`。 它只通过我们设置为可访问的`method`对象的实例,使调用的方法可访问。

我已经看到代码检查是否通过调用`isAccessible()`方法来访问方法并保存此信息; 它将该方法设置为可访问,如果无法访问并在调用后恢复原始可访问性。 这完全没用。 一旦`method`变量超出范围,并且没有引用对象我们将辅助功能标志设置为,设置效果磨损。 此外,没有用于设置`public`的可访问性或其他可调整方法的惩罚。

# 设置字段

我们还可以在`Field`对象上调用`setAccessible`,然后我们甚至可以使用反射设置私有字段的值。 没有进一步的假故事,只是为了这个例子,让我们成为一个`ConsistencyChecker`名为`SettableChecker`

```
@Component 
@PoweredDevice 
public class SettableChecker implements ConsistencyChecker { 
    private static final Logger log = LoggerFactory.getLogger(SettableChecker.class); 

    private boolean setValue = false; 

    public boolean isInconsistent(Order order) { 
        return setValue; 
    } 
}
```

此检查器将返回`false`,除非使用反射将字段设置为`true`。 我们这样做了。 我们在`Checker`类中创建方法,并从每个检查器的检查过程中调用它:

```
private void setValueInChecker(ConsistencyChecker checker) { 
    Field[] fields = checker.getClass().getDeclaredFields(); 
    for( final Field field : fields ){ 
        if( field.getName().equals("setValue") && 
            field.getType().equals(boolean.class)){ 
            field.setAccessible(true); 
            try { 
                log.info("Setting field to true"); 
                field.set(checker,true); 
            } catch (IllegalAccessException e) { 
                log.error("SNAFU",e); 
            } 
        } 
    } 
}
```

该方法通过所有已声明的字段,如果名称是`setValue`,则该类型是`boolean`,那么它将它设置为`true`。 这将基本上呈现包含作为拒绝的有源设备的所有订单。

请注意,虽然`boolean`是一个内置语言原语,但它不是任何均值的类,它仍然有一个类,以便反射可以将字段的类型与`boolean`人为人人为的类比较。 现在,`boolean.class`是语言中的类文字,对于每个原始,可以使用类似的常量。 编译器将它们标识为类文字,并在字节码中创建适当的伪类引用,以便也可以以这种方式检查基元,如`setValueInChecker()`方法的示例代码中所示。

我们检查了该字段具有适当的类型,我们还在字段上称为`setAccessible()`方法。 尽管编译器不知道我们真的做了一切避免了`IllegalAccessException`,但它仍然认为在`field`上调用`set`可以抛出这样的例外,因为它被声明。 但是,我们知道它不应该发生(着名的程序员的最后一句话?)。 为了处理这种情况,我们将使用`try`块括起方法调用,在`catch`分支中,我们记录异常。

# Java 中的功能编程

由于我们在本章的示例中创建了大量代码,我们将研究 Java 的功能编程功能,这将帮助我们从我们的代码中删除许多行。 我们拥有的代码越少,维护应用程序的更容易; 因此,程序员喜欢功能规划。 但这不是功能规划如此受欢迎的唯一原因。 它也是一种以比传统环路更可读和更少的误差方式描述某些算法的优异方法。

功能编程不是新的东西。 它后面的数学背景是在 20 世纪 30 年代开发的。 第一个(如果不是第一个)功能编程语言之一是 LISP。 它是在 20 世纪 50 年代开发的,仍在使用,因此在 JVM(Clojure)上有一个语言的版本。

简而言之意味着我们在功能方面表达程序结​​构。 在这种含义中,我们应该将函数视为数学中的功能,而不是在术语中用于编程语言,例如 C.在 Java 中,我们有方法,当我们遵循功能规划范例时,我们创建和使用方法 表现得像数学函数。 无论我们调用多少次,方法都是功能性的,就像我们调用它一样,就像 *sin(0)*始终为零。 功能编程避免了更改对象状态,并且因为状态不变,结果始终相同。 这也缓解调试。

如果函数已返回给定参数的某个值,则它将始终返回相同的值。 我们还可以将代码读为计算的声明,而不是作为一个接一个地执行的命令。 如果执行顺序并不重要,则代码的可读性也可能增加。

Java 有助于使用 Lambda 表达式和流的功能编程风格。 请注意,这些流不是 I / O 流,并没有与这些流有任何关系。

我们首先要简要介绍 Lambda 表达式以及哪些流是,然后,我们将转换我们程序的某些部分来使用这些编程构造。 我们还将看到这些代码变得更加可读。

可读性是一个可辩论的主题。 代码可以可读于一个开发人员,并且可以不太可读对另一个开发者。 它非常取决于他们习惯的东西。 从我的经历来看,我知道开发人员经常被溪流分心。 开发人员首先遇到溪流时,思考他们的方式以及它们看起来如何奇怪。 但这与开始骑自行车的开始是一样的。 虽然你还在学习如何骑行,但你比你实际前进的更多,而且它肯定比走路慢。 另一方面,一旦你学会了如何骑...

# lambda.

我们已经在[中使用了 Lambda 表达式](3.html)*优化了排序 - 制作代码专业人员*,当我们写了异常抛出测试时。 在该代码中,我们将比较器设置为在每次调用时抛出`RuntimeException`的特殊值:

```
sort.setComparator((String a, String b) -> { 
        throw new RuntimeException(); 
    });
```

参数类型是`Comparator`; 因此,我们必须设置的内容应该有一个实现`java.util.Comparator`界面的类的实例。 该接口只定义了一种方法,即实现必须定义-`compare.`,因此,我们可以将其定义为 lambda 表达式。 如果没有 lambda,如果我们需要一个实例,我们必须输入很多。 我们必须创建一个类,名称它,声明它中的`compare()`方法,并写下该方法的正文,如以下代码段所示:

```
public class ExceptionThrowingComparator implements Comparator { 
  public int compare(T o1, T o2){ 
    throw new RuntimeException(); 
  } 
}
```

在使用它的位置,我们应该实例化课程并将其传递为参数:

```
sort.setComparator(new ExceptionThrowingComparator());
```

如果我们将类定义为匿名类,我们可能会保存几个字符,但开销仍在存在。 我们真正需要的是我们必须定义的一个和单一方法的正文。 这是 Lambda 进入图片的地方。

我们可以在任何地方使用 Lambda 表达,否则我们需要一个只有一个方法的类实例。 从`Object`定义和继承的方法不计算,我们也不关心接口中定义为`default`方法的方法。 他们在那里。 lambda 定义了尚未定义的那个。 换句话说,Lambda 清楚地描绘了,作为匿名类的开销要小得多,所以该值是一个功能,并且我们将其作为参数传递。

Lambda 表达式的简单形式如下:

```
parameters -> body
```

参数可以括在括号之间,或者如果只有一个,那么它可以静置。 身体类似地可以括在`{``}`字符之间,否则它可以是一个简单的表达式。 这样,Lambda 表达式可以将开销减少到最小值,仅使用括号在真正需要的地方。

它也是 Lambda 表达式的一个非常有用的特征,我们不需要指定参数的类型,以防我们使用表达式的上下文显而易见的情况。 因此,前面的代码段甚至可以更短,如下所示:

```
sort.setComparator((a, b) -> { 
    throw new RuntimeException(); 
});
```

或者,我们可以写下列:

```
sort.setComparator((var a, var b) -> { 
    throw new RuntimeException(); 
});
```

参数`a``b`,将根据需要具有类型。 为了使其更加简单,我们还可以省略参数周围的`(``)`字符,以防只有一个。

如果存在多个参数,则括号不是可选的。 这是为了避免在某些情况下歧义。 例如,方法调用`f(x,y->x+y)`可以是具有两个参数 - `x`的方法,以及具有一个参数的 lambda 表达式`y`。 同时,它也可以是具有λ表达的方法调用,该表达式具有两个参数,`x``y`。 当有多个参数并且可以通过编译器计算参数的类型时,可以使用 Java 11 发布以来使用`var`关键字。

当我们想要将功能传递为争论时,Lambda 表达式非常方便。 方法声明的参数类型的声明应该是函数界面类型。 这些接口可以可选地使用`@FunctionalInterface`注释。 Java Runtime 在`java.util.function`包中定义了许多此类接口。 我们将在下一节中讨论其中一些,以及它们在流中的使用。 对于其余的,标准 Java 文档可从 Oracle 获得。

# 溪流

java 8 中的流也是新的,就像 lambda 表达式一样。 他们非常强烈地工作,所以他们的外表同时不是一个惊喜。 Lambda 表达式以及流,支持功能规划风格。

首先要澄清的是,除名称之外,流与输入和输出流无关没有任何关系。 他们是完全不同的事情。 溪流更像是具有一些显着差异的集合。 (如果没有差异,他们就会是集合。)流是基本上可以顺序运行或并行运行的操作的流水线。 他们从集合或其他来源获取数据,包括在飞行中制造的数据。

Streams 支持执行多个数据的相同计算。 该结构称为**单指令多数据****SIMD** )。 不要害怕表达。 这是一件非常简单的事情。 我们已经在这本书中完成了很多次。 循环也是一种 SIMD 结构。 当我们循环通过检查器类时要查看是否存在订单的任何一种,我们对每个检查器执行相同的指令。 多个跳棋意味着多个数据。

循环有一个问题是,我们在不需要时定义执行顺序。 在检查员的情况下,我们并不真正关心跳棋在线执行的顺序。我们关心的是所有的订单都可以。 当我们编写循环时,我们仍然指定一些订单。 这来自循环的性质,我们无法改变这一点。 这就是他们的工作方式。 然而,如果我们可以以某种方式说明*“,这是一个很好的话,说*和每个检查者的*。 这是流播放的地方。*

另一点是使用循环的代码更加令人必要而不是描述性。 当我们阅读循环构造的程序时,我们专注于各个步骤。 我们首先看到循环中的命令是什么。 这些命令在数据的各个元素上工作,而不是整个集合或数组。

当我们将个人步骤放在我们的大脑中,我们意识到大局是什么,循环是什么。 在流的情况下,操作的描述是更高的级别。 一旦我们学习流方法,就更容易读取它们。 流方法在整个流上工作,而不是在各个元素上,因此更具描述性。

`java.lang.Stream`是一个接口。 具有实现此接口类型的对象表示许多对象,并提供可用于在这些对象上执行指令的方法。 当我们在其中一个启动操作时,对象可能或可能不可用,或者可以在需要时创建。 这是达到`Stream`界面的实际实现。 例如,假设我们使用以下代码生成包含`int`值的流:

```
IntStream.iterate( 0, (s) -> s+1 )
```

在前面的代码片段中,无法生成所有元素,因为流包含无限数量的元素。 此示例将返回数字 0,1,2 等,直到此处未列出的进一步流操作,终止计算。

当我们编程`Stream`时,我们通常会从`Collection` - not 中创建流,但常常。 在 Java 8 中扩展了`Collection`接口,以提供`stream``parallelStream()`方法。 它们都返回表示集合元素的流对象。 虽然`stream`以与在收集中相同的顺序返回元素,但是在存在自然的顺序时,`parallelStream`创建可以以并行方式工作的流。 在这种情况下,如果我们在流上使用的一些方法是以这种方式实现的,则代码可以使用计算机中可用的多个处理器。

一旦我们有流,我们就可以使用`Stream`接口定义的方法。 一个开始的是`forEach()`。 该方法具有一个参数,该参数通常作为 Lambda 表达提供,并且将对流的每个元素执行 Lambda 表达式。

`Checker`类中,我们有`isConsistent()`方法。 在此方法中,存在通过检查器类的注释的循环。 如果我们想记录循环实现中的录音的接口,我们可以添加以下内容:

```
for (ConsistencyChecker checker :checkers) { 
  for (Annotation annotation : checker.getClass().getAnnotations()) { 
    Arrays.stream(annotation.getClass().getInterfaces()).forEach( 
      t ->log.info("annotation implemented interfaces {}",t)); 
...
```

在此示例中,我们使用来自`Arrays`类的工厂方法从数组创建流。 该阵列包含反射方法返回的接口`getInterfaces()`。 Lambda 表达只有一个参数; 因此,我们不需要在它周围使用括号。 表达式的是一种返回任何值的方法调用; 因此,我们还省略了`{``}`字符。

为什么所有这次麻烦? 什么是收益? 为什么我们不能只写一个记录数组元素的简单循环? 收益是可读性和可维护性。 当我们创建一个程序时,我们必须专注于*哪个*该程序应该做,而不是*它应该如何做到这一点。 在理想的世界中,规范只会是可执行的。 当编程工作将被人工智能取代时,我们将来可能会到达那里。 (但不是程序员。)我们不在那里。 我们必须告诉计算机如何做我们想要实现的目标。 我们曾经必须在 PDP-11 的控制台上输入二进制代码,以将部署到内存中的机器代码以使其执行。 后来,我们有装配者; 后来仍然,我们有 Fortran 和其他高级编程语言,这些语言已经在 40 年前取代了大部分编程工作。 编程中的所有这些开发从**向*转移到*是什么*。 今天,我们在 Java 11 中编程,道路仍然有英里。 我们越是可以表达该做什么,而不是如何做到,而不是如何做到这一点,我们的计划将更短,更易懂。 它将包含精华,而不是机器需要的人为垃圾只能做我们想要的事情。 当我看到一个代码中的循环时,我假设执行循环的顺序存在一些重要的顺序。 可能根本没有重要。 几秒钟后可能是显而易见的。 它可能需要几分钟或更长时间来意识到订购并不重要。 这次是浪费的,可以使用编程构造来保存,更好地表达*该部件,而不是*如何执行* *部分*。***

# 功能界面

该方法的参数应该是`java.util.function.Consumer`。 这是一个需要定义`accept()`方法的接口,此方法是`void`。 Lambda 表达式或实现此界面的类将*消耗*方法的参数,并且不会产生任何内容。

在该包中定义了几个其他接口,每个接口都用作用于描述某些方法参数的功能界面,该参数可以在实际参数中作为 lambda 表达式给出。

例如,`Consumer`的相反是`Supplier`。 此接口具有命名为`get()`的方法,该方法不需要任何参数,但这会给一些`Object`作为返回值。

如果有一个参数和返回值,则界面称为`Function`。 如果返回的值必须与参数相同类型,则`UnaryOperator`界面是我们的朋友。 同样,存在`BinaryOperator`界面,返回与参数相同类型的对象。 就像我们从`Function``UnaryOperator`一样,我们可以看到,在另一个方向上,在参数和返回值不共享类型的情况下,还有`BiFunction`

这些接口不彼此独立地定义。 如果方法需要`Function`,我们有`UnaryOperator`通过,它不应该是一个问题。 `UnaryOperator`基本上与`Function`相同,具有相同类型的参数。 可以使用`Function`的方法,该方法接受对象并返回对象,如果它们具有相同类型,则不应存在问题。 那些可以,但不需要,不同。 要发生这种情况,`UnaryOperator`接口扩展了`Function`,因此可以用于代替`Function`

到目前为止我们遇到的此类的接口是使用泛型定义的。 由于通用类型不能是基元,所以应在原始值上运行的接口分别定义。 例如,`Predicate`是定义`booleantest(T t)`的接口。 它是返回`boolean`值的函数,通常用于流方法。

还有接口,例如`BooleanSupplier``DoubleConsumer``DoubleToIntFunction`,以及与原始`boolean``double``int`一起使用。 不同参数类型和返回值的可能组合的数量是无限的......几乎。

**Fun fact**: To be very precise, it is not infinite. A method can have at most 254 arguments. This limit is specified in the JVM and not in the Java language specification. Of course, one is useless without the other. There are 8 primitive types (plus `Object`, plus the possibility that there are less than 254 arguments), which means that the total number of possible functional interfaces is 10<sup>254</sup>, give or take a few magnitudes. Almost infinite!

我们不应该指望在此包中拥有 JDK 中定义的所有可能接口。 这些只是最有用的界面。 例如,没有接口,它使用`short``char`。 如果我们需要这样的东西,那么我们可以在代码中定义`interface`。 或者只是思考,并找出如何使用已经定义的一个。 (我在职业承运人期间从未使用`short`类型。从来没有必要。)

流中使用这些功能接口如何? `Stream` interface 定义了具有一些功能界面类型作为参数的方法。 例如,`allMatch()`方法具有`Predicate`参数,并返回`Boolean`值,该值是`true`,如果流匹配的所有元素`Predicate`。 换句话说,此方法返回`true` if,且仅当`Predicate`作为参数提供,返回流的每个元素的`true`

在以下代码中,我们将重写我们在使用循环使用流的示例代码中实现的一些方法,并且通过这些示例,我们将讨论流提供的最重要方法。 我们保存了两类`ProductsCheckerCollector``ProductInformationCollector`,以演示流使用。 我们可以从这些开始。 `ProductsCheckerCollector`通过所有包含在`Order`中的产品,并收集产品中列出的注释。 每个产品可能包含零,一个或多个注释。 这些可用于列表。 可以多次引用相同的注释。 为避免重复,我们使用`HashSet`,这将仅包含元素的一个实例,即使产品中有多个实例:

```
public class ProductsCheckerCollector {
    private static final Logger log =
            LoggerFactory.getLogger(ProductsCheckerCollector.class);

    private final ProductInformationCollector pic;

    public ProductsCheckerCollector
            (@Autowired ProductInformationCollector pic) {
        this.pic = pic;
    }

    public Set<Class<? extends Annotation>> getProductAnnotations(Order order) {
        var piMap = pic.collectProductInformation(order);
        final var annotations = new HashSet<Class<? extends Annotation>>();
        for (var item : order.getItems()) {
            final var pi = piMap.get(item);
            if (pi != null && pi.getCheck() != null) {
                for (final var check : pi.getCheck()) {
                    annotations.addAll(pi.getCheck());
                }
            }
        }
        return annotations;
    }
```

现在,让我们看看使用 Streams 重新介绍此方法时如何了解:

```
public Set<Class<? extends Annotation>> getProductAnnotations(Order order) {
    var piMap = pic.collectProductInformation(order);
    return order.getItems().stream()
            .map(piMap::get)
            .filter(Objects::nonNull)
            .peek(pi -> {
                if (pi.getCheck() == null) {
                    log.info("Product {} has no annotation", pi.getId());
                }
            })
            .filter(ProductInformation::hasCheck)
            .peek(pi -> log.info("Product {} is annotated with class {}", pi.getId(), pi.getCheck()))
            .flatMap(pi -> pi.getCheck().stream())
            .collect(Collectors.toSet());
}
```

该方法的主要工作进入了一个单一的,虽然庞大,流表达式。 我们将涵盖即将到来的页面中表达的要素。

`order.getItems`返回的`List`被转换调用`stream()`方法:

```
return order.getItems().stream()
```

正如我们已经简要提到的那样,`stream()`方法是`Collection`界面的一部分。 实现`Collection`接口的任何类都将具有此方法,即使是在 Java 8 中引入了流之前实现的方法。这是因为`stream()`方法在接口中实现为`default`方法。 这样,如果我们碰巧实现了一个实现这个接口的类,即使我们不需要流,我们也可以免费获得它作为额外的。

引入了 Java 8 中的`default`方法以支持接口的向后兼容性。 JDK 的一些接口将被修改为支持 Lambda 和功能规划。 一个例子是`stream()`方法。 使用前 Java 8 功能集,应修改实现某些修改的接口的类。 他们将需要实施新方法。 这样的变化不是向后兼容的,而 Java 作为语言和 JDK 正在渴望向后兼容。 因此,引入了`default`方法。 这些让开发人员扩展界面并仍然保持向后兼容,为方法提供默认实现,这是新的。 与这种哲学相反,Java 8 JDK 的全新功能界面也有`default`方法,但是,在 JDK 中没有先前版本,他们无意中兼容。 在 Java 9 中,界面也延长,现在它们不仅可以包含`default``static`方法,还可以包含`private`方法。 这样,接口变得等同于抽象类,尽管除了常量`static`字段之外的接口中没有字段。 此接口功能扩展是一个很多批评的功能,它只是构成了允许多类继承面的其他语言的编程风格和结构问题。 Java 正在避免这种情况,直到 Java 8 和 Java 9 的出现。
这是什么外来的? 注意`default`方法,也要在接口中使用`private`方法。 明智地使用它们,如果有的话。

此流的元素是`OrderItem`对象。 我们需要每个`OrderItem`

# 方法参考文献

很幸运的是,我们有`Map`,它与产品信息的一对订单项目,所以我们可以在`Map`上调用`get()`

```
.map(piMap::get)
```

`map()`方法再次与 Java 中的其他内容具有相同的名称,不应该混淆。 虽然`Map`类是数据结构,`Stream`接口中的`map()`方法执行流元素的映射。 该方法的参数是`Function`(回想一下,这是我们最近讨论过的功能界面)。 此函数转换值`T`,其作为原始流(`Stream<T>`)的元素可用,到值,`R``map()`方法的返回值是`Stream<R>``map()`方法使用给定的`Function<T,R>``Stream<T>`转换为`Stream<R>`,调用其原始流的每个元素并从转换元素创建新流。

我们可以说`Map`接口以静态方式将键映射到数据结构中的值,并且流方法`map()`,动态地将一种类型的值映射到另一个(或相同)的值。

我们已经看到,我们可以以 Lambda 表达式的形式提供功能界面的实例。 这个论点不是 lambda 表达。 这是一种方法参考。 它说`map()`方法应该使用实际流元素作为参数调用`Map piMap`上的`get()`方法。 我们很幸运的是`get()`也需要一个争论,不是我们吗? 我们也可以如下写作:

```
.map( orderItem ->piMap.get(orderItem))
```

但是,这与`piMap::get`完全相同。

这样,我们可以引用在某个实例上工作的实例方法。 在我们的示例中,实例是`piMap`变量引用的实例。 还可以引用`static`方法。 在这种情况下,类的名称应该写在`::`字符的前面。 当我们使用`static`方法`nonNull`,从`Objects` class 中,我们很快就会看到这个例子(请注意,类名是复数形式,并且它位于`java.util`包中而不是`java.lang`)。

也可以引用实例方法而不提供应调用它的参考。 这可以在功能界面方法具有额外的第一个参数的地方使用,这将被用作实例。 我们已经在[第 3 章](3.html)*优化了排序代码专业人员*,当我们通过`String::compareTo`时,我们在预期的参数是`Comparator`时,我们已经使用了这一章。 `compareTo()`方法期望一个参数,但`Comparator`界面中的`compare()`方法需要两个。 在这种情况下,第一个参数将用作必须调用`compare()`的实例,第二个参数传递给`compare()`。 在这种情况下,`String::compareTo`与写入 Lambda 表达式`(String a, String b) -> a.compareTo(b)`相同。

最后但并非最不重要的是,我们可以使用方法引用构造函数。 当我们需要一个`Supplier`的(让我们保持简单)`Object`,我们可以写`Object::new`

下一步是从流中过滤掉`null`元素。 请注意,此时,流具有`ProductInformation`元素:

```
.filter(Objects::nonNull)
```

`filter()`方法使用`Predicate`并创建仅包含与谓词匹配的元素的流。 在这种情况下,我们使用了参考`static`方法。 `filter()`方法不会改变流的类型。 它只过滤出元素。

我们应用的下一个方法有点反功能。 纯功能流方法不会改变对象的状态。 它们创建了它们返回的新对象,但除此之外,没有副作用。 `peek()`本身是没有什么不同,因为它只返回与它应用于它的相同元素的流。 但是,这一个*无操作*特征诱使新手程序员做一些非功能性和写代码,副作用。 毕竟,为什么如果没有(侧面)效应调用它?

```
.peek(pi -> { 
    if (pi.getCheck() == null) { 
        log.info("Product {} has no annotation", pi.getId()); 
    } 
})
```

虽然`peek()`方法本身没有任何副作用,但是λ表达的执行可能具有。 但是,这对于任何其他方法也是如此。 这只是事实上,在这种情况下,做一些事情更为令人诱人。 别。 我们是纪律的成年人。 作为该方法的名称表明,我们可能会偷看流,但我们不应该做任何其他事情。 通过编程成为特定活动,在这种情况下,偷看是足够的。 这就是我们在代码中实际做的:我们记录的东西。

在此之后,我们摆脱了没有`ProductInformation`的元素; 我们希望摆脱具有它的元素,但没有定义检查器:

```
.filter(pi -> pi.getCheck() != null)
```

在这种情况下,我们无法使用方法引用。 相反,我们使用 Lambda 表达式。 作为替代解决方案,我们可以在`ProductInformation`中创建一个`boolean hasCheck()`方法,如果`private`字段检查不是`null`,则返回`true`。 然后,这将如下阅读:

```
.filter(ProductInformation::hasCheck)
```

这完全有效和工作,虽然该类不实现任何功能界面并具有许多方法,而不仅仅是这个功能。 但是,方法引用是显式的,并指定要调用哪种方法。

在第二个过滤器之后,我们再次记录元素:

```
.peek(pi -> log.info( 
     "Product {} is annotated with class {}", pi.getId(), 
                                            pi.getCheck()))
```

下一个方法是`flatMap`,这是一个特别的东西,不容易理解。 至少对我来说,当我学习功能规划时,它比理解`map()``filter()`更困难:

```
.flatMap(pi ->pi.getCheck().stream())
```

此方法希望 Lambda,方法引用或任何传递给它作为参数,为原始流的每个元素创建一个全新的对象流该方法。 然而,结果不是流流,这也可以是可能的,而是返回的流被连接到一个巨大的流中。

如果流我们将其应用于整数值的流,例如 1,2,3,......以及每个数字 *n* 的函数返回三个元素的流 *n**N + 1**N + 2* ,然后得到的流`flatMap()`,产生含有 1,2,3,2,3,4 的流, 3,4,5,4,5,6 等。

最后,我们应该收集到`Set`的流。 这是通过调用`collector()`方法来完成的:

```
.collect(Collectors.toSet());
```

`collector()`方法的参数是(再次,一个过度使用的表达式)`Collector`。 它可用于将流的元素收集到集合中。 请注意,`Collector`*而不是*功能界面。 您不能只使用 Lambda 或简单的方法收集一些东西。 要收集元素,我们肯定需要一些地方,因为较新的元素来自流。 `Collector`界面并不简单。 幸运的是,`java.util.streams.Collectors`级(再次,注意复数)有很多`static`方法,可以创建和返回`Object`字段,反过来,创建和返回`Collector`对象。

其中一个是`toSet()`,它返回一个`Collector`,有助于将流的元素收集到`Set`中。 当存在所有元素时,`collect()`方法将返回`Set`。 还有其他方法通过求解元素,计算平均值或`List``Collection``Map`来帮助收集流元素。 收集元素到 A `Map`是一种特殊的事情,因为`Map`的每个元素实际上是一个键值对。 当我们查看`ProductInformationCollector`时,我们会看到这个例子。

`ProductInformationCollector`类代码包含`collectProductInformation()`方法,我们将从`Checker`类以及`ProductsCheckerCollector`类中使用:

```
private Map<OrderItem, ProductInformation> map = null;

public Map<OrderItem, ProductInformation> collectProductInformation(Order order) {
    if (map == null) {
        log.info("Collecting product information");
        map = new HashMap<>();
        for (OrderItem item : order.getItems()) {
            final ProductInformation pi = lookup.byId(item.getProductId());
            if (!pi.isValid()) {
                map = null;
                return null;
            }
            map.put(item, pi);
        }
    }
    return map;
}
```

简单的技巧是将收集的值存储在`Map`中,如果不是`null`,那么只需返回已经计算的值,就可以保存大量的服务调用,以防此方法称为多次处理 相同的 HTTP 请求。

有两种编码这种结构的方法。 一个是通过检查`Map`的非无效,如果`Map`已经存在,则返回。 此模式广泛使用并具有名称。 如果,这被称为保护*。 在这种情况下,该方法中存在多个返回声明,其可以被视为弱点或防模式。 另一方面,该方法的制表是一个选项卡浅。 这是一个味道,万一你发现自己在一个关于一个或另一个解决方案的辩论的中间,只是帮自己一个忙,让你的同伴赢得这个话题并拯救你的耐力更重要的问题,例如更重要的问题 ,是否应该使用 Streams 或只是普通的旧环。*

现在,让我们看看我们如何将此解决方案转换为功能样式:

```
public Map<OrderItem, ProductInformation> collectProductInformation(Order order) {
    if (map == null) {
        log.info("Collecting product information");
        map =
        order.getItems()
                .stream()
                .map(item -> tuple(item, item.getProductId()))
                .map(t -> tuple(t.r, lookup.byId((String) t.s)))
                .filter(t -> ((ProductInformation)t.s).isValid())
                .collect(Collectors.toMap(t -> (OrderItem)t.r, t -> (ProductInformation)t.s));
        if (map.keySet().size() != order.getItems().size()) {
            log.error("Some of the products in the order do " +
                            "not have product information, {} != {} ",
                    map.keySet().size(),order.getItems().size());
            map = null;
        }
    }
    return map;
}
```

我们使用辅助类`Tuple`,除了两个`Object`实例名为`r``s`。 我们稍后将列出此类的代码。 这很简单。

在流表达式中,我们首先从集合创建流,然后将`OrderItem`元素映射到`OrderItem``productId`元组的流。 然后,我们将这些元组映射到现在包含`OrderItem``ProductInformation`的元组。 这两个映射可以在单个映射呼叫中完成,这将在一个映射呼叫中执行这两个步骤。 我决定在徒劳的每行中创建两个具有更简单的步骤希望结果代码更容易理解。

过滤步骤也不是新的。 它只是过滤出无效的产品信息元素。 实际上应该没有。 如果订单包含到不存在产品的订单 ID,则会发生这种情况。 当我们查看收集的产品信息元素的数量时,将在下一个语句中进行检查,以查看所有项目具有正确的信息。

有趣的代码是我们如何将流的元素收集到`Map`中。 为此,我们再次使用`collect()`方法以及`Collectors`类。 这次,`toMap()`方法创建`Collector`。 这需要两个`Function`结果表达。 第一个应该将流的元素转换为键,第二个应导致在`Map`中使用的值。 因为从通过的 Lambda 表达式的结果计算了键的实际类型和值,所以我们显式必须将元组的字段转换为所需的类型。

最后,简单的`Tuple`类如下:

```
public class Tuple<R, S> {
    final public R r;
    final public S s;

    private Tuple(R r, S s) {
        this.r = r;
        this.s = s;
    }

    public static <R, S> Tuple tuple(R r, S s) {
        return new Tuple<>(r, s);
    }
}
```

我们的代码中还有一些课程,值得转换为功能样式。 这些是`Checker``CheckerHelper`类。

`Checker`类中,我们可以重写`isConsistent()`方法:

```
public boolean isConsistent(Order order) {
    var map = piCollector.collectProductInformation(order);
    if (map == null) {
        return false;
    }
    final var as = pcCollector.getProductAnnotations(order);
    return !checkers.stream().anyMatch(
            c -> Arrays.stream(c.getClass().getAnnotations()
            ).filter(a -> as.contains(a.annotationType())
            ).anyMatch(x -> c.isInconsistent(order)
            ));
}
```

由于您已经了解到大部分重要的流方法,这里几乎没有任何新的问题。 我们可以提到`anyMatch()`方法,如果存在至少一个元素,则返回`true`,以便传递给`anyMatch()``Predicate`参数是`true`。 它也可能需要一些住宿,以便我们可以在另一个流中使用流。 当流表达式过度复杂并且需要使用局部变量拆分成较小的部分时,这很好。

最后,在我们留下功能样式之前,我们在`CheckHelper`类中重写`containsOneOf()`方法。 这不包含任何新的元素,并将帮助您查看关于`map()``filter()``flatMap()``Collector`所学到的内容。 请注意,在我们讨论的情况下,此方法返回`true`(如果`order`包含作为字符串给出的至少一个订单 ID):

```
public boolean containsOneOf(String... ids) {
    return order.getItems().parallelStream()
        .map(OrderItem::getProductId)
        .flatMap(itemId -> Arrays.stream(ids)
            .map(id -> tuple(itemId, id)))
        .filter(t -> Objects.equals(t.s, t.r))
        .collect(Collectors.counting()) > 0;
}
```

我们创建了`OrderItem`对象的流,然后我们将其映射到流中包含的产品的 IDS 流。 然后,我们为每个 ID 创建另一个流,其中包含 ID 的元素和作为参数给出的字符串 ID 之一。 然后,我们将这些子流达到一个流。 此流将包含`order.getItems().size()``ids.length`元素:所有可能对。 我们将过滤耗尽包含相同 ID 的那些对,最后,我们将计算流中的元素数。

# 脚本在 Java 中

我们几乎准备好了我们本章的示例计划。 有一个问题,虽然它不是专业的。 当我们有一个新产品需要一个新的检查器时,我们必须创建一个新的代码版本。

专业环境中的程序具有发布。 修改代码时,错误是固定的,或者实现了新功能,在应用程序进入生产之前,组织需要多个步骤。 这些步骤包括释放过程。 某些环境具有轻量级释放过程; 其他人需要严格和昂贵的检查。 然而,这不是组织中人民的偏好。 当非工作生产代码的成本低时,在程序中是否存在中断或运行不正确时,释放过程可能很简单。 这样,释放速度更快,更便宜。 一个例子可以是聊天程序,用于由用户使用。 在这种情况下,释放新的花哨功能可能更为重要,而不是确保无窃听的工作。 在频谱的另一端,如果您创建控制原子发电厂的代码,则失效成本可能很高。 严重的测试和仔细检查所有功能,即使在最小的变化之后也可以得到回报。

在我们的示例中,简单的跳棋可能是一个不太可能引起严重错误的区域。 这不是不可能的,但代码太简单了......是的,我知道这样的论点有点腥,但是让我们假设这些小例程可以随比较少的测试而更容易地改变。 代码。 那么如何分离这些小脚本的代码,因此它们不需要技术版本,新版本的应用程序,甚至没有重新启动应用程序? 我们有一个新的产品,需要一个新的检查,我们希望有一些方法可以在没有任何服务中断的情况下将此检查注入应用程序环境。

我们选择的解决方案是脚本。 Java 程序可以在 *javascript* 中编写的脚本, *groovy**jython* (这是 *JVM* 语言的版本 *python* )和许多其他语言。 除了 *javascript* ,这些语言的语言解释器不是 JDK 的一部分,但它们都提供了一个标准接口,它在 JDK 中定义。 结果是我们可以在我们的代码和提供脚本的开发人员中实现脚本执行,可以自由选择任何可用语言; 我们不需要关心执行 *javascript* 代码。 我们将使用与执行 *Groovy**Jython* 的同一 API。 我们唯一应该知道的是脚本所在的语言。这通常是简单的 - 我们可以猜测从文件扩展名,而且,如果猜测是不够的,我们可以要求脚本开发人员放入 *javascript* 使用`.js`扩展名为`.js`扩展名, *JYTHON*`.jy``.py`*groovy*`.groovy`的文件,等等。 同样重要的是要注意,如果我们希望我们的程序执行其中一种语言,我们应该确保翻译是在类路径上。 在 *javascript* 的情况下,这是给定的; 因此,通过本章中的演示,我们将在 *JavaScript* 中编写我们的脚本。 不会有很多; 这是毕竟,java 书而不是 *javascript* 书。

当我们希望通过以编程方式配置或扩展我们的应用程序的能力时,脚本通常是一个不错的选择。 这是我们现在的情况。

我们要做的第一件事是扩展生产信息。 如果有一个脚本检查产品的顺序的一致性,我们需要一个字段,我们可以指定脚本的名称:

```
private String checkScript;

public String getCheckScript() {
    return checkScript;
}

public void setCheckScript(String checkScript) {
    this.checkScript = checkScript;
}
```

我们不想每种产品指定多个脚本; 因此,我们不需要脚本名称列表。 我们只有一个名称指定的脚本。

要诚实地,检查器类和注释的数据结构,允许每个产品的多个注释以及每个检查器类都太复杂。 然而,我们无法避免具有足够复杂的结构,可以证明流表达的力量和能力。 既然我们已经结束了这个主题,我们可以使用更简单的数据结构进行,专注于脚本执行。

我们还必须修改`Checker`类,不仅使用 Checker 类,还可以修改脚本。 我们不能丢弃检查员类,因为我们意识到我们为此目的需要更好的脚本,我们已经有很多检查员类,我们没有融资来将它们重写为脚本。 嗯,我们是在一本书,而不是现实生活中,但在企业中,就是这样。 这就是为什么你应该非常小心,同时为企业设计解决方案。 结构和解决方案将在那里长时间在那里,并不容易抛出一块代码,因为它在技术上不是最好的。 如果它有效并在那里,业务将非常不愿意花钱在代码维护和重构上。

在摘要中 - 我们修改`Checker`类。 我们需要一个可以执行我们脚本的新课程; 因此,我们必须插入新的`final`字段,如下所示:

```
private final CheckerScriptExecutor executor; 
```

我们还必须通过添加新参数来修改构造函数以初始化`final`字段。

我们还必须在`isConsistent()`方法中使用此`executor`

```
public boolean isConsistent(Order order) {
    final var map = piCollector.collectProductInformation(order);
    if (map == null) {
        return false;
    }
    final var annotations = pcCollector.getProductAnnotations(order);
    var needAnntn = (Predicate<Annotation>) an ->
            annotations.contains(an.annotationType());
    var consistent = (Predicate<ConsistencyChecker>) c ->
            Arrays.stream(c.getClass().getAnnotations())
                    .parallel()
                    .unordered()
                    .filter(needAnntn)
                    .anyMatch(x -> c.isInconsistent(order));
    final var checkersOK = !checkers.stream().anyMatch(consistent);
    final var scriptsOK = !map.values().parallelStream().
            map(ProductInformation::getCheckScript).
            filter(Objects::nonNull).
            anyMatch(s -> executor.notConsistent(s, order));
    return checkersOK && scriptsOK;
}
```

注意,在此代码中,我们使用并行流,因为,为什么不呢? 每当可以,我们可以使用并行流,甚至无序,告诉底层系统,以及维护代码的程序员研究员,顺序并不重要。

我们还通过一些注释将我们的产品 JSON 文件修改为引用脚本而不是 Checker 类:

```
{ 
  "id" : "124", 
  "title": "Desk Lamp", 
  "checkScript" : "powered_device", 
  "description": "this is a lamp that stands on my desk", 
  "weight": "600", 
  "size": [ "300", "20", "2" ] 
}
```

甚至 JSON 都很简单。 请注意,正如我们决定使用 JavaScript 的情况下,我们不需要在命名脚本时指定文件名扩展名。

我们稍后可以考虑进一步的开发,我们将允许产品检查器脚本维护者使用不同的脚本语言。 在这种情况下,我们仍然要求他们指定扩展,并且如果没有扩展,我们的程序将由我们的程序添加为`.js`。 在我们当前的解决方案中,我们不检查一下,但我们可能会投入几秒钟才能思考它,以确保解决方案可以进一步开发。 为了进一步发展,我们不制定额外的代码。 开发人员不是算命者,不能可靠地讲述未来的需求。 这是商界人士的任务。

我们将脚本放入`scripts`目录下我们项目的`resource`目录。 文件的名称必须是`powered_device.js`,因为这是我们在 json 文件中指定的名称:

```
function isInconsistent(order){
    isConsistent = false
    items = order.getItems()
    for( i in items ){
    item = items[i]
    print( item )
        if( item.getProductId() == "126" ||
            item.getProductId() == "127" ||
            item.getProductId() == "128"  ){
            isConsistent = true
            }
    }
    return ! isConsistent
}
```

这是一个非常简单的 JavaScript 程序。 作为侧面说明,当您在 JavaScript 中迭代列表或数组时,循环变量将迭代集合或数组的索引。 由于我很少在 JavaScript 中编程,我陷入了这个陷阱,我花了超过半小时来调试我所做的错误。

我们准备了我们需要调用脚本的一切。 我们仍然需要调用它。 为此,我们使用 JDK Scripting API。 首先,我们需要一个`ScriptEngineManager`。 此管理器用于访问 JavaScript 引擎。 虽然 JavaScript 解释器是 JDK 自 Java 7 以来的一部分,但它仍然以抽象的方式管理。 它是 Java 程序可以用于执行脚本的许多可能的解释器之一。 它恰好在 JDK 中就在那里,所以我们不需要将解释器 jar 添加到类路径。 `ScriptEngineManager`发现类路径上的所有解释器并注册它们。

它使用服务提供商规范进行了很长时间已经是 JDK 的一部分,而且,通过 Java 9,它还在模块处理中得到了额外的支持。 这要求脚本解释器实现`ScriptEngineFactory`界面,也可以列出在`META-INF/services/javax.script.ScriptEngineFactory`文件中的类。 这些文件来自属于类路径的一部分的 jar 文件,通过`ScriptEngineManager`读取为资源,并且通过此,它知道它是哪些类实现脚本解释器。 `ScriptEngineFactory`接口要求解释器提供诸如`getNames()``getExtensions()``getMimeTypes()`的方法。 经理称这些方法收集有关解释器的信息。 当我们询问 JavaScript 解释器时,经理将返回由工厂创建的那个,说明其一个名称是`JavaScript`

要通过名称访问解释器,文件名扩展名或 MIME-TYPE 只是`ScriptEngineManager`的函数之一。 另一个是管理`Bindings`

当我们从 Java 代码中执行脚本时,我们不这样做,因为我们想要增加我们的多巴胺水平。 在脚本的情况下,它不会发生。 我们想要一些结果。 我们要通过参数,并在执行脚本之后,我们希望从我们可以在 Java 代码中使用的脚本中返回值。 这可能以两种方式发生。 一个是通过将参数传递给脚本中实现的方法或函数并从脚本获取返回值。 这通常有效,但它甚至可能发生一些脚本语言甚至没有函数或方法的概念。 在这种情况下,它不是一种可能性。 在执行脚本后,可能会将环境传递给脚本并从环境中读取值。 此环境由`Bindings`表示。

`Bindings`是具有`String`键和`Object`值的地图。

例如,在大多数脚本语言的情况下,例如,在 JavaScript 中,`Bindings`连接到我们执行的脚本中的全局变量。 换句话说,如果在调用脚本之前我们在 Java 程序中执行以下命令,则 JavaScript 全局变量`globalVariable`将引用`myObject`对象:

```
myBindings.put("globalVariable",myObject)
```

我们可以创建`Bindings`并将其传递给`ScriptEngineManager`,但我们也可以使用它自动创建的那个,并且可以直接在引擎对象上调用`put()`方法。

执行脚本时有两个`Bindings`。 一个人设置在`ScriptEngineManager`级别。 这被命名为全局绑定。 `ScriptEngine`本身也有一个管理。 这是本地`Bindings`。 从脚本的角度来看,没有区别。 从嵌入的角度来看,有一定程度的差异。 如果我们使用相同的`ScriptEngineManager`以创建多个`ScriptEngine`实例,则将它们共享全局绑定。 如果一个值,则所有这些都看到相同的值; 如果一个设置一个值,则稍后将看到所有其他人都会看到更改的值。 本地绑定特定于其管理的发动机。 由于我们只在本书中介绍 Java Scripting API,因此我们不会更详细地进行详细且不会使用`Bindings`。 我们很好地调用 JavaScript 函数并从中获取结果。

实现脚本调用的类是`CheckerScriptExecutor`。 它从以下行开始:

```
package packt.java11.bulkorder.services;
import ...

@Component
public class CheckerScriptExecutor {
    private static final Logger log =
            LoggerFactory.getLogger(CheckerScriptExecutor.class);

    private final ScriptEngineManager manager = new ScriptEngineManager();

    public boolean notConsistent(String script, Order order) {

        try {
            final var scriptReader = getScriptReader(script);
            final var result = evalScript(script, order, scriptReader);
            assertResultIsBoolean(script, result);
            log.info("Script {} was executed and returned {}", script, result);
            return (boolean) result;

        } catch (Exception wasAlreadyHandled) {
            return true;
        }
    }
```

唯一的`public`方法`notConsistent()`,获取要执行的脚本的名称以及`order`。 后者必须传递给脚本。 首先,它得到`Reader`,它可以读取脚本文本,评估它,最后返回结果,以防它是`boolean`或至少转换为`boolean`。 如果从此处调用的任何方法,我们在此类中实现的是错误的,它将抛出异常,但仅在适当地记录之后。 在这种情况下,安全的方式是拒绝订单。

实际上,这是企业应该决定的事情。 如果有一个无法执行的检查脚本,则显然是错误的情况。 在这种情况下,接受订单并稍后处理问题手动具有一定的成本。 拒绝订单或确认,因为某些内部错误也不是订单进程的快乐路径。 我们必须检查哪种方法对公司造成最低损害。 它肯定不是程序员的责任。 我们的情况是一种简单的情况。

我们假设商家代表说,应拒绝这种情况下的命令。 在现实生活中,商业代表拒绝了类似的决定,称它只是不应该发生,而 IT 部门必须确保该计划和整个操作完全无窃听。 这种反应存在心理原因,但这真的需要我们远离 Java 编程。

引擎可以执行通过`Reader``String`传递的脚本。 因为现在我们在资源文件中拥有脚本代码,所以似乎是一个更好的想法,让引擎读取资源而不是将其读取到`String`

```
private Reader getScriptReader(String script) throws IOException {
    final Reader scriptReader;
    try (final var scriptIS = new ClassPathResource(
            "scripts/" + script + ".js").getInputStream()) {
        scriptReader = new InputStreamReader(scriptIS);
    } catch (IOException ioe) {
        log.error("The script {} is not readable", script);
        log.error("Script opening exception", ioe);
        throw ioe;
    }
    return scriptReader;
}
```

要从资源文件中读取脚本,我们使用 Spring `ClassPathResource`类。 脚本的名称是使用`scripts`目录的,并由`.js`扩展名附加。 其余的是相当标准的,我们没有在这本书中看到过。 评估脚本的下一个方法更有趣:

```
private Object evalScript(String script, Order order, Reader scriptReader)
        throws ScriptException, NoSuchMethodException {
    final Object result;
    final var engine = manager.getEngineByName("JavaScript");
    try {
        engine.eval(scriptReader);
        final var inv = (Invocable) engine;
        result = inv.invokeFunction("isInconsistent", order);
    } catch (ScriptException | NoSuchMethodException se) {
        log.error("The script {} thruw up", script);
        log.error("Script executing exception", se);
        throw se;
    }
    return result;
}
```

要在脚本中执行方法,首先,我们需要一个能够处理 JavaScript 的脚本引擎。 我们通过名称从经理中获取发动机。 如果不是 JavaScript,我们应该检查返回的`engine`不是`null`。 在 JavaScript 的情况下,解释器是 JDK 的一部分,并检查 JDK 符合标准将是偏执狂。

如果我们想扩展这个类不仅可以处理 JavaScript,还要处理其他类型的脚本,必须完成此检查,并且还应根据文件名扩展名从管理器请求脚本引擎,我们没有 在此`private`方法中可以访问。 但这是未来的发展,也不是这本书的一部分。

当我们有引擎时,我们必须评估脚本。 这将定义脚本中的函数,以便我们可以在之后调用它。 要调用它,我们需要一些`Invocable`对象。 在 JavaScript 的情况下,引擎还实现了`Invocable`界面。 并非所有脚本引擎都实现了此界面。 某些脚本没有函数或方法,并且无法在其中调用。 再次,当我们想要不仅允许 JavaScript 脚本而且还有其他类型的脚本时,这是稍后的事情。

要调用函数,我们将其名称传递给`invokeFunction()`方法,以及我们要传递的参数。 在这种情况下,这是`order`。 在 JavaScript 的情况下,两种语言之间的集成相当开发。 与我们的示例一样,我们可以访问该字段,并将其作为参数传递的 Java 对象以及返回的 JavaScript `true``false`值也神奇地转换为`Boolean`。 当访问不那么简单时,有一些情况:

```
private void assertResultIsBoolean(String script, Object result) {
    if (!(result instanceof Boolean)) {
        log.error("The script {} returned non boolean", script);
        if (result == null) {
            log.error("returned value is null");
        } else {
            log.error("returned type is {}", result.getClass());
        }
        throw new IllegalArgumentException();
    }
}
```

该类的最终方法检查返回的值,因为这是脚本引擎的返回值,可以转换为`boolean`

重要的是要注意,一些功能在脚本中实现的事实并不能保证应用程序无缝地工作。 可能有几个问题,脚本可能会影响整个应用程序的内部工作。 一些脚本引擎提供了保护应用程序免受糟糕脚本保护应用的特殊方法,而其他则为其他脚本。 我们没有通过的事实,但给出命令,脚本不保证脚本无法访问其他对象。 使用反射,`static`方法和其他技术,有些方法可以在 Java 程序中访问任何内容。 测试周期只有在我们的代码库中的脚本更改时,我们可能有点易于更容易,但这并不意味着我们应该盲目地信任任何脚本。

在我们的示例中,让产品的生产者将脚本上传到我们的系统可能是一个非常糟糕的想法。 它们可以提供其检查脚本,但必须从部署到系统中的安全性点中从安全的角度审查这些脚本。 如果这是正确完成的,那么脚本是 Java 生态系统的一个极其强大的扩展,对我们的程序提供了极大的灵活性。

# 概括

在本章中,我们开发了企业应用程序的订购系统。 随着代码的发展,我们遇到了许多新事物。 您了解了诠释以及如何通过反思处理。 虽然没有强烈相关,但您学会了如何使用 Lambda 表达式和流来表达多个编程构造比传统环路更简单。 在本章的最后一部分中,我们通过调用 Java 的 javaScript 函数来使用脚本来扩展应用程序,也可以通过从 JavaScript 中调用 Java 方法。

事实上,通过所有这些知识,我们已经成熟到企业编程所需的 Java 级别。 本书封面的其余内容适用于 ACES。 但你想成为一个,不是吗? 这就是为什么我写下剩下的章节。 阅读!