在 Spring MVC 中处理域对象

本文探讨了在 Spring MVC 框架中处理域对象的一些常见误解和最佳实践。内容包括:无需为生成的 ID 字段提供 Setter,域对象不一定需要默认构造函数,以及如何处理具有子实体的域实体。文章强调了保持领域模型纯净的重要性,避免让表示层关注点影响域逻辑。
摘要由CSDN通过智能技术生成

我最近对一个代码库如何在其所有域实体中具有公共默认构造函数(即零参数构造函数)以及所有字段具有 getter 和 setter感到惊讶。随着我深入挖掘,我发现域实体之所以如此,很大程度上是因为团队认为它是 web/MVC 框架所需要的。我认为这将是一个澄清一些误解的好机会。

具体来说,我们将研究以下案例:

  1. 生成的 ID 字段没有 setter(即生成的 ID 字段有 getter 但没有 setter)
  2. 没有默认构造函数(例如,没有公共零参数构造函数)
  3. 具有子实体的域实体(例如,子实体不作为可修改列表公开)

绑定 Web 请求参数

首先,一些细节和一些背景。让我们基于一个特定的 web/MVC 框架——Spring MVC。使用 Spring MVC 时,其数据绑定按名称绑定请求参数。让我们举个例子。

<font style="vertical-align: inherit;"><font style="vertical-align: inherit;">@控制器</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
@RequestMapping("/accounts")</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
... 班级 ... {</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    ...</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    @PostMapping</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    public ... save(@ModelAttribute Account account, ...) {...}</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    ...</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
}</font></font>

给定上面映射到“/accounts”的控制器,Account实例从哪里来?

根据文档,Spring MVC 将使用以下选项获取实例:

  • 如果已经通过模型添加Model(例如通过同一控制器中的@ModelAttribute方法),则从模型中添加。
  • 从 HTTP 会话通过@SessionAttributes.
  • 从通过Converter.
  • 通过调用默认构造函数。
  • (仅适用于 Kotlin)通过调用具有与 Servlet 请求参数匹配的参数的“主构造函数”;参数名称通过 JavaBeans@ConstructorProperties或字节码中运行时保留的参数名称确定。

假设会话中没有Account添加对象,并且没有方法,Spring MVC 最终将使用其默认构造函数实例化一个对象,并通过 name绑定 Web 请求参数。例如,请求包含“id”和“name”参数。Spring MVC 将尝试通过分别调用“setId”和“setName”方法将它们绑定到“id”和“name”bean 属性。这遵循 JavaBean 约定。@ModelAttribute

生成的 ID 字段没有 Setter 方法

让我们从简单的事情开始。假设我们有一个Account域实体。它有一个由持久化存储生成的 ID 字段,并且只提供了一个 getter 方法(但没有 setter 方法)。

<font style="vertical-align: inherit;"><font style="vertical-align: inherit;">@实体</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
...类帐户{</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    @Id @GeneratedValue(...) private Long id;</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    ...</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    公共帐户() { ... }</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    public Long getId() { return id; </font><font style="vertical-align: inherit;">}</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    // 但没有 setId() 方法</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
}</font></font>

那么,我们如何让 Spring MVC 将请求参数绑定到Account域实体呢?我们是否被迫为生成的只读字段使用公共 setter 方法?

在我们的 HTML 表单中,我们不会将“id”作为请求参数。我们将把它作为一个路径变量来代替。

我们使用一种@ModelAttribute方法。它在请求处理方法之前被调用。它支持与常规请求处理方法几乎相同的参数。在我们的例子中,我们使用它来检索Account具有给定唯一标识符的域实体,并将其用于进一步绑定。我们的控制器看起来像这样。

