首先还是把demo跑起来:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
@SpringBootApplication
@EnableFeignClients//添加这个注解
public class FeignBootApplication {
public static void main(String[] args) {
SpringApplication.run(FeignBootApplication.class, args);
}
}
//这是feign接口
@FeignClient(name="ServerDemoFeignClient",
url="http://localhost:8080")
public interface ServerDemoFeignClient {
@GetMapping("/server/demo/hello/{username}")
public String hello(@PathVariable("username")String username,
@RequestParam("msg") String msg);
}
//这是服务端接口
@RestController
@RequestMapping("/server/demo")
public class ServerDemoController {
@GetMapping("/hello/{username}")
public String hello(@PathVariable("username")String username,
@RequestParam("msg")String msg){
return "hello:" + username+",say:"+ msg;
}
}
需要注意的是:
1.在feign接口里面用的注解已经全都替换成了Web的注解,而不再是feign原生的注解,@SpringQueryMap用来代替@QueryMap完成url传参。
2.@FeignClient必须得有个名字,可以用name、value来指定。这个名字是用来创建负载均衡的时候用的。
3.url和name都可以用占位符的方式来使用,比如:@FeignClient(name = “ f e i g n . n a m e " , u r l = " {feign.name}", url = " feign.name",url="{feign.url}”)
4.feign接口对应的bean在spring容器中的名字是这个类的全路径名,也可以设置qualifier来设置bean的别名。
因为feign接口使用的注解和web使用的是同样的注解,这样更方便抽出父接口,让服务端controller和feign接口同时继承父接口即可,比如:
先抽一个父接口出来:
@RequestMapping("/server/demo2")
public interface DemoApi {
@GetMapping("/hello/{username}")
public String hello(@PathVariable("username") String username,
@RequestParam("msg") String msg);
}
父接口中定义了完整的请求路径和请求参数。
feign客户端:
@FeignClient(name="ServerDemoFeignClient2",
url="http://localhost:8080")
public interface ServerDemoFeignClient2 extends DemoApi {
}
客户端需要继承这个接口,但是里面的代码却很干净,而且不再需要去指定url信息。
服务提供者server端:
@RestController
public class ServerDemoController2 implements DemoApi {
public String hello(String username, String msg){
return "hello:" + username+",say:"+ msg;
}
}
server端也不再需要指定url路径信息和web相关的注解。
@EnableFeignClients(defaultConfiguration=FooConfiguration.class)或者@FeignClient(configuration = FooConfiguration.class)都可以使用configuration覆盖默认的一些配置,比如自定义encoder、decoder、contract等等,defaultConfiguration是对所有的feign客户端都起作用,而configuration只对当前的feign客户端起作用,可以参考:FeignClientsConfiguration。
spring-cloud-starter-openfeign默认已经配置了:
Decoder: ResponseEntityDecoder
Encoder: SpringEncoder
Logger: Slf4jLogger
Contract: SpringMvcContract
Feign.Builder: HystrixFeign.Builder
Client feignClient: 如果有Ribbon那么就是LoadBalancerFeignClient,否则如果有FeignBlockingLoadBalancerClient那么就是用这个,否则使用默认的feign client。
这也是为什么我们可以直接在接口参数和返回值上使用对象而不用手动去定义encoder和decoder,并且为啥可以直接使用web里面的注解而不是使用feign原生的注解,都是这些默认的配置起了作用。
spring-cloud-starter-openfeign默认没有配置哪些bean呢:
Logger.Level
Retryer
ErrorDecoder
Request.Options
Collection<RequestInterceptor>
SetterFactory
QueryMapEncoder
但是,如果你配置了,框架也会帮你加载使用。
比如,我们要自定义一个拦截器:
@Configuration
public class FooConfiguration {
@Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
return new BasicAuthRequestInterceptor("user", "password");
}
}
当然也可以在配置文件配置:
feign:
client:
config:
feignName:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: full
errorDecoder: com.example.SimpleErrorDecoder
retryer: com.example.SimpleRetryer
requestInterceptors:
- com.example.FooRequestInterceptor
- com.example.BarRequestInterceptor
decode404: false
encoder: com.example.SimpleEncoder
decoder: com.example.SimpleDecoder
contract: com.example.SimpleContract
feignName的规则是conetxtId>value>name>serviceId。
如果是配置全局默认的配置,只需要指定名字是default就可以,比如:
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: basic
注意:
(1)如果同时在配置文件 和 @Bean都配置了,那么配置文件的优先级更高,当然可以设置这个feign.client.default-to-properties属性为false,那么@Bean的优先级会更高。
(2)configuration 的类上不需要添加@Configuration注解,否则这里面的配置的encoder、decoder等bean就变成默认配置的bean了。
feign客户端的ApplicationContext
每一个feign接口客户端在Spring容器里面都对应了一个单独的ApplicationContext,这些配置都是配置在这个context里面。因此如果想给同样的feign客户端设置不同的配置,可以给他们设置不同的context id,这样,这些配置就在不同的ApplicationContext中,不会造成冲突,比如:
@FeignClient(contextId = "fooClient", name = "stores",
configuration = FooConfiguration.class)
public interface FooClient {
}
@FeignClient(contextId = "barClient", name = "stores",
configuration = BarConfiguration.class)
public interface BarClient {
}
name一样,因此可以访问同样的服务器,但是contextId不一样,因此里面的配置可以不一样,因为是不同的ApplicationContext。Spring里面的ApplicationContext是存在父子结构的继承关系的,如果不想继承父Context,可以:
@Configuration
public class CustomConfiguration{
@Bean
public FeignClientConfigurer feignClientConfigurer() {
return new FeignClientConfigurer() {
@Override
public boolean inheritParentConfiguration() {
return false;
}
};
}
}
如果自定义配置还是无法满足你的需求,那就只能手动创建feign的客户端了,比如:
@Import(FeignClientsConfiguration.class)
class FooController {
private FooClient fooClient;
private FooClient adminClient;
@Autowired
public FooController(Decoder decoder, Encoder encoder, Client client, Contract contract) {
this.fooClient = Feign.builder().client(client)
.encoder(encoder)
.decoder(decoder)
.contract(contract)
.requestInterceptor(new BasicAuthRequestInterceptor("user", "user"))
.target(FooClient.class, "https://PROD-SVC");
this.adminClient = Feign.builder().client(client)
.encoder(encoder)
.decoder(decoder)
.contract(contract)
.requestInterceptor(new BasicAuthRequestInterceptor("admin", "admin"))
.target(FooClient.class, "https://PROD-SVC");
}
}
注意:@Import(FeignClientsConfiguration.class)是导入默认的配置,然后再做各种自定义的操作。
feign默认依赖了hystrix,但是默认并没有开启hystrix,除非设置feign.hystrix.enabled=true手动开启,然后hystrix就会把每个方法都包装到断路器里面,此时feign的客户端方法是可以返回HystrixCommand的,这样就可以做异步或者响应式编程。
@FeignClient()里面可以设置fallback或者fallbackFactory,fallbackFactory可以捕获异常,但是并不是原始的异常,而是feign经过包装以后的异常,可以定义ErrorDecoder完成自定义异常的处理。
看一个fallback的demo:
@FeignClient(name="ServerHystrixFeignClient",
url="http://localhost:8080",
path = "/server/hystrix",
// 这里可以配置fallback或者fallbackFactory
// fallback的优先级更高
fallback = ServerHystrixFeignClientFallback.class,
fallbackFactory = ServerHystrixFeignClientFallbackFactory.class
)
public interface ServerHystrixFeignClient {
@GetMapping("/hello")
public String hello();
}
//必须要注入到spring容器,
//此时容器中会有两个类型是ServerHystrixFeignClient的bean
//一个feign客户端,一个是fallabck的bean
@Service
public class ServerHystrixFeignClientFallback implements
ServerHystrixFeignClient{
@Override
public String hello() {
return "ServerHystrixFeignClientFallback fallback";
}
}
//必须要注入到spring容器
@Service
public class ServerHystrixFeignClientFallbackFactory implements
FallbackFactory<ServerHystrixFeignClient> {
@Override
public ServerHystrixFeignClient create(Throwable cause) {
//注意这里的异常类型:feign.FeignException$InternalServerError
System.out.println(cause.getClass());
return new ServerHystrixFeignClient(){
@Override
public String hello() {
return "ServerHystrixFeignClientFallbackFactory fallback";
}
};
}
}
注意:这里的ServerHystrixFeignClient不能再去继承接口,因为接口上有@RequestMapping,而fallback最终也需要继承接口,因此,会把fallback的方法当成feign客户端的方法,这样就冲突了。但是,此时在spring容器里面仍然有两个相同类型的feign客户端,一个是feign客户端本身,另一个是fallback的bean,所以框架默认给feign客户端上添加了@Primary注解,因此feign客户端才可以正常注入,因为它的优先级比fallabck的优先级要高。如果想关闭primary,可以设置primary = false:
@FeignClient(name = "hello", primary = false)
public interface HelloClient {}
如果开启了hystrix,默认是所有的feign客户端都开启了断路器,但是如果某个客户端不想开启,可以单独给它配置一个Feign.Builder(开启了hystrix后默认的Builder是HystrixFeign.Builder),并且设置scope为prototype,,举个例子:
@FeignClient(name="ServerHystrixPrototypeClient",
url="http://localhost:8080",
path = "/server/hystrix",
// 自定义配置
configuration = PrototypeConfiguration.class,
fallback = ServerHystrixPrototypeClientFallback.class
)
public interface ServerHystrixPrototypeClient {
@GetMapping("/prototype")
public String prototype();
}
//在这个配置里面设置Feign.Builder的scope
public class PrototypeConfiguration {
@Bean
@Scope("prototype")
//注意这里的类型
public Feign.Builder feignBuilder() {
return Feign.builder();
}
}
只要让feign抛出HystrixBadRequestException,那么本次请求就不会触发fallback和断路器,我们可以自定义feign的ErrorDecoder,在里面根据要访问的路径决定是否抛出HystrixBadRequestException:
public static class MyErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String methodKey, Response response) {
try{
String res = Util.toString(response.body().asReader(Util.UTF_8));
JsonElement je = new JsonParser().parse(res);
JsonObject jo = je.getAsJsonObject();
String path = jo.get("path").getAsString();
System.out.println("path="+path);
if(path.equals("/server/hystrix/error/hello")){
return new HystrixBadRequestException(res);
}else{
return FeignException.errorStatus(methodKey, response);
}
}catch(Exception e){
e.printStackTrace();
return e ;
}
}
}
如果开启了hystrix(默认是关闭的feign.hystrix.enabled=false),同时还想在拦截器里面使用ThreadLocal,要么就禁用hystrix,要么把并发策略设置为SEMAPHORE,要么需要自定义并发策略才可以。
# 禁用hystrix
feign:
hystrix:
enabled: false
# 并发策略设置为SEMAPHORE
hystrix:
command:
default:
execution:
isolation:
strategy: SEMAPHORE
关于如何自定义并发策略,可以查看前面文章,本文后面的参考代码中也有。
只需要设置以下配置项即可:
feign.compression.request.enabled=true
feign.compression.request.mime-types=text/xml,application/xml,application/json
feign.compression.request.min-request-size=2048
feign.compression.response.enabled=true
feign.compression.response.useGzipDecoder=true
参考代码下载:https://github.com/xjs1919/enumdemo下面的openfeign/feign-boot。