Skip to content

Proxy and Annotation System

CoCache uses a declarative, annotation-driven approach to cache configuration. Developers define cache interfaces annotated with @CoCache, and the framework automatically creates JDK dynamic proxy implementations backed by DefaultCoherentCache. This system eliminates boilerplate cache wiring and allows cache behavior to be configured entirely through annotations.

Overview

mermaid
flowchart TD
    subgraph sg_14 ["Developer Defines"]

        Interface["Cache Interface<br>+ @CoCache annotation"]
    end

    subgraph sg_15 ["Startup Registration"]

        Enable["@EnableCoCache<br>caches = [...]"]
        Registrar["EnableCoCacheRegistrar"]
        Parse["CoCacheMetadataParser<br>parse interface"]
        RegisterBean["Register CacheProxyFactoryBean<br>as Spring Bean"]
    end

    subgraph sg_16 ["Bean Creation"]

        FactoryBean["CacheProxyFactoryBean<br>.getObject()"]
        ProxyFactory["DefaultCacheProxyFactory<br>.create(metadata)"]
        InvocationHandler["CoCacheInvocationHandler"]
        Proxy["JDK Proxy<br>implements Cache interface"]
    end

    Interface --> Enable
    Enable --> Registrar
    Registrar --> Parse
    Parse --> RegisterBean
    RegisterBean --> FactoryBean
    FactoryBean --> ProxyFactory
    ProxyFactory --> InvocationHandler
    InvocationHandler --> Proxy

    style Interface fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Enable fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Registrar fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Parse fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style RegisterBean fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style FactoryBean fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style ProxyFactory fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style InvocationHandler fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Proxy fill:#2d333b,stroke:#6d5dfc,color:#e6edf3

The @EnableCoCache Annotation

The entry point is @EnableCoCache, a Spring @Import annotation that triggers the registration process:

kotlin
@Import(EnableCoCacheRegistrar::class)
@Target(AnnotationTarget.CLASS)
annotation class EnableCoCache(
    val caches: Array<KClass<out Cache<*, *>>> = []
)

Usage in a Spring configuration class:

kotlin
@EnableCoCache(caches = [UserProfileCache::class, ProductCache::class])
class CacheConfiguration

EnableCoCacheRegistrar -- Bean Definition Registration

EnableCoCacheRegistrar implements Spring's ImportBeanDefinitionRegistrar interface. During application startup, it:

  1. Reads the caches array from the @EnableCoCache annotation
  2. Separates JoinCache types from regular Cache types
  3. Parses each interface into CoCacheMetadata or JoinCacheMetadata
  4. Registers Spring FactoryBean definitions for each cache
mermaid
sequenceDiagram
autonumber
    participant SC as Spring Container
    participant R as EnableCoCacheRegistrar
    participant P as CoCacheMetadataParser
    participant F as CacheProxyFactoryBean
    participant JF as JoinCacheProxyFactoryBean

    SC->>R: registerBeanDefinitions(metadata, registry)
    R->>R: getCacheTypes from @EnableCoCache annotation

    loop For each non-JoinCache type
        R->>P: parse(KClass) via toCoCacheMetadata()
        P-->>R: CoCacheMetadata
        R->>R: Register CoCacheMetadata bean
        R->>R: Register CacheProxyFactoryBean bean
    end

    loop For each JoinCache type
        R->>R: parse via toJoinCacheMetadata()
        R->>R: Register JoinCacheProxyFactoryBean bean
    end

    SC->>F: getObject() [lazy bean creation]
    F-->>SC: Cache proxy instance

The key logic in registerBeanDefinitions() at line 45:

kotlin
override fun registerBeanDefinitions(importingClassMetadata: AnnotationMetadata, registry: BeanDefinitionRegistry) {
    val cacheMetadataList = resolveCacheMetadataList(importingClassMetadata)
    cacheMetadataList.forEach { cacheMetadata ->
        registry.registerCacheMetadata(cacheMetadata)
        val beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(CacheProxyFactoryBean::class.java)
        beanDefinitionBuilder.addConstructorArgValue(cacheMetadata)
        beanDefinitionBuilder.setPrimary(true)
        registry.registerBeanDefinition(cacheMetadata.cacheName, beanDefinitionBuilder.beanDefinition)
    }
    val joinCacheMetadataList = resolveJoinCacheMetadataList(importingClassMetadata)
    joinCacheMetadataList.forEach { cacheMetadata ->
        val beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(JoinCacheProxyFactoryBean::class.java)
        beanDefinitionBuilder.addConstructorArgValue(cacheMetadata)
        beanDefinitionBuilder.setPrimary(true)
        registry.registerBeanDefinition(cacheMetadata.cacheName, beanDefinitionBuilder.beanDefinition)
    }
}

CoCacheMetadata and CoCacheMetadataParser

CoCacheMetadata

CoCacheMetadata is a data class that holds all parsed configuration from a cache interface:

kotlin
data class CoCacheMetadata(
    override val proxyInterface: KClass<*>,
    override val name: String,
    val keyPrefix: String,
    val keyExpression: String,
    override val ttl: Long,
    override val ttlAmplitude: Long,
    val keyType: KType,
    val valueType: KType
) : ComputedNamedCache, TtlConfiguration {
    override val cacheName: String = name.ifBlank {
        proxyInterface.simpleName!!
    }
}

If name is blank, the cacheName defaults to the interface's simple class name.

CoCacheMetadataParser

CoCacheMetadataParser parses a KClass into CoCacheMetadata at line 30:

mermaid
flowchart TD
    Input["KClass&lt;out Cache&lt;*, *&gt;&gt;"] --> IsInterface{"Is it an interface?"}
    IsInterface -->|No| Error["Throw error:<br>must be interface"]
    IsInterface -->|Yes| FindAnnotation["Find @CoCache annotation<br>(or use defaults)"]
    FindAnnotation --> FindSuper["Find Cache&lt;K, V&gt; supertype"]
    FindSuper --> ExtractTypes["Extract keyType and valueType<br>from generic arguments"]
    ExtractTypes --> BuildMetadata["Build CoCacheMetadata"]

    style Input fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style IsInterface fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style Error fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style FindAnnotation fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style FindSuper fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style ExtractTypes fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style BuildMetadata fill:#2d333b,stroke:#6d5dfc,color:#e6edf3

The parser enforces that the target must be an interface. It reads the @CoCache annotation (or uses defaults if absent), extracts the generic type parameters from the Cache<K, V> supertype, and produces a CoCacheMetadata instance.

JDK Dynamic Proxy Creation

CoCacheProxy -- Abstract InvocationHandler

CoCacheProxy is an abstract InvocationHandler that provides the core delegation logic:

kotlin
abstract class CoCacheProxy<DELEGATE> : InvocationHandler, CacheDelegated<DELEGATE>
    where DELEGATE : Cache<*, *> {

    abstract val proxyInterface: Class<*>

    private val declaredDefaultMethods by lazy {
        proxyInterface.declaredMethods.filter { it.isDefault }
    }

    override fun invoke(proxy: Any, method: Method, args: Array<out Any>?): Any? {
        val methodArgs = args ?: EMPTY_ARGS
        if (method.isDefault && declaredDefaultMethods.contains(method)) {
            return InvocationHandler.invokeDefault(proxy, method, *methodArgs)
        }
        return method.invoke(delegate, *methodArgs)
    }
}

The proxy distinguishes between:

  • Default methods (declared on the interface itself with a body) -- invoked via InvocationHandler.invokeDefault()
  • Abstract methods (from Cache<K, V> and parent interfaces) -- delegated to the DefaultCoherentCache instance

CoCacheInvocationHandler -- Concrete Handler

CoCacheInvocationHandler extends CoCacheProxy and adds special handling for the delegate and cacheMetadata accessor methods:

kotlin
class CoCacheInvocationHandler<DELEGATE>(
    override val cacheMetadata: CoCacheMetadata,
    override val delegate: DELEGATE
) : CacheDelegated<DELEGATE>, CacheMetadataCapable, CoCacheProxy<DELEGATE>()
    where DELEGATE : Cache<*, *>, DELEGATE : NamedCache {

    override fun invoke(proxy: Any, method: Method, args: Array<out Any>?): Any? {
        if (DELEGATE_METHOD_SIGN == method.name) return delegate
        if (CACHE_METADATA_METHOD_SIGN == method.name) return cacheMetadata
        return super.invoke(proxy, method, args)
    }
}

This allows callers to access the underlying delegate (the DefaultCoherentCache) and the cacheMetadata from the proxy, enabling introspection without casting.

DefaultCacheProxyFactory -- Factory

DefaultCacheProxyFactory orchestrates the creation of a cache proxy at line 40:

mermaid
sequenceDiagram
autonumber
    participant F as DefaultCacheProxyFactory
    participant CID as ClientIdGenerator
    participant CSF as ClientSideCacheFactory
    participant DF as DistributedCacheFactory
    participant SF as CacheSourceFactory
    participant KCF as KeyConverterFactory
    participant CHF as CoherentCacheFactory
    participant IH as CoCacheInvocationHandler
    participant P as JDK Proxy

    F->>CID: generate()
    CID-->>F: clientId
    F->>CSF: create(metadata)
    CSF-->>F: ClientSideCache (L2)
    F->>DF: create(metadata)
    DF-->>F: DistributedCache (L1)
    F->>SF: create(metadata)
    SF-->>F: CacheSource (L0)
    F->>KCF: create(metadata)
    KCF-->>F: KeyConverter

    F->>CHF: create(CoherentCacheConfiguration)
    CHF-->>F: DefaultCoherentCache (delegate)

    F->>IH: new CoCacheInvocationHandler(metadata, delegate)
    F->>P: Proxy.newProxyInstance(classLoader, interfaces, handler)
    P-->>F: CACHE proxy instance

The proxy implements four interfaces simultaneously:

  1. The user's cache interface (e.g., UserProfileCache)
  2. CoherentCache<K, V> -- full coherent cache API
  3. CacheDelegated -- access to the underlying delegate
  4. CacheMetadataCapable -- access to the parsed metadata

CacheProxyFactoryBean -- Spring Integration

CacheProxyFactoryBean bridges the Spring FactoryBean contract with the proxy factory:

kotlin
class CacheProxyFactoryBean(private val cacheMetadata: CoCacheMetadata) :
    FactoryBean<Cache<Any, Any>>, ApplicationContextAware {

    override fun getObject(): Cache<Any, Any> {
        val cacheProxyFactory = appContext.getBean(CacheProxyFactory::class.java)
        return cacheProxyFactory.create(cacheMetadata)
    }

    override fun getObjectType(): Class<*> {
        return cacheMetadata.proxyInterface.java
    }
}

It lazily resolves the CacheProxyFactory from the Spring ApplicationContext when getObject() is first called.

JoinCache Proxy Flow

For JoinCache interfaces (which compose two cached values), a parallel registration path exists through JoinCacheProxyFactoryBean and DefaultJoinCacheProxyFactory.

mermaid
sequenceDiagram
autonumber
    participant F as DefaultJoinCacheProxyFactory
    participant CF as CacheFactory
    participant JK as JoinKeyExtractorFactory
    participant SC as SimpleJoinCache
    participant IH as JoinCacheInvocationHandler
    participant P as JDK Proxy

    F->>CF: getCache(firstCacheName)
    CF-->>F: firstCache (Cache<K1, V1>)
    F->>CF: getCache(joinCacheName)
    CF-->>F: joinCache (Cache<K2, V2>)
    F->>JK: create(metadata)
    JK-->>F: JoinKeyExtractor

    F->>SC: new SimpleJoinCache(firstCache, joinCache, joinKeyExtractor)
    SC-->>F: delegate

    F->>IH: new JoinCacheInvocationHandler(metadata, delegate)
    F->>P: Proxy.newProxyInstance(classLoader, interfaces, handler)
    P-->>F: JoinCache proxy instance

