SpringBoot 教程核心功能-Web 开发(请求处理)

1 Rest 映射

请求映射对应注解为 @xxxxMapping,它有以下几种:

  • @GetMapping
  • @PostMapping
  • @DeleteMapping
  • @PutMapping

以前我们对于数据的增删改查操作都是按照 url 区分的,比如:

  • /getUser 获取用户
  • /deleteUser 删除用户
  • /editUser 修改用户
  • /saveUser 保存用户

而现在我们使用 Rest 风格,即  url 一样,比如 /user,只是根据 method 不同:

  • GET 获取用户
  • DELETE 删除用户
  • PUT 修改用户
  • POST 保存用户

Rest 解释:使用 HTTP 请求方式动词来表示对资源的操作

我们对 Rest 风格的 method 的处理的核心 Filter 是 HiddenHttpMethodFilter。

1.1 用法

配置文件开启表单的 Rest 功能

spring:
  mvc:
    hiddenmethod:
      filter:
        enabled: true #开启页面表单的Rest功能

页面 form 的 method=post,隐藏域 _method=put 、delete 等(如果直接是 get 或 post,则无需写隐藏域)

<!Document>
<html>
<head>
    <meta charset="UTF-8">
    <title>测试</title>
</head>
<body>
    <form action="/user" method="get">
        <input value="REST-GET提交" type="submit"/>
    </form>

    <form action="/user" method="post">
        <input value="REST-POST提交" type="submit"/>
    </form>

    <form action="/user" method="post">
        <input name="_method" type="hidden" value="DELETE"/>
        <input value="REST-DELETE 提交" type="submit"/>
    </form>

    <form action="/user" method="post">
        <input name="_method" type="hidden" value="PUT"/>
        <input value="REST-PUT提交" type="submit"/>
    </form>
</body>
</html>

编写请求映射

@RestController
public class WelcomeController {

    @GetMapping("/user")
//@RequestMapping(value = "/user",method = RequestMethod.GET)
    public String getUser(){
        return "GET-张三";
    }

    @PostMapping("/user")
//@RequestMapping(value = "/user",method = RequestMethod.POST)
    public String saveUser(){
        return "POST-张三";
    }

    @PutMapping("/user")
//@RequestMapping(value = "/user",method = RequestMethod.PUT)
    public String putUser(){
        return "PUT-张三";
    }

    @DeleteMapping("/user")
//@RequestMapping(value = "/user",method = RequestMethod.DELETE)
    public String deleteUser(){
        return "DELETE-张三";
    }

}

1.2 Rest 源码分析(表单提交要使用 Rest 的时候)

1)表单提交 _method=put

2)请求过来被  HiddenHttpMethodFilter 拦截,进入 doFilterInternal()

3)检查请求是否正常,并且请求是 POST

4)获取 _method 的值并转成大写,并判断是否包含这个值(PUT  DELETE  PATCH)

5)put 是属于 ALLOWED_METHODS 的,将原生的 request 包装成 requestWrapper。

包装 requestWrapper 重写了 getMethod() 方法,返回值 传入的值。

6)过滤器链放行的是 requestWrapper,后面调用的 getMethod() 返回的就是 _method 传入的值。

public class HiddenHttpMethodFilter extends OncePerRequestFilter {
    private static final List<String> ALLOWED_METHODS;
    public static final String DEFAULT_METHOD_PARAM = "_method";
    private String methodParam = "_method";

    public HiddenHttpMethodFilter() {
    }

    public void setMethodParam(String methodParam) {
        Assert.hasText(methodParam, "'methodParam' must not be empty");
        this.methodParam = methodParam;
    }

    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        HttpServletRequest requestToUse = request;
        if ("POST".equals(request.getMethod()) && request.getAttribute("javax.servlet.error.exception") == null) {
            String paramValue = request.getParameter(this.methodParam);
            if (StringUtils.hasLength(paramValue)) {
                String method = paramValue.toUpperCase(Locale.ENGLISH);
                if (ALLOWED_METHODS.contains(method)) {
                    requestToUse = new HiddenHttpMethodFilter.HttpMethodRequestWrapper(request, method);
                }
            }
        }

        filterChain.doFilter((ServletRequest)requestToUse, response);
    }

    static {
        ALLOWED_METHODS = Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(), HttpMethod.DELETE.name(), HttpMethod.PATCH.name()));
    }

    private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
        private final String method;

        public HttpMethodRequestWrapper(HttpServletRequest request, String method) {
            super(request);
            this.method = method;
        }

        public String getMethod() {
            return this.method;
        }
    }
}

1.3 改变默认的 _method

WebMvcAutoConfiguration  的源码如下

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
@AutoConfigureOrder(-2147483638)
@AutoConfigureAfter({DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class})
public class WebMvcAutoConfiguration {
    ...

    @Bean
    @ConditionalOnMissingBean({HiddenHttpMethodFilter.class})
    @ConditionalOnProperty(
        prefix = "spring.mvc.hiddenmethod.filter",
        name = {"enabled"},
        matchIfMissing = false
    )
    public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
        return new OrderedHiddenHttpMethodFilter();
    }

    ...
}

