[spring-projects/spring-boot]支持声明式 HTTP 客户端

2024-06-26 942 views
2

Spring Framework 6.0引入了声明式 HTTP 客户端。我们应该在 Boot 中添加一些自动配置,例如提供HttpServiceProxyFactory

我们甚至可以考虑使用注释来注释 HTTP 接口,然后将其直接注入到消费者中(如@FeignClient)。

回答

9

我觉得这个可以自己尝试注入IOC 例如我这样做

public class HttpServiceFactory implements BeanFactoryAware, ImportBeanDefinitionRegistrar, ResourceLoaderAware {

    private final HttpServiceProxyFactory proxyFactory;

    private BeanFactory beanFactory;

    private ResourceLoader resourceLoader;

    public HttpServiceFactory() {
        WebClient client = WebClient.builder().build();
        this.proxyFactory = HttpServiceProxyFactory.builder(new WebClientAdapter(client)).build();
    }

    @Override
    public void registerBeanDefinitions(@NonNull AnnotationMetadata importingClassMetadata,
            @NonNull BeanDefinitionRegistry registry) {
        List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
        Set<Class<?>> typesAnnotatedClass = findByAnnotationType(HttpExchange.class, resourceLoader,
                packages.toArray(String[]::new));
        for (Class<?> exchangeClass : typesAnnotatedClass) {
            BeanName name = AnnotationUtils.getAnnotation(exchangeClass, BeanName.class);
            String beanName = name != null ? name.value()
                    : CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, exchangeClass.getSimpleName());
            registry.registerBeanDefinition(beanName, getBeanDefinition(exchangeClass));
        }
    }

    private <T> BeanDefinition getBeanDefinition(Class<T> exchangeClass) {
        return new RootBeanDefinition(exchangeClass, () -> proxyFactory.createClient(exchangeClass));
    }

    @Override
    public void setResourceLoader(@NonNull ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    @Override
    public void setBeanFactory(@NonNull BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }

public Set<Class<?>> findByAnnotationType(Class<? extends Annotation> annotationClass,
            ResourceLoader resourceLoader, String... packages) {
        Assert.notNull(annotationClass, "annotation not null");
        Set<Class<?>> classSet = new HashSet<>();
        if (packages == null || packages.length == 0) {
            return classSet;
        }
        ResourcePatternResolver resolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader);
        CachingMetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader);
        try {
            for (String packageStr : packages) {
                packageStr = packageStr.replace(".", "/");
                Resource[] resources = resolver.getResources("classpath*:" + packageStr + "/**/*.class");
                for (Resource resource : resources) {
                    MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(resource);
                    String className = metadataReader.getClassMetadata().getClassName();
                    Class<?> clazz = Class.forName(className);
                    if (AnnotationUtils.findAnnotation(clazz, annotationClass) != null) {
                        classSet.add(clazz);
                    }
                }
            }
        }
        catch (IOException | ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
        return classSet;
    }

}

