Java 幂等 实现
基于 redis 实现API操作幂等,为更新类接口添加特定的注解,增加幂等参数ClientToken,在基于ClientToken唯一的情况下先检查redis里是否有响应的结果,否则去请求service层再将结果放入redis,来达到请求幂等的效果。
文章通过为更新类接口添加特定的注解,并在每次更新类请求时增加幂等参数ClientToken,在基于ClientToken唯一的情况下先检查redis里是否有响应的结果,否则去请求service层再将结果放入redis,来达到请求幂等的效果。
幂等设计规范
幂等API的设计应确保同一个 ClientToken多次调用的返回结果一致,应确保
【强制】每次请求的ClientToken不能相同,所有写类型的API应支持幂等。
【强制】HTTP状态码为200(响应成功)时,重试请求应得到上次相同的结果。
【建议】ClientToken :数据类型String, 大小写敏感,长度64,格式: [0-9a-zA-Z-]{1,64},建议使用UUID。服务端保留时间,建议1个月,一个月内保证幂等。
查看完整代码直接拉到底部
首先,示例demo
//接口
public interface StudentService {
Result instertStudent(AuthContext context,StudentDTO studentDTO);
Result<StudentDTO> queryStudent(AuthContext context,String name);
}
//接口实现
@Service
public class StudentServiceImpl implements StudentService{
@Override
public Result instertStudent(AuthContext context, StudentDTO studentDTO) {
//插入数据逻辑
return Result.buildSuccessResult(context.getRequestId());
}
@Override
public Result<StudentDTO> queryStudent(AuthContext context, String name) {
//查询逻辑
StudentDTO studentDTO = new StudentDTO();
return Result.buildSuccessResult(studentDTO, context.getRequestId());
}
}
//请求权限相关
@Data
public class AuthContext {
private String requestId;
//todo 加入身份或权限校验相关参数
}
//请求数据
@Data
public class StudentDTO {
private String name;
private String address;
private Integer age;
}
//响应的结果
@Data
public class Result<T extends Serializable> implements Serializable {
private Boolean success;
private String message;
private String requestId;
private T obj;
public Result() {
}
public Result(Boolean success, String requestId) {
this.success = success;
this.requestId = requestId;
}
public Result(Boolean success, String message, String requestId) {
this.success = success;
this.message = message;
this.requestId = requestId;
}
public Result(Boolean success, String requestId, T obj) {
this.success = success;
this.requestId = requestId;
this.obj = obj;
}
public static <T extends Serializable> Result<T> buildSuccessResult(String requestId) {
return new Result<T>(true, requestId);
}
public static <T extends Serializable> Result<T> buildSuccessResult(T obj, String requestId) {
return new Result<T>(true, requestId, obj);
}
public static <T extends Serializable> Result<T> buildFailedResult(String errMsg, String requestId) {
return new Result<T>(false, errMsg, requestId);
}
}
第一步
增加代表每次请求标示的属性ClientToken
@Data
public class StudentDTO {
private String name;
private String address;
private Integer age;
private String clientToken;
}
第二步自定义一个拦截器
其中自定义一个幂等注解@Idempotent
spelKey代表幂等token的来源,expireDate为本次请求的结果将被存储多久
另外定义拦截器,拦截service层标注有@Idempotent注解的请求,
首先解析出幂等的clientToken值,再以此为redis存储的key,来完成幂等的实现
@Aspect
@Component
public class AuthCheckerAspect {
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Idempotent {
/**
* 幂等key定义
*/
String spelKey() default "";
/**
* 存储幂等有效期,时间单位天
*/
int expireDate() default 1;
}
@Autowired
private RedisTemplate redisTemplate;
@Order(1)
@Around("execution(public * com.demo.service.*.*(..)) && @annotation(idempotent)")
public Object providerIdempotent(ProceedingJoinPoint pjp, Idempotent idempotent) throws Throwable {
Object[] args = pjp.getArgs();
AuthContext contexts = (AuthContext) args[0];
//获取幂等txId
String clientToken = ExpressionEvaluator.eval(idempotent.spelKey(), ((MethodSignature) pjp.getSignature()).getMethod(), pjp.getArgs(), String.class);
if (StringUtils.isBlank(clientToken)) {
return Result.buildFailedResult("The clientToken is mandatory for this action.", contexts.getRequestId());
}
//todo 根据业务需要加锁
if (redisTemplate.hasKey(clientToken)) {
return redisTemplate.opsForValue().get(clientToken);
} else {
Object proceed = pjp.proceed(args);
redisTemplate.opsForValue().set(clientToken, proceed, idempotent.expireDate(), TimeUnit.DAYS);
return proceed;
}
}
}
//通用解析注解
public class ExpressionEvaluator {
private static ExpressionParser parser = new SpelExpressionParser();
private static LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
public ExpressionEvaluator() {
}
public static <T> T eval(String spelExpression, Method targetMethod, Object[] args, Class<T> tClass) {
if(StringUtils.isBlank(spelExpression)){
return null;
}
String[] params = discoverer.getParameterNames(targetMethod);
EvaluationContext context = new StandardEvaluationContext();
for(int len = 0; len < params.length; ++len) {
context.setVariable(params[len], args[len]);
}
Expression expression = parser.parseExpression(spelExpression);
return expression.getValue(context, tClass);
}
}
第三步,在实现上加上注解
@Override
@AuthCheckerAspect.Idempotent(spelKey = "#studentDTO.clientToken", expireDate = 7)
public Result instertStudent(AuthContext context, StudentDTO studentDTO) {
//插入数据逻辑
return Result.buildSuccessResult(context.getRequestId());
}
综上所
完整代码
service层
public interface StudentService {
Result instertStudent(AuthContext context,StudentDTO studentDTO);
Result<StudentDTO> queryStudent(AuthContext context,String name);
}
=====================================================================
import org.springframework.stereotype.Service;
@Service
public class StudentServiceImpl implements StudentService{
@Override
@AuthCheckerAspect.Idempotent(spelKey = "#studentDTO.clientToken", expireDate = 7)
public Result instertStudent(AuthContext context, StudentDTO studentDTO) {
//插入数据逻辑
return Result.buildSuccessResult(context.getRequestId());
}
@Override
public Result<StudentDTO> queryStudent(AuthContext context, String name) {
//查询逻辑
StudentDTO studentDTO = new StudentDTO();
return Result.buildSuccessResult(studentDTO, context.getRequestId());
}
}
实体类
import lombok.Data;
@Data
public class AuthContext {
private String requestId;
//todo 加入身份或权限校验相关参数
}
============================================================
import lombok.Data;
import java.io.Serializable;
@Data
public class StudentDTO implements Serializable {
private String name;
private String address;
private Integer age;
private String clientToken;
}
===========================================================
import lombok.Data;
import java.io.Serializable;
//返回结果
@Data
public class Result<T extends Serializable> implements Serializable {
private Boolean success;
private String message;
private String requestId;
private T obj;
public Result() {
}
public Result(Boolean success, String requestId) {
this.success = success;
this.requestId = requestId;
}
public Result(Boolean success, String message, String requestId) {
this.success = success;
this.message = message;
this.requestId = requestId;
}
public Result(Boolean success, String requestId, T obj) {
this.success = success;
this.requestId = requestId;
this.obj = obj;
}
public static <T extends Serializable> Result<T> buildSuccessResult(String requestId) {
return new Result<T>(true, requestId);
}
public static <T extends Serializable> Result<T> buildSuccessResult(T obj, String requestId) {
return new Result<T>(true, requestId, obj);
}
public static <T extends Serializable> Result<T> buildFailedResult(String errMsg, String requestId) {
return new Result<T>(false, errMsg, requestId);
}
}
拦截器
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
@Aspect
@Component
public class AuthCheckerAspect {
//自定义幂等注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Idempotent {
/**
* 幂等key定义
*/
String spelKey() default "";
/**
* 存储幂等有效期,时间单位天
*/
int expireDate() default 1;
}
@Autowired
private RedisTemplate redisTemplate;
//幂等拦截器
@Order(1)
@Around("execution(public * com.demo.service.*.*(..)) && @annotation(idempotent)")
public Object providerIdempotent(ProceedingJoinPoint pjp, Idempotent idempotent) throws Throwable {
Object[] args = pjp.getArgs();
AuthContext contexts = (AuthContext) args[0];
//获取幂等txId
String clientToken = ExpressionEvaluator.eval(idempotent.spelKey(), ((MethodSignature) pjp.getSignature()).getMethod(), pjp.getArgs(), String.class);
if (StringUtils.isBlank(clientToken)) {
return Result.buildFailedResult("The clientToken is mandatory for this action.", contexts.getRequestId());
}
//todo 根据业务需要加锁
if (redisTemplate.hasKey(clientToken)) {
return redisTemplate.opsForValue().get(clientToken);
} else {
Object proceed = pjp.proceed(args);
redisTemplate.opsForValue().set(clientToken, proceed, idempotent.expireDate(), TimeUnit.DAYS);
return proceed;
}
}
}
=================================================================
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.lang.reflect.Method;
//解析注解属性值
public class ExpressionEvaluator {
private static ExpressionParser parser = new SpelExpressionParser();
private static LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
public ExpressionEvaluator() {
}
public static <T> T eval(String spelExpression, Method targetMethod, Object[] args, Class<T> tClass) {
if(StringUtils.isBlank(spelExpression)){
return null;
}
String[] params = discoverer.getParameterNames(targetMethod);
EvaluationContext context = new StandardEvaluationContext();
for(int len = 0; len < params.length; ++len) {
context.setVariable(params[len], args[len]);
}
Expression expression = parser.parseExpression(spelExpression);
return expression.getValue(context, tClass);
}
}
如果觉得不错,收藏起来呗
更多推荐
所有评论(0)