springBoot整合Redis(三、整合Spring Cache)

        缓存的框架太多了,各有各的优势,比如Redis、Memcached、Guava、Caffeine等等。
如果我们的程序想要使用缓存,就要与这些框架耦合。聪明的架构师已经在利用接口来降低耦合了,利用面向对象的抽象和多态的特性,做到业务代码与具体的框架分离。
        但我们仍然需要显式地在代码中去调用与缓存有关的接口和方法,在合适的时候插入数据到缓存里,在合适的时候从缓存中读取数据。
        想一想AOP的适用场景,这不就是天生就应该AOP去做的吗?
        是的,Spring Cache就是一个这个框架。它利用了AOP,实现了基于注解的缓存功能,并且进行了合理的抽象,业务代码不用关心底层是使用了什么缓存框架,只需要简单地加一个注解,就能实现缓存功能了。而且Spring Cache也提供了很多默认的配置,用户可以3秒钟就使用上一个很不错的缓存功能。

一、什么是Spring Boot Cache?

        Spring Cache本身是一个缓存体系的抽象实现,并没有具体的缓存能力,要使用Spring Cache还需要具体的缓存实现来完成。
        Spring Boot 集成了多种cache的实现,如果你没有在配置类中声明CacheManager或者CacheResolvoer,那么SpringBoot会按顺序在下面的实现类中寻找:

  • 每次调用需要缓存功能的方法时,Spring会检查检查指定参数的指定的目标方法是否已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。

  • 使用Spring缓存抽象时我们需要关注以下两点;

    1、确定方法需要被缓存以及他们的缓存策略

    2、从缓存中读取之前缓存存储的数据

二、简单使用SpringCache

        分为很简单的三步:加依赖,开启缓存,加缓存注解。

        1) 加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

        2 )开启缓存

        在启动类加上@EnableCaching注解即可开启使用缓存。

@SpringBootApplication
@EnableCaching
public class CachingApplication {

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

}

        3)加缓存注解

        在要缓存的方法上面添加@Cacheable注解,即可缓存这个方法的返回值。

    @Cacheable(value = "emp" ,key = "targetClass + methodName +#p0")
    public List<NewJob> queryAll(User uid) {
        return newJobDao.findAllByUid(uid);
    }

        此处的value是必需的,它指定了你的缓存存放在哪块命名空间。

        此处的key是使用的spEL表达式,参考上章。这里有一个小坑,如果你把methodName换成method运行会报错,观察它们的返回类型,原因在于methodNameStringmethohMethod

        此处的User实体类一定要实现序列化public class User implements Serializable,否则会报java.io.NotSerializableException异常。

        需要注意的是,调用加了@Cacheable 的方法,调用方法必须跟加了@Cacheable 的方法 在不同的类中,跟反向代理有关,不然不会生效。

三、SpringCache中的概念以及用法

      1) 相关概念:
名称解释
Cache缓存接口,定义缓存操作。实现有:RedisCache、EhCacheCache、ConcurrentMapCache等
CacheManager缓存管理器,管理各种缓存(cache)组件
@Cacheable主要针对方法配置,能够根据方法的请求参数对其进行缓存
@CacheEvict清空缓存
@CachePut保证方法被调用,又希望结果被缓存。
与@Cacheable区别在于是否每次都调用方法,常用于更新
@EnableCaching开启基于注解的缓存
keyGenerator缓存数据时key生成策略
serialize缓存数据时value序列化策略
@CacheConfig统一配置本类的缓存注解的属性

  CacheManager, Cache是接口 
 @Cacheable/@CachePut/@CacheEvict 是放在方法尚主要控制缓存的注解
 @EnableCaching 放在启动类上

 @CacheConfig 放在配置类上。
 

 
