SpringBoot项目中使用缓存的正确姿势,太优雅了!

前言
缓存可以通过将经常访问的数据存储在内存中,减少底层数据源如数据库的压力,从而有效提高系统的性能和稳定性。我想大家的项目中或多或少都有使用过,我们项目也不例外,但是最近在review公司的代码的时候写的很蠢且low, 大致写法如下:
-
public User getById(String id) {
-
User user = cache.getUser();
-
if(user != null) {
-
return user;
-
}
-
// 从数据库获取
-
user = loadFromDB(id);
-
cahce.put(id, user);
-
return user;
-
}
其实Spring Boot 提供了强大的缓存抽象,可以轻松地向您的应用程序添加缓存。本文就讲讲如何使用 Spring 提供的不同缓存注解实现缓存的最佳实践。
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/YunaiV/ruoyi-vue-pro
视频教程:https://doc.iocoder.cn/video/
启用缓存@EnableCaching
现在大部分项目都是是SpringBoot项目,我们可以在启动类添加注解@EnableCaching来开启缓存功能。
-
@SpringBootApplication
-
@EnableCaching
-
public class SpringCacheApp {
-
-
public static void main(String[] args) {
-
SpringApplication.run(Cache.class, args);
-
}
-
}
既然要能使用缓存,就需要有一个缓存管理器Bean,默认情况下,@EnableCaching 将注册一个ConcurrentMapCacheManager的Bean,不需要单独的 bean 声明。ConcurrentMapCacheManager将值存储在ConcurrentHashMap的实例中,这是缓存机制的最简单的线程安全实现。
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
项目地址:https://github.com/YunaiV/yudao-cloud
视频教程:https://doc.iocoder.cn/video/
自定义缓存管理器
默认的缓存管理器并不能满足需求,因为她是存储在jvm内存中的,那么如何存储到redis中呢?这时候需要添加自定义的缓存管理器。
-
添加依赖
-
<dependency>
-
<groupId>org.springframework.boot</groupId>
-
<artifactId>spring-boot-starter-data-redis</artifactId>
-
</dependency>
-
配置Redis缓存管理器
-
@Configuration
-
@EnableCaching
-
public class CacheConfig {
-
-
@Bean
-
public RedisConnectionFactory redisConnectionFactory() {
-
return new LettuceConnectionFactory();
-
}
-
-
@Bean
-
public CacheManager cacheManager() {
-
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
-
.disableCachingNullValues()
-
.serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
-
-
RedisCacheManager redisCacheManager = RedisCacheManager.builder(redisConnectionFactory())
-
.cacheDefaults(redisCacheConfiguration)
-
.build();
-
-
return redisCacheManager;
-
}
-
}
现在有了缓存管理器以后,我们如何在业务层面操作缓存呢?
我们可以使用@Cacheable、@CachePut 或@CacheEvict 注解来操作缓存了。
@Cacheable
该注解可以将方法运行的结果进行缓存,在缓存时效内再次调用该方法时不会调用方法本身,而是直接从缓存获取结果并返回给调用方。

例子1:缓存数据库查询的结果。
-
@Service
-
public class MyService {
-
-
@Autowired
-
private MyRepository repository;
-
-
@Cacheable(value = "myCache", key = "#id")
-
public MyEntity getEntityById(Long id) {
-
return repository.findById(id).orElse(null);
-
}
-
}
在此示例中,@Cacheable 注解用于缓存 getEntityById()方法的结果,该方法根据其 ID 从数据库中检索 MyEntity 对象。
但是如果我们更新数据呢?旧数据仍然在缓存中?
@CachePut
然后@CachePut 出来了, 与 @Cacheable 注解不同的是使用 @CachePut 注解标注的方法,在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式写入指定的缓存中。@CachePut 注解一般用于更新缓存数据,相当于缓存使用的是写模式中的双写模式。
-
@Service
-
public class MyService {
-
-
@Autowired
-
private MyRepository repository;
-
-
@CachePut(value = "myCache", key = "#entity.id")
-
public void saveEntity(MyEntity entity) {
-
repository.save(entity);
-
}
-
}
@CacheEvict
标注了 @CacheEvict 注解的方法在被调用时,会从缓存中移除已存储的数据。@CacheEvict 注解一般用于删除缓存数据,相当于缓存使用的是写模式中的失效模式。

