# Java - Spring Boot Notes
## New Project
* JDK 跟 Java版本須保持一致
* Spring Boot版本若為2開頭,則JDK需要選擇舊版(11),若為3開頭則可選擇JDK17
* Spring Boot 3多為Library更新,後續再提
---
## Change Version
Edit pom.xml file
```
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
```
Edit "version" will do the trick
---
## @SpringBootApplication
* @ is an annotation, meaning adding a new function or feature to the class
* classes with a @SpringBootApplication means that the class can be run under Spring Boot
---
## First Spring Boot Application
MyController
```java
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MyController {
@RequestMapping("/test")
public String test() {
System.out.println("Hi");
return "Hello, world!";
}
}
```
Then open a browser, type in "http://localhost:8080/test" in URL, and Hello world will appear!
---
## Spring IoC & Dependency Injection(DI)
### 1. IoC & DI
**IoC = Inversion of Control 控制反轉**
**意義:將object的控制權交給了外部的Spring容器來管理**
以往的做法是需要在每一個class內單獨new一個新的object來使用,但如果要修改這個object就很麻煩(需要一個一個進到class裡面做修改)。如果使用IoC,Spring會直接在容器中建立object,各個class如果需要用到的時候再去容器內取用即可。
優點:
1. Loose coupling between classes (類間的鬆耦合,降低類之間的關聯性)
2. Lifecycle Management
3. More testable
實作:
需要用到"**@Component**"這個註解
* 用法:只能用在class上
* 用途:將該class變成由Spring容器所管理的object
i.e. 只要有加上@Component,每當Spring被啟動時,系統就會自動在容器內建立該class的object,**這些object又被稱為bean**
Sample
Interface
```java
public interface Printer {
void print(String message);
}
```
Interface implementation
```java
@Component
public class HpPrinter implements Printer {
@Override
public void print(String message) {
System.out.println("HP Printer: " + message);
}
}
```
A class uses this object
```java
@Component
public class Teacher {
// 不用再new object
@Autowired
private Printer printer;
public void teach() {
printer.print("I am a teacher");
}
}
```
Container內就會自動生成一個bean hpPrinter
(很像是自動執行Printer printer = new HpPrinter(); 這行程式)
重點:
1. 要使用該物件的class,也要加上@Component的註解
2. 物件內的變數需要加上@Autowired的註解,稱為"Dependency Injection(DI 依賴注入)"
### 2. @Autowired & @Qualifier
@Autowired
* 用法:通常加在class變數上方
* 用途:根據**變數的類型**,去Spring容器中尋找有沒有符合類型的bean
* **變數類型盡量使用Interface(因為多型的關係,比較有彈性)**
這時候就會產生一個問題,如果在Spring容器中有兩個class實作了同一個Interface,那在注入的時候會注入哪一個bean呢?

要解決這個問題就需要使用"@Qualifier"
@Qualifier
* 用法:通常加在class變數上方,通常會跟@Autowired一起使用
* 用途:**指定要載入的bean的名字**
* Syntax: **@Qualifier("name of bean")**
* 需要注意的是這邊的bean的名字會是**class名稱,但第一個英文字母會是小寫**
* @Autowired跟@Qualifier沒有使用順序上的差異
Sample Code
```Java
@RestController
public class MyController {
@Autowired
// @Qualifier("canonPrinter")
@Qualifier("hpPrinter")
private Printer printer;
@RequestMapping("/test")
public String test() {
// System.out.println("Hi");
printer.Print("Hello, world!");
return "Hello, world!";
}
}
```
### 3. 創建bean的方法
1. 在class上面加上@Component(較常見)
2. 使用@Configuration + @Bean註解
@Configuration
* 用法:只能加在class上
* 用途:Spring中的**設定用的註解**,表示這個class是用來設定Spring用的
* class名稱不重要,Spring在意的是設定的內容(也就是這個class裡面的程式碼)
@Bean
* 用法:只能加在**帶有@Configuration**的class的**方法**上面(注意是加在"method"上)
* 用途:在Spring容器中創建一個bean
```java
@Configuration
public class MyConfiguration {
@Bean
public Printer myPrinter() {
return new HpPrinter();
}
}
```
### 4. Bean的初始化:@PostConstruct & InitializingBean
1. 使用@PostConstruct(較常見)
2. 實現InitializingBean interface的afterPropertiesSet()方法
@PostConstruct
* 在要初始化的那個bean的class當中建立一個初始化的方法,並加上@PostConstruct
* **必須為public**
* **返回值必須為void**
* 方法名稱可以隨便取
* **必須不帶參數**
Sample:
```java
@Component
public class HpPrinter implements Printer {
private int count;
@PostConstruct
public void initialize() {
count = 5;
}
@Override
public void Print(String message) {
count--;
System.out.println("HP Printer prints: " + message);
System.out.println("Times left: " + count);
}
}
```
@InitializingBean interface
* 該class要實現InitializingBean interface
* 改寫afterPropertiesSet()
* 因為要再implements一個interface,比較麻煩,所以不常用
**Notes: 一次使用一種方式初始化即可**
### 5. Life Cycle of Bean
基本的Bean生命週期
1. 啟動Spring Boot
2. 創建Bean (@Component | @Configuration + @Bean)
3. 初始化Bean (@PostConstruct | implements initializingBean interface)
4. 可以開始使用Bean
5. 運行成功
Notes:
1. 若在容器內有多個Bean,Spring Boot會等所有Bean都可以被使用之後才會開始運行,也就是說只要**有一個Bean創建失敗,Spring Boot就不會被啟動**
2. Spring會去處理Bean之間的依賴關係。流程:Controller -> 遇到需要用到新的Bean的情況 -> 創建另一個Bean -> 初始化 -> 注入 -> 初始化Controller。**因此要特別注意不要循環注入Bean(不要循環依賴)**
### 6. 讀取Spring Boot設定檔 - application.properties + @Value
存放位置:src/main/resources
最重要的設定檔:application.properties
* 用法:使用properties語法 **(key=value)**
* 用途:存放Spring Boot的設定值
* =兩邊不用space
* key的名字中可以帶有.,相當於中文的「的」
* comment: #
讀取方式:使用@Value
* 用法:加在Bean或是設定Spring用的class裡面的**變數**上
* 用途:讀取Spring Boot設定檔(application.properties)中key的值
* 語法:
```
無預設值的語法: @Value("${key}")
有預設值的語法: @Value("${key:value}")
-> 只有在找不到設定檔的key的情況下才會使用預設值
```
* 設定檔變數類型與class變數類型要一致
Sample:
application.properties
```properties
my.name=John
```
java class
```java
@Component
public class myBean {
@Value("${my.name}")
private String name;
}
```
---
## AOP - Aspect-Oriented Programming
AOP(Aspect-Oriented Programming) 切面導向程式設計
目的:**將各class裡面需要用到的共同邏輯獨立出來成一個「切面」,讓所有類別方法都可以使用到這一個共同邏輯**。例如:測量方法執行時間,產生log紀錄等等
### 1. @Aspect
* 用法:加在class上面 **(需要和@Component一起使用)**
(這個class一定要先成為一個bean才能變成切面)
* 用途:宣告這個class是一個切面
* @Aspect與@Component沒有加註順序問題
### 2. @Before
* 用法:加在切面class的方法上
* 用途:在切入點的方法 **執行前**執行
寫法:
```java
@Aspect
@Component
public class MyAspect {
@Before("execution(* com.example.demo.HpPrinter.*(..))")
public void before() {
System.out.println("I am before!!!");
}
}
```
說明:
1. @Before裡面的參數"execution...""是指我們指定的方法(**切入點**)
2. @Before註解的意思是在執行()裡面的方法之前先執行下面的方法
3. 下面的方法就是告訴方法的內容
換句話說,Spring在執行我們指定的方法之前,會先去執行System.out.println("I am before!!!");的這行程式
### 2. @After
* 用法:加在切面class的方法上
* 用途:在切入點的方法 **執行後**執行
其餘都跟@Before一樣
```java
@After("execution(* com.example.demo.HpPrinter.*(..))")
public void after() {
System.out.println("I am after!!!");
}
```
### 3. @Around
* 用法:加在切面class的方法上
* 用途:在切入點的方法 **執行前與後**執行(i.e 包住)
* 寫法比較複雜,不用記,用到再查即可
* Object obj = pjp.proceed();的意思就是指"執行插入方法"
```java
@Around("execution(* com.example.demo.HpPrinter.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("I am around before...");
Object obj = pjp.proceed();
System.out.println("I am around after...");
return obj;
}
```
利用@Around方法實現測量方法執行時間:
```java
@Around("execution(* com.example.demo.HpPrinter.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Date start = new Date();
Object obj = pjp.proceed();
Date end = new Date();
long time = end.getTime() - start.getTime();
System.out.println("Execution Time: " + time + "ms");
return obj;
}
```
切入點表達式與其意義:
execution(* com.example.demo.HpPrinter.*(..))代表
切入點為com.example.demo package裡面的HpPrinter Class內所有的方法
有用到再去查即可
使用AOP的步驟:
步驟:
1. 先在pom.xml的檔案中新增AOP dependency
```
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
```
2. 新增class
```java
@Aspect
@Component
public class MyAspect {
@Before("execution(* com.example.demo.HpPrinter.*(..))")
public void before() {
System.out.println("I am before!!!");
}
// ...
}
```
使用AOP的情境:
* 權限驗證 (實務用法:Spring Security)
* 統一的Exception處理 (實務用法:@ControllerAdvice)
* Log紀錄
---
## Spring MVC
### 1. HTTP協議
* 規定前後端之間資料的傳輸格式
* 可以分成request(請求)和response(回應)兩部分
#### Http Request
* http method(GET, POST...)
* url
* request header: 通用資訊
* request body: 請求參數
#### Http Response
* http status code (Spring Boot預設返回200 500,要自定義的話需要使用ResponseEntity,詳細後述)
* 1xx:資訊 (少用)
* **2xx:成功**
* 200 OK:請求成功
* 201 Created:請求成功並創建資源,通常用在POST的response
* 202 Accepted:請求已經接受,但尚未處理完成
* **3xx:重新導向**
* 301 Moved Permanently:永久重新導向,新的url通常放在response header的"Location"中回傳
* 302 Found:臨時重新導向。url也是放在response header的"Location"
* **4xx:前端請求錯誤**
* 400 Bad Request:前端請求的參數有誤(請求格式有問題等等)
* 401 Unauthorized:沒有通過身分驗證
* 403 Forbidden:請求被後端拒絕,通常是權限不足導致的
* 404 Not Found:網頁不存在
* **5xx:後端處理有問題**
* 500 Internal Server Error:後端處理錯誤(可能是有bug導致)
* 503 Service Unavailable:後端暫時無法處理請求
* 504 Gateway Timeout:請求超時
* response header: 通用資訊
* response body:**後端要回傳的數據**
### 2. URL Route
Spring Boot URL:
```
http://localhost:8080/test
```
* http: protocol
* localhost: domain name(域名)
* 8080: port
* /test: route
#### @RequestMapping("route")
* 用法:加在class或方法上,小括號裡填寫url路徑
* 用途:將url路徑對應到方法上
on method:
```java
@RequestMapping("/test")
public String test() {
return "Hello, world!";
}
```
```
the url is http://localhost:8080/test
```
on class:
```java
@RequestMapping("/detail")
@RestController
public class MyController {
@RequestMapping("/product")
public String product() {
return "The first one is apple, the second one is orange";
}
@RequestMapping("/user")
public String user() {
return "{user: John}";
}
}
```
```
> the urls are
http://localhost:8080/detail/product
http://localhost:8080/detail/user
```
Notes:
* **class上一定要加上@Controller或是@RestController才會生效。**(@Controller/@RestController的用途就是將class變成bean,並且讓這個class可以使用@RequestMapping)
* Do not include domain name in the url route
### 3. JSON
Format: Key-Value pair
Key must usd double quotation mark
Value:
- null
- int
- float
- String
- boolean
- Object
```jsonld
{
"id": 123,
"name": "John Wick",
"score": 95.5,
"graduated": true,
"age": null,
"courseList": ["Math", "English"],
"pet": {
"name": "blacky",
"age": 5
},
"petList": [
{
"name": "white",
"age": 3
}, {
"name": "brown",
"age": 4
}
]
}
```
#### Return JSON in Spring Boot
There are two ways:
- @RestController(較常用)
- @Controller(加在class) + @ResponseBody(加在方法)
@RestController
```java
@RestController
public class MyController {
@RequestMapping("/product")
public String product() {
return XXX;
}
}
```
@Controller + @ResponseBody
```java
@Controller
public class MyController {
@ResponseBody
@RequestMapping("/product")
public String product() {
return XXX;
}
}
```
Create JSON
1. Create a class with getter and setter
```java
public class Student {
String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
```
2. Instantiate the class
3. Return the object
```java
@RestController
public class MyController {
@RequestMapping("/user")
public Student user() {
Student student = new Student();
student.setName("John");
return student;
}
}
```
當RequestMapping所註解的方法是自定義類別時,Spring Boot會自動將class轉換成JSON(底層使用Jackson library)

