「一个优秀的开发者,一定是会利用各种工具来提升自己的开发效率。」 前段时间,博主在Gitee/Github开源了一个提升开发效率的工具,工具内集成了各种常用工具如csv、Excel、ftp、文件系统等等,「只需要简单调用API,就可以得到想要的结果,可以极大帮助开发者提升效率」,下面来一起看看这款工具如何使用吧。
报表的导出、导入功能、文件上传、下载等在平常业务中是最常见不过的功能了,「许多小伙伴在开发的时候才会去网上找之前别人编写过的案例参考,但是许多博客记载的都是时间比较长远或者不完整的代码,这导致在引入的时候还要处理引入的许多未知问题。」
现在博主开源的“轮子之王”包含了这些常见的功能,「源码全开发,每种功能都有相应的例子说明,项目会持续更新迭代,有问题还可以及时给项目提issue,相信比大多数网上的博客代码可靠性更高。」
「项目地址如下:」
Github:https://github.com/it-learning-diary/it-wheels-king
Gitee:https://gitee.com/it-learning-diary/it-wheels-king
「项目结构如下:」
该工具实现采用的是开源的easyexcel框架。easyexcel是阿里的开发人员对poi框架进行了优化,解决了poi在大数据量时可能出现OOM异常,并且兼容xls和xlsx两种文件类型的一个开源框架。
「excel工具集成功能如下:」
「excel工具的特点如下:」
使用过easyexcel框架的一些读者知道,每个导入功能都要写一个对应的Listener进行数据转换,在很多时间其实转换的逻辑都是类似的,不同的只不过是转换后数据处理的业务逻辑不一样。
「本开源项目的excel工具则利用JAVA中的泛型和Java8中的Consumer接口将相同的部分(转换逻辑)抽取出来,不同的部分则单独传入(数据处理的业务逻辑),这样就避免了每个导入都需要创建一个相类似的Listerner,减少了类的创建和提高了开发效率。」
「部分源码如下:」
/** * 通用导入excel文件方法 * * @param fileStream 导入的文件流 * @param rowDto 接收excel每行数据的实体 * @param rowAction 将接收到的实体进行自定义的业务处理逻辑方法 * @param <T> 实体类型 */ public static <T> void importFile(InputStream fileStream, T rowDto, ThrowingConsumer<List<T>> rowAction) { // 获取excel通用监听器 ExcelImportCommonListener<T> commonListener = new ExcelImportCommonListener<>(rowAction); // 读取excel文件并导入 EasyExcel.read(fileStream, rowDto.getClass(), commonListener).sheet().doRead(); } /** * excel文件导出(可以包含多个sheet页),固定表头(通过实体指定属性的方式) * @param response * @param fileName 导出文件名 * @param head 导出表头(多个sheet页就是多个集合元素) * @param exportData 需要导出数据 * @param sheetNames sheet页的名称,为空则默认以:sheet + 数字规则命名 */ public static <T> void exportFile(String fileName, List<T> head, List<List<T>> exportData, List<String> sheetNames, HttpServletResponse response) { if (Objects.isNull(response) || StrUtil.isBlank(fileName) || CollUtil.isEmpty(head)) { log.info("ExcelExportUtil exportFile required param can't be empty"); return; } ExcelWriter writer = null; try { response.setContentType(ExportConstant.EXCEL_CONTENT_TYPE); response.setCharacterEncoding(ExportConstant.UTF_8); response.setHeader(ExportConstant.CONTENT_DISPOSITION, ExportConstant.ATTACHMENT_FILENAME + fileName + ExportConstant.XLSX_SUFFIX); // 设置导出的表格样式 HorizontalCellStyleStrategy horizontalCellStyleStrategy = getExportDefaultStyle(); writer = EasyExcel.write(response.getOutputStream()).registerWriteHandler(horizontalCellStyleStrategy).build(); for (int itemIndex = 0; itemIndex < exportData.size(); itemIndex++) { // 表头数据 Object headData = head.get(itemIndex); // sheet页的数据 List<T> list = exportData.get(itemIndex); WriteSheet sheet = EasyExcel.writerSheet(itemIndex, CollUtil.isEmpty(sheetNames) ? ExportConstant.SHEET_NAME + itemIndex + 1 : sheetNames.get(itemIndex)).head(headData.getClass()).build(); writer.write(list, sheet); } } catch (Exception e) { log.error("ExcelExportUtil exportFile in error:{}", e); } finally { if (null != writer) { writer.finish(); } } }
「使用案例如下(在工具中每个项目都有具体的案例,不懂的还可以留言跟博主沟通):」
/** * 导入用户数据案例 * * @param file */ @Transactional(rollbackFor = Exception.class) public void uploadUserListDemoWithExcel(MultipartFile file, String username) throws Exception { // 此处先校验导入的文件类型是否为excel String type = FileTypeUtil.getType(file.getInputStream()); if (StrUtil.isBlank(type) || type.contains(ImportConstant.XLS_TYPE) || type.contains(ImportConstant.XLSX_TYPE)) { // 返回校验失败信息 return; } User user = new User(); user.setId(100); user.setName("外层"); user.setPassword("外层"); userService.save(user); // 调用统一导入方法 ExcelImportUtil.importFile(file.getInputStream(), new UserDto(), UserServiceImpl::saveUserList); } /** * 导出案例 * * @param response */ public void exportUserListDemoWithExcel(HttpServletResponse response) { // 表头(使用excel中的注解定义,如果表头不固定,请使用ExcelExportUtil.exportWithDynamicData进行导出) List<UserExportVo> head = Stream.of(new UserExportVo()).collect(Collectors.toList()); // 数据(使用两层list为了兼容多个sheet页,如果是不同的sheet页则放在不同的List集合中) List<List<UserExportVo>> exportDataList = new ArrayList<>(); List<UserExportVo> exportItem = new ArrayList<>(); // 查询数据 List<User> dbData = userService.list(); // 将数据转换成导出需要的实际数据格式,此处只是演示 for (User user : dbData) { UserExportVo vo = new UserExportVo(); BeanUtil.copyProperties(user, vo); exportItem.add(vo); } exportDataList.add(exportItem); // sheet页名称-自定义,如果没有则传空 List<String> sheetNameList = Stream.of("sheet1").collect(Collectors.toList()); ExcelExportUtil.exportFile("user", head, exportDataList, sheetNameList, response); }
Csv即逗号分隔值,也可以称为字符分隔符,「与excel等文件相比,excel文件中会包含许多格式信息,占用的空间会更大,所以Csv在很多大数据场景导出、导入场景是非常常见的。该工具实现采用的是开源的univocity-parsers框架实现。」
之前有一篇专门讲解轮子之王项目为何使用univocity-parsers框架集成csv的详细过程,有兴趣的读者可以点击链接查看:集成csv工具的前因后果
「部分源码如下:」
/** * 使用实体bean接收csv数据文件并进行数据落盘 * * @param inputStream * @param errorList * @param rowDtoClass * @param rowAction * @param <T> */ public static <T> void importCsvWithBean(InputStream inputStream, List<String> errorList, Class rowDtoClass, ThrowingConsumer<List<T>> rowAction) { // 定义bean解析者:用于将csv中数据绑定到实体属性中,然后存储带list集合上 BeanListProcessor<T> rowProcessor = new BeanListProcessor<>(rowDtoClass); CsvParserSettings setting = getDefaultSetting(errorList); setting.setProcessor(rowProcessor); // 创建csv文件解析 CsvParser csvParser = new CsvParser(setting); csvParser.parse(inputStream); // 获取数据映射后的集合 List<T> dataList = rowProcessor.getBeans(); // 校验必填字段 for (T row : dataList) { // 校验导入字段 ImportValid.validRequireField(row, errorList); } // 执行数据持久化 persistentBeanDataToDb(dataList, rowAction); } /** * 导出csv文件(表头和行都以实体的方式) * * @param response * @param head * @param rowDataList */ public static <T> void exportCsvWithBean(HttpServletResponse response, String fileName, T head, List<T> rowDataList) { CsvWriter writer = null; try { // 设置响应头格式 response.setContentType(ExportConstant.EXCEL_CONTENT_TYPE); response.setCharacterEncoding(ExportConstant.UTF_8); response.setHeader(ExportConstant.CONTENT_DISPOSITION, ExportConstant.ATTACHMENT_FILENAME + fileName + ExportConstant.CSV_SUFFIX); // 设置导出格式 CsvWriterSettings setting = getDefaultWriteSetting(); // 创见bean处理器,用于处理写入数据 BeanWriterProcessor<?> beanWriter = new BeanWriterProcessor<>(head.getClass()); setting.setRowWriterProcessor(beanWriter); // 导出数据 writer = new CsvWriter(response.getOutputStream(), setting); writer.processRecords(rowDataList); writer.flush(); } catch (Exception e) { log.error("CsvExportUtil exportCsvWithBean in error:{}", e); } finally { if (Objects.nonNull(writer)) { writer.close(); } } }
「使用案例如下:」
/** * 导出案例 * * @param response */ public void exportUserListWithCsv(HttpServletResponse response) { List<UserExportCsvVo> exportItem = new ArrayList<>(); // 查询数据 List<User> dbData = userService.list(); // 使用字符串数组方式作为表头导出csv数据 List<Object> head = Stream.of("id", "name", "password").collect(Collectors.toList()); List<List<Object>> dataList = new ArrayList<>(); for (User user : dbData) { List<Object> row = new ArrayList<>(); row.add(user.getId()); row.add(user.getName()); row.add(user.getPassword()); dataList.add(row); } CsvExportUtil.exportCsvWithString(response, "demo", head, dataList); } /** * 导入用户数据案例(csv模式) * * @param file */@Transactional(rollbackFor = Exception.class) public void uploadUserListWithCsv(MultipartFile file) throws Exception { // 此处先校验导入的文件类型是否为csv String type = FileTypeUtil.getType(file.getInputStream()); if (StrUtil.isBlank(type) || type.contains(ImportConstant.CSV_TYPE)) { // 返回校验失败信息 return; } User user = new User(); user.setId(100); user.setName("外层"); user.setPassword("外层"); userService.save(user); List<String> errorLogList = new ArrayList<>(); // 调用统一导入方法 // 方式一:使用csv数据映射到dto实体的方式进行数据导入 //CsvImportUtil.importCsvWithBean(file.getInputStream(), errorLogList, UserCsvDto.class, UserServiceImpl::saveUserListWithCsv); // 方式二、使用csv数据映射到字符串数组的方式进行数据导入 CsvImportUtil.importCsvWithString(file.getInputStream(), errorLogList, UserCsvDto.class, UserServiceImpl::saveUserListWithCsvStringArrDemo); // 如果存在解析异常,输出解析异常并进行事务回滚 if (CollUtil.isNotEmpty(errorLogList)) { throw new RuntimeException(StrUtil.toString(errorLogList)); } }
Ftp文件上传下载相比excel、csv等出现的场景较少,「但是,如果你参与的项目是政府或者涉及到第三方旧系统对接的时候,很多时候就需要使用到它。因为很多旧系统或者政府项目使用的技术比较旧或者有制度限制,一般都是以文件的形式与你进行交互,此时ftp工具就很有效了。」
Ftp工具使用的commons.NET开源框架进行实现,具体的集成流程之前单独使用一篇文章进行了非常详细的介绍,有需要的读者可以点击后面链接查看:手把手教你搭建ftp服务器,并用程序完成ftp上传下载功能
「部分源码如下:」
/** * 上传 * * @return */ public boolean upload(FtpUploadParam param) { boolean flag = false; FTPClient ftpClient = new FTPClient(); //1 测试连接 if (connect(ftpClient, param.getHostname(), param.getPort(), param.getUsername(), param.getPassword())) { try { //2 检查工作目录是否存在,不存在则创建 if (!ftpClient.changeWorkingDirectory(param.getWorkingPath())) { ftpClient.makeDirectory(param.getWorkingPath()); } // 将文件编码成Ftp服务器支持的编码类型(FTP协议里面,规定文件名编码为iso-8859-1,所以目录名或文件名需要转码。) String fileName = new String(param.getSaveName().getBytes(ftpClientCharset), ftpServerCharset); // 3 上传文件 if (ftpClient.storeFile(fileName, param.getInputStream())) { flag = true; } else { log.warn("FtpUtils uploadFile unsuccessfully!!"); } } catch (IOException e) { log.error("FtpUtils upload in error:{}", e); } finally { disconnect(ftpClient); } } return flag; }/** * @description: 下载ftp文件 * @param: * @param: param * @param: downloadFileName * @return: * @date: 2022/7/14 10:56 */ public boolean download(FtpDownloadParam param, String downloadFileName) { FTPClient ftpClient = new FTPClient(); FileOutputStream out = null; boolean downloadResult = false; //1 测试连接 if (connect(ftpClient, param.getHostname(), param.getPort(), param.getUsername(), param.getPassword())) { try { String localPath = param.getDownloadPath() + param.getFileName(); out = new FileOutputStream(new File(localPath)); //2 检查工作目录是否存在,不存在返回 // if (!ftpClient.changeWorkingDirectory(param.getWorkingPath())) { // return false; // } /* * 打开FTP服务器的PASS模式(不记得FTP协议支持的模式请翻到文章第一阶段) * 这个方法的意思就是每次数据连接之前,ftp client告诉ftp server开通一个端口来传输数据. 因为ftp * server可能每次开启不同的端口来传输数据,但是在linux上,由于安全限制,可能某些端口没有开启,可能出现出现阻塞 */ ftpClient.enterLocalPassiveMode(); // 设置文件的传输方式-二进制 ftpClient.setFileType(FTP.BINARY_FILE_TYPE); // 将文件编码成Ftp服务器支持的编码类型(FTP协议里面,规定文件名编码为iso-8859-1,所以目录名或文件名需要转码。) // 缺少编码转换会导致:从FTP服务器下载下来的文件是破损的,无法被打开 downloadResult = ftpClient.retrieveFile(new String(downloadFileName .getBytes(ftpClientCharset), ftpServerCharset), out); out.flush(); } catch (IOException e) { log.error("FtpUtils upload in error:{}", e); return false; } finally { try { if (Objects.nonNull(out)) { out.close(); } } catch (Exception e) { log.error("FtpUtils upload in error:{}", e); } disconnect(ftpClient); } } return downloadResult; }
「具体使用案例如下:」
@PostMApping("/ftp/upload") public void upload() { try { FtpUploadParam param = new FtpUploadParam(); param.setHostname(ftpConfig.getServerHostname()); param.setPort(ftpConfig.getServerPort()); param.setUsername(ftpConfig.getServerUsername()); param.setPassword(ftpConfig.getServerPassword()); param.setWorkingPath(ftpConfig.getServerWorkingPath()); param.setSaveName("xxx.mp3"); InputStream in = new FileInputStream(new File("D:/uploadfile/like.mp3")); param.setInputStream(in); ftpUtils.upload(param); } catch (Exception e) { log.error("TestFtpServerController upload 错误:{}", e); } } @PostMapping("/ftp/download") public void download() { try { FtpDownloadParam param = new FtpDownloadParam(); param.setHostname(ftpConfig.getServerHostname()); param.setPort(ftpConfig.getServerPort()); param.setUsername(ftpConfig.getServerUsername()); param.setPassword(ftpConfig.getServerPassword()); param.setWorkingPath(ftpConfig.getServerWorkingPath()); param.setDownloadPath("D:/downloadFile/"); param.setFileName("xxx.mp3"); ftpUtils.download(param, "xxxx.mp3"); } catch (Exception e) { log.error("TestFtpServerController download 错误:{}", e); } }
「非结构化数据通常是使用文件的方式进行存储,这时候不可避免地要使用到文件系统进行管理。」 分布式文件系统工具使用了第三方开源框架seaweedfs进行搭建,可以实现程序上传,删除、下载、查询,并有文件分布式存储,避免单点故障,节约成本等特点。
前面也专门通过一篇文章讲解了:为何要使用seaweedfs框架搭建分布式文件系统的,感兴趣的读者可以通过下方链接进行查看:Gitee图床崩溃后,我使用Seaweedfs搭建了文件系统并封装成轮子开源
「部分源码如下:」
/** * @description: 上传单个文件到文件服务器 * @param: file * @return: 文件的fid + 文件的请全访问地址 * @author: it */ public String uploadFile(MultipartFile file) throws Exception { FileSource fileSource = getFileSource(); FileTemplate fileTemplate = new FileTemplate(fileSource.getConnection()); // 上传文件 FileHandleStatus handleStatus = fileTemplate.saveFileByStream(file.getOriginalFilename(), file.getInputStream(), contentType); // 获取上传文件的访问地址 String fileUrl = fileTemplate.getFileUrl(handleStatus.getFileId()); // 关闭当前连接 fileSource.shutdown(); return handleStatus.getFileId() + StrUtil.DASHED + fileUrl; }/** * @description: 根据文件下载文件 * @param: fid * @param: response * @param: fileName * @author: it */ public void downloadFileByFid(HttpServletResponse response, HttpServletRequest request, String fid, String fileName) throws Exception { FileSource fileSource = getFileSource(); FileTemplate fileTemplate = new FileTemplate(fileSource.getConnection()); StreamResponse fileStream = fileTemplate.getFileStream(fid); // 设置响应头 response.setContentType(CommonConstant.CONTENT_TYPE); response.setCharacterEncoding(CommonConstant.UTF_8); String encodeFileName = buildingFileNameAdapterBrowser(request, fileName); response.setHeader(CommonConstant.CONTENT_DISPOSITION, CommonConstant.ATTACHMENT_FILENAME + encodeFileName); // 读取并写入到响应输出 InputStream inputStream = fileStream.getInputStream(); byte[] fileByte = new byte[inputStream.available()]; inputStream.read(fileByte); response.getOutputStream().write(fileByte); response.getOutputStream().flush(); fileSource.shutdown(); }
「具体使用案例如下:」
/** * @description: 上传文件 * @param: * @param: file * @return: * @author: it * @date: 2022/7/14 17:01 */ @ResponseBody @RequestMapping("upload") public void uploadFile(MultipartFile file) { try { String fileUrl = seaweedFsUtil.uploadFile(file); System.out.println(fileUrl); } catch (Exception e) { log.error("TestSeaweedFsController uploadFile in error:{}", e); } } /** * @description: 下载文件 * @param: * @param: fileId * @return: * @author: it * @date: 2022/7/14 17:01 */ @RequestMapping("download") public void downloadFile(HttpServletResponse response, HttpServletRequest request, String fileId, String fileName) { try { seaweedFsUtil.downloadFileByFid(response, request, fileId, fileName); } catch (Exception e) { log.error("TestSeaweedFsController downloadFile in error:{}", e); } }
到此为止,轮子之王已集成的工具就介绍完毕了,后续还会不断更新、集成新的轮子,下面给大家介绍一下下一段时间项目的一些工作(「如果读者有想要集成的轮子,欢迎提issue或者这文章下面留言」):
开源之路不容易,开源之心不忘记!「如果博主开源的项目对您有所帮助,请给项目star,给博主更多动力,如果阅读文章给您有所帮助,请给博主点赞、关注。」
该开源项目会持续更新和维护,希望有更多读者能够提出建议和想法,