1hr37mins30secs de SpringBoot
SpringBoot 2023
SpringBoot initializr
Dependencies:
- SpringBoot DevTools
- Spring Web
- Spring Data JPA and Hibernate
- PostgreSQL Driver
Starting the server
src
- main
- java
- com.example.demo.DemoApplication
- resources
- static
- templates
- application.properties
- test
- java
- resources
Create a simple API
|
|
Student Class
|
|
|
|
API Layer
|
|
Business Layer
|
|
Properties File
|
|
Connecting to Database
打開 shell,輸入 psql
\l
可以查看現有的資料庫清單
建立新的資料庫
|
|
\du
可以查看現有角色 role
|
|
\c student
連線進去此資料庫
\d
確認此資料庫有無relations (如果有app正在存取應該會顯示出來)
- Run project -> Hikari-Pool started
JPA and @Entity
|
|
JPA repository
Implement data access layer
StudentRepository — 使用 xxx+Repository 命名,當程式要存取資料庫,尤其是使用JPA存取DB的時候
繼承JpaRepository,Generic#1 的 T 放 the type of object that we want this repository to work with;Generic#2 the ID for the type that we want.
@Repository
— responsible for data access
|
|
在student service 使用student repository
|
|
Saving Students
建立一個學生配置檔,這樣啟動專案的時候就會將以下資料插入資料庫的table
|
|
@Transient
存入資料庫table時,與其塞入年齡值,選擇直接讓系統藉由生日(dob)計算出年齡
使用 javax.persistence.*
,如此以來之後如果把Hibernate換成其它provider,還是能正常運作
標註@Transient
的類別欄位:There is no need for you to be a column in our database, meaning that age will be calculated first.
把 age 從 Student constructor 移除之後,在 getAge 方法中
修改回傳值 Period.between(this.dob, LocalDate.now()).getYears();
|
|
PostMapping
Post is used when you want to add new resources to your system. 在這個例子就是新增一筆 Student。
- 在 StudentController.java 新增一個 registerNewStudent 方法
|
|
- 在 StudentService.java 新增 addNewStudent 方法
|
|
-
右鍵上面的
@PostMapping
,點擊 Open in HTTP client---Actions for URL--- Go to declaration or usages Open in HTTP client ✅ click this ✅ Show all endpoints of module
-
generated-requests.http
### POST http://localhost:8080/api/vi/student CONTENT-TYPE: application/json { "firstName": "Alicia", "lastName": "Hadid", "email": "[email protected]", "password": "passw0rd" }
-
寫好 payload 之後,點擊 POST 左邊的綠色play按鈕,Run this request,就可以在 console 查看
-
步驟3~6也可以改用postman完成
Writing Business Logic
在StudentRepository寫自定義的商業邏輯
|
|
在 StudentService.addNewStudent 方法加入判斷是否有重複 email 的商業邏輯
|
|
Testing post request
試著發送email與現有資料一樣的payload, 結果發現 status為 500,但 message 是空的 ""
在 application.properties 加入錯誤訊息的顯示
|
|
Delete student
在 StudentController 加入 deleteStudent 方法
|
|
在 StudentService 加入 deleteStudent 方法
|
|
測試這個方法,刪除 id 為 1 的學生
|
|
Exercise
PutMapping - to update both name and email。要掛上@Transaction
標註。
StudentController:
|
|
StudentService:
When we use @Transactional
annotation, the entity goes into managed state (參考 Spring Data JPA course).
|
|
測試更新一筆資料 request payload
|
|
|
|
10 SpringBoot mistakes
-
Leaking internals
洩漏 user 不需要知道的訊息,例如 Customer entity 的 password 欄位
方法… 使用
@JsonIgnore
1 2 3
@Override @JsonIgnore public String getPassword() { return this.password; }
-
Not using Record … 好東西幹嘛不用
取代
class CustomerResponse
1 2 3 4 5 6 7 8
record CustomerResponse (String name) { } // Before appearance of Record class CustomerResponse { String name; //... }
-
Not using dependency injection correctly
不要再用 new … initialize 物件,改用依賴注入
Beans, they are singleton, which means they are reused all over the shot.
需要有建構子較好,不用加 物件的 setter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
public class Test { private final CustomerRepository customerRepository; //@Autowired ❌ public Test(CustomerRepository customerRepository) { this.customerRepository = customerRepository; } // some codes // Setter injection ❌ // public void setCustomerRepository(CustomerRepository customerRepository) { // this.customerRepository = customerRepository; // } }
-
Separation of misconcern
Controller 不應該直接呼叫 Repository 的方法存許資料庫
Controller 的職責是處理 httpRequest、Service 處理商業邏輯、Repository與資料來源互動
-
Lack of ErrorHandling
以下程式碼為例,找不到就會拋出錯誤,message=“foo”,但statusCode=500
比較適切的回應是 404 error 才對
1 2 3 4 5
@GetMapping("{id}") private Customer getCustomer(@PathVariable Integer id) { return customerRepository.findById(id) .orElseThrow(() -> new RuntimeException("foo")); }
最佳方式是寫自定義的錯誤處理機制
src.main.java.com.example.exception.ApiError
1 2 3 4 5 6 7 8 9
package com.example.exception; import java.time.LocalDateTime; public record ApiError(String path, String message, int statusCode, LocalDateTime localDateTime) { }
DefaultExceptionHandler: 負責處理所有不同類型的例外
@ControllerAdvice
@ExcpetionHandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
@ControllerAdvice public class DefaultExceptionHandler { @ExceptionHandler(ResourceNotFoundException.class) public ResponseEntity<ApiError> handleException(ResourceNotFoundException e, HttpServletRequest request) { ApiError apiError = new ApiError( request.getRequestURI(), e.getMessage(), HttpStatus.NOT_FOUND.value(), LocalDateTime.now() ); return new ResponseEntity<>(apiError, HttpStatus.NOT_FOUND); } @ExceptionHandler(InsufficientAuthenticationException.class) public ResponseEntity<ApiError> handleException(InsufficientAuthenticationException e, HttpServletRequest request) { ApiError apiError = new ApiError( request.getRequestURI(), e.getMessage(), HttpStatus.FORBIDDEN.value(), // there LocalDateTime.now() ); return new ResponseEntity<>(apiError, HttpStatus.FORBIDDEN); } @ExceptionHandler(BadCredentialException.class) public ResponseEntity<ApiError> handleException(BadCredentialException e, HttpServletRequest request) { ApiError apiError = new ApiError( request.getRequestURI(), e.getMessage(), HttpStatus.UNAUTHORIZED.value(), // there LocalDateTime.now() ); return new ResponseEntity<>(apiError, HttpStatus.UNAUTHORIZED); } // @ExceptionHandler(Exception.class) @ExceptionHandler({Exception.class, RuntimeException.class}) public ResponseEntity<ApiError> handleException(Exception e, HttpServletRequest request) { ApiError apiError = new ApiError( request.getRequestURI(), e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR.value(), // there LocalDateTime.now() ); return new ResponseEntity<>(apiError, HttpStatus.INTERNAL_SERVER_ERROR); } }
這樣的寫法就不會導致拋出容易使人混淆的 statusCode 了
見上述程式碼最後一個方法
@ExceptionHandler({Exception.class, RuntimeException.class})
-
Testing (NOOB Level)
@DataJpaTest
@TestContainers
整合測試使用的
@SpringBootTest
-
Not Using Annotation Validation
使用 Validation 標註可以減少很多 if 判斷
1 2 3 4 5 6
@GetMapping("{id}") private void getCustomer(@Validated CustomerRequest customerRequest) { // some codes } record CustomerRequest (@NonNull String name) {}
使用
@Validated
+@NonNull
再加上自定義的 ExceptionHandler 捕獲錯誤,轉換成user看的懂的payload -
Don’t log enough
-
Know how to log (when to info, debug, warn)
-
Don’t log frequently, because it could also impact the response round trip
-
-
Using xml to build Spring applications
-
Immutability