-
@Service
-
public class MyService {
-
-
@Autowired
-
private MyRepository repository;
-
-
@CacheEvict(value = "myCache", key = "#id")
-
public void deleteEntityById(Long id) {
-
repository.deleteById(id);
-
}
-
}
@Caching
@Caching 注解用于在一个方法或者类上,同时指定多个 Spring Cache 相关的注解。

例子1:@Caching注解中的evict属性指定在调用方法 saveEntity 时失效两个缓存。
-
@Service
-
public class MyService {
-
-
@Autowired
-
private MyRepository repository;
-
-
@Cacheable(value = "myCache", key = "#id")
-
public MyEntity getEntityById(Long id) {
-
return repository.findById(id).orElse(null);
-
}
-
-
@Caching(evict = {
-
@CacheEvict(value = "myCache", key = "#entity.id"),
-
@CacheEvict(value = "otherCache", key = "#entity.id")
-
})
-
public void saveEntity(MyEntity entity) {
-
repository.save(entity);
-
}
-
-
}
例子2:调用getEntityById方法时,Spring会先检查结果是否已经缓存在myCache缓存中。如果是,Spring 将返回缓存的结果而不是执行该方法。如果结果尚未缓存,Spring 将执行该方法并将结果缓存在 myCache 缓存中。方法执行后,Spring会根据@CacheEvict注解从otherCache缓存中移除缓存结果。
-
@Service
-
public class MyService {
-
-
@Caching(
-
cacheable = {
-
@Cacheable(value = "myCache", key = "#id")
-
},
-
evict = {
-
@CacheEvict(value = "otherCache", key = "#id")
-
}
-
)
-
public MyEntity getEntityById(Long id) {
-
return repository.findById(id).orElse(null);
-
}
-
-
}
例子3:当调用saveData方法时,Spring会根据@CacheEvict注解先从otherCache缓存中移除数据。然后,Spring 将执行该方法并将结果保存到数据库或外部 API。
方法执行后,Spring 会根据@CachePut注解将结果添加到 myCache、myOtherCache 和 myThirdCache 缓存中。Spring 还将根据@Cacheable注解检查结果是否已缓存在 myFourthCache 和 myFifthCache 缓存中。如果结果尚未缓存,Spring 会将结果缓存在适当的缓存中。如果结果已经被缓存,Spring 将返回缓存的结果,而不是再次执行该方法。
-
@Service
-
public class MyService {
-
-
@Caching(
-
put = {
-
@CachePut(value = "myCache", key = "#result.id"),
-
@CachePut(value = "myOtherCache", key = "#result.id"),
-
@CachePut(value = "myThirdCache", key = "#result.name")
-
},
-
evict = {
-
@CacheEvict(value = "otherCache", key = "#id")
-
},
-
cacheable = {
-
@Cacheable(value = "myFourthCache", key = "#id"),
-
@Cacheable(value = "myFifthCache", key = "#result.id")
-
}
-
)
-
public MyEntity saveData(Long id, String name) {
-
// Code to save data to a database or external API
-
MyEntity entity = new MyEntity(id, name);
-
return entity;
-
}
-
-
}
@CacheConfig
通过@CacheConfig 注解,我们可以将一些缓存配置简化到类级别的一个地方,这样我们就不必多次声明相关值:
-
@CacheConfig(cacheNames={"myCache"})
-
@Service
-
public class MyService {
-
-
@Autowired
-
private MyRepository repository;
-
-
@Cacheable(key = "#id")
-
public MyEntity getEntityById(Long id) {
-
return repository.findById(id).orElse(null);
-
}
-
-
@CachePut(key = "#entity.id")
-
public void saveEntity(MyEntity entity) {
-
repository.save(entity);
-
}
-
-
@CacheEvict(key = "#id")
-
public void deleteEntityById(Long id) {
-
repository.deleteById(id);
-
}
-
}
Condition & Unless
-
condition作用:指定缓存的条件(满足什么条件才缓存),可用SpEL表达式(如#id>0,表示当入参 id 大于 0 时才缓存) -
unless作用 : 否定缓存,即满足unless指定的条件时,方法的结果不进行缓存,使用unless时可以在调用的方法获取到结果之后再进行判断(如 #result == null,表示如果结果为 null 时不缓存)
-
//when id >10, the @CachePut works.
-
@CachePut(key = "#entity.id", condition="#entity.id > 10")
-
public void saveEntity(MyEntity entity) {
-
repository.save(entity);
-
}
-
-
-
//when result != null, the @CachePut works.
-
@CachePut(key = "#id", condition="#result == null")
-
public void saveEntity1(MyEntity entity) {
-
repository.save(entity);
-
}
清理全部缓存
通过allEntries、beforeInvocation属性可以来清除全部缓存数据,不过allEntries是方法调用后清理,beforeInvocation是方法调用前清理。
-
//方法调用完成之后,清理所有缓存
-
@CacheEvict(value="myCache",allEntries=true)
-
public void delectAll() {
-
repository.deleteAll();
-
}
-
-
//方法调用之前,清除所有缓存
-
@CacheEvict(value="myCache",beforeInvocation=true)
-
public void delectAll() {
-
repository.deleteAll();
-
}
SpEL表达式
Spring Cache注解中频繁用到SpEL表达式,那么具体如何使用呢?
SpEL 表达式的语法

Spring Cache可用的变量

最佳实践
通过Spring缓存注解可以快速优雅地在我们项目中实现缓存的操作,但是在双写模式或者失效模式下,可能会出现缓存数据一致性问题(读取到脏数据),Spring Cache 暂时没办法解决。最后我们再总结下Spring Cache使用的一些最佳实践。
-
只缓存经常读取的数据:缓存可以显着提高性能,但只缓存经常访问的数据很重要。很少或从不访问的缓存数据会占用宝贵的内存资源,从而导致性能问题。
-
根据应用程序的特定需求选择合适的缓存提供程序和策略。
SpringBoot支持多种缓存提供程序,包括Ehcache、Hazelcast和Redis。 -
使用缓存时请注意潜在的线程安全问题。对缓存的并发访问可能会导致数据不一致或不正确,因此选择线程安全的缓存提供程序并在必要时使用适当的同步机制非常重要。
-
避免过度缓存。缓存对于提高性能很有用,但过多的缓存实际上会消耗宝贵的内存资源,从而损害性能。在缓存频繁使用的数据和允许垃圾收集不常用的数据之间取得平衡很重要。
-
使用适当的缓存逐出策略。使用缓存时,重要的是定义适当的缓存逐出策略以确保在必要时从缓存中删除旧的或陈旧的数据。
-
使用适当的缓存键设计。缓存键对于每个数据项都应该是唯一的,并且应该考虑可能影响缓存数据的任何相关参数,例如用户 ID、时间或位置。
-
常规数据(读多写少、即时性与一致性要求不高的数据)完全可以使用 Spring Cache,至于写模式下缓存数据一致性问题的解决,只要缓存数据有设置过期时间就足够了。
-
特殊数据(读多写多、即时性与一致性要求非常高的数据),不能使用 Spring Cache,建议考虑特殊的设计(例如使用 Cancal 中间件等)。
SpringBoot3实现优雅停机的完整流程
在现代微服务架构中,优雅停机(Graceful Shutdown)是一项重要功能,可以确保服务在关闭时处理完所有当前请求,避免突然终止连接或丢失数据。Spring Boot 3 提供了对优雅停机的内置支持,允许在关闭应用程序上下文期间为现有请求设置一个宽限期,同时防止新请求进入。本文将详细介绍 Spring Boot 3 的优雅停机机制,重点分析 Tomcat 和 Reactor Netty 两种常用的嵌入式 Web 服务器的优雅停机流程。
1. 什么是优雅停机?
优雅停机的目标是在服务关闭时:
- 允许当前的处理请求在指定的宽限期内完成。
- 阻止新的请求进入。
- 向外部监控或负载均衡器标记服务为不可用。
这种机制可以确保服务在维护或版本升级时避免数据丢失和请求中断,提供更高的稳定性和可用性。
2. Spring Boot 3 优雅停机的配置
在 Spring Boot 3 中,我们可以使用 server.shutdown 配置来开启优雅停机,并指定宽限期。配置项如下:
|
1
2
3
4
5
|
server: shutdown: "graceful" # 开启优雅停机spring: lifecycle: timeout-per-shutdown-phase: "20s" # 停机的宽限期,默认为 30 秒 |
此配置项适用于所有四种嵌入式 Web 服务器:Tomcat、Jetty、Reactor Netty 和 Undertow。
注意:Spring Boot 3 默认禁用优雅停机,需要将
server.shutdown设置为graceful以启用。
3. Tomcat 和 Reactor Netty 的优雅停机机制
Spring Boot 3 支持在不同的 Web 服务器上实现优雅停机。以下是 Tomcat 和 Reactor Netty 的具体停机方式:
3.1 Tomcat 优雅停机
使用Tomcat的优雅关机需要Tomcat 9.0.33或更高版本,在 Tomcat 上启用优雅停机后,当收到关闭信号时,它将停止接受新的连接请求,并在网络层阻止传入流量:
- 阻止新请求:一旦启动关闭流程,Tomcat 将在网络层拒绝新的请求连接。
- 完成现有请求:Tomcat 会确保已有请求在指定的宽限期内完成。如果请求未完成且宽限期到达,将强制终止。
注意:若某些请求未在宽限期内完成,则这些请求将被中断。
3.2 Reactor Netty 优雅停机
Reactor Netty 是 Spring WebFlux 默认使用的非阻塞式 Web 服务器,适合响应式编程。Reactor Netty 的优雅停机实现方式如下:
- 网络层停止:当关闭信号到达,Reactor Netty 将停止接受新请求连接,并释放相关资源。
- 等待宽限期:当前所有活动请求在宽限期内继续处理;在宽限期结束后,未完成的请求将被强制中止。
Reactor Netty 在优雅停机期间通过停止接受新的连接来实现无缝停机。其无阻塞模型让服务在短时间内完成停机。
Reactor Netty 在优雅停机期间通过停止接受新的连接来实现无缝停机。其无阻塞模型让服务在短时间内完成停机。
4. 优雅停机的流程
在 Tomcat 和 Reactor Netty 上的优雅停机流程类似,大致包含以下几个步骤:
- 标记服务不可用:停止接收新的请求,通常是通过在负载均衡器中剔除该服务或在网络层阻断连接来实现。
- 设置宽限期:当前请求允许在宽限期内继续处理。
- 关闭活动连接:宽限期结束后,所有未完成的请求会被中止,资源释放。
Spring Boot 3 的 SmartLifecycle 和 ApplicationContext 控制器在关闭阶段对生命周期进行管理,保证所有组件按照顺序优雅停止。
5. 实现优雅停机的完整示例
我们可以创建一个简单的 Spring Boot 3 应用来展示优雅停机配置。
5.1 代码示例
在 application.yml 中启用优雅停机并设置宽限期为 30 秒:
|
1
2
3
4
5
|
server: shutdown: "graceful" # 开启优雅停机spring: lifecycle: timeout-per-shutdown-phase: "30s" # 停机的宽限期,默认为 30 秒 |
创建一个简单的 REST 控制器,模拟一个处理时间较长的请求:
|
1
2
3
4
5
6
7
8
9
10
11
|
@RestController@RequestMapping("/api")public class DemoController { @GetMapping("/long-running") public String longRunningTask() throws InterruptedException { System.out.println("开始执行耗时任务..."); Thread.sleep(20000); // 模拟耗时任务 return "任务完成"; }} |
此控制器会等待 20 秒后返回结果。通过优雅停机机制,即使应用关闭,也会允许该任务在 30 秒宽限期内完成。
启动类里添加一段代码方便打印服务何时停止运行:
|
1
2
3
4
|
@PreDestroypublic void destroy() { System.out.println("Application is destroyed");} |
5.2 IDEA 停止服务
由于 IDEA 运行的服务点击红点结束,会直接停止程序,无法模拟停机,Linux 上通过 java -jar 运行的程序没有这种烦恼,所有此处引入 actuator 的功能,它可以执行 shutdown。
|
1
2
3
4
|
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId></dependency> |
增加如下 actuator 配置:
|
1
2
3
4
5
6
7
8
|
management: endpoints: web: exposure: include: '*' endpoint: shutdown: enabled: true # 开放停机端口 |
调用 curl -X POST http://localhost:8080/actuator/shutdown,即可停止服务:

5.3 测试优雅停机
启动应用并访问 http://localhost:8080/api/long-running,然后调用 http://localhost:8080/actuator/shutdown 停止服务。
请求在宽限期内返回 任务完成。

修改超期时间为 10s,超过宽限期后,请求被中止。

6. 负载均衡器中的停机策略
在实际应用中,负载均衡器(如 Nginx、Kubernetes)也可以在服务停机时配合优雅停机流程,通过从负载均衡池中剔除当前实例来防止新流量进入。这样可以确保所有请求被其他实例接管,而当前实例只处理已有请求,直至完成后停机。
7. 优雅停机的注意事项
- 宽限期配置:设置合理的宽限期,确保长时间请求可以完成。
- 负载均衡器协作:在生产环境中建议与负载均衡器配合,实现完整的优雅停机流程。
- 避免频繁停机:频繁停机会中断长时间任务,应避免在高负载时频繁重启应用。
8. 总结
在 Spring Boot 3 中,通过简单配置即可实现优雅停机,确保服务在关闭时能够完整处理当前请求,减少对用户体验的影响。在 Tomcat 和 Reactor Netty 上实现的优雅停机过程相似,都采用了在网络层阻止新请求和在应用层设置宽限期的方式。优雅停机机制在高并发服务中显得尤为重要,是微服务架构中保持稳定性和一致性的关键。
背景
想象一下,你正在乘坐一辆出租车,突然司机说:“车到一半我得下班了,您赶紧下车吧!”这样的经历肯定会让人感到非常糟糕。这不仅会导致用户体验极差,还可能让出租车平台的订单状态变得混乱——明明行程还在,却被迫中断,最终产生“脏数据”。
在微服务应用中,这种情况的对应场景便是应用程序的突然关闭。无论是因为维护需要还是意外问题,都可能导致用户请求中断,数据未能及时处理。为了解决这个问题,优雅停机机制成为现代系统中的重要一环。
1. 什么是优雅停机?
定义: 优雅停机是指在应用停止时,确保以下几点:
- 拒绝新请求:关闭过程开始后,系统不再接收新的用户请求。
- 完成当前请求:对已接收的请求完成处理,避免突然中断。
- 资源清理:在停机前,释放各种资源(数据库连接、线程池等),保证系统的状态完整性。
目标: 优雅停机的核心是提供一种“无感知”的下线体验,让用户和系统都能安全退出。
2. Spring Boot 优雅停机的基础实现
从 Spring Boot 2.3 开始,优雅停机的支持更加简单和强大。通过设置 server.shutdown 配置,可以决定应用停机时的行为。
2.1立即停机模式
在立即停机模式下,应用会立刻中断所有请求和任务。
server:
shutdown: immediate
虽然简单高效,但这种方式通常只适用于测试或无状态服务。
2.2优雅停机模式
在优雅停机模式下,Spring Boot 会等待当前的处理任务完成,再进行停机操作。
server:
shutdown: graceful
注意: 该模式下的默认等待时间为 30 秒,可通过
spring.lifecycle.timeout-per-shutdown-phase进行配置。
3. 核心机制解析
3.1. 启用 Shutdown Hook
Spring Boot 默认会通过 JVM 的 Shutdown Hook 触发优雅停机。确保以下配置启用:
spring:
main:
register-shutdown-hook: true
3.2. 自定义资源释放逻辑
如果需要在停机时执行特定的清理操作,比如关闭数据库连接或停止线程池,可以通过添加 Shutdown Hook 或实现 DisposableBean 接口。
示例代码:
@Component
public class GracefulShutdownTask implements DisposableBean {
@Override
public void destroy() throws Exception {
System.out.println("关闭数据库连接...");
System.out.println("释放线程池...");
// 其他清理操作
}
}
或者直接通过 JVM 钩子实现:
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("执行自定义的资源清理逻辑");
}));
3.3. 超时机制
避免因某些请求耗时过长导致系统停机过程被阻塞,可以通过以下配置设置超时时间:
spring:
lifecycle:
timeout-per-shutdown-phase: 20s # 默认30秒
4. 优雅停机的实际应用场景
4.1. 服务更新
在系统版本升级时,通过优雅停机完成请求处理和资源释放,避免对用户造成干扰。
4.2. 流量调控
在高并发场景下,如果需要暂时下线部分服务节点,优雅停机可以帮助实现“无感”迁移。
4.3. 订单处理
如出租车平台,在订单完成后再下线服务,避免出现“中途被抛弃”的情况。
5. 优雅停机可能失效的情况
- 强制关闭:使用
kill -9强制终止进程将导致优雅停机机制无法触发。 - 资源耗尽:系统资源不足可能导致清理操作无法完成。
- 未配置超时:如果未配置超时时间,处理长时间任务可能导致停机时间过长。
6. 实现无感知上下线
在高可用系统中,优雅停机通常需要配合流量控制机制实现。例如:
- Nginx 或 服务网关:停机前先从负载均衡器中移除节点,停止分发新请求。
- 健康检查:通过关闭健康检查响应,通知其他服务节点下线。
总结
优雅停机是一项提高用户体验和系统稳定性的关键机制。它通过拒绝新请求、完成当前任务、清理资源等方式,让应用的下线过程更安全、更友好。
通过 Spring Boot 提供的简单配置和扩展接口,我们可以轻松实现优雅停机,同时结合流量控制机制进一步优化用户体验。
推荐实践:
- 开发自定义的停机钩子,记录日志或上报指标,便于监控。
- 在服务升级流程中,优雅停机应作为标准操作。
- 配合负载均衡器进行无感上下线,进一步优化用户体验。
参考
【Spring Boot】【优雅停机一】Spring Boot 停机的正确方式 - 酷酷- - 博客园 (cnblogs.com)
【Spring Boot】【优雅停机二】Spring Boot 停机的正确方式 - 酷酷- - 博客园 (cnblogs.com)
【SpringBoot】SpringBoot优雅停机机制-CSDN博客
如果在application.yml上有这样一个报错
在application.YMLDeprecated configuration property ‘spring.mvc.favicon.enabled’
翻译为 : spring boot设置favicon,favicon不生效,不成功,不起作用
修改方式为 将mvc注释掉 或者删除MVC即可
方式一:
关闭默认图标
在application.properties中添加:
spring.mvc.favicon.enabled=false
或者(我这个有效果):
spring.favicon.enabled = false
方式二:

如果具体操作可以这个链接
如果该文件使用git进行add,那么在该项目下在创建类之前会有一个是否添加到git上,如果,添加那么类名就会变为绿色,如果不添加name类名就会变红色;
Favicon配置
默认的Favicon图标

关闭默认图标
在application.properties中添加:
spring.mvc.favicon.enabled=false
或者(我这个有效果):
spring.favicon.enabled = false
效果

具体原因:https://jira.spring.io/browse/SPR-12851
spring boot设置favicon,favicon不生效,不成功,不起作用
springboot显示的是一片叶子,我们如何使用自己的favicon呢?
1.将favicon.icon放到resources目录下 例如:/public,/static等等
2.完成上面的步骤还不能显示,还需在你的页面的head标签添加代码
-
<head>
-
<meta charset="UTF-8">
-
<title>登录</title>
-
<link rel="shortcut icon" th:href="@{/favicon.ico}"/>
-
<link rel="bookmark" th:href="@{/favicon.ico}"/>
-
</head>
3.注意我使用的thymeleaf所以是以上代码片段如果你不是请这样添加
-
<head>
-
<meta charset="UTF-8">
-
<title>登录</title>
-
<link rel="shortcut icon" href="/favicon.ico"/>
-
<link rel="bookmark" href="/favicon.ico"/>
-
</head>
1.应用监控介绍
Spring Boot大部分模块都是用于开发业务功能或者连接外部资源。除此之外,Spring Boot还提供了spring-boot-starter-actuator模块,该模块主要用于管理和监控应用,它是一个用于暴露自身信息的模块,可以有效地减少监控系统在采集应用指标时的开发量。
spring-boot-starter-actuator模块提供了监控和管理端点以及一些常用的扩展和配置方式,具体如表所示。

2.使用监控
2.1 引入依赖
在Spring Boot中使用监控,首先需要在pom.xml文件中引入所需的依赖spring-boot-starter-actuator,具体代码如下:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> <version>1.5.10.RELEASE</version> </dependency>2.2 添加配置
在pom.xml文件引入spring-boot-starter-actuator依赖包之后,需要在application.properties文件中添加如下的配置信息:
### 应用监控配置 # 指定访问这些监控方法的端口 management.server.port=8080- management.server.port:用于指定访问这些监控方法的端口。
2.3 测试
spring-boot-starter-actuator依赖和配置都添加成功之后,重新启动spring-boot-book-v2项目,项目启动成功之后,在浏览器中输入http://localhost:8080/actuator,可以看到如图所示的输出信息。

从图中可以看出,actuator只暴露了3个简单的endpoint,并且只有/health接口的内容还有点用,可以检查应用服务是否健康。当然,actuator绝对不止这么点功能,只是出于安全考虑,其余的endpoint默认被禁用了。在浏览器中输入:http://localhost:8080/actuator/health,可以看到应用的健康信息,“UP”代表应用是健康状态。
为了简单起见,我们来开启所有的接口。只需要在application.properties文件中加入一行配置即可:
### 开启所有的端点 management.endpoints.web.exposure.include=*重新在浏览器中输入http://localhost:8080/actuator,便可以看到所有的endpoint,如图所示。

对于不带任何参数的读取操作,端点自动缓存对其响应。要配置端点缓存响应的时间,请使用cache.time-live属性,以下示例将beans端点缓存的生存时间设置为10秒:
默认情况下,端点通过使用端点的ID在/actuator路径下的HTTP上公开,例如,beans端点暴露在/actuator/beans下。如果要将端点映射到其他路径,则可以使用management.endpoints.web.path-mapping属性。另外,如果想更改基本路径,则可以使用management.endpoints.web.base-path。以下示例将/actuator/health重新映射到/healthcheck:
management.endpoints.web.base-path=/ management.endpoints.web.path-mapping.health=healthcheck要配置单个端点的启用,请使用management.endpoint.<id>.enabled属性,以下示例启用了shutdown端点:
management.endpoint.shutdown.enabled= true另外,可以通过management.endpoints.enabled-by-default来修改全局端口的默认配置,以下示例启用info端点并禁用所有其他端点:
management.endpoints.enabled-by-default=false management.endpoints.info.enabled=true其他端点测试,可以按照表所示的访问路径依次访问测试。

在浏览器中可以把返回的数据格式化成json格式,这是因为在Google浏览器中安装了JsonView插件,具体安装步骤如下:
- 浏览器中输入链接:https://github.com/search?utf8=%E2%9C%93&q=jsonview,在弹出的页面中单击gildas-lormeau/JSONView-for-Chrome,如图所示。

- 单击【Download Zip】,插件下载完成,解压缩到相应目录中(D:\Download\JSONView-for-Chrome-master)。
- 在浏览器右上角单击【更多工具】→【扩展程序】→【加载已解压的扩展程序】。选择插件目录(D:\Download\JSONView-for-Chrome-master\WebContent)。
- 安装完成后,重新启动浏览器(快捷键Ctrl+R)。
3.自定义端点
3.1 自定义端点EndPoint
spring-boot-starter-actuator模块中已经提供了许多原生端点。根据端点的作用,我们可以把原生端点分为以下3大类。
- 应用配置类:获取应用程序中加载的应用配置、环境变量、自动化配置报告等与Spring Boot应用密切相关的配置类信息。
- 度量指标类:获取应用程序运行过程中用于监控的度量指标,比如内存信息、线程池信息、HTTP请求统计等。
- 操作控制类:提供了对应用的关闭等操作类功能。
如果spring-boot-starter-actuator模块提供的这些原生端点无法满足需求,还可以自定义端点,自定义端点时,只要继承抽象类AbstractEndpoint即可。这里在spring-boot-book-v2目录/src/main/java/com.example.demo下新建actuator包,在actuator包下新建自定义端点类AyUserEndPoint,AyUserEndPoint主要用来监控数据库用户信息情况,比如用户总数量、被删除用户数量、活跃用户数量等。自定义端点AyUserEndPoint类的代码如下:
@Component @Endpointid="userEndPoints") public class AyUserEndpoint { @Resource private AyUserService ayUserService; @ReadOperation public Map<String, Object> invoke(){ Map<String,Object> map = new HashMap<String, Object>(); //当前时间 map.put("current_time",new Date()); //用户总数量 map.put("user_num", ayUserService.findUserTotalNum()); return map; } }- @Endpoint(id=“userEndPoints”):@Endpoint注解简化了创建用户自定义端点的过程,@Endpoint相当于@WebEndpoint和@JmxEndpoint的整合,Web和jmx方式都支持。
- @WebEndpoint:只会生成Web的方式的端点监控。
- @JmxEndpoint:只会生成Jmx的方式监控。
在AyUserEndPoint类中,我们注入AyUserService接口,并在invoke方法中调用findUserTotalNum方法,查询当前数据库总的用户数。所以需要在AyUserService接口中添加方法findUserTotalNum,具体代码如下:
//查询用户数量 Long findUserTotalNum();同时,在AyUserServiceImpl类中实现方法findUserTotalNum,具体代码如下:
@Override public Long findUserTotalNum(){ return ayUserRepository.count(); }AyUserService类与AyUserServiceImpl类开发完成之后,就可以在invoke方法中使用。在invoke方法中定义Map集合,并向Map集合存放当前时间current_time和数据库用户总数user_num。
3.2 测试
代码开发完成之后,重启启动spring-boot-book-v2项目,在浏览器中输入访问地址:http://localhost:8080/actuator/userEndPoints,便可以看到请求到数据,具体数据如下:
{"user_num" : 3, "current":1512817762910}从返回数据中,可以看出当前数据库总共有3个用户,以及当前具体时间(毫秒)。
3.3 自定义HealthIndicator
默认端点Health的信息是从HealthIndicator的bean中收集的,Spring中内置了一些HealthIndicator,如表所示。

启动项目spring-boot-book-v2,在浏览器中输入访问链接:http://localhost:8080/actuator/health,可以看到返回的Spring Boot应用健康数据只有:
{ "status":"UP" }如果想要查看详细的应用健康信息,需要添加以下配置:
management.endpoint.health.show-details=always配置完成之后,再次访问http://localhost:8080/actuator/health,获取的信息如下:

从上面的信息中,可以方便地查看目前应用所依赖资源(Redis、MongoDB)的运行情况及其他信息。
Tips:management.endpoint.health.show-details的值除了always之外还有when-authorized、never,默认值是never。
如果想要自定义符合自己业务需求的检查健康,需要自定义HealthIndicator来获得更多应用健康的信息。在spring-boot-book-v2项目目录/src/main/java/com.example.actuator下新建MyHealthIndicator类,该类实现HealthIndicator接口并重写health方法,MyHealthIndicator类具体代码如下:
@Component public class MyHealthIndicator implements HealthIndicator{ @Override public Health health(){ Long totalSpace = checkTocalSpace(); Long free checkFree(); String status= checkstatus(); checkFree(); return new Health.Builder() .up() .withDetail("status", status) .withDetail("total", totalSpace) .withDetail("free", free) .build(); } private String checkStatus(){ //结合真实项目,获取相关参数 return "Up"; } private Long checkTocalSpace(){ //结合真实项目,获取相关参数 return 10000L; } private Long checkFree (){ //结合真实项目,获取相关参数 return 5000L; } }3.4 测试
代码开发完成之后,重新启动spring-boot-book-v2项目,项目启动成功之后,在浏览器中输入访问链接:http://localhost:8080/actuator/health,可以获得自定义健康类MyHealthIndicator返回的结果,具体结果信息如下:
my: { status:"UP", total: 10000, free: 5000 } //忽略其他健康数据从上面返回的json结果信息可以看出,json结果信息的key:my,也就是英文MyHealthIndicator去掉HealthIndicator。如果自定义健康类取名为MyDefineHealthIndicator,则返回结果信息将会变成:
myDefine: { status:"UP", total: 10000, free: 5000 } //忽略其他健康数据一般情况下,不会直接实现 HealthIndicator接口,而是继承AbstractHealthIndicator抽象类。因此,我们只需要重写doHealthCheck方法,并在这个方法中关注具体的健康检测的业务逻辑服务即可。
4.保护Actuator端点
Actuator端点发布的信息很多都涉及敏感信息和高危操作。比如/shutdown端点,它可以直接关闭应用程序,如果随便某个人都有权限访问该端点,那是非常危险的。因此,有必要控制Actuator端点的访问权限以避免Actuator端点被非法访问。想要保护Actuator端点,可以使用保护其他URL路径一样的方式,通过使用Spring Security来控制URL路径的授权访问。
在第14章中,我们已经在Spring Boot中集成了Spring Security,并且开发了WebSecurityConfig配置类对用户登录进行授权访问,现在我们改造该类,具体代码如下:
@Configuration @EnableWebSecurity public class WebsecurityConfig extends WebSecurityConfigurerAdapter { //省略代码 @Override protected void configure(HttpSecurity http) throws Exception { //路由策略和访问权限的简单配置 http .authorizeRequests() //要求有管理员的权限 //登录失败返回URL:/loginerror.antMatchers("/shutdown").access("hasRole('ADMIN')") //登录成功跳转URL,这里跳转到用户首页 //登录页面全部权限可访问 .antMatchers("/**").permitAll() .and() .formLogin() //启用默认登录页面 .failureUrl("/login?error") .defaultSuccessUrl("/ayUser/test") .permitAll(); super.configure(http); } }通过使用antMatchers("/shutdown").access(“hasRole(‘READER’)”)方法,对/shutdown进行授权访问,/shutdown端点现在仅允许拥有ADMIN权限的用户进行访问。
端点/shutdown已经被保护起来了,假如现在想保护其他端点,例如/metrics、/health等,只需要为antMatchers()传入输入参数即可。具体代码如下:
.authorizeRequests() //要求有管理员的权限 .antMatchers("/shutdown","/metrics","/health").access("hasRole('READER')")如果觉得每次添加一个端点的访问权限都得在antMatchers()方法中修改很麻烦,可以在application.properties配置文件中配置端点访问的上下文,具体配置如下:
### 配置端点访问的上下文路径 management.endpoints.web.base-path=/manage此时,在为Actuator端点赋予ADMIN权限限制的时候就能借助这个上下文/manage:
//要求有管理员的权限 .antMatchers("/manage/**").access("hasRole('READER')")

浙公网安备 33010602011771号