Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
kuitos opened this issue Aug 23, 2015 · 11 comments
Open

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

kuitos opened this issue Aug 23, 2015 · 11 comments

Comments

@kuitos
Copy link
Owner

kuitos commented Aug 23, 2015

SpringMVC4.1之Controller层最佳实践

原文写于 2014-09-28

前几天突发奇想想去看看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会提示你哪些引入是无效的。
  • <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容器单独注册。
更多详细代码请访问:spring-mvc4-seed,欢迎拍砖!

@eclipse2x
Copy link

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

@xuyang2
Copy link

xuyang2 commented Sep 30, 2016

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

@sherryriver
Copy link

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

@Lnybb
Copy link

Lnybb commented Mar 13, 2017

感谢分享,有所收获

@KochamCie
Copy link

@sherryriver you can try one try

@monical1
Copy link

monical1 commented Jun 7, 2017

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

@kuitos
Copy link
Owner Author

kuitos commented Jun 8, 2017

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

@YiuTerran
Copy link

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

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

@mvity
Copy link

mvity commented Jan 5, 2018

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

@yutaoli9655
Copy link

可以纳入教科书了

@ldongxu
Copy link

ldongxu commented Sep 27, 2018

棒 棒 棒

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests