第七色在线视频,2021少妇久久久久久久久久,亚洲欧洲精品成人久久av18,亚洲国产精品特色大片观看完整版,孙宇晨将参加特朗普的晚宴

為了賬號(hào)安全,請(qǐng)及時(shí)綁定郵箱和手機(jī)立即綁定
2. 如何訪問 RabbitMQ 消息監(jiān)控平臺(tái) ?

什么是 RabbitMQ 消息監(jiān)控平臺(tái)RabbitMQ 消息監(jiān)控平臺(tái),即用來(lái)監(jiān)控 RabbitMQ 中所有的消息所處的狀態(tài),以及當(dāng)前 RabbitMQ Server 的狀態(tài)的一個(gè)網(wǎng)絡(luò)平臺(tái),為 RabbitMQ 內(nèi)嵌的自我監(jiān)控平臺(tái),在安裝好了 RabbitMQ 之后就可以通過特定的路徑來(lái)訪問這一消息監(jiān)控平臺(tái)。出于方便考慮,后續(xù)我們都將 RabbitMQ 消息監(jiān)控平臺(tái),稱為 RabbitMQ 消息管控臺(tái)。在 RabbitMQ 消息管控臺(tái)中,可以對(duì)我們應(yīng)用程序中,所有的消息進(jìn)行監(jiān)控,包括消息是否已經(jīng)被發(fā)送、消息是否已經(jīng)被接收,以及 RabbitMQ Server 中,交換機(jī)、頻道、消息隊(duì)列的狀態(tài)等,經(jīng)過對(duì)這些關(guān)鍵指標(biāo)進(jìn)行監(jiān)控,我們可以很好地掌握我們應(yīng)用程序中的消息在 RabbitMQ Server 中的狀態(tài),以及我們的 RabbitMQ Server 節(jié)點(diǎn)工作是否正常等關(guān)鍵性能指標(biāo)。如何訪問 RabbitMQ 消息監(jiān)控平臺(tái) ?在介紹如何安裝 RabbitMQ 小節(jié)內(nèi)容中,已經(jīng)為大家介紹了如何來(lái)驗(yàn)證我們的 RabbitMQ 是否已經(jīng)安裝成功了,而這一驗(yàn)證手段正是我們?cè)L問 RabbitMQ 消息監(jiān)控平臺(tái)的方法,這里我們來(lái)簡(jiǎn)單回顧一下。在將 RabbitMQ 安裝成功之后,需要我們啟動(dòng) RabbitMQ Server 服務(wù),該服務(wù)會(huì)占用我們的 5672 端口,然后對(duì)外暴露 15672 端口,來(lái)供我們?cè)L問,所以,要想訪問 RabbitMQ 消息管控臺(tái),我們只需要輸入 RabbitMQ Server 所在節(jié)點(diǎn)的 ip 地址,以及 15672 端口的組合方式就可以了。以本地為例,訪問 RabbitMQ 消息管控臺(tái)的地址如下:http://localhost:15672

2.1 什么是 HaProxy 組件?

什么是 HaProxy 組件呢?我們可以像理解 KeepAlived 組件那樣,去理解 HaProxy 組件。HaProxy 這一名詞和 KeepAlived 名詞一樣,也是一個(gè)組合詞,我們拆開來(lái)看,Ha 其實(shí)是 High Availability 簡(jiǎn)稱,取的是兩個(gè)單詞的首字符,即 HA ,中文含義就是我們常說的高可用。而 proxy 就比較簡(jiǎn)單了,proxy 翻譯過來(lái)具有代理的含義,代理我們可以理解為我們平常生活中的第三方辦事機(jī)構(gòu),就是我們自己的事情交給第三方去做,等第三方完成我們交代的任務(wù)后,會(huì)給我們一個(gè)通知,HaProxy 中的 proxy 就是這個(gè)意思。在清楚了 HaProxy 各組成名詞之后,我們就不難理解到底什么是 HaProxy 了。HaProxy 翻譯過來(lái)其實(shí)指的就是一種高可用的代理工具,或者可以叫做一種軟件。即 HaProxy 就是一種為集群提供高可用支持與服務(wù)的,一種高可用代理工具,其可以管理集群間的通信方式,可以對(duì)集群進(jìn)行健康檢查,甚至也可以對(duì)集群進(jìn)行全方位的監(jiān)控,說白了,HaProxy 就是一種輔助集群實(shí)現(xiàn)高可用、對(duì)集群進(jìn)行監(jiān)控、對(duì)集群進(jìn)行健康檢查的一種組件或軟件。而實(shí)現(xiàn)這種高可用,以及集群監(jiān)控和集群健康檢查的手段,則是通過 HaProxy 中的 Proxy 代理實(shí)現(xiàn)的,我們可以配置采用 tcp 還是 http 代理的方式,這點(diǎn)同學(xué)們簡(jiǎn)單了解即可。

3. 通過 XML 實(shí)現(xiàn)一個(gè)登錄界面

在編寫代碼之前,我們先來(lái)一起來(lái)分析分析這個(gè)頁(yè)面,一步一步的把它拆分成一個(gè)網(wǎng)格:整個(gè)頁(yè)面占滿全屏,我們知道直接把 GridView 的長(zhǎng)寬設(shè)置成match_parent即可從左到右一共分為 3 列:賬號(hào)/密碼下劃線輸入框提交第一行和第二行占滿全列,“提交”在最后一行的最后一列基本上按照以上思路就可以把整個(gè)頁(yè)面按照網(wǎng)格分割好,接下來(lái)就可以開始 xml 代碼的編寫了:<?xml version="1.0" encoding="utf-8"?><GridLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:columnCount="3" android:useDefaultMargins="true"> <TextView android:layout_columnSpan="3" android:layout_gravity="center_horizontal" android:text="用戶登錄" android:textSize="32sp" /> <TextView android:layout_columnSpan="3" android:layout_gravity="left" android:text="請(qǐng)輸入您的賬號(hào)密碼:" android:textSize="20sp" /> <TextView android:layout_gravity="right" android:text="賬號(hào):" /> <EditText android:ems="10" /> <TextView android:layout_column="0" android:layout_gravity="right" android:text="密碼:" /> <EditText android:ems="8" /> <Button android:layout_row="3" android:layout_column="2" android:layout_gravity="bottom" android:text="提交" /></GridLayout>以上代碼有幾點(diǎn)需要注意:我們僅僅指定了列數(shù),系統(tǒng)可以根據(jù)列數(shù)計(jì)算出行數(shù);GridLayout 內(nèi)部的 View 在沒有設(shè)置具體的行列序號(hào)的時(shí)候,默認(rèn)按照順序依次從左往右、從上往下依次排列;GridLayout 非常智能,為了向大家展示它的優(yōu)勢(shì),本代碼中也省略了大量的屬性設(shè)置,全都交給系統(tǒng)自行計(jì)算。不過在實(shí)際使用中建議還是標(biāo)注出來(lái),這樣可讀性更高。

2. 異常處理原則

異常是程序運(yùn)行過程中不可避免的問題。異常出現(xiàn)的原因很多,但不管怎樣,都需要提前預(yù)知或者當(dāng)異常發(fā)生后采取相應(yīng)的處理措施。異常的處理原則是:能預(yù)知的盡可能在邏輯層面提前制止。如用戶注冊(cè)時(shí),要求登錄名是唯一的,可先檢查數(shù)據(jù)庫(kù)是否存在同名用戶名后,再進(jìn)行添加操作;以一種友好的方式告知使用者出錯(cuò)的原因;采用多層體系結(jié)構(gòu)的項(xiàng)目中,建議異常由下逐層向上拋出,一直到達(dá)應(yīng)用層面;使用日志記錄功能把異常信息記錄在日志文件中,便于開發(fā)者分析。如下面的控制器方法:@Controllerpublic class ExceptionAction {@RequestMapping("/exception01")public String exception01(@RequestParam("userName") String userName) { return "exception";}}在瀏覽器中輸入:http://localhost:8888/sm-demo/exception01 ,頁(yè)面中會(huì)出現(xiàn)錯(cuò)誤提示。這個(gè)原因是 @RequestParam(“userName”) 注解在默認(rèn)情況下,要求請(qǐng)求包中一定要有 userName 這個(gè)參數(shù)。顯然,頁(yè)面中顯示出來(lái)的錯(cuò)誤信息是不友好的。所謂的異常處理,并不能完全阻止異常的發(fā)生。而是把異常信息對(duì)外、對(duì)內(nèi)做一個(gè)封裝,換一個(gè)淺白的、直接的、非專業(yè)的方式告訴使用者。對(duì)于前面的異常解決方案,可以在 @RequestParam(value = “userName”,required = false) 中添加一個(gè) required = false 的設(shè)置。這是一種最理想的異常解決方案。

1. 對(duì)接 Eclipse 使用

Eclipse 作為一個(gè)老牌的 Java 開發(fā)工具,在很長(zhǎng)一段時(shí)間都是市面上最流行的?,F(xiàn)在,我們就介紹一下在 Eclipse 中如何使用 Maven 來(lái)構(gòu)建項(xiàng)目。首先,我們來(lái)安裝 m2eclipse 插件。在菜單中選擇:Help >> Install New Software,并在彈出的窗口中輸入 m2eclipse 插件的地址 http://download.eclipse.org/technology/m2e/releases;安裝插件的過程沒什么太多差異,一路 Next 即可完成安裝;插件安裝完成后,我們配置已經(jīng)安裝好的 Maven ,菜單路徑:Window >> Preferences,在彈窗中選擇Maven >> Installations;并且在 User Settings 中配置對(duì)應(yīng)的 setting.xml 文件和本地倉(cāng)庫(kù);配置好 Maven 之后,我們來(lái)構(gòu)建項(xiàng)目。在 Eclipse 中使用 Maven 一般有兩種方式,一種是使用命令行的形式,一種是在 Configuration 中進(jìn)行配置,下面我們來(lái)介紹一下這兩種使用方式。使用命令行構(gòu)建。這種構(gòu)建方式其實(shí)和我們使用 cmd 來(lái)構(gòu)建是一樣的。菜單選擇Window >> Show view >> Other,在彈出的窗口中選擇 Terminal。在控制臺(tái)中即可執(zhí)行任何 Maven 相關(guān)的命令;配置 Configuration。這種方式直接在項(xiàng)目的根目錄上單擊右鍵,選擇Run As >> Maven build,在彈出的窗口中可以輸入對(duì)應(yīng)的目標(biāo),以及要激活的 profile ,選擇好之后,點(diǎn)擊下方的 run 按鈕即可執(zhí)行。兩種方式本質(zhì)上沒有什么差別,常用的操作可以配置到 Configuration 中,重復(fù)使用起來(lái)更方便,如果要靈活使用的話,命令行應(yīng)該是更好的選擇。

2.1 Session 的相關(guān)配置

由于 Session 的數(shù)據(jù)是保存在服務(wù)器端的,所以很多工作是需要在服務(wù)器端來(lái)完成的,所以 Django 中 Session 的操作相比 Cookie 操作會(huì)略顯復(fù)雜。首先需要介紹 Django 中和 Session 相關(guān)的配置,同樣是在 settings.py 文件中:?jiǎn)⒂?Session:需要在 MIDDLEWARE 值中添加相應(yīng)的 Session 中間件,去對(duì) Session 攔截和處理。另外,還需要再INSTALLED_APPS 中注冊(cè) Session 應(yīng)用。Django 中默認(rèn)是有這個(gè)配置的。MIDDLEWARE = [ # ... 'django.contrib.sessions.middleware.SessionMiddleware', # ...]INSTALLED_APPS = [ # 啟用 sessions 應(yīng)用 'django.contrib.sessions',]配置 Session 引擎:主要是配置 Session 保存方式,比如數(shù)據(jù)庫(kù)保存、內(nèi)存保存、文件系統(tǒng)保存等。# 數(shù)據(jù)庫(kù)SessionSESSION_ENGINE = 'django.contrib.sessions.backends.db' # 默認(rèn)引擎# 緩存SessionSESSION_ENGINE = 'django.contrib.sessions.backends.cache' # 使用的緩存別名(默認(rèn)內(nèi)存緩存,也可以是memcache),此處別名依賴緩存的設(shè)置SESSION_CACHE_ALIAS = 'default' # 文件SessionSESSION_ENGINE = 'django.contrib.sessions.backends.file' # 緩存文件路徑,如果為None,則使用tempfile模塊獲取一個(gè)臨時(shí)地址tempfile.gettempdir() SESSION_FILE_PATH = None # 緩存+數(shù)據(jù)庫(kù)SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db' # 加密Cookie SessionSESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies' 其他配置如下:# Session的cookie保存在瀏覽器上時(shí)的key,即:sessionid=隨機(jī)字符串(默認(rèn))SESSION_COOKIE_NAME = "sessionid" # Session的cookie保存的路徑(默認(rèn))SESSION_COOKIE_PATH = "/" # Session的cookie保存的域名(默認(rèn))SESSION_COOKIE_DOMAIN = None # 是否Https傳輸cookie(默認(rèn))SESSION_COOKIE_SECURE = False # 是否Session的cookie只支持http傳輸(默認(rèn))SESSION_COOKIE_HTTPONLY = True # Session的cookie失效日期(2周)(默認(rèn))SESSION_COOKIE_AGE = 1209600 # 是否關(guān)閉瀏覽器使得Session過期(默認(rèn))SESSION_EXPIRE_AT_BROWSER_CLOSE = False # 是否每次請(qǐng)求都保存Session,默認(rèn)修改之后才保存(默認(rèn))SESSION_SAVE_EVERY_REQUEST = False 關(guān)于上述這些未出現(xiàn)在 settings.py 中的配置,默認(rèn)的值都會(huì)在 django/conf/global_settings.py 中找到,如下圖所示:

2.4 服務(wù)消費(fèi)者測(cè)試

在進(jìn)行服務(wù)消費(fèi)者測(cè)試之前,我們可以多啟動(dòng)幾個(gè)服務(wù)提供者實(shí)例來(lái)模擬集群中的機(jī)器。在 IDEA 中使用快捷鍵 alt + 8 呼出 Services 面板,右鍵點(diǎn)擊 Services 面板中的 ZookeeperProviderApplication ,選擇 copy Configuration 復(fù)制我們的服務(wù)提供者,在彈出的 Edit Configuration 界面中,修改實(shí)例名稱,添加 VM options 參數(shù) -Dserver.port=8091 來(lái)修改啟動(dòng)端口號(hào)為 8091 ,點(diǎn)擊 OK 確認(rèn)。在這里我們復(fù)制了兩個(gè)服務(wù)提供者,也就是說一共有 3 個(gè)服務(wù)提供者來(lái)提供服務(wù)。保持 Zookeeper 服務(wù)端的啟動(dòng),然后依次啟動(dòng) 3 個(gè)服務(wù)提供者,觀察控制臺(tái),我們可以看到 3 個(gè)服務(wù)提供者都輸出了服務(wù)上線的信息:>>> 服務(wù)提供者連接 Zookeeper ...>>> 本服務(wù)已上線服務(wù)提供者啟動(dòng)完畢,接下來(lái)我們就可以啟動(dòng)服務(wù)消費(fèi)者來(lái)進(jìn)行測(cè)試了,啟動(dòng)服務(wù)消費(fèi)者,觀察控制臺(tái):>>> 服務(wù)消費(fèi)者連接 Zookeeper ...>>> CuratorCacheListener 初始化我們可以看到,服務(wù)消費(fèi)者連接上了 Zookeeper 服務(wù)器,并且開啟了監(jiān)聽,接下來(lái)我們就可以訪問服務(wù)消費(fèi)者的 RESTful 接口來(lái)測(cè)試了,訪問 http://localhost:9090/consumer/callMethod ,查看瀏覽器內(nèi)容:調(diào)用了服務(wù)提供者 192.168.0.102:8090 的方法刷新頁(yè)面,再次訪問:調(diào)用了服務(wù)提供者 192.168.0.102:8091 的方法再次刷新頁(yè)面進(jìn)行訪問:調(diào)用了服務(wù)提供者 192.168.0.102:8092 的方法我們可以發(fā)現(xiàn),服務(wù)消費(fèi)者依次調(diào)用了 8090 ,8091 ,8092 這 3 個(gè)服務(wù)提供者的方法,并且實(shí)現(xiàn)了輪詢的負(fù)載均衡策略。測(cè)試成功后,我們就可以模擬對(duì)集群管理的各種場(chǎng)景了。

2. CSRF 攻擊原理

我們用一個(gè)實(shí)例演示「CSRF」攻擊的過程。假設(shè)我們登陸了一個(gè)銀行網(wǎng)站(bank.example.com),這個(gè)網(wǎng)站的作用是實(shí)現(xiàn)跨行轉(zhuǎn)賬的表單提交,通常情況下,我們會(huì)生成如下一個(gè) Form 表單。<form method="post" action="/transfer"> <!-- 匯款金額 --> <input type="text" name="amount"/> <!-- 匯款路由號(hào) --> <input type="text" name="routingNumber"/> <!-- 匯款賬戶 --> <input type="text" name="account"/> <input type="submit" value="提交"/></form>那我們發(fā)出的「post」請(qǐng)求格式可能如下:POST /transfer HTTP/1.1Host: bank.example.comCookie: JSESSIONID=randomidContent-Type: application/x-www-form-urlencodedamount=100.00&routingNumber=1234&account=9876此時(shí),如果我們未登出,并且訪問了其他惡意網(wǎng)站,并且其他惡意網(wǎng)站同樣包含了可提交的表單,表單形式如下:<form method="post" action="https://bank.example.com/transfer"> <!-- 隱藏項(xiàng)不可見,轉(zhuǎn)賬金額,固定 100 元 --> <input type="hidden" name="amount" value="100.00"/> <!-- 隱藏項(xiàng)不可見,轉(zhuǎn)賬路由碼 --> <input type="hidden" name="routingNumber" value="evilsRoutingNumber"/> <!-- 隱藏項(xiàng)不可見,轉(zhuǎn)賬賬戶 --> <input type="hidden" name="account" value="evilsAccountNumber"/> <!-- 可見 --> <input type="submit" value="快來(lái)點(diǎn)我!"/></form>當(dāng)我們很好奇,點(diǎn)擊了「快來(lái)點(diǎn)我」按鈕時(shí),我們會(huì)觸發(fā)轉(zhuǎn)賬請(qǐng)求,并將錢匯款到一個(gè)未知賬戶里。在這個(gè)過程中,雖然惡意網(wǎng)站并不知道我們的「Cookies」值,但是由于未登出,我們和銀行網(wǎng)站之間的 Cookies 還在,所以當(dāng)我們?cè)俅伟l(fā)起請(qǐng)求時(shí),該 Cookies 依然有效,這使得不知不覺被觸發(fā)的轉(zhuǎn)賬請(qǐng)求同樣有效。除此之外,如果惡意網(wǎng)站使用 JS 腳本自動(dòng)提交表單的話,用戶可能沒有任何被攻擊的感覺。

