·# 《深入理解 Spring Cloud 与微服务构建》第八章 声明式调用 Feign

文章目录

  • 一、Feign 简介
    • 1.简介
    • 2.工作原理
  • 二、写一个 Feign 客户端
  • 三、FeignClient 详解
  • 四、FeignClient 的配置
  • 五、在 Feign 中使用 HttpClient 和 OkHttp
  • 六、Feign 是如何实现负载均衡的
  • 七、Feign 的源码实现过程

一、Feign 简介

1.简介

Feign 受 Retrofit、JAXRS-2.0 和 WebSocket 的影响,采用了声明式 API 接口的风格,将 Java Http 客户端绑定到它的内部。Feign 的首要目标是将 Java Http 客户端的书写过程变得简单。Feign 的源码地址:https://github.com/OpenFeign/feign

2.工作原理

Feign 是一个伪 Java Http 客户端,Feign 不做任何的请求处理。Feign 通过处理注解生成 Request 模板,从而简化了 Http API 的开发。开发人员可以使用注解的方式定制 Request API 模板。在发送 Http Request 请求之前,Feign 通过处理注解的方式替换掉 Request 模板中的参数,生成真正的 Request,并交给 Java Http 客户端去处理。利用这种方式,开发者只需要关注 Feign 注解模板的开发,而不用关注 Http 请求本身,简化了 Http 请求的过程,使得 Http 请求变得简单和容易理解

二、写一个 Feign 客户端

本案例在第七章 Ribbon 的工程基础之上进行改造

新建一个 Spring Boot 的 Moudle 工程,取名为 eureka-feign-client。首先,在工程的 pom 文件中加入相关的依赖,包括继承了主 Maven 工程的 pom 文件、Feign 的起步依赖 spring-cloud-starter-feign、Eureka Client 的起步依赖 spring-cloud-starter-eureka、WEB 功能的起步依赖 spring-boot-starter-web,以及 Spring Boot 测试的起步依赖 spring-boot-starter-test,代码如下:

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>Eureka</artifactId><groupId>org.sisyphus</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>eureka-feign-client</artifactId><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency></dependencies></project>

引入这些依赖之后,在工程的配置文件 application.yml 做相关的配置,包括配置程序名为 eureka-feign-client,端口号为 8765,服务注册地址为 http://localhost:8761/eureka/,代码如下:

spring:application:name: eureka-feign-clientserver:port: 8765eureka:client:serviceUrl:defaultZone: http://localhost:8761/eureka/

在程序的启动类 EurekaFeignClientApplication 加上注解 @EnableEurekaClient 开启 Eureka Client 的功能,通过注解 @EnableFeignClients 开启 Feign Client 的功能,代码如下:

package com.sisyphus;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class EurekaFeignClientApplication {public static void main(String[] args) {SpringApplication.run(EurekaFeignClientApplication.class, args);}
}

通过以上 3 个步骤,该程序就具备了 Feign 的功能,现在来实现一个简单得 Feign Client。新建一个 EurekaClientFeign 的接口,在接口上加 @FeignClient 注解来声明一个 Feign Client,其中 value 为远程调用其它服务的服务名,FeignConfig.class 为 Feign Client 的配置类。在 EurekaClientFeign 接口内部有一个 sayHiFromClientEureka() 方法,该方法通过 Feign 来调用 eureka-client 服务的 “/hi” 的 API 接口,代码如下:

package com.sisyphus.feign;import com.sisyphus.config.FeignConfig;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;@FeignClient(value = "eureka-client", configuration =  FeignConfig.class)
public interface EurekaClientFeign {@GetMapping(value = "/hi")String sayHiFromClientEureka(@RequestParam(value = "name") String name);
}

在 FeignConfig 类加上 @Configuration 注解,表明该类是一个配置类,并注入一个 BeanName 为 feignRetryer 的 Bean。注入该 Bean 之后,Feign 在远程调用失败后会进行重试。代码如下:

package com.sisyphus.config;import feign.Retryer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import static java.util.concurrent.TimeUnit.SECONDS;@Configuration
public class FeignConfig {@Beanpublic Retryer feignRetryer(){return new Retryer.Default(100, SECONDS.toMillis(1), 5);}
}

