使用 Spring 构建 RESTful 语录服务

工程 | Greg L. Turnquist | 2014年8月21日 | ...

最近我得知我们用于某个指南的公共 API 包含了令人反感的内容。在确认此事后,我立即回复说我们将选择另一个源。为了将来避免此类问题,我认为最好的解决方案是构建我们自己的 RESTful 语录服务。所以我决定使用最好的工具来实现这一目标,即 Spring 技术栈,并能够在第二天就完成了迁移。

选择你的工具

为了开始这项工作,我列出了我认为对于创建 RESTful Web 服务来说是正确的工具的清单。

我很快放弃了通过网页添加、删除、管理或查看数据的想法。取而代之的是,我的重点是提供一套固定的内容,其结构与指南预期消费的内容完全相同。

选择你的内容

指南的原始内容是一系列“Chunk Norris”笑话。我喜欢好笑话。但当我重新查看公共 API 时,我发现其中几个笑话有点低俗。与同事简短讨论后,提出了引用历史名言的想法。我采纳了这个想法并做了一点修改。最近我出于个人原因收集了一些开发者关于 Spring Boot 的语录,所以我决定用它们作为精选内容。

开始编码!

为了开始,我访问了 http://start.spring.io。这个 Spring Boot 应用允许您输入新项目的详细信息、选择 Java 版本,并选择所需的 Spring Boot 启动器。我使用了上面的清单,创建了一个新的基于 gradle 的项目。

定义你的域

解压项目并将其导入到我的 IDE 后,我做的第一件事就是复制了 Reactor 指南中显示的域对象。这样,我就可以确保我的 REST 服务发送出去的数据是正确的。由于我的 Quoters Incorporated 应用中的 POJO 几乎完全相同,我在这里就不发布它们了。

然后我创建了一个 Spring Data Repository。

public interface QuoteRepository extends CrudRepository<Quote, Long> {}

这个空的接口定义处理内部主键类型为 LongQuote 对象。通过扩展 Spring Data Commons 的 CrudRepository,它继承了一系列数据库操作,我们稍后会用到它们。

下一步是什么?初始化一些数据。我创建了一个 DatabaseLoader,代码如下

@Service
public class DatabaseLoader {

	private final QuoteRepository repository;

	@Autowired
	public DatabaseLoader(QuoteRepository repository) {
		this.repository = repository;
	}

	@PostConstruct
	void init() {
		repository.save(new Quote("Working with Spring Boot is like pair-programming with the Spring developers."));
		// more quotes...
	}

}
  • 它被标记为 @Service,因此当应用启动时,@ComponentScan 会自动检测到它。
  • 它使用带有自动装配的构造函数注入,以确保 QuoteRepository 的副本可用。
  • @PostConstruct 告诉 Spring MVC 在所有 bean 创建后运行数据加载方法。
  • 最后,init() 方法使用 Spring Data JPA 创建了一整批语录。

因为我在 build.gradle 中选择了 H2 作为我的数据库 (com.h2database:h2),所以根本不需要进行数据库设置(这要感谢 Spring Boot)。

创建 Controller

构建完数据库层后,我继续创建 API。使用 Spring MVC,这一点都不难。

@RestController
public class QuoteController {

	private final QuoteRepository repository;

	private final static Quote NONE = new Quote("None");

	private final static Random RANDOMIZER = new Random();

	@Autowired
	public QuoteController(QuoteRepository repository) {
		this.repository = repository;
	}

	@RequestMapping(value = "/api", method = RequestMethod.GET)
	public List<QuoteResource> getAll() {
		return StreamSupport.stream(repository.findAll().spliterator(), false)
			.map(q -> new QuoteResource(q, "success"))
			.collect(Collectors.toList());
	}

	@RequestMapping(value = "/api/{id}", method = RequestMethod.GET)
	public QuoteResource getOne(@PathVariable Long id) {
		if (repository.exists(id)) {
			return new QuoteResource(repository.findOne(id), "success");
		} else {
			return new QuoteResource(NONE, "Quote " + id + " does not exist");
		}
	}

