1. 前言
最近公司的项目里使用到了 Feign 开源项目,这里作学习笔记
2. Feign 架构(来自官方)
feign 由五大部分组成,由于刚开始接触 feign ,我们自然比较关注的 clients 跟 encoders/decoders
3. 代码测试
3.1 官方教程
接触一个项目最直接的方式就是从官方 Demo 开始,刚开始接触 feign 的童鞋可能会找不到官方教程的 GsonDecoder 源,它在 feign-gson 模块中,让我们引入 Maven 依赖
1 <properties> 2 <feign-version>9.5.0</feign-version> 3 </properties> 4 5 <dependencies> 6 <dependency> 7 <groupId>io.github.openfeign</groupId> 8 <artifactId>feign-core</artifactId> 9 <version>${feign-version}</version> 10 </dependency> 11 12 <dependency> 13 <groupId>io.github.openfeign</groupId> 14 <artifactId>feign-gson</artifactId> 15 <version>${feign-version}</version> 16 </dependency> 17 </dependencies>
接着是官方的Demo:
1 public class SimpleGit { 2 interface GitHub { 3 4 @RequestLine("GET /repos/{owner}/{repo}/contributors") 5 List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo); 6 7 @RequestLine("POST /repos/{owner}/{repo}/issues") 8 void createIssue(Issue issue, @Param("owner") String owner, @Param("repo") String repo); 9 10 } 11 12 public static class Contributor { 13 String login; 14 int contributions; 15 } 16 17 public static class Issue { 18 String title; 19 String body; 20 List<String> assignees; 21 int milestone; 22 List<String> labels; 23 } 24 25 public static void main(String[] args) { 26 GitHub github = Feign.builder() 27 .decoder(new GsonDecoder()) 28 .target(GitHub.class, "https://api.github.com"); 29 30 // Fetch and print a list of the contributors to this library. 31 List<Contributor> contributors = github.contributors("OpenFeign", "feign"); 32 for (Contributor contributor : contributors) { 33 System.out.println(contributor.login + " (" + contributor.contributions + ")"); 34 } 35 } 36 }
从 Demo 可以看到,我们需要一个 decoder ,GsonDecoder#decode 方法,可以看到这是一个转换 Json 串到实体的方法,同时 还捕捉了 IOExeption , IOExeption 包装了 IOException(包括 HTTP 请求中的错误)。
运行它,可以看到打印的结果:
adriancole (358)
velo (86)
kdavisk6 (82)
...
3.2 使用 Jackson 改写官方 Demo
你可能会想使用 Jackson 作为 encoder/decoder 方法 ,官方也提供了 feign-json 模块,我们可以引入 Maven 依赖,或者手动编写一个,然后手动替换相应的语句。不过我们这里缺了一些内容,我们需要补上一些 Jackson 注解。不然会遇到 FeignException 错误(原因在于请求的返回不止两个字段) :
1 @Data 2 @JsonIgnoreProperties(ignoreUnknown = true) 3 public static class Contributor { 4 String login; 5 int contributions; 6 }
4. 注解
在官方的 Demo 中,我们看到了一个注解 RequestLine,除此,feign 还提供了其他注解完成一个 HTTP 请求中所需要的各种信息
4.1 RequestLine
用于填充 请求类别,请求路径,http 版本( HTTP /1.1 )(METHOD)
4.3 Body
用于填充请求体(METHOD)
4.3 Headers
用于填充请求头(METHOD, TYPE)
4.3 HeaderMap
用于填充请求头(PARAMETER)
4.2 Param
用于填充 Body/Headers/RequestLine 上的占位符(PARAMETER, FIELD, METHOD)
4.3 QueryMap
用于填充请求参数 (PARAMETER)
5. 两个完整的注解例子
5.1 使用 Jackson
让我们结合 SpringMVC 写一个完整的例子,这样可以让我们更深的了解它的作用,我们需要定义一个词典,一个控制器,一个供 Feign 调用的接口,还有一个测试类:
5.1.1 词典类
1 /** 2 * @author pancc 3 * @version 1.0 4 */ 5 @Data 6 @Accessors(chain = true) 7 public class Dict { 8 9 private int size; 10 private List<String> words; 11 12 public int getSize() { 13 return words == null ? 0 : words.size(); 14 } 15 16 public static Dict Instance; 17 18 static { 19 String[] names = { 20 "auto", "autoCycle", "autoMan", 21 "bicycle", "bike", 22 "cream", "clean", "cycle", 23 "day", "doom", "dying", 24 "envy", "em..", "eye" 25 }; 26 Instance = new Dict().setWords(Lists.newArrayList(names)); 27 } 28 }
5.1.2 控制器
1 /** 2 * @author pancc 3 * @version 1.0 4 */ 5 @RestController 6 @RequestMapping("dic") 7 public class FeignController { 8 private static final Dict dict = Dict.Instance; 9 10 11 @GetMapping("details") 12 public Dict details() { 13 return dict; 14 } 15 16 17 @GetMapping("startsWith/{query}") 18 public List<String> startsWith(@PathVariable("query") String query) { 19 return dict.getWords().stream().filter(s -> s.startsWith(query)).collect(Collectors.toList()); 20 } 21 22 @GetMapping("query") 23 public List<String> startAndEnd(@RequestParam("startsWith") String start, @RequestParam("endsWith") String end) { 24 return dict.getWords().stream().filter(s -> s.startsWith(start) && s.endsWith(end)) 25 .collect(Collectors.toList()); 26 } 27 28 @PostMapping(value = "add", consumes = MediaType.APPLICATION_JSON_VALUE) 29 public List<String> add(@RequestBody Map<String, String> map) { 30 List<String> strings = Lists.newArrayList(dict.getWords()); 31 map.forEach((key, value) -> strings.add(value)); 32 return strings; 33 } 34 35 @PutMapping(value = "replace", consumes = MediaType.APPLICATION_JSON_VALUE) 36 public List<String> replace(@RequestBody Map<String, String> map) { 37 return dict.getWords().stream().map(s -> { 38 if (map.containsKey(s)) { 39 return map.get(s); 40 } 41 return s; 42 }).collect(Collectors.toList()); 43 } 44 45 @PutMapping(value = "updateFirst") 46 public List<String> updateFirst(@RequestParam("target") String str) { 47 return dict.getWords().stream().map(s -> { 48 if (dict.getWords().get(0).equals(s)) { 49 return str; 50 } 51 return s; 52 }).collect(Collectors.toList()); 53 } 54 55 @GetMapping(value = "headers") 56 public Map<String, Object> headers(HttpServletRequest request) { 57 return Collections.list(request.getHeaderNames()) 58 .stream().collect(Collectors.toMap(s -> s, request::getHeader)); 59 } 60 61 @DeleteMapping("deleteFirst") 62 public List<String> deleteFirst() { 63 return dict.getWords().stream().filter(s -> !dict.getWords().get(0).equals(s)).collect(Collectors.toList()); 64 } 65 66 }
5.1.3 Feign 接口
1 /** 2 * @author pancc 3 * @version 1.0 4 */ 5 @Headers("Accept: application/json") 6 public interface DictFeign { 7 /** 8 * @see FeignController#details 9 */ 10 @RequestLine("GET /dic/details") 11 Dict details(); 12 13 14 /** 15 * @see FeignController#startsWith 16 */ 17 @RequestLine("GET /dic/startsWith/{query}") 18 List<String> startsWith(@Param("query") String query); 19 20 21 /** 22 * @see FeignController#updateFirst 23 */ 24 @RequestLine("PUT /dic/updateFirst?target={target}") 25 @Headers(HttpHeaders.APPLICATION_JSON) 26 List<String> updateFirst(@Param("target") String target); 27 28 /** 29 * @see FeignController#headers 30 */ 31 @RequestLine("GET /dic/headers") 32 @Headers(HttpHeaders.APPLICATION_JSON) 33 Map<String, Object> headers(@HeaderMap Map<String, Object> headers); 34 35 /** 36 * @see FeignController#startAndEnd 37 */ 38 @RequestLine("GET /dic/query") 39 List<String> startAndEnd(@QueryMap Map<String, String> map); 40 41 /** 42 * @see FeignController#replace 43 */ 44 @RequestLine("PUT /dic/replace") 45 @Headers(HttpHeaders.APPLICATION_JSON) 46 List<String> replace(Map<String, String> map); 47 48 49 /** 50 * @see FeignController#add 51 */ 52 @RequestLine("POST /dic/add") 53 @Headers(HttpHeaders.APPLICATION_JSON) 54 @Body("%7B\"var1\" : \"{v1}\",\"var2\": \"{v2}\" %7D") 55 List<String> add(@Param("v1") String var1, @Param("v2") String var2); 56 57 /** 58 * @see FeignController#deleteFirst 59 */ 60 @RequestLine("DELETE /dic/deleteFirst") 61 List<String> deleteFirst(); 62 63 }
这里需要注意使用 %7B 代替 {, %7D 代替 }
5.1.4 测试类
1 /** 2 * @author pancc 3 * @version 1.0 4 */ 5 @SpringBootTest( 6 value = {"server.port=8081", "logging.level.web=debug"}, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) 7 class DictFeignTest { 8 9 static DictFeign dictFeign; 10 private static Dict dict = Dict.Instance; 11 12 @BeforeAll 13 public static void beforeAll() { 14 dictFeign = Feign.builder().decoder(new JacksonDecoder()).encoder(new JacksonEncoder()) 15 .target(DictFeign.class, "http://127.0.0.1:8081"); 16 } 17 18 @Test 19 void details() { 20 Assertions.assertEquals(dict, dictFeign.details()); 21 } 22 23 @Test 24 void startsWith() { 25 Assertions.assertEquals(3, dictFeign.startsWith("a").size()); 26 } 27 28 29 @Test 30 void startAndEnd() { 31 Map<String, String> map = Maps.newHashMap(); 32 map.put("startsWith", "e"); 33 map.put("endsWith", "e"); 34 Assertions.assertEquals(1, dictFeign.startAndEnd(map).size()); 35 } 36 37 @Test 38 void replace() { 39 Assertions.assertNotEquals(dictFeign.replace(Collections.singletonMap("bike", "bikes")).indexOf("bikes"), -1); 40 } 41 42 @Test 43 void updateFirst() { 44 Assertions.assertEquals("game", dictFeign.updateFirst("game").get(0)); 45 } 46 47 @Test 48 void deleteFirst() { 49 Assertions.assertEquals(13, dictFeign.deleteFirst().size()); 50 } 51 52 53 @Test 54 void headers() { 55 Map<String, Object> headers = Maps.newHashMap(); 56 headers.put("age", 15); 57 headers.put("length", 21); 58 Assertions.assertTrue(dictFeign.headers(headers).containsKey("age")); 59 Assertions.assertTrue(dictFeign.headers(headers).containsKey("length")); 60 61 } 62 63 @Test 64 void add() { 65 String var1 = "go~"; 66 String var2 = "back"; 67 List<String> adds = dictFeign.add(var1, var2); 68 Assertions.assertTrue(adds.contains(var1)); 69 Assertions.assertTrue(adds.contains(var2)); 70 71 } 72 }
5.2 当遇到表单
在上边的例子中,我们沿用了 JacksonDecoder/JacksonEncoder,它们用于序列化/反序列化 POJO 类,要使用表单时,就需要实现 FormEncoder,同样我们加入 Maven 依赖:
1 <dependency> 2 <groupId>io.github.openfeign.form</groupId> 3 <artifactId>feign-form</artifactId> 4 <version>3.8.0</version> 5 </dependency>
接下来,模拟一个用户登录的流程,我们将尝试三种登录方式,表单,Json ,与 JWT
5.2.1 用户登录实体
1 /** 2 * @author pancc 3 * @version 1.0 4 */ 5 @Data 6 @Accessors(chain = true) 7 public class UserInfo { 8 9 private String username; 10 private String password; 11 12 13 boolean isValid() { 14 return "root".equals(this.getPassword()) && "admin".equals(this.getUsername()); 15 } 16 17 static boolean isValid(String token) { 18 if (Strings.nullToEmpty(token).trim().startsWith("Bearer ")) { 19 return "user-token".equals(token.split(" ")[1]); 20 } 21 return false; 22 23 } 24 25 }
5.2.2 响应实体
1 /** 2 * @author pancc 3 * @version 1.0 4 */ 5 @Data 6 @Accessors(chain = true) 7 @SuppressWarnings("unused") 8 public class Response { 9 private int status = 200; 10 private String msg; 11 private Object data; 12 13 public String getMsg() { 14 return status == 200 ? "Success" : "Failed"; 15 } 16 }
5.2.3 控制器
1 /** 2 * @author pancc 3 * @version 1.0 4 */ 5 @RestController 6 @RequestMapping("/") 7 public class LoginController { 8 9 10 @PostMapping(value = "login", consumes = MediaType.APPLICATION_JSON_VALUE) 11 public Response loginWithJson(@RequestBody UserInfo userInfo) { 12 if (userInfo.isValid()) { 13 return new Response().setData("Well Come " + userInfo.getUsername()); 14 } 15 return new Response().setStatus(404); 16 } 17 18 @PostMapping(value = "login", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) 19 public Response loginWithForm(UserInfo userInfo) { 20 if (userInfo.isValid()) { 21 return new Response().setData("Well Come " + userInfo.getUsername()); 22 } 23 return new Response().setStatus(404); 24 } 25 26 @PostMapping(value = "login") 27 public Response loginWithToken(@Nullable @RequestHeader(HttpHeaders.AUTHORIZATION) String token) { 28 if (UserInfo.isValid(token)) { 29 return new Response().setData("Well Come"); 30 } 31 return new Response().setStatus(404); 32 } 33 34 }
5.2.4 Feign 接口
1 /** 2 * @author pancc 3 * @version 1.0 4 */ 5 public interface LoginFeign { 6 /** 7 * @see LoginController#loginWithJson 8 */ 9 @RequestLine("POST /login") 10 @Headers("Content-Type: application/json") 11 Response loginWithJson(UserInfo userInfo); 12 13 /** 14 * @see LoginController#loginWithForm 15 */ 16 @RequestLine("POST /login") 17 @Headers("Content-Type: application/x-www-form-urlencoded") 18 Response loginWithForm(UserInfo userInfo); 19 20 21 /** 22 * @see LoginController#loginWithToken 23 */ 24 @RequestLine("POST /login") 25 @Headers("Authorization: Bearer {token}") 26 Response loginWithToken(@Param("token") String token); 27 28 29 }
5.2.5 测试类
1 @SpringBootTest( 2 value = {"server.port=8082", "logging.level.web=debug"}, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) 3 class LoginFeignTest { 4 5 @Test 6 void loginWithJson() { 7 LoginFeign feign = Feign.builder() 8 .encoder(new JacksonEncoder()) 9 .decoder(new JacksonDecoder()) 10 .target(LoginFeign.class, "http://127.0.0.1:8082"); 11 Response res = feign.loginWithJson(new UserInfo().setPassword("root").setUsername("admin")); 12 Assertions.assertEquals(200,res.getStatus()); 13 } 14 15 @Test 16 void loginWithForm() { 17 LoginFeign feign = Feign.builder() 18 .encoder(new FormEncoder()) 19 .decoder(new JacksonDecoder()) 20 .target(LoginFeign.class, "http://127.0.0.1:8082"); 21 Response res = feign.loginWithForm(new UserInfo().setPassword("root").setUsername("admin")); 22 Assertions.assertEquals(200,res.getStatus()); 23 } 24 25 @Test 26 void loginWithToken() { 27 LoginFeign feign = Feign.builder() 28 .encoder(new JacksonEncoder()) 29 .decoder(new JacksonDecoder()) 30 .target(LoginFeign.class, "http://127.0.0.1:8082"); 31 Response res = feign.loginWithToken("user-token"); 32 Assertions.assertEquals(200,res.getStatus()); 33 } 34 }
7. 代理
7.1 网络连接的难题
在上边测试官方 Demo 的过程中,我们很大可能会遇到网络问题,这时候就需要使用代理,我们可以使用 Ok-Http 来设置代理,它的依赖:
1 <dependency> 2 <groupId>io.github.openfeign</groupId> 3 <artifactId>feign-okhttp</artifactId> 4 <version>${feign-version}</version> 5 </dependency>
接着需要改写一下官方 demo ,设置代理:
1 /** 2 * @author pancc 3 * @version 1.0 4 */ 5 public class SimpleGit { 6 interface GitHub { 7 8 @RequestLine("GET /repos/{owner}/{repo}/contributors") 9 List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo); 10 11 } 12 13 @Data 14 @JsonIgnoreProperties(ignoreUnknown = true) 15 public static class Contributor { 16 String login; 17 int contributions; 18 } 19 20 public static void main(String[] args) { 21 Proxy proxy = new Proxy(Proxy.Type.SOCKS, new InetSocketAddress("127.0.0.1", 10808)); 22 OkHttpClient client = new OkHttpClient.Builder().proxy(proxy).build(); 23 GitHub github = Feign.builder() 24 .decoder(new JacksonDecoder()) 25 .client(new feign.okhttp.OkHttpClient(client)) 26 .target(GitHub.class, "https://api.github.com"); 27 // Fetch and print a list of the contributors to this library. 28 List<Contributor> contributors = github.contributors("OpenFeign", "feign"); 29 for (Contributor contributor : contributors) { 30 System.out.println(contributor.login + " (" + contributor.contributions + ")"); 31 } 32 } 33 }