0.介绍
Apache POI 是基于 Office Open XML 标准(OOXML)和 Microsoft 的 OLE 2 复合文档格式(OLE2)处理各种文件格式的开源项目。 简而言之,您可以使用 Java 读写 MS Excel 文件,可以使用 Java 读写 MS Word 和 MS PowerPoint 文件。
1.准备
1.1.poi依赖包
操作高版本的excel,word和ppt,需要引入
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
操作低版本的excel
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.1.2</version>
</dependency>
操作低版本的word和ppt
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
<version>4.1.2</version>
</dependency>
1.2.基本操作
@RequestMapping("/poiDemo/uploadStudentData")
@ResponseBody
public String uploadStudentData(@RequestParam("file") MultipartFile excelFile) throws IOException {
//根据 MultipartFile 创建 工作簿对象
XSSFWorkbook workbook = new XSSFWorkbook(excelFile.getInputStream());
// 得到 sheet工作表 数量
int numberOfSheets = workbook.getNumberOfSheets();
for (int i = 0; i < numberOfSheets; i++) {
// 根据 序号 得到 sheet工作表
XSSFSheet sheet = workbook.getSheetAt(i);
//获取有数据的最后一行
int lastRowNum = sheet.getLastRowNum();
for (int j = 0; j <= lastRowNum; j++) {
// 得到 row
XSSFRow row = sheet.getRow(j);
// 得到 row 行号
int rowNum = row.getRowNum();
System.out.println("每 " + rowNum + " 行");
// 获取有数据的最后一个单元格数
int lastCellNum = row.getLastCellNum();
for (int k = 0; k < lastCellNum; k++) {
XSSFCell cell = row.getCell(k);
//TRUE或FALSE:BOOLEAN
//公式:FORMULA
//没有填值(空单元格):BLANK
//数字(整数或小数):NUMERIC
//字符串:STRING
String str = "";
switch (cell.getCellType()) {
case BOOLEAN:
str = String.valueOf(cell.getBooleanCellValue());
break;
case FORMULA:
str = cell.getCellFormula();
break;
case BLANK:
str = "";
break;
case NUMERIC:
if (DateUtil.isCellDateFormatted(cell)) {
LocalDateTime date = DateUtil.getLocalDateTime(cell.getNumericCellValue());
str = date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
} else {
str = String.format("%f", cell.getNumericCellValue());
}
break;
case STRING:
str = cell.getStringCellValue();
break;
default:
//_NONE, ERROR, BLANK 都按空来处理
break;
}
System.out.print("\t 每" + k + "格:" + cell.getCellType() + "= " + str);
}
System.out.println();
}
}
System.out.println("numberOfSheets = " + numberOfSheets);
return "ok";
}
1.3.页面样式
//1、创建workbook,对应一个excel
XSSFWorkbook wb = new XSSFWorkbook();
//1.5、生成excel中可能用到的单元格样式
//首先创建字体样式
//XSSFFont font = wb.createFont();//创建字体样式
//font.setFontName("宋体");//使用宋体
//font.setFontHeightInPoints((short) 12);//字体大小
//font.setBold(true);
然后创建单元格样式style
//XSSFCellStyle style1 = wb.createCellStyle();
//style1.setFont(font);//将字体注入
//style1.setWrapText(true);// 自动换行
//style1.setAlignment(HorizontalAlignment.CENTER);// 左右居中
//style1.setVerticalAlignment(VerticalAlignment.CENTER);// 上下居中
//style1.setFillForegroundColor(IndexedColors.WHITE.getIndex());// 设置单元格的背景颜色
//style1.setFillPattern(FillPatternType.SOLID_FOREGROUND);
//style1.setBorderTop(BorderStyle.THIN);// 边框的大小
//style1.setBorderBottom(BorderStyle.THIN);
//style1.setBorderLeft(BorderStyle.THIN);
//style1.setBorderRight(BorderStyle.THIN);
//2、生成一个sheet,对应excel的sheet,参数为excel中sheet显示的名字
XSSFSheet sheet = wb.createSheet(fileName);
//3、设置sheet中每列的宽度,第一个参数为第几列,0为第一列;第二个参数为列的宽度,可以设置为0。
// Test中有三个属性,因此这里设置三列,第0列设置宽度为0,第1~3列用以存放数据
//for (int i = 0; i < dataTempList.size(); i++) {
// sheet.setColumnWidth(i, 20*256);
//}
//4、生成sheet中一行,从0开始
XSSFRow row = sheet.createRow(0);
//row.setHeight((short) 500);// 设定行的高度//5、创建row中的单元格,从0开始
//得到标题行
for (int i = 0; i < dataTempList.size(); i++) {
XSSFCell cell = row.createCell(i);
cell.setCellValue(dataTempList.get(i).get("title").toString());
//cell.setCellStyle(style1);
}
2.上传文件页面
2.0.下载文本模版
通过 a 超链接 下载在 /static/temp/student.xlsx 文件
<a th:href="@{/static/temp/student.xlsx}" >
下载模板
</a>
2.1.指定浏览的文件类型
<input type='file'>
上面的input标签用来浏览本地文件, 但是默认情况下能看到所有文件, 如果想只看到电子表格文件,应该怎么办?
只看到高版本的电子表格
<input type="file" name="file" accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet">
高低版本都能看到
<input type="file" name="file" accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel">
2.2.上传前的准备-编写页面js脚本
给form添加id值,方便抓取form表单,
给input添加name用来上传文件
给button添加单击事件, 用来驱动上传的脚本代码
<form id="fileForm" class="form form-inline" >
<input type="file" name="file" accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet">
<button onclick="importData()" type="button" class="btn btn-sm btn-primary">上传数据</button>
</form>
function importData(){
var v = $("[name='file']").val();
if (v === "") {
layer.msg("请先选择要上传的文件");
return;
}
var formData = new FormData($("#fileForm")[0]);
$.ajax({
type: "post",
url: "/student/uploadStudentData",
data: formData,
contentType: false,
processData: false,
dataType: "json",
async: true,
cache: false,
beforeSend: function(){layer.load();},
success: function(result){
//提示: 提示成功的行数, 如果有失败的, 提示第几行失败.
layer.alert(result.msg, function(index){
location.reload();
});
},
error: function(){layer.alert("请求失败");},
complete: function(){layer.closeAll("loading");}
});
}
3.服务器处理
3.1.封装Excel工具类
都按字符串来读取单元格内容
public class ExcelUtils {
public static String readData(XSSFCell cell){
if (cell == null) {
return "";
}
String str = "";
switch (cell.getCellType()) {
case BOOLEAN:
str = String.valueOf(cell.getBooleanCellValue());
break;
case FORMULA:
str = cell.getCellFormula();
break;
case NUMERIC:
if (DateUtil.isCellDateFormatted(cell)) {
LocalDateTime date = DateUtil.getLocalDateTime(cell.getNumericCellValue());
str = date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
} else {
str = String.format("%f", cell.getNumericCellValue());
}
break;
case STRING:
str = cell.getStringCellValue();
break;
default:
//_NONE, ERROR, BLANK 都按空来处理
break;
}
return str;
}
}
3.2.上传并解析电子表格
控制层接值, 变量的名称和页面的name属性要相同, 都叫file
@PostMapping("/student/uploadStudentData")
@ResponseBody
public ResultJson uploadStudentData(MultipartFile file) throws IOException{
if (file.isEmpty()) {
return ResultJson.error("文件不能为空");
}
return studentService.uploadStudentData(file);
}
数据传给service, 解析电子表格
序号 | 姓名 | 年龄 | 出生日期 | 体重 | 学号 | 性别 | 所属班级 |
---|---|---|---|---|---|---|---|
1 | 李小三 | 14 | 2012-11-22 | 58.4705 | ST403190003 | 女 | 序幕之星 |
2 | 赵小四 | 17 | 1976-09-15 | 801.9828 | ST400190005 | 男 | 魔法班 |
3 | 刘小翠 | 123 | 1993-08-02 | 30.8466 | ST403190007 | 女 | 序幕之星 |
4 | 4 | 1993-11-29 | 435.2564 | ST402190008 | 女 | 策略组 | |
5 | 武小奎 | 5 | 1995-02-15 | 912.907 | ST403190009 | 男 | 序幕之星 |
6 | 孙小六 | 1.2 | 1965-06-201 | 62.8663苛 | ST403190031 | 男 | 序幕之星 |
7 | 王小二 | 12 | 1984-11-273 | 68.7244 | ST399190034 | 男 | 科技班 |
8 | 闩小七 | 12 | 1992-01-11 | 65.43a我24 | ST399190037 | 男 | 科技班 |
9 | 汪小汪 | 89 | 2003-07-26 | 86.6427 | ST399190038 | 男 | 科技班 |
10 | 李小龙 | 42 | 2009-12-19 | 88.7301 | ST400190039 | 男12 | 魔法班 |
11 | 地小芳 | 120 | 2018-12-05 | 123.45 | ST399190041 | 女 | 科技班12 |
业务: 姓名不能为空, 出生日期不能为空, 出生日期格式要正确
@Autowired
private TaaClassTeamService taaClassTeamService;
@Override
@Transactional(rollbackFor = {Exception.class, Error.class})
public ResultJson uploadStudentData(MultipartFile file) throws IOException {
//1, 工作簿
XSSFWorkbook workbook = new XSSFWorkbook(file.getInputStream());
//2, 工作表
XSSFSheet sheet = workbook.getSheetAt(0);
//3, 行
int lastRowNum = sheet.getLastRowNum();
//成功的行数
int successCount = 0;
//记录错误的原因
StringBuilder error = new StringBuilder();
//班级主键
Integer ctId = null;
//4, 单元格
for (int i = 1; i <= lastRowNum; i++) {
XSSFRow row = sheet.getRow(i);
String name = ExcelUtils.readData(row.getCell(1));
String age = ExcelUtils.readData(row.getCell(2));
String birth = ExcelUtils.readData(row.getCell(3));
String weight = ExcelUtils.readData(row.getCell(4));
String sn = ExcelUtils.readData(row.getCell(5));
String sex = ExcelUtils.readData(row.getCell(6));
String ctName = ExcelUtils.readData(row.getCell(7));
if(name.isEmpty() && age.isEmpty() && birth.isEmpty()
&& weight.isEmpty() && sn.isEmpty() && sex.isEmpty() && ctName.isEmpty()){
continue;
}
//开关变量, 假设没有错误
boolean bj = false;
//行内错误收集器
StringBuilder lineError = new StringBuilder("第" + i + "行:→");
if ("".equals(name)) {
bj = true;
lineError.append("姓名不能为空;");
}
if ("".equals(age)) {
bj = true;
lineError.append("年龄不能为空;");
}else if(!Pattern.matches("\\d+", age)){
bj = true;
lineError.append("年龄必须为整型;");
}else if(!Pattern.matches("^[1][2-9]|[2-9][0-9]|1[01][0-9]|120$", age)){
bj = true;
lineError.append("年龄必须为12~120岁之间;");
}else{
if (age.contains(".")) {
age = age.substring(0, age.indexOf("."));
}
}
if ("".equals(birth)) {
bj = true;
lineError.append("出生日期不能为空;");
} else if(!Pattern.matches("\\d{4}-\\d{2}-\\d{2}( \\d{2}:\\d{2}:\\d{2})?", birth)){
bj = true;
lineError.append("出生日期格式错误;");
} else {
//保留yyyy-MM-dd格式, 如果是yyyy-MM-dd HH:mm:ss则去掉HH:mm:ss
if (birth.contains(" ")) {
birth = birth.split(" ")[0];
}
try {
//只能解析 yyyy-MM-dd格式,其他格式都会报错
LocalDate.parse(birth);
} catch (Exception e) {
bj = true;
lineError.append("出生日期格式错误;");
}
}
if ("".equals(weight)) {
bj = true;
lineError.append("体重不能为空;");
}else if(!Pattern.matches("\\d+(\\.\\d+)?", weight)){
bj = true;
lineError.append("体重必须为数值类型;");
}
if ("".equals(sn)) {
bj = true;
lineError.append("学号不能为空;");
} else if(sn.length()>11){
bj = true;
lineError.append("学号的长度不能大于11位;");
}
if ("".equals(sex)) {
bj = true;
lineError.append("性别不能为空;→");
} else if(!"男".equals(sex) && !"女".equals(sex)){
bj = true;
lineError.append("性别必须是男或者女;");
}
if ("".equals(ctName)) {
bj = true;
lineError.append("班级不能为空;");
} else {
LambdaQueryWrapper<TaaClassTeam> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(TaaClassTeam::getCtName, ctName).last("limit 1");
TaaClassTeam classes = taaClassTeamService.getOne(queryWrapper);
if (classes == null) {
bj = true;
lineError.append("所属班级不存在;");
} else {
ctId = classes.getCtId();
}
}
if (!bj) {
successCount++;
//存储数据
StudentEntity s = new StudentEntity();
//判断名字是否重复, 如果重复则添加A,B,C等
String newName = checkRepeatName(name);
s.setStuName( newName );
s.setStuInAge(Integer.parseInt(age));
LocalDate parse = LocalDate.parse(birth);
Date date = Date.from(parse.atStartOfDay(ZoneId.systemDefault()).toInstant());
s.setStuBirth(date);
s.setStuWeight(Double.parseDouble(String.valueOf(weight)));
s.setStuSn(sn);
s.setStuSex("男".equals(sex) ? "1" : "0");
s.setCtId(ctId);
s.setStuInTime(new Date());
s.setStuFaceUrl("");
super.baseMapper.insert(s);
} else {
//存储错误信息
error.append("<br>").append(lineError);
}
}
return ResultJson.error("成功加入"+successCount+"条<br>"+error.toString());
}
3.3.重复名称处理
重复处理
/**
* 不能重名, 王小二 , 王小二A , 王小二B...
* 王小二Z, 王小二AA, 王小二AB...王小二AZ, 王小二AAA
*/
private String checkRepeatName(String name) {
List<StudentEntity> list = this.baseMapper.selectList(new LambdaQueryWrapper<StudentEntity>()
.likeRight(StudentEntity::getStuName, name).orderByDesc(StudentEntity::getStuName));
System.out.println("list 1=>>> " + list);
if (list != null && list.size()>0) {
list = list.stream().sorted((s1,s2)->{
return s2.getStuName().length() - s1.getStuName().length();
}).collect(Collectors.toList());
System.out.println("list 2=>>> " + list);
String stuName = list.get(0).getStuName();
char i = stuName.charAt(stuName.length() - 1) ;
System.out.println("i = " + i);
if ("Z".equals(String.valueOf(i))) {
stuName = stuName.substring(0, stuName.length() - 1) + "AA";
}else if(((int)i) >=65 && ((int)i)<=89){
char lastChar = (char)(i + 1);
System.out.println("lastChar = " + lastChar);
stuName = stuName.substring(0, stuName.length() - 1) + lastChar;
}else{
stuName += 'A';
}
return stuName;
}else{
return name;
}
}
或者通过 sql 统计姓名出现的次数
<select id="selectListByName" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from tab_student
where stu_name like concat(#{name},'%')
order by stu_name desc
</select>
3.4.java使用正则表达式的判断
if(Pattern.matches("正则表达式", "需要验证的字符串")){
//满足正则的验证
} else {
//不满足正则的验证
}
常用正则表达式
"\\d+" 整数
"^([1-9][0-9]\\*)+(\\.[0-9]{1,2})?$" 非零开头的最多带两位小数的数字
"^\\d+(\\.\\d+)?$" 非负浮点数
"^1[3-9]\d{9}$" 手机号码
"^[1][2-9]|[2-9][0-9]|1[01][0-9]|120$" 12~120
"^(((?:19|20)\d\d)-(0?[1-9]|1[0-2])-(0?[1-9]|[12][0-9]|3[01]))$" yyyy-MM-dd格式日期
"[\\u4e00-\\u9fa5]+" 汉字
例子, 整数的判断
// 4, 4.0, 5.000000 都是整数的话, 怎么验证
if(Pattern.matches("\\d+(\\.0+)?", age)){}