dom4j解析XML配置,Java反射與POI API實現Excel文件導出導入模板工具類(下)

POI API實現Excel文件導入到數據庫

上一篇博客筆者利用 POI API實現了Excel文件的導出功能,只是當時做的不是很完善,只是做了一個excel文件的後臺的接口,通過瀏覽器調用很容易實現,但是後來放到form表單的按鈕上實現時發現出了Bug,採用axios的post請求調用導出接口後一直出現瀏覽器沒有下載excel文件的Bug,後來改爲form表單提交發現最終解決了這一Bug。之後筆者會修改上一篇博客,實現在前臺頁面通過導出按鈕實現帶查詢條件的Excel文件導出功能。這篇博客筆者主要記錄自己通過工具類實現Excel文件的導入功能。
導入excel文件中的數據到數據庫需要經過以下流程才能實現,筆者使用processon在線製作流程圖畫了一個簡單的流程圖如下:
![excel文件導入到數據庫路程圖]
excel文件數據導入到數據庫流程圖

一、前端上傳文件UI界面實現

由於筆者最近兩年在公司裏完成的JavaWeb項目都是採用前後端分離架構模式,而且前後端分離也是目前Web項目開發的趨勢,因此個人的demo學習項目也同樣採用這一模式。參考vue官方文檔採用Vue CLI搭建前端項目腳手架的創建項目配置參考兩部分搭建了自己的前端項目
由於這篇博客主要側重於excel文件的導入實現邏輯,項目的搭建細節本人不贅述,官網的參考文檔都很詳細。本人的前端UI項目碼雲地址爲:https://gitee.com/heshengfu1211/vue-cli-project
感興趣的讀者可以使用git克隆下來參考具體代碼

Vue-Cli聯合Element-UI實現頁面展示

  1. 在vue-cli-demo項目的根目錄下打開一個git bash命令控制檯窗口
  2. 執行命令 npm install 安裝項目依賴(本項目使用的@vue/cli-service版本爲3.11.0,element-ui版本爲2.12)
  3. 執行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
        //     }
        // }
    }
}
  1. 編輯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')
  1. 配置頁面路由
	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')
	    }
	  ]
	})
  1. 使用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 然後回車,看到的頁面效果圖如下:
Home菜單主頁
點擊About菜單可看到文件上傳界面如下圖所示:
文件上傳界面
支持我們的前端vue項目算是搭建完了,下面來看看後臺接口如何實現Excel文件的導入

二、後臺接口實現

  1. 工具類讀取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;

    }
  1. 修改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直接界面修改
通過mysql-workbench UI界面修改表字段類型和增加字段
完成修改和添加字段後點擊apply按鈕即可完成修改表並生成相應的sql腳本
之後修改province_cities表中city_code和parent_code列的值全部改爲英文字符。

  1. 在之前的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;
        }

    }
  1. IExcelService接口中增加importExcel方法,代碼如下
ServiceResponse<String> importExcel(File file, boolean isExcel2003);
  1. 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;
    }
}


  1. 修改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";
    }
}
  1. 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>
  1. 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頁中編輯數據如下
userInfo Sheet數據
province_cities頁中編輯數據如下圖所示
province_cities Sheet頁數據
以上province_cities Sheet頁中還有一部分數據筆者就沒必要繼續截取出來了,格式都一樣,編輯完數據後點擊保存。

  1. 這裏需要注意在啓動後臺項目之前需要修改數據源連接的url屬性值,否則導入中文字符到數據庫後會出現亂碼,修改後的spring.datasource.url屬性值如下
 datasource:
    name: test
    url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF8
  1. 然後在前端頁面點擊上傳按鈕選中我的電腦 D:\excel目錄下的tbl_userInfo.xls文件,完成數據的導入,導入成功後接口的data會返回 import Excel success 信息
    另外我們也可以通過數據庫查看導入數據是否成功
  2. 這裏我們可以通過點擊IDEA中右側的Database新建MySql客戶端連接,需要輸入數據庫服務的IP、端口號、用戶名和密碼。你是用的什麼類型的數據庫就在左邊的Drivers選擇與之對應的數據庫驅動,筆者用的是MySql數據庫,所以選擇了MySql驅動。IDEA中創建數據庫客戶端的步驟如下面三幅示意圖所示:
    創建MySQL數據庫客戶端連接步驟1
    創建MySQL數據庫客戶端連接步驟2
    創建MySQL數據庫客戶端連接步驟3
    點擊Database菜單下帶有+號按鈕下面的倒三角形圖標會彈出一系列子菜單,然後選擇Data Source, 之後在其左邊會彈出可供你選擇的驅動集合,這裏筆者選擇的是MySQL,如果讀者服務端用的是Oracle, PostgreSQL, SQL Server數據,則選擇與之對應的驅動即可,IDEA對目前大多數數據庫的驅動都有提供。然後在彈出的數據庫客戶端連接配置對話框中選擇General頁籤,然後輸入數據庫服務所在的主機名、端口號(默認爲3306)、數據庫實例名(筆者用的是MySQL數據庫安裝時自動創建的test數據庫實例,讀者也可以使用自創建的數據庫實例)、用戶名和登錄密碼。然後點擊TestConnnection按鈕,如上圖按鈕右側出現綠色的Successful信息則顯示測試通過,說明我們配置的數據庫客戶端可用了。
  3. 然後點擊對話框右下角的OK按鈕,我們建立的數據連接客戶端會話就會出現在Database欄下面,選中數據庫連接會話後右鍵->Open Console 打開一個命令控制檯窗口
    MySQL-Seesion
    Open Console
    這時我們就可以在輸入SQL語句後,選中需要執行的SQL語句,然後點擊下圖中左上角的綠色三角按鈕執行SQL腳本。
    命令控制檯執行SQL腳本
    你會發現IDEA的這個數據庫客戶端比Mysql-Workbench要好用的多。
    我們輸入 select * from userinfo 命令,然後執行,命令控制檯的下方會以表格的形式顯示查詢結果集,如下圖所示:
    userInfo表查詢結果集
    通過user_account或nick_name任何一列與updated_time兩列中的信息與導入的Excel文件中userInfo Sheet中的數據對比我們可以看出userInfo Sheet頁中的數據導入成功了;
    同理 執行 select * from province_cities; SQL語句可查看province_cities Sheet頁中的數據是否導入成功了。
    province_cities表查詢結果集

總結

這篇博客筆者通過Vue-Cli搭建了一個簡易的前端項目,並整合element-ui庫使我們具備一個能夠上傳文件的UI界面。後臺導入Excel問件的接口中主要完成了以下三件事情:
(1)上傳Excel問件到服務器目錄下;
(2)使用POI API 讀取傳到服務器上的Excel問件中的數據,並通過反射的方式將數據轉成實體類集合,這一步是關鍵,確定單元格中的數據類型很重要,否則程序容易在這一步拋異常;
(3)調用Dao將解析後的實體類結果集插入或更新到數據庫,這一步如果涉及到項目部署到多臺服務器,且存在多個客戶同時導入數據的情況是需要加分佈式鎖的。

xml配置文件存儲了要導入excel文件轉換的實體類信息源數據,其實還有另一中方式,也是在SpringBoot項目中廣泛使用的,那就是註解的形式。讀者有空可以自己嘗試,筆者未來也會嘗試採用這種方式動態獲取實體類及屬性名等信息。
後臺SpringBoot項目 gitee地址如下:https://gitee.com/heshengfu1211/mybatisProjectDemo
筆者已提交最新代碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章