开源了,优雅的Controller,应该这样写!

2 阅读5分钟

关注我的公众号:【编程朝花夕拾】,可获取首发内容。

01 引言

你的Controller(控制层)还在通过if-else校验参数?还在通过try-catch捕捉异常么?还在因为参数校验不通过,给客户端响应错误信息么......

我们看看优雅的控制层是怎么写的?

02 传统控制层的写法

2.1 参数校验

@RequestMapping("/foo")
public JsonResult login(String cellphone,String name) {
    String cellphone = C2BLoginUtil.getLoginCellphone();
    if (StringUtils.isBlank(cellphone)) {
        return new JsonResult(false,"手机号不能为空!");
    }
    if (StringUtils.isBlank(name)) {
        return new JsonResult(false,"姓名不能为空!");
    }
    
    // ......
 
    return new JsonResult(true);
}

在控制层内实现参数的校验,如果参数较多,就会出现大量的校验,代码就会显的很臃肿。如果多个方法复用参数,每个方法都要去写一遍,造成代码冗余。

2.2 异常的捕捉

@RequestMapping("/foo")
public JsonResult login(String cellphone,String name) {
    try {
        // ...
    } catch (Exception e) {
        log.error("服务调用异常", e);
         return new JsonResult(false, "服务调用异常");
    } 
    
    
    // ......
 
    return new JsonResult(true);
}

每一个方法都需要异常捕获,这也是代码冗余的体现。

03 优雅的写法

统一参数校验, 统一异常捕获。整合控制才能重复的逻辑,减少代码冗余。

3.1 统一参数处理

假如有一个Book 类需要校验,我们需要将参数的校验,统一处理。然后服务端只需要一个注解就可轻松解决,还可以复用。

@Data
public class Book implements Serializable {

    private static final long serialVersionUID = -4746928436252252305L;

    /**
     *  ID
     */
    private Integer bookId;

    /**
     *  名称
     */
    @Length(min = 5, max = 20, message = "书名的长度必须在5~20之间")
    private String bookName;

    /**
     *  定价
     */
    @Min(value = 15, message = "书的价格不能低于15元")
    @Max(value = 100, message = "书的价格不能超过100元")
    private BigDecimal bookPrice;

    /**
     *  发布日期
     */
    @NotNull(message = "publishDate 不能为空")
    private Date publishDate;

    /**
     *  编写完成日期
     */
    @NotNull(message = "finishDateTime 不能为空")
    private LocalDateTime finishDateTime;

    /**
     * script
     */
    private String script;

}

服务端的处理

只需要一个@Valid注解就可完美的解决参数校验的问题。

@RequestMapping("/testFormData")  
public String testFormData(@Valid Book book){
    System.out.println(book);
    return "success";
}

是不是少了好多代码量。

3.2 统一异常捕获

将所有的异常放在一起处理,控制层就不需要处理了,如果需要只需要抛出异常即可。

