关于spring:SpringMVCRequestMappingHandlerMapping

前言

应用SpringBoot进行web开发时,控制器类由@RestController注解润饰,通常@RestController注解与@RequestMapping配合应用,被润饰的类用于解决由DispatcherServlet散发下来的web申请。那么当一个web申请达到时,DispatcherServlet是如何将申请下发给对应的控制器解决呢。该篇文章将联合SpringMVC源码,对在散发申请过程中起重要作用的类RequestMappingHandlerMapping进行学习。

SpringBoot版本:2.4.1

注释

一. DispatcherServlet散发申请

当一个web申请到来时,DispatcherServlet负责接管申请并响应后果。DispatcherServlet首先须要找到以后申请对应的Handler(处理器)来解决申请,流程如下图所示。

HandlerMapping称为处理器映射器,是一个接口,定义web申请和Handler之间的映射。DispatcherServlet中有一个成员变量叫做handlerMappings,是一个HandlerMapping的汇合,当申请到来时,DispatcherServlet遍历handlerMappings中的每一个HandlerMapping以获取对应的handler。上述步骤产生在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);

            //依据申请获取Handler
            mappedHandler = getHandler(processedRequest);
            if (mappedHandler == null) {
                noHandlerFound(processedRequest, response);
                return;
            }
            ......
        }
        catch (Exception ex) {
                ......
        }
        catch (Throwable err) {
            ......
        }
        ......
    }
    catch (Exception ex) {
        ......
    }
    catch (Throwable err) {
        ......
    }
    finally {
        ......
    }
}

Handler的获取由DispatcherServlet的getHandler()办法实现,上面再看一下getHandler()具体做了什么事件。

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;
}

前文已知handlerMappings是HandlerMapping的汇合,因而getHandler()次要实现遍历每一个HandlerMapping并依据申请获取对应的Handler。仅看源码不够直观,当初通过打断点的形式理论看一下handlerMappings里的内容。

察看handlerMappings的内容能够发现,handlerMappings中加载了ApplicationContext中的所有HandlerMapping,例如BeanNameUrlHandlerMapping,将url与名称以/结尾的bean建设了映射关系,再例如本文重点探讨的RequestMappingHandlerMapping,可能将@Controller注解润饰的类中的@RequestMapping注解的内容解析成RequestMappingInfo数据结构。每一种HandlerMapping都有本人相应的实现,来实现通过申请获取Handler的性能。

大节:DispatcherServlet散发申请次要是通过遍历HandlerMapping的汇合并将申请传递给HandlerMapping以获取对应的Handler。

二. RequestMappingHandlerMapping初始化

首先通过类图认识一下RequestMappingHandlerMapping。

由类图可知,RequestMappingHandlerMapping的父类AbstractHandlerMethodMapping实现了InitializingBean接口,RequestMappingHandlerMapping和AbstractHandlerMethodMapping均实现了afterPropertiesSet()办法,该办法会在bean属性实现初始化后被调用。这里先剖析RequestMappingHandlerMapping实现的afterPropertiesSet()办法。

public void afterPropertiesSet() {

    this.config = new RequestMappingInfo.BuilderConfiguration();
    this.config.setTrailingSlashMatch(useTrailingSlashMatch());
    this.config.setContentNegotiationManager(getContentNegotiationManager());

    if (getPatternParser() != null) {
        this.config.setPatternParser(getPatternParser());
        Assert.isTrue(!this.useSuffixPatternMatch && !this.useRegisteredSuffixPatternMatch,
                "Suffix pattern matching not supported with PathPatternParser.");
    }
    else {
        this.config.setSuffixPatternMatch(useSuffixPatternMatch());
        this.config.setRegisteredSuffixPatternMatch(useRegisteredSuffixPatternMatch());
        this.config.setPathMatcher(getPathMatcher());
    }
    
    //调用AbstractHandlerMethodMapping的afterPropertiesSet()办法
    super.afterPropertiesSet();
}

在RequestMappingHandlerMapping的afterPropertiesSet()办法中调用了AbstractHandlerMethodMapping的afterPropertiesSet()办法,上面再剖析AbstractHandlerMethodMapping的afterPropertiesSet()办法。

public void afterPropertiesSet() {
    initHandlerMethods();
}

protected void initHandlerMethods() {
    //获取容器中所有bean的名称
    for (String beanName : getCandidateBeanNames()) {
        if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
            //依据bean名称解决bean
            processCandidateBean(beanName);
        }
    }
    handlerMethodsInitialized(getHandlerMethods());
}

