Implementing Automatic Idempotency in Spring Boot Using Redis and Custom Annotations
This tutorial explains how to achieve automatic API idempotency in Spring Boot by leveraging Redis for token storage, creating a custom @AutoIdempotent annotation, configuring an interceptor to validate tokens, and providing complete code examples and test cases to prevent duplicate operations.
In real development projects, an exposed API often receives many requests, so idempotency ensures that multiple executions have the same effect as a single execution, preventing duplicate data changes.
Common ways to guarantee idempotency include creating a unique database index, using a token mechanism, applying pessimistic or optimistic locks, and performing a check‑then‑insert pattern.
Redis can be used to implement automatic idempotency. First, set up a Redis server and inject Spring Boot's RedisTemplate for cache operations.
/**
* redis工具类
*/
@Component
public class RedisService {
@Autowired
private RedisTemplate redisTemplate;
/** 写入缓存 */
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;
}
/** 写入缓存并设置过期时间 */
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 */
public boolean exists(final String key) {
return redisTemplate.hasKey(key);
}
/** 读取缓存 */
public Object get(final String key) {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
return operations.get(key);
}
/** 删除对应的value */
public boolean remove(final String key) {
if (exists(key)) {
Boolean delete = redisTemplate.delete(key);
return delete;
}
return false;
}
}Define a custom annotation @AutoIdempotent to mark methods that require automatic idempotency.
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempotent {}Create a TokenService interface and its implementation to generate a UUID token, store it in Redis with an expiration time, and verify the token on subsequent requests.
public interface TokenService {
/** 创建 token */
String createToken();
/** 检验 token */
boolean checkToken(HttpServletRequest request) throws Exception;
}
@Service
public class TokenServiceImpl implements TokenService {
@Autowired
private RedisService redisService;
@Override
public String createToken() {
String uuid = RandomUtil.randomUUID();
String token = Constant.Redis.TOKEN_PREFIX + uuid;
redisService.setEx(token, token, 10000L);
return token;
}
@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 removed = redisService.remove(token);
if (!removed) {
throw new ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
}
return true;
}
}Configure a Spring MVC interceptor that detects the @AutoIdempotent annotation, invokes TokenService.checkToken, and returns a JSON error response when validation fails.
@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 hm = (HandlerMethod) handler;
Method method = hm.getMethod();
AutoIdempotent annotation = method.getAnnotation(AutoIdempotent.class);
if (annotation != null) {
try {
return tokenService.checkToken(request);
} catch (Exception ex) {
ResultVo failed = ResultVo.getFailedResult(101, ex.getMessage());
writeReturnJson(response, JSONUtil.toJsonStr(failed));
throw ex;
}
}
return true;
}
private void writeReturnJson(HttpServletResponse response, String json) throws Exception {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=utf-8");
PrintWriter writer = null;
try {
writer = response.getWriter();
writer.print(json);
} finally {
if (writer != null) writer.close();
}
}
// postHandle and afterCompletion omitted for brevity
}Add the interceptor to the Spring MVC configuration.
@Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter {
@Resource
private AutoIdempotentInterceptor autoIdempotentInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(autoIdempotentInterceptor);
super.addInterceptors(registry);
}
}Provide a test controller that exposes an endpoint to obtain a token and another endpoint annotated with @AutoIdempotent to demonstrate idempotent behavior; the first call succeeds, subsequent calls are rejected as duplicate operations.
@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 result = new ResultVo();
result.setCode(Constant.code_success);
result.setMessage(Constant.SUCCESS);
result.setData(token);
return JSONUtil.toJsonStr(result);
}
return StrUtil.EMPTY;
}
@AutoIdempotent
@PostMapping("/test/Idempotence")
public String testIdempotence() {
String businessResult = testService.testIdempotence();
if (StrUtil.isNotEmpty(businessResult)) {
ResultVo success = ResultVo.getSuccessResult(businessResult);
return JSONUtil.toJsonStr(success);
}
return StrUtil.EMPTY;
}
}The article concludes that combining Spring Boot, Redis, a custom annotation, and an interceptor offers an elegant and automatic solution for API idempotency, preventing duplicate data modifications, reducing unnecessary concurrency, and improving overall system scalability.
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.
Java Architect Essentials
Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.
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.
