Implementing API Idempotency with Spring Boot, Redis, and Custom Annotations
This article explains how to achieve reliable API idempotency in Spring Boot applications by using Redis for token storage, defining a custom @AutoIdempotent annotation, implementing token generation and validation services, configuring an interceptor, and providing test cases to demonstrate the workflow.
In real-world development, an exposed API may receive multiple requests, so ensuring idempotency—where repeated executions have the same effect as a single execution—is essential to prevent duplicate database operations.
The article outlines several common idempotency strategies, such as unique database indexes, token mechanisms, pessimistic/optimistic locks, and pre‑check queries, and then focuses on a Redis‑based solution.
1. Building a Redis Service
/** * redis工具类 */
@Component
public class RedisService {
@Autowired
private RedisTemplate redisTemplate;
/** * 写入缓存 * @param key * @param value * @return */
public boolean set(final String key, Object value) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/** * 写入缓存设置时效时间 * @param key * @param value * @return */
public boolean setEx(final String key, Object value, Long expireTime) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/** * 判断缓存中是否有对应的value * @param key * @return */
public boolean exists(final String key) {
return redisTemplate.hasKey(key);
}
/** * 读取缓存 * @param key * @return */
public Object get(final String key) {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
return operations.get(key);
}
/** * 删除对应的value * @param key */
public boolean remove(final String key) {
if (exists(key)) {
Boolean delete = redisTemplate.delete(key);
return delete;
}
return false;
}
}2. Defining a Custom Annotation
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempotent {
}3. Token Service Interface
public interface TokenService {
/** * 创建token */
public String createToken();
/** * 检验token */
public boolean checkToken(HttpServletRequest request) throws Exception;
}4. Token Service Implementation
@Service
public class TokenServiceImpl implements TokenService {
@Autowired
private RedisService redisService;
@Override
public String createToken() {
String str = RandomUtil.randomUUID();
StrBuilder token = new StrBuilder();
try {
token.append(Constant.Redis.TOKEN_PREFIX).append(str);
redisService.setEx(token.toString(), token.toString(), 10000L);
if (StrUtil.isNotEmpty(token.toString())) {
return token.toString();
}
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
@Override
public boolean checkToken(HttpServletRequest request) throws Exception {
String token = request.getHeader(Constant.TOKEN_NAME);
if (StrUtil.isBlank(token)) {
token = request.getParameter(Constant.TOKEN_NAME);
if (StrUtil.isBlank(token)) {
throw new ServiceException(Constant.ResponseCode.ILLEGAL_ARGUMENT, 100);
}
}
if (!redisService.exists(token)) {
throw new ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
}
boolean remove = redisService.remove(token);
if (!remove) {
throw new ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
}
return true;
}
}5. Web Configuration to Register the Interceptor
@Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter {
@Resource
private AutoIdempotentInterceptor autoIdempotentInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(autoIdempotentInterceptor);
super.addInterceptors(registry);
}
}6. Interceptor Implementation
@Component
public class AutoIdempotentInterceptor implements HandlerInterceptor {
@Autowired
private TokenService tokenService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);
if (methodAnnotation != null) {
try {
return tokenService.checkToken(request);
} catch (Exception ex) {
ResultVo failedResult = ResultVo.getFailedResult(101, ex.getMessage());
writeReturnJson(response, JSONUtil.toJsonStr(failedResult));
throw ex;
}
}
return true;
}
// postHandle and afterCompletion omitted for brevity
private void writeReturnJson(HttpServletResponse response, String json) throws Exception {
PrintWriter writer = null;
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=utf-8");
try {
writer = response.getWriter();
writer.print(json);
} catch (IOException e) {
} finally {
if (writer != null) writer.close();
}
}
}7. Test Controller Demonstrating the Flow
@RestController
public class BusinessController {
@Resource
private TokenService tokenService;
@Resource
private TestService testService;
@PostMapping("/get/token")
public String getToken(){
String token = tokenService.createToken();
if (StrUtil.isNotEmpty(token)) {
ResultVo resultVo = new ResultVo();
resultVo.setCode(Constant.code_success);
resultVo.setMessage(Constant.SUCCESS);
resultVo.setData(token);
return JSONUtil.toJsonStr(resultVo);
}
return StrUtil.EMPTY;
}
@AutoIdempotent
@PostMapping("/test/Idempotence")
public String testIdempotence() {
String businessResult = testService.testIdempotence();
if (StrUtil.isNotEmpty(businessResult)) {
ResultVo successResult = ResultVo.getSuccessResult(businessResult);
return JSONUtil.toJsonStr(successResult);
}
return StrUtil.EMPTY;
}
}The article concludes that using Spring Boot, Redis, and a custom annotation provides an elegant, automated way to guarantee API idempotency, preventing duplicate data modifications and reducing unnecessary load on backend services.
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.
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
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.
