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

2024-09-02 23:52

本文主要是介绍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

<!--  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 配置文件内容

    ## 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:6379redis.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

    #########################################
    # 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下添加 对应的一级缓存配置文件]

    #L1: 进程内缓存 caffeine/ehcache
    #L2: 集中式缓存 Redis/Memcached
    j2cache:
    #  config-location: j2cache.propertiescache-clean-mode: passiveallow-null-values: trueredis-client: lettuce #指定redis客户端使用lettuce,也可以使用Jedisl2-cache-open: true #开启二级缓存#利用Redis的发布/订阅功能实现的广播策略。通过这个配置,可以在使用J2Cache缓存时,实现缓存的广播通知,确保缓存的一致性。broadcast: net.oschina.j2cache.cache.support.redis.SpringRedisPubSubPolicy
    #  broadcast: jgroupsL1: #指定一级缓存提供者为ehcache/caffeineprovider_class: caffeineL2: #指定二级缓存提供者为redisprovider_class: net.oschina.j2cache.cache.support.redis.SpringRedisProviderconfig_section: lettuce# 是否启用同步一级缓存的Time-To-Live超时时间到Redis TTL(true启用,false不启用则永不超时)sync_ttl_to_redis: truedefault_cache_null_object: falseserialization: json  #序列化方式:fst、kyro、Java\json
    caffeine:  # 这个配置文件需要放在项目中properties: /caffeine.properties
    lettuce:mode: singlenamespace:storage: genericchannel: j2cachescheme: redishosts: ${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: -1mssentinelMasterId:# =========== 配置文件参数设置 ===========test:redis:ip: 127.0.0.1port: 6379password: lcjRedis123..database: 15
    

2.3 其它实行方案

2.3.1 使用ehcache作为一级缓存

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

<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 配置文件

    <!-- 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 throughthe CacheManager.The following attributes are required for defaultCache:maxInMemory       - Sets the maximum number of objects that will be created in memoryeternal           - Sets whether elements are eternal. If eternal,  timeouts are ignored and the elementis never expired.timeToIdleSeconds - Sets the time to idle for an element before it expires. Is only usedif the element is not eternal. Idle time is now - last accessed timetimeToLiveSeconds - Sets the time to live for an element before it expires. Is only usedif the element is not eternal. TTL is now - creation timeoverflowToDisk    - Sets whether elements can overflow to disk when the in-memory cachehas reached the maxInMemory limit.--><!--默认缓存策略 --><!-- external:是否永久存在,设置为true则不会被清除,此时与timeout冲突,通常设置为false--><!-- diskPersistent:是否启用磁盘持久化--><!-- maxElementsInMemory:最大缓存数量--><!-- overflowToDisk:超过最大缓存数量是否持久化到磁盘--><!-- timeToIdleSeconds:最大不活动间隔,设置过长缓存容易溢出,设置过短无效果,可用于记录时效性数据,例如验证码--><!-- timeToLiveSeconds:最大存活时间--><!-- memoryStoreEvictionPolicy:缓存清除策略--><defaultCacheeternal="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 theCacheManager startsThe 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 memoryeternal           - Sets whether elements are eternal. If eternal,  timeouts are ignored and the elementis never expired.timeToIdleSeconds - Sets the time to idle for an element before it expires. Is only usedif the element is not eternal. Idle time is now - last accessed timetimeToLiveSeconds - Sets the time to live for an element before it expires. Is only usedif the element is not eternal. TTL is now - creation timeoverflowToDisk    - Sets whether elements can overflow to disk when the in-memory cachehas reached the maxInMemory limit.--><cache name="example"maxElementsInMemory="5000"eternal="false"timeToIdleSeconds="1800"timeToLiveSeconds="1800"overflowToDisk="false"></cache></ehcache>
    
  • ehcache3.xml 配置文件

    <!-- for ehcache 3.x -->
    <configxmlns: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 的支持:

<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 的支持:

<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 中引入

<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 自定义缓存注解类
  • 自定义注解,用于指定对应方法,标记该方法的返回值进行缓存

    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 "";  //缓存keyString params() default ""; //缓存参数
    }
    

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

    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 自定义缓存注解模型映射对象
  • 缓存处理数据映射对象实体类

    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;@Overridepublic String toString(){if (annotation == null){return null;}return JSONObject.toJSONString(this);}
    }
    
  • 缓存数据处理结果实映射对象

    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

    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

    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;@Autowiredprivate ApplicationContext applicationContext;@PostConstructprivate void init() {springContext = applicationContext;}/*** 获取当前ApplicationContext** @return ApplicationContext*/public static ApplicationContext getApplicationContext() {return springContext;}
    }
    

  • 字符串封装工具 StringGenius

    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

    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

    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 两个子处理器的父类

    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
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*/@Overridepublic 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);}
}

  • 清除缓存数据处理器

    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*/@Overridepublic 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只能支持到方法级别的切入。换句话说,切入点只能是某个方法。

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注解

    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

    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创建

    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的子类)。

这篇关于Springboot整合J2cache实现声明式缓存方案的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1131429

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

无人叉车3d激光slam多房间建图定位异常处理方案-墙体画线地图切分方案

墙体画线地图切分方案 针对问题:墙体两侧特征混淆误匹配,导致建图和定位偏差,表现为过门跳变、外月台走歪等 ·解决思路:预期的根治方案IGICP需要较长时间完成上线,先使用切分地图的工程化方案,即墙体两侧切分为不同地图,在某一侧只使用该侧地图进行定位 方案思路 切分原理:切分地图基于关键帧位置,而非点云。 理论基础:光照是直线的,一帧点云必定只能照射到墙的一侧,无法同时照到两侧实践考虑:关

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu