Custom Annotation For Taking Redis Lock

Custom Annotation For Taking Redis Lock

I assume you know how to use Redis service in the spring boot service. Redis uses Distributed Lock method, which ensures lock safety for multiple servers and containers. The lock key is stored in a centralized way so all the containers can take it. This is very helpful when we have deployed our service where multiple containers are making updates.

Annotation:- An annotation is a form of metadata that can be added to Java code elements such as classes, methods, fields, parameters, etc. Annotations provide additional information about the code to the compiler, runtime environment, or other tools. Custom Annotation is the user-defined annotation used to perform specific actions.

To config Radisson with our Redis service first we need to import the dependency in our service. For this, you also take the reference here.

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.20.0</version>
</dependency>
Config config = new Config();
config.useSingleServer()
  .setAddress("redis://127.0.0.1:6379");

RedissonClient client = Redisson.create(config);

Here we will be using a Single-node config, but you can use whatever is required in your service. There are more configs that you can add to this. Which you can refer to here.

{
    "singleServerConfig": {
        "idleConnectionTimeout": 10000,
        "connectTimeout": 10000,
        "timeout": 3000,
        "retryAttempts": 3,
        "retryInterval": 1500,
        "password": null,
        "subscriptionsPerConnection": 5,
        "clientName": null,
        "address": "redis://127.0.0.1:6379",
        "subscriptionConnectionMinimumIdleSize": 1,
        "subscriptionConnectionPoolSize": 50,
        "connectionMinimumIdleSize": 10,
        "connectionPoolSize": 64,
        "database": 0,
        "dnsMonitoringInterval": 5000
    },
    "threads": 0,
    "nettyThreads": 0,
    "codec": null
}

To create the custom annotation we first need to define the interface for that annotation. The field in the interface will present the thing which is needed to perform certain actions with the annotation.

