精尽Spring MVC源码分析 - HandlerAdapter 组件(四)之 HandlerMethodReturnValueHandler

2023-05-18编程技术187660

该系列文档是本人在学习 Spring MVC 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释 Spring MVC 源码分析 GitHub 地址 进行阅读

Spring 版本:5.2.4.RELEASE

该系列其他文档请查看:《精尽 Spring MVC 源码分析 - 文章导读》

HandlerAdapter 组件

HandlerAdapter 组件,处理器的适配器。因为处理器 handler 的类型是 Object 类型,需要有一个调用者来实现 handler 是怎么被执行。Spring 中的处理器的实现多变,比如用户的处理器可以实现 Controller 接口或者 HttpRequestHandler 接口,也可以用 @RequestMapping 注解将方法作为一个处理器等,这就导致 Spring MVC 无法直接执行这个处理器。所以这里需要一个处理器适配器,由它去执行处理器

由于 HandlerMapping 组件涉及到的内容较多,考虑到内容的排版,所以将这部分内容拆分成了五个模块,依次进行分析:

《HandlerAdapter 组件(一)之 HandlerAdapter》
《HandlerAdapter 组件(二)之 ServletInvocableHandlerMethod》
《HandlerAdapter 组件(三)之 HandlerMethodArgumentResolver》
《HandlerAdapter 组件(四)之 HandlerMethodReturnValueHandler》
《HandlerAdapter 组件(五)之 HttpMessageConverter》

HandlerAdapter 组件(四)之 HandlerMethodReturnValueHandler

本文是接着《HandlerAdapter 组件(三)之 HandlerMethodArgumentResolver》一文来分享 HandlerMethodReturnValueHandler 组件。在 HandlerAdapter 执行处理器的过程中,具体的执行过程交由 ServletInvocableHandlerMethod 对象来完成,其中需要先通过 HandlerMethodArgumentResolver 参数解析器从请求中解析出方法的入参,然后再通过反射机制调用对应的方法,获取到执行结果后需要通过 HandlerMethodReturnValueHandler 结果处理器来进行处理。

回顾

先来回顾一下 ServletInvocableHandlerMethod 在哪里调用返回值处理器的,可以回到 《HandlerAdapter 组件(二)之 ServletInvocableHandlerMethod》ServletInvocableHandlerMethod 小节下面的 invokeAndHandle 方法,如下:

public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
// <1> 执行调用
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
// <2> 设置响应状态码
setResponseStatus(webRequest); // <3> 设置 ModelAndViewContainer 为请求已处理,返回,和 @ResponseStatus 注解相关
if (returnValue == null) {
if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
disableContentCachingIfNecessary(webRequest);
mavContainer.setRequestHandled(true);
return;
}
} else if (StringUtils.hasText(getResponseStatusReason())) {
mavContainer.setRequestHandled(true);
return;
} // <4> 设置 ModelAndViewContainer 为请求未处理
mavContainer.setRequestHandled(false);
Assert.state(this.returnValueHandlers != null, "No return value handlers");
try {
// <5> 处理返回值
this.returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
} catch (Exception ex) {
if (logger.isTraceEnabled()) {
logger.trace(formatErrorForReturnValue(returnValue), ex);
}
throw ex;
}
}

<5> 处调用 returnValueHandlers 对返回结果进行处理
returnValueHandlers 为 HandlerMethodReturnValueHandlerComposite 组合对象,包含了许多的结果处理器

HandlerMethodReturnValueHandler 接口

org.springframework.web.method.support.HandlerMethodReturnValueHandler,返回结果处理器

public interface HandlerMethodReturnValueHandler {

	/**
* 是否支持该类型
*/
boolean supportsReturnType(MethodParameter returnType);
/**
* 处理返回值
*/
void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception;
}

类图

因为返回结果类型是多变的,所以会有许多的 HandlerMethodReturnValueHandler 的实现类,上图仅列出了本文会分析的两个实现类

ModelAndViewContainer

org.springframework.web.method.support.ModelAndViewContainer,主要是作为 Model 和 View 的容器

构造方法

