我是如何通过反射机制写一个通用excel导入导出的【上篇--导入】

快一年没写推文了,感觉肚子里也积了点东西,今天就搞一搞个人觉得还是挺酷的东西--结合反射机制搞一个通用Excel导入,通用导出功能。

想了解下反射api的使用可以看下JAVASchool的说明:http://www.51gjie.com/java/782.html

以前写POI导入Excel的时候,基本都是来一个模板,就写一个解析读取接口,就下面像这样:

一列一列的取,取完再赋值给定义的实体类,这样来一个模板写一段代码去解析读取呢,其实也没啥问题,在需要读取的Excel很少的时候,写也很快。但是当需要读取的Excel越来越多的时候,每来一个模板就写一个Excel,来一个写一个的话,那就很难受了,效率也很低!像之前我搞的一个项目,需要读取解析的Excel模板四十多个的,一个一个写的话,那我要写到吐了。

 

所以,写出一段可以解析不同Excel模板的代码,还是很有必要的,大体的实现思路如下:

1:根据不同的Excel文档模型,在数据库中定义一个解析模板

2:接口调用的时候把Excel文件与相应的Excel模板类型名传入,后台根据模板类型名找相应的解析模板去解析Excel

3:解析读取数据

4:拿到数据进行后续的处理

 

详细过程:

1:先在数据库中定义一个模板解析表(用来保存每个Excel的解析模板),sql如下:

CREATE TABLE `template_data` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `type` varchar(100) NOT NULL DEFAULT '' COMMENT '模板类型',
  `start_row` int(11) NOT NULL DEFAULT '0' COMMENT '开始行',
  `field` json DEFAULT NULL COMMENT '字段',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='模板数据表';

提示一下,filed字段是json类型的,mysql 5.7 以上的版本才支持json类型,低于5.7的版本sql会执行失败,需要升级mysql。

表字段说明:

type:用来存模板类型,也可以说是这个模板的名称

start_row:表示从第几行开始读取,一般的Excel表前几行都会是描述或者表头,所以需要定义好从第几行开始读取数据,像我用来做测试用的张大炮的Excel样例,就是从第三行开始读取

field:用来存每一列的数据的类型,对应的java类属性名(关键点,决定在利用反射的时候赋值给java类中的哪个属性),是否允许为空等。张大炮Excel文档的一条模板解析sql如下:

INSERT INTO `template_data`(`id`, `type`, `start_row`, `field`) VALUES (1, 'ZhangDaPao', 2, '[{\"fieldLong\": 50, \"fieldName\": \"name\", \"fieldType\": \"String\", \"isNotNull\": \"是\", \"templateColumn\": 0}, {\"fieldLong\": 11, \"fieldName\": \"age\", \"fieldType\": \"Integer\", \"isNotNull\": \"是\", \"templateColumn\": 1}, {\"fieldLong\": 10, \"fieldName\": \"sex\", \"fieldType\": \"String\", \"isNotNull\": \"是\", \"templateColumn\": 2}, {\"fieldLong\": 255, \"fieldName\": \"address\", \"fieldType\": \"String\", \"isNotNull\": \"是\", \"templateColumn\": 3}, {\"fieldLong\": 1024, \"fieldName\": \"remark\", \"fieldType\": \"String\", \"isNotNull\": \"是\", \"templateColumn\": 4}]');
 

filed字段里面的json数据说明

fieldLong-用来限制Excel列的数据长度

filedName-对应的类属性名

fieldType-属性类型

isNotNull-是否不能为空

templateColumn-数据在Excel行中的第几列,下标从0开始。

之后每新增一个Excel模板,就在数据库中配置多一条sql就好了,接下来就是具体的代码实现了。

项目结构如下:

pom文件依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <!--  web依赖  -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--  POI依赖  -->
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi</artifactId>
        <version>3.17</version>
    </dependency>
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml</artifactId>
        <version>3.17</version>
    </dependency>
    <!--  lombok依赖  -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.6</version>
    </dependency>
    <!--  阿里数据源依赖  -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.0</version>
    </dependency>
    <!-- mysql依赖-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>javax.persistence</groupId>
        <artifactId>javax.persistence-api</artifactId>
        <version>2.2</version>
    </dependency>
    <dependency>
        <groupId>net.sf.json-lib</groupId>
        <artifactId>json-lib</artifactId>
        <version>2.4</version>
        <classifier>jdk15</classifier>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
    </dependency>
    <!-- mybatis-plus依赖-->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.3.1.tmp</version>
    </dependency>

    <!--  json-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.49</version>
    </dependency>
    <dependency>
        <groupId>javax</groupId>
        <artifactId>javaee-web-api</artifactId>
        <version>7.0</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>


