前言
需求:正如標題所言,需求有數據的導入、導出
導入:給用戶提供給一個導入數據的模板,用戶填寫數據后上傳,實現文件的批量導入。
導出:將數據列表直接導進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() +"\t"); } 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; } }
如有錯誤,歡迎批評指正~