public class ModelAndViewContainer {
/**
* 是否在 redirect 重定向时,忽略 {@link #redirectModel}
*/
private boolean ignoreDefaultModelOnRedirect = false; /**
* 视图,Object 类型。
*
* 实际情况下,也可以是 String 类型的逻辑视图
*/
@Nullable
private Object view; /**
* 默认使用的 Model 。实际上是个 Map
*/
private final ModelMap defaultModel = new BindingAwareModelMap(); /**
* redirect 重定向的 Model ,在重定向时使用。
*/
@Nullable
private ModelMap redirectModel; /**
* 处理器返回 redirect 视图的标识
*/
private boolean redirectModelScenario = false; /**
* Http 响应状态
*/
@Nullable
private HttpStatus status; private final Set<String> noBinding = new HashSet<>(4); private final Set<String> bindingDisabled = new HashSet<>(4); /**
* 用于设置 SessionAttribute 的标识
*/
private final SessionStatus sessionStatus = new SimpleSessionStatus(); /**
* 请求是否处理完的标识
*/
private boolean requestHandled = false;
}

getModel

getModel() 方法,获得 Model 对象。代码如下:

public ModelMap getModel() {
// 是否使用默认 Model
if (useDefaultModel()) {
return this.defaultModel;
}
else {
if (this.redirectModel == null) {
this.redirectModel = new ModelMap();
}
return this.redirectModel;
}
} /**
* Whether to use the default model or the redirect model.
*/
private boolean useDefaultModel() {
return (!this.redirectModelScenario || (this.redirectModel == null && !this.ignoreDefaultModelOnRedirect));
}

从代码中,可以看出,有两种情况下,使用 defaultModel 默认 Model :

情况一 !this.redirectModelScenario ,处理器返回 redirect 视图的标识为 false 的时候,即不重定向
情况二 this.redirectModel == null && !this.ignoreDefaultModelOnRedirectredirectModel 重定向 Model 为,并且 ignoreDefaultModelOnRedirecttrue ,即忽略 defaultModel

那么,问题就来了,redirectModelScenario 和 ignoreDefaultModelOnRedirect 什么时候被改变?

redirectModelScenario 属性,在下文的 ViewNameMethodReturnValueHandlerhandleReturnValue方法中会设置为true,详情见下文

ignoreDefaultModelOnRedirect 属性,和 RequestMappingHandlerAdapter 的 ignoreDefaultModelOnRedirect 的属性是一致的,默认为false

在 RequestMappingHandlerAdapter 的 invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) 方法中,进行设置

另外,org.springframework.ui.ModelMap 是继承 LinkedHashMap 类,并增加了部分常用方法,比较简单

View 相关的方法

public void setViewName(@Nullable String viewName) {
this.view = viewName;
}
@Nullable
public String getViewName() {
return (this.view instanceof String ? (String) this.view : null);
} public void setView(@Nullable Object view) {
this.view = view;
}
@Nullable
public Object getView() {
return this.view;
} public boolean isViewReference() {
return (this.view instanceof String);
}

requestHandled 属性

请求是否处理完的标识

关于 requestHandled 的修改地方,实际在 Spring MVC 地方蛮多处都可以进行修改,例如:

在本文的开始处,ServletInvocableHandlerMethod 对象的 invokeAndHandle 方法中,会先设置为 false,表示请求还未处理,再交由 HandlerMethodReturnValueHandler 结果处理器去处理

在后文的 RequestResponseBodyMethodProcessorhandleReturnValue 会设置为 true

处理完结果后,接下来 RequestMappingHandlerAdapter 需要通过 ModelAndViewContainer 获取 ModelAndView 对象,会用到 requestHandled 这个属性

// RequestMappingHandlerAdapter.java
@Nullable
private ModelAndView getModelAndView(ModelAndViewContainer mavContainer,
ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception { modelFactory.updateModel(webRequest, mavContainer);
// 情况一,如果 mavContainer 已处理,则返回“空”的 ModelAndView 对象。
if (mavContainer.isRequestHandled()) {
return null;
}
// 情况二,如果 mavContainer 未处理,则基于 `mavContainer` 生成 ModelAndView 对象
ModelMap model = mavContainer.getModel();
// 创建 ModelAndView 对象,并设置相关属性
ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, mavContainer.getStatus());
if (!mavContainer.isViewReference()) {
mav.setView((View) mavContainer.getView());
}
if (model instanceof RedirectAttributes) {
Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
if (request != null) {
RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
}
}
return mav;
}

