星空网 > 软件开发 > Java

SpringMVC之HandlerMapping源码剖析(一)

学习一种知识,我喜欢看看源码是怎么进行它们类之间的关系以及方法的调用,是怎么实现的。这样我才感觉踏实。

既然现在谈到HandlerMapping,我们先知道HandlerMapping的作用:HandlerMapping的作用就是解析请求链接,然后根据请求链接找到执行这个请求的类(HandlerMapping所说的handler,也就是我们写的Controller或是Action)。

现在我们来了解HandlerMapping的继承体系图:

SpringMVC之HandlerMapping源码剖析(一)

至于我们在配置文件中配置的BeanNameUrlHandlerMapping或者是SimpleUrlHandlerMapping,他们的目的是一样的,只是通过请求链接来找handler的方式不一样。

我们再来看看更详细的继承关系:

SpringMVC之HandlerMapping源码剖析(一)

HandlerMapping的使用主要分为两步:注册和查找。

注册是根据配置文件中的配置将一个字符串和一个Controller类以<key,value>的形式存入到Map中,这个key就是对应的url中的某个字段。

        查找就是HandlerMapping根据url中的的某个字段,在Map中以这个字段为key值对应的Controller类,并将Controller类封装成一个HandlerExecutionChain对象,HandlerExecutionChain中除了有Controller对象外,还有一组**。

现在我简单以SimpleUrlHandlerMapping为例子来分析HandlerMapping是如何根据请求链接找到Controller类的。

1.注册

  <!-- SpringMVC中的HandlerMapping配置  配置映射器 -->  <bean id="" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">  <property name="mappings">  <props>  <prop key="/*.do">helloword</prop>  </props>  </property>  </bean>  <!--配置处理器 -->    <bean id="helloword" class="cn.controller.HelloController">  <property name="methodNameResolver" ref="nameResolver">    </property>  </bean>

当我们第一次访问服务器的时候IOC容器会根据配置文件中的红色的部分生成一个Map<String, Object>,这个map里面的值就是{/*.do=/helloworld}。

SimpleUrlHandlerMapping的作用就是获取这个集合,然后根据这个集合里的value找到对应的bean,这样就可以把url中的某个字段和我们写的处理器对应起来。下面是SimpleUrlHandlerMapping中的关键源码

  /**   * Calls the {@link #registerHandlers} method in addition to the   * superclass's initialization.   */  @Override  public void initApplicationContext() throws BeansException {    super.initApplicationContext();    registerHandlers(this.urlMap);  }

我们来看看HandlerMapping的父类

SpringMVC之HandlerMapping源码剖析(一)

但是在这个类中没有initApplicationContext()方法,我们就再来看看AbstractUrlHandlerMapping的父类

SpringMVC之HandlerMapping源码剖析(一)

的确,在这个类中有了我们想要的initApplicationContext()方法。

/**   * Initializes the interceptors.   * @see #extendInterceptors(java.util.List)   * @see #initInterceptors()   */  @Override  protected void initApplicationContext() throws BeansException {    extendInterceptors(this.interceptors);    detectMappedInterceptors(this.adaptedInterceptors);    initInterceptors();  }