4. Spring Boot 集成 RabbitMQ 消息通信中間件是否成功的必要性測(cè)試

無(wú)論使用哪一種集成方式,驗(yàn)證 RabbitMQ 集成成功與否的方式都是類似下方的內(nèi)容:當(dāng)我們的 Maven 包管理工具解析完成 RabbitMQ 的依賴之后,此時(shí),需要運(yùn)行我們的 SpringBoot 項(xiàng)目,在項(xiàng)目啟動(dòng)完成之后,我們可以在本地瀏覽器地址欄中,輸入以下地址:http://localhost:15672如果看到 RabbitMQ 提示的登錄信息框,如下圖所示,則表示我們已經(jīng)成功把 RabbitMQ 消息通信中間件集成到了 SpringBoot 框架中去了。RabbitMQ 內(nèi)置的默認(rèn)賬號(hào)和密碼都是 guest ,我們可以登錄進(jìn)去看下,如下圖所示:Tips:1.各位同學(xué)請(qǐng)務(wù)必按照老師要求的內(nèi)容來(lái)安裝 RabbitMQ-Server ,當(dāng)然,如果你足夠清楚不同的安裝包中的內(nèi)容,那么,你可以通過下載 Binary 形式的完全安裝包進(jìn)行安裝 RabbitMQ ,否則,請(qǐng)使用老師提供的安裝包進(jìn)行安裝。2.在安裝 RabbitMQ 之前,請(qǐng)確保 erlang 語(yǔ)言支持庫(kù)已經(jīng)成功安裝,如果 erlang 語(yǔ)言支持庫(kù)沒有安裝成功,請(qǐng)不要安裝 RabbitMQ ,如果已經(jīng)在錯(cuò)誤的環(huán)境安裝 RabbitMQ 導(dǎo)致報(bào)錯(cuò),那么請(qǐng)完全刪除掉已經(jīng)安裝的 RabbitMQ 內(nèi)容,從頭開始安裝。3.請(qǐng)確保下載的 erlang 語(yǔ)言支持庫(kù)的版本為 V23.0 最新版本,否則可能會(huì)導(dǎo)致 RabbitMQ 無(wú)法安裝成功。4.由于 RabbitMQ 是基于 AMQP 協(xié)議的(后續(xù)會(huì)介紹),所以,Maven 依賴的名稱為 amqp-client ,即 AMQP 客戶端,這一點(diǎn),同學(xué)們簡(jiǎn)單了解即可。5.在啟動(dòng)項(xiàng)目之前,請(qǐng)先啟動(dòng)自己本地電腦中的 RabbitMQ 服務(wù),否則,項(xiàng)目無(wú)法啟動(dòng)。

1. 前言

TCP 的英文全稱是 Transmission Control Protocol,翻譯成中文叫做傳輸控制協(xié)議,它是 TCP/IP 協(xié)議族中非常重要的一個(gè)傳輸層協(xié)議。TCP 是一個(gè)面向連接的、面向字節(jié)流的、可靠的傳輸層協(xié)議,有丟包重傳機(jī)制、有流控機(jī)制、有擁塞控制機(jī)制。TCP 保證數(shù)據(jù)包的順序,并且對(duì)重復(fù)包進(jìn)行過濾。相比不可靠傳輸協(xié)議 UDP,TCP 完全是相反的。對(duì)于可靠性要求很高的應(yīng)用場(chǎng)景來(lái)說,選擇可靠 TCP 作為傳輸層協(xié)議肯定是正確的。例如,著名的 HTTP 協(xié)議和 FTP 協(xié)議都是采用 TCP 進(jìn)行傳輸。當(dāng)然 TCP 為了保證傳輸?shù)目煽啃?,引入了非常?fù)雜的保障機(jī)制,比如:TCP 連接建立時(shí)的三次握手和連接關(guān)閉時(shí)的四次揮手機(jī)制,滑動(dòng)窗口機(jī)制,發(fā)送流控機(jī)制,慢啟動(dòng)和擁塞避免機(jī)制等。當(dāng)然,操作系統(tǒng)的網(wǎng)絡(luò)協(xié)議棧已經(jīng)實(shí)現(xiàn)了這些復(fù)雜的機(jī)制,本小節(jié)主要是介紹通過 Java 語(yǔ)言編寫 TCP 客戶端、服務(wù)器程序的方法。編寫 TCP 客戶端、服務(wù)器程序主要分為如下幾個(gè)步驟:創(chuàng)建客戶端 Socket,連接到某個(gè)服務(wù)器監(jiān)聽的端口,需要指定服務(wù)器監(jiān)聽的 host 和 port。host 可以是 IP 地址,也可以是域名。創(chuàng)建服務(wù)端 Socket,綁定到一個(gè)固定的服務(wù)端口,監(jiān)聽客戶端的連接請(qǐng)求。客戶端發(fā)起連接請(qǐng)求,完成三次握手過程。TCP 連接建立成功后,雙方進(jìn)行數(shù)據(jù)流交互。數(shù)據(jù)流交互完成后,關(guān)閉連接。

4. 獲取用戶的輸入信息

TextView 的功能只是作為信息輸出展示給用戶,而 EditText 除了可以展示信息之外,還需要從用戶那里得到輸入的內(nèi)容,我們可以通過getText().toString()獲取到 EditText 的輸入信息。修改一下布局文件,添加一個(gè) button 來(lái)觸發(fā)獲取的時(shí)機(jī):<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="20dp"> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:text="確定" /> <EditText android:id="@+id/input_password" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginRight="10dp" android:layout_toLeftOf="@id/button" android:hint="請(qǐng)輸入密碼" android:imeOptions="actionDone" android:inputType="textPassword" android:maxLines="5" android:textSize="20sp" /></RelativeLayout>我們?cè)诘谝粋€(gè)示例代碼基礎(chǔ)之上,加了一個(gè) Button,并放在了一個(gè) RelativeLayout 中(大家可以思考一下這里為什么要用 RelativeLayout,如果用 LinearLayout 能實(shí)現(xiàn)嗎?),我們希望用 Button 作為獲取 EditText 內(nèi)容的觸發(fā)事件,當(dāng)點(diǎn)擊 Button 的時(shí)候,我們獲取用戶在 EditText 中輸入的內(nèi)容,并打印到屏幕上驗(yàn)證結(jié)果。相關(guān) Java 代碼如下:final EditText editText = findViewById(R.id.input_password); Button button = findViewById(R.id.button); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(MainActivity.this, editText.getText(), Toast.LENGTH_SHORT).show(); } });首先我們根據(jù) ID 拿到 EditText 和 Button 的實(shí)例,接著對(duì) Button 設(shè)置一個(gè)監(jiān)聽器(關(guān)于 Button 監(jiān)聽器的使用,如果不清楚可以參考后續(xù) Button 章節(jié),不是本節(jié)重點(diǎn)),在監(jiān)聽器中我們獲取 EditText 的內(nèi)容,并通過 Toast 打印到屏幕,效果如下:擴(kuò)展:其實(shí)getText()不是 EditText 特有的,TextView 也可以通過getText()拿到顯示的內(nèi)容,只不過 TextView 的內(nèi)容是由我們?cè)O(shè)置上去的,所以需要去獲取的場(chǎng)景比較少,用的也相對(duì)少。

4.6 添加 MyBatis 映射文件

編寫數(shù)據(jù)訪問層接口之后,MyBatis 需要知道,如何將接口方法及參數(shù)轉(zhuǎn)換為 SQL 語(yǔ)句,以及 SQL 語(yǔ)句執(zhí)行結(jié)果如何轉(zhuǎn)換為對(duì)象。這些都是通過映射文件描述的, MyBatis 映射文件就是描述對(duì)象 - 關(guān)系映射的配置文件。首先我們通過 application.properties 指定映射文件的位置:實(shí)例:# 指定MyBatis配置文件位置mybatis.mapper-locations=classpath:mapper/*.xml然后在 resources/mapper 目錄下新建 GoodsMapper.xml 文件,該文件就是 goods 表對(duì)應(yīng)的映射文件,內(nèi)容如下:實(shí)例:<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><!-- 本映射文件對(duì)應(yīng)GoodsDao接口 --><mapper namespace="com.imooc.springbootmybatis.GoodsDao"> <!-- 對(duì)應(yīng)GoodsDao中的insert方法 --> <insert id="insert" parameterType="com.imooc.springbootmybatis.GoodsDo"> insert into goods (name,price,pic) values (#{name},#{price},#{pic}) </insert> <!-- 對(duì)應(yīng)GoodsDao中的delete方法 --> <delete id="delete" parameterType="java.lang.Long"> delete from goods where id=#{id} </delete> <!-- 對(duì)應(yīng)GoodsDao中的update方法 --> <update id="update" parameterType="com.imooc.springbootmybatis.GoodsDo"> update goods set name=#{name},price=#{price},pic=#{pic} where id=#{id} </update> <!-- 對(duì)應(yīng)GoodsDao中的selectOne方法 --> <select id="selectOne" resultMap="resultMapBase" parameterType="java.lang.Long"> select <include refid="sqlBase" /> from goods where id = #{id} </select> <!-- 對(duì)應(yīng)GoodsDao中的selectAll方法 --> <select id="selectAll" resultMap="resultMapBase"> select <include refid="sqlBase" /> from goods </select> <!-- 可復(fù)用的sql模板 --> <sql id="sqlBase"> id,name,price,pic </sql> <!-- 保存SQL語(yǔ)句查詢結(jié)果與實(shí)體類屬性的映射 --> <resultMap id="resultMapBase" type="com.imooc.springbootmybatis.GoodsDo"> <id column="id" property="id" /> <result column="name" property="name" /> <result column="price" property="price" /> <result column="pic" property="pic" /> </resultMap></mapper>

2. 實(shí)戰(zhàn):使用 flask 調(diào)用 redis 容器應(yīng)用

接下來(lái)我們將通過一個(gè)flask web應(yīng)用,實(shí)現(xiàn)主頁(yè)訪問計(jì)數(shù)功能,我們使用 redis 服務(wù)幫助我們實(shí)現(xiàn)計(jì)數(shù)統(tǒng)計(jì)功能。這個(gè) redis 服務(wù)運(yùn)行在我們的 Docker 容器中。Tips: 這里使用 flask 只是想借助一個(gè)簡(jiǎn)單的 web 框架來(lái)演示使用 docker 容器運(yùn)行的 redis,不了解也沒有關(guān)系,這里只是一個(gè)小例子。首先,我們?cè)?CentOS 中安裝 Python3 和flask框架,以及 Python 的 redis 客戶端庫(kù)。# 安裝python3sudo dnf install -y python3# 安裝flask與redis python客戶端pip3 install redis flask --user接下來(lái),將下面的代碼保存到~/test/app.pyimport flaskfrom flask import Flaskapp = Flask(__name__)from redis import StrictRedisfrom redis import ConnectionPool# 指定redis服務(wù)地址REDIS_HOST = os.getenv('REDIS_HOST','127.0.0.1')# 指定redis端口號(hào)REDIS_PORT = os.getenv('REDIS_PORT', '6379')# 指定redis的數(shù)據(jù)庫(kù)REDIS_DB = os.getenv('REDIS_DB', '0')# 指定redis的密碼REDIS_PASSWORD = os.getenv('REDIS_PASSWORD', '12345678')class Redis: def __init__(self): self.cli = None def connect(self): pool = ConnectionPool(host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB, password=REDIS_PASSWORD) return StrictRedis(connection_pool=pool) def add_pv(self): self.connection.incr('pv', 1) def get_pv(self): count = self.connection.get('pv') return int(count) @property def connection(self): if self.cli: return self.cli else: self.cli = self.connect() return self.cliredis = Redis()@app.route('/')def index(): redis.add_pv() return "<h1>Hello World, 本頁(yè)已訪問{}次。</h1>".format(redis.get_pv())在 ~/docker/redis/ 目錄下,配置 redis.conf 文件cd ~/docker/redis# 獲取官方提供的redis配置文件模板wget http://download.redis.io/redis-stable/redis.conf# 修改redis密碼echo "requirepass 12345678" >> redis.conf配置文件修改好后,用它來(lái)配置 Docker 的 redis 容器應(yīng)用。docker run --restart=always --network host -d -it -v ~/docker/redis/redis.conf:/usr/local/etc/redis/redis.conf --name myredis redis redis-server /usr/local/etc/redis/redis.confredis容器啟動(dòng)完成后,在 ~/test/ 目錄下執(zhí)行 python3 -m flask run,打開Linux系統(tǒng)中的瀏覽器,輸入127.0.0.1:5000 訪問這個(gè) web 應(yīng)用的主頁(yè),多次刷新查看效果。

