目录
常见的HttpClient
Spring 提供的HttpClient
RestTemplate
Spring 提供的模板类 XXXTemplate
RestTemplate的使用
RestTemplate的使用技巧
RestTemplate的问题
RestClient
RestClinet的基本使用
RestClient的自动配置
RestClient 序列化对象
异常处理 onStatus
更精细的控制 exchange
HTTP消息转换 - Jackson Json 视图
RestClient真正使用的HTTP库
非异步调用
WebClient
简单使用
请求拦截器
深度配置WebClient
HTTP接口
配置和使用
常见的HttpClient
我们项目中通常要调用内部或者外部的HTTP接口,这时候就需要用到Http Client, 以前我们经常用第三方的组件比如:
- Apache HttpClient
- OkHttp
- Spring Cloud提供的Feign Http客户端
但是对于一般的项目,我们直接使用Spring 提供的HttpClient客户端就可以满足大部分的场景了。
所以严格上说,本文介绍的不是Spring Boot的特性,而是Spring提供的特性。
Spring 提供的HttpClient
Spring框架提供了几种调用REST API的方式:
- RestTemplate:调用API的同步客户端
- RestClient:异步客户端
- WebClient:异步并且是响应式的客户端
- Http Interface:基于动态代理的声明式接口
说明:
本文我们会用一个开源的HTTP REST API用来测试:
https://jsonplaceholder.typicode.com/users
为此,我们定义了Java类接收json响应
@Builder @Data public class TypiUser {private Integer id;private String name;private String username;private String email;private String phone;private String website;private TypiAddress address; }@Builder @Data public class TypiAddress {private String street;private String suite;private String city;private String zipcode; }
Tips:
使用builder构造对象时,报错:
java: cannot find symbolsymbol: method builder()location: class lab.rest.TypiUser
后来打开target/classes 看编译后的类才发现TypiUser没有编译正确,所以执行 `mvn clean build` 后可以解决报错问题。
发现另一个地方设置也问题,应该不要勾选Processor Path
RestTemplate
Spring 提供的模板类 XXXTemplate
熟悉Spring框架的开发都知道,Spring提供了各种XXXTemplate,使用模板方法模式封装了复杂的底层操作,简化了我们对外部组件的操作和使用。
常见的有:
- JdbcTemplate:封装了JDBC的底层操作,简化了数据库操作
- JmsTemplate:封装了对JMS API的操作,简化了收发消息的操作
- ElasticsearchRestTemplate:是Spring Data ES模块的一部分,简化了对ES的操作
- RedisTemplate:是Spring Data Redis模块的一部分,简化了对Redis的操作
- HibernateTemplate:简化了对Hibernate的CRUD操作
- ……
扩展:模板方法模式
这些XXXTemplate用到的是模版方法模式
- 在在抽象父类中定义了算法的骨架
- 然后子类中实现某些步骤(抽象方法)
RestTemplate的使用
而本章节,我们要介绍的是RestTemplate,它是Spring用于进行HTTP 发送请求和接收响应的客户端工具。使得我们调用REST API变得非常的简单。
使用RestTemplate发送post请求
@Test
public void basicTest(){RestTemplate restTemplate = new RestTemplate();String url = "https://jsonplaceholder.typicode.com/users";TypiUser user = TypiUser.builder().name("Joe").username("joe").email("joe@gmail.com").phone("123456789").website("joe.com").address(TypiAddress.builder().street("street").suite("suite").city("city").zipcode("zipcode").build()).build();ResponseEntity<TypiUser> response = restTemplate.postForEntity(url, user, TypiUser.class);TypiUser body = response.getBody();System.out.printf("用户创建成功,id:%s, name:%s%n", body.getId(), body.getName());
}
可以看到使用RestTemplate 发送RestTemplate非常简单。
我们在Web开发中通常会通过配置类来配置RestTemplate实例
@Configuration
public class RestTemplateConfig {@Beanpublic RestTemplate restTemplate(RestTemplateBuilder builder) {return builder.setConnectTimeout(Duration.ofSeconds(10)) //设置连接超时时间.setReadTimeout(Duration.ofSeconds(10)) // 设置读取超时时间.build();}
}
然后注入RestTemplate直接使用
@Service
public class TypiUserService {private final RestTemplate restTemplate;public TypiUserService(RestTemplate restTemplate) {this.restTemplate = restTemplate;}public TypiUser getUser(Integer id) {return restTemplate.getForObject("https://jsonplaceholder.typicode.com/users/" + id,TypiUser.class);}
}
RestTemplate的使用技巧
一般场景,我们可以直接用getXXX方法发起get请求,通过postForEntity发起post请求,通过put()方法发起put请求,通过delete发起delete请求。
但某些时候我们想更精准的控制client的时候,就需要用到更原始的方法exchange(),它允许我们指定HTTP方法,处理请求头和请求体。
public TypiUser getUserByExchange(String id) {String url = "https://jsonplaceholder.typicode.com/users/" + id;ResponseEntity<TypiUser> response = restTemplate.exchange(url, HttpMethod.GET, null, TypiUser.class);TypiUser user = response.getBody();System.out.println(user);System.out.println(response.getStatusCode());System.out.println(response.getHeaders());return user;
}
异常处理
接口如果处理有问题,RestTemplate 也会抛出一些异常,尤其是在遇到错误响应时。常见的异常包括:
- HttpClientErrorException:用于 4xx 错误。
- HttpServerErrorException:用于 5xx 错误。
- ResourceAccessException:网络或连接问题时抛出。
我们需要捕获并处理这些异常。
一种方式是我们直接在service中捕获
public TypiUser getUser(Integer id) {try{return restTemplate.getForObject("https://jsonplaceholder.typicode.com/users/" + id,TypiUser.class);}catch (HttpClientErrorException e){throw new RuntimeException("客户端异常", e);}catch (HttpServerErrorException e){throw new RuntimeException("服务器端异常", e);}catch (RestClientException e) {throw new RuntimeException("Rest Client异常", e);}
}
但是一般没人这么干,因为如果我们有100个方法调用rest client,那不是得写100遍。所以我们一般会统一的处理异常,使用Spring提供的@ControllerAdvice 和 @ExceptionHandler全局捕获并处理异常。
@ControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandler(HttpClientErrorException.class)public ResponseEntity<ApiErrorResponse> handleHttpClientError(HttpClientErrorException e) {ApiErrorResponse errorResponse = new ApiErrorResponse(e.getStatusCode().value(),"Client Error",e.getResponseBodyAsString());return new ResponseEntity<>(errorResponse, HttpStatus.valueOf(e.getStatusCode().value()));}// 略
}
不仅仅是RestTemplate,其它的Client也需要这种全局的异常处理。
RestTemplate的问题
RestTemplate简单,但是也存在一些问题。
最重要的一点是它是同步的,它的每个请求都会阻塞直到收到响应。因为每个请求都占用一个线程,所以当大量请求同时发起时,导致系统的线程数会很快被耗尽,所以他的并发性能比较低。
RestClient
Spring引入了更现代化的HTTP客户端 RestClient.
它本身也是同步的,不支持异步。但是它支持流式的调用方式,相比RestTemplate提供一个方法发起请求,流式调用更容易使用和控制。
RestClinet的基本使用
@Service
public class TypiRestClientService {private final RestClient.Builder builder;private RestClient restClient;public TypiRestClientService(RestClient.Builder builder) {this.builder = builder;}// 使用 @PostConstruct 注解在 Spring 完成构造器注入后再进行初始化@PostConstructpublic void init() {// 使用 builder 创建 RestClient 实例,进行初始化this.restClient = this.builder.baseUrl("https://jsonplaceholder.typicode.com").build();}public TypiUser getUser(Integer id) {return restClient.get().uri("/users/" + id).retrieve().body(TypiUser.class);}
}
RestClient的自动配置
我们都没有显示的配置RestClient.Builder,它是怎么自动注入的呢?
还是那一套,SpringBoot的自动配置。
在autoconfiguration包下面
可以看到,自动配置了RestClient和RestTemplate
这里自动配置了Builder Bean
@ConditionalOnClass(RestClient.class) 这个注解表示,RestClientAutoConfiguration 只会在类路径中存在 RestClient 类时才会被加载
@Conditional(NotReactiveWebApplicationCondition.class)如果是反应式应用,RestClientAutoConfiguration 就不会被加载。
@AutoConfiguration(after = { HttpClientAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class,SslAutoConfiguration.class })
@ConditionalOnClass(RestClient.class)
@Conditional(NotReactiveWebApplicationCondition.class)
public class RestClientAutoConfiguration {@Bean@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)@ConditionalOnMissingBeanRestClient.Builder restClientBuilder(RestClientBuilderConfigurer restClientBuilderConfigurer) {return restClientBuilderConfigurer.configure(RestClient.builder());}
}
直接注入RestClient行不行呢?
@Service
public class TypiRestClientService {private final RestClient restClient;public TypiRestClientService(RestClient restClient) {this.restClient = restClient;}
当然不行,因为自动配置类中并没有配置RestClient的Bean.
启动报错如下:
Action:
Consider defining a bean of type 'org.springframework.web.client.RestClient' in your configuration.
RestClient 序列化对象
requestbody和responseBody会自动转对象
public TypiUser saveUser() {TypiUser user = TypiUser.builder().name("Joe").username("joe").email("joe@gmail.com").address(TypiAddress.builder().city("Beijing").build()).build();TypiUser result = restClient.post().uri("/users").body(user).retrieve().body(TypiUser.class);return result;}
retrieve方法之前的body()是request body, 后面的body()是 response body.
异常处理 onStatus
它提供了很优雅的异常处理
TypiUser result = restClient.post().uri("/users").body(user).retrieve().onStatus(HttpStatusCode::is4xxClientError, ((request, response) -> {throw new RuntimeException("status code: " + response.getStatusCode());})).body(TypiUser.class);
也是抛出异常,在全局类里面捕获并处理异常。
更精细的控制 exchange
如果想要更精准的控制请求和响应,我们可以利用更底层的方法来发起请求。
TypiUser result = restClient.post().uri("/users").body(user).exchange((request, response) -> {if(response.getStatusCode().is2xxSuccessful()){return response.bodyTo(TypiUser.class);} else if(response.getStatusCode().is4xxClientError()){throw new RuntimeException("客户端错误,status code: " + response.getStatusCode());} else {throw new RuntimeException("其它错误,status code: " + response.getStatusCode());}});
虽然可以更精细操作,但是操作得都是底层的request和response对象,也变得更麻烦了。世界上没有绝对完美的事情。
HTTP消息转换 - Jackson Json 视图
发送请求时,我们不想把整个对象的所有字段都序列化成json后发送,而是序列化部分字段,可以做到的吗?
当然可以,最笨的方法是直接新建一个新的对象,只包含部分属性,然后用BeanUtils.copy复制属性。
因为我们用的是Jackson,所以可以更优雅
使用Jackson的json视图
MappingJacksonValue value = new MappingJacksonValue(user);
value.setSerializationView(TypiUser.TypiUserView.class);
TypiUser result = restClient.post().uri("/users").body(value).retrieve().onStatus(HttpStatusCode::is4xxClientError, ((request, response) -> {throw new RuntimeException("status code: " + response.getStatusCode());})).body(TypiUser.class);return result;
第一次听到这个技术,挺有意思。
RestClient真正使用的HTTP库
RestClient是对外提供的客户端,要执行HTTP请求,它底层还是得依赖其它HTTP库。
很像Slf4j, 它是个门面,底层是需要用log4j等日志框架的。
在 Spring 中,RestClient 通过适配不同的 HTTP 库来执行 HTTP 请求。这些库的适配是通过实现 ClientRequestFactory 接口来实现的。
具体实现有:
- JdkClientHttpRequestFactory:使用的是 Java的 HttpClient
- HttpComponentsClientHttpRequestFactory 使用的是Apache HTTP Components HttpClient
- JettyClientHttpRequestFactory:使用Jetty的 HttpClient
- ReactorNettyClientRequestFactory:使用Reactor Netty的 HttpClient
- SimpleClientHttpRequestFactory:简单的实现
我们什么也没有配置,使用默认配置,看看是什么工厂类:
可以看到,使用的是JDK的HTTP Client。
非异步调用
RestClient也是同步HTTP客户端。但是对比RestTemplate,API是流式的更容易使用。如果没有异步的场景需求,我们可以使用RestClient。
WebClient
RestTemplate和RestClient都是同步的,WebClient是非阻塞 响应式的HTTP 客户端。
它是Spring5引入的,用来替代RestTemplate。
简单使用
WebClient是Spring WebFlux模块的功能,所以我们需要先引入依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId></dependency>
WebClient也是SpringBoot自动配置的,我们直接注入使用即可
@Service
public class TypiWebClientService {private final WebClient webClient;public TypiWebClientService(WebClient.Builder builder) {this.webClient = builder.baseUrl("https://jsonplaceholder.typicode.com").build();}
}
自动配置类
配置条件是classpath下找到了WebClient就自动配置WebClient.Builder这个Bean。所以我们注入的是WebClient.Builder这个Bean 而不是WebClient。
这块配置配置和RestClient一样!
@AutoConfiguration(after = { CodecsAutoConfiguration.class, ClientHttpConnectorAutoConfiguration.class })
@ConditionalOnClass(WebClient.class)
public class WebClientAutoConfiguration {@Bean@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)@ConditionalOnMissingBeanpublic WebClient.Builder webClientBuilder(ObjectProvider<WebClientCustomizer> customizerProvider) {WebClient.Builder builder = WebClient.builder();customizerProvider.orderedStream().forEach((customizer) -> customizer.customize(builder));return builder;}
发起异步调用
public TypiUser getUser(Integer id) {Mono<TypiUser> mono = webClient.get().uri("/users/{id}", id).retrieve().bodyToMono(TypiUser.class).doOnTerminate(() -> System.out.println("调用结束"));System.out.println("继续调用");TypiUser user = mono.block(); // 阻塞等待System.out.println("获取到用户:" + user);return user;}
继续调用
调用结束
获取到用户:TypiUser(id=1, name=Leanne Graham, username=Bret, email=Sincere@april.biz, phone=1-770-736-8031 x56442, website=hildegard.org, address=TypiAddress(street=Kulas Light, suite=Apt. 556, city=Gwenborough, zipcode=92998-3874))
可以看到,发起请求后,线程并没有blocked住,而是继续往下走,直到执行到block()方法时,线程才会阻塞。
以前看了这么多八股文,很难理解异步WebClient。其实只要写个最简单的例子,就非常容易理解了。Java八股害死人。
请求拦截器
我们可以拦截请求,做一些特殊的处理。
public TypiWebClientService(WebClient.Builder builder) {this.webClient = builder.baseUrl("https://jsonplaceholder.typicode.com").filter((request, next) -> {System.out.println("开始调用,路径 " + request.url().toString());return next.exchange(request);}).build();}
这样所有的请求,都会执行这个操作
继续调用
开始调用,路径 https://jsonplaceholder.typicode.com/users/1
调用结束
深度配置WebClient
在配置类中配置WebClient Bean
配置连接池和选择底层使用的HTTP客户端。
@Configuration
public class WebClientConfig {@Beanpublic WebClient webClient() {ConnectionProvider provider = ConnectionProvider.builder("custom").maxConnections(50) // 设置最大连接数.build();HttpClient httpClient = HttpClient.create(provider).responseTimeout(Duration.ofSeconds(5)) // 设置响应超时.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);return WebClient.builder().clientConnector(new ReactorClientHttpConnector(httpClient)).baseUrl("https://jsonplaceholder.typicode.com").build();}
}
这里我们使用的HTTP Client是Netty的客户端。
并设置了连接池的大小还有HTTP的响应时间。
这样我们可以直接注入使用WebClient了
@Service
public class TypiWebClientService {private final WebClient webClient;public TypiWebClientService(WebClient webClient) {this.webClient = webClient;}
HTTP接口
Spring允许我们通过Java接口的方式调用HTTP服务。我们不需要写代码显示调用HTTP,而是通过注解声明即可。
底层是利用动态代理技术,简化了远程 HTTP 调用。
配置和使用
首先,我们定义一个接口,并用注解声明
public interface TypiUserRestService {@GetExchange("/users/{id}")TypiUser getUser(@PathVariable Integer id);
}
然后,我们需要创建一个代理,底层还是需要其它HTTP库的
当然我们也可以选用其它的库比如WebClient等
@Configuration
public class TypiUserRestServiceConfig {@Beanpublic TypiUserRestService config(){RestClient restClient = RestClient.builder().baseUrl("https://jsonplaceholder.typicode.com").build();RestClientAdapter adapter = RestClientAdapter.create(restClient);HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();return factory.createClient(TypiUserRestService.class);}
}
接下来,我们就可以在我们的Controller中注入直接使用了。
@RestController
@RequestMapping("/typi")
public class TypiUserController {private final TypiUserRestService typiUserRestService;@GetMapping("/v4/user/{id}")public TypiUser getUser4(@PathVariable Integer id){return typiUserRestService.getUser(id);}
这种声明式的调用,让我们的代码显得非常简洁!
很类似Spring Cloud Feign的调用方式。