001/* 002 * Copyright (c) 2023-2026, Agents-Flex (fuhai999@gmail.com). 003 * <p> 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * <p> 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * <p> 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package com.agentsflex.core.model.client; 017 018import com.agentsflex.core.util.StringUtil; 019import okhttp3.*; 020import org.slf4j.Logger; 021import org.slf4j.LoggerFactory; 022 023import java.io.IOException; 024import java.net.InetSocketAddress; 025import java.net.Proxy; 026import java.util.concurrent.TimeUnit; 027 028public class OkHttpClientUtil { 029 030 private static final Logger log = LoggerFactory.getLogger(OkHttpClientUtil.class); 031 032 // 系统属性前缀 033 private static final String PREFIX = "okhttp."; 034 035 // 环境变量前缀(大写) 036 private static final String ENV_PREFIX = "OKHTTP_"; 037 038 private static volatile OkHttpClient defaultClient; 039 private static volatile OkHttpClient.Builder customBuilder; 040 041 public static void setOkHttpClientBuilder(OkHttpClient.Builder builder) { 042 if (defaultClient != null) { 043 throw new IllegalStateException("OkHttpClient has already been initialized. " + 044 "Please set the builder before first usage."); 045 } 046 customBuilder = builder; 047 } 048 049 public static OkHttpClient buildDefaultClient() { 050 if (defaultClient == null) { 051 synchronized (OkHttpClientUtil.class) { 052 if (defaultClient == null) { 053 OkHttpClient.Builder builder = customBuilder != null 054 ? customBuilder 055 : createDefaultBuilder(); 056 defaultClient = builder.build(); 057 log.debug("OkHttpClient initialized with config: connectTimeout={}s, readTimeout={}s, writeTimeout={}s, " + 058 "connectionPool(maxIdle={}, keepAlive={}min)", 059 getConnectTimeout(), getReadTimeout(), getWriteTimeout(), 060 getMaxIdleConnections(), getKeepAliveMinutes()); 061 } 062 } 063 } 064 return defaultClient; 065 } 066 067 private static OkHttpClient.Builder createDefaultBuilder() { 068 OkHttpClient.Builder builder = new OkHttpClient.Builder() 069 .connectTimeout(getConnectTimeout(), TimeUnit.SECONDS) 070 .readTimeout(getReadTimeout(), TimeUnit.SECONDS) 071 .writeTimeout(getWriteTimeout(), TimeUnit.SECONDS) 072 .connectionPool(new ConnectionPool(getMaxIdleConnections(), getKeepAliveMinutes(), TimeUnit.MINUTES)); 073 074 configureProxy(builder); 075 return builder; 076 } 077 078 // ==================== 配置读取方法 ==================== 079 080 private static int getConnectTimeout() { 081 return getIntConfig("connectTimeout", "CONNECT_TIMEOUT", 60); 082 } 083 084 private static int getReadTimeout() { 085 return getIntConfig("readTimeout", "READ_TIMEOUT", 300); 086 } 087 088 private static int getWriteTimeout() { 089 return getIntConfig("writeTimeout", "WRITE_TIMEOUT", 60); 090 } 091 092 private static int getMaxIdleConnections() { 093 return getIntConfig("connectionPool.maxIdleConnections", "CONNECTION_POOL_MAX_IDLE_CONNECTIONS", 5); 094 } 095 096 private static long getKeepAliveMinutes() { 097 return getLongConfig("connectionPool.keepAliveMinutes", "CONNECTION_POOL_KEEP_ALIVE_MINUTES", 10); 098 } 099 100 private static String getProxyHost() { 101 String host = getPropertyOrEnv("proxy.host", "PROXY_HOST", null); 102 if (StringUtil.hasText(host)) return host.trim(); 103 104 // 兼容 Java 标准代理属性(作为 fallback) 105 host = System.getProperty("https.proxyHost"); 106 if (StringUtil.hasText(host)) return host.trim(); 107 108 host = System.getProperty("http.proxyHost"); 109 if (StringUtil.hasText(host)) return host.trim(); 110 111 return null; 112 } 113 114 private static String getProxyPort() { 115 String port = getPropertyOrEnv("proxy.port", "PROXY_PORT", null); 116 if (StringUtil.hasText(port)) return port.trim(); 117 118 // 兼容 Java 标准代理属性 119 port = System.getProperty("https.proxyPort"); 120 if (StringUtil.hasText(port)) return port.trim(); 121 122 port = System.getProperty("http.proxyPort"); 123 if (StringUtil.hasText(port)) return port.trim(); 124 125 return null; 126 } 127 128 // 新增:获取代理用户名 129 private static String getProxyUsername() { 130 String username = getPropertyOrEnv("proxy.username", "PROXY_USERNAME", null); 131 if (StringUtil.hasText(username)) return username.trim(); 132 133 // 兼容 Java 标准代理属性 134 username = System.getProperty("https.proxyUser"); 135 if (StringUtil.hasText(username)) return username.trim(); 136 137 username = System.getProperty("http.proxyUser"); 138 if (StringUtil.hasText(username)) return username.trim(); 139 140 return null; 141 } 142 143 // 新增:获取代理密码 144 private static String getProxyPassword() { 145 String password = getPropertyOrEnv("proxy.password", "PROXY_PASSWORD", null); 146 if (StringUtil.hasText(password)) return password.trim(); 147 148 // 兼容 Java 标准代理属性 149 password = System.getProperty("https.proxyPassword"); 150 if (StringUtil.hasText(password)) return password.trim(); 151 152 password = System.getProperty("http.proxyPassword"); 153 if (StringUtil.hasText(password)) return password.trim(); 154 155 return null; 156 } 157 158 // ==================== 工具方法 ==================== 159 160 private static int getIntConfig(String sysPropKey, String envKey, int defaultValue) { 161 String value = getPropertyOrEnv(sysPropKey, envKey, null); 162 if (value == null) return defaultValue; 163 try { 164 return Integer.parseInt(value.trim()); 165 } catch (NumberFormatException e) { 166 log.warn("Invalid integer value for '{}': '{}'. Using default: {}", fullSysPropKey(sysPropKey), value, defaultValue); 167 return defaultValue; 168 } 169 } 170 171 private static long getLongConfig(String sysPropKey, String envKey, long defaultValue) { 172 String value = getPropertyOrEnv(sysPropKey, envKey, null); 173 if (value == null) return defaultValue; 174 try { 175 return Long.parseLong(value.trim()); 176 } catch (NumberFormatException e) { 177 log.warn("Invalid long value for '{}': '{}'. Using default: {}", fullSysPropKey(sysPropKey), value, defaultValue); 178 return defaultValue; 179 } 180 } 181 182 private static String getPropertyOrEnv(String sysPropKey, String envKey, String defaultValue) { 183 // 1. 系统属性优先 184 String value = System.getProperty(fullSysPropKey(sysPropKey)); 185 if (value != null) return value; 186 187 // 2. 环境变量 188 value = System.getenv(ENV_PREFIX + envKey); 189 if (value != null) return value; 190 191 return defaultValue; 192 } 193 194 private static String fullSysPropKey(String key) { 195 return PREFIX + key; 196 } 197 198 // ==================== 代理配置 ==================== 199 200 private static void configureProxy(OkHttpClient.Builder builder) { 201 String proxyHost = getProxyHost(); 202 String proxyPort = getProxyPort(); 203 204 if (StringUtil.hasText(proxyHost) && StringUtil.hasText(proxyPort)) { 205 try { 206 int port = Integer.parseInt(proxyPort); 207 InetSocketAddress address = new InetSocketAddress(proxyHost, port); 208 builder.proxy(new Proxy(Proxy.Type.HTTP, address)); 209 210 // 配置代理认证 211 String username = getProxyUsername(); 212 String password = getProxyPassword(); 213 214 if (StringUtil.hasText(username) && StringUtil.hasText(password)) { 215 configureProxyAuthenticator(builder, username, password); 216 log.debug("HTTP proxy with authentication configured: {}@{}:{}", 217 username, proxyHost, port); 218 } else { 219 log.debug("HTTP proxy configured (no authentication): {}:{}", proxyHost, port); 220 } 221 } catch (NumberFormatException e) { 222 log.warn("Invalid proxy port '{}'. Proxy will be ignored.", proxyPort, e); 223 } 224 } 225 } 226 227 // 配置代理认证器 228 private static void configureProxyAuthenticator(OkHttpClient.Builder builder, String username, String password) { 229 builder.proxyAuthenticator(new Authenticator() { 230 @Override 231 public Request authenticate(Route route, Response response) throws IOException { 232 // 检查是否是代理认证挑战 233 if (response.code() != 407) { 234 return null; // 不是代理认证,不处理 235 } 236 237 // 如果已经尝试过认证,直接放弃,防止死循环 238 if (response.request().header("Proxy-Authorization") != null) { 239 log.error("Proxy authentication failed for user: {}", username); 240 return null; 241 } 242 243 // 生成 Basic 认证凭证 (格式: "Basic base64(username:password)") 244 String credential = Credentials.basic(username, password); 245 246 // 添加 Proxy-Authorization 头并重新发送请求 247 return response.request().newBuilder() 248 .header("Proxy-Authorization", credential) 249 .build(); 250 } 251 }); 252 } 253}