目录

Life in Flow

知不知,尚矣;不知知,病矣。
不知不知,殆矣。

X

访问 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

 1package geektime.spring.springbucks.customer.model;
 2
 3import lombok.AllArgsConstructor;
 4import lombok.Builder;
 5import lombok.Data;
 6import lombok.NoArgsConstructor;
 7
 8import java.io.Serializable;
 9import java.math.BigDecimal;
10import java.util.Date;
11
12@Data
13@Builder
14@NoArgsConstructor
15@AllArgsConstructor
16public class Coffee implements Serializable {
17    private Long id;
18    private String name;
19    private BigDecimal price; // 先用BigDecimal,下次换Money
20    private Date createTime;
21    private Date updateTime;
22}

CustomerServiceApplication

  1package geektime.spring.springbucks.customer;
  2
  3import geektime.spring.springbucks.customer.model.Coffee;
  4import lombok.extern.slf4j.Slf4j;
  5import org.springframework.beans.factory.annotation.Autowired;
  6import org.springframework.boot.ApplicationArguments;
  7import org.springframework.boot.ApplicationRunner;
  8import org.springframework.boot.Banner;
  9import org.springframework.boot.WebApplicationType;
 10import org.springframework.boot.autoconfigure.SpringBootApplication;
 11import org.springframework.boot.builder.SpringApplicationBuilder;
 12import org.springframework.boot.web.client.RestTemplateBuilder;
 13import org.springframework.context.annotation.Bean;
 14import org.springframework.http.ResponseEntity;
 15import org.springframework.web.client.RestTemplate;
 16import org.springframework.web.util.UriComponentsBuilder;
 17
 18import java.math.BigDecimal;
 19import java.net.URI;
 20
 21@SpringBootApplication
 22@Slf4j
 23public class CustomerServiceApplication implements ApplicationRunner {
 24	@Autowired
 25	private RestTemplate restTemplate;
 26
 27	public static void main(String[] args) {
 28		new SpringApplicationBuilder()
 29				.sources(CustomerServiceApplication.class)
 30				.bannerMode(Banner.Mode.OFF)
 31				.web(WebApplicationType.NONE)//禁止启动tomcat运行
 32				.run(args);
 33	}
 34
 35	@Bean
 36	public RestTemplate restTemplate(RestTemplateBuilder builder) {
 37//		return new RestTemplate();  //两种方式都可以
 38		return builder.build();
 39	}
 40
 41	@Override
 42	public void run(ApplicationArguments args) throws Exception {
 43		//restTemplate.getForEntity(uri,obj)
 44		URI uri = UriComponentsBuilder
 45				.fromUriString("http://localhost:8080/coffee/{id}")
 46				.build(1);
 47		ResponseEntity<Coffee> c = restTemplate.getForEntity(uri, Coffee.class);
 48		log.info("Response Status: {}, Response Headers: {}", c.getStatusCode(), c.getHeaders().toString());
 49		//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"]
 50		log.info("Coffee: {}", c.getBody());
 51		//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)
 52
 53		//restTemplate.postForObject(String, obj, obj.class);
 54		String coffeeUri = "http://localhost:8080/coffee/";
 55		////addCoffeeWithoutBindingResult(@Valid NewCoffeeRequest newCoffee)   consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE
 56		Coffee request = Coffee.builder()
 57				.name("Americano")
 58				.price(BigDecimal.valueOf(25.00))
 59				.build();
 60		Coffee response = restTemplate.postForObject(coffeeUri, request, Coffee.class);
 61		log.info("New Coffee: {}", response);
 62		//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)
 63
 64		//http://localhost:8080/coffee/  请求所有coffee
 65		String s = restTemplate.getForObject(coffeeUri, String.class);
 66		log.info("String: {}", s);
 67		//String: String: [ {
 68		//  "id" : 1,
 69		//  "createTime" : "2020-01-21T14:02:56.262+0800",
 70		//  "updateTime" : "2020-01-21T14:02:56.262+0800",
 71		//  "name" : "espresso",
 72		//  "price" : 20.00
 73		//}, {
 74		//  "id" : 2,
 75		//  "createTime" : "2020-01-21T14:02:56.264+0800",
 76		//  "updateTime" : "2020-01-21T14:02:56.264+0800",
 77		//  "name" : "latte",
 78		//  "price" : 25.00
 79		//}, {
 80		//  "id" : 3,
 81		//  "createTime" : "2020-01-21T14:02:56.264+0800",
 82		//  "updateTime" : "2020-01-21T14:02:56.264+0800",
 83		//  "name" : "capuccino",
 84		//  "price" : 25.00
 85		//}, {
 86		//  "id" : 4,
 87		//  "createTime" : "2020-01-21T14:02:56.264+0800",
 88		//  "updateTime" : "2020-01-21T14:02:56.264+0800",
 89		//  "name" : "mocha",
 90		//  "price" : 30.00
 91		//}, {
 92		//  "id" : 5,
 93		//  "createTime" : "2020-01-21T14:02:56.264+0800",
 94		//  "updateTime" : "2020-01-21T14:02:56.264+0800",
 95		//  "name" : "macchiato",
 96		//  "price" : 30.00
 97		//}, {
 98		//  "id" : 6,
 99		//  "createTime" : "2020-01-21T14:03:02.683+0800",
100		//  "updateTime" : "2020-01-21T14:03:02.683+0800",
101		//  "name" : "Americano",
102		//  "price" : 25.00
103		//} ]
104	}
105}

