7.1.1 从服务器获取数据
Taco Cloud 最酷的事情之一是它允许 Taco 狂热者设计他们自己的 Taco 作品,并与他们的 Taco 爱好者分享。为此,Taco Cloud 需要能够在单击最新设计链接时显示最近创建的 Taco 的列表。
为支持这个特性,我们增加一个 /design/recent
接口,使用 GET 请求,参数中包含 recent
,返回一个最近设计的 taco 列表。我们创建一个新的控制器来处理这样的请求,下面的程序清单显示了完成这件事的控制器。
程序清单 7.2 处理 taco 设计 API 请求的 RESTful 控制器
package tacos.web.api;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tacos.Taco;
import tacos.data.TacoRepository;
@RestController
@RequestMapping(path="/api/tacos",
produces="application/json")
@CrossOrigin(origins="*")
public class TacoController {
private TacoRepository tacoRepo;
public TacoController(TacoRepository tacoRepo) {
this.tacoRepo = tacoRepo;
}
@GetMapping(params="recent")
public Iterable<Taco> recentTacos() {
PageRequest page = PageRequest.of(
0, 12, Sort.by("createdAt").descending());
return tacoRepo.findAll(page).getContent();
}
}
您可能感觉这个控制器的名字听起来很熟悉。在第 2 章中,您创建了一个处理类似类型请求的 DesignTacoController,但那个控制器是用于返回 HTML 结果的。这里的 TacoController 是一个 REST 控制器,正如 @RestController
注解所示。
@RestController
注解有两个用途。首先,它是一个像 @Controller
和 @Service
这样的原型注解,它通过组件扫描来标记一个类。但是与 REST 的讨论最相关的是,@RestController
注解告诉 Spring,控制器中的所有处理程序方法都应该将它们的返回值直接写入响应体,而不是在模型中被带到视图中进行呈现。
实际上,可以使用 @Controller
来注解 TacoController,就像使用任何 Spring MVC 控制器一样。但是,还需要使用 @ResponseBody
注解所有处理程序方法,以获得相同的结果。另一个选项是返回一个 ResponseEntity 对象,我们稍后将讨论它。
类级别的 @RequestMapping
注解与 recentTacos()
方法上的 @GetMapping
注解一起工作,以指定 recentTacos()
方法负责处理 /design?recent
接口的 GET 请求。
注意,@RequestMapping
注解还设置了一个 produces 属性。这指定了 TacoController 中的任何处理程序方法只在请求的 Accept 头包含 “application/json” 时才处理请求。这不仅限制了 API 只生成 JSON 结果,还允许另一个控制器(可能是第 2 章中的 TacoController)处理具有相同路径的请求,只要这些请求不需要 JSON 输出。
尽管这将 API 限制为基于 JSON 的,但是欢迎将 produces 设置为多个内容类型的字符串数组。例如,为了允许 XML 输出,可以向 produces 属性添加 “text/html”:
@RequestMapping(path="/api/tacos",
produces={"application/json", "text/xml"})
在程序清单 7.2 中可能注意到的另一件事是,该类是用 @CrossOrigin
注解了的。由于应用程序的 Angular 部分将运行在独立于 API 的主机或端口上(至少目前是这样),web 浏览器将阻止 Angular 客户端使用 API。这个限制可以通过在服务器响应中包含 CORS(跨源资源共享)头来克服。Spring 使得使用 @CrossOrigin
注解应用 CORS 变得很容易。正如这里所应用的,@CrossOrigin
允许来自任何域的客户端使用 API。
recentTacos()
方法中的逻辑相当简单。它构造了一个 PageRequest 对象,该对象指定只想要包含 12 个结果的第一个(第 0 个)页面,结果按照 taco 的创建日期降序排序。简而言之就是您想要一打最新设计的 tacos。PageRequest 被传递到 TacoRepository 的 findAll()
方法的调用中,结果页面的内容被返回给客户机(如程序清单 7.1 所示,它将作为模型数据显示给用户)。
现在,您已经开始为您的客户端开发了 Taco Cloud API。出于测试目的,您可能还希望使用 curl 或 HTTPie 等命令行程序 (https://httpie.org/) 调用相关 API。例如,下面的命令行显示了如何获取最近创建的玉米卷:
$ curl localhost:8080/api/tacos?recent
如果您喜欢使用 HTTPie:
$ http :8080/api/tacos?recent
最初,数据库将是空的,因此来自这些请求的结果也将是空的。稍后我们将看到如何处理保存玉米卷的 POST 请求。但与此同时,您可以添加一个 bean,用 CommandLineRunner 预加载数据库中的一些测试数据。下面的 CommandLineRunner bean 方法显示了如何预加载一些配料,还有一些玉米卷:
@Bean
public CommandLineRunner dataLoader(
IngredientRepository repo,
UserRepository userRepo,
PasswordEncoder encoder,
TacoRepository tacoRepo) {
return args -> {
Ingredient flourTortilla = new Ingredient(
"FLTO", "Flour Tortilla", Type.WRAP);
Ingredient cornTortilla = new Ingredient(
"COTO", "Corn Tortilla", Type.WRAP);
Ingredient groundBeef = new Ingredient(
"GRBF", "Ground Beef", Type.PROTEIN);
Ingredient carnitas = new Ingredient(
"CARN", "Carnitas", Type.PROTEIN);
Ingredient tomatoes = new Ingredient(
"TMTO", "Diced Tomatoes", Type.VEGGIES);
Ingredient lettuce = new Ingredient(
"LETC", "Lettuce", Type.VEGGIES);
Ingredient cheddar = new Ingredient(
"CHED", "Cheddar", Type.CHEESE);
Ingredient jack = new Ingredient(
"JACK", "Monterrey Jack", Type.CHEESE);
Ingredient salsa = new Ingredient(
"SLSA", "Salsa", Type.SAUCE);
Ingredient sourCream = new Ingredient(
"SRCR", "Sour Cream", Type.SAUCE);
repo.save(flourTortilla);
repo.save(cornTortilla);
repo.save(groundBeef);
repo.save(carnitas);
repo.save(tomatoes);
repo.save(lettuce);
repo.save(cheddar);
repo.save(jack);
repo.save(salsa);
repo.save(sourCream);
Taco taco1 = new Taco();
taco1.setName("Carnivore");
taco1.setIngredients(Arrays.asList(
flourTortilla, groundBeef, carnitas,
sourCream, salsa, cheddar));
tacoRepo.save(taco1);
Taco taco2 = new Taco();
taco2.setName("Bovine Bounty");
taco2.setIngredients(Arrays.asList(
cornTortilla, groundBeef, cheddar,
jack, sourCream));
tacoRepo.save(taco2);
Taco taco3 = new Taco();
taco3.setName("Veg-Out");
taco3.setIngredients(Arrays.asList(
flourTortilla, cornTortilla, tomatoes,
lettuce, salsa));
tacoRepo.save(taco3);
};
}
现在,如果您尝试使用 curl 或 HTTPie 向最近的 tacos 端点发出请求,您将得到类似下面这样的响应(为可读性而格式化了响应数据):
$ curl localhost:8080/api/tacos?recent
[
{
"id": 4,
"name": "Veg-Out",
"createdAt": "2021-08-02T00:47:09.624+00:00",
"ingredients": [
{ "id": "FLTO", "name": "Flour Tortilla", "type": "WRAP" },
{ "id": "COTO", "name": "Corn Tortilla", "type": "WRAP" },
{ "id": "TMTO", "name": "Diced Tomatoes", "type": "VEGGIES" },
{ "id": "LETC", "name": "Lettuce", "type": "VEGGIES" },
{ "id": "SLSA", "name": "Salsa", "type": "SAUCE" }
]
},
{
"id": 3,
"name": "Bovine Bounty",
"createdAt": "2021-08-02T00:47:09.621+00:00",
"ingredients": [
{ "id": "COTO", "name": "Corn Tortilla", "type": "WRAP" },
{ "id": "GRBF", "name": "Ground Beef", "type": "PROTEIN" },
{ "id": "CHED", "name": "Cheddar", "type": "CHEESE" },
{ "id": "JACK", "name": "Monterrey Jack", "type": "CHEESE" },
{ "id": "SRCR", "name": "Sour Cream", "type": "SAUCE" }
]
},
{
"id": 2,
"name": "Carnivore",
"createdAt": "2021-08-02T00:47:09.520+00:00",
"ingredients": [
{ "id": "FLTO", "name": "Flour Tortilla", "type": "WRAP" },
{ "id": "GRBF", "name": "Ground Beef", "type": "PROTEIN" },
{ "id": "CARN", "name": "Carnitas", "type": "PROTEIN" },
{ "id": "SRCR", "name": "Sour Cream", "type": "SAUCE" },
{ "id": "SLSA", "name": "Salsa", "type": "SAUCE" },
{ "id": "CHED", "name": "Cheddar", "type": "CHEESE" }
]
}
]
现在,假设需要提供一个端点,该端点通过其 ID 获取单个 taco。通过在处理程序方法的路径中使用占位符变量并接受 path 变量的方法,可以捕获该 ID 并使用它通过存储库查找 taco 对象:
@GetMapping("/{id}")
public Optional<Taco> tacoById(@PathVariable("id") Long id) {
return tacoRepo.findById(id);
}
因为控制器的基本路径是 /api/tacos
,所以这个控制器方法处理 /api/tacos/{id}
的 GET 请求,其中路径的 {id}
部分是占位符。请求中的实际值指定给 id 参数,该参数通过 @PathVariable
映射到 {id}
占位符。
在 tacoById()
内部,将 id 参数传递给存储库的 findById()
方法来获取 Taco。findById()
返回一个 Optional<Taco>
,因为可能没有具有给定 ID 的 Taco,控制器只是简单的返回 Optional<Taco>
。
Spring 获取 Optional<Taco>
然后调用 get()
方法完成响应。如果 ID 不匹配任何已知的 taco,则返回 null,HTTP 响应状态码是 200 (OK)。客户端会收到一个不能使用的响应,但是状态代码表明一切正常。更好的方法是返回一个带有 HTTP 404(NOT FOUND)状态的响应。
正如它目前所写的,没有简单的方法可以从 tacoById() 返回 404 状态代码。但如果您做一些小的调整,您可以设置适当的状态代码:
@GetMapping("/{id}")
public ResponseEntity<Taco> tacoById(@PathVariable("id") Long id) {
Optional<Taco> optTaco = tacoRepo.findById(id);
if (optTaco.isPresent()) {
return new ResponseEntity<>(optTaco.get(), HttpStatus.OK);
}
return new ResponseEntity<>(null, HttpStatus.NOT_FOUND);
}
现在,tacoById()
不返回 Taco 对象,而是返回一个 ResponseEntity<Taco>
。如果发现 taco,则将 taco 对象包装在 HTTP 状态为 OK 的 ResponseEntity 中(这是之前的行为)。但是,如果没有找到 taco,则在 ResponseEntity 中包装一个 null,并加上一个 HTTP status(NOT FOUND),以指示客户端试图获取一个不存在的 taco。
但是,定义返回信息的端点只是开始。如果 API 需要从客户端接收数据呢?让我们看看如何编写处理请求输入的控制器方法。