Back to Javatutorial

SpringCloudOpenFeign源码分析

docs/Spring全家桶/SpringCloud源码分析/SpringCloudOpenFeign源码分析.md

1.0.023.8 KB
Original Source

| ľľ Դ |ͷ

ѧϰĿ

  1. ΪʲôһעʵԶ̵̹أƵײʵ̣
  2. OpenFeignôʵRPCĻܵ
  3. ͨԴ֤ 1 OpenFeignƵ ҪȷOpenFeignǻҪȷĺĿʲô

˵ˣOpenFeignĵĿÿͻԶ̵ùвҪʲôIJֻҪõһȻøöķͺˣʣµIJOpenFeignȥɣʣһЩʲôأ

  1. ȿ϶DZ֤ͨţǴ󵨵ز²һ£OpenFeignʵײǷװĵַ˿ڡԼӦIJ
  2. ΣҪöȥ󷽷Զ̵ķ϶򵥣Ҳ󵨲²һ£öҲOpenFeignǴġ
  3. ȻڵùУɶ̨ṩģ漰ؾˣ϶ҲOpenFeignˡ
  4. ⣬һ£ȻڷǼȺǷ˵ĵַͶ˿ڻҪһעעᣬ϶ҲɿͻɣΪͻֻעҵ롣붼룬ҲOpenFeignˡ

OKƵOpenFeignӦɵҪĿ꣬ôġ ֮ǰнһʲôֻҪǼspringspringbootĻһͨspringspringbootȥbeanĴģͨõ֮ȥöĺķOpenFeignڼspringbootʱҲӦ

  1. ԵһOpenFeignspringbootͨspringbootõbeanͼеuserService
  2. ϶򵥣ֻgetUserĹܡһ룬springǿʲôأ;ͺ֮ˣ
  3. ôķʱȽ뵽invokeУinvokeУǿ˸ؾLoadBalanceΪgetUserʱҪ֪ǵ̨ķ
  4. ؾ͵ƴӾhttpͷַ˿ˡ OKϷOpenFeignײҪʵֵľ幦ܣҲĴ̣ôͨԴ֤һ£Dzôġ

2 Դ֤

2.1 EnableFeignClients Ǵע룬ע⿪FeignClientĽ̡

@EnableFeignClients(basePackages = "com.example.client")

ע£õһ@Importע⣬֪ImportһģȥһFeignClientsRegistrarĶ塣

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

FeignClientsRegistrarʵ ImportBeanDefinitionRegistrarһ̬עbeanĽӿڣSpring Bootʱ򣬻ȥеregisterBeanDefinitionsʵֶ̬BeanװءregisterBeanDefinitionsspringʱִinvokeBeanFactoryPostProcessorsȻӦнעᣬImportSelector

class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware,EnvironmentAware {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {
        registerDefaultConfiguration(metadata, registry);
        registerFeignClients(metadata, registry);
    }
}

2.1.1 ImportBeanDefinitionRegistrar 򵥸ʾһ ImportBeanDefinitionRegistrará

  • һҪװصIOCеHelloService
public class HelloService {
}
  • һRegistrarʵ֣һbeanװصIOC
public class FeignImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        BeanDefinition beanDefinition = new GenericBeanDefinition();
        beanDefinition.setBeanClassName(HelloService.class.getName());
        registry.registerBeanDefinition("helloService",beanDefinition);
    }
}
  • һע

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE})
    @Documented
    @Import({FeignImportBeanDefinitionRegistrar.class})
    public @interface EnableFeignTest {
    }
    
@EnableFeignClients(basePackages = "com.example.clients")
@EnableFeignTest
@SpringBootApplication
public class OpenfeignUserServiceApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(OpenfeignUserServiceApplication.class, args);
        System.out.println(context.getBean(HelloService.class));
    }

}
  • ͨʾԷ֣HelloServicebean ѾװصIOC Ƕ̬װصĹʵ֣@Configurationע룬˺ܶԡ okٻصFeignClientĽ

2.1.2 FeignClientsRegistrar

  • registerDefaultConfiguration ڲ SpringBoot ϼǷ@EnableFeignClients, иעĻ Feign صһЩע

  • registerFeignClients ڲ classpath У ɨ @FeignClient ε࣬ ݽΪ BeanDefinition , ͨ Spring еBeanDefinitionReaderUtils.resgisterBeanDefinition FeignClientBeanDeifinition ӵ spring .

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {
        //ע@EnableFeignClientsжdefaultConfigurationµ࣬װFeignClientSpecificationעᵽSpring
        //@FeignClientһԣconfigurationDZʾFeignClientԶ࣬ҲͨregisterClientConfigurationעFeignClientSpecification
        //ԣȫ@EnableFeignClientsõΪ׵ãڸ@FeignClientõľԶ
        registerDefaultConfiguration(metadata, registry);
        registerFeignClients(metadata, registry);
    }
    

