Feign解决了什么问题 一个服务里要调用别的HTTP接口,每次要手写一些重复的HTTP处理逻辑,比如我们如果单独用Ribbon调用别的服务,就需要每次写这样一坨代码
String result = restTemplate.getForObject("http://ServiceName/InterfacePath", String.class);
虽然Spring的RestTemplate
已经将HTTP调用的代码成本降到很低了,但如果极致一些,完全不写行不行?答案是可以。Feign就是提我们完成这件事情的不二之选。
快速体验Feign 此次我们依然在以前的示例上添加代码,这样就可以将精力放到体会Feign的特性上了。
1、给ServiceA加几个接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 @PostMapping ("/" )public String createUser (@RequestBody User user) { Integer port = environment.getProperty("local.server.port" , Integer.class); System.out.println("创建用户," + user); return "{'msg': 'success', 'from': " + port + "}" ; } @PutMapping ("/{id}" )public String updateUser (@PathVariable("id" ) Long id, @RequestBody User user) { System.out.println("更新用户,id:" + id); Integer port = environment.getProperty("local.server.port" , Integer.class); return "{'msg': 'success', 'from': " + port + "}" ; } @DeleteMapping ("/{id}" )public String deleteUser (@PathVariable("id" ) Long id) { System.out.println("删除用户,id:" + id); Integer port = environment.getProperty("local.server.port" , Integer.class); return "{'msg': 'success', 'from': " + port + "}" ; } @GetMapping ("/{id}" )public User getById (@PathVariable("id" ) Long id) { System.out.println("查询用户,id:" + id); return new User("sam" , 25 , id); } static class User { private String username; private Integer age; private Long id; public User (String username, Integer age, Long id) { this .username = username; this .age = age; this .id = id; } public Long getId () { return id; } public void setId (Long id) { this .id = id; } public String getUsername () { return username; } public void setUsername (String username) { this .username = username; } public Integer getAge () { return age; } public void setAge (Integer age) { this .age = age; } @Override public String toString () { return "User{" + "username='" + username + '\'' + ", age=" + age + '}' ; } }
2、重构ServiceB:基于Feign调用ServiceA 在serviceB的pom.xml中添加依赖
1 2 3 4 <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-openfeign</artifactId > </dependency >
在启动类中添加@FeignClients
注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Slf 4j@EnableFeignClients (basePackages = {"cc.houqian.eurekalearn.client" })@EnableEurekaClient @SpringBootApplication public class ServiceBApplication { @Bean @LoadBalanced public RestTemplate getRestTemplate () { return new RestTemplate(); } public static void main (String[] args) { SpringApplication.run(ServiceBApplication.class, args); log.info("ServiceB 启动成功" ); } }
新建一个Feign客户端类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 @FeignClient ("serviceA" )public interface ServiceAClient { @GetMapping ("/sayHello/{name}" ) String sayHello (@PathVariable("name" ) String name) ; @PostMapping ("/" ) String createUser (@RequestBody User user) ; @PutMapping ("/{id}" ) String updateUser (@PathVariable("id" ) Long id, @RequestBody User user) ; @DeleteMapping ("/{id}" ) String deleteUser (@PathVariable("id" ) Long id) ; @GetMapping ("/{id}" ) User getById (@PathVariable("id" ) Long id) ; static class User { private String username; private Integer age; private Long id; public User (String username, Integer age, Long id) { this .username = username; this .age = age; this .id = id; } public Long getId () { return id; } public void setId (Long id) { this .id = id; } public String getUsername () { return username; } public void setUsername (String username) { this .username = username; } public Integer getAge () { return age; } public void setAge (Integer age) { this .age = age; } @Override public String toString () { return "User{" + "username='" + username + '\'' + ", age=" + age + '}' ; } } }
改造接口调用处,使用Feign客户端
1 2 3 4 5 6 7 8 9 10 @Resource private ServiceAClient serviceAClient; @GetMapping ("/greeting/{name}" ) public String greeting (@PathVariable("name" ) String name) { String response = serviceAClient.sayHello(name); log.info(response); return response; }
打开浏览器,访问http://localhost:9000/greeting/sam
,你会发现与之前使用RestTemplate
时的效果一致
此处功能已经实现,我们可以再想多一些,还有什么问题? 如果serviceA有多个调用方serviceC、D等等,按照现在这个套路,ServiceAClient这个Feign客户端是得每个服务都来一遍,非常不优雅。
我们可以想一下,提供服务的serviceA肯定是最了解自己的,根据知识专家原则,由他来提供feign客户端供其他消费者调用时合情合理的,这也解决了代码复用的问题。
所以,我们此时完全可以将ServiceAClient挪到一个新的工程里去,由serviceA自己维护,里面的职责就是维护serviceA一系列的暴露出去的feign客户端,然后让其他服务依赖这个工程,就可以消费serviceA的接口了。
3、Feign的继承特性
在上面,我们讨论了一个更好的做法。其实,Feign能提供的编码体验更为极致,我们上面还有一处代码是重复的,就是serviceA暴露接口的SpringMVC注解代码,现在使用Feign的继承特性,我们只需要定义一处SpringMVC的注解逻辑就可以了,这提供了极致的编码体验,非常的Clean。
(1)单独定义一个工程 service-a-api,定义统一对外暴露的接口和实体类 依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 <artifactId > service-a-api</artifactId > <properties > <maven.compiler.source > 8</maven.compiler.source > <maven.compiler.target > 8</maven.compiler.target > </properties > <dependencies > <dependency > <groupId > org.springframework.cloud</groupId > <artifactId > spring-cloud-starter-openfeign</artifactId > </dependency > </dependencies >
服务A对外暴露接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 package cc.houqian.eurekalearn.servicea.client;public interface ServiceAInterface { @GetMapping ("/sayHello/{name}" ) String sayHello (@PathVariable("name" ) String name) ; @PostMapping ("/" ) String createUser (@RequestBody User user) ; @PutMapping ("/{id}" ) String updateUser (@PathVariable("id" ) Long id, @RequestBody User user) ; @DeleteMapping ("/{id}" ) String deleteUser (@PathVariable("id" ) Long id) ; @GetMapping ("/{id}" ) User getById (@PathVariable("id" ) Long id) ; static class User { private String username; private Integer age; private Long id; public User (String username, Integer age, Long id) { this .username = username; this .age = age; this .id = id; } public Long getId () { return id; } public void setId (Long id) { this .id = id; } public String getUsername () { return username; } public void setUsername (String username) { this .username = username; } public Integer getAge () { return age; } public void setAge (Integer age) { this .age = age; } @Override public String toString () { return "User{" + "username='" + username + '\'' + ", age=" + age + '}' ; } } }
(2)ServiceA对定义好的接口实现业务逻辑 在serviceA的pom.xml中新增依赖
1 2 3 4 5 <dependency > <groupId > cc.houqian</groupId > <artifactId > service-a-api</artifactId > <version > 1.0-SNAPSHOT</version > </dependency >
改造ServiceAController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @RestController public class ServiceAController implements ServiceAInterface { @Resource private Environment environment; public String sayHello (@PathVariable("name" ) String name) { Integer port = environment.getProperty("local.server.port" , Integer.class); return "hello," + name + ", came from " + port; } public String createUser (@RequestBody User user) { Integer port = environment.getProperty("local.server.port" , Integer.class); System.out.println("创建用户," + user); return "{'msg': 'success', 'from': " + port + "}" ; } public String updateUser (@PathVariable("id" ) Long id, @RequestBody User user) { System.out.println("更新用户,id:" + id); Integer port = environment.getProperty("local.server.port" , Integer.class); return "{'msg': 'success', 'from': " + port + "}" ; } public String deleteUser (@PathVariable("id" ) Long id) { System.out.println("删除用户,id:" + id); Integer port = environment.getProperty("local.server.port" , Integer.class); return "{'msg': 'success', 'from': " + port + "}" ; } public User getById (@PathVariable("id" ) Long id) { System.out.println("查询用户,id:" + id); return new User("sam" , 25 , id); } }
(3)改造ServiceB依赖ServiceA的接口Jar包 在serviceB的pom.xml中新增依赖
1 2 3 4 5 <dependency > <groupId > cc.houqian</groupId > <artifactId > service-a-api</artifactId > <version > 1.0-SNAPSHOT</version > </dependency >
改造ServiceAClient
1 2 3 4 5 6 7 8 9 package cc.houqian.eurekalearn.client;import cc.houqian.eurekalearn.servicea.client.ServiceAInterface;import org.springframework.cloud.openfeign.FeignClient;@FeignClient ("serviceA" )public interface ServiceAClient extends ServiceAInterface {}
(4)实验 使用POSTMAN,随意调用接口,这里以createUser为例,可以看到基于Feign继承功能的调用是OK的。
Feign的自定义配置
Feign提供了请求拦截器的功能,当我们调用的服务,有共性的需求需要处理时,非常适合使用拦截器实现,比如Token获取等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public static class MyConf { @Bean public RequestInterceptor requestInterceptor () { return new MyRequestInterceptor(); } } @Slf 4jpublic static class MyRequestInterceptor implements RequestInterceptor { @Override public void apply (RequestTemplate template) { log.info("url:{}" , template.url()); log.info("variables:{}" , template.variables()); log.info("method:{}" , template.method()); log.info("headers:{}" , template.headers()); log.info("body:{}" , template.bodyTemplate()); } }
核心组件
笔者觉得核心组件这种东西完全可以放到入门篇里,因为这是你了解一门技术的必经之路,哪怕入门也是有必要了解的。 下面这张图摘录自https://github.com/OpenFeign/feign#feature-overview
Client(客户端) 在SpringCloud环境里就是FeignClient
,我们使用Feign最核心的入口。其实,Spring就是帮我们构造了一个FeignClient,里面包含了各种核心组件,比如Encoder
、Decoder
、Logger
、Contract
等待
Contract(协议) 意思是说除了Feign原生的那些API外,他还对各种其他技术做了集成,这个集成好像一个个协议,将Feign的核心API与别的技术连接起来,进行通信。 以Spring为例,Feign的核心API肯定不会识别Spring web mvc的各种注解,比如@PatchVariable、@RequestMapping、@RequestParam等,但他通过Spring4或者SpringBoot这些为三方库定制的协议,就可以将Spring的这么些个web注解解释成自己的API了,让人家不用改变自己的使用习惯就可以使用Feign。
Encoder、Decoder(编码器、解码器) 其实就是你调用接口的时候,传递的参数是个对象,feign需要将这个对象进行encode,编码,比如转成一个JSON格式,这就是Encoder的职责。 Decoder,解码器,就是你的接口收到一个JSON字符串后,Feign给你转成你定义的对象
Feign.Builder(图中未列出) Feign Core提供的一个构造器,用来构造一个FeignClient实例
Logger(图中未列出) Feign Core提供的日志组件
SpringCloud提供的Feign默认组件
笔者在前面Eureka、Ribbon的系列文章里,已经详细的剖析过SpringCloud整合三方技术的套路了,此处笔者不再赘述,直接给出对应的结论。
Spring提供的默认FeignClient是LoadBalancerFeignClient
Spring提供的默认解码器是ResponseEntityDecoder
,带有分页功能的解码器是PageableSpringEncoder
Spring提供的默认编码器是SpringEncoder
Spring提供的默认协议是SpringMvcContract
Spring提供的默认构造器是Feign.Builder
(这个就是Feign Core自带的,原生的)
Spring提供的默认日志组件是Slf4jLogger
常见配置 超时相关配置
1 2 3 4 5 6 7 8 9 10 11 12 13 feign: client: config: serviceA: connectionTimeout: 500 readTimeout: 1000 loggerLevel: full decode404: false defaul: connectionTimeout: 500 readTimeout: 1000 loggerLevel: full decode404: false
压缩相关配置
1 2 3 4 5 6 7 8 9 feign: compression: request: enabled: true mime-types: text/xml.application/xml,application/json min-request-size: 2048 response: enabled: true useGzipDecoder: false
日志相关配置
1 2 # 此处配合feign的日志级别使用 logging.level.cc.houqian.eurekalearn.client.ServiceAClient=debug
1 2 3 4 5 6 7 8 9 10 public static class MyConf { @Bean public Logger.Level feignLoggerLevel () { return Logger.Level.FULL; } }
效果
总结 本篇我们了解如下内容
Feign的核心概念
SpringCloud环境下Feign的使用方法
SpringCloud环境下Feign的默认组件
SpringCloud环境下Feign的配置