玩转Spring Cache --- @Cacheable/@CachePut/@CacheEvict注解的原理深度剖析和使用【享学Spring】(上)

简介: 玩转Spring Cache --- @Cacheable/@CachePut/@CacheEvict注解的原理深度剖析和使用【享学Spring】(上)

前言


上篇文章介绍了@EnableCaching,用它来开启Spring对缓存注解的支持。本篇文章将继续分析Spring Cache,并且讲解的是我们最为关心的:缓存注解实操方面的原理支持和使用。


开发过程中因注解的优雅、使用简单使得这种方式广泛被大家所接受和使用,本文将按照先原理再实操的步骤,一步步解惑Spring缓存注解的原理


缓存注解


关于Spring的缓存注解,一共有如下5个:


  1. @Cacheable:缓存
// @since 3.1  可以标注在方法上、类上  下同
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {
  // 缓存名称  可以写多个~
  @AliasFor("cacheNames")
  String[] value() default {};
  @AliasFor("value")
  String[] cacheNames() default {};
  // 支持写SpEL,切可以使用#root
  String key() default "";
  // Mutually exclusive:它和key属性互相排斥。请只使用一个
  String keyGenerator() default "";
  String cacheManager() default "";
  String cacheResolver() default "";
  // SpEL,可以使用#root。  只有true时,才会作用在这个方法上
  String condition() default "";
  // 可以写SpEL #root,并且可以使用#result拿到方法返回值~~~
  String unless() default "";
  // true:表示强制同步执行。(若多个线程试图为**同一个键**加载值,以同步的方式来进行目标方法的调用)
  // 同步的好处是:后一个线程会读取到前一个缓存的缓存数据,不用再查库了~~~ 
  // 默认是false,不开启同步one by one的
  // @since 4.3  注意是sync而不是Async
  // 它的解析依赖于Spring4.3提供的Cache.get(Object key, Callable<T> valueLoader);方法
  boolean sync() default false;
}


  1. @CachePut:缓存更新
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {
  @AliasFor("cacheNames")
  String[] value() default {};
  @AliasFor("value")
  String[] cacheNames() default {};
  // 注意:它和上面区别是。此处key它还能使用#result
  String key() default "";
  String keyGenerator() default "";
  String cacheManager() default "";
  String cacheResolver() default "";
  String condition() default "";
  String unless() default "";
}


  1. @CacheEvict:缓存删除
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {
  @AliasFor("cacheNames")
  String[] value() default {};
  @AliasFor("value")
  String[] cacheNames() default {};
  // 它也能使用#result
  String key() default "";
  String keyGenerator() default "";
  String cacheManager() default "";
  String cacheResolver() default "";
  String condition() default "";
  // 是否把上面cacheNames指定的所有的缓存都清除掉,默认false
  boolean allEntries() default false;
  // 是否让清理缓存动作在目标方法之前执行,默认是false(在目标方法之后执行)
  // 注意:若在之后执行的话,目标方法一旦抛出异常了,那缓存就清理不掉了~~~~
  boolean beforeInvocation() default false;
}


  1. @Caching:用于处理复杂的缓存情况。比如用户既要根据id缓存一份,也要根据电话缓存一份,还要根据电子邮箱缓存一份,就可以使用它
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Caching {
  Cacheable[] cacheable() default {};
  CachePut[] put() default {};
  CacheEvict[] evict() default {};
}


  1. @CacheConfig:可以在类级别上标注一些共用的缓存属性。(所有方法共享,@since 4.1)
// @since 4.1 出现得还是比较晚的
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CacheConfig {
  String[] cacheNames() default {};
  String keyGenerator() default "";
  String cacheManager() default "";
  String cacheResolver() default "";
}


属性说明表格:


image.png


原理分析


先阅读:【小家Spring】玩转Spring Cache — @Cacheable/@CachePut/@CacheEvict缓存注解相关基础类打点 再读本文,效果会像德芙一般丝滑~


从上篇文章中已经知道了@EnableCaching主要向容器注入了三个Bean:CacheOperationSource、BeanFactoryCacheOperationSourceAdvisor、CacheInterceptor。他们是让注解生效的核心类。


CacheOperationSource

它代表缓存操作源,已经分析过。


BeanFactoryCacheOperationSourceAdvisor

