quartz (一) 基于 Quartz 开发企业级任务调度应用
本文轉(zhuǎn)自:http://www.ibm.com/developerworks/cn/opensource/os-cn-quartz/
Quartz 基本概念及原理
Quartz Scheduler 開(kāi)源框架
Quartz 是 OpenSymphony 開(kāi)源組織在任務(wù)調(diào)度領(lǐng)域的一個(gè)開(kāi)源項(xiàng)目,完全基于 Java 實(shí)現(xiàn)。該項(xiàng)目于 2009 年被 Terracotta 收購(gòu),目前是 Terracotta 旗下的一個(gè)項(xiàng)目。讀者可以到?http://www.quartz-scheduler.org/站點(diǎn)下載 Quartz 的發(fā)布版本及其源代碼。筆者在產(chǎn)品開(kāi)發(fā)中使用的是版本 1.8.4,因此本文內(nèi)容基于該版本。本文不僅介紹如何應(yīng)用 Quartz 進(jìn)行開(kāi)發(fā),也對(duì)其內(nèi)部實(shí)現(xiàn)原理作一定講解。
作為一個(gè)優(yōu)秀的開(kāi)源調(diào)度框架,Quartz 具有以下特點(diǎn):
另外,作為 Spring 默認(rèn)的調(diào)度框架,Quartz 很容易與 Spring 集成實(shí)現(xiàn)靈活可配置的調(diào)度功能。
下面是本文中用到的一些專用詞匯,在此聲明:
scheduler:Quartz 任務(wù)調(diào)度的基本實(shí)現(xiàn)原理
核心元素
Quartz 任務(wù)調(diào)度的核心元素是 scheduler, trigger 和 job,其中 trigger 和 job 是任務(wù)調(diào)度的元數(shù)據(jù), scheduler 是實(shí)際執(zhí)行調(diào)度的控制器。
在 Quartz 中,trigger 是用于定義調(diào)度時(shí)間的元素,即按照什么時(shí)間規(guī)則去執(zhí)行任務(wù)。Quartz 中主要提供了四種類型的 trigger:SimpleTrigger,CronTirgger,DateIntervalTrigger,和 NthIncludedDayTrigger。這四種 trigger 可以滿足企業(yè)應(yīng)用中的絕大部分需求。我們將在企業(yè)應(yīng)用一節(jié)中進(jìn)一步討論四種 trigger 的功能。
在 Quartz 中,job 用于表示被調(diào)度的任務(wù)。主要有兩種類型的 job:無(wú)狀態(tài)的(stateless)和有狀態(tài)的(stateful)。對(duì)于同一個(gè) trigger 來(lái)說(shuō),有狀態(tài)的 job 不能被并行執(zhí)行,只有上一次觸發(fā)的任務(wù)被執(zhí)行完之后,才能觸發(fā)下一次執(zhí)行。Job 主要有兩種屬性:volatility 和 durability,其中 volatility 表示任務(wù)是否被持久化到數(shù)據(jù)庫(kù)存儲(chǔ),而 durability 表示在沒(méi)有 trigger 關(guān)聯(lián)的時(shí)候任務(wù)是否被保留。兩者都是在值為 true 的時(shí)候任務(wù)被持久化或保留。一個(gè) job 可以被多個(gè) trigger 關(guān)聯(lián),但是一個(gè) trigger 只能關(guān)聯(lián)一個(gè) job。
在 Quartz 中, scheduler 由 scheduler 工廠創(chuàng)建:DirectSchedulerFactory 或者 StdSchedulerFactory。 第二種工廠 StdSchedulerFactory 使用較多,因?yàn)?DirectSchedulerFactory 使用起來(lái)不夠方便,需要作許多詳細(xì)的手工編碼設(shè)置。 Scheduler 主要有三種:RemoteMBeanScheduler, RemoteScheduler 和 StdScheduler。本文以最常用的 StdScheduler 為例講解。這也是筆者在項(xiàng)目中所使用的 scheduler 類。
Quartz 核心元素之間的關(guān)系如下圖所示:
圖 1. Quartz 核心元素關(guān)系圖
線程視圖
在 Quartz 中,有兩類線程,Scheduler 調(diào)度線程和任務(wù)執(zhí)行線程,其中任務(wù)執(zhí)行線程通常使用一個(gè)線程池維護(hù)一組線程。
圖 2. Quartz 線程視圖
Scheduler 調(diào)度線程主要有兩個(gè): 執(zhí)行常規(guī)調(diào)度的線程,和執(zhí)行 misfired trigger 的線程。常規(guī)調(diào)度線程輪詢存儲(chǔ)的所有 trigger,如果有需要觸發(fā)的 trigger,即到達(dá)了下一次觸發(fā)的時(shí)間,則從任務(wù)執(zhí)行線程池獲取一個(gè)空閑線程,執(zhí)行與該 trigger 關(guān)聯(lián)的任務(wù)。Misfire 線程是掃描所有的 trigger,查看是否有 misfired trigger,如果有的話根據(jù) misfire 的策略分別處理。下圖描述了這兩個(gè)線程的基本流程:
圖 3. Quartz 調(diào)度線程流程圖
關(guān)于 misfired trigger,我們?cè)谄髽I(yè)應(yīng)用一節(jié)中將進(jìn)一步描述。
數(shù)據(jù)存儲(chǔ)
Quartz 中的 trigger 和 job 需要存儲(chǔ)下來(lái)才能被使用。Quartz 中有兩種存儲(chǔ)方式:RAMJobStore, JobStoreSupport,其中 RAMJobStore 是將 trigger 和 job 存儲(chǔ)在內(nèi)存中,而 JobStoreSupport 是基于 jdbc 將 trigger 和 job 存儲(chǔ)到數(shù)據(jù)庫(kù)中。RAMJobStore 的存取速度非常快,但是由于其在系統(tǒng)被停止后所有的數(shù)據(jù)都會(huì)丟失,所以在通常應(yīng)用中,都是使用 JobStoreSupport。
在 Quartz 中,JobStoreSupport 使用一個(gè)驅(qū)動(dòng)代理來(lái)操作 trigger 和 job 的數(shù)據(jù)存儲(chǔ):StdJDBCDelegate。StdJDBCDelegate 實(shí)現(xiàn)了大部分基于標(biāo)準(zhǔn) JDBC 的功能接口,但是對(duì)于各種數(shù)據(jù)庫(kù)來(lái)說(shuō),需要根據(jù)其具體實(shí)現(xiàn)的特點(diǎn)做某些特殊處理,因此各種數(shù)據(jù)庫(kù)需要擴(kuò)展 StdJDBCDelegate 以實(shí)現(xiàn)這些特殊處理。Quartz 已經(jīng)自帶了一些數(shù)據(jù)庫(kù)的擴(kuò)展實(shí)現(xiàn),可以直接使用,如下圖所示:
圖 4. Quartz 數(shù)據(jù)庫(kù)驅(qū)動(dòng)代理
作為嵌入式數(shù)據(jù)庫(kù)的代表,Derby 近來(lái)非常流行。如果使用 Derby 數(shù)據(jù)庫(kù),可以使用上圖中的 CloudscapeDelegate 作為 trigger 和 job 數(shù)據(jù)存儲(chǔ)的代理類。
回頁(yè)首
基本開(kāi)發(fā)流程及簡(jiǎn)單實(shí)例
搭建開(kāi)發(fā)環(huán)境
利用 Quartz 進(jìn)行開(kāi)發(fā)相當(dāng)簡(jiǎn)單,只需要將下載開(kāi)發(fā)包中的 quartz-all-1.8.4.jar 加入到 classpath 即可。根據(jù)筆者的經(jīng)驗(yàn),對(duì)于任務(wù)調(diào)度功能比較復(fù)雜的企業(yè)級(jí)應(yīng)用來(lái)說(shuō),最好在開(kāi)發(fā)階段將 Quartz 的源代碼導(dǎo)入到開(kāi)發(fā)環(huán)境中來(lái)。一方面可以通過(guò)閱讀源碼了解 Quartz 的實(shí)現(xiàn)機(jī)理,另一方面可以通過(guò)擴(kuò)展或修改 Quartz 的一些類來(lái)實(shí)現(xiàn)某些 Quartz 尚不提供的功能。
圖 5. Quartz 實(shí)例工程及源碼導(dǎo)入
上圖中左邊是源碼導(dǎo)入后的截圖,其中 org.quartz.* 即為 quartz 的源碼。導(dǎo)入源碼后可能會(huì)有一些編譯錯(cuò)誤,通常出現(xiàn)在 org.quartz.ee.* 和 org.quartz.jobs.ee.* 包中。下載開(kāi)發(fā)包中有一個(gè) lib 目錄,讀者可以將該目錄下的 jar 文件加入到編譯環(huán)境。如果還有編譯錯(cuò)誤,讀者可以參考上圖中右側(cè)的 jar 列表,到網(wǎng)上去搜索下載。
項(xiàng)目中 com.ibm.zxn.sample.quartz 是我們自己的類包,下面的實(shí)例中我們會(huì)用到它。
一個(gè)簡(jiǎn)單實(shí)例
Quartz 開(kāi)發(fā)包中有一個(gè) examples 目錄,其中有 15 個(gè)基本實(shí)例。建議讀者閱讀并實(shí)踐這些例子。本文這里只列舉一個(gè)小的實(shí)例,介紹基本的開(kāi)發(fā)方法。
準(zhǔn)備數(shù)據(jù)庫(kù)和 Quartz 用的數(shù)據(jù)表
圖 6. Quartz 數(shù)據(jù)表
準(zhǔn)備配置文件,加入到項(xiàng)目中
圖 7. 實(shí)例配置文件
通過(guò)實(shí)現(xiàn) job 接口定義我們自己的任務(wù)類,如下所示:
圖 8. 定義任務(wù)類
然后,實(shí)現(xiàn)任務(wù)調(diào)度的主程序,如下所示:
本實(shí)例中,我們利用 DateIntervalTrigger 實(shí)現(xiàn)一個(gè)每?jī)煞昼妶?zhí)行一次的任務(wù)調(diào)度。
圖 9. 實(shí)現(xiàn)主程序
完成后項(xiàng)目結(jié)構(gòu)如下所示:
圖 10. 實(shí)例項(xiàng)目結(jié)構(gòu)圖
運(yùn)行程序,查看數(shù)據(jù)庫(kù)表和運(yùn)行結(jié)果
數(shù)據(jù)庫(kù)中,QRTZ_TRIGGERS 表中添加了一條 trigger 記錄,如下所示:
圖 11. QRTZ_TRIGGERS 表中的記錄
QRTZ_JOB_DETAILS 表中添加了一條 job 記錄,如下所示:
圖 12. QRTZ_JOB_DETAILES 表中的記錄
從運(yùn)行結(jié)果來(lái)看,任務(wù)每?jī)煞昼姳粓?zhí)行一次:
圖 13. 運(yùn)行結(jié)果
回頁(yè)首
企業(yè)級(jí)開(kāi)發(fā)中的常見(jiàn)應(yīng)用
在應(yīng)用 Quartz 進(jìn)行企業(yè)級(jí)的開(kāi)發(fā)時(shí),有一些問(wèn)題會(huì)經(jīng)常遇到。本節(jié)筆者根據(jù)自己在項(xiàng)目開(kāi)發(fā)中的經(jīng)驗(yàn),介紹企業(yè)開(kāi)發(fā)中常見(jiàn)的一些問(wèn)題以及通常的解決辦法。
應(yīng)用一:如何使用不同類型的 Trigger
前面我們提到 Quartz 中四種類型的 Trigger:SimpleTrigger,CronTirgger,DateIntervalTrigger, 和 NthIncludedDayTrigger。
SimpleTrigger 一般用于實(shí)現(xiàn)每隔一定時(shí)間執(zhí)行任務(wù),以及重復(fù)多少次,如每 2 小時(shí)執(zhí)行一次,重復(fù)執(zhí)行 5 次。SimpleTrigger 內(nèi)部實(shí)現(xiàn)機(jī)制是通過(guò)計(jì)算間隔時(shí)間來(lái)計(jì)算下次的執(zhí)行時(shí)間,這就導(dǎo)致其不適合調(diào)度定時(shí)的任務(wù)。例如我們想每天的 1:00AM 執(zhí)行任務(wù),如果使用 SimpleTrigger 的話間隔時(shí)間就是一天。注意這里就會(huì)有一個(gè)問(wèn)題,即當(dāng)有 misfired 的任務(wù)并且恢復(fù)執(zhí)行時(shí),該執(zhí)行時(shí)間是隨機(jī)的(取決于何時(shí)執(zhí)行 misfired 的任務(wù),例如某天的 3:00PM)。這會(huì)導(dǎo)致之后每天的執(zhí)行時(shí)間都會(huì)變成 3:00PM,而不是我們?cè)瓉?lái)期望的 1:00AM。
CronTirgger 類似于 LINUX 上的任務(wù)調(diào)度命令 crontab,即利用一個(gè)包含 7 個(gè)字段的表達(dá)式來(lái)表示時(shí)間調(diào)度方式。例如,"0 15 10 * * ? *" 表示每天的 10:15AM 執(zhí)行任務(wù)。對(duì)于涉及到星期和月份的調(diào)度,CronTirgger 是最適合的,甚至某些情況下是唯一選擇。例如,"0 10 14 ? 3 WED" 表示三月份的每個(gè)星期三的下午 14:10PM 執(zhí)行任務(wù)。讀者可以在具體用到該 trigger 時(shí)再詳細(xì)了解每個(gè)字段的含義。
DateIntervalTrigger 是 Quartz 1.7 之后的版本加入的,其最適合調(diào)度類似每 N(1, 2, 3...)小時(shí),每 N 天,每 N 周等的任務(wù)。雖然 SimpleTrigger 也能實(shí)現(xiàn)類似的任務(wù),但是 DateIntervalTrigger 不會(huì)受到我們上面說(shuō)到的 misfired 任務(wù)的影響。另外,DateIntervalTrigger 也不會(huì)受到 DST(Daylight Saving Time, 即中國(guó)的夏令時(shí))調(diào)整的影響。筆者就曾經(jīng)因?yàn)樵撛驅(qū)㈨?xiàng)目中的 SimpleTrigger 改為了 DateIntervalTrigger,因?yàn)槿绻褂?SimpleTrigger,本來(lái)設(shè)定的調(diào)度時(shí)間就會(huì)由于 DST 的調(diào)整而提前或延遲一個(gè)小時(shí),而 DateIntervalTrigger 不會(huì)受此影響。
NthIncludedDayTrigger 的用途比較簡(jiǎn)單明確,即用于每隔一個(gè)周期的第幾天調(diào)度任務(wù),例如,每個(gè)月的第 3 天執(zhí)行指定的任務(wù)。
除了上面提到的 4 種 Trigger,Quartz 中還定義了一個(gè) Calendar 類(注意,是 org.quartz.Calendar)。這個(gè) Calendar 與 Trigger 一起使用,但是它們的作用相反,它是用于排除任務(wù)不被執(zhí)行的情況。例如,按照 Trigger 的規(guī)則在 10 月 1 號(hào)需要執(zhí)行任務(wù),但是 Calendar 指定了 10 月 1 號(hào)是節(jié)日(國(guó)慶),所以任務(wù)在這一天將不會(huì)被執(zhí)行。通常來(lái)說(shuō),Calendar 用于排除節(jié)假日的任務(wù)調(diào)度,從而使任務(wù)只在工作日?qǐng)?zhí)行。
應(yīng)用二:使用有狀態(tài)(StatefulJob)還是無(wú)狀態(tài)的任務(wù)(Job)
在 Quartz 中,Job 是一個(gè)接口,企業(yè)應(yīng)用需要實(shí)現(xiàn)這個(gè)接口以定義自己的任務(wù)。基本來(lái)說(shuō),任務(wù)分為有狀態(tài)和無(wú)狀態(tài)兩種。實(shí)現(xiàn) Job 接口的任務(wù)缺省為無(wú)狀態(tài)的。Quartz 中還有另外一個(gè)接口 StatefulJob。實(shí)現(xiàn) StatefulJob 接口的任務(wù)為有狀態(tài)的,上一節(jié)的簡(jiǎn)單實(shí)例中,我們定義的 SampleJob 就是實(shí)現(xiàn)了 StatefulJob 接口的有狀態(tài)任務(wù)。下圖列出了 Quartz 中 Job 接口的定義以及一些自帶的實(shí)現(xiàn)類:
圖 14. Quartz 中 Job 接口定義
無(wú)狀態(tài)任務(wù)一般指可以并發(fā)的任務(wù),即任務(wù)之間是獨(dú)立的,不會(huì)互相干擾。例如我們定義一個(gè) trigger,每 2 分鐘執(zhí)行一次,但是某些情況下一個(gè)任務(wù)可能需要 3 分鐘才能執(zhí)行完,這樣,在上一個(gè)任務(wù)還處在執(zhí)行狀態(tài)時(shí),下一次觸發(fā)時(shí)間已經(jīng)到了。對(duì)于無(wú)狀態(tài)任務(wù),只要觸發(fā)時(shí)間到了就會(huì)被執(zhí)行,因?yàn)閹讉€(gè)相同任務(wù)可以并發(fā)執(zhí)行。但是對(duì)有狀態(tài)任務(wù)來(lái)說(shuō),是不能并發(fā)執(zhí)行的,同一時(shí)間只能有一個(gè)任務(wù)在執(zhí)行。
在筆者項(xiàng)目中,某些任務(wù)需要對(duì)數(shù)據(jù)庫(kù)中的數(shù)據(jù)進(jìn)行增刪改處理。這些任務(wù)不能并發(fā)執(zhí)行,否則會(huì)造成數(shù)據(jù)混亂。因此我們使用 StatefulJob 接口。現(xiàn)在回到上面的例子,任務(wù)每 2 分鐘執(zhí)行一次,若某次任務(wù)執(zhí)行了 5 分鐘才完成,Quartz 會(huì)怎么處理呢?按照 trigger 的規(guī)則,第 2 分鐘和第 4 分鐘分別會(huì)有一次預(yù)定的觸發(fā)執(zhí)行,但是由于是有狀態(tài)任務(wù),因此實(shí)際不會(huì)被觸發(fā)。在第 5 分鐘第一次任務(wù)執(zhí)行完畢時(shí),Quartz 會(huì)把第 2 和第 4 分鐘的兩次觸發(fā)作為 misfired job 進(jìn)行處理。對(duì)于 misfired job,Quartz 會(huì)查看其 misfire 策略是如何設(shè)定的,如果是立刻執(zhí)行,則會(huì)馬上啟動(dòng)一次執(zhí)行,如果是等待下次執(zhí)行,則會(huì)忽略錯(cuò)過(guò)的任務(wù),而等待下次(即第 6 分鐘)觸發(fā)執(zhí)行。
讀者可以在自己的項(xiàng)目中體會(huì)兩種任務(wù)的區(qū)別以及 Quartz 的處理方法,根據(jù)具體情況選擇不同類型的任務(wù)。
應(yīng)用三:如何設(shè)置 Quartz 的線程池和并發(fā)任務(wù)
Quartz 中自帶了一個(gè)線程池的實(shí)現(xiàn):SimpleThreadPool。類如其名,這只是線程池的一個(gè)簡(jiǎn)單實(shí)現(xiàn),沒(méi)有提供動(dòng)態(tài)自發(fā)調(diào)整等高級(jí)特性。Quartz 提供了一個(gè)配置參數(shù):org.quartz.threadPool.threadCount,可以在初始化時(shí)設(shè)定線程池的線程數(shù)量,但是一次設(shè)定后不能再修改。假定這個(gè)數(shù)目是 10,則在并發(fā)任務(wù)達(dá)到 10 個(gè)以后,再有觸發(fā)的任務(wù)就無(wú)法被執(zhí)行了,只能等待有空閑線程的時(shí)候才能得到執(zhí)行。因此有些 trigger 就可能被 misfire。但是必須指出一點(diǎn),這個(gè)初始線程數(shù)并不是越大越好。當(dāng)并發(fā)線程太多時(shí),系統(tǒng)整體性能反而會(huì)下降,因?yàn)橄到y(tǒng)把很多時(shí)間花在了線程調(diào)度上。根據(jù)一般經(jīng)驗(yàn),這個(gè)值在 10 -- 50 比較合適。
對(duì)于一些注重性能的線程池來(lái)說(shuō),會(huì)根據(jù)實(shí)際線程使用情況進(jìn)行動(dòng)態(tài)調(diào)整,例如初始線程數(shù),最大線程數(shù),空閑線程數(shù)等。讀者在應(yīng)用中,如果有更好的線程池,則可以在配置文件中通過(guò)下面參數(shù)替換 SimpleThreadPool:org.quartz.threadPool.class = myapp.GreatThreadPool。
應(yīng)用四:如何處理 Misfired 任務(wù)
在 Quartz 應(yīng)用中,misfired job 是經(jīng)常遇到的情況。一般來(lái)說(shuō),下面這些原因可能造成 misfired job:
1)系統(tǒng)因?yàn)槟承┰虮恢貑ⅰT谙到y(tǒng)關(guān)閉到重新啟動(dòng)之間的一段時(shí)間里,可能有些任務(wù)會(huì)
被 misfire;
2)Trigger 被暫停(suspend)的一段時(shí)間里,有些任務(wù)可能會(huì)被 misfire;
3)線程池中所有線程都被占用,導(dǎo)致任務(wù)無(wú)法被觸發(fā)執(zhí)行,造成 misfire;
4)有狀態(tài)任務(wù)在下次觸發(fā)時(shí)間到達(dá)時(shí),上次執(zhí)行還沒(méi)有結(jié)束;
為了處理 misfired job,Quartz 中為 trigger 定義了處理策略,主要有下面兩種:
MISFIRE_INSTRUCTION_FIRE_ONCE_NOW:針對(duì) misfired job 馬上執(zhí)行一次;
MISFIRE_INSTRUCTION_DO_NOTHING:忽略 misfired job,等待下次觸發(fā);
建議讀者在應(yīng)用開(kāi)發(fā)中,將該設(shè)置作為可配置選項(xiàng),使得用戶可以在使用過(guò)程中,針對(duì)已經(jīng)添加的 tirgger 動(dòng)態(tài)配置該選項(xiàng)。
應(yīng)用五:如何保留已經(jīng)結(jié)束的 Trigger
在 Quartz 中,一個(gè) tirgger 在最后一次觸發(fā)完成之后,會(huì)被自動(dòng)刪除。Quartz 默認(rèn)不會(huì)保留已經(jīng)結(jié)束的 trigger,如下面 Quartz 源代碼所示:
圖 15. executionComplete( ) 源碼
但是在實(shí)際應(yīng)用中,有些用戶需要保留以前的 trigger,作為歷史記錄,或者作為以后創(chuàng)建其他 trigger 的依據(jù)。如何保留結(jié)束的 trigger 呢?
一個(gè)辦法是應(yīng)用開(kāi)發(fā)者自己維護(hù)一份數(shù)據(jù)備份記錄,并且與 Quartz 原表的記錄保持一定的同步。這個(gè)辦法實(shí)際操作起來(lái)比較繁瑣,而且容易出錯(cuò),不推薦使用。
另外一個(gè)辦法是通過(guò)修改并重新編譯 Quartz 的 trigger 類,修改其默認(rèn)的行為。我們以 org.quartz.SimpleTrigger 為例,修改上面代碼中 if (!mayFireAgain()) 部分的代碼如下:
圖 16. 修改 executionComplete( ) 源碼
另外我們需要在 SimpleTrigger 中定義一個(gè)新的類屬性:needRetain,如下所示:
圖 17. 定義新屬性 needRetain
在定義自己的 trigger 時(shí),設(shè)置該屬性,就可以選擇是否在 trigger 結(jié)束時(shí)刪除 trigger。如下代碼所示:
圖 18. 使用修改后的 SimpleTrigger
有人可能會(huì)考慮通過(guò)定義一個(gè)新的類,然后繼承 org.quartz.SimpleTrigger 類并覆蓋 executionComplete( ) 方法來(lái)實(shí)現(xiàn)。但是這種方法是行不通的,因?yàn)?Quartz 內(nèi)部在處理時(shí)會(huì)根據(jù) trigger 的類型重新生成 SimpleTrigger 類的實(shí)例,而不是使用我們自己定義的類創(chuàng)建的實(shí)例。這一點(diǎn)應(yīng)該是 Quartz 的一個(gè)小小的不足之處,因?yàn)樗褦U(kuò)展 trigger 的能力堵死了。好在 Quartz 是開(kāi)源的,我們可以根據(jù)需要進(jìn)行修改。
轉(zhuǎn)載于:https://www.cnblogs.com/Jtianlin/p/4524015.html
總結(jié)
以上是生活随笔為你收集整理的quartz (一) 基于 Quartz 开发企业级任务调度应用的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: JAVA学习博客---2015.5
- 下一篇: HttpClient异常处理手册