近年來,隨著 HTML5 和 w3c 的推廣開來,WebSocket 協議被提出,它實現了瀏覽器與服務器的實時通信,使服務端也能主動向客戶端發送數據。在 WebSocket 協議提出之前,開發人員若要實現這些實時性較強的功能,經常會使用一種替代性的解決方案——輪詢。輪詢的原理是采用定時的方式不斷的向服務端發送 HTTP 請求,頻繁地請求數據。明顯地,這種方法命中率較低,浪費服務器資源。伴隨著 WebSocket 協議的推廣,真正實現了 Web 的即時通信。WebSocket 的原理是通過 JavaScript 向服務端發出建立 WebSocket 連接的請求,在 WebSocket 連接建立成功后,客戶端和服務端可以實現一個長連接的網絡管道。因為 WebSocket 本質上是 TCP 連接,它是一個長連接,除非斷開連接否則無需重新創建連接,所以其開銷相對 HTTP 節省了很多。
本小節主要給大家簡單說明了 HTTPS 協議和傳統 HTTP 協議的區別,另外給出了對稱加密和非對稱加密算法的流程,我們需要掌握不同加密算法的特點,在下一章節中會給大家介紹 HTTPS 協議的具體流程。
創建工程:為了區分 xml 工程,坐標名稱換成 spring_an ,其實無所謂,大家自行創建即可。導入依賴:依賴的坐標跟 xml 的工程坐標一致即可,無需導入多余的依賴。<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.0.2.RELEASE</version> </dependency></dependencies>項目代碼:為了測試,在工程內部創建 UserDao 的接口和 UserDao 的實現類 UserDaoImpl。UserDao 代碼如下:public interface UserDao { public void saveUser();}UserDaoImpl 的實現類代碼如下:@Repositorypublic class UserDaoImpl implements UserDao { public void saveUser() { System.out.println("執行dao的保存方法"); }}注意事項: 由于我們是基于注解的方式實現對 bean 的管理,所以在實現類上面需要添加一個注解 @Repository,此注解的作用是為了 Spring 的容器啟動后,需要要自動檢測這些被注解的類并注冊相應的 bean 實例到容器中。Spring 的核心配置文件:<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="com.wyan.dao"></context:component-scan></beans>上面是本案例的配置文件,那么可以看出跟 xml 的配置文件有很大的區別:?配置節點:context-component-scan 標簽,這是 Spring 框架自定義的 xml 標簽,通過 base-package 的屬性,指明需要被自動掃描實例化的類所在位置。如上圖所示,我們在 com.wyan.dao 下的類是需要掃描自動注入容器的。小細節:不是在 com.wyan.dao 下的所有類都會自動注入到容器,而是要搭配注解:比如我們的 @Repository 當然還有其余的注解,我們后面章節會詳細講解。測試類測試結果:代碼解釋:測試類其實跟 xml 的方式一模一樣,我們本次測試的目的一樣也是通過 Spring 容器管理注冊的 bean 對象,只不過對象的實例化方式換成了注解,那么我們看到成功輸出在控制臺的測試語句,說明案例搭建完成。
首先 Scrapy 中關于 Request 相關的源碼位置如下:scrapy 中 Request 相關的代碼可以看到 Request 定義相關的代碼并不多,這也方便我們去學習和探索。先來看 Request 類的定義:# 源碼位置:scrapy/http/request/__init__.pyfrom w3lib.url import safe_url_string# ...class Request(object_ref): def __init__(self, url, callback=None, method='GET', headers=None, body=None, cookies=None, meta=None, encoding='utf-8', priority=0, dont_filter=False, errback=None, flags=None, cb_kwargs=None): self._encoding = encoding # this one has to be set first self.method = str(method).upper() self._set_url(url) self._set_body(body) if not isinstance(priority, int): raise TypeError("Request priority not an integer: %r" % priority) self.priority = priority if callback is not None and not callable(callback): raise TypeError('callback must be a callable, got %s' % type(callback).__name__) if errback is not None and not callable(errback): raise TypeError('errback must be a callable, got %s' % type(errback).__name__) self.callback = callback self.errback = errback self.cookies = cookies or {} self.headers = Headers(headers or {}, encoding=encoding) self.dont_filter = dont_filter self._meta = dict(meta) if meta else None self._cb_kwargs = dict(cb_kwargs) if cb_kwargs else None self.flags = [] if flags is None else list(flags) # ...從上面的源碼中可以看到 Scrapy 框架使用了 w3lib 模塊來完成一些 Web 相關的功能,這里用到了 url 模塊的相關功能。safe_url_string() 方法是將 url 轉成合法的形式,也就是將一些特殊字符比如中文、空格等進行想要的編碼。來看下面的例子:>>> from w3lib.url import safe_url_strin>>> url = "http://www.baidu.com/?xxx= zyz">>> safe_url_string(url)'http://www.baidu.com/?xxx=%20zyz'最后得到的 URL 形式和我們在瀏覽器按下 Enter 鍵時一致。此外,對于 Request 類實例化時可以傳入多種初始屬性,常用的屬性含義如下:url:請求地址;method:請求類型,GET|POST|PUT|DELETE 等;callback: HTTP 請求的回調方法,用于指定該 HTTP 請求的解析響應數據的方法;headers: 設置請求頭。一般而言時設置請求頭的 User-Agent 字段,模擬瀏覽器請求;body: 用于設置請求參數,比如登錄請求需要帶上用戶名/密碼等參數;cookies: 請求 cookies 信息,一般和登錄認證相關,帶上 cookies 用于表明身份信息。熟悉了這個 Request 類后,我們來看一些在 Request 基礎上進一步擴展的請求類。其中一個是 FormRequest:# 源碼位置:scrapy/http/request/form.py# ...class FormRequest(Request): valid_form_methods = ['GET', 'POST'] def __init__(self, *args, **kwargs): formdata = kwargs.pop('formdata', None) if formdata and kwargs.get('method') is None: kwargs['method'] = 'POST' super(FormRequest, self).__init__(*args, **kwargs) if formdata: items = formdata.items() if isinstance(formdata, dict) else formdata querystr = _urlencode(items, self.encoding) if self.method == 'POST': self.headers.setdefault(b'Content-Type', b'application/x-www-form-urlencoded') self._set_body(querystr) else: self._set_url(self.url + ('&' if '?' in self.url else '?') + querystr) # ...FormRequest 類主要用于提交表單請求,比如登錄認證、比如提交訂單等。它只支持 GET 和 POST 請求,且相比 Request 類,FormRequest 類多了一個表單參數屬性,這個是檢查提交表單請求的數據。來分析實例化時對表單參數的處理,代碼如下:if formdata: items = formdata.items() if isinstance(formdata, dict) else formdata querystr = _urlencode(items, self.encoding) if self.method == 'POST': self.headers.setdefault(b'Content-Type', b'application/x-www-form-urlencoded') self._set_body(querystr) else: self._set_url(self.url + ('&' if '?' in self.url else '?') + querystr) # ...def _urlencode(seq, enc): values = [(to_bytes(k, enc), to_bytes(v, enc)) for k, vs in seq for v in (vs if is_listlike(vs) else [vs])] return urlencode(values, doseq=1)這個代碼的邏輯是非常清晰的,如果有表單數據,會分成 GET 和 POST 請求處理:GET 請求:將請求參數添加到 url 后面,用 “?” 連接,參數之間用 “&” 連接;POST 請求:一方面設置請求的 header,另一方面將數據放到 body 體中;還有兩個 JsonRequest 和 XmlRpcRequest 類,都是使用不同的形式來發送 HTTP 請求,我們來看兩個類中非常關鍵的幾行語句:# 源碼位置:scrapy/http/request/json_request.py# ...class JsonRequest(Request): def __init__(self, *args, **kwargs): # ... if body_passed and data_passed: # ... elif not body_passed and data_passed: kwargs['body'] = self._dumps(data) if 'method' not in kwargs: kwargs['method'] = 'POST' super(JsonRequest, self).__init__(*args, **kwargs) self.headers.setdefault('Content-Type', 'application/json') self.headers.setdefault('Accept', 'application/json, text/javascript, */*; q=0.01') # ...這里 JsonRequest 中主要講 data 數據轉成 json 格式,然后保存到 body 屬性中,然后設置了請求頭的 Content-Type 屬性為 “application/json”。# 源碼位置:scrapy/http/request/rpc.pyimport xmlrpc.client as xmlrpclib# ...class XmlRpcRequest(Request): def __init__(self, *args, **kwargs): # ... if 'body' not in kwargs and 'params' in kwargs: kw = dict((k, kwargs.pop(k)) for k in DUMPS_ARGS if k in kwargs) # 關鍵地方 kwargs['body'] = xmlrpclib.dumps(**kw) # ...XmlRpcRequest 用來發送 XML-RPC 請求,關鍵的地方在于請求數據設置,使用了 xmlrpc 模塊。
2.2.1 表單認證的過程說明Spring Security 支持從 HTML 的 Form 表單形式提交登錄用戶信息。表單認證可分為以下步驟:用戶請求受保護資源;Spring Security 的 FilterSecurityInterceptor 對象,檢測到當前用戶認證未通過,應予以拒絕,并拋出 AccessDeniedException;當 AccessDeniedException 被 ExceptionTranslationFilter 接收后,其認定需要發起認證流程,此時用戶被要求登錄,認證服務器將登錄地址(默認由 LoginUrlAuthenticationEntryPoint)返回給客戶端;客戶端瀏覽重定向到登錄頁面;登錄頁面有服務端渲染生成。圖 2 表單登錄流程當用戶提交登錄信息,認證服務器端的 UsernamePasswordAuthenticationFilter 就會被執行。此過程的具體執行過程如下:UsernamePasswordAuthenticationFilter 產生 UsernamePasswordAuthenticationToken,并存入從請求中獲取的用戶名、密碼等信息;創建出的 Token 被傳遞給 AuthenticationManager 用于認證;認證成功或失敗的后續流程同上一小節中關于 AbstractAuthenticationProcessingFilter 的執行過程一致。2.2.2 表單認證的開啟默認情況下,Spring Security 開啟了表單認證功能。如果我們需要顯式配置,可用如下方式實現。創建 Security 配置文件: src/main/java/imooc/springsecurity/usernamepassword/config/WebSecurityConfig.java,并在其中添加 http.formLogin(withDefaults()) 的配置,完整代碼如下:package imooc.springsecurity.usernamepassword.config;import org.springframework.context.annotation.Configuration;import org.springframework.security.config.Customizer;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;@Configurationpublic class WebSecurityConfig extends WebSecurityConfigurerAdapter { protected void configure(HttpSecurity http) throws Exception { http.formLogin(Customizer.withDefaults()); }}訪問 http://localhost:8080/user/me ,網頁會自動跳轉到登錄頁面。登錄頁面輸入默認生成的用戶名 「user」, 默認生成密碼可在控制臺日志中找到。如下圖:提交登錄后,通過認證,我們將在瀏覽器看到當前登錄的用戶名。當前登錄用戶為:「user」2.2.3 表單認證的配置默認情況下,表單登錄的跳轉地址是 /login,登錄參數中用戶名變量名為 username,密碼變量名為 password。如果我們需要修改這些配置信息,可以通過如下方式實現:在 configure(HttpSecurity http) 方法中,為 http 的 formLogin 項修改配置。 protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/login").permitAll() // 表單認證頁面不需要權限 .anyRequest().authenticated(); // 其他頁面需要登錄用戶才能訪問 http.formLogin() .loginPage("/login") // 自定義表單認證頁面地址 .usernameParameter("user") .passwordParameter("pass"); http.csrf().disable(); // 關閉 csrf 以通過認證,注意,這不是最好的做法,后續章節會有介紹。 }當然這一步中配置 /login 頁面需要我們自己去實現。這里有幾個需要注意的地方:自定義表單提交地址為 /login ,提交方法僅支持 POST;表單需要支持 CSRF 票據,即附帶 _csrf 參數;用戶名字段需要命名為 user;密碼字段需要命名為 pass;當認證失敗時,表單頁面會收到 error 參數;當用戶退出成功時,表單頁面會收到 logout 參數。為了測試上述配置,我們創建一個測試登錄頁:新建 src/main/java/imooc/springsecurity/usernamepassword/controller/LoginController.java。package imooc.springsecurity.usernamepassword.controller;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;@Controllerpublic class LoginController { @RequestMapping("/login") public String viewLogin2() { return "/login.html"; }}新建 src/main/resources/templates/login.html<form method="post" action="/login"> <input type="text" name="user"> <input type="password" name="pass"> <input type="submit" value="登錄"></form>訪問測試:http://localhost:8080/user/me ,此時跳轉到我們新建的登錄頁面。登錄頁面輸入用戶名密碼后可看到用戶信息。
定義:protocols () 屬性就是對接口所使用的網絡協議進行一個約定,常用的網絡協議有:http、https。hidden () 屬性就是控制接口在 Swagger 界面中的顯隱性。code () 屬性就是控制接口的返回狀態,常見的有:200,201,404,500 等。使用方法:protocols () 屬性默認值為空,但是 Swagger 在處理時會默認獲取項目所采用的網絡協議,我們可以不用專門設置,如果一定要設置該屬性,則只允許設置 http 協議規定的屬性,不能隨意設置,http, https, ws, wss 這些都是被允許的。code () 屬性一般不用特定設置, Swagger 會自動生成接口返回狀態,這里不再演示。hidden () 屬性允許我們在 Swagger 生成的接口列表界面上控制接口是否需要顯示,默認值為 false,即接口顯示,為 true 時則接口不顯示,如下代碼段所示。@ApiOperation(hidden = true)public CommonResponse<User> userLogin(HttpSession session, @RequestBody User user){ return userService.login(session, user);}代碼解釋:第 1 行,我們在 userLogin 方法的上方使用了 @ApiOperation 注解的 hidden 屬性來隱藏我們的接口。顯示結果:可以看到在接口列表界面,已經看不到我們的用戶登錄接口了,這就是當 hidden 屬性設置為 true 時所起的作用。Tips :接口的顯隱控制應該根據特定安全策略和特定客戶需求來決定顯隱,不能無故隱藏接口,更不能頻繁的切換接口的顯隱。在實際工作中,如果需要隱藏接口則需要和項目組報備情況,說明原因。
分別針對 OrderDao 、 ErpOrderDao 編寫對應的映射文件,然后按照配置類指定的位置,兩個文件分別放到 resources/mapper1 和 resources/mapper2 目錄下。實例:<?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"><!-- 本映射文件對應OrderDao接口 --><mapper namespace="com.imooc.springbootmultidb.mapper1.OrderDao"> <!-- 對應OrderDao中的insert方法 --> <insert id="insert" parameterType="com.imooc.springbootmultidb.mapper1.OrderDo" useGeneratedKeys="true" keyProperty="id"> insert into `order` (goods_id,count) values (#{goodsId},#{count}) </insert></mapper>實例:<?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"><!-- 本映射文件對應ErpOrderDao接口 --><mapper namespace="com.imooc.springbootmultidb.mapper2.ErpOrderDao"> <!-- 對應ErpOrderDao中的insert方法 --> <insert id="insert" parameterType="com.imooc.springbootmultidb.mapper2.ErpOrderDo"> insert into erp_order (out_id,goods_id,count) values (#{outId},#{goodsId},#{count}) </insert></mapper>
Nginx 中的 expires 指令通過控制 HTTP 相應中的" Expires" 和 "Cache-Control"的頭部值,達到控制瀏覽器緩存時間的效果。指令格式如下:Syntax: expires [modified] time;expires epoch | max | off;Default: expires off;Context: http, server, location, if in locationNginx 中的時間單位有s(秒), m(分), h(小), d(天)。指令參數說明:epoch: 指定"Expires"的值為1, 即 January,1970,00:00:01 GMT;max: 指定"Expires"的值為31 December2037 23:59:59GMT, "Cache-Control"的值為10年;-1:指定"Expires"的值為當前服務器時間-1s,即永遠過期;off:不修改"Expires"和"Cache-Control"的值time中出現@表示具體的時間,比如@18h30m表示的是下午6點半;官方的示例如下:expires 24h; # 24小時過期expires modified +24h;expires @24h;expires 0; # 不緩存,立即過期expires -1; # 用不過期expires epoch;expires $expires;
服務器響應完成之后,我們通常會使用 XMLHttpRequest.status 來查看當前 XMLHttpRequest 響應中的數字狀態碼。這個數字狀態碼是一個無符號短整型狀態碼,代表著我們的 Ajax 請求的狀態成功與否。在 XMLHttpRequest 中, status 碼對應著標準的 HTTP 狀態碼。并且在請求完成前,該值為 0。HTTP 狀態碼很多,這里就不做過多的鋪開,具體可以到 HTTP 響應代碼 進行學習和查閱。接下來我們來講幾個常見的狀態碼。是的,這也是很常見的兩個狀態碼。1.2.1 200 和 304 狀態碼在 HTTP 狀態碼中,200 代表著 HTTP 請求成功,而 304 代表著由于瀏覽器緩存原因,GET 請求命中并返回了緩存中的數據。結合 上面 XMLHttpRequest.readyState , 假設請求成功,我們的響應模塊應該如下:xhr.open("GET", "http://localhost:8080/simple/get?mk=慕課網");xhr.send();xhr.onreadystatechange = function() { // 當前 this 為 xhr if (this.readyState == 4) { if (this.status === 200 || this.status === 304) { // code ... } }};在后端設置了協商緩存的情況下,我們來看看效果:第一次請求資源:刷新頁面,進行第二次請求同樣的資源:由于瀏覽器的緩存機制,GET請求有可能會緩存我們的請求內容。上面前后兩次請求中,第一次請求的時候獲取新的內容,返回的是 200 的狀態碼;而第二次再進行獲取,我們就有可能獲取第二圖的結果,使用的是本地緩存。因此,在對 Ajax 成功的判斷中,我們不應該遺漏 304 狀態碼的判斷。1.2.2 404 和 500 狀態碼有正確的返回,那當然也會有錯誤的返回。打個比方,讓我們來假設這樣的場景:客戶端發送一個請求,剛好請求的接口找不到,因為服務端并沒有提供??蛻舳税l送一個請求,服務端內部發生錯誤了。如果遇到這樣的情況,Ajax 當然不能坐以待斃——我們總不該不把任何響應告訴用戶吧!真實的情況是,Ajax 會返回我們相應的 status ,客戶端根據該 status 進行必要的操作。首先,我們來請求一個捏造的接口,即服務端并沒有支持的接口。html 關鍵容器:<div id="container"></div>JavaScript 腳本關鍵代碼:var container = document.getElementById('container')xhr.onreadystatechange = function() { // 當前 this 為 xhr if (this.readyState == 4) { if (this.status === 200 || this.status === 304) { container.innerHTML = "當前狀態碼為: " + this.status; } else { container.innerHTML = "當前錯誤狀態碼為: " + this.status; // 主要看這里,出現非 200 和 304 狀態會在這邊進行顯示 } }};看看運行后的效果圖:404 Not Found,顯而易見,當我們在查詢的時候,服務端找不到對應的資源的時候就會返回該狀態碼,表示你要找的東西沒有,不存在。在我們的實際工作中,我們經常會遇到這樣的錯誤,往往這個時候你就應該警惕:是不是你的請求 url 寫錯了?是不是前后端線上資源不同步?比方說后端還沒上線對應接口而你已經在開始在代碼中請求了。講完 404 狀態碼,我們接下來繼續來看看一個很常見的場景,服務器內部發生錯誤了?。?!代碼沿用上一個示例,接口改為服務端提供的接口,這次我們會在服務端假設發生錯誤,并返回 500 錯誤。來看看請求的結果:事實上,500 錯誤碼也是非常常見的,500 Internal Server Error 代表著服務端錯誤,如果我們在開發過程中遇到這樣的錯誤,那么,就需要后端的同學來查找原因了。除此之外,HTTP 狀態碼還有很多,每個都有不同的含義,這里也不會做過多的展開,有興趣的同學可以做一個額外的學習查閱。HTTP 協議中,狀態碼可以讓我們在請求之后,獲知請求的狀態??蛻舳艘材軌蛞源俗龀鱿鄳捻憫?。
下面我們加上一些高級屬性,可以做出不同尋常的效果:<TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:padding="20dp" android:shadowColor="#5919C9" android:shadowDx="20.0" android:shadowDy="20.0" android:shadowRadius="4.5" android:text="跟著超哥學Android" android:textColor="#BECF0B" android:textSize="40sp" />我們在第一個 demo 的基礎之上加上了陰影效果,如圖:
本章節和大家講解了 Spring MVC 中如何優雅的處理異常。主要有 3 種方案:將異常映射成為 HTTP 狀態碼;使用全局異常處理組件。建議大家使用這種方式,具有很多的隔離性、統一性;使用注解的方式處理異常。
MyBatis 在指定 Java 類時需要使用到類的全路徑,如 com.imooc.mybatis.model.Blog,typeAliases 可以為全路徑定義一個別名,這樣就能減少一定的重復工作。例如,將 com.imooc.mybatis.model.Blog 的別名定義為 Blog:<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration> <typeAliases> <typeAlias type="com.imooc.mybatis.model.Blog" alias="Blog"/> </typeAliases></configuration>MyBatis 還支持為一個包下所有類定義別名:<?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration> <typeAliases> <package name="com.imooc.mybatis.model"/> </typeAliases></configuration>這樣在 com.imooc.mybatis.model 包中的所有類都有了別名,每個類的別名都是其類的名稱首字母小寫,如 Author 類的別名為 author。
目前大家依然比較習慣用 SSL 稱呼,但是 SSL 系列的所有版本都已經棄用了。TLS 可以認為是 SSL 的后續版本,低版本的 TLS 有存在較為明顯的漏洞,所以并不是選擇了 Https 就是決定安全的,建議至少選擇 TLS 1.2 以上版本。近年來各個大廠都不留余力的推動 Https ,騰訊小程序更是要求全站啟用 Https 。在選擇了 Https 的情況下我們要明白一點,Https 與 Http 相比,需要更多的 CPU 資源去計算加密算法的。我們的架構在設計的時候我是比較建議網關對外部分用 Https 算法,站點內部的服務間通信用 Http 或 Rpc 協議。
瀏覽器訪問 http://127.0.0.1:8000/docs/,即可看到自動生成的接口文檔 。注意:視圖集 ViewSet 中的 retrieve 方法,在接口文檔中被稱作 read;參數的 Description 需要在模型類或序列化器類的字段中以 help_text 選項定義,如:class StudentsModel(models.Model): ... s_name = models.CharField(max_length=8, verbose_name='學生姓名', help_text='學生姓名') ...
現在我們的后端 Spring Boot 應用已啟動,前端項目也通過 nginx 啟動起來。我們在瀏覽器地址欄打開 http://x.x.x.x/shop-front/goods.html ,效果如下,說明我們的項目全部部署成功。項目部署成功后頁面顯示效果
布局文件很簡單,直接在根布局中放置一個 Spinner 即可:<?xml version="1.0" encoding="utf-8"?><Spinner xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/spinner" android:layout_width="match_parent" android:layout_height="match_parent" android:gravity="center" />Spinner 的屬性都比較好理解,大家可以在閱讀的同時自行添加嘗試。
Http 協議及其請求過程是用于瀏覽器與后臺服務的數據交互的,選擇一個瀏覽器,筆者下面用 chrome 進行演示。打開瀏覽器;快捷鍵按下 F12;地址欄輸入 http://www.xianlaiwan.cn/;觀察底部 network 欄目的網絡請求信息。
Age 主要記錄的是代理服務器跟原站的響應時間差,如果 Age: 0,它可能只是從原始服務器獲取; 否則它通常是根據代理的當前日期和Date HTTP 響應中包含的通用頭部之間的差異來計算的。
在 db.py 中完成數據庫訪問相關的函數,db.py 分為如下幾個部分:
這里介紹了 Nginx 處理 Http 請求的 11 個階段,并重點介紹了 前三個階段POST_READ、REWRITE以及FIND_CONFIG以及這些階段中涉及到的模塊和指令。前面講到的指令都是 Nginx 中的高頻指令,必須要熟練掌握。
Socket 監聽連接,在沒有連接到來之前一直是阻塞在 serverSocket.accept(); 有請求過來就可以運行到下面的代碼,然后可以根據我們的輸入流讀取信息,根據 Http 協議拆開獲取我們要的請求數據。
在通訊錄的 RESTful 架構中,聯系人被抽象為一種資源,使用 URI 表示如下:http://localhost/users/123每個聯系人都有自己的 URI,同時,每個聯系人有一個唯一的 id,/users/123 是 id 為 123 的聯系人對應的 URI。
這個文檔一般我們在日常開發中不需要去動它,這個文檔主要是項目范圍的梯度設置,通過 AndroidStudio 配置的漸變設置將覆蓋此文件中指定的任何設置。# Project-wide Gradle settings.# IDE (e.g. Android Studio) users:# Gradle settings configured through the IDE *will override*# any settings specified in this file.# For more details on how to configure your build environment visit# http://www.gradle.org/docs/current/userguide/build_environment.html# Specifies the JVM arguments used for the daemon process.# The setting is particularly useful for tweaking memory settings.org.gradle.jvmargs=-Xmx1024m# When configured, Gradle will run in incubating parallel mode.# This option should only be used with decoupled projects. More details, visit# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects# org.gradle.parallel=true通過上面的代碼我們看到主要有一句沒有注釋,第 9 行,這句的作用就是設置運行時的最大內存。Tips: 這里分享一個經驗,如果你的電腦編譯項目耗時比較久,我們可以修改這個文件的配置,適當增加編譯時的內存,使 Gradle 獨立運行。筆者親測有效:第11、12行。# Project-wide Gradle settings.# IDE (e.g. Android Studio) users:# Gradle settings configured through the IDE *will override*# any settings specified in this file.# For more details on how to configure your build environment visit# http://www.gradle.org/docs/current/userguide/build_environment.html# Specifies the JVM arguments used for the daemon process.# The setting is particularly useful for tweaking memory settings.org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8# 提高AndroidStudio的并發性,使Gradle獨立運行。org.gradle.parallel=trueorg.gradle.daemon=true
Shape Drawables 可以用來定義一個 View 的外形、顏色、漸變等等屬性,它的最大的有點就是可以根據任意尺寸的 View進行自適應,代碼示例如下:<?xml version="1.0" encoding="UTF-8"?><shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <stroke android:width="10dp" android:color="#FFFFFFFF" /> <gradient android:endColor="#EF3434AB" android:startColor="#FF98df9d" android:angle="45" /> <corners android:bottomRightRadius="10dp" android:bottomLeftRadius="10dp" android:topLeftRadius="5dp" android:topRightRadius="5dp" /></shape>以上代碼分別為背景設置了邊框、漸變、角弧度,編寫完直接作為背景資源設置即可:<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@drawable/myshape" android:orientation="vertical" > <EditText android:id="@+id/editText1" android:layout_width="match_parent" android:layout_height="wrap_content" > </EditText> <RadioGroup android:id="@+id/radioGroup1" android:layout_width="match_parent" android:layout_height="wrap_content" > <RadioButton android:id="@+id/radio0" android:layout_width="wrap_content" android:layout_height="wrap_content" android:checked="true" android:text="@string/celsius" > </RadioButton> <RadioButton android:id="@+id/radio1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/fahrenheit" > </RadioButton> </RadioGroup> <Button android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/calc" android:onClick="myClickHandler"> </Button></LinearLayout>
2.3.1 引入相關模塊#!/usr/bin/python3from flask import Flask, request, session, render_template, redirectimport os, base64import sysapp = Flask(__name__)app.config['SECRET_KEY'] = os.urandom(24)在第 2 行,引入 os 模塊和 base64 模塊,需要使用 os.urandom 和 b64encode 用于生成 CSRF Token。在第 7 行,Flask 程序在使用 Session 時,需要配置 SECRET_KEY。2.3.2 用戶數據庫class User: def __init__(self, name, password, amount): self.name = name self.password = password self.amount = amountusers = [ User('victim', '123', 100), User('hacker', '123', 100)]def findUser(name): for user in users: if user.name == name: return user return Nonedef checkUser(name, password): for user in users: if user.name == name and user.password == password: return user return None在第 1 行,定義類 User 用于描述銀行賬戶信息,包括:姓名、密碼、賬戶余額等屬性。在第 7 行,預定義了兩個用戶:victim 和 hacker,將它們存儲在全局變量 users 中。在第 12 行,定義函數 findUser,在 users 中根據姓名查找 user。在第 18 行,定義函數 checkUser,在 users 中根據姓名和密碼查找 user。2.3.3 首頁面@app.route('/')def index(): hasLogin = session.get('hasLogin') name = session.get('name') user = findUser(name) csrfToken = getCsrfToken() session['csrfToken'] = csrfToken return render_template('index.html', hasLogin = hasLogin, user = user, csrfToken = csrfToken)設置首頁面 / 的處理函數為 index,函數在 session 中查找 hasLogin、name、csrfToken 變量,將它們傳遞給頁面模板 index.html。在第 6 行,調用函數 getCsrfToken() 生成一個隨機的、不可預測的 CSRF Token,并將其存儲在 Session 中。2.3.4 登錄頁面@app.route('/login', methods = ['POST'])def login(): name = request.form['name'] password = request.form['password'] user = checkUser(name, password) if user != None: session['hasLogin'] = True session['name'] = name return redirect('/') else: return '登錄失敗'設置頁面 /login 的處理函數為 login,該函數首先提取請求中的 name 和 password,然后調用 checkUser 在所有的 users 中查找匹配的 User。如果找到了匹配的 User,則設置 Session 中的 hasLogin 為真,調用 redirect(’/’) 讓客戶端瀏覽器重定向到首頁面。2.3.5 退出頁面@app.route('/logout', methods = ['POST'])def logout(): session['hasLogin'] = False session['name'] = None return redirect('/')設置頁面 /logout 的處理函數為 logout,該函數設置 Session 中的 hasLogin 為假,調用 redirect(’/’) 讓客戶端瀏覽器重定向到首頁面。2.3.5 檢查 CSRF 攻擊def getCsrfToken(): return bytes.decode(base64.b64encode(os.urandom(16)))def checkCsrfAttack(): csrfTokenFromRequest = request.form.get('csrfToken') csrfTokenFromSession = session.get('csrfToken') return csrfTokenFromRequest != csrfTokenFromSession函數 getCsrfToken 返回一個隨機的字符串,os.urandom(16) 產生一個包含 16 個字節的 bytes,base64.b64encode 將 bytes 轉換為 base64 編碼的字符串。函數 checkCsrfAttack 檢測 CSRF 攻擊,在第 5 行,從請求的表單中獲取參數 csrfToken,在第 6 行,從 Session 中獲取變量 csrfToken。對兩者進行比較,如果相等,表示此次請求合法;如果不相等,表示此次請求是 CSRF 攻擊。2.3.6 轉賬頁面@app.route('/transfer', methods = ['POST'])def transfer(): if not session.get('hasLogin'): return '請先登錄' if checkFlag and checkCsrfAttack(): print('警告:檢測到 CSRF 攻擊!') return '轉賬失敗' sourceName = session['name'] sourceUser = findUser(sourceName) targetName = request.form['name'] amount = int(request.form['amount']) targetUser = findUser(targetName) if targetUser != None: sourceUser.amount -= amount targetUser.amount += amount return redirect('/') else: return '轉賬失敗'設置頁面 /transfer 的處理函數為 transfer。在第 3 行,如果 Session 中的 hasLogin 變量未假,表示請求來自于未登錄的用戶,返回 ‘轉賬失敗’。在第 6 行,如果 checkFlag 為真并且 checkCsrfAttack 函數檢測到了 CSRF 攻擊,在控制臺打印 CSRF 的警告,返回 ‘轉賬失敗’。如果 checkFlag 為假,程序不檢測 CSRF 攻擊。在第 10 行到第 15 行,獲取來源賬戶,并從轉賬請求中獲取參數:轉賬數量、接受賬戶。在第 18 行和第 19 行,進行轉賬操作,最后調用 redirect(’/’) 讓客戶端瀏覽器重定向到首頁面。2.3.6 設置選項checkFlag = Falseif len(sys.argv) == 2 and sys.argv[1] == 'check': checkFlag = Trueapp.run(debug = True, port = 8888)設置全局變量 checkFlag,如果 checkFlag 為真,程序檢測 CSRF 攻擊;如果 checkFlag 為假,程序不檢測 CSRF 攻擊。
WEB 程序的應用層使用的是 HTTP 協議,HTTP 協議有一個特點,無狀態。所謂無狀態指上一次請求與下一次請求之間是隔離的,沒有內在的聯系。更通俗的講,可理解為一個患有健忘癥的人,只記得當前自己在做什么,不記得自己曾經做過什么,更不會知道自己將來要做什么。HTTP 協議的這種無狀態,最初設計時是從安全角度考慮。但是,在某些應用場景下,如購物車的應用場景下,卻顯得無能為力。購物車中的商品不一定是一次請求下的結果,往往是多次請求下的結果。也就是說,購物車需要保存每一次請求獲取到的數據。顯然,直接使用 HTTP 協議是無法做到的。就需從技術層面上提供解決方案。原生 Servlet 提供了 3 個作用域,可以根據用戶的需要來保存每一次請求過程中產生的數據。請求作用域: 使用 HttpServletRequest 組件存儲的數據可以在每一次的請求周期內存在。 請求結束,數據也將消失;會話作用域: 使用 HttpSession 組件保存的數據能在整個會話生命周期內存在。如購物車就可以保存在會話作用域中;應用程序作用域: 使用 ServletContext 組件保存的數據在整個應用程序生命周期之內存在。Spring MVC 中,把數據保存在請求作用域,或是說在整個請求過程中數據都有效。有 2 種解決方案直接使用 HttpServletRequest 組件;使用 Spriing MVC 提供的高級數據模型組件。
和前幾節的例子一樣,我們僅需要在根布局中防止一個 ExpandableListView 即可,然后設置上相應的屬性,如下:<ExpandableListView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/expandableListView" android:layout_width="match_parent" android:layout_height="match_parent" android:divider="@android:color/darker_gray" android:dividerHeight="0.5dp" android:indicatorLeft="?android:attr/expandableListPreferredItemIndicatorLeft" android:padding="30dp" />
創建代碼倉庫的步驟跟之前一樣,但是要注意的是,這里起名很有講究,格式是http://username.github.io,其中 username 是你 GitHub 的用戶名,這個名字不能出錯,如果出錯,則需要刪除倉庫重新來。
pom.xml 文件配置如下:<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.2.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>cn.cdd</groupId> <artifactId>curator-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>curator-demo</name> <description>curator-demo project for Spring Boot</description> <properties> <java.version>11</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.curator/curator-framework --> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-framework</artifactId> <version>5.1.0</version> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>5.1.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build></project>引入 Curator 的依賴后,我們先來介紹一下 Curator 的 API ,然后再編寫測試用例進行 API 測試。
在 Windows 操作系統中,使用 Python 輸出中文時,可能會出現亂碼的的情況,例如,使用 Windows 自帶的記事本編寫程序 chinese.py,內容如下:10運行該程序,輸出結果如下:C:\>python chinese.py File "chinese.py", line 1SyntaxError: Non-UTF-8 code starting with '\xd6' in file chinese.py on line 1, but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details