SpringMVC4.1之Controller层最佳实践

原文地址:https://github.com/kuitos/kuitos.github.io/issues/9

先说说我们要实现的目标(接口层):

统一的响应体、请求体,规避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> 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 {

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 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;

}

........

}

调用示例

@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, "成功!");

}

}

统一的错误信息

思路:需要使用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配置

javax.validation

validation-api

1.1.0.Final

org.hibernate

hibernate-validator

5.0.1.Final

统一的接口异常捕获

思路:起初想通过代码中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 ResponseResult 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中配置了context:component-scan,在springmvc-servlet.xml中又配置了context:component-scan,这样会导致容器中的bean注册两次。

更合理的配置

// applicationContext.xml

// springmvc-servlet.xml

spring容器不注册controller层组件,controller组件由springMVC容器单独注册。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容