在 Service 层的 HiService 类注入 EurekaClientFeign 的 Bean,通过 EurekaClientFeign 去调用 sayHiFromClientEureka() 方法,其代码如下:

package com.sisyphus.service;import com.sisyphus.feign.EurekaClientFeign;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;@Service
public class HiService {@AutowiredEurekaClientFeign eurekaClientFeign;public String sayHi(String name){return eurekaClientFeign.sayHiFromClientEureka(name);}
}

在 HiController 上加上 @RestController 注解,开启 RestController 的功能,写一个 API 接口 “/hi”,在该接口调用了 HiService 的 sayHi() 方法。HiService 通过 EurekaClientFeign 远程调用 eureka-client 服务的 API 接口 “/hi”。代码如下:

package com.sisyphus.controller;import com.sisyphus.service.HiService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;@RestController
public class HiController {@AutowiredHiService hiService;@GetMapping("/hi")public String sayHi(@RequestParam(defaultValue = "sisyphus", required = false) String name){return hiService.sayHi(name);}
}

启动 eureka-server 工程,端口号为 8761;启动两个 eureka-client 工程的实例,端口号分别为 8762 和 8763;启动 eureka-feign-client 工程,端口号为 8765

在浏览器上多次访问 http://lcoalhost:8765/hi,浏览器会轮流显示以下内容:

hi sisyphus, i am from port:8762
hi sisyphus, i am from port:8763

由此可见,Feign Client 远程调用了 eureka-client 服务(存在端口为 8762 和 8763 两个实例)的 “/hi” API 接口,Feign Client 有负载均衡的能力

查看起步依赖 spring-cloud-starter-openfeign 的 pom 文件,可以看到该起步依赖默认引入了 Ribbon 和 Hystrix 的依赖,即负载均衡和熔断器的依赖。spring-cloud-starter-openfeign 的 pom 文件的代码如下:

<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 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-openfeign</artifactId><version>2.1.0.RELEASE</version><relativePath>..</relativePath></parent><artifactId>spring-cloud-starter-openfeign</artifactId><name>Spring Cloud Starter OpenFeign</name><description>Spring Cloud Starter OpenFeign</description><url>https://projects.spring.io/spring-cloud</url><organization><name>Pivotal Software, Inc.</name><url>https://www.spring.io</url></organization><properties><main.basedir>${basedir}/../..</main.basedir></properties><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-openfeign-core</artifactId></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-web</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-commons</artifactId></dependency><dependency><groupId>io.github.openfeign</groupId><artifactId>feign-core</artifactId></dependency><dependency><groupId>io.github.openfeign</groupId><artifactId>feign-slf4j</artifactId></dependency><dependency><groupId>io.github.openfeign</groupId><artifactId>feign-hystrix</artifactId></dependency><dependency><groupId>io.github.openfeign</groupId><artifactId>feign-java8</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-ribbon</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-archaius</artifactId><optional>true</optional></dependency></dependencies>
</project>

三、FeignClient 详解

首先来查看 FeignClient 注解 @FeignClient 的源码,其代码如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//package org.springframework.cloud.openfeign;import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FeignClient {@AliasFor("name")String value() default "";/** @deprecated */@DeprecatedString serviceId() default "";String contextId() default "";@AliasFor("value")String name() default "";String qualifier() default "";String url() default "";boolean decode404() default false;Class<?>[] configuration() default {};Class<?> fallback() default void.class;Class<?> fallbackFactory() default void.class;String path() default "";boolean primary() default true;
}

FeignClient 注解被 @Target(ElementType.TYPE) 修饰,表示 FeignClient 注解的作用目标在接口上。@Retention(RetentionPolicy.RUNTIME) 注解表明该注解会在 Class 字节码文件中存在,在运行时可以通过反射获取到。@Documented 表示该注解将被包含在 Javadoc 中

@FeignClient 注解用于创建声明式 API 接口,该接口是 RESTful 风格的。Feign 被设计成插拔式的,可以注入其它组件和 Feign 一起使用。最典型的是如果 Ribbon 可用,Feign 会和 Ribbon 相结合进行负载均衡

在代码中,value() 方法和 name() 方法一样,是被调用的服务的 ServiceId。url() 方法直接填写硬编码的 Url 地址。decode404() 方法即 404 是被解码,还是抛异常。configuration() 方法指明 FeignClient 的配置类,默认的配置类为 FeignClientsConfiguration 类,在缺省的情况下,这个类注入了默认的 Decoder、Encoder 和 Contract 等配置的 Bean。fallback() 为配置熔断器的处理类

