Spring Cache 缓存原理与 Redis 实践

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: Spring Cache 缓存原理与 Redis 实践

说到Spring Boot缓存,那就不得不提JSR-107规范,它告诉我们在Java中如何规范地使用缓存。

JSR是Java Specification Requests的简称,通常译为”Java 规范提案“。具体而言,是指向JCP(Java Community Process,Java标准制定组织)提出新增一个标准化技术规范的正式请求。任何人都可以提交JSR,通过一定的标准测试后,就可以向Java平台增添新的API和服务。JSR已成为Java界的一个重要标准。

JSR-107规范即JCache API,JCache规范定义了一种对Java对象临时在内存中进行缓存的方法,包括对象的创建、共享访问、假脱机(spooling)、失效、各JVM的一致性等,可被用于缓存JSP内最经常读取的数据,如产品目录和价格列表。利用JCache的缓存数据,可以加快大多数查询的反应时间(内部测试表明反应时间大约快15倍)。

一、JCache

在Spring Boot中使用缓存之前,我们有必要了解一下JCache。JCache定义了五个核心接口,分别是CachingProvider,CacheManager,Cache,Entry和Expiry。

  • 一个CachingProvider可以创建和管理多个CacheManager,并且一个CacheManager只能被一个CachingProvider所拥有,而一个应用可以在运行期间访问多个CachingProvider。
  • 一个CacheManager可以创建和管理多个唯一命名的Cache,且一个Cache只能被一个CacheManager所拥有,这些Cache存在于CacheManager的上下文中。
  • Cache是一个类似Map的数据结构
  • Entry是一个存储在Cache中的key-value对
  • Expiry是指存储在Cache中的Entry的有效期,一旦超过这个时间,Entry将处于过期状态,即不可访问、更新和删除。缓存有效期可以通过ExpiryPolicy设置。


二、Spring Cache原理

Spring 3.1开始,引入了Spring Cache,即Spring 缓存抽象。通过定义org.springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存技术,并支持使用JCache注解简化开发过程。

Cache接口为缓存的组件规范定义,包含缓存的各种操作集合。Spring中为Cache接口提供了各种xxxCache的实现:RedisCache,EhCacheCache,ConcurrentMapCache等。

我们通过部分源码详细了解一下Cache接口和CacheManager接口。

Cache接口

 public interface Cache {  
    //Cache名称
    String getName();
    //Cache负责缓存的对象
    Object getNativeCache();
    /**
     * 获取key对应的ValueWrapper
     * 没有对应的key,则返回null
     * key对应的value是null,则返回null对应的ValueWrapper
     */
    @Nullable
    Cache.ValueWrapper get(Object key);
    //返回key对应type类型的value
    @Nullable    <T> T get(Object key, @Nullable Class<T> type);
    //返回key对应的value,没有则缓存Callable::call,并返回
    @Nullable    <T> T get(Object key, Callable<T> valueLoader);
    //缓存目标key-value(替换旧值),不保证实时性
    void put(Object key, @Nullable Object value);
    //插入缓存,并返回该key对应的value;先调用get,不存在则用put实现
    @Nullable
    default Cache.ValueWrapper putIfAbsent(Object key, @Nullable Object value) {
        Cache.ValueWrapper existingValue = this.get(key);
        if (existingValue == null) {
            this.put(key, value);
        }
        return existingValue;
    }
    //删除缓存,不保证实时性
    void evict(Object key);
    //立即删除缓存:返回false表示剔除前不存在制定key活不确定是否存在;返回true,表示该key之前存在
    default boolean evictIfPresent(Object key) {
        this.evict(key);
        return false;
    }
    //清除所有缓存,不保证实时性
    void clear();
    //立即清楚所有缓存,返回false表示清除前没有缓存或不能确定是否有;返回true表示清除前有缓存
    default boolean invalidate() {
        this.clear();
        return false;
    }
    public static class ValueRetrievalException extends RuntimeException {
        @Nullable        private final Object key;
        public ValueRetrievalException(@Nullable Object key, Callable<?> loader, Throwable ex) {
            super(String.format("Value for key '%s' could not be loaded using '%s'", key, loader), ex);
            this.key = key;
        }
        @Nullable        public Object getKey() {
            return this.key;
        }
    }
    //缓存值的一个包装器接口,实现类为SimpleValueWrapper
    @FunctionalInterface
    public interface ValueWrapper {
        @Nullable
        Object get();
    }
}