1. 網(wǎng)絡(luò)爬蟲之網(wǎng)站分析

這一節(jié)我們來(lái)爬取一個(gè)圖書網(wǎng)站:互動(dòng)出版網(wǎng)。之所以選擇這個(gè)網(wǎng)站,主要是它的數(shù)據(jù)比較好爬取,沒有反爬蟲機(jī)制,且網(wǎng)站的結(jié)構(gòu)也不復(fù)雜,比較適合作為菜鳥進(jìn)行練手。我們首先來(lái)分析網(wǎng)站及其相關(guān)的 HTML 元素,確定要爬取的內(nèi)容?;?dòng)出版網(wǎng)的網(wǎng)站首頁(yè)如下:互動(dòng)出版網(wǎng)首頁(yè)可以看到,這個(gè)網(wǎng)站沒有用到 https,依舊使用的是 http 協(xié)議,這個(gè)網(wǎng)站是極不安全的。我們現(xiàn)在要爬取的是這個(gè)網(wǎng)站的計(jì)算機(jī)類的圖書,我們可以點(diǎn)擊全部圖書分類那里,得到所有圖書的分類情況?;?dòng)出版網(wǎng)全部圖書分類通過 F12 可以看到,每個(gè)計(jì)算機(jī)的分類對(duì)應(yīng)著一個(gè)鏈接。我們點(diǎn)進(jìn)去看就會(huì)得到對(duì)應(yīng)分類下的圖書列表,還帶著分頁(yè)信息:互動(dòng)出版網(wǎng)計(jì)算機(jī)分類圖書列表從這個(gè)頁(yè)面中,我們可以分析到很多,首先對(duì)于一個(gè)圖書信息,我們想要提取的數(shù)據(jù)有:圖書標(biāo)題;圖書作者;出版社;ISBN;出版時(shí)間;圖書價(jià)格。至于圖書的詳情頁(yè)面我們就不再進(jìn)去看了,詳情頁(yè)中能到到更多信息,比如總頁(yè)數(shù)、圖書簡(jiǎn)介、目錄等等。此外,這里有一個(gè)分頁(yè)信息,通過多次點(diǎn)擊可以發(fā)現(xiàn),只是前面的 url 中的一個(gè)數(shù)字發(fā)生了變化,因此我們可以直接構(gòu)造出相應(yīng)頁(yè)數(shù)的 url 請(qǐng)求,獲取其他頁(yè)的圖書列表、還等什么呢?開始激動(dòng)人心的圖書數(shù)據(jù)爬取流程吧?。?!

1.1 發(fā)出請(qǐng)求

注意 twisted.web.client.Agent 這個(gè)類,它是客戶端 API 的入口點(diǎn),請(qǐng)求是使用 request() 方法發(fā)出的,該方法以請(qǐng)求方法、請(qǐng)求URI、請(qǐng)求頭和可以生成請(qǐng)求體的對(duì)象作為參數(shù)。代理負(fù)責(zé)連接設(shè)置。因此,它需要一個(gè) reactor 作為初始值設(shè)定項(xiàng)的參數(shù)。來(lái)看官方給的第一個(gè)簡(jiǎn)單例子:from __future__ import print_functionfrom twisted.internet import reactorfrom twisted.web.client import Agentfrom twisted.web.http_headers import Headersagent = Agent(reactor)d = agent.request( b'GET', b'http://idcbgp.cn/wiki/', Headers({'User-Agent': ['Twisted Web Client Example']}), None)def cbResponse(ignored): print('Response received')d.addCallback(cbResponse)# 關(guān)閉reactor()def cbShutdown(ignored): reactor.stop()d.addBoth(cbShutdown)reactor.run()上述代碼簡(jiǎn)單實(shí)例化一個(gè) agent,然后調(diào)用 request() 方法請(qǐng)求 http://idcbgp.cn/wiki/ 這個(gè)地址,這個(gè)動(dòng)作也是一個(gè)延遲加載的方式;接下來(lái)的回調(diào)鏈中還會(huì)有請(qǐng)求完成后打印收到響應(yīng)的方法以及最后關(guān)閉 reactor 的方法;執(zhí)行的結(jié)果如下:[root@server2 scrapy-test]# python3 request.py Response received如果想要給請(qǐng)求帶上參數(shù),就需要傳遞一個(gè) twisted.web.iweb.IBodyProducer 類型的對(duì)象到 Agent.request。我們繼續(xù)來(lái)學(xué)習(xí)官方給出的第二個(gè)例子:下面的代碼給出了一個(gè)簡(jiǎn)單的 IBodyProducer 實(shí)現(xiàn),它向使用者寫入內(nèi)存中的字符串# 代碼文件命名為:bytesprod.py from zope.interface import implementerfrom twisted.internet.defer import succeedfrom twisted.web.iweb import IBodyProducer@implementer(IBodyProducer)class BytesProducer(object): def __init__(self, body): self.body = body self.length = len(body) def startProducing(self, consumer): consumer.write(self.body) return succeed(None) def pauseProducing(self): pass def stopProducing(self): pass下面的代碼則在請(qǐng)求中帶上了 body 體:# 代碼文件:sendbody.pyfrom twisted.internet import reactorfrom twisted.web.client import Agentfrom twisted.web.http_headers import Headersfrom bytesprod import BytesProduceragent = Agent(reactor)# 構(gòu)造請(qǐng)求體body = BytesProducer(b"hello, world")d = agent.request( b'POST', b'http://httpbin.org/post', Headers({'User-Agent': ['Twisted Web Client Example'], 'Content-Type': ['text/x-greeting']}), # 帶上body body)# 回調(diào)鏈,收到上個(gè)request的請(qǐng)求響應(yīng)def cbResponse(ignored): print('Response received')d.addCallback(cbResponse)# 關(guān)閉reactordef cbShutdown(ignored): reactor.stop()d.addBoth(cbShutdown)reactor.run()

2. 如何深入插件源碼學(xué)習(xí)?