四、FeignClient 的配置

Feign Client 默认的配置类为 FeignClientsConfiguration,这个类在 spring-cloud-openfeign-core 的 jar 包下。打开这个类,可以发现这个类注入了很多 Feign 相关的配置 Bean,包括 FeignRetryer、FeignLoggerFactory 和 FormattingConversionService 等。另外,Decoder、Encoder 和 Contract 这 3 个类在没有 Bean 被注入的情况下,会自动注入默认配置的 Bean,即 ResponseEntityDecoder、SpringEncoder 和 SpringMvcContract。默认注入的配置如下:

  • Decoder feignDecoder:ResponseEntityDecoder
  • Encoder feignEncoder:SpringEncoder
  • Logger feignLogger:S1f4jLogger
  • Contract feignContract:SpringMvcContract
  • Feign.Builder feignBuilder:HystrixFeign.Builder

FeignClientsConfiguration 的配置类部分代码如下,@ConditionalOnMissingBean 注解表示如果没有注入该类的 Bean,那么就会默认注入一个 Bean

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//package org.springframework.cloud.openfeign;import com.netflix.hystrix.HystrixCommand;
import feign.Contract;
import feign.Feign;
import feign.Logger;
import feign.Retryer;
import feign.Feign.Builder;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.hystrix.HystrixFeign;
import feign.optionals.OptionalDecoder;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.support.ResponseEntityDecoder;
import org.springframework.cloud.openfeign.support.SpringDecoder;
import org.springframework.cloud.openfeign.support.SpringEncoder;
import org.springframework.cloud.openfeign.support.SpringMvcContract;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.core.convert.ConversionService;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.format.support.FormattingConversionService;@Configuration
public class FeignClientsConfiguration {//省略代码@Bean@ConditionalOnMissingBeanpublic Retryer feignRetryer() {return Retryer.NEVER_RETRY;}@Bean@Scope("prototype")@ConditionalOnMissingBeanpublic Builder feignBuilder(Retryer retryer) {return Feign.builder().retryer(retryer);}@Bean@ConditionalOnMissingBean({FeignLoggerFactory.class})public FeignLoggerFactory feignLoggerFactory() {return new DefaultFeignLoggerFactory(this.logger);}//省略代码
}

重写 FeignClientsConfiguration 类中的 Bean,覆盖掉默认的配置 Bean,从而达到自定义配置的目的。例如 Feign 默认的配置在请求失败后,重试次数为 0,即不重试(Retryer.NEVER_RETRY)。现在希望在请求失败后能够重试,这时需要写一个配置 FeignConfig 类,在该类中注入 Retryer 的 Bean,覆盖掉默认的 Retryer 的 Bean,并将 FeignConfig 指定为 FeignClient 的配置类。FeignConfig 类的代码如下:

package com.sisyphus.config;import feign.Retryer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import static java.util.concurrent.TimeUnit.SECONDS;@Configuration
public class FeignConfig {@Beanpublic Retryer feignRetryer(){return new Retryer.Default(100, SECONDS.toMillis(1), 5);}
}

在上面的代码中,通过覆盖了默认的 Retryer 的 Bean,更改了该 FeignClient 的请求失败重试的策略,重试间隔为 100 毫秒,最大重试时间为 1 秒,重试次数为 5 次

五、在 Feign 中使用 HttpClient 和 OkHttp

在 Feign 中,Client 是一个非常重要的组件,Feign 最终发送 Request 请求以及接收 Response 响应都是由 Client 组件完成的。Client 在 Feign 源码中是一个接口,在默认情况下,Client 的实现类是 Client.Default,Client.Default 是由 HttpURLConnection 来实现网络请求的。另外,Client 还支持 HttpClient 和 OkHttp 来进行网络请求

那么,如何在 Feign 中使用 HttpClient 的网络请求框架呢?只需要在 pom 文件上加上 HttpClient 的 Classpath 即可。另外需要在配置文件 application.yml 中配置 feign.httpclient.enabled 为 true

在工程的 pom 文件加上 feign-httpclient 的依赖,Feign 就会采用 HttpClient 作为网络请求框架,而不是默认的 HttpURLConnection。代码如下:

<dependency><groupId>io.github.openfeign</groupId><artifactId>feign-httpclient</artifactId><version>RELEASE</version>
</dependency>

同理,如果想要在 Feign 中使用 OkHttp 作为网络请求框架,则只需要在 pom 文件上加上 feign-okhttp 的依赖,代码如下:

<dependency><groupId>com.netflix.feign</groupId><artifactId>feign-okhttp</artifactId><version>RELEASE</version>
</dependency>

六、Feign 是如何实现负载均衡的

FeignRibbonClientAutoConfiguration 类配置了 Client 的类型(包括 HttpURLConnection、OjHttp 和 HttpClient),最终向容器注入的是 Client 的实现类 LoadBalancerFeignClient LoadBalancerFeignClient,即负载均衡客户端。查看 LoadBalancerFeignClient 类中的 execute 方法,即执行请求的方法,代码如下:

 @Overridepublic Response execute(Request request, Request.Options options) throws IOException {try {URI asUri = URI.create(request.url());String clientName = asUri.getHost();URI uriWithoutHost = cleanUrl(request.url(), clientName);FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(this.delegate, request, uriWithoutHost);IClientConfig requestConfig = getClientConfig(options, clientName);return lbClient(clientName).executeWithLoadBalancer(ribbonRequest,requestConfig).toResponse();}catch (ClientException e) {IOException io = findIOException(e);if (io != null) {throw io;}throw new RuntimeException(e);}}

其中有一个 executeWithLoadBalancer() 方法,即通过负载均衡的方式来执行网络请求,代码如下:

 public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {LoadBalancerCommand command = this.buildLoadBalancerCommand(request, requestConfig);try {return (IResponse)command.submit(new ServerOperation<T>() {public Observable<T> call(Server server) {URI finalUri = AbstractLoadBalancerAwareClient.this.reconstructURIWithServer(server, request.getUri());ClientRequest requestForServer = request.replaceUri(finalUri);try {return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));} catch (Exception var5) {return Observable.error(var5);}}}).toBlocking().single();} catch (Exception var6) {Throwable t = var6.getCause();if (t instanceof ClientException) {throw (ClientException)t;} else {throw new ClientException(var6);}}}

