Java Spring JPA 參數校驗 JSON 等常用註解 及解析

0. 註解基本原理

以前,『XML』是各大框架的青睞者,它以松耦合的方式完成了框架中幾乎所有的配置,但是隨着項目越來越龐大,『XML』的內容也越來越複雜,維護成本變高。

於是就有人提出來一種標記式高耦合的配置方式,『註解』。方法上可以進行註解,類上也可以註解,字段屬性上也可以註解,反正幾乎需要配置的地方都可以進行註解。

關於『註解』和『XML』兩種不同的配置模式,爭論了好多年了,各有各的優劣,註解可以提供更大的便捷性,易於維護修改,但耦合度高,而 XML 相對於註解則是相反的。

追求低耦合就要拋棄高效率,追求效率必然會遇到耦合。本文意不再辨析兩者誰優誰劣,而在於以最簡單的語言介紹註解相關的基本內容。

0.1 註解的本質

「java.lang.annotation.Annotation」接口中有這麼一句話,用來描述『註解』。

The common interface extended by all annotation types

所有的註解類型都繼承自這個普通的接口(Annotation)

這句話有點抽象,但卻說出了註解的本質。我們看一個 JDK 內置註解的定義:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {

}

這是註解 @Override 的定義,其實它本質上就是:

public interface Override extends Annotation{
    
}

沒錯,註解的本質就是一個繼承了 Annotation 接口的接口。有關這一點,你可以去反編譯任意一個註解類,你會得到結果的。

一個註解準確意義上來說,只不過是一種特殊的註釋而已,如果沒有解析它的代碼,它可能連註釋都不如。

而解析一個類或者方法的註解往往有兩種形式,一種是編譯期直接的掃描,一種是運行期反射。反射的事情我們待會說,而編譯器的掃描指的是編譯器在對 java 代碼編譯字節碼的過程中會檢測到某個類或者方法被一些註解修飾,這時它就會對於這些註解進行某些處理。

典型的就是註解 @Override,一旦編譯器檢測到某個方法被修飾了 @Override 註解,編譯器就會檢查當前方法的方法簽名是否真正重寫了父類的某個方法,也就是比較父類中是否具有一個同樣的方法簽名。

這一種情況只適用於那些編譯器已經熟知的註解類,比如 JDK 內置的幾個註解,而你自定義的註解,編譯器是不知道你這個註解的作用的,當然也不知道該如何處理,往往只是會根據該註解的作用範圍來選擇是否編譯進字節碼文件,僅此而已。

0.2 元註解

『元註解』是用於修飾註解的註解,通常用在註解的定義上,例如:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {

}

這是我們 @Override 註解的定義,你可以看到其中的 @Target@Retention 兩個註解就是我們所謂的『元註解』,『元註解』一般用於指定某個註解生命週期以及作用目標等信息。

JAVA 中有以下幾個『元註解』:

  • @Target:註解的作用目標
  • @Retention:註解的生命週期
  • @Documented:註解是否應當被包含在 JavaDoc 文檔中
  • @Inherited:是否允許子類繼承該註解

其中,@Target 用於指明被修飾的註解最終可以作用的目標是誰,也就是指明,你的註解到底是用來修飾方法的?修飾類的?還是用來修飾字段屬性的。

@Target 的定義如下:
image
我們可以通過以下的方式來爲這個 value 傳值:

@Target(value = {ElementType.FIELD})

被這個 @Target 註解修飾的註解將只能作用在成員字段上,不能用於修飾方法或者類。其中,ElementType 是一個枚舉類型,有以下一些值:

  • ElementType.TYPE:允許被修飾的註解作用在類、接口和枚舉上
  • ElementType.FIELD:允許作用在屬性字段上
  • ElementType.METHOD:允許作用在方法上
  • ElementType.PARAMETER:允許作用在方法參數上
  • ElementType.CONSTRUCTOR:允許作用在構造器上
  • ElementType.LOCAL_VARIABLE:允許作用在本地局部變量上
  • ElementType.ANNOTATION_TYPE:允許作用在註解上
  • ElementType.PACKAGE:允許作用在包上

@Retention 用於指明當前註解的生命週期,它的基本定義如下:
image
同樣的,它也有一個 value 屬性:

