中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Spring源碼解析-applicationContext.xml加載和bean的注冊

發布時間:2020-07-03 19:40:33 來源:網絡 閱讀:345 作者:java架構師1 欄目:編程語言

applicationContext文件加載和bean注冊流程
? Spring對于從事Java開發的boy來說,再熟悉不過了,對于我們這個牛逼的框架的介紹就不在這里復述了,Spring這個大雜燴,怎么去使用怎么去配置,各種百度谷歌都能查到很多大牛教程,但是,當我們按著教程一步步的把spring的開發框架搭建起來的時候,有沒有一種想搞明白spring的沖動,萬事開頭難,就要從開頭開始,而我認為spring開頭就是如何加載配置文件,并初始化配置文件里面的bean當然也包括了我們用注解Service、Component等注解注解的bean,spring在容器啟動的時候就要去加載這些內容,然后統一管理這些bean(統一管理的是他們的bean definition),這也就是spring的一個重要概念bean的容器。

? applicationContext.xml到底是如何加載的呢?我把他簡化成以下流程,當然了每個環節里Spring的實現都是錯綜復雜的,也是很佩服寫Spring的大神。
Spring源碼解析-applicationContext.xml加載和bean的注冊
Spring初始化
? 當我們初學Spring的教程的時候,教程里面肯定會有這樣的一步操作,就是新建一個applicationContext.xml文件,當然了這是Spring里必須要有的一個文件,在這個文件里面我們可以進行bean的配置等等工作,讓Spring來管理我們的Bean。然后,這個文件放在哪里也是個比較講究的事情,可能對于初學者來說可額能會往WEB-INF文件夾一放就了事了,確實這樣是可以的,因為Spring默認的位置就是這個,但是我們一般不這么做,一般會把這個文件放在resource里面,那這樣子做的話,你就要指定位置,讓Spring知道你這個文件的位置,這就有了下面一段代碼,我們的Spring項目都會在web.xml配置這樣的代碼:

<context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:applicationContext.xml</param-value>
</context-param>

那問題來了,當項目啟動的時候,spring是怎么去初始化應用的上下文的呢?答案就在類ContextLoader.java里面。當Tomcat啟動時候會調用該類里面的一個方法public WebApplicationContext initWebApplicationContext(ServletContext servletContext),這個方法主要完成,根據我們在web.xml里面配置的contextConfigLocation初始化spring的web的應用上下文。具體看下改方法的實現(非完整代碼,PS:由于太長了):

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
    ......
    this.context = createWebApplicationContext(servletContext);//主要代碼,創建web應用上下文  
    ......
    configureAndRefreshWebApplicationContext(cwac, servletContext);//配置參數并調用初始化方法
    ......  
}

