前言

优雅?看到这个词,我第一反应是什么是优雅?怎么写才算优雅?一千个读者有一千个哈姆雷特,每个人的经验、阅历不同,也许理解就不同。我对优雅的理解很简单,就是简洁有效、容易理解,别那么多套路。java中使用Sping的web项目通常会分为三层,分别是controller、service、dao,这似乎已成为了一个既定规则。很少有人去想为什么要这样分?可不可不以不这样分?java属于面向对象的高级编程语言,其实这种分法并不符合面向对象的理念,而实际这是按照一次B-S请求过程从外到内的调用过程划分的,然后根据面向接口编程的理念,外层调用内层接口,内层接口实际为外层提供服务能力的是内层接口的实现类,接口是标准接口,实现类可以根据实际业务更换,按照这种设计实现了层间解耦,提供了程序维护的便利性和开发效率。因此,虽然这种分法不符合面向对象的理念,但是很优雅(简单有效、容易被大多数人理解)。

java项目分层的含义

  • Controller:俗称控制器,用于处理请求映射,在jsp时代,调用service层业务接口,在controll层包装一个视频图对象,返回给页面;现在通常直接返回数据对象,springboot会自动把返回结果格式化为json返回给前端;

  • Service层:通常是系统的具体业务逻辑,供controller层调用;

  • DAO层:操作数据库,供service层调用;

正如前面说的,这种分法不能算是面对对象,倒是有点面向过程的味道,但是这种分法实践了面向接口编程的理念,使层与层之间解耦,提高了程序的可维护性和开发效率,所以还是优雅的。

其中controller层作为前端与后端实际业务接口的连接者,如何优雅写好这一层的代码至关重要。要想优雅写好这一层代码,可以从以下几个方面着手

入口参数统一校验

正例:

  1. 引入spring-boot-starter-validation包;

    org.springframework.boot    spring-boot-starter-validation    2.3.9.RELEASE
  1. 在用于接收参数的实体类上,使用@NotNull、@Null等结束注解对参数属性进行标记;