protected void processCandidateBean(String beanName) {
    Class<?> beanType = null;
    try {
        //先获取容器,而后依据bean名称获取bean的Class对象
        beanType = obtainApplicationContext().getType(beanName);
    }
    catch (Throwable ex) {
        if (logger.isTraceEnabled()) {
            logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
        }
    }
    //判断bean是否是由@Controller注解或者@RequestMapping注解润饰的对象
    if (beanType != null && isHandler(beanType)) {
        detectHandlerMethods(beanName);
    }
}

在AbstractHandlerMethodMapping的afterPropertiesSet()办法中,调用了initHandlerMethods()办法,该办法次要是从容器中将所有bean获取进去(这里是获取的所有bean的名称),而后又在processCandidateBean()办法中判断每个bean是否是由@Controller注解或者@RequestMapping注解润饰的对象,如果是则判断该bean为一个处理器,则须要在detectHandlerMethods()办法中查找出该处理器的处理器办法。detectHandlerMethods()办法是一个重要办法,并且浏览起来有一点绕,上面具体看一下这个办法做的事件。

protected void detectHandlerMethods(Object handler) {
    //获取handler的Class对象
    Class<?> handlerType = (handler instanceof String ?
            obtainApplicationContext().getType((String) handler) : handler.getClass());

    if (handlerType != null) {
        //获取handler的实在Class对象(假若handler是cglib代理生成的子类,则获取原始类的Class对象)
        Class<?> userType = ClassUtils.getUserClass(handlerType);
        //调用getMappingForMethod()获取method和RequestMappingInfo的map汇合
        Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
                (MethodIntrospector.MetadataLookup<T>) method -> {
                    try {
                        //在selectMethods()办法中理论调用的是getMappingForMethod()办法
                        return getMappingForMethod(method, userType);
                    }
                    catch (Throwable ex) {
                        throw new IllegalStateException("Invalid mapping on handler class [" +
                                userType.getName() + "]: " + method, ex);
                    }
                });
        if (logger.isTraceEnabled()) {
            logger.trace(formatMappings(userType, methods));
        }
        methods.forEach((method, mapping) -> {
            Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
            //将handler,method和RequestMappingInfo缓存,并建设映射关系
            registerHandlerMethod(handler, invocableMethod, mapping);
        });
    }
}

detectHandlerMethods()中首先是获取handler的实在Class对象,而后应用MethodIntrospector.selectMethods(Class<?> targetType, final MetadataLookup<T> metadataLookup)办法将handler的办法解析成<Method, RequestMappingInfo>的map汇合。metadataLookup是一个回调函数,metadataLookup的具体应用稍后再剖析,当初再看一下MethodIntrospector.selectMethods()的具体实现。

public static <T> Map<Method, T> selectMethods(Class<?> targetType, final MetadataLookup<T> metadataLookup) {
    final Map<Method, T> methodMap = new LinkedHashMap<>();
    Set<Class<?>> handlerTypes = new LinkedHashSet<>();
    Class<?> specificHandlerType = null;

    //判断给定对象是否是JDK动静代理生成对象
    //如果不是,(如果是CGLIB动静代理生成对象)则获取其原始类的Class对象,并增加到Class的Set汇合中
    if (!Proxy.isProxyClass(targetType)) {
        specificHandlerType = ClassUtils.getUserClass(targetType);
        handlerTypes.add(specificHandlerType);
    }
    //获取给定对象和给定对象父类实现的所有接口的Class对象,并增加到Class的Set汇合中
    handlerTypes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetType));

    for (Class<?> currentHandlerType : handlerTypes) {
        final Class<?> targetClass = (specificHandlerType != null ? specificHandlerType : currentHandlerType);

        ReflectionUtils.doWithMethods(currentHandlerType, method -> {
            //获取实在办法(如果办法是接口的办法则依据Class对象找到实在实现的办法)
            Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
            //执行回调函数metadataLookup
            T result = metadataLookup.inspect(specificMethod);
            if (result != null) {
                //依据实在办法获取其桥接办法,但如果实在办法不是桥接办法则返回其自身
                Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
                if (bridgedMethod == specificMethod || metadataLookup.inspect(bridgedMethod) == null) {
                    //将实在办法与其回调函数执行后果寄存到map中
                    methodMap.put(specificMethod, result);
                }
            }
        }, ReflectionUtils.USER_DECLARED_METHODS);
    }

    return methodMap;
}

