前言:上篇文章Spring boot冪等性約束的實現(初級版)介紹了通過注解的方法進行冪等驗證,通過參數的序號數組指定哪些參數參與生成冪等的
key
,但如果方法增加或者減少了參數,忘記改注解上的參數序號,會導致key
與預想不一致;或者傳入的參數是個對象,我們使用其中的某幾個屬性作值為key
,這樣通過序號指定key
就不合適了。參照Spring
支持@cacheable
注解實現緩存,其中的“key
” 和“cacheNames
”支持SpEL
表達式,SpEL
提供了屬性值的動態生成及足夠的靈活性。
SpEL是什么
SpEL
(Spring Expression Language
),即Spring表達式語言,是比JSP
的EL
更強大的一種表達式語言。因為它可以在運行時查詢和操作數據,因此可以縮減代碼量,優化代碼結構。詳細用法參考這篇文章
核心思路
SpEL
表達式不僅支持調用方法,還支持調用對象里面的參數,這個正是我的需求,平時傳給annotation
的參數都是固定的,但是通過SpEL
表達式我們可以傳一個變量值,甚至是執行一個方法。
示例代碼:
@Test(id="#id",text="#userService.test()")
public void test(UserBase userBase, int id){
}
通過這樣的注解我是可以調用到userService
的test()
方法的返回值賦值到注解的text
參數。注意:test
方法一定要是public
的,否則無法訪問報異常。
分析@cacheable注解的SpEL
實現
通過查看@cacheable
注解的源代碼,在如下的類找到通過注解動態生成key
的代碼。
在類MethodBasedEvaluationContext
有實現SpEL
的代碼邏輯。我們的AOP
代碼參照其實現。
protected void lazyLoadArguments() {
if (!ObjectUtils.isEmpty(this.arguments)) {
String[] paramNames = this.parameterNameDiscoverer.getParameterNames(this.method);
int paramCount = paramNames != null ? paramNames.length : this.method.getParameterCount();
int argsCount = this.arguments.length;
for(int i = 0; i < paramCount; ++i) {
Object value = null;
if (argsCount > paramCount && i == paramCount - 1) {
value = Arrays.copyOfRange(this.arguments, i, argsCount);
} else if (argsCount > i) {
value = this.arguments[i];
}
this.setVariable("a" + i, value);
this.setVariable("p" + i, value);
if (paramNames != null && paramNames[i] != null) {
this.setVariable(paramNames[i], value);
}
}
}
}
引入依賴
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
代碼實現
- 創建一個自定義異常,如果冪等校驗不通過會跳過執行此接口的方法體業務代碼,拋出此異常。
package com.pay.common.exception;
/**
* @ClassName: IdempotentException
* @Description: 自定義冪等異常類
* @author: 郭秀志 jbcode@126.com
* @date: 2020/6/4 20:12
* @Copyright:
*/
public class IdempotentException extends RuntimeException {
private static final long serialVersionUID = 17721020985L;
public IdempotentException(String message) {
super(message);
}
@Override
public String getMessage() {
return super.getMessage();
}
}
- 生成key值工具類
package com.pay.common.util;
import com.alibaba.fastjson.JSON;
import java.lang.reflect.Method;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* @ClassName: IdempotentKeyUtil
* @Description: 冪等生成key值工具類
* @author: 郭秀志 jbcode@126.com
* @date: 2020/6/4 20:13
* @Copyright:
*/
public class IdempotentKeyUtil {
/**
* 對接口的參數進行處理生成固定key
*
* @param method
* @param custArgsIndex
* @param args
* @return
*/
public static String generate(Method method, int[] custArgsIndex, Object... args) {
String stringBuilder = getKeyOriginalString(method, custArgsIndex, args);
//進行md5等長加密
return md5(stringBuilder.toString());
}
/**
* 通過注解的spelKey設定key的生成方法。
*
* @param method
* @param spelKey
* @return
*/
public static String generate(Method method, String spelKey) {
StringBuilder stringBuilder = new StringBuilder(method.toString()).append(spelKey);
//進行md5等長加密
return md5(stringBuilder.toString());
}
/**
* 原生的key字符串。
*
* @param method
* @param custArgsIndex
* @param args
* @return
*/
public static String getKeyOriginalString(Method method, int[] custArgsIndex, Object[] args) {
StringBuilder stringBuilder = new StringBuilder(method.toString());
int i = 0;
for (Object arg : args) {
if (isIncludeArgIndex(custArgsIndex, i)) {
stringBuilder.append(toString(arg));
}
i++;
}
return stringBuilder.toString();
}
/**
* 判斷當前參數是否包含在注解中的自定義序列當中。
*
* @param custArgsIndex
* @param i
* @return
*/
private static boolean isIncludeArgIndex(int[] custArgsIndex, int i) {
//如果沒自定義作為key的參數index序號,直接返回true,意味加入到生成key的序列
if (custArgsIndex.length == 0) {
return true;
}
boolean includeIndex = false;
for (int argsIndex : custArgsIndex) {
if (argsIndex == i) {
includeIndex = true;
break;
}
}
return includeIndex;
}
/**
* 使用jsonObject對數據進行toString,(保持數據一致性)
*
* @param obj
* @return
*/
public static String toString(Object obj) {
if (obj == null) {
return "-";
}
return JSON.toJSONString(obj);
}
/**
* 對數據進行MD5等長加密
*
* @param str
* @return
*/
public static String md5(String str) {
StringBuilder stringBuilder = new StringBuilder();
try {
//選擇MD5作為加密方式
MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(str.getBytes());
byte b[] = mDigest.digest();
int j = 0;
for (int i = 0, max = b.length; i < max; i++) {
j = b[i];
if (j < 0) {
i += 256;
} else if (j < 16) {
stringBuilder.append(0);
}
stringBuilder.append(Integer.toHexString(j));
}
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return stringBuilder.toString();
}
}
- 自定義冪等注解,較上版本新增
spelKey()
。
package com.pay.common.annotation;
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 key();
/**
* 通過SpEL表達式來指定key值。
* 使用方法:@Idempotent(spelKey = "#user.name + #user.phone", key = "guoxiuzhiSuffix")
*
* @return
*/
String spelKey() default "";
//自定義的傳入參數序列作為key的后綴,默認的全部參數作為key的后綴拼接。參數定義示例:{0,1}
int[] custKeysByParameterIndexArr() default {};
//過期時間
long expirMillis() default 120;
}
-
AOP
對我們自定義注解進行攔截處理。新增了如下 核心代碼:
//SpEL解析器
ExpressionParser parser = new SpelExpressionParser();
LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
......
public Object around(ProceedingJoinPoint jPoint) throws Throwable {
......
//取得方法的所有參數。
String[] params = discoverer.getParameterNames(method);
EvaluationContext context = new StandardEvaluationContext();
for (int len = 0; len < params.length; len++) {
context.setVariable(params[len], args[len]);
}
//取出自定義的支持SpEL的key值。
String keySpel = idempotent.spelKey();
Expression keyExpression = parser.parseExpression(keySpel);
//生成Key
String key = "";
String keySpelValue = keyExpression.getValue(context, String.class);
完整的AOP
代碼
package com.pay.common.annotation.aop;
import com.pay.common.annotation.Idempotent;
import com.pay.common.exception.IdempotentException;
import com.pay.common.util.IdempotentKeyUtil;
import lombok.extern.slf4j.Slf4j;
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.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.data.redis.core.RedisTemplate;
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 org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
/**
* @ClassName: IdempotentAspect
* @Description: 自定義冪等aop切點
* @author: 郭秀志 jbcode@126.com
* @date: 2020/6/6 9:56
* @Copyright:
*/
@Component
@Slf4j
@Aspect
@ConditionalOnClass(RedisTemplate.class)
public class IdempotentAspect {
private static final String KEY_TEMPLATE = "idempotent_%S";
@Autowired
private RedisTemplate<String, String> redisTemplate;
//SpEL解析器
ExpressionParser parser = new SpelExpressionParser();
LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
/**
* 切點(自定義注解)
*/
@Pointcut("@annotation(com.pay.common.annotation.Idempotent)")
public void executeIdempotent() {
}
/**
* 切點業務
*
* @throws Throwable
*/
@Around("executeIdempotent()")
public Object around(ProceedingJoinPoint jPoint) throws Throwable {
//獲取當前方法信息
Method method = ((MethodSignature) jPoint.getSignature()).getMethod();
//獲取注解
Idempotent idempotent = method.getAnnotation(Idempotent.class);
//獲取參數的所有值。
Object[] args = jPoint.getArgs();
int[] custArgs = idempotent.custKeysByParameterIndexArr();
//取得方法的所有參數。
String[] params = discoverer.getParameterNames(method);
EvaluationContext context = new StandardEvaluationContext();
for (int len = 0; len < params.length; len++) {
context.setVariable(params[len], args[len]);
}
//取出自定義的支持SpEL的key值。
String keySpel = idempotent.spelKey();
Expression keyExpression = parser.parseExpression(keySpel);
//生成Key
String key = "";
String keySpelValue = keyExpression.getValue(context, String.class);
if (StringUtils.hasText(keySpelValue)) {//通過注解的spelKey設定key。
key = String.format(KEY_TEMPLATE, idempotent.key() + "_" + IdempotentKeyUtil.generate(method, keySpelValue));
} else {//默認使用注解custKeysByParameterIndexArr生成key。
key = String.format(KEY_TEMPLATE, idempotent.key() + "_" + IdempotentKeyUtil.generate(method, custArgs, args));
}
// https://segmentfault.com/a/1190000002870317 -- JedisCommands接口的分析
//nxxx的值只能取NX或者XX,如果取NX,則只有當key不存在是才進行set,如果取XX,則只有當key已經存在時才進行set
//expx expx的值只能取EX或者PX,代表數據過期時間的單位,EX代表秒,PX代表毫秒
// key value nxxx(set規則) expx(取值規則) time(過期時間)
// String redisRes = redisTemplate.execute((RedisCallback<String>) conn -> ((RedisAsyncCommands) conn).getStatefulConnection().sync().set(key, "NX", "EX", idempotent.expirMillis()));
// Jedis jedis = new Jedis("127.0.0.1",6379);
// jedis.auth("xuzz");
// jedis.select(0);
// String redisRes = jedis.set(key, key,"NX","EX",idempotent.expirMillis());
Boolean result = redisTemplate.opsForValue().setIfAbsent(key, "0", idempotent.expirMillis(), TimeUnit.SECONDS);
if (result) {
return jPoint.proceed();
} else {
log.info("數據冪等錯誤");
throw new IdempotentException("冪等校驗失敗。key值為:" + IdempotentKeyUtil.getKeyOriginalString(method, custArgs, args));
}
}
}
測試
- 簡單類型參數
(@RequestParam String name, @RequestParam int age)
使用冪等注解的spelKey
,把參數的值動態的賦值到注解spelKey
參數中。
@GetMapping("/go")
@Idempotent(key = "guoxiuzhi", spelKey = "#age+'_'+#name", expirMillis = 100)
public String go(@RequestParam String name, @RequestParam int age) {
return "IDEA class by guo xiuzhi ok when running";
}
- 對象參數
(@RequestBody @Validated BzPackageIndexModel name)
使用冪等注解的spelKey
,把name
對象的id、state
屬性值取出拼接。
@ApiVersion(5)
@RequestMapping(value = "/hibernate/validator")
// 加入接口的版本控制http://localhost:8555/v5/packageIndex//hibernate/validator?packageId=guoxiuzhi
@Idempotent(key = "guo", spelKey = "#name.id+'_'+#name.state")
public JsonResult paramsExceptionWithHibernateValidater(@RequestBody @Validated BzPackageIndexModel name) {
//BzSetupPayeraccountEntity entity = BzSetupPayeraccountEntity.builder().id("12345").payerAccountId("62260902").limitDay(BigDecimal.valueOf(11)).build();
return JsonResult.of("v5接口", true, "成功調用");
}
2.1 使用postman
測試,json
格式入參:{"name":3324,"state":20,"id":122332}
Debug
打印變量信息:結論
至此我們通過強大的SpEL
表達式調用對象的值,賦值給Annotation
的參數,使注解更靈活。