Spring6 HTTP interface 初体检

本文作者: ggggght
github: https://github.com/GGGGGHT
从Spring6和SpringBoot3开始, Spring框架支持通过注解的形式将远程HTTP服务代理为Java接口. 类似于Feign和Retrofit.
声明式的HTTP接口包括请求的方式等信息.这样我们可以使用带注解的Java接口简单地表达远程API的细节.让Spring生成一个实现该接口并执行请求的代理. 这样可以有效的减少模板代码.
请求方式
@HttpExchange是可以应用于HTTP接口及exchange方法的根注解. 如果在某个接口上声明,则该方法支持以下的所有的注解的类型. 类似于@RequestMapping.
子注解有如下:
- @GetExchange 用于HTTP GET请求
- @PostExchange 用于HTTP POST请求
- @PutExchange 用于HTTP PUT请求
- @PatchExchange 用于HTTP PATCH请求
- @DeleteExchange 用于HTTP DELETE请求
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.service.annotation.DeleteExchange;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.service.annotation.PostExchange;
import org.springframework.web.service.annotation.PutExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@HttpExchange(url = "/users", accept = "application/json", contentType = "application/json")
public interface UserClient {
@GetExchange("/")
Flux<User> getAll();
@GetExchange("/{id}")
Mono<User> getById(@PathVariable("id") Long id);
@PostExchange("/")
Mono<ResponseEntity<Void>> save(@RequestBody User user);
@PutExchange("/{id}")
Mono<ResponseEntity<Void>> update(@PathVariable Long id, @RequestBody User user);
@DeleteExchange("/{id}")
Mono<ResponseEntity<Void>> delete(@PathVariable Long id);
}
返回值
上面的示例中,exchange方法都是阻塞示的返回值, 同样的声明示的HTTP interface 也支持响应示的返回值.即flux. 此外,我们也可以选择返回特定的响应信息,例如返回状态码或者响应头等等, 此外也可以直接返回void. 具体支持的返回值如下:
- void, Mono<Void> 执行请求忽略响应内容
- HttpHeaders, Mono<HttpHeaders> 执行请求,返回响应头
- <T>, Mono<T>:执行请求并将响应内容解码为声明的类型
- <T>, Flux<T>:执行请求并将响应内容解码为声明类型的流
- ResponseEntity<Void>, Mono<ResponseEntity<Void> :执行请求,忽略 响应内容,返回一个包含状态和头部的ResponseEntity
- ResponseEntity<T>, Mono<ResponseEntity<T>:执行请求,释放响应内容,并返回一个包含状态、头和解码主体的ResponseEntity
- Mono<ResponseEntity<Flux<T> :执行请求,忽略响应内容,返回一个包含状态、头部和解码后的响应体流的ResponseEntity
// 阻塞式获取
@GetExchange("/{id}")
User getById(...);
// 响应式获取
@GetExchange("/{id}")
Mono<User> getById(...);
客户端代理
HTTP interface是spring-web依赖的一部分,当我们包含spring-boot-starter-web或者spring-boot-starter-webflux时,它会被传递进来. 如果我们需要返回响应式的结果,需要依赖webflux
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
// 首先需要一个响应式的Web客户端
WebClient webClient = WebClient.builder()
.baseUrl(serviceUrl)
.build();
// HttpServiceProxyFactory是一个从HTTP interface创建客户端代理的工厂 spring 6之后提供
HttpServiceProxyFactory httpServiceProxyFactory = HttpServiceProxyFactory
.builder(WebClientAdapter.forClient(webClient))
.build();
booksService = httpServiceProxyFactory.createClient(BooksService.class);
生成客户端代理之后,我们可以将客户端代理实例注册为Spring Bean,并使用它与服务提供者进行交换数据. baseUrl的方法的参数即是服务提供者的url. 可以使用loadBalance进行客户端的负载均衡. 之后将会演示.
启动简单的HTTP server
- 通过jwebserver启动 jwebserver 是jdk18之后提供的一个新的命令行工具.是一个开箱即用,设置简单,功能单一的HTTP文件服务器.
- 通过jshell启动一个最小的自定义的服务器 jshell 是JDK9之后提供的一个REPL(Read-Eval-Print Loop)环境.通过这个交互式的工具可以减少许多模板代码的编写.
jshell> import com.sun.net.httpserver.*
jshell> var server = HttpServer.create(new InetSocketAddress(8080), 10, "/hello", HttpHandlers.of(200, Headers.of(), "Hello from jwebserver port: 8080")
jshell> server.start()
.\jwebserver.exe -d C:\Users\xx\http_server\9093 -p 9093
code
plugins {
id 'org.springframework.boot' version '3.0.2'
id 'io.spring.dependency-management' version '1.1.0'
id 'org.jetbrains.kotlin.jvm' version '1.7.22'
id 'org.jetbrains.kotlin.plugin.spring' version '1.7.22'
}
dependencies {
implementation 'com.alibaba.boot:nacos-config-spring-boot-starter:0.2.12'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin'
implementation 'org.jetbrains.kotlin:kotlin-reflect'
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
implementation 'com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-discovery:2022.0.0.0-RC1'
implementation 'org.springframework.cloud:spring-cloud-starter-gateway:4.0.3'
implementation 'org.springframework.cloud:spring-cloud-starter-loadbalancer:4.0.1'
}
package com.ggggght.spring6
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.event.ApplicationReadyEvent
import org.springframework.boot.runApplication
import org.springframework.cloud.client.DefaultServiceInstance
import org.springframework.cloud.client.ServiceInstance
import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction
import org.springframework.cloud.gateway.route.RouteLocator
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier
import org.springframework.context.ApplicationListener
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Primary
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.support.WebClientAdapter
import org.springframework.web.service.annotation.GetExchange
import org.springframework.web.service.invoker.HttpServiceProxyFactory
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
@SpringBootApplication
@LoadBalancerClient(name = "say-hello", configuration = [SayHelloConfiguration::class])
class K8sApplication {
@Bean
fun gateway(rlb: RouteLocatorBuilder): RouteLocator? {
return rlb
.routes()
.route { rs ->
rs.path("/proxy")
.filters { f -> f.setPath("/") }
.uri("http://localhost:9093/")
}
.build()
}
@Bean
fun myHttpClient(lb: ReactorLoadBalancerExchangeFilterFunction) : MyHttpClient {
return HttpServiceProxyFactory.builder(
WebClientAdapter.forClient(
WebClient.builder().filter(lb).baseUrl("http://say-hello").build()
)
).build().createClient(MyHttpClient::class.java)
}
// @Bean
fun readyListener(client: MyHttpClient): ApplicationListener<ApplicationReadyEvent?>? {
return ApplicationListener {
client.getFromServer().subscribe {
println("===")
println(it)
println("===")
}
}
}
}
@RestController
class MyController(val myHttpClient: MyHttpClient) {
@GetMapping("/getPort")
fun getPort() {
myHttpClient.getFromServer().subscribe {
println("current port is $it")
}
}
}
interface MyHttpClient {
@GetExchange
fun getFromServer(): Mono<String>
}
fun main(args: Array<String>) {
runApplication<K8sApplication>(*args)
}
class SayHelloConfiguration {
@Bean
@Primary
fun serviceInstanceListSupplier(): ServiceInstanceListSupplier {
return DemoServiceInstanceListSuppler("echo self port")
}
}
internal class DemoServiceInstanceListSuppler(private val serviceId: String) : ServiceInstanceListSupplier {
override fun getServiceId(): String {
return serviceId
}
override fun get(): Flux<List<ServiceInstance>> {
return Flux.just(
listOf(
DefaultServiceInstance(serviceId + "1", serviceId, "localhost", 9091, false),
DefaultServiceInstance(serviceId + "2", serviceId, "localhost", 9092, false),
DefaultServiceInstance(serviceId + "3", serviceId, "localhost", 9093, false)
)
)
}
}
misc
待续.