### 4. 取得參數
- @RequestParam
- @RequestBody
- @RequestHeader
- @PathVariable
#### 4-1. @RequestParam
* 用法:只能加在方法的參數上
* 用途:取得url裡面的參數(query parameter,即?後面的部分)
* **多傳參數會被Spring Boot忽略,少傳參數會Bad Request**
* 一些設定:
* 自定義變數名稱:
@RequestParam(name = "testId") Integer id。
不常用,看看即可。
* **是否為必須(預設是true):**
@RequestParam(required = false)
* **提供預設值:(最常用)**
@RequestParam(defaultValue = "10")
使用預設值可以避免NullPointerException的問題
* 以上三者可同時使用,用逗號間隔
Example:
Single Parameter
url: http://localhost:8080/test?id=123
```java
@RestController
public class MyController {
@RequestMapping("/test")
public void test(@RequestParam Integer id) {
System.out.println("id: " + id);
}
}
```
If the query parameter is not Integer, then will result in 400 Bad Request.
Multiple Parameter
```java
@RestController
public class MyController {
@RequestMapping("/test")
public void test(@RequestParam Integer id,
@RequestParam String name) {
System.out.println("id: " + id);
System.out.println("name: " + name);
}
}
```
#### 4-2. @RequestBody
* 用法:只能加在方法的參數上
* 用途:取得request body裡面的參數(JSON to Java Object)
* Request Method記得用POST
步驟:
1. 建立一個class,其參數一一對應JSON Key
2. 將class產生的物件當參數傳入方法
Example:
request body(JSON)
```jsonld
{
"id": 123,
"name": "John"
}
```
corresponded Java class
```java
public class Student {
Integer id;
String name;
// Getter and Setter
}
```
restcontroller
```java
@RequestMapping("/test4")
public String test4(@RequestBody Student student) {
System.out.println("student id: " + student.getId());
System.out.println("student name: " + student.getName());
return "test4 Success";
```
* request body可以多傳參數,但接收的參數數量只看class裡面定義的變數數量(多的會被忽略)
* request body也可以少傳參數,沒有值得部分會變成null
#### 4-3. @RequestHeader
* 用法:只能加在方法的參數上
* 用途:取得request header裡面的參數
* 格式:key:value (冒號區隔)
* 設定:與@RequestParam用法相同
**Notes: RequestHeader通常會使用name="Content-Type"這種寫法**
```java
@RequestMapping("/test5")
public String test5(@RequestHeader(name = "Content-Type") String contentType) {
// code
return "xxx";
}
```
常見Header
| Request header | 意義 | 常見的值 |
| -------- | -------- | -------- |
| Content-Type | 用於表示Request Body的格式 |application/json (JSON), application/octet-stream (文件上傳), multipart/form-data (圖片上傳)|
| Authorization | 用於身分驗證 | |
#### 4-4. @PathVariable
* 用法:只能加在方法的參數上
* 用途:取得**url路徑的值**
Example:
```
http://localhost:8080/test6/123
```
這邊的```test6/123```就是url路徑,```123```就是路徑上面的值
寫法
```java
@RequestMapping("/test6/{id}")
public String test6(@PathVariable Integer id) {
// code
return "xxx";
}
```
**Notes: {}內的參數名稱一定要跟方法內的參數一致**
多參數寫法
```
http://localhost:8080/test6/123/john
```
```java
@RequestMapping("/test6/{id}/{name}")
public String test6(@PathVariable Integer id, @PathVariable String name) {
// code
return "xxx";
}
```
---
### 5. RESTful API Implementation
#### API
- 用工程師的方法去說明某個功能該如何使用
- 目的:寫清楚這個功能要怎麼使用
Example
API Documentation:
HTTP Request
GET /getProducts
| Name | Type | Description | Required?|
| -------- | -------- | -------- |--------|
| size | Integer | xxx |yes|
| search | String | xxx |no|
Corresponding Code:
```java
@RequestMapping("/getProducts")
public Product getProduct(@RequestParams Integer size,
@RequestParams(required = false) String search) {
//...
}
```
#### RESTful
- 一種設計風格,目的在簡化溝通成本
- REST風格:
1. 使用http method表示動作
| http method | DB Manipulation |
| -------- | -------- |
| POST | Create |
| GET | Read |
| PUT | Update |
| DELETE | Delet |
2. 使用URL Route來描述資源之間的階層關係
**Keyword: @PathVariable**
| http method + url | Purpose |
| -------- | -------- |
| GET /users | Get all users info |
| GET /users/123 | Get user info where id=123 |
| GET /users/123/article | Get articles from user id=123 |
| GET /users/123/article/456 | Get no.456 article from user id=123 |
3. response body返回JSON or XML
**Keyword: @RestController**
#### 實作
New Class
```java
pblic class Student {
Integer id;
String name;
// Create Getter and Setter
}
```
API Controller
1. Create controller class
```java
@RestController
public class StudentController {
}
```
2. Limit http request method
These two codes are the same
```java
@RequestMapping(value = "/students", method = RequestMethod.POST)
```
```java
// This is a more common way
@PostMapping("/students")
```
Other http methods' syntax are identical
3. Implement
Notes:
* When designing POST/PUT method, values in url and in request body are both used, so we have to use @PathVariable & @RequestBody to get those values
```java
@RestController
public class StudentController {
@PostMapping("/students")
public String create(@RequestBody Student student) {
return "Execute Create in DB";
}
@GetMapping("/students/{studentId}")
public String read(@PathVariable Integer studentId) {
return "Execute Read in DB";
}
@PutMapping("/students/{studentId}")
public String update(@PathVariable Integer studentId,
@RequestBody Student student) {
return "Execute Update in DB";
}
@DeleteMapping("/students/{studentId}")
public String delete(@PathVariable Integer studentId) {
return "Execute Delete in DB";
}
}
```
### 6. Value Validation
- if-else statement (not efficient)
- validation annotation
1. Add dependency **(for spring boot version >2.3)**
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
```
2. Add @Valid on parameter
```java
@PostMapping("/students")
public String create(@RequestBody @Valid Student student) {
return "Execute Create in DB";
}
```
3. Add @NotNUll on class attribute
```java
public class Student {
@NotNull
Integer id;
//...
}
```
- Some Useful Annotation
* @NotNull
* @NotBlank: used on Strings
* @NotEmpty: used on collections(lists, sets, maps), size should > 0
* @Min(value)
* @Max(value)
**Notes:**
- 使用@RequestBody時,要在該參數加上@Valid註解,才能讓class裡面的驗證請求參數生效
- 使用@RequestParam, @RequestHeader, @PathVariable的時候,**則需要在Controller上面加上@Validated**才能讓驗證請求生效
```java
@Validated
@RestController
public class StudentController {
@PostMapping("/students")
public String create(@RequestBody @Valid Student student) {
return "Execute Create in DB";
}
@GetMapping("/students/{studentId}")
public String read(@PathVariable Integer studentId) {
return "Execute Read in DB";
}
@PutMapping("/students/{studentId}")
public String update(@PathVariable @Min(100) Integer studentId,
@RequestBody Student student) {
return "Execute Update in DB";
}
@DeleteMapping("/students/{studentId}")
public String delete(@PathVariable Integer studentId) {
return "Execute Delete in DB";
}
}
```
### 7. ResponseEntity<?>
* 用法:作為方法的返回類型
* 用途:自定義回傳的http response細節
* <?> 為泛型
```java
@RestController
public class MyController {
@RequestMapping("/test1")
public ResponseEntity<String> test1() {
return ResponseEntity.status(HttpStatus.ACCEPTED).body("HELLO WORLD!");
}
}
```
.body()傳入的參數type要跟泛型定義的type一致
常用寫法:把對應table的java class以物件的方式傳入body
.build(): 可以創建ResponseEntity物件
```java
@GetMapping("/products/{productId}")
public ResponseEntity<Product> getProduct(@PathVariable Integer productId) {
// Get product from service
Product product = productService.getProductById(productId);
// Check null
if (product != null) {
return ResponseEntity.status(HttpStatus.OK).body(product);
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
}
```
### 8. Controller層統一的Exception處理
利用 **@ControllerAdvice + @ExceptionHandler** 來自定義每個Exception所返回的http response
#### @ControllerAdvice
- 用法:只能加在class上
- 用途:將這個class變成一個bean,並且可以在內部使用@ExceptionHandler
#### @ExceptionHandler
- 用法:只能加在方法上
- 用途:去cath所有Exception
1. Create a new class
2. Add @ControllerAdvice on the class
3. Write a handle function with parameter of Exception class
4. Add @ExceptionHandler
5. Return customized HTTP response
Controller class
```java
@RestController
public class MyController {
@RequestMapping("/test1")
public String test1() {
throw new RuntimeException("test1 error");
}
}
```
Exception Handling class
```java
@ControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<String> handle(RuntimeException exception) {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body("RuntimeException: " + exception.getMessage());
}
}
```
**For a more generalized handling for multiple exceptions, we just need to define the "super class" of those exceptions**
```java
@ControllerAdvice
public class MyExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handle(Exception exception) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body("Exception: " + exception.getMessage());
}
}
```
### 9. Interceptor 攔截器
攔截器的作用:
1. 擋下http request並檢查
2. 檢查無誤後,允許該request進入Controller,並執行方法
3. 若檢查發現有問題,就會直接返回錯誤給前端。此時Spring boot就不會執行方法
Steps:
Create an Interceptor class
```java
@Component
public class MyInterceptor implements HandlerInterceptor {
}
```
Override prehandler method
```java
@Component
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("Execute prehandle method in MyInterceptor");
return true;
}
}
```
Create a Config, and inject interceptor object
```java
@Configuration
public class MyConfig implements WebMvcConfigurer {
@Autowired
private MyInterceptor myInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(myInterceptor).addPathPatterns("/**");
}
}
```
---
## Spring JDBC
### 1. Connect database in Spring boot
**Add 2 dependencies in pom.xml**
This will allow Spring boot to use Spring JDBC
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
```
This will allow Spring to connect to MySQL
https://mvnrepository.com/artifact/mysql/mysql-connector-java
```xml
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
```
Add connection information into application.properties file (Configuring db settings)
```
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/myjdbc?serverTimezone=Asia/Taipei&characterEncoding=utf-8
spring.datasource.username=....
spring.datasource.password=....
```
Notes:
* myjdbc -> database to be used
* serverTimezone -> Timezone you're in
### 2. Update
- Java Bean "NamedParameterJdbcTemplate" (instantiation is done automatically by Spring)
- need @Autowired
Syntax
```java
update(String sql, Map<String, Object> map);
```
sql: SQL Syntax
map: Set value to the variable in SQL syntax
update() includes INSERT, DELETE, UPDATE
**Insert Example**
```java
@RestController
public class StudentController {
@Autowired
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
@PostMapping("/students")
public String insert(@RequestBody Student student){
String sql = "INSERT INTO student (id, name) VALUE (:studentId, :studentName)";
Map<String, Object> map = new HashMap<>();
map.put("studentId", student.getId());
map.put("studnetName", student.getName());
namedParameterJdbcTemplate.update(sql, map);
return "INSERT successfully";
}
}
```
Notes:
- When we post the ```/students``` API, data will be inserted into database danamically
- Be sure to create a ```Student``` class so that variables inside @RequestBody could be received
**Delete Example**
```java
@DeleteMapping("/students/{studentId}")
public String delete(@PathVariable Integer studentId){
String sql = "DELETE FROM student WHERE id = :studentId";
Map<String, Object> map = new HashMap<>();
map.put("studentId", studentId);
namedParameterJdbcTemplate.update(sql, map);
return "DELETE successfully";
}
```
### 3. Advanced Update
#### 3.1. 如何知道**Auto_increment**的值為何
使用```KeyHolder keyHolder = new GeneratedKeyHolder();```
```java
@PostMapping("/students")
public String insert(@RequestBody Student student) {
String sql = "INSERT INTO student (name) VALUE (:studentName)";
Map<String, Object> map = new HashMap<>();
map.put("studentName", student.getName());
KeyHolder keyHolder = new GeneratedKeyHolder();
namedParameterJdbcTemplate.update(sql, new MapSqlParameterSource(map), keyHolder);
int id = keyHolder.getKey().intValue();
return "INSERT successfully" + "mysql generated id:" + id;
}
```
**Notes: 在```int id = keyHolder.getKey().intValue();```的地方,要根據table的類型來決定用哪個方法,如果是int就是.intValue()**
#### 3.2. 大量插入數據的問題
使用```batchUpdate()```
```java
@PostMapping("/students/batch")
public String insertList(@RequestBody List<Student> studentList) {
String sql = "INSERT INTO student (name) VALUE (:studentName);";
MapSqlParameterSource[] parameterSources = new MapSqlParameterSource[studentList.size()];
for (int i = 0; i < studentList.size(); i++) {
Student student = studentList.get(i);
parameterSources[i] = new MapSqlParameterSource();
parameterSources[i].addValue("studentName", student.getName());
}
namedParameterJdbcTemplate.batchUpdate(sql, parameterSources);
return "Batch insert successfully";
}
```
Notes: ```batchUpdate()```的執行效率會比for loop+update()的效率高
#### 4. Query
Syntax
```java
query(String sql, Map<String, Object> map, RowMapper<T> rowMapper);
```
sql: SQL Syntax
map: Set value to the variable in SQL syntax
rowMapper: Convert query result into Java Object
**Return: List**
**RowMapper實作**
- Create ```RowMapper class```
- Override ```mapRow``` method
```java
public class StudentRowMapper implements RowMapper<Student> {
@Override
public Student mapRow(ResultSet resultSet, int i) throws SQLException {
// Get data from database
Integer id = resultSet.getInt("id");
String name = resultSet.getString("name");
// Convert into Java Object
Student student = new Student();
student.setId(id);
student.setName(name);
// Return Java Object
return student;
}
}
```
A more common way to write this method
```java
public class StudentRowMapper implements RowMapper<Student> {
@Override
public Student mapRow(ResultSet resultSet, int i) throws SQLException {
Student student = new Student();
student.setId(resultSet.getInt("id"););
student.setName(resultSet.getString("name"));
return student;
}
}
```
- Instantiate ```RowMapper Class```
```java
@GetMapping("/students")
public List<Student> select() {
String sql = "SELECT id, name FROM student;";
Map<String, Object> map = new HashMap<>();
List<Student> list = namedParameterJdbcTemplate.query(sql, map, new StudentRowMapper());
return list;
}
```
Data Type Mapping Reference:

Query for only 1 item
```java
@GetMapping("/students/{studentId}")
public Student select_one(@PathVariable int studentId) {
String sql = "SELECT id, name FROM student WHERE id = :studentId";
Map<String, Object> map = new HashMap<>();
map.put("studentId", studentId);
List<Student> list = namedParameterJdbcTemplate.query(sql, map, new StudentRowMapper());
// To avoid IndexOutOfBoundException
if (list.size() > 0) {
return list.get(0);
} else {
return null;
}
}
```
#### 5. QueryForObject
* **Return: Java Object**
* **僅適合用在```SELECT COUNT(*)```的情境**
Syntax
```java
@GetMapping("/students/{studentId}")
public void count(@PathVariable int studentId) {
String countSql = "SELECT COUNT(*) FROM student";
Map<String, Object> map = new HashMap<>();
Integer count = namedParameterJdbcTemplate.queryForObject(countSql, map, Integer.class);
System.out.println("Total count in student table: " + count);
}
```
---
## MVC架構模式 - Controller-Service-Dao
### What is MVC:
MVC架構模式的用途就是將系統拆分**為Model, View, Controller**三個部分。
- **Controller(控制器)**:負責轉發Http request
- **Model(模型)**:負責業務邏輯、數據處理(與database互動)
- **View(視圖)**:用html模板呈現數據
因為目前主流工程都是前後端分離,並使用JSON進行數據傳遞,因此View的重要性就愈來愈低。
MVC架構的優點:
- 職責分離、易於維護
- 利於團隊分工
- 可重複使用寫好的程式
### MVC in Spring Boot:
Will be presented in **Controller-Service-Dao** structure
- **Controller**:負責接收Http request、驗證請求參數。
- **Service**:負責業務邏輯
- **Dao(Data Access Object)**:數據處理(與database互動)

Notes:
1. Class命名需要以Controller, Service, Dao做結尾
2. 需要將Controller, Service, Dao都變成Bean,並使用@Autowired注入
(只需Controller需要用```@RestController```,其他兩層用```@Component```)
3. Controller should not directly connected to Dao.
4. **Dao only allows SQL queries, no service logic allowed.**
### Implementation:
#### Step 1. 在專案package下新增各層的package(controller, service, dao)

#### Step 2. 從Dao開始,先建立一個interface來決定要實作哪些方法
StudentDao Interface:
```java
public interface StudentDao {
Student getById(Integer studentId);
}
```
Note: 方法名稱getById的意思就是「用Id去取得資料」,也有findById或queryById的寫法
#### Step 3. 實作Dao Interface的class
StudentDaoImpl class:
```java
@Component
public class StudentDaoImpl implements StudentDao {
@Autowired
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
@Override
public Student getById(Integer studentId) {
String sql = "SELECT id, name FROM student WHERE id = :studentId";
Map<String, Object> map = new HashMap<>();
map.put("studentId", studentId);
List<Student> list = namedParameterJdbcTemplate.query(sql, map, new StudentRowMapper());
if (list.size() > 0) {
return list.get(0);
} else {
return null;
}
}
}
```
Notes:
1. 記得在class加上```@Component```,讓這個class變成bean
2. 記得使用```@Autowired```注入Spring JDBC
#### Step 4. 建立Service Interface
StudentService Interface:
```java
public interface StudentService {
Student getById(Integer studentId);
}
```
#### Step 5. 實作Service class
StudentServiceImpl class:
```java
@Component
public class StudentServiceImpl implements StudentService {
@Autowired
private StudentDao studentDao;
@Override
public Student getById(Integer studentId) {
return studentDao.getById(studentId);
}
}
```
Notes:
1. 記得在class加上```@Component```,讓這個class變成bean
2. 記得使用```@Autowired``` **注入Dao的Interface**
(注入Interface的原因前面有提過,原因是可以減少類之間的耦合,方便程式修改,以後修改的class可以透過多型來向上轉換)
#### Step 6. 完成Controller
```java
@RestController
public class StudentController {
@Autowired
private StudentService studentService;
@GetMapping("/students/{studentId}")
public Student select(@PathVariable Integer studentId) {
return studentService.getById(studentId);
}
}
```
Notes:
1. 記得在加上```@RestController```
2. 記得使用```@Autowired``` **注入Service的Interface**
三層結構圖:

#### Appendix. 對應table的class
1. 通常會把對應table查詢欄位的該class物件,統一整理到一個package當中(一般來說是model package)
2. 一個對應model會有一個專屬對應的Dao
#### 另一種寫法
- Controller: ```@RestController```
- Service: ```@Service```
- Component: ```@Repository```
---
## Transaction 交易
- 「交易」是一種資料庫的用法
- 用途:可以在交易中包含「多個資料庫操作」,這些資料操作會一起成功or一起失敗
- 若交易中發生失敗時,就會撤回原本的資料庫操作,稱為Rollback
### @Transactional
- 用法:加在class或是方法上 **(通常是加在Service的方法上)**
- 用途:使用「交易」來管理這個方法中的資料庫操作
```java
@Transactional
@Override
public void transfer(Integer fromAccountId, Integer toAccountId, Integer money) {
// User A 扣除轉帳金額
accountDao.decreaseMoney(fromAccountId, money);
// simulate exception
Integer a = 1 / 0;
// User B 收到轉入金額
accountDao.addMoney(toAccountId, money);
}
```
**Note: 只有支援ACID的資料庫才能夠支援transaction交易**
---
## Mutiple Database Connection
- 需要在datasource後面指定要連線的database
- url寫法由```.url=jdbc:```改為```jdbc-url=jdbc:```
實際設定步驟如下:
1. 設定application.properties
2. 設定configuration class
3. 在Dao層注入bean **(記得用```@Qualifier```區分prefix)**
application.properties設定範例如下:
```
# Multiple Database
spring.datasource.test1.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.test1.jdbc-url=jdbc:mysql://localhost:3306/test1?serverTimezone=Asia/Taipei&characterEncoding=utf-8
spring.datasource.test1.username=xxx
spring.datasource.test1.password=xxx
spring.datasource.test2.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.test2.jdbc-url=jdbc:mysql://localhost:3306/test2?serverTimezone=Asia/Taipei&characterEncoding=utf-8
spring.datasource.test2.username=xxx
spring.datasource.test2.password=xxx
```
Configuration class創建bean
```java
@Configuration
public class DataSourceConfiguration {
// 連線到 test1 資料庫的 DataSource 和 NamedParameterJdbcTemplate
@Bean
@ConfigurationProperties(prefix = "spring.datasource.test1")
public DataSource test1DataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public NamedParameterJdbcTemplate test1JdbcTemplate(
@Qualifier("test1DataSource") DataSource dataSource) {
return new NamedParameterJdbcTemplate(dataSource);
}
// 連線到 test2 資料庫的 DataSource 和 NamedParameterJdbcTemplate
@Bean
@ConfigurationProperties(prefix = "spring.datasource.test2")
public DataSource test2DataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public NamedParameterJdbcTemplate test2JdbcTemplate(
@Qualifier("test2DataSource") DataSource dataSource) {
return new NamedParameterJdbcTemplate(dataSource);
}
}
```
注入bean
```java
@Autowired
@Qualifier("test1JdbcTemplate")
private NamedParameterJdbcTemplate test1JdbcTemplate;
@Autowired
@Qualifier("test2JdbcTemplate")
private NamedParameterJdbcTemplate test2JdbcTemplate;
```
---
## Spring Data JPA
- JPA:全稱為Java Persistence API
- 用來「定義」要如何操作DB
- 提供許多註解使用(@Entity, @Table...etc)
- Hibernate
- 一種ORM框架,去實現JPA
- 根據JPA註解,負責自動生成SQL語法去操作DB
- 優點:開發效率高
- 缺點:效能差,較難寫出複雜的SQL語法
### 1. Setting
Add dependency
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
```
Add driver(same as JDBC)
Configurate DB setting at application.properties(same as JDBC)
```
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/myjpa?serverTimezone=Asia/Tokyo&characterEncoding=utf-8
spring.datasource.username=xxx
spring.datasource.password=xxx
```
### 2. Map the class with table
Step 1. Create a class that matches db table
```java
@Entity
@Table(name="student") // match table name in db
public class Student {
// @Column -> match variables with columns
// @Id -> for primary key
// @GenerateValue -> for auto_increment
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="id")
Integer id;
@Column(name="name")
String name;
// getter and setter
}
```
Step 2. Create an interface that extends CrudRepository
```java
public interface StudentRepository extends CrudRepository<Student, Integer> {
}
```
**Note: ```CrudRepository<a, b>```
a: the class of the corresponding table
b: data type of primary key**
Step 3. Create a Controller
```java
@RestController
public class StudentController {
@Autowired
private StudentRepository studentRepository;
@PostMapping("/students")
public String insert(@RequestBody Student student) {
studentRepository.save(student);
return "INSERT db Executed.";
}
}
```
Note:
可以在propertires檔案中增加```spring.jpa.show-sql=true```來查看自動生成的SQL
### 3. Details in CrudRespository Interface
- save(): 可以「新增」或「修改」DB中的資料
**Note: 如果要使用save方法來Update data,一定要先查詢data是否存在**
- findById(): 根據ID去查詢資料,返回值是Optional\<T>
- deletById(): 根據ID去刪除資料
CRUD Operation Sample:
```java
@RestController
public class StudentController {
@Autowired
private StudentRepository studentRepository;
// Create
@PostMapping("/students")
public String insert(@RequestBody Student student) {
studentRepository.save(student);
return "INSERT data Executed.";
}
// Read
@GetMapping("/students/{studentId}")
public Student read(@PathVariable Integer studentId) {
Student student = studentRepository.findById(studentId).orElse(null);
return student;
}
// Update
@PutMapping("/students/{studentId}")
public String update(@PathVariable Integer studentId,
@RequestBody Student student) {
Student s = studentRepository.findById(studentId).orElse(null);
if (s != null) {
s.setName(student.getName());
studentRepository.save(s);
return "UPDATE data Executed.";
} else {
return "UPDATE Failed, data does not exist.";
}
}
// Delete
@DeleteMapping("/students/{studentId}")
public String delete(@PathVariable Integer studentId) {
studentRepository.deleteById(studentId);
return "DELETE data Executed.";
}
}
```
Note:
1. ```Student student = studentRepository.findById(studentId).orElse(null);```這一段的意思是如果在db找不到資料,就會回傳null(找到的話就回傳object)
2. **Update的寫法要注意,因為有多一道判斷**
### 4. Customized Queries
在Interface中加入findByXXX()的方法
```java
public interface StudentRepository extends CrudRepository<Student, Integer> {
List<Student> findByName(String name);
Student findByIdAndName(Integer id, String name);
}
```
**Note: 參數名稱不重要,順序很重要,命名規則要依照邏輯去寫**
### 5. 原生SQL查詢:@Query
目的:用來解決findByXxx無法寫出複雜查詢邏輯的問題
用途:在Spring Data JPA中執行SQL語法
位置:加註在方法上,讓該方法去執行SQL
Notes:
1. 用問號代表參數,用數字代表第幾個參數
2. **```nativeQuery = true```才是代表一般的SQL語法**(false會是JPQL的語法)
3. 優先使用findByXxx(),複雜邏輯才會用@Query
Sample:
```java
public interface StudentRepository extends CrudRepository<Student, Integer> {
@Query(value = "SELECT id, name FROM student WHERE id = ?1 AND name = ?2", nativeQuery = true)
Student test1(Integer id, String name);
}
```
---
## Spring Boot Unit Test
Version Check:
* Spring Boot <= 2.1 -> 只能用JUnit4
* Spring Boot >= 2.4 -> 只能用JUnit5
Add dependency
(通常在使用Spring boot時就會自帶,可以檢查有沒有)
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
```
其中exclusions的那段程式目的是要禁用JUnit4的功能,避免不必要的困擾
Add test class(Could be Generated by IntelliJ)
Notes:
- need to add ```@Test``` on the unit test method.
- **class should be pulic**
Sample:
```java
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class CalculatorTest {
@Test
public void add() {
Calculator calculator = new Calculator();
int result = calculator.add(1, 2);
assertEquals(3, result);
}
}
```
Unit Testing的特性:
- 可被自動化
- 各個單元測試互相獨立,彼此不依賴
- 結果要穩定,不被外部服務所影響
Notes:
- **測試程式要放在test資料夾裡面**
- 測試class要用「原class的名字加上Test做結尾」來命名
- 測試class的package要跟原class的package保持一致
### 1. JUnit5:
#### @Test
用途:加在方法上,讓該方法成為一個單元測試code(Test Case)
注意:只能在test資料夾底下使用
**格式:固定為public void,且不帶參數**
命名:方法命名上建議讓人可以清楚了解這個測試是在測甚麼
#### assert用法
常用的assert見下表:

