国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學(xué)院 > 開發(fā)設(shè)計 > 正文

Tomcat源碼分析——Session管理分析(上)

2019-11-14 15:12:25
字體:
供稿:網(wǎng)友

前言

  對于廣大java開發(fā)者而已,對于J2EE規(guī)范中的session應(yīng)該并不陌生,我們可以使用Session管理用戶的會話信息,最常見的就是拿Session用來存放用戶登錄、身份、權(quán)限及狀態(tài)等信息。對于使用Tomcat作為Web容器的大部分開發(fā)人員而言,Tomcat是如何實現(xiàn)Session標(biāo)記用戶和管理Session信息的呢?

概述

Session

  Tomcat內(nèi)部定義了Session和HttpSession這兩個會話相關(guān)的接口,其類繼承體系如圖1所示。

圖1  Session類繼承體系

圖1中額外列出了Session的類繼承體系,這里對他們逐個進(jìn)行介紹。

Session:Tomcat中有關(guān)會話的基本接口規(guī)范,圖1列出了它定義的主要方法,表1對這些方法進(jìn)行介紹。

表1  Session接口說明

方法描述
getCreationTime()/setCreationTime(time : long) 獲取與設(shè)置Session的創(chuàng)建時間
getId()/setId(id : String)  獲取與設(shè)置Session的ID
getThisaccessedTime()獲取最近一次請求的開始時間
getLastAccessedTime()獲取最近一次請求的完成時間
getManager()/setManager(manager : Manager) 獲取與設(shè)置Session管理器
getMaxInactiveInterval()/setMaxInactiveInterval(interval : int)獲取與設(shè)置Session的最大訪問間隔
getSession()獲取HttpSession
isValid()/setValid(isValid : boolean) 獲取與設(shè)置Session的有效狀態(tài)
access()/endAccess() 開始與結(jié)束Session的訪問
expire()設(shè)置Session過期

HttpSession:在HTTP客戶端與HTTP服務(wù)端提供的一種會話的接口規(guī)范,圖1列出了它定義的主要方法,表2對這些方法進(jìn)行介紹。

表2  HttpSession接口說明

方法描述
getCreationTime()獲取Session的創(chuàng)建時間
getId()獲取Session的ID
getLastAccessedTime()獲取最近一次請求的完成時間
getServletContext() 獲取當(dāng)前Session所屬的ServletContext
getMaxInactiveInterval()/setMaxInactiveInterval(interval : int)獲取與設(shè)置Session的最大訪問間隔
getAttribute(name : String) /setAttribute(name : String, value : Object)獲取與設(shè)置Session作用域的屬性
removeAttribute(name : String)清除Session作用域的屬性
invalidate()使Session失效并解除任何與此Session綁定的對象

ClusterSession:集群部署下的會話接口規(guī)范,圖1列出了它的主要方法,表3對這些方法進(jìn)行介紹。

表3  ClusterSession接口說明

方法描述
isPRimarySession()是否是集群的主Session
setPrimarySession(boolean primarySession)設(shè)置集群主Session

StandardSession:標(biāo)準(zhǔn)的HTTP Session實現(xiàn),本文將以此實現(xiàn)為例展開。

在部署Tomcat集群時,需要使集群中各個節(jié)點的會話狀態(tài)保持同步,目前Tomcat提供了兩種同步策略:

  • ReplicatedSession:每次都把整個會話對象同步給集群中的其他節(jié)點,其他節(jié)點然后更新整個會話對象。這種實現(xiàn)比較簡單方便,但會造成大量無效信息的傳輸。
  • DeltaSession:對會話中增量修改的屬性進(jìn)行同步。這種方式由于是增量的,所以會大大降低網(wǎng)絡(luò)I/O的開銷,但是實現(xiàn)上會比較復(fù)雜因為涉及到對會話屬性操作過程的管理。

Session管理器

  Tomcat內(nèi)部定義了Manager接口用于制定Session管理器的接口規(guī)范,目前已經(jīng)有很多Session管理器的實現(xiàn),如圖2所示。

圖2  Session管理器的類繼承體系

對應(yīng)圖2中的內(nèi)容我們下面逐個描述:

Manager:Tomcat對于Session管理器定義的接口規(guī)范,圖2已經(jīng)列出了Manager接口中定義的主要方法,表4詳細(xì)描述了這些方法的作用。

