如何在Spring-Web中使用RestTemplate解析gzip编码响应

23

在我修改了Consuming a RESTful Web Service示例以从api.stackexchange.com调用根据ID获取用户后,我遇到了JsonParseException:

com.fasterxml.jackson.core.JsonParseException:非法字符((CTRL-CHAR,代码31)):仅允许在记号之间使用常规空格(\r,\n,\t)

api.stackexchange.com的响应是gzip压缩的。

如何将Spring-Web RestTemplate添加对gzip压缩的支持?

我正在使用Spring boot parent ver。1.3.1.RELEASE因此Spring-Web 4.2.4-RELEASE

这是我调整后的示例:

User.java

package stackexchange.dto;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.PropertyNamingStrategy.LowerCaseWithUnderscoresStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonNaming(LowerCaseWithUnderscoresStrategy.class)
public class User {

    // Properties made public in order to shorten the example
    public int userId;
    public String displayName;
    public int reputation;

    @Override
    public String toString() {
        return "user{"
                + "display_name='" + displayName + '\''
                + "reputation='" + reputation + '\''
                + "user_id='" + userId + '\''
                + '}';
    }
}

CommonWrapper.java

package stackexchange.dto;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.PropertyNamingStrategy.LowerCaseWithUnderscoresStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonNaming(LowerCaseWithUnderscoresStrategy.class)
public class CommonWrapper {

    // Properties made public in order to shorten the example
    public boolean hasMore;
    // an array of the type found in type
    public User[] items;
    public int page;
    public int pageSize;
    public int quotaMax;
    public int quotaRemaining;

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        for (User user : items) {
            sb.append("{" + user.toString() + "}\n");
        }

        return "common_wrapper{"
        + "\"items\"=[\n"
        + sb
        + "]"
        + "has_more='" + hasMore + '\''
        + "page='" + page + '\''
        + "page_size='" + pageSize + '\''
        + "quota_max='" + quotaMax + '\''
        + "quota_remaining='" + quotaRemaining + '\''
        + '}';
    }
}

StackExchange.java

package stackexchange;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.web.client.RestTemplate;

import stackexchange.dto.CommonWrapper;

import com.fasterxml.jackson.databind.PropertyNamingStrategy.LowerCaseWithUnderscoresStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;

@JsonNaming(LowerCaseWithUnderscoresStrategy.class)
public class StackExchange implements CommandLineRunner{

    private static final Logger log = LoggerFactory.getLogger(StackExchange.class);

    public static void main(String args[]) {
        SpringApplication.run(StackExchange.class);
    }

    @Override
    public void run(String... strings) throws Exception {

        RestTemplate restTemplate = new RestTemplate();
        CommonWrapper response = restTemplate
                .getForObject(
                        "https://api.stackexchange.com/2.2/users/4607349?site=stackoverflow",
                        CommonWrapper.class);

        log.info(response.toString());
    }

}

pom.xml - 与示例中相同

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>stackexchangetest</groupId>
  <artifactId>stackexchangetest</artifactId>
  <version>0.0.1</version>
  <name>stackexchangetest</name>
  <description>api.stackexchange.com Test</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.3.1.RELEASE</version>
    </parent>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>   
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

1
你有没有看过这个文档:http://docs.spring.io/autorepo/docs/spring-android/1.0.x/reference/html/rest-template.html#d4e395 ? - ben75
1
是的,我已经阅读了。这是针对Spring Android的解决方案,而我使用的是Spring Web。在Spring Web中,'HttpHeaders'不包含'setAcceptEncoding'方法。为避免混淆,我将在问题中说明。谢谢。 - Michal Foksa
我喜欢这个问题的元性。 - mR_fr0g
4个回答

63

用来自Apache HttpClient的请求工厂(可以在传输过程中解码GZIP)替换默认的请求工厂:

HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(
            HttpClientBuilder.create().build());
RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory);

将 Apache Http Client 添加到 pom.xml 文件中

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <!--Version is not needed when used with Spring Boot parent pom file -->
    <version>4.5.1</version>
</dependency>

谢谢Michal, 它工作了。通常的文档描述的是Android而不是Spring Web,正如你所说的。如果有任何博客或链接,为什么不也添加一下参考呢? - Ashutosh Jha
太好了,我很高兴能帮忙。当时我没有找到任何文章或博客帖子。请随意记录它并让Google正确索引它,这样就不会再有人遇到困难了 :) - Michal Foksa
嗨,Michal,你知道在多线程环境中使用HttpComponentsClientHttpRequestFactory有什么缺点吗?我的意思是当用户的请求非常高时。 - Ashutosh Jha
不,我不知道有任何问题。我发现HttpComponentsClientHttpRequestFactory比默认工厂好得多。它更好地处理cookies、重定向、代理服务器等。 - Michal Foksa
1
分享这篇文章,因为它详细解释了httpclient在必要时如何/在哪里解压缩响应。请参见@Garry在此帖子上的回答。 - Norbert
这个解决方案并不完美。HttpClient有自己的线程池。当服务器超时时,在这种方法中,HttpClient可能会一直等待。这最终会耗尽池中的所有线程。at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039) at org.apache.http.pool.AbstractConnPool.getPoolEntryBlocking(AbstractConnPool.java:393) .. at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:776) ··· - wxh