2.2 registerDefaultConfiguration

private void registerDefaultConfiguration(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    // ȡmetadataйEnableFeignClientsֵֵԡ
    Map<String, Object> defaultAttrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName(), true);
    //defaultConfiguration ,ûʹĬϵconfiguration
    if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
        String name;
        if (metadata.hasEnclosingClass()) {
            name = "default." + metadata.getEnclosingClassName();
        } else {
            name = "default." + metadata.getClassName();
        }
        //ע
        this.registerClientConfiguration(registry, name, defaultAttrs.get("defaultConfiguration"));
    }

}
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, Object configuration) {
    //ʹBeanDefinitionBuilderBeanDefinition,ע
    BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(FeignClientSpecification.class);
    builder.addConstructorArgValue(name);
    builder.addConstructorArgValue(configuration);
    registry.registerBeanDefinition(name + "." + FeignClientSpecification.class.getSimpleName(), builder.getBeanDefinition());
}

BeanDefinitionRegistryspringڶ̬עBeanDefinitionϢĽӿڣregisterBeanDefinitionԽBeanDefinitionעᵽSpringУnameԾעBeanDefinitionƣעһFeignClientSpecificationĶ

FeignClientSpecificationʵ NamedContextFactory.SpecificationӿڣFeignʵҪһķУԶõʵSpringCloudʹNamedContextFactoryһЩеApplicationContextöӦSpecificationЩдʵ

NamedContextFactory3ܣ

  • AnnotationConfigApplicationContextġ
  • дȡbeanʵ
  • ʱеfeignʵ NamedContextFactoryиdzҪFeignContextڴ洢OpenFeignʵ
public class FeignContext extends NamedContextFactory<FeignClientSpecification> {
    public FeignContext() {
       super(FeignClientsConfiguration.class, "feign", "feign.client.name");
    }
 }

FeignContextﹹأ

ü pring-cloud-openfeign-core-2.2.3.RELEASE.jar!\META-INF\spring.factories2.2.1 FeignAutoConfiguration

ĬϵFeignClientsConfigurationΪݸ캯

FeignContextʱὫ֮ǰFeignClientSpecificationͨsetConfigurationsøcontextġ

2.2.2 createContext

org.springframework.cloud.context.named.NamedContextFactory#createContext

FeignContextĸcreateContextὫ AnnotationConfigApplicationContextʵʵΪǰĵģڹfeignIJͬʵڵFeignClientFactoryBeangetObjectʱácreateContextĻὲ⣩

protected AnnotationConfigApplicationContext createContext(String name) {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
	//ȡnameӦconfiguration,оעᵽcontext
    if (this.configurations.containsKey(name)) {
        for (Class<?> configuration : this.configurations.get(name)
             .getConfiguration()) {
            context.register(configuration);
        }
    }
    //עdefaultConfiguration,Ҳ FeignClientsRegistrarregisterDefaultConfigurationעConfiguration
    for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
        if (entry.getKey().startsWith("default.")) {
            for (Class<?> configuration : entry.getValue().getConfiguration()) {
                context.register(configuration);
            }
        }
    }
    //עPropertyPlaceholderAutoConfiguration
    context.register(PropertyPlaceholderAutoConfiguration.class,
                     this.defaultConfigType);
    //EnvironmentpropertySourcesԴ
    context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(
        this.propertySourceName,
        Collections.<String, Object>singletonMap(this.propertyName, name)));
    if (this.parent != null) {
        // Uses Environment from parent as well as beans
        context.setParent(this.parent);
        // jdk11 issue
        // https://github.com/spring-cloud/spring-cloud-netflix/issues/3101
        context.setClassLoader(this.parent.getClassLoader());
    }
    context.setDisplayName(generateDisplayName(name));
    context.refresh();
    return context;
}

NamedContextFactoryʵDisposableBeanԵʵʱ

@Override
public void destroy() {
    Collection<AnnotationConfigApplicationContext> values = this.contexts.values();
    for (AnnotationConfigApplicationContext context : values) {
        // This can fail, but it never throws an exception (you see stack traces
        // logged as WARN).
        context.close();
    }
    this.contexts.clear();
}

ܽ᣺NamedContextFactoryᴴ AnnotationConfigApplicationContextʵnameΪΨһʶȻÿAnnotationConfigApplicationContextʵעᲿ࣬ӶԸһϵеĻɵʵͿԻnameһϵеʵΪͬFeignClient׼ͬʵ