@Retention(value = RetentionPolicy.RUNTIME

這裏的 RetentionPolicy 依然是一個枚舉類型,它有以下幾個枚舉值可取:

  • RetentionPolicy.SOURCE:當前註解編譯期可見,不會寫入 class 文件
  • RetentionPolicy.CLASS:類加載階段丟棄,會寫入 class 文件
  • RetentionPolicy.RUNTIME:永久保存,可以反射獲取

@Retention 註解指定了被修飾的註解的生命週期,一種是隻能在編譯期可見,編譯後會被丟棄,一種會被編譯器編譯進 class 文件中,無論是類或是方法,乃至字段,他們都是有屬性表的,而 JAVA 虛擬機也定義了幾種註解屬性表用於存儲註解信息,但是這種可見性不能帶到方法區,類加載時會予以丟棄,最後一種則是永久存在的可見性。

剩下兩種類型的註解我們日常用的不多,也比較簡單,這裏不再詳細的進行介紹了,你只需要知道他們各自的作用即可。@Documented 註解修飾的註解,當我們執行 JavaDoc 文檔打包時會被保存進 doc 文檔,反之將在打包時丟棄。@Inherited 註解修飾的註解是具有可繼承性的,也就說我們的註解修飾了一個類,而該類的子類將自動繼承父類的該註解。

0.3 JAVA 的內置三大註解

除了上述四種元註解外,JDK 還爲我們預定義了另外三種註解,它們是:

  • @Override
  • @Deprecated
  • @SuppressWarnings

@Override 註解想必是大家很熟悉的了,它的定義如下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

它沒有任何的屬性,所以並不能存儲任何其他信息。它只能作用於方法之上,編譯結束後將被丟棄。

所以你看,它就是一種典型的『標記式註解』,僅被編譯器可知,編譯器在對 java 文件進行編譯成字節碼的過程中,一旦檢測到某個方法上被修飾了該註解,就會去匹對父類中是否具有一個同樣方法簽名的函數,如果不是,自然不能通過編譯。

@Deprecated 的基本定義如下:
image
依然是一種『標記式註解』,永久存在,可以修飾所有的類型,作用是,標記當前的類或者方法或者字段等已經不再被推薦使用了,可能下一次的 JDK 版本就會刪除。

當然,編譯器並不會強制要求你做什麼,只是告訴你 JDK 已經不再推薦使用當前的方法或者類了,建議你使用某個替代者。

@SuppressWarnings 主要用來壓制 java 的警告,它的基本定義如下:
image
它有一個 value 屬性需要你主動的傳值,這個 value 代表一個什麼意思呢,這個 value 代表的就是需要被壓制的警告類型。例如:

public static void main(String[] args) {
    Date date = new Date(2018, 7, 11);
}

這麼一段代碼,程序啓動時編譯器會報一個警告。

Warning:(8, 21) java: java.util.Date 中的 Date(int,int,int) 已過時

而如果我們不希望程序啓動時,編譯器檢查代碼中過時的方法,就可以使用 @SuppressWarnings 註解並給它的 value 屬性傳入一個參數值來壓制編譯器的檢查。

@SuppressWarning(value = "deprecated")
public static void main(String[] args) {
    Date date = new Date(2018, 7, 11);
}

這樣你就會發現,編譯器不再檢查 main 方法下是否有過時的方法調用,也就壓制了編譯器對於這種警告的檢查。

當然,JAVA 中還有很多的警告類型,他們都會對應一個字符串,通過設置 value 屬性的值即可壓制對於這一類警告類型的檢查。

自定義註解的相關內容就不再贅述了,比較簡單,通過類似以下的語法即可自定義一個註解。

public @interface InnotationName{
    
}

當然,自定義註解的時候也可以選擇性的使用元註解進行修飾,這樣你可以更加具體的指定你的註解的生命週期、作用範圍等信息。

0.4 註解與反射

上述內容我們介紹了註解使用上的細節,也簡單提到,「註解的本質就是一個繼承了 Annotation 接口的接口」,現在我們就來從虛擬機的層面看看,註解的本質到底是什麼。

首先,我們自定義一個註解類型:

image

這裏我們指定了 Hello 這個註解只能修飾字段和方法,並且該註解永久存活,以便我們反射獲取。

之前我們說過,虛擬機規範定義了一系列和註解相關的屬性表,也就是說,無論是字段、方法或是類本身,如果被註解修飾了,就可以被寫進字節碼文件。屬性表有以下幾種:

  • RuntimeVisibleAnnotations:運行時可見的註解
  • RuntimeInVisibleAnnotations:運行時不可見的註解
  • RuntimeVisibleParameterAnnotations:運行時可見的方法參數註解
  • RuntimeInVisibleParameterAnnotations:運行時不可見的方法參數註解
  • AnnotationDefault:註解類元素的默認值

給大家看虛擬機的這幾個註解相關的屬性表的目的在於,讓大家從整體上構建一個基本的印象,註解在字節碼文件中是如何存儲的。

所以,對於一個類或者接口來說,Class 類中提供了以下一些方法用於反射註解。

  • getAnnotation:返回指定的註解
  • isAnnotationPresent:判定當前元素是否被指定註解修飾
  • getAnnotations:返回所有的註解
  • getDeclaredAnnotation:返回本元素的指定註解
  • getDeclaredAnnotations:返回本元素的所有註解,不包含父類繼承而來的

方法、字段中相關反射註解的方法基本是類似的,這裏不再贅述,我們下面看一個完整的例子。

首先,設置一個虛擬機啓動參數,用於捕獲 JDK 動態代理類。

-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

然後 main 函數。

image

我們說過,註解本質上是繼承了 Annotation 接口的接口,而當你通過反射,也就是我們這裏的 getAnnotation 方法去獲取一個註解類實例的時候,其實 JDK 是通過動態代理機制生成一個實現我們註解(接口)的代理類。

我們運行程序後,會看到輸出目錄裏有這麼一個代理類,反編譯之後是這樣的:
imageimage
代理類實現接口 Hello 並重寫其所有方法,包括 value 方法以及接口 Hello 從 Annotation 接口繼承而來的方法。

而這個關鍵的 InvocationHandler 實例是誰?

AnnotationInvocationHandler 是 JAVA 中專門用於處理註解的 Handler, 這個類的設計也非常有意思。

image這裏有一個 memberValues,它是一個 Map 鍵值對,鍵是我們註解屬性名稱,值就是該屬性當初被賦上的值。
image
image
而這個 invoke 方法就很有意思了,大家注意看,我們的代理類代理了 Hello 接口中所有的方法,所以對於代理類中任何方法的調用都會被轉到這裏來。

var2 指向被調用的方法實例,而這裏首先用變量 var4 獲取該方法的簡明名稱,接着 switch 結構判斷當前的調用方法是誰,如果是 Annotation 中的四大方法,將 var7 賦上特定的值。

如果當前調用的方法是 toString,equals,hashCode,annotationType 的話,AnnotationInvocationHandler 實例中已經預定義好了這些方法的實現,直接調用即可。

那麼假如 var7 沒有匹配上這四種方法,說明當前的方法調用的是自定義註解字節聲明的方法,例如我們 Hello 註解的 value 方法。這種情況下,將從我們的註解 map 中獲取這個註解屬性對應的值。

其實,JAVA 中的註解設計個人覺得有點反人類,明明是屬性的操作,非要用方法來實現。當然,如果你有不同的見解,歡迎留言探討。

最後我們再總結一下整個反射註解的工作原理:

首先,我們通過鍵值對的形式可以爲註解屬性賦值,像這樣:@Hello(value = “hello”)。

接着,你用註解修飾某個元素,編譯器將在編譯期掃描每個類或者方法上的註解,會做一個基本的檢查,你的這個註解是否允許作用在當前位置,最後會將註解信息寫入元素的屬性表。

然後,當你進行反射的時候,虛擬機將所有生命週期在 RUNTIME 的註解取出來放到一個 map 中,並創建一個 AnnotationInvocationHandler 實例,把這個 map 傳遞給它。

最後,虛擬機將採用 JDK 動態代理機制生成一個目標註解的代理類,並初始化好處理器。

那麼這樣,一個註解的實例就創建出來了,它本質上就是一個代理類,你應當去理解好 AnnotationInvocationHandler 中 invoke 方法的實現邏輯,這是核心。一句話概括就是,通過方法名返回註解屬性值

1. @SpringBootApplication

這裏先單獨拎出@SpringBootApplication 註解說一下,雖然我們一般不會主動去使用它。

這個註解是 Spring Boot 項目的基石,創建 SpringBoot 項目之後會默認在主類加上。

@SpringBootApplication
public class SpringSecurityJwtGuideApplication {
      public static void main(java.lang.String[] args) {
        SpringApplication.run(SpringSecurityJwtGuideApplication.class, args);
    }
}

我們可以把 @SpringBootApplication看作是 @Configuration@EnableAutoConfiguration@ComponentScan 註解的集合。

package org.springframework.boot.autoconfigure;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
		@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
   ......
}