通过源码,@ConditionalOnMissingBean({HiddenHttpMethodFilter.class}) 意味着在没有 HiddenHttpMethodFilter 这个组件时,才执行 hiddenHttpMethodFilter() 。因此,我们可以自定义 filter,改变默认的 _method。例如

@Configuration(proxyBeanMethods = false)
public class WebConfig {
    @Bean
    public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
        HiddenHttpMethodFilter filter = new HiddenHttpMethodFilter();
        filter.setMethodParam("_m");
        return filter;
    }
}

就可以将默认的 _method 改成 _m 。

    <form action="/user" method="post">
    <!-- <input name="_method" type="hidden" value="DELETE"/>-->
        <input name="_m" type="hidden" value="DELETE"/>
        <input value="REST-DELETE 提交" type="submit"/>
    </form>

1.4 请求映射原理-源码分析

由上图可以分析, SpringMVC 的请求都从 DispatcherServlet.doDispatch() 开始的 

public class DispatcherServlet extends FrameworkServlet {
    protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HttpServletRequest processedRequest = request;
        HandlerExecutionChain mappedHandler = null;
        boolean multipartRequestParsed = false;
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

        try {
            try {
                ModelAndView mv = null;
                Object dispatchException = null;

                try {
                    processedRequest = this.checkMultipart(request);
                    multipartRequestParsed = processedRequest != request;
                    // 找到当前请求使用哪个Handler(Controller的方法)处理
                    mappedHandler = this.getHandler(processedRequest);
                   
                    ...
                }
            ...
    }
                    
    ...
}

doDispatch() 的核心功能由 getHandler() 方法完成

    @Nullable
    protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
        if (this.handlerMappings != null) {
            Iterator var2 = this.handlerMappings.iterator();

            while(var2.hasNext()) {
                HandlerMapping mapping = (HandlerMapping)var2.next();
                HandlerExecutionChain handler = mapping.getHandler(request);
                if (handler != null) {
                    return handler;
                }
            }
        }

        return null;
    }

在 Debug 模式下展现  this.handlerMappings 的内容

发现其保存了所有 @RequestMapping 和 Handler 的映射规则。 

所有的请求映射都在 HandlerMapping 中:

  • SpringBoot 自动配置 欢迎页的 WelcomePageHandlerMapping 。访问【/】能访问到 index.html。
  • SpringBoot 自动配置了默认的 RequestMappingHandlerMapping(即注解 @XxxMapping 的类和方法)
  • 请求进来户,会挨个尝试所有的 HandlerMapping 看是否有符合的请求信息。
  1. 如果有符合的,就找到这个请求对应的 Handler
  2. 如果没有,就从下一个 HandlerMapping 继续查找
  • 有时,我们需要自定义的映射处理(HandlerMapping)。  

2.普通参数与基本注解

2.1 常用参数注解

  • @PathVariable 路径变量
  • @RequestHeader 获取请求头
  • @RequestParam 获取请求参数(指 url 问号后的参数, url?a=1&b=2)
  • @CookieValue 获取Cookie 值
  • @RequestAttribute 获取 Request 域属性值
  • @RequestBody 获取请求体【post方式】
  • @MatrixVariable 矩阵变量
  • @ModelAttribute

使用用例

@RestController
public class ParameterTestController {
    @GetMapping("/path/{id}/owner/{username}")
    public Map path(@PathVariable("id") Integer id,
                      @PathVariable("username") String name,
                      @PathVariable Map<String,String> pv) {
        Map<String,Object> map = new HashMap<>();
        map.put("id",id);
        map.put("name",name);
        map.put("pv",pv);
        return map;
    }

    @GetMapping("/header")
    public Map header(@RequestHeader("User-Agent") String userAgent,
                      @RequestHeader Map<String,String> headers,
                      @RequestHeader HttpHeaders httpHeaders) {
        Map<String,Object> map = new HashMap<>();
        map.put("userAgent",userAgent);
        map.put("headers",headers);
        map.put("httpHeaders",httpHeaders);
        return map;
    }

    @GetMapping("/param")
    public Map param(@RequestParam("age") Integer age,
                     @RequestParam("inters") List<String> inters,
                     @RequestParam Map<String,String> params) {
        Map<String,Object> map = new HashMap<>();
        map.put("age",age);
        map.put("inters",inters);
        map.put("params",params);
        return map;
    }

    @GetMapping("/cookie")
    public Map cookie(@CookieValue("Idea-661643a5") String Idea,
                      @CookieValue("Idea-661643a5") Cookie cookie) {
        Map<String,Object> map = new HashMap<>();
        map.put("Idea-661643a5",Idea);
        map.put("cookie",cookie);
        return map;
    }

    @PostMapping("/requestBody")
    public Map requestBody(@RequestBody String content) {
        Map<String,Object> map = new HashMap<>();
        map.put("content",content);
        return map;
    }
}
<!Document>
<html>
<head>
    <meta charset="UTF-8">
    <title>测试</title>
</head>
<body>
    <ul>
        <li><a href="/path/1/owner/chenjian">@PathVariable</a></li>
        <li><a href="/header">@RequestHeader</a></li>
        <li><a href="/param?age=18&inters=足球&inters=篮球">@RequestParam</a></li>
        <li><a href="/cookie">@CookieValue</a></li>
    </ul>

    <h3>@RequestBody</h3>
    <form action="/requestBody" method="post">
        <input name="name"/>
        <input name="age"/>
        <input value="提交" type="submit"/>
    </form>
</body>
</html>

2.1.2 @RequestAttribute 用法

@Controller
public class RequestAttributeTestController {
    @GetMapping("/goto")
    public String goToPage(HttpServletRequest request){

        request.setAttribute("msg","成功了...");
        request.setAttribute("code",200);
        return "forward:/success";  //转发到  /success请求
    }

    @ResponseBody
    @GetMapping("/success")
    public Map success(@RequestAttribute(value = "msg",required = false) String msg,
                       @RequestAttribute(value = "code",required = false)Integer code,
                       HttpServletRequest request){
        Object msg1 = request.getAttribute("msg");
        Map<String,Object> map = new HashMap<>();
        map.put("reqMethod_msg",msg1);
        map.put("annotation_msg",msg);
        map.put("code",code);

        return map;
    }
}

2.1.3 @MatrixVariable 与 UrlPathHelper

  • 语法:/cars/sell;low=34;brand=byd,audi,yd
  • SpringBoot 默认禁用了矩阵变量的功能,将 UrlPathHelper 的 removeSemicolonContent 设置为 false,让其支持矩阵变量。
  • 矩阵变量必须有 url 路径变量才能被解析

手动开启矩阵变量的实现有两种方式,方式一:

@Configuration(proxyBeanMethods = false)
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        UrlPathHelper urlPathHelper = new UrlPathHelper();
        urlPathHelper.setRemoveSemicolonContent(false);
        configurer.setUrlPathHelper(urlPathHelper);
    }
}

方式二:

@Configuration(proxyBeanMethods = false)
public class WebConfig {
    @Bean
    public WebMvcConfigurer webMvcConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void configurePathMatch(PathMatchConfigurer configurer) {
                UrlPathHelper urlPathHelper = new UrlPathHelper();
                urlPathHelper.setRemoveSemicolonContent(false);
                configurer.setUrlPathHelper(urlPathHelper);
            }
        };
    }
}

@MatrixVariable 的例子,其 Controller 如下:

@RestController
public class ParameterTestController {
    //  /car/sell;low=34;brand=byd,audi,yd
    @GetMapping("/car/{path}")
    public Map carSell(@MatrixVariable("low") String low,
                       @MatrixVariable("brand") List<String> brand,
                       @PathVariable("path") String path) {
        Map<String,Object> map = new HashMap<>();
        map.put("low",low);
        map.put("brand",brand);
        map.put("path",path);
        return map;
    }

    //两个 path 变量的 矩阵变量一样的情况
    // /boss/1;age=20/2;age=10
    @GetMapping("/boss/{boss}/{emp}")
    public Map boss(@MatrixVariable(value = "age", pathVar = "boss") Integer bossAge,
            @MatrixVariable(value = "age", pathVar = "emp") Integer empAge) {
        Map<String,Object> map = new HashMap<>();
        map.put("bossAge",bossAge);
        map.put("empAge",empAge);
        return map;
    }
}

Html 源码:

<!Document>
<html>
<head>
    <meta charset="UTF-8">
    <title>测试</title>
</head>
<body>
    <ul>
        <li><a href="/car/sell;low=34;brand=byd,audi,yd">@MatrixVariable</a></li>
        <li><a href="/boss/1;age=20/2;age=10">@MatrixVariable两个path变量相同</a></li>
    </ul>
</body>
</html>

3.各类型参数解析原理

这要从 DispatcherServlet 开始说起

	protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
		HttpServletRequest processedRequest = request;
		HandlerExecutionChain mappedHandler = null;
		boolean multipartRequestParsed = false;

		WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

		try {
			ModelAndView mv = null;
			Exception dispatchException = null;

			try {
				processedRequest = checkMultipart(request);
				multipartRequestParsed = (processedRequest != request);

                //1
				// Determine handler for the current request.
				mappedHandler = getHandler(processedRequest);
				if (mappedHandler == null) {
					noHandlerFound(processedRequest, response);
					return;
				}

                //2
				// Determine handler adapter for the current request.
				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); 
                ...

                //3
				// Actually invoke the handler.
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());                
         ...
    }
  • getHandler():由 【2.2.2 请求映射原理-源码分析】章节我们知道 HandlerMapping 中可以找到处理请求的 Handler。(Controller.method())
  • getHandlerAdapter() :为当前 Handler 找一个适配器(HandlerAdapter),用的最多的就是 RequestMappingHandlerAdapter
  • 适配器执行目标方法并确定方法参数的每一个值

