背景

公司项目有个需求, 前端上传excel文件, 后端读取数据、处理数据、返回错误数据, 最简单的方式同步处理, 客户端上传文件后一直阻塞等待响应, 但用户体验无疑很差, 处理数据可能十分耗时, 没人愿意傻等, 由于项目暂未使用ActiveMQ等消息队列中间件, 而redis的lpush和rpop很适合作为一种轻量级的消息队列实现, 所以用它完成此次功能开发

一、本文涉及知识点

  1. excel文件读写--阿里easyexcel sdk
  2. 文件上传、下载--腾讯云对象存储
  3. 远程服务调用--restTemplate
  4. 生产者、消费者--redisTemplate leftPush和rightPop操作
  5. 异步处理数据--Executors线程池
  6. 读取网络文件流--HttpClient
  7. 自定义注解实现用户身份认证--JWT token认证, 拦截器拦截标注有@LoginRequired注解的请求入口

当然, Java实现咯

涉及的知识点比较多, 每一个知识点都可以作为专题进行学习分析, 本文将完整实现呈现出来, 后期拆分与小伙伴分享学习

二、项目目录结构

说明: 数据库DAO层放到另一个模块了, 不是本文重点

三、主要maven依赖

  1. easyexcel
<easyexcel-latestVersion>1.1.2-beta4</easyexcel-latestVersion>

		<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>${easyexcel-latestVersion}</version>
</dependency>
  1. JWT
		<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
  1. redis
		<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
<version>1.3.5.RELEASE</version>
</dependency>
  1. 腾讯cos
		<dependency>
<groupId>com.qcloud</groupId>
<artifactId>cos_api</artifactId>
<version>5.4.5</version>
</dependency>

四、流程

  1. 用户上传文件
  2. 将文件存储到腾讯cos
  3. 将上传后的文件id及上传记录保存到数据库
  4. redis生产一条导入消息, 即保存文件id到redis
  5. 请求结束, 返回"处理中"状态
  6. redis消费消息
  7. 读取cos文件, 异步处理数据
  8. 将错误数据以excel形式上传至cos, 以供用户下载, 并更新处理状态为"处理完成"
  9. 客户端轮询查询处理状态, 并可以下载错误文件
  10. 结束

五、实现效果

  1. 上传文件

  2. 数据库导入记录

  3. 导入的数据

  4. 下载错误文件

  5. 错误数据提示

  6. 查询导入记录

六、代码实现

1、导入excel控制层

    @LoginRequired
@RequestMapping(value = "doImport", method = RequestMethod.POST)
public JsonResponse doImport(@RequestParam("file") MultipartFile file, HttpServletRequest request) {
PLUser user = getUser(request);
return orderImportService.doImport(file, user.getId());
}

2、service层

    @Override
public JsonResponse doImport(MultipartFile file, Integer userId) {
if (null == file || file.isEmpty()) {
throw new ServiceException("文件不能为空");
} String filename = file.getOriginalFilename();
if (!checkFileSuffix(filename)) {
throw new ServiceException("当前仅支持xlsx格式的excel");
} // 存储文件
String fileId = saveToOss(file);
if (StringUtils.isBlank(fileId)) {
throw new ServiceException("文件上传失败, 请稍后重试");
} // 保存记录到数据库
saveRecordToDB(userId, fileId, filename); // 生产一条订单导入消息
redisProducer.produce(RedisKey.orderImportKey, fileId); return JsonResponse.ok("导入成功, 处理中...");
} /**
* 校验文件格式
* @param fileName
* @return
*/
private static boolean checkFileSuffix(String fileName) {
if (StringUtils.isBlank(fileName) || fileName.lastIndexOf(".") <= 0) {
return false;
} int pointIndex = fileName.lastIndexOf(".");
String suffix = fileName.substring(pointIndex, fileName.length()).toLowerCase();
if (".xlsx".equals(suffix)) {
return true;
} return false;
} /**
* 将文件存储到腾讯OSS
* @param file
* @return
*/
private String saveToOss(MultipartFile file) {
InputStream ins = null;
try {
ins = file.getInputStream();
} catch (IOException e) {
e.printStackTrace();
} String fileId;
try {
String originalFilename = file.getOriginalFilename();
File f = new File(originalFilename);
inputStreamToFile(ins, f);
FileSystemResource resource = new FileSystemResource(f); MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
param.add("file", resource); ResponseResult responseResult = restTemplate.postForObject(txOssUploadUrl, param, ResponseResult.class);
fileId = (String) responseResult.getData();
} catch (Exception e) {
fileId = null;
} return fileId;
}

