feat(biz): 实现可重试的HTTP客户端服务

- 新增RetryableHttpClient,支持自动登录和token刷新
- 实现线程安全的令牌管理机制
- 添加RestTemplate配置和条件化注入
- 创建业务HTTP客户端服务接口及实现类
- 集成Hutool工具库进行数据转换处理
- 实现API请求失败后的重试逻辑
- 添加详细的日志记录和异常处理机制
This commit is contained in:
2025-12-02 10:36:02 +08:00
parent d90be48256
commit f5314c0ae1
4 changed files with 215 additions and 0 deletions

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

@@ -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();
}
}