認識Spring引數解析器

神易風發表於2022-04-12
文章出處shenyifengtk.github.io 轉載請註明

使用Spring MVC開發的,應該都使用過@RequstBody接收json引數,轉換成pojo物件,非常方便阿,但是功能並不是很全面,有點點瑕疵,並不能支援json key 方式注入到String、Integer 這類型別物件。

前端傳值後端接收結果
{"id": 3,"name":"xxx"}User(id,name)成功注入
{"id": 3,"name":"xxx"}(String name,int id)並不支援這個方式

有時候一個介面只有兩個引數上傳,我們並不想為此建立一個單獨的物件,這樣可能會導致每一個hangdler 方法可能都存在一個pojo物件,大部分物件可能並不能被其他handler 共享。 而是希望使用一兩個引數就可以接收到。先看下@RequestBody 這個註解如何實現將json 轉換成pojo物件的,通過spring HandlerMethodArgumentResolver 引數解析器來實現的,瞭解下介面。

public interface HandlerMethodArgumentResolver {

    /**
     *   是否支援方法上引數的處理,只有返回ture,才會執行下面方法
     * @param parameter the method parameter to check
     * @return {@code true} if this resolver supports the supplied parameter;
     * {@code false} otherwise
     */
    boolean supportsParameter(MethodParameter parameter);

    /**
     * 將方法引數解析為給定請求的引數值
     * A {@link ModelAndViewContainer} provides access to the model for the
     * request. A {@link WebDataBinderFactory} provides a way to create
     * a {@link WebDataBinder} instance when needed for data binding and
     * type conversion purposes.
     * @param parameter the method parameter to resolve. This parameter must
     * have previously been passed to {@link #supportsParameter} which must
     * have returned {@code true}.
     * @param mavContainer the ModelAndViewContainer for the current request
     * @param webRequest the current request
     * @param binderFactory a factory for creating {@link WebDataBinder} instances
     * @return the resolved argument value, or {@code null} if not resolvable
     * @throws Exception in case of errors with the preparation of argument values
     */
    @Nullable
    Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;

}

RequestMappingHandlerAdapter下可以看見預設引數解析器

    private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
        List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(30);

        // Annotation-based argument resolution
        resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));  //@RequestPart  檔案注入
        resolvers.add(new RequestParamMapMethodArgumentResolver()); //@RequestParam 
        resolvers.add(new PathVariableMethodArgumentResolver()); //@PathVariable
        resolvers.add(new PathVariableMapMethodArgumentResolver()); //@PathVariable 會返回一個Map物件
        resolvers.add(new MatrixVariableMethodArgumentResolver()); //@MatrixVariable
        resolvers.add(new MatrixVariableMapMethodArgumentResolver()); //MatrixVariable 會返回Map 物件
        resolvers.add(new ServletModelAttributeMethodProcessor(false)); //屬性板頂
        resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice)); //@RequestBody 後面重點講解
        resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice)); //@RequestPart
        resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory())); // @RequestHeader
        resolvers.add(new RequestHeaderMapMethodArgumentResolver()); //@RequestHeader Map物件
        resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory())); //Cookie 值 注入
        resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory())); //@Value
        resolvers.add(new SessionAttributeMethodArgumentResolver()); //@SessionAttribute
        resolvers.add(new RequestAttributeMethodArgumentResolver()); //@RequestAttribute

        // Type-based argument resolution
        resolvers.add(new ServletRequestMethodArgumentResolver());  //servlet api物件 HttpServletRequest  這類
        resolvers.add(new ServletResponseMethodArgumentResolver()); //ServletResponse 物件注入
        resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice)); //RequestEntity、HttpEntity
        resolvers.add(new RedirectAttributesMethodArgumentResolver()); //重定向
        resolvers.add(new ModelMethodProcessor()); //返回Model 物件
        resolvers.add(new MapMethodProcessor()); // 處理方法引數返回一個Map
        resolvers.add(new ErrorsMethodArgumentResolver()); //處理錯誤方法引數,返回最後一個物件
        resolvers.add(new SessionStatusMethodArgumentResolver()); //SessionStatus
        resolvers.add(new UriComponentsBuilderMethodArgumentResolver());  //UriComponentsBuilder
        if (KotlinDetector.isKotlinPresent()) {
            resolvers.add(new ContinuationHandlerMethodArgumentResolver());
        }

        // Custom arguments 自定義
        if (getCustomArgumentResolvers() != null) {
            resolvers.addAll(getCustomArgumentResolvers());
        }

        // Catch-all
        resolvers.add(new PrincipalMethodArgumentResolver()); 
        resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
        resolvers.add(new ServletModelAttributeMethodProcessor(true));

        return resolvers;
    }

