专业的JAVA编程教程与资源

网站首页 > java教程 正文

微服务 - 服务接口调用 OpenFeign

temp10 2025-09-29 11:42:34 java教程 2 ℃ 0 评论

本文来学一下openFeign,openFeign也是微服务中常用的一个服务调用方式,接下来我们结合下面这五个大方面来学习一下。生命不止,学习不止~~

1、概述

OpenFeign是什么

Feign是一个声明式WebService客户端。使用Feign能让编写Web Service客户端更加简单。它的使用方法是定义一个服务接口然后在上面添加注解。Feign也支持可拔插式的编码器和解码器。Spring Cloud对Feign进行了封装,使其支持了Spring MVC标准注解和HttpMessageConverters。Feign可以与Eureka和Ribbon组合使用以支持负载均衡。

微服务 - 服务接口调用 OpenFeign

能干嘛

Feign旨在使编写Java Http客户端变得更容易。也就是说,feign来简化我们发起远程调用的代码的,那简化到什么程度呢?简化成就像调用本地方法那样简单。

在Feign的实现下,我们只需创建一个接口并使用注解的方式来配置它,在相关的微服务接口上面标注一个Feign注解即可),即可完成对服务提供方的接口绑定。

Feign和OpenFeign两者区别

OpenFeign 组件的前身是 Netflix Feign 项目,它最早是作为 Netflix OSS 项目的一部分,由 Netflix 公司开发。后来 Feign 项目被贡献给了开源组织,于是才有了我们今天使用的 Spring Cloud OpenFeign 组件。

Feign 和 OpenFeign 有很多大同小异之处,不同的是 OpenFeign 支持 MVC 注解。可以认为 OpenFeign 为 Feign 的增强版。

2、项目演示

本文我们主要是为了演示openfeign是如何调用的,所以我们还以我们原来搭建的服务为基础,使用eureka作为服务注册中心。

前面文章我们已经学习过如何搭建eureka服务中心,如何注册服务到eureka,所以在这个地方就不在着重进行介绍。只进行一个简单的说明。

服务提供者配置

server:
	servlet:
    context-path: producer-service
  port: 8001
eureka:
  client:
    #表示是否将自己注册进EurekaServer默认为true。
    register-with-eureka: true
    #是否从EurekaServer抓取已有的注册信息,默认为true。单节点无所谓,集群必须设置为true才能配合ribbon使用负载均衡
    fetchRegistry: true
    service-url:
      #单机版
      defaultZone: http://localhost:7001/eureka
      # 集群版
      #defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
  instance:
    instance-id:  producer-8001
    #访问路径可以显示IP地址
    prefer-ip-address: true
    
	#Eureka客户端向服务端发送心跳的时间间隔,单位为秒(默认是30秒)
    #lease-renewal-interval-in-seconds: 1
    
	#Eureka服务端在收到最后一次心跳后等待时间上限,单位为秒(默认是90秒),超时将剔除服务
    #lease-expiration-duration-in-seconds: 2

服务消费者

pom

<!--openfeign-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--eureka client-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

配置

server:
  port: 80

eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/

logging:
  level:
    # feign日志以什么级别监控哪个接口
    com.atguigu.springcloud.service.PaymentFeignService: debug

启动类

增加@EnableFeignClients注解

@SpringBootApplication
@EnableFeignClients
public class FeignConsumer {
    public static void main(String[] args) {
        SpringApplication.run(OrderFeignMain80.class, args);
    }
}

服务调用

@FeignClient(value = "producer-service", fallbackFactory = DemoProviderFeignClientFallbackFactory.class)
@RequestMapping("/service-a/test")
public interface TestApi {

    @GetMapping("/get/{id}")
    TestVO testGet(@PathVariable(name = "id") Long id);
}

请求端

@RestController
@RequestMapping("/sample")
public class SampleController {

    @Autowired
    TestApi testApi;

    @GetMapping("/get/{id}")
    public TestVO testGet(@PathVariable(name = "id")Long id) {
        return testApi.testGet(id);
    }
}

以上就搭演示完了我们使用openfeign调用的简易demo,当然啦,openfeign肯定提供的不单单只有这么多功能。我们先来看一下openfeign都有哪些常用配置,后面我们再详细的进行介绍。

