继续丰富我们的项目功能。
这次是文件上传的功能,而在我们的element官网上是有文件上传的组件功能的,我们直接拿来用就行。
老方法,粘贴复制。但是上图的服务器接口(也就是上传下载的路由地址)我们需要自己去写,所以现在去后端服务器上写一个接口,FileController:
同时我们在resources文件下创建一个files文件夹,这个文件夹就是用来存放从前台上传过来的文件的。
因为写入文件的时候要使用IO流,但是java的原生io过于繁琐和麻烦,所以我们使用hutool这个工具,在pom中引入:
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.3</version>
</dependency>
现在写接口中的逻辑:
package com.why.demo.controller;
import cn.hutool.core.io.FileUtil;
import com.why.demo.common.Result;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
//文件上传接口
@RestController
@RequestMapping("/files")
public class FileController {
@PostMapping("/upload")
public Result<?> upload(MultipartFile file) throws IOException {
// MultipartFile用来接收前台传送过来的文件对象
// 获取文件名称
String originalFilename = file.getOriginalFilename();
//获取我们的文件要存储的位置(即resources下的files位置)
String rootFilePath = System.getProperty("user.dir")+"/springboot/src/main/resources/files/"+originalFilename;
//现在要将这个文件写入进我们的文件夹中,用hutool工具实现,io过于繁琐了不用
// file.getBytes()得到文件的字节流,rootFilePath:要写入文件的位置
FileUtil.writeBytes(file.getBytes(),rootFilePath);
return Result.success();
}
}
然后打开postman测试一下:
千万要记得图中的红色圈圈圈起来的都要记得点的和我一模一样,其中的那个圈内没有内容的选的是“file”类型,尤其尤其要记得点哪个黑色勾勾,否则就会报空指针异常。
可以看到已经上传上来了。
但是现在有个问题是,我们如果上传了相同名字的文件,那么新上传的文件则会覆盖掉我们之前的同名文件,这不合理,我们需要解决。
解决的办法就是在每一个上传上来的文件前面加前缀,不加后缀是因为文件名本来就有后缀了,现在代码如下:
package com.why.demo.controller;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import com.why.demo.common.Result;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
//文件上传接口
@RestController
@RequestMapping("/files")
public class FileController {
@PostMapping("/upload")
public Result<?> upload(MultipartFile file) throws IOException {
// MultipartFile用来接收前台传送过来的文件对象
// 获取文件名称
String originalFilename = file.getOriginalFilename();
// 定义文件的唯一标识去(前缀),hutool的这个工具不会生成重复的字符串
String flag = IdUtil.fastSimpleUUID();
//获取我们的文件要存储的位置(即resources下的files位置)
String rootFilePath = System.getProperty("user.dir")+"/springboot/src/main/resources/files/"+flag+"_"+originalFilename;
//现在要将这个文件写入进我们的文件夹中,用hutool工具实现,io过于繁琐了不用
// file.getBytes()得到文件的字节流,rootFilePath:要写入文件的位置
FileUtil.writeBytes(file.getBytes(),rootFilePath);
return Result.success(); //返回结果url
}
}
再次测试:
可以看到新上传的文件就拥有了一个只属于自己的前缀。
然后我们再实现一下文件下载的接口:
// 文件下载
@GetMapping("/{flag}")
public void getFiles (@PathVariable String flag, HttpServletResponse response){
OutputStream os; //新建一个输出流对象
String basePath = System.getProperty("user.dir")+"/springboot/src/main/resources/files/"; //定义文件上传的根路径
List<String> fileNames = FileUtil.listFileNames(basePath); //获取该根路径下的所有的文件名称
//找到跟参数一致的文件
String fileName = fileNames.stream().filter(name -> name.contains(flag)).findAny().orElse("");
try{
if(StrUtil.isNotEmpty(fileName)){
response.addHeader("Content-Disposition","attachment;filename="+URLEncoder.encode(fileName,"UTF-8"));
response.setContentType("application/octet-stream");
//通过文件路径读取文件的字节流
byte[] bytes = FileUtil.readBytes(basePath+fileName);
os = response.getOutputStream(); //通过输出流返回文件
os.write(bytes);
os.flush();
os.close();
}
} catch (Exception e){
System.out.println("文件下载失败!");
}
}
这个控制器还有些部分要完善,所以我直接贴源码:
package com.why.demo.controller;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import com.why.demo.common.Result;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.List;
//文件上传接口
@RestController
@RequestMapping("/files")
public class FileController {
@Value("${server.port}")
private String port;
private static final String ip = "http://localhost";
// 文件上传
@PostMapping("/upload")
public Result<?> upload(MultipartFile file) throws IOException {
// MultipartFile用来接收前台传送过来的文件对象
// 获取文件名称
String originalFilename = file.getOriginalFilename();
// 定义文件的唯一标识去(前缀),hutool的这个工具不会生成重复的字符串
String flag = IdUtil.fastSimpleUUID();
//获取我们的文件要存储的位置(即resources下的files位置)
String rootFilePath = System.getProperty("user.dir")+"/springboot/src/main/resources/files/"+flag+"_"+originalFilename;
//现在要将这个文件写入进我们的文件夹中,用hutool工具实现,io过于繁琐了不用
// file.getBytes()得到文件的字节流,rootFilePath:要写入文件的位置
FileUtil.writeBytes(file.getBytes(),rootFilePath);
return Result.success(ip+":"+port+"/files/"+flag); //返回结果url
}
// 文件下载
@GetMapping("/{flag}")
public void getFiles (@PathVariable String flag, HttpServletResponse response){
OutputStream os; //新建一个输出流对象
String basePath = System.getProperty("user.dir")+"/springboot/src/main/resources/files/"; //定义文件上传的根路径
List<String> fileNames = FileUtil.listFileNames(basePath); //获取该根路径下的所有的文件名称
//找到跟参数一致的文件
String fileName = fileNames.stream().filter(name -> name.contains(flag)).findAny().orElse("");
try{
if(StrUtil.isNotEmpty(fileName)){
response.addHeader("Content-Disposition","attachment;filename="+URLEncoder.encode(fileName,"UTF-8"));
response.setContentType("application/octet-stream");
//通过文件路径读取文件的字节流
byte[] bytes = FileUtil.readBytes(basePath+fileName);
os = response.getOutputStream(); //通过输出流返回文件
os.write(bytes);
os.flush();
os.close();
}
} catch (Exception e){
System.out.println("文件下载失败!");
}
}
}
现在用过postman测试上传文件功能:
可以看见已经上传上去了:
此时会上传文件的方法会返回一个url,而这个url正是我们下载文件的路径;复制postman上返回的那串url,在浏览器地址栏输入即可下载我们刚刚上传的文件:
可以看见左下角已经在下载了。
然后我们去写前端,前面已经提到了,element已经提供了组件,我们需要写的只是接口路由,所以很简单。假如我们现在想给我们的Book菜单加一个上传或者下载图片封面的功能,那么我们现在就先去给数据库中的book表加一个封面的字段:
把下面这一段放入Book.vue组件内的封面中:
有些东西我们不需要,可以删掉,如下:
现在访问点击编辑:
可以上传东西了,但是我们此时缺少东西去接收从后台返回回来的文件下载的路由,但是element依然给我们提供好了这样的组件。
文件上传时有一个“on-success”的钩子,所以我们可以在上传按钮中加一个方法:
methods中定义一下,先让它输出打印一下:
然后访问点击上传会报跨域错误,我也不懂为啥,但是up主说后台再加个跨域就可以解决了:
后台解决跨域就在common配置包下加个配置类CorsConfig:
package com.why.demo.common;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class CorsConfig {
//当前跨域请求最大有效时长,这里默认一天
private static final long MX_AGE = 24 * 60 * 60;
private CorsConfiguration buildConfig(){
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*"); //1、设置访问源地址
corsConfiguration.addAllowedHeader("*"); //2、设置访问源请求头
corsConfiguration.addAllowedMethod("*"); //3、设置访问源请求方法
corsConfiguration.setMaxAge(MX_AGE);
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter(){
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**",buildConfig()); //4、对接口配置跨域设置
return new CorsFilter(source);
}
}
不知道为啥配置了这一段之后就可以访问了:
可以看到现在访问就没有问题了。
可以看见我们可以拿到路由的值了,那么现在我们只需要将这段路由值传到后台去就可以了。
不要忘了去给实体类加上我们刚刚在数据库中加的字段:
现在我们访问上传文件:
可以看见数据库中已经出现了下载的链接。要注意的一点是,在idea中上传的文件不能大于1M,否则上传会失败。
怎么显示出来呢让这图片,element依然给我们提供了一个展示图片的组件:
copy下来在Book.vue中进行操作,直接贴源码了:
<template>
<!--padding设置一点内边距-->
<div style="padding: 10px">
<!--功能区域-->
<div style="margin: 10px 0">
<el-button type="primary" @click="add">新增</el-button>
<el-button type="primary">导入</el-button>
<el-button type="primary">导出</el-button>
</div>
<!--搜索区域-->
<div style="margin: 10px 0">
<el-input v-model="search" placeholder="请输入关键字" style="width: 20%" clearable/>
<el-button type="primary" style="margin-left: 5px" @click="load">搜索</el-button>
</div>
<!--后面的width可以不写,浏览器会自适应
stripe是斑马纹效果-->
<el-table :data="tableData" border stripe style="width: 100%">
<!--sortable让日期排序-->
<el-table-column prop="id" label="ID" sortable/>
<el-table-column prop="name" label="书名"/>
<el-table-column prop="price" label="单价" />
<el-table-column prop="author" label="作者"/>
<el-table-column prop="createTime" label="出版日期"/>
<el-table-column label="封面">
<template #default="scope">
<el-image
style="width: 100px; height: 100px"
:src="scope.row.cover"
:preview-src-list="[scope.row.cover]">
</el-image>
</template>
</el-table-column>
<el-table-column fixed="right" label="操作">
<template #default="scope">
<el-button size="mini" @click="handleEdit(scope.row)">编辑</el-button>
<!--弹出消息确认框-->
<el-popconfirm
confirm-button-text="是的,没错"
cancel-button-text="妈的,我再想想"
icon="el-icon-info"
icon-color="red"
title="确定删除吗?" @confirm="handleDelete(scope.row.id)">
<template #reference>
<el-button size="mini" type="danger">删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<!--分页功能区域-->
<div style="margin: 10px 0">
<el-pagination
v-model:currentPage="currentPage4"
:page-sizes="[5,10,20]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange">
</el-pagination>
</div>
<!--弹窗输入用户信息-->
<el-dialog v-model="dialogVisible" title="提示" width="30%">
<!--定义表单在这个弹出框内以收集新增的用户信息-->
<el-form :model="form" label="用户名">
<el-form-item label="书名">
<el-input v-model="form.name" style="width: 80%"></el-input>
</el-form-item>
<el-form-item label="价格">
<el-input v-model="form.price" style="width: 80%"></el-input>
</el-form-item>
<el-form-item label="作者">
<el-input v-model="form.author" style="width: 80%"></el-input>
</el-form-item>
<el-form-item label="出版时间">
<el-date-picker v-model="form.createTime" value-format="YYYY-MM-DD" time="date" style="width: 80%" clearable></el-date-picker>
</el-form-item>
<el-form-item label="封面">
<!--文件上传器-->
<el-upload ref="upload" action="http://localhost:9090/files/upload" :on-success="fileUploadSuccess">
<el-button type="primary">点击上传</el-button>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="save">确 定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
import request from "../utils/request";
export default {
name: 'Book',
components: {
},
data() {
return {
form: {},
dialogVisible: false,
search: '',
//当前页
currentPage4: 1,
pageSize: 10,
total: 0,
tableData: [
],
}
},
//表示在页面被加载的时候,在这个方法里面的方法就全部调用
created() {
this.load()
},
methods: {
fileUploadSuccess(res){
console.log(res)
this.form.cover = res.data
},
//查询方法
load(){
request.get("/book",{
params: {
pageNum: this.currentPage4,
pageSize: this.pageSize,
search: this.search
}
}).then(res => {
console.log(res)
this.tableData = res.data.records
this.total = res.data.total
})
},
add(){
this.dialogVisible = true;
//清空表单域
this.form = {};
this.$refs['upload'].clearFiles() //清楚历史文件列表
},
save(){
if(this.form.id){ //更新操作
request.put("/book",this.form).then(res => {
console.log(res)
if(res.code == '0'){
//element给我们提供了一个可以供显示的结果的东西,如下
this.$messageBox({
type: "success",
message: "更新成功"
})
}else{
//element给我们提供了一个可以供显示的结果的东西,如下
this.$messageBox({
type: "error",
message: res.msg
})
}
})
this.load() //每一次更新后都自动刷新表格
this.dialogVisible = false //关闭弹窗
}else{ //新增操作
request.post("/book",this.form).then(res => {
console.log(res)
if (res.code == '0') {
//element给我们提供了一个可以供显示的结果的东西,如下
this.$messageBox({
type: "success",
message: "新增成功"
})
} else {
//element给我们提供了一个可以供显示的结果的东西,如下
this.$messageBox({
type: "error",
message: res.msg
})
}
this.load() //每次新增完都自动刷新表格
this.dialogVisible = false //关闭弹窗
}
)}
},
handleEdit(row){
this.form = JSON.parse(JSON.stringify(row))
this.dialogVisible = true
this.$nextTick(()=>{
this.$refs['upload'].clearFiles() //清楚历史上传记录
})
},
handleDelete(id){
console.log(id)
request.delete("/book/"+id).then(res =>{
if (res.code == '0') {
//element给我们提供了一个可以供显示的结果的东西,如下
this.$messageBox({
type: "success",
message: "删除成功"
})
} else {
//element给我们提供了一个可以供显示的结果的东西,如下
this.$messageBox({
type: "error",
message: res.msg
})
}
this.load() //删除之后重新加载表格的数据
})
},
handleSizeChange(pageSize){
// 改变当前每页的数据条数时触发
this.pageSize = pageSize
this.load()
},
handleCurrentChange(pageNum){
// 改变当前页码时触发
this.currentPage4 = pageNum
this.load()
}
}
}
</script>
访问可以看见图片已经出现:
至此文件的上传下载也结束啦!