原文地址:https://github.com/kuitos/kuitos.github.io/issues/9
先說(shuō)說(shuō)我們要實(shí)現(xiàn)的目標(biāo)(接口層):
統(tǒng)一的響應(yīng)體、請(qǐng)求體,規(guī)避Map、List作參數(shù)或者響應(yīng)結(jié)果的方式(尤其是參數(shù)用Map來(lái)包裝,這種代碼有時(shí)候看起來(lái)真的讓人很沮喪)
統(tǒng)一的錯(cuò)誤信息
統(tǒng)一的請(qǐng)求數(shù)據(jù)校驗(yàn)
統(tǒng)一的接口異常捕獲
首先來(lái)介紹下springMVC新增的一個(gè)很人性化的注解:
@RestController組合了@controller和@responsebody,使用該注解聲明的controller下的每一個(gè)@requestmapping方法,都會(huì)默認(rèn)加上@responsebody,即默認(rèn)該controller提供的全部是rest服務(wù),返回的不會(huì)是視圖。
@RestController
public class DemoRestController {
@Resource
private DemoService demoService;
@RequestMapping(value = "getUser", method = RequestMethod.GET)
public ResponseResult> getUser(String userName) {
// do something
}
}
基于開(kāi)頭提到的四個(gè)目標(biāo),我們以代碼的形式來(lái)說(shuō)明一下具體的實(shí)現(xiàn)方案
統(tǒng)一的請(qǐng)求體、響應(yīng)體
思路:所有的rest響應(yīng)均返回一致的數(shù)據(jù)格式,所有的post請(qǐng)求均采用bean接收。(不要使用List、Map萬(wàn)金油。。。)
目的:統(tǒng)一的響應(yīng)體能確保rest接口的一致性,同時(shí)可以提供給前端js一個(gè)可封裝http請(qǐng)求的環(huán)境(如:封裝的http錯(cuò)誤日志、結(jié)果攔截等)(吐槽一句,有時(shí)候我們想在前端做統(tǒng)一的響應(yīng)攔截和日志處理,可是接口返回的數(shù)據(jù)格式五花八門(mén),實(shí)在讓人無(wú)能為力。。。) post請(qǐng)求均采用bean接收可以使得代碼更具可讀性,直接通過(guò)bean可以獲知接口所需參數(shù),而不是一行行讀代碼看你從map里面get出了些什么玩意。
ps:部分思路來(lái)源于忠誠(chéng)度項(xiàng)目接口實(shí)現(xiàn)方式,特此表示感謝!
統(tǒng)一響應(yīng)體
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class ResponseResult {
private boolean success;
private String message;
private T data;
/* 不提供直接設(shè)置errorCode的接口,只能通過(guò)setErrorInfo方法設(shè)置錯(cuò)誤信息 */
private String errorCode;
private ResponseResult() {
}
.........
}
統(tǒng)一結(jié)果生成方式
public class RestResultGenerator {
private static final Logger LOGGER = LoggerFactory.getLogger(RestResultGenerator.class);
/**
* 生成響應(yīng)成功(帶正文)的結(jié)果
*
* @param data? ? 結(jié)果正文
* @param message 成功提示信息
* @return ResponseResult
*/
public static ResponseResult genResult(T data, String message) {
ResponseResult result = ResponseResult.newInstance();
result.setSuccess(true);
result.setData(data);
result.setMessage(message);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("--------> result:{}", JacksonMapper.toJsonString(result));
}
return result;
}
........
}
調(diào)用示例
@RestController
public class DemoRestController {
@Resource
private DemoService demoService;
@RequestMapping(value = "getUser", method = RequestMethod.GET)
public ResponseResult> getUser(String userName) {
List userList = demoService.getUser(userName);
return RestResultGenerator.genResult(userList, "成功!");
}
}
統(tǒng)一的錯(cuò)誤信息
思路:需要使用errorCode來(lái)聲明的錯(cuò)誤信息,統(tǒng)一通過(guò)enum定義,ResponseResult不提供單獨(dú)設(shè)置errorCode的接口
public class RestResultGenerator {
private static final Logger LOGGER = LoggerFactory.getLogger(RestResultGenerator.class);
.......
/**
* 生成響應(yīng)失敗(帶errorCode)的結(jié)果
*
* @param responseErrorEnum 失敗信息
* @return ResponseResult
*/
public static ResponseResult genErrorResult(ResponseErrorEnum responseErrorEnum) {
ResponseResult result = ResponseResult.newInstance();
result.setSuccess(false);
result.setErrorInfo(responseErrorEnum);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("--------> result:{}", JacksonMapper.toJsonString(result));
}
return result;
}
}
統(tǒng)一的請(qǐng)求數(shù)據(jù)校驗(yàn)
思路:基于注解的bean校驗(yàn),采用JSR-303的Bean Validation。
目的:xx參數(shù)不能為空,格式必須為xxx等校驗(yàn)就不用在接口中去硬編碼干擾業(yè)務(wù)邏輯了。讓框架統(tǒng)一幫忙驗(yàn)證
bean示例
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class User {
@NotBlank
private String userName;
@NotNull
@Max(150)
@Min(1)
private Integer age;
private User() {
}
}
調(diào)用示例
@RestController
public class DemoRestController {
@Resource
private DemoService demoService;
@RequestMapping(value = "saveUser", method = RequestMethod.POST)
public ResponseResult saveUser(@Valid @RequestBody User user, Errors errors) {
if (errors.hasErrors()) {
return RestResultGenerator.genErrorResult(ResponseErrorEnum.ILLEGAL_PARAMS);
} else {
demoService.saveUser(user);
return RestResultGenerator.genResult("保存成功!");
}
}
}
由于依賴于JSR-303規(guī)范,我們的pom文件需要加入新的依賴
maven配置
javax.validation
validation-api
1.1.0.Final
org.hibernate
hibernate-validator
5.0.1.Final
統(tǒng)一的接口異常捕獲
思路:起初想通過(guò)代碼中try..catch的方式捕獲異常,然后通過(guò)RestResultGenerator生成錯(cuò)誤信息。后來(lái)覺(jué)得這種方式太傻了,然后想到通過(guò)aop的方式,以Controller的RequestMapping為切面織入異常捕獲代碼,然后返回錯(cuò)誤信息。再后來(lái)發(fā)現(xiàn)springMVC早在3.x時(shí)代便提供了@ExceptionHandler注解。。。再后來(lái)又發(fā)現(xiàn)了@controlleradvice。。。這不就是我想要的嘛!! 可見(jiàn)使用一門(mén)技術(shù)前對(duì)其有一定的系統(tǒng)認(rèn)知該多么重要,不僅能避免重復(fù)造輪子還能避免坑自己坑別人
目的:無(wú)侵入式的異常捕獲,不干擾業(yè)務(wù)邏輯
名詞解釋:
ExceptionHandler:顧名思義,異常處理器。單獨(dú)的ExceptionHandler沒(méi)什么特別之處,配合ControllerAdvice就會(huì)分分鐘變神器!
ControllerAdvice: 從命名我們就能猜到,這家伙肯定是基于aop實(shí)現(xiàn)的一個(gè)東西,用于增強(qiáng)controller功能的。它可以把@controlleradvice注解內(nèi)部使用@ExceptionHandler、@initbinder、@modelattribute注解的方法應(yīng)用到所有的 @requestmapping注解的方法。其中ExceptionHandler實(shí)際作用最大,其他兩個(gè)用的少。Spring3.x時(shí)代ControllerAdvice會(huì)增強(qiáng)一個(gè)servlet中的所有controller,Spring4以后 ControllerAdvice又得到了增強(qiáng),可以應(yīng)用于controller的子類(lèi),控制范圍更精確。
代碼示例
使用controllerAdvice實(shí)現(xiàn)的全局異常處理
// 指定增強(qiáng)范圍為使用RestContrller注解的控制器
@ControllerAdvice(annotations = RestController.class)
public class RestExceptionHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(RestExceptionHandler.class);
/**
* bean校驗(yàn)未通過(guò)異常
*
* @see javax.validation.Valid
* @see org.springframework.validation.Validator
* @see org.springframework.validation.DataBinder
*/
@ExceptionHandler(UnexpectedTypeException.class)
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
private ResponseResult illegalParamsExceptionHandler(UnexpectedTypeException e) {
LOGGER.error("--------->請(qǐng)求參數(shù)不合法!", e);
return RestResultGenerator.genErrorResult(ResponseErrorEnum.ILLEGAL_PARAMS);
}
}
Controller里面不用寫(xiě)任何多余的代碼,如果@Valid校驗(yàn)失敗接口會(huì)拋出UnexpectedTypeException從而被ControllerAdvice捕獲并返回錯(cuò)誤信息,httpstatus為503 Bad Request 錯(cuò)誤
@RestController
public class DemoRestController {
@Resource
private DemoService demoService;
@RequestMapping(value = "saveUser", method = RequestMethod.POST)
public ResponseResult saveUser(@Valid @RequestBody User user) {
demoService.saveUser(user);
return RestResultGenerator.genResult("保存成功!");
}
}
注意這里參數(shù)列表里面就不要加Errors或其子類(lèi)作參數(shù)了,有這個(gè)參數(shù)校驗(yàn)失敗就不會(huì)拋異常,而是把錯(cuò)誤信息填充到Errors對(duì)象中。
寫(xiě)在最后
至此,在Controller層我們一開(kāi)始的目標(biāo)基本上都已經(jīng)達(dá)成了,之后我們編寫(xiě)接口只需要實(shí)現(xiàn)業(yè)務(wù)邏輯,參數(shù)校驗(yàn)、異常捕獲等工作全部交由外圍設(shè)施處理,而不是手動(dòng)編碼做重復(fù)工作。SpringMVC部分還有很多已有的東西我們沒(méi)有開(kāi)發(fā),有點(diǎn)暴殄天物的感覺(jué)。磨刀不誤砍柴工,這樣才能避免重復(fù)造輪子跟寫(xiě)出可維護(hù)的代碼。雖然是碼農(nóng),但是也不能只滿足于復(fù)制粘貼吧。。。
附(目前大部分項(xiàng)目中關(guān)于springMVC錯(cuò)誤的(更準(zhǔn)確說(shuō)是不合理的)配置一覽表):
schema無(wú)效引入:也就是xml頭部引入的xsd,很多都是無(wú)效的引入,不過(guò)切換到idea之后IDE會(huì)提示你哪些引入是無(wú)效的。
和 :component-scan會(huì)自動(dòng)加上annotation-config功能,有了component-scan不用再寫(xiě)annotation-config了。參見(jiàn)spring官方reference
applicationContext.xml中配置了context:component-scan,在springmvc-servlet.xml中又配置了context:component-scan,這樣會(huì)導(dǎo)致容器中的bean注冊(cè)兩次。
更合理的配置
// applicationContext.xml
// springmvc-servlet.xml
spring容器不注冊(cè)controller層組件,controller組件由springMVC容器單獨(dú)注冊(cè)。