/**
 * Just used to set the BeanName
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface BeanName {

    String value();

}

你对此有何看法

8

额外的注释会带来什么呢@HttpMapping

6

底层客户端可能需要配置不同的基本 URL、编解码器等。这意味着,@HttpExchange如果基于相同的底层客户端设置,检测带注释的接口和为它们声明 bean 可能会变得不灵活。

给定一个HttpServiceProxyFactory,创建代理很简单,因此自动化创建代理不会带来太多好处。不过,我在想,Boot 应该可以创建并自动配置一个HttpServiceProxyFactory实例,然后将其与不同的客户端设置相结合。

目前HttpServiceProxyFactory采用HttpClientAdapter构造函数,但我们可以进行更改以允许将其传递给重载createClient(Class<?>, HttpClientAdapter)方法。因此,您可以HttpServiceProxyFactory在任何地方注入相同的内容,并将其与默认客户端设置一起使用(基于WebClientCustomizerWebClient.Builder),或者也可以选择注入WebClient.Builder并使用与默认设置不同的客户端设置。

1

谢谢,这有助于我推进我的思考过程。我现在明白有必要更正式地将HttpServiceProxyFactory与底层客户端分开。

我在本地进行了一个实验,每次调用时都HttpServiceProxyFactory需要HttpClientAdapter传入createClient。另外,还有一个使用和WebClientServiceProxyFactory创建的,并且只使用代理接口公开的。HttpServiceProxyFactoryWebClientcreateClient

然后,Boot 自动配置可以声明单个HttpServiceProxyFactorybean,应用程序将创建任意数量的WebClientServiceProxyFactorybean,每个 bean 委托给相同的beanHttpServiceProxyFactoryWebClient为特定的远程进行配置。

2

经过几次不同的实验,我认为尝试HttpServiceProxyFactory为多个客户端实例设置一个实例会带来额外的复杂性,而收获甚微。最容易理解的模型仍然是一对一HttpServiceProxyFactory的模型。即使没有 Boot 的任何帮助,它也相当简单:

@Bean
HttpServiceProxyFactory httpServiceProxyFactory1(WebClient.Builder clientBuilder) {
    WebClient client = clientBuilder.baseUrl("http://host1.com").build();
    return new HttpServiceProxyFactory(new WebClientAdapter(client));
}

@Bean
HttpServiceProxyFactory httpServiceProxyFactory2(WebClient.Builder clientBuilder) {
    WebClient client = clientBuilder.baseUrl("http://host2.com").build();
    return new HttpServiceProxyFactory(new WebClientAdapter(client));
}

一些额外的快捷方式WebClientAdapter可以使这成为一行:

@Bean
HttpServiceProxyFactory httpServiceProxyFactory1(WebClient.Builder clientBuilder) {
    return WebClientAdapter.createProxyFactory(clientBuilder.baseUrl("http://host1.com"));
}

@Bean
HttpServiceProxyFactory httpServiceProxyFactory2(WebClient.Builder clientBuilder) {
    return WebClientAdapter.createProxyFactory(clientBuilder.baseUrl("http://host2.com"));
}

如果我们将上述内容作为预期配置,那么我认为启动任何 Boot 自动配置都不是必需的,尽管可能仍会出现一些想法。也许,在属性中指定 baseUrl,这将允许 Boot 创建上述 bean?

0

谢谢,罗森。

也许,在属性中指定 baseUrl,这将允许 Boot 创建上述 bean?

我们尚不支持在 Boot 的任何地方指定多个属性值来自动配置多个 bean。这是我们想要做的事情,但这是一个复杂且范围广泛的话题。例如,https://github.com/spring-projects/spring-boot/issues/15732正在跟踪自动配置的多个数据源。

今天讨论过这个问题后,我们认为 Boot 目前没有什么可做的。如果自动配置多个 bean 的情况发生变化,我们可以重新讨论这个问题。

5

由于 spring-projects/spring-framework#29296 中的更改而重新开放,Spring Boot 可以为HttpServiceProxyFactory.Builder上下文提供预配置,以便开发人员可以从中构建自己的客户端。

4

关于这个关于 HTTP 接口的精彩教程https://softice.dev/posts/introduction_to_spring_framework_6_http_interfaces/

我不太明白。为什么开发人员需要手动编写一个@Bean返回代理 bean(实现接口)的方法,尤其是在我们使用 spring boot 的情况下?我记得使用@FeignClient,我不必为它定义任何代理 bean,所以我假设 spring boot 可以为我们完成。

另外为什么要使用 Http 接口@FeignClient

7

我们甚至可以考虑使用注释来注释 HTTP 接口,然后将其直接注入到消费者中(如@FeignClient)。

我认为我们需要一个像@EnableFeignClients而不是 这样的注释@FeignClient

我们已经可以通过 知道一个接口是否是http客户端@HttpExchange,我们需要一个注解来扫描接口并注册bean(如@EnableFeignClients)。

这是我的解决方法

9

我认为人们已经习惯使用 Feign 客户端方法。

在这里你可以找到非常相似的方法:Exchange 客户端

使用起来非常简单。

2

我已经制作了以下方法的原型,将样板文件减少到最低限度:

@HttpClient注释将接口标记为 http 客户端,并添加选项来设置WebClient要使用的 bean 名称。
@HttpClient("todo-client")
public interface TodoClient {
    @GetExchange("/todos")
    List<Todo> get();
}

该注释由一个注册器实现处理,ImportBeanDefinitionRegistrar它为每个 http 客户端注册 bean 定义,HttpServiceProxyFactoryWebClientAdapter使用注释中的名称为 WebClient 创建一个适配器。

WebClient从环境创建实例

考虑到许多 Web 客户端相对简单,有一组可以使用简单属性设置的通用属性:url、basic auth、timeouts 等。

鉴于此,可以选择通过 yaml/properties 创建 WebClient,如下所示:

http.clients:
    todo-client:
        url: https://jsonplaceholder.typicode.com
    bar:
        url: http://foo/bar

如果您认为这有意义,我可以准备一个 PR,或者如果说这还为时过早,我可以将其作为一个单独的项目发布,一旦 Spring Boot 内置了类似的功能,它就会被弃用。

更新:

该库可在 Maven Central 上找到:https://github.com/maciejwalkowiak/spring-boot-http-clients