first commit

This commit is contained in:
张乾
2024-10-16 06:37:41 +08:00
parent 633f45ea20
commit 206fad82a2
3590 changed files with 680090 additions and 0 deletions

View File

@ -0,0 +1,75 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
01 导读:课程概要
背景
大家都知道Spring Framework 是 Java/Spring 应用程序跨平台开发框架,也是 Java EEJava Enterprise Edition 轻量级框架,其 Spring 平台为 Java 开发者提供了全面的基础设施支持,虽然 Spring 基础组件的代码是轻量级,但其配置依旧是重量级的。
那是怎么解决了呢?当然是 Spring BootSpring Boot 提供了新的编程模式,让开发 Spring 应用变得更加简单方便。本课将会由各个最佳实践工程出发,涉及 Spring Boot 开发相关的各方面,下面先了解下 Spring Boot 框架。
Spring Boot 是什么
Spring BootBoot 顾名思义,是引导的意思)框架是用于简化 Spring 应用从搭建到开发的过程。应用开箱即用,只要通过一个指令,包括命令行 java -jar、SpringApplication 应用启动类、Spring Boot Maven 插件等就可以启动应用了。另外Spring Boot 强调只需要很少的配置文件,所以在开发生产级 Spring 应用中让开发变得更加高效和简易。目前Spring Boot 版本是 2.x 版本。
Spring Boot WebFlux 是什么
可以看到上面的图WebFlux 相对 MVC 一样重要。大多数场景使用 MVC 都是阻塞式的WebFlux 使用的场景是异步非阻塞的那什么是响应式编程Reactive Programming
官方文档这样描述:响应式编程是基于异步和事件驱动的非阻塞程序,只是垂直通过在 JVM 内启动少量线程扩展,而不是水平通过集群扩展。
Spring Boot 2.0 包括一个新的 spring-webflux 模块,该模块包含对响应式 HTTP 和 WebSocket 客户端的支持,以及对 REST、HTML 和 WebSocket 交互等程序的支持。
为什么学习 Spring Boot WebFlux
微服务架构越来越火Spring Boot 是一大趋势,因为 Spring Cloud 是基于 Spring Boot 的,所以学好 Spring Boot WebFlux 会受益匪浅。
从编程来讲,虽然阻塞式编程是避免不了的,但是 Reactive 编程在大多数场景,能够提高资源利用率。所以,学习 WebFlux尤其某些 IO 密集型场景很刚需。
本课程可以学到什么
本课程是一个系列基础教程,目标是带领读者上手实战,课程以新版本 Spring Boot 2.0 WebFlux 的核心概念作为主线。围绕 Spring Boot 2.0 WebFlux 技术栈的系列教程,目标是带领读者了解 Spring Boot 2.0 WebFlux 各种特性,并学会使用 Spring Boot 相关技术栈上手开发项目。尤其对于微服务下,如果是 IO 密集型等场景,可以考虑使用 WebFlux 去实现。
主要包含知识点快速入门、CRUD 、MongoDB Reactive、Thymeleaf、Redis、WebSocket 等,一步一步了解每个组件的使用方式,最后综合演练。
认真学完这个系列文章之后,会对 Spring Boot 2.0 WebFlux 有进一步的了解,具备使用 Spring Boot 2.0 WebFlux 上手进行开发的基本能力。
我早期写的文章可以看我的 GitHub。
适合阅读的人群
本 GitChat 课程适合以下人群阅读:
Spring Boot 2.0 WebFlux 新版本的初学人员
对 Spring Boot 2.0 WebFlux 开发感兴趣的开发人员
希望了解 Spring Boot 2.0 WebFlux 的研发人员
版本使用 Spring Boot 2.0.1.RELEASE 版本进行开发演示,技术人员需要具备 Maven、Git 基本技能。
开发运行环境:
JDK 1.8 + Spring Boot 2.x 要求 JDK 1.8 环境及以上版本。另外Spring Boot 2.x 只兼容 Spring Framework 5.0 及以上版本。
Maven 3.2+ 为 Spring Boot 2.x 提供了相关依赖构建工具是 Maven版本需要 3.2 及以上版本,使用 Gradle 则需要 1.12 及以上版本。Maven 和 Gradle 大家各自挑选下喜欢的就好。
IntelliJ IDEA IntelliJ IDEA (简称 IDEA是常用的开发工具也是本书推荐使用的同样使用 Eclipse IDE 自然也是可以的。
开发使用框架:
MongoDB
Redis
Thymeleaf
WebSocket

View File

@ -0,0 +1,291 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
02 WebFlux 快速入门实践
Spring Boot 2.0
spring.io 官网有句醒目的话是:
BUILD ANYTHING WITH SPRING BOOT
Spring Boot Boot 顾名思义,是引导的意思)框架是用于简化 Spring 应用从搭建到开发的过程。应用开箱即用,只要通过一个指令,包括命令行 java -jar 、SpringApplication 应用启动类 、 Spring Boot Maven 插件等就可以启动应用了。另外Spring Boot 强调只需要很少的配置文件,所以在开发生产级 Spring 应用中让开发变得更加高效和简易。目前Spring Boot 版本是 2.x 版本。Spring Boot 包括 WebFlux。
Spring Boot 2.0 WebFlux
了解 WebFlux首先了解下什么是 Reactive Streams。Reactive Streams 是 JVM 中面向流的库标准和规范:
处理可能无限数量的元素
按顺序处理
组件之间异步传递
强制性非阻塞背压Backpressure
Backpressure背压
背压是一种常用策略,使得发布者拥有无限制的缓冲区存储元素,用于确保发布者发布元素太快时,不会去压制订阅者。
Reactive Streams响应式流
一般由以下组成:
发布者:发布元素到订阅者
订阅者:消费元素
订阅:在发布者中,订阅被创建时,将与订阅者共享
处理器:发布者与订阅者之间处理数据
响应式编程
有了 Reactive Streams 这种标准和规范,利用规范可以进行响应式编程。那再了解下什么是 Reactive programming 响应式编程。响应式编程是基于异步和事件驱动的非阻塞程序,只是垂直通过在 JVM 内启动少量线程扩展,而不是水平通过集群扩展。这就是一个编程范例,具体项目中如何体现呢?
响应式项目编程实战中,通过基于 Reactive Streams 规范实现的框架 Reactor 去实战。Reactor 一般提供两种响应式 API
Mono实现发布者并返回 0 或 1 个元素
Flux实现发布者并返回 N 个元素
Spring Webflux
Spring Boot Webflux 就是基于 Reactor 实现的。Spring Boot 2.0 包括一个新的 spring-webflux 模块。该模块包含对响应式 HTTP 和 WebSocket 客户端的支持,以及对 RESTHTML 和 WebSocket 交互等程序的支持。一般来说Spring MVC 用于同步处理Spring Webflux 用于异步处理。
Spring Boot Webflux 有两种编程模型实现,一种类似 Spring MVC 注解方式,另一种是使用其功能性端点方式。注解的会在第二篇文章讲到,下面快速入门用 Spring Webflux 功能性方式实现。
Spring Boot 2.0 WebFlux 特性
常用的 Spring Boot 2.0 WebFlux 生产的特性如下:
响应式 API
编程模型
适用性
内嵌容器
Starter 组件
还有对日志、Web、消息、测试及扩展等支持。
响应式 API
Reactor 框架是 Spring Boot Webflux 响应库依赖,通过 Reactive Streams 并与其他响应库交互。提供了 两种响应式 APIMono 和 Flux。一般是将 Publisher 作为输入,在框架内部转换成 Reactor 类型并处理逻辑,然后返回 Flux 或 Mono 作为输出。
适用性
一图就很明确了WebFlux 和 MVC 有交集,方便大家迁移。但是注意:
MVC 能满足场景的,就不需要更改为 WebFlux。
要注意容器的支持,可以看看下面内嵌容器的支持。
微服务体系结构WebFlux 和 MVC 可以混合使用。尤其开发 IO 密集型服务的时候,选择 WebFlux 去实现。
编程模型
Spring 5 web 模块包含了 Spring WebFlux 的 HTTP 抽象。类似 Servlet API , WebFlux 提供了 WebHandler API 去定义非阻塞 API 抽象接口。可以选择以下两种编程模型实现:
注解控制层。和 MVC 保持一致WebFlux 也支持响应性 @RequestBody 注解。
功能性端点。基于 lambda 轻量级编程模型,用来路由和处理请求的小工具。和上面最大的区别就是,这种模型,全程控制了请求 - 响应的生命流程
内嵌容器
跟 Spring Boot 大框架一样启动应用,但 WebFlux 默认是通过 Netty 启动,并且自动设置了默认端口为 8080。另外还提供了对 Jetty、Undertow 等容器的支持。开发者自行在添加对应的容器 Starter 组件依赖,即可配置并使用对应内嵌容器实例。
但是要注意,必须是 Servlet 3.1+ 容器,如 Tomcat、Jetty或者非 Servlet 容器,如 Netty 和 Undertow。
Starter 组件
跟 Spring Boot 大框架一样Spring Boot Webflux 提供了很多 “开箱即用” 的 Starter 组件。Starter 组件是可被加载在应用中的 Maven 依赖项。只需要在 Maven 配置中添加对应的依赖配置,即可使用对应的 Starter 组件。例如,添加 spring-boot-starter-webflux 依赖,就可用于构建响应式 API 服务,其包含了 Web Flux 和 Tomcat 内嵌容器等。
开发中,很多功能是通过添加 Starter 组件的方式来进行实现。那么Spring Boot 2.x 常用的 Starter 组件有哪些呢?
Spring Boot 2.0 WebFlux 组件
Spring Boot WebFlux 官方提供了很多 Starter 组件,每个模块会有多种技术实现选型支持,来实现各种复杂的业务需求:
WebSpring WebFlux
模板引擎Thymeleaf
存储Redis、MongoDB、Cassandra不支持 MySQL
内嵌容器Tomcat、Jetty、Undertow
Spring Initializr 快速构建项目骨架
Spring Boot Maven 工程,就是普通的 Maven 工程,加入了对应的 Spring Boot 依赖即可。Spring Initializr 则是像代码生成器一样,自动就给你出来了一个 Spring Boot Maven 工程。Spring Initializr 有两种方式可以得到 Spring Boot Maven 骨架工程:
start.spring.io 在线生成
Spring 官方提供了名为 Spring Initializr 的网站,去引导你快速生成 Spring Boot 应用。网站地址,详见这里,操作步骤如下:
第一步,选择 Maven 或者 Gradle 构建工具,开发语言 Java 、Kotlin 或者 Groovy最后确定 Spring Boot 版本号。这里默认选择 Maven 构建工具、Java 开发语言和 Spring Boot 2.0.1。
第二步,输入 Maven 工程信息,即项目组 groupId 和名字 artifactId这里对应 Maven 信息为:
groupIdspringboot
artifactIdsspringboot-webflux-1-quickstart
这里默认版本号 version 为 0.0.1-SNAPSHOT三个属性在 Maven 依赖仓库是唯一标识的。
第三步,选择工程需要的 Starter 组件和其他依赖,最后单击“生成”按钮,即可获得骨架工程压缩包,这里快速入门,只要选择 Reactive Web 即可,如图所示。
配置 POM 依赖
检查工程 POM 文件中,是否配置了 spring-boot-starter-webflux 依赖。如果是上面自动生成的,配置如下:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
spring-boot-starter-webflux 依赖,是我们核心需要学习 webflux 的包,里面默认包含了 spring-boot-starter-reactor-netty 、spring 5 webflux 包,也就是说默认是通过 netty 启动的。
reactor-test、spring-boot-starter-test 两个依赖搭配是用于单元测试。
spring-boot-maven-plugin 是 Spring Boot Maven 插件,可以运行、编译等调用。
编写处理器类 Handler
新建包 org.spring.springboot.handler作为编写功能处理类。新建城市City例子的处理类 CityHandler代码如下
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
@Component
public class CityHandler {
public Mono<ServerResponse> helloCity(ServerRequest request) {
return ServerResponse.ok().contentType(MediaType.TEXT_PLAIN)
.body(BodyInserters.fromObject("Hello, City!"));
}
}
ServerResponse 是对响应的封装,可以设置响应状态、响应头、响应正文。比如 ok 代表的是 200 响应码、MediaType 枚举是代表这文本内容类型、返回的是 String 的对象。
这里用 Mono 作为返回对象,是因为返回包含了一个 ServerResponse 对象,而不是多个元素。
编写路由器类 Router
新建 org.spring.springboot.router 包作为编写路由器类。新建城市City例子的路由类 CityRouter代码如下
import org.spring.springboot.handler.CityHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
@Configuration
public class CityRouter {
@Bean
public RouterFunction<ServerResponse> routeCity(CityHandler cityHandler) {
return RouterFunctions
.route(RequestPredicates.GET("/hello")
.and(RequestPredicates.accept(MediaType.TEXT_PLAIN)),
cityHandler::helloCity);
}
}
RouterFunctions 对请求路由处理类,即将请求路由到处理器,这里将一个 GET 请求 /hello 路由到处理器 cityHandler 的 helloCity 方法上。跟 Spring MVC 模式下的 HandleMapping 的作用类似。
RouterFunctions.route(RequestPredicate, HandlerFunction) 方法,对应的入参是请求参数和处理函数,如果请求匹配,就调用对应的处理器函数。
到这里一个简单的服务就写好了,下面怎么运行该服务。
启动运行项目
一个简单的 Spring Boot Webflux 工程就开发完毕了,下面运行工程验证下。使用 IDEA 右侧工具栏,点击 Maven Project Tab ,点击使用下 Maven 插件的 install 命令,或者使用命令行的形式,在工程根目录下,执行 Maven 清理和安装工程的指令:
cd springboot-webflux-1-quickstart
mvn clean install
在控制台中看到成功的输出:
... 省略
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01:30 min
[INFO] Finished at: 2017-10-15T10:00:54+08:00
[INFO] Final Memory: 31M/174M
[INFO] ------------------------------------------------------------------------
运行工程
在 IDEA 中执行 Application 类启动,任意正常模式或者 Debug 模式,可以在控制台看到成功运行的输出:
... 省略
2018-04-10 08:43:39.932 INFO 2052 --- [ctor-http-nio-1] r.ipc.netty.tcp.BlockingNettyContext : Started HttpServer on /0:0:0:0:0:0:0:0:8080
2018-04-10 08:43:39.935 INFO 2052 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8080
2018-04-10 08:43:39.960 INFO 2052 --- [ main] org.spring.springboot.Application : Started Application in 6.547 seconds (JVM running for 9.851)
一看,确实是 Netty 启动的。
打开浏览器,访问 /hello 地址,会看到如图所示的返回结果:
总结
本文主要讲了 Spring Boot 2.0 WebFlux 背景和快速入门使用,用的是基于功能性端点去创建一个服务,但这个有点代码偏多。下一课一个 CRUD 我们使用注解控制层,让开发更方便。