2)@Cacheable/@CachePut/@CacheEvict 主要的参数
名称解释
value缓存的名称,在 spring 配置文件中定义,必须指定至少一个
例如:
@Cacheable(value=”mycache”) 或者
@Cacheable(value={”cache1”,”cache2”}
key缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,
如果不指定,则缺省按照方法的所有参数进行组合
例如:
@Cacheable(value=”testcache”,key=”#id”)
condition缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,
只有为 true 才进行缓存/清除缓存
例如:@Cacheable(value=”testcache”,condition=”#userName.length()>2”)
unless否定缓存。当条件结果为TRUE时,就不会缓存。
@Cacheable(value=”testcache”,unless=”#userName.length()>2”)
allEntries
(@CacheEvict )
是否清空所有缓存内容,缺省为 false,如果指定为 true,
则方法调用后将立即清空所有缓存
例如:
@CachEvict(value=”testcache”,allEntries=true)
beforeInvocation
(@CacheEvict)
是否在方法执行前就清空,缺省为 false,如果指定为 true,
则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法
执行抛出异常,则不会清空缓存
例如:
@CachEvict(value=”testcache”,beforeInvocation=true)

例如:

    @Cacheable(value = "DEMO:TEST" ,key = "#id")
    public Demo getDemo(Long id){
        System.out.println("序号"+id);
        return new Demo("1234455", LocalDateTime.now());
    }

当第一次调用时会进入方法,打印id:

 第二次调用时就不会调用方法,redis中就会多出一个key:

 我们生成的key就是 vaule + key中设置的值。但是此处目前有一个坑,就是生成的key在 value 与 key的衔接处是双引号。这个后面可以通过配置修改为单引号。

3) Spel表达式

一、spel语法

具体语法可以参考此篇博客:SpEL表达式总结 - 简书

二、SpringCache也提供了root对象,具体功能使用如下。

4) 使用spel表达式栗子

1、使用参数作为key:使用方法参数时我们可以直接使用“#参数名”或者“#p参数index”。

@Cacheable(value="users", key="#id")
     public User find(Integer id) {
         returnnull;
     }
     @Cacheable(value="users", key="#p0")
     public User find(Integer id) {
         returnnull;
     }
     @Cacheable(value="users", key="#user.id")
     public User find(User user) {
         returnnull;
     }
     @Cacheable(value="users", key="#p0.id")
     public User find(User user) {
         returnnull;
     }


2、当我们要使用root对象的属性作为key时我们也可以将“#root”省略,因为Spring默认使用的就是root对象的属性。如:

@Cacheable(value={"users", "xxx"}, key="caches[1].name")
     public User find(User user) {
         returnnull;
     }

如果要调用类里面的方法:

@Cacheable(value={"TeacherAnalysis_public_chart"}, key="#root.target.getDictTableName() + '_' + #root.target.getFieldName()")
     public List<Map<String, Object>> getChartList(Map<String, Object> paramMap) {
     }
     public String getDictTableName(){
         return "";
     }
     public String getFieldName(){
         return "";
     }


3、最好使用所有参数作为key,当然,也分情况。

@Cacheable(cacheNames = "c2",key = "#id")
    public User getUserById(Long id,String username){
        User user = new User();
        user.setId(id);
        return user;
    }
    @Test
    void testGetUserById() {
        User u1 = userService.getUserById(98L, "dong");
        User u2 = userService.getUserById(98L, "lisi");
    }

以参数id作为key会出现逻辑错误,当调用第一次getUserById方法时,存入key为id,值为dong,当调用第二次getUserById方法时,因为已经存入缓存id,所以不会进入第二次getUserById方法,所以lisi不能进入缓存

5) 自定义key生成器
@Component
public class MyKeyGenerate implements KeyGenerator {
        @Override
            public Object generate(Object target, Method method, Object... params) {
            return target.getClass().getSimpleName() + ":"
                    + method.getName() + ":"
                    + StringUtils.arrayToDelimitedString(params, ":");
        }
}

//将myKeyGenerate注入
@Cacheable(cacheNames = "test",keyGenerator = "myKeyGenerate")
    public User getUserById(Long id,String username){
        User user = new User();
        user.setId(id);
        user.setUsername(username);
        return user;
    }

这个要讲的不是很多,自定义key生成器内容比较简单,有兴趣可以自行再搜索一下。

6)condition

符合条件的情况下才缓存。方法返回的数据要不要缓存,可以做一个动态判断。


7)unless

否定缓存。当 unless 指定的条件为 true ,方法的返回值就不会被缓存。



一般用于结果不为空时判断  unless = "#result.data==null"

四、各个注解详解

在上面,已经把各个注解的公共属性抽了出来,这里只做一些注解的特有属性,当然,可能某些属性也是公有的。

1)@Cacheable

        在方法执行前查看是否有缓存对应的数据,如果有直接返回数据,如果没有调用方法获取数据返回,并缓存起来。

1、unless:条件符合则不缓存,是对出参进行判断

        unless属性可以使用#result表达式。效果: 缓存如果有符合要求的缓存数据则直接返回,没有则去数据库查数据,查到了就返回并且存在缓存一份,没查到就不存缓存。

condition 不指定相当于 true,unless 不指定相当于 false

    当 condition = false,一定不会缓存;

    当 condition = true,且 unless = true,不缓存;

    当 condition = true,且 unless = false,缓存;
2、sync:是否使用异步,默认是false.

        在一个多线程的环境中,某些操作可能被相同的参数并发地调用,同一个 value 值可能被多次计算(或多次访问 db),这样就达不到缓存的目的。针对这些可能高并发的操作,我们可以使用 sync 参数来告诉底层的缓存提供者将缓存的入口锁住,这样就只能有一个线程计算操作的结果值,而其它线程需要等待。当值为true,相当于同步可以有效的避免缓存击穿的问题。

@Cacheable(value="user_cache",key="#userId", unless="#result == null")
public User getUserById(Long userId) {
 User user = userMapper.getUserById(userId);
 return user;
}

2)@CachePut

@CachePut注解的作用 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存,和 @Cacheable 不同的是,它每次都会触发真实方法的调用 。简单来说就是用户更新缓存数据。但需要注意的是该注解的value 和 key 必须与要更新的缓存相同,也就是与@Cacheable 相同。示例:

    @CachePut(value = "emp", key = "targetClass + #p0")
    public NewJob updata(NewJob job) {
        NewJob newJob = newJobDao.findAllById(job.getId());
        newJob.updata(job);
        return job;
    }


    @Cacheable(value = "emp", key = "targetClass +#p0")//清空缓存
    public NewJob save(NewJob job) {
        newJobDao.save(job);
        return job;
    }

也就是说@Cacheable注解  是在调用方法之前去看看是否已经有了缓存 如果有缓存就不会执行方法,@CachePut注解  是不管有没有缓存 都先执行方法,然后将方法的结果更新缓存。

3) @CacheEvict:清空缓存

注解的方法在调用时会从缓存中移除已存储的数据。

@CacheEvict(value = "user_cache", key = "#id")
public void deleteUserById(Long id) {
         userMapper.deleteUserById(id);
}

1、allEntries:是否清空左右缓存。默认为false

当指定了allEntries为true时,Spring Cache将忽略指定的key

2、beforeInvocation:是否在方法执行前就清空,默认为 false

清除操作默认是在对应方法成功执行之后触发的,即方法如果因为抛出异常而未能成功返回时也不会触发清除操作。使用beforeInvocation可以改变触发清除操作的时间,当我们指定该属性值为true时,Spring会在调用该方法之前清除缓存中的指定元素。

4)@Caching:

        可以让我们在一个方法或者类上同时指定多个Spring Cache相关的注解

1、其拥有三个属性:cacheable、put和evict,分别用于指定@Cacheable、@CachePut和@CacheEvict。

    @Caching(
            cacheable = {@Cacheable(value = "stu",key = "#userName")},
            put = {@CachePut(value = "stu", key = "#result.id"),
                    @CachePut(value = "stu", key = "#result.age")
            }
    )
    public Student getStuByStr(String userName) {
        StudentExample studentExample = new StudentExample();
        studentExample.createCriteria().andUserNameEqualTo(userName);
        List<Student> students = studentMapper.selectByExample(studentExample);
        return Optional.ofNullable(students).orElse(null).get(0);
}

