Ở bài trước mình có hướng dẫn các bạn làm thế nào để excel data ra file excel một cách dễ dàng và đơn giản, các bạn có thể xem lại tại đây.
Giới thiệu Import Excel Spring Boot
Trong bài viết này mình sẽ hướng dẫn các bạn import excel spring boot. Mục tiêu mình đăt ra là:
- Import data từ excel vào csdl
- Khi dữ liệu bị lỗi thì tự động trả lại file excel và bôi đỏ dòng bị lỗi
- Comment nội dung bị lỗi để người dùng biết
- Code tối ưu

Thư viện import excel trong java
Các bạn thêm thư viện vào project của mình, bên cạnh đó các bạn có thể tìm thư viện trên trang mvnrepository.com
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.poi/poi-ooxml -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.2.2</version>
</dependency>
<dependency>
<groupId>com.github.ozlerhakan</groupId>
<artifactId>poiji</artifactId>
<version>3.1.9</version>
</dependency>
Hai thư viện đầu tiên thì hẳn các bạn cũng đã biết nhiều, thư viện thứ 3 mình dùng để xử lý đọc file excel to object. Thư viện Poiji cũng có các hướng dẫn về cách sử dụng các bạn xem tại đây nhé.
Import Excel Spring Boot
Vì import excel mình dùng ở rất nhiều nơi nên mình sẽ chia tiếp thành 2 phần, đầu tiên sẽ code setup những thứ dùng chung. Làm sao có thể tái sử dụng và tùy biến được nhiều nhất có thể, phần tiếp theo là áp dụng các abstract class mình đã viết như thế nào cho api.
Code abstract class và validator dùng chung
Đầu tiên chúng ta hình dung những cái gì mình sẽ hay dùng chung? Các bạn liệt kê thử xem. Như mình thấy thì có: đọc file excel, 1 phần của validate lỗi, xuất file excel, ghi lỗi vào cell, xóa comment đã ghi nếu file đã có, xử lý lỗi trả về cho nhiều ngôn ngữ khác nhau…
Vậy nên mình sẽ tạo 1 file abstract class tại các module nào cần api import excel thì mình extends lôi ra dùng.
Abstract class sẽ nhận 2 Object làExcelImportDTO ( là dto của các trường có trong file import excel) và Object Entity.
@Getter
@Slf4j
public abstract class ImportExcelValidator<S, T> {
}
@Getter và @Slf4j cho bạn nào chưa biết là mình dùng lombok các bạn tra google rồi đọc thêm nhé. Cái này có nhiều rồi, cũng dễ thôi.
Tiếp theo chúng ta cần xác định sẽ khai bảo những gì. Mình xác định cái căn bản trước, quá trình làm mà thiếu thì thêm.
@Getter
@Slf4j
public abstract class ImportExcelValidator<S, T> {
private final List<S> sourceObjects;
private final List<T> destinationObjects;
private final MultipartFile multipartFile;
private final int headerStart;
private final Locale locale;
private final int lastCellNum;
private final List<ErrorDetail> errorsDetail;
}
Chúng ta có thể thấy:
- sourceObjects là DTO của file excel nhận dữ liệu .
- destinationObjects là object entity đây là kết quả cuối cùng mình nhận được để saveAll.
- multipartFile thằng này sẽ nhận file mình tải lên
- headerStart là số dòng bắt đầu trong excel
- locale dùng để xác định loại ngôn ngữ mà lỗi sẽ trả về ghi vào file
- lastCellNum là số cột kết thúc trong excel
- errorsDetail là object sẽ thể hiện số dòng bị lỗi và thông tin lỗi
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class ErrorDetail {
private int index;
private String message;
}
Xử lý đọc data từ file excel
TrongImportExcelValidator.class chúng ta xử lý đọc excel bằng thư viện poiji như sau:
public ImportExcelValidator(MultipartFile file, int headerStart, Locale locale, int lastCellNum) {
// Convert Excel to Object
PoijiOptions options = PoijiOptions.PoijiOptionsBuilder.settings().rawData(true).trimCellValue(true).headerStart(headerStart).dateFormatter(DateTimeFormatter.ofPattern(Constants.PRINT_DATE_FORMAT)).build();
try {
Class<S> sourceType = (Class<S>) ((ParameterizedType)getClass().getGenericSuperclass()).getActualTypeArguments()[0];
this.sourceObjects = Poiji.fromExcel(file.getInputStream(), PoijiExcelType.XLSX, sourceType, options);
} catch (IOException e) {
// add return lỗi
}
this.errorsDetail = new ArrayList<>();
this.destinationObjects = new ArrayList<>();
this.multipartFile = file;
this.headerStart = headerStart;
this.locale = locale;
this.lastCellNum = lastCellNum;
}
Mình sử dụng thư viện Poiji nên các bạn vào link trên để đọc thêm nhé. Nếu các bạn muốn check xem định dạng ngày tháng đã đúng hay chưa thì có thể tìm hiểu thêm, code trong phần withCasting của Poiji.
public abstract ImportExcelValidator<S, T> validate();
/**
* Xử lý data khi đã pass qua hết tất cả các validate
* @return
*/
public abstract List<T> thenMappingAndReturnDestinationObjects();
Hàm trên mình để validate cho từng object import excel khác nhau.
Xử lý validate trong DTO
Trong DTO chúng ta thường có các validate như: @NotBank, @NotNull, @Size, @Digits … hay là một Annotation mà bạn tự viết. Annotation hay lắm các bạn thử học viết 1 số cái đơn giản xem, rất tiện khi dung luôn. Ở bài viết khác mình sẽ hướng dẫn các bạn viết 1 số Annotation đơn giản mà rất hay sử dụng trong import excel, các bạn theo dõi tại đây nhé.
Ví dụ một dto import excel như này:
@Data
@NoArgsConstructor
@AllArgsConstructor
@ExcelCheckDuplication
public class ExcelDTO {
public ExcelDTO (String code, String name) {
this.code = code;
this.name = name;
}
@ExcelCell(0)
@NotBlank(message = "name_empty")
@Size(max = 200, message = "name_validation_size")
public String name;
@ExcelCell(1)
@NotBlank(message = "code_not_empty")
@Size(max = 255, message = "code_validation_size")
public String code;
@ExcelCell(2)
@NotNull(message = "max_value_empty")
@Digits(integer = 9, fraction = 2, message = "max_value_invalid")
public BigDecimal maxValue;
}
Một số điểm đáng chú ý:
- @ExcelCheckDuplication là annotation mình tự viết để check duplication
- @ExcelCell(0) : là Annotation của poiji dùng để xác định vị trí cột trong excel ( bạn cũng có thể xác định bằng tên, poiji có hỗ trợ hết)
- message : là nội dung lỗi sẽ trả về nếu data không pass qua được. Hiện tại các lỗi này mình đang set là key ví dụ: code_validation_size key này có thể dịch sang nhiều ngôn ngữ khác nhau. Nội dung key sẽ lấy từ resources/i18n trong project spring boot của bạn.
Tiếp theo mình sẽ viết hàm xử lý chung cho tất cả các phần này. Sau mình chỉ việc bắt validate trong dto, nếu còn sót cái nào thì mình kế thừa lại validate() trong ImportExcelValidator.class mình đã viết để xử lý.
/**
*
* @return
*/
public ImportExcelValidator<S, T> validatePre() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
ReloadableResourceBundleMessageSource messageSource = messageSource();
int temp = 0;
for (S dto: this.getSourceObjects()) {
ErrorDetail errorDetail = new ErrorDetail();
List<String> comments = new ArrayList<>();
temp++;
// Pre validation
Set<ConstraintViolation<S>> violations = validator.validate(dto);
if (violations.size() > 0) {
for (ConstraintViolation<S> violation: violations) {
comments.add(messageSource.getMessage(violation.getMessage(), null, locale));
}
errorDetail.setIndex(temp);
errorDetail.setMessage(String.join(", ", comments));
errorsDetail.add(errorDetail);
}
}
return this;
}
private static ReloadableResourceBundleMessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:i18n/messages");
messageSource.setFallbackToSystemLocale(false);
messageSource.setDefaultEncoding(String.valueOf(StandardCharsets.UTF_8));
messageSource.setCacheSeconds(0);
return messageSource;
}
Để có thể kiểm tra xem data có pass qua các annotation đã check và lấy ra message không thì mình dùng ValidatorFactory ( bạn có thể xem thêm tại baeldung.com ).
entityErrorsDetail mình cũng đã khai báo rồi giờ mình chỉ việc add số dong bị lỗi và message vào list nữa thôi. ReloadableResourceBundleMessageSource mình dùng để xử lý đa ngôn ngữ nhé.
Vậy là chúng ta đã xong phần đọc file excel trong java và 1 nửa của validate dữ liệu. Vì sao validate data mới được 1 nửa, tại mỗi api import excel thì sẽ có những validate đặc thù riêng như lấy data từ database check tồn tại, các thứ nữa rất nhiều. Nên tới đây mình chỉ xong được 1 nửa validate thôi.
Xử lý validate không phải trong DTO
Khi mình kế thừa class ImportExcelValidator.class thì mình cũng kế thừa lại hàm validate() để sử dụng. Kết quả hàm này trả về là gì?
Hàm validate() sẽ return 1 List<String> keyComments lỗi và số dòng bị lỗi. Công việc tiếp theo chúng ta chỉ cần add vào errorsDetail.
/**
* @param keyComments key messenger error
* @param temp row error
* @return
*/
public void errorMessengers(List<String> keyComments, int temp) {
List<String> comments = new ArrayList<>();
ReloadableResourceBundleMessageSource messageSource = messageSource();
for (String str: keyComments) {
comments.add(messageSource.getMessage(str, null, locale));
}
String comment = String.join(", ", comments);
if (!ObjectUtils.isEmpty(comment)) {
ErrorDetail errorDetail = new ErrorDetail(temp, comment);
this.getErrorsDetail().add(errorDetail);
}
}
Xuất Dữ Liệu Ra File Excel Trong Java
Ý tưởng của mình là lấy luôn data từ HttpServletResponse response mình sẽ chỉ xóa comment và style đang có trong file. Sau đó set comment và style mới vào là được.
/**
* @param sheet workbook
* @return
*/
public void removeComment(XSSFSheet sheet) {
// Get comment exists
Map<CellAddress, XSSFComment> oldComments = sheet.getCellComments();
Cell cell;
for (Map.Entry<CellAddress, XSSFComment> oldComment: oldComments.entrySet()) {
cell = sheet.getRow(oldComment.getKey().getRow()).getCell(oldComment.getKey().getColumn());
cell.removeCellComment();
cell.getCellStyle().setFillForegroundColor(IndexedColors.WHITE.getIndex());
}
}
public void setCommentStyle(XSSFWorkbook workbook, XSSFSheet sheet, Map<Integer, String> mapErrorDetail) {
Row row; Cell cell; Comment comment;
CellStyle cellStyle = workbook.getCellStyleAt(0);
cellStyle.setFillForegroundColor(IndexedColors.CORAL.getIndex());
cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
cellStyle.setBorderBottom(BorderStyle.THIN);
cellStyle.setBorderRight(BorderStyle.THIN);
CreationHelper factory = workbook.getCreationHelper();
ClientAnchor anchor = factory.createClientAnchor();
for (Map.Entry<Integer, String> errorDetail: mapErrorDetail.entrySet()) {
int rowNumber = errorDetail.getKey() + headerStart;
row = sheet.getRow(rowNumber);
int lastColumn = Math.max(row.getLastCellNum(), lastCellNum);
RichTextString errorMessenger = factory.createRichTextString(errorDetail.getValue());
for (int i = 0; i < lastColumn; i++) {
cell = row.getCell(i, Row.MissingCellPolicy.CREATE_NULL_AS_BLANK);
cell.setCellStyle(cellStyle);
comment = sheet.createDrawingPatriarch().createCellComment(anchor);
comment.setString(errorMessenger);
cell.setCellComment(comment);
}
}
}
Trên là delete comment in file excel và set lại background cho ô, tiếp theo là mình ghi lại ra file excel và trả về cho người dùng.
/**
*
* @param response
* @return
*/
public ImportExcelValidator<S, T> thenReturnExcelIfError(HttpServletResponse response) {
if (!ObjectUtils.isEmpty(errorsDetail)) {
try {
XSSFWorkbook workbook = new XSSFWorkbook(multipartFile.getInputStream());
XSSFSheet sheet = workbook.getSheetAt(0);
// remove comment
this.removeComment(sheet);
// map row index and message error
Map<Integer, String> mapErrorDetail = errorsDetail.stream().collect(Collectors.toMap(ErrorDetail::getIndex, ErrorDetail::getMessage));
// Set comment and style in sheet
this.setCommentStyle(workbook, sheet, mapErrorDetail);
response.setHeader("Content-Disposition", "attachment; filename=" + multipartFile.getOriginalFilename() + ".xlsx");
ServletOutputStream out = response.getOutputStream();
workbook.write(out);
out.flush();
out.close();
} catch (IOException e) {
// add return Error when create new file excel
}
// add return validate error
}
return this;
}
Áp dụng Abstract class vào Import excel
Do bài này cũng hơi dài nên mình sẽ tách thành 2 bài nhé. Phần 2 Áp dụng Abstract class và project code mẫu
Cảm ơn các bạn đã ghé thăm. Chúc các bạn thành công!