原创

Spring MVC 路径解析:相对路径、绝对路径与源码阅读

围绕原有源码分析梳理 Spring MVC 路径问题,区分浏览器 URL 解析、应用上下文路径与服务端资源路径。

路径问题通常来自把浏览器视角与服务器视角混在一起。看到以斜杠开头的字符串时,先确认它是页面链接、重定向地址还是服务端资源定位;同一个写法在不同语境下含义并不相同。

1. 前言

我们在使用java做web开发,进行页面跳转时,一般都直接使用页面的名称进行匹配。springboot-web已经帮我们自动配置好了mvc相关的配置。

    //测试接口
    @RequestMapping
    @Controller
    public class TestController {
		//此处返回index页面没有携带/
        @GetMapping("/hello/kugou")
        public String index(){
            return "index";
        }
    }
    #yml基础配置
    spring:
      mvc:
        view:
          suffix: .html     #配置后缀
    //pom只用了基础的spring-web
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

springboot版本2.x+,使用以上的基础配置,在浏览器进行访问127.0.0.1:8080/hello/kugou时,按照我们的想法应该是正常返回默认静态路径下的index页面。

classpath:/static
classpath:/public
classpath:/resources
classpath:/META-INF/resources

但是结果却是404!!!

Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.

Wed Jun 29 15:38:03 CST 2022
There was an unexpected error (type=Not Found, status=404).