feign:
  client:
    config:
      ## default 设置的全局超时时间,指定服务名称可以设置单个服务的超时时间
      default:
        connectTimeout: 1000
        readTimeout: 1000
        logger-level: BASIC
      producer-service:
        connectTimeout: 10000
        readTimeout: 10000
        logger-level: FULL
  # Feign Apache HttpClient 配置项,对应 FeignHttpClientProperties 配置属性类
  httpclient:
    enabled: true # 是否开启。默认为 true
    max-connections: 200 # 最大连接数。默认为 200
    max-connections-per-route: 50 # 每个路由的最大连接数。默认为 50。router = host + port
  okhttp:
    enabled: false
  ## 开启压缩
  compression:
    request:
      enabled: true
      ## 开启压缩的阈值,单位字节,默认2048,即是2k,这里为了演示效果设置成1字节
      min-request-size: 1
      mime-types: text/xml,application/xml,application/json
    response:
      enabled: true
  sentinel:
    enabled: true

3、超时控制

在上面的配置中我们已经写了如何配置超时时间了,下面我们就来详细介绍一下。

openFeign设置超时时间非常简单,只需要在配置文件中配置,如下:

feign:
  client:
    config:
      ## default 设置的全局超时时间,指定服务名称可以设置单个服务的超时时间
      default:
        connectTimeout: 1000
        readTimeout: 1000

default设置的是全局超时时间,对所有的openFeign接口服务都生效,如果有的服务调用相对来说比较慢,我们也可以针对服务进行单独的配置。

feign:
  client:
    config:
      ## default 设置的全局超时时间,指定服务名称可以设置单个服务的超时时间
      default:
        connectTimeout: 5000
        readTimeout: 5000
      ## 为producer-service这个服务单独配置超时时间
      producer-service:
        connectTimeout: 30000
        readTimeout: 30000

4、开启日志增强

openFeign虽然提供了日志增强功能,但是默认是不显示任何日志的,不过开发者在调试阶段可以自己配置日志的级别。

openFeign的日志级别如下:

  • NONE:默认的,不显示任何日志;
  • BASIC:仅记录请求方法、URL、响应状态码及执行时间;
  • HEADERS:除了BASIC中定义的信息之外,还有请求和响应的头信息;
  • FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据。

配置起来也很简单,步骤如下:自定义一个配置类,在其中设置日志级别,如下:

@Configuration
@ConditionalOnProperty(value = "spring.profiles.active", havingValue = "dev")
@Slf4j
public class FeignConfig {

    @Bean
    Logger.Level feignLoggerLevel() {
        log.info("配置feign日志");
        return Logger.Level.FULL;
    }
}

如果你仔细观察我们在上面列出的常用配置就会发现,日志增加我们也可以通过配置文件进行配置。

5、底层设计原理

前面我们说的都是openFeign如何使用的,接下来我们来说一下openFeign的底层实现。

我们先来看一下http的请求过程,因为 OpenFeign 是声明式的 HTTP 客户端,提供了HTTP请求的模板,编写简单的接口和插入注解,就可以定义好HTTP请求的参数、格式、地址等信息。


5.1 http请求流程:

openFeign的本质也是通过http进行调用的,采用的动态代理的方式来生成了实现类。我们先在这里换一下openFeign的工作流程,然后再来一步一步的进行解析底层实现原理。

5.2 openFeign工作流程:

  • 在 Spring 项目启动阶段,服务 A 的OpenFeign 框架会发起一个主动的扫包流程。从指定的目录下扫描并加载所有被 @FeignClient 注解修饰的接口,然后将这些接口转换成 Bean,统一交给 Spring 来管理。
  • 根据这些接口会经过 MVC Contract 协议解析,将方法上的注解都解析出来,放到 MethodMetadata 元数据中。
  • 基于上面加载的每一个 FeignClient 接口,会生成一个动态代理对象,指向了一个包含对应方法的 MethodHandler 的 HashMap。生成的动态代理对象会被添加到 Spring 容器中,并注入到对应的服务里。
  • 调用方调用接口,准备发起远程调用。从动态代理对象 Proxy 中找到一个 MethodHandler 实例,生成 Request,包含有服务的请求 URL(不包含服务的 IP)。
  • 经过负载均衡算法找到一个服务的 IP 地址,拼接出请求的 URL。
  • 拦截器负责对请求和返回进行装饰处理。
  • 发送Http请求,服务提供者进行业务逻辑处理。

上述的流程只是其中一些比较重要的步骤,并不是完整的的处理流程。如果对完整的流程的同学可以自己去看看源码,接下来我们就针对上述的一些步骤进行一下深度的学习。

5.3 OpeFeign 包扫描 @EnableFeignClients

我们在使用openfeign的时候,会在启动类上加上这样一个注解:@EnableFeignClients,我们从字面意思也可以看出来这个注解是开启openfeign功能的。

