学生通讯录
項目簡介
這是一個前后端分離的項目,基本技術棧是SpringBoot+SpringDataJPA+EasyExcel+Vue+ElementUI。這里說明一下,Easy Excel是阿里巴巴一個開源項目,可以方便的將數據庫數據導出為Excel,也可以根據提供的模板將Excel中的數據導入到數據庫中。SpringDataJPA(Hibernate)使用起來比較方便,可以動態拼寫sql語句,當然感興趣的朋友也可以使用MyBatis,這里不再贅述。這個項目可以:
1、使用SpringDataJPA實現基本的增刪改查,分頁查詢(每頁10條數據,自動計算頁數)
2、動態查詢(給出的查詢條件不確定個)
3、模糊查詢(關鍵字查詢,比如給我一個“王”字可以找出所有姓王的同學)
4、將數據庫數據導出為Excel并下載到本地,也可以根據模板將本地Excel中的數據導入到數據庫,學號已存在的信息不會導入數據庫。
5、根據不同的專業查找信息
6、備份功能:本項目有設置回收站,當從Student表中刪除數據后會保存在回收站中,還可以恢復或者徹底刪除。
7、添加信息檢查:如果學號已經存在,或者郵箱、手機不合法則不能插入信息,會彈出警告;生日自動計算,選擇專業時選項是從數據庫中查詢出來的,而不是自己錄入。
項目展示:
代碼目錄:
以下內容是項目說明,具體源碼請移步GitHub
https://github.com/Nown1/contacts
跨域訪問問題
這是一個前后端分離的項目,后端SpringBoot+SpringDataJPA+EasyExcel,服務端口是8181,需要在application.yml文件添加以下信息
spring:datasource:url: jdbc:mysql://localhost:3306/nown_test02?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghaiusername: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driverjpa:show-sql: trueproperties:hibernate:format_sql: true server:port: 8181前端服務端口是8080,當8080端口去向8181端口發送請求時便涉及到了跨域訪問,因此需要添加配置類信息,代碼如下:
import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration public class CorsConfig implements WebMvcConfigurer { //重載WebMvcConfigurer的addCorsMappings方法@Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping("/**").allowedOriginPatterns("*").allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS").allowCredentials(true).maxAge(3600).allowedHeaders("*");} }基本思路應該是,前端向后端發送請求,后端Controller要給出響應,然后去調用Service類的方法,Service又需要去調用Dao層接口。關于學生的信息,我們不妨設置一個StudentController去響應請求,設置一個StudentService類去調用StudentDao的方法。SpringDataJPA最大的好處就是,我們不需要自己去寫SQL語句,只要繼承一些接口就可以實現增刪改查功能。JpaRepository接口繼承了PagingAndSortingRepository和QueryByExampleExecutor兩個接口,能夠實現增刪改查。為了實現動態查詢,需要再繼承JpaSpecificationExecutor接口。這樣StudentDao可以通過方法名來自動拼寫sql,實現方法。比如通過id查找信息,方法名就是findById(Integer id)。如果是模糊查詢,就要加一個關鍵字Like。比如通過名字模糊查詢,方法名就是findByNameLike。代碼如下:
StudentDao
StudentService:
import com.nown.contacts.entity.Student; import com.nown.contacts.entity.StudentDaoSpec; import com.nown.contacts.repository.StudentDao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service;import java.util.Iterator; import java.util.List;@Service public class StudentService {@Autowiredprivate StudentDao studentDao;public List<Student> findByParams(Student student){List<Student> list = studentDao.findAll(StudentDaoSpec.getSpec(student.getNumber(),student.getName(),student.getSex(),student.getAge(),student.getBirthday(),student.getTel(),student.getAddress(),student.getMail(),student.getMajor()));return list;}public Page<Student> findAll(PageRequest request) {return studentDao.findAll(request);}public Student save(Student student) {return studentDao.save(student);}public void save(List<Student> list){Iterator<Student> iterator=list.iterator();while (iterator.hasNext()){Student student= iterator.next();if (findByNumber(student.getNumber())==null){save(student);}else{System.out.println("學號已存在,未添加到數據庫:"+student);}}}public Student findById(Integer id) {return studentDao.findById(id).get();}public void deleteById(Integer id) {studentDao.deleteById(id);}public List<Student> findAll(){return studentDao.findAll();}public Student findByNumber(String number){return studentDao.findStudentByNumber(number);}public List<Student> findByNameLike(String name){return studentDao.findStudentsByNameLike("%"+name+"%");} }關于Vue
Vue是一個比較好用的前端框架,官網有教程,官網地址:https://cn.vuejs.org/v2/guide/
為了方便向后端發送請求,我安裝了axios插件。為了盡量讓視圖美觀,提高開發效率,我又安裝了elementUI插件。當我們開發時可以找到想要的模板,直接把代碼copy下來修改一下就可以使用。npm安裝命令:
這里附上ElementUI地址:https://element.eleme.cn/#/zh-CN
vue我理解為是一個嵌套的視圖。它有一個App.vue 的主頁面。當我們要“跳轉”到其他頁面時,需要在App.vue開辟一個入口,使用<router-view>標簽。這樣我們要訪問的頁面就會呈現在這個標簽里面。
Vue目錄:
為了解決頁面跳轉的路徑問題,需要把我們寫的所有視圖導入,比如import AddStudent from '../views/AddStudent' 。在const routes里面添加路徑,重點是path和component兩個屬性。在index.js添加如下信息:
App.vue代碼:
<template><div id="app"><el-container style="height: 2000px; border: 1px solid #eee"><el-aside width="200px" style="background-color: rgb(238, 241, 246)"><el-menu router :default-openeds="['0']"><el-submenu index="0"><template slot="title"><i class="el-icon-notebook-1"></i>通信錄</template><el-menu-item index="StudentManage"><i class="el-icon-refresh"></i>信息查詢</el-menu-item><el-menu-item index="AddStudent"><i class="el-icon-edit"></i>添加信息</el-menu-item><el-menu-item index="StudentSearch"><i class="el-icon-search"></i>查找</el-menu-item><el-menu-item index="Fuzzy"><i class="el-icon-view"></i>模糊查詢</el-menu-item><el-menu-item index="Dustbin"><i class="el-icon-delete"></i>回收站</el-menu-item></el-submenu><el-submenu index="1"><template slot="title"><i class="el-icon-document"></i>文件</template><el-menu-item @click="exportWord"><i class="el-icon-download"></i>下載文件</el-menu-item><el-upload ref="upload" action :multiple="false" :auto-upload="false" :limit="1" :http-request="importWordConfirm"><el-button slot="trigger" size="small" type="primary" style="margin-bottom: 10px;" plain>選取文件</el-button><el-button style="margin-left: 10px;" size="small" type="success" @click="submitUpload" plain>上傳文件</el-button></el-upload></el-submenu><el-submenu index="2"><template slot="title"><i class="el-icon-folder-opened"></i>分類</template><el-menu-item v-for="item in majors" @click="jump(item.name)"><i class="el-icon-collection-tag"></i>{{item.name}}</el-menu-item></el-submenu></el-menu></el-aside><el-main><router-view></router-view></el-main></el-container></div> </template><style>.el-header {background-color: #B3C0D1;color: #333;line-height: 60px;}.el-aside {color: #333;} </style><script>export default {data() {return {form: {id:null,name: null,number: null,sex: null,age: null,birthday: null,tel: null,address: null,mail: null,major: null,},fileList: [{id: 0,name: ''}],majors:[]};},methods: {exportWord() {//將數據庫數據導出為Excel并下載this.$axios.get('http://localhost:8181/file/download', {responseType: 'blob'}).then(res => {let blob = new Blob([res.data], {type: 'application/ms-excel;charset=utf-8'});let downloadElement = document.createElement('a');let href = window.URL.createObjectURL(blob); //創建下載的鏈接downloadElement.href = href;downloadElement.download = '學生信息.xlsx'; //下載后文件名document.body.appendChild(downloadElement);downloadElement.click(); //點擊下載document.body.removeChild(downloadElement); //下載完成移除元素window.URL.revokeObjectURL(href); //釋放掉blob對象})},submitUpload() {this.$refs.upload.submit();},importWordConfirm(item) {//上傳Excel文件并導入數據庫const _this=this;const fileObj = item.fileconst formData = new FormData()formData.append('file', fileObj)this.$axios.post('http://localhost:8181/file/upload/', formData, {headers: {'Content-Type': 'multipart/form-data'}}).then(res => {_this.$alert('上傳成功!', '消息', {confirmButtonText: '確定',callback: action => {_this.$router.push({path:"/StudentManage"})}})})},jump(x){const _this=this;this.form.major=xthis.$router.push({path:'/list',query:{data:_this.form}})}},created() {const _this = thisaxios.get('http://localhost:8181/major/findAll').then(function(resp) {console.log(resp)_this.majors = resp.data})}}; </script>下面我們根據功能順序依次介紹實現方法:
分頁查詢
當前端向后端發送分頁查詢請求時,需要有頁數和每頁多少條的參數,所以這里有一個findAll方法:
@GetMapping("/findAll/{page}/{size}")public Page<Student> findAll(@PathVariable("page") Integer page, @PathVariable("size") Integer size){//當我們分頁查詢時需要一個PageRequest類型的參數//調用它的of方法,第一個參數表示頁碼,第二個表示這一頁多少條數據PageRequest request = PageRequest.of(page,size);return studentService.findAll(request);}前端Vue的話,要實現分頁查詢,添加了page方法:
page(currentPage) {const _this = thisaxios.get('http://localhost:8181/student/findAll/' + (currentPage - 1) + '/10').then(function (resp) {console.log(resp)_this.tableData = resp.data.content_this.pageSize = resp.data.size_this.total = resp.data.totalElements})}為了訪問8080端口時就加載第一頁的數據,要在created方法里面添加如下代碼:
created() {const _this = this //注意下面的this指針,可能不在是指全局的this指針,而是axios的指針,因此設置了一個_this的臨時變量 axios.get('http://localhost:8181/student/findAll/0/10').then(function (resp) {console.log(resp)_this.tableData = resp.data.content_this.pageSize = resp.data.size_this.total = resp.data.totalElements})}模糊查詢
只要在StudentController添加findLike(String name)方法即可
@GetMapping("/findLike/{name}")public List<Student> findLike(@PathVariable("name") String name){System.out.println(name); // return studentService.findByNameLike(name);List<Student> list=studentService.findByNameLike(name);Iterator<Student> iterator=list.iterator();while (iterator.hasNext()){System.out.println(iterator.next());}return list;}vue的話要注意傳過來的是一個List集合,要進行匹配
deleteStudent(row) {const _this = thisaxios.delete('http://localhost:8181/student/deleteById/' + row.id).then(function (resp) {_this.$alert('刪除成功!', '消息', {confirmButtonText: '確定',callback: action => {window.location.reload()}})})},editStudent(row) {this.$router.push({path: '/update',query: {id: row.id}})},queryStudents(){const _this=thisaxios.get('http://localhost:8181/student/findLike/'+_this.forml.name).then(function(resp){console.log(resp)_this.datas = resp.data})}},data(){return{forml:{name:''},datas:[]}}動態查詢
為了實現動態查詢,需要讓StudentDao繼承JpaSpecificationExecutor接口。
public interface JpaSpecificationExecutor<T> {Optional<T> findOne(@Nullable Specification<T> var1);List<T> findAll(@Nullable Specification<T> var1);Page<T> findAll(@Nullable Specification<T> var1, Pageable var2);List<T> findAll(@Nullable Specification<T> var1, Sort var2);long count(@Nullable Specification<T> var1); }由此可見,要實現findAll方法還要實現Specification接口。實現這個接口需要根據實體類Student類寫一個數據元類Student_.java。
import javax.persistence.metamodel.SingularAttribute; import javax.persistence.metamodel.StaticMetamodel;@StaticMetamodel(Student.class) public class Student_ {public static volatile SingularAttribute<Student, Integer> id;// 用戶ID,自增量public static volatile SingularAttribute<Student, String> number;public static volatile SingularAttribute<Student, String> name;public static volatile SingularAttribute<Student, String> sex;public static volatile SingularAttribute<Student, Integer> age;public static volatile SingularAttribute<Student, String> birthday;public static volatile SingularAttribute<Student, String> tel;public static volatile SingularAttribute<Student, String> address;public static volatile SingularAttribute<Student, String> mail;public static volatile SingularAttribute<Student, String> major;}然后是寫一個獲取Specification接口的方法,用于動態拼寫Sql。在我們動態查詢時,傳過來的是一些條件,如果是空則不需要and,如果不空就要把這個條件拼上。
import org.springframework.data.jpa.domain.Specification;import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root;public class StudentDaoSpec {public static Specification<Student> getSpec(final String number,final String name,final String sex,final Integer age,final String birthday,final String tel,final String address,final String mail,final String major) {return new Specification<Student>() {@SuppressWarnings("unused")@Overridepublic Predicate toPredicate(Root<Student> root, CriteriaQuery<?> query, CriteriaBuilder cb) {Predicate p1 = null;if (number!=null){System.out.println("正在操作number!!!");Predicate p2 = cb.equal(root.get(Student_.number), number);if (p1 != null) {p1 = cb.and(p1, p2);} else {p1 = p2;}}if (name!=null) {System.out.println("正在操作name!!!");Predicate p2 = cb.equal(root.get(Student_.name), name);if (p1 != null) {p1 = cb.and(p1, p2);} else {p1 = p2;}}if (sex!=null){System.out.println("正在操作sex!!!");Predicate p2 = cb.equal(root.get(Student_.sex), sex);if (p1 != null) {p1 = cb.and(p1, p2);} else {p1 = p2;}}if (age != null) {System.out.println("正在操作age!!!");Predicate p2 = cb.equal(root.get(Student_.age), age);if (p1 != null) {p1 = cb.and(p1, p2);} else {p1 = p2;}}if (birthday!=null) {System.out.println("正在操作birthday!!!");Predicate p2 = cb.equal(root.get(Student_.birthday), birthday);if (p1 != null) {p1 = cb.and(p1, p2);} else {p1 = p2;}}if (tel!=null) {System.out.println("正在操作tel!!!");Predicate p2 = cb.equal(root.get(Student_.tel), tel);if (p1 != null) {p1 = cb.and(p1, p2);} else {p1 = p2;}}if (address!=null) {System.out.println("正在操作address!!!");Predicate p2 = cb.equal(root.get(Student_.address), address);if (p1 != null) {p1 = cb.and(p1, p2);} else {p1 = p2;}}if (mail!=null) {System.out.println("正在操作mail!!!");Predicate p2 = cb.equal(root.get(Student_.mail), mail);if (p1 != null) {p1 = cb.and(p1, p2);} else {p1 = p2;}}if (major!=null) {System.out.println("正在操作major!!!");Predicate p2 = cb.equal(root.get(Student_.major), major);if (p1 != null) {p1 = cb.and(p1, p2);} else {p1 = p2;}}return p1;}};} }由此,我們動態查詢的準備工作已經做好了。在前端要動態查詢的時候,為了方便傳參,我們讓它把條件綁定到一個Student類的對象中,然后依次獲取它的屬性值。
StudentSearch.vue中的部分代碼:
List.vue中的部分代碼:
data() {return {tableData:[] };},created() {const _this = thisaxios.post('http://localhost:8181/student/search', _this.$route.query.data).then(function(resp) {console.log(resp)_this.tableData = resp.data})}后端要響應的話需要一個search方法:
@PostMapping("/search")public List<Student> search(@RequestBody Student student){System.out.println(student);List<Student> list=studentService.findByParams(student);Iterator<Student> iterator=list.iterator(); // while (iterator.hasNext()){ // System.out.println("查找的數據是:"+iterator.next()); // }return studentService.findByParams(student);}分類查詢
既然我們已經寫好了動態查詢接口,我們就沒有必要再寫一個findByMajor方法了,直接動態查詢吧。后端StudentController的search方法去處理請求即可。前端的話,分專業查詢,我是把專業直接放在App.vue的左側菜單欄的,只要在App.vue的created方法里面添加查詢所有專業的方法就可以了。
<el-submenu index="2"><template slot="title"><i class="el-icon-folder-opened"></i>分類</template><el-menu-item v-for="item in majors" @click="jump(item.name)"><i class="el-icon-collection-tag"></i>{{item.name}}</el-menu-item> </el-submenu>data() {return {form: {id:null,name: null,number: null,sex: null,age: null,birthday: null,tel: null,address: null,mail: null,major: null,},fileList: [{id: 0,name: ''}],majors:[]};},jump(x){const _this=this;this.form.major=xthis.$router.push({path:'/list',query:{data:_this.form}})}created() {const _this = thisaxios.get('http://localhost:8181/major/findAll').then(function(resp) {console.log(resp)_this.majors = resp.data})}后端也很簡單,只要寫一個查詢所有方法即可
@GetMapping("/findAll")public List<Major> findAll(){return majorDao.findAll();}添加信息
在添加數據時,我們要對學號、郵箱、手機號進行校驗,同時計算年齡。要檢查學號是否存在,因此要在StudentController方法里面添加一個findByNum方法。保存的話需要一個Save方法:
@GetMapping("/findByNum/{num}")public String findByNum(@PathVariable("num") String num){System.out.println(num);Student student=studentService.findByNumber(num);if (student!=null){return "已存在";}return "不存在";}@PostMapping("/save")public String save(@RequestBody Student student){System.out.println(student);if (studentService.findByNumber(student.getNumber())!=null){return "學號已存在!";}Student result = studentService.save(student);if(result != null){return "保存成功!";}return "保存失敗!";}Vue校驗方法:
this.$refs[formName].validate((valid) => {if (valid) {axios.get('http://localhost:8181/student/findByNum/' + _this.form.number).then(function(resp) {if (resp.data == "已存在") {alert("學號已存在")return false;} else {axios.post('http://localhost:8181/student/save', _this.form).then(function(resp) {_this.$alert(resp.data, '消息', {confirmButtonText: '確定',callback: action => {_this.$router.push('/StudentManage')}})})}})}校驗郵箱和電話號碼我們可以使用正則表達式來完成,為了不讓已填寫的信息丟失,我們只要讓方法return false即可(AddStudent.vue的部分代碼):
const _this = thisvar x = this.form.mailvar atpos = x.indexOf("@");var dotpos = x.lastIndexOf(".");if (atpos < 1 || dotpos < atpos + 2 || dotpos + 2 >= x.length) {alert("不是一個有效的 e-mail 地址");return false;}var myreg = /^[1][3,4,5,7,8][0-9]{9}$/;x = this.form.telif (!myreg.test(x.toString())) {alert("手機號不合法");return false;}自動計算年齡:
var birth = this.form.birthday;birth = Date.parse(birth.replace('/-/g', "/"));if (birth) {var year = 1000 * 60 * 60 * 24 * 365;var now = new Date();var birthday = new Date(birth);var age = parseInt((now - birthday) / year);}// alert(age);this.form.age = age;我們傳到后端的數據是要綁定成一個Student對象的,注意數據綁定(AddStudent.vue和StudentUpdate.vue的部分代碼):
form: {name: '',number: '',sex: '',age: '',birthday: '',tel: '',address: '',mail: '',major: '',},修改信息
我們在修改信息時,要跳到修改界面,需要根據當前學生的id獲得學生信息,所以StudentController要寫一個findById(Integer id)方法和修改的update方法:
@GetMapping("/findById/{id}")public Student findById(@PathVariable("id") Integer id){System.out.println(new Date()+";"+id);return studentService.findById(id);}@PostMapping("/update")public String update(@RequestBody Student student){System.out.println(student);Student result = studentService.save(student);if(result != null){return "修改成功!";}else{return "修改失敗!";}}在StudentUpdate.vue的created方法里面添加:
created() {const _this = thisaxios.get('http://localhost:8181/student/findById/' + this.$route.query.id).then(function(resp) {console.log(resp)_this.form = resp.data})// alert("this.id="+this.$route.query.id+";_this.id="+_this.$route.query.id)//在選擇學生專業的時候下拉選項需要加載所有專業,因此要向MajorController發送請求axios.get('http://localhost:8181/major/findAll').then(function(resp) {console.log(resp)_this.tableData = resp.data})}不能修改學號,但是可見,因此學號這個標簽應該添加屬性readOnly(StudentUpdate.vue部分代碼):
<el-form-item label="學生學號" prop="number" ><el-input v-model="form.number" style="width:160px" readOnly></el-input> </el-form-item>修改完成之后要提交:
vue部分代碼:
刪除信息
根據學生的id刪除即可,注意刪除的時候應該是先把這條信息存到dustbin這張表中,然后在student表中是刪除。因此Student Controller的deleteById方法如下:
@DeleteMapping("/deleteById/{id}")public void deleteById(@PathVariable("id") Integer id){System.out.println("刪除操作"+new Date());dustbinDao.save(converter.toDustbin(studentService.findById(id)));studentService.deleteById(id);}雖然dustbin和student表中的內容非常相似,但是畢竟是兩個不同的類,所以這里又寫了一個Converter類用于兩者之間的互化:
import com.nown.contacts.entity.Dustbin; import com.nown.contacts.entity.Student; import org.springframework.stereotype.Component;@Component public class Converter {public Student toStudent(Dustbin dustbin){Student student=new Student();student.setName(dustbin.getName());student.setNumber(dustbin.getNumber());student.setAddress(dustbin.getAddress());student.setAge(dustbin.getAge());student.setBirthday(dustbin.getBirthday());student.setId(dustbin.getStuid());student.setMail(dustbin.getMail());student.setMajor(dustbin.getMajor());student.setSex(dustbin.getSex());student.setTel(dustbin.getTel());return student;}public Dustbin toDustbin(Student student){Dustbin dustbin=new Dustbin();dustbin.setStuid(student.getId());dustbin.setNumber(student.getNumber());dustbin.setName(student.getName());dustbin.setAddress(student.getAddress());dustbin.setAge(student.getAge());dustbin.setBirthday(student.getBirthday());dustbin.setMail(student.getMail());dustbin.setMajor(student.getMajor());dustbin.setSex(student.getSex());dustbin.setTel(student.getTel());return dustbin;} }vue的刪除代碼如下(Student Manage.vue部分代碼):
deleteStudent(row) {const _this = thisaxios.delete('http://localhost:8181/student/deleteById/' + row.id).then(function (resp) {_this.$alert('刪除成功!', '消息', {confirmButtonText: '確定',callback: action => {window.location.reload()}})})},備份功能
以上已經說明了如何將刪除的數據保存到dustbin表中,下面解決如何從回收站恢復。在訪問Dustbin.vue時我們就要加載所有已刪除的信息,因此后端DustbinController類中有findAll方法,并且是分頁查詢的。然后回收站主要負責恢復和徹底刪除,分別對應著recover和delete方法。不管恢復還是徹底刪除,我們都需要一個id屬性用于確定信息。
DustbinController的部分代碼:
Dustbin.vue中的部分代碼:
methods: {delete(row) {const _this = thisaxios.delete('http://localhost:8181/dustbin/deleteById/' + row.id).then(function (resp) {_this.$alert('刪除成功!', '消息', {confirmButtonText: '確定',callback: action => {window.location.reload()}})})},recover(row) {const _this=thisaxios.get('http://localhost:8181/dustbin/recover/' + row.id).then(function (resp) {if (resp.data == 'success') {_this.$alert('恢復成功!', '消息', {confirmButtonText: '確定',callback: action => {_this.$router.push('/StudentManage')}})} else {alert('恢復失敗!')return false;}})},page(currentPage) {const _this = thisaxios.get('http://localhost:8181/dustbin/findAll/' + (currentPage - 1) + '/10').then(function (resp) {console.log(resp)_this.tableData = resp.data.content_this.pageSize = resp.data.size_this.total = resp.data.totalElements})}},data() {return {pageSize: 0,total: 0,tableData: []}},created() {const _this = thisaxios.get('http://localhost:8181/dustbin/findAll/0/10').then(function (resp) {console.log(resp)_this.tableData = resp.data.content_this.pageSize = resp.data.size_this.total = resp.data.totalElements})}關于EasyExcel
下面解決導出和導入的問題。關于處理Excel,我選用的時EasyExcel,感興趣的朋友可以去對比一下Apache poi。
EasyExcel地址:https://github.com/alibaba/easyexcel
語雀地址:https://www.yuque.com/easyexcel/doc/easyexcel
首先要對實體類Student類進行補充,為其添加@ExcelProperty注解,后面兩個參數value代表了表格的表頭名稱,index代表了序號。
package com.nown.contacts.entity;import com.alibaba.excel.annotation.ExcelProperty; import lombok.Data;import javax.persistence.*;@Data @Entity @Table(name = "student") public class Student {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)@ExcelProperty(value = "序號",index = 0)private Integer id;@ExcelProperty(value = "學號",index = 1)private String number;@ExcelProperty(value = "姓名",index = 2)private String name;@ExcelProperty(value = "性別",index = 3)private String sex;@ExcelProperty(value = "年齡",index = 4)private Integer age;@ExcelProperty(value = "出生日期",index = 5)private String birthday;@ExcelProperty(value = "手機號碼",index = 6)private String tel;@ExcelProperty(value = "家庭住址",index = 7)private String address;@ExcelProperty(value = "電子郵箱",index = 8)private String mail;@ExcelProperty(value = "專業",index = 9)private String major; }然后是寫一個監聽器,用于在讀取Excel時將讀出的數據導入到數據庫中:
package com.nown.contacts.util;import java.util.ArrayList; import java.util.List;import com.nown.contacts.entity.Student; import com.nown.contacts.service.StudentService;import com.alibaba.excel.context.AnalysisContext; import com.alibaba.excel.event.AnalysisEventListener; import org.springframework.beans.factory.annotation.Autowired;// 有個很重要的點 DemoDataListener 不能被spring管理,要每次讀取excel都要new,然后里面用到spring可以構造方法傳進去 public class ExcelListener extends AnalysisEventListener<Student> {private static final int BATCH_COUNT = 1;private List<Student> list;private StudentService studentService;@Autowiredpublic ExcelListener(StudentService studentService) {this.studentService=studentService;this.list = new ArrayList<Student>();}@Overridepublic void invoke(Student student, AnalysisContext context) {list.add(student);// 達到BATCH_COUNT了,需要去存儲一次數據庫,防止數據幾萬條數據在內存,容易OOMif (list.size() >= BATCH_COUNT) {saveData();// 存儲完成清理 listlist.clear();}}@Overridepublic void doAfterAllAnalysed(AnalysisContext context) {System.out.println("插入完成");}/*** 加上存儲數據庫*/private void saveData() {System.out.println("{}條數據,開始存儲數據庫!");studentService.save(list);System.out.println("存儲數據庫成功!");} }這里注意,我們是把Excel表格中的數據讀出后放到了一個List集合里面了,因此我們需要重載StudentService的save方法(之前的save方法參數是Student類,這個方法的參數是List集合),同時注意如果學號已存在則不能將信息插入數據庫
public Student save(Student student) {return studentDao.save(student);}public void save(List<Student> list){Iterator<Student> iterator=list.iterator();while (iterator.hasNext()){Student student= iterator.next();if (findByNumber(student.getNumber())==null){save(student);}else{System.out.println("學號已存在,未添加到數據庫:"+student);}}}然后是寫一個操作類,Operator類,用于寫doWrite和doRead方法。
package com.nown.contacts.util;import com.alibaba.excel.EasyExcel; import com.alibaba.excel.context.AnalysisContext; import com.alibaba.excel.event.AnalysisEventListener; import com.nown.contacts.entity.Student; import com.nown.contacts.service.StudentService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile;import java.io.IOException; import java.io.OutputStream; import java.util.List; import java.util.Map;@Component public class Operator {@AutowiredStudentService studentService;public void doWrite(OutputStream outputStream, List<Student> list){EasyExcel.write(outputStream, Student.class).sheet("模板").doWrite(list);}public void doRead(MultipartFile file) throws IOException {EasyExcel.read(file.getInputStream(), Student.class, new ExcelListener(studentService)).sheet().doRead();} }為了方便Controller調用這些方法,我們再來寫一個ExcelService類
package com.nown.contacts.service;import com.nown.contacts.entity.Student; import com.nown.contacts.util.Operator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile;import java.io.IOException; import java.io.OutputStream; import java.util.List;@Service public class ExcelService {@Autowiredprivate Operator operator;@Autowiredprivate StudentService studentService;public void doWrite(OutputStream outputStream){List<Student> list=studentService.findAll();operator.doWrite(outputStream,list);}public void doRead(MultipartFile file) throws IOException {operator.doRead(file);} }這樣我們在處理請求時,只要Controller去響應請求,調用Service的方法,然后Service再去調用operator的方法就可以了。
至此,使用EasyExcel導入導出Excel已經做好準備,下面開始添加功能:
導出Excel
為了響應上傳下載請求,可以寫一個FileController類。我們先來看一下如何導出Excel??梢垣@取Response的輸出流,這樣導出Excel:
@Autowiredprivate ExcelService service;@GetMapping("download")public void download(HttpServletResponse response) throws IOException {// 這里注意 有同學反應使用swagger 會導致各種問題,請直接用瀏覽器或者用postmanresponse.setContentType("application/vnd.ms-excel");response.setCharacterEncoding("utf-8");// 這里URLEncoder.encode可以防止中文亂碼 當然和easyexcel沒有關系String fileName = URLEncoder.encode("學生信息", "UTF-8").replaceAll("\\+", "%20");System.out.println(fileName);response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");service.doWrite(response.getOutputStream());}前端Vue的話,要響應后端傳過來的文件流(App.vue的部分代碼):
exportWord() {this.$axios.get('http://localhost:8181/file/download', {responseType: 'blob'}).then(res => {let blob = new Blob([res.data], {type: 'application/ms-excel;charset=utf-8'});let downloadElement = document.createElement('a');let href = window.URL.createObjectURL(blob); //創建下載的鏈接downloadElement.href = href;downloadElement.download = '學生信息.xlsx'; //下載后文件名document.body.appendChild(downloadElement);downloadElement.click(); //點擊下載document.body.removeChild(downloadElement); //下載完成移除元素window.URL.revokeObjectURL(href); //釋放掉blob對象})},Excel導入
首先我們前端要發送請求(App.vue的部分代碼):
submitUpload() {this.$refs.upload.submit();},importWordConfirm(item) {const _this=this;const fileObj = item.fileconst formData = new FormData()formData.append('file', fileObj)this.$axios.post('http://localhost:8181/file/upload/', formData, {headers: {'Content-Type': 'multipart/form-data'}}).then(res => {_this.$alert('上傳成功!', '消息', {confirmButtonText: '確定',callback: action => {_this.$router.push({path:"/StudentManage"})}})})},后端要處理請求(FileController中部分代碼):
@PostMapping("/upload")@ResponseBodypublic String upload(MultipartFile file) throws IOException {service.doRead(file);return "success";}注意:
在使用Excel導入信息到數據庫時,相同學號的信息不會被導入,可以從控制臺看到哪條信息沒有被導入。從Excel導入信息時,模板和下載模板類似,但是要刪除id那一列,否則相同id的數據會覆蓋掉數據庫原有的信息。
OK,整個項目已經介紹完畢,具體代碼已經上傳至我的GitHub,歡迎大家參考,給個Star唄!
Github地址:https://github.com/Nown1/contacts
總結
- 上一篇: treelist自动定位行_国内首创!金
- 下一篇: freemarker 去掉最后一个逗号_