最近深圳房價大漲,一個在深圳的學姐也加入了買房的隊伍,什么現在還不買,一天漲一兩百萬,可憐的小宋現在連一個廁所都買不起🤣,還是老老實實繼續寫博客吧,
本章小宋會帶大家去了解使用Spring和Spring Boot的詳細注解,這個章節還是花了小宋比較長的時間,下面要講的基本上也就是大家平常作業時會使用的場景,
注解大全
- @SpringBootAplication
- Spring Bean注解
- @Autowired
- @Component,@Repository,@Service, @Controller注解
- @Resource
- @Resource 和 @Autowired區別
- @RestController
- @Scope
- @Configuration
- 處理常見的HTTP請求注解
- GET請求
- POST請求
- PUT請求
- DELETE請求
- PATCH請求
- 前后端傳值注解
- @PathVariable 和 @RequestParam
- @RequestBody
- 讀取配置資訊注解
- @Value
- @ConfigurationProperties
- @PropertySource
- 引數校驗注解
- 相關依賴
- 物體類
- 一些常用的欄位驗證的注解
- JSR提供的校驗注解:
- Hibernate Validator提供的校驗注解:
- 驗證Controller的輸入
- 驗證請求體(RequestBody)
- 驗證請求引數(Path Variables 和 Request Parameters)
- 驗證 Service 的輸入
- Validator 編程方式手動進行引數驗證
- 自定義 Validator(實用)
- 案例一:校驗特定欄位的值是否在可選范圍
- 案例二:校驗電話號碼
- 使用驗證組
- @NotNull vs @Column(nullable = false)(重要)
- SpringBoot 幾種常見處理例外的方式
- 一.使用 @RestControllerAdvice 和 @ExceptionHandler 處理全域例外
- 1. 新建例外資訊物體類
- 2. 自定義例外型別
- 3. 新建例外處理類
- 4. controller模擬拋出例外
- 5. 撰寫測驗類
- 二.使用 @ExceptionHandler 處理 Controller 級別的例外
- 三.ResponseStatusException
- ResponseStatusException 提供了三個構造方法:
- JPA注解和簡單操作
- 1.相關依賴
- 2.配置資料庫連接資訊和JPA配置
- 3.物體類
- 3.1 創建表
- 3.2 創建主鍵
- 3.3 設定欄位型別
- 3.4 指定不持久化特定欄位
- 3.5 宣告大欄位
- 3.6 創建列舉型別的欄位
- 4.創建操作資料庫的 Repository 介面
- 4.1 JPA自帶方法用例
- 4.1.1 增刪改查
- 4.1.2 條件查詢
- 4.2 JPA自定義Sql陳述句
- 4.3 創建異步方法
- 5.測驗
- JPA連表查詢和分頁
- 1.物體類
- 2. 自定義Sql實作連表查詢
- 2. 自定義 SQL 陳述句連表查詢并實作分頁操作
- 3. IN的操作查詢
- 4. BETWEEN操作查詢
- 5. 測驗
- 事務@Transactional注解使用詳解
- 1.@Transactional 的作用范圍
- 2.@Transactional 的常用配置引數
- 3.@Transactional 事務注解原理
- 4.Spring AOP自呼叫問題
- 5. @Transactional 的使用注意事項
- 6.@Transactional 失效場景
- 6.1 @Transactional 應用在非public修飾的方法上
- 6.2 @Transactional 注解屬性 propagation 設定錯誤
- 6.3 @Transactional 注解屬性 rollbackFor 設定錯誤
- 6.4 同一個類中方法呼叫,導致@Transactional失效
- 6.5 例外被你的 catch“吃了”導致@Transactional失效
- 6.6 資料庫引擎不支持事務
- json資料處理注解
- 1. 過濾json資料
- 2. 格式化json資料
- 3.扁平化物件
- 測驗注解
@SpringBootAplication
首先講一下SpringBoot的基石注解@SpringBootAplication,這個注解使用在啟動類上,
@SpringBootApplication
public class XiaoSongApplication {
public static void main(java.lang.String[] args) {
SpringApplication.run(XiaoSongApplication .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 {
}
作用如下:
- @EnableAutoConfiguration:啟用 SpringBoot 的自動配置機制
- @ComponentScan: 掃描被@Component (@Service,@Controller)注解的 bean,注解默認會掃描該類所在的包下所有的類,
- @Configuration:允許在 Spring 背景關系中注冊額外的 bean 或匯入其他配置類
Spring Bean注解
這里小宋詳細講一下Spring Bean相關的一些注解,
@Autowired
@Autowired 自動裝配Bean,注解會自動匯入物件到類中,被注入進的類同樣要被 Spring 容器管理比如:Service 類注入到 Controller 類中,
@Service
public class UserService {
......
}
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
......
}
@Component,@Repository,@Service, @Controller注解
大家一般使用 @Autowired 注解讓 Spring 容器幫我們自動裝配 bean,要想把類標識成可用于 @Autowired 注解自動裝配的 bean 的類,可以采用以下注解實作:
- @Component :通用的注解,可標注任意類為 Spring 組件,如果一個 Bean 不知道屬于哪個層,可以使用@Component 注解標注,
- @Repository : 對應持久層即 Dao 層,主要用于資料庫相關操作,
- @Service : 對應服務層,主要涉及一些復雜的邏輯,需要用到 Dao 層,
- @Controller : 對應 Spring MVC 控制層,主要用于接受用戶請求并呼叫 Service 層回傳資料給前端頁面,
@Resource
spring不但支持自己定義的@Autowired注解,還支持幾個由JSR-250規范定義的注解,它們分別是@Resource、@PostConstruct以及@PreDestroy,
@Resource的作用相當于@Autowired,只不過@Autowired按byType自動注入,而@Resource默認按 byName自動注入罷了,@Resource有兩個屬性是比較重要的,分是name和type,Spring將@Resource注解的name屬性決議為bean的名字,而type屬性則決議為bean的型別,所以如果使用name屬性,則使用byName的自動注入策略,而使用type屬性時則使用byType自動注入策略,如果既不指定name也不指定type屬性,這時將通過反射機制使用byName自動注入策略,
@Resource裝配順序
1. 如果同時指定了name和type,則從Spring背景關系中找到唯一匹配的bean進行裝配,找不到則拋出例外
2. 如果指定了name,則從背景關系中查找名稱(id)匹配的bean進行裝配,找不到則拋出例外
3. 如果指定了type,則從背景關系中找到型別匹配的唯一bean進行裝配,找不到或者找到多個,都會拋出例外
4. 如果既沒有指定name,又沒有指定type,則自動按照byName方式進行裝配;如果沒有匹配,則回退為一個原始型別進行匹配,如果匹配則自動裝配;
@Resource 和 @Autowired區別
-
@Autowired與@Resource都可以用來裝配bean. 都是用來實作依賴注入的.
-
@Autowired默認按型別裝配(這個注解是屬于spring的,spring提供),@AutoWried按byType自動注入,默認情況下必須要求依賴物件必須存在,如果要允許null值,可以設定它的required屬性為false,如:@Autowired(required=false) ,如果我們想使用名稱裝配可以結合@Qualifier注解進行使用
-
@Resource(這個注解屬于J2EE的,jdk提供),@Resource默認按byName自動注入,默認按照名稱進行裝配,名稱可以通過name屬性進行指定,如果沒有指定name屬性,當注解寫在欄位上時,默認取欄位名進行按照名稱查找,如果注解寫在setter方法上默認取屬性名進行裝配,當找不到與名稱匹配的bean時才按照型別進行裝配,但是需要注意的是,如果name屬性一旦指定,就只會按照名稱進行裝配,
推薦使用:@Resource注解在欄位上,這樣就不用寫setter方法了,并且這個注解是屬于J2EE的,減少了與spring的耦合,這樣代碼看起就比較優雅,
@RestController
@RestController注解是@Controller和@ResponseBody的合集,表示這是個控制器 bean,并且是將函式的回傳值去直接填入 HTTP 回應體中,是 REST 風格的控制器,
現在都是前后端分離,說實話我已經很久沒有用過@Controller,當然如果你的專案太老了的話,還是有可能,
單獨使用 @Controller 不加 @ResponseBody的話一般使用在要回傳一個視圖的情況,這種情況屬于比較傳統的 Spring MVC 的應用,對應于前后端不分離的情況,@Controller +@ResponseBody 回傳 JSON 或 XML 形式資料
@Scope
@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 內有效,
@Configuration
一般用來宣告配置類,可以使用 @Component注解替代,不過使用@Configuration注解宣告配置類更加語意化,
@Configuration
public class AppConfig {
@Bean
public TransferService transferService() {
return new TransferServiceImpl();
}
}
處理常見的HTTP請求注解
下面我們來看看常見的5種http請求型別:
- GET :請求從服務器獲取特定資源,舉個例子:GET /users(獲取所有學生)
- POST :在服務器上創建一個新的資源,舉個例子:POST /users(創建學生)
- PUT :更新服務器上的資源(客戶端提供更新后的整個資源),舉個例子:PUT /users/12(更新編號為 12 的學生)
- DELETE :從服務器洗掉特定的資源,舉個例子:DELETE /users/12(洗掉編號為 12 的學生)
- PATCH :更新服務器上的資源(客戶端提供更改的屬性,可以看做作是部分更新),使用的比較少,下面就不舉例子了
GET請求
@GetMapping("/list") == @RequestMapping(value="/list",method=RequestMethod.GET)
@GetMapping("/list")
public Result<Map<String,Object>> getList() {
IPage<CreditsDetailPageVo> pageList=iCreditsDetailService.pageList(new Page<Map<String,Object>>(pageNo, pageSize),map);
map.put("pageList",pageList);
result.setResult(map);
result.setSuccess(true);
return result;
}
POST請求
@PostMapping(“users”) == @RequestMapping(value="/users",method=RequestMethod.POST)
@PostMapping("/users")
public ResponseEntity<User> createUser(@Valid @RequestBody UserCreateRequest userCreateRequest) {
return userRespository.save(user);
}
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) {
......
}
DELETE請求
@DeleteMapping("/users/{userId}") == @RequestMapping(value="/users/{userId}",method=RequestMethod.DELETE)
@DeleteMapping("/users/{userId}")
public ResponseEntity deleteUser(@PathVariable(value = "userId") Long userId){
......
}
PATCH請求
一般在專案中,我們都是 PUT 不夠用了之后才用 PATCH 請求去更新資料,
@PatchMapping("/profile")
public ResponseEntity updateStudent(@RequestBody StudentUpdateRequest studentUpdateRequest) {
studentRepository.updateDetail(studentUpdateRequest);
return ResponseEntity.ok().build();
}
前后端傳值注解
掌握前后端的傳值,是我們開始 CRUD 的第一步!
@PathVariable 和 @RequestParam
@PathVariable用于獲取路徑引數,@RequestParam請求引數可用于獲取查詢引數,
eg:
@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的相關資料,
@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;
@NotBlank
private String fullName;
}
我們發送 post 請求到這個介面,并且 body 攜帶 JSON 資料:
{"userName":"coder","fullName":"shuangkou","password":"123456"}
然后后端直接把 json 格式的資料映射到我們的 UserRegisterRequest 類上,
注意:一個請求方法只可以有一個@RequestBody,但是可以有多個@RequestParam和@PathVariable, 如果你的方法必須要用兩個 @RequestBody來接受資料的話,大概率是你的資料庫設計或者系統設計出問題了!
讀取配置資訊注解
很多時候我們需要將一些常用的配置資訊比如阿里云 oss 配置、發送短信的相關資訊配置等等放到組態檔中,
下面我們來看一下 Spring 為我們提供了哪些方式幫助我們從組態檔中讀取這些配置資訊,
組態檔內容如下:
server:
port: 8080
tomcat:
max-swallow-size: -1
servlet:
context-path: /vyun
compression:
enabled: true
mime-types: application/javascript,application/json,application/xml,text/html,text/xml,text/plain,text/css,image/*
management:
endpoints:
web:
exposure:
include: metrics,httptrace
spring:
servlet:
multipart:
max-file-size: 1024MB
max-request-size: 1024MB
## quartz定時任務,采用資料庫方式
quartz:
job-store-type: jdbc
#json 時間戳統一轉換
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
#介面回傳的資料中不含value為null的key
#default-property-inclusion: non_null
使用 @Value("${property}") 讀取
@Value
@Value我們常用來讀取比較簡單的配置資訊
@Value("${server.port}")
int port;
@ConfigurationProperties
使用@ConfigurationProperties讀取配置資訊并與 bean 系結,
@Component
@ConfigurationProperties(prefix = "spring.servlet.multipart")
public class MultipartProperties {
@NotEmpty
private String maxFileSize; // 組態檔中是max-file-size, 轉駝峰命名便可以系結成
private String maxRequestSize;
省略getter/setter
......
}
你可以像使用普通的 Spring bean 一樣,將其注入到類中使用,
@PropertySource
這個注解不怎么常用,但還是講一下,
@PropertySource讀取指定 properties 檔案
@Component
@PropertySource("classpath:config.properties")
public class Config {
@Value("${url}")
private String url;
省略getter/setter
......
}
引數校驗注解
資料的校驗在程式中的重要性,即使在前端對資料進行校驗的情況下,我們還是要對傳入后端的資料再進行一遍校驗,避免用戶繞過瀏覽器直接通過一些 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)的參考實作,
相關依賴
如果開發普通的Java程式的話,依賴入下:
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.9.Final</version>
</dependency>
<dependency>
<groupId>javax.el</groupId>
<artifactId>javax.el-api</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>javax.el</artifactId>
<version>2.2.6</version>
</dependency>
SpringBoot 專案的 spring-boot-starter-web 依賴中已經有 hibernate-validator 包,除了這個依賴,下面的演示還用到了 lombok ,所以不要忘記添加上相關依賴,如下:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

注意一下: 所有的注解,推薦使用 JSR 注解,即javax.validation.constraints,而不是org.hibernate.validator.constraints
物體類
@Data
@AllArgsConstructor
@NoArgsConstructor
publicclass 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;
}
一些常用的欄位驗證的注解
JSR提供的校驗注解:
- @Null 被注釋的元素必須為 null
- @NotNull 被注釋的元素必須不為 null
- @AssertTrue 被注釋的元素必須為 true
- @AssertFalse 被注釋的元素必須為 false
- @Min(value) 被注釋的元素必須是一個數字,其值必須大于等于指定的最小值
- @Max(value) 被注釋的元素必須是一個數字,其值必須小于等于指定的最大值
- @DecimalMin(value) 被注釋的元素必須是一個數字,其值必須大于等于指定的最小值
- @DecimalMax(value) 被注釋的元素必須是一個數字,其值必須小于等于指定的最大值
- @Size(max=, min=) 被注釋的元素的大小必須在指定的范圍內
- @Digits (integer, fraction) 被注釋的元素必須是一個數字,其值必須在可接受的范圍內
- @Past 被注釋的元素必須是一個過去的日期
- @Future 被注釋的元素必須是一個將來的日期
- @Pattern(regex=,flag=) 被注釋的元素必須符合指定的正則運算式
正則運算式說明:- ^string : 匹配以 string 開頭的字串
- string$ :匹配以 string 結尾的字串
- ^string$ :精確匹配 string 字串
- ((Man$|^Woman$|UGM$)) : 值只能在 Man,Woman,UGM 這三個值中選擇
Hibernate Validator提供的校驗注解:
- @NotBlank(message =) 驗證字串非null,且長度必須大于0
- @Email 被注釋的元素必須是電子郵箱地址
- @Length(min=,max=) 被注釋的字串的大小必須在指定的范圍內
- @NotEmpty 被注釋的字串的必須非空
- @Range(min=,max=,message=) 被注釋的元素必須在合適的范圍內
驗證Controller的輸入
驗證請求體(RequestBody)
Controller:
我們在需要驗證的引數上加上了@Valid注解,如果驗證失敗,它將拋出MethodArgumentNotValidException,默認情況下,Spring會將此例外轉換為HTTP Status 400(錯誤請求),
@RestController
@RequestMapping("/api")
publicclass PersonController {
@PostMapping("/person")
public ResponseEntity<Person> getPerson(@RequestBody @Valid Person person) {
return ResponseEntity.ok().body(person);
}
}
ExceptionHandler:
自定義例外處理器可以幫助我們捕獲例外,并進行一些簡單的處理,
@ControllerAdvice(assignableTypes = {PersonController.class})
publicclass GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors);
}
}
通過測驗驗證:
下面我通過 MockMvc 模擬請求 Controller 的方式來驗證是否生效,也可以通過 Postman 工具來驗證,
- 所有引數輸入正確的情況:
@RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc publicclass PersonControllerTest { @Autowired private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; @Test public void should_get_person_correctly() throws Exception { Person person = new Person(); person.setName("SnailClimb"); person.setSex("Man"); person.setClassId("82938390"); person.setEmail("Snailclimb@qq.com"); mockMvc.perform(post("/api/person") .contentType(MediaType.APPLICATION_JSON_UTF8) .content(objectMapper.writeValueAsString(person))) .andExpect(MockMvcResultMatchers.jsonPath("name").value("SnailClimb")) .andExpect(MockMvcResultMatchers.jsonPath("classId").value("82938390")) .andExpect(MockMvcResultMatchers.jsonPath("sex").value("Man")) .andExpect(MockMvcResultMatchers.jsonPath("email").value("Snailclimb@qq.com")); } - 驗證出現引數不合法的情況拋出例外并且可以正確被捕獲,
使用 Postman 驗證結果如下:@Test public void should_check_person_value() throws Exception { Person person = new Person(); person.setSex("Man22"); person.setClassId("82938390"); person.setEmail("SnailClimb"); mockMvc.perform(post("/api/person") .contentType(MediaType.APPLICATION_JSON_UTF8) .content(objectMapper.writeValueAsString(person))) .andExpect(MockMvcResultMatchers.jsonPath("sex").value("sex 值不在可選范圍")) .andExpect(MockMvcResultMatchers.jsonPath("name").value("name 不能為空")) .andExpect(MockMvcResultMatchers.jsonPath("email").value("email 格式不正確")); }

驗證請求引數(Path Variables 和 Request Parameters)
Controller:
一定一定不要忘記在類上加上 Validated 注解了,這個引數可以告訴 Spring 去校驗方法引數,
@RestController
@RequestMapping("/api")
@Validated
publicclass PersonController {
@GetMapping("/person/{id}")
public ResponseEntity<Integer> getPersonByID(@Valid @PathVariable("id") @Max(value = 5,message = "超過 id 的范圍了") Integer id) {
return ResponseEntity.ok().body(id);
}
@PutMapping("/person")
public ResponseEntity<String> getPersonByName(@Valid @RequestParam("name") @Size(max = 6,message = "超過 name 的范圍了") String name) {
return ResponseEntity.ok().body(name);
}
}
ExceptionHandler:
@ExceptionHandler(ConstraintViolationException.class)
ResponseEntity<String> handleConstraintViolationException(ConstraintViolationException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
}
通過測驗驗證:
@Test
public void should_check_param_value() throws Exception {
mockMvc.perform(get("/api/person/6")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().isBadRequest())
.andExpect(content().string("getPersonByID.id: 超過 id 的范圍了"));
}
@Test
public void should_check_param_value2() throws Exception {
mockMvc.perform(put("/api/person")
.param("name","snailclimbsnailclimb")
.contentType(MediaType.APPLICATION_JSON_UTF8))
.andExpect(status().isBadRequest())
.andExpect(content().string("getPersonByName.name: 超過 name 的范圍了"));
}
驗證 Service 的輸入
我們還可以驗證任何Spring組件的輸入,而不是驗證控制器級別的輸入,我們可以使用 @Validated 和 @Valid 注釋的組合來實作這一需求,
一定一定不要忘記在類上加上 Validated 注解了,這個引數可以告訴 Spring 去校驗方法引數,
@Service
@Validated
public class PersonService {
public void validatePerson(@Valid Person person){
// do something
}
}
測驗:
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
publicclass PersonServiceTest {
@Autowired
private PersonService service;
@Test(expected = ConstraintViolationException.class)
public void should_throw_exception_when_person_is_not_valid() {
Person person = new Person();
person.setSex("Man22");
person.setClassId("82938390");
person.setEmail("SnailClimb");
service.validatePerson(person);
}
}
Validator 編程方式手動進行引數驗證
某些場景下可能會需要我們手動校驗并獲得校驗結果,
@Test
public void check_person_manually() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Person person = new Person();
person.setSex("Man22");
person.setClassId("82938390");
person.setEmail("SnailClimb");
Set<ConstraintViolation<Person>> violations = validator.validate(person);
//output:
//email 格式不正確
//name 不能為空
//sex 值不在可選范圍
for (ConstraintViolation<Person> constraintViolation : violations) {
System.out.println(constraintViolation.getMessage());
}
}
上面我們是通過 Validator 工廠類獲得的 Validator 示例,當然你也可以通過 @Autowired 直接注入的方式,但是在非 Spring Component 類中使用這種方式的話,只能通過工廠類來獲得 Validator,
@Autowired
Validator validate
自定義 Validator(實用)
如果自帶的校驗注解無法滿足你的需求的話,你還可以自定義實作注解,
案例一:校驗特定欄位的值是否在可選范圍
比如我們現在多了這樣一個需求:Person類多了一個 region 欄位,region 欄位只能是China、China-Taiwan、China-HongKong這三個中的一個,
-
第一步需要創建一個注解:
@Target({FIELD}) @Retention(RUNTIME) @Constraint(validatedBy = RegionValidator.class) @Documented public@interface Region { String message() default "Region 值不在可選范圍內"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; } -
第二步你需要實作 ConstraintValidator介面,并重寫isValid 方法:
import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.util.HashSet; publicclass RegionValidator implements ConstraintValidator<Region, String> { @Override public boolean isValid(String value, ConstraintValidatorContext context) { HashSet<Object> regions = new HashSet<>(); regions.add("China"); regions.add("China-Taiwan"); regions.add("China-HongKong"); return regions.contains(value); } }
現在你就可以使用這個注解:
@Region
private String region;
案例二:校驗電話號碼
校驗我們的電話號碼是否合法,這個可以通過正則運算式來做,相關的正則運算式都可以在網上搜到,你甚至可以搜索到針對特定運營商電話號碼段的正則運算式,
PhoneNumber.java:
import javax.validation.Constraint;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
importstatic java.lang.annotation.ElementType.FIELD;
importstatic java.lang.annotation.ElementType.PARAMETER;
importstatic java.lang.annotation.RetentionPolicy.RUNTIME;
@Documented
@Constraint(validatedBy = PhoneNumberValidator.class)
@Target({FIELD, PARAMETER})
@Retention(RUNTIME)
public@interface PhoneNumber {
String message() default "Invalid phone number";
Class[] groups() default {};
Class[] payload() default {};
}
PhoneNumberValidator.java:
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
publicclass PhoneNumberValidator implements ConstraintValidator<PhoneNumber,String> {
@Override
public boolean isValid(String phoneField, ConstraintValidatorContext context) {
if (phoneField == null) {
// can be null
return true;
}
return phoneField.matches("^1(3[0-9]|4[57]|5[0-35-9]|8[0-9]|70)\\d{8}$") && phoneField.length() > 8 && phoneField.length() < 14;
}
}
現在就可以使用這個注解了,
@PhoneNumber(message = "phoneNumber 格式不正確")
@NotNull(message = "phoneNumber 不能為空")
private String phoneNumber;
使用驗證組
某些場景下我們需要使用到驗證組,這樣說可能不太清楚,說簡單點就是對物件操作的不同方法有不同的驗證規則,示例如下(這個就我目前經歷的專案來說使用的比較少,因為本身這個在代碼層面理解起來是比較麻煩的,然后寫起來也比較麻煩),
先創建兩個介面
public interface AddPersonGroup {
}
public interface DeletePersonGroup {
}
接著使用如下:
@NotNull(groups = DeletePersonGroup.class)
@Null(groups = AddPersonGroup.class)
private String group;
@Service
@Validated
publicclass PersonService {
public void validatePerson(@Valid Person person) {
// do something
}
@Validated(AddPersonGroup.class)
public void validatePersonGroupForAdd(@Valid Person person) {
// do something
}
@Validated(DeletePersonGroup.class)
public void validatePersonGroupForDelete(@Valid Person person) {
// do something
}
}
測驗:
@Test(expected = ConstraintViolationException.class)
public void should_check_person_with_groups() {
Person person = new Person();
person.setSex("Man22");
person.setClassId("82938390");
person.setEmail("SnailClimb");
person.setGroup("group1");
service.validatePersonGroupForAdd(person);
}
@Test(expected = ConstraintViolationException.class)
public void should_check_person_with_groups2() {
Person person = new Person();
person.setSex("Man22");
person.setClassId("82938390");
person.setEmail("SnailClimb");
service.validatePersonGroupForDelete(person);
}
使用驗證組這種方式的時候一定要小心,這是一種反模式,還會造成代碼邏輯性變差,
@NotNull vs @Column(nullable = false)(重要)
在使用 JPA 操作資料的時候會經常碰到 @Column(nullable = false) 這種型別的約束,那么它和 @NotNull 有何區別呢?搞清楚這個還是很重要的!
- @NotNull是 JSR 303 Bean驗證注解,它與資料庫約束本身無關,
- @Column(nullable = false) : 是JPA宣告列為非空的方法,
總結來說就是即前者用于驗證,而后者則用于指示資料庫創建表的時候對表的約束,
SpringBoot 幾種常見處理例外的方式
這里講一下spring boot 3種常見的處理例外的方式,
一.使用 @RestControllerAdvice 和 @ExceptionHandler 處理全域例外
Spring 專案必備的全域處理 Controller 層例外,
這是目前很常用的一種方式,非常推薦,測驗中用到了 Junit 5,如果你新建專案驗證下面的代碼的話,記得添加上相關依賴,
1. 新建例外資訊物體類
非必要的類,主要用于包裝例外資訊,
/**
* @author song
*/
public class ErrorResponse {
private String message;
private String errorTypeName;
public ErrorResponse(Exception e) {
this(e.getClass().getName(), e.getMessage());
}
public ErrorResponse(String errorTypeName, String message) {
this.errorTypeName = errorTypeName;
this.message = message;
}
......省略getter/setter方法
}
2. 自定義例外型別
一般我們處理的都是 RuntimeException ,所以如果你需要自定義例外型別的話直接集成這個類就可以了,
/**
* @author song
* 自定義例外型別
*/
public class ResourceNotFoundException extends RuntimeException {
private String message;
public ResourceNotFoundException() {
super();
}
public ResourceNotFoundException(String message) {
super(message);
this.message = message;
}
public ResourceNotFoundException(Throwable cause){
super(cause);
}
public ResourceNotFoundException(String message,Throwable cause){
super(message,cause);
}
@Override
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
3. 新建例外處理類
相關的3個注解如下:
- @ControllerAdvice :注解定義全域例外處理類
- @RestControllerAdvice :注解定義全域例外處理類
- @ExceptionHandler :注解宣告例外處理方法
ControllerAdvice 和 RestControllerAdvice 區別也就是和 Controller 與 RestController 一樣,現在基本都是Spring boot 和rest風格開發用的是@RestControllerAdvice,還可以用assignableTypes指定特定的 Controller 類
/**
* @author song
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
ErrorResponse illegalArgumentResponse = new ErrorResponse(new IllegalArgumentException("引數錯誤!"));
ErrorResponse resourseNotFoundResponse = new ErrorResponse(new ResourceNotFoundException("Sorry, the resourse not found!"));
// 攔截所有例外, 這里只是為了演示,一般情況下一個方法特定處理一種例外
@ExceptionHandler(value = Exception.class)
public ResponseEntity<ErrorResponse> exceptionHandler(Exception e) {
if (e instanceof IllegalArgumentException) {
return ResponseEntity.status(400).body(illegalArgumentResponse);
} else if (e instanceof ResourceNotFoundException) {
return ResponseEntity.status(404).body(resourseNotFoundResponse);
}
return null;
}
}
4. controller模擬拋出例外
/**
* @author song
*/
@RestController
@RequestMapping("/api")
public class ExceptionController {
@GetMapping("/illegalArgumentException")
public void throwException() {
throw new IllegalArgumentException();
}
@GetMapping("/resourceNotFoundException")
public void throwException2() {
throw new ResourceNotFoundException();
}
}
使用 Get 請求 localhost:8080/api/resourceNotFoundException(curl -i -s -X GET url),服務端回傳的 JSON 資料如下:
{
"message": "Sorry, the resourse not found!",
"errorTypeName": "com.saijia.common.exception.ResourceNotFoundException"
}
5. 撰寫測驗類
MockMvc 由org.springframework.boot.test包提供,實作了對Http請求的模擬,一般用于我們測驗 controller 層,
/**
* @author song
*/
@AutoConfigureMockMvc
@SpringBootTest
public class ExceptionTest {
@Autowired
MockMvc mockMvc;
@Test
void should_return_400_if_param_not_valid() throws Exception {
mockMvc.perform(get("/api/illegalArgumentException"))
.andExpect(status().is(400))
.andExpect(jsonPath("$.message").value("引數錯誤!"));
}
@Test
void should_return_404_if_resourse_not_found() throws Exception {
mockMvc.perform(get("/api/resourceNotFoundException"))
.andExpect(status().is(404))
.andExpect(jsonPath("$.message").value("Sorry, the resourse not found!"));
}
}
二.使用 @ExceptionHandler 處理 Controller 級別的例外
上面也說了使用@ControllerAdvice注解 可以通過 assignableTypes指定特定的類,讓例外處理類只處理特定類拋出的例外,實際上這種方式現在使用的比較少了,這里還是舉一下例子,
/**
* @author shuang.kou
*/
@ControllerAdvice(assignableTypes = {ExceptionController.class})
@ResponseBody
public class GlobalExceptionHandler {
ErrorResponse illegalArgumentResponse = new ErrorResponse(new IllegalArgumentException("引數錯誤!"));
ErrorResponse resourseNotFoundResponse = new ErrorResponse(new ResourceNotFoundException("Sorry, the resourse not found!"));
@ExceptionHandler(value = Exception.class)// 攔截所有例外, 這里只是為了演示,一般情況下一個方法特定處理一種例外
public ResponseEntity<ErrorResponse> exceptionHandler(Exception e) {
if (e instanceof IllegalArgumentException) {
return ResponseEntity.status(400).body(illegalArgumentResponse);
} else if (e instanceof ResourceNotFoundException) {
return ResponseEntity.status(404).body(resourseNotFoundResponse);
}
return null;
}
}
controller模擬拋出例外
/**
* @author song
*/
@RestController
@RequestMapping("/api")
public class ExceptionController {
@GetMapping("/illegalArgumentException")
public void throwException() {
throw new IllegalArgumentException();
}
@GetMapping("/resourceNotFoundException")
public void throwException2() {
throw new ResourceNotFoundException();
}
}
三.ResponseStatusException
研究 ResponseStatusException 我們先來看看,通過 ResponseStatus注解簡單處理例外的方法(將例外映射為狀態碼),
@ResponseStatus(code = HttpStatus.NOT_FOUND)
public class ResourseNotFoundException2 extends RuntimeException {
public ResourseNotFoundException2() {
}
public ResourseNotFoundException2(String message) {
super(message);
}
}
Controller 層拋出例外測驗
@RestController
@RequestMapping("/api")
public class ResponseStatusExceptionController {
@GetMapping("/resourceNotFoundException2")
public void throwException3() {
throw new ResourseNotFoundException2("Sorry, the resourse not found!");
}
}
使用 Get 請求 localhost:8080/api/resourceNotFoundException2 ,服務端回傳的 JSON 資料如下:
{
"timestamp": "2020-12-08T07:11:43.744+0000",
"status": 404,
"error": "Not Found",
"message": "Sorry, the resourse not found!",
"path": "/api/resourceNotFoundException2"
}
通過 ResponseStatus注解簡單處理例外的方法的好處是比較簡單,但是一般我們不會這樣做,通過ResponseStatusException會更加方便,可以避免我們額外的例外類,
@GetMapping("/resourceNotFoundException2")
public void throwException3() {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Sorry, the resourse not found!", new ResourceNotFoundException());
}
使用 Get 請求 localhost:8080/api/resourceNotFoundException2 ,服務端回傳的 JSON 資料如下,和使用 ResponseStatus 實作的效果一樣:
{
"timestamp": "2020-12-08T07:20:53.017+0000",
"status": 404,
"error": "Not Found",
"message": "Sorry, the resourse not found!",
"path": "/api/resourceNotFoundException2"
}
ResponseStatusException 提供了三個構造方法:
public ResponseStatusException(HttpStatus status) {
this(status, null, null);
}
public ResponseStatusException(HttpStatus status, @Nullable String reason) {
this(status, reason, null);
}
public ResponseStatusException(HttpStatus status, @Nullable String reason, @Nullable Throwable cause) {
super(null, cause);
Assert.notNull(status, "HttpStatus is required");
this.status = status;
this.reason = reason;
}
建構式中的引數:
- status :http status
- reason :response 的訊息內容
- cause :拋出的例外
JPA注解和簡單操作
這里小宋講解一下Spring Data Jpa的相關知識,
1.相關依賴
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
2.配置資料庫連接資訊和JPA配置
下面的配置中要先去單獨說一下 spring.jpa.hibernate.ddl-auto=create這個配置選項,
該屬性常用的選項包含4種:
- create:每次重新啟動專案都會重新創建新表結構,會導致資料丟失
- create-drop:每次啟動專案創建表結構,關閉專案洗掉表結構
- update:每次啟動專案會更新表結構
- validate:驗證表結構,不對資料庫進行任何更改
一定要不要在生產環境使用 ddl 自動生成表結構,一般推薦手寫 SQL 陳述句配合 Flyway 來做這些事情,
spring.datasource.url=jdbc:mysql://localhost:3306/springboot_jpa?useSSL=false&serverTimezone=CTT
spring.datasource.username=root
spring.datasource.password=123456
# 列印出 sql 陳述句
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=create
spring.jpa.open-in-view=false
# 創建的表的 ENGINE 為 InnoDB
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL55Dialect
3.物體類
我們為這個類添加 @Entity 注解代表它是資料庫持久化類,并配置主鍵 id,
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
@Data
@NoArgsConstructor
publicclass Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String name;
private Integer age;
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
}
3.1 創建表
@Entity:宣告一個類對應一個資料庫物體,
@Table:設定表名
@NoArgsConstructor:無參構造方法
@Entity
@Data
@NoArgsConstructor
publicclass Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String name;
private Integer age;
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
3.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() );
}
}
}
3.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;
3.4 指定不持久化特定欄位
@Transient :宣告不需要與資料庫映射的欄位,在保存的時候不需要保存進資料庫 ,
如果我們想讓secrect 這個欄位不被持久化,可以使用 @Transient關鍵字宣告,
@Entity(name="USER")
public class User {
......
@Transient
private String secrect; // not persistent because of @Transient
}
除了 @Transient關鍵字宣告, 還可以采用下面3種方法:
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
但是一般注解的方式使用的比較多
3.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;
3.6 創建列舉型別的欄位
public enum Gender {
MALE("男性"),
FEMALE("女性");
private String value;
Gender(String str){
value=str;
}
}
可以通過@Enumerated注解去使用列舉型別的欄位
@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,
如何驗證我們已經成功,運行專案后,查看控制臺是否列印出創建表的 sql 陳述句,并且資料庫中表真的被創建出來的話,說明你成功了,
控制臺列印出來的 sql 陳述句類似下面這樣:
droptableifexists person
CREATETABLE`person` (
`id`bigint(20) NOTNULL AUTO_INCREMENT,
`age`int(11) DEFAULTNULL,
`name`varchar(255) DEFAULTNULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDBDEFAULTCHARSET=utf8;
altertable person addconstraint UK_p0wr4vfyr2lyifm8avi67mqw5 unique (name)
4.創建操作資料庫的 Repository 介面
@Repository
public interface PersonRepository extends JpaRepository<Person, Long> {
}
首先這個介面加了 @Repository 注解,代表它和資料庫操作有關,另外,它繼承了 JpaRepository<Person, Long>介面,而JpaRepository<Person, Long>原始碼如下:
@NoRepositoryBean
publicinterface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
List<T> findAll();
List<T> findAll(Sort var1);
List<T> findAllById(Iterable<ID> var1);
<S extends T> List<S> saveAll(Iterable<S> var1);
void flush();
<S extends T> S saveAndFlush(S var1);
void deleteInBatch(Iterable<T> var1);
void deleteAllInBatch();
T getOne(ID var1);
<S extends T> List<S> findAll(Example<S> var1);
<S extends T> List<S> findAll(Example<S> var1, Sort var2);
}
所以當我們繼承了JpaRepository<T, ID> ,也就可以去呼叫 JPA 為我們提供好的增刪改查、分頁查詢以及根據條件查詢等方法,
4.1 JPA自帶方法用例
4.1.1 增刪改查
- 增(保存用戶到資料庫)
Person person = new Person("SnailClimb", 23);
personRepository.save(person);
save()方法對應 sql 陳述句是: insert into person (age, name) values (23,"snailclimb")
- 查(根據id查找用戶)
Optional<Person> personOptional = personRepository.findById(id);
findById()方法對應 sql 陳述句就是:select * from person p where p.id = id
- 刪(根據id洗掉用戶)
personRepository.deleteById(id);
deleteById()方法對應 sql 陳述句就是:delete from person where id=id
- 改(根據id更新用戶資訊)
更新操作也是通過save()方法實作,
Person person = new Person("SnailClimb", 23);
Person savedPerson = personRepository.save(person);
// 更新 person 物件的姓名
savedPerson.setName("UpdatedName");
personRepository.save(savedPerson);
這里是先新增再去更新,也可以先查出一個用戶的資訊再去更新,
這里最后的save()方法相當于 sql 陳述句:update person set name="UpdatedName" where id=id
4.1.2 條件查詢
下面這些方法是根據 JPA 提供的語法自定義的,你需要將下面這些方法寫到 PersonRepository 中,
假如我們想要根據 Name 來查找 Person ,你可以這樣:
Optional<Person> findByName(String name);
如果你想要找到年齡大于某個值的人,你可以這樣:
List<Person> findByAgeGreaterThan(int age);
4.2 JPA自定義Sql陳述句
很多時候我們自定義 sql 陳述句會非常有用,
根據 name 來查找 Person:
@Query("select p from Person p where p.name = :name")
Optional<Person> findByNameCustomeQuery(@Param("name") String name);
Person 部分屬性查詢,避免 select *操作:
@Query("select p.name from Person p where p.id = :id")
String findPersonNameById(@Param("id") Long id);
根據 id 更新Person name:
@Modifying
@Transactional
@Query("update Person p set p.name = ?1 where p.id = ?2")
void updatePersonNameById(String name, Long id);
根據 id 洗掉Person name:
@Modifying
@Transactional
@Query("delete from Person where id = ?1")
void deletePersonNameById(Long id);
4.3 創建異步方法
如果我們需要創建異步方法的話,也比較方便,
異步方法在呼叫時立即回傳,然后會被提交給TaskExecutor執行,當然你也可以選擇得出結果后才回傳給客戶端,
@Async
Future<User> findByName(String name);
@Async
CompletableFuture<User> findByName(String name);
5.測驗
@SpringBootTest
@RunWith(SpringRunner.class)
publicclass PersonRepositoryTest {
@Autowired
private PersonRepository personRepository;
private Long id;
/**
* 保存person到資料庫
*/
@Before
public void setUp() {
assertNotNull(personRepository);
Person person = new Person("SnailClimb", 23);
Person savedPerson = personRepository.saveAndFlush(person);// 更新 person 物件的姓名
savedPerson.setName("UpdatedName");
personRepository.save(savedPerson);
id = savedPerson.getId();
}
/**
* 使用 JPA 自帶的方法查找 person
*/
@Test
public void should_get_person() {
Optional<Person> personOptional = personRepository.findById(id);
assertTrue(personOptional.isPresent());
assertEquals("SnailClimb", personOptional.get().getName());
assertEquals(Integer.valueOf(23), personOptional.get().getAge());
List<Person> personList = personRepository.findByAgeGreaterThan(18);
assertEquals(1, personList.size());
// 清空資料庫
personRepository.deleteAll();
}
/**
* 自定義 query sql 查詢陳述句查找 person
*/
@Test
public void should_get_person_use_custom_query() {
// 查找所有欄位
Optional<Person> personOptional = personRepository.findByNameCustomeQuery("SnailClimb");
assertTrue(personOptional.isPresent());
assertEquals(Integer.valueOf(23), personOptional.get().getAge());
// 查找部分欄位
String personName = personRepository.findPersonNameById(id);
assertEquals("SnailClimb", personName);
System.out.println(id);
// 更新
personRepository.updatePersonNameById("UpdatedName", id);
Optional<Person> updatedName = personRepository.findByNameCustomeQuery("UpdatedName");
assertTrue(updatedName.isPresent());
// 清空資料庫
personRepository.deleteAll();
}
}
JPA連表查詢和分頁
這里我們繼續講JPA如何實作連表和分頁,因為JPA可以在repository層自定義sql所以也不難,
1.物體類
創建三個物體類,
@Entity
@Data
@NoArgsConstructor
publicclass Company {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String companyName;
private String description;
public Company(String name, String description) {
this.companyName = name;
this.description = description;
}
}
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
publicclass School {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String name;
private String description;
}
@Entity
@Data
@NoArgsConstructor
publicclass Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
private String name;
private Integer age;
private Long schoolId;
private Long companyId;
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
}
2. 自定義Sql實作連表查詢
假如我們當前要通過 person 表的 id 來查詢 Person 的話,我們知道 Person 的資訊一共分布在Company、School、Person這三張表中,所以,我們如果要把 Person 的資訊都查詢出來的話是需要進行連表查詢的,
首先我們需要創建一個包含我們需要的 Person 資訊的 DTO 物件,我們簡單第將其命名為 UserDTO,用于保存和傳輸我們想要的資訊,
@Data
@NoArgsConstructor
@Builder(toBuilder = true)
@AllArgsConstructor
publicclass UserDTO {
private String name;
privateint age;
private String companyName;
private String schoolName;
}
寫一個方法查詢Person的基本資訊
/**
* 連表查詢
*/
@Query(value = "select new com.saijia.modules.live.entity.UserDTO(p.name,p.age,c.companyName,s.name) " +
"from Person p left join Company c on p.companyId=c.id " +
"left join School s on p.schoolId=s.id " +
"where p.id=:personId")
Optional<UserDTO> getUserInformation(@Param("personId") Long personId);
2. 自定義 SQL 陳述句連表查詢并實作分頁操作
查詢當前所有的人員資訊并實作分頁,可以按照下面這種方式.為了實作分頁,我們在@Query注解中還添加了 countQuery 屬性
@Query(value = "select new com.saijia.modules.live.entity.UserDTO(p.name,p.age,c.companyName,s.name) " +
"from Person p left join Company c on p.companyId=c.id " +
"left join School s on p.schoolId=s.id ",
countQuery = "select count(p.id) " +
"from Person p left join Company c on p.companyId=c.id " +
"left join School s on p.schoolId=s.id ")
Page<UserDTO> getUserInformationList(Pageable pageable);
使用方法
//分頁選項
PageRequest pageRequest = PageRequest.of(0, 3, Sort.Direction.DESC, "age");
Page<UserDTO> userInformationList = personRepository.getUserInformationList(pageRequest);
//查詢結果總數
System.out.println(userInformationList.getTotalElements());// 6
//按照當前分頁大小,總頁數
System.out.println(userInformationList.getTotalPages());// 2
System.out.println(userInformationList.getContent());
3. IN的操作查詢
在 sql 陳述句中加入我們需要篩選出符合幾個條件中的一個的情況下,可以使用 IN 查詢,對應到 JPA 中也非常簡單,比如下面的方法就實作了,根據名字過濾需要的人員資訊,
@Query(value = "select new com.saijia.modules.live.entity.UserDTO(p.name,p.age,c.companyName,s.name) " +
"from Person p left join Company c on p.companyId=c.id " +
"left join School s on p.schoolId=s.id " +
"where p.name IN :peopleList")
List<UserDTO> filterUserInfo(List peopleList);
實際使用:
List<String> personList=new ArrayList<>(Arrays.asList("person1","person2"));
List<UserDTO> userDTOS = personRepository.filterUserInfo(personList);
4. BETWEEN操作查詢
查詢滿足某個范圍的值,比如下面的方法就實作查詢滿足某個年齡范圍的人員的資訊,
@Query(value = "select new com.saijia.modules.live.entity.UserDTO(p.name,p.age,c.companyName,s.name) " +
"from Person p left join Company c on p.companyId=c.id " +
"left join School s on p.schoolId=s.id " +
"where p.age between :small and :big")
List<UserDTO> filterUserInfoByAge(int small,int big);
實際使用:
List<UserDTO> userDTOS = personRepository.filterUserInfoByAge(19,20);
5. 測驗
@SpringBootTest
@RunWith(SpringRunner.class)
publicclass PersonRepositoryTest2 {
@Autowired
private PersonRepository personRepository;
@Sql(scripts = {"classpath:/init.sql"})
@Test
public void find_person_age_older_than_18() {
List<Person> personList = personRepository.findByAgeGreaterThan(18);
assertEquals(1, personList.size());
}
@Sql(scripts = {"classpath:/init.sql"})
@Test
public void should_get_user_info() {
Optional<UserDTO> userInformation = personRepository.getUserInformation(1L);
System.out.println(userInformation.get().toString());
}
@Sql(scripts = {"classpath:/init.sql"})
@Test
public void should_get_user_info_list() {
PageRequest pageRequest = PageRequest.of(0, 3, Sort.Direction.DESC, "age");
Page<UserDTO> userInformationList = personRepository.getUserInformationList(pageRequest);
//查詢結果總數
System.out.println(userInformationList.getTotalElements());// 6
//按照當前分頁大小,總頁數
System.out.println(userInformationList.getTotalPages());// 2
System.out.println(userInformationList.getContent());
}
@Sql(scripts = {"classpath:/init.sql"})
@Test
public void should_filter_user_info() {
List<String> personList=new ArrayList<>(Arrays.asList("person1","person2"));
List<UserDTO> userDTOS = personRepository.filterUserInfo(personList);
System.out.println(userDTOS);
}
@Sql(scripts = {"classpath:/init.sql"})
@Test
public void should_filter_user_info_by_age() {
List<UserDTO> userDTOS = personRepository.filterUserInfoByAge(19,20);
System.out.println(userDTOS);
}
}
事務@Transactional注解使用詳解
日常編碼中,當我們要使用事務的時候,只需要在要開啟事務的方法上加上@Transactional注解即可,
@Transactional(rollbackFor = Exception.class)
public void delete() {
......
}
1.@Transactional 的作用范圍
@Transactional 注解一般用在可以作用在類或者方法上,
-
方法 :推薦將注解使用于方法上,不過需要注意的是:該注解只能應用到 public 方法上,否則不生效,
-
類 :如果這個注解使用在類上的話,表明該注解對該類中所有的 public 方法都生效,所有該類的public 方法都配置相同的事務屬性資訊,
-
介面 :不推薦在介面上使用,
但是當類配置了@Transactional,方法也配置了@Transactional,方法的事務會覆寫類的事務配置資訊,
2.@Transactional 的常用配置引數
首先我們來看一下@Transactional的原始碼,如下(里面包含了基本事務屬性的配置):
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
@AliasFor("transactionManager")
String value() default "";
@AliasFor("value")
String transactionManager() default "";
Propagation propagation() default Propagation.REQUIRED;
Isolation isolation() default Isolation.DEFAULT;
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
boolean readOnly() default false;
Class<? extends Throwable>[] rollbackFor() default {};
String[] rollbackForClassName() default {};
Class<? extends Throwable>[] noRollbackFor() default {};
String[] noRollbackForClassName() default {};
}
@Transactional 的常用配置引數總結(列5 個我平時比較常用的):
| 屬性名 | 說明 |
|---|---|
| propagation | 事務的傳播行為,默認值為 Propagation.REQUIRED |
| isolation | 事務的隔離級別,默認值采用 Isolation.DEFAULT |
| timeout | 事務的超時時間,默認值為-1(不會超時),如果超過該時間限制但事務還沒有完成,則自動回滾事務, |
| readOnly | 指定事務是否為只讀事務,默認值為 false,如果要去忽略那些不需要事務的方法,比如讀取資料,可以設定 read-only 為 true, |
| rollbackFor | 用于指定能夠觸發事務回滾的例外型別,并且可以指定多個例外型別, |
這里還是說一下propagation傳播行為:
- Propagation.REQUIRED:如果當前存在事務,則加入該事務,如果當前不存在事務,則創建一個新的事務,( 也就是說如果A方法和B方法都添加了注解,在默認傳播模式下,A方法內部呼叫B方法,會把兩個方法的事務合并為一個事務 )
- Propagation.SUPPORTS:如果當前存在事務,則加入該事務;如果當前不存在事務,則以非事務的方式繼續運行,
- Propagation.MANDATORY:如果當前存在事務,則加入該事務;如果當前不存在事務,則拋出例外,
- Propagation.REQUIRES_NEW:重新創建一個新的事務,如果當前存在事務,暫停當前的事務,( 當類A中的 a 方法用默認Propagation.REQUIRED模式,類B中的 b方法加上采用 Propagation.REQUIRES_NEW模式,然后在 a 方法中呼叫 b方法操作資料庫,然而 a方法拋出例外后,b方法并沒有進行回滾,因為Propagation.REQUIRES_NEW會暫停 a方法的事務 )
- Propagation.NOT_SUPPORTED:以非事務的方式運行,如果當前存在事務,暫停當前的事務,
- Propagation.NEVER:以非事務的方式運行,如果當前存在事務,則拋出例外,
- Propagation.NESTED :和 Propagation.REQUIRED 效果一樣,
isolation 事務的隔離級別:
- TransactionDefinition.ISOLATION_DEFAULT: 使用后端資料庫默認的隔離級別,Mysql 默認采用的 REPEATABLE_READ隔離級別 Oracle 默認采用的 READ_COMMITTED隔離級別.
- TransactionDefinition.ISOLATION_READ_UNCOMMITTED: 最低的隔離級別,允許讀取尚未提交的資料變更,可能會導致臟讀、幻讀或不可重復讀
- TransactionDefinition.ISOLATION_READ_COMMITTED: 允許讀取并發事務已經提交的資料,可以阻止臟讀,但是幻讀或不可重復讀仍有可能發生
- TransactionDefinition.ISOLATION_REPEATABLE_READ: 對同一欄位的多次讀取結果都是一致的,除非資料是被本身事務自己所修改,可以阻止臟讀和不可重復讀,但幻讀仍有可能發生,
- TransactionDefinition.ISOLATION_SERIALIZABLE: 最高的隔離級別,完全服從ACID的隔離級別,所有的事務依次逐個執行,這樣事務之間就完全不可能產生干擾,也就是說,該級別可以防止臟讀、不可重復讀以及幻讀,但是這將嚴重影響程式的性能,通常情況下也不會用到該級別,
對于事務一些相關的基礎知識還不太了解的同學可以去看一下 Spring系列之事務博客的相關知識,
在開始的時候我舉了一個方法使用@Transactional注解的例子,就有用到rollbackFor配置引數,我們知道 Exception 分為運行時例外 RuntimeException 和非運行時例外,在@Transactional注解中如果不配置rollbackFor屬性,那么事物只會在遇到RuntimeException的時候才會回滾,加上rollbackFor=Exception.class,可以讓事物在遇到非運行時例外時也回滾,
3.@Transactional 事務注解原理
我們在面試中問到 AOP 的時候可能會被問的一個問題,
基本都知道,@Transactional 的作業機制是基于 AOP 實作的,AOP 又是使用動態代理實作的,如果目標物件實作了介面,默認情況下會采用 JDK 的動態代理,如果目標物件沒有實作了介面,會使用 CGLIB 動態代理,
多說一下,createAopProxy() 方法 決定了是使用 JDK 還是 Cglib 來做動態代理,原始碼如下:
public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}
.......
}
如果一個類或者一個類中的 public 方法上被標注@Transactional 注解的話,Spring 容器就會在啟動的時候為其創建一個代理類,在呼叫被@Transactional 注解的 public 方法的時候,實際呼叫的是,TransactionInterceptor 類中的 invoke()方法,這個方法的作用就是在目標方法之前開啟事務,方法執行程序中如果遇到例外的時候回滾事務,方法呼叫完成之后提交事務,
TransactionInterceptor 類中的 invoke()方法內部實際呼叫的是 TransactionAspectSupport 類的 invokeWithinTransaction()方法,
由于新版本的 Spring 對這部分重寫很大,而且用到了很多回應式編程的知識,這里就不列原始碼了,
4.Spring AOP自呼叫問題
若同一類中的其他沒有 @Transactional 注解的方法內部呼叫有 @Transactional 注解的方法,有@Transactional 注解的方法的事務會失效,
這是由于Spring AOP代理的原因造成的,因為只有當 @Transactional 注解的方法在類以外被呼叫的時候,Spring 事務管理才生效,
MyService 類中的method1()呼叫method2()就會導致method2()的事務失效,
@Service
public class MyService {
private void method1() {
method2();
//......
}
@Transactional
public void method2() {
//......
}
}
解決辦法: 避免同一類中自呼叫或者使用 AspectJ 取代 Spring AOP 代理,
5. @Transactional 的使用注意事項
- @Transactional 注解只有作用到 public 方法上事務才生效,不推薦在介面上使用;
- 避免同一個類中呼叫 @Transactional 注解的方法,這樣會導致事務失效;
- 正確的設定 @Transactional 的 rollbackFor 和 propagation 屬性,否則事務可能會回滾失敗
6.@Transactional 失效場景
我們結合具體的代碼分析一下,哪些場景下@Transactional 注解會失效
6.1 @Transactional 應用在非public修飾的方法上
如果Transactional注解應用在非public 修飾的方法上,Transactional將會失效,

