JetCache源码解析——缓存处理

在Java技术体系中,如果想要在不改变已有代码逻辑的情况下,对已有的函数进行功能增强,一般可以使用两种方式,如AOP(Aspect Oriented Programming),即面向切面编程,以及代理模式,如使用JDK 动态代理或 CGLIB 动态代理。例如Mybatis的源码中同时使用了JDK 动态代理或 CGLIB 动态代理,但是对于Mapper接口是使用JDK动态代理模式。在JetCache则使用AOP的方式,在spring环境下,使用@Cached注解可以为一个方法添加缓存,@CacheUpdate用于更新缓存,@CacheInvalidate用于移除缓存元素。注解可以加在接口上也可以加在类上,加注解的类必须是一个spring bean,例如:

public interface UserService {
    @Cached(name="userCache.", key="#userId", expire = 3600)
    User getUserById(long userId);

    @CacheUpdate(name="userCache.", key="#user.userId", value="#user")
    void updateUser(User user);

    @CacheInvalidate(name="userCache.", key="#userId")
    void deleteUser(long userId);
}

Spring中AOP代理由Spring的IOC容器负责生成、管理,其依赖关系也由IOC容器负责管理。因此,AOP代理可以直接使用容器中的其它bean实例作为目标,这种关系可由IOC容器的依赖注入提供。Spring创建代理的规则为:

  1. 默认使用Java动态代理来创建AOP代理,这样就可以为任何接口实例创建代理了
  2. 当需要代理的类不是代理接口的时候,Spring会切换为使用CGLIB代理,也可强制使用CGLIB

CacheAdvisor类 

切面选择AbstractBeanFactoryPointcutAdvisor实现,切点是通过StaticMethodMatcherPointcut匹配包含Cached、CacheUpdate和CacheInvalidate注解的方法。通过实现MethodInterceptor接口实现操作相应函数的增强逻辑。CacheAdvisor是AbstractBeanFactoryPointcutAdvisor的子类,源码如下:

/**
 * 缓存顾问类,继承自AbstractBeanFactoryPointcutAdvisor。
 */
public class CacheAdvisor extends AbstractBeanFactoryPointcutAdvisor {

    /**
     * 缓存顾问的bean名称。
     */
    public static final String CACHE_ADVISOR_BEAN_NAME = "jetcache2.internalCacheAdvisor";

    @Autowired
    private ConfigMap cacheConfigMap; // 注入缓存配置映射

    private String[] basePackages; // 基础包路径

    /**
     * 获取Pointcut对象。
     *
     * @return Pointcut对象
     */
    @Override
    public Pointcut getPointcut() {
        CachePointcut pointcut = new CachePointcut(basePackages); // 创建CachePointcut对象
        pointcut.setCacheConfigMap(cacheConfigMap); // 设置CachePointcut的缓存配置映射
        return pointcut;
    }

    /**
     * 设置缓存配置映射。
     *
     * @param cacheConfigMap 缓存配置映射
     */
    public void setCacheConfigMap(ConfigMap cacheConfigMap) {
        this.cacheConfigMap = cacheConfigMap;
    }

    /**
     * 设置基础包路径。
     *
     * @param basePackages 基础包路径
     */
    public void setBasePackages(String[] basePackages) {
        this.basePackages = basePackages;
    }
}

在上面的类中会设置JetCache扫描的基础包路径,缓存配置信息。并实现了PointcutAdvisor接口的getPointcut()函数,PointcutAdvisor接口是Advisor的子接口,Advisor是Spring AOP的顶层抽象,用来管理Advice和Pointcut,所以毫无疑问PointcutAdvisor接口也是用来管理Advice和Pointcut的。PointcutAdvisor源码如下:

/**
 * 所有由切点驱动的Advisor的超接口。
 * 这涵盖了几乎所有除了引入Advisor之外的Advisor,
 * 对于方法级别的匹配不适用。
 *
 * @author Rod Johnson
 */
public interface PointcutAdvisor extends Advisor {

    /**
     * 获取驱动该顾问的Pointcut。
     */
    Pointcut getPointcut();

}

CachePointcut类

在上面的CacheAdvisor类中实现了PointcutAdvisor接口的getPointcut()函数,并创建了CachePointcut类的实例。接下来,我们需要先看一下CachePointcut的类图。

 Pointcut接口

Pointcut接口有两个接口方法,分别用于加载ClassFilter和MethodMatcher接口实例,并通过这两个实例实现切入点的逻辑功能。

/**
 * 核心Spring切点抽象类。
 *
 * <p>切点由{@link ClassFilter}和{@link MethodMatcher}组成。
 * 这些基本概念和一个Pointcut可以通过{@link org.springframework.aop.support.ComposablePointcut}组合起来。
 *
 * @author Rod Johnson
 * @see ClassFilter
 * @see MethodMatcher
 * @see org.springframework.aop.support.Pointcuts
 * @see org.springframework.aop.support.ClassFilters
 * @see org.springframework.aop.support.MethodMatchers
 */
public interface Pointcut {

    /**
     * 返回该点切的ClassFilter。
     * @return ClassFilter(永远不会为null)
     */
    ClassFilter getClassFilter();

    /**
     * 返回该点切的MethodMatcher。
     * @return MethodMatcher(永远不会为null)
     */
    MethodMatcher getMethodMatcher();

    /**
     * 常规Pointcut实例,始终匹配。
     */
    Pointcut TRUE = TruePointcut.INSTANCE;

}

 ClassFilter接口

ClassFilter的matches方法定义判断某个类是否需要被纳入切面,源码如下:

/**
 * 过滤器,用于限制目标类集中对切点或引入的匹配。
 *
 * <p>可用于作为{@link Pointcut}的一部分或针对整个{@link IntroductionAdvisor}的目标。
 *
 * <p>此类的具体实现通常应提供适当的{@link Object#equals(Object)}和{@link Object#hashCode()}
 * 的实现,以便允许在缓存场景中使用过滤器 - 例如,在由CGLIB生成的代理中。
 *
 * @author 罗德·约翰逊
 * @see Pointcut
 * @see MethodMatcher
 */