Sample:
```java
@Test
public void add() {
Calculator calculator = new Calculator();
int result = calculator.add(1, 2);
assertNotNull(result);
assertEquals(3, result);
assertTrue(result > 1);
}
@Test
public void divide() {
Calculator calculator = new Calculator();
assertThrows(ArithmeticException.class, () -> {
calculator.divide(1, 0);
});
}
```
**Note: assertThrows通常會用lambda表達式的寫法來寫**
### 2. JUnit5其他常用註解:
#### @BeforeEach, @AfterEach
- @BeforeEach: 每次@Test開始前,都會執行一次
- @AfterEach: 每次@Test結束後,都會執行一次
- 比較常用
```java
public class MyTest {
@BeforeEach
public void beforeEach() {
System.out.println("Execute beforeEach");
}
@AfterEach
public void afterEach() {
System.out.println("Execute afterEach");
}
@Test
public void test1() {
System.out.println("Execute test1");
}
@Test
public void test2() {
System.out.println("Execute test2");
}
}
```
> Execute beforeEach
Execute test1
Execute afterEach
Execute beforeEach
Execute test2
Execute afterEach
#### @BeforeAll, @AfterAll
- @BeforeAll: 在所有@Test開始前執行一次
- @AfterAll: 在所有@Test結束後執行一次
- **兩者必須為static方法**
- 不常用,因為是static,沒辦法與容器中的bean有互動
```java
public class MyTest {
@BeforeAll
public static void beforeAll() {
System.out.println("Execute beforeAll");
}
@AfterAll
public static void afterAll() {
System.out.println("Execute afterAll");
}
@Test
public void test1() {
System.out.println("Execute test1");
}
@Test
public void test2() {
System.out.println("Execute test2");
}
}
```
> Execute beforeAll
Execute test1
Execute test2
Execute afterAll
#### @Disabled, @DisplayName
- @Disabled: 忽略該@Test不執行(加在Test Case上)
- @DisplayName: 自定義顯示名稱(後面參數傳入字串)
### 3. Controller-Server-Dao Testing
#### 3.1. Service / Dao Layer
- 只要**在測試的class上加上```@SpringBootTest```**,則在運行單元測試的時候,Spring Boot就會去啟動Spring容器,創建所有的bean出來
- 同時@Configuration的設定也會被執行,效果等同於直接運行Spring Boot Application
**重點:**
若執行測試刪除的test case時,資料庫裡面的測試資料就會被刪除,此時測試取得資料的test case就會失敗。
解法有二:
1. 將getById()與deleteById()的測試對象分開(不太好的解法,無法確認數據是否乾淨)
2. **在deleteById()上面加上@Transactional註解,讓spring boot進行rollback**
Notes: 兩個不同的@Transactional

Dao Layer Unit Testing Sample Code
```java
@SpringBootTest
public class StudentDaoImplTest {
@Autowired
private StudentDao studentDao;
@Test
public void getById() {
Student student = studentDao.getById(3);
assertNotNull(student);
assertEquals("Judy", student.getName());
assertEquals(100, student.getScore());
assertTrue(student.isGraduate());
assertNotNull(student.getCreateDate());
}
@Test
@Transactional
public void deleteById() {
studentDao.deleteById(3);
Student student = studentDao.getById(3);
assertNull(student);
}
@Test
@Transactional
public void insert() {
Student student = new Student();
student.setName("John");
student.setScore(66.6);
student.setGraduate(true);
Integer studentId = studentDao.insert(student);
Student result = studentDao.getById(studentId);
assertNotNull(result);
assertEquals("John", result.getName());
assertEquals(66.6, result.getScore());
assertTrue(result.isGraduate());
assertNotNull(result.getCreateDate());
}
@Test
@Transactional
public void update() {
Student student = studentDao.getById(4);
student.setName("Kevin");
studentDao.update(student);
Student result = studentDao.getById(4);
assertNotNull(result);
assertEquals("Kevin", result.getName());
}
}
```
#### 3.2. Controller Layer - MockMvc
目的:
- MockMvc即是以「模擬前端的行為」來測試API是否運行正確
- 不能用直接注入bean的方式來測試
方法:
- 在測試用class加上```@SpringBootTest```與```@AutoConfigureMockMvc```
- 用```@Autowired```注入MockMvc
架構:

JSONPath Reference: https://jsonpath.com/
Controller Layer Unit Testing Sample Code
```java
@SpringBootTest
@AutoConfigureMockMvc
public class StudentControllerTest {
@Autowired
private MockMvc mockMvc;
// test GET method
@Test
public void getById() throws Exception {
RequestBuilder requestBuilder = MockMvcRequestBuilders // create a mock request object that use GET method
.get("/students/{studentId}", 3) // URL path
.header("headerName", "headerValue") // add customized header
.queryParam("graduate", "true");
MvcResult mvcResult = mockMvc.perform(requestBuilder) // execute request
.andDo(print()) // allow JSON to be printed out (for better understanding of JSON structure)
.andExpect(status().is(200))
.andExpect(jsonPath("$.id", equalTo(3)))
.andExpect(jsonPath("$.name", notNullValue()))
.andReturn(); // andReturn: can obtain the complete return result from API (can be only put at the end)
String body = mvcResult.getResponse().getContentAsString(); // get response body
System.out.println("Returned Response body is " + body);
}
// test POST method
@Test
public void create() throws Exception {
ObjectMapper = objectMapper = new ObjectMapper();
Student student = new Student();
student.setName("Hank");
student.setScore(14.6);
student.setGraduate(false);
// java object -> json string
String jsonString = objectMapper.writeValueAsString(student)
RequestBuilder requestBuilder = MockMvcRequestBuilders
.post("/students")
.contentType(MediaType.APPLICATION_JSON) // this line is very important!!!
.content(jsonString);
mockMvc.perform(requestBuilder)
.andExpect(status().is(201));
}
}
```
**Note:**
- **在測試POST的時候一定要記得加contentType(MediaType.APPLICATION_JSON)**
### 4. 隔絕外部依賴(提升測試穩定性): Mockito
#### Mock測試
目的:避免為了測試某個單元測試,而去構建整個bean的dependency
做法:創造一個假的bean,去替換掉原本Spring容器中的bean
講白話點:StudentService跟StudentDao有關聯,如果要測試StudentService的時候StudentDao有問題或是資料庫有問題,這時候做StudentService的測試就會失敗。
所以我們就需要模擬一個bean,去吐出我們想要的結果
#### Syntax:
- 在注入的bean上加上```@MockBean```註解
- 創造一個模擬物件並指定返回值
- ```Mockito.when(測試方法).thenReturn(模擬物件);```
Mockito Sample Code
```java
@SpringBootTest
public class StudentServiceImplMockTest {
@Autowired
private StudentService studentService;
@MockBean
private StudentDao studentDao;
@BeforeEach
public void BeforeEach() {
// Create a mock object simulating return value from studentDao
Student mockStudent = new Student();
mockStudent.setId(100);
mockStudent.setName("I am Mock");
// Return mock object when method studentDao.getById() is invoked
Mockito.when(studentService.getById(Mockito.any())).thenReturn(mockStudent);
}
@Test
public void getById() {
Student student = studentService.getById(3);
assertNotNull(student);
assertEquals(3, student.getId());
assertEquals("Judy", student.getName());
}
}
```
> Test Failed
> Expected :3
> Actual :100
Notes:
* ```studentDao.getById(Mockito.any())```中的```Mockito.any()```的意思為接受任意參數,可以避免掉空指針問題
* 使用```@BeforeEach```註解,就可以在進行所有test的時候先跑一次產生Mock object的程式,讓測試程式更簡潔
#### 其他用法
模擬噴出Exception:
```Mockito.when(測試方法).thenReturn(new Runtime Exception());```
紀錄方法的使用次數、順序:
```Mockito.verify(studentDao, Mockito.times(2)).getById(Mockito.any());```
#### 限制:
- 不能mock static方法
- 不能mock private方法
- 不能mock final class
#### @MockBean vs. @SpyBean
- ```@MockBean```:產生假的bean替換原本的bean。沒定義的方法預設返回null
- ```@SpyBean```: 原本的bean仍正常,只是替換掉幾個方法。沒定義的方法就預設使用原本真實的方法
### 5. H2資料庫
* H2是一種嵌入式資料庫
* 可在Spring Boot啟動時生成,運行結束後銷毀
* H2資料庫可以降低單元測試對實體資料庫的依賴,讓單元測試在任何一台電腦都可以跑測試,也不用安裝資料庫軟體
Step 1. Add dependency
URL: https://mvnrepository.com/artifact/com.h2database/h2
```xml
<!-- https://mvnrepository.com/artifact/com.h2database/h2 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.2.224</version>
<scope>test</scope>
</dependency>
```
Step 2. 在test資料夾增加一個單元測試用的設定檔,來設定H2資料庫連線資訊
```test->resources->application.properties```
**Note: 主程式中的設定檔跟測試用的設定檔裡面的設定要保持一致**
properties setting by convention:
```
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.username=sa
spring.datasource.password=sa
#spring.jpa.hibernate.ddl-auto=none # This line is for Spring Data JPA
```
**解決中文亂碼問題**
在properties檔案下入以下兩行
```
server.servlet.encoding.charset=UTF-8
server.servlet.encoding.force=true
```
Step 3. Create a file named **```schema.sql```** under resources, and add **CREATE TABLE IF NOT EXISTS** SQL syntax
Step 4. Create a file named **```data.sql```** under resources, and add **INSERT INTO** SQL syntax
### 6. Convention over Configuration
意思為「約定大於配置」或「慣例優於設定」,是Spring Boot的特性之一。意思是我們不需要添加任何設定,只要知道運作規則就可以直接使用。
e.g. 添加的```data.sql```與```schema.sql```這兩個檔案,就是這種特性的展現。
---
## Maven
### Intro
* Maven是在Spring Boot開發中,負責「Library管理」與「Project構建」的工具
### Library管理
* 用途:管理這個Spring Boot project可以使用哪些功能
用法:透過```pom.xml```檔案進行管理
* Library都統一放在\<dependencies>標籤下,每一個\<dependency>代表一個Library。基本上每個載入的寫法結構上都差不多一樣,**但需要注意的是,spring-boot-starter開頭的library是不可以指定version的值的**。
(因為這些library都是由spring boot開發的,為避免版本混用產生問題,故都是在一開始設定spring boot版本的時候統一設定)
* \<scope>標籤:指定使用場景。例如H2資料庫的scope就是test
* \<exclusions>標籤:指定不加載某個library。(參見前文exclude JUnit4的地方)
### Maven Repository(Maven倉庫)
* 用途:儲存Spring Boot project所使用到的Library(以jar檔形式儲存)
* Maven倉庫可分為local(本地)與remote(遠端),查找jar檔順序是先local再remote
### Project構建
* 用途:打包Spring Boot project中的程式
用法:透過```pom.xml```檔案和```Maven指令```進行操作
* Maven指令:(分3個生命週期)
同一條生命週期裡,後面的指令會去執行前面的指令
* clean生命週期:
* clean:刪除target資料夾中的檔案(Compiled過後的程式)
* default生命週期:
* compile
* test
* package: 打包成.jar檔,並存放在target資料夾
* install
* deploy
**Note: 要打包之前記得先clean一次,這樣才能確保打包的程式是最乾淨的**
---
## 不同環境下的application.properties設定
開發環境:```application-dev.properties```
測試環境:```application-test.properties```
兩個設定檔是可以同時存在的
切換設定檔的方式:intellij右上角>Edit Configurations...>Spring Boot>application>Active Profile的欄位,填上dev就是使用dev的設定,test就是test的設定