RestTemplate 的高阶用法

传递 HTTP Header

  • RestTemplate.exchange()
  • RequestEntity / ResponseEntity

类型转换

  • JsonSerializer / JsonDeserializer
  • @JsonComponent

解析泛型对象

  • RestTemplate.exchange()
  • ParameterizedTypeReference

示例
POM 依赖

 1<?xml version="1.0" encoding="UTF-8"?>
 2<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 3	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 4	<modelVersion>4.0.0</modelVersion>
 5	<parent>
 6		<groupId>org.springframework.boot</groupId>
 7		<artifactId>spring-boot-starter-parent</artifactId>
 8		<version>2.1.3.RELEASE</version>
 9		<relativePath/> <!-- lookup parent from repository -->
10	</parent>
11	<groupId>geektime.spring.springbucks</groupId>
12	<artifactId>customer-service</artifactId>
13	<version>0.0.1-SNAPSHOT</version>
14	<name>customer-service</name>
15	<description>Demo project for Spring Boot</description>
16
17	<properties>
18		<java.version>1.8</java.version>
19	</properties>
20
21	<dependencies>
22		<dependency>
23			<groupId>org.springframework.boot</groupId>
24			<artifactId>spring-boot-starter-web</artifactId>
25		</dependency>
26
27		<dependency>
28			<groupId>org.joda</groupId>
29			<artifactId>joda-money</artifactId>
30			<version>1.0.1</version>
31		</dependency>
32
33		<dependency>
34			<groupId>org.projectlombok</groupId>
35			<artifactId>lombok</artifactId>
36			<optional>true</optional>
37		</dependency>
38		<dependency>
39			<groupId>org.springframework.boot</groupId>
40			<artifactId>spring-boot-starter-test</artifactId>
41			<scope>test</scope>
42		</dependency>
43	</dependencies>
44
45	<build>
46		<plugins>
47			<plugin>
48				<groupId>org.springframework.boot</groupId>
49				<artifactId>spring-boot-maven-plugin</artifactId>
50			</plugin>
51		</plugins>
52	</build>
53</project>

Coffee

 1package geektime.spring.springbucks.customer.model;
 2
 3import lombok.AllArgsConstructor;
 4import lombok.Builder;
 5import lombok.Data;
 6import lombok.NoArgsConstructor;
 7import org.joda.money.Money;
 8
 9import java.io.Serializable;
10import java.util.Date;
11
12@Data
13@Builder
14@NoArgsConstructor
15@AllArgsConstructor
16public class Coffee implements Serializable {
17    private Long id;
18    private String name;
19    private Money price;
20    private Date createTime;
21    private Date updateTime;
22}

MoneySerializer

 1package geektime.spring.springbucks.customer.support;
 2
 3import com.fasterxml.jackson.core.JsonGenerator;
 4import com.fasterxml.jackson.databind.SerializerProvider;
 5import com.fasterxml.jackson.databind.ser.std.StdSerializer;
 6import org.joda.money.Money;
 7import org.springframework.boot.jackson.JsonComponent;
 8
 9import java.io.IOException;
10
11@JsonComponent
12public class MoneySerializer extends StdSerializer<Money> {
13    protected MoneySerializer() {
14        super(Money.class);
15    }
16
17    @Override
18    public void serialize(Money money, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
19        jsonGenerator.writeNumber(money.getAmount());
20    }
21}

MoneyDeserializer

 1package geektime.spring.springbucks.customer.support;
 2
 3import com.fasterxml.jackson.core.JsonParser;
 4import com.fasterxml.jackson.core.JsonProcessingException;
 5import com.fasterxml.jackson.databind.DeserializationContext;
 6import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
 7import org.joda.money.CurrencyUnit;
 8import org.joda.money.Money;
 9import org.springframework.boot.jackson.JsonComponent;
10
11import java.io.IOException;
12
13@JsonComponent
14public class MoneyDeserializer extends StdDeserializer<Money> {
15    protected MoneyDeserializer() {
16        super(Money.class);
17    }
18
19    @Override
20    public Money deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
21        return Money.of(CurrencyUnit.of("CNY"), p.getDecimalValue());
22    }
23}

CustomerServiceApplication

 1package geektime.spring.springbucks.customer;
 2
 3import geektime.spring.springbucks.customer.model.Coffee;
 4import lombok.extern.slf4j.Slf4j;
 5import org.joda.money.CurrencyUnit;
 6import org.joda.money.Money;
 7import org.springframework.beans.factory.annotation.Autowired;
 8import org.springframework.boot.ApplicationArguments;
 9import org.springframework.boot.ApplicationRunner;
