Spring Framework integration for ShedLock distributed locking system providing annotation-based task scheduling locks.
—
Extended configuration extraction and AOP infrastructure for advanced ShedLock use cases.
Extended interface for extracting lock configuration from method calls with parameter support.
public interface ExtendedLockConfigurationExtractor extends LockConfigurationExtractor {
// Extract lock configuration for method with parameters
Optional<LockConfiguration> getLockConfiguration(
Object object,
Method method,
Object[] parameterValues
);
}This interface extends the core LockConfigurationExtractor to provide Spring-specific method parameter handling.
The main implementation of lock configuration extraction with Spring-specific features.
public class SpringLockConfigurationExtractor implements ExtendedLockConfigurationExtractor {
// Constructor without BeanFactory (limited functionality)
public SpringLockConfigurationExtractor(
Duration defaultLockAtMostFor,
Duration defaultLockAtLeastFor,
StringValueResolver embeddedValueResolver,
Converter<String, Duration> durationConverter
);
// Full constructor with BeanFactory support
public SpringLockConfigurationExtractor(
Duration defaultLockAtMostFor,
Duration defaultLockAtLeastFor,
StringValueResolver embeddedValueResolver,
Converter<String, Duration> durationConverter,
BeanFactory beanFactory
);
// Extract configuration from Runnable (for scheduler mode)
public Optional<LockConfiguration> getLockConfiguration(Runnable task);
// Extract configuration from method call with parameters
public Optional<LockConfiguration> getLockConfiguration(
Object target,
Method method,
Object[] parameterValues
);
// Duration calculation methods
Duration getLockAtMostFor(AnnotationData annotation);
Duration getLockAtLeastFor(AnnotationData annotation);
// Static annotation finding utilities
static AnnotationData findAnnotation(Object target, Method method);
static <A extends Annotation> A findAnnotation(Object target, Method method, Class<A> annotationType);
}@EnableSchedulerLock@EnableSchedulerLock@SchedulerLock on method, then target class methodLockConfiguration with current timestampThe extractor supports Spring Expression Language (SpEL) in lock names when method parameters are available:
@SchedulerLock(name = "processUser-#{#userId}", lockAtMostFor = "10m")
public void processUser(Long userId) {
// Lock name becomes "processUser-123" when userId = 123
}
@SchedulerLock(name = "processOrder-#{#order.id}-#{#order.customerId}", lockAtMostFor = "5m")
public void processOrder(Order order) {
// Lock name becomes "processOrder-456-789"
}Lock names and durations can reference application properties:
@SchedulerLock(name = "${app.scheduled.task.name}", lockAtMostFor = "${app.scheduled.task.timeout}")
public void configurableTask() {
// Values resolved from application.properties
}Internal record for holding parsed annotation data:
record AnnotationData(
String name,
long lockAtMostFor,
String lockAtMostForString,
long lockAtLeastFor,
String lockAtLeastForString
) {}The extractor uses sophisticated annotation finding that handles Spring AOP proxies:
AnnotatedElementUtils.getMergedAnnotation() for composed annotations// Static utility method
static <A extends Annotation> A findAnnotation(Object target, Method method, Class<A> annotationType) {
// First try the method directly
A annotation = AnnotatedElementUtils.getMergedAnnotation(method, annotationType);
if (annotation != null) {
return annotation;
}
// Try to find annotation on proxied class
Class<?> targetClass = AopUtils.getTargetClass(target);
try {
Method methodOnTarget = targetClass.getMethod(method.getName(), method.getParameterTypes());
return AnnotatedElementUtils.getMergedAnnotation(methodOnTarget, annotationType);
} catch (NoSuchMethodException e) {
return null;
}
}For SpEL expression support, the extractor uses Spring's parameter name discovery:
public static final PrioritizedParameterNameDiscoverer PARAMETER_NAME_DISCOVERER;
// Supports multiple discovery strategies:
// - KotlinReflectionParameterNameDiscoverer (if Kotlin is present)
// - SimpleParameterNameDiscoverer (custom implementation)private static class SimpleParameterNameDiscoverer implements ParameterNameDiscoverer {
public String[] getParameterNames(Method method);
public String[] getParameterNames(Constructor<?> ctor);
}This custom discoverer avoids calling hasRealParameterData() which can cause issues in certain test scenarios.
For SpEL expression evaluation, the extractor creates Spring evaluation contexts:
private Optional<EvaluationContext> getEvaluationContext(Method method, Object[] parameterValues) {
if (method.getParameters().length > 0 && method.getParameters().length == parameterValues.length) {
StandardEvaluationContext evaluationContext = new MethodBasedEvaluationContext(
beanFactory, method, parameterValues, PARAMETER_NAME_DISCOVERER
);
originalEvaluationContext.applyDelegatesTo(evaluationContext);
return Optional.of(evaluationContext);
}
return Optional.empty();
}The evaluation context includes:
#userId)The extractor supports multiple duration input formats with fallback logic:
private Duration getValue(
long valueFromAnnotation, // Numeric milliseconds (-1 if not set)
String stringValueFromAnnotation, // String duration ("" if not set)
Duration defaultValue, // Default from @EnableSchedulerLock
String paramName // For error messages
) {
// Priority 1: Numeric value (if >= 0)
if (valueFromAnnotation >= 0) {
return Duration.of(valueFromAnnotation, MILLIS);
}
// Priority 2: String value (if not empty)
if (StringUtils.hasText(stringValueFromAnnotation)) {
// Resolve properties
if (embeddedValueResolver != null) {
stringValueFromAnnotation = embeddedValueResolver.resolveStringValue(stringValueFromAnnotation);
}
// Convert string to duration
Duration result = durationConverter.convert(stringValueFromAnnotation);
if (result.isNegative()) {
throw new IllegalArgumentException("Invalid " + paramName + " value - cannot set negative duration");
}
return result;
}
// Priority 3: Default value
return defaultValue;
}Invalid duration formats throw descriptive IllegalArgumentException:
// Examples of error messages:
"Invalid lockAtMostForString value \"invalid\" - cannot parse into long nor duration"
"Invalid lockAtLeastForString value \"-5s\" - cannot set negative duration"@Component
public class AdvancedScheduledTasks {
@SchedulerLock(
name = "user-#{#user.department}-#{#user.id}",
lockAtMostFor = "#{#user.department == 'VIP' ? '30m' : '10m'}"
)
public void processUser(User user) {
// Lock name: "user-Engineering-123"
// Duration: 30 minutes for VIP department, 10 minutes otherwise
}
@SchedulerLock(
name = "batch-#{T(java.time.LocalDate).now()}-#{#batchSize}",
lockAtMostFor = "PT#{#batchSize / 100}M" // 1 minute per 100 items
)
public void processBatch(int batchSize) {
// Lock name: "batch-2023-12-01-1000"
// Duration: 10 minutes for 1000 items
}
}# application.yml
app:
locking:
user-processing:
name: "user-processor-${spring.application.name}"
timeout: "PT15M"
batch-processing:
timeout: "PT1H"@SchedulerLock(
name = "${app.locking.user-processing.name}",
lockAtMostFor = "${app.locking.user-processing.timeout}"
)
public void processUsers() {
// Configuration externalized to properties
}For advanced use cases, you can provide your own extractor:
@Configuration
public class CustomLockConfig {
@Bean
@Primary
public ExtendedLockConfigurationExtractor customLockConfigurationExtractor() {
return new CustomSpringLockConfigurationExtractor(
Duration.ofMinutes(30), // default lockAtMostFor
Duration.ZERO, // default lockAtLeastFor
embeddedValueResolver,
StringToDurationConverter.INSTANCE,
beanFactory
);
}
}Install with Tessl CLI
npx tessl i tessl/maven-net-javacrumbs-shedlock--shedlock-spring