Springboot整合J2cache实现声明式缓存方案

Springboot整合J2Cache

一、J2Caceh多级缓存

​ J2Cache 是 OSChina 目前正在使用的两级缓存框架(要求至少 Java 8)。

​ 第一级缓存使用内存(同时支持 Ehcache 2.x、Ehcache 3.x 和 Caffeine),第二级缓存使用 Redis(推荐)/Memcached 。

  • L1: 进程内缓存 caffeine(默认使用) / ehcache

  • L2: 集中式缓存 Redis(推荐使用) / Memcached

      由于大量的缓存读取会导致 L2 的网络成为整个系统的瓶颈,因此 **L1 的目标是降低对 L2 的读取次数**。 该缓存框架主要用于集群环境中。单机也可使用,用于**避免应用重启导致的缓存冷启动后对后端业务的冲击**、重启导致的ehcache缓存数据丢失。
    
      J2Cache **默认使用Caffeine(https://gitee.com/link?target=https%3A%2F%2Fwww.oschina.net%2Fp%2Fben-manes-caffeine)作为一级缓存**,使用 Redis 作为二级缓存(https://so.csdn.net/so/search?q=%E4%BA%8C%E7%BA%A7%E7%BC%93%E5%AD%98&spm=1001.2101.3001.7020) 。你还可以选择 Ehcache2 和 Ehcache3 作为一级缓存。J2Cache 从 1.3.0 版本开始支持 JGroups 和 Redis Pub/Sub 两种方式进行缓存事件的通知。在某些云平台上可能无法使用 JGroups 组播方式,可以采用 Redis 发布订阅的方式。详情请看 j2cache.properties 配置文件的说明。
    

1.1 设计思路

​ 将Ehcache和Redis结合起来,将Ehcache(也可以使用caffeine cache)作为一级缓存、将redis(也可以使用memcached )作为二级缓存,取长补短。

​ 尽量从本机取数据,取不到的时候再去redis里面取。这样结合可以保证高性能。数据基本上都是从Ehcache里面取得,有效的缓解应用冷启动对数据库的压力,应用和redis之间不会频繁的有大量数据传输。数据传输只存在应用冷启动及缓存变更时。

  • 数据读取顺序:-> L1(进程内缓存) -> L2(集中式缓存) -> DB(数据库缓存)

​ J2Cache 目前提供两种节点间数据同步的方案 ------ Redis Pub/Sub 和 JGroups 。当某个节点的缓存数据需要更新时,J2Cache 会通过 Redis 的消息订阅机制或者是 JGroups 的组播来通知集群内其他节点。当其他节点收到缓存数据更新的通知时,它会清掉自己内存里的数据,然后重新从 Redis 中读取最新数据。这就完成了 J2Cache 缓存数据读写的闭环。

  • 数据更新顺序:
    1. 从数据库中读取最新数据,依次更新 L1 -> L2 ,发送广播清除某个缓存信息
    2. 接收到广播(手工清除缓存 & 一级缓存自动失效),从 L1 中清除指定的缓存信息

二、J2Cache二级缓存的实现

​ J2Cache 默认使用 Caffeine 作为一级缓存,使用 Redis 作为二级缓存。你还可以选择 Ehcache2 和 Ehcache3 作为一级缓存。

准备工作:

  1. 安装 Redis

  2. 新建一个基于 Maven 的 Java 项目

2.1 引入Maven

xml 复制代码
<!--  J2Cache(两级缓存): 一个基于Redis的Java缓存框架,支持多种缓存策略,并提供了多种缓存注解,可以方便的集成到Spring、Spring Boot等主流框架中。   -->
        <!--  j2cache与spring bott整合的工具 -->
        <dependency>
            <groupId>net.oschina.j2cache</groupId>
            <artifactId>j2cache-spring-boot2-starter</artifactId>
            <version>2.8.0-release</version>
        </dependency>
        <!--  j2cache 的核心包 -->
        <dependency>
            <groupId>net.oschina.j2cache</groupId>
            <artifactId>j2cache-core</artifactId>
            <version>2.8.4-release</version>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-simple</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-api</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

2.2 配置文件的准备

拷贝 j2cache.propertiescaffeine.properties 到你项目的源码目录,并确保这些文件会被编译到项目的 classpath 中。

  • j2cache.properties 配置文件内容

    properties 复制代码
    ## 1级缓存
    #j2cache.L1.provider_class = ehcache
    #ehcache.configXml = ehcache.xml
    #
    ## 2级缓存
    #j2cache.L2.provider_class =net.oschina.j2cache.cache.support.redis.SpringRedisProvider
    #j2cache.L2.config_section = redis
    #redis.hosts = localhost:6379
    ## 1级缓存中的数据如何到达2级缓存
    #j2cache.broadcast =net.oschina.j2cache.cache.support.redis.SpringRedisPubSubPolicy
    
    
    #J2Cache configuration
    
    
    #########################################
    # Level 1&2 provider
    # values:
    # none -> disable this level cache
    # ehcache -> use ehcache2 as level 1 cache
    # ehcache3 -> use ehcache3 as level 1 cache
    # caffeine -> use caffeine as level 1 cache(only in memory)
    # redis -> use redis as level 2 cache (using jedis)
    # lettuce -> use redis as level 2 cache (using lettuce)
    # readonly-redis -> use redis as level 2 cache ,but never write data to it. if use this provider, you must uncomment `j2cache.L2.config_section` to make the redis configurations available.
    # memcached -> use memcached as level 2 cache (xmemcached),
    # [classname] -> use custom provider
    #########################################
    
    # 一级缓存
    
    j2cache.L1.provider_class = caffeine
    caffeine.properties = /caffeine.properties
    # 内嵌的方式配置caffeine 中的配置信息
    #caffeine.default = 1000, 30m
    #caffeine.rx=50, 2h
    #caffeine.users=50, 2h
    
    # 二级缓存
    # 启用L2缓存 Redis
    #j2cache.L2.provider_class = redis
    # 是否启用同步一级缓存的Time-To-Live超时时间到Redis TTL(true启用,false不启用则永不超时)
    j2cache.sync_ttl_to_redis = true
    j2cache.serialization = json
    # 启用L2缓存 lettuce
    j2cache.L2.provider_class = lettuce
    # xxx使用rabbitmq广播通知
    #j2cache.broadcast = lettuce
    j2cache.broadcast =net.oschina.j2cache.cache.support.redis.SpringRedisPubSubPolicy
    
    # When L2 provider isn't `redis`, using `L2.config_section = redis` to read redis configurations
    # j2cache.L2.config_section = redis
    
    # Enable/Disable ttl in redis cache data (if disabled, the object in redis will never expire, default:true)
    # NOTICE: redis hash mode (redis.storage = hash) do not support this feature)
    
    # Whether to cache null objects by default (default false)
    j2cache.default_cache_null_object = true
    
    #########################################
    # Cache Serialization Provider
    # values:
    # fst -> using fast-serialization (recommend)
    # kryo -> using kryo serialization
    # json -> using fst's json serialization (testing)
    # fastjson -> using fastjson serialization (embed non-static class not support)
    # java -> java standard
    # fse -> using fse serialization
    # [classname implements Serializer]
    #########################################
    #json.map.person = net.oschina.j2cache.demo.Person
    
    #########################################
    # Ehcache configuration
    #########################################
    
    # ehcache.configXml = /ehcache.xml
    
    # ehcache3.configXml = /ehcache3.xml
    # ehcache3.defaultHeapSize = 1000
    
    #########################################
    # Caffeine configuration
    # caffeine.region.[name] = size, xxxx[s|m|h|d]
    #
    #########################################
    
    #########################################
    # Redis connection configuration
    #########################################
    
    #########################################
    # Redis Cluster Mode
    #
    # single -> single redis server
    # sentinel -> master-slaves servers
    # cluster -> cluster servers (\u6570\u636e\u5e93\u914d\u7f6e\u65e0\u6548\uff0c\u4f7f\u7528 database = 0\uff09
    # sharded -> sharded servers  (\u5bc6\u7801\u3001\u6570\u636e\u5e93\u5fc5\u987b\u5728 hosts \u4e2d\u6307\u5b9a\uff0c\u4e14\u8fde\u63a5\u6c60\u914d\u7f6e\u65e0\u6548 ; redis://user:password@127.0.0.1:6379/0\uff09
    #
    #########################################
    
    redis.mode = single
    
    #redis storage mode (generic|hash)
    redis.storage = generic
    
    ## redis pub/sub channel name
    redis.channel = j2cache
    ## redis pub/sub server (using redis.hosts when empty)
    redis.channel.host =
    
    #cluster name just for sharded
    redis.cluster_name = j2cache
    
    ## redis cache namespace optional, default[empty]
    redis.namespace =
    
    ## redis command scan parameter count, default[1000]
    #redis.scanCount = 1000
    
    ## connection
    # Separate multiple redis nodes with commas, such as 192.168.0.10:6379,192.168.0.11:6379,192.168.0.12:6379
    
    redis.hosts = 119.91.255.122:6379
    redis.timeout = 5000
    redis.password = lcjRedis123..
    redis.database = 10
    redis.ssl = false
    
    ## redis pool properties
    # 最大连接数(获取连接时进行判断,也即连接数的上限)
    redis.maxTotal = 100
    # 最大空闲连接数(在归还连接时判断,若当前连接数超过maxIdle连接数,则释放归还的连接)
    redis.maxIdle = 10
    # 获取连接的最大等待时长ms(当连接池已耗尽时)
    redis.maxWaitMillis = 5000
    # 驱逐空闲连接的最小间隔时间
    redis.minEvictableIdleTimeMillis = 60000
    # 最小空闲连接数(超过maxIdle则使用maxIdle值,单独的Evict任务负责清理超时的连接)
    redis.minIdle = 1
    redis.numTestsPerEvictionRun = 10
    # 获取资源后进先出(false则对应先进先出)
    redis.lifo = false
    redis.softMinEvictableIdleTimeMillis = 10
    # 是否测试连接可用
    redis.testOnBorrow = true
    redis.testOnReturn = false
    redis.testWhileIdle = true
    redis.timeBetweenEvictionRunsMillis = 300000
    # 当资源池耗尽,获取连接时是否需要等待
    redis.blockWhenExhausted = false
    redis.jmxEnabled = false
    
    #########################################
    # Lettuce scheme
    #
    # redis -> single redis server
    # rediss -> single redis server with ssl
    # redis-sentinel -> redis sentinel
    # redis-cluster -> cluster servers
    #
    #########################################
    
    #########################################
    # Lettuce Mode
    #
    # single -> single redis server
    # sentinel -> master-slaves servers
    # cluster -> cluster servers (\u6570\u636e\u5e93\u914d\u7f6e\u65e0\u6548\uff0c\u4f7f\u7528 database = 0\uff09
    # sharded -> sharded servers  (\u5bc6\u7801\u3001\u6570\u636e\u5e93\u5fc5\u987b\u5728 hosts \u4e2d\u6307\u5b9a\uff0c\u4e14\u8fde\u63a5\u6c60\u914d\u7f6e\u65e0\u6548 ; redis://user:password@127.0.0.1:6379/0\uff09
    #
    #########################################
    
    ## redis command scan parameter count, default[1000]
    #lettuce.scanCount = 1000
    lettuce.mode = single
    lettuce.namespace =
    lettuce.storage = hash
    lettuce.channel = j2cache
    lettuce.scheme = redis
    lettuce.hosts = 119.91.255.122:6379
    lettuce.password = lcjRedis123..
    lettuce.database = 10
    lettuce.sentinelMasterId =
    lettuce.sentinelPassword =
    lettuce.maxTotal = 100
    lettuce.maxIdle = 10
    lettuce.minIdle = 10
    # timeout in milliseconds
    lettuce.timeout = 10000
    # redis cluster topology refresh interval in milliseconds
    lettuce.clusterTopologyRefresh = 3000
    
    
    ## 1级缓存中的数据如何到达2级缓存
    #########################################
    # Cache Broadcast Method
    # values:
    # jgroups -> use jgroups's multicast
    # redis -> use redis publish/subscribe mechanism (using jedis)
    # lettuce -> use redis publish/subscribe mechanism (using lettuce, Recommend)
    # rabbitmq -> use RabbitMQ publisher/consumer mechanism
    # rocketmq -> use RocketMQ publisher/consumer mechanism
    # none -> don't notify the other nodes in cluster
    # xx.xxxx.xxxx.Xxxxx your own cache broadcast policy classname that implement net.oschina.j2cache.cluster.ClusterPolicy
    #########################################
    
    #j2cache.broadcast = redis
    #j2cache.broadcast =net.oschina.j2cache.cache.support.redis.SpringRedisPubSubPolicy
    
    # jgroups properties
    jgroups.channel.name = j2cache
    jgroups.configXml = /network.xml
    
    # RabbitMQ properties
    rabbitmq.exchange = j2cache
    rabbitmq.host = localhost
    rabbitmq.port = 5672
    rabbitmq.username = guest
    rabbitmq.password = guest
    
    # RocketMQ properties
    rocketmq.name = j2cache
    rocketmq.topic = j2cache
    # use ; to split multi hosts
    rocketmq.hosts = 127.0.0.1:9876
  • caffeine作为一级缓存时的配置文件 caffeine.properties

    properties 复制代码
    #########################################
    # Caffeine configuration
    # [name] = size, xxxx[s|m|h|d]
    #########################################
    # 定义缓存名default,对象大小1000,缓存数据有效时间30分钟。 可以定义多个不同名称的缓存。
    default = 1000, 30m
    rx=50, 2h

​ 注意:如果你选择了 ehcache 作为一级缓存,需要拷贝 ehcache.xml 或者 ehcache3.xml 到源码目录(后者对应的是 Ehcache 3.x 版本)2.3.1中有提供

  • 采用application.yml 配置相关信息方案 [该方式也需要在resources下添加 对应的一级缓存配置文件]

    yaml 复制代码
    #L1: 进程内缓存 caffeine/ehcache
    #L2: 集中式缓存 Redis/Memcached
    j2cache:
    #  config-location: j2cache.properties
      cache-clean-mode: passive
      allow-null-values: true
      redis-client: lettuce #指定redis客户端使用lettuce,也可以使用Jedis
      l2-cache-open: true #开启二级缓存
      #利用Redis的发布/订阅功能实现的广播策略。通过这个配置,可以在使用J2Cache缓存时,实现缓存的广播通知,确保缓存的一致性。
      broadcast: net.oschina.j2cache.cache.support.redis.SpringRedisPubSubPolicy
    #  broadcast: jgroups
      L1: #指定一级缓存提供者为ehcache/caffeine
        provider_class: caffeine
      L2: #指定二级缓存提供者为redis
        provider_class: net.oschina.j2cache.cache.support.redis.SpringRedisProvider
        config_section: lettuce
      # 是否启用同步一级缓存的Time-To-Live超时时间到Redis TTL(true启用,false不启用则永不超时)
      sync_ttl_to_redis: true
      default_cache_null_object: false
      serialization: json  #序列化方式:fst、kyro、Java\json
    caffeine:  # 这个配置文件需要放在项目中
      properties: /caffeine.properties
    lettuce:
      mode: single
      namespace:
      storage: generic
      channel: j2cache
      scheme: redis
      hosts: ${test.redis.ip}:${test.redis.port}
      password: ${test.redis.password}
      database: ${test.redis.database}
      pool:
        database: 15
        # 连接池中的最小空闲连接
        min-idle: 2
        # 连接池中的最大空闲连接
        max-idle: 5
        # 连接池的最大数据库连接数
        max-active: 10
        # #连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1ms
      sentinelMasterId:
      
    # =========== 配置文件参数设置 ===========
      test:
        redis:
          ip: 127.0.0.1
          port: 6379
          password: lcjRedis123..
          database: 15

2.3 其它实行方案

2.3.1 使用ehcache作为一级缓存

​ 首先修改 j2cache.properties 中的 j2cache.L1.provider_class 为 ehcache 或者 ehcache3,然后拷贝 ehcache.xml 或者 ehcache3.xml 到类路径,并配置好缓存,需要在项目中引入对 ehcache 的支持:

xml 复制代码
<dependency><!-- Ehcache 2.x //-->
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>2.10.4</version>
</dependency>

<dependency><!-- Ehcache 3.x //-->
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>3.4.0</version>
</dependency>

由于ehcache的配置有独立的配置文件格式,因此还需要指定ehcache的配置文件,以便于读取相应配置

  • ehcache.xml 配置文件

    xml 复制代码
    <!-- for ehcache 2.x -->
    <ehcache updateCheck="false" dynamicConfig="false">
    
        <diskStore path="java.io.tmpdir"/>
    
    	<cacheManagerEventListenerFactory class="" properties=""/>
    
        <!--Default Cache configuration. These will applied to caches programmatically created through
            the CacheManager.
    
            The following attributes are required for defaultCache:
    
            maxInMemory       - Sets the maximum number of objects that will be created in memory
            eternal           - Sets whether elements are eternal. If eternal,  timeouts are ignored and the element
                                is never expired.
            timeToIdleSeconds - Sets the time to idle for an element before it expires. Is only used
                                if the element is not eternal. Idle time is now - last accessed time
            timeToLiveSeconds - Sets the time to live for an element before it expires. Is only used
                                if the element is not eternal. TTL is now - creation time
            overflowToDisk    - Sets whether elements can overflow to disk when the in-memory cache
                                has reached the maxInMemory limit.
    
            -->
         <!--默认缓存策略 -->
        <!-- external:是否永久存在,设置为true则不会被清除,此时与timeout冲突,通常设置为false-->
        <!-- diskPersistent:是否启用磁盘持久化-->
        <!-- maxElementsInMemory:最大缓存数量-->
        <!-- overflowToDisk:超过最大缓存数量是否持久化到磁盘-->
        <!-- timeToIdleSeconds:最大不活动间隔,设置过长缓存容易溢出,设置过短无效果,可用于记录时效性数据,例如验证码-->
        <!-- timeToLiveSeconds:最大存活时间-->
        <!-- memoryStoreEvictionPolicy:缓存清除策略-->
        <defaultCache
            eternal="false"
            diskPersistent="false"
            maxElementsInMemory="1000"
            eternal="false"
            timeToIdleSeconds="1800"
            timeToLiveSeconds="1800"
            overflowToDisk="true">
        </defaultCache>
    
        <!--Predefined caches.  Add your cache configuration settings here.
            If you do not have a configuration for your cache a WARNING will be issued when the
            CacheManager starts
    
            The following attributes are required for defaultCache:
    
            name              - Sets the name of the cache. This is used to identify the cache. It must be unique.
            maxInMemory       - Sets the maximum number of objects that will be created in memory
            eternal           - Sets whether elements are eternal. If eternal,  timeouts are ignored and the element
                                is never expired.
            timeToIdleSeconds - Sets the time to idle for an element before it expires. Is only used
                                if the element is not eternal. Idle time is now - last accessed time
            timeToLiveSeconds - Sets the time to live for an element before it expires. Is only used
                                if the element is not eternal. TTL is now - creation time
            overflowToDisk    - Sets whether elements can overflow to disk when the in-memory cache
                                has reached the maxInMemory limit.
    
            -->
    
        <cache name="example"
            maxElementsInMemory="5000"
            eternal="false"
            timeToIdleSeconds="1800"
            timeToLiveSeconds="1800"
            overflowToDisk="false"
            >
        </cache>
    
    </ehcache>
  • ehcache3.xml 配置文件

    xml 复制代码
    <!-- for ehcache 3.x -->
    <config
            xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
            xmlns='http://www.ehcache.org/v3'
            xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core.xsd">
    
        <!-- Don't remote default cache configuration -->
        <cache-template name="default">
            <key-type>java.lang.String</key-type>
            <value-type>java.io.Serializable</value-type>
            <expiry>
                <ttl unit="seconds">1800</ttl>
            </expiry>
            <resources>
                <heap>1000</heap>
                <offheap unit="MB">100</offheap>
            </resources>
        </cache-template>
    
        <cache alias="default" uses-template="default"/>
    
    </config>
2.3.2 使用 RabbitMQ 作为消息通知

​ 首先修改 j2cache.properties 中的 j2cache.broadcast 为 rabbitmq,然后在 j2cache.properties 中配置 rabbitmq.xxx 相关信息。

需要在项目中引入对 rabbitmq 的支持:

xml 复制代码
<dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
    <version>5.3.0</version>
</dependency>
2.3.3 使用 memcached 作为二级缓存

首先修改 j2cache.properties 中的 j2cache.L2.provider_class 为 memcached,然后在 j2cache.properties 中配置 memcached.xxx 相关信息。

需要在项目中引入对 memcached 的支持:

xml 复制代码
<dependency>
    <groupId>com.googlecode.xmemcached</groupId>
    <artifactId>xmemcached</artifactId>
    <version>2.4.5</version>
</dependency>
2.3.4 如何使用 JGroups组传播方式(无法在云主机中使用)

​ 首先修改 j2cache.properties 中的 j2cache.broadcast 值为 jgroups,然后在 maven 中引入

xml 复制代码
<dependency>
    <groupId>org.jgroups</groupId>
    <artifactId>jgroups</artifactId>
    <version>3.6.13.Final</version>
</dependency>

三、声明式缓存

​ 使用j2cache可以将数据进行多级缓存。如果项目中很多模块都需要使用缓存功能,这些模块都需要调用j2cache的API来进行缓存操作,这种j2cache提供的原生API使用起来就比较繁琐了,并且操作缓存的代码和我们的业务代码混合到一起,即j2cache的API对我们的业务代码具有侵入性。

​ 基于上述情况,我们可以采用声明式缓存。即:定义缓存注解,在需要使用缓存功能的方法上加入缓存注解即可自动进行缓存操作。

​ 注意:j2cache原生API和我们实现的声明式缓存可以兼容,即在项目中可以同时使用,互为补充。例如在Controller的方法中需要将多类业务数据载入缓存,此时通过声明式缓存就无法做到(因为声明式缓存只能将方法的返回值载入缓存),这种场景下就需要调用j2cache的原生API来完成。

3.1 实现思路

​ 声明式缓存底层实现原理是基于AOP,通过代理技术来实现的。更确切的说,就是通过Spring提供的拦截器来拦截Controller,在拦截器中动态获取Controller方法上的注解,从而进行缓存相关操作。

​ 要实现声明式缓存,需要设计如下主要的类和注解:

  • Cache:缓存注解,在Controller的方法上使用,用于缓存此方法的返回值

  • CacheEvictor:清理缓存注解,在Controller的方法上使用,用于清理指定缓存数据

  • CacheMethodInterceptor:缓存拦截器,用于拦截加入缓存相关注解的Controller方法

  • AbstractCacheAnnotationProcessor:抽象缓存注解处理器,为缓存操作提供一些公共方法

  • CachesAnnotationProcessor:缓存注解处理器,当Controller的方法上加入Cache注解时由此处理器进行缓存处理

  • CacheEvictorAnnotationProcessor:失效缓存注解处理器,当Controller的方法上加入CacheEvictor注解时由此处理器进行缓存失效处理

  • EnableCache:开启缓存功能注解,一般在项目的启动类上使用,用于开启缓存功能

3.2 声明式缓存代码实现

3.2.1 自定义缓存注解类
  • 自定义注解,用于指定对应方法,标记该方法的返回值进行缓存

    java 复制代码
    package com.test.test_redis_j2cache.J2cache.annotation;
    
    import java.lang.annotation.*;
    
    /**
     * @ClassName : Cache
     * @Description : 缓存注解
     * 1. @Documented -- 表示使用该注解的元素应被javadoc或类似工具文档化,它应用于类型声明,类型声明的注解会影响客户端对注解元素的使用。如果一个类型声明添加了Documented注解,那么它的注解会成为被注解元素的公共API的一部分。
     * 2. @Target -- 表示支持注解的程序元素的种类,一些可能的值有TYPE, METHOD, CONSTRUCTOR, FIELD等等。如果Target元注解不存在,那么该注解就可以使用在任何程序元素之上。
     * 3. @Inherited -- 表示一个注解类型会被自动继承,如果用户在类声明的时候查询注解类型,同时类声明中也没有这个类型的注解,那么注解类型会自动查询该类的父类,这个过程将会不停地重复,直到该类型的注解被找到为止,或是到达类结构的顶层(Object)。
     * 4. @Retention -- 表示注解类型保留时间的长短,它接收RetentionPolicy参数,可能的值有SOURCE, CLASS, 以及RUNTIME。
     * @Author : AD
     */
    
    @Documented
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Cache {
        String region() default "rx"; //缓存区域
        String key() default "";  //缓存key
        String params() default ""; //缓存参数
    }

  • 自定义注解,用于标记清理指定方法的返回值缓存数据。(比如更新操作、删除操作中的缓存数据)

    java 复制代码
    package com.test.test_redis_j2cache.J2cache.annotation;
    
    import java.lang.annotation.*;
    
    /**
     * @ClassName : CacheEvictor
     * @Description : 失效缓存--清理缓存注解,用于清理指定缓存数据
     * @Author : AD
     */
    @Documented
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface CacheEvictor {
        Cache[] value() default {};
    }
3.2.2 自定义缓存注解模型映射对象
  • 缓存处理数据映射对象实体类

    java 复制代码
    package com.test.test_redis_j2cache.J2cache.model;
    
    import com.alibaba.fastjson2.JSONObject;
    import lombok.Data;
    
    import java.lang.annotation.Annotation;
    
    /**
     * @ClassName : CacheAnnotationInfo
     * @Description : 自定义缓存注解Cache信息包装--缓存数据模型
     * @Author : AD
     */
    
    @Data
    public class CacheAnnotationInfo<T extends Annotation> {
        private T annotation;
        private String key;
        private String region;
    
        @Override
        public String toString(){
            if (annotation == null){
                return null;
            }
            return JSONObject.toJSONString(this);
        }
    }
  • 缓存数据处理结果实映射对象

    java 复制代码
    package com.test.test_redis_j2cache.J2cache.model;
    
    /**
     * @ClassName : CacheHolder
     * @Description : 缓存处理结果封装类
     * @Author : AD
     */
    public class CacheHolder {
        /**
         * 缓存的数据
         * */
        private Object value;
    
        /**
         * 缓存数据是否存在
         * */
        private boolean existsCache;
    
       /**
        * 异常信息
        * */
        private Throwable throwable;
    
        /**
         * 初始化缓存占位
         */
        private CacheHolder() {
        }
    
        /**
         * 获取值
         *
         * @return
         */
        public Object getValue() {
            return value;
        }
    
        /**
         * 是否存在缓存
         *
         * @return
         */
        public boolean isExistsCache() {
            return existsCache;
        }
    
        /**
         * 是否有错误
         *
         * @return
         */
        public boolean hasError() {
            return throwable != null;
        }
    
        /**
         * 生成缓存结果的占位
         *
         * @param value       结果
         * @param existsCache 是否存在缓存
         * @return 缓存
         */
        public static CacheHolder newResult(Object value, boolean existsCache) {
            CacheHolder cacheHolder = new CacheHolder();
            cacheHolder.value = value;
            cacheHolder.existsCache = existsCache;
            return cacheHolder;
        }
    
        /**
         * 生成缓存异常的占位
         *
         * @param throwable 异常
         * @return 缓存
         */
        public static CacheHolder newError(Throwable throwable) {
            CacheHolder cacheHolder = new CacheHolder();
            cacheHolder.throwable = throwable;
            return cacheHolder;
        }
    }
3.2.3 缓存处理器工具类封装
  • 缓存键生成工具 CacheKeyBuilder

    java 复制代码
    package com.test.test_redis_j2cache.J2cache.utils;
    import org.springframework.util.StringUtils;
    /**
     * 缓存键生成工具
     */
    public class CacheKeyBuilder {
        /**
         * 生成key
         *
         * @param key      键
         * @param params   参数
         * @param args     参数值
         * @return
         * @throws IllegalAccessException 当访问异常时抛出
         */
        public static String generate(String key, String params, Object[] args) throws IllegalAccessException {
            StringBuilder keyBuilder = new StringBuilder("");
            if (StringUtils.hasText(key)) {
                keyBuilder.append(key);
            }
            if (StringUtils.hasText(params)) {
                String paramsResult = ObjectAccessUtils.get(args, params, String.class, "_", "null");
                keyBuilder.append(":");
                keyBuilder.append(paramsResult);
            }
            return keyBuilder.toString();
        }
    }
  • Spring上下文工具类 SpringApplicationContextUtils

    java 复制代码
    package com.test.test_redis_j2cache.J2cache.utils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.Primary;
    import org.springframework.stereotype.Component;
    import javax.annotation.PostConstruct;
    /**
     * Spring上下文工具类
     */
    @Primary
    @Component
    public class SpringApplicationContextUtils {
        private static ApplicationContext springContext;
        @Autowired
        private ApplicationContext applicationContext;
        @PostConstruct
        private void init() {
            springContext = applicationContext;
        }
    
        /**
         * 获取当前ApplicationContext
         *
         * @return ApplicationContext
         */
        public static ApplicationContext getApplicationContext() {
            return springContext;
        }
    }
  • 字符串封装工具 StringGenius

    java 复制代码
    package com.test.test_redis_j2cache.J2cache.utils;
    
    import org.springframework.util.StringUtils;
    
    import java.util.regex.Pattern;
    /**
     * 字符串工具类
     */
    public class StringGenius {
        /**
         * 是否是整数
         *
         * @param text 文本
         * @return true:是;false:否
         */
        public static boolean isInteger(String text) {
            if (!StringUtils.hasText(text)) {
                return false;
            }
            Pattern pattern = Pattern.compile("^[-\\+]?[\\d]*$");
            return pattern.matcher(text).matches();
        }
    }
  • 为访问对象提供路径支持 FieldAccessDescriptor

    java 复制代码
    package com.test.test_redis_j2cache.J2cache.utils;
    import java.util.Arrays;
    import java.util.List;
    import java.util.concurrent.ConcurrentHashMap;
    import java.util.stream.Collectors;
    /**
     * 为访问对象提供路径支持
     * <p>
     * address.id
     * <br>
     * detailList[0].name
     * <br>
     * payInfo.payMethodList[0].name
     * <br>
     * 0.text
     * <br>
     * data.address.city.text
     * <br>
     * ?0.text
     * <br>
     * detailList[?0].name
     * <br>
     * data.address?.city?.text
     * </p>
     */
    public class FieldAccessDescriptor {
        private static ConcurrentHashMap<String, FieldAccessDescriptor> cacheMap = new ConcurrentHashMap<>(128);
        private String currentPath;
        private String currentField;
        private int currentIndex;
        private boolean isOptionalAccess;
        private FieldAccessDescriptor nextFieldAccessDescriptor;
    
        private FieldAccessDescriptor(String fieldPath) {
            this(fieldPath, true);
        }
    
        private FieldAccessDescriptor(String fieldPath, boolean isFirst) {
            try {
                // 移除[和.符号
                if (fieldPath.startsWith("[") || fieldPath.startsWith(".")) {
                    fieldPath = fieldPath.substring(1);
                }
                if (fieldPath.startsWith("?")) {
                    this.isOptionalAccess = true;
                    // 移除可选访问符[?]
                    fieldPath = fieldPath.substring(1);
                    // 如果是逗点开头,则移除逗点[.]
                    if (fieldPath.startsWith(".")) {
                        fieldPath = fieldPath.substring(1);
                    }
                }
                int bracketIndex = fieldPath.indexOf('[');
                int commaIndex = fieldPath.indexOf('.');
                int optionalIndex = fieldPath.indexOf('?');
                List<Integer> indexList = Arrays.asList(bracketIndex, commaIndex, optionalIndex).stream().filter(i -> i > -1).collect(Collectors.toList());
                int index = -1;
                if (indexList.size() > 0) {
                    index = indexList.stream().min((i, i2) -> (i - i2)).get().intValue();
                }
                if (index < 0) {
                    // 解析终止
                    // 移除可能存在的右中括号
                    fieldPath = fieldPath.replace("]", "");
                    int accessIndex = getInt(fieldPath);
                    this.currentIndex = accessIndex;
                    this.currentField = fieldPath;
                    if (accessIndex > -1) {
                        this.currentPath = "[" + accessIndex + "]";
                    } else {
                        this.currentPath = fieldPath;
                    }
                } else {
                    String left = fieldPath.substring(0, index);
                    if (left == "") {
                        throw new IllegalArgumentException("非法的fieldPath格式");
                    }
                    String right = fieldPath.substring(index);
                    left = left.replace("]", "");
                    int accessIndex = getInt(left);
                    this.currentIndex = accessIndex;
                    this.currentField = left;
                    if (accessIndex > -1) {
                        this.currentPath = "[" + accessIndex + "]";
                    } else {
                        this.currentPath = left;
                    }
                    if (right != "") {
                        this.nextFieldAccessDescriptor = new FieldAccessDescriptor(right, false);
                    }
                }
                if (isFirst) {
                    fixPath();
                }
            } catch (Exception e) {
                throw new IllegalArgumentException("非法的fieldPath格式");
            }
        }
    
        private void fixPath() {
            if (hasNext()) {
                String separator = this.nextFieldAccessDescriptor.currentIndex > -1 ? "" : ".";
                this.nextFieldAccessDescriptor.currentPath = this.currentPath + separator + this.nextFieldAccessDescriptor.currentPath;
                this.nextFieldAccessDescriptor.fixPath();
            }
        }
    
        private int getInt(String text) {
            if (StringGenius.isInteger(text)) {
                return Integer.parseInt(text);
            } else {
                return -1;
            }
        }
    
        public String getCurrentPath() {
            return currentPath;
        }
    
        public String getCurrentField() {
            return currentField;
        }
    
        public int getCurrentIndex() {
            return currentIndex;
        }
    
        public boolean isOptionalAccess() {
            return isOptionalAccess;
        }
    
        public FieldAccessDescriptor getNextFieldAccessDescriptor() {
            return nextFieldAccessDescriptor;
        }
    
        public boolean accessArray() {
            return this.currentIndex > -1;
        }
    
        public boolean hasNext() {
            return this.nextFieldAccessDescriptor != null;
        }
    
        public String nextPath() {
            if (!hasNext()) {
                return "";
            } else {
                if (this.nextFieldAccessDescriptor.accessArray()) {
                    return String.format("[%s]", this.nextFieldAccessDescriptor.getCurrentIndex());
                } else {
                    return this.nextFieldAccessDescriptor.getCurrentField();
                }
            }
        }
    
        public static FieldAccessDescriptor parse(String fieldPath) {
            if (cacheMap.containsKey(fieldPath)) {
                return cacheMap.get(fieldPath);
            } else {
                FieldAccessDescriptor fieldAccessDescriptor = new FieldAccessDescriptor(fieldPath);
                cacheMap.put(fieldPath, fieldAccessDescriptor);
                return fieldAccessDescriptor;
            }
        }
    }
  • 对象访问工具类 ObjectAccessUtils

    java 复制代码
    package com.test.test_redis_j2cache.J2cache.utils;
    
    import com.alibaba.fastjson.JSON;
    import com.alibaba.fastjson.JSONArray;
    import com.alibaba.fastjson.JSONObject;
    import org.springframework.util.StringUtils;
    
    import java.util.ArrayList;
    /**
     * 对象访问工具类
     * <br>
     * 支持的路径格式参考:
     * <p>
     * address.id
     * <br>
     * detailList[0].name
     * <br>
     * payInfo.payMethodList[0].name
     * <br>
     * 0.text
     * <br>
     * data.address.city.text
     * <br>
     * ?0.text
     * <br>
     * detailList[?0].name
     * <br>
     * data.address?.city?.text
     * </p>
     */
    public class ObjectAccessUtils {
        /**
         * 以字段路径形式访问对象
         * <br>
         * 字段路径支持所有常见访问场景。比如:<br>
         * address.id <br>
         * detailList[0].name <br>
         * payInfo.payMethodList[0].name <br>
         * 0.text <br>
         * data.address.city.text <br>
         * ?0.text <br>
         * detailList[?2].name <br>
         * data.address?.city?.text <br>
         *
         * @param target    要访问的目标对象
         * @param fieldPath 字段路径
         * @param clazz     要返回的类型的类
         * @param <T>       要返回的类型
         * @return 值
         * @throws IllegalAccessException 当无法访问对象或者字段时触发
         */
        public static <T> T get(Object target, String fieldPath, Class<T> clazz) throws IllegalAccessException {
            return get(target, fieldPath, clazz, "");
        }
    
        /**
         * 以字段路径形式访问对象
         * <br>
         * 字段路径支持所有常见访问场景。比如:<br>
         * address.id <br>
         * detailList[0].name <br>
         * payInfo.payMethodList[0].name <br>
         * 0.text <br>
         * data.address.city.text <br>
         * ?0.text <br>
         * detailList[?2].name <br>
         * data.address?.city?.text <br>
         *
         * @param target    要访问的目标对象
         * @param fieldPath 字段路径
         * @param clazz     要返回的类型的类
         * @param delimiter 分隔符
         * @param <T>       要返回的类型
         * @return 值
         * @throws IllegalAccessException 当无法访问对象或者字段时触发
         */
        public static <T> T get(Object target, String fieldPath, Class<T> clazz, String delimiter) throws IllegalAccessException {
            return get(target, fieldPath, clazz, delimiter, null);
        }
    
        /**
         * 以字段路径形式访问对象
         * <br>
         * 字段路径支持所有常见访问场景。比如:<br>
         * address.id <br>
         * detailList[0].name <br>
         * payInfo.payMethodList[0].name <br>
         * 0.text <br>
         * data.address.city.text <br>
         * ?0.text <br>
         * detailList[?2].name <br>
         * data.address?.city?.text <br>
         *
         * @param target    要访问的目标对象
         * @param fieldPath 字段路径
         * @param clazz     要返回的类型的类
         * @param delimiter 分隔符
         * @param nullText  null时替代文本
         * @param <T>       要返回的类型
         * @return 值
         * @throws IllegalAccessException 当无法访问对象或者字段时触发
         */
        public static <T> T get(Object target, String fieldPath, Class<T> clazz, String delimiter, String nullText) throws IllegalAccessException {
            if (target == null) {
                throw new IllegalArgumentException("要访问的目标对象不能为空");
            } else if (fieldPath == null) {
                throw new IllegalArgumentException("要访问的目标对象的字段路径不能为空");
            } else if (!StringUtils.hasText(fieldPath)) {
                throw new IllegalArgumentException("要访问的目标对象的字段路径不能为空");
            }
            fieldPath = fieldPath.replaceAll(",", "+");
            if (fieldPath.contains("+")) {
                if (!String.class.equals(clazz)) {
                    throw new IllegalArgumentException("当字段路径中包含+时,clazz只能是String.class");
                }
                String[] fieldPathList = fieldPath.split("\\+");
                ArrayList<String> results = new ArrayList<>();
                if (StringUtils.hasText("s.")) {
                    results.add(get(target, fieldPathList[0], String.class));
                } else {
                    for (String fp : fieldPathList) {
                        if (StringUtils.hasText(fp)) {
                            String item = get(target, fp, String.class);
                            if (item == null) {
                                item = nullText;
                            }
                            results.add(item);
                        }
                    }
                }
                return (T) String.join(delimiter, results);
            }
    
            if (fieldPath.startsWith("*")) {
                // fieldPath = fieldPath.substring(1);
                throw new IllegalArgumentException("路径不能以*开头");
            }
            JSON targetElement;
            if (target instanceof JSON) {
                targetElement = (JSON) target;
            } else {
                String jsonText = JSONObject.toJSONString(target);
                targetElement = (JSON) JSONObject.parse(jsonText);
            }
            if (targetElement == null) {
                throw new IllegalArgumentException("无法以json形式访问目标对象");
            }
            FieldAccessDescriptor fieldAccessDescriptor = FieldAccessDescriptor.parse(fieldPath);
            Object valueElement = getValue(targetElement, fieldAccessDescriptor);
            Object result;
            if (valueElement == null) {
                result = null;
            } else {
                if (valueElement.getClass().equals(clazz)) {
                    result = valueElement;
                } else if (String.class.equals(clazz)) {
                    result = JSONObject.toJSONString(valueElement);
                } else {
                    result = JSONObject.parseObject(JSONObject.toJSONString(valueElement), clazz);
                }
            }
            if (result == null && String.class.equals(clazz)) {
                result = nullText;
            }
            return (T) result;
        }
    
        /**
         * 获取值
         *
         * @param targetElement         目标元素
         * @param fieldAccessDescriptor 访问标识符
         * @return 值
         * @throws IllegalAccessException 访问出错时抛出
         */
        private static Object getValue(Object targetElement, FieldAccessDescriptor fieldAccessDescriptor) throws IllegalAccessException {
            Object valueElement;
            boolean needBreak = false;
            if (fieldAccessDescriptor.accessArray()) {
                if (!(targetElement instanceof JSONArray)) {
                    throw new IllegalAccessException("要访问的索引的目标不是对象:" + fieldAccessDescriptor.getCurrentIndex());
                }
                JSONArray jsonArray = (JSONArray) targetElement;
                if (fieldAccessDescriptor.getCurrentIndex() < 0 || jsonArray.size() <= fieldAccessDescriptor.getCurrentIndex()) {
                    if (fieldAccessDescriptor.isOptionalAccess()) {
                        valueElement = null;
                        needBreak = true;
                    } else {
                        throw new IndexOutOfBoundsException("索引超界:" + fieldAccessDescriptor.getCurrentPath());
                    }
                } else {
                    valueElement = jsonArray.get(fieldAccessDescriptor.getCurrentIndex());
                }
            } else {
                if (targetElement == null) {
                    if (fieldAccessDescriptor.isOptionalAccess()) {
                        valueElement = null;
                        needBreak = true;
                    } else {
                        throw new IllegalAccessException("无法访问对象" + fieldAccessDescriptor.getCurrentPath());
                    }
                } else {
                    if (!(targetElement instanceof JSONObject)) {
    //                    throw new IllegalAccessException("要访问的字段的目标不是对象:" + fieldAccessDescriptor.getCurrentField());
                        if (targetElement instanceof JSONArray) {
                            JSONArray jsonArray = (JSONArray) targetElement;
                            String s = "";
                            for (int i = 0; i < jsonArray.size(); i++) {
                                s += jsonArray.getString(i) + "_";
                            }
                            return s.substring(0, s.length() - 1);
                        }
                    }
                    JSONObject jsonObject = (JSONObject) targetElement;
                    if (!jsonObject.containsKey(fieldAccessDescriptor.getCurrentField())) {
                        if (fieldAccessDescriptor.isOptionalAccess()) {
                            valueElement = null;
                            needBreak = true;
                        } else {
                            throw new IllegalAccessException("无法访问对象" + fieldAccessDescriptor.getCurrentPath());
                        }
                    } else {
                        valueElement = jsonObject.get(fieldAccessDescriptor.getCurrentField());
                    }
                }
            }
            if (!needBreak && fieldAccessDescriptor.hasNext()) {
                valueElement = getValue(valueElement, fieldAccessDescriptor.getNextFieldAccessDescriptor());
            }
            return valueElement;
        }
    }

3.2.4 缓存处理器封装

​ 这里需要注意需要创建三个缓存处理器。

​ 一个用来处理对应 @Cache注解的处理器,用来处理需要缓存的接口;另一个是用来处理 @CacheEvictor 注解的缓存处理器,来用处理需要清理指定缓存数据。

​ 最后一个缓存处理类为抽象类的缓存处理类,作为该两个处理器的父类,

  • 提供了一种模板或者接口,规定了子类必须实现的抽象方法,同时提供了一些通用的实现。

  • 提供了一种标准化的结构,可以被多个子类共享。

  • 用于在设计接口时提供一些共同的实现逻辑,减少了代码的重复性。

  • AbstractCacheAnnotationProcessor 两个子处理器的父类

    java 复制代码
    package com.test.test_redis_j2cache.J2cache.aop;
    
    import com.test.test_redis_j2cache.J2cache.annotation.Cache;
    import com.test.test_redis_j2cache.J2cache.annotation.CacheEvictor;
    import com.test.test_redis_j2cache.J2cache.model.CacheAnnotationInfo;
    import com.test.test_redis_j2cache.J2cache.utils.CacheKeyBuilder;
    import com.test.test_redis_j2cache.J2cache.utils.SpringApplicationContextUtils;
    import net.oschina.j2cache.CacheChannel;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.springframework.context.ApplicationContext;
    import org.springframework.util.StringUtils;
    
    import java.lang.reflect.Method;
    /**
     * @ClassName : AbstractCacheAnnotationProcessor
     * @Description : @Cache 缓存注解处理器[抽象类]
     * @Author : AD
     */
    public abstract class AbstractCacheAnnotationProcessor {
    
        protected CacheChannel cacheChannel;
     /**
       * Description: 初始化缓存注解处理器
       *     proceedingJoinPoint 切点
       * @param
       * @return
      */
      protected AbstractCacheAnnotationProcessor(){
          ApplicationContext applicationContext = SpringApplicationContextUtils.getApplicationContext();
          cacheChannel = applicationContext.getBean(CacheChannel.class);
      }
    
      /**
       * Description: 转换为注解信息
       *
       * @param proceedingJoinPoint
       * @param cache 注解信息
       * @return com.test.test_redis_j2cache.J2cache.model.CacheAnnotationInfo<com.test.test_redis_j2cache.J2cache.annotation.Cache> 注解信息实体类
      */
      protected CacheAnnotationInfo<Cache> getAnnotationInfo(ProceedingJoinPoint proceedingJoinPoint, Cache cache){
          CacheAnnotationInfo<Cache> annotationCacheAnnotationInfo = new CacheAnnotationInfo<>();
          annotationCacheAnnotationInfo.setAnnotation(cache);
          annotationCacheAnnotationInfo.setRegion(cache.region());
          try {
              annotationCacheAnnotationInfo.setKey(generateKey(proceedingJoinPoint, annotationCacheAnnotationInfo.getAnnotation()));
          } catch (IllegalAccessException e) {
              throw new IllegalArgumentException("生成键出错:", e);
          }
          return annotationCacheAnnotationInfo;
      }
    
      /**
       * 生成key字符串
       *
       * @param cache 缓存注解
       * @return key字符串
       */
      protected String generateKey(ProceedingJoinPoint proceedingJoinPoint, Cache cache) throws IllegalAccessException {
          String key = cache.key();
          if (!StringUtils.hasText(key)) {
              String className = proceedingJoinPoint.getTarget().getClass().getSimpleName();
              MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
              Method method = methodSignature.getMethod();
              key = className + ":" + method.getName();
          }
          key = CacheKeyBuilder.generate(key, cache.params(), proceedingJoinPoint.getArgs());
          return key;
      }
      /**
       * 抽象方法,处理缓存操作,具体应该由子类具体实现
       *
       * @param proceedingJoinPoint 切点
       * @return 处理结果
       */
      public abstract Object process(ProceedingJoinPoint proceedingJoinPoint) throws Throwable;
    
      /**
       *  获得缓存注解处理器对象
       *
       * @param proceedingJoinPoint 切点
       * @param cache               注解
       * @return 注解处理器
       */
      public static CachesAnnotationProcessor getProcessor(ProceedingJoinPoint proceedingJoinPoint, Cache cache) {
          return new CachesAnnotationProcessor(proceedingJoinPoint, cache);
      }
    
      /**
       * 获得清理缓存注解处理器对象
       *
       * @param proceedingJoinPoint 切点
       * @param cacheEvictor        注解
       * @return 注解处理器
       */
      public static CacheEvictorAnnotationProcessor  getProcessor(ProceedingJoinPoint proceedingJoinPoint, CacheEvictor cacheEvictor) {
          return new CacheEvictorAnnotationProcessor(proceedingJoinPoint, cacheEvictor);
      }
        
    }
  • 缓存注解处理器:CachesAnnotationProcessor
java 复制代码
package com.test.test_redis_j2cache.J2cache.aop;

import com.test.test_redis_j2cache.J2cache.annotation.Cache;
import com.test.test_redis_j2cache.J2cache.model.CacheAnnotationInfo;
import com.test.test_redis_j2cache.J2cache.model.CacheHolder;
import net.oschina.j2cache.CacheObject;
import org.aspectj.lang.ProceedingJoinPoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @ClassName : CacheAnnotationProcessor
 * @Description : Cache注解,缓存注解处理器
 * @Author : AD
 */
public class CachesAnnotationProcessor extends AbstractCacheAnnotationProcessor{

    private static final Logger logger = LoggerFactory.getLogger(CachesAnnotationProcessor.class);
    private CacheAnnotationInfo annotationInfo;
      /**
   * Description: 初始化处理器,同时将相关的对象进行初始化
   *
   * @param proceedingJoinPoint
   * @param cache
   * @return
  */
  public CachesAnnotationProcessor(ProceedingJoinPoint proceedingJoinPoint, Cache cache)
  {
      super();
      //创建注解信息对象
      annotationInfo = getAnnotationInfo(proceedingJoinPoint , cache);
  }

  /**
   * Description: 具体缓存处理逻辑
   *
   * @param proceedingJoinPoint
   * @return java.lang.Object
  */
  @Override
  public Object process(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
      Object result = null;
      boolean existsCache = false;
      //1、获取缓存数据
      CacheHolder cacheHolder = getCache(annotationInfo);
      if(cacheHolder.isExistsCache()){
          //2、如果缓存数据存在则直接返回(相当于controller的目标方法没有执行)
          result = cacheHolder.getValue();//缓存结果数据
          existsCache = true;
      }
      //如果不存在数据,则执行方法,将方法数据存入缓存
      if(!existsCache){
          //3、如何缓存数据不存在,放行调用Controller的目标方法
          result = invoke(proceedingJoinPoint);
          //4、将目标方法的返回值载入缓存
          setCache(result);
      }
      //5、将结果返回
      return result;
  }

  /**
   * 获取缓存数据
   * @param annotationInfo
   * @return
   */
  private CacheHolder getCache(CacheAnnotationInfo<Cache> annotationInfo){
      Object value = null;
      String region = annotationInfo.getRegion();
      String key = annotationInfo.getKey();
      boolean exists = cacheChannel.exists(region, key);
      int check = cacheChannel.check(region, key);
      logger.info("@Cache缓存数据[{}.{}]存在状态为: {}。  存在级缓存某级缓存:{}",region, key, exists,check);
      if(exists){
          CacheObject cacheObject = cacheChannel.get(region, key);
          //获得缓存结果数据
          value = cacheObject.getValue();
          return CacheHolder.newResult(value,true);
      }
      return CacheHolder.newResult(value,false);
  }
      /**
   * 调用目标方法
   * @param proceedingJoinPoint
   * @return
   */
  private Object invoke(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
      return proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
  }

  /**
   * 设置缓存数据
   * @param result
   */
  private void setCache(Object result){
      cacheChannel.set(annotationInfo.getRegion(),annotationInfo.getKey(),result);
  }
}
  • 清除缓存数据处理器

    java 复制代码
    package com.test.test_redis_j2cache.J2cache.aop;
    
    import com.test.test_redis_j2cache.J2cache.annotation.Cache;
    import com.test.test_redis_j2cache.J2cache.annotation.CacheEvictor;
    import com.test.test_redis_j2cache.J2cache.model.CacheAnnotationInfo;
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * @ClassName : CacheEvictorAnnotationProcessor
     * @Description : 清理缓存数据处理器
     * @Author : AD
     */
    public class CacheEvictorAnnotationProcessor extends AbstractCacheAnnotationProcessor{
        private static final Logger logger = LoggerFactory.getLogger(CacheEvictorAnnotationProcessor.class);
    
        /**
         * 封装注解信息集合
         * */
        private List<CacheAnnotationInfo<Cache>> cacheList = new ArrayList<>();
          /**
       * 初始化清理缓存注解处理器对象,同时初始化一些缓存操作的对象
       * @param proceedingJoinPoint
       * @param cacheEvictor
       */
      public CacheEvictorAnnotationProcessor(ProceedingJoinPoint proceedingJoinPoint, CacheEvictor cacheEvictor) {
          super();
          Cache[] value = cacheEvictor.value();
          for(Cache cache : value){
              CacheAnnotationInfo<Cache> annotationInfo = getAnnotationInfo(proceedingJoinPoint, cache);
              cacheList.add(annotationInfo);
          }
      }
    
      /**
       * Description: 具体清理缓存处理逻辑
       *
       * @param proceedingJoinPoint
       * @return java.lang.Object
      */
      @Override
      public Object process(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
          for (CacheAnnotationInfo<Cache> annotationInfo : cacheList) {
              String region = annotationInfo.getRegion();
              String key = annotationInfo.getKey();
              boolean exists = cacheChannel.exists(region, key);
              int check = cacheChannel.check(region, key);
              logger.info("@CacheEvictor清除缓存数据[{}.{}]存在状态为: {}。  存在级缓存某级缓存:{}",region, key, exists,check);
              //清理缓存数据
              cacheChannel.evict(region,key);
          }
          //调用目标方法(就是Controller中的方法)
          return proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
      }
    }
3.2.5 自定义缓存拦截器

​ 注意这里的Interceptor是org.aopalliance.intercept包下的Spring的AOP只能支持到方法级别的切入。换句话说,切入点只能是某个方法。

java 复制代码
package com.test.test_redis_j2cache.J2cache.aop;

import com.test.test_redis_j2cache.J2cache.annotation.Cache;
import com.test.test_redis_j2cache.J2cache.annotation.CacheEvictor;
import com.test.test_redis_j2cache.J2cache.utils.SpringApplicationContextUtils;
import com.test.test_redis_j2cache.TestRedisJ2CacheApplication;
import org.aopalliance.intercept.Interceptor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Import;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * @ClassName : 拦截方法上使用Cache注解的Controller
 * @Description : 缓存拦截器Aop
 * @Author : AD
 */
@Aspect
@Component
@EnableAspectJAutoProxy(proxyTargetClass = true) //指定使用cglib方式为Controller创建代理对象,代理对象其实是目标对象的子类
@Import(SpringApplicationContextUtils.class)
public class CacheMethodHandleInterceptor implements Interceptor {

    //@Around注解,表示这是一个环绕通知。环绕通知是所有通知里功能最为强大的通知,可以实现前置通知、后置通知、异常通知以及返回通知的功能。目标方法进入环绕通知后,通过调用ProceedingJoinPoint对象的proceed方法使目标方法继续执行,开发者可以在此修改目标方法的执行参数、返回值等,并且可以在此处理目标方法的异常。
    @Around("@annotation(com.test.test_redis_j2cache.J2cache.annotation.Cache)")
    public Object invokeCacheAllMethod(ProceedingJoinPoint proceedingJoinPoint) throws Throwable
    {
        MethodSignature methodSignature  =(MethodSignature) proceedingJoinPoint.getSignature();
        Cache cacheAnnotation = AnnotationUtils.findAnnotation(methodSignature.getMethod(), Cache.class);
        if (cacheAnnotation!= null)
        {
            System.out.println("需要进行设置缓存数据处理。。。。");
            //创建处理器,具体处理缓存逻辑
            CachesAnnotationProcessor processor = AbstractCacheAnnotationProcessor.getProcessor(proceedingJoinPoint, cacheAnnotation);
            return processor.process(proceedingJoinPoint);
        }
        //没有获取到Cache注解信息,直接放行
        return proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
    }


    /**
     * Description: 拦截方法上使用CacheEvictor注解的Controller
     *
     * @param proceedingJoinPoint
     * @return java.lang.Object
    */
    @Around("@annotation(com.test.test_redis_j2cache.J2cache.annotation.CacheEvictor)")
    public Object invokeCacheEvictorAllMethod(ProceedingJoinPoint proceedingJoinPoint) throws Throwable
    {
        //获得方法前面对象
        MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
        //获得当前拦截到的Controller方法对象
        Method method = signature.getMethod();
        //获得方法上的Cache注解信息
        CacheEvictor cacheEvictor =  AnnotationUtils.findAnnotation(method, CacheEvictor.class);
        if (cacheEvictor != null){
            System.out.println("清理缓存处理...");
            //创建清理缓存的处理器
            CacheEvictorAnnotationProcessor processor = AbstractCacheAnnotationProcessor.getProcessor(proceedingJoinPoint, cacheEvictor);
            return processor.process(proceedingJoinPoint);
        }
        return proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
    }
}
3.2.6 声明式注解的配置
  • @EnableCache注解

    java 复制代码
    package com.test.test_redis_j2cache.J2cache.annotation;
    
    import com.test.test_redis_j2cache.J2cache.aop.CacheMethodHandleInterceptor;
    import org.springframework.context.annotation.Import;
    
    import java.lang.annotation.*;
    
    /**
     * @ClassName : EnableCache
     * @Description : 开启缓存功能注解
     * @Author : AD
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE})
    @Documented
    @Import( CacheMethodHandleInterceptor.class)
    public @interface EnableCache {
    }
  • 在启动类上使用 @EnableCache

    java 复制代码
    package com.test.test_redis_j2cache;
    
    import com.test.test_redis_j2cache.J2cache.annotation.EnableCache;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    /**
     * @author AD
     */
    
    @SpringBootApplication
    @EnableCache
    public class TestRedisJ2CacheApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(TestRedisJ2CacheApplication.class, args);
        }
    }
3.2.7 测试类的创建
  • 测试类Controller创建

    java 复制代码
    package com.test.test_redis_j2cache.J2cache;
    
    import com.test.test_redis_j2cache.J2cache.annotation.Cache;
    import com.test.test_redis_j2cache.J2cache.annotation.CacheEvictor;
    import org.springframework.web.bind.annotation.*;
    
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * @ClassName : TestCacheAnnotationController
     * @Description : 测试 声明式缓存注解的使用
     * @Author : AD
     */
    
    @RestController
    @RequestMapping(value = "testCache")
    public class TestCacheAnnotationController {
    
        private String region = "testCache";
        /**
         * 查询地址簿详情
         * @param id
         * @return
         */
        @GetMapping("detail/{id}")
        @Cache(region = "testCache",key = "ab",params = "id")
        public Map detail(@PathVariable(name = "id") String id) {
            System.out.println("L1 、 L2 均不存在该数据缓存:查询方法执行了 DB !!");
            Map map = new HashMap<String, String>();
            map.put("查询id",id);
            map.put("name", "测试查询功能1");
            return map;
        }
    
        /**
         * 修改
         * @param id
         * @param map
         * @return
         */
        @PutMapping("put/{id}")
        @CacheEvictor(value = {@Cache(region = "testCache",key = "ab",params = "1.id")})
        public Map update(@PathVariable(name = "id") String id, @RequestBody Map map) {
            map.put("传入待修改的id",id);
            return map;
        }
    
        /**
         * 删除
         * @param id
         * @return
         */
        @DeleteMapping("del/{id}")
        @CacheEvictor({@Cache(region = "testCache",key = "ab",params = "id")})
        public Map del(@PathVariable(name = "id") String id) {
            Map map = new HashMap<String, String>();
            map.put("待删除id",id);
            map.put("name", "测试查询功能1");
            return map;
        }
    }
  • 查询接口 detail 测试

  • 修改接口 update 调用效果
  • 删除接口 del 测试效果

3.3 总结

​ 这里使用的是aspectj而非Springaop,故使用时用法有不一样。使用j2cache框架的整体逻辑:自定义缓存注解,类似springboot自带的cache,但是这里粒度更细,而且更好控制超时时间。

缓存层类似如下图:

​ 然后需要用到aspectj的aop逻辑,自定义横切关注点,这里的连接点即是controller层的方法,需要判断每个方法上是否存在cahce注解,如果不存在则直接放行( proceedingJoinPoint.proceed),如果存在则交给缓存处理器进行处理,这里添加和删除缓存主要用的是j2cache组件的cachechannel,个人理解它这里类似一个连接到缓存服务器的通道,且有相应的api可以供增删操作(cacheChannel.set(annotationInfo.getRegion(),annotationInfo.getKey(),result))。在读取缓存时首先是从一级缓存中取,然后从二级缓存中取,如果没找到则查询数据库。对于缓存结果的获得通过封装一个缓存结果类和获得cache注解的信息类来获得( AnnotationInfo ,制定了这个类的数据类型是Annotation的子类)。

相关推荐
郑祎亦11 分钟前
Spring Boot 项目 myblog 整理
spring boot·后端·java-ee·maven·mybatis
本当迷ya24 分钟前
💖2025年不会Stream流被同事排挤了┭┮﹏┭┮(强烈建议实操)
后端·程序员
计算机毕设指导61 小时前
基于 SpringBoot 的作业管理系统【附源码】
java·vue.js·spring boot·后端·mysql·spring·intellij-idea
paopaokaka_luck2 小时前
[371]基于springboot的高校实习管理系统
java·spring boot·后端
煎饼小狗2 小时前
Redis五大基本类型——Zset有序集合命令详解(命令用法详解+思维导图详解)
数据库·redis·缓存
捂月3 小时前
Spring Boot 深度解析:快速构建高效、现代化的 Web 应用程序
前端·spring boot·后端
瓜牛_gn3 小时前
依赖注入注解
java·后端·spring
Estar.Lee3 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
喜欢猪猪3 小时前
Django:从入门到精通
后端·python·django
一个小坑货3 小时前
Cargo Rust 的包管理器
开发语言·后端·rust