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