今天學(xué)習(xí)feigen遇到了一些問題,在此記錄一下。
- feigen用GET方式傳遞對(duì)象的時(shí)候遇到405錯(cuò)誤。
- 配置使用HttpClient的時(shí)候遇到
java.lang.NoSuchMethodError: feign.Response.create(ILjava/lang/String;Ljava/util/Map;Lfeign/Response$Body;)Lfeign/Response;錯(cuò)誤。
首先模擬一下feigen利用GET方式傳遞對(duì)象。
1. 創(chuàng)建服務(wù)提供者
pom.xml 截取
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Greenwich.SR2</spring-cloud.version>
</properties>
<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>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
application.yml
spring:
application:
name: provider
server:
port: 8080
eureka:
client:
service-url:
defaultZone: http://eureka.springcloud.cn/eureka/
這里的Eureka我用的是公益-Eureka Server注冊(cè)中心,哈哈,不想在本地創(chuàng)建Eureka項(xiàng)目了。
啟動(dòng)類:ProviderApplication.java
@SpringBootApplication
@EnableEurekaClient
public class ProviderApplication {
public static void main(String[] args) {
SpringApplication.run(ProviderApplication.class, args);
}
}
創(chuàng)建一個(gè)簡(jiǎn)單的domain,作為傳輸參數(shù)。
User.java
public class User {
private Integer id;
private String name;
// getter ...
// setter ...
}
創(chuàng)建測(cè)試用的服務(wù)接口
UserController.java
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping(value = "",method = RequestMethod.GET)
public String getUser(@RequestBody User user){
return "user: " + user.getName();
}
}
這里我們用GET方式接收參數(shù)。
2.創(chuàng)建服務(wù)消費(fèi)者
這里只是比服務(wù)提供者多了一個(gè)openfeign的依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
application.yml
spring:
application:
name: consumer
server:
port: 8081
eureka:
client:
service-url:
defaultZone: http://eureka.springcloud.cn/eureka/
啟動(dòng)類:ConsumerApplication.java
@SpringBootApplication
@EnableFeignClients
@EnableHystrix
@EnableEurekaClient
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
}
UserService.java
@FeignClient(value = "provider")
public interface UserService {
@RequestMapping(value = "/user",method = RequestMethod.GET)
public String getUser(@RequestBody User user);
}
ConsumerController.java
@RestController
public class ConsumerController {
@Autowired
private UserService userService;
@RequestMapping(value = "/getUser",method = RequestMethod.GET)
@ResponseBody
public String getUser(User user){
return userService.getUser(user);
}
}
服務(wù)提供者和消費(fèi)者都創(chuàng)建完畢了,我們來測(cè)試一下。
啟動(dòng)項(xiàng)目,用Postman調(diào)用一下服務(wù)消費(fèi)者的接口。

消費(fèi)者端控制臺(tái)日志
feign.FeignException$MethodNotAllowed: status 405 reading UserService#getUser(User)
at feign.FeignException.errorStatus(FeignException.java:100) ~[feign-core-10.2.3.jar:na]
at feign.FeignException.errorStatus(FeignException.java:86) ~[feign-core-10.2.3.jar:na]
at feign.codec.ErrorDecoder$Default.decode(ErrorDecoder.java:93) ~[feign-core-10.2.3.jar:na]
at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:149) ~[feign-core-10.2.3.jar:na]
at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:78) ~[feign-core-10.2.3.jar:na]
at feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:103) ~[feign-core-10.2.3.jar:na]
at com.sun.proxy.$Proxy88.getUser(Unknown Source) ~[na:na]
at com.zhaojun.consumer.controller.ConsumerController.getUser(ConsumerController.java:20) ~[classes/:na]
服務(wù)提供者控制臺(tái)日志
2019-08-23 01:25:22.215 WARN 15944 --- [nio-8080-exec-7] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'POST' not supported]
可以看到我們明明定義的是以GET請(qǐng)求的方式調(diào)用服務(wù)提供者,但是卻變成了用POST方式調(diào)用。這是為什么呢。
然后通過斷點(diǎn)調(diào)試發(fā)現(xiàn),feign默認(rèn)使用的是HttpURLConnection作為請(qǐng)求的客戶端,feign-core包下的Client.java的convertAndSend方法下有如下代碼

這里request.requestBody().asBytes() 是不為空的,requestBody就是為我們傳的參數(shù),接下來會(huì)執(zhí)行這段代碼
OutputStream out = connection.getOutputStream();
我們看看這個(gè)方法

然后再進(jìn)入getOutputStream0()方法

