StackExchange REST Client with Spring and RestTemplate – 使用Spring和RestTemplate的StackExchange REST客户端

最后修改: 2013年 2月 25日

中文/混合/英文(键盘快捷键:t)

This article will cover a quick side-project – a bot to automatically tweet Top Questions from the various Q&A StackExchange sites, such as StackOverflow, ServerFault, SuperUser, etc. We will build a simple client for the StackExchange API and then we’ll setup the interaction with the Twitter API using Spring Social – this first part will focus on the StackExchange Client only.

本文将介绍一个快速的副业–一个自动推送各种Q&A StackExchange网站的热门问题的机器人,例如StackOverflow, 服务器故障超级用户,等等。我们将为StackExchange API构建一个简单的客户端,然后我们将使用Spring Social设置与Twitter API的交互 – 这第一部分将只关注StackExchange客户端的问题。

The initial purpose of this implementation is not to be a full fledged Client for the entire StackExchange API – that would be outside the scope of this project. The only reason the Client exists is that I couldn’t fine one that would work against the 2.x version of the official API.

这个实现的最初目的是不成为整个StackExchange API的成熟的客户端 – 这将超出这个项目的范围。这个客户端存在的唯一原因是,我无法找到一个可以对抗2.x版本的官方API的客户端。

1. The Maven Dependencies

1.Maven的依赖性

To consume the StackExchange REST API, we will need very few dependencies – essentially just an HTTP client – the Apache HttpClient will do just fine for this purpose:

为了使用StackExchange REST API,我们需要很少的依赖性–基本上就是一个HTTP客户端–Apache HttpClient就可以达到这个目的。

<dependency>
   <groupId>org.apache.httpcomponents</groupId>
   <artifactId>httpclient</artifactId>
   <version>4.3.3</version>
</dependency>

The Spring RestTemplate could also have been used to interact with the HTTP API, but that would have introduced quite a lot of other Spring related dependencies into the project, dependencies which are not strictly necessary, so HttpClient will keep things light and simple.

Spring RestTemplate也可以用来与HTTP API交互,但那会在项目中引入相当多的其他Spring相关的依赖,这些依赖并不是严格必要的,所以HttpClient会保持轻盈和简单。

2. The Questions Client

2.问题客户

The goal of this Client is to consume the /questions REST Service that StackExchange publishes, not to provide a general purpose client for the entire StackExchange APIs – so for the purpose of this article we will only look at that.
The actual HTTP communication using HTTPClient is relatively straightforward:

这个客户端的目标是消费StackExchange 发布的REST服务,而不是为整个StackExchange API提供一个通用的客户端 – 所以在这篇文章中,我们只看这个。
使用HTTPClient的实际HTTP通信是相对简单的。

public String questions(int min, String questionsUri) {
   HttpGet request = null;
   try {
      request = new HttpGet(questionsUri);
      HttpResponse httpResponse = client.execute(request);
      InputStream entityContentStream = httpResponse.getEntity().getContent();
      return IOUtils.toString(entityContentStream, Charset.forName("utf-8"));
   } catch (IOException ex) {
      throw new IllegalStateException(ex);
   } finally {
      if (request != null) {
         request.releaseConnection();
      }
   }
}

This simple interaction is perfectly adequate for obtaining the questions raw JSON that the API publishes – the next step will be processing that JSON.
There is one relevant detail here – and that is the questionsUri method argument – there are multiple StackExchange APIs that can publish questions (as the official documentation suggests), and this method needs to be flexible enough to consume all of them. It can consume for example the simplest API that returns questions by setting questionUri set to https://api.stackexchange.com/2.1/questions?site=stackoverflow or it may consume the tag based https://api.stackexchange.com/2.1/tags/{tags}/faq?site=stackoverflow API instead, depending on what the client needs.

这种简单的互动完全可以获得API发布的问题原始JSON–下一步将是处理这些JSON。

这里有一个相关的细节–那就是questionsUri方法参数–有多个StackExchange API可以发布问题(正如官方文档所示),这个方法需要足够灵活,以消费所有这些问题。例如,它可以消费最简单的API,通过将questionUri设置为https://api.stackexchange.com/2.1/questions?site=stackoverflow 来返回问题,或者它可以消费基于标签的https://api.stackexchange.com/2.1/tags/{tags}/faq?site=stackoverflow API,这取决于客户的需求。

A request to the StackExchange API is fully configured with query parameters, even for the more complex advanced search queries – there is no body being send. To construct the questionsUri, we’ll build a basic fluent RequestBuilder class that will make use of the URIBuilder from the HttpClient library. This will take care of correctly encoding the URI and generally making sure that the end result is valid:

对StackExchange API的请求是完全配置了查询参数的,即使是更复杂的高级搜索查询,也没有发送任何主体。为了构建questionsUri,我们将构建一个基本的流畅的RequestBuilder类,它将利用HttpClient库中的URIBuilder。这将负责对URI进行正确编码,并确保最终的结果是有效的。

public class RequestBuilder {
   private Map<String, Object> parameters = new HashMap<>();

   public RequestBuilder add(String paramName, Object paramValue) {
       this.parameters.put(paramName, paramValue);
      return this;
   }
   public String build() {
      URIBuilder uriBuilder = new URIBuilder();
      for (Entry<String, Object> param : this.parameters.entrySet()) {
         uriBuilder.addParameter(param.getKey(), param.getValue().toString());
      }

      return uriBuilder.toString();
   }
}

So now, to construct a valid URI for the StackExchange API:

所以现在,要为StackExchange API构建一个有效的URI。