	@RequestMapping(value = "/api/random", method = RequestMethod.GET)
	public QuoteResource getRandomOne() {
		return getOne(nextLong(1, repository.count() + 1));
	}

	private long nextLong(long lowerRange, long upperRange) {
		return (long)(RANDOMIZER.nextDouble() * (upperRange - lowerRange)) + lowerRange;
	}

}

让我们分解一下

  • 整个类被标记为 @RestController。这意味着所有路由都返回对象而不是视图。
  • 我有一些静态对象,特别是一个 NONE 语录和一个用于随机选择语录的 Java 8 Random
  • 它使用构造函数注入来获取 QuoteRepository
API 描述
/api 获取所有语录
/api/{id} 获取语录 id
/api/random 获取一个随机语录

要获取所有语录,我使用 Java 8 stream 来包装 Spring Data 的 findAll(),然后将每个结果包装到 QuoteResource 中。结果被转换为 List

要获取单个语录,它首先检查给定的 id 是否存在。如果不存在,则返回 NONE。否则,返回包装好的语录。

最后,要获取一个随机语录,我在 nextLong() 工具方法内部使用了 Java 8 的 Random 工具来获取一个包含 lowerRangeupperRangeLong

问题:为什么我使用 QuoteResourceQuote 是由 QuoteRepository 返回的核心域对象。为了匹配之前的公共 API,我将每个实例包装在 QuoteResource 中,该资源包含一个 status 代码。

测试结果

设置完成后,由 http://start.spring.io 创建的默认 Application 类就可以运行了。

$ curl localhost:8080/api/random
{
	type: "success",
	value: {
		id: 1,
		quote: "Working with Spring Boot is like pair-programming with the Spring developers."
	}
}
```

Ta dah! 

To wrap things up, I built the JAR file and pushed it up to [Pivotal Web Services](https://run.pivotal.io/). You can view the site yourself at http://gturnquist-quoters.cfapps.io/api/random.

Suffice it to say, I was able to tweak the [Reactor guide](https://springjava.cn/guides/gs/messaging-reactor/) by altering [ONE LINE OF CODE](https://github.com/spring-guides/gs-messaging-reactor/blob/master/complete/src/main/java/hello/Receiver.java#L21). With that in place, I did some other clean up of the content and was done!

To see the code, please visit https://github.com/gregturn/quoters.

### Outstanding issues

* This RESTful service satisfies [Level 2 - HTTP Verbs](https://martinfowler.com.cn/articles/richardsonMaturityModel.html#level2) of the Richardson Maturity Model. While good, it's best to shoot for [Level 3 - Hypermedia](https://martinfowler.com.cn/articles/richardsonMaturityModel.html#level3). With [Spring HATEOAS](http://projects.spring.io/spring-hateoas), it's easier than ever to add hypermedia links. Stay tuned.
* There is no friendly web page. This would be nice, but it isn't required.
* Content is fixed and defined inside the app. To make content flexible, we would need to open the door to POSTs and PUTs. This would introduce the desire to also secure things properly.

These are some outstanding things that didn't fit inside the time budget and weren't required to solve the original problem involving the Reactor guide. But they are good exercises you can explore! You can clone the project in github and take a shot at it yourself!

### SpringOne 2GX 2014

Book your place at [SpringOne](https://2014.event.springone2gx.com/register) in Dallas, TX for Sept 8-11 soon. It's simply the best opportunity to find out first hand all that's going on and to provide direct feedback. You can see myself and Roy Clarkson talk about [Spring Data REST - Data Meets Hypermedia](https://2014.event.springone2gx.com/schedule/sessions/spring_data_rest_data_meets_hypermedia.html) to see how to merge Spring Data and RESTful services.

获取 Spring 新闻简报

订阅 Spring 新闻简报保持联系

订阅

领先一步

VMware 提供培训和认证,助您快速发展。

了解更多

获取支持

Tanzu Spring 通过一项简单订阅即可提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将到来的活动

查看 Spring 社区即将到来的所有活动。

查看全部