草庐IT

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

凡夫贩夫 2023-07-04 原文

前言

优雅?看到这个词,我第一反应是什么是优雅?怎么写才算优雅?一千个读者有一千个哈姆雷特,每个人的经验、阅历不同,也许理解就不同。我对优雅的理解很简单,就是简洁有效、容易理解,别那么多套路。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||body instanceof String) {
            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层的代码清爽很多了,而且效率还高了,终于结束无效的加班了。

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

Springboot扩展点系列实现方式、工作原理集合:
Springboot扩展点之ApplicationContextInitializer
Springboot扩展点之BeanDefinitionRegistryPostProcessor
Springboot扩展点之BeanFactoryPostProcessor
Springboot扩展点之BeanPostProcessor
Springboot扩展点之InstantiationAwareBeanPostProcessor
Springboot扩展点之SmartInstantiationAwareBeanPostProcessor
Springboot扩展点之ApplicationContextAwareProcessor
Springboot扩展点之@PostConstruct
Springboot扩展点之InitializingBean
Springboot扩展点之DisposableBean
Springboot扩展点之SmartInitializingSingleton
Springboot核心功能工作原理:
Springboot实现调度任务的工作原理
Springboot事件监听机制的工作原理

有关Springboot中如何优雅的写好Controller层代码的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  3. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  4. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

    给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

  5. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  6. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  7. ruby - 如何在 buildr 项目中使用 Ruby 代码? - 2

    如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby​​

  8. ruby - 如何指定 Rack 处理程序 - 2

    Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

  9. ruby-on-rails - Rails 源代码 : initialize hash in a weird way? - 2

    在rails源中:https://github.com/rails/rails/blob/master/activesupport/lib/active_support/lazy_load_hooks.rb可以看到以下内容@load_hooks=Hash.new{|h,k|h[k]=[]}在IRB中,它只是初始化一个空哈希。和做有什么区别@load_hooks=Hash.new 最佳答案 查看rubydocumentationforHashnew→new_hashclicktotogglesourcenew(obj)→new_has

  10. ruby-on-rails - 渲染另一个 Controller 的 View - 2

    我想要做的是有2个不同的Controller,client和test_client。客户端Controller已经构建,我想创建一个test_clientController,我可以使用它来玩弄客户端的UI并根据需要进行调整。我主要是想绕过我在客户端中内置的验证及其对加载数据的管理Controller的依赖。所以我希望test_clientController加载示例数据集,然后呈现客户端Controller的索引View,以便我可以调整客户端UI。就是这样。我在test_clients索引方法中试过这个:classTestClientdefindexrender:template=>

随机推荐