使用Redission和Aop以及注解实现接口幂等性

关于什么是接口幂等性这里不再赘述,本文将使用分布式锁来解决接口幂等性的问题。


本文接口幂等判断标准:
String name = IP + 请求方式 + URI + 参数摘要值
当相同的name来临时,且上一个相同name对于的接口还未正常执行完毕,则判断为重复调用接口。

举例:
IP为10.10.10.10的客户端以Get请求方式调用/test接口,参数为a=1&b=2,当第一次调用接口后,服务端还没有处理完成,客户端又重复发起相同的请求,则会被服务端认作重复提交。


本篇文章所使用的相关技术或框架:

技术或框架使用原因
Spring boot Aop对被调用的接口方法进行增强
Redission存储以及管理锁,主要是使用里面的看门狗机制
自定义注解用于标注哪些接口方法需要实现接口幂等性
反射在Aop中获取被调用method上面的注解,根据有没有自定义注解来判断该方法是否开启接口幂等性
Servlet获取客户端的IP、请求方式、请求URI、参数列表
Jackson将接口参数从Object转为Json格式的字符串
MD5根据获得的Json字符串,计算其摘要值

前置需求
导入依赖

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.16.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
			
		<!--用于将byte数组转为16进制字符串-->
        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.14</version>
        </dependency>

编写Redisson配置类

@Configuration
public class RedissionConfig {
    @Bean
    public RedissonClient redisson(){
        // 单机模式
        Config config = new Config();
        config.useSingleServer()
                .setAddress("redis://xxx:6379")
                .setPassword("xxx")
                .setDatabase(0);
        return Redisson.create(config);
    }
}

具体实现思路

编写自定义注解,写在被调用的method上面,用于标注是否需要开启接口幂等性

import java.lang.annotation.*;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestLimit {
    boolean enable() default true;
}	

在这里插入图片描述

不写该注解,或者写成@RequestLimit(enable = false). ,则对于该方法不会开启接口幂等性

编写Aop,对Controller Method进行增强

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zq.test.annotations.RequestLimit;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Hex;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@Aspect
@Component
@Slf4j
public class RequestAop {
	//当同一用户、相等请求方法、相同URI、相同参数重复请求时,返回值为这个,自己改下
    private final Object resultObject=new String("系统繁忙");

    @Autowired
    private RedissonClient redissonClient;
	
	//切面路径,自己改
    @Pointcut(value = "execution(* com.zq.test.controller..*.*(..))")
    public void pointCut(){}

    @Around(value = "pointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        //获取被调用的方法签名
        MethodSignature methodSignature= (MethodSignature) point.getSignature();
        //获取被调用的方法
        Method method=methodSignature.getMethod();
        //获取该方法上面的@RequestLimit注解
        RequestLimit requestLimit = method.getAnnotation(RequestLimit.class);
        Object result=null;
        RLock lock=null;
        //如果该方法上面有这个注解,并且没有主动关闭接口幂等性
        if(requestLimit!=null&&requestLimit.enable()){
            //获取当前进程的ServletRequest对象
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            //获取请求参数名
            String[] params = methodSignature.getParameterNames();
            //获取请求参数值
            Object[] args = point.getArgs();
            //将参数名和参数值转成Map,且一一对应
            Map<String, Object> maps = new HashMap<>();
            for(int i=0; i<params.length; i++){
                maps.put(params[i], args[i]);
            }
            //根据 客户端IP + http请求方式 + 请求的URI + 参数的摘要值 来计算锁名
            String lockName=getIpAddr(request)+'|'+request.getMethod()+'|'+request.getRequestURI()+'|'+md5Encrypt(maps);
    
            lock = redissonClient.getLock(lockName);
            //从redis中尝试再20ms内获取该锁,若是超过20ms也没获取到,则放弃,并返回false,如果获取到,则返回true,并自动上锁
            if(!lock.tryLock(20, TimeUnit.MILLISECONDS)){
                log.info("系统繁忙");
                //没有获取到,表名接口被重复调用
                return resultObject;
            }
        }
        //获取到锁
        try {
            log.info("------------------------执行开始------------------------");
            result=point.proceed();
        } finally {
            //如果获取到锁,且处于上锁状态,已经执行完毕,释放锁
            if(lock!=null&&lock.isLocked())
                lock.unlock();
            log.info("------------------------执行结束------------------------");
        }
        return result;
    }

    //将Map转为Json字符串,再根据Json字符串计算摘要值,并返回,用于判断是不是相同参数的请求重复提交了
    public String md5Encrypt(Map<String, Object> maps) throws JsonProcessingException, NoSuchAlgorithmException {
        ObjectMapper objectMapper = new ObjectMapper();
        String json = null;
        json = objectMapper.writeValueAsString(maps);
        MessageDigest md5 = MessageDigest.getInstance("md5");
        md5.update(json.getBytes(StandardCharsets.UTF_8));
        byte[] bytes = md5.digest();
        md5.reset();
        //bytes数组转16进制字符串
        return Hex.encodeHexString(bytes);
    }
		
	//获取客户端IP,实际上这里可以直接用UserId替代
    public String getIpAddr(HttpServletRequest request) {
        String ipAddress = request.getHeader("X-Forwarded-For");
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getRemoteAddr();
            if (ipAddress.equals("127.0.0.1") || ipAddress.equals("0:0:0:0:0:0:0:1")) {
                // 根据网卡取本机配置的IP
                try {
                    InetAddress inet = InetAddress.getLocalHost();
                    ipAddress = inet.getHostAddress();
                } catch (UnknownHostException e) {
                    log.error("根据网卡获取本机配置的IP异常", e);
                }

            }
        }

        // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
        if (ipAddress != null && ipAddress.indexOf(",") > 0) {
            ipAddress = ipAddress.split(",")[0];
        }

        return ipAddress;
    }

}

以上代码还有可以继续改进的地方,例如将IP换成UserIDlockName可以继续计算其摘要值,缩短lockName的长度

上述代码在实际使用中,需要修改的地方如下:
在这里插入图片描述

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值