5)配置@CacheConfig#

当我们需要缓存的地方越来越多,你可以使用@CacheConfig(cacheNames = {"myCache"})注解来统一指定value的值,这时可省略value,如果你在你的方法依旧写上了value,那么依然以方法的value值为准。

使用方法如下:

@CacheConfig(cacheNames = {"myCache"})
public class BotRelationServiceImpl implements BotRelationService {

@Override
@Cacheable(key = "targetClass + methodName +#p0")//此处没写value
public List<BotRelation> findAllLimit(int num) {

    return botRelationRepository.findAllLimit(num);

    }
.....
}

五、自定义过期时间

       关于自定义配置如果需要理解,需要简单得了解一下源码,这里就不做详细讲解了,大概说一下如何。

        首先要实现 RedisCacheManager 接口,创建实现类。自定义RedisCache。如下所示:

public class PlusCacheManager extends RedisCacheManager {

    public PlusCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
        super(cacheWriter, defaultCacheConfiguration);
    }

    @Override
    protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
        String[] array = StringUtils.delimitedListToStringArray(name, "#");
        name = array[0];
        if (array.length > 1) {
            long ttl = Long.parseLong(array[1]);
            cacheConfig = cacheConfig.entryTtl(Duration.ofSeconds(ttl));
        }
        return super.createRedisCache(name, cacheConfig);
    }
}

        大概讲解一下,这个重写得 createRedisCache 方法,就是自定义实现 RedisCache 的方法, name 就是  @Cacheable 中 value的值。所以这个方法可以起到过虑 name 作用
        例如:  @Cacheable(value = "name#3600" ,keyGenerator = "myKeyGenerate")

        这个value 值是:name#3600 , 上面的方法可以将name 用 ’#‘ 分割,如果 '#'  后面有值,就可以将 ’#‘ 后面的值设置进入缓存时间中。

        另外还需要增加一个配置类,用于自定义RedisCacheManager,如下所示,就可以简单的解决自定义key的时间问题。还有key键中的双引号问题。

    @Bean
    @Primary
    public PlusCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                // 60s缓存失效
                .entryTtl(Duration.ofSeconds(60))
                // 设置key的序列化方式
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer()))
                // 设置value的序列化方式
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer()))
                .computePrefixWith((name) -> RedisKeyConstants.CACHE + name + StrUtil.COLON)
                // 不缓存null值
                .disableCachingNullValues();

        PlusCacheManager plusCacheManager = new PlusCacheManager(RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory), config);
        log.info("自定义RedisCacheManager加载完成");
        return plusCacheManager;
    }

    // key键序列化方式
    private RedisSerializer<String> keySerializer() {

        return new StringRedisSerializer();
    }

    // value值序列化方式
    private GenericJackson2JsonRedisSerializer valueSerializer(){
        ObjectMapper objectMapper = new ObjectMapper();
        // 反序列化时候遇到不匹配的属性并不抛出异常
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        // 序列化时候遇到空对象不抛出异常
        objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        // 反序列化的时候如果是无效子类型,不抛出异常
        objectMapper.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false);
        // 不使用默认的dateTime进行序列化,
        objectMapper.configure(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS, false);
        // 使用JSR310提供的序列化类,里面包含了大量的JDK8时间序列化类
        objectMapper.registerModule(new JavaTimeModule());
        // 启用反序列化所需的类型信息,在属性中添加@class
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL,
                JsonTypeInfo.As.PROPERTY);
        // 配置null值的序列化器
        GenericJackson2JsonRedisSerializer.registerNullValueSerializer(objectMapper, null);
        return new GenericJackson2JsonRedisSerializer(objectMapper);
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/454470.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Cesium ion 简介