可以看到,他會(huì)判斷請(qǐng)求是否是GET如果是的話,就把請(qǐng)求方式變?yōu)镻OST。
所以,我們請(qǐng)求服務(wù)提供者的時(shí)候會(huì)出現(xiàn)405的錯(cuò)誤。
那么,為什么我們get請(qǐng)求會(huì)有requestBody呢?
因?yàn)樵趏pfeign-core包下的SpringEncoder類下的encode中

將我們的請(qǐng)求參數(shù)轉(zhuǎn)成了json,然后設(shè)置header=application/json;charset=UTF-8,然后再將json轉(zhuǎn)成byte數(shù)組放到了requesBody中。
所以,HttpURLConnection判斷我們的requestBody不為空,然后認(rèn)為是POST請(qǐng)求。那么我們?nèi)绾谓鉀Q這個(gè)問題呢?
方法一、使用httpclient代替HttpURLConnection
既然HttpURLConnection會(huì)把帶有body的GET請(qǐng)求轉(zhuǎn)成POST那么我們不用他好了。
首先,引入相關(guān)依賴
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
<version>10.2.3</version>
</dependency>
這里注意feign-httpclient版本要與feign-core版本一致。
我這里就犯了一個(gè)錯(cuò)誤,我根據(jù)網(wǎng)上以及書上的的教材進(jìn)行配置,他們說需要引入這個(gè)依賴
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-httpclient</artifactId>
<version>8.18.0</version>
</dependency>
但是他們的spring-boot是2.0.3.RELEASE,spring-cloud版本是Finchley.RELEASE版的,而我的版本和他們不同,導(dǎo)致在進(jìn)行調(diào)用的時(shí)候產(chǎn)生了java.lang.NoSuchMethodError: feign.Response.create(ILjava/lang/String;Ljava/util/Map;Lfeign/Response$Body;)Lfeign/Response異常。
這是因?yàn)閏reate方法在Feign 10就已經(jīng)移除掉了

好了,引入依賴后,在配置文件中開啟使用httpclient
feign:
httpclient:
enabled: true
在我使用這個(gè)版本這個(gè)值是默認(rèn)為true的,但是還是配上吧。

然后重新啟動(dòng)項(xiàng)目后用Postman調(diào)用,發(fā)現(xiàn)已經(jīng)成功了。

方法二、增加攔截器處理requestBody中的參數(shù)
如果我們不想用httpclient,也不想讓GET請(qǐng)求中包含requestBody,那我們就需要把requestTemplate中的requestBody置為空,并且將條件拼接到url后面。這里參考《重新定義Spring Cloud實(shí)戰(zhàn)》一書給出代碼(根據(jù)SpringBoot版本不同代碼有所改動(dòng))。
@Component
public class FeignRequestInterceptor implements RequestInterceptor {
@Autowired
private ObjectMapper objectMapper;
@Override
public void apply(RequestTemplate template) {
// feign 不支持 GET 方法傳 POJO, json body轉(zhuǎn)query
if (template.method().equals("GET") && template.requestBody() != null) {
try {
JsonNode jsonNode = objectMapper.readTree(template.requestBody().asBytes());
template.body(Request.Body.empty());
Map<String, Collection<String>> queries = new HashMap<>();
buildQuery(jsonNode, "", queries);
template.queries(queries);
} catch (IOException e) {
//提示:根據(jù)實(shí)踐項(xiàng)目情況處理此處異常,這里不做擴(kuò)展。
e.printStackTrace();
}
}
}
private void buildQuery(JsonNode jsonNode, String path, Map<String, Collection<String>> queries) {
if (!jsonNode.isContainerNode()) { // 葉子節(jié)點(diǎn)
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()) { // 數(shù)組節(jié)點(diǎn)
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 { // 根節(jié)點(diǎn)
buildQuery(entry.getValue(), entry.getKey(), queries);
}
}
}
}
}
可以看到請(qǐng)求改造前后的變化。
改造前:

改造后:

這樣就變成了傳統(tǒng)的get方法獲取數(shù)據(jù),那么服務(wù)提供者這邊需要把@RequestBody注解去掉,因?yàn)楝F(xiàn)在RequestBody已經(jīng)是空了。
@RestController
@RequestMapping("/user")
public class UserController {
/**
* 去掉 @RequestBody
*/
@RequestMapping(value = "",method = RequestMethod.GET)
public String getUser(User user){
return "user: " + user.getName();
}
}
接下來測(cè)試一下,發(fā)現(xiàn)傳參成功了。

大功告成!