1. 創(chuàng)建事務(wù)管理器類package com.offcn.transaction;/** * @Auther: wyan * @Date: 2020-05-26 21:20 * @Description: */import com.offcn.utils.ConnectionUtils;/** * 和事務(wù)管理相關(guān)的工具類,它包含了,開啟事務(wù),提交事務(wù),回滾事務(wù)和釋放連接 */public class TransactionManager { //獲取數(shù)據(jù)庫(kù)連接的工具類 private ConnectionUtils connectionUtils; public void setConnectionUtils(ConnectionUtils connectionUtils) { this.connectionUtils = connectionUtils; } /** * 開啟事務(wù) */ public void beginTransaction(){ try { connectionUtils.getThreadConnection().setAutoCommit(false); }catch (Exception e){ e.printStackTrace(); } } /** * 提交事務(wù) */ public void commit(){ try { connectionUtils.getThreadConnection().commit(); }catch (Exception e){ e.printStackTrace(); } } /** * 回滾事務(wù) */ public void rollback(){ try { connectionUtils.getThreadConnection().rollback(); }catch (Exception e){ e.printStackTrace(); } } /** * 釋放連接 */ public void release(){ try { connectionUtils.getThreadConnection().close();//還回連接池中 connectionUtils.removeConnection(); }catch (Exception e){ e.printStackTrace(); } }}代碼解釋:此工具類主要作用是對(duì)數(shù)據(jù)庫(kù)連接實(shí)現(xiàn)事務(wù)的開啟,提交以及回滾。至于何時(shí)開啟事務(wù),何時(shí)提交事務(wù),何時(shí)回滾事務(wù),那就根據(jù)業(yè)務(wù)場(chǎng)景需要調(diào)用該類的方法即可。2. 創(chuàng)建動(dòng)態(tài)處理器package com.offcn.utils;import com.offcn.service.IAccountService;import com.offcn.transaction.TransactionManager;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;/** * @Auther: wyan * @Date: 2020-05-26 21:08 * @Description: */public class TransactionProxyFactory { //被代理的業(yè)務(wù)類接口 private IAccountService accountService; //提供事務(wù)管理的工具類 private TransactionManager txManager; public void setTxManager(TransactionManager txManager) { this.txManager = txManager; } public final void setAccountService(IAccountService accountService) { this.accountService = accountService; } /** * 獲取Service代理對(duì)象 * @return */ public IAccountService getAccountService() { return (IAccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(), accountService.getClass().getInterfaces(), new InvocationHandler() { /** * 添加事務(wù)的支持 * * @param proxy * @param method * @param args * @return * @throws Throwable */ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // Object rtValue = null; try { //1.開啟事務(wù) txManager.beginTransaction(); //2.執(zhí)行操作 rtValue = method.invoke(accountService, args); //3.提交事務(wù) txManager.commit(); //4.返回結(jié)果 return rtValue; } catch (Exception e) { //5.回滾操作 txManager.rollback(); throw new RuntimeException(e); } finally { //6.釋放連接 txManager.release(); } } }); }}代碼解釋:此類的核心代碼就是 getAccountService 方法,該方法返回代理業(yè)務(wù)類示例,而在代理對(duì)象的 invoke 方法內(nèi)部,實(shí)現(xiàn)對(duì)原始被代理對(duì)象的增強(qiáng)。方法的參數(shù)解釋如下:proxy: 該參數(shù)就是被代理的對(duì)象實(shí)例本身;method: 該參數(shù)是被代理對(duì)象正在執(zhí)行的方法對(duì)象;args: 該參數(shù)是正在訪問的方法參數(shù)對(duì)象。在方法內(nèi)部,method.invoke() 的方法調(diào)用,即表示被代理業(yè)務(wù)類的方法執(zhí)行,我們調(diào)用 txManager 的開啟事務(wù)方法。在 method.invoke() 方法執(zhí)行之后,調(diào)用提交事務(wù)的方法。一旦執(zhí)行過程出現(xiàn)異常,在 catch 代碼塊中調(diào)用事務(wù)回滾的方法。這樣就保證了事務(wù)的原子性,執(zhí)行的任務(wù),要么全部成功,要么全部失敗。最終在 finally 的代碼塊中,調(diào)用釋放連接的方法。3. 配置文件的修改:添加事務(wù)管理的相關(guān)配置,完整配置文件如下:<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- 配置Service --> <bean id="accountService" class="com.offcn.service.impl.AccountServiceImpl"> <!-- 注入dao --> <property name="accountDao" ref="accountDao"></property> </bean> <!--配置Dao對(duì)象--> <bean id="accountDao" class="com.offcn.dao.impl.AccountDaoImpl"> <!-- 注入QueryRunner --> <property name="runner" ref="runner"></property> <!-- 注入ConnectionUtils --> <property name="connectionUtils" ref="connectionUtils"></property> </bean> <!--配置QueryRunner--> <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"></bean> <!-- 配置數(shù)據(jù)源 --> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <!--連接數(shù)據(jù)庫(kù)的必備信息--> <property name="driverClass" value="com.mysql.jdbc.Driver"></property> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/transmoney"></property> <property name="user" value="root"></property> <property name="password" value="root"></property> </bean> <!-- 配置Connection的工具類 ConnectionUtils --> <bean id="connectionUtils" class="com.offcn.utils.ConnectionUtils"> <!-- 注入數(shù)據(jù)源--> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 配置事務(wù)管理器--> <bean id="txManager" class="com.offcn.transaction.TransactionManager"> <!-- 注入ConnectionUtils --> <property name="connectionUtils" ref="connectionUtils"></property> </bean> <!--配置beanfactory--> <bean id="beanFactory" class="com.offcn.utils.TransactionProxyFactory"> <!-- 注入service --> <property name="accountService" ref="accountService"></property> <!-- 注入事務(wù)管理器 --> <property name="txManager" ref="txManager"></property> </bean> <!--配置代理的service--> <bean id="proxyAccountService" factory-bean="beanFactory" factory-method="getAccountService"></bean></beans>4. 測(cè)試類代碼代碼解釋:本測(cè)試代碼發(fā)生一個(gè)小變化,第 23 行的位置,多了一個(gè)注解 @Qualifier 。此注解的作用不知各位是否還記得,如果在 Spring 的容器中,出現(xiàn)多種同類型的 bean ,可以通過此注解指定引入的實(shí)例,所以這里的 注解內(nèi)的字符串 proxyAccountService 表示本 IAccountService 接口引入的實(shí)例為代理對(duì)象。那么為什么要引入代理對(duì)象呢?因?yàn)榇韺?duì)象的方法內(nèi)部已經(jīng)做了增強(qiáng)邏輯,通過 TransactionManager 類實(shí)現(xiàn)對(duì)事務(wù)的開啟,提交和回滾。5. 測(cè)試結(jié)果:為了測(cè)試效果更明顯,我們先把數(shù)據(jù)庫(kù)的數(shù)據(jù)還原為每人各 1000,如圖:執(zhí)行代碼后結(jié)果:當(dāng)然還會(huì)繼續(xù)報(bào)錯(cuò),但是數(shù)據(jù)庫(kù)呢?上次是一個(gè)賬號(hào)減去了 100 塊錢,另外一個(gè)賬號(hào)卻沒有增加錢,這次我們來看看:可以看到:賬號(hào)的金錢依然是原樣,這就說明事務(wù)的控制已經(jīng)生效了,保證了數(shù)據(jù)的一致性。
以上四種本質(zhì)上其實(shí)都是通過實(shí)現(xiàn)OnClickListener接口去監(jiān)聽點(diǎn)擊事件的,除此之外還可以在通過布局文件中添加onClick標(biāo)簽的方式靜態(tài)綁定點(diǎn)擊事件。這種寫法非常少見,在某些場(chǎng)景下可以幫助簡(jiǎn)化很多代碼,但是它不太靈活,大家了解一下即可:首先在 xml 中找到對(duì)應(yīng)的 <Button/> 標(biāo)簽,然后添加onClick屬性:<?xml version="1.0" encoding="utf-8"?><Button xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:onClick="eventHandle" android:text="點(diǎn)擊事件處理"></Button>接著在 Activity 中聲明eventHandle方法,這樣就不需要手動(dòng)獲取 Button 實(shí)例,也不用綁定點(diǎn)擊事件了。(注意eventHandle的方法簽名必須是固定的)package com.emercy.myapplication;import android.app.Activity;import android.os.Bundle;import android.view.View;import android.widget.Button;import android.widget.Toast;public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } // Java代碼中無需綁定,直接實(shí)現(xiàn)處理函數(shù)即可 public void eventHandle(View v) { Toast.makeText(getApplicationContext(), "Button被點(diǎn)擊了", Toast.LENGTH_LONG).show(); }}
點(diǎn)擊“拍照” Button 獲取到當(dāng)前畫面之后,我們可以就可以按照自己的邏輯對(duì)圖片進(jìn)行操作了。比如可以通過 Http 上傳、通過 Socket 發(fā)送給其他設(shè)備、保存到本地、或者傳遞給其他 App 等。本例子中將圖片傳遞給另一個(gè) Activity 專門用于查看圖片,下面是 PhotoActivity 的代碼:package com.emercy.myapplication;import android.app.Activity;import android.net.Uri;import android.os.Bundle;import android.widget.ImageView;import java.io.File;import static com.emercy.myapplication.MainActivity.CAMERA_PATH;public class PhotoActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ImageView img = new ImageView(this); String path = getIntent().getStringExtra(CAMERA_PATH); if (path != null) { img.setImageURI(Uri.fromFile(new File(path))); } setContentView(img); }}通過 Intent 接收?qǐng)D片路徑,然后展示在 ImageView 上,需要注意的是PhotoActivity 中是直接將 ImageView 作為參數(shù)直接設(shè)置給了 SetContentView(),這樣相當(dāng)于布局文件中只有一個(gè) ImageView 的寫法。最后別忘了在 Manifest 當(dāng)中添加權(quán)限: <uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>編譯運(yùn)行效果如下:點(diǎn)擊拍照進(jìn)入 PhotoActivity,展示的就是我們拍照的畫面。
在早期的互聯(lián)網(wǎng)發(fā)展中,瀏覽器制定了不同的標(biāo)準(zhǔn)用于存儲(chǔ)離線數(shù)據(jù),其中比較出名的有微軟 IE 瀏覽器的 userData(單個(gè)頁面可存儲(chǔ) 64 kb)、Adobe 的 flash6 中的 flash-cookies(允許存儲(chǔ) 100kb)、flash8 中的 externalinterface、Google 的 gears,不幸的是這些技術(shù)沒有統(tǒng)一的標(biāo)準(zhǔn),而且只適用于單一的瀏覽器,不能跨平臺(tái),所以沒有收錄在 HTML 標(biāo)準(zhǔn)中。HTML5 之前,Cookie 是唯一在 HTML 標(biāo)準(zhǔn)中用于離線存儲(chǔ)的技術(shù),但是 Cookie 有一些不太友好的特征限制了它的應(yīng)用場(chǎng)景:Cookie 會(huì)被附加在 HTTP 協(xié)議中,每次請(qǐng)求都會(huì)被發(fā)送到服務(wù)器端,增加了不必要的流量損耗Cookie 大小限制在 4kb 左右(不同的瀏覽器有一些區(qū)別),對(duì)于一些復(fù)雜的業(yè)務(wù)場(chǎng)景可能不夠這兩個(gè)缺點(diǎn)在 Localstorage 中得到了有效的解決,下面我們就開始學(xué)習(xí) Localstorage。
由于 HTTP 請(qǐng)求是一種無狀態(tài)的請(qǐng)求,所以服務(wù)器不知道是誰在操作,所以服務(wù)器為用戶創(chuàng)建了指定的 SESSION,主要作用是用來標(biāo)識(shí)并且跟蹤用戶的,當(dāng)某個(gè)用戶向服務(wù)器發(fā)起請(qǐng)求時(shí),服務(wù)器會(huì)首先檢查這個(gè)用戶的請(qǐng)求里是否已包含了一個(gè) SESSION 標(biāo)識(shí)(SESSION ID),如果已包含則說明以前已經(jīng)為此客戶端創(chuàng)建過 SESSION,服務(wù)器就按照 SESSION ID 把這個(gè) SESSION 檢索出來使用,如果用戶的請(qǐng)求不包含 SESSION ID,就會(huì)為該用戶創(chuàng)建一個(gè) SESSION,并且生成一個(gè)與該 SESSION 關(guān)聯(lián)的 SESSION ID。COOKIE 是服務(wù)器在用戶本地機(jī)器上存儲(chǔ)的小段文本并隨每一個(gè)請(qǐng)求發(fā)送至同一個(gè)服務(wù)器,COOKIE 也可以用來保存用戶信息,和 SESSION 相比,COOKIE 是保存在用戶端的(如瀏覽器),下面通過登錄注冊(cè)的例子來說明 SESSION 和 COOKIE 的作用。
一般寫 web 應(yīng)用,會(huì)涉及到很多 html 文件,我們不可能將其全部都放在 Go 文件的字符串里,不方便調(diào)試的同時(shí)也影響代碼維護(hù)。所以我們一般會(huì)直接加載 html 文件。代碼示例:package mainimport ( "net/http" "text/template")func main() { http.HandleFunc("/index", index) //設(shè)置訪問的路由 http.ListenAndServe("127.0.0.1:9300", nil) //設(shè)置監(jiān)聽的端口}func index(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { t, _ := template.ParseFiles("view/index.html")//加載html文件 t.Execute(w, nil)//將文件輸出到瀏覽器 }}目錄結(jié)構(gòu)如下index.html 的代碼如下:<!DOCTYPE html><html><head> <meta charset="utf-8"> <title>Go語言實(shí)戰(zhàn)1</title></head><body> <div> <h3>登錄</h3> <form> <div> <div> <input type="text" id="username" name="username" placeholder="請(qǐng)輸入賬號(hào)"> </div> </div> <div> <div> <input type="password" id="password" name="password" placeholder="請(qǐng)輸入密碼"> </div> </div> <div > <div > <button id="loginbtn" type="button" >登錄</button> </div> </div> </form> </div></body></html>執(zhí)行上述 Go 語言代碼,在瀏覽器中輸入127.0.0.1:9300/index。
大家還記得第 10 節(jié)中,我們通過 GridLayout 實(shí)現(xiàn)了一個(gè)登陸頁面嗎?這一節(jié)我們用 AbsoluteLayout 來實(shí)現(xiàn)一個(gè)簡(jiǎn)單的登陸頁面。首先我們需要一個(gè) TextView 作為“賬號(hào)”提示文案、一個(gè) TextView 作為“密碼”提示文案,再加兩個(gè) EditText 作為輸入框,然后還有一個(gè)確認(rèn)按鍵。最后在添加之后我們對(duì)每個(gè) View 設(shè)置一個(gè)坐標(biāo)即可,代碼如下:<AbsoluteLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_x="38dp" android:layout_y="38dp" android:text="賬號(hào)" /> <EditText android:layout_width="200dp" android:layout_height="wrap_content" android:layout_x="131dp" android:layout_y="17dp" android:width="33dp" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_x="38dp" android:layout_y="66dp" android:text="密碼" /> <EditText android:layout_width="200dp" android:layout_height="wrap_content" android:layout_x="131dp" android:layout_y="40dp" android:width="33dp" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_x="39dp" android:layout_y="109dp" android:text="確認(rèn)" /></AbsoluteLayout>最后的運(yùn)行效果:也許在我的這個(gè)設(shè)備上運(yùn)行效果還行,你可以直接拷貝代碼到自己的設(shè)備或者虛擬機(jī)上運(yùn)行,會(huì)發(fā)現(xiàn)在不同尺寸的手機(jī)上效果差異會(huì)很大。極端情況如果你的屏幕更寬、或者干脆就是一臺(tái)平板,那么我們的內(nèi)容會(huì)全部集中在左側(cè),視覺效果就很差。
Django定義了服務(wù)發(fā)布、路由映射、模板編程、數(shù)據(jù)處理的一整套功能。這也意味著Django模塊之間緊密耦合,開發(fā)者需要學(xué)習(xí)Django自己定義的這一整套技術(shù)。Django是遵循MVC架構(gòu)的Web開發(fā)框架,其主要由以下幾部分組成。管理工具(Management):一套內(nèi)置的創(chuàng)建站點(diǎn)、遷移數(shù)據(jù)、維護(hù)靜態(tài)文件的命令工具;模型(Model):提供數(shù)據(jù)訪問接口和模塊,包括數(shù)據(jù)字段、元數(shù)據(jù)、數(shù)據(jù)關(guān)系等的定義及操作;視圖(View):Django 的視圖層封裝了 HTTP Reques t和 Response 的一系列操作和數(shù)據(jù)流,其主要功能包括URL映射機(jī)制、綁定模板等;模板(Template):是一套 Django 自己的頁面渲染模板語言,用若干內(nèi)置的 tags 和 filters 定義頁面的生成方式;表單(Form):通過內(nèi)置的數(shù)據(jù)類型和控件生成 HTML 表單;管理站(Admin):通過聲明需要管理的 Model,快速生成后臺(tái)數(shù)據(jù)管理網(wǎng)站。
重點(diǎn)先看看這個(gè) handlers 屬性值,它是 DownloadHandlers 類的一個(gè)實(shí)例,對(duì)應(yīng)的代碼位置為:scrapy/core/downloader/handlers/__init__.py 。其類定義如下:# ...class DownloadHandlers: def __init__(self, crawler): self._crawler = crawler self._schemes = {} # stores acceptable schemes on instancing self._handlers = {} # stores instanced handlers for schemes self._notconfigured = {} # remembers failed handlers handlers = without_none_values( crawler.settings.getwithbase('DOWNLOAD_HANDLERS')) for scheme, clspath in handlers.items(): self._schemes[scheme] = clspath self._load_handler(scheme, skip_lazy=True) crawler.signals.connect(self._close, signals.engine_stopped) def _get_handler(self, scheme): """Lazy-load the downloadhandler for a scheme only on the first request for that scheme. """ if scheme in self._handlers: return self._handlers[scheme] if scheme in self._notconfigured: return None if scheme not in self._schemes: self._notconfigured[scheme] = 'no handler available for that scheme' return None return self._load_handler(scheme) def _load_handler(self, scheme, skip_lazy=False): path = self._schemes[scheme] try: dhcls = load_object(path) if skip_lazy and getattr(dhcls, 'lazy', True): return None dh = create_instance( objcls=dhcls, settings=self._crawler.settings, crawler=self._crawler, ) except NotConfigured as ex: self._notconfigured[scheme] = str(ex) return None except Exception as ex: # ... else: self._handlers[scheme] = dh return dh def download_request(self, request, spider): scheme = urlparse_cached(request).scheme handler = self._get_handler(scheme) if not handler: raise NotSupported("Unsupported URL scheme '%s': %s" % (scheme, self._notconfigured[scheme])) return handler.download_request(request, spider) @defer.inlineCallbacks def _close(self, *_a, **_kw): # ...我們分別來解析這個(gè)類中定義的方法,都是非常重要的。此外,每個(gè)方法含義明確,而且代碼精煉,我們對(duì)此一一進(jìn)行說明。首先是 __init__() 方法,我們可以看到一個(gè)核心的語句:handlers = without_none_values(crawler.settings.getwithbase('DOWNLOAD_HANDLERS'))。其中 getwithbase() 方法的定義位于 scapy/settings/__init__.py 文件中,其內(nèi)容如下:# 源碼位置:scrapy/settings/__init__.py# ...class BaseSettings(MutableMapping): # ... def getwithbase(self, name): """Get a composition of a dictionary-like setting and its `_BASE` counterpart. :param name: name of the dictionary-like setting :type name: string """ compbs = BaseSettings() compbs.update(self[name + '_BASE']) compbs.update(self[name]) return compbs # ...從上面的代碼中,可以知道 crawler.settings.getwithbase('DOWNLOAD_HANDLERS') 語句其實(shí)是獲取了 settings.py 配置中的 DOWNLOAD_HANDLERS 和 DOWNLOAD_HANDLERS_BASE 值,并返回包含這兩個(gè)屬性值的 BaseSettings 實(shí)例。我們從默認(rèn)的 scrapy/settings/default_settings.py 中可以看到這兩個(gè)屬性值如下:# 源碼位置:scrapy/settings/default_settings.pyDOWNLOAD_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',}通常我們項(xiàng)目中的下載請(qǐng)求一般是 http 或者 https,對(duì)應(yīng)的都是 scrapy.core.downloader.handlers.http.HTTPDownloadHandler 這個(gè)位置的 handler,從這里也可以看到 scrapy 框架其實(shí)是支持非常多協(xié)議下載的,比如 s3 下載、文件下載以及 ftp下載等。緊接著的兩句就是在設(shè)置 self._schemes 和 self._handlers 的值。其中 self._handlers 會(huì)加載對(duì)應(yīng) Handler 類: def _load_handler(self, scheme, skip_lazy=False): # 獲取協(xié)議對(duì)應(yīng)的handler類路徑,比如 ftp協(xié)議對(duì)應(yīng)著的handler類為 # scrapy.core.downloader.handlers.ftp.FTPDownloadHandler path = self._schemes[scheme] try: # 獲取相應(yīng)的handler類,非字符串形式 dhcls = load_object(path) # 默認(rèn)__init__()方法調(diào)用時(shí)參數(shù)skip_lazy為True,我們需要檢查對(duì)應(yīng)handler類中的lazy屬性值,沒有設(shè)置時(shí)默認(rèn)為True;如果handler類中l(wèi)azy屬性設(shè)置為True或者不設(shè)置,則該handler不會(huì)加入到self._handlers中 if skip_lazy and getattr(dhcls, 'lazy', True): return None # 創(chuàng)建相應(yīng)的handler類實(shí)例,需要的參數(shù)為: dh = create_instance( objcls=dhcls, settings=self._crawler.settings, crawler=self._crawler, ) except NotConfigured as ex: # ... except Exception as ex: # ... else: # 設(shè)置self._handlers self._handlers[scheme] = dh return dh上面的注釋已經(jīng)非常清楚,在眾多 handlers 中,只有 S3DownloadHandler 類中沒有設(shè)置 lazy。所以在 __init__() 方法執(zhí)行完后,self._handlers 中不會(huì)有鍵 “s3” 及其對(duì)應(yīng)的實(shí)例。_get_handler() 就非常明顯了,由于我們?cè)诔跏蓟椒?__init__() 中已經(jīng)得到了 self._handlers,此時(shí)該方法就是根據(jù)傳入的協(xié)議獲取相應(yīng) Handler 類的實(shí)例。 例如傳入的 scheme="http",則返回就是 HTTPDownloadHandler 類的一個(gè)實(shí)例。最后一個(gè) download_request() 方法可以說是下載網(wǎng)頁的核心調(diào)用方法。我們來逐步分析該方法中的語句:scheme = urlparse_cached(request).schemehandler = self._get_handler(scheme)上面兩句是獲取對(duì)應(yīng)的下載請(qǐng)求的 Handler 實(shí)例,比較容易理解。接下來的 return 就是調(diào)用 handler 實(shí)例中的 download_request() 方法按照對(duì)應(yīng)協(xié)議方式下載請(qǐng)求數(shù)據(jù):return handler.download_request(request, spider)每個(gè) handler 類都會(huì)有對(duì)應(yīng)的 download_request() 方法。我們重點(diǎn)看看 http 和 https 協(xié)議對(duì)應(yīng)的 handler 類:# 源碼位置:scrapy/core/downloader/handlers/http.pyfrom scrapy.core.downloader.handlers.http10 import HTTP10DownloadHandlerfrom scrapy.core.downloader.handlers.http11 import ( HTTP11DownloadHandler as HTTPDownloadHandler,)可以看到,這里的 HTTPDownloadHandler 類實(shí)際上是 http11.py 中的 HTTP11DownloadHandler 類。這里面內(nèi)容有點(diǎn)多,我們簡(jiǎn)要地分析下:# 源碼位置:scrapy/core/downloader/handlers/http11.py# ...class HTTP11DownloadHandler: # ... def download_request(self, request, spider): """Return a deferred for the HTTP download""" agent = ScrapyAgent( contextFactory=self._contextFactory, pool=self._pool, maxsize=getattr(spider, 'download_maxsize', self._default_maxsize), warnsize=getattr(spider, 'download_warnsize', self._default_warnsize), fail_on_dataloss=self._fail_on_dataloss, crawler=self._crawler, ) return agent.download_request(request) # ...是不是挺簡(jiǎn)單的兩條語句:得到 agent,然后調(diào)用 agent.download_request() 方法得到請(qǐng)求的結(jié)果?我們繼續(xù)追蹤這個(gè) ScrapyAgent 類:# 源碼位置:scrapy/core/downloader/handlers/http11.py# ...from twisted.web.client import Agent, HTTPConnectionPool, ResponseDone, ResponseFailed, URI# ...class ScrapyAgent: _Agent = Agent # ... def _get_agent(self, request, timeout): from twisted.internet import reactor bindaddress = request.meta.get('bindaddress') or self._bindAddress # 從請(qǐng)求的meta參數(shù)中獲取proxy proxy = request.meta.get('proxy') if proxy: # 有代理的情況 # ... # 沒有代理返回Agent的一個(gè)實(shí)例 return self._Agent( reactor=reactor, contextFactory=self._contextFactory, connectTimeout=timeout, bindAddress=bindaddress, pool=self._pool, ) def download_request(self, request): from twisted.internet import reactor # 從meta中獲取下載超時(shí)時(shí)間 timeout = request.meta.get('download_timeout') or self._connectTimeout # 獲取agent agent = self._get_agent(request, timeout) # request details url = urldefrag(request.url)[0] method = to_bytes(request.method) headers = TxHeaders(request.headers) if isinstance(agent, self._TunnelingAgent): headers.removeHeader(b'Proxy-Authorization') if request.body: bodyproducer = _RequestBodyProducer(request.body) else: bodyproducer = None start_time = time() # 使用agent發(fā)起請(qǐng)求 d = agent.request(method, to_bytes(url, encoding='ascii'), headers, bodyproducer) # set download latency d.addCallback(self._cb_latency, request, start_time) # response body is ready to be consumed d.addCallback(self._cb_bodyready, request) d.addCallback(self._cb_bodydone, request, url) # check download timeout self._timeout_cl = reactor.callLater(timeout, d.cancel) d.addBoth(self._cb_timeout, request, url, timeout) return d看完上面的代碼就應(yīng)該比較清楚了,我們先不考慮代理的相關(guān)代碼。直接看 _get_agent() 方法就是獲取 twisted 模塊中 Agent 的一個(gè)實(shí)例,然后通過這個(gè) agent 去請(qǐng)求網(wǎng)頁 (在 download_request() 方法中完成 ),最后返回的仍然是一個(gè) Deferred 對(duì)象。在 download_request() 中的代碼就和我們?cè)诘谝恍」?jié)中介紹的類似,這里便是 Scrapy 最后完成網(wǎng)頁抓取的地方,就是基于 Twisted 的 web client 部分的方法。其中,最核心的一行語句就是:d = agent.request(method, to_bytes(url, encoding='ascii'), headers, bodyproducer)這個(gè)分析已經(jīng)走到了 Scrapy 下載器的最后一層,在往下就是研究 Twisted 框架的源碼了。同學(xué)們有興趣的話,可以繼續(xù)追蹤下去,對(duì)本節(jié)課而言,追蹤到此已經(jīng)結(jié)束了。我們跳出 handlers,繼續(xù)研究 Downloader 類。
運(yùn)行啟動(dòng)類,從 RabbitMQ 管理界面可以看到已生成指定名稱的隊(duì)列了。RabbitMQ 已生成隊(duì)列此時(shí)我們定義一個(gè)控制器用于發(fā)起測(cè)試,直接使用 rabbitTemplate 發(fā)送消息即可。實(shí)例:@RestControllerpublic class TestController { @Autowired private RabbitTemplate rabbitTemplate; @GetMapping("/test") public void test() { // 發(fā)送消息 參數(shù)分別為:交換機(jī)名稱、路由鍵、消息內(nèi)容 rabbitTemplate.convertAndSend("exchange-topic", "apple", "蘋果來了10斤"); rabbitTemplate.convertAndSend("exchange-topic", "banana", "香蕉來了5斤"); rabbitTemplate.convertAndSend("exchange-topic", "apple.banana", "蘋果來了8斤;香蕉來了20斤"); }}convertAndSend() 方法的第 1 個(gè)參數(shù)表示交換機(jī),第 2 個(gè)參數(shù)表示路由鍵(消息的話題),第 3 個(gè)是消息內(nèi)容。所以第 1 個(gè)消息會(huì)被 apple-queue 接收,第 2 個(gè)消息會(huì)被 banana-queue 接收,第 3 個(gè)消息會(huì)被兩個(gè)隊(duì)列接收。我們啟動(dòng)項(xiàng)目,然后訪問 http://127.0.0.1:8080/test ,控制臺(tái)輸出如下,驗(yàn)證成功。趙五知道:香蕉來了5斤李四知道:蘋果來了10斤趙五知道:蘋果來了8斤;香蕉來了20斤李四知道:蘋果來了8斤;香蕉來了20斤
首先我們?cè)?CentOS7.8 的系統(tǒng)上源碼編譯安裝 redis-5.0 的最新版。按照下列步驟進(jìn)行:安裝 gcc 等編譯工具,下載 redis 5 的最新源碼并解壓:[root@server2 shen]# wget http://download.redis.io/releases/redis-5.0.9.tar.gz--2020-06-27 13:10:23-- http://download.redis.io/releases/redis-5.0.9.tar.gzResolving download.redis.io (download.redis.io)... 109.74.203.151Connecting to download.redis.io (download.redis.io)|109.74.203.151|:80... connected.HTTP request sent, awaiting response... 200 OKLength: 1986574 (1.9M) [application/x-gzip]Saving to: ‘redis-5.0.9.tar.gz’100%[=========================================================================================================================================>] 1,986,574 12.9KB/s in 2m 11s 2020-06-27 13:12:35 (14.8 KB/s) - ‘redis-5.0.9.tar.gz’ saved [1986574/1986574][root@server2 shen]# tar xzf redis-5.0.9.tar.gz進(jìn)入 redis 源碼目錄,直接安裝:[root@server2 shen]# cd redis-5.0.9[root@server2 redis-5.0.9]# make MALLOC=libc PREFIX=/usr/local/redis install添加 redis 命令路徑:[root@server2 redis-5.0.9]# cat /etc/profile...# 添加下面兩行內(nèi)容REDIS_HOME=/usr/local/redisexport PATH=$REDIS_HOME/bin:$PATH[root@server2 redis-5.0.9]# source /etc/profile[root@server2 redis-5.0.9]# which redis-cli/usr/local/redis/bin/redis-cli添加并修改 redis.conf 配置文件:[root@server2 redis-5.0.9]# mkdir /etc/redis[root@server2 redis-5.0.9]# cp redis.conf /etc/redis[root@server2 redis-5.0.9]# cat /etc/redis# ...# 修改第一處,改為后臺(tái)守護(hù)進(jìn)程方式啟動(dòng)daemonize yes# ...# 修改端口,比如改為6777port 6777# ...# 需要密碼# requirepass foobaredrequirepass spyinx# ...# 允許通過公網(wǎng)訪問該redis# bind 127.0.0.1bind 0.0.0.0加入 systemd 服務(wù),統(tǒng)一管理:[root@server2 redis-5.0.9]# cat /etc/systemd/system/redis.service[Unit]Description=RedisAfter=network.target [Service]Type=forkingExecStart=/usr/local/redis/bin/redis-server /etc/redis/redis.confExecReload=/bin/kill -s HUP $MAINPIDExecStop=/usr/local/redis/bin/redis-cli -p 6777 shutdownPrivateTmp=true [Install]WantedBy=multi-user.target[root@server2 redis-5.0.9]# systemctl start redis[root@server2 redis-5.0.9]# systemctl status redis● redis.service - Redis Loaded: loaded (/etc/systemd/system/redis.service; disabled; vendor preset: disabled) Active: active (running) since Sat 2020-06-27 14:08:44 CST; 3s ago Process: 7080 ExecStart=/usr/local/redis/bin/redis-server /etc/redis/redis.conf (code=exited, status=0/SUCCESS) Main PID: 7081 (redis-server) CGroup: /system.slice/redis.service └─7081 /usr/local/redis/bin/redis-server 0.0.0.0:6777Jun 27 14:08:44 server2 systemd[1]: Starting Redis...Jun 27 14:08:44 server2 redis-server[7080]: 7080:C 27 Jun 2020 14:08:44.938 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0OoJun 27 14:08:44 server2 redis-server[7080]: 7080:C 27 Jun 2020 14:08:44.938 # Redis version=5.0.9, bits=64, commit=00000000, ...startedJun 27 14:08:44 server2 redis-server[7080]: 7080:C 27 Jun 2020 14:08:44.938 # Configuration loadedJun 27 14:08:44 server2 systemd[1]: Started Redis.Hint: Some lines were ellipsized, use -l to show in full.# 設(shè)置開機(jī)啟動(dòng)[root@server2 redis-5.0.9]# systemctl enable redisCreated symlink from /etc/systemd/system/multi-user.target.wants/redis.service to /etc/systemd/system/redis.service.最后測(cè)試下 redis 服務(wù)是否能正常工作:[root@server2 redis-5.0.9]# redis-cli -p 6777127.0.0.1:6777> auth spyinxOK127.0.0.1:6777> ping "hello, world""hello, world"127.0.0.1:6777> set hello worldOK127.0.0.1:6777> get hello"world"127.0.0.1:6777> 完成 redis 服務(wù)搭建后我們就可以開始 redis 服務(wù)的使用了。
發(fā)動(dòng) CSRF 攻擊的步驟如下圖所示:2.2.1 登錄受害者使用用戶名、密碼登錄銀行網(wǎng)站。2.2.2 驗(yàn)證通過銀行網(wǎng)站根據(jù)用戶名、密碼查詢數(shù)據(jù)庫(kù),如果匹配,則在 Session 中記錄用戶已經(jīng)通過身份驗(yàn)證。銀行網(wǎng)站提供了轉(zhuǎn)賬的功能,在實(shí)現(xiàn)轉(zhuǎn)賬功能時(shí),首先在 Session 中檢查是用戶是否是登錄用戶,如果是登錄用戶,則允許執(zhí)行轉(zhuǎn)賬操作;如果不是登錄用戶,則不允許執(zhí)行轉(zhuǎn)賬操作。2.2.3 被誘導(dǎo)攻擊者創(chuàng)辦了一個(gè)惡意網(wǎng)站,提供一些吸引人的內(nèi)容(例如:色情、賭博、盜版小說等信息),誘導(dǎo)受害者訪問惡意網(wǎng)站。2.2.4 惡意 HTML當(dāng)受害者訪問惡意網(wǎng)站時(shí),惡意網(wǎng)站傳輸給受害者一段惡意的 HTML 代碼,惡意的 HTML 代碼向銀行發(fā)起轉(zhuǎn)賬請(qǐng)求,代碼可能如下:<form action='http://www.bank.com/transfer' method='post'> <input type="text" name="name" value="hacker" placeholder="收款賬戶"/> <input type="text" name="amount" value="100" placeholder="轉(zhuǎn)賬數(shù)量"/> <input type="submit" id="submit" value="轉(zhuǎn)賬"> </form><script>var submit = document.getElementById('submit');submit.click();</script>在第 1 行,定義了一個(gè)向銀行網(wǎng)站提出轉(zhuǎn)賬請(qǐng)求的表單,通過 POST 方法向頁面 http://www.bank.com/transfer’ 提出轉(zhuǎn)賬請(qǐng)求。在第 2 行,定義了一個(gè)名稱為 ‘name’、值為 ‘hacker’ 的文本字段,表示轉(zhuǎn)賬的收款賬戶是 ‘hacker’。在第 3 行,定義了一個(gè)名稱為 ‘a(chǎn)mount’、值為 ‘100’ 的文本字段,表示轉(zhuǎn)賬的數(shù)量是 100。在第 8 行和第 9 行,找到提交按鈕,模擬提交按鈕的 click 操作,實(shí)現(xiàn)了自動(dòng)提交表單。即:用戶瀏覽器訪問這段惡意的 HTML 代碼后,會(huì)自動(dòng)向銀行網(wǎng)站發(fā)出轉(zhuǎn)賬 100 元的請(qǐng)求。顯然,這次轉(zhuǎn)賬不是受害者用戶的本意,是攻擊者偽造的用戶請(qǐng)求。2.2.5 轉(zhuǎn)賬受害者的瀏覽器收到惡意的 HTML 后,執(zhí)行惡意的 HTML 中的 Javascript 代碼,自動(dòng)向銀行提出了一個(gè)轉(zhuǎn)賬請(qǐng)求。銀行網(wǎng)站收到轉(zhuǎn)賬請(qǐng)求后進(jìn)行處理,如果受害者沒有退出銀行網(wǎng)站的登錄,此次轉(zhuǎn)賬請(qǐng)求可以通過身份驗(yàn)證,可以正常完成轉(zhuǎn)賬的功能。通過以上的 5 個(gè)步驟,攻擊者成功的挾制了受害者,在當(dāng)前已登錄的銀行網(wǎng)站上執(zhí)行非本意的轉(zhuǎn)賬操作。
開啟 Zookeeper 服務(wù)端,然后啟動(dòng)主類 ZookeeperConfigApplication 的 main 方法,查看控制臺(tái)輸出:>>> CuratorCacheListener 初始化這行輸出說明監(jiān)聽已經(jīng)開啟了,現(xiàn)在就可以訪問 http://localhost:8888/imooc/getAll 來查詢數(shù)據(jù)庫(kù)的數(shù)據(jù)了:[{"id":1,"username":"Java","password":"123","phone":"123","address":"北京"},{"id":2,"username":"Go","password":"321","phone":"321","address":"上海"},{"id":3,"username":"Python","password":"456","phone":"654","address":"深圳"}]訪問成功,接下來我們使用 Zookeeper 命令行客戶端連接 Zookeeper 服務(wù)端查詢節(jié)點(diǎn)數(shù)據(jù):# 查詢節(jié)點(diǎn)數(shù)據(jù)get /imooc/datasource# 打印數(shù)據(jù){"Username":"root","DriverClassName":"com.mysql.cj.jdbc.Driver","Url":"jdbc:mysql://localhost:3306/imooc?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai","Password":"021998"}我們可以看見 /imooc/datasource 節(jié)點(diǎn)已經(jīng)保存的數(shù)據(jù)源信息了。在修改數(shù)據(jù)源信息之前,我們需要在 MySQL新建另一個(gè)數(shù)據(jù) wiki,然后在 wiki 數(shù)據(jù)庫(kù)下新建 imooc 數(shù)據(jù)表:/* Navicat Premium Data Transfer Source Server : localhost Source Server Type : MySQL Source Server Version : 80019 Source Host : localhost:3306 Source Schema : wiki Target Server Type : MySQL Target Server Version : 80019 File Encoding : 65001 Date: 25/09/2020 00:55:14*/SET NAMES utf8mb4;SET FOREIGN_KEY_CHECKS = 0;-- ------------------------------ Table structure for imooc-- ----------------------------DROP TABLE IF EXISTS `imooc`;CREATE TABLE `imooc` ( `id` int(0) NOT NULL AUTO_INCREMENT, `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `phone` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `address` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;-- ------------------------------ Records of imooc-- ----------------------------INSERT INTO `imooc` VALUES (1, 'C', '789', '987', '北京');INSERT INTO `imooc` VALUES (2, 'C#', '567', '765', '上海');INSERT INTO `imooc` VALUES (3, 'C++', '654', '456', '深圳');SET FOREIGN_KEY_CHECKS = 1;完成上面的操作后,我們就可以修改數(shù)據(jù)源信息了,使用 set 命令修改 data :# 修改 imooc 數(shù)據(jù)庫(kù)為 wiki 數(shù)據(jù)庫(kù)set /imooc/datasource {"Username":"root","DriverClassName":"com.mysql.cj.jdbc.Driver","Url":"jdbc:mysql://localhost:3306/wiki?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai","Password":"021998"}執(zhí)行修改命令后,我我們查看控制臺(tái)輸出:{dataSource-0} restart{dataSource-0} closing ...{dataSource-0} closed{dataSource-1} inited>>> 從配置中心更新數(shù)據(jù)源: {"Username":"root","DriverClassName":"com.mysql.cj.jdbc.Driver","Url":"jdbc:mysql://localhost:3306/wiki?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai","Password":"021998"}我們發(fā)現(xiàn),dataSource 重新啟動(dòng)并初始化了。接下來我們?cè)僭L問 http://localhost:8888/imooc/getAll 來查詢數(shù)據(jù)庫(kù)的數(shù)據(jù):[{"id":1,"username":"C","password":"789","phone":"987","address":"北京"},{"id":2,"username":"C#","password":"567","phone":"765","address":"上海"},{"id":3,"username":"C++","password":"654","phone":"456","address":"深圳"}]我們發(fā)現(xiàn)數(shù)據(jù)變成了 wiki 數(shù)據(jù)庫(kù)的信息,說明我們的動(dòng)態(tài)數(shù)據(jù)源配置成功。
JAVA 中的數(shù)據(jù)很多時(shí)候都是以 OOP 的形式存在的,如學(xué)生對(duì)象數(shù)據(jù)、老師對(duì)象數(shù)據(jù)、用戶對(duì)象數(shù)據(jù)……那么,控制器中的方法能不能直接把數(shù)據(jù)以對(duì)象為單位寫入響應(yīng)包后返回給瀏覽器了?如下面的實(shí)例:@RequestMapping("/test02")@ResponseBodypublic User testJson01() { return new User("mk", "123");}打開瀏覽器,在地址欄上輸入:http://localhost:8888/sm-demo/json/test02 。在瀏覽器你將看到如下圖所示結(jié)果:拋異常了。對(duì)于出錯(cuò),大家應(yīng)該有預(yù)感。User 是 JAVA 語言中的類類型,對(duì)于瀏覽器而言對(duì)它的了解是一片空白。但是,為什么前面返回字符串時(shí)卻可以了?那是因?yàn)樽址彩且环N通用類型,瀏覽器沒有不認(rèn)識(shí)的道理,但是,User 類型,瀏覽器只能摸后腦勺了。如果想讓瀏覽器識(shí)別出 User 類型數(shù)據(jù)。想想也簡(jiǎn)單,自己編碼,把對(duì)象數(shù)據(jù)轉(zhuǎn)換成字符串格式。@RequestMapping("/test03")@ResponseBodypublic String testJson03() { User user=new User("mk", "123"); return user.getUserName()+","+user.getUserPassword();}經(jīng)過上面的修改后,瀏覽器中能顯示出數(shù)據(jù)。但是,這里會(huì)有 2 個(gè)問題需要思考一下:如前所述,前后端分離最主要的思想是讓前端承擔(dān)一部分?jǐn)?shù)據(jù)業(yè)務(wù)邏輯。一串沒有特定格式的字符串傳遞給前端,真要交給 JS 處理,你還真不怕 JS 煩心,你叫它如何從中識(shí)別出誰是誰;直接返回值給瀏覽器之前,需要通過手工編碼的方式把 OOP 數(shù)據(jù)格式轉(zhuǎn)換成字符串,這番折騰,勞心勞力。好!先解決第一個(gè)問題。字符串?dāng)?shù)據(jù)類型是非結(jié)構(gòu)化的,但是,可以把它轉(zhuǎn)換成具有特定結(jié)構(gòu)格式的 JSON 字符串。@RequestMapping("/test04")@ResponseBodypublic String testJson04() { User user = new User("mk", "123"); String json = "{\"userName\":\"" + user.getUserName() + "\",\"userPassWord\":" + "\"" + user.getUserPassword() + "\"" + "}"; return json; }打開瀏覽器,地址欄中輸入 http://localhost:8888/sm-demo/json/test04 。瀏覽器中將顯示如下信息:傳遞給瀏覽器的雖然還是字符串,但是是具有特定格式的 JSON 字符串,如果要交給 JS 處理,JS 表示很開心。是的,數(shù)據(jù)格式的問題解決了,但是,編碼的工作量增加了很多。其實(shí),你所想要的結(jié)果,Spring MVC 能輕松幫你實(shí)現(xiàn)。
視圖解析器需要在 Spring MVC 項(xiàng)目中顯示配置,Spring MVC 雖然提供了視圖解析器,但它不可能知道開發(fā)者會(huì)把物理視圖放在哪個(gè)位置,所以,需要通過配置指定物理視圖的真正位置。配置 InternalResourceViewResolver 很簡(jiǎn)單。打開 WebConfig 配置類,在配置類中添加如下代碼;@Beanpublic InternalResourceViewResolver viewResolver() { InternalResourceViewResolver inResolver=new InternalResourceViewResolver(); inResolver.setPrefix("/WEB-INF/jsp/"); inResolver.setSuffix(".jsp"); return inResolver;}解釋一下上面的代碼:@Bean 注解表示此對(duì)象由 Spring 容器創(chuàng)建;inResolver.setPrefix ("/WEB-INF/jsp/") 表示 JSP 頁面視圖所在物理位置;inResolver.setSuffix (".jsp") 表示 JSP 視圖的后綴。Tips : 如果控制器中返回的是 “hello” 字符串,經(jīng)視圖解析器解析后,則認(rèn)為對(duì)應(yīng)的物理視圖是 “/WEB-INF/jsp/hello.jsp”需要保證存在這個(gè)文件,否則瀏覽器上就會(huì)出現(xiàn) 404 錯(cuò)誤。Ok 按要求在項(xiàng)目的 WEB-INF 目錄下創(chuàng)建 jsp 目錄,再在此目錄下創(chuàng)建名為 hello.jsp 文件,并編輯內(nèi)容。再次在瀏覽器中輸入:http://localhost:8888/sm-demo/hello 。你會(huì)看到:Spring MVC 除了支持 JSP 視圖,還支持其它如:freemarker、thymeleaf 等視圖技術(shù)。會(huì)另設(shè)專題講解。
原生 Servlet 開發(fā)中,需要在 web.xml 中注冊(cè)、映射 Servlet 后瀏覽器才能請(qǐng)求到。 基于 Spring MVC 的 WEB 項(xiàng)目,用戶如何在瀏覽器中請(qǐng)求到用戶控制器?使用注解 2 步就可以搞定:在類前面添加 @Controller 注解,此注解的作用是通知 Spring MVC 的上下文對(duì)象(WebApplicationContext), 控制器的創(chuàng)建交給你了;@Controllerpublic class HelloAction { //省略……}在控制器的方法前面添加 @RequestMapping 注解。使用此注解可提供一個(gè)邏輯名向用戶映射此方法。@RequestMapping("/hello")public String hello() { System.out.pirntln("hello"); return "hello";}難道就這么簡(jiǎn)單,是的,要不咱們測(cè)試一下:重新向服務(wù)器發(fā)布 Spring MVC 項(xiàng)目;打開瀏覽器,輸入 http://localhost:8888/sm-demo/hello 。然后,你會(huì)看到:Tips: 404 ?頁面資源沒找到,為什么會(huì)是這個(gè)樣子?哦!那是因?yàn)檫€沒有告訴 Spring MVC 當(dāng)控制器響應(yīng)瀏覽器請(qǐng)求時(shí),如何找到頁面視圖,這是視圖解析器組件的工作。好吧,等會(huì)兒,我們就會(huì)聊到視圖解析器。Tips: 怎么證明控制器被請(qǐng)求到,很簡(jiǎn)單,你將在控制臺(tái)上看到 hello 字符的輸出。
根據(jù)前面的進(jìn)程模型對(duì)比,我們可以看出:Nginx 是輕量級(jí)、支持高并發(fā)、海量請(qǐng)求的 web 服務(wù)器,而 Apache 是重量級(jí)、不支持高并發(fā)的 web 服務(wù)器。Nginx 在處理靜態(tài)文件方面的性能要遠(yuǎn)超于Apache,而且支持壓縮、緩存等配置。 Nginx 最擅長(zhǎng)的是靜態(tài)資源訪問和反向代理。其反向代理模塊也同時(shí)支持 4 層協(xié)議(主要是 TCP 協(xié)議/ UDP 協(xié)議)和七層協(xié)議(HTTP 協(xié)議、WebSocket 協(xié)議、WSGI 協(xié)議等)的反向代理。同時(shí)也可以作為負(fù)載均衡服務(wù)器,也支持 4 層和 7 層的負(fù)載均衡,這些優(yōu)勢(shì)是 Apache 無法比擬的。但是 Apache 在處理動(dòng)態(tài)請(qǐng)求方面有較大優(yōu)勢(shì),比如 rewrite(對(duì) url 重寫)功能。如果網(wǎng)站使用 rewrite 頻繁的情況下,建議用 Apache。此外,根據(jù) Nginx 和 Apache 的進(jìn)程模型,可以看到,Apache 一個(gè)進(jìn)程對(duì)應(yīng)一個(gè)連接請(qǐng)求,而 Nginx 的一個(gè) worker 進(jìn)程可能對(duì)應(yīng)很多個(gè)連接請(qǐng)求,這樣如果 Nginx 的一個(gè)進(jìn)程死掉,會(huì)影響比較多的用戶請(qǐng)求,所以 Apache 相比 Nginx 會(huì)更穩(wěn)定一些。
以上就是 SeekBar 常用的屬性和 API 及回調(diào)的用法,接下來我們一起通過剛剛學(xué)習(xí)的知識(shí)實(shí)現(xiàn)一個(gè)簡(jiǎn)單功能。首先添加一個(gè) SeekBar 并為其添加回調(diào)接口實(shí)時(shí)監(jiān)聽進(jìn)度變化,然后通過將進(jìn)度展示到 TextView 上,完整的布局代碼如下:<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <SeekBar android:id="@+id/seekBar" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center" android:max="100" android:progress="60" android:progressDrawable="@drawable/seekbar_progress" /></RelativeLayout>在以上布局中我們?cè)谄聊恢醒胩砑恿艘粋€(gè) SeekBar 并在其下方添加了一個(gè) TextView 用來展示 SeekBar 當(dāng)前的進(jìn)度。接著編寫 Java 代碼,主要完成兩個(gè)任務(wù):獲取 SeekBar 的實(shí)例注冊(cè) SeekBar 的狀態(tài)監(jiān)聽器,實(shí)時(shí)監(jiān)控 SeekBar 的進(jìn)度變化并通過 Toast 打印當(dāng)前進(jìn)度package com.emercy.myapplication;import android.app.Activity;import android.os.Bundle;import android.widget.SeekBar;import android.widget.Toast;public class MainActivity extends Activity { private SeekBar seekBar; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); seekBar = findViewById(R.id.seekBar); seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { Toast.makeText(MainActivity.this, "當(dāng)前進(jìn)度為 : " + progress, Toast.LENGTH_LONG).show(); } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { } }); }}通過 Toast 觀察,我們?cè)谕献У耐瑫r(shí),可以實(shí)時(shí)監(jiān)聽到 SeekBar 的狀態(tài),效果圖如下:
看到這個(gè)結(jié)果,大家可能對(duì)本篇文章不滿了,說了半天,也沒看出 Druid 好在哪兒啊,為啥還費(fèi)勁將默認(rèn)的 Hikari 更換掉呢。不要著急,我們仔細(xì)看下官方介紹: Druid 在阿里巴巴開源項(xiàng)目官網(wǎng)的描述可以看到, Druid 是為監(jiān)控而生,說明 Druid 最強(qiáng)大的功能實(shí)際上是監(jiān)控,接下來我們就演示下如何實(shí)現(xiàn) Druid 監(jiān)控。添加監(jiān)控相關(guān)的配置類,需要注意的是我們?cè)O(shè)定了監(jiān)控功能的賬號(hào)和密碼。實(shí)例:/** * Druid配置 */@Configurationpublic class DruidConfig { /** * 注冊(cè)servletRegistrationBean */ @Bean public ServletRegistrationBean servletRegistrationBean() { ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*"); servletRegistrationBean.addInitParameter("allow", ""); // 賬號(hào)密碼 servletRegistrationBean.addInitParameter("loginUsername", "imooc"); servletRegistrationBean.addInitParameter("loginPassword", "123456"); servletRegistrationBean.addInitParameter("resetEnable", "true"); return servletRegistrationBean; } /** * 注冊(cè)filterRegistrationBean */ @Bean public FilterRegistrationBean filterRegistrationBean() { FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new WebStatFilter()); // 添加過濾規(guī)則. filterRegistrationBean.addUrlPatterns("/*"); filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"); return filterRegistrationBean; }}此時(shí)打開網(wǎng)址 http://127.0.0.1:8080/druid 即可顯示 Druid 登錄頁面: Druid 登錄頁面我們使用指定的用戶名 imooc 密碼 123456 登錄后,即可查看各類監(jiān)控信息,內(nèi)容還是非常全面的,此處就不再展開介紹了。 Druid 監(jiān)控頁面
Vue 實(shí)例銷毀后調(diào)用。調(diào)用后,Vue 實(shí)例指示的所有東西都會(huì)解綁定,所有的事件監(jiān)聽器會(huì)被移除,所有的子實(shí)例也會(huì)被銷毀。實(shí)例代碼:<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title></head><body> <div id = "app"> {{ name }} <button @click="updateName">更新</button> <button @click="destroy">銷毀</button> </div></body><script src="https://unpkg.com/vue/dist/vue.js"></script><script type = "text/javascript"> var vm = new Vue({ el: '#app', data: { name:'hello !' }, methods : { updateName() { console.log('準(zhǔn)備修改名字啦!') this.name = 'hello 慕課!' }, destroy(){ vm.$destroy() } }, beforeCreate() { // 此時(shí)頁面數(shù)據(jù)未初始化 console.log('beforeCreate:' + this.name) // beforeCreate: undefined }, created() { // 頁面數(shù)據(jù)已經(jīng)初始化 console.log('created:' + this.name) // beforeCreate: hello ! }, beforeMount() { console.log('beforeMount:' + this.name) // beforeCreate: hello ! }, mounted() { console.log('mounted:' + this.name) // beforeCreate: hello ! }, // 點(diǎn)擊更新按鈕后會(huì)先觸發(fā) beforeUpdate beforeUpdate() { console.log('beforeUpdate:' + this.name) // beforeCreate: hello 慕課! }, updated() { console.log('updated:' + this.name) // updated hello 慕課 ! }, // 點(diǎn)擊銷毀按鈕后會(huì)先觸發(fā) beforeDestroy beforeDestroy(){ console.log('beforeDestroy: before destroy') // beforeDestroy: before destroy }, destroyed(){ console.log('destroyed: success') // destroyed: success // 在這之后點(diǎn)擊頁面 更新 按鈕將無任何效果 } });</script></html>
crawl: 使用項(xiàng)目中的 Spider 進(jìn)行爬取,該命令要求輸入啟動(dòng)的 spider 名稱;# 命令格式, 帶的參數(shù)是: [spider名稱](scrapy-test) [root@server china_pub]# scrapy crawl China-Pub-Crawler# ...check:運(yùn)行 contract 檢查;(scrapy-test) [root@server china_pub]# scrapy check----------------------------------------------------------------------Ran 0 contracts in 0.000sOKlist:列出當(dāng)前項(xiàng)目中所有可用的 spider;(scrapy-test) [root@server china_pub]# scrapy listChina-Pub-Crawlergenspider:在當(dāng)前項(xiàng)目中創(chuàng)建 spider。該方法可以使用提前定義好的模板來生成相應(yīng)的 spider,也可以自己創(chuàng)建 spider 源碼文件;我們來看一個(gè)簡(jiǎn)單的示例:(scrapy-test) [root@server scrapy-test]# mkdir genspider(scrapy-test) [root@server scrapy-test]# cd genspider/(scrapy-test) [root@server genspider]# ls# 使用genspider命令創(chuàng)建spider文件(scrapy-test) [root@server genspider]# scrapy genspider qidian_yuepiao www.qidian.comCreated spider 'qidian_yuepiao' using template 'basic' (scrapy-test) [root@server genspider]# lsqidian_yuepiao.py(scrapy-test) [root@server genspider]# cat qidian_yuepiao.py import scrapyclass QidianYuepiaoSpider(scrapy.Spider): name = 'qidian_yuepiao' allowed_domains = ['www.qidian.com'] start_urls = ['http://www.qidian.com/'] def parse(self, response): pass上面的操作可以看到,我們使用 scrapy 的 genspider 生成了 spider 文件,這個(gè) spider 文件內(nèi)定義了 QidianYuepiaoSpider 這個(gè)爬蟲類,爬蟲名稱是我們輸入的第一個(gè)參數(shù),允許的爬取域名和起始的 urls 是根據(jù)第二個(gè)參數(shù)得到的。
在 Nginx 命令行的使用中,有幾個(gè)非常重要的參數(shù),也是在操作 Nginx 時(shí)經(jīng)常用到的:常用參數(shù)作用 -t 測(cè)試 Nginx.conf 文件的語法是否正常 -c 指定 nginx.conf 文件 -s 最重要的操作,不帶 - s 是啟動(dòng),-s reload 是熱加載 -s stop 是停止,-s reopen 是重新打開日志實(shí)例:# 進(jìn)入sbin目錄$ cd /root/nginx/sbin # 啟動(dòng)Nginx$ ./nginx # 檢查nginx.conf$ ./nginx -tc /root/nginx/conf/nginx.conf # 重新加載nginx$ ./nginx -s reload # 停止nginx$ ./nginx -s stop 啟動(dòng) Nginx 后,首先使用 ps -ef | grep nginx 可以查看 Nginx 進(jìn)程是否已經(jīng)啟動(dòng),基于默認(rèn)的配置,我們將看到 2 個(gè) Nginx 的啟動(dòng)進(jìn)程:master 進(jìn)程和 worker 進(jìn)程。這是我們?cè)谇懊嬷v到的 Nginx 的 Master-Worker 機(jī)制,后面會(huì)進(jìn)行詳細(xì)講解。另外,我們可以用命令 netstat -anltp | grep 80,看到 CentOS 上已經(jīng)在監(jiān)聽 80 端口,而這個(gè)監(jiān)聽服務(wù)正是 Nginx。最后可以用瀏覽器或者 curl 命令直接在 CentOS 機(jī)器上檢查 Nginx 服務(wù):$ curl http://localhost當(dāng)出現(xiàn) “Welcome to Nginx!" 這樣的歡迎語句,表明我們的 Nginx 已經(jīng)正常運(yùn)行了。
上面我們已經(jīng)看到了 Splash 的強(qiáng)大之處,而這正是 Scrapy 框架所無法做到的,它只能爬取靜態(tài)的網(wǎng)頁而無法直接爬取經(jīng)過動(dòng)態(tài)渲染的網(wǎng)頁數(shù)據(jù),因此有了這種 Scrapy 和 Splash 的結(jié)合使用的想法,也產(chǎn)生了 Scrapy-Splash 插件。Scrapy-Splash 插件使用的正是Splash HTTP API,因此我們?cè)诰帉憣?duì)應(yīng)的爬蟲程序時(shí)需要啟動(dòng)一個(gè) Splash 服務(wù),然后 scrapy-splash 模塊會(huì)通過調(diào)用 api 的方式將我們需要渲染的網(wǎng)頁以及相應(yīng)的腳本帶過去執(zhí)行,然后拿到渲染后的頁面,再交給 Scrapy 框架去執(zhí)行。我們?nèi)ス倬W(wǎng)看看 Scrapy-Splash 插件的使用:安裝 scrapy-splash 插件:`pip install scrapy-splash`;另外該插件在 Scrapy 中的配置和使用均在 github 上有詳細(xì)的介紹,許多關(guān)于 scrapy-splash 的使用文章內(nèi)容均來源于此,這里就不做過多介紹,我們直接在實(shí)戰(zhàn)中使用即可,至于背后的配置讀取原理,就需要各位去仔細(xì)閱讀插件源碼了。
根據(jù)上面的分析,我們來設(shè)計(jì)相應(yīng)的爬取流程,總體上有如下幾個(gè)步驟:獲取計(jì)算機(jī)圖書下的分類列表,包括對(duì)應(yīng)的 URL。我們可以實(shí)現(xiàn)一個(gè)函數(shù)專門請(qǐng)求分類頁面,然后提取相應(yīng)的 URL 列表:分類列表的HTML元素專門完成一個(gè)函數(shù),讀取計(jì)算機(jī)分類下的圖書列表。通過不斷的分頁查詢將這個(gè)分類下的所有圖書信息全部抓取到。http://product.china-pub.com/cache/browse2/59/{頁號(hào)}_1_59-{分類編號(hào)}_0.html分類編號(hào)不用管,我們會(huì)自行提取 URL。而對(duì)于請(qǐng)求的頁號(hào),可以自行設(shè)定,從1開始請(qǐng)求,每次加1,直到請(qǐng)求的 URL 返回 404 時(shí),表明這個(gè)分類下的圖書列表請(qǐng)求完畢,然后就可以進(jìn)行下一個(gè)分類的請(qǐng)求了。請(qǐng)求不存在的頁號(hào)最后完成一個(gè)簡(jiǎn)單的保存函數(shù),保存采集到的數(shù)據(jù)庫(kù)。這個(gè)就比較簡(jiǎn)單了,我們直接將采集到的圖書信息成批地保存到 MongoDB 數(shù)據(jù)庫(kù)中。
在項(xiàng)目的 WEB-INF 目錄下新建 templates 目錄,并在此目錄下新建名為 hello.html 的文件。Tips: hello.html 雖然擴(kuò)展名是 html。 其實(shí)是遵循 Thymeleaf 模板語法規(guī)范的模板文件。本課程主要講解在 Spring MVC 中如何使用 Thymeleaf , 會(huì)講解一點(diǎn) Thymeleaf 模板語法,但更多的了解需要你移步到 Thymeleaf 的官方網(wǎng)站:https://www.thymeleaf.org/。hello.html 文件的內(nèi)容:<div> <table> <thead> <tr> <th>name</th> <th>password</th> </tr> </thead> <tbody> <tr> <td th:text="${user.userName}"></td> <td th:text="${user.userPassword}"></td> </tr> </tbody> </table></div>Tips: hello.html 文件內(nèi)容和普通的 HTML 頁面沒有多大的區(qū)別,區(qū)別于在 HTML 頁面標(biāo)簽中使用了 Thymeleaf 提供的一些語法元素,如:th:text 用來動(dòng)態(tài)讀取作用域中的數(shù)據(jù)。編寫控制器;@Controller@RequestMapping("/thymeleaf")public class ThymeleafAction { @RequestMapping("/hello") public String thymeleaf(ModelMap map) { User user=new User("mk", "123456"); map.addAttribute("user", user); return "hello"; }}發(fā)布項(xiàng)目、啟動(dòng) tomcat、打開瀏覽器,在瀏覽器中輸入: http://localhost:8888/sm-demo/thymeleaf/hello。Tips: 再聲明一下,為了讓 Thymeleaf 的測(cè)試更干凈,注釋或刪除掉原來配置過的視圖技術(shù)相關(guān)信息。Thymeleaf 的語法元素也稱其為指令,以 th 開頭,如前面用到的 th:text。
1. 創(chuàng)建 maven 工程:pom 文件的 jar 包坐標(biāo)如下:<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.0.2.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.0.2.RELEASE</version> </dependency> <dependency> <groupId>commons-dbutils</groupId> <artifactId>commons-dbutils</artifactId> <version>1.4</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.6</version> </dependency> <dependency> <groupId>c3p0</groupId> <artifactId>c3p0</artifactId> <version>0.9.1.2</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.7</version> </dependency></dependencies>2. 實(shí)體類 Accountpublic class Account implements Serializable { //數(shù)據(jù)id private Integer id; //賬號(hào)編碼 private String accountNum; //賬號(hào)金額 private Float money; //省略get 和set 方法}3. 數(shù)據(jù)庫(kù)連接工具類public class ConnectionUtils { private ThreadLocal<Connection> tl = new ThreadLocal<Connection>(); private DataSource dataSource; public void setDataSource(DataSource dataSource) { this.dataSource = dataSource; } /** * 獲取當(dāng)前線程上的連接 * @return */ public Connection getThreadConnection() { try{ //1.先從ThreadLocal上獲取 Connection conn = tl.get(); //2.判斷當(dāng)前線程上是否有連接 if (conn == null) { //3.從數(shù)據(jù)源中獲取一個(gè)連接,并且存入ThreadLocal中 conn = dataSource.getConnection(); tl.set(conn); } //4.返回當(dāng)前線程上的連接 return conn; }catch (Exception e){ throw new RuntimeException(e); } } /** * 把連接和線程解綁 */ public void removeConnection(){ tl.remove(); }}4. 持久層 dao 和 dao 的 實(shí)現(xiàn)類://dao的接口public interface IAccountDao { /** * 更新 * @param account */ void updateAccount(Account account); /** * 根據(jù)編號(hào)查詢賬戶 */ Account findAccountByNum(String accountNum);}//dao的實(shí)現(xiàn)類public class AccountDaoImpl implements IAccountDao { //dbutil的查詢工具類 private QueryRunner runner; //連接的工具類 private ConnectionUtils connectionUtils; public void setRunner(QueryRunner runner) { this.runner = runner; } public void setConnectionUtils(ConnectionUtils connectionUtils) { this.connectionUtils = connectionUtils; } //修改賬號(hào) public void updateAccount(Account account) { try{ runner.update(connectionUtils.getThreadConnection(),"update account set accountNum=?,money=? where id=?",account.getAccountNum(),account.getMoney(),account.getId()); }catch (Exception e) { throw new RuntimeException(e); } } //根據(jù)賬號(hào)查詢 public Account findAccountByNum(String accountNum) { try{ List<Account> accounts = runner.query(connectionUtils.getThreadConnection(),"select * from account where accountNum = ? ",new BeanListHandler<Account>(Account.class),accountNum); if(accounts == null || accounts.size() == 0){ return null; } if(accounts.size() > 1){ throw new RuntimeException("結(jié)果集不唯一,數(shù)據(jù)有問題"); } return accounts.get(0); }catch (Exception e) { throw new RuntimeException(e); } }}5. 業(yè)務(wù)類 Service 和 Service 的實(shí)現(xiàn)類//業(yè)務(wù)接口public interface IAccountService { /** * 轉(zhuǎn)賬 * @param sourceAccount 轉(zhuǎn)出賬戶名稱 * @param targetAccount 轉(zhuǎn)入賬戶名稱 * @param money 轉(zhuǎn)賬金額 */ void transfer(String sourceAccount, String targetAccount, Integer money);}//業(yè)務(wù)實(shí)現(xiàn)類public class AccountServiceImpl implements IAccountService { //持久層對(duì)象 private IAccountDao accountDao; //省略 set 和 get 方法 //轉(zhuǎn)賬的方法 public void transfer(String sourceAccount, String targetAccount, Integer money) { //查詢?cè)假~戶 Account source = accountDao.findAccountByNum(sourceAccount); //查詢目標(biāo)賬戶 Account target = accountDao.findAccountByNum(targetAccount); //原始賬號(hào)減錢 source.setMoney(source.getMoney()-money); //目標(biāo)賬號(hào)加錢 target.setMoney(target.getMoney()+money); //更新原始賬號(hào) accountDao.updateAccount(source); //更新目標(biāo)賬號(hào) accountDao.updateAccount(target); System.out.println("轉(zhuǎn)賬完畢"); }}6. 事務(wù)管理器類package com.offcn.transaction;/** * @Auther: wyan * @Date: 2020-05-26 21:20 * @Description: */import com.offcn.utils.ConnectionUtils;/** * 和事務(wù)管理相關(guān)的工具類,它包含了,開啟事務(wù),提交事務(wù),回滾事務(wù)和釋放連接 */public class TransactionManager { private ConnectionUtils connectionUtils; public void setConnectionUtils(ConnectionUtils connectionUtils) { this.connectionUtils = connectionUtils; } /** * 開啟事務(wù) */ public void beginTransaction(){ try { System.out.println("開啟事務(wù)"); connectionUtils.getThreadConnection().setAutoCommit(false); }catch (Exception e){ e.printStackTrace(); } } /** * 提交事務(wù) */ public void commit(){ try { System.out.println("提交事務(wù)"); connectionUtils.getThreadConnection().commit(); }catch (Exception e){ e.printStackTrace(); } } /** * 回滾事務(wù) */ public void rollback(){ try { System.out.println("回滾事務(wù)"); connectionUtils.getThreadConnection().rollback(); }catch (Exception e){ e.printStackTrace(); } } /** * 釋放連接 */ public void release(){ try { System.out.println("釋放連接"); connectionUtils.getThreadConnection().close();//還回連接池中 connectionUtils.removeConnection(); }catch (Exception e){ e.printStackTrace(); } }}代碼解釋:此工具類就作為 Spring 使用 AOP 管理事務(wù)的通知類,里面的各個(gè)方法用于配置 Spring 的通知使用。為了測(cè)試效果,在每個(gè)通知方法內(nèi),我們輸出打印了測(cè)試語句。7. 配置文件中添加 AOP 的相關(guān)配置<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- 配置Service --> <bean id="accountService" class="com.offcn.service.impl.AccountServiceImpl"> <property name="accountDao" ref="accountDao"></property> </bean> <!--配置Dao對(duì)象--> <bean id="accountDao" class="com.offcn.dao.impl.AccountDaoImpl"> <property name="runner" ref="runner"></property> <property name="connectionUtils" ref="connectionUtils"></property> </bean> <!--配置QueryRunner--> <bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype"></bean> <!-- 配置數(shù)據(jù)源 --> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <!--連接數(shù)據(jù)庫(kù)的必備信息--> <property name="driverClass" value="com.mysql.jdbc.Driver"></property> <property name="jdbcUrl" value="jdbc:mysql://localhost:3306/transmoney"></property> <property name="user" value="root"></property> <property name="password" value="root"></property> </bean> <!-- 配置Connection的工具類 ConnectionUtils --> <bean id="connectionUtils" class="com.offcn.utils.ConnectionUtils"> <!-- 注入數(shù)據(jù)源--> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 配置事務(wù)管理器--> <bean id="txManager" class="com.offcn.transaction.TransactionManager"> <!-- 注入ConnectionUtils --> <property name="connectionUtils" ref="connectionUtils"></property> </bean> <!-- aop相關(guān)的節(jié)點(diǎn)配置 --> <aop:config> <aop:pointcut expression="execution ( * com.offcn.service.*.*(..))" id="pc"/> <aop:aspect ref="txManager"> <aop:before method="beginTransaction" pointcut-ref="pc"/> <aop:after-returning method="commit" pointcut-ref="pc"/> <aop:after method="release" pointcut-ref="pc"/> <aop:after-throwing method="rollback" pointcut-ref="pc"/> </aop:aspect> </aop:config></beans>配置文件說明:connectionUtils: 是獲取數(shù)據(jù)庫(kù)連接的工具類;dataSource: 采用 c3p0 數(shù)據(jù)源,大家一定要注意數(shù)據(jù)庫(kù)的名稱與賬號(hào)名和密碼;queryRunner: dbutils 第三方框架提供用于執(zhí)行 SQL 語句,操作數(shù)據(jù)庫(kù)的一個(gè)工具類;accountDao 和 accountService: 是我們自定義的業(yè)務(wù)層實(shí)現(xiàn)類和持久層實(shí)現(xiàn)類;aop:config: 此節(jié)點(diǎn)是新增加 AOP 配置,AOP 相關(guān)信息都在這;aop:pointcut: 此節(jié)點(diǎn)是切入點(diǎn),表示哪些類的哪些方法在執(zhí)行的時(shí)候會(huì)應(yīng)用 Spring 配置的通知進(jìn)行增強(qiáng);aop:aspect: 此節(jié)點(diǎn)是配置切面類的節(jié)點(diǎn),在 AOP 介紹的小節(jié)解釋過,它的作用主要就是整合通知和切入點(diǎn)。null 前置、后置、異常、和最終??梢钥吹贸鰜?before 前置通知執(zhí)行的方法是開啟事務(wù), after-returning 成功執(zhí)行的方法是提交事務(wù),after 最終執(zhí)行的方法是釋放連接,after-throwing 出現(xiàn)異常執(zhí)行的方法是回滾。8. 測(cè)試類代碼@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(locations = "classpath:applicationContext.xml")public class AccountServiceTest { @Autowired private IAccountService accountService; @Test public void testTransfer(){ accountService.transfer("622200009999","622200001111",100); }}測(cè)試結(jié)果:執(zhí)行代碼后結(jié)果:可以看到,我們通過在 xml 文件中配置 Spring 的 AOP 相關(guān)配置,就可以實(shí)現(xiàn)對(duì)我們業(yè)務(wù)類中的方法實(shí)現(xiàn)了增強(qiáng),無需自定義對(duì)業(yè)務(wù)類做代理實(shí)現(xiàn)。
在屬性值中使用插值應(yīng)該算是比較常用的,插值使你在屬性值中不僅可以插入值,還可以插入表達(dá)式來計(jì)算。除此之外我們?cè)谇懊娴倪\(yùn)算章節(jié)中,不知道你是否還記得,我們對(duì)兩個(gè)變量使用 / 標(biāo)識(shí)符的時(shí)候,如果你不想對(duì)這兩個(gè)變量進(jìn)行除法運(yùn)算而是進(jìn)行分隔,那么就可以使用插值避免運(yùn)算??梢哉f插值在屬性值中的應(yīng)用很廣泛也很實(shí)用,我們來舉例看下:$one: 20px;$two: 2;$family: "UaTy";div { margin: $one / $two; // 除法運(yùn)算 margin: #{$one} / #{$two}; // 分隔 font-family: "MyFo #{$family}"; // 帶引號(hào)的字符串會(huì)轉(zhuǎn)換為不帶引號(hào) width: calc(100% - $one * 2 *$two); // calc函數(shù)中內(nèi)容會(huì)被當(dāng)作字符串處理 width: calc(100% - #{$one * 2 *$two}); // calc函數(shù)中插值的內(nèi)容會(huì)進(jìn)行運(yùn)算}上面的代碼中我對(duì)每一行都進(jìn)行了標(biāo)注,你要仔細(xì)看下,在屬性值中你可以用這些方式來使用插值,上面的代碼將會(huì)被轉(zhuǎn)換為如下的 CSS 代碼:div { margin: 10px; margin: 20px/2; font-family: "MyFo UaTy"; background-image: url(http://xxx.xxx.xxx/a.jpg); width: calc(100% - $one * 2 *$two); width: calc(100% - 80px);}在屬性值中應(yīng)用插值的場(chǎng)景還蠻多的,你可以這么來使用以提高你的開發(fā)效率~
Java 代碼相對(duì)比較簡(jiǎn)單,因?yàn)檠a(bǔ)全的結(jié)果是一個(gè)字符串?dāng)?shù)組,補(bǔ)全列表的列表項(xiàng)也都是單個(gè)項(xiàng)目,所以這里直接使用ArrayAdapter再好不過(關(guān)于 ArrayAdapter 的使用詳見 23 節(jié)),代碼如下:package com.emercy.myapplication;import android.app.Activity;import android.os.Bundle;import android.widget.ArrayAdapter;import android.widget.AutoCompleteTextView;public class MainActivity extends Activity { private AutoCompleteTextView mTextView; private String[] mDataName = {"慕課", "慕課網(wǎng)", "慕課Android教程", "慕斯蛋糕", "慕容復(fù)"}; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mTextView = findViewById(R.id.autoCompleteTextView); ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line, mDataName); mTextView.setAdapter(adapter); }}首先我們將補(bǔ)全項(xiàng)存入字符串?dāng)?shù)組中,然后獲取 AutoCompleteTextView 對(duì)象,創(chuàng)建 ArrayAdapter,最后為 AutoCompleteTextView 對(duì)象指定 Adapter 即可。其中在創(chuàng)建 ArrayAdapter 的時(shí)候我們傳入了一個(gè) id 為android.R.layout.simple_dropdown_item_1line的布局文件,它是 Android 系統(tǒng)為我們內(nèi)置的專門用于下拉菜單使用的布局文件,其實(shí)里面只有一個(gè) TextView 用于顯示下拉菜單項(xiàng),查看源碼如下:<TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@android:id/text1" style="?android:attr/dropDownItemStyle" android:textAppearance="?android:attr/textAppearanceLargePopupMenu" android:singleLine="true" android:layout_width="match_parent" android:layout_height="?android:attr/listPreferredItemHeight" android:ellipsize="marquee" />我們?cè)谑褂孟吕藛晤愋偷臉邮綍r(shí)都可考慮直接采用系統(tǒng)樣式,最終編譯出來屏幕中有一個(gè)輸入框,我們輸入一個(gè)“慕”字,會(huì)展示以慕開頭的所有可補(bǔ)全的字符串,結(jié)果如圖所示:
定義:httpMethod () 屬性就是對(duì)接口的請(qǐng)求類型進(jìn)行一個(gè)約定,常用的接口類型有:“GET”, “HEAD”, “POST”, “PUT”, “DELETE”, “OPTIONS”。nickname () 屬性是為接口起一個(gè)別名,方便前后端溝通使用。使用方法:httpMethod () 屬性默認(rèn)值為空,但是 Swagger 在處理時(shí)會(huì)默認(rèn)獲取項(xiàng)目所采用的接口請(qǐng)求類型,我們可以不用專門設(shè)置,如果一定要設(shè)置該屬性,則只允許設(shè)置 http 協(xié)議規(guī)定的屬性,不能隨意設(shè)置。nickname () 屬性允許我們?yōu)榻涌谠O(shè)置一個(gè)別名,在設(shè)置別名之后,我們?cè)O(shè)置的別名會(huì)出現(xiàn)在瀏覽器地址欄中,如下代碼段所示(httpMethod () 屬性自動(dòng)獲取值,這里不再演示)。@ApiOperation(nickname = "userLoginNickName")public CommonResponse<User> userLogin(HttpSession session, @RequestBody User user){ return userService.login(session, user);}代碼解釋:第 1 行,我們?cè)?userLogin 方法的上方使用了 @ApiOperation 注解的 nickname 屬性來為接口起一個(gè)別名。顯示結(jié)果:在我用紅框圈起來的地方我們可以看到 userLoginNickName 字樣,這就是我們?yōu)榻涌谒O(shè)置的別名。Tips :不要隨意定義接口的別名,要根據(jù)特定業(yè)務(wù)場(chǎng)景來設(shè)置。在項(xiàng)目前后端聯(lián)合測(cè)試過程中,給接口起一個(gè)別名更方便前后端開發(fā)人員的溝通,并沒有其他特殊意義。
Spring Security 中的 SecurityFilterChain 對(duì)象,是通過 HttpSecurity 類來使用,它還提供了安全過濾器增、減的調(diào)用接口。如前文所述,安全過濾器的執(zhí)行順序是至關(guān)重要的,而順序的確定是由 FilterComparator 對(duì)象實(shí)現(xiàn)的。FilterComparator 類的全路徑為 org.springframework.security.config.annotation.web.builders.FilterComparator。在其構(gòu)造的時(shí)候配置了過濾器的基本順序,代碼如下:FilterComparator() { FilterComparator.Step order = new FilterComparator.Step(100, 100); this.put(ChannelProcessingFilter.class, order.next()); this.put(ConcurrentSessionFilter.class, order.next()); this.put(WebAsyncManagerIntegrationFilter.class, order.next()); this.put(SecurityContextPersistenceFilter.class, order.next()); this.put(HeaderWriterFilter.class, order.next());... this.put(SessionManagementFilter.class, order.next()); this.put(ExceptionTranslationFilter.class, order.next()); this.put(FilterSecurityInterceptor.class, order.next()); this.put(SwitchUserFilter.class, order.next());}注意,以上 FilterComparator 中的定義僅作為內(nèi)置過濾器排序的依據(jù),并不等于過濾器的真正注入。除了內(nèi)置的過濾器外,當(dāng)我們需要添加自定義過濾器的時(shí)候,也需要依賴 FilterComparator 對(duì)象完成順序的配置。實(shí)際開發(fā)時(shí)可用以下方法完成向指定順位插入過濾器的功能:http.addFilterAfter、http.addFilterBefore、http.addFilter、http.addFilterAt(http 為 HttpSecurity 實(shí)例)。另一方面,HttpSecurity 還為各個(gè)內(nèi)置過濾器提供了配置接口。例如:針對(duì) HeaderWriterFilter 提供了 headers() 方法,用于獲取或?yàn)?HeaderWriterFilter 配置參數(shù),例如設(shè)置響應(yīng)緩存時(shí)效 Expires 等 。HttpSecurity 在構(gòu)建時(shí),會(huì)根據(jù)已有的配置信息逐一對(duì) Filter 進(jìn)行實(shí)例化。HttpSecurity 還完成了對(duì)請(qǐng)求地址的配置,通過 RequestMatcherConfigurer 對(duì)象使請(qǐng)求地址與 Filter 匹配。