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