我們以 DRF 框架為例,聊一聊如何深入 DRF 框架的源碼學(xué)習(xí)。首先肯定是下載穩(wěn)定版本為 DRF 源碼到本地,這是為了方便自己閱讀代碼。截止到2020年5月10日,DRF 的 Github 官方地址發(fā)布的最新版本為3.11.0,我們會(huì)用該版本的代碼來(lái)進(jìn)行相關(guān)的演示和說明。以下是 DRF-3.11.0 源代碼截圖,里面的代碼量還是比較大的,不過相對(duì)于 Django 的代碼而言就會(huì)少很多,我們前面能學(xué)習(xí)并跟蹤 Django 框架的源碼,拿下 DRF 源碼自然也不在話下。一般而言,推薦學(xué)習(xí)一個(gè) Django 第三方插件源碼的過程如下:第一步:熟練使用 Django 框架以及熟悉 Django 框架源碼。所有的 Django 第三方插件代碼里會(huì)大量調(diào)用 Django 源碼的類或者方法,并在其基礎(chǔ)上進(jìn)行擴(kuò)展或者進(jìn)一步創(chuàng)新。以我們必須先掌握 Django 的源碼,才能繼續(xù)學(xué)習(xí) DRF 的源碼;第二步:仔細(xì)閱讀官方文檔手冊(cè)進(jìn)行學(xué)習(xí),掌握框架的基本用法;第三步:通過官方文檔,實(shí)戰(zhàn) DRF 框架;每次在用熟練 DRF 提供的類或者方法后,就可以對(duì)應(yīng)地查看源碼,并分析 DRF 背后所做的工作。每掌握一個(gè)模塊的基本用法,就可以深入學(xué)習(xí)對(duì)應(yīng)模塊的源碼,同時(shí)在源碼中我們還可以發(fā)現(xiàn)該模塊中的更多用法,然后再次實(shí)踐,以加深對(duì)源碼的理解。我們按照上面的過程來(lái)簡(jiǎn)單走一遍。首先我們前面對(duì) Django 的幾大模塊的源碼都有涉獵,算是滿足了第一步要求。接下來(lái)我們用官方給的快速入門教程完成我們的第一次 Django REST framework 框架的初體驗(yàn)。模型序列化器:給會(huì)員表 member 添加一個(gè)序列化器類,放到新建的 serializers.py 文件中。from rest_framework import serializersfrom hello_app.models import Memberclass MemberSerializer(serializers.ModelSerializer): class Meta: model = Member fields = ("id", "name", "age", "sex", "occupation", "phone_num", "email", "city", "vip_level_id")準(zhǔn)備 View 視圖:添加一個(gè)對(duì)會(huì)員表操作的視圖類,我們用最簡(jiǎn)單的形式即可。# 代碼位置:hello_app/views.py# ...from rest_framework import viewsetsfrom rest_framework import permissions# ...class MemberViewSet(viewsets.ModelViewSet): # 設(shè)置queryset queryset = Member.objects.all().order_by('-register_date') # 設(shè)置序列化器 serializer_class = MemberSerializer # 設(shè)置認(rèn)證器 permission_classes = [permissions.IsAuthenticated]編寫 URLConf 配置:Django REST framework 框架改良了 URLConf 配置的寫法,后面會(huì)研究這種寫法,先直接使用官方的示例即可。# 代碼位置:hello_app/urls.py# ...from rest_framework import routersrouter = routers.DefaultRouter()router.register(r'members', views.MemberViewSet)urlpatterns = [ # ... path('', include(router.urls))]另外,由于我們對(duì) MemberViewSet 視圖加上了認(rèn)證,所以必須要在入口的 urls.py 中上如下的 URLConf 的配置。# 代碼位置: first_django_app/urls.py# ...urlpatterns = [ # ... path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))]注意:不添加和添加這行 URLConf 配置的效果圖如下所示。接下來(lái),最后一步是設(shè)置視圖的相關(guān)配置以及注冊(cè) rest_framework 應(yīng)用。# 代碼位置:first_django_app/settings.py# ...INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # 注冊(cè)第三方應(yīng)用 'rest_framework', # 注冊(cè)應(yīng)用 'hello_app']REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 5}# ...最后我們啟動(dòng)服務(wù),來(lái)一起看看效果。我們之前創(chuàng)建過一個(gè)超級(jí)用戶admin/admin.1234!,接下來(lái)會(huì)用這個(gè)通過 DRF 的認(rèn)證。25從上面的演示中,我們看到了 Django REST framework 框架給我們做的接口測(cè)試頁(yè)面,我們只需要簡(jiǎn)單繼承下MemberViewSet 即可,然后添加相關(guān)屬性即可立即擁有這樣一個(gè)完整的接口測(cè)試頁(yè)面。后臺(tái)服務(wù)主要提供接口數(shù)據(jù),我們也可以使用 curl 命令來(lái)獲取和操作相應(yīng)的模型表。[root@server ~]# curl -H 'Accept: application/json; indent=4' -u admin:admin.1234! http://127.0.0.1:8888/hello/members/?page=3{ "count": 103, "next": "http://127.0.0.1:8888/hello/members/?page=4", "previous": "http://127.0.0.1:8888/hello/members/?page=2", "results": [ { "id": 9, "name": "spyinx-5", "age": "39", "sex": 0, "occupation": "product", "phone_num": "18015702646", "email": "225@qq.com", "city": "shanghai", "vip_level_id": null }, { "id": 10, "name": "spyinx-6", "age": "26", "sex": 0, "occupation": "ops", "phone_num": "18790082215", "email": "226@qq.com", "city": "beijing", "vip_level_id": null }, { "id": 11, "name": "spyinx-7", "age": "23", "sex": 0, "occupation": "security", "phone_num": "18354491889", "email": "227@qq.com", "city": "guangzhou", "vip_level_id": null }, { "id": 12, "name": "spyinx-8", "age": "26", "sex": 1, "occupation": "ui", "phone_num": "18406891676", "email": "228@qq.com", "city": "wuhan", "vip_level_id": null }, { "id": 13, "name": "spyinx-9", "age": "26", "sex": 0, "occupation": "ops", "phone_num": "18036496230", "email": "229@qq.com", "city": "wuhan", "vip_level_id": null } ]}在上面這個(gè)過程走通之后,我們可以看到其實(shí)這個(gè)例子中已經(jīng)涉及到了 DRF 中的許多類,比如用于序列化的類ModelSerializer、視圖類 ModelViewSet、分頁(yè)類 PageNumberPagination 等等。從這個(gè)案例中,我們可以找到許多學(xué)習(xí) DRF 源碼的切入點(diǎn)。首先看用到的視圖類 ModelViewSet:# 源碼位置:rest_framework/viewsets.pyclass ModelViewSet(mixins.CreateModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, mixins.ListModelMixin, GenericViewSet): """ A viewset that provides default `create()`, `retrieve()`, `update()`, `partial_update()`, `destroy()` and `list()` actions. """ pass通過學(xué)習(xí) Django 的視圖,我們了解了 Mixin 這個(gè)概念,所以容易理解這里的代碼,視圖繼承 GenericViewSet,同時(shí)也繼承了數(shù)個(gè) Mixin。這些 Mixin 從命名上就很容易知道其功能用法。進(jìn)一步翻看其實(shí)現(xiàn)類,也能發(fā)現(xiàn)其具體含義 。以 mixins.CreateModelMixin 類為例:# rest_framework/mixins.pyclass CreateModelMixin: """ Create a model instance. """ def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) self.perform_create(serializer) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) def perform_create(self, serializer): serializer.save() def get_success_headers(self, data): try: return {'Location': str(data[api_settings.URL_FIELD_NAME])} except (TypeError, KeyError): return {}CreateModelMixin 的主要功能就是提供了 create() 方法,讓視圖擁有新增記錄的功能。其他的 Mixin 會(huì)提供類似的函數(shù),讓視圖具有某一特定的功能。接下來(lái)我們的重點(diǎn)放到 GenericViewSet 類的學(xué)習(xí)上。# 源碼位置:rest_framework/viewsets.pyclass GenericViewSet(ViewSetMixin, generics.GenericAPIView): """ The GenericViewSet class does not provide any actions by default, but does include the base set of generic view behavior, such as the `get_object` and `get_queryset` methods. """ pass這里又是多繼承,一個(gè) ViewSetMixin 類,另一個(gè) generics.GenericAPIView 類。先追后面的 View 類,實(shí)現(xiàn)代碼如下:從這里我們看到了一些熟悉的屬性,如 queryset,serializer_class 以及用于分頁(yè)的 pagination_class。這個(gè)繼承的 APIView 類同樣也是 Django REST framework 框架自己定義的類,我們繼續(xù)追進(jìn) APIView 類的實(shí)現(xiàn)代碼:最后 APIView 這個(gè)類繼承的 View 正是 Django 中我們學(xué)過的 View 視圖類。from django.views.generic import View那這樣子,我們也算清楚了一些事情。Django REST framework 框架中定義的視圖是在 Django 的 View 視圖類上封裝和改進(jìn)來(lái)的?,F(xiàn)在一個(gè)疑問就來(lái)了,看我們前面使用 Django 的視圖中,URLConf 配置如下: urlpatterns = [ path('test-cbv/', views.TestView.as_view(), name="test-cbv"), ]我們也分析過對(duì)應(yīng)的 View 類以及 as_view() 方法,它將 GET 請(qǐng)求映射到視圖類的 get() 方法,POST 請(qǐng)求則映射到 post() 方法;然而我們這里一路走下來(lái)并有沒有看到對(duì)應(yīng)的 get() 或者 post() 方法。但是視圖類繼承的多個(gè) Mixin 中提供了 create()、list() 等這樣的方法,那么他們是如何和 URLConf 配置對(duì)應(yīng)上的呢?我們現(xiàn)在要通過代碼去找出前面配置 URLConf 代碼的內(nèi)部原理:from django.conf.urls import includefrom rest_framework import routersrouter = routers.DefaultRouter()router.register(r'members', views.MemberViewSet)urlpatterns = [ # ... path('', include(router.urls))]來(lái)看看上面的 URLConf 配置。這個(gè)時(shí)候,我們需要去看 Django REST Framework 中的 DefaultRouter 類,包括注冊(cè)方法 register() 以及 urls 屬性值的獲取。最后還要看 Django 中的 include() 方法的代碼,才能理清楚 URL 和視圖的映射關(guān)系。先追蹤 Django REST Framework 中的 DefaultRouter 類實(shí)現(xiàn),該類繼承自 SimpleRouter,SimpleRouter 又繼承自 BaseRouter。為了加快速度,我們直接定位到基類 BaseRouter,可以看到 register() 方法和 urls 屬性的定義,如下:# 源碼位置:rest_framework/routers.pyclass BaseRouter: def __init__(self): self.registry = [] def register(self, prefix, viewset, basename=None): if basename is None: basename = self.get_default_basename(viewset) self.registry.append((prefix, viewset, basename)) # invalidate the urls cache if hasattr(self, '_urls'): del self._urls def get_default_basename(self, viewset): """ If `basename` is not specified, attempt to automatically determine it from the viewset. """ raise NotImplementedError('get_default_basename must be overridden') def get_urls(self): """ Return a list of URL patterns, given the registered viewsets. """ raise NotImplementedError('get_urls must be overridden') @property def urls(self): if not hasattr(self, '_urls'): self._urls = self.get_urls() return self._urls可以看到,在執(zhí)行 router.register(r'members', views.MemberViewSet) 后其實(shí)等同于給 registry 數(shù)組添加一個(gè)元組元素,用于存儲(chǔ)映射關(guān)系。而 urls 屬性值則是調(diào)用 get_urls() 方法得到的。class DefaultRouter(SimpleRouter): """ The default router extends the SimpleRouter, but also adds in a default API root view, and adds format suffix patterns to the URLs. """ # ... def get_urls(self): """ Generate the list of URL patterns, including a default root view for the API, and appending `.json` style format suffixes. """ urls = super().get_urls() if self.include_root_view: view = self.get_api_root_view(api_urls=urls) root_url = url(r'^$', view, name=self.root_view_name) urls.append(root_url) if self.include_format_suffixes: urls = format_suffix_patterns(urls) return urls可以看到它先是調(diào)用了父類的 get_urls() 方法,另外又添加了一些映射規(guī)則。我們添加如下一行 print() 語(yǔ)句:class DefaultRouter(SimpleRouter): def get_urls(self): """ Generate the list of URL patterns, including a default root view for the API, and appending `.json` style format suffixes. """ urls = super().get_urls() print('父類調(diào)用得到的urls={}'.format(urls)) # ...然后啟動(dòng)服務(wù),可以看到如下的結(jié)果:(django-manual) [root@server first_django_app]# python manage.py runserver 0:8888Watching for file changes with StatReloaderPerforming system checks...父類調(diào)用得到的urls=[<URLPattern '^members/$' [name='member-list']>, <URLPattern '^members/(?P<pk>[^/.]+)/$' [name='member-detail']>]System check identified no issues (0 silenced).May 15, 2020 - 13:30:04Django version 2.2.12, using settings 'first_django_app.settings'Starting development server at http://0:8888/Quit the server with CONTROL-C可以看到,這個(gè) ^members/$ 的URL 配置是由父類的 get_urls() 方法得到的。在父類 SimpleRouter 中的get_urls()方法中,我已經(jīng)做好了相關(guān)的注釋,最關(guān)鍵的代碼就在最后的 append() 部分,那里添加的便是最后 URL 和 視圖函數(shù)的關(guān)系。class SimpleRouter(BaseRouter): # ... def get_urls(self): # ... # 前面介紹過這個(gè) registry 屬性,就是通過 register() 方法得到的 for prefix, viewset, basename in self.registry: # ... for route in routes: # Only actions which actually exist on the viewset will be bound mapping = self.get_method_map(viewset, route.mapping) if not mapping: continue # Build the url pattern regex = route.url.format( prefix=prefix, lookup=lookup, # 尾部加上"/" trailing_slash=self.trailing_slash ) # 處理一些簡(jiǎn)單情況 if not prefix and regex[:2] == '^/': regex = '^' + regex[2:] initkwargs = route.initkwargs.copy() initkwargs.update({ 'basename': basename, 'detail': route.detail, }) # 最最核心的部分代碼,這里得到視圖函數(shù) view = viewset.as_view(mapping, **initkwargs) # 視圖名稱 name = route.name.format(basename=basename) # 添加映射規(guī)則 ret.append(url(regex, view, name=name)) return ret 我們可以看到最后添加的映射規(guī)則就是這一句:ret.append(url(regex, view, name=name)) ,我們繼續(xù)看看這個(gè) url() 方法,它調(diào)用的正是 Django 中的 url() 方法,內(nèi)容如下:# 源碼路徑:django/conf/urls.py# ...def url(regex, view, kwargs=None, name=None): return re_path(regex, view, kwargs, name)這個(gè) url() 方法和我們之前在 Django 中用 repath() 以及 path() 差不多一致的。第一個(gè)參數(shù)是 url 規(guī)則,第二個(gè)便是視圖函數(shù)。比較重要的就是這里得到 view 的函數(shù)了,它便是真正的視圖函數(shù)。它和前面 Django 中的一樣,通過 as_view() 得到的。那么這個(gè) as_view() 方法在哪呢,通過父類追蹤,可知 Django 的父類中本身就有 as_view() 方法,但是在前一個(gè)繼承的Mixin 中重寫了該方法,因此調(diào)用的便是該 Mixin 中的 as_view() 方法:class ViewSetMixin: """ This is the magic. Overrides `.as_view()` so that it takes an `actions` keyword that performs the binding of HTTP methods to actions on the Resource. For example, to create a concrete view binding the 'GET' and 'POST' methods to the 'list' and 'create' actions... view = MyViewSet.as_view({'get': 'list', 'post': 'create'}) """ @classonlymethod def as_view(cls, actions=None, **initkwargs): """ Because of the way class based views create a closure around the instantiated view, we need to totally reimplement `.as_view`, and slightly modify the view function that is created and returned. """ # ... def view(request, *args, **kwargs): self = cls(**initkwargs) # We also store the mapping of request methods to actions, # so that we can later set the action attribute. # eg. `self.action = 'list'` on an incoming GET request. self.action_map = actions # Bind methods to actions # This is the bit that's different to a standard view for method, action in actions.items(): handler = getattr(self, action) setattr(self, method, handler) if hasattr(self, 'get') and not hasattr(self, 'head'): self.head = self.get self.request = request self.args = args self.kwargs = kwargs # And continue as usual return self.dispatch(request, *args, **kwargs) # take name and docstring from class update_wrapper(view, cls, updated=()) # and possible attributes set by decorators # like csrf_exempt from dispatch update_wrapper(view, cls.dispatch, assigned=()) # We need to set these on the view function, so that breadcrumb # generation can pick out these bits of information from a # resolved URL. view.cls = cls view.initkwargs = initkwargs view.actions = actions return csrf_exempt(view)和 Django 中的一樣,這里最后的 as_view() 方法最后返回的便是視圖函數(shù)。那么對(duì)應(yīng)的 /hello/members/ 請(qǐng)求進(jìn)來(lái)后,有 view() 方法進(jìn)行處理,最后調(diào)用的和 Django 中的一樣:return self.dispatch(request, *args, **kwargs)我們?nèi)?Django 中看這個(gè) dispatch() 方法的源碼:# 源碼位置:django/views/generic/base.pyclass View: """ Intentionally simple parent class for all views. Only implements dispatch-by-method and simple sanity checking. """ # ... def dispatch(self, request, *args, **kwargs): # Try to dispatch to the right method; if a method doesn't exist, # defer to the error handler. Also defer to the error handler if the # request method isn't on the approved list. if request.method.lower() in self.http_method_names: handler = getattr(self, request.method.lower(), self.http_method_not_allowed) else: handler = self.http_method_not_allowed return handler(request, *args, **kwargs)那么執(zhí)行 /hello/members/ 請(qǐng)求到這里是,handler 是哪個(gè)?我們繼續(xù)翻看前面的 Mixin 類,有這樣一段代碼:class ViewSetMixin: # ... @classonlymethod def as_view(cls, actions=None, **initkwargs): def view(request, *args, **kwargs): # ... for method, action in actions.items(): handler = getattr(self, action) setattr(self, method, handler) # ... # ... # ...這里就非常明顯了,我們大概也能猜到一些。就是設(shè)置 (get|post|put|delete) 請(qǐng)求對(duì)應(yīng)的方法,比較好的方式時(shí)我們?cè)谶@里打印下請(qǐng)求,并在前端進(jìn)行下請(qǐng)求測(cè)試,看看這里到底設(shè)置了啥?class ViewSetMixin: # ... @classonlymethod def as_view(cls, actions=None, **initkwargs): def view(request, *args, **kwargs): # ... for method, action in actions.items(): print('請(qǐng)求處理view視圖函數(shù):method={}, action={}'.format(method, action)) handler = getattr(self, action) setattr(self, method, handler) # ... # ... # ...我們啟動(dòng)服務(wù)請(qǐng)求以下路徑 /hello/members/,可以得到如下輸出結(jié)果:(django-manual) [root@server first_django_app]# python manage.py runserver 0:8888Watching for file changes with StatReloaderPerforming system checks...System check identified no issues (0 silenced).May 15, 2020 - 17:07:38Django version 2.2.12, using settings 'first_django_app.settings'Starting development server at http://0:8888/Quit the server with CONTROL-C.請(qǐng)求處理view視圖函數(shù):method=get, action=list請(qǐng)求處理view視圖函數(shù):method=post, action=create[15/May/2020 17:07:43] "GET /hello/members/ HTTP/1.1" 200 14426結(jié)合 Django 中的 dispatch() 方法,我們終于知道了 get 請(qǐng)求最后會(huì)調(diào)用視圖類中的 list() 方法去處理,而這個(gè) list() 方法正是 ListModelMixin 中的。另外 post 請(qǐng)求則對(duì)應(yīng)著視圖類中的 create() 方法,而這個(gè)屬性則來(lái)自 CreateModelMixin。這樣我們總算理解了前面的 URLConf 的映射流程以及對(duì)應(yīng)的真正視圖處理函數(shù)。帶著問題去追源碼是我比較推薦的一個(gè)學(xué)習(xí)方式。完成一個(gè)模塊的學(xué)習(xí)就要去思考,去追蹤這個(gè)案例背后的執(zhí)行過程,這樣才能更好的掌握這個(gè)模塊。今天的分享就到此結(jié)束了,DRF 中還有很多代碼等著你們?nèi)ヌ剿鳎?shí)踐,祝大家學(xué)習(xí)愉快!

1.1 src 屬性

src 的全稱是 source,表示圖片的 URL 地址源。源就是表示圖片的地址路徑,這個(gè)路徑可以是相對(duì)路徑,也可以是絕對(duì)路徑。絕對(duì)路徑指的是一個(gè)包含網(wǎng)絡(luò)協(xié)議頭的完整路徑,常用的網(wǎng)絡(luò)協(xié)議是 HTTP 協(xié)議,例如 https://www.baidu.com/img/bd_logo1.png;相對(duì)路徑是指這個(gè)圖片文件跟當(dāng)前的網(wǎng)頁(yè)在同一個(gè)服務(wù)域,例如:/img/bd_logo1.png。962有時(shí)當(dāng)圖片過大時(shí)或者過多時(shí),網(wǎng)頁(yè)加載可能會(huì)比較慢,這時(shí)需要針對(duì)圖片使用懶加載的方式(圖片懶加載),懶加載的原理就是將圖片的 src 先設(shè)置為空,網(wǎng)頁(yè)其他內(nèi)容加載完之后,再通過 JavaScript 將 src 屬性賦值,例如:<div class="container"> <img src="img/loading.png" alt="1" data-src="photo-1.jpeg"> <img src="img/loading.png" alt="2" data-src="photo-2.jpeg"> <img src="img/loading.png" alt="3" data-src="photo-3.jpeg"></div><script>window.onload = function (){//頁(yè)面加載完之后再加載圖片 var a = document.getElementsByTagName("img");//獲取圖片DOM for(var i in a){ a[i].src = a[i].getAttribute("data-src") }}</script>點(diǎn)擊下面的“運(yùn)行案例”,可以試試真實(shí)的運(yùn)行效果:963以上代碼通過定義 onload 事件,將圖片延后加載。1.1.1 Base64 方式加載圖片我們都知道 exe、jpg、pdf 這些格式的文件是使用二進(jìn)制方式保存的,其中包含很多無(wú)法顯示和打印的字符,如果要讓文本處理軟件能處理二進(jìn)制數(shù)據(jù),需要將其編碼成一種明文顯示的安全格式,Base64 是一種最常見的二進(jìn)制編碼方法。有時(shí)為了方便處理,圖片并非使用二進(jìn)制流方式保存,而是使用 Base64 方式編碼之后保存在數(shù)據(jù)庫(kù),img 標(biāo)簽的 src 屬性可以識(shí)別 Base64 格式的編碼圖片格式,例如:964

3.1 框架層說明