@RestControllerAdvice
public class GlobalExceptionHandler 

    public static String LOG_TEMPLATE = "请求路径[url]={},[%s] 异常信息:";
    public static String REQUEST_PARAMS_EX_TEMPLATE = "参数[%s]:%s";
    public static String REQUEST_PARAMS_MISS_TEMPLATE = "请求参数缺失,缺失的参数:%s";

    @Autowired(required = false)
    private GlobalExceptionResponseResolver globalExceptionResponseResolver;

    /**
     * 校验参数绑定异常
     *
     * @author ws
     * @date 2024/1/29 11:13
     * @param e
     */
    @ExceptionHandler(BindException.class)
    public Object bindExceptionHandler(HttpServletRequest request, BindException e) {
        log.error(String.format(LOG_TEMPLATE, "bindExceptionHandler"), request.getRequestURL(), e);
        return doGenerateResult(e, getErrorMsgDefault(e.getBindingResult().getFieldErrors(), e.getMessage()));
    }

    /**
     * 方法参数校验异常处理
     *
     * @author ws
     * @date 2024/1/29 13:25
     * @param e
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Object methodArgumentNotValidExceptionHandler(HttpServletRequest request, MethodArgumentNotValidException e) {
        log.error(String.format(LOG_TEMPLATE, "methodArgumentNotValidExceptionHandler"), request.getRequestURL(), e);
        return doGenerateResult(e, getErrorMsgDefault(e.getBindingResult().getFieldErrors(), e.getMessage()));
    }

    /**
     * 参数校验配合@Requestparam的异常
     *
     * @author ws
     * @date 2024/3/7 16:29
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public Object constraintViolationExceptionExceptionHandler(HttpServletRequest request, ConstraintViolationException e) {
        log.error(String.format(LOG_TEMPLATE, "constraintViolationExceptionExceptionHandler"), request.getRequestURL(), e);
        return doGenerateResult(e, getConstraintViolationExceptionMsg(e));
    }

    /**
     * 请求参数缺失异常
     *
     * @author ws
     * @date 2024/1/29 13:33
     * @param e
     */
    @ExceptionHandler(MissingServletRequestParameterException.class)
    public Object missingServletRequestParameterExceptionHandler(HttpServletRequest request, MissingServletRequestParameterException e) {
        log.error(String.format(LOG_TEMPLATE, "missingServletRequestParameterExceptionHandler"), request.getRequestURL(), e);
        return doGenerateResult(e, String.format(REQUEST_PARAMS_MISS_TEMPLATE, e.getParameterName()));
    }

    /**
     * 重复的请求拦截异常
     *
     * @author ws
     * @date 2024/1/30 16:51
     * @param request
     * @param e
     */
    @ExceptionHandler(WebConfigException.class)
    public Object requestLimitingInterceptorExceptionHandler(HttpServletRequest request, WebConfigException e) {
        log.error(String.format(LOG_TEMPLATE, "requestLimitingInterceptorExceptionHandler"), request.getRequestURL(), e);
        return doGenerateResult(e, e.getMessage());
    }

    /**
     * 兜底的异常处理
     *
     * @author ws
     * @date 2024/1/29 13:35
     * @param e
     */
    @ExceptionHandler(Exception.class)
    public Object exceptionHandler(HttpServletRequest request, Exception e) {
        log.error(String.format(LOG_TEMPLATE, "exceptionHandler"), request.getRequestURL(), e);
        return doGenerateResult(e, e.getMessage());
    }

    /**
     * 处理ConstraintViolationException的参数
     *
     * @author ws
     * @date 2024/3/7 16:34
     */
    private String getConstraintViolationExceptionMsg(ConstraintViolationException e) {
        Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
        if (CollectionUtils.isEmpty(violations)) {
            return e.getMessage();
        }

        return violations.stream().filter(Objects::nonNull)
                .map(cv -> String.format(REQUEST_PARAMS_EX_TEMPLATE, cv.getPropertyPath(), cv.getMessage()))
                .collect(Collectors.joining("\n"));
    }

    /**
     * 处理公用的参数校验异常的信息
     *
     * @author ws
     * @date 2024/1/31 10:45
     */
    private String getErrorMsgDefault(List<FieldError> fieldErrors, String detaultMsg) {
        String errorMsg = null;
        if (!CollectionUtils.isEmpty(fieldErrors)) {
            List<String> msgs = new ArrayList<>(fieldErrors.size());
            fieldErrors.forEach(error -> msgs.add(showErrorMsg(error)));
            errorMsg = String.join("\n", msgs);
        }
        return Optional.ofNullable(errorMsg).orElse(detaultMsg);
    }

    private String showErrorMsg(FieldError fieldError) {
        return String.format(REQUEST_PARAMS_EX_TEMPLATE, fieldError.getField(), fieldError.getDefaultMessage());
    }

    /**
     * 处理通用结果
     *
     * @author ws
     * @date 2024/1/31 10:43
     */
    private Object doGenerateResult(Exception e, String errorMsg) {
        disposeExceptionTrace(errorMsg);
        if (globalExceptionResponseResolver != null) {
            globalExceptionResponseResolver.pushExceptionNotice(e, errorMsg);
            return globalExceptionResponseResolver.resolveExceptionResponse(e, errorMsg);
        }
        return JsonResult.ofFail(errorMsg);
    }
}

服务端的处理

理论上服务端已经不需要处理异常了。但是如果需要校验方法内部的异常,只需要抛出异常即可。

@RequestMapping("/testEx")
public String testEx() {
    Assert.notNull(obj, "obj 不能为空");
    return "success";
}

这里如果obj 如果为空,就会抛异常,异常会被统一处理。

3.3 统一Web的日期处理

Form表单的如果传递日期格式,需要控制层处理。如果日期的格式都不相同,处理起来就会麻烦。我们可以使用统一处理日期格式,兼容所有的场景。

@ControllerAdvice
public class GlobalWebHandler {

    /**
     * 处理web的data数据,这里主要处理form表单的日期
     *
     * @author ws
     * @date 2024/1/29 14:13
     * @param binder
     */
    @InitBinder
    public void globalWebDataBinder(WebDataBinder binder){
        DateFormatter dateFormatter = new DateFormatter(DateFormatConstant.STANDARD_DATE_TIME);
        dateFormatter.setFallbackPatterns(DateFormatConstant.FORMAT_PATTERNS);
        binder.addCustomFormatter(dateFormatter, Date.class);

        //兼容jdk8 LocalDateTime
        binder.addCustomFormatter(new LocalDateTimeFormatter(), LocalDateTime.class);
    }
}

该方法兼容常用的日志格式以及LocalDateTime, 只要传递对应的字符串,就可以正常解析。支持的格式:

04 开源项目

控制层的优雅写法,技术文章中多次被各个大佬讲过,但是都只是教你如何处理,却没有现成的工具封装。为了能够更好的使用优雅的写法,我自己从总结了常用的类型、以及以及处理方案并开源,欢迎大家使用。

只要引入对应的依赖,就可体验这种写法,不需要繁琐的配置,开箱即用。

GitHub地址:github.com/simonking-w…

Gitee地址: gitee.com/simonkingws…

该项目不仅仅处理了通用的参数,还增加常用的拦截器、重复提交、链路追踪、在线运维等方法: