redis caching with Spring custom annotations

Posted by jaymc on Wed, 13 Oct 2021 04:26:16 +0200

1, Foreword

Redis is one of the basic components that must be used in distributed microservices. At present, it is basically used in most domestic projects, and caching is one of its main functions. Frequent use of set() method to add annotations in projects will cause code duplication and bloated. For those who do not have enough development experience, even improper addition of caching will directly affect the normal business process, This will lead to accidents. Therefore, mature companies will automatically add redis cache through annotations by encapsulating basic components. Starting from the principle, this paper will lead you to personally implement custom annotations and complete the development of redis cache. If you learn it, you can show it in front of your colleagues.

2, Parameter description of custom annotation

@Target:

The purpose of annotations, that is, where annotations can be used, usually has
@Target(ElementType.TYPE) -- interface, class, enumeration and annotation
@Target(ElementType.FIELD) -- constant of field and enumeration
@Target(ElementType.METHOD) -- Method
@Target(ElementType.PARAMETER) -- Method Parameter
@Target(ElementType.CONSTRUCTOR) -- constructor
@Target(ElementType.LOCAL_VARIABLE) -- local variable
@Target(ElementType.ANNOTATION_TYPE) -- Annotation
@Target(ElementType.PACKAGE) - package

@Retention position of annotation

It is used to define the life cycle of annotations, and RetentionPolicy needs to be specified when used. RetentionPolicy has three policies, namely:
SOURCE - the annotation is only kept in the SOURCE file. When the Java file is compiled into a class file, the annotation is discarded.
class - annotations are retained in the class file, but are discarded when the jvm loads the class file, which is the default life cycle.
RUNTIME - the annotation is not only saved in the class file, but still exists after the jvm loads the class file.

@Documented

Annotations are only used for identification. They have no practical effect. Just understand them.
If @ Documented annotation is used, the @ Documented annotation will be displayed when generating javadoc.

3, Custom redis annotation

JhRedisCache -- add the annotation of rediscache

/**
 * @author ljx
 * @Description: Add redis cache annotation
 * @date 2020/6/9 4:11 afternoon
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JhRedisCache {
    /*
     * redis The key in the cache supports spel expressions
     */
    String key() default "";
    /*
     * Cache time. The default cache time is 60 * 60 * 24 a day
     */
    long expire() default 86400L;
    /*
     * If the annotation is added to the method that returns the list, you need to specify the class type in the list through this field
     */
    Class type() default Object.class;
}

JhRedisCacheEvict -- delete the annotation of redis cache

/**
 * @author ljx
 * @Description: Delete the redis cache annotation
 * @date 2020/6/10 3:38 afternoon
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JhRedisCacheEvict {
    String key() default "";
}

4, Custom AOP cut

/**
 * @author ljx
 * @Description: Annotation section
 * @date 2020/6/9 11:12 morning
 */
@Component
@Aspect
public class RedisCacheAspect {
    private static final String SPEL = "#";
    private static final String KEY_SEPARATOR = "_";
    private static final int TWO = 2;
    private static final Logger logger = LoggerFactory.getLogger(RedisCacheAspect.class);

    private RedisClient redisClient;

    private AppInfo appInfo;

    /**
     * @Description: Cut into this point where JhRedisCache annotation is used to query whether the cache exists in redis. If it already exists, it will be returned directly. Otherwise, query the database
     * @param pjp Pointcut information
     * @return java.lang.Object Method return value
     * @Author: ljx
     * @Date: 2020/6/9 4:20 afternoon
     */
    @Around("@annotation(edu.jiahui.redis.starter.annotation.JhRedisCache)")
    private Object handleCache(final ProceedingJoinPoint pjp) throws Throwable {
        // Get the method object of the cut in
        // This m is a proxy object and does not contain annotations
        Method m = ((MethodSignature) pjp.getSignature()).getMethod();
        // this() returns the proxy object, target() returns the target object, and the method object obtained by the target object reflection contains the annotation
        Method methodWithAnnotations = pjp.getTarget().getClass().getDeclaredMethod(pjp.getSignature().getName(), m.getParameterTypes());
        // Gets the annotation object from the target method object
        JhRedisCache cacheAnnotation = methodWithAnnotations.getDeclaredAnnotation(JhRedisCache.class);
        // Parse key
        String keyExpr = cacheAnnotation.key();
        Object[] as = pjp.getArgs();
        String key = getRedisKeyBySpel(keyExpr,methodWithAnnotations, as);
        // Get cache from redis
        String cache = null;
        try {
            cache = redisClient.get(key);
        } catch (Exception e) {
            logger.error("{}query redis Cache exception:{}",keyExpr,e.getMessage());
        }
        if (StringUtils.isBlank(cache)) {
            // If it does not exist, get it from the database
            Object result = pjp.proceed();
            // After being obtained from the database, it is stored in redis. If the expiration time is specified, it is set
            try {
                long expireTime = cacheAnnotation.expire();
                if (expireTime > 0) {
                    redisClient.set(key,JSON.toJSONString(result), expireTime, TimeUnit.SECONDS);
                }else{
                    redisClient.set(key, JSON.toJSONString(result));
                }
            } catch (Exception e) {
                logger.warn("{}{}cache redis abnormal:{}",keyExpr,e.getMessage(),result);
            }
            return result;
        }
        // Get the annotation on the proxy method
        Class modelType = cacheAnnotation.type();
        // Get the return value type of the proxied method
        Class returnType = ((MethodSignature) pjp.getSignature()).getReturnType();
        // Returns the json obtained from the cache by deserialization
        return deserialize(cache, returnType, modelType);

    }


