💪💪永久更新承诺
我们郑重承诺,所有订阅合集的粉丝都将享受永久免费的后续更新服务。
💌💌如何获取
订阅我们的合集《点我订阅》,并通过私信联系我们,我们将第一时间将电子书发送给您。
环境:SpringBoot3.4.2
1. 简介
项目开发中常需调用其他微服务或第三方 API,传统 HTTP 客户端(如 RestTemplate、HttpClient、RestClient)存在代码冗余、配置繁琐、缺乏服务治理能力等问题。OpenFeign 作为声明式 HTTP 客户端,通过接口定义和注解驱动(如 @FeignClient),将远程调用抽象为本地方法调用,我们只需定义接口,无需手动处理连接池、重试、负载均衡等底层细节。
虽然 OpenFeign 提供了开箱即用的声明式 HTTP 客户端能力,但理解其底层原理并手写简化版实现,能帮助我们深入掌握动态代理、服务调用的核心逻辑。
本篇文章我们将从零开始手写一个简化版OpenFeign,深入解析声明式HTTP客户端的核心实现原理。如下是最终示例:
(name = "userClient",url = "http://localhost:8080",configuration = UserClientConfiguration.class)public interface UserClient {("/api/{id}")public String query( Long id, String state) ;("/api/save")public Map<String, Object> save( Map<String, Object> body) ;}
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)public @interface RemoteClient {/**绝对URL*/String url() ;/**唯一标识*/String name() ;/**自定义配置*/Class<?> configuration() default void.class ;}
public EnableRemoteClient {/**扫描的包*/String[] value() default {} ;String[] basePackages() default {};}
该注解通过 value 或 basePackages 属性指定要扫描的包路径(非必填)。若未显式定义,则默认以当前使用该注解的类所在包作为扫描起点。
public class RemoteClientRegistrar implements ImportBeanDefinitionRegistrar {private final Environment environment;public RemoteClientRegistrar(Environment environment) {this.environment = environment;}public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {registerFeignClients(metadata, registry) ;}// 注册@RemoteClient标注的接口,而这里我们是通过注册 RemoteClientFactoryBean 方式实现对接口创建代理public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();ClassPathScanningCandidateComponentProvider scanner = getScanner() ;scanner.addIncludeFilter(new AnnotationTypeFilter(RemoteClient.class)) ;Set<String> basePackages = this.getPackagesToScan(metadata) ;for (String basePackage : basePackages) {candidateComponents.addAll(scanner.findCandidateComponents(basePackage));}for (BeanDefinition candidateComponent : candidateComponents) {if (candidateComponent instanceof AnnotatedBeanDefinition beanDefinition) {AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();Assert.isTrue(annotationMetadata.isInterface(), "@RemoteClient 注解的类必须是接口");Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(RemoteClient.class.getCanonicalName()) ;String className = annotationMetadata.getClassName();BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(RemoteClientFactoryBean.class) ;definition.addPropertyValue("url", attributes.get("url")) ;definition.addPropertyValue("name", attributes.get("name")) ;definition.addPropertyValue("type", className) ;registry.registerBeanDefinition(className, definition.getBeanDefinition()) ;Object configuration = attributes.get("configuration") ;if (configuration != void.class) {this.registerClientConfiguration(registry, attributes.get("name"), configuration);}}}}// 将@RemoteClient注解中配置的configuration类注册为beanprivate void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, Object configuration) {BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(RemoteClientSpecification.class);builder.addConstructorArgValue(name);builder.addConstructorArgValue(configuration);registry.registerBeanDefinition(name + "." + RemoteClientSpecification.class.getSimpleName(),builder.getBeanDefinition());}// 自定义组件扫描protected ClassPathScanningCandidateComponentProvider getScanner() {return new ClassPathScanningCandidateComponentProvider(false, this.environment) {protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {boolean isCandidate = false;if (beanDefinition.getMetadata().isIndependent()) {if (!beanDefinition.getMetadata().isAnnotation()) {isCandidate = true;}}return isCandidate;}};}// 获取要扫描的包(如果没有定义包路径,则会根据当前使用住的类所在的包作为起点)private Set<String> getPackagesToScan(AnnotationMetadata metadata) {AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(EnableRemoteClient.class.getName()));Set<String> packagesToScan = new LinkedHashSet<>();for (String basePackage : attributes.getStringArray("basePackages")) {String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(basePackage),ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);Collections.addAll(packagesToScan, tokenized);}if (packagesToScan.isEmpty()) {String packageName = ClassUtils.getPackageName(metadata.getClassName());return Collections.singleton(packageName);}return packagesToScan;}}

