Spring WebFlux + React搭建后台管理系统(7): 实现excel文件上传下载_zerocopyhttpoutputmessage_泛泛之素的博客-程序员宅基地

技术标签: umi  后台项目  react  

后台管理系统的excel导出功能,以及使用excel进行批量导入都是必不可少的功能,本篇主要介绍内容如下:

  • java后端 excel的读写
  • excel类型判断以及通过反射适配class
  • 后端接收upload服务逻辑实现
  • 后端download服务逻辑
  • 前端upload组建使用
  • 前端download配置

先上效果图:

在这里插入图片描述

1. 读取Excel文件

1.1 添加依赖

通过使用poi进行excel文件的解析:

implementation 'org.apache.poi:poi:4.0.1'
implementation 'org.apache.poi:poi-ooxml:4.0.1'

版本不要太高容易报错,spring使用的基础包版本可能不支持

1.2 生成workbook

  • workbook是excel的基本类,输入一个inputStream,作为数据源
  • 用过workbook生成一个sheet
  • 通过sheet生成row,用过row生成cell
  • 读取就是获取cell中的数据,写入就是将数据写入cell,设置cell的Style等
  • 由于xls和xlsx版本不同,xls只是xml写的文件,而xlsx是一个压缩包,解析模板不同,需要通过不同的解析系统生成workbook
    private static final String XLS = "xls";
    private static final String XLSX = "xlsx";

    public static Workbook getWorkbook(InputStream inputStream, String fileType) throws IOException {
    
        Workbook workbook = null;
        if (fileType.equalsIgnoreCase(XLS)) {
    
            workbook = new HSSFWorkbook(inputStream);
        } else if (fileType.equalsIgnoreCase(XLSX)) {
    
            workbook = new XSSFWorkbook(inputStream);
        }
        return workbook;
    }
  • 生成sheet
Sheet sheet = workbook.getSheetAt(sheetId);

1.3 获取类数据

  • 为了方便使用反射,这里默认表格第一行为属性名
  • 根据表格中顺序获取属性名
Row header = sheet.getRow(0);
if (header == null) {
    
    log.warn("解析失败表头没有数据");
    return null;
}
int columnNum = header.getPhysicalNumberOfCells();
String[] properties = new String[columnNum];
for (int i = 0; i < properties.length; i++) {
    
    properties[i] = header.getCell(i).toString();
}
  • 因为要转化为对象,这里获取类的所有set方法
  • 获取所有属性对应的类型
Map<String, Method> methods = Stream.of(clazz.getMethods())
        .filter(method -> method.getName().startsWith("set"))
        .collect(Collectors.toMap(Method::getName, it->it));

Map<String, Type> fieldMap = Stream.of(clazz.getDeclaredFields())
        .collect(Collectors.toMap(Field::getName, Field::getType));

1.4 获取数据

  • 通过循环行获取row
  • row循环列获取cell
  • 通过cell获取数据
  • 通过反射将以及对应set方法,将数据写入对象
var objs = new ArrayList<>();
for (int i = startRowNum; i <= endRowNum; ++i) {
    
    Row row = sheet.getRow(i);

    if (row == null) continue;

    Object obj = clazz.getDeclaredConstructor().newInstance();
    try {
    
        for (int j = 0; j < columnNum; j++) {
    
            var methodName = "set" + properties[j].substring(0, 1).toUpperCase() + properties[j].substring(1);
            if (methods.containsKey(methodName)){
    
                Method method = methods.get(methodName);
                Cell cell = row.getCell(j);
                Type type = fieldMap.get(properties[j]);
                method.invoke(obj,cell2Obj(cell, type));
            }
        }
    } catch (Exception e) {
    
        log.error("{}文件的第{}行解析出现错误,错误类型为{},错误内容{}", fileName, i, e.getClass(), e.getMessage());
    }
    objs.add(obj);
}

1.5 数据类型转化

