• SpingMVC参数解析过程


      SpringMVC 可以用@PathVariable、@RequestParam等来接收不同的参数类型。下面研究其参数解析过程,不管是何种参数方式,都是从Request 中拿的参数。

    1. 测试Controller

    代码如下:

    User 类:

    package com.xm.ggn.test;
    
    import lombok.Data;
    
    @Data
    public class User {
    
        private String username;
    
        private String fullname;
    
        private String createDate;
    }

    Controller 接口:

        @PostMapping("/user/add/{userId}")
        public User addUser(@RequestBody User user, @PathVariable String userId, @RequestParam String username, User user2) {
            System.out.println(user);
            System.out.println(userId);
            System.out.println(username);
            System.out.println(user2);
            return user;
        }

    测试访问:

    curl -X POST --header 'Content-Type: application/json' -d '{"username": "lisi", "fullname": "lisi"}' 'http://localhost:8088/user/add/3?username=zs'
      % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                     Dload  Upload   Total   Spent    Left  Speed
      0     0    0     0    0     0      0      0 --:--:-- --:100   150    0   110  100    40   2340    851 --:--:-- --:--:-- --:--:--  6875{"success":true,"data":{"username":"lisi","fullname":"lisi","createDate":null},"msg":"成功","errorCode":"0"}

    2. 分析其解析过程

    1. SpringMVC 入口是在方法:org.springframework.web.servlet.DispatcherServlet#doDispatch, 其大致流程如下:

    (1)  获取一个处理器执行器链: HandlerExecutionChain (包含Handler 和Inteceptors)。 org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandler

    (2)  遍历处理器链获取一个HandlerAdapter 处理器适配器, 适配的类型是org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter。 

    (3)  调用到方法开始处理:org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#invokeHandlerMethod

    2. 接下来研究处理过程的参数解析环节

    1. org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter#handle

    2. 调用到 org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#handleInternal

        @Override
        protected ModelAndView handleInternal(HttpServletRequest request,
                HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
    
            ModelAndView mav;
            checkRequest(request);
    
            // Execute invokeHandlerMethod in synchronized block if required.
            if (this.synchronizeOnSession) {
                HttpSession session = request.getSession(false);
                if (session != null) {
                    Object mutex = WebUtils.getSessionMutex(session);
                    synchronized (mutex) {
                        mav = invokeHandlerMethod(request, response, handlerMethod);
                    }
                }
                else {
                    // No HttpSession available -> no mutex necessary
                    mav = invokeHandlerMethod(request, response, handlerMethod);
                }
            }
            else {
                // No synchronization on session demanded at all...
                mav = invokeHandlerMethod(request, response, handlerMethod);
            }
    
            if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
                if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
                    applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
                }
                else {
                    prepareResponse(response);
                }
            }
    
            return mav;
        }

    3. org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#invokeHandlerMethod

    4. org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#invokeAndHandle

    5. org.springframework.web.method.support.InvocableHandlerMethod#invokeForRequest

        @Nullable
        public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
            Object[] args = this.getMethodArgumentValues(request, mavContainer, providedArgs);
            if (this.logger.isTraceEnabled()) {
                this.logger.trace("Arguments: " + Arrays.toString(args));
            }
    
            return this.doInvoke(args);
        }

    6. 参数解析:org.springframework.web.method.support.InvocableHandlerMethod#getMethodArgumentValues

        protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
            MethodParameter[] parameters = this.getMethodParameters();
            if (ObjectUtils.isEmpty(parameters)) {
                return EMPTY_ARGS;
            } else {
                Object[] args = new Object[parameters.length];
    
                for(int i = 0; i < parameters.length; ++i) {
                    MethodParameter parameter = parameters[i];
                    parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
                    args[i] = findProvidedArgument(parameter, providedArgs);
                    if (args[i] == null) {
                        if (!this.resolvers.supportsParameter(parameter)) {
                            throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
                        }
    
                        try {
                            args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
                        } catch (Exception var10) {
                            if (this.logger.isDebugEnabled()) {
                                String exMsg = var10.getMessage();
                                if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
                                    this.logger.debug(formatArgumentError(parameter, exMsg));
                                }
                            }
    
                            throw var10;
                        }
                    }
                }
    
                return args;
            }
        }

      可以看到参数解析是在这里,根据参数的个数,然后循环遍历获取参数信息。

    1》单个参数的解析过程如下:org.springframework.web.method.support.HandlerMethodArgumentResolverComposite#resolveArgument

        @Nullable
        public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
            HandlerMethodArgumentResolver resolver = this.getArgumentResolver(parameter);
            if (resolver == null) {
                throw new IllegalArgumentException("Unsupported parameter type [" + parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
            } else {
                return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
            }
        }

    获取到参数解析器,然后进行参数的解析,获取参数解析器的方法如下:

        private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
            HandlerMethodArgumentResolver result = (HandlerMethodArgumentResolver)this.argumentResolverCache.get(parameter);
            if (result == null) {
                Iterator var3 = this.argumentResolvers.iterator();
    
                while(var3.hasNext()) {
                    HandlerMethodArgumentResolver resolver = (HandlerMethodArgumentResolver)var3.next();
                    if (resolver.supportsParameter(parameter)) {
                        result = resolver;
                        this.argumentResolverCache.put(parameter, resolver);
                        break;
                    }
                }
            }
    
            return result;
        }

      可以看到先从缓存里面拿,如果缓存里面没有拿到,遍历MVC内置的一些解析器,然后判断是否支持该参数的解析,如果支持解析完存入缓存map,下次直接从缓存中拿。这个类内置了大概30个参数解析器,如下:

    主要的参数解析器为:

    RequestResponseBodyMethodProcessor - 解析@RequestBody和@ResponseBody 的参数

    PathVariableMethodArgumentResolver- 解析@PathVariable 注解的

    RequestParamMethodArgumentResolver - 解析@RequestParam 注解修饰的参数

    ServletModelAttributeMethodProcessor - 解析默认不加任何注解的参数以及解析@ModelAttribute 注解修饰的参数

    2》 然后开始解析参数调用的方法是参数解析器的resolveArgument 方法

    分析四个解析过程:

    (1)如果是RequestBody 修饰的参数:

    org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor#resolveArgument

        @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 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);
        }

    请求转交给父类:org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters(org.springframework.http.HttpInputMessage, org.springframework.core.MethodParameter, java.lang.reflect.Type)

        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);
    
                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);
            }
    
            if (body == NO_VALUE) {
                if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||
                        (noContentType && !message.hasBody())) {
                    return null;
                }
                throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
            }
    
            MediaType selectedContentType = contentType;
            Object theBody = body;
            LogFormatUtils.traceDebug(logger, traceOn -> {
                String formatted = LogFormatUtils.formatValue(theBody, !traceOn);
                return "Read "" + selectedContentType + "" to [" + formatted + "]";
            });
    
            return body;
        }

      这里实际就是读取请求体中的数据,然后转换成JSON对象。

      判断messageConverter 消息转换器是否能读取消息,对于普通的Bean 支持的转换器是org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; 然后处理消息,也就是读取请求体重的数据,然后转换成对应的java Bean。 注意这里会交给JackSON工具包的com.fasterxml.jackson.databind.ObjectMapper#_readMapAndClose 方法。

    (2) 如果是@PathVariable 修饰的参数: 请求交给org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver#resolveArgument

    父类会调用子类的org.springframework.web.servlet.mvc.method.annotation.PathVariableMethodArgumentResolver#resolveName 方法进行解析参数:

        protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
            Map<String, String> uriTemplateVars = (Map<String, String>) request.getAttribute(
                    HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
            return (uriTemplateVars != null ? uriTemplateVars.get(name) : null);
        }

      实际就是从参数模板的Map 中根据名称获取到一个参数的值

    (3) 如果是@RequestParam 注解修饰的参数:

    最后也是会打到org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver#resolveArgument

    请求打到:org.springframework.web.method.annotation.RequestParamMethodArgumentResolver#resolveName。

        protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
            HttpServletRequest servletRequest = (HttpServletRequest)request.getNativeRequest(HttpServletRequest.class);
            Object arg;
            if (servletRequest != null) {
                arg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
                if (arg != MultipartResolutionDelegate.UNRESOLVABLE) {
                    return arg;
                }
            }
    
            arg = null;
            MultipartRequest multipartRequest = (MultipartRequest)request.getNativeRequest(MultipartRequest.class);
            if (multipartRequest != null) {
                List<MultipartFile> files = multipartRequest.getFiles(name);
                if (!files.isEmpty()) {
                    arg = files.size() == 1 ? files.get(0) : files;
                }
            }
    
            if (arg == null) {
                String[] paramValues = request.getParameterValues(name);
                if (paramValues != null) {
                    arg = paramValues.length == 1 ? paramValues[0] : paramValues;
                }
            }
    
            return arg;
        }

      实际就是调用request.getParameterValues 获取到参数的值 

    (4) 如果没有注解修饰或者@ModelAttribute 修饰的参数:

    org.springframework.web.method.annotation.ModelAttributeMethodProcessor#resolveArgument

        public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
            Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer");
            Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory");
            String name = ModelFactory.getNameForParameter(parameter);
            ModelAttribute ann = (ModelAttribute)parameter.getParameterAnnotation(ModelAttribute.class);
            if (ann != null) {
                mavContainer.setBinding(name, ann.binding());
            }
    
            Object attribute = null;
            BindingResult bindingResult = null;
            if (mavContainer.containsAttribute(name)) {
                attribute = mavContainer.getModel().get(name);
            } else {
                try {
                    attribute = this.createAttribute(name, parameter, binderFactory, webRequest);
                } catch (BindException var10) {
                    if (this.isBindExceptionRequired(parameter)) {
                        throw var10;
                    }
    
                    if (parameter.getParameterType() == Optional.class) {
                        attribute = Optional.empty();
                    }
    
                    bindingResult = var10.getBindingResult();
                }
            }
    
            if (bindingResult == null) {
                WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
                if (binder.getTarget() != null) {
                    if (!mavContainer.isBindingDisabled(name)) {
                        this.bindRequestParameters(binder, webRequest);
                    }
    
                    this.validateIfApplicable(binder, parameter);
                    if (binder.getBindingResult().hasErrors() && this.isBindExceptionRequired(binder, parameter)) {
                        throw new BindException(binder.getBindingResult());
                    }
                }
    
                if (!parameter.getParameterType().isInstance(attribute)) {
                    attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
                }
    
                bindingResult = binder.getBindingResult();
            }
    
            Map<String, Object> bindingResultModel = bindingResult.getModel();
            mavContainer.removeAttributes(bindingResultModel);
            mavContainer.addAllAttributes(bindingResultModel);
            return attribute;
        }

    3.  自定义自己的参数解析器以及DataBinder 

     1. 声明一个注解用于从标记使用自定义的参数处理器

    package com.xm.ggn.test.paramresolver;
    
    import java.lang.annotation.*;
    
    /**
     * 标记Head中取值的注解
     */
    @Target({ElementType.PARAMETER})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface HeaderParam {
    
    }

    2. 编写一个参数处理器,模拟从Header 中获取到参数之后设置值

    package com.xm.ggn.test;
    
    import com.xm.ggn.test.paramresolver.HeaderParam;
    import org.apache.commons.lang3.ArrayUtils;
    import org.springframework.core.MethodParameter;
    import org.springframework.objenesis.instantiator.util.ClassUtils;
    import org.springframework.web.bind.support.WebDataBinderFactory;
    import org.springframework.web.context.request.NativeWebRequest;
    import org.springframework.web.method.support.HandlerMethodArgumentResolver;
    import org.springframework.web.method.support.ModelAndViewContainer;
    import sun.reflect.misc.FieldUtil;
    
    import javax.servlet.http.HttpServletRequest;
    import java.lang.reflect.Field;
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    
    public class HeaderParamMethodArgumentResolver implements HandlerMethodArgumentResolver {
    
        /**
         * 查看是否有
         *
         * @param parameter
         * @return
         */
        @Override
        public boolean supportsParameter(MethodParameter parameter) {
            return parameter.hasParameterAnnotation(HeaderParam.class);
        }
    
        /**
         * 解析参数: 从Header 中拿取属性
         *
         * @param parameter
         * @param mavContainer
         * @param webRequest
         * @param binderFactory
         * @return
         * @throws Exception
         */
        @Override
        public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
            HttpServletRequest servletRequest = (HttpServletRequest) webRequest.getNativeRequest(HttpServletRequest.class);
            Class<?> parameterType = parameter.getParameterType();
            String parameterName = parameter.getParameterName();
            // 如果是基本数据类型设置值之后返回
            if (isPrimitive(parameterType)) {
                return getArg(parameterType, servletRequest.getHeader(parameterName));
            }
    
            // 如果是String获取之后返回
            if (String.class.equals(parameterType)) {
                return servletRequest.getHeader(parameterName);
            }
    
            // 包装类型的话直接创造对象,然后遍历属性进行设置
            Field[] fields = FieldUtil.getDeclaredFields(parameterType);
            Object result = ClassUtils.newInstance(parameterType);
            Object arg = null;
            for (Field field : fields) {
                arg = servletRequest.getHeader(field.getName());
                if (arg == null) {
                    continue;
                }
                if (isPrimitive(field.getType())) {
                    Class<?> parType = field.getType();
                    arg = getArg(parType, arg);
                }
                if (arg == null) {
                    continue;
                }
                Method setter = getSetterMethod(parameterType, field);
                if (setter != null) {
                    setter.invoke(result, arg);
                }
            }
            return result;
        }
    
        /**
         * 获取到Set 方法
         *
         * @param clazz
         * @param field
         * @return
         * @throws NoSuchMethodException
         */
        private Method getSetterMethod(Class<?> clazz, Field field) throws NoSuchMethodException {
            return clazz.getDeclaredMethod("set" + toUpperCaseFirstOne(field.getName()), field.getType());
        }
    
        /**
         * 字段第一个转为大写
         *
         * @param fieldName
         * @return
         */
        private String toUpperCaseFirstOne(String fieldName) {
            if (Character.isUpperCase(fieldName.charAt(0))) {
                return fieldName;
            }
    
            return String.valueOf(Character.toUpperCase(fieldName.charAt(0))) + fieldName.substring(1);
        }
    
        private static final Class[] PRIMITIVE_CLAZZ = {Byte.class, Short.class, Integer.class, Long.class, Character.class, Boolean.class, Float.class, Double.class};
    
        /**
         * 是否是8种基本类型的包装类型
         *
         * @param clazz
         * @return
         */
        private boolean isPrimitive(Class<?> clazz) {
            return ArrayUtils.contains(PRIMITIVE_CLAZZ, clazz);
        }
    
        private Object getArg(Class<?> primitiveClass, Object value) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
            if (value == null) {
                return null;
            }
    
            // 获取到类型名称
            if (isPrimitive(primitiveClass)) {
                // 还可以用 String methodName = "parse" + firstUpperprimitiveName 构造方法名称; 然后用 clazz.getDeclaredMethod(methodName, String.class) 获取方法
                String primitiveName = primitiveClass.getSimpleName();
                switch (primitiveName) {
                    case "Byte":
                        return Byte.parseByte(primitiveName);
                    case "Short":
                        return Short.parseShort(primitiveName);
                    case "Integer":
                        return Integer.parseInt(primitiveName);
                    case "Long":
                        return Long.parseLong(primitiveName);
                    case "Character":
                        value.toString().charAt(0);
                    case "Boolean":
                        return Boolean.parseBoolean(primitiveName);
                    case "Float":
                        return Float.parseFloat(primitiveName);
                    case "Double":
                        return Double.parseDouble(primitiveName);
                }
            }
    
            return null;
        }
    
    }

    3. 注册到SpringMVC 的参数解析器列表中

    package com.xm.ggn.config;
    
    import com.xm.ggn.test.HeaderParamMethodArgumentResolver;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.Ordered;
    import org.springframework.web.method.support.HandlerMethodArgumentResolver;
    import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
    import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
    
    import java.util.List;
    
    /**
     * 1.JSON返回实体类报错2.设置页面的默认页面
     *
     * @author Administrator
     */
    @Configuration
    public class MVCConfig extends WebMvcConfigurerAdapter {
        /**
         * 解决JSON返回实体类报错
         */
        @Override
        public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
            configurer.favorPathExtension(false);
        }
    
        /**
         * 设置页面的默认页面
         */
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            registry.addViewController("/").setViewName("forward:/index.html");
            registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
            super.addViewControllers(registry);
        }
    
        /**
         * 将UserArgumentResolver将入到处理器队列中来
         *
         * @param argumentResolvers
         */
        @Override
        public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
            if (argumentResolvers.isEmpty()) {
                argumentResolvers.add(0, new HeaderParamMethodArgumentResolver());
            } else {
                argumentResolvers.set(0, new HeaderParamMethodArgumentResolver());
            }
        }
    }

    4. 编写测试Controller

        @PostMapping("/user/add/{userId}")
        public User addUser(@RequestBody User user, @PathVariable String userId, @RequestParam String username, @HeaderParam User user2, @HeaderParam String headKey, User user3) {
            System.out.println(user);
            System.out.println(userId);
            System.out.println(username);
            System.out.println(user2);
            System.out.println(headKey);
            System.out.println(user3);
            return user;
        }

      上面据完成了一个简单的自定义参数解析器,从header 中读取属性。

    【当你用心写完每一篇博客之后,你会发现它比你用代码实现功能更有成就感!】
  • 相关阅读:
    单元测试,集成测试与系统测试
    关于 单窗口服务模型模拟 进行的小测试
    软件测试新随笔
    白盒测试
    黑盒测试小实验
    JUnit框架初次体验
    等价类划分进阶篇
    等价类划分
    因果图法测试小例
    android中将EditText改成不可编辑的状态
  • 原文地址:https://www.cnblogs.com/qlqwjy/p/14660221.html
Copyright © 2020-2023  润新知