diff --git a/lock4j-core/src/main/java/com/baomidou/lock/AbortLockFailureStrategy.java b/lock4j-core/src/main/java/com/baomidou/lock/AbortLockFailureStrategy.java new file mode 100644 index 0000000000000000000000000000000000000000..993cbe86c1415f0a0a7abc24a75cbc0ddb607b5a --- /dev/null +++ b/lock4j-core/src/main/java/com/baomidou/lock/AbortLockFailureStrategy.java @@ -0,0 +1,209 @@ +package com.baomidou.lock; + +import com.baomidou.lock.exception.LockFailureException; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.util.*; + +import java.lang.annotation.*; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; + +/** + *

当加锁失败时,抛出异常以终止方法执行。
+ * 该策略可以通过{@link Options}注解来指定当失败时要抛出的异常和异常信息。 + * + * @author huangchengxing + * @see Options + */ +@RequiredArgsConstructor +public class AbortLockFailureStrategy implements LockFailureStrategy { + + /** + * 在SpEL表达式中可通过该参数名引用获取失败的锁键值 + */ + private static final String KEY_NAME = "key"; + + /** + * 默认的错误信息,与{@link DefaultLockFailureStrategy}保持一致 + */ + public static final String DEFAULT_EXCEPTION_MESSAGE = "request failed, please retry it."; + + /** + * 默认的异常处理器,即直接抛出{@link LockFailureException}异常 + */ + private final FailureHandler defaultExceptionFactory = new FailureHandler(); + + /** + * 异常处理器缓存 + */ + private final Map exceptionHandlerCaches = new ConcurrentReferenceHashMap<>(8); + + /** + * 表达式执行器 + */ + private final MethodBasedExpressionEvaluator methodBasedExpressionEvaluator; + + /** + * 是否允许将不可执行的表达式作为字符串使用 + */ + @Setter + private boolean allowedMakeNonExecutableExpressionsAsString = true; + + /** + * 当加锁失败时的处理策略 + * + * @param key 用于获取锁的key + * @param method 方法 + * @param arguments 方法参数 + */ + @Override + public void onLockFailure(String key, Method method, Object[] arguments) throws Exception { + exceptionHandlerCaches.computeIfAbsent(method, this::resolveExceptionHandler) + .handle(key, method, arguments); + } + + /** + * 获取该方法对应的失败处理器。 + * + * @param method 方法 + * @return 失败处理器 + */ + @NonNull + protected FailureHandler resolveExceptionHandler(Method method) { + Options options = AnnotatedElementUtils.findMergedAnnotation(method, Options.class); + return Objects.isNull(options) ? + defaultExceptionFactory : new FailureMessageFailureHandler(options.lockFailureException(), options.lockFailureMessage()); + } + + /** + * 获取异常信息 + * + * @param key 获取失败的锁的key + * @param method 方法 + * @param arguments 调用参数 + * @param expression 用于获取异常信息的表达式 + * @return 异常信息 + */ + @Nullable + protected final String resolveMessage( + String key, Method method, Object[] arguments, String expression) { + // 若未指定错误信息,则返回默认的异常信息 + if (!StringUtils.hasText(expression)) { + return DEFAULT_EXCEPTION_MESSAGE; + } + try { + return methodBasedExpressionEvaluator.getValue( + method, arguments, expression, String.class, Collections.singletonMap(KEY_NAME, key) + ); + } catch (Exception ex) { + if (allowedMakeNonExecutableExpressionsAsString) { + return expression; + } + throw ex; + } + } + + /** + * 失败处理器 + * + * @author huangchengxing + */ + protected static class FailureHandler { + public void handle(String key, Method method, Object[] arguments) throws Exception { + throw new LockFailureException(DEFAULT_EXCEPTION_MESSAGE); + } + } + + /** + * 当用户在方法上配置了{@link Options}注解时使用的失败处理器, + * 将根据用户配置抛出携带有指定信息的异常。 + * + * @author huangchengxing + */ + @RequiredArgsConstructor + private class FailureMessageFailureHandler extends FailureHandler { + + private final Class exceptionType; + private final String messageTemplate; + @SuppressWarnings("java:S3077") + private volatile Constructor constructor; + + @Override + public void handle(String key, Method method, Object[] arguments) throws Exception { + String message = resolveMessage(key, method, arguments, messageTemplate); + throw newExceptionInstance(message); + } + + private Exception newExceptionInstance(String message) { + if (constructor == null) { + synchronized (this) { + if (constructor == null) { + constructor = determineConstructor(); + } + } + } + try { + return constructor.getParameterCount() > 0 ? + constructor.newInstance(message) : constructor.newInstance(); + } catch (Exception e) { + throw new IllegalStateException("创建异常实例失败,指定的异常类型:" + + constructor.getDeclaringClass().getName() + "是否可以被实例化?", e); + } + } + + private Constructor determineConstructor() { + // 异常类需要保证至少有一个可用的构造器 + Constructor constr = ClassUtils.getConstructorIfAvailable(exceptionType, String.class); + if (Objects.isNull(constr)) { + constr = ClassUtils.getConstructorIfAvailable(exceptionType); + } + Assert.notNull(constr, "异常类型[" + exceptionType.getName() + + "]必须有一个无参构造函数,或有有一个接受String类型参数的造函数"); + if (!constr.isAccessible()) { + ReflectionUtils.makeAccessible(constr); + } + return constr; + } + } + + /** + * 当加锁失败时,要抛出的异常信息 + * + * @author huangchengxing + */ + @Target(value = {ElementType.METHOD, ElementType.ANNOTATION_TYPE}) + @Retention(value = RetentionPolicy.RUNTIME) + @Inherited + @Documented + public @interface Options { + + /** + *

抛出异常的错误消息。 + * + *

错误信息支持 SpEL 表达式,你可以在表达式中引用上下文参数: + *

+ * + * @return 错误消息 + */ + String lockFailureMessage() default DEFAULT_EXCEPTION_MESSAGE; + + /** + *

获取锁失败时,抛出的异常。
+ * 异常类必须提供一个无参构造函数,或者提供一个接受{@code String}类型参数的构造函数。 + * + * @return 异常类型 + */ + Class lockFailureException() default LockFailureException.class; + } +} diff --git a/lock4j-core/src/main/java/com/baomidou/lock/LockFailureStrategy.java b/lock4j-core/src/main/java/com/baomidou/lock/LockFailureStrategy.java index ffa5e64d1b64eabba43519e80d60b14d9b846ce0..e2e5d21bd2de3dac0d68756402fa4d70189ce079 100644 --- a/lock4j-core/src/main/java/com/baomidou/lock/LockFailureStrategy.java +++ b/lock4j-core/src/main/java/com/baomidou/lock/LockFailureStrategy.java @@ -19,13 +19,21 @@ package com.baomidou.lock; import java.lang.reflect.Method; /** + * 当加锁失败时的处理策略 + * * @author zengzhihong */ public interface LockFailureStrategy { /** - * 锁失败事件 + * 当加锁失败时的处理策略 + * + * @param key 用于获取锁的key + * @param method 方法 + * @param arguments 方法参数 + * @throws Exception 处理过程中可能抛出的异常,如果抛出异常则会终止方法执行 */ - void onLockFailure(String key, Method method, Object[] arguments); + void onLockFailure(String key, Method method, Object[] arguments) throws Exception; + // TODO 释放锁失败时也应该进行处理,具体参见:https://gitee.com/baomidou/lock4j/issues/I4LG1U } diff --git a/lock4j-core/src/main/java/com/baomidou/lock/MethodBasedExpressionEvaluator.java b/lock4j-core/src/main/java/com/baomidou/lock/MethodBasedExpressionEvaluator.java index f919a28ac9bca9d381ee8b25a375971d1a216229..baab14701b3229662c649081668fda3130f475bb 100644 --- a/lock4j-core/src/main/java/com/baomidou/lock/MethodBasedExpressionEvaluator.java +++ b/lock4j-core/src/main/java/com/baomidou/lock/MethodBasedExpressionEvaluator.java @@ -1,6 +1,10 @@ package com.baomidou.lock; +import org.springframework.lang.NonNull; + import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Map; /** * 基于方法调用的表达式计算器 @@ -9,6 +13,19 @@ import java.lang.reflect.Method; */ public interface MethodBasedExpressionEvaluator { + /** + * 执行表达式,返回执行结果 + * + * @param method 方法 + * @param arguments 调用参数 + * @param expression 表达式 + * @param resultType 返回值类型 + * @param variables 表达式中的变量 + * @return 表达式执行结果 + */ + T getValue( + Method method, Object[] arguments, String expression, Class resultType, @NonNull Map variables); + /** * 执行表达式,返回执行结果 * @@ -18,5 +35,8 @@ public interface MethodBasedExpressionEvaluator { * @param resultType 返回值类型 * @return 表达式执行结果 */ - T getValue(Method method, Object[] arguments, String expression, Class resultType); + default T getValue( + Method method, Object[] arguments, String expression, Class resultType) { + return getValue(method, arguments, expression, resultType, Collections.emptyMap()); + } } diff --git a/lock4j-core/src/main/java/com/baomidou/lock/SpelMethodBasedExpressionEvaluator.java b/lock4j-core/src/main/java/com/baomidou/lock/SpelMethodBasedExpressionEvaluator.java index 56e65659a568b82f8304968466fdc6d847cf9f63..3deb4d71beefa5a8dfa6a60cdfffa667bc6b9c05 100644 --- a/lock4j-core/src/main/java/com/baomidou/lock/SpelMethodBasedExpressionEvaluator.java +++ b/lock4j-core/src/main/java/com/baomidou/lock/SpelMethodBasedExpressionEvaluator.java @@ -54,12 +54,16 @@ public class SpelMethodBasedExpressionEvaluator * @param arguments 调用参数 * @param expression 表达式 * @param resultType 返回值类型 + * @param variables 表达式中的变量 * @return 表达式执行结果 */ @Override public T getValue( - Method method, Object[] arguments, String expression, Class resultType) { + Method method, Object[] arguments, String expression, Class resultType, @NonNull Map variables) { EvaluationContext context = createEvaluationContext(method, arguments); + if (!variables.isEmpty()) { + variables.forEach(context::setVariable); + } Expression exp = parseExpression(expression, expressionParser); return exp.getValue(context, resultType); } @@ -74,14 +78,16 @@ public class SpelMethodBasedExpressionEvaluator /** * 解析表达式 * - * @param exp 表达式 + * @param expression 表达式 * @param parser 表达式解析器 * @return 表达式对象 */ - protected Expression parseExpression(String exp, ExpressionParser parser) { - exp = embeddedValueResolver.resolveStringValue(exp); - Assert.notNull(exp, "Expression must not be null: " + exp); - return expressionCache.computeIfAbsent(exp, parser::parseExpression); + protected Expression parseExpression(String expression, ExpressionParser parser) { + return expressionCache.computeIfAbsent(expression, exp -> { + exp = embeddedValueResolver.resolveStringValue(exp); + Assert.notNull(exp, "Expression must not be null: " + exp); + return parser.parseExpression(exp); + }); } @Override diff --git a/lock4j-core/src/main/java/com/baomidou/lock/annotation/LockWithDefault.java b/lock4j-core/src/main/java/com/baomidou/lock/annotation/LockWithDefault.java new file mode 100644 index 0000000000000000000000000000000000000000..3ea368ecb0651abcfbfc02cef35ae5daa7c2767a --- /dev/null +++ b/lock4j-core/src/main/java/com/baomidou/lock/annotation/LockWithDefault.java @@ -0,0 +1,123 @@ +package com.baomidou.lock.annotation; + +import com.baomidou.lock.AbortLockFailureStrategy; +import com.baomidou.lock.DefaultLockKeyBuilder; +import com.baomidou.lock.LockKeyBuilder; +import com.baomidou.lock.exception.LockFailureException; +import com.baomidou.lock.executor.LockExecutor; +import com.baomidou.lock.spring.boot.autoconfigure.Lock4jProperties; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AliasFor; + +import java.lang.annotation.*; + +/** + * 基于默认配置进行加锁 + * + * @author huangchengxing + * @see Lock4j + * @see AbortLockFailureStrategy.Options + */ +@AbortLockFailureStrategy.Options +@Lock4j(failStrategy = AbortLockFailureStrategy.class) +@Target(value = {ElementType.METHOD, ElementType.ANNOTATION_TYPE}) +@Retention(value = RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface LockWithDefault { + + /** + * 应用条件表达式,当执行结果为{@code true}或{@code 'true'}时,才会执行锁操作 + * + * @return 名称 + */ + String condition() default ""; + + /** + * 用于多个方法锁同一把锁 可以理解为锁资源名称 为空则会使用 包名+类名+方法名 + * + * @return 名称 + */ + String name() default ""; + + /** + * @return lock 执行器 + */ + Class executor() default LockExecutor.class; + + /** + * support SPEL expresion 锁的key = name + keys + * + * @return KEY + */ + String[] keys() default ""; + + /** + * @return 过期时间 单位:毫秒 + *

+     *     过期时间一定是要长于业务的执行时间. 未设置则为默认时间30秒 默认值:{@link Lock4jProperties#expire}
+     * 
+ */ + long expire() default -1; + + /** + * @return 获取锁超时时间 单位:毫秒 + *
+     *     结合业务,建议该时间不宜设置过长,特别在并发高的情况下. 未设置则为默认时间3秒 默认值:{@link Lock4jProperties#acquireTimeout}
+     * 
+ */ + long acquireTimeout() default -1; + + /** + * 业务方法执行完后(方法内抛异常也算执行完)自动释放锁,如果为false,锁将不会自动释放直至到达过期时间才释放 {@link com.baomidou.lock.annotation.Lock4j#expire()} + * + * @return 是否自动释放锁 + */ + boolean autoRelease() default true; + + /** + * key生成器策略,默认使用{@link DefaultLockKeyBuilder} + * + * @return LockKeyBuilder + */ + Class keyBuilderStrategy() default DefaultLockKeyBuilder.class; + + /** + * 获取顺序,值越小越先执行 + * + * @return 顺序值 + */ + int order() default Ordered.LOWEST_PRECEDENCE; + + /** + *

抛出异常的错误消息。 + * + *

错误信息支持 SpEL 表达式,你可以在表达式中引用上下文参数: + *

+ * + * @return String + * @see AbortLockFailureStrategy.Options#lockFailureMessage + */ + @AliasFor( + annotation = AbortLockFailureStrategy.Options.class, + attribute = "lockFailureMessage" + ) + String lockFailureMessage() default AbortLockFailureStrategy.DEFAULT_EXCEPTION_MESSAGE; + + /** + *

获取锁失败时,抛出的异常。
+ * 异常类必须提供一个无参构造函数,或者提供一个接受{@code String}类型参数的构造函数。 + * + * @return String + * @see AbortLockFailureStrategy.Options#lockFailureException + */ + @AliasFor( + annotation = AbortLockFailureStrategy.Options.class, + attribute = "lockFailureException" + ) + Class lockFailureException() default LockFailureException.class; +} diff --git a/lock4j-core/src/main/java/com/baomidou/lock/spring/boot/autoconfigure/Lock4jProperties.java b/lock4j-core/src/main/java/com/baomidou/lock/spring/boot/autoconfigure/Lock4jProperties.java index d7d7db8c69275f7fdbe5aca98d1b18157ce09e4a..4c04e55c87e6ed950cbc8883b6a5bb1a65f4901f 100644 --- a/lock4j-core/src/main/java/com/baomidou/lock/spring/boot/autoconfigure/Lock4jProperties.java +++ b/lock4j-core/src/main/java/com/baomidou/lock/spring/boot/autoconfigure/Lock4jProperties.java @@ -52,10 +52,12 @@ public class Lock4jProperties { * 默认执行器,不设置默认取容器第一个(默认注入顺序,redisson>redisTemplate>zookeeper) */ private Class primaryExecutor; + /** * 默认失败策略,不设置存在多个时默认根据PriorityOrdered、Ordered排序规则选择|注入顺序选择 */ private Class primaryFailureStrategy; + /** * 默认key生成策略,不设置存在多个时默认根据PriorityOrdered、Ordered排序规则选择|注入顺序选择 */ @@ -65,4 +67,9 @@ public class Lock4jProperties { * 锁key前缀 */ private String lockKeyPrefix = "lock4j"; + + /** + * 是否允许将非可执行表达式作为字符串 + */ + private boolean allowedMakeNonExecutableExpressionsAsString = true; } diff --git a/lock4j-core/src/main/java/com/baomidou/lock/spring/boot/autoconfigure/LockAutoConfiguration.java b/lock4j-core/src/main/java/com/baomidou/lock/spring/boot/autoconfigure/LockAutoConfiguration.java index 314cf510db2e3e4bad779de7a539cdced8d3304e..bab44048e254cbfec3c24bd10b295935737fee32 100644 --- a/lock4j-core/src/main/java/com/baomidou/lock/spring/boot/autoconfigure/LockAutoConfiguration.java +++ b/lock4j-core/src/main/java/com/baomidou/lock/spring/boot/autoconfigure/LockAutoConfiguration.java @@ -26,6 +26,7 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Role; import org.springframework.core.Ordered; @@ -72,9 +73,20 @@ public class LockAutoConfiguration { } @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + @Primary @Bean @ConditionalOnMissingBean - public LockFailureStrategy lockFailureStrategy() { + public AbortLockFailureStrategy abortLockFailureStrategy( + MethodBasedExpressionEvaluator methodBasedExpressionEvaluator, Lock4jProperties lock4jProperties) { + AbortLockFailureStrategy strategy = new AbortLockFailureStrategy(methodBasedExpressionEvaluator); + strategy.setAllowedMakeNonExecutableExpressionsAsString(lock4jProperties.isAllowedMakeNonExecutableExpressionsAsString()); + return strategy; + } + + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + @Bean + @ConditionalOnMissingBean + public DefaultLockFailureStrategy defaultLockFailureStrategy() { return new DefaultLockFailureStrategy(); } diff --git a/lock4j-core/src/test/java/com/baomidou/lock/executor/AbortLockFailureStrategyTest.java b/lock4j-core/src/test/java/com/baomidou/lock/executor/AbortLockFailureStrategyTest.java new file mode 100644 index 0000000000000000000000000000000000000000..a3176c28ddf8727cd1913b53e02bcf1d43d733c8 --- /dev/null +++ b/lock4j-core/src/test/java/com/baomidou/lock/executor/AbortLockFailureStrategyTest.java @@ -0,0 +1,108 @@ +package com.baomidou.lock.executor; + +import com.baomidou.lock.AbortLockFailureStrategy; +import com.baomidou.lock.SpelMethodBasedExpressionEvaluator; +import com.baomidou.lock.annotation.LockWithDefault; +import com.baomidou.lock.exception.LockFailureException; +import lombok.SneakyThrows; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.expression.spel.SpelEvaluationException; + +import java.lang.reflect.Method; + +/** + * test for {@link AbortLockFailureStrategy} + * + * @author huangchengxing + */ +@SpringBootTest( + properties = "example.service.message=k3", + classes = {SpelMethodBasedExpressionEvaluator.class, AbortLockFailureStrategy.class} +) +public class AbortLockFailureStrategyTest { + + @Autowired + public AbortLockFailureStrategy abortLockFailureStrategy; + private static Method method; + private static Method annotatedMethod1; + private static Method annotatedMethod2; + + @SneakyThrows + @BeforeAll + public static void init() { + method = AbortLockFailureStrategyTest.class.getDeclaredMethod("method", String.class, String.class); + annotatedMethod1 = AbortLockFailureStrategyTest.class.getDeclaredMethod("annotatedMethod1", String.class, String.class); + annotatedMethod2 = AbortLockFailureStrategyTest.class.getDeclaredMethod("annotatedMethod2", String.class, String.class); + } + + @SneakyThrows + @Test + void testNonAnnotatedMethod() { + Assertions.assertThrows( + LockFailureException.class, + () -> abortLockFailureStrategy.onLockFailure( + "test", method, new Object[]{ "param1", "param2"} + ), + "request failed, please retry it." + ); + } + + @SneakyThrows + @Test + void testAnnotatedMethod1() { + Assertions.assertThrows( + IllegalArgumentException.class, + () -> abortLockFailureStrategy.onLockFailure( + "test", annotatedMethod1, new Object[]{ "param1", "param2"} + ), + "error:param1" + ); + } + + @SneakyThrows + @Test + void testAnnotatedMethod2() { + Assertions.assertThrows( + TestException.class, + () -> abortLockFailureStrategy.onLockFailure( + "test", annotatedMethod2, new Object[]{ "param1", "param2"} + ), + "error" + ); + + abortLockFailureStrategy.setAllowedMakeNonExecutableExpressionsAsString(false); + Assertions.assertThrows( + SpelEvaluationException.class, + () -> abortLockFailureStrategy.onLockFailure( + "test", annotatedMethod2, new Object[]{ "param1", "param2"} + ) + ); + abortLockFailureStrategy.setAllowedMakeNonExecutableExpressionsAsString(true); + } + + public static class TestException extends RuntimeException { + } + + @AbortLockFailureStrategy.Options( + lockFailureException = TestException.class, + lockFailureMessage = "error" + ) + private void annotatedMethod2(String param1, String param2) { + // do nothing + } + + @LockWithDefault( + lockFailureException = IllegalArgumentException.class, + lockFailureMessage = "'error:' + #param1" + ) + private void annotatedMethod1(String param1, String param2) { + // do nothing + } + private void method(String param1, String param2) { + // do nothing + } +}