SpringBoot高级技术
文章目录
- SpringBoot高级技术
- 1、缓存
- Spring缓存
- 缓存注解
- 注解
- @Cacheable/@CachePut/@CacheEvict 主要的属性
- @EnableCaching
- @Cacheable
- @CachePut
- @CacheEvict
- @Caching
- @CacheConfig()
- 步骤
- Redis缓存
- 使用RestTemplate操作redis
- 步骤
- 2、消息(RabbitMQ)
- 概念
- RabbitMQ整合使用
- 第一步:引入配置
- 第二步:在 application.properties 中配置环境
- 第三步:创建 Exchange,Queue 和 绑定
- 在网页上创建
- 在类中配置(AmqpAdmin:管理组件)
- 第四步:使用
- 消息监听
- 发送、接收消息(RabbitTemplate:消息发送处理组件)
- 3、异步、定时、邮件任务
- 异步任务
- 概念
- 步骤
- 定时任务
- 概念
- 步骤
- 邮箱任务
- 4、安全(Spring Security)
- 概述
- 使用
- 第一步:引入依赖
- 第二步:创建Spring Security配置
- 第三步:登陆/注销
- 第四步:Thymeleaf提供的SpringSecurity标签使用
- 5、开发热部署
- 方法一:模板引擎
- 方法二:Spring Boot Devtools(推荐)
1、缓存
Spring缓存
缓存注解
作用 | |
Cache | 缓存接口,定义缓存操作。实现有:RedisCache、EhCacheCache、ConcurrentMapCache等 |
CacheManager | 缓存管理器,管理各种缓存(Cache)组件 |
@EnableCaching | 开启基于注解的缓存 |
@Cacheable | 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存 |
@CacheEvict | 清空缓存 |
@CachePut | 保证方法被调用,又希望结果被缓存 |
keyGenerator | 缓存数据时key生成策略 |
serialize | 缓存数据时value序列化策略 |
注解
@Cacheable/@CachePut/@CacheEvict 主要的属性
属性 | 作用 | 示例 |
value | 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 | @Cacheable(value=”mycache”) 或者 @Cacheable(value={”cache1”,”cache2”} |
key | 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 | @Cacheable(value=”testcache”,key=”#userName”) |
condition | 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存/清除缓存,在调用方法之前之后都能判断 | @Cacheable(value=”testcache”,condition=”#userName.length()>2”) |
allEntries (@CacheEvict ) | 是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存 | @CachEvict(value=”testcache”,allEntries=true) |
beforeInvocation (@CacheEvict) | 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存 | @CachEvict(value=”testcache”,beforeInvocation=true) |
unless (@CachePut) (@Cacheable) | 用于否决缓存的,不像condition,该表达式只在方法执行之后判断,此时可以拿到返回值result进行判断。条件为true不会缓存,fasle才缓存 | @Cacheable(value=”testcache”,unless=”#result == null”) |
@EnableCaching
- 开启基于注解的缓存
@Cacheable
- 可缓存的
- 运行时机
- 先查找缓存中是否有,若有,则使用,若没有,则运行方法并将结果缓存
- 属性
- value:指定缓存组件的名字,将方法的返回结果放在那个缓存中,是数组的方式,至少指定一个
- cacheNames:相当于 value 属性
- key:缓存数据使用的key,可以用它来指定
- 默认是使用方法参数的值 方法的返回值
- 编写SpEL:
名字 | 位置 | 描述 | 示例 |
methodName | root object | 当前被调用的方法名 | #root.methodName |
method | root object | 当前被调用的方法 | #root.method.name |
target | root object | 当前被调用的目标对象 | #root.target |
targetClass | root object | 当前被调用的目标对象类 | #root.targetClass |
args | root object | 当前被调用的方法的参数列表 | #root.args[0] |
caches | root object | 当前方法调用使用的缓存列表(如@Cacheable(value={“cache1”, “cache2”})),则有两个cache | #root.caches[0].name |
argument name | evaluation context | 方法参数的名字. 可以直接 #参数名 ,也可以使用 #p0或#a0 的形式,0代表参数的索引; | #iban 、 #a0 、 #p0 |
result | evaluation context | 方法执行后的返回值(仅当方法执行之后的判断有效,如‘unless’,’cache put’的表达式 ’cache evict’的表达式beforeInvocation=false) | #result |
- 例如:
- #参数值:参数id的值 等同于: #a0 #p0 #root.args[0]
- keyGenerator
- key的生成器:可以自己指定key的生成器的组件id
自己写一个 keyGenerator 方法,添加到配置中 @Configuration,并加在组件中 @Bean - key/keyGenerator:二选一使用
- cacheManager:指定缓存管理器;或者cacheResolver指定获取解析器
- condition:指定符合条件的情况下才缓存
- 例如
- condition = “#uerId>0” :userId 大于零才缓存
- unless:否定缓存;当unless指定的条件为true,方法的返回值就不会被缓存;可以获取到结果进行判断
- 例如:
- unless = “#result == null” :当 result 为空不缓存
- sync:是否使用异步模式
@CachePut
- 更新缓存
- 运行时机
- 先运行方法,再将运行结果缓存
- 属性:跟 @Cacheable 差不多
@CacheEvict
- 清空缓存
- 属性
- @Cacheable 中所有属性
- allEntries
- allEntries=true :清除缓存中的所有数据
- beforeInvocation
- beforeInvocation=false :缓存的清除是否在方法之前执行,默认是方法之后执行
@Caching
- 多个缓存规则
- 可在 @Caching 使用 @Cacheable,@CachePut,@CacheEvict 中的一个或多个注解
@Caching(
cacheable = {@Cacheable(value = "", key = "", ...)},
put = {@CachePut(value = "", key = "", ...)},
evict = {@CacheEvict(value = "", key = "", ...)}
)
public void delete(int userId) {
userDaoCacheMyBatis.deleteMyBatis(userId);
}
@CacheConfig()
- 缓存配置,在类上配置,表示该类的所有方法的属性为 @CacheConfig 中的属性值
- 属性
- 属性有四个,作用跟 @Cacheable 一样,可以用一个或多个属性
- cacheNames
- keyGenerator
- cacheManager
- cacheResolver
@CacheConfig(cacheNames = "", keyGenerator = "", cacheManager = "", cacheResolver = "")
public class UserServiceCacheMyBatisImpl {
}
步骤
第一步:开启注解缓存
- 在 SpringbootinitializrApplication.java 中 添加 @EnableCaching
@SpringBootApplication
@EnableCaching //开启缓存注解功能
public class SpringbootinitializrApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootinitializrApplication.class, args);
}
}
第二步:标注缓存注解
- 在需要缓存的地方使用注解 @Cacheable/@CachePut/@CacheEvict/@Caching/@CacheConfig 并添加相应的属性
@CacheConfig(cacheNames = "user") //@Cacheable/@CachePut/@CacheEvict 注解
@Service
public class UserServiceCacheMyBatisImpl {
@Autowired
private IUserDaoCacheMyBatis userDaoCacheMyBatis;
/**
* 根据 Id 查询用户信息
* @Cacheable : 可缓存的
*/
@Cacheable(value = "user")
public User findById(int userId) {
return userDaoCacheMyBatis.findByIdMyBatis(userId);
}
/**
* 修改信息
* @CachePut :更新缓存
*/
@CachePut(value = "user", key = "#user.userId")
public User update(User user) {
userDaoCacheMyBatis.updateMyBatis(user);
return user;
}
/**
* 删除信息
* @CacheEvict : 清除缓存
*/
@CacheEvict(value = "user", key = "#userId")
public void delete(int userId) {
userDaoCacheMyBatis.deleteMyBatis(userId);
}
}
Redis缓存
redis命令: http://www.redis.cn/commands.html#list
使用RestTemplate操作redis
//redis 操作
@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
RedisTemplate redisTemplate;
@Test
public void redisTest(){
stringRedisTemplate.opsForValue();//操作字符串
stringRedisTemplate.opsForHash();//操作hash
stringRedisTemplate.opsForList();//操作list
stringRedisTemplate.opsForSet();//操作set
stringRedisTemplate.opsForZSet();//操作有序set
redisTemplate.opsForValue();//操作字符串
redisTemplate.opsForHash();//操作hash
redisTemplate.opsForList();//操作list
redisTemplate.opsForSet();//操作set
redisTemplate.opsForZSet();//操作有序set
}
步骤
第一步:引入spring-boot-starter-data-redis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
第二步:application.properties 配置redis连接地址
spring.redis.host=ip地址
第三步:与 Spring 缓存的步骤一样
2、消息(RabbitMQ)
概念
- Message
- 消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。
- Publisher
- 消息的生产者,也是一个向交换器发布消息的客户端应用程序。
- Exchange
- 交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。
Exchange有4种类型:direct(默认),fanout, topic, 和headers,不同类型的Exchange转发消息的策略有所区别
- Queue
- 消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
- Binding
- 绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。
- Exchange 和Queue的绑定可以是多对多的关系。
- Connection
- 网络连接,比如一个TCP连接。
- Channel
- 信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内的虚拟连接,AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接。
- Consumer
- 消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。
- Virtual Host
- 虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个 vhost 本质上就是一个 mini 版的 RabbitMQ 服务器,拥有自己的队列、交换器、绑定和权限机制。vhost 是 AMQP 概念的基础,必须在连接时指定,RabbitMQ 默认的 vhost 是 / 。
- Broker
- 表示消息队列服务器实体
RabbitMQ整合使用
第一步:引入配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
第二步:在 application.properties 中配置环境
spring.rabbitmq.host=116.62.20.166
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
第三步:创建 Exchange,Queue 和 绑定
在网页上创建
- IP:15672 登录,创建
在类中配置(AmqpAdmin:管理组件)
@Autowired
AmqpAdmin amqpAdmin; //自动导入配置
@Test
public void creatExchange(){
//创建 Exchange
amqpAdmin.declareExchange(new DirectExchange("fzk.direct")); //创建类型为 direct 的 Exchange
amqpAdmin.declareExchange(new FanoutExchange("fzk.fanout")); //创建类型为 fanout 的 Exchange
amqpAdmin.declareExchange(new TopicExchange("fzk.topic")); //创建类型为 topic 的 Exchange
//创建 Queue
amqpAdmin.declareQueue(new Queue("fzk.queue"));
//绑定 Exchange,Queue
//"fzk.queue" :需要绑定的队列(queue)名称
//Binding.DestinationType.QUEUE :绑定的类型
//"fzk.direct" :绑定到 Exchange 为 fzk.direct 中
//"fzk.queue" :Routing key:路由键
amqpAdmin.declareBinding(new Binding("fzk.queue", Binding.DestinationType.QUEUE, "fzk.direct", "fzk.queue", null));
}
第四步:使用
消息监听
在主类中开启消息注解功能
@EnableRabbit
@SpringBootApplication
public class SpringbootTestApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootTestApplication.class, args);
}
}
编写消息监听类
@Service
public class RabbitMQListener {
//消息监听注解
//queues = "fzk" :监听消息队列名称为 fzk 的队列(类型为数组:可监听多可消息队列)
@RabbitListener(queues = {"fzk"})
/**
* 监听消息方法
* @param object 接收到的消息
*/
public void rabbitMQListener(String str){
System.out.println("收到消息");
System.out.println(str);
}
}
发送、接收消息(RabbitTemplate:消息发送处理组件)
@Autowired
RabbitTemplate rabbitTemplate;
//发送消息
@Test
public void sendTest(){
//"itfzk.direct" :Exchanges:交换器
//"fzk" :Routing key:路由键
//"hello fzk" :发送的内容
rabbitTemplate.convertAndSend("itfzk.direct", "fzk", "hello fzk");
}
//接收消息
@Test
public void receiveTest(){
//"fzk" :Routing key:路由键
Object fzk = rabbitTemplate.receiveAndConvert("fzk");
System.out.println(fzk);
}
3、异步、定时、邮件任务
异步任务
概念
- 当有 Thread.sleep(3000) 时,若使用同步需要等待 3s,使用异步就可以不用等待 3s
- 在Java应用中,绝大多数情况下都是通过同步的方式来实现交互处理的;但是在处理与第三方系统交互的时候,容易造成响应迟缓的情况,之前大部分都是使用多线程来完成此类任务
- 两个注解
- @EnableAysnc、@Aysnc
步骤
第一步:开启异步注解功能
- 在主类上使用 @EnableAysnc 注解
@EnableAsync //开启异步注解功能
@SpringBootApplication
public class SpringbootTestApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootTestApplication.class, args);
}
}
第二步:编写异步类
- 在需要异步的方法前使用 @Async 注解
@Service
public class AsyncServiceImpl {
@Async //告诉 Spring 这是个异步方法
public void async(){
//模拟网络延迟
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//三秒后执行
System.out.println("fzk");
}
}
定时任务
概念
- 项目开发中经常需要执行一些定时任务,比如需要在每天凌晨时候,分析一次前一天的日志信息。Spring为我们提供了异步执行任务调度的方式,提供TaskExecutor 、TaskScheduler 接口。
- 两个注解
- @EnableScheduling :开启定时注解功能
- @Scheduled :定时
- 属性:cron
- “* * * * * *” :second, minute, hour, day of month, month, day of week
- “0 * * * * MON-FRI” 表示:星期一到星期五秒为 0 时启动一次
字段 | 允许值 | 允许的特殊字符 |
秒 | 0-59 | , - * / |
分 | 0-59 | , - * / |
小时 | 0-23 | , - * / |
日期 | 1-31 | , - * ? / L W C |
月份 | 1-12 | , - * / |
星期 | 0-7或SUN-SAT 0,7是SUN | , - * ? / L C # |
特殊字符 | 代表含义 |
, | 枚举 |
- | 区间 |
* | 任意 |
/ | 步长 |
? | 日/星期冲突匹配 |
L | 最后 |
W | 工作日 |
C | 和calendar联系后计算过的值 |
# | 星期,4#2,第2个星期四 |
步骤
第一步:开启定时注解功能
- 在主类上使用 @EnableScheduling 注解
@EnableScheduling //开启定时注解功能
@SpringBootApplication
public class SpringbootTestApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootTestApplication.class, args);
}
}
第二步:编写异步类
- 在需要定时的方法前使用 @Scheduled 注解
@Service
public class SchedulingServiceImpl {
//second, minute, hour, day of month, month, day of week
@Scheduled(cron = "0 * * * * *") //定时,每当秒为 0 时运行一次
public void acheduling(){
System.out.println("hello fzk");
}
}
邮箱任务
第一步:引入配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
第二步:配置 mail
spring.mail.username=QQ邮箱 //QQ邮箱
spring.mail.password=授权码 //授权码
spring.mail.host=smtp.qq.com //QQ邮箱地址
第三步:编写邮件发送
- 发送简单邮件
@Autowired
JavaMailSenderImpl javaMailSender;
@Test
public void mailTest(){
//发送简单的邮件
SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
simpleMailMessage.setSubject("通知学习"); //标题
simpleMailMessage.setText("每天学习"); //内容
simpleMailMessage.setTo("15727229185@163.com"); //发给谁
simpleMailMessage.setFrom("1820713352@qq.com"); //谁发的
javaMailSender.send(simpleMailMessage);
}
- 发送复杂邮件
@Autowired
JavaMailSenderImpl javaMailSender;
@Test
public void mailTest(){
//发送复杂的邮件(图片,文件。。。)
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper mimeMessageHelper = null;
try {
mimeMessageHelper = new MimeMessageHelper(mimeMessage, true);
mimeMessageHelper.setSubject("通知学习"); //标题
mimeMessageHelper.setText("每天学习"); //内容
mimeMessageHelper.addAttachment("1.jpg", new File("C:\\Users\\dell\\Pictures\\景色\\1.jpg")); //发送文件(图片)
mimeMessageHelper.setTo("15727229185@163.com"); //发给谁
mimeMessageHelper.setFrom("1820713352@qq.com"); //谁发的
} catch (MessagingException e) {
e.printStackTrace();
}
javaMailSender.send(mimeMessage);
}
4、安全(Spring Security)
SpringBoot和Spring Security整合
概述
- Spring Security是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型。他可以实现强大的web安全控制。对于安全控制,我们仅需引入spring-boot-starter-security模块,进行少量的配置,即可实现强大的安全管理
- WebSecurityConfigurerAdapter:自定义Security策略
- AuthenticationManagerBuilder:自定义认证策略
- @EnableWebSecurity:开启WebSecurity模式
- 应用程序的两个主要区域是“认证”和“授权”
- “认证”(Authentication),是建立一个他声明的主体的过程(一个“主体”一般是指用户,设备或一些可以在你的应用程序中执行动作的其他系统)。
- “授权”(Authorization)指确定一个主体是否允许在你的应用程序执行一个动作的过程。为了抵达需要授权的店,主体的身份已经有认证过程建立。
- Thymeleaf提供的SpringSecurity标签支持
- sec:authorize=“isAuthenticated()” : 当前用户已通过身份验证
- sec:authentication=“name” :当前用户的用户名
- sec:authentication=“principal.authorities” : 当前用户的权限
- sec:authorize=“hasRole(‘ADMIN’)” :当前用户是否拥有ADMIN权限
使用
第一步:引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
第二步:创建Spring Security配置
@EnableWebSecurity //开启WebSecurity模式
public class MySecurity extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/").permitAll(); //如何权限都能访问
http.authorizeRequests().antMatchers("/vip1/**").hasRole("VIP1"); //只有 VIP1 权限才能访问
http.authorizeRequests().antMatchers("/vip2/**").hasRole("VIP2"); //只有 VIP2 权限才能访问
http.authorizeRequests().antMatchers("/vip3/**").hasRole("VIP3"); //只有 VIP3 权限才能访问
}
//授权
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//该用户权限为 VIP1,VIP2
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("fff")
.password(new BCryptPasswordEncoder().encode("123")).roles("VIP1", "VIP2");
//该用户权限为 VIP1,VIP3
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("zzz")
.password(new BCryptPasswordEncoder().encode("123")).roles("VIP1", "VIP3");
//该用户权限为 VIP3,VIP2
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("kkk")
.password(new BCryptPasswordEncoder().encode("123")).roles("VIP3", "VIP2");
}
}
第三步:登陆/注销
@EnableWebSecurity
public class MySecurity extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/").permitAll();
http.authorizeRequests().antMatchers("/vip1/**").hasRole("VIP1");
http.authorizeRequests().antMatchers("/vip2/**").hasRole("VIP2");
http.authorizeRequests().antMatchers("/vip3/**").hasRole("VIP3");
http.formLogin(); //登录验证
http.logout().logoutSuccessUrl("/"); //退出登录
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("fff")
.password(new BCryptPasswordEncoder().encode("123")).roles("VIP1", "VIP2");
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("zzz")
.password(new BCryptPasswordEncoder().encode("123")).roles("VIP1", "VIP3");
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("kkk")
.password(new BCryptPasswordEncoder().encode("123")).roles("VIP3", "VIP2");
}
}
第四步:Thymeleaf提供的SpringSecurity标签使用
<!DOCTYPE html>
<!-- 导入需要的配置 -->
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--当前用户没通过身份验证 -->
<div sec:authorize="!isAuthenticated()">
<a th:href="@{/login}">登录</a><br/>
</div>
<!-- 当前用户已通过身份验证 -->
<div sec:authorize="isAuthenticated()">
<!-- 当前用户的用户名 -->
<span sec:authentication="name"></span> |
<!-- 当前用户的权限 -->
<span sec:authentication="principal.authorities"></span>
<a href="/logout">注销</a><br/>
</div>
<!-- 当前用户必须拥有 VIP1 权限时才会显示标签内容 -->
<div sec:authorize="hasRole('VIP1')">
<a href="vip1/success">vip1</a><br/>
</div>
<!-- 当前用户必须拥有 VIP2 权限时才会显示标签内容 -->
<div sec:authorize="hasRole('VIP2')">
<a href="vip2/success">vip2</a><br/>
</div>
<!-- 当前用户必须拥有 VIP3 权限时才会显示标签内容 -->
<div sec:authorize="hasRole('VIP3')">
<a href="vip3/success">vip3</a>
</div>
</body>
</html>
5、开发热部署
方法一:模板引擎
- 在Spring Boot中开发情况下禁用模板引擎的cache页面模板
- spring.thymeleaf.cache=false
- ctrl+F9可以重新编译当前页面并生效
方法二:Spring Boot Devtools(推荐)
- 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
- ctrl+F9可以重新编译