可以看出,Cache接口抽象了缓存的 get put evict 等相关操作。

AbstractValueAdaptingCache

public abstract class AbstractValueAdaptingCache implements Cache {
    //是否允许Null值
    private final boolean allowNullValues;
    protected AbstractValueAdaptingCache(boolean allowNullValues) {
        this.allowNullValues = allowNullValues;
    }
    public final boolean isAllowNullValues() {
        return this.allowNullValues;
    }
    @Nullable    public ValueWrapper get(Object key) {
        return this.toValueWrapper(this.lookup(key));
    }
    @Nullable    public <T> T get(Object key, @Nullable Class<T> type) {
        //查询到的缓存值做fromStoreValue转换
        Object value = this.fromStoreValue(this.lookup(key));
        //转换后非null值且无法转换为type类型则抛出异常
        if (value != null && type != null && !type.isInstance(value)) {
            throw new IllegalStateException("Cached value is not of required type [" + type.getName() + "]: " + value);
        } else {
            return value;
        }
    }
    //从缓存中获取key对应的value,子类实现
    @Nullable
    protected abstract Object lookup(Object key);
    //对于从缓存中获取的值,允许为空且值为NullValue时,处理为null
    @Nullable
    protected Object fromStoreValue(@Nullable Object storeValue) {
        return this.allowNullValues && storeValue == NullValue.INSTANCE ? null : storeValue;
    }
    //对于要插入缓存的null值,在允许null值的情况下处理为NullValue;否则抛出异常IllegalArgumentException
    protected Object toStoreValue(@Nullable Object userValue) {
        if (userValue == null) {
            if (this.allowNullValues) {
                return NullValue.INSTANCE;
            } else {
                throw new IllegalArgumentException("Cache '" + this.getName() + "' is configured to not allow null values but null was provided");
            }
        } else {
            return userValue;
        }
    }
    //get操作依据,查询到缓存值非null,则fromStoreValue转换后包装成SimpleValueWrapper返回
    @Nullable
    protected ValueWrapper toValueWrapper(@Nullable Object storeValue) {
        return storeValue != null ? new SimpleValueWrapper(this.fromStoreValue(storeValue)) : null;
    }
}

抽象类AbstractValueAdaptingCache实现了Cache接口,主要抽象了对NULL值的处理逻辑。

  • allowNullValues属性表示是否允许处理NULL值的缓存
  • fromStoreValue方法处理NULL值的get操作,在属性allowNullValues为true的情况下,将NullValue处理为NULL
  • toStoreValue方法处理NULL值得put操作,在属性allowNullValues为true的情况下,将NULL处理为NullValue,否则抛出异常
  • toValueWrapper方法提供Cache接口中get方法的默认实现,从缓存中读取值,再通过fromStoreValue转化,最后包装为SimpleValueWrapper返回
  • ValueWrapper get(Object key)T get(Object key, @Nullable Class<T> type)方法基于上述方法实现
  • ValueWrapper get(Object key)@Nullable Class<T> type)方法基于上述方法实现
  • lookup抽象方法用于给子类获取真正的缓存值

ConcurrentMapCache