---
## Log的級別與規範
### 添加Log
在Controller class加上
```java
private final static Logger log = LoggerFactory.getLogger(StudentController.class);
```
(from org.slf4j)
### 使用log
```java
log.info("some string {}", variable)
```
{} represent the variable
### 常用等級
info():資訊
warn():警告
error():錯誤,需要協助查看
---
## ObjectMapper - JSON String <-> Java Object
* 用途:將JSON字串和Java Object互相轉換
* 使用條件:只要確保pom.xml中有加上spring-boot-starter-web即可
* 用法:
* ```writeValueAsString()```: Java Object to JSON String
* ```readValue()```: JSON String to Java Object
* preparation: 在主package下新增一個```objectmapper```的package,然後把所有轉換class都放進去
### 範例:
Example:
先建立一個對應的class
```java
@JsonInclude(JsonInclude.Include.NON_NULL) // 如果要過濾null值可以加上這一行
@JsonIgnoreProperties(ignoreUnknown = true) // 可以解決key對不上的問題
public class User {
private Integer id;
private String name;
// omit getter and setter
}
```
主程式:
```java
@RestController
public class ObjectMapperController {
@GetMapping("/convert")
public String convert() throws JsonProcessingException {
User user = new User();
user.setId(1);
user.setName("John");
ObjectMapper objectMapper = new ObjectMapper();
// User -> json string
String jsonResult = objectMapper.writeValueAsString(user);
System.out.println("json value is: " + jsonResult);
// json string -> User
String jsonString = "{\"id\":3,\"name\":\"Amy\"}";
User userResult = objectMapper.readValue(jsonString, User.class);
System.out.println("id: " + userResult.getId());
System.out.println("name: " + userResult.getName());
return "convert success";
}
}
```
Notes:
* readValue()的第二個參數,就是對應的class的名稱
### 解決沒設定到的值會顯示null的問題
若有些值在setter設定的時候沒有設定到,就會用null來呈現
可以透過```@JsonInclude(JsonInclude.Include.NON_NULL)```的註解過濾掉null **(加在class上)**
### 解決JSON String突然多出key-value pair的問題
如果JSON String有多出欄位,除了在class新增對應欄位之外,也可以使用```@JsonIgnoreProperties(ignoreUnknown = true)```直接無視,這樣就不用新增欄位,降低以後維護的成本
### 解決變數命名風格不同的問題
若變數不是使用java傳統的駝峰式命名,可以在變數上加```@JsonProperty("對應的key變數")```註解,這樣就不會在轉換的時候出問題
### 複雜轉換(List, Map等等)
參考:https://kucw.github.io/blog/2020/6/java-jackson/
---
## 使用RestTemplate發起Http Request
### RestTemplate
用途:在Spring Boot中發起一個RESTful Http Request
- 即,可以發起GET、POST、PUT、DELETE的請求
- 可以將收到的response body中的JSON字串,轉換成Java Object
- 條件:確保pom.xml中有spring-boot-starter-web即可
Quick API Setup: https://mocki.io/
```jsonld
{
"id": 123,
"name": "Allen"
}
```
### 實作
創建對應的class
```java
public class Student {
Integer id;
String name;
// omit getter and setter
}
```
這裡一樣也可以使用```@JsonProperty()```的註解
Controller
```java
@RestController
public class RestTemplateController {
@GetMapping("/getForObject")
public String getForObject() {
RestTemplate restTemplate = new RestTemplate();
Student student = restTemplate.getForObject(
"https://mocki.io/v1/b2d46933-83e8-4fc6-b2c1-f04c93cc3f65",
Student.class);
System.out.println("id: " + student.getId());
System.out.println("name: " + student.getName());
return "getForObject success";
}
}
```
### 其他用法
* Query String
```java
@GetMapping("/getForObjectWithParam")
public String getForObjectWithParam() {
RestTemplate restTemplate = new RestTemplate();
Map<String, Object> queryParamMap = new HashMap<>();
queryParamMap.put("graduate", true);
Student student = restTemplate.getForObject(
"url",
Student.class,
queryParamMap);
return "success";
}
```
用Map增加參數後call API,其效果相當於在url後面加上```?graduate=true```的query string
* ResponseEntity: 可以取status code
```java
@GetMapping("/getForEntity")
public String getForEntity() {
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<Student> studentEntity = restTemplate.getForEntity(
"url",
Student.class);
// Get http request status code
System.out.println(studentEntity.getStatusCode());
return "success";
}
```
* Post For Object
注意Request Body的寫法
會先new一個物件,設定參數後當成第二的參數傳入post方法
```java
@GetMapping("/postForObject")
public String postForObject() {
RestTemplate restTemplate = new RestTemplate();
Student studentRequestBody = new Student();
studentRequestBody.setName("John");
Student result = restTemplate.postForObject(
"url",
studentRequestBody,
Student.class
);
return "success";
}
```
上面的寫法就等同於post url, request body參數是{"id": null, "name": "John", "contact_phone": null}
* Post For Entity
```java
@GetMapping("/postForEntity")
public String postForEntity() {
RestTemplate restTemplate = new RestTemplate();
Student studentRequestBody = new Student();
studentRequestBody.setName("John");
ResponseEntity<Student> responseEntity = restTemplate.postForEntity(
"url",
studentRequestBody,
Student.class
);
return "success";
}
```
* Exchange
Exchange可以發出GET request也可以發出POST request,不過寫法比較複雜
Sample Code:
```java
@GetMapping("/exchange")
public String exchange() {
RestTemplate restTemplate = new RestTemplate();
// use exchange to execute GET request
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.set("header1", "123");
HttpEntity requestEntity = new HttpEntity(requestHeaders);
Map<String, Object> queryParamMap = new HashMap<>();
queryParamMap.put("graduate", true);
ResponseEntity<Student> getStudentEntity = restTemplate.exchange(
"url",
HttpMethod.GET,
requestEntity,
Student.class,
queryParamMap
);
// use exchange to execute POST request
HttpHeaders requestHeaders2 = new HttpHeaders();
requestHeaders2.set("header2", "456");
requestHeaders2.setContentType(MediaType.APPLICATION_JSON); // This line of code is important!!
Student studentRequestBody = new Student();
studentRequestBody.setName("John");
HttpEntity requestEntity2 = new HttpEntity(studentRequestBody, requestHeaders2);
ResponseEntity<Student> postStudentEntity = restTemplate.exchange(
"url",
HttpMethod.POST,
requestEntity2,
Student.class
);
return "success";
}
```
## Thymeleaf - 前端模板引擎
用途:支援html開發與渲染
條件:需要在pom.xml加入```spring-boot-starter-thymeleaf```
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
```
**注意:添加的註解是```@Contoller```註解**
### 實作
```java
@Controller
public class ThymeleafController {
@GetMapping("/home")
public String home() {
return "index"; //字串要跟html檔名相同
}
}
```
**特別注意一下返回值與返回的字串**
其對應的index.html通常是放在resources/templates裡面
(Thymeleaf會幫忙做轉換)
### 其他用法:
- 動態傳遞數據:
後端java
```java
@Controller
public class ThymeleafController {
@GetMapping("/home")
public String home(Model model) {
Student student = new Student();
student.setId(1);
student.setName("John");
model.addAttribute("myStudent", student);
return "index";
}
}
```
前端html變數應對寫法
```htmlembedded
<p th:text="${myStudent.id}"></p>
<p th:text="${myStudent.name}"></p>
```
- 頁面跳轉
新增對應的html連結跟Controller方法即可
前
```html
<a href="/hello">Go To Hello Page</a>
```
後
```java
@GetMapping("/hello")
public String hello() {
return "hello";
}
```
- 顯示圖片
放置路徑: static/images
新增對應的html標籤即可
```html
<img th:src="@{/images/dog.png}"/>
```
- 提交Form表單
前
```html
<form method="post" action="login">
Username: <input type="text" name="userName"></br>
Password: <input type="text" name="userPassword">
<input type="submit" value="Login">
</form>
```
後
```java
@PostMapping("/login")
public String login(String userName, String userPassword) {
// service logic
return "login";
}
```
再返回一個login.html頁面給user
---
## Bean的三種注入方式
### 變數注入 (Field Injection)
優點:簡單
缺點:容易注入過多的bean進來
Spring官方不太推薦使用field injection(但目前看起來沒什麼差別)
```java
@Component
public class Teacher {
@Autowired
private Printer printer;
@Autowired
private Book book;
}
```
### 建構子注入 (Constructor Injection)
Spring官方推薦寫法
優點:容易釐清依賴關係、測試,變數可為final
缺點:寫起來冗長(可以用Lombok簡化)
```java
@Component
public class Teacher {
private final Printer printer;
private final Book book;
@Autowired
public Teacher(Printer printer, Book book) {
this.printer = printer;
this.book = book
}
}
```
### Setter注入 (Setter Injection)
參考即可,不推薦
```java
@Component
public class Teacher {
private Printer printer;
private Book book;
@Autowired
public void setPrinter(Printer printer) {
this.printer = printer;
}
@Autowired
public void setBook(Book book) {
this.book = book;
}
}
```
---
## Spring Boot 3 更新內容
- 僅支援Java 17之後的版本
- 從Java EE 遷移到 Jakarta EE (import package會不同)
- javax.* 轉移到 jakarta.*
- 持續優化GraalVM技術
- 編譯出啟動速度更快、jar檔更小的Spring Boot程式
Reference: https://kucw.github.io/blog/2019/10/java-graalvm/
---
## 實戰 - 打造電商網站
### 實作商品查詢功能
1. 創建Git repository、Spring Boot Project
2. 在MySQL建立product table
3. 在application.properties檔案中建立database連線資訊
4. 建立Controller-Service-Dao layer package
5. 定義product對應的java class,加上getter & setter
7. 建立Dao Interface + implement class
8. 撰寫SQL, Map, ProductRowMapper\<Product>,並實作裡面的方法
9. 建立Service Interface + implement class,注入Dao Interface
10. 建立Controller class,注入Service Interface
11. 返回ResponseEntity\<Product>
#### 定義商品種類 - 利用Enum
Notes:
1. 一定要大寫
2. 兩單字以上用underscore連接
```java
public enum ProductCategory {
FOOD,
CAR,
BOOK,
E_BOOK
}
```
使用:
利用name()或是valueOf()的方法在Enum跟String之間做轉換
```java
public class MyTest {
public static void main(String[] args) {
ProductCategory category = ProductCategory.FOOD;
String s = category.name();
System.out.println(s); // FOOD
String s1 = "CAR";
ProductCategory category1 = ProductCategory.valueOf(s1);
System.out.println(category1); // CAR
}
}
```
實作:
可以將Product.class裡面的category變數由String轉成Enum。
優點:
- 可以直接從程式中直接知道類別資訊
- 透過預先定義data來檢查資料庫是否有髒資料出現
```java
public class Product {
private ProductCategory category;
}
```
**改寫之後要檢查RowMapper,基本上都要改寫,因為資料型別不同**
```java
// Convert data type from string to Enum
String categoryStr = resultSet.getString("category");
ProductCategory category = ProductCategory.valueOf(categoryStr);
product.setCategory(category);
```
#### 格式化返回的時間
class中Date所返回的時間,其實時區是(GMT+0),會跟資料庫上面記錄的時間不同,所以需要調整
調整方式:在properties檔案加入以下設定
```
spring.jackson.time-zone=GMT+8
```
設定時間格式
```
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss
```
#### Timestamp
- 是一個整數
- 計算從UTC 1970/1/1 00\:00\:00 到現在的總秒數
#### Java 8 新增的時間系列用法
https://kucw.github.io/blog/2020/6/java-date/
### 實作CRUD
**這邊會建議在多建立一個新的class來專門接收前端的post request,讓validation的code比較乾淨,不會影響原本的table對應class**
通常會新增一個dto package(data transfer object),然後把class放進去
Notes:
- Dto的class: 專門跟前端互動
- Dao的class: 專門跟db互動
**Notes: 在class的變數上加上```@NotNull```註解後,記得在Controller的方法參數再加上```@Valid```**
#### Read的細節
- 記得要先check回傳的值是否為null
#### Update的細節
- **在實作Update功能時,通常會建議先檢查商品存不存在,不存在回傳not found,存在才去update後返回更新的物件**
- **last modified date也記得要更新**
example
```java
// Check if product exists
Product product = productService.getProductById(productId);
// if not, return NOT_FOUND
if (product == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
```
#### Delete的細節
- **delete反而不需要去檢查商品存不存在**,只需要回傳結果(NOT FOUND)即可
- 多一個判斷對delete API來說是多餘且不正確的設計
### 查詢商品列表
概念:前端都會call同一個API,但差別就是帶入的參數不同(i.e. ?後面的條件),後端就要根據不同條件給出不同查詢結果
通常會有以下三種功能
- Filtering 查詢條件
- Keyword 關鍵字查詢
- Sorting 排序
- Pagination 分頁
#### 基本功能 - 回傳全部結果
- 依照RESTful的設計理念,因為每個url都是一個資源,因此如果要實作回傳list類的API,**不管如何都要回傳200給前端,代表有查到東西(只是可能list裡面沒有內容)。因此,在實作的時候就不用判斷要查詢的資料是否存在**
#### 進階功能 - Filtering
Controller Example
**重點:```@RequestParam(required = false)```**
因為同一支查詢的API,有可能查全部或是只查部分,此時條件不一定是required
```java
@GetMapping("/products")
public ResponseEntity<List<Product>> getProducts(
@RequestParam(required = false) ProductCategory category
) {
List<Product> productList = productService.getProducts(category);
return ResponseEntity.status(HttpStatus.OK).body(productList);
}
```
Dao Example
**重點:SQL拼接**
* **用```WHERE 1=1```的這個條件當拼接的地方,可以大幅減少多個查詢時sql撰寫的問題**
* AND前面記得加space
```java
@Override
public List<Product> getProducts(ProductCategory category) {
// SQL query
String sql = "SELECT product_id, product_name, category, " +
"image_url, price, stock, description, created_date, last_modified_date " +
"FROM product WHERE 1=1";
Map<String, Object> map = new HashMap<>();
if (category != null) {
sql = sql + " AND category = :category";
map.put("category", category.name());
}
List<Product> productList = namedParameterJdbcTemplate.query(sql,map, new ProductRowMapper());
return productList;
}
```
#### 進階功能 - Keyword
sql拼接後寫法(前後%為模糊查詢,寫法要注意一下)
```java
if (search != null) {
sql = sql + " AND product_name LIKE :search";
map.put("search", "%" + search + "%");
}
```
#### 改善參數傳遞的方式
如果Request Parameter有很個,用@RequestParam的方式傳參數進去方法會很容易出錯,也不好維護
**解決辦法:在dto新增一個class去存那些傳進來的參數**
```java
public class ProductQueryParams {
private ProductCategory category;
private String search;
// getter and setter
}
```
Controller 修改
```java
@GetMapping("/products")
public ResponseEntity<List<Product>> getProducts(
@RequestParam(required = false) ProductCategory category,
@RequestParam(required = false) String search
) {
ProductQueryParams productQueryParams = new ProductQueryParams();
productQueryParams.setCategory(category);
productQueryParams.setSearch(search);
List<Product> productList = productService.getProducts(productQueryParams);
return ResponseEntity.status(HttpStatus.OK).body(productList);
}
```
其他Service跟Dao層也按照這個方式修改
#### 進階功能 - Sorting
這邊採取的設計是```@RequestParam(defaultValue=xxx)```
即便前端沒傳東西,還是會有預設的排序方式(所以後面也不需要null判斷)
Controller:
```java
@GetMapping("/products")
public ResponseEntity<List<Product>> getProducts(
@RequestParam(defaultValue = "created_date") String orderBy,
@RequestParam(defaultValue = "desc") String sort
) {
List<Product> productList = productService.getProducts(category);
return ResponseEntity.status(HttpStatus.OK).body(productList);
}
```
SQL Syntax concat
```sql
sql = sql + " ORDER BY " + productQueryParams.getOrderBy() + " " + productQueryParams.getSort();
```
#### 進階功能 - Pagination
Controller:
```java
@Validated
@GetMapping("/products")
public ResponseEntity<List<Product>> getProducts(
@RequestParam(defaultValue = "5") @Max(1000) @Min(0) Integer limit,
@RequestParam(defaultValue = "0") @Min(0) Integer offset
) {
List<Product> productList = productService.getProducts(category);
return ResponseEntity.status(HttpStatus.OK).body(productList);
}
```
**Note:**
- 這邊選擇用defaultValue而不是用required=false的原因是為了**降低資料庫的負擔,如果每次都去存取全部的資料,會大幅影響資料庫效能**
- **這邊也建議設計前端傳來的數字的上限(同樣for效能考量)**
- 新增下限以防錯誤數值傳入(譬如負數)
- **記得加上```@Validated```**
#### 進階功能 - Pagination加強版(分頁+總筆數)
分頁:
- 根據返回的response body格式,可以區分成兩類
1. 僅回傳商品數據
2. 回傳商品數據+總筆數(回傳一個java object)
回傳架構:
```jsonld
{
"limit": 3,
"offset": 0,
"total": 7,
"result" : [商品]
}
```
回傳總筆數的用意是可以給前端拿去計算分頁頁數
Create a Page class using Generics
```java
public class Page <T> {
private Integer limit;
private Integer offset;
private Integer total;
private List<T> results;
// getter and setter
}
```
調整controller
```java
// 前略
// 計算總筆數
Integer total = productService.countProduct(productQueryParams);
// 新的回傳物件
Page<Product> page = new Page<>();
page.setLimit(limit);
page.setOffset(offset);
page.setTotal(total);
page.setResults(productList);
return ResponseEntity.status(HttpStatus.OK).body(page);
```
Dao
```java
public Integer countProduct(ProductQueryParams productQueryParams) {
// SQL query
String sql = "SELECT COUNT(*) FROM product WHERE 1=1";
Map<String, Object> map = new HashMap<>();
// Filtering Search
if (productQueryParams.getCategory() != null) {
sql = sql + " AND category = :category";
map.put("category", productQueryParams.getCategory().name());
}
// Keyword Search
if (productQueryParams.getSearch() != null) {
sql = sql + " AND product_name LIKE :search";
map.put("search", "%" + productQueryParams.getSearch() + "%");
}
Integer totalCount = namedParameterJdbcTemplate.queryForObject(sql, map, Integer.class);
return totalCount;
}
```
---
### 實作註冊帳號功能
#### 檢查註冊的email
- 在Service / Dao Layer中增加邏輯
- 用前端傳過來的email值,由Service去呼叫Dao查詢db
```java
// UserServiceImpl
@Override
public Integer register(UserRegisterRequest userRegisterRequest) {
User user = userDao.getUserByEmail(userRegisterRequest.getEmail());
// if email is registered
if (user != null) {
log.warn("The email: {} has been registered.", userRegisterRequest.getEmail());
// Return 400 to frontend
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
return userDao.createUser(userRegisterRequest);
}
// UserDaoImpl
@Override
public User getUserByEmail(String email) {
String sql = "SELECT user_id, email, password, created_date, last_modified_date " +
"FROM user WHERE email = :email";
Map<String, Object> map = new HashMap<>();
map.put("email", email);
List<User> userList = namedParameterJdbcTemplate.query(sql, map, new UserRowMapper());
if (userList.size() > 0) {
return userList.get(0);
} else {
return null;
}
}
```
#### 添加Log
final變數
```java
private final static Logger log = LoggerFactory.getLogger(UserServiceImpl.class);
```
#### 常用的email檢查註解 - ```@Email```
```java
public class UserRegisterRequest {
@NotBlank
@Email
private String email;
}
```
#### 隱藏返回的password - ```@JsonIgnore```
```java
public class User {
@JsonIgnore
private String password;
}
```
#### 登入功能
**Note:
String的比較一定要用```.equals()```**
Service Layer實作
```java
@Override
public User login(UserLoginRequest userLoginRequest) {
User user = userDao.getUserByEmail(userLoginRequest.getEmail());
// check user's existence
if (user == null) {
log.warn("This email: {} is not registered", userLoginRequest.getEmail());
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
// check password
if (user.getPassword().equals(userLoginRequest.getPassword())) {
return user;
} else {
log.warn("Password incorrect for email: {}", userLoginRequest.getEmail());
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
}
```
#### 如何安全地儲存密碼
- 禁止使用明碼的方式儲存密碼
- 資料庫加密
#### Hash
- 經過一個函數將字串轉換成亂碼雜湊值(hash value)
- 同一個字串,轉換出來的hash value一定是一樣的
- 可能會有collision(碰撞)的狀況,也就是多個字串轉換成同一個hash value
- 嚴格定義上Hash不能算加密,因為hash value沒辦法被解密
#### 對稱加密
- 只有一把key,可以使用該key進行加密跟解密
#### 非對稱加密
- 有兩把key:
- 私鑰:自己保管,不可洩漏
- 公鑰:公開,可廣發所有人
- 私鑰加密,公鑰解密
- 公鑰加密,私鑰解密
Hash實作(比較簡單) - MD5
實作邏輯:
1. 使用者註冊新帳號
2. 在service layer進行MD5 hash
3. User table儲存的就是hash value
4. 使用者登入時也是在service layer加密後,跟table比較,再決定是否可以讓user登入
語法:
```java
String hashedPassword = DigestUtils.md5DigestAsHex(userRegisterRequest.getPassword().getBytes());
```
實作:
註冊
```java
@Override
public Integer register(UserRegisterRequest userRegisterRequest) {
User user = userDao.getUserByEmail(userRegisterRequest.getEmail());
// if email is registered
if (user != null) {
log.warn("The email: {} has been registered.", userRegisterRequest.getEmail());
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
// Use MD5 to generate hash value for password
String hashedPassword = DigestUtils.md5DigestAsHex(userRegisterRequest.getPassword().getBytes());
userRegisterRequest.setPassword(hashedPassword);
return userDao.createUser(userRegisterRequest);
}
```
登入
```java
@Override
public User login(UserLoginRequest userLoginRequest) {
User user = userDao.getUserByEmail(userLoginRequest.getEmail());
// check user's existence
if (user == null) {
log.warn("This email: {} is not registered", userLoginRequest.getEmail());
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
// Use MD5 to generate hashed password
String hashedPassword = DigestUtils.md5DigestAsHex(userLoginRequest.getPassword().getBytes());
// check password
if (user.getPassword().equals(hashedPassword)) {
return user;
} else {
log.warn("Password incorrect for email: {}", userLoginRequest.getEmail());
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
}
```
---
### 實作訂單功能
#### 創建訂單
- url路徑: /users/{userId}/orders
- 用JSON object 模擬訂單內容
```json
{
"buyItemList": [
{
"productId": 9,
"quantity": 1
},
{
"productId": 10,
"quantity": 2
}
]
}
```
Note: 這邊建議直接設計成JSON Objec,如果只設計成JSON List的話擴充性不高
- 針對該JSON設計class
```java
public class CreateOrderRequest {
@NotEmpty
private List<BuyItem> buyItemList;
}
```
```java
public class BuyItem {
@NotNull
private Integer productId;
@NotNull
private Integer quantity;
}
```
Controller
```java
@PostMapping("/users/{userId}/orders")
public ResponseEntity<?> createOrder(@PathVariable Integer userId,
@RequestBody @Valid CreateOrderRequest createOrderRequest) {
}
```
Service層的createOrder(訂單)細節
- 因為訂單table有兩個表(order, orderItem),所以會call兩次Dao層
- **如果需要更新兩張以上的表,切記要加上```@Transactional```的註解**
createOrderItems(訂單明細)細節
- 用batchUpdate效率會比for loop還高
#### 查詢訂單
- 取得OrderItem的時候,記得LEFT JOIN product table
- 如果有join其他table,RowMapper也要去接住那些欄位,連帶影響class需要去擴充欄位
example
```java
public class OrderItemRowMapper implements RowMapper<OrderItem> {
@Override
public OrderItem mapRow(ResultSet resultSet, int i) throws SQLException {
OrderItem orderItem = new OrderItem();
orderItem.setOrderId(resultSet.getInt("order_id"));
orderItem.setProductId(resultSet.getInt("product_id"));
orderItem.setQuantity(resultSet.getInt("quantity"));
orderItem.setAmount(resultSet.getInt("amount"));
// 下面是原本不該table class的變數,需要另外新增
// 因為join了商品的table
orderItem.setPoductName(resultSet.getString("product_name"));
orderItem.setImageUrl(resultSet.getString("image_url"));
return orderItem;
}
}
```
- 另外因為在service層要一起將Order跟OrderItemList一起回傳給前端,所以Order class也需要擴充
```java
private List<OrderItem> orderItemList;
// Getter and Setter
```
```java
@Override
public Order getOrderById(Integer orderId) {
// 取得訂單
Order order = orderDao.getOrderById(orderId);
// 取得訂單明細
List<OrderItem> orderItemList = orderDao.getOrderItemsByOrderId(orderId);
order.setOrderItemList(orderItemList);
return order;
}
```
#### 檢查user是否存在、扣除庫存、更新庫存功能
- Check User
```java
// Add log
private final static Logger log = LoggerFactory.getLogger(OrderServiceImpl.class);
@Autowired
private UserDao userDao;
```
```java
// Check user's existence
User user = userDao.getUserById(userId);
if (user == null) {
log.warn("User: {} doest not exist.", userId);
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
```
- Check Stock
```java
@Transactional
@Override
public Integer createOrder(Integer userId, CreateOrderRequest createOrderRequest) {
// Check user's existence
User user = userDao.getUserById(userId);
if (user == null) {
log.warn("User: {} doest not exist.", userId);
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
int totalAmount = 0;
List<OrderItem> orderItemList = new ArrayList<>();
for (BuyItem buyItem : createOrderRequest.getBuyItemList()) {
Product product = productDao.getProductById(buyItem.getProductId());
// check stock of product
// no stock
if (product == null) {
log.warn("Product {} does not exist", buyItem.getProductId());
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
// not enough
} else if (product.getStock() < buyItem.getQuantity()) {
log.warn("Product {} is not enough. You ordered {}, and the remaining stock is {}.",
product.getProductId(), buyItem.getQuantity(), product.getStock());
throw new ResponseStatusException(HttpStatus.BAD_REQUEST);
}
// Minus product stock
productDao.updateStock(product.getProductId(), product.getStock() - buyItem.getQuantity());
// Count total amount
int itemAmount = buyItem.getQuantity() * product.getPrice();
totalAmount += itemAmount;
// Convert BuyItem to OrderItem object
OrderItem orderItem = new OrderItem();
orderItem.setProductId(buyItem.getProductId());
orderItem.setQuantity(buyItem.getQuantity());
orderItem.setAmount(totalAmount);
orderItemList.add(orderItem);
}
// Create order
Integer orderId = orderDao.createOrder(userId, totalAmount);
orderDao.createOrderItems(orderId, orderItemList);
return orderId;
}
```
Update Stock
```java
// Minus product stock
productDao.updateStock(product.getProductId(), product.getStock() - buyItem.getQuantity());
```
```java
@Override
public void updateStock(Integer productId, Integer stock) {
String sql = "UPDATE product SET stock = :stock, last_modified_date = :lastModifiedDate" +
" WHERE product_id = :productId";
Map<String, Object> map = new HashMap<>();
map.put("stock", stock);
map.put("lastModifiedDate", new Date());
map.put("productId", productId);
namedParameterJdbcTemplate.update(sql, map);
}
```
#### 查詢訂單明細
Controller Layer
```java
@GetMapping("/users/{userId}/orders")
public ResponseEntity<Page<Order>> getOrders(
@PathVariable Integer userId,
@RequestParam(defaultValue = "10") @Max(1000) @Min(0) Integer limit,
@RequestParam(defaultValue = "0") @Min(0) Integer offset
) {
OrderQueryParams orderQueryParams = new OrderQueryParams();
orderQueryParams.setUserId(userId);
orderQueryParams.setLimit(limit);
orderQueryParams.setOffset(offset);
// Get order list
List<Order> orderList = orderService.getOrders(orderQueryParams);
// Get total number of orders
Integer count = orderService.countOrder(orderQueryParams);
// Pagination
Page<Order> page = new Page<>();
page.setLimit(limit);
page.setOffset(offset);
page.setTotal(count);
page.setResults(orderList);
return ResponseEntity.status(HttpStatus.OK).body(page);
}
```
Service Layer
```java
@Override
public Integer countOrder(OrderQueryParams orderQueryParams) {
return orderDao.countOrder(orderQueryParams);
}
@Override
public List<Order> getOrders(OrderQueryParams orderQueryParams) {
List<Order> orderList = orderDao.getOrders(orderQueryParams);
for (Order order: orderList) {
List<OrderItem> orderItemList = orderDao.getOrderItemsByOrderId(order.getOrderId());
order.setOrderItemList(orderItemList);
}
return orderList;
}
```
Dao Layer
```java
@Override
public Integer countOrder(OrderQueryParams orderQueryParams) {
String sql = "SELECT COUNT(*) FROM `order` WHERE 1=1";
Map<String, Object> map = new HashMap<>();
sql = addFilteringSql(sql, map, orderQueryParams);
Integer total = namedParameterJdbcTemplate.queryForObject(sql, map, Integer.class);
return total;
}
@Override
public List<Order> getOrders(OrderQueryParams orderQueryParams) {
String sql = "SELECT order_id, user_id, total_amount, created_date, last_modified_date FROM `order`" +
" WHERE 1+1";
Map<String, Object> map = new HashMap<>();
// filter
sql = addFilteringSql(sql, map, orderQueryParams);
// sorting
sql = sql + " ORDER BY created_date DESC";
// pagination
sql = sql + " LIMIT :limit OFFSET :offset";
map.put("limit", orderQueryParams.getLimit());
map.put("offset", orderQueryParams.getOffset());
List<Order> orderList = namedParameterJdbcTemplate.query(sql, map, new OrderRowMapper());
return orderList;
}
private String addFilteringSql(String sql, Map<String, Object> map, OrderQueryParams orderQueryParams) {
if (orderQueryParams.getUserId() != null) {
sql = sql + " AND user_id = :userId";
map.put("userId", orderQueryParams.getUserId());
}
return sql;
}
```
### 完整Source Code
https://github.com/css186/springboot-mall
## Deployment 問題
- java 版本
https://stackoverflow.com/questions/53604111/heroku-cannot-deploy-java-11-spring-boot-app
解決方式:在專案資料夾最外面(跟src同level)新增```system.properties```檔案,裡面加上```java.runtime.version=11```
其他版本亦同
- 隱藏Database connection credentials問題
適用spring boot 2.4版本以上
1. 在resource底下新增secret.properties(檔名隨意),然後把db帳號密碼放進去
2. 在application.properties import secret.properties ```spring.config.import=optional:./src/main/resources/secret.properties```optional的用意在於即便沒有找到檔案也可以執行
3. 上傳github時將secret.properties放入.gitignore裡面
4. 如果要部屬到線上平台,記得在平台提供的cofig設定中加入帳號密碼設定(格式同application.properties),避免因為連線資訊沒有一起上傳github而無法執行