</dependencies>

 

实体类准备:

TemplateData:模板数据类

import lombok.Data;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

/**
 * 模板数据
 */
@Data
public class TemplateData {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    /**实体对象*/
    private String type;
    
    /**开始行*/
    private int startRow;
    
    /**字段*/
    private String field;
}

 

FieldTemplate:JSON字段实体类

import lombok.Data;

/**
 * 字段模板
 */
@Data
public class FieldTemplate {

    /**字段名*/
    private String fieldName;
    
    /**模板列*/
    private int templateColumn;
    
    /**字段类型*/
    private String fieldType;

    /**是否非空 (是  否)*/
    private String isNotNull;

    /**字段长度限制*/
    private int fieldLong;
}

 

ZhangDaPao:张大炮实体类

import lombok.Data;

@Data
public class ZhangDaPao {

    private String name;

    private Integer age;

    private String sex;

    private String address;

    private String remark;
}

 

Mybatis映射类:

TemplateDataMapper:模板数据映射类

import com.example.excel.template.entity.TemplateData;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

@Mapper
@Repository
public interface TemplateDataMapper {

    @Select("select * from template_data where type = #{type}")
    TemplateData findByType(@Param("type") String type);

}

 

业务层:ReportService,写了很多注释,希望对理解逻辑有帮助

主要逻辑:

1-获取解析模板

2-匹配模板字段与实体类的成员变量

3-校验数据列数据与赋值给成员变量

4-返回数据

import com.alibaba.fastjson.JSON;
import com.example.excel.template.entity.FieldTemplate;
import com.example.excel.template.entity.TemplateData;
import com.example.excel.template.entity.ZhangDaPao;
import com.example.excel.template.mapper.TemplateDataMapper;
import lombok.extern.slf4j.Slf4j;
import net.sf.json.JSONArray;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

@Service
@Slf4j
public class ReportService {

    @Autowired
    private TemplateDataMapper templateDataMapper;