public class ConcurrentMapCache extends AbstractValueAdaptingCache {    private final String name;
    //定义ConcurrentMap缓存
    private final ConcurrentMap<Object, Object> store;
    //如果要缓存的是值对象的copy,则由此序列化代理类处理
    @Nullable
    private final SerializationDelegate serialization;
    public ConcurrentMapCache(String name) {
        this(name, new ConcurrentHashMap(256), true);
    }
    //默认允许处理null
    public ConcurrentMapCache(String name, boolean allowNullValues) {
        this(name, new ConcurrentHashMap(256), allowNullValues);
    }
    //默认serialization = null
    public ConcurrentMapCache(String name, ConcurrentMap<Object, Object> store, boolean allowNullValues) {
        this(name, store, allowNullValues, (SerializationDelegate)null);
    }
    protected ConcurrentMapCache(String name, ConcurrentMap<Object, Object> store, boolean allowNullValues, @Nullable SerializationDelegate serialization) {
        super(allowNullValues);
        Assert.notNull(name, "Name must not be null");
        Assert.notNull(store, "Store must not be null");
        this.name = name;
        this.store = store;
        this.serialization = serialization;
    }
    //serialization不为空,缓存值对象的copy
    public final boolean isStoreByValue() {
        return this.serialization != null;
    }
    public final String getName() {
        return this.name;
    }
    public final ConcurrentMap<Object, Object> getNativeCache() {
        return this.store;
    }
    //实现lookup:store#get
    @Nullable
    protected Object lookup(Object key) {
        return this.store.get(key);
    }
    //基于ConcurrentMap::computeIfAbsent方法实现;get和put的值由fromStoreValue和toStoreValue处理Null
    @Nullable
    public <T> T get(Object key, Callable<T> valueLoader) {
        return this.fromStoreValue(this.store.computeIfAbsent(key, (k) -> {
            try {
                return this.toStoreValue(valueLoader.call());
            } catch (Throwable var5) {
                throw new ValueRetrievalException(key, valueLoader, var5);
            }
        }));
    }
    public void put(Object key, @Nullable Object value) {
        this.store.put(key, this.toStoreValue(value));
    }
    @Nullable
    public ValueWrapper putIfAbsent(Object key, @Nullable Object value) {
        Object existing = this.store.putIfAbsent(key, this.toStoreValue(value));
        return this.toValueWrapper(existing);
    }
    public void evict(Object key) {
        this.store.remove(key);
    }
    public boolean evictIfPresent(Object key) {
        return this.store.remove(key) != null;
    }
    public void clear() {
        this.store.clear();
    }
    public boolean invalidate() {
        boolean notEmpty = !this.store.isEmpty();
        this.store.clear();
        return notEmpty;
    }
    protected Object toStoreValue(@Nullable Object userValue) {
        Object storeValue = super.toStoreValue(userValue);
        if (this.serialization != null) {
            try {
                return this.serialization.serializeToByteArray(storeValue);
            } catch (Throwable var4) {
                throw new IllegalArgumentException("Failed to serialize cache value '" + userValue + "'. Does it implement Serializable?", var4);
            }
        } else {
            return storeValue;
        }
    }
    protected Object fromStoreValue(@Nullable Object storeValue) {
        if (storeValue != null && this.serialization != null) {
            try {
                return super.fromStoreValue(this.serialization.deserializeFromByteArray((byte[])((byte[])storeValue)));
            } catch (Throwable var3) {
                throw new IllegalArgumentException("Failed to deserialize cache value '" + storeValue + "'", var3);
            }
        } else {
            return super.fromStoreValue(storeValue);
        }
    }
}

ConcurrentMapCache继承了抽象类AbstractValueAdaptingCache,是Spring的默认缓存实现。它支持对缓存对象copy的缓存,由SerializationDelegate serialization 处理序列化,默认为 null 即基于引用的缓存。缓存相关操作基于基类 AbstractValueAdaptingCache 的 null 值处理,默认允许为 null。

CacheManager

public interface CacheManager {    @Nullable
    //获取指定name的Cache,可能延迟创建
    Cache getCache(String name);
    //获取当前CacheManager下的Cache name集合
    Collection<String> getCacheNames();
}

CacheManager 基于 name 管理一组 Cache。当然,CacheManager也有很多实现类,如ConcurrentMapCacheManager、AbstractCacheManager及SimpleCacheManager,这些xxxCacheManager类都是为了制定Cache的管理规则,这里就不再深入探讨了。

三、Spring Cache实践

