目录

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

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)
    }
}

作者:Soulboy