可以看見大部分方法引數都是通過上面處理器去實現的,重點放在RequestResponseBodyMethodProcessor 如何注入json的,主要看兩個方法如何實現的。

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
                //只有使用了@RequestBody 註解預設就開啟處理
        return parameter.hasParameterAnnotation(RequestBody.class); 
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
            NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

        parameter = parameter.nestedIfOptional();
              //轉換成物件了
        Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
               //獲取引數變數名稱
        String name = Conventions.getVariableNameForParameter(parameter);

        if (binderFactory != null) { //使用WebDatabinder 對已經序列化物件屬性繫結處理
            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 adaptArgumentIfNecessary(arg, parameter);
    }

將json 序列號成物件實現都是在readWithMessageConverters 方法中

    protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
            Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {

        MediaType contentType;
        boolean noContentType = false;
        try {
            contentType = inputMessage.getHeaders().getContentType();
        }
        catch (InvalidMediaTypeException ex) {
            throw new HttpMediaTypeNotSupportedException(ex.getMessage());
        }
        if (contentType == null) {
            noContentType = true;
            contentType = MediaType.APPLICATION_OCTET_STREAM;
        }

        Class<?> contextClass = parameter.getContainingClass();
        Class<T> targetClass = (targetType instanceof Class ? (Class<T>) targetType : null);
        if (targetClass == null) {
            ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
            targetClass = (Class<T>) resolvableType.resolve();
        }

        HttpMethod httpMethod = (inputMessage instanceof HttpRequest ? ((HttpRequest) inputMessage).getMethod() : null);
        Object body = NO_VALUE;

        EmptyBodyCheckingHttpInputMessage message;
        try {
                        //建立重複讀寫流
            message = new EmptyBodyCheckingHttpInputMessage(inputMessage); 
                       //通過HttpMessageConverter 來對String json 轉換成物件
            for (HttpMessageConverter<?> converter : this.messageConverters) {
                Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
                GenericHttpMessageConverter<?> genericConverter =
                        (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
                if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
                        (targetClass != null && converter.canRead(targetClass, contentType))) {
                    if (message.hasBody()) {
                        HttpInputMessage msgToUse =
                                getAdvice().beforeBodyRead(message, parameter, targetType, converterType); //前置處理
                        body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
                                ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
                        body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType); //後置處理 型別通知
                    }
                    else {
                        body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
                    }
                    break;
                }
            }
        }
        catch (IOException ex) {
            throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
        }
                //縮減部分程式碼
        return body;
    }

http 請求內容呼叫GenericHttpMessageConverter read 方法轉換成物件,這時呼叫的是AbstractJackson2HttpMessageConverter

    private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {
        MediaType contentType = inputMessage.getHeaders().getContentType();
        Charset charset = getCharset(contentType);

        ObjectMapper objectMapper = selectObjectMapper(javaType.getRawClass(), contentType);
        Assert.state(objectMapper != null, "No ObjectMapper for " + javaType);

        boolean isUnicode = ENCODINGS.containsKey(charset.name()) ||
                "UTF-16".equals(charset.name()) ||
                "UTF-32".equals(charset.name());
        try {
            if (inputMessage instanceof MappingJacksonInputMessage) { //這個是使用了JsonView 
                Class<?> deserializationView = ((MappingJacksonInputMessage) inputMessage).getDeserializationView();
                if (deserializationView != null) {
                    ObjectReader objectReader = objectMapper.readerWithView(deserializationView).forType(javaType);
                    if (isUnicode) {
                        return objectReader.readValue(inputMessage.getBody());
                    }
                    else {
                        Reader reader = new InputStreamReader(inputMessage.getBody(), charset);
                        return objectReader.readValue(reader);  //反序列化成Java 物件
                    }
                }
            }
            if (isUnicode) {
                return objectMapper.readValue(inputMessage.getBody(), javaType);
            }
            else {
                Reader reader = new InputStreamReader(inputMessage.getBody(), charset);
                return objectMapper.readValue(reader, javaType);
            }
        }
        catch (InvalidDefinitionException ex) {
            throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
        }
        catch (JsonProcessingException ex) {
            throw new HttpMessageNotReadableException("JSON parse error: " + ex.getOriginalMessage(), ex, inputMessage);
        }
    }

可以看見@RequestBody 是通過GenericHttpMessageConverter class 呼叫ObjectMapper readValue 進行轉換的,只能將json轉換成pojo 物件, 並不支援簡單型別 String、Integer、Long這類轉換的,有著天然缺陷。如果想將json key 注入到引數,需要手動實現一個引數解析器,下面展示簡單實現程式碼。

