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 }
来源:https://www.cnblogs.com/siweipancc/p/12635294.html