1> 点进去@EnableFeignClients注解我们会发现它使用了@Spring 框架的 @Import 注解导入了 FeignClientsRegistrar 类,开始了 OpenFeign 组件的加载。

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({FeignClientsRegistrar.class})
public @interface EnableFeignClients {
    String[] value() default {};

    String[] basePackages() default {};

    Class<?>[] basePackageClasses() default {};

    Class<?>[] defaultConfiguration() default {};

    Class<?>[] clients() default {};
}

2> FeignClientsRegistrar 类中的 registerBeanDefinitions 方法负责 Feign 接口的加载。

@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
        BeanDefinitionRegistry registry) {
    //注册默认配置
    registerDefaultConfiguration(metadata, registry);
    //注册feignClients
    registerFeignClients(metadata, registry);
}

3> registerFeignClients 扫描指定,查找指定路径 basePackage 的所有带有 @FeignClients 注解的带有 @FeignClient 注解的类、接口。

public void registerFeignClients(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {

		LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
		Map<String, Object> attrs = metadata
				.getAnnotationAttributes(EnableFeignClients.class.getName());
		final Class<?>[] clients = attrs == null ? null
				: (Class<?>[]) attrs.get("clients");
		if (clients == null || clients.length == 0) {
			ClassPathScanningCandidateComponentProvider scanner = getScanner();
			scanner.setResourceLoader(this.resourceLoader);
			scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
            //指定包的扫描路径
			Set<String> basePackages = getBasePackages(metadata);
			for (String basePackage : basePackages) {
				candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
			}
		}
		else {
			for (Class<?> clazz : clients) {
				candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
			}
		}

		for (BeanDefinition candidateComponent : candidateComponents) {
            //判断是否是带有注解的 Bean。
			if (candidateComponent instanceof AnnotatedBeanDefinition) {
				// verify annotated class is an interface
                //判断是否是接口
				AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
				AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
				Assert.isTrue(annotationMetadata.isInterface(),
						"@FeignClient can only be specified on an interface");
            	//获取feignclient的接口合集
				Map<String, Object> attributes = annotationMetadata
						.getAnnotationAttributes(FeignClient.class.getCanonicalName());

				String name = getClientName(attributes);
				registerClientConfiguration(registry, name,
						attributes.get("configuration"));

				registerFeignClient(registry, annotationMetadata, attributes);
			}
		}
	}

5.4 注册 FeignClient 到 Spring

还是在 registerFeignClients 方法中,当 FeignClient 扫描完后,就要为这些 FeignClient 接口生成一个动态代理对象。进到这个方法里面,可以看到这一段代码:

FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
		factoryBean.setBeanFactory(beanFactory);
		factoryBean.setName(name);
		factoryBean.setContextId(contextId);
		factoryBean.setType(clazz);
		BeanDefinitionBuilder definition = BeanDefinitionBuilder
				.genericBeanDefinition(clazz, () -> {
					factoryBean.setUrl(getUrl(beanFactory, attributes));
					factoryBean.setPath(getPath(beanFactory, attributes));
					factoryBean.setDecode404(Boolean
							.parseBoolean(String.valueOf(attributes.get("decode404"))));
					Object fallback = attributes.get("fallback");
					if (fallback != null) {
						factoryBean.setFallback(fallback instanceof Class
								? (Class<?>) fallback
								: ClassUtils.resolveClassName(fallback.toString(), null));
					}
					Object fallbackFactory = attributes.get("fallbackFactory");
					if (fallbackFactory != null) {
						factoryBean.setFallbackFactory(fallbackFactory instanceof Class
								? (Class<?>) fallbackFactory
								: ClassUtils.resolveClassName(fallbackFactory.toString(),
										null));
					}
					return factoryBean.getObject();
				});

核心就是 FeignClientFactoryBean 类,根据类的名字我们可以知道这是一个工厂类,用来创建 FeignClient Bean 的。

我们先来通过下面一个示例来看下是如何创建feignClient的。

@FeignClient(name = ServerConstant.FLOW_SERVICE, fallbackFactory = RemoteFlowFallbackFactory.class)
public interface RemoteFlowService {

    /**
     * 流程退回
     *
     * @param token
     */
    @PostMapping(value = "/backCommit")
    void backCommit(@RequestBody Map<String, Object> data,
                    @RequestHeader(value = "token", required = true) String token) throws Throwable;

     /**
     * 发起流程
     **/
    @PostMapping(value = "/startFlow")
    R startFlow(@RequestBody FlowStartParam flowStartParam);

    /**
     * 流程审核通过
     **/
    @PostMapping(value = "/auditPass")
    R auditPass(@RequestParam("uid") Integer uid, @RequestParam("utype") Integer utype);

}

过程大致:

  • 解析 @FeignClient 定义的属性。
  • 将注解@FeignClient 的属性 + 接口 RemoteFlowService 的信息构造成一个 RemoteFlowService 的 beanDefinition。
  • 然后将 beanDefinition 转换成一个 BeanDefinitionHolder,这个 holder 就是包含了 beanDefinition, alias, beanName 信息。
  • 最后将这个 holder 注册到 Spring 容器中。

源码如下:

private void registerFeignClient(BeanDefinitionRegistry registry,
			AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
		String className = annotationMetadata.getClassName();
		Class clazz = ClassUtils.resolveClassName(className, null);
		ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory
				? (ConfigurableBeanFactory) registry : null;
		String contextId = getContextId(beanFactory, attributes);
		String name = getName(attributes);
		FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
		factoryBean.setBeanFactory(beanFactory);
		factoryBean.setName(name);
		factoryBean.setContextId(contextId);
		factoryBean.setType(clazz);
		BeanDefinitionBuilder definition = BeanDefinitionBuilder
				.genericBeanDefinition(clazz, () -> {
					factoryBean.setUrl(getUrl(beanFactory, attributes));
					factoryBean.setPath(getPath(beanFactory, attributes));
					factoryBean.setDecode404(Boolean
							.parseBoolean(String.valueOf(attributes.get("decode404"))));
					Object fallback = attributes.get("fallback");
					if (fallback != null) {
						factoryBean.setFallback(fallback instanceof Class
								? (Class<?>) fallback
								: ClassUtils.resolveClassName(fallback.toString(), null));
					}
					Object fallbackFactory = attributes.get("fallbackFactory");
					if (fallbackFactory != null) {
						factoryBean.setFallbackFactory(fallbackFactory instanceof Class
								? (Class<?>) fallbackFactory
								: ClassUtils.resolveClassName(fallbackFactory.toString(),
										null));
					}
					return factoryBean.getObject();
				});
		definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
		definition.setLazyInit(true);
		validate(attributes);

		String alias = contextId + "FeignClient";
    	//生成beanDefinition
		AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
		beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
		beanDefinition.setAttribute("feignClientsRegistrarFactoryBean", factoryBean);

		// has a default, won't be null
		boolean primary = (Boolean) attributes.get("primary");

		beanDefinition.setPrimary(primary);

		String qualifier = getQualifier(attributes);
		if (StringUtils.hasText(qualifier)) {
			alias = qualifier;
		}
        // 转换成holder,包含了beanDefinition,alias,beanName信息
		BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
				new String[] { alias });
    	//注册到 Spring 上下文中。
		BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
	}

