SpringMVC4.1之Controller層實踐
注:SpringMVC4.1的jackson版本升級到了2.x,不再支援Jackson1.x。
先說說我們要實現的目標(介面層):
- 統一的響應體、請求體,規避Map、List作引數或者響應結果的方式(尤其是引數用Map來包裝,這種程式碼有時候看起來真的讓人很沮喪)
- 統一的錯誤資訊
- 統一的請求資料校驗
- 統一的介面異常捕獲
首先來介紹下springMVC新增的一個很人性化的註解:
@RestController組合了@Controller和@ResponseBody,使用該註解宣告的controller下的每一個@RequestMapping方法,都會預設加上@ResponseBody,即預設該controller提供的全部是rest服務,返回的不會是檢視。
@RestController public class DemoRestController { @Resource private DemoService demoService; @RequestMapping(value = "getUser", method = RequestMethod.GET) public ResponseResult<List<User>> getUser(String userName) { // do something } }
基於開頭提到的四個目標,我們以程式碼的形式來說明一下具體的實現方案
- 統一的請求體、響應體
思路:所有的rest響應均返回一致的資料格式,所有的post請求均採用bean接收。(不要使用List、Map萬金油。。。)
目的:統一的響應體能確保rest介面的一致性,同時可以提供給前端js一個可封裝http請求的環境(如:封裝的http錯誤日誌、結果攔截等)(吐槽一句,有時候我們想在前端做統一的響應攔截和日誌處理,可是介面返回的資料格式五花八門,實在讓人無能為力。。。) post請求均採用bean接收可以使得程式碼更具可讀性,直接通過bean可以獲知介面所需引數,而不是一行行讀程式碼看你從map裡面get出了些什麼玩意。
ps:部分思路來源於忠誠度專案介面實現方式,特此表示感謝!
統一響應體
@JsonInclude(JsonInclude.Include.NON_EMPTY) public class ResponseResult<T> { private boolean success; private String message; private T data; /* 不提供直接設定errorCode的介面,只能通過setErrorInfo方法設定錯誤資訊 */ private String errorCode; private ResponseResult() { } ......... }
統一結果生成方式
public class RestResultGenerator { private static final Logger LOGGER = LoggerFactory.getLogger(RestResultGenerator.class); /** * 生成響應成功(帶正文)的結果 * * @param data 結果正文 * @param message 成功提示資訊 * @return ResponseResult */ public static <T> ResponseResult<T> genResult(T data, String message) { ResponseResult<T> result = ResponseResult.newInstance(); result.setSuccess(true); result.setData(data); result.setMessage(message); if (LOGGER.isDebugEnabled()) { LOGGER.debug("--------> result:{}", JacksonMapper.toJsonString(result)); } return result; } ........ }
呼叫示例
@RestController public class DemoRestController { @Resource private DemoService demoService; @RequestMapping(value = "getUser", method = RequestMethod.GET) public ResponseResult<List<User>> getUser(String userName) { List<User> userList = demoService.getUser(userName); return RestResultGenerator.genResult(userList, "成功!"); } }
- 統一的錯誤資訊
思路:需要使用errorCode來宣告的錯誤資訊,統一通過enum定義,ResponseResult不提供單獨設定errorCode的介面
public class RestResultGenerator { private static final Logger LOGGER = LoggerFactory.getLogger(RestResultGenerator.class); ....... /** * 生成響應失敗(帶errorCode)的結果 * * @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; } }
- 統一的請求資料校驗
思路:基於註解的bean校驗,採用JSR-303的Bean Validation。
目的:xx引數不能為空,格式必須為xxx等校驗就不用在介面中去硬編碼干擾業務邏輯了。讓框架統一幫忙驗證
bean示例
@JsonInclude(JsonInclude.Include.NON_EMPTY) public class User { @NotBlank private String userName; @NotNull @Max(150) @Min(1) private Integer age; private User() { } }
呼叫示例
@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規範,我們的pom檔案需要加入新的依賴
maven配置
<dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>1.1.0.Final</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId> <version>5.0.1.Final</version> </dependency>
-
統一的介面異常捕獲
思路:起初想通過程式碼中try..catch的方式捕獲異常,然後通過RestResultGenerator生成錯誤資訊。後來覺得這種方式太傻了,然後想到通過aop的方式,以Controller的RequestMapping為切面織入異常捕獲程式碼,然後返回錯誤資訊。再後來發現springMVC早在3.x時代便提供了@ExceptionHandler註解。。。再後來又發現了@ControllerAdvice。。。這不就是我想要的嘛!! 可見使用一門技術前對其有一定的系統認知該多麼重要,不僅能避免重複造輪子還能避免坑自己坑別人
目的:無侵入式的異常捕獲,不干擾業務邏輯名詞解釋:
- ExceptionHandler:顧名思義,異常處理器。單獨的ExceptionHandler沒什麼特別之處,配合ControllerAdvice就會分分鐘變神器!
- ControllerAdvice: 從命名我們就能猜到,這傢伙肯定是基於aop實現的一個東西,用於增強controller功能的。它可以把@ControllerAdvice註解內部使用@ExceptionHandler、@InitBinder、@ModelAttribute註解的方法應用到所有的 @RequestMapping註解的方法。其中ExceptionHandler實際作用最大,其他兩個用的少。Spring3.x時代ControllerAdvice會增強一個servlet中的所有controller,Spring4以後 ControllerAdvice又得到了增強,可以應用於controller的子類,控制範圍更精確。
程式碼示例
使用controllerAdvice實現的全域性異常處理
// 指定增強範圍為使用RestContrller註解的控制器 @ControllerAdvice(annotations = RestController.class) public class RestExceptionHandler { private static final Logger LOGGER = LoggerFactory.getLogger(RestExceptionHandler.class); /** * bean校驗未通過異常 * * @see javax.validation.Valid * @see org.springframework.validation.Validator * @see org.springframework.validation.DataBinder */ @ExceptionHandler(UnexpectedTypeException.class) @ResponseBody @ResponseStatus(HttpStatus.BAD_REQUEST) private <T> ResponseResult<T> illegalParamsExceptionHandler(UnexpectedTypeException e) { LOGGER.error("--------->請求引數不合法!", e); return RestResultGenerator.genErrorResult(ResponseErrorEnum.ILLEGAL_PARAMS); } }
Controller裡面不用寫任何多餘的程式碼,如果@Valid校驗失敗介面會丟擲UnexpectedTypeException從而被ControllerAdvice捕獲並返回錯誤資訊,httpstatus為503 Bad Request 錯誤
@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("儲存成功!"); } }
注意這裡引數列表裡面就不要加Errors或其子類作引數了,有這個引數校驗失敗就不會拋異常,而是把錯誤資訊填充到Errors物件中。
寫在最後
至此,在Controller層我們一開始的目標基本上都已經達成了,之後我們編寫介面只需要實現業務邏輯,引數校驗、異常捕獲等工作全部交由外圍設施處理,而不是手動編碼做重複工作。SpringMVC部分還有很多已有的東西我們沒有開發,有點暴殄天物的感覺。磨刀不誤砍柴工,這樣才能避免重複造輪子跟寫出可維護的程式碼。雖然是碼農,但是也不能只滿足於複製貼上吧。。。
附(目前大部分專案中關於springMVC錯誤的(更準確說是不合理的)配置一覽表):
- schema無效引入:也就是xml頭部引入的xsd,很多都是無效的引入,不過切換到idea之後IDE會提示你哪些引入是無效的。
- <context:annotation-config /> 和 <context:component-scan />:component-scan會自動加上annotation-config功能,有了component-scan不用再寫annotation-config了。參見spring官方reference
- applicationContext.xml中配置了context:component-scan,在springmvc-servlet.xml中又配置了context:component-scan,這樣會導致容器中的bean註冊兩次。
更合理的配置
// applicationContext.xml <context:component-scan base-package="com.shuyun.channel"> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller" /> <context:exclude-filter type="annotation" expression="org.springframework.web.bind.annotation.RestController" /> <context:exclude-filter type="annotation" expression="org.springframework.web.bind.annotation.ControllerAdvice" /> </context:component-scan> // springmvc-servlet.xml <context:component-scan base-package="com.shuyun.channel" use-default-filters="false"> <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" /> <context:include-filter type="annotation" expression="org.springframework.web.bind.annotation.RestController" /> <context:include-filter type="annotation" expression="org.springframework.web.bind.annotation.ControllerAdvice" /> </context:component-scan>
spring容器不註冊controller層元件,controller元件由springMVC容器單獨註冊。