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

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

  • 15
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值