在以前的關於swagger文章裏提到過,程序員最討厭的兩件事,一件是別人不寫文檔,另外一件就是本身寫文檔。這裏若是把文檔換成單元測試也一樣成立。每一個開發人員都明白單元測試的做用,也都知道代碼覆蓋率越高越好。高覆蓋率的代碼,相對來講出現 BUG 的機率就越低,在線上運行就越穩定,接的鍋也就越少,就也不會懼怕測試同事忽然的關心。既然這麼多好處,爲何還會討厭他呢?至少在我看來,單測有以下幾點讓我喜歡不起來的理由。第一,要額外寫不少不少的代碼,一個高覆蓋率的單測代碼,每每比你要測試的,真正開發的業務代碼要多,甚至是業務代碼的好幾倍。這讓人以爲難以接受,你想一想開發 5 分鐘,單測 2 小時是什麼樣的心情。並且並非單測寫完就沒事了,後面業務要是變動了,你所寫的單測代碼也要同步維護。第二,即便你有那個耐心去寫單測,可是在當前這個拼速度擠時間的大環境下,會給你那麼多寫單測的時間嗎?寫一個單測的時間能夠實現一個需求,你會如何去選?第三,寫單測一般是一件很無趣的事,由於他比較死,主要目的就是爲了驗證,相比之下他更像是個體力活,沒有真正寫業務代碼那種創造的成就感。寫出來,驗證不出bug很失落,白寫了,驗證出bug又感到本身是在打本身臉。css
因此獲得的結論就是不寫單測?那麼問題又來了,出來混早晚是要還的,上線出了問題,最終責任人是誰?不是提需求的產品、不是沒發現問題的測試同窗,他們頂多就是連帶責任。最該負責的確定是寫這段代碼的你。特別是對於那些從事金融、交易、電商等息息相關業務的開發人員,跟每行代碼打交通的都是真金白銀。每次明星搞事,微博就掛,已經被傳爲笑談,畢竟只是娛樂相關,若是掛的是支付寶、微信,那用戶就沒有那麼大的包容度了。這些業務若是出現嚴重問題,輕則掃地出門,而後整個職業生涯揹負這個污點,重則直接從面向對象開發變成面向監獄開發。因此單元測試保護的不只僅是程序,更保護的是寫程序的你。最後得出了一個迫不得已的結論,單測是個讓人又愛又恨的東西,是不想作但又不得不作的事情。雖然咱們沒辦法改變要寫單測這件事,可是咱們能夠改變怎麼去寫單元測試這件事。html
固然,本文不是教你用旁門左道的方法提升代碼覆蓋率。而是經過一個神奇的框架 spock 去提升你編寫單元測試的效率。spock 這名稱來源,我的猜想是由於《星際迷航》的同名人物(封面圖)。那麼spock 是如何提升編寫單測的效率呢?我以爲有如下幾點:第一,他能夠用更少的代碼去實現單元測試,讓你能夠更加專一於去驗證結果而不是寫單測代碼的過程。那麼他又是如何作到少寫代碼這件事呢?原來他使用一種叫作 groovy 的魔法。groovy 實際上是一門基於 jvm 的動態語言。能夠簡單的理解成跑在 jvm 上的 python 或 js。說到這裏,可能沒有接觸過動態語言的同窗,對它們都會有一個比較刻板的印象,太過於靈活,很容易出現問題,且可維護性差,因此有了那一句『動態一時爽,全家 xxx』的梗。首先,這些的確是他的問題,嚴格的說是使用不當時才帶來的問題。因此主要仍是看使用的人。好比安卓領域的官方依賴管理工具 gradle 就是基於 groovy 開發的。另外不要誤覺得我學這門框架,還要多學一門語言,成本太大。其實大可沒必要擔憂,你若是會 groovy 固然更好,若是不會也沒有關係。由於 groovy 是基於 java 的,因此徹底能夠放心大膽的使用 java 的語法,某些要用到的 groovy 獨有的語法不多,並且後面都會告訴你。第二,他有更好的語義化,讓你的單測代碼可讀性更高。語義化這個詞可能不太好理解。舉兩個例子來講吧,第一個是語義化比較好的語言 -- HTML。他的語法特色就是標籤,不一樣的類型放在不一樣的標籤裏。好比 head 就是頭部的信息,body 是主體內容的信息,table 就是表格的信息,對於沒有編程經驗的人來講,也能夠很容易理解。第二個是語義化比較差的語言 -- 正則。他能夠說基本上沒有語義這種東西,由此致使的直接問題就是,即便是你本身的寫的正則,幾天以後你都不知道當時寫的是什麼。好比下面這個正則,你能猜出他是什麼意思嗎?(能夠留言回覆)java
((?:(?:25[0-5]|2[0-4]\d|[01]?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d?\d))複製代碼
<!--若是沒有使得 spring boot,如下包能夠省略-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--引入spock 核心包-->
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>1.3-groovy-2.5</version>
<scope>test</scope>
</dependency>
<!--引入spock 與 spring 集成包-->
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-spring</artifactId>
<version>1.3-groovy-2.5</version>
<scope>test</scope>
</dependency>
<!--引入 groovy 依賴-->
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.5.7</version>
<scope>test</scope>
</dependency>複製代碼
註釋已經標明,第一個包是 spring boot 項目須要使用的,若是你只是想使用 spock,只要最下面 3 個便可。其中第一個包 spock-core 提供了 spock 的核心功能,第二個包 spock-spring 提供了與 spring 的集成(不用 spring 的狀況下也能夠不引入)。 注意這兩個包的版本號 -> 1.3-groovy-2.5。第一個版本號 1.3 其實表明是 spock 的版本,第二個版本號表明的是 spock 所要依賴的 groovy 環境的版本。最後一個包就是咱們要依賴的 groovy 。python
/*
* *
* * blog.coder4j.cn
* * Copyright (C) 2016-2019 All Rights Reserved.
*
*/
package cn.coder4j.study.example.spock;
/**
* @author buhao
* @version Calculator.java, v 0.1 2019-10-30 10:34 buhao
*/
public class Calculator {
/**
* 加操做
*
* @param num1
* @param num2
* @return
*/
public static int add(int num1, int num2) {
return num1 + num2;
}
/**
* 整型除操做
*
* @param num1
* @param num2
* @return
*/
public static int divideInt(int num1, int num2) {
return num1 / num2;
}
/**
* 浮點型操做
* @param num1
* @param num2
* @return
*/
public static double divideDouble(double num1, double num2){
return num1 / num2;
}
}複製代碼
這是一個很簡單的計算器類。只寫了三個方法,一個是加法的操做、一個整型的除法操做、一個浮點類型的除法操做。git
class CalculatorTest extends Specification {
}複製代碼
這裏必定要注意,以前咱們已經說了 spock 是基於 groovy 。因此單測類的後綴不是 .java 而 .groovy。千萬不要建立成普通 java 類了。不然建立沒有問題,可是寫一些 groovy 語法會報錯。若是你用的是 IDEA 能夠經過以下方式建立,之前建立 Java 類咱們都是選擇第一個選項,如今咱們選擇第三個 Groovy Class 就能夠了。另外就是 spock 的測試類須要繼承 spock.lang.Specification 類。程序員
def "test add"(){
expect:
Calculator.add(1, 1) == 2
}複製代碼
def 是 groovy 的關鍵字,能夠用來定義變量跟方法名。後面 "test add" 是你單元測試的名稱,也能夠用中文。最後重點說明的是 expect 這個關鍵字。expect 字面上的意思是指望,咱們指望什麼樣的事情發生。在使用其它單測框架時,與之相似的是 assert 。好比 Assert.assertEquals(Calculator.add(1 + 1), 2) 這樣,表示咱們斷言加操做傳入1 與 1 相加結果爲 2。若是結果是這樣則用例經過,若是不是則用例失敗。這與咱們上面的代碼功能上完成一致。expect 的語法意義就是在 expect 的塊內,全部表達式成立則驗證經過,反之有任一個不成立則驗證失敗。這裏引入了一個塊的概念。怎麼理解 spock 的塊呢?咱們上面說 spock 有良好的語義化及更好的閱讀性就是由於這個塊的做用。能夠類比成 html 中的標籤。html 的標籤的範圍是兩個標籤之間,而 spock 更簡潔一點,從這個標籤開始到下一個標籤開始或代碼結束的地方,就是他的範圍。咱們只要看到 expect 這個標籤就明白,他的範圍內都是咱們預期要獲得的結果。github
這裏代碼比較簡單,參數我只使用了一次,因此直接寫死。若是想複用,我就得把這些參數抽成變量。這個時候可使用 spock 的 given 塊。given 的語法意義至關因而一個初始化的代碼塊。spring
def "test add with given"(){
given:
def num1 = 1
def num2 = 1
def result = 2
expect:
Calculator.add(num1, num2) == result
}複製代碼
固然你也能夠像下面這樣寫,可是嚴重不推薦,由於雖然能夠達到一樣的效果,可是不符合 spock 的語義。就像咱們通常是在 head 裏面引入 js、css,可是你在 body 或者任何標籤裏均可以引入,語法沒有問題可是破壞了語義,不便理解與維護。數據庫
// 反倒
def "test add with given"(){
expect:
def num1 = 1
def num2 = 1
def result = 2
Calculator.add(num1, num2) == result
}複製代碼
若是你還想讓語義更好一點,咱們能夠把參數與結果分開定義,這個時候可使用 and 塊。它的語法功能能夠理解成同他上面最近的一個標籤。編程
def "test add with given and"(){
given:
def num1 = 1
def num2 = 1
and:
def result = 2
expect:
Calculator.add(num1, num2) == result
}複製代碼
看了上面例子,可能以爲 spock 只是語義比較好,可是沒有少寫幾行代碼呀。別急,下面咱們就來看 spock 的一大殺器 where。
def "test add with expect where"(){
expect:
Calculator.add(num1, num2) == result
where:
num1 | num2 | result
1 | 1 | 2
1 | 2 | 3
1 | 3 | 4
}複製代碼
where 塊能夠理解成準備測試數據的地方,他能夠跟 expect 組合使用。上面代碼裏 expect 塊裏面定義了三個變量 num一、num二、result。這些數據咱們能夠在 where 塊裏定義。where 塊使用了一種很像 markdown 中表格的定義方法。第一行或者說表頭,列出了咱們要傳數據的變量名稱,這裏要與 expect 中對應,不能少可是能夠多。其它行都是數據行,與表頭同樣都是經過 『 | 』 號分隔。經過這樣,spock 就會跑 3 次用例,分別是 1 + 2 = 二、1 + 2 = 三、1 + 3 = 4 這些用例。怎麼樣?是否是很方便,後面再擴充用例只要再加一行數據就能夠了。
上面這些用例都是正常能夠跑通的,若是是 IDEA 跑完以後會以下所示:那麼如今咱們看看若是有用例不經過會怎麼樣,把上面代碼的最後一個 4 改爲 5
def "test add with expect where"(){
expect:
Calculator.add(num1, num2) == result
where:
num1 | num2 | result
1 | 1 | 2
1 | 2 | 3
1 | 3 | 5
}複製代碼
再跑一次,IDEA 會出現以下顯示左邊標註出來的是用例執行結果,能夠看出來雖然有 3 條數據,其中 2 條數據是成功,可是隻會顯示總體的成功與否,因此顯示未經過。可是 3 條數據,我怎麼知道哪條沒經過呢?右邊標註出來的是 spock 打印的的錯誤日誌。能夠很清楚的看到,在 num1 爲 1,num2 爲 3,result 爲 5 而且 他們之間的判斷關係爲 == 的結果是 false 纔是正確的。 spock 的這個日誌打印的是至關歷害,若是是比較字符串,還會計算異常字符串與正確字符串之間的匹配度,有興趣的同窗,能夠自行測試。嗯,雖然能夠經過日誌知道哪一個用例沒經過,可是仍是以爲有點麻煩。spock 也知道這一點。因此他還同時提供了一個 @Unroll 註解。咱們在上面的代碼上再加上這個註解:
@Unroll
def "test add with expect where unroll"(){
expect:
Calculator.add(num1, num2) == result
where:
num1 | num2 | result
1 | 1 | 2
1 | 2 | 3
1 | 3 | 5
}複製代碼
運行結果以下: 經過添加 @Unroll 註解,spock 自動把上面的代碼拆分紅了 3 個獨立的單測測試,分別運行,運行結果更清晰了。那麼還能更清晰嗎?固然能夠,咱們發現 spock 拆分後,每一個用例的名稱其實都是你寫的單測方法的名稱,而後後面加一個數組下標,不是很直觀。咱們能夠經過 groovy 的字符串語法,把變量放入用例名稱中,代碼以下:
@Unroll
def "test add with expect where unroll by #num1 + #num2 = #result"(){
expect:
Calculator.add(num1, num2) == result
where:
num1 | num2 | result
1 | 1 | 2
1 | 2 | 3
1 | 3 | 5
}複製代碼
如上,咱們在方法名後加了一句 #num1 + #num2 = #result。這裏有點相似咱們在 mybatis 或者一些模板引擎中使用的方法。# 號拼接聲明的變量就能夠了,執行後結果以下。這下更清晰了。另一點,就是 where 默認使用的是表格的這種形式:
where:
num1 | num2 | result
1 | 1 | 2
1 | 2 | 3
1 | 3 | 5複製代碼
很直觀,可是這種形式有一個弊端。上面 『 | 』 號對的這麼整齊。都是我一個空格一個 TAG 按出來的。雖然語法不要求對齊,可是逼死強迫症。不過,好在還能夠有另外一種形式:
@Unroll
def "test add with expect where unroll arr by #num1 + #num2 = #result"(){
expect:
Calculator.add(num1, num2) == result
where:
num1 << [1, 1, 2]
num2 << [1, 2, 3]
result << [1, 3, 4]
}複製代碼
能夠經過 『<<』 符(注意方向),把一個數組賦給變量,等同於上面的數據表格,沒有表格直觀,可是比較簡潔也不用考慮對齊問題,這兩種形式看我的喜愛了。<="" p="">
咱們都知道一個整數除以0 會有拋出一個『/ by zero』異常,那麼若是斷言這個異常呢。用上面 expect 不太好操做,咱們可使用另外一個相似的塊 when ... then。
@Unroll
def "test int divide zero exception"(){
when:
Calculator.divideInt(1, 0)
then:
def ex = thrown(ArithmeticException)
ex.message == "/ by zero"
}複製代碼
when ... then 一般是成對出現的,它表明着當執行了 when 塊中的操做,會出現 then 塊中的指望。好比上面的代碼說明了,當執行了 Calculator.divideInt(1, 0) 的操做,就必定會拋出 ArithmeticException 異常,而且異常信息是 / by zero。
上面咱們已經學會了 spock 的基礎用法,下面咱們將學習與 spring 整合的知識,首先建立幾個用於測試的demo 類
/*
* *
* * blog.coder4j.cn
* * Copyright (C) 2016-2019 All Rights Reserved.
*
*/
package cn.coder4j.study.example.spock.model;
import java.util.Objects;
/**
* @author buhao
* @version User.java, v 0.1 2019-10-30 16:23 buhao
*/
public class User {
private String name;
private Integer age;
private String passwd;
public User(String name, Integer age, String passwd) {
this.name = name;
this.age = age;
this.passwd = passwd;
}
/**
* Getter method for property <tt>passwd</tt>.
*
* @return property value of passwd
*/
public String getPasswd() {
return passwd;
}
/**
* Setter method for property <tt>passwd</tt>.
*
* @param passwd value to be assigned to property passwd
*/
public void setPasswd(String passwd) {
this.passwd = passwd;
}
/**
* Getter method for property <tt>name</tt>.
*
* @return property value of name
*/
public String getName() {
return name;
}
/**
* Setter method for property <tt>name</tt>.
*
* @param name value to be assigned to property name
*/
public void setName(String name) {
this.name = name;
}
/**
* Getter method for property <tt>age</tt>.
*
* @return property value of age
*/
public Integer getAge() {
return age;
}
/**
* Setter method for property <tt>age</tt>.
*
* @param age value to be assigned to property age
*/
public void setAge(Integer age) {
this.age = age;
}
public User() {
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(name, user.name) &&
Objects.equals(age, user.age) &&
Objects.equals(passwd, user.passwd);
}
@Override
public int hashCode() {
return Objects.hash(name, age, passwd);
}
}複製代碼
/*
* *
* * blog.coder4j.cn
* * Copyright (C) 2016-2019 All Rights Reserved.
*
*/
package cn.coder4j.study.example.spock.dao;
import cn.coder4j.study.example.spock.model.User;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* @author buhao
* @version UserDao.java, v 0.1 2019-10-30 16:24 buhao
*/
@Component
public class UserDao {
/**
* 模擬數據庫
*/
private static Map<String, User> userMap = new HashMap<>();
static {
userMap.put("k",new User("k", 1, "123"));
userMap.put("i",new User("i", 2, "456"));
userMap.put("w",new User("w", 3, "789"));
}
/**
* 經過用戶名查詢用戶
* @param name
* @return
*/
public User findByName(String name){
return userMap.get(name);
}
}複製代碼
/*
* *
* * blog.coder4j.cn
* * Copyright (C) 2016-2019 All Rights Reserved.
*
*/
package cn.coder4j.study.example.spock.service;
import cn.coder4j.study.example.spock.dao.UserDao;
import cn.coder4j.study.example.spock.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author buhao
* @version UserService.java, v 0.1 2019-10-30 16:29 buhao
*/
@Service
public class UserService {
@Autowired
private UserDao userDao;
public User findByName(String name){
return userDao.findByName(name);
}
public void loginAfter(){
System.out.println("登陸成功");
}
public void login(String name, String passwd){
User user = findByName(name);
if (user == null){
throw new RuntimeException(name + "不存在");
}
if (!user.getPasswd().equals(passwd)){
throw new RuntimeException(name + "密碼輸入錯誤");
}
loginAfter();
}
}複製代碼
/*
* *
* * blog.coder4j.cn
* * Copyright (C) 2016-2019 All Rights Reserved.
*
*/
package cn.coder4j.study.example.spock;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
複製代碼
/*
* *
* * blog.coder4j.cn
* * Copyright (C) 2016-2019 All Rights Reserved.
*
*/
package cn.coder4j.study.example.spock.service
import cn.coder4j.study.example.spock.model.User
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import spock.lang.Specification
import spock.lang.Unroll
@SpringBootTest
class UserServiceFunctionTest extends Specification {
@Autowired
UserService userService
@Unroll
def "test findByName with input #name return #result"() {
expect:
userService.findByName(name) == result
where:
name << ["k", "i", "kk"]
result << [new User("k", 1, "123"), new User("i", 2, "456"), null]
}
@Unroll
def "test login with input #name and #passwd throw #errMsg"() {
when:
userService.login(name, passwd)
then:
def e = thrown(Exception)
e.message == errMsg
where:
name | passwd | errMsg
"kd" | "1" | "${name}不存在"
"k" | "1" | "${name}密碼輸入錯誤"
}
}
複製代碼
spock 與 spring 集成特別的簡單,只要你加入了開頭所說的 spock-spring 和 spring-boot-starter-test。再於測試代碼的類上加上 @SpringBootTest 註解就能夠了。想用的類直接注入進來就能夠了,可是要注意的是這裏只能算功能測試或集成測試,由於在跑用例時是會啓動 spring 容器的,外部依賴也必須有。很耗時,並且有時候外部依賴本地也跑不了,因此咱們一般都是經過 mock 來完成單元測試。
/*
* *
* * blog.coder4j.cn
* * Copyright (C) 2016-2019 All Rights Reserved.
*
*/
package cn.coder4j.study.example.spock.service
import cn.coder4j.study.example.spock.dao.UserDao
import cn.coder4j.study.example.spock.model.User
import spock.lang.Specification
import spock.lang.Unroll
class UserServiceUnitTest extends Specification {
UserService userService = new UserService()
UserDao userDao = Mock(UserDao)
def setup(){
userService.userDao = userDao
}
def "test login with success"(){
when:
userService.login("k", "p")
then:
1 * userDao.findByName("k") >> new User("k", 12,"p")
}
def "test login with error"(){
given:
def name = "k"
def passwd = "p"
when:
userService.login(name, passwd)
then:
1 * userDao.findByName(name) >> null
then:
def e = thrown(RuntimeException)
e.message == "${name}不存在"
}
@Unroll
def "test login with "(){
when:
userService.login(name, passwd)
then:
userDao.findByName("k") >> null
userDao.findByName("k1") >> new User("k1", 12, "p")
then:
def e = thrown(RuntimeException)
e.message == errMsg
where:
name | passwd | errMsg
"k" | "k" | "${name}不存在"
"k1" | "p1" | "${name}密碼輸入錯誤"
}
}複製代碼
spock 使用 mock 也很簡單,直接使用 Mock(類) 就能夠了。如上代碼 UserDao userDao = Mock(UserDao) 。上面寫的例子中有幾點要說明一下,以以下這個方法爲例:
def "test login with error"(){
given:
def name = "k"
def passwd = "p"
when:
userService.login(name, passwd)
then:
1 * userDao.findByName(name) >> null
then:
def e = thrown(RuntimeException)
e.message == "${name}不存在"
}複製代碼
given、when、then 不用說了,你們已經很熟悉了,可是第一個 then 裏面的 1 * userDao.findByName(name) >> null 是什麼鬼?首先,咱們能夠知道的是,一個用例中能夠有多個 then 塊,對於多個指望能夠分別放在多個 then 中。第二, 1 * xx 表示 指望 xx 操做執行了 1 次。1 * userDao.findByName(name) 就表現當執行 userService.login(name, passwd) 時我指望執行 1 次 userDao.findByName(name) 方法。若是指望不執行這個方法就是0 * xx,這在條件代碼的驗證中頗有用,而後 >> null 又是什麼意思?他表明當執行了 userDao.findByName(name) 方法後,我讓他結果返回 null。由於 userDao 這個對象是咱們 mock 出來的,他就是一個假對象,爲了讓後續流程按咱們的想法進行,我能夠經過『 >>』 讓 spock 模擬返回指定數據。第三,要注意第二個 then 代碼塊使用 ${name} 引用變量,跟標題的 #name 是不一樣的。
方法名 | 做用 |
---|---|
setup() | 每一個方法執行前調用 |
cleanup() | 每一個方法執行後調用 |
setupSpec() | 每一個方法類加載前調用一次 |
cleanupSpec() | 每一個方法類執行完調用一次 |
這些方法一般用於測試開始前的一些初始化操做,和測試完成後的清理操做,以下:
def setup() {
println "方法開始前初始化"
}
def cleanup() {
println "方法執行完清理"
}
def setupSpec() {
println "類加載前開始前初始化"
}
def cleanupSpec() {
println "因此方法執行完清理"
}複製代碼
對於某些方法,須要規定他的時間,若是運行時間超過了指定時間就算失敗,這時可使用 timeout 註解
@Timeout(value = 900, unit = TimeUnit.MILLISECONDS)
def "test timeout"(){
expect:
Thread.sleep(1000)
1 == 1
}複製代碼
註解有兩個值,一個是 value 咱們設置的數值,unit 是數值的單位。
def "test findByName by verity"() {
given:
def userDao = Mock(UserDao)
when:
userDao.findByName("kk") >> new User("kk", 12, "33")
then:
def user = userDao.findByName("kk")
with(user) {
name == "kk"
age == 12
passwd == "33"
}
}複製代碼
with 算是一個語法糖,沒有他以前咱們要判斷對象的值只能,user.getXxx() == xx。若是屬性過多也是挺麻煩的,用 with 包裹以後,只要在花括號內直接寫屬性名稱便可,如上代碼所示。
由於篇幅有限,沒法貼完全部代碼,完整代碼已上傳 github。
本文在瞻仰了以下博主的精彩博文後,再加上自身的學習總結加工而來,若是本文在看的時候有不明白的地方能夠看一下下方連接。