由于excel使用的原因,通过泛型处理会出现类型对不上的情况,比如一个数值,excel默认所有数值都是double类型,如果你的对象中是Long的话,那么就要进行处理不然没法cast

  • 时间的处理比较烦,这里默认时间都是字符串类型
  • 还有list类型也要特别处理
private static Object cell2Obj(Cell cell, Type type) {
    
    if (cell == null) return null;
    Object value = null;
    switch (cell.getCellType()) {
    
        case NUMERIC:
            if (type.getTypeName().equals("long")) value = Long.valueOf(cell.getStringCellValue());
            else {
    
                Double doubleValue = cell.getNumericCellValue();
                DecimalFormat df = new DecimalFormat("0");
                value = df.format(doubleValue);
            }
            break;
        case STRING:
            if (cell.getStringCellValue().equals("")) break;
            switch (type.getTypeName()) {
    
                case "long":
                    value = Long.valueOf(cell.getStringCellValue());
                    break;
                case "java.time.LocalDateTime":
                    String date = cell.getStringCellValue().trim();
                    DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
                    value = LocalDateTime.parse(date, df);
                    break;
                case "java.util.List":
                    String data = cell.getStringCellValue();
                    value = Arrays.asList(data.substring(1, data.length() - 1).split(","));
                    break;
                default:
                    value = cell.getStringCellValue();
                    break;
            }
            break;
        case BOOLEAN:
            value = cell.getBooleanCellValue();
            break;
        case FORMULA:
            value = cell.getCellFormula();
            break;
        default:
            break;
    }
    return value;
}

2. 写入Excel

写入的简单步骤:

  • 生成一个workbook,通过workbook生成一个sheet,默认sheet0
  • 将传入的对象数组一一对应写到对应的cell中
  • cell可以设置对应的cellStyle,跟使用excel差不多,底色,加粗啥的
  • 写完之后通过write写到一个outputStream中,生成文件

2. 1 生成workboot

  • 这里head默认使用对象中属性的顺序
List<String> header = fields.stream()
        .map(Field::getName)
        .collect(Collectors.toList());

Workbook workbook = new SXSSFWorkbook();
Sheet sheet = buildDataSheet(workbook, header);
  • 通过反射获取类所有属性
  • 不要落了super的属性
public static List<Field> getFieldsInfo(Class<?> clazz) {
    

    Field[] fields = clazz.getDeclaredFields();
    List<Field> list = new ArrayList<>(Arrays.asList(fields));
    Class<?> superClazz = clazz.getSuperclass();
    if (superClazz != null) {
    
        Field[] superFields = superClazz.getDeclaredFields();
        list.addAll(Arrays.asList(superFields));
    }
    return list;
}

2.2 生成单元格风格

  • 如果需要head与众不容,更加美观,可以设置风格
  • 和swing表格设置有点像
private static CellStyle buildHeadCellStyle(Workbook workbook) {
    
    CellStyle style = workbook.createCellStyle();

    style.setAlignment(HorizontalAlignment.CENTER);

    style.setBorderBottom(BorderStyle.THIN);
    style.setBottomBorderColor(IndexedColors.BLACK.getIndex()); 
    style.setBorderLeft(BorderStyle.THIN);
    style.setLeftBorderColor(IndexedColors.BLACK.getIndex()); 
    style.setBorderRight(BorderStyle.THIN);
    style.setRightBorderColor(IndexedColors.BLACK.getIndex());
    style.setBorderTop(BorderStyle.THIN);
    style.setTopBorderColor(IndexedColors.BLACK.getIndex()); 

    style.setFillForegroundColor(IndexedColors.SKY_BLUE.getIndex());
    style.setFillPattern(FillPatternType.SOLID_FOREGROUND);

    Font font = workbook.createFont();
    font.setBold(true);
    style.setFont(font);
    return style;
}
  • 设置普通单元格风格,中间对齐
private static CellStyle buildNormalCellStyle(Workbook workbook) {
    
    CellStyle style = workbook.createCellStyle();
    //对齐方式设置
    style.setAlignment(HorizontalAlignment.CENTER);
    return style;
}
  • 设置宽度高度
