Spring的@RequestParam對象綁定

翻譯:叩丁狼教育吳嘉俊

在Spring中,如果在方法參數(shù)列表中使用@RequestParam標(biāo)注多個(gè)參數(shù),會讓映射方法的可讀性大大降低。

如果映射請求的參數(shù)只有一兩個(gè)的話,使用@RequestParam會非常直觀,但是如果參數(shù)列表越來越長,就很容易暈菜。

雖然我們不能直接在參數(shù)對象中使用@RequestParam標(biāo)簽,但是并不代表沒有其他的辦法。這篇文章就會演示怎么使用對象的封裝來簡化多個(gè)@RequestParams標(biāo)簽。

【注:SpringMVC注入請求參數(shù)到對象中,這個(gè)對于很多開發(fā)是再正常不過的,但是這里強(qiáng)調(diào)的是使用@RequestParam來綁定參數(shù),因?yàn)锧RequestParam可以對綁定參數(shù)有更多的限制】

過長的@RequestParam列表

不管是controller還是其他的類,過長的參數(shù)列表會讓代碼的可讀性變差,這一點(diǎn)是所有開發(fā)人員都認(rèn)同的。更不要說,如果大量的參數(shù)的類型還是一致的情況下,參數(shù)就更容易混淆了。

很多代碼檢查工具,都會把方法的參數(shù)個(gè)數(shù)作為檢查條件,也是因?yàn)檫^長的參數(shù)列表被認(rèn)為是一種錯(cuò)誤的代碼規(guī)范。

常見的一種解決方案,就是把一組參數(shù)合并起來,并作為應(yīng)用的獨(dú)立的一層。常見的,這組參數(shù)可以合并到一個(gè)對象中,并給予這個(gè)對象一個(gè)恰當(dāng)?shù)拿旨纯伞?/p>

我們來看一個(gè)GET請求服務(wù)端的例子:

@RestController
@RequestMapping("/products")
class ProductController {

   //...

   @GetMapping
   List<Product> searchProducts(@RequestParam String query,
                                @RequestParam(required = false, defaultValue = "0") int offset,
                                @RequestParam(required = false, defaultValue = "10") int limit) {
       return productRepository.search(query, offset, limit);
   }

}

雖然該方法只有三個(gè)參數(shù),但是參數(shù)列表很容易增長的,比如既然代碼中是查詢商品服務(wù),那么常常需要包含按照一些額外的過濾條件進(jìn)行排序等操作。在我們的代碼中,因?yàn)閰?shù)是直接傳遞給數(shù)據(jù)連接層,所以我們可以直接使用ParameterObject模式來處理【注:ParameterObject就是把參數(shù)組裝成對象】。

使用@RequestParam綁定POJO

根據(jù)我的經(jīng)驗(yàn),很多開發(fā)沒有替換較長的@RequestParams列表,主要還是因?yàn)樗麄儾恢烙惺裁刺娲姆桨?,因?yàn)樵赟pring的文檔中沒有提及。

下面,我們就開始來闡述替換的方案。首先我們可以使用一個(gè)POJO來包裝這些參數(shù)。

@GetMapping
List<Product> searchProducts(ProductCriteria productCriteria) {
   return productRepository.search(productCriteria);
}

就已經(jīng)完成了!

這個(gè)POJO本身沒有要求額外的注解,但是POJO本身必須包含和請求參數(shù)完全匹配的字段,標(biāo)準(zhǔn)的setter/getter,和一個(gè)無參的構(gòu)造器:

class ProductCriteria {

   private String query;
   private int offset;
   private int limit;

   ProductCriteria() {
   }

   public String getQuery() {
       return query;
   }

   public void setQuery(String query) {
       this.query = query;
   }

   // other getters/setters

}

在POJO中對請求參數(shù)進(jìn)行校驗(yàn)

雖然上面的案例已經(jīng)可以正常使用,但是我們知道,使用@RequestParam注解,不僅僅只是為了綁定請求參數(shù),一個(gè)非常重要的功能是,我們可以對綁定的參數(shù)請求驗(yàn)證,比如參數(shù)是否必要,如果請求中缺少該參數(shù),則我們的服務(wù)端可以拒絕該請求。

為了達(dá)到相同的功能,我們常常使用的替換方案是使用Java Bean Validation。java有很多內(nèi)置的實(shí)現(xiàn),我們也可以創(chuàng)建自己的bean驗(yàn)證器。

回到我們的POJO,我們想為我們的POJO中的字段添加驗(yàn)證規(guī)則。如果想模仿@RequestParam(required = false)的表現(xiàn),我們可以使用@NotNull注解在對應(yīng)的字段上即可。

在更多的情況下,我們一般使用@NotBlack多于@NotNull,因?yàn)锧NotBlank考慮了空字符串的情況。

final class ProductCriteria {

   @NotBlank
   private String query;
   @Min(0)
   private int offset;
   @Min(1)
   private int limi;

   // ...

}

這里務(wù)必注意一點(diǎn):

如果僅僅只是在對象的字段上添加驗(yàn)證注解是不夠的。

一定要在controller的方法參數(shù)里誒包中,在POJO對應(yīng)的參數(shù)前加上@Valid注解。該注解會讓Spring在綁定參數(shù)前執(zhí)行校驗(yàn)動作。

@GetMapping
List<Product> searchProducts(@Valid ProductCriteria productCriteria) {
   // ...
}

在POJO中設(shè)置請求參數(shù)的默認(rèn)值

@RequestParam注解的另一個(gè)非常有用的功能就是設(shè)置參數(shù)的默認(rèn)值。