3.1 HandlerAdapter

使用 IDEA 的Debug 调试,进入 getHandlerAdapter() 方法,发现 this.handlerAdapters 有如下几种:

  • RequestMappingHandlerAdapter : @RequestMapping 方法的适配器
  • HandlerFunctionAdapter:支持函数式的编程
  • HttpRequestHandlerAdapter
  • SimpleControllerHandlerAdapter

 

3.2 执行目标方法

也就是源码中 标注的 3,ha.handle()。继续调试进入 handler 方法,进入 AbstractHandlerMethodAdapter  的 handle(),。

public abstract class AbstractHandlerMethodAdapter extends WebContentGenerator implements HandlerAdapter, Ordered {
	@Override
	@Nullable
	public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {

		return handleInternal(request, response, (HandlerMethod) handler);
	}
}

handlerInternal() 的是抽象方法,当前我们使用的是 RequestMappingHandlerAdapter 的实现,如下所示:

public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
		implements BeanFactoryAware, InitializingBean {
    ...
	@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 {
                // 该方法为 handleInternal 的核心
				// 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.2 参数解析器

我们继续往下跟踪调试 invokeHandlerMethod(),SpringMVC 目标方法有多少中参数类型,取决于参数解析器 this.argumentResolvers ,而我们会在方法执行前会将所有的参数解析器和返回值解析器设置到 我们的处理器。

	@Nullable
	protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
			HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

		ServletWebRequest webRequest = new ServletWebRequest(request, response);
		try {
			WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
			ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);

			ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
			if (this.argumentResolvers != null) {//将参数解析器加载到 handler
				invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
			}
			if (this.returnValueHandlers != null) { //将返回值处理器加载到 handler
				invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
			}
			...

			invocableMethod.invokeAndHandle(webRequest, mavContainer);
			if (asyncManager.isConcurrentHandlingStarted()) {
				return null;
			}

			return getModelAndView(mavContainer, modelFactory, webRequest);
		}
		finally {
			webRequest.requestCompleted();
		}
	}

一共有 26 中参数解析器,如下图:

3.2.1 获取每个参数值 getMethodArgumentValues()

继续执行顺序如下:

//RequestMappingHandlerAdapter
invocableMethod.invokeAndHandle(webRequest, mavContainer);

//ServletInvocableHandlerMethod.
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);

//ServletInvocableHandlerMethod
Object[] args = this.getMethodArgumentValues(request, mavContainer, providedArgs);

ServletInvocableHandlerMethod 的 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;
        }
    }

在对参数进行解析之前首先需要判断参数是否支持解析,关键代码如下:

if (!this.resolvers.supportsParameter(parameter)) {

supportsParameter() 的实现如下:


public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver {
    ...
    public boolean supportsParameter(MethodParameter parameter) {
        return this.getArgumentResolver(parameter) != null;
    }


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

    ...
}

当判断参数有支持的解析器后,则获取对应的解析器,并调用其 resolveArgument() 对参数进行解析

args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);

解析的源码如下:

// HandlerMethodArgumentResolverComposite.class
    @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);
        }
    }


// AbstractNamedValueMethodArgumentResolver.class
    public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
        AbstractNamedValueMethodArgumentResolver.NamedValueInfo namedValueInfo = this.getNamedValueInfo(parameter);
        // 拿到参数信息信息
        MethodParameter nestedParameter = parameter.nestedIfOptional();
        // 拿到参数名称
        Object resolvedName = this.resolveEmbeddedValuesAndExpressions(namedValueInfo.name);
        if (resolvedName == null) {
            throw new IllegalArgumentException("Specified name must not resolve to null: [" + namedValueInfo.name + "]");
        } else {
            // 拿到参数的值
            Object arg = this.resolveName(resolvedName.toString(), nestedParameter, webRequest);
            if (arg == null) {
                if (namedValueInfo.defaultValue != null) {
                    arg = this.resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
                } else if (namedValueInfo.required && !nestedParameter.isOptional()) {
                    this.handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
                }

                arg = this.handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
            } else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
                arg = this.resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
            }

            if (binderFactory != null) {
                WebDataBinder binder = binderFactory.createBinder(webRequest, (Object)null, namedValueInfo.name);

                try {
                    arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
                } catch (ConversionNotSupportedException var11) {
                    throw new MethodArgumentConversionNotSupportedException(arg, var11.getRequiredType(), namedValueInfo.name, parameter, var11.getCause());
                } catch (TypeMismatchException var12) {
                    throw new MethodArgumentTypeMismatchException(arg, var12.getRequiredType(), namedValueInfo.name, parameter, var12.getCause());
                }
            }

            this.handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);
            return arg;
        }
    }

resolveName() 它是一个抽象方法,它是由 对应的解析器进行实现的, 比如 PathVariableMethodArgumentResolver 的实现如下:

	@Override
	@SuppressWarnings("unchecked")
	@Nullable
	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);
	}

