Spring boot 學習筆記 – 使用 Kotlin 做後端開發

學了這麼厲害的 Kotlin 可不可以拿它來開發後端(Backend),答案是當然可以的!
我們使用 Kotlin 跟 Gradle 來做開發
先做一個非常簡單的 API 再來慢慢進階

前端?後端?前台?後台?有差嗎?

在這邊簡單解釋一下 Web 技術這些詞彙的定義,大家也常常搞錯。

  • 前端 (Frontend):由後端 (Backend) 收到資料,透過瀏覽器渲染出來跟使用者互動的介面 (UI) 與程式都稱前端。

  • 後端 (Backend):也就是伺服器 (Server) 的部分,專門處理由使用者的要求,提供對應資料的程式。

  • 前台:大家比較通俗的講法,使用者看得到的操作得到的區域,如同舞台一般。

  • 後台:大家比較通俗的講法,通常是指管理者介面,如同舞台的後台。

那前台、後台跟前端、後端有關係嗎?
答案是:沒有什麼關係。
前台需要有前端(介面),也需要後端(伺服器程式)幫忙,後台也是。

工作上可以細分成:

  • 前台的前端
  • 前台的後端
  • 後台的前端
  • 後台的後端

分別有前端工程師與後端工程師負責開發。
今天的主題是後端開發的主題,所以沒有介面只有資料。

學習目標

  • 手把手製作一個 REST API
  • 熟悉 Spring boot 的 Annotation 寫法
  • 調整 404 頁面

準備開發環境

這邊有三種做法,好壞都不一

  • 直接推薦:InteliJ IDEA Ultimate 付費版
  • Visual studio code 外掛 Spring Initializr plug-ins
  • Spring Tools 4 for Eclipse (前身為 STS3)

💡 小提醒:這個需要付費版 InteliJ IDEA Ultimate, 免費的 Community 會無法正確 Compile,小弟已經親身試過了。

小弟建議使用 InteliJ,因為他的優異程式碼提示、搜尋與重構等功能好過其他的 IDE 編輯器

小提醒:因為 InteliJ IDEA 有可能會小改版,介面可能會稍微長的不太一樣,如果真有找不到選項、或者文章失效歡迎聯繫小弟我。

開新專案

新增SpringBoot專案

New Project,選擇 Spring Initializr

  • Group: <自己填>
  • Artifact: <自己填>
  • Type: Gradle Project (Generate a Gradle based project archive.)
  • Language: Kotlin
  • Packaging: Jar
  • Java Version: 11 (選更新的版本也可以)
  • Version: <自己填>
  • Name: <自己填>
  • Description: <自己填>
  • Package: <自己填>

然後按「Next」。

新增SpringBoot專案

這邊選擇二個:

  • Web > Spring Web
  • Developer Tools > Spring Boot DevTools

不選也沒關係,後面可以自行新增。
按下「Finish」完成建立專案。

如果直接新增完跑的話,Log 大概會長這樣。

瀏覽會長這樣:

資料夾結構

如果都是預設值的話,會看到以下資料夾結構:

  • src/main/kotlin/\<packageName>/
    主要程式碼的目錄
  • src/test/kotlin/\<packageName>/
    測試程式碼的目錄
  • build.gradle.kts
    專案的 gradle 檔案
  • src/main/resources/static
    存放一些靜態資源(例如:JavaScript、css、圖片…等)
  • src/main/resources/templates
    存放一些樣板檔案
  • src/main/resources/application.properties
    參數設定配置檔,存放一些資料連線資訊、雜項設定等等

修改啟動的連接埠 (Port)

找到 application.properties 檔案,加入這句即可

server.port=8086

啟動 Port 就改成 8086 了

其他參數請見:
https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html

增加 Dependencies

打開 build.gradle.kts 找到 dependencies { } 的區塊

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    developmentOnly("org.springframework.boot:spring-boot-devtools")
}

即是 Spring Web 跟 Spring Boot DevTools


觀察檔案

DemoApplication.kt (如果沒改名字的話)
或者有 @SpringBootApplication 標明字樣的檔案
即主要程式,程式的進入點
可以找到一個

fun main(args: Array<String>) {
    runApplication<DemoApplication>(*args)
}