package org.springframework.boot;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {

}

根據 SpringBoot 官網,這三個註解的作用分別是:

  • @EnableAutoConfiguration:啓用 SpringBoot 的自動配置機制
  • @ComponentScan: 掃描被@Component (@Service,@Controller)註解的 bean,註解默認會掃描該類所在的包下所有的類。
  • @Configuration:允許在 Spring 上下文中註冊額外的 bean 或導入其他配置類

2. Spring Bean 相關

2.1. @Autowired

自動導入對象到類中,被注入進的類同樣要被 Spring 容器管理比如:Service 類注入到 Controller 類中。

@Service
public class UserService {
  ......
}

@RestController
@RequestMapping("/users")
public class UserController {
   @Autowired
   private UserService userService;
   ......
}

2.2. @Component,@Repository,@Service, @Controller

我們一般使用 @Autowired 註解讓 Spring 容器幫我們自動裝配 bean。要想把類標識成可用於 @Autowired 註解自動裝配的 bean 的類,可以採用以下註解實現:

  • @Component :通用的註解,可標註任意類爲 Spring 組件。如果一個 Bean 不知道屬於哪個層,可以使用@Component 註解標註。
  • @Repository : 對應持久層即 Dao 層,主要用於數據庫相關操作。
  • @Service : 對應服務層,主要涉及一些複雜的邏輯,需要用到 Dao 層。
  • @Controller : 對應 Spring MVC 控制層,主要用戶接受用戶請求並調用 Service 層返回數據給前端頁面。