    @Around("@annotation(edu.jiahui.redis.starter.annotation.JhRedisCacheEvict)")
    private Object handleCacheEvict(ProceedingJoinPoint pjp) throws Throwable {
        // Get the method object of the cut in
        // This m is a proxy object and does not contain annotations
        Method m = ((MethodSignature) pjp.getSignature()).getMethod();
        // this() returns the proxy object, target() returns the target object, and the method object obtained by the target object reflection contains the annotation
        Method methodWithAnnotations = pjp.getTarget().getClass().getDeclaredMethod(pjp.getSignature().getName(), m.getParameterTypes());
        // Gets the annotation object from the target method object
        JhRedisCacheEvict cacheEvictAnnotation = methodWithAnnotations.getDeclaredAnnotation(JhRedisCacheEvict.class);
        // Parse key
        String keyExpr = cacheEvictAnnotation.key();
        Object[] as = pjp.getArgs();
        String key = getRedisKeyBySpel(keyExpr,methodWithAnnotations, as);
        // Delete the user information in the database before deleting the cache
        Object result = pjp.proceed();
        redisClient.delete(key);
        return result;
    }

    public RedisCacheAspect() {
    	// Different projects may implement different redisClient objects when initializing them. This is implemented in combination with redis in their own projects
        appInfo= SpringContext.getBean(AppInfo.class);
        String appName = appInfo.getAppName();
        this.redisClient = SpringContext.getBean(appName, RedisClient.class);
    }

    /**
     * @Description: Parse the key in the annotation and support the parsing of spel expressions
     * @param spelExpress spel expression in annotation
     * @param method Method object
     * @param params Method parameters
     * @return java.lang.String
     * @Author: ljx
     * @Date: 2020/6/10 3:16 afternoon
     */
    private String getRedisKeyBySpel(String spelExpress, Method method, Object[] params) {
        String redisKey = appInfo.getAppName()+KEY_SEPARATOR+method.getName();
        // If it is blank, the default service name is_ Method name
        if (StringUtils.isBlank(spelExpress)){
            return redisKey;
        }
        // If it is not a spel expression, the key passed in by the user is used directly
        if(!spelExpress.contains(SPEL)){
            return spelExpress;
        }
        // If it is a spel expression, but the parameter is empty, the default service name is_ Method name
        if(params==null){
            return redisKey;
        }
        ExpressionParser parser = new SpelExpressionParser();
        EvaluationContext context = new StandardEvaluationContext();
        // Set the first parameter of the variable used in the spel expression
        context.setVariable("entity", params[0]);
        // Set the second parameter
        if(params.length>1&&params[1]!=null){
            context.setVariable("entityTwo", params[1]);
        }
        // Set the third parameter
        if(params.length>TWO&&params[TWO]!=null){
            context.setVariable("entityTrd", params[2]);
        }
        // Parsing spel expressions
        Expression expression = parser.parseExpression(spelExpress, new TemplateParserContext());
        final Object value = expression.getValue(context);
        return redisKey + KEY_SEPARATOR+"_"+Objects.toString(value,"");
    }

    /**
     * @Description: FastJSON Deserialize get object
     * @param json String obtained from redis cache
     * @param clazz The class type of the return value of the annotated method
     * @param modelType Convert to class type in list
     * @return java.lang.Object
     * @Author: ljx
     * @Date: 2020/6/11 3:50 afternoon
     */
    private Object deserialize(String json, Class clazz, Class modelType) {
        return clazz.isAssignableFrom(List.class) ? JSON.parseArray(json, modelType) : JSON.parseObject(json, clazz);
    }

}

5, Use case

Simple use:

   @JhRedisCache(key = "#{#entity}")
    public TeachCenter selectTeacherCenter(Integer teachCenterId) {
        return teachCenterMapper.selectByPrimaryKey(teachCenterId);
    }

Complex use:

    @JhRedisCache(key = "#{#entity.getProvinceId()}", type = TeachCenter.class)
    public List<TeachCenter> selectTeachCenterList(TeachCenterCommonRequest teacherCenterCommonRequest) {
        //Obtain the region id;
        Integer provinceId = teacherCenterCommonRequest.getProvinceId();
        //paging
        PageHelper.startPage(teacherCenterCommonRequest.getPageNum(), teacherCenterCommonRequest.getPageSize());
        //Get all teaching centers
        List<TeachCenter> teachCenterList = teachCenterMapper.selectByProvinceId(provinceId);
        return teachCenterList;
}        

My personal test is effective and has been used on a large scale in the company's projects. Because it depends on the redis configuration, I will not lead you to test here. Interested partners can test in the project. If you have any problems, please communicate at any time.

Topics: Redis Spring Concurrent Programming Cache