springboot3.2 内置http 要跟feign说88了

一,提前准备

提前准备几个接口,也可以自己提供,我这里直接在网上找了几个公开的api。本文提供的所有代码都已经公开,请参考https://gitee.com/zengmoss/exchange

二,新建项目

然后新建一个springboot的项目,这里只需要spring-web模块就行了,完整的pom文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.2.0</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>exchange</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>exchange</name>
	<description>springboot3  内置HTTP</description>
	<properties>
		<java.version>17</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</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>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

3.2.0版本不仅支持WebClient,也支持RestClient和RestTemplate,本文使用RestClient,更多可以参考spring官网关于http interfacehttps://docs.spring.io/spring-framework/reference/integration/rest-clients.html

三,建立对应接口

public interface VvhanClient {

    @GetExchange("/api/joke")
    String apiJoke();
}

四,配置对应的域名

在配置文件application.yml配置好域名,这里仅仅展示,就不区分环境了,生产使用就在对应环境里面配置

client:
  url:
    vvhan: https://api.vvhan.com/

五,配置http interface

@Configuration
public class HttpExchangeConfig {
    @Bean
    public VvhanClient vvhanClient(RestClient.Builder restClientBuilder, @Value("${client.url.vvhan}") String url){
        RestClient restClient = restClientBuilder.baseUrl(url).build();
        RestClientAdapter adapter = RestClientAdapter.create(restClient);
        HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
        return factory.createClient(VvhanClient.class);
    }
}

六,所有事情已经处理好

现在,可以把接口直接注入使用了。


@SpringBootTest
class ExchangeApplicationTests {

    @Autowired
    private VvhanClient vvhanClient;

    @Test
    void apiJoke() {
        System.out.println(vvhanClient.apiJoke());
    }
    
}

看下运行结果:

七,再对接其它系统

操作参考前面的步骤,这里不再演示,嫌麻烦的人直接去gitee下载源码,最后再配置http interface,就可以注入接口使用了。再看看配置文件:


@Configuration
public class HttpExchangeConfig {
    @Bean
    public VvhanClient vvhanClient(RestClient.Builder restClientBuilder, @Value("${client.url.vvhan}") String url){
        RestClient restClient = restClientBuilder.baseUrl(url).build();
        RestClientAdapter adapter = RestClientAdapter.create(restClient);
        HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
        return factory.createClient(VvhanClient.class);
    }

    @Bean
    public XyGengClient xyGengClient(RestClient.Builder restClientBuilder, @Value("${client.url.xygeng}") String url){
        RestClient restClient = restClientBuilder.baseUrl(url).build();
        RestClientAdapter adapter = RestClientAdapter.create(restClient);
        HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
        return factory.createClient(XyGengClient.class);
    }
}

这里的restClientBuilder是原型的,不用多虑单例覆盖问题。从springboot自动配置RestClientAutoConfiguration里面可以很清楚地看到。

	@Bean
	@Scope("prototype")
	@ConditionalOnMissingBean
	RestClient.Builder restClientBuilder(RestClientBuilderConfigurer restClientBuilderConfigurer) {
		RestClient.Builder builder = RestClient.builder()
			.requestFactory(ClientHttpRequestFactories.get(ClientHttpRequestFactorySettings.DEFAULTS));
		return restClientBuilderConfigurer.configure(builder);
	}

官方的指导到此结束,一般项目对接个10个系统就差不多了,所以写下配置文件也不复杂,这里的所有代码可以从分支看到exchange: springboot3 内置http客户端 取代feign - Gitee.com

八,再对比下feign

最大的区别就是feign把域名放在@FeignClient里,然后通过@EnableFeignClients发现,自动注册成代理,而内置的http,则需要在配置文件里面手动注册成代码。

九,模仿下feign

我们发现,想要自动注册成代理也是很容易的事情,所以模仿下吧。

我们先写一个factoryBean,这样spring实例化的时候会调用getObject(),这样才有了操作空间。

@Getter
@Setter
public class HttpExchangeFactoryBean<T> implements FactoryBean<T> {
    private Class<T> mapperInterface;
    private String baseUrl;
    @Autowired
    private RestClient.Builder restClientBuilder;
    public HttpExchangeFactoryBean(Class<T> mapperInterface) {
        this.mapperInterface = mapperInterface;
    }

    @Override
    public T getObject() {
        RestClient restClient = restClientBuilder.baseUrl(baseUrl).build();
        RestClientAdapter adapter = RestClientAdapter.create(restClient);
        HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
        return factory.createClient(mapperInterface);
    }

    @Override
    public Class<?> getObjectType() {
        return mapperInterface;
    }
}

给一个自定义注解承担域名吧,类似@FeignClient,

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface ExchangeClient {
    /**
     * 声明式接口的基础地址
     * baseUrl
     * @return
     */
    String value();
}

给一个发现注解的入口吧,类似@EnableFeignClients,

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(ExchangeClientsRegistrar.class)
public @interface EnableExchangeClients {
}

这里引入了一个配置文件,ExchangeClientsRegistrar,就是在这里实现自动注册。

public class ExchangeClientsRegistrar implements ImportBeanDefinitionRegistrar{

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        ClassPathExchangeScanner scanner = new ClassPathExchangeScanner(registry);
        //扫描带@ExchangeClient的类
        scanner.addIncludeFilter(new AnnotationTypeFilter(ExchangeClient.class));
        scanner.scan(ClassUtils.getPackageName(importingClassMetadata.getClassName()));
    }

    @Slf4j
    private static class ClassPathExchangeScanner extends ClassPathBeanDefinitionScanner{
        public ClassPathExchangeScanner(BeanDefinitionRegistry registry) {
            super(registry, false);
        }
        public Set<BeanDefinitionHolder> doScan(String... basePackages) {
            Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
            if (!beanDefinitions.isEmpty()) {
               for(BeanDefinitionHolder holder : beanDefinitions){
                   AbstractBeanDefinition definition = (AbstractBeanDefinition)holder.getBeanDefinition();
                   String beanClassName = definition.getBeanClassName();
                   //把类名注入
                   definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName);
                   //指定FactoryBean生成
                   definition.setBeanClass(HttpExchangeFactoryBean.class);
                   if (definition instanceof AnnotatedBeanDefinition) {
                       // verify annotated class is an interface
                       AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) definition;
                       AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
                       Map<String, Object> attributes = annotationMetadata
                               .getAnnotationAttributes(ExchangeClient.class.getCanonicalName());
                       String baseUrl = (String)attributes.get("value");
                       //把域名注入 这里的spel不用解释好  spring会自动解释
                       definition.getPropertyValues().add("baseUrl", baseUrl);
                   }
               }
            }
            return beanDefinitions;
        }

        protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
            return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent();
        }
    }

}

然后手动注册代理的文件HttpExchangeConfig 可以删除了。在接口里加上域名,

@ExchangeClient("${client.url.vvhan}")
public interface VvhanClient {

    @GetExchange("/api/joke")
    String apiJoke();
}

在入口加入发现,

@SpringBootApplication
@EnableExchangeClients
public class ExchangeApplication {

	public static void main(String[] args) {
		SpringApplication.run(ExchangeApplication.class, args);
	}

}

这样,就跟feign的用法一模一样了,就是注解的名称改了改,看起来,是时候跟feign说88了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值