表4  Manager接口說明

方法描述
getContainer()/setContainer(container : Container) 獲取或設(shè)置Session管理器關(guān)聯(lián)的容器,一般為Context容器
getDistributable()/setDistributable(distributable : boolean)  獲取或設(shè)置Session管理器是否支持分布式
getMaxInactiveInterval()/setMaxInactiveInterval(interval : int)  獲取或設(shè)置Session管理器創(chuàng)建的Session的最大非活動時間間隔
getSessionIdLength()/setSessionIdLength(idLength : int) 獲取或設(shè)置Session管理器創(chuàng)建的Session ID的長度
getSessionCounter()/setSessionCounter(sessionCounter : long)  獲取或設(shè)置Session管理器創(chuàng)建的Session總數(shù)
getMaxActive()/setMaxActive(maxActive : int) 獲取或設(shè)置當(dāng)前已激活Session的最大數(shù)量
getActiveSessions()  獲取當(dāng)前激活的所有Session
getExpiredSessions()/setExpiredSessions(expiredSessions : long) 獲取或設(shè)置當(dāng)前已過期Session的數(shù)量
getRejectedSessions()/setRejectedSessions(rejectedSessions : int) 獲取或設(shè)置已拒絕創(chuàng)建Session的數(shù)量
getSessionMaxAliveTime()/setSessionMaxAliveTime(sessionMaxAliveTime : int)  獲取或設(shè)置已過期Session中的最大活動時長
getSessionAverageAliveTime()/setSessionAverageAliveTime(sessionAverageAliveTime : int) 獲取或設(shè)置已過期Session的平均活動時長
add(session : Session)/remove(session : Session) 給Session管理器增加或刪除活動Session
changeSessionId(session : Session) 給Session設(shè)置新生成的隨機(jī)Session ID
createSession(sessionId : String) 基于Session管理器的默認(rèn)屬性配置創(chuàng)建新的Session
findSession(id : String) 返回sessionId參數(shù)唯一標(biāo)記的Session
findSessions() 返回Session管理器管理的所有活動Session
load()/unload() 從持久化機(jī)制中加載Session或向持久化機(jī)制寫入Session
backgroundProcess() 容器接口中定義的為具體容器在后臺處理相關(guān)工作的實現(xiàn),Session管理器基于此機(jī)制實現(xiàn)了過期Session的銷毀

ManagerBase:封裝了Manager接口通用實現(xiàn)的抽象類,未提供對load()/unload()等方法的實現(xiàn),需要具體子類去實現(xiàn)。所有的Session管理器都繼承自ManagerBase。

ClusterManager:在Manager接口的基礎(chǔ)上增加了集群部署下的一些接口,所有實現(xiàn)集群下Session管理的管理器都需要實現(xiàn)此接口。

PersistentManagerBase:提供了對于Session持久化的基本實現(xiàn)。

PersistentManager:繼承自PersistentManagerBase,可以在Server.xml的<Context>元素下通過配置<Store>元素來使用。PersistentManager可以將內(nèi)存中的Session信息備份到文件或數(shù)據(jù)庫中。當(dāng)備份一個Session對象時,該Session對象會被復(fù)制到存儲器(文件或者數(shù)據(jù)庫)中,而原對象仍然留在內(nèi)存中。因此即便服務(wù)器宕機(jī),仍然可以從存儲器中獲取活動的Session對象。如果活動的Session對象超過了上限值或者Session對象閑置了的時間過長,那么Session會被換出到存儲器中以節(jié)省內(nèi)存空間。

 

StandardManager:不用配置<Store>元素,當(dāng)Tomcat正常關(guān)閉,重啟或Web應(yīng)用重新加載時,它會將內(nèi)存中的Session序列化到Tomcat目錄下的/work/Catalina/host_name/webapp_name/SESSIONS.ser文件中。當(dāng)Tomcat重啟或應(yīng)用加載完成后,Tomcat會將文件中的Session重新還原到內(nèi)存中。如果突然終止該服務(wù)器,則所有Session都將丟失,因為StandardManager沒有機(jī)會實現(xiàn)存盤處理。

ClusterManagerBase:提供了對于Session的集群管理實現(xiàn)。