動手實現

目標: 我們希望建立一個類似@RequestBody的註解,標註當前引數支援json key直接注入,這個註解也可以直接在方法修飾,表示整個方法引數都是用註解,當然也可以支援整個類,表示所有方法的引數都支援。在實現一個對應註解的引數解析器。

建立註解類

@Target({ElementType.PARAMETER,ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface JsonKeyValue {

    /**
     *  json key 如何沒有則使用 型別變數名
     * @return
     */
    @AliasFor("name")
    String value() default "";

    @AliasFor("value")
    String name() default "";
}

實現引數解析器

public class RequestJsonKeyValueMethodProcessor  implements HandlerMethodArgumentResolver {

    private ObjectMapper objectMapper ;

    public RequestJsonKeyValueMethodProcessor(ObjectMapper objectMapper){
        this.objectMapper = objectMapper;
    }

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        boolean b = parameter.hasParameterAnnotation(JsonKeyValue.class);
        if (!b) {
            JsonKeyValue value = parameter.getMethodAnnotation(JsonKeyValue.class);  //從方法上找註解
            b = value != null;
            if (!b){
                value = parameter.getContainingClass().getAnnotation(JsonKeyValue.class); //從類上找註解
                b = value != null;
            }
        }
        return b;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        parameter = parameter.nestedIfOptional();
        HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
        Assert.state(servletRequest != null, "No HttpServletRequest");
        ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);
        MediaType contentType = inputMessage.getHeaders().getContentType();
        Charset charset = getCharset(contentType);
        Reader reader = new InputStreamReader(inputMessage.getBody(), charset);
        JsonNode jsonNode = objectMapper.readTree(reader);
        String parameterName = parameterName(parameter);
        JsonNode path = jsonNode.path(parameterName);
        Object o = objectMapper.convertValue(path, parameter.getNestedParameterType());
        return o;
    }


    private String parameterName(MethodParameter parameter){
        JsonKeyValue annotation = parameter.getParameterAnnotation(JsonKeyValue.class);
        if (annotation != null){
            String name = annotation.name();
            if (StringUtils.hasText(name))
                return annotation.name();
        }
        return parameter.getParameterName();
    }

    //抄襲  AbstractMessageConverterMethodArgumentResolver  主要是防止將流讀入後,controller 方法不能在讀了
    private static class EmptyBodyCheckingHttpInputMessage implements HttpInputMessage {

        private final HttpHeaders headers;

        @Nullable
        private final InputStream body;

        public EmptyBodyCheckingHttpInputMessage(HttpInputMessage inputMessage) throws IOException {
            this.headers = inputMessage.getHeaders();
            InputStream inputStream = inputMessage.getBody();
            if (inputStream.markSupported()) {
                inputStream.mark(1);
                this.body = (inputStream.read() != -1 ? inputStream : null);
                inputStream.reset();
            }
            else {
                PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream);
                int b = pushbackInputStream.read();
                if (b == -1) {
                    this.body = null;
                }
                else {
                    this.body = pushbackInputStream;
                    pushbackInputStream.unread(b);
                }
            }
        }

        @Override
        public HttpHeaders getHeaders() {
            return this.headers;
        }

        @Override
        public InputStream getBody() {
            return (this.body != null ? this.body : StreamUtils.emptyInput());
        }

        public boolean hasBody() {
            return (this.body != null);
        }
    }

    private Charset getCharset(@Nullable MediaType contentType) {
        if (contentType != null && contentType.getCharset() != null) {
            return contentType.getCharset();
        }
        else {
            return StandardCharsets.UTF_8;
        }
    }
}

新增自定義引數解析器

@EnableWebMvc
@Configuration
public class WebMvcConfig implements WebMvcConfigurer{

    private Logger logger = LoggerFactory.getLogger(WebMvcConfig.class);

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new RequestJsonKeyValueMethodProcessor(objectMapper));
    }
}

這裡不用擔心自定義引數解析器會覆蓋spring 原生的解析器,這裡會將addArgumentResolvers 新增解析器自動放入到CustomArgumentResolvers中。
一個簡單自定義引數解析器就完成了,支援普通類的String、Integer 這些引數的解析。注意的是這個引數解析器並沒有考慮引數為空的處理,Optional 這類情況只滿足一些簡單、快速開發的場景。不知道各位同行是否在日常開發中有沒有使用到這些知識,我公司的基礎框架就是使用這些技術來做快速開發的,對開發效率提升特別大的。

相關文章