从名字就能看出它是一个增强器Advisor,并且还和BeanFactory有关。


  @Bean(name = CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME)
  @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
  public BeanFactoryCacheOperationSourceAdvisor cacheAdvisor() {
    BeanFactoryCacheOperationSourceAdvisor advisor = new BeanFactoryCacheOperationSourceAdvisor();
    advisor.setCacheOperationSource(cacheOperationSource());
    advisor.setAdvice(cacheInterceptor());
    if (this.enableCaching != null) {
      advisor.setOrder(this.enableCaching.<Integer>getNumber("order"));
    }
    return advisor;
  }


从上配置知道,这个增强器的切面Advice是CacheInterceptor,并且持有CacheOperationSource的引用。


public class BeanFactoryCacheOperationSourceAdvisor extends AbstractBeanFactoryPointcutAdvisor {
  @Nullable
  private CacheOperationSource cacheOperationSource;
  // 切面Pointcut
  private final CacheOperationSourcePointcut pointcut = new CacheOperationSourcePointcut() {
    @Override
    @Nullable
    protected CacheOperationSource getCacheOperationSource() {
      return cacheOperationSource;
    }
  };
  public void setCacheOperationSource(CacheOperationSource cacheOperationSource) {
    this.cacheOperationSource = cacheOperationSource;
  }
  // 注意:此处你可以自定义一个ClassFilter,过滤掉你想忽略的类
  public void setClassFilter(ClassFilter classFilter) {
    this.pointcut.setClassFilter(classFilter);
  }
  @Override
  public Pointcut getPointcut() {
    return this.pointcut;
  }
}


此Advisor的实现非常的简单,切点是CacheOperationSourcePointcut,核心逻辑都依托于缓存属性源。所以还没有看这块的,此处再一次推荐:【小家Spring】玩转Spring Cache — @Cacheable/@CachePut/@CacheEvict缓存注解相关基础类打点


CacheInterceptor


缓存拦截器。先说明一点,它的实现模式几乎和TransactionInterceptor一毛一样。所以我又想建议一句了,有空先看看它吧:【小家Spring】源码分析Spring的事务拦截器:TransactionInterceptor和事务管理器:PlatformTransactionManager


同样,CacheInterceptor是缓存真正执行的核心,处理逻辑还是稍显复杂的。


// @since 3.1  它是个MethodInterceptor环绕增强器~~~
public class CacheInterceptor extends CacheAspectSupport implements MethodInterceptor, Serializable {
  @Override
  @Nullable
  public Object invoke(final MethodInvocation invocation) throws Throwable {
    Method method = invocation.getMethod();
    // 采用函数的形式,最终把此函数传交给父类的execute()去执行
    // 但是很显然,最终**执行目标方法**的是invocation.proceed();它
    //这里就是对执行方法调用的一次封装,主要是为了处理对异常的包装。
    CacheOperationInvoker aopAllianceInvoker = () -> {
      try {
        return invocation.proceed();
      }
      catch (Throwable ex) {
        throw new CacheOperationInvoker.ThrowableWrapper(ex);
      }
    };
    try {
      // //真正地去处理缓存操作的执行,很显然这是父类的方法,所以我们要到父类CacheAspectSupport中去看看。
      return execute(aopAllianceInvoker, invocation.getThis(), method, invocation.getArguments());
    } catch (CacheOperationInvoker.ThrowableWrapper th) {
      throw th.getOriginal();
    }
  }
}


这个类本身的实现很少,主要逻辑都在他的抽象父类:CacheAspectSupport


CacheAspectSupport


它类似于TransactionAspectSupport,父类实现了所有的核心逻辑


