spring的webflux初探
不久前, spring進行了較大的改動, 主要目的是爲了增加對響應式編程的支持.
spring 默認是採用了reactor項目作爲響應式編程(reactive programming)的支持, 我也以此作爲基礎來談.
reactor項目地址: https://github.com/reactor/reactor
爲什麼要reactor
總的來說, reactor也是一個用於編寫異步代碼的庫, 衆所周知, 對於同步程序來說, 有IO耗時長之類的開銷. 所以人們不斷的推崇使用異步的方式來編寫一些代碼, 而java也提供了編寫異步程序的方法給開發者, 那麼我們爲什麼需要reactor. 就我短時間的使用體驗來說, reactor使我們編寫異步代碼變得更加簡單快捷, 讓某項工作更加簡單或讓其更有效率, 我覺得就是一個庫應該解決的問題, 顯然reactor做到了, 在使用了reactor後, 你就再也不用寫callback那種又臭又長的麪條代碼了, 代碼的可讀性與可維護性大大加強了. 相比於future, reactor又提供了更多功能齊全的操作, 編程複雜的也大大降低
好了, 我們並不是來介紹reactor的, 更多有關reactor的資料以及它與jvm其他異步方式的對比請參考reactor文檔: http://projectreactor.io/docs/core/release/reference
webflux實例
- webflux與webmvc的類比
webmvc | webflux |
---|---|
controller | handler |
request mapping | router |
* pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.edu.ncu</groupId>
<artifactId>reactive-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>reactive-demo</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.M7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-webflux -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mindrot/jbcrypt -->
<dependency>
<groupId>org.mindrot</groupId>
<artifactId>jbcrypt</artifactId>
<version>0.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.44</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
- 編寫handler
先寫個hello world handler練練手
package cn.edu.ncu.reactivedemo.handlers;
@Service
public class HelloWorldHandler {
public Mono<ServerResponse> helloWorld(ServerRequest request){
return ServerResponse.ok()
.contentType(MediaType.TEXT_PLAIN)
.body(BodyInserters.fromObject("hello world"));
}
}
- 註冊路由
將寫好的handler註冊到路由上
package cn.edu.ncu.reactivedemo;
@Configuration
public class Router {
@Autowired private HelloWorldHandler helloWorldHandler;
@Autowired private UserHandler userHandler;
@Bean
public RouterFunction<?> routerFunction(){
return RouterFunctions.route(RequestPredicates.GET("/hello"), helloWorldHandler::helloWorld);
}
}
- 啓動類
默認採用netty作爲reactor的底層啓動
package cn.edu.ncu.reactivedemo;
@SpringBootApplication
public class ReactiveDemoApplication {
public static void main(String[] args) {
SpringApplication.run(ReactiveDemoApplication.class, args);
}
}
訪問http://127.0.0.1:8080/hello
返回hello world表示成功
使用數據庫
暫時支持reactive編程的數據庫只有MongoDB, redis, Cassandra, Couchbase
我們直接採用redis作爲測試, 做一個簡陋的註冊登錄的接口就行了
* 配置redis
package cn.edu.ncu.reactivedemo.config;
@Configuration
public class RedisConfig {
@Autowired
private RedisConnectionFactory factory;
@Bean
public ReactiveRedisTemplate<String, String> reactiveRedisTemplate(ReactiveRedisConnectionFactory connectionFactory){
return new ReactiveRedisTemplate<String, String>(connectionFactory, RedisSerializationContext.string());
}
@Bean
public ReactiveRedisConnection connection(ReactiveRedisConnectionFactory connectionFactory){
return connectionFactory.getReactiveConnection();
}
public @PreDestroy void flushDb(){
factory.getConnection().flushDb();
}
}
- 測試redis接口
package cn.edu.ncu.reactivedemo;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = ReactiveDemoApplication.class)
public class RedisTests {
@Autowired
private ReactiveRedisConnection connection;
@Test
public void testRedis(){
connection
.stringCommands().set(ByteBuffer.wrap("h".getBytes()), ByteBuffer.wrap("w".getBytes()))
.subscribe(System.out::println);
}
}
- 編寫userHandler
package cn.edu.ncu.reactivedemo.handlers;
@Service
public class UserHandler {
@Autowired
private ReactiveRedisConnection connection;
public Mono<ServerResponse> register(ServerRequest request) {
Mono<Map> body = request.bodyToMono(Map.class);
return body.flatMap(map -> {
String username = (String) map.get("username");
String password = (String) map.get("password");
String hashedPassword = BCrypt.hashpw(password, BCrypt.gensalt());
return connection.stringCommands()
.set(ByteBuffer.wrap(username.getBytes()), ByteBuffer.wrap(hashedPassword.getBytes()));
}).flatMap(aBoolean -> {
Map<String, String> result = new HashMap<>();
ServerResponse serverResponse = null;
if (aBoolean){
result.put("message", "successful");
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject(result));
}else {
result.put("message", "failed");
return ServerResponse.status(HttpStatus.BAD_REQUEST)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject(request));
}
});
}
public Mono<ServerResponse> login(ServerRequest request){
Mono<Map> body = request.bodyToMono(Map.class);
return body.flatMap(map -> {
String username = (String) map.get("username");
String password = (String) map.get("password");
return connection.stringCommands().get(ByteBuffer.wrap(username.getBytes())).flatMap(byteBuffer -> {
byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get(bytes, 0, bytes.length);
String hashedPassword = null;
try {
hashedPassword = new String(bytes, "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
Map<String, String> result = new HashMap<>();
if (hashedPassword == null || !BCrypt.checkpw(password, hashedPassword)){
result.put("message", "賬號或密碼錯誤");
return ServerResponse.status(HttpStatus.UNAUTHORIZED)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject(result));
}else {
result.put("token", "假token");
return ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject(result));
}
});
});
}
}
- 添加router
package cn.edu.ncu.reactivedemo;
@Configuration
public class Router {
@Autowired private HelloWorldHandler helloWorldHandler;
@Autowired private UserHandler userHandler;
@Bean
public RouterFunction<?> routerFunction(){
return RouterFunctions.route(RequestPredicates.GET("/hello"), helloWorldHandler::helloWorld)
.andRoute(RequestPredicates.POST("/register"), userHandler::register)
.andRoute(RequestPredicates.POST("/login"), userHandler::login);
}
}
接口很粗糙,沒有寫model層, 也沒有數據驗證, 測試也直接用http requester進行測試了
參考:
https://spring.io/blog/2016/11/28/going-reactive-with-spring-data
http://projectreactor.io/docs/core/release/reference/
http://projectreactor.io/docs/core/release/api/
https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html#webflux-fn-handler-functions