@FunctionalInterface
public interface ClassFilter {

    /**
     * 是否应应用切点到给定的接口或目标类?
     * @param clazz 候选目标类
     * @return 是否应应用给定的目标类
     */
    boolean matches(Class<?> clazz);


    /**
     * 匹配所有类的类过滤器的规范实例。
     */
    ClassFilter TRUE = TrueClassFilter.INSTANCE;
}

MethodMatcher接口 

 MethodMatcher的matches方法则是定义判断某个方法是否需要被纳入切面。MethodMatcher源码如下:

/**
 * 是`Pointcut`的一部分:用于检查目标方法是否有资格应用建议。
 *
 * <p>MethodMatcher可以静态地(`statically`)或动态地(`dynamically`)进行评估(`evaluated`)。
 * 静态匹配涉及方法和(可能)方法属性。动态匹配还提供了特定调用的参数,并且在运行Pointcut上的先前建议时产生的任何效果。
 *
 * <p>如果实现从其`#isRuntime()`方法返回`false`,则可以进行静态评估,并且此方法的所有调用的参数将相同,无论其参数如何。这意味着如果`#isRuntime()`方法返回`false`,则3个参数的`#matches(java.lang.reflect.Method, Class, Object[])`方法将永远不会被调用。
 *
 * <p>如果实现从其2个参数的`#matches(java.lang.reflect.Method, Class)`方法返回`true`并且`#isRuntime()`方法返回`true`,则会在每个相关建议的潜在执行之前立即调用3个参数的`#matches(java.lang.reflect.Method, Class, Object[])`方法,以决定是否应运行建议。之前运行的所有建议,例如拦截器链中的早期拦截器,都已经运行,因此他们在参数或线程局部状态中产生的任何状态更改都将可用。
 *
 * <p>此接口的 concrete 实现通常应适当地提供`Object#equals(Object)`和`Object#hashCode()`,以便允许matcher用作缓存场景中的matcher——例如,在由CGLIB生成的代理中。
 *
 * @author Rod Johnson
 * @since 11.11.2003
 * @see Pointcut
 * @see ClassFilter
 */
public interface MethodMatcher {

    /**
     * 执行静态检查,以确定给定方法是否匹配。
     * <p>如果此方法返回`false`或`#isRuntime()`方法返回`false`,则不会进行运行时检查(即不会调用`#matches(java.lang.reflect.Method, Class, Object[])`)。
     * @param method 候选方法
     * @param targetClass 目标类
     * @return 如果此方法静态匹配,则返回true
     */
    boolean matches(Method method, Class<?> targetClass);

    /**
     * 是否为MethodMatcher动态,即必须在2个参数的matches方法返回true的情况下,最终调用`#matches(java.lang.reflect.Method, Class, Object[])`方法进行运行时检查?
     * <p>可以在创建AOP代理时调用,无需在每次方法调用之前再次调用。
     * @return 如果静态匹配通过,则在3个参数的`#matches(java.lang.reflect.Method, Class, Object[])`方法上的运行时匹配是否需要
     */
    boolean isRuntime();

    /**
     * 检查此方法是否有运行时(动态)匹配,该方法必须静态匹配。
     * <p>仅在2个参数的matches方法针对给定方法和目标类返回true,并且`#isRuntime()`方法返回true时调用该方法。在建议运行之前立即调用,之前在建议链中的任何早期建议已经运行。
     * @param method 候选方法
     * @param targetClass 目标类
     * @param args 方法的方法
     * @return 是否存在运行时匹配
     * @see MethodMatcher#matches(Method, Class)
     */
    boolean matches(Method method, Class<?> targetClass, Object... args);


    /**
     * 与所有方法都匹配的规范实例。
     */
    MethodMatcher TRUE = TrueMethodMatcher.INSTANCE;

}

在上面的接口中分别支持对类的匹配和函数的匹配,那我们先看一下CachePointcut中对于类的匹配,源码如下:

public class CachePointcut extends StaticMethodMatcherPointcut implements ClassFilter {

    private static final Logger logger = LoggerFactory.getLogger(CachePointcut.class);

    private ConfigMap cacheConfigMap;
    private String[] basePackages;

    /**
     * 构造函数
     *
     * @param basePackages 基础包名数组
     */
    public CachePointcut(String[] basePackages) {
        setClassFilter(this);
        this.basePackages = basePackages;
    }

    /**
     * 判断类是否匹配
     *
     * @param clazz 类对象
     * @return 是否匹配
     */
    @Override
    public boolean matches(Class clazz) {
        boolean b = matchesImpl(clazz);
        logger.trace("check class match {}: {}", b, clazz);
        return b;
    }