在上述代码中,有一个 submit 方法,进入 submit() 方法的内部可以看出它是 LoadBalancerCommand 类的方法,代码如下:

 public Observable<T> submit(final ServerOperation<T> operation) {final LoadBalancerCommand<T>.ExecutionInfoContext context = new LoadBalancerCommand.ExecutionInfoContext();if (this.listenerInvoker != null) {try {this.listenerInvoker.onExecutionStart();} catch (AbortExecutionException var6) {return Observable.error(var6);}}final int maxRetrysSame = this.retryHandler.getMaxRetriesOnSameServer();final int maxRetrysNext = this.retryHandler.getMaxRetriesOnNextServer();Observable<T> o = (this.server == null ? this.selectServer() : Observable.just(this.server)).concatMap(new Func1<Server, Observable<T>>() {public Observable<T> call(Server server) {context.setServer(server);final ServerStats stats = LoadBalancerCommand.this.loadBalancerContext.getServerStats(server);Observable<T> o = Observable.just(server).concatMap(new Func1<Server, Observable<T>>() {public Observable<T> call(final Server server) {context.incAttemptCount();LoadBalancerCommand.this.loadBalancerContext.noteOpenConnection(stats);if (LoadBalancerCommand.this.listenerInvoker != null) {try {LoadBalancerCommand.this.listenerInvoker.onStartWithServer(context.toExecutionInfo());} catch (AbortExecutionException var3) {return Observable.error(var3);}}final Stopwatch tracer = LoadBalancerCommand.this.loadBalancerContext.getExecuteTracer().start();return operation.call(server).doOnEach(new Observer<T>() {private T entity;public void onCompleted() {this.recordStats(tracer, stats, this.entity, (Throwable)null);}public void onError(Throwable e) {this.recordStats(tracer, stats, (Object)null, e);LoadBalancerCommand.logger.debug("Got error {} when executed on server {}", e, server);if (LoadBalancerCommand.this.listenerInvoker != null) {LoadBalancerCommand.this.listenerInvoker.onExceptionWithServer(e, context.toExecutionInfo());}}public void onNext(T entity) {this.entity = entity;if (LoadBalancerCommand.this.listenerInvoker != null) {LoadBalancerCommand.this.listenerInvoker.onExecutionSuccess(entity, context.toExecutionInfo());}}private void recordStats(Stopwatch tracerx, ServerStats statsx, Object entity, Throwable exception) {tracerx.stop();LoadBalancerCommand.this.loadBalancerContext.noteRequestCompletion(statsx, entity, exception, tracerx.getDuration(TimeUnit.MILLISECONDS), LoadBalancerCommand.this.retryHandler);}});}});if (maxRetrysSame > 0) {o = o.retry(LoadBalancerCommand.this.retryPolicy(maxRetrysSame, true));}return o;}});if (maxRetrysNext > 0 && this.server == null) {o = o.retry(this.retryPolicy(maxRetrysNext, false));}return o.onErrorResumeNext(new Func1<Throwable, Observable<T>>() {public Observable<T> call(Throwable e) {if (context.getAttemptCount() > 0) {if (maxRetrysNext > 0 && context.getServerAttemptCount() == maxRetrysNext + 1) {e = new ClientException(ErrorType.NUMBEROF_RETRIES_NEXTSERVER_EXCEEDED, "Number of retries on next server exceeded max " + maxRetrysNext + " retries, while making a call for: " + context.getServer(), (Throwable)e);} else if (maxRetrysSame > 0 && context.getAttemptCount() == maxRetrysSame + 1) {e = new ClientException(ErrorType.NUMBEROF_RETRIES_EXEEDED, "Number of retries exceeded max " + maxRetrysSame + " retries, while making a call for: " + context.getServer(), (Throwable)e);}}if (LoadBalancerCommand.this.listenerInvoker != null) {LoadBalancerCommand.this.listenerInvoker.onExecutionFailed((Throwable)e, context.toFinalExecutionInfo());}return Observable.error((Throwable)e);}});}

在上述代码中,有一个 selectServer(),用于选择服务进行负载均衡,代码如下:

 private Observable<Server> selectServer() {return Observable.create(new OnSubscribe<Server>() {public void call(Subscriber<? super Server> next) {try {Server server = LoadBalancerCommand.this.loadBalancerContext.getServerFromLoadBalancer(LoadBalancerCommand.this.loadBalancerURI, LoadBalancerCommand.this.loadBalancerKey);next.onNext(server);next.onCompleted();} catch (Exception var3) {next.onError(var3);}}});}

由上述代码可知,最终负载均衡交给 loadBalancerContext 来处理,即第七章的 Ribbon

七、Feign 的源码实现过程

  1. 首先通过 @EnableFeignClients 注解开启 FeignClient 的功能。只有这个注解存在,才会在程序启动时开启对 @FeignClient 注解的包扫描
  2. 根据 Feign 的规则实现接口,并在接口上面加上 @FeignClient 注解
  3. 程序启动后,会进行包扫描,扫描所有的 @FeignClient 的注解的类,并将这些信息注入 IoC 容器中
  4. 当接口的方法被调用时,通过 JDK 的代理来生成具体的 RequestTemplate 模板对象
  5. 根据 RequestTemplate 再生成 HTTP 请求的 Request 对象
  6. Request 对象交给 Client 去处理,其中 Client 的网络请求框架可以是 HttpURLConnection、HttpClient 和 OkHttp
  7. 最后 Client 被封装到 LoadBalancerFeignClient 类,这个类结合 Ribbon 做到了负载均衡

