Java spring mvc请求详情介绍

目录
  • 一、源码执行流程
  • 二、源码执行流程图
  • 三、spring mvc中的一核心组件
  • 四、源码分析
  • 五、获取组件相关逻辑:
  • 六、获取参数,执行方法源码分析
  • 七、渲染视图逻辑

前言:

  • 本文源码基于spring-framework-5.3.10
  • mvcspring源码中的一个子模块!

一、源码执行流程

  • 用户发送请求至前端控制器DispatcherServlet。
  • DispatcherServlet收到请求调用处理器映射器HandlerMapping。处理器映射器根据请求url找到具体的处理器,生成处理器执行链HandlerExecutionChain(包括处理器对象和处理器拦截器)一并返回给DispatcherServlet。
  • DispatcherServlet根据处理器Handler获取处理器适配器HandlerAdapter,执行HandlerAdapter处理一系列的操作,如:参数封装,数据格式转换,数据验证等操作
  • 执行处理器Handler(Controller,也叫页面控制器)。Handler执行完成返回ModelAndViewHandlerAdapter将Handler执行结果ModelAndView返回到DispatcherServlet。
  • DispatcherServletModelAndView传给ViewReslover视图解析器。ViewReslover解析后返回具体View
  • DispatcherServlet对View进行渲染视图(即将模型数据model填充至视图中)。
  • DispatcherServlet响应用户。

二、源码执行流程图

三、spring mvc中的一核心组件

  • DispatcherServlet: 前端调度器 , 负责将请求拦截下来分发到各控制器方法中
  • HandlerMapping: 负责根据请求的URL和配置@RequestMapping映射去匹配, 匹配到会返回Handler(具体控制器的方法)
  • HandlerAdaper: 负责调用Handler-具体的方法- 返回视图的名字 Handler将它封装到ModelAndView(封装视图名,request域的数据)
  • ViewReslover: 根据ModelAndView里面的视图名地址去找到具体的jsp封装在View对象中
  • View:进行视图渲染(将jsp转换成html内容 --这是Servlet容器的事情了) 最终response到的客户端

四、源码分析

/**
 * 最核心的控制器
 * 源码位置:org.springframework.web.servlet.DispatcherServlet.doDispatch(HttpServletRequest, HttpServletResponse)
 */
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 {
			// 验证是不是上传的请求,上传的请求会转化为MultipartHttpServletRequest
			processedRequest = checkMultipart(request);
			multipartRequestParsed = (processedRequest != request);

			// 进行映射
			mappedHandler = getHandler(processedRequest);
			if (mappedHandler == null) {
				noHandlerFound(processedRequest, response);
				return;
			}

			// 找到最合适的HandlerAdapter,按照顺序,那个先解析到就是那个
			HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

			// Process last-modified header, if supported by the handler.  HTTP缓存相关
			String method = request.getMethod();
			boolean isGet = HttpMethod.GET.matches(method);
			if (isGet || HttpMethod.HEAD.matches(method)) {
				long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
				if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
					return;
				}
			}
			// 前置拦截器
			if (!mappedHandler.applyPreHandle(processedRequest, response)) {
				// 返回false就不进行后续处理了
				return;
			}

			// Actually invoke the handler.
			// 获取参数,执行方法
			mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

			if (asyncManager.isConcurrentHandlingStarted()) {
				return;
			}
			// 如果mv有  视图没有,给你设置默认视图
			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);
	}
	catch (Exception ex) {
		triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
	}
	catch (Throwable err) {
		triggerAfterCompletion(processedRequest, response, mappedHandler,
				new NestedServletException("Handler processing failed", err));
	}
	finally {
		if (asyncManager.isConcurrentHandlingStarted()) {
			// Instead of postHandle and afterCompletion
			if (mappedHandler != null) {
				mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
			}
		}
		else {
			// Clean up any resources used by a multipart request.
			if (multipartRequestParsed) {
				cleanupMultipart(processedRequest);
			}
		}
	}
}

五、获取组件相关逻辑:

原理:谁先解析到就用谁!

/**
 * 获取处理器映射器
 * 源码位置:org.springframework.web.servlet.DispatcherServlet.getHandler(HttpServletRequest)
 */
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
	if (this.handlerMappings != null) {
		/** 拿到所有handlerMappings (容器启动阶段初始化:拿到所有实现了HandlerMapping的Bean)
		 * @see DispatcherServlet#initHandlerMappings
		 * 测试发现: 不同的HandlerMapping可以有相同path, 谁先解析到就用哪个
		 * */
		for (HandlerMapping mapping : this.handlerMappings) {
			HandlerExecutionChain handler = mapping.getHandler(request);
			if (handler != null) {
				return handler;
			}
		}
	}
	return null;
}

/**
 * 获取处理器适配器
 * 源码位置:org.springframework.web.servlet.DispatcherServlet.getHandlerAdapter(Object)
 */
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");
}

六、获取参数,执行方法源码分析

/**
 * 获取参数,执行方法最外层的调用
 * 源码位置:org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(HttpServletRequest, HttpServletResponse, Object)
 */
public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
		throws Exception {
	// 直接调用这个方法
	return handleInternal(request, response, (HandlerMethod) handler);
}

/**
 * 获取参数,执行方法内部的调用逻辑
 * 源码位置:org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(HttpServletRequest, HttpServletResponse, HandlerMethod)
 */
protected ModelAndView handleInternal(HttpServletRequest request,
		HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

	ModelAndView mav;
	// 检查当前请求的method是否为支持的method(默认Null,可通过继承AbstractController设置supportedMethods)
	// 检查当前请求是否必须session  (默认false,可通过继承AbstractController设置requireSession)
	checkRequest(request);

	/**
	 * 判断当前是否需要支持在同一个session中只能线性地处理请求:一个session同时只能处理一个线程
	 * 因为锁是通过 synchronized 是 JVM 进程级,所以在分布式环境下,
	 * 无法达到同步相同 Session 的功能。默认情况下,synchronizeOnSession 为 false
	 */
	if (this.synchronizeOnSession) {
		// 获取当前请求的session对象
		HttpSession session = request.getSession(false);
		if (session != null) {
			// 为当前session生成一个唯一的可以用于锁定的key
			Object mutex = WebUtils.getSessionMutex(session);
			synchronized (mutex) {
				// 对HandlerMethod进行参数等的适配处理,并调用目标handler
				mav = invokeHandlerMethod(request, response, handlerMethod);
			}
		}
		else {
			// 如果当前不存在session,则直接对HandlerMethod进行适配
			mav = invokeHandlerMethod(request, response, handlerMethod);
		}
	}
	else {
		// *如果当前不需要对session进行同步处理,则直接对HandlerMethod进行适配
		mav = invokeHandlerMethod(request, response, handlerMethod);
	}

	//判断当前请求头中是否包含Cache-Control请求头,如果不包含,则对当前response进行处理
	if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
		// 如果当前SessionAttribute中存在配置的attributes,则为其设置过期时间。
		// 这里SessionAttribute主要是通过@SessionAttribute注解生成的
		if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
			applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
		}
		else {
			// 如果当前不存在SessionAttributes,则判断当前是否存在Cache-Control设置,
			// 如果存在,则按照该设置进行response处理,如果不存在,则设置response中的
			// Cache的过期时间为-1,即立即失效
			prepareResponse(response);
		}
	}

	return mav;
}

