六、远程访问@HttpExchange[SpringBoot3]
- 远程访问是开发的常用技术,一个应用能够访问其他应用的功能。SpringBoot提供了多种远程访问的技术。基于HTTP协议的远程访问是最广泛的。
- SpringBoot中定义接口提供HTTP服务。生成的代理对象实现此接口,代理对象实现HTTP的远程访问,需要理解:
- @HttpExchange
- WebClient
WebClient特性
- 我们想要调用其他系统提供的HTTP服务,通常可以使用Spring提供的RestTemplate来访问,RestTemplate是SpringBoot3中引入的同步阻塞式HTTP客户端,因此存在一定性能瓶颈。Spring官方在Spring5中引入了WebClient作为非阻塞式HTTP客户端。
- 非阻塞,异步请求
- 它的响应式编程基于Reactor
- 高并发,硬件资源少
- 支持Java 8 lambdas函数式编程
什么是异步非阻塞
- 异步和同步针对调用者,调用者发送请求,如果等待对方回应之后才去做其他事情,就是同步,如果发送请求之后不等着对方回应就去做其他事情就是异步
- 阻塞和非阻塞针对被调度者,被调度者收到请求后,做完请求任务之后才给出反馈就是阻塞,收到请求之后马上给出反馈然后去做事情,就是非阻塞。
6.1准备工作
- 安装GsonFormat插件,方便json和Bean的转换
6.2声明式HTTP远程服务
- 需求:访问https://jsonplaceholder.typicode.com/提供的todos服务。基于RESTful风格,增删改查。
1.Maven依赖pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--WebClient-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</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>
2.声明Todo数据类
@Data
public class Todo {
private Integer userId;
private Integer id;
private String title;
private Boolean completed;
}
3.声明服务接口
public interface TodoService {
// 一个方法就是一个远程服务(远程调用)
@GetExchange("/todos/{id}")
Todo getTodoById(@PathVariable("id") Integer id);
//增加资源
@PostExchange(value = "/todos/", accept = MediaType.APPLICATION_JSON_VALUE)
Todo createTodo(@RequestBody Todo newTodo);
//修改资源
@PutExchange("/todos/{id}")
ResponseEntity<Todo> modifyTodo(@PathVariable Integer id, @RequestBody Todo todo);
//删除资源
@DeleteExchange("/todos/{sid}")
void removeTodo(@PathVariable("sid") Integer id);
}
4.创建HTTP服务代理对象
//proxyBeanMethods = false:多实例对象,无论被取出多少此都是不同的bean实例,在该模式下SpringBoot每次启动会跳过检查容器中是否存在该组件
@Configuration(proxyBeanMethods = false)
public class HttpConfiguration {
//创建服务接口的代理对象,基于WebClient
@Bean
public TodoService requestService() {
WebClient webClient =
WebClient.builder().baseUrl("https://jsonplaceholder.typicode.com").build();
//创建代理工厂,设置超时时间
HttpServiceProxyFactory proxyFactory =
HttpServiceProxyFactory.builder(WebClientAdapter.forClient(webClient)).blockTimeout(Duration.ofSeconds(60)).build();
//创建某个接口的代理服务
return proxyFactory.createClient(TodoService.class);
}
}
5.单元测试
@SpringBootTest
class Springboot18HttpServiceApplicationTests {
//注入代理对象
@Resource
private TodoService todoService;
//测试访问todos/1
@Test
void testQuery() {
Todo todo = todoService.getTodoById(1);
System.out.println("todo = " + todo);
System.out.println(todo.getTitle());
}
//创建资源
@Test
void testCreateTodo() {
Todo todo = new Todo();
todo.setId(1222);
todo.setUserId(1223);
todo.setTitle("事项1");
todo.setCompleted(true);
Todo res = todoService.createTodo(todo);
System.out.println("res = " + res);
}
//修改资源
@Test
void testModify() {
Todo todo = new Todo();
todo.setId(1002);
todo.setUserId(5002);
todo.setTitle("事项2");
todo.setCompleted(true);
ResponseEntity<Todo> entity = todoService.modifyTodo(2, todo);
HttpHeaders headers = entity.getHeaders();
System.out.println("headers = " + headers);
Todo body = entity.getBody();
System.out.println("body = " + body);
HttpStatusCode statusCode = entity.getStatusCode();
System.out.println("statusCode = " + statusCode);
}
//删除资源
@Test
void testDelete() {
todoService.removeTodo(10);
}
}
6.3Http服务接口的方法定义
-
@HttpExchange注解用于声明接口作为HTTP远程服务。在方法、类级别使用。通过注解属性以及方法的参数设置HTTP请求的细节。
-
快捷注解简化不同的请求方式:
- GetExchange
- PostExchange
- PutExchange
- PatchExchange
- DeleteExchange
-
@GetExchange就是@HttpExchange表示的GET请求方式
-
作为HTTP服务接口中的方法允许使用的参数列表
- 接口中方法返回值
6.4组合使用注解
- @HttpExchange、@GetExchange等可以组合使用。
1.创建Albums数据类
@Data
public class Albums {
private Integer id;
private Integer userId;
private String title;
}
2.创建AlbumsService接口
- 接口声明方法,提供HTTP远程服务。
@HttpExchange(url = "https://jsonplaceholder.typicode.com/")
public interface AlbumsService {
//查询专辑
@HttpExchange(method = "GET",url = "/albums/{id}")
Albums getById(@PathVariable Integer id);
}
3.声明代理
@Bean
//创建代理
public AlbumsService albumsService() {
WebClient webClient = WebClient.create();
HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builder(WebClientAdapter.forClient(webClient)).blockTimeout(Duration.ofSeconds(60)).build();
return proxyFactory.createClient(AlbumsService.class);
}
4.单元测试
@SpringBootTest
public class AlbumsServiceTest {
@Resource
private AlbumsService albumsService;
@Test
void testQuery() {
Albums albums = albumsService.getById(5);
System.out.println("albums = " + albums);
}
}
6.5Java Record
- 测试Java Record作为返回类型。
创建Albums的Java Record
public record AlbumsRecord(Integer id, Integer userId, String title) {
}
其余步骤一样
6.6定制HTTP请求服务
- 设置HTTP远程的超时时间,异常处理
- 在创建接口代理对象前,先设置WebClient的有关配置。
1.设置超时,异常处理
//定制HTTP服务
@Bean
public AlbumsService albumsService() {
//超时
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 30000)//连接时间
.doOnConnected(conn -> {
conn.addHandlerLast(new ReadTimeoutHandler(10));//读超时
conn.addHandlerLast(new WriteTimeoutHandler(10));//写超时
});
//设置异常
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
//定制 4XX,5XX 的回调函数
.defaultStatusHandler(HttpStatusCode::isError, clientResponse -> {
System.out.println("WebClient请求异常");
return Mono.error(new RuntimeException("请求异常" + clientResponse.statusCode().value()));
}).build();
HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builder(WebClientAdapter.forClient(webClient)).blockTimeout(Duration.ofSeconds(60)).build();
return proxyFactory.createClient(AlbumsService.class);
}
2.单元测试