Stories

Detail Return Return

HttpUtil請求工具類(基於OkHttp) - Stories Detail

在系統編寫的時候,常常會與外圍系統的接口進行對接,或者調用一些子系統的接口,那麼此時會涉及到http請求。

在以往,我沒有采用統一的方式編寫這類程序,會使用hutool、RestTemplate,所以,在此,我想給自己編寫一套統一的請求方式,以便學習。

採用框架:OkHttp3,原因是瞭解到此框架性能良好,安全性好。

需求:

  1. 普通的get、post請求
  2. 異步的post請求

OkHttp基礎概念

一、OkHttpClient

此類是框架的核心客户端

二、RequestBody
用於構建不同的請求類型

  1. application/x-www-form-urlencoded

上手使用

添加Maven依賴

<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.12.0</version> <!-- 2023年最新穩定版 -->
</dependency>

工具類

package org.example;

import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.*;
import okio.Buffer;
import okio.BufferedSink;
import okio.Okio;
import okio.Source;

import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

/**
 * 基於okhttp4的HTTP請求工具類
 * @author song
 */
public class HttpUtil {
    private static final OkHttpClient client = new OkHttpClient.Builder()
            .connectTimeout(15, TimeUnit.SECONDS)
            .readTimeout(30, TimeUnit.SECONDS)
            .callTimeout(60, TimeUnit.SECONDS)
            .build();

    private static final ObjectMapper objectMapper = new ObjectMapper();
    private static final MediaType JSON_MEDIA_TYPE = MediaType.get("application/json; charset=utf-8");
    private static final MediaType STREAM_MEDIA_TYPE = MediaType.parse("application/octet-stream");

    private HttpUtil() {}

    /**
     * 同步GET請求
     */
    public static String get(String url, Map<String, Object> headers, Map<String, Object> queryParams) throws IOException {
        HttpUrl httpUrl = buildGetUrl(url, queryParams);
        Request request = new Request.Builder()
                .url(httpUrl)
                .headers(buildHeaders(headers))
                .get()
                .build();
        try (Response response = client.newCall(request).execute()) {
            validateResponse(response);
            return Objects.requireNonNull(response.body()).string();
        }
    }

    /**
     * 同步POST請求(JSON格式)
     */
    public static String postJson(String url, Map<String, Object> headers, Object bean) throws IOException {
        String json = objectMapper.writeValueAsString(bean);
        RequestBody body = RequestBody.create(json, JSON_MEDIA_TYPE);
        Request request = new Request.Builder()
                .url(url)
                .headers(buildHeaders(headers))
                .post(body)
                .build();
        try (Response response = client.newCall(request).execute()) {
            validateResponse(response);
            return Objects.requireNonNull(response.body()).string();
        }
    }
    /**
     * 同步POST請求(JSON格式)
     */
    public static String postJson(String url, Map<String, Object> headers, String json) throws IOException {
        RequestBody body = RequestBody.create(json, JSON_MEDIA_TYPE);
        Request request = new Request.Builder()
                .url(url)
                .headers(buildHeaders(headers))
                .post(body)
                .build();
        try (Response response = client.newCall(request).execute()) {
            validateResponse(response);
            return Objects.requireNonNull(response.body()).string();
        }
    }

    /**
     * 文件下載方法
     * @param savePath 文件保存路徑(包含文件名)
     */
    public static void downloadFile(String url, Map<String, Object> headers, String savePath) throws IOException {
        Request request = new Request.Builder()
                .url(url)
                .headers(buildHeaders(headers))
                .build();

        try (Response response = client.newCall(request).execute()) {
            validateResponse(response);

            File file = new File(savePath);
            try (BufferedSink sink = Okio.buffer(Okio.sink(file))) {
                sink.writeAll(Objects.requireNonNull(response.body()).source());
            }
        }
    }