<font style="vertical-align: inherit;"><font style="vertical-align: inherit;">@控制器</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
@RequestMapping("/accounts")</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
... 班级 ... {</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    ...</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    @ModelAttribute</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    公众号populateModel(</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
            HttpMethod httpMethod,</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
            @PathVariable(required=false) 长 id) {</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        如果(id!= null){</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
            返回 accountRepository.findById(id).orElseThrow(...);</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        }</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        如果(httpMethod == HttpMethod.POST){</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
            返回新帐户();</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        }</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        返回空值;</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    }</font></font>
<font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    @PutMapping("/{id}")</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    公共...更新(...,</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
            @ModelAttribute @Valid Account account, ...) {</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        ...</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        accountRepository.save(帐户);</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        返回 ...;</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    }</font></font>
<font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    @PostMapping</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    public ... save(@ModelAttribute @Valid Account account, ...) {</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        ...</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        accountRepository.save(帐户);</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        返回 ...;</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    }</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    ...</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
}</font></font>

更新现有帐户时,请求将是PUT“/accounts/{id}” URI。在这种情况下,我们的控制器需要检索具有给定唯一标识符的域实体,并将相同的域对象提供给 Spring MVC 以进行进一步绑定(如果有)。“id”字段不需要设置方法。

添加或保存新帐户时,请求将是POST“/accounts”。在这种情况下,我们的控制器需要创建一个带有一些请求参数的域实体,并将相同的域对象提供给 Spring MVC 以进一步绑定(如果有)。对于新的域实体,留下“id”字段null。底层持久性基础设施将在存储时产生价值。不过,“id”字段将不需要 setter 方法。

在这两种情况下,该@ModelAttribute方法在映射的请求处理方法之前populateModel被调用。因此,我们需要使用参数 in来确定它被用于哪种情况。populateModel

域对象中没有默认构造函数

假设我们的Account域实体不提供默认构造函数(即没有零参数构造函数)。

<font style="vertical-align: inherit;"><font style="vertical-align: inherit;">...类帐户{</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    公共帐户(字符串名称){...}</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    ...</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    // 没有公共默认构造函数</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    //(即没有公共零参数构造函数)</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
}</font></font>

那么,我们如何让 Spring MVC 将请求参数绑定到Account域实体呢?它不提供默认构造函数。

我们可以使用一种@ModelAttribute方法。在这种情况下,我们希望创建一个Account带有请求参数的域实体,并将其用于进一步绑定。我们的控制器看起来像这样。

<font style="vertical-align: inherit;"><font style="vertical-align: inherit;">@控制器</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
@RequestMapping("/accounts")</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
... 班级 ... {</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    ...</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    @ModelAttribute</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    公众号populateModel(</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
            HttpMethod httpMethod,</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
            @PathVariable(required=false) 长 ID,</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
            @RequestParam(required=false) 字符串名称) {</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        如果(id!= null){</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
            返回 accountRepository.findById(id).orElseThrow(...);</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        }</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        如果(httpMethod == HttpMethod.POST){</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
            返回新帐户(名称);</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        }</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        返回空值;</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    }</font></font>
<font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    @PutMapping("/{id}")</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    公共...更新(...,</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
            @ModelAttribute @Valid Account account, ...) {</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        ...</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        accountRepository.save(帐户);</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        返回 ...;</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    }</font></font>
<font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    @PostMapping</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    public ... save(@ModelAttribute @Valid Account account, ...) {</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        ...</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        accountRepository.save(帐户);</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        返回 ...;</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    }</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    ...</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
}</font></font>

具有子实体的域实体

现在,让我们看一个具有子实体的域实体。像这样的东西。

<font style="vertical-align: inherit;"><font style="vertical-align: inherit;">...类订单{</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    私有 Map<..., OrderItem> 项目;</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    公共秩序() {...}</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    公共无效添加项(整数数量,...){...}</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    ...</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    公共集合<CartItem> getItems() {</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        返回 Collections.unmodifiableCollection(items.values());</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    }</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
}</font></font>
<font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
... 类 OrderItem {</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    私有 int 数量;</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    // 没有公共默认构造函数</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    ...</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
}</font></font>

