1. 程式人生 > >訪問RestController報404錯誤

訪問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中下載

如有問題歡迎討論!~~