10import org.springframework.boot.Banner;
11import org.springframework.boot.WebApplicationType;
12import org.springframework.boot.autoconfigure.SpringBootApplication;
13import org.springframework.boot.builder.SpringApplicationBuilder;
14import org.springframework.boot.web.client.RestTemplateBuilder;
15import org.springframework.context.annotation.Bean;
16import org.springframework.core.ParameterizedTypeReference;
17import org.springframework.http.HttpMethod;
18import org.springframework.http.MediaType;
19import org.springframework.http.RequestEntity;
20import org.springframework.http.ResponseEntity;
21import org.springframework.web.client.RestTemplate;
22import org.springframework.web.util.UriComponentsBuilder;
23
24import javax.print.attribute.standard.Media;
25import java.math.BigDecimal;
26import java.net.URI;
27import java.util.List;
28
29@SpringBootApplication
30@Slf4j
31public class CustomerServiceApplication implements ApplicationRunner {
32	@Autowired
33	private RestTemplate restTemplate;
34
35	public static void main(String[] args) {
36		new SpringApplicationBuilder()
37				.sources(CustomerServiceApplication.class)
38				.bannerMode(Banner.Mode.OFF)
39				.web(WebApplicationType.NONE)//禁用tomcat
40				.run(args);
41	}
42
43	@Bean
44	public RestTemplate restTemplate(RestTemplateBuilder builder) {
45//		return new RestTemplate();
46		return builder.build();
47	}
48
49	@Override
50	public void run(ApplicationArguments args) throws Exception {
51		//RequestEntity.get(uri).accept(MediaType.APPLICATION_XML)
52		URI uri = UriComponentsBuilder
53				.fromUriString("http://localhost:8080/coffee/?name={name}")
54				.build("mocha");
55		RequestEntity<Void> req = RequestEntity.get(uri)
56				.accept(MediaType.APPLICATION_XML)
57				.build();
58		ResponseEntity<String> resp = restTemplate.exchange(req, String.class);
59		log.info("Response Status: {}, Response Headers: {}", resp.getStatusCode(), resp.getHeaders().toString());
60		//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"]
61		log.info("Coffee: {}", resp.getBody());
62		//Coffee: <Coffee>
63		//  <id>4</id>
64		//  <createTime>2020-01-21T14:02:56.264+0800</createTime>
65		//  <updateTime>2020-01-21T14:02:56.264+0800</updateTime>
66		//  <name>mocha</name>
67		//  <price>30.00</price>
68		//</Coffee>
69
70		String coffeeUri = "http://localhost:8080/coffee/";
71		Coffee request = Coffee.builder()
72				.name("Americano")
73				.price(Money.of(CurrencyUnit.of("CNY"), 25.00))
74				.build();
75		Coffee response = restTemplate.postForObject(coffeeUri, request, Coffee.class);
76		log.info("New Coffee: {}", response);
77		//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)
78
79		//"http://localhost:8080/coffee/"; 获取所有coffee
80		//解析泛型对象
81		ParameterizedTypeReference<List<Coffee>> ptr = new ParameterizedTypeReference<List<Coffee>>() {};
82		ResponseEntity<List<Coffee>> list = restTemplate.exchange(coffeeUri, HttpMethod.GET, null, ptr);
83		list.getBody().forEach(c -> log.info("Coffee: {}", c));
84		//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)
85		//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)
86		//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)
87		//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)
88		//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)
89		//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)
90	}
91}

简单定制 RestTemplate

通用接口

  • ClientHttpRequestFactory

默认实现

  • SimpleClientHttpRequestFactory(JDK 自带的)

RestTemplate 支持的 HTTP 库

  • Apache HttpComponents: HttpComponentsClientHttpRequestFactory
  • Netty: Netty4ClientHttpRequestFactory
  • OkHttp: OkHttp3ClientHttpRequestFactory

优化底层请求策略

  • 连接管理
1* PoolingHttpClientConnectionManager
2* KeepAlive 策略
  • 超时设置
1* connectTimeout / readTimeout
  • SSL 校验
1证书检查策略

示例
依赖

 1<?xml version="1.0" encoding="UTF-8"?>
 2<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 3	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 4	<modelVersion>4.0.0</modelVersion>
 5	<parent>
 6		<groupId>org.springframework.boot</groupId>
 7		<artifactId>spring-boot-starter-parent</artifactId>
 8		<version>2.1.3.RELEASE</version>
 9		<relativePath/> <!-- lookup parent from repository -->
10	</parent>
11	<groupId>geektime.spring.springbucks</groupId>
12	<artifactId>customer-service</artifactId>
13	<version>0.0.1-SNAPSHOT</version>
14	<name>customer-service</name>
15	<description>Demo project for Spring Boot</description>
16
17	<properties>
18		<java.version>1.8</java.version>
19	</properties>
20
21	<dependencies>
22		<dependency>
23			<groupId>org.springframework.boot</groupId>
24			<artifactId>spring-boot-starter-web</artifactId>
25		</dependency>
26
27		<dependency>
28			<groupId>org.joda</groupId>
29			<artifactId>joda-money</artifactId>
30			<version>1.0.1</version>
31		</dependency>
32
33		<dependency>
34			<groupId>org.apache.commons</groupId>
35			<artifactId>commons-lang3</artifactId>
36		</dependency>
37		<!-- 作为底层Http支持的库 -->
38		<dependency>
39			<groupId>org.apache.httpcomponents</groupId>
40			<artifactId>httpclient</artifactId>
41			<version>4.5.7</version>
42		</dependency>
43
44		<dependency>
45			<groupId>org.projectlombok</groupId>
46			<artifactId>lombok</artifactId>
47			<optional>true</optional>
48		</dependency>
49		<dependency>
50			<groupId>org.springframework.boot</groupId>
51			<artifactId>spring-boot-starter-test</artifactId>
52			<scope>test</scope>
53		</dependency>
54	</dependencies>
55
56	<build>
57		<plugins>
58			<plugin>
59				<groupId>org.springframework.boot</groupId>
60				<artifactId>spring-boot-maven-plugin</artifactId>
61			</plugin>
62		</plugins>
63	</build>
64
65</project>

