Spring Boot通過Redis和自定義註解實現api冪等處理

1、簡述

爲了實現同一時間段,同樣的請求(請求url和請求參數相同),只能成功一次,其餘被拒絕,需實現冪等處理。

本示例實現原理:

通過自定義註解並配合切點切面實現請求驗證功能。其中驗證功能通過redis實現,當第一次獲取請求時,根據請求url、controller層調用方法、請求參數生成MD5值,將其作爲redis的key和value值,並設置失效時間,當在失效時間之內再次請求時,根據是否已存在key值來判斷接收還是拒絕請求來實現攔截。

2、pom.xml依賴

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
	<exclusions>
		<exclusion>
			<groupId>io.lettuce</groupId>
			<artifactId>lettuce-core</artifactId>
		</exclusion>
	</exclusions>
	<version>2.0.6.RELEASE</version>
</dependency>
<dependency>
	<groupId>org.aspectj</groupId>
	<artifactId>aspectjweaver</artifactId>
	<version>1.8.9</version>
</dependency>
<dependency>
	<groupId>redis.clients</groupId>
	<artifactId>jedis</artifactId>
	<version>2.9.1</version>
</dependency>
<dependency>
	<groupId>org.apache.commons</groupId>
	<artifactId>commons-pool2</artifactId>
	<version>2.6.0</version>
</dependency>
<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>fastjson</artifactId>
	<version>1.2.31</version>
</dependency>

注:在spring-boot-starter-data-redis中排除lettuce-core,並添加jedis依賴是因爲SpringBoot2.0中默認使用Lettuce來集成Redis服務,Spring-boot-starter-data-redis默認只引入了Lettuce包,並沒有引入jedis包(redis操作需使用其中的類)支持。另外commons-pool2、fastjson(json格式化類)和aspectjweaver(增加切面支持)依賴也需添加。

3、Redis配置

在application.yml中添加(當然在application.properties添加也可以,按規範即可):

spring:

  redis:

    database: 0

    host: XXX

    port: 6379

    password: XXX

    timeout: 1000

    pool:

      max-active: 1

      max-wait: 10

      max-idle: 2

      min-idle: 50

4、冪等處理代碼

(1)自定義異常類IdempotentException

package com.demo.controller;
public class IdempotentException extends RuntimeException {
	public IdempotentException(String message) {
		super(message);
	}
	
	@Override
	public String getMessage() {
		return super.getMessage();
	}
}

(2)自定義註解

package com.demo.controller;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Idempotent {
	// redis的key值一部分
	String value();
	// 過期時間
	long expireMillis();
}

(3)自定義切面

package com.demo.controller;

import java.lang.reflect.Method;
import java.util.Objects;

import javax.annotation.Resource;

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.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import redis.clients.jedis.JedisCommands;

@Component
@Aspect
@ConditionalOnClass(RedisTemplate.class)
public class IdempotentAspect {
	private static final String KEY_TEMPLATE = "idempotent_%s";

	@Resource
	private RedisTemplate<String, String> redisTemplate;

	// @annotation中包路徑根據實際填寫
	@Pointcut("@annotation(com.demo.controller.Idempotent)")
	public void executeIdempotent() {

	}

	@Around("executeIdempotent()")
	public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
		Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
		Idempotent idempotent = method.getAnnotation(Idempotent.class);
		String key = String.format(KEY_TEMPLATE,
				idempotent.value() + "_" + KeyUtil.generate(method, joinPoint.getArgs()));
		String redisRes = redisTemplate
				.execute((RedisCallback<String>) conn -> ((JedisCommands) conn.getNativeConnection()).set(key, key,
						"NX", "PX", idempotent.expireMillis()));
		if (Objects.equals("OK", redisRes)) {
			return joinPoint.proceed();
		} else {
			throw new IdempotentException("Idempotent hits, key=" + key);
		}
	}
}

(4)Key生成工具類

package com.demo.controller;

import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import com.alibaba.fastjson.JSON;

public class KeyUtil {
	public static String generate(Method method, Object... args) throws UnsupportedEncodingException {
		StringBuilder sb = new StringBuilder(method.toString());
		for (Object arg : args) {
			sb.append(toString(arg));
		}
		return md5(sb.toString());
	}

	private static String toString(Object object) {
		if (object == null) {
			return "null";
		}
		if (object instanceof Number) {
			return object.toString();
		}
		return JSON.toJSONString(object);
	}

	public static String md5(String str) {
		StringBuilder buf = new StringBuilder();
		try {
			MessageDigest md = MessageDigest.getInstance("MD5");
			md.update(str.getBytes());
			byte b[] = md.digest();
			int i;
			for (int offset = 0; offset < b.length; offset++) {
				i = b[offset];
				if (i < 0)
					i += 256;
				if (i < 16)
					buf.append(0);
				buf.append(Integer.toHexString(i));
			}
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
		return buf.toString();
	}

(5)在Controller中標記註解

package com.demo.controller;

import javax.annotation.Resource;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoController {
	@Resource
	private RedisTemplate<String, String> redisTemplate;
	
	@PostMapping("/redis")
	@Idempotent(value = "/redis", expireMillis = 5000L)
	public String redis(@RequestBody User user) {
		return "redis access ok:" + user.getUserName() + " " + user.getUserAge();
	}
}

(6)運行調用

運行SpringBoot,然後通過接口調用工具連續兩次調用,顯示如下:

 

參考文章: https://blog.csdn.net/hanchao5272/article/details/92073405https://blog.csdn.net/thewaiting/article/details/89679132

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章