Spring Boot 2.0最近在GA上线了,所以我决定在相当长一段时间内写我的第一篇有关Spring的文章。 自发布以来,我已经看到越来越多的提到Spring WebFlux以及有关如何使用它的教程。 但是,在阅读完它们并尝试使它们自己工作之后,我发现很难从我所阅读的帖子和教程中包含的代码过渡到编写代码,该代码实际上比返回字符串更有趣从后端。 现在,我希望我不会因为说到您可能对本文中使用的代码有同样的批评而无视我的脚步,但这是我尝试给出一个实际上类似于Spring WebFlux的教程的尝试。您可能在野外使用的东西。
在继续之前,以及在提到WebFlux之后,实际上是什么? Spring WebFlux是Spring MVC的完全不阻塞的反应性替代方案。 它允许更好的垂直扩展,而无需增加硬件资源。 现在,它是反应性的,它利用反应性流来异步处理从调用返回到服务器的数据。 这意味着我们将看到更少的List
, Collection
或什至单个对象,而是它们的反应等效项,例如Flux
和Mono
(来自Reactor)。 我不会深入探讨什么是Reactive Streams,因为说实话,在尝试向任何人解释它之前,我需要自己更多地研究它。 相反,让我们重新关注WebFlux。
我像往常一样使用Spring Boot在本教程中编写代码。
以下是我在本文中使用的依赖项。
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-cassandra-reactive</artifactId><version>2.0.0.RELEASE</version></dependency></dependencies>
尽管我没有在上面的依赖代码片段中包含它,但是使用了spring-boot-starter-parent
,最终可以将其2.0.0.RELEASE
到2.0.0.RELEASE
版本。 作为本教程的主题是有关WebFlux的,包括spring-boot-starter-webflux
显然是一个好主意。 spring-boot-starter-data-cassandra-reactive
也已包括在内,因为我们将其用作示例应用程序的数据库,因为它是(在撰写本文时)为数不多的具有响应支持的数据库之一。 通过一起使用这些依赖关系,我们的应用程序可以从前到后完全反应。
WebFlux引入了一种不同的方式来处理请求,而不是使用Spring MVC中使用的@Controller
或@RestController
编程模型。 但是,它不能替代它。 相反,它已被更新以允许使用反应类型。 这使您可以保持与使用Spring编写时相同的格式,但是对返回类型进行一些更改,因此可以返回Flux
或Mono
。 以下是一个非常人为的示例。
@RestController
public class PersonController {private final PersonRepository personRepository;public PersonController(PersonRepository personRepository) {this.personRepository = personRepository;}@GetMapping("/people")public Flux<Person> all() {return personRepository.findAll();}@GetMapping("/people/{id}")Mono<Person> findById(@PathVariable String id) {return personRepository.findOne(id);}
}
在我看来,这看起来很熟悉,而且乍一看,它与标准的Spring MVC控制器并没有什么不同,但是在阅读完这些方法之后,我们可以看到与通常期望的不同的返回类型。 在此示例中, PersonRepository
必须是一个反应式存储库,因为我们已经能够直接返回其搜索查询的结果,以供参考,反应式存储库将为集合返回Flux
,对于单个实体返回Mono
。
注解方法不是我在本文中要关注的重点。 这对我们来说还不够酷和时髦。 没有足够的使用lambda来满足我们以更实用的方式编写Java的需求。 但是Spring WebFlux有我们的支持。 它提供了一种替代方法来路由和处理到我们服务器的请求,该方法仅使用lambda来编写路由器功能。 让我们看一个例子。
@Configuration
public class PersonRouter {@Beanpublic RouterFunction<ServerResponse> route(PersonHandler personHandler) {return RouterFunctions.route(GET("/people/{id}").and(accept(APPLICATION_JSON)), personHandler::get).andRoute(GET("/people").and(accept(APPLICATION_JSON)), personHandler::all).andRoute(POST("/people").and(accept(APPLICATION_JSON)).and(contentType(APPLICATION_JSON)), personHandler::post).andRoute(PUT("/people/{id}").and(accept(APPLICATION_JSON)).and(contentType(APPLICATION_JSON)), personHandler::put).andRoute(DELETE("/people/{id}"), personHandler::delete).andRoute(GET("/people/country/{country}").and(accept(APPLICATION_JSON)), personHandler::getByCountry);}
}
这些都是通往PersonHandler
中方法的所有路由,我们将在后面介绍。 我们创建了一个将处理路由的bean。 为了设置路由功能,我们使用了命名良好的RouterFunctions
类,为我们提供了许多静态方法,但是现在我们只对它的route
方法感兴趣。 以下是route
方法的签名。
public static <T extends ServerResponse> RouterFunction<T> route(RequestPredicate predicate, HandlerFunction<T> handlerFunction) {// stuff
}
该方法显示,它与HandlerFunction
一起接收RequestPredicate
并输出RouterFunction
。
RequestPredicate
是我们用来指定路由行为的内容,例如处理程序函数的路径,请求的类型以及可接受的输入类型。 由于我使用静态导入来使所有内容读起来更加清晰,因此一些重要信息已对您隐藏了。 要创建RequestPredicate
我们应该使用RequestPredicates
(复数),这是一个静态帮助器类,为我们提供了所需的所有方法。 我个人确实建议静态导入RequestPredicates
否则由于您可能需要使用RequestPredicates
静态方法的次数而使代码混乱。 在上面的示例中, GET
, POST
, PUT
, DELETE
, accept
和contentType
都是静态RequestPredicates
方法。
下一个参数是HandlerFunction
,它是一个功能接口。 这里有3条重要信息,它具有<T extends ServerResponse>
的通用类型,其handle
方法返回Mono<T>
并接受ServerRequest
。 使用这些,我们可以确定我们需要传递一个返回Mono<ServerResponse>
(或其子类型之一)的函数。 显然,这对我们的处理程序函数返回的内容施加了严格的约束,因为它们必须满足此要求,否则将不适合以这种格式使用。
最后,输出是RouterFunction
。 然后可以将其返回,并将其用于路由到我们指定的任何函数。 但是通常我们希望将许多不同的请求立即路由到各种处理程序,WebFlux可以满足这些请求。 由于route
返回RouterFunction
和事实RouterFunction
也有提供自己的路由方法, andRoute
,我们可以链通话在一起,不断增加的所有额外的路线,我们需要。
如果再回顾一下上面的PersonRouter
示例,我们可以看到这些方法以REST动词(例如GET
和POST
命名,它们定义了处理程序将采用的请求的路径和类型。 例如,如果我们以第一个GET
请求为例,它将使用路径变量名称id
(由{id}
表示的路径变量)路由到/people
,并且返回内容的类型,特别是APPLICATION_JSON
( MediaType
静态字段)使用accept
方法。 如果使用其他路径,则不会处理。 如果路径正确,但是Accept标头不是接受的类型之一,则请求将失败。
在继续之前,我想contentType
一下accept
和contentType
方法。 这两个设置的请求标头都accept
对Accept标头的匹配,将contentType
匹配到Content-Type。 Accept标头定义了响应可接受的媒体类型,因为我们返回的Person
对象的JSON表示将其设置为APPLICATION_JSON
(实际标头中为application/json
)。 Content-Type具有相同的想法,但是描述的是发送的请求正文中的媒体类型。 这就是为什么只有POST
和PUT
动词包含contentType
的原因,其他动词的主体中没有任何动词。 DELETE
不包含accept
和contentType
因此我们可以得出结论,既不希望返回任何内容,也不希望在其请求正文中包含任何内容。
现在我们知道了如何设置路由,让我们看一下编写处理传入请求的处理程序方法。 以下是处理来自先前示例中定义的路由的所有请求的代码。
@Component
public class PersonHandler {private final PersonManager personManager;public PersonHandler(PersonManager personManager) {this.personManager = personManager;}public Mono<ServerResponse> get(ServerRequest request) {final UUID id = UUID.fromString(request.pathVariable("id"));final Mono<Person> person = personManager.findById(id);return person.flatMap(p -> ok().contentType(APPLICATION_JSON).body(fromPublisher(person, Person.class))).switchIfEmpty(notFound().build());}public Mono<ServerResponse> all(ServerRequest request) {return ok().contentType(APPLICATION_JSON).body(fromPublisher(personManager.findAll(), Person.class));}public Mono<ServerResponse> put(ServerRequest request) {final UUID id = UUID.fromString(request.pathVariable("id"));final Mono<Person> person = request.bodyToMono(Person.class);return personManager.findById(id).flatMap(old ->ok().contentType(APPLICATION_JSON).body(fromPublisher(person.map(p -> new Person(p, id)).flatMap(p -> personManager.update(old, p)),Person.class))).switchIfEmpty(notFound().build());}public Mono<ServerResponse> post(ServerRequest request) {final Mono<Person> person = request.bodyToMono(Person.class);final UUID id = UUID.randomUUID();return created(UriComponentsBuilder.fromPath("people/" + id).build().toUri()).contentType(APPLICATION_JSON).body(fromPublisher(person.map(p -> new Person(p, id)).flatMap(personManager::save), Person.class));}public Mono<ServerResponse> delete(ServerRequest request) {final UUID id = UUID.fromString(request.pathVariable("id"));return personManager.findById(id).flatMap(p -> noContent().build(personManager.delete(p))).switchIfEmpty(notFound().build());}public Mono<ServerResponse> getByCountry(ServerRequest serverRequest) {final String country = serverRequest.pathVariable("country");return ok().contentType(APPLICATION_JSON).body(fromPublisher(personManager.findAllByCountry(country), Person.class));}
}
值得注意的一件事是缺少注释。 PersonHandler
@Component
注释自动创建PersonHandler
bean,没有其他Spring注释。
我试图将大多数存储库逻辑都排除在此类之外,并通过委派给它所包含的PersonRepository
的PersonManager
来隐藏对实体对象的任何引用。 如果您对PersonManager
中的代码感兴趣,那么可以在我的GitHub上看到它,本文将不再对此进行进一步的解释,因此我们可以专注于WebFlux本身。
好的,回到手头的代码。 让我们仔细看看get
和post
方法,以了解发生了什么。
public Mono<ServerResponse> get(ServerRequest request) {final UUID id = UUID.fromString(request.pathVariable("id"));final Mono<Person> person = personManager.findById(id);return person.flatMap(p -> ok().contentType(APPLICATION_JSON).body(fromPublisher(person, Person.class))).switchIfEmpty(notFound().build());
}
此方法用于从支持此示例应用程序的数据库中检索一条记录。 由于Cassandra是首选数据库,因此我决定对每个记录的主键使用UUID
,这具有使测试示例更加烦人的不幸效果,但是有些复制和粘贴无法解决。
请记住,此GET
请求的路径中包含一个路径变量。 在传递给该方法的ServerRequest
上使用pathVariable
方法,我们可以通过提供变量名称(在本例中为id
来提取其值。 然后将ID转换为UUID
,如果字符串的格式不正确,则将引发异常,我决定忽略此问题,以使示例代码不会更混乱。
获得ID后,我们可以查询数据库中是否存在匹配记录。 返回一个Mono<Person>
,它包含映射到Person
的现有记录,或者保留为空的Mono
。
使用返回的Mono
我们可以根据它的存在输出不同的响应。 这意味着我们可以将有用的状态代码与主体内容一起返回给客户端。 如果记录存在,则flatMap
返回状态为OK
的ServerResponse
。 除了此状态外,我们还希望输出记录,为此,我们指定主体的内容类型(在本例中为APPLICATION_JSON
,然后将记录添加到其中。 fromPublisher
将我们的Mono<Person>
(它是Publisher
)与Person
类一起使用,因此它知道它映射到主体中的内容。 fromPublisher
是BodyInserters
类中的静态方法。
如果该记录不存在,那么该流程将移至switchIfEmpty
块并返回NOT FOUND
状态。 由于找不到任何内容,因此可以将主体保留为空,因此我们只需要创建其中的ServerResponse
。
现在进入post
处理程序。
public Mono<ServerResponse> post(ServerRequest request) {final Mono<Person> person = request.bodyToMono(Person.class);final UUID id = UUID.randomUUID();return created(UriComponentsBuilder.fromPath("people/" + id).build().toUri()).contentType(APPLICATION_JSON).body(fromPublisher(person.map(p -> new Person(p, id)).flatMap(personManager::save), Person.class));
}
即使只是从第一行开始,我们也可以看到它与get
方法的工作方式已经不同。 由于这是一个POST
请求,因此需要从请求的主体中接受我们要保留的对象。 当我们尝试插入单个记录时,我们将使用请求的bodyToMono
方法从正文中检索Person
。 如果要处理多个记录,则可能要使用bodyToFlux
。
我们将使用created
方法返回CREATED
状态,该方法采用URI
来确定插入记录的路径。 然后,通过使用fromPublisher
方法将新记录添加到响应的正文中,从而遵循与get
方法类似的设置。 构成Publisher
的代码略有不同,但是输出仍然是Mono<Person>
,这很重要。 只是为了进一步说明如何完成插入,使用我们生成的UUID
将请求中传入的Person
映射到新的Person
,然后通过调用flatMap
save
其传递到save
。 通过创建一个新的Person
我们仅将值插入我们允许的Cassandra中,在这种情况下,我们不希望UUID
从请求主体传入。
因此,关于处理程序,就是这样。 显然,还有其他一些我们没有经历过的方法。 它们的工作方式不同,但是都遵循相同的概念,即返回ServerResponse
,该ServerResponse
包含一个适当的状态代码和正文中的记录(如果需要)。
现在,我们已经编写了运行基本Spring WebFlux后端所需的所有代码。 剩下的就是将所有配置捆绑在一起,这对于Spring Boot来说很容易。
@SpringBootApplication
public class Application {public static void main(String args[]) {SpringApplication.run(Application.class);}
}
而不是在这里结束帖子,我们可能应该研究如何实际使用代码。
Spring提供了WebClient
类来处理请求而不会阻塞。 现在,我们可以利用它作为测试应用程序的方法,尽管这里也可以使用WebTestClient
。 创建响应式应用程序时,将使用WebClient
而不是阻塞RestTemplate
。
下面的代码调用了PersonHandler
中定义的处理程序。
public class Client {private WebClient client = WebClient.create("http://localhost:8080");public void doStuff() {// POSTfinal Person record = new Person(UUID.randomUUID(), "John", "Doe", "UK", 50);final Mono<ClientResponse> postResponse =client.post().uri("/people").body(Mono.just(record), Person.class).accept(APPLICATION_JSON).exchange();postResponse.map(ClientResponse::statusCode).subscribe(status -> System.out.println("POST: " + status.getReasonPhrase()));// GETclient.get().uri("/people/{id}", "a4f66fe5-7c1b-4bcf-89b4-93d8fcbc52a4").accept(APPLICATION_JSON).exchange().flatMap(response -> response.bodyToMono(Person.class)).subscribe(person -> System.out.println("GET: " + person));// ALLclient.get().uri("/people").accept(APPLICATION_JSON).exchange().flatMapMany(response -> response.bodyToFlux(Person.class)).subscribe(person -> System.out.println("ALL: " + person));// PUTfinal Person updated = new Person(UUID.randomUUID(), "Peter", "Parker", "US", 18);client.put().uri("/people/{id}", "ec2212fc-669e-42ff-9c51-69782679c9fc").body(Mono.just(updated), Person.class).accept(APPLICATION_JSON).exchange().map(ClientResponse::statusCode).subscribe(response -> System.out.println("PUT: " + response.getReasonPhrase()));// DELETEclient.delete().uri("/people/{id}", "ec2212fc-669e-42ff-9c51-69782679c9fc").exchange().map(ClientResponse::statusCode).subscribe(status -> System.out.println("DELETE: " + status));}
}
不要忘记在某个地方实例化Client
,下面是一种不错的懒惰方式!
@SpringBootApplication
public class Application {public static void main(String args[]) {SpringApplication.run(Application.class);Client client = new Client();client.doStuff();}
}
首先,我们创建WebClient
。
private final WebClient client = WebClient.create("http://localhost:8080");
一旦创建,我们就可以开始使用它做事,因此可以使用doStuff
方法。
让我们分解一下发送到后端的POST
请求。
final Mono<ClientResponse> postResponse =client.post().uri("/people").body(Mono.just(record), Person.class).accept(APPLICATION_JSON).exchange();
postResponse.map(ClientResponse::statusCode).subscribe(status -> System.out.println("POST: " + status.getReasonPhrase()));
我将此内容写下的略有不同,因此您可以看到从发送请求返回了Mono<ClientResponse>
。 exchange
方法将HTTP请求发送到服务器。 然后,无论何时到达,都会处理该响应。
当然,使用WebClient
我们指定我们要使用post
方法发送POST
请求。 的URI
,然后用所添加的uri
的方法(重载的方法,这一个发生在一个String
,但另一个接受一个URI
)。 我不敢说此方法执行了该方法所要求的操作,然后将正文内容与Accept标头一起添加。 最后,我们通过调用exchange
发送请求。
请注意, APPLICATION_JSON
的媒体类型与POST
路由器功能中定义的类型匹配。 如果我们要发送其他类型,请说TEXT_PLAIN
,则将收到404
错误,因为不存在与请求返回的请求相匹配的处理程序。
使用通过调用exchange
返回的Mono<ClientResponse>
,我们可以将其内容映射到所需的输出。 在上面的示例中,状态代码将打印到控制台。 如果我们回想一下PersonHandler
的post
方法,请记住它只能返回“已创建”状态,但是如果发送的请求与之不正确匹配,则将打印出“未找到”。
让我们看一下其他请求之一。
client.get().uri("/people/{id}", "a4f66fe5-7c1b-4bcf-89b4-93d8fcbc52a4").accept(APPLICATION_JSON).exchange().flatMap(response -> response.bodyToMono(Person.class)).subscribe(person -> System.out.println("GET: " + person));
这是我们的典型GET
请求。 它看起来与我们刚经历的POST
请求非常相似。 主要区别在于uri
将请求和UUID
的路径(在这种情况下为String
)都作为参数,它将替换路径变量{id}
,并且正文保留为空。 响应的处理方式也不同。 在此示例中,它提取响应的正文并将其映射到Mono<Person>
并打印出来。 可以使用前面的POST
示例完成此操作,但是响应的状态代码在该场景中更为有用。
从稍微不同的角度来看,我们可以使用cURL发出请求并查看响应的外观。
CURL -H "Accept:application/json" -i localhost:8080/people
HTTP/1.1 200 OK
transfer-encoding: chunked
Content-Type: application/json[{"id": "13c403a2-6770-4174-8b76-7ba7b75ef73d","firstName": "John","lastName": "Doe","country": "UK","age": 50},{"id": "fbd53e55-7313-4759-ad74-6fc1c5df0986","firstName": "Peter","lastName": "Parker","country": "US","age": 50}
]
响应看起来像这样,显然,它会根据您存储的数据而有所不同。
注意响应头。
transfer-encoding: chunked
Content-Type: application/json
此处的transfer-encoding
表示可分块传输的数据,可用于传输数据。 这就是我们所需要的,以便客户端可以对返回的数据做出反应。
我认为这应该是一个停止的好地方。 我们在这里介绍了很多资料,希望可以帮助您更好地理解Spring WebFlux。 我还想介绍WebFlux的其他一些主题,但是我将在单独的文章中介绍这些主题,因为我认为这足够长了。
总之,在本文中,我们非常简要地讨论了为什么要在典型的Spring MVC后端上使用Spring WebFlux。 然后,我们研究了如何设置路由和处理程序以处理传入的请求。 处理程序实现了可以处理大多数REST动词的方法,并在响应中返回了正确的数据和状态代码。 最后,我们研究了两种向后端发出请求的方法,一种是使用WebClient
直接在客户端上处理输出,另一种是通过cURL查看返回的JSON的外观。
如果您有兴趣查看我用来创建本文示例应用程序的其余代码,可以在我的GitHub上找到它。
与往常一样,如果您发现此帖子有帮助,请分享它;如果您想了解我的最新帖子,则可以通过Twitter @LankyDanDev关注我。
翻译自: https://www.javacodegeeks.com/2018/03/doing-stuff-with-spring-webflux.html