DeltaManager:繼承自ClusterManagerBase。此Session管理器是Tomcat在集群部署下的默認(rèn)管理器,當(dāng)集群中的某一節(jié)點生成或修改Session后,DeltaManager將會把這些修改增量復(fù)制到其他節(jié)點。

BackupManager:沒有繼承ClusterManagerBase,而是直接實現(xiàn)了ClusterManager接口。是Tomcat在集群部署下的可選的Session管理器,集群中的所有Session都被全量復(fù)制到一個備份節(jié)點。集群中的所有節(jié)點都可以訪問此備份節(jié)點,達(dá)到Session在集群下的備份效果。

  為簡單起見,本文以StandardManager為例講解Session的管理。StandardManager是StandardContext的子組件,用來管理當(dāng)前Context的所有Session的創(chuàng)建和維護(hù)。如果你已經(jīng)閱讀或者熟悉了《Tomcat源碼分析——生命周期管理》一文的內(nèi)容,那么你就知道當(dāng)StandardContext正式啟動,也就是StandardContext的startInternal方法(見代碼清單1)被調(diào)用時,StandardContext還會啟動StandardManager。

代碼清單1

    @Override    protected synchronized void startInternal() throws LifecycleException {                       // 省略與Session管理無關(guān)的代碼                                       // Acquire clustered manager                Manager contextManager = null;                if (manager == null) {                    if ( (getCluster() != null) && distributable) {                        try {                            contextManager = getCluster().createManager(getName());                        } catch (Exception ex) {                            log.error("standardContext.clusterFail", ex);                            ok = false;                        }                    } else {                        contextManager = new StandardManager();                    }                }                                 // Configure default manager if none was specified                if (contextManager != null) {                    setManager(contextManager);                }                if (manager!=null && (getCluster() != null) && distributable) {                    //let the cluster know that there is a context that is distributable                    //and that it has its own manager                    getCluster().registerManager(manager);                }     // 省略與Session管理無關(guān)的代碼                        try {                // Start manager                if ((manager != null) && (manager instanceof Lifecycle)) {                    ((Lifecycle) getManager()).start();                }                    // Start ContainerBackgroundProcessor thread                super.threadStart();            } catch(Exception e) {                log.error("Error manager.start()", e);                ok = false;            }                 // 省略與Session管理無關(guān)的代碼    }

從代碼清單1可以看到StandardContext的startInternal方法中涉及Session管理的執(zhí)行步驟如下:

  1. 創(chuàng)建StandardManager;
  2. 如果Tomcat結(jié)合Apache做了分布式部署,會將當(dāng)前StandardManager注冊到集群中;
  3. 啟動StandardManager;

StandardManager的start方法用于啟動StandardManager,實現(xiàn)見代碼清單2。

代碼清單2

    @Override    public synchronized final void start() throws LifecycleException {                //省略狀態(tài)校驗的代碼if (state.equals(LifecycleState.NEW)) {            init();        } else if (!state.equals(LifecycleState.INITIALIZED) &&                !state.equals(LifecycleState.STOPPED)) {            invalidTransition(Lifecycle.BEFORE_START_EVENT);        }        setState(LifecycleState.STARTING_PREP);        try {            startInternal();        } catch (LifecycleException e) {            setState(LifecycleState.FAILED);            throw e;        }        if (state.equals(LifecycleState.FAILED) ||                state.equals(LifecycleState.MUST_STOP)) {            stop();        } else {            // Shouldn't be necessary but acts as a check that sub-classes are            // doing what they are supposed to.            if (!state.equals(LifecycleState.STARTING)) {                invalidTransition(Lifecycle.AFTER_START_EVENT);            }                        setState(LifecycleState.STARTED);        }    }

從代碼清單2可以看出啟動StandardManager的步驟如下:

  1. 調(diào)用init方法初始化StandardManager;
  2. 調(diào)用startInternal方法啟動StandardManager;

StandardManager的初始化

   經(jīng)過上面的分析,我們知道啟動StandardManager的第一步就是調(diào)用父類LifecycleBase的init方法,關(guān)于此方法已在《Tomcat源碼分析——生命周期管理》一文詳細(xì)介紹,所以我們只需要關(guān)心StandardManager的initInternal。StandardManager本身并沒有實現(xiàn)initInternal方法,但是StandardManager的父類ManagerBase實現(xiàn)了此方法,其實現(xiàn)見代碼清單3。

代碼清單3

    @Override    protected void initInternal() throws LifecycleException {                super.initInternal();                setDistributable(((Context) getContainer()).getDistributable());        // Initialize random number generation        getRandomBytes(new byte[16]);    }

閱讀代碼清單3,我們總結(jié)下ManagerBase的initInternal方法的執(zhí)行步驟:

  1. 將容器自身即StandardManager注冊到JMX(LifecycleMBeanBase的initInternal方法的實現(xiàn)請參考《Tomcat源碼分析——生命周期管理》一文);
  2. 從父容器StandardContext中獲取當(dāng)前Tomcat是否是集群部署,并設(shè)置為ManagerBase的布爾屬性distributable;
  3. 調(diào)用getRandomBytes方法從隨機(jī)數(shù)文件/dev/urandom中獲取隨機(jī)數(shù)字節(jié)數(shù)組,如果不存在此文件則通過反射生成java.security.SecureRandom的實例,用它生成隨機(jī)數(shù)字節(jié)數(shù)組。

注意:此處調(diào)用getRandomBytes方法生成的隨機(jī)數(shù)字節(jié)數(shù)組并不會被使用,之所以在這里調(diào)用實際是為了完成對隨機(jī)數(shù)生成器的初始化,以便將來分配Session ID時使用。

我們詳細(xì)閱讀下getRandomBytes方法的代碼實現(xiàn),見代碼清單4。

代碼清單4

    protected void getRandomBytes(byte bytes[]) {        // Generate a byte array containing a session identifier        if (devRandomSource != null && randomIS == null) {            setRandomFile(devRandomSource);        }        if (randomIS != null) {            try {                int len = randomIS.read(bytes);                if (len == bytes.length) {                    return;                }                if(log.isDebugEnabled())                    log.debug("Got " + len + " " + bytes.length );            } catch (Exception ex) {                // Ignore            }            devRandomSource = null;                        try {                randomIS.close();            } catch (Exception e) {                log.warn("Failed to close randomIS.");            }                        randomIS = null;        }        getRandom().nextBytes(bytes);    }

代碼清單4中的setRandomFile方法(見代碼清單5)用于從隨機(jī)數(shù)文件/dev/urandom中獲取隨機(jī)數(shù)字節(jié)數(shù)組。

代碼清單5

    public void setRandomFile( String s ) {        // as a hack, you can use a static file - and generate the same        // session ids ( good for strange debugging )        if (Globals.IS_SECURITY_ENABLED){            randomIS = AccessController.doPrivileged(new PrivilegedSetRandomFile(s));        } else {            try{                devRandomSource=s;                File f=new File( devRandomSource );                if( ! f.exists() ) return;                randomIS= new DataInputStream( new FileInputStream(f));                randomIS.readLong();                if( log.isDebugEnabled() )                    log.debug( "Opening " + devRandomSource );            } catch( IOException ex ) {                log.warn("Error reading " + devRandomSource, ex);                if (randomIS != null) {                    try {                        randomIS.close();                    } catch (Exception e) {                        log.warn("Failed to close randomIS.");                    }                }                devRandomSource = null;                randomIS=null;            }        }    }

代碼清單4中的getRandom方法(見代碼清單6)通過反射生成java.security.SecureRandom的實例,并用此實例生成隨機(jī)數(shù)字節(jié)數(shù)組。

代碼清單6

    public Random getRandom() {        if (this.random == null) {            // Calculate the new random number generator seed            long seed = System.currentTimeMillis();            long t1 = seed;            char entropy[] = getEntropy().toCharArray();            for (int i = 0; i < entropy.length; i++) {                long update = ((byte) entropy[i]) << ((i % 8) * 8);                seed ^= update;            }            try {                // Construct and seed a new random number generator                Class<?> clazz = Class.forName(randomClass);                this.random = (Random) clazz.newInstance();                this.random.setSeed(seed);            } catch (Exception e) {                // Fall back to the simple case                log.error(sm.getString("managerBase.random", randomClass),                        e);                this.random = new java.util.Random();                this.random.setSeed(seed);            }            if(log.isDebugEnabled()) {                long t2=System.currentTimeMillis();                if( (t2-t1) > 100 )                    log.debug(sm.getString("managerBase.seeding", randomClass) + " " + (t2-t1));            }        }                return (this.random);    }

根據(jù)以上的分析,StandardManager的初始化主要就是執(zhí)行了ManagerBase的initInternal方法。

StandardManager的啟動

  調(diào)用StandardManager的startInternal方法用于啟動StandardManager,見代碼清單7。

 代碼清單7

    @Override    protected synchronized void startInternal() throws LifecycleException {        // Force initialization of the random number generator        if (log.isDebugEnabled())            log.debug("Force random number initialization starting");        generateSessionId();        if (log.isDebugEnabled())            log.debug("Force random number initialization completed");        // Load unloaded sessions, if any        try {            load();        } catch (Throwable t) {            log.error(sm.getString("standardManager.managerLoad"), t);        }        setState(LifecycleState.STARTING);    }

 從代碼清單7可以看出啟動StandardManager的步驟如下:

步驟一 調(diào)用generateSessionId方法(見代碼清單8)強(qiáng)制初始化隨機(jī)數(shù)生成器;

注意:此處調(diào)用generateSessionId方法的目的不是為了生成Session ID,而是為了強(qiáng)制初始化隨機(jī)數(shù)生成器。

代碼清單8

    protected synchronized String generateSessionId() {        byte random[] = new byte[16];        String jvmRoute = getJvmRoute();        String result = null;        // Render the result as a String of hexadecimal digits        StringBuilder buffer = new StringBuilder();        do {            int resultLenBytes = 0;            if (result != null) {                buffer = new StringBuilder();                duplicates++;            }            while (resultLenBytes < this.sessionIdLength) {                getRandomBytes(random);                random = getDigest().digest(random);                for (int j = 0;                j < random.length && resultLenBytes < this.sessionIdLength;                j++) {                    byte b1 = (byte) ((random[j] & 0xf0) >> 4);                    byte b2 = (byte) (random[j] & 0x0f);                    if (b1 < 10)                        buffer.append((char) ('0' + b1));                    else                        buffer.append((char) ('A' + (b1 - 10)));                    if (b2 < 10)                        buffer.append((char) ('0' + b2));                    else                        buffer.append((char) ('A' + (b2 - 10)));                    resultLenBytes++;                }            }            if (jvmRoute != null) {                buffer.append('.').append(jvmRoute);            }            result = buffer.toString();        } while (sessions.containsKey(result));        return (result);    }

步驟二  加載持久化的Session信息。為什么Session需要持久化?由于在StandardManager中,所有的Session都維護(hù)在一個ConcurrentHashMap中,因此服務(wù)器重啟或者宕機(jī)會造成這些Session信息丟失或失效,為了解決這個問題,Tomcat將這些Session通過持久化的方式來保證不會丟失。下面我們來看看StandardManager的load方法的實現(xiàn),見代碼清單9所示。

代碼清單9

    public void load() throws ClassNotFoundException, IOException {        if (SecurityUtil.isPackageProtectionEnabled()){            try{                AccessController.doPrivileged( new PrivilegedDoLoad() );            } catch (PrivilegedActionException ex){                Exception exception = ex.getException();                if (exception instanceof ClassNotFoundException){                    throw (ClassNotFoundException)exception;                } else if (exception instanceof IOException){                    throw (IOException)exception;                }                if (log.isDebugEnabled())                    log.debug("Unreported exception in load() "                        + exception);            }        } else {            doLoad();        }    }

如果需要安全機(jī)制是打開的并且包保護(hù)模式打開,會通過創(chuàng)建PrivilegedDoLoad來加載持久化的Session,其實現(xiàn)如代碼清單10所示。

代碼清單10

    private class PrivilegedDoLoad        implements PrivilegedExceptionAction<Void> {        PrivilegedDoLoad() {            // NOOP        }        public Void run() throws Exception{           doLoad();           return null;        }    }

從代碼清單10看到實際負(fù)責(zé)加載的方法是doLoad,根據(jù)代碼清單9知道默認(rèn)情況下,加載Session信息的方法也是doLoad。所以我們只需要看看doLoad的實現(xiàn)了,見代碼清單11。

代碼清單11

    protected void doLoad() throws ClassNotFoundException, IOException {        if (log.isDebugEnabled())            log.debug("Start: Loading persisted sessions");        // Initialize our internal data structures        sessions.clear();        // Open an input stream to the specified pathname, if any        File file = file();        if (file == null)            return;        if (log.isDebugEnabled())            log.debug(sm.getString("standardManager.loading", pathname));        FileInputStream fis = null;        BufferedInputStream bis = null;        ObjectInputStream ois = null;        Loader loader = null;        ClassLoader classLoader = null;        try {            fis = new FileInputStream(file.getAbsolutePath());            bis = new BufferedInputStream(fis);            if (container != null)                loader = container.getLoader();            if (loader != null)                classLoader = loader.getClassLoader();            if (classLoader != null) {                if (log.isDebugEnabled())                    log.debug("Creating custom object input stream for class loader ");                ois = new CustomObjectInputStream(bis, classLoader);            } else {                if (log.isDebugEnabled())                    log.debug("Creating standard object input stream");                ois = new ObjectInputStream(bis);            }        } catch (FileNotFoundException e) {            if (log.isDebugEnabled())                log.debug("No persisted data file found");            return;        } catch (IOException e) {            log.error(sm.getString("standardManager.loading.ioe", e), e);            if (fis != null) {                try {                    fis.close();                } catch (IOException f) {                    // Ignore                }            }            if (bis != null) {                try {                    bis.close();                } catch (IOException f) {                    // Ignore                }            }            throw e;        }        // Load the previously unloaded active sessions        synchronized (sessions) {            try {                Integer count = (Integer) ois.readObject();                int n = count.intValue();                if (log.isDebugEnabled())                    log.debug("Loading " + n + " persisted sessions");                for (int i = 0; i < n; i++) {                    StandardSession session = getNewSession();                    session.readObjectData(ois);                    session.setManager(this);                    sessions.put(session.getIdInternal(), session);                    session.activate();                    if (!session.isValidInternal()) {                        // If session is already invalid,                        // expire session to prevent memory leak.                        session.setValid(true);                        session.expire();                    }                    sessionCounter++;                }            } catch (ClassNotFoundException e) {                log.error(sm.getString("standardManager.loading.cnfe", e), e);                try {                    ois.close();                } catch (IOException f) {                    // Ignore                }                throw e;            } catch (IOException e) {                log.error(sm.getString("standardManager.loading.ioe", e), e);                try {                    ois.close();                } catch (IOException f) {                    // Ignore                }                throw e;            } finally {                // Close the input stream                try {                    ois.close();                } catch (IOException f) {                    // ignored                }                // Delete the persistent storage file                if (file.exists() )                    file.delete();            }        }        if (log.isDebugEnabled())            log.debug("Finish: Loading persisted sessions");    }

 從代碼清單11看到StandardManager的doLoad方法的執(zhí)行步驟如下:

  1. 清空sessions緩存維護(hù)的Session信息;
  2. 調(diào)用file方法返回當(dāng)前Context下的Session持久化文件,比如:D:/workspace/Tomcat7.0/work/Catalina/localhost/host-manager/SESSIONS.ser;
  3. 打開Session持久化文件的輸入流,并封裝為CustomObjectInputStream;
  4. 從Session持久化文件讀入持久化的Session的數(shù)量,然后逐個讀取Session信息并放入sessions緩存中。

至此,有關(guān)StandardManager的啟動就介紹到這里,我將會在《TOMCAT源碼分析——SESSION管理分析(下)》一文講解Session的分配、追蹤、銷毀等內(nèi)容。

如需轉(zhuǎn)載,請標(biāo)明本文作者及出處——作者:jiaan.gja,本文原創(chuàng)首發(fā):博客園,原文鏈接:http://m.survivalescaperooms.com/jiaan-geng/p/4913616.html 

上一篇:容器

下一篇:spring事務(wù)管理

發(fā)表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發(fā)表
主站蜘蛛池模板: 运城市| 黄骅市| 南召县| 东乡县| 赫章县| 巴彦淖尔市| 景德镇市| 六盘水市| 莒南县| 万宁市| 涞源县| 镇坪县| 武穴市| 郎溪县| 日土县| 金华市| 长丰县| 固阳县| 石楼县| 精河县| 万载县| 八宿县| 芒康县| 枣庄市| 仁化县| 临邑县| 庐江县| 彝良县| 莱芜市| 晴隆县| 南投市| 乐山市| 永嘉县| 三台县| 榆社县| 藁城市| 通州区| 三原县| 郯城县| 印江| 永寿县|