美文网首页
SpringBoot 集成Hibernate Validator

SpringBoot 集成Hibernate Validator

作者: CarolineXi | 来源:发表于2018-04-19 09:33 被阅读0次

工程是用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 user, BindingResult result) {

        return new Result("0", "登陆成功!");

    }

}

Entity就不写了,启动工程之后, 打开 http://localhost:7070/swagger-ui.html/ 显示成了这样式儿的

image

我晕,上网查了好多,根本没有跟我这个问题相关的,于是呢,我就把“implements UserController"这句删了,不让它实现接口,然后把@PostMapping注解挪到实现类里面,重启,就好用了。。。好用了。。用了。了。。。

这是为啥呢,我不甘心,又尝试了一下,回退成有问题那样,然后把方法里的BindingResult参数给删了,里面用到result对象的代码也都注释掉了,不让它报错,再重启, 又好用了。。。好用了。。用了。了。。。

我去,难道是controller实现接口的话方法声明中不能有接口类型的参数吗(BindingResult是个接口), 于是我又自己定义了一个接口IResult, 把它当做参数传给login方法,结果呢,呵呵,果然不好用

我又把这个参数改成了类类型的比如String,这种就好用

难道我要是想用BindingResult的话必须controller不能实现接口吗??这俩有几毛钱的关系啊?

这时我的内心是这样的

image

哪位大神给我解答一下,不胜感激。。。

-----------------------我是一条华丽丽的分割线----------------------------------

问题已经解决了,通过研究了一下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更直观的看到

image

我用的beyond Compare对比了两次的启动log发现了这个猫腻, 人家明明白白告诉你了,我没有注入你这个login的mapping

于是我就想知道为啥,我到底哪错了,你告诉我,我改还不行么。。。

要想知道为啥,就得看源码了,我在log里面发现了这个类AbstractHandlerMethodMapping, 在这个类的initHandlerMethods方法上加了断点,大家可以看源码截图

image

为什么要在这加断点?因为我debug时发现只有在获取UserControllerImpl的时候得到的beanType是这个奇怪的东东(spring启动时其他所有bean都能正确加载,即获取到正确的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 [com.caroline.controller.UserControllerImpl@31a2a9fa]];

proxyTargetClass=false; optimize=false; opaque=false; exposeProxy=false; frozen=false

如果你有耐心看完应该就知道了,因为这里面的约束起了作用,get不到正确的beanType, 所以返回了上面那个奇怪的东东。

其实原因在这句

expression [execution(* com.caroline.controller.*.*(..)) && args(..,bindingResult)];

这个切面表达式写的有问题,上网查了一下,像我上面那种写法只能扫到controller的子包及其子包的类,是扫不到controller下面的类的,而我的类呢?

image image

悲剧,所以spring就没办法注入这个userControllerImpl的bean了。。。 解决方法就是改成这样

expression [execution(* com.caroline.*.*(..)) && args(..,bindingResult)];

完美解决!!!

image

码了这么多字是给我自己做个笔记,也希望帮助到大家

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 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参数啦

image

这个是截图,因为代码显示不出消除线,想copy代码的可以看上面喔。

虽然我很不服气,但是不得不承认他这种方式对代码的侵入性更小,我要向他学习~!

以上代码可以在github中下载

移步github走起:https://github.com/CarolineHuang5954/Validator.git

如有问题欢迎讨论!~~

相关文章

网友评论

      本文标题:SpringBoot 集成Hibernate Validator

      本文链接:https://www.haomeiwen.com/subject/zubikftx.html