由于大部分的預(yù)先認(rèn)證模式比較一致,Spring Security 為此提供了一個(gè)框架,用來(lái)實(shí)現(xiàn)預(yù)先認(rèn)證的身份提供器。預(yù)先認(rèn)證模塊的實(shí)現(xiàn)類均在包 org.springframework.security.web.authentication.preauth 之下。此處介紹幾個(gè)核心的成員對(duì)象。3.1.1 AbstractPreAuthenticatedProcessingFilter該類用來(lái)檢查當(dāng)前安全上下文的有效性,如果內(nèi)容為空,它會(huì)嘗試從 Http 請(qǐng)求中解析出用戶信息,并將其提交到認(rèn)證管理器 AuthenticationManager。使用該基類需要實(shí)現(xiàn)如下兩個(gè)方法:獲取認(rèn)證身份信息protected abstract Object getPreAuthenticatedPrincipal(HttpServletRequest request);獲取認(rèn)證憑證protected abstract Object getPreAuthenticatedCredentials(HttpServletRequest request);通過對(duì)這兩個(gè)方法的調(diào)用,該過濾器將創(chuàng)建一個(gè)票據(jù)對(duì)象實(shí)例 PreAuthenticatedAuthenticationToken ,該實(shí)例包含用戶認(rèn)證結(jié)果,并用于認(rèn)證過程。但是此處的認(rèn)證過程,僅僅是用來(lái)獲取權(quán)限,只是為了滿足標(biāo)準(zhǔn) Spring Security 流程而實(shí)施的步驟。和其他的安全過濾器類似,預(yù)先認(rèn)證的過濾器包含一個(gè) authenticationDetailsSource 屬性,用于保存預(yù)先認(rèn)證流程中的相關(guān)數(shù)據(jù)。3.1.2 J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource如果前述過濾器配置了 authenticationDetailsSource,其權(quán)限信息可以通過 isUserInRole(String role) 方法判斷,角色信息的配置通過 MappableAttributesRetriever 獲取,或者通過 <security-role> 標(biāo)簽配置到 web.xml 文件中。3.1.3 PreAuthenticatedAuthenticationProvider預(yù)先認(rèn)證身份提供者中的 UserDetails 對(duì)象與其他的身份提供者邏輯略有不同,主要體現(xiàn)在獲取 Authentication 認(rèn)證信息時(shí),僅需用戶名參數(shù)即可。public interface AuthenticationUserDetailsService { UserDetails loadUserDetails(Authentication token) throws UsernameNotFoundException;}3.1.4 Http403ForbiddenEntryPoint對(duì) AuthenticationEntryPoint 的配置用對(duì)未認(rèn)證用戶來(lái)發(fā)起認(rèn)證流程。在預(yù)先認(rèn)證流程中,該配置不起作用,我們只需要配置 ExceptionTranslationFilter 用于處理安全過濾器無(wú)法獲得用戶信息的情形,通常在這種情況下,請(qǐng)求端將收到 403 錯(cuò)誤。

3. Docker 安裝

在不同的操作系統(tǒng)中都可以安裝 Docker ,本節(jié)內(nèi)容中只演示 Ubuntu 環(huán)境下的 Docker 安裝。本次安裝演示的 Ubuntu 版本為 Ubuntu 20.04.1 LTS 。apt 更換國(guó)內(nèi)源在安裝應(yīng)用之前,我們需要把 apt 更換為國(guó)內(nèi)源,這里我們選擇阿里云的 mirros.aliyun.com。# 備份 apt 源列表文件sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak# 更換源為 mirros.aliyun.comsudo sed -i s@/archive.ubuntu.com/@/mirrors.aliyun.com/@g /etc/apt/sources.list# 更新源sudo apt-get cleansudo apt-get update更換完畢后,我們還需要安裝 apt 的一些工具,如 https,curl 等。安裝 apt 依賴包sudo apt-get -y install apt-transport-https ca-certificates curl gnupg-agent software-properties-common獲取 GPG 密鑰證書我們這里使用阿里云的鏡像來(lái)獲取 GPG 密鑰:curl -fsSL http://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | sudo apt-key add -獲取成功會(huì)返回 OK ,我們使用 apt-key finger 命令查看:apt-key finger# 輸出密鑰信息/etc/apt/trusted.gpg--------------------pub rsa4096 2017-02-22 [SCEA]9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88uid [ unknown] Docker Release (CE deb) <docker@docker.com>sub rsa4096 2017-02-22 [S]密鑰添加成功后,我們就可以開始后續(xù)的安裝了。添加 Docker 源為了更快速的安裝 Docker,這里我們添加阿里云的 Docker 源,首先我們先使用 lsb_release -a 命令獲取當(dāng)前系統(tǒng)的 Codename:lsb_release -a# 輸出系統(tǒng)信息No LSB modules are available.Distributor ID: UbuntuDescription: Ubuntu 20.04.1 LTSRelease: 20.04Codename: focal本系統(tǒng)的 Codename 也就是版本代碼為 focal,我們?cè)谔砑?Docker 源的時(shí)候就會(huì)使用這個(gè)版本:# 添加 docker-ce 源,系統(tǒng)為 ubuntu,系統(tǒng)版本為 focal, stable 為 docker 穩(wěn)定版。sudo add-apt-repository "deb [arch=amd64] http://mirrors.aliyun.com/docker-ce/linux/ubuntu focal stable"執(zhí)行完畢后,我們需要更新 apt 源:sudo apt-get updateDocker 安裝更新完源后,我們就可以開始安裝 Docker 組件了:sudo apt-get install docker-ce docker-ce-cli containerd.io執(zhí)行這一行命令需要一點(diǎn)時(shí)間,稍等片刻。完成后我們就可以來(lái)查看 Docker 是否安裝成功了。查看 Docker 版本使用 docker -v 來(lái)查看 Docker 版本:docker -v# 輸出 docker 版本信息Docker version 19.03.13, build 4484c46d9d看到版本信息輸出就說明我們的 Docker 源安裝成功了。Tips: 如果安裝失敗,需要注意系統(tǒng)的版本和添加的 Docker 源是否能使用。安裝成功后,我們來(lái)添加 Docker Image 鏡像源。添加 Docker Image 鏡像源使用阿里云的 Docker Image 鏡像源,需要登錄阿里云官網(wǎng)開啟 容器鏡像服務(wù):https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors添加 Docker Image 鏡像源為阿里云鏡像,這里同學(xué)們使用自己賬號(hào)的加速器地址即可:sudo mkdir -p /etc/dockersudo tee /etc/docker/daemon.json <<-'EOF'{"registry-mirrors": ["https://xxxxxx.mirror.aliyuncs.com"]}EOF添加完畢后,我們就可以啟動(dòng)我們的 Docker 服務(wù)了。啟動(dòng) Dockerservice docker start# 輸出啟動(dòng)信息* Starting Docker: docker 啟動(dòng)完成,接下來(lái)我們進(jìn)行測(cè)試。Docker 測(cè)試執(zhí)行測(cè)試命令:docker run hello-world輸出:Unable to find image 'hello-world:latest' locallylatest: Pulling from library/hello-world0e03bdcc26d7: Pull complete Digest: sha256:4cf9c47f86df71d48364001ede3a4fcd85ae80ce02ebad74156906caff5378bcStatus: Downloaded newer image for hello-world:latestHello from Docker!This message shows that your installation appears to be working correctly.To generate this message, Docker took the following steps:1. The Docker client contacted the Docker daemon.2. The Docker daemon pulled the "hello-world" image from the Docker Hub.(amd64)3. The Docker daemon created a new container from that image which runs theexecutable that produces the output you are currently reading.4. The Docker daemon streamed that output to the Docker client, which sent itto your terminal.To try something more ambitious, you can run an Ubuntu container with:$ docker run -it ubuntu bashShare images, automate workflows, and more with a free Docker ID:https://hub.docker.com/For more examples and ideas, visit:https://docs.docker.com/get-started/我們看到這段信息就說明,我們的 Docker 測(cè)試成功了。那么接下來(lái),我們就可以使用 Docker 來(lái)安裝 Zookeeper 服務(wù)。

3.2 構(gòu)建 OAuth2.0 客戶端

3.2.1 創(chuàng)建 Maven 項(xiàng)目創(chuàng)建 Maven 項(xiàng)目,本例中 Group ID 為 imooc.tonglei0429.springsecurity,Artifact ID 為 login.oauth2.github在控制臺(tái)輸入以下命令:mvn archetype:generate \ -DgroupId=imooc.tonglei0429.springsecurity \ -DartifactId=login.oauth2.github \ -DarchetypeArtifactId=maven-archetype-quickstart \ -DinteractiveMode=false等待執(zhí)行完畢后,執(zhí)行目錄下會(huì)自動(dòng)生成名為 login.oauth2.github 的目錄,包含完整的 Maven 項(xiàng)目結(jié)構(gòu)。3.2.2 添加相關(guān)依賴打開 login.oauth2.github/pom.xml 文件,在其中添加以下依賴項(xiàng):<!-- Spring Security 框架的配置模塊 --><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>5.3.2.RELEASE</version></dependency><!-- Spring Security OAuth2 認(rèn)證客戶端模塊 --><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-client</artifactId> <version>5.3.2.RELEASE</version></dependency><!-- Spring Security OAuth2 授權(quán)信息加密模塊 --><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-jose</artifactId> <version>5.3.2.RELEASE</version></dependency><!-- Spring Boot thymeleaf 啟動(dòng)模塊,用于 Web 服務(wù)中的頁(yè)面模板 --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> <version>2.3.0.RELEASE</version></dependency><!-- Spring Boot thymeleaf 啟動(dòng)模塊,用于啟動(dòng) Web 服務(wù) --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.3.0.RELEASE</version></dependency><!-- Spring Boot thymeleaf 啟動(dòng)模塊,包含適用于 Thymeleaf 的 Spring Security 擴(kuò)展 --><dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> <version>3.0.4.RELEASE</version></dependency>添加完以上內(nèi)容后,保持文件。在命令行中輸入 mvn compile 測(cè)試依賴是否添加成功。3.2.3 創(chuàng)建 Web 項(xiàng)目創(chuàng)建 src/main/resources 目錄,增加配置文件 application.yml,并添加如下配置:server: port: 8080spring: thymeleaf: cache: false修改 src/main/java/imooc/tonglei0429/springsecurity/App.java,使其作為 Spring Boot 項(xiàng)目啟動(dòng)。@SpringBootApplicationpublic class App { public static void main( String[] args ) { SpringApplication.run(App.class, args); }}創(chuàng)建 src/main/resources/templates/index.html 頁(yè),用于展示測(cè)試結(jié)果,index.html 文件內(nèi)容如下:<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity5"><head> <title>OAuth2 登錄測(cè)試</title> <meta charset="utf-8" /></head><body><div style="float: right" th:fragment="logout" sec:authorize="isAuthenticated()"> <div style="float:left"> <span style="font-weight:bold">User: </span><span sec:authentication="name"></span> </div> <div style="float:none"> </div> <div style="float:right"> <form action="#" th:action="@{/logout}" method="post"> <input type="submit" value="Logout" /> </form> </div></div><h1>Spring Security 示例:使用 OAuth2.0 + Github 實(shí)現(xiàn)用戶認(rèn)證</h1><div> 您已成功登錄,當(dāng)前用戶 ID 為: <span style="font-weight:bold" th:text="${userName}"></span> 所屬的客戶端名稱為: <span style="font-weight:bold" th:text="${clientName}"></span></div><div> </div><div> <span style="font-weight:bold">返回的用戶屬性:</span> <ul> <li th:each="userAttribute : ${userAttributes}"> <span style="font-weight:bold" th:text="${userAttribute.key}"></span>: <span th:text="${userAttribute.value}"></span> </li> </ul></div></body></html>為了使 http://localhost:8080/springsecurity/ 可以訪問,還需要配置該路徑的控制器,src/main/java/imooc/tonglei0429/springsecurity/web/OAuth2LoginController.java,其代碼如下:@Controllerpublic class OAuth2LoginController { @GetMapping("/") public String index(Model model, @RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient authorizedClient, @AuthenticationPrincipal OAuth2User oauth2User) { model.addAttribute("userName", oauth2User.getName()); model.addAttribute("clientName", authorizedClient.getClientRegistration().getClientName()); model.addAttribute("userAttributes", oauth2User.getAttributes()); return "index"; }}此時(shí)如果運(yùn)行項(xiàng)目,訪問 localhost:8080/springsecurity,系統(tǒng)會(huì)默認(rèn)跳轉(zhuǎn)到 localhost:8080/login 要求登錄,我們需要繼續(xù)將認(rèn)證源指向 Github。3.2.4 配置 OAuth2 認(rèn)證服務(wù)器繼續(xù)配置 application.yml 文件,添加配置項(xiàng) security.oauth2.client.registration.github.client-id 和 security.oauth2.client.registration.github.client-secret 兩項(xiàng),其值來(lái)自于 Github 上注冊(cè)應(yīng)用后的返回頁(yè)面。具體代碼如下:spring: thymeleaf: cache: false security: oauth2: client: registration: github: client-id: 300c4622cd932acad32a client-secret: 71cb7971c2260f84c7b59693fbcf97b9c118c638

2.2 Admin 中的 actions

我們前面看到,Admin 中的 actions 是控制模型列表的動(dòng)作,比如頁(yè)面上批量更新操作等。來(lái)看如下的示例代碼:from django.contrib import adminfrom .models import Member, VIPLevel# Register your models here.def make_men(modeladmin, request, queryset): queryset.update(sex=0)make_men.short_description = "全部轉(zhuǎn)成男性"@admin.register(Member)class MemberAdmin(admin.ModelAdmin): actions_on_top = False actions_on_bottom = True date_hierarchy = 'register_date' empty_value_display = '-empty-' list_display = ('name', 'age', 'city', 'sex', 'occupation', 'phone_num') list_editable = ('age', 'sex') list_filter = ('occupation', 'city') ordering = ['-age'] list_per_page = 5 actions = [make_men]@admin.register(VIPLevel)class VIPLevelAdmin(admin.ModelAdmin): pass我們自定義了一個(gè) make_men() 方法,這個(gè)方法會(huì)作為 MemberAdmin 對(duì)象的一個(gè) action 方法。然后給該方法添加 short_description 屬性,最后將該方法添加到 ModelAdmin 對(duì)象的 actions 屬性中去。這樣在模型管理頁(yè)面的 Action 欄會(huì)出現(xiàn)對(duì)應(yīng)的標(biāo)簽,用于實(shí)現(xiàn)相應(yīng)的動(dòng)作,如下圖所示:該 action 方法固定有三個(gè)參數(shù):當(dāng)前的 ModelAdmin 對(duì)象實(shí)例;一個(gè) HttpRequest 實(shí)例,用于表示當(dāng)前的 HTTP 請(qǐng)求;用戶選中記錄對(duì)應(yīng)組成的 QuerySet 實(shí)例;選擇我們定義的 action 方法,然后點(diǎn)擊 Go 按鈕,就可以將所有選中記錄的性別改為男性。操作結(jié)果如下:我們可以看到每個(gè)模型管理頁(yè)面的 Action 欄的下拉框都會(huì)有一個(gè)默認(rèn)的動(dòng)作,就是刪除選中的記錄。如果我們不想要這個(gè)動(dòng)作選項(xiàng),可以在 admin.py 中加上admin.site.disable_action 代碼即可。如果有的模型想要有這個(gè)選項(xiàng),而另一個(gè)模型不想要這個(gè)選項(xiàng),則可以使用如下的方式完成需求:# 關(guān)閉本應(yīng)用模型的刪除選中動(dòng)作選項(xiàng)admin.site.disable_action('delete_selected')# Member模型管理頁(yè)面不會(huì)有刪除選中動(dòng)作選項(xiàng)@admin.register(Member)class MemberAdmin(admin.ModelAdmin): ...# VIPLevel模型管理頁(yè)面則會(huì)有刪除選中動(dòng)作選項(xiàng)class VIPLevelAdmin(admin.ModelAdmin): actions = ['delete_selected', 'a_third_action'] ...最后,如果直接不想要這個(gè) Action 欄,我們直接給模型對(duì)應(yīng)的 ModelAdmin 對(duì)象設(shè)置 action = None 即可,結(jié)果如下:

