Feign
一、Feign概述
Feign是一个声明式的Web Service客户端。在Spring Cloud 中使用Feign,可以做到
使用HTTP请求访问远程服务,就像调用本地方法一样,同时它整合了Ribbon和Hystrix。
入门案例:
主要依赖:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring Cloud OpenFeign的Starter的依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> </dependencies>
主入口程序注解:
@SpringBootApplication //该注解表示当程序启动时,会进行包扫描,扫描所有带@FeignClient的注解类并进行处理 @EnableFeignClients public class SpringCloudFeignApplication { public static void main(String[] args) { SpringApplication.run(SpringCloudFeignApplication.class, args); } }
config:
@Configuration public class HelloFeignServiceConfig { @Bean Logger.Level feignLoggerLevel() { return Logger.Level.FULL; } }
FeignClient:
/** * url = "https://api.github.com",该调用地址用于根据传入的字符串搜索github上的 * 仓库信息。HellFeignService最终会根据指定的URL和@RequestMapping对应方法,转换成最终 * 请求地址: https://api.github.com/search/repositories?q=spring-cloud-dubbo
* @FeignClient中name:指定FeignClient名称,如果项目使用了Ribbon,该名称会用户服务发现
* configuration:Feign配置类,可以自定义Feign的Encoder、Decoder、LogLevel
* fallback:定义容错处理类,当调用远程接口失败时,会调用接口的容错逻辑,fallback指定的类必须实现@FeignClient标记接口。
* fallbackFactory: 用于生成fallback类示例,可实现每个接口的通用容错逻辑,减少重复代码。 * @author Tang Jiujia * @since 2019-03-26 */ @FeignClient(name = "github-client", url = "https://api.github.com", configuration = HelloFeignServiceConfig.class) public interface HelloFeignService { @RequestMapping(value = "/search/repositories", method = RequestMethod.GET) String searchRepo(@RequestParam("q") String queryStr); }
controller:
@RestController public class HelloFeignController { @Autowired HelloFeignService helloFeignService; @RequestMapping(value = "/search/github") public String searchGithubRepoByStr(@RequestParam("str") String queryStr) { return helloFeignService.searchRepo(queryStr); } }
启动,浏览器输入:http://localhost:8012/search/github?str=spring-cloud-dubbo
二、Feign工作原理
主程序入口添加@EnableFeignClients注解---->定义FeignClient接口并添加@FeignClients/@FeignClient注解--->
程序启动扫描所有拥有@FeignClients/@FeignClient注解的接口并将这些信息注入Spring IOC容器--->定义的FeignClient
接口中的方法被调用---->通过JDK代理为每个接口方法生成RequestTemplate对象(该对象封装了HTTP请求需要的全部信息)
---->由RequestTemplate对象生成Request--->Request交给Client(URLConnection、Http Client等)处理--->Client被封装
到LoadBalanceClient类,这个类结合Ribbon负载均衡发起服务之间的调用。
三、Feign基础功能
1.开启GZIP压缩
Spring Cloud Feign支持对响应和请求进行GZIP压缩,以提高通信效率。
通过application.yml配置:
feign: compression: request: enabled: true mime-types: text/xml,application/xml,application/json # 配置压缩支持的MIME TYPE min-request-size: 2048 # 配置压缩数据大小的下限 response: enabled: true # 配置响应GZIP压缩
由于开启GZIP压缩后,Feign之间的调用通过二进制协议进行传输,返回值需要修改为ResponseEntity<byte[]>,
才可以正常显示:
@FeignClient(name = "github-client", url = "https://api.github.com", configuration = FeignGzipConfig.class) public interface FeignClinet { @RequestMapping(value = "/search/repositories", method = RequestMethod.GET) ResponseEntity<byte[]> searchRepo(@RequestParam("q") String queryStr); }
2.开启日志
application.yml:
logging:
level:
cn.springcloud.book.feign.service.HelloFeignService: debug
配置类:
@Bean Logger.Level feignLoggerLevel() { return Logger.Level.FULL; }
四、Feign实战应用
1.Feign默认Client的替换
Feign默认使用的是JDK原生的URLConnection发送HTTP请求,没有连接池。
1)使用HTTP Client替换默认Client
依赖:
<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> </dependency> <dependency> <groupId>com.netflix.feign</groupId> <artifactId>feign-httpclient</artifactId> <version>8.17.0</version> </dependency>
application.yml:
server: port: 8010 spring: application: name: ch4-3-httpclient feign: httpclient: enabled: true
2.使用okhttp替换Feign默认的Client
依赖:
<dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-okhttp</artifactId> </dependency>
application.yml:
server: port: 8011 spring: application: name: okhttp feign: httpclient: enabled: false okhttp: enabled: true
构建自定义的OkHttpClient:
@Configuration @ConditionalOnClass(Feign.class) @AutoConfigureBefore(FeignAutoConfiguration.class) public class FeignOkHttpConfig { @Bean public okhttp3.OkHttpClient okHttpClient() { return new okhttp3.OkHttpClient.Builder() .connectTimeout(60, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS) .writeTimeout(60, TimeUnit.SECONDS) .retryOnConnectionFailure(true) .connectionPool(new ConnectionPool()) .build(); } }
3.Feign的Post和Get的多参数传递
SpringMVC是支持GET方法直接绑定POJO的,但是Feign的实现并没有覆盖所有的SpringMVC功能。
最佳解决方式,通过Feign拦截器的方式处理:
1)通过实现Feign的RequestInterceptor中的apply方法进行统一拦截转换处理Feign
中的GET方法参数传递的问题。
@Component public class FeignRequestInterceptor implements RequestInterceptor{ @Autowired private ObjectMapper objectMapper; @Override public void apply(RequestTemplate template) { // feign 不支持 GET 方法传 POJO, json body转query if (template.method().equals("GET") && template.body() != null) { try { JsonNode jsonNode = objectMapper.readTree(template.body()); template.body(null); HashMap<String, Collection<String>> queries = new HashMap<>(); buildQuery(jsonNode, "", queries); template.queries(queries); } catch (IOException e) { e.printStackTrace(); } } } private void buildQuery(JsonNode jsonNode, String path, Map<String, Collection<String>> queries) { if (!jsonNode.isContainerNode()) { if (jsonNode.isNull()) { return; } Collection<String> values = queries.get(path); if (null == values) { values = new ArrayList<>(); queries.put(path, values); } values.add(jsonNode.asText()); return; } if (jsonNode.isArray()) { // 数组节点 Iterator<JsonNode> it = jsonNode.elements(); while (it.hasNext()) { buildQuery(it.next(), path, queries); } } else { Iterator<Map.Entry<String, JsonNode>> it = jsonNode.fields(); while (it.hasNext()) { Map.Entry<String, JsonNode> entry = it.next(); if (StringUtils.hasText(path)) { buildQuery(entry.getValue(), path + "." + entry.getKey(), queries); } else { // 根节点 buildQuery(entry.getValue(), entry.getKey(), queries); } } } } }
2)集成Swagger2用于多参数传递:
@Configuration @EnableSwagger2 public class Swagger2Config { @Bean public Docket createRestApi() { return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select() .apis(RequestHandlerSelectors .basePackage("com.demon.feign")) .paths(PathSelectors.any()).build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder().title("Feign多参数传递问题").description("Feign多参数传递问题") .contact("Software_King@qq.com").version("1.0").build(); } }
3)消费者:
@RestController @RequestMapping("/user") public class UserController { @Autowired private UserFeignService userFeignService; @RequestMapping(value = "/add", method = RequestMethod.POST) public String addUser(@RequestBody @ApiParam(name="用户", value="传入json格式",required=true) User user) { return userFeignService.addUser(user); } @RequestMapping(value = "/update", method = RequestMethod.POST) public String updateUser( @RequestBody @ApiParam(name="用户",value="传入json格式",required=true) User user){ return userFeignService.updateUser(user); } }
4)Feign Client:
@FeignClient(name = "provider") public interface UserFeignService { @RequestMapping(value = "/user/add", method = RequestMethod.GET) public String addUser(User user); @RequestMapping(value = "/user/update", method = RequestMethod.POST) public String updateUser(@RequestBody User user); }
5)服务提供者:
@RestController @RequestMapping("/user") public class UserController { @RequestMapping(value = "/add", method = RequestMethod.GET) public String addUser(User user , HttpServletRequest request){ String token=request.getHeader("oauthToken"); return "hello,"+user.getName(); } @RequestMapping(value = "/update", method = RequestMethod.POST) public String updateUser( @RequestBody User user){ return "hello,"+user.getName(); } }