部署业务并使用Feign进行服务间调用(附带一部分源码分析)

前言

在前面我们搭建起来了eureka,和配置中心,基于这两者我们就能愉快的写业务代码了。这章节我们看下怎么业务服务和业务服务直接如何完成调用。

这里我们需要创建两个项目分别叫demo1-service,demo2-service。demo1-service将会调用demo2提供的服务完成一个hello world。
一个架构图如下:

依赖

当前最新的spring boot版本为2.1.7 我们将他作为我们的parent。主要是方便三方jar的版本管理和maven插件的使用。

1
2
3
4
5
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
</parent>

加入spring cloud的pom依赖,方便spring cloud 提供的相关jar的版本管理。这里需要将此依赖放入dependencyManagement

1
2
3
4
5
6
7
8
9
10
11
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

需要服务发现所以导入eureka。不爱用jersey,直接排除掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<exclusions>
<exclusion>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-client</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-core</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jersey.contribs</groupId>
<artifactId>jersey-apache-client4</artifactId>
</exclusion>
</exclusions>
</dependency>

还需要ClientLB,加入ribbon。同样排除掉jersey

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
<exclusions>
<exclusion>
<artifactId>jersey-client</artifactId>
<groupId>com.sun.jersey</groupId>
</exclusion>
<exclusion>
<artifactId>jersey-apache-client4</artifactId>
<groupId>com.sun.jersey.contribs</groupId>
</exclusion>
</exclusions>
</dependency>

需要方便服务调用,加入openfeign依赖。

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

加入服务追踪zipkinsleuth

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

POM

完整pom如下

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>io.qingmu</groupId>
<artifactId>demo1-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo1-service</name>

<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR2</spring-cloud.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<exclusions>
<exclusion>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-client</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-core</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jersey.contribs</groupId>
<artifactId>jersey-apache-client4</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
<exclusions>
<exclusion>
<artifactId>jersey-client</artifactId>
<groupId>com.sun.jersey</groupId>
</exclusion>
<exclusion>
<artifactId>jersey-apache-client4</artifactId>
<groupId>com.sun.jersey.contribs</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.spotify</groupId>
<artifactId>docker-maven-plugin</artifactId>
<configuration>
<imageName>
freemanliu/eureka:v1.0.0
</imageName>
<registryUrl></registryUrl>
<workdir>/work</workdir>
<rm>true</rm>
<env>
<TZ>Asia/Shanghai</TZ>
<JAVA_OPTS>
-XX:+UnlockExperimentalVMOptions \
-XX:+UseCGroupMemoryLimitForHeap \
-XX:MaxRAMFraction=2 \
-XX:CICompilerCount=8 \
-XX:ActiveProcessorCount=8 \
-XX:+UseG1GC \
-XX:+AggressiveOpts \
-XX:+UseFastAccessorMethods \
-XX:+UseStringDeduplication \
-XX:+UseCompressedOops \
-XX:+OptimizeStringConcat
</JAVA_OPTS>
</env>
<baseImage>freemanliu/openjre:8.212</baseImage>
<cmd>
/sbin/tini java ${JAVA_OPTS} -jar ${project.build.finalName}.jar
</cmd>
<!--是否推送image-->
<pushImage>true</pushImage>
<resources>
<resource>
<directory>${project.build.directory}</directory>
<include>${project.build.finalName}.jar</include>
</resource>
</resources>
<serverId>docker-hub</serverId>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

bootstrap.yml