    /**
     * 实现类是否匹配
     *
     * @param clazz 类对象
     * @return 是否匹配
     */
    private boolean matchesImpl(Class clazz) {
        if (matchesThis(clazz)) {
            return true;
        }
        Class[] cs = clazz.getInterfaces();
        if (cs != null) {
            for (Class c : cs) {
                if (matchesImpl(c)) {
                    return true;
                }
            }
        }
        if (!clazz.isInterface()) {
            Class sp = clazz.getSuperclass();
            if (sp != null && matchesImpl(sp)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 判断类是否匹配
     *
     * @param clazz 类对象
     * @return 是否匹配
     */
    public boolean matchesThis(Class clazz) {
        String name = clazz.getName();
        if (exclude(name)) {
            return false;
        }
        return include(name);
    }

    /**
     * 判断类是否包含在基础包名数组中
     *
     * @param name 类名
     * @return 是否包含
     */
    private boolean include(String name) {
        if (basePackages != null) {
            for (String p : basePackages) {
                if (name.startsWith(p)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 判断类是否排除
     *
     * @param name 类名
     * @return 是否排除
     */
    private boolean exclude(String name) {
        if (name.startsWith("java")) {
            return true;
        }
        if (name.startsWith("org.springframework")) {
            return true;
        }
        if (name.indexOf("$$EnhancerBySpringCGLIB$$") >= 0) {
            return true;
        }
        if (name.indexOf("$$FastClassBySpringCGLIB$$") >= 0) {
            return true;
        }
        return false;
    }

}

上面的代码逻辑还是比较简单的,主要是判断类的包名是否以配置的JetCache扫描的包路径开头,如果以配置的JetCache扫描的包路径开头就匹配上了。

接下来的matches(Method, Class)函数用于匹配对应类的相应函数,主要是带有注解Cached、CacheUpdate和CacheInvalidate的方法,源码如下:

    /**
     * 判断给定的方法是否与目标类匹配
     *
     * @param method 待判断的方法
     * @param targetClass 目标类
     * @return 如果匹配则返回true,否则返回false
     */
    @Override
    public boolean matches(Method method, Class targetClass) {
        boolean b = matchesImpl(method, targetClass);
        if (b) {
            if (logger.isDebugEnabled()) {
                logger.debug("check method match true: method={}, declaringClass={}, targetClass={}",
                        method.getName(),
                        ClassUtil.getShortClassName(method.getDeclaringClass().getName()),
                        targetClass == null ? null : ClassUtil.getShortClassName(targetClass.getName()));
            }
        } else {
            if (logger.isTraceEnabled()) {
                logger.trace("check method match false: method={}, declaringClass={}, targetClass={}",
                        method.getName(),
                        ClassUtil.getShortClassName(method.getDeclaringClass().getName()),
                        targetClass == null ? null : ClassUtil.getShortClassName(targetClass.getName()));
            }
        }
        return b;
    }

    /**
     * 实现方法匹配的逻辑
     *
     * @param method 待判断的方法
     * @param targetClass 目标类
     * @return 如果匹配则返回true,否则返回false
     */
    private boolean matchesImpl(Method method, Class targetClass) {
        if (!matchesThis(method.getDeclaringClass())) {
            return false;
        }
        if (exclude(targetClass.getName())) {
            return false;
        }
        String key = getKey(method, targetClass);
        CacheInvokeConfig cac = cacheConfigMap.getByMethodInfo(key);
        if (cac == CacheInvokeConfig.getNoCacheInvokeConfigInstance()) {
            return false;
        } else if (cac != null) {
            return true;
        } else {
            cac = new CacheInvokeConfig();
            CacheConfigUtil.parse(cac, method);

            String name = method.getName();
            Class<?>[] paramTypes = method.getParameterTypes();
            parseByTargetClass(cac, targetClass, name, paramTypes);

            if (!cac.isEnableCacheContext() && cac.getCachedAnnoConfig() == null &&
                    cac.getInvalidateAnnoConfigs() == null && cac.getUpdateAnnoConfig() == null) {
                cacheConfigMap.putByMethodInfo(key, CacheInvokeConfig.getNoCacheInvokeConfigInstance());
                return false;
            } else {
                cacheConfigMap.putByMethodInfo(key, cac);
                return true;
            }
        }
    }

在上面的matchesImpl函数中会执行如下几个步骤:

1、调用matchesThis函数判断指定的类的包名是否以配置的JetCache扫描的包路径开头,这是为了排除没有JetCache不会扫描的类。

2、调用exclude函数用于排除Java和Spring自带的类名,以及使用CGLIB动态代理生成的类。

3、根据类和函数信息生成唯一键Key,然后查询缓存cacheConfigMap中对应的缓存调用配置信息。

4、如果cacheConfigMap缺少指定类的指定函数的缓存调用配置信息,则构建CacheInvokeConfig实例,并解析函数的注解信息。

CacheConfigUtil.parse函数会解析函数上的Cached、CacheUpdate和CacheInvalidate注解上的信息。其源码如下:

    /**
     * 解析方法上的注解,并将解析结果存入CacheInvokeConfig对象
     * @param cac CacheInvokeConfig对象,用于存储解析结果
     * @param method 待解析的方法
     * @return 是否存在有效的注解
     */
    public static boolean parse(CacheInvokeConfig cac, Method method) {
        boolean hasAnnotation = false;
        CachedAnnoConfig cachedConfig = parseCached(method);
        if (cachedConfig != null) {
            cac.setCachedAnnoConfig(cachedConfig);
            hasAnnotation = true;
        }
        boolean enable = parseEnableCache(method);
        if (enable) {
            cac.setEnableCacheContext(true);
            hasAnnotation = true;
        }
        List<CacheInvalidateAnnoConfig> invalidateAnnoConfigs = parseCacheInvalidates(method);
        if (invalidateAnnoConfigs != null) {
            cac.setInvalidateAnnoConfigs(invalidateAnnoConfigs);
            hasAnnotation = true;
        }
        CacheUpdateAnnoConfig updateAnnoConfig = parseCacheUpdate(method);
        if (updateAnnoConfig != null) {
            cac.setUpdateAnnoConfig(updateAnnoConfig);
            hasAnnotation = true;
        }

        if (cachedConfig != null && (invalidateAnnoConfigs != null || updateAnnoConfig != null)) {
            throw new CacheConfigException("@Cached不能同时存在@CacheInvalidate或@CacheUpdate: " + method);
        }

        return hasAnnotation;
    }

在上面的方法中,第一步会调用函数parseCached来解析注解@Cached中的信息,源码如下:

    private static CachedAnnoConfig parseCached(Method m) {
        Cached anno = m.getAnnotation(Cached.class);
        if (anno == null) {
            return null;
        }
        CachedAnnoConfig cc = new CachedAnnoConfig();
        cc.setArea(anno.area());
        cc.setName(anno.name());
        cc.setCacheType(anno.cacheType());
        cc.setSyncLocal(anno.syncLocal());
        cc.setEnabled(anno.enabled());
        cc.setTimeUnit(anno.timeUnit());
        cc.setExpire(anno.expire());
        cc.setLocalExpire(anno.localExpire());
        cc.setLocalLimit(anno.localLimit());
        cc.setCacheNullValue(anno.cacheNullValue());
        cc.setCondition(anno.condition());
        cc.setPostCondition(anno.postCondition());
        cc.setSerialPolicy(anno.serialPolicy());
        cc.setKeyConvertor(anno.keyConvertor());
        cc.setKey(anno.key());
        cc.setDefineMethod(m);

        CacheRefresh cacheRefresh = m.getAnnotation(CacheRefresh.class);
        if (cacheRefresh != null) {
            RefreshPolicy policy = parseRefreshPolicy(cacheRefresh);
            cc.setRefreshPolicy(policy);
        }

        CachePenetrationProtect protectAnno = m.getAnnotation(CachePenetrationProtect.class);
        if (protectAnno != null) {
            PenetrationProtectConfig protectConfig = parsePenetrationProtectConfig(protectAnno);
            cc.setPenetrationProtectConfig(protectConfig);
        }

        return cc;
    }

异步调用缓存保护注解@CachePenetrationProtect和缓存刷新注解@CacheRefresh仅支持使用@Cached的函数上可用,所以JetCache就将其在一个函数中实现了,不过分开实现可能会更合理。

第二步就会调用parseEnableCache函数解析@EnableCache注解,如果相应的函数存在@EnableCache注解,则CacheInvokeConfig实例的enableCacheContext字段为true。如果在需要使用缓存的函数上加了@Cached注解后再加@EnableCache注解可能会嫌麻烦,实际上不在函数上加注解@EnableCache也能启用缓存,后面再详细介绍。

第三步开始调用parseCacheInvalidates函数解析@CacheInvalidate注解和@CacheInvalidateContainer注解,@CacheInvalidateContainer注解中可以定义多个@CacheInvalidate。而一个函数只能有一个@CacheInvalidate注解,当一个函数的修改需要将多种缓存数据清除时就可以选择@CacheInvalidateContainer注解,当然@CacheInvalidate注解中的multi可以支持清空多个key的缓存,但需要在key字段中指定获取多个删除key的表达式。源码如下:

    public static List<CacheInvalidateAnnoConfig> parseCacheInvalidates(Method m) {
        List<CacheInvalidateAnnoConfig> annoList = null;
        CacheInvalidate ci = m.getAnnotation(CacheInvalidate.class);
        if (ci != null) {
            annoList = new ArrayList<>(1);
            annoList.add(createCacheInvalidateAnnoConfig(ci, m));
        } else {
            CacheInvalidateContainer cic = m.getAnnotation(CacheInvalidateContainer.class);
            if (cic != null) {
                CacheInvalidate[] cacheInvalidates = cic.value();
                annoList = new ArrayList<>(cacheInvalidates.length);
                for (CacheInvalidate cacheInvalidate : cacheInvalidates) {
                    annoList.add(createCacheInvalidateAnnoConfig(cacheInvalidate, m));
                }
            }
        }
        return annoList;
    }

第四步就是调用parseCacheUpdate函数解析@CacheUpdate注解,源码如下:


    private static CacheUpdateAnnoConfig parseCacheUpdate(Method m) {
        CacheUpdate anno = m.getAnnotation(CacheUpdate.class);
        if (anno == null) {
            return null;
        }
        CacheUpdateAnnoConfig cc = new CacheUpdateAnnoConfig();
        cc.setArea(anno.area());
        cc.setName(anno.name());
        if (cc.getName() == null || cc.getName().trim().equals("")) {
            throw new CacheConfigException("name is required for @CacheUpdate: " + m.getClass().getName() + "." + m.getName());
        }
        cc.setKey(anno.key());
        cc.setValue(anno.value());
        if (cc.getValue() == null || cc.getValue().trim().equals("")) {
            throw new CacheConfigException("value is required for @CacheUpdate: " + m.getClass().getName() + "." + m.getName());
        }
        cc.setCondition(anno.condition());
        cc.setMulti(anno.multi());
        cc.setDefineMethod(m);
        return cc;
    }

5、调用parseByTargetClass函数获取指定类的父类或接口上的缓存配置信息。源码如下:


    private void parseByTargetClass(CacheInvokeConfig cac, Class<?> clazz, String name, Class<?>[] paramTypes) {
        if (!clazz.isInterface() && clazz.getSuperclass() != null) {
            parseByTargetClass(cac, clazz.getSuperclass(), name, paramTypes);
        }
        Class<?>[] intfs = clazz.getInterfaces();
        for (Class<?> it : intfs) {
            parseByTargetClass(cac, it, name, paramTypes);
        }

        boolean matchThis = matchesThis(clazz);
        if (matchThis) {
            Method[] methods = clazz.getDeclaredMethods();
            for (Method method : methods) {
                if (methodMatch(name, method, paramTypes)) {
                    CacheConfigUtil.parse(cac, method);
                    break;
                }
            }
        }
    }

6、将上面解析到缓存函数配置信息保存到缓存cacheConfigMap中。如果函数带有Cached、CacheUpdate和CacheInvalidate等注解时matchesImpl函数就返回true,否则返回false。

至此,针对带有Cached、CacheUpdate和CacheInvalidate等注解的函数就可以纳入到Spring的切面,当对应的函数被调用时就会调用CacheAdvisor类的getAdvice()函数获取对应的切面Advice,接下来就是要定义一个Advice增强器用于提供加载缓存、更新缓存和删除缓存数据的能力。

JetCacheInterceptor类

MethodInterceptor是AOP项目中的拦截器,它拦截的目标是方法。实现MethodInterceptor拦截器大致也分为两种,一种是实现MethodInterceptor接口,另一种利用AspectJ的注解或配置。JetCache采用的是使用JetCacheInterceptor类来实现MethodInterceptor接口。源码如下:

/**
 * @author huangli
 */
public class JetCacheInterceptor implements MethodInterceptor, ApplicationContextAware {

    private static final Logger logger = LoggerFactory.getLogger(JetCacheInterceptor.class);

    @Autowired
    private ConfigMap cacheConfigMap;  // 缓存配置映射

    private ApplicationContext applicationContext;  // 上下文对象

    private GlobalCacheConfig globalCacheConfig;  // 全局缓存配置

    ConfigProvider configProvider;  // 配置提供者

    CacheManager cacheManager;  // 缓存管理器

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;  // 设置上下文对象
    }

    @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();  // 执行目标方法
        }
        if (cacheManager == null) {
            cacheManager = applicationContext.getBean(CacheManager.class);  // 获取缓存管理器
            if (cacheManager == null) {
                logger.error("There is no cache manager instance in spring context");  // 输出错误日志
                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(logger.isTraceEnabled()){
            logger.trace("JetCacheInterceptor invoke. foundJetCacheConfig={}, method={}.{}(), targetClass={}",
                    cac != null,
                    method.getDeclaringClass().getName(),
                    method.getName(),
                    invocation.getThis() == null ? null : invocation.getThis().getClass().getName());
        }
        */

        if (cac == null || cac == CacheInvokeConfig.getNoCacheInvokeConfigInstance()) {
            return invocation.proceed();  // 执行目标方法
        }

        CacheInvokeContext context = configProvider.newContext(cacheManager).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);  // 执行缓存处理
    }

    public void setCacheConfigMap(ConfigMap cacheConfigMap) {
        this.cacheConfigMap = cacheConfigMap;  // 设置缓存配置映射
    }

}

在上面的JetCacheInterceptor类中,cacheConfigMap会缓存每个缓存实例的配置信息,就是是通过上面解析函数中带有的@Cached、@CacheUpdate和@CacheInvalidate等注解中定义的配置信息。JetCacheInterceptor类的invoke函数主要实现如下几个步骤:

1、获取全局缓存配置实例globalCacheConfig,如果全局配置中配置了不启用缓存,就执行目标方法。启用缓存的全局配置就是在例如Application类上加一个@EnableMethodCache注解,设置一下JetCache扫描的包名前缀。

2、获取MethodInvocation类型参数的目标方法和目标对象信息,然后根据目标类和目标方法获取目标方法的缓存配置信息。如果目标方法不存在缓存配置信息,即没有@Cached、@CacheUpdate或@CacheInvalidate注解,则执行执行目标方法。源码如下:

        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();  // 执行目标方法
        }

3、构建缓存执行上下文,执行CacheHandler类的静态方法invoke(CacheInvokeContext),源码如下:

        CacheInvokeContext context = configProvider.newContext(cacheManager).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);  // 执行缓存处理

在进一步分析 CacheHandler类的静态方法invoke之前,我们需要看一下JetCache是如何创建JetCacheInterceptor和CacheAdvisor的Bean,实现逻辑很简单,就是在JetCacheProxyConfiguration中直接实现的,源码如下:

    /**
     * 创建一个名为CACHE_ADVISOR_BEAN_NAME的CacheAdvisor Bean,并设置其角色为BeanDefinition.ROLE_INFRASTRUCTURE。
     * 使用JetCacheInterceptor创建CacheAdvisor的缓存拦截器。
     * 设置CacheAdvisor的缓存拦截器为jetCacheInterceptor。
     * 设置CacheAdvisor的基包为enableMethodCache.getStringArray("basePackages")。
     * 设置CacheAdvisor的顺序为enableMethodCache.getNumber("order")。
     * 返回创建的CacheAdvisor Bean。
     */
    @Bean(name = CacheAdvisor.CACHE_ADVISOR_BEAN_NAME)
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public CacheAdvisor jetcacheAdvisor(JetCacheInterceptor jetCacheInterceptor) {
        CacheAdvisor advisor = new CacheAdvisor();
        advisor.setAdvice(jetCacheInterceptor);
        advisor.setBasePackages(this.enableMethodCache.getStringArray("basePackages"));
        advisor.setOrder(this.enableMethodCache.<Integer>getNumber("order"));
        return advisor;
    }

    /**
     * 创建一个名为jetCacheInterceptor的JetCacheInterceptor Bean,并设置其角色为BeanDefinition.ROLE_INFRASTRUCTURE。
     * 返回创建的JetCacheInterceptor Bean。
     */
    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public JetCacheInterceptor jetCacheInterceptor() {
        return new JetCacheInterceptor();
    }

至此,当目标函数被调用时,Spring就会执行JetCacheInterceptor类的invoke函数,接下来,我们就需要针对CacheHandler类的静态方法invoke(CacheInvokeContext)做进一步的分析。源码如下:

    public static Object invoke(CacheInvokeContext context) throws Throwable {
        if (context.getCacheInvokeConfig().isEnableCacheContext()) {
            try {
                CacheContextSupport._enable();
                return doInvoke(context);
            } finally {
                CacheContextSupport._disable();
            }
        } else {
            return doInvoke(context);
        }
    }

上面的方法会首先判断缓存执行上下文是否启用缓存上下文,只有目标函数上带有@EnableCache注解时,context .getCacheInvokeConfig( ).isEnableCacheContext()才会返回true。当启用缓存上下文时就会在函数doInvoke执行前后分别调用CacheContextSupport类的_enable()和_disable()方法。否则就会直接调用doInvoke函数继续进行处理,doInvoke源码如下:

private static Object doInvoke(CacheInvokeContext context) throws Throwable {
        CacheInvokeConfig cic = context.getCacheInvokeConfig();
        CachedAnnoConfig cachedConfig = cic.getCachedAnnoConfig();
        if (cachedConfig != null && (cachedConfig.isEnabled() || CacheContextSupport._isEnabled())) {
            return invokeWithCached(context);
        } else if (cic.getInvalidateAnnoConfigs() != null || cic.getUpdateAnnoConfig() != null) {
            return invokeWithInvalidateOrUpdate(context);
        } else {
            return invokeOrigin(context);
        }
    }

invokeWithCached

如果目标函数的缓存调用配置实例不为空,即目标方法有@Cached注解,且开启缓存就会调用invokeWithCached方法。注意,CachedAnnoConfig中的isEnabled()函数是取自@Cached注解的enabled,默认为true。方法invokeWithCached的源码如下:

    /**
     * 使用缓存调用方法
     *
     * @param context 缓存调用上下文
     * @return 方法返回值
     * @throws Throwable 可抛出异常
     */
    private static Object invokeWithCached(CacheInvokeContext context) throws Throwable {
        CacheInvokeConfig cic = context.getCacheInvokeConfig();  // 获取缓存调用配置
        CachedAnnoConfig cac = cic.getCachedAnnoConfig();  // 获取缓存注解配置
        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());  // 根据表达式计算缓存的key
        if (key == null) {  // 判断key是否为空
            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() {  // 判断是否禁止缓存更新
                    return !ExpressionUtil.evalPostCondition(context, cic.getCachedAnnoConfig());  // 根据表达式判断是否禁止缓存更新
                }
            };
            Object result = cache.computeIfAbsent(key, loader);  // 根据key计算缓存结果
            return result;  // 返回缓存结果
        } catch (CacheInvokeException e) {  // 捕获缓存调用异常
            throw e.getCause();  // 抛出异常原因
        }
    }

这个函数是一个私有的静态方法,用于在给定的缓存上下文中执行方法调用。它首先获取缓存配置和注解配置,然后根据配置获取缓存。如果缓存为空,它会记录错误并调用原始方法。接下来,它通过表达式计算缓存的键,并根据条件判断是否需要加载缓存。如果需要加载,它会创建一个缓存加载器,并使用该加载器计算缓存结果。最后,它会返回缓存结果。如果发生缓存调用异常,它会抛出该异常的原因。

上面的函数主要的核心逻辑概括起来,可以用下面的源码来解释:

Cache cache = context.getCacheFunction().apply(context, cac);  // 获取缓存函数并应用得到缓存
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() {  // 判断是否禁止缓存更新
       return !ExpressionUtil.evalPostCondition(context, cic.getCachedAnnoConfig());  // 根据表达式判断是否禁止缓存更新
    }
};
Object result = cache.computeIfAbsent(key, loader);  // 根据key计算缓存结果
return result;  // 返回缓存结果

