JetCache

background

Github.com/alibaba/jet…

First, use method:

1. Create a project – The SpringBoot project is used here

2. The introduction of the starter

<dependency> <groupId>com.alicp.jetcache</groupId> <artifactId>jetcache-starter-redis</artifactId> The < version > 2.5.14 < / version > < / dependency >Copy the code

3. Configure the application.yaml file

For the convenience of testing, The machine to install redis brew install redis start redis 1, modify the conf - / opt/homebrew/etc/redis. Conf daemonize no change to yes to start 2, start redis daemon Jetcache: statIntervalMinutes: 15 areaInCacheName: false local: default: type: linkedhashmap keyConvertor: fastjson remote: default: type: redis keyConvertor: fastjson valueEncoder: Java valueDecoder: Java poolConfig: minIdle: 5 maxIdle: 20 maxTotal: 50 host: 127.0.0.1 port: 6379Copy the code

4. Enable the scan

EnableMethodCache EnableCreateCacheAnnotation this two annotations activate the Cached and CreateCache annotations, other Spring Boot procedures and standards are the same. This class can be run directly from the main method.

package com.ghq.jetcachelearn; import com.alicp.jetcache.anno.config.EnableCreateCacheAnnotation; import com.alicp.jetcache.anno.config.EnableMethodCache; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @EnableMethodCache(basePackages = "com.ghq.jetcachelearn") @EnableCreateCacheAnnotation public class JetcacheLearnApplication { public static void main(String[] args) { SpringApplication.run(JetcacheLearnApplication.class, args); }}Copy the code

5. Use Cases:

The User class:

package com.ghq.learn.jetcachelearn.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

/**
 * @author 
 * @date 2022/2/14 10:44 上午
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {

    private Long id;
    private String name;

}
Copy the code

UserService:

package com.ghq.learn.jetcachelearn.service;

import com.alicp.jetcache.anno.CacheType;
import com.alicp.jetcache.anno.Cached;
import com.ghq.learn.jetcachelearn.entity.User;

/**
 * @author 
 * @date 2022/2/14 10:43 上午
 */
public interface UserService {

    @Cached(expire = 10,cacheType = CacheType.LOCAL)
    User getUser(Long id);
}
Copy the code

UserSercviceImpl

package com.ghq.learn.jetcachelearn.service.impl; import com.ghq.learn.jetcachelearn.entity.User; import com.ghq.learn.jetcachelearn.service.UserService; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; /** * @author * @date 2022/2/14 10:43 aM */ @slf4j @service public class UserServiceImpl implements UserService { @Override public User getUser(Long id) { log.info("userId {}", id); return new User(id, "" + id); }}Copy the code

Testing:

package com.ghq.learn.jetcachelearn; import com.ghq.learn.jetcachelearn.service.UserService; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import javax.annotation.Resource; @SpringBootTest class JetcacheLearnApplicationTests { @Resource UserService userService; @Test void contextLoads() { } @Test void testUser(){ userService.getUser(1L); userService.getUser(1L); }}Copy the code

6. Precautions:

  1. Cache entities must be serialized

  2. Package conflicts may occur during use:

    1. Solutions:

    Jetcache-starter-redis depends on redis 2.9.0, Maven package may print 3.7.x at this time startup error, modify the POM file to forcibly specify redis version

    Clients </groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency> <dependency> <groupId>com.alicp.jetcache</groupId> <artifactId>jetcache-starter-redis</artifactId> Clients </groupId> <artifactId>jedis</artifactId> </exclusion> </exclusions> </dependency>Copy the code

Source code analysis:

Related links:

JetCache source address: github.com/alibaba/jet…

I. Initialization process

1. Automatic assembly entrance:

jetcache-autoconfigure

spring.factorys

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alicp.jetcache.autoconfigure.JetCacheAutoConfiguration
Copy the code

2, JetCacheAutoConfiguration:

Function:

  1. Initialize _globalCacheConfig to generate the Bean –GlobalCacheConfig;
  2. Import Different types of AutoConfiguration correspond to different cache modes. – AutoConfigurationg corresponds to inheritance diagrams.

  1. Each AutoConfiguration generates a different CacheBuilder, stored in AutoConfigureBeans.

    
    package com.alicp.jetcache.autoconfigure;
    
    import com.alicp.jetcache.AbstractCacheBuilder;
    import com.alicp.jetcache.CacheBuilder;
    import com.alicp.jetcache.anno.support.ConfigProvider;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.InitializingBean;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.core.env.ConfigurableEnvironment;
    import org.springframework.util.Assert;
    
    import java.util.*;
    
    /**
     * Created on 2016/11/29.
     *
     * @author <a href="mailto:[email protected]">huangli</a>
     */
    public abstract class AbstractCacheAutoInit implements InitializingBean {
    
        private static Logger logger = LoggerFactory.getLogger(AbstractCacheAutoInit.class);
    
        @Autowired
        protected ConfigurableEnvironment environment;
    
        @Autowired
        protected AutoConfigureBeans autoConfigureBeans;
    
        @Autowired
        protected ConfigProvider configProvider;
    
        protected String[] typeNames;
    
        private boolean inited = false;
    
        public AbstractCacheAutoInit(String... cacheTypes) {
            Objects.requireNonNull(cacheTypes,"cacheTypes can't be null");
            Assert.isTrue(cacheTypes.length > 0."cacheTypes length is 0");
            this.typeNames = cacheTypes;
        }
    
        @Override
        public void afterPropertiesSet(a) {
            if(! inited) {synchronized (this) {
                    if(! inited) { process("jetcache.local.", autoConfigureBeans.getLocalCacheBuilders(), true);
                        process("jetcache.remote.", autoConfigureBeans.getRemoteCacheBuilders(), false);
                        inited = true; }}}}private void process(String prefix, Map cacheBuilders, boolean local) {
            ConfigTree resolver = new ConfigTree(environment, prefix);
            Map<String, Object> m = resolver.getProperties();
            Set<String> cacheAreaNames = resolver.directChildrenKeys();
            for (String cacheArea : cacheAreaNames) {
                final Object configType = m.get(cacheArea + ".type");
                boolean match = Arrays.stream(typeNames).anyMatch((tn) -> tn.equals(configType));
                if(! match) {continue;
                }
                ConfigTree ct = resolver.subTree(cacheArea + ".");
                logger.info("init cache area {} , type= {}", cacheArea, typeNames[0]);
                CacheBuilder c = initCache(ct, local ? "local." + cacheArea : "remote." + cacheArea);
    	// Put the CacheBuilder generated by the subclass into autoConfigureBeanscacheBuilders.put(cacheArea, c); }}protected void parseGeneralConfig(CacheBuilder builder, ConfigTree ct) {
            AbstractCacheBuilder acb = (AbstractCacheBuilder) builder;
            acb.keyConvertor(new FunctionWrapper<>(() -> configProvider.parseKeyConvertor(ct.getProperty("keyConvertor"))));
    
            String expireAfterWriteInMillis = ct.getProperty("expireAfterWriteInMillis");
            if (expireAfterWriteInMillis == null) {
                / / compatible with 2.1
                expireAfterWriteInMillis = ct.getProperty("defaultExpireInMillis");
            }
            if(expireAfterWriteInMillis ! =null) {
                acb.setExpireAfterWriteInMillis(Long.parseLong(expireAfterWriteInMillis));
            }
    
            String expireAfterAccessInMillis = ct.getProperty("expireAfterAccessInMillis");
            if(expireAfterAccessInMillis ! =null) { acb.setExpireAfterAccessInMillis(Long.parseLong(expireAfterAccessInMillis)); }}protected abstract CacheBuilder initCache(ConfigTree ct, String cacheAreaWithPrefix);
    }
    Copy the code

Code:

package com.alicp.jetcache.autoconfigure;

import com.alicp.jetcache.anno.support.GlobalCacheConfig;
import com.alicp.jetcache.anno.support.SpringConfigProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

/**
 * Created on 2016/11/17.
 *
 * @author <a href="mailto:[email protected]">huangli</a>
 */
@Configuration
@ConditionalOnClass(GlobalCacheConfig.class)
@ConditionalOnMissingBean(GlobalCacheConfig.class)
@EnableConfigurationProperties(JetCacheProperties.class)
// IMport is initialized for different types of cache configurations
@Import({RedisAutoConfiguration.class, CaffeineAutoConfiguration.class, MockRemoteCacheAutoConfiguration.class, LinkedHashMapAutoConfiguration.class, RedisLettuceAutoConfiguration.class, RedisSpringDataAutoConfiguration.class})
public class JetCacheAutoConfiguration {

    public static final String GLOBAL_CACHE_CONFIG_NAME = "globalCacheConfig";

    private SpringConfigProvider _springConfigProvider = new SpringConfigProvider();

    private AutoConfigureBeans _autoConfigureBeans = new AutoConfigureBeans();

    private GlobalCacheConfig _globalCacheConfig;

    @Bean
    @ConditionalOnMissingBean
    public SpringConfigProvider springConfigProvider(a) {
        return _springConfigProvider;
    }

    @Bean
    public AutoConfigureBeans autoConfigureBeans(a) {
        return _autoConfigureBeans;
    }

    @Bean
    public static BeanDependencyManager beanDependencyManager(a){
        return new BeanDependencyManager();
    }

    @Bean(name = GLOBAL_CACHE_CONFIG_NAME)
    public GlobalCacheConfig globalCacheConfig(AutoConfigureBeans autoConfigureBeans, JetCacheProperties props) {
        if(_globalCacheConfig ! =null) {
            return _globalCacheConfig;
        }
        _globalCacheConfig = new GlobalCacheConfig();
        _globalCacheConfig.setHiddenPackages(props.getHiddenPackages());
        _globalCacheConfig.setStatIntervalMinutes(props.getStatIntervalMinutes());
        _globalCacheConfig.setAreaInCacheName(props.isAreaInCacheName());
        _globalCacheConfig.setPenetrationProtect(props.isPenetrationProtect());
        _globalCacheConfig.setEnableMethodCache(props.isEnableMethodCache());
        _globalCacheConfig.setLocalCacheBuilders(autoConfigureBeans.getLocalCacheBuilders());
        _globalCacheConfig.setRemoteCacheBuilders(autoConfigureBeans.getRemoteCacheBuilders());
        return_globalCacheConfig; }}Copy the code

2. Cache parsing

Cache is the abstract interface at the top level of jetCache. Each type of Cache corresponds to a subclass implementation.

Different subclasses of Cache correspond to different CacheBuilder generators, with an eye on MultiLevelCacheBuilder.

As you can see from the above use case, JetCache adds annotations (@cached, @cachedupdate, etc.) to the interface or implementation interface, and caching takes effect automatically. As you can imagine, JetCache enhances the method using SpringAOP, the AOP code is not shown here. Look directly at the key logic related to caching

AOP – related code classes are listed here for those interested

CacheAdvisor advisor –

CachePointcut – pointCut

JetCacheInterceptor – The real aspect enhancement logic is here

JetCacheProxyConfiguration – spring config class

Cache logic (only the key code is pasted) :

#JetCacheInterceptor
@Override
    public Object invoke(final MethodInvocation invocation) throws Throwable {
        if (configProvider == null) {
            configProvider = applicationContext.getBean(ConfigProvider.class);
        }
        if(configProvider ! =null && globalCacheConfig == null) {
            globalCacheConfig = configProvider.getGlobalCacheConfig();
        }
        if (globalCacheConfig == null| |! globalCacheConfig.isEnableMethodCache()) {return invocation.proceed();
        }

        Method method = invocation.getMethod();
        Object obj = invocation.getThis();
        CacheInvokeConfig cac = null;
        if(obj ! =null) {
            String key = CachePointcut.getKey(method, obj.getClass());
            cac  = cacheConfigMap.getByMethodInfo(key);
        }

        if (cac == null || cac == CacheInvokeConfig.getNoCacheInvokeConfigInstance()) {
            return invocation.proceed();
        }

				
				// From here, IDEA follows the code to the final implementation logic
        CacheInvokeContext context = configProvider.getCacheContext().createCacheInvokeContext(cacheConfigMap);
        context.setTargetObject(invocation.getThis());
        context.setInvoker(invocation::proceed);
        context.setMethod(method);
        context.setArgs(invocation.getArguments());
        context.setCacheInvokeConfig(cac);
        context.setHiddenPackages(globalCacheConfig.getHiddenPackages());
        return CacheHandler.invoke(context);
    }
# CacheContext
public CacheInvokeContext createCacheInvokeContext(ConfigMap configMap) {
        CacheInvokeContext c = newCacheInvokeContext();
        c.setCacheFunction((invokeContext, cacheAnnoConfig) -> {
            Cache cache = cacheAnnoConfig.getCache();
            if (cache == null) {
                if (cacheAnnoConfig instanceof CachedAnnoConfig) {
                    cache = createCacheByCachedConfig((CachedAnnoConfig) cacheAnnoConfig, invokeContext);
                } else if ((cacheAnnoConfig instanceof CacheInvalidateAnnoConfig) || (cacheAnnoConfig instanceof CacheUpdateAnnoConfig)) {
                    CacheInvokeConfig cacheDefineConfig = configMap.getByCacheName(cacheAnnoConfig.getArea(), cacheAnnoConfig.getName());
                    if (cacheDefineConfig == null) {
                        String message = "can't find @Cached definition with area=" + cacheAnnoConfig.getArea()
                                + " name=" + cacheAnnoConfig.getName() +
                                ", specified in " + cacheAnnoConfig.getDefineMethod();
                        CacheConfigException e = new CacheConfigException(message);
                        logger.error("Cache operation aborted because can't find @Cached definition", e);
                        return null;
                    }
                    cache = createCacheByCachedConfig(cacheDefineConfig.getCachedAnnoConfig(), invokeContext);
                }
                cacheAnnoConfig.setCache(cache);
            }
            return cache;
        });
        return c;
    }
#CacheHandler
private static Object invokeWithCached(CacheInvokeContext context)
            throws Throwable {
        CacheInvokeConfig cic = context.getCacheInvokeConfig();
        CachedAnnoConfig cac = cic.getCachedAnnoConfig();
	/ / here apply BiFunction. See the implementation of the logic CacheContext createCacheInvokeContext
        Cache cache = context.getCacheFunction().apply(context, cac);

        if (cache == null) {
            logger.error("no cache with name: " + context.getMethod());
            return invokeOrigin(context);
        }

        Object key = ExpressionUtil.evalKey(context, cic.getCachedAnnoConfig());
        if (key == null) {
            return loadAndCount(context, cache, key);
        }

        if(! ExpressionUtil.evalCondition(context, cic.getCachedAnnoConfig())) {return loadAndCount(context, cache, key);
        }

        try {
            CacheLoader loader = new CacheLoader() {
                @Override
                public Object load(Object k) throws Throwable {
                    Object result = invokeOrigin(context);
                    context.setResult(result);
                    return result;
                }

                @Override
                public boolean vetoCacheUpdate(a) {
                    return !ExpressionUtil.evalPostCondition(context, cic.getCachedAnnoConfig());
                }
            };
            Object result = cache.computeIfAbsent(key, loader);
            return result;
        } catch (CacheInvokeException e) {
            throw e.getCause();
        }
    }
# AbstractCache
static <K, V> V computeIfAbsentImpl(K key, Function<K, V> loader, boolean cacheNullWhenLoaderReturnNull,
                                               long expireAfterWrite, TimeUnit timeUnit, Cache<K, V> cache) {
        AbstractCache<K, V> abstractCache = CacheUtil.getAbstractCache(cache);
        CacheLoader<K, V> newLoader = CacheUtil.createProxyLoader(cache, loader, abstractCache::notify);
        CacheGetResult<V> r;
	// @cacherefresh takes this piece
        if (cache instanceof RefreshCache) {
            RefreshCache<K, V> refreshCache = ((RefreshCache<K, V>) cache);
            r = refreshCache.GET(key);
            refreshCache.addOrUpdateRefreshTask(key, newLoader);
        } else {
            r = cache.GET(key);
        }
        if (r.isSuccess()) {
            return r.getValue();
        } else {
            Consumer<V> cacheUpdater = (loadedValue) -> {
                if(needUpdate(loadedValue, cacheNullWhenLoaderReturnNull, newLoader)) {
                    if(timeUnit ! =null) {
                        cache.PUT(key, loadedValue, expireAfterWrite, timeUnit).waitForResult();
                    } else{ cache.PUT(key, loadedValue).waitForResult(); }}}; V loadedValue;if (cache.config().isCachePenetrationProtect()) {
                loadedValue = synchronizedLoad(cache.config(), abstractCache, key, newLoader, cacheUpdater);
            } else {
                loadedValue = newLoader.apply(key);
		// In producer consumer mode, call the cache.PUT method above
                cacheUpdater.accept(loadedValue);
            }

            returnloadedValue; }} # RedisCache = RedisCache@Override
    protected CacheResult do_PUT(K key, V value, long expireAfterWrite, TimeUnit timeUnit) {
        try (Jedis jedis = config.getJedisPool().getResource()) {
            CacheValueHolder<V> holder = new CacheValueHolder(value, timeUnit.toMillis(expireAfterWrite));
            byte[] newKey = buildKey(key);
            String rt = jedis.psetex(newKey, timeUnit.toMillis(expireAfterWrite), valueEncoder.apply(holder));
            if ("OK".equals(rt)) {
                return CacheResult.SUCCESS_WITHOUT_MSG;
            } else {
                return newCacheResult(CacheResultCode.FAIL, rt); }}catch (Exception ex) {
            logError("PUT", key, ex);
            return newCacheResult(ex); }}Copy the code

other

  • Jetcache-anno-api: Defines jetcache annotations and constants without passing dependencies. If you want to add Cached annotations to your interface and don’t want your interface JAR to pass too many dependencies, you can make the jar rely on jetcache-anno-API.
  • Jetcache-core: Core API that is configured entirely programmaticallyCacheDo not rely on Spring. Two in-memory cache implementationsLinkedHashMapCacheandCaffeineCacheAlso provided by it.
  • Jetcache-anno: Support for @cached and @createcache annotations based on Spring.
  • Jetcache-redis: Use Jedis to provide redis support.
  • Jetcache-redis-lettuce (JetCache2.3 or above) : Use the redis support to implement an interface for asynchronous access to the cache by jetcache.
  • Jetcache-starter-redis: starter in Spring Boot mode, based on Jedis.
  • Jetcache-starter-redis-lettuce (JetCache2.3 or above) : Starter in Spring Boot mode, based on lettuce.