2.3 registerFeignClients Ҫɨ·е@FeignClientע⣬Ȼж̬Beanע롣ջ registerFeignClient

public void registerFeignClients(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {
    //ʡԴ...
    registerFeignClient(registry, annotationMetadata, attributes);
}

УȥװBeanDefinitionҲBeanĶ壬ȻעᵽSpring IOC

private void registerFeignClient(BeanDefinitionRegistry registry,AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
    String className = annotationMetadata.getClassName();
    BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
    //ʡԴ...
    BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition,className,new String[] { alias });
    BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}

ǹעһ£BeanDefinitionBuilderһBeanDefinitionģͨgenericBeanDefinition ģҴһFeignClientFactoryBeanࡣ

ǿԷ֣FeignClient̬עһFactoryBean

Spring Cloud FengnClientʵSpringĴɴ࣬ŻеFeignClientBeanDefinitionΪFeignClientFactoryBeanͣFeignClientFactoryBean̳FactoryBeanһBean

SpringУFactoryBeanһBeanBean

Bean һ Bean, Bean ˵ ߼Ǹ֪ Bean ͨ Bean ǹ Bean, ֻǰĻȡ Bean ʽȥã bean 󷵻صʵǹBean ִй Bean getObject ߼صʾҲʵBeanʱȥgetObject

public static BeanDefinitionBuilder genericBeanDefinition(Class<?> beanClass) {
    BeanDefinitionBuilder builder = new BeanDefinitionBuilder(new GenericBeanDefinition());
    builder.beanDefinition.setBeanClass(beanClass);
    return builder;
}

˵FeignClientעӿڣͨ FeignClientFactoryBean.getObject()һ

2.3.1 FeignClientFactoryBean.getObject getObjectõgetTargetapplicationContextȡFeignContextFeignContext̳NamedContextFactoryͳһάfeignиfeignͻ໥ġ

ţfeign.builderڹʱFeignContextȡõEncoderDecoderȸϢFeignContextƪѾᵽΪÿFeignͻ˷һǵĸspring

Feign.Builder֮жǷҪLoadBalanceҪͨLoadBalanceķáʵյõTarget.target()

@Override
public Object getObject() throws Exception {
    return getTarget();
}
<T> T getTarget() {
    //ʵFeignĶFeignContext
    FeignContext context = this.applicationContext.getBean(FeignContext.class);
    Feign.Builder builder = feign(context);//Builder
    if (!StringUtils.hasText(this.url)) {//urlΪգ߸ؾ⣬иؾ⹦ܵĴ
        if (!this.name.startsWith("http")) {
            this.url = "http://" + this.name;
        }
        else {
            this.url = this.name;
        }
        this.url += cleanPath();
        return (T) loadBalance(builder, context,new HardCodedTarget<>(this.type, this.name,this.url));
    }
    //ָurlĬϵĴ
    if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
        this.url = "http://" + this.url;
    }
    String url = this.url + cleanPath();
    //FeignContextgetInstanceȡClient
    Client client = getOptional(context, Client.class);
    if (client != null) {
        if (client instanceof LoadBalancerFeignClient) {
            // not load balancing because we have a url,
            // but ribbon is on the classpath, so unwrap
            client = ((LoadBalancerFeignClient) client).getDelegate();
        }
        if (client instanceof FeignBlockingLoadBalancerClient) {
            // not load balancing because we have a url,
            // but Spring Cloud LoadBalancer is on the classpath, so unwrap
            client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
        }
        builder.client(client);
    }//Ĭϴ
    Targeter targeter = get(context, Targeter.class);
    return (T) targeter.target(this, builder, context,new HardCodedTarget<>(this.type, this.name,url));
}

2.3.2 loadBalance ɾ߱ؾfeignͻˣΪfeignͻ˹󶨸ؾͻ.

Client client = (Client)this.getOptional(context, Client.class); лȡһClientĬLoadBalancerFeignClient

FeignRibbonClientAutoConfigurationԶװУͨImportʵֵ

@Import({ HttpClientFeignLoadBalancedConfiguration.class,OkHttpFeignLoadBalancedConfiguration.class,DefaultFeignLoadBalancedConfiguration.class })
protected <T> T loadBalance(Builder builder, FeignContext context,
                            HardCodedTarget<T> target) {
    Client client = (Client)this.getOptional(context, Client.class);
    if (client != null) {
        builder.client(client);
        Targeter targeter = (Targeter)this.get(context, Targeter.class);
        return targeter.target(this, builder, context, target);
    } else {
        throw new IllegalStateException("No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?");
    }
}