上面的代码逻辑相对比较简单,我们继续看一下computeIfAbsent函数,该函数会先查询Cache对象中是否存在指定的缓存,如果不存在就会调用load函数获取数据并写入Cache对象中。computeIfAbsent函数源码如下:

    @Override
    public V computeIfAbsent(K key, Function<K, V> loader) {
        return computeIfAbsent(key, loader, config().isCacheNullValue());
    }

    @Override
    public V computeIfAbsent(K key, Function<K, V> loader, boolean cacheNullWhenLoaderReturnNull) {
        return AbstractCache.computeIfAbsentImpl(key, loader, cacheNullWhenLoaderReturnNull,
                0, null, this);
    }


    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;
        if (cache instanceof RefreshCache) {
            // 如果缓存是 RefreshCache 类型,则通过该类型的具体方法获取缓存值,并添加刷新任务
            RefreshCache<K, V> refreshCache = ((RefreshCache<K, V>) cache);
            r = refreshCache.GET(key);
            refreshCache.addOrUpdateRefreshTask(key, newLoader);
        } else {
            // 否则直接通过缓存的 GET 方法获取缓存值
            r = cache.GET(key);
        }
        if (r.isSuccess()) {
            // 如果获取成功,则返回获取到的缓存值
            return r.getValue();
        } else {
            // 如果获取失败,则根据条件执行加载和更新逻辑
            Consumer<V> cacheUpdater = (loadedValue) -> {
                // 判断是否需要更新缓存
                if(needUpdate(loadedValue, cacheNullWhenLoaderReturnNull, newLoader)) {
                    // 若需要更新,则根据过期时间和时间单位调用缓存的 PUT 方法
                    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);
                cacheUpdater.accept(loadedValue);
            }

            // 返回加载的值
            return loadedValue;
        }
    }