2. Scrapy 數(shù)據(jù)處理流程

而下面這張圖是非常經(jīng)典的 Scrapy 框架的數(shù)據(jù)處理流程圖,這張數(shù)據(jù)流圖非常重要,它詳細(xì)描述了 Scrapy 框架背后的運(yùn)行流程。按照?qǐng)D中的序號(hào),我來(lái)給大家描述下 Scrapy 框架中的數(shù)據(jù)處理流程:Scrapy框架數(shù)據(jù)流圖Spider 構(gòu)造 Request 請(qǐng)求并提交給 Scrapy 引擎;這步中存在中間件操作,可以對(duì)請(qǐng)求設(shè)置代理 IP 等后再發(fā)送給引擎;引擎將請(qǐng)求發(fā)生給調(diào)度器,調(diào)度器會(huì)根據(jù)請(qǐng)求中設(shè)置的優(yōu)先級(jí)確定要執(zhí)行的請(qǐng)求順序;引擎從調(diào)度器中獲取將要執(zhí)行的請(qǐng)求;引擎通過下載中間件后,將請(qǐng)求傳給下載器執(zhí)行網(wǎng)頁(yè)下載;得到 HTTP 響應(yīng)結(jié)果并將其封裝成 Response 類,然后經(jīng)過下載中間件處理后傳遞回引擎;引擎接收到請(qǐng)求的響應(yīng)結(jié)果后,通過爬蟲的中間件將其發(fā)送給爬蟲 (Spider) 去處理,對(duì)應(yīng)我們前面案例中的 parse() 方法或者自定義的回調(diào)方法 book_list_parse();爬蟲會(huì)將網(wǎng)頁(yè)數(shù)據(jù)抽取成一條數(shù)據(jù)實(shí)體 (Item) 以及新的請(qǐng)求(比如下一頁(yè)的 URL),再次發(fā)送給引擎處理;引擎將爬蟲獲取到的 Item 發(fā)送給項(xiàng)目管道 (Item Pipelines),在項(xiàng)目管道中我們可以實(shí)現(xiàn)數(shù)據(jù)的持久化,比如保存到 MySQL 數(shù)據(jù)庫(kù)或者 MongoDB 中 。同時(shí),如果有新的請(qǐng)求也會(huì)發(fā)送給引擎,在繼續(xù)從第二步開始重復(fù)執(zhí)行,直到最后調(diào)度器中沒有請(qǐng)求為止。大家將這八個(gè)步驟和我們前面實(shí)現(xiàn)的互動(dòng)出版網(wǎng)數(shù)據(jù)爬取的代碼進(jìn)行實(shí)例分析,是不是會(huì)有更進(jìn)一步的理解呢?這上面的架構(gòu)圖和數(shù)據(jù)流圖對(duì)于我們學(xué)習(xí)和理解 Scrapy 框架都非常重要,大家一定要掌握!

1. Scrapy 的 <code>settings.py</code> 配置

從前面的學(xué)習(xí)中我們知道,settings.py 是 Scrapy 使用 startproject 命令生成的,這里的配置會(huì)默認(rèn)覆蓋 Scrapy 內(nèi)置的配置項(xiàng),這些默認(rèn)的配置項(xiàng)都位于 Scrapy 的 scrapy/settings/default_settings.py 中:Scrapy的默認(rèn)配置文件我們來(lái)看看 default_settings.py 中的一些默認(rèn)配置項(xiàng)。AJAXCRAWL_ENABLED:通用爬取經(jīng)常會(huì)抓取大量的 index 頁(yè)面;AjaxCrawlMiddleware 能幫助我們正確地爬取,AJAXCRAWL_ENABLED 配置正是開啟該中間件的開關(guān)。由于有些性能問題,且對(duì)于特定爬蟲沒有什么意義,該中間默認(rèn)關(guān)閉;自動(dòng)限速擴(kuò)展 (AutoThrottle):這類配置主要是以 Scrapy 爬蟲以及正在抓取網(wǎng)站的負(fù)載來(lái)自動(dòng)優(yōu)化爬取速度。它能自動(dòng)調(diào)整 Scrapy 達(dá)到最佳的爬取速度,使用者無(wú)需自己設(shè)置下載延遲,只要設(shè)置好最大并發(fā)請(qǐng)求數(shù)即可。來(lái)看看有關(guān)該擴(kuò)展的配置項(xiàng):AUTOTHROTTLE_ENABLED = False # 默認(rèn)關(guān)閉 AUTOTHROTTLE_DEBUG = False # 關(guān)閉調(diào)試 AUTOTHROTTLE_MAX_DELAY = 60.0 # 最高下載延遲 AUTOTHROTTLE_START_DELAY = 5.0 # 初始化下載延遲 AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0 # Scrapy 同時(shí)請(qǐng)求目標(biāo)網(wǎng)站的平均請(qǐng)求數(shù)下面四個(gè)配置用于設(shè)置爬蟲自動(dòng)關(guān)閉條件:CLOSESPIDER_TIMEOUT:一個(gè)整數(shù)值,單位為秒。如果一個(gè) spider 在指定的秒數(shù)后仍在運(yùn)行, 它將以 closespider_timeout 的原因被自動(dòng)關(guān)閉。 如果值設(shè)置為0 (或者沒有設(shè)置),spiders 不會(huì)因?yàn)槌瑫r(shí)而關(guān)閉;CLOSESPIDER_ITEMCOUNT:一個(gè)整數(shù)值,指定條目的個(gè)數(shù)。如果 spider 爬取條目數(shù)超過了設(shè)置的值, 并且這些條目通過 item pipelines 傳遞,spider 將會(huì)以 closespider_itemcount 的原因被自動(dòng)關(guān)閉;CLOSESPIDER_PAGECOUNT:一個(gè)整數(shù)值,指定最大的抓取響應(yīng) (reponses) 數(shù)。 如果 spider 抓取數(shù)超過指定的值,則會(huì)以 closespider_pagecount 的原因自動(dòng)關(guān)閉。 如果設(shè)置為0(或者未設(shè)置),spiders不會(huì)因?yàn)樽ト〉捻憫?yīng)數(shù)而關(guān)閉;CLOSESPIDER_ERRORCOUNT:一個(gè)整數(shù)值,指定spider可以接受的最大錯(cuò)誤數(shù)。 如果spider生成多于該數(shù)目的錯(cuò)誤,它將以 closespider_errorcount 的原因關(guān)閉。 如果設(shè)置為0(或者未設(shè)置),spiders不會(huì)因?yàn)榘l(fā)生錯(cuò)誤過多而關(guān)閉;以上四個(gè)參數(shù)在 default_settings.py 中設(shè)置的默認(rèn)值都是0并發(fā)相關(guān),的設(shè)置會(huì)較大影響 Scrapy 爬蟲的性能。下面是默認(rèn)的配置值,其中都已經(jīng)進(jìn)行了詳細(xì)的注釋說明:# pipelines中并發(fā)處理items數(shù)CONCURRENT_ITEMS = 100# scrapy中并發(fā)下載請(qǐng)求數(shù)CONCURRENT_REQUESTS = 16# 對(duì)任何單個(gè)域執(zhí)行的并發(fā)請(qǐng)求的最大數(shù)量CONCURRENT_REQUESTS_PER_DOMAIN = 8# 將對(duì)任何單個(gè)IP執(zhí)行的并發(fā)請(qǐng)求的最大數(shù)量。如果非零CONCURRENT_REQUESTS_PER_IP = 0Cookie相關(guān)配置:# 是否啟用cookiesmiddleware。如果關(guān)閉,cookies將不會(huì)發(fā)送給web serverCOOKIES_ENABLED = True# 如果啟用,Scrapy將記錄所有在request(cookie 請(qǐng)求頭)發(fā)送的cookies及response接收到的cookies(set-cookie接收頭),這也會(huì)間接影響性能,因此默認(rèn)關(guān)閉。COOKIES_DEBUG = False請(qǐng)求深度相關(guān)配置,比如 DEPTH_LIMIT 設(shè)置請(qǐng)求允許的最大深度。如果為 0 ,則表示不受限;DEPTH_STATS_VERBOSE 參數(shù)控制是否收集詳細(xì)的深度統(tǒng)計(jì)信息;如果啟用此選項(xiàng),則在統(tǒng)計(jì)信息中收集每個(gè)深度的請(qǐng)求數(shù)。DEPTH_PRIORITY 參數(shù)用于根據(jù)深度調(diào)整請(qǐng)求優(yōu)先級(jí)。來(lái)看看他們的默認(rèn)設(shè)置:DEPTH_LIMIT = 0DEPTH_STATS_VERBOSE = FalseDEPTH_PRIORITY = 0DNS 相關(guān)配置。DNSCACHE_ENABLED 用于控制是否啟用 DNS 緩存,DNSCACHE_SIZE參數(shù)設(shè)置緩存大小,DNS_TIMEOUT 處理 DNS 查詢超時(shí)時(shí)間;我們來(lái)具體看看 default_settings.py 中的默認(rèn)配置:DNSCACHE_ENABLED = TrueDNSCACHE_SIZE = 10000# 緩存解析器DNS_RESOLVER = 'scrapy.resolver.CachingThreadedResolver'DNS_TIMEOUT = 60下載器相關(guān)。這部分的配置比較多,也是主要影響性能的地方。我們對(duì)一些關(guān)鍵的配置進(jìn)行說明,具體如下:DOWNLOAD_DELAY:下載器在從同一網(wǎng)站下載連續(xù)頁(yè)面之前應(yīng)等待的時(shí)間,通過該配置可以限制爬蟲的爬取速度。此外,該設(shè)置也受RANDOMIZE_DOWNLOAD_DELAY 設(shè)置(默認(rèn)情況下啟用)的影響。DOWNLOAD_TIMEOUT:下載超時(shí)時(shí)間;DOWNLOAD_MAXSIZE:下載器將下載的最大響應(yīng)大?。籇OWNLOAD_HANDLERS_BASE:處理不同類型下載的下載器;DOWNLOAD_FAIL_ON_DATALOSS:數(shù)據(jù)丟失后是否繼續(xù)下載;DOWNLOADER_MIDDLEWARES 和DOWNLOADER_MIDDLEWARES_BASE:分別表示自定義的下載中間件類和默認(rèn)的下載中間件類;DOWNLOADER_STATS:是否啟用下載器統(tǒng)計(jì)信息收集。來(lái)看看 default_settings.py 中的默認(rèn)配置,具體如下:DOWNLOAD_DELAY = 0DOWNLOAD_HANDLERS = {}DOWNLOAD_HANDLERS_BASE = { 'data': 'scrapy.core.downloader.handlers.datauri.DataURIDownloadHandler', 'file': 'scrapy.core.downloader.handlers.file.FileDownloadHandler', 'http': 'scrapy.core.downloader.handlers.http.HTTPDownloadHandler', 'https': 'scrapy.core.downloader.handlers.http.HTTPDownloadHandler', 's3': 'scrapy.core.downloader.handlers.s3.S3DownloadHandler', 'ftp': 'scrapy.core.downloader.handlers.ftp.FTPDownloadHandler',}DOWNLOAD_TIMEOUT = 180 # 3minsDOWNLOAD_MAXSIZE = 1024 * 1024 * 1024 # 1024mDOWNLOAD_WARNSIZE = 32 * 1024 * 1024 # 32mDOWNLOAD_FAIL_ON_DATALOSS = TrueDOWNLOADER = 'scrapy.core.downloader.Downloader'DOWNLOADER_HTTPCLIENTFACTORY = 'scrapy.core.downloader.webclient.ScrapyHTTPClientFactory'DOWNLOADER_CLIENTCONTEXTFACTORY = 'scrapy.core.downloader.contextfactory.ScrapyClientContextFactory'DOWNLOADER_CLIENT_TLS_CIPHERS = 'DEFAULT'# Use highest TLS/SSL protocol version supported by the platform, also allowing negotiation:DOWNLOADER_CLIENT_TLS_METHOD = 'TLS'DOWNLOADER_CLIENT_TLS_VERBOSE_LOGGING = FalseDOWNLOADER_MIDDLEWARES = {}DOWNLOADER_MIDDLEWARES_BASE = { # Engine side 'scrapy.downloadermiddlewares.robotstxt.RobotsTxtMiddleware': 100, 'scrapy.downloadermiddlewares.httpauth.HttpAuthMiddleware': 300, 'scrapy.downloadermiddlewares.downloadtimeout.DownloadTimeoutMiddleware': 350, 'scrapy.downloadermiddlewares.defaultheaders.DefaultHeadersMiddleware': 400, 'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': 500, 'scrapy.downloadermiddlewares.retry.RetryMiddleware': 550, 'scrapy.downloadermiddlewares.ajaxcrawl.AjaxCrawlMiddleware': 560, 'scrapy.downloadermiddlewares.redirect.MetaRefreshMiddleware': 580, 'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 590, 'scrapy.downloadermiddlewares.redirect.RedirectMiddleware': 600, 'scrapy.downloadermiddlewares.cookies.CookiesMiddleware': 700, 'scrapy.downloadermiddlewares.httpproxy.HttpProxyMiddleware': 750, 'scrapy.downloadermiddlewares.stats.DownloaderStats': 850, 'scrapy.downloadermiddlewares.httpcache.HttpCacheMiddleware': 900, # Downloader side}DOWNLOADER_STATS = TrueDUPEFILTER_CLASS:指定去重類;DUPEFILTER_CLASS = 'scrapy.dupefilters.RFPDupeFilter'自定義擴(kuò)展和內(nèi)置擴(kuò)展配置:EXTENSIONS = {}EXTENSIONS_BASE = { 'scrapy.extensions.corestats.CoreStats': 0, 'scrapy.extensions.telnet.TelnetConsole': 0, 'scrapy.extensions.memusage.MemoryUsage': 0, 'scrapy.extensions.memdebug.MemoryDebugger': 0, 'scrapy.extensions.closespider.CloseSpider': 0, 'scrapy.extensions.feedexport.FeedExporter': 0, 'scrapy.extensions.logstats.LogStats': 0, 'scrapy.extensions.spiderstate.SpiderState': 0, 'scrapy.extensions.throttle.AutoThrottle': 0,}文件存儲(chǔ)相關(guān):FILES_STORE_S3_ACL = 'private'FILES_STORE_GCS_ACL = ''FTP 服務(wù)配置, Scrapy 框架內(nèi)置 FTP 下載程序。我們可以指定 FTP 的相關(guān)參數(shù):FTP_USER = 'anonymous'FTP_PASSWORD = 'guest'FTP_PASSIVE_MODE = TrueHTTP 緩存相關(guān)配置。Scrapy 的 HttpCacheMiddleware 組件(默認(rèn)情況下沒有啟用)提供了一個(gè)底層的對(duì)HTTP請(qǐng)求和響應(yīng)的緩存。如果啟用的話(把HTTPCACHE_ENABLED設(shè)置為True),它會(huì)緩存每個(gè)請(qǐng)求和對(duì)應(yīng)的響應(yīng)。來(lái)看看和其相關(guān)的配置和含義:# 是否啟用http緩存HTTPCACHE_ENABLED = False# 緩存數(shù)據(jù)目錄HTTPCACHE_DIR = 'httpcache'HTTPCACHE_IGNORE_MISSING = False# 緩存存儲(chǔ)的插件HTTPCACHE_STORAGE = 'scrapy.extensions.httpcache.FilesystemCacheStorage'# 緩存過期時(shí)間HTTPCACHE_EXPIRATION_SECS = 0HTTPCACHE_ALWAYS_STORE = False# 緩存忽略的Http狀態(tài)碼HTTPCACHE_IGNORE_HTTP_CODES = []HTTPCACHE_IGNORE_SCHEMES = ['file']HTTPCACHE_IGNORE_RESPONSE_CACHE_CONTROLS = []HTTPCACHE_DBM_MODULE = 'dbm'# 設(shè)置緩存策略,DummyPolicy是所有請(qǐng)求均緩存,下次在請(qǐng)求直接訪問原來(lái)的緩存即可HTTPCACHE_POLICY = 'scrapy.extensions.httpcache.DummyPolicy'# 是否啟用緩存數(shù)據(jù)壓縮HTTPCACHE_GZIP = FalseItem 和 Item pipelines相關(guān)配置:# ITEM處理器ITEM_PROCESSOR = 'scrapy.pipelines.ItemPipelineManager'# 自定義的 item pipelinesITEM_PIPELINES = {}ITEM_PIPELINES_BASE = {}日志相關(guān)的配置:# 啟動(dòng)日志功能LOG_ENABLED = True# 日志編碼LOG_ENCODING = 'utf-8'# 日志格式器LOG_FORMATTER = 'scrapy.logformatter.LogFormatter'# 日志格式LOG_FORMAT = '%(asctime)s [%(name)s] %(levelname)s: %(message)s'# 日志時(shí)間格式LOG_DATEFORMAT = '%Y-%m-%d %H:%M:%S'LOG_STDOUT = False# 日志級(jí)別LOG_LEVEL = 'DEBUG'# 指定日志輸出文件LOG_FILE = NoneLOG_SHORT_NAMES = False郵件配置:在 Scrapy 中提供了郵件功能,該功能使用十分簡(jiǎn)便且采用了 Twisted 非阻塞模式,避免了對(duì)爬蟲的影響。我們只需要在 Scrapy 中進(jìn)行簡(jiǎn)單的設(shè)置,就能通過 API 發(fā)送郵件。郵件的默認(rèn)配置項(xiàng)如下:MAIL_HOST = 'localhost'MAIL_PORT = 25MAIL_FROM = 'scrapy@localhost'MAIL_PASS = NoneMAIL_USER = None我們現(xiàn)在可以簡(jiǎn)單的使用下 Scrapy 給我們提供的郵件類,來(lái)利用它給我們自己發(fā)送一封郵件。首先需要找下自己的 qq 郵箱或者其他郵箱,開啟 POP3/SMTP服務(wù),然后我們可以得到一個(gè)授權(quán)碼。這個(gè)就是我們登陸這個(gè)郵箱服務(wù)的密碼。然后我們配置 settings.py 中的相應(yīng)項(xiàng):MAIL_HOST = 'smtp.qq.com'MAIL_PORT = 25MAIL_FROM = '2894577759@qq.com'MAIL_PASS = '你的授權(quán)碼'MAIL_USER = '2894577759@qq.com'接下來(lái)我們?cè)?scrapy shell 中來(lái)調(diào)用相應(yīng)的郵件接口,發(fā)送郵件:(scrapy-test) [root@server china_pub]# scrapy shell --nolog[s] Available Scrapy objects:[s] scrapy scrapy module (contains scrapy.Request, scrapy.Selector, etc)[s] crawler <scrapy.crawler.Crawler object at 0x7f1c3d4e9100>[s] item {}[s] settings <scrapy.settings.Settings object at 0x7f1c3d4e6dc0>[s] Useful shortcuts:[s] fetch(url[, redirect=True]) Fetch URL and update local objects (by default, redirects are followed)[s] fetch(req) Fetch a scrapy.Request and update local objects [s] shelp() Shell help (print this help)[s] view(response) View response in a browser>>> from scrapy.mail import MailSender>>> mailer = MailSender().from_settings(settings)>>> mailer.send(to=["2894577759@qq.com"], subject="這是一個(gè)測(cè)試", body="來(lái)自百度云主機(jī)發(fā)送的一封郵件", cc=["2894577759@qq.com"])<Deferred at 0x7f1c3c4d1c40>調(diào)用 Scrapy 的郵件接口發(fā)送郵件內(nèi)存相關(guān)參數(shù):MEMDEBUG_ENABLED = False # enable memory debuggingMEMDEBUG_NOTIFY = [] # send memory debugging report by mail at engine shutdownMEMUSAGE_CHECK_INTERVAL_SECONDS = 60.0# 是否啟用內(nèi)存使用擴(kuò)展MEMUSAGE_ENABLED = True# 在關(guān)閉Scrapy之前允許的最大內(nèi)存量,為0則不檢查MEMUSAGE_LIMIT_MB = 0# 要達(dá)到內(nèi)存限制時(shí)通知的電子郵件列表MEMUSAGE_NOTIFY_MAIL = []# 在發(fā)送警告電子郵件通知之前,要允許的最大內(nèi)存量(以兆字節(jié)為單位)。如果為零,則不會(huì)產(chǎn)生警告MEMUSAGE_WARNING_MB = 0調(diào)度器相關(guān)配置:# 調(diào)度器類SCHEDULER = 'scrapy.core.scheduler.Scheduler'# 指定調(diào)度器的三種隊(duì)列類SCHEDULER_DISK_QUEUE = 'scrapy.squeues.PickleLifoDiskQueue'SCHEDULER_MEMORY_QUEUE = 'scrapy.squeues.LifoMemoryQueue'SCHEDULER_PRIORITY_QUEUE = 'scrapy.pqueues.ScrapyPriorityQueue'# 正在處理響應(yīng)數(shù)據(jù)的軟限制(以字節(jié)為單位),如果所有正在處理的響應(yīng)的大小總和高于此值,Scrapy不會(huì)處理新的請(qǐng)求SCRAPER_SLOT_MAX_ACTIVE_SIZE = 5000000spider 中間件相關(guān)配置,有我們熟悉的 SPIDER_MIDDLEWARES 和 SPIDER_MIDDLEWARES_BASE,表示自定義的 Spider 中間件和 Scrapy 內(nèi)置的 Spider 中間件;SPIDER_MIDDLEWARES = {}SPIDER_MIDDLEWARES_BASE = { # Engine side 'scrapy.spidermiddlewares.httperror.HttpErrorMiddleware': 50, 'scrapy.spidermiddlewares.offsite.OffsiteMiddleware': 500, 'scrapy.spidermiddlewares.referer.RefererMiddleware': 700, 'scrapy.spidermiddlewares.urllength.UrlLengthMiddleware': 800, 'scrapy.spidermiddlewares.depth.DepthMiddleware': 900, # Spider side}指定模板文件目錄,這個(gè)在使用 scrapy startproject 項(xiàng)目名 命令創(chuàng)建項(xiàng)目時(shí),對(duì)應(yīng)的模板文件所在的目錄:TEMPLATES_DIR = abspath(join(dirname(__file__), '..', 'templates'))USER_AGENT:設(shè)置請(qǐng)求頭的 User-Agent 參數(shù),用來(lái)模擬瀏覽器。我們通常都會(huì)添加一個(gè)瀏覽器的 User-Agent 值,防止爬蟲直接被屏蔽;Scrapy 的大體配置就是這些,還有一些沒有介紹到的參數(shù),課后可以仔細(xì)查看官方文檔進(jìn)行了解。

