【Spring MVC】MVC请求的处理过程一览-HandlerMethodArgumentResolver解析参数过程【一】
1 前言
上节【Spring MVC】MVC请求的处理过程一览-HandlerMethodArgumentResolver概述以及分类,我们主要看了下解析器的概念以及常见的解析器的定义,那么我们本节要着重看下这些解析器的解析过程,以及解析出来的参数进行的一些转换支持,很重要,了解解析过程,我们才能更好的对请求的参数进行转换,比如加解密处理等。
2 解析过程回顾
首先我们结合之前的图,整体看一下 HandlerMethod 的执行过程,如下:
我大概分了三块(红色五角星标记的):第一块主要是 HandlerMethod 执行的主逻辑过程,第二块主要是表达一下 HandlerMethod 内部的 resolvers 的属性值的由来方便更好的理解解析过程,也可以看到解析的对象是HandlerMethodArgumentResolverComposite 对象类型,然后第三块顺便看下这个类的核心属性和方法。
那我们就顺便从代码简单了解下 HandlerMethodArgumentResolverComposite,首先看下他两个属性的定义:
// HandlerMethodArgumentResolverComposite 核心属性 public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver { // 解析器集合 private final List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<>(); // 缓存加速 map 比如我们的 controller 每个方法的参数 只要获取过一次,下次就不用再遍历集合寻找他的参数解析器了,直接从缓存拿即可 private final Map<MethodParameter, HandlerMethodArgumentResolver> argumentResolverCache = new ConcurrentHashMap<>(256); ... }
看注释即可明白哈,就不过多解释了,还有它内部的两个核心方法:
(1)根据某个参数获取具体的解析器 getArgumentResolver:
// HandlerMethodArgumentResolverComposite#getArgumentResolver 根据参数获取解析器 private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { // 首先从缓存中获取 HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter); // 为空的话,则遍历所有解析器,直到找到第一个支持的,就放入缓存,结束 // 可以看到每个参数 仅可匹配一个解析器就完事了 if (result == null) { for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) { // 解析器是否支持该参数 if (resolver.supportsParameter(parameter)) { result = resolver; // 放入缓存 this.argumentResolverCache.put(parameter, result); // 结束循环 break; } } } // 缓存中有的话直接返回该解析器 return result; }
顺便带一下,如果没有找到会怎么办呢?这就是我们要看的第二个方法 resolveArgument 解析参数。
(2)resolveArgument 解析参数方法
// HandlerMethodArgumentResolverComposite#resolveArgument 解析参数 public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { // 首先根据方法参数获取一个具体的解析器 HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter); // 如果没获取到直接抛异常,也就是请求直接会返回报错 if (resolver == null) { throw new IllegalArgumentException("Unsupported parameter type [" + parameter.getParameterType().getName() + "]. supportsParameter should be called first."); } // 找到的话,那么调用对应解析器的解析参数方法 return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); }
解析过程就回顾到这里,解析的入口就是 HandlerMethodArgumentResolverComposite 的 resolveArgument 方法,他首先会根据当前方法参数获取一个解析器,如果为空直接报错,不会空的话调用对应解析器的解析参数方法。
3 解析器解析
那我们接下来就要看下上节那些具体解析器是如何解析参数的,我们本节就看几个常用的解析器。
3.1 RequestParamMethodArgumentResolver
RequestParamMethodArgumentResolver 它是 @RequestParam 注解的解析器,大家应该经常会用到,主要是针对我们的 URL 中的参数进行解析处理比如下边的这个例子:
@GetMapping("/receiveByRequestParam") public String receiveByRequestParam(@RequestParam String orderNo, @RequestParam List<String> orderNoList) { log.info("orderNo={}, orderNoList={}", orderNo, orderNoList); return "success"; }
当我们的请求路径为 receiveByRequestParam?orderNo=111&orderNoList=222&orderNoList=333 我们的参数 orderNo就等于111,orderNoList=[222,333]。
我们先看下这个类的类图,可以看见它是继承了 AbstractNamedValueMethodArgumentResolver 这个类,属于模版模式并且这个类下边有很多的子类,我们后边慢慢看。
解析参数的方法 resolveArgument 在 AbstractNamedValueMethodArgumentResolver 那我们直接看下:
// AbstractNamedValueMethodArgumentResolver#resolveArgument public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { // 给当前参数创建一个 NamedValueInfo 对象,其实可以理解为键值对对象 也就是名称是什么 值是什么 这样的一个对象 NamedValueInfo namedValueInfo = getNamedValueInfo(parameter); // 嵌套的处理这里略过暂时不看 MethodParameter nestedParameter = parameter.nestedIfOptional(); // 如果是表达式的话 进行表达式的解析 比如@Value指定的表达式会进行解析 Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name); // 如果最后判断的参数名是空 直接抛异常 if (resolvedName == null) { throw new IllegalArgumentException( "Specified name must not resolve to null: [" + namedValueInfo.name + "]"); } // 抽象方法需要子类具体实现 这里就是获取当前参数的值 Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest); // 如果解析出来的值为空 if (arg == null) { // 默认值不为空的话 赋值默认值 if (namedValueInfo.defaultValue != null) { arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); } // 如果为必传的话,那么就会抛异常了 else if (namedValueInfo.required && !nestedParameter.isOptional()) { handleMissingValue(namedValueInfo.name, nestedParameter, webRequest); } // 不是必传的话,进行空值的处理 // 比如你的参数是基本数据类型 但是空值又不能给基本数据类型赋值 所以会报错 相当于校验 // 特殊的是如果是 Boolean 类型的话 会默认赋值为 false 其他会报错 arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType()); } // 如果是空串,并且默认值不为空 优先按默认值处理 else if ("".equals(arg) && namedValueInfo.defaultValue != null) { arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue); } // 数据绑定工厂 默认都不是空 if (binderFactory != null) { // 给当前属性创建一个绑定者出来 // Servlet 都是创建的 ExtendedServletRequestDataBinder 对象 // 这个绑定对象里有一个很重要的属性 conversionService 转换服务对象 WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name); try { // 是否需要进行转换处理 arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter); } catch (ConversionNotSupportedException ex) { throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(), namedValueInfo.name, parameter, ex.getCause()); } catch (TypeMismatchException ex) { throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(), namedValueInfo.name, parameter, ex.getCause()); } // Check for null value after conversion of incoming argument value // 怕转换后为空 这里会再进行一次 空判断 if (arg == null && namedValueInfo.defaultValue == null && namedValueInfo.required && !nestedParameter.isOptional()) { handleMissingValueAfterConversion(namedValueInfo.name, nestedParameter, webRequest); } } // 对值的再一次处理 目前只有 PathVariableMethodArgumentResolver 有特殊处理 其余什么也不做 handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest); return arg; }
主逻辑如上,大概的步骤是:
(1)首先是 getNamedValueInfo 根据当前的参数对象,创建一个 NamedValueInfo 键值对对象 也就是名称是什么 值是什么 这样的一个对象
(2)调用抽象方法 resolveName 解析参数值
(3)空值处理,比如有默认值的话赋值默认值以及一些必传参数校验检查
(4)创建数据绑定对象,进行值转换处理,其实就是解析出来的值类型和你参数定义的类型看看是否存在转换器(比如你传的是时间格式的字符串,你的参数类型定义的Date类型, spring有很多默认的内置转换器,就会给你做转换),存在的话就转换,不存在的话就不做处理
(5)handleResolvedValue 抽象方法,看子类还需要再对解析出来的值作何处理 目前只有 PathVariableMethodArgumentResolver 有处理,其他默认无处理
我们看下步骤(1)getNamedValueInfo:
// AbstractNamedValueMethodArgumentResolver#getNamedValueInfo private NamedValueInfo getNamedValueInfo(MethodParameter parameter) { // 缓存加速 NamedValueInfo namedValueInfo = this.namedValueInfoCache.get(parameter); if (namedValueInfo == null) { // 给当前参数创建一个 NamedValueInfo 对象 // 这个方法是抽象的需要子类根据自己的特性进行创建 // 比如 @RequestParam 注解 就是解析这个注解 得到设置的名称或者默认值等信息 namedValueInfo = createNamedValueInfo(parameter); // 这个就是补充信息,比如你的 @RequestParam 注解没有指定 name 名称 他就会获取你方法的属性名 来作为name // 比如 @RequestParam String orderNo,他就会将 orderNo属性名设置为 namedValueInfo 的 name 表示参数名 // NamedValueInfo 的 name 就代表后续要获取的参数名称 比如就是要获取 orderNo 的值 namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo); // 然后放入到缓存中 this.namedValueInfoCache.put(parameter, namedValueInfo); } return namedValueInfo; }
RequestParamMethodArgumentResolver 的 createNamedValueInfo 实现其实就是根据 @RequestParam 注解信息创建一个对象即可:
// RequestParamMethodArgumentResolver#createNamedValueInfo protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { RequestParam ann = parameter.getParameterAnnotation(RequestParam.class); return (ann != null ? new RequestParamNamedValueInfo(ann) : new RequestParamNamedValueInfo()); }
我们再看下 updateNamedValueInfo 方法就是当你没有设置参数名的话,就会获取你方法定义的参数名:
// AbstractNamedValueMethodArgumentResolver#updateNamedValueInfo private NamedValueInfo updateNamedValueInfo(MethodParameter parameter, NamedValueInfo info) { String name = info.name; // 如果没有设置 name 的话 if (info.name.isEmpty()) { // 获取你方法定义的参数名称 name = parameter.getParameterName(); // 如果获取失败的话,直接报错 if (name == null) { throw new IllegalArgumentException( "Name for argument of type [" + parameter.getNestedParameterType().getName() + "] not specified, and parameter name information not found in class file either."); } } // 默认值处理 String defaultValue = (ValueConstants.DEFAULT_NONE.equals(info.defaultValue) ? null : info.defaultValue); return new NamedValueInfo(name, info.required, defaultValue); }
说白了步骤(1)其实就是要知道你当前要解析的参数名是什么,以及参数的默认值有没有等信息即可。
然后进入步骤(2)调用抽象方法 resolveName 进行参数值的解析获取,我们看下 RequestParamMethodArgumentResolver 的解析:
// RequestParamMethodArgumentResolver#resolveName // name 就是 @RequestParam 的name值 比如 @RequestParam String orderNo name就是 orderNo // parameter 方法的参数对象 // request 请求对象 protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { // 获取到请求对象 HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); // 多媒体形式的处理 略过 if (servletRequest != null) { Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest); if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) { return mpArg; } } // 略过 Object arg = null; 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) { // 从请求中获取我们的值比如 receiveByRequestParam?orderNo=111&orderNoList=222&orderNoList=333 // 获取 orderNo 的值 String[] paramValues = request.getParameterValues(name); // 这里就获取到 orderNo 的值是 111 的数组 if (paramValues != null) { // 如果数组长度为1 则直接取第一个, 否则全部返回 arg = (paramValues.length == 1 ? paramValues[0] : paramValues); } } // 返回参数值 return arg; }
其实就是从请求对象获取参数的值,返回的要么是一个单个 string 要么是一个字符串数组,然后进入步骤(3)进行一些空值的处理,我们这里简单看一下,他也是模版类做了统一处理:
// AbstractNamedValueMethodArgumentResolver#handleNullValue @Nullable private Object handleNullValue(String name, @Nullable Object value, Class<?> paramType) { if (value == null) { // 只有 Boolean 类型会默认处理为 false if (Boolean.TYPE.equals(paramType)) { return Boolean.FALSE; } // 如果是其他 8 种基本类型 + Void 类型的话 直接报错 空值没法给基本数据类型 else if (paramType.isPrimitive()) { throw new IllegalStateException("Optional " + paramType.getSimpleName() + " parameter '" + name + "' is present but cannot be translated into a null value due to being declared as a " + "primitive type. Consider declaring it as object wrapper for the corresponding primitive type."); } } return value; }
空值处理完,进入步骤(4)值转换,这个很关键也很复杂,因为对象存在各种嵌套,核心的提供转换功能的对象是 ConversionService,spring在它内部填充了很多默认的转换器,我这里贴一个 debug 的图:
他的转换内部是比较复杂的,大致的逻辑其实跟获取参数解析器的方式差不多,首先有一个解析出来的值的类型,然后还有一个方法定义的参数类型,前者是 source 后者是 target ,根据 source 和 target 找一个符合这种类型转换的转换器,这个逻辑是在基础实现类 GenericConversionService 里,我们看下:
// GenericConversionService#getConverter protected GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) { // 根据 source target 定义一个缓存 key ConverterCacheKey key = new ConverterCacheKey(sourceType, targetType); // 缓存加速 缓存中有就直接返回 GenericConverter converter = this.converterCache.get(key); // 缓存中有一个特殊情况就是 NO_MATCH 他也是一种转换器只是不做任何处理 类似我们缓存空对象 if (converter != null) { // 如果是 NO_MATCH 返回空,否则返回具体的转换器 return (converter != NO_MATCH ? converter : null); } // 缓存中没有进行寻找 converter = this.converters.find(sourceType, targetType); // 如果没有发现就取默认转换器 if (converter == null) { /** * 比如你的 source target 是同类型或者继承关系 那不需要转换 NO_OP_CONVERTER 否则返回空 * protected GenericConverter getDefaultConverter(TypeDescriptor sourceType, TypeDescriptor targetType) { * return (sourceType.isAssignableTo(targetType) ? NO_OP_CONVERTER : null); * } */ converter = getDefaultConverter(sourceType, targetType); } // 是同类型或者继承关系缓存默认的转换器 if (converter != null) { this.converterCache.put(key, converter); return converter; } // 不是同类型或者继承关系 也没找到转换器 直接缓存 NO_MATCH // 上边的 NO_OP_CONVERTER 和这里的 NO_MATCH 其实都是 NoOpConverter 只是对象的 name 不一样罢了 /** * NoOpConverter 转换方法 直接返回 source * public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { * return source; * } */ this.converterCache.put(key, NO_MATCH); return null; } // GenericConversionService#find public GenericConverter find(TypeDescriptor sourceType, TypeDescriptor targetType) { // Search the full type hierarchy // 首先根据 source 类型找到转换器支持该类型的 List<Class<?>> sourceCandidates = getClassHierarchy(sourceType.getType()); // 再根据 target 类型找到转换器支持该类型的 List<Class<?>> targetCandidates = getClassHierarchy(targetType.getType()); // 双层 for 循环 找到符合 source -》 target 的转换器 找到了就直接结束 for (Class<?> sourceCandidate : sourceCandidates) { for (Class<?> targetCandidate : targetCandidates) { ConvertiblePair convertiblePair = new ConvertiblePair(sourceCandidate, targetCandidate); GenericConverter converter = getRegisteredConverter(sourceType, targetType, convertiblePair); if (converter != null) { return converter; } } } return null; }
我们这里尝试一个,比如我把参数类型该为 Integer,看下转换器:
@GetMapping("/receiveByRequestParam") public String receiveByRequestParam(@RequestParam Integer orderNo, @RequestParam List<String> orderNoList) { log.info("orderNo={}, orderNoList={}", orderNo, orderNoList); return "success"; }
然后我们打下断点,
看下效果,会根据我们的参数类型进行了转换:
那么我们延伸一个问题,我能自己加一个类型转换器么?比如对接收的字符串转为我自定义的新字符串类:
@ToString @Data public class MyString { private String name; public MyString(String value) { this.name = "hello:" + value; } }其实是可以的,只需要我们加一个字符串到 MyString 类型的转换器即可:
import com.nsws.service.base.dto.MyString; import org.springframework.core.convert.converter.Converter; import org.springframework.stereotype.Component; @Component public class StringToMyStringConverter implements Converter<String, MyString> { @Override public MyString convert(String source) { // 实现你的转换逻辑 return new MyString(source); } }看效果:
另外延伸一点就是,我们写的 Converter 是谁加入到转换服务对象中去的呢?
WebMvcAutoConfiguration 自动装配类里有个 WebMvcAutoConfigurationAdapter 对象,他实现了 WebMvcConfigurer 的 addFormatters 方法,代码如下:
// WebMvcAutoConfigurationAdapter#addFormatters public void addFormatters(FormatterRegistry registry) { ApplicationConversionService.addBeans(registry, this.beanFactory); } public static void addBeans(FormatterRegistry registry, ListableBeanFactory beanFactory) { Set<Object> beans = new LinkedHashSet<>(); beans.addAll(beanFactory.getBeansOfType(GenericConverter.class).values()); beans.addAll(beanFactory.getBeansOfType(Converter.class).values()); beans.addAll(beanFactory.getBeansOfType(Printer.class).values()); beans.addAll(beanFactory.getBeansOfType(Parser.class).values()); for (Object bean : beans) { if (bean instanceof GenericConverter) { registry.addConverter((GenericConverter) bean); } else if (bean instanceof Converter) { registry.addConverter((Converter<?, ?>) bean); } else if (bean instanceof Formatter) { registry.addFormatter((Formatter<?>) bean); } else if (bean instanceof Printer) { registry.addPrinter((Printer<?>) bean); } else if (bean instanceof Parser) { registry.addParser((Parser<?>) bean); } } }
3.2 RequestParamMapMethodArgumentResolver
那我们紧接着看下 RequestParamMapMethodArgumentResolver 是直接实现了 HandlerMethodArgumentResolver 接口,它的 resolveArgument 解析参数值的方法如下:
// RequestParamMapMethodArgumentResolver#resolveArgument public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter); // 参数类型是多 value 形式的类似 table 那种的 map处理 我们重点看下普通的map 解析 if (MultiValueMap.class.isAssignableFrom(parameter.getParameterType())) { // MultiValueMap ... } else { // Regular Map // 获取到 Map<K,V> 的 value 的类型 Class<?> valueType = resolvableType.asMap().getGeneric(1).resolve(); // 是多媒体文件的处理 if (valueType == MultipartFile.class) { MultipartRequest multipartRequest = MultipartResolutionDelegate.resolveMultipartRequest(webRequest); return (multipartRequest != null ? multipartRequest.getFileMap() : new LinkedHashMap<>(0)); } // 是 Part 类型的处理 else if (valueType == Part.class) { HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); if (servletRequest != null && MultipartResolutionDelegate.isMultipartRequest(servletRequest)) { Collection<Part> parts = servletRequest.getParts(); LinkedHashMap<String, Part> result = CollectionUtils.newLinkedHashMap(parts.size()); for (Part part : parts) { if (!result.containsKey(part.getName())) { result.put(part.getName(), part); } } return result; } return new LinkedHashMap<>(0); } // 剩余类型处理 else { // 获取到请求的路径参数 map Map<String, String[]> parameterMap = webRequest.getParameterMap(); Map<String, String> result = CollectionUtils.newLinkedHashMap(parameterMap.size()); // 逐个进行遍历 parameterMap.forEach((key, values) -> { // 如果存在值的情况下,并且只取数组的首个元素 if (values.length > 0) { // 这里只取首个元素 这里要避坑 // 比如你的路径 receiveByRequestParam?orderNo=111&orderNoList=222&orderNoList=333 // 你的 map 里的 key=orderNoList val=222 result.put(key, values[0]); } }); return result; } } }
解析参数值比较简单,就是获取请求的参数 map,然后遍历如果某个 key 有多个值的情况,只取第一个元素,并且要注意这个解析器 RequestParamMapMethodArgumentResolver 没有类型转换的功能。
3.3 PathVariableMethodArgumentResolver
PathVariableMethodArgumentResolver 跟 3.1的 RequestParamMethodArgumentResolver 一样都是继承了 AbstractNamedValueMethodArgumentResolver:
它解析参数值的方法 resolveName 如下:
// 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,然后根据 name 从 map中获取对应的值完事了,并且对于最后 handleResolvedValue 解析的值会放进当前的请求中,方便有的时候需要:
3.4 PathVariableMapMethodArgumentResolver
PathVariableMapMethodArgumentResolver 它又跟上边的 3.2 RequestParamMapMethodArgumentResolver 有点像,直接实现了 HandlerMethodArgumentResolver,所以它也没有转换器的功能,解析方法如下:
// PathVariableMapMethodArgumentResolver#resolveArgument public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { @SuppressWarnings("unchecked") // 也是一样直接获取请求中的路径变量 map Map<String, String> uriTemplateVars = (Map<String, String>) webRequest.getAttribute( HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); // 不为空的话,创建一个新的 map 进行包装返回 if (!CollectionUtils.isEmpty(uriTemplateVars)) { return new LinkedHashMap<>(uriTemplateVars); } // 否则返回一个空map else { return Collections.emptyMap(); } }
MatrixVariableMethodArgumentResolver、MatrixVariableMapMethodArgumentResolver 这两个解析器基本没见用到过,这里就不看了哈,但是要记住前者有转换器的功能,后者无。
4 小结
好啦,本节我们暂时先看这四个解析器,要知道的一个小细节继承了 AbstractNamedValueMethodArgumentResolver 的类的解析出来的参数会多一个转换器的功能,比如RequestParamMethodArgumentResolver、PathVariableMethodArgumentResolver、MatrixVariableMethodArgumentResolver,而 RequestParamMapMethodArgumentResolver、PathVariableMapMethodArgumentResolver、MatrixVariableMapMethodArgumentResolver 这三个是没有转换器功能的,切记,下节我们继续看四个解析器,有理解不对的地方还请指正哈。