CustomConnectionKeepAliveStrategy

 1package geektime.spring.springbucks.customer.support;
 2
 3import org.apache.commons.lang3.StringUtils;
 4import org.apache.commons.lang3.math.NumberUtils;
 5import org.apache.http.HttpResponse;
 6import org.apache.http.conn.ConnectionKeepAliveStrategy;
 7import org.apache.http.protocol.HTTP;
 8import org.apache.http.protocol.HttpContext;
 9
10import java.util.Arrays;
11
12public class CustomConnectionKeepAliveStrategy implements ConnectionKeepAliveStrategy {
13    private final long DEFAULT_SECONDS = 30;
14
15    @Override
16    public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
17        return Arrays.asList(response.getHeaders(HTTP.CONN_KEEP_ALIVE))
18                .stream()
19                .filter(h -> StringUtils.equalsIgnoreCase(h.getName(), "timeout")
20                        && StringUtils.isNumeric(h.getValue()))
21                .findFirst()
22                .map(h -> NumberUtils.toLong(h.getValue(), DEFAULT_SECONDS))
23                .orElse(DEFAULT_SECONDS) * 1000;
24    }
25}

CustomerServiceApplication

  1package geektime.spring.springbucks.customer;
  2
  3import geektime.spring.springbucks.customer.model.Coffee;
  4import geektime.spring.springbucks.customer.support.CustomConnectionKeepAliveStrategy;
  5import lombok.extern.slf4j.Slf4j;
  6import org.apache.http.impl.client.CloseableHttpClient;
  7import org.apache.http.impl.client.HttpClients;
  8import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
  9import org.joda.money.CurrencyUnit;
 10import org.joda.money.Money;
 11import org.springframework.beans.factory.annotation.Autowired;
 12import org.springframework.boot.ApplicationArguments;
 13import org.springframework.boot.ApplicationRunner;
 14import org.springframework.boot.Banner;
 15import org.springframework.boot.WebApplicationType;
 16import org.springframework.boot.autoconfigure.SpringBootApplication;
 17import org.springframework.boot.builder.SpringApplicationBuilder;
 18import org.springframework.boot.web.client.RestTemplateBuilder;
 19import org.springframework.context.annotation.Bean;
 20import org.springframework.core.ParameterizedTypeReference;
 21import org.springframework.http.HttpMethod;
 22import org.springframework.http.MediaType;
 23import org.springframework.http.RequestEntity;
 24import org.springframework.http.ResponseEntity;
 25import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
 26import org.springframework.web.client.RestTemplate;
 27import org.springframework.web.util.UriComponentsBuilder;
 28
 29import java.net.URI;
 30import java.time.Duration;
 31import java.util.List;
 32import java.util.concurrent.TimeUnit;
 33
 34@SpringBootApplication
 35@Slf4j
 36public class CustomerServiceApplication implements ApplicationRunner {
 37	@Autowired
 38	private RestTemplate restTemplate;
 39
 40	public static void main(String[] args) {
 41		new SpringApplicationBuilder()
 42				.sources(CustomerServiceApplication.class)
 43				.bannerMode(Banner.Mode.OFF)
 44				.web(WebApplicationType.NONE)
 45				.run(args);
 46	}
 47
 48	/**
 49	 * 自定义 HttpComponentsClientHttpRequestFactory
 50	 * @return
 51	 */
 52	@Bean
 53	public HttpComponentsClientHttpRequestFactory requestFactory() {
 54		//连接池管理器
 55		PoolingHttpClientConnectionManager connectionManager =
 56				new PoolingHttpClientConnectionManager(30, TimeUnit.SECONDS);//ttl 30秒
 57		connectionManager.setMaxTotal(200);//最大保持200个连接
 58		connectionManager.setDefaultMaxPerRoute(20);//每个Route最多20个连接
 59
 60		//自定制 HttpClient
 61		CloseableHttpClient httpClient = HttpClients.custom()
 62				.setConnectionManager(connectionManager) //设置连接池
 63				.evictIdleConnections(30, TimeUnit.SECONDS) //空闲连接设置为30秒
 64				.disableAutomaticRetries()//关闭自动重试机制
 65
 66				// 有 Keep-Alive 认里面的值,没有的话永久有效
 67				//.setKeepAliveStrategy(DefaultConnectionKeepAliveStrategy.INSTANCE)
 68				// 换成自定义的
 69				.setKeepAliveStrategy(new CustomConnectionKeepAliveStrategy())
 70				.build();
 71
 72		HttpComponentsClientHttpRequestFactory requestFactory =
 73				new HttpComponentsClientHttpRequestFactory(httpClient);
 74
 75		return requestFactory;
 76	}
 77
 78	/**
 79	 * RestTemplate 相关配置
 80	 * @param builder
 81	 * @return
 82	 */
 83	@Bean
 84	public RestTemplate restTemplate(RestTemplateBuilder builder) {
 85//		return new RestTemplate();
 86
 87		return builder
 88				.setConnectTimeout(Duration.ofMillis(100)) //连接超时 100毫秒
 89				.setReadTimeout(Duration.ofMillis(500)) 
 90				.requestFactory(this::requestFactory)
 91				.build();
 92	}
 93
 94	@Override
 95	public void run(ApplicationArguments args) throws Exception {
 96		URI uri = UriComponentsBuilder
 97				.fromUriString("http://localhost:8080/coffee/?name={name}")
 98				.build("mocha");
 99		RequestEntity<Void> req = RequestEntity.get(uri)
100				.accept(MediaType.APPLICATION_XML)
101				.build();
102		ResponseEntity<String> resp = restTemplate.exchange(req, String.class);
103		log.info("Response Status: {}, Response Headers: {}", resp.getStatusCode(), resp.getHeaders().toString());
104		log.info("Coffee: {}", resp.getBody());
105
106		String coffeeUri = "http://localhost:8080/coffee/";
107		Coffee request = Coffee.builder()
108				.name("Americano")
109				.price(Money.of(CurrencyUnit.of("CNY"), 25.00))
110				.build();
111		Coffee response = restTemplate.postForObject(coffeeUri, request, Coffee.class);
112		log.info("New Coffee: {}", response);
113
114		ParameterizedTypeReference<List<Coffee>> ptr =
115				new ParameterizedTypeReference<List<Coffee>>() {};
116		ResponseEntity<List<Coffee>> list = restTemplate
117				.exchange(coffeeUri, HttpMethod.GET, null, ptr);
118		list.getBody().forEach(c -> log.info("Coffee: {}", c));
119	}
120}