@Datapublic class RemindTaskBean implements Serializable {    private static final long serialVersionUID = 777197918651078049L;    @NotNull(message = "调度任务名称不能为空")    private String taskName;    @Pattern(regexp = "\\d/\\d+ \\*{1} \\*{1} \\*{1} \\*{1} \\" />优雅的Springboot参数校验(一)优雅的Springboot参数校验(二)

反例:

真的不想看到为了校验参数,在controller层的方法内写了大量的if else判断,(这样不感觉累吗,加班到天亮也是活该)如下:

@RequestMapping("/add2")public CommRes add2(@Valid @RequestBody RemindTaskBean remindTaskBean){    if (remindTaskBean.getTaskName() == null) {        CommRes.fail("调度任务名不能为空");    }    if (remindTaskBean.getTaskStatus() == null) {        CommRes.fail("调度任务状态不能为空");    }    if (remindTaskBean.getTaskStatus()!=0||remindTaskBean.getTaskStatus()!=1) {        CommRes.fail("调度任务状态错误,请更正");    }    if (remindTaskBean.getCron() != null) {        String reg="\\d/\\d+ \\*{1} \\*{1} \\*{1} \\*{1} \\?{1}";        if (!remindTaskBean.getCron().matches(reg)) {            CommRes.fail("表达式格式错误,请更正");        }    }    remindTaskService.add(remindTaskBean);    return CommRes.success(remindTaskBean);}

异常信息统一处理

在controller层使用统一的参数校验后,如果入参数与约束注解相违背,框架就会自动抛出异常处理,再使用异常信息的统一处理机制来捕获这些异常,把异常提示信息进行包装返回给前端友好提示用户。

@RestControllerAdvicepublic class CommonExceptionHandler {    //用于捕获@RequestBody类型参数触发校验规则抛出的异常    @ExceptionHandler(value = MethodArgumentNotValidException.class)    public CommRes handleValidException(MethodArgumentNotValidException e) {        StringBuilder sb = new StringBuilder();        List allErrors = e.getBindingResult().getAllErrors();        if (!CollectionUtils.isEmpty(allErrors)) {            for (ObjectError error : allErrors) {                sb.append(error.getDefaultMessage()).append(";");            }        }        return CommRes.fail(sb.toString());    }    //用于捕获@RequestParam/@PathVariable参数触发校验规则抛出的异常    @ExceptionHandler(value = ConstraintViolationException.class)    public CommRes handleConstraintViolationException(ConstraintViolationException e) {        StringBuilder sb = new StringBuilder();        Set<ConstraintViolation> conSet = e.getConstraintViolations();        for (ConstraintViolation con : conSet) {            String message = con.getMessage();            sb.append(message).append(";");        }        return CommRes.fail(sb.toString());    }    @ExceptionHandler(value = BindException.class)    public CommRes handleConstraintViolationException(BindException e) {        StringBuilder sb =  new StringBuilder();        List allErrors = e.getAllErrors();        for (ObjectError allError : allErrors) {            String defaultMessage = allError.getDefaultMessage();            sb.append(defaultMessage).append(";");        }        return CommRes.fail(sb.toString());    }    @ExceptionHandler(value = Exception.class)    public CommRes exception(Exception e) {        return CommRes.fail(e.getMessage());    }}

另外关于异常处理,不要动不动就try catch,除非有必要(有人犟劲上来了,我感觉都很有必要呀,所以他的代码里到处是try catch,我也真是醉了),比如:有人用try catch把增加一个调度任务的dao接口调用包上,他加try catch的理由是如果sql写错了呢不就异常了(锤死他的心都有了,你就不会写对喽!!!),有人还担心万一数据库挂了呢(如果数据库真挂了,抛个异常有什么用,难道能把数据库恢复了不成);最搞笑的是,有的人只try catch,然后就没有然后了;总而言之,有的小朋友真是超可爱。

反例:

public CommRes add2(RemindTaskBean remindTask) {    RemindTaskBean taskBean = this.remindTaskDao.queryByTaskName("测试任务");    if (taskBean != null) {      return  CommRes.fail("调度任务已存在,请勿重复注册");    }    try {        this.remindTaskDao.insert(remindTask);    } catch (Exception e) {        e.printStackTrace();    }    return CommRes.success("");}

那什么叫除非有必要呢?比如增加一个调度任务,但是要求相同的名字不能重复注册,这时可以在插入调度信息前查询是否有相同名字的调度任务,如果有,则抛出异常提示,优雅的写法应该是这样的(ServiceException是自定义的异常):

正例:

@Overridepublic void add(RemindTaskBean remindTask) {    RemindTaskBean taskBean  = this.remindTaskDao.queryByTaskName("测试任务");    if (taskBean != null) {        throw new ServiceException("调度任务已存在,请勿重复注册");    }    this.remindTaskDao.insert(remindTask);}
public class ServiceException extends RuntimeException    {    public ServiceException(String message) {        super(message);    }}

返回结果统一格式

细心的小伙伴发现了CommRes.java,这个类是把返回结果统一格式的包装类

@Datapublic class CommRes {    private String code;    private String msg;    private Object data;    public static CommRes success(Object data){        CommRes commRes = new CommRes();        commRes.setCode("200");        commRes.setMsg("操作成功");        commRes.setData(data);        return commRes;    }    public static CommRes fail(String msg){        CommRes commRes = new CommRes();        commRes.setCode("400");        commRes.setMsg(msg);        commRes.setData("");        return commRes;    }}

相信很多人也知道,要封装一个包装类对返回结果统一格式,有的小伙伴是这样用,其实不是,这是一个反例:

@RequestMapping("/add2")public CommRes add2(@Valid @RequestBody RemindTaskBean remindTaskBean){    if (remindTaskBean.getTaskName() == null) {        CommRes.fail("调度任务名不能为空");    }    remindTaskService.add(remindTaskBean);    return CommRes.success(remindTaskBean);}

优雅的用法应该是这样的,不要手动去调用它,而是使用@RestControllerAdvice或@ControllerAdvice标记一个类并实现ResponseBodyAdvicer接口,作为返回结果统一处理类,然后在controller层方法里得到返回结果直接返回就好,被@RestControllerAdvice标记的ResponseBodyAdvicer接口的实现类可以帮你完成所有返回值的统一格式包装,看下面的正例

正例:

@RestControllerAdvicepublic class ResultResponseBoydAdvice implements ResponseBodyAdvice {    @Override    public boolean supports(MethodParameter returnType, Class converterType) {        return true;    }    @Override    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {        if (body instanceof CommRes) {            return body;        }        return CommRes.success(body);    }}
@GetMapping("/list")public List list(){    List remindTasks = dynamicScheduleTask.taskList();    return remindTasks;}

经过这样一处理,返回结果就是这样了

正常情况下的返回结果:

{    "code": "200",    "msg": "操作成功",    "data": [返回数据在这里面]}

异常情况下的返回结果:

{    "code": "400",    "msg": "表达式格式错误,请更正;",    "data": ""}

小结

在controller层,统一进行参数校验、统一处理异常、统一返回结果格式后,是不是感觉controller层的代码清爽很多了,而且效率还高了,终于结束无效的加班了。

示例是所用源代码地址:https://gitcode.net/fox9916/fanfu-web.git的优雅的controller分支