前言
需求:正如标题所言,需求有数据的导入、导出
导入:给用户提供给一个导入数据的模板,用户填写数据后上传,实现文件的批量导入。
导出:将数据列表直接导进excel,用户通过浏览器下载。
首先考虑用经典的apache poi实现这一功能,但是发现不是很好用,后面有换了阿里的 easy excel,效率比较高。
如果需求不高,只是简单的导入导出,不涉及复杂对象,可以直接使用第一版的代码。
excel基本构成
虽然只写个导入导出并不要求我们对excel有多熟悉,但是最起码得知道excel有哪些构成。
整个文件:student.xlsx,对应与poi中的Workbook
sheet:一张表
cell:单元格,每一小格
apache poi
依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.poi/poi 03版的excel --> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>4.1.2</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml 新版的excel --> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>4.1.2</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>
实体类
@Data
@AllArgsConstructor
public class Student {
private String name;
private Integer age;
private String hobby;
private Float score;
private Date birth;
}
读取
HSSF开头是老版本的excel,拓展名为xls
我们这里用的是新版本得,拓展名xlsx
public class TestRead { public static void main(String[] args) throws IOException { XSSFWorkbook workbook = new XSSFWorkbook("E:\personcode\office-learn\src\main\resources\excel\test.xlsx"); XSSFSheet sheet = workbook.getSheetAt(0); for (Row row : sheet) { for (Cell cell : row) { System.out.print(cell.toString() +" "); } System.out.println(); } workbook.cloneSheet(0); } }
写入
public class TestWrite { public static List<Student> getStudentList() { return Arrays.asList( new Student("学生1", 18, "学习", 59.5F, new Date()), new Student("学生2", 19, "游泳", 68F, new Date()), new Student("学生3", 19, "游泳", 90F, new Date()), new Student("学生4", 19, "游泳", 100F, new Date()) ); } public static SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd"); public static void main(String[] args) throws IllegalAccessException { //创建一个表格 XSSFWorkbook xssfWorkbook = new XSSFWorkbook(); List<Student> studentList = getStudentList(); //创建一个sheet XSSFSheet sheet = xssfWorkbook.createSheet("学生成绩"); //单元格格式 XSSFCellStyle cellStyle = xssfWorkbook.createCellStyle(); cellStyle.setFillBackgroundColor(IndexedColors.PINK.getIndex()); //字体样式 XSSFFont font = xssfWorkbook.createFont(); font.setFontName("黑体"); font.setColor(IndexedColors.BLUE.getIndex()); cellStyle.setFont(font); //设置第一行 XSSFRow row = sheet.createRow(0); row.createCell(0).setCellValue("姓名"); row.createCell(1).setCellValue("年龄"); row.createCell(2).setCellValue("兴趣"); row.createCell(3).setCellValue("分数"); row.createCell(4).setCellValue("日期"); for (int i = 1; i < studentList.size(); i++) { Student student = studentList.get(i); XSSFRow xrow = sheet.createRow(i); //如果不设置格式 // xrow.createCell(0).setCellValue(student.getName()); // xrow.createCell(1).setCellValue(student.getAge()); // xrow.createCell(2).setCellValue(student.getHobby()); // xrow.createCell(3).setCellValue(student.getScore()); // xrow.createCell(4).setCellValue(student.getBirth()); XSSFCell cell1 = xrow.createCell(0); cell1.setCellValue(student.getName()); cell1.setCellStyle(cellStyle); XSSFCell cell2 = xrow.createCell(1); cell2.setCellValue(student.getAge()); cell2.setCellStyle(cellStyle); XSSFCell cell3 = xrow.createCell(2); cell3.setCellValue(student.getHobby()); cell3.setCellStyle(cellStyle); XSSFCell cell4 = xrow.createCell(3); cell4.setCellValue(student.getScore()); cell4.setCellStyle(cellStyle); XSSFCell cell5 = xrow.createCell(4); cell5.setCellValue(format.format(student.getBirth())); cell5.setCellStyle(cellStyle); } //获取样式 // XSSFCellStyle cellStyle = xssfWorkbook.createCellStyle(); // XSSFDataFormat format = xssfWorkbook.createDataFormat(); // cellStyle.setDataFormat(format.getFormat("yyyy年m月d日")); FileOutputStream fileOutputStream = null; try { fileOutputStream = new FileOutputStream("E:\personcode\office-learn\src\main\resources\excel\student.xlsx"); xssfWorkbook.write(fileOutputStream); } catch (IOException e) { e.printStackTrace(); } finally { try { xssfWorkbook.close(); } catch (IOException e) { e.printStackTrace(); } try { if (fileOutputStream != null) { fileOutputStream.close(); } } catch (IOException e) { e.printStackTrace(); } } } }
Easy Excel
我们先用简单的数据结构测试一下,能跑起来才是王道,然后再考虑集成进SpringBoot,以及复杂数据。
依赖
spring之类的依赖就不赘述了
<!-- https://mvnrepository.com/artifact/com.alibaba/easyexcel --> <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>2.2.6</version> </dependency>
实体类
@Data @AllArgsConstructor @NoArgsConstructor //如果加上面一个注解,这个必须加上,否则easy excel会报无法实例化 public class Student { @ExcelProperty("姓名") private String name; @ExcelProperty("年龄") private Integer age; @ExcelProperty("爱好") private String hobby; @ExcelProperty("分数") private Float score; @ExcelProperty("生日") private Date birth; }
简单写入
public class TestWrite { public static List<Student> getStudentList() { return Arrays.asList( new Student("学生1", 18, "学习", 59.5F, new Date()), new Student("学生2", 19, "游泳", 68F, new Date()), new Student("学生3", 19, "游泳", 90F, new Date()), new Student("学生4", 19, "游泳", 100F, new Date()) ); } public static void main(String[] args) { String fileName = "E:\personcode\office-learn\src\main\resources\excel\student.xlsx"; // 这里 需要指定写用哪个class去读,然后写到第一个sheet,名字为模板 然后文件流会自动关闭 // 如果这里想使用03 则 传入excelType参数即可 EasyExcel.write(fileName, Student.class).sheet("学生信息").doWrite(getStudentList()); } }
执行结果
对比POI,有一种从SSM换到了SpringBoot的感觉。
简单读取
public class TestRead { public static void main(String[] args) { String fileName = "E:\personcode\office-learn\src\main\resources\excel\student.xlsx"; // 这里 需要指定读用哪个class去读,然后读取第一个sheet 文件流会自动关闭 EasyExcel.read(fileName, Student.class, new DemoDataListener()).sheet().doRead(); } }
简单读取需要配置一个监听类,在这个监听类里处理数据,可以边读取边处理。
下面监听类的作用是,打印每一行数据
public class DemoDataListener extends AnalysisEventListener<Student> { public DemoDataListener() {} //解析每条数据的时候会调用这个方法 @Override public void invoke(Student student, AnalysisContext analysisContext) { System.out.println(student); } //解析完成后调用这个方法 @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { } }
控制台输出
Student(name=学生1, age=18, hobby=学习, score=59.5, birth=Wed Nov 11 20:32:20 CST 2020) Student(name=学生2, age=19, hobby=游泳, score=68.0, birth=Wed Nov 11 20:32:20 CST 2020) Student(name=学生3, age=19, hobby=游泳, score=90.0, birth=Wed Nov 11 20:32:20 CST 2020) Student(name=学生4, age=19, hobby=游泳, score=100.0, birth=Wed Nov 11 20:32:20 CST 2020)
集成进SpringBoot
在完成基本的导入导出功能后,我们就可以考虑将代码集成进SpringBoot了。
第一版
仅仅针对上面的代码,做初步集成,暂不考虑其他需求,例如多个sheet、日期转换等
既然要继承进Spring环境,我主张完全交给Spring来管理,不要再new对象了。
依赖
模拟真实开发环境,需要加入数据库相关的依赖,这里我还是用的mybatis plus:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- https://mvnrepository.com/artifact/com.alibaba/easyexcel --> <dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>2.2.6</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.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>
配置文件
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://ip:3306/test?useUnicode=true&characterEncoding=UTF-8 username: root password: root mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl type-enums-package: com.dayrain.nums global-config: db-config: logic-not-delete-value: 1 logic-delete-value: 0 mapper-locations: classpath*:/mapper/**/*.xml
实体类
与上面相比,添加了一个id作为主键,
其中@Excelgnore表示导出的时候忽略该字段。
@Data @AllArgsConstructor @NoArgsConstructor //如果加上面一个注解,这个必须加上,否则easy excel会报无法实例化 public class Student { @TableId(type = IdType.AUTO) @ExcelIgnore private Integer id; @ExcelProperty("姓名") private String name; @ExcelProperty("年龄") private Integer age; @ExcelProperty("爱好") private String hobby; @ExcelProperty("分数") private Float score; @ExcelProperty("生日") private Date birth; }
DAO
@Mapper public interface StudentMapper extends BaseMapper<Student> { }
service
@Service public class StudentServiceImpl implements StudentService { @Autowired StudentMapper studentMapper; @Override public void insertStudent(Student student) { studentMapper.insert(student); } @Override public void insertStudents(List<Student> students) { students.forEach(student -> studentMapper.insert(student)); } @Override public List<Student> selectStudents() { return studentMapper.selectList(null); } }
public interface StudentService { void insertStudent(Student student); void insertStudents(List<Student>students); List<Student> selectStudents(); }
excel
excel相关的类我也写成了service,便于拓展和使用
public interface ExcelService { void simpleDownload(HttpServletResponse response) throws IOException; void simpleUpload(MultipartFile file) throws IOException; }
@Service public class ExcelServiceImpl implements ExcelService { @Autowired StudentServiceImpl studentService; @Autowired StudentDataListener studentDataListener; @Override public void simpleDownload(HttpServletResponse response) throws IOException { // 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman response.setContentType("application/vnd.ms-excel"); response.setCharacterEncoding("utf-8"); // 这里URLEncoder.encode可以防止中文乱码 String fileName = URLEncoder.encode("测试", "UTF-8").replaceAll("\+", "%20"); response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx"); EasyExcel.write(response.getOutputStream(), Student.class).sheet("学生信息").doWrite(studentService.selectStudents()); } @Override public void simpleUpload(MultipartFile file) throws IOException { EasyExcel.read(file.getInputStream(), Student.class, studentDataListener).sheet().doRead(); } }
@Component public class StudentDataListener extends AnalysisEventListener<Student> { //因为相关的类没有交给spring管理,所以不能直接通过注解注入 @Autowired private StudentService studentService; //解析每条数据的时候会调用这个方法 @Override public void invoke(Student student, AnalysisContext analysisContext) { studentService.insertStudent(student); } //解析完成后调用这个方法 @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { } }
controller
@RestController public class ExcelController { @Autowired ExcelService excelService; //导出 @GetMapping("/download") public void download(HttpServletResponse response) throws IOException { excelService.simpleDownload(response); } //导入 @PostMapping("/upload") public String upload(MultipartFile file) throws IOException { excelService.simpleUpload(file); return "success"; } }
目录结构
分析
1、对比
web版的excel导入导出,与之前简单demo相比,不需要指定文件的存放路径。
导入的时候,web端直接分析流文件,将每一行都插进数据库。
导出的时候,web端将查到数据,直接写入response,无需在服务器端生成临时文件。
2、问题
一个项目里会有很多个导入导出,并不只是Student类的导出。
但是不管是何种数据的导出,api都是相同的,区别就在于handler类不一样(因为我觉得与netty中的handler很像,故如此取名)。
例如我们现在需要导出学校信息,我们可以这么做:
第二版
新增一个实体类;
@Data public class School { @TableId(type = IdType.AUTO) private Integer id; private String name; private Student year; }
写好对应的service:
@Service public class SchoolServiceImpl implements SchoolService { @Autowired SchoolMapper schoolMapper; @Override public void insert(School school) { schoolMapper.insert(school); } }
新增对应的处理类
@Component public class SchoolHandler extends AnalysisEventListener<School> {//因为相关的类没有交给spring管理,所以不能直接通过注解注入 @Autowired private SchoolService schoolService; //解析每条数据的时候会调用这个方法 @Override public void invoke(School school, AnalysisContext analysisContext) { schoolService.insert(school); } //解析完成后调用这个方法 @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { } }
修改excel相关类,将接口设计成可以动态添加 Listen 类
public interface ExcelService { void simpleDownload(HttpServletResponse response) throws IOException; void simpleUpload(MultipartFile file, AnalysisEventListener listener) throws IOException; }
@Service public class ExcelServiceImpl implements ExcelService { @Autowired StudentServiceImpl studentService; @Autowired StudentDataListener studentDataListener; @Override public void simpleDownload(HttpServletResponse response) throws IOException { // 这里注意 有同学反应使用swagger 会导致各种问题,请直接用浏览器或者用postman response.setContentType("application/vnd.ms-excel"); response.setCharacterEncoding("utf-8"); // 这里URLEncoder.encode可以防止中文乱码 String fileName = URLEncoder.encode("测试", "UTF-8").replaceAll("\+", "%20"); response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx"); EasyExcel.write(response.getOutputStream(), Student.class).sheet("学生信息").doWrite(studentService.selectStudents()); } @Override public void simpleUpload(MultipartFile file, AnalysisEventListener listener) throws IOException { EasyExcel.read(file.getInputStream(), Student.class, listener).sheet().doRead(); } }
controller层调用
@RestController public class ExcelController { @Autowired ExcelService excelService; @Autowired SchoolHandler schoolHandler; @Autowired StudentDataListener studentDataListener; //导出 @GetMapping("/download") public void download(HttpServletResponse response) throws IOException { excelService.simpleDownload(response); } //导入 @PostMapping("/student/upload") public String upload(MultipartFile file) throws IOException { excelService.simpleUpload(file, studentDataListener); return "success"; } @PostMapping("/school/upload") public String upload2(MultipartFile file) throws IOException { excelService.simpleUpload(file, schoolHandler); return "success"; } }
其他代码不变。
补充
大体框架基本拉出来了,我们需要考虑一下细节问题。
导出所有sheet
ExcelService添加一个类
注意是doReadAll()
@Override public void allSheetUpload(MultipartFile file, AnalysisEventListener listener) throws IOException { EasyExcel.read(file.getInputStream(), Student.class, listener).doReadAll(); }
导出部分sheet
这里写死了只有两个sheet。
一般用户使用的模板都是系统提供的,所以当每个sheet存放不同类型的内容时,我们可以提前感知,在后台进行相应的映射。
@Override public void PartSheetUpload(MultipartFile file, List<AnalysisEventListener>listeners) { // 读取部分sheet ExcelReader excelReader = null; try { excelReader = EasyExcel.read(file.getInputStream()).build(); // 这里为了简单 所以注册了 同样的head 和Listener 自己使用功能必须不同的Listener ReadSheet readSheet1 = EasyExcel.readSheet(0).head(Student.class).registerReadListener(listeners.get(0)).build(); ReadSheet readSheet2 = EasyExcel.readSheet(1).head(School.class).registerReadListener(listeners.get(1)).build(); // 这里注意 一定要把sheet1 sheet2 一起传进去,不然有个问题就是03版的excel 会读取多次,浪费性能 excelReader.read(readSheet1, readSheet2); } catch (IOException e) { e.printStackTrace(); } finally { if (excelReader != null) { // 这里千万别忘记关闭,读的时候会创建临时文件,到时磁盘会崩的 excelReader.finish(); } } }
格式转换
可以在对象上直接加注解,也可以自己写一个转化类。
@Data @AllArgsConstructor @NoArgsConstructor //如果加上面一个注解,这个必须加上,否则easy excel会报无法实例化 //@ColumnWidth(25)设置行高 public class Student { @TableId(type = IdType.AUTO) @ExcelIgnore private Integer id; @ExcelProperty("姓名") // @ColumnWidth(50)设置行高,覆盖上面的设置 private String name; @ExcelProperty("年龄") private Integer age; @ExcelProperty("爱好") private String hobby; @ExcelProperty("分数") private Float score; @ExcelProperty("生日") @DateTimeFormat("yyyy年MM月dd日") private Date birth; }
如果业务比较复杂,需要自己创建转换器
public class CustomStringStringConverter implements Converter<String> { @Override public Class supportJavaTypeKey() { return null; } @Override public CellDataTypeEnum supportExcelTypeKey() { return null; } @Override public String convertToJavaData(CellData cellData, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception { return null; } @Override public CellData convertToExcelData(String s, ExcelContentProperty excelContentProperty, GlobalConfiguration globalConfiguration) throws Exception { return null; } }
上面根据自己的业务进行填写
比如要转化成LocalDateTime,就把String替换成 LocalDateTime
可以参考:https://blog.csdn.net/weixin_47098539/article/details/109385543?utm_medium=distribute.pc_aggpage_search_result.none-task-blog-2~all~sobaiduend~default-2-109385543.nonecase&utm_term=easyexcel%20%E6%97%B6%E9%97%B4%E7%B1%BB%E5%9E%8B%E7%9A%84%E8%BD%AC%E6%8D%A2&spm=1000.2123.3001.4430
添加转换器
@Override public void Convert(MultipartFile file) throws IOException { // 这里 需要指定读用哪个class去读,然后读取第一个sheet EasyExcel.read(file.getInputStream(), Student.class, studentDataListener) // 这里注意 我们也可以registerConverter来指定自定义转换器, 但是这个转换变成全局了, 所有java为string,excel为string的都会用这个转换器。 // 如果就想单个字段使用请使用@ExcelProperty 指定converter .registerConverter(new CustomStringStringConverter()) // 读取sheet .sheet().doRead(); }
如果工作中用到了其他功能,后面再来补充。
异常处理
消息转换
控制台报的一个warning,虽然不影响结果,但是看着很烦。
SpringMvc默认的消息转换 MessageConverters不支持我们设置的媒体类型。添加一个配置类即可解决问题。
这里用的是默认的jackson,也可以用Gson或者fastjson
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**").allowedOrigins("*") .allowedHeaders("*") .allowedMethods("*") .allowCredentials(true) .maxAge(30*1000); }
@Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); converter.setSupportedMediaTypes(getSupportedMediaTypes()); converters.add(converter); } public List<MediaType> getSupportedMediaTypes() { //创建fastJson消息转换器 List<MediaType> supportedMediaTypes = new ArrayList<>(); supportedMediaTypes.add(MediaType.APPLICATION_JSON); supportedMediaTypes.add(MediaType.APPLICATION_ATOM_XML); supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED); supportedMediaTypes.add(MediaType.APPLICATION_OCTET_STREAM); supportedMediaTypes.add(MediaType.APPLICATION_PDF); supportedMediaTypes.add(MediaType.APPLICATION_RSS_XML); supportedMediaTypes.add(MediaType.APPLICATION_XHTML_XML); supportedMediaTypes.add(MediaType.APPLICATION_XML); supportedMediaTypes.add(MediaType.IMAGE_GIF); supportedMediaTypes.add(MediaType.IMAGE_JPEG); supportedMediaTypes.add(MediaType.IMAGE_PNG); supportedMediaTypes.add(MediaType.TEXT_EVENT_STREAM); supportedMediaTypes.add(MediaType.TEXT_HTML); supportedMediaTypes.add(MediaType.TEXT_MARKDOWN); supportedMediaTypes.add(MediaType.TEXT_PLAIN); supportedMediaTypes.add(MediaType.TEXT_XML); supportedMediaTypes.add(MediaType.ALL); return supportedMediaTypes; } }
如有错误,欢迎批评指正~