当前位置:   article > 正文

Spring MVC底层原理和九大组件_springmvc底层架构

springmvc底层架构

Spring MVC

springMVC是一种web层mvc框架,用于替代servlet,简化 web 编程。是一种基于Java的实现了Web MVC 设计模式的请求驱动类型的轻量级Web框架,即使用了MVC架构模式的思想,将web层进行职责解耦。

九大组件

  • HandlerMapping(处理器映射器):
    HandlerMapping 是用来查找 Handler的,可以是类也可以是一个方法。用 @RequestMapping 标注的就是一个 Handler,负责实际的请求处理。也就是说 HandlerMapping负责根据请求的URL来找到具体
public interface HandlerMapping {
	@Nullable
	HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;
}
  • 1
  • 2
  • 3
  • 4
  • HandlerAdapter(处理器适配器):在 MVC 中 Handler可以是任意形式的,只有能处理就可以。而 Servlet 里面的机构都是固定的doService(HttpServletRequest request, HttpServletResponse response) 只能处理 request 和 response。任意形式的Handler通过使用适配器,可以“转换”成固定形式,然后交给Servlet来处理。
public interface HandlerAdapter {

    // 判断是否支持传入的handler
	boolean supports(Object handler);

    // 使用给定的handler处理请求
	@Nullable
	ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;

    // 返回上次修改时间,可以返回-1表示不支持
	long getLastModified(HttpServletRequest request, Object handler);

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • HandlerExceptionResolver(处理器异常解析器):
    是用来处理 Handler 产生异常情况的组件,据异常设置ModelAndView。

public interface HandlerExceptionResolver {