这个方法就是初始化SpringMVC容器,并对handler进行注册,urlMap中的值根据上面的配置文件就是{/*.do=/helloWorld}的

现在我们一起来看registerHandlers方法,

/**   * Register all handlers specified in the URL map for the corresponding paths.   * @param urlMap Map with URL paths as keys and handler beans or bean names as values   * @throws BeansException if a handler couldn't be registered   * @throws IllegalStateException if there is a conflicting handler registered   */  protected void registerHandlers(Map<String, Object> urlMap) throws BeansException {    if (urlMap.isEmpty()) {      logger.warn("Neither 'urlMap' nor 'mappings' set on SimpleUrlHandlerMapping");    }    else {      for (Map.Entry<String, Object> entry : urlMap.entrySet()) {        String url = entry.getKey();        Object handler = entry.getValue();        // Prepend with slash if not already present.        if (!url.startsWith("/")) {          url = "/" + url;        }        // Remove whitespace from handler bean name.        if (handler instanceof String) {          handler = ((String) handler).trim();        }         registerHandler(url, handler);      }    }  }

主要是对urlMap中的key值进行了一些处理,要是没有“/”的就加上"/",去掉空格等处理。这个方法中的重点是调用了registerHandler(url, handler)这个方法,在这个方法是它的父类AbstractUrlHandlerMapping中的方法。

我们来看看AbstractUrlHandlerMapping中的registerHandler(url, handler)的方法

/**   * Register the specified handler for the given URL path.   * @param urlPath the URL the bean should be mapped to   * @param handler the handler instance or handler bean name String   * (a bean name will automatically be resolved into the corresponding handler bean)   * @throws BeansException if the handler couldn't be registered   * @throws IllegalStateException if there is a conflicting handler registered   */  protected void registerHandler(String urlPath, Object handler) throws BeansException, IllegalStateException {    Assert.notNull(urlPath, "URL path must not be null");    Assert.notNull(handler, "Handler object must not be null");    Object resolvedHandler = handler;    // Eagerly resolve handler if referencing singleton via name.    if (!this.lazyInitHandlers && handler instanceof String) {      String handlerName = (String) handler;      if (getApplicationContext().isSingleton(handlerName)) {        resolvedHandler = getApplicationContext().getBean(handlerName);      }    }    Object mappedHandler = this.handlerMap.get(urlPath);    if (mappedHandler != null) {      if (mappedHandler != resolvedHandler) {        throw new IllegalStateException(            "Cannot map " + getHandlerDescription(handler) + " to URL path [" + urlPath +            "]: There is already " + getHandlerDescription(mappedHandler) + " mapped.");      }    }    else {      if (urlPath.equals("/")) {        if (logger.isInfoEnabled()) {          logger.info("Root mapping to " + getHandlerDescription(handler));        }        setRootHandler(resolvedHandler);      }      else if (urlPath.equals("/*")) {        if (logger.isInfoEnabled()) {          logger.info("Default mapping to " + getHandlerDescription(handler));        }        setDefaultHandler(resolvedHandler);      }      else {        this.handlerMap.put(urlPath, resolvedHandler);        if (logger.isInfoEnabled()) {          logger.info("Mapped URL path [" + urlPath + "] onto " + getHandlerDescription(handler));        }      }    }

看registerHandler方法红色的部分大家,可以看出是根据SimpleUrlHandlerMapping中的urlMap中的value值在ioc容器中找到对应的bean,并将url的某个字段作为key值,bean作为value存入到AbstractUrlHandlerMapping的urlMap属性中去,这样就达到url的某个字段对应到具体的controller了的目的,当遇到有请求访问服务器的时候,就可以根据url找到具体的controller去执行这个请求了。

 2.查找

  在Dispatcher类中,根据配置文件对handlerMapping进行注册,即对handlerMapping的初始化。

 protected void initStrategies(ApplicationContext context) { initMultipartResolver(context); initLocaleResolver(context); initThemeResolver(context); <span style="color:#ff0000;">initHandlerMappings(context);</span> initHandlerAdapters(context); initHandlerExceptionResolvers(context); initRequestToViewNameTranslator(context); initViewResolvers(context); 

 private void initHandlerMappings(ApplicationContext context) { this.handlerMappings = null;  if (this.detectAllHandlerMappings) {   // Find all HandlerMappings in the ApplicationContext, including ancestor contexts.   <span style="color:#ff0000;">Map<String, HandlerMapping> matchingBeans =       BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);</span>   if (!matchingBeans.isEmpty()) {     <span style="color:#ff0000;">this.handlerMappings = new ArrayList<HandlerMapping>(matchingBeans.values());</span>     // We keep HandlerMappings in sorted order.     OrderComparator.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.   } } 

于在配置文件中有两种不同类型的handlerMapping,所以从ioc容器中读取出来的handlerMapping有两个,然后将这两个handlerMapping的实例放入Dodispatcher中的handlerMappings属性中。

 

下面一步就是真正的根据url中的某个字段到已经注册好了的Map<urlString,Controller>中找出执行这个url请求的Controller,用户的请求在被Dispatcher拦截后,会交给Dispatcher的doDispatch执行。在doDispatch方法中主要看红色标记的getHandler方法

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {     HttpServletRequest processedRequest = request;     HandlerExecutionChain mappedHandler = null;     int interceptorIndex = -1;      try {       ModelAndView mv;       boolean errorView = false;        try {         processedRequest = checkMultipart(request);          // Determine handler for the current request.         <span style="color:#ff0000;">mappedHandler = getHandler(processedRequest, false);</span>         if (mappedHandler == null || mappedHandler.getHandler() == null) {           noHandlerFound(processedRequest, response);           return;         }          // Determine handler adapter for the current request.         HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());          // Process last-modified header, if supported by the handler.         String method = request.getMethod();         boolean isGet = "GET".equals(method);         if (isGet || "HEAD".equals(method)) {           long lastModified = ha.getLastModified(request, mappedHandler.getHandler());           if (logger.isDebugEnabled()) {             String requestUri = urlPathHelper.getRequestUri(request);             logger.debug("Last-Modified value for [" + requestUri + "] is: " + lastModified);           }           if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {             return;           }         }          // Apply preHandle methods of registered interceptors.         HandlerInterceptor[] interceptors = mappedHandler.getInterceptors();         if (interceptors != null) {           for (int i = 0; i < interceptors.length; i++) {             HandlerInterceptor interceptor = interceptors[i];             if (!interceptor.preHandle(processedRequest, response, mappedHandler.getHandler())) {               triggerAfterCompletion(mappedHandler, interceptorIndex, processedRequest, response, null);               return;             }             interceptorIndex = i;           }         }          // Actually invoke the handler.         mv = ha.handle(processedRequest, response, mappedHandler.getHandler());          // Do we need view name translation?         if (mv != null && !mv.hasView()) {           mv.setViewName(getDefaultViewName(request));         }          // Apply postHandle methods of registered interceptors.         if (interceptors != null) {           for (int i = interceptors.length - 1; i >= 0; i--) {             HandlerInterceptor interceptor = interceptors[i];             interceptor.postHandle(processedRequest, response, mappedHandler.getHandler(), mv);           }         }       }       catch (ModelAndViewDefiningException ex) {         logger.debug("ModelAndViewDefiningException encountered", ex);         mv = ex.getModelAndView();       }       catch (Exception ex) {         Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);         mv = processHandlerException(processedRequest, response, handler, ex);         errorView = (mv != null);       }        // Did the handler return a view to render?       if (mv != null && !mv.wasCleared()) {         render(mv, processedRequest, response);         if (errorView) {           WebUtils.clearErrorRequestAttributes(request);         }       }       else {         if (logger.isDebugEnabled()) {           logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + getServletName() +               "': assuming HandlerAdapter completed request handling");         }       }        // Trigger after-completion for successful outcome.       triggerAfterCompletion(mappedHandler, interceptorIndex, processedRequest, response, null);     } 

getHandler方法主要会调用已经注册好了的handlerMapping中的getHandler方法

DispatcherServlet中的getHandler方法

/**   * Return the HandlerExecutionChain for this request.   * <p>Tries all handler mappings in order.   * @param request current HTTP request   * @return the HandlerExecutionChain, or {@code null} if no handler could be found   */  protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {    for (HandlerMapping hm : this.handlerMappings) {      if (logger.isTraceEnabled()) {        logger.trace(            "Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'");      }      HandlerExecutionChain handler = hm.getHandler(request);      if (handler != null) {        return handler;      }    }    return null;  }

 

现在再来看看HandlerMapping的getHandler方法,可以看到HandlerMapping接口中只有一个getHandler方法

public interface HandlerMapping {  /**   * Name of the {@link HttpServletRequest} attribute that contains the path   * within the handler mapping, in case of a pattern match, or the full   * relevant URI (typically within the DispatcherServlet's mapping) else.   * <p>Note: This attribute is not required to be supported by all   * HandlerMapping implementations. URL-based HandlerMappings will   * typically support it, but handlers should not necessarily expect   * this request attribute to be present in all scenarios.   */  String PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE = HandlerMapping.class.getName() + ".pathWithinHandlerMapping";  /**   * Name of the {@link HttpServletRequest} attribute that contains the   * best matching pattern within the handler mapping.   * <p>Note: This attribute is not required to be supported by all   * HandlerMapping implementations. URL-based HandlerMappings will   * typically support it, but handlers should not necessarily expect   * this request attribute to be present in all scenarios.   */  String BEST_MATCHING_PATTERN_ATTRIBUTE = HandlerMapping.class.getName() + ".bestMatchingPattern";  /**   * Name of the boolean {@link HttpServletRequest} attribute that indicates   * whether type-level mappings should be inspected.   * <p>Note: This attribute is not required to be supported by all   * HandlerMapping implementations.   */  String INTROSPECT_TYPE_LEVEL_MAPPING = HandlerMapping.class.getName() + ".introspectTypeLevelMapping";  /**   * Name of the {@link HttpServletRequest} attribute that contains the URI   * templates map, mapping variable names to values.   * <p>Note: This attribute is not required to be supported by all   * HandlerMapping implementations. URL-based HandlerMappings will   * typically support it, but handlers should not necessarily expect   * this request attribute to be present in all scenarios.   */  String URI_TEMPLATE_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName() + ".uriTemplateVariables";  /**   * Name of the {@link HttpServletRequest} attribute that contains a map with   * URI matrix variables.   * <p>Note: This attribute is not required to be supported by all   * HandlerMapping implementations and may also not be present depending on   * whether the HandlerMapping is configured to keep matrix variable content   * in the request URI.   */  String MATRIX_VARIABLES_ATTRIBUTE = HandlerMapping.class.getName() + ".matrixVariables";  /**   * Name of the {@link HttpServletRequest} attribute that contains the set of   * producible MediaTypes applicable to the mapped handler.   * <p>Note: This attribute is not required to be supported by all   * HandlerMapping implementations. Handlers should not necessarily expect   * this request attribute to be present in all scenarios.   */  String PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE = HandlerMapping.class.getName() + ".producibleMediaTypes";  /**   * Return a handler and any interceptors for this request. The choice may be made   * on request URL, session state, or any factor the implementing class chooses.   * <p>The returned HandlerExecutionChain contains a handler Object, rather than   * even a tag interface, so that handlers are not constrained in any way.   * For example, a HandlerAdapter could be written to allow another framework's   * handler objects to be used.   * <p>Returns {@code null} if no match was found. This is not an error.   * The DispatcherServlet will query all registered HandlerMapping beans to find   * a match, and only decide there is an error if none can find a handler.   * @param request current HTTP request   * @return a HandlerExecutionChain instance containing handler object and   * any interceptors, or {@code null} if no mapping found   * @throws Exception if there is an internal error   */  HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception;}

再看看实现了HandlerMapping的AbstractHandlerMapping抽象类,AbstractHandlerMapping中的getHandler方法,这个方法的主要作用是根据url找到controller后,并将controller封装成一个HandlerExecutionChain对象

@Override  public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {    Object handler = getHandlerInternal(request);    if (handler == null) {      handler = getDefaultHandler();    }    if (handler == null) {      return null;    }    // Bean name or resolved handler?    if (handler instanceof String) {      String handlerName = (String) handler;      handler = getApplicationContext().getBean(handlerName);    }    HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);    if (CorsUtils.isCorsRequest(request)) {      CorsConfiguration globalConfig = this.corsConfigSource.getCorsConfiguration(request);      CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);      CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);      executionChain = getCorsHandlerExecutionChain(request, executionChain, config);    }    return executionChain;  }

AbstractHandlerMapping的getHandlerInternal方法是个抽象方法,由AbstractHandlerMapping的子类AbstractUrlHandlerMapping实现

/**   * Look up a handler for the given request, returning {@code null} if no   * specific one is found. This method is called by {@link #getHandler};   * a {@code null} return value will lead to the default handler, if one is set.   * <p>On CORS pre-flight requests this method should return a match not for   * the pre-flight request but for the expected actual request based on the URL   * path, the HTTP methods from the "Access-Control-Request-Method" header, and   * the headers from the "Access-Control-Request-Headers" header thus allowing   * the CORS configuration to be obtained via {@link #getCorsConfigurations},   * <p>Note: This method may also return a pre-built {@link HandlerExecutionChain},   * combining a handler object with dynamically determined interceptors.   * Statically specified interceptors will get merged into such an existing chain.   * @param request current HTTP request   * @return the corresponding handler instance, or {@code null} if none found   * @throws Exception if there is an internal error   */  protected abstract Object getHandlerInternal(HttpServletRequest request) throws Exception;

 

AbstractUrlHandlerMapping实现类里面的getHandlerInternal方法

/**   * Look up a handler for the URL path of the given request.   * @param request current HTTP request   * @return the handler instance, or {@code null} if none found   */  @Override  protected Object getHandlerInternal(HttpServletRequest request) throws Exception {    String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);    Object handler = lookupHandler(lookupPath, request);    if (handler == null) {      // We need to care for the default handler directly, since we need to      // expose the PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE for it as well.      Object rawHandler = null;      if ("/".equals(lookupPath)) {        rawHandler = getRootHandler();      }      if (rawHandler == null) {        rawHandler = getDefaultHandler();      }      if (rawHandler != null) {        // Bean name or resolved handler?        if (rawHandler instanceof String) {          String handlerName = (String) rawHandler;          rawHandler = getApplicationContext().getBean(handlerName);        }        validateHandler(rawHandler, request);        handler = buildPathExposingHandler(rawHandler, lookupPath, lookupPath, null);      }    }    if (handler != null && logger.isDebugEnabled()) {      logger.debug("Mapping [" + lookupPath + "] to " + handler);    }    else if (handler == null && logger.isTraceEnabled()) {      logger.trace("No handler mapping found for [" + lookupPath + "]");    }    return handler;  }

getLookupPathForRequest方法主要是截取url中对应controller的那一部分,lookupHandler方法根据截取的url字段找到对应的controller,看到红色的部分就和我们注册handlerMapping的那一步相关了,我们早早的就将url的部分字段所对应的controller放到了AbstractUrlHandlerMapping中的handlerMap属性中了,现在就能根据url找到对应的controller了

/**   * Look up a handler instance for the given URL path.   * <p>Supports direct matches, e.g. a registered "/test" matches "/test",   * and various Ant-style pattern matches, e.g. a registered "/t*" matches   * both "/test" and "/team". For details, see the AntPathMatcher class.   * <p>Looks for the most exact pattern, where most exact is defined as   * the longest path pattern.   * @param urlPath URL the bean is mapped to   * @param request current HTTP request (to expose the path within the mapping to)   * @return the associated handler instance, or {@code null} if not found   * @see #exposePathWithinMapping   * @see org.springframework.util.AntPathMatcher   */  protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception {    // Direct match?    Object handler = this.handlerMap.get(urlPath);    if (handler != null) {      // Bean name or resolved handler?      if (handler instanceof String) {        String handlerName = (String) handler;        handler = getApplicationContext().getBean(handlerName);      }      validateHandler(handler, request);      return buildPathExposingHandler(handler, urlPath, urlPath, null);    }    // Pattern match?    List<String> matchingPatterns = new ArrayList<String>();    for (String registeredPattern : this.handlerMap.keySet()) {      if (getPathMatcher().match(registeredPattern, urlPath)) {        matchingPatterns.add(registeredPattern);      }      else if (useTrailingSlashMatch()) {        if (!registeredPattern.endsWith("/") && getPathMatcher().match(registeredPattern + "/", urlPath)) {          matchingPatterns.add(registeredPattern +"/");        }      }    }    String bestPatternMatch = null;    Comparator<String> patternComparator = getPathMatcher().getPatternComparator(urlPath);    if (!matchingPatterns.isEmpty()) {      Collections.sort(matchingPatterns, patternComparator);      if (logger.isDebugEnabled()) {        logger.debug("Matching patterns for request [" + urlPath + "] are " + matchingPatterns);      }      bestPatternMatch = matchingPatterns.get(0);    }    if (bestPatternMatch != null) {      handler = this.handlerMap.get(bestPatternMatch);      if (handler == null) {        Assert.isTrue(bestPatternMatch.endsWith("/"));        handler = this.handlerMap.get(bestPatternMatch.substring(0, bestPatternMatch.length() - 1));      }      // Bean name or resolved handler?      if (handler instanceof String) {        String handlerName = (String) handler;        handler = getApplicationContext().getBean(handlerName);      }      validateHandler(handler, request);      String pathWithinMapping = getPathMatcher().extractPathWithinPattern(bestPatternMatch, urlPath);      // There might be multiple 'best patterns', let's make sure we have the correct URI template variables      // for all of them      Map<String, String> uriTemplateVariables = new LinkedHashMap<String, String>();      for (String matchingPattern : matchingPatterns) {        if (patternComparator.compare(bestPatternMatch, matchingPattern) == 0) {          Map<String, String> vars = getPathMatcher().extractUriTemplateVariables(matchingPattern, urlPath);          Map<String, String> decodedVars = getUrlPathHelper().decodePathVariables(request, vars);          uriTemplateVariables.putAll(decodedVars);        }      }      if (logger.isDebugEnabled()) {        logger.debug("URI Template variables for request [" + urlPath + "] are " + uriTemplateVariables);      }      return buildPathExposingHandler(handler, bestPatternMatch, pathWithinMapping, uriTemplateVariables);    }    // No handler found...    return null;  }

到这里算是完了。但是大家可能感觉有点蒙,所以还总结了,方便记忆和理解

就对源码中是如何根据url找到对应的controller进行总结

1.SimpleUrlHandlerMapping根据配置文件中的SimpleUrlHandlerMapping的配置,获得一个map集合,map中存储的是{urlString=beanId}。SimpleUrlHandlerMapping调用父类
AbstractUrlHandlerMapping的registerHandler方法。

2.AbstractUrlHandlerMapping的registerHandler方法有SimpleUrlHandlerMapping传入的map中的urlString和beanId,并根据beanId找到对应的bean即controller,将urlString和urlString对应的controller放入AbstractUrlHandlerMapping的handlerMap中。

3.Dispatcher获取IOC容器中已经初始化好的HandlerMapping,再由HandlerMapping调用自己的getHandler方法根据请求返回HandlerExecutionChain对象。AbstractHandlerMapping方法实现了HandlerMapping接口的getHandler方法。AbstractHandlerMapping中的getHandler方法的主要作用是找到controller,并对controller进行封装成HandlerExecutionChain对象,HandlerExecutionChain中除了controller对象外,还有**对象的集合。

4.AbstractHandlerMapping的getHandler方法中又 调用了AbstractHandlerMapping子类的AbstractUrlHandlerMapping getHandlerInternal方法。getHandlerInternal方法就是截取url中对应的controller字段,并以这个字段为key值去AbstractUrlHandlerMapping 的handlerMap中找寻对应的value,即controlle。

 




原标题:SpringMVC之HandlerMapping源码剖析(一)

关键词:Spring

*特别声明:以上内容来自于网络收集,著作权属原作者所有,如有侵权,请联系我们: admin#shaoqun.com (#换成@)。

又一亚马逊大卖宣告破产!:https://www.ikjzd.com/articles/1670599649988911105
亚马逊申诉大全,这些模板(中英文)帮你撤掉小红旗!:https://www.ikjzd.com/articles/16706
独立站零售和批发可以一起做吗?需要注意什么呢?:https://www.ikjzd.com/articles/1670604961059905538
沃尔玛自养号运营的重要性和优势详解:https://www.ikjzd.com/articles/1670606689780658177
Facebook海外户—为什么广告没有效果,看看这几个错误你有没有犯:https://www.ikjzd.com/articles/1670607433870548994
注册美国公司入驻亚马逊、TIKTOK等平台,公司类型LLC和INC哪个更适合注册?:https://www.ikjzd.com/articles/1670610424388939778
三亚有哪些酒店值得入住?:https://www.vstour.cn/a/366173.html
零售晚报:丽人丽妆2023年扭亏为盈 玉容初、美壹堂等自有品牌增速超40% :https://www.kjdsnews.com/a/1836649.html
相关文章
我的浏览记录
最新相关资讯
海外公司注册 | 跨境电商服务平台 | 深圳旅行社 | 东南亚物流