6.3启用数据为中心的服务

6.3启用以数据为中心的服务

正如在第3章中看到的,Spring Data拥有一种特殊的魔力,它根据在代码中定义的接口自动创建存储库的实现。但是Spring Data还有另一个技巧,可以为应用程序定义API

Spring Data RESTSpring Data家族中的另一个成员,它为Spring Data创建的存储库自动创建REST API。只需将Spring Data REST添加到构建中,就可以获得一个API,其中包含所定义的每个存储库接口的操作。

要开始使用Spring Data REST,需要在构建中添加以下依赖项:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-rest</artifactId>
</dependency>

信不信由你,这就是在一个已经将Spring Data用于自动存储库的项目中公开REST API所需要的全部内容。通过在构建中简单地使用Spring Data REST starter,应用程序可以自动配置,从而为Spring Data创建的任何存储库(包括Spring Data JPASpring Data Mongo等)自动创建REST API

Spring Data REST创建的REST端点至少与自己创建的端点一样好(甚至可能更好。因此,在这一点上,可以做一些拆卸工作,并在继续之前删除到目前为止创建的任何@RestController注解的类。

要尝试Spring Data REST提供的端点,可以启动应用程序并开始查看一些url。基于已经为Taco Cloud定义的存储库集,应该能够执行针对Taco、Ingredient、OrderUserGET请求。

例如,可以通过向 /ingredients 接口发出GET请求来获得所有Ingredient的列表。使用curl,可能会得到这样的结果(经过删节,只显示第一个Ingredient

$ curl localhost:8080/ingredients
{
    "_embedded" : {
        "ingredients" : [
            {
                "name" : "Flour Tortilla",
                "type" : "WRAP",
                "_links" : {
                    "self" : {"href" : "http://localhost:8080/ingredients/FLTO"},
                    "ingredient" : {
                        "href" : "http://localhost:8080/ingredients/FLTO"
                    }
                }
            },
            ...
        ]
    },
    "_links" : {
        "self" : {
            "href" : "http://localhost:8080/ingredients"
        },
        "profile" : {
            "href" : "http://localhost:8080/profile/ingredients"
        }
    }
}

哇!通过向构建中添加一个依赖项,不仅获得了Ingredient的端点,而且返回的资源也包含超链接!假装是这个API的客户端,也可以使用curl来跟踪特定入口的自链接:

$ curl localhost:8080/ingredients/FLTO
{
    "name" : "Flour Tortilla",
    "type" : "WRAP",
    "_links" : {
        "self" : {
            "href" : "http://localhost:8080/ingredients/FLTO"
        },
        "ingredient" : {
            "href" : "http://localhost:8080/ingredients/FLTO"
        }
    }
}

为了避免过于分散注意力,在本书中我们不会浪费太多时间来深入研究Spring Data REST创建的每个端点和选项。但是应该知道,它还支持其创建的端点的POSTPUTDELETE方法。没错:可以通过向 /ingredients 接口发送POST请求创建一个新的Ingredient,然后通过向 /indegredient/FLTO 接口发送DELETE请求来从菜单上移除面粉玉米饼。

可能想要做的一件事是为API设置一个基本路径,这样它的端点是不同的,并且不会与编写的任何控制器发生冲突(事实上,如果不删除先前创建的IngredientController,它将干扰Spring Data REST提供的 /ingredients 端点)要调整API的基本路径,请设置spring.data.rest基本路径属性:

spring:
  data:
    rest:
      base-path: /api

这将设置Spring Data REST端点的基本路径为 /api。因此,Ingredient端点现在是 /api/ingredients。现在,通过请求一个tacos列表来使用这个新的基本路径:

$ curl http://localhost:8080/api/tacos
{
    "timestamp": "2018-02-11T16:22:12.381+0000",
    "status": 404,
    "error": "Not Found",
    "message": "No message available",
    "path": "/api/tacos"
}

噢?这并没有达到预期的效果。有一个Ingredient实体和一个IngredintRepository接口,其中Spring Data REST暴露 /api/ingredients 端点。因此,如果有一个Taco实体和一个TacoRepository接口,为什么Spring Data REST不能提供 /api/tacos 端点呢?

6.3.1调整资源路径和关系名称

实际上,Spring Data REST提供了处理tacos的端点。但是,尽管Spring Data REST非常智能,但它在暴露tacos端点方面的表现却稍微逊色一些。

在为Spring Data存储库创建端点时,Spring Data REST尝试使关联多元化的实体类。对于Ingredient实体,端点是 /ingredients。对于OrderUser实体,它是 /orders/users。到目前为止,一切顺利。

但有时,比如 “taco”,它会在一个字母上出错,这样复数形式就不太正确了。事实证明,Spring Data REST将复数形式 “taco” 表示为 “tacoes”,因此,要想对tacos发出请求,你必须请求 /api/tacoes

% curl localhost:8080/api/tacoes
{
    "_embedded" : {
        "tacoes" : [ {
            "name" : "Carnivore",
            "createdAt" : "2018-02-11T17:01:32.999+0000",
            "_links" : {
                "self" : {
                    "href" : "http://localhost:8080/api/tacoes/2"
                },
                "taco" : {
                    "href" : "http://localhost:8080/api/tacoes/2"
                },
                "ingredients" : {
                    "href" : "http://localhost:8080/api/tacoes/2/ingredients"
                }
            }
        }]
    },
    "page" : {
        "size" : 20,
        "totalElements" : 3,
        "totalPages" : 1,
        "number" : 0
    }
}

你可能想知道我怎么知道 “taco” 会被误拼成 “tacoes”。事实证明,Spring Data REST还公开了一个home资源,其中包含所有公开端点的链接。只需向API基础路径发出GET请求即可获得:

$ curl localhost:8080/api
{
    "_links" : {
        "orders" : {
            "href" : "http://localhost:8080/api/orders"
        },
        "ingredients" : {
            "href" : "http://localhost:8080/api/ingredients"
        },
        "tacoes" : {
            "href" : "http://localhost:8080/api/tacoes{?page,size,sort}",
            "templated" : true
        },
        "users" : {
            "href" : "http://localhost:8080/api/users"
        },
        "profile" : {
            "href" : "http://localhost:8080/api/profile"
        }
    }
}

可以看到,home资源显示了所有实体的链接。除了tacoes链接之外,一切看起来都很好,其中关系名称和URL都有 “taco” 的单数复数形式。

好消息是,不必接受Spring Data REST的这个小怪癖。通过向Taco类添加一个简单的注解,可以调整关系名称和路径:

@Data
@Entity
@RestResource(rel="tacos", path="tacos")
public class Taco {
    ...
}

@RestResource注解让你可以给定任何你想要的的名称和路径的关系,在这个例子中,把它们都设定为了 “tacos”。现在当请求home资源的时候,将会看到tacos链接正确的复数形式:

"tacos": {
    "href": "http://localhost:8080/api/tacos{?page,size,sort}",
    "templeted": true
}

这还可以对端点的路径进行排序,这样就可以针对 /api/tacos 接口发起请求来使用taco资源了。

说到排序,让我们看看如何对Spring Data REST端点的结果进行排序。

6.3.2分页和排序

你可能注意到了在home资源的链接中,全部都有pagesizesort参数。默认情况下,像是对 /api/tacos 这种集合资源请求的接口来说,将会从第一页返回每页20个数据项。但是可以根据请求的要求,通过指定特定的pagesize参数来调整页面大小和哪一页。

举个例子,要请求tacos的页面大小为5的第一页,可以发起以下GET请求(使用curl

$ curl "localhost:8080/api/tacos?size=5"

假设有多于5tacos数据,可以通过添加page参数请求tacos数据的第二页:

$ curl "localhost:8080/api/tacos?size5&page=1"

注意page参数是从0开始的,意思是请求第1页实际上是请求的第2(还会注意到许多shell命令行在请求中的 & 符号上出错,这就是为什么我在前面的curl命令中引用整个URL的原因

可以使用字符串操作将这些参数添加到URL中,但是HATEOAS提供了响应中第一个、最后一个、下一个和前一个页面的链接:

"_links" : {
    "first" : {
        "href" : "http://localhost:8080/api/tacos?page=0&size=5"
    },
    "self" : {
        "href" : "http://localhost:8080/api/tacos"
    },
    "next" : {
        "href" : "http://localhost:8080/api/tacos?page=1&size=5"
    },
    "last" : {
        "href" : "http://localhost:8080/api/tacos?page=2&size=5"
    },
    "profile" : {
        "href" : "http://localhost:8080/api/profile/tacos"
    },
    "recents" : {
        "href" : "http://localhost:8080/api/tacos/recent"
    }
}

有了这些链接,API的客户端就不需要跟踪它所在的页面并将参数连接到URL。相反,它必须知道如何根据这些页面导航链接的名称查找其中一个链接并跟踪它。

sort参数允许根据实体的任何属性对结果列表进行排序。例如,需要一种方法来获取最近创建的12tacos,以便UI显示,可以通过指定以下分页和排序参数组合来做到这一点:

$ curl "localhost:8080/api/tacos?sort=createAt,desc?page=0&size=12"

这里,sort参数指定了应该根据createdDate属性进行排序,并按降序排序(以便最新的tacos排在前面。页面和大小参数的指定确定了应该在第一个页面上看到12tacos

这正是UI为了显示最近创建的tacos所需要的。它与在本章前面的DesignTacoController中定义的 /design/recent 端点大致相同。

不过有个小问题,需要对UI代码进行硬编码,以请求包含这些参数的tacos列表。但是,通过使客户端对如何构造API请求了解得太多而增加了客户端的一些弱兼容性。如果客户端可以从链接列表中查找URL,那就太好了。如果URL更简洁,就像以前的 /design/recent 端点一样,那就更好了。

6.3.3添加用户端点

Spring Data REST非常擅长创建针对Spring Data存储库执行CRUD操作的端点。但是有时需要脱离默认的CRUD API,并创建一个能够解决核心问题的端点。

没有任何东西可以阻止你在@RestController注解的bean中实现任何想要的端点,来补充Spring Data REST自动生成的内容。实际上,可以重新使用本章前面的DesignTacoController,它仍然可以与Spring Data REST提供的端点一起工作。

但是,当你编写自己的API控制器时,它们的端点似乎以以下两种方式与Spring Data REST端点分离:

  • 自己的控制器端点没有映射到Spring Data REST的基本路径下。可以强制它们的映射以任何想要的基本路径作为前缀,包括Spring Data REST基本路径,但是如果基本路径要更改,需要编辑控制器的映射来匹配。
  • 在自己的控制器中定义的任何端点都不会自动作为超链接包含在Spring Data REST端点返回的资源中。这意味着客户端将无法发现具有关系名称的自定义端点。

让我们首先解决关于基本路径的问题。Spring Data REST包括@RepositoryRestController,这是一个用于控制器类的新注解,其映射应该采用与为Spring Data REST端点配置的基本路径相同的基本路径。简单地说,@RepositoryRestController注解的控制器中的所有映射的路径都将以spring.data.rest.base-path的值为前缀(已配置为 /api

将创建一个只包含recentTacos()方法的新控制器,而不是重新启用DesignTacoController,它有几个不需要的处理程序方法。下一个程序清单中的RecentTacosController使用@RepositoryRestController进行注解,以采用Spring Data REST的基本路径进行其请求映射。程序清单6.7为控制器应用Spring Data REST基础路径

package tacos.web.api;

import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;

import java.util.List;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.rest.webmvc.RepositoryRestController;
import org.springframework.hateoas.Resources;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;

import tacos.Taco;
import tacos.data.TacoRepository;

@RepositoryRestController
public class RecentTacosController {

    private TacoRepository tacoRepo;

    public RecentTacosController(TacoRepository tacoRepo) {
        this.tacoRepo = tacoRepo;
    }

    @GetMapping(path="/tacos/recent", produces="application/hal+json")
    public ResponseEntity<Resources<TacoResource>> recentTacos() {
        PageRequest page = PageRequest.of(
            0, 12, Sort.by("createdAt").descending());
        List<Taco> tacos = tacoRepo.findAll(page).getContent();

        List<TacoResource> tacoResources =
            new TacoResourceAssembler().toResources(tacos);
        Resources<TacoResource> recentResources =
            new Resources<TacoResource>(tacoResources);

        recentResources.add(
            linkTo(methodOn(RecentTacosController.class).recentTacos())
            .withRel("recents"));

        return new ResponseEntity<>(recentResources, HttpStatus.OK);
    }
}

尽管@GetMapping映射到路径 /tacos/recent,但是类级别的@RepositoryRestController注解将确保它以Spring Data REST的基本路径作为前缀。正如所配置的,recentTacos()方法将处理 /api/tacos/recentGET请求。

需要注意的一件重要事情是,尽管@RepositoryRestController的名称与@RestController类似,但它的语义与@RestController不同。具体来说,它不确保从处理程序方法返回的值被自动写入响应体。因此,需要使用@ResponseBody对方法进行注解,或者返回一个包装响应数据的ResponseEntity

使用RecentTacosController,对 /api/tacos/recent 的请求将返回最多15个最近创建的tacos,而不需要在URL中对参数进行分页和排序。但是,当请求 /api/tacos 时,它仍然不会出现在超链接列表中。让我们解决这个问题。

6.3.4Spring Data端点添加用户超链接

如果最近的tacos端点不在 /api/tacos 返回的超链接中,客户端如何知道如何获取最近的tacos?它要么猜测,要么使用分页和排序参数。无论哪种方式,它都将在客户端代码中硬编码,这并不理想。

不过,通过声明资源处理器bean,可以将链接添加到Spring Data REST自动包含的链接列表中。Spring Data HATEOAS提供了ResourceProcessor,这是一个在通过API返回资源之前操作资源的接口。出于需要自动包含链接列表的目的,需要对ResourceProcessor进行实现,该实现将一个最近链接添加到类型为PagedResources<Resource>的任何资源(为 /api/tacos 端点返回的类型。下一个程序清单显示了定义这样一个ResourceProcessorbean方法声明。程序清单6.8Spring Data REST端点添加用户链接

@Bean
public ResourceProcessor<PagedResources<Resource<Taco>>>
    tacoProcessor(EntityLinks links) {

    return new ResourceProcessor<PagedResources<Resource<Taco>>>() {
        @Override
        public PagedResources<Resource<Taco>> process(
            PagedResources<Resource<Taco>> resource) {
            resource.add(
                links.linkFor(Taco.class)
                .slash("recent")
                .withRel("recents"));

            return resource;
        }
    };
}

程序清单6.8中显示的ResourceProcessor被定义为一个匿名内部类,并声明为一个将在Spring应用程序上下文中创建的beanSpring HATEOAS将自动发现这个bean(以及ResourceProcessor类型的任何其他bean,并将它们应用于适当的资源。在这种情况下,如果从控制器返回PagedResources<Resource>,它将收到最近创建的Taco的链接。这包括对 /api/tacos 请求的响应。

上一页
下一页