Cesium ion SaaS 是一个强大、可扩展且安全的 3D 地理空间数据平台。可以上传您的数据&#xff0c;Cesium ion 会将其优化为 3D Tiles&#xff0c;并将其托管在云端&#xff0c;并将其流式传输到任何设备。 Cesium ion 包括访问精选的全球 3D 内容&#xff0c;包括 Cesium Wor…

jeecg 项目 springcloud 项目有一个模块 没加载进来 只需要 把这个模块放到 可以加载到模块的位置 刷新依赖

springcloud 项目有一个模块 没加载进来 只需要 把这个模块放到 可以加载到模块的位置 刷新依赖

04-自媒体文章-自动审核

自媒体文章-自动审核 1)自媒体文章自动审核流程 1 自媒体端发布文章后&#xff0c;开始审核文章 2 审核的主要是审核文章的内容&#xff08;文本内容和图片&#xff09; 3 借助第三方提供的接口审核文本 4 借助第三方提供的接口审核图片&#xff0c;由于图片存储到minIO中&…

基于YOLOv8/YOLOv7/YOLOv6/YOLOv5的零售柜商品检测软件(Python+PySide6界面+训练代码)

摘要&#xff1a;开发高效的零售柜商品识别系统对于智能零售领域的进步至关重要。本文深入介绍了如何运用深度学习技术开发此类系统&#xff0c;并分享了全套实现代码。系统采用了领先的YOLOv8算法&#xff0c;并与YOLOv7、YOLOv6、YOLOv5进行了性能比较&#xff0c;呈现了诸如…

算法学习系列(四十):贡献法

目录 引言概念一、孤独的照片二、牛的基因学三、字串分值 引言 关于这个贡献法考的不是很多&#xff0c;主要题型是出现在需要枚举每一个组合这类题&#xff0c;出现的次数较多。没有固定的模板&#xff0c;就是一种思想&#xff0c;跟贪心一样&#xff0c;每个题都是不一样的…

基于opencv的手势识别

当然可以&#xff0c;下面是一个使用OpenCV实现简单手势识别&#xff0c;并在摄像头捕捉的视频中描绘出手部轮廓为线条的示例。该代码会读取摄像头流&#xff0c;然后检测出手部&#xff0c;并用线条描绘出手的轮廓。 首先&#xff0c;你需要安装OpenCV库。如果你还没有安装&am…

C# 第三方曲线库及其特点

在 C# 中&#xff0c;有几个第三方库可以用于绘制曲线图&#xff0c;每个库都有自己的特点和优势。以下是一些常见的 C# 第三方曲线库及其特点&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。 1.LiveC…

普通人也能年入百万的超级秘籍!2024超级机会,低薪人的第二事业

一、选对行业与把握时机尤为关键。 入场时机的选择&#xff0c;往往决定了你的起跑线。那些在行业赛道上升期便早早布局的人&#xff0c;无疑占据了极大的优势。想象一下&#xff0c;你置身于一个市场需求持续增长、发展空间巨大的行业&#xff0c;成功的机会自然大增。比如现…

Parade Series - WebRTC ( < 300 ms Low Latency )

Parade Series - FFMPEG (Stable X64) C:\Conda\parading-cam>ffmpeg -f dshow -i video"Surface Camera Front" -vcodec libx264 -preset:v ultrafast -tune:v zerolatency -an -rtsp_transport tcp -f rtsp://127.0.0.1:8554/cam0801

生存预后不显著?最佳阈值来帮你!| 附完整代码 + 注释

大家在进行生存预后分析时发现结果不显著&#xff0c;是不是当头一棒&#xff01;两眼一黑&#xff01;难不成这就代表我们的研究没意义吗&#xff1f;NONONO&#xff01;别慌&#xff01;说不定还有救&#xff01;快来看看最佳阈值能不能捞你一把&#xff01; 对生存分析感兴趣…

