简介
为什么要用fallback
Feign默认认为非2XX都是异常。对于404这种非常容易抛出的业务异常来说,没两下就circuit break(断路)了。
可降级设计中,希望调用某个服务失败的时候能够让流程走下去而非抛异常,但是又不希望每个调用的地方加try catch,这个时候可以用Feign的Fallback。
但是,这种情况很少见。一般直接全局异常处理的。
hystrix配置
fallback是hystrix的功能,所以必须开启hystrix(默认是关闭的)。
共存问题
fallback比fallbackfactory优先级高。若都存在,会走fallback调用。
防止hystrix直接调用fallback
有时候可能feign直接到了fallback中,原因如下:
Hystrix默认的超时时间是1秒,如果超过这个时间尚未响应,将会进入fallback代码。而首次请求往往会比较慢(由于Ribbon是懒加载的,在首次请求时,才会开始初始化相关类),这个响应时间可能就大于1秒了。
解决方法
法1:Ribbon配置饥饿加载(最佳推荐)
饥饿加载:即在启动的时候便加载所有配置项的应用程序上下文。
ribbon: # 饥饿加载 eager-load: # 是否开启饥饿加载 enabled: true # 饥饿加载的服务 clients: demo-goods,demo-product
法2:调用端配置
eureka.client.fetch-registy: true
Fallback
说明
Fallback只能覆写方法,但无法捕获异常(获取不到HTTP请求错误状态码和信息)。
HystrixTargeter.targetWithFallback方法实现了@FeignClient.fallback处理逻辑,通过源码可以知道UserFeignFallback回调类是从Spring容器中获取的,所以UserFeignFallback由spring创建。
UserFeignClient
package com.example.product.feign; import com.example.product.entity.User; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @FeignClient(name = "user", fallback = UserFeignClientFallback.class) public interface UserFeignClient { @GetMapping("/user/{id}") public User getUser(@PathVariable("id") Long id); }
UserFeignFallback
package com.example.feign; import org.springframework.stereotype.Component; @Component public class UserFeignClientFallback implements UserFeignClient { @Override public User getUserByID(String id) { User user = new User(); user.setId(-1); return user; } }
ProductController
package com.example.product.controller; import com.example.product.entity.User; import com.example.product.feign.UserFeignClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/product") public class ProductController { @Autowired UserFeignClient userFeignService; @GetMapping("/feign") public User testFeign(){ User user = userFeignService.getUser(1L);; return user; } }
UserController
package com.example.user.controller; import com.example.user.entity.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.logging.Logger; @RestController @RequestMapping("/user") public class UserController { @GetMapping("/{id}") public User getUser(@PathVariable("id") Long id) { User user = new User(); user.setId(id); user.setUserName("user_name_" + id); user.setAge(20); return user; } }
postman访问: http://localhost:9001/product/feign
正常结果:
{ "id": 1, "userName": "user_name_1", "age": 20 }
将User服务直接关掉:结果
postman结果:
{ "id": -1, "userName": null, "age": null }
FallbackFactory
简介
上面的实现方式简单,但无法捕获异常(获取不到HTTP请求错误状态码和信息) ,这时可用工厂模式来实现Fallback
UserFeignClient
package com.example.product.feign; import com.example.product.entity.User; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @FeignClient(name = "user", fallbackFactory = UserFeignClientFallbackFactory.class) public interface UserFeignClient { @GetMapping("/user/{id}") public User getUser(@PathVariable("id") Long id); }
UserFeignClientFallbackFactory
package com.example.product.feign; import com.example.product.entity.User; import feign.hystrix.FallbackFactory; import org.springframework.stereotype.Component; @Component public class UserFeignClientFallbackFactory implements FallbackFactory<UserFeignClient> { @Override public UserFeignClient create(Throwable throwable) { System.out.println("fallback throwable: " + throwable); System.out.println("fallback message: " + throwable.getMessage()); return new UserFeignClient() { @Override public User getUser(Long id) { User user = new User(); user.setId(-1L); return user; } }; } }
ProductController
package com.example.product.controller; import com.example.product.entity.User; import com.example.product.feign.UserFeignClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/product") public class ProductController { @Autowired UserFeignClient userFeignService; @GetMapping("/feign") public User testFeign(){ User user = userFeignService.getUser(1L);; return user; } }
UserController
package com.example.user.controller; import com.example.user.entity.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.logging.Logger; @RestController @RequestMapping("/user") public class UserController { @GetMapping("/{id}") public User getUser(@PathVariable("id") Long id) { User user = new User(); user.setId(id); user.setUserName("user_name_" + id); user.setAge(20); return user; } }
postman访问: http://localhost:9001/product/feign
正常结果:
{ "id": 1, "userName": "user_name_1", "age": 20 }
将User服务直接关掉:结果
postman结果:
{ "id": -1, "userName": null, "age": null }
后端打印
fallback throwable: java.lang.RuntimeException: com.netflix.client.ClientException: Load balancer does not have available server for client: user fallback message: com.netflix.client.ClientException: Load balancer does not have available server for client: user
ErrorDecoder接口处理请求错误信息,ErrorDecoder.Default这个默认实现抛出FeignException异常。
FeignException.status 方法返回HTTP状态码,FallbackFactory.create默认情况下可以强制转换成FeignException异常这样就可以获取到HTTP状态码了。
自定义ErrorDecoder
ErrorDecoder与fallback的执行顺序:先走ErrorDecoder拦截器,再走熔断的fallback。
@FeignClient加上decode404 = true这一个参数,Feign对于2XX和404 ,都不会走Fallback了。
排除404,已经基本上够用了,如果想把409、400等status也加到例外中,可以重写一下Feign的errorDecoder。
方案1.解析错误
package com.example.product.config; import feign.Response; import feign.Util; import feign.codec.ErrorDecoder; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; import java.io.IOException; @Component public class KeepErrMsgConfiguration implements ErrorDecoder { @Override public Exception decode(String methodKey, Response response) { System.out.println(response.status()); if (response.status() != HttpStatus.OK.value()) { try { // 原始的返回内容 String json = Util.toString(response.body().asReader()); System.out.println(json); return new RuntimeException("try中的异常"); } catch (IOException e) { return new RuntimeException("cat中的异常"); } } return new RuntimeException("if外的异常"); } }
方案2. 不熔断,把异常原样往外抛。
package com.example.product.config; import com.netflix.hystrix.exception.HystrixBadRequestException; import feign.Response; import feign.Util; import feign.codec.ErrorDecoder; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; import java.io.IOException; @Component public class NotBreakerConfiguration implements ErrorDecoder { @Override public Exception decode(String methodKey, Response response) { System.out.println(response.status()); Exception exception = null; if (response.status() != HttpStatus.OK.value()) { try { // 获取原始的返回内容(来自GlobalExceptionHandler类中的返回信息) String json = Util.toString(response.body().asReader()); System.out.println(json); exception = new HystrixBadRequestException("if中的"); } catch (IOException e) { exception = new HystrixBadRequestException(e.getMessage()); } } return exception; } }
配置方法
配置方法1:@Component
如下边所示,注入到容器即可。
package com.example.product.config; import com.netflix.hystrix.exception.HystrixBadRequestException; import feign.Response; import feign.Util; import feign.codec.ErrorDecoder; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; import java.io.IOException; @Component public class NotBreakerConfiguration implements ErrorDecoder { @Override public Exception decode(String methodKey, Response response) { System.out.println(response.status()); Exception exception = null; if (response.status() != HttpStatus.OK.value()) { try { // 获取原始的返回内容(来自GlobalExceptionHandler类中的返回信息) String json = Util.toString(response.body().asReader()); System.out.println(json); exception = new HystrixBadRequestException("if中的"); } catch (IOException e) { exception = new HystrixBadRequestException(e.getMessage()); } } return exception; } }
配置方法2:@EnableFeignClients
全局配置@EnableFeignClients注解。
(NotBreakerConfiguration要去掉@Component)。
package com.example; import com.example.feign.FeignClientsConfig; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.feign.EnableFeignClients; @EnableFeignClients( defaultConfiguration = NotBreakerConfiguration.class ) @SpringBootApplication public class FeignApplication { public static void main(String[] args) { SpringApplication.run(FeignApplication.class, args); } }
配置方法3:@FeignClient
@FeignClient注解。作用范围是Feign接口,优先级要高于上面两种。
(NotBreakerConfiguration要去掉@Component)。
package com.example.feign; import org.springframework.cloud.netflix.feign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import java.util.List; @FeignClient(name = "user", url = "${user.url}", decode404 = true, fallbackFactory = UserFeignFactory.class, configuration = NotBreakerConfiguration.class ) public interface UserFeign { @PostMapping void save(User user); @GetMapping("/{id}") User getUserByID(@PathVariable("id") String id); }
请先
!