2.3. @RestController

@RestController註解是@Controller和@ResponseBody的合集,表示這是個控制器 bean,並且是將函數的返回值直接填入 HTTP 響應體中,是 REST 風格的控制器。

單獨使用 @Controller 不加 @ResponseBody的話一般使用在要返回一個視圖的情況,這種情況屬於比較傳統的 Spring MVC 的應用,對應於前後端不分離的情況,一般老項目纔會這麼用了。@Controller +@ResponseBody 返回 JSON 或 XML 形式數據

2.4. @Scope

聲明 Spring Bean 的作用域,使用方法:

@Bean
@Scope("singleton")
public Person personSingleton() {
    return new Person();
}

四種常見的 Spring Bean 的作用域:

  • singleton : 唯一 bean 實例,Spring 中的 bean 默認都是單例的。
  • prototype : 每次請求都會創建一個新的 bean 實例。
  • request : 每一次 HTTP 請求都會產生一個新的 bean,該 bean 僅在當前 HTTP request 內有效。
  • session : 每一次 HTTP 請求都會產生一個新的 bean,該 bean 僅在當前 HTTP session 內有效。

2.5. @Configuration

一般用來聲明配置類,可以使用 @Component註解替代,不過使用Configuration註解聲明配置類更加語義化。

@Configuration
public class AppConfig {
    @Bean
    public TransferService transferService() {
        return new TransferServiceImpl();
    }

}

3. 處理常見的 HTTP 請求類型

5 種常見的請求類型:

  • GET :請求從服務器獲取特定資源。舉個例子:GET /users(獲取所有學生)
  • POST :在服務器上創建一個新的資源。舉個例子:POST /users(創建學生)
  • PUT :更新服務器上的資源(客戶端提供更新後的整個資源)。舉個例子:PUT /users/12(更新編號爲 12 的學生)
  • DELETE :從服務器刪除特定的資源。舉個例子:DELETE /users/12(刪除編號爲 12 的學生)
  • PATCH :更新服務器上的資源(客戶端提供更改的屬性,可以看做作是部分更新),使用的比較少,這裏就不舉例子了。

3.1. GET 請求

@GetMapping("users")` 等價於`@RequestMapping(value="/users",method=RequestMethod.GET)
@GetMapping("/users")
public ResponseEntity<List<User>> getAllUsers() {
 return userRepository.findAll();
}

3.2. POST 請求

@PostMapping("users")` 等價於`@RequestMapping(value="/users",method=RequestMethod.POST)

關於@RequestBody註解的使用,在下面的“前後端傳值”這塊會講到。

@PostMapping("/users")
public ResponseEntity<User> createUser(@Valid @RequestBody UserCreateRequest userCreateRequest) {
 return userRespository.save(user);
}

3.3. PUT 請求

@PutMapping("/users/{userId}")` 等價於`@RequestMapping(value="/users/{userId}",method=RequestMethod.PUT)
@PutMapping("/users/{userId}")
public ResponseEntity<User> updateUser(@PathVariable(value = "userId") Long userId,
  @Valid @RequestBody UserUpdateRequest userUpdateRequest) {
  ......
}

3.4. DELETE 請求

@DeleteMapping("/users/{userId}")`等價於`@RequestMapping(value="/users/{userId}",method=RequestMethod.DELETE)
@DeleteMapping("/users/{userId}")
public ResponseEntity deleteUser(@PathVariable(value = "userId") Long userId){
  ......
}

3.5. PATCH 請求

一般實際項目中,我們都是 PUT 不夠用了之後才用 PATCH 請求去更新數據。

  @PatchMapping("/profile")
  public ResponseEntity updateStudent(@RequestBody StudentUpdateRequest studentUpdateRequest) {
        studentRepository.updateDetail(studentUpdateRequest);
        return ResponseEntity.ok().build();
    }

4. 前後端傳值

掌握前後端傳值的正確姿勢,是你開始 CRUD 的第一步!

4.1. @PathVariable@RequestParam

@PathVariable用於獲取路徑參數,@RequestParam用於獲取查詢參數。

舉個簡單的例子:

@GetMapping("/klasses/{klassId}/teachers")
public List<Teacher> getKlassRelatedTeachers(
         @PathVariable("klassId") Long klassId,
         @RequestParam(value = "type", required = false) String type ) {
...
}

如果我們請求的 url 是:/klasses/{123456}/teachers?type=web

那麼我們服務獲取到的數據就是:klassId=123456,type=web

4.2. @RequestBody

