1. 采用类似于Spring MVC的注解的方式来开发。
此时开发时感觉Spring MVC差异不大,但底层依然是反应式API。
2. 使用函数式编程来开发
使用函数式开发WebFlux时需要开发两个组件:
▲ Handler:作用:该处理器组件相当于控制器,它负责处理客户端的请求、并对客户端生成响应。
该Handler组件的每个方法都只带一个ServerRequest参数(不是Servlet API)——代表客户端请求对象,
且每个方法的返回值类型都是Mono,代表作为服务器响应的消息发布者。
mono 代表一个消息发布者
▲ Router:作用:该组件通过函数式的编程方式来定义URL与Handler处理方法之间的映射关系。
这两种方式并不是可以自由选择的,而是根据数据的来源不同,需要采用对应的获取策略。
- 对于以请求体提交的数据,通常会通过formData()(表单数据)或bodyToFlux()或bodyToMono()(RESTful)方法来获取,
由于这种方式都需要通过网络IO读取数据,可能会造成阻塞,
因此它们都采用了订阅-发布的异步方式,这三个方法的返回值都是Mono或Flux(消息发布者)。
- 对于URL中的数据(包括传统请求参数和路径参数),由于它们只要直接解析URL字符串即可读取数据,
不会造成阻塞,因此没有采用订阅-发布的异步方式。直接用pathVariable()或queryParam()方法即可读取数据。
Handler作用: 该处理器组件相当于控制器,它负责处理客户端的请求、并对客户端生成响应。
Handler处理方法的返回值类型是Mono,
调用ServerResponse的ok()(相当于将响应状态码设为200)、
contentType()方法返回ServerResponse.BodyBuilder对象。
有了ServerResponse.BodyBuilder对象之后,根据响应类型不同,
可调用如下两个方法来生成Mono作为返回值:
▲ render(String name, Map model):使用模板引擎来生成响应,
其中第一个参数代表逻辑视图名,第二个参数代表传给模板的model数据。render()方法还有其他重载形式,功能类似。
▲ body(P publisher, Class elementClass):直接设置响应体类生成响应,同样用于生成RESTful响应。
body()方法还有其他重载形式,功能类似。
Router作用: 该组件通过函数式的编程方式来定义URL与Handler处理方法之间的映射关系。
▲ Router就是容器中RouterFunctions类型的Bean。
——通常来说,就是使用@Configuration修饰的配置类来配置该Bean即可。
return RouterFunctions
// 定义映射地址和处理器方法之间的对应关系
.route(RequestPredicates.POST("/login")
.and(RequestPredicates.accept(MediaType.TEXT_HTML)), handler::login)
.andRoute(RequestPredicates.GET("/viewBook/{id}")
.and(RequestPredicates.accept(MediaType.TEXT_HTML)), handler::viewBook);
同个请求,演示跟 spring mvc 不同的实现方法。
请求的数据是简单的url数据,就是前端传来的数据(id)是写在url 的。
总结:通过添加 Handler 类,相当于之前的controller ,然后创建一个 Router 配置类,通过在配置类 配置 Router Bean 这个bean,来实现对客户端请求来的URL 与 Handler处理方法之间的映射关系。最终响应回json格式的数据或者 html 页面。
Handler:该处理器组件相当于控制器,它负责处理客户端的请求、并对客户端生成响应,这个类就是handler组件
现在弄一个 Handler 类,用来处理客户端的请求,是一个处理数据的类,相当于controller
这个方法是生成 RESTful 响应的,就是 Json 响应
这个方法是生成 HTML 响应的
Router:作用:该组件通过函数式的编程方式来定义URL与Handler处理方法之间的映射关系。
配置 Router Bean ,负责完成请求 URL 和 Handler 处理方法之间的映射。
设置方法的请求路径是 “/viewBookabc/{id}” ,走这个路径就会访问这个 handler::viewBook 方法。
而 handler::viewBooks 是 lambda 中的方法引用 ,会找到 BookHandler 类中的 viewBook 方法
bean 方法里面的参数是 BookHandler,所以可以用 lambda 的方法引用功能 来引用该类的viewBook方法。
负责完成 【请求URL】 和 【Handler处理方法】 之间的映射。
Handler处理方法:就是 BookHandler 的 viewBook 方法。
调用方法看看流程:
访问方法,就会走 BookHandler 的 这个方法。
测试结果:
完整代码:
BookHandler
// Handler:该处理器组件相当于控制器,它负责处理客户端的请求、并对客户端生成响应,这个类就是handler组件
@Component
public class BookHandler
{
private BookService bookService;
//有参构造器完成依赖注入
public BookHandler(BookService bookService)
{
this.bookService = bookService;
}
// Handler:该处理器组件相当于控制器,它负责处理客户端的请求、并对客户端生成响应,这个类就是handler组件
//这个方法是生成 RESTful 响应的
public Mono<ServerResponse> viewBook(ServerRequest request)
{
//如果请求参数是通过 URL 字符串即可解析,可用 pathVariable()或queryParam()方法获取参数
Integer id = Integer.parseInt(request.pathVariable("id"));
Book book = bookService.getBook(id);
//ok() 表示服务器响应正常
Mono<ServerResponse> body = ServerResponse.ok()
//选择生成 JSON 响应类型
.contentType(MediaType.APPLICATION_JSON)
//如果要生成 JSON 响应,直接用 body 方法
//参数1:代表数据发布者(Publisher),参数2:指定 Mono 中每个数据项的类型
//Mono 的 justOrEmpty 将单个及可能为null的数据包装成 Mono
//如果是设计良好的应用(就是底层数据库的访问也是用 reactor api ,
// 这样此处从数据库返回的数据就是 Mono 或者 Flux,根本不需要包装)
.body(Mono.justOrEmpty(book), Book.class);
return body;
}
//这个方法是生成 HTML 响应的
public Mono<ServerResponse> viewBookHtml(ServerRequest request)
{
//如果请求参数是通过 URL 字符串即可解析,可用 pathVariable()或queryParam()方法获取参数
Integer id = Integer.parseInt(request.pathVariable("id"));
Book book = bookService.getBook(id);
//ok() 表示服务器响应正常
Mono<ServerResponse> render = ServerResponse.ok()
//选择生成 HTML 响应类型
.contentType(MediaType.TEXT_HTML)
//参数1:逻辑视图名 参数2:相当于 spring mvc 的 model,用于向视图页面传输数据
.render("viewBook", Map.of("book", book));
return render;
}
}
RouterConfig
package cn.ljh.FunctionalFlux.router;
import cn.ljh.FunctionalFlux.handler.BookHandler;
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 RouterConfig
{
//配置 Router Bean ,负责完成请求 URL 和 Handler 处理方法之间的映射。
@Bean
public RouterFunction<ServerResponse> routerFunctions(BookHandler handler)
{
//MediaType.APPLICATION_JSON 设置响应类型 , handler::viewBooks 是 lambda 中的方法引用
RouterFunction<ServerResponse> route =
RouterFunctions
//这里就映射到 BookHandler 类里面的 viewBook 方法,/viewBookabc/{id}这个是我们这边给的访问路径
.route(RequestPredicates.GET("/viewBookabc/{id}")
.and(RequestPredicates.accept(MediaType.APPLICATION_JSON)), handler::viewBook)
//这里就映射到 BookHandler 类里面的 viewBookHtml 方法,/viewBookHtml/{id}这个是我们这边给的访问路径
.andRoute(RequestPredicates.GET("/viewBookHtml/{id}")
.and(RequestPredicates.accept(MediaType.TEXT_HTML)), handler::viewBookHtml);
return route;
}
}
上面的代码演示,请求的数据是简单的url数据,就是前端传来的数据(id)是写在url 的。
这次演示的是前端以 表单 的方式 或 restful 方式提交数据。
这边接收前端传来的数据并进行处理,相当于controller
这里的bean就是处理 请求url 和 handler处理方法 之间的映射关系
测试结果:
成功处理添加书本的方法,添加的书本的数据在postman中实现。
测试结果:
注意:发现因为 handler处理方法那里,因为使用了map ,把源 Mono 转成新的 Mono,当时转换的结果没去用它,所以出现添加不成功的问题。
如图:
如果不需要使用 Mono 转换之后的结果,此时就不需要使用 map() 方法
map() 方法就是负责将 源Mono 转换成新的 Mono
如果只是希望用到 Mono 中的数据,此时成为消费数据,
就是把这条消息消费掉就行,因为不需要把 Mono 的结果返回到视图页面,所以不需要用map方法进行转换。
前端注意小知识:
在 templates 路径下的静态页面是不能直接访问的,得通过控制器的处理方法进行转发才能访问到。
或者直接把页面放在静态资源目录(static、public),才能直接访问。
注意:页面得是静态页面,不能有动态内容,不能是动态页面。
处理类:BookHandler,类似于controller
package cn.ljh.FunctionalFlux.handler;
import cn.ljh.FunctionalFlux.domain.Book;
import cn.ljh.FunctionalFlux.service.BookService;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.Collection;
import java.util.Map;
// Handler:该处理器组件相当于控制器,它负责处理客户端的请求、并对客户端生成响应,这个类就是handler组件
@Component
public class BookHandler
{
private BookService bookService;
//有参构造器完成依赖注入
public BookHandler(BookService bookService)
{
this.bookService = bookService;
}
// Handler:该处理器组件相当于控制器,它负责处理客户端的请求、并对客户端生成响应,这个类就是handler组件
//这个方法是生成 RESTful 响应的 ,就是 Json 响应
public Mono<ServerResponse> viewBook(ServerRequest request)
{
//如果请求参数是通过 URL 字符串即可解析,可用 pathVariable()或queryParam()方法获取参数
Integer id = Integer.parseInt(request.pathVariable("id"));
Book book = bookService.getBook(id);
//ok() 表示服务器响应正常
Mono<ServerResponse> body = ServerResponse.ok()
//选择生成 JSON 响应类型
.contentType(MediaType.APPLICATION_JSON)
//如果要生成 JSON 响应,直接用 body 方法
//参数1:代表数据发布者(Publisher),参数2:指定 Mono 中每个数据项的类型
//Mono 的 justOrEmpty 将单个及可能为null的数据包装成 Mono
//如果是设计良好的应用(就是底层数据库的访问也是用 reactor api ,
// 这样此处从数据库返回的数据就是 Mono 或者 Flux,根本不需要包装)
.body(Mono.justOrEmpty(book), Book.class);
return body;
}
//这个方法是生成 HTML 响应的
public Mono<ServerResponse> viewBookHtml(ServerRequest request)
{
//如果请求参数是通过 URL 字符串即可解析,可用 pathVariable()或queryParam()方法获取参数
Integer id = Integer.parseInt(request.pathVariable("id"));
Book book = bookService.getBook(id);
//ok() 表示服务器响应正常
Mono<ServerResponse> render = ServerResponse.ok()
//选择生成 HTML 响应类型
.contentType(MediaType.TEXT_HTML)
//参数1:逻辑视图名 参数2:相当于 spring mvc 的 model,用于向视图页面传输数据
.render("viewBook", Map.of("book", book));
return render;
}
//以 RESTful 方式提交的数据的处理
public Mono<ServerResponse> addBook(ServerRequest request)
{
//假设数据来自 RESTful 的 POST 请求,此时用 bodyToMono() 或 bodyToFlux() 来获取数据
//bodyToFlux():如果请求的数据中包含多个数据,就用这个。
//bodyToMono():如果请求的数据只有一个数据,那就用这个
//这两个方法参数指定了 Mono 或 Flux 中数据的类型
// 添加一本图书,只是一个对象,所以用.bodyToMono() ,
// 如果是一个集合,就应该使用 .bodyToFlux()
Mono<Book> bookMono = request.bodyToMono(Book.class);
//map() 负责将 Mono 或者 Flux 中的元素,转换成新的 Mono 或 Flux 中的元素
Mono<Book> resultMono = bookMono.map(book ->
{
//添加 Book 对象
bookService.addBook(book);
return book;
});
Mono<ServerResponse> body = ServerResponse.ok()
//选择生成 JSON 响应类型
.contentType(MediaType.APPLICATION_JSON)
//如果要生成 JSON 响应,直接用 body 方法
//参数1:代表数据发布者(Publisher),参数2:指定 Mono 中每个数据项的类型
//Mono 的 justOrEmpty 将单个及可能为null的数据包装成 Mono
//如果是设计良好的应用(就是底层数据库的访问也是用 reactor api ,
// 这样此处从数据库返回的数据就是 Mono 或者 Flux,根本不需要包装)
.body(resultMono, Book.class);
return body;
}
//通过表单页面提交请求
public Mono<ServerResponse> addBookHtml(ServerRequest request)
{
//假设数据来自 表单页面 的 POST 请求,通过 formData() 获取表单的数据
Mono<MultiValueMap<String, String>> formData = request.formData();
/*
* 如果不需要使用 Mono 转换之后的结果,此时就不需要使用 map() 方法
* map() 方法就是负责将 源Mono 转换成新的 Mono
* 如果只是希望用到 Mono 中的数据,此时成为消费数据
*/
formData.subscribe(map ->
{
String name = map.get("name").get(0);
String price = map.get("price").get(0);
String author = map.get("author").get(0);
Book book = new Book(null, name, Double.parseDouble(price), author);
bookService.addBook(book);
});
Mono<ServerResponse> render = ServerResponse.ok()
//选择生成 JSON 响应类型
.contentType(MediaType.TEXT_HTML)
.render("addBookResult", Map.of("tip", "添加书籍成功"));
return render;
}
}
配置类:RouterConfig,添加个bean处理url和handler类中的方法的映射关系
package cn.ljh.FunctionalFlux.router;
import cn.ljh.FunctionalFlux.handler.BookHandler;
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 RouterConfig
{
//配置 Router Bean ,负责完成请求 URL 和 Handler 处理方法之间的映射。
@Bean
public RouterFunction<ServerResponse> routerFunctions(BookHandler handler)
{
//MediaType.APPLICATION_JSON 设置响应类型 , handler::viewBooks 是 lambda 中的方法引用
RouterFunction<ServerResponse> route =
RouterFunctions
//这里就映射到 BookHandler 类里面的 viewBook 方法,/viewBookabc/{id}这个是我们这边给的访问路径
.route(RequestPredicates.GET("/viewBookabc/{id}")
.and(RequestPredicates.accept(MediaType.APPLICATION_JSON)), handler::viewBook)
//这里就映射到 BookHandler 类里面的 viewBookHtml 方法,/viewBookHtml/{id}这个是我们这边给的访问路径
.andRoute(RequestPredicates.GET("/viewBookHtml/{id}")
.and(RequestPredicates.accept(MediaType.TEXT_HTML)), handler::viewBookHtml)
//这里就映射到 BookHandler 类里面的 addBook 方法,/addBook 这个是我们这边给的访问路径
.andRoute(RequestPredicates.POST("/addBook")
.and(RequestPredicates.accept(MediaType.APPLICATION_JSON)), handler::addBook)
//这里就映射到 BookHandler 类里面的 addBookHtml 方法,/addBookHtml/{id}这个是我们这边给的访问路径
.andRoute(RequestPredicates.POST("/addBookHtml")
.and(RequestPredicates.accept(MediaType.TEXT_HTML)), handler::addBookHtml);
return route;
}
}
BookService
package cn.ljh.FunctionalFlux.service;
import cn.ljh.FunctionalFlux.domain.Book;
import java.util.Collection;
public interface BookService
{
Book getBook(Integer id);
Integer addBook(Book book);
Collection<Book> getAllBooks();
}
BookServiceImpl
package cn.ljh.FunctionalFlux.service.impl;
import cn.ljh.FunctionalFlux.domain.Book;
import cn.ljh.FunctionalFlux.service.BookService;
import org.springframework.stereotype.Service;
import java.util.*;
//添加这个@Service注解,springboot就可以自动扫描这个Service组件的实现类,然后把这个类部署成容器中的bean。
@Service
public class BookServiceImpl implements BookService
{
//添加一个 Map 集合,假设为数据库
public static final Map<Integer, Book> bookDB = new LinkedHashMap<>();
//创建一个自增id
static int nextId = 4;
//初始化这个数据库
static
{
bookDB.put(1, new Book(1, "火影忍者", 100.0, "岸本"));
bookDB.put(2, new Book(2, "家庭教师", 110.0, "天野明"));
bookDB.put(3, new Book(3, "七龙珠Z", 120.0, "鸟山明"));
}
//查看图书
@Override
public Book getBook(Integer id)
{
Book book = bookDB.get(id);
if (book == null){
throw new RuntimeException("没有此图书信息!");
}
return book;
}
//添加图书
@Override
public Integer addBook(Book book)
{
book.setId(nextId);
bookDB.put(nextId,book);
//返回id,先返回在自增。
return nextId++;
}
//查看所有的图书
@Override
public Collection<Book> getAllBooks()
{
//获取集合中的所有元素
Collection<Book> values = bookDB.values();
return values;
}
}
添加图书页面:addBook.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>添加图书页面</title>
</head>
<body>
<h2>添加图书页面</h2>
<form method="post" action="/addBookHtml">
书名:<input name="name" id="name" type="text"><br>
价格:<input name="price" id="price" type="text"><br>
作者:<input name="author" id="author" type="text"><br>
<input type="submit" value="提交"/>
<input type="reset" value="重设"/>
</form>
</body>
</html>
添加图书结果页面:addBookResult.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>添加图书结果</title>
</head>
<body>
<h2>添加图书结果</h2>
<div th:text="${tip}">
</div>
</body>
</html>
根据id查询图书:viewBook.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>查看图书</title>
</head>
<body>
<h2>查看图书</h2>
<div th:text="${book.name}"></div>
<div th:text="${book.price}"></div>
<div th:text="${book.author}"></div>
</div>
</body>
</html>