看到没,如果已处理,则返回的 ModelAndView 对象为 null

HandlerMethodReturnValueHandlerComposite

org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite,实现 HandlerMethodReturnValueHandler 接口,复合的 HandlerMethodReturnValueHandler 实现类

构造方法

public class HandlerMethodReturnValueHandlerComposite implements HandlerMethodReturnValueHandler {
/** HandlerMethodReturnValueHandler 数组 */
private final List<HandlerMethodReturnValueHandler> returnValueHandlers = new ArrayList<>();
}

《HandlerAdapter 组件(一)之 HandlerAdapter》RequestMappingHandlerAdapter小节的 getDefaultReturnValueHandlers 方法中可以看到,默认的 returnValueHandlers 有哪些 HandlerMethodReturnValueHandler 实现类,注意这里是有顺序的添加哦

getReturnValueHandler

getReturnValueHandler(MethodParameter returnType) 方法,获得方法返回值对应的 HandlerMethodReturnValueHandler 对象,方法如下:

@Nullable
private HandlerMethodReturnValueHandler getReturnValueHandler(MethodParameter returnType) {
for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
if (handler.supportsReturnType(returnType)) {
return handler;
}
}
return null;
}

很简单,遍历所有的 HandlerMethodReturnValueHandler 实现类,如果支持这个返回结果,则直接返回

这里为什么不加缓存呢?

supportsReturnType

supportsReturnType(MethodParameter returnType)方法,判断是否支持该返回类型,方法如下:

@Override
public boolean supportsReturnType(MethodParameter returnType) {
return getReturnValueHandler(returnType) != null;
}

实际上就是调用 getReturnValueHandler(MethodParameter returnType) 方法,存在对应的 HandlerMethodReturnValueHandler 实现类表示支持

handleReturnValue

handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest)方法,处理返回值,方法如下:

@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
// <x> 获得 HandlerMethodReturnValueHandler 对象
HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
}
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}

这里好奇的是没有调用 getReturnValueHandler(MethodParameter returnType)方法获取对应的 HandlerMethodReturnValueHandler 对象,而是调用 selectHandler(Object value, MethodParameter returnType) 方法,方法如下:

@Nullable
private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
// 判断是否为异步返回值
boolean isAsyncValue = isAsyncReturnValue(value, returnType);
// 遍历 HandlerMethodReturnValueHandler 数组,逐个判断是否支持
for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
continue;
}
// 如果支持,则返回
if (handler.supportsReturnType(returnType)) {
return handler;
}
}
return null;
} private boolean isAsyncReturnValue(@Nullable Object value, MethodParameter returnType) {
for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
if (handler instanceof AsyncHandlerMethodReturnValueHandler &&
((AsyncHandlerMethodReturnValueHandler) handler).isAsyncReturnValue(value, returnType)) {
return true;
}
}
return false;
}

getReturnValueHandler(MethodParameter returnType) 的基础上,增加了异步处理器 AsyncHandlerMethodReturnValueHandler 的判断

【重点】RequestResponseBodyMethodProcessor

org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor,继承 AbstractMessageConverterMethodProcessor 抽象类,处理方法参数添加了 @RequestBody 注解方法入参,或者处理方法添加了 @ResponseBody 注解的返回值。

因为前后端分离之后,后端基本是提供 Restful API ,所以 RequestResponseBodyMethodProcessor 成为了目前最常用的 HandlerMethodReturnValueHandler 实现类。

从图中,我们也会发现,RequestResponseBodyMethodProcessor 也是 HandlerMethodArgumentResolver 的实现类。示例代码:

@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/walks")
public List<User> walk(@RequestBody User user) {
List<User> users = new ArrayList();
users.add(new User().setUsername("nihao"));
users.add(new User().setUsername("zaijian"));
return users;
}
}

