POI API实现Excel文件导入到数据库
上一篇博客笔者利用 POI API实现了Excel文件的导出功能,只是当时做的不是很完善,只是做了一个excel文件的后台的接口,通过浏览器调用很容易实现,但是后来放到form表单的按钮上实现时发现出了Bug,采用axios的post请求调用导出接口后一直出现浏览器没有下载excel文件的Bug,后来改为form表单提交发现最终解决了这一Bug。之后笔者会修改上一篇博客,实现在前台页面通过导出按钮实现带查询条件的Excel文件导出功能。这篇博客笔者主要记录自己通过工具类实现Excel文件的导入功能。
导入excel文件中的数据到数据库需要经过以下流程才能实现,笔者使用processon在线制作流程图画了一个简单的流程图如下:
![excel文件导入到数据库路程图]
一、前端上传文件UI界面实现
由于笔者最近两年在公司里完成的JavaWeb项目都是采用前后端分离架构模式,而且前后端分离也是目前Web项目开发的趋势,因此个人的demo学习项目也同样采用这一模式。参考vue官方文档采用Vue CLI搭建前端项目脚手架的创建项目和配置参考两部分搭建了自己的前端项目
由于这篇博客主要侧重于excel文件的导入实现逻辑,项目的搭建细节本人不赘述,官网的参考文档都很详细。本人的前端UI项目码云地址为:https://gitee.com/heshengfu1211/vue-cli-project
感兴趣的读者可以使用git克隆下来参考具体代码
Vue-Cli联合Element-UI实现页面展示
- 在vue-cli-demo项目的根目录下打开一个git bash命令控制台窗口
- 执行命令
npm install
安装项目依赖(本项目使用的@vue/cli-service版本为3.11.0,element-ui版本为2.12) - 执行
touch vue.config.js
命令创建vue.config.js文件,使用VS Code打开项目目录下的vue.config.js文件后进行配置代码编辑,配置后的代码如下
module.exports = {
/**
* 默认情况下,Vue CLI 会假设你的应用是被部署在一个域名的根路径上,例如 https://www.my-app.com/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。
* 例如,如果你的应用被部署在 https://www.my-app.com/my-app/,则设置 publicPath 为 /my-app/。
*/
publicPath: '/',
//输出目录
outputDir: process.env.NODE_ENV==='production'?'dist/prod':'dist/dev',
//静态文件目录
assetsDir: 'static',
lintOnSave: process.env.NODE_ENV!=='production',
productionSourceMap: process.env.NODE_ENV!=='production',
devServer: {
// C:\Windows\System32\drivers\etc\hosts 文件中配置 127.0.0.1 vue.dev.com
//host: process.env.NODE_ENV==='production'? 'vue.prod.com': 'vue.dev.com',
//由于笔者的jenkins服务占用了8080端口,因此前端项目端口改为3000,后端的spring-boot项目改为8081端口
port: 3000,
/**
* 设置代理,解决跨域问题;也可以在服务端解决
*/
// proxy:{
// '/api':{
// target: 'http://localhost:8080/springboot',
// changeOrigin: true
// }
// }
}
}
- 编辑src/main.js文件,导入element-ui所有组件
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store';
import ElementUI from 'element-ui';
import {Message} from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
Vue.config.productionTip = false;
// Vue.config.devtools=true;
Vue.prototype.message = Message;
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
- 配置页面路由
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export default new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [
{
path: '/',
name: 'home',
component: () => import('./views/Home.vue')
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('./views/About.vue')
}
]
})
- 使用element-ui库的upload组件,编辑src/views/about.vue文件,实现文件上传客户端功能,代码如下:
<template>
<div class="about">
<el-upload class="uploadDemo"
ref="upload"
drag
name="uploadFile"
action="http://localhost:8081/springboot/importExcel"
:before-upload="beforeUpload"
:on-progress="onProgress" >
<i class="el-icon-upload"></i>
<div class="el-upload__text">将文件拖到此处,或<em>上传excel文件</em></div>
<div class="el-upload__tip" slot="tip">只能上传xls/xlsx文件,且不超过1MB</div>
</el-upload>
<!-- action对应的值为后台导入接口url-->
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'about',
data(){
return {
}
},
methods:{
//上传之前对文件格式进行校验
beforeUpload(file){
console.log(file);
const name = file.name+'';
const flag = name.endsWith(".xlsx") || name.endsWith("xls");
if(!flag){
return false;
}
},
onProgress(event,file){
console.log("action: "+this.$refs.upload.action);
console.log(file);
}
}
}
</script>
关于element-ui库的upload组件使用说明API文档,读者可参考链接:https://element.eleme.cn/#/zh-CN/component/upload
6 回到项目根目录下的控制台,执行 npm run serve
命令运行本地开发环境
服务器起来后控制台会显示如下信息:
App running at:
- Local: http://localhost:3000/
- Network: http://192.168.1.102:3000/
Note that the development build is not optimized.
To create a production build, run npm run build
在浏览器中输入 http://localhost:3000 然后回车,看到的页面效果图如下:
点击About菜单可看到文件上传界面如下图所示:
支持我们的前端vue项目算是搭建完了,下面来看看后台接口如何实现Excel文件的导入
二、后台接口实现
- 工具类读取Excel文件中数据方法的实现
在之前的ExcelReadWriteUtil类中增加readExcel方法,解析Excel文件中的数据后返回一个Map<String,List<?>> 类型的对象,key值存放sheet名,value存放该sheet解析出来的数据集合
/**
*读取Excel中表格数据工具类
* @param sheetInfoMap
* @param file
* @param isExcel2003
* @return
* @throws Exception
*/
public static Map<String,List<?>> readExcel(Map<String,SheetInfo> sheetInfoMap, File file, boolean isExcel2003) throws Exception{
Map<String,List<?>> dataMap = new HashMap<>();
InputStream is = new FileInputStream(file);
Workbook workbook = null;
if(isExcel2003){ //如果文件后缀为.xls,则使用HSSFWorkbook类解析文件
workbook = new HSSFWorkbook(is);
}else{ //默认使用XSSFWorkbook类
workbook = new XSSFWorkbook(is);
}
Set<String> sheetNames = sheetInfoMap.keySet();
for(String sheetName: sheetNames){
Sheet sheet = workbook.getSheet(sheetName);
int rowNumbers = sheet.getPhysicalNumberOfRows();
SheetInfo sheetInfo = sheetInfoMap.get(sheetName);
List<ColumnInfo> columns = sheetInfo.getColumns();
Class clazz = Class.forName(sheetInfo.getVoClass());
List<Object> list = new ArrayList<>();
for(int i=1;i<rowNumbers;i++){
Object instance = clazz.newInstance();
Row contentRow = sheet.getRow(i);
for(int j=0;j<columns.size();j++){
ColumnInfo columnInfo = columns.get(j);
String fieldName = columnInfo.getName();
String setMethodName = "set"+upCaseFirstChar(fieldName);
Field field = clazz.getDeclaredField(fieldName);
Class fieldClass = field.getType();
Method method = clazz.getDeclaredMethod(setMethodName,fieldClass);
Cell cell = contentRow.getCell(j);
switch (columnInfo.getType()){
case "String":
String value = cell.getStringCellValue();
method.invoke(instance,value);
break;
case "Integer":
String doubleStr = String.valueOf(cell.getNumericCellValue());
String intStr = doubleStr.substring(0,doubleStr.indexOf('.'));
Integer intVal = Integer.valueOf(intStr);
if("String".equals(fieldClass.getSimpleName())){
method.invoke(instance,intStr);
}else{
method.invoke(instance,intVal);
}
break;
case "Long":
String phoneStr = cell.getStringCellValue();
if(!StringUtils.isEmpty(phoneStr)){
Long longVal = Long.valueOf(phoneStr);
method.invoke(instance,longVal);
}
break;
case "Date":
Date birthDate = cell.getDateCellValue();
if(birthDate!=null){
String dateVal = sdf.format(birthDate);
method.invoke(instance,dateVal);
}
break;
default:
break;
}
}
list.add(instance);
}
dataMap.put(sheetName,list);
}
return dataMap;
}
- 修改importExample.xml
为了方便使用反射将excel表格单元格中解析出来的数据set到实体类对象中去,笔者对src/main/resources/excelConfig目录下的importExample.xml
中文件中的内容作了一定程度的修改,修改后的内容如下:
<?xml version="1.0" encoding="UTF-8" ?>
<workbook name="importExample">
<sheet name="userInfo" voClass="com.example.mybatis.model.UserTO">
<!--column中displayName的顺序必须与导入表格中的顺序一致 -->
<column name="userAccount" displayName="用户账号" type="String" />
<column name="password" displayName="密码" type="String"/>
<column name="nickName" displayName="昵称" type="String" />
<column name="deptNo" displayName="部门编号" type="Integer" />
<column name="phoneNum" displayName="手机号码" type="Long" />
<column name="emailAddress" displayName="邮箱地址" type="String" />
<column name="birthDay" displayName="出生日期" type="Date" />
<column name="updatedBy" displayName="更新人" type="String"/>
</sheet>
<sheet name="province_cities" voClass="com.example.mybatis.model.CityTO">
<column name="cityCode" displayName="城市编码" type="String" />
<column name="parentCode" displayName="父城市编码" type="String" />
<column name="cityName" displayName="城市名称" type="String" />
<column name="updatedBy" displayName="更新人" type="String"/>
</sheet>
</workbook>
同时修改SheetInfo实体类的columns属性的类型为List,修改后的SheetInfo如下
package com.example.mybatis.model;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
public class SheetInfo implements Serializable {
private String name;
private String voClass;
public SheetInfo(String name, String voClass) {
this.name = name;
this.voClass = voClass;
}
List<ColumnInfo> columns = new ArrayList<>();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getVoClass() {
return voClass;
}
public List<ColumnInfo> getColumns() {
return columns;
}
}
3.为了解决程序解析excel文件中的数据时当province_cities表中当parentCode字段值为0是总是报无法从Cell单元格中获取String类型的值异常,因为该列其他单元格都是String类型的值,而程序却默认把0当成了Numeric类型的值,因此不再使用-1,0分别代表国家和省的city_code值,city_code修改为VARCHAR类型,city_code和parent_code的值统一使用英文字符;另外为了方便查看数据入库的时间和操作人,给userInfo表和province_cities表均加上updated_by和updated_time两个字段。依次执行如下sql
ALTER TABLE `test`.`userinfo`
ADD COLUMN `updated_by` VARCHAR(20) NOT NULL DEFAULT 'system' COMMENT '更新人' after `birthDay`,
ADD COLUMN `updated_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间' after `updated_by`;
ALTER TABLE `test`.`province_cities` change `city_code` VARCAHR(20) NOT NULL;
ALTER TABLE `test`.`province_cities`
ADD COLUMN `updated_by` VARCHAR(20) NOT NULL DEFAULT 'system' COMMENT '更新人' AFTER `city_name`,
ADD COLUMN `updated_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间' AFTER `updated_by`;
也可以通过mysql-workbench客户端的UI直接界面修改
完成修改和添加字段后点击apply按钮即可完成修改表并生成相应的sql脚本
之后修改province_cities表中city_code和parent_code列的值全部改为英文字符。
- 在之前的ExcelController类中增加实现excel文件的上传功能的importExcel方法,代码如下:
@PostMapping("/importExcel")
public ServiceResponse<String> importExcel(MultipartFile uploadFile, HttpServletRequest request){
String requestUrl = request.getRequestURI();
logger.info("requestUrl={}",requestUrl);
String fileName = uploadFile.getOriginalFilename();
logger.info("fileName={}",fileName);
//先要实现文件上传到服务器
String workDir = System.getProperty("user.dir");
String user = (String) request.getSession().getAttribute("userName");
if(user==null){
user = "system";
}
String saveDir = workDir+"/src/main/resources/static/upload/"+user;
String savePath = saveDir;
File folder = new File(savePath);
if(!folder.isDirectory()){
folder.mkdirs();
}
String filename = uploadFile.getOriginalFilename();
try {
uploadFile.transferTo(new File(folder,filename));
StringBuilder builder = new StringBuilder(savePath);
builder.append('/').append(filename);
String filePath = builder.toString();
File excelFile = new File(filePath);
boolean isExcel2003 = false;
if(fileName.endsWith(".xls")){
isExcel2003 = true;
}
return excelService.importExcel(excelFile,isExcel2003);
} catch (IOException e) {
logger.error("upload file failed",e);
ServiceResponse<String> response = new ServiceResponse<String>();
response.setStatus(500);
response.setMessage("Inner Server Error,Caused by: "+e.getMessage());
return response;
}
}
- IExcelService接口中增加importExcel方法,代码如下
ServiceResponse<String> importExcel(File file, boolean isExcel2003);
- IExcelService接口的实现类ExcelService类中实现importExcel方法,并在ExcelService类中注入IExcelBusiness接口类的实现类实例,另外修改解析exportExample.xml和importExample.xml文件的逻辑不再放在静态代码块中实现,而在程序启动后第一次调用导出和导入接口时完成,修改后的ExcelService类代码如下:
package com.example.mybatis.service.impl;
import com.example.mybatis.business.IExcelBusiness;
import com.example.mybatis.business.IUserBusiness;
import com.example.mybatis.model.*;
import com.example.mybatis.service.IExcelService;
import com.example.mybatis.utils.ExcelReadWriteUtil;
import com.example.mybatis.utils.ReadWorkbookXmlUtil;
import org.dom4j.io.SAXReader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.*;
@Service("excelService")
public class ExcelService implements IExcelService {
private WorkbookInfo exportWorkbook = null;
private WorkbookInfo importWorkbook = null;
@Autowired
private IUserBusiness userBusiness;
@Autowired
private IExcelBusiness excelBusiness;
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
private static final Logger logger = LoggerFactory.getLogger(ExcelService.class);
private String exportFilePath = "src/main/resources/excelConfig/exportExample.xml";
private String importFilePath = "src/main/resources/excelConfig/importExample.xml";
@Override
public ServiceResponse<String> exportSheet(String sheetName, Integer page,Integer pageSize,HttpServletResponse response) {
logger.info("sheetName={}",sheetName);
ServiceResponse<String> returnResponse = new ServiceResponse<>();
Date now = new Date();
String dateTime = sdf.format(now);
//设置文件名
String fileName = sheetName+dateTime+".xlsx";
try {
response.setHeader("Content-Disposition","attachment;filename="+ URLEncoder.encode(fileName, "UTF-8"));
} catch (UnsupportedEncodingException e) {
returnResponse.setStatus(500);
returnResponse.setMessage(e.getMessage());
logger.error("encode fileName failed!",e);
return returnResponse;
}
if(exportWorkbook==null){
SAXReader saxReader = new SAXReader();
//读取导出xml配置
exportWorkbook = ReadWorkbookXmlUtil.readWorkbookXml(saxReader,exportFilePath);
}
SheetInfo sheetInfo = exportWorkbook.getSheetInfoMap().get(sheetName);
Map<String,SheetInfo> sheetInfoMap = new HashMap<>();
sheetInfoMap.put(sheetName,sheetInfo);
Map<String,List<?>> dataMap = new HashMap<>();
//计算查询起始索引
int startIndex = (page-1)*pageSize+1;
try {
List<UserTO> userTOList = userBusiness.selectAllUser(startIndex,pageSize);
dataMap.put(sheetName,userTOList);
//从HttpServletResponse对象中获取输出流
OutputStream os = response.getOutputStream();
ExcelReadWriteUtil.writeExcel(sheetInfoMap,dataMap,os);
returnResponse.setStatus(200);
returnResponse.setMessage("success");
returnResponse.setData("ok");
} catch (Exception e) {
returnResponse.setStatus(500);
returnResponse.setMessage("exportSheet failed:"+e.getMessage());
returnResponse.setData("error");
logger.error("exportSheet failed!",e);
}
return returnResponse;
}
@Override
public ServiceResponse<String> importExcel(File file, boolean isExcel2003) {
ServiceResponse<String> response = new ServiceResponse<>();
if(importWorkbook==null){
SAXReader saxReader = new SAXReader();
//读取导入xml配置
importWorkbook = ReadWorkbookXmlUtil.readWorkbookXml(saxReader,importFilePath);
}
if(importWorkbook==null){
throw new NullPointerException("importWorkbook instance cannot be null");
}
Map<String,SheetInfo> sheetInfoMap = importWorkbook.getSheetInfoMap();
//调用工具类解析出数据
Map<String,List<?>> dataMap = null;
try {
dataMap = ExcelReadWriteUtil.readExcel(sheetInfoMap,file,isExcel2003);
} catch (Exception e) {
logger.error("readExcel failed",e);
response.setStatus(500);
response.setMessage("Inner Server Error,caused by: "+e.getMessage());
return response;
}
try {
String importMsg = "";
if(dataMap!=null && dataMap.size()>0){
importMsg = excelBusiness.importExcelResolvedData(dataMap);
}
response.setStatus(200);
response.setMessage("ok");
response.setMessage(importMsg);
} catch (Exception ex) {
logger.error("importExcel failed",ex);
response.setStatus(500);
response.setMessage("Inner Server Error,caused by: "+ex.getMessage());
response.setData("importExcel failed");
}
return response;
}
}
- 修改UserTO和CityTO实体类
package com.example.mybatis.model;
import java.io.Serializable;
public class UserTO implements Serializable {
private Integer id;
private Integer deptNo;
private String deptName;
private String userAccount;
private String password;
private String nickName;
private String emailAddress;
private String birthDay;
private Long phoneNum;
private String updatedBy;
private String updatedTime;
public void setId(Integer id) {
this.id = id;
}
public Integer getId() {
return id;
}
public Integer getDeptNo() {
return deptNo;
}
public void setDeptNo(Integer deptNo) {
this.deptNo = deptNo;
}
public String getDeptName() {
return deptName;
}
public void setDeptName(String deptName) {
this.deptName = deptName;
}
public String getUserAccount() {
return userAccount;
}
public void setUserAccount(String userAccount) {
this.userAccount = userAccount;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getNickName() {
return nickName;
}
public void setNickName(String nickName) {
this.nickName = nickName;
}
public String getEmailAddress() {
return emailAddress;
}
public void setEmailAddress(String emailAddress) {
this.emailAddress = emailAddress;
}
public Long getPhoneNum() {
return phoneNum;
}
public void setPhoneNum(Long phoneNum) {
this.phoneNum = phoneNum;
}
public String getBirthDay() {
return birthDay;
}
public void setBirthDay(String birthDay) {
this.birthDay = birthDay;
}
public String getUpdatedBy() {
return updatedBy;
}
public void setUpdatedBy(String updatedBy) {
this.updatedBy = updatedBy;
}
public String getUpdatedTime() {
return updatedTime;
}
public void setUpdatedTime(String updatedTime) {
this.updatedTime = updatedTime;
}
}
package com.example.mybatis.model;
import java.io.Serializable;
public class CityTO implements Serializable {
private String cityCode;
private String parentCode;
private String cityName;
private String updatedBy;
private String updatedTime;
public String getCityCode() {
return cityCode;
}
public void setCityCode(String cityCode) {
this.cityCode = cityCode;
}
public String getParentCode() {
return parentCode;
}
public void setParentCode(String parentCode) {
this.parentCode = parentCode;
}
public String getCityName() {
return cityName;
}
public void setCityName(String cityName) {
this.cityName = cityName;
}
public String getUpdatedBy() {
return updatedBy;
}
public void setUpdatedBy(String updatedBy) {
this.updatedBy = updatedBy;
}
public String getUpdatedTime() {
return updatedTime;
}
public void setUpdatedTime(String updatedTime) {
this.updatedTime = updatedTime;
}
}
7.增加IExcelBusines接口类及其实现类ExcelBusines,实现数据入库逻辑,为了实现数据要么全部导入成功,要么全部导入失败,需要在方法上加上@Transactional注解,详细代码如下:
package com.example.mybatis.business.impl;
import com.example.mybatis.business.IExcelBusiness;
import com.example.mybatis.dao.ICityDao;
import com.example.mybatis.dao.IUserDao;
import com.example.mybatis.model.CityTO;
import com.example.mybatis.model.UserTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.thymeleaf.util.StringUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@Component
public class ExcelBusiness implements IExcelBusiness {
@Autowired
private IUserDao userDao;
@Autowired
private ICityDao cityDao;
@Override
@Transactional(rollbackFor ={Exception.class})
public String importExcelResolvedData(Map<String, List<?>> dataMap) throws Exception{
List<UserTO> userTOList = (List<UserTO>) dataMap.get("userInfo");
List<CityTO> cityTOList = (List<CityTO>) dataMap.get("province_cities");
List<String> userAccounts = new ArrayList<>();
userTOList.stream().forEach((item)->{
userAccounts.add(item.getUserAccount());
});
List<String> existAccounts = userDao.queryExistUserAccounts(userAccounts);
List<UserTO> insertUsers = new ArrayList<>();
List<UserTO> updateUsers = new ArrayList<>();
userTOList.forEach((item)->{
if(existAccounts.contains(item.getUserAccount())){ //用户名已存在则放入待更新列表,否则放入待插入列表
updateUsers.add(item);
}else{
if(!StringUtils.isEmpty(item.getUserAccount())){
insertUsers.add(item);
}
}
});
List<String> cityCodes = new ArrayList<>();
cityTOList.stream().forEach((item)->{
cityCodes.add(item.getCityCode());
});
List<String> existCityCodes = cityDao.queryExistCityCodes(cityCodes);
List<CityTO> addCityTOS = new ArrayList<>();
List<CityTO> updateCityTOS = new ArrayList<>();
cityTOList.stream().forEach((item)->{
if(!existCityCodes.contains(item.getCityCode())){ //城市代码已存在则放入更新列表,否则放入添加列表
addCityTOS.add(item);
}else{
if(!StringUtils.isEmpty(item.getCityCode())){
updateCityTOS.add(item);
}
}
});
if(insertUsers.size()>0){
userDao.batchAddUserInfo(insertUsers);
}
if(updateUsers.size()>0){
userDao.batchUpdateUserInfo(updateUsers);
}
if(addCityTOS.size()>0){
cityDao.batchAddCities(addCityTOS);
}
if(updateCityTOS.size()>0){
cityDao.batchUpdateCities(updateCityTOS);
}
return "import Excel success";
}
}
- IUserDao接口中增加
List<String> queryExistUserAccounts(List<String> userAccounts);
方法
IUserDao.xml映射文件中增加其对应的sql
<select id="queryExistUserAccounts" parameterType="java.util.List" resultType="java.lang.String">
select user_account from userinfo
where user_account in
<foreach collection="list" item="item" open="(" separator="," close=")">
#{item,jdbcType=VARCHAR}
</foreach>
</select>
- ICityDao接口中增加如下三个方法
List<String> queryExistCityCodes(List<String> cityCodes);
void batchAddCities(List<CityTO> cities);
void batchUpdateCities(List<CityTO> cities);
ICityDao.xml文件中其对应的sql如下
<select id="queryExistCityCodes" parameterType="java.util.List" resultType="java.lang.String">
select city_code from province_cities
where city_code in
<foreach collection="list" item="item" open="(" close=")" separator=",">
#{item,jdbcType=VARCHAR}
</foreach>
</select>
<insert id="batchAddCities" parameterType="java.util.List">
insert into province_cities(city_code, parent_code, city_name,updated_by)
<foreach collection="list" item="item" separator="union">
select #{item.cityCode,jdbcType=VARCHAR},
#{item.parentCode,jdbcType=VARCHAR},
#{item.cityName,jdbcType=VARCHAR},
#{item.updatedBy,jdbcType=VARCHAR}
from dual
</foreach>
</insert>
<update id="batchUpdateCities" parameterType="java.util.List">
<foreach collection="list" item="item" separator=";" close=";">
update province_cities
<trim prefix="set" suffixOverrides=",">
<if test="item.parentCode!=null and item.parentCode!='' ">parent_code=#{item.parentCode,jdbcType=VARCHAR},</if>
<if test="item.cityName!=null and item.cityName!='' ">city_name=#{cityName,jdbcType=VARCHAR},</if>
updated_by=#{item.updatedBy,jdbcType=VARCHAR}
</trim>
where city_code=#{cityCode,jdbcType=VARCHAR}
</foreach>
</update>
10 实现服务端跨域逻辑
10.1 新建拦截器WebCorsInterceptor并实现HandlerInterceptor接口
package com.example.mybatis.interceptors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
public class WebCorsInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(WebCorsInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
response.setCharacterEncoding("utf-8");
String method = request.getMethod();
String origin = request.getHeader("origin");
logger.info("method={},origin={}",method,origin);
//跨域处理
if(origin!=null){
response.setHeader("Access-Control-Allow-Origin",origin);
response.setHeader("Access-Control-Allow-Headers", "Content-Type,Content-Length, Authorization, Accept,Content-Disposition");
response.setHeader("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
response.setHeader("Access-Control-Allow-Credentials","true");
response.setHeader("X-Powered-By","Tomcat");
if("OPTIONS".equals(method)||"GET".equals(method)||"POST".equals(method)||"PUT".equals(method)){
return true;
}else{
return false;
}
}else{
return true;
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
10.2 configuration包中新建WebCorsConfiguration类,代码如下:
在这里插入代码片package com.example.mybatis.configuration;
import com.example.mybatis.interceptors.WebCorsInterceptor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebCorsConfiguration {
private static final Logger logger = LoggerFactory.getLogger(WebCorsConfiguration.class);
private String origin = "http://localhost:3000";
@Bean
public WebMvcConfigurer webMvcConfigure(){
return new WebMvcConfigurer(){
@Override
public void addCorsMappings(CorsRegistry registry) {
logger.info("配置跨域信息");
//添加映射路径
registry.addMapping("/springboot/**")
.allowedOrigins(origin) //放行哪些原始域
.allowCredentials(true) //是否发送Cookie信息
.maxAge(3600)
.allowedMethods("GET","POST","PUT","DELETE","OPTIONS") //放行哪些请求方式
.allowedHeaders("Content-Disposition","Authorization") //放行哪些请求头
.exposedHeaders("Content-Disposition"); //暴露哪些非默认的头部信息
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
logger.info("添加拦截器");
HandlerInterceptor interceptor = new WebCorsInterceptor();
registry.addInterceptor(interceptor);
}
};
}
}
三、编写数据测试导入Excel文件接口
1.使用微软的Excel办公软件新建文件命名为tbl_userinfo,并保存在我的电脑:D:/excel目录下,
双击打开空工作簿后新建两个sheet依次命名为userInfo和province_cities, userInfo Sheet页中编辑数据如下
province_cities页中编辑数据如下图所示
以上province_cities Sheet页中还有一部分数据笔者就没必要继续截取出来了,格式都一样,编辑完数据后点击保存。
- 这里需要注意在启动后台项目之前需要修改数据源连接的url属性值,否则导入中文字符到数据库后会出现乱码,修改后的spring.datasource.url属性值如下
datasource:
name: test
url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF8
- 然后在前端页面点击上传按钮选中我的电脑 D:\excel目录下的tbl_userInfo.xls文件,完成数据的导入,导入成功后接口的data会返回 import Excel success 信息
另外我们也可以通过数据库查看导入数据是否成功 - 这里我们可以通过点击IDEA中右侧的Database新建MySql客户端连接,需要输入数据库服务的IP、端口号、用户名和密码。你是用的什么类型的数据库就在左边的Drivers选择与之对应的数据库驱动,笔者用的是MySql数据库,所以选择了MySql驱动。IDEA中创建数据库客户端的步骤如下面三幅示意图所示:
点击Database菜单下带有+号按钮下面的倒三角形图标会弹出一系列子菜单,然后选择Data Source, 之后在其左边会弹出可供你选择的驱动集合,这里笔者选择的是MySQL,如果读者服务端用的是Oracle, PostgreSQL, SQL Server数据,则选择与之对应的驱动即可,IDEA对目前大多数数据库的驱动都有提供。然后在弹出的数据库客户端连接配置对话框中选择General页签,然后输入数据库服务所在的主机名、端口号(默认为3306)、数据库实例名(笔者用的是MySQL数据库安装时自动创建的test数据库实例,读者也可以使用自创建的数据库实例)、用户名和登录密码。然后点击TestConnnection按钮,如上图按钮右侧出现绿色的Successful信息则显示测试通过,说明我们配置的数据库客户端可用了。 - 然后点击对话框右下角的OK按钮,我们建立的数据连接客户端会话就会出现在Database栏下面,选中数据库连接会话后右键->Open Console 打开一个命令控制台窗口
这时我们就可以在输入SQL语句后,选中需要执行的SQL语句,然后点击下图中左上角的绿色三角按钮执行SQL脚本。
你会发现IDEA的这个数据库客户端比Mysql-Workbench要好用的多。
我们输入 select * from userinfo 命令,然后执行,命令控制台的下方会以表格的形式显示查询结果集,如下图所示:
通过user_account或nick_name任何一列与updated_time两列中的信息与导入的Excel文件中userInfo Sheet中的数据对比我们可以看出userInfo Sheet页中的数据导入成功了;
同理 执行 select * from province_cities; SQL语句可查看province_cities Sheet页中的数据是否导入成功了。
总结
这篇博客笔者通过Vue-Cli搭建了一个简易的前端项目,并整合element-ui库使我们具备一个能够上传文件的UI界面。后台导入Excel问件的接口中主要完成了以下三件事情:
(1)上传Excel问件到服务器目录下;
(2)使用POI API 读取传到服务器上的Excel问件中的数据,并通过反射的方式将数据转成实体类集合,这一步是关键,确定单元格中的数据类型很重要,否则程序容易在这一步抛异常;
(3)调用Dao将解析后的实体类结果集插入或更新到数据库,这一步如果涉及到项目部署到多台服务器,且存在多个客户同时导入数据的情况是需要加分布式锁的。
xml配置文件存储了要导入excel文件转换的实体类信息源数据,其实还有另一中方式,也是在SpringBoot项目中广泛使用的,那就是注解的形式。读者有空可以自己尝试,笔者未来也会尝试采用这种方式动态获取实体类及属性名等信息。
后台SpringBoot项目 gitee地址如下:https://gitee.com/heshengfu1211/mybatisProjectDemo
笔者已提交最新代码