Java?Web開發(fā)中的分頁與參數(shù)校驗舉例詳解
前言
在 Java Web 開發(fā)中,分頁和參數(shù)校驗是兩個非常重要的功能。本文將圍繞 分頁設(shè)計 和 參數(shù)校驗 進(jìn)行探討,包括如何設(shè)計合理的分頁查詢參數(shù),以及如何利用 Java 注解 實現(xiàn)參數(shù)校驗。
分頁設(shè)計
為什么需要分頁?
當(dāng)數(shù)據(jù)庫表數(shù)據(jù)量較大時,如果直接查詢所有數(shù)據(jù),可能會導(dǎo)致 查詢緩慢,甚至造成 內(nèi)存溢出(OOM)。分頁是一種常見的優(yōu)化方式,可以 減少數(shù)據(jù)庫負(fù)載 并 提升前端渲染速度。
如何設(shè)計分頁查詢參數(shù)?
分頁通常包含以下幾個核心參數(shù):
pageNo
:當(dāng)前頁碼,默認(rèn)為1
。pageSize
:每頁返回的記錄數(shù),默認(rèn)為20
。sortBy
:排序字段,如id
、create_time
。isAsc
:是否升序,默認(rèn)為true
。
我們可以設(shè)計一個公共的父類PageQuery
來幫助提供默認(rèn)的參數(shù),同時我們在開發(fā)中也會用到mybatisplus,提供出轉(zhuǎn)成page對象的方法
@Data @ApiModel(description = "分頁請求參數(shù)") @Accessors(chain = true) public class PageQuery { public static final Integer DEFAULT_PAGE_SIZE = 20; public static final Integer DEFAULT_PAGE_NUM = 1; @ApiModelProperty(value = "頁碼", example = "1") @Min(value = 1, message = "頁碼不能小于1") private Integer pageNo = DEFAULT_PAGE_NUM; @ApiModelProperty(value = "每頁大小", example = "5") @Min(value = 1, message = "每頁查詢數(shù)量不能小于1") private Integer pageSize = DEFAULT_PAGE_SIZE; @ApiModelProperty(value = "是否升序", example = "true") private Boolean isAsc = true; @ApiModelProperty(value = "排序字段", example = "id") private String sortBy; public int from(){ return (pageNo - 1) * pageSize; } public <T> Page<T> toMpPage(OrderItem ... orderItems) { Page<T> page = new Page<>(pageNo, pageSize); // 是否手動指定排序方式 if (orderItems != null && orderItems.length > 0) { for (OrderItem orderItem : orderItems) { page.addOrder(orderItem); } return page; } // 前端是否有排序字段 if (StringUtils.isNotEmpty(sortBy)){ OrderItem orderItem = new OrderItem(); orderItem.setAsc(isAsc); orderItem.setColumn(sortBy); page.addOrder(orderItem); } return page; } public <T> Page<T> toMpPage(String defaultSortBy, boolean isAsc) { if (StringUtils.isBlank(sortBy)){ sortBy = defaultSortBy; this.isAsc = isAsc; } Page<T> page = new Page<>(pageNo, pageSize); OrderItem orderItem = new OrderItem(); orderItem.setAsc(this.isAsc); orderItem.setColumn(sortBy); page.addOrder(orderItem); return page; } public <T> Page<T> toMpPageDefaultSortByCreateTimeDesc() { return toMpPage(Constant.DATA_FIELD_NAME_CREATE_TIME, false); } }
設(shè)計優(yōu)點
- 默認(rèn)分頁參數(shù),即使前端未傳分頁參數(shù),也不會報錯。
- 支持排序,可以根據(jù)前端傳遞的
sortBy
和isAsc
進(jìn)行排序。 - 與 MyBatis-Plus 兼容,直接轉(zhuǎn)換為
Page<T>
,減少重復(fù)代碼。
使用方式
在實際開發(fā)中,我們可以在 Service 層調(diào)用 toMpPage()
方法,將 PageQuery
轉(zhuǎn)換為 MyBatis-Plus 的 Page<T>
對象。
public Page<User> getUserList(PageQuery query) { Page<User> page = query.toMpPage(); return userMapper.selectPage(page, new QueryWrapper<>()); }
參數(shù)校驗的藝術(shù):從基礎(chǔ)校驗到深度防御
為什么參數(shù)校驗是系統(tǒng)安全的第一道防線?
在實際開發(fā)中,我們常遇到這樣的問題:
- 用戶輸入手機號為"1381234abcd"
- 訂單金額出現(xiàn)負(fù)數(shù)
- 狀態(tài)字段傳入非法數(shù)值
- 接口被惡意構(gòu)造異常參數(shù)攻擊
參數(shù)校驗如同系統(tǒng)的門衛(wèi),負(fù)責(zé):
- 攔截80%以上的常規(guī)攻擊
- 保證業(yè)務(wù)數(shù)據(jù)的有效性
- 提高代碼可讀性和健壯性
- 降低下游服務(wù)的校驗壓力
JSR 380規(guī)范的核心武器庫
基礎(chǔ)校驗實戰(zhàn)
@PostMapping("/create") public Result createCoupon(@Valid @RequestBody CouponFormDTO dto) { // 業(yè)務(wù)邏輯 } @Data public class CouponFormDTO { @NotNull(message = "優(yōu)惠券類型不能為空") private Integer couponType; @Range(min=1, max=10, message="限領(lǐng)數(shù)量超出范圍") private Integer limitCount; @EnumValid(enumeration = {0,1}, message="領(lǐng)取方式非法") private ReceiveEnums receiveType; }
常用注解矩陣:
注解 | 適用類型 | 適用場景 |
---|---|---|
@NotNull | 任意對象 | 確保字段不能為空 |
@NotBlank | String | 確保字符串不能為空 |
@NotEmpty | 集合/數(shù)組 | 確保列表有數(shù)據(jù) |
@Size | String/集合 | 限制長度或元素數(shù)量 |
@Min/@Max | 數(shù)值類型 | 限制最小/最大值 |
@Pattern | String | 正則表達(dá)式校驗 |
String | 郵箱格式校驗 | |
@Future | Date | 時間必須是未來時間 |
@Past | Date | 時間必須是過去時間 |
@Digits | 數(shù)值類型 | 限制整數(shù)位數(shù)和小數(shù)位數(shù) |
深度解析參數(shù)校驗原理
JSR 380 校驗流程
- HTTP 請求進(jìn)入 Controller 層,綁定參數(shù)到 DTO 對象。
**@Valid**
觸發(fā)校驗機制,調(diào)用 Hibernate Validator。- 執(zhí)行校驗邏輯,遍歷 DTO 字段并檢查注解規(guī)則。
- 校驗失敗拋出
**MethodArgumentNotValidException**
。 - 全局異常處理器捕獲異常,封裝并返回錯誤信息。
@Valid vs. @Validated 區(qū)別
特性 | @Valid | @Validated |
---|---|---|
作用范圍 | 單個 DTO | DTO + 分組校驗 |
分組支持 | 不支持 | 支持 |
適用場景 | 基礎(chǔ)校驗 | 復(fù)雜業(yè)務(wù)場景 |
@PostMapping("/update") public Result update(@Validated(UpdateGroup.class) @RequestBody UserDTO userDTO) { // 業(yè)務(wù)邏輯處理 }
自定義枚舉校驗的黑科技
數(shù)據(jù)庫設(shè)計的隱痛
我們在設(shè)計數(shù)據(jù)庫時通常會使用某個數(shù)字來代表某個狀態(tài),如:
CREATE TABLE coupon ( status TINYINT COMMENT '0-未激活 1-已生效 2-已過期' )
傳統(tǒng)校驗方式:
if (!Arrays.asList(0,1,2).contains(status)) { throw new IllegalArgumentException(); }
缺陷:
- 校驗邏輯分散
- 可維護(hù)性差
- 無法復(fù)用
自定義注解解決方案
定義枚舉校驗注解:
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = EnumValidator.class) public @interface EnumValid { int[] value() default {}; String message() default "非法枚舉值"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
實現(xiàn)校驗邏輯:
public class EnumValidator implements ConstraintValidator<EnumValid, Integer> { private Set<Integer> allowedValues = new HashSet<>(); @Override public void initialize(EnumValid constraintAnnotation) { Arrays.stream(constraintAnnotation.value()) .forEach(allowedValues::add); } @Override public boolean isValid(Integer value, ConstraintValidatorContext context) { if (value == null) return true; return allowedValues.contains(value); } }
原理深度解析
ConstraintValidator生命周期:
- 初始化:讀取注解配置
- 校驗時:執(zhí)行isValid方法
- 結(jié)果處理:返回布爾值
JSR 380規(guī)范實現(xiàn)要點:
- 校驗器發(fā)現(xiàn)機制:SPI方式加載
- 級聯(lián)校驗:支持對象嵌套校驗
- 分組校驗:實現(xiàn)不同場景的校驗規(guī)則
復(fù)雜校驗的終極方案
有時,僅僅靠我們的校驗比較復(fù)雜,這時我們可能需要自己來編寫校驗邏輯
我們可以通過自定義注解+AOP來幫我們實現(xiàn)
/** * 實現(xiàn)后在接口訪問時如果接口實現(xiàn)了這個接口 * 會被自動自行接口check進(jìn)行校驗 **/ public interface Checker<T> { /** * 用于實現(xiàn)validation不能校驗的數(shù)據(jù)邏輯 */ default void check(){ } default void check(T data){ } }
使用示例
Data @ApiModel(description = "章節(jié)") public class CataSaveDTO implements Checker { @ApiModelProperty("章、節(jié)、練習(xí)id") private Long id; @ApiModelProperty("目錄類型1:章,2:節(jié),3:測試") @NotNull(message = "") private Integer type; @ApiModelProperty("章節(jié)練習(xí)名稱") private String name; @ApiModelProperty("章排序,章一定要傳,小節(jié)和練習(xí)不需要傳") private Integer index; @ApiModelProperty("當(dāng)前章的小節(jié)或練習(xí)") @Size(min = 1, message = "不能出現(xiàn)空章") private List<CataSaveDTO> sections; @Override public void check() { //名稱為空校驗 if(type == CourseConstants.CataType.CHAPTER && StringUtils.isEmpty(name)) { throw new BadRequestException(CourseErrorInfo.Msg.COURSE_CATAS_SAVE_NAME_NULL); }else if(StringUtils.isEmpty(name)){ throw new BadRequestException(CourseErrorInfo.Msg.COURSE_CATAS_SAVE_NAME_NULL2); } //名稱長度問題 if (type == CourseConstants.CataType.CHAPTER && name.length() > 30){ throw new BadRequestException(CourseErrorInfo.Msg.COURSE_CATAS_SAVE_NAME_SIZE); }else if(name.length() > 30) { throw new BadRequestException(CourseErrorInfo.Msg.COURSE_CATAS_SAVE_NAME_SIZE2); } if(CollUtils.isEmpty(sections)){ throw new BadRequestException("不能出現(xiàn)空章"); } } }
接口方法參數(shù)校驗器
/** * 接口方法參數(shù)校驗器 **/ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ParamChecker { }
定義切面類
@Aspect @Slf4j @SuppressWarnings("all") public class CheckerAspect { @Before("@annotation(paramChecker)") public void before(JoinPoint joinPoint, ParamChecker paramChecker) { Object[] args = joinPoint.getArgs(); if(ArrayUtils.isNotEmpty(args)){ //遍歷方法參數(shù),參數(shù)是否實現(xiàn)了Checker接口 for (Object arg : args){ if(arg instanceof Checker) { //調(diào)用check方法,校驗業(yè)務(wù)邏輯 ((Checker)arg).check(); }else if(arg instanceof List){ //如果參數(shù)是一個集合也要校驗 CollUtils.check((List) arg); } } } } }
工具方法
/** * 集合校驗邏輯 * * @param data 要校驗的集合 * @param checker 校驗器 * @param <T> 集合元素類型 */ public static <T> void check(List<T> data, Checker<T> checker){ if(data == null){ return; } for (T t : data){ checker.check(t); } } /** * 集合校驗邏輯 * * @param data 要校驗的集合 * @param <T> 集合元素類型 */ public static <T extends Checker<T>> void check(List<T> data){ if(data == null){ return; } for (T t : data){ t.check(); } }
注解使用
@PostMapping("baseInfo/save") @ApiOperation("保存課程基本信息") @ParamChecker //校驗非業(yè)務(wù)限制的字段 public CourseSaveVO save(@RequestBody @Validated(CourseSaveBaseGroup.class) CourseBaseInfoSaveDTO courseBaseInfoSaveDTO) { return courseDraftService.save(courseBaseInfoSaveDTO); }
注意:切面類沒有納入ioc容器管理,如果是單體項目加上component注解即可,如果是多模塊項目,使用自動裝配功能
總結(jié)
到此這篇關(guān)于Java Web開發(fā)中的分頁與參數(shù)校驗舉例詳解的文章就介紹到這了,更多相關(guān)Java Web分頁與參數(shù)校驗內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java實現(xiàn)多級表頭和復(fù)雜表頭的導(dǎo)出功能
這篇文章主要為大家詳細(xì)介紹了Java實現(xiàn)多級表頭和復(fù)雜表頭的導(dǎo)出功能的相關(guān)知識,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2024-03-03基于Java代碼實現(xiàn)數(shù)字在數(shù)組中出現(xiàn)次數(shù)超過一半
這篇文章主要介紹了基于Java代碼實現(xiàn)數(shù)字在數(shù)組中出現(xiàn)次數(shù)超過一半的相關(guān)資料,需要的朋友可以參考下2016-02-02