Springboot中如何优雅的写好controller层代码

news2024/11/16 9:57:00

前言

优雅?看到这个词,我第一反应是什么是优雅?怎么写才算优雅?一千个读者有一千个哈姆雷特,每个人的经验、阅历不同,也许理解就不同。我对优雅的理解很简单,就是简洁有效、容易理解,别那么多套路。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包;

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

@Data
public class RemindTaskBean implements Serializable {
    private static final long serialVersionUID = 777197918651078049L;
    @NotNull(message = "调度任务名称不能为空")
    private String taskName;
    @Pattern(regexp = "\\d/\\d+ \\*{1} \\*{1} \\*{1} \\*{1} \\?{1}",message = "表达式格式错误,请更正")
    private String cron;
    @NotNull(message = "任务状态不能为空")
    @Range(min = 0,max = 1,message = "任务状态错误,请更正")
    private Integer taskStatus;
}
  1. 在controller层入口参数处,使用@Valid或@Validated注解对@RequstBody类型的参数进行校验;

@RequestMapping("/add")
public void add(@Valid @RequestBody RemindTaskBean remindTaskBean){
    remindTaskService.add(remindTaskBean);
}

关于参数的统一校验其实不止这点,还有一些更高级的用法,如嵌套校验、分组校验、集合校验、自定义校验,在之前的文章里已经总结的很详细,如有需要可移步这里:优雅的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层使用统一的参数校验后,如果入参数与约束注解相违背,框架就会自动抛出异常处理,再使用异常信息的统一处理机制来捕获这些异常,把异常提示信息进行包装返回给前端友好提示用户。

