Skip to content

自定义 HTTP 代理(JSViewHttpProxy)

JSViewApp 的业务网络请求(本地化、升级下载、AppId 解析等)以及内核部分建连,都会经过统一出口 JSViewHttpRequester
通过 JSViewHttpRequester.setHttpProxy(...) 注入自己的 JSViewHttpProxy,可以接管这些 HTTP 请求,例如:

  • 使用 OkHttp / 自研网络库
  • HttpDNS / 指定域名走固定 IP
  • 统一加 Header、超时、证书策略等

未设置时,使用内置的 HttpURLConnection 实现。

调用链路

text
JsView 内核 / JSViewApp 业务

JSViewHttpRequester
        ↓  openConnection(url)
JSViewHttpProxy(自定义)
        ↓  返回 HttpConnection
真正发请求(OkHttp / HttpURLConnection / …)

如何设置

Application.onCreate() 中尽早设置(建议在启动任何小程序之前):

java
import com.tvcode.js_view_app.util.http.JSViewHttpRequester;

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        // 注入自定义 HTTP 代理
        JSViewHttpRequester.setHttpProxy(new MyHttpProxy());
    }
}

设置后,以下调用都会走你的 Proxy:

  • JSViewHttpRequester.getText(...)
  • JSViewHttpRequester.downloadFile(...)
  • JSViewHttpRequester.getHttpConnection(...)
  • 内核通过 URLResolve 发起的 HTTP(S) 连接

需要实现的接口

1. JSViewHttpProxy

java
package com.tvcode.js_view_app.util.http;

import java.net.InetAddress;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.List;

public interface JSViewHttpProxy {
    /**
     * 创建一个连接
     * @param url 连接地址
     * @return 连接对象
     */
    HttpConnection openConnection(URL url);

    /**
     * 可选:自定义 DNS 解析。默认使用系统 DNS。
     */
    default List<InetAddress> lookup(String hostname) throws UnknownHostException {
        // 默认实现:InetAddress.getAllByName(hostname)
        ...
    }
}

2. HttpConnection

把底层客户端(OkHttp / HttpURLConnection 等)包装成与 HttpURLConnection 语义接近的接口,需实现例如:

  • connect / disconnect
  • getResponseCode / getResponseMessage
  • getInputStream / getOutputStream / getErrorStream
  • setRequestMethod / setRequestProperty / addRequestProperty
  • setConnectTimeout / setReadTimeout
  • HTTPS:setSSLSocketFactory / setHostnameVerifier / 证书相关方法

完整方法列表见 SDK 中的 com.tvcode.js_view_app.util.http.HttpConnection

最小实现示例(OkHttp)

下面示例演示如何实现 JSViewHttpProxy,并用 OkHttp 实现完整的 HttpConnection。可按业务再增强(自定义 DNS、重试、缓存等)。

java
import com.tvcode.js_view_app.util.http.HttpConnection;
import com.tvcode.js_view_app.util.http.JSViewHttpProxy;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ProtocolException;
import java.net.URL;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okio.BufferedSink;

public class MyHttpProxy implements JSViewHttpProxy {
    private final OkHttpClient mHttpClient = new OkHttpClient();

    @Override
    public HttpConnection openConnection(URL url) {
        return new MyHttpConnection(mHttpClient, url);
    }
}

class MyHttpConnection implements HttpConnection {
    private Response mResponse;
    private final Request.Builder mRequestBuilder;
    private RequestBody mRequestBody;
    private final OkHttpClient.Builder mClientBuilder;
    private OkHttpClient mHttpClient;
    private String mMethod = "GET";
    private final Map<String, List<String>> mRequestProperties = new HashMap<>();

    MyHttpConnection(OkHttpClient httpClient, URL url) {
        mRequestBuilder = new Request.Builder().url(url);
        mClientBuilder = httpClient.newBuilder();
    }

    private void makeResponse() throws IOException {
        if (mHttpClient == null) {
            mHttpClient = mClientBuilder.build();
        }
        if (mResponse == null) {
            for (Map.Entry<String, List<String>> entry : mRequestProperties.entrySet()) {
                String key = entry.getKey();
                for (String value : entry.getValue()) {
                    mRequestBuilder.addHeader(key, value);
                }
            }
            mResponse = mHttpClient.newCall(mRequestBuilder.build()).execute();
        }
    }

    @Override
    public void connect() throws IOException {
        if (mRequestBuilder == null) {
            throw new IOException("connection is null");
        }
    }

    @Override
    public int getResponseCode() throws IOException {
        makeResponse();
        if (mResponse == null) {
            throw new IOException("connection is null");
        }
        return mResponse.code();
    }

    @Override
    public String getResponseMessage() throws IOException {
        makeResponse();
        if (mResponse == null) {
            throw new IOException("connection is null");
        }
        return mResponse.message();
    }

    @Override
    public long getContentLength() throws IOException {
        makeResponse();
        if (mResponse == null) {
            throw new IOException("connection is null");
        }
        if (mResponse.body() == null) {
            return 0;
        }
        return mResponse.body().contentLength();
    }

    @Override
    public InputStream getInputStream() throws IOException {
        makeResponse();
        if (mResponse == null || mResponse.body() == null) {
            throw new IOException("connection is null");
        }
        return mResponse.body().byteStream();
    }