虽然,walks() 方法的返回值没添加 @ResponseBody 注解,但是 @RestController 注解,默认有 @ResponseBody 注解

构造方法

public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {

	public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> converters) {
super(converters);
} public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> converters,
@Nullable ContentNegotiationManager manager) { super(converters, manager);
} public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> converters,
@Nullable List<Object> requestResponseBodyAdvice) { super(converters, null, requestResponseBodyAdvice);
} public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> converters,
@Nullable ContentNegotiationManager manager, @Nullable List<Object> requestResponseBodyAdvice) { super(converters, manager, requestResponseBodyAdvice);
}
}

converters 参数,HttpMessageConverter 数组。关于 HttpMessageConverter,就是将返回结果设置到响应中,供客户端获取。例如,我们想要将 POJO 对象,返回成 JSON 数据给前端,就会使用到 MappingJackson2HttpMessageConverter 类。

requestResponseBodyAdvice 参数,在父类 AbstractMessageConverterMethodArgumentResolver 中会将其转换成 RequestResponseBodyAdviceChain 对象 advice,不知你是否还记得这个参数,来回顾一下:

《HandlerAdapter 组件(一)之 HandlerAdapter》RequestMappingHandlerAdapter1.afterPropertiesSet 初始化方法中,第一步就会初始化所有 ControllerAdvice 相关的类

然后在1.4 getDefaultReturnValueHandlers方法中,创建 RequestResponseBodyMethodProcessor 处理器时,会传入 requestResponseBodyAdvice 参数

使用示例可以参考 SpringMVC 中 @ControllerAdvice 注解的三种使用场景

supportsParameter

实现 supportsParameter(MethodParameter returnType) 方法,判断是否支持处理该方法参数,方法如下:

@Override
public boolean supportsParameter(MethodParameter parameter) {
// 该参数是否有 @RequestBody 注解
return parameter.hasParameterAnnotation(RequestBody.class);
}

该方法参数是否有 @RequestBody 注解

resolveArgument

实现 resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) 方法,从请求中解析出带有 @RequestBody 注解的参数,方法如下:

@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { parameter = parameter.nestedIfOptional();
// 从请求体中解析出方法入参对象
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
String name = Conventions.getVariableNameForParameter(parameter); // 数据绑定相关
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
if (arg != null) {
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}
// 返回方法入参对象,如果有必要,则通过 Optional 获取对应的方法入参
return adaptArgumentIfNecessary(arg, parameter);
}

调用readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter, Type paramType)方法,从请求体中解析出方法入参对象

【核心】readWithMessageConverters

从请求体中解析出方法入参,方法如下:

@Override
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,
Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
// <1> 创建 ServletServerHttpRequest 请求对象
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
Assert.state(servletRequest != null, "No HttpServletRequest");
ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest); // <2> 读取请求体中的消息并转换成入参对象
Object arg = readWithMessageConverters(inputMessage, parameter, paramType);
// <3> 校验方法入参对象
if (arg == null && checkRequired(parameter)) {
throw new HttpMessageNotReadableException("Required request body is missing: " +
parameter.getExecutable().toGenericString(), inputMessage);
}
return arg;
}
// AbstractMessageConverterMethodArgumentResolver.java
@Nullable
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
// <1> 获取使用的 MediaType 对象
MediaType contentType;
boolean noContentType = false;
try {
// <1.1> 从请求头中获取 "Content-Type"
contentType = inputMessage.getHeaders().getContentType();
}
catch (InvalidMediaTypeException ex) {
throw new HttpMediaTypeNotSupportedException(ex.getMessage());
}
if (contentType == null) {
noContentType = true;
// <1.2> 为空则默认为 application/octet-stream
contentType = MediaType.APPLICATION_OCTET_STREAM;
} // <2> 获取方法参数的 containing class 和 目标类型,用于 HttpMessageConverter 解析
Class<?> contextClass = parameter.getContainingClass();
Class<T> targetClass = (targetType instanceof Class ? (Class<T>) targetType : null);
if (targetClass == null) {
// 如果为空,则从方法参数中解析出来
ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
targetClass = (Class<T>) resolvableType.resolve();
} // <3> 获取 HTTP 方法
HttpMethod httpMethod = (inputMessage instanceof HttpRequest ? ((HttpRequest) inputMessage).getMethod() : null);
Object body = NO_VALUE; // <4> 开始从请求中解析方法入参
EmptyBodyCheckingHttpInputMessage message;
try {
// <4.1> 将请求消息对象封装成 EmptyBodyCheckingHttpInputMessage,用于校验是否有请求体,没有的话设置为 `null`
message = new EmptyBodyCheckingHttpInputMessage(inputMessage); // <4.2> 遍历 HttpMessageConverter
for (HttpMessageConverter<?> converter : this.messageConverters) {
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
GenericHttpMessageConverter<?> genericConverter = (converter instanceof GenericHttpMessageConverter ?
(GenericHttpMessageConverter<?>) converter : null);
// 如果该 HttpMessageConverter 能够读取当前请求体解析出方法入参
if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
(targetClass != null && converter.canRead(targetClass, contentType))) {
// <4.2.1> 如果请求体不为空
if (message.hasBody()) {
HttpInputMessage msgToUse = getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
// 通过该 HttpMessageConverter 从请求体中解析出方法入参对象
body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
// 调用 RequestResponseBodyAdvice 的 afterBodyRead 方法,存在 RequestBodyAdvice 则对方法入参进行修改
body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
}
// <4.2.2> 如果请求体为空,则无需解析请求体
else {
// 调用 RequestResponseBodyAdvice 的 afterBodyRead 方法,存在 RequestBodyAdvice 则对方法入参进行修改
body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
}
break;
}
}
}
catch (IOException ex) {
throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
} // <5> 校验解析出来的方法入参对象是否为空
if (body == NO_VALUE) {
if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||
(noContentType && !message.hasBody())) {
return null;
}
throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
} // 打印日志
MediaType selectedContentType = contentType;
Object theBody = body;
LogFormatUtils.traceDebug(logger, traceOn -> {
String formatted = LogFormatUtils.formatValue(theBody, !traceOn);
return "Read \"" + selectedContentType + "\" to [" + formatted + "]";
}); // <6> 返回方法入参对象
return body;
}

我们直接看到父类 AbstractMessageConverterMethodArgumentResolver 的 readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,Type targetType)这个核心方法,大致逻辑如下:

    获取使用的 MediaType 对象 contentType

      从请求头中获取 Content-Type
      请求头中没有则设置为默认的类型 application/octet-stream

    获取方法参数的 containing classtargetClass 目标类型,用于 HttpMessageConverter 解析

    获取 HTTP 方法

    开始从请求中解析方法入参Object body

      将请求消息对象封装成 EmptyBodyCheckingHttpInputMessage,用于校验是否有请求体,没有的话设置为 null

      遍历所有的 HttpMessageConverter 实现类,调用其 canRead(Type type, @Nullable Class<?> contextClass, @Nullable MediaType mediaType)方法,判断当前 HttpMessageConverter 实现类是否支持解析该方法入参,如果返回 true,则使用该 HttpMessageConverter 实现类进行解析

        如果请求体不为空,则通过该 HttpMessageConverter 从请求体中解析出方法入参对象
        如果请求体为空,则无需解析请求体

      注意:上面不管请求体是否为空,都会调用 RequestResponseBodyAdviceafterBodyRead 方法,存在 RequestBodyAdvice 则对方法入参进行修改

    校验解析出来的方法入参对象是否为空,抛出异常或者返回null

    返回方法入参对象body


方法虽然很长,但是不难理解,大致逻辑就是找到合适的 HttpMessageConverter 实现类从请求体中获取到方法入参对象

逻辑和下面的 writeWithMessageConverters 差不多,我们重点来看到下面这个方法

精尽Spring MVC源码分析 - HandlerAdapter 组件(四)之 HandlerMethodReturnValueHandler的相关教程结束。

本文地址:https://www.ufcn.cn/tutorials/2650559.html

如非特殊说明,本站内容均来自于网友自主分享,概不代表本站观点,如有任何问题我们都将在收到反馈后的第一时间进行处理!