invokeWithInvalidateOrUpdate

如果目标函数的缓存调用配置实例不为空,即目标方法有@CacheUpdate或@CacheInvalidate等注解,且开启缓存就会调用invokeWithInvalidateOrUpdate方法。invokeWithInvalidateOrUpdate方法的源码如下:

    private static Object invokeWithInvalidateOrUpdate(CacheInvokeContext context) throws Throwable {
        Object originResult = invokeOrigin(context);
        context.setResult(originResult);
        CacheInvokeConfig cic = context.getCacheInvokeConfig();

        if (cic.getInvalidateAnnoConfigs() != null) {
            doInvalidate(context, cic.getInvalidateAnnoConfigs());
        }
        CacheUpdateAnnoConfig updateAnnoConfig = cic.getUpdateAnnoConfig();
        if (updateAnnoConfig != null) {
            doUpdate(context, updateAnnoConfig);
        }

        return originResult;
    }

上面的方法首先会执行目标函数,获取相应的结果,然后开始根据目标方法上的@CacheUpdate或@CacheInvalidate注解更新或删除缓存数据。

如果目标方法存在注解@CacheInvalidate,就会调用doInvalidate方法删除缓存数据,源码如下:

    // 清除指定缓存
    private static void doInvalidate(CacheInvokeContext context, List<CacheInvalidateAnnoConfig> annoConfig) {
        for (CacheInvalidateAnnoConfig config : annoConfig) {
            doInvalidate(context, config);
        }
    }

    // 清除单个缓存
    private static void doInvalidate(CacheInvokeContext context, CacheInvalidateAnnoConfig annoConfig) {
        Cache cache = context.getCacheFunction().apply(context, annoConfig);  // 获取指定缓存
        if (cache == null) {
            return;
        }
        boolean condition = ExpressionUtil.evalCondition(context, annoConfig);  // 判断是否满足条件
        if (!condition) {
            return;
        }
        Object key = ExpressionUtil.evalKey(context, annoConfig);  // 获取缓存的键
        if (key == null) {
            return;
        }
        if (annoConfig.isMulti()) {  // 判断是否为多个键
            Iterable it = toIterable(key);  // 将键转为迭代器
            if (it == null) {
                logger.error("jetcache @CacheInvalidate key is not instance of Iterable or array: " + annoConfig.getDefineMethod());
                return;
            }
            Set keys = new HashSet();
            it.forEach(k -> keys.add(k));  // 将键添加到集合中
            cache.removeAll(keys);  // 清除所有指定键的缓存
        } else {
            cache.remove(key);  // 清除指定键的缓存
        }
    }