for (int i=0; i<header.size(); i++) {
    
    sheet.setColumnWidth(i, 4000);
}
// 设置默认行高
sheet.setDefaultRowHeight((short) 400);

2.3 写入数据

  • 通过list多少为row数
  • 通过对象属性为columns数
  • 获取cell,写入数据,配置风格
  • 需要特殊局里时间类型,直接格式化为字符串,跟对去那里同步
for (int i = 0; i < objs.size(); i++) {
    
    Row row = sheet.createRow(i + 1);
    Object obj = objs.get(i);
    for (int j = 0; j < header.size(); j++) {
    
        Cell cell = row.createCell(j);
        var methodName = "get" + header.get(j).substring(0, 1).toUpperCase() + header.get(j).substring(1);
        int index = methodNames.indexOf(methodName);
        if (index != -1) {
    
            Method method = methods.get(index);
            Class<?> fieldType = fields.get(j).getType();
            Object value = method.invoke(obj);
            if (value != null && fieldType == LocalDateTime.class) {
    
                DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
                String date = df.format((LocalDateTime)value);
                cell.setCellValue(date);
            } else {
    
                if (value == null) value = "";
                cell.setCellValue(value.toString());
            }
            cell.setCellStyle(normalStyle);
        }
    }
}
  • 写完数据的workbook写入到文件
OutputStream outputStream = new FileOutputStream(fileName);
if (workbook != null) {
    
    workbook.write(outputStream);
    workbook.close();
}

3. 后端upload服务

  • Path filePath = Files.createTempFile("",".xlsx");生成一个tmp文件夹的文件,这里要.xlsx结尾不然打不开
  • part.transferTo(filePath);根据路径将filepart中的内容写到path文件
  • List<Object> user = ReadExcelUtil.readExcel(in, file.getPath(), 0, SysUser.class);解析excel文件生成数据
  • 通过response返回数据到前端
@PostMapping(value = "/upload/excel", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@ResponseStatus(value = HttpStatus.OK)
public Flux<SysUser> upload(@RequestPart("file") Flux<FilePart> filePart){
    

    return filePart
            .flatMap(part -> {
    
                try {
    
                    Path filePath = Files.createTempFile("",".xlsx");
                    part.transferTo(filePath);
                    File file = new File(filePath.toString());
                    InputStream in = new FileInputStream(file);
                    List<Object> user = ReadExcelUtil.readExcel(in, file.getPath(), 0, SysUser.class);
                    if (user != null) return Mono.justOrEmpty(user);
                    in.close();
                } catch (IOException e) {
    
                    log.error(e.getMessage());
                }
                return Mono.empty();
            })
            .flatMap(it -> Flux
                    .fromIterable(it)
                    .cast(SysUser.class)
            );
}

4. 前端upload服务

  • 使用antd的upload组建
  • showUploadList: false,不显示进度条,上传文件
  • setUserData(info.file.response);done的时候通过hook将数据写到userdata中,然后传入table
  • name: 'file',这里的name和后端对应
  const [userData, setUserData] = useState([]);

const uploadProps = {
    
    name: 'file',
    action: 'http://localhost:8080/api/io/upload/excel',
    headers: {
    
      authorization: getToken(),
    },
    showUploadList: false,
    onChange(info:any) {
    
      if (info.file.status !== 'uploading') {
    
        // console.log(info.file, info.fileList);
      }
      if (info.file.status === 'done') {
    
        setUserData(info.file.response);
        // console.log(info.file.response);
        message.success(`${
      info.file.name} file uploaded successfully`);
      } else if (info.file.status === 'error') {
    
        message.error(`${
      info.file.name} file upload failed.`);
      }
    },
  };

<Dragger {
    ...uploadProps}>
  <p className="ant-upload-drag-icon">
    <InboxOutlined />
  </p>
  <p className="ant-upload-text">点击或者拖拽文件进行上传</p>
</Dragger>

5. download 后端服务

  • 获取数据库数据,或者接收前端传回数据,写入excel,然后传到前端
  • new String(("test-" + LocalDateTime.now().toLocalDate() + ".xlsx").getBytes(StandardCharsets.UTF_8),"iso8859-1");这里避免文件名乱码
  • ZeroCopyHttpOutputMessage零拷贝传输
@PostMapping("/download/excel/db")
public Mono<Void> downloadFromDb(ServerHttpResponse response) throws UnsupportedEncodingException {
    
    String fileName = new String(("test-" + LocalDateTime.now().toLocalDate() + ".xlsx").getBytes(StandardCharsets.UTF_8),"iso8859-1");
    File file = new File(fileName);
    return sysUserService.findAll()
            .collectList()
            .flatMap(list -> WriteExcelUtil.data2Workbook(list, SysUser.class))
            .flatMap(workbook-> {
    
                try {
    
                    workbook.write(new FileOutputStream(file));
                } catch (IOException e) {
    
                    e.printStackTrace();
                }
                return downloadFile(response, file, fileName);
            });
}


private Mono<Void> downloadFile(ServerHttpResponse response, File file, String fileName) {
    
    ZeroCopyHttpOutputMessage zeroCopyHttpOutputMessage = (ZeroCopyHttpOutputMessage) response;
    try {
    
        response.getHeaders()
                .set(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=".concat(
                        URLEncoder.encode(fileName, StandardCharsets.UTF_8.displayName())));
        return zeroCopyHttpOutputMessage.writeWith(file, 0, file.length());
    } catch (UnsupportedEncodingException e) {
    
        throw new UnsupportedOperationException();
    }
}

