Springboot2.0WebFlux 开发
简单了解下其用法。
1. JDK9的Reactive Stream 用法
响应式流,和发布订阅者模式一样,只不过订阅者可以自己控制生产者发送数据的速度。
1. 背压
背压是一种常用策略,使得发布者拥有无限制的缓冲区存储元素,用于确保发布者发布元素太快时,不会去压制订阅者。举个例子就是消费者需要水的时候就打开水龙头, 不需要就关闭开关,也就是消费者可以自己控制流量。
2. 简单使用
1. 不带处理器
package flux; import java.util.concurrent.Flow; import java.util.concurrent.SubmissionPublisher; public class Test1 { public static void main(String[] args) throws InterruptedException { // 1. 定义发布者,发布者的数据类型是Integer // 使用SubmissionPublisher, 该类实现了接口 java.util.concurrent.Flow.Publisher SubmissionPublisher<Integer> integerSubmissionPublisher = new SubmissionPublisher<>(); // 2. 定义订阅者 Flow.Subscriber<Integer> subscriber = new Flow.Subscriber<>() { private Flow.Subscription subscription; // 建立订阅关系时候调用 @Override public void onSubscribe(Flow.Subscription subscription) { // 保存起来 this.subscription = subscription; // 请求数据 this.subscription.request(1); } // 有数据到达会调用 @Override public void onNext(Integer item) { // 消费数据 System.out.println("接收到数据: " + item); // 下面方法就是可以控制速率 // 再次请求一条 subscription.request(1); // 或者调用cancel 告知已经不需要消费 // subscription.cancel(); } // 一般是处理异常会调用到 @Override public void onError(Throwable throwable) { System.out.println("onError, " + throwable.getMessage()); this.subscription.cancel(); } // 处理完成。 一般是调用 integerSubmissionPublisher.close(); 会调用下面方法 @Override public void onComplete() { System.out.println("java.util.concurrent.Flow.Subscriber.onComplete"); } }; // 3. 发布者和订阅者建立关系 integerSubmissionPublisher.subscribe(subscriber); // 4.生产数据并发布 integerSubmissionPublisher.submit(1); integerSubmissionPublisher.submit(2); integerSubmissionPublisher.submit(3); // 5. 关闭相关流。 一般在finally 关闭 integerSubmissionPublisher.close(); // 当前线程阻塞等待,便于看到效果 Thread.currentThread().join(1 * 1000); } }
2. 带处理器
package flux; import java.util.concurrent.Flow; import java.util.concurrent.SubmissionPublisher; public class Test2 { public static void main(String[] args) throws InterruptedException { // 1. 定义发布者,发布者的数据类型是Integer // 使用SubmissionPublisher, 该类实现了接口 java.util.concurrent.Flow.Publisher SubmissionPublisher<Integer> publisher = new SubmissionPublisher<>(); // 2. 定义一个处理器,对数据进行过滤 Myprocessor myprocessor = new Myprocessor(); publisher.subscribe(myprocessor); // 3. 定义订阅者 Flow.Subscriber<String> subscriber = new Flow.Subscriber<>() { private Flow.Subscription subscription; // 建立订阅关系时候调用 @Override public void onSubscribe(Flow.Subscription subscription) { // 保存起来 this.subscription = subscription; // 请求数据 this.subscription.request(1); } // 有数据到达会调用 @Override public void onNext(String item) { // 消费数据 System.out.println("接收到数据: " + item); // 下面方法就是可以控制速率 // 再次请求一条 subscription.request(1); // 或者调用cancel 告知已经不需要消费 // subscription.cancel(); } // 一般是处理异常会调用到 @Override public void onError(Throwable throwable) { System.out.println("onError, " + throwable.getMessage()); this.subscription.cancel(); } // 处理完成。 一般是调用 integerSubmissionPublisher.close(); 会调用下面方法 @Override public void onComplete() { System.out.println("java.util.concurrent.Flow.Subscriber.onComplete"); } }; // 4. 发布者和订阅者建立关系 myprocessor.subscribe(subscriber); // 5.生产数据并发布, submit 方法是一个阻塞方法, 实际是 publisher.submit(1); publisher.submit(-2); publisher.submit(-3); // 6. 关闭相关流。 一般在finally 关闭 publisher.close(); // myprocessor.close(); // 当前线程阻塞等待,便于看到效果 Thread.currentThread().join(1 * 1000); } private static class Myprocessor extends SubmissionPublisher<String> implements Flow.Processor<Integer, String> { private Flow.Subscription subscription; @Override public void onSubscribe(Flow.Subscription subscription) { this.subscription = subscription; subscription.request(1); } @Override public void onNext(Integer item) { System.out.println("处理器接收到的数据: " + item); if (item > 0) { this.submit(item.toString()); } // 再次请求一条 subscription.request(1); // 或者调用cancel 告知已经不需要消费 // subscription.cancel(); } @Override public void onError(Throwable throwable) { // ingore } @Override public void onComplete() { // ignore } } }
结果
处理器接收到的数据: 1 处理器接收到的数据: -2 处理器接收到的数据: -3 接收到数据: 1
2. 异步Servlet 用法
1. 同步Servlet
package test; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet("/SyncServlet") public class SyncServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { long start = System.currentTimeMillis(); doSomeThing(request, response); long end = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName() + " use: " + (end - start)); } private void doSomeThing(HttpServletRequest request, HttpServletResponse response) { try { Thread.currentThread().sleep(5 * 1000); response.getWriter().append("Served at: ").append(request.getContextPath()); } catch (Exception e) { e.printStackTrace(); } } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }
结果:
http-bio-8080-exec-12 use: 5000
2. AsyncServlet 异步Servlet
(1) . asyncSupported = true 开启异步支持
(2). request.startAsync() 开启异步环境并且获取一个对象: AsyncContext
(3). 执行完成后提交: asyncContext.complete();
异步代码:
package test; import java.io.IOException; import java.util.concurrent.CompletableFuture; import javax.servlet.AsyncContext; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @WebServlet(value = "/AsyncServlet", asyncSupported = true) public class AsyncServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { long start = System.currentTimeMillis(); AsyncContext asyncContext = request.startAsync(); CompletableFuture.runAsync(() -> { doSomeThing(asyncContext, request, response); }); long end = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName() + " use: " + (end - start)); } private void doSomeThing(AsyncContext asyncContext, HttpServletRequest request, HttpServletResponse response) { try { Thread.currentThread().sleep(5 * 1000); response.getWriter().append("Served at: ").append(request.getContextPath()); } catch (Exception e) { e.printStackTrace(); } asyncContext.complete(); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }
结果:
http-bio-8080-exec-9 use: 1
3. 前端界面展示:
可以看出来,前端看到的是一样的, 都是阻塞的。后端只是tomcat 执行任务的线程池 exec 线程是非阻塞的。 个人理解是tomcat 的线程池可以执行更多的任务,我们执行业务耗时的操作放到了自己的线程池里面。
3. SSE 简单用法
Server Send Event 类似于Websocket,只不过websocket 是双通道,客户端和服务端都可以发送消息; sse 是只能server 向客户端发送。
server 端向客户端发送数据的格式为: "data:" + 真实数据 + "\n\n"
1. 服务器端代码:
package test; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Server Send Event 基于浏览器的 Web 应用程序中仅从服务器向客户端发送文本消息的技术 */ @WebServlet("/SSEServlet") public class SSEServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { long start = System.currentTimeMillis(); doSomeThing(request, response); long end = System.currentTimeMillis(); System.out.println(Thread.currentThread().getName() + " use: " + (end - start)); } private void doSomeThing(HttpServletRequest request, HttpServletResponse response) { response.setHeader("Content-Type", "text/event-stream"); response.setCharacterEncoding("utf-8"); for (int index = 0; index < 5; index++) { try { Thread.currentThread().sleep(1* 1000); // 添加事件类型标识 response.getWriter().write("event:me\n"); // 数据格式 "data:" + 真实数据 + 两个回车 response.getWriter().write("data:" + index + "\n\n"); response.getWriter().flush(); } catch (Exception e) { e.printStackTrace(); } } } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }
2. 客户端代码
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <script type="text/javascript"> // 如果支持则能获取到 window.EventSource 对象,否则就是浏览器不支持 console.log(window.EventSource); // 依赖H5 // 参数为url var sse = new EventSource("SSEServlet"); /* sse.onmessage = function(msg) { console.log(msg); } */ // 指定消息类型 sse.addEventListener("me", function(e) { // 关闭重连,否则会一直重复刷新数据。 默认好像是3s 后自动重新连接获取笑嘻嘻 console.log("me data:" + e.data); if (e.data == 4) { sse.close(); } }); </script> </body> </html>
结果:
查看network 如下:
4. webflux 简单使用
1. 简介
(1).什么是webflux
Spring Framework 中包含的原始 Web 框架 Spring Web MVC 是专门为 Servlet API 和 Servlet 容器构建的。反应式堆栈 Web 框架 Spring WebFlux 是在 5.0 版的后期添加的。它是完全非阻塞的,支持反应式流(Reactive Stream)背压,并在Netty,Undertow和Servlet 3.1 +容器等服务器上运行。
(2).Reactive Stream、Reactor 和 WebFlux 三者的关系?
Reactive Stream 是一套反应式编程 标准 和 规范;Reactor 是基于 Reactive Streams 一套 反应式编程框架;WebFlux 以 Reactor 为基础,实现 Web 领域的 反应式编程框架。
(3).webflux 有两个重要的类。 Mono 和 Flux。 Mono 表示0-1 个元素, Flux 表示0-N 个元素。例如:
package flux; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.publisher.Flux; public class ReactorDemo { public static void main(String[] args) throws InterruptedException { // reactor = jdk8 stream + jdk9 reactive stream // Mono 0-1 个元素 // Flux 0-N 个元素 Subscriber<Integer> integerSubscriber = new Subscriber<Integer>() { private Subscription subscription; @Override public void onSubscribe(Subscription s) { this.subscription = s; subscription.request(1); } @Override public void onNext(Integer o) { System.out.println("org.reactivestreams.Subscriber.onNext, o: " + o); subscription.request(1); } @Override public void onError(Throwable t) { t.printStackTrace(); } @Override public void onComplete() { System.out.println("complete"); } }; String []strs = {"1", "2", "3"}; // jdk8 的stream Flux.fromArray(strs).map(s -> Integer.parseInt(s)) // jdk9 的reactor stream .subscribe(integerSubscriber); Thread.sleep(5 * 1000); } }
结果:
org.reactivestreams.Subscriber.onNext, o: 1 org.reactivestreams.Subscriber.onNext, o: 2 org.reactivestreams.Subscriber.onNext, o: 3 complete
2. webflux 简单使用
1. pom 文件依赖
<dependencies> <!-- validate 相关注解 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <!-- <dependency>--> <!-- <groupId>org.springframework.boot</groupId>--> <!-- <artifactId>spring-boot-starter-tomcat</artifactId>--> <!-- </dependency>--> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>r09</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies>
其有两种使用方式。 第一种是springMVC模式注解,第二种是基于RoutingFunction 模式。
2. 控制层简单使用-注解模式
(1) 启动项目, 启动后日志如下:可以检出默认使用的是netty 作为服务器
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v2.3.1.RELEASE) [ WARN] 2022-06-04 13:41:29.756 5849 --- [ main] o.s.boot.StartupInfoLogger : InetAddress.getLocalHost().getHostName() took 5002 milliseconds to respond. Please verify your network configuration (macOS machines may need to add entries to /etc/hosts). [ INFO] 2022-06-04 13:41:34.764 5849 --- [ main] flux.Main : Starting Main on aaadeMacBook-Pro.local with PID 5849 (/Users/aaa/app/ideaspace/mvnpro/target/classes started by aaa in /Users/aaa/app/ideaspace/mvnpro) [ INFO] 2022-06-04 13:41:34.765 5849 --- [ main] flux.Main : No active profile set, falling back to default profiles: default [ INFO] 2022-06-04 13:41:40.643 5849 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8081 [ INFO] 2022-06-04 13:41:40.651 5849 --- [ main] flux.Main : Started Main in 21.157 seconds (JVM running for 26.646)
(2) 编写Controller
1》简单的Mono、Flux、String 类型的返回以及上面SSE类型交互
package flux.controller; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.stream.IntStream; @RestController @Slf4j public class TestController1 { @GetMapping("/test1") public String test1() { log.info("test1 start"); String result = create(); log.info("test1 end"); return result; } private String create() { try { Thread.sleep(5 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } log.info("create"); return Thread.currentThread().getName() + " test1"; } @GetMapping("/test3") public Mono<String> test3() { log.info("test3 start"); Mono<String> result = Mono.fromSupplier(() -> create()); log.info("test3 end"); return result; } @GetMapping("/test4") public Flux<String> test4() { log.info("test4 start"); Flux<String> objectFlux = Flux.fromStream(IntStream.range(1, 5).mapToObj(i -> { try { Thread.sleep(1 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } log.info(i + ""); return i + ""; })); log.info("test4 end"); return objectFlux; } @GetMapping(value = "/test5", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<String> test5() { log.info("test5 start"); Flux<String> objectFlux = Flux.fromStream(IntStream.range(1, 5).mapToObj(i -> { try { Thread.sleep(1 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } return String.valueOf(i); })); log.info("test5 end"); return objectFlux; } }
测试:1
curl 上面的前三个接口, 查看日志(三个接口对前端看来都是同步阻塞的)
aaa@aaadeMacBook-Pro ~ % curl localhost:8081/test1 reactor-http-nio-2 test1% aaa@aaadeMacBook-Pro ~ % curl localhost:8081/test3 reactor-http-nio-3 test1% aaa@aaadeMacBook-Pro ~ % curl localhost:8081/test4 1234%
日志如下:
[ INFO] 2022-06-04 13:52:08.721 5890 --- [ctor-http-nio-2] flux.controller.TestController1 : test1 start [ INFO] 2022-06-04 13:52:13.725 5890 --- [ctor-http-nio-2] flux.controller.TestController1 : create [ INFO] 2022-06-04 13:52:13.725 5890 --- [ctor-http-nio-2] flux.controller.TestController1 : test1 end [ INFO] 2022-06-04 13:52:21.967 5890 --- [ctor-http-nio-3] flux.controller.TestController1 : test3 start [ INFO] 2022-06-04 13:52:21.968 5890 --- [ctor-http-nio-3] flux.controller.TestController1 : test3 end [ INFO] 2022-06-04 13:52:26.975 5890 --- [ctor-http-nio-3] flux.controller.TestController1 : create [ INFO] 2022-06-04 13:52:36.636 5890 --- [ctor-http-nio-4] flux.controller.TestController1 : test4 start [ INFO] 2022-06-04 13:52:36.637 5890 --- [ctor-http-nio-4] flux.controller.TestController1 : test4 end [ INFO] 2022-06-04 13:52:37.645 5890 --- [ctor-http-nio-4] flux.controller.TestController1 : 1 [ INFO] 2022-06-04 13:52:38.674 5890 --- [ctor-http-nio-4] flux.controller.TestController1 : 2 [ INFO] 2022-06-04 13:52:39.678 5890 --- [ctor-http-nio-4] flux.controller.TestController1 : 3 [ INFO] 2022-06-04 13:52:40.682 5890 --- [ctor-http-nio-4] flux.controller.TestController1 : 4
test1接口是原来SprignMVC 的处理方式,所以其是同步的方式; test3 接口是返回单个,且我们将create 方法放到了Mono 获取中,所以其是异步的, 从日志也可以看出; test4 接口同test3, 只不过test4是返回Flux,返回多个。(这里可以看出返回Mono和Flux,且构造结果是用Mono和Flux 的api 的操作都是异步的.)
测试2: 界面访问test5, 查看SSE类型的处理, 结果如下:
2》编写简单的UserController, 模拟简单的用户增加和查询操作
代码
package flux.controller; import flux.bean.User; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import javax.validation.Valid; import java.util.ArrayList; import java.util.List; @RestController @Slf4j @RequestMapping("/user") public class UserController { /** * 模拟新增用户 * * @param user * @return */ @PostMapping(value = "/createUser") public Mono<User> createUser(@RequestBody @Valid User user) { log.info("test6 start"); Mono<User> userMono = Mono.fromSupplier(() -> doCreateUser(user)); log.info("test6 end"); return userMono; } @GetMapping(value = "/listUsers") public Flux<User> listUsers() { log.info("listUsers start"); Flux<User> userFlux = Flux.fromStream(listAllUser().stream()); log.info("listUsers start"); return userFlux; } private List<User> listAllUser() { List<User> objects = new ArrayList<>(); User user = new User(); user.setAge(25); user.setUsername("username1"); User user2 = new User(); user2.setAge(25); user2.setUsername("username2"); objects.add(user); objects.add(user2); try { Thread.sleep(2 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } log.info("listAllUser sleep end"); return objects; } // 模拟入库操作 private User doCreateUser(User user) { try { Thread.sleep(5 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } user.setId(20); log.info("doCreateUser sleep end"); return user; } }
全局异常拦截器处理参数错误
package flux.config; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.support.WebExchangeBindException; /** * 全局异常处理器 * * @author Administrator * */ @ControllerAdvice @Slf4j public class BXExceptionHandler { @ExceptionHandler(Throwable.class) public ResponseEntity<String> errorHandler(WebExchangeBindException e) { log.error("errorHandler", e); return new ResponseEntity<String>(toStr(e), HttpStatus.BAD_REQUEST); } private String toStr(WebExchangeBindException e) { return e.getFieldErrors().stream().map(e1 -> e1.getField() + ": " + e1.getDefaultMessage() ).reduce(((s, s2) -> s + "\r\n" + s2)).get(); } }
测试:
curl 测试验证参数
aaa@aaadeMacBook-Pro ~ % curl --header 'Content-Type: application/json' -d '{"age": 9}' localhost:8081/user/createUser
username: username 不能为空
age: 年龄不能小于10%
curl 两个接口查看日志:
aaa@aaadeMacBook-Pro ~ % curl -X POST --header 'Content-Type: application/json' -d '{"age": 22, "username": "zhangsan"}' localhost:8081/user/createUser {"id":20,"username":"zhangsan","age":22}% aaa@aaadeMacBook-Pro ~ % curl -X 'GET' localhost:8081/user/listUsers [{"id":null,"username":"username1","age":25},{"id":null,"username":"username2","age":25}]%
查看后端日志:(可以看出创建接口的doCreateUser 方法是异步的; listUsers的listAllUser 方法是同步的,可以理解为先同步调用listAllUser,然后转为Flux。 起始和上面是一致的,设计Mono、Flux的相关API是异步的)
[ INFO] 2022-06-04 14:39:09.673 6124 --- [ctor-http-nio-2] flux.controller.UserController : test6 start [ INFO] 2022-06-04 14:39:09.673 6124 --- [ctor-http-nio-2] flux.controller.UserController : test6 end [ INFO] 2022-06-04 14:39:14.689 6124 --- [ctor-http-nio-2] flux.controller.UserController : doCreateUser sleep end [ INFO] 2022-06-04 14:39:16.524 6124 --- [ctor-http-nio-3] flux.controller.UserController : listUsers start [ INFO] 2022-06-04 14:39:18.529 6124 --- [ctor-http-nio-3] flux.controller.UserController : listAllUser sleep end [ INFO] 2022-06-04 14:39:18.532 6124 --- [ctor-http-nio-3] flux.controller.UserController : listUsers end
3. 基于Handler 和RouteFuntion 的方式
(1) 编写handler
package flux.controller; import flux.bean.User; import flux.config.JSONResultUtil; import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; 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.util.ArrayList; import java.util.List; @Component @Slf4j public class UserHandler { public Mono<ServerResponse> listAllUser(ServerRequest serverRequest) { log.info("listAllUser start"); // 返回集合用 Flux Flux<User> tFlux = Flux.fromStream(listAllUser().stream()); Mono<ServerResponse> body = ServerResponse.ok() .contentType(MediaType.APPLICATION_JSON_UTF8) .body(tFlux, User.class); log.info("listAllUser end"); return body; } public Mono<ServerResponse> findById(ServerRequest serverRequest) { log.info("findById start"); Integer id = Integer.valueOf(serverRequest.pathVariable("id")); // id 大于10 返回资源部不存在 if (id > 10) { return ServerResponse.notFound().build(); } Mono<ServerResponse> body = ServerResponse.ok() .contentType(MediaType.APPLICATION_JSON_UTF8) .body(Mono.fromSupplier(() -> { // 否则返回元素。并且用JSONResultUtil 进行包装 User user = new User(); user.setId(5); user.setUsername("user" + id); JSONResultUtil<User> ok = JSONResultUtil.ok(); ok.setData(user); try { Thread.sleep(5 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } log.info("findById sleep end"); return ok; }), JSONResultUtil.class); log.info("findById start"); return body; } public Mono<ServerResponse> createUser(ServerRequest serverRequest) { log.info("createUser start"); Mono<ServerResponse> create = serverRequest.bodyToMono(User.class).flatMap(user1 -> { System.out.println(user1); user1.setId(1); try { Thread.sleep(5 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } log.info("create"); return Mono.just(user1); }).flatMap(temp -> ServerResponse.ok().bodyValue(temp) ); log.info("createUser end"); return create; } public Mono<ServerResponse> deleteUser(ServerRequest serverRequest) { String id = serverRequest.pathVariable("id"); System.out.println(id); // 模拟删除成功 return ServerResponse.ok() .contentType(MediaType.APPLICATION_JSON) .body(Mono.just(Boolean.TRUE), Boolean.class); } private List<User> listAllUser() { List<User> objects = new ArrayList<>(); User user = new User(); user.setAge(25); user.setUsername("username1"); User user2 = new User(); user2.setAge(25); user2.setUsername("username2"); objects.add(user); objects.add(user2); try { Thread.sleep(2 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } log.info("listAllUser sleep end"); return objects; } }
(2) 编写RouteFunction
package flux.config; import flux.controller.UserHandler; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; 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 AllRouters { @Bean RouterFunction<ServerResponse> userRouter(UserHandler handler) { RouterFunction<ServerResponse> nest = RouterFunctions.nest( // 相当于统一的前缀 RequestPredicates.path("/user"), // @GetMapping("/listAllUser") RouterFunctions.route(RequestPredicates.GET("/listAllUser"), handler::listAllUser) .andRoute(RequestPredicates.POST("/find/{id}"), handler::findById) .andRoute(RequestPredicates.POST("/createUser"), handler::createUser) .andRoute(RequestPredicates.DELETE("/{id}"), handler::deleteUser) ); return nest; } }
(3) 测试以及查看日志
curl 接口进行测试
aaa@aaadeMacBook-Pro ~ % curl -X 'GET' localhost:8081/user/listAllUser [{"id":null,"username":"username1","age":25},{"id":null,"username":"username2","age":25}]% aaa@aaadeMacBook-Pro ~ % curl -X 'GET' localhost:8081/user/find/2 {"success":true,"data":{"id":5,"username":"user2","age":null},"msg":""}% aaa@aaadeMacBook-Pro ~ % curl -X POST --header 'Content-Type: application/json' -d '{"age": 22, "username": "zhangsan"}' localhost:8081/user/createUser {"id":1,"username":"zhangsan","age":22}% aaa@aaadeMacBook-Pro ~ % curl -X 'DELETE' localhost:8081/user/2 true%
日志:
[ INFO] 2022-06-04 15:24:45.112 6259 --- [ctor-http-nio-2] flux.controller.UserHandler : listAllUser start [ INFO] 2022-06-04 15:24:47.112 6259 --- [ctor-http-nio-2] flux.controller.UserHandler : listAllUser sleep end [ INFO] 2022-06-04 15:24:47.127 6259 --- [ctor-http-nio-2] flux.controller.UserHandler : listAllUser end [ INFO] 2022-06-04 15:24:50.826 6259 --- [ctor-http-nio-3] flux.controller.UserHandler : findById start [ INFO] 2022-06-04 15:24:50.828 6259 --- [ctor-http-nio-3] flux.controller.UserHandler : findById end [ INFO] 2022-06-04 15:24:55.837 6259 --- [ctor-http-nio-3] flux.controller.UserHandler : findById sleep end [ INFO] 2022-06-04 15:25:22.159 6259 --- [ctor-http-nio-4] flux.controller.UserHandler : createUser start [ INFO] 2022-06-04 15:25:22.180 6259 --- [ctor-http-nio-4] flux.controller.UserHandler : createUser end User(id=null, username=zhangsan, age=22) [ INFO] 2022-06-04 15:25:27.233 6259 --- [ctor-http-nio-4] flux.controller.UserHandler : create 2
(4) 这种模式的参数校验
自定义异常加自定义全局异常处理器。
1》pom 增加hutu 工具包
<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.5.2</version> </dependency>
2》增加自定义异常和检测的工具类
package flux.exception; public class MyException extends RuntimeException { public MyException(String msg) { super(msg); } } package flux.exception; public class CheckUtils { public static void isTrue(boolean expression, String msg) { if (!expression) { throw new MyException(msg); } } }
3》增加全局异常处理器
package flux.config; import cn.hutool.json.JSONUtil; import flux.exception.MyException; import org.springframework.core.annotation.Order; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebExceptionHandler; import reactor.core.publisher.Mono; import java.nio.charset.StandardCharsets; @Order(-2) @Component public class ExceptionHandler implements WebExceptionHandler { @Override public Mono<Void> handle(ServerWebExchange serverWebExchange, Throwable throwable) { // 设置响应头 ServerHttpResponse response = serverWebExchange.getResponse(); response.getHeaders().setContentType(MediaType.APPLICATION_JSON); // 判断异常是已知异常还是未知异常 String errorMsg = toStr(throwable); JSONResultUtil jsonResultUtil = new JSONResultUtil(false); jsonResultUtil.setMsg(errorMsg); String jsonStr = JSONUtil.toJsonStr(jsonResultUtil); DataBuffer wrap = response.bufferFactory().wrap(jsonStr.getBytes(StandardCharsets.UTF_8)); return response.writeWith(Mono.just(wrap)); } private String toStr(Throwable throwable) { if (throwable instanceof MyException) { return throwable.getMessage(); } else { // 未知异常 return throwable.toString(); } } }
4》修改创建方法
public Mono<ServerResponse> createUser(ServerRequest serverRequest) { // springboot 2.0.0 可以用下面代码获取参数的user, 2.0.1 会报错。 所以都在flatMap 处理异常 // User block = serverRequest.bodyToMono(User.class).block(); log.info("createUser start"); Mono<ServerResponse> create = serverRequest.bodyToMono(User.class).flatMap(user1 -> { CheckUtils.isTrue(user1.getUsername() != null && !"".equals(user1.getUsername()), "username 不能为空"); CheckUtils.isTrue(user1.getAge() != null && user1.getAge() > 50, "age 不能小于50"); System.out.println(user1); user1.setId(1); try { Thread.sleep(5 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } log.info("create"); return Mono.just(user1); }).flatMap(temp -> ServerResponse.ok().bodyValue(temp) ); log.info("createUser end"); return create; }
测试:
aaa@aaadeMacBook-Pro ~ % curl -X POST --header 'Content-Type: application/json' -d '{"age": 22, "username": "zhangsan"}' localhost:8081/user/createUser {"msg":"age 不能小于50","success":false}% aaa@aaadeMacBook-Pro ~ % curl -X POST --header 'Content-Type: application/json' -d '{"age": 220, "username": "zhangsan"}' localhost:8081/user/createUser {"id":1,"username":"zhangsan","age":220}% aaa@aaadeMacBook-Pro ~ %
至此简单研究了下基于SpringMVC 原生注解编写Controller 以及基于Handler+RouteFunction 模式的简单使用,同时简单了解了下对参数的校验。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了