通过 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()

示例
依赖

 1<?xml version="1.0" encoding="UTF-8"?>
 2<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 3	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 4	<modelVersion>4.0.0</modelVersion>
 5	<parent>
 6		<groupId>org.springframework.boot</groupId>
 7		<artifactId>spring-boot-starter-parent</artifactId>
 8		<version>2.1.3.RELEASE</version>
 9		<relativePath/> <!-- lookup parent from repository -->
10	</parent>
11	<groupId>geektime.spring.reactor</groupId>
12	<artifactId>webclient-demo</artifactId>
13	<version>0.0.1-SNAPSHOT</version>
14	<name>webclient-demo</name>
15	<description>Demo project for Spring Boot</description>
16
17	<properties>
18		<java.version>1.8</java.version>
19	</properties>
20
21	<dependencies>
22		<dependency>
23			<groupId>org.springframework.boot</groupId>
24			<artifactId>spring-boot-starter-webflux</artifactId>
25		</dependency>
26
27		<dependency>
28			<groupId>org.joda</groupId>
29			<artifactId>joda-money</artifactId>
30			<version>1.0.1</version>
31		</dependency>
32
33		<dependency>
34			<groupId>org.projectlombok</groupId>
35			<artifactId>lombok</artifactId>
36			<optional>true</optional>
37		</dependency>
38		<dependency>
39			<groupId>org.springframework.boot</groupId>
40			<artifactId>spring-boot-starter-test</artifactId>
41			<scope>test</scope>
42		</dependency>
43		<dependency>
44			<groupId>io.projectreactor</groupId>
45			<artifactId>reactor-test</artifactId>
46			<scope>test</scope>
47		</dependency>
48	</dependencies>
49
50	<build>
51		<plugins>
52			<plugin>
53				<groupId>org.springframework.boot</groupId>
54				<artifactId>spring-boot-maven-plugin</artifactId>
55			</plugin>
56		</plugins>
57	</build>
58</project>

Coffee

 1package geektime.spring.reactor.webclient.model;
 2
 3import lombok.AllArgsConstructor;
 4import lombok.Builder;
 5import lombok.Data;
 6import lombok.NoArgsConstructor;
 7import org.joda.money.Money;
 8
 9import java.io.Serializable;
10import java.util.Date;
11
12@Data
13@Builder
14@NoArgsConstructor
15@AllArgsConstructor
16public class Coffee implements Serializable {
17    private Long id;
18    private String name;
19    private Money price;
20    private Date createTime;
21    private Date updateTime;
22}

MoneySerializer

 1package geektime.spring.reactor.webclient.support;
 2
 3import com.fasterxml.jackson.core.JsonGenerator;
 4import com.fasterxml.jackson.databind.SerializerProvider;
 5import com.fasterxml.jackson.databind.ser.std.StdSerializer;
 6import org.joda.money.Money;
 7import org.springframework.boot.jackson.JsonComponent;
 8
 9import java.io.IOException;
10
11@JsonComponent
12public class MoneySerializer extends StdSerializer<Money> {
13    protected MoneySerializer() {
14        super(Money.class);
15    }
16
17    @Override
18    public void serialize(Money money, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
19        jsonGenerator.writeNumber(money.getAmount());
20    }
21}

MoneyDeserializer

 1package geektime.spring.reactor.webclient.support;
 2
 3import com.fasterxml.jackson.core.JsonParser;
 4import com.fasterxml.jackson.core.JsonProcessingException;
 5import com.fasterxml.jackson.databind.DeserializationContext;
 6import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
 7import org.joda.money.CurrencyUnit;
 8import org.joda.money.Money;
 9import org.springframework.boot.jackson.JsonComponent;
