使用 Spring 构建 RESTful 报价服务

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

我最近意识到我们正在为其中一个指南使用的公共 API 包含令人反感的内容。在确认此事后,我立即回复说我们将选择另一个来源。为了避免将来出现此类问题,我决定最好的解决方案是自己构建一个 RESTful 报价服务。因此,我决定使用最好的工具来做到这一点,即 Spring 技术栈,并且能够在第二天迁移。

选择你的工具

为了开始,我列了一个清单,列出了我知道哪些工具适合创建 RESTful Web 服务。

我很快放弃了通过网页添加、删除、管理或查看数据的想法。相反,我的重点是提供一组固定的内容,其结构与指南期望使用的结构完全相同。

选择你的内容

指南的原始内容是一系列“卓别林”的笑话。我喜欢开怀大笑。但是当我重新访问公共 API 时,我发现一些笑话有点腐败。在与同事简短讨论后,有人提出了引用历史名言的想法。我接受了这个想法并稍作修改。我最近出于个人原因收集了各种开发人员关于 Spring Boot 的名言,因此我决定将其用作精选内容。

开始编码!

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

定义你的领域

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

然后我创建了一个 Spring Data 存储库。

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

此空的接口定义处理具有类型为 Long 的内部主键的 Quote 对象。通过扩展 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 创建大量报价。

因为我选择将 H2 作为我的数据库(build.gradle 中的 **com.h2database:h2**),所以根本没有设置数据库(感谢 Spring Boot)。

创建控制器

构建了这个数据库层之后,我继续创建 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 流来包装 Spring 数据的 findAll(),依次将每个报价包装到 QuoteResource 中。结果被转换为 List

为了获取单个报价,它首先测试给定的 ID 是否存在。如果不存在,则返回 NONE。否则,返回包装后的报价。

最后,为了获取随机报价,我在 nextLong() 实用程序方法中使用 Java 8 的 Random 实用程序来获取具有 lowerRangeupperRange(包含)的 Long

问题:为什么我使用 QuoteResourceQuoteQuoteRepository 返回的核心领域对象。为了匹配之前的公共 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://springframework.org.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 社区中所有即将举行的活动。

查看全部