Comprehensive Guide to Building a SpringBoot Backend with HttpClient, Token Management, Redis Caching, and Rate Limiting
This article provides a step‑by‑step tutorial on creating a SpringBoot demo that integrates Apache HttpClient for GET/POST requests, handles token acquisition and caching with Redis, optimizes configuration via application.yml, and implements distributed locking and Guava‑based rate limiting for high‑concurrency scenarios.
Requirement analysis: the platform needs to interface with another system, ensuring stability and security.
Implementation steps start with initializing a SpringBoot demo (initialization details omitted) and adding necessary dependencies such as commons-httpclient, gson, and lombok:
<!-- https://mvnrepository.com/artifact/commons-httpclient/commons-httpclient -->
<dependency>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
<version>3.1</version>
</dependency>
<!-- use google's json conversion tool -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>Utility class HttpClientUtils provides static methods sendGet and sendPost that create an HttpClient, configure timeouts, add Bearer token headers, execute the request, and read the response stream into a String. Both methods release the connection in a finally block.
public static String sendGet(String urlParam, String token) {
// 1. create httpClient instance
HttpClient httpClient = new HttpClient();
httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(15000);
// 2. create GetMethod instance
GetMethod getMethod = new GetMethod(urlParam);
// 3. set timeout and headers
getMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT, 60000);
getMethod.addRequestHeader("Content-Type", "application/json");
if (!StringUtils.isEmpty(token)) {
Header header = new Header("Authorization", "Bearer " + token);
getMethod.addRequestHeader(header);
}
try {
httpClient.executeMethod(getMethod);
InputStream is = getMethod.getResponseBodyAsStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
StringBuffer res = new StringBuffer();
String str = "";
while ((str = br.readLine()) != null) {
res.append(str);
}
return res.toString();
} catch (IOException e) {
e.printStackTrace();
} finally {
getMethod.releaseConnection();
}
return null;
}The main method demonstrates obtaining a token via a POST request, parsing the JSON response with Gson, extracting the token, and then performing a GET request using the token.
// 1. call token interface
String baseUrl = "http://*****/";
String url = baseUrl + "/****/token";
Map<String, Object> jsonMap = new HashMap<>();
jsonMap.put("username", "***");
jsonMap.put("password", "***");
String res = sendPost(url, jsonMap, null);
log.info("获得的请求结果:{}", res);
// 2. parse JSON to get token
Gson gson = new Gson();
Type mapType = new TypeToken<HashMap<String, Object>>(){}.getType();
HashMap<String, Object> resMap = gson.fromJson(res, mapType);
LinkedTreeMap data = (LinkedTreeMap) resMap.get("data");
String token = (String) data.get("token");
log.info("token为:{}", token);
// 3. simulate GET request
url = baseUrl + "******";
log.info("获得的请求结果:{}", sendGet(url, token));To expose the functionality to front‑end callers, a unified response wrapper Result<T> is defined with fields code, msg, and data, plus fluent ok and error methods.
public class Result<T> implements Serializable {
private static final long serialVersionUID = 1L;
private int code = 0; // 0 = success
private String msg = "success";
private T data;
public Result<T> ok(T data) { this.setData(data); return this; }
public Result<T> error(String msg) { this.code = 500; this.msg = msg; return this; }
}An ApiController (annotated with @RestController) defines endpoints for token‑protected GET and POST operations, using static fields for base URL, credentials, and token. A static block fetches the token at startup.
@RestController
@RequestMapping("/identity/")
@Slf4j
public class ApiController {
public static String TOKEN = "";
public static final String BASE_URL = "http://*****";
public static final String USERNAME = "****";
public static final String PASSWORD = "****";
static {
String url = BASE_URL + "/identity/token";
Map<String, Object> jsonMap = new HashMap<>();
jsonMap.put("username", USERNAME);
jsonMap.put("password", PASSWORD);
String res = sendPost(url, jsonMap, null);
Gson gson = new Gson();
Type mapType = new TypeToken<HashMap<String, Object>>(){}.getType();
HashMap<String, Object> resMap = gson.fromJson(res, mapType);
LinkedTreeMap data = (LinkedTreeMap) resMap.get("data");
TOKEN = (String) data.get("token");
log.info("token获取成功:{}", TOKEN);
}
@GetMapping("/getDetail_get")
public Result<String> getDataGet(@RequestParam String handle) {
log.info("开始发起Get请求, token为:{}", TOKEN);
Assert.notNull(handle);
String url = BASE_URL + "/****" + handle;
try {
String res = sendGet(url, TOKEN);
return new Result<String>().ok(res);
} catch (Exception e) {
e.printStackTrace();
return new Result<String>().error("请求失败!");
}
}
@PostMapping("/getDetail_post")
public Result<String> getDataPost(@RequestBody HashMap<String, Object> requestBody) {
String url = BASE_URL + "/****";
try {
String res = sendPost(url, requestBody, TOKEN);
return new Result<String>().ok(res);
} catch (Exception e) {
e.printStackTrace();
return new Result<String>().error("请求失败!");
}
}
}Further optimizations include moving configuration values to application.yml and binding them with a @ConfigurationProperties class ApiConfig, injecting the config into the controller, and using a @PostConstruct method to initialize static fields.
api:
baseUrl: http://*****
username: ****
password: ****Token caching is enhanced with Redis: a RedisTemplate stores the token for six hours, and a read‑write lock (Redisson) prevents cache stampede when the token expires.
@Autowired
RedisTemplate<String, String> redisTemplate;
@Autowired
RedissonClient redisson;
private String getToken() {
// read lock
RReadWriteLock lock = redisson.getReadWriteLock("token-lock");
RLock rLock = lock.readLock();
String token = "";
try {
rLock.lock();
token = redisTemplate.opsForValue().get("token");
} finally { rLock.unlock(); }
if (!StringUtils.isEmpty(token)) return token;
// write lock to refresh token
RLock wLock = lock.writeLock();
try {
wLock.lock();
// fetch token from remote service, then cache
// ... (same logic as before) ...
redisTemplate.opsForValue().set("token", token, 6, TimeUnit.HOURS);
} finally { wLock.unlock(); }
return token;
}To protect high‑traffic endpoints, a Guava RateLimiter based token‑bucket algorithm is introduced via a custom annotation @Limit and an AOP aspect LimitAop. The aspect creates a per‑key RateLimiter, attempts to acquire a permit within a configurable timeout, and returns a JSON error response if the limit is exceeded.
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface Limit {
String key() default "";
double permitsPerSecond();
long timeout();
TimeUnit timeunit() default TimeUnit.MILLISECONDS;
String msg() default "系统繁忙,请稍后再试.";
} @Aspect
@Component
public class LimitAop {
private final Map<String, RateLimiter> limitMap = Maps.newConcurrentMap();
@Around("@annotation(com.example.demo.Limit)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
Limit limit = method.getAnnotation(Limit.class);
if (limit != null) {
String key = limit.key();
limitMap.computeIfAbsent(key, k -> RateLimiter.create(limit.permitsPerSecond()));
RateLimiter rl = limitMap.get(key);
if (!rl.tryAcquire(limit.timeout(), limit.timeunit())) {
responseFail(limit.msg());
return null;
}
}
return joinPoint.proceed();
}
private void responseFail(String msg) throws IOException {
HttpServletResponse resp = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse();
resp.setCharacterEncoding("utf-8");
resp.setContentType("application/json; charset=utf-8");
PrintWriter writer = resp.getWriter();
writer.write(new Result<String>().error(msg).toString());
}
}Applying @Limit to the GET endpoint limits calls to one request per second with a 1‑second wait timeout, returning a custom message when the limit is hit.
@GetMapping("/getDetail_get")
@Limit(key = "limit1", permitsPerSecond = 1, timeout = 1000, timeunit = TimeUnit.MILLISECONDS, msg = "当前排队人数较多,请稍后再试!")
public Result<String> getDataGet(@RequestParam String handle) { /* ... */ }The article concludes with performance screenshots showing the rate‑limiting behavior under repeated requests.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Selected Java Interview Questions
A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
