近况

最近自己懈怠了,没有在努力学习,还没有很好的认清自己,没有看清自己的方向,所以才没有紧张感
马上就要毕业了,毕设还是没有着落,说着要做分布式毕设,但积极性不够,专心学习吧,未来的路还很长
一步一步来,你不会比别人晚,不会错过什么。

进入正题,今天我又要水一篇博客了,若依微服务系列博客的目的是为了当我自己要去完成这些工作时,
我能有很好的借鉴,多看看别人的代码是怎么写的,功能是怎么实现的,才能不断进步。

那么本篇博客讲的是导入导出功能,将数据库的数据导出成excel表格,使用的是apache的POI吧。
我也是第一次学习这类接口,POI 提供 API 接口给 Java 程序对 Microsoft office 格式文档读写能力。
若依项目中,通过在实体类上添加@Excel注解,就能将数据导出到excel表中,或者将excel表中数据转成实体类
通过POI接口+注解+反射结合使用从而实现该功能,下面看看本博客具体内容:

POI接口的API基本使用

  • 创建excel对象,Workbook
    若依代码中有这样一句代码:this.wb = WorkbookFactory.create(is);
    WorkbookFactory自然是Workbook的工厂类,根据输入流is,创建一个Workbook对象
    相当于创建了一个excel文件,再次体会到了什么叫万物皆对象,思绪再次展开,
    那么类不可以是对象吗?如果类是对象,那是什么产生的类?Class类啊!这就是为什么它叫类的模板。
    那Class类不可以是对象吗?没完没了了是吧,笑~

  • 得到一个excel表格,Sheet
    我们知道一个excel文件是可以有多个表格的,例如下面这样子:

    那么我们如何得到指定的表格对象呢?在若依项目中,当导入excel文件时,我们需要拿到excel表格对象
    Sheet,通过Workbook对象,可以根据指定名称拿到某个表格,或者根据下标位置拿到excel表格

    // 如果指定sheet名,则取指定sheet中的内容 否则默认指向第1个sheet
    Sheet sheet = StringUtils.isNotEmpty(sheetName) ? wb.getSheet(sheetName) : wb.getSheetAt(0);

    这个Sheet对象就是代表excel文件中某个表格对象了
    这个对象中有一些常用的方法:

    int getLastRowNum();	   //获取最后一个非空行的行下标,比如总行数为n,则返回的为n-1
    Row getRow(int var1);    //拿到指定行的行对象,Sheet是某个表格对象,表格中的每一行就是Row对象
    void removeRow(Row var1);
    Row createRow(int var1);
    String getSheetName();   //获取表格的名称
    Workbook getWorkbook();  //获取该excel文件对象
  • 得到excel表格某一行对象,Row
    就是上面的getRow方法,若依项目中有如下使用:

    // 获取表头,每一行都是ROW对象
    Row heard = sheet.getRow(titleNum);
    //拿到这一行的列数,并遍历这一行中所有单元格,将单元格的值和列号存到cellMap中
    for (int i = 0; i < heard.getPhysicalNumberOfCells(); i++)
    {
      Cell cell = heard.getCell(i);
      if (StringUtils.isNotNull(cell))
      {
        String value = this.getCellValue(heard, i).toString();
        cellMap.put(value, i);  //存表头中,列号和列号的值,列号从0开始,例如 0:用户序号  1:部门编号
      }
      else
      {
        cellMap.put(null, i);
      }
    }

    Row对象常用的方法:

    short getFirstCellNum();    //获取这一行第一个单元格的位置
    short getLastCellNum();     //获取这一行最后一个单元的位置
    int getPhysicalNumberOfCells();  //获取这一行实际的单元格数量
    Sheet getSheet();  //获取这一行所在的excel表格
    //还有创建单元格,移出单元格,得到该行所在行数,得到指定位置的单元格对象,就不一一列举了
  • 得到某一个单元格对象,Cell
    如上:Cell cell = heard.getCell(i);,我们可以通过Row对象,根据列号,拿到指定的单元格对象
    这个对象有下面的一些方法:

    int getColumnIndex();  //得到该单元格所在的列号
    int getRowIndex();  //得到该单元格所在行号
    Sheet getSheet();  //得到该单元格所在excel表格对象
    Row getRow();      //得到该单元格所在行对象
    void setBlank();   //将该单元格值设置为空
    CellType getCellType();  //得到该单元格的数据类型

若依源代码实现

