SpringMVC 组件 您所在的位置:网站首页 handleradapter翻译 SpringMVC 组件

SpringMVC 组件

2023-12-23 23:04| 来源: 网络整理| 查看: 265

HandlerMapping 在 SpringMVC 中的作用简而言之就是根据请求,找到对应的处理器。 HandlerMapping 算是 SpringMVC 中最重要的一个组件之一了。

HandlerMapping 的初始化

本小节将介绍 HandlerMapping 的加载方式。首先把初始化 HandlerMapping 的方法丢上来。

private void initHandlerMappings(ApplicationContext context) { this.handlerMappings = null; if (this.detectAllHandlerMappings) { // Find all HandlerMappings in the ApplicationContext, including ancestor contexts. // 查找所有的 HandlerMapping ,包括祖先中的 HandlerMapping // 依赖查找 Map matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false); if (!matchingBeans.isEmpty()) { this.handlerMappings = new ArrayList(matchingBeans.values()); // We keep HandlerMappings in sorted order. // 排个序 AnnotationAwareOrderComparator.sort(this.handlerMappings); } } else { try { // 依赖查找,先查找是否在配置文件中有定义 handlerMapping HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class); this.handlerMappings = Collections.singletonList(hm); } catch (NoSuchBeanDefinitionException ex) { // Ignore, we'll add a default HandlerMapping later. // 找不到或者找到了多个,不处理,后续会创建默认的 } } // Ensure we have at least one HandlerMapping, by registering // a default HandlerMapping if no other mappings are found. // 如果还没有 handlerMapping if (this.handlerMappings == null) { // 创建一个默认的 this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class); if (logger.isTraceEnabled()) { logger.trace("No HandlerMappings declared for servlet '" + getServletName() + "': using default strategies from DispatcherServlet.properties"); } } }

在启动 WebApplicationContext 容器的最后阶段,会通过发布一个 ContextRefreshedEvent 事件,最终回调到 DispatcherServlet#initStrategies 方法,对组件进行加载。 首先,根据属性 detectAllHandlerMappings,来决定是否加载所有的 HandlerMapping。默认为 true。但是我们可以通过配置参数,来改变其初始化的行为。修改配置的方式为在 web.xml 中对 DispatcherServlet 这个 servlet 添加初始化参数。配置如下。

detectAllHandlerMappings false

配置该参数之后,就会走 else 的逻辑,依赖查找一个 HandlerMapping。(前提是在 Spring 配置文件中有配置 HandlerMapping 类型的 Bean,并且只能有一个 HandlerMapping 类型的 Bean,多个的话还是会走默认的配置)。 如果经过上面的加载之后, handlerMappings 还没初始化,那么将会通过加载 DispatcherServlet.properties 文件中定义的属性去查找。DispatcherServlet.properties 中定义了三个 HandlerMapping。分别是 BeanNameUrlHandlerMapping、RequestMappingHandlerMapping、RouterFunctionMapping。这三个 HandlerMapping 仅会赋值给 handlerMappings,并不会注册到 WebApplicationContext 中。 这里顺便再讲一下 与 HandlerMapping 的关系。 注解由 AnnotationDrivenBeanDefinitionParser 进行解析,会解析出两个 HandlerMapping 的 Bean 并注册到 WebApplicationContext 中。这两个 Bean 分别是 RequestMappingHandlerMapping(Order 0) 与 BeanNameUrlHandlerMapping(Order 2) 。

handlerMappings 的初始化就完成了。

HandlerMapping 接口

HandlerMapping 中定义了一个接口方法以及一堆常量。

String BEST_MATCHING_HANDLER_ATTRIBUTE = HandlerMapping.class.getName() + ".bestMatchingHandler"; String LOOKUP_PATH = HandlerMapping.class.getName() + ".lookupPath"; String PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE = HandlerMapping.class.getName() + ".pathWithinHandlerMapping"; String BEST_MATCHING_PATTERN_ATTRIBUTE = HandlerMapping.class.getName() + ".bestMatchingPattern"; String INTROSPECT_TYPE_LEVEL_MAPPING = HandlerMapping.class.getName() + ".introspectTypeLevelMapping"; String URI_TEMPLATE_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName() + ".uriTemplateVariables"; String MATRIX_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName() + ".matrixVariables"; String PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE = HandlerMapping.class.getName() + ".producibleMediaTypes"; @Nullable HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;

getHandler 方法定义了通过该方法,可以根据 request 找到一个 HandlerExecutionChain。 HandlerExecutionChain 中有三个比较重要的成员变量,handler,interceptors,interceptorList。其中 interceptorList 与 interceptors 是一致的,一个以数组的形式存储,另一个以 List 的形式存储。handler 则是当前 HandlerExecutionChain 的执行器。此外,HandlerExecutionChain 还定义了四个执行 HandlerInterceptor 的方法。

HandlerMapping 的类结构图如下图所示,后续将对每一个实现类进行分析。

HandlerMapping.png

可以看到 AbstractHandlerMapping 有三个直接实现,分别是 AbstractHandlerMethodMapping, AbstractUrlHandlerMapping, 以及 RouterFunctionMapping。

AbstractHandlerMapping

AbstractHandlerMapping 实现了 Ordered 接口以及 BeanNameAware 接口,使得其具有将 HandlerMapping 排序以及设置 BeanName 的能力。 AbstractHandlerMapping 是 HandlerMapping 接口的抽象实现。在该抽象实现中,使用了几个工具,比如 UrlPathHelper、 PathMatcher、 CorsConfigurationSource。 UrlPathHelper 用于对 URL 进行处理, PathMatcher 用于对 path 的匹配条件判断, CorsConfigurationSource 则是与跨域问题相关的。有兴趣的可以去查阅相关资料。

先看下 getHandler 方法

public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { Object handler = getHandlerInternal(request); // 如果找不到,则使用默认的 if (handler == null) { handler = getDefaultHandler(); } if (handler == null) { return null; } // Bean name or resolved handler? // 如果是 string 类型,则去依赖查找 Bean if (handler instanceof String) { String handlerName = (String) handler; handler = obtainApplicationContext().getBean(handlerName); } // 获取拦截器链 // 将 handler 也会封装进 HandlerExecutionChain 中 HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request); if (logger.isTraceEnabled()) { logger.trace("Mapped to " + handler); } else if (logger.isDebugEnabled() && !request.getDispatcherType().equals(DispatcherType.ASYNC)) { logger.debug("Mapped to " + executionChain.getHandler()); } if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) { CorsConfiguration config = (this.corsConfigurationSource != null ? this.corsConfigurationSource.getCorsConfiguration(request) : null); CorsConfiguration handlerConfig = getCorsConfiguration(handler, request); config = (config != null ? config.combine(handlerConfig) : handlerConfig); executionChain = getCorsHandlerExecutionChain(request, executionChain, config); } return executionChain; }

AbstractHandlerMapping#getHandler 是 HandlerMapping 中定义的接口方法。其主要逻辑调用了模板方法 AbstractHandlerMapping#getHandlerExecutionChain 来寻找 handler, 由子类实现。如果找到的 handler 为 String 类型,则还需要通过依赖查找,找到其真实的类型。最后将 handler 封装成 HandlerExecutionChain 对象。

protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) { // 获取 HandlerExecutionChain // 如果 handler 已经是 HandlerExecutionChain 类型,则直接用 HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ? (HandlerExecutionChain) handler : new HandlerExecutionChain(handler)); // 获取查找的路径 String lookupPath = this.urlPathHelper.getLookupPathForRequest(request, LOOKUP_PATH); for (HandlerInterceptor interceptor : this.adaptedInterceptors) { // 如果是 MappedInterceptor 类型,则需要匹配查找路径是否满足条件 if (interceptor instanceof MappedInterceptor) { MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor; // 只有匹配了才会被加入到 chain 中 if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) { chain.addInterceptor(mappedInterceptor.getInterceptor()); } } else { // 直接加入到 chain 中 chain.addInterceptor(interceptor); } } return chain; }

封装的过程中将 adaptedInterceptors 中的 Interceptor 加入到该 HandlerExecutionChain 中,如果是 MappedInterceptor, 还需要判断是否满足 MappedInterceptor 中配置的匹配条件。

RouterFunctionMapping

RouterFunctionMapping 是 Spring 5.2 引入的新的 HandlerMapping 。 留到以后再分析吧....

AbstractUrlHandlerMapping 及其子类

AbstractUrlHandlerMapping 一般情况请求都对应到一个处理类上,处理类需要实现 Controller 接口。 主要看下 AbstractUrlHandlerMapping#getHandlerInternal 方法。

protected Object getHandlerInternal(HttpServletRequest request) throws Exception { // 获取请求的 path,最后一个 / 后面的路径,带 / String lookupPath = getUrlPathHelper().getLookupPathForRequest(request); // 设置仅 request 中 request.setAttribute(LOOKUP_PATH, lookupPath); // 根据 lookupPath 和 请求查找 handler // 得到的 handler 添加了 两个 interceptor Object handler = lookupHandler(lookupPath, request); if (handler == null) { // We need to care for the default handler directly, since we need to // expose the PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE for it as well. Object rawHandler = null; // 如果是访问根目录 if ("/".equals(lookupPath)) { // 返回 rootHandler // 单独处理 rawHandler = getRootHandler(); } if (rawHandler == null) { rawHandler = getDefaultHandler(); } if (rawHandler != null) { // Bean name or resolved handler? // 如果是 string 类型,则依赖查找 if (rawHandler instanceof String) { String handlerName = (String) rawHandler; rawHandler = obtainApplicationContext().getBean(handlerName); } validateHandler(rawHandler, request); // 这里会给 handler 注册两个 interceptor handler = buildPathExposingHandler(rawHandler, lookupPath, lookupPath, null); } } return handler; } 首先获取到请求的路径,并且将请求路径设置到 request 的属性中去。 通过 AbstractUrlHandlerMapping#lookupHandler 查找 handler。下面看 lookupHandler 方法。 如果没有找到 handler,会有兜底策略。首先如何请求路径是 "/",则获取 root handler。否则获取默认的 handler。最后还会为当前 handler 关联的 HandlerExecutionChain 添加两个 intercaptors。PathExposingHandlerInterceptor 以及 UriTemplateVariablesHandlerInterceptor。

下面看下 AbstractUrlHandlerMapping#lookupHandler 方法。

protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception { // Direct match? // 查找预先配置的请求与该请求对应的处理器 Object handler = this.handlerMap.get(urlPath); if (handler != null) { // Bean name or resolved handler? // 如果是 String 类型 if (handler instanceof String) { String handlerName = (String) handler; // 依赖查找 handler handler = obtainApplicationContext().getBean(handlerName); } // 验证 handler validateHandler(handler, request); return buildPathExposingHandler(handler, urlPath, urlPath, null); } // Pattern match? // 路径匹配 List matchingPatterns = new ArrayList(); // 遍历 map 中保存的所有的 路径 for (String registeredPattern : this.handlerMap.keySet()) { // 如果匹配请求路径 if (getPathMatcher().match(registeredPattern, urlPath)) { // 加到目标 List 中 matchingPatterns.add(registeredPattern); } // 如果是否后缀模式匹配 else if (useTrailingSlashMatch()) { // 路径加上 / 后进行匹配 if (!registeredPattern.endsWith("/") && getPathMatcher().match(registeredPattern + "/", urlPath)) { // 如果匹配也加入到 matchingPatterns 中 matchingPatterns.add(registeredPattern + "/"); } } } // matchingPatterns 中可能有多个,需要找到最佳匹配 String bestMatch = null; // 排序规则 Comparator patternComparator = getPathMatcher().getPatternComparator(urlPath); if (!matchingPatterns.isEmpty()) { matchingPatterns.sort(patternComparator); if (logger.isTraceEnabled() && matchingPatterns.size() > 1) { logger.trace("Matching patterns " + matchingPatterns); } // 获取第一个 bestMatch = matchingPatterns.get(0); } if (bestMatch != null) { // 根据找到的最佳匹配获取 handler handler = this.handlerMap.get(bestMatch); if (handler == null) { if (bestMatch.endsWith("/")) { // handlerMap 中的 key 去掉 / 再尝试查找 handler = this.handlerMap.get(bestMatch.substring(0, bestMatch.length() - 1)); } // 还找不到就抛异常 if (handler == null) { throw new IllegalStateException( "Could not find handler for best pattern match [" + bestMatch + "]"); } } // Bean name or resolved handler? // 如果 handler 是 String 类型 if (handler instanceof String) { String handlerName = (String) handler; // 依赖查找 handler = obtainApplicationContext().getBean(handlerName); } validateHandler(handler, request); String pathWithinMapping = getPathMatcher().extractPathWithinPattern(bestMatch, urlPath); // There might be multiple 'best patterns', let's make sure we have the correct URI template variables // for all of them Map uriTemplateVariables = new LinkedHashMap(); // 前面虽然排序得到第一个,但是第二个以及后续的可能与第一个是相等的 // 处理有多个匹配相同的情况 for (String matchingPattern : matchingPatterns) { // 如果相等 if (patternComparator.compare(bestMatch, matchingPattern) == 0) { // 提取路径中的模板数据 Map vars = getPathMatcher().extractUriTemplateVariables(matchingPattern, urlPath); // 将 vars 中的 value 进行编码,根据 request 的编码格式 Map decodedVars = getUrlPathHelper().decodePathVariables(request, vars); // 放进 uriTemplateVariables 中 uriTemplateVariables.putAll(decodedVars); } } if (logger.isTraceEnabled() && uriTemplateVariables.size() > 0) { logger.trace("URI variables " + uriTemplateVariables); } return buildPathExposingHandler(handler, bestMatch, pathWithinMapping, uriTemplateVariables); } // No handler found... return null; } 先查询 handlerMap ,也就是查看是否存在预先定义的请求与处理关系。 handlerMap 是能够保存具有通配符(*)的请求路径,并且能对通配符进行处理。 如果在 handlerMap 中找到了。如果 handler 是 String 类型,则会通过依赖查找到对应的 Bean。随后调用 validateHandler 方法对找到的 handler 进行验证。 validateHandler 方法留给子类实现。 SpringMVC 中提供的实现类均没有重写该验证方法。随后调用 AbstractUrlHandlerMapping#buildPathExposingHandler 方法。 如果没有在 handlerMap 中找到。说明没有预先定义请求与处理关系,或者 handlerMap 中保存的请求路径中有通配符,需要对通配符进行处理后再匹配。 遍历 handlerMap 中的所有 key,进行匹配,如果匹配上了,则保存到 matchingPatterns 中。匹配的规则由 PathMatcher 定义,PathMatcher 的具体实现类是 AntPathMatcher。 如果还是匹配不上,那么只能返回 null 了。说明 AbstractUrlHandlerMapping 是匹配不到了,只能通过另外的 HandlerMapping 去匹配。 如果匹配上了,并且有多个,那么需要排个序,找出排序后的第一个路径(String) 作为最佳匹配。 根据找到的最佳匹配去 handlerMap 中查找对应的 Handler。 SimpleUrlHandlerMapping

AbstractUrlHandlerMapping 维护了一个 handlerMap 用来保存请求路径与 handler 的关系,SimpleUrlHandlerMapping 中维护了一个 urlMap,做了同样的事情。 需要注意的是, SimpleUrlHandlerMapping 对应的 HandlerAdapter 为 SimpleControllerHandlerAdapter。因此处理定义在 handlerMap 中的请求对应的 handler 需要实现 org.springframework.web.servlet.mvc.Controller 接口。

AbstractDetectingUrlHandlerMapping

AbstractDetectingUrlHandlerMapping 继承了 AbstractUrlHandlerMapping 抽象类。主要看下 AbstractDetectingUrlHandlerMapping#detectHandlers 方法。

protected void detectHandlers() throws BeansException { ApplicationContext applicationContext = obtainApplicationContext(); // 把所有的 Bean 都查出来了 String[] beanNames = (this.detectHandlersInAncestorContexts ? // 依赖查找,包括祖先容器中的 bean BeanFactoryUtils.beanNamesForTypeIncludingAncestors(applicationContext, Object.class) : // 依赖查找,当前容器中的 bean applicationContext.getBeanNamesForType(Object.class)); // Take any bean name that we can determine URLs for. // 遍历 for (String beanName : beanNames) { // 当前 beanName 关联的 urls // 根据 beanName 去关联 urls String[] urls = determineUrlsForHandler(beanName); if (!ObjectUtils.isEmpty(urls)) { // URL paths found: Let's consider it a handler. // 注册 urls 与 beanName 的对应关系 registerHandler(urls, beanName); } } if ((logger.isDebugEnabled() && !getHandlerMap().isEmpty()) || logger.isTraceEnabled()) { logger.debug("Detected " + getHandlerMap().size() + " mappings in " + formatMappingName()); } } 在当前上下文中查找出所有的 Bean。 根据 beanName 去关联能够处理的 Url, Url 可能有多个。determineUrlsForHandler 有子类实现。 将 找到的 urls 与 handler 进行注册,注册到 handlerMap 中。

随后继续看下子类 BeanNameUrlHandlerMapping#determineUrlsForHandler 的实现。

protected String[] determineUrlsForHandler(String beanName) { List urls = new ArrayList(); if (beanName.startsWith("/")) { urls.add(beanName); } // 获取别名 String[] aliases = obtainApplicationContext().getAliases(beanName); for (String alias : aliases) { if (alias.startsWith("/")) { urls.add(alias); } } return StringUtils.toStringArray(urls); }

在 WebApplicationContext 容器初始化的时候,会遍历所有的 bean,如果遍历到的 beanName 是以 / 开头的,那么会将该 Bean 的 beanName 以及该 Bean 对应的对象存入到 handlerMap 中。也就是说,请求的路径是 beanName 的时候,会调用该 beanName 对应的 对象(也可以成为处理器) 来进行处理。同样,该 bean 也是需要实现 org.springframework.web.servlet.mvc.Controller 接口的。因为根据 handler 寻找 handlerAdapter 的时候同样需要调用 HandlerAdapter 的 support 方法来判断 HandlerAdapter 是否支持使用该 Handler 来进行处理。

AbstractHandlerMethodMapping 及其子类

与 AbstractUrlHandlerMapping 不同的是, AbstractHandlerMethodMapping 以一个方法作为 handler。AbstractHandlerMethodMapping 有两个子类,分别是 RequestMappingHandlerMapping 以及 RequestMappingInfoHandlerMapping。与获取 HandlerMethod 相关的主线逻辑还是在 AbstractHandlerMethodMapping 中的,所以两个字类就不展开分析了,有兴趣可以去我的 Github 的相应 repo 下查看源码注释。的同样先看下 AbstractHandlerMethodMapping#getHandlerInternal 方法。

protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception { // 获取到请求的路径 String lookupPath = getUrlPathHelper().getLookupPathForRequest(request); // 设置到 request 中 request.setAttribute(LOOKUP_PATH, lookupPath); // 请求锁 // mappingRegistry 中持有一个读写锁 this.mappingRegistry.acquireReadLock(); try { // 根据 path 和 request 获取到 HandlerMethod HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request); // 如果 handlerMethod 中的 handler 为 String 类型,则找到其真实类型。 return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null); } finally { this.mappingRegistry.releaseReadLock(); } } 首先还是获取到请求的路径。 随后获取读锁,说明 mappingRegistry 在注册的时候,是不能够获取 Handler 的。 随后根据 path 去查找 HandlerMethod。

看下 AbstractUrlHandlerMapping#lookupHandlerMethod 方法。

protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception { List matches = new ArrayList(); // 获取匹配条件 List directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath); if (directPathMatches != null) { // 找到的条件保存到 matches 中 addMatchingMappings(directPathMatches, matches, request); } if (matches.isEmpty()) { // No choice but to go through all mappings... addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request); } if (!matches.isEmpty()) { // 默认获取第一个作为 bestMatch Match bestMatch = matches.get(0); // 如果有多个 if (matches.size() > 1) { Comparator comparator = new MatchComparator(getMappingComparator(request)); // 排序 matches.sort(comparator); // 获取第一个 bestMatch = matches.get(0); if (logger.isTraceEnabled()) { logger.trace(matches.size() + " matching mappings: " + matches); } if (CorsUtils.isPreFlightRequest(request)) { return PREFLIGHT_AMBIGUOUS_MATCH; } Match secondBestMatch = matches.get(1); // 如果第二个与第一个相同,则抛异常 if (comparator.compare(bestMatch, secondBestMatch) == 0) { Method m1 = bestMatch.handlerMethod.getMethod(); Method m2 = secondBestMatch.handlerMethod.getMethod(); String uri = request.getRequestURI(); throw new IllegalStateException( "Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}"); } } // 设置当前请求的 bestMatchingHandler 属性 request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod); handleMatch(bestMatch.mapping, lookupPath, request); return bestMatch.handlerMethod; } else { // 无匹配 return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request); } }

先提前介绍一下比较重要的三个 map:

urlLookup: 保存了 url 与 RequestMappingInfo 的对应关系。 mappingLookup:保存了 RequestMappingInfo 与 handlerMethod 的关系。 nameLookup:

继续上面的方法的分析:

先通过 url 查找 mappingRegistry#urlLookup 注册的请求路径与 RequestMappingInfo 的对应关系(在启动的时候会初始化该对应关系),保存在 directPathMatches 中。可能会有多个,一个路径可能有多个处理方法,比如请求方法是不同的。也可能查找不到,比如带有 @PathVariable 的请求路径不会保存在 urlLookup 关系中。 如果找到了对应关系 将找到的封装成 Match。 如果找不到对应的关系,一般情况为 handlerMethod 中存在 @PathVariable 参数 去 mappingRegistry#mappingLookup 对应关系中继续查找。 通过遍历所有的 RequestMappingInfo 去 match request。 match 的逻辑在 RequestMappingInfo#getMatchingCondition 方法中。最后返回 match 的 RequestMappingInfo。并添加到 matches 列表中。 获取 matches 列表中的第一个,即为最佳匹配。如果 matchers 列表中存在大于 1 个,则需要根据规则排序后取第一个。详细逻辑这里就不展开了,可以自己去 debug 源码。 随后调用 RequestMappingInfoHandlerMapping#handleMatch 对 request 进行处理。在 handleMatch 中会对请求路径中的参数提取出来保存到 map 中。 并设置到 request 的 uriTemplateVariables 属性中。 最后返回找到的 HandlerMethod。 如果在 mappingLookup 中依然没有找到匹配的。则会调用 RequestMappingInfoHandlerMapping#handleNoMatch 方法。一般情况下会返回 null。对应页面上显示的就是 404。

回到上面的 AbstractHandlerMethodMapping#getHandlerInternal 方法,如果找到了 HandlerMethod,还会调用 HandlerMethod#createWithResolvedBean 方法,该方法的唯一作用就是如果 HandlerMethod 中的 handler 是 String 类型,则通过依赖查找,把真正的类型找出来。

至此,寻找 Handler 的相关方法也已经分析完了。整体来说还有很多细节需要去写的,鉴于控制下文章篇幅,一些匹配的方法就没展开分析了。有兴趣小伙伴可以去深入 debug。

下一篇 HandlerAdapter 见~

推荐阅读


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有