6. download 前端服务

  • responseType: 'arrayBuffer',这里要选arrayBuffer,默认是json
  • 通过blob获取,antd的这个request返回的直接就是res.data
  • 模拟点击链接下载文件,获取的文件有文件名,不是自动生成的字符
export async function downloadExcel(filename:string) {
    
  const token = `Bearer ${
    localStorage.getItem('token')}`;
  return request('http://localhost:8080/api/io/download/excel/db', {
    
    method: 'post',
    headers:{
    
      'Authorization': token,
    },
    responseType: 'arrayBuffer',
  })
  .then(res => {
    
    const blob = new Blob([res], {
    type: "application/vnd.ms-excel"});
    download(blob, filename);
  })
}

export function download(blobData: Blob, forDownLoadFileName: string ): any {
    
  const elink = document.createElement('a');
  elink.download = forDownLoadFileName;
  elink.style.display = 'none';
  elink.href = URL.createObjectURL(blobData);
  document.body.appendChild(elink);
  elink.click();
  URL.revokeObjectURL(elink.href); // 释放URL 对象
  document.body.removeChild(elink);
}

7. 代码

github 前端(antd pro) 后端(spring webflux)
gitee 前端(antd pro) 后端(spring webflux)
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/tonydz0523/article/details/108329018

智能推荐

linux sudo 权限_iteye_10062的博客-程序员宅基地

用sudo时提示"xxx is not in the sudoers file. This incident will be reported.其中XXX是你的用户名,也就是你的用户名没有权限使用sudo,我们只要修改一下/etc/sudoers文件就行了。下面是修改方 法:1)进入超级用户模式。也就是输入"su -",系统会让你输入超级用户密码,输入密码后就进入了超级用户模式。(当然,你...

实现财务自由的重要工具-程序员宅基地

**实现财务自由的重要工具**1.三大核心工具A.企业B.股票(指数基金)C.房地产(REITS):用好企业这个核心工具,会成为企业家;用好股票和房地产,会成为投资家用户企业,股票,房地产,会成为资本家2、三个辅助工具(在没有好的机会的时候,使用辅助工具)A、可转债B、逆回购C、货币基金3、一个保障工具(转移财务风险)A、保障型保险...

递归累加求和及其原理_求一到n的累加和6次递归_Only MI的博客-程序员宅基地

