翻譯:叩丁狼教育吳嘉俊
在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