发现所有的参数的值都是从 request 的 属性中取出的,这是什么时候放入 request 的属性的呢?

它是在进行路径映射的时候就已经解析成 map 并存入 request 的 attribute

3.2.2 Servlet API 参数解析原理

Servlet API 相关的参数如下:

  • WebRequest
  • ServletRequest
  • MultipartRequest
  • HttpSession
  • PushBuilder (as of Spring 5.0 on Servlet 4.0)
  • Principal
  • InputStream
  • Reader
  • HttpMethod (as of Spring 4.0)
  • Locale
  • TimeZone (as of Spring 4.0)
  • java.time.ZoneId (as of Spring 4.0 and Java 8)

ServletRequestMethodArgumentResolver  用来处理以上的参数:

public class ServletRequestMethodArgumentResolver implements HandlerMethodArgumentResolver {

	@Nullable
	private static Class<?> pushBuilder;

	static {
		try {
			pushBuilder = ClassUtils.forName("javax.servlet.http.PushBuilder",
					ServletRequestMethodArgumentResolver.class.getClassLoader());
		}
		catch (ClassNotFoundException ex) {
			// Servlet 4.0 PushBuilder not found - not supported for injection
			pushBuilder = null;
		}
	}


	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		Class<?> paramType = parameter.getParameterType();
		return (WebRequest.class.isAssignableFrom(paramType) ||
				ServletRequest.class.isAssignableFrom(paramType) ||
				MultipartRequest.class.isAssignableFrom(paramType) ||
				HttpSession.class.isAssignableFrom(paramType) ||
				(pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||
				Principal.class.isAssignableFrom(paramType) ||
				InputStream.class.isAssignableFrom(paramType) ||
				Reader.class.isAssignableFrom(paramType) ||
				HttpMethod.class == paramType ||
				Locale.class == paramType ||
				TimeZone.class == paramType ||
				ZoneId.class == paramType);
	}

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

		Class<?> paramType = parameter.getParameterType();

		// WebRequest / NativeWebRequest / ServletWebRequest
		if (WebRequest.class.isAssignableFrom(paramType)) {
			if (!paramType.isInstance(webRequest)) {
				throw new IllegalStateException(
						"Current request is not of type [" + paramType.getName() + "]: " + webRequest);
			}
			return webRequest;
		}

		// ServletRequest / HttpServletRequest / MultipartRequest / MultipartHttpServletRequest
		if (ServletRequest.class.isAssignableFrom(paramType) || MultipartRequest.class.isAssignableFrom(paramType)) {
			return resolveNativeRequest(webRequest, paramType);
		}

		// HttpServletRequest required for all further argument types
		return resolveArgument(paramType, resolveNativeRequest(webRequest, HttpServletRequest.class));
	}

	private <T> T resolveNativeRequest(NativeWebRequest webRequest, Class<T> requiredType) {
		T nativeRequest = webRequest.getNativeRequest(requiredType);
		if (nativeRequest == null) {
			throw new IllegalStateException(
					"Current request is not of type [" + requiredType.getName() + "]: " + webRequest);
		}
		return nativeRequest;
	}

	@Nullable
	private Object resolveArgument(Class<?> paramType, HttpServletRequest request) throws IOException {
		if (HttpSession.class.isAssignableFrom(paramType)) {
			HttpSession session = request.getSession();
			if (session != null && !paramType.isInstance(session)) {
				throw new IllegalStateException(
						"Current session is not of type [" + paramType.getName() + "]: " + session);
			}
			return session;
		}
		else if (pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) {
			return PushBuilderDelegate.resolvePushBuilder(request, paramType);
		}
		else if (InputStream.class.isAssignableFrom(paramType)) {
			InputStream inputStream = request.getInputStream();
			if (inputStream != null && !paramType.isInstance(inputStream)) {
				throw new IllegalStateException(
						"Request input stream is not of type [" + paramType.getName() + "]: " + inputStream);
			}
			return inputStream;
		}
		else if (Reader.class.isAssignableFrom(paramType)) {
			Reader reader = request.getReader();
			if (reader != null && !paramType.isInstance(reader)) {
				throw new IllegalStateException(
						"Request body reader is not of type [" + paramType.getName() + "]: " + reader);
			}
			return reader;
		}
		else if (Principal.class.isAssignableFrom(paramType)) {
			Principal userPrincipal = request.getUserPrincipal();
			if (userPrincipal != null && !paramType.isInstance(userPrincipal)) {
				throw new IllegalStateException(
						"Current user principal is not of type [" + paramType.getName() + "]: " + userPrincipal);
			}
			return userPrincipal;
		}
		else if (HttpMethod.class == paramType) {
			return HttpMethod.resolve(request.getMethod());
		}
		else if (Locale.class == paramType) {
			return RequestContextUtils.getLocale(request);
		}
		else if (TimeZone.class == paramType) {
			TimeZone timeZone = RequestContextUtils.getTimeZone(request);
			return (timeZone != null ? timeZone : TimeZone.getDefault());
		}
		else if (ZoneId.class == paramType) {
			TimeZone timeZone = RequestContextUtils.getTimeZone(request);
			return (timeZone != null ? timeZone.toZoneId() : ZoneId.systemDefault());
		}

		// Should never happen...
		throw new UnsupportedOperationException("Unknown parameter type: " + paramType.getName());
	}


	/**
	 * Inner class to avoid a hard dependency on Servlet API 4.0 at runtime.
	 */
	private static class PushBuilderDelegate {

		@Nullable
		public static Object resolvePushBuilder(HttpServletRequest request, Class<?> paramType) {
			PushBuilder pushBuilder = request.newPushBuilder();
			if (pushBuilder != null && !paramType.isInstance(pushBuilder)) {
				throw new IllegalStateException(
						"Current push builder is not of type [" + paramType.getName() + "]: " + pushBuilder);
			}
			return pushBuilder;

		}
	}

}

