kuitos / kuitos.github.io

📝Kuitos's Blog https://github.com/kuitos/kuitos.github.io/issues
https://kuitos.github.io/
MIT License
1.13k stars 81 forks source link

SpringMVC4.1之Controller层最佳实践 #9

Open kuitos opened 9 years ago

kuitos commented 9 years ago

SpringMVC4.1之Controller层最佳实践

原文写于 2014-09-28

前几天突发奇想想去看看spring现在到升级到什么版本了,有些啥New Features。结果发现了一个很人性化的新注解,刚好最近在构建客服系统新的接口层结构,然后重新研究了下spring mvc,一些成果跟大家分享一下(SpringMVC4.1的jackson版本升级到了2.x,不再支持Jackson1.x,同学们注意。详细代码请右转:seed )。

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

首先来介绍下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示例

@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>
// 指定增强范围为使用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错误的(更准确说是不合理的)配置一览表):

// 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,欢迎拍砖!

eclipse2x commented 8 years ago

if (LOGGER.isDebugEnabled()) { LOGGER.debug("--------> result:{}", JacksonMapper.toJsonString(result)); } 这种写法是多余的,直接 LOGGER.debug("--------> result:{}", JacksonMapper.toJsonString(result)); 就够了,slf4j 已经做了判断

xuyang2 commented 8 years ago

@eclipse2x 后一种写法,即使DEBUG未开启,也会执行 JacksonMapper.toJsonString(result), 而toJsonString是有开销的。

sherryriver commented 8 years ago

想问下楼主, 如果 service抛过来的异常 Controller层也可以使用这种统一的接口异常吗? 不需要再try。。catch了?

Lnybb commented 7 years ago

感谢分享,有所收获

KochamCie commented 7 years ago

@sherryriver you can try one try

monical1 commented 7 years ago

entity返回仅适合简单业务, 一些聚合或者计算数据用entity包装会更难适应变化, map返回更高效

kuitos commented 7 years ago

map 只是看起来“高效”,对于维护来说是灾难,聚合或计算数据也可以通过封装一组 entity 来做

YiuTerran commented 7 years ago

感谢楼主的分享,少走了不少弯路~

同时怀恋一下用Python的爽快=。=

mvity commented 6 years ago

定义好响应结构,Map响应更爽

yutaoli9655 commented 6 years ago

可以纳入教科书了

ldongxu commented 6 years ago

棒 棒 棒