请注意,订单中的项目不会显示为可修改的列表。Spring MVC 支持索引属性并将它们绑定到数组、列表或其他自然排序的集合。但是,在这种情况下,该getItems方法返回一个不可修改的集合。这意味着当对象尝试向其中添加/删除项目时将引发异常。那么,我们如何让 Spring MVC 将请求参数绑定到Order域实体呢?我们是否被迫将订单项公开为可变列表?

并不真地。我们必须避免使用表示层关注点(如 Spring MVC)来稀释域模型。相反,我们使表示层成为域模型的客户端。为了处理这种情况,我们创建了另一种符合 Spring MVC 的类型,并使我们的域实体与表示层无关。

<font style="vertical-align: inherit;"><font style="vertical-align: inherit;">... 类 OrderForm {</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    public static OrderForm fromDomainEntity(Order order) {...}</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    ...</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    // 公共默认构造函数</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    //(即公共零参数构造函数)</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    私有列表<OrderFormItem> 项;</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    public List<OrderFormItem> getItems() { 返回项目;</font><font style="vertical-align: inherit;">}</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    public void setItems(List<OrderFormItem> items) { this.items = items; </font><font style="vertical-align: inherit;">}</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    公共秩序 toDomainEntity() {...}</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
}</font></font>
<font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
... 类 OrderFormItem {</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    ...</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    私有 int 数量;</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    // 公共默认构造函数</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    //(即公共零参数构造函数)</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    // 公共 getter 和 setter</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
}</font></font>

请注意,创建一个了解域实体的表示层类型是完全可以的。但是让域实体知道表示层对象并不完全正确。更具体地说,表示层OrderForm知道Order域实体。但Order不知道表示层OrderForm。

这是我们的控制器的外观。

<font style="vertical-align: inherit;"><font style="vertical-align: inherit;">@控制器</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
@RequestMapping("/orders")</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
... 班级 ... {</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    ...</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    @ModelAttribute</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    公共 OrderForm 填充模型(</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
            HttpMethod httpMethod,</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
            @PathVariable(required=false) 长 ID,</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
            @RequestParam(required=false) 字符串名称) {</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        如果(id!= null){</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
            返回 OrderForm.fromDomainEntity(</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
                orderRepository.findById(id).orElseThrow(...));</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        }</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        如果(httpMethod == HttpMethod.POST){</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
            返回新的 OrderForm(); </font><font style="vertical-align: inherit;">// 新命令()</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        }</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        返回空值;</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    }</font></font>
<font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    @PutMapping("/{id}")</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    公共...更新(...,</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
            @ModelAttribute @Valid OrderForm orderForm, ...) {</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        ...</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        orderRepository.save(orderForm.toDomainEntity());</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        返回 ...;</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    }</font></font>
<font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    @PostMapping</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    public ... save(@ModelAttribute @Valid OrderForm orderForm, ...) {</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        ...</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        orderRepository.save(orderForm.toDomainEntity());</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
        返回 ...;</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    }</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
    ...</font></font><font style="vertical-align: inherit;"><font style="vertical-align: inherit;">
}</font></font>

结束的想法

正如我在之前的文章中提到的,让你的域对象看起来像一个带有公共默认零参数构造函数、getter 和 setter 的 JavaBean 是可以的。但是,如果域逻辑开始变得复杂,并且要求某些域对象失去其 JavaBean 特性(例如,不再有公共零参数构造函数,不再有 setter),请不要担心。定义新的 JavaBean 类型以满足与表示相关的问题。不要稀释领域逻辑。

目前为止就这样了。我希望这有帮助。

最后给大家分享Spring系列的学习笔记和面试题,包含spring面试题、spring cloud面试题、spring boot面试题、spring教程笔记、spring boot教程笔记、最新阿里巴巴开发手册(63页PDF总结)、2022年Java面试手册。一共整理了1184页PDF文档。私信博主(777)领取,祝大家更上一层楼!!!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值