3、redis生产者

@Service
public class RedisProducerImpl implements RedisProducer { @Autowired
private RedisTemplate redisTemplate; @Override
public JsonResponse produce(String key, String msg) {
Map<String, String> map = Maps.newHashMap();
map.put("fileId", msg);
redisTemplate.opsForList().leftPush(key, map);
return JsonResponse.ok();
} }

4、redis消费者

@Service
public class RedisConsumer { @Autowired
public RedisTemplate redisTemplate; @Value("${txOssFileUrl}")
private String txOssFileUrl; @Value("${txOssUploadUrl}")
private String txOssUploadUrl; @PostConstruct
public void init() {
processOrderImport();
} /**
* 处理订单导入
*/
private void processOrderImport() {
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> {
while (true) {
Object object = redisTemplate.opsForList().rightPop(RedisKey.orderImportKey, 1, TimeUnit.SECONDS);
if (null == object) {
continue;
}
String msg = JSON.toJSONString(object);
executorService.execute(new OrderImportTask(msg, txOssFileUrl, txOssUploadUrl));
}
});
} }

5、处理任务线程类

public class OrderImportTask implements Runnable {
public OrderImportTask(String msg, String txOssFileUrl, String txOssUploadUrl) {
this.msg = msg;
this.txOssFileUrl = txOssFileUrl;
this.txOssUploadUrl = txOssUploadUrl;
}
} /**
* 注入bean
*/
private void autowireBean() {
this.restTemplate = BeanContext.getApplicationContext().getBean(RestTemplate.class);
this.transactionTemplate = BeanContext.getApplicationContext().getBean(TransactionTemplate.class);
this.orderImportService = BeanContext.getApplicationContext().getBean(OrderImportService.class);
} @Override
public void run() {
// 注入bean
autowireBean(); JSONObject jsonObject = JSON.parseObject(msg);
String fileId = jsonObject.getString("fileId"); MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
param.add("id", fileId); ResponseResult responseResult = restTemplate.postForObject(txOssFileUrl, param, ResponseResult.class);
String fileUrl = (String) responseResult.getData();
if (StringUtils.isBlank(fileUrl)) {
return;
} InputStream inputStream = HttpClientUtil.readFileFromURL(fileUrl);
List<Object> list = ExcelUtil.read(inputStream);
process(list, fileId);
} /**
* 将文件上传至oss
* @param file
* @return
*/
private String saveToOss(File file) {
String fileId;
try {
FileSystemResource resource = new FileSystemResource(file);
MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
param.add("file", resource); ResponseResult responseResult = restTemplate.postForObject(txOssUploadUrl, param, ResponseResult.class);
fileId = (String) responseResult.getData();
} catch (Exception e) {
fileId = null;
}
return fileId;
}

说明: 处理数据的业务逻辑代码就不用贴了

6、上传文件到cos

    @RequestMapping("/txOssUpload")