上傳文件

最后看一看 requests 庫(kù)中如何上傳文件:>>> url = 'https://httpbin.org/post'>>> files = {'file': open('/home/store/shen/start.sh', 'rb')}>>> r = requests.post(url, files=files)>>> r.text'{\n "args": {}, \n "data": "", \n "files": {\n "file": "#!/bin/bash\\n########################################################\\n# author: spyinx (https://blog.csdn.net/qq_40085317) #\\n# email: 2894577759@qq.com #\\n# date: 2020/6/24 #\\n# function: start agent server on CentOS 7.7 #\\n########################################################\\nAGENT_PORT=8765\\n\\n# check the agent process first\\nmain_pid=$(pstree -ap|grep gunicorn|grep -v grep|awk \'NR==1{print}\'|grep -o \\"[0-9]*\\"|awk \'NR==1{print}\')\\nif [ -n \\"$main_pid\\" ]; then\\n echo \\"get the agent server\'s main pid: $main_pid\\"\\n sudo kill -9 $main_pid\\n echo \\"stop the server first\\"\\n sleep 15\\n process_num=$(ps -ef|grep gunicorn|grep -v grep|wc -l)\\n if [ $process_num -ne 0 ]; then\\n echo \\"close agent server failed\\uff0cexit!\\"\\n exit 1\\n fi\\nfi\\n\\n# start agent server\\nmaster_addr=$(cat /etc/hosts | grep `hostname` | awk \'{print $1}\')\\necho \\"start agent server\\"\\ngunicorn -w 4 -b $master_addr:$AGENT_PORT xstore_agent.agent:app --daemon\\nsleep 5\\nprocess_num=$(ps -ef|grep gunicorn|grep -v grep|wc -l)\\nif [ $process_num -eq 0 ]; then\\n echo \\"start agent server failed\\uff0cplease check it!\\"\\n exit 2\\nfi\\necho \\"start agent server success\\uff0cok!\\""\n }, \n "form": {}, \n "headers": {\n "Accept": "*/*", \n "Accept-Encoding": "gzip, deflate", \n "Content-Length": "1356", \n "Content-Type": "multipart/form-data; boundary=565e2040b1d37bad527477863e64ba6c", \n "Host": "httpbin.org", \n "User-Agent": "python-requests/2.24.0", \n "X-Amzn-Trace-Id": "Root=1-5ef49e5f-a02b3e64f58fe4a3ff51fa94"\n }, \n "json": null, \n "origin": "47.115.61.209", \n "url": "https://httpbin.org/post"\n}\n'>>>在 requests 庫(kù)中,只需要將上傳文件參數(shù)傳遞給 post() 方法即可,是不是非常簡(jiǎn)單?另外,我們還可以在請(qǐng)求中添加 cookie 或者在相應(yīng)中獲取相應(yīng)的 cookie 信息。另外,我們還可以使用 requests 的 Session 來(lái)維持會(huì)話,這在有登錄需求的網(wǎng)站獲取數(shù)據(jù)時(shí)會(huì)非常有用:# 創(chuàng)建一個(gè)session對(duì)象,用來(lái)存儲(chǔ)session信息>>> s = requests.session() >>> s.get("http://www.baidu.com") 如果在登錄之后,繼續(xù)使用 session 對(duì)象再請(qǐng)求該網(wǎng)站的其他頁(yè)面的 url,就會(huì)帶著 session 信息去與該網(wǎng)站進(jìn)行交互,模擬登錄后的訪問。

3. Scrapy 源碼初探

看過了 Scrapy 的架構(gòu)和數(shù)據(jù)處理流程,我們來(lái)簡(jiǎn)單了解下 Scrapy 框架的源碼結(jié)構(gòu)。熟悉和理解 Scrapy 框架的源碼,對(duì)于我們?nèi)粘i_發(fā)的好處不言而喻,我總結(jié)了如下三點(diǎn)原因:熟悉掌握 Scrapy 框架用法的最好方式就是閱讀源碼;提升編程能力的最好途徑也是閱讀源碼;此外,Twisted 模塊在 Scrapy 框架中應(yīng)用廣泛,而國(guó)內(nèi)關(guān)于該框架資源十分匱乏,我們可以借助 Scrapy 框架來(lái)完整學(xué)習(xí) Twisted 模塊的使用,體驗(yàn)這樣一個(gè)異步通信機(jī)制帶給我們的性能體驗(yàn);方便問題排錯(cuò)以及后續(xù)基于 Scrapy 的深度定制開發(fā)。只有熟悉了 Scrapy 源碼,我們才能針對(duì) Scrapy 框架進(jìn)行深度定制開發(fā),實(shí)現(xiàn)與我們業(yè)務(wù)相關(guān)的爬蟲框架;另外,熟悉源碼能方便我們?cè)谡{(diào)試 Scrapy 爬蟲時(shí)快速定位為題原因,高效解決問題,這是一個(gè)經(jīng)驗(yàn)豐富的爬蟲工程師必須具備的技能;截止到這篇文章撰寫完成(2020 年 7 月 12 日),Scrapy 最新發(fā)布的版本是 2.2.0 版本。我們從 github 上選擇穩(wěn)定的 scrapy 源碼包,下載下來(lái)后解壓并使用 VScode 軟件打開該源碼目錄。下面就是 Scrapy-2.2.0 的源碼內(nèi)容:Scrapy-2.2.0版本源碼結(jié)構(gòu)我們依次對(duì)這些源碼目錄和文件進(jìn)行簡(jiǎn)單說明,后面我們?cè)趯W(xué)習(xí)中會(huì)逐漸深入源碼去分析 Scrapy 框架的行為以及追蹤爬蟲的執(zhí)行過程。來(lái)依次看看 Scrapy 源碼的目錄結(jié)構(gòu):commands目錄:該目錄下的文件正是 scrapy 所支持的命令。比如我們生成爬蟲項(xiàng)目使用的命令 scrapy startproject xxxx 對(duì)應(yīng)著文件 startproject.py,命令 scrapy shell http://www.baidu.com 對(duì)應(yīng)的執(zhí)行文件為 shell.py。因此,如果我們要追蹤 scrapy 命令的運(yùn)行過程就可以從這里入手跟蹤代碼了;contracts 目錄:定義了若干簡(jiǎn)單 python 文件;core 目錄:非常核心,定義了 scrapy 框架中的核心類與方法;downloadermiddleswares 目錄:下載中間件相關(guān)的代碼;extensions 目錄:定義了一些擴(kuò)展方法,比如 debug.py,telnet.py 等代碼文件;http 目錄:該目錄下定義了 Request 和 Response 類及其相關(guān)的擴(kuò)展類。下節(jié)和下下節(jié)會(huì)詳細(xì)介紹該目錄下的源碼文件;linkextractors 目錄:這里的代碼主要是輔助 scrapy 核心對(duì)網(wǎng)頁(yè)的鏈接進(jìn)行抽取,定義了一系列的抽取類;loader目錄:該目錄下的代碼是 Item Loader,具體信息可以參考源碼下的 docs/topics/loaders.rst 文件進(jìn)行深入了解;pipelines 目錄:和 pipelines 模塊相關(guān),主要對(duì)爬蟲抽取到的 items 數(shù)據(jù)進(jìn)行再次處理;selector 目錄:該目錄下定義了解析網(wǎng)頁(yè)的 Selector,主要是封裝了 parsel 模塊的 Selector 和 SelectorList;settings 目錄:這里定義了操作配置文件的相關(guān)類以及一個(gè)默認(rèn)的配置文件 (default_settings.py);spidermiddlewares 目錄:定義了爬蟲中間件的相關(guān)類與方法,spiders 目錄:定義了爬蟲模塊的核心類;templates 目錄:下面保存了創(chuàng)建 scrapy 項(xiàng)目所需要的一些模板文件;utils 目錄:一些常用的輔助函數(shù);其他文件:一些比較核心的代碼文件都在這里,比如 cmdline.py、crawler.py、shell.py 等。看完這些介紹后有沒有什么感覺?是不是覺得一個(gè)非常流行的 Python 框架也就那樣?當(dāng)然不是,在這里我們只是介紹了它的表面,并沒有深究其內(nèi)部細(xì)節(jié)。任何一個(gè)流行的框架必有其獨(dú)特的優(yōu)勢(shì),且必須代碼精良。Scrapy 框架在簡(jiǎn)潔易用上做的非常不錯(cuò),此外由于其使用 Twisted 作為其異步處理框架,因此基于 Scrapy 框架開發(fā)的爬蟲一般具有不錯(cuò)的性能,加之良好的社區(qū)、文檔和框架生態(tài),終造就了今天的 Scrapy。接下來(lái),我們會(huì)慢慢深入這些目錄去探索 Scrapy 的源碼,最后實(shí)現(xiàn)徹底掌握 Scrapy 框架的目的。

