1. 程式人生 > >SpringMVC4.1之Controller層實踐

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容器單獨註冊。