除了第二章中提到的Cache接口和CacheManager接口,在使用Spring 缓存抽象时,我们还会用到一些JCache注解。

Spring Cache中一些概念和注解

@Cacheable、@CacheEvict和@CachePut三个注解都是对方法进行配置,主要参数如下图所示:

Cache中可用的SpEL表达式如下图所示:

@EnableCaching

这个注解表示开启基于注解的缓存,一般放在主程序类前面,如下所示:

@EnableCaching
public class SpringBootCacheApplication {    
    public static void main(String[] args) {
        SpringApplication.run(SpringBootCacheApplication.class, args);
    }
}

@Cacheable

这个注解放在方法前,可以将方法的运行结果进行缓存,之后就不用调用方法了,直接从缓存中取值即可。常用属性可以见上图。以下是常见用法:

@Cacheable(cacheNames = {"emp"}, key = "#id", conditon = "mid>0", unless = "#result == null")
public Employee getEmp(Integer id) {
    Employee emp = employeeMapper.getEmpById(id);return emp;
    }
}

@Cacheable注解中常用的参数有cacheNames/value(指定缓存组件的名字,可以指定多个)、key(缓存数据时使用的key,默认使用方法参数的值,也可以自定义)、keyGenerator(key的生成器,可以自定义,key与keyGenerator二选一)、cacheManager(指定缓存管理器,或者使用cacheResolver指定获取解析器)、condition(符合条件才缓存)、unless(符合条件则不缓存,可以获取方法运行结果进行判断)、sync(是否使用异步模式,不可与unless一起使用)。@Cacheable的运行原理:

  1. 方法运行前,程序会使用cacheManager根据cacheNames获取Cache,如果没有对应名称的Cache,则自动创建一个Cache。
  2. 使用key去Cache中查找对应的缓存内容。key默认使用SimpleKeyGenerator生成,其生成策略如下:
  • 如果没有参数,key=new SimpleKey()
  • 如果只有一个参数,key=参数值
  • 如果有多个参数,key=new SimpleKey(params)
  1. 没有查到缓存值,则调用目标方法
  2. 以步骤二中返回的key,目标方法返回的结果为value,存入缓存

@CachePut

@CachePut注解先调用目标方法,然后再缓存目标方法的结果。

@CachePut(value = "emp", key = "#result.id")
public Employee updateEmp(Employee employee) {
   employeeMapper.updateEmp(employee);return employee;
}

@CacheEvict

@CacheEvict用于清除缓存,可以通过key指定需要清除的缓存,allEntries置为true表示删除所有缓存。

@CacheEvict(value = "emp", key = "#id", allEntries = true)
public void deleteEmp(Integer id) {
  employeeMapper.deleteEmpById(id);
}

默认删除缓存行为在方法执行之后(beforeInvocation=false),如果方法运行异常,则该缓存不会被删除。但可以通过设置beforeInvocation = true,将删除缓存行为在方法执行之前。

@Caching&@CacheConfig

@Caching注解中包含了@Cacheable、@CachePut和@CacheEvict注解,可以同时指定多个缓存规则。示例如下所示:

@Caching(cacheable = {
    @Cacheable(value = "emp", key = "#lastName")
}, put = {
    @CachePut(value = "emp", key = "#result.id")
    @CachePut(value = "emp", key = "#result.email")
 })

@CacheConfig注解放在类上,抽取缓存的公共配置,如cacheNames、cacheManager等,这样就不用在每个缓存注解中重复配置了。

四、Redis测试

Spring Boot里面默认使用的Cache和CacheManager分别是ConcurrentMapCache和ConcurrentMapCacheManager,将数据存储在ConcurrentMap<Object,Object>中。

然而,在实际开发过程中,一般会使用一些缓存中间件,如Redis、Memcached和Encache等。接下来,演示一下Redis环境搭建与测试。

Redis环境搭建

Redis是一个开源的、内存中的数据结构存储系统,可以作为数据库、缓存和消息中间件。这里选择用Docker搭建Redis环境。首先需要下载镜像,然后启动,具体命令如下:

// 默认拉取最新的Redis镜像docker pull redis
// 启动Redis容器
docker run -d -p 6379:6379 --name myredis redis

接下来,我们使用Redis Desktop Manager软件连接Redis,Redis的端口号默认为6379。

RedisAutoConfiguration.java文件里面定义了StringRedisTemplate(操作字符串)和RedisTemplate组件,将组件自动注入代码中,即可使用。两个组件都有针对Redis不同数据类型的处理方法。Redis常见的五大数据类型:

String(字符串)、List(列表)、Set(集合)、Hash(散列)、ZSet(有序集合)

stringRedisTemplate.opsForValue()[字符串]stringRedisTemplate.opsForList()[列表]
stringRedisTemplate.opsForSet()[集合]
stringRedisTemplate.opsForHash()[散列]
stringRedisTemplate.opsForZSet()[有序集合]

下面是使用示例:

public class RedisTest {
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Autowired
    RedisTemplate redisTemplate;
    // 测试保存数据
    public void test01 {
        stringRedisTemplate.opsForValue().append("msg","hello");//存入数据
        String s = stringRedisTemplate.opsForValue().get("msg");//获取数据
        stringRedisTemplate.opsForList().leftPush("mylist","1");
    }
    //测试保存对象
    public void test02 {
        Employee emp = new Employee();
        //默认使用jdk序列化机制,将对象序列化后的数据保存至Redis中
        redisTemplate.opsForValue().set("emp-01", empById);
    }
}

如果想将对象以json的方式保存,可将对象转换为json或改变RedisTemplate中的默认序列化规则。后者的参考代码如下,首先找到Redis的自动加载类RedisAutoConfiguration,自定义一个RedisTemplate,然后放入容器中。

public class MyRedisConfig {
    @Bean
    public RedisTemplate<Object, Employee>      empRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        RedisTemplate<Object, Employee> template = new RedisTemplate<Object, Employee>();
        template.setConnectionFactory(redisConnectionFactory);
        Jackson2JsonRedisSerializer<Employee> ser = new Jackson2JsonRedisSerializer<Employee>(Employee.class);
        template.setDefaultSerizlizer(ser);
        return template;
    }
}

自定义CacheManager

当Spring Boot项目中引入Redis的starter依赖时,会将RedisCacheManager作为默认的CacheManager。RedisCacheManager管理RedisCache,后者使用RedisTemplate<Object,Object>操作Redis,默认序列化机制是jdk。如果需要改变Redis序列化机制,可以自定义CacheManager。参考代码如下:

@Bean
public RedisCacheManager employeeCacheManager(RedisTemplate<Object,Object> employeeRedisTemplate) {
    RedisCacheManager cacheManager = new RedisCacheManager(employeeRedisTemplate);
    // 将cacheNames作为key的前缀
    cacheManager.setUserPrefix(true);
    return cacheManager;
}