2.3.3 DefaultTarget.target

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

2.3.4 ReflectiveFeign.newInstance һ̬ķɶ̬֮ǰContractЭ飨Э򣬽ӿעϢڲMethodHandlerĴʽ

ʵֵĴпԿϤProxy.newProxyInstanceࡣҪÿĽӿڷضĴʵ֣һMethodHandlerĸǶӦInvocationHandler

public <T> T newInstance(Target<T> target) {
    //ݽӿContractЭʽӿϵķע⣬תڲMethodHandlerʽ
    Map<String, MethodHandler> nameToHandler = this.[targetToHandlersByName.apply(target)];
    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap();
    List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList();
    Method[] var5 = target.type().getMethods();
    int var6 = var5.length;
    for(int var7 = 0; var7 < var6; ++var7) {
        Method method = var5[var7];
        if (method.getDeclaringClass() != Object.class) {
            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)));
            }
        }
    }
    InvocationHandler handler = this.factory.create(target, methodToHandler);
    // Proxy.newProxyInstance Ϊӿഴ̬ʵ֣еתInvocationHandler 
    T proxy = Proxy.newProxyInstance(target.type().getClassLoader(), new Class[]{target.type()}, handler);
    Iterator var12 = defaultMethodHandlers.iterator();
    while(var12.hasNext()) {
        DefaultMethodHandler defaultMethodHandler = (DefaultMethodHandler)var12.next();
        defaultMethodHandler.bindTo(proxy);
    }
    return proxy;
}

2.4 ӿڶIJ FeignClientӿڵӦݡ

2.4.1 targetToHandlersByName.apply(target) ContractЭ򣬽ӿעϢڲ֣

targetToHandlersByName.apply(target);ӿڷϵע⣬ӶȵضϢȻһSynchronousMethodHandler ȻҪάһ<methodMethodHandler>mapInvocationHandlerʵFeignInvocationHandlerС

public Map<String, MethodHandler> apply(Target target) {
    List<MethodMetadata> metadata = contract.parseAndValidateMetadata(target.type());
    Map<String, MethodHandler> result = new LinkedHashMap<String,MethodHandler>();
    for (MethodMetadata md : metadata) {
        BuildTemplateByResolvingArgs buildTemplate;
        if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null)
        {
            buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder,target);
        } else if (md.bodyIndex() != null) {
            buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder,queryMapEncoder, target);
        } else {
            buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder,target);
        }
        if (md.isIgnored()) {
            result.put(md.configKey(), args -> {
                throw new IllegalStateException(md.configKey() + " is not a method handled by feign");
            });
        } else {
            result.put(md.configKey(),
                       factory.create(target, md, buildTemplate, options, decoder,errorDecoder));
        }
    }
    return result;
}

2.4.2 SpringMvcContract ǰSpring Cloud ΢УΪ˽ѧϰɱSpring MVCIJע Ҳ˵ дͻӿںд˴һͻ˺ͷ˿ͨSDKķʽԼͻֻҪ˷SDK APIͿʹӿڵı뷽ʽԽӷ

̳Contract.BaseContractʵResourceLoaderAwareӿڣ

þǶRequestMappingRequestParamRequestHeaderעнġ

2.5 OpenFeignù ǰķУ֪OpenFeignշصһ# ReflectiveFeign.FeignInvocationHandlerĶ

ôͻ˷ʱ뵽 FeignInvocationHandler.invokeУҶ֪һ̬ʵ֡

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (!"equals".equals(method.getName())) {
        if ("hashCode".equals(method.getName())) {
            return this.hashCode();
        } else {
            return "toString".equals(method.getName()) ? this.toString() :
            ((MethodHandler)this.dispatch.get(method)).invoke(args);
        }
    } else {
        try {
            Object otherHandler = args.length > 0 && args[0] != null ?
                Proxy.getInvocationHandler(args[0]) : null;
            return this.equals(otherHandler);
        } catch (IllegalArgumentException var5) {
            return false;
        }
    }
}

ţinvokeУ this.dispatch.get(method)).invoke(args) this.dispatch.get(method) ᷵һSynchronousMethodHandler,ش

ݲɵRequestTemplateHttpģ棬¡

