【前沿】JAVA8与spring5 - hippowc/hippowc.github.io GitHub Wiki
Spring 5 于 2017 年 9 月发布了通用版本 (GA),它标志着自 2013 年 12 月以来第一个主要 Spring Framework 版本。它提供了一些人们期待已久的改进,还采用了一种全新的编程范例,以反应式宣言中陈述的反应式原则为基础。Spring 5.0 基于 Java 8,spring的源代码使用jdk8重写过,它集成了反应式流,以便提供一种颠覆性方法来实现端点和 Web 应用程序开发。
Reactor 是一个完全基于反应式流规范的全新实现,也是 Spring 5 默认的反应式框架。Reactor 的两个最核心的类是 Flux 和 Mono。Reactor 采用了两个不同的类来表示流。Flux 表示的包含0到无限个元素的流,而 Mono 则表示最多一个元素的流。虽然从逻辑上来说,Mono 表示的流都可以用 Flux 来表示,这样的区分使得很多操作的语义更容易理解。Flux 和 Mono 的强大之处来源于各种不同的操作符。
WebFlux 是 Spring 5 中新引入的开发反应式 Web 应用的模块。该模块中包含了对反应式 HTTP、服务器推送事件(Server-sent Events)和 WebSocket 的客户端和服务器端的支持。在服务器端,WebFlux 支持两种不同的编程模型:第一种是 Spring MVC 中使用的基于 Java 注解的方式;第二种是基于 Java 8 的 Lambda 表达式的函数式编程模型。
基于 Java 注解的编程模型与之前的 Spring MVC 的注解方式并没有太大的区别,容易上手。函数式编程模型功能强大,也更灵活,可以实现动态路由等复杂场景,相应的也更难上手。
与传统 Spring MVC 的区别在于,WebFlux 的请求和响应使用的都是 Flux 或 Mono 对象。一般的 REST API 使用 Mono 来表示请求和响应对象;服务器推送事件使用 Flux 来表示从服务器端推送的事件流;WebSocket 则使用 Flux 来表示客户端和服务器之间的双向数据传递。
为了最大程度的发挥反应式流和负压的作用,WebFlux 应用的各个部分都应该是支持反应式的,也就是说各个部分都应该是异步非阻塞的。要做到这一点,需要其他的库提供支持,主要是与外部系统和服务整合的部分。比如在数据访问层,可以通过 Spring Data 的反应式支持来访问不同类型的数据源。当然这也需要底层驱动的支持。越来越多的数据源驱动已经提供了对反应式流规范的支持。
- 及时响应(Responsive):系统在尽可能的情况下及时响应请求。
- 有韧性(Resilient):系统在出现失败时仍然可以及时响应。
- 有弹性(Elastic):在不同的负载下,系统仍然保持及时响应。
- 消息驱动(Message Driven):系统使用异步消息传递来确定不同组件之间的边界,并确保松散耦合、隔离和位置透明性。
及时响应是核心价值,是反应式系统所追求的目标。有韧性和有弹性是反应式系统的外在表现形式,通过它们才能实现及时响应这个核心价值。消息驱动则是实现手段。
是系统在负载过大时的重要反馈手段。当一个组件的负载过大时,可能导致该组件崩溃。为了避免组件失败,它应该通过负压来通知其上游组件减少负载。负压可能会一直级联往上传递,最终到达用户处,进而影响响应的及时性。
反应式流(Reactive Streams)是一个反应式编程相关的规范。反应式流为带负压的异步非阻塞流处理提供了标准。反应式流规范的出发点是作为不同反应式框架互操作的基础,因此它所提供的接口很简单。在其 Java API 中,只定义了4个接口。
- 直接的方法调用
- 使用 Iterable
- 使用 Future
- 反应式流
Future异步方式的问题:很难找到一个合适的时机来获取 Future 对象的计算结果。因为 get 方法是阻塞的,如果调用早了,主线程仍然会被阻塞;如果调用晚了,在某种程度上降低了并发的效率。
Java 8 的 CompletableFuture 的出现解决了上面提到的 Future 的问题,解决的办法是允许异步操作进行级联。提交一个异步操作后返回一个CompletableFuture 对象,只需要通过 thenApply 或 thenRun 就可以调用后续的方法。CompletableFuture 仍然只能表示一个结果。如果把 CompletableFuture 的思路进一步扩展,就是反应式流解决问题的思路。
编程思维模式的转换:从以逻辑为中心到以数据为中心的转换,也是命令式到声明式的转换。
传统的命令式编程范式以控制流为核心,通过顺序、分支和循环三种控制结构来完成不同的行为。开发人员在程序中编写的是执行的步骤。而以数据为中心侧重的是数据在不同组件的流动。开发人员在程序中编写的是对数据变化的声明式反应。
一个例子: 希望实现一个购物车,购物车可以显示车内商品总价。
传统命令式思路:会有一个更新车内商品的方法,这个方法完后再去调用计算总价方法。
事件驱动思路:为不同的动作创建相应的事件。每个事件有自己的类型和相应的数据(payload)。比如,商品数量更新事件的数据中会包含商品的 ID 和新的数量。引入事件的好处是可以把调用者和处理者进行解耦。具体步骤:
- 调用者创建事件并发布。
- 事件中间件负责传递事件,通常采用事件总线(Event Bus)来完成。
- 处理者接收到事件进行处理。
流的思路:值可能产生变化的变量都是一个流。流中的元素代表了变量在不同时刻的值。如果一个变量的值的变化会引起另外一个变量的变化,则把前一个变量所表示的流作为它所能引起变化另外一个变量对应的流的上游。
stream不是io中的InputStream,而是针对集合功能的增强,专注于增强聚合操作,同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势。
在传统的 J2EE 应用中,Java 代码经常不得不依赖于关系型数据库的聚合操作来完成诸如:取平均值,获取最值,取某十个值等等。但在很多时候不得不脱离 RDBMS,或者以底层返回的数据为基础进行更上层的数据统计。
一个例子:如果要发现 type 为 grocery 的所有交易,然后返回以交易值降序排序好的交易 ID 集合: Java 7:
List<Transaction> groceryTransactions = new Arraylist<>();
for(Transaction t: transactions){
if(t.getType() == Transaction.GROCERY){
groceryTransactions.add(t);
}
}
Collections.sort(groceryTransactions, new Comparator(){
public int compare(Transaction t1, Transaction t2){
return t2.getValue().compareTo(t1.getValue());
}
});
List<Integer> transactionIds = new ArrayList<>();
for(Transaction t: groceryTransactions){
transactionsIds.add(t.getId());
}
Java 8
List<Integer> transactionsIds = transactions.parallelStream().
filter(t -> t.getType() == Transaction.GROCERY).
sorted(comparing(Transaction::getValue).reversed()).
map(Transaction::getId).
collect(toList());
Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次.而和迭代器又不同的是,Stream 可以并行化操作
数据会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。Stream 的并行操作依赖于 Java7 中引入的 Fork/Join 框架(JSR166y)来拆分任务和加速处理过程。
Java 的并行 API 演变历程基本如下:
- 1.0-1.4 中的 java.lang.Thread
- 5.0 中的 java.util.concurrent
- 6.0 中的 Phasers 等
- 7.0 中的 Fork/Join 框架
- 8.0 中的 Lambda
流的操作类型分为两种:
- Intermediate:一个流可以后面跟随零个或多个 intermediate 操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。
- Terminal:一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果,或者一个 side effect。
// 1. Individual values
Stream stream = Stream.of("a", "b", "c");
// 2. Arrays
String [] strArray = new String[] {"a", "b", "c"};
stream = Stream.of(strArray);
stream = Arrays.stream(strArray);
// 3. Collections
List<String> list = Arrays.asList(strArray);
stream = list.stream();
对于基本数值型,目前有三种对应的包装类型 Stream: IntStream、LongStream、DoubleStream。当然我们也可以用 Stream、Stream >、Stream,但是 boxing 和 unboxing 会很耗时,所以特别为这三种基本数值型提供了对应的 Stream。
// 1. Array
String[] strArray1 = stream.toArray(String[]::new);
// 2. Collection
List<String> list1 = stream.collect(Collectors.toList());
List<String> list2 = stream.collect(Collectors.toCollection(ArrayList::new));
Set set1 = stream.collect(Collectors.toSet());
Stack stack1 = stream.collect(Collectors.toCollection(Stack::new));
// 3. String
String str = stream.collect(Collectors.joining()).toString();
- Intermediate:map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered
- Terminal:forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator
- Short-circuiting:anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 limit
具体用法:流的用法
lambda表达式其实就是实现SAM接口的语法糖。
lambda表达式语法:
lambda表达式的一般语法:
(Type1 param1, Type2 param2, ..., TypeN paramN) -> {
statment1;
statment2;
//.............
return statmentM;
}
单参数语法:
param1 -> {
statment1;
statment2;
//.............
return statmentM;
}
单语句写法:
param1 -> statment
lambda表达是理解:
Lambda允许把函数作为一个方法的参数(函数作为参数传递进方法中),或者把代码看成数据,同时引入了函数式接口的概念,函数式接口就是一个具有一个方法的普通接口,这样的接口,可以被隐士转换为lambda表达式,在实际使用过程中,函数式接口时容易出错的,如某个人在接口定义中增加了另一个方法,这时这个接口就不再是函数式接口了,并且编译过程会失败,为了克服这种脆弱性并且能够明确声明接口作为函数式接口的意图,Java 8增加了一种特殊的注解@FunctionalInterface(静态方法和默认方法并不影响函数式接口的契约)。这个新的特性的目的,就是为了消除单方法接口实现的匿名内部类。
Java 8中增加了一个新的包:Java.util.function,它里面包含了常用的函数式接口:
Predicate——接收T对象并返回boolean
Consumer——接收T对象,不返回值
Function<T, R>——接收T对象,返回R对象
Supplier——提供T对象(例如工厂),不接收值
UnaryOperator——接收T对象,返回T对象
BinaryOperator——接收两个T对象,返回T对象
理解一下:其实lambda表达式就相当于new一个接口并对接口中的方法进行实现。所有只有一个方法的接口都可以隐式的使用lambda表达式来写实现,譬如Runnable接口
而方法引用则是lambda表达式的一种更简单的写法:
方法引用是用来直接访问类或者实例的已经存在的方法或者构造方法。方法引用提供了一种引用而不执行方法的方式,方法引用的唯一用途是支持Lambda的简写,使用::操作符将方法名和对象或类的名字分隔开.
理解下,什么时候lambda表达式可以写成方法引用呢?当lambda表达式的方法体是一个静态或者示例方法时,就可以考虑了,但由于方法引用是没有参数的,所以要能正确调用,lambda表达式的参数(时刻记得lambda表达式实际就是接口中定义的方法),与方法体中调用方法的参数需要有某种对应关系。
方法引用共分为四类:
1.类名::静态方法名
当传入参数和静态方法所需参数个数一致时,就不存在歧义
2.对象::实例方法名
与类方法引用不同的是,对象方法引用方法的调用者是一个外部的对象
3.类名::实例方法名
类的成员方法不能是静态的,而这个情况其实和静态方法类似,区别是,Lambda表达式的参数个数需要等于所调用方法的入参个数加一,因为类的成员方法不能通过类名直接调用,只能通过对象来调用,也就是Lambda表达式的第一个参数,是方法的调用者,从第二个开始的参数个数要和需要调用方法的入参个数一致即可
4.类名::new
Lambda的参数个数和类的构造方法个数一致
- 在lambda表达式中访问外层标记了final的局部变量,或者这个变量隐含不能修改(譬如方法参数),同时lambda表达式中也是不允许修改局部变量的
- 可以访问对象字段和静态变量,这些都是可以读写
- lambda表达式中的this,会引用创建该 Lambda 表达式的方法的 this 参数
Java 9 中的 Flow 只是简单的把反应式流规范的4个接口整合到了一个类中
- Publisher。Publisher 是数据的发布者。Publisher 接口只有一个方法 subscribe 来添加数据的订阅者,也就是下面的 Subscriber。
- Subscriber 是数据的订阅者。Subscriber 接口有4个方法,都是作为不同事件的处理器。在订阅者成功订阅到发布者之后,其 onSubscribe(Subscription s) 方法会被调用。Subscription 表示的是当前的订阅关系。
- Subscription 表示的是一个订阅关系。除了之前提到的 request 方法之外,还有 cancel 方法用来取消订阅。
- Processor 表示的一种特殊的对象,既是生产者,又是订阅者
反应式流规范所提供的 API 很简单,目前 Java 平台上主流的反应式库有两个,分别是 Netflix 维护的 RxJava 和 Pivotal 维护的 Reactor。RxJava 是 Java 平台反应式编程的鼻祖。反应式流规范在很大程度上借鉴了 RxJava 的理念。
Reactor 是一个完全基于反应式流规范的全新实现,也是 Spring 5 默认的反应式框架。
Java Hipster,是一个应用代码产生器,一个跨越前后端的全栈Boot,JHipster使用Node.js和Yeoman产生Java应用代码,使用Maven(Gradle)运行产生的代码,产生代码有如下关键特征:
- src/main/java 目录有Spring Boot 配置类在config包中,JHipster使用Spring的Java 配置,没有XML配置。
- JPA实体或MongoDB文档类是在domain包. JPA实体使用缓存和auto-generated 主键配置. 如果你使用JHipster产生你的JPA实体, 可以创建1:N和N:N关系。
- 在repostiory包中是Spring Data 仓储.
- 可选,你有通常@Service-beans 在服务层. 这些服务通常是配置为事务的 安全的业务对象。
- REST 端点存在web.rest 包中, 支持Spring MVC的REST
- JHipster也产生 Liquibase 改变日志文件,用来处理数据库更新,增加一个实体将创建特定的schema更新,这将会版本化,当应用重启时可被执行。
- 集成Spring的 Test 上下文测试支持.
- JHipster 创建完整可用的AngularJS 前端,使用CRUD来管理你产生的实体。