后面服务要调用接口的时候,就可以直接用 FeignClient 的接口方法了,但是我们并没有细讲这个 FeignClient 的创建细节,下面我们看下 FeignClient 的创建细节,这个也是 OpenFeign 核心原理。

5.5 动态代理分析

上面的源码解析中我们也提到了是由这个工厂类 FeignClientFactoryBean 来创建 FeignCient Bean,在创建 FeignClient Bean 的过程中就会去生成动态代理对象。调用接口时,其实就是调用动态代理对象的方法来发起请求的。

分析动态代理的入口方法为 getObject( ),接着调用 target 方法

@Override
public Object getObject() {
    return getTarget();
}

<T> T getTarget() {
		FeignContext context = beanFactory != null
				? beanFactory.getBean(FeignContext.class)
				: applicationContext.getBean(FeignContext.class);
		Feign.Builder builder = feign(context);

        //判断feignclient的url属性是否为空
		if (!StringUtils.hasText(url)) {
			if (!name.startsWith("http")) {
				url = "http://" + name;
			}
			else {
				url = name;
			}
			url += cleanPath();
			return (T) loadBalance(builder, context,
					new HardCodedTarget<>(type, name, url));
		}
		...
	}

因为我们在上述那个 @FeignClient 注解的例子中是使用 name 而不是 url,所以会执行负载均衡策略的分支。

protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
			HardCodedTarget<T> target) {
		Client client = getOptional(context, Client.class);
		if (client != null) {
			builder.client(client);
			Targeter targeter = get(context, Targeter.class);
			return targeter.target(this, builder, context, target);
		}

		throw new IllegalStateException(
				"No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?");
	}

Client: Feign 发送请求以及接收响应等都是由 Client 完成,该类默认 Client.Default,另外支持 HttpClient、OkHttp 等客户端

