訪問RestController報404錯誤
工程是用Springboot實現, 想要實現請求中的實體類的基本校驗,用的是hibernate的 Validator, 用Swagger2構建RestAPI文件 問題是這樣的,
有個controller是個介面:
public interface UserController {
@PostMapping("/login")
Result login(User user, BindingResult result);
}
一個實現類實現了它
@RestController @Api(value = "測試登陸介面") public class UserControllerImpl implements UserController{ @ApiOperation(value = "登陸") @PostMapping("/login") public Result login(@RequestBody @Valid User<ChildUser> user, BindingResult result) { return new Result("0", "登陸成功!"); } }
Entity就不寫了,啟動工程之後, 開啟 http://localhost:7070/swagger-ui.html/ 顯示成了這樣式兒的
我暈,上網查了好多,根本沒有跟我這個問題相關的,於是呢,我就把“implements UserController"這句刪了,不讓它實現介面,然後把@PostMapping註解挪到實現類裡面,重啟,就好用了。。。好用了。。用了。了。。。
這是為啥呢,我不甘心,又嘗試了一下,回退成有問題那樣,然後把方法裡的BindingResult引數給刪了,裡面用到result物件的程式碼也都註釋掉了,不讓它報錯,再重啟, 又好用了。。。好用了。。用了。了。。。
我去,難道是controller實現介面的話方法宣告中不能有介面型別的引數嗎(BindingResult是個介面), 於是我又自己定義了一個介面IResult, 把它當做引數傳給login方法,結果呢,呵呵,果然不好用
我又把這個引數改成了類型別的比如String,這種就好用
難道我要是想用BindingResult的話必須controller不能實現介面嗎??這倆有幾毛錢的關係啊?
這時我的內心是這樣的
哪位大神給我解答一下,不勝感激。。。
-----------------------我是一條華麗麗的分割線----------------------------------
問題已經解決了,通過研究了一下spring的核心程式碼,現在總結一下,希望能幫到跟我一樣遇到這種問題的童鞋
上面的程式碼其實沒有描述完整,其實我加的@Valid註解細心的童鞋應該看出來我是想校驗這個入參,但是因為公司的controller介面方法是在太多了, 每一個方法裡面都加上if(result.hasErrors()){...}會瘋掉的,程式碼也不好看,於是我就想加個切面,
切面裡的方法是這樣式兒的
@Around("execution(* com.caroline.controller.*.*(..)) && args(..,bindingResult)")
public ErrorResult doAround(ProceedingJoinPoint pjp, BindingResult bindingResult) throws Throwable {
ErrorResult retVal = new ErrorResult();
if (bindingResult.hasErrors()) {
retVal = doErrorHandle(bindingResult);
} else {
retVal = pjp.proceed();
}
return retVal;
}
在這裡面統一處理BindingResult. 但是啟動的時候spring根本沒有注入我的controller,大家可以通過log更直觀的看到
我用的beyond Compare對比了兩次的啟動log發現了這個貓膩, 人家明明白白告訴你了,我沒有注入你這個login的mapping
於是我就想知道為啥,我到底哪錯了,你告訴我,我改還不行麼。。。
要想知道為啥,就得看原始碼了,我在log裡面發現了這個類AbstractHandlerMethodMapping, 在這個類的initHandlerMethods方法上加了斷電,大家可以看原始碼截圖
為什麼要在這加斷點?因為我debug時發現只有在獲取UserControllerImpl的時候得到的beanType是這個奇怪的東東
com.sun.proxy.$Proxy71
這是啥?調查發現,會返回這個東西,是因為Spring通過註解發現了我這個controller它不是一般的controller,裡面用了@Valid註解需要校驗,還用了切面,要去切面的註解裡面查一些約束的東西
org.springframework.aop.framework.ProxyFactory: 1 interfaces [com.caroline.controller.UserController];
2 advisors [org.springframework.aop.interceptor.ExposeInvocationInterceptor.ADVISOR,
InstantiationModelAwarePointcutAdvisor:
expression [execution(* com.caroline.controller.*.*(..)) && args(..,bindingResult)];
advice method [public com.caroline.Entity.ErrorResult
com.caroline.interceptor.ControllerValidatorInterceptor.doAround(org.aspectj.lang.ProceedingJoinPoint,org.springframework.validation.BindingResult)
throws java.lang.Throwable]; perClauseKind=SINGLETON];
targetSource [SingletonTargetSource for target object [[email protected]]];
proxyTargetClass=false; optimize=false; opaque=false; exposeProxy=false; frozen=false
如果你有耐心看完應該就知道了,因為這裡面的約束起了作用,get不到正確的beanType, 所以返回了上面那個奇怪的東東。
其實原因我已經標出來了,是那個切面表示式寫的有問題,上網查了一下,像我上面那種寫法只能掃到controller的子包及其子包的類,是掃不到controller下面的類的,而我的類呢?
悲劇,所以spring就沒辦法注入這個userControllerImpl的bean了。。。 解決方法就是改成這樣
expression [execution(* com.caroline.controller.*.*(..)) && args(..,bindingResult)];
完美解決!!!
碼了這麼多字是給我自己做個筆記,也希望幫助到大家
PS: 最近總是遇到包名的問題還有bean name格式的問題
比如
public class EntityA{
private EntityB entityB;//不要寫成eb這樣,最好是型別copy一下然後首字母小寫
}
可能例子舉得不太好,但是意思應該明白,如果是因為命名引起的問題調查半天是很崩潰的,我自己就遇到過,太低階了
下回寫一篇我在用Hibernate Validator的遇到的問題吧,先立個flag。
-------------------------------------我還是那條華麗的分割線--------------------------------------------------------------
今天續更,我本來已經解決了這個問題,然後我們組另外一個開發用了另一種方式實現了目的,說是我的方法太繁瑣。。。咳咳好吧,在每個controller方法裡面加一個BindingResult引數確實繁瑣。 下面我說一下這種方式具體的實現。
如果controller裡面沒有BindingResult這個引數,那麼如果校驗失敗,會丟擲一個這樣的異常,叫這個
org.springframework.web.bind.MethodArgumentNotValidException
這個post request 的返回值差不多是這樣
{
"timestamp": 1523589643865,
"status": 400,
"error": "Bad Request",
"exception": "org.springframework.web.bind.MethodArgumentNotValidException",
"errors": [
{
"codes": [
"NotBlank.user.childUser.childUsername",
"NotBlank.childUser.childUsername",
"NotBlank.childUsername",
"NotBlank.java.lang.String",
"NotBlank"
],
"arguments": [
{
"codes": [
"user.childUser.childUsername",
"childUser.childUsername"
],
"arguments": null,
"defaultMessage": "childUser.childUsername",
"code": "childUser.childUsername"
}
],
"defaultMessage": "子使用者名稱不能為空",
"objectName": "user",
"field": "childUser.childUsername",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotBlank"
},
{
"codes": [
"NotBlank.user.password",
"NotBlank.password",
"NotBlank.java.lang.String",
"NotBlank"
],
"arguments": [
{
"codes": [
"user.password",
"password"
],
"arguments": null,
"defaultMessage": "password",
"code": "password"
}
],
"defaultMessage": "使用者名稱不能為空",
"objectName": "user",
"field": "password",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotBlank"
}
],
"message": "Validation failed for object='user'. Error count: 2",
"path": "/login"
}
其實已經有了error message了,我們想要的就是需要定義一個全域性處理異常的切面類,在裡面加工一下這個exception,返回一個我們需要的json串
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public Result beanValidation(Exception exception){
if (exception instanceof MethodArgumentNotValidException) {
MethodArgumentNotValidException e = (MethodArgumentNotValidException)exception;
User req = (User) e.getBindingResult().getTarget();
final List<String> errors = e.getBindingResult().getFieldErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(toList());
return new Result("-1", errors.toString());
}
return new Result("-99", "未知異常");
}
}
這樣就可以了,重啟之後傳送同樣的訊息,得到如下結果
{
"code": "-1",
"message": "[使用者名稱不能為空, 子使用者名稱不能為空]"
}
之前我們的ControllerImpl就可以省去BindingResult引數啦
這個是截圖,因為程式碼顯示不出消除線,想copy程式碼的可以看上面喔。
雖然我很不服氣,但是不得不承認他這種方式對程式碼的侵入性更小,我要向他學習~!
以上程式碼可以在github中下載
如有問題歡迎討論!~~