【优雅设计】Java Web开发: 构建高效优雅的RESTful API (Controller代码实战篇)

news2024/9/8 13:07:52

文章目录

  • 让Controller代码更优雅
    • 接收 HTTP(s)请求
    • 解析请求参数
      • 原始 HTTP请求和响应对象
      • 路径变量 (`@PathVariable`)
      • 请求参数 (`@RequestParam`)
      • 请求体 (`@RequestBody`)
      • 模型属性 (`@ModelAttribute`)
      • 会话属性 (`@SessionAttribute`)
      • 请求头 (`@RequestHeader`)
      • Cookie 值 (`@CookieValue`)
      • 自定义参数解析器
    • 验证请求参数
    • 调用业务方法
    • 组织返回数据
    • 统一异常处理
    • 建议和总结
    • 结论

让Controller代码更优雅

作为一名 Java 程序员,对 Controller肯定不陌生,它是与外部客户端通信的入口,比如常见的 REST
操作(GET、PUT、POST、DELETE等),那么,Controller里面应该如何编写才算优雅呢?
其实,一个优雅的 Controller,里面的代码主要包含下面 6个部分:

  • 接收 HTTP(s)请求
  • 解析请求参数
  • 验证请求参数
  • 调用业务方法
  • 组织返回数据
  • 统一异常处理

下面一一讲解这 6个部分:

接收 HTTP(s)请求

接收 HTTP(s)请求是 Controller的入口,这里以查询用户信息为例进行说明,如下代码:


@RestController
public class UserController {
    @GetMapping("/user/{userId}")
    public void getUserById(@PathVariable String userId) {
        // 业务逻辑
    }
}

在上面的示例中,我们使用 URL/user/{id}接收用户发出的 GET请求,然后通过getUserById方法进行真实的业务处理。通过上面的代码,一个请求就被Controller层成功接收了。
说明:@RestController=@Controller+ResponseBody

解析请求参数

接收到请求后,一般需要对请求参数进行解析,如下示例代码:

@RestController
public class UserController {
    @PostMapping("/user/register")
    public void getGradeById(@RequestBody User user) {
        // 代码逻辑
    }
}

public class User {
    private String nickname;
    private Integer age;
    // getters and setters and constructors
}

上述示例代码将请求的 body映射到 User 对象上,因此,请求的 body体应该是:

{
  "nickname": "huahua",
  "age": "18"
}

在 SpringMVC 中,常见的参数类型及其用途如下:

原始 HTTP请求和响应对象

直接接收原始的 HTTP请求和响应对象,HttpServletRequestHttpServletResponse

@RequestMapping("/test")
public void example(HttpServletRequest request, HttpServletResponse response) {
    // 处理请求和响应
}

路径变量 (@PathVariable)

用于获取 URL 路径中的动态部分

@RequestMapping("/user/{id}")
public String getUser(@PathVariable("id") String userId) {
    // 使用 userId 进行处理
    return "userDetail";
}

请求参数 (@RequestParam)

用于获取 URL 查询参数或表单数据

@RequestMapping("/search")
public String search(@RequestParam("query") String query) {
    // 使用 query 进行搜索
    return "searchResults";
}

请求体 (@RequestBody)

用于接收请求体中的数据,常用于处理 JSON 或 XML 格式的数据

@RequestMapping(value = "/create", method = RequestMethod.POST)
public String create(@RequestBody User user) {
    // 处理 user 对象
    return "user";
}

模型属性 (@ModelAttribute)

用于绑定表单数据到模型对象

@RequestMapping("/register")
public String register(@ModelAttribute User user) {
    // 处理 user 对象
    return "user";
}

会话属性 (@SessionAttribute)

用于访问会话中的属性

@RequestMapping("/profile")
public String profile(@SessionAttribute("user") User user) {
    // 处理会话中的 user 对象
    return "profile";
}

请求头 (@RequestHeader)

用于访问 HTTP 请求头信息

@RequestMapping("/headers")
public String headers(@RequestHeader("User-Agent") String userAgent) {
    // 使用 userAgent 进行处理
    return "headerInfo";
}

Cookie 值 (@CookieValue)

用于访问 Cookie 的值。

@RequestMapping("/cookies")
public String cookies(@CookieValue("sessionId") String sessionId) {
    // 使用 sessionId 进行处理
    return sessionId;
}

自定义参数解析器

可以通过实现 HandlerMethodArgumentResolver接口来自定义参数解析逻辑。