测试用例:

@Controller
public class RequestAttributeTestController {
    @GetMapping("/goto")
    public String goToPage(HttpServletRequest request){

        request.setAttribute("msg","成功了...");
        request.setAttribute("code",200);
        return "forward:/success";  //转发到  /success请求
    }
}

3.6.3 复杂参数解析

复杂参数:

  • Map
  • Model(Map、Model 里面的数据会被放在 request 的请求域 request.setAttribute)
  • Errors / BindingResult
  • RedirectAttribute (重定向携带数据)
  • ServletResponse (response)
  • SessionStatus
  • UriComponentBuilder
  • ServletUriComponentBuilder

用例代码:

@Controller
public class RequestAttributeTestController {
    @GetMapping("/params")
    public String testParam(Map<String,Object> map,
                            Model model,
                            HttpServletRequest request,
                            HttpServletResponse response){
        // 以下三维都是可以给 request 域中放数据
        map.put("hello","hello666");
        model.addAttribute("world","hello666");
        request.setAttribute("message","HelloWorld");

        return "forward:/success";
    }

    @ResponseBody
    @GetMapping("/success")
    public Map success(@RequestAttribute(value = "msg",required = false) String msg,
                       @RequestAttribute(value = "code",required = false)Integer code,
                       HttpServletRequest request){
        Object msg1 = request.getAttribute("msg");
        Map<String,Object> map = new HashMap<>();
        map.put("reqMethod_msg",msg1);
        map.put("annotation_msg",msg);
        map.put("code",code);

        map.put("hello", request.getAttribute("hello"));
        map.put("world", request.getAttribute("world"));
        map.put("message", request.getAttribute("message"));
        return map;
    }
}

由用例输出结果可知:map、model、request 都是可以给 request 中放数据域数据,用 request.getAttribute() 获取。

接下来我们使用调试模式来看看 map 与 model 用什么参数处理器,通过跟踪 HandlerMethodArgumentResolverComposite.getArgumentResolver() 中对参数解析器的遍历,我们发现 map 参数采用的是 MapMethodProcessor,Model 采用的是 ModelAndViewContainer。

public class MapMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {

	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		return (Map.class.isAssignableFrom(parameter.getParameterType()) &&
				parameter.getParameterAnnotations().length == 0);
	}

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

		Assert.state(mavContainer != null, "ModelAndViewContainer is required for model exposure");
		return mavContainer.getModel();
	}
}

public class ModelMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {

	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		return Model.class.isAssignableFrom(parameter.getParameterType());
	}

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

		Assert.state(mavContainer != null, "ModelAndViewContainer is required for model exposure");
		return mavContainer.getModel();
	}
}

发现他们最终返回的都是 mavContainer.getModel()。继续查看源码

public class ModelAndViewContainer {
    ...
	private final ModelMap defaultModel = new BindingAwareModelMap();

	public ModelMap getModel() {
		if (useDefaultModel()) {
			return this.defaultModel;
		}
		else {
			if (this.redirectModel == null) {
				this.redirectModel = new ModelMap();
			}
			return this.redirectModel;
		}
	}
    ...
}

public class BindingAwareModelMap extends ExtendedModelMap {}

public class ExtendedModelMap extends ModelMap implements Model {}

public class ModelMap extends LinkedHashMap<String, Object> {}

我们发现 mavContainer.getModel() 最终返回 ModelAndViewContainer ,它即使 Map 也是 Model。

接下来我们执行 handler (即 用例的代码),代码执行完成后,会对 ModelAndViewContainer 的进行处理,它的值如下:

 ModelAndViewContainer 包含了页面地址和 Model 数据。

接下来我们会将 ModelAndViewContainer 转化成 ModelAndView ,从 handle() 返回,并调用 processDispatchResult() 处理返回结果

// DisPatcherServlet.class
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
...
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

 接着往下调试

// DispatcherServlet.class
	private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
			@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
			@Nullable Exception exception) throws Exception {
		...
		// Did the handler return a view to render?
		if (mv != null && !mv.wasCleared()) {
			render(mv, request, response);
			if (errorView) {
				WebUtils.clearErrorRequestAttributes(request);
			}
		}
        ...
    }

	protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
		...
		try {
			if (mv.getStatus() != null) {
				response.setStatus(mv.getStatus().value());
			}
			view.render(mv.getModelInternal(), request, response);
		}
		...
	}

