Java 开源项目 OpenFeign —— feign 的基本使用

1. 前言

最近公司的项目里使用到了 Feign 开源项目,这里作学习笔记

2. Feign 架构(来自官方)

feign 由大部分组成,由于刚开始接触 feign ,我们自然比较关注的 clientsencoders/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 }
原文地址:https://www.cnblogs.com/siweipancc/p/12635294.html