@RequestMapping("/custom")
public String custom(CustomObject customObject) {
// 使用自定义对象进行处理
    return "";
}

验证请求参数

请求参数的验证需要在 Controller 层完成,如下代码,对 nickname 进行判空处理,参数验证一般有 2种方式:

  1. 原始方式,这种方式比较灵活,如果需要对参数进行一些逻辑计算后再校验;
  2. 借助三方工具,比如 Spring validationjavax validation 等,这种方式灵活度会低一些,但是更优雅;
// 原始方式校验参数
@RestController
public class UserController {
    @PostMapping("/user/register")
    public void getGradeById(@RequestBody User user) {
        // 代码逻辑
        if (StringUtils.isBlank(user.getNickname)) {
            throw new Exception("Nickname is required.");
        }
    }
}

或者使用 Spring validation 验证机制,Controller 需要增加 @Validated 注解,User 对象中增加 @NotBlank 注解。

// 借助Spring validation方式校验参数
@RestController
public class UserController {
    @PostMapping("/user/register")
    public void getGradeById(@Validated @RequestBody User user) {
        // 代码逻辑
    }
}

public class User {
    @NotBlank(message = "Nickname is required.")
    private String nickname;
    private Integer age;
    // getters and setters and constructors
}

调用业务方法

如下代码,调用 UserService.register()进行注册业务处理:


@RestController
public class UserController {
    private final UserService userService;
    public UserController(UserService userService) {
        this.userService = userService;
    }

    @PostMapping("/user/register")
    public void getGradeById(@Validated @RequestBody User user) {
        // 调用注册的业务方法
        userService.register(user);
    }
}

public class User {
    @NotBlank(message = "Nickname is required.")
    private String nickname;
    private Integer age;
    // getters and setters and constructors
}

关于调用业务方法,这里的业务方法是写一个大而全的方法?还是需要按业务归类?

遵守一个原则:有强关联性的逻辑放在一个 service 方法内,没有强关联性的单令拎出来。

这里以用户注册之后需要新人发券为例进行说明:

大而全的方法

@PostMapping("/user/register")
public void getGradeById(@Validated @RequestBody User user) {
    // 调用注册的业务方法
    userService.doRegister(user);
}

public String doRegister(Uswr user){
    String userId = userService.register(user);
    coupon.sendCoupon(userId);
    // 其他业务逻辑
    return userId;
}

业务归类

@PostMapping("/user/register")
public void getGradeById(@Validated @RequestBody User user) {
    // 调用注册的业务方法
    userService.register(user);
    coupon.sendCoupon(userId);
}

组织返回数据

如下代码,调用 UserService.register()进行注册业务处理:

@RestController
public class UserController {
    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;

    }

    @PostMapping("/user/register")
    public UserResponse getGradeById(@Validated @RequestBody User user) {
        // 调用注册的业务方法
        String userId = userService.regist(user);

        return new UserResponse(userId, user.getNickname);
    }
}

public class UserResponse {
    private String userId;
    private String nickname;
    // getters and setters and constructors
}

统一异常处理

比如上述过程在 userService.regist(user);出现异常时,可以做一个 try-catch,然后在 Controller 层封装有业务意思的异常信息:


@RestController
public class UserController {
    private final UserService userService;

    @PostMapping("/user/register")
    public UserResponse getGradeById(@Validated @RequestBody User user) {
        // 调用注册的业务方法
        try {
            String userId = userService.regist(user);
        } catch (Exception e) {
            throw new CustomException();
        }
        return new UserResponse(userId, user.getNickname);
    }
}

当然可以。下面是针对“建议和总结”章节的一个重写版本:

建议和总结

虽然将所有业务逻辑都放在 Controller 层的做法可能在某些情况下可行,但通常这不是最佳实践。一个优雅且可维护的应用程序应该遵循良好的设计原则和架构模式。为此,我们可以参考 SOLID 原则来指导我们的设计决策。

