Okhttp 通过拦截器实现对Response编码的设置

Okhttp 一个被广泛用于Android and Java applications中作为http请求的基础类库, 具有简单和方便的API接口, 支持同步和异步, 在http2协议下可以允许多个请求共享一个socket, 连接池降低延迟等特性;

问题抛出

今天做一个新项目, 应团队规范, 采用okhttp替换HttpComponents, 然后在做http请求时, 返回的内容中文乱码, 第一次使用okhttp, 遇到这样的问题, 就想先去官方的查找解决方法。。。项目介绍也太简单了吧, 跳转到github上的项目说明, 也没啥有用信息, 那就debug看看吧!

问题分析

  1. okhttp请求示例(流式api哇)
  logger.info("http ask: {}, request:\n {}", url, data);
  RequestBody body = RequestBody.create(type, data);
  Request request = new Request.Builder()
      .url(url)
      .post(body)
      .build();
  Response response = getInstance().newCall(request).execute();
  String content = response.body().string();
  logger.info("http ask: {}, response:\n {}", url, content);
  1. 直接进到最后读取server-response content的部分, 即 body().string();

1). 读取Response的content

  public final String string() throws IOException {
    BufferedSource source = source();
    try {
      Charset charset = Util.bomAwareCharset(source, charset());
      return source.readString(charset);
    } finally {
      Util.closeQuietly(source);
    }
  }

2). 获取编码, 如果contentType中的charset为空, 就默认UTF_8

  private Charset charset() {
    MediaType contentType = contentType();
    return contentType != null ? contentType.charset(UTF_8) : UTF_8;
  }

3). 根据Response.Body中的contentTypeString解析资源信息, 包含编码

  @Override public MediaType contentType() {
    return contentTypeString != null ? MediaType.parse(contentTypeString) : null;
  }

4). 解析charset, 如果服务端没返回, 那就为null

   public static MediaType get(String string) {
    Matcher typeSubtype = TYPE_SUBTYPE.matcher(string);
    if (!typeSubtype.lookingAt()) {
      throw new IllegalArgumentException("No subtype found for: \"" + string + '"');
    }
    String type = typeSubtype.group(1).toLowerCase(Locale.US);
    String subtype = typeSubtype.group(2).toLowerCase(Locale.US);

    String charset = null;
    Matcher parameter = PARAMETER.matcher(string);
    for (int s = typeSubtype.end(); s < string.length(); s = parameter.end()) {
      parameter.region(s, string.length());
      if (!parameter.lookingAt()) {
        throw new IllegalArgumentException("Parameter is not formatted correctly: \""
            + string.substring(s)
            + "\" for: \""
            + string
            + '"');
      }

      String name = parameter.group(1);
      if (name == null || !name.equalsIgnoreCase("charset")) continue;
      String charsetParameter;
      String token = parameter.group(2);
      if (token != null) {
        // If the token is 'single-quoted' it's invalid! But we're lenient and strip the quotes.
        charsetParameter = (token.startsWith("'") && token.endsWith("'") && token.length() > 2)
            ? token.substring(1, token.length() - 1)
            : token;
      } else {
        // Value is "double-quoted". That's valid and our regex group already strips the quotes.
        charsetParameter = parameter.group(3);
      }
      if (charset != null && !charsetParameter.equalsIgnoreCase(charset)) {
        throw new IllegalArgumentException("Multiple charsets defined: \""
            + charset
            + "\" and: \""
            + charsetParameter
            + "\" for: \""
            + string
            + '"');
      }
      charset = charsetParameter;
    }

    return new MediaType(string, type, subtype, charset);
  }

5). 原因, 服务端给我返了个不包含charset的contentType, 导致客户端默认按照UTF_8去解析, 导致服务端返回的GBK编码的内容就中文乱码咯

text/html

6). 问题找到了, 怎么解决呢?

解决问题

  1. 既然okhttp是从Response.Body中的contentTypeString解析编码的, 那我们可以在它读取Response.content之前设置客户端想要的contentType嘛
  2. 用拦截器来做

import com.alibaba.fastjson.JSON;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import okhttp3.Headers;
import okhttp3.Headers.Builder;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * logging and response encoding
 * @author Hinsteny
 * @version $ID: EncodingInterceptor 2018-10-30 14:16 All rights reserved.$
 */
public class EncodingInterceptor implements Interceptor {

    private static final Logger logger = LoggerFactory.getLogger(EncodingInterceptor.class);

    /**
     * 自定义编码
     */
    private String encoding;

    public EncodingInterceptor(String encoding) {
        this.encoding = encoding;
    }

    @Override public Response intercept(Interceptor.Chain chain) throws IOException {
        Request request = chain.request();
        long start = System.nanoTime();
        logger.info("Sending request: {}, headers: {}, request: {}", request.url(), request.headers(), JSON.toJSONString(request.body()));
        Response response = chain.proceed(request);
        long end = System.nanoTime();
        logger.info(String.format("Received response for %s in %.1fms%n %s", response.request().url(), (end - start) / 1e6d, JSON.toJSONString(response.headers())));
        settingClientCustomEncoding(response);
        return response;
    }

    /**
     * setting client custom encoding when server not return encoding
     * @param response
     * @throws IOException
     */
    private void settingClientCustomEncoding(Response response) throws IOException {
//        setHeaderContentType(response);
        setBodyContentType(response);
    }

    /**
     * set contentType in headers
     * @param response
     * @throws IOException
     */
    private void setHeaderContentType(Response response) throws IOException {
        String contentType = response.header("Content-Type");
        if (StringUtils.isNotBlank(contentType) && contentType.contains("charset")) {
            return;
        }
        // build new headers
        Headers headers = response.headers();
        Builder builder = headers.newBuilder();
        builder.removeAll("Content-Type");
        builder.add("Content-Type", (StringUtils.isNotBlank(contentType) ? contentType + "; ":"" ) + "charset=" + encoding);
        headers = builder.build();
        // setting headers using reflect
        Class  _response = Response.class;
        try {
            Field field = _response.getDeclaredField("headers");
            field.setAccessible(true);
            field.set(response, headers);
        } catch (NoSuchFieldException e) {
            throw new IOException("use reflect to setting header occurred an error", e);
        } catch (IllegalAccessException e) {
            throw new IOException("use reflect to setting header occurred an error", e);
        }
    }

    /**
     * set body contentType
     * @param response
     * @throws IOException
     */
    private void setBodyContentType(Response response) throws IOException {
        ResponseBody body = response.body();
        // setting body contentTypeString using reflect
        Class<? extends ResponseBody> aClass = body.getClass();
        try {
            Field field = aClass.getDeclaredField("contentTypeString");
            field.setAccessible(true);
            Field modifiersField = Field.class.getDeclaredField("modifiers");
            modifiersField.setAccessible(true);
            modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
            String contentTypeString = String.valueOf(field.get(body));
            if (StringUtils.isNotBlank(contentTypeString) && contentTypeString.contains("charset")) {
                return;
            }
            field.set(body, (StringUtils.isNotBlank(contentTypeString) ? contentTypeString + "; ":"" ) + "charset=" + encoding);
        } catch (NoSuchFieldException e) {
            throw new IOException("use reflect to setting header occurred an error", e);
        } catch (IllegalAccessException e) {
            throw new IOException("use reflect to setting header occurred an error", e);
        }
    }
}
  1. 由于Response.body没有定义方法去设置contentType, 因此用了反射去设置自定义的内容, 测试了一下, 完美通关, 至此问题解决!
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值