    @Override
    public OutputStream getOutputStream() throws IOException {
        if (mRequestBuilder == null) {
            throw new IOException("connection is null");
        }
        if (mRequestBody == null) {
            mRequestBody = new ByteArrayRequestBody();
            mRequestBuilder.post(mRequestBody);
        }
        return ((ByteArrayRequestBody) mRequestBody).getOutputStream();
    }

    @Override
    public void disconnect() {
        if (mResponse != null) {
            mResponse.close();
        }
    }

    @Override
    public boolean usingProxy() {
        return false;
    }

    @Override
    public void setRequestMethod(String method) throws ProtocolException {
        if ("GET".equals(method) || "HEAD".equals(method)) {
            mRequestBody = null;
        } else {
            if (mRequestBody == null) {
                mRequestBody = new ByteArrayRequestBody();
            }
        }
        mRequestBuilder.method(method, mRequestBody);
        mMethod = method;
    }

    @Override
    public Map<String, List<String>> getRequestProperties() {
        return mRequestProperties;
    }

    @Override
    public String getRequestMethod() {
        return mMethod;
    }

    @Override
    public void setRequestProperty(String key, String value) {
        List<String> values = new ArrayList<>();
        values.add(value);
        mRequestProperties.put(key, values);
    }

    @Override
    public void addRequestProperty(String key, String value) {
        if (!mRequestProperties.containsKey(key)) {
            setRequestProperty(key, value);
        } else {
            mRequestProperties.get(key).add(value);
        }
    }

    @Override
    public String getRequestProperty(String key) {
        List<String> values = mRequestProperties.get(key);
        return (values != null && !values.isEmpty()) ? values.get(0) : null;
    }

    @Override
    public void setConnectTimeout(int timeout) {
        mClientBuilder.connectTimeout(timeout, TimeUnit.MILLISECONDS);
    }

    @Override
    public int getConnectTimeout() {
        return mClientBuilder.build().connectTimeoutMillis();
    }

    @Override
    public void setReadTimeout(int timeout) {
        mClientBuilder.readTimeout(timeout, TimeUnit.MILLISECONDS);
    }

    @Override
    public void setInstanceFollowRedirects(boolean followRedirects) {
        mClientBuilder.followRedirects(followRedirects);
    }

    @Override
    public boolean getInstanceFollowRedirects() {
        return mClientBuilder.build().followRedirects();
    }

    @Override
    public void setDoInput(boolean doinput) {
    }

    @Override
    public boolean getDoInput() {
        return true;
    }

    @Override
    public InputStream getErrorStream() {
        return null;
    }

    @Override
    public String getHeaderField(String name) {
        if (mResponse != null) {
            return mResponse.header(name);
        }
        return null;
    }

    @Override
    public Map<String, List<String>> getHeaderFields() {
        if (mResponse == null) {
            return null;
        }
        return mResponse.headers().toMultimap();
    }

    @Override
    public void setUseCaches(boolean usecaches) {
    }

    @Override
    public String getCipherSuite() {
        return null;
    }

    @Override
    public Certificate[] getLocalCertificates() {
        return new Certificate[0];
    }

    @Override
    public Certificate[] getServerCertificates() throws SSLPeerUnverifiedException {
        return new Certificate[0];
    }

    @Override
    public void setSSLSocketFactory(SSLSocketFactory sf) {
        try {
            TrustManagerFactory trustManagerFactory =
                    TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            trustManagerFactory.init((KeyStore) null);
            TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
            if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
                throw new IllegalStateException(
                        "Unexpected default trust managers:" + Arrays.toString(trustManagers));
            }
            X509TrustManager trustManager = (X509TrustManager) trustManagers[0];
            mClientBuilder.sslSocketFactory(sf, trustManager);
        } catch (NoSuchAlgorithmException | KeyStoreException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void setHostnameVerifier(HostnameVerifier v) {
        mClientBuilder.hostnameVerifier(v);
    }
}

class ByteArrayRequestBody extends RequestBody {
    private final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

    @Override
    public MediaType contentType() {
        return null;
    }

    @Override
    public void writeTo(BufferedSink sink) throws IOException {
        sink.write(byteArrayOutputStream.toByteArray());
    }

    public OutputStream getOutputStream() {
        return byteArrayOutputStream;
    }
}

自定义 DNS 时,覆写 lookup 即可,例如指定域名返回固定 IP:

java
@Override
public List<InetAddress> lookup(String hostname) throws UnknownHostException {
    if ("cdn.example.com".equals(hostname)) {
        return Collections.singletonList(InetAddress.getByName("1.2.3.4"));
    }
    return JSViewHttpProxy.super.lookup(hostname);
}

注意事项

  1. 尽早设置:在 Application.onCreate 中设置,避免部分请求已走默认实现。
  2. 返回 nullopenConnection 若返回 nullJSViewHttpRequester 会回退到默认 HttpURLConnection
  3. 仅 HTTP(S):内核 URLResolve 对非 http/https 的 URL 不会走 Proxy。
  4. 线程安全openConnection 可能在多线程调用,底层客户端建议使用线程安全实现(如共享 OkHttpClient)。
  5. 按需增强:示例为可运行的简易实现;上线前可按业务补齐重试、缓存、错误流、证书策略等。