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)文章
Eclipse使用maven搭建spring mvc圖文教程
這篇文章主要為大家分享了Eclipse使用maven搭建spring mvc圖文教程,感興趣的小伙伴們可以參考一下2016-05-05
springboot配置多數(shù)據(jù)源后mybatis攔截器失效的解決
這篇文章主要介紹了springboot配置多數(shù)據(jù)源后mybatis攔截器失效的解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-09-09
詳解Java數(shù)據(jù)庫連接JDBC基礎(chǔ)知識(操作數(shù)據(jù)庫:增刪改查)
這篇文章主要介紹了詳解Java數(shù)據(jù)庫連接JDBC基礎(chǔ)知識(操作數(shù)據(jù)庫:增刪改查),本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-01-01
java環(huán)境變量的配置方法圖文詳解【win10環(huán)境為例】
這篇文章主要介紹了java環(huán)境變量的配置方法,結(jié)合圖文形式詳細(xì)分析了win10環(huán)境下java環(huán)境變量的配置方法與相關(guān)操作注意事項,需要的朋友可以參考下2020-04-04
SpringBoot集成 Prometheus進(jìn)行高效監(jiān)控的實現(xiàn)
Prometheus作為一個開源的監(jiān)控和告警工具,以其強大的數(shù)據(jù)采集、存儲和查詢能力,受到了眾多開發(fā)者的青睞,本文主要介紹了SpringBoot集成 Prometheus進(jìn)行高效監(jiān)控的實現(xiàn),感興趣的可以了解一下2024-07-07
基于SqlSessionFactory的openSession方法使用
這篇文章主要介紹了SqlSessionFactory的openSession方法使用,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12