在MethodIntrospector.selectMethods()中有一个Class对象的Set汇合,外面寄存了给定对象的Class对象以及给定对象实现的接口的Class对象(如果给定对象有父类,则还包含父类实现的接口的Class对象),而后遍历Set汇合,并应用ReflectionUtils.doWithMethods(Class<?> clazz, MethodCallback mc, @Nullable MethodFilter mf)解决Set汇合中的每一个Class对象。mc是一个回调函数,mc的具体应用稍后剖析,最初再来看一下ReflectionUtils.doWithMethods()的具体应用。

public static void doWithMethods(Class<?> clazz, MethodCallback mc, @Nullable MethodFilter mf) {
    //获取给定对象的所有申明办法
    Method[] methods = getDeclaredMethods(clazz, false);
    for (Method method : methods) {
        //对method依据传入的MethodFilter进行过滤,满足指定的条件的method才执行回调办法
        if (mf != null && !mf.matches(method)) {
            continue;
        }
        try {
            //对满足条件的method执行回调办法
            mc.doWith(method);
        }
        catch (IllegalAccessException ex) {
            throw new IllegalStateException("Not allowed to access method '" + method.getName() + "': " + ex);
        }
    }
    //递归对给定对象的父对象执行雷同操作
    if (clazz.getSuperclass() != null && (mf != USER_DECLARED_METHODS || clazz.getSuperclass() != Object.class)) {
        doWithMethods(clazz.getSuperclass(), mc, mf);
    }
    else if (clazz.isInterface()) {
        for (Class<?> superIfc : clazz.getInterfaces()) {
            doWithMethods(superIfc, mc, mf);
        }
    }
}

ReflectionUtils.doWithMethods()中做的事件很简略,先将给定的Class对象的所有申明办法获取进去,而后针对每一个申明办法用给定的MethodFilter进行过滤,再将过滤后的申明办法传入回到函数mc并执行,(当初往前推)回调函数mc中理论就是将申明办法传入回调函数metadataLookup并执行,而后将申明办法和metadataLookup执行失去的后果存入map汇合,回调函数metadataLookup中理论就是将申明办法传入getMappingForMethod()办法,在getMappingForMethod()中会将申明办法和handler上的@RequestMapping注解信息解析成RequestMappingInfo并返回。

前文可知MethodIntrospector.selectMethods()中调用ReflectionUtils.doWithMethods()时传入的MethodFilter为ReflectionUtils.USER_DECLARED_METHODS,ReflectionUtils.USER_DECLARED_METHODS示意如果办法即不是桥接办法也不是合成办法时则匹配胜利,此时调用matches()返回true。

阐明一下getMappingForMethod()办法,该办法是AbstractHandlerMethodMapping申明的形象办法,RequestMappingHandlerMapping对其的实现如下。

protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
    //将method上的@RequestMapping注解内容解析为RequestMappingInfo
    RequestMappingInfo info = createRequestMappingInfo(method);
    if (info != null) {
        //将类上的@RequestMapping注解内容解析为RequestMappingInfo
        RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
        if (typeInfo != null) {
            //将method和类上的@RequestMethod注解解析成的RequestMappingInfo组合
            info = typeInfo.combine(info);
        }
        String prefix = getPathPrefix(handlerType);
        if (prefix != null) {
            info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);
        }
    }
    return info;
}

RequestMappingHandlerMapping在getMappingForMethod()中先后别离获取办法和类的@RequestMapping注解解析成的RequestMappingInfo并进行组合。@RequestMapping注解信息的解析产生在createRequestMappingInfo()办法中,其实现如下。

private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
    //查找@RequestMapping注解
    RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
    RequestCondition<?> condition = (element instanceof Class ?
            getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
    //可能查找到@RequestMapping注解,解析@RequestMapping的信息
    return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
}

protected RequestMappingInfo createRequestMappingInfo(
        RequestMapping requestMapping, @Nullable RequestCondition<?> customCondition) {

    RequestMappingInfo.Builder builder = RequestMappingInfo
            .paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
            .methods(requestMapping.method())
            .params(requestMapping.params())
            .headers(requestMapping.headers())
            .consumes(requestMapping.consumes())
            .produces(requestMapping.produces())
            .mappingName(requestMapping.name());
    if (customCondition != null) {
        builder.customCondition(customCondition);
    }
    return builder.options(this.config).build();
}