用於讀取 Request 請求(可能是 POST,PUT,DELETE,GET 請求)的 body 部分並且Content-Type 爲 application/json 格式的數據,接收到數據之後會自動將數據綁定到 Java 對象上去。系統會使用HttpMessageConverter或者自定義的HttpMessageConverter將請求的 body 中的 json 字符串轉換爲 java 對象。

我用一個簡單的例子來給演示一下基本使用!

我們有一個註冊的接口:

@PostMapping("/sign-up")
public ResponseEntity signUp(@RequestBody @Valid UserRegisterRequest userRegisterRequest) {
  userService.save(userRegisterRequest);
  return ResponseEntity.ok().build();
}

UserRegisterRequest對象:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserRegisterRequest {
    @NotBlank
    private String userName;
    @NotBlank
    private String password;
    @FullName
    @NotBlank
    private String fullName;
}

我們發送 post 請求到這個接口,並且 body 攜帶 JSON 數據:

{"userName":"coder","fullName":"shuangkou","password":"123456"}

這樣我們的後端就可以直接把 json 格式的數據映射到我們的 UserRegisterRequest 類上。

[img👉 需要注意的是:一個請求方法只可以有一個@RequestBody,但是可以有多個@RequestParam@PathVariable。 如果你的方法必須要用兩個 @RequestBody來接受數據的話,大概率是你的數據庫設計或者系統設計出問題了!

5. 讀取配置信息

很多時候我們需要將一些常用的配置信息比如阿里雲 oss、發送短信、微信認證的相關配置信息等等放到配置文件中。

下面我們來看一下 Spring 爲我們提供了哪些方式幫助我們從配置文件中讀取這些配置信息。

我們的數據源application.yml內容如下::

wuhan2020: 2020年初武漢爆發了新型冠狀病毒,疫情嚴重,但是,我相信一切都會過去!武漢加油!中國加油!

library:
  location: 湖北武漢加油中國加油
  books:
    - name: 天才基本法
      description: 二十二歲的林朝夕在父親確診阿爾茨海默病這天,得知自己暗戀多年的校園男神裴之即將出國深造的消息——對方考取的學校,恰是父親當年爲她放棄的那所。
    - name: 時間的秩序
      description: 爲什麼我們記得過去,而非未來?時間“流逝”意味着什麼?是我們存在於時間之內,還是時間存在於我們之中?卡洛·羅韋利用詩意的文字,邀請我們思考這一亙古難題——時間的本質。
    - name: 了不起的我
      description: 如何養成一個新習慣?如何讓心智變得更成熟?如何擁有高質量的關係? 如何走出人生的艱難時刻?

5.1. @value(常用)

使用 @Value("${property}") 讀取比較簡單的配置信息:

@Value("${wuhan2020}")
String wuhan2020;

5.2. @ConfigurationProperties(常用)

通過@ConfigurationProperties讀取配置信息並與 bean 綁定。

@Component
@ConfigurationProperties(prefix = "library")
@Data
class LibraryProperties {
    @NotEmpty
    private String location;
    private List<Book> books;

    @Data
    @ToString
    static class Book {
        String name;
        String description;
    }
}

可以像使用普通的 Spring bean 一樣,將其注入到類中使用。

5.3. PropertySource(不常用)

@PropertySource讀取指定 properties 文件

@Component
@PropertySource("classpath:website.properties")
@Data
class WebSite {
    @Value("${url}")
    private String url;
}

6. 參數校驗

數據的校驗的重要性就不用說了,即使在前端對數據進行校驗的情況下,我們還是要對傳入後端的數據再進行一遍校驗,避免用戶繞過瀏覽器直接通過一些 HTTP 工具直接向後端請求一些違法數據。

JSR(Java Specification Requests) 是一套 JavaBean 參數校驗的標準,它定義了很多常用的校驗註解,我們可以直接將這些註解加在我們 JavaBean 的屬性上面,這樣就可以在需要校驗的時候進行校驗了,非常方便!

校驗的時候我們實際用的是 Hibernate Validator 框架。Hibernate Validator 是 Hibernate 團隊最初的數據校驗框架,Hibernate Validator 4.x 是 Bean Validation 1.0(JSR 303)的參考實現,Hibernate Validator 5.x 是 Bean Validation 1.1(JSR 349)的參考實現,目前最新版的 Hibernate Validator 6.x 是 Bean Validation 2.0(JSR 380)的參考實現。

SpringBoot 項目的 spring-boot-starter-web 依賴中已經有 hibernate-validator 包,不需要引用相關依賴。

👉 需要注意的是: 所有的註解,推薦使用 JSR 註解,即javax.validation.constraints,而不是org.hibernate.validator.constraints

6.1. 一些常用的字段驗證的註解

  • @NotEmpty 被註釋的字符串的不能爲 null 也不能爲空
  • @NotBlank 被註釋的字符串非 null,並且必須包含一個非空白字符
  • @Null 被註釋的元素必須爲 null
  • @NotNull 被註釋的元素必須不爲 null
  • @AssertTrue 被註釋的元素必須爲 true
  • @AssertFalse 被註釋的元素必須爲 false
  • @Pattern(regex=,flag=)被註釋的元素必須符合指定的正則表達式
  • @Email 被註釋的元素必須是 Email 格式。
  • @Min(value)被註釋的元素必須是一個數字,其值必須大於等於指定的最小值
  • @Max(value)被註釋的元素必須是一個數字,其值必須小於等於指定的最大值
  • @DecimalMin(value)被註釋的元素必須是一個數字,其值必須大於等於指定的最小值
  • @DecimalMax(value) 被註釋的元素必須是一個數字,其值必須小於等於指定的最大值
  • @Size(max=, min=)被註釋的元素的大小必須在指定的範圍內
  • @Digits (integer, fraction)被註釋的元素必須是一個數字,其值必須在可接受的範圍內
  • @Past被註釋的元素必須是一個過去的日期
  • @Future 被註釋的元素必須是一個將來的日期

6.2. 驗證請求體(RequestBody)

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {

    @NotNull(message = "classId 不能爲空")
    private String classId;

    @Size(max = 33)
    @NotNull(message = "name 不能爲空")
    private String name;

    @Pattern(regexp = "((^Man$|^Woman$|^UGM$))", message = "sex 值不在可選範圍")
    @NotNull(message = "sex 不能爲空")
    private String sex;

    @Email(message = "email 格式不正確")
    @NotNull(message = "email 不能爲空")
    private String email;

}

我們在需要驗證的參數上加上了@Valid註解,如果驗證失敗,它將拋出MethodArgumentNotValidException

@RestController
@RequestMapping("/api")
public class PersonController {

    @PostMapping("/person")
    public ResponseEntity<Person> getPerson(@RequestBody @Valid Person person) {
        return ResponseEntity.ok().body(person);
    }
}

6.3. 驗證請求參數(Path Variables 和 Request Parameters)

一定一定不要忘記在類上加上 Validated 註解了,這個參數可以告訴 Spring 去校驗方法參數。

@RestController
@RequestMapping("/api")
@Validated
public class PersonController {

    @GetMapping("/person/{id}")
    public ResponseEntity<Integer> getPersonByID(@Valid @PathVariable("id") @Max(value = 5,message = "超過 id 的範圍了") Integer id) {
        return ResponseEntity.ok().body(id);
    }
}

7. 全局處理 Controller 層異常

介紹一下我們 Spring 項目必備的全局處理 Controller 層異常。

相關注解:

  1. @ControllerAdvice :註解定義全局異常處理類
  2. @ExceptionHandler :註解聲明異常處理方法

如何使用呢?拿我們在第 5 節參數校驗這塊來舉例子。如果方法參數不對的話就會拋出MethodArgumentNotValidException,我們來處理這個異常。

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

    /**
     * 請求參數異常處理
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex, HttpServletRequest request) {
       ......
    }
}

更多關於 Spring Boot 異常處理的內容,請看我的這兩篇文章:

8. JPA 相關

8.1. 創建表

@Entity聲明一個類對應一個數據庫實體。

@Table 設置表明

@Entity
@Table(name = "role")
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String description;
    省略getter/setter......
}

8.2. 創建主鍵

@Id :聲明一個字段爲主鍵。

使用@Id聲明之後,我們還需要定義主鍵的生成策略。我們可以使用 @GeneratedValue 指定主鍵生成策略。

1.通過 @GeneratedValue直接使用 JPA 內置提供的四種主鍵生成策略來指定主鍵生成策略。

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

JPA 使用枚舉定義了 4 中常見的主鍵生成策略,如下:

public enum GenerationType {

    /**
     * 使用一個特定的數據庫表格來保存主鍵
     * 持久化引擎通過關係數據庫的一張特定的表格來生成主鍵,
     */
    TABLE,

    /**
     *在某些數據庫中,不支持主鍵自增長,比如Oracle、PostgreSQL其提供了一種叫做"序列(sequence)"的機制生成主鍵
     */
    SEQUENCE,

    /**
     * 主鍵自增長
     */
    IDENTITY,

    /**
     *把主鍵生成策略交給持久化引擎(persistence engine),
     *持久化引擎會根據數據庫在以上三種主鍵生成 策略中選擇其中一種
     */
    AUTO
}
@GeneratedValue`註解默認使用的策略是`GenerationType.AUTO
public @interface GeneratedValue {

