在做springcloud項目時,出現了如下報錯:
關鍵異常信息為:
1.簡單看一下報錯信息,找不到依賴,或者可以理解為依賴失敗,看一下截取的部分報錯信息
org.springframework.beans.factory.UnsatisfiedDependencyException:
Error creating bean with name 'goshowController':
針對這個報錯,網上找到了很多關於它可能的解決方案
(1) 啟動類沒有創建(項目都啟動了,沒啟動類,或者啟動類注解引入錯誤,這個一般不會是此情況,除非自己手敲啟動類):
有人遇到的是這樣的:測試包下的啟動類不能和main的啟動類 不能同名,所導致的問題;
(2) 沒有在配置文件中,配置啟動自動掃描Service所在的包:截取網上的兩端配置信息演示:
<context:component-scan base-package="com.liglei.service"></context:component-scan>
<!-- 啟動自動掃描 -->
<context:component-scan base-package="com.ssm.blog.*">
<!-- 制定掃包規則 ,不掃描@Controller注解的JAVA類 -->
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
(3) service接口實現類上有沒有加@Service注解,注解是不是引用的spring的類?不要導錯包:
或者干脆,接口有沒有寫實現類,實現類是實現的對應接口么?比如CategoryServiceImpl implementsCategoryDAO 一不小心根據自動提示,本來應該實現CategoryService,結果實現了CategoryDAO
(4) 查看注入是否正確:比如有網友遇到:
在mapper中使用的@Mapper注解的時候
導了錯誤的包
import org.mapstruct.Mapper;
應該導入
import org.apache.ibatis.annotations.Mapper;
(5) 看看jar包是否下載完整,是否有這個jar包,或者依賴沖突:
比如,加了thymeleaf的jar,加入的配置如下
<properties> <thymeleaf.version>3.0.9.RELEASE</thymeleaf.version> <!-- 布局功能的支持程序 thymeleaf3主程序 layout2以上版本 --> <!-- thymeleaf2 layout1--> <thymeleaf-layout-dialect.version>2.2.2</thymeleaf-layout-dialect.version> </properties> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
加入配置后,我檢查我們maven的jar,問題就出來了,我們springboot自己下載的版本是3.0.9,然后我再指定一個3.0.9,就導致報錯了,我們只需要刪掉一個即可解決此問題
去除方法如下圖
或者,看看下圖紅色方框內是否有包沒導進去,有就按照下面的步驟完成:
File–>Project Structure–>Artifacts–>右鍵demoPage–>Put into Output Root–>OK
確保以上步驟沒問題的,重啟一下項目試試~
------------------------------------------------------------------------------------------
但我遇到的問題,都不是以上情況:我的還有如下報錯信息:
2.
nested exception is java.lang.IllegalStateException:
Method has too many Body parameters:
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'goshowController': Unsatisfied dependency expressed through field 'goshowService'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'com.csair.lds.shell.openfeign.GoshowService': FactoryBean threw exception on object creation; nested exception is java.lang.IllegalStateException: Method has too many Body parameters: public abstract com.csair.lds.model.result.Results com.csair.lds.shell.openfeign.GoshowService.addGoshowPsg(com.csair.lds.shell.dto.PaxInfo,com.csair.lds.shell.dto.FlightInfo)
網上給出如下解釋:
feign中,如果發送的是get請求去調用其它模塊的方法,在接口處,形參需要添加 @RequestParam 注解。
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'dispathcherController': Unsatisfied dependency expressed through field 'userServiceFeign'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'com.offcn.webui.service.UserServiceFeign': FactoryBean threw exception on object creation; nested exception is java.lang.IllegalStateException: Method has too many Body parameters: public abstract com.offcn.common.response.AppResponse com.offcn.webui.service.UserServiceFeign.login(java.lang.String,java.lang.String)
會先報創建bean異常
org.springframework.beans.factory.UnsatisfiedDependencyException
再報
java.lang.IllegalStateException
其內容為:Method has too many Body parameters
將方法上添加注解 @RequestParam 后正常
//調用user模塊的方法 @GetMapping("/user/login") public AppResponse<UserRespVo> login(@RequestParam String username,@RequestParam String password);
原因為spring底層校驗導致的。
那么,很明顯,我的報錯信息如出一轍,我需要傳入的參數為兩個對象信息,但使用openFeign接口,有如下兩個注解@ResquestParam和@RequestBody:
openFeign接口方法傳遞多個參數時錯誤:nested exception is java.lang.IllegalStateException: Method has too many Body parameters: public abstract xxx
這是因為feign客戶端的方法參數沒有用相關的注解
@RequestParam
1. 客戶端@RequestParam注解的value屬性必須指定值,要和服務端接口參數名保持一致
2. 如果需要傳遞多個字符串參數,要使用多個@RequestParam注解與服務端接口參數保持對應
@RequestBody
1. @RequestBody 注解在服務端和客戶端都需要使用
2. 參數名和參數類型在服務端和客戶端需要保持一致
3. 一般用於傳遞對象
例:
遠程Controller
@RestController @RequestMapping("/storage") public class StorageController { @Autowired private StorageService storageService; @RequestMapping("/decrease") public CommonResult decrease(Long productId,Integer count){ storageService.decrease(productId,count); return new CommonResult(200,"扣減庫存成功"); } }
FeignClient
@FeignClient("seata-storage") @Component public interface StorageService { @PostMapping("/storage/decrease") CommonResult decrease(@RequestParam(value = "productId") Long productId, @RequestParam(value = "count") Integer count); }
注:
openFeign默認使用@RequestBody,但只允許一個@RequestBody,所以:
1. 只傳遞一個參數無需注解
2. 傳遞多個參數時要用@RequestParam
若遠程Controlle方法參數有@PathVariable注解,則FeignClient傳遞參數也必須用@PathVariable注解,且指定@PathVariable的value對應Controller。
很明顯,這兩個注解都不能滿足我在項目中的要求,那么,我又開始搜索,feign如何傳遞兩個對象參數。
Springcloud-feign:
在微服務盛行的時代,我們必不可少的使用到遠程調用,可遠程調用的組件和框架有很多,restTemplate和feign就是其中很經典的使用組件,由於feign通常會配合spring cloud使用,而restTemplate因其api復雜,分布式場景支持有限,feign必將會是遠程調用的首選。在進行springboot拆分成springcloud項目的時候,我使用feign來進行微服務的調用
了解Feign歷史的朋友會知道,Feign本身是Netflix的產品,Spring Cloud Feign是在原生Feign的基礎上進行了封裝,引入了大量的SpringMVC注解支持,這一方面使得其更容易被廣大的Spring使用者開箱即用,但也產生了不小的混淆作用。所以在使用Spring Cloud Feign之前,筆者先介紹一下SpringMVC的一個入參機制。預設一個RestController,在本地的8080端口啟動一個應用,用於接收http請求
SpringMVC的請求參數綁定機制:
@RestController public class BookController { @RequestMapping(value = "/hello") // <1> public String hello(String name) { // <2> return "hello " + name; } }
這個接口寫起來非常簡單,但實際springmvc做了非常多的兼容,使得這個接口可以接受多種請求方式。
-
RequestMapping代表映射的路徑,使用GET,POST,PUT,DELETE方式都可以映射到該端點。
-
SpringMVC中常用的請求參數注解有(@RequestParam,@RequestBody,@PathVariable)等。name被默認當做@RequestParam。形參String name由框架使用字節碼技術獲取name這個名稱,自動檢測請求參數中key值為name的參數,也可以使用@RequestParam(“name”)覆蓋變量本身的名稱。當我們在url中攜帶name參數或者form表單中攜帶name參數時,會被獲取到。
POST /hello HTTP/1.1 Host: localhost:8080 Content-Type: application/x-www-form-urlencoded name=formParam
或者:
GET /hello?name=queryString HTTP/1.1
Host: localhost:8080
Feign的請求參數綁定機制
上述的SpringMVC參數綁定機制,大家應該都是非常熟悉的,但這一切在Feign中有些許的不同。
我們來看一個非常簡單的,但是實際上錯誤的接口寫法:
//注意:錯誤的接口寫法 @FeignClient("book") public interface BookApi { @RequestMapping(value = "/hello",method = RequestMethod.GET) String hello(String name); }
配置請求地址:
ribbon: eureka: enabled: false book: ribbon: listOfServers: http://localhost:8080
我們按照寫SpringMVC的RestController的習慣寫了一個FeignClient,按照我們的一開始的想法,由於指定了請求方式是GET,那么name應該會作為QueryString拼接到Url中吧?發出一個這樣的GET請求:
GET /hello?name=xxx HTTP/1.1
Host: localhost:8080
而實際上,RestController並沒有接收到,我們在RestController一側的應用中獲得了一些提示:
-
並沒有按照期望使用GET方式發送請求,而是POST方式
-
name參數沒有被封裝,獲得了一個null值
查看文檔發現,如果不加默認的注解,Feign則會對參數默認加上@RequestBody注解,而RequestBody一定是包含在請求體中的,GET方式無法包含。所以上述兩個現象得到了解釋。Feign在GET請求包含RequestBody時強制轉成了POST請求,而不是報錯。
理解清楚了這個機制我們就可以在開發Feign接口避免很多坑。而解決上述這個問題也很簡單
-
在Feign接口中為name添加@RequestParam(“name”)注解,name必須指定,Feign的請求參數不會利用SpringMVC字節碼的機制自動給定一個默認的名稱。
-
由於Feign默認使用@RequestBody,也可以改造RestController,使用@RequestBody接收。但是,請求參數通常是多個,推薦使用上述的@RequestParam,而@RequestBody一般只用於傳遞對象。
Feign綁定復合參數
指定請求參數的類型與請求方式,上述問題的出現實際上是由於在沒有理清楚Feign內部機制的前提下想當然的和SpringMVC進行了類比。同樣,在使用對象作為參數時,也需要注意這樣的問題。
對於這樣的接口
@FeignClient("book") public interface BookApi { @RequestMapping(value = "/book",method = RequestMethod.POST) Book book(@RequestBody Book book); // <1> @RequestMapping(value = "/book",method = RequestMethod.POST) Book book(@RequestParam("id") String id,@RequestParam("name") String name); // <2> @RequestMapping(value = "/book",method = RequestMethod.POST) Book book(@RequestParam Map map); // <3> //錯誤的寫法 @RequestMapping(value = "/book",method = RequestMethod.POST) Book book(@RequestParam Book book); // <4> }
-
使用@RequestBody傳遞對象是最常用的方式。
-
如果參數並不是很多,可以平鋪開使用@RequestParam
-
使用Map,這也是完全可以的,但不太符合面向對象的思想,不能從代碼立刻看出該接口需要什么樣的參數。
-
錯誤的用法,Feign沒有提供這樣的機制自動轉換實體為Map。
Feign中使用@PathVariable與RESTFUL規范
這涉及到一個如何設計RESTFUL接口的話題,我們知道在自從RESTFUL在2000年初被提出來之后,就不乏文章提到資源,契約規范,CRUD對應增刪改查操作等等。下面筆者從兩個實際的接口來聊聊自己的看法。
根據id查找用戶接口:
@FeignClient("user") public interface UserApi { @RequestMapping(value = "/user/{userId}",method = RequestMethod.GET) String findById(@PathVariable("id") String userId); }
這應該是沒有爭議的,注意前面強調的,@PathVariable(“id”)括號中的id不可以忘記。那如果是“根據郵箱查找用戶呢”?很有可能下意識的寫出這樣的接口:
@FeignClient("user") public interface UserApi { @RequestMapping(value = "/user/{email}",method = RequestMethod.GET) String findByEmail(@PathVariable("email") String email); }
首先看看Feign的問題。email中通常包含’.‘這個特殊字符,如果在路徑中包含,會出現意想不到的結果。我不想探討如何去解決它(實際上可以使用{email:.+}的方式),因為我覺得這不符合設計。
再談談規范的問題。這兩個接口是否是相似的,email是否應該被放到path中?這就要聊到RESTFUL的初衷,為什么userId這個屬性被普遍認為適合出現在RESTFUL路徑中,因為id本身起到了資源定位的作用,他是資源的標記。而email不同,它可能是唯一的,但更多的,它是資源的屬性,所以,筆者認為不應該在路徑中出現非定位性的動態參數。而是把email作為@RequestParam參數。
RESUFTL結構化查詢
筆者成功的從Feign的話題過度到了RESTFUL接口的設計問題,也導致了本文的篇幅變長了,不過也不打算再開一片文章談了。
再考慮一個接口設計,查詢某一個月某個用戶的訂單,可能還會攜帶分頁參數,這時候參數變得很多,按照傳統的設計,這應該是一個查詢操作,也就是與GET請求對應,那是不是意味着應當將這些參數拼接到url后呢?再思考Feign,正如本文的第二段所述,是不支持GET請求攜帶實體類的,這讓我們設計陷入了兩難的境地。而實際上參考一些DSL語言的設計如elasticSearch,也是使用POST JSON的方式來進行查詢的,所以在實際項目中,筆者並不是特別青睞CRUD與四種請求方式對應的這種所謂的RESTFUL規范,如果說設計RESTFUL應該遵循什么規范,那大概是另一些名詞,如契約規范和領域驅動設計。
@FeignClient("order") public interface BookApi { @RequestMapping(value = "/order/history",method = RequestMethod.POST) Page<List<Orders>> queryOrderHistory(@RequestBody QueryVO queryVO); }
RESTFUL行為限定
在實際接口設計中,我遇到了這樣的需求,用戶模塊的接口需要支持修改用戶密碼,修改用戶郵箱,修改用戶姓名,而筆者之前閱讀過一篇文章,也是講舍棄CRUD而是用領域驅動設計來規范RESTFUL接口的定義,與項目中我的想法不謀而合。看似這三個屬性是同一個實體類的三個屬性,完全可以如下設計:
@FeignClient("user") public interface UserApi { @RequestMapping(value = "/user",method = RequestMethod.POST) User update(@RequestBody User user); }
但實際上,如果再考慮多一層,就應該產生這樣的思考:這三個功能所需要的權限一致嗎?真的應該將他們放到一個接口中嗎?實際上,筆者並不希望接口調用方傳遞一個實體,因為這樣的行為是不可控的,完全不知道它到底是修改了什么屬性,如果真的要限制行為,還需要在User中添加一個操作類型的字段,然后在接口實現方加以校驗,這太麻煩了。而實際上,筆者覺得規范的設計應當如下:
@FeignClient("user") public interface UserApi { @RequestMapping(value = "/user/{userId}/password/update",method = RequestMethod.POST) ResultBean<Boolean> updatePassword(@PathVariable("userId) String userId,@RequestParam("password") password); @RequestMapping(value = "/user/{userId}/email/update",method = RequestMethod.POST) ResultBean<Boolean> updateEmail(@PathVariable("userId) String userId,@RequestParam("email") String email); @RequestMapping(value = "/user/{userId}/username/update",method = RequestMethod.POST) ResultBean<Boolean> updateUsername(@PathVariable("userId) String userId,@RequestParam("username") String username); }
-
一般意義上RESTFUL接口不應該出現動詞,這里的update並不是一個動作,而是標記着操作的類型,因為針對某個屬性可能出現的操作類型可能會有很多,所以我習慣加上一個update后綴,明確表達想要進行的操作,而不是僅僅依賴於GET,POST,PUT,DELETE。實際上,修改操作推薦使用的請求方式應當是PUT,這點筆者的理解是,已經使用update標記了行為,實際開發中不習慣使用PUT。
-
password,email,username都是user的屬性,而userId是user的識別符號,所以userId以PathVariable的形式出現在url中,而三個屬性出現在ReqeustParam中。
順帶談談邏輯刪除,如果一個需求是刪除用戶的常用地址,這個api的操作類型,我通常也不會設計為DELETE請求,而是同樣使用delete來標記操作行為
@RequestMapping(value = "/user/{userId}/address/{addressId}/delete",method = RequestMethod.POST)
ResultBean<Boolean> updateEmail(@PathVariable("userId") String userId,@PathVariable("userId") String email);
官方對feign有如下說明:https://github.com/OpenFeign/feign
Feign makes writing java http clients easier
Feign is a Java to HTTP client binder inspired by Retrofit, JAXRS-2.0, and WebSocket. Feign's first goal was reducing the complexity of binding Denominator uniformly to HTTP APIs regardless of ReSTfulness.
Feign 是一個聲明式 Web服務客戶端,使用它創建一個接口並注解,使得編寫 Web服務客戶端變得更加容易。
它支持可插拔注解,包括 Feign 注解和 JAX-RS 注解,還支持可插拔的編碼、解碼器。
Cloud 增加了對 Spring MVC 注解的支持,默認使用 httpmessageconverter 的支持。
Cloud 集成了 Ribbon 和 Eureka以及 BalanceLoad,使得在使用 Feign 時支持 Http 客戶端的負載均衡。
Feign 自定義的客戶端配置不需要使用 @Configuration 配置注解。如果使用了,需要排除在 @ComponentScan 注解掃描之外。否則,它將成為 feign.Encoder、feign.Decoder、feign.Contract等組件的默認實現。如果指定了,可以在 @ComponentScan 中顯示排除。
參考文章:
1. https://blog.csdn.net/qq_38152400/article/details/109597676
2. https://blog.csdn.net/pc8650/article/details/108749590
feign的介紹與使用:
1.https://blog.csdn.net/xianghe_qqq/article/details/109693420
2.https://blog.csdn.net/liu_ares/article/details/103599418
3.https://blog.csdn.net/weixin_42794008/article/details/106881431
feign搭建:
1. https://blog.csdn.net/weixin_38312502/article/details/105920219