最初回到detectHandlerMethods()办法,该办法中执行完MethodIntrospector.selectMethods()后会失去method和RequestMappingInfo的map汇合,而后遍历map汇合并调用registerHandlerMethod()办法将handler,method和RequestMappingInfo缓存,并建设映射关系。registerHandlerMethod()办法中会调用mappingRegistry的register()办法,mappingRegistry是AbstractHandlerMethodMapping的一个外部类对象,register()办法次要是将handler,method和RequestMappingInfo写入mappingRegistry的pathLookUp,nameLookUp,corsLookUp和registry数据结构中。相干源码如下所示。

protected void registerHandlerMethod(Object handler, Method method, T mapping) {
    this.mappingRegistry.register(mapping, handler, method);
}

public void register(T mapping, Object handler, Method method) {
    this.readWriteLock.writeLock().lock();
    try {
        //获取HandlerMethod的实例,HandlerMethod对handler method进行了一层封装,其持有handler的对象,并且能够不便获取办法入参和出参
        HandlerMethod handlerMethod = createHandlerMethod(handler, method);
        //在registry中依据RequestMappingInfo获取曾经缓存的cachedHandlerMethod,如果cachedHandlerMethod不为空且不等于handlerMethod,则报错
        validateMethodMapping(handlerMethod, mapping);

        //依据RequestMappingInfo获取path的Set汇合,并建设path和RequestMappingInfo的映射关系:Map<String, List<RequestMappingInfo>> pathLookUp
        Set<String> directPaths = AbstractHandlerMethodMapping.this.getDirectPaths(mapping);
        for (String path : directPaths) {
            this.pathLookup.add(path, mapping);
        }

        String name = null;
        if (getNamingStrategy() != null) {
            //拼接name
            //规定:handler的Class对象名称取大写字母 + # + 办法名
            name = getNamingStrategy().getName(handlerMethod, mapping);
            //建设name与handlerMethod的映射关系:Map<String, List<HandlerMethod>> nameLookup
            addMappingName(name, handlerMethod);
        }

        //获取跨域配置对象
        CorsConfiguration config = initCorsConfiguration(handler, method, mapping);
        if (config != null) {
            config.validateAllowCredentials();
            //建设handlerMethod和跨域配置对象的映射关系:Map<HandlerMethod, CorsConfiguration> corsLookup
            this.corsLookup.put(handlerMethod, config);
        }

        //建设RequestMappingInfo与MappingRegistration的映射关系:Map<RequestMappingInfo, MappingRegistration<RequestMappingInfo>>
        this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directPaths, name));
    }
    finally {
        this.readWriteLock.writeLock().unlock();
    }
}

MappingRegistration将传入的RequestMappingInfo,获取的handlerMethod,取得的directPaths和拼接的name做了一层封装。

static class MappingRegistration<T> {

    private final T mapping;

    private final HandlerMethod handlerMethod;

    private final Set<String> directPaths;

    @Nullable
    private final String mappingName;

    public MappingRegistration(T mapping, HandlerMethod handlerMethod,
            @Nullable Set<String> directPaths, @Nullable String mappingName) {

        Assert.notNull(mapping, "Mapping must not be null");
        Assert.notNull(handlerMethod, "HandlerMethod must not be null");
        this.mapping = mapping;
        this.handlerMethod = handlerMethod;
        this.directPaths = (directPaths != null ? directPaths : Collections.emptySet());
        this.mappingName = mappingName;
    }

    public T getMapping() {
        return this.mapping;
    }

    public HandlerMethod getHandlerMethod() {
        return this.handlerMethod;
    }

    public Set<String> getDirectPaths() {
        return this.directPaths;
    }

    @Nullable
    public String getMappingName() {
        return this.mappingName;
    }
}

大节:RequestMappingHandlerMapping初始化时会先获取容器中所有被@Controller注解或@RequestMapping注解润饰的类的对象(handler对象),而后遍历这些对象和其父对象的所有办法,将这些办法的@RequestMapping注解信息(如果有)解析成RequestMappingInfo,最初将handler对象,handler办法和RequestMappingInfo退出缓存并建设映射关系。

三. RequestMappingHandlerMapping获取handler

回顾上文,一大节中提到,web申请来到DispatcherServlet之后,会先遍历HandlerMapping的汇合,而后将申请传入HandlerMapping并获取handler。再贴出源码如下所示。

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;
}

实际上,RequestMappingHandlerMapping获取handler是产生在其父类AbstractHandlerMapping的getHandler()办法中,源码如下。