自定义RedisCache和CacheManager都可以更改缓存方式,两者区别在于:前者用于自定义缓存层,后者将缓存交给spring管理(spring cache中不止Redis)。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
15天前
|
XML Java 开发者
Spring Boot开箱即用可插拔实现过程演练与原理剖析
【11月更文挑战第20天】Spring Boot是一个基于Spring框架的项目,其设计目的是简化Spring应用的初始搭建以及开发过程。Spring Boot通过提供约定优于配置的理念,减少了大量的XML配置和手动设置,使得开发者能够更专注于业务逻辑的实现。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,为开发者提供一个全面的理解。
26 0
|
2月前
|
消息中间件 缓存 NoSQL
Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。
【10月更文挑战第4天】Redis 是一个高性能的键值对存储系统,常用于缓存、消息队列和会话管理等场景。随着数据增长,有时需要将 Redis 数据导出以进行分析、备份或迁移。本文详细介绍几种导出方法:1)使用 Redis 命令与重定向;2)利用 Redis 的 RDB 和 AOF 持久化功能;3)借助第三方工具如 `redis-dump`。每种方法均附有示例代码,帮助你轻松完成数据导出任务。无论数据量大小,总有一款适合你。
78 6
|
19天前
|
缓存 NoSQL 关系型数据库
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
本文详解缓存雪崩、缓存穿透、缓存并发及缓存预热等问题,提供高可用解决方案,帮助你在大厂面试和实际工作中应对这些常见并发场景。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:如何解决Redis缓存雪崩、缓存穿透、缓存并发等5大难题
|
20天前
|
存储 缓存 NoSQL
【赵渝强老师】基于Redis的旁路缓存架构
本文介绍了引入缓存后的系统架构,通过缓存可以提升访问性能、降低网络拥堵、减轻服务负载和增强可扩展性。文中提供了相关图片和视频讲解,并讨论了数据库读写分离、分库分表等方法来减轻数据库压力。同时,文章也指出了缓存可能带来的复杂度增加、成本提高和数据一致性问题。
【赵渝强老师】基于Redis的旁路缓存架构
|
28天前
|
缓存 NoSQL Redis
Redis 缓存使用的实践
《Redis缓存最佳实践指南》涵盖缓存更新策略、缓存击穿防护、大key处理和性能优化。包括Cache Aside Pattern、Write Through、分布式锁、大key拆分和批量操作等技术,帮助你在项目中高效使用Redis缓存。
157 22
|
27天前
|
缓存 NoSQL 中间件
redis高并发缓存中间件总结!
本文档详细介绍了高并发缓存中间件Redis的原理、高级操作及其在电商架构中的应用。通过阿里云的角度,分析了Redis与架构的关系,并展示了无Redis和使用Redis缓存的架构图。文档还涵盖了Redis的基本特性、应用场景、安装部署步骤、配置文件详解、启动和关闭方法、systemctl管理脚本的生成以及日志警告处理等内容。适合初学者和有一定经验的技术人员参考学习。
136 7
|
1月前
|
存储 缓存 监控
利用 Redis 缓存特性避免缓存穿透的策略与方法
【10月更文挑战第23天】通过以上对利用 Redis 缓存特性避免缓存穿透的详细阐述,我们对这一策略有了更深入的理解。在实际应用中,我们需要根据具体情况灵活运用这些方法,并结合其他技术手段,共同保障系统的稳定和高效运行。同时,要不断关注 Redis 缓存特性的发展和变化,及时调整策略,以应对不断出现的新挑战。
63 10
|
1月前
|
缓存 监控 NoSQL
Redis 缓存穿透的检测方法与分析
【10月更文挑战第23天】通过以上对 Redis 缓存穿透检测方法的深入探讨,我们对如何及时发现和处理这一问题有了更全面的认识。在实际应用中,我们需要综合运用多种检测手段,并结合业务场景和实际情况进行分析,以确保能够准确、及时地检测到缓存穿透现象,并采取有效的措施加以解决。同时,要不断优化和改进检测方法,提高检测的准确性和效率,为系统的稳定运行提供有力保障。
49 5
|
1月前
|
缓存 监控 NoSQL
Redis 缓存穿透及其应对策略
【10月更文挑战第23天】通过以上对 Redis 缓存穿透的详细阐述,我们对这一问题有了更深入的理解。在实际应用中,我们需要根据具体情况综合运用多种方法来解决缓存穿透问题,以保障系统的稳定运行和高效性能。同时,要不断关注技术的发展和变化,及时调整策略,以应对不断出现的新挑战。
46 4
|
1月前
|
数据采集 Java 数据安全/隐私保护
Spring Boot 3.3中的优雅实践:全局数据绑定与预处理
【10月更文挑战第22天】 在Spring Boot应用中,`@ControllerAdvice`是一个强大的工具,它允许我们在单个位置处理多个控制器的跨切面关注点,如全局数据绑定和预处理。这种方式可以大大减少重复代码,提高开发效率。本文将探讨如何在Spring Boot 3.3中使用`@ControllerAdvice`来实现全局数据绑定与预处理。
61 2