调试发现【view.render(mv.getModelInternal(), request, response)】, view 的值如下

继续往下调试,执行顺序为 

AbstractView.render() -->  InternalResourceView.renderMergedOutputModel() --> AbstractView.exposeModelAsRequestAttributes()

public abstract class AbstractView extends WebApplicationObjectSupport implements View, BeanNameAware {
    ...
	public void render(@Nullable Map<String, ?> model, HttpServletRequest request,
			HttpServletResponse response) throws Exception {

		if (logger.isDebugEnabled()) {
			logger.debug("View " + formatViewName() +
					", model " + (model != null ? model : Collections.emptyMap()) +
					(this.staticAttributes.isEmpty() ? "" : ", static attributes " + this.staticAttributes));
		}

		Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
		prepareResponse(request, response);
		renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
	}
    ...
   
	protected void exposeModelAsRequestAttributes(Map<String, Object> model,
			HttpServletRequest request) throws Exception {

		model.forEach((name, value) -> {
			if (value != null) {
				request.setAttribute(name, value);
			}
			else {
				request.removeAttribute(name);
			}
		});
	}
    ...
}


public class InternalResourceView extends AbstractUrlBasedView {
    ...
	@Override
	protected void renderMergedOutputModel(
			Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {

		// Expose the model object as request attributes.
		exposeModelAsRequestAttributes(model, request);

		...
	}
    ...
}

由 AbstractView.exposeModelAsRequestAttributes() 的源码可以知道,它是将 Model 中的所有值都设置到 request 的域中。

3.6.4 自定义类型(class 类)参数解析

用例

@RestController
public class ParameterTestController {
    /**
     * 数据绑定:页面提交的请求数据(GET、POST)都可以和对象属性进行绑定
     */
    @PostMapping("/saveUser")
    public Person saveUser(Person person) {
        return person;
    }
}

@Data
public class Person {
    private String username;
    private Integer age;
    private Date birth;
    private Pet pet;
}

@Data
public class Pet {
    private String name;
    private String age;
}
<!Document>
<html>
<head>
    <meta charset="UTF-8">
    <title>测试</title>
</head>
<body>
    <form action="/saveUser" method="POST">
        姓名: <input name="userName" value="chenj"/> <br/>
        年龄: <input name="age" value="18"/> <br/>
        生日: <input name="birth" value="2009/12/10"/> <br/>
        宠物姓名:<input name="pet.name" value="阿毛"/><br/>
        宠物年龄:<input name="pet.age" value="3"/>
        <input type="submit" value="提交"/>
    </form>
</body>
</html>

可以发现浏览器正常输出:

{"username":null,"age":18,"birth":"2009-12-09T16:00:00.000+00:00","pet":{"name":"阿毛","age":"3"}}

接下来我们调试跟踪代码看下源码:还是通过 HandlerMethodArgumentResolverComposite.getArgumentResolver() 来遍历所有的参数解析器,最后发现是 ServletModelAttributeMethodProcessor 这个,它的是 ModelAttributeMethodProcessor  的子类。

public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
    ...
	@Override
	public boolean supportsParameter(MethodParameter parameter) {
		return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
				(this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType())));
	}

	/**
	 * Resolve the argument from the model or if not found instantiate it with
	 * its default if it is available. The model attribute is then populated
	 * with request values via data binding and optionally validated
	 * if {@code @java.validation.Valid} is present on the argument.
	 * @throws BindException if data binding and validation result in an error
	 * and the next method parameter is not of type {@link Errors}
	 * @throws Exception if WebDataBinder initialization fails
	 */
	@Override
	@Nullable
	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 = 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 {
			// Create attribute instance
			try {
				attribute = createAttribute(name, parameter, binderFactory, webRequest);
			}
			catch (BindException ex) {
				if (isBindExceptionRequired(parameter)) {
					// No BindingResult parameter -> fail with BindException
					throw ex;
				}
				// Otherwise, expose null/empty value and associated BindingResult
				if (parameter.getParameterType() == Optional.class) {
					attribute = Optional.empty();
				}
				bindingResult = ex.getBindingResult();
			}
		}

		if (bindingResult == null) {
			// Bean property binding and validation;
			// skipped in case of binding failure on construction.
			WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
			if (binder.getTarget() != null) {
				if (!mavContainer.isBindingDisabled(name)) {
					bindRequestParameters(binder, webRequest);
				}
				validateIfApplicable(binder, parameter);
				if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
					throw new BindException(binder.getBindingResult());
				}
			}
			// Value type adaptation, also covering java.util.Optional
			if (!parameter.getParameterType().isInstance(attribute)) {
				attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
			}
			bindingResult = binder.getBindingResult();
		}

		// Add resolved attribute and BindingResult at the end of the model
		Map<String, Object> bindingResultModel = bindingResult.getModel();
		mavContainer.removeAttributes(bindingResultModel);
		mavContainer.addAllAttributes(bindingResultModel);

		return attribute;
	}
    ...

}