@ResponseBody
public ResponseResult txOssUpload(@RequestParam("file") MultipartFile file) throws UnsupportedEncodingException {
if (null == file || file.isEmpty()) {
return ResponseResult.fail("文件不能为空");
} String originalFilename = file.getOriginalFilename();
originalFilename = MimeUtility.decodeText(originalFilename);// 解决中文乱码问题
String contentType = getContentType(originalFilename);
String key; InputStream ins = null;
File f = null; try {
ins = file.getInputStream();
f = new File(originalFilename);
inputStreamToFile(ins, f);
key = iFileStorageClient.txOssUpload(new FileInputStream(f), originalFilename, contentType);
} catch (Exception e) {
return ResponseResult.fail(e.getMessage());
} finally {
if (null != ins) {
try {
ins.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (f.exists()) {// 删除临时文件
f.delete();
}
} return ResponseResult.ok(key);
} public static void inputStreamToFile(InputStream ins,File file) {
try {
OutputStream os = new FileOutputStream(file);
int bytesRead = 0;
byte[] buffer = new byte[8192];
while ((bytesRead = ins.read(buffer, 0, 8192)) != -1) {
os.write(buffer, 0, bytesRead);
}
os.close();
ins.close();
} catch (Exception e) {
e.printStackTrace();
}
} public String txOssUpload(FileInputStream inputStream, String key, String contentType) {
key = Uuid.getUuid() + "-" + key;
OSSUtil.txOssUpload(inputStream, key, contentType);
try {
if (null != inputStream) {
inputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
return key;
} public static void txOssUpload(FileInputStream inputStream, String key, String contentType) {
ObjectMetadata objectMetadata = new ObjectMetadata();
try{
int length = inputStream.available();
objectMetadata.setContentLength(length);
}catch (Exception e){
logger.info(e.getMessage());
}
objectMetadata.setContentType(contentType);
cosclient.putObject(txbucketName, key, inputStream, objectMetadata);
}

7、下载文件

    /**
* 腾讯云文件下载
* @param response
* @param id
* @return
*/
@RequestMapping("/txOssDownload")
public Object txOssDownload(HttpServletResponse response, String id) {
COSObjectInputStream cosObjectInputStream = iFileStorageClient.txOssDownload(id, response);
String contentType = getContentType(id);
FileUtil.txOssDownload(response, contentType, cosObjectInputStream, id);
return null;
} public static void txOssDownload(HttpServletResponse response, String contentType, InputStream fileStream, String fileName) {
FileOutputStream fos = null;
response.reset();
OutputStream os = null;
try {
response.setContentType(contentType + "; charset=utf-8");
if(!contentType.equals(PlConstans.FileContentType.image)){
try {
response.setHeader("Content-Disposition", "attachment; filename=" + new String(fileName.getBytes("UTF-8"), "ISO8859-1"));
} catch (UnsupportedEncodingException e) {
response.setHeader("Content-Disposition", "attachment; filename=" + fileName);
logger.error("encoding file name failed", e);
}
} os = response.getOutputStream(); byte[] b = new byte[1024 * 1024];
int len;
while ((len = fileStream.read(b)) > 0) {
os.write(b, 0, len);
os.flush();
try {
if(fos != null) {
fos.write(b, 0, len);
fos.flush();
}
} catch (Exception e) {
logger.error(e.getMessage());
}
}
} catch (IOException e) {
IOUtils.closeQuietly(fos);
fos = null;
} finally {
IOUtils.closeQuietly(os);
IOUtils.closeQuietly(fileStream);
if(fos != null) {
IOUtils.closeQuietly(fos);
}
}
}

8、读取网络文件流

	/**
* 读取网络文件流
* @param url
* @return
*/
public static InputStream readFileFromURL(String url) {
if (StringUtils.isBlank(url)) {
return null;
} HttpClient httpClient = new DefaultHttpClient();
HttpGet methodGet = new HttpGet(url);
try {
HttpResponse response = httpClient.execute(methodGet);
if (response.getStatusLine().getStatusCode() == 200) {
HttpEntity entity = response.getEntity();
return entity.getContent();
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

9、ExcelUtil

    /**
* 读excel
* @param inputStream 文件输入流
* @return list集合
*/
public static List<Object> read(InputStream inputStream) {
return EasyExcelFactory.read(inputStream, new Sheet(1, 1));
} /**
* 写excel
* @param data list数据
* @param clazz
* @param saveFilePath 文件保存路径
* @throws IOException
*/
public static void write(List<? extends BaseRowModel> data, Class<? extends BaseRowModel> clazz, String saveFilePath) throws IOException {
File tempFile = new File(saveFilePath);
OutputStream out = new FileOutputStream(tempFile);
ExcelWriter writer = EasyExcelFactory.getWriter(out);
Sheet sheet = new Sheet(1, 3, clazz, "Sheet1", null);
writer.write(data, sheet);
writer.finish();
out.close();
}

说明: 至此, 整个流程算是完整了, 下面将其他知识点代码也贴出来参考

七、其他

1、@LoginRequired注解

/**
* 在需要登录验证的Controller的方法上使用此注解
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginRequired {
}

2、MyControllerAdvice

@ControllerAdvice
public class MyControllerAdvice { @ResponseBody
@ExceptionHandler(TokenValidationException.class)
public JsonResponse tokenValidationExceptionHandler() {
return JsonResponse.loginInvalid();
} @ResponseBody
@ExceptionHandler(ServiceException.class)
public JsonResponse serviceExceptionHandler(ServiceException se) {
return JsonResponse.fail(se.getMsg());
} @ResponseBody
@ExceptionHandler(Exception.class)
public JsonResponse exceptionHandler(Exception e) {
e.printStackTrace();
return JsonResponse.fail(e.getMessage());
} }

3、AuthenticationInterceptor

public class AuthenticationInterceptor implements HandlerInterceptor {

    private static final String CURRENT_USER = "user";

    @Autowired
private UserService userService; @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 如果不是映射到方法直接通过
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod(); // 判断接口是否有@LoginRequired注解, 有则需要登录
LoginRequired methodAnnotation = method.getAnnotation(LoginRequired.class);
if (methodAnnotation != null) {
// 验证token
Integer userId = JwtUtil.verifyToken(request);
PLUser plUser = userService.selectByPrimaryKey(userId);
if (null == plUser) {
throw new RuntimeException("用户不存在,请重新登录");
}
request.setAttribute(CURRENT_USER, plUser);
return true;
}
return true;
} @Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
} @Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}

4、JwtUtil

    public static final long EXPIRATION_TIME = 2592_000_000L; // 有效期30天
public static final String SECRET = "pl_token_secret";
public static final String HEADER = "token";
public static final String USER_ID = "userId"; /**
* 根据userId生成token
* @param userId
* @return
*/
public static String generateToken(String userId) {
HashMap<String, Object> map = new HashMap<>();
map.put(USER_ID, userId);
String jwt = Jwts.builder()
.setClaims(map)
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
return jwt;
} /**
* 验证token
* @param request
* @return 验证通过返回userId
*/
public static Integer verifyToken(HttpServletRequest request) {
String token = request.getHeader(HEADER);
if (token != null) {
try {
Map<String, Object> body = Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody(); for (Map.Entry entry : body.entrySet()) {
Object key = entry.getKey();
Object value = entry.getValue();
if (key.toString().equals(USER_ID)) {
return Integer.valueOf(value.toString());// userId
}
}
return null;
} catch (Exception e) {
logger.error(e.getMessage());
throw new TokenValidationException("unauthorized");
}
} else {
throw new TokenValidationException("missing token");
}
}

结语: OK, 搞定,睡了, 好困

redisTemplate实现轻量级消息队列, 异步处理excel并实现腾讯云cos文件上传下载的更多相关文章

  1. 基于Spring Mvc实现的Excel文件上传下载

    最近工作遇到一个需求,需要下载excel模板,编辑后上传解析存储到数据库.因此为了更好的理解公司框架,我就自己先用spring mvc实现了一个样例. 基础框架 之前曾经介绍过一个最简单的spring ...

  2. [SAP ABAP开发技术总结]客户端文本文件、Excel文件上传下载

    声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将 ...

  3. 文件一键上传、汉字转拼音、excel文件上传下载功能模块的实现

    ----------------------------------------------------------------------------------------------[版权申明: ...

  4. SpringMVC异步文件上传下载

    首先了解一下File的构造方法: File(String pathname):根据一个路径得到File对象 File(String parent,String child):根据一个目录和一个子文件/ ...

  5. ABAP EXCEL 文件上传下载 用SMW0

    T-CODE: SMW0 在这里只介绍二进制数据,HTML模板的上传也一样. 另外也可以用CBO TABLE管理文件 可以看我另一个博文:CBO TABLE管理文件上传下载 选择 二进制 写包名: 进 ...

  6. 文件上传下下载(不包含断点续传) Excel,Word导入导出基础

    1.文件上传下载(MVC应用) 视图:form表单,编码方式为multipart/form-data <body> <div> <form action="/D ...

  7. Java Servlet异步处理、非阻塞I/O和文件上传

    异步处理 应用服务器中的 web容器通常对各个客户端情求分别使用一个服务器线程.在工作负载很繁重的情况下,容器常要大量线程来为所有客户端请求服务.可扩展性限制包括内存用尽,或容器线程池耗尽.为了创建可 ...

  8. HTML5——摒弃插件和前端框架的异步文件上传

    之前我从来没有体会到HTML5的便利,直到这次需要一个异步上传的功能功能.一开始我以为文件的一些声明必须为HTML5才管用,后来才知道添加了很多以前没有的标签,并可以直接播放视频,音频等.可以不再使用 ...

  9. 异步文件上传组件 Uploader

    Uploader是非常强大的异步文件上传组件,支持ajax.iframe.flash三套方案,实现浏览器的全兼容,调用非常简单,内置多套主题支持 和常用插件,比如验证.图片预览.进度条等,广泛应用于淘 ...

随机推荐

  1. 实战--利用Lloyd算法进行酵母基因表达数据的聚类分析

    背景:酵母会在一定的时期发生diauxic shift,有一些基因的表达上升,有一些基因表达被抑制,通过聚类算法,将基因表达的变化模式聚成6类. ORF Name R1.Ratio R2.Ratio ...

  2. _ZSkill_快捷键_Xcode快捷键

    Xcode 快捷键使用 Command 用来导航,控制导航区域 Alt 控制右边的部分. 如Assistant Editor ,utility editor. Control 编辑区域上的jump b ...

  3. LED点阵书写显示

    LED点阵书写显示屏   摘要:随着时代的发展,数字电子技术已经普及到我们生活,工作,科研,各个领域,而LED显示以其组构方式灵活.显示稳定.功耗低.寿命长.技术成熟.成本低廉等特点在车站.证券所.运 ...

  4. 把EXE可执行文件等作为资源包含在Delphi编译文件中

    摘自我自己过去写的一段心得. 1.编辑资源文件 *.RCWave: 资源文件是声音文件:RCDATA: 二进制数据AVI: AVI动画:ICON: 图标文件:BITMAP: 位图文件:CURSOR: ...

  5. What makes for effective detection proposals? 论文解析

    1 介绍(INTRODUCTION) 本文主要对最近的 proposal 检测方法做一个总结和评价.主要是下面这些方法.  2 Detection Proposal 方法(DETECTION PROP ...

  6. 用NginX+keepalived实现高可用的负载均衡

    Table of Contents 1 规划和准备 2 安装 3 配置 3.1 配置NginX 3.2 配置keepalived 3.3 让keepalived监控NginX的状态 4 还可以做什么 ...

  7. JS学习笔记7_表单脚本

    1.获取表单及表单元素引用的方式 var mForm = document.forms[formName];获取表单引用 mForm.elements[elemName]获取表单元素,如有同名的,则得 ...

  8. SQL Server主要系统视图说明

    SELECT * FROM sys.all_columns --显示属于用户定义对象和系统对象的所有列的联合--https://docs.microsoft.com/zh-cn/sql/relatio ...

  9. .netcore-FreeSql的使用-搭建context

    之前用netcore搭建了一个小项目,数据库操作用的是要手写sql语句的connection和command,一直想调个EFCore或者类似SOA那样的框架 今天看到了DotNet公众号提到的.NET ...

  10. .NET Entity Framework (with Oracle ODP.NET) -Code First

    上一篇文章介绍了.NET Entity Framework ,并演示了Model First模式,本文将继续讨论 Code First 模式的实现. 一.摘要 1.目标 本文验证了通过Oracle D ...