Figen GET傳參遇到的問題以及解決方法

今天學(xué)習(xí)feigen遇到了一些問題,在此記錄一下。

  1. feigen用GET方式傳遞對(duì)象的時(shí)候遇到405錯(cuò)誤。
  2. 配置使用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)者的接口。

Postman

消費(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)傳參成功了。

大功告成!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容