近况
最近自己懈怠了,没有在努力学习,还没有很好的认清自己,没有看清自己的方向,所以才没有紧张感
马上就要毕业了,毕设还是没有着落,说着要做分布式毕设,但积极性不够,专心学习吧,未来的路还很长
一步一步来,你不会比别人晚,不会错过什么。
进入正题,今天我又要水一篇博客了,若依微服务系列博客的目的是为了当我自己要去完成这些工作时,
我能有很好的借鉴,多看看别人的代码是怎么写的,功能是怎么实现的,才能不断进步。
那么本篇博客讲的是导入导出功能,将数据库的数据导出成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上,有需要的可以查看具体导入导出过程,当然肯定是以后的自己需要~
导入导出相关代码