标签:patch ref pes trail ppi back 没有 nsa plain
对于请求的处理,Spring首先考虑的是对于Multipart的处理,如果是MultipartContent类型的request,则转换为MultipartHttpServletRequest类型的Request。
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException { if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) { if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) { if (request.getDispatcherType().equals(DispatcherType.REQUEST)) { logger.trace("Request already resolved to MultipartHttpServletRequest, e.g. by MultipartFilter"); } } else if (hasMultipartException(request) ) { logger.debug("Multipart resolution previously failed for current request - " + "skipping re-resolution for undisturbed error rendering"); } else { try { return this.multipartResolver.resolveMultipart(request); } catch (MultipartException ex) { if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) != null) { logger.debug("Multipart resolution failed for error dispatch", ex); // Keep processing error dispatch with regular request handle below } else { throw ex; } } } } // If not returned before: return original request. return request; }
在Spring加载的过程中,Spring会将类型为SimpleUrlHandlerMapping的实例加载到this.HandlerMappings中,按照常理推断,根据request提取对应的Handler,但是该Handler继承自AbstractController类型实例,与HandlerExecutionChain并无任何关联,那么这一步是如何封装的呢?
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { if (this.handlerMappings != null) { for (HandlerMapping mapping : this.handlerMappings) { HandlerExecutionChain handler = mapping.getHandler(request); if (handler != null) { return handler; } } } return null; }
在系统启动的时候Spring会将所有的映射类型的bean注册到this.handlerMappings变量中,所以此函数的目的就是遍历所有的HandlerMapping,并调用其getHandler方法进行封装处理。以SimpleURLHandlerMapping为例查看其getHandler方法如下:
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { //根据request获取对应的handler Object handler = getHandlerInternal(request); if (handler == null) { handler = getDefaultHandler(); } if (handler == null) { //如果没有对应request的handler则使用默认的handler return null; } // 如果也没有提供默认的handler则无法继续处理返回null if (handler instanceof String) { String handlerName = (String) handler; handler = obtainApplicationContext().getBean(handlerName); } 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 (CorsUtils.isCorsRequest(request)) { CorsConfiguration globalConfig = this.corsConfigurationSource.getCorsConfiguration(request); CorsConfiguration handlerConfig = getCorsConfiguration(handler, request); CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig); executionChain = getCorsHandlerExecutionChain(request, executionChain, config); } return executionChain; }
上述函数首先使用getHandlerInternal方法根据request信息获取对应的Handler,如果以SimpleUrlHandlerMapping为例分析,那么我们推断此步骤提供的功能很可能就是根据URL找到匹配的Controller并返回,当然如果没有找到对应的Controller处理器那么程序会尝试去查找配置中的默认处理器,当然,当查找的Controller为String类型时,那就意味着返回的是配置的bean名称,需要根据bean名称查找对应的bean,最后,还要通过getHandlerExecutionChain方法返回的Handler进行封装,以保证满足返回类型的匹配。下面详细分析这个过程:
首先从根据request查找对应的Handler开始分析:
protected Object getHandlerInternal(HttpServletRequest request) throws Exception { //截取用于匹配的URL有效路径 String lookupPath = getUrlPathHelper().getLookupPathForRequest(request); //根据路径寻找Handler Object handler = lookupHandler(lookupPath, request); if (handler == null) { Object rawHandler = null; if ("/".equals(lookupPath)) { //如果请求的路径仅仅是“/",那么使用RootHandler进行处理 rawHandler = getRootHandler(); } if (rawHandler == null) { //无法找到Handler则使用默认的Handler rawHandler = getDefaultHandler(); } if (rawHandler != null) { // 根据beanName获取对应的bean if (rawHandler instanceof String) { String handlerName = (String) rawHandler; rawHandler = obtainApplicationContext().getBean(handlerName); } //模板方法 validateHandler(rawHandler, request); handler = buildPathExposingHandler(rawHandler, lookupPath, lookupPath, null); } } return handler; }
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? if (handler instanceof String) { String handlerName = (String) handler; handler = obtainApplicationContext().getBean(handlerName); } validateHandler(handler, request); return buildPathExposingHandler(handler, urlPath, urlPath, null); } // Pattern match? List<String> matchingPatterns = new ArrayList<>(); for (String registeredPattern : this.handlerMap.keySet()) { if (getPathMatcher().match(registeredPattern, urlPath)) { matchingPatterns.add(registeredPattern); } else if (useTrailingSlashMatch()) { if (!registeredPattern.endsWith("/") && getPathMatcher().match(registeredPattern + "/", urlPath)) { matchingPatterns.add(registeredPattern +"/"); } } } String bestMatch = null; Comparator<String> 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 = this.handlerMap.get(bestMatch); if (handler == null) { if (bestMatch.endsWith("/")) { 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? 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<String, String> uriTemplateVariables = new LinkedHashMap<>(); for (String matchingPattern : matchingPatterns) { if (patternComparator.compare(bestMatch, matchingPattern) == 0) { Map<String, String> vars = getPathMatcher().extractUriTemplateVariables(matchingPattern, urlPath); Map<String, String> decodedVars = getUrlPathHelper().decodePathVariables(request, vars); 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; }
根据URL获取对应的Handler的匹配规则代码实现起来虽然很长,但是并不难理解,考虑了直接匹配与通配符两种情况,其中要提到的是buildPathExposingHandler函数,它将Handler封装成了HandlerExecutionChain类型。
protected Object buildPathExposingHandler(Object rawHandler, String bestMatchingPattern, String pathWithinMapping, @Nullable Map<String, String> uriTemplateVariables) { HandlerExecutionChain chain = new HandlerExecutionChain(rawHandler); chain.addInterceptor(new PathExposingHandlerInterceptor(bestMatchingPattern, pathWithinMapping)); if (!CollectionUtils.isEmpty(uriTemplateVariables)) { chain.addInterceptor(new UriTemplateVariablesHandlerInterceptor(uriTemplateVariables)); } return chain; }
在函数中我们看到了通过将Handler以参数形式传入,并构建HandlerExecutionChain类型实例,加入了两个拦截器。此时我们似乎已经了解了Spring这样大番周折的目的。链处理机制,是Spring中非常常用的处理方式,是AOP中重要组成部分,可以方便地对目标对象进行扩展及拦截,这是非常优秀的设计。
getHandlerExecutionChain函数最主要的目的是将配置中的对应拦截器加入到执行链中,以保证这些拦截器可以有效地作用于目标对象。
protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) { HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ? (HandlerExecutionChain) handler : new HandlerExecutionChain(handler)); String lookupPath = this.urlPathHelper.getLookupPathForRequest(request); for (HandlerInterceptor interceptor : this.adaptedInterceptors) { if (interceptor instanceof MappedInterceptor) { MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor; if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) { chain.addInterceptor(mappedInterceptor.getInterceptor()); } } else { chain.addInterceptor(interceptor); } } return chain; }
每个请求都应该对应着一个Handler,因为每个请求都会在后台有相应的逻辑对应,而逻辑的实现就是在Handler中,所以一旦遇到没有找到Handler的情况(正常情况下如果没有URL匹配的Handler,开发人员可以设置默认的Handler来处理请求,但是如果默认请求也未设置就会出现Handler为空的情况),就只能通过response向用户返回错误信息。
protected void noHandlerFound(HttpServletRequest request, HttpServletResponse response) throws Exception { if (pageNotFoundLogger.isWarnEnabled()) { pageNotFoundLogger.warn("No mapping for " + request.getMethod() + " " + getRequestUri(request)); } if (this.throwExceptionIfNoHandlerFound) { throw new NoHandlerFoundException(request.getMethod(), getRequestUri(request), new ServletServerHttpRequest(request).getHeaders()); } else { response.sendError(HttpServletResponse.SC_NOT_FOUND); } }
在WebApplicationContext的初始化过程中我们讨论了HandlerAdapters的初始化,了解了在默认的情况下普通的Web请求会交给SimpleControllerHandlerAdapter去处理。下面我们以SimpleControllerHandlerAdapter为例来分析获取适配器的逻辑。
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException { if (this.handlerAdapters != null) { for (HandlerAdapter adapter : this.handlerAdapters) { if (adapter.supports(handler)) { return adapter; } } } throw new ServletException("No adapter for handler [" + handler + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler"); }
通过上面的函数我们了解到,对于适配器的获取逻辑无非就是遍历所有适配器来选择合适的适配器并返回它,而某个适配器是否适用于当前的Handler逻辑被封装在具体的适配器中国。进一步查看SimpleControllerHandlerAdapter中supports方法:
public boolean supports(Object handler) { return (handler instanceof Controller); }
分析到这里,一切都明了了,SimpleControllerHandlerAdapter就是用于处理普通的Web请求的,而且对于SpringMVC来说,我们会把逻辑封装至Controller的子类中。
在研究Spring对缓存处理的功能之前,我们先了解一个概念:Last-Modified缓存机制。
(1)在客户端第一次输入URL时,服务器端会返回内容和状态码200,表示请求成功,同时会添加一个“Last-modified”的响应头,表示此文件在服务器上的最后更新时间。
(2)客户端第二次请求此URL时,客户端会向服务器发送请求头“If-Modified-Since”,询问服务器该时间段之后当前请求内容是否有被修改过,如果服务器端的内容没有发生改变,则自动返回HTTP 304状态码(只要响应头,内容为空,这样就节省了网络带宽)。
Spring提供的对Last-modified机制的支持,只需要实现LastModified接口。如下所示:
public class HelloController extends AbstractController implements LastModified { private long lastModified; @Override protected ModelAndView handleRequestInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception { //点击后再次请求当前页面 httpServletResponse.getWriter().write("<a href=‘‘>this</a>"); return null; } @Override public long getLastModified(HttpServletRequest httpServletRequest) { if (lastModified == 0L){ //第一次或者逻辑有变化的时候,应该重新返回内容最新修改的时间戳 lastModified = System.currentTimeMillis(); } return lastModified; } }
HelloController只需要实现LastModified接口的getLastModified方法,保证内容发生改变时返回最新的修改时间即可。
Spring判断是否过期,通过判断请求的“If-Modified-Since”是否大于等于当前的getLastModified方法 的时间戳,如果是,则认为没有修改。上面的Controller与普通的Controller并无太大区别,声明如下:
<bean name="/helloController" class="com.joe.controller.HelloController"/>
Servlet API定义的servlet过滤器可以在servlet处理每个web请求的前后分别对它进行前置处理和后置处理,此外,有些时候,你可能只想处理由某些SpringMVC处理程序处理的web请求,并在这些处理程序返回的模型属性被传递到视图之前,对它们进行一些操作。
SpringMVC允许你通过处理拦截web请求,进行前置处理和后置处理。处理拦截是在Spring的web应用程序上下文中配置的,因此它们可以利用各种容器特性,并引用容器中声明的任何bean。处理拦截是针对特殊的处理程序映射进行注册的,因此它只拦截通过这些处理程序映射的请求。每个处理拦截都必须实现HandlerInterceptor接口,它包含三个需要你实现的回调方法:preHandle、postHandle、afterHandle。第一个和第二个方法分别是在处理请求之前和之后被调用的。第二个方法还允许访问返回的ModelAndView对象,因此可以在它里面操作模型属性。最后一个方法是在所有请求处理完成之后被调用的。
对于逻辑处理其实是通过适配器中转调用Handler并返回视图的,对应代码:
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
之前分析过,对于普通的web请求,Spring默认使用SimpleControllerHandlerAdapter类进行处理,我们进入SimpleControllerHandlerAdapter类的handle方法:
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return ((Controller) handler).handleRequest(request, response); }
有时候系统运行过程中出现异常,而我们并不希望就此中断对用户的服务,而是至少告知客户当前系统在处理逻辑的过程中出现了异常,甚至告知他们因为什么原因导致的。Spring中的异常处理机制会帮助我们完成这个工作。其实,这里Spring主要的工作就是将逻辑引导至HandlerExceptionResolver类的resolveException方法,而HandlerExceptionResolver的使用,我们在讲解WebApplicationContext的初始化的时候已经介绍过了。
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) throws Exception { // Success and error responses may use different content types request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); // Check registered HandlerExceptionResolvers... ModelAndView exMv = null; if (this.handlerExceptionResolvers != null) { for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) { exMv = resolver.resolveException(request, response, handler, ex); if (exMv != null) { break; } } } if (exMv != null) { if (exMv.isEmpty()) { request.setAttribute(EXCEPTION_ATTRIBUTE, ex); return null; } // We might still need view name translation for a plain error model... if (!exMv.hasView()) { String defaultViewName = getDefaultViewName(request); if (defaultViewName != null) { exMv.setViewName(defaultViewName); } } if (logger.isTraceEnabled()) { logger.trace("Using resolved error view: " + exMv, ex); } if (logger.isDebugEnabled()) { logger.debug("Using resolved error view: " + exMv); } WebUtils.exposeErrorRequestAttributes(request, ex, getServletName()); return exMv; } throw ex; }
无论是一个系统还是一个站点,最重要的工作都是与用户进行交互,用户操作系统后无论下发的命令成功与否都需要给用户一个反馈,以便于用户进行下一步的判断。所以,在逻辑处理的最后一定会涉及一个页面跳转的问题。
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception { // Determine locale for request and apply it to the response. Locale locale = (this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale()); response.setLocale(locale); View view; String viewName = mv.getViewName(); if (viewName != null) { // We need to resolve the view name. view = resolveViewName(viewName, mv.getModelInternal(), locale, request); if (view == null) { throw new ServletException("Could not resolve view with name ‘" + mv.getViewName() + "‘ in servlet with name ‘" + getServletName() + "‘"); } } else { // No need to lookup: the ModelAndView object contains the actual View object. view = mv.getView(); if (view == null) { throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " + "View object in servlet with name ‘" + getServletName() + "‘"); } } // Delegate to the View object for rendering. if (logger.isTraceEnabled()) { logger.trace("Rendering view [" + view + "] "); } try { if (mv.getStatus() != null) { response.setStatus(mv.getStatus().value()); } view.render(mv.getModelInternal(), request, response); } catch (Exception ex) { if (logger.isDebugEnabled()) { logger.debug("Error rendering view [" + view + "]", ex); } throw ex; } }
在上文中我们提到DispatcherServlet会根据ModelAndView选择合适的视图来进行渲染,而这一功能就是在resolveViewName函数中完成的。
protected View resolveViewName(String viewName, @Nullable Map<String, Object> model, Locale locale, HttpServletRequest request) throws Exception { if (this.viewResolvers != null) { for (ViewResolver viewResolver : this.viewResolvers) { View view = viewResolver.resolveViewName(viewName, locale); if (view != null) { return view; } } } return null; }
我们以org.Springframework.web.servlet.view.InternalResourceViewResolver 为例来分析ViewResolver逻辑的解析过程,其中resolveViewName函数的实现是在其父类AbstractCachingViewResolver中完成的。
public View resolveViewName(String viewName, Locale locale) throws Exception { if (!isCache()) { //不存在缓存的情况下直接创建视图 return createView(viewName, locale); } else { //直接从缓存中提取 Object cacheKey = getCacheKey(viewName, locale); View view = this.viewAccessCache.get(cacheKey); if (view == null) { synchronized (this.viewCreationCache) { view = this.viewCreationCache.get(cacheKey); if (view == null) { // Ask the subclass to create the View object. view = createView(viewName, locale); if (view == null && this.cacheUnresolved) { view = UNRESOLVED_VIEW; } if (view != null) { this.viewAccessCache.put(cacheKey, view); this.viewCreationCache.put(cacheKey, view); } } } } else { if (logger.isTraceEnabled()) { logger.trace(formatKey(cacheKey) + "served from cache"); } } return (view != UNRESOLVED_VIEW ? view : null); } }
在父类UrlBasedViewResolver中重写了createView函数。
protected View createView(String viewName, Locale locale) throws Exception { // 如果当前解析器不支持当前视图的情况如viewName为空等情况 if (!canHandle(viewName, locale)) { return null; } //处理前缀为redirect:xx的情况 if (viewName.startsWith(REDIRECT_URL_PREFIX)) { String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length()); RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible()); String[] hosts = getRedirectHosts(); if (hosts != null) { view.setHosts(hosts); } return applyLifecycleMethods(REDIRECT_URL_PREFIX, view); } //处理前缀为forward:xx的情况 if (viewName.startsWith(FORWARD_URL_PREFIX)) { String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length()); InternalResourceView view = new InternalResourceView(forwardUrl); return applyLifecycleMethods(FORWARD_URL_PREFIX, view); } // Else fall back to superclass implementation: calling loadView. return super.createView(viewName, locale); }
protected View createView(String viewName, Locale locale) throws Exception { return loadView(viewName, locale); }
protected View loadView(String viewName, Locale locale) throws Exception { AbstractUrlBasedView view = buildView(viewName); View result = applyLifecycleMethods(viewName, view); return (view.checkResource(locale) ? result : null); }
protected AbstractUrlBasedView buildView(String viewName) throws Exception { Class<?> viewClass = getViewClass(); Assert.state(viewClass != null, "No view class"); AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(viewClass); //添加前缀以及后缀 view.setUrl(getPrefix() + viewName + getSuffix()); String contentType = getContentType(); if (contentType != null) { //设置contentType view.setContentType(contentType); } view.setRequestContextAttribute(getRequestContextAttribute()); view.setAttributesMap(getAttributesMap()); Boolean exposePathVariables = getExposePathVariables(); if (exposePathVariables != null) { view.setExposePathVariables(exposePathVariables); } Boolean exposeContextBeansAsAttributes = getExposeContextBeansAsAttributes(); if (exposeContextBeansAsAttributes != null) { view.setExposeContextBeansAsAttributes(exposeContextBeansAsAttributes); } String[] exposedContextBeanNames = getExposedContextBeanNames(); if (exposedContextBeanNames != null) { view.setExposedContextBeanNames(exposedContextBeanNames); } return view; }
通读以上代码,我们发现对于InternalResourceViewResolver 所提供的解析功能主要考虑到了几个方面的处理:
? 基于效率的考虑,提供了缓存的支持。
? 提供了对redirect:xx和forward:xx前缀的支持。
? 添加了前缀及后缀,并向View中加入了必需的属性设置。
当通过viewName解析到对应的View后,就可以进一步地处理跳转逻辑了。
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); }
对于ModelAndView的使用,可以将一些属性直接放入其中,然后在页面上直接通过JSTL语法或者原始的request获取。这是一个很方便也很神奇的功能,但是实现却并不复杂,无非是把我们将要用到的属性放入request中,以便在其他地方可以直接调用,而解析这些属性的工作就是在createMergedOutputModel函数中完成的。
protected Map<String, Object> createMergedOutputModel(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) { @SuppressWarnings("unchecked") Map<String, Object> pathVars = (this.exposePathVariables ? (Map<String, Object>) request.getAttribute(View.PATH_VARIABLES) : null); // Consolidate static and dynamic model attributes. int size = this.staticAttributes.size(); size += (model != null ? model.size() : 0); size += (pathVars != null ? pathVars.size() : 0); Map<String, Object> mergedModel = new LinkedHashMap<>(size); mergedModel.putAll(this.staticAttributes); if (pathVars != null) { mergedModel.putAll(pathVars); } if (model != null) { mergedModel.putAll(model); } // Expose RequestContext? if (this.requestContextAttribute != null) { mergedModel.put(this.requestContextAttribute, createRequestContext(request, response, mergedModel)); } return mergedModel; }
protected void renderMergedOutputModel( Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { // 将model中的数据以属性的方式设置到request中 exposeModelAsRequestAttributes(model, request); // Expose helpers as request attributes, if any. exposeHelpers(request); // Determine the path for the request dispatcher. String dispatcherPath = prepareForRendering(request, response); // Obtain a RequestDispatcher for the target resource (typically a JSP). RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath); if (rd == null) { throw new ServletException("Could not get RequestDispatcher for [" + getUrl() + "]: Check that the corresponding file exists within your web application archive!"); } // If already included or response already committed, perform include, else forward. if (useInclude(request, response)) { response.setContentType(getContentType()); if (logger.isDebugEnabled()) { logger.debug("Including [" + getUrl() + "]"); } rd.include(request, response); } else { // Note: The forwarded resource is supposed to determine the content type itself. if (logger.isDebugEnabled()) { logger.debug("Forwarding to [" + getUrl() + "]"); } rd.forward(request, response); } }
参考:《Spring源码深度解析》 郝佳 编著:
标签:patch ref pes trail ppi back 没有 nsa plain
原文地址:https://www.cnblogs.com/Joe-Go/p/10304928.html