《深入理解 Spring Cloud 与微服务构建》第八章 声明式调用 Feign相关推荐

  1. 《深入理解Spring Cloud与微服务构建》出版啦!

    作者简介 方志朋,毕业于武汉理工大学,CSDN博客专家,专注于微服务.大数据等领域,乐于分享,爱好开源,活跃于各大开源社区.著有<史上最简单的Spring Cloud教程>,累计访问量超过 ...

  2. 《深入理解 Spring Cloud 与微服务构建》第十八章 使用 Spring Security OAuth2 和 JWT 保护微服务系统

    <深入理解 Spring Cloud 与微服务构建>第十八章 使用 Spring Security OAuth2 和 JWT 保护微服务系统 文章目录 <深入理解 Spring Cl ...

  3. 《深入理解 Spring Cloud 与微服务构建》第十七章 使用 Spring Cloud OAuth2 保护微服务系统

    <深入理解 Spring Cloud 与微服务构建>第十七章 使用 Spring Cloud OAuth2 保护微服务系统 文章目录 <深入理解 Spring Cloud 与微服务构 ...

  4. 《深入理解 Spring Cloud 与微服务构建》第十六章 Spring Boot Security 详解

    <深入理解 Spring Cloud 与微服务构建>第十六章 Spring Boot Security 详解 文章目录 <深入理解 Spring Cloud 与微服务构建>第十 ...

  5. 《深入理解 Spring Cloud 与微服务构建》第十五章 微服务监控 Spring Boot Admin

    <深入理解 Spring Cloud 与微服务构建>第十五章 微服务监控 Spring Boot Admin 文章目录 <深入理解 Spring Cloud 与微服务构建>第十 ...

  6. 《深入理解 Spring Cloud 与微服务构建》第十四章 服务链路追踪 Spring Cloud Sleuth

    <深入理解 Spring Cloud 与微服务构建>第十四章 服务链路追踪 Spring Cloud Sleuth 文章目录 <深入理解 Spring Cloud 与微服务构建> ...

  7. 《深入理解 Spring Cloud 与微服务构建》第十三章 配置中心 Spring Cloud Config

    <深入理解 Spring Cloud 与微服务构建>第十三章 配置中心 Spring Cloud Config 文章目录 <深入理解 Spring Cloud 与微服务构建>第 ...

  8. 《深入理解 Spring Cloud 与微服务构建》第十二章 服务注册和发现 Consul

    <深入理解 Spring Cloud 与微服务构建>第十二章 服务注册和发现 Consul 文章目录 <深入理解 Spring Cloud 与微服务构建>第十二章 服务注册和发 ...

  9. 《深入理解 Spring Cloud 与微服务构建》第十一章 服务网关

    <深入理解 Spring Cloud 与微服务构建>第十一章 服务网关 文章目录 <深入理解 Spring Cloud 与微服务构建>第十一章 服务网关 一.服务网关简介 二. ...

  10. 《深入理解 Spring Cloud 与微服务构建》第十章 路由网关 Spring Cloud Zuul

    <深入理解 Spring Cloud 与微服务构建>第十章 路由网关 Spring Cloud Zuul 文章目录 <深入理解 Spring Cloud 与微服务构建>第十章 ...

最新文章

  1. 基于oracle的数据系统,基于Oracle 的数据库系统
  2. Asp.net SignalR 应用并实现群聊功能 开源代码
  3. memcached 双主复制
  4. Struts2+Hibernate+Spring 整合示例
  5. c/c++ 前置声明 -- typedef问题
  6. 教你如何一篇博客读懂设计模式之—--工厂模式
  7. UnhookWindowsHookEx
  8. 华为给力!算力最强AI处理器在中国!
  9. 如何将商业策略与项目管理相关联
  10. c语言获取栈可用大小,[求助]求教各位大神如何获得C语言函数体的大小?
  11. 图像处理-线性滤波-2 图像微分(1、2阶导数和拉普拉斯算子)
  12. 学习RedHat7第一课
  13. 二十年驷之过隙,互联网归来仍是少年
  14. 土木学matlab还是python_五行属土的字大全
  15. (20201209已解决)从window访问wsl地址
  16. windows查看电池损耗
  17. C语言入门知识1(零基础新手适用)
  18. bitset简单用法
  19. postgreSQL的外键级联删除
  20. linux read 少读末尾一行的问题

热门文章

  1. LeetCode Assign Cookies
  2. C# DataTable学习
  3. ZOJ 1610 Count the Colors
  4. 基于.NET实现数据挖掘--关联规则分析算法
  5. javascript如何用户的判断操作系统
  6. 一步一步SharePoint 2007之十五:实现Form认证(5)——更改认证的Provider
  7. python统计元音总数_python – 元音序列计数
  8. keras padding_GAN整体思路以及使用Keras搭建DCGAN
  9. 计算机快捷键m是什么,教程方法;m、ac快捷键电脑技巧-琪琪词资源网
  10. (85)FPGA显示激励(monitor)