SpringMVC4.1之Controller层最佳实践
原文写于 2014-09-28 https://github.com/kuitos/kuitos.github.io/issues/9
前几天突发奇想想去看看spring现在到升级到什么版本了,有些啥New Features。结果发现了一个很人性化的新注解,刚好最近在构建客服系统新的接口层结构,然后重新研究了下spring mvc,一些成果跟大家分享一下(SpringMVC4.1的jackson版本升级到了2.x,不再支持Jackson1.x,同学们注意。详细代码请右转:seed )。
先说说我们要实现的目标(接口层):
- 统一的响应体、请求体,规避Map、List作参数或者响应结果的方式(尤其是参数用Map来包装,这种代码有时候看起来真的让人很沮丧)
- 统一的错误信息
- 统一的请求数据校验
- 统一的接口异常捕获
首先来介绍下springMVC新增的一个很人性化的注解:
@RestController
@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会提示你哪些引入是无效的。
- 和 :component-scan会自动加上annotation-config功能,有了component-scan不用再写annotation-config了。参见spring官方reference
- applicationContext.xml中配置了,在springmvc-servlet.xml中又配置了,这样会导致容器中的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容器单独注册。
更多详细代码请访问:spring-mvc4-seed,欢迎拍砖!