这段代码定义了两个私有的静态函数:doInvalidate。这两个函数的作用是根据传入的参数,对缓存进行清空操作。其中,doInvalidate函数接受一个CacheInvokeContext对象和一个CacheInvalidateAnnoConfig对象,通过遍历CacheInvalidateAnnoConfig对象的列表,依次调用doInvalidate函数进行清空操作。在doInvalidate函数中,首先通过CacheInvokeContext对象获取缓存对象,并根据CacheInvalidateAnnoConfig对象中的条件判断是否执行清空操作。若满足条件,则根据CacheInvalidateAnnoConfig对象中的配置,对缓存进行清空操作。如果配置中指定了多个key,则将多个key放入Set集合中进行清空操作;如果只指定了一个key,则直接进行清空操作。

如果目标方法存在注解@CacheUpdate,就会调用doUpdate方法更新缓存数据,源码如下:

    // 更新缓存
    private static void doUpdate(CacheInvokeContext context, CacheUpdateAnnoConfig updateAnnoConfig) {
        // 获取缓存
        Cache cache = context.getCacheFunction().apply(context, updateAnnoConfig);
        // 若缓存为空,直接返回
        if (cache == null) {
            return;
        }
        // 判断是否满足更新条件
        boolean condition = ExpressionUtil.evalCondition(context, updateAnnoConfig);
        if (!condition) {
            return;
        }
        // 获取键值
        Object key = ExpressionUtil.evalKey(context, updateAnnoConfig);
        Object value = ExpressionUtil.evalValue(context, updateAnnoConfig);
        // 若键值为空或者获取键值失败,直接返回
        if (key == null || value == ExpressionUtil.EVAL_FAILED) {
            return;
        }
        // 判断是否为批量更新
        if (updateAnnoConfig.isMulti()) {
            // 若值为空,直接返回
            if (value == null) {
                return;
            }
            // 将键值转换为可迭代对象
            Iterable keyIt = toIterable(key);
            Iterable valueIt = toIterable(value);
            // 若键为null,打印错误日志并返回
            if (keyIt == null) {
                logger.error("jetcache @CacheUpdate key is not instance of Iterable or array: " + updateAnnoConfig.getDefineMethod());
                return;
            }
            // 若值为null,打印错误日志并返回
            if (valueIt == null) {
                logger.error("jetcache @CacheUpdate value is not instance of Iterable or array: " + updateAnnoConfig.getDefineMethod());
                return;
            }
            // 转换为列表
            List keyList = new ArrayList();
            List valueList = new ArrayList();
            keyIt.forEach(o -> keyList.add(o));
            valueIt.forEach(o -> valueList.add(o));
            // 若键列表与值列表大小不一致,打印错误日志并返回
            if (keyList.size() != valueList.size()) {
                logger.error("jetcache @CacheUpdate key size not equals with value size: " + updateAnnoConfig.getDefineMethod());
                return;
            } else {
                // 构建键值对映射
                Map m = new HashMap();
                for (int i = 0; i < valueList.size(); i++) {
                    m.put(keyList.get(i), valueList.get(i));
                }
                // 批量更新缓存
                cache.putAll(m);
            }
        } else {
            // 批量更新缓存
            cache.put(key, value);
        }
    }

