近期有個需求,說是要用到excel導入導出,一般我們的想法都是按照行數,於是實現了,后面發現公司需求的是列讀,甚至不規則的單個excel的讀。於是就用poi自己寫了按照單元格讀的實現。
一、按行讀
想起了之前用到的poi,經過搜索發現,開源的項目中有比較好的封裝poi的框架,一個是阿里出的easyExcel,另一個是easypoi,感覺使用起來都很方便。網上說easyExcel能解決大文件內存溢出問題,於是項目中就使用easyExcel了。
簡單普及下easyExcel原理,不做底層碼農,了解點上層設計有好處:
easyExcel核心原理
寫有大量數據的xlsx文件時,POI為我們提供了SXSSFWorkBook類來處理,這個類的處理機制是當內存中的數據條數達到一個極限數量的時候就flush這部分數據,再依次處理余下的數據,這個在大多數場景能夠滿足需求。
讀有大量數據的文件時,使用WorkBook處理就不行了,因為POI對文件是先將文件中的cell讀入內存,生成一個樹的結構(針對Excel中的每個sheet,使用TreeMap存儲sheet中的行)。
如果數據量比較大,則同樣會產生java.lang.OutOfMemoryError: Java heap space錯誤。POI官方推薦使用“XSSF and SAX(event API)”方式來解決。
分析清楚POI后要解決OOM有3個關鍵.
- 讀取的數據轉換流程
- easyexcel解析數據的 設計思想和相關角色。
根據上面官網給的信息,我們得有個模型來接收每行的數據,本例用CommonUser
對象,該對象上在這上面也可以加數據校驗,還需要個解析每個行的監聽器CommonUserListener
,可以來處理每行的數據,然后進行數據庫操作讀寫。
來個小demo(用的mybatis-plus框架)
controler
@RestController
@RequestMapping("info/commonuser")
public class CommonUserController {
@Autowired
private CommonUserService commonUserService;
/**
* excel導入(按照行讀)
* <p>
* 1. 創建excel對應的實體對象 參照{@link CommonUser}
* <p>
* 2. 由於默認一行行的讀取excel,所以需要創建excel一行一行的回調監聽器,參照{@link CommonUserListener}
* <p>
* 3. 直接讀即可
*/
@PostMapping("upload")
@ResponseBody
public String upload(MultipartFile file) throws IOException {
EasyExcel.read(file.getInputStream(), CommonUser.class, new CommonUserListener(commonUserService))
.sheet()
.doRead();
return "success";
}
/**
* 文件下載(失敗了會返回一個有部分數據的Excel)
* <p>
* 1. 創建excel對應的實體對象 參照{@link CommonUser}
* <p>
* 2. 設置返回的 參數
* <p>
* 3. 直接寫,這里注意,finish的時候會自動關閉OutputStream,當然你外面再關閉流問題不大
*/
@GetMapping("download")
public void download(HttpServletResponse response) throws IOException {
// 這里注意 有同學反應使用swagger 會導致各種問題,請直接用瀏覽器或者用postman
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
// 這里URLEncoder.encode可以防止中文亂碼 當然和easyexcel沒有關系
String fileName = URLEncoder.encode("用戶表", "UTF-8");
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
EasyExcel.write(response.getOutputStream(), CommonUser.class).sheet("模板").doWrite(data());
}
/**
* excel導出
* 文件下載並且失敗的時候返回json(默認失敗了會返回一個有部分數據的Excel)
*
* @since 2.1.1
*/
@GetMapping("downloadFailedUsingJson")
public void downloadFailedUsingJson(HttpServletResponse response) throws IOException {
// 這里注意 有同學反應使用swagger 會導致各種問題,請直接用瀏覽器或者用postman
try {
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
// 這里URLEncoder.encode可以防止中文亂碼 當然和easyexcel沒有關系
String fileName = URLEncoder.encode("測試", "UTF-8");
response.setHeader("Content-disposition", "attachment;filename=" + fileName + ".xlsx");
// 這里需要設置不關閉流
EasyExcel.write(response.getOutputStream(), CommonUser.class).autoCloseStream(Boolean.FALSE).sheet("模板")
.doWrite(data());
} catch (Exception e) {
// 重置response
response.reset();
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
Map<String, String> map = new HashMap<String, String>();
map.put("status", "failure");
map.put("message", "下載文件失敗" + e.getMessage());
response.getWriter().println(new Gson().toJson(map));
}
}
private List<CommonUser> data() {
List<CommonUser> list = commonUserService.list();
return list;
}
}
CommonUserService (和讀excel無關,業務需要)
/**
* 用戶Service
*
* @author hfl 690328661@qq.com
* @date 2020-05-16 08:42:50
*/
public interface CommonUserService extends IService<CommonUser> {
}
CommonUserServiceImpl
@Service("commonUserService")
public class CommonUserServiceImpl extends ServiceImpl<CommonUserMapper, CommonUser> implements CommonUserService {
private final static Logger logger = LoggerFactory.getLogger(CommonUserServiceImpl.class);
}
CommonUserListener(負責獲取每行的數據,然后根據需要進行db保存)
public class CommonUserListener extends AnalysisEventListener<CommonUser> {
private static final Logger LOGGER = LoggerFactory.getLogger(TestController.class);
/**
* 每隔5條存儲數據庫,實際使用中可以3000條,然后清理list ,方便內存回收
* mybatis-plus 默認1000
*/
private static final int BATCH_COUNT = 1000;
List<CommonUser> list = new ArrayList<>();
/**
* 假設這個是一個DAO,當然有業務邏輯這個也可以是一個service。當然如果不用存儲這個對象沒用。
*/
private CommonUserService commonUserService;
public CommonUserListener(CommonUserService commonUserService) {
// 這里是demo,所以隨便new一個。實際使用如果到了spring,請使用下面的有參構造函數
this.commonUserService = commonUserService;
}
/**
* 每隔5條存儲數據庫,實際使用中可以3000條,然后清理list ,方便內存回收
*/
@Override
public void invoke(CommonUser data, AnalysisContext context) {
LOGGER.info("解析到一條數據:{}", new Gson().toJson(data));
list.add(data);
// 達到BATCH_COUNT了,需要去存儲一次數據庫,防止數據幾萬條數據在內存,容易OOM
if (list.size() >= BATCH_COUNT) {
saveData();
// 存儲完成清理 list
list.clear();
}
}
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 這里也要保存數據,確保最后遺留的數據也存儲到數據庫
saveData();
LOGGER.info("所有數據解析完成!");
}
/**
* 加上存儲數據庫
*/
private void saveData() {
LOGGER.info("{}條數據,開始存儲數據庫!", list.size());
commonUserService.saveBatch(list);
LOGGER.info("存儲數據庫成功!");
}
}
CommonUserMapper(數據庫操作的)
/**
* 用戶表
*
* @author hfl
* @date 2020-05-16 08:42:50
*/
@Mapper
public interface CommonUserMapper extends BaseMapper<CommonUser> {
}
實體對象
@Data
@TableName("common_user")
public class CommonUser extends BaseEntity {
/**
* 用戶ID
*/
@TableId
private String userId;
/**
* 用戶名
*/
@ExcelProperty("字符串標題")
private String userName;
/**
* 真實姓名
*/
private String userRealname;
/**
* 密碼
*/
private String userPassword;
/**
* 鹽
*/
private String userSalt;
/**
* 手機號碼
*/
private String userMobile;
/**
* 性別
*/
private String userSex;
/**
* 頭像url
*/
private String userAvatar;
/**
* 電子郵箱
*/
private String userEmail;
/**
* 賬號狀態(0-正常,1-凍結)
*/
private Integer userStatus;
/**
* 擴展字段
*/
private String userEx;
@Override
public String toString() {
return "CommonUser{" +
"userId='" + userId + '\'' +
", userName='" + userName + '\'' +
", userRealname='" + userRealname + '\'' +
", userPassword='" + userPassword + '\'' +
", userSalt='" + userSalt + '\'' +
", userMobile='" + userMobile + '\'' +
", userSex='" + userSex + '\'' +
", userAvatar='" + userAvatar + '\'' +
", userEmail='" + userEmail + '\'' +
", userStatus=" + userStatus +
", userEx='" + userEx + '\'' +
'}';
}
}
數據庫表結構:
CREATE TABLE `common_user` (
`user_id` varchar(32) NOT NULL COMMENT '用戶ID',
`user_name` varchar(50) DEFAULT NULL COMMENT '用戶名',
`user_realname` varchar(50) DEFAULT NULL COMMENT '真實姓名',
`user_password` varchar(50) DEFAULT NULL COMMENT '密碼',
`user_salt` varchar(50) DEFAULT NULL COMMENT '鹽',
`user_mobile` varchar(20) DEFAULT NULL COMMENT '手機號碼',
`user_sex` varchar(20) DEFAULT NULL COMMENT '性別',
`user_avatar` varchar(255) DEFAULT NULL COMMENT '頭像url',
`user_email` varchar(50) DEFAULT NULL COMMENT '電子郵箱',
`user_status` tinyint(1) DEFAULT NULL COMMENT '賬號狀態(0-正常,1-凍結)',
`user_ex` text COMMENT '擴展字段',
`creater` varchar(32) DEFAULT NULL COMMENT '創建者',
`modifier` varchar(32) DEFAULT NULL COMMENT '修改者',
`create_time` datetime DEFAULT NULL COMMENT '創建時間',
`update_time` datetime DEFAULT NULL COMMENT '更新時間',
`is_deleted` tinyint(1) DEFAULT NULL COMMENT '0:未刪除,1:刪除',
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用戶表';
啟動項目:
使用postman先把本地的數據庫數據導出到excel中試試:
瀏覽器中輸入: localhost:7000/info/commonuser/downloadFailedUsingJson
可以看出數據庫中的被導出來了: 標題頭和實體中的@ExcelProperty對應:這里只寫了字符串標題這個表頭,其他的會默認顯示英文字段。
按行讀
將剛才數據庫中的數據全部刪除:導入剛才的Excel數據試試
使用post測試:
可以看到控制已經顯示成功,
看下數據庫:顯示已經按照行讀,並寫入數據庫中了
二、按列讀(按單元格讀:適合模板型的excel)
上面的導入excel數據是按照行讀的,但是我的需求是這樣的:
列形式,很多個sheet都不一樣,怎么處理呢?
於是想到,easyExcel肯定是實現不了了,干脆使用poi自帶的按照單元格自己去讀。
思路: 每個模板excel對應一個實體,每個單元格的位置(行號和列號)在實體上通過注解對應好,這樣我解析單元格,取出每個單元格的值,和位置,賦值給對應的實體的屬性。
解析單元格的小demo
因為easyExcel默認引用poi,所以不需要引maven包,直接寫就好了:
@Test
public void AllExcelRead() throws IOException, InvalidFormatException {
//1、指定要讀取EXCEL文檔名稱
String filename="C:\\Users\\69032\\Desktop\\vechicleService.xlsx";
//2、創建輸入流
FileInputStream input=new FileInputStream(filename);
//3、通過工作簿工廠類來創建工作簿對象
Workbook workbook= WorkbookFactory.create(input);
//4、獲取工作表 (可以按照sheet名字,也可以按照sheet的索引)
String sheetName = "車輛信息備案申請";
Sheet sheet=workbook.getSheet(sheetName);
// Sheet sheet=workbook.getSheet("工作表1");
// Sheet sheet = workbook.getSheetAt(0);
//5、獲取行
Row row=sheet.getRow(53);
//6、獲取單元格
Cell cell=row.getCell(2);
//7、讀取單元格內容
System.out.println(cell.getStringCellValue());
}
運行結果如下:
完美找到excel對應單元格的數據:
實戰:
定義個屬性上的注解,指定某個單元格的屬性:
RecordTemplate
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RecordTemplate {
//行號
int rowNo();
//列號
int columnNo();
//是否必填
FillCommentEnum comment();
//單元格名稱
String name();
//數據類型
}
是否必填的枚舉:
FillCommentEnum
/**
* Title: FillCommentEnum
* Description: 單元格 填充枚舉
*
* @author hfl
* @version V1.0
* @date 2020-05-29
*/
public enum FillCommentEnum {
FILL(0, "必填"),
EMPTY(1, "非必填");
private int code;
private String description;
FillCommentEnum(int code, String description) {
this.code = code;
this.description = description;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
接下來,我需要定義實體和模板對應。
@Data
@TableName("vehicle_static_info")
public class VehicleStaticInfo extends BaseEntity {
/**
* 車輛ID(主鍵)
*/
@TableId
private String vehicleId;
/**
* 最大基准扭矩
*/
private String engineMaxTorque;
/**
* 車牌號碼((GB17691-2005必填,GB17691-2018選填))
* 此字段,數據庫中已經移除,提取到車輛公共信息表中了
*/
@RecordTemplate(rowNo = 3, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "車牌號碼")
@TableField(exist = false)
private String vehicleLicense;
/**
* 車牌顏色(車牌顏色)
*/
@RecordTemplate(rowNo = 4, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "車牌顏色")
private String licenseColor;
/**
* 車體結構((編碼見說明3))
*/
@RecordTemplate(rowNo = 5, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "車體結構")
private Integer vehicleStructure;
/**
* 車輛顏色
*/
@RecordTemplate(rowNo = 6, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "車輛顏色")
private String vehicleColor;
/**
* 核定載重
*/
@RecordTemplate(rowNo = 7, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "核定載重")
private Double vehicleLoad;
/**
* 車輛尺寸mm(長)(32960、17691等)
*/
@RecordTemplate(rowNo = 8, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "車輛尺寸mm(長)")
private Double vehicleLong;
/**
* 車輛尺寸mm(寬)(國六,新能源等字典)
*/
@RecordTemplate(rowNo = 9, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "車輛尺寸mm(寬)")
private Double vehicleWide;
/**
* 車輛尺寸mm(高)
*/
@RecordTemplate(rowNo = 10, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "車輛尺寸mm(高)")
private Double vehicleHigh;
/**
* 總質量
*/
@RecordTemplate(rowNo = 11, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "總質量")
private Double grossVehicleMass;
/**
* 車輛類型((編碼見說明1))
*/
@RecordTemplate(rowNo = 12, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "車輛類型(編碼見右面1)")
private Integer vehicleType;
/**
* 行業類型((編碼見說明8 http://www.stats.gov.cn/tjsj/tjbz/hyflbz/201905/P020190716349644060705.pdf))
*/
@RecordTemplate(rowNo = 13, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "行業類型(編碼見右面8)")
private String industryType;
/**
* 車輛型號((GB17691-2005必填,GB17691-2018選填))
*/
@RecordTemplate(rowNo = 14, columnNo = 2, comment = FillCommentEnum.FILL, name = "車輛型號(GB17691-2005必填,GB17691-2018選填)")
@NotEmpty(message = "車輛型號不能為空", groups = {ImportGroup.class})
private String vehicleModel;
/**
* 購買時間
*/
@RecordTemplate(rowNo = 15, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "購買時間")
private String buyingDate;
/**
* 車架號VIN((必填))
*/
@RecordTemplate(rowNo = 16, columnNo = 2, comment = FillCommentEnum.FILL, name = "車架號VIN(必填)")
@NotEmpty(message = "車架號VIN不能為空", groups = {ImportGroup.class})
@TableField(exist = false)
private String vehicleFrameNo;
/**
* 行駛證號
*/
@RecordTemplate(rowNo = 17, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "行駛證號")
private String drivingLicenseNo;
/**
* 發動機型號((GB17691-2005必填,GB17691-2018選填))
*/
@RecordTemplate(rowNo = 18, columnNo = 2, comment = FillCommentEnum.FILL, name = "發動機型號(GB17691-2005必填,GB17691-2018選填)")
@NotEmpty(message = "發動機型號不能為空", groups = {ImportGroup.class})
private String engineModel;
/**
* 發動機編號
*/
@RecordTemplate(rowNo = 19, columnNo = 2, comment = FillCommentEnum.FILL, name = "發動機編號")
@NotEmpty(message = "發動機編號不能為空", groups = {ImportGroup.class})
private String engineNo;
/**
* 車籍地
*/
@RecordTemplate(rowNo = 20, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "車籍地")
private String vehiclePlace;
/**
* 車輛技術等級((編碼見說明2))
*/
@RecordTemplate(rowNo = 21, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "車輛技術等級(編碼見右面2)")
private Integer technicalLevel;
/**
* 出廠日期
*/
@RecordTemplate(rowNo = 22, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "出廠日期")
private String productionDate;
/**
* 等級評定日期
*/
@RecordTemplate(rowNo = 23, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "等級評定日期")
private String gradeAssessmentDate;
/**
* 二級維護日期
*/
@RecordTemplate(rowNo = 24, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "二級維護日期")
private String twoMaintenanceDate;
/**
* 二級維護狀態((編碼見說明5))
*/
@RecordTemplate(rowNo = 25, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "二級維護狀態(編碼見右面5)")
private Integer twoMaintenanceStatus;
/**
* 年審狀態((編碼見說明4))
*/
@RecordTemplate(rowNo = 26, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "年審狀態(編碼見右面4)")
private Integer yearEvaluationStatus;
/**
* 年檢有效期
*/
@RecordTemplate(rowNo = 27, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "年檢有效期")
private String yearinspectionPeriod;
/**
* 保險有效期
*/
@RecordTemplate(rowNo = 28, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "保險有效期")
private String insurancePeriod;
/**
* 保養有效期
*/
@RecordTemplate(rowNo = 29, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "保養有效期")
private String maintenancePeriod;
/**
* 所屬單位名稱
*/
@RecordTemplate(rowNo = 30, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "所屬單位名稱")
private String enterpriseName;
/**
* 車輛聯系人
*/
@RecordTemplate(rowNo = 31, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "車輛聯系人")
private String contactsName;
/**
* 車輛聯系電話
*/
@RecordTemplate(rowNo = 32, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "車輛聯系電話")
private String contactPhone;
/**
* 車輛sim卡號
*/
@RecordTemplate(rowNo = 33, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "車輛sim卡號")
private String terminalSim;
/**
* 車輛注冊時間
*/
@RecordTemplate(rowNo = 34, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "車輛注冊時間")
private String registerDate;
/**
* 所屬組織ID
*/
@RecordTemplate(rowNo = 35, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "所屬組織ID")
private String organizationId;
/**
* 環保局車輛類型((編碼見說明6))
*/
@RecordTemplate(rowNo = 36, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "環保局車輛類型(編碼見右面6)")
private Integer epaVehicleType;
/**
* 運輸局車輛類型((編碼見說明7))
*/
@RecordTemplate(rowNo = 37, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "運輸局車輛類型(編碼見右面7)")
private Integer transVehicleType;
/**
* 所有綁定的sim卡
*/
@RecordTemplate(rowNo = 38, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "所有綁定的sim卡")
private String terminalAllSim;
/**
* 所有者地址
*/
@RecordTemplate(rowNo = 39, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "所有者地址")
private String ownerAddress;
/**
* 車牌型號
*/
@RecordTemplate(rowNo = 40, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "車牌型號")
private String licenseModel;
/**
* 行政區划
*/
@RecordTemplate(rowNo = 41, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "行政區划")
private String administrativeArea;
/**
* 行政地址
*/
@RecordTemplate(rowNo = 42, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "行政地址")
private String administrativeAddress;
/**
* 總客數
*/
@RecordTemplate(rowNo = 43, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "總客數")
private Integer totalnumberGuest;
/**
* 整備質量
*/
@RecordTemplate(rowNo = 44, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "整備質量")
private Double curbWeight;
/**
* 列車最大總質量
*/
@RecordTemplate(rowNo = 45, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "列車最大總質量")
private Double maximumTotalMassOfTrain;
/**
* 入網證號
*/
@RecordTemplate(rowNo = 46, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "入網證號")
private String netNumber;
/**
* 初次登記日期
*/
@RecordTemplate(rowNo = 47, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "初次登記日期")
private String initialRegistrationDate;
/**
* 年檢日期
*/
@RecordTemplate(rowNo = 48, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "年檢日期")
private String annualInspectionDate;
/**
* 強制報廢日期
*/
@RecordTemplate(rowNo = 49, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "強制報廢日期")
private String mandatoryScrapDate;
/**
* 所屬企業簡稱
*/
@RecordTemplate(rowNo = 50, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "所屬企業簡稱")
private String enterpriseShortName;
/**
* 車輛SN
*/
@RecordTemplate(rowNo = 51, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "車輛SN")
private String vehicleSn;
/**
* 安全芯片型號((車載終端含有安全芯片的必填))
*/
@RecordTemplate(rowNo = 52, columnNo = 2, comment = FillCommentEnum.EMPTY, name = "安全芯片型號(車載終端含有安全芯片的必填)")
private String chipType;
/**
* 車載終端型號((必填))
*/
@RecordTemplate(rowNo = 53, columnNo = 2, comment = FillCommentEnum.FILL, name = "車載終端型號(必填)")
@NotEmpty(message = "車載終端型號不能為空", groups = {ImportGroup.class})
private String tboxType;
/**
* 激活模式((編碼見說明9,必填)0-無需激活,1-需要激活)
*/
@RecordTemplate(rowNo = 54, columnNo = 2, comment = FillCommentEnum.FILL, name = "激活模式(編碼見右面9,必填)")
@NotEmpty(message = "激活模式不能為空", groups = {ImportGroup.class})
private Integer vehRegisterMode;
/**
* 排放水平((編碼見說明10,必填)(1-國六,2-國五,3-國四,4-國三,5-國二,6-排氣后處理系統改裝車輛))
*/
@RecordTemplate(rowNo = 55, columnNo = 2, comment = FillCommentEnum.FILL, name = "排放水平(編碼見右面10,必填)")
@NotEmpty(message = "放水平不能為空", groups = {ImportGroup.class})
private Integer emissionlLevelType;
/**
*
* 整車生產企業
*/
@RecordTemplate(rowNo = 56, columnNo = 2, comment = FillCommentEnum.FILL, name = "整車生產企業")
@NotEmpty(message = "整車生產企業不能為空", groups = {ImportGroup.class})
private String vehicleFirm;
/**
* 安全芯片編號(備案不需要)
*/
private String chipCode;
/**
* 備注
*/
private String remark;
/**
* 車輛備案結果(0:草稿;1:待審核;2:未備案 3:審核通過4:審核未通過)
* @return
*/
@TableField(exist = false)
private Integer recordResult;
解析工具類(最重要)
思路: 遍歷實體類上的有@ExcelTemplate注解的屬性,有的話說明事由單元格和它對應的,把該屬性的行號和列號傳遞給 解析單元格數據的方法,返回單元格的值,再通過java的 Filed的反射機制,給類賦值,就獲取了excel中所有值了。在解析單元格的時候根據是否必填,也可以提前拋出異常。
解析單元格的相關方法:
/**
* 獲取sheet對象
*/
public static Sheet getSheetByStream(InputStream inputStream, String sheetName) {
Workbook workbook = null;
try {
workbook = WorkbookFactory.create(inputStream);
} catch (Exception e) {
throw new ServiceException("excel文件有誤");
}
Sheet sheet = null;
if (StringUtils.isBlank(sheetName)) {
//取第一個
sheet = workbook.getSheetAt(0);
} else {
sheet = workbook.getSheet(sheetName.trim());
}
return sheet;
}
/**
* 讀取單個單元格的值
*
* @param sheet
* @param name
* @param rowNo
* @param columnNo
*/
public static String readCell(Sheet sheet, int rowNo, int columnNo, FillCommentEnum comment, String name) {
//5、獲取行
Row row = sheet.getRow(rowNo);
//6、獲取單元格
Cell cell = row.getCell(columnNo);
//7、讀取單元格內容
String stringCellValue = getCellValueByType(cell, name);
if (comment.getCode() == 0 && StringUtils.isBlank(stringCellValue)) {
throw new ServiceException(name + "不能為空");
}
logger.info(stringCellValue);
return stringCellValue;
}
public static String getCellValueByType(Cell cell,String name){
String cellValue = "";
if(cell.getCellTypeEnum() == CellType.NUMERIC){
if (HSSFDateUtil.isCellDateFormatted(cell)) {
cellValue = DateFormatUtils.format(cell.getDateCellValue(), "yyyy-MM-dd");
} else {
NumberFormat nf = NumberFormat.getInstance();
cellValue = String.valueOf(nf.format(cell.getNumericCellValue())).replace(",", "");
}
logger.info(cellValue);
}else if(cell.getCellTypeEnum() == CellType.STRING){
cellValue = String.valueOf(cell.getStringCellValue());
}else if(cell.getCellTypeEnum() == CellType.BOOLEAN){
cellValue = String.valueOf(cell.getBooleanCellValue());
}else if(cell.getCellTypeEnum() == CellType.ERROR){
cellValue = "錯誤類型";
throw new ServiceException("單元格"+name+": "+cellValue);
}
return cellValue;
}
反射給對象賦值的相關方法:
/**
* 解析excel備案數據到對象
*
* @return
* @throws ClassNotFoundException
* @throws IllegalAccessException
* @throws InstantiationException
*/
public static Object parseExcelToModel(String className, Sheet sheet) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
Class<?> clazz = Class.forName(className);
Field[] declaredFields = clazz.getDeclaredFields();
Object o = clazz.newInstance();
//獲取excel的流,前端傳入
for (Field field: declaredFields) {
field.setAccessible(true);
if (field.isAnnotationPresent(RecordTemplate.class)) {
RecordTemplate annotation = field.getAnnotation(RecordTemplate.class);
Class<?> type = field.getType();
logger.info(type.getName());
//單元格的值
String value = ReadExcellCellUtils.readCell(sheet, annotation.rowNo(), annotation.columnNo(), annotation.comment(), annotation.name());
//通過反射把 單元格的值 給對象屬性
setFieldValue(o, field, type, value);
}
}
return o;
}
/**
* 通過反射把 單元格的值 給對象屬性
* @param o
* @param field
* @param type
* @param value
* @throws IllegalAccessException
*/
private static void setFieldValue(Object o, Field field, Class<?> type, String value) throws IllegalAccessException {
Object targetValue = null;
if (StringUtils.isEmpty(value)) {
return;
}
if (type.equals(Integer.class)) {
targetValue = Integer.parseInt(value);
} else if (type.equals(Double.class)) {
targetValue = Double.parseDouble(value);
} else if (type.equals(Float.class)) {
targetValue = Float.parseFloat(value);
} else if (type.equals(Boolean.class)) {
targetValue = Boolean.getBoolean(value);
}else{
targetValue = value;
}
field.set(o, targetValue);
}
/**
* 解析數據到model
*
* @param className 類名
* @param inputStream 輸入流
* @param sheetName sheetname 可以為null,為null時取第一頁
* @return 映射對象
*/
public static Object parseExcelToModel(String className, InputStream inputStream, String sheetName) {
Sheet sheetByStream = getSheetByStream(inputStream, sheetName);
try {
return parseExcelToModel(className, sheetByStream);
} catch (Exception e) {
e.printStackTrace();
throw new ServiceException("解析數據到model失敗");
}
}
如何使用:
提供這3個值就可以了。
舉例說明:
測試:
使用postman請求: excel的數據都獲取到了:
自此,按照單元格的例子就完成了,這種適合不規則的excel,而且行數確定,特別是針對模板excel的讀都是很通用的。
總結:本文主要實現了easyExcel按照行數的例子,還有自己封裝的針對通用不規則excel按照列(確切是按照單元格)讀的例子。如有問題或者錯誤,歡迎留言討論。
個人微信公眾號:
搜索: 怒放de每一天
不定時推送相關文章,期待和大家一起成長!!
完
謝謝大家支持!