6. 狀態(tài)碼(Status Codes)

服務(wù)器向用戶返回的狀態(tài)碼和提示信息,常見的有以下一些(方括號(hào)中是該狀態(tài)碼對(duì)應(yīng)的 HTTP 動(dòng)詞)。不同的狀態(tài)碼代表著不同的含義,比如以 2 開頭的狀態(tài)碼通常代表服務(wù)器成功響應(yīng),3 開頭的狀態(tài)碼代表發(fā)生了重定性(即跳轉(zhuǎn)到了別的鏈接),4 開頭的狀態(tài)碼通常表示客戶端這邊提供的信息有誤,而 5 開頭的狀態(tài)碼則表示服務(wù)器內(nèi)部出現(xiàn)的錯(cuò)誤。通過返回的狀態(tài)碼,用戶即可判斷請(qǐng)求成功與否,不成功問題在何處。一些常用的狀態(tài)碼列舉如下:200 OK - [GET]:服務(wù)器成功返回用戶請(qǐng)求的數(shù)據(jù)201 CREATED - [POST/PUT/PATCH]:用戶新建或修改數(shù)據(jù)成功。202 Accepted - [*]:表示一個(gè)請(qǐng)求已經(jīng)進(jìn)入后臺(tái)排隊(duì)(異步任務(wù))204 NO CONTENT - [DELETE]:用戶刪除數(shù)據(jù)成功400 INVALID REQUEST - [POST/PUT/PATCH]:用戶發(fā)出的請(qǐng)求有錯(cuò)誤,服務(wù)器沒有進(jìn)行新建或修改數(shù)據(jù)的操作401 Unauthorized - [*]:表示用戶沒有權(quán)限(令牌、用戶名、密碼錯(cuò)誤)403 Forbidden - [*] 表示用戶得到授權(quán)(與401錯(cuò)誤相對(duì)),但是訪問是被禁止的404 NOT FOUND - [*]:用戶發(fā)出的請(qǐng)求針對(duì)的是不存在的記錄,服務(wù)器沒有進(jìn)行操作,該操作是冪等的406 Not Acceptable - [GET]:用戶請(qǐng)求的格式不可得(比如用戶請(qǐng)求JSON格式,但是只有XML格式)410 Gone -[GET]:用戶請(qǐng)求的資源被永久刪除,且不會(huì)再得到的422 Unprocesable entity - [POST/PUT/PATCH]: 當(dāng)創(chuàng)建一個(gè)對(duì)象時(shí),發(fā)生一個(gè)驗(yàn)證錯(cuò)誤500 INTERNAL SERVER ERROR - [*]:服務(wù)器發(fā)生錯(cuò)誤,用戶將無(wú)法判斷發(fā)出的請(qǐng)求是否成功狀態(tài)碼的完全列表參見這里或這里。

1. 課程簡(jiǎn)介

本課程的核心內(nèi)容可以分為三個(gè)部分,分別是需要理解記憶的計(jì)算機(jī)底層基礎(chǔ),后端通用組件以及需要不斷編碼練習(xí)的數(shù)據(jù)結(jié)構(gòu)和算法。計(jì)算機(jī)底層基礎(chǔ)可以包含計(jì)算機(jī)網(wǎng)絡(luò)、操作系統(tǒng)、編譯原理、計(jì)算機(jī)組成原理,后兩者在面試中出現(xiàn)的頻率很少,課程主要關(guān)注網(wǎng)絡(luò)和系統(tǒng)兩個(gè)模塊,計(jì)算機(jī)網(wǎng)絡(luò)模塊主要介紹了常見的 TCP 協(xié)議、HTTP/HTTPS 協(xié)議,操作系統(tǒng)模塊主要介紹了操作系統(tǒng)的進(jìn)程和線程、內(nèi)存管理的頁(yè)面置換算法等高頻題。后端通用組件主要分為存儲(chǔ)持久化數(shù)據(jù)的數(shù)據(jù)庫(kù),存儲(chǔ)臨時(shí)數(shù)據(jù)的緩存以及通信中間件。常用的支持持久化存儲(chǔ)的數(shù)據(jù)庫(kù)有 Oracle Database、SQLite 以及 MySQL,其中 MysQL 已經(jīng)是后端必備的數(shù)據(jù)庫(kù)技能。常用的緩存方案有 memcached、guava cache、Redis,其中 Redis 是目前大型應(yīng)用系統(tǒng)首選的緩存組件,課程介紹了 Redis 的數(shù)據(jù)結(jié)構(gòu)和應(yīng)用問題。通信中間件則介紹了 RabbitMQ 的常見應(yīng)用問題。數(shù)據(jù)結(jié)構(gòu)和算法部分最能提現(xiàn)候選人的編程基本功以及邏輯思考能力,課程主要介紹了最常見的算法案例,例如快排、鏈表、二叉樹、動(dòng)態(tài)規(guī)劃。課程總體上是面向工作 3 年內(nèi)的初中級(jí)程序員以及準(zhǔn)備面試后端崗位的應(yīng)屆生,因?yàn)槠虿⒉荒芎w所有的面試題,候選人需要做到舉一反三,例如能夠通過對(duì)于二叉樹的各種遍歷操作總結(jié)得到解決二叉樹問題的遞歸算法模板,這也是本課程的最終目的。

2. 生成 Spring Boot 項(xiàng)目

打開 Spring Initializr 網(wǎng)址 http://start.spring.io ,根據(jù)我們項(xiàng)目的情況填入以下信息。Spring Initializr 生成 Spring Boot 項(xiàng)目這是第一次接觸 Spring Initializr ,我們來(lái)詳細(xì)了解界面上選項(xiàng)的作用。1. 構(gòu)建方式選擇:此處我們選擇 Maven Project 即可,表示生成的項(xiàng)目使用 Maven 構(gòu)建。當(dāng)然我們也可以發(fā)現(xiàn),Spring Boot 項(xiàng)目亦可采用 Gradle 構(gòu)建,目前 Spring Boot 主流的構(gòu)建方式還是 Maven; 2. 編程語(yǔ)言選擇:此處選擇 Java 即可; 3. Spring Boot 版本選擇: 2.x 版本與 1.x 版本還是有一些區(qū)別的,咱們學(xué)習(xí)肯定是選擇 2.x 新版本。此處雖然選擇了 2.2.6 版本,但是由于 2.2.6 版本剛推出沒多久,國(guó)內(nèi)一些 Maven 倉(cāng)庫(kù)尚不支持。后面我們手工改為 2.2.5 版本,便于使用國(guó)內(nèi) Maven 倉(cāng)庫(kù)快速構(gòu)建項(xiàng)目; 4. 所屬機(jī)構(gòu)設(shè)置:Group 表示項(xiàng)目所屬的機(jī)構(gòu),就是開發(fā)項(xiàng)目的公司或組織。因?yàn)楣究赡軙?huì)重名,所以習(xí)慣上采用倒置的域名作為 Group 的值。例如慕課網(wǎng)的域名是 imooc.com , 此處寫 com.imooc 就行了;5. 項(xiàng)目標(biāo)識(shí)設(shè)置:Artifact 是項(xiàng)目標(biāo)識(shí),用來(lái)區(qū)分項(xiàng)目。此處我們命名為 spring-boot-hello ,注意項(xiàng)目標(biāo)識(shí)習(xí)慣性地采用小寫英文單詞,單詞間加橫杠的形式。比如 Spring Boot 官方提供的很多依賴,都是 spring-boot-starter-xxx 的形式;6. 項(xiàng)目名稱設(shè)置:Name 是項(xiàng)目名稱,保持與 Artifact 一致即可; 7. 默認(rèn)包名設(shè)置:Package name 是默認(rèn)包名,保持默認(rèn)即可; 8. 打包方式選擇:此處選擇將項(xiàng)目打包為 Jar 文件; 9. 添加項(xiàng)目依賴:此處不必修改,我們直接在 pom.xml 中添加依賴更加方便。注意 pom.xml 就是 Maven 的配置文件,可以指定我們項(xiàng)目需要引入的依賴; 10. 生成項(xiàng)目:點(diǎn)擊 Generate 按鈕,即可按我們?cè)O(shè)置的信息生成 Spring Boot 項(xiàng)目了。

2.2 Cookie的特點(diǎn)

Cookie 有如下特點(diǎn):不可跨域名性:這個(gè)是 Cookie 非常重要的一個(gè)特點(diǎn),域名限制。表示 A 網(wǎng)站頒發(fā)的 Cookie 在用戶訪問 B 網(wǎng)站時(shí)并不會(huì)被提交到 B 網(wǎng)站上去,這些都是由 Cookie 規(guī)范確定。此外,W3C 標(biāo)準(zhǔn)還禁止了 JavaScript 讀寫任何不屬于自己網(wǎng)站的 Cookie;時(shí)間限制:Cookie 同樣有著自己的生命周期,其 maxAge 值決定著 Cookie 的有效期,單位為秒。如果maxAge 屬性為正,則表示該 Cookie 會(huì)在 maxAge 秒之后自動(dòng)失效。瀏覽器會(huì)將 maxAge 為正數(shù)的 Cookie持久化,即寫到對(duì)應(yīng)的 Cookie 文件中。無(wú)論客戶關(guān)閉了瀏覽器還是電腦,只要還在 maxAge 秒之前,登錄網(wǎng)站時(shí)該 Cookie 仍然有效;如果 maxAge 為負(fù)數(shù),則表示該 Cookie 僅在本瀏覽器窗口以及本窗口打開的子窗口內(nèi)有效,關(guān)閉窗口后該 Cookie 即失效。maxAge 為負(fù)數(shù)的 Cookie,為臨時(shí)性 Cookie,不會(huì)被持久化,不會(huì)被寫到 Cookie 文件中。Cookie 信息保存在瀏覽器內(nèi)存中,因此關(guān)閉瀏覽器該 Cookie 就消失了。Cookie 默認(rèn)的 maxAge 值為–1;而如果 maxAge 為0,則表示刪除該 Cookie。Cookie 機(jī)制沒有提供刪除 Cookie 的方法,因此通過設(shè)置該 Cookie 即時(shí)失效實(shí)現(xiàn)刪除 Cookie 的效果。失效的 Cookie 會(huì)被瀏覽器從 Cookie 文件或者內(nèi)存中刪除;空間限制:Cookie 只能存儲(chǔ)4-10KB;數(shù)量限制:一般而言,每個(gè)域下最多不能超過50個(gè) Cookie存儲(chǔ)數(shù)據(jù)類型限制:Cookie只能存儲(chǔ)字符串從上面這些特點(diǎn),我們也能發(fā)現(xiàn)使用 Cookie 的一些缺點(diǎn)。使用 Cookie 最大的問題就是安全性,因?yàn)?Cookie 是保存在客戶端的,且每次發(fā)送 HTTP 請(qǐng)求都會(huì)將 Cookie 帶過去。這些都導(dǎo)致 Cookie 很容易泄露出去,如果一旦 Cookie 泄露,且 Cookie 中設(shè)置的時(shí)間較長(zhǎng),那么很可能攻擊者拿著這個(gè) Cookie 就能冒充受害者的身份去做一些事情,最后留下的記錄都是受害者的。然而接下來(lái)的 Session 在某種程度上避免了這一類的問題。

2. 適配器

所謂適配器組件,其本質(zhì)就是運(yùn)用適配器設(shè)計(jì)模式,匹配不兼容的接口規(guī)范。如上圖,調(diào)用者只能識(shí)別接口 2 類型,但是 A 提供的是接口 1 類型。適配器可以把接口 1 轉(zhuǎn)換成接口 2。這樣使用者就能使用 A 提供的功能了。為什么要使用適配器組件?欲解答這個(gè)問題,則先要了解如果不使用適配器組件,則如何編寫用戶控制器。既然稱為用戶控制器,則是開發(fā)者根據(jù)需要在框架外部定義的一個(gè)組件,Spring MVC 不可能未卜先知它的存在。如果要讓 Spring MVC 識(shí)別這個(gè)控制器,有一種方案 :預(yù)先定義好接口,強(qiáng)制性要求開發(fā)者在設(shè)計(jì)控制器時(shí)遵循接口規(guī)范。比如說實(shí)現(xiàn) Controller 接口編寫控制器。@Controllerpublic class HelloAction implements org.springframework.web.servlet.mvc.Controller {public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { return null;}}除此之外,Spriing MVC 提供有更靈活的用戶控制器設(shè)計(jì)方案,可使用 “ 普通 JAVA 類” 充當(dāng)控制器,控制器中的方法也可由開發(fā)者隨性命名。此時(shí),就需要適配器組件把這些不符合規(guī)范的控制器以統(tǒng)一的接口方式告訴給 Spring MVC 。Spring MVC 提供了 3 個(gè)默認(rèn)適配器:org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\ org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\ org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter這些適配對(duì)象都實(shí)現(xiàn)了 HandlerAdapter 接口,此接口就有一個(gè)統(tǒng)一的內(nèi)部調(diào)用方法。@NullableModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;簡(jiǎn)要描述 3 個(gè)適配器的應(yīng)用場(chǎng)景:SimpleControllerHandlerAdapter: 簡(jiǎn)單的控制器處理器適配器,支持實(shí)現(xiàn)了 Controller 接口的控制器;HttpRequestHandlerAdapter: http 請(qǐng)求處理器適配器,要求編寫的控制器時(shí)實(shí)現(xiàn) HttpRequestHandler 接口。此類控制器可以很方便的獲取請(qǐng)求包中的相關(guān)信息。但,真正使用的并不多;RequestMappingHandlerAdapter: 注解處理器適配器,適配使用了注解的用戶控制器。本課程中的就是使用了此適配器,此適配器的實(shí)現(xiàn)比前兩個(gè)都復(fù)雜。因?yàn)橛羞m配器的存在,可以讓控制器的設(shè)計(jì)變得靈活。Tips: 這 3 類適配器都是 Spring MVC 默認(rèn)提供的,可以不用顯示配置,除非有定制需求。

直播
查看課程詳情
微信客服

購(gòu)課補(bǔ)貼
聯(lián)系客服咨詢優(yōu)惠詳情

幫助反饋 APP下載

慕課網(wǎng)APP
您的移動(dòng)學(xué)習(xí)伙伴

公眾號(hào)

掃描二維碼
關(guān)注慕課網(wǎng)微信公眾號(hào)