这里配置一下eureka服务的地址,config-service会自动服务发现,不需要配置地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
spring:
cloud:
config:
label: master
failFast: true
discovery:
enabled: true
serviceId: config-service
eureka:
client:
serviceUrl:
defaultZone: ${EUREKA_SERVER:http://172.224.4.7:8761/eureka/}
instance:
# 使用ip注册。
prefer-ip-address: true
lease-renewal-interval-in-seconds: 10
lease-expiration-duration-in-seconds: 20
instance-id: ${spring.application.name}:${server.port}@${random.long(1000000,9999999)}

application.yml

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
server:
undertow:
worker-threads: 50
io-threads: 2
# demo1 port 8100. demo2 8101
port: 8100
compression:
enabled: true
mime-types: application/json,application/javascript,text/plain,application/x-javascript,text/css,application/xml
min-response-size: 4096
spring:
sleuth:
web:
client:
enabled: true
sampler:
# 配置采集率1.0表示100%,0.1表示10%
probability: ${SAMPLER_PROBABILITY:1.0}
application:
# demo1-service,demo2-service
name: demo1-service
http:
encoding:
charset: UTF-8
enabled: true
force: true
zipkin:
# 配置zipkin服务地址
base-url: ${ZIPKIN:http://10.96.0.13:9411/}


logging:
logPath: /var/log/${spring.application.name}
level:
com.netflix.discovery.shared.resolver.aws: ERROR

management:
endpoints:
web:
exposure:
include: "*"

在config git上添加配置文件

我们需要在config的git仓库添加如下两个配置文件。方便我们从config-service获取到配置信息。

  • demo1-service-default.properties

    1
    demo1Value=hello
  • demo1-service-default.properties

    1
    demo2Value=world

Demo1-service

Demo1Resource

这里我们先编写demo1的资源API,这是一个十分简单的接口,当启动时,config-client活从远程的config-service获取到配置demo1Value的值,并注入进来,在访问/hello时,我们将返回给浏览器”hello”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package io.qingmu.demo1;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class Demo1Resource {

@Value("${demo1Value}")
private String demo1Value;

@GetMapping("hello")
public String hello() {
return demo1Value;
}
}

编写启动类Demo1ServiceApplication.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package io.qingmu.demo1;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableDiscoveryClient
public class Demo1ServiceApplication {
public static void main(String[] args) {
SpringApplication.run(Demo1ServiceApplication.class, args);
}
}

然后我们启动demo1-service。使用curl访问我们业务接口,会看到我们的

script
1
2
$ curl http://localhost:8100/hello
hello

Demo2-service

demo2的resource和demo1相差不多,这里不多说。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package io.qingmu.demo2service;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class Demo2Resource {

@Value("${demo2Value}")
private String demo2Value;

@GetMapping("world")
public String hello() {
return demo2Value;
}
}

编写启动类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package io.qingmu.demo2service;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class Demo2ServiceApplication {

public static void main(String[] args) {
SpringApplication.run(Demo2ServiceApplication.class, args);
}

}

启动成功后。我们使用curl访问demo2的服务

script
1
2
$ curl http://localhost:8101/world
world

这样我们就完成了demo1-service和demo2-service的基本编写。接下来我们来看下demo1需要调demo2应该怎么处理。

Feign Client 服务调服务。

首先我们在demo1中编写一个demo2的接口,并给接口加上@FeignClient注解,表示这是一个内部服务,注解的括号中我们填入服务名即可。

1
2
3
4
5
6
7
8
9
10
11
package io.qingmu.demo1;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient("demo2-service")
public interface Demo2Client {

@GetMapping("/world")
String world();
}

然后我们在Demo1Resource中注入该client。并改写hello方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
public class Demo1Resource {

@Autowired
private Demo2Client demo2Client;

@Value("${demo1Value}")
private String demo1Value;

@GetMapping("hello")
public String hello() {
return demo1Value + " " + demo2Client.world();
}
}

由于我们注入了feignclient,我们需要声明式的启用Feign组件,在启动类上添加注解@EnableFeignClients

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class Demo1ServiceApplication {
public static void main(String[] args) {
SpringApplication.run(Demo1ServiceApplication.class, args);
}
}

然后我们再次启动Demo1Service。
部分启动日志如下:

1
2
3
4
5
6
2019-08-23 15:46:57.135  INFO [demo1-service,,,] 3710 --- [           main] org.xnio                                 : XNIO version 3.3.8.Final
2019-08-23 15:46:57.148 INFO [demo1-service,,,] 3710 --- [ main] org.xnio.nio : XNIO NIO Implementation Version 3.3.8.Final
2019-08-23 15:46:57.151 INFO [demo1-service,,,] 3710 --- [nfoReplicator-0] com.netflix.discovery.DiscoveryClient : DiscoveryClient_DEMO1-SERVICE/demo1-service:8100@6670754 - registration status: 204
2019-08-23 15:46:57.230 INFO [demo1-service,,,] 3710 --- [ main] o.s.b.w.e.u.UndertowServletWebServer : Undertow started on port(s) 8100 (http) with context path ''
2019-08-23 15:46:57.230 INFO [demo1-service,,,] 3710 --- [ main] .s.c.n.e.s.EurekaAutoServiceRegistration : Updating port to 8100
2019-08-23 15:46:57.233 INFO [demo1-service,,,] 3710 --- [ main] io.qingmu.demo1.Demo1ServiceApplication : Started Demo1ServiceApplication in 5.057 seconds (JVM running for 5.77)

再次访问demo1

script
1
2
$ curl http://localhost:8100/hello
hello world

  • 查看zipkin

  • 查看eureka

Feign Get 传递对象

在使用feignclient时,默认是不允许http get 方法传递对象参数的,只能传递基本类型,但是我们实际业务中是需要get并传递对象进行业务功能的,比如说QueryModel查询对象。我们先来试验下,能不能传递。
首先我们定义一个模型就叫DemoQueryModel

1
2
3
4
5
6
7
8
9
10
11
12
package io.qingmu.demo1;

import lombok.*;

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class DemoQueryModel {
private String nameEQ;
}

并在demo2-service中添加新的资源api,world2,这个方法将接受一个Query模型,并新构建一个返回出去。

1
2
3
4
@GetMapping("world2")
public DemoQueryModel hello2(DemoQueryModel demo) {
return DemoQueryModel.builder().nameEQ(demo.getNameEQ()).build();
}

写好了demo2-service之后我们接下来改造demo1-service
我们将这个资源clent添加到Demo2Client中。

1
2
@GetMapping("world2")
DemoQueryModel hello2(DemoQueryModel demo);

Demo1Resource,也新增一个资源叫hello2

1
2
3
4
@GetMapping("hello2")
public DemoQueryModel hello2() {
return demo2Client.hello2(DemoQueryModel.builder().nameEQ("world").build());
}

我们期待demo1demo2,然后返回demo1提供的hello2资源。

script
1
2
3
4
5
6
freemandeMacBook-Pro:demo1-service freeman$ curl -i http://localhost:8100/hello2
HTTP/1.1 500 Internal Server Error
Connection: keep-alive
Transfer-Encoding: chunked
Content-Type: application/json;charset=UTF-8
Date: Tue, 27 Aug 2019 04:36:17 GMT

可以看到如下的结果,Feign抛出了405,MethodNotAllowed异常,感觉很奇怪。

1
2
3
4
5
6
{
"timestamp":"2019-08-26T20:36:17.638+0000"
,"status":500
,"error":"Internal Server Error","message":"Request processing failed; nested exception is feign.FeignException$MethodNotAllowed: status 405 reading Demo2Client#hello2(DemoQueryModel)"
,"path":"/hello2"
}

我们在看demo2这边的。控制台日志中记录了一条很奇怪的异常。它居然说Request method 'POST' not supported,可是我们明明发的是Get请求呀。

1
1584 --- [  XNIO-1 task-3] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'POST' not supported]

Feign Get请求自动变成了POST?

一路火花带闪电,debug下来,走到了sun.net.www.protocol.http.HttpURLConnection类中。
你问我为啥为啥会走到这个类,聊这个之前我们先看下feign实现桥接了几种后端httpclient具体实现。由于他们都是开源项目,我们可以直接通过代码直接看。
首先我们打开FeignAutoConfiguration这个类,这是配置feign的核心类。通过这个类我们可以知道,feign桥接了ApacheHttpClientOkHttp
这里我们没有导入ApacheHttpClient的maven依赖,所以这里不会被激活。

在看OkHttp,同样的道理,我们也没有导入OkHttp的maven依赖,这里依然不会被激活。


如果你也跟着我去看了这个类。你会发现这里都走完了,没有看到其他的实现啊。邪门了!! 我们的http调用到底是咋个发起的呢?!!
怀着心有不甘,我点进了feign.Client中,发现了一片新天地,没错这里看到了我们想看到的。HttpURLConnection

但是问题由来了,他是在哪里激活的呢?经过一番找寻。我找到了DefaultFeignLoadBalancedConfiguration,给他打上断,走你~~。果然走到这里来,当发现ApacheHttpClientOkHttp都不能使用时,feign留了后手,直接用jdk自带的HttpURLConnection发起请求。

那这又和我们的疑问Get请求变成Post有啥关系呢?看到下面的截取自HttpURLConnection的代码就清楚了。这里居然把我们的GET请求换成了POST。我没太想明白为啥要这样做?历史原因??

1
2
3
4
5
6
7
8
9
10
private synchronized OutputStream getOutputStream0() throws IOException {
// ...
if (!this.doOutput) {
throw new ProtocolException("cannot write to a URLConnection if doOutput=false - call setDoOutput(true)");
} else {
if (this.method.equals("GET")) {
this.method = "POST";
}
// ...
}

激活OKHTTP

好那么找到问题根源了,是后端实现的锅,我可更换一下后端实现换成OkHttp
在maven中加入一下依赖,分别是okttp,feign-okhttp的桥接(即提供类feign.okhttp.OkHttpClient)实现。

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.1.0</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
<version>10.3.0</version>
</dependency>

紧接着需要在application中配置激活okhttp作为feign的后端实现。

1
2
3
4
5
feign:
okhttp:
enabled: true
httpclient:
enabled: false

此时我们在发起请求访问资源

script
1
2
3
4
5
6
7
8
freemandeMacBook-Pro:demo1-service freeman$ curl -i http://localhost:8100/hello2
HTTP/1.1 500 Internal Server Error
Connection: keep-alive
Transfer-Encoding: chunked
Content-Type: application/json;charset=UTF-8
Date: Tue, 27 Aug 2019 08:07:40 GMT

{"timestamp":"2019-08-27T08:07:40.639+0000","status":500,"error":"Internal Server Error","message":"Request processing failed; nested exception is java.lang.RuntimeException: com.netflix.client.ClientException: Number of retries on next server exceeded max 1 retries, while making a call for: 192.168.0.66:8101","path":"/hello2"}

现在访问抛出的异常错误是Number of retries on next server exceeded max 1 retries, while making a call for: 192.168.0.66:810
这个从字面意思看上去就是异常了在重试,最好还是失败了。我们可以先禁用ribbon的重试。配置如下:

1
2
3
4
5
ribbon:
okhttp:
enabled: true
MaxAutoRetries: 0
MaxAutoRetriesNextServer: 0

禁用之后我们在发起,

script
1
2
3
4
5
6
7
8
freemandeMacBook-Pro:demo1-service freeman$ curl -i http://localhost:8100/hello2
HTTP/1.1 500 Internal Server Error
Connection: keep-alive
Transfer-Encoding: chunked
Content-Type: application/json;charset=UTF-8
Date: Tue, 27 Aug 2019 08:12:09 GMT

{"timestamp":"2019-08-27T08:12:09.936+0000","status":500,"error":"Internal Server Error","message":"Request processing failed; nested exception is java.lang.RuntimeException: com.netflix.client.ClientException","path":"/hello2"}

这次我们从返回的json中无法看到有用信息了,直接看控制台日志信息。这次换了个后端,虽然还是抛出异常,但是已经没有把我们的GET请求变成POST了。看异常很明显的意思是就是把我们的DemoQueryModel当成body了,然而get方法不允许传递body。这个难搞了。

1
2
3
4
5
6
......
Caused by: java.lang.IllegalArgumentException: method GET must not have a request body.
at okhttp3.Request$Builder.method(Request.kt:258) ~[okhttp-4.1.0.jar:na]
at feign.okhttp.OkHttpClient.toOkHttpRequest(OkHttpClient.java:88) ~[feign-okhttp-10.3.0.jar:na]
at feign.okhttp.OkHttpClient.execute(OkHttpClient.java:166) ~[feign-okhttp-10.3.0.jar:na]
......

@SpringQueryMap

在spring cloud 2.1.x 以上的版本,提供了一个新的注解@SpringQueryMap,使用这个注解呢,我们就可以传递了。接着我们把代码修改一下,在参数前添加一个注解。

1
2
@GetMapping("world2")
DemoQueryModel hello2(@SpringQueryMap DemoQueryModel demo);

再次请求,可以发现我们成功了。

script
1
2
3
4
5
6
7
8
freemandeMacBook-Pro:demo1-service freeman$ curl -i http://localhost:8100/hello2
HTTP/1.1 200 OK
Connection: keep-alive
Transfer-Encoding: chunked
Content-Type: application/json;charset=UTF-8
Date: Tue, 27 Aug 2019 08:47:54 GMT

{"nameEQ":"world"}

为啥加了一个注解就行了呢?为什么??其实也很简单,既然不支持body传递,那么我们只需要些个拦截器,将body解析为url传参即可。
我们接下来简单的跟一下源码。
首先是注解处理器QueryMapParameterProcessor,一下代码截取自该类,这个注解处理器会在容器启动的时候对带有@SpringQueryMap参数注解的方法进行处理。
可以看到metadata.queryMapIndex() == null这句判断表示它只会处理参数中第一个@SpringQueryMap注解,其余的不会进行处理。紧接着把带有注解的参数的索引下标存了起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
public class QueryMapParameterProcessor implements AnnotatedParameterProcessor {
private static final Class<SpringQueryMap> ANNOTATION = SpringQueryMap.class;
@Override
public Class<? extends Annotation> getAnnotationType() {
return ANNOTATION;
}
@Override
public boolean processArgument(AnnotatedParameterContext context,
Annotation annotation, Method method) {
int paramIndex = context.getParameterIndex();
MethodMetadata metadata = context.getMethodMetadata();
if (metadata.queryMapIndex() == null) {
metadata.queryMapIndex(paramIndex);
metadata.queryMapEncoded(SpringQueryMap.class.cast(annotation).encoded());
}
return true;
}

}

看完了解析部分,在看些具体使用的地方ReflectiveFeign,在这个类中有个方法是create,在225的位置,就用上了我们注解处理器设置好的元信息。
下面代码十分好懂:

  1. 首先是判断是否需要处理querymap。
  2. 然后通过下标获取到需要特殊处理的对象。
  3. 将对象转换外map
  4. 拼接追加map到url中。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public RequestTemplate create(Object[] argv) {
    ...
    if (metadata.queryMapIndex() != null) {
    // add query map parameters after initial resolve so that they take
    // precedence over any predefined values
    Object value = argv[metadata.queryMapIndex()];
    //将对象转换为map
    Map<String, Object> queryMap = toQueryMap(value);
    //拼接解析完成的对象为URL参数
    template = addQueryMapQueryParameters(queryMap, template);
    }
    ...

    }

我们通过一个小实验来看下,我们现在deme2-service中打一个断点。然后不放行,等到几秒之后,查看demo1-service抛出的异常信息。timeout executing GET http://demo2-service/world2?nameEQ=world 可以看到我们的模型被转换成了K=V参数。

script
1
2
3
4
5
6
7
8
freemandeMacBook-Pro:demo1-service freeman$ curl -i http://localhost:8100/hello2
HTTP/1.1 500 Internal Server Error
Connection: keep-alive
Transfer-Encoding: chunked
Content-Type: application/json;charset=UTF-8
Date: Tue, 27 Aug 2019 08:45:05 GMT

{"timestamp":"2019-08-27T08:45:05.800+0000","status":500,"error":"Internal Server Error","message":"Request processing failed; nested exception is feign.RetryableException: timeout executing GET http://demo2-service/world2?nameEQ=world","path":"/hello2"}

激活ApacheHttpClient

当然如果你更喜欢ApacheHttpClient,你就需要加入如下配置了
maven依赖,即提供feign.httpclient.ApacheHttpClient

1
2
3
4
5
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
<version>10.3.0</version>
</dependency>

yaml文件

1
2
3
4
5
feign:
okhttp:
enabled: false
httpclient:
enabled: true

如果使用ApacheHttpClient的话不会抛出像OkHttp那样的错误,当然也不能正常处理,也是需要加上@SpringQueryMap注解,才能正常工作。

部署到kubernetes

demo1-servicedemo2-service分别执行打包命令

script
1
mvn clean package

部署demo2

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87

apiVersion: v1
kind: Service
metadata:
name: demo2-service
namespace: default
labels:
app: demo2-service
spec:
ports:
- port: 8101
name: tcp
selector:
app: demo2-service
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo2-service
namespace: default
spec:
revisionHistoryLimit: 10
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 25%
maxSurge: 25%
replicas: 1
selector:
matchLabels:
app: demo2-service
template:
metadata:
labels:
app: demo2-service
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
topologyKey: kubernetes.io/hostname
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- demo2-service
weight: 1
containers:
- name: demo2-service
image: freemanliu/demo2-service:v1.0.0
imagePullPolicy: Always
lifecycle:
preStop:
httpGet:
port: 8101
path: /spring/shutdown
livenessProbe:
httpGet:
path: /actuator/health
port: 8101
periodSeconds: 5
timeoutSeconds: 10
successThreshold: 1
failureThreshold: 5
readinessProbe:
httpGet:
path: /actuator/health
port: 8101
periodSeconds: 5
timeoutSeconds: 10
successThreshold: 1
failureThreshold: 5
resources:
requests:
memory: 1Gi
limits:
memory: 1Gi
ports:
- containerPort: 8101
env:
- name: EUREKA_SERVER
value: "http://eureka-0.eureka:8761/eureka/,http://eureka-1.eureka:8761/eureka/,http://eureka-2.eureka:8761/eureka/"
- name: SAMPLER_PROBABILITY
value: "1.0"
- name: ZIPKIN
value: "http://10.96.0.13:9411/"

部署

script
1
kubectl apply -f demo2-service.yaml

部署demo1

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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
---
apiVersion: v1
kind: Service
metadata:
name: demo1-service
namespace: default
labels:
app: demo1-service
spec:
ports:
- port: 8100
name: tcp
selector:
app: demo1-service
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo1-service
namespace: default
spec:
revisionHistoryLimit: 10
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 25%
maxSurge: 25%
replicas: 1
selector:
matchLabels:
app: demo1-service
template:
metadata:
labels:
app: demo1-service
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
topologyKey: kubernetes.io/hostname
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- demo1-service
weight: 1
containers:
- name: demo1-service
image: freemanliu/demo1-service:v1.0.0
imagePullPolicy: Always
lifecycle:
preStop:
httpGet:
port: 8100
path: /spring/shutdown
livenessProbe:
httpGet:
path: /actuator/health
port: 8100
periodSeconds: 5
timeoutSeconds: 10
successThreshold: 1
failureThreshold: 5
readinessProbe:
httpGet:
path: /actuator/health
port: 8100
periodSeconds: 5
timeoutSeconds: 10
successThreshold: 1
failureThreshold: 5
resources:
requests:
memory: 1Gi
limits:
memory: 1Gi
ports:
- containerPort: 8100
env:
- name: EUREKA_SERVER
value: "http://eureka-0.eureka:8761/eureka/,http://eureka-1.eureka:8761/eureka/,http://eureka-2.eureka:8761/eureka/"
- name: SAMPLER_PROBABILITY
value: "1.0"
- name: ZIPKIN
value: "http://10.96.0.13:9411/"

部署

script
1
kubectl apply -f demo1-service.yaml

查看状态

script
1
2
3
freemandeMacBook-Pro$ kubectl get pods -owide | grep demo
demo1-service-7fbd969f45-fzs9q 1/1 Running 0 3m52s 172.224.5.53 node3 <none> <none>
demo2-service-fcf59b8c7-z5znb 1/1 Running 0 14m 172.224.7.39 node6 <none> <none>

使用浏览器访问demo1-service,可以看到成功了。

Github

https://github.com/qingmuio/demo2-service

https://github.com/qingmuio/demo1-service

推荐文章