SOLID 原则是五个面向对象设计原则的缩写:

  1. 单一职责原则 (SRP): 每个类都应该只有一个改变的理由。这意味着每个类、方法或组件应该只负责一项功能,并且这个功能应该是清晰且独立的。例如,注册逻辑和发放优惠券的逻辑应该被分离到不同的服务方法中。

  2. 开放封闭原则 (OCP): 类的设计应当是可扩展的,但不可修改。即软件实体(类、模块、函数等)应该是可以扩展的,但不应该被修改。为了满足这一原则,我们可以通过接口或抽象类来定义行为,并通过实现这些接口或继承抽象类来扩展功能。

  3. Liskov 替换原则 (LSP): 子类必须能够替换它们的基类。这意味着任何父类出现的地方,子类都可以出现。在设计时,我们应该确保继承关系不会破坏这种替换性。

  4. 接口隔离原则 (ISP): 客户端不应该被迫依赖于它不需要的接口。换句话说,接口应该是细粒度的,并且客户端只需要知道它们实际需要的部分。这有助于减少耦合性和提高灵活性。

  5. 依赖倒置原则 (DIP): 高层次的模块不应该依赖于低层次的模块,二者都应该依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。这通常意味着我们通过依赖注入来管理依赖关系,并使用接口或抽象类来定义高层和低层模块之间的交互。

结论

为了编写优雅的 Controller,我们需要确保它们专注于接收请求、验证输入、调用服务层并返回适当的响应。具体的业务逻辑应该被移至 Service 层或其他适当的组件中。此外,利用 Spring Framework 提供的各种注解来简化参数绑定、验证和异常处理等任务,可以使代码更加简洁和易于维护。

最后,通过遵循 SOLID 原则,我们可以创建出易于理解、扩展和维护的系统。建议多参考优秀的开源项目和框架,学习它们如何组织代码结构和设计模式,这样可以帮助你形成一套自己的最佳实践。

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

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

相关文章

一些Kafka面试题

Kafka是如何保证消息不丢失? 1.生产者发送消息到Broker丢失: 设置异步发送:发送失败则使用回调进行记录或者重发 消息重试:参数配置,可以设置重试次数 2.消息在broker中存储丢失 发送确认机制acks acks0&#xf…

创新突破 | OpenCSG发布StarShip CodeReview v1.0.0 Beta版

1. 代码审查很关键但耗时耗力 在软件开发过程中,代码审查是确保代码质量的关键环节。代码审查有助于维护代码标准和发现潜在错误,但也常常耗费大量时间和精力。审查者不仅需要深入理解代码逻辑,还要在繁复的逻辑中识别Bug,这个过…

如何使用 Odoo 16 主生产调度程序规划生产

为了优化运营并提高生产力,企业需要生产管理软件。在当今竞争激烈的经济环境中,有效的资源管理对企业至关重要。为制造业务设计的软件经常用于控制收入增长和盈利能力。ERP(企业资源规划)系统是专门为制造业创建的,可以…

Java刷题: 丑数判断

题目 丑数 就是只包含质因数 2、3 和 5 的正整数。 给你一个整数 n ,请你判断 n 是否为 丑数 。如果是,返回 true ;否则,返回 false 。 解题思路 我觉得刷题是为了扩宽思考的广度。看到这题的时候,我的大脑是发懵的…

技术成神之路:设计模式(十)备忘录模式

介绍 备忘录模式(Memento Pattern)是一种行为设计模式,它允许在不破坏封装性的前提下捕获和恢复对象的内部状态。通过备忘录模式,可以在程序运行过程中保存和恢复对象的某个状态,从而实现“撤销”等功能。 1.定义 备忘…

11 优化器

目录 1. 随机梯度下降系优化器:SGD 1.1 算法种类 1.2 优缺点 2 SGDM 即为SGD with momentum 动量 2.1 公式 2.2 动量的优缺点 优点 缺点 2.3 使用场景 3 AdaGrad 3.1 公式 3.2 AdaGrad的优缺点 优点 缺点 3.3 使用场景 3.4 Adam 3.4.1 Adam优化器的…

倒计时:60秒后再输入验证码