public Object invoke(Object[] argv) throws Throwable {
    RequestTemplate template = this.buildTemplateFromArgs.create(argv);
    Options options = this.findOptions(argv);
    Retryer retryer = this.retryer.clone();
    while(true) {
        try {
            return this.executeAndDecode(template, options);
        } catch (RetryableException var9) {
            RetryableException e = var9;
            try {
                retryer.continueOrPropagate(e);
            } catch (RetryableException var8) {
                Throwable cause = var8.getCause();
                if (this.propagationPolicy == ExceptionPropagationPolicy.UNWRAP
                    && cause != null) {
                    throw cause;
                }
                throw var8;
            }
            if (this.logLevel != Level.NONE) {
                this.logger.logRetry(this.metadata.configKey(), this.logLevel);
            }
        }
    }
}

2.5.1 executeAndDecode Ĵ룬ѾrestTemplateƴװɣĴһ executeAndDecode() ÷ͨRequestTemplateRequestȻHttp ClientȡresponseȡӦϢ

Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
    //תΪHttp
    Request request = this.targetRequest(template);
    if (this.logLevel != Level.NONE) {
        this.logger.logRequest(this.metadata.configKey(), this.logLevel,request);
    }
    long start = System.nanoTime();
    Response response;
    try {
        //Զͨ
        response = this.client.execute(request, options);
        //ȡؽ
        response = response.toBuilder().request(request).requestTemplate(template).build();
    } catch (IOException var16) {
        if (this.logLevel != Level.NONE) {
            this.logger.logIOException(this.metadata.configKey(), this.logLevel,var16, this.elapsedTime(start));
        }
        throw FeignException.errorExecuting(request, var16);
    }
    long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
    boolean shouldClose = true;
    Response var10;
    try {
        if (this.logLevel != Level.NONE) {
            response = this.logger.logAndRebufferResponse(this.metadata.configKey(), this.logLevel,response, elapsedTime);
        }
        if (Response.class != this.metadata.returnType()) {
            Object result;
            Object var21;
            if (response.status() >= 200 && response.status() < 300) {
                if (Void.TYPE == this.metadata.returnType()) {
                    var10 = null;
                    return var10;
                }
                result = this.decode(response);
                shouldClose = this.closeAfterDecode;
                var21 = result;
                return var21;
            }
            if (this.decode404 && response.status() == 404 && Void.TYPE != this.metadata.returnType()) {
                result = this.decode(response);
                shouldClose = this.closeAfterDecode;
                var21 = result;
                return var21;
            }
            throw this.errorDecoder.decode(this.metadata.configKey(), response);
        }
        if (response.body() == null) {
            var10 = response;
            return var10;
        }
        if (response.body().length() != null && (long)response.body().length() <= 8192L) {
            byte[] bodyData = Util.toByteArray(response.body().asInputStream());
            Response var11 = response.toBuilder().body(bodyData).build();
            return var11;
        }
        shouldClose = false;
        var10 = response;
    } catch (IOException var17) {
        if (this.logLevel != Level.NONE) {
            this.logger.logIOException(this.metadata.configKey(), this.logLevel,var17, elapsedTime);
        }
        throw FeignException.errorReading(request, response, var17);
    } finally {
        if (shouldClose) {
            Util.ensureClosed(response.body());
        }
    }
    return var10;
}

2.5.2 Client.execute ĬϲJDK HttpURLConnection Զ̵á

@Override
public Response execute(Request request, Options options) throws IOException {
    HttpURLConnection connection = convertAndSend(request, options);
    return convertResponse(connection, request);
}
Response convertResponse(HttpURLConnection connection, Request request) throws IOException {
    int status = connection.getResponseCode();
    String reason = connection.getResponseMessage();
    if (status < 0) {
        throw new IOException(format("Invalid status(%s) executing %s %s",status,connection.getRequestMethod(),connection.getURL()));
    }
    Map<String, Collection<String>> headers = new LinkedHashMap<>();
    for (Map.Entry<String, List<String>> field :
         connection.getHeaderFields().entrySet()) {
        // response message
        if (field.getKey() != null) {
            headers.put(field.getKey(), field.getValue());
        }
    }
    Integer length = connection.getContentLength();
    if (length == -1) {
        length = null;
    }
    InputStream stream;
    if (status >= 400) {
        stream = connection.getErrorStream();
    } else {
        stream = connection.getInputStream();
    }
    return Response.builder()
        .status(status)
        .reason(reason)
        .headers(headers)
        .request(request)
        .body(stream, length)
        .build();
}

ο

https://lijunyi.xyz/docs/SpringCloud/SpringCloud.html#_2-2-x-%E5%88%86%E6%94%AF https://mp.weixin.qq.com/s/2jeovmj77O9Ux96v3A0NtA https://juejin.cn/post/6931922457741770760 https://github.com/D2C-Cai/herring http://c.biancheng.net/springcloud https://github.com/macrozheng/springcloud-learning