可以找到 main 即程式進入點


寫第一個 Controller 寫一隻 API

著手來寫第一個 Controller

定義資料 Model

我們先定義幾個的 model class,

GeneralMessageResponse 是用來顯示範例 API 資料用的

GeneralMessageResponse.kt

package com.example.demo.model

data class GeneralMessageResponse(val message:String)

就這樣二句,就可以定義好資料 Model。

GeneralErrorResponse 用來顯示錯誤訊息的

GeneralErrorResponse.kt

package com.example.demo.model

data class GeneralErrorResponse(var message:String)

寫一個 Controller

製作一個 class 名叫 HelloController 使用 @RestController 標示,內容如下:

HelloController.kt

package com.example.demo

import com.example.demo.model.GeneralMessageResponse
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class HelloController{

    @RequestMapping("/")
    fun hello(): GeneralMessageResponse {
        return GeneralMessageResponse("Hello, world!")
    }
}

做一個方法 (Method),方法名稱可以自訂,標上 @RequestMapping 標示,回傳一個物件
即回傳的資料

@RequestMapping 上面需標明綁定的 API 路徑(不可重複)
可綁定何種傳送方式 (GET, POST, PUT, DELETE)

可編譯此專案,並瀏覽之
預設會是 http://localhost:8080/

會得到以下結果:

{"message":"Hello, world!"}

新增另外一個 Method

在剛剛的 HelloController 在新增一個方法 (Method),名叫 getHello()
程式碼片段如下:

@RequestMapping("/hello", method = [RequestMethod.GET])
fun getHello(@RequestParam("name", defaultValue = "World") name: String): GeneralMessageResponse {
    return GeneralMessageResponse("Hello, $name")
}

比起剛剛的 Method 多了參數,只接受 GET 方法(使用其他方式存取會錯誤,因為沒定義)
不標明等於接收所有的方式

參數部分使用 @RequestParam 修飾字標明,標上參數名稱,
defaultValue 如果沒填資料的話的預設值為何
然後像剛才一樣,回傳一個 JSON 物件

方式+名稱 的組合不可重複,但可接受同名但不同的方式的 Method
例如這樣是合法的:

@RequestMapping("/hello")
fun hello(): GeneralMessageResponse {
    return GeneralMessageResponse("Hello, world (from default method)!")
}

@RequestMapping("/hello", method = [RequestMethod.GET])
fun getHello(@RequestParam("name", defaultValue = "World") name: String): GeneralMessageResponse {
    return GeneralMessageResponse("Hello, $name (from get)")
}

@RequestMapping("/hello", method = [RequestMethod.POST])
fun postHello(@RequestParam("name", defaultValue = "World") name: String): GeneralMessageResponse {
    return GeneralMessageResponse("Hello, $name (from post)")
}

後面 Restful API 的時候會再提到。


錯誤處理

錯誤處理有二個部分,不太一樣:

  • 執行時出現 Exception 時,可自訂回傳的錯誤訊息
  • 當存取一個不存在的 API 時,預設回的訊息( 404 Not found 頁面)

處理 Exception 錯誤

先新增一個 GeneralErrorResponse 做為錯誤訊息的回應

GeneralErrorResponse.kt

package com.example.demo.model

import com.fasterxml.jackson.annotation.JsonInclude

@JsonInclude(JsonInclude.Include.NON_NULL)
data class GeneralErrorResponse(
    val message:String, 
    val requestUrl: String? = null, 
    val errorStackStace: String? = null
)

新增一個 ThrowableExtension.kt 新增一個 stackTraceAsString() 拿到 stackTrace 的字串

ThrowableExtension.kt

package com.example.demo.utils

import java.io.PrintWriter
import java.io.StringWriter

fun Throwable.stackTraceAsString(): String {
    val errors = StringWriter()
    this.printStackTrace(PrintWriter(errors))
    return errors.toString()
}

最後新增 GlobalExceptionHandler 用來處理全域的 API 錯誤問題

GlobalExceptionHandler.kt

package com.example.demo

import com.example.demo.model.GeneralErrorResponse
import com.example.demo.utils.stackTraceAsString
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.ResponseBody
import org.springframework.web.bind.annotation.ResponseStatus
import javax.servlet.http.HttpServletRequest