    public static String uploadFile(String url, Map<String, Object> headers, String fileFieldName,
                                    File file, Consumer<Progress> progressConsumer) throws IOException {
        return uploadFile(url, headers, fileFieldName, file, null, progressConsumer);
    }

    public static String uploadFile(String url, Map<String, Object> headers,
                                    String fieldName, File file,
                                    Map<String, String> formParams,
                                    Consumer<Progress> progressConsumer) throws IOException {
        if (file == null) {
            throw new IOException("上傳文件不能為null");
        }
        if (!file.exists() || !file.isFile()) {
            throw new IOException("文件不存在或不是有效文件: " + file.getAbsolutePath());
        }
        RequestBody fileBody = createStreamingBody(file, STREAM_MEDIA_TYPE, progressConsumer);
        MultipartBody.Builder builder = new MultipartBody.Builder()
                .setType(MultipartBody.FORM)
                .addFormDataPart(fieldName, file.getName(), fileBody);

        // 添加額外的表單參數
        if (formParams != null) {
            for (Map.Entry<String, String> entry : formParams.entrySet()) {
                builder.addFormDataPart(entry.getKey(), entry.getValue());
            }
        }

        MultipartBody body = builder.build();
        Request request = new Request.Builder()
                .url(url)
                .headers(buildHeaders(headers))
                .post(body)
                .build();
        try (Response response = client.newCall(request).execute()) {
            validateResponse(response);
            return Objects.requireNonNull(response.body()).string();
        }
    }

    /**
     * 創建流式上傳的請求體(帶進度回調)
     */
    private static RequestBody createStreamingBody(final File file, final MediaType mediaType,
                                                   final Consumer<Progress> progressConsumer) {
        return new RequestBody() {
            @Override
            public MediaType contentType() {
                return mediaType;
            }

            @Override
            public long contentLength() {
                return file.length();
            }

            @Override
            public void writeTo(BufferedSink sink) throws IOException {
                try (Source source = Okio.source(file)) {
                    long total = file.length();
                    long uploaded = 0;
                    Buffer buffer = new Buffer();
                    long read;

                    while ((read = source.read(buffer, 8192)) != -1) {
                        sink.write(buffer, read);
                        uploaded += read;
                        buffer.clear();

                        if (progressConsumer != null && total > 0) {
                            progressConsumer.accept(new Progress(uploaded, total));
                        }
                    }
                }
            }
        };
    }

    // 構建GET請求URL
    private static HttpUrl buildGetUrl(String url, Map<String, Object> params) throws IOException {
        if (url == null || url.trim().isEmpty()) {
            throw new IOException("請求URL不能為空");
        }
        HttpUrl baseUrl = Objects.requireNonNull(HttpUrl.parse(url), "無效的URL: " + url);
        HttpUrl.Builder urlBuilder = baseUrl.newBuilder();
        if (params != null) {
            for (Map.Entry<String, Object> entry : params.entrySet()) {
                urlBuilder.addQueryParameter(entry.getKey(), String.valueOf(entry.getValue()));
            }
        }
        return urlBuilder.build();
    }

    // 構建請求頭
    private static Headers buildHeaders(Map<String, Object> headers) {
        Headers.Builder builder = new Headers.Builder();
        if (headers != null) {
            for (Map.Entry<String, Object> entry : headers.entrySet()) {
                builder.add(entry.getKey(), String.valueOf(entry.getValue()));
            }
        }
        return builder.build();
    }

    // 驗證響應狀態
    private static void validateResponse(Response response) throws IOException {
        if (!response.isSuccessful()) {
            throw new IOException("HTTP請求失敗: " + response.code() + " - " + response.message());
        }
    }
    public static class Progress {
        private final long current;
        private final long total;
        public Progress(long current, long total) {
            this.current = current;
            this.total = total;
        }
        public long getCurrent() {
            return current;
        }
        public long getTotal() {
            return total;
        }

        public double getPercentage() {
            return total > 0 ? (current * 100.0 / total) : 0;
        }
    }
}

Add a new Comments

Some HTML is okay.