这里是关于若依项目中部分代码,有些代码放在了github上,代码行数太多展示不出来了
下面看看若依项目中如何使用注解实现导入导出吧

  • 导入POI依赖

    <!-- excel工具 -->
    <dependency>
      <groupId>org.apache.poi</groupId>
      <artifactId>poi-ooxml</artifactId>
    </dependency>
  • @Excel和@Excels注解,以及他们实际使用

    • @Excel

      package com.ruoyi.common.core.annotation;
      //省略常用的导入代码
      import java.math.BigDecimal;
      import com.ruoyi.common.core.utils.poi.ExcelHandlerAdapter;
      
      /**
       * 自定义导出Excel数据注解
       */
      @Retention(RetentionPolicy.RUNTIME)
      @Target(ElementType.FIELD)
      public @interface Excel {
          /**
           * 导出时在excel中排序
           */
          public int sort() default Integer.MAX_VALUE;
      
          /**
           * 导出到Excel中的名字.
           */
          public String name() default "";
      
          /**
           * 日期格式, 如: yyyy-MM-dd
           */
          public String dateFormat() default "";
      
          /**
           * 读取内容转表达式 (如: 0=男,1=女,2=未知)
           */
          public String readConverterExp() default "";
      
          /**
           * 分隔符,读取字符串组内容
           */
          public String separator() default ",";
      
          /**
           * BigDecimal 精度 默认:-1(默认不开启BigDecimal格式化)
           */
          public int scale() default -1;
      
          /**
           * BigDecimal 舍入规则 默认:BigDecimal.ROUND_HALF_EVEN
           */
          public int roundingMode() default BigDecimal.ROUND_HALF_EVEN;
      
          /**
           * 导出类型(0数字 1字符串)
           */
          public ColumnType cellType() default ColumnType.STRING;
      
          /**
           * 导出时在excel中每个列的高度 单位为字符
           */
          public double height() default 14;
      
          /**
           * 导出时在excel中每个列的宽 单位为字符
           */
          public double width() default 16;
      
          /**
           * 文字后缀,如% 90 变成90%
           */
          public String suffix() default "";
      
          /**
           * 当值为空时,字段的默认值
           */
          public String defaultValue() default "";
      
          /**
           * 提示信息
           */
          public String prompt() default "";
      
          /**
           * 设置只能选择不能输入的列内容.
           */
          public String[] combo() default {};
      
          /**
           * 是否导出数据,应对需求:有时我们需要导出一份模板,这是标题需要但内容需要用户手工填写.
           */
          public boolean isExport() default true;
      
          /**
           * 另一个类中的属性名称,支持多级获取,以小数点隔开
           */
          public String targetAttr() default "";
      
          /**
           * 是否自动统计数据,在最后追加一行统计数据总和
           */
          public boolean isStatistics() default false;
      
          /**
           * 导出字段对齐方式(0:默认;1:靠左;2:居中;3:靠右)
           */
          public Align align() default Align.AUTO;
      
          /**
           * 自定义数据处理器
           */
          public Class<?> handler() default ExcelHandlerAdapter.class;
      
          /**
           * 自定义数据处理器参数
           */
          public String[] args() default {};
      
          public enum Align {
              AUTO(0), LEFT(1), CENTER(2), RIGHT(3);
              private final int value;
      
              Align(int value) {
                  this.value = value;
              }
      
              public int value() {
                  return this.value;
              }
          }
      
          /**
           * 字段类型(0:导出导入;1:仅导出;2:仅导入)
           */
          Type type() default Type.ALL;
      
          public enum Type {
              ALL(0), EXPORT(1), IMPORT(2);
              private final int value;
      
              Type(int value) {
                  this.value = value;
              }
      
              public int value() {
                  return this.value;
              }
          }
      
          public enum ColumnType {
              NUMERIC(0), STRING(1), IMAGE(2);
              private final int value;
      
              ColumnType(int value) {
                  this.value = value;
              }
      
              public int value() {
                  return this.value;
              }
          }
      }
    • @Excels

      /**
       * Excel注解集
       */
      @Target(ElementType.FIELD)
      @Retention(RetentionPolicy.RUNTIME)
      public @interface Excels {
          Excel[] value();
      }
    • 实际使用
      @Excel注解和@Excels注解都使用在实体类的字段上,下面是某个实体类上的@Excel字段

      /**
       * 用户对象 sys_user
       */
      public class SysUser extends BaseEntity {
          private static final long serialVersionUID = 1L;
      
          /**
           * 用户ID
           */
          @Excel(name = "用户序号", cellType = ColumnType.NUMERIC, prompt = "用户编号")
          private Long userId;
      
          /**
           * 部门ID
           */
          @Excel(name = "部门编号", type = Type.IMPORT)
          private Long deptId;
      
          /**
           * 用户账号
           */
          @Excel(name = "登录名称")
          private String userName;
      
          /**
           * 用户昵称
           */
          @Excel(name = "用户名称")
          private String nickName;
      
          /**
           * 用户邮箱
           */
          @Excel(name = "用户邮箱")
          private String email;
      
          /**
           * 手机号码
           */
          @Excel(name = "手机号码")
          private String phonenumber;
      
          /**
           * 用户性别
           */
          @Excel(name = "用户性别", readConverterExp = "0=男,1=女,2=未知")
          private String sex;
      
          /**
           * 用户头像
           */
          private String avatar;
      
          /**
           * 密码
           */
          private String password;
      
          /**
           * 帐号状态(0正常 1停用)
           */
          @Excel(name = "帐号状态", readConverterExp = "0=正常,1=停用")
          private String status;
      
          /**
           * 删除标志(0代表存在 2代表删除)
           */
          private String delFlag;
      
          /**
           * 最后登录IP
           */
          @Excel(name = "最后登录IP", type = Type.EXPORT)
          private String loginIp;
      
          /**
           * 最后登录时间
           */
          @Excel(name = "最后登录时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss", type = Type.EXPORT)
          private Date loginDate;
      
          /**
           * 部门对象
           */
          @Excels({
                  @Excel(name = "部门名称", targetAttr = "deptName", type = Type.EXPORT),
                  @Excel(name = "部门负责人", targetAttr = "leader", type = Type.EXPORT)
          })
          private SysDept dept;
      
          //省略部分代码
      }
  • ExcelUtil
    重要的Excel工具类,使用该工具类完成Excel文件与实体类之间的数据转换,下面是导入的转换方法,
    即将Excel文件中数据转成实体类:

    /**
    * 对excel表单指定表格索引名转换成list
    *
    * @param sheetName 表格索引名
    * @param titleNum  标题占用行数
    * @param is        输入流
    * @return 转换后集合
    */     
    public List<T> importExcel(String sheetName, InputStream is, int titleNum) throws Exception {
      this.type = Type.IMPORT;
      this.wb = WorkbookFactory.create(is);
      List<T> list = new ArrayList<T>();
      // 如果指定sheet名,则取指定sheet中的内容 否则默认指向第1个sheet
      Sheet sheet = StringUtils.isNotEmpty(sheetName) ? wb.getSheet(sheetName) : wb.getSheetAt(0);
      if (sheet == null) {
        throw new IOException("文件sheet不存在");
      }
    
      // 获取最后一个非空行的行下标,比如总行数为n,则返回的为n-1
      int rows = sheet.getLastRowNum();
    
      if (rows > 0) {
        // 定义一个map用于存放excel列的序号和field.
        Map<String, Integer> cellMap = new HashMap<String, Integer>();
        // 获取表头
        Row heard = sheet.getRow(titleNum); //titleNum: excel中标题所在下标,即第0行
        for (int i = 0; i < heard.getPhysicalNumberOfCells(); i++) {
          Cell cell = heard.getCell(i);
          if (StringUtils.isNotNull(cell)) {
            String value = this.getCellValue(heard, i).toString();
            cellMap.put(value, i);  //存表头中,列号和列号的值,列号从0开始,例如 0:用户序号  1:部门编号
          } else {
            cellMap.put(null, i);
          }
        }
        // 有数据时才处理 得到类的所有field.  确定哪些字段要导出,[{userId,userId上的excel注解},{}]
        List<Object[]> fields = this.getFields();
        Map<Integer, Object[]> fieldsMap = new HashMap<Integer, Object[]>();
        for (Object[] objects : fields) {
          Excel attr = (Excel) objects[1];
          Integer column = cellMap.get(attr.name());
          if (column != null) {
            fieldsMap.put(column, objects);
          }
        }
        for (int i = titleNum + 1; i <= rows; i++) {
          // 从第2行开始取数据,默认第一行是表头.
          Row row = sheet.getRow(i);
          // 判断当前行是否是空行
          if (isRowEmpty(row)) {
            continue;
          }
          T entity = null;
          for (Map.Entry<Integer, Object[]> entry : fieldsMap.entrySet()) {
            Object val = this.getCellValue(row, entry.getKey());
    
            // 如果不存在实例则新建.
            entity = (entity == null ? clazz.newInstance() : entity);
            // 从map中得到对应列的field.
            Field field = (Field) entry.getValue()[0];
            Excel attr = (Excel) entry.getValue()[1];
            // 取得类型,并根据对象类型设置值.
            Class<?> fieldType = field.getType();
            if (String.class == fieldType) {
              String s = Convert.toStr(val);
              if (StringUtils.endsWith(s, ".0")) {
                val = StringUtils.substringBefore(s, ".0");
              } else {
                String dateFormat = field.getAnnotation(Excel.class).dateFormat();
                if (StringUtils.isNotEmpty(dateFormat)) {
                  val = DateUtils.parseDateToStr(dateFormat, (Date) val);
                } else {
                  val = Convert.toStr(val);
                }
              }
            } else if ((Integer.TYPE == fieldType || Integer.class == fieldType) && 
              StringUtils.isNumeric(Convert.toStr(val))) {
              val = Convert.toInt(val);
            } else if (Long.TYPE == fieldType || Long.class == fieldType) {
              val = Convert.toLong(val);
            } else if (Double.TYPE == fieldType || Double.class == fieldType) {
              val = Convert.toDouble(val);
            } else if (Float.TYPE == fieldType || Float.class == fieldType) {
              val = Convert.toFloat(val);
            } else if (BigDecimal.class == fieldType) {
              val = Convert.toBigDecimal(val);
            } else if (Date.class == fieldType) {
              if (val instanceof String) {
                val = DateUtils.parseDate(val);
              } else if (val instanceof Double) {
                val = DateUtil.getJavaDate((Double) val);
              }
            } else if (Boolean.TYPE == fieldType || Boolean.class == fieldType) {
              val = Convert.toBool(val, false);
            }
            if (StringUtils.isNotNull(fieldType)) {
              String propertyName = field.getName();
              if (StringUtils.isNotEmpty(attr.targetAttr())) {
                propertyName = field.getName() + "." + attr.targetAttr();
              } else if (StringUtils.isNotEmpty(attr.readConverterExp())) {
                val = reverseByExp(Convert.toStr(val), attr.readConverterExp(), attr.separator());
              } else if (!attr.handler().equals(ExcelHandlerAdapter.class)) {
                val = dataFormatHandlerAdapter(val, attr);
              }
              ReflectUtils.invokeSetter(entity, propertyName, val);
            }
          }
          list.add(entity);
        }
      }
      return list;
    }

    全部的类方法,请见:ExcelUtil

  • ExcelHandlerAdapter接口
    这个自定义接口是用于数据格式处理的,有时候我们希望数据展现为一个特殊的格式,或者需要对数据进行其它处理。
    只要实现该接口的format方法,并在@Excel注解上标注该接口的实现类
    即可使用该实现类去转换数据,这个接口的实现类用于对Excel文件中数据进行加工,加工后的数据再绑定到实体类属性中。

    • ExcelHandlerAdapter接口

      /**
       * Excel数据格式处理适配器
       */
      public interface ExcelHandlerAdapter {
          /**
           * 格式化
           *
           * @param value 单元格数据值
           * @param args  excel注解args参数组
           * @return 处理后的值
           */
          Object format(Object value, String[] args);
      }
    • 使用方式:
      1,在实体类用Excel注解handler属性指定自定义的数据处理器

      public class User extends BaseEntity{
          @Excel(name = "用户名称", handler = MyDataHandler.class, args = { "aaa", "bbb" })
          private String userName;
      }

      2,编写数据处理器MyDataHandler继承ExcelHandlerAdapter,返回值为处理后的值。

      public class MyDataHandler implements ExcelHandlerAdapter{
          @Override
          public Object format(Object value, String[] args){
              // value 为单元格数据值
      		// args 为excel注解args参数组
      		return value;
          }
      }
  • 最后让我们来看看Controller中的实现
    上面介绍了关于如何实现数据的导入导出,通过ExcelUtil工具类,结合POI的相关API,通过注解反射的形式实现
    下面是当导入请求来到后的实际过程:

    @Log(title = "用户管理", businessType = BusinessType.IMPORT)
    @RequiresPermissions("system:user:import")
    @PostMapping("/importData")
    public AjaxResult importData(MultipartFile file, boolean updateSupport) throws Exception
    {
      ExcelUtil<SysUser> util = new ExcelUtil<SysUser>(SysUser.class);
      List<SysUser> userList = util.importExcel(file.getInputStream());
      String operName = SecurityUtils.getUsername();
      String message = userService.importUser(userList, updateSupport, operName);
      return AjaxResult.success(message);
    }

    这是用户导入的controller,Excel文件中包含用户数据,当我们导入该Excel后,请求来到这里,
    通过ExcelUtil工具类解析,将Excel中用户数据转换成实体类集合,再持久化用户数据,
    该方法的updateSupport参数表示,如果数据库存在相同的用户数据,是否更新。

  • 最后的最后
    我已经相关导入导出的代码全部放在GitHub上,有需要的可以查看具体导入导出过程,当然肯定是以后的自己需要~
    导入导出相关代码