DAY by DAY 史上最全的Linux常用命令汇总----man

man是按照手册的章节号的顺序进行搜索的。 man设置了如下的功能键&#xff1a; 功能键 功能 空格键 显示手册页的下一屏 Enter键 一次滚动手册页的一行 b 回滚一屏 f 前滚一屏 q 退出man命令 h 列出所有功能键 /word 搜索word字符串 注意&#xff1a…

多数问题求解之蒙特卡洛与分治法

多数问题&#xff08;Majority Problem&#xff09;是一个有多种求解方法的经典问题&#xff0c;其问题定义如下&#xff1a; 给定一个大小为 n n n的数组&#xff0c;找出其中出现次数超过 n / 2 n/2 n/2的元素 例如&#xff1a;当输入数组为 [ 5 , 3 , 5 , 2 , 3 , 5 , 5 ] […

Qt 如何搭建Lua的运行环境

一、Lua简介 Lua 是一种强大的、高效的、轻量级的、可嵌入的脚本语言。它支持过程&#xff08;procedural&#xff09;编程、面向对象编程、函数式编程以及数据描述。Lua 是动态类型的&#xff0c;运行速度快&#xff0c;支持自动内存管理&#xff0c;因此被广泛用于配置、脚本…

java-集合工具类Collections

我们在使用它的时候记得导包 常见API 我们就简单看看第一第二个方法&#xff0c;代码如下&#xff0c;其余的知道用就行

基于经验模式分解和小波阈值的自适应降噪研究_杨铮

目的 针对轴承信号在采集过程中容易受到不同环境下噪声干扰&#xff0c;提出EMD分解结合小波阈值的自适应降噪的方法&#xff0c;对轴承振动信号进行降噪处理&#xff0c;提取出所需要的振动信号。方法 首先对含有噪声的轴承信号进行EMD分解&#xff0c;得到n个IMF并进行小波阈…

GUROBI之数学启发式算法Matheuristics

参考运小筹的帖子&#xff1a;优化求解器 | Gurobi 数学启发式算法&#xff1a;参数类型与案例实现 - 知乎 (zhihu.com) 简言之&#xff0c;数学启发式是算法就是数学规划和启发式算法的融合&#xff0c;与元启发式算法相比&#xff0c;数学启发式算法具有更强的理论性。 在GUR…

C++初阶

1.缺省参数 给缺省参数的时候&#xff0c;不能声明&#xff0c;定义同时给&#xff0c;只能声明的时候给缺省参数&#xff0c;同时给程序报错&#xff1b; 2.函数重载 C语言不允许同名函数的存在&#xff0c;函数名不能相同&#xff0c;C引入函数重载&#xff0c;函数名可以…

SOLIDWORKS 2024新版价格 SOLIDWORKS2024专业版白金版多少钱?

达索 SOLIDWORKS 一直以来都致力于让每位设计师和工程师的设计都触手可及。SOLIDWORKS贯彻的使命就是通过功能强大且易于使用的产品开发解决方案&#xff0c;在创造、协作和提供创新的产品体验方面助您一臂之力。SOLIDWORKS 2024延续了这一期望&#xff0c;同时开启了强化使用S…

Altium Designer快速入门及项目实战教程之PCB设计(四)

一、引言 在我们的Altium Designer系列教程中&#xff0c;我们已经一起走过了软件界面的初识、原理图的绘制&#xff0c;以及元件库的建立。今天&#xff0c;我们将进入这一系列教程的高潮部分——PCB设计。 PCB设计不仅是电子产品开发过程中的核心&#xff0c;也是检验一个电…

Maven项目添加依赖

maven仓库&#xff1a;Maven Repository: Search/Browse/Explore (mvnrepository.com) 1.在maven仓库中搜素自己想要的依赖&#xff0c;选择合适的版本号&#xff0c;复制以下内容(依赖坐标)。 2.在pom.xml中把复制的粘贴进去。刷新。&#xff08;注意内容要放在dependencies双…
最新文章