2.分析

  1. 我们都知道MVC的请求流程第一步,前端发起的请求都会被DispatcherServlet拦截,所以将断点打在doDispatch方法中

    ...
    				if (!mappedHandler.applyPreHandle(processedRequest, response)) {
    					return;
    				}
    
    				// Actually invoke the handler(实际调用处理器)
    				//处理请求,得到模型视图
    				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    
    				if (asyncManager.isConcurrentHandlingStarted()) {
    					return;
    				}
    
    				applyDefaultViewName(processedRequest, mv);
    				mappedHandler.applyPostHandle(processedRequest, response, mv);
                    }
                    catch (Exception ex) {
                        dispatchException = ex;
                    }
                    catch (Throwable err) {
                        // As of 4.3, we're processing Errors thrown from handler methods as well,
                        // making them available for @ExceptionHandler methods and other scenarios.
                        dispatchException = new NestedServletException("Handler dispatch failed", err);
                    }
    				//解析视图
                    processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
                }
    ...
    
    
  2. 调用处理映射器,获取到对应的处理handler mappedHandler.getHandler() ==》 com.wu.springlearn.controller.TestController#index()

  3. 处理适配器ha(HandlerAdapter)进行处理,得到模型视图mv(ModelAndView) ha.handle ==》ModelAndView [view="index"; model={}]

  4. 得到模型视图后进行解析 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

    由于解析视图后出现问题,所以直接进入解析视图的方法进行查找。

    	//1. org.springframework.web.servlet.DispatcherServlet#processDispatchResult
    	private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
    			@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
    			@Nullable Exception exception) throws Exception {
    
    		boolean errorView = false;
    
    		if (exception != null) {
    			if (exception instanceof ModelAndViewDefiningException) {
    				logger.debug("ModelAndViewDefiningException encountered", exception);
    				mv = ((ModelAndViewDefiningException) exception).getModelAndView();
    			}
    			else {
    				Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
    				mv = processHandlerException(request, response, handler, exception);
    				errorView = (mv != null);
    			}
    		}
    
    		// Did the handler return a view to render?
    		if (mv != null && !mv.wasCleared()) {
                //渲染模型视图
    			render(mv, request, response);
    			if (errorView) {
    				WebUtils.clearErrorRequestAttributes(request);
    			}
    		}
    		else {
    			if (logger.isTraceEnabled()) {
    				logger.trace("No view rendering, null ModelAndView returned.");
    			}
    		}
    
    		if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
    			// Concurrent handling started during a forward
    			return;
    		}
    
    		if (mappedHandler != null) {
    			// Exception (if any) is already handled..
    			mappedHandler.triggerAfterCompletion(request, response, null);
    		}
    	}
    
    	//2. 渲染render(mv, request, response);   org.springframework.web.servlet.DispatcherServlet#render
    		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.
                //解析视图 ==》 org.springframework.web.servlet.view.InternalResourceView: name 'index'; URL [index.html]
    			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;
    		}
    	}
    	//3. 视图渲染   org.springframework.web.servlet.view.AbstractView#render
    	@Override
    	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);
    	}
    	//4. 渲染合并输出模型renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
    	//org.springframework.web.servlet.view.InternalResourceView#renderMergedOutputModel
    	protected void renderMergedOutputModel(
    			Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
    
    		// Expose the model object as request attributes.
            //将model里的值放到request中,这也是为什么我们在页面能直接使用model里面的参数的原因
    		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).
            //获取当前请求的调度器
            //不带/  ==>requestURI=/hello/index.html   带/  ==>requestURI=/index.html
    		RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
    		......
    	}
    	//5. 获取调度器
        @Override
        public RequestDispatcher getRequestDispatcher(String path) {
    
            Context context = getContext();
            if (context == null) {
                return null;
            }
    
            if (path == null) {
                return null;
            }
    
            int fragmentPos = path.indexOf('#');
            if (fragmentPos > -1) {
                log.warn(sm.getString("request.fragmentInDispatchPath", path));
                path = path.substring(0, fragmentPos);
            }
    
            // If the path is already context-relative, just pass it through
            //如果path是以/开头,则返回由当前path生成的调度器
            if (path.startsWith("/")) {
                return context.getServletContext().getRequestDispatcher(path);
            }
            /* 绝对路径和相对路径的解释
             * From the Servlet 4.0 Javadoc:
             * - The pathname specified may be relative, although it cannot extend
             *   outside the current servlet context.
             * - If it is relative, it must be relative against the current servlet
             *
             * From Section 9.1 of the spec:
             * - The servlet container uses information in the request object to
             *   transform the given relative path against the current servlet to a
             *   complete path.
             *
             * It is undefined whether the requestURI is used or whether servletPath
             * and pathInfo are used. Given that the RequestURI includes the
             * contextPath (and extracting that is messy) , using the servletPath and
             * pathInfo looks to be the more reasonable choice.
             */
            // Convert a request-relative path to a context-relative one
            //将请求相对路径转换为上下文相对路径
            String servletPath = (String) getAttribute(
                    RequestDispatcher.INCLUDE_SERVLET_PATH);
            if (servletPath == null) {
                servletPath = getServletPath();
            }
    
            // Add the path info, if there is any
            String pathInfo = getPathInfo();
            String requestPath = null;
    
            if (pathInfo == null) {
                requestPath = servletPath;
            } else {
                requestPath = servletPath + pathInfo;
            }
    
            int pos = requestPath.lastIndexOf('/');
            String relative = null;
            if (context.getDispatchersUseEncodedPaths()) {
                if (pos >= 0) {
                    relative = URLEncoder.DEFAULT.encode(
                            requestPath.substring(0, pos + 1), StandardCharsets.UTF_8) + path;
                } else {
                    relative = URLEncoder.DEFAULT.encode(requestPath, StandardCharsets.UTF_8) + path;
                }
            } else {
                if (pos >= 0) {
                    relative = requestPath.substring(0, pos + 1) + path;
                } else {
                    relative = requestPath + path;
                }
            }
    		//返回相对路径生成的调度器
            return context.getServletContext().getRequestDispatcher(relative);
        }
    
    

3. 总结

当我们的视图(index)返回带/ 即 “/index”,调度器返回的是绝对路径。

当我们的视图(index)返回不带/ 即 “index”,调度器返回的是相对路径,会将我们的请求的URI进行拼接重组。

正文到此结束