protected TransactionAttribute computeTransactionAttribute(Method method,
Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
returnnull;
}
此方法會檢查目標方法的修飾符是否為 public,不是 public則不會獲取@Transactional 的屬性配置資訊,
這里注意一下:protected、private 修飾的方法上使用 @Transactional 注解,雖然事務無效,但不會有任何報錯,這是我們容易犯錯的一點,
6.2 @Transactional 注解屬性 propagation 設定錯誤
這種失效是由于配置錯誤,若是錯誤的配置以下三種 propagation,事務將不會發生回滾,
-
TransactionDefinition.PROPAGATION_SUPPORTS:如果當前存在事務,則加入該事務;如果當前沒有事務,則以非事務的方式繼續運行,
-
TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事務方式運行,如果當前存在事務,則把當前事務掛起,
-
TransactionDefinition.PROPAGATION_NEVER:以非事務方式運行,如果當前存在事務,則拋出例外,
6.3 @Transactional 注解屬性 rollbackFor 設定錯誤
rollbackFor 可以指定能夠觸發事務回滾的例外型別,Spring默認拋出了未檢查unchecked例外(繼承自 RuntimeException 的例外)或者 Error才回滾事務;其他例外不會觸發回滾事務,如果在事務中拋出其他型別的例外,但卻期望 Spring 能夠回滾事務,就需要指定 rollbackFor屬性,

