文章目录
推荐
spring cloud openfeign官方文档介绍
SpringCloud-OpenFeign官方文档使用大全详解
SpringCloud OpenFeign 全功能配置详解(一文吃透OpenFeign)
如何处理Feign的重试问题
Spring Cloud OpenFeign 关键知识详解
OpenFeign 全方位讲解
Spring Cloud Open Feign专栏
关于Feign的重试机制
OpenFeign 的超时重试机制以及底层实现原理
如何处理Feign的重试问题
下面文档基本就是官方文档的翻译,源自:spring cloud openfeign官方文档介绍,做了一丢丢的补充
一、OpenFeign简介
github:https://github.com/spring-cloud/spring-cloud-openfeign
官方文档:https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#spring-cloud-feign
Feign 是一个声明式的 Web Service 客户端。它使编写 Web Service 客户端更容易。
要使用 Feign,需要创建一个接口并对其进行注解。
它有可插拔的注解支持,包括 Feign 注解和 JAX-RS 注解。Feign 还支持可插拔的编码器和解码器。
Spring Cloud 增加了对 Spring MVC 注解的支持,并支持使用 Spring Web 中默认使用的 HttpMessageConverters。
Spring Cloud 集成了 Eureka、Spring Cloud CircuitBreaker以及Spring Cloud LoadBalancer,以便在使用Feign时提供一个负载均衡的http客户端。
OpenFeign利用Ribbon维护了服务列表信息,并且通过轮询实现了客户端的负载均衡。而与Ribbon不同的是,通过feign只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用。
二、Springboot集成OpenFeign
1、引入starter
<!--openfeign--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency>
在引入具体的版本之前,可以看下spring-cloud与springboot对应版本的兼容性
Release TrainSpring Boot Generation2023.0.x aka Leyton3.2.x2022.0.x aka Kilburn3.0.x, 3.1.x (Starting with 2022.0.3)2021.0.x aka Jubilee2.6.x, 2.7.x (Starting with 2021.0.3)2020.0.x aka Ilford2.4.x, 2.5.x (Starting with 2020.0.3)Hoxton2.2.x, 2.3.x (Starting with SR5)Greenwich2.1.xFinchley2.0.xEdgware1.5.xDalston1.5.x
2、在启动类或者配置类上加@EnableFeignClients注解:
@SpringBootApplication@EnableFeignClientspublicclassApplication{publicstaticvoidmain(String[] args){SpringApplication.run(Application.class, args);}}
3、声明Feign接口
@FeignClient("stores")publicinterfaceStoreClient{@RequestMapping(method =RequestMethod.GET, value ="/stores")List<Store>getStores();@RequestMapping(method =RequestMethod.GET, value ="/stores")Page<Store>getStores(Pageable pageable);@RequestMapping(method =RequestMethod.POST,
value ="/stores/{storeId}",
consumes ="application/json")Storeupdate(@PathVariable("storeId")Long storeId,Store store);@RequestMapping(method =RequestMethod.DELETE, value ="/stores/{storeId:\\d+}")voiddelete(@PathVariableLong storeId);}
@FeignClient注解用于创建1个feign客户端,它在容器中的bean的名称就是接口的全限定名(可以通过@FeignClient注解的qualifiers属性来修改);
@FeignClient的value值为客户端的名称(此时可以做到负载均衡),当然也可以写完整的主机名或者是ip端口值;
可以通过@FeignClient的url属性来指定要访问的url(可以是全路径名,也可以是主机名);
上面例子中的feign客户端会去寻找stores服务对应的物理地址,如果你使用了Eureka作为注册中心,那么它就会从Eureka中服务列表中解析stores服务。如果你不想使用Eureka,你可以通过SimpleDiscoveryClient 配置stores服务列表。
4、@EnableFeignClients属性解析
@EnableFeignClients用于开启Feign自动配置。
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)@Documented@Import(FeignClientsRegistrar.class)public@interfaceEnableFeignClients{// basePackages的别名,允许更简洁的注释声明,// 例如:@ComponentScan("org.my.pkg"), 而不是@ComponentScan(basePackages="org.my.pkg")String[]value()default{};// 用户扫描Feign客户端的包,也就是@FeignClient标注的类,与value同义,并且互斥String[]basePackages()default{};// basePackages()的类型安全替代方案,用于指定要扫描带注释的组件的包。每个指定类所在的包都将被扫描。 // 考虑在每个包中创建一个特殊的无操作标记类或接口,除了被该属性引用之外没有其他用途。Class<?>[]basePackageClasses()default{};// 为所有扫描到的客户端定制@Configuration,默认配置都在FeignClientsConfiguration中,可以自己定制Class<?>[]defaultConfiguration()default{};// 可以指定@FeignClient标注的类,如果不为空,就会禁用类路径扫描Class<?>[]clients()default{};}
5、@FeignClient属性解析
@FeignClient用于标注Feign客户端。
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inheritedpublic@interfaceFeignClient{// name和value属性用于标注客户端名称,也可以用${propertyKey}获取配置属性@AliasFor("name")Stringvalue()default"";// 该类的Bean名称StringcontextId()default"";// name和value属性用于标注客户端名称,也可以用${propertyKey}获取配置属性@AliasFor("value")Stringname()default"";// 弃用 被qualifiers()替代。@DeprecatedStringqualifier()default"";// 模拟客户端的@Qualifiers值。// 如果qualifier()和qualifiers()都存在,我们将使用后者,// 除非qualifier()返回的数组为空或只包含空值或空白值,// 在这种情况下,我们将首先退回到qualifier(),// 如果也不存在,则使用default = contextId + "FeignClient"。String[]qualifiers()default{};// 绝对URL或可解析主机名Stringurl()default"";// 是否应该解码404而不是抛出FeignExceptionsbooleandecode404()defaultfalse;// 用于模拟客户端的自定义配置类。可以包含组成客户端部分的覆盖@Bean定义,// 默认配置都在FeignClientsConfiguration类中,可以指定FeignClientsConfiguration类中所有的配置Class<?>[]configuration()default{};// 指定失败回调类Class<?>fallback()defaultvoid.class;// 为指定的假客户端接口定义一个fallback工厂。// fallback工厂必须生成fallback类的实例,这些实例实现了由FeignClient注释的接口。Class<?>fallbackFactory()defaultvoid.class;// 所有方法级映射使用的路径前缀Stringpath()default"";// 是否将虚拟代理标记为主bean。默认为true。booleanprimary()defaulttrue;}
可以通过以下任何一种方式向Feign客户端提供URL:
三、覆盖默认配置
1、覆盖默认配置
在spring cloud feign中的1个核心概念就是命名客户端,每1个feign客户端都由各种组件,按照协议要求从远程服务器发起请求完成功能,每个这样的feign客户端都使用@FeignClient注解来标识。
spring cloud 会为每1个feign客户端使用FeignClientsConfiguration这个配置类创建1个spring容器,FeignClientsConfiguration类中定义的组件有:feign.Decoder、feign.Encoder、feign.Contract,并且可以使用@FeignClient注解的contextId属性来覆盖spring容器的名字。
在FeignClientsConfiguration类中,OpenFeign为我们做了很多默认配置,其中所有的配置我们都可以自定义并且覆盖。
@FeignClient(name ="stores", configuration =FooConfiguration.class)publicinterfaceStoreClient{//..}
在指定了我们自定义的FooConfiguration配置类之后,FooConfiguration配置类中自定义的配置会与FeignClientsConfiguration中的配置合并,并且FooConfiguration中的配置的组件的优先级会更高(覆盖FeignClientsConfiguration配置类中给我们的默认配置)。
注意!FooConfiguration类并不需要@Configuration注解,如果加上了@Configuration,就会全局生效,那么它里面定义的
feign.Decoder
,
feign.Encoder
,
feign.Contract
, etc.等组件就会成为默认配置(如果不想要FooConfiguration类中定义的组件成为默认组件,但是FooConfiguration上又加了@Configuration注解,那么就需要排除它,不让它被扫描到)。如果只在==@FeignClient中指定,那么就会只在该@FeignClient标注的类中生效==。@EnableFeignClients注解也可以指定配置类,它会在由该注解扫描到的客户端中应用指定配置类中定义的组件。
注意!@FeignClient4.0.2以版本前,使用url属性时,不需要name属性。现在name属性是必需的。
// name属性和url属性支持占位符表达式@FeignClient(name ="${feign.name}", url ="${feign.url}")publicinterfaceStoreClient{//..}
2、配置列表
Spring Cloud OpenFeign默认为Feign提供了以下bean配置:
- Decoder feign解码器: 是一个ResponseEntityDecoder (被包装成了SpringDecoder)
- Encoder feign编码器: 是一个SpringEncoder
- Logger feign的Logger: 是一个Slf4jLogger
- MicrometerObservationCapability micrometerObservationCapability: 如果feign-micrometer在类路径中并且ObservationRegistry可用
- CachingCapability cachingCapability:如果使用了@EnableCaching注解会使用。可以通过spring.cloud.openfeign.cache.enabled配置禁用。
- Contract feignContract: 是一个==SpringMvcContract ==
- Feign.Builder feignBuilder: 是一个FeignCircuitBreaker.Builder
- Client feignClient: 如果Spring Cloud LoadBalancer在类路径上,则使用FeignBlockingLoadBalancerClient。如果它们都不在类路径中,则使用默认的feign客户端。
spring-cloud-starter-openfeign支持spring-cloud-starter-loadbalancer,但是因为后者是个可选依赖,如果想使用这个依赖的话,那么就需要自己引入它。
可以通过设置feign.okhttp.enabled、feign.httpclient.enabled、feign.httpclient.hc5.enabled为true,来分别开启对应的OkHttpClient、ApacheHttpClient 、ApacheHC5 客户端,并且要把它们的依赖放在类路径上。或者在容器中定义1个org.apache.http.impl.client.CloseableHttpClient、或者okhttp3.OkHttpClient、或者org.apache.hc.client5.http.impl.classic.CloseableHttpClient的客户端bean来切换不同的客户端实现。
Spring Cloud OpenFeign没有为Feign默认提供以下bean,但仍然从应用程序上下文中查找这些类型的bean来创建feign客户端:
- Logger.Level
- Retryer
- ErrorDecoder
- Request.Options
- Collection<RequestInterceptor>
- SetterFactory
- QueryMapEncoder
- Capability (MicrometerObservationCapability and CachingCapability are provided by default)
其中Retryer 默认是Retryer.NEVER_RETRY,这将禁止重试。请注意,这种重试行为不同于openfeign默认行为,它将自动重试IOExceptions,将它们视为暂时的网络相关异常,以及从ErrorDecoder抛出的任何RetryableException。
我们可以自定义以上任意一个Bean,来覆盖默认的配置:
@ConfigurationpublicclassFooConfiguration{@BeanpublicContractfeignContract(){returnnewfeign.Contract.Default();}@BeanpublicBasicAuthRequestInterceptorbasicAuthRequestInterceptor(){returnnewBasicAuthRequestInterceptor("user","password");}}
这个配置会使用feign.Contract.Default替换默认的SpringMvcContract,并且会将定义的BasicAuthRequestInterceptor这个bean添加到RequestInterceptor集合当中去。
3、使用配置文件进行配置
@FeignClient的配置也可以在配置文件中进行配置,其中feignName就是@FeignClient的value值、name值和contextId值,同时,在使用负载均衡时,这里的feignName也会被用来查询服务实例。
在如下配置中指定的类,必须在容器中有定义1个或者有1个默认的构造器。
feign:client:config:feignName:connectTimeout:5000readTimeout:5000loggerLevel: full
errorDecoder: com.example.SimpleErrorDecoder
retryer: com.example.SimpleRetryer
defaultQueryParameters:query: queryValue
defaultRequestHeaders:header: headerValue
requestInterceptors:- com.example.FooRequestInterceptor
- com.example.BarRequestInterceptor
decode404:falseencoder: com.example.SimpleEncoder
decoder: com.example.SimpleDecoder
contract: com.example.SimpleContract
capabilities:- com.example.FooCapability
- com.example.BarCapability
queryMapEncoder: com.example.SimpleQueryMapEncoder
metrics.enabled:false
也可以通过@EnableFeignClient注解的defaultConfiguration属性根据上面类似的方式来指定1个配置类,区别在于这种方式将会应用到所有的feign客户端。
也可以通过设置名为default的feignName来作全局的配置,并且配置文件优先(相比于配置类的方式,但是如果你想更改这个优先级,可以把feign.client.default-to-properties设置为false):
spring:cloud:openfeign:client:config:default:connectTimeout:5000readTimeout:5000loggerLevel: basic
4、创建多个相同名称客户端
如果我们想要创建多个具有相同name或url的feign客户端,以便它们指向相同的服务器,但是每个客户端都具有不同的自定义配置,那么我们必须使用@FeignClient的contextId属性,以避免这些配置beans的名称冲突。
@FeignClient(contextId ="fooClient", name ="stores", configuration =FooConfiguration.class)publicinterfaceFooClient{//..}@FeignClient(contextId ="barClient", name ="stores", configuration =BarConfiguration.class)publicinterfaceBarClient{//..}
5、配置FeignClient不从父上下文继承beans
可以通过配置1个FeignClientConfigurer的bean,并且重写这个bean的inheritParentConfiguration(),并且返回false,来配置feign客户端不从父容器中拿bean组件
@ConfigurationpublicclassCustomConfiguration{@BeanpublicFeignClientConfigurerfeignClientConfigurer(){returnnewFeignClientConfigurer(){@OverridepublicbooleaninheritParentConfiguration(){returnfalse;}};}}
提示:默认情况下,feign客户端不会对
/
编码,可以通过设置feign.client.decodeSlash为false来更改这个行为。
6、SpringEncoder 的配置
在我们提供的SpringEncoder中,我们为二进制内容类型设置空字符集,为所有其他内容类型设置UTF-8。
您可以通过将spring.cloud.openfeign.encoder.charset-from-content-type的值设置为true来修改此行为,以从Content-Type头字符集派生字符集。
7、Feign拦截器的配置及使用
拦截器是OpenFeign可用的一种强大的工具,它可以被用来在请求和响应前后进行一些额外的处理。要使用OpenFeign拦截器,可以通过以下步骤进行配置:
publicclassMyInterceptorimplementsRequestInterceptor{@Overridepublicvoidapply(RequestTemplate requestTemplate){// 在这里添加额外的处理逻辑,添加请求头RequestAttributes requestAttributes =RequestContextHolder.getRequestAttributes();if(requestAttributes instanceofServletRequestAttributes){ServletRequestAttributes attributes =(ServletRequestAttributes) requestAttributes;HttpServletRequest request = attributes.getRequest();String value = request.getHeader(headerName);
template.header(headerName, headerValue);}}}
将拦截器注册到OpenFeign:
@ConfigurationpublicclassMyFeignConfiguration{@BeanpublicMyInterceptormyInterceptor(){returnnewMyInterceptor();}// 非必须@BeanpublicFeign.BuilderfeignBuilder(){returnFeign.builder().requestInterceptor(myInterceptor());}}
8、OpenFeign超时时间设置
我们可以配置默认的超时时间,也可以为指定的feign客户端配置超时时间。
Open Feign提供了2个超时参数供设置:connectTimeout(防止由于服务器处理时间过长而阻塞调用者)、readTimeout(从连接建立开始到响应花费时间)
(1)使用配置文件配置
在应用程序的配置文件(application.yml或application.properties)中,可以使用以下属性设置超时时间:
# YAMLfeign:client:config:default:connectTimeout:5000# 连接超时时间readTimeout:10000# 读取超时时间# Properties
feign.client.config.default.connectTimeout=5000 # 连接超时时间
feign.client.config.default.readTimeout=10000 # 读取超时时间
上述代码中,我们使用feign.client.config.default属性来配置全局默认的超时时间。connectTimeout属性设置连接超时时间,readTimeout属性设置读取超时时间。单位是毫秒。
(2)通过Java代码设置超时时间
如果你更喜欢使用Java代码来配置openfeign,可以通过以下方式设置超时时间:
importfeign.Request;// 创建一个Request.Options对象来设置超时时间Request.Options options =newRequest.Options(connectTimeoutMillis, readTimeoutMillis);// 在创建Feign客户端时指定Options对象MyApi myApi =Feign.builder().options(options).target(MyApi.class,"https://example.com");
在上述代码中,我们创建了一个Request.Options对象,该对象包含连接超时时间和读取超时时间。然后将Options对象传递给Feign客户端。
(3)使用@FeignClient设置超时时间
使用@FeignClient注解的configuration属性来指定配置类。
首先,创建一个配置类,继承自feign.Request.Options类,并重写connectTimeoutMillis和readTimeoutMillis方法,以设置超时时间。
importfeign.Request;publicclassMyApiConfigurationextendsRequest.Options{publicMyApiConfiguration(int connectTimeoutMillis,int readTimeoutMillis){super(connectTimeoutMillis, readTimeoutMillis);}@OverridepublicIntegerconnectTimeoutMillis(){return5000;// 设置连接超时时间为5秒}@OverridepublicIntegerreadTimeoutMillis(){return10000;// 设置读取超时时间为10秒}}
然后,在使用@FeignClient注解进行声明时,使用configuration属性指定该配置类。
@FeignClient(name ="my-service", configuration =MyApiConfiguration.class)publicinterfaceMyApi{// 接口定义}
这样,只有针对MyApi接口的请求会使用这个配置类中的超时时间,级别更加细致。当然,你也可以在上述配置类中加入其它一些针对MyApi接口的配置,比如重试次数等等。
(4)使用拦截器设置超时时间
要为单独请求设置超时时间,可以通过实现RequestInterceptor接口,并在其中为请求添加超时时间信息。具体方法如下:
importfeign.RequestInterceptor;importfeign.RequestTemplate;publicclassTimeoutRequestInterceptorimplementsRequestInterceptor{privatefinalint connectTimeoutMillis;privatefinalint readTimeoutMillis;publicTimeoutRequestInterceptor(int connectTimeoutMillis,int readTimeoutMillis){this.connectTimeoutMillis = connectTimeoutMillis;this.readTimeoutMillis = readTimeoutMillis;}@Overridepublicvoidapply(RequestTemplate template){
template.options(newRequest.Options(connectTimeoutMillis, readTimeoutMillis));}}
在上述代码中,我们创建了一个TimeoutRequestInterceptor类,实现了RequestInterceptor接口,并重写了其中的apply方法。在该方法中,将请求的超时时间信息添加到请求模板中。
然后,在实际使用Feign客户端时,创建该拦截器对象并加入到Feign客户端的拦截器链中。
例如,我们想要对一个名为MyApi的Feign客户端接口的某个请求设置超时时间,可以这样:
MyApi myApi =Feign.builder().requestInterceptor(newTimeoutRequestInterceptor(3000,5000))// 为该客户端指定一个拦截器.target(MyApi.class,"https://example.com");
在上述代码中,我们创建了一个TimeoutRequestInterceptor对象,并使用requestInterceptor方法将其加入到Feign客户端的拦截器链中。这样,在名为MyApi的Feign客户端中发出的所有请求都会使用该超时时间。
如果只想为某些请求设置超时时间,而不是所有请求,可以在该拦截器中添加一些判断逻辑,根据请求的条件来判断是否要添加超时时间信息。
(5)使用@Headers设置超时时间
通过在接口方法上加上@Headers注解,将超时时间信息直接加在请求头中,从而实现为单独请求设置超时时间。
例如,我们想要针对MyApi接口的someMethod方法单独设置超时时间,可以这样:
@Headers({"connect-timeout:5000","read-timeout:10000"})@GET("/someMethod")StringsomeMethod();
在上述代码中,我们在@Headers注解中添加了connect-timeout和read-timeout两个请求头信息,用于设置连接超时时间和读取超时时间。这样,在调用someMethod方法时,会使用这些请求头信息中指定的超时时间设置。
需要注意的是,这种方法需要在每个接口方法上都进行设置,因此比较麻烦。但它的优点是灵活性比较高,可以为不同的接口方法设置不同的超时时间。同时,也可以在其他注解中添加相应的超时信息,如@PostMapping、@PutMapping等。
(6)为单独接口设置超时时间
在feign接口里加入Request.Options这个参数就可以单独为接口单独设置超时时间了
@PostMapping("test/")ResponseVO<?>test(Request.Options options,@RequestBodyTestRequestEntity entity);
调用的时候new 一下Options对象
ResponseVO<?> resp = client.test(newRequest.Options(70,TimeUnit.SECONDS,70,TimeUnit.SECONDS,true),
entity);
9、OpenFeign设置重试次数
(1)一般写法
定义一个继承自 Retryer 接口的类:
publicclassCustomRetryerimplementsRetryer{privatefinalint maxAttempts;privatefinallong backoff;int attempt;publicCustomRetryer(){this(5,1000);}publicCustomRetryer(int maxAttempts,long backoff){this.maxAttempts = maxAttempts;this.backoff = backoff;this.attempt =1;}@OverridepublicvoidcontinueOrPropagate(RetryableException e){if(attempt++>= maxAttempts){throw e;}try{Thread.sleep(backoff);}catch(InterruptedException ignored){Thread.currentThread().interrupt();throw e;}}@OverridepublicRetryerclone(){returnnewCustomRetryer();}}
在 FeignClient 中使用上一步定义的重试器:
@FeignClient(name ="demo", url ="${demo.base-url}", configuration =CustomRetryer.class)publicinterfaceDemoFeignClient{//...}
在这个例子中,使用的是自定义的重试器 CustomRetryer,它重试 5 次,在每次重试之间休眠 1000 毫秒。如果重试次数超限,则抛出 RetryableException 异常。
(2)简单写法
除了使用自定义的 Retryer 之外,OpenFeign 还提供了另外一种设置重试次数的方式,那就是通过 Feign 的配置项进行设置。具体操作如下:
在 FeignClient 中引入 Feign 的默认配置:
@FeignClient(name ="demo", url ="${demo.base-url}", configuration =FeignConfiguration.class)publicinterfaceDemoFeignClient{//...}
自定义 FeignConfiguration 类:
@ConfigurationpublicclassFeignConfiguration{@BeanpublicRetryerretryer(){returnnewRetryer.Default(500,5000,3);}}
在这里,我们使用 Retryer.Default 类生成一个默认的重试器,它会在当前请求失败后重试 3 次,并会在第一次重试前等待 500 毫秒,在第二次重试前等待 1000 毫秒,在第三次重试前等待 2000 毫秒,以此类推。
通过这两个步骤,我们就可以为每个 FeignClient 设置默认的重试次数了。
(3)为每个请求设置重试次数
如果我们需要为特定的请求设置不同的重试策略,则可以在对应的方法上加上 @Retryable 注解,并指定对应的 Retryer 类型,如下所示:
@FeignClient(name ="demo", url ="${demo.base-url}", configuration =FeignConfiguration.class)publicinterfaceDemoFeignClient{@RequestMapping(method =RequestMethod.GET, value ="/get")@Retryable(maxAttempts =2, value ={SomeRetryer.class})StringgetDemo();}
在这个例子中,我们使用了自定义的重试器 SomeRetryer,并指定了最大重试次数为 2。注意,为了使用 @Retryable 注解,我们需要引入 Spring Retry 库的依赖:
<dependency><groupId>org.springframework.retry</groupId><artifactId>spring-retry</artifactId><version>1.2.5.RELEASE</version></dependency>
使用上述方式,我们可以为每个请求设置不同的重试策略,从而更加灵活地处理重试问题。
10、Feign请求日志级别设置
每1个feign客户端都会创建1个logger,默认情况下,logger的名字就是接口的全类名,feign日志只会对debug级别才打印出来。
Feign提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解Feign中Http请求的细节。默认显示的是DEBUG级别日志。
// 设置指定客户端的日志logging.level.com.zzhua.user.UserClient:DEBUG
就是对Feign接口的调用情况进行监控和输出。
总共有以下日志级别:
- NONE:默认的,不显示任何日志。
- BASIC:仅记录请求方法、URL、响应状态码、执行时间。
- HEADERS:除了BASIC中定义的信息之外,还有请求和响应头。
- FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据。
// 代码设置日志级别(修改默认的日志级别)@ConfigurationpublicclassFooConfiguration{@BeanLogger.LevelfeignLoggerLevel(){returnLogger.Level.FULL;}}
四、手动创建feign客户端
可以使用Feign Builder API创建客户端来进行定制。
// 手动创建两个Feign客户端并配置其拦截器和name属性,FeignClientsConfiguration.class仍然是它们的默认配置// FeignClientsConfiguration是Spring Cloud OpenFeign提供的默认配置类@Import(FeignClientsConfiguration.class)classFooController{privateFooClient fooClient;privateFooClient adminClient;@AutowiredpublicFooController(Client client,Encoder encoder,Decoder decoder,Contract contract,MicrometerObservationCapability micrometerObservationCapability){this.fooClient =Feign.builder().client(client).encoder(encoder).decoder(decoder)// Contract 定义了在接口上能够使用的注解, 这里自动注入的Contract支持springmvc注解, // 而不是feign的原始注解.contract(contract).addCapability(micrometerObservationCapability).requestInterceptor(newBasicAuthRequestInterceptor("user","user"))// PROD-SVC是请求的服务名.target(FooClient.class,"https://PROD-SVC");this.adminClient =Feign.builder().client(client).encoder(encoder).decoder(decoder).contract(contract).addCapability(micrometerObservationCapability).requestInterceptor(newBasicAuthRequestInterceptor("admin","admin")).target(FooClient.class,"https://PROD-SVC");}}
还可以使用Builder 来配置FeignClient不从父上下文继承beans。可以通过在生成器上重写调用“inheritParentContext(false)”来实现这一点。
五、Feign的SpringCloud断路器
如果Spring Cloud CircuitBreaker在classpath,并且spring.cloud.openfeign.circuitbreaker.enabled=true,Feign将使用断路器包装所有方法。
要在每个客户端的基础上禁用Spring Cloud CircuitBreaker支持,请创建一个普通的Feign.Builder。具有“prototype”范围的构建器,例如:
@ConfigurationpublicclassFooConfiguration{@Bean@Scope("prototype")publicFeign.BuilderfeignBuilder(){returnFeign.builder();}}
断路器的名字遵循这样的格式:<feign客户端类名>#<被调用的方法名>(<参数类型>)。比如当调用1个FooClient接口的bar方法,并且这个方法没有参数时,断路器的名字就是:FooClient#bar()
注意:从2020.0.2开始,circuit breaker 名称模式已经从
<feignClientName>_<calledMethod>
改变。使用2020.0.4中引入的 CircuitBreakerNameResolver,circuit breaker 名称可以保留旧模式。
通过提供CircuitBreakerNameResolver的bean,可以更改断路器名称模式,如下所示。
@ConfigurationpublicclassFooConfiguration{@BeanpublicCircuitBreakerNameResolvercircuitBreakerNameResolver(){return(String feignClientName,Target<?> target,Method method)->
feignClientName +"_"+ method.getName();}}
要启用Spring Cloud CircuitBreaker组,请将spring.cloud.openfeign.circuitbreaker.group.enabled属性设置为true(默认为false)。
1、使用配置属性配置断路器
假如说有一个Feign客户端:
@FeignClient(url ="http://localhost:8080")publicinterfaceDemoClient{@GetMapping("demo")StringgetDemo();}
可以通过执行以下操作,使用配置属性对其进行配置:
spring:cloud:
openfeign
circuitbreaker:enabled:truealphanumeric-ids:enabled:trueresilience4j:circuitbreaker:instances:DemoClientgetDemo:minimumNumberOfCalls:69timelimiter:instances:DemoClientgetDemo:timeoutDuration: 10s
如果你想切换回 Spring Cloud 2022.0.0 之前使用的 circuit breaker name,你可以将 spring.cloud.openfeign.circuitbreaker.alphanumeric-ids.enabled 设置为 false。
2、fallback
fallback降级处理
Spring Cloud CircuitBreaker支持fallback的概念:当电路断开或出现错误时,执行的默认代码路径。要为给定的@FeignClient启用降级,请将fallback属性设置为实现降级的类名。并且还需要将其定义为Spring bean。
@FeignClient(name ="test", url ="http://localhost:${server.port}/", fallback =Fallback.class)protectedinterfaceTestClient{@RequestMapping(method =RequestMethod.GET, value ="/hello")HellogetHello();@RequestMapping(method =RequestMethod.GET, value ="/hellonotfound")StringgetException();}@ComponentstaticclassFallbackimplementsTestClient{@OverridepublicHellogetHello(){thrownewNoFallbackAvailableException("Boom!",newRuntimeException());}@OverridepublicStringgetException(){return"Fixed response";}}
fallbackFactory降级处理
如果有需要知道触发fallback的原因,可以使用@FeignClient中的fallbackFactory属性。
@FeignClient(name ="testClientWithFactory",
url ="http://localhost:${server.port}/",// 使用fallbackFactory属性指定TestFallbackFactory(它要实现FallbackFactory)
fallbackFactory =TestFallbackFactory.class)protectedinterfaceTestClientWithFactory{@RequestMapping(method =RequestMethod.GET, value ="/hello")HellogetHello();@RequestMapping(method =RequestMethod.GET, value ="/hellonotfound")StringgetException();}@Component// 实现FallbackFactory接口, 在create(Throwable)方法种返回1个实现了feign接口的对象staticclassTestFallbackFactoryimplementsFallbackFactory<FallbackWithFactory>{@OverridepublicFallbackWithFactorycreate(Throwable cause){returnnewFallbackWithFactory();}}staticclassFallbackWithFactoryimplementsTestClientWithFactory{@OverridepublicHellogetHello(){thrownewNoFallbackAvailableException("Boom!",newRuntimeException());}@OverridepublicStringgetException(){return"Fixed response";}}
3、Feign客户端的primary属性
(要知道有这回事)
当使用Feign和Spring Cloud CircuitBreaker 降级功能时,在ApplicationContext中有多个相同类型的beans。这将导致@Autowired不起作用,因为没有确切的一个bean,或者一个被标记为@Primary注解的bean。
为了解决这个问题,Spring Cloud OpenFeign将所有的Feign实例都标记为了@Primary,因此Spring Framework将知道要注入哪个bean。在某些情况下,这可能并不理想。要关闭此行为,请将@FeignClient的primary属性设置为false(默认为true)。
@FeignClient(name ="hello", primary =false)publicinterfaceHelloClient{// methods here}
六、Feign的继承重用
Feign通过单一继承接口支持样板API。这允许将常见操作分组到方便的基本接口中。
// 共用接口实例publicinterfaceUserService{@RequestMapping(method =RequestMethod.GET, value ="/users/{id}")UsergetUser(@PathVariable("id")long id);}// 提供(方)服务@RestControllerpublicclassUserResourceimplementsUserService{}// 调用(方)服务@FeignClient("users")publicinterfaceUserClientextendsUserService{}
注意:@FeignClient接口不应在服务端和客户端之间共享,并且不再支持在类级别上同时使用@RequestMapping和@FeignClient注解。
七、Feign请求响应的压缩
可以考虑为您的feign请求启用请求或响应GZIP压缩。您可以通过启用以下属性之一来实现这一点:
spring.cloud.openfeign.compression.request.enabled=true
spring.cloud.openfeign.compression.response.enabled=true
Feign请求压缩为您提供了类似于您可能为web服务器的设置:
spring.cloud.openfeign.compression.request.enabled=true
spring.cloud.openfeign.compression.request.mime-types=text/xml,application/xml,application/json
spring.cloud.openfeign.compression.request.min-request-size=2048
以上这些属性压缩的媒体类型和最小请求阈值长度都是可选的。
注意!由于OkHttpClient使用“透明”压缩,如果存在content-encoding或accept-encoding头,则该压缩将被禁用,因此当feign.okhttp.OkHttpClient存在于classpath中并且spring.cloud.openfeign.okhttp.enabled设置为true时,我们不启用压缩。
八、Feign Capability 的支持
Feign Capability 暴露了Feign的核心组件,因此这些组件可以被修改。例如,这些功能可以接受客户端,对其进行装饰,并将装饰后的实例反馈给 Feign。对 Micrometer 的支持就是一个很好的现实生活中的例子。参见 [micrometer-support]。
创建一个或多个 Capability Bean并将其置于 @FeignClient 配置中,可以让你注册它们并修改相关客户端的行为
@ConfigurationpublicclassFooConfiguration{@BeanCapabilitycustomCapability(){returnnewCustomCapability();}}
九、Feign Metrics
如果以下所有条件为 true,就会创建并注册一个 MicrometerCapability Bean,这样你的 Feign 客户端就可以被 Micrometer 观察到:
- feign-micrometer 在 classpath 上。
- MeterRegistry bean 可用。
- feign micrometer 属性设置为 true (默认) - spring.cloud.openfeign.micrometer.enabled=true (针对所有客户)- spring.cloud.openfeign.client.config.feignName.micrometer.enabled=true (针对单个客户端)
如果你的应用程序已经使用了 Micrometer,启用这个功能就像把 feign-micrometer 放到你的classpath上一样简单。
你也可以通过以下两种方式禁用该功能:
- 从你的 classpath 中排除 feign-micrometer。
- 将 feign micrometer 一个属性设置为 false - spring.cloud.openfeign.micrometer.enabled=false- spring.cloud.openfeign.client.config.feignName.micrometer.enabled=false
注意:spring.cloud.openfeign.micrometer.enabled=false 禁用所有 Feign 客户端的 Micrometer 支持,而不考虑客户端级标志的值:spring.cloud.openfeign.client.config.feignName.micrometer.enabled。如果你想启用或禁用每个客户端的 Micrometer 支持,不要设置 spring.cloud.openfeign.micrometer.enabled 并使用 spring.cloud.openfeign.client.config.feignName.micrometer.enabled。
你也可以通过注册你自己的bean来自定义 MicrometerObservationCapability:
@ConfigurationpublicclassFooConfiguration{@BeanpublicMicrometerObservationCapabilitymicrometerObservationCapability(ObservationRegistry
registry){returnnewMicrometerObservationCapability(registry);}}
仍然可以在 Feign 中使用 MicrometerCapability(仅支持指标),你需要禁用 Micrometer 支持(spring.cloud.openfeign.micrometer.enabled=false)并创建一个 MicrometerCapability Bean:
@ConfigurationpublicclassFooConfiguration{@BeanpublicMicrometerCapabilitymicrometerCapability(MeterRegistry meterRegistry){returnnewMicrometerCapability(meterRegistry);}}
十、开启Feign的缓存
如果使用了@EnableCaching注释,将创建并注册一个CachingCapability bean,这样您的Feign客户端能够识别其接口上的@Cache*注解:
publicinterfaceDemoClient{@GetMapping("/demo/{filterParam}")@Cacheable(cacheNames ="demo-cache", key ="#keyParam")StringdemoEndpoint(String keyParam,@PathVariableString filterParam);}
还可以通过属性spring.cloud.openfeign.cache.enabled=false禁用该功能。
十、@SpringQueryMap注解支持
Spring Cloud OpenFeign提供了一个等价的@SpringQueryMap注释,用于将POJO或Map参数注释为查询参数Map。
例如,Params类定义了参数param1和param2:
// Params.javapublicclassParams{privateString param1;privateString param2;// [Getters and setters omitted for brevity]}
下面的feign客户端通过使用@SpringQueryMap注解来使用Params类:
@FeignClient("demo")publicinterfaceDemoTemplate{@GetMapping(path ="/demo")StringdemoEndpoint(@SpringQueryMapParams params);}
如果您需要对生成的查询参数映射进行更多的控制,您可以实现一个自定义的QueryMapEncoder bean。
十一、HATEOAS 的支持
Spring提供了一些API来创建遵循 HATEOAS 原则的REST表示, Spring Hateoas 和 Spring Data REST。
如果你的项目使用 org.springframework.boot:spring-boot-starter-hateoas starter 或 org.springframework.boot:spring-boot-starter-data-rest starter,Feign HATEOAS 支持被默认启用。
当HATEOAS支持被启用时,Feign 客户端被允许序列化和反序列化 HATEOAS 表示模型: EntityModel、 CollectionModel 和 PagedModel.。
@FeignClient("demo")publicinterfaceDemoTemplate{@GetMapping(path ="/stores")CollectionModel<Store>getStores();}
十二、Spring @MatrixVariable 的支持
Spring Cloud OpenFeign提供对Spring @MatrixVariable 注解的支持。
如果一个 map 被作为方法参数传递,@MatrixVariable 的路径片段是通过用 = 连接 map 中的键值对来创建的。
如果传递了一个不同的对象,那么在 @MatrixVariable 注解中提供的 name(如果定义了的话)或者注解的变量名称将使用 = 与提供的方法参数结合起来。
尽管在服务器端,Spring 并不要求用户将路径段占位符的名称与 matrix variable 的名称相同,因为这在客户端太模糊了,Spring Cloud OpenFeign要求你添加一个路径段占位符,其名称要与 @MatrixVariable 注解(如果定义了)中提供的 name 或注解的变量名称相符。例如:
@GetMapping("/objects/links/{matrixVars}")Map<String,List<String>>getObjects(@MatrixVariableMap<String,List<String>> matrixVars);
注意,变量名和 path 段占位符都被称为 matrixVars。
@FeignClient("demo")publicinterfaceDemoTemplate{@GetMapping(path ="/stores")CollectionModel<Store>getStores();}
十三、FeignCollectionFormat的支持
我们通过提供 @CollectionFormat 注解来支持 feign.CollectionFormat。你可以通过传递所需的 feign.CollectionFormat 作为注解值,用它来注解一个 Feign 客户端方法(或整个类来影响所有方法)。
在下面的例子中,使用 CSV 格式而不是默认的 EXPLODED 来处理这个方法。
@FeignClient(name ="demo")protectedinterfaceDemoFeignClient{@CollectionFormat(feign.CollectionFormat.CSV)@GetMapping(path ="/test")ResponseEntityperformRequest(String test);}
十四、响应式的支持
由于 OpenFeign项目 目前不支持响应式客户端,如 Spring WebClient,Spring Cloud OpenFeign也不支持。一旦核心项目中可用,我们将在这里添加对它的支持。
初始化错误
根据你使用 Feign 客户端的方式,你可能会在启动你的应用程序时看到初始化错误。为了解决这个问题,你可以在自动连接客户端时使用一个 ObjectProvider。
@AutowiredObjectProvider<TestFeignClient> testFeignClient;
十五、Spring Data 的支持
如果 Jackson Databind 和 Spring Data Commons 在classpath上,org.springframework.data.domain.Page 和 org.springframework.data.domain.Sort 的 converter 将被自动添加。
要禁用这种行为,请设置:
spring.cloud.openfeign.autoconfiguration.jackson.enabled=false
详见 org.springframework.cloud.openfeign.FeignAutoConfiguration.FeignJacksonConfiguration。
十六、Spring@RefreshScope的支持
如果启用了Feign客户端刷新,每个Feign客户端的创建都有:
- feign.Request.Options 作为一个 refresh scope 的bean。这意味着诸如 connectTimeout 和 readTimeout 等属性可以针对任何Feign客户端实例进行刷新。
- 在 org.springframework.cloud.openfeign.RefreshableUrl 下包装的url。这意味着如果用 spring.cloud.openfeign.client.config.{feignName}.url 属性定义 Feign 客户端的URL,可以针对任何 Feign 客户端实例进行刷新。
你可以通过 POST /actuator/refresh 刷新这些属性。
默认情况下,Feign 客户端的刷新行为是禁用的。使用以下属性来启用刷新行为:
spring.cloud.openfeign.client.refresh-enabled=true
注意:不要在@FeignClient 接口上使用 @RefreshScope 注解
十七、支持向 Feign 客户端提供URL的方法
你可以通过以下任何一种方式向Feign客户端提供一个URL:
场景例子细节URL是在 @FeignClient 注解中提供的。@FeignClient(name=“testClient”, url=“http://localhost:8081”)URL是从注解的 url 属性中解析出来的,没有负载均衡。URL是在 @FeignClient 注解和配置属性中提供的。@FeignClient(name=“testClient”, url=“http://localhost:8081”) 和定义在 application.yml 中的属性 spring.cloud.openfeign.client.config.testClient.url=http://localhost:8081URL是从注解的 url 属性中解析出来的,没有负载均衡。在配置属性中提供的URL仍未使用。URL没有在 @FeignClient 注解中提供,而是在配置属性中提供。@FeignClient(name=“testClient”) 和定义在 application.yml 中的属性 spring.cloud.openfeign.client.config.testClient.url=http://localhost:8081URL 从配置属性中解析,没有负载均衡。如果 spring.cloud.openfeign.client.refresh-enabled=true,那么配置属性中定义的 URL 可以被刷新,如 Spring RefreshScope 的支持 中所述。在 @FeignClient 注解中和配置属性中都没有提供这个URL。@FeignClient(name=“testClient”)URL是从注解的 name 属性中解析出来的,具有负载均衡性。
十八、FeignClient的参数传递给服务提供方的方式
1、path路径上携带参数
/**
* 服务提供方:path路径上携带参数
*/@GetMapping("/test1/{myId}")publicStringtest1(@PathVariableString myId){System.out.println("LiveRoomController.test1");System.out.println(myId);return"success";}/**
* FeignClient:path路径上携带参数
*/@GetMapping("/test1/{myId}")Stringtest1(@PathVariable("myId")String myId);
2、单个简单数据类型
/**
* 服务提供方:单个简单数据类型
*/@GetMapping("/test2")publicStringtest2(String test2Str){System.out.println("LiveRoomController.test2");System.out.println(test2Str);return"success";}/**
* FeignClient:单个简单数据类型
*/@GetMapping("/test2")Stringtest2(@RequestParam("test2Str")String test2Str);
3、多个简单数据类型
/**
* 服务提供方:多个简单数据类型
*/@GetMapping("/test3")publicStringtest3(String test3Str1,String test3Str2){System.out.println("LiveRoomController.test3");System.out.println(test3Str1 +"||"+ test3Str2);return"success";}/**
* FeignClient:多个简单数据类型
*/@GetMapping("/test3")Stringtest3(@RequestParam("test3Str1")String test3Str1,@RequestParam("test3Str2")String test3Str2);
4、Path + 多个简单数据类型
/**
* 服务提供方:Path + 多个简单数据类型
*/@GetMapping("/test4/{myId}")publicStringtest4(@PathVariableString myId,String test4Str1,String test4Str2){System.out.println("LiveRoomController.test4");System.out.println(myId +"||"+ test4Str1 +"||"+ test4Str2);return"success";}/**
* FeignClient:Path + 多个简单数据类型
*/@GetMapping("/test4/{myId}")Stringtest4(@PathVariable("myId")String myId,@RequestParam("test4Str1")String test4Str1,@RequestParam("test4Str2")String test4Str2);
5、JavaBean对象
/**
* 服务提供方:JavaBean对象
*/@GetMapping("/test5/{myId}")publicStringtest5(@PathVariableString myId,Student student){System.out.println("LiveRoomController.test5");System.out.println(myId +"||"+ student);return"success";}/**
* FeignClient:JavaBean对象、Map
*/@GetMapping("/test5/{myId}")Stringtest5(@PathVariable("myId")String myId,Student student);
6、多path路径上携带参数
/**
* 服务提供方:多path路径上携带参数
*/@GetMapping("/test6/{myId}/test66/{myId2}")publicStringtest6(@PathVariable("myId")String myId,@PathVariable("myId2")String myId2){System.out.println("LiveRoomController.test6");System.out.println(myId +"||"+ myId2);return"success";}/**
* FeignClient:多path路径上携带参数
*/@GetMapping("/test6/{myId}/test66/{myId2}")Stringtest6(@PathVariable("myId")String myId,@PathVariable("myId2")String myId2);
7、post获取请求体
/**
* 服务提供方:post获取请求体
*/@PostMapping("/test7/{myId}")publicStringtest7(@PathVariable("myId")String myId,@RequestBodyStudent student){System.out.println("LiveRoomController.test7");System.out.println(myId +"||"+ student);return"success";}/**
* FeignClient:post获取请求体
*/@PostMapping("/test7/{myId}")Stringtest7(@PathVariable("myId")String myId,@RequestBodyStudent student);
测试一下吧
System.out.println(commonSurface.test1("this is test1"));System.out.println("-------------");System.out.println(commonSurface.test2("this is test2"));System.out.println("-------------");System.out.println(commonSurface.test3("this is test3","this is test3-2"));System.out.println("-------------");System.out.println(commonSurface.test4("this is myId","this is test4","this is test4-2"));System.out.println("-------------");Student s =newStudent();
s.setId(1);
s.setName("张三");System.out.println(commonSurface.test5("this is myId", s));System.out.println("-------------");System.out.println(commonSurface.test6("this is myId","this is myId2"));System.out.println("-------------");System.out.println(commonSurface.test7("this is myId", s));System.out.println("-------------");
十九、feign实践
demo-learn-feign
pom.xml
<?xml version="1.0" encoding="UTF-8"?><projectxmlns="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><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.9.RELEASE</version><relativePath/></parent><modelVersion>4.0.0</modelVersion><groupId>com.zzhua</groupId><artifactId>demo-learn-feign</artifactId><version>1.0-SNAPSHOT</version><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><java.version>1.8</java.version><spring-cloud.version>Hoxton.SR8</spring-cloud.version><alibaba.version>2.2.5.RELEASE</alibaba.version></properties><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><dependency><groupId>org.springframework.retry</groupId><artifactId>spring-retry</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-dependencies</artifactId><version>${alibaba.version}</version><type>pom</type><scope>import</scope></dependency><!-- springCloud --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
application.yml
server:port:8095spring:application:name: feign-service
cloud:nacos:server-addr: localhost:8848hystrix:command:default:circuitBreaker:# 触发熔断的最小请求次数,默认20 每10srequestVolumeThreshold:2# 熔断多少秒后再次去尝试请求sleepWindowInMilliseconds:10000# 触发熔断的失败请求最小占比,默认50%errorThresholdPercentage:10feign:hystrix:# 开启服务降级enabled:trueclient:config:# (配置文件的优先级高于代码中的配置)# 所有feign客户端的默认配置#default:# 全局设置连接超时时间#connectTimeout: 2000# 全局设置读取响应超时时间#readTimeout: 3000# 仅针对name为configN的feign客户端(优先级高于default配置)configN:# 设置日志级别, 但前提是feign客户端日志级别必须是debug才会打印出来loggerLevel: BASIC
# 设置读取响应超时时间, 超过5s将会抛出异常SocketTimeoutException: Read timed outreadTimeout:5000retryN:# 自定义错误解码器errorDecoder: com.zzhua.feign03.CustomErrorDecoder
# 设置重试器为Retryer.Default(默认的重试器), 或 Retryer.NEVER_RETRY(不重试)retryer: feign.Retryer.Default
# 开启重试条件retryable:true# 最多重试4次#maxAttempts: 4#backoff:# 开启退避算法#enabled: true# 初始重试间隔时间为1秒#delay: 1000# 最大重试间隔时间为5秒#maxDelay: 5000# 重试间隔时间按2的指数增长#multiplier: 2.0
logback.xml
<?xml version="1.0" encoding="UTF-8"?><configurationdebug="false"><!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径--><propertyname="LOG_BASE_PATH"value="logs/"/><propertyname="maxFileSize"value="5MB"/><propertyname="maxHistory"value="30"/><propertyname="commonPattern"value="%d{HH:mm:ss.SSS}-${PID}-[%thread] %-5level %logger{30} - %msg%n"/><!--控制台日志, 控制台输出 --><appendername="STDOUT"class="ch.qos.logback.core.ConsoleAppender"><encoderclass="ch.qos.logback.classic.encoder.PatternLayoutEncoder"><!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度,%msg:日志消息,%n是换行符--><pattern>${commonPattern}</pattern></encoder></appender><!--文件日志, 按照每天生成日志文件 --><appendername="FILE"class="ch.qos.logback.core.rolling.RollingFileAppender"><file>${LOG_BASE_PATH}/feignApp.log</file><rollingPolicyclass="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><!--日志文件输出的文件名--><FileNamePattern>${LOG_BASE_PATH}/%d/xxx.log.%d{yyyy-MM-dd}-%i.log</FileNamePattern><!--日志文件保留天数--><MaxHistory>${maxHistory}</MaxHistory><timeBasedFileNamingAndTriggeringPolicyclass="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"><maxFileSize>${maxFileSize}</maxFileSize></timeBasedFileNamingAndTriggeringPolicy></rollingPolicy><encoderclass="ch.qos.logback.classic.encoder.PatternLayoutEncoder"><!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符--><pattern>${commonPattern}</pattern></encoder></appender><!-- 在这里单独配置某个feign客户端, 让Feign能够打印日志, 至于打印多少日志, 就要设置Logger.Level了;
这里的name不能使用通配符哦, 但是可以设置为: com.zzhua.feign02, 它会将这个包下的所有类都设置为指定的日志级别
--><loggername="com.zzhua.feign02.ConfigFeignClient"level="DEBUG"additivity="false"><appender-refref="STDOUT"/></logger><loggername="com.zzhua.feign03.RetryFeignClient"level="DEBUG"additivity="false"><appender-refref="STDOUT"/></logger><!-- 日志输出级别 --><rootlevel="DEBUG"><appender-refref="STDOUT"/><appender-refref="FILE"/></root></configuration>
FeignApp
@SpringBootApplication@EnableFeignClientspublicclassFeignApp{publicstaticvoidmain(String[] args){SpringApplication.run(FeignApp.class, args);}}
feign01
IndexController
@RestControllerpublicclassIndexController{@AutowiredprivateStaticRemoteFeignClient remoteFeignClient;@GetMapping("findPerson")publicPersonfindPerson(){Person person = remoteFeignClient.findPerson("zzhua",26);return person;}@GetMapping("getPerson")publicPersongetPerson(){Person person = remoteFeignClient.getPerson(newPerson("zzhua",26,newAddress("CN","HN")));return person;}@GetMapping("getPerson1")publicPersongetPerson1(){Person person = remoteFeignClient.getPerson1("zzhua",26,newAddress("CN","HN"));return person;}@GetMapping("getPerson2")publicPersongetPerson2(){HashMap<String,Object> map =newHashMap<>();
map.put("name","zzhua");
map.put("age",26);
map.put("address",newAddress("CN","HN"));Person person = remoteFeignClient.getPerson2(map);return person;}@GetMapping("addPerson")publicPersonaddPerson(){Person person = remoteFeignClient.addPerson(newPerson("zzhua",26,newAddress("CN","HN")),2);return person;}@GetMapping("addPerson2")publicPersonaddPerson2(){Person person = remoteFeignClient.addPerson2(newPerson("zzhua",26,newAddress("CN","HN")),newAddress("CN2","HN2"));return person;}@GetMapping("checkPerson")publicPersoncheckPerson(){return remoteFeignClient.checkPerson(newPerson("zzhua",26,newAddress("CN","HN")),newInteger[]{1,2,3});}@GetMapping("checkPerson2")publicPersoncheckPerson2(){return remoteFeignClient.checkPerson2(newPerson("zzhua",26,newAddress("CN","HN")),Arrays.asList(1,2,3));}}
StaticRemoteFeignClient
/* 重点是测试参数写法 */@FeignClient(name ="remote",url ="http://localhost:8084",path ="/orderService/remote")publicinterfaceStaticRemoteFeignClient{// 接口提供方: http://localhost:8084/orderService/remote/findPerson?pName=zzhua&pAge=26 能通@GetMapping("findPerson")// 多于1个参数,则必须写@RequestParam注解(并且必须写value)PersonfindPerson(@RequestParam(name ="pName")String name,@RequestParam(name ="pAge")Integer age);@GetMapping("getPerson")// @RequestParam后面是自定义参数类型将不会封装到接口的方法参数中PersongetPerson(@RequestParam("person")Person person);@GetMapping("getPerson1")// feign将会把url拼接成url?name=xx&age=yy&address=zz(address字符串形式)// 这个address将会导致接口那边在获取address时,不能正常封装成Address对象而导致报错PersongetPerson1(@RequestParam(name ="name")String name,@RequestParam(name ="age")Integer age
,@RequestParam(name ="address")Address address);@PostMapping("getPerson2")// 可以使用Map封装(远程接口使用@RequestBody Map来接收(address属性能正常接收到))PersongetPerson2(Map<String,Object> map);@PostMapping("addPerson")// 不能使用超过1个@RequestBodyPersonaddPerson(@RequestBodyPerson person,@RequestParam("pAge")Integer age);@PostMapping("addPerson2")// @RequestParam后面是自定义参数Address将不会封装到接口的方法参数中PersonaddPerson2(@RequestBodyPerson person,@RequestParam("addr")Address addr);@PostMapping("checkPerson")// 多于1个参数,则必须写@RequestParam注解(并且必须写value)// feign拼接url?ids=1%2C2%2C3 (%2C,即逗号)PersoncheckPerson(@RequestBodyPerson person,@RequestParam("ids")Integer[] ids);@PostMapping("checkPerson2")// 多于1个参数,则必须写@RequestParam注解(并且必须写value)// feign拼接url?ids=1&ids=2&ids=3PersoncheckPerson2(@RequestBodyPerson person,@RequestParam("ids")List<Integer> ids);}
Person
@Data@ToString@AllArgsConstructor@NoArgsConstructorpublicclassPerson{privateString name;privateInteger age;privateAddress address;}
Address
@Data@NoArgsConstructor@AllArgsConstructorpublicclassAddress{privateString country;privateString province;}
feign02
ConfigController
@RestControllerpublicclassConfigController{@AutowiredprivateConfigFeignClient configFeignClient;// 测试日志配置// (需要先配置对应的feign客户端的日志器的日志级别为DEBUG,// 然后再通过java代码的方式配置1个Logger.Level的bean到@FeignClient注解指定的配置类中即可,// 或者通过配置文件的方式配置:// 在配置文件中配置feign.client.config.{feignName}.loggerLevel: BASIC// 或者在配置文件中配置feign.client.config.default.loggerLevel: BASIC(全局))@GetMapping("testLog")publicObjecttestLog(){return configFeignClient.testLog();}// 测试响应超时配置// (在配置文件中配置: 可以全局配置, 也可以给单个指定的feign客户端配置;// 在代码中配置: 可以给全局配置, 也可以给单个指定的feign客户端配置;// 在代码中给feign接口中的单个方法指定超时时间(如下面的testTimeout2方法所示);// 当超过指定的时间还没有返回时, 就会抛出异常;// 默认情况下(在配置文件和代码中都不配置超时时间), 默认的读取响应超时时间为60秒, 连接超时时间为10s,// 也就是说, 如果发起响应后, 60秒之内还没有给出响应, 就会抛出异常了;)@GetMapping("testTimeout")publicObjecttestTimeout(Long sec){return configFeignClient.testTimeout(sec);}@GetMapping("testTimeout2")publicObjecttestTimeout2(Long sec){Request.Options options =newRequest.Options(2,TimeUnit.SECONDS,3,TimeUnit.SECONDS,true);return configFeignClient.testTimeout2(options, sec);}}
ConfigFeignClient
/* 重点是验证各种配置 */@FeignClient(name ="configN", url ="http://localhost:8084", path ="/orderService/config",
configuration ={ConfigFeignClientConfig.class})publicinterfaceConfigFeignClient{// 访问: http://localhost:8084/orderService/config/testLog 能通@GetMapping("/testLog")StringtestLog();// 访问: http://localhost:8084/orderService/config/testTimeout?sec=1 能通@GetMapping("/testTimeout")StringtestTimeout(@RequestParam("sec")Long sec);@GetMapping("/testTimeout")StringtestTimeout2(Request.Options options,@RequestParam("sec")Long sec);}
ConfigFeignClientConfig
publicclassConfigFeignClientConf{/* 配置feign客户端打印日志的注意点:
1. feign客户端只有在配置feign客户端全类名对应的日志级别为debug的时候, 才有可能输出日志
2. 我们需要在配置类中如下定义1个Logger.Level的bean即可。
或者, 在配置文件中配置feign.client.config.{feignName}.loggerLevel: BASIC
或者, 在配置文件中配置feign.client.config.{feignName}.loggerLevel: BASIC(全局)
*/@BeanLogger.LevelfeignLoggerLevel(){// return Logger.Level.NONE;// return Logger.Level.BASIC;// return Logger.Level.HEADERS;returnLogger.Level.FULL;}/* 也可以通过代码的方式配置超时时间 *//*@Bean
public Request.Options requestOptions() {
return new Request.Options(2, TimeUnit.SECONDS, 3, TimeUnit.SECONDS, true);
}*/}
feign03
RetryController
@RestControllerpublicclassRetryController{@AutowiredprivateRetryFeignClient retryFeignClient;// 测试重试机制(默认不会重试)// Feign的重试机制主要包括以下几个方面:// 1. 配置重试次数和重试间隔时间// 2. 配置重试条件和重试策略// 3. 实现重试回退机制(这个不包括当前示例中)// 什么情况下会重试?// 默认情况下只有超时的情况下才会重试, 而如果被调用方抛出了其它异常, 则不会重试// (其实是否重试, 不是由被调用方来决定的, 而是由调用方来确定的;// 【默认情况下超时会引起重试】: 发起feign调用后, SynchronousMethodHandler#executeAndDecode 执行请求时,// 会由于被调用方一直没有响应而抛出IO异常,捕获到IO异常后,转而抛出 RetryException,// 抛出的RetryException在 SynchronousMethodHandler#invoke 的while(true)循环中又被捕捉到,从而交给retryer继续重试;// 【被调用方抛出其它异常不会引起重试】: 发起feign调用后, SynchronousMethodHandler#executeAndDecode执行请求时,// 会获得响应对象, 并将此响应对象交给 AsyncResponseHandler#handleResponse处理, 其中会根据响应状态码作不同处理,// 如果状态码是200到300之间, 那么正常处理, 如果不是, 则会交给ErrorDecoder处理返回1个异常, 然后以异常结束, 然后返回到// SynchronousMethodHandler#invoke 的while(true)循环中, 如果这个异常是 RetryException, 则交给retryer继续重试, 如果不是, 则跳出while循环;// 【因此, 我们如果要重试就需要自己实现ErrorDecoder, 在需要重试的时候, 返回1个RetryException即可】)// 如何配置重试?// 可以通过配置文件的方式配置errorDecoder和retryer;// 可以通过代码的方式配置1个errorDecoder的bean和1个Retryer的bean(这里的retryer可以配置重试次数)// (使用配置文件配置的无效backoff无效、使用@Retryable注解配置的也无效, 后面再看)@GetMapping("testRetry")publicObjecttestRetry(Integer flag,Integer sec){return retryFeignClient.testRetry(flag, sec);}@GetMapping("testRetry2")publicObjecttestRetry2(Integer flag,Integer sec){return retryFeignClient.testRetry2(flag, sec);}}
RetryFeignClient
@FeignClient(name ="retryN", url ="http://localhost:8084", path ="/orderService/config",
configuration ={RetryConfig.class})publicinterfaceRetryFeignClient{// 访问: http://localhost:8084/orderService/config/testRetry?flag=1 能通@GetMapping("/testRetry")StringtestRetry(@RequestParam("flag")Integer flag,@RequestParam("sec")Integer sec);// 访问: http://localhost:8084/orderService/config/testRetry?flag=1 能通@GetMapping("/testRetry")// @Retryable(value = {IOException.class}, maxAttempts = 3, backoff = @Backoff(delay = 1000, maxDelay = 5000, multiplier = 2))StringtestRetry2(@RequestParam("flag")Integer flag,@RequestParam("sec")Integer sec);}
RetryConfig
publicclassRetryConfig{@BeanLogger.LevelfeignLoggerLevel(){// return Logger.Level.NONE;returnLogger.Level.BASIC;// return Logger.Level.HEADERS;// return Logger.Level.FULL;}/* 设置5s的超时时间, 以测定什么情况下会重试 */@BeanpublicRequest.OptionsrequestOptions(){returnnewRequest.Options(2,TimeUnit.SECONDS,5,TimeUnit.SECONDS,true);}/*
使用 Retryer.Default 类生成一个默认的重试器,
它会在当前请求失败后重试 3 次,并会在第一次重试前等待 500 毫秒,
在第二次重试前等待 1000 毫秒,第三次重试前等待 2000 毫秒,以此类推
*//*@Bean
public Retryer retryer() {
return new Retryer.Default(500, 5000, 3); // 最多请求3次
}*//*@Bean
public CustomErrorDecoder customErrorDecoder() {
return new CustomErrorDecoder();
}*/}
CustomErrorDecoder
@Slf4jpublicclassCustomErrorDecoderimplementsErrorDecoder{@OverridepublicExceptiondecode(String methodKey,Response response){if(response.status()==500){
log.info("重试一波...");// 认为有必要重试就返回RetryableExceptionreturnnewRetryableException(500,"",
response.request().httpMethod(),null,
response.request());}
log.info("认为没必要重试...");returnnewRuntimeException("Unknown error");}}
feign04
FallbackController
@RestControllerpublicclassFallbackController{@AutowiredprivateFallbackFeignClient fallbackFeignClient;// 测试降级、熔断// 需要配置feign.hystrix.enabled: true, 来开启hystrix, 否则被调用方抛出异常后, 不会走指定的降级逻辑// 访问1: http://localhost:8095/testFallback?flag=1&sec=0 正常返回 "ok"// 访问2: http://localhost:8095/testFallback?flag=-1&sec=0 首先发1个请求给被调用方, 返回500状态码, 然后走降级逻辑 正常返回 "ojdk"// 测试: 当在浏览器上多次快速点击访问2时, 刚开始, 请求会发起远程调用, 当多次后就会直接熔断, 然后直接走降级的方法, 多次都熔断之后, 我再访问1, 也是直接熔断走了降级方法。// 但是熔断结束的时间很短, 稍微停一下, 就会发起远程调用。熔断的目的是为了保护下游服务, 在统计到下游服务多次响应失败后, 就会熔断一段时间, 等过会儿再去访问。// 我们可以通过 hystrix.command.default.circuitBreaker.xxx来配置熔断相关的东西(如配置文件中所示)// 降级:// 可以使用@FeignClient的fallback来直接指定降级的类, 必须定义为bean且实现feign接口// 也可以使用@FeignClient的fallbackFactory来直接指定降级的工厂类, 必须定义为bean且实现FallbackFactory, 并且在重写的方法中返回feign接口的实现类, 好处是可以拿到发生错误的异常// fallback与fallbackFactory之间只能使用1个@GetMapping("testFallback")publicObjecttestFallback(Integer flag,Integer sec){return fallbackFeignClient.testFallback(flag, sec);}}
FallbackFeignClient
@FeignClient(name ="fallbackN", url ="http://localhost:8084", path ="/orderService/config",
configuration ={FallbackConfig.class},fallback =FallbackFeignClientImpl.class// ,fallbackFactory = FallbackFeignClientFactory.class)publicinterfaceFallbackFeignClient{// 访问: http://localhost:8084/orderService/config/testFallback?flag=1 能通@GetMapping("/testFallback")StringtestFallback(@RequestParam("flag")Integer flag,@RequestParam("sec")Integer sec);}
FallbackConfig
publicclassFallbackConfig{@BeanLogger.LevelfeignLoggerLevel(){// return Logger.Level.NONE;// return Logger.Level.BASIC;// return Logger.Level.HEADERS;returnLogger.Level.FULL;}}
FallbackFeignClientImpl
// 容器中同时存在FallbackFeignClient接口实现的2个bean, 其中1个是feign的动态代理, 1个就是当前这个类// 而feign的动态代理具有primary, 所以注入时, 会优先@Slf4j@ComponentpublicclassFallbackFeignClientImplimplementsFallbackFeignClient{@OverridepublicStringtestFallback(Integer flag,Integer sec){
log.info("降级使用fallback...");return"ojdk";}}
FallbackFeignClientFactory
@Slf4j@ComponentpublicclassFallbackFeignClientFactoryimplementsFallbackFactory<FallbackFeignClient>{@OverridepublicFallbackFeignClientcreate(Throwable throwable){
log.info("获取到异常: {}", throwable.toString());returnnewFallbackFeignClient(){@OverridepublicStringtestFallback(Integer flag,Integer sec){
log.info("FallbackFeignClientFactory$1处理异常了...");return"nook";}};}}
feign05
LbController
@RestControllerpublicclassLbController{@AutowiredprivateLbFeignClient lbFeignClient;// 测试负载均衡// 访问: http://localhost:8095/testLb,// 当没有服务时, 直接走的降级;// 当启动1个order-service服务时, 立即访问, 仍然走的是降级, 过了一小段时间后, 成功调用到服务;// 当再启动1个order-service服务时, 立即访问, 仍然调用的是第1个服务, 过了一小段时间后, 才会轮询访问;// 当其中1个服务挂掉的时候, 还是会走降级逻辑(一直走降级方法), 过了一小段时间后, 一直成功调用到还存活的服务// (这个是开启了feign.hystrix.enabled: true时的现象, 大概是因为只要有1个服务挂了, 就认为服务不可用, 然后直接熔断走的降级。// 然后, 又试着关闭feign.hystrix.enabled: false, 再测试一遍, 负载均衡访问后, 关闭1个order-service, 在轮询到关闭的服务时, 稍微会等待一小会儿,// 就把请求打到了还存活的服务上。);@GetMapping("testLb")publicObjecttestLb(){return lbFeignClient.testLb();}// 测试降级、熔断// 需要配置feign.hystrix.enabled: true, 来开启hystrix, 否则被调用方抛出异常后, 不会走指定的降级逻辑// 访问1: http://localhost:8095/testLb2?flag=1&sec=0 正常// 访问2: http://localhost:8095/testLb2?flag=-1&sec=0 故意报错// 行为同FallbackController中一致@GetMapping("testLb2")publicObjecttestLb2(Integer flag,Integer sec){return lbFeignClient.testLb2(flag, sec);}}
LbFeignClient
@FeignClient(name ="order-service", path ="/orderService/config",
fallback =LbFeignClientImpl.class,
configuration =LbConfig.class)publicinterfaceLbFeignClient{@GetMapping("testLb")StringtestLb();@GetMapping("testLb2")StringtestLb2(@RequestParam("flag")Integer flag,@RequestParam("sec")Integer sec);}
LbConfig
publicclassLbConfig{@BeanLogger.LevelfeignLoggerLevel(){// return Logger.Level.NONE;returnLogger.Level.BASIC;// return Logger.Level.HEADERS;// return Logger.Level.FULL;}}
LbFeignClientImpl
@Slf4j@ComponentpublicclassLbFeignClientImplimplementsLbFeignClient{@OverridepublicStringtestLb(){
log.info("lb2降级...");return"lb~ok-降级";}@OverridepublicStringtestLb2(Integer flag,Integer sec){
log.info("lb2降级...");return"lb2~ok-降级";}}
order-service
pom.xml
<?xml version="1.0" encoding="UTF-8"?><projectxmlns="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><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.9.RELEASE</version><relativePath/></parent><modelVersion>4.0.0</modelVersion><groupId>com.zzhua</groupId><artifactId>order-service</artifactId><version>1.0-SNAPSHOT</version><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><java.version>1.8</java.version><spring-cloud.version>Hoxton.SR8</spring-cloud.version><alibaba.version>2.2.5.RELEASE</alibaba.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-dependencies</artifactId><version>${alibaba.version}</version><type>pom</type><scope>import</scope></dependency><!-- springCloud --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
application.yml
server:port:8084servlet:context-path: /orderService
spring:application:name: order-service
cloud:nacos:server-addr: localhost:8848
RemoteController
@RestController@RequestMapping("remote")publicclassRemoteController{@GetMapping("findPerson")PersonfindPerson(String pName,Integer pAge){returnnewPerson(pName, pAge,newAddress("CN","HN"));}@GetMapping("getPerson")PersongetPerson(Person person){System.out.println(person);return person;}@PostMapping("getPerson2")PersongetPerson2(@RequestBodyMap<String,Object> cMap){System.out.println(cMap);returnnewPerson();}@PostMapping("addPerson")PersonaddPerson(@RequestBodyPerson person,Integer pAge){System.out.println(person);System.out.println(pAge);if(person.getAge()!=null){
person.setAge( person.getAge()+1);}return person;}@PostMapping("addPerson2")PersonaddPerson2(@RequestBodyPerson person,Address addr){System.out.println(person);System.out.println(addr);if(person !=null){
person.setAddress(addr);}return person;}@PostMapping("checkPerson")PersoncheckPerson(@RequestBodyPerson person,Integer[] ids){System.out.println(person);System.out.println(StringUtils.arrayToCommaDelimitedString(ids));return person;}@PostMapping("checkPerson2")// List<Integer> ids必须要带@RequestParam注解才能接收到PersoncheckPerson2(@RequestBodyPerson person,@RequestParam("ids")List<Integer> ids){System.out.println(person);System.out.println(ids);return person;}}
ConfigController
@Slf4j@RestController@RequestMapping("config")publicclassConfigController{@GetMapping("testLog")StringtestLog(){return"ok";}@GetMapping("/testTimeout")StringtestTimeout(Long sec){try{TimeUnit.SECONDS.sleep(sec);}catch(InterruptedException e){
e.printStackTrace();}return"ok";}@GetMapping("/testRetry")StringtestRetry(Integer flag,@RequestParam(required =false, defaultValue ="0")Integer sec)throwsException{
log.info("testRetry请求: {}", flag);if(sec ==0){if(flag >=0){return"ok";}elseif(Objects.equals(flag,-1)){thrownewRuntimeException("flag为-1");}elseif(Objects.equals(flag,-2)){thrownewNullPointerException("flag为-2");}elseif(Objects.equals(flag,-3)){thrownewIOException("flag为-3");}thrownewIndexOutOfBoundsException("越界异常");}else{TimeUnit.SECONDS.sleep(sec);return"ok2";}}@GetMapping("/testFallback")StringtestFallback(Integer flag,@RequestParam(required =false, defaultValue ="0")Integer sec)throwsException{
log.info("testFallback请求: {}", flag);if(sec ==0){if(flag >=0){return"ok";}elseif(Objects.equals(flag,-1)){thrownewRuntimeException("flag为-1");}elseif(Objects.equals(flag,-2)){thrownewNullPointerException("flag为-2");}elseif(Objects.equals(flag,-3)){thrownewIOException("flag为-3");}thrownewIndexOutOfBoundsException("越界异常");}else{TimeUnit.SECONDS.sleep(sec);return"ok2";}}@Value("${lb.name:unknown}")privateString lbName;@GetMapping("testLb")StringtestLb(){return"loadBalance~"+ lbName;}@GetMapping("/testLb2")StringtestLb2(Integer flag,@RequestParam(required =false, defaultValue ="0")Integer sec)throwsException{
log.info("testLb2请求: {}", flag);if(sec ==0){if(flag >=0){return"ok";}elseif(Objects.equals(flag,-1)){thrownewRuntimeException("flag为-1");}elseif(Objects.equals(flag,-2)){thrownewNullPointerException("flag为-2");}elseif(Objects.equals(flag,-3)){thrownewIOException("flag为-3");}thrownewIndexOutOfBoundsException("越界异常");}else{TimeUnit.SECONDS.sleep(sec);return"ok2";}}}
OrderApp
@SpringBootApplicationpublicclassOrderApp{publicstaticvoidmain(String[] args){SpringApplication.run(OrderApp.class, args);}}
测试
启动nacos,再启动order-service,再启动demo-learn-feign即可
版权归原作者 ps酷教程 所有, 如有侵权,请联系我们删除。