    GenerationType strategy() default AUTO;
    String generator() default "";
}

一般使用 MySQL 數據庫的話,使用GenerationType.IDENTITY策略比較普遍一點(分佈式系統的話需要另外考慮使用分佈式 ID)。

2.通過 @GenericGenerator聲明一個主鍵策略,然後 @GeneratedValue使用這個策略

@Id
@GeneratedValue(generator = "IdentityIdGenerator")
@GenericGenerator(name = "IdentityIdGenerator", strategy = "identity")
private Long id;

等價於:

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

jpa 提供的主鍵生成策略有如下幾種:

public class DefaultIdentifierGeneratorFactory
		implements MutableIdentifierGeneratorFactory, Serializable, ServiceRegistryAwareService {

	@SuppressWarnings("deprecation")
	public DefaultIdentifierGeneratorFactory() {
		register( "uuid2", UUIDGenerator.class );
		register( "guid", GUIDGenerator.class );			// can be done with UUIDGenerator + strategy
		register( "uuid", UUIDHexGenerator.class );			// "deprecated" for new use
		register( "uuid.hex", UUIDHexGenerator.class ); 	// uuid.hex is deprecated
		register( "assigned", Assigned.class );
		register( "identity", IdentityGenerator.class );
		register( "select", SelectGenerator.class );
		register( "sequence", SequenceStyleGenerator.class );
		register( "seqhilo", SequenceHiLoGenerator.class );
		register( "increment", IncrementGenerator.class );
		register( "foreign", ForeignGenerator.class );
		register( "sequence-identity", SequenceIdentityGenerator.class );
		register( "enhanced-sequence", SequenceStyleGenerator.class );
		register( "enhanced-table", TableGenerator.class );
	}

	public void register(String strategy, Class generatorClass) {
		LOG.debugf( "Registering IdentifierGenerator strategy [%s] -> [%s]", strategy, generatorClass.getName() );
		final Class previous = generatorStrategyToClassNameMap.put( strategy, generatorClass );
		if ( previous != null ) {
			LOG.debugf( "    - overriding [%s]", previous.getName() );
		}
	}

}

