1. JSR 303是什么?
JSR 303(Java Specification Request 303),也稱為Bean Validation,是Java中的一個規范,用于定義Java對象的校驗規則。
1.1 JSR 303的主要功能
- 注解驅動:通過注解直接在Java類上定義校驗規則。
- 內置約束:如@NotNull、@Size、@Min、@Max等。
- 自定義約束:可以定義自定義的校驗注解和邏輯。
- 分組校驗:支持對不同場景(如創建和更新)進行分組校驗。
1.2 常用注解
- @NotNull:驗證注解的元素值不是null。
- @Size:驗證注解的元素的大小在指定范圍內。
- @Min和@Max:驗證注解的元素值在指定范圍內。
- @Email:驗證注解的元素是一個合法的電子郵件地址。
2. 使用步驟
JSR 303 是一個規范,所以需要具體的實現。Hibernat Bean Validator 就是Bean Validator的實現
2.1.引入庫
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency><dependency><groupId>javax.validation</groupId><artifactId>validation-api</artifactId><version>2.0.1.Final</version></dependency><dependency><groupId>org.hibernate</groupId><artifactId>hibernate-validator</artifactId><version>8.0.1.Final</version></dependency>
2.2 實體類編寫校驗規則
代碼如下(示例):
@Data
public class BrandEntity implements Serializable {/*** 品牌ID,用于標識品牌。* * 此字段通過注解進行了不同的驗證邏輯配置,以適應不同的業務場景。* 在更新操作(UpdateGroup)中,要求此字段不為空,確保了更新操作有明確的目標品牌ID。* 在新增操作(AddGroup)中,要求此字段為空,因為新增品牌時不應該預先指定ID。* 這種通過注解進行驗證的方式,提高了代碼的靈活性和可維護性,避免了在業務邏輯中硬編碼驗證邏輯。*/@NotNull(message = "修改必須指定品牌id",groups = {UpdateGroup.class})@Null(message = "新增不能指定id",groups = {AddGroup.class})private Long brandId;/*** 品牌名稱字段。* * 該字段是必填的,不允許為空字符串,這在添加和更新品牌信息時都必須遵守。* 使用@NotBlank注解來強制驗證品牌名的非空性,如果為空,則會觸發驗證失敗,* 返回相應的錯誤消息。*/@NotBlank(message = "品牌名必須提交",groups = {AddGroup.class,UpdateGroup.class})private String name;/*** 品牌logo地址* * 此字段在添加(AddGroup)和更新(UpdateGroup)時都必須是一個合法的URL地址,* 以確保公司徽標的鏈接是有效和可訪問的。使用@URL注解進行驗證,* 如果不符合URL格式,則會提示指定的錯誤信息。* * 使用@NotBlank注解確保在添加時該字段不為空,為空則認為是無效的輸入。* 這是因為在更新時,如果用戶沒有提供新的徽標URL,可以保留舊的URL,* 所以在更新組(UpdateGroup)中,@NotBlank約束被移除,允許為空。*/@NotBlank(groups = {AddGroup.class})@URL(message = "logo必須是一個合法的url地址",groups={AddGroup.class,UpdateGroup.class})private String logo;/*** 展示狀態字段,用于標記對象的展示狀態。* 顯示狀態[0-不顯示;1-顯示]* * 此字段受到兩個驗證組(AddGroup, UpdateStatusGroup)的約束。* 在這兩個組中,該字段不能為空(@NotNull)且其值必須在預定義的列表中(@ListValue)。* 這樣的設計確保了在添加對象和更新狀態操作時,展示狀態的值是有效且受控的。* * @NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})* 表明在AddGroup和UpdateStatusGroup驗證組中,此字段不能為空。* @ListValue(vals={0,1},groups = {AddGroup.class, UpdateStatusGroup.class})* 表明在AddGroup和UpdateStatusGroup驗證組中,此字段的值必須是0或1。*/@NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})@ListValue(vals={0,1},groups = {AddGroup.class, UpdateStatusGroup.class})private Integer showStatus;/*** 字段firstLetter用于存儲實體的首字母。* 該字段的驗證有以下規則:* 1. 在添加(AddGroup)時,不能為空,確保數據完整性。* 2. 在添加(AddGroup)和更新(UpdateGroup)時,必須是一個字母,確保數據的格式符合預期。* 這些驗證規則通過注解的方式進行聲明,以在運行時對數據進行校驗。*/@NotEmpty(groups={AddGroup.class})@Pattern(regexp="^[a-zA-Z]$",message = "檢索首字母必須是一個字母",groups={AddGroup.class,UpdateGroup.class})private String firstLetter;/*** 排序字段,用于控制元素的顯示順序。* * @NotNull 標注指示該字段在添加(AddGroup)時不能為空,確保了排序值的有效性。* @Min 標注指定了排序值必須大于等于0,適用于添加(AddGroup)和更新(UpdateGroup)操作,保證了排序的邏輯正確性。*/@NotNull(groups={AddGroup.class})@Min(value = 0,message = "排序必須大于等于0",groups={AddGroup.class,UpdateGroup.class})private Integer sort;}
2.3 自定義約束
通過上面的代碼可以看出,如果要指定注解作用的范圍,就要自己添加分組。
AddGroup
public interface AddGroup {
}
UpdateGroup
public interface UpdateGroup {
}
UpdateStatusGroup
public interface UpdateStatusGroup {
}
上述代碼中為了對狀態取值進行驗證,我們采用了自定義驗證器的方式
ListValue
@Documented
@Constraint(validatedBy = { ListValueConstraintValidator.class })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {String message() default "{com.xunqi.common.valid.ListValue.message}";Class<?>[] groups() default { };Class<? extends Payload>[] payload() default { };int[] vals() default { };
}
ListValueConstraintValidator
/*** 實現了ListValue注解的驗證器,用于驗證一個整數是否在預定義的整數列表中。* 該驗證器在驗證階段檢查給定的整數是否存在于初始化時指定的整數集合中。*/
public class ListValueConstraintValidator implements ConstraintValidator<ListValue, Integer> {/*** 用于存儲驗證器初始化時指定的整數列表的集合。*/private Set<Integer> set = new HashSet<>();/*** 初始化驗證器,加載注解中指定的整數列表到集合中。** @param constraintAnnotation ListValue注解實例,其中包含需要驗證的整數列表。*/@Overridepublic void initialize(ListValue constraintAnnotation) {int[] value = constraintAnnotation.vals();for (int val : value) {set.add(val);}}/*** 驗證給定的整數是否在集合中。** @param value 待驗證的整數。* @param context 驗證上下文,提供關于驗證操作的上下文信息。* @return 如果給定的整數存在于集合中,則返回true;否則返回false。*/@Overridepublic boolean isValid(Integer value, ConstraintValidatorContext context) {return set.contains(value);}
}
3. 業務層使用
/*** <p>* 商品品牌 前端控制器* </p>** @author shiqi* @version 1.0.0* @createTime 2024-06-26*/
@RestController
@RequestMapping("product/brand")
public class BrandController {/*** 保存品牌信息。* <p>* 該方法通過@RequestMapping注解映射了"/save"的HTTP請求,用于保存BrandEntity對象。* 使用@Validated注解對brandEntity參數進行驗證,確保添加或修改品牌時數據的合法性。* BindingResult參數用于接收驗證后的錯誤信息,可以進一步處理和反饋給前端。* <p>* 方法返回一個R對象,通常表示操作的成功或失敗狀態。** @param brandEntity 品牌實體對象,包含待保存的品牌信息。* @param bindingResult 驗證結果對象,用于存儲brandEntity驗證過程中產生的錯誤信息。* @return 返回一個表示操作結果的對象,通常是一個包含成功狀態和相關消息的R對象。*/@RequestMapping("/save")public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brandEntity, BindingResult bindingResult) {// 方法體中應包含保存品牌信息的具體邏輯,此處省略。return R.ok();}}
4. 封裝統一異常
4.1 業務異常狀態枚舉類
/*** <p>* 描述:業務異常枚舉類* </p>** @author shiqi* @version 1.0.0* @createTime 2024-06-26*/
public enum BizCodeEnum {UNKNOWN_EXCEPTION(10000,"系統未知異常"),VALID_EXCEPTION(10001,"參數格式校驗失敗"),TO_MANY_REQUEST(10002,"請求流量過大,請稍后再試"),SMS_CODE_EXCEPTION(10002,"驗證碼獲取頻率太高,請稍后再試"),PRODUCT_UP_EXCEPTION(11000,"商品上架異常"),USER_EXIST_EXCEPTION(15001,"存在相同的用戶"),PHONE_EXIST_EXCEPTION(15002,"存在相同的手機號"),NO_STOCK_EXCEPTION(21000,"商品庫存不足"),LOGIN_ACCOUNT_PASSWORD_EXCEPTION(15003,"賬號或密碼錯誤");private int code;private String message;BizCodeEnum(int code, String message) {this.code = code;this.message = message;}public int getCode() {return code;}public String getMessage() {return message;}
}
4.2 封裝統一返回結果
/*** <p>* 統一返回結果* </p>** @author shiqi* @version 1.0.0* @createTime 2024-06-26*/public class R extends HashMap<String, Object> {private static final long serialVersionUID = 1L;public R setData(Object data) {put("data",data);return this;}//利用fastjson進行反序列化public <T> T getData(TypeReference<T> typeReference) {Object data = get("data"); //默認是mapString jsonString = JSON.toJSONString(data);T t = JSON.parseObject(jsonString, typeReference);return t;}//利用fastjson進行反序列化public <T> T getData(String key,TypeReference<T> typeReference) {Object data = get(key); //默認是mapString jsonString = JSON.toJSONString(data);T t = JSON.parseObject(jsonString, typeReference);return t;}public R() {put("code", 0);put("msg", "success");}public static R error() {return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知異常,請聯系管理員");}public static R error(String msg) {return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);}public static R error(int code, String msg) {R r = new R();r.put("code", code);r.put("msg", msg);return r;}public static R ok(String msg) {R r = new R();r.put("msg", msg);return r;}public static R ok(Map<String, Object> map) {R r = new R();r.putAll(map);return r;}public static R ok() {return new R();}public R put(String key, Object value) {super.put(key, value);return this;}public Integer getCode() {return (Integer) this.get("code");}}
自定義校驗異常處理器
/*** <p>* 集中處理所有異常* </p>** @author shiqi* @version 1.0.0* @createTime 2024-06-26*/@Slf4j
@RestControllerAdvice(basePackages = {"com.shiqi.jsr303demo"})
public class CustomExceptionControllerAdvice {/*** 處理方法參數不合法異常。* 當方法參數不滿足驗證條件時,Spring MVC會拋出MethodArgumentNotValidException異常。* 該異常處理器專門捕獲此類異常,以統一的方式處理參數驗證失敗的情況。** @param e MethodArgumentNotValidException異常實例,包含驗證失敗的詳細信息。* @return 返回一個包含錯誤信息的響應對象。*/@ExceptionHandler(value = MethodArgumentNotValidException.class)public R handleMethodArgumentNotValidException(MethodArgumentNotValidException e){// 獲取驗證結果對象,其中包含了具體的驗證錯誤信息。BindingResult bindingResult = e.getBindingResult();// 初始化一個映射,用于存儲字段名和對應的錯誤信息。HashMap<String,String> errMap=new HashMap<String,String>();// 檢查是否有驗證錯誤,如果有,則遍歷所有字段錯誤,并將字段名和錯誤信息添加到errMap中。if (bindingResult.hasErrors()){bindingResult.getFieldErrors().forEach((item)->{errMap.put(item.getField(),item.getDefaultMessage());});}// 返回一個包含錯誤代碼、錯誤消息和具體錯誤詳情的響應對象。// 錯誤代碼為400,表示客戶端請求錯誤,錯誤消息為"參數校驗不合法"。// errMap作為數據部分的一部分,包含了所有驗證失敗的字段和對應的錯誤信息。return R.error(400,"參數校驗不合法").put("data",errMap);}/*** 處理所有異常的控制器異常處理器。* <p>* 該方法旨在捕獲控制器層拋出的任何異常,無論是預期的業務異常還是未預期的運行時異常。* 它的目的是統一異常的處理方式,向客戶端返回一個標準的響應體,而不是直接暴露服務器內部錯誤信息。** @param throwable 拋出的異常對象,無論異常類型為何。* @return 返回一個表示錯誤響應的R對象。這個響應體可以幫助客戶端識別請求處理過程中發生了什么錯誤。*/@ExceptionHandler(value = Throwable.class)private R handleValidException(Throwable throwable) {// 記錄異常信息到日志系統,以便后續的問題排查和分析。log.error("出現異常{},異常類型{}", throwable.getMessage(), throwable.getClass());// 返回一個通用的錯誤響應體,通知客戶端請求處理過程中發生了錯誤。return R.error();}}
5. 實際業務中的應用
- 表單校驗:確保用戶輸入的數據合法,如用戶注冊、登錄、表單提交等。
- 數據傳輸對象(DTO)校驗:在進行數據傳輸時,確保傳輸的數據符合預期,如API請求和響應。
- 領域對象校驗:確保業務邏輯中的對象狀態合法,如訂單處理、支付處理等。
使用JSR 303可以有效減少手動校驗代碼,簡化代碼結構,提高代碼可讀性和維護性。在實際應用中,常結合Spring框架和Hibernate Validator一起使用。
6. 擴展知識
6.1 PO(Persistence Object)
定義:持久化對象,通常對應數據庫中的一張表,每個實例對應表中的一行。
應用場景:用于數據持久化層,與數據庫的表結構一一對應,通過ORM(如Hibernate)進行數據操作。
使用時機:當需要將數據持久化到數據庫,或者從數據庫中讀取數據時使用。
示例:
public class UserPO {private Long id;private String username;private String password;// Getters and Setters
}
具體使用場景:
- 在需要進行數據庫操作(CRUD)時使用。
- 與DAO一起使用,以實現數據持久化邏輯。
簡單來說就是Java Bean 對應的是數據庫中的一張表
6.2. BO(Business Object)
定義:業務對象,封裝業務邏輯的Java對象,通常包含業務操作方法。
應用場景:在業務層使用,用于處理業務邏輯,可能會調用多個DAO或與多個PO交互。
使用時機:當需要處理復雜的業務邏輯或操作多個數據對象時使用。
示例:
public class UserBO {private Long id;private String username;private String password;public void changePassword(String newPassword) {// 業務邏輯:更新密碼this.password = newPassword;}// Getters and Setters
}
具體使用場景:
- 在業務層需要進行復雜業務邏輯處理時。
- 需要將多個數據對象(PO)進行組合或處理時。
6.3. VO(Value Object)
定義:值對象,用于在應用層傳遞數據,通常是只讀的。
應用場景:在視圖層(如MVC中的View)使用,用于展示數據,不包含業務邏輯。
使用時機:當需要在視圖層展示數據,而不需要修改數據時使用。
示例:
public class UserVO {private Long id;private String username;// 只包含展示所需的字段// Getters and Setters
}
具體使用場景:
- 在需要將數據傳遞給前端進行展示時。
- 在只讀數據場景下使用,如顯示用戶信息。
簡單來說就是對PO進行閹割或者強化,比如我們需要查詢用戶列表的時候,需要對敏感信息進行加密,不太關心的數據我們也不必返回給前端,造成帶寬的浪費。
6.4 DTO(Data Transfer Object)
定義:數據傳輸對象,用于在不同層之間傳輸數據,通常是無狀態的。
應用場景:在數據傳輸層使用,如在服務接口之間傳遞數據,減少遠程調用次數。
使用時機:當需要在不同系統或不同層之間傳遞數據時使用。
示例:
public class UserDTO {private Long id;private String username;private String email;// Getters and Setters
}
具體使用場景:
- 在服務之間的遠程調用中傳輸數據。
- 在Controller與Service之間傳遞數據。
這個怎么理解了,就是我們進行表單提交的時候,或者更新查詢操作時,需要前端傳遞很多參數,如果每個參數都寫在Controller層的參數列表中就很不優雅,而且如果需新增參數的話就需要改動很多地方,會導致容易遺漏,這個時候我們就把這些請求參數封裝成一個請求對象,這就是DTO。
6.5. POJO(Plain Old Java Object)
定義:簡單的Java對象,不依賴任何特定框架或庫。
應用場景:用于定義普通的Java對象,通常作為PO、BO、VO、DTO的基礎。
使用時機:在需要創建簡單的數據容器而不依賴任何特定框架時使用。
示例:
public class User {private Long id;private String username;private String password;// Getters and Setters
}
具體使用場景:
- 創建簡單的Java對象用于數據封裝。
- 作為其他對象(PO、BO、VO、DTO)的基礎類。
也就是我們自己編寫的業務類,無需與數據庫打交道的。上述的統一返回結果類,我們就可以理解為POJO
6.6. DAO(Data Access Object)
定義:數據訪問對象,提供對數據庫的抽象和封裝,包含CRUD操作。
應用場景:在數據訪問層使用,負責與數據庫進行交互。
使用時機:當需要對數據庫進行操作(如查詢、插入、更新、刪除)時使用。
示例:
public void save(UserPO user) {// 保存用戶到數據庫}public UserPO findById(Long id) {// 根據ID查詢用戶return new UserPO();}// 其他CRUD操作
}
具體使用場景:
- 在需要與數據庫進行直接交互時。
- 在業務邏輯中需要持久化或檢索數據時。