Spring-MultiRequestBody icon indicating copy to clipboard operation
Spring-MultiRequestBody copied to clipboard

无法支持 @Validated 注解

Open big-camel opened this issue 5 years ago • 4 comments

@PostMapping("/register")
    public Result register(@Validated(UserInfoModel.RegisterAction.class) @MultiRequestBody UserInfoModel userInfoModel, @MultiRequestBody @NotBlank String code){
        System.out.println(userInfoModel);
        return new Result(0,"test");
    }

在方法中添加 @Validated 注解 无法执行验证,有什么解决办法吗?

big-camel avatar Nov 26 '19 14:11 big-camel

这个问题我今天找到了解决方案,记录一下 在 RequestBody 中有继承 AbstractMessageConverterMethodArgumentResolver ,里面有两个方法

/**
	 * Validate the binding target if applicable.
	 * <p>The default implementation checks for {@code @javax.validation.Valid},
	 * Spring's {@link org.springframework.validation.annotation.Validated},
	 * and custom annotations whose name starts with "Valid".
	 * @param binder the DataBinder to be used
	 * @param parameter the method parameter descriptor
	 * @since 4.1.5
	 * @see #isBindExceptionRequired
	 */
	protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
		Annotation[] annotations = parameter.getParameterAnnotations();
		for (Annotation ann : annotations) {
			Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
			if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
				Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
				Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
				binder.validate(validationHints);
				break;
			}
		}
	}

	/**
	 * Whether to raise a fatal bind exception on validation errors.
	 * @param binder the data binder used to perform data binding
	 * @param parameter the method parameter descriptor
	 * @return {@code true} if the next method argument is not of type {@link Errors}
	 * @since 4.1.5
	 */
	protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter parameter) {
		int i = parameter.getParameterIndex();
		Class<?>[] paramTypes = parameter.getExecutable().getParameterTypes();
		boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1]));
		return !hasBindingResult;
	}

将它们复制到 MultiRequestBodyArgumentResolver 类中,然后在 resolveArgument 方法中,需要将之前获取参数的代码提取出来单独放在一个方法,最后:

/**
     * 参数解析,利用fastjson
     * 注意:非基本类型返回null会报空指针异常,要通过反射或者JSON工具类创建一个空对象
     */
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        Object arg = readWithMessage(webRequest,parameter);
        String name = parameter.getParameterName();
        if (binderFactory != null) {
            WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
            if (arg != null) {
                validateIfApplicable(binder, parameter);
                if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
                    throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
                }
            }
            if (mavContainer != null) {
                mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
            }
        }
        return arg;
    }

这样就可以 @MultiRequestBody 和 @Validated 一起使用了

@PostMapping("/register")
    public Result register(@Validated(UserInfoModel.RegisterAction.class) @MultiRequestBody UserInfoModel userInfoModel,@MultiRequestBody @NotBlank String code){
        System.out.println(userInfoModel);
        System.out.println(code);
        return new Result(0,"test");
    }

我也尝试过去继承 AbstractMessageConverterMethodArgumentResolver 这个类,但是里面要配置消息解析器还有返回值等很多东西,水平有限很多也不明白,所以单独把那两个方法提取出来比较好用

big-camel avatar Nov 27 '19 08:11 big-camel

还有 @RequestBody 和 @MultiRequestBody 一起使用会报错,因为使用这两个注解会调用 getInputStream 获取 HttpServletRequest 中传过来的数据,而 getInputStream 只能读取一次,然后就标记为不可读取了,两个注解一起使用,第二次去 getInputStream 的时候就会报错。解决办法:

先将第一次读取到 request 中的值记录下来:

package com.itellyou.api.handler;

import org.apache.commons.io.IOUtils;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

public class HttpServletRequestWrapperHandler extends HttpServletRequestWrapper {
    private final String body;
    /**
     * Constructs a request object wrapping the given request.
     *
     * @param request The request to wrap
     * @throws IllegalArgumentException if the request is null
     */
    public HttpServletRequestWrapperHandler(HttpServletRequest request) throws IOException {
        super(request);
        this.body = IOUtils.toString(request.getReader());
    }

    public String getBody() {
        return body;
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream bais = new ByteArrayInputStream(body.getBytes());
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener listener) {

            }

            @Override
            public int read() throws IOException {
                return bais.read();
            }
        };
    }
}

然后使用过滤器将上面的类实例化

package com.itellyou.api.handler;

import org.springframework.context.annotation.Configuration;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@WebFilter(filterName="FilterHandler",urlPatterns="/*")
@Configuration
public class FilterHandler implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        //取Body数据
        HttpServletRequestWrapperHandler requestWrapper = new HttpServletRequestWrapperHandler(request);
        //TODO something
        chain.doFilter(requestWrapper != null ? requestWrapper : request,servletResponse);
    }
}

这样就记录好 request 中的值了 以下使用就不会报错了:

@PostMapping("/register")
    public Result register(@Validated(UserInfoModel.RegisterAction.class) @RequestBody UserInfoModel userInfoModel,@MultiRequestBody @NotBlank String code){
        System.out.println(userInfoModel);
        System.out.println(code);
        return new Result(0,"test");
    }

big-camel avatar Nov 27 '19 08:11 big-camel

可以写下测试代码,测试通过后提下PR

chujianyun avatar Nov 30 '19 09:11 chujianyun

上个PR?

lunxian8 avatar May 18 '20 09:05 lunxian8