// 希望自定義的例外可以進行回滾
@Transactional(propagation= Propagation.REQUIRED,rollbackFor= MyException.class
若在目標方法中拋出的例外是 rollbackFor 指定的例外的子類,事務同樣會回滾,Spring 原始碼如下:
private int getDepth(Class<?> exceptionClass, int depth) {
if (exceptionClass.getName().contains(this.exceptionName)) {
// Found it!
return depth;
}
// If we've gone as far as we can go and haven't found it...
if (exceptionClass == Throwable.class) {
return -1;
}
return getDepth(exceptionClass.getSuperclass(), depth + 1);
}
6.4 同一個類中方法呼叫,導致@Transactional失效
開發中避免不了會對同一個類里面的方法呼叫,比如有一個類Test,它的一個方法A,A再呼叫本類的方法B(不論方法B是用public還是private修飾),但方法A沒有宣告注解事務,而B方法有,則外部呼叫方法A之后,方法B的事務是不會起作用的,這也是經常犯錯誤的一個地方,
那為啥會出現這種情況?其實這還是由于使用Spring AOP代理造成的,因為只有當事務方法被當前類以外的代碼呼叫時,才會由Spring生成的代理物件來管理,
//@Transactional
@GetMapping("/test")
private Integer A() throws Exception {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setCityName("2");
/**
* B 插入欄位為 3的資料
*/
this.insertB();
/**
* A 插入欄位為 2的資料
*/
int insert = cityInfoDictMapper.insert(cityInfoDict);
return insert;
}
@Transactional()
public Integer insertB() throws Exception {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setCityName("3");
cityInfoDict.setParentCityId(3);
return cityInfoDictMapper.insert(cityInfoDict);
}
6.5 例外被你的 catch“吃了”導致@Transactional失效
這種情況其實是最常見的 @Transactional 注解失效場景
@Transactional
private Integer A() throws Exception {
int insert = 0;
try {
CityInfoDict cityInfoDict = new CityInfoDict();
cityInfoDict.setCityName("2");
cityInfoDict.setParentCityId(2);
/**
* A 插入欄位為 2的資料
*/
insert = cityInfoDictMapper.insert(cityInfoDict);
/**
* B 插入欄位為 3的資料
*/
b.insertB();
} catch (Exception e) {
e.printStackTrace();
}
}
如果B方法內部拋了例外,而A方法此時try catch了B方法的例外,那這個事務還能正常回滾嗎?
不能回滾,
會拋出例外:
org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
因為當ServiceB中拋出了一個例外以后,ServiceB標識當前事務需要rollback,但是ServiceA中由于你手動的捕獲這個例外并進行處理,ServiceA認為當前事務應該正常commit,此時就出現了前后不一致,也就是因為這樣,拋出了前面的UnexpectedRollbackException例外,
spring的事務是在呼叫業務方法之前開始的,業務方法執行完畢之后才執行commit or rollback,事務是否執行取決于是否拋出runtime例外,如果拋出runtime exception 并在你的業務方法中沒有catch到的話,事務會回滾,
在業務方法中一般不需要catch例外,如果非要catch一定要拋出throw new RuntimeException(),或者注解中指定拋例外型別@Transactional(rollbackFor=Exception.class),否則會導致事務失效,資料commit造成資料不一致,所以有些時候 try catch反倒會畫蛇添足,
6.6 資料庫引擎不支持事務
這種情況出現的概率并不高,事務能否生效,看資料庫引擎是否支持事務,常用的MySQL資料庫默認使用支持事務的innodb引擎,但是資料庫引擎切換成不支持事務的myisam,那事務就從根本上失效了,
json資料處理注解
講一下關于json資料處理的一些相關注解
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<>();
}
2. 格式化json資料
@JsonFormat一般用來格式化 json 資料:
@JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", timezone="GMT")
private Date date;
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"
}
測驗注解
@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 {
......
}
講到這里本章對Spring和Spring Boot注解的講解也就結束了,如果想了解更多知識可以在對應的專欄中看系列文章,謝謝大家的觀看,希望能給各位同學帶來幫助,如果覺得博主寫的還可以的,可以點贊收藏, 😉
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/233151.html
標籤:其他