@Retaintion(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @intreface RedisLock{
    String lockId() default "lockId";
    String key() default "key";
    long waitTime() default 0;
    long LLTime default 50l;
}

The @Retention allows us to define how long an annotation should be retained or available during the runtime of a Java program.
The @Target annotation is another meta-annotation in Java that is used to specify the elements to which a custom annotation can be applied.

Wait time for the thread to wait at max to take the lock for this example we have taken 100 ms and Lease Lock time, the max time for which thread can acquire the lock we have given is 50 ms in this example case.

Now we will going to use Aspect-oriented programming in this to write the logic for the annotation. In AOP, define aspects that encapsulate cross-cutting concerns. An aspect is a class that contains advice and point-cut expressions. Advice represents the action to be taken, such as code to execute before or after a method execution, Pointcut expressions define where the advice should be applied.

To enable Aop in our service we need to add @EnableAspectJAutoProxy annotation in application class.

@EnableAspectJAutoProxy
@SpringBootApplication
public class Application {
    public static void main(String[] args){
        SpringApplication.run(Application.Class, args);    
    }
}

So here there 3 conditions for which you want to use the custom lock.

Key is defined

when you know the lockId(which helps uniquely identify the lock ) and you can directly pass it in the annotation call just like this.

@RedisLock(key="key" , lockId="lockId", waitTime=100, LLTime=80)
public String MethodName(){
    // method operation
}

in this case, the Aspect class will look like this

@org.aspectj.lang.annotation.Aspect
@Component
public class RedisLockAspect {

  @Autowired private RedissonClient redissonClient;

  private static final Logger LOGGER = LoggerFactory.getLogger(RedisLockAspect.class);

  @Before("@annotation(RedisLock)")
  public Object acquireRedisLock(ProceedingJoinPoint joinPoint) throws Throwable {
    Method method = getTargetMethod(joinPoint);
    RedisLock annotation = method.getAnnotation(RedisLock.class);
    String key = annotation.key();
    String lockId = annotation.lockId();
    String lockName = key.concat("_").concat(lockId);
    long waitTime = annotation.waitTime();
    long LLTime = annotation.LLTime();
    try {
        RLock lock = redissonClient.getLock(lockName);
        lock.tryLock( waitTime, LLTime , TimeUnit.MILLISECONDS);
        return joinPoint.proceed();
    } catch (InterruptedException e) {
        LOGGER.error("Exception while trying to acquire the redis lock", e);
    } catch (Exception e) {
        LOGGER.error("Unhandled Exception while trying to acquire the redis lock", e);
    } 
    return null;
  }


  @After("@annotation(RedisLock)")
  public void releaseLock(ProceedingJoinPoint joinPoint){
    Method method = getTargetMethod(joinPoint);
    RedisLock annotation = method.getAnnotation(RedisLock.class);
    String key = annotation.key();
    String lockId = annotation.lockId();
    String lockName = key.concat("_").concat(lockId);
    RLock lock = redissonClient.getLock(lockName);
    if(lock.isLocked() && lock.isHoldByCurrentThread()){
        lock.unlock();
    }
  }

  private Method getTargetMethod(ProceedingJoinPoint joinPoint) {
    MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
    return methodSignature.getMethod();
  }
}

The key is defined as the last arg value

@RedisLock(key="key" , lockId="lockId", waitTime=100, LLTime=80)
public String MethodName(..., String lockId){
    // method operation
}

As the key is a dynamic value it is possible that the key value is present in the last argument value(as we need to define some rule in order to get the key). So this case the Aspect class will look like this.

@org.aspectj.lang.annotation.Aspect
@Component
public class AspectClassForLockInArg {
    @Autowired private RedissonClient redissonClient;

  private static final Logger LOGGER = LoggerFactory.getLogger(RedisLockAspect.class);

  @Before("@annotation(RedisLock)")
  public Object acquireRedisLock(ProceedingJoinPoint joinPoint) throws Throwable {
    Method method = getTargetMethod(joinPoint);
    RedisLock annotation = method.getAnnotation(RedisLock.class);
    Object[] args = joinPoint.getArgs();
    String key = annotation.key();
    String lockId = (String) args[args.length-1];
    String lockName = key.concat("_").concat(lockId);
    long waitTime = annotation.waitTime();
    long LLTime = annotation.LLTime();
    try {
        RLock lock = redissonClient.getLock(lockName);
        lock.tryLock( waitTime, LLTime , TimeUnit.MILLISECONDS);
        return joinPoint.proceed();
    } catch (InterruptedException e) {
        LOGGER.error("Exception while trying to acquire the redis lock", e);
    } catch (Exception e) {
        LOGGER.error("Unhandled Exception while trying to acquire the redis lock", e);
    } 
    return null;
  }


  @After("@annotation(RedisLock)")
  public void releaseLock(ProceedingJoinPoint joinPoint){
    Method method = getTargetMethod(joinPoint);
    RedisLock annotation = method.getAnnotation(RedisLock.class);
   Object[] args = joinPoint.getArgs();
    String key = annotation.key();
    String lockId = (String) args[args.length-1];
    String lockName = key.concat("_").concat(lockId);
    RLock lock = redissonClient.getLock(lockName);
    if(lock.isLocked() && lock.isHoldByCurrentThread()){
        lock.unlock();
    }
  }

  private Method getTargetMethod(ProceedingJoinPoint joinPoint) {
    MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
    return methodSignature.getMethod();
  }   
}

The key is present in the field of arg

@RedisLock(key="key" , lockId="{args[0].fieldName}", waitTime=100, LLTime=80)
public String MethodName(){
    // method operation
}

This might be a possibility that the key is present in the field of the class present in the argument passed in the method. Now in this situation, we have to define the rule of passing the value locked in a certain way so we can parse these values. Here we are wrapping the string with "{}" for the indication that this needs some parsing. Also In which field do we want the parsing? So this code will look like this.

@org.aspectj.lang.annotation.Aspect
@Component
public class AspectClassForLockInArgClass {
  @Autowired private RedissonClient redissonClient;

  private static final Logger LOGGER = LoggerFactory.getLogger(RedisLockAspect.class);

  private ExpressionParser expressionParser = new SpelExpressionParser();
  private ParserContext parserContext = new TemplateParserContext();

  @Before("@annotation(in.dreamplug.charles.spring.annotations.RedisLock)")
  public Object acquireRedisLock(ProceedingJoinPoint joinPoint) throws Throwable {
    Method method = getTargetMethod(joinPoint);
    RedisLock annotation = method.getAnnotation(RedisLock.class);
    String key = annotation.key();
    Object[] args = joinPoint.getArgs();
    String getLockKeys = getLockId(annotation.lockId(), args);
    String lockName = key.concat("_").concat(getLockKeys);
    long waitTime = annotation.waitTime();
    long LLTime = annotation.LLTime();
    try {
      RLock lock = redissonClient.getLock(lockName);
      lock.tryLock( waitTime, LLTime, TimeUnit.MILLISECONDS);
      return joinPoint.proceed();
    } catch (InterruptedException e) {
      LOGGER.error("Exception while trying to acquire the redis lock", e);
    } catch (Exception e) {
      LOGGER.error("Unhandled Exception while trying to acquire the redis lock", e);
    }
    return null;
  }

  @After("@annotation(RedisLock)")
  public void releaseLock(ProceedingJoinPoint joinPoint){
    Method method = getTargetMethod(joinPoint);
    RedisLock annotation = method.getAnnotation(RedisLock.class);
    Object[] args = joinPoint.getArgs();
    String key = annotation.key();
    String lockId = (String) args[args.length-1];
    String lockName = key.concat("_").concat(lockId);
    RLock lock = redissonClient.getLock(lockName);
    if(lock.isLocked() && lock.isHoldByCurrentThread()){
        lock.unlock();
    }
  }

  private Method getTargetMethod(ProceedingJoinPoint joinPoint) {
    MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
    return methodSignature.getMethod();
  }

  private String getLockId(String authExpression, Object[] args)
      throws IllegalAccessException, NoSuchFieldException {
    String[] parseString = expressionParsingHelper(authExpression);
    Expression expression = expressionParser.parseExpression(parseString[0], parserContext);
    Object value = expression.getValue(new RootObject(args), Object.class);
    Class<?> objectType = value.getClass();
    Field temp = value.getClass().getDeclaredField(parseString[1]);
    temp.setAccessible(true);
    System.out.println(objectType.getName());
    System.out.println(objectType.getSimpleName());
    return (String) temp.get(value);
  }

  private String[] expressionParsingHelper(String expression) {
    String[] split = expression.split("\\.");
    if (split.length == 1) {
      return new String[] {expression, ""};
    } else {
      return new String[] {split[0] + "}", split[1].split("}")[0]};
    }
  }

  private static class TemplateparserContext implements ParserContext {
    @Override
    public boolean isTemplate() {
      return true;
    }

    @Override
    public String getExpressionPrefix() {
      return "#{";
    }

    @Override
    public String getExpressionSuffix() {
      return "}";
    }
  }

  protected static class RootObject {
    private final Object[] args;

    private RootObject(Object[] args) {
      super();
      this.args = args;
    }  

    public Object[] getArgs() {
      return args;
    }
  }   
}
/* For reference 
https://github.com/Richa-b/custom-annotation-with-dynamic-values-using-aop/tree/master
https://stackoverflow.com/questions/21186435/custom-annotation-with-dynamic-argument
 https://github.com/Mittal9269/RedisLockAspectClass/tree/main
*/

By using Aop, reflection and parsing, and custom annotation we can create our own Redis lock annotation for most of the required cases.

This is all for this article, hope this article was able to solve the problem you were looking for.