在這個方法里面有兩句重要代碼,第一句createWebApplicationContext(servletContext),這個會根據你配置的contextClass創建一個WebApplicationContext對象,但是我們一般不會配置這個參數,所以Spring默認會創建一個XMLWebApplicationContext對象,而這個就是后續操作的的重要對象,然后接下來一句重要代碼configureAndRefreshWebApplicationContext(cwac, servletContext)這個就會去讀取我們在web.xml里面配置的參數并set到變量里頭去,這樣Spring就能找到我們項目的applicationContext.xml文件了,到底如何找到下面會講。接下來我們來看下configureAndRefreshWebApplicationContext方法的實現如下:

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
        if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
            // The application context id is still set to its original default value
            // -> assign a more useful id based on available information
            String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
            if (idParam != null) {
                wac.setId(idParam);
            }
            else {
                // Generate default id...
                wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
                        ObjectUtils.getDisplayString(sc.getContextPath()));
            }
        }

        wac.setServletContext(sc);
        String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
        if (configLocationParam != null) {
            wac.setConfigLocation(configLocationParam);
        }

        // The wac environment's #initPropertySources will be called in any case when the context
        // is refreshed; do it eagerly here to ensure servlet property sources are in place for
        // use in any post-processing or initialization that occurs below prior to #refresh
        ConfigurableEnvironment env = wac.getEnvironment();
        if (env instanceof ConfigurableWebEnvironment) {
            ((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
        }

        customizeContext(sc, wac);
        wac.refresh();
    }

在這個方法中我們只要關注兩個地方,第一個:

String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
        if (configLocationParam != null) {
            wac.setConfigLocation(configLocationParam);
        }

這塊代碼塊就是,講我們配置在web.xml里面的參數set到我們的變量中去。第二個地方就是:

wac.refresh();

調用這個執行后續的加載文件操作等后續操作。

Spring是如何找到applicationContext.xml文件
? 其實,從refresh到Spring里去查找配置文件路徑之間,有很多步驟,這些也都要花點時間去理解的,在這里不展開講,我們只要知道,XmlWebApplicationContext會委托給XmlBeanDefinitionReader類去解析配置文件,在XmlWebApplicationContext類里面有個方法loadBeanDefinitions如下:

protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws IOException {
        String[] configLocations = getConfigLocations();
        if (configLocations != null) {
            for (String configLocation : configLocations) {
                reader.loadBeanDefinitions(configLocation);
            }
        }
    }

該方法就是將一個個的配置文件委托給XmlBeanDefinitionReader去解析配置文件,但是解析之前有句代碼String[] configLocations = getConfigLocations();這個就是查找我們的配置的文件的方法,

protected String[] getConfigLocations() {
        return (this.configLocations != null ? this.configLocations : getDefaultConfigLocations());
    }

實現很簡單,就是我們有配置該位置地址就會去讀我們配置的路徑,否則就會去讀默認的配置文件路徑,這就是開篇說到的要是沒配置路徑也能讀取到配置文件,前提就是要跟Spring默認定義好的文件路徑及文件名保持一致才行。getDefaultConfigLocations函數的實現也很簡單:

/** Default config location for the root context */
public static final String DEFAULT_CONFIG_LOCATION = "/WEB-INF/applicationContext.xml";

/** Default prefix for building a config location for a namespace */
public static final String DEFAULT_CONFIG_LOCATION_PREFIX = "/WEB-INF/";

/** Default suffix for building a config location for a namespace */
public static final String DEFAULT_CONFIG_LOCATION_SUFFIX = ".xml";

protected String[] getDefaultConfigLocations() {
        if (getNamespace() != null) {
            return new String[] {DEFAULT_CONFIG_LOCATION_PREFIX + getNamespace() + DEFAULT_CONFIG_LOCATION_SUFFIX};
        }
        else {
            return new String[] {DEFAULT_CONFIG_LOCATION};
        }
    }

如果配置了namespace就會去找這個名字的xml配置文件,如果沒有配置就去找默認的配置文件。所以不管如何,這個配置文件是必須在spring項目中的。至此,配置文件基本將完,接下來就是重頭戲了,就是解析xml以及xml里面的節點,并注冊到spring的bean容器中去。

將xml文件轉成Document處理對象
如何將xml轉成Document對象,這個也是很復雜的操作,首先將resource讀取InputStream流,在將InputStream流包裝成InputSource對象,在處理成Document對象,直接上代碼:

public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException {
        Assert.notNull(encodedResource, "EncodedResource must not be null");
        if (logger.isInfoEnabled()) {
            logger.info("Loading XML bean definitions from " + encodedResource.getResource());
        }

        Set<EncodedResource> currentResources = this.resourcesCurrentlyBeingLoaded.get();
        if (currentResources == null) {
            currentResources = new HashSet<EncodedResource>(4);
            this.resourcesCurrentlyBeingLoaded.set(currentResources);
        }
        if (!currentResources.add(encodedResource)) {
            throw new BeanDefinitionStoreException(
                    "Detected cyclic loading of " + encodedResource + " - check your import definitions!");
        }
        try {
            InputStream inputStream = encodedResource.getResource().getInputStream();//獲取流
            try {
                InputSource inputSource = new InputSource(inputStream);
                if (encodedResource.getEncoding() != null) {
                    inputSource.setEncoding(encodedResource.getEncoding());
                }
                return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
            }
            finally {
                inputStream.close();
            }
        }
        catch (IOException ex) {
            throw new BeanDefinitionStoreException(
                    "IOException parsing XML document from " + encodedResource.getResource(), ex);
        }
        finally {
            currentResources.remove(encodedResource);
            if (currentResources.isEmpty()) {
                this.resourcesCurrentlyBeingLoaded.remove();
            }
        }
    }

接下來又到doLoadBeanDefinitions(inputSource, encodedResource.getResource());方法去了,該方法就是生成Doucument對象的,然后就是解析具體的節點了,部分源碼如下:

protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource)
            throws BeanDefinitionStoreException {

            Document doc = doLoadDocument(inputSource, resource);//這就是解析成Document對象的操作
            return registerBeanDefinitions(doc, resource);
            ......
}