    /**
     * @param file:需要解析的Excel
     * @param type:文件模板名
     * @return
     */
    public Object dataUpload(MultipartFile file, String type) {
        if (file == null || file.isEmpty()) {
            log.error("上报文件为空");
            return "上报文件为空";
        }
        Workbook workbook = null;
        try {
            workbook = WorkbookFactory.create(file.getInputStream());
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (workbook == null) {
            log.error("workbook == null");
            return "workbook == null";
        }
        return execute(workbook, type);
    }


    /**
     * 上报处理
     */
    public Object execute(Workbook hssfWorkbook, String type) {
        TemplateData templateData = null;
        switch (type) {
            case "ZhangDaPao":
                //根据模板类型去数据库取相应的解析模板
                templateData = templateDataMapper.findByType("ZhangDaPao");
                return addData(templateData, hssfWorkbook,
                        new ZhangDaPao());
            default:
                log.error("没有找到对应的模板...");
        }
        return null;
    }

    /**
     * @param templateData
     * @param hssfWorkbook
     * @param t 与Excel中数据对应的实体类
     * @param <T>
     * @return
     */
    public <T> List<T> addData(TemplateData templateData, Workbook hssfWorkbook, T t) {
        JSONArray jsonArray = JSONArray.fromObject(new ArrayList<>());
        //读第一个Excel表
        Sheet hssfSheet = hssfWorkbook.getSheetAt(0);
        //拿解析模板中的field字段的json数据列,转为字段模板列表
        List<FieldTemplate> fieldTemplateList = JSON.parseArray(templateData.getField(), FieldTemplate.class);
        //从模板中定义的开始行开始遍历读取行
        for (int rowNum = templateData.getStartRow(); rowNum <= hssfSheet.getLastRowNum(); rowNum++) {
            Row hssfRow = hssfSheet.getRow(rowNum);
            if (hssfRow != null) {
                T data = t;
                //遍历field字段中的json数据列
                for (FieldTemplate sqlFt : fieldTemplateList) {
                    //获取泛型的class,再用getDeclaredFields()获取类的成员变量
                    //遍历成员变量
                    for (Field field : t.getClass().getDeclaredFields()) {
                        //如果成员变量跟数据库中定义的属性名匹配,进入数据读取解析
                        //也就是sql中,field定义的 "fieldName" 与实体ZhangDaPao中的属性名相同,进入数据读取
                        if (field.getName().equals(sqlFt.getFieldName())) {
                            Cell cell = hssfRow.getCell(sqlFt.getTemplateColumn());

                            /*Excel数据列的校验,读取,赋值操作
                            * cell:数据列
                            * sqlFt:数据库中field字段中的json模板
                            * field:反射中的成员变量
                            * rowNum:读取的Excel是哪一行,用来抛异常的时候记录是第几行有问题
                            * data:与Excel数据对应的实体类
                            * */
                            verifyType(cell, sqlFt, field, rowNum, data);
                        }
                    }
                }
                //把读取成功的data加到json数组中,用于后续泛型转换
                jsonArray.add(data);
            }
        }
        //转换为相应的实体列表返回
        return (List<T>) JSONArray.toCollection(jsonArray, t.getClass());
    }

    /**
     * 校验,读取
     * @param cell:数据列
     *
     * @param sqlFt:数据库中field字段中的json模板,格式是下面这样的
     * {"fieldLong": 50, "fieldName": "name", "fieldType": "String", "isNotNull": "是", "templateColumn": 0}
     *
     * @param field:反射中获取到的成员变量
     * @param rowNum:读取的Excel是哪一行,用来抛异常的时候记录是第几行有问题
     * @param data:与Excel数据对应的实体类
     */
    private <T> T verifyType(Cell cell, FieldTemplate sqlFt, Field field, int rowNum, T data) {
        // 校验非空
        if (sqlFt.getIsNotNull().equals("是")) {
            if (cell == null || StringUtils.isBlank(cell.toString())) {
                log.error("数据格式错误:行:" + (rowNum + 1) + ";列:" + (sqlFt.getTemplateColumn() + 1) + ",必要数据不能为空");
            }
        }
        // 校验长度
        if (cell != null && cell.toString().length() >= sqlFt.getFieldLong()) {
            log.error("数据格式错误:行:" + (rowNum + 1) + ";列:" + (sqlFt.getTemplateColumn() + 1) + ",长度超出限制");
        }
        try {
            //打破封装
            //一般成员变量我们都是定义为private的,在反射机制中,需要设置该属性允许被操作
            field.setAccessible(true);
            //根据数据库中json数据中定义的该列是什么数据类型,进行相应的数据处理,这里只弄了String跟Integer的
            if (sqlFt.getFieldType().equals("String")) {
                //String类型,直接toString赋值
                field.set(data, cell.toString());
            } else if (sqlFt.getFieldType().equals("Integer")) {
                //Integer类型,读取到的格式是这样的 30.0 先转  double再转为int类型
                field.set(data, new Double(cell.toString()).intValue());
            }
        } catch (Exception e) {
            log.error("数据格式错误:行:" + (rowNum + 1) + ";列:" + (sqlFt.getTemplateColumn() + 1));
        }
        return data;
    }
}

 

控制类:

import com.example.excel.template.service.ReportService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
@Slf4j
public class TemplateUploadController {

    @Autowired
    private ReportService reportService;

    @RequestMapping(value = "/upload", method = RequestMethod.POST)
    public void illegalBuildingUpload(MultipartFile file, @RequestParam String type) {
        log.info("文件上传---------");
        reportService.dataUpload(file, type);
    }
}

 

postman调用测试:调用成功,返回了张大炮等人的信息

接下来我们增加一个Excel模板,如下

新增Teacher实体类如下:

import lombok.Data;

@Data
public class Teacher {

    private String name;

    private String school;

    private String course;
}

新增一条模板数据到数据库

INSERT INTO `template_data`(`id`, `type`, `start_row`, `field`) VALUES (2, 'Teacher', 2, '[{\"fieldLong\": 50, \"fieldName\": \"name\", \"fieldType\": \"String\", \"isNotNull\": \"是\", \"templateColumn\": 0}, {\"fieldLong\": 50, \"fieldName\": \"school\", \"fieldType\": \"String\", \"isNotNull\": \"是\", \"templateColumn\": 1}, {\"fieldLong\": 50, \"fieldName\": \"course\", \"fieldType\": \"String\", \"isNotNull\": \"是\", \"templateColumn\": 2}]');
 

在ReportService的execute中加入Teacher类型的捕获,如下:

具体代码:

case "Teacher":
    templateData = templateDataMapper.findByType("Teacher");
    return addData(templateData, hssfWorkbook,
            new Teacher());

postman调用:

获取数据成功,后续继续新增Excel模板的时候,只需要在数据库中配置新的模板,再建一个实体类就可以了。

通用的Excel导入就写到这,哪天心血来潮了再继续写通用导出的。

程序员-就得搞搞偷懒式的代码

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值