一、简介
传统feignclient底层使用RestClient并且已经有了功能非常完善的工具包
spring-cloud-starter-openfeign
最近项目需要使用非阻塞式调用,于是考虑到使用webclient,但查了相关资料,发现没有类似于openfeign这种的声明式(接口)的客户端,本文将解决这一问题,供后续开发人员接入
二、代码
-
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>3.3.10</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.36</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>\
<version>4.1.5</version>
</dependency>
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>2.1.1</version>
</dependency>
springboot版本为3.3.10
-
自定义注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(WebClientRegister.class)
public @interface EnableWebClient {//类似于@EnableFeignClients
}
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface WebfluxClient { // 类似于@FeignClient
String name(); // 服务名
String url() default ""; // url
long connectTimeout() default 3000; //连接超时时间
long readTimeout() default 5000; // 响应超时时间
}
//服务名和url同时存在时,优先使用url,使用服务名支持负载均衡
服务名和url同时存在时,优先使用url,使用服务名支持负载均衡
-
注册器
public class WebClientRegister implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
//自定义的 包扫描器
WebClientScan scanHandle = new WebClientScan(beanDefinitionRegistry,false);
//扫描指定路径下的接口
scanHandle.doScan(ClassUtils.getPackageName(annotationMetadata.getClassName()));
}
}
-
扫描器
@Slf4j
public class WebClientScan extends ClassPathBeanDefinitionScanner {
public WebClientScan(BeanDefinitionRegistry registry, boolean useDefaultFilters) {
super(registry, useDefaultFilters);
}
@Override
protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
addIncludeFilter(new AnnotationTypeFilter(WebfluxClient.class));
Set<BeanDefinitionHolder> beanDefinitionHolders = super.doScan(basePackages);
if(!beanDefinitionHolders.isEmpty()){
beanDefinitionHolders.forEach(beanDefinitionHolder -> {
GenericBeanDefinition beanDefinition = (GenericBeanDefinition) beanDefinitionHolder.getBeanDefinition();
try {
Class<?> aClass = Class.forName(beanDefinition.getBeanClassName());
WebfluxClient webfluxClient = aClass.getAnnotation(WebfluxClient.class);
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(aClass);
beanDefinition.getConstructorArgumentValues().addGenericArgumentValue(webfluxClient);
} catch (ClassNotFoundException e) {
log.error("class not found", e);
}
beanDefinition.setBeanClass(WebClientFactory.class);
beanDefinition.setAutowireMode(GenericBeanDefinition.AUTOWIRE_BY_TYPE);
});
}
return beanDefinitionHolders;
}
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent();
}
}
-
工厂类(重要)
public class WebClientFactory implements FactoryBean<Object>, ApplicationContextAware, InitializingBean , EnvironmentAware {
private final Class<?> interFace;
private ApplicationContext applicationContext;
private Object proxyClient;
private final WebfluxClient webfluxClient;
private Environment environment;
public WebClientFactory(Class<?> interFace,WebfluxClient webfluxClient) {
this.interFace = interFace;
this.webfluxClient = webfluxClient;
}
@Override
public Object getObject() {
return this.proxyClient;
}
@Override
public Class<?> getObjectType() {
return interFace;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public void afterPropertiesSet() {
String url = environment.resolvePlaceholders(webfluxClient.url()); //支持${}读取环境变量
String targetUrl = url.isEmpty() ? this.webfluxClient.name() : url;
WebClient webClient = WebClient.builder()
.filters(filters -> {
if(!url.isEmpty()){
return;
}
LoadBalancerFilterFunction loadBalancerFilterFunction = applicationContext.getBean(LoadBalancerFilterFunction.class);
filters.add(loadBalancerFilterFunction);
})
.clientConnector(new ReactorClientHttpConnector(HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) webfluxClient.connectTimeout()) // 连接超时(毫秒)
.responseTimeout(Duration.ofMillis(webfluxClient.readTimeout())) // 响应超时
.doOnConnected(conn ->
conn.addHandlerLast(new ReadTimeoutHandler(webfluxClient.readTimeout(), TimeUnit.MILLISECONDS)) // 读取超时(秒)
)))
.baseUrl(targetUrl.startsWith("http") ? targetUrl : "http://" + targetUrl)
.build();
this.proxyClient = HttpServiceProxyFactory.builderFor(WebClientAdapter.create(webClient))
.build()
.createClient(interFace);
}
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
}
-
负载均衡过滤器
@Slf4j
public class LoadBalancerFilterFunction implements ExchangeFilterFunction {
@Resource
private LoadBalancerClient loadBalancerClient;
@Override
@Nonnull
public Mono<ClientResponse> filter(ClientRequest request, @Nonnull ExchangeFunction next) {
URI originalUrl = request.url();
String serviceName = originalUrl.getHost();
return Mono.defer(() -> Mono.justOrEmpty(loadBalancerClient.choose(serviceName)))
.switchIfEmpty(Mono.error(new WebfluxClientException(
"No instances available for service: " + serviceName, HttpStatus.SERVICE_UNAVAILABLE)))
.flatMap(instance -> {
URI reconstructedUrl = reconstructUri(instance, originalUrl);
ClientRequest newRequest = ClientRequest.from(request)
.url(reconstructedUrl)
.build();
return next.exchange(newRequest);
});
}
private static URI reconstructUri(ServiceInstance instance, URI originalUri) {
String path = originalUri.getPath();
if(originalUri.getQuery() != null && !originalUri.getQuery().isEmpty()) {
path += "?" + originalUri.getQuery();
}
return URI.create(String.format("%s://%s:%s%s",
originalUri.getScheme(),
instance.getHost(),
instance.getPort(),
path));
}
}
-
自定义异常类
@Getter
public class WebfluxClientException extends RuntimeException {
private final HttpStatus status;
public WebfluxClientException(String message, HttpStatus status) {
super(message);
this.status = status;
}
}
三、使用
- 定义声明式接口
@WebfluxClient(name = "ws-service",url = "${i.url}")
public interface VoiceClient {
@PostExchange(value = "/llm-connection/sse/sendMessage",accept = MediaType.TEXT_EVENT_STREAM_VALUE)
Flux<JSONObject> get(@RequestHeader(value = "Device-Id") String deviceId,
@RequestHeader(value = "Content-Type") String contentType,
@RequestBody JSONObject jsonObject);
}
springboot版本3.0以上需要使用@HttpExchange注解进行远程调用
- 启动类加@EnableWebClient注解
@SpringBootApplication
@EnableWebClient
public class SseServiceApplication {
public static void main(String[] args) {
SpringApplication.run(SseServiceApplication.class, args);
}
}
- 测试使用
@Service
@Slf4j
public class TestServiceImpl implements TestService {
@Resource
private VoiceClient voiceClient;
@Override
public void consume() {
String body = "";
JSONObject jsonObject = JSONObject.parseObject(body);
voiceClient.get("deviceId", MediaType.APPLICATION_JSON + ";charset=UTF-8",jsonObject)
.doOnNext(json->{
log.info("json:{}",json);
}).subscribe();
}
}
将第二章节部分代码制作成starter,后续项目直接引用依赖即可,目前功能比较基础,大家可根据自己的业务需求对代码进行修改,有问题的小伙伴可以留言