@ControllerAdvice
class GlobalExceptionHandler {
    @ExceptionHandler(Throwable::class)
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    @ResponseBody
    fun handleException(request: HttpServletRequest, error: Exception): GeneralErrorResponse {
        return GeneralErrorResponse("Error occurred!", request.requestURL.toString(), error.stackTraceAsString())
    }
}

這邊使用了 @ControllerAdvice@ExceptionHandler 標示
當然不同情境有不同的用法,這邊示範回傳一個 JSON

最後在 Controller 新增一個測試用的 Exception 方法

@RequestMapping("/testErr")
fun testErr() {
    throw Exception("test exception")
}

然後瀏覽測試之。

處理 404 Not found 頁面

這裡處理當存取一個不存在的 API 時,預設回的資料。
我們再新增一個 Controller 名叫 MyErrorController 實作 ErrorController 介面,
範例如下:

MyErrorController.kt

package com.example.demo

import com.example.demo.model.GeneralErrorResponse
import org.springframework.boot.web.servlet.error.ErrorController
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class MyErrorController : ErrorController {
    override fun getErrorPath(): String {
        return "/error"
    }

    @RequestMapping("/error")
    fun handleError(): GeneralErrorResponse {
        return GeneralErrorResponse("404 Error")
    }
}

這裡需實作 ErrorController 介面,定義錯誤發生的預設 Path 將走去哪裡
這裡使用 "/error"
然後綁定前述的值,回應一個預設訊息。


參考資料

[HTTP相關] 解析HTTP封包,瞭解Session ID和3-way handshake(三向交握)

這就是我們一直在用,不管是手機還是瀏覽器
在這虛擬化技術,雲端科技,Web 2.0等等蓬勃發展的年代
只要是上網,一定完全脫離不了Http的關係

對於網路,這是很基底的概念
想要讓技術層面更進一步
可以從日常接觸到的概念開始
讀通了,就很有用

—————————————————————–(以上都是廢話)—————————————————————

好吧,說真的我不大會教網路這種東西
可能有修過網路相關課程的人,可能會比較懂一點

我盡量從觀察到實際結果,配合網路概念解釋一遍 

 

HTTP封包

我先放這次抓HTTP封包的截圖,再來解釋可能會比較有概念一點

2011-10-21 00 09 30.png  

 

這次所使用的軟體是Wireshark,是個抓封包的軟體

http://www.wireshark.org/download.html

裝好之後選擇網路介面然後就可以看到類似像上圖的東西了喔
我想你一定是在看我的文章,你看你的視窗上面有pixnet耶

 


 

講起網路,不免還是從OSI七層開始講起

這個部份我覺得wiki分類的真好  http://en.wikipedia.org/wiki/OSI_model

2011-10-21 00 17 37.png    

有興趣可以點開wiki的連結看,裡面有對應的文章