如果我們使用POJO的方式來綁定參數(shù),沒有什么特別牛逼的方法,只需要在定義參數(shù)的時(shí)候設(shè)置好字段的默認(rèn)值就行了。如果請求中沒有該參數(shù),Spring不會把參數(shù)的默認(rèn)值覆蓋為null的。

private int offset = 0;
private int limit = 10;

綁定多個(gè)參數(shù)對象

一般情況下,我們也不會強(qiáng)行把所有請求參數(shù)全部封裝到一個(gè)對象中,我們可以把請求參數(shù)按照功能分布到多個(gè)POJO中。

為了驗(yàn)證這點(diǎn),我們在查詢方法中,添加一個(gè)排序的功能。首先,我們需要一個(gè)額外的對象,并添加一些校驗(yàn)約束:

final class SortCriteria {

   @NotNull
   private SortOrder order;
   @NotBlank
   private String sortAttribute;

   // constructor, getters/setters

}

在controller中,我們只需要把這個(gè)POJO作為另一個(gè)參數(shù)即可。但是仍然注意,想讓校驗(yàn)生效,還是需要在參數(shù)對象前添加@Valid注解。

@GetMapping
List<Product> searchProducts(@Valid ProductCriteria productCriteria, @Valid SortCriteria sortCriteria) {
   // ...
}

內(nèi)嵌對象

另一種處理請求參數(shù)對象的方式是使用組合。參數(shù)綁定對這種內(nèi)嵌對象同樣適用。

下面我們給出一個(gè)改進(jìn)的例子,把查詢對象移動到ProductCriteria中。

要讓內(nèi)置對象的字段能夠執(zhí)行驗(yàn)證,我們需要在內(nèi)置對象對應(yīng)的字段上添加@Valid注解。注意一點(diǎn)的是,如果這個(gè)內(nèi)置對象的字段是null,Spring是不會校驗(yàn)這個(gè)屬性的,這可以簡單理解為,所有的內(nèi)置對象屬性都是可選的。如果想避免這種情況,在內(nèi)置對象的字段上添加@NotNull注解。

final class ProductCriteria {

   @NotNull//注意這個(gè)@NotNull注解
   @Valid
   private SortCriteria sort;

   // ...

}

HTTP請求參數(shù)必須按照參數(shù)路徑的方式命名。比如我們的例子中,請求參數(shù)就必須是:

sort.order=ASC&sort.attribute=name

不可變的DTO

現(xiàn)在,我們發(fā)現(xiàn)一個(gè)趨勢,越來越多的開發(fā)會把傳統(tǒng)的POJO中的setter方法去掉,讓POJO變成一個(gè)不可變對象。

不可變對象有很多的好處,但是在我看來,最大的優(yōu)勢在于便于維護(hù)。

在你的開發(fā)中,是否有這樣的情況,開著debug,在整個(gè)應(yīng)用大量的代碼中,去追蹤一個(gè)對象的狀態(tài)是怎么變化的?在什么地方,一個(gè)狀態(tài)發(fā)生了什么樣的變化?什么情況下,這個(gè)對象狀態(tài)需要修改?對于很多對象來說,僅僅從setter方法的名字來看,是很難看出具體的業(yè)務(wù)邏輯的。

當(dāng)Spring框架剛被創(chuàng)建出來的時(shí)候,是嚴(yán)格按照J(rèn)ava Bean規(guī)范來開發(fā)的。但是,時(shí)至今日,很多過去推崇的模式,已經(jīng)變成了反模式。

要去掉setter方法,綁定請求參數(shù),有兩種方式,通過構(gòu)造器或者字段直接綁定。但是目前沒有一種非常簡單的辦法,直接通過構(gòu)造方法將請求參數(shù)綁定,因?yàn)槟J(rèn)的構(gòu)造方法是必須的。雖然我們可以把POJO的構(gòu)造方法變?yōu)閜rivate的,并移除掉setter方法,從外部訪問來看,確實(shí)變成了私有的,但是這種方式有一定的缺陷,比如內(nèi)部組合對象無法使用這種方式。

默認(rèn)情況下,Spring要求通過字段的setter方法來綁定參數(shù),但是我們可以通過自定義的綁定器(binder)來直接把請求參數(shù)通過字段綁定。

為了可以在我們整個(gè)應(yīng)用中,都使用這種綁定方式,我么你可以定義一個(gè)controller advice組件。通過@InitBinder 注解,來修改默認(rèn)的綁定請求參數(shù)方法。

@ControllerAdvice
class BindingControllerAdvice {

   @InitBinder
   public void initBinder(WebDataBinder binder) {
       binder.initDirectFieldAccess();
   }

}

當(dāng)我們創(chuàng)建好這個(gè)類之后,我們就可以把POJO中所有的setter方法去掉,讓我們的POJO成為不可變對象。

final class ProductCriteria {

   @NotBlank
   private String query;
   @Min(0)
   private int offset = 0;
   @Min(1)
   private int limit = 10;

   private ProductCriteria() {
   }

   public String getQuery() {
       return query;
   }

   public int getOffset() {
       return offset;
   }

   public int getLimit() {
       return limit;
   }

}

小結(jié)

在本文中,我們總結(jié)了在Spring MVC控制器中使用參數(shù)對象來簡化過長的@RequestParam參數(shù)列表綁定;并討論了如果使用不可變的DTO對象來替換簡單的POJO。

原文:https://www.javacodegeeks.com/2018/10/how-bind-requestparam-object-spring.html

想獲取更多技術(shù)視頻,請前往叩丁狼官網(wǎng):http://www.wolfcode.cn/openClassWeb_listDetail.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容