10
11import java.io.IOException;
12
13@JsonComponent
14public class MoneyDeserializer extends StdDeserializer<Money> {
15    protected MoneyDeserializer() {
16        super(Money.class);
17    }
18
19    @Override
20    public Money deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
21        return Money.of(CurrencyUnit.of("CNY"), p.getDecimalValue());
22    }
23}

启动类 WebclientDemoApplication:运行顺序是不一定的

 1package geektime.spring.reactor.webclient;
 2
 3import geektime.spring.reactor.webclient.model.Coffee;
 4import lombok.extern.slf4j.Slf4j;
 5import org.joda.money.CurrencyUnit;
 6import org.joda.money.Money;
 7import org.springframework.beans.factory.annotation.Autowired;
 8import org.springframework.boot.ApplicationArguments;
 9import org.springframework.boot.ApplicationRunner;
10import org.springframework.boot.Banner;
11import org.springframework.boot.WebApplicationType;
12import org.springframework.boot.autoconfigure.SpringBootApplication;
13import org.springframework.boot.builder.SpringApplicationBuilder;
14import org.springframework.context.annotation.Bean;
15import org.springframework.http.MediaType;
16import org.springframework.web.reactive.function.client.WebClient;
17import reactor.core.publisher.Mono;
18import reactor.core.scheduler.Schedulers;
19
20import java.util.concurrent.CountDownLatch;
21
22@SpringBootApplication
23@Slf4j
24public class WebclientDemoApplication implements ApplicationRunner {
25	@Autowired
26	private WebClient webClient;
27
28	public static void main(String[] args) {
29		new SpringApplicationBuilder(WebclientDemoApplication.class)
30				.web(WebApplicationType.NONE)
31				.bannerMode(Banner.Mode.OFF)
32				.run(args);
33	}
34
35	/**
36	 * 构造 WebClient
37	 * @param builder
38	 * @return
39	 */
40	@Bean
41	public WebClient webClient(WebClient.Builder builder) {
42		return builder.baseUrl("http://localhost:8080").build();
43	}
44
45	@Override
46	public void run(ApplicationArguments args) throws Exception {
47		CountDownLatch cdl = new CountDownLatch(2);
48
49		//get请求:/coffee/{id}
50		webClient.get()
51				.uri("/coffee/{id}", 1)
52				.accept(MediaType.APPLICATION_JSON_UTF8)//设置头信息:要求响应类型是JSON
53				.retrieve()//获取结果
54				.bodyToMono(Coffee.class)//设置body的类型为Coffee类型
55				.doOnError(t -> log.error("Error: ", t))//打印错误如果有的话
56				.doFinally(s -> cdl.countDown())//finally操作
57				.subscribeOn(Schedulers.single())
58				.subscribe(c -> log.info("Coffee 1: {}", c));
59				//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)
60
61		//post请求:/coffee/
62		Mono<Coffee> americano = Mono.just(
63				Coffee.builder()
64						.name("americano")
65						.price(Money.of(CurrencyUnit.of("CNY"), 25.00))
66						.build()
67		);
68		webClient.post()
69				.uri("/coffee/")
70				.body(americano, Coffee.class)
71				.retrieve()
72				.bodyToMono(Coffee.class)
73				.doFinally(s -> cdl.countDown())
74				.subscribeOn(Schedulers.single())
75				.subscribe(c -> log.info("Coffee Created: {}", c));
76				//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)
77		cdl.await();
78
79		//get请求:获取coffee整个列表
80		webClient.get()
81				.uri("/coffee/")
82				.retrieve()
83				.bodyToFlux(Coffee.class)//Flux代表取得多个对象 列表
84				.toStream()
85				.forEach(c -> log.info("Coffee in List: {}", c));
86				//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)
87				//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)
88				//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)
89				//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)
90				//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)
91				//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)
92	}
93}

customer-service (示例 访问 Web 资源)

  • 通过编码方式查询咖啡
  • 通过编码方式创建订单

依赖

 1<?xml version="1.0" encoding="UTF-8"?>
 2<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 3	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 4	<modelVersion>4.0.0</modelVersion>
 5	<parent>
 6		<groupId>org.springframework.boot</groupId>
 7		<artifactId>spring-boot-starter-parent</artifactId>
 8		<version>2.1.3.RELEASE</version>
 9		<relativePath/> <!-- lookup parent from repository -->
10	</parent>
11	<groupId>geektime.spring.springbucks</groupId>
12	<artifactId>customer-service</artifactId>
13	<version>0.0.1-SNAPSHOT</version>
14	<name>customer-service</name>
15	<description>Demo project for Spring Boot</description>
16
17	<properties>
18		<java.version>1.8</java.version>
19	</properties>
20
21	<dependencies>
22		<dependency>
23			<groupId>org.springframework.boot</groupId>
24			<artifactId>spring-boot-starter-web</artifactId>
25		</dependency>
26
27		<dependency>
28			<groupId>org.joda</groupId>
29			<artifactId>joda-money</artifactId>
30			<version>1.0.1</version>
31		</dependency>
32
33		<dependency>
34			<groupId>org.apache.commons</groupId>
35			<artifactId>commons-lang3</artifactId>
36		</dependency>
37
38		<dependency>
39			<groupId>org.apache.httpcomponents</groupId>
40			<artifactId>httpclient</artifactId>
41			<version>4.5.7</version>
42		</dependency>
43
44		<dependency>
45			<groupId>org.projectlombok</groupId>
46			<artifactId>lombok</artifactId>
47			<optional>true</optional>
48		</dependency>
49		<dependency>
50			<groupId>org.springframework.boot</groupId>
51			<artifactId>spring-boot-starter-test</artifactId>
52			<scope>test</scope>
53		</dependency>
54	</dependencies>
55
56	<build>
57		<plugins>
58			<plugin>
59				<groupId>org.springframework.boot</groupId>
60				<artifactId>spring-boot-maven-plugin</artifactId>
61			</plugin>
62		</plugins>
63	</build>
64</project>