The DefaultJoinCacheProxyFactory.create() at line 30:

  1. Looks up the first cache by firstCacheName (or by type if name is blank)
  2. Looks up the join cache by joinCacheName (or by type)
  3. Creates a JoinKeyExtractor that extracts the join key from the first cache's value
  4. Wraps them in a SimpleJoinCache delegate
  5. Creates a JDK proxy implementing the user's JoinCache interface

Complete Registration Flow Diagram

mermaid
graph TB
    subgraph sg_17 ["1. Annotation Parsing"]

        A1["@EnableCoCache<br>caches = [MyCache::class]"]
        A2["EnableCoCacheRegistrar"]
        A3["CoCacheMetadataParser.parse()"]
        A4["CoCacheMetadata"]
    end

    subgraph sg_18 ["2. Bean Definition"]

        B1["Register CoCacheMetadata bean<br>(name: cacheName.CacheMetadata)"]
        B2["Register CacheProxyFactoryBean<br>(name: cacheName, primary: true)"]
    end

    subgraph sg_19 ["3. Proxy Construction"]

        C1["CacheProxyFactoryBean.getObject()"]
        C2["DefaultCacheProxyFactory.create()"]
        C3["Create L2 + L1 + L0 + KeyConverter"]
        C4["CoherentCacheFactory.create()"]
        C5["DefaultCoherentCache<br>(registers with Event Bus)"]
        C6["CoCacheInvocationHandler"]
        C7["JDK Proxy.newProxyInstance()"]
    end

    A1 --> A2 --> A3 --> A4
    A4 --> B1
    A4 --> B2
    B2 --> C1 --> C2 --> C3 --> C4 --> C5 --> C6 --> C7

    style A1 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style A2 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style A3 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style A4 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style B1 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style B2 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style C1 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style C2 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style C3 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style C4 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style C5 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style C6 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3
    style C7 fill:#2d333b,stroke:#6d5dfc,color:#e6edf3

Key Class Relationships

Class/InterfaceRoleModuleSource
@EnableCoCacheTriggers registration via @Importcocache-springEnableCoCache.kt
EnableCoCacheRegistrarParses annotations, registers bean definitionscocache-springEnableCoCacheRegistrar.kt
CoCacheMetadataParsed cache configurationcocache-coreCoCacheMetadata.kt
CoCacheMetadataParserReflective interface parsercocache-coreCoCacheMetadataParser.kt
CoCacheProxyAbstract InvocationHandler with default method supportcocache-coreCoCacheProxy.kt
CoCacheInvocationHandlerConcrete handler with delegate/metadata accesscocache-coreCoCacheInvocationHandler.kt
DefaultCacheProxyFactoryAssembles all components and creates proxycocache-coreDefaultCacheProxyFactory.kt
CacheProxyFactoryBeanSpring FactoryBean bridgecocache-springCacheProxyFactoryBean.kt
JoinCacheProxyFactoryBeanSpring FactoryBean for JoinCachecocache-springJoinCacheProxyFactoryBean.kt
DefaultJoinCacheProxyFactoryCreates JoinCache proxies with two cache compositioncocache-coreDefaultJoinCacheProxyFactory.kt

Source References

FileLine(s)Description
EnableCoCache.kt20-24@EnableCoCache annotation definition
EnableCoCacheRegistrar.kt31-98Bean definition registrar
CoCacheMetadata.kt20-33Parsed metadata data class
CoCacheMetadataParser.kt30-57Reflective parser
CoCacheProxy.kt34-41Abstract InvocationHandler
CoCacheInvocationHandler.kt37-46Concrete invocation handler
DefaultCacheProxyFactory.kt40-68Proxy factory assembling all components
CacheProxyFactoryBean.kt23-39Spring FactoryBean
JoinCacheProxyFactoryBean.kt23-39JoinCache FactoryBean
DefaultJoinCacheProxyFactory.kt30-65JoinCache proxy factory

Released under the Apache License 2.0.