主頁 > 軟體設計 > 達摩院--Spring和Spring Boot注解大全

達摩院--Spring和Spring Boot注解大全

2020-12-11 12:31:13 軟體設計

最近深圳房價大漲,一個在深圳的學姐也加入了買房的隊伍,什么現在還不買,一天漲一兩百萬,可憐的小宋現在連一個廁所都買不起🤣,還是老老實實繼續寫博客吧,
在這里插入圖片描述

本章小宋會帶大家去了解使用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區別

  1. @Autowired與@Resource都可以用來裝配bean. 都是用來實作依賴注入的.

  2. @Autowired默認按型別裝配(這個注解是屬于spring的,spring提供),@AutoWried按byType自動注入,默認情況下必須要求依賴物件必須存在,如果要允許null值,可以設定它的required屬性為false,如:@Autowired(required=false) ,如果我們想使用名稱裝配可以結合@Qualifier注解進行使用

  3. @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的作用域:

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

@Configuration

一般用來宣告配置類,可以使用 @Component注解替代,不過使用@Configuration注解宣告配置類更加語意化,

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

處理常見的HTTP請求注解

下面我們來看看常見的5種http請求型別:

  1. GET :請求從服務器獲取特定資源,舉個例子:GET /users(獲取所有學生)
  2. POST :在服務器上創建一個新的資源,舉個例子:POST /users(創建學生)
  3. PUT :更新服務器上的資源(客戶端提供更新后的整個資源),舉個例子:PUT /users/12(更新編號為 12 的學生)
  4. DELETE :從服務器洗掉特定的資源,舉個例子:DELETE /users/12(洗掉編號為 12 的學生)
  5. 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 工具來驗證,

  1. 所有引數輸入正確的情況:
    @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"));
    }
    
  2. 驗證出現引數不合法的情況拋出例外并且可以正確被捕獲,
    @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 格式不正確"));
        }
    
    使用 Postman 驗證結果如下:
    在這里插入圖片描述

驗證請求引數(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這三個中的一個,

  1. 第一步需要創建一個注解:

    @Target({FIELD})
    @Retention(RUNTIME)
    @Constraint(validatedBy = RegionValidator.class)
    @Documented
    public@interface Region {
    
        String message() default "Region 值不在可選范圍內";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    }
    
  2. 第二步你需要實作 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種:

  1. create:每次重新啟動專案都會重新創建新表結構,會導致資料丟失
  2. create-drop:每次啟動專案創建表結構,關閉專案洗掉表結構
  3. update:每次啟動專案會更新表結構
  4. 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 增刪改查

  1. 增(保存用戶到資料庫)
    Person person = new Person("SnailClimb", 23);
    personRepository.save(person);
save()方法對應 sql 陳述句是: insert into person (age, name) values (23,"snailclimb")
  1. 查(根據id查找用戶)
 Optional<Person> personOptional = personRepository.findById(id);
findById()方法對應 sql 陳述句就是:select * from person p where p.id = id
  1. 刪(根據id洗掉用戶)
  personRepository.deleteById(id);
deleteById()方法對應 sql 陳述句就是:delete from person where id=id
  1. 改(根據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 的使用注意事項

  1. @Transactional 注解只有作用到 public 方法上事務才生效,不推薦在介面上使用;
  2. 避免同一個類中呼叫 @Transactional 注解的方法,這樣會導致事務失效;
  3. 正確的設定 @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

標籤:其他

上一篇:媒介網站軟文營銷寫作避免這五點少走彎路

下一篇:疫情面前,沒有人能夠獨善其身(一)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more