String params = new RequestBuilder().
   add("order", "desc").add("sort", "votes").add("min", min).add("site", site).build();
return "https://api.stackexchange.com/2.1/questions" + params;

3. Testing the Client

3.测试客户

The Client will output raw JSON, but to test that, we will need a JSON processing library, specifically Jackson 2:

客户端将输出原始JSON,但为了测试,我们将需要一个JSON处理库,特别是杰克逊2

<dependency>
   <groupId>com.fasterxml.jackson.core</groupId>
   <artifactId>jackson-databind</artifactId>
   <version>2.3.3</version>
   <scope>test</scope>
</dependency>

The tests we’ll look at will interact with the actual StackExchange API:

我们要看的测试将与实际的StackExchange API互动。

@Test
public void whenRequestIsPerformed_thenSuccess() 
      throws ClientProtocolException, IOException {
   HttpResponse response = questionsApi.questionsAsResponse(50, Site.serverfault);
   assertThat(response.getStatusLine().getStatusCode(), equalTo(200));
}
@Test
public void whenRequestIsPerformed_thenOutputIsJson() 
      throws ClientProtocolException, IOException {
   HttpResponse response = questionsApi.questionsAsResponse(50, Site.serverfault);
   String contentType = httpResponse.getHeaders(HttpHeaders.CONTENT_TYPE)[0].getValue();
   assertThat(contentType, containsString("application/json"));
}
@Test
public void whenParsingOutputFromQuestionsApi_thenOutputContainsSomeQuestions() 
     throws ClientProtocolException, IOException {
   String questionsAsJson = questionsApi.questions(50, Site.serverfault);

   JsonNode rootNode = new ObjectMapper().readTree(questionsAsJson);
   ArrayNode questionsArray = (ArrayNode) rootNode.get("items");
   assertThat(questionsArray.size(), greaterThan(20));
}

The first test has verified that the response provided by the API was indeed a 200 OK, so the GET request to retrieve the questions was actually successful. After that basic condition is ensured, we moved on to the Representation – as specified by the Content-Type HTTP header – that needs to be JSON. Next, we actually parse the JSON and verify that there are actually Questions in that output – that parsing logic itself is low level and simple, which is enough for the purpose of the test.

第一个测试已经验证了API提供的响应确实是200 OK,所以检索问题的GET请求实际上是成功的。在这个基本条件得到保证后,我们转到了需要JSON的表述–正如Content-Type HTTP头所指定的那样。接下来,我们实际解析JSON,并验证该输出中是否真的有问题–该解析逻辑本身是低级和简单的,这对于测试的目的来说已经足够了。

Note that these requests count towards your rate limits specified by the API – for that reason, the Live tests are excluded from the standard Maven build:

请注意,这些请求将计入您的速率限制,由API指定 – 为此,Live测试不在标准Maven构建范围内。

<plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-surefire-plugin</artifactId>
   <version>2.17</version>
   <configuration>
      <excludes>
         <exclude>**/*LiveTest.java</exclude>
      </excludes>
   </configuration>
</plugin>

4. The Next Step

4.下一步

The current Client is only focused on a single type of Resource from the many available types published by the StackExchange APIs. This is because it’s initial purpose is limited – it only needs to enable a user to consume Questions from the various sites in the StackExchange portfolio. Consequently, the Client can be improved beyond the scope of this initial usecase to be able to consume the other types of the API.
The implementation is also very much raw – after consuming the Questions REST Service, it simply returns JSON output as a String – not any kind of Questions model out of that output. So, a potential next step would be to unmarshall this JSON into a proper domain DTO and return that back instead of raw JSON.

目前的客户端只关注StackExchange APIs发布的许多可用类型中的单一类型资源。这是因为它的初始目的是有限的–它只需要使用户能够从StackExchange组合中的各个网站消费Questions。因此,客户端可以在这个初始用例的范围之外进行改进,以便能够消费API 的其他类型
该实现也是非常原始的–在消费了Questions REST服务之后,它只是将JSON输出作为一个字符串返回,而不是从该输出中获得任何类型的Questions模型。因此,潜在的下一步是将该 JSON 解读为适当的域 DTO,并返回该域而不是原始 JSON。

5. Conclusion

5.结论

The purpose of this article was to show how to start building an integration with the StackExchange API, or really an HTTP based API out there. It covered how to write integration tests against the live API and make sure the end to end interaction actually works.

这篇文章的目的是展示如何开始构建与StackExchange API的集成,或者说是基于HTTP的API。它涵盖了如何编写针对实时API的集成测试,并确保端到端交互的实际工作。

The second part of this article will show how to interact with the Twitter API by using the Spring Social library, and how to use the StackExchange Client we built here to tweet questions on a new twitter account.

本文的第二部分将展示如何通过使用Spring Social库与Twitter API进行交互,以及如何使用我们在此构建的StackExchange客户端在新的Twitter账户上推送问题。

I have already set up a few twitter accounts that are now tweeting the 2 Top Questions per day, for various disciplines:

我已经建立了几个微博账户,现在每天都会在微博上发布不同学科的2个热门问题。

  • SpringTip – Two of the best Spring questions from StackOverflow each day
  • JavaTopSO – Two of the best Java questions from StackOverflow each day
  • AskUbuntuBest – Two of the best questions from AskUbuntu each day
  • BestBash – Two of the best Bash questions from all StackExchange sites each day
  • ServerFaultBest – Two of the best questions from ServerFault each day

The full implementation of this StackExchange Client is on github.

这个StackExchange客户端的完整实现是on github