文章目录
在 Spring 5 之前,如果我们想要调用其他系统提供的 HTTP 服务,通常可以使用 Spring 提供的 RestTemplate 来访问,不过由于 RestTemplate 是 Spring 3 中引入的同步阻塞式 HTTP 客户端,因此存在一定性能瓶颈。根据 Spring 官方文档介绍,在将来的版本中它可能会被弃用。
WebClient是Spring WebFlux模块提供的一个非阻塞的基于响应式编程的进行Http请求的客户端工具,从Spring5.0开始提供。
WebClient有一个基于Reactor的功能性的、流畅的API,它支持异步逻辑的声明式组合,而无需处理线程或并发性。它是完全无阻塞的,支持流,并且依赖于同样的编解码器,这些编解码器也用于在服务器端编码和解码请求和响应内容。
(1)、创建WebClient
# 创建WebClient
WebClient.create()
# 创建WebClient并且指定baseURL
WebClient.create(String baseUrl)
(2)、指定额外配置
可以使用WebClient.builder() 指定额外的配置。
例如:
WebClient.builder(...)
.uriBuilderFactory(...)
.defaultCookie(...)
.defaultHeaders(...)
.build();
(3)、不变性
一旦构建完成,WebClient就是不可变的。但是,可以克隆它并构建一个修改后的副本,如下所示:
WebClient client1 = WebClient.builder()
.filter(filterA).filter(filterB).build();
WebClient client2 = client1.mutate()
.filter(filterC).filter(filterD).build();
// client1 has filterA, filterB
// client2 has filterA, filterB, filterC, filterD
为了避免应用程序内存问题,编解码器对内存中的数据缓存有限制。默认情况下,它们被设置为256KB。如果这还不够,你会得到以下错误:
org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer
若要更改默认编解码器的限制,请使用以下方法:
WebClient webClient = WebClient.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
.build();
要自定义Reactor Netty设置,请提供一个预配置的HttpClient:
HttpClient httpClient = HttpClient.create().secure(sslSpec -> ...);
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
(1)、资源(Resources)
默认情况下,HttpClient参与reactor.netty.http.HttpResources中保存的全局Reactor Netty资源,包括事件循环线程和连接池。这是推荐的模式,因为固定的共享资源是事件循环并发的首选。在这种模式下,全局资源保持活动状态,直到进程退出。
如果服务器与进程同步,则通常不需要显式关闭。然而,如果服务器可以在进程内启动或停止(例如,一个部署为WAR的Spring MVC应用程序),你可以声明一个类型为ReactorResourceFactory的Spring管理bean,使用globalResources=true(默认值)来确保当Spring ApplicationContext关闭时,Reactor Netty全局资源被关闭,如下例所示:
@Bean
public ReactorResourceFactory reactorResourceFactory() {
return new ReactorResourceFactory();
}
您也可以选择不参与Reactor Netty的全局资源。然而,在这种模式下,确保所有Reactor Netty客户端和服务器实例使用共享资源的责任在您身上,如下面的示例所示:
// 1.创建独立于全局资源的资源
@Bean
public ReactorResourceFactory resourceFactory() {
ReactorResourceFactory factory = new ReactorResourceFactory();
factory.setUseGlobalResources(false);
return factory;
}
@Bean
public WebClient webClient() {
Function<HttpClient, HttpClient> mapper = client -> {
// Further customizations...
};
// 2.将ReactorClientHttpConnector构造函数与资源工厂一起使用。
ClientHttpConnector connector =
new ReactorClientHttpConnector(resourceFactory(), mapper);
// 3.将连接器插入WebClient.Builder。
return WebClient.builder().clientConnector(connector).build();
}
(2)、超时时间设定(Timeouts)
// 配置连接超时
import io.netty.channel.ChannelOption;
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
// 分别配置读取或写入超时
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
HttpClient httpClient = HttpClient.create()
.doOnConnected(conn -> conn
.addHandlerLast(new ReadTimeoutHandler(10))
.addHandlerLast(new WriteTimeoutHandler(10)));
// Create WebClient...
// 为所有请求配置响应超时
HttpClient httpClient = HttpClient.create()
.responseTimeout(Duration.ofSeconds(2));
// Create WebClient...
// 为特定请求配置响应超时
WebClient.create().get()
.uri("https://example.org/path")
.httpRequest(httpRequest -> {
HttpClientRequest reactorRequest = httpRequest.getNativeRequest();
reactorRequest.responseTimeout(Duration.ofSeconds(2));
})
.retrieve()
.bodyToMono(String.class);
// 使用Jetty
HttpClient httpClient = new HttpClient();
httpClient.setCookieStore(...);
WebClient webClient = WebClient.builder()
.clientConnector(new JettyClientHttpConnector(httpClient))
.build();
默认情况下,HttpClient创建自己的资源(Executor、ByteBufferPool、Scheduler),这些资源保持活动状态,直到进程退出或调用stop()为止。
你可以在Jetty客户端(和服务器)的多个实例之间共享资源,并通过声明一个类型为JettyResourceFactory的Spring管理bean来确保当Spring ApplicationContext关闭时资源被关闭,如下例所示:
@Bean
public JettyResourceFactory resourceFactory() {
return new JettyResourceFactory();
}
@Bean
public WebClient webClient() {
HttpClient httpClient = new HttpClient();
// Further customizations...
// 1.使用资源工厂的JettyClientHttpConnector构造函数
ClientHttpConnector connector =
new JettyClientHttpConnector(httpClient, resourceFactory());
// 2.将连接器插入WebClient.Builder
return WebClient.builder().clientConnector(connector).build();
}
// 使用自定义Apache HttpComponents的 HttpClient
HttpAsyncClientBuilder clientBuilder = HttpAsyncClients.custom();
clientBuilder.setDefaultRequestConfig(...);
CloseableHttpAsyncClient client = clientBuilder.build();
ClientHttpConnector connector = new HttpComponentsClientHttpConnector(client);
WebClient webClient = WebClient.builder().clientConnector(connector).build();
// retrieve()方法可用于声明如何提取响应,将响应转为ResponseEntity。
WebClient client = WebClient.create("https://example.org");
Mono<ResponseEntity<Person>> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntity(Person.class);
// 也可以用来提取body体,将body内容转为指定类型的对象
WebClient client = WebClient.create("https://example.org");
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(Person.class);
// 默认情况下,4xx或5xx响应会导致WebClientResponseException,包括特定HTTP状态代码的子类。若要自定义错误响应的处理,请使用onStatus处理程序,如下所示
Mono<Person> result = client.get()
.uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> ...)
.onStatus(HttpStatus::is5xxServerError, response -> ...)
.bodyToMono(Person.class);
exchangeToMono()和exchangeToFlux()方法适用于需要更多控制的更高级情况,例如根据响应状态对响应进行不同的解码
Mono<Person> entityMono = client.get()
.uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(Person.class);
}
else {
// Turn to error
return response.createException().flatMap(Mono::error);
}
});
当使用上述方法时,在返回的Mono或Flux完成后,检查响应体,如果没有被使用,则释放它以防止内存和连接泄漏。因此,该响应不能在下游进一步解码。如果需要,由提供的函数来声明如何解码响应。
// 请求体可以从ReactiveAdapterRegistry处理的任何异步类型进行编码
Mono<Person> personMono = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.body(personMono, Person.class)
.retrieve()
.bodyToMono(Void.class);
// 对对象流进行编码
Flux<Person> personFlux = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_STREAM_JSON)
.body(personFlux, Person.class)
.retrieve()
.bodyToMono(Void.class);
// 如果有实际值,可以使用bodyValue快捷方法
Person person = ... ;
Mono<Void> result = client.post()
.uri("/persons/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(person)
.retrieve()
.bodyToMono(Void.class);
要发送表单数据,可以提供一个MultiValueMap<String,String >作为主体。注意,内容是由FormHttpMessageWriter自动设置为application/x-www-form-urlencoded的。以下示例显示了如何使用MultiValueMap<String,String >:
MultiValueMap<String, String> formData = ... ;
Mono<Void> result = client.post()
.uri("/path", id)
.bodyValue(formData)
.retrieve()
.bodyToMono(Void.class);
// 使用BodyInserters提供内嵌的表单数据
import static org.springframework.web.reactive.function.BodyInserters.*;
Mono<Void> result = client.post()
.uri("/path", id)
.body(fromFormData("k1", "v1").with("k2", "v2"))
.retrieve()
.bodyToMono(Void.class);
要发送多部分数据,需要提供一个MultiValueMap<String,?>其值是表示部件内容的对象实例或表示部件内容和标题的HttpEntity实例。MultipartBodyBuilder提供了一个方便的API来准备多部分请求。以下示例显示了如何创建多值映射< String,?>:
MultipartBodyBuilder builder = new MultipartBodyBuilder();
builder.part("fieldPart", "fieldValue");
builder.part("filePart1", new FileSystemResource("...logo.png")); // 本地文件
builder.part("jsonPart", new Person("Jason"));
builder.part("myPart", part); // Part from a server request
MultiValueMap<String, HttpEntity<?>> parts = builder.build();
在大多数情况下,不必为每个部分指定内容类型。内容类型是根据选择用来序列化它的HttpMessageWriter自动确定的,或者在资源的情况下,是根据文件扩展名确定的。如有必要,可以通过重载的构建器部件方法之一显式地提供用于每个部件的MediaType。
准备好多值映射后,将它传递给WebClient的最简单方法是通过body方法,如下例所示:
MultipartBodyBuilder builder = ...;
Mono<Void> result = client.post()
.uri("/path", id)
.body(builder.build())
.retrieve()
.bodyToMono(Void.class);
如果MultiValueMap包含至少一个非字符串值,也可以表示常规的表单数据(即application/x-www-form-urlencoded),则不需要将Content-Type设置为multipart/form-data。使用MultipartBodyBuilder时总是如此,这确保了HttpEntity包装器。
作为MultipartBodyBuilder的替代方法,还可以通过内置的BodyInserters以内嵌样式提供多部分内容,如下例所示:
import static org.springframework.web.reactive.function.BodyInserters.*;
Mono<Void> result = client.post()
.uri("/path", id)
.body(fromMultipartData("fieldPart", "value").with("filePart", resource))
.retrieve()
.bodyToMono(Void.class);
// 也可以指定MultiValueMap
MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
param.add("timestamp", time);
param.add("file", new FileSystemResource(tempFile));
// post方式
result = client.post()
.uri(/path)
.body(BodyInserters.fromMultipartData(param))
.retrieve()
.bodyToMono(Void.class)
通过WebClient注册客户端过滤器(ExchangeFilterFunction)。生成器来拦截和修改请求,如下例所示:
WebClient client = WebClient.builder()
.filter((request, next) -> {
ClientRequest filtered = ClientRequest.from(request)
.header("foo", "bar")
.build();
return next.exchange(filtered);
})
.build();
这可以用于跨领域的问题,比如身份验证。以下示例通过静态工厂方法使用筛选器进行基本身份验证:
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
WebClient client = WebClient.builder()
.filter(basicAuthentication("user", "password"))
.build();
可以通过改变现有的WebClient实例来添加或删除过滤器,从而产生不影响原始实例的新WebClient实例。例如:
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
WebClient client = webClient.mutate()
.filters(filterList -> {
filterList.add(0, basicAuthentication("user", "password"));
})
.build();
WebClient是围绕过滤器链的一个瘦门面,后面跟着一个ExchangeFunction。它提供了一个工作流来发出请求,对更高级别的对象进行编码,并帮助确保总是使用响应内容。当过滤器以某种方式处理响应时,必须格外小心,始终使用其内容,或者将它向下游传播到WebClient,这将确保相同的内容。下面是一个过滤器,它处理未授权的状态代码,但确保发布任何响应内容,无论是否是预期的:
public ExchangeFilterFunction renewTokenFilter() {
return (request, next) -> next.exchange(request).flatMap(response -> {
if (response.statusCode().value() == HttpStatus.UNAUTHORIZED.value()) {
return response.releaseBody()
.then(renewToken())
.flatMap(token -> {
ClientRequest newRequest = ClientRequest.from(request).build();
return next.exchange(newRequest);
});
} else {
return Mono.just(response);
}
});
}
// 通过过滤器链传递信息并影响过滤器对给定请求的行为
WebClient client = WebClient.builder()
.filter((request, next) -> {
Optional<Object> usr = request.attribute("myAttribute");
// ...
})
.build();
// 向请求添加属性
client.get().uri("https://example.org/")
.attribute("myAttribute", "...")
.retrieve()
.bodyToMono(Void.class);
}
可以在WebClient上全局配置defaultRequest回调。构建器级别,允许将属性插入到所有请求中,例如,可以在Spring MVC应用程序中使用它来基于ThreadLocal数据填充请求属性。
属性提供了一种向过滤器链传递信息的便捷方式,但是它们只影响当前的请求。如果您想要传递传播到嵌套的附加请求的信息,例如通过flatMap,或者之后执行的信息,例如通过concatMap,那么您将需要使用Reactor上下文。
反应器上下文需要在反应链的末端填充,以便应用于所有操作。例如:
WebClient client = WebClient.builder()
.filter((request, next) ->
Mono.deferContextual(contextView -> {
String value = contextView.get("foo");
// ...
}))
.build();
client.get().uri("https://example.org/")
.retrieve()
.bodyToMono(String.class)
.flatMap(body -> {
// perform nested request (context propagates automatically)...
})
.contextWrite(context -> context.put("foo", ...));
// 通过在结果的末尾阻塞,可以在同步模式下使用WebClient:
Person person = client.get().uri("/person/{id}", i).retrieve()
.bodyToMono(Person.class)
.block();
List<Person> persons = client.get().uri("/persons").retrieve()
.bodyToFlux(Person.class)
.collectList()
.block();
// 如果需要进行多次调用,更有效的方法是避免单独阻塞每个响应,而是等待综合结果:
Mono<Person> personMono = client.get().uri("/person/{id}", personId)
.retrieve().bodyToMono(Person.class);
Mono<List<Hobby>> hobbiesMono = client.get().uri("/person/{id}/hobbies", personId)
.retrieve().bodyToFlux(Hobby.class).collectList();
Map<String, Object> data = Mono.zip(personMono, hobbiesMono, (person, hobbies) -> {
Map<String, String> map = new LinkedHashMap<>();
map.put("person", person);
map.put("hobbies", hobbies);
return map;
})
.block();
以上仅仅是一个例子。有许多其他的模式和操作符可以组合成一个反应式的管道,进行许多远程调用,可能是嵌套的、相互依赖的,直到最后都不会阻塞。
有了Flux或Mono,你就永远不必在Spring MVC或Spring WebFlux控制器中阻塞。只需从控制器方法返回结果反应类型。同样原理也适用于Kotlin协同程序和Spring WebFlux,只需在控制器方法中使用暂停函数或返回流。
https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-client
我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div
我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看rubyzip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d
类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc
很好奇,就使用rubyonrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提
假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于
我正在尝试使用ruby和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我
关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。
我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t
我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h
我想为Heroku构建一个Rails3应用程序。他们使用Postgres作为他们的数据库,所以我通过MacPorts安装了postgres9.0。现在我需要一个postgresgem并且共识是出于性能原因你想要pggem。但是我对我得到的错误感到非常困惑当我尝试在rvm下通过geminstall安装pg时。我已经非常明确地指定了所有postgres目录的位置可以找到但仍然无法完成安装:$envARCHFLAGS='-archx86_64'geminstallpg--\--with-pg-config=/opt/local/var/db/postgresql90/defaultdb/po