解析Document不展開講了,不是本篇的重點,重點是下面的,spring如何解析xml文件的bean及注解的bean然后注冊到容器中去,registerBeanDefinitions(doc, resource)是下面的重點。

解析Document里面的節點
XmlBeanDfinitionReader本身又不是直接取解析document的,他是委托給了DefaultBeanDefinitionDocumentReader類去實現,源代碼中,會去創建DefaultBeanDefinitionDocumentReader對象實例,然后調用實例的注冊方法,代碼如下:

public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException {
        BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader();
        int countBefore = getRegistry().getBeanDefinitionCount();
        documentReader.registerBeanDefinitions(doc, createReaderContext(resource));
        return getRegistry().getBeanDefinitionCount() - countBefore;
    }

首先,我們必須知道,spring的xml文件里面有兩種類型的節點,一種是默認節點,相對于默認節點之外的節點統稱自定義節點,這可以從源碼里面知道,而默認節點有以下幾個:beans、import、alias、bean這幾個節點是默認節點,而相對于這幾個節點之外的都是默認節點,applicationContext里面有幾個自定義節點,如下:property-placeholder、property-override、annotation-config、component-scan、load-time-weaver、spring-configured、mbean-export、mbean-server,這里面常見的有component-scan等,為什么spring要分成默認和自定義節點呢,是因為自定義節點都有特定的業務,比如component-scan,他是去掃描程序包,加載用注解定義的bean,例如開發中的service等bean,所以這些自定義節點都配備了解析器,這些解析器預先初始化好的,解析到什么節點就去獲取相應的解析器去處理相應的業務,自定義節點解析器配置如下:

@Override
    public void init() {
        registerBeanDefinitionParser("property-placeholder", new PropertyPlaceholderBeanDefinitionParser());
        registerBeanDefinitionParser("property-override", new PropertyOverrideBeanDefinitionParser());
        registerBeanDefinitionParser("annotation-config", new AnnotationConfigBeanDefinitionParser());
        registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser());
        registerBeanDefinitionParser("load-time-weaver", new LoadTimeWeaverBeanDefinitionParser());
        registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
        registerBeanDefinitionParser("mbean-export", new MBeanExportBeanDefinitionParser());
        registerBeanDefinitionParser("mbean-server", new MBeanServerBeanDefinitionParser());
    }

從以上源碼分析,我們可以得到一個推論:

我們自己可以自定義xml的節點,spring可以去解析我們自定義的xml節點。

其實這個推論明顯成立,我們可以看到spring里面到處都是這種自定義的節點的。

這里又引申出一個問題:spring怎么去區分默認節點和自定義節點的呢?答案是通過節點的namespaceUri屬性去判斷,namespaceUri是什么東東?我們來看下,默認節點的namespaceUri是怎么樣的,源碼是這樣定義的:

public static final String BEANS_NAMESPACE_URI = "http://www.springframework.org/schema/beans";

是不是很熟悉,這貨就是我們配置文件里面的beans根節點會寫的東西,如下:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd 
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context.xsd">
</beans>

但是問題又來了,子節點上我們根本沒配置這貨,但是也能讀取到,以下是個人推論:

子節點會繼承父節點的屬性,這就說的通,子節點即使沒配置那一堆東西也能判斷為默認節點。

接下來,就是解析Document的元素,從root元素開始解析,這時候spring是創建了一個解析類的代理類,所有的比較和解析操作都有該類完成,我們來看下spring的源碼實現:

protected void doRegisterBeanDefinitions(Element root) {        
        BeanDefinitionParserDelegate parent = this.delegate;
        this.delegate = createDelegate(getReaderContext(), root, parent);

        if (this.delegate.isDefaultNamespace(root)) {
            String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE);
            if (StringUtils.hasText(profileSpec)) {
                String[] specifiedProfiles = StringUtils.tokenizeToStringArray(
                        profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS);
                if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) {
                    if (logger.isInfoEnabled()) {
                        logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec +
                                "] not matching: " + getReaderContext().getResource());
                    }
                    return;
                }
            }
        }

        preProcessXml(root);
        parseBeanDefinitions(root, this.delegate);
        postProcessXml(root);

        this.delegate = parent;
    }