这个函数是一个用于更新缓存的方法。首先根据传入的参数获取缓存对象,如果缓存为空,则直接返回。然后通过判断条件来确定是否执行更新操作。接着通过调用ExpressionUtil的evalKey方法和evalValue方法来获取键和值。如果键或值获取失败,则直接返回。如果updateAnnoConfig.isMulti()为true,则表示是批量更新操作。这时需要将值转换为可迭代对象,并判断是否为空。接着将键和值分别转换为列表,并进行大小判断。如果不相等,则打印错误日志并返回。如果相等,则构建一个键值对映射,然后将该映射批量更新到缓存中。如果updateAnnoConfig.isMulti()为false,则表示是单个更新操作,直接将键值对更新到缓存中。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/311937.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

最详细手把手教你安装 Vivado2019.2

Vivado 是 FPGA 厂商赛灵思公司&#xff08;Xilinx&#xff09;于 2012 年起发布的集成设计环境。 Vivado 2019.2 是 2019 年 Xilinx 推出的 Vivado 最后一个版本&#xff0c;相对稳定&#xff0c;并推出了新式的嵌入式开发平台 Vitis。 软件下载 官网可下载各个版本百度网盘…

Android 通知简介

Android 通知简介 1. 基本通知 图1: 基本通知详情 小图标 : 必须提供,通过 setSmallIcon( ) 进行设置.应用名称 : 由系统提供.时间戳 : 由系统提供,也可隐藏时间.大图标(可选) : 可选内容(通常仅用于联系人照片,请勿将其用于应用图标),通过setLargeIcon( ) 进行设置.标题 : 可选…

一日难再晨及时当勉励 date

文章目录 Linux shell 获取更改系统时间默认输入显示时区世界协调时格式化日期更多信息 Linux shell 获取更改系统时间 … note:: 时光只解催人老&#xff0c;不信多情&#xff0c;长恨离亭&#xff0c;泪滴春衫酒易醒。 - 晏殊《采桑子时光只解催人老》date命令可以用来打印…

12.扩展字典(ExtensionDictionary)

愿你出走半生,归来仍是少年! 环境:.NET FrameWork4.5、ObjectArx 2016 64bit、Entity Framework 6. 在10.扩展数据(XData)中我们讲到每个DbObject有一个XData对象可以存储数据,除此之外每个DbObject对象还有一个ExtensionDictionary(扩展字典)可以进行数据存储。…

P1328 [NOIP2014 提高组] 生活大爆炸版石头剪刀布————C++

目录 [NOIP2014 提高组] 生活大爆炸版石头剪刀布题目背景题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1 样例 #2样例输入 #2样例输出 #2 提示 解题思路Code调用函数的Code&#xff08;看起来简洁一点&#xff09;运行结果 [NOIP2014 提高组] 生活大爆炸版石头剪刀布 …

Tensorflow2.0笔记 - Tensor的数据索引和切片

主要涉及的了基础下标索引"[]",逗号",",冒号":",省略号"..."操作&#xff0c;以及gather,gather_nd和boolean_mask的相关使用方法。 import tensorflow as tf import numpy as nptf.__version__tensor tf.random.uniform([1,5,5,3],…