View File

@ -0,0 +1,383 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
03 WebFlux Web CRUD 实践
上一篇基于功能性端点去创建一个简单服务,实现了 Hello。这一篇用 Spring Boot WebFlux 的注解控制层技术创建一个 CRUD WebFlux 应用,让开发更方便。这里我们不对数据库储存进行访问,因为后续会讲到,而且这里主要是讲一个完整的 WebFlux CRUD。
结构
这个工程会对城市City进行管理实现 CRUD 操作。该工程创建编写后,得到下面的结构,其目录结构如下:
├── pom.xml
├── src
&nbsp;&nbsp; └── main
&nbsp;&nbsp; ├── java
&nbsp;&nbsp;&nbsp;&nbsp; └── org
&nbsp;&nbsp;&nbsp;&nbsp; └── spring
&nbsp;&nbsp;&nbsp;&nbsp; └── springboot
&nbsp;&nbsp;&nbsp;&nbsp; ├── Application.java
&nbsp;&nbsp;&nbsp;&nbsp; ├── dao
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; └── CityRepository.java
&nbsp;&nbsp;&nbsp;&nbsp; ├── domain
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; └── City.java
&nbsp;&nbsp;&nbsp;&nbsp; ├── handler
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; └── CityHandler.java
&nbsp;&nbsp;&nbsp;&nbsp; └── webflux
&nbsp;&nbsp;&nbsp;&nbsp; └── controller
&nbsp;&nbsp;&nbsp;&nbsp; └── CityWebFluxController.java
&nbsp;&nbsp; └── resources
&nbsp;&nbsp; └── application.properties
└── target
如目录结构,我们需要编写的内容按顺序有:
对象
数据访问层类 Repository
处理器类 Handler
控制器类 Controller
对象
新建包 org.spring.springboot.domain作为编写城市实体对象类。新建城市City对象 City代码如下
/**
* 城市实体类
*
*/
public class City {
/**
* 城市编号
*/
private Long id;
/**
* 省份编号
*/
private Long provinceId;
/**
* 城市名称
*/
private String cityName;
/**
* 描述
*/
private String description;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getProvinceId() {
return provinceId;
}
public void setProvinceId(Long provinceId) {
this.provinceId = provinceId;
}
public String getCityName() {
return cityName;
}
public void setCityName(String cityName) {
this.cityName = cityName;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
城市包含了城市编号、省份编号、城市名称和描述。具体开发中,会使用 Lombok 工具来消除冗长的 Java 代码,尤其是 POJO 的 getter / setter 方法,具体查看 Lombok 官网地址。
数据访问层 CityRepository
新建包 org.spring.springboot.dao作为编写城市数据访问层类 Repository。新建 CityRepository代码如下
import org.spring.springboot.domain.City;
import org.springframework.stereotype.Repository;
import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;
@Repository
public class CityRepository {
private ConcurrentMap<Long, City> repository = new ConcurrentHashMap<>();
private static final AtomicLong idGenerator = new AtomicLong(0);
public Long save(City city) {
Long id = idGenerator.incrementAndGet();
city.setId(id);
repository.put(id, city);
return id;
}
public Collection<City> findAll() {
return repository.values();
}
public City findCityById(Long id) {
return repository.get(id);
}
public Long updateCity(City city) {
repository.put(city.getId(), city);
return city.getId();
}
public Long deleteCity(Long id) {
repository.remove(id);
return id;
}
}
@Repository 用于标注数据访问组件,即 DAO 组件。实现代码中使用名为 repository 的 Map 对象作为内存数据存储并对对象具体实现了具体业务逻辑。CityRepository 负责将 Book 持久层(数据操作)相关的封装组织,完成新增、查询、删除等操作。
这里不会涉及到数据存储这块,具体数据存储会在后续介绍。
处理器类 Handler
新建包 org.spring.springboot.handler作为编写城市处理器类 CityHandler。新建 CityHandler代码如下
import org.spring.springboot.dao.CityRepository;
import org.spring.springboot.domain.City;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Component
public class CityHandler {
private final CityRepository cityRepository;
@Autowired
public CityHandler(CityRepository cityRepository) {
this.cityRepository = cityRepository;
}
public Mono<Long> save(City city) {
return Mono.create(cityMonoSink -> cityMonoSink.success(cityRepository.save(city)));
}
public Mono<City> findCityById(Long id) {
return Mono.justOrEmpty(cityRepository.findCityById(id));
}
public Flux<City> findAllCity() {
return Flux.fromIterable(cityRepository.findAll());
}
public Mono<Long> modifyCity(City city) {
return Mono.create(cityMonoSink -> cityMonoSink.success(cityRepository.updateCity(city)));
}
public Mono<Long> deleteCity(Long id) {
return Mono.create(cityMonoSink -> cityMonoSink.success(cityRepository.deleteCity(id)));
}
}
@Component 泛指组件,当组件不好归类的时候,使用该注解进行标注,然后用 final 和 @Autowired 标注在构造器注入 CityRepository Bean代码如下
private final CityRepository cityRepository;
@Autowired
public CityHandler(CityRepository cityRepository) {
this.cityRepository = cityRepository;
}
从返回值可以看出Mono 和 Flux 适用于两个场景,即:
Mono实现发布者并返回 0 或 1 个元素,即单对象。
Flux实现发布者并返回 N 个元素,即 List 列表对象。
有人会问,这为啥不直接返回对象,比如返回 City/Long/List。原因是直接使用 Flux 和 Mono 是非阻塞写法,相当于回调方式。利用函数式可以减少了回调,因此会看不到相关接口。这恰恰是 WebFlux 的好处:集合了非阻塞 + 异步。
Mono
Mono 是什么? 官方描述如下A Reactive Streams Publisher with basic rx operators that completes successfully by emitting an element, or with an error.
Mono 是响应流 Publisher 具有基础 rx 操作符,可以成功发布元素或者错误,如图所示:
Mono 常用的方法有:
Mono.create():使用 MonoSink 来创建 Mono。
Mono.justOrEmpty():从一个 Optional 对象或 null 对象中创建 Mono。
Mono.error():创建一个只包含错误消息的 Mono。
Mono.never():创建一个不包含任何消息通知的 Mono。
Mono.delay():在指定的延迟时间之后,创建一个 Mono产生数字 0 作为唯一值。
Flux
Flux 是什么官方描述如下A Reactive Streams Publisher with rx operators that emits 0 to N elements, and then completes (successfully or with an error).
Flux 是响应流 Publisher 具有基础 rx 操作符,可以成功发布 0 到 N 个元素或者错误。Flux 其实是 Mono 的一个补充,如图所示:
所以要注意:如果知道 Publisher 是 0 或 1 个,则用 Mono。
Flux 最值得一提的是 fromIterable 方法fromIterable(Iterable it) 可以发布 Iterable 类型的元素。当然Flux 也包含了基础的操作map、merge、concat、flatMap、take这里就不展开介绍了。
控制器类 Controller
Spring Boot WebFlux 开发中不需要配置。Spring Boot WebFlux 可以使用自动配置加注解驱动的模式来进行开发。
新建包目录 org.spring.springboot.webflux.controller并在目录中创建名为 CityWebFluxController 来处理不同的 HTTP Restful 业务请求。代码如下:
import org.spring.springboot.domain.City;
import org.spring.springboot.handler.CityHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping(value = "/city")
public class CityWebFluxController {
@Autowired
private CityHandler cityHandler;
@GetMapping(value = "/{id}")
public Mono<City> findCityById(@PathVariable("id") Long id) {
return cityHandler.findCityById(id);
}
@GetMapping()
public Flux<City> findAllCity() {
return cityHandler.findAllCity();
}
@PostMapping()
public Mono<Long> saveCity(@RequestBody City city) {
return cityHandler.save(city);
}
@PutMapping()
public Mono<Long> modifyCity(@RequestBody City city) {
return cityHandler.modifyCity(city);
}
@DeleteMapping(value = "/{id}")
public Mono<Long> deleteCity(@PathVariable("id") Long id) {
return cityHandler.deleteCity(id);
}
}
这里按照 REST 风格实现接口,那具体什么是 REST?
REST 是属于 Web 自身的一种架构风格,是在 HTTP 1.1 规范下实现的。Representational State Transfer 全称翻译为表现层状态转化。Resource资源。比如 newsfeedRepresentational表现形式比如用 JSON、富文本等State Transfer状态变化。通过 HTTP 动作实现。
理解 REST要明白五个关键要素
资源Resource
资源的表述Representation
状态转移State Transfer
统一接口Uniform Interface
超文本驱动Hypertext Driven
6 个主要特性:
面向资源Resource Oriented
可寻址Addressability
连通性Connectedness
无状态Statelessness
统一接口Uniform Interface
超文本驱动Hypertext Driven
具体这里就不一一展开,详见这里。
请求入参、Filters、重定向、Conversion、formatting 等知识会和以前 MVC 的知识一样,详情见文档。
运行工程
一个 CRUD 的 Spring Boot Webflux 工程就开发完毕了,下面运行工程验证下。使用 IDEA 右侧工具栏,点击 Maven Project Tab点击使用下 Maven 插件的 install 命令,或者使用命令行的形式,在工程根目录下,执行 Maven 清理和安装工程的指令:
cd springboot-webflux-2-restful
mvn clean install
在控制台中看到成功的输出:
... 省略
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01:30 min
[INFO] Finished at: 2017-10-15T10:00:54+08:00
[INFO] Final Memory: 31M/174M
[INFO] ------------------------------------------------------------------------
在 IDEA 中执行 Application 类启动,任意正常模式或者 Debug 模式。可以在控制台看到成功运行的输出:
... 省略
2018-04-10 08:43:39.932 INFO 2052 --- [ctor-http-nio-1] r.ipc.netty.tcp.BlockingNettyContext : Started HttpServer on /0:0:0:0:0:0:0:0:8080
2018-04-10 08:43:39.935 INFO 2052 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8080
2018-04-10 08:43:39.960 INFO 2052 --- [ main] org.spring.springboot.Application : Started Application in 6.547 seconds (JVM running for 9.851)
打开 POST MAN 工具,开发必备。进行下面操作:
新增城市信息 POST http://127.0.0.1:8080/city
获取城市信息列表 GET http://127.0.0.1:8080/city
其他接口就不演示了。
总结
这里,探讨了 Spring WebFlux 的一些功能,构建没有底层数据库的基本 CRUD 工程。为了更好的展示了如何创建 Flux 流,以及如何对其进行操作,下篇内容会讲到如何操作数据存储。

View File

@ -0,0 +1,481 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
04 WebFlux 整合 MongoDB
前言
上一课的内容讲解了用 Map 数据结构内存式存储了数据,这样数据就不会持久化,本文我们用 MongoDB 来实现 WebFlux 对数据源的操作。
什么是 MongoDB详见官网。
MongoDB 是一个基于分布式文件存储的数据库,由 C++ 语言编写,旨在为 Web 应用提供可扩展的高性能数据存储解决方案。
MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库。
由于操作方便,本文用 Docker 启动一个 MognoDB 服务。如果 Docker 不会安装请参考此文Docker 安装与基本操作。
Docker 安装 MognoDB 并启动如下。
1创建挂载目录
docker volume create mongo_data_db
docker volume create mongo_data_configdb
2启动 MognoDB
docker run -d \
--name mongo \
-v mongo_data_configdb:/data/configdb \
-v mongo_data_db:/data/db \
-p 27017:27017 \
mongo \
--auth
3初始化管理员账号
docker exec -it mongo mongo admin
// 容器名 // mongo命令 数据库名
# 创建最高权限用户
db.createUser({ user: 'admin', pwd: 'admin', roles: [ { role: "root", db: "admin" } ] });
4测试连通性
docker run -it --rm --link mongo:mongo mongo mongo -u admin -p admin --authenticationDatabase admin mongo/admin
MognoDB 基本操作
类似 MySQL 命令,显示库列表:
show dbs
使用某数据库:
use admin
显示表列表:
show collections
如果存在 city 表,格式化显示 city 表内容:
db.city.find().pretty()
结构
类似上面讲的工程搭建,新建一个工程编写此案例。工程如图:
目录核心如下:
pom.xml maven 配置;
application.properties 配置文件;
dao 数据访问层,本文要点。
新增 POM 依赖与配置
在 pom.xml 配置新的依赖:
<!-- Spring Boot 响应式 MongoDB 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
类似配了 MySQL 和 JDBC 驱动,肯定得去配置数据库。在 application.properties 配置下上面启动的 MongoDB 配置:
数据库名为 admin账号密码也为 admin。
spring.data.mongodb.host=localhost
spring.data.mongodb.database=admin
spring.data.mongodb.port=27017
spring.data.mongodb.username=admin
spring.data.mongodb.password=admin
这就一个巨大的问题了,为啥不用我们常用的 MySQL 数据库呢?
答案是 Spring Data Reactive Repositories 目前支持 Mongo、Cassandra、Redis、Couchbase。不支持 MySQL那究竟为啥呢那就说明下 JDBC 和 Spring Data 的关系。
Spring Data Reactive Repositories 突出点是 Reactive即非阻塞的。区别如下
基于 JDBC 实现的 Spring Data比如 Spring Data JPA 是阻塞的。原理是基于阻塞 IO 模型 消耗每个调用数据库的线程Connection
事务只能在一个 java.sql.Connection 使用,即一个事务一个操作。
那如何异步非阻塞封装下 JDBC 的思想也不新鲜Scala 库 Slick 3 就实现了。简单的实现原理如下:
一个事务多个操作,那么共享一个 java.sql.Connection可以使用透明事务管理利用回调编程模型去传递。
保持有限的空闲连接。
最后,我坚信非阻塞 JDBC 很快就会出现的,这样我们就开心的调用 MySQL 了。
对象
修改 org.spring.springboot.domain 包里面的城市实体对象类。修改城市City对象 City代码如下
import org.springframework.data.annotation.Id;
/**
* 城市实体类
*
*/
public class City {
/**
* 城市编号
*/
@Id
private Long id;
/**
* 省份编号
*/
private Long provinceId;
/**
* 城市名称
*/
private String cityName;
/**
* 描述
*/
private String description;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getProvinceId() {
return provinceId;
}
public void setProvinceId(Long provinceId) {
this.provinceId = provinceId;
}
public String getCityName() {
return cityName;
}
public void setCityName(String cityName) {
this.cityName = cityName;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
@Id 注解标记对应库表的主键或者唯一标识符。因为这个是我们的 DO数据访问对象一一映射到数据存储。
MongoDB 数据访问层 CityRepository
修改 CityRepository 类,代码如下:
import org.spring.springboot.domain.City;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CityRepository extends ReactiveMongoRepository<City, Long> {
}
CityRepository 接口只要继承 ReactiveMongoRepository 类即可,默认会提供很多实现,比如 CRUD 和列表查询参数相关的实现。ReactiveMongoRepository 接口默认实现了如下:
<S extends T> Mono<S> insert(S var1);
<S extends T> Flux<S> insert(Iterable<S> var1);
<S extends T> Flux<S> insert(Publisher<S> var1);
<S extends T> Flux<S> findAll(Example<S> var1);
<S extends T> Flux<S> findAll(Example<S> var1, Sort var2);
如图ReactiveMongoRepository 的集成类 ReactiveSortingRepository、ReactiveCrudRepository 实现了很多常用的接口:
ReactiveCrudRepository 接口如图所示:
另外可以看出,接口的命名是遵循规范的,常用命名规则如下:
关键字
方法命名
And
findByNameAndPwd
Or
findByNameOrSex
Is
findById
Between
findByIdBetween
Like
findByNameLike
NotLike
findByNameNotLike
OrderBy
findByIdOrderByXDesc
Not
findByNameNot
常用案例,代码如下:
Flux<Person> findByLastname(String lastname);
@Query("{ 'firstname': ?0, 'lastname': ?1}")
Mono<Person> findByFirstnameAndLastname(String firstname, String lastname);
// Accept parameter inside a reactive type for deferred execution
Flux<Person> findByLastname(Mono<String> lastname);
Mono<Person> findByFirstnameAndLastname(Mono<String> firstname, String lastname);
@Tailable // Use a tailable cursor
Flux<Person> findWithTailableCursorBy();
源码层面
ReactiveCrudRepository 抽象在 reactive 包,如图:
这里我们可以看出,支持了 Reactive 还支持了 RxJava。对应老的 CrudRepository 新增了 ReactiveCrudRepository 接口及各种存储实现。
处理器类 Handler 和控制器类 Controller
修改下 Handler代码如下
@Component
public class CityHandler {
private final CityRepository cityRepository;
@Autowired
public CityHandler(CityRepository cityRepository) {
this.cityRepository = cityRepository;
}
public Mono<City> save(City city) {
return cityRepository.save(city);
}
public Mono<City> findCityById(Long id) {
return cityRepository.findById(id);
}
public Flux<City> findAllCity() {
return cityRepository.findAll();
}
public Mono<City> modifyCity(City city) {
return cityRepository.save(city);
}
public Mono<Long> deleteCity(Long id) {
cityRepository.deleteById(id);
return Mono.create(cityMonoSink -> cityMonoSink.success(id));
}
}
不要对 Mono、Flux 陌生,把它当成对象即可。继续修改控制器类 Controller代码如下
@RestController
@RequestMapping(value = "/city")
public class CityWebFluxController {
@Autowired
private CityHandler cityHandler;
@GetMapping(value = "/{id}")
public Mono<City> findCityById(@PathVariable("id") Long id) {
return cityHandler.findCityById(id);
}
@GetMapping()
public Flux<City> findAllCity() {
return cityHandler.findAllCity();
}
@PostMapping()
public Mono<City> saveCity(@RequestBody City city) {
return cityHandler.save(city);
}
@PutMapping()
public Mono<City> modifyCity(@RequestBody City city) {
return cityHandler.modifyCity(city);
}
@DeleteMapping(value = "/{id}")
public Mono<Long> deleteCity(@PathVariable("id") Long id) {
return cityHandler.deleteCity(id);
}
}
运行工程
一个 CRUD 的 Spring Boot WebFlux 工程就开发完毕了,下面运行工程验证下。使用 IDEA 右侧工具栏,单击 Maven Project Tab 选项,点击使用 Maven 插件的 install 命令。或者使用命令行的形式,在工程根目录下,执行 Maven 清理和安装工程的指令:
cd springboot-webflux-3-mongodb
mvn clean install
在控制台中看到成功的输出:
... 省略
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01:30 min
[INFO] Finished at: 2017-10-15T10:00:54+08:00
[INFO] Final Memory: 31M/174M
[INFO] ------------------------------------------------------------------------
在 IDEA 中执行 Application 类启动,任意正常模式或者 Debug 模式,可以在控制台看到成功运行的输出:
... 省略
2018-04-10 08:43:39.932 INFO 2052 --- [ctor-http-nio-1] r.ipc.netty.tcp.BlockingNettyContext : Started HttpServer on /0:0:0:0:0:0:0:0:8080
2018-04-10 08:43:39.935 INFO 2052 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8080
2018-04-10 08:43:39.960 INFO 2052 --- [ main] org.spring.springboot.Application : Started Application in 6.547 seconds (JVM running for 9.851)
打开 POST MAN 工具,开发必备。进行下面操作。
新增城市信息 POST http://127.0.0.1:8080/city。
连接 MongoDB验证数据
连接 MongoDB
docker run -it --rm --link mongo:mongo mongo mongo -u admin -p admin --authenticationDatabase admin mongo/admin
显示库列表:
show dbs
使用某数据库:
use admin
显示表列表:
show collections
如果存在 city 表,格式化显示 city 表内容:
db.city.find().pretty()
总结
这里探讨了 Spring WebFlux 的如何整合 MongoDB整合其他存储 Cassandra、Redis、Couchbase 就大同小异了。下面,我们已经可以整合 Thymeleaf更好的页面展示给大家顺便让大家学习下 Thymeleaf 的基本用法。

View File

@ -0,0 +1,292 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
05 WebFlux 整合 Thymeleaf
上一篇介绍的是用 MongoDB 来实现 WebFlux 对数据源的操作,那么有了数据需要渲染到前台给用户展示,这就是本文关心的 View 层View 的表现形式有很多,比如 JSON 和 HTML。开发中常用模板语言很常见的有 Thymeleaf、Freemarker等那什么是模板语言
常见的模板语言都包含以下几个概念数据Data、模板Template、模板引擎Template Engine和结果文档Result Documents
数据
数据是信息的表现形式和载体,可以是符号、文字、数字、语音、图像、视频等。数据和信息是不可分离的,数据是信息的表达,信息是数据的内涵。数据本身没有意义,数据只有对实体行为产生影响时才成为信息。
模板
模板,是一个蓝图,即一个与类型无关的类。编译器在使用模板时,会根据模板实参对模板进行实例化,得到一个与类型相关的类。
模板引擎
模板引擎(这里特指用于 Web 开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的 HTML 文档。
结果文档
一种特定格式的文档,比如用于网站的模板引擎就会生成一个标准的 HTML 文档。
模板语言用途广泛,常见的用途如下:
页面渲染
文档生成
代码生成
所有 “数据+模板=文本” 的应用场景
Spring Boot 推荐使用的模板语言是 Thymeleaf那什么是 Thymeleaf
官方的解释如下:
Thymeleaf is a modern server-side Java template engine for both web and standalone environments.
Thymeleaf 是现代的模板语言引擎,可以独立运行也可以服务于 Web主要目标是为开发提供天然的模板并且能在 HTML 里面准确的显示。
Thymeleaf 是新一代 Java 模板引擎,在 Spring 4 后推荐使用。目前是 Spring 5 自然更加推荐。
结构
类似上面讲的工程搭建,新建一个工程编写此案例,工程图如图所示:
目录如下:
org.spring.springboot.webflux.controllerController 层
org.spring.springboot.dao数据操作层 DAO
org.spring.springboot.domain实体类
org.spring.springboot.handler业务逻辑层
Application应用启动类
application.properties应用配置文件
pom.xml maven 配置
application.properties 配置文件
模板是会用到下面两个目录:
static 目录是存放 CSS、JS 等资源文件;
templates 目录是存放视图。
本文重点在 Controller 层 和 templates 视图的编写。
新增 POM 依赖与配置
在 pom.xml 配置新的依赖:
<dependencies>
<!-- Spring Boot Web Flux 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- 模板引擎 Thymeleaf 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Spring Boot Test 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
这里我们增加了 Thymeleaf 依赖,但不用在 application.properties - 应用配置文件中配置任何配置。默认启动其默认配置,如需修改配置参考 Thymeleaf 依赖配置,如下:
spring.thymeleaf.cache=true # Enable template caching.
spring.thymeleaf.check-template=true # Check that the template exists before rendering it.
spring.thymeleaf.check-template-location=true # Check that the templates location exists.
spring.thymeleaf.enabled=true # Enable Thymeleaf view resolution for Web frameworks.
spring.thymeleaf.encoding=UTF-8 # Template files encoding.
spring.thymeleaf.excluded-view-names= # Comma-separated list of view names that should be excluded from resolution.
spring.thymeleaf.mode=HTML5 # Template mode to be applied to templates. See also StandardTemplateModeHandlers.
spring.thymeleaf.prefix=classpath:/templates/ # Prefix that gets prepended to view names when building a URL.
spring.thymeleaf.reactive.max-chunk-size= # Maximum size of data buffers used for writing to the response, in bytes.
spring.thymeleaf.reactive.media-types= # Media types supported by the view technology.
spring.thymeleaf.servlet.content-type=text/html # Content-Type value written to HTTP responses.
spring.thymeleaf.suffix=.html # Suffix that gets appended to view names when building a URL.
spring.thymeleaf.template-resolver-order= # Order of the template resolver in the chain.
spring.thymeleaf.view-names= # Comma-separated list of view names that can be resolved.
包括常用的编码、是否开启缓存等等。
WebFlux 中使用 Thymeleaf
在 CityWebFluxController 控制层,添加两个方法如下:
@GetMapping("/hello")
public Mono<String> hello(final Model model) {
model.addAttribute("name", "泥瓦匠");
model.addAttribute("city", "浙江温岭");
String path = "hello";
return Mono.create(monoSink -> monoSink.success(path));
}
private static final String CITY_LIST_PATH_NAME = "cityList";
@GetMapping("/page/list")
public String listPage(final Model model) {
final Flux<City> cityFluxList = cityHandler.findAllCity();
model.addAttribute("cityList", cityFluxList);
return CITY_LIST_PATH_NAME;
}
解释下语法:
返回值 Mono 或者 String 都行,但是 Mono 代表着我这个返回 View 也是回调的。
return 字符串,该字符串对应的目录在 resources/templates 下的模板名字。
Model 对象来进行数据绑定到视图。
一般会集中用常量管理模板视图的路径。
Tymeleaf 视图
然后编写两个视图 hello 和 cityList代码分别如下。
hello.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"/>
<title>欢迎页面</title>
</head>
<body>
<h1 >你好,欢迎来自<p th:text="${city}"></p><p th:text="${name}"></p></h1>
</body>
</html>
cityList.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"/>
<title>城市列表</title>
</head>
<body>
<div>
<table>
<legend>
<strong>城市列表</strong>
</legend>
<thead>
<tr>
<th>城市编号</th>
<th>省份编号</th>
<th>名称</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr th:each="city : ${cityList}">
<td th:text="${city.id}"></td>
<td th:text="${city.provinceId}"></td>
<td th:text="${city.cityName}"></td>
<td th:text="${city.description}"></td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
常用语法糖如下:
${…}:变量表达式;
th:text处理 Tymeleaf 表达式;
th:each遍历表达式可遍历的对象有实现 java.util.Iterable、java.util.Map遍历时取 java.util.Map.Entry、array 等。
还有很多使用,可以参考官方文档。
运行工程
下面运行工程验证下,使用 IDEA 右侧工具栏,点击 Maven Project Tab ,点击使用下 Maven 插件的 install 命令;或者使用命令行的形式,在工程根目录下,执行 Maven 清理和安装工程的指令:
cd springboot-webflux-4-thymeleaf
mvn clean install
在控制台中看到成功的输出:
... 省略
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01:30 min
[INFO] Finished at: 2017-10-15T10:00:54+08:00
[INFO] Final Memory: 31M/174M
[INFO] ------------------------------------------------------------------------
在 IDEA 中执行 Application 类启动,任意正常模式或者 Debug 模式,可以在控制台看到成功运行的输出:
... 省略
2018-04-10 08:43:39.932 INFO 2052 --- [ctor-http-nio-1] r.ipc.netty.tcp.BlockingNettyContext : Started HttpServer on /0:0:0:0:0:0:0:0:8080
2018-04-10 08:43:39.935 INFO 2052 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8080
2018-04-10 08:43:39.960 INFO 2052 --- [ main] org.spring.springboot.Application : Started Application in 6.547 seconds (JVM running for 9.851)
打开浏览器,访问 http://localhost:8080/city/hello ,可以看到如图的响应:
继续访问 http://localhost:8080/city/page/list , 发现没有值,那么按照上一讲插入几条数据即可有值,如图:
总结
这里探讨了 Spring WebFlux 的如何整合 Thymeleaf整合其他模板语言 Thymeleaf、Freemarker就大同小异了。下面我们可以整合 Thymeleaf 和 MongoBD 来实现一个整体的简单案例。

View File

@ -0,0 +1,442 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
06 WebFlux 中 Thymeleaf 和 MongoDB 实践
前言
本节内容主要还是总结上面两篇内容的操作,并实现一个复杂查询的小案例,那么没安装 MongoDB 的可以进行下面的安装流程。
Docker 安装 MognoDB 并启动如下。
1创建挂载目录
docker volume create mongo_data_db
docker volume create mongo_data_configdb
2启动 MognoDB
docker run -d \
--name mongo \
-v mongo_data_configdb:/data/configdb \
-v mongo_data_db:/data/db \
-p 27017:27017 \
mongo \
--auth
3初始化管理员账号
docker exec -it mongo mongo admin
// 容器名 // mongo命令 数据库名
# 创建最高权限用户
db.createUser({ user: 'admin', pwd: 'admin', roles: [ { role: "root", db: "admin" } ] });
4测试连通性
docker run -it --rm --link mongo:mongo mongo mongo -u admin -p admin --authenticationDatabase admin mongo/admin
MognoDB 基本操作
类似 MySQL 命令,显示库列表:
show dbs
使用某数据库:
use admin
显示表列表:
show collections
如果存在 city 表,格式化显示 city 表内容:
db.city.find().pretty()
如果已经安装后,只要重启即可。
查看已有的镜像:
docker images
然后 docker start mogno 即可Mongo 是镜像唯一名词。
结构
类似上面讲的工程搭建,新建一个工程编写此案例,工程如图:
核心目录如下:
pom.xml Maven 依赖配置
application.properties 配置文件,配置 mongo 连接属性配置
dao 数据访问层
controller 展示层实现
新增 POM 依赖与配置
在 pom.xml 配置新的依赖:
<!-- Spring Boot 响应式 MongoDB 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
<!-- 模板引擎 Thymeleaf 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
类似配了 MySQL 和 JDBC 驱动,肯定得去配置数据库。在 application.properties 配置中启动 MongoDB 配置。
数据库名为 admin账号密码也为 admin。
spring.data.mongodb.host=localhost
spring.data.mongodb.database=admin
spring.data.mongodb.port=27017
spring.data.mongodb.username=admin
spring.data.mongodb.password=admin
MongoDB 数据访问层 CityRepository
修改 CityRepository 类,代码如下:
import org.spring.springboot.domain.City;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface CityRepository extends ReactiveMongoRepository<City, Long> {
Mono<City> findByCityName(String cityName);
}
CityRepository 接口只要继承 ReactiveMongoRepository 类即可。
这里实现了通过城市名找出唯一的城市对象方法:
Mono<City> findByCityName(String cityName);
复杂查询语句实现也很简单,只要依照接口实现规范,即可实现对应 MySQL 的 where 查询语句。这里 findByxxx 的 xxx 可以映射任何字段,包括主键等。
接口的命名是遵循规范的,常用命名规则如下:
关键字
方法命名
And
findByNameAndPwd
Or
findByNameOrSex
Is
findById
Between
findByIdBetween
Like
findByNameLike
NotLike
findByNameNotLike
OrderBy
findByIdOrderByXDesc
Not
findByNameNot
处理器类 Handler 和控制器类 Controller
修改下 Handler代码如下
@Component
public class CityHandler {
private final CityRepository cityRepository;
@Autowired
public CityHandler(CityRepository cityRepository) {
this.cityRepository = cityRepository;
}
public Mono<City> save(City city) {
return cityRepository.save(city);
}
public Mono<City> findCityById(Long id) {
return cityRepository.findById(id);
}
public Flux<City> findAllCity() {
return cityRepository.findAll();
}
public Mono<City> modifyCity(City city) {
return cityRepository.save(city);
}
public Mono<Long> deleteCity(Long id) {
cityRepository.deleteById(id);
return Mono.create(cityMonoSink -> cityMonoSink.success(id));
}
public Mono<City> getByCityName(String cityName) {
return cityRepository.findByCityName(cityName);
}
}
新增对应的方法,直接返回 Mono 对象,不需要对 Mono 进行转换,因为 Mono 本身是个对象,可以被 View 层渲染。继续修改控制器类 Controller代码如下
@Autowired
private CityHandler cityHandler;
@GetMapping(value = "/{id}")
@ResponseBody
public Mono<City> findCityById(@PathVariable("id") Long id) {
return cityHandler.findCityById(id);
}
@GetMapping()
@ResponseBody
public Flux<City> findAllCity() {
return cityHandler.findAllCity();
}
@PostMapping()
@ResponseBody
public Mono<City> saveCity(@RequestBody City city) {
return cityHandler.save(city);
}
@PutMapping()
@ResponseBody
public Mono<City> modifyCity(@RequestBody City city) {
return cityHandler.modifyCity(city);
}
@DeleteMapping(value = "/{id}")
@ResponseBody
public Mono<Long> deleteCity(@PathVariable("id") Long id) {
return cityHandler.deleteCity(id);
}
private static final String CITY_LIST_PATH_NAME = "cityList";
private static final String CITY_PATH_NAME = "city";
@GetMapping("/page/list")
public String listPage(final Model model) {
final Flux<City> cityFluxList = cityHandler.findAllCity();
model.addAttribute("cityList", cityFluxList);
return CITY_LIST_PATH_NAME;
}
@GetMapping("/getByName")
public String getByCityName(final Model model,
@RequestParam("cityName") String cityName) {
final Mono<City> city = cityHandler.getByCityName(cityName);
model.addAttribute("city", city);
return CITY_PATH_NAME;
}
新增 getByName 路径,指向了新的页面 city。使用 @RequestParam 接收 GET 请求入参,接收的参数为 cityName城市名称。视图返回值 Mono 或者 String 都行。
Tymeleaf 视图
然后编写两个视图 city 和 cityList代码分别如下。
city.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"/>
<title>城市</title>
</head>
<body>
<div>
<table>
<legend>
<strong>城市单个查询</strong>
</legend>
<tbody>
<td th:text="${city.id}"></td>
<td th:text="${city.provinceId}"></td>
<td th:text="${city.cityName}"></td>
<td th:text="${city.description}"></td>
</tbody>
</table>
</div>
</body>
</html>
cityList.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"/>
<title>城市列表</title>
</head>
<body>
<div>
<table>
<legend>
<strong>城市列表</strong>
</legend>
<thead>
<tr>
<th>城市编号</th>
<th>省份编号</th>
<th>名称</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr th:each="city : ${cityList}">
<td th:text="${city.id}"></td>
<td th:text="${city.provinceId}"></td>
<td th:text="${city.cityName}"></td>
<td th:text="${city.description}"></td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
运行工程
一个 CRUD 的 Spring Boot Webflux 工程就开发完毕了,下面运行工程验证一下。使用 IDEA 右侧工具栏,单击 Maven Project Tab 按钮,然后单击使用下 Maven 插件的 install 命令;或者使用命令行的形式,在工程根目录下,执行 Maven 清理和安装工程的指令:
cd springboot-webflux-5-thymeleaf-mongodb
mvn clean install
在控制台中看到成功的输出:
... 省略
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01:30 min
[INFO] Finished at: 2017-10-15T10:00:54+08:00
[INFO] Final Memory: 31M/174M
[INFO] ------------------------------------------------------------------------
在 IDEA 中执行 Application 类启动,任意正常模式或者 Debug 模式。可以在控制台看到成功运行的输出:
... 省略
2018-04-10 08:43:39.932 INFO 2052 --- [ctor-http-nio-1] r.ipc.netty.tcp.BlockingNettyContext : Started HttpServer on /0:0:0:0:0:0:0:0:8080
2018-04-10 08:43:39.935 INFO 2052 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8080
2018-04-10 08:43:39.960 INFO 2052 --- [ main] org.spring.springboot.Application : Started Application in 6.547 seconds (JVM running for 9.851)
打开 POST MAN 工具,开发必备,进行下面操作。
新增城市信息 POST http://127.0.0.1:8080/city
打开浏览器,访问 http://localhost:8080/city/getByName?cityName=杭州,可以看到如图的响应:
继续访问 http://localhost:8080/city/page/list发现没有值那么按照上一篇的内容插入几条数据即可有值如图
总结
这里初步实现了一个简单的整合,具体复杂的案例我们在后面的综合案例中实现,会很酷炫。下面整合 Redis基于 Redis 可以实现常用的缓存、锁,下一篇我们将学习如何整合 Reids。

View File

@ -0,0 +1,281 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
07 WebFlux 整合 Redis
前言
上一篇内容讲了如何整合 MongoDB这里继续讲如何操作 Redis 这个数据源,那什么是 Reids
Redis 是一个高性能的 key-value 数据库GitHub 地址详见这里。GitHub 是这么描述的:
Redis is an in-memory database that persists on disk. The data model is key-value, but many different kind of values are supported: Strings, Lists, Sets, Sorted Sets, Hashes, HyperLogLogs, Bitmaps.
Redis 是内存式数据库存储在磁盘支持的数据类型很多Strings、Lists、Sets、Sorted Sets、Hashes、HyperLogLogs、Bitmaps 等。
安装简易教程(适用 Mac/Linux
下载并解压:
下载安装包 redis-x.x.x.tar.gz
## 解压
tar zxvf redis-2.8.17.tar.gz
编译安装:
cd redis-x.x.x/
make ## 编译
启动 Redis
cd src/
redis-server
如果需要运行在守护进程,设置 daemonize 从 no 修改成 yes并指定运行redis-server redis.conf。
结构
类似上面讲的工程搭建,新建一个工程编写此案例,工程如图:
目录核心如下:
pom.xml maven 配置
application.properties 配置文件
domain 实体类
controller 控制层,本文要点
新增 POM 依赖与配置
在 pom.xml 配置新的依赖:
<!-- Spring Boot 响应式 Redis 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
类似 MongoDB 配置,在 application.properties 配置连接 Redis
## Redis 配置
## Redis服务器地址
spring.redis.host=127.0.0.1
## Redis服务器连接端口
spring.redis.port=6379
## Redis服务器连接密码默认为空
spring.redis.password=
# 连接超时时间(毫秒)
spring.redis.timeout=5000
默认 密码为空,这里注意的是连接超时时间不能太少或者为 0不然会引起异常 RedisCommandTimeoutException: Command timed out。
对象
修改 org.spring.springboot.domain 包里面的城市实体对象类城市City对象 City代码如下
import org.springframework.data.annotation.Id;
import java.io.Serializable;
/**
* 城市实体类
*
*/
public class City implements Serializable {
private static final long serialVersionUID = -2081742442561524068L;
/**
* 城市编号
*/
@Id
private Long id;
/**
* 省份编号
*/
private Long provinceId;
/**
* 城市名称
*/
private String cityName;
/**
* 描述
*/
private String description;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getProvinceId() {
return provinceId;
}
public void setProvinceId(Long provinceId) {
this.provinceId = provinceId;
}
public String getCityName() {
return cityName;
}
public void setCityName(String cityName) {
this.cityName = cityName;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
值得注意点:
@Id 注解标记对应库表的主键或者唯一标识符。因为这个是我们的 DO数据访问对象一一映射到数据存储。
City 必须实现序列化,因为需要将对象序列化后存储到 Redis。如果没实现 Serializable会引出异常java.lang.IllegalArgumentException: DefaultSerializer requires a Serializable payload but received an object of type。
如果不是用默认的序列化,需要自定义序列化实现,只要实现 RedisSerializer 接口去实现即可,然后在使用 RedisTemplate.setValueSerializer 方法去设置你实现的序列化实现,支持 JSON、XML 等。
控制层 CityWebFluxController
代码如下:
import org.spring.springboot.domain.City;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping(value = "/city")
public class CityWebFluxController {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping(value = "/{id}")
public Mono<City> findCityById(@PathVariable("id") Long id) {
String key = "city_" + id;
ValueOperations<String, City> operations = redisTemplate.opsForValue();
boolean hasKey = redisTemplate.hasKey(key);
City city = operations.get(key);
if (!hasKey) {
return Mono.create(monoSink -> monoSink.success(null));
}
return Mono.create(monoSink -> monoSink.success(city));
}
@PostMapping()
public Mono<City> saveCity(@RequestBody City city) {
String key = "city_" + city.getId();
ValueOperations<String, City> operations = redisTemplate.opsForValue();
operations.set(key, city, 60, TimeUnit.SECONDS);
return Mono.create(monoSink -> monoSink.success(city));
}
@DeleteMapping(value = "/{id}")
public Mono<Long> deleteCity(@PathVariable("id") Long id) {
String key = "city_" + id;
boolean hasKey = redisTemplate.hasKey(key);
if (hasKey) {
redisTemplate.delete(key);
}
return Mono.create(monoSink -> monoSink.success(id));
}
}
代码详解:
使用 @Autowired 注入 RedisTemplate 对象,这个对象和 Spring 的 JdbcTemplate 功能十分相似RedisTemplate 封装了 RedisConnection具有连接管理、序列化和各个操作等还有针对 String 的支持对象 StringRedisTemplate。
删除 Redis 某对象,直接通过 key 值调用 delete(key)。
Redis 操作视图接口类用的是 ValueOperations对应的是 Redis String/Value 操作get 是获取数据set 是插入数据,可以设置失效时间,这里设置的失效时间是 60 s。
还有其他的操作视图ListOperations、SetOperations、ZSetOperations 和 HashOperations。
运行工程
一个操作 Redis 工程就开发完毕了,下面运行工程验证一下,使用 IDEA 右侧工具栏,单击 Maven Project Tab单击使用下 Maven 插件的 install 命令。或者使用命令行的形式,在工程根目录下,执行 Maven 清理和安装工程的指令:
cd springboot-webflux-6-redis
mvn clean install
在控制台中看到成功的输出:
... 省略
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01:30 min
[INFO] Finished at: 2018-10-15T10:00:54+08:00
[INFO] Final Memory: 31M/174M
[INFO] ------------------------------------------------------------------------
在 IDEA 中执行 Application 类启动,任意正常模式或者 Debug 模式,可以在控制台看到成功运行的输出:
... 省略
2018-04-10 08:43:39.932 INFO 2052 --- [ctor-http-nio-1] r.ipc.netty.tcp.BlockingNettyContext : Started HttpServer on /0:0:0:0:0:0:0:0:8080
2018-04-10 08:43:39.935 INFO 2052 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8080
2018-04-10 08:43:39.960 INFO 2052 --- [ main] org.spring.springboot.Application : Started Application in 6.547 seconds (JVM running for 9.851)
打开 POST MAN 工具,开发必备。进行下面操作:
新增城市信息 POST http://127.0.0.1:8080/city
获取城市信息 GET http://127.0.0.1:8080/city/2
如果等待 60s 以后,再次则会获取为空,因为保存的时候设置了失效时间是 60 s。
总结
这里探讨了 Spring WebFlux 的如何整合 Redis介绍了如何通过 RedisTemplate 去操作 Redis。因为 Redis 在获取资源性能极佳,常用 Redis 作为缓存存储对象,下面我们利用 Reids 实现缓存操作。

View File

@ -0,0 +1,294 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
08 WebFlux 中 Redis 实现缓存
前言
首先补充下上一篇的内容RedisTemplate 实现操作 Redis但操作是同步的不是 Reactive 的。自然,支持 Reactive 的操作类为 ReactiveRedisTemplate下面我们写个小案例。
ReactiveRedisTemplate
在上一篇工程中,新建 CityWebFluxReactiveController 类,路由为 /city2 开头。
@RestController
@RequestMapping(value = "/city2")
public class CityWebFluxReactiveController {
@Autowired
private ReactiveRedisTemplate reactiveRedisTemplate;
@GetMapping(value = "/{id}")
public Mono<City> findCityById(@PathVariable("id") Long id) {
String key = "city_" + id;
ReactiveValueOperations<String, City> operations = reactiveRedisTemplate.opsForValue();
Mono<City> city = operations.get(key);
return city;
}
@PostMapping
public Mono<City> saveCity(@RequestBody City city) {
String key = "city_" + city.getId();
ReactiveValueOperations<String, City> operations = reactiveRedisTemplate.opsForValue();
return operations.getAndSet(key, city);
}
@DeleteMapping(value = "/{id}")
public Mono<Long> deleteCity(@PathVariable("id") Long id) {
String key = "city_" + id;
return reactiveRedisTemplate.delete(key);
}
}
写法和以前保持一致,@Autowired 注入 ReactiveRedisTemplate 对象。
ReactiveValueOperations 是 String或 value的操作视图操作视图还有 ReactiveHashOperations、ReactiveListOperations、ReactiveSetOperations 和 ReactiveZSetOperations 等。
不一样的是,操作视图 set 方法是操作 City 对象,但可以 get 回 Mono 或者 Flux 对象。
结构
回到这个工程,新建一个工程编写整合 Redis 实现缓存案例,工程如图:
目录核心如下:
pom.xml maven 配置
application.properties 配置文件
domain 实体类
dao mongodb数据操作层
handler 业务层,本文要点
controller 控制层
单击这里查看源代码。
控制层 CityWebFluxController
代码如下:
@RestController
@RequestMapping(value = "/city")
public class CityWebFluxController {
@Autowired
private CityHandler cityHandler;
@GetMapping(value = "/{id}")
public Mono<City> findCityById(@PathVariable("id") Long id) {
return cityHandler.findCityById(id);
}
@GetMapping()
public Flux<City> findAllCity() {
return cityHandler.findAllCity();
}
@PostMapping()
public Mono<City> saveCity(@RequestBody City city) {
return cityHandler.save(city);
}
@PutMapping()
public Mono<City> modifyCity(@RequestBody City city) {
return cityHandler.modifyCity(city);
}
@DeleteMapping(value = "/{id}")
public Mono<Long> deleteCity(@PathVariable("id") Long id) {
return cityHandler.deleteCity(id);
}
}
CityHandler 业务层
目前,@Cacheable 等注解形式实现缓存没有很好的集成,二者 Mono / Flux 对象没有实现 Serializable无法通过默认序列化器解决方式是需要自定义序列化这里通过手动方式与 Redis 手动集成,并实现缓存策略。
参考《缓存更新的套路》缓存更新的模式有四种Cache aside、Read through、Write through、Write behind caching。
这里使用的是 Cache Aside 策略,从三个维度(摘自耗子叔叔博客):
失效:应用程序先从 Cache 取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
命中:应用程序从 Cache 中取数据,取到后返回。
更新:先把数据存到数据库中,成功后,再让缓存失效。
代码如下:
@Component
public class CityHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(CityHandler.class);
@Autowired
private RedisTemplate redisTemplate;
private final CityRepository cityRepository;
@Autowired
public CityHandler(CityRepository cityRepository) {
this.cityRepository = cityRepository;
}
public Mono<City> save(City city) {
return cityRepository.save(city);
}
public Mono<City> findCityById(Long id) {
// 从缓存中获取城市信息
String key = "city_" + id;
ValueOperations<String, City> operations = redisTemplate.opsForValue();
// 缓存存在
boolean hasKey = redisTemplate.hasKey(key);
if (hasKey) {
City city = operations.get(key);
LOGGER.info("CityHandler.findCityById() : 从缓存中获取了城市 >> " + city.toString());
return Mono.create(cityMonoSink -> cityMonoSink.success(city));
}
// 从 MongoDB 中获取城市信息
Mono<City> cityMono = cityRepository.findById(id);
if (cityMono == null)
return cityMono;
// 插入缓存
cityMono.subscribe(cityObj -> {
operations.set(key, cityObj);
LOGGER.info("CityHandler.findCityById() : 城市插入缓存 >> " + cityObj.toString());
});
return cityMono;
}
public Flux<City> findAllCity() {
return cityRepository.findAll().cache();
}
public Mono<City> modifyCity(City city) {
Mono<City> cityMono = cityRepository.save(city);
// 缓存存在,删除缓存
String key = "city_" + city.getId();
boolean hasKey = redisTemplate.hasKey(key);
if (hasKey) {
redisTemplate.delete(key);
LOGGER.info("CityHandler.modifyCity() : 从缓存中删除城市 ID >> " + city.getId());
}
return cityMono;
}
public Mono<Long> deleteCity(Long id) {
cityRepository.deleteById(id);
// 缓存存在,删除缓存
String key = "city_" + id;
boolean hasKey = redisTemplate.hasKey(key);
if (hasKey) {
redisTemplate.delete(key);
LOGGER.info("CityHandler.deleteCity() : 从缓存中删除城市 ID >> " + id);
}
return Mono.create(cityMonoSink -> cityMonoSink.success(id));
}
}
首先这里注入了 RedisTemplate 对象,联想到 Spring 的 JdbcTemplate RedisTemplate 封装了 RedisConnection具有连接管理序列化和 Redis 操作等功能,还有针对 String 的支持对象 StringRedisTemplate。
回到更新缓存的逻辑。
a. findCityById 获取城市逻辑:
如果缓存存在,从缓存中获取城市信息;
如果缓存不存在,从 DB 中获取城市信息,然后插入缓存。
b. deleteCity 删除 / modifyCity 更新城市逻辑:
如果缓存存在,删除;
如果缓存不存在,不操作。
运行工程
一个操作 Redis 工程就开发完毕了,下面运行工程验证下。使用 IDEA 右侧工具栏,点击 Maven Project Tab点击使用下 Maven 插件的 install 命令;或者使用命令行的形式,在工程根目录下,执行 Maven 清理和安装工程的指令:
cd springboot-webflux-7-redis-cache
mvn clean install
在控制台中看到成功的输出:
... 省略
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01:30 min
[INFO] Finished at: 2018-10-15T10:00:54+08:00
[INFO] Final Memory: 31M/174M
[INFO] ------------------------------------------------------------------------
在 IDEA 中执行 Application 类启动,任意正常模式或者 Debug 模式,可以在控制台看到成功运行的输出:
... 省略
2018-04-10 08:43:39.932 INFO 2052 --- [ctor-http-nio-1] r.ipc.netty.tcp.BlockingNettyContext : Started HttpServer on /0:0:0:0:0:0:0:0:8080
2018-04-10 08:43:39.935 INFO 2052 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8080
2018-04-10 08:43:39.960 INFO 2052 --- [ main] org.spring.springboot.Application : Started Application in 6.547 seconds (JVM running for 9.851)
打开 POST MAN 工具,开发必备。进行下面操作:
新增城市信息 POST http://127.0.0.1:8080/city
获取城市信息 GET http://127.0.0.1:8080/city/2
再请求一次,获取城市信息会发现数据获取的耗时快了很多,服务端 Console 输出的日志:
2017-04-13 18:29:00.273 INFO 13038 --- [nio-8080-exec-1] findCityById() : 城市插入缓存 >> City{id=12, provinceId=3, cityName='三亚', description='水好,天蓝'}
2017-04-13 18:29:03.145 INFO 13038 --- [nio-8080-exec-2] findCityById() : 从缓存中获取了城市 >> City{id=12, provinceId=3, cityName='三亚', description='水好,天蓝'}
可见,第一次是从数据库 MongoDB 获取数据,并插入缓存,第二次直接从缓存中取。
更新 / 删除城市信息,这两种操作中,如果缓存有对应的数据,则删除缓存。服务端 Console 输出的日志:
2017-04-13 18:29:52.248 INFO 13038 --- [nio-8080-exec-9] deleteCity() : 从缓存中删除城市 ID >> 12
总结
这一讲,主要补充了 Redis 对响应式的支持操作,以及缓存更新策略及实际应用小例子。

View File

@ -0,0 +1,222 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
09 WebFlux 中 WebSocket 实现通信
前言
WebFlux 该模块中包含了对反应式 HTTP、服务器推送事件和 WebSocket 的客户端和服务器端的支持。这里我们简单实践下 WebFlux 中 WebSocket 实现通信。
什么是 WebSocket
WebSocket 是一种通信协议,类比下 HTTP 协议HTTP 协议只能有客户端发起请求,然后得到响应。 一般通过 HTTP 的轮询方式,实现 WebSocket 类似功能。
因为轮询每次新建连接请求响应浪费资源。WebSocket 就出现了,它支持客户端和服务端双向通讯。类似 http 和 httpsWebSocket 的标识符为 ws 和 wss案例地址为
ws://localhost:8080/echo
结构
回到这个工程,新建一个工程编写 WebSocket 实现通信案例。工程如图:
目录核心如下:
EchoHandler websocket 处理类(类似 HTTP Servlet 处理)
WebSocketConfiguration websocket 配置类
websocket-client.html HTML 客户端实现
WSClient java 客户端实现
单击这里查看源代码。
EchoHandler 处理类
代码如下:
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.WebSocketSession;
import reactor.core.publisher.Mono;
@Component
public class EchoHandler implements WebSocketHandler {
@Override
public Mono<Void> handle(final WebSocketSession session) {
return session.send(
session.receive()
.map(msg -> session.textMessage(
"服务端返回:小明, -> " + msg.getPayloadAsText())));
}
}
代码详解:
WebSocketHandler 接口,实现该接口来处理 WebSokcet 消息。
handle(WebSocketSession session) 方法,接收 WebSocketSession 对象,即获取客户端信息、发送消息和接收消息的操作对象。
receive() 方法,接收消息,使用 map 操作获取的 Flux 中包含的消息持续处理,并拼接出返回消息 Flux 对象。
send() 方法,发送消息。消息为“服务端返回:小明, -> ”开头的。
WebSocketConfiguration 配置类
代码如下:
@Configuration
public class WebSocketConfiguration {
@Autowired
@Bean
public HandlerMapping webSocketMapping(final EchoHandler echoHandler) {
final Map<String, WebSocketHandler> map = new HashMap<>();
map.put("/echo", echoHandler);
final SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setOrder(Ordered.HIGHEST_PRECEDENCE);
mapping.setUrlMap(map);
return mapping;
}
@Bean
public WebSocketHandlerAdapter handlerAdapter() {
return new WebSocketHandlerAdapter();
}
}
代码详解:
WebSocketHandlerAdapter 负责将 EchoHandler 处理类适配到 WebFlux 容器中;
SimpleUrlHandlerMapping 指定了 WebSocket 的路由配置;
使用 map 指定 WebSocket 协议的路由,路由为 ws://localhost:8080/echo。
运行工程
一个操作 Redis 工程就开发完毕了,下面运行工程验证下。使用 IDEA 右侧工具栏,点击 Maven Project Tab点击使用下 Maven 插件的 install 命令。或者使用命令行的形式,在工程根目录下,执行 Maven 清理和安装工程的指令:
cd springboot-webflux-8-websocket
mvn clean install
在控制台中看到成功的输出:
... 省略
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01:30 min
[INFO] Finished at: 2018-10-15T10:00:54+08:00
[INFO] Final Memory: 31M/174M
[INFO] ------------------------------------------------------------------------
在 IDEA 中执行 Application 类启动,任意正常模式或者 Debug 模式,可以在控制台看到成功运行的输出:
... 省略
2018-04-10 08:43:39.932 INFO 2052 --- [ctor-http-nio-1] r.ipc.netty.tcp.BlockingNettyContext : Started HttpServer on /0:0:0:0:0:0:0:0:8080
2018-04-10 08:43:39.935 INFO 2052 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8080
2018-04-10 08:43:39.960 INFO 2052 --- [ main] org.spring.springboot.Application : Started Application in 6.547 seconds (JVM running for 9.851)
打开 https://www.websocket.org/echo.html网页大多数浏览器是支持 WebSokcet 协议的。
Location - 输入通信地址、点击 Conect 会出现 CONNECTED。
然后发送消息,可以看到服务端返回对应的消息。如果此时关闭了服务端,那么会出现 DISCONNECTED
websocket-client.html HTML 客户端实现
实现 HTML 客户端:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Client WebSocket</title>
</head>
<body>
<div class="chat"></div>
<script>
var clientWebSocket = new WebSocket("ws://localhost:8080/echo");
clientWebSocket.onopen = function () {
console.log("clientWebSocket.onopen", clientWebSocket);
console.log("clientWebSocket.readyState", "websocketstatus");
clientWebSocket.send("你好!");
}
clientWebSocket.onclose = function (error) {
console.log("clientWebSocket.onclose", clientWebSocket, error);
events("聊天会话关闭!");
}
function events(responseEvent) {
document.querySelector(".chat").innerHTML += responseEvent + "<br>";
}
</script>
</body>
</html>
大多数浏览器是支持 WebSocket代码详解如下
网页打开是,会调用 onopen 方法,并发送消息给服务端“你好!”;
如果服务端关闭,会调用 onclose 方法,页面会出现“聊天会话关闭!”信息。
WSClient Java 客户端实现
类似HTTPClient 调用 HTTPWebSocket 客户端去调用 WebSokcet 协议,并实现服务。代码如下:
public class WSClient {
public static void main(final String[] args) {
final WebSocketClient client = new ReactorNettyWebSocketClient();
client.execute(URI.create("ws://localhost:8080/echo"), session ->
session.send(Flux.just(session.textMessage("你好")))
.thenMany(session.receive().take(1).map(WebSocketMessage::getPayloadAsText))
.doOnNext(System.out::println)
.then())
.block(Duration.ofMillis(5000));
}
}
代码详解:
ReactorNettyWebSocketClient 是 WebFlux 默认 Reactor Netty 库提供的 WebSocketClient 实现。
execute 方法,与 ws://localhost:8080/echo 建立 WebSokcet 协议连接。
execute 需要传入 WebSocketHandler 的对象,用来处理消息,这里的实现和前面的 EchoHandler 类似。
通过 WebSocketSession 的 send 方法来发送字符串“你好”到服务器端,然后通过 receive 方法来等待服务器端的响应并输出。
总结
这一篇内容主要一起实践了简单的 WebSocket 的应用操作,以及 WebSocket 客户端小例子。
工程springboot-webflux-8-websocket

View File

@ -0,0 +1,218 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
10 WebFlux 集成测试及部署
前言
在日常工作中,免不了自测 UT因为覆盖率不达标是不允许提交测试那怎么进行 WebFlux 项目的测试呢。@WebFluxTest 是 WebFlux 测试的重要注解。
结构
回到这个工程中,使用 springboot-webflux-3-mongodb 工程,工程如图:
目录核心如下:
pom.xml 添加 Test 相关依赖;
test / CityWebFluxControllerTest WebFlux API 测试类;
代码 GiHub 详见这里。
POM 依赖
pom.xml 添加对应的测试依赖:
<!-- Spring Boot Test 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
CityWebFluxControllerTest WebFlux API 测试类
@WebFluxTest 用于测试 Spring WebFlux 控制器,支持自动配置 Spring WebFlux 基础组件,可以限制扫描范围等。
代码如下:
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CityWebFluxControllerTest {
@Autowired
private WebTestClient webClient;
private static Map<String, City> cityMap = new HashMap<>();
@BeforeClass
public static void setup() throws Exception {
City wl = new City();
wl.setId(1L);
wl.setProvinceId(2L);
wl.setCityName("WL");
wl.setDescription("WL IS GOOD");
cityMap.put("WL", wl);
}
@Test
public void testSave() throws Exception {
City expectCity = webClient.post().uri("/city")
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromObject(cityMap.get("WL")))
.exchange()
.expectStatus().isOk()
.expectBody(City.class).returnResult().getResponseBody();
Assert.assertNotNull(expectCity);
Assert.assertEquals(expectCity.getId(), cityMap.get("WL").getId());
Assert.assertEquals(expectCity.getCityName(), cityMap.get("WL").getCityName());
}
}
代码详解:
@WebFluxTest 注入了 WebTestClient 对象,用于测试 WebFlux 控制器,好处是快速,并无需启动完整 HTTP 容器。
WebTestClient.post() 方法构造了 POST 测试请求,并使用 uri 指定路由。
expectStatus() 用于验证返回状态是否为 ok(),即 200 返回码。
expectBody(City.class) 用于验证返回对象体是为 City 对象,并利用 returnResult 获取对象。
Assert 是以前我们常用的断言方法验证测试结果。
运行 Test得到如图验证结果
工程运行方式
了解工程服务器部署,先了解工程如何运行。
上面使用应用启动类运行工程这是其中一种工程运行方式。Spring Boot 应用的运行方式很简单,下面介绍下这三种运行方式。
1. 使用应用启动类
在 IDEA 中直接执行应用启动类,来运行 Spring Boot 应用,日常开发中,会经常使用这种方式启动应用。常用的会有 Debug 启动模式,方便在开发中进行代码调试和 bug 处理。自然Debug 启动模式会比正常模式稍微慢一些。
2. 使用 Maven 运行
通过 Maven 运行,需要配置 Spring Boot Maven 插件,在 pom.xml 配置文件中,新增 build 节点并配置插件 spring-boot-maven-plugin代码如下
<build>
<plugins>
<!-- Spring Boot Maven 插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
在工程根目录中,运行如下 Maven 命令来运行 Spring Boot 应用:
mvn spring-boot:run
实际调用的是 pom.xml 配置的 Spring Boot Maven 插件 spring-boot-maven-plugin上面执行了插件提供的 run 指令。也可以在 IDEA 右侧工具栏的 Maven Project Tab 中,找到 Maven 插件的 spring-boot-maven-plugin执行相应的指令。所有指令如下
# 生成构建信息文件
spring-boot:build-info
# 帮助信息
spring-boot:help
# 重新打包
spring-boot:repackage
# 运行工程
spring-boot:run
# 将工程集成到集成测试阶段,进行工程的声明周期管理
spring-boot:start
spring-boot:stop
3. 使用 Java 命令运行
使用 Maven 或者 Gradle 安装工程,生成可执行的工程 jar 后,运行如下 Java 命令来运行 Spring Boot 应用:
java -jar target/xxx.jar
这里运行了 spring-boot-maven-plugin 插件编译出来的可执行 jar 文件。通过上述三种方式都可以成功运行 Spring Boot 工程,成功运行输出的控制台信息如图 1-10 所示。
工程服务器部署
基础环境安装如上面说的,需要 JDK 环境、Maven 环境等。
Win 服务器
推荐使用 AlwaysUp
使用方式也很简单:
Linux 服务器
推荐 yum 安装基础环境,比如安装 JDK
yum -y list java*
yum -y install java-1.8.0-openjdk*
java -version
安装 Maven
yum -y list apache-maven
sudo wget http://repos.fedorapeople.org/repos/dchen/apache-maven/epel-apache-maven.repo -O /etc/yum.repos.d/epel-apache-maven.repo
sudo yum install -y apache-maven
mvn --v
Linux 使用 nohup 命令进行对后台程序的启动关闭。
关闭应用的脚本stop.sh
启动应用的脚本start.sh
重启应用的脚本stop.sh
总结
这一篇主要一起实践了简单的 WebFlux API 控制层的测试Service 测试 Mock 和以前一样,以及工程运行、服务器部署的操作。
工程springboot-webflux-9-test。

View File

@ -0,0 +1,278 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
11 WebFlux 实战图书管理系统
前言
本篇内容我们会实现如下图所示的城市管理系统,因为上面案例都用的是 City所以这里直接使用城市作为对象写一个简单的城市管理系统如图所示
结构
类似上面讲的工程搭建,新建一个工程编写此案例。工程如图:
下面目录和上面类似,这边不重复讲解:
pom.xml Maven 依赖配置
application.properties 配置文件,配置 mongo 连接属性配置
dao 数据访问层
单击这里查看源代码。
本文主要介绍:
controller 控制层实现
static 存放 css 图片静态资源
templates 编写页面逻辑
CityController 控制层
使用注解驱动的模式来进行开发,代码如下:
/**
* city 控制层
* <p>
* Created by bysocket
*/
@Controller
@RequestMapping(value = "/city")
public class CityController {
private static final String CITY_FORM_PATH_NAME = "cityForm";
private static final String CITY_LIST_PATH_NAME = "cityList";
private static final String REDIRECT_TO_CITY_URL = "redirect:/city";
@Autowired
CityService cityService;
@RequestMapping(method = RequestMethod.GET)
public String getCityList(final Model model) {
model.addAttribute("cityList", cityService.findAll());
return CITY_LIST_PATH_NAME;
}
@RequestMapping(value = "/create", method = RequestMethod.GET)
public String createCityForm(final Model model) {
model.addAttribute("city", new City());
model.addAttribute("action", "create");
return CITY_FORM_PATH_NAME;
}
@RequestMapping(value = "/create", method = RequestMethod.POST)
public String postCity(@ModelAttribute City city) {
cityService.insertByCity(city);
return REDIRECT_TO_CITY_URL;
}
@RequestMapping(value = "/update/{id}", method = RequestMethod.GET)
public String getCity(@PathVariable Long id, final Model model) {
final Mono<City> city = cityService.findById(id);
model.addAttribute("city", city);
model.addAttribute("action", "update");
return CITY_FORM_PATH_NAME;
}
@RequestMapping(value = "/update", method = RequestMethod.POST)
public String putBook(@ModelAttribute City city) {
cityService.update(city);
return REDIRECT_TO_CITY_URL;
}
@RequestMapping(value = "/delete/{id}", method = RequestMethod.GET)
public String deleteCity(@PathVariable Long id) {
cityService.delete(id);
return CITY_LIST_PATH_NAME;
}
}
可以创建不同功能的控制层,来处理不同的 HTTP 业务请求,比如 CityFrontController、CityAdminController 等分别处理不同场景的问题。
getCityList 方法:处理“/city”的 GET 请求,用来获取 City 列表。
getCity 方法:处理“/city/update/{id}”的 GET 请求,用来获取 City 信息。
postCity 方法:处理“/book/create”的 POST 请求,用来新建 Book 信息;通过 @ModelAttribut 绑定实体参数,也通过 @RequestBody @RequestParam 传递参数。
putCity 方法:处理“/update”的 PUT 请求,用来更新 City 信息,并使用 redirect 重定向到列表页面。
cityForm 提交表单页面
表单页面如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<script type="text/javascript" th:src="@{https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js}"></script>
<link th:href="@{https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css}" rel="stylesheet"/>
<link th:href="@{/css/default.css}" rel="stylesheet"/>
<link rel="icon" th:href="@{/images/favicon.ico}" type="image/x-icon"/>
<meta charset="UTF-8"/>
<title>城市管理</title>
</head>
<body>
<div class="contentDiv">
<legend>
<strong>城市管理</strong>
</legend>
<form th:action="@{/city/{action}(action=${action})}" method="post" class="form-horizontal">
<div class="form-group">
<label for="city_id" class="col-sm-2 control-label">城市编号:</label>
<div class="col-xs-4">
<input type="text" class="form-control" id="city_id" name="id" th:value="${city.id}"/>
</div>
</div>
<div class="form-group">
<label for="city_name" class="col-sm-2 control-label">城市名称:</label>
<div class="col-xs-4">
<input type="text" class="form-control" id="city_name" name="cityName" th:value="${city.cityName}"/>
</div>
</div>
<div class="form-group">
<label for="city_description" class="col-sm-2 control-label">城市描述:</label>
<div class="col-xs-4">
<input class="form-control" id="city_description" rows="3" name="description"
th:value="${city.description}" />
</div>
</div>
<div class="form-group">
<label for="city_provinceId" class="col-sm-2 control-label">省份编号:</label>
<div class="col-xs-4">
<input type="text" class="form-control" id="city_provinceId" name="provinceId" th:value="${city.provinceId}"
/>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<input class="btn btn-primary" type="submit" value="提交"/>&nbsp;&nbsp;
<input class="btn" type="button" value="返回" onclick="history.back()"/>
</div>
</div>
</form>
</div>
</body>
</html>
利用的是 Thymeleaf 语法,上面章节也讲过具体使用方法,这里实现新增城市和更新城市两个操作。巧妙利用了 action 字段去动态判断请求是新增还是更新的控制层方法,然后进行 form 表单提交。
cityList 城市列表页面
列表页面代码如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<script type="text/javascript" th:src="@{https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js}"></script>
<link th:href="@{https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css}" rel="stylesheet"/>
<link th:href="@{/css/default.css}" rel="stylesheet"/>
<link rel="icon" th:href="@{/images/favicon.ico}" type="image/x-icon"/>
<meta charset="UTF-8"/>
<title>城市列表</title>
</head>
<body>
<div class="contentDiv">
<table class="table table-hover table-condensed">
<legend>
<strong>城市列表</strong>
</legend>
<thead>
<tr>
<th>城市编号</th>
<th>城市名称</th>
<th>描述</th>
<th>省份编号</th>
<th>管理</th>
</tr>
</thead>
<tbody>
<tr th:each="city : ${cityList}">
<th scope="row" th:text="${city.id}"></th>
<td><a th:href="@{/city/update/{cityId}(cityId=${city.id})}" th:text="${city.cityName}"></a></td>
<td th:text="${city.description}"></td>
<td th:text="${city.provinceId}"></td>
<td><a class="btn btn-danger" th:href="@{/city/delete/{cityId}(cityId=${city.id})}">删除</a></td>
</tr>
</tbody>
</table>
<div><a class="btn btn-primary" href="/city/create" role="button">新增城市</a></div>
</div>
</body>
</html>
这里编写了一个列表对象的循环和简单的页面跳转逻辑,下面看看这两个页面组合使用的运行场景。
运行工程
一个 CRUD 的 Spring Boot Webflux 工程就开发完毕了,下面运行工程验证一下。使用 IDEA 右侧工具栏,点击 Maven Project Tab 选项,单击使用下 Maven 插件的 install 命令;或者使用命令行的形式,在工程根目录下,执行 Maven 清理和安装工程的指令:
cd springboot-webflux-10-book-manage-sys
mvn clean install
在控制台中看到成功的输出:
... 省略
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01:30 min
[INFO] Finished at: 2017-10-15T10:00:54+08:00
[INFO] Final Memory: 31M/174M
[INFO] ------------------------------------------------------------------------
在 IDEA 中执行 Application 类启动,任意正常模式或者 Debug 模式,可以在控制台看到成功运行的输出:
... 省略
2018-04-10 08:43:39.932 INFO 2052 --- [ctor-http-nio-1] r.ipc.netty.tcp.BlockingNettyContext : Started HttpServer on /0:0:0:0:0:0:0:0:8080
2018-04-10 08:43:39.935 INFO 2052 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8080
2018-04-10 08:43:39.960 INFO 2052 --- [ main] org.spring.springboot.Application : Started Application in 6.547 seconds (JVM running for 9.851)
打开浏览器输入地址http://localhost:8080/city即打开城市列表页面
然后新增,或者单击城市名称修改,到表单提交页面:
总结
这里,初步实现了小案例城市管理系统,基本满足日常的 CRUD 业务流程操作。上手教程只是上手,具体复杂逻辑,欢迎一起多交流学习。