DefaultTargeter 和 HystrixTargeter。而不论是哪种 target,都需要去调用 Feign.java 的 builder 方法去构造一个 feign client。

在构造的过程中,依赖 ReflectiveFeign 去构造。源码如下:

@Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
        FeignContext context, Target.HardCodedTarget<T> target) {
    if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) {
        return feign.target(target);
    }
    ...
}


public <T> T target(Target<T> target) {
    return this.build().newInstance(target);
}

//创建反射类 ReflectiveFeign,然后执行创建实例类
public Feign build() {
    Client client = (Client)Capability.enrich(this.client, this.capabilities);
    Retryer retryer = (Retryer)Capability.enrich(this.retryer, this.capabilities);
    List<RequestInterceptor> requestInterceptors = (List)this.requestInterceptors.stream().map((ri) -> {
        return (RequestInterceptor)Capability.enrich(ri, this.capabilities);
    }).collect(Collectors.toList());
    Logger logger = (Logger)Capability.enrich(this.logger, this.capabilities);
    Contract contract = (Contract)Capability.enrich(this.contract, this.capabilities);
    Options options = (Options)Capability.enrich(this.options, this.capabilities);
    Encoder encoder = (Encoder)Capability.enrich(this.encoder, this.capabilities);
    Decoder decoder = (Decoder)Capability.enrich(this.decoder, this.capabilities);
    InvocationHandlerFactory invocationHandlerFactory = (InvocationHandlerFactory)Capability.enrich(this.invocationHandlerFactory, this.capabilities);
    QueryMapEncoder queryMapEncoder = (QueryMapEncoder)Capability.enrich(this.queryMapEncoder, this.capabilities);
    Factory synchronousMethodHandlerFactory = new Factory(client, retryer, requestInterceptors, logger, this.logLevel, this.decode404, this.closeAfterDecode, this.propagationPolicy, this.forceDecoding);
    ParseHandlersByName handlersByName = new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder, this.errorDecoder, synchronousMethodHandlerFactory);
    return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
}

ReflectiveFeign 做的工作就是为带有 @FeignClient 注解的接口,创建出接口方法的动态代理对象。

this.build().newInstance(target)方法对 @FeignClient 修饰的接口中 SpringMvc 等配置进行解析转换,对接口类中的方法进行归类,生成动态代理类。

 public <T> T newInstance(Target<T> target) {
     
     //处理 @FeignCLient 注解(SpringMvc 注解等)封装为 MethodHandler 包装类
    Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
     //方法 和 MethodHandler 的对应关系
    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
    List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();

    for (Method method : target.type().getMethods()) {
      if (method.getDeclaringClass() == Object.class) {
        continue;
      } else if (Util.isDefault(method)) {
        DefaultMethodHandler handler = new DefaultMethodHandler(method);
        defaultMethodHandlers.add(handler);
        methodToHandler.put(method, handler);
      } else {
        methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
      }
    }
     //根据target 和 methodToHandler 创建 InvocationHandler
    InvocationHandler handler = factory.create(target, methodToHandler);
    //创建 代理类 proxy
    T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
        new Class<?>[] {target.type()}, handler);

    for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
      defaultMethodHandler.bindTo(proxy);
    }
    return proxy;
  }

原理图:

  • 解析 FeignClient 接口上各个方法级别的注解,比如远程接口的 URL、接口类型(Get、Post 等)、各个请求参数等。
  • 然后将解析到的数据封装成元数据,并为每一个方法生成一个对应的 MethodHandler 类作为方法级别的代理。相当于把服务的请求地址、接口类型等都帮我们封装好了。这些 MethodHandler 方法会放到一个 HashMap 中。
  • 然后会生成一个 InvocationHandler 用来管理这个 hashMap,其中 Dispatch 指向这个 HashMap。
  • 然后使用 Java 的 JDK 原生的动态代理,实现了 FeignClient 接口的动态代理 Proxy 对象。这个 Proxy 会添加到 Spring 容器中。
  • 当要调用接口方法时,其实会调用动态代理 Proxy 对象的 methodHandler 来发送请求。

6、总结

到这里我们也就可以 openFeign 的工作方式了。大致如下:

  • 在我们调用 @FeignClient 接口时,会被 FeignInvocationHandler#invoke 拦截,并在动态代理方法中执行下述逻辑。
  • 接口注解信息封装为 HTTP Request。
  • 对服务列表进行负载均衡调用(服务名转换为 ip+port)。
  • 请求调用后,将返回的数据封装为 HTTP Response,继而转换为接口中的返回类型。

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表