if单分支,二分支,多分支,语句嵌套,while语句,for语句(Python实现)

一、主要目的&#xff1a; 1&#xff0e;熟悉程序设计结构的三种方式 2.掌握if单分支语句、if二分支语句、if多分支语句及if语句嵌套的使用方法 3.掌握while语句的使用方法 4.掌握for语句的使用方法 5.掌握循环嵌套的使用方法 二、主要内容和结果展现&#xff1a; 1&…

国产ULN2803达林顿驱动芯片为什么可兼容TI ULN2803A的参数特性分享,且可用于红外摄像机等产品中

随着安防视频监控系统工程的需求越来越广&#xff0c;销量也与日俱增。在红外摄像机的红外LED驱动电路应用中&#xff0c;驱动大多数选用达林顿驱动芯片&#xff0c;行业上有用到TI 的ULN2803A&#xff0c;在目前行情&#xff0c;国外芯片紧缺的情况下&#xff0c;不少企业会多…

第一次作业

作业一&#xff1a;安装Euler系统&#xff1a; 和以前安装红帽没多大差别&#xff0c;看以前文章就行 作业二&#xff1a;通过两台Linux主机怕配置ssh实现互相免密登录&#xff1a; 1. 客户端地址&#xff1a;192.168.146.131 服务器地址&#xff1a; 192.168.146.129 1、…

Spacedesk | 最新版本移动端扩展PC副屏

我的设备&#xff1a; 电脑:戴尔G15 5511、i7-11800H、Windows 11、RTX3060&#xff08;推荐显卡高级一些&#xff0c;算力差点的可能带不动这款软件&#xff09; 平板&#xff1a;荣耀V6、麒麟985、安卓10、分辨率2000*1200&#xff08;手机也行&#xff0c;我用的平板&…

纯前端 —— 200行JS代码、实现导出Excel、支持DIY样式,纵横合并

前期回顾 Vue3 TS Element-Plus 封装Tree组件 《亲测可用》_vue3ts 组件封装-CSDN博客https://blog.csdn.net/m0_57904695/article/details/131664157?spm1001.2014.3001.5501 目录 具体思路&#xff1a; 1. 准备HTML结构 2. 定义CSS样式 3. 初始化表格数据 4. 创建表…

【K8S 存储卷】K8S的存储卷+PV/PVC

目录 一、K8S的存储卷 1、概念&#xff1a; 2、挂载的方式&#xff1a; 2.1、emptyDir&#xff1a; 2.2、hostPath&#xff1a; 2.3、NFS共享存储&#xff1a; 二、PV和PVC&#xff1a; 1、概念 2、请求方式 3、静态请求流程图&#xff1a; 4、PV和PVC的生命周期 5、…

鸿蒙Harmony--状态管理器--@Provide装饰器和@Consume装饰器详解

今天是1月11日号星期四&#xff0c;农历腊月初一&#xff0c;辞旧的岁月里&#xff0c;愿你守得云开、终见月明&#xff0c;迎新的时光中&#xff0c;愿你心御寒冬、顺遂无忧&#xff0c;岁末冬深&#xff0c;希望接下来的日子里足够幸运&#xff0c;攒足勇气、信心和运气&…

2024年1月1日孙溟㠭篆刻艺术展开幕式于北京大学北大书店成功举办

“印记青春——会说话的石头” 主题文化展盛大开幕 2024年1月1日正值新年伊始&#xff0c;由北京大学出版社、北大书店、不黑文化艺术学社、中国诗书画研究会三才书画院联合举办的“印记 青春——会说话的石头”主题篆刻艺术展&#xff0c;在北京大学新太阳学生中心拉开帷幕。 …

深度学习”和“多层神经网络”的区别

在讨论深度学习与多层神经网络之间的差异时&#xff0c;我们必须首先理解它们各自是什么以及它们在计算机科学和人工智能领域的角色。 深度学习是一种机器学习的子集&#xff0c;它使用了人工神经网络的架构。深度学习的核心思想是模拟人脑神经元的工作方式&#xff0c;以建立…

tailwindcss真的好用吗?

写在前面 今天写一篇关于tailwindcss 的文章&#xff0c;其实这个css技术已经出现很久了&#xff0c;在一些大型项目很多人也已经在用了&#xff0c;虽然不是说必须要会吧&#xff0c;但是没听说过肯定是不行的&#xff0c;他的操作逻辑应该是和unocss差不多&#xff0c;但是今…

【UE Niagara学习笔记】07 - 火焰的热变形效果

目录 效果 步骤 一、创建热变形材质 二、添加新的发射器 2.1 设置粒子材质 2.2 设置粒子初始大小 2.3 设置粒子持续生成 三、修改材质 四、设置粒子效果 在上一篇博客&#xff08;【UE Niagara学习笔记】06 - 制作火焰喷射过程中飞舞的火星&#xff09;的基础上继续…

[力扣 Hot100]Day2 字母异位词分组

题目描述 给你一个字符串数组&#xff0c;请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。 字母异位词 是由重新排列源单词的所有字母得到的一个新单词。 出处 思路 这题有点考阅读理解&#xff0c;意思就是把输入数组中的所含字母相同但顺序不同的单词放到同…

主动学习基础-贝叶斯神经网络

引言 传统的深度神经网络一般都有过度自信的问题。 即使我给神经网络提供一个从来没有训练过的类别图像&#xff0c;神经网络也会输出一个类别。比如训练猫狗的分类器&#xff0c;如果你抛出一个人的图像&#xff0c;网络也会将其分类为猫或者狗。 在几乎所有现实世界的问题中…

Netty通信中的粘包半包问题(一)

前言 我们在日常开发过程中&#xff0c;客户端和服务端的连接大多使用的是TCP协议,因为我们要保证数据的可靠传输&#xff0c; 当网络中出现丢包时要求&#xff0c;要求数据包的发送端重传给接收端。而TCP是一种面向连接的传输层协议&#xff0c; 当使用TCP进行传输时&#xf…
最新文章