工程是用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/ 显示成了这样式儿的
我晕,上网查了好多,根本没有跟我这个问题相关的,于是呢,我就把“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是这个奇怪的东东(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下面的类的,而我的类呢?
悲剧,所以spring就没办法注入这个userControllerImpl的bean了。。。 解决方法就是改成这样
expression [execution(* com.caroline.*.*(..)) && 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 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中下载
移步github走起:https://github.com/CarolineHuang5954/Validator.git
如有问题欢迎讨论!~~