feat(biz): 实现可重试的HTTP客户端服务
- 新增RetryableHttpClient,支持自动登录和token刷新 - 实现线程安全的令牌管理机制 - 添加RestTemplate配置和条件化注入 - 创建业务HTTP客户端服务接口及实现类 - 集成Hutool工具库进行数据转换处理 - 实现API请求失败后的重试逻辑 - 添加详细的日志记录和异常处理机制
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
package com.jeelowcode.module.biz.config;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
/**
|
||||
* HttpClient配置类
|
||||
*
|
||||
* @author lingma
|
||||
*/
|
||||
@Configuration
|
||||
public class HttpClientConfig {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public RestTemplate restTemplate() {
|
||||
return new RestTemplate();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package com.jeelowcode.module.biz.http;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* 可重试的HttpClient,支持自动登录和token刷新
|
||||
* 线程安全的实现,可以在并发环境下使用
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@ConditionalOnProperty(
|
||||
name = "jeelowcode.powerenv.baseurl", matchIfMissing = true)
|
||||
public class RetryableHttpClient {
|
||||
|
||||
@Value("${jeelowcode.powerenv.baseurl}")
|
||||
private String baseUrl;
|
||||
|
||||
// 访问令牌URL
|
||||
private static final String ACCESS_TOKEN_URL = "/_campus/open/accessToken.json";
|
||||
|
||||
// API接口URL
|
||||
private static final String API_URL = "/_campus/open/api/invoked.json";
|
||||
|
||||
// 应用凭证
|
||||
private static final String APP_KEY = "campusT0Yoh1U6tXD3ZVXy";
|
||||
private static final String APP_SECRET = "3d4d89b5-8a14-4b83-a9aa-715bdc8264a1";
|
||||
|
||||
// 存储访问令牌及相关信息
|
||||
private volatile String accessToken;
|
||||
private volatile long tokenExpireTime;
|
||||
|
||||
// 用于保护令牌更新操作的锁
|
||||
private final ReentrantLock tokenLock = new ReentrantLock();
|
||||
|
||||
// RestTemplate实例
|
||||
@Resource
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// 初始化时获取一次访问令牌
|
||||
refreshAccessToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取API数据
|
||||
*
|
||||
* @return ResponseEntity<String> 响应结果
|
||||
*/
|
||||
public ResponseEntity<String> getApiData() {
|
||||
// 检查令牌是否过期,如果过期则刷新
|
||||
if (isTokenExpired()) {
|
||||
refreshAccessToken();
|
||||
}
|
||||
|
||||
// 构建带access_token参数的URL
|
||||
String url = API_URL + "?access_token=" + accessToken;
|
||||
|
||||
try {
|
||||
// 尝试请求API
|
||||
return restTemplate.getForEntity(url, String.class);
|
||||
} catch (HttpClientErrorException.Unauthorized e) {
|
||||
log.warn("API请求未授权,尝试刷新访问令牌后重试");
|
||||
// 如果是401未授权错误,则刷新令牌后重试
|
||||
refreshAccessToken();
|
||||
url = API_URL + "?access_token=" + accessToken;
|
||||
return restTemplate.getForEntity(url, String.class);
|
||||
} catch (Exception e) {
|
||||
log.error("请求API时发生异常", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新访问令牌
|
||||
*/
|
||||
private void refreshAccessToken() {
|
||||
tokenLock.lock();
|
||||
try {
|
||||
// 再次检查令牌是否已经被其他线程更新
|
||||
if (!isTokenExpired()) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("开始刷新访问令牌");
|
||||
|
||||
// 构造获取令牌的参数
|
||||
String tokenUrl = baseUrl + ACCESS_TOKEN_URL + "?appKey=" + APP_KEY + "&" + APP_SECRET;
|
||||
|
||||
try {
|
||||
// 请求访问令牌
|
||||
ResponseEntity<?> response = restTemplate.getForEntity(tokenUrl, Map.class);
|
||||
|
||||
if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
|
||||
Map<String, Object> responseBody = Convert.toMap(String.class, Object.class, response.getBody());
|
||||
|
||||
// 检查响应是否成功
|
||||
Boolean success = (Boolean) responseBody.get("success");
|
||||
if (success != null && success) {
|
||||
Map<String, Object> content = Convert.toMap(String.class, Object.class, responseBody.get("content"));
|
||||
|
||||
// 更新访问令牌和过期时间
|
||||
accessToken = (String) content.get("access_token");
|
||||
Integer expiresIn = (Integer) content.get("expires_in");
|
||||
|
||||
// 设置过期时间(提前5分钟过期以确保安全)
|
||||
tokenExpireTime = System.currentTimeMillis() + (expiresIn - 300) * 1000L;
|
||||
|
||||
log.info("成功刷新访问令牌,新令牌将在 {} 过期", new java.util.Date(tokenExpireTime));
|
||||
} else {
|
||||
log.error("获取访问令牌失败: {}", responseBody);
|
||||
throw new RuntimeException("无法获取访问令牌: " + responseBody.get("msg"));
|
||||
}
|
||||
} else {
|
||||
log.error("获取访问令牌失败,HTTP状态码: {}", response.getStatusCode());
|
||||
throw new RuntimeException("无法获取访问令牌,HTTP状态码: " + response.getStatusCode());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("刷新访问令牌时发生异常", e);
|
||||
throw new RuntimeException("刷新访问令牌失败", e);
|
||||
}
|
||||
} finally {
|
||||
tokenLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查令牌是否已过期
|
||||
*
|
||||
* @return boolean 如果令牌已过期返回true,否则返回false
|
||||
*/
|
||||
private boolean isTokenExpired() {
|
||||
// 如果令牌为空或者已经过期
|
||||
return accessToken == null || System.currentTimeMillis() >= tokenExpireTime;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.jeelowcode.module.biz.service;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
/**
|
||||
* 业务HTTP客户端服务接口
|
||||
*
|
||||
* @author lingma
|
||||
*/
|
||||
public interface IBizHttpClientService {
|
||||
|
||||
/**
|
||||
* 获取API数据
|
||||
*
|
||||
* @return ResponseEntity<String> 响应结果
|
||||
*/
|
||||
ResponseEntity<String> getApiData();
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.jeelowcode.module.biz.service.impl;
|
||||
|
||||
import com.jeelowcode.module.biz.http.RetryableHttpClient;
|
||||
import com.jeelowcode.module.biz.service.IBizHttpClientService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
|
||||
/**
|
||||
* 业务HTTP客户端服务实现类
|
||||
*
|
||||
* @author lingma
|
||||
*/
|
||||
@Service
|
||||
public class BizHttpClientServiceImpl implements IBizHttpClientService {
|
||||
|
||||
@Resource
|
||||
private RetryableHttpClient retryableHttpClient;
|
||||
|
||||
@Override
|
||||
public ResponseEntity<String> getApiData() {
|
||||
return retryableHttpClient.getApiData();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user