思路1:每隔 1 秒种减 1 ,直到减到 0 为止。可以写成公用方法。亲测有效。 countDown(){ const TIME_COUNT 60; if (!this.timer) { this.count TIME_COUNT; this.codeShow false; this.timer setInterval(() > { if (this.count > 0 &&…

苹果密码解锁工具已注册专业版_不限制电脑

Aiseesoft iPhone Unlocker:轻松解锁iPhone。功能强大:一键移除4位、6位密码、Touch ID和Face ID。隐私保护:创建密码,安全无忧。数据提醒:解锁时,注意数据和设置将被清除。Apple ID 解锁:快速删…

【Spring Boot教程:从入门到精通】掌握Spring Boot开发技巧与窍门(三)-配置git环境和项目创建

主要介绍了如何创建一个Springboot项目以及运行Springboot项目访问内部的html页面!!! 文章目录 前言 配置git环境 创建项目 ​编辑 在SpringBoot中解决跨域问题 配置Vue 安装Nodejs 安装vue/cli 启动vue自带的图形化项目管理界面 总结 前言 …

Qt基础 | TCP通信 | TCP服务端与客户端程序设计 | QTcpServer | QTcpSocket

文章目录 一、TCP 通信1.TCP 通信概述2.TCP 服务器端程序设计2.1 主窗口定义与构造函数2.2 网络监听与 socket 连接的建立2.3 与 TCP 客户端进行数据通信 3.TCP 客户端程序设计3.1 主窗口定义与构造函数3.2 与服务器端建立 socket 连接3.3 与 TCPServer 的数据收发 4.小结 Qt 网…

[Docker][Docker Image]详细讲解

目录 1.Docker镜像是什么?2.Docker镜像加载原理1.bootfs2.rootfs3.为什么CentOS镜像几个G,而Docker CentOS镜像才几百M?1.CentOS2.Docker CentOS 3.镜像分层1.Union FS2.分层理解3.容器层 vs 镜像层 4.镜像命令1.docker images2.docker image…

大数据信用报告查询会不会留下查询记录?怎么选择查询平台?

最近有不少网友都在咨询一个问题,那就是大数据信用报告查询会不会留下查询记录,会不会对自己的征信产生影响,下面本文就详细为大家介绍一下,希望对你了解大数据信用有帮助。 首先、大数据信用与人行征信是独立的 很多人只知道人行…

innovus:如何only select highlighted

我正在「拾陆楼」和朋友们讨论有趣的话题,你⼀起来吧? 拾陆楼知识星球入口 来自星球提问; 1)ICC2 2)innovus View->Set Preference,勾选Only Select Highlighted

新装centos7虚拟机如何配置网络,NAT配置固定IP

首先声明,我想使用的是NAT连接模式,并且设置完IP之后,使得这个IP固定住,以后不会再变了。 文章目录 1,打开Vmware软件的【编辑】-【虚拟网络编辑器】2,先选择VMnet8(画1处)&#xf…

2025上海国际显示技术及应用创展览会

DIC EXPO2025中国(上海)国际显示技术及应用创展览会 时间:2025年8月7-9日 地点:上海新国际博览中心 主办单位: 中国光学光电子行业协会液晶分会 联合主办: 中国电子材料行业协会 中国电子商会 韩国…

嵌入式人工智能(32-基于树莓派4B的旋转编码器-EnCoder11)

1、旋转编码器 旋转编码器是一种输入设备,通常用于测量和控制旋转运动。它由一个旋转轴和一系列编码器组成。旋转编码器可以根据旋转轴的位置和方向来测量旋转角度,并将其转化为电子信号输出。 旋转编码器通常分为两种类型:绝对值编码器和增…

【微服务】Spring Cloud Gateway

文章目录 强烈推荐引言主要功能关键概念示例配置依赖添加常见的几种整合案例1. 与 Spring Cloud Eureka 整合2. 与 Spring Cloud Config 整合3. 与 Spring Cloud Sleuth 和 Zipkin 整合4. 与 Spring Cloud Security 整合5. 与 Resilience4j 整合6. 与 Redis 整合 结论强烈推荐专…

Matlab编程资源库(19)级数与符号方程求解

一、级数符号求和 求无穷级数的和需要 符号表达式求和函数 symsum ,其调用 格式为: symsum(s,v,n,m) 其中 s 表示一个级数的通项,是一个符号表达式。 v 是求和变 量, v 省略时使用系统的默认变量。 n 和 m 是求和的开始项 和…

如何恢复WPS文档中未保存或删除的文件

由于各种原因,您可能会丢失 WPS 文档,例如意外删除、硬盘格式化、病毒攻击等。您是否遇到过丢失未保存的 WPS 文件的情况?您知道如何恢复 WPS 文档中未保存的文件吗? WPS Office 是一款办公套件,可以作为 Microsoft O…

火山引擎VeDI数据技术分享:两个步骤,为Parquet降本提效

更多技术交流、求职机会,欢迎关注字节跳动数据平台微信公众号,回复【1】进入官方交流群 作者:王恩策、徐庆 火山引擎 LAS 团队 火山引擎数智平台 VeDI 是火山引擎推出的新一代企业数据智能平台,基于字节跳动数据平台多年的“数据…