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