/**
 * 获取参数,执行方法前的准备逻辑
 * 源码位置:org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(HttpServletRequest, HttpServletResponse, HandlerMethod)
 */
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
		HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
	// 把我们的请求req resp包装成 ServletWebRequest
	ServletWebRequest webRequest = new ServletWebRequest(request, response);
	try {
		// 获取容器中全局配置的InitBinder和当前HandlerMethod所对应的Controller中
		// 配置的InitBinder,用于进行参数的绑定
		WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);

		// 获取容器中全局配置的ModelAttribute和当前HandlerMethod所对应的Controller 中配置的ModelAttribute,
		// 这些配置的方法将会在目标方法调用之前进行调用
		ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);

		// 封装handlerMethod,会在调用前解析参数、调用后对返回值进行处理
		ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
		if (this.argumentResolvers != null) {
			// 让invocableMethod拥有参数解析能力
			invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
		}
		if (this.returnValueHandlers != null) {
			// 让invocableMethod拥有返回值处理能力
			invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
		}
		// 让invocableMethod拥有InitBinder解析能力
		invocableMethod.setDataBinderFactory(binderFactory);
		// 设置ParameterNameDiscoverer,该对象将按照一定的规则获取当前参数的名称
		invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
		// ModelAndView处理容器
		ModelAndViewContainer mavContainer = new ModelAndViewContainer();
		// 将request的Attribute复制一份到ModelMap
		mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
		// *调用我们标注了@ModelAttribute的方法,主要是为我们的目标方法预加载
		modelFactory.initModel(webRequest, mavContainer, invocableMethod);
		// 重定向的时候,忽略model中的数据 默认false
		mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);

		// 获取当前的AsyncWebRequest,这里AsyncWebRequest的主要作用是用于判断目标
		// handler的返回值是否为WebAsyncTask或DeferredResult,如果是这两种中的一种,
		// 则说明当前请求的处理应该是异步的。所谓的异步,指的是当前请求会将Controller中
		// 封装的业务逻辑放到一个线程池中进行调用,待该调用有返回结果之后再返回到response中。
		// 这种处理的优点在于用于请求分发的线程能够解放出来,从而处理更多的请求,提高吞吐。
		// 只有待目标任务完成之后才会回来将该异步任务的结果返回。
		AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
		asyncWebRequest.setTimeout(this.asyncRequestTimeout);
		// 封装异步任务的线程池、request、interceptors到WebAsyncManager中
		WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
		asyncManager.setTaskExecutor(this.taskExecutor);
		asyncManager.setAsyncWebRequest(asyncWebRequest);
		asyncManager.registerCallableInterceptors(this.callableInterceptors);
		asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);

		// 这里就是用于判断当前请求是否有异步任务结果的,如果存在,则对异步任务结果进行封装
		if (asyncManager.hasConcurrentResult()) {
			Object result = asyncManager.getConcurrentResult();
			mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
			asyncManager.clearConcurrentResult();
			LogFormatUtils.traceDebug(logger, traceOn -> {
				String formatted = LogFormatUtils.formatValue(result, !traceOn);
				return "Resume with async result [" + formatted + "]";
			});
			invocableMethod = invocableMethod.wrapConcurrentResult(result);
		}
		// *对请求参数进行处理,调用目标HandlerMethod,并且将返回值封装为一个ModelAndView对象
		invocableMethod.invokeAndHandle(webRequest, mavContainer);
		if (asyncManager.isConcurrentHandlingStarted()) {
			return null;
		}

		// 对封装的ModelAndView进行处理,主要是判断当前请求是否进行了重定向,如果进行了重定向,
		// 还会判断是否需要将FlashAttributes封装到新的请求中
		return getModelAndView(mavContainer, modelFactory, webRequest);
	}
	finally {
		webRequest.requestCompleted();
	}
}

/**
 * 获取参数,执行方法
 * 源码位置:org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletWebRequest, ModelAndViewContainer, Object...)
 */
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
		Object... providedArgs) throws Exception {

	/*真正的调用我们的目标对象 很重要 很重要*/
	Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
	// 设置相关的返回状态
	setResponseStatus(webRequest);
	// 如果请求处理完成,则设置requestHandled属性
	if (returnValue == null) {
		if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
			disableContentCachingIfNecessary(webRequest);
			mavContainer.setRequestHandled(true);
			return;
		}
	}
	// 如果请求失败,但是有错误原因,那么也会设置requestHandled属性
	else if (StringUtils.hasText(getResponseStatusReason())) {
		mavContainer.setRequestHandled(true);
		return;
	}

	mavContainer.setRequestHandled(false);
	Assert.state(this.returnValueHandlers != null, "No return value handlers");
	try {
		// 遍历当前容器中所有ReturnValueHandler,判断哪种handler支持当前返回值的处理,
		// 如果支持,则使用该handler处理该返回值
		this.returnValueHandlers.handleReturnValue(
				returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
	}
	catch (Exception ex) {
		if (logger.isTraceEnabled()) {
			logger.trace(formatErrorForReturnValue(returnValue), ex);
		}
		throw ex;
	}
}

七、渲染视图逻辑

/**
 * 渲染视图逻辑
 * 源码位置:org.springframework.web.servlet.DispatcherServlet.processDispatchResult(HttpServletRequest, HttpServletResponse, HandlerExecutionChain, ModelAndView, Exception)
 */
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..   拦截器:AfterCompletion
		mappedHandler.triggerAfterCompletion(request, response, null);
	}
}

到此这篇关于Java spring mvc请求详情介绍的文章就介绍到这了,更多相关spring mvc请求内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • SpringMVC框架中使用Filter实现请求日志打印方式

    目录 查找资料后确定两种技术方案 1. 使用AOP对所有Controller的方法进行环绕通知处理: 2. 使用Filter拦截所有的Request和Response,并获取body. 最后选择了第二种方式,具体实现记录如下. 具体实现 日志记录过滤器 public class RequestFilter implements Filter{private static final String LOG_FORMATTER_IN = "请求路径:{%s},请求方法:{%s},参数:{%s},来源

  • springmvc接口接收参数与请求参数格式的整理

    目录 springmvc接口接收参数与请求参数格式 一.首先我们需要认识下http请求中的Content-Type 二.注解@RequestParam(value="id") 三.注解@RequestBody springmvc接口接受前端传递参数数据类型总结 一.springMVC中controller参数是自动注入 二. 接受前端传递的对象 三.小结一下 springmvc接口接收参数与请求参数格式 前言: 相信大家在刚开始接触接口定义与调用时遇到过接口接收不到请求参数的问题,本人

  • 关于SpringMVC请求域对象的数据共享问题

    SpringMVC支持路径中的占位符. 可以通过路径的方式来传参.restful风格.使用{}做占位符在路径中指定参数,使用@PathVariable注解在参数列表中指定. <a th:href="@{/test/1}">传了参数</a> @RequestMapping("/test/{id}") public String test(@PathVariable("id")Integer id){ System.out.p

  • Java Spring MVC获取请求数据详解操作

    目录 1. 获得请求参数 2. 获得基本类型参数 3. 获得POJO类型参数 4. 获得数组类型参数 5. 获得集合类型参数 6. 请求数据乱码问题 7. 参数绑定注解 @requestParam 8. 获得Restful风格的参数 9. 自定义类型转换器 1.定义转换器类实现Converter接口 2.在配置文件中声明转换器 3.在<annotation-driven>中引用转换器 10. 获得Servlet相关API 11. 获得请求头 11.1 @RequestHeader 11.2 @

  • 基于spring mvc请求controller访问方式

    目录 spring mvc请求controller访问 1.一个Controller里含有不同的请求url 2.采用一个url访问 3.RequestMapping在Class上 4.在SpringMVC中常用的注解 springmvc请求一次,访问多个controller方法 举例 结论 spring mvc请求controller访问 1.一个Controller里含有不同的请求url @Controller //类似Struts的Action public class TestContro

  • 使用springmvc的controller层获取到请求的数据方式

    目录 content-type 1.application/x-www-form-urlencoded 1.1 String 参数值=reqeust.getParameter("参数名"); 1.2 Map<String,String[]> paramMap=request.getParameterMap(); 2.multipart/form-data 3.text/xml 4.application/json 4.1 页面请求的数据获取 4.1.1 页面js请求如下 4

  • SpringMVC 重新定向redirect请求中携带数据方式

    目录 SpringMVC 重新定向redirect请求中携带数据 使用URL模版重定向 使用flash属性 SpringMVC 几种重定向携带数据方法 1.拼接字符串 2.采用RedirectAttribute来传参数 3.RedirectAttribute-–addFlashAttribute()的用法 SpringMVC 重新定向redirect请求中携带数据 在控制器方法返回的视图名称中使用了redirect:前缀,这时返回的String不是用来寻找视图,而是浏览器进行跳转的路径: ret

  • Java spring mvc请求详情介绍

    目录 一.源码执行流程 二.源码执行流程图 三.spring mvc中的一核心组件 四.源码分析 五.获取组件相关逻辑: 六.获取参数,执行方法源码分析 七.渲染视图逻辑 前言: 本文源码基于spring-framework-5.3.10. mvc是spring源码中的一个子模块! 一.源码执行流程 用户发送请求至前端控制器DispatcherServlet. DispatcherServlet收到请求调用处理器映射器HandlerMapping.处理器映射器根据请求url找到具体的处理器,生成

  • java spring mvc处理器映射器介绍

    目录 一.RequestMappingHandlerMapping解析映射简单介绍 二.@RequestMapping解析源码流程 三.@RequestMapping映射源码流程 四.@RequestMapping解析源码 五.@RequestMapping映射源码 前言: 本文源码基于spring-framework-5.3.10. mvc是spring源码中的一个子模块! 一.RequestMappingHandlerMapping解析映射简单介绍 @RequestMapping通过Requ

  • Java Spring Boot请求方式与请求映射过程分析

    目录 请求方式 非Rest风格 使用Rest风格 表单提交 过程分析 请求映射过程分析 请求方式 Spring Boot支持Rest风格:使用HTTP请求方式的动词来表示对资源的操作 非Rest风格 以前这样写接口: /getUser 获取用户 /deleteUser 删除用户 /updateUser 修改用户 /saveUser 保存用户 @RequestMapping(value = "/getUser", method = RequestMethod.GET) public St

  • Java Spring MVC 上传下载文件配置及controller方法详解

    下载: 1.在spring-mvc中配置(用于100M以下的文件下载) <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"> <property name="messageConverters"> <list> <!--配置下载返回类型--> <bean class="or

  • Spring MVC请求参数的深入解析

    请求参数解析 客户端请求在handlerMapping中找到对应handler后,将会继续执行DispatchServlet的doPatch()方法. 首先是找到handler对应的适配器. HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); 进入到getHandlerAdapter(mappedHandler.getHandler())方法中 protected HandlerAdapter getHandler

  • Java Spring集成MapStruct详情

    目录 待转换的类 目标类 转换接口 测试类 前言: MapStruct支持Spring的依赖注入机制,只须要在@Mapper注解中添加componentModel配置项,并设置为“spring”便可. 待转换的类 @Getter @Setter @ToString @Builder @AllArgsConstructor @NoArgsConstructor public class Spu { private Integer id; private String name; private S

  • Spring MVC请求参数接收的全面总结教程

    前提 在日常使用SpringMVC进行开发的时候,有可能遇到前端各种类型的请求参数,这里做一次相对全面的总结.SpringMVC中处理控制器参数的接口是HandlerMethodArgumentResolver,此接口有众多子类,分别处理不同(注解类型)的参数,下面只列举几个子类: RequestParamMethodArgumentResolver:解析处理使用了@RequestParam注解的参数.MultipartFile类型参数和Simple类型(如long.int)参数. Reques

  • 详解spring mvc 请求转发和重定向

    请求重定向与请求转发的比较,HttpServletResponse.sendRedirect方法和RequestDispatcher.forward方法都可以让浏览器获得另外一个URL所指向的资源,但两者的内部运行机制有很大的区别. 1.RequestDispatcher.forward方法只能将请求转发给同一个Web应用中的组件,HttpServletResponse.sendRedirect不仅可以重定向到当前应用程序的其他资源,还可以重定向到同一个站点上的其他应用程序的资源,甚至是使用绝对

随机推荐