-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontent.json
More file actions
1 lines (1 loc) · 211 KB
/
content.json
File metadata and controls
1 lines (1 loc) · 211 KB
1
{"meta":{"title":"Devin Blog","subtitle":"","description":"","author":"Devin","url":"http://devinwangg.github.io","root":"/"},"pages":[{"title":"404 Not Found","date":"2023-09-03T08:45:01.781Z","updated":"2023-09-03T08:45:01.781Z","comments":false,"path":"/404.html","permalink":"http://devinwangg.github.io/404.html","excerpt":"","text":""},{"title":"links","date":"2023-09-03T08:44:44.163Z","updated":"2023-09-03T08:44:44.163Z","comments":true,"path":"links/index.html","permalink":"http://devinwangg.github.io/links/index.html","excerpt":"","text":""},{"title":"About","date":"2023-09-03T08:44:47.795Z","updated":"2023-09-03T08:44:47.795Z","comments":false,"path":"about/index.html","permalink":"http://devinwangg.github.io/about/index.html","excerpt":"","text":""},{"title":"Categories","date":"2023-08-26T06:07:18.908Z","updated":"2020-10-11T12:30:33.000Z","comments":false,"path":"categories/index.html","permalink":"http://devinwangg.github.io/categories/index.html","excerpt":"","text":""},{"title":"Tags","date":"2023-08-26T06:07:18.917Z","updated":"2020-10-11T10:10:44.000Z","comments":false,"path":"tags/index.html","permalink":"http://devinwangg.github.io/tags/index.html","excerpt":"","text":""}],"posts":[{"title":"[Day 03] 遠征 Kotlin × 變數型別","slug":"ironman-2020-03","date":"2020-10-11T11:30:04.000Z","updated":"2020-10-11T11:30:04.000Z","comments":true,"path":"2020/10/11/ironman-2020-03/","link":"","permalink":"http://devinwangg.github.io/2020/10/11/ironman-2020-03/","excerpt":"","text":"在任何一種程式語言都有資料型別介紹,而此篇我們將來了解 Kotlin 在資料型別上的特性、操作、轉換等內容。 在 Kotlin 官方文件中有提到: In Kotlin, everything is an object in the sense that we can call member functions and properties on any variable. 上述內容得知,Kotlin 的任何東西都是一個物件,可以存取任何對象的相關方法與屬性,不像 Java 有區分原始型別(Primitive Type)與參考型態(Reference Types),在開發上有時候甚至需要做轉換才可使用。而 Kotlin 在宣告變數時使用的是靜態類型系統(static type system),即編輯器會按照變數類型辨識程式碼,判斷是否有存在類型與數值不符合的狀況發生,若有出現,編輯器會立即指出,例如下圖提示訊息: 變數宣告Kotlin 在變數宣告時主要會使用到兩種關鍵字 val 和 var : val 用於唯讀變數,一旦給值就無法再修改 var 用於需要重新修改數值的情況 12345fun main() { val readOnlyVariable = "鐵人賽第十二屆" // 宣告一個唯讀變數 var playerName = "選手一號" // 宣告一個可重新修改數值的變數 playerName = "選手二號" // 重新賦予新數值} Kotlin 官方這邊也有建議開發者在開發上建議優先使用 val,當遇到需要修改數值時再轉為 var 即可,若使用 var 宣告變數,開發者若沒有在程式中修改過,Intellij 編輯器也會提示建議改為 val,如下圖: 空值型態還記得嗎?我們在上一章有提到 Kotlin 有一個優勢是可以避免以前 Java 開發中常見的 NullPointerException 情況發生,主要原因是因為 Kotlin 預設宣告都只能是非 null 型態,例如以下範例,當我們想要進行指派 null 值給 String 時會發生編譯錯誤狀況: 這樣的錯誤檢查就能夠避免開發者經常會有出現錯誤的問題,而如果在開發情境上確實有必要使用 null 值,則可以將變數定義為 nullable 狀態,即在變數的型態定義上加上 ? 即可,如下範例: 12345fun main() { var test: String? = "鐵人賽" test = null println(test) // 印出 null} 型別判斷處理在介紹基本型別前,先介紹 Kotlin 在變數上有個特色是型別判斷處理,可對於已指派預設值的宣告變數自動定義型別,允許開發者省略型別定義,以下我們嘗試宣告一個變數,並輸出該變數的型別來看 Kotlin 是否有自動幫我們進行型別宣告,如下範例: 此範例先宣告變數 name 為「鐵人賽」,再利用「::class.simpleName」印出變數型別結果為 String 1234fun main() { val name = "鐵人賽" println(name::class.simpleName) // 印出 String -> 代表 Kotlin 自動幫我們定義型態} 資料型別Kotlin 在資料型別與 Java 非常相似,只差在變數型態必須使用首字大寫,型別分別如下: 數值型別 Numbers (種類可依長度區分) Byte (8 Bits) Short (16 Bits) Int (32 Bits) Long (64 Bits) Float (32 Bits) Double (64 Bits) 數值變數在操作上可直接宣告型態或是透過型別判斷進行操作: 123456789101112131415fun main() { val byte: Byte = 1 val short: Short = 2 val int: Int = 3 val long: Long = 4L val float: Float = 5f val double: Double = 6.0 println("Byte => $byte") println("Short => $short") println("Int => $int") println("Long => $long") println("Float => $float") println("Double => $double")} 前面有提到 Kotlin 的一切都是物件,在以前 Java 變數型態有分為基本型別(Primitive type)與參考型別(Reference type),即 int 與 Integer 的差別,而在 J2SE 5.0 時有提供自動裝箱(autoboxing)與拆箱(unboxing)來進行包裹基本型態,但在 Kotlin 中,只存在數值的裝箱,不存在拆箱,因為 Kotlin 是沒有存在基本資料型態的,下面將示範如何進行裝箱操作: 此範例操作須搭配上面提到的概念-空值型態達成裝箱效果,會發現裝箱前與裝箱後的數值都一樣 123456fun main() { val number: Int = 913 val numberInBox: Int? = number println("裝箱前數值: $number , 裝箱後數值: $numberInBox") // 裝箱前數值: 913 , 裝箱後數值: 913} 上面範例我們會發現兩個數值印出來雖然是相等的,但其實在 Kotlin 判斷數值是否相等有兩種比較方式(== 與 ===),== 是判斷數值是否相等, === 則是判斷兩個數值在記憶體位置是否相等,而其實 Kotlin 在變數裝箱操作時,記憶體位置會根據其資料型別的數值範圍進行定義,我們可以利用下面範例進行示範: 我們會發現當 a 變數為 127 時,判斷兩個裝箱變數會為 true,因為 Int 型態定義數值範圍為 -128 ~ 127,當 b 變數超過 127 數值時,Kotlin 在記憶體分配上會有不同位置狀況發生。 123456789101112fun main() { val a: Int = 127 val boxedA: Int? = a val anotherBoxedA: Int? = a val b: Int = 128 val boxedB: Int? = b val anotherBoxedB: Int? = b println(boxedA === anotherBoxedA) // true println(boxedB === anotherBoxedB) // false} Kotlin 在數值轉換上有分顯性轉換與隱性轉換,隱性轉換即 Kotlin 會自動幫我們進行轉換,但若兩個數值為不同型態時,會自動以定義數值範圍較大的型態為轉換後的最終型態,例如以下範例: 此範例為兩數相加,999為 Long 型態,1為 Int 型態,兩數相加後的結果 number 為 Long 型態 1234fun main() { val number = 999L + 1 println(number::class.simpleName) // 印出資料型別為 Long} 而為了避免隱性轉換時自動選擇型態問題,我們在開發上可使用顯性轉換方式,即下面範例: 12345678910fun main() { val number: Int = 65 println(number.toByte()) // 印出 65 println(number.toShort()) // 印出 65 println(number.toLong()) // 印出 65 println(number.toFloat()) // 印出 65.0 println(number.toDouble()) // 印出 65.0 println(number.toChar()) // 印出 A println(number.toString()) // 印出 65} 字元型別 Char Char 表示字元類型,字元變數必須使用單引號(‘’)表示,在轉換上可利用顯性轉換為數字型態,如以下範例: 1234fun main() { val char: Char = 'A' println(char.toInt()) // 印出 65} 字串型別 String String 表示字串類型,在輸出時可使用字串模板表示式處理字串組成,再進行輸出,如下範例: 1234fun main() { val username: String = "Devin" println("第十二屆鐵人賽 參加者 $username") // 印出「第十二屆鐵人賽 參加者 Devin」} 布林型別 Boolean Boolean 表示為布林類型,其值有 true 與 false 123456fun main() { val isFalse: Boolean = false val isTrue: Boolean = true println(isFalse && isTrue) // 印出「false」} 陣列型別 Array<T> Kotlin 的 Array 型別在宣告上是以 Array<T> 表示,我們可以到 Kotlin 的 Array 型態定義查看,會發現原始型態已經幫我們定義 get、set、size 與 iterator 方法: 故我們在 Array 操作上可以如下範例進行操作: 1234fun main() { val data: Array<Int> = arrayOf(1,2,3,4,5) // 宣告Array並賦予 1-5 數值 data.forEach { println(it) } // 利用 forEach 分別印出數值} Const 作用在前述有提到唯讀變數 val 不允許重新設定數值,但其實 val 是在程式執行階段(Run time)才進行賦值(Assign Value)動作,而我們若要限制程式在編輯階段(Compile time)就進行賦值動作,應使用 const 關鍵字搭配 val 進行變數宣告,我們可用一個範例來說明 const 與 val 的差異: 透過上面範例我們會發現兩件事: normalVariable 可利用 getRandomValue() 隨機取得 1 - 6 數值,表示程式是先在執行階段利用 getRandomValue() 方法取得數值後,才對 normalVariable 進行賦值 當我們嘗試將 constVariableFromGetValue 賦予 getRandomValue 方法時,會出現 const val 只能接受常數(constant value) 型別檢測與轉換 is 運算子 is運算子可檢查物件或變數是否屬於某資料型別,如Int、String等,類似於Java的 instanceof 12345fun main() { val data = "abc" println(data is String); // 印出 true println(data is Any); // 印出 true} as 運算子進行型別轉換 as運算子用於型別轉換,若要轉換的數值與指定型別相容,轉換就會成功;如果型別不相容,使用 as? 運算子就會返回值null,如下範例: 12345678fun main() { val x: Int = 2 val y: Int = x as Int val z: String? = y as? String println(y) // 印出 2 println(z) // 印出 null} 特殊型別除了上述基本型別以外,Kotlin 還有一些特殊型別運用於物件或函數上,這邊會先進行簡單介紹,會在後續章節介紹時會再深入說明: 1. Any 型別根據 Kotlin 官方文件所述: The root of the Kotlin class hierarchy. Every Kotlin class has [Any] as a superclass. 在此篇文章一開始介紹說明,Kotlin的一切都是物件,而每個物件其實都是繼承 Any 這個型別,此型別相當於Java的 Object 型別,而此型別也可再細分為 Any 與 Any?,Any屬於非空型別的根物件,Any?屬於可空型別的根物件。 2. Unit 型別在 Java 中,當我們所設計的 function 不需回傳值時,我們會使用到 void 型別,而在 Kotlin 可使用 Unit 型別代替,而且若我們不特地為 function 設定回傳型態時,Kotlin 會自動幫我們預設型態為 Unit 型別,會返回 Unit 型別,例如以下範例。 12345678fun main() { val username = getUserName() println(username::class.simpleName) // 印出 Unit 型別}fun getUserName() {} 3. Nothing 型別Nothing 型別其實類似於 Unit,Nothing 型別也是不返回任何東西,但差別在於 Nothing 型別意味著此函數不可能成功執行完成,只會拋出異常或是再也回不去函數呼叫的地方。 而 Nothing? 型別則會有一個使用情境,在 Java 中,void不能是變數的型別。也不能被當數值列印輸出。但是,在Java中有個包裝類Void是 void 的自動裝箱型別,如果我們想讓 function 返回型別永遠是 null 的話,可以把返回型別置為這個大寫的V的Void型別,而 Void 即對應 Kotlin 中的 Nothing? 型別。 範例(1) 使用 Nothing 型別 1234567fun main() { getUserName() // 使用 Nothing 型別}fun getUserName(): Nothing { throw NotImplementedError() // 丟出異常} 範例(2) 使用 Nothing? 型別 1234567fun main() { getUserName() // 使用 Nothing? 型別}fun getUserName(): Nothing? { return null // 保持回傳 null} Kotlin 轉換 Java Code有時候我們可能會好奇在 Kotlin 所撰寫的程式,實際轉換為 Java 會是怎麼樣的語法,此時我們可以利用 intellij 內建的工具進行轉換觀察。 在 Intellij 連續按 Shift 鍵兩次,搜尋「show kotlin」關鍵字,選擇「Show Kotlin Bytecode」,會出現Kotlin位元組碼工具視窗,再點擊「Decompile」按鈕即可觀看轉譯的Java 程式碼。 Reference [官方] Kotlin 官方文件 [網站] Kotlin 實戰範例","categories":[{"name":"2020-IThome鐵人賽","slug":"2020-IThome鐵人賽","permalink":"http://devinwangg.github.io/categories/2020-IThome%E9%90%B5%E4%BA%BA%E8%B3%BD/"}],"tags":[{"name":"Kotlin","slug":"Kotlin","permalink":"http://devinwangg.github.io/tags/Kotlin/"}]},{"title":"[Day 04] 遠征 Kotlin × 流程控制","slug":"ironman-2020-04","date":"2020-10-11T11:29:42.000Z","updated":"2020-10-11T11:29:42.000Z","comments":true,"path":"2020/10/11/ironman-2020-04/","link":"","permalink":"http://devinwangg.github.io/2020/10/11/ironman-2020-04/","excerpt":"","text":"此篇將介紹 Kotlin 在流程控制相關語法與特性,在 Kotlin 世界中,條件運算式、迴圈使用方式與其他程式語言相似,這邊快速帶大家了解在 Kotlin 中如何操作。 條件判斷1. if..else 操作在專案開發中,我們經常會使用到條件判斷來處理邏輯問題,我們可以直接利用下面範例來觀看 Kotlin 在 if..else 的操作,參考如下: 範例是當我們輸入分數(0-100)時,程式會依據所輸入的分數輸出對應的訊息 12345678910111213141516fun main() { print("請輸入分數:") // 宣告 Score 變數並將使用者輸入數值轉為 Int val score: Int = readLine()!!.toInt() // 利用 if 判斷輸出對應結果 if (score == 100) { // 當分數為 100 時 println("恭喜考滿分") } else if (score >= 60) { // 當分數大於 60 時 println("恭喜及格") } else if (score >= 0) { // 當分數大於 0 且小於 60 時 println("再接再勵") } else { println("數值必須介於 0 - 100 之間") // 當數值不是介於 0 - 100 時 }} 備註: !! 操作符號為非空斷言(Not-null assertion operator),表示我們認為輸入的數值不會是空值,若為空值則會立即出現錯誤狀況,而假如我們不使用 !! 操作符號時,Intellij 編輯器會告訴我們轉型會有錯誤,可參考下圖實際狀況: 而與其他語言相比,Kotlin 比較特別的是條件判斷式可根據條件同時進行變數賦值,我們將上面範例改為以下程式: 此範例我們改變上面範例,使用判斷式賦值方法將分數所對應的回應字串回傳給 output 變數,再印出結果 12345678910111213141516fun main() { print("請輸入分數:") // 宣告 score 變數並將使用者輸入數值轉為 Int val score: Int = readLine()!!.toInt() // 宣告 output 變數儲存分數對應的回應字串 val output: String = if (score == 100) // 當分數為 100 時 "恭喜考滿分" else if (score >= 60) // 當分數大於 60 時 "恭喜及格" else if (score >= 0) // 當分數大於 0 且小於 60 時 "再接再勵" else // 當數值不是介於 0 - 100 時 "成績輸入錯誤" println(output)} 2. When 運算式Kotlin 與其他程式語言比較不同的還有 when 運算式, 在 Kotlin 中,沒有提供其他語言常見的 switch 表達式,而是提供 when 表達式,when 是 Kotlin 的另一個控制流程語法,允許開發者編寫條件式,當滿足某個條件時執行對應程式碼,語法上非常簡潔,條件與執行程式用 → 箭頭符號語法進行配對,此方法適合三到四個流程情況使用,範例如下: 此範例先定義一個 score 變數,再利用 when 表達式進行分數判斷,再回傳對應的訊息,此部份記得此表達式與傳統 switch 不同,when 在條件結束時不需要加上 break 12345678910111213fun main() { print("請輸入分數:") // 宣告 score 變數並將使用者輸入數值轉為 Int val score: Int = readLine()!!.toInt() // 宣告 comment 變數並將分數對應的回應字串儲存 val comment: String = when (score) { 100 -> "恭喜滿分" in 60..99 -> "恭喜及格" in 0..60 -> "再接再勵" else -> "分數輸入錯誤" } println(comment)} when 也可以同時多條件判斷,我們試著將上面範例修改為以下: 123456789101112131415161718fun main() { print("請輸入分數:") // 宣告 score 變數並將使用者輸入數值轉為 Int val score: Int = readLine()!!.toInt() val comment: String = when (score) { 100 -> "恭喜滿分" in 60..99 -> { if (score > 80) { "表現優良" } else { "恭喜及格" } } in 0..60 -> "再接再勵" else -> "分數輸入錯誤" } println(comment)} 3. Elvis 語法進行null 判斷Kotlin 還有提供一個簡潔的語法-Elvis,在實務開發中,我們經常會遇到需要判斷該變數目前是否為 null 的情況,若為 null,則有預設值進行替代或拋出錯誤提醒,原先情況我們可能會如下範例進行撰寫: 12345678910fun main() { val username: String? = "Devin" val usernameLength: Int = if (username == null) { 0 } else { username.length } println(usernameLength) // 輸出 username 字元長度 5} 而 Kotlin 提供 elvis 語法,可以簡單處理這種情況: 1234567891011fun main() { // 若為空,則回傳 0 val username: String? = null val usernameLength: Int = username?.length ?: 0 println(usernameLength) // 輸出 0 // 拋出錯誤 val password: String? = null val passwordLength: Int = password?.length ?: throw IllegalArgumentException("資料為 null") println(passwordLength) // 輸出 Exception in thread "main" java.lang.IllegalArgumentException: 資料為 null} 使用 ?: 即可檢查對應的值是否為 null,若為否,則回傳右邊的數值,此數值若開發者不想賦予任何值,也可以使用 return 回傳 123val username: String? = "Devin"val usernameLength: Int = username?.length ?: returnprintln(usernameLength) // 此行不會執行 迴圈使用迴圈是程式依據設定的條件進行重複工作,當條件為真就持續進行,反之就結束,而 Kotlin 在迴圈方法上提供與Java 相同,都有 for 、while、 do while 等方法,其中 Kotlin 迴圈操作的方式非常像 Python,如下範例所示 - 利用 for 迴圈將集合內所有元素逐步印出: 123456789101112131415161718192021// 使用 for in 迴圈val names = listOf("Anne", "Peter", "Jeff")for (name in names) { println(name)}// 使用 while 迴圈val number = 0while (number < 10) { println(number) number++}// 使用 do...while 迴圈val doorPassword = "20200913"var keyInAmount= 0do { println("請輸入密碼進門") val password = readLine()!! // 轉換 nullable to non-nullable keyInAmount++} while (password != doorPassword && keyInAmount < 3) Kotlin 也提供 Ranges 表達式可讓開發者快速建立兩個值的區間,一般通常會與in 、in! 一起使用,其餘操作如下說明與實作範例,建議閱讀朋友可以直接: .. 操作符號表示為數值區間,例如 x..y 即為數值範圍在 x 與 y 區間 until 類似 .. 操作符號,差別在於最後的數值會是目標值 -1,例如 1 until 3 會印出「1 2」 rangeTo 與 .. 屬於相同效果 downTo 表示為反向區間,例如 x downTo y 即為 x 遞減到 y step 為指定區間遞增或遞減值,例如 x .. y step 2 即為 x 每次遞增 2 到目標值 y 12345678910111213141516171819202122fun main() { // 建立1-4區間數值 for (i in 1..4) print(i) // 此段會印出「1 2 3 4」 // rangeTo 與 .. 屬於相同效果 for (i in 1.rangeTo(4)) println(i) //此段會印出「1 2 3 4」 // until 與 .. 類似相同效果,差別在最終值會-1, for (i in 1 until 4) println(i) // 此段會印出「1 2 3」 // downTo 為反向區間使用 for (i in 4 downTo 1) print(i) // 此段會印出「4 3 2 1」 // last方法會只輸出區間最後一個值,若要輸出第一個值,可以使用 first 方法 println((1..12 step 2).last) // 此段會印出「11」 // 組合 downTo 與 step 方法 for (i in 8 downTo 5 step 2) print(i) // 此段會印出「8 6」 // 區間也可以使用字母 for (i in 'a' .. 'd') println(i) // 此段會印出「a b c d」} 在其他程式語言中,我們常會看見迴圈可以搭配 break 與 continue 方法, break 可讓當前迴圈直接整個結束,continue 則是可讓迴圈結束單次運行,而在 Kotlin 可以為迴圈指定名稱,當我們今天運用巢狀迴圈時,可指定要讓哪一個迴圈結束,如下範例: 迴圈在 item1 為 1 與 item2 為 0 時,結束外層單次中斷 123456789101112131415161718192021fun main() { // 命名外層迴圈為名稱「a」 a@for(item1 in 0 until 3){ // 命名內層迴圈為名稱「b」 b@for(item2 in 0 until 3){ // 當符合條件時結束外層(a)迴圈跳過此次運行 if(item1 == 1 && item2 == 0){ continue@a } println("$element1 => $element2") } }}// 印出結果為// 0 => 0// 0 => 1// 0 => 2// 2 => 0// 2 => 1// 2 => 2 迴圈在 item1 為 1 與 item2 為 1 時,結束外層迴圈 1234567891011121314151617181920fun main() { // 命名外層迴圈為名稱「a」 a@for(item1 in 0 until 3){ // 命名內層迴圈為名稱「b」 b@for(item2 in 0 until 3){ println("$element => $element1") // 當符合條件時結束整個外層(a)迴圈 if(item1 == 1 && item2 == 1){ break@a } } }}// 印出結果為// 0 => 0// 0 => 1// 0 => 2// 1 => 0// 1 => 1","categories":[{"name":"2020-IThome鐵人賽","slug":"2020-IThome鐵人賽","permalink":"http://devinwangg.github.io/categories/2020-IThome%E9%90%B5%E4%BA%BA%E8%B3%BD/"}],"tags":[{"name":"Kotlin","slug":"Kotlin","permalink":"http://devinwangg.github.io/tags/Kotlin/"}]},{"title":"[Day 05] 遠征 Kotlin × 函數介紹","slug":"ironman-2020-05","date":"2020-10-11T11:28:23.000Z","updated":"2020-10-11T11:28:23.000Z","comments":true,"path":"2020/10/11/ironman-2020-05/","link":"","permalink":"http://devinwangg.github.io/2020/10/11/ironman-2020-05/","excerpt":"","text":"這篇將帶大家了解 Kotlin 在函數 Function 的基本操作,函數 Function 在程式設計中扮演非常重要的角色,通常使用 function 會有以下好處: 減少撰寫重複程式碼,提高程式維護性 將程式碼以有意義的命名進行組織 若有相同流程的行為,可藉由參數傳遞進行處理 函數定義Function 是我們為了能夠完成某項特定任務或可重新使用的程式碼區塊,在 Kotlin 中是使用 fun 關鍵字來定義一個函式,其定義格式可參考下圖與利用一個範例進行說明: 此範例建立一個 getSumValue Function 進行兩數加總,在 main function 進行呼叫並印出結果 123456789fun main() { val sum = getSumValue(1, 2) // 傳入參數 1 和 參數 2 println(sum) // 印出 3}fun getSumValue(x: Int, y: Int): Int { val sum = x + y // 計算 return sum} 預設參數值Kotlin 在 Function 中提供預設函數參數值方法,假設我們想呼叫某一個函數,但不傳遞任何參數,此時就可以使用參數預設值作為函數的參數值,範例如下: 修改前一個範例,當我們沒有傳入任何參數給 getSumValue 方法時,照理應回傳 0 的結果,此時可設定兩個參數預設值為 0 12345678910fun main() { val sum = getSumValue() println(sum)}// 利用預設參數 x = 0, y = 0fun getSumValue(x: Int = 0, y: Int = 0): Int { val sum = x + y // 計算 return sum} 具名參數在前面第一個範例中,我們會發現 Function 呼叫時,必須根據函式參數順序(x, y)進行參數傳遞,而 Kotlin 有提供「具名參數」方法,讓我們可以不用照著函數定義時的參數順序進行呼叫,而是呼叫時搭配參數名稱進行賦值呼叫,此作法可以讓我們的程式碼更清晰直觀,例如以下範例: 123456789fun main() { val sum = getSumValue(y = 20, x = 30) // 加入參數名稱就可以不用管實際函數參數順序 println(sum)}fun getSumValue(x: Int = 0, y: Int = 0): Int { val sum = x + y // 計算 return sum} 匿名函數 Anonymous Function前面介紹的函數其實都是使用具名函數來定義,使用此方法就必須賦予一個函數名稱,而匿名函數則相反,不需要給予函數名稱,匿名函數也可稱為 Lambda 運算式,它通常具有以下特性: 此函數不需要 return 關鍵字返回資料,則是會隱式自動回傳函數最後一行結果或運算式 在定義只有一個參數的匿名函數中,可以使用 it 關鍵字進行呼叫表示該參數 1234567fun main() { val data = arrayOf(1, 2, 3) // 原始資料 val multiply = {x: Int, y: Int -> x * y} // 提供一個乘法匿名函數,自動回傳最後一行運算式(x * y) data.forEach { println(multiply(it, 3)) } // 只有一個參數時,可使用 it 代替 item 數值 // 印出 3 6 9} 高階函數 Higher-Order Functions在 Kotlin 中支援 Higher - Order Function(高階函數) 方法,所謂高階函數是指我們可以將 Function 作為變數來進行傳遞,或是 Function 的返回值是一個 Function 類型進行回傳,此種特性也代表 Kotlin Function 是支援 First-class-object (第一類物件)函數。 我們利用字串反轉應用為範例如下: 123456789101112fun main() { // 宣告反轉函數 val reverseMethod: (String) -> String = { text: String -> text.reversed() } // 呼叫欲反轉字串與帶入反轉函數 val result: String = getReverseResult("字串反轉", reverseMethod) println("反轉結果為: $result") // 反轉結果為: 轉反串字}// 傳入字串與 Functionfun getReverseResult(text: String, callMethod: (String) -> String): String { return callMethod(text)} 函數返回值為一個函數類型,可參考下面的範例: 1234567891011121314151617fun main() { val data = listOf(10, 11, 12, 13, 14, 15) // 定義一個數值 List val dividedByTwo = data.filter(divide(2)) //被2整除的元素列表 val dividedByThree = data.filter(divide(3)) //被3整除的元素列表 // 印出「可被 2 整除的數值有: [10, 12, 14]」 println("可被 2 整除的數值有: $dividedByTwo") // 印出「可被 3 整除的數值有: [12, 15]」 println("可被 3 整除的數值有: $dividedByThree")}// 傳入要除的數值,回傳則為一個函數 function,傳入一個數值判斷是否可整除(回傳 Boolean)fun divide(number: Int): (Int) -> Boolean { return { it: Int -> it % number == 0 }}","categories":[{"name":"2020-IThome鐵人賽","slug":"2020-IThome鐵人賽","permalink":"http://devinwangg.github.io/categories/2020-IThome%E9%90%B5%E4%BA%BA%E8%B3%BD/"}],"tags":[{"name":"Kotlin","slug":"Kotlin","permalink":"http://devinwangg.github.io/tags/Kotlin/"}]},{"title":"[Day 06] 遠征 Kotlin × Collections 介紹","slug":"ironman-2020-06","date":"2020-10-11T11:27:51.000Z","updated":"2020-10-11T11:27:51.000Z","comments":true,"path":"2020/10/11/ironman-2020-06/","link":"","permalink":"http://devinwangg.github.io/2020/10/11/ironman-2020-06/","excerpt":"","text":"集合(Collections)是可以儲存一群相同型別資料的物件,Kotlin 集合類型主要有 List、Set、Map,又可再細分為可變(mutable )集合與不可變(immutable)集合, Kotlin 官方這邊有提供一張 Collection 結構圖(參考 Kotlin 官方文件): 我們可以從上圖觀察出 Collection 是集合結構的根節點 root,而 Collection 還繼承了 Iterable interface<T>,其中 Iterable、Collection、List、Set與Map 都會再延伸出可變(Mutable)集合,清楚表達出集合成員們的關係。 Collection 既然作為根節點,我們可以觀察它內部是如何定義,下圖會發現它的內部除了繼承 Iterable 以外,也包含了 size、isEmpty、contains與 override iterator 迭代元素的操作: 我們利用一個範例進行測試,在下面範例中我們先定義一個 List 與一個 Set 的集合,再定義一個參數為 Collection 的函數,觀察兩者是否會印出一樣的值: 1234567891011121314fun main() { // 定義一個 List 集合 val stringList = listOf("one", "two", "three") printAll(stringList) // 印出 one two three // 定義一個 Set 集合 val stringSet = setOf("one", "two", "three") printAll(stringSet) // 印出 one two three}fun printAll(strings: Collection<String>) { for(s in strings) print("$s ") println()} 集合類型 List 是一個有序集合,可利用索引來存取項目(item)資料,同樣的項目數值在 list 中可重複出現多次 1234567891011121314151617181920fun main() { val numbers = listOf(1, 4, 3, 4) // 印出集合共有幾個元素 println("集合共有 ${numbers.size} 個元素") // 索引起始值為 0,故 get(2) 是取得第三個數值 println("第三個元素為 ${numbers.get(2)}") // 同上,索引起始值為 0,故 numbers[3] 是取得第四個數值 println("第四個元素為 ${numbers[3]}") // 數值 3 所在索引值為 2 println("利用數值找出所在的索引值 ${numbers.indexOf(3)}") // 此段程式會印出下列訊息: // 集合共有 4 個元素 // 第三個元素為 3 // 第四個元素為 4 // 利用數值找出所在的索引值 2} Set 是一個無序集合,與 List 最大差別在於 Set 不可儲存重複數值項目,對於 Set 來說,元素的顺序並不重要 12345678910111213fun main() { val numbers = setOf<Int>(1, 4, 3, 4) // 因 set 集合元素值不會重複,故 size 會為 3 println("集合共有 ${numbers.size} 個元素") // 回傳 true println("集合是否存在 3 的元素 ${numbers.contains(3)}") // 印出下列訊息: // 集合共有 3 個元素 // 集合是否存在 3 的元素 true} Map 是由鍵值(Key)與數值(Value)所組成的集合,Key 必須符合唯一性,每個 Key 值都會搭配一個 Value 123456789101112131415fun main() { val numbers = mapOf<String, Int>("key1" to 1, "key2" to 4, "key3" to 3, "key4" to 4) println("集合共有 ${numbers.size} 個元素") // 檢查是否有該索引值,若存在則回傳 true println("集合是否存在 key2 的索引值 ${"key2" in numbers}") // 檢查是否有該數值,若有則回傳 true println("集合是否存在 4 的數值 ${numbers.containsValue(4)}") // 印出下列訊息: // 集合共有 4 個元素 // 集合是否存在 key2 的索引值 true // 集合是否存在 4 的數值 true} 可變(mutable)與不可變(immutable)在文章開頭有提到, Kotlin 在集合這塊會再細分為可變(mutable)集合與不可變(immutable)集合,依照文章開頭的 Kotlin 官方集合結構圖會發現,所有的可變集合都是繼承自不可變的集合,兩者只差在可變集合可以改變原集合的元素數值、順序、數量等,而不可變集合只能對元素進行讀取和查詢,我們利用下面範例進行測試: 12345678910fun main() { // 定義一個不可變集合 List,將無法針對內容修改 val list = listOf(1, 2, 3, 4) // 定義一個可變集合 mutableList,此集合可修改內容 val mutableList = mutableListOf(1, 2, 3, 4) list[0] = 1 // 此行會出現編譯錯誤,錯誤訊息可參考下圖 mutableList[0] = 5 // 成功編譯} 編譯錯誤可參考下圖訊息,會發現到 list 集合無法修改內容: 集合操作在實務開發中,我們經常會遇到產品的某個業務邏輯問題需要操作集合,此時就會需要了解集合的操作方式,像是如何建立一個空集合、如何加入元素到集合、如何進行集合複製、如何逐步印出集合內所有元素、如何在集合取得特定條件的元素等方法,我們利用一個範例進行深入探討: 1234567891011121314151617181920212223242526272829303132333435363738fun main() { // 建立一個空集合 List val list: MutableList<Int> = mutableListOf<Int>() list.add(1) // 加入元素 1 list.add(2) // 加入元素 2 list.add(3) // 加入元素 3 list.add(4) // 加入元素 4 // 此段會進行集合複製,利用 toMutableList() 方法 val copyList = list.toMutableList() // 嘗試印出 copyList 共有幾個元素,此段會印出 「copyList 有 4 個元素」 println("copyList 有 ${copyList.size} 個元素") // 嘗試利用 forEach 方法逐步印出集合內元素,結果印出「1 2 3 4 」結果 copyList.forEach { print("$it ") } // 嘗試利用 filter 方法加入偶數判斷條件,印出「2 4」結果 copyList.filter { it % 2 == 0 }.forEach { print("$it ") } println("集合取值方法") // slice 是利用區間索引值進行取值,此行會印出 [2, 3, 4] println(copyList.slice(1..3)) // take 是取得0-2的元素,此行會印出 [1, 2] println(copyList.take(2)) // takeList 則是取得倒數0-2的元素,此行會印出 [3, 4] println(copyList.takeLast(2)) // drop 會回傳指定索引的後面全部元素,此行會印出 [3, 4] println(copyList.drop(2)) // 此段會印出 copyList 共有 4 個元素 println("copyList 有 ${copyList.size} 個元素")} Reference Kotlin 官方文件-Collection","categories":[{"name":"2020-IThome鐵人賽","slug":"2020-IThome鐵人賽","permalink":"http://devinwangg.github.io/categories/2020-IThome%E9%90%B5%E4%BA%BA%E8%B3%BD/"}],"tags":[{"name":"Kotlin","slug":"Kotlin","permalink":"http://devinwangg.github.io/tags/Kotlin/"}]},{"title":"[Day 07] 遠征 Kotlin × 類別與物件","slug":"ironman-2020-07","date":"2020-10-11T11:23:50.000Z","updated":"2020-10-11T11:23:50.000Z","comments":true,"path":"2020/10/11/ironman-2020-07/","link":"","permalink":"http://devinwangg.github.io/2020/10/11/ironman-2020-07/","excerpt":"","text":"類別定義Kotlin 在物件導向這塊與其他程式語言類似,類別上也包含建構式、函式、屬性、物件宣告等,而所謂類別就像一張藍圖,以蓋房子為例,它只是給予我們如何蓋出房子的細節,並非是一棟蓋好的房子。 在 Kotlin 使用關鍵字 class 宣告類別,主要包含兩類內容:行為 和 資料,以 類別函數 定義類別的行為,以 類別屬性 增加類別的 資料,操作如下範例所示: 我們可以透過下面範例觀察出三件事 在定義類別時,我們會使用到 class 關鍵字進行定義 在呼叫類別時,不會像 Java 需要使用 new 關鍵字進行實現(Instance),而是直接使用類別名稱再加上括號 ( ) 即可 在建立屬性時,若我們有 取值 get 或 賦值 set 需求時,不需要像 Java 必須在類別裡面建立 getter 與 setter,這些屬性的 getter 與 setter 是 Kotlin 編譯器為我們自動產生的,讓我們在程式碼中保持 12345678910111213141516fun main() { // 呼叫類別與呼叫 printName 函數 Person().printName()}// 建立 Person 類別(Class)class Person { // 建立 userName 屬性(Property) var userName: String = "user" // 建立 printName 函數(Function) fun printName() { println("Name is $userName") }} 在 Kotlin 中,如果一個類別是空的,沒有內容,括號是可以直接省略的 1class Person 建構函數 Constructor在 Java 的建構函數(Constructor)可以讓我們建構出多個不同參數的建構函數,但是 Java 每個建構函數都是同級別的,而在 Kotlin 中卻是分成兩級運算子(主建構函數與次建構函數),主建構函數是直接包含在類別名稱之後,次建構函數則是在類別裡面進行實現,我們利用下面範例進行觀察: 下面範例是當我們加入主建構函數時的程式操作狀況 1234567891011121314151617fun main() { // 在呼叫類別時,傳進去的參數是由主建構函式進行定義 val person = Person("devin", "abc@gmail.com") print(person.name) // 印出 Devin}// 主建構函式定義臨時變數 _name 與 _emailclass Person(_name: String, _email: String) { var name = _name // 呼叫 capitalize 方法會為數值設定為首字大寫 get() = field.capitalize() set(value) { // 呼叫 trim 方法會為傳進來的數值去除空白再儲存 field = value.trim() } var email = _email} 下面範例是加入次建構函數時,增加初始值設定與初始邏輯判斷 123456789101112131415161718192021222324252627fun main() { val person = Person() print(person.name) // 印出下列結果 // 使用者未輸入參數 // 路人甲}class Person(_name: String, _email: String) { var name = _name // 呼叫 capitalize 方法會為數值設定為首字大寫 get() = field.capitalize() set(value) { // 呼叫 trim 方法會為傳進來的數值去除空白再儲存 field = value.trim() } var email = _email // 若主建構函數沒有帶入參數,則自動帶入預設值 constructor() : this(_name = "路人甲", _email = "") { // 帶入初始邏輯條件 if (name == "路人甲") { println("使用者未輸入參數") } }} 在上面範例中,我們在次建構函數使用了預設參數方法,但實際上此方法在主建構函數與次建構函數都可以使用,我們也可以將上面範例的預設值改為在主建構函數使用: 123456789101112131415161718192021222324252627fun main() { val person = Person() print(person.name) // 印出下列結果 // 使用者未輸入參數 // 路人甲}// 修改主建構函數預設值class Person(_name: String = "路人甲", _email: String) { var name = _name // 呼叫 capitalize 方法會為數值設定為首字大寫 get() = field.capitalize() set(value) { // 呼叫 trim 方法會為傳進來的數值去除空白再儲存 field = value.trim() } var email = _email constructor() : this(_email = "") { // 帶入初始邏輯條件 if (name == "路人甲") { println("使用者未輸入參數") } }} 除了主次建構函數可設置初始值以外,我們也可以另外為函數定義一個初始化區塊 init,此區塊除了設定初始值以外,也可以進行數值的有效性檢查,可觀察以下範例: 123456789101112131415161718192021222324252627fun main() { val person = Person("","") print(person.name) // 印出下列結果 // 使用者未輸入參數 // 路人甲}class Person(_name: String, _email: String) { var name = _name // 呼叫 capitalize 方法會為數值設定為首字大寫 get() = field.capitalize() set(value) { // 呼叫 trim 方法會為傳進來的數值去除空白再儲存 field = value.trim() } var email = _email // 利用初始化區塊(init)進行初始值設定與有效值檢查 init { name = "路人甲" // 帶入參數檢查判斷,若檢查不通過,則拋出 IllegalArgumentException 異常 // 異常結果可看下面圖片顯示 require(name.isNotBlank()) { "使用者未輸入姓名參數" } }} 當我們在呼叫類別時,可加入有效值判斷,當檢查不通過時,會如下面圖片呈現出 IllegalArgumentException 的錯誤訊息 物件(Object)介紹前面我們介紹類別(Class)的介紹,我們會發現,假設我們有很多個類別需求,只需多次呼叫類別即可,但產生多個類別的時候,我們可能會遇到一個問題-如何進行類別之間的資料溝通,此時我們就必須要為這樣的需求進行處理。 而假設我們需求只想要使用一個實例(Instance)來管理整個程式的狀態,我們就可以定義一個單例(Singleton)即可,而在 Kotlin 程式語言,根據上述需求,我們可以使用 object 關鍵字進行定義出一個在應用程式中只有它存在的實例 故我們可以歸納出物件(Object)有幾個特性: 整個應用程式中只會存在一個實例(Instance) 相當於 Singleton 設計模式 物件宣告物件宣告主要會利用 object 關鍵字進行定義物件(Object),在定義上類似於類別,也有初始區塊、屬性資料等操作方法,相對於類別,物件可自動實例化(Instance),但在 object 無法使用建構函數 Constructor,即無法在初始化時從外部傳遞參數進行實現,但我們利用一個範例進行觀察: 12345678910111213141516171819202122fun main() { // 呼叫Family object Family}// 建立物件object Family { // 建立 object 資料 private val person = Person(_name = "devin") // 初始區塊 init { println("歡迎來到 Family 家族") printFamilyMember() } // 建立 object function private fun printFamilyMember() { print("目前家族成員有:") println(person.name) }} 物件運算原本使用類別進行處理很重要,能夠幫助我們減少重複邏輯一再出現,而將邏輯抽象為一個新事物概念,但往往有時候需求上不見得都會有重複使用的狀況發生,有時候只會有一次性使用,這時候object 就可以幫助我們進行處理這樣的情境,將 object 作為匿名類別來使用,可參考下面範例: 12345678fun main() { // 建立 object 匿名類別 val person = object { var userName: String = "Devin" var email: String = "abc@gmail.com" } println(person.userName) // 輸出「Devin」} 伴生物件如果我們在開發上想要把類別實現與物件初始化綁在一起,此時就可以考慮使用伴生物件,使用 companion 關鍵字,我們直接利用下面範例進行觀察: 1234567891011121314151617181920fun main() { // 呼叫 Person 類別的伴生物件,再呼叫類別方法 Person.data.printUserName()}class Person(_name: String = "", _email: String = "") { val name: String = _name val email: String = _email // 建立伴生物件 companion object { val data = Person("Devin", "abc@gmail.com") } fun printUserName(){ println(name) }}// 範例輸出結果為 Devin 資料類別在物件導向程式設計中,我們經常會建立專門儲存資料的類別,再將此類別進行實例化物件進行資料溝通,此物件我們會稱為資料傳輸物件(Data Transfer Object, DTO),在 Kotlin 中特別針對此物件設計一個「資料類別(Data Class)」,我們直接用一個範例來示範: 123456789101112131415161718192021222324252627fun main() { // 實例化 person 物件 val person1 = Person("devin", "abc@gmail.com") // 輸出 println("person1 姓名:${person1.name}") // 資料拷貝 val person2 = person1.copy() // 輸出 println("person2 姓名:${person2.name}") // 解構數值 val (name, email) = person2 // 輸出 println("解構輸出姓名:${name}") // 最後輸出結果 // person1 姓名:devin // person2 姓名:devin // 解構輸出姓名:devin}// 建立 Person 資料類別data class Person( val name: String, val email: String)","categories":[{"name":"2020-IThome鐵人賽","slug":"2020-IThome鐵人賽","permalink":"http://devinwangg.github.io/categories/2020-IThome%E9%90%B5%E4%BA%BA%E8%B3%BD/"}],"tags":[{"name":"Kotlin","slug":"Kotlin","permalink":"http://devinwangg.github.io/tags/Kotlin/"}]},{"title":"[Day 08] 遠征 Kotlin × 類別繼承、介面、抽象","slug":"ironman-2020-08","date":"2020-10-11T11:23:03.000Z","updated":"2020-10-11T11:23:03.000Z","comments":true,"path":"2020/10/11/ironman-2020-08/","link":"","permalink":"http://devinwangg.github.io/2020/10/11/ironman-2020-08/","excerpt":"","text":"昨日我們已經介紹 Kotlin 類別的基本使用方式,接下來我們來談繼承、介面與抽象的使用方法,在 Kotlin 中,我們要使用繼承時,會有以下三件事要注意: 需要使用 : 操作符號 被繼承的類別必須在 class 前面加上 open 關鍵字 若父類別的主建構函數(Primary Constructor)有參數,必須在繼承時帶入資料 我們利用上面三點事項撰寫下面範例: 12345678910111213141516fun main() { // 呼叫 Author 類別 val author = Author("Devin") // 印出 Devin println(author.name)}// 加上 open 關鍵字代表此類別可被繼承open class Person(name: String){ val name: String = name}// 建立一個 Author 類別繼承 Person 類別class Author(name: String) : Person(name) 而當子類別繼承後,如果子類別要使用父類別的函數,我們就要使用到 super 關鍵字進行呼叫,如下範例: 12345678910111213141516171819202122fun main() { val man1 = SuperMan("Devin") val man2 = SuperMan("Eric", "Eric@Eric.com") println(man1.name) // 印出 Devin println(man1.email) // 印出 "" println(man2.name) // 印出 Eric println(man2.email) // 印出 "Eric@Eric.com"}open class Person(val name: String) { var email: String = "" constructor(name: String, email: String) : this(name) { this.email = email }}class SuperMan : Person { constructor(name: String) : super(name) constructor(name: String, email: String) : super(name, email)} 當子類別繼承父類別後,若子類別想要覆寫函數可使用 override 關鍵字,記得繼承的函數也要使用 open 關鍵字進行宣告,如下範例: 1234567891011121314151617181920212223242526fun main() { val man = SuperMan("Devin") man.sayHello()}open class Person(val name: String) { var email: String = "" constructor(name: String, email: String) : this(name) { this.email = email } open fun sayHello() { println("Hi, 我是Person") }}class SuperMan : Person { constructor(name: String) : super(name) constructor(name: String, email: String) : super(name, email) // 覆寫方法 override fun sayHello() { println("Hi, 我是SuperMan") }} 在 Kotlin 中使用繼承時,要注意程式執行先後順序,我們會直接利用範例搭配下面步驟逐步觀察: 程式會先執行 SuperMan 類別主要建構函數的 println 方法 再進入父類別 Person ,執行該類別的 init 區塊程式 再執行父類別 Person的次建構函數 回到子類別,執行該類別的 init 區塊程式 再因 main 函數呼叫 sayHello 方法,藉由 super 關鍵字呼叫父類別的 sayHello 函數 最後才會執行子類別的 sayHello println 函數 123456789101112131415161718192021222324252627282930313233343536373839fun main() { val man = SuperMan("Devin") man.sayHello()}open class Person(open val name: String) { var email: String = "" // 執行步驟 2 init { println("Person init 區塊") } // 執行步驟 3 constructor(name: String, email: String) : this(name) { println("Person Name: $name") this.email = email } // 執行步驟 5 open fun sayHello() { println("Hi, 我是Person") }}// 執行步驟 1class SuperMan(override val name: String) : Person(name, email = "Test".also { println("帶入 Email 資料") }) { // 執行步驟 4 init { println("SuperMan init 區塊") } // 執行步驟 6 override fun sayHello() { super.sayHello() println("Hi, 我是SuperMan") }} 在繼承特性中,我們可以使用 var 定義的變數覆寫(override) val 父類別屬性\u001d,但要記得我們無法使用 val 覆寫(override) var 屬性,如下範例: 12345678910111213141516171819fun main() { SuperMan("devin")}// 定義可繼承的 Person 類別與 val name 屬性open class Person(open val name: String)// override 父類別屬性,將 val 改為 varclass SuperMan(private var _name: String): Person(_name) { override var name: String = _name get() = field.capitalize() set(value) { field = value.trim() } init { println(name) }} 介面 InterfaceKotlin 與 Java 一樣,只能繼承一個類別,但可以實作多個介面,而介面實作也是使用 : 操作符號進行實現,而 Kotlin 與 Java 不同的地方是 Kotlin 的 Interface 可以自己實作函數,而使用介面的好處主要是為了解決耦合問題(Coupling)與支援多重繼承功能,例如以下範例: 123456789101112131415161718192021222324252627282930fun main() { val myClass = MyClass() myClass.sayHello() myClass.printData()}interface Interface1 { fun printData() // 介面本身自已實作 fun haveImplement() { println("Kotlin 介面可自己實作,而且類別不需要實作") }}interface Interface2 { fun sayHello()}// 繼承多重介面class MyClass : Interface1, Interface2 { override fun printData() { // 呼叫介面已實作函數 haveImplement() } override fun sayHello() { println("Hi") }} 當介面方法相同時,我們可以使用 super 關鍵字進行呼叫特定介面的方法,如下範例: 1234567891011121314151617181920212223fun main() { MyClass().haveImplement()}interface Interface1 { fun haveImplement() { println("Interface1 實作") }}interface Interface2 { fun haveImplement() { println("Interface2 實作") }}// 繼承多重介面class MyClass : Interface1, Interface2 { override fun haveImplement() { // 利用 super 呼叫指定介面的函數 super<Interface2>.haveImplement() }} 前面提到的耦合(Coupling)其實就是指兩個模組之間的相依性,若相依性越高,則耦合度越高,即為高耦合問題,耦合性越高的話,容易因為小需求變動而連貫影響整個系統或其他模組,例如以下範例,類別 A 與 類別 B 存在直接相依性的問題: 123456789class A { val message: String}class B { fun sayHello(a: A) { println(a.message) }} 實現低耦合就是對兩類別之間進行解耦,解除類別之間的直接關係,將直接關係轉換成間接關係: 將類別共用方法抽離成 Interface,再直接使用 override 方法執行 123456789101112131415161718192021222324252627fun main() { Boy().sayHello() Girl().sayHello() Woman().sayHello()}interface Person { fun sayHello(): Unit}class Boy : Person { override fun sayHello() { println("Hello, Boy") }}class Girl : Person { override fun sayHello() { println("Hello, Girl") }}class Woman : Person { override fun sayHello() { println("Hello, Woman") }} 利用依賴注入(Dependency Injection, DI)方法達到類別彼此間的間接關係,即我們是將被依賴物件注入被動接收物件當中,以下面範例為例: 123456789101112131415161718192021222324252627282930313233fun main() { // 將被依賴物件注入被動接收物件 // 若學生有學習新的語言,只要新增一個類別再丟入 MyStudent 即可 MyStudent(English()).study() MyStudent(Chinese()).study()}// 建立學生類別class MyStudent(private val language: Language) { // 正在讀書 fun study() { language.speak() }}// 建立邏輯共用介面-語言interface Language { fun speak();}// 當需要 English 時,建立類別class English : Language { override fun speak() { println("學生正在練習英文口說") }}// 當需要 Chinese 時,建立類別class Chinese : Language { override fun speak() { println("學生正在練習中文口說") }} 抽象類別 abstract class在 Kotlin 中抽象類別會使用到 abstract 關鍵字,必須加在 class 或 function 前面,而抽象類別無法像普通類別一樣被實例化(Instance),它只能被類別繼承,而抽象類別也能使用建構函數進行外部參數引入,如下範例: 12345678910111213141516fun main() { SuperMan("Devin", "").hello()}// 定義抽象類別abstract class Person (val name: String, val email: String) { abstract fun hello()}// 定義class SuperMan(name: String, email: String) : Person(name, email) { override fun hello() { // 印出「我是 Devin」 println("我是 $name ") }} 抽象類別與介面主要差別還是在於使用場景或身份的不同,因類別只能單一繼承,所以使用抽象類別的子類別幾乎都是會有高關聯的,但介面不見得,我們可以依需求來選擇合適的介面實作,建議大家還是要依照需求來選擇合適方式。","categories":[{"name":"2020-IThome鐵人賽","slug":"2020-IThome鐵人賽","permalink":"http://devinwangg.github.io/categories/2020-IThome%E9%90%B5%E4%BA%BA%E8%B3%BD/"}],"tags":[{"name":"Kotlin","slug":"Kotlin","permalink":"http://devinwangg.github.io/tags/Kotlin/"}]},{"title":"[Day 09] 遠征 Kotlin × 例外處理","slug":"ironman-2020-09","date":"2020-10-11T11:21:46.000Z","updated":"2020-10-11T11:21:46.000Z","comments":true,"path":"2020/10/11/ironman-2020-09/","link":"","permalink":"http://devinwangg.github.io/2020/10/11/ironman-2020-09/","excerpt":"","text":"這章要來為大家介紹例外處理(Exception Handing),但其實在介紹例外處理之前,想先和大家介紹錯誤(Error)與例外(Exception)的差別,避免大家搞混兩者。 錯誤與例外錯誤(Error) 通常是指程式在正常運行之下,可能受到硬體資源影響所導致的錯誤,進而導致Java虛擬機(JVM)處於一種不正常且不可恢復的狀態,例如記憶體溢位 OutOfMemoryError。而像 Error 這類型的錯誤在Java 或 Kotlin 都是使用 Error 類別表示,不同錯誤類別代表著不同錯誤,且每個錯誤類別都是繼承自 Error 類別,例如StackOverflowError、OutOfMemoryError。 例外(Exception)通常是指程式的語法錯誤或語意錯誤,即程式在編譯時期(Compile Time)和執行時期(Execute Time)出現的錯誤,此類錯誤我們可以稱為例外(Exception),在Java 或 Kotlin 都是使用 Exception 類別來表示,而 Exception 又可分為 Checked Exception (已檢查例外)與 Unchecked Exception(未檢查例外),Checked Exception 是指程式碼必須明確配合例外檢查方法(例如 try/catch )進行檢查,否則編譯時期會無法編譯;Unchecked Exception 是指開發者可以自行判斷程式碼是否需要加上例外檢查,在編譯時期不會強制要求。 而在 Java 與 Kotlin 中, Error 與 Exception 都是繼承自 Throwable 類別,也只有 Throwable 類別才能夠拋出(throw)錯誤,類別關係可參考下圖: 圖片引用自 instanceofjava 文章 例外處理介紹接下來,我們來認識什麼是例外處理(Exception Handing ),它通常是指我們針對程式執行時所發生的例外狀況進行有效處理的方法,若我們能夠妥善處理例外情形,則可以提高程式的強健度,讓程式即便發生錯誤也能正常執行,不會因為某一個錯誤而導致整個軟體崩潰,保持良好的軟體使用體驗。 而 Kotlin 也是一門編譯型程式語言,即程式碼會先編譯成機器語言,再由編譯器進行執行。在編譯階段(Compiler Time)時,編譯器會檢查程式碼是否符合特定要求,確定沒問題後再進行編譯,例如在變數章節我們所提到的空值檢查機制,編譯器會幫我們判斷是否將 null 值指派給非空類型。 在文章開頭,我們有介紹什麼是例外(Exception),例外又可分為兩種類型,即 Checked Exception (已檢查例外)與 Unchecked Exception(未檢查例外),在 Java 世界中,兩種例外類型都有支援,但在 Kotlin 世界中,本身不支援 Checked Exception 類型(可參考官方文件說明),所以當我們撰寫的程式碼有可能拋出 Exception,在編譯時期都會直接通過,在執行時期才會發現。 可能有些人會好奇,Kotlin 取消 Checked Exception 類型會不會容易造成問題發生,Kotlin 官方其實也有做出回應並引用 Bruce Eckel 的論述: Examination of small programs leads to the conclusion that requiring exception specifications could both enhance developer productivity and enhance code quality, but experience with large software projects suggests a different result – decreased productivity and little or no increase in code quality. 當初 Kotlin 在設計考量時也是參考過去 Java 的開發經驗,設計讓部份錯誤檢查可以從執行時期提前到編譯時期發現,能夠讓我們更早發現程式問題,防患於未然,這也是選用 Kotlin 設計程式的優勢之一,假設保留Checked Exception 類型的話,當開發者在大型軟體專案中使用過多時,大部份開發上只會造成程式碼可讀性變差與程式碼品質下降。 例外處理方法- try..catch..finally上面我們介紹了例外處理的基本概念,我們再來介紹 Kotlin 處理例外狀況的方法,一般程式語言都是利用 try..catch..finally 來處理例外狀況,而 Kotlin 也不例外,使用方法與範例如下: 使用方法說明: 123456789try { // 預期可能會發生錯誤的程式碼}catch (exception: SomeException) { // 當發生錯誤時,則執行這裡的程式碼}finally { // 最後執行的程式碼區塊,此區塊可以忽略} 我們利用一個簡單範例,當運算式為 數字 1 除 數字 0時,程式會出現 ArithmeticException 類型錯誤: 1234567891011121314151617fun main(){ try { println("1.執行程式") val data = 1 / 0 } catch (exception: ArithmeticException) { println("2.發生錯誤") println(exception) } finally { println("3.最後執行的程式碼") }}// 此程式會輸出以下結果// 1.執行程式// 2.發生錯誤// java.lang.ArithmeticException: / by zero// 3.最後執行的程式碼 例外處理方法-主動拋出例外 throwKotlin 也可以允許開發者主動拋例外物件,會由 throw 運算子所觸發,拋出異常就代表程式若要繼續執行,必須先解決這個問題才能夠正常繼續執行,例如以下範例,我們有一個函數是判斷數值是否符合正整數,如果不符合則主動拋出例外錯誤: 例外處理方法-自訂例外錯誤類別我們也可以自定義一個例外錯誤類別,宣告一個類別並繼承 Exception 類別即可,自定義例外錯誤類別可以讓我們在除錯時更清楚拋出的資訊是屬於哪一種問題,例如以下範例: 責任分工上面是我們一般會對於處理例外狀況所撰寫的程式方法,但實際上在這三個區塊會有各自要處理的責任,這邊稍微簡單說明: try 區塊除了提到必須負責實作業務需求之外,也必須負責準備錯誤發生時的狀態回復方法,建立將程式狀態回復至發生錯誤之前的方法 catch 區塊除了回報錯誤狀況以外,其實還要身兼錯誤的對應處理或重試其他替代方案,例如上面範例,當使用者輸入為錯誤數值時,是否可以藉由 Catch 提供錯誤提示給予使用者,給予使用者選擇重試或取消此計算功能 finally 區塊則是擔任釋放資源與回報發生錯誤例外的角色,假設我們執行的程式碼是與資料庫溝通的程式,在 finally 則是必須釋放資料庫連線。 此篇文章介紹了例外處理的基本概念與使用方法,其實例外處理要考量的地方還有許多,可能沒辦法利用一篇文章進行詳細說明,後續在 Spring Boot 章節會再補充例外處理的方法,而在實務開發上,團隊通常也都會有一份開發規定進行對應處理,方便每個人在開發專案時有一定的共識。 Reference [官方] Kotlin 官方資料 [文章] Kotlin & Checked Exception [文章] Java的try、catch、finally(7):責任分擔","categories":[{"name":"2020-IThome鐵人賽","slug":"2020-IThome鐵人賽","permalink":"http://devinwangg.github.io/categories/2020-IThome%E9%90%B5%E4%BA%BA%E8%B3%BD/"}],"tags":[{"name":"Kotlin","slug":"Kotlin","permalink":"http://devinwangg.github.io/tags/Kotlin/"}]},{"title":"[Day 10] 遠征 Kotlin × 泛型 Generic","slug":"ironman-2020-10","date":"2020-10-11T11:21:25.000Z","updated":"2020-10-11T11:21:25.000Z","comments":true,"path":"2020/10/11/ironman-2020-10/","link":"","permalink":"http://devinwangg.github.io/2020/10/11/ironman-2020-10/","excerpt":"","text":"泛型 Generic 介紹在 Collections 章節中,我們有提到 List、Set 等集合用法,眼尖的朋友可能會發現到,在宣告一個新集合時,我們都必須使用 < > 和設定型態來進行宣告, 而這樣的方法其實就是一種泛型(Generic)應用,集合只是一個容器,為我們提供迭代元素、新增元素、刪除元素等操作方法,當我們需求上需要儲存什麼樣別的資料,再為這個容器加上型別即可使用。 可能有朋友會好奇,為什麼會需要使用泛型?假設今天我們有一個集合物件,裡面充滿著各種型別的資料,那我們在引用這個集合時,必然會在程式撰寫上處理許多型別轉型的工作,而型別轉型工作會讓我們程式多了一層轉換工作,所以為了減少不必要的轉型工作,我們可以提前在編譯期間(Compile Time)利用泛型告知此集合或方法屬於哪種型別,也是方便我們在開發時清楚要使用什麼類型的資料進行溝通,所以在型別安全檢查、程式碼品質、開發效率等都會帶來好處。 泛型使用泛型可以讓我們使用在類別、介面、函數上,我們可以直接使用下面範例來觀察,我們定義一個使用泛型的Person 類別,再定義其他資料類別, 12345678910111213141516171819202122fun main() { val teacher: Person<Teacher> = Person(Teacher("Eric", "A12345")) val student: Person<Student> = Person(Student("Devin", "B991")) println("老師姓名: ${teacher.data.name}, 職員編號: ${teacher.data.employeeNumber}") println("學生姓名: ${student.data.name}, 學號: ${student.data.studentID}") // 印出以下結果: // 老師姓名: Eric, 職員編號: A12345 // 學生姓名: Devin, 學號: B991}// 建立一個使用泛型物件的類別class Person<T>(person: T) { var data: T = person}// 建立一個老師類別,具有name、employeeNumber參數class Teacher(val name: String, val employeeNumber: String)// 建立一個學生類別,具有name、studentID參數class Student(val name: String, val studentID: String) 泛型參數我們通常會利用字母 T(英文 Type)表示,若要使用其他名稱也可以,但在支援泛型的程式語言中大多使用 T 來表示,這樣可以讓其他開發者更容易了解我們的程式碼,而泛型還有其他常用的命名,如下: E - Element K - Key N - Number T - Type V - Value R - Return S, U, V etc. - 2nd, 3rd, 4th types 多泛型參數泛型也允許使用多個泛型參數,參數名稱建議可參考上面常見規範,我們可以將上面的範例進行修改,在原本的 Person 類別中加入一個支援多種泛型的函數,如下範例: 123456789101112131415161718192021222324fun main() { val teacher: Person<Teacher> = Person(Teacher("Eric", "A12345")) val student: Person<Student> = Person(Student("Devin", "B991")) println("老師姓名: ${teacher.data.name}, 職員編號: ${teacher.data.employeeNumber}") println("學生姓名: ${student.data.name}, 學號: ${student.data.studentID}") teacher.speak { println("${teacher.data.name}: 開始上課")} student.speak { println("${student.data.name}: 老師好")}}// 建立一個使用泛型物件的類別class Person<T>(person: T) { var data: T = person // 定義一個支援 lambda 運算式的函數,R為新增的泛型參數,作為函數的返回類型 fun <R> speak(func: (T) -> R): R? { return func(data) }}data class Teacher(val name: String, val employeeNumber: String)data class Student(val name: String, val studentID: String) 多泛型實例操作上面範例我們都只使用一個資料進行操作,若我們想要一次使用多筆資料,此時可以使用 vararg 關鍵字,讓泛型類別可支援多個參數,參數即為元素陣列,而既然是陣列資料,我們就可以使用索引進行取值,我們可以搭配 get 運算函數進行索引取值動作,如下範例: 12345678910111213141516171819202122232425262728fun main() { val teacher: Person<Teacher> = Person(Teacher("Eric", "A12345")) val student: Person<Student> = Person(Student("Devin", "B991"), Student("Jack", "B992")) println("老師姓名: ${teacher[0]?.name}, 職員編號: ${teacher[0]?.employeeNumber}") println("學生姓名: ${student[0]?.name}, 學號: ${student[0]?.studentID}") println("學生姓名: ${student[1]?.name}, 學號: ${student[1]?.studentID}") teacher.speak(0) { println("${teacher[0]?.name}: 開始上課")} student.speak(0) { println("${student[0]?.name}: 老師好")} student.speak(0) { println("${student[1]?.name}: 老師好")}}// 建立一個使用泛型物件的類別class Person<T>(vararg person: T) { // 修改 data 資料型別為 Array<out T>, out 代表我們要將泛型 T 作為內部函數的返回值 var data: Array<out T> = person operator fun get(index: Int): T? = data[index] fun <R> speak(index: Int, func: (T) -> R): R? { return func(data[index]) }}data class Teacher(val name: String, val employeeNumber: String)data class Student(val name: String, val studentID: String) in & out在前一個範例中我們有用到 out 關鍵字,我們發現若在泛型類別中要將泛型用在內部函數的返回值上,必須加上 out 關鍵字,而 out 關鍵字其實有一個夥伴- in 關鍵字,in 則是將泛型用在函數參數值上。 而泛型參數其實扮演兩種角色:生產者(producer) 或 消費者(consumer),若身為生產者時,只能讀不能寫;消費者則相反,不能讀只能寫,而生產者為 out 關鍵字,消費者則為 in 關鍵字。 接下來,我們利用範例來觀察 in & out 的實際狀況,首先介紹 out 關鍵字, out 泛型可以讓我們將子類別的泛型物件賦值給父類別泛型物件: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051fun main() { // 正常情況 // 食品商店屬於食品商店 val producer1 : Production<Food> = FoodStore() // 速食商品屬於食品商店 val producer2 : Production<Food> = FastFoodStore() // 漢堡商店也屬於食品商店 val producer3 : Production<Food> = InOutBurger() // 錯誤情況 // 食品商店不見得屬於漢堡商店 // val producer1 : Production<Burger> = FoodStore() // 速食商店也不見得屬於漢堡商店// val producer2 : Production<Burger> = FastFoodStore() // 漢堡商店屬於漢堡商店// val producer3 : Production<Burger> = InOutBurger()}open class Foodopen class FastFood : Food()class Burger : FastFood()// 定義一個生產者介面,運用 out 關鍵字interface Production<out T> { // 將泛型 T 作為回傳值 fun produce(): T}// 定義一個 FoodStore 類別,並利用 Food 類別實作生產者介面class FoodStore : Production<Food> { override fun produce(): Food { println("食品商店") return Food() }}// 定義一個 FastFoodStore 類別,並利用 FastFood 類別實作生產者介面class FastFoodStore : Production<FastFood> { override fun produce(): FastFood { println("速食商店") return FastFood() }}// 定義一個 FastFoodStore 類別,並利用 FastFood 類別實作生產者介面class InOutBurger : Production<Burger> { override fun produce(): Burger { println("漢堡商店") return Burger() }} 再來是 in 關鍵字的用法,in 泛型可以讓我們將父類別泛型物件賦值給子類別泛型物件,以下是範例: 123456789101112131415161718192021222324252627282930313233343536373839404142fun main() { // 正常情況 // 想購買肉品食物的消費者可能也會想買漢堡 val consumer1 : Consumer<Burger> = PurchaseFood() // 想購買速食食物的消費者可能也會想買漢堡 val consumer2 : Consumer<Burger> = EatFastFood() val consumer3 : Consumer<Burger> = EatBurger() // 錯誤情況// val consumer1 : Consumer<Food> = PurchaseFood() // 直接想購買速食商品的消費者通常不會想買肉品// val consumer2 : Consumer<Food> = EatFastFood() // 直接想購買漢堡商品的消費者通常不見想買肉品// val consumer3 : Consumer<Food> = EatBurger()}// 利用 in 關鍵字配合泛型interface Consumer<in T> { // 將泛型 T 作為函數參數 fun consume(item: T)}// 想購買食品商品class PurchaseFood : Consumer<Food> { override fun consume(item: Food) { println("購買食品商品") }}// 想購買速食食物class EatFastFood : Consumer<FastFood> { override fun consume(item: FastFood) { println("購買速食食物") }}// 想購買漢堡食物class EatBurger : Consumer<Burger> { override fun consume(item: Burger) { println("購買漢堡食物") }} Reference 【官方】Kotlin 官方文件 【文章】Kotlin 泛型中的 in 和 out 【文章】深入理解 Java 泛型","categories":[{"name":"2020-IThome鐵人賽","slug":"2020-IThome鐵人賽","permalink":"http://devinwangg.github.io/categories/2020-IThome%E9%90%B5%E4%BA%BA%E8%B3%BD/"}],"tags":[{"name":"Kotlin","slug":"Kotlin","permalink":"http://devinwangg.github.io/tags/Kotlin/"}]},{"title":"[Day 11] 遠征 Kotlin × 函數式程式設計","slug":"ironman-2020-11","date":"2020-10-11T11:20:26.000Z","updated":"2020-10-11T11:20:26.000Z","comments":true,"path":"2020/10/11/ironman-2020-11/","link":"","permalink":"http://devinwangg.github.io/2020/10/11/ironman-2020-11/","excerpt":"","text":"函數式程式設計特性我們在前面函數章節有提到 Lambda 的基本概念,而如果我們想要更好運用 Lambda 語法與相關函數API,可以先認識函數程式設計(Functional Programming, 簡稱 FP)會有非常大幫助,FP 是一種程式設計方法,與前面章節提到的物件導向程式設計(Object-Oriented Programming)是不同的設計概念,兩種設計的思考方式有許多不同,FP 主要會有以下特性: 會依賴於前面函數章節提到的高階函數(Higher-Order Functions)所傳回的結果,所謂高階函數即為利用函數作為參數或返回值的方法 函數必須符合第一類物件(First-Class-Object)原則 保持純函數(Pure functions)特性,即函數在執行時,不會有任何副作用(Side Effect)的狀況,無副作用是指函數內部不會影響到函數外部的任何狀態 保持 immutable 特性,即資料一經賦值後就不能被修改,重視函數回傳結果(Output),不修改傳入的參數狀態(Input) 函數類別一般我們在使用 FP 設計方法時,通常會由三種函數所構成-轉換(Transform)、過濾(Filter)與合併(Combine),每種函數目標都是為了取得最終結果進行設計,而函數彼此間可以互相配合使用,代表我們可以利用這樣的特性關係,將多個函數進行組合,處理複雜的計算行為。 轉換(Transform)是指我們會將輸入(Input)參數利用轉換器進行特定條件處理,再回傳處理的新結果,在 Kotlin 中常使用的轉換函數為 map 和 flatMap,可參考以下範例: 1234567fun main() { val data = listOf<Int>(1, 2, 3) val result = data.map { it * 2} println(result) // 印出 [2, 4, 6]} 過濾(Filter)則是具有過濾符合特定條件的作用,一般會配合條件運算式(predicate)函數,利用此函數判斷傳入參數是否符合條件判斷,依照判斷結果回傳 true 或 false,若為 true,則將元素加入返回的新集合內,我們可以運用 filter 函數進行過濾處理,可參考以下範例: 12345678910fun main() { // 建立原始資料 val data = listOf<Int>(1, 2, 3) // 進行過濾的結果資料 val result = data.filter { it > 1 } .map { it * 2} println(result) // 印出 [4, 6]} 合併(Combine)是將不同資料或不同集合組合成一個新集合,我們可以運用 zip 合併函數進行合併處理,可參考以下範例: 1234567891011121314fun main() { // 建立員工資料 val personData = listOf<String>("Devin", "Eric", "Mary") // 建立薪資資料 val salaryData = listOf<Int>(1300, 1500, 1200).map { it * 5 } // 組合員工與薪資資料 val result = personData.zip(salaryData).toMap() // 印出員工對應的薪水資料 println(result["Devin"]) println(result["Eric"]) println(result["Mary"]) // 印出 6500, 7500, 6000} 標準函數在 Kotlin 標準函式庫中有提供一些支援 lambda 的標準函數-Scope Function,如 apply、let、run、also、takeIf等五種常用函數,若能善用這些函數進行開發,會讓我們的程式增加可讀性,以下分別進行介紹: apply apply 函數可視為配置函數,將需要設定的接收者傳入,再針對需求進行函數設定,例如以下範例: 12345678fun main() { // 使用 apply 函數,可更直觀的方式進行設定 val fileUsingApply = File("data.txt").apply { setReadable(true) setWritable(true) setExecutable(false) }} let let 函數可以產生一個暫時變數(預設為 it)作用於 lambda 運算式,let 只會將最後一行作為返回值(lambda 結果值)進行回傳,例如以下範例 123456789fun main() { // 建立一個數值集合 val data: List<Int> = listOf<Int>(4, 5, 6) // 取得集合第一個資料並使用 let 函數進行相乘 val result = data.first().let { it * it } println(result) // 印出結果為 16} run run 函數與 apply 函數相似,差別在於 run 函數不會返回接收者,返回的是一個 Lambda 結果,例如以下範例: 123456789fun main() { val data: List<Int> = listOf<Int>(4, 5, 6) val result = data.first() // 取得集合第一個資料 .let { it * it } // 利用 let 函數進行相乘 .run { this == 16 } // 利用 run 函數判斷結果值是否等於 16 println(result) // 印出結果為 true} also also 函數與 let 函數相似,差別在於 also 函數返回的是接收者,而 let 函數返回的是 Lambda 結果,參考以下範例: 123456789101112131415fun main() { val data: List<Int> = listOf<Int>(4, 5, 6) // 取得集合第一個資料並使用 also 函數進行內部計算 val result = data .first() .also { val calculateResult = it * it println("相乘計算結果 $calculateResult") } println("返回結果:$result") // 印出結果為 // 相乘計算結果 16 // 返回結果 4 -> 代表 also 是回傳原接收者物件} takeIf takeIf 與前面介紹的函數有些不同,takeIf 函數必須與 Lambda 提供的條件運算式(predicate)函數進行搭配使用,如果條件運算式成立結果為 true,則 takeIf 函數則會回傳原接收者物件,反之,若為 false,就會回傳 null,可參考以下範例: 12345678910fun main() { val data: List<Int> = listOf<Int>(4, 5, 6) val result = data.first() // 取得集合第一個資料 .run { this + 4 } // 利用 run 函數進行數值加 4, .takeIf { it == 8 } // 利用 takeIf 函數搭配判斷運算式,若數值符合則回傳計算值,不符合則回傳 null println("返回結果 $result") // 印出「返回結果 8」} 結論在這篇提到函數程式設計的基本概念、三種函數設計與 Lambda 相關函數介紹,後續也會逐漸在 Spring Boot 章節進一步介紹實際運用。這邊也希望大家能夠理解函數程式設計只是一種設計方法,而既然是設計方法,就不會有所謂的好壞之分,只有應用場景是否適合的差別,而 Kotlin 可支援多種程式設計方法,有時候我們也會混用物件導向程式設計與函數式程式設計解決手上的專案需求。 Reference [官方] Kotlin 官方文件 [文章] Kotlin 的 scope function:Kotlin 的 scope function: apply, let, run..等等 [書籍] Kotlin Programming: The Big Nerd Ranch Guide","categories":[{"name":"2020-IThome鐵人賽","slug":"2020-IThome鐵人賽","permalink":"http://devinwangg.github.io/categories/2020-IThome%E9%90%B5%E4%BA%BA%E8%B3%BD/"}],"tags":[{"name":"Kotlin","slug":"Kotlin","permalink":"http://devinwangg.github.io/tags/Kotlin/"}]},{"title":"[Day 12] 遠征 Kotlin × 進入 Spring Boot 世界","slug":"ironman-2020-12","date":"2020-10-11T11:18:55.000Z","updated":"2020-10-11T11:18:55.000Z","comments":true,"path":"2020/10/11/ironman-2020-12/","link":"","permalink":"http://devinwangg.github.io/2020/10/11/ironman-2020-12/","excerpt":"","text":"前言從這章開始我們即將進入 Spring Boot 世界,前面章節是希望能與大家一起了解 Kotlin 基本語法,再進入後面章節的 Web 開發介紹,而自己在接觸 Spring 框架之前,其實有先接觸過 .Net MVC、PHP Laravel、Python Flask、Django等框架,後來才在因緣際會之下進入 Spring 世界進行開發,還記得當初開始寫 Spring 時,其實對於 Spring 生態系有感受到不太好上手的狀況,但也有可能是因為我一開始接觸的 Spring 專案是早期開發配置的關係,就是利用許多XML配置和充滿非常多jar檔案的專案,光是在環境設定與學習就花了非常久的時間。 如果有朋友與我一樣,在因緣際會之下來到了 Spring 世界,我覺得可以從 Spring Boot 框架出發,至少在學習這條路上,開發的成就感會比較大,接下來的文章,我也將會從 Spring Boot 基礎由淺入深的方式進行介紹,與大家一同學習。 Spring 介紹早期 Sun 公司在 1999 年 6月公佈新的Java體系架構,該架構根據不同級別的應用開發區分許多不同的應用版本,即J2SE(Java 2 Platform, Standard Edition)、J2EE(Java 2 Platform, Enterprise Edition)與J2ME(Java 2 Platform, Micro Edition): J2SE 主要用於開發桌面應用軟體的程式設計 J2EE 主要用於網路程式開發 J2ME 主要用於嵌入式系統開發 而 J2SE、J2EE與J2ME 是當時所命名的名稱,直到 Java SE 6 出世後,Java不再帶有 2 這個數字,因此 J2SE、J2EE與J2ME分別被命名為Java SE、Java EE與Java ME,而 Sun 公司在2006年底,就將三大平台正名為Java SE、Java ME與Java EE,但時至今日,J2SE、J2ME與J2EE這個名詞還是很多人用。 其中,Java SE 又身為各應用平台基礎,許多人剛開始學習 Java 時,最先接觸的也是Java SE,它又可分作四個主要的部份:JVM、JRE、JDK與Java語言。為了要能運行Java撰寫好的程式,必須有Java虛擬機器(Java Virtual Machine, JVM),JVM 包括在 Java 執行環境(Java SE Runtime Environment, JRE)中,所以為了要運行Java程式,必須安裝JRE。如果要開發Java程式,必須取得JDK(Java SE Development Kits),JDK包括JRE以及開發過程中需要的一些工具程式,像是javac、java等工具程式。 Java EE 也以 Java SE 為基礎,定義了一系列的服務、API、協定等,適用於開發分散式、多層式架構、以元件為基礎、以Web為基礎的應用程式, 整個 Java EE 的體系其實是相當龐大的,比較為人熟悉的技術像是Java Server Pages (JSP)、Servlet、JavaMail、Enterprise JavaBeans(EJB)等。而早期J2EE應用程式是由 JSP、Java Servlet 與 EJB 模組等元件所組成,這些元件可供軟體開發人員建立大型之分散式應用程式,再由開發人員將 J2EE 應用程式封裝為 JAR 檔案部署至應用程式伺服器(Application Server)。 而 Spring Framework 是一個基於Java EE 的 MVC 框架,在設計目標上主要是為了簡化 Java EE 的應用程式開發為目的,進而取代 Java EE 早期非常龐大的技術-EJB,相較於 EJB 而言,Spring提供了更輕量和簡單的方法建構應用程式,加強 Java Plain Old Java Object(POJO) 功能,使整個框架具備之前只有 EJB 和其他企業級 Java 規範才具有的功能。 Spring 是一套開源框架,最早是由 Rod Johnson 為了解決企業級應用的開發複雜性所設計出來的框架,在Spring設計核心中,採取四種關鍵原則: 使用POJO進行輕量級及最小侵入式開發 透過依賴注入(Dependency, DI)和介面(Interface)實現鬆耦合 透過AOP(Aspect Oriented Programming)和默認習慣進行宣告式程式設計 透過AOP和樣板減少模式化程式碼 簡單來說,就是利用 Annotation 告訴 Spring 框架,標註的程式碼是代表什麼,進而減少重複例行性程式碼 在過去,Java 因具備有物件導向設計特性,大幅提升程式碼的維護與重用性,但也造成容易產生類別與類別之間的依賴關係,當專案不斷龐大時,程式容易造成高耦合性的發生。而在 Spring 框架核心部份運用了控制反轉(Inversion of Control, IOC)與依賴注入(Dependency Injectionm, DI), 解決前述所提到的狀況。 IOC 其實是一種設計概念,將某物件對另一物件的控制權移轉給第三者進行管理,例如A物件程式內部需要使用到B物件時,代表A、B兩個物件具有依賴關係,而控制反轉則是將A對B的控制權移轉給第三者,讓A與B都必須倚賴第三者,降低A對B物件的耦合性。 以Spring框架來說,IOC概念主要運用在Spring可建立Bean物件負責控制物件的生命週期和物件間的關係,而Bean 只是普通的Java物件,由Spring IOC容器根據XML文件、Java Annotation、Java Config文件進行創建、配對和管理。IOC容器執行的主要任務有「創建Bean物件」、「根據配置文件配對相依的Bean」、「為Bean設置初始化參數」、「管理Bean生命週期」,可讓開發者只需聲明所需要的物件,就可輕鬆達到寬鬆耦合(loose coupling)的目的,直接使用Bean的功能。IOC容器在 Spring 中有兩種類型(BeanFactory、ApplicationContext),其中 ApplicationContext 又比 BeanFactory 更加強大,故經常在專案中看見 ApplicationContext 的使用。 如上所述,IOC容器會根據開發者設定方式進行 Bean 的操作,而 Bean 目前在 Spring 可配置的方式有三種方式: XML - 使用 XML 文件進行配置 Annotation - 使用 @Service 或 @Component 註解配置 Java - 從 Spring 3 開始,可使用 Java 程式配置Bean,主要註解是 @Configuration、@ComponentScan 和 @Bean DI 則是實現 IOC 的方法之一,是達到 抽離類別實體化 行為的一種設計模式,即是把被依賴物件注入被動接收物件中,有效解決兩個類別間耦合性過高的問題,通常會搭配介面(Interface)方式進行注入,DI概念與範例在前面的函數章節有提到。 前述有提到,Spring Framework是一個基於 Java EE 的 MVC 架構,常見的 MVC 架構也就是將一個應用程式(Application)架構分為模型層(Model)、展示層 (View)、控制層(Controller),通常Model會有一般類別或資料庫存取邏輯,View是與使用者互動的介面,Controller則是將Model與View串連起來的關鍵角色,而 Spring 除了 MVC 架構之外,它還提供相當多的元件,如:Spring Security、Spring Validation、Spring Data等,讓我們在開發應用程式時,能夠將更多時間專注在系統的業務邏輯實現上。 以一個最基本的 Spring Controller來舉例,它本身就是一個類別,我們在撰寫時,會定義 package 以及需要載入(import)的類別,再利用 Java Annotation-@Controller 定義這個類別是個Controller。在類別中,只要指定HelloWorld方法的RequestMapping,也就是指定這個Controller的相對URI。以下面的程式範例來說,當瀏覽器要求 /Hello (實際網址為:http://localhost:8080/Hello 就會呼叫sayHi() 方法,而sayHi()方法的回傳值會是執行完這段程式碼之後要執行的事情,即回傳「Hello Spring & Kotlin」字串。 1234567891011121314package com.devin;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;@Controllerclass HomeController { @RequestMapping("/Hello") @ResponseBody fun sayHi(): String { return "Hello Spring & Kotlin" }} 上述這隻範例程式,當我們在瀏覽器輸入URL後,讓我們得到對應的字串「Hello Spring & Kotlin」,其實是透過 Web Container 與 Servlet 進行 HTTP 傳遞與處理,早期 JSP 檔案會被 Web 容器轉譯為 Servlet 的 .java 原始檔,再編譯為 .class 檔案載入容器進行運作,而在 Spring Web 中任何 Entry point 都是 Servlet,而 Web Container 是 Servlet 唯一認得的 HTTP 伺服器,以我們範例而言,使用的 Web Container 是 Tomcat。 上述有提到 Servlet 概念,Java Servlet 其實是運行在 Web 伺服器或應用伺服器上的程式,它是作為來自 Web 瀏覽器或 HTTP 用戶端請求和 HTTP 伺服器上的資料庫或應用程式之間的中介層(Middleware),使用Servlet,可以用來收集網頁表單的輸入值、呈現來自資料庫的資料或是可以動態創建網頁。 圖片引用自 w3big Servlet介紹 最後,Spring Boot 其實不算是一門新技術,以本質上來說,Spring Boot 就是 Spring,它是為了簡化 Spring 應用的建立、執行、除錯、部署等而出現的,使用它可以讓我們把時間更專注於業務邏輯的需求開發,無需過多關注 XML 配置,讓開發者可快速構建Spring應用。 Spring Boot 特色是: 能夠快速開發基於 Spring 的應用程式 預設使用內嵌的 Tomcat 作為應用伺服器 自動管理套件依賴版本 方便開發各種對外服務,如 REST API、WebSocket、Web、Streaming、Tasks 提供快速方便使用的微服務相關技術 提供一些大型項目常用的非功能性特性,例如:嵌入式服务、安全、監控、健康檢查、外部配置 Reference [官方] Spring Boot 官方文件 [文章] 一文读懂 Spring Boot、微服务架构和大数据治理三者之间的故事","categories":[{"name":"2020-IThome鐵人賽","slug":"2020-IThome鐵人賽","permalink":"http://devinwangg.github.io/categories/2020-IThome%E9%90%B5%E4%BA%BA%E8%B3%BD/"}],"tags":[{"name":"Spring Boot","slug":"Spring-Boot","permalink":"http://devinwangg.github.io/tags/Spring-Boot/"}]},{"title":"[Day 13] 遠征 Kotlin × 建置 Spring Boot 專案","slug":"ironman-2020-13","date":"2020-10-11T11:18:22.000Z","updated":"2020-10-11T11:18:22.000Z","comments":true,"path":"2020/10/11/ironman-2020-13/","link":"","permalink":"http://devinwangg.github.io/2020/10/11/ironman-2020-13/","excerpt":"","text":"編輯器環境說明接下來,這篇文章將帶大家建置 Spring Boot 專案,本系列文章使用的編輯器為 Intellij ,但因為使用的 intellij 為 ultimate 版本,ultimate 本身其實會內建許多工具,例如建立 Spring Boot 專案時會有一個快速建置 Spring Boot 專案的選項 Spring Initializr,如下圖: 如果正在閱讀的朋友是使用 community 版本的話,大家可以到 intellij 的 Preferences → Plugins → 安裝 Spring Assistant 套件,即可在 Create 專案裡面找到 Spring Boot 起始專案建立選項,如下圖(1)、(2)所示: (1) 至 intellij 的 Preferences → Plugins → 搜尋 Spring Assistant 套件 (2) 建立專案(New → Project)時,會出現 Spring Assistant 選項,即可像 ultimate 版本快速建置 Spring Boot 專案 建置步驟接下來,我們就來建置一個 Spring Boot 專案: (1) 開啟 intellij 編輯器 → New → Project 建立專案 (2) 此頁面是可以設定 Project Metadata 資訊,資料除了meta設定以外,要記得在 Language 選擇 Kotlin,Java 版本要記得用自己環境裝的JDK版本,此範例使用 Java 8 進行開發,Type 選擇使用 Maven Project (下一個章節也會介紹如何用 Gradle 建置專案) (3) 設定 Spring Boot 版本與勾選需要的套件,這邊可以先勾選 Web -> Spring Web,Spring Boot 版本選擇 2.3.4 (4) 設定專案路徑 (5) 若您的版本為 ultimate 版本,可以直接點擊右上角執行專案,可以參考下面第一張圖(a),如果是 Community 版本,因為沒有預設啟動功能,但仍可以利用右側 Maven 區塊點擊 Plugins → spring-boot → spring-boot:run 的方式運行專案,則參考下面第二張圖(b) a. ultimate 版本直接點擊右上角運行鈕(三角形)運行專案 b. community 版本可以用 Maven 區塊點擊 Plugins → spring-boot → spring-boot:run 的方式運行專案 (6) 可以在 DemoApplication.kt 檔案中加入下面這段程式,請參考下面圖(a),加入程式碼後再重新運行程式,打開瀏覽器進入(http://localhost:8080/hello) 即可看到「Hello, Kotlin」文字,可參考下圖(b)、(c) a. 在 DemoApplication.kt 直接在下面加入以下程式 1234567@RestControllerclass HomeController() { @GetMapping("/hello") fun getHelloString(): String { return "Hello, Kotlin" }} b. 加入程式碼後的結果圖 c. 執行專案後,打開瀏覽器觀看執行結果「Hello, Kotlin」 以上,我們已經完成 Spring Boot 專案建置,也成功設計了一隻取得「Hello, Kotlin」文字的API,我們可以感受到 Spring Boot 協助我們快速進入開發,若是使用以往的Spring配置,可能還需要設定非常多環境設定才可以進行開發動作,而後續章節會陸續開始說明每個 Spring Boot 開發應用。","categories":[{"name":"2020-IThome鐵人賽","slug":"2020-IThome鐵人賽","permalink":"http://devinwangg.github.io/categories/2020-IThome%E9%90%B5%E4%BA%BA%E8%B3%BD/"}],"tags":[{"name":"Spring Boot","slug":"Spring-Boot","permalink":"http://devinwangg.github.io/tags/Spring-Boot/"}]},{"title":"[Day 14] 遠征 Kotlin × Spring Boot 專案配置介紹","slug":"ironman-2020-14","date":"2020-10-11T11:17:57.000Z","updated":"2020-10-11T11:17:57.000Z","comments":true,"path":"2020/10/11/ironman-2020-14/","link":"","permalink":"http://devinwangg.github.io/2020/10/11/ironman-2020-14/","excerpt":"","text":"文章回顧昨日,我們將 Spring Boot 專案建置完成,可能會有朋友好奇,為什麼我們在 DemoApplication.kt 裡面加上一段程式碼,就可以用網址找到對應的字串結果,我們來看看昨日我們加入的程式: a. 直接在 DemoApplication.kt main函數下面加入以下程式 1234567@RestControllerclass HomeController() { @GetMapping("/hello") fun getHelloString(): String { return "Hello, Kotlin" }} b. 加入程式碼後的結果圖 其實我們可以觀察建置專案完成後,DemoApplication.kt 檔案的 main 函數上面會有一個@SpringBootApplication Annotation,此標註其實是 Spring Boot 專案核心標註,目的是為了開啟自動配置,而此標註裡面其實也包含了許多功能,我們可以直接進入看 @SpringBootApplication 的定義內容,如下圖: 我們會觀察到它的類別名稱上使用了許多標註,我們快速簡介每個標註的作用: @Target:描述此標註會用在什麼樣的地方@Retention:指定此標註會保留多長時間@Documented:主要作用是產生 javadoc 文件時,會將該標註寫入文件中@Inherited:此標註用在類別上時,表示子類別會自動繼承此標註@SpringBootConfiguration:繼承自 @Configuration,標註當前類別屬於配置型態@EnableAutoConfiguration:啟動自動加入配置,導入專案內有使用到的套件@ComponentScan:掃描專案內所有的 @Controller、@Service、@Component、@Repository 標註 此時,我們在上面所困惑的問題就可以知道答案,是因為 Spring Boot 有透過 @ComponentScan 標註可進行抓取對應的類別與程式,而 Spring Boot 的設計目的就是將原本開發 Spring 常見複雜的開發配置進行自動化處理,讓我們快速建構專案進行後續業務邏輯的開發。但實務開發上,我們還是得需要了解如何在 Spring Boot 修改這些自動化配置,以應付實務上特殊的開發環境,故下面將會說明 Spring Boot 預設提供的 [application.properties](http://application.properties) 檔案,此檔案可以讓我們針對專案進行配置調整。 專案配置檔案-application在 Spring Boot 專案建立完成後,會自動在專案內建立一個 application.properties 檔案進行應用程式的配置,全部參數可從 Spring 官方文件中找到說明,此篇會先說明常用的幾個參數進行說明: 配置檔案 application 預設是提供檔案格式 properties 進行配置,也同時支援現在廣泛推薦使用的 YAML 檔案格式,但 YAML 文件格式風格不像 properties 是以 鍵值(Key-Value) 文件格式表示,反而是比較偏向類似 階層縮排 文件格式進行表示,下面我們利用範例來看看兩者差異: application.properties 12345678server.port = 9999 # 設定 Spring Boot 運行專案的 port 阜號spring.jpa.show-sql = true # Spring Data JPA 相關資訊 - 後面章節會說明spring.jpa.hibernate = truespring.jpa.hibernate.ddl-auto = truespring.jpa.database = h2spring.h2.console.enabled = true # Spring H2 資料庫相關資訊 - 後面章節會說明spring.h2.console.path = /h2-consolespring.datasource.url = jdbc:h2:file:./src/main/resources/data/employees;AUTO_SERVER=true application.yml 1234567891011121314server: port: 9999 # 設定 Spring Boot 運行專案的 port 阜號spring: jpa: show-sql: true # Spring Data JPA 相關資訊 - 後面章節會說明 hibernate: ddl-auto: update database: h2 h2: # Spring H2 資料庫相關資訊 - 後面章節會說明 console: enabled: true path: /h2-console datasource: url: jdbc:h2:file:./src/main/resources/data/employees;AUTO_SERVER=true 由上面範例我們可以看到,YAML檔案格式在配置上利用階層縮排的方式表達,結構上相對更為清晰易讀,而在 Spring Boot 中若使用 YAML 檔案格式進行配置時,會無法直接支援 @PropertySource Annotation 進行配置,可以參考下圖官方文件說明,還有 YAML 在撰寫上是必須要依照順序的,否則會無法判讀,而兩種檔案格式的好壞其實是比較主觀的,很難定論哪一種格式特別好,通常會以團隊開發習慣來進行規範,而本系列目前會先採用 YAML 格式進行實作。 在 Spring 世界目前主要有兩大專案建構工具-Maven、Gradle,專案建構工具可以幫助我們處理專案說明、建構專案、依賴套件、部署等專案開發事項,幫助我們讓專案有規範、自動與彈性擴充,而 Maven 早於 Gradle 發布,Maven 在文件格式上仍舊使用 XML 格式作為編寫建構配置,Gradle 則是不使用 XML 方式,採用 Groovy DSL (Domain Specific Languages)特性進行編寫,使得 Gradle 在建構配置上會比 Maven 來得更簡潔清晰、靈活性更好。 而昨天我們使用了 Maven 進行專案建置,今天我們嘗試將建立專案改為 Gradle 進行建置,只要將 New Project的 Type 改為 Gradle Project 即可,如下圖所示: (1) 修改 New Project Type 為 Gradle Project、Language 為 Kotlin (2) 運行專案可使用Gradle面板 Tasks → application → bootRun,或是直接在 Terminal 輸入 ./gradlew bootRun(參考圖 3) (3) 如不想使用 Gradle 功能表運行的朋友,也可以使用 Terminal 進行專案運行,如果是使用 Linux/Mac 的朋友輸入「./gradlew bootRun」即可,如果是使用 Windows 的朋友,可以輸入「gradlew.bat bootRun」 後續文章,我們會選擇使用 Gradle 進行後續功能介紹,前面會先使用 Maven 進行專案建置的原因,主要是因為在Spring 生態系找到的資料還是以 Maven 居多,希望大家還是可以了解此兩種工具是如何進行專案建置,避免在參考設定上比較不會有問題。 Reference 【官方】Spring Boot 文件 【文章】 SpringBoot 启动类 @SpringBootApplication 注解 以及执行流程","categories":[{"name":"2020-IThome鐵人賽","slug":"2020-IThome鐵人賽","permalink":"http://devinwangg.github.io/categories/2020-IThome%E9%90%B5%E4%BA%BA%E8%B3%BD/"}],"tags":[{"name":"Spring Boot","slug":"Spring-Boot","permalink":"http://devinwangg.github.io/tags/Spring-Boot/"}]},{"title":"[Day 15] 遠征 Kotlin × Spring Boot 設定資料庫與匯入初始資料","slug":"ironman-2020-15","date":"2020-10-11T11:17:25.000Z","updated":"2020-10-11T11:17:25.000Z","comments":true,"path":"2020/10/11/ironman-2020-15/","link":"","permalink":"http://devinwangg.github.io/2020/10/11/ironman-2020-15/","excerpt":"","text":"接下來,我們將嘗試在專案內連接資料庫,資料庫部份會採用 H2 資料庫進行練習,H2 是一個開源的資料庫引擎,其底層是使用 Java 所實作的資料庫,它可以作為嵌入式資料庫使用,在開發環境使用與測試非常方便,不像MySQL等資料庫需要啟動一個服務與設定相關配置才能夠使用,大大減少環境設定成本。 我們利用昨日所建置的專案繼續實作,本章文章也有將專案放到 Github 供大家參考,我們先在專案的 build.gradle.kts 檔案加入兩個套件 JDBC與H2配置到 Gradle 工具設定中,詳細可參考下面內容與結果圖片: (1) 在 build.gradle.kts 的 dependencies 加入以下內容 1234// 在 dependencies 加入下面兩段implementation("org.springframework.boot:spring-boot-starter-jdbc")runtimeOnly("com.h2database:h2") (2) 加入完畢後,應為以下結果,供大家參考 (3) 設定 application.yml 檔案內容與說明如下 1234567891011server: port: 8080 # 設定專案 port 為 8080spring: h2: console: enabled: true # 啟動 H2 console path: /h2-console # 設定 H2 管理頁面路徑 datasource: url: jdbc:h2:mem:ironman;DB_CLOSE_DELAY=-1 # 設定 H2 資料庫連線位置 username: sa # 設定資料庫登入使用者 password: Ironman0924! # 設定資料庫登入密碼 完成上面步驟後,我們可以運行專案,打開瀏覽器瀏覽 http://localhost:8080/h2-console 頁面,就可以看到 H2 後端管理頁面,如下圖: 這邊我們要修改登入資訊 JDBC URL 、 UserName、Password,此三個欄位要與 application 內容設定相同,設定完成後按連線(Connect),即可進入管理頁面: 完成資料庫設定後,我們就要來匯入資料進行測試,而 H2 資料庫會自動抓取專案 resource 資料夾下的 SQL 檔案進行建置,故我們在專案資料夾的 src/main/resources 建立兩個檔案 schema.sql、data.sql,schema 檔案為建立資料表,data.sql 檔案為 insert 資料到資料表內,檔案內容參考如下: schema.sql 1234567DROP TABLE IF EXISTS Student;CREATE TABLE Student ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, email VARCHAR(100) DEFAULT NULL) data.sql 1234INSERT INTO Student (id, name, email) VALUES(1, 'Devin', 'test1@gmail.com'),(2, 'Jack', 'test2@gmail.com'),(3, 'Eric', 'test3@gmail.com'); 檔案放置完畢後,我們將專案重新運行,重新進入 H2 管理頁面(http://localhost:8080/h2-console),會發現到左手邊已經自動建立 Student 資料表,我們利用中間的視窗輸入查詢 SQL-SELECT * FROM Student 並運行此段 SQL,會發現預設資料也已經匯入,如下圖: 透過上面步驟,我們已經順利完成資料庫建置與匯入初始資料,而上面我們使用的 H2 資料庫是採用記憶體類型進行資料儲存,所以當我們將專案重新運行後,會發現到資料其實會立即被清空,此功能其實非常適合測試環境,當測試完畢就立即刪除資料。但如果有朋友是想要將資料保存下來,H2 其實也有提供檔案類型方式進行資料儲存,執行方式說明如下: 執行方式 在記憶體執行 表示資料庫資料是儲存在伺服器的記憶體,當我們關閉連線後資料庫會立即被清空,此方式非常適合測試環境 12# 在 application 檔案需要設定為此段jdbc:h2:mem:ironman;DB_CLOSE_DELAY=-1 嵌入式 可將資料庫儲存為檔案,進行資料持久性保存 12# 在 application 檔案需要設定為此段jdbc:h2:file:./src/main/resources/data/ironman;AUTO_SERVER=true file: 後面接的字串「./src/main/resources/data/ironman」為檔案所儲存路徑,表示資料庫會為我們儲存在 resource/data/ 資料夾下,並將資料庫命名為 ironman 資料庫,若第一次連線會自動建立資料庫與路徑對應的資料夾 服務模式 H2支援三種服務模式: Web server 支援使用瀏覽器瀏覽管理頁面 H2 Console TCP server 支援 Client/ Server 連線方式 PG server 支援 PostgreSQL Client 連線字串參數 DB_CLOSE_DELAY 要求最後一個正在連線的連線斷開後,不要關閉資料庫 MODE=MySQL 相容模式,H2相容多種資料庫,該值可以為:DB2、Derby、HSQLDB、MSSQLServer、MySQL、Oracle、PostgreSQL AUTO_RECONNECT=TRUE 連線丟失後自動重新連線 AUTO_SERVER=TRUE 啟動自動混合模式,允許開啟多個連線,該引數不支援在記憶體中執行模式 TRACE_LEVEL_SYSTEM_OUT、TRACE_LEVEL_FILE 輸出跟蹤日誌到控制檯或檔案, 取值0為OFF,1為ERROR(預設值),2為INFO,3為DEBUG SET TRACE_MAX_FILE_SIZE mb 設定跟蹤日誌檔案的大小,預設為16M 我們嘗試將原本的設定改為使用檔案方式進行儲存,修改 application.yml 檔案的 datasource.url 設定,會發現專案 Resource 會自動出現 Data 資料夾與 DB 檔案,如下圖: 而為了要驗證資料有保存,可以先將 schema.sql 檔案刪除,避免重新運行專案後還是會將資料表 drop的狀況,再進入管理後台 insert 資料(insert sql 可參考下面),再重新運行專案與查詢資料,此時會發現資料確實有六筆出現,如下面結果: 1234INSERT INTO Student (id, name, email) VALUES(4, 'Devin', 'test1@gmail.com'),(5, 'Jack', 'test2@gmail.com'),(6, 'Eric', 'test3@gmail.com'); 最後,我們來嘗試撰寫讀取資料庫的程式進行抓取資料庫資料,運用專案設定時所加入的 JDBC 套件撰寫,步驟與程式說明如下: 將昨日在 DemoApplication.kt 檔案中加入的 HomeController 程式進行調整,修改為以下: 12345678910111213141516171819202122232425262728293031323334@RestControllerclass HomeController(@Autowired environment: Environment) { // 取得 application.yml 設定的配置數值 private final val url = environment.getProperty("spring.datasource.url"); private final val username = environment.getProperty("spring.datasource.username"); private final val password = environment.getProperty("spring.datasource.password"); // 資料庫連線 val connection: Connection = DriverManager.getConnection(url, username, password) /** * 取得 Student 所有資料 */ @GetMapping("/students") fun getStudentData(): ArrayList<MutableMap<String, Any>> { // 建立 Statement 進行資料庫操作 val statement: Statement = connection.createStatement() // 取得 Student 資料表所有資料 val record: ResultSet = statement.executeQuery("SELECT * FROM Student") // 將 Student 資料取出並儲存在一個集合進行輸出 val result: ArrayList<MutableMap<String, Any>> = ArrayList() while (record.next()) { val item = mutableMapOf<String, Any>() item["id"] = record.getInt("id") item["name"] = record.getString("name") item["email"] = record.getString("email") result.add(item) } return result }} 將專案重新運行,打開瀏覽器輸入「http://localhost:8080/students」進行查詢,會得到一份 JSON檔案,即為我們資料庫目前所儲存的資料。 以上專案有放在 Github 上,如有遇到問題,大家可以直接參考","categories":[{"name":"2020-IThome鐵人賽","slug":"2020-IThome鐵人賽","permalink":"http://devinwangg.github.io/categories/2020-IThome%E9%90%B5%E4%BA%BA%E8%B3%BD/"}],"tags":[{"name":"Spring Boot","slug":"Spring-Boot","permalink":"http://devinwangg.github.io/tags/Spring-Boot/"}]},{"title":"[Day 16] 遠征 Kotlin × 使用 Spring Data JPA 操作資料庫 (1)","slug":"ironman-2020-16","date":"2020-10-11T11:16:55.000Z","updated":"2020-10-11T11:16:55.000Z","comments":true,"path":"2020/10/11/ironman-2020-16/","link":"","permalink":"http://devinwangg.github.io/2020/10/11/ironman-2020-16/","excerpt":"","text":"昨日我們已經學會資料庫設定與使用 JDBC 取得資料庫資料,而今天我們將 JDBC 這段程式改為使用 Spring Data JPA 進行資料庫資料操作,而 Spring Data JPA 是 Spring Boot 官方相當推薦使用的 ORM 框架,可以讓使用者利用極簡的程式碼實現資料操作處理,它內部已經提供了資料庫 CRUD(Create、Read、Update、Delete)等常用功能,可以讓我們大幅提高開發效率,我們直接進入實作體驗: 此文章有提供範例程式碼在 Github 供大家參考 在 build.gradle.kts 的 dependencies 加入 Spring DataJPA 套件 1implementation("org.springframework.boot:spring-boot-starter-data-jpa") 設定 application.yml JPA 設定,這邊列出整個 application.yml 內容 1234567891011121314151617server: port: 8080 # 設定 Spring Boot 啟動 portspring: h2: # 設定 H2 資料庫 console: enabled: true path: /h2-console jpa: # 設定 JPA 相關設定 hibernate: ddl-auto: update # 設定update值,表示只有第一次載入時需要自動建立資料表,其餘載入則是更新資料表結構 database-platform: H2 # 設定 JPA database 為 H2 資料庫 show-sql: true # 顯示 JPA 運行的SQL語法 generate-ddl: false datasource: url: jdbc:h2:file:./src/main/resources/data/ironman;AUTO_SERVER=true username: sa password: Ironman0924! 新增 Student.kt 檔案,我們利用data class屬性建立 Student Entity,如果有使用 Java 開發過 Spring Boot 的朋友,會發現Kotlin是可以利用data class取代 Java 的 lombok 套件,檔案內容如下: 12345678910111213@Entity@Tabledata class Student( @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) val id: Int = 0, @Column val name: String = "", @Column val email: String = "") 新增 StudentDao.kt 檔案,建立 DAO 操作物件 1interface StudentDao: JpaRepository<Student, Long>, JpaSpecificationExecutor<Student> 新增 StudentController.kt 檔案,建立兩個 API-取得所有學生資料API、新增學生資料API 12345678910111213141516171819@RestController@RequestMapping("/api")class StudentController(@Autowired val studentDao: StudentDao) { /** * 取得 Student 所有資料 */ @GetMapping("/students") fun getStudentData(): MutableList<Student> { return studentDao.findAll() } /** * 新增 Student 資料 */ @PostMapping("/students") fun addStudentData(@RequestBody student: Student): Student { return studentDao.save(student) }} 新增 Student.http 檔案,利用 Http Client 工具進行 API 測試 1234567891011### 取得所有學生資料 APIGET http://localhost:8080/api/students### 新增學生資料 APIPOST http://localhost:8080/api/studentsContent-Type: application/json{ "name": "Devin", "email": "test@gmail.com"} 在 API 測試這塊,可能有些朋友接觸過 Postman 測試工具,而這邊我們使用的工具是 Http Client ,這套工具也可以讓開發者模擬 Http Request 動作,利用建立 Http 檔案與撰寫測試案例進行 API測試與取得驗證結果,而目前在工作開發上也經常使用這套工具,此工具若相較於Postman,個人認為 Http Client 帶給我們更多的好處,像是可以利用版本控制維護測試檔案,也可以與團隊共享此份測試檔案,在靈活度上佔有很大優勢,Plugin 其餘介紹也可以從這邊查看 Document: 我們實際測試 新增學生資料API 與 取得所有學生資料API 來觀看實際運作:(1) 新增兩筆學生資料,實際操作結果如下 (2) 取得所有學生資料,實際操作結果如下 此文章有提供範例程式碼在 Github 供大家參考 今天我們直接帶入實作步驟來讓大家感受 Spring Data JPA 的效果,而後續我們再來細談 Spring Data JPA 的介紹與其餘使用方式的介紹,相信大家會對於 Spring Data JPA 更了解。","categories":[{"name":"2020-IThome鐵人賽","slug":"2020-IThome鐵人賽","permalink":"http://devinwangg.github.io/categories/2020-IThome%E9%90%B5%E4%BA%BA%E8%B3%BD/"}],"tags":[{"name":"Spring Boot","slug":"Spring-Boot","permalink":"http://devinwangg.github.io/tags/Spring-Boot/"}]},{"title":"[Day 17] 遠征 Kotlin × 使用 Spring Data JPA 操作資料庫 (2)","slug":"ironman-2020-17","date":"2020-10-11T11:16:25.000Z","updated":"2020-10-11T11:16:25.000Z","comments":true,"path":"2020/10/11/ironman-2020-17/","link":"","permalink":"http://devinwangg.github.io/2020/10/11/ironman-2020-17/","excerpt":"","text":"ORM 介紹昨日,我們直接利用實作來感受 Spring Data JPA 的快速開發,會發現到我們只要建立 Entity,就可以幫助我們做資料庫映射來新增資料表與欄位,而此特性就是 Object-Relation Mapping-簡稱 ORM,就是將資料庫的內容映射為物件,讓我們可以用操作物件的方式對資料庫進行操作,而不用直接使用 SQL 語法對資料庫進行操作,甚至我們不用管底層的資料庫系統是什麼樣的資料庫,例如 SQL Server、MySQL,僅須使用同一套語法撰寫存取資料庫的邏輯,大幅降低程式與資料庫之間的耦合關係。 Spring Data JPA 介紹再來,我們來深入了解什麼是 Spring Data JPA,它其實是 Spring 與 JPA 的整合,JPA 是一種 ORM 規範,是 Hibernate(ORM 框架)的一個抽象,就像 JDBC 與 JDBC driver的關係,Hibernate 實作了 JPA 定義的規範,而 Spring Data JPA 則是基於 JPA 對資料庫溝通層進行封裝的應用框架,目標是希望簡化資料庫溝通層操作,內部涵蓋許多針對資料庫資料操作的解決方案。 資料持久層 Data Persistence Layer昨日我們在實作程式有建立一個 StudentDao 程式,而所謂 DAO 其實是 Data Access Object 資料存取物件的縮寫,而資料持久層的意思就是把資料儲存的相關操作從原本架構解耦,即降低程式與資料庫的相依性,獨立出一個專門處理相關事務邏輯的物件,達到不同資料庫的統一存取方法,單一職責原則。 資料傳輸物件 Data Transfer Object (DTO)上面我們介紹了 DAO 物件,我們再介紹一個物件-DTO,此物件的作用如同名稱,主要是作為傳輸資料所使用,使用 DTO 可以讓我們減少參數傳遞的混亂,增加程式可讀性,也具備封裝性與擴充性,可以讓我們將一些必要傳遞但不希望被操作的資料進行封裝,或如果業務需要增加傳遞資料或對傳遞資料進行特定處理,只需要在物件增加欄位或修改即可。 Spring Data JPA 方法定義規則昨日我們在撰寫 Dao 程式中會去實作 JpaRepository 與 JpaSpecificationExecutor 介面,而此兩個介面內容就是 Spring Data 所設計的基礎資料庫操作方法,可以讓類別實作後就可以輕易操作資料庫動作。JpaRepository 是提供基本 CRUD 相關操作方法,JpaSpecificationExecutor 則是提供一些複雜查詢方法,我們可以直接使用方法名稱快速進行資料庫操作,如下範例: 此文章有提供範例程式碼在 Github 供大家參考 在昨天的範例中,我們增加一個專案需求-利用姓名參數查詢學生資料: 在 StudentDao 加入 findByName 函數 1fun findByName(name: String): Student? 在 StudentController 加入「查詢指定姓名的學生資料」函數- getStudentByName 函數 123456789101112131415161718192021222324252627@RestController@RequestMapping("/api")class StudentController(@Autowired val studentDao: StudentDao) { /** * 取得 Student 所有資料 */ @GetMapping("/students") fun getStudentData(): MutableList<Student> { return studentDao.findAll() } /** * 新增 Student 資料 */ @PostMapping("/students") fun addStudentData(@RequestBody student: Student): Student { return studentDao.save(student) } /** * 查詢指定姓名的學生資料 */ @PostMapping("/students/search") fun getStudentByName(@RequestParam name: String): Student? { return studentDao.findByName(name) }} 在 Student.http 加入測試API 方法 123### 利用姓名參數查詢學生資料POST http://localhost:8080/api/students/search?name=DevinContent-Type: application/json 最後再進行測試,假設我們資料庫的學生資料表有 Devin 這筆資料時,我們應得到以下結果: 上面範例就是我們使用 Spring Data JPA 的方法名稱特性進行查詢的範例,其餘方法名稱的查詢規則,我們可以到 Spring Data JPA 官網進行查詢,官網在規則這塊寫的非常清楚,此部份就不再補充,建議大家可以邊查詢邊撰寫程式進行測試,官網內容如下圖: 此文章有提供範例程式碼在 Github 供大家參考 Reference [官方] Spring Data JPA [文章] ORM介紹及ORM優點、缺點 [文章] 持久層(Data Persistence Layer)","categories":[{"name":"2020-IThome鐵人賽","slug":"2020-IThome鐵人賽","permalink":"http://devinwangg.github.io/categories/2020-IThome%E9%90%B5%E4%BA%BA%E8%B3%BD/"}],"tags":[{"name":"Spring Boot","slug":"Spring-Boot","permalink":"http://devinwangg.github.io/tags/Spring-Boot/"}]},{"title":"[Day 18] 遠征 Kotlin × Spring Boot 使用 RESTful API (1)","slug":"ironman-2020-18","date":"2020-10-11T11:16:22.000Z","updated":"2020-10-11T11:16:22.000Z","comments":true,"path":"2020/10/11/ironman-2020-18/","link":"","permalink":"http://devinwangg.github.io/2020/10/11/ironman-2020-18/","excerpt":"","text":"前面兩篇我們已經介紹完資料庫操作,可能有朋友會疑問 API 細節是如何進行實現,為什麼我們在 程式中加入 @RestController、@GetMapping 就可以實作一個 API 進行呼叫,甚至好奇什麼是 API,這篇將來解釋 API 實作細節並介紹 RESTful API 設計風格。 API 的基本介紹我想可以直接參考這篇文章,作者提到的 API 概念非常清晰,我們整理內容大概描述,應用程式介面(Application Programming Interface, API)是扮演應用程式與應用程式之間溝通的橋樑,以幫助使用者達到目的為目標導向,讓使用者在操作 API 的過程,不需要知道內部程式運作的邏輯,只要告訴 API 你的需求,即可達到目的。 而 API 常見設計的風格有 REST、GraphQL、gRPC和 Webhooks,我們在這篇會先提到目前普遍率最高的 RESTful API來介紹。REST 全名為 Representational State Transfer( 表現層狀態轉移),是此段開頭提到的其中一種 API 設計風格,而 RESTful 只是將 REST 轉為形容詞,一般在設計 Restful API 我們會通常會考慮四個重要觀念,如下: Nouns 名詞 API Endpoint 必須使用 名詞 進行定義 URL,而每個資源都要保持唯一性,資源採用複數命名,例如學生資料就是 /students Verbs 動詞 利用 Verbs 動詞 對 Nouns 名詞 (資源 URL) 進行操作,在 HTTP 1.1 的實作就是 HTTP Method,即 Get、Post、Put、Delete、Patch等,例如使用 Get Method 取得學生資料、用 Delete Method 刪除學生資料,動詞分別描述如下: Get 讀取資源 Post 新增替源或作為utility API,例如檢查帳號是否存在 Delete 刪除資源 Put 替換單一資源 Patch 更新資源部份內容 Content Types 資源呈現方式 當我們如果要取得某一個 API 的資料,此資料格式可以有 HTML、 XML、JSON 等格式,同樣的 URL 資源可以有不同型態的表現方式。 HTTP 回傳狀態碼 API 回傳結果應使用正確的 HTTP 狀態碼,這樣呼叫者才可以了解 API 實際運行狀況,這邊整理常見的狀態碼,若要了解完整狀態碼說明可參考 wiki-List of HTTP status codes: 2xx 成功運行 200-OK 成功回傳結果 201-Created 資源新增成功 202-Accepted 請求接受,但結果還在處理 204-No Content 沒有回應任何內容 3xx 重新導向 301-Moved Permanently 重新導向URI 304-Not Modified 請求資源並未修改 4xx 用戶端錯誤 400-Bad Request 錯誤請求 401-Unauthorized 使用者尚未通過身份驗證 403-Forbidden 用戶端被禁止使用此請求 404-Not Found 請求資源不存在 405-Method Not Allowed 不支援請求的 Http method 414-Url Too Long URI太長 5xx 伺服器錯誤 500-Internal Server Error 內部伺服器錯誤 這邊也推薦大家可以參考 HTTP DECISION DIAGRAM 來了解每個 Http 狀態運行的活動圖,下圖僅擷取 Request 時的活動圖給予大家進行參考,如果有興趣可以前往觀看完整版。 而我們在 Controlelr 中加入的 @RestController 其實是 @Controller + @ResponseBody 組成,@Controller 是將標注(Annotation)的類別注入到 Spring IOC 容器,會讓該類別在運行中會被實例化(Instance),表示該類別具有 Controller 功用,而 @ResponseBody 會將我們函數所回傳的結果轉換為 JSON 格式傳送給 client 端。 這篇文章希望大家能夠先將API概念與RESTful設計概念了解清楚,明日將會介紹前面實作的專案加入 RESTful API 設計風格後的成果 Reference API 到底是什麼? 用白話文帶你認識 RESTful API 設計準則與實務經驗 HTTP Status Codes Decision Diagram – Infographic","categories":[{"name":"2020-IThome鐵人賽","slug":"2020-IThome鐵人賽","permalink":"http://devinwangg.github.io/categories/2020-IThome%E9%90%B5%E4%BA%BA%E8%B3%BD/"}],"tags":[{"name":"Spring Boot","slug":"Spring-Boot","permalink":"http://devinwangg.github.io/tags/Spring-Boot/"}]},{"title":"[Day 19] 遠征 Kotlin × Spring Boot 使用 RESTful API (2)","slug":"ironman-2020-19","date":"2020-10-11T11:16:11.000Z","updated":"2020-10-11T11:16:11.000Z","comments":true,"path":"2020/10/11/ironman-2020-19/","link":"","permalink":"http://devinwangg.github.io/2020/10/11/ironman-2020-19/","excerpt":"","text":"在前一篇 Spring Boot 使用 RESTful API (1) 我們已經說明 RESTful API 的基本概念,接下來這篇我們要將前面所做的範例進行修改,實作步驟如下: 運用的範例是在 第16篇「使用 Spring Data JPA 操作資料庫 (1)」 的 Github 範例,有興趣的朋友也可以直接下載範例練習實作從此篇開始,我們預計會在實作上加入 Kotlin 特性的撰寫風格,例如 Kotlin 章節所提到的 Scope Function、Elvis等特性 我們在之前的範例,其實已經完成 Get、Post 等 API 操作,使用資源名稱為 students,如下範例: 1234567891011121314151617181920212223242526@RestController@RequestMapping("/api")class StudentController(@Autowired val studentDao: StudentDao) { /** * 取得 Student 所有資料 */ @GetMapping("/students") fun getStudentData(): MutableList<Student> = studentDao.findAll() /** * 新增 Student 資料 */ @PostMapping("/students") fun addStudentData(@RequestBody student: Student): Student = studentDao.save(student) /** * 利用姓名查詢學生資料 */ @PostMapping("/students/search") fun getStudentByName(@RequestParam name: String): ResponseEntity<List<Student>> = studentDao .findByName(name) .let { return ResponseEntity(it, HttpStatus.OK) }} 新增 PUT 實作 - 更新學生資料 123456789101112/** * 修改學生全部資料 */@PutMapping("/students/{id}")fun updateStudent(@PathVariable id: Int, @RequestBody student: Student): ResponseEntity<Student?> = studentDao .findById(id) .run { this ?: return ResponseEntity<Student?>(null, HttpStatus.NOT_FOUND) }.run { return ResponseEntity<Student?>(studentDao.save(this), HttpStatus.OK) } 新增 PATCH 實作 - 修改部份學生資料,此步驟要記得 PUT 與 PATCH 的差異,根據 RESTful 定義,通常會使用 PATCH 的呼叫會屬於僅修改部份資料的 API,此範例以修改學生信箱 API 為例: 1234567891011121314151617181920/** * 修改學生信箱(欲更新部份資料) */@PatchMapping("/students/{id}")fun updateStudentEmail(@PathVariable id: Int, @RequestBody email: String): ResponseEntity<Student?> = studentDao .findById(id) .run { this ?: return ResponseEntity<Student?>(null, HttpStatus.NOT_FOUND) } .run { Student( id = this.id, name = this.name, email = email ) } .run { return ResponseEntity<Student?>(studentDao.save(this), HttpStatus.OK) } 新增 DELETE 實作 - 刪除學生資料 12345678910111213/** * 刪除學生資料 */@DeleteMapping("/students/{id}")fun deleteStudent(@PathVariable id: Int): ResponseEntity<Any> = studentDao .findById(id) .run { this ?: return ResponseEntity<Any>(null, HttpStatus.NOT_FOUND) } .run { return ResponseEntity<Any>(studentDao.delete(this), HttpStatus.NO_CONTENT) } 修改測試檔案 Student.http - 增加 PUT、PATCH、DELETE 測試方法 1234567891011121314151617181920212223242526272829303132333435### 取得所有學生資料 APIGET http://localhost:8080/api/students### 新增學生資料 APIPOST http://localhost:8080/api/studentsContent-Type: application/json{ "name": "Devin", "email": "test@gmail.com"}### 利用姓名參數查詢學生資料POST http://localhost:8080/api/students/search?name=DevinContent-Type: application/json### 修改學生資料PUT http://localhost:8080/api/students/1Content-Type: application/json{ "name": "Eric", "email": "Eric@gmail.com"}### 修改學生信箱資料PATCH http://localhost:8080/api/students/1Content-Type: application/json{ "email": "test@gmail.com"}### 刪除學生資料DELETE http://localhost:8080/api/students/1 以上,是我們根據 RESTful API 設計風格進行實作,最後這邊還是想提醒閱讀的朋友,雖然 RESTful是一個非常流行的API設計風格,大多公司都會使用到,但實際在開發專案時,還是會根據專案需求、團隊規範或是公司文化而會有所不同,畢竟設計風格只是一種參考,千萬不要變成開發的阻礙,架構應該要因應每種需求而有不同的改變,進而找出最適合的方式。 此文章有提供範例程式碼在 Github 供大家參考","categories":[{"name":"2020-IThome鐵人賽","slug":"2020-IThome鐵人賽","permalink":"http://devinwangg.github.io/categories/2020-IThome%E9%90%B5%E4%BA%BA%E8%B3%BD/"}],"tags":[{"name":"Spring Boot","slug":"Spring-Boot","permalink":"http://devinwangg.github.io/tags/Spring-Boot/"}]},{"title":"[Day 20] 遠征 Kotlin × Spring Boot 使用分層架構 Layered Architecture","slug":"ironman-2020-20","date":"2020-10-11T11:15:37.000Z","updated":"2020-10-11T11:15:37.000Z","comments":true,"path":"2020/10/11/ironman-2020-20/","link":"","permalink":"http://devinwangg.github.io/2020/10/11/ironman-2020-20/","excerpt":"","text":"軟體系統架構是建構者賦予系統的樣貌,而該樣貌是由不同元件組合而成,元件之間會有不同的合作與溝通方式,目的是為了讓軟體系統在開發、部署、運行和維護都能輕鬆理解與開發,也讓系統的生命週期成本趨近最小化,使程式設計師生產力最大化。—《Clean Architecture》 而本章將要介紹架構—分層架構(Layered Architecture),又稱為N層架構模式(N-tier Architecture Pattern),是軟體開發中經常看到的架構之一,它的每一層都有自己所負責的任務,每一層也有許多好處,例如: 簡化複雜性,達到關注點分離、結構清晰 降低耦合度,隔離層與層之間的關聯,降低彼此依賴,上層不需要了解下層狀況,利於分工、測試與維護 提高靈活性,可以靈活替換某一層的實作方法 提高擴展性,方便實現分散式部署方法 而在 Spring Boot 常見的階層架構會將專案分為四個主要類別: 表示層 Presentation Layer 屬於該架構頂層,主要負責 Http 請求、路由處理、身份驗證與Json資料轉換處理,會將資料傳遞到業務邏輯層進行溝通 業務邏輯層 Business Layer 主要處理專案所有相關業務邏輯,包含處理業務規則、流程、資料完整性等,並接收來自表示層的資料請求,進行邏輯處理後,會轉向與資料持久層提交請求並傳遞資料結果。 資料持久層 Persistence Layer 作為應用程式與資料庫之間的抽象層,將業務層需要使用的物件映射到資料庫進行相互轉換與溝通 資料庫層 Database Layer 主要由資料庫組成,所有資料庫相關操作與設定都會於此層處理 在實作上,可參考下圖《 Spring Boot Flow Architecture》,Client 端會與 Controller 層進行 Http 請求溝通,而 Service 層會針對專案業務邏輯進行處理與請求數據,持久層則是利用 DAO 物件進行資料庫溝通實現,達到不同層處理各自的職責。 接下來我們進入實作步驟部份: 首先在專案內建立 Controller 資料夾並將之前的 Controller 改用 Interface 進行定義,此作法主要是為了解耦合,當我們要修改Controller 實現方法時,只要修改實作 Implement 即可 Interface 部份定義需求 12345678910111213141516171819202122232425262728293031323334353637interface StudentController { /** * 取得 Student 所有資料 */ @GetMapping("/students") fun getStudentData(): MutableList<Student> /** * 新增 Student 資料 */ @PostMapping("/students") fun addStudentData(@RequestBody student: Student) : Student /** * 利用姓名查詢學生資料 */ @PostMapping("/students/search") fun getStudentByName(@RequestParam name: String) : ResponseEntity<List<Student>> /** * 修改學生全部資料 */ @PutMapping("/students/{id}") fun updateStudent(@PathVariable id: Int, @RequestBody student: Student) : ResponseEntity<Student?> /** * 修改學生信箱(欲更新部份資料) */ @PatchMapping("/students/{id}") fun updateStudentEmail(@PathVariable id: Int, @RequestBody student: Student): ResponseEntity<Student?> /** * 刪除學生資料 */ @DeleteMapping("/students/{id}") fun deleteStudent(@PathVariable id: Int): ResponseEntity<Any>} implement controller 進行實作 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152@RestController@RequestMapping("/api")class StudentControllerImpl(@Autowired val studentDao: StudentDao) : StudentController { override fun getStudentData(): MutableList<Student> = studentDao.findAll() override fun addStudentData(student: Student): Student = studentDao.save(student) override fun getStudentByName(name: String): ResponseEntity<List<Student>> = studentDao .findByName(name) .let { return ResponseEntity(it, HttpStatus.OK) } override fun updateStudent(id: Int, student: Student): ResponseEntity<Student?> = studentDao .findById(id) .run { this ?: return ResponseEntity<Student?>(null, HttpStatus.NOT_FOUND) }.run { return ResponseEntity<Student?>(studentDao.save(this), HttpStatus.OK) } override fun updateStudentEmail(id: Int, student: Student): ResponseEntity<Student?> = studentDao .findById(id) .run { this ?: return ResponseEntity<Student?>(null, HttpStatus.NOT_FOUND) } .run { Student( id = this.id, name = this.name, email = student.email ) } .run { return ResponseEntity<Student?>(studentDao.save(this), HttpStatus.OK) } override fun deleteStudent(id: Int): ResponseEntity<Any> = studentDao .findById(id) .run { this ?: return ResponseEntity<Any>(null, HttpStatus.NOT_FOUND) } .run { return ResponseEntity<Any>(studentDao.delete(this), HttpStatus.NO_CONTENT) }} 建立 Data 資料夾存放 DAO 、 Entity 物件,再建立 Service 資料夾準備建立 Service 物件,資料夾結構應如下圖: 建立 Service 物件 StudentService.kt,建立時如同第一步驟的Controller,先使用 Interface 定義業務邏輯需求再進行實作,最後再將原本的Controller改使用Service進行資料請求,程式如下: Interface 定義業務邏輯需求 12345678910111213141516171819202122232425262728293031323334353637interface StudentService { /** * 查詢所有學生資料 */ fun findAllStudent(): MutableList<Student> /** * 新增學生資料 */ fun addStudent(student: Student): Student /** * 查詢符合姓名條件的學生資料 */ fun findByStudentId(id: Int): Student? /** * 查詢符合姓名條件的學生資料 */ fun findByStudentName(name: String): List<Student> /** * 更新學生整個資料 */ fun updateStudent(student: Student): Student /** * 更新學生信箱資料 */ fun updateStudentEmail(student: Student): Student /** * 刪除學生資料 */ fun deleteStudent(student: Student): Unit} Implement Service 進行實作 12345678910111213141516171819202122232425262728293031323334353637@Serviceclass StudentServiceImpl(@Autowired val studentDao: StudentDao) : StudentService { override fun findAllStudent(): MutableList<Student> = studentDao.findAll() override fun addStudent(student: Student): Student = Student( name = student.name.trim(), email = student.email.trim() ).run { return studentDao.save(this) } override fun findByStudentId(id: Int): Student? = studentDao.findById(id) override fun findByStudentName(name: String): List<Student> = studentDao.findByName(name) override fun updateStudent(student: Student): Student = Student( id = student.id, name = student.name.trim(), email = student.email.trim() ).run { return studentDao.save(this) } override fun updateStudentEmail(student: Student): Student = Student( id = student.id, name = student.name, email = student.email.trim() ).run { return studentDao.save(this) } override fun deleteStudent(student: Student): Unit = studentDao.delete(student)} 修改 Controller 對業務邏輯層的呼叫請求方法(原先是直接使用 DAO 物件) 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869@RestController@RequestMapping("/api")class StudentControllerImpl(@Autowired val studentService: StudentService) : StudentController { /** * 取得 Student 所有資料 */ override fun getStudentData(): MutableList<Student> = studentService.findAllStudent() /** * 新增 Student 資料 */ override fun addStudentData(student: Student): Student = studentService.addStudent(student) /** * 利用姓名查詢學生資料 */ override fun getStudentByName(name: String): ResponseEntity<List<Student>> = studentService .findByStudentName(name) .let { return ResponseEntity(it, HttpStatus.OK) } /** * 修改學生全部資料 */ override fun updateStudent(id: Int, student: Student): ResponseEntity<Student?> = studentService .findByStudentId(id) .run { this ?: return ResponseEntity<Student?>(null, HttpStatus.NOT_FOUND) }.run { return ResponseEntity<Student?>(studentService.updateStudent(this), HttpStatus.OK) } /** * 修改學生信箱(欲更新部份資料) */ override fun updateStudentEmail(id: Int, student: Student): ResponseEntity<Student?> = studentService .findByStudentId(id) .run { this ?: return ResponseEntity<Student?>(null, HttpStatus.NOT_FOUND) } .run { Student( id = this.id, name = this.name, email = student.email ) } .run { return ResponseEntity<Student?>(studentService.updateStudentEmail(this), HttpStatus.OK) } /** * 刪除學生資料 */ override fun deleteStudent(id: Int): ResponseEntity<Any> = studentService .findByStudentId(id) .run { this ?: return ResponseEntity<Any>(null, HttpStatus.NOT_FOUND) } .run { return ResponseEntity<Any>(studentService.deleteStudent(this), HttpStatus.NO_CONTENT) }} 此文章有提供範例程式碼在 Github 供大家參考 Reference [書籍] Clean Architecture: A Craftsman’s Guide to Software Structure and Design [文章] Spring Boot Architecture","categories":[{"name":"2020-IThome鐵人賽","slug":"2020-IThome鐵人賽","permalink":"http://devinwangg.github.io/categories/2020-IThome%E9%90%B5%E4%BA%BA%E8%B3%BD/"}],"tags":[{"name":"Spring Boot","slug":"Spring-Boot","permalink":"http://devinwangg.github.io/tags/Spring-Boot/"}]},{"title":"[Day 21] 遠征 Kotlin × Spring Boot 爬蟲實戰教學","slug":"ironman-2020-21","date":"2020-10-11T11:15:11.000Z","updated":"2020-10-11T11:15:11.000Z","comments":true,"path":"2020/10/11/ironman-2020-21/","link":"","permalink":"http://devinwangg.github.io/2020/10/11/ironman-2020-21/","excerpt":"","text":"今日看到有許多鐵人賽的朋友紛紛完賽,有點好奇目前還有幾位鐵人還在一起努力,於是想到可以撰寫爬蟲 Web scraper 程式來了解一下,而在 Java library 中有個 JSOUP 套件,此套件有提供許多方便易用的 API 可以解析 HTML,使用方法與 CSS 或 jQuery 選擇器類似,也因為 Kotlin 與 Java 整合度非常好,所以 Kotlin 可以直接呼叫 Java Library 讓我們順利處理許多事情,下面我們來介紹 JSOUP 的使用方式與實作範例「鐵人賽比賽現況」 引入方法若要使用 JSOUP 套件要記得先引入套件,下面是 Maven 與 Gradle 分別引用方式 Maven 12345<dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId> <version>1.13.1</version></dependency> Gradle 1compile 'org.jsoup:jsoup:1.13.1' 資料輸入方法JSOUP 主要支援四種輸入(Input)方式進行解析成 Document 物件,如下: 從 字串 解析 此方法要注意字串必須包含 head 與 body 元素 12val html : String = "<html><head><title>First parse</title></head>" + "<body><p>Parsed HTML into a doc.</p></body></html>";val doc : Document = Jsoup.parse(html); 從 HTML 片段解析 我們也可以將 HTML Body 元素下的部份元素進行分析,例如一部份的 Div 元素,如下: 123val html : String = "<div><p>Lorem ipsum.</p>";val doc : Document = Jsoup.parseBodyFragment(html);val body : Element = doc.body(); 利用 URL 載入 Document 此方式應該是最常用的方式,利用網頁 url 直接進行分析,其中會使用到 connect 方法,此方法會我們建立一個新的連線,也可以在此方法設定請求細節,例如 cookie、userAgent、timeout等設定,如下: 12val doc : Document = Jsoup.connect("http://example.com/").get();val title : String = doc.title(); 利用 File 載入 Document 我們也可以將 HTML 檔案進行讀檔分析,如下: 12val input : File = new File("/tmp/input.html");val doc : Document = Jsoup.parse(input, "UTF-8", "http://example.com/"); 資料解析方法在解析方法中,主要會推薦使用兩種方法,再看大家比較喜歡哪一種方式: DOM 方法 此方法就是利用 DOM 操作的寫法讓我們學習如何在取得的 Document 物件進行取得元素值 Element,範例如下: 123456789val input : File = new File("/tmp/input.html");val doc : Document = Jsoup.parse(input, "UTF-8", "http://example.com/");val content : Element = doc.getElementById("content");val links : Elements = content.getElementsByTag("a");for (val link : links) { val linkHref : String = link.attr("href"); val linkText : String = link.text();} 尋找元素方法有以下幾種 [getElementById(String id)] 利用 id 進行尋找 [getElementsByTag(String tag)] 利用 tag 進行尋找 [getElementsByClass(String className)] 利用 class 進行尋找 [getElementsByAttribute(String key)] 利用屬性值進行尋找 也可以使用下面方法找出與元素有關聯的元素 [siblingElements()] [firstElementSibling()] [lastElementSibling()] [nextElementSibling()] [previousElementSibling()] [parent()] [children()] [child(int index)] 元素細節操作方法 [attr(String key)] 利用元素 key 值取得元素屬性 [attr(String key, String value)] 設定元素屬性 [attributes()] 取得所有元素屬性 [id()], [className()] and [classNames()] [text()] 取得元素文字資料 [html()] 取得元素 HTML 資料 [tag()] 、[tagName()] 取得 Tag 資料 控制 HTML 元素 與 文字 [append(String html)], [prepend(String html)] [appendText(String text)], [prependText(String text)] [appendElement(String tagName)], [prependElement(String tagName)] [html(String value)] 選取器方法 此方法類似於 CSS、jQuery的選取器使用方法,如下: 12345678val input : File = new File("/tmp/input.html");val doc : Document = Jsoup.parse(input, "UTF-8", "http://example.com/");val links : Elements = doc.select("a[href]");val pngs : Elements = doc.select("img[src$=.png]");val masthead : Element = doc.select("div.masthead").first();val resultLinks : Elements = doc.select("h3.r > a"); 選取器(Selector)使用方式 tagname 利用 Tag 找到元素,例如 a 元素 #id利用 # 符號加上 id 尋找元素 .class 利用 . 符號加上 class 值尋找元素 [attribute] 設定元素是否包含某個屬性進行進階條件尋找 [attr=value] 設定元素是否包含某個屬性欄位與對應值,例如 width=500 [attr^=value], [attr$=value], [attr*=value] 可針對屬性值使用模糊查詢 [attr~=regex]: 針對屬性值使用 regular expression,例如 img[src~=(?i)\\.(png|jpe?g)] 選取器組合(Selector combinations )方式 el#id 利用元素加上 id 值進行尋找,例如 div#logo el.class 利用元素加上 class 值進行尋找,例如 div.masthead el[attr] 利用元素搭配屬性值進行尋找,例如 a[href] 或是使用任何元素與屬性進行尋找,例如 a[href].highlight 元素擷取細節上面已經介紹如何取得 Document 物件與取得特定元素 Element,再來想要介紹如何取得元素Elements 的細節資料,例如元素的文字(Text)、連結(href)等欄位,如下範例: 12345678910111213141516171819202122val html : String = "<p>An <a href='http://example.com/'><b>example</b></a> link.</p>";val doc : Document = Jsoup.parse(html);val link : Element = doc.select("a").first();val elementId = doc.id()val elementTagName = doc.tagName()val elementClassName = doc.className()// 取得 An example link.val text : String = doc.body().text();// 取得 http://example.com/val linkHref : String = link.attr("href");// 取得 exampleval linkText : String = link.text();// 取得 <a href="http://example.com/"><b>example</b></a>val linkOuterH : String = link.outerHtml();// 取得 <b>example</b>val linkInnerH : String = link.html(); 實作範例如本文開頭所述,這個範例是想了解鐵人賽還有多少參賽者還在一起努力,有多少鐵人朋友已經順利達陣完成30天目標,故我們從鐵人賽頁面的選手列表進行觀察,我們可以開啟瀏覽器的開發者工具了解網站每個元素的規則,這邊將觀察到的規則整理如下: (1) 開啟瀏覽器開發者工具,觀察每個元素如何進行命名,找出對應的規則 contestants-list clearfix 為每一個參賽者資料區塊 contestants-list__title 參賽者參賽主題 contestants-list__name 參賽者暱稱 contestants-list__desc 主題描述 contestants-expect__number 敲碗數 team-dashboard__day 挑戰天數 contestants-group contestants-list__group 挑戰組別 contestants-list__date 報名日期 team-dashboard__box team-progress–challenge 正在挑戰的樣式 team-dashboard__box team-progress–fail 挑戰失敗的樣式 (2) 觀察出關鍵元素-正在挑戰 / 挑戰失敗的樣式差異,如下圖 (3) 接下來,我們利用上述整理的規則進行撰寫程式,說明如下: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364@RestController@RequestMapping("/api")class HomeController { @GetMapping("/getIronManData") fun getData(): HashMap<String, Any> { // 初始化 API 輸出集合 val response = HashMap<String, Any>() // 設定爬蟲會用到的基本參數 // 鐵人賽網站連結 val ironManUrl: String = "https://ithelp.ithome.com.tw/2020-12th-ironman/signup/list" var document = Jsoup.connect(ironManUrl).get() // 取得全部網站註冊人數 val totalRegisterPerson = document.select(".contestants-num")[0].text().replace("報名數 ", "").toInt() // 取得每頁參加者數量 val onePageCount = document.select(".contestants-list").size // 取得全部頁面數量 val totalPageCount = totalRegisterPerson / onePageCount + 1 // 初始化參數 var challengingCount = 0 // 仍正在挑戰中的人數 var challengeSuccessCount = 0 // 挑戰成功的人數 var challengeFailedCount = 0 // 挑戰失敗的人數 var unchallengedCount = 0 // 已經報名,但未開賽的人數 // 初始化每日進度集合 val daysCount = HashMap<String, Int>() for (index in 0..30) daysCount[index.toString()] = 0 // 帶入每頁頁碼參數 for (page in 1..totalPageCount) { // 連結加入頁碼參數 document = Jsoup.connect("$ironManUrl?page=$page").get() // 查詢此頁參加者區塊數量 val cardSize = document.select(".contestants-list").size // 帶入此頁區塊數量 for (index in 0 until cardSize) { // 取得區塊元素 Element val item = document.select(".contestants-list") // 取得挑戰天數資料 val challengeDay = item.select(".team-dashboard__day")[index].text().replace("DAY ", "").replace("尚未開賽", "0").toString() // 將該挑戰天數的挑賽人數 + 1 daysCount[challengeDay] = daysCount[challengeDay]!!.toInt().plus(1) // 取得挑戰狀態 val progressByChallengeStatus = ! item.select(".team-progress--challenge").isEmpty() val progressByFailStatus = ! item.select(".team-progress--fail").isEmpty() // 計算挑戰成功、挑戰中、挑戰失敗、已報名未挑戰人數 if (progressByChallengeStatus && !progressByFailStatus && challengeDay.toInt() == 30) challengeSuccessCount++ if (progressByChallengeStatus && !progressByFailStatus && challengeDay.toInt() != 30) challengingCount++ if (!progressByChallengeStatus && progressByFailStatus && challengeDay.toInt() == 0) unchallengedCount++ if (!progressByChallengeStatus && progressByFailStatus && challengeDay.toInt() > 0) challengeFailedCount++ } } // 儲存 API 結果進行輸出 response["全部參賽人數"] = totalRegisterPerson response["挑戰成功人數"] = challengeSuccessCount response["挑戰進行人數"] = challengingCount response["挑戰失敗人數"] = challengeFailedCount response["挑戰進度文章數量(天/篇)"] = daysCount return response }} (4) 接著執行程式 ,會產生如下 API 爬蟲結果: (5) 接著,當我們完成爬蟲程式並取得資料結果,後續其實就可以做很多事情,像是資料分析、資料視覺化等動作,下面也是我們針對結果產生出圖表,可以從圖表觀察出目前比賽進度的人數比例: 以上是 JSOUP 爬蟲介紹,建議大家可以練習實作看看,爬蟲程式在實作上不難,但卻可以讓我們在後續實作出很多很有趣的應用。 Rerference [官方文件] Jsoup cookbook","categories":[{"name":"2020-IThome鐵人賽","slug":"2020-IThome鐵人賽","permalink":"http://devinwangg.github.io/categories/2020-IThome%E9%90%B5%E4%BA%BA%E8%B3%BD/"}],"tags":[{"name":"Spring Boot","slug":"Spring-Boot","permalink":"http://devinwangg.github.io/tags/Spring-Boot/"}]},{"title":"[Day 22] 遠征 Kotlin × Spring Boot 介紹單元測試 (1)","slug":"ironman-2020-22","date":"2020-10-11T11:14:01.000Z","updated":"2020-10-11T11:14:01.000Z","comments":true,"path":"2020/10/11/ironman-2020-22/","link":"","permalink":"http://devinwangg.github.io/2020/10/11/ironman-2020-22/","excerpt":"","text":"此篇想談論單元測試並使用 Junit 工具進行測試撰寫,單元測試是針對程式模組(軟體設計的最小單位)進行正確性檢驗的測試工作,並且是一段可自動化執行的程式碼,程式會呼叫被測試的工作單元,再針對此單元所執行的最終結果進行假設驗證,驗證此單元結果是否符合我們所預期的行為,而工作單元通常是程式模組最小的單位,當單元測試檢測發現程式錯誤時,我們也可以在第一時間進行修正,已證實程式達到專案需求目標,故單元測試應該具備以下特質: 它應該是自動化,而且可被重複執行的 它應該很容易被實現 它的存在對於專案是具有意義的,並非臨時性作用 它的執行應該是容易的 它應該要能完全掌握被測試的單元 它應該是能完全被隔離的,執行時獨立於其他測試 如果檢測驗證失敗時,應該要能清楚呈現期望值與實際值差異,並且要能很清楚知道發生的原因為何,進一步修正錯誤 好的單元測試,應該要具備三種特色: 可信賴性(Trustworthiness) 開發者應對自己所撰寫測試的結果有信心,並且是針對實際專案需求進行正確的測試 可維護性(Maintainability) 測試也應保持好的可維護性,無法維護的測試會是一場惡夢,只會導致拖累專案整體進度 可閱讀性(Readability) 每次修改程式時都會持續進行單元測試檢測,當測試發生問題時,為了快速找到癥結點所在,保持好的閱讀性相當重要。 而實際在測試方法撰寫中,我們可以採取 3A 測試原則,如下: Arrange 初始化目標物件、相依物件、方法參數、預期結果 Act 執行測試工作單元,取得實際測試結果 Assert 驗證結果是否符合預期結果 以下直接將先前的 RESTful API 範例撰寫 Service Unit Test: Spring Boot 在建置專案時已經先引入 Test 套件org.springframework.boot:spring-boot-starter-test,裡面會包含相關測試模組,如 Junit、AssertJ、Mockito等元件 測試類別設定參數(@SpringBootTest、@MockBean、@Autowired): @SpringBootTest Annotation 會為我們引入測試元件 @MockBean 則是要新增一個 DAO 假物件,幫助我們順利進行Service的單元測試 @Autowired 新增一個 Service 物件進行測試 123456789@SpringBootTestclass TestStudentService { @MockBean lateinit var studentDao: StudentDao @Autowired lateinit var studentServiceImpl: StudentServiceImpl} 加入測試方法 測試取得所有學生資料 1234567891011121314@Testfun shouldGetAllStudentWhenCallMethod() { // Arrange 初始化測試資料與預期結果 val expectedResult : MutableList<Student> = mutableListOf<Student>() expectedResult.add(Student(1, "Devin", "devin@gmail.com")) expectedResult.add(Student(2, "Eric", "eric@gmail.com")) given(studentDao.findAll()).willReturn(expectedResult) // Act 執行測試工作單元,取得實際測試結果 val actual : MutableList<Student> = studentServiceImpl.findAllStudent() // Assert 驗證結果是否符合預期結果 assertEquals(expectedResult, actual)} 測試利用 id 取得單一學生資料 123456789@Testfun shouldGetOneStudentWhenCallMethodById() { val expectedResult = Student(1, "Devin", "devin@gmail.com") given(studentDao.findById(1)).willReturn(expectedResult) val actual : Student? = studentServiceImpl.findByStudentId(1) assertEquals(expectedResult, actual)} 測試利用 Name 欄位取得學生資料 12345678910@Testfun shouldGetStudentsWhenCallMethodByName() { val expectedResult : MutableList<Student> = mutableListOf<Student>() expectedResult.add(Student(1, "Devin", "devin@gmail.com")) given(studentDao.findByName("Devin")).willReturn(expectedResult) val actual : MutableList<Student> = studentServiceImpl.findByStudentName("Devin") assertEquals(expectedResult, actual)} 測試建立學生資料 12345678910@Testfun shouldGetNewStudentWhenCallMethodByStudent() { val expectedResult = Student( 1, "Devin", "devin@gmail.com") val requestParameter = Student( name = "Devin", email = "devin@gmail.com") given(studentDao.save(requestParameter)).willReturn(expectedResult) val actual : Student = studentServiceImpl.addStudent(requestParameter) assertEquals(expectedResult, actual)} 測試更新整個學生資料 12345678910@Testfun shouldUpdatedStudentWhenCallMethodByStudent() { val expectedResult = Student(1, "Devin", "devin@gmail.com") val requestParameter = Student(1, "Eric", "eric@gmail.com") given(studentDao.save(requestParameter)).willReturn(expectedResult) val actual : Student? = studentServiceImpl.updateStudent(requestParameter) assertEquals(expectedResult, actual)} 測試更新學生信箱 12345678910@Testfun shouldUpdatedEmailWhenCallMethodByStudent() { val expectedResult = Student(1, "Devin", "devin@gmail.com") val requestParameter = Student(1, "Devin", "test@gmail.com") given(studentDao.save(requestParameter)).willReturn(expectedResult) val actual : Student? = studentServiceImpl.updateStudentEmail(requestParameter) assertEquals(expectedResult.email, actual?.email)} 測試刪除學生資料 12345678910@Testfun shouldDeletedStudentWhenCallMethodByStudent() { val expectedResult = true val expectedSaveResult = Student(1, "Devin", "devin@gmail.com") given(studentDao.findById(1)).willReturn(expectedSaveResult) val actual = studentServiceImpl.deleteStudent(1) assertEquals(expectedResult, actual)} 此文章有提供範例程式碼在 Github 供大家參考","categories":[{"name":"2020-IThome鐵人賽","slug":"2020-IThome鐵人賽","permalink":"http://devinwangg.github.io/categories/2020-IThome%E9%90%B5%E4%BA%BA%E8%B3%BD/"}],"tags":[{"name":"Spring Boot","slug":"Spring-Boot","permalink":"http://devinwangg.github.io/tags/Spring-Boot/"}]},{"title":"[Day 23] 遠征 Kotlin × Spring Boot 介紹單元測試 (2)","slug":"ironman-2020-23","date":"2020-10-11T11:13:50.000Z","updated":"2020-10-11T11:13:50.000Z","comments":true,"path":"2020/10/11/ironman-2020-23/","link":"","permalink":"http://devinwangg.github.io/2020/10/11/ironman-2020-23/","excerpt":"","text":"上一篇我們完成了 Service 的單元測試,而這篇我們要來測試 Controller 單元測試,在前面架構章節有提到 Controller 是負責處理 Http 請求、路由處理、身份驗證與Json資料轉換等處理內容,而在測試我們為了要處理這些內容,會需要使用到兩個新的測試物件,@WebMvcTest 負責處理 Http請求與路由處理,ObjectMapper 負責協助我們將物件轉換為 Json 資料,以下直接進入實作觀察: 在測試Test資料夾新增測試檔案-TestStudentController,並替測試類別設定參數(@WebMvcTest、@MockBean、@Autowired、ObjectMapper),範例如下: 123456789101112@WebMvcTest(StudentController::class)class TestStudentController() { @MockBean lateinit var studentServiceImpl: StudentServiceImpl @Autowired lateinit var mockMvc: MockMvc private val objectMapper = ObjectMapper()} 加入測試方法 取得所有學生資料 API 1234567891011121314@Testfun shouldGetAllStudentWhenCallMethod() { val expectedResult : MutableList<Student> = mutableListOf<Student>() expectedResult.add(Student(1, "Devin", "devin@gmail.com")) expectedResult.add(Student(2, "Eric", "eric@gmail.com")) given(studentServiceImpl.findAllStudent()).willReturn(expectedResult) mockMvc.perform( get("/api/students") .contentType(MediaType.APPLICATION_JSON) ).andExpect(status().isOk) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(content().string(objectMapper.writeValueAsString(expectedResult)))} 取得單一學生資料 API 1234567891011121314@Testfun shouldGetOneStudentWhenCallMethodById() { val expectedResult : MutableList<Student> = mutableListOf<Student>() expectedResult.add(Student(1, "Devin", "devin@gmail.com")) given(studentServiceImpl.findByStudentName("Devin")).willReturn(expectedResult) mockMvc.perform( post("/api/students/search") .contentType(MediaType.APPLICATION_JSON) .param("name", "Devin") ).andExpect(status().isOk) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(content().string(objectMapper.writeValueAsString(expectedResult)))} 建立學生資料 API 1234567891011121314@Testfun shouldGetNewStudentWhenCallMethodByStudent() { val expectedResult = Student( 1, "Devin", "devin@gmail.com") val requestParameter = Student( name = "Devin", email = "devin@gmail.com") given(studentServiceImpl.addStudent(requestParameter)).willReturn(expectedResult) mockMvc.perform( post("/api/students") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestParameter)) ).andExpect(status().isOk) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(content().string(objectMapper.writeValueAsString(expectedResult)))} 更新學生資料 API 123456789101112131415@Testfun shouldUpdatedStudentWhenCallMethodByStudent() { val expectedResult = Student(1, "Devin", "devin@gmail.com") val requestParameter = Student(1, "Eric", "eric@gmail.com") given(studentServiceImpl.findByStudentId(1)).willReturn(requestParameter) given(studentServiceImpl.updateStudent(requestParameter)).willReturn(expectedResult) mockMvc.perform( put("/api/students/1") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestParameter)) ).andExpect(status().isOk) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(content().string(objectMapper.writeValueAsString(expectedResult)))} 更新學生信箱資料 API 1234567891011121314@Testfun shouldUpdatedEmailWhenCallMethodByStudent() { val expectedResult = Student(1, "Devin", "devin@gmail.com") val requestParameter = Student(1, "Devin", "test@gmail.com") given(studentServiceImpl.findByStudentId(1)).willReturn(requestParameter given(studentServiceImpl.updateStudentEmail(requestParameter)).willReturn(expectedResult) mockMvc.perform( patch("/api/students/1") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(requestParameter)) ).andExpect(status().isOk) .andExpect(content().contentType(MediaType.APPLICATION_JSON)) .andExpect(content().string(objectMapper.writeValueAsString(expectedResult)))} 刪除學生資料成功 API 12345678910@Testfun shouldGetIsNotContentStatusWhenDeleteSuccess() { val expectedResult = true given(studentServiceImpl.deleteStudent(1)).willReturn(expectedResult) mockMvc.perform( delete("/api/students/1") .contentType(MediaType.APPLICATION_JSON) ).andExpect(status().isNoContent)} 刪除學生資料失敗 API 12345678910@Testfun shouldGetBadRequestStatusWhenDeleteFailed() { val expectedResult = false given(studentServiceImpl.deleteStudent(1)).willReturn(expectedResult) mockMvc.perform( delete("/api/students/1") .contentType(MediaType.APPLICATION_JSON) ).andExpect(status().isBadRequest)} 最後我們利用下圖來觀察專案的測試覆蓋率(Test Coverage)發現都是 100%,表示我們已經將每一段程式都有進行測試,但這邊建議測試覆蓋率只是一個參考數值,頂多只能了解自己有沒有地方少做測試,這與專案會不會有問題沒有絕對關係,以前有待過專案測試覆蓋率要求100%的團隊,也有待過測試覆蓋率要求 70-80% 左右的團隊,但個人覺得最主要還是在於我們是否有將產品核心相關的功能盡可能做到測試,畢竟這些功能才是與使用者有高度相關的。 此文章有提供範例程式碼在 Github 供大家參考","categories":[{"name":"2020-IThome鐵人賽","slug":"2020-IThome鐵人賽","permalink":"http://devinwangg.github.io/categories/2020-IThome%E9%90%B5%E4%BA%BA%E8%B3%BD/"}],"tags":[{"name":"Spring Boot","slug":"Spring-Boot","permalink":"http://devinwangg.github.io/tags/Spring-Boot/"}]},{"title":"[Day 24] 遠征 Kotlin × Spring Boot 介紹 Template Engine (1)","slug":"ironman-2020-24","date":"2020-10-11T11:13:37.000Z","updated":"2020-10-11T11:13:37.000Z","comments":true,"path":"2020/10/11/ironman-2020-24/","link":"","permalink":"http://devinwangg.github.io/2020/10/11/ironman-2020-24/","excerpt":"","text":"Thymeleaf 是什麼Thymeleaf 是一個 XML/HTML5 模板引擎,能夠應用於模板設計檔案,非常適合 Spring 框架進行開發 HTML5 Web 應用程式 環境設置以下將介紹使用 Thymeleaf 進行專案開發時,所需要設定的環境: Spring Boot 在使用 Thymeleaf 時,須在配置加入 Thymeleaf 依賴套件 12<!-- thymeleaf 相關依賴 -->implementation("org.springframework.boot:spring-boot-starter-thymeleaf") 在 application.yml 新增 Thymeleaf 相關配置,具體配置如下: 123456789101112131415161718192021spring:h2: # 設定 H2 資料庫相關配置 console: enabled: true path: /h2-consoledatasource: # 設定資料庫相關配置 url: jdbc:h2:file:./src/main/resources/data/ironman;AUTO_SERVER=true username: sa password: Ironman0924!jpa: # 設定 JPA 相關配置 hibernate: ddl-auto: update database-platform: H2 show-sql: true generate-ddl: falsethymeleaf: cache: false # 關閉 Cache encoding: UTF-8 # 編碼設定 mode: HTML5 # 模式 suffix: .html # 檔案副檔名 prefix: classpath:/templates/ # 檔案儲存位置 撰寫 Controller,新增 HomeController.kt 檔案,內容如下: 12345678@Controllerclass HomeController { @RequestMapping("/") fun home() : String { return "home" }} 在 resources/templates 路徑下新增頁面檔案 home.html,並新增內容如下: 12345678910111213<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>Document</title></head><body>Hi, Thymeleaf</body></html> 開啟瀏覽器進行測試,會發現以下頁面內容: Thymeleaf 常用語法變數表達式Thymeleaf 預設帶有標準方言 Standard,它們定義了一組功能,這些功能足以讓我們應付大多數專案需求,而這些標準方言在模板的使用方式會包含以th字首開頭的屬性,如<span th:text="...">,表達式共有五種型別,分別整理如下: ${...} 變數表示式 {...} 選擇表示式 #{...} 訊息 (i18n) 表示式 @{...} 連結 (URL) 表示式 ~{...} : 片段表示式 變量表達式變量表達式即 OGNL 表示式或 Spring EL 表示式(在 Spring 術語中也叫 model attributes) 1234${session.user.name}<span th:text="${book.author.name}"><li th:each="book : ${books}"> 選擇(星號)表示式選擇表示式很像變量表達式,不過它們用一個預先選擇的物件來代替上下文變數容器(map)來執行 1234567*{customer.name}<div th:object="${book}"> ... <span th:text="*{title}">...</span> ...</div> 文字國際化表示式文字國際化表示式允許我們從一個外部檔案獲取區域文字資訊(.properties),用 Key 索引 Value,還可以提供一組引數 123456789#{main.title} #{message.entrycreated(${entryId})}<table> ... <th th:text="#{header.address.city}">...</th> <th th:text="#{header.address.country}">...</th> ...</table> URL表示式URL 表示式指的是把一個有用的上下文資訊新增到 URL,這個過程經常被叫做 URL Rewrite 1234567@{/order/list}@{/order/details(id=${orderId})}@{../documents/report}<form th:action="@{/createOrder}"><a href="main.html" th:href="@{/main}"> 以上主要介紹 Thymeleaf 環境建置與基本的變數表達用法,下一篇將進行實作範例,實作會讓我們對於 Thymeleaf 的使用上更加了解。","categories":[{"name":"2020-IThome鐵人賽","slug":"2020-IThome鐵人賽","permalink":"http://devinwangg.github.io/categories/2020-IThome%E9%90%B5%E4%BA%BA%E8%B3%BD/"}],"tags":[{"name":"Spring Boot","slug":"Spring-Boot","permalink":"http://devinwangg.github.io/tags/Spring-Boot/"}]},{"title":"[Day 25] 遠征 Kotlin × Spring Boot 介紹 Template Engine (2)","slug":"ironman-2020-25","date":"2020-10-11T11:10:36.000Z","updated":"2020-10-11T11:10:36.000Z","comments":true,"path":"2020/10/11/ironman-2020-25/","link":"","permalink":"http://devinwangg.github.io/2020/10/11/ironman-2020-25/","excerpt":"","text":"昨日我們介紹了 Thymeleaf 樣板引擎,並已經完成 Template 環境建置與初始頁面,接下來我們嘗試利用樣板引擎來製作一個待辦清單 TodoList,我們直接進入實際實作步驟與介紹: 設定環境設定 application.yml 123456789101112131415161718192021spring: h2: # 設定 H2 資料庫相關配置 console: enabled: true path: /h2-console datasource: # 設定資料庫相關配置 url: jdbc:h2:file:./src/main/resources/data/ironman;AUTO_SERVER=true username: sa password: Ironman0924! jpa: # 設定 JPA 相關配置 hibernate: ddl-auto: update database-platform: H2 show-sql: true generate-ddl: false thymeleaf: cache: false # 關閉 Cache encoding: UTF-8 # 編碼設定 mode: HTML5 # 模式 suffix: .html # 檔案副檔名 prefix: classpath:/templates/ # 檔案儲存位置 建立 Todo Entity,映射 Todo 資料表與欄位,此部份我們將 id 改為 UUID 來進行自動編號,並且利用資料庫自動新增資料建立時間 createTime 與資料修改時間 updateTime 123456789101112131415161718@Entity@Table@EntityListeners(AuditingEntityListener::class)@EnableJpaAuditingdata class Todo( @Id val id: UUID = UUID.randomUUID(), val task: String = "", var status: Int = 0, @CreatedDate @Column(updatable = false, nullable = false) val createTime: Date = Date(), @LastModifiedDate @Column(nullable = false) val updateTime: Date = Date()) 建立 Todo DAO,建立DAO物件處理資料庫溝通 1234567interface TodoDao : JpaRepository<Todo, Long>, JpaSpecificationExecutor<Todo> { /** * 查詢符合 Id 條件的資料 */ fun findById(id: UUID): Todo?} 建立 Service Interface-TodoService,預計會使用四種動作(取得資料、建立資料、更新狀態、刪除資料) 12345678910111213141516171819202122interface TodoService { /** * 取得所有 Todo 資料 */ fun getTodos(): Iterable<Todo> /** * 建立 Todo 資料 */ fun createTodo(todo: Todo): Todo /** * 更新 Todo 狀態 */ fun updateTodoStatus(id: String): Boolean /** * 刪除 Todo 資料 */ fun deleteTodo(id: String): Boolean} 實作 Service - TodoServiceImpl 123456789101112131415161718192021222324252627@Serviceclass TodoServiceImpl(@Autowired val todoDao: TodoDao) : TodoService { override fun getTodos(): Iterable<Todo> = todoDao.findAll() override fun createTodo(todo: Todo): Todo = todoDao.save(todo) override fun updateTodoStatus(id: String): Boolean = todoDao.findById(UUID.fromString(id)).run { return try { this?.let { if (it.status == 1) it.status = 0 else it.status = 1 todoDao.save(it) } true } catch (exception: Exception) { false } } override fun deleteTodo(id: String): Boolean = todoDao.findById(UUID.fromString(id)).run { return try { this?.let { todoDao.delete(it) } true } catch (exception: Exception) { false } }} 建立 Controller Interface - HomeController,建立資料(createTodo)會使用到 @ModelAttribute Annotation接收來自前端表單資料,而修改狀態資料(updateTodoStatus)與刪除資料(deleteTodo)則預計利用呼叫 API 方式進行動作,故須加上 @ResponseBody 標註: 12345678910111213141516interface HomeController { @GetMapping("/todos") fun getTodos(model: Model) : String @PostMapping("/todos") fun createTodo(@ModelAttribute todo: Todo) : String @PutMapping("/todos/{id}") @ResponseBody fun updateTodoStatus(@PathVariable id: String) @DeleteMapping("/todos/{id}") @ResponseBody fun deleteTodo(@PathVariable id: String)} 實作 Controller,建立資料(createTodo)完成後要記得轉向取得資料頁面,會使用到 redirect 轉向方法 1234567891011121314151617181920212223@Controllerclass HomeControllerImpl(@Autowired val todoService: TodoService): HomeController { override fun getTodos(model: Model): String { model.addAttribute("todolist", todoService.getTodos()); model.addAttribute("todoObject", Todo()) return "home" } override fun createTodo(todo: Todo): String { todoService.createTodo(todo) return "redirect:/todos" } override fun updateTodoStatus(id: String) { todoService.updateTodoStatus(id) } override fun deleteTodo(id: String) { todoService.deleteTodo(id) }} 在 resource / templates 資料夾建立 home.html 12345678910111213141516171819202122232425262728293031323334353637383940414243444546<!DOCTYPE html><html lang="en" xmlns:th="http://www.thymeleaf.org"><head> <meta charset="UTF-8"> <link rel="stylesheet" th:href="@{/style.css}"> <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet"> <title>Todo List</title></head><body><div class="container"> <h2>待辦事項清單</h2> <form class="inputBox" th:action="@{/todos}" method="post" th:object="${todoObject}"> <input type="text" id="input" placeholder="新增項目" th:field="*{task}"> <button type="submit" class="submit">加入</button> </form> <ul th:each="todo: ${todolist}"> <li th:class="${todo.status} == 1 ? 'checked': '' " th:onclick="updateTodoStatus([[${todo.id}]])"> <span th:text="${todo.task}"></span> <span class="close" th:onclick="deleteTodo([[${todo.id}]])">x</span> </li> </ul></div><script> const deleteTodo = (id) => { fetch('todos/'+ id, { method: 'delete', }).then((response) => { if (response.status === 200) { location.reload(); } }) }; const updateTodoStatus = (id) => { fetch('todos/' + id, { method: 'put', }).then((response) => { if (response.status === 200) { location.reload(); } }) };</script></body></html> 在 resource / static 建立 style.css 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120body{ background-color: #d5827b; font-family: arial, "Microsoft JhengHei","微軟正黑體", sans-serif !important;}body h2{ color: white;}.container { width: 50%; margin: 10% auto;}ul { padding: 0px;}ul li { cursor: pointer; position: relative; padding: 12px 8px 12px 40px; background: #eee; font-size: 18px; transition: 0.2s; box-shadow: 0px 10px 15px #666; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; list-style: none;}ul li:nth-child(odd) { background: #f9f9f9;}ul li:hover { background: #ddd;}h2 { font-size: 32px; font-weight: 700;}ul li.checked { background: #7a534f; color: #fff; text-decoration: line-through; border: 0.5px solid black;}ul li.checked::before { content: ''; position: absolute; border-color: #fff; border-style: solid; border-width: 0 2px 2px 0; top: 10px; left: 16px; transform: rotate(45deg); height: 15px; width: 7px;}.close { position: absolute; right: 0; top: 0; padding: 12px 16px 12px 16px;}.close:hover { background-color: #f44336; color: white;}.inputBox { background-color: #e0e2c6; padding: 30px 40px; color: #5C4319; text-align: center; box-shadow: 0px 10px 15px #666;}.inputBox:after { content: ""; display: table; clear: both;}input { margin: 0; border: none; border-radius: 0; width: 75%; padding: 10px; float: left; font-size: 16px;}.submit { padding: 8px; background: #79b786; color: #fbfffd;; float: left; text-align: center; font-size: 16px; cursor: pointer; transition: 0.3s; border-radius: 0; margin-left: 10px;}.submit:hover { background-color: #bbb;} 最後執行專案,即可看到 Todo List: 此文章有提供範例程式碼在 Github 供大家參考","categories":[{"name":"2020-IThome鐵人賽","slug":"2020-IThome鐵人賽","permalink":"http://devinwangg.github.io/categories/2020-IThome%E9%90%B5%E4%BA%BA%E8%B3%BD/"}],"tags":[{"name":"Spring Boot","slug":"Spring-Boot","permalink":"http://devinwangg.github.io/tags/Spring-Boot/"}]},{"title":"[Day 26] 遠征 Kotlin × Spring Boot 部署網站到 Heroku","slug":"ironman-2020-26","date":"2020-10-11T11:10:06.000Z","updated":"2020-10-11T11:10:06.000Z","comments":true,"path":"2020/10/11/ironman-2020-26/","link":"","permalink":"http://devinwangg.github.io/2020/10/11/ironman-2020-26/","excerpt":"","text":"前面我們利用 Thymeleaf 樣板引擎結合 H2 資料庫實作 Todo 待辦事項清單,而既然我們完成了一個小作品,我們就來將作品發佈到網路上吧!這邊我們利用 Heroku 服務進行網站部署,而 Heroku 是一個平台即服務(PaaS),可以讓我們部署各種網站,減少維護管理系統底層的成本,類似的平台有 GCP、AWS、Azure、阿里雲等。 而 Heroku 在收費方面,以個人用途而言,提供每個帳戶每個月有 550 小時的免費額度(如下圖),若加入信用卡認證可以增加到 1000 小時的免費額度,也可以讓開發者客製化命名 Domains ,而在部署方面又非常快速簡單,非常適合開發者測試部署。 圖片來源自Heroku官方網站 接下來,我們實際介紹部署步驟: 專案部份我們直接利用昨日範例(Github連結)進行部署,也可以使用自己的專案進行部署 首先必須要先確保大家有 Heroku 帳號,可以利用此 Heroku 登入網頁(https://id.heroku.com/login)嘗試登入,若沒有帳號的朋友,請記得先註冊 接下來需要下載 Heroku CLI 開發工具,下載方式可以進入HeroKu 下載頁面,再根據對應的作業系統進行下載 下載完畢後,打開終端機(Terminal)並切換終端機路徑到部署專案路徑下,再輸入 heroku login 進行 Heroku 登入,會需要輸入第二步驟登入或註冊的信箱與密碼 1$ heroku login 接著在專案路徑底下輸入 git 初始設定 與 heroku 發佈,輸入如下: 12345$ git init$ git add .$ git commit -m "first commit"$ heroku create$ git push heroku master 發佈完成後,我們會在終端機畫面上看到 Build Success提示訊息 與 部署完成的網址 ,此時我們打開瀏覽器開啟網址就可以看到我們部署的專案成果 接著我們可以到 Heroku 的 Dashboard 管理頁面,會發現到我們剛上傳的應用程式(immense-mesa-06828) 而應用程式名稱是 Heroku 幫我們自動命名,若大家想要重新命名,可以點擊該應用程式進入頁面,再點擊 Settings 頁面,直接在 App Name 欄位進行名稱設定即可, Domains 網址也會自動更新 ,如下圖: 我們打開瀏覽器瀏覽( https://ithome-2020.herokuapp.com/todos ) 會發現網站已經成功修改為我們客製化命名的 Domain Reference [官方] Heroku 網站","categories":[{"name":"2020-IThome鐵人賽","slug":"2020-IThome鐵人賽","permalink":"http://devinwangg.github.io/categories/2020-IThome%E9%90%B5%E4%BA%BA%E8%B3%BD/"}],"tags":[{"name":"Spring Boot","slug":"Spring-Boot","permalink":"http://devinwangg.github.io/tags/Spring-Boot/"}]},{"title":"[Day 27] 遠征 Kotlin × Spring Boot 介紹 Spring AOP 機制","slug":"ironman-2020-27","date":"2020-10-11T11:08:22.000Z","updated":"2020-10-11T11:08:22.000Z","comments":true,"path":"2020/10/11/ironman-2020-27/","link":"","permalink":"http://devinwangg.github.io/2020/10/11/ironman-2020-27/","excerpt":"","text":"切面導向程式設計(Aspect-oriented programming, AOP),又譯為面向方面程式設計、剖面導向程式設計,此設計最主要目的是實現關注點分離(Separation of concerns),希望將專案的橫切關注點與業務核心主體進行分離,以提高程式碼的模組化程度,使得我們可以直接將與核心業務功能關係較不相關的功能直接添加至程式中,同時又不會造成核心功能的程式可讀性複雜化,例如 Log 紀錄檔功能。 故 AOP 機制可以讓我們將一些非功能性配置與核心業務功能進行分離,非功能性配置有例如日誌紀錄、效能統計、安全控制、事務處理、異常處理等配置,此優點又可以讓我們更專注在業務邏輯上的開發,不會有非功能性配置與業務功能耦合性問題。 AOP 有以下相關主要術語: Aspect 切面:由 切入點(PointCut)與 通知(Advice)組成,主要就是用來設定切入點(PointCut)與切入特定動作(Advice) PointCut 切點:設定要被 AOP 切入的位置,例如某個類別或函數 JoinPoint 連接點:為 PointCut 切入後的實際切入點,通常是一個函數 Advice 通知:為 Joint Point 切入點實際要執行的動作,通常會將 Advice 模擬為一個攔截器(Interceptor),並且會在連接點(Join Point)上維護多個 Advice 進行層層攔截 Advice 又可以分為五種類型: @Before 前置通知 — 在呼叫方法前執行 @AfterReturn 正常返回通知 — 正常返回方法後執行 @AfterThrowing 異常返回通知 — 在連接點拋出異常後執行 @After 返回通知 — 方法最終結束後執行,相當於finaly @Around 環繞通知 — 圍繞整個方法 五種類型執行順序為 @Around > @Before > @Around > @After > @AfterReturning 接下來我們直接介紹實作: 使用前面專案的 Todo 專案增加 AOP 方法,當使用者操作 Service 時,新增 Log 紀錄檔 新增 ServiceAspect.kt 檔案,內容如下: 12345678910111213141516171819202122232425262728293031323334353637@Aspect@Componentclass ServiceAspect { // 第一個 * 表示任意返回值 // com.inroman.demo... 為 package 路徑 // 第二個 * 表示任何 Service 物件 // 第三個 .*(..) 則表示任何方法 @Pointcut("execution(* com.ironman.demo.service.*.*(..))") fun pointcut() {} // 設定 Before 通知並執行 pointcut 切點 @Before("pointcut()") fun before(joinPoint: JoinPoint) { // 設定 Logger 帶入切入點類別名稱 val logger = LoggerFactory.getLogger(joinPoint.target.javaClass.name) // 取得切入點方法 val methodSignature: MethodSignature = joinPoint.signature as MethodSignature // 取得切入點方法名稱 val methodName = methodSignature.method.name // 取得切入點方法類別 val className = joinPoint.target.javaClass.name // 取得切入點方法參數 val argsInfo = joinPoint.args logger.info("[處理開始] Service: $className, Method:$methodName, Args: $argsInfo") } // 設定 After 通知並執行 pointcut 切點 @After("pointcut()") fun after(joinPoint: JoinPoint) { val logger = LoggerFactory.getLogger(joinPoint.target.javaClass.name) val methodSignature: MethodSignature = joinPoint.signature as MethodSignature val methodName = methodSignature.method.name val className = joinPoint.target.javaClass.name val argsInfo = joinPoint.args logger.info("[處理結束] Service: $className, Method: $methodName, Args: $argsInfo") }} 觀察專案運行 Log Reference [官方] Spring AOP","categories":[{"name":"2020-IThome鐵人賽","slug":"2020-IThome鐵人賽","permalink":"http://devinwangg.github.io/categories/2020-IThome%E9%90%B5%E4%BA%BA%E8%B3%BD/"}],"tags":[{"name":"Spring Boot","slug":"Spring-Boot","permalink":"http://devinwangg.github.io/tags/Spring-Boot/"}]},{"title":"[Day 28] 遠征 Kotlin × Spring Boot 介紹 WebSocket 實作","slug":"ironman-2020-28","date":"2020-10-11T11:08:09.000Z","updated":"2020-10-11T11:08:09.000Z","comments":true,"path":"2020/10/11/ironman-2020-28/","link":"","permalink":"http://devinwangg.github.io/2020/10/11/ironman-2020-28/","excerpt":"","text":"先前我們設計的 API 其實都是利用 HTTP 協定進行傳輸,而 HTTP 只能利用 Client 端發送請求到 Service端,這類型屬於單向的,而 WebSocket 也是一種網路傳輸協定,它可以支援我們在 TCP 連接上進行全雙工通訊,使得 Client 端和 Server 端之間的資料交換可以變得更簡單,允許 Server 端主動向 Client 端傳輸資料。 早期,網站為了實現 Server 端向 Client 端傳輸資料,會使用到輪詢(Polling)技術,此技術主要就是在 Client端設計由瀏覽器每隔一段時間向 Server 端發送 HTTP 請求(Request),再由 Server 端回應最新的資料給 Client端,而此技術最大的缺點就是瀏覽器會不斷向 Server 端發送請求,可能會造成浪費許多頻寬資源。 目前較新的 Polling 技術是 Comet ,採用的方法是長時間輪詢(Long-Polling),設計概念則是讓 Server 在接收到瀏覽器所送出的 Http 請求後,Server 端會等待一段時間,若在這段時間內伺服器有新的資料,他就會把最新的資料傳給瀏覽器,倘若沒有新的資料,則會回應瀏覽器資料沒有更新。雖然 Long-Polling 可以減少原先 Polling 技術造成網路頻寬浪費的狀況,但如果專案功能是屬於資料更新頻率很高的狀況下,Long-Polling 其實不會比 Polling 還要有效率。 而此篇要介紹的 WebSocket 協定其實也是建立於 HTTP 架構之上,它背後基本上還是以 HTTP 作為傳輸層,與 HTTP 一樣使用 80、443 port(https),但 WebSocket 大幅改善了 Comet 缺點,連線數量減少為一條,當 Server 端有資料更新時,會自動傳送給 Client 端,進行即時更新(Realtime)的動作,所以WebSocket非常適用於即時系統上,例如聊天室、遊戲、證券交易系統、多人共同編輯工具等。 接下來,我們直接實作聊天室應用來深入感受 WebSocket 技術: 在 Gradle build.gradle.kts 加入 WebSocket 套件 1implementation("org.springframework.boot:spring-boot-starter-websocket") 建立 WebSocket 配置- WebSocketConfig 12345678910111213@Configuration@EnableWebSocketMessageBrokerclass WebSocketConfig : WebSocketMessageBrokerConfigurer { override fun registerStompEndpoints(stompEndpointRegistry: StompEndpointRegistry) { stompEndpointRegistry.addEndpoint("/ws").setAllowedOrigins().withSockJS() } override fun configureMessageBroker(messageBrokerRegistry: MessageBrokerRegistry) { messageBrokerRegistry.setApplicationDestinationPrefixes("/app") messageBrokerRegistry.enableSimpleBroker("/topic") }} 建立 Data Class- ChatMessage 1234567891011enum class MessageType { CHAT, JOIN, LEAVE}data class ChatMessage( val type: MessageType, val content: String? = null, val sender: String) 建立 WebSocketConfig 監聽器- WebSocketConfig 12345678910111213141516171819202122232425262728293031@Componentclass WebSocketEventListener(@Autowired val simpleMessageSendingOperations: SimpMessageSendingOperations) { val logger: Logger = LoggerFactory.getLogger(WebSocketEventListener::class.java) /** * WebSocket 連線監聽器 */ @EventListener fun handleWebSocketConnectListener(sessionConnectedEvent: SessionConnectedEvent) { logger.info("接收到新的連線"); } /** * WebSocket 中斷連線監聽器 */ @EventListener fun handleWebSocketDisconnectListener(sessionDisconnectEvent: SessionDisconnectEvent) { val stompHeaderAccessor: StompHeaderAccessor = StompHeaderAccessor.wrap(sessionDisconnectEvent.message) val username: String? = stompHeaderAccessor.sessionAttributes?.get("username") as String? if (username != null) { logger.info("$username 使用者離開聊天室"); val chatMessage = ChatMessage( type = MessageType.LEAVE, sender = username ) simpleMessageSendingOperations.convertAndSend("/topic/public", chatMessage); } }} 建立 Controller 方法- ChatController 123456789101112131415161718192021@Controllerclass ChatController { /** * 新增聊天訊息 */ @MessageMapping("/sendMessage") @SendTo("/topic/public") fun sendMessage(@Payload chatMessage: ChatMessage): ChatMessage = chatMessage /** * 新增使用者 */ @MessageMapping("/addUser") @SendTo("/topic/public") fun addUser(@Payload chatMessage: ChatMessage, simpMessageHeaderAccessor: SimpMessageHeaderAccessor): ChatMessage { // 設定使用者姓名 simpMessageHeaderAccessor.sessionAttributes?.put("username", chatMessage.sender) return chatMessage }} 完成後端的 WebSocket 後,接下來我們實作前端聊天室部份,前端聊天室部份會採用 Sockjs 套件簡化 WebSocket 呼叫,而一般使用 Sockjs 會搭配 Stomp 套件一起使用。 在 resources/static 資料夾下新增 index.html,內容如下: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354<!DOCTYPE html><html> <head> <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0"> <title>Spring Boot WebSocket 聊天室應用</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <link rel="stylesheet" href="/css/main.css" /> </head> <body> <noscript> <h2>Sorry! 您的瀏覽器不支援</h2> </noscript> <div id="username-page"> <div class="username-page-container"> <h1 class="title">輸入聊天室使用者名稱</h1> <form id="usernameForm" name="usernameForm"> <div class="form-group"> <input type="text" id="name" placeholder="使用者名稱" autocomplete="off" class="form-control" /> </div> <div class="form-group"> <button type="submit" class="accent username-submit">開始聊天</button> </div> </form> </div> </div> <div id="chat-page" class="hidden"> <div class="chat-container"> <div class="chat-header"> <h2>Spring Boot WebSocket 聊天室</h2> </div> <div class="connecting"> 連線中... </div> <ul id="messageArea"> </ul> <form id="messageForm" name="messageForm" nameForm="messageForm"> <div class="form-group"> <div class="input-group clearfix"> <input type="text" id="message" placeholder="輸入對話訊息" autocomplete="off" class="form-control"/> <button type="submit" class="primary">送出</button> </div> </div> </form> </div> </div> <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script> <script src="/js/main.js"></script> </body></html> 在 resources/static/js 資料夾新增 main.js 檔案,內容如下: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111const usernamePage = document.querySelector('#username-page');const chatPage = document.querySelector('#chat-page');const usernameForm = document.querySelector('#usernameForm');const messageForm = document.querySelector('#messageForm');const messageInput = document.querySelector('#message');const messageArea = document.querySelector('#messageArea');const connectingElement = document.querySelector('.connecting');let stompClient = null;let username = null;let colors = [ '#2196F3', '#32c787', '#00BCD4', '#ff5652', '#ffc107', '#ff85af', '#FF9800', '#39bbb0'];// 設定WebSocket連線const connect = (event) => { username = document.querySelector('#name').value.trim(); if(username) { usernamePage.classList.add('hidden'); chatPage.classList.remove('hidden'); let socket = new SockJS('/ws'); stompClient = Stomp.over(socket); stompClient.connect({}, onConnected, onError); } event.preventDefault();};// 連線成功時發出 addUser 請求const onConnected = (options) => { stompClient.subscribe('/topic/public', onMessageReceived); stompClient.send("/app/addUser", {}, JSON.stringify({sender: username, type: 'JOIN'}) ); connectingElement.classList.add('hidden');};// 無法連線到 WebSocket 時出現錯誤const onError = (error) => { connectingElement.textContent = '無法連到 WebSocket 伺服器'; connectingElement.style.color = 'red';};// 發送對話訊息const sendMessage = (event) => { let messageContent = messageInput.value.trim(); if(messageContent && stompClient) { let chatMessage = { sender: username, content: messageInput.value, type: 'CHAT' }; stompClient.send("/app/sendMessage", {}, JSON.stringify(chatMessage)); messageInput.value = ''; } event.preventDefault();};// 接收 WebSocket 回應進行處理const onMessageReceived = (payload) => { let message = JSON.parse(payload.body); let messageElement = document.createElement('li'); if(message.type === 'JOIN') { messageElement.classList.add('event-message'); message.content = message.sender + ' joined!'; } if (message.type === 'LEAVE') { messageElement.classList.add('event-message'); message.content = message.sender + ' left!'; } if (message.type === 'CHAT'){ messageElement.classList.add('chat-message'); let avatarElement = document.createElement('i'); let avatarText = document.createTextNode(message.sender[0]); avatarElement.appendChild(avatarText); avatarElement.style['background-color'] = getHashBackgroundColor(message.sender); messageElement.appendChild(avatarElement); let usernameElement = document.createElement('span'); let usernameText = document.createTextNode(message.sender); usernameElement.appendChild(usernameText); messageElement.appendChild(usernameElement); } let textElement = document.createElement('p'); let messageText = document.createTextNode(message.content); textElement.appendChild(messageText); messageElement.appendChild(textElement); messageArea.appendChild(messageElement); messageArea.scrollTop = messageArea.scrollHeight;};// 取得姓名象徵顏色const getHashBackgroundColor = (messageSender) => { let hash = 0; for (let i = 0; i < messageSender.length; i++) { hash = 31 * hash + messageSender.charCodeAt(i); } let index = Math.abs(hash % colors.length); return colors[index];};// 設定 Submit 事件usernameForm.addEventListener('submit', connect, true);messageForm.addEventListener('submit', sendMessage, true); 最後完成結果 此文章有提供範例程式碼在 Github 供大家參考 Reference [文章] 維基百科(WebSocket) [文章]WebSocket 通訊協定簡介:比較 Polling、Long-Polling 與 Streaming 的運作原理) [文章]Building a chat application with Spring Boot and WebSocket [文章]常用的Websocket技術一覽","categories":[{"name":"2020-IThome鐵人賽","slug":"2020-IThome鐵人賽","permalink":"http://devinwangg.github.io/categories/2020-IThome%E9%90%B5%E4%BA%BA%E8%B3%BD/"}],"tags":[{"name":"Spring Boot","slug":"Spring-Boot","permalink":"http://devinwangg.github.io/tags/Spring-Boot/"}]},{"title":"[Day 29] 遠征 Kotlin × Spring Boot 介紹多資料庫連線配置","slug":"ironman-2020-29","date":"2020-10-11T11:07:50.000Z","updated":"2020-10-11T11:07:50.000Z","comments":true,"path":"2020/10/11/ironman-2020-29/","link":"","permalink":"http://devinwangg.github.io/2020/10/11/ironman-2020-29/","excerpt":"","text":"在實務開發上,我們有可能會遇到專案的業務需求越來越複雜,會使用的資料庫相對變得比較分散,這時就可以採用多資料來源方式取得資料,而這篇文章將介紹如何在 Spring Boot 使用多資料庫連線配置,我們一樣直接使用實作來體驗如何完成功能: 由於這篇要介紹多資料庫範例,我們選擇常見資料庫(SQL Server、MySQL)進行示範,而這邊為了實作方便,會直接利用 Docker 進行示範,大家可以在電腦內安裝 Docker 與 Docker-Compose,若朋友電腦裡面本身就有 SQL Server 與 MySQL的話,也可以直接修改為自己電腦的資料庫,不需要使用 Docker,而Docker-Compose 配置如下: 1234567891011121314151617181920212223version: '3'services: # MySQL 配置 ironman_mysql: container_name: ironman_mysql image: mysql ports: - 3333:3306 command: --port 3306 environment: - MYSQL_ROOT_PASSWORD=root # SQL Server 配置 ironman_mssql: container_name: ironman_mssql image: microsoft/mssql-server-linux:2017-latest ports: - 3334:1433 environment: - ACCEPT_EULA=Y - SA_PASSWORD=SqlServer123!@# - MSSQL_PID=Developer 資料庫設定完成後,我們可以先連到資料庫建立資料表與資料,SQL 範例如下: MySQL 123456789101112CREATE DATABASE IF NOT EXISTS ironman DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;USE ironman;CREATE TABLE article( id INT NOT NULL AUTO_INCREMENT, title VARCHAR(200), author VARCHAR(30), PRIMARY KEY (id));INSERT INTO article (title, author) VALUES ('[Day 29] 遠征 Kotlin × Spring Boot 介紹多資料庫連線配置', 'Devin');INSERT INTO article (title, author) VALUES ('[Day 30] 遠征 Kotlin × Spring Boot', 'Devin'); SQL Server 123456789101112131415IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'ironman')BEGIN CREATE DATABASE ironmanENDIF NOT EXISTS (SELECT * FROM sys.objects WHERE name = 'userData')BEGIN CREATE TABLE userData ( id int primary key identity (1, 1), name varchar(100) )ENDINSERT INTO userData (name) VALUES ('Devin')INSERT INTO userData (name) VALUES ('Eric') 接下來要進入實際專案開發,首先在專案中引入資料庫套件配置,這篇文章將選擇 SQL Server、MySQL作為示範,若大家需要使用其他資料庫,請記得要先設定資料庫配置,本篇資料庫配置設定如下: 12implementation("com.microsoft.sqlserver:mssql-jdbc")implementation("mysql:mysql-connector-java") 再來設定 application.yml YAML檔案,內容主要是設定要連接的兩個資料庫,命名利用 primary 與 secondary 進行區分,此命名會關係到待會設定的 Config 檔案,內容如下: 12345678910111213spring: datasource: primary: url: jdbc:mysql://localhost:3333/ironman username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver secondary: url: jdbc:sqlserver://localhost:3334 databaseName: ironman username: sa password: SqlServer123!@# driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver 建立兩個資料庫關聯的 Entity 與 Repository 檔案,內容如下: entity / mssql / user.kt 1234567891011@Entity@Table(name = "userData")data class User( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") var id: Int = 0, @Column(name = "name") var name: String = "") entity / mysql / article.kt 123456789101112131415@Entity@Table(name = "article")data class Article( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "id") var id: Int = 0, @Column(name = "title") var title: String = "", @Column(name = "author") var author: String = "") repository / mssql / UserRepository 12@Repositoryinterface UserRepository : JpaRepository<User, Int> repository / mysql / ArticleRepository 12@Repositoryinterface ArticleRepository : JpaRepository<Article, Int> 當我們建立完成與資料庫相關的 Entity 與 Repository 檔案後,就可以來設定多資料庫連線的配置檔案,內容如下: PrimaryDBConfig 12345678910111213141516171819202122232425262728293031323334353637383940414243444546@Configuration@EnableJpaRepositories( basePackages = ["com.ironman.multipledatabase.repository.mysql"], entityManagerFactoryRef = "primaryDBEntityManager", transactionManagerRef = "primaryDBTransactionManager")class PrimaryDBConfig { @Bean @Primary @ConfigurationProperties(prefix = "spring.datasource.primary") fun primaryDBProperties(): DataSourceProperties { return DataSourceProperties() } @Bean @Primary @Autowired fun primaryDBDataSource( @Qualifier("primaryDBProperties") properties: DataSourceProperties ): DataSource { return properties.initializeDataSourceBuilder().build() } @Bean @Primary @Autowired fun primaryDBEntityManager( builder: EntityManagerFactoryBuilder, @Qualifier("primaryDBDataSource") dataSource: DataSource ): LocalContainerEntityManagerFactoryBean { return builder.dataSource(dataSource) .packages("com.ironman.multipledatabase.entity.mysql") .properties(mapOf("hibernate.hbm2ddl.auto" to "update")) .persistenceUnit("primary") .build() } @Bean @Primary @Autowired fun primaryDBTransactionManager( @Qualifier("primaryDBEntityManager") primaryDBEntityManager: EntityManagerFactory ): JpaTransactionManager { return JpaTransactionManager(primaryDBEntityManager) }} SecondaryDBConfig 123456789101112131415161718192021222324252627282930313233343536373839404142@Configuration@EnableJpaRepositories( basePackages = ["com.ironman.multipledatabase.repository.mssql"], entityManagerFactoryRef = "secondaryDBEntityManager", transactionManagerRef = "secondaryDBTransactionManager")class SecondaryDBConfig { @Bean @ConfigurationProperties(prefix = "spring.datasource.secondary") fun secondaryDBProperties(): DataSourceProperties { return DataSourceProperties() } @Bean @Autowired fun secondaryDBDataSource( @Qualifier("secondaryDBProperties") properties: DataSourceProperties ): DataSource { return properties.initializeDataSourceBuilder().build() } @Bean @Autowired fun secondaryDBEntityManager( builder: EntityManagerFactoryBuilder, @Qualifier("secondaryDBDataSource") dataSource: DataSource ): LocalContainerEntityManagerFactoryBean { return builder.dataSource(dataSource) .packages("com.ironman.multipledatabase.entity.mssql") .properties(mapOf("hibernate.hbm2ddl.auto" to "update")) .persistenceUnit("secondary") .build() } @Bean @Autowired fun secondaryDBTransactionManager( @Qualifier("secondaryDBEntityManager") primaryDBEntityManager: EntityManagerFactory ): JpaTransactionManager { return JpaTransactionManager(primaryDBEntityManager) }} 當我們設定完資料庫部份後,我們再利用 Controller 建立 API 取得資料庫資料,內容如下: controller / MssqlUserController 1234567891011@RestController@RequestMapping("/users")class MssqlUserController ( val userRepository: UserRepository){ @GetMapping("/") @ResponseBody fun getAllUser(): ResponseEntity<Any>{ return ResponseEntity.ok(userRepository.findAll()) }} controller / MysqlArticleController 1234567891011@RestController@RequestMapping("/articles")class MysqlArticleController ( val storeRepository: ArticleRepository){ @GetMapping("/") @ResponseBody fun getAllStore(): ResponseEntity<Any>{ return ResponseEntity.ok(storeRepository.findAll()) }} 執行結果如下: 此文章有提供範例程式碼在 Github 供大家參考","categories":[{"name":"2020-IThome鐵人賽","slug":"2020-IThome鐵人賽","permalink":"http://devinwangg.github.io/categories/2020-IThome%E9%90%B5%E4%BA%BA%E8%B3%BD/"}],"tags":[{"name":"Spring Boot","slug":"Spring-Boot","permalink":"http://devinwangg.github.io/tags/Spring-Boot/"}]},{"title":"[Day 30] 遠征 Kotlin × Spring Boot 完賽心得分享","slug":"ironman-2020-30","date":"2020-10-11T11:07:04.000Z","updated":"2020-10-11T11:07:04.000Z","comments":true,"path":"2020/10/11/ironman-2020-30/","link":"","permalink":"http://devinwangg.github.io/2020/10/11/ironman-2020-30/","excerpt":"","text":"終於來到第 30 天文章,其實到了今天還一直在思考今天這篇要寫技術分享還是完賽心得,最後還是選擇寫完賽心得,畢竟在這過程中真的有許多感觸。還記得當初在學期間就經常會關注 IThome 鐵人賽這個網站,每年都可以從眾多參賽者的文章中學習到很多知識,我想這也算是在台灣這邊很難得的一個技術活動,也從來沒想過自己也會有一年參加這項活動,畢竟整整 30 天要持續分享文章,有可能光是想文章標題都是一件難事(其實真的到中後期都是每天在思考今天要講什麼),參加前其實也有想過要事先準備文章內容,但中後期的每一天都會想要再重新調整文章的整體內容。但我想這樣的心境,或許也是一件好事,代表自己是持續在成長的,30天前所設定的文章架構、標題、內容,經過每天的奮戰與思考,才會有了最後這份系列文章。 自己其實從以前就很想要培養寫文章的習慣,但遲遲沒有開始進行,每次開始總會遇到各種事情而放下這個念頭,不過也覺得自己有一個很妙的點,自己在過去其實經常參加許多資訊相關競賽,大多屬於專案或產品型競賽,這樣的背景也讓自己比較常在撰寫專案企劃或商業計劃書(BP),但是在撰寫學習型文章這件事卻是一個很大的障礙,也非常感謝 IThome 每年都持續辦這個競賽,透過今年這次的鐵人賽,也算是為自己打開寫作習慣這條路,希望自己後續能夠持續下去,透過寫作來強化自己的學習深度。 在這邊想感謝這次參賽的親朋好友,首先是今年加入技術團隊的好朋友 cailiwu,還記得八月初,我很突然的發起一起參加鐵人賽這個活動,朋友二話不說就一起參加了,謝謝這 30 天的一路陪伴。再來想感謝 Kotlin 鐵人陣 的大家,自己是從今年五月初就開始持續參加 Kotlin 線上讀書會的活動,感謝你們非常認真在經營這個社群,覺得這段期間的收穫非常多,而在這次鐵人賽中,也關注到大家很多很精彩的文章分享,謝謝。 最後想感謝女友的支持,在參加比賽的過程中,老是為了找空閒時間來專心寫文,一直比較沒辦法陪伴妳,非常感謝這陣子的包容與支持。期待自己在未來能夠持續撰寫文章,強化自己的學習與知識,我想這 30 天會成為自己 2020 年很棒的一個回憶!","categories":[{"name":"2020-IThome鐵人賽","slug":"2020-IThome鐵人賽","permalink":"http://devinwangg.github.io/categories/2020-IThome%E9%90%B5%E4%BA%BA%E8%B3%BD/"}],"tags":[]},{"title":"[Day 02] 遠征預備 Kotlin × 開發環境介紹","slug":"ironman-2020-02","date":"2020-10-11T10:45:46.000Z","updated":"2020-10-11T10:45:46.000Z","comments":true,"path":"2020/10/11/ironman-2020-02/","link":"","permalink":"http://devinwangg.github.io/2020/10/11/ironman-2020-02/","excerpt":"","text":"為了遠征 Spring Boot 開發,我們預計會花幾天快速帶大家認識 Kotlin 基本語法。 首先,在開始學習 Kotlin 程式語法與特性之前,我們先來稍微認識 Kotlin 程式語言,如果正在閱讀的朋友,先前有使用過 Intellij、PhpStorm、PyCharm、Rider、GoLand 等編輯器的話,其實 Kotlin 就是這間 IDE 軟體公司-JetBrains 設計開發並開源誕生,而 Kotlin 是在 Java虛擬機 (Java Virtual Machine, JVM)執行的靜態型別程式設計語言,在Google I/O 2017中,Google 宣布 Kotlin 成為 Android 官方開發語言,並在 2019 年 Kotlin 替代 Java 成為 Android 開發預設語言,Spring 也在 5.x 版本開始支持 Kotlin,而 Kotlin 在開發上更具有以下優勢(後續章節會再深入介紹): 開發上可以 100% 兼容 Java 程式語言,兩者語言甚至可互相混合開發 開發上比 Java 更安全,例如可靜態檢測常見陷阱,例如 NullPointerException 檢測 開發上比 Java 更簡潔、高閱讀性,例如 Scope Function、Extension function、Lambda 等語法特性 接下來,為了幫助每位閱讀朋友能夠順利開發 Kotlin 程式,將一步步帶領大家進入 Kotlin 開發環境,而個人目前在開發 Kotlin 專案都是使用 JetBrains 的 Intellij 編輯器進行開發,其編輯器已內建許多工具方便開發者進行開發,例如重構、測試、版控等工具,推薦可以使用此編輯器進行開發。 但礙於可能有許多撰寫 Java 的朋友已經習慣使用 Eclipse 開發專案,下面後半段也會附上如何在 Eclipse 開發 Kotlin 專案,給大家進行參考與選用。若有朋友暫時不想下載編輯器,僅想先學習基礎 Kotlin 語法,也可先使用 Kotlin 官方提供的 Playground 頁面體驗 Kotlin 開發,例如下圖在頁面嘗試印出「Hello Kotlin」字串程式: 範例測試連結 下面內容將會逐步介紹 Intellij 編輯器如何安裝並建立專案執行 Kotlin 程式,後面也會介紹我們如何利用 Eclipse 進行Kotlin專案開發: 1. Intellij 安裝與執行 Kotlin 專案(1)我們可至 Intellij 頁面進行下載 Community 免費版: IntelliJ IDEA Community 版本下載:https://www.jetbrains.com/idea/download/index.html (2)下載並安裝完畢後,可開啟 Intellij 建置新專案(Create New Project) (3) 選擇左邊選單「Kotlin」專案,再選擇「JVM|IDEA」,最後點擊「Next」即可 (4)輸入專案名稱(Project Name )與專案存放路徑 (5) 在 src 資料夾下新增 Kotlin 程式「hello.kt」 註:Kotlin 程式檔案是以「 .kt 」副檔名作為結尾,如:hello.kt 、app.kt (6) 輸入程式片段並執行左邊的綠色執行按鈕,即可得到下方「Hello Kotlin」結果,這邊要記得 Kotlin 在每行程式結尾不需要加上分號「;」 2. Eclipse 安裝與執行 Kotlin 專案(1) 開啟 Eclipse 後,點擊工作列上的「Help」→「Eclipse Marketplace…」 (2) 在 Find 欄位上輸入 Kotlin 進行搜尋,點擊 Kotlin Plugin for Eclipse 的 Install 進行安裝 (3) 開啟 Window Perspective 設定 (4) 切換 Perspective 至 Kotlin 進行開發 (5) 新增 Kotlin 專案與設定專案名稱、存放路徑 (6) 新增 Kotlin 程式「hello.kt」,並撰寫main程式輸出「Hello, Kotlin」 Reference 【官方】Kotlin 官方網站","categories":[{"name":"2020-IThome鐵人賽","slug":"2020-IThome鐵人賽","permalink":"http://devinwangg.github.io/categories/2020-IThome%E9%90%B5%E4%BA%BA%E8%B3%BD/"}],"tags":[{"name":"Kotlin","slug":"Kotlin","permalink":"http://devinwangg.github.io/tags/Kotlin/"}]},{"title":"[Day 01] 遠征 Kotlin × Spring Boot 前言","slug":"ironman-2020-01","date":"2020-10-11T02:10:10.000Z","updated":"2023-09-03T08:46:08.944Z","comments":true,"path":"2020/10/11/ironman-2020-01/","link":"","permalink":"http://devinwangg.github.io/2020/10/11/ironman-2020-01/","excerpt":"","text":"主題選定這次鐵人賽文章主要是想要介紹 Kotlin 運用在 Spring Boot 開發上,其實當時在思考主題時猶豫了很久,因為 JetBrains 有推出 Kotlin Web 框架-Ktor,個人對於這個框架也有興趣,但礙於所待的公司在技術生態系上還是偏向 Spring / Spring Boot 架構,最後決定還是先以 Kotlin 在 Spring Boot 框架當作這次分享內容。 接觸起源個人在過去開發上,其實曾開發過 Java、C#、Swift、Python、JavaScript 等語言的專案開發,因 Kotlin 在語法上其實融合了 Scala、Groovy、Python、Swift 等語言特性,若閱讀者曾接觸過提及的語言,上手 Kotlin 應會容易許多,Kotlin 的誕生也彌補了許多過去使用 Java 進行開發時常出現的缺點。 而第一次接觸 Kotlin 其實是在 Android 專案開發上,但因為個人目前開發所接觸的專案幾乎還是以 Web 居多,那時候就沒有花太多時間深入使用,後來也是在 2019 年在 Youtube 看到 KotlinConf 2018 Nicolas Frankel 的分享內容《Kotlin and Spring Boot, a Match Made in Heaven 》,才對於 Kotlin 重新點燃了興趣,喜愛 Kotlin 在 Spring Boot 開發時的簡潔與特性,就此開始對於 Kotlin 深入研究。 系列規劃本系列文章目前安排會先介紹 Kotlin 程式語言基本語法,再介紹 Spring Boot 框架與一些後端開發技巧,相信閱讀者只要具備基礎程式語言能力就可以加入一起學習,閱讀的朋友們如果有地方說明不清楚的,希望再留言告知。 系列預期目標希望讓閱讀者能夠認識 Kotlin 基本程式語法與特性,能夠因為此系列學習到 Spring Boot Web 開發,期望降低 Kotlin 與 Spring Boot 學習門檻,期待大家一起深入探索 Web 開發世界。 章節導覽[Day 01] 遠征 Kotlin × Spring Boot 前言[Day 02] 遠征預備 Kotlin × 開發環境介紹[Day 03] 遠征 Kotlin × 變數型別[Day 04] 遠征 Kotlin × 流程控制[Day 05] 遠征 Kotlin × 函數介紹[Day 06] 遠征 Kotlin × Collections 介紹[Day 07] 遠征 Kotlin × 類別與物件[Day 08] 遠征 Kotlin × 類別繼承、介面、抽象[Day 09] 遠征 Kotlin × 例外處理[Day 10] 遠征 Kotlin × 泛型 Generic[Day 11] 遠征 Kotlin × 函數式程式設計[Day 12] 遠征 Kotlin × 進入 Spring Boot 世界[Day 13] 遠征 Kotlin × 建置 Spring Boot 專案[Day 14] 遠征 Kotlin × Spring Boot 專案配置介紹[Day 15] 遠征 Kotlin × Spring Boot 設定資料庫與匯入初始資料[Day 16] 遠征 Kotlin × 使用 Spring Data JPA 操作資料庫 (1)[Day 17] 遠征 Kotlin × 使用 Spring Data JPA 操作資料庫 (2)[Day 18] 遠征 Kotlin × Spring Boot 使用 RESTful API (1)[Day 19] 遠征 Kotlin × Spring Boot 使用 RESTful API (2)[Day 20] 遠征 Kotlin × Spring Boot 使用分層架構 Layered Architecture[Day 21] 遠征 Kotlin × Spring Boot 爬蟲實戰教學[Day 22] 遠征 Kotlin × Spring Boot 介紹單元測試 (1)[Day 23] 遠征 Kotlin × Spring Boot 介紹單元測試 (2)[Day 24] 遠征 Kotlin × Spring Boot 介紹 Template Engine (1)[Day 25] 遠征 Kotlin × Spring Boot 介紹 Template Engine (2)[Day 26] 遠征 Kotlin × Spring Boot 部署網站到 Heroku[Day 27] 遠征 Kotlin × Spring Boot 介紹 Spring AOP 機制[Day 28] 遠征 Kotlin × Spring Boot 介紹 WebSocket 實作[Day 29] 遠征 Kotlin × Spring Boot 介紹多資料庫連線配置[Day 30] 遠征 Kotlin × Spring Boot 完賽心得分享 學習資源此次系列主題會參考一些學習資源,這邊也將資源列出,供有興趣的朋友也可以前往學習: 【網站】Kotlin 官方文件 【網站】Kotlin 讀書會 【書籍】Kotlin 權威 2.0:Android 專家養成術 【書籍】Kotlin Cookbook 【文章】v1.4 發佈在即,誕生近 10 年的 Kotlin 取代 Java 了嗎?","categories":[{"name":"2020-IThome鐵人賽","slug":"2020-IThome鐵人賽","permalink":"http://devinwangg.github.io/categories/2020-IThome%E9%90%B5%E4%BA%BA%E8%B3%BD/"}],"tags":[{"name":"Kotlin","slug":"Kotlin","permalink":"http://devinwangg.github.io/tags/Kotlin/"},{"name":"Spring Boot","slug":"Spring-Boot","permalink":"http://devinwangg.github.io/tags/Spring-Boot/"}]}],"categories":[{"name":"2020-IThome鐵人賽","slug":"2020-IThome鐵人賽","permalink":"http://devinwangg.github.io/categories/2020-IThome%E9%90%B5%E4%BA%BA%E8%B3%BD/"}],"tags":[{"name":"Kotlin","slug":"Kotlin","permalink":"http://devinwangg.github.io/tags/Kotlin/"},{"name":"Spring Boot","slug":"Spring-Boot","permalink":"http://devinwangg.github.io/tags/Spring-Boot/"}]}