文章通过为更新类接口添加特定的注解,并在每次更新类请求时增加幂等参数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);
    }
}

如果觉得不错,收藏起来呗

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