Spring Boot项目如何热部署?
这可以使用 DEV 工具来实现。通过这种依赖关系,您可以节省任何更改,嵌入式tomcat 将重新启动。
Spring Boot 有一个开发工具(DevTools)模块,它有助于提高开发人员的生产力。Java 开发人员面临
的一个主要挑战是将文件更改自动部署到服务器并自动重启服务器。开发人员可以重新加载 Spring
Boot 上的更改,而无需重新启动服务器。这将消除每次手动部署更改的需要。
Spring Boot 在发布它的第一个版本时没有这个功能。这是开发人员 需要的功能。DevTools 模块完全满
足开发人员的需求。该模块将在生产环境中被禁用。
它还提供 H2 数据库控制台以更好地测试应用程序。
您使用了哪些 starter maven 依赖项?
使用了下面的一些依赖项
spring-boot-starter-activemq
spring-boot-starter-security
这有助于增加更少的依赖关系,并减少版本的冲突。
Spring Boot 中的 starter 到底是什么 ?
首先,这个 Starter 并非什么新的技术点,基本上还是基于 Spring 已有功能来实现的。首先它提供了一
个自动化配置类,一般命名为 XXXAutoConfiguration
,在这个配置类中通过条件注解来决定一个配置是否生效(条件注解就是
Spring 中原本就有的),然后它还会提供一系列的默认配置,也允许开发者根据实际情况自定义相关配
置,然后通过类型安全的属性注入将这些配置属性注入进来,新注入的属性会代替掉默认属性。正因为
如此,很多第三方框架,我们只需要引入依赖就可以直接使用了。当然,开发者也可以自定义 Starter
spring-boot-starter-parent 有什么用 ?
我们都知道,新创建一个 Spring Boot 项目,默认都是有 parent 的,这个
parent 就是 spring-boot-starter-parent ,spring-boot-starter-parent 主要有如下作用:
1. 定义了 Java 编译版本为 1.8 。
2. 使用 UTF-8 格式编码。
3. 继承自 spring-boot-dependencies,这个里边定义了依赖的版本,也正是因为继承了这个依赖,
所以我们在写依赖时才不需要写版本号。
4. 执行打包操作的配置。
5. 自动化的资源过滤。
6. 自动化的插件配置。
1 <dependency>
2 <groupId>org.springframework.boot</groupId>
3 <artifactId>spring-boot-devtools</artifactId>
4 </dependency>7. 针对 application.properties 和 application.yml 的资源过滤,包括通过 profile 定义的不同环境的
配置文件,例如 applicationdev.properties 和 application-dev.yml。
Spring Boot 打成的 jar 和普通的 jar 有什么区别 ?
Spring Boot 项目 终打包成的 jar 是可执行 jar ,这种 jar 可以直接通过 java jar xxx.jar 命令来运行,
这种 jar 不可以作为普通的 jar 被其他项目依赖,即使依赖了也无法使用其中的类。
Spring Boot 的 jar 无法被其他项目依赖,主要还是他和普通 jar 的结构不同。普通的 jar 包,解压后直
接就是包名,包里就是我们的代码,而 Spring Boot
打包成的可执行 jar 解压后,在 \BOOT-INF\classes 目录下才是我们的代码,因此无法被直接引用。如
果非要引用,可以在 pom.xml 文件中增加配置,将 Spring Boot 项目打包成两个 jar ,一个可执行,一
个可引用。
运行 Spring Boot 有哪几种方式?
1) 打包用命令或者放到容器中运行
2) 用 Maven/ Gradle 插件运行
3)直接执行 main 方法运行
Spring Boot 需要独立的容器运行吗?
开启 Spring Boot 特性有哪几种方式?
1) 继承spring-boot-starter-parent项目
2) 导入spring-boot-dependencies项目依赖
如何使用 Spring Boot 实现异常处理?
Spring 提供了一种使用 ControllerAdvice 处理异常的非常有用的方法。 我们通过实现一个
ControlerAdvice 类,来处理控制器类抛出的所有异常。
如何使用 Spring Boot 实现分页和排序?
使用 Spring Boot 实现分页非常简单。使用 Spring Data-JPA 可以实现将可分页的传递给存储库方法。
微服务中如何实现 session 共享 ?
在微服务中,一个完整的项目被拆分成多个不相同的独立的服务,各个服务独立部署在不同的服务器
上,各自的 session 被从物理空间上隔离开了,但是经
常,我们需要在不同微服务之间共享 session ,常见的方案就是 Spring
Session + Redis 来实现 session 共享。将所有微服务的 session 统一保存在 Redis 上,当各个微服务对
session 有相关的读写操作时,都去操作 Redis 上的 session 。这样就实现了 session 共享,Spring
Session 基于 Spring 中的代理过滤器实现,使得 session 的同步操作对开发人员而言是透明的,非常简
便。
Spring Boot 中如何实现定时任务 ?
定时任务也是一个常见的需求,Spring Boot 中对于定时任务的支持主要还是来自 Spring 框架。
在 Spring Boot 中使用定时任务主要有两种不同的方式,一个就是使用 Spring 中的 @Scheduled 注
解,另一个则是使用第三方框架 Quartz。
使用 Spring 中的 @Scheduled 的方式主要通过 @Scheduled 注解来实现。使用 Quartz ,则按照 Quartz 的方式,定义 Job 和 Trigger 即可。
Spring Boot 原理
Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化新 Spring 应用的初始搭建以及
开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。通过这
种方式,Spring Boot 致力于在蓬勃发展的快速应用开发领域(rapid application development)成为领导
者。其特点如下:
1. 创建独立的 Spring 应用程序
2. 嵌入的 Tomcat,无需部署 WAR 文件
3. 简化 Maven 配置
4. 自动配置 Spring
5. 提供生产就绪型功能,如指标,健康检查和外部配置
6. 绝对没有代码生成和对 XML 没有要求配置 [1]
Spring Boot比Spring做了哪些改进?
1)Spring Boot可以建立独立的Spring应用程序;
2)内嵌了如Tomcat,Jetty和Undertow这样的容器,也就是说可以直接跑起来,用不着再做 部署工作
了;
3)无需再像Spring那样搞一堆繁琐的xml文件的配置;
4)可以自动配置Spring。SpringBoot将原有的XML配置改为Java配置,将bean注入改为使 用注解注入
的方式(@Autowire),并将多个xml、properties配置浓缩在一个appliaction.yml 配置文件中。
5)提供了一些现有的功能,如量度工具,表单数据验证以及一些外部配置这样的一些第三方功 能;
6)整合常用依赖(开发库,例如spring-webmvc、jackson-json、validation-api和tomcat 等),提供
的POM可以简化Maven的配置。当我们引入核心依赖时,SpringBoot会自引入其 他依赖。
Spring boot 热加载在实际的开发中避免不了自己测试的时候修修改改,甚至有些源代码的修改是需要重启项目的,这个时
候热加载就帮了大忙了,其会自动将修改的代码应用到部署的项目中去,而不用自己再次的去手动重
启,大大的提高了我们开发的效率,实现了代码随时改效果立马生效的效果,好了废话不多说了,下面
来介绍怎解嵌入热加载的实现。
在pom文件中添加依赖(optional-->true表示覆盖父级项目中的引用):
org.springframework.boot </ groupId>
spring-boot-devtools </ artifactId>
true </ optional>
</ dependency>
就是这么简单,这样就可以了,当然了,有些时候我们再修改一些文件时是并不希望其触发重启的,例
如一些静态资源等,此时我们可以设置一些排序路径来将其排除出去,在此之前先来介绍一下触发重启
的条件吧:当DevTools监视类路径资源时,触发重新启动的唯一方法是更新类路径。导致类路径更新的
方式取决于您正在使用的IDE。在Eclipse中,保存修改的文件将导致类路径被更新并触发重新启动。在
IntelliJ IDEA中,构建project(Build -> Make Project)将具有相同的效果。
如果要自定义一些排除项,您可以使用该spring.devtools.restart.exclude属性。例如,仅排除 /static
和/public你设置如下:
spring.devtools.restart.exclude=static/,public/
当您对不在类路径中的文件进行更改时,可能需要重新启动或重新加载应用程序。为此,请使用该
spring.devtools.restart.additional-paths属性配置其他路径来监视更改。
如果您不想使用重新启动功能,可以使用该spring.devtools.restart.enabled属性禁用它 。在大多数情
况下,您可以将其设置为 application.properties(仍将初始化重新启动类加载器,但不会监视文件更
改)。
例如,如果您需要完全禁用重新启动支持,因为它不适用于特定库,则需要System在调用之前设置属
性 SpringApplication.run(…)。例如:
public static void main(String[] args) {
System.setProperty("spring.devtools.restart.enabled", "false");
SpringApplication.run(MyApp.class, args);
}
我不知道大家都使用的是什么IDE,我现在用的是IntelliJ IDEA,而这个工具有各特点就是编辑文件后其
会立刻自动保存,并不需要手动的Ctrl+s来操作,造成的结果就是每当我修改一个触发启动的文件的时
候其就会自动进行热加载,而我们并不想这样频繁的去热加载的话,可以进行一些特殊的设计实现仅在
特定的时间去触发热加载:我们可以使用“触发文件”,这是一个特殊文件,当您要实际触发重新启动检
查时,必须修改它。更改文件只会触发检查,只有在Devtools检测到它必须执行某些操作时才会重新启
动。触发文件可以手动更新,也可以通过IDE插件更新。要使用触发器文件使用该
spring.devtools.restart.trigger-file属性。
Spring Boot设置有效时间和自动刷新缓存,时间支持在配
置文件中配置Spring Boot缓存实战 Redis 设置有效时间和自动刷新缓存,时间支
持在配置文件中配置
问题描述
Spring Cache提供的@Cacheable注解不支持配置过期时间,还有缓存的自动刷新。
我们可以通过配置CacheManneg来配置默认的过期时间和针对每个缓存容器(value)单独配置过期时
间,但是总是感觉不太灵活。下面是一个示例:
我们想在注解上直接配置过期时间和自动刷新时间,就像这样:
value属性上用#号隔开,第一个是原始的缓存容器名称,第二个是缓存的有效时间,第三个是缓存的自
动刷新时间,单位都是秒。
缓存的有效时间和自动刷新时间支持SpEl表达式,支持在配置文件中配置,如:
解决思路
查看源码你会发现缓存最顶级的接口就是CacheManager和Cache接口。
@Bean
public CacheManager cacheManager(RedisTemplate redisTemplate) {
RedisCacheManager cacheManager= new RedisCacheManager(redisTemplate);
cacheManager.setDefaultExpiration(60);
Map<String,Long> expiresMap=new HashMap<>();
expiresMap.put("Product",5L);
cacheManager.setExpires(expiresMap);
return cacheManager;
}
@Cacheable(value = "people#120#90", key = "#person.id")
public Person findOne(Person person) {
Person p = personRepository.findOne(person.getId());
System.out.println("为id、key为:" + p.getId() + "数据做了缓存");
return p;
}
@Cacheable(value =
"people#${select.cache.timeout:1800}#${select.cache.refresh:600}", key =
"#person.id", sync = true)//3
public Person findOne(Person person) {
Person p = personRepository.findOne(person.getId());
System.out.println("为id、key为:" + p.getId() + "数据做了缓存");
return p;
}CacheManager说明
CacheManager功能其实很简单就是管理cache,接口只有两个方法,根据容器名称获取一个Cache。还
有就是返回所有的缓存名称。
Cache说明
Cache接口主要是操作缓存的。get根据缓存key从缓存服务器获取缓存中的值,put根据缓存key将数据
放到缓存服务器,evict根据key删除缓存中的数据。
请求步骤
1. 请求进来,在方法上面扫描@Cacheable注解,那么会触发
org.springframework.cache.interceptor.CacheInterceptor缓存的拦截器。
2. 然后会调用CacheManager的getCache方法,获取Cache,如果没有(第一次访问)就新建一
Cache并返回。
3. 根据获取到的Cache去调用get方法获取缓存中的值。RedisCache这里有个bug,源码是先判断key
是否存在,再去缓存获取值,在高并发下有bug。
代码分析
在最上面我们说了Spring Cache可以通过配置CacheManager来配置过期时间。那么这个过期时间是在
哪里用的呢?设置默认的时间setDefaultExpiration,根据特定名称设置有效时间setExpires,获取一个
缓存名称(value属性)的有效时间computeExpiration,真正使用有效时间是在createCache方法里
面,而这个方法是在父类的getCache方法调用。通过RedisCacheManager源码我们看到:
public interface CacheManager {
/**
* 根据名称获取一个Cache(在实现类里面是如果有这个Cache就返回,没有就新建一个Cache放到
Map容器中)
* @param name the cache identifier (must not be {@code null})
* @return the associated cache, or {@code null} if none found
*/
Cache getCache(String name);
/**
* 返回一个缓存名称的集合
* @return the names of all caches known by the cache manager
*/
Collection<String> getCacheNames();
}
public interface Cache {
ValueWrapper get(Object key);
void put(Object key, Object value);
void evict(Object key);
...
}// 设置默认的时间
public void setDefaultExpiration(long defaultExpireTime) {
this.defaultExpiration = defaultExpireTime;
}
// 根据特定名称设置有效时间
public void setExpires(Map<String, Long> expires) {
this.expires = (expires != null ? new ConcurrentHashMap<String, Long>
(expires) : null);
}
// 获取一个key的有效时间
protected long computeExpiration(String name) {
Long expiration = null;
if (expires != null) {
expiration = expires.get(name);
}
return (expiration != null ? expiration.longValue() : defaultExpiration);
}
@SuppressWarnings("unchecked")
protected RedisCache createCache(String cacheName) {
// 调用了上面的方法获取缓存名称的有效时间
long expiration = computeExpiration(cacheName);
// 创建了Cache对象,并使用了这个有效时间
return new RedisCache(cacheName, (usePrefix ? cachePrefix.prefix(cacheName)
: null), redisOperations, expiration,
cacheNullValues);
}
// 重写父类的getMissingCache。去创建Cache
@Override
protected Cache getMissingCache(String name) {
return this.dynamic ? createCache(name) : null;
}
AbstractCacheManager父类源码:
// 根据名称获取Cache如果没有调用getMissingCache方法,生成新的Cache,并将其放到Map容器中去。
@Override
public Cache getCache(String name) {
Cache cache = this.cacheMap.get(name);
if (cache != null) {
return cache;
}
else {
// Fully synchronize now for missing cache creation...
synchronized (this.cacheMap) {
cache = this.cacheMap.get(name);
if (cache == null) {
// 如果没找到Cache调用该方法,这个方法默认返回值NULL由子类自己实现。上面的
就是子类自己实现的方法
cache = getMissingCache(name);
if (cache != null) {
cache = decorateCache(cache);
this.cacheMap.put(name, cache);
updateCacheNames(name);
}}
return cache;
}
}
}
由此这个有效时间的设置关键就是在getCache方法上,这里的name参数就是我们注解上的value属性。
所以在这里解析这个特定格式的名称我就可以拿到配置的过期时间和刷新时间。getMissingCache方法
里面在新建缓存的时候将这个过期时间设置进去,生成的Cache对象操作缓存的时候就会带上我们的配
置的过期时间,然后过期就生效了。解析SpEL表达式获取配置文件中的时间也在也一步完成。
CustomizedRedisCacheManager源码:
package com.xiaolyuh.redis.cache;
import com.xiaolyuh.redis.cache.helper.SpringContextHolder;
import com.xiaolyuh.redis.utils.ReflectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.cache.Cache;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.core.RedisOperations;
import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
/**
* 自定义的redis缓存管理器
* 支持方法上配置过期时间
* 支持热加载缓存:缓存即将过期时主动刷新缓存
*
* @author yuhao.wang
*/
public class CustomizedRedisCacheManager extends RedisCacheManager {
private static final Logger logger =
LoggerFactory.getLogger(CustomizedRedisCacheManager.class);
/**
* 父类cacheMap字段
*/
private static final String SUPER_FIELD_CACHEMAP = "cacheMap";
/**
* 父类dynamic字段
*/
private static final String SUPER_FIELD_DYNAMIC = "dynamic";
/**
* 父类cacheNullValues字段
*/
private static final String SUPER_FIELD_CACHENULLVALUES = "cacheNullValues";
/**
* 父类updateCacheNames方法*/
private static final String SUPER_METHOD_UPDATECACHENAMES =
"updateCacheNames";
/**
* 缓存参数的分隔符
* 数组元素0=缓存的名称
* 数组元素1=缓存过期时间TTL
* 数组元素2=缓存在多少秒开始主动失效来强制刷新
*/
private static final String SEPARATOR = "#";
/**
* SpEL标示符
*/
private static final String MARK = "$";
RedisCacheManager redisCacheManager = null;
@Autowired
DefaultListableBeanFactory beanFactory;
public CustomizedRedisCacheManager(RedisOperations redisOperations) {
super(redisOperations);
}
public CustomizedRedisCacheManager(RedisOperations redisOperations,
Collection<String> cacheNames) {
super(redisOperations, cacheNames);
}
public RedisCacheManager getInstance() {
if (redisCacheManager == null) {
redisCacheManager =
SpringContextHolder.getBean(RedisCacheManager.class);
}
return redisCacheManager;
}
@Override
public Cache getCache(String name) {
String[] cacheParams = name.split(SEPARATOR);
String cacheName = cacheParams[0];
if (StringUtils.isBlank(cacheName)) {
return null;
}
// 有效时间,初始化获取默认的有效时间
Long expirationSecondTime = getExpirationSecondTime(cacheName,
cacheParams);
// 自动刷新时间,默认是0
Long preloadSecondTime = getExpirationSecondTime(cacheParams);
// 通过反射获取父类存放缓存的容器对象
Object object = ReflectionUtils.getFieldValue(getInstance(),
SUPER_FIELD_CACHEMAP);
if (object != null && object instanceof ConcurrentHashMap) {ConcurrentHashMap<String, Cache> cacheMap =
(ConcurrentHashMap<String, Cache>) object;
// 生成Cache对象,并将其保存到父类的Cache容器中
return getCache(cacheName, expirationSecondTime, preloadSecondTime,
cacheMap);
} else {
return super.getCache(cacheName);
}
}
/**
* 获取过期时间
*
* @return
*/
private long getExpirationSecondTime(String cacheName, String[] cacheParams)
{
// 有效时间,初始化获取默认的有效时间
Long expirationSecondTime = this.computeExpiration(cacheName);
// 设置key有效时间
if (cacheParams.length > 1) {
String expirationStr = cacheParams[1];
if (!StringUtils.isEmpty(expirationStr)) {
// 支持配置过期时间使用EL表达式读取配置文件时间
if (expirationStr.contains(MARK)) {
expirationStr =
beanFactory.resolveEmbeddedValue(expirationStr);
}
expirationSecondTime = Long.parseLong(expirationStr);
}
}
return expirationSecondTime;
}
/**
* 获取自动刷新时间
*
* @return
*/
private long getExpirationSecondTime(String[] cacheParams) {
// 自动刷新时间,默认是0
Long preloadSecondTime = 0L;
// 设置自动刷新时间
if (cacheParams.length > 2) {
String preloadStr = cacheParams[2];
if (!StringUtils.isEmpty(preloadStr)) {
// 支持配置刷新时间使用EL表达式读取配置文件时间
if (preloadStr.contains(MARK)) {
preloadStr = beanFactory.resolveEmbeddedValue(preloadStr);
}
preloadSecondTime = Long.parseLong(preloadStr);
}
}
return preloadSecondTime;
}/**
* 重写父类的getCache方法,真假了三个参数
*
* @param cacheName 缓存名称
* @param expirationSecondTime 过期时间
* @param preloadSecondTime 自动刷新时间
* @param cacheMap 通过反射获取的父类的cacheMap对象
* @return Cache
*/
public Cache getCache(String cacheName, long expirationSecondTime, long
preloadSecondTime, ConcurrentHashMap<String, Cache> cacheMap) {
Cache cache = cacheMap.get(cacheName);
if (cache != null) {
return cache;
} else {
// Fully synchronize now for missing cache creation...
synchronized (cacheMap) {
cache = cacheMap.get(cacheName);
if (cache == null) {
// 调用我们自己的getMissingCache方法创建自己的cache
cache = getMissingCache(cacheName, expirationSecondTime,
preloadSecondTime);
if (cache != null) {
cache = decorateCache(cache);
cacheMap.put(cacheName, cache);
// 反射去执行父类的updateCacheNames(cacheName)方法
Class<?>[] parameterTypes = {String.class};
Object[] parameters = {cacheName};
ReflectionUtils.invokeMethod(getInstance(),
SUPER_METHOD_UPDATECACHENAMES, parameterTypes, parameters);
}
}
return cache;
}
}
}
/**
* 创建缓存
*
* @param cacheName 缓存名称
* @param expirationSecondTime 过期时间
* @param preloadSecondTime 制动刷新时间
* @return
*/
public CustomizedRedisCache getMissingCache(String cacheName, long
expirationSecondTime, long preloadSecondTime) {
logger.info("缓存 cacheName:{},过期时间:{}, 自动刷新时间:{}", cacheName,
expirationSecondTime, preloadSecondTime);
Boolean dynamic = (Boolean) ReflectionUtils.getFieldValue(getInstance(),
SUPER_FIELD_DYNAMIC);
Boolean cacheNullValues = (Boolean)
ReflectionUtils.getFieldValue(getInstance(), SUPER_FIELD_CACHENULLVALUES);return dynamic ? new CustomizedRedisCache(cacheName, (this.isUsePrefix()
? this.getCachePrefix().prefix(cacheName) : null),
this.getRedisOperations(), expirationSecondTime,
preloadSecondTime, cacheNullValues) : null;
}
}
那自动刷新时间呢?
在RedisCache的属性里面没有刷新时间,所以我们继承该类重写我们自己的Cache的时候要多加一个属
性preloadSecondTime来存储这个刷新时间。并在getMissingCache方法创建Cache对象的时候指定该
值。
CustomizedRedisCache部分源码:
/**
* 缓存主动在失效前强制刷新缓存的时间
* 单位:秒
*/
private long preloadSecondTime = 0;
// 重写后的构造方法
public CustomizedRedisCache(String name, byte[] prefix, RedisOperations<?
extends Object, ? extends Object> redisOperations, long expiration, long
preloadSecondTime) {
super(name, prefix, redisOperations, expiration);
this.redisOperations = redisOperations;
// 指定自动刷新时间
this.preloadSecondTime = preloadSecondTime;
this.prefix = prefix;
}
// 重写后的构造方法
public CustomizedRedisCache(String name, byte[] prefix, RedisOperations<?
extends Object, ? extends Object> redisOperations, long expiration, long
preloadSecondTime, boolean allowNullValues) {
super(name, prefix, redisOperations, expiration, allowNullValues);
this.redisOperations = redisOperations;
// 指定自动刷新时间
this.preloadSecondTime = preloadSecondTime;
this.prefix = prefix;
}
那么这个自动刷新时间有了,怎么来让他自动刷新呢?
在调用Cache的get方法的时候我们都会去缓存服务查询缓存,这个时候我们在多查一个缓存的有效时
间,和我们配置的自动刷新时间对比,如果缓存的有效时间小于这个自动刷新时间我们就去刷新缓存
(这里注意一点在高并发下我们最好只放一个请求去刷新数据,尽量减少数据的压力,所以在这个位置
加一个分布式锁)。所以我们重写这个get方法。
CustomizedRedisCache部分源码:
/**
* 重写get方法,获取到缓存后再次取缓存剩余的时间,如果时间小余我们配置的刷新时间就手动刷新缓存。
* 为了不影响get的性能,启用后台线程去完成缓存的刷。
* 并且只放一个线程去刷新数据。
** @param key
* @return
*/
@Override
public ValueWrapper get(final Object key) {
RedisCacheKey cacheKey = getRedisCacheKey(key);
String cacheKeyStr = new String(cacheKey.getKeyBytes());
// 调用重写后的get方法
ValueWrapper valueWrapper = this.get(cacheKey);
if (null != valueWrapper) {
// 刷新缓存数据
refreshCache(key, cacheKeyStr);
}
return valueWrapper;
}
/**
* 重写父类的get函数。
* 父类的get方法,是先使用exists判断key是否存在,不存在返回null,存在再到redis缓存中去取值。
这样会导致并发问题,
* 假如有一个请求调用了exists函数判断key存在,但是在下一时刻这个缓存过期了,或者被删掉了。
* 这时候再去缓存中获取值的时候返回的就是null了。
* 可以先获取缓存的值,再去判断key是否存在。
*
* @param cacheKey
* @return
*/
@Override
public RedisCacheElement get(final RedisCacheKey cacheKey) {
Assert.notNull(cacheKey, "CacheKey must not be null!");
// 根据key获取缓存值
RedisCacheElement redisCacheElement = new RedisCacheElement(cacheKey,
fromStoreValue(lookup(cacheKey)));
// 判断key是否存在
Boolean exists = (Boolean) redisOperations.execute(new
RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws
DataAccessException {
return connection.exists(cacheKey.getKeyBytes());
}
});
if (!exists.booleanValue()) {
return null;
}
return redisCacheElement;
}
/**
* 刷新缓存数据
*/
private void refreshCache(Object key, String cacheKeyStr) {Long ttl = this.redisOperations.getExpire(cacheKeyStr);
if (null != ttl && ttl <= CustomizedRedisCache.this.preloadSecondTime) {
// 尽量少的去开启线程,因为线程池是有限的
ThreadTaskHelper.run(new Runnable() {
@Override
public void run() {
// 加一个分布式锁,只放一个请求去刷新缓存
RedisLock redisLock = new RedisLock((RedisTemplate)
redisOperations, cacheKeyStr + "_lock");
try {
if (redisLock.lock()) {
// 获取锁之后再判断一下过期时间,看是否需要加载数据
Long ttl =
CustomizedRedisCache.this.redisOperations.getExpire(cacheKeyStr);
if (null != ttl && ttl <=
CustomizedRedisCache.this.preloadSecondTime) {
// 通过获取代理方法信息重新加载缓存数据
CustomizedRedisCache.this.getCacheSupport().refreshCacheByKey(CustomizedRedisCa
che.super.getName(), key.toString());
}
}
} catch (Exception e) {
logger.info(e.getMessage(), e);
} finally {
redisLock.unlock();
}
}
});
}
}
那么自动刷新肯定要掉用方法访问数据库,获取值后去刷新缓存。这时我们又怎么能去调用方法呢?
我们利用java的反射机制。所以我们要用一个容器来存放缓存方法的方法信息,包括对象,方法名称,
参数等等。我们创建了CachedInvocation类来存放这些信息,再将这个类的对象维护到容器中。
CachedInvocation源码:
public final class CachedInvocation {
private Object key;
private final Object targetBean;
private final Method targetMethod;
private Object[] arguments;
public CachedInvocation(Object key, Object targetBean, Method targetMethod,
Object[] arguments) {
this.key = key;
this.targetBean = targetBean;
this.targetMethod = targetMethod;
if (arguments != null && arguments.length != 0) {
this.arguments = Arrays.copyOf(arguments, arguments.length);
}
}public Object[] getArguments() {
return arguments;
}
public Object getTargetBean() {
return targetBean;
}
public Method getTargetMethod() {
return targetMethod;
}
public Object getKey() {
return key;
}
/**
* 必须重写equals和hashCode方法,否则放到set集合里没法去重
* @param o
* @return
*/
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
CachedInvocation that = (CachedInvocation) o;
return key.equals(that.key);
}
@Override
public int hashCode() {
return key.hashCode();
}
}
维护缓存方法信息的容器和刷新缓存的类CacheSupportImpl 关键代码:
private final String SEPARATOR = "#";
/**
* 记录缓存执行方法信息的容器。
* 如果有很多无用的缓存数据的话,有可能会照成内存溢出。
*/
private Map<String, Set<CachedInvocation>> cacheToInvocationsMap = new
ConcurrentHashMap<>();
@Autowired
private CacheManager cacheManager;
// 刷新缓存
private void refreshCache(CachedInvocation invocation, String cacheName) {boolean invocationSuccess;
Object computed = null;
try {
// 通过代理调用方法,并记录返回值
computed = invoke(invocation);
invocationSuccess = true;
} catch (Exception ex) {
invocationSuccess = false;
}
if (invocationSuccess) {
if (!CollectionUtils.isEmpty(cacheToInvocationsMap.get(cacheName))) {
// 通过cacheManager获取操作缓存的cache对象
Cache cache = cacheManager.getCache(cacheName);
// 通过Cache对象更新缓存
cache.put(invocation.getKey(), computed);
}
}
}
private Object invoke(CachedInvocation invocation)
throws ClassNotFoundException, NoSuchMethodException,
InvocationTargetException, IllegalAccessException {
final MethodInvoker invoker = new MethodInvoker();
invoker.setTargetObject(invocation.getTargetBean());
invoker.setArguments(invocation.getArguments());
invoker.setTargetMethod(invocation.getTargetMethod().getName());
invoker.prepare();
return invoker.invoke();
}
// 注册缓存方法的执行类信息
@Override
public void registerInvocation(Object targetBean, Method targetMethod, Object[]
arguments,
Set<String> annotatedCacheNames, String cacheKey) {
// 获取注解上真实的value值
Collection<String> cacheNames = generateValue(annotatedCacheNames);
// 获取注解上的key属性值
Class<?> targetClass = getTargetClass(targetBean);
Collection<? extends Cache> caches = getCache(cacheNames);
Object key = generateKey(caches, cacheKey, targetMethod, arguments,
targetBean, targetClass,
CacheOperationExpressionEvaluator.NO_RESULT);
// 新建一个代理对象(记录了缓存注解的方法类信息)
final CachedInvocation invocation = new CachedInvocation(key, targetBean,
targetMethod, arguments);
for (final String cacheName : cacheNames) {
if (!cacheToInvocationsMap.containsKey(cacheName)) {
cacheToInvocationsMap.put(cacheName, new CopyOnWriteArraySet<>());
}
cacheToInvocationsMap.get(cacheName).add(invocation);
}}
@Override
public void refreshCache(String cacheName) {
this.refreshCacheByKey(cacheName, null);
}
// 刷新特定key缓存
@Override
public void refreshCacheByKey(String cacheName, String cacheKey) {
// 如果根据缓存名称没有找到代理信息类的set集合就不执行刷新操作。
// 只有等缓存有效时间过了,再走到切面哪里然后把代理方法信息注册到这里来。
if (!CollectionUtils.isEmpty(cacheToInvocationsMap.get(cacheName))) {
for (final CachedInvocation invocation :
cacheToInvocationsMap.get(cacheName)) {
if (!StringUtils.isBlank(cacheKey) &&
invocation.getKey().toString().equals(cacheKey)) {
logger.info("缓存:{}-{},重新加载数据", cacheName,
cacheKey.getBytes());
refreshCache(invocation, cacheName);
}
}
}
}
现在刷新缓存和注册缓存执行方法的信息都有了,我们怎么来把这个执行方法信息注册到容器里面呢?
这里还少了触发点。
所以我们还需要一个切面,当执行@Cacheable注解获取缓存信息的时候我们还需要注册执行方法的信
息,所以我们写了一个切面:
/**
* 缓存拦截,用于注册方法信息
* @author yuhao.wang
*/
@Aspect
@Component
public class CachingAnnotationsAspect {
private static final Logger logger =
LoggerFactory.getLogger(CachingAnnotationsAspect.class);
@Autowired
private InvocationRegistry cacheRefreshSupport;
private <T extends Annotation> List<T> getMethodAnnotations(AnnotatedElement
ae, Class<T> annotationType) {
List<T> anns = new ArrayList<T>(2);
// look for raw annotation
T ann = ae.getAnnotation(annotationType);
if (ann != null) {
anns.add(ann);
}
// look for meta-annotations
for (Annotation metaAnn : ae.getAnnotations()) {ann = metaAnn.annotationType().getAnnotation(annotationType);
if (ann != null) {
anns.add(ann);
}
}
return (anns.isEmpty() ? null : anns);
}
private Method getSpecificmethod(ProceedingJoinPoint pjp) {
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
Method method = methodSignature.getMethod();
// The method may be on an interface, but we need attributes from the
// target class. If the target class is null, the method will be
// unchanged.
Class<?> targetClass =
AopProxyUtils.ultimateTargetClass(pjp.getTarget());
if (targetClass == null && pjp.getTarget() != null) {
targetClass = pjp.getTarget().getClass();
}
Method specificMethod = ClassUtils.getMostSpecificMethod(method,
targetClass);
// If we are dealing with method with generic parameters, find the
// original method.
specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
return specificMethod;
}
@Pointcut("@annotation(org.springframework.cache.annotation.Cacheable)")
public void pointcut() {
}
@Around("pointcut()")
public Object registerInvocation(ProceedingJoinPoint joinPoint) throws
Throwable {
Method method = this.getSpecificmethod(joinPoint);
List<Cacheable> annotations = this.getMethodAnnotations(method,
Cacheable.class);
Set<String> cacheSet = new HashSet<String>();
String cacheKey = null;
for (Cacheable cacheables : annotations) {
cacheSet.addAll(Arrays.asList(cacheables.value()));
cacheKey = cacheables.key();
}
cacheRefreshSupport.registerInvocation(joinPoint.getTarget(), method,
joinPoint.getArgs(), cacheSet, cacheKey);
return joinPoint.proceed();
}
}
注意:一个缓存名称(@Cacheable的value属性),也只能配置一个过期时间,如果配置多个以第一
次配置的为准。至此我们就把完整的设置过期时间和刷新缓存都实现了,当然还可能存在一定问题,希望大家多多指
教。
使用这种方式有个不好的地方,我们破坏了Spring Cache的结构,导致我们切换Cache的方式的时候要
改代码,有很大的依赖性。
下一篇我将对 redisCacheManager.setExpires()方法进行扩展来实现过期时间和自动刷新,进而不会去 文章来源:https://www.toymoban.com/news/detail-460286.html
破坏Spring Cache的原有结构,切换缓存就不会有问题了。 文章来源地址https://www.toymoban.com/news/detail-460286.html
到了这里,关于springboot的一些延伸问题的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!