diff --git a/src/main/java/im/zhaojun/zfile/core/config/RestTemplateConfig.java b/src/main/java/im/zhaojun/zfile/core/config/RestTemplateConfig.java index d3e485b..86cef7b 100644 --- a/src/main/java/im/zhaojun/zfile/core/config/RestTemplateConfig.java +++ b/src/main/java/im/zhaojun/zfile/core/config/RestTemplateConfig.java @@ -1,25 +1,10 @@ package im.zhaojun.zfile.core.config; -import im.zhaojun.zfile.module.storage.constant.StorageConfigConstant; -import im.zhaojun.zfile.module.storage.model.entity.StorageSourceConfig; -import im.zhaojun.zfile.module.storage.service.StorageSourceConfigService; -import org.apache.http.client.HttpClient; -import org.apache.http.impl.client.HttpClientBuilder; +import im.zhaojun.zfile.core.httpclient.ZFileOkHttp3ClientHttpRequestFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpHeaders; -import org.springframework.http.client.ClientHttpRequestInterceptor; -import org.springframework.http.client.ClientHttpResponse; -import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; -import org.springframework.http.client.OkHttp3ClientHttpRequestFactory; -import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.web.client.RestTemplate; -import javax.annotation.Resource; -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.List; - /** * restTemplate 相关配置 * @@ -27,53 +12,16 @@ import java.util.List; */ @Configuration public class RestTemplateConfig { - - @Resource - private StorageSourceConfigService storageSourceConfigService; - - /** - * OneDrive 请求 RestTemplate. - * 获取 header 中的 storageId 来判断到底是哪个存储源 ID, 在请求头中添加 Bearer: Authorization {token} 信息, 用于 API 认证. - */ - @Bean - public RestTemplate oneDriveRestTemplate() { - RestTemplate restTemplate = new RestTemplate(); - OkHttp3ClientHttpRequestFactory factory = new OkHttp3ClientHttpRequestFactory(); - restTemplate.setRequestFactory(factory); - ClientHttpRequestInterceptor interceptor = (httpRequest, bytes, clientHttpRequestExecution) -> { - HttpHeaders headers = httpRequest.getHeaders(); - Integer storageId = Integer.valueOf(((List)headers.get("storageId")).get(0).toString()); - - StorageSourceConfig accessTokenConfig = - storageSourceConfigService.findByStorageIdAndName(storageId, StorageConfigConstant.ACCESS_TOKEN_KEY); - - String tokenValue = String.format("%s %s", "Bearer", accessTokenConfig.getValue()); - httpRequest.getHeaders().add("Authorization", tokenValue); - return clientHttpRequestExecution.execute(httpRequest, bytes); - }; - restTemplate.setInterceptors(Collections.singletonList(interceptor)); - return restTemplate; - } - - - /** - * restTemplate 设置请求和响应字符集都为 UTF-8, 并设置响应头为 Content-Type: application/text; - */ - @Bean - public RestTemplate restTemplate(){ - HttpComponentsClientHttpRequestFactory httpRequestFactory = new HttpComponentsClientHttpRequestFactory(); - HttpClient httpClient = HttpClientBuilder.create().build(); - httpRequestFactory.setHttpClient(httpClient); - RestTemplate restTemplate = new RestTemplate(httpRequestFactory); - restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8)); - restTemplate.setInterceptors(Collections.singletonList((request, body, execution) -> { - ClientHttpResponse response = execution.execute(request, body); - HttpHeaders headers = response.getHeaders(); - headers.put("Content-Type", Collections.singletonList("application/text")); - return response; - })); - - return restTemplate; - } - + + /** + * OneDrive 请求 RestTemplate. + * 获取 header 中的 storageId 来判断到底是哪个存储源 ID, 在请求头中添加 Bearer: Authorization {token} 信息, 用于 API 认证. + */ + @Bean + public RestTemplate oneDriveRestTemplate() { + RestTemplate restTemplate = new RestTemplate(); + restTemplate.setRequestFactory(new ZFileOkHttp3ClientHttpRequestFactory()); + return restTemplate; + } + } \ No newline at end of file diff --git a/src/main/java/im/zhaojun/zfile/core/exception/ZFileRetryException.java b/src/main/java/im/zhaojun/zfile/core/exception/ZFileRetryException.java new file mode 100644 index 0000000..28138e7 --- /dev/null +++ b/src/main/java/im/zhaojun/zfile/core/exception/ZFileRetryException.java @@ -0,0 +1,26 @@ +package im.zhaojun.zfile.core.exception; + +/** + * @author zhaojun + */ +public class ZFileRetryException extends RuntimeException { + + public ZFileRetryException() { + } + + public ZFileRetryException(String message) { + super(message); + } + + public ZFileRetryException(String message, Throwable cause) { + super(message, cause); + } + + public ZFileRetryException(Throwable cause) { + super(cause); + } + + public ZFileRetryException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} \ No newline at end of file diff --git a/src/main/java/im/zhaojun/zfile/core/httpclient/ZFileOkHttp3ClientHttpRequestFactory.java b/src/main/java/im/zhaojun/zfile/core/httpclient/ZFileOkHttp3ClientHttpRequestFactory.java new file mode 100644 index 0000000..97ee508 --- /dev/null +++ b/src/main/java/im/zhaojun/zfile/core/httpclient/ZFileOkHttp3ClientHttpRequestFactory.java @@ -0,0 +1,22 @@ +package im.zhaojun.zfile.core.httpclient; + +import im.zhaojun.zfile.core.httpclient.logging.HttpLoggingInterceptor; +import okhttp3.OkHttpClient; +import org.springframework.http.client.OkHttp3ClientHttpRequestFactory; + +/** + * 自建 OkHTTP3 客户端工厂类, 增加日志输出拦截器. + * @author zhaojun + */ +public class ZFileOkHttp3ClientHttpRequestFactory extends OkHttp3ClientHttpRequestFactory { + + public ZFileOkHttp3ClientHttpRequestFactory() { + // 使用 OkHttp3 作为底层请求库, 并设置重试机制和日志拦截器 + super(new OkHttpClient() + .newBuilder() + .addNetworkInterceptor(new HttpLoggingInterceptor(HttpLoggingInterceptor.Logger.DEBUG) + .setLevel(HttpLoggingInterceptor.Level.HEADERS)) + .build()); + } + +} \ No newline at end of file diff --git a/src/main/java/im/zhaojun/zfile/core/httpclient/logging/HttpLoggingInterceptor.java b/src/main/java/im/zhaojun/zfile/core/httpclient/logging/HttpLoggingInterceptor.java new file mode 100644 index 0000000..a84b66c --- /dev/null +++ b/src/main/java/im/zhaojun/zfile/core/httpclient/logging/HttpLoggingInterceptor.java @@ -0,0 +1,340 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package im.zhaojun.zfile.core.httpclient.logging; + +import lombok.extern.slf4j.Slf4j; +import okhttp3.Connection; +import okhttp3.Headers; +import okhttp3.Interceptor; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okhttp3.internal.http.HttpHeaders; +import okhttp3.internal.platform.Platform; +import okio.Buffer; +import okio.BufferedSource; +import okio.GzipSource; + +import java.io.EOFException; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.TimeUnit; + +import static okhttp3.internal.platform.Platform.INFO; + +/** + * 此类代码来源于 logging-interceptor. + *
+ * An OkHttp interceptor which logs request and response information. Can be applied as an + * {@linkplain OkHttpClient#interceptors() application interceptor} or as a {@linkplain + * OkHttpClient#networkInterceptors() network interceptor}.

The format of the logs created by + * this class should not be considered stable and may change slightly between releases. If you need + * a stable logging format, use your own interceptor. + *
+ * @author zhaojun + */ +@Slf4j +public final class HttpLoggingInterceptor implements Interceptor { + + private static final Charset UTF8 = StandardCharsets.UTF_8; + + public enum Level { + /** No logs. */ + NONE, + /** + * Logs request and response lines. + * + *

Example: + *

{@code
+		 * --> POST /greeting http/1.1 (3-byte body)
+		 *
+		 * <-- 200 OK (22ms, 6-byte body)
+		 * }
+ */ + BASIC, + /** + * Logs request and response lines and their respective headers. + * + *

Example: + *

{@code
+		 * --> POST /greeting http/1.1
+		 * Host: example.com
+		 * Content-Type: plain/text
+		 * Content-Length: 3
+		 * --> END POST
+		 *
+		 * <-- 200 OK (22ms)
+		 * Content-Type: plain/text
+		 * Content-Length: 6
+		 * <-- END HTTP
+		 * }
+ */ + HEADERS, + /** + * Logs request and response lines and their respective headers and bodies (if present). + * + *

Example: + *

{@code
+		 * --> POST /greeting http/1.1
+		 * Host: example.com
+		 * Content-Type: plain/text
+		 * Content-Length: 3
+		 *
+		 * Hi?
+		 * --> END POST
+		 *
+		 * <-- 200 OK (22ms)
+		 * Content-Type: plain/text
+		 * Content-Length: 6
+		 *
+		 * Hello!
+		 * <-- END HTTP
+		 * }
+ */ + BODY + } + + public interface Logger { + void log(String message); + + /** A {@link Logger} defaults output appropriate for the current platform. */ + Logger DEFAULT = message -> Platform.get().log(INFO, message, null); + + Logger DEBUG = log::debug; + Logger TRACE = log::trace; + + } + + public HttpLoggingInterceptor() { + this(Logger.DEFAULT); + } + + public HttpLoggingInterceptor(Logger logger) { + this.logger = logger; + } + + private final Logger logger; + + private volatile Set headersToRedact = Collections.emptySet(); + + public void redactHeader(String name) { + Set newHeadersToRedact = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); + newHeadersToRedact.addAll(headersToRedact); + newHeadersToRedact.add(name); + headersToRedact = newHeadersToRedact; + } + + private volatile Level level = Level.NONE; + + /** Change the level at which this interceptor logs. */ + public HttpLoggingInterceptor setLevel(Level level) { + if (level == null) throw new NullPointerException("level == null. Use Level.NONE instead."); + this.level = level; + return this; + } + + public Level getLevel() { + return level; + } + + @Override public Response intercept(Chain chain) throws IOException { + Level level = this.level; + + Request request = chain.request(); + if (level == Level.NONE) { + return chain.proceed(request); + } + + boolean logBody = level == Level.BODY; + boolean logHeaders = logBody || level == Level.HEADERS; + + RequestBody requestBody = request.body(); + boolean hasRequestBody = requestBody != null; + + Connection connection = chain.connection(); + String requestStartMessage = "--> " + + request.method() + + ' ' + request.url() + + (connection != null ? " " + connection.protocol() : ""); + if (!logHeaders && hasRequestBody) { + requestStartMessage += " (" + requestBody.contentLength() + "-byte body)"; + } + logger.log(requestStartMessage); + + if (logHeaders) { + if (hasRequestBody) { + // Request body headers are only present when installed as a network interceptor. Force + // them to be included (when available) so there values are known. + if (requestBody.contentType() != null) { + logger.log("Content-Type: " + requestBody.contentType()); + } + if (requestBody.contentLength() != -1) { + logger.log("Content-Length: " + requestBody.contentLength()); + } + } + + Headers headers = request.headers(); + for (int i = 0, count = headers.size(); i < count; i++) { + String name = headers.name(i); + // Skip headers from the request body as they are explicitly logged above. + if (!"Content-Type".equalsIgnoreCase(name) && !"Content-Length".equalsIgnoreCase(name)) { + logHeader(headers, i); + } + } + + if (!logBody || !hasRequestBody) { + logger.log("--> END " + request.method()); + } else if (bodyHasUnknownEncoding(request.headers())) { + logger.log("--> END " + request.method() + " (encoded body omitted)"); + } else if (requestBody.isDuplex()) { + logger.log("--> END " + request.method() + " (duplex request body omitted)"); + } else { + Buffer buffer = new Buffer(); + requestBody.writeTo(buffer); + + Charset charset = UTF8; + MediaType contentType = requestBody.contentType(); + if (contentType != null) { + charset = contentType.charset(UTF8); + } + + logger.log(""); + if (isPlaintext(buffer)) { + logger.log(buffer.readString(charset)); + logger.log("--> END " + request.method() + + " (" + requestBody.contentLength() + "-byte body)"); + } else { + logger.log("--> END " + request.method() + " (binary " + + requestBody.contentLength() + "-byte body omitted)"); + } + } + } + + long startNs = System.nanoTime(); + Response response; + try { + response = chain.proceed(request); + } catch (Exception e) { + logger.log("<-- HTTP FAILED: " + e); + throw e; + } + long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs); + + ResponseBody responseBody = response.body(); + long contentLength = responseBody.contentLength(); + String bodySize = contentLength != -1 ? contentLength + "-byte" : "unknown-length"; + logger.log("<-- " + + response.code() + + (response.message().isEmpty() ? "" : ' ' + response.message()) + + ' ' + response.request().url() + + " (" + tookMs + "ms" + (!logHeaders ? ", " + bodySize + " body" : "") + ')'); + + if (logHeaders) { + Headers headers = response.headers(); + for (int i = 0, count = headers.size(); i < count; i++) { + logHeader(headers, i); + } + + if (!logBody || !HttpHeaders.hasBody(response)) { + logger.log("<-- END HTTP"); + } else if (bodyHasUnknownEncoding(response.headers())) { + logger.log("<-- END HTTP (encoded body omitted)"); + } else { + BufferedSource source = responseBody.source(); + source.request(Long.MAX_VALUE); // Buffer the entire body. + Buffer buffer = source.getBuffer(); + + Long gzippedLength = null; + if ("gzip".equalsIgnoreCase(headers.get("Content-Encoding"))) { + gzippedLength = buffer.size(); + try (GzipSource gzippedResponseBody = new GzipSource(buffer.clone())) { + buffer = new Buffer(); + buffer.writeAll(gzippedResponseBody); + } + } + + Charset charset = UTF8; + MediaType contentType = responseBody.contentType(); + if (contentType != null) { + charset = contentType.charset(UTF8); + } + + if (!isPlaintext(buffer)) { + logger.log(""); + logger.log("<-- END HTTP (binary " + buffer.size() + "-byte body omitted)"); + return response; + } + + if (contentLength != 0) { + logger.log(""); + logger.log(buffer.clone().readString(charset)); + } + + if (gzippedLength != null) { + logger.log("<-- END HTTP (" + buffer.size() + "-byte, " + + gzippedLength + "-gzipped-byte body)"); + } else { + logger.log("<-- END HTTP (" + buffer.size() + "-byte body)"); + } + } + } + + return response; + } + + private void logHeader(Headers headers, int i) { + String value = headersToRedact.contains(headers.name(i)) ? "██" : headers.value(i); + logger.log(headers.name(i) + ": " + value); + } + + /** + * Returns true if the body in question probably contains human readable text. Uses a small sample + * of code points to detect unicode control characters commonly used in binary file signatures. + */ + static boolean isPlaintext(Buffer buffer) { + try { + Buffer prefix = new Buffer(); + long byteCount = buffer.size() < 64 ? buffer.size() : 64; + buffer.copyTo(prefix, 0, byteCount); + for (int i = 0; i < 16; i++) { + if (prefix.exhausted()) { + break; + } + int codePoint = prefix.readUtf8CodePoint(); + if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) { + return false; + } + } + return true; + } catch (EOFException e) { + return false; // Truncated UTF-8 sequence. + } + } + + private static boolean bodyHasUnknownEncoding(Headers headers) { + String contentEncoding = headers.get("Content-Encoding"); + return contentEncoding != null + && !contentEncoding.equalsIgnoreCase("identity") + && !contentEncoding.equalsIgnoreCase("gzip"); + } +} \ No newline at end of file