@RestControllerAdvice
public class CommonExceptionHandler {
    //用于捕获@RequestBody类型参数触发校验规则抛出的异常
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public CommRes handleValidException(MethodArgumentNotValidException e) {
        StringBuilder sb = new StringBuilder();
        List<ObjectError> 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<ObjectError> 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是自定义的异常):

正例:

@Override
public 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,这个类是把返回结果统一格式的包装类

@Data
public 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接口的实现类可以帮你完成所有返回值的统一格式包装,看下面的正例

正例:

@RestControllerAdvice
public 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<RemindTaskBean> list(){
    List<RemindTaskBean> remindTasks = dynamicScheduleTask.taskList();
    return remindTasks;
}

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

正常情况下的返回结果:

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

异常情况下的返回结果:

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

小结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/150848.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

递归、迭代、单向快排的实现和两种优化方法

目录 快速排序 实现代码​​​​​​​ 时间复杂度 快排的优化 随机选择策略 三位取中法 非递归的快排 单向快排 快速排序 快速排序算法是基于分治策略的一个排序算法&#xff0c;其基本思想是对于输入的子数组进行分解、递归求解&#xff0c;最后合并。 分解&#xff…

Fiddler - 夜神模拟器证书安装App抓包

Fiddler- 夜神模拟器证书安装App抓包 文章目录Fiddler- 夜神模拟器证书安装App抓包前言一、软件安装1.Openssl安装1.1下载安装1.2配置环境变量1.3查看openssl版本&#xff0c;输入命令&#xff1a;openssl version2.夜神模拟器安装1.1 下载安装1.2工具准备&#xff0c;MT管理器…

React相关扩展一(setState、lazyLoad、Hooks相关)(九)

系列文章目录 第一章&#xff1a;React基础知识&#xff08;React基本使用、JSX语法、React模块化与组件化&#xff09;&#xff08;一&#xff09; 第二章&#xff1a;React基础知识&#xff08;组件实例三大核心属性state、props、refs&#xff09;&#xff08;二&#xff0…

基于轻量级CNN的WHDLD多标签遥感分类识别系统

WHDLD数据成像波段包括R、G、B波段&#xff0c;数据覆盖包括6类地貌&#xff1a;裸地、建筑物、人行道、道路、植被、水域。数据集中包含4940张遥感影像及对应地物分类标记样本&#xff0c;影像大小为256x256像素&#xff0c;影像以jpg格式存储&#xff0c;标签数据格式为单通道…

高级前端常考手写面试题合集

解析 URL Params 为对象 let url http://www.domain.com/?useranonymous&id123&id456&city%E5%8C%97%E4%BA%AC&enabled; parseParam(url) /* 结果 { user: anonymous,id: [ 123, 456 ], // 重复出现的 key 要组装成数组&#xff0c;能被转成数字的就转成数字…

React(coderwhy)- 09(项目实战 - 1)

创建React项目 ◼ 创建项目的方式&#xff1a;create-react-app ◼ 项目配置:  配置项目的icon  配置项目的标题  配置jsconfig.json 新建jsconfig.json文件&#xff0c;在文件中粘贴以下内容{"compilerOptions": {"target": "es5","…

【数据结构趣味多】循环队列

目录 函数介绍及模拟实现 Front()函数 Rear()函数 enQueue()函数 deQueue()函数 isEmpty()函数 isFull()函数 循环队列模拟题 定义&#xff1a;把队列的头尾相连接的的顺序存储结构称为循环队列&#xff1b;循环队列的是由顺序表实现的。 为什么要使用循环队列&#…

Android MVVM之SavedStateHandle数据保存之详解与使用。

一、介绍 SavedStateHandle从名字可以看出&#xff0c;是保存状态的。这个类常和MVVM中的ViewModel搭配使用&#xff0c;对页面生命周期的数据状态的缓存与恢复做一个容器。这个容易相对onSaveInstanceState(Bundle)要更强一点&#xff0c;保存的数据类型也比较丰富&#xff0c…

算法刷题打卡第60天:回文链表

回文链表 难度&#xff1a;简单 给定一个链表的 头节点 head &#xff0c;请判断其是否为回文链表。 如果一个链表是回文&#xff0c;那么链表节点序列从前往后看和从后往前看是相同的。 示例 1&#xff1a; 输入: head [1,2,3,3,2,1] 输出: true示例 2&#xff1a; 输入:…

文本摘要,基于Pytorch和Hugging Face Transformers构建示例,有源码

​ 文本摘要的常见问题和解决方法概述&#xff0c;以及使用Hugging Face Transformers库构建基于新浪微博数据集的文本摘要示例。 作 者丨程旭源 学习笔记 1 前言简介 文本摘要旨在将文本或文本集合转换为包含关键信息的简短文本。主流方法有两种类型&#xff0c;抽取式和生…

Nodejs模块的封装(数据库Mysql)

文章目录项目结构本次演示需要使用的第三方包为1.app.js相关配置2.router下的user.js相关配置3.db/index.js文件相关操作4.router_handler下的user.js相关操作项目结构 后面的项目相关文件的创建步骤按照我写的博客从上往下一步一步来 本次演示需要使用的第三方包为 "cor…

【操作系统实验/Golang】实验4:虚拟内存页面置换算法

目录 1 实验问题描述 2 测试数据 3 流程图 4 实验结果 4 实验代码 1 实验问题描述 设计程序模拟先进先出FIFO&#xff0c;最佳置换OPT和最近最久未使用LRU页面置换算法的工作过程。 假设内存中分配给每个进程的最小物理块数为m&#xff0c;在进程运行过程中要访问的页面个…

【Leetcode面试常见题目题解】1. 两数相加

题目描述 本文是leetcode第2题的题解&#xff0c;题目描述摘自leetcode。如下 给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c;并且每个节点只能存储 一位 数字。 请你将两个数相加&#xff0c;并以相同形式返回一个…

海外服务器提供商选择中存在哪些风险?

开展海外业务时&#xff0c;毫无疑问&#xff0c;选择一个高质量的海外服务器提供商可以省去不少麻烦。但是&#xff0c;同时有一些海外服务商需要避开。毕竟一个服务器不靠谱&#xff0c;这跟提供商有很大的原因。下面主要是关于低于标准的海外服务器提供商的一些潜在风险。 1…

ES6中字符串和数组新增的方法

ES6中字符串和数组新增的方法一、字符串中新增的方法1、模板字符串 (表达式、函数的调用、变量)2、repeat(次数)函数 : 将目标字符串重复N次&#xff0c;返回一个新的字符串&#xff0c;不影响目标字符串3、includes()函数 :判断字符串中是否含有指定的子字符串&#xff0c;返回…

mysql 8 新旧密码可以同时生效

在MySQL8.0以前版本&#xff0c;给MySQL更改密码&#xff0c;明确写到开发规范中&#xff0c;拒绝更在线更改更密码&#xff0c;因为在8.0以前操作非常麻烦且不太完美。 MySQL 8.0之前的处理方法&#xff1a; 1. 创建一个同样权限的帐号通过 show grants for ‘user_name’1…

通用vue编辑按钮和新建按钮事件逻辑

一、编辑按钮对话框 1.首先先创建一个文件夹page-model&#xff0c;在里面使用elemengt-plus提供的对话框组件el-dialog。 2.在page-model里面去使用之前封装好的form表单&#xff0c;就是之前封装好的搜索组件的hy-form 3.在form组件里面加一个插槽&#xff0c;对应 page-m…

微信小程序:会议OA项目-首页

目录 一、flex布局 Flex布局简介 什么是flex布局&#xff1f; flex属性 flex的属性 二、轮播图组件及mockjs的使用 三、会议OA小程序首页布局 一、flex布局 Flex布局简介 布局的传统解决方案&#xff0c;基于盒状模型&#xff0c;依赖 display属性 position属性 float…

CompletableFuture详解

CompletableFuture详解 概要 RunnableThread虽然提供了多线程的能力但是没有返回值。CallableThread的方法提供多线程和返回值的能力但是在获取返回值的时候会阻塞主线程。 上述的情况只适合不关心返回值&#xff0c;只要提交的Task执行了就可以。另外的就是能够容忍等待。 C…

Java并发容器

一、并发容器总结1、大部分在 java.util.concurrent 包中。ConcurrentHashMap: 线程安全的HashMapCopyOnWriteArrayList: 线程安全的List&#xff0c;在读多写少的场合性能非常好&#xff0c;远远好于Vector.ConcurrentLinkedQueue: 高效的并发队列&#xff0c;使用链表实现。可…