	@Nullable
	ModelAndView resolveException(
			HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • ViewResolver(实体解析器):将 String 类型的视图名和 Locale 解析为 View 类型的视图。 ViewResolver 做两件事情,一是找到所用渲染的模板,二是所用到的技术(JSP)
public interface ViewResolver {

	@Nullable
	View resolveViewName(String viewName, Locale locale) throws Exception;

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • RequestToViewNameTranslator(请求获取视图名称转换器):从请求中获取视图名称。 ViewResolver 根据 ViewName 获取到 View,而某些 Handler 处理完成之后并没有设置 View 或 ViewName,便从这个组件去查找。
public interface RequestToViewNameTranslator {

	@Nullable
	String getViewName(HttpServletRequest request) throws Exception;

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • LocaleResolver(本地化解析器):ViewResolver 需要两个参数,viewName 和 locale,而 Locale 就是从 LocaleResolver 出来的。主要是用来处理 request 里面的区域,如:zh-cn。也可以用来做国际化处理。
public interface LocaleResolver {


	Locale resolveLocale(HttpServletRequest request);

	void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale);

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • ThemeResolver(主题解析器):用来解析主题的。主题就是样式、图片。
  • MultipartResolver(上传解析器):用来处理上传请求。通过将普通的请求包装成 MultipartHttpServletRequest 来实现。封装普通请求让其拥有文件删除的功能。
  • FlashMapManager: 用来管理FlashMap的,FlashMap主要用在redirect中传递参数。

MVC 初始化时序图

字母转换类

  • A:DispatcherServlet
  • B:HttpServletBean
  • C:AbstractDetectingUrlHandlerMapping
A B C init() [0] onRefresh() [1] initStrategies() [2] initHandlerMappings() [3] getDefaultStrategies() [4] initApplicationContext() [5] detectHandlers() [6] A B C

步骤说明:

  • [0]:Spring MVC 初始化过程,init() 具体实现又 DispatcherServlet 父类 HttpServletBean 进行初始化工作。
	public final void init() throws ServletException {

		// Set bean properties from init parameters.
		// 从init参数设置bean属性。
		PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
		if (!pvs.isEmpty()) {
			try {
			    // 定位资源
				BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
				// 加载配置信息
				ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
				bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
				initBeanWrapper(bw);
				bw.setPropertyValues(pvs, true);
			}
			catch (BeansException ex) {
				if (logger.isErrorEnabled()) {
					logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
				}
				throw ex;
			}
		}

		// Let subclasses do whatever initialization they like.
		// 初始化 IOC 容器
		initServletBean();
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • [1]:定义 MVC 的初始化, DispatcherServlet.onRefresh() 继承父类 FrameworkServlet 并重写 Bean 初始化方法,在 [0] 步骤中 == initServletBean()== 进行触发,使用委派给子类进行实现
	protected void onRefresh(ApplicationContext context) {
		initStrategies(context);
	}
  • 1
  • 2
  • 3
  • [2]:MVC 九大组件初始化定义策略
	protected void initStrategies(ApplicationContext context) {
	    // 初始化 多文件上传 组件
		initMultipartResolver(context);
		// 初始化 本地化解析器 组件
		initLocaleResolver(context);
		// 初始化 主题解析器 组件
		initThemeResolver(context);
		// 初始化 处理器映射器 组件
		initHandlerMappings(context);
		// 初始化 处理器适配器 组件
		initHandlerAdapters(context);
		// 初始化 处理器异常解析器 组件
		initHandlerExceptionResolvers(context);
		// 初始化 请求获取视图名称转换器 组件
		initRequestToViewNameTranslator(context);
		// 初始化 视图解析器 组件
		initViewResolvers(context);
		// 初始化 FlashMap 组件
		initFlashMapManager(context);
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • [3]:初始化 HandlerMappings
	private void initHandlerMappings(ApplicationContext context) {
		this.handlerMappings = null;

		if (this.detectAllHandlerMappings) {
			// Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
			// 在ApplicationContext中查找所有HandlerMappings,包括祖先上下文。
			Map<String, HandlerMapping> 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 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(如果没有找到其他映射),确保我们至少有一个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");
			}
		}
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • [4]:使用默认策略,确保我们至少有一个HandlerMapping,默认的配置在 DispatcherServlet.properties 下
org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
	org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping,\
	org.springframework.web.servlet.function.support.RouterFunctionMapping
  • 1
  • 2
  • 3


	protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) {
		String key = strategyInterface.getName();
		// 获取 DispatcherServlet.properties 下 HandlerMapping 的值
		String value = defaultStrategies.getProperty(key);
		if (value != null) {
			String[] classNames = StringUtils.commaDelimitedListToStringArray(value);
			List<T> strategies = new ArrayList<>(classNames.length);
			for (String className : classNames) {
				try {
					Class<?> clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader());
					// 实际初始化 HandlerMapping 入口
					Object strategy = createDefaultStrategy(context, clazz);
					strategies.add((T) strategy);
				}
				catch (ClassNotFoundException ex) {
					throw new BeanInitializationException(
							"Could not find DispatcherServlet's default strategy class [" + className +
							"] for interface [" + key + "]", ex);
				}
				catch (LinkageError err) {
					throw new BeanInitializationException(
							"Unresolvable class definition for DispatcherServlet's default strategy class [" +
							className + "] for interface [" + key + "]", err);
				}
			}
			return strategies;
		}
		else {
			return new LinkedList<>();
		}
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • [5]:以 BeanNameUrlHandlerMapping 为例, 初始化工作在其父类AbstractDetectingUrlHandlerMapping.initApplicationContext() 方法中。
	public void initApplicationContext() throws ApplicationContextException {
		super.initApplicationContext();
		// 进行 HandlerMapping 初始化
		detectHandlers();
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • [6]:建立当前 ApplicationContext 中所有 Controller 和 URL 的关系,HandlerMapping 初始化完成。
	protected void detectHandlers() throws BeansException {
		ApplicationContext applicationContext = obtainApplicationContext();
		// 获取 IOC 容器所有 Bean 的名字
		String[] beanNames = (this.detectHandlersInAncestorContexts ?
				BeanFactoryUtils.beanNamesForTypeIncludingAncestors(applicationContext, Object.class) :
				applicationContext.getBeanNamesForType(Object.class));

		// Take any bean name that we can determine URLs for.
		// 遍历 beanNames,并找到 Bean 所对应的 URL
		for (String beanName : beanNames) {
			String[] urls = determineUrlsForHandler(beanName);
			if (!ObjectUtils.isEmpty(urls)) {
				// URL paths found: Let's consider it a handler.
				// 保存 URL 和 beanName 的关系
				registerHandler(urls, beanName);
			}
		}

		if ((logger.isDebugEnabled() && !getHandlerMap().isEmpty()) || logger.isTraceEnabled()) {
			logger.debug("Detected " + getHandlerMap().size() + " mappings in " + formatMappingName());
		}
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

MVC 运行流程图

1
HandlerExecutionChain
2,Handler
ModelAndView
执行方法,处理拦截器
ModelAndView
3,ModelAndView
View
4,Model
用户Request
DispatcherServlet
HandlerMapping
HandlerAdapter
HandlerMethod
ViewResolvers
View

用户发出一个请求,进入 DispatcherServlet.doService() 方法。跳转至 doDispatch() 方法,为 MVC 请求实际处理方法。

  1. 进入 HandlerMapping,找到该请求所对应的 Handler 并返回 HandlerExecutionChain(封装了 Handler 和 Interceptor ),如果没找到对应的 Handler 则返回 404
  2. 根据获取到的 Handler 传入 HandlerAdapter 找到对应的适配器,找到HandlerMethod(Controller 里面的业务方法)并执行返回ModelAndView,如果在此过程中有拦截器则运行拦截器,如果没有则不运行。
  3. DispatcherServlet 传入 ModelAndView,这是的View 还是只是个 ViewName(String),经过 ViewResolvers 解析成实际的渲染页面
  4. 把数据(数据来源于request,modelMap,session中)与模板的变量进行替换,最终通过 Servlet 返回给用户

MVC 运行时序图

字母替换类说明:

  • A:DispatcherServlet
  • B: AbstractHandlerMethodAdapter
  • C: RequestMappingHandlerAdapter
Request A B C doService() [0] doDispatch() [1] getHandler() [2] getHandlerAdapter() [3] handle() [4] handleInternal() [5] Request A B C

步骤说明:

  • [0]:用户发出一个请求,进入 DispatcherServlet,因为 DispatcherServlet 继承 HttpServlet 所以会进 doService() 方法。
	protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
		logRequest(request);

		// Keep a snapshot of the request attributes in case of an include,
		// to be able to restore the original attributes after the include.
		Map<String, Object> attributesSnapshot = null;
		if (WebUtils.isIncludeRequest(request)) {
			attributesSnapshot = new HashMap<>();
			Enumeration<?> attrNames = request.getAttributeNames();
			while (attrNames.hasMoreElements()) {
				String attrName = (String) attrNames.nextElement();
				if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
					attributesSnapshot.put(attrName, request.getAttribute(attrName));
				}
			}
		}

		// Make framework objects available to handlers and view objects.
		request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
		request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
		request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
		request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());

		if (this.flashMapManager != null) {
			FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
			if (inputFlashMap != null) {
				request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
			}
			request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
			request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
		}

		try {
		    // 进入 MVC 实际控制方法
			doDispatch(request, response);
		}
		finally {
			if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
				// Restore the original attribute snapshot, in case of an include.
				if (attributesSnapshot != null) {
					restoreAttributesAfterInclude(request, attributesSnapshot);
				}
			}
		}
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • [1]:DispatcherServlet 核心方法 doDispatch()
	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 {
			    // 检查是否是文件上传的请求
				processedRequest = checkMultipart(request);
				multipartRequestParsed = (processedRequest != request);

				// Determine handler for the current request.
				// 获取处理当前请求的 Controller(Handler)
				// 返回 HandlerExecutionChain(处理器执行链),封装了 Handler 和 Interceptor
				mappedHandler = getHandler(processedRequest);
				// 如果 Handler 为空,则返回 404
				if (mappedHandler == null) {
					noHandlerFound(processedRequest, response);
					return;
				}

				// Determine handler adapter for the current request.
				// 获取 处理器适配器 HandlerAdapter
				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

				// Process last-modified header, if supported by the handler.
				// 处理 last-modified 请求头
				String method = request.getMethod();
				boolean isGet = "GET".equals(method);
				if (isGet || "HEAD".equals(method)) {
					long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
					if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
						return;
					}
				}

				if (!mappedHandler.applyPreHandle(processedRequest, response)) {
					return;
				}

				// Actually invoke the handler.
				// 实际处理器请求
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

				if (asyncManager.isConcurrentHandlingStarted()) {
					return;
				}
                // 返回结果 ModelAndView
				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);
				}
			}
		}
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • [2]:获取 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;
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • [3]:根据 Handler 查找出对应的 HandlerAdapter
	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");
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • [4]:实际处理器请求
	public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {

		return handleInternal(request, response, (HandlerMethod) handler);
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • [5]:根据 HandlerAdapter 查找到对应的 RequestMappingHandlerAdapter 实际处理对象,Adapter的实现子类并返回 ModelAndView,拦截器也在这里面进行处理
	protected ModelAndView handleInternal(HttpServletRequest request,
			HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

		ModelAndView mav;
		checkRequest(request);

		// Execute invokeHandlerMethod in synchronized block if required.
		if (this.synchronizeOnSession) {
			HttpSession session = request.getSession(false);
			if (session != null) {
				Object mutex = WebUtils.getSessionMutex(session);
				synchronized (mutex) {
					mav = invokeHandlerMethod(request, response, handlerMethod);
				}
			}
			else {
				// No HttpSession available -> no mutex necessary
				mav = invokeHandlerMethod(request, response, handlerMethod);
			}
		}
		else {
			// No synchronization on session demanded at all...
			mav = invokeHandlerMethod(request, response, handlerMethod);
		}

		if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
			if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
				applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
			}
			else {
				prepareResponse(response);
			}
		}

		return mav;
	}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

总结

熟悉 Spring MVC 的请求流程和底层结构可以更好的了解它,知道拦截器在那个位置进行触发。用户发出请求后了解整个的请求过程具体做了什么、干了什么。而没了解之前就跟一个黑盒一样,只知传入的参数和返回的结果,具体怎么转变的却不知道。熟悉后对写业务更加的得心应手。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/67717
推荐阅读
相关标签
  

闽ICP备14008679号