访问 Web 资源:RestTemplate 、WebClient
通过 RestTemplate 访问 Web 资源
Spring Boot 中没有自动配置 RestTemplate;
Spring Boot 提供了 RestTemplateBuilder:RestTemplateBuilder.build()
常用方法
- GET 请求:
getForObject() / getForEntity()
- POST 请求:
postForObject() / postForEntity()
- PUT 请求:
put()
- DELETE 请求:
delete()
构造 URI
- 构造 URI:
UriComponentsBuilder
- 构造相对于当前请求的 URI:
ServletUriComponentsBuilder
- 构造指向 Controller 的 URI:
MvcUriComponentsBuilder
示例
Coffee
package geektime.spring.springbucks.customer.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Coffee implements Serializable {
private Long id;
private String name;
private BigDecimal price; // 先用BigDecimal,下次换Money
private Date createTime;
private Date updateTime;
}
CustomerServiceApplication
package geektime.spring.springbucks.customer;
import geektime.spring.springbucks.customer.model.Coffee;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.Banner;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.math.BigDecimal;
import java.net.URI;
@SpringBootApplication
@Slf4j
public class CustomerServiceApplication implements ApplicationRunner {
@Autowired
private RestTemplate restTemplate;
public static void main(String[] args) {
new SpringApplicationBuilder()
.sources(CustomerServiceApplication.class)
.bannerMode(Banner.Mode.OFF)
.web(WebApplicationType.NONE)//禁止启动tomcat运行
.run(args);
}
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
// return new RestTemplate(); //两种方式都可以
return builder.build();
}
@Override
public void run(ApplicationArguments args) throws Exception {
//restTemplate.getForEntity(uri,obj)
URI uri = UriComponentsBuilder
.fromUriString("http://localhost:8080/coffee/{id}")
.build(1);
ResponseEntity<Coffee> c = restTemplate.getForEntity(uri, Coffee.class);
log.info("Response Status: {}, Response Headers: {}", c.getStatusCode(), c.getHeaders().toString());
//Response Status: 200 OK, Response Headers: [Content-Type:"application/json;charset=UTF-8", Transfer-Encoding:"chunked", Date:"Tue, 21 Jan 2020 06:03:02 GMT"]
log.info("Coffee: {}", c.getBody());
//Coffee: Coffee(id=1, name=espresso, price=20.00, createTime=Tue Jan 21 14:02:56 CST 2020, updateTime=Tue Jan 21 14:02:56 CST 2020)
//restTemplate.postForObject(String, obj, obj.class);
String coffeeUri = "http://localhost:8080/coffee/";
////addCoffeeWithoutBindingResult(@Valid NewCoffeeRequest newCoffee) consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE
Coffee request = Coffee.builder()
.name("Americano")
.price(BigDecimal.valueOf(25.00))
.build();
Coffee response = restTemplate.postForObject(coffeeUri, request, Coffee.class);
log.info("New Coffee: {}", response);
//New Coffee: Coffee(id=6, name=Americano, price=25.00, createTime=Tue Jan 21 14:03:02 CST 2020, updateTime=Tue Jan 21 14:03:02 CST 2020)
//http://localhost:8080/coffee/ 请求所有coffee
String s = restTemplate.getForObject(coffeeUri, String.class);
log.info("String: {}", s);
//String: String: [ {
// "id" : 1,
// "createTime" : "2020-01-21T14:02:56.262+0800",
// "updateTime" : "2020-01-21T14:02:56.262+0800",
// "name" : "espresso",
// "price" : 20.00
//}, {
// "id" : 2,
// "createTime" : "2020-01-21T14:02:56.264+0800",
// "updateTime" : "2020-01-21T14:02:56.264+0800",
// "name" : "latte",
// "price" : 25.00
//}, {
// "id" : 3,
// "createTime" : "2020-01-21T14:02:56.264+0800",
// "updateTime" : "2020-01-21T14:02:56.264+0800",
// "name" : "capuccino",
// "price" : 25.00
//}, {
// "id" : 4,
// "createTime" : "2020-01-21T14:02:56.264+0800",
// "updateTime" : "2020-01-21T14:02:56.264+0800",
// "name" : "mocha",
// "price" : 30.00
//}, {
// "id" : 5,
// "createTime" : "2020-01-21T14:02:56.264+0800",
// "updateTime" : "2020-01-21T14:02:56.264+0800",
// "name" : "macchiato",
// "price" : 30.00
//}, {
// "id" : 6,
// "createTime" : "2020-01-21T14:03:02.683+0800",
// "updateTime" : "2020-01-21T14:03:02.683+0800",
// "name" : "Americano",
// "price" : 25.00
//} ]
}
}
RestTemplate 的高阶用法
传递 HTTP Header
- RestTemplate.exchange()
- RequestEntity
/ ResponseEntity
类型转换
- JsonSerializer / JsonDeserializer
- @JsonComponent
解析泛型对象
- RestTemplate.exchange()
- ParameterizedTypeReference
示例
POM 依赖
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>geektime.spring.springbucks</groupId>
<artifactId>customer-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>customer-service</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.joda</groupId>
<artifactId>joda-money</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Coffee
package geektime.spring.springbucks.customer.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.joda.money.Money;
import java.io.Serializable;
import java.util.Date;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Coffee implements Serializable {
private Long id;
private String name;
private Money price;
private Date createTime;
private Date updateTime;
}
MoneySerializer
package geektime.spring.springbucks.customer.support;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import org.joda.money.Money;
import org.springframework.boot.jackson.JsonComponent;
import java.io.IOException;
@JsonComponent
public class MoneySerializer extends StdSerializer<Money> {
protected MoneySerializer() {
super(Money.class);
}
@Override
public void serialize(Money money, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeNumber(money.getAmount());
}
}
MoneyDeserializer
package geektime.spring.springbucks.customer.support;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.springframework.boot.jackson.JsonComponent;
import java.io.IOException;
@JsonComponent
public class MoneyDeserializer extends StdDeserializer<Money> {
protected MoneyDeserializer() {
super(Money.class);
}
@Override
public Money deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
return Money.of(CurrencyUnit.of("CNY"), p.getDecimalValue());
}
}
CustomerServiceApplication
package geektime.spring.springbucks.customer;
import geektime.spring.springbucks.customer.model.Coffee;
import lombok.extern.slf4j.Slf4j;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.Banner;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import javax.print.attribute.standard.Media;
import java.math.BigDecimal;
import java.net.URI;
import java.util.List;
@SpringBootApplication
@Slf4j
public class CustomerServiceApplication implements ApplicationRunner {
@Autowired
private RestTemplate restTemplate;
public static void main(String[] args) {
new SpringApplicationBuilder()
.sources(CustomerServiceApplication.class)
.bannerMode(Banner.Mode.OFF)
.web(WebApplicationType.NONE)//禁用tomcat
.run(args);
}
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
// return new RestTemplate();
return builder.build();
}
@Override
public void run(ApplicationArguments args) throws Exception {
//RequestEntity.get(uri).accept(MediaType.APPLICATION_XML)
URI uri = UriComponentsBuilder
.fromUriString("http://localhost:8080/coffee/?name={name}")
.build("mocha");
RequestEntity<Void> req = RequestEntity.get(uri)
.accept(MediaType.APPLICATION_XML)
.build();
ResponseEntity<String> resp = restTemplate.exchange(req, String.class);
log.info("Response Status: {}, Response Headers: {}", resp.getStatusCode(), resp.getHeaders().toString());
//Response Status: 200 OK, Response Headers: [Content-Type:"application/xml;charset=UTF-8", Transfer-Encoding:"chunked", Date:"Tue, 21 Jan 2020 07:15:50 GMT"]
log.info("Coffee: {}", resp.getBody());
//Coffee: <Coffee>
// <id>4</id>
// <createTime>2020-01-21T14:02:56.264+0800</createTime>
// <updateTime>2020-01-21T14:02:56.264+0800</updateTime>
// <name>mocha</name>
// <price>30.00</price>
//</Coffee>
String coffeeUri = "http://localhost:8080/coffee/";
Coffee request = Coffee.builder()
.name("Americano")
.price(Money.of(CurrencyUnit.of("CNY"), 25.00))
.build();
Coffee response = restTemplate.postForObject(coffeeUri, request, Coffee.class);
log.info("New Coffee: {}", response);
//New Coffee: Coffee(id=6, name=Americano, price=CNY 25.00, createTime=Tue Jan 21 15:23:01 CST 2020, updateTime=Tue Jan 21 15:23:01 CST 2020)
//"http://localhost:8080/coffee/"; 获取所有coffee
//解析泛型对象
ParameterizedTypeReference<List<Coffee>> ptr = new ParameterizedTypeReference<List<Coffee>>() {};
ResponseEntity<List<Coffee>> list = restTemplate.exchange(coffeeUri, HttpMethod.GET, null, ptr);
list.getBody().forEach(c -> log.info("Coffee: {}", c));
//Coffee: Coffee(id=1, name=espresso, price=CNY 20.00, createTime=Tue Jan 21 15:22:51 CST 2020, updateTime=Tue Jan 21 15:22:51 CST 2020)
//Coffee: Coffee(id=2, name=latte, price=CNY 25.00, createTime=Tue Jan 21 15:22:51 CST 2020, updateTime=Tue Jan 21 15:22:51 CST 2020)
//Coffee: Coffee(id=3, name=capuccino, price=CNY 25.00, createTime=Tue Jan 21 15:22:51 CST 2020, updateTime=Tue Jan 21 15:22:51 CST 2020)
//Coffee: Coffee(id=4, name=mocha, price=CNY 30.00, createTime=Tue Jan 21 15:22:51 CST 2020, updateTime=Tue Jan 21 15:22:51 CST 2020)
//Coffee: Coffee(id=5, name=macchiato, price=CNY 30.00, createTime=Tue Jan 21 15:22:51 CST 2020, updateTime=Tue Jan 21 15:22:51 CST 2020)
//Coffee: Coffee(id=6, name=Americano, price=CNY 25.00, createTime=Tue Jan 21 15:23:01 CST 2020, updateTime=Tue Jan 21 15:23:01 CST 2020)
}
}
简单定制 RestTemplate
通用接口
- ClientHttpRequestFactory
默认实现
- SimpleClientHttpRequestFactory(JDK 自带的)
RestTemplate 支持的 HTTP 库
- Apache HttpComponents:
HttpComponentsClientHttpRequestFactory
- Netty:
Netty4ClientHttpRequestFactory
- OkHttp:
OkHttp3ClientHttpRequestFactory
优化底层请求策略
- 连接管理
* PoolingHttpClientConnectionManager
* KeepAlive 策略
- 超时设置
* connectTimeout / readTimeout
- SSL 校验
证书检查策略
示例
依赖
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>geektime.spring.springbucks</groupId>
<artifactId>customer-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>customer-service</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.joda</groupId>
<artifactId>joda-money</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- 作为底层Http支持的库 -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.7</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
CustomConnectionKeepAliveStrategy
package geektime.spring.springbucks.customer.support;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.http.HttpResponse;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import java.util.Arrays;
public class CustomConnectionKeepAliveStrategy implements ConnectionKeepAliveStrategy {
private final long DEFAULT_SECONDS = 30;
@Override
public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
return Arrays.asList(response.getHeaders(HTTP.CONN_KEEP_ALIVE))
.stream()
.filter(h -> StringUtils.equalsIgnoreCase(h.getName(), "timeout")
&& StringUtils.isNumeric(h.getValue()))
.findFirst()
.map(h -> NumberUtils.toLong(h.getValue(), DEFAULT_SECONDS))
.orElse(DEFAULT_SECONDS) * 1000;
}
}
CustomerServiceApplication
package geektime.spring.springbucks.customer;
import geektime.spring.springbucks.customer.model.Coffee;
import geektime.spring.springbucks.customer.support.CustomConnectionKeepAliveStrategy;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.Banner;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.TimeUnit;
@SpringBootApplication
@Slf4j
public class CustomerServiceApplication implements ApplicationRunner {
@Autowired
private RestTemplate restTemplate;
public static void main(String[] args) {
new SpringApplicationBuilder()
.sources(CustomerServiceApplication.class)
.bannerMode(Banner.Mode.OFF)
.web(WebApplicationType.NONE)
.run(args);
}
/**
* 自定义 HttpComponentsClientHttpRequestFactory
* @return
*/
@Bean
public HttpComponentsClientHttpRequestFactory requestFactory() {
//连接池管理器
PoolingHttpClientConnectionManager connectionManager =
new PoolingHttpClientConnectionManager(30, TimeUnit.SECONDS);//ttl 30秒
connectionManager.setMaxTotal(200);//最大保持200个连接
connectionManager.setDefaultMaxPerRoute(20);//每个Route最多20个连接
//自定制 HttpClient
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(connectionManager) //设置连接池
.evictIdleConnections(30, TimeUnit.SECONDS) //空闲连接设置为30秒
.disableAutomaticRetries()//关闭自动重试机制
// 有 Keep-Alive 认里面的值,没有的话永久有效
//.setKeepAliveStrategy(DefaultConnectionKeepAliveStrategy.INSTANCE)
// 换成自定义的
.setKeepAliveStrategy(new CustomConnectionKeepAliveStrategy())
.build();
HttpComponentsClientHttpRequestFactory requestFactory =
new HttpComponentsClientHttpRequestFactory(httpClient);
return requestFactory;
}
/**
* RestTemplate 相关配置
* @param builder
* @return
*/
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
// return new RestTemplate();
return builder
.setConnectTimeout(Duration.ofMillis(100)) //连接超时 100毫秒
.setReadTimeout(Duration.ofMillis(500))
.requestFactory(this::requestFactory)
.build();
}
@Override
public void run(ApplicationArguments args) throws Exception {
URI uri = UriComponentsBuilder
.fromUriString("http://localhost:8080/coffee/?name={name}")
.build("mocha");
RequestEntity<Void> req = RequestEntity.get(uri)
.accept(MediaType.APPLICATION_XML)
.build();
ResponseEntity<String> resp = restTemplate.exchange(req, String.class);
log.info("Response Status: {}, Response Headers: {}", resp.getStatusCode(), resp.getHeaders().toString());
log.info("Coffee: {}", resp.getBody());
String coffeeUri = "http://localhost:8080/coffee/";
Coffee request = Coffee.builder()
.name("Americano")
.price(Money.of(CurrencyUnit.of("CNY"), 25.00))
.build();
Coffee response = restTemplate.postForObject(coffeeUri, request, Coffee.class);
log.info("New Coffee: {}", response);
ParameterizedTypeReference<List<Coffee>> ptr =
new ParameterizedTypeReference<List<Coffee>>() {};
ResponseEntity<List<Coffee>> list = restTemplate
.exchange(coffeeUri, HttpMethod.GET, null, ptr);
list.getBody().forEach(c -> log.info("Coffee: {}", c));
}
}
通过 WebClient 访问 Web 资源
WebClient
一个以 Reactive 方式处理 HTTP 请求的非阻塞式的客户端
支持的底层 HTTP 库
- Reactor Netty - ReactorClientHttpConnector
- Jetty ReactiveStream HttpClient - JettyClientHttpConnector
创建 WebClient
- WebClient.create()
- WebClient.builder()
发起请求
- get() / post() / put() / delete() / patch()
获得结果
- retrieve() / exchange()
处理 HTTP Status
- onStatus()
应答正文
- bodyToMono() / bodyToFlux()
示例
依赖
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>geektime.spring.reactor</groupId>
<artifactId>webclient-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>webclient-demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.joda</groupId>
<artifactId>joda-money</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Coffee
package geektime.spring.reactor.webclient.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.joda.money.Money;
import java.io.Serializable;
import java.util.Date;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Coffee implements Serializable {
private Long id;
private String name;
private Money price;
private Date createTime;
private Date updateTime;
}
MoneySerializer
package geektime.spring.reactor.webclient.support;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import org.joda.money.Money;
import org.springframework.boot.jackson.JsonComponent;
import java.io.IOException;
@JsonComponent
public class MoneySerializer extends StdSerializer<Money> {
protected MoneySerializer() {
super(Money.class);
}
@Override
public void serialize(Money money, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeNumber(money.getAmount());
}
}
MoneyDeserializer
package geektime.spring.reactor.webclient.support;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.springframework.boot.jackson.JsonComponent;
import java.io.IOException;
@JsonComponent
public class MoneyDeserializer extends StdDeserializer<Money> {
protected MoneyDeserializer() {
super(Money.class);
}
@Override
public Money deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
return Money.of(CurrencyUnit.of("CNY"), p.getDecimalValue());
}
}
启动类 WebclientDemoApplication:运行顺序是不一定的
package geektime.spring.reactor.webclient;
import geektime.spring.reactor.webclient.model.Coffee;
import lombok.extern.slf4j.Slf4j;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.Banner;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.util.concurrent.CountDownLatch;
@SpringBootApplication
@Slf4j
public class WebclientDemoApplication implements ApplicationRunner {
@Autowired
private WebClient webClient;
public static void main(String[] args) {
new SpringApplicationBuilder(WebclientDemoApplication.class)
.web(WebApplicationType.NONE)
.bannerMode(Banner.Mode.OFF)
.run(args);
}
/**
* 构造 WebClient
* @param builder
* @return
*/
@Bean
public WebClient webClient(WebClient.Builder builder) {
return builder.baseUrl("http://localhost:8080").build();
}
@Override
public void run(ApplicationArguments args) throws Exception {
CountDownLatch cdl = new CountDownLatch(2);
//get请求:/coffee/{id}
webClient.get()
.uri("/coffee/{id}", 1)
.accept(MediaType.APPLICATION_JSON_UTF8)//设置头信息:要求响应类型是JSON
.retrieve()//获取结果
.bodyToMono(Coffee.class)//设置body的类型为Coffee类型
.doOnError(t -> log.error("Error: ", t))//打印错误如果有的话
.doFinally(s -> cdl.countDown())//finally操作
.subscribeOn(Schedulers.single())
.subscribe(c -> log.info("Coffee 1: {}", c));
//Coffee 1: Coffee(id=1, name=espresso, price=CNY 20.00, createTime=Wed Jan 22 10:25:18 CST 2020, updateTime=Wed Jan 22 10:25:18 CST 2020)
//post请求:/coffee/
Mono<Coffee> americano = Mono.just(
Coffee.builder()
.name("americano")
.price(Money.of(CurrencyUnit.of("CNY"), 25.00))
.build()
);
webClient.post()
.uri("/coffee/")
.body(americano, Coffee.class)
.retrieve()
.bodyToMono(Coffee.class)
.doFinally(s -> cdl.countDown())
.subscribeOn(Schedulers.single())
.subscribe(c -> log.info("Coffee Created: {}", c));
//Coffee Created: Coffee(id=6, name=americano, price=CNY 25.00, createTime=Wed Jan 22 10:25:22 CST 2020, updateTime=Wed Jan 22 10:25:22 CST 2020)
cdl.await();
//get请求:获取coffee整个列表
webClient.get()
.uri("/coffee/")
.retrieve()
.bodyToFlux(Coffee.class)//Flux代表取得多个对象 列表
.toStream()
.forEach(c -> log.info("Coffee in List: {}", c));
//Coffee in List: Coffee(id=1, name=espresso, price=CNY 20.00, createTime=Wed Jan 22 10:25:18 CST 2020, updateTime=Wed Jan 22 10:25:18 CST 2020)
//Coffee in List: Coffee(id=2, name=latte, price=CNY 25.00, createTime=Wed Jan 22 10:25:18 CST 2020, updateTime=Wed Jan 22 10:25:18 CST 2020)
//Coffee in List: Coffee(id=3, name=capuccino, price=CNY 25.00, createTime=Wed Jan 22 10:25:18 CST 2020, updateTime=Wed Jan 22 10:25:18 CST 2020)
//Coffee in List: Coffee(id=4, name=mocha, price=CNY 30.00, createTime=Wed Jan 22 10:25:18 CST 2020, updateTime=Wed Jan 22 10:25:18 CST 2020)
//Coffee in List: Coffee(id=5, name=macchiato, price=CNY 30.00, createTime=Wed Jan 22 10:25:18 CST 2020, updateTime=Wed Jan 22 10:25:18 CST 2020)
//Coffee in List: Coffee(id=6, name=americano, price=CNY 25.00, createTime=Wed Jan 22 10:25:22 CST 2020, updateTime=Wed Jan 22 10:25:22 CST 2020)
}
}
customer-service (示例 访问 Web 资源)
- 通过编码方式查询咖啡
- 通过编码方式创建订单
依赖
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>geektime.spring.springbucks</groupId>
<artifactId>customer-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>customer-service</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.joda</groupId>
<artifactId>joda-money</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.7</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Coffee
package geektime.spring.springbucks.customer.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.joda.money.Money;
import java.io.Serializable;
import java.util.Date;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Coffee implements Serializable {
private Long id;
private String name;
private Money price;
private Date createTime;
private Date updateTime;
}
CoffeeOrder
package geektime.spring.springbucks.customer.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CoffeeOrder {
private Long id;
private String customer;
private List<Coffee> items;
private OrderState state;
private Date createTime;
private Date updateTime;
}
OrderState
package geektime.spring.springbucks.customer.model;
public enum OrderState {
INIT, PAID, BREWING, BREWED, TAKEN, CANCELLED
}
NewOrderRequest
package geektime.spring.springbucks.customer.model;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.util.List;
@Builder
@Getter
@Setter
public class NewOrderRequest {
private String customer;
private List<String> items;
}
MoneySerializer
package geektime.spring.springbucks.customer.support;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import org.joda.money.Money;
import org.springframework.boot.jackson.JsonComponent;
import java.io.IOException;
@JsonComponent
public class MoneySerializer extends StdSerializer<Money> {
protected MoneySerializer() {
super(Money.class);
}
@Override
public void serialize(Money money, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeNumber(money.getAmount());
}
}
MoneyDeserializer
package geektime.spring.springbucks.customer.support;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.springframework.boot.jackson.JsonComponent;
import java.io.IOException;
@JsonComponent
public class MoneyDeserializer extends StdDeserializer<Money> {
protected MoneyDeserializer() {
super(Money.class);
}
@Override
public Money deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
return Money.of(CurrencyUnit.of("CNY"), p.getDecimalValue());
}
}
CustomConnectionKeepAliveStrategy
package geektime.spring.springbucks.customer.support;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.http.HttpResponse;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import java.util.Arrays;
public class CustomConnectionKeepAliveStrategy implements ConnectionKeepAliveStrategy {
private final long DEFAULT_SECONDS = 30;
@Override
public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
return Arrays.asList(response.getHeaders(HTTP.CONN_KEEP_ALIVE))
.stream()
.filter(h -> StringUtils.equalsIgnoreCase(h.getName(), "timeout")
&& StringUtils.isNumeric(h.getValue()))
.findFirst()
.map(h -> NumberUtils.toLong(h.getValue(), DEFAULT_SECONDS))
.orElse(DEFAULT_SECONDS) * 1000;
}
}
启动类
package geektime.spring.springbucks.customer;
import geektime.spring.springbucks.customer.support.CustomConnectionKeepAliveStrategy;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.boot.Banner;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
@SpringBootApplication
@Slf4j
public class CustomerServiceApplication {
public static void main(String[] args) {
new SpringApplicationBuilder()
.sources(CustomerServiceApplication.class)
.bannerMode(Banner.Mode.OFF)
.web(WebApplicationType.NONE)
.run(args);
}
@Bean
public HttpComponentsClientHttpRequestFactory requestFactory() {
PoolingHttpClientConnectionManager connectionManager =
new PoolingHttpClientConnectionManager(30, TimeUnit.SECONDS);
connectionManager.setMaxTotal(200);
connectionManager.setDefaultMaxPerRoute(20);
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(connectionManager)
.evictIdleConnections(30, TimeUnit.SECONDS)
.disableAutomaticRetries()
// 有 Keep-Alive 认里面的值,没有的话永久有效
//.setKeepAliveStrategy(DefaultConnectionKeepAliveStrategy.INSTANCE)
// 换成自定义的
.setKeepAliveStrategy(new CustomConnectionKeepAliveStrategy())
.build();
HttpComponentsClientHttpRequestFactory requestFactory =
new HttpComponentsClientHttpRequestFactory(httpClient);
return requestFactory;
}
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder
.setConnectTimeout(Duration.ofMillis(100))
.setReadTimeout(Duration.ofMillis(500))
.requestFactory(this::requestFactory)
.build();
}
}
CustomerRunner
package geektime.spring.springbucks.customer;
import geektime.spring.springbucks.customer.model.Coffee;
import geektime.spring.springbucks.customer.model.CoffeeOrder;
import geektime.spring.springbucks.customer.model.NewOrderRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpMethod;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.util.Arrays;
import java.util.List;
@Component
@Slf4j
public class CustomerRunner implements ApplicationRunner {
@Autowired
private RestTemplate restTemplate;
@Override
public void run(ApplicationArguments args) throws Exception {
readMenu();
Long id = orderCoffee();
queryOrder(id);
}
//遍历菜单
private void readMenu() {
ParameterizedTypeReference<List<Coffee>> ptr =
new ParameterizedTypeReference<List<Coffee>>() {};
ResponseEntity<List<Coffee>> list = restTemplate
.exchange("http://localhost:8080/coffee/", HttpMethod.GET, null, ptr);
list.getBody().forEach(c -> log.info("Coffee: {}", c));
}
//Coffee: Coffee(id=1, name=espresso, price=CNY 20.00, createTime=Wed Jan 22 10:46:54 CST 2020, updateTime=Wed Jan 22 10:46:54 CST 2020)
//Coffee: Coffee(id=2, name=latte, price=CNY 25.00, createTime=Wed Jan 22 10:46:54 CST 2020, updateTime=Wed Jan 22 10:46:54 CST 2020)
//Coffee: Coffee(id=3, name=capuccino, price=CNY 25.00, createTime=Wed Jan 22 10:46:54 CST 2020, updateTime=Wed Jan 22 10:46:54 CST 2020)
//Coffee: Coffee(id=4, name=mocha, price=CNY 30.00, createTime=Wed Jan 22 10:46:54 CST 2020, updateTime=Wed Jan 22 10:46:54 CST 2020)
//Coffee: Coffee(id=5, name=macchiato, price=CNY 30.00, createTime=Wed Jan 22 10:46:54 CST 2020, updateTime=Wed Jan 22 10:46:54 CST 2020)
//下单
private Long orderCoffee() {
NewOrderRequest orderRequest = NewOrderRequest.builder()
.customer("Li Lei")
.items(Arrays.asList("capuccino"))
.build();
RequestEntity<NewOrderRequest> request = RequestEntity
.post(UriComponentsBuilder.fromUriString("http://localhost:8080/order/").build().toUri())
.body(orderRequest);
ResponseEntity<CoffeeOrder> response = restTemplate.exchange(request, CoffeeOrder.class);
log.info("Order Request Status Code: {}", response.getStatusCode());
//Order Request Status Code: 201 CREATED
Long id = response.getBody().getId();
log.info("Order ID: {}", id);
//Order ID: 1
return id;
}
//根据订单号查询订单
private void queryOrder(Long id) {
CoffeeOrder order = restTemplate
.getForObject("http://localhost:8080/order/{id}", CoffeeOrder.class, id);
log.info("Order: {}", order);
//CoffeeOrder(id=1, customer=Li Lei, items=[Coffee(id=3, name=capuccino, price=CNY 25.00, createTime=Wed Jan 22 10:46:54 CST 2020, updateTime=Wed Jan 22 10:46:54 CST 2020)], state=INIT, createTime=Wed Jan 22 13:10:52 CST 2020, updateTime=Wed Jan 22 13:10:52 CST 2020)
}
}