public class ServletModelAttributeMethodProcessor extends ModelAttributeMethodProcessor {}

HandlerMethodArgumentResolver.supportsParameter() 中 BeanUtils.isSimpleProperty(parameter.getParameterType()) 判断传入参数的类似是否是简单类型的原型如下:

    public static boolean isSimpleValueType(Class<?> type) {
        return Void.class != type && Void.TYPE != type && (ClassUtils.isPrimitiveOrWrapper(type) || Enum.class.isAssignableFrom(type) || CharSequence.class.isAssignableFrom(type) || Number.class.isAssignableFrom(type) || Date.class.isAssignableFrom(type) || Temporal.class.isAssignableFrom(type) || URI.class == type || URL.class == type || Locale.class == type || Class.class == type);
    }

然后继续往下调试,会进入到 ModelAttributeMethodProcessor.resolveArgument() 方法中,继续跟踪执行到下面这句话

WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);

WebDataBinder :web 数据绑定器,将请求参数的值绑定到指定的 JavaBean 中。

WebDataBinder 后面会利用它里面的 Converters 将请求数据转换成指定的数据类型,封装到 JavaBean。

继续往下调试

//ModelAttributeMethodProcessor -->
bindRequestParameters(binder, webRequest)

//ServletModelAttributeMethodProcessor -->
servletBinder.bind(servletRequest);

//ServletRequestDataBinder -->
doBind(mpvs);

//WebDataBinder -->
super.doBind(mpvs);

//DataBinder -->
this.applyPropertyValues(mpvs);

//DataBinder -->
this.getPropertyAccessor().setPropertyValues(mpvs, this.isIgnoreUnknownFields(), this.isIgnoreInvalidFields());

//AbstractPropertyAccessor -->
this.setPropertyValue(pv);

//AbstractPropertyAccessor -->
nestedPa.setPropertyValue(tokens, pv);

//AbstractPropertyAccessor -->
this.processLocalProperty(tokens, pv);

//AbstractPropertyAccessor -->
valueToApply = this.convertForProperty(tokens.canonicalName, oldValue, originalValue, ph.toTypeDescriptor());

//AbstractPropertyAccessor -->
return this.convertIfNecessary(propertyName, oldValue, newValue, td.getType(), td);

//AbstractPropertyAccessor -->
return this.typeConverterDelegate.convertIfNecessary(propertyName, oldValue, newValue, requiredType, td);

//TypeConverterDelegate -->
conversionService.canConvert(sourceTypeDesc, typeDescriptor);

//GenericConversionService -->
//从很多的 Converter 中查找对应的 转化器
GenericConverter converter = this.getConverter(sourceType, targetType);

//TypeConverterDelegate -->
//用找到的 Converter 来转换数据
return conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);

//AbstractPropertyAccessor -->
//将转化后的值设置到 类里面(利用反射管理)
ph.setValue(valueToApply);

通过跟踪发现,在过程中,会用到 GenericConversionService :它是在设置值的时候,先从它里面的 Converters 查找那个匹配的转换器,然后将字符串转化成对应的数据类型(如 Integer、Boolean 等),最后再将转化后的值设置到 类中。

未来我们可以给 WebDataBinder 里面放自己的 Converter;

private static final class StringToNumber<T extends Number> implements Converter<String, T> {}

3.6.5 自定义 Converter

这一小节我们演示下给 WebDataBinder 里面加入自己的 Converter。

有个需求,我们将需要将 “阿毛,3” 转化成 Pet 对象,前台源码如下,后台 Controller 不变。

<!Document>
<html>
<head>
    <meta charset="UTF-8">
    <title>测试</title>
</head>
<body>
    <form action="/saveUser" method="POST">
        姓名: <input name="userName" value="chenj"/> <br/>
        年龄: <input name="age" value="18"/> <br/>
        生日: <input name="birth" value="2009/12/10"/> <br/>
        宠物:  <input name="pet" value="阿毛,3"/><br/>
<!--        宠物姓名:<input name="pet.name" value="阿毛"/><br/>-->
<!--        宠物年龄:<input name="pet.age" value="3"/>-->
        <input type="submit" value="提交"/>
    </form>
</body>
</html>

 如何添加自定义的转换器,如下所示:

@Configuration(proxyBeanMethods = false)
public class WebConfig {
    @Bean
    public WebMvcConfigurer webMvcConfigurer() {
        return new WebMvcConfigurer() {
            @Override
            public void addFormatters(FormatterRegistry registry) {
                registry.addConverter(new Converter<String, Pet>() {
                    @Override
                    public Pet convert(String source) {
                        String[] arr = source.split(",");
                        Pet pet = new Pet();
                        pet.setName(arr[0]);
                        pet.setAge(arr[1]);
                        return pet;
                    }
                });
            }
        };
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值