Coffee

 1package geektime.spring.springbucks.customer.model;
 2
 3import lombok.AllArgsConstructor;
 4import lombok.Builder;
 5import lombok.Data;
 6import lombok.NoArgsConstructor;
 7import org.joda.money.Money;
 8
 9import java.io.Serializable;
10import java.util.Date;
11
12@Data
13@Builder
14@NoArgsConstructor
15@AllArgsConstructor
16public class Coffee implements Serializable {
17    private Long id;
18    private String name;
19    private Money price;
20    private Date createTime;
21    private Date updateTime;
22}

CoffeeOrder

 1package geektime.spring.springbucks.customer.model;
 2
 3import lombok.AllArgsConstructor;
 4import lombok.Data;
 5import lombok.NoArgsConstructor;
 6
 7import java.util.Date;
 8import java.util.List;
 9
10@Data
11@NoArgsConstructor
12@AllArgsConstructor
13public class CoffeeOrder {
14    private Long id;
15    private String customer;
16    private List<Coffee> items;
17    private OrderState state;
18    private Date createTime;
19    private Date updateTime;
20}

OrderState

1package geektime.spring.springbucks.customer.model;
2
3public enum OrderState {
4    INIT, PAID, BREWING, BREWED, TAKEN, CANCELLED
5}

NewOrderRequest

 1package geektime.spring.springbucks.customer.model;
 2
 3import lombok.Builder;
 4import lombok.Getter;
 5import lombok.Setter;
 6import lombok.ToString;
 7
 8import java.util.List;
 9
10@Builder
11@Getter
12@Setter
13public class NewOrderRequest {
14    private String customer;
15    private List<String> items;
16}

MoneySerializer

 1package geektime.spring.springbucks.customer.support;
 2
 3import com.fasterxml.jackson.core.JsonGenerator;
 4import com.fasterxml.jackson.databind.SerializerProvider;
 5import com.fasterxml.jackson.databind.ser.std.StdSerializer;
 6import org.joda.money.Money;
 7import org.springframework.boot.jackson.JsonComponent;
 8
 9import java.io.IOException;
10
11@JsonComponent
12public class MoneySerializer extends StdSerializer<Money> {
13    protected MoneySerializer() {
14        super(Money.class);
15    }
16
17    @Override
18    public void serialize(Money money, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
19        jsonGenerator.writeNumber(money.getAmount());
20    }
21}

MoneyDeserializer

 1package geektime.spring.springbucks.customer.support;
 2
 3import com.fasterxml.jackson.core.JsonParser;
 4import com.fasterxml.jackson.core.JsonProcessingException;
 5import com.fasterxml.jackson.databind.DeserializationContext;
 6import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
 7import org.joda.money.CurrencyUnit;
 8import org.joda.money.Money;
 9import org.springframework.boot.jackson.JsonComponent;
10
11import java.io.IOException;
12
13@JsonComponent
14public class MoneyDeserializer extends StdDeserializer<Money> {
15    protected MoneyDeserializer() {
16        super(Money.class);
17    }
18
19    @Override
20    public Money deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
21        return Money.of(CurrencyUnit.of("CNY"), p.getDecimalValue());
22    }
23}

CustomConnectionKeepAliveStrategy

 1package geektime.spring.springbucks.customer.support;
 2
 3import org.apache.commons.lang3.StringUtils;
 4import org.apache.commons.lang3.math.NumberUtils;
 5import org.apache.http.HttpResponse;
 6import org.apache.http.conn.ConnectionKeepAliveStrategy;
 7import org.apache.http.protocol.HTTP;
 8import org.apache.http.protocol.HttpContext;
 9
10import java.util.Arrays;
11
12public class CustomConnectionKeepAliveStrategy implements ConnectionKeepAliveStrategy {
13    private final long DEFAULT_SECONDS = 30;
14
15    @Override
16    public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
17        return Arrays.asList(response.getHeaders(HTTP.CONN_KEEP_ALIVE))
18                .stream()
19                .filter(h -> StringUtils.equalsIgnoreCase(h.getName(), "timeout")
20                        && StringUtils.isNumeric(h.getValue()))
21                .findFirst()
22                .map(h -> NumberUtils.toLong(h.getValue(), DEFAULT_SECONDS))
23                .orElse(DEFAULT_SECONDS) * 1000;
24    }
25}