解析節點的過程是個遞歸的過程,每次都要記錄節點的父節點,首先會創建一個delegate對象,然后再去解析節點,調用parseBeanDefinitions(root, this.delegate);這個方法進行解析操作;

繼續來看下parseBeanDefinitions(root, this.delegate);的實現:

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
        if (delegate.isDefaultNamespace(root)) {
            NodeList nl = root.getChildNodes();
            for (int i = 0; i < nl.getLength(); i++) {
                Node node = nl.item(i);
                if (node instanceof Element) {
                    Element ele = (Element) node;
                    if (delegate.isDefaultNamespace(ele)) {
                        parseDefaultElement(ele, delegate);
                    }
                    else {
                        delegate.parseCustomElement(ele);
                    }
                }
            }
        }
        else {
            delegate.parseCustomElement(root);
        }
    }

很簡單,可以很清晰的看出,解析是分默認節點和自定義節點分開解析的,而自定義的節點的解析其實就是找到對應的解析器各自處理對應的業務,如component-scan會找到ComponentScanBeanDefinitionParser類來處理對應的掃描包注冊bean的操作,而默認的節點的處理有如下幾種,代碼如下:

private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
        //處理import
        if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
            importBeanDefinitionResource(ele);
        }
        //處理alias
        else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
            processAliasRegistration(ele);
        }
        //處理bean
        else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
            processBeanDefinition(ele, delegate);
        }
        //處理beans
        else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) {
            // recurse
            doRegisterBeanDefinitions(ele);
        }
    }

import的處理相對其他幾種比較復雜點,但最終還是處理變成其他3種的處理,而beans的處理就重新遞歸上面提到的方法,最重要的是bean的處理,bean的處理其實就是下面要講的內容,解析bean并注冊bean definition的過程。

注冊bean
終于到了最后一個內容了,也是最重要的一個內容,上面講的所有都是為了這個而服務的,讀取配置文件也是為了加載bean,然后注冊到spring的容器里面,讓spring統一管理我們定義的bean。大家都很明白,spring的bean的容器,但是如果沒有去看源碼的話,是不是都認為spring,是把每個實例對象注冊到容器里面然后統一管理的?其實,spring其實不是這樣的做的,spring注冊的bean最終是個bean的定義,即BeanDefinition這個實例,并不是一個個類的具體實例。我們可以簡單理解這些注冊的bean definition是為了方便后續的實例化bean進行的一步準備操作。所謂的注冊,其實就是把各種這些實例用一個Map來管理,所以,spring的bean的容器的底層存儲其實是用Map來實現的(這個之前面試被問過)。接下來,看看源碼的實現:

protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
        BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
        if (bdHolder != null) {
            //這個是對bean definition進行修改如果有必要,如配置了代理的bean等
            bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
            try {
                // Register the final decorated instance.
                BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
            }
            catch (BeanDefinitionStoreException ex) {
                getReaderContext().error("Failed to register bean definition with name '" +
                        bdHolder.getBeanName() + "'", ele, ex);
            }
            // Send registration event.
            getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
        }
    }

從源碼里可以看出,bean的解析類代理會去解析ele元素,并返回一個BeanDefinitionHolder的實例,而這個BeanDefinitionHolder我們可以簡單理解為BeanDefinition對象的持有對象。然后,通過調用BeanDefinitionReaderUtils工具類去執行具體的注冊操作。繼續看BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry())這個的實現如下:

public static void registerBeanDefinition(
            BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)
            throws BeanDefinitionStoreException {

        // Register bean definition under primary name.
        String beanName = definitionHolder.getBeanName();
        registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());

        // Register aliases for bean name, if any.
        String[] aliases = definitionHolder.getAliases();
        if (aliases != null) {
            for (String alias : aliases) {
                registry.registerAlias(beanName, alias);
            }
        }
    }

從上面代碼中,spring注冊bean其實注冊的是BeanDfinition,注冊bean其實就是綁定bean的name和BeanDfinition的關系。那么,我們繼續看看bean的具體注冊過程,代碼如下:

public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
            throws BeanDefinitionStoreException {

        Assert.hasText(beanName, "Bean name must not be empty");
        Assert.notNull(beanDefinition, "BeanDefinition must not be null");

        if (beanDefinition instanceof AbstractBeanDefinition) {
            try {
                ((AbstractBeanDefinition) beanDefinition).validate();
            }
            catch (BeanDefinitionValidationException ex) {
                throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
                        "Validation of bean definition failed", ex);
            }
        }

        BeanDefinition oldBeanDefinition;

        oldBeanDefinition = this.beanDefinitionMap.get(beanName);
        if (oldBeanDefinition != null) {
            if (!isAllowBeanDefinitionOverriding()) {
                throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
                        "Cannot register bean definition [" + beanDefinition + "] for bean '" + beanName +
                        "': There is already [" + oldBeanDefinition + "] bound.");
            }
            else if (oldBeanDefinition.getRole() < beanDefinition.getRole()) {
                // e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE
                if (this.logger.isWarnEnabled()) {
                    this.logger.warn("Overriding user-defined bean definition for bean '" + beanName +
                            "' with a framework-generated bean definition: replacing [" +
                            oldBeanDefinition + "] with [" + beanDefinition + "]");
                }
            }
            else if (!beanDefinition.equals(oldBeanDefinition)) {
                if (this.logger.isInfoEnabled()) {
                    this.logger.info("Overriding bean definition for bean '" + beanName +
                            "' with a different definition: replacing [" + oldBeanDefinition +
                            "] with [" + beanDefinition + "]");
                }
            }
            else {
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Overriding bean definition for bean '" + beanName +
                            "' with an equivalent definition: replacing [" + oldBeanDefinition +
                            "] with [" + beanDefinition + "]");
                }
            }
            this.beanDefinitionMap.put(beanName, beanDefinition);
        }
        else {
            if (hasBeanCreationStarted()) {
                // Cannot modify startup-time collection elements anymore (for stable iteration)
                synchronized (this.beanDefinitionMap) {
                    this.beanDefinitionMap.put(beanName, beanDefinition);
                    List<String> updatedDefinitions = new ArrayList<String>(this.beanDefinitionNames.size() + 1);
                    updatedDefinitions.addAll(this.beanDefinitionNames);
                    updatedDefinitions.add(beanName);
                    this.beanDefinitionNames = updatedDefinitions;
                    if (this.manualSingletonNames.contains(beanName)) {
                        Set<String> updatedSingletons = new LinkedHashSet<String>(this.manualSingletonNames);
                        updatedSingletons.remove(beanName);
                        this.manualSingletonNames = updatedSingletons;
                    }
                }
            }
            else {
                // Still in startup registration phase
                this.beanDefinitionMap.put(beanName, beanDefinition);
                this.beanDefinitionNames.add(beanName);
                this.manualSingletonNames.remove(beanName);
            }
            this.frozenBeanDefinitionNames = null;
        }

        if (oldBeanDefinition != null || containsSingleton(beanName)) {
            resetBeanDefinition(beanName);
        }
    }

這段代碼還是比較容易理解的,首先先判斷容器里面有沒這個bean,沒有的話判斷是否在創建過程,如果不是直接將該bean注冊到容器里并設置其他信息。簡單的說,其實就是將一個個的bean的定義跟bean的名稱綁定起來,存放到map里面。至此,spring加載applicationContext.xml的大致流程已經說清楚了,不過這里面涉及很多比較細又難懂的類并沒有體現出來,最終要的是搞清楚spring加載配置文件的過程和注冊bean的過程。要想深入,可以繼續研讀源碼。

覺得不錯請點贊支持,歡迎留言或進我的個人群855801563領取【架構資料專題目合集90期】、【BATJTMD大廠JAVA面試真題1000+】,本群專用于學習交流技術、分享面試機會,拒絕廣告,我也會在群內不定期答題、探討。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

萨迦县| 华宁县| 怀集县| 耿马| 屏东县| 华阴市| 赤水市| 门源| 丰原市| 个旧市| 惠安县| 库车县| 马山县| 清徐县| 中牟县| 正镶白旗| 兴安县| 屏东市| 霍林郭勒市| 宁海县| 兰州市| 濮阳县| 威信县| 怀仁县| 佳木斯市| 建宁县| 土默特左旗| 内黄县| 大渡口区| 大英县| 龙山县| 蚌埠市| 禹州市| 和林格尔县| 甘肃省| 东安县| 甘南县| 星座| 自贡市| 东乌珠穆沁旗| 富锦市|