天音:這時候要像老師一樣諄諄教誨 (雖然我很不想~>"<)

 

OSI七層協定從下到上分別為

實體層(Physical Layer),資料鏈結層(Data Link Layer),網路層(Network Layer),傳輸層(Transport Layer),會談層(Session Layer),展現層(Presentation Layer),應用層(Application Layer)

裡面藍字的內容都是現有協定名字

wiki的目錄,一堆藍字都沒看過沒關係,在這裡,我們只要管  TCP  HTTP  在哪一層就行了


 

對應到我們抓到的HTTP封包

Screen Shot 2012-03-13 at 下午5.36.47  

第一行,是個概覽,這個封包總共收到幾個Frame(訊框)等等

第二行,就是我們收到的Frame(訊框),可以看到我們用Ethernet協定傳送,裡面記錄著最後傳和目標傳到的Mac位址….等等

第三行,就是TCP/IP中的IP協定(Internet Protocol),紀錄著來源和目標的IP等等資訊

第四行,Transmission Control Protocol縮寫就是TCP協定啦,HTTP連線也是基於TCP協定的,可以看到來源和目的的port(連接埠)號

第五行,就是HTTP協定(Hypertext Transfer Protocol)啦

這裡有展開,可以看到完整的HTTP標頭,一個HTTP連線原來附著這麼多的資訊,也是等下要講的重點

第六行,沒甚麼特別的,就只是網頁原始碼

 


封包和傳送原理

封包大概是這樣子的

osi1  

從程式的眼光來看好了,軟體要送個資料到另一台電腦

除了  實體層(Physical Layer) 之外  

會依序從上往下包

你可以想成要送人家禮物要包裝

或想成郵局要送郵務,要在包裹上貼很多標籤

或是辦公室的便利貼,貼一些備註訊息

 

每往下一層就加一些東西上去,到別人的電腦也是這樣,一層層拿掉標籤

最後得到資料

 

HTTP(應用層)也是如此,HTTP是基於TCP(傳輸層)的

在連線的時候也是如此

要先有TCP先連線才會可以跑HTTP的協定

 

 

不要問我為什麼要分那麼多層,網路這東西是有歷史的

很多東西到現今也還在變化 (但這個概念短時間已經是不會變了)

 


先解釋TCP

看到一堆紅線了嗎

分成前三組和後三組

前三組,就是出名的三向交握 (3-way handshake 也有人翻譯三次握手)

(天音:甚麼三向交握都看攏無)

 

看英文比中文意思比較準,意思就是 握手寒暄 啦

 

 

請設想一個情況,想你打skype給朋友
剛接起來網路電話的時候有沒有遇到這樣的情況

[SYN]              我: 哈嚕, 有沒有聽到聲音?

[SYN, ACK]      A: 有聽到,那我呢?有沒有聽到聲音

[ACK]              我: 有滴~

(…….然後開始聊天,喔不是,是傳資料=  =")

 

沒錯,三向交握就這麼簡單

標籤看起來很複雜而已

 

另外這組

[FIN, ACK]

[ACK]

是四向交握協定Four-way Handshake,用來關閉連線的

引用一下文章的內容

1. (B) –> ACK/FIN –> (A)

2. (B) <–     ACK    <– (A)

3. (B) <– ACK/FIN <– (A)

4. (B) –>    ACK     –> (A)

理當要有四個,為何只有二個?我想是Keep-Alive的關係

所以連線沒有斷,保持連線,若有其他的要求就可以馬上繼續連線
而不用重新建立連線


HTTP標頭

HTTP標頭裡面有很多資訊,像是Server的資訊,很明顯是跟Apache做連線

這裡也很清楚看到中間有Set-Coookie: 和PHPSESSID就是Session的ID啦

當Server使用Session時

雖然Session的資料是放在Server上沒錯〈Client看不到〉

為了分辨這個是那個Client連來的
所以會放一個Session ID給Client

(像是你去寄放物品的櫃台寄放東西,櫃台會交給你一把鑰匙一樣)
用鑰匙認顧客

而且這個Session ID會隨著你的連線,在下次要求的時候,放在標頭裡面
一起發送給Server

 

而在網頁設計當中,Session往往都被當成會員登入等依據

當你在清理Cookie的時候,Session ID也一起被你清掉了
也理所當然的被登出了

 

但Session使用上有些限制

  1.  他只能在同一個網域名字下使用,不能跨網域

  2.  Client端要開啟Cookie (當然這項,一般的瀏覽器通常都是開啟的)

  3.  通常Session和Cookie一樣,有Expires(失效時間)的

所以Facebook的認證上考慮到這些限制,才會採用OAuth 2.0

就是模擬Session的原理,只是把Session ID變成token的方式,用HTTP最簡單的GET方式

掛在網址串上面,以做身分的識別

(有關OAuth2.0相關的東西下次再講)


所以Hsuan網友在Android建立HTTP連線裡面

http://j796160836.pixnet.net/blog/post/28994669

問的問題:

 

我在1.php 寫 session 
在2.php echo出session的值
這是沒問題的

但是如果用android端做測試的話
會抓不到session的值
請問這個跟android端有關係嗎??

所以在程式中,Session ID並沒有存下來

 


參考資料:

TCP: SYN ACK FIN RST PSH URG 詳解

https://sites.google.com/site/cimpleteam/articles/tcpsynackfinrstpshurgxiangjie

TCP/IP 概論

http://www2.meps.tp.edu.tw/documents/memo/TCP%EF%BC%8FIP%E6%A6%82%E8%AB%96/index.htm