启动类

 1package geektime.spring.springbucks.customer;
 2
 3import geektime.spring.springbucks.customer.support.CustomConnectionKeepAliveStrategy;
 4import lombok.extern.slf4j.Slf4j;
 5import org.apache.http.impl.client.CloseableHttpClient;
 6import org.apache.http.impl.client.HttpClients;
 7import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
 8import org.springframework.boot.Banner;
 9import org.springframework.boot.WebApplicationType;
10import org.springframework.boot.autoconfigure.SpringBootApplication;
11import org.springframework.boot.builder.SpringApplicationBuilder;
12import org.springframework.boot.web.client.RestTemplateBuilder;
13import org.springframework.context.annotation.Bean;
14import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
15import org.springframework.web.client.RestTemplate;
16
17import java.time.Duration;
18import java.util.concurrent.TimeUnit;
19
20@SpringBootApplication
21@Slf4j
22public class CustomerServiceApplication {
23
24	public static void main(String[] args) {
25		new SpringApplicationBuilder()
26				.sources(CustomerServiceApplication.class)
27				.bannerMode(Banner.Mode.OFF)
28				.web(WebApplicationType.NONE)
29				.run(args);
30	}
31
32	@Bean
33	public HttpComponentsClientHttpRequestFactory requestFactory() {
34		PoolingHttpClientConnectionManager connectionManager =
35				new PoolingHttpClientConnectionManager(30, TimeUnit.SECONDS);
36		connectionManager.setMaxTotal(200);
37		connectionManager.setDefaultMaxPerRoute(20);
38
39		CloseableHttpClient httpClient = HttpClients.custom()
40				.setConnectionManager(connectionManager)
41				.evictIdleConnections(30, TimeUnit.SECONDS)
42				.disableAutomaticRetries()
43				// 有 Keep-Alive 认里面的值,没有的话永久有效
44				//.setKeepAliveStrategy(DefaultConnectionKeepAliveStrategy.INSTANCE)
45				// 换成自定义的
46				.setKeepAliveStrategy(new CustomConnectionKeepAliveStrategy())
47				.build();
48
49		HttpComponentsClientHttpRequestFactory requestFactory =
50				new HttpComponentsClientHttpRequestFactory(httpClient);
51
52		return requestFactory;
53	}
54
55	@Bean
56	public RestTemplate restTemplate(RestTemplateBuilder builder) {
57		return builder
58				.setConnectTimeout(Duration.ofMillis(100))
59				.setReadTimeout(Duration.ofMillis(500))
60				.requestFactory(this::requestFactory)
61				.build();
62	}
63}

CustomerRunner

 1package geektime.spring.springbucks.customer;
 2
 3import geektime.spring.springbucks.customer.model.Coffee;
 4import geektime.spring.springbucks.customer.model.CoffeeOrder;
 5import geektime.spring.springbucks.customer.model.NewOrderRequest;
 6import lombok.extern.slf4j.Slf4j;
 7import org.springframework.beans.factory.annotation.Autowired;
 8import org.springframework.boot.ApplicationArguments;
 9import org.springframework.boot.ApplicationRunner;
10import org.springframework.core.ParameterizedTypeReference;
11import org.springframework.http.HttpMethod;
12import org.springframework.http.RequestEntity;
13import org.springframework.http.ResponseEntity;
14import org.springframework.stereotype.Component;
15import org.springframework.web.client.RestTemplate;
16import org.springframework.web.util.UriComponentsBuilder;
17
18import java.util.Arrays;
19import java.util.List;
20
21@Component
22@Slf4j
23public class CustomerRunner implements ApplicationRunner {
24    @Autowired
25    private RestTemplate restTemplate;
26
27    @Override
28    public void run(ApplicationArguments args) throws Exception {
29        readMenu();
30        Long id = orderCoffee();
31        queryOrder(id);
32    }
33
34    //遍历菜单
35    private void readMenu() {
36        ParameterizedTypeReference<List<Coffee>> ptr =
37                new ParameterizedTypeReference<List<Coffee>>() {};
38        ResponseEntity<List<Coffee>> list = restTemplate
39                .exchange("http://localhost:8080/coffee/", HttpMethod.GET, null, ptr);
40        list.getBody().forEach(c -> log.info("Coffee: {}", c));
41    }
42    //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)
43    //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)
44    //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)
45    //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)
46    //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)
47
48    //下单
49    private Long orderCoffee() {
50        NewOrderRequest orderRequest = NewOrderRequest.builder()
51                .customer("Li Lei")
52                .items(Arrays.asList("capuccino"))
53                .build();
54        RequestEntity<NewOrderRequest> request = RequestEntity
55                .post(UriComponentsBuilder.fromUriString("http://localhost:8080/order/").build().toUri())
56                .body(orderRequest);
57        ResponseEntity<CoffeeOrder> response = restTemplate.exchange(request, CoffeeOrder.class);
58        log.info("Order Request Status Code: {}", response.getStatusCode());
59        //Order Request Status Code: 201 CREATED
60        Long id = response.getBody().getId();
61        log.info("Order ID: {}", id);
62        //Order ID: 1
63        return id;
64    }
65
66    //根据订单号查询订单
67    private void queryOrder(Long id) {
68        CoffeeOrder order = restTemplate
69                .getForObject("http://localhost:8080/order/{id}", CoffeeOrder.class, id);
70        log.info("Order: {}", order);
71        //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)
72    }
73}

作者:Soulboy