8.3. 設置字段類型

@Column 聲明字段。

示例:

設置屬性 userName 對應的數據庫字段名爲 user_name,長度爲 32,非空

@Column(name = "user_name", nullable = false, length=32)
private String userName;

設置字段類型並且加默認值,這個還是挺常用的。

Column(columnDefinition = "tinyint(1) default 1")
private Boolean enabled;

8.4. 指定不持久化特定字段

@Transient :聲明不需要與數據庫映射的字段,在保存的時候不需要保存進數據庫 。

如果我們想讓secrect 這個字段不被持久化,可以使用 @Transient關鍵字聲明。

Entity(name="USER")
public class User {

    ......
    @Transient
    private String secrect; // not persistent because of @Transient

}

除了 @Transient關鍵字聲明, 還可以採用下面幾種方法:

static String secrect; // not persistent because of static
final String secrect = “Satish”; // not persistent because of final
transient String secrect; // not persistent because of transient

一般使用註解的方式比較多。

8.5. 聲明大字段

@Lob:聲明某個字段爲大字段。

@Lob
private String content;

更詳細的聲明:

@Lob
//指定 Lob 類型數據的獲取策略, FetchType.EAGER 表示非延遲 加載,而 FetchType. LAZY 表示延遲加載 ;
@Basic(fetch = FetchType.EAGER)
//columnDefinition 屬性指定數據表對應的 Lob 字段類型
@Column(name = "content", columnDefinition = "LONGTEXT NOT NULL")
private String content;

8.6. 創建枚舉類型的字段

可以使用枚舉類型的字段,不過枚舉字段要用@Enumerated註解修飾。

public enum Gender {
    MALE("男性"),
    FEMALE("女性");

    private String value;
    Gender(String str){
        value=str;
    }
}
@Entity
@Table(name = "role")
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String description;
    @Enumerated(EnumType.STRING)
    private Gender gender;
    省略getter/setter......
}

數據庫裏面對應存儲的是 MAIL/FEMAIL。

8.7. 增加審計功能

只要繼承了 AbstractAuditBase的類都會默認加上下面四個字段。

@Data
@AllArgsConstructor
@NoArgsConstructor
@MappedSuperclass
@EntityListeners(value = AuditingEntityListener.class)
public abstract class AbstractAuditBase {

    @CreatedDate
    @Column(updatable = false)
    @JsonIgnore
    private Instant createdAt;

    @LastModifiedDate
    @JsonIgnore
    private Instant updatedAt;

    @CreatedBy
    @Column(updatable = false)
    @JsonIgnore
    private String createdBy;

    @LastModifiedBy
    @JsonIgnore
    private String updatedBy;
}

我們對應的審計功能對應地配置類可能是下面這樣的(Spring Security 項目):

@Configuration
@EnableJpaAuditing
public class AuditSecurityConfiguration {
    @Bean
    AuditorAware<String> auditorAware() {
        return () -> Optional.ofNullable(SecurityContextHolder.getContext())
                .map(SecurityContext::getAuthentication)
                .filter(Authentication::isAuthenticated)
                .map(Authentication::getName);
    }
}