练习:使用递归计算1-n之间的和 定义一个方法,使用递归计算1-n之间的和 1+2+3+...+n n+(n-1)+(n-2)+...+1 已知: 最大值:n 最小值:1 使用递归必须明确: 1.递归的结束条件 获取到1的时候结束 2.递归的目的 获取下一个倍加的数字(n-1)_求一到n的累加和6次递归

C++ string的c_str函数极易产生bug, 有陷阱, 请慎用---强烈建议用strncpy来拷贝c_str_c_str()函数 崩溃原因_涛歌依旧的博客-程序员宅基地

string的c_str函数很怪异很危险, 先来看一个简单的例子:#include #include using namespace std;int main(){ string s = "abc"; const char *p = s.c_str(); cout << p << endl; // abc s = "xyz"; cout << p << endl; // _c_str()函数 崩溃原因

Vue关闭当前页面_fairy啊的博客-程序员宅基地

Vue关闭当前页面。_vue关闭当前页面

python urllib3教程_python urllib3-程序员宅基地

【实例简介】python urllib3 安装文件包【实例截图】【核心代码】urllib3-1.8.2.tar└── urllib3-1.8.2├── CHANGES.rst├── CONTRIBUTORS.txt├── dummyserver│ ├── certs│ │ ├── cacert.key│ │ ├── cacert.pem│ │ ├── client_ba..._proxy.pyc

随便推点

我的vscode json 配置_vscode 配置json-程序员宅基地

【代码】我的vscode json 配置。_vscode 配置json

字符串是否包含数字、字母、符号3项组合的正则表达式_eyes的博客-程序员宅基地

判断字符串是否包含数字、字母、符号3项组合的正则表达式 ,字符串长度为 8~16位var re = /^(?:(?=.*[0-9].*)(?=.*[A-Za-z].*)(?=.*[\W].*))[\W0-9A-Za-z]{8,16}$/; if (re.test(字符串)){}...

HDLCoder的系统设计_hdl coder_Mr_Wing5的博客-程序员宅基地

目录Fix Point conversion定点数据类型浮点数据类型硬件为什么需要定点化Fixed-Point ConversionCode generationSpeed and Area Optimization速度优化面积优化延迟平衡HDL Coder 的系统设计定点数据类型浮点数据类型硬件为什么需要定点化Fixed-Point ConversionCode generationFix Point conversion定点数据类型浮点数据类型硬件为什么需要定点化Fixed-Point Con_hdl coder

BigDecimal类型比较大小_weixin_34127717的博客-程序员宅基地

这个类是java里精确计算的类 1 比较对象是否相等 一般的对象用equals,但是BigDecimal比较特殊,举个例子: BigDecimal a=BigDecimal.valueOf(1.0); BigDecimal b=BigDecimal.valueOf(1.000); 在现实中这两个数字是相等的,但是问题来来了 a.equals(b)结果..._bigdecimal类型比较大小相等

划分字母区间_c++划分字母区间_-Billy的博客-程序员宅基地

解题思路:先用一个map统计出,每一个字符的最后出现位置。遍历字符串S,对于每一个字符,判断它的最后出现位置是否包含在 前面的字符最后出现位置内,如果不包含,就更新最后出现位置。每次达到最后出现位置时,就计算片段的长度,直到遍历结束为止。 public List&lt;Integer&gt; partitionLabels(String S) { ArrayList&l..._c++划分字母区间

Ubuntu安装配置Samba服务_ubuntu samba_JohnnyCV工程师的博客-程序员宅基地

一、什么是SambaSamba是在Linux和UNIX系统上实现SMB协议的一个免费软件,由服务器及客户端程序构成。SMB(Server Messages Block,信息服务块)是一种在局域网上共享文件和打印机的一种通信协议,它为局域网内的不同计算机之间提供文件及打印机等资源的共享服务。SMB协议是客户机/服务器型协议,客户机通过该协议可以访问服务器上的共享文件系统、打印机及其他资源。通过设置“NetBIOS over TCP/IP”使得Samba不但能与局域网络主机分享资源,还能与全世界的电脑分享资源_ubuntu samba

推荐文章

热门文章

相关标签