1
private String callViaRest(String requestString, Steps step) {
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.TEXT_XML);
    headers.add("Accept-Encoding", "application/gzip");
    HttpEntity<String> entity = new HttpEntity<String>(requestString, headers);

    byte[] responseBytes = jsonRestTemplate
            .exchange("yourUrl", HttpMethod.POST, entity, byte[].class).getBody();
    String decompressed = null;
    try {
        decompressed= new String(CompressionUtil.decompressGzipByteArray(responseBytes),Charsets.UTF_8);
    } catch (IOException e) {
        LOGGER.error("network call failed.", e);
    }
    return decompressed;
}

将org.apache.commons.jcs的依赖项添加到pom文件。导入org.apache.commons.jcs.utils.zip.CompressionUtil; - Ashutosh Jha
通过添加headers.add("Accept-Encoding", "application/gzip"),我的问题得到解决,谢谢您的解决方案。 - Kishor K
我知道这个答案发布已经有很长时间了,但我有一个问题。当抛出HttpStatusCodeException时,responseBody怎么办?我的意思是,使用接受的答案中发布的工厂,您可以在exchange方法抛出HttpStatusCodeException时获取响应正文,但是使用此答案,我无法在该错误情况下恢复响应。 - Anne
@KishorK 你太棒了! - dian mushkov

1

我想解决相同的问题,但不使用额外的库。

  1. 使用 responseType 为 byte[].class
  2. 自己解压响应
  3. 通过 ObjectMapper mapper 自己映射实体。

这不是最优雅的解决方案,但它可以工作。

    ResponseEntity<byte[]> response = restTemplate.exchange(url, HttpMethod.GET, new HttpEntity<byte[]>(createHeaders()), byte[].class);
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    try (GZIPInputStream gzipInputStream = new GZIPInputStream(new ByteArrayInputStream(response.getBody()))) {
        gzipInputStream.transferTo(byteArrayOutputStream);
    }
    byte[] content = byteArrayOutputStream.toByteArray();
    GenericResponseDto question = mapper.readValue(content, GenericResponseDto .class);  
    log.info("Response :" + question.toString());

-1

我想评论一下wxh对已接受答案的评论..评论太短了,而且带有代码很难阅读。wxh说已接受的答案是一个“不完美”的解决方案,但是..至少它是“相当完美”的,对吧?如果你想深入了解wxh提到的细节,你可以随时使用(我总是这样做),一个自定义的httpclient,并将其设置为你的确切口味。在这个配置为自定义httpclient设置压缩支持中,它将支持所有三种最常见的压缩方法,gzip、deflate和brotli。如果你想控制超时等等,只需添加类似于以下内容的东西

    private static final RequestConfig DEFAULT_CONFIG = RequestConfig.custom()
        .setConnectTimeout(20 * 1000)           // the time to establish the connection with the remote host
        .setConnectionRequestTimeout(10 * 1000) // timeout used when requesting a connection from the local cm
        .setSocketTimeout(40 * 1000).build();   // the time waiting for data – after establishing the connection; 
                                                // maximum time of inactivity between two data packets

或者类似这样的东西,然后像钩子一样挂上去

.setDefaultRequestConfig(DEFAULT_CONFIG)
..
.build();

如果您也想(正如wxh所建议的那样)控制连接池等内容,则可以像下面这样添加:

and/or

        PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
        // Increase max total connection to 200
        cm.setMaxTotal(200);
        // Increase default max connection per route to 20
        cm.setDefaultMaxPerRoute(20);

        SocketConfig sc = SocketConfig.custom().setSoKeepAlive(true)
                .setSoReuseAddress(true)
                .setTcpNoDelay(true)
                .setSoTimeout(40 * 1000)
                .setSoLinger(5).build();
        cm.setDefaultSocketConfig(sc);

(最大并行请求为200可能有点过头了,上面是针对特殊情况的...)如果这是你想要的,那么就像这样挂钩进去

   .setConnectionManager(cm)
   ..
   .build();

简而言之 - 使用自定义的httpclient,您几乎可以控制每一个细节。我也不喜欢“自动重试”,所以我进行了钩入。

  .setRetryHandler(new DefaultHttpRequestRetryHandler(0, false))

同样,永远如此。不过 - 当你完成并感到满意时,只需按照已接受的答案中所述将其挂钩即可。

    HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(myCustomHttpClient);
    RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接