一. 问题背景
为了提升用户体验,开发中有很多场景需要用到进度条,比如导入、导出、大规模更新操作等。进度条在许多大型系统中使用频率较高,反复编写既麻烦又不利于维护,因此基于Redis抽成公共方法供不同功能调用。
二. 实现方案
1.引入依赖
如果系统已集成Redis,直接跳到第5步,进度条实现。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.配置数据源
# redis 配置
redis:
# 地址
host: 127.0.0.1
# 端口,默认为6379
port: 6379
# 数据库索引
database: 0
# 密码
password: 123456
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
3.配置类
package com.cms.framework.config;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
/**
* redis配置
*
* @author cms
*/
@Configuration
@EnableCaching
public class RedisConfig
{
@Bean
@SuppressWarnings(value = { "unchecked", "rawtypes" })
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
{
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
serializer.setObjectMapper(mapper);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(serializer);
// Hash的key也采用StringRedisSerializer的序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
4.Redis工具类
package com.cms.common.core.redis;
import java.util.Collection;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
/**
* spring redis 工具类
*
* @author daixin
**/
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
@Autowired
public RedisTemplate redisTemplate;
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
*/
public <T> void setCacheObject(final String key, final T value)
{
redisTemplate.opsForValue().set(key, value);
}
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
*/
public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit)
{
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout)
{
return expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @param unit 时间单位
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout, final TimeUnit unit)
{
return redisTemplate.expire(key, timeout, unit);
}
/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public <T> T getCacheObject(final String key)
{
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
}
/**
* 删除单个对象
*
* @param key
*/
public boolean deleteObject(final String key)
{
return redisTemplate.delete(key);
}
/**
* 删除集合对象
*
* @param collection 多个对象
* @return
*/
public long deleteObject(final Collection collection)
{
return redisTemplate.delete(collection);
}
}
5.进度条接口
package com.cms.service.dataCenter;
import java.util.Map;
/**
* @author daixin
* @version 1.0
* @description: TODO
* @date 2024/7/26 11:43
*/
public interface IProcessService {
/**
* 初始化进度
* @param totalKey
* @param addingKey
*/
void initProcess(String totalKey,String addingKey);
/**
* 加进度
* @param addingKey
* @param size
*/
void addSize(String addingKey,int size);
/**
* 设置总进度
* @param totalKey
* @param size
*/
void setTotalSize(String totalKey,int size);
/**
* 设置最终进度
* @param addingKey
* @param total
*/
void setFinalSize(String addingKey,int total);
/**
* 获取进度
* @param totalKey
* @param addingKey
* @return
*/
Map<String, Object> getProcess(String totalKey, String addingKey);
}
6.进度条实现类
package com.cms.service.impl.dataCenter;
import com.cms.common.core.redis.RedisCache;
import com.cms.service.dataCenter.IProcessService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.text.NumberFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @author daixin
* @version 1.0
* @description: TODO
* @date 2024/7/26 11:46
*/
@Service
public class ProcessServiceImpl implements IProcessService {
@Autowired
RedisCache redisCache;
public void initProcess(String totalKey,String addingKey) {
redisCache.setCacheObject(totalKey, 0, 30, TimeUnit.MINUTES);
redisCache.setCacheObject(addingKey, 0, 30, TimeUnit.MINUTES);
}
public void addSize(String addingKey,int size) {
int addingSize = redisCache.getCacheObject(addingKey);
redisCache.setCacheObject(addingKey, addingSize + size, 30, TimeUnit.MINUTES);
}
public void setTotalSize(String totalKey,int size) {
redisCache.setCacheObject(totalKey, size, 30, TimeUnit.MINUTES);
}
public void setFinalSize(String addingKey,int total){
redisCache.setCacheObject(addingKey,total);
}
public Map<String, Object> getProcess(String totalKey, String addingKey) {
int addingSize = redisCache.getCacheObject(addingKey) == null ? 0 : redisCache.getCacheObject(addingKey);
int totalSize = redisCache.getCacheObject(totalKey) == null ? 0 : redisCache.getCacheObject(totalKey);
double percent = 0.000;
if (totalSize != 0) {
percent = (double) addingSize / totalSize;
}
Map process = new HashMap<>();
NumberFormat formatter = NumberFormat.getPercentInstance();
formatter.setMaximumFractionDigits(2);
process.put("percent", formatter.format(percent));
return process;
}
}
7.管理Redis Key
package com.cms.common.constant;
/**
* @author daixin
* @data 2024/1/25 17:06
*/
public class ProjectRedis {
public static final String IMPORT_PROJECT_TOTLESIEZE_KEY = "import_project_totle";
public static final String IMPORT_PROJECT_ADDSIZEING_KEY = "import_project_addingSize";
public static final String EXPORT_PROJECT_TOTLESIEZE_KEY = "export_project_totle";
public static final String EXPORT_PROJECT_ADDSIZEING_KEY = "export_project_addingSize";
}
8.进度条应用
/**
* 导出项目
* @param projectIds 导出项目的id
* @param processNum 进度条标识,本系统使用Long型时间戳
*/
public void exportProject(Long [] projectIds,String processNum){
final String totalKey = ProjectRedis.EXPORT_PROJECT_TOTLESIEZE_KEY + processNum;
final String addingKey = ProjectRedis.EXPORT_PROJECT_ADDSIZEING_KEY + processNum;
processService.initProcess(totalKey, addingKey);
//计算总操作数,并存入redis,此处以1000为例
int totle = 1000;
processService.setTotalSize(totalKey,totle);
for(Long projectId : projectIds){
//此处省略具体导出逻辑...
//每次循环当前操作数加1
processService.addSize(addingKey,1);
}
//方法结束时,将当前操作数和总操作数记为相等
processService.setFinalSize(addingKey,totle);
}
9.项目导出和进度查询
前端先调用项目导出接口(/exportProject),传入进度条标识(processNum);
再循环调用获取进度接口(/exportProjectProcess),传入相同进度条标识(processNum),
即可获取当前进度进行展示。
@SneakyThrows
@GetMapping("/exportProject")
public void exportProject(@RequestParam Long [] projectId,@RequestParam String processNum, HttpServletResponse response){
response.setContentType("application/zip");
response.setHeader("Content-Disposition", "attachment; filename=\"project.zip\"");
String zipPath = cmsProjectService.exportProject(projectId,processNum);
File zipFile = new File(zipPath); // 假设example.zip已经存在并且包含要下载的内容
FileInputStream fis = new FileInputStream(zipFile);
OutputStream os = response.getOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
os.write(buffer, 0, len); // 将数据从输入流写入输出流
}
os.flush(); // 确保所有数据都被写入输出流
os.close(); // 关闭输出流
fis.close(); // 关闭输入流
}
@GetMapping("/exportProjectProcess")
public AjaxResult exportProjectProcess(@RequestParam String processNum) {
return AjaxResult.success(processService.getProcess(ProjectRedis.EXPORT_PROJECT_TOTLESIEZE_KEY + processNum,ProjectRedis.EXPORT_PROJECT_ADDSIZEING_KEY + processNum));
}
10.不同功能应用进度条时,只需根据实际业务定义不同的Redis Key,按照上述逻辑调用方法,即可直接实现进度条复用。