Easypoi(一)表格工具类封装

Easypoi(一)表格工具类封装

作者:lomtom

个人网站:https://lomtom.cn 🔗

个人公众号:博思奥园 🔗

你的支持就是我最大的动力。

表格导入工具类系列:

  1. Easypoi(一)表格导入工具类封装 🔗
  2. Easypoi(二)表格工具类使用 🔗

注: 工具类及测试github源码 🔗

需求是这样子的: 现在需要对一个表格进行导入,导入后还需要把图片上传带OSS里面,方便后面访问,导入的数据保存到数据库里面。 目前已经有成熟的Excel工具类对Excel进行导入导出操作,也可以将文件的图片转换出来,但是把图片上传到OSS上面的操作还需要自己来写,恰好,业务需要用到这种需求的场景还比较多,那么我为啥不把它封装成一个工具类呢?

说干就干!!!

准备

导入poi的依赖

<dependency>
  <groupId>cn.afterturn</groupId>
  <artifactId>easypoi-base</artifactId>
  <version>3.2.0</version>
</dependency>

编写实体类

  1. 使用@Excel标注属性,对应Excel表格的一列,并且使用name标明Excel中的表头明(默认为第一行)

  2. 如果是图片,就加一个type=2,并且标明保存的路径(默认为/excel/upload/img

  3. 高级用法,如果需要实现自己的自定义校验器,就需要实现 IExcelDataModelIExcelModel接口,并且增加rowNumerrorMsg两个属性,因为会将行号与错误信息保存在这两个字段里面。

    注意:实现 IExcelDataModelIExcelModel接口,将不能使用链式编程,即不能在实体类上加 @Accessors(chain = true)

  4. 更多用法,请参见官方文档:http://easypoi.mydoc.io/ 🔗

package com.lomtom.entity;

import cn.afterturn.easypoi.excel.annotation.Excel;
import cn.afterturn.easypoi.handler.inter.IExcelDataModel;
import cn.afterturn.easypoi.handler.inter.IExcelModel;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;

import java.util.Date;

/**
 * @author Feyoung
 * @description:商品入库模板
 * @date 2021/8/4 14:42
 */
@Data
public class CommodityTemplate implements IExcelDataModel, IExcelModel {
    /**
     * 行号
     */
    private Integer rowNum;

    /**
     * 错误消息
     */
    private String errorMsg;

    @Excel(name = "编号")
    private String id;

    /**
     * 商品名字
     */
    @Excel(name = "名字")
    private String name;

    /**
     * 商品图片
     */
    @Excel(name = "图片", type = 2, savePath = "\\imgggg")
    private String imageUrl;

    /**
     * 创建时间
     */
    @Excel(name = "日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date createTime;

    @Excel(name = "头像", type = 2, savePath = "\\imgggg")
    private String img;
}

文件转换为流

由于只支持流或者本地件的形式,那么我们则需要将文件转换为流的形式,或者保存到本地再进行读取,这里以流为例。

第一步:前提,需要引入OSS的sdk依赖,可见我另一篇文章:https://blog.csdn.net/qq_41929184/article/details/115555334 🔗

第二步: 使用OSS的客户端对文件进行读取,然后转换为流,方便后续进行处理。

1、配置通过配置文件配置后获取的
String url
String bucket
String regionId
String accessKeyId
String accessKeySecret

2、读取OSS的文件
String head= "https://oss-" + regionId + ".aliyuncs.com";
String root = "https://" + bucket + ".oss-" + regionId + ".aliyuncs.com/";
url = url.replace(root,"");
OSS ossClient = new OSSClientBuilder().build(head,
        accessKeyId, accessKeySecret);
OSSObject ossObject = ossClient.getObject(bucket, url);
InputStream in = ossObject.getObjectContent();

使用OSS的客户端对文件进行读取,这里需要注意几个小点

  1. 由于ossClient.getObject的文件地址不是全地址,只需要去除域名后的地址,所以使用String.replace函数进行去除。
  2. new OSSClientBuilder().build()的参数分别为Bucket所在地域对应的Endpoint、AccessKey、秘钥
  3. 参数分别对应(配置通过配置文件配置后获取的)
        String bucket               bucket: demo-excel
        String regionId             regionId: cn-hangzhou
        String accessKeyId          ak: *************
        String accessKeySecret      sk: ********************
    

使用easypoi进行读取

使用easypoi进行文件读取,如果没有图片或者没有其他数据验证的需求,那么到这里就结束了。

ImportParams params = new ImportParams();
List<T> results = ExcelImportUtil.importExcel(in, tClass, importParams);

其中

  1. 因为要封装,所以使用泛型,,如果没有图片的需求,可以将这里的T进行替换,例如实体类名为CommodityTemplate ,那么将T换成CommodityTemplate
  2. tClass为CommodityTemplate.class

对图片进行存储

在此之前,需要明白几个问题

  1. 因为封装成工具类,那么我的代码针对不同的实体类都能适用,不能因为字段的改变而导致工具类不能使用。
  2. 针对不同的场景(例如读取OSS文件,本地,不带图片,带图片),都能进行相应的操作。
  3. 还需要考虑自定义验证的功能。

针对第一个问题:

  1. 因为需要对图片进行操作,所以得知道,图片对应的字段是哪一个。

  2. 而easypoi会将图片保存到本地,然后将图片地址保存到对应的字段,例如,文章开始时展示的表格的头像会保存到\imggg(本地的imggg),会将图片地址保存到img属性。

  3. 那么我最初的想法是自定义一个注解,在我需要保存的属性上加这个注解,读取时,对每个属性进行扫描,如果该属性有我加的注解,那么就对该属性进行存储操作。

  4. 但是,恰好,easypoi不是有一个注解 @Excel,并且注解有一个属性(type)标识该字段为图片。这不恰好可以利用起来吗。

针对第二个问题: 可以参考公司大佬工具类的写法,对某些代码进行复用,这个问题不大。

针对第三个问题: 这个我也是之前没有想到的问题 查看easypoi的源码,可以看到easypoi有支持自定义验证的,而且恰好参数是自己传的,那么不就可以利用起来,只要自定义验证器实现IExcelVerifyHandler接口即可。

public class ImportParams extends ExcelBaseParams {
    private IExcelVerifyHandler verifyHandler;
}

ok,思路有了,进行实际行动。

第一步:对easypoi返回的结果进行遍历,这里使用foreach进行操作,这样可以获取到集合中的每一个元素。

list.forEach(obj -> {
	doSomeThing....
}

第二步:获取每个实体的属性,使用obj.getClass().getDeclaredFields()获取全部字段,然后再对每一个字段进行判断操作。

List<Field> fieldList = Arrays.asList(obj.getClass().getDeclaredFields());
fieldList.forEach(item -> {
		doSomeThing....
}

第三步:判断该字段是否被某个注解修饰,使用Field.isAnnotationPresent进行判断,参数为注解类对象,这里为easypoi的@Excel

boolean annotationPresent = item.isAnnotationPresent(Excel.class);
if (annotationPresent) {
		doSomeThing....
}

第四步:使用Field.getAnnotation获取该注解,因为@Excel 的参数type为2时表示为图片,所以通过anno.type() == 2判断是否符合条件。

 Excel anno = item.getAnnotation(Excel.class);
 if (anno.type() == 2) {
		doSomeThing....
}

第五步:拿到该字段的值,也就是说图片存在本地的路径地址,读取并且转换为流,在传到OSS里面,再将OSS的文件地址保存到该属性中,大功就告成了

String path = getFieldValueByName(item.getName(), obj);
InputStream inputStream = new FileInputStream(path);
String ossFileName = path.replace("\\", "");
ossClient.putObject(bucket, rootPath + ossFileName, inputStream);
String ossPath = "https://" + bucket + "." + "oss-" + regionId + ".aliyuncs.com" + "/" + rootPath + ossFileName;
item.setAccessible(true);
item.set(obj, ossPath);

自定义函数:

    private static String getFieldValueByName(String fieldName, Object o) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        String firstLetter = fieldName.substring(0, 1).toUpperCase();
        String getter = "get" + firstLetter + fieldName.substring(1);
        Method method = o.getClass().getMethod(getter);
        Object value = method.invoke(o);
        return value.toString();
    }

第六步:针对该方法对不同的场景进行代码的复用、抽取。

public static <T> ExcelImportResult<T> importExcelByLocalUrlWithImg(String url, Class<T> tClass, OssProperties ossProperties, String rootPath, ImportParams importParams) throws Exception {
    return importExcelByLocalUrlWithImg(url, tClass, ossProperties.getBucket(), ossProperties.getRegionId(), ossProperties.getAk(), ossProperties.getSk(), rootPath, importParams);
}

public static <T> ExcelImportResult<T> importExcelByLocalUrl(String url, Class<T> tClass, ImportParams importParams) throws Exception {
    InputStream in = new FileInputStream(url);
    return importExcelByStream(in, tClass, importParams);
}

public static <T> ExcelImportResult<T> importExcelByOssUrlWithImg(String url, Class<T> tClass, OssProperties ossProperties, String rootPath, ImportParams importParams) throws Exception {
    return importExcelByOssUrlWithImg(url, tClass, ossProperties.getBucket(), ossProperties.getRegionId(), ossProperties.getAk(), ossProperties.getSk(), rootPath, importParams);
}


public static <T> ExcelImportResult<T> importExcelByOssUrl(String url, Class<T> tClass, OssProperties ossProperties, ImportParams importParams) throws Exception {
    return importExcelByOssUrl(url, tClass, ossProperties.getBucket(), ossProperties.getRegionId(), ossProperties.getAk(), ossProperties.getSk(), importParams);
}

public static <T> ExcelImportResult<T> importExcelByStreamWithImg(InputStream in, Class<T> tClass, OssProperties ossProperties, String rootPath, ImportParams importParams) throws Exception {
    return importExcelByStreamWithImg(in, tClass, ossProperties.getBucket(), ossProperties.getRegionId(), ossProperties.getAk(), ossProperties.getSk(), rootPath, importParams);
}

public static <T> ExcelImportResult<T> importExcelByStream(InputStream in, Class<T> tClass, ImportParams importParams) throws Exception {
    return getResults(in, tClass, importParams);
}

测试


/**
 * 调用OSS(带有图片)
 * @throws Exception Exception
 */
@Test
public void importExcelByOssUrlWithImg() throws Exception {
    ImportParams params = new ImportParams();
    ExcelImportResult<CommodityTemplate> result= ExcelImportUtils.importExcelByOssUrlWithImg(url1,
            CommodityTemplate.class,ossProperties,"changxing/test/",params);
    if (result.isVerifyFail()){
        for (CommodityTemplate entity : result.getFailList()) {
            String msg = "第" + entity.getRowNum() + "行的错误是:" + entity.getErrorMsg();
            System.out.println(msg);
        }
    }
    result.getList().forEach(System.out::println);
}

注意

  1. 实现 IExcelDataModelIExcelModel接口,将不能使用链式编程,即不能在实体类上加 @Accessors(chain = true),具体原因是使用该注解后,set方法将返回该对象,而不是void,这将会产生冲突。
  2. 若项目中还是用了其他excel工具类,请将公有的依赖排除,例如easyexcel
    <dependency>
        <groupId>cn.afterturn</groupId>
        <artifactId>easypoi-base</artifactId>
        <version>2.3.0</version>
        <exclusions>
            <exclusion>
                <groupId>com.google.guava</groupId>
                <artifactId>guava</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>easyexcel</artifactId>
        <version>2.2.0-beta2</version>
        <exclusions>
            <exclusion>
                <groupId>org.apache.poi</groupId>
                <artifactId>poi-ooxml</artifactId>
            </exclusion>
            <exclusion>
                <groupId>org.apache.poi</groupId>
                <artifactId>poi-ooxml-schemas</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    
  3. 若使用了easyexcel,在引入easypoi并且排除共有依赖导致easyexcel的部分不可用,请升级easypoi的版本。
     <dependency>
         <groupId>cn.afterturn</groupId>
         <artifactId>easypoi-base</artifactId>
         <version>4.1.3</version>
     </dependency>
    

注:本工具类是基于easypoi的工具类封装

参考:

  1. easypoi导入Excel最佳实践 🔗
  2. 自定义注解,判断带注解的类或属性是否符合条件 🔗
  3. java使用反射机制设置私有成员变量的值 🔗
lomtom

标题:Easypoi(一)表格工具类封装

作者:lomtom

链接:https://lomtom.cn/880b73e4