public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    //依据request获取handler,理论获取到的handler是一个HandlerMethod对象
    Object handler = getHandlerInternal(request);
    if (handler == null) {
        handler = getDefaultHandler();
    }
    if (handler == null) {
        return null;
    }
    if (handler instanceof String) {
        String handlerName = (String) handler;
        handler = obtainApplicationContext().getBean(handlerName);
    }

    //依据request和handler创立HandlerExecutionChain对象,该对象还会蕴含和request匹配的拦截器
    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());
    }

    //如果handler有跨域配置,则更新HandlerExecutionChain对象使得其能够进行跨域解决
    if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
        CorsConfiguration config = getCorsConfiguration(handler, request);
        if (getCorsConfigurationSource() != null) {
            CorsConfiguration globalConfig = getCorsConfigurationSource().getCorsConfiguration(request);
            config = (globalConfig != null ? globalConfig.combine(config) : config);
        }
        if (config != null) {
            config.validateAllowCredentials();
        }
        executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
    }

    return executionChain;
}

getHandler()要害的操作就是在getHandlerInternal()办法中依据request获取到了handler对象(理论是一个HandlerMethod对象),RequestMappingHandlerMapping的getHandlerInternal()会调用父类AbstractHandlerMethodMapping的getHandlerInternal()办法,当初看一下其做了什么事件。

protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
    //依据request获取申请门路
    String lookupPath = initLookupPath(request);
    this.mappingRegistry.acquireReadLock();
    try {
        //依据申请门路和request获取最匹配的HandlerMethod对象
        HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
        return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
    }
    finally {
        this.mappingRegistry.releaseReadLock();
    }
}

protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
    List<Match> matches = new ArrayList<>();
    //通过缓存pathLookup获取申请门路映射的RequestMappingInfo汇合
    List<T> directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath);
    if (directPathMatches != null) {
        //汇合中的每一个RequestMappingInfo均会和request进行匹配,匹配上的话就创立一个Match对象并退出Match对象汇合
        addMatchingMappings(directPathMatches, matches, request);
    }
    if (matches.isEmpty()) {
        addMatchingMappings(this.mappingRegistry.getRegistrations().keySet(), matches, request);
    }
    //Match汇合不为空则从Match汇合中找到最匹配的Match对象,并返回该Match对象的HandlerMethod对象
    if (!matches.isEmpty()) {
        Match bestMatch = matches.get(0);
        if (matches.size() > 1) {
            Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
            matches.sort(comparator);
            bestMatch = matches.get(0);
            if (logger.isTraceEnabled()) {
                logger.trace(matches.size() + " matching mappings: " + matches);
            }
            if (CorsUtils.isPreFlightRequest(request)) {
                return PREFLIGHT_AMBIGUOUS_MATCH;
            }
            Match secondBestMatch = matches.get(1);
            if (comparator.compare(bestMatch, secondBestMatch) == 0) {
                Method m1 = bestMatch.handlerMethod.getMethod();
                Method m2 = secondBestMatch.handlerMethod.getMethod();
                String uri = request.getRequestURI();
                throw new IllegalStateException(
                        "Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
            }
        }
        request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
        handleMatch(bestMatch.mapping, lookupPath, request);
        return bestMatch.handlerMethod;
    }
    else {
        return handleNoMatch(this.mappingRegistry.getRegistrations().keySet(), lookupPath, request);
    }
}

大节:RequestMappingHandlerMapping获取handler理论就是依据request在映射缓存中寻找最匹配的HandlerMethod对象并封装成HandlerExecutionChain。

总结

RequestMappingHandlerMapping次要用于@Controller注解和@RequestMapping注解联合应用的场景,可能将咱们编写的控制器信息缓存并在申请到来时依据申请信息找到最合适的控制器来解决申请。最初,学习SpringMVC源码,通过打断点察看外部数据结构的形式往往可能更直观的帮忙咱们了解,值得尝试。

第一次写博客,集体能力无限,有许多中央不够深刻,也有许多中央了解存在偏差,敬请大家批评指正。

【腾讯云】云产品限时秒杀,爆款1核2G云服务器,首年50元

阿里云限时活动-2核2G-5M带宽-60G SSD-1000G月流量 ,特惠价99元/年(原价1234.2元/年,可以直接买3年),速抢

本文由乐趣区整理发布,转载请注明出处,谢谢。

您可能还喜欢...

发表评论

您的电子邮箱地址不会被公开。

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据