// @since 3.1  它相较于TransactionAspectSupport额外实现了SmartInitializingSingleton接口
// SmartInitializingSingleton应该也不会陌生。它在初始化完所有的单例Bean后会执行这个接口的`afterSingletonsInstantiated()`方法
// 比如我们熟悉的ScheduledAnnotationBeanPostProcessor、EventListenerMethodProcessor都是这么来处理的
// 另外还需要注意,它还继承自AbstractCacheInvoker:主要对异常情况用CacheErrorHandler处理
public abstract class CacheAspectSupport extends AbstractCacheInvoker implements BeanFactoryAware, InitializingBean, SmartInitializingSingleton {
  // CacheOperationCacheKey:缓存的key  CacheOperationMetadata就是持有一些基础属性的性息
  // 这个缓存挺大,相当于每一个类、方法都有气对应的**缓存属性元数据**
  private final Map<CacheOperationCacheKey, CacheOperationMetadata> metadataCache = new ConcurrentHashMap<>(1024);
  // 解析一些condition、key、unless等可以写el表达式的处理器~~~
  // 之前讲过的熟悉的有:EventExpressionEvaluator
  private final CacheOperationExpressionEvaluator evaluator = new CacheOperationExpressionEvaluator();
  // 属性源,默认情况下是基于注解的`AnnotationCacheOperationSource`
  @Nullable
  private CacheOperationSource cacheOperationSource;
  // 看到了吧  key生成器默认使用的SimpleKeyGenerator
  // 注意SingletonSupplier是Spring5.1的新类,实现了接口java.util.function.Supplier  主要是对null值进行了容错
  private SingletonSupplier<KeyGenerator> keyGenerator = SingletonSupplier.of(SimpleKeyGenerator::new);
  @Nullable
  private SingletonSupplier<CacheResolver> cacheResolver;
  @Nullable
  private BeanFactory beanFactory;
  private boolean initialized = false;
  // @since 5.1
  public void configure(@Nullable Supplier<CacheErrorHandler> errorHandler, @Nullable Supplier<KeyGenerator> keyGenerator,@Nullable Supplier<CacheResolver> cacheResolver, @Nullable Supplier<CacheManager> cacheManager) {
    // 第二个参数都是默认值,若调用者没传的话
    this.errorHandler = new SingletonSupplier<>(errorHandler, SimpleCacheErrorHandler::new);
    this.keyGenerator = new SingletonSupplier<>(keyGenerator, SimpleKeyGenerator::new);
    this.cacheResolver = new SingletonSupplier<>(cacheResolver, () -> SimpleCacheResolver.of(SupplierUtils.resolve(cacheManager)));
  }
  // 此处:若传入了多个cacheOperationSources,那最终使用的就是CompositeCacheOperationSource包装起来
  // 所以发现,Spring是支持我们多种 缓存属性源的
  public void setCacheOperationSources(CacheOperationSource... cacheOperationSources) {
    Assert.notEmpty(cacheOperationSources, "At least 1 CacheOperationSource needs to be specified");
    this.cacheOperationSource = (cacheOperationSources.length > 1 ? new CompositeCacheOperationSource(cacheOperationSources) : cacheOperationSources[0]);
  }
  // @since 5.1 单数形式的设置
  public void setCacheOperationSource(@Nullable CacheOperationSource cacheOperationSource) {
    this.cacheOperationSource = cacheOperationSource;
  }
  ... // 省略各种get/set方法~~~
  // CacheOperationSource必须不为null,因为一切依托于它
  @Override
  public void afterPropertiesSet() {
    Assert.state(getCacheOperationSource() != null, "The 'cacheOperationSources' property is required: " + "If there are no cacheable methods, then don't use a cache aspect.");
  }
  // 这个来自于接口:SmartInitializingSingleton  在实例化完所有单例Bean后调用
  @Override
  public void afterSingletonsInstantiated() {
    // 若没有给这个切面手动设置cacheResolver  那就去拿CacheManager吧
    // 这就是为何我们只需要把CacheManager配进容器里即可  就自动会设置在切面里了
    if (getCacheResolver() == null) {
      // Lazily initialize cache resolver via default cache manager...
      Assert.state(this.beanFactory != null, "CacheResolver or BeanFactory must be set on cache aspect");
      try {
        // 请注意:这个方法实际上是把CacheManager包装成了一个SimpleCacheResolver
        // 所以最终还是给SimpleCacheResolver赋值
        setCacheManager(this.beanFactory.getBean(CacheManager.class));
      } ...
    }
    this.initialized = true;
  }
  // 主要为了输出日志,子类可复写
  protected String methodIdentification(Method method, Class<?> targetClass) {
    Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
    return ClassUtils.getQualifiedMethodName(specificMethod);
  }
  // 从这里也能看出,至少要指定一个Cache才行(也就是cacheNames)
  protected Collection<? extends Cache> getCaches(CacheOperationInvocationContext<CacheOperation> context, CacheResolver cacheResolver) {
    Collection<? extends Cache> caches = cacheResolver.resolveCaches(context);
    if (caches.isEmpty()) {
      throw new IllegalStateException("No cache could be resolved for '" +
          context.getOperation() + "' using resolver '" + cacheResolver +
          "'. At least one cache should be provided per cache operation.");
    }
    return caches;
  }
  // 这个根据CacheOperation 这部分还是比较重要的
  protected CacheOperationMetadata getCacheOperationMetadata(CacheOperation operation, Method method, Class<?> targetClass) {
    CacheOperationCacheKey cacheKey = new CacheOperationCacheKey(operation, method, targetClass);
    CacheOperationMetadata metadata = this.metadataCache.get(cacheKey);
    if (metadata == null) {
      // 1、指定了KeyGenerator就去拿这个Bean(没有就报错,所以key不要写错了)
      // 没有指定就用默认的
      KeyGenerator operationKeyGenerator;
      if (StringUtils.hasText(operation.getKeyGenerator())) {
        operationKeyGenerator = getBean(operation.getKeyGenerator(), KeyGenerator.class);
      } else {
        operationKeyGenerator = getKeyGenerator();
      }
      // 1、自己指定的CacheResolver
      // 2、再看指定的的CacheManager,包装成一个SimpleCacheResolver
      // 3、
      CacheResolver operationCacheResolver;
      if (StringUtils.hasText(operation.getCacheResolver())) {
        operationCacheResolver = getBean(operation.getCacheResolver(), CacheResolver.class);
      } else if (StringUtils.hasText(operation.getCacheManager())) {
        CacheManager cacheManager = getBean(operation.getCacheManager(), CacheManager.class);
        operationCacheResolver = new SimpleCacheResolver(cacheManager);
      } else { //最终都没配置的话,取本切面默认的
        operationCacheResolver = getCacheResolver();
        Assert.state(operationCacheResolver != null, "No CacheResolver/CacheManager set");
      }
      // 封装成Metadata
      metadata = new CacheOperationMetadata(operation, method, targetClass, operationKeyGenerator, operationCacheResolver);
      this.metadataCache.put(cacheKey, metadata);
    }
    return metadata;
  }
  // qualifiedBeanOfType的意思是,@Bean类上面标注@Qualifier注解也生效
  protected <T> T getBean(String beanName, Class<T> expectedType) {
    if (this.beanFactory == null) {
      throw new IllegalStateException(
          "BeanFactory must be set on cache aspect for " + expectedType.getSimpleName() + " retrieval");
    }
    return BeanFactoryAnnotationUtils.qualifiedBeanOfType(this.beanFactory, expectedType, beanName);
  }
  // 请Meta数据的缓存
  protected void clearMetadataCache() {
    this.metadataCache.clear();
    this.evaluator.clear();
  }
  // 父类最为核心的方法,真正执行目标方法 + 缓存操作
  @Nullable
  protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) {
    // Check whether aspect is enabled (to cope with cases where the AJ is pulled in automatically)
    // 如果已经表示初始化过了(有CacheManager,CacheResolver了),执行这里
    if (this.initialized) {
      // getTargetClass拿到原始Class  解剖代理(N层都能解开)
      Class<?> targetClass = getTargetClass(target);
      CacheOperationSource cacheOperationSource = getCacheOperationSource();
      if (cacheOperationSource != null) {
        // 简单的说就是拿到该方法上所有的CacheOperation缓存操作,最终一个一个的执行~~~~
        Collection<CacheOperation> operations = cacheOperationSource.getCacheOperations(method, targetClass);
        if (!CollectionUtils.isEmpty(operations)) {
          // CacheOperationContexts是非常重要的一个私有内部类
          // 注意它是复数哦~不是CacheOperationContext单数  所以它就像持有多个注解上下文一样  一个个执行吧
          // 所以我建议先看看此类的描述,再继续往下看~~~
          return execute(invoker, method, new CacheOperationContexts(operations, method, args, target, targetClass));
        }
      }
    }
    // 若还没初始化  直接执行目标方法即可
    return invoker.invoke();
  }
  @Nullable
  private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
    // Special handling of synchronized invocation
    // 如果是需要同步执行的话,这块还是
    if (contexts.isSynchronized()) {
      CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
      if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
        Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
        Cache cache = context.getCaches().iterator().next();
        try {
          return wrapCacheValue(method, cache.get(key, () -> unwrapReturnValue(invokeOperation(invoker))));
        }
        catch (Cache.ValueRetrievalException ex) {
          // The invoker wraps any Throwable in a ThrowableWrapper instance so we
          // can just make sure that one bubbles up the stack.
          throw (CacheOperationInvoker.ThrowableWrapper) ex.getCause();
        }
      }
      else {
        // No caching required, only call the underlying method
        return invokeOperation(invoker);
      }
    }
    // sync=false的情况,走这里~~~
    // Process any early evictions  beforeInvocation=true的会在此处最先执行~~~
    // 最先处理@CacheEvict注解~~~真正执行的方法请参见:performCacheEvict
    // context.getCaches()拿出所有的caches,看看是执行cache.evict(key);方法还是cache.clear();而已
    // 需要注意的的是context.isConditionPassing(result); condition条件此处生效,并且可以使用#result
    // context.generateKey(result)也能使用#result
    // @CacheEvict没有unless属性
    processCacheEvicts(contexts.get(CacheEvictOperation.class), true, CacheOperationExpressionEvaluator.NO_RESULT);
    // 执行@Cacheable  看看缓存是否能够命中
    Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
    // Collect puts from any @Cacheable miss, if no cached item is found
    List<CachePutRequest> cachePutRequests = new LinkedList<>();
    // 如果缓存没有命中,那就准备一个cachePutRequest
    // 因为@Cacheable首次进来肯定命中不了,最终肯定是需要执行一次put操作的~~~这样下次进来就能命中了呀
    if (cacheHit == null) {
      collectPutRequests(contexts.get(CacheableOperation.class), CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
    }
    Object cacheValue;
    Object returnValue;
    // 如果缓存命中了,并且并且没有@CachePut的话,也就直接返回了~~
    if (cacheHit != null && !hasCachePut(contexts)) {
      // If there are no put requests, just use the cache hit
      cacheValue = cacheHit.get();
      // wrapCacheValue主要是支持到了Optional
      returnValue = wrapCacheValue(method, cacheValue);
    } else { //到此处,目标方法就肯定是需要执行了的~~~~~
      // Invoke the method if we don't have a cache hit
      // 啥都不说,先invokeOperation执行目标方法,拿到方法的的返回值  后续在处理put啥的
      returnValue = invokeOperation(invoker);
      cacheValue = unwrapReturnValue(returnValue);
    }
    // Collect any explicit @CachePuts   explicit:明确的
    collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
    // Process any collected put requests, either from @CachePut or a @Cacheable miss
    for (CachePutRequest cachePutRequest : cachePutRequests) {
      // 注意:此处unless啥的生效~~~~
      // 最终执行cache.put(key, result);方法
      cachePutRequest.apply(cacheValue);
    }
    // Process any late evictions beforeInvocation=true的会在此处最先执行~~~  beforeInvocation=false的会在此处最后执行~~~
    // 所以中途若抛出异常,此部分就不会执行了~~~~
    processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
    return returnValue;
  }
  // 缓存属性的上下文们。每个方法可以对应多个上下文~~~
  private class CacheOperationContexts {
    // 因为方法上可以标注多个注解  
    // 需要注意的是它的key是Class,而CacheOperation的子类也就那三个哥们而已~
    private final MultiValueMap<Class<? extends CacheOperation>, CacheOperationContext> contexts;
    // 是否要求同步执行,默认值是false
    private final boolean sync;
    public CacheOperationContexts(Collection<? extends CacheOperation> operations, Method method, Object[] args, Object target, Class<?> targetClass) {
      this.contexts = new LinkedMultiValueMap<>(operations.size());
      for (CacheOperation op : operations) {
        this.contexts.add(op.getClass(), getOperationContext(op, method, args, target, targetClass));
      }
      // sync这个属性虽然不怎么使用,但determineSyncFlag这个方法可以看一下
      this.sync = determineSyncFlag(method);
    }
    public Collection<CacheOperationContext> get(Class<? extends CacheOperation> operationClass) {
      Collection<CacheOperationContext> result = this.contexts.get(operationClass);
      return (result != null ? result : Collections.emptyList());
    }
    public boolean isSynchronized() {
      return this.sync;
    }
    // 因为只有@Cacheable有sync属性,所以只需要看CacheableOperation即可
    private boolean determineSyncFlag(Method method) {
      List<CacheOperationContext> cacheOperationContexts = this.contexts.get(CacheableOperation.class);
      if (cacheOperationContexts == null) {  // no @Cacheable operation at all
        return false;
      }
      boolean syncEnabled = false;
      // 单反只要有一个@Cacheable的sync=true了,那就为true  并且下面还有检查逻辑
      for (CacheOperationContext cacheOperationContext : cacheOperationContexts) {
        if (((CacheableOperation) cacheOperationContext.getOperation()).isSync()) {
          syncEnabled = true;
          break;
        }
      }
      // 执行sync=true的检查逻辑
      if (syncEnabled) {
        // 人话解释:sync=true时候,不能还有其它的缓存操作 也就是说@Cacheable(sync=true)的时候只能单独使用
        if (this.contexts.size() > 1) {
          throw new IllegalStateException("@Cacheable(sync=true) cannot be combined with other cache operations on '" + method + "'");
        }
        // 人话解释:@Cacheable(sync=true)时,多个@Cacheable也是不允许的
        if (cacheOperationContexts.size() > 1) {
          throw new IllegalStateException("Only one @Cacheable(sync=true) entry is allowed on '" + method + "'");
        }
        // 拿到唯一的一个@Cacheable
        CacheOperationContext cacheOperationContext = cacheOperationContexts.iterator().next();
        CacheableOperation operation = (CacheableOperation) cacheOperationContext.getOperation();
        // 人话解释:@Cacheable(sync=true)时,cacheName只能使用一个
        if (cacheOperationContext.getCaches().size() > 1) {
          throw new IllegalStateException("@Cacheable(sync=true) only allows a single cache on '" + operation + "'");
        }
        // 人话解释:sync=true时,unless属性是不支持的~~~并且是不能写的
        if (StringUtils.hasText(operation.getUnless())) {
          throw new IllegalStateException("@Cacheable(sync=true) does not support unless attribute on '" + operation + "'");
        }
        return true; // 只有校验都通过后,才返回true
      }
      return false;
    }
  }
  ...
}


以上,拦截器实现了Spring Cache处理注解缓存的执行的核心步骤,个人建议上述代码可多读几遍,其义自见。

相关文章
|
3天前
|
Java
Springboot 使用自定义注解结合AOP方式校验接口参数
Springboot 使用自定义注解结合AOP方式校验接口参数
Springboot 使用自定义注解结合AOP方式校验接口参数
|
4天前
|
存储 缓存 Java
【JavaEE】Spring中注解的方式去获取Bean对象
【JavaEE】Spring中注解的方式去获取Bean对象
3 0
|
4天前
|
存储 Java 对象存储
【JavaEE】Spring中注解的方式去存储Bean对象
【JavaEE】Spring中注解的方式去存储Bean对象
7 0
|
4天前
|
JSON 前端开发 Java
【JAVA进阶篇教学】第七篇:Spring中常用注解
【JAVA进阶篇教学】第七篇:Spring中常用注解
|
4天前
|
Java 应用服务中间件 Maven
SpringBoot 项目瘦身指南
SpringBoot 项目瘦身指南
60 0
|
4天前
|
缓存 Java Maven
Spring Boot自动配置原理
Spring Boot自动配置原理
52 0
|
4天前
|
缓存 安全 Java
Spring Boot 面试题及答案整理,最新面试题
Spring Boot 面试题及答案整理,最新面试题
142 0
|
4天前
|
存储 JSON Java
SpringBoot集成AOP实现每个接口请求参数和返回参数并记录每个接口请求时间
SpringBoot集成AOP实现每个接口请求参数和返回参数并记录每个接口请求时间
49 2
|
4天前
|
前端开发 搜索推荐 Java
【Spring底层原理高级进阶】基于Spring Boot和Spring WebFlux的实时推荐系统的核心:响应式编程与 WebFlux 的颠覆性变革
【Spring底层原理高级进阶】基于Spring Boot和Spring WebFlux的实时推荐系统的核心:响应式编程与 WebFlux 的颠覆性变革
|
4天前
|
前端开发 Java 应用服务中间件
Springboot对MVC、tomcat扩展配置
Springboot对MVC、tomcat扩展配置