簡單介紹一下上面設計到的一些註解:

  1. @CreatedDate: 表示該字段爲創建時間時間字段,在這個實體被 insert 的時候,會設置值

  2. @CreatedBy :表示該字段爲創建人,在這個實體被 insert 的時候,會設置值

    @LastModifiedDate@LastModifiedBy同理。

@EnableJpaAuditing:開啓 JPA 審計功能。

8.8. 刪除/修改數據

@Modifying 註解提示 JPA 該操作是修改操作,注意還要配合@Transactional註解使用。

@Repository
public interface UserRepository extends JpaRepository<User, Integer> {

    @Modifying
    @Transactional(rollbackFor = Exception.class)
    void deleteByUserName(String userName);
}

8.9. 關聯關係

  • @OneToOne 聲明一對一關係
  • @OneToMany 聲明一對多關係
  • @ManyToOne聲明多對一關係
  • MangToMang聲明多對多關係

9. 事務 @Transactional

在要開啓事務的方法上使用@Transactional註解即可!

@Transactional(rollbackFor = Exception.class)
public void save() {
  ......
}

我們知道 Exception 分爲運行時異常 RuntimeException 和非運行時異常。在@Transactional註解中如果不配置rollbackFor屬性,那麼事物只會在遇到RuntimeException的時候纔會回滾,加上rollbackFor=Exception.class,可以讓事物在遇到非運行時異常時也回滾。

@Transactional 註解一般用在可以作用在或者方法上。

  • 作用於類:當把@Transactional 註解放在類上時,表示所有該類的public 方法都配置相同的事務屬性信息。
  • 作用於方法:當類配置了@Transactional,方法也配置了@Transactional,方法的事務會覆蓋類的事務配置信息。

更多關於關於 Spring 事務的內容請查看:

10. json 數據處理

10.1. 過濾 json 數據

@JsonIgnoreProperties 作用在類上用於過濾掉特定字段不返回或者不解析。

//生成json時將userRoles屬性過濾
@JsonIgnoreProperties({"userRoles"})
public class User {

    private String userName;
    private String fullName;
    private String password;
    @JsonIgnore
    private List<UserRole> userRoles = new ArrayList<>();
}

@JsonIgnore一般用於類的屬性上,作用和上面的@JsonIgnoreProperties 一樣。

public class User {

    private String userName;
    private String fullName;
    private String password;
   //生成json時將userRoles屬性過濾
    @JsonIgnore
    private List<UserRole> userRoles = new ArrayList<>();
}

10.2. 格式化 json 數據

@JsonFormat一般用來格式化 json 數據。:

比如:

@JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone="GMT")
private Date date;

10.3. 扁平化對象

@Getter
@Setter
@ToString
public class Account {
    @JsonUnwrapped
    private Location location;
    @JsonUnwrapped
    private PersonInfo personInfo;

  @Getter
  @Setter
  @ToString
  public static class Location {
     private String provinceName;
     private String countyName;
  }
  @Getter
  @Setter
  @ToString
  public static class PersonInfo {
    private String userName;
    private String fullName;
  }
}

未扁平化之前:

{
    "location": {
        "provinceName":"湖北",
        "countyName":"武漢"
    },
    "personInfo": {
        "userName": "coder1234",
        "fullName": "shaungkou"
    }
}

使用@JsonUnwrapped 扁平對象之後:

@Getter
@Setter
@ToString
public class Account {
    @JsonUnwrapped
    private Location location;
    @JsonUnwrapped
    private PersonInfo personInfo;
    ......
}
{
  "provinceName":"湖北",
  "countyName":"武漢",
  "userName": "coder1234",
  "fullName": "shaungkou"
}

11. 測試相關

@ActiveProfiles一般作用於測試類上, 用於聲明生效的 Spring 配置文件。

@SpringBootTest(webEnvironment = RANDOM_PORT)
@ActiveProfiles("test")
@Slf4j
public abstract class TestBase {
  ......
}

@Test聲明一個方法爲測試方法

@Transactional被聲明的測試方法的數據會回滾,避免污染測試數據。

@WithMockUser Spring Security 提供的,用來模擬一個真實用戶,並且可以賦予權限。

    @Test
    @Transactional
    @WithMockUser(username = "user-id-18163138155", authorities = "ROLE_TEACHER")
    void should_import_student_success() throws Exception {
        ......
    }

Ref

  1. https://www.zhihu.com/question/24401191
  2. https://juejin.im/post/5b45bd715188251b3a1db54f
  3. https://blog.csdn.net/doc_sgl/article/details/50367083
  4. https://github.com/Snailclimb/JavaGuide/blob/master/docs/system-design/framework/spring/spring-annotations.md
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章