Springboot-Thymeleaf-Customer-File
接口要写严格,一个接口一个用途。
这样别人调用接口时,不会有任何误会。
前言
之前的程序:
之前用springboot+thymeleaf做了前后端实现的用户的增删改查,现在要加个单文件上传、多文件上传与下载,并且优化页面。前面三大操作都是借助了项目提供的接口。我在自己的服务器上启动一个程序,通过调用它提供的接口,在项目服务器上实现三大功能,并且把相应的信息发送至Mysql,并实现可视化。
由于我的程序是对应了项目,所以有些地方具有特殊性。
先看页面
用户目录
list.html
<!DOCTYPEhtml>
<htmllang="en"xmlns:th="http://www.thymeleaf.org">
<head>
<metacharset="UTF-8"/>
<title>客户列表</title>
<style>
table{border-collapse:collapse;}
table,th,td{border:1pxsolidblack;padding:5px;text-align:center;}
</style>
<linkrel="stylesheet"th:href="@{/css/bootstrap.css}"/>
</head>
<bodyclass="container">
<br/>
<br/>
<ath:href="edit">新增用户</a>
<br/>
<br/>
<table>
<thead>
<thwidth="12.5%">id</th>
<thwidth="12.5%">name</th>
<thwidth="12.5%">age</th>
<thwidth="12.5%">used</th>
<thwidth="50%">操作</th>
</thead>
<tbody>
<trth:each="customer : ${customers}">
<tdth:text="${customer.id}"></td>
<tdth:text="${customer.name}"></td>
<tdth:text="${customer.age}"></td>
<tdth:text="${customer.used}"></td>
<td>
<ath:href="@{/customers/edit(id=${customer.id})}">编辑</a>
<ath:href="@{/customers/goingToDel(pageNum=${pageNum},id=${customer.id})}">删除</a>
<ath:href="@{/customers/file(customerId=${customer.id})}">单文件上传</a>
<ath:href="@{/customers/multiple(customerId=${customer.id})}">多文件上传</a>
<ath:href="@{/customers/goToFileSystem(customerId=${customer.id})}">我的文件</a>
</td>
</tr>
</tbody>
</table>
<divth:text="当前页数"></div>
<divth:text="${pageNum}"></div>
<divclass="modal-footer no-margin-top">
<ulclass="pagination pull-right no-margin">
<!--首页-->
<li>
<ath:href="'/customers/list?pageNum=0'">首页</a>
</li>
<!--上一页-->
<lith:if="${customers.hasPrevious()}">
<ath:href="'/customers/list?pageNum=' + ${customers.previousPageable().getPageNumber()}"th:text="上一页"></a>
</li>
<!--中间页-->
<lith:each="pageNum:${#numbers.sequence(0, customers.getTotalPages()-1)}">
<ath:href="'/customers/list?pageNum=' + ${pageNum}"th:text="${pageNum + 1}"th:style="'font-weight:bold;background: #6faed9;'"></a>
</li>
<!--下一页-->
<lith:if="${customers.hasNext()}">
<ath:href="'/customers/list?pageNum=' + ${customers.nextPageable().getPageNumber()}"th:text="下一页"></a>
</li>
<!--尾页-->
<li>
<ath:href="'/customers/list?pageNum=' + ${customers.getTotalPages() - 1}">尾页</a>
</li>
</ul>
</div>
</body>
</html>
单文件上传
file.html
<!DOCTYPEhtml>
<htmlxmlns="http://www.w3.org/1999/xhtml"xmlns:th="http://www.thymeleaf.org"xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<metacharset="UTF-8"/>
<title>Inserttitlehere</title>
</head>
<body>
<h1th:inlines="text">文件上传</h1>
<formth:action="@{/customers/singleFile(customerId=${customerId})}"method="post"enctype="multipart/form-data">
<p>选择文件:<inputtype="file"name="file"/></p>
<p><inputtype="submit"value="上传"/></p>
<p><inputtype="button"value="返回"onclick="JavaScript:history.go(-1)"/></p>
</form>
</body>
</html>
文件列表
fielList.html
<!DOCTYPEhtml>
<htmllang="en"xmlns:th="http://www.thymeleaf.org">
<head>
<metacharset="UTF-8"/>
<title>客户文件列表</title>
<style>
table{border-collapse:collapse;}
table,th,td{border:1pxsolidblack;padding:5px;text-align:center;}
</style>
<linkrel="stylesheet"th:href="@{/css/bootstrap.css}"/>
<scripttype="text/javascript">
functionconfirmDel(fileId,customerId)
{
if(window.confirm("您确定要删除该条数据吗?")){
document.location="/customers/delFile?fileId="+fileId+"&customerId="+customerId
}
}
</script>
</head>
<bodyclass="container">
<table>
<thead>
<thwidth="20%">线上文件Id</th>
<thwidth="20%">线上文件用户Id</th>
<thwidth="20%">线上文件名</th>
<thwidth="20%">线上文件地址</th>
<thwidth="20%">操作</th>
</thead>
<tbody>
<trth:each="file : ${files}">
<tdth:text="${file.fileId}"></td>:
<tdth:text="${file.customerId}"></td>
<tdth:text="${file.fileName}"></td>
<tdth:text="${file.fullPath}"></td>
<td>
<ath:href="@{/customers/download(fullPath=${file.fullPath},fileName=${file.fileName})}">下载</a>
<ahref="javascript:void(0)"th:onclick="|confirmDel( ${file.fileId} ,${file.customerId}) |">删除</a>
</td>
</tr>
</tbody>
<div>
<inputtype="button"value="返回"onclick="JavaScript:history.go(-1)"/>
</div>
</table>
</body>
</html>
下载文件
TestController中的一部分
@GetMapping(value="download")
publicResponseEntity<byte[]>download(@RequestParam("fullPath")StringfilePath,@RequestParam("fileName")StringfileName)throwsUnsupportedEncodingException{
Stringauthorization="Bearer ...";
byte[]res=feignService.downLoadFile(filePath,authorization);
ResponseEntity<byte[]>entity=newResponseEntity<byte[]>(HttpStatus.OK);
HttpHeadersheaders=newHttpHeaders();
headers.add("Content-Type","application/octet-stream");
headers.add("Connection","close");
headers.add("Accept-Ranges","bytes");
headers.add("Content-Disposition",
"attachment;filename="+newString(fileName.getBytes("GB2312"),"ISO8859-1"));
entity=newResponseEntity<byte[]>(res,headers,HttpStatus.OK);
returnentity;
}
删除文件弹窗
<scripttype="text/javascript">
functionconfirmDel(fileId,customerId)
{
if(window.confirm("您确定要删除该条数据吗?")){
document.location="/customers/delFile?fileId="+fileId+"&customerId="+customerId
}
}
</script>
<ahref="javascript:void(0)"th:onclick="|confirmDel( ${file.fileId} ,${file.customerId}) |">删除</a>
多文件上传
multiple.html
<!DOCTYPEhtml>
<htmlxmlns="http://www.w3.org/1999/xhtml"xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<metacharset="UTF-8"/>
<title>Inserttitlehere</title>
</head>
<body>
<h1th:inlines="text">文件上传</h1>
<formth:action="@{/customers/multiFiles(customerId=${customerId})}"method="post"enctype="multipart/form-data">
<p>选择文件1:<inputtype="file"name="files"/></p>
<p>选择文件2:<inputtype="file"name="files"/></p>
<p>选择文件3:<inputtype="file"name="files"/></p>
<p>选择文件4:<inputtype="file"name="files"/></p>
<p>选择文件5:<inputtype="file"name="files"/></p>
<p><inputtype="submit"value="上传"/></p>
<p><inputtype="button"value="返回"onclick="JavaScript:history.go(-1)"/></p>
</form>
</body>
</html>
选择任意数量的不同的或相同的文件
这里有点复杂,之后讲
上传至不同用户管辖的文件列表
点击"我的文件"直达文件系统
若该用户没有所属的文件,则显示空
接口文档阅读
作为小白,我之前没有读过接口文档说明。不同公司的接口文档不一样。
我这里使用了之前学习的Feign来调用这些接口,非常方便。
接口调用
不管是谁给的接口,只要我们要用,就要先postman调用,试试水。如果自己程序写好了,发现接口有问题导致delay,血亏。
随便举个例子
这些接口都会返回string字符串,我所要的文件线上地址以及错误码都需要我自己提取。
文件结构
程序里待调用的接口
packagecom.mybatis_druid_mysql.demo.Service;
importcom.mybatis_druid_mysql.demo.Entity.File;
importorg.springframework.cloud.openfeign.FeignClient;
importorg.springframework.stereotype.Service;
importorg.springframework.web.bind.annotation.*;
importorg.springframework.web.multipart.MultipartFile;
importjava.util.List;
@FeignClient(name="jerryNiu",url="http://product360-newproduct-dev.kg.plantdata.cn/")
publicinterfaceFeignService{
@PostMapping(value="api/kgms/file",consumes="multipart/form-data")
StringupLoadFile(@RequestPart(value="file")MultipartFilefile,@RequestHeader(value="authorization")Stringauthorization);
@PostMapping(value="api/kgms/file/multi",consumes="multipart/form-data")
StringupLoadMultiFile(@RequestPart(value="files")List<MultipartFile>files,@RequestHeader(value="authorization")Stringauthorization);
@GetMapping(value="api/kgms/file/download")
byte[]downLoadFile(@RequestParam(value="filePath")StringfilePath,@RequestHeader(value="authorization")Stringauthorization);
}
RequestPart:
Annotation that can be used to associate the part of a "multipart/form-data" request with a method argument.
RequestHeader:
Annotation which indicates that a method parameter should be bound to a web request header
启动类
package com.mybatis_druid_mysql.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients
public class DemoApplication {
public static void main(String[] args){
SpringApplication.run(DemoApplication.class,args);
}
}
EnableFeignClients:
Scans for interfaces that declare they are feign clients (via FeignClient @FeignClient). Configures component scanning directives for use with org.springframework.context.annotation.Configuration @Configuration classes.
MapperScan:
Use this annotation to register MyBatis mapper interfaces when using Java Config. It performs when same work as MapperScannerConfigurer via MapperScannerRegistrar.
至于其他俩注解就不介绍了。
Controller
package com.mybatis_druid_mysql.demo.Controller;
import com.mybatis_druid_mysql.demo.Entity.Customer;
import com.mybatis_druid_mysql.demo.Entity.File;
import com.mybatis_druid_mysql.demo.Service.FeignService;
import com.mybatis_druid_mysql.demo.Service.Service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServlet;
import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@org.springframework.stereotype.Controller
@RequestMapping("customers")
@Slf4j
public class TestController extends HttpServlet {
@Autowired
private Service service;
@Autowired
private FeignService feignService;
...
}
单文件上传
TestController里的一部分
@RequestMapping("file")
public String file(ModelMap map, @RequestParam(value = "customerId") Integer customerId){
map.addAttribute("customerId",customerId);
return "customer/file";
}
@PostMapping(value = "singleFile" ,consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public String fileUpload(ModelMap map,
MultipartFile file,
@RequestParam(value = "customerId") Integer customerId){
String authorization = "Bearer ...";
File file1 = new File();
file1.setCustomerId(customerId);
file1.setFileName(file.getOriginalFilename());
System.out.println("Hello World");
System.out.println(file.getOriginalFilename());
System.out.println(file.getContentType());
file1.setFullPath(feignService.upLoadFile(file,authorization).substring(35,87));
System.out.println(file1);
if(file1.getFileId()==null){
file1.setFileId(0);
}
else {
file1.setFileId(file1.getFileId()+1);
}
System.out.println(file1);
List file2 = service.getAllFiles();
System.out.println(file2);
System.out.println(feignService.upLoadFile(file,authorization).substring(35,87));
System.out.println(feignService.upLoadFile(file, authorization).substring(11,14));
System.out.println(feignService.upLoadFile(file,authorization));
log.error(feignService.upLoadFile(file,authorization));
if(feignService.upLoadFile(file, authorization).substring(11,14).equals("200")){
service.insertOneFile(file1);
List file3 = service.getAllFiles();
map.addAttribute("files",file3);
return "redirect:/customers/fileList?customerId="+customerId;
}
map.addAttribute("files",file2);
return "redirect:/customers/fileList?customerId="+customerId;
}
ModelMap是向页面传数据的工具。
给Mysql传数据时,不用手动设置id,只要在Mysql中设置id为自增主键,他会自己配置。
MultiPartFile:
A representation of an uploaded file received in a multipart request.The file contents are either stored in memory or temporarily on disk. In either case, the user is responsible for copying file contents to a session-level or persistent store as and if desired. The temporary storage will be cleared at the end of request processing.
要看程序中各项数据的值,最好用log.info,而不是像这里用的sout。
文件下载
@GetMapping(value = "download")
public ResponseEntity download(@RequestParam("fullPath") String filePath,@RequestParam("fileName") String fileName) throws UnsupportedEncodingException {
String authorization = "Bearer ...";
byte[] res = feignService.downLoadFile(filePath,authorization);
ResponseEntity entity = new ResponseEntity(HttpStatus.OK);
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "application/octet-stream");
headers.add("Connection", "close");
headers.add("Accept-Ranges", "bytes");
headers.add("Content-Disposition",
"attachment;filename=" + new String(fileName.getBytes("GB2312"), "ISO8859-1"));//获取文件名
entity = new ResponseEntity(res, headers, HttpStatus.OK);
return entity;
}
ResponseEntity:
Extension of HttpEntity that adds a HttpStatus status code. Used in RestTemplate as well
HttpEntity :
Represents an HTTP request or response entity, consisting of headers and body.
这里的Headers与postman上写的headers如出一辙。
代码效果一开始就展示过了。
多文件上传
/*
* 获取multifile.html页面
*/
@RequestMapping("multiple")
public String multifile(ModelMap map, @RequestParam(value = "customerId") Integer customerId){
map.addAttribute("customerId",customerId);
return "customer/multiple";
}
/**
* 实现多文件上传
* */
@PostMapping(value="multiFiles",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public String multifileUpload(ModelMap map,
@RequestPart List files,
@RequestParam(value = "customerId") Integer customerId) throws IOException {
String authorization = "Bearer ...";
if(files.get(0).isEmpty()&&files.get(1).isEmpty()&&files.get(2).isEmpty()&&files.get(3).isEmpty()&&files.get(4).isEmpty()){
return "redirect:/customers/fileList?customerId="+customerId;
}
System.out.println(files.size());
//上传文件
String result = feignService.upLoadMultiFile(files,authorization);
System.out.println(result);
int num = 0;//计算总文件数
List availableFile = new ArrayList<>();//记录非空的文件index
List filePath = new ArrayList<>();//记录各个文件线上地址
//多文件上传返回值预处理
String returnMessage;
returnMessage = result
.replaceAll("errCode","")
.replaceAll("message","")
.replaceAll("}","")
.replace('{',':')
.replaceAll(":","")
.replaceAll(":","")
.replaceAll(",","")
.replace("\"", "")
.replaceAll("data","")
.replaceAll("]","")
.replace('[',':')
.replaceAll(":","")
.replaceAll("fullPath","")
.replaceAll("fileName"," : ");
//分析并拆解上传文件的返回值
for(int i=0 ; i
if(!files.get(i).isEmpty()){
availableFile.add(i);//第i号文件有东西
num++;
log.info(returnMessage);
int begin = returnMessage.lastIndexOf("/");
int end = returnMessage.lastIndexOf(":");
log.info(returnMessage.substring(begin-17,end-1));
filePath.add(returnMessage.substring(begin-17,end-1));
log.info(returnMessage.replaceAll(returnMessage.substring(begin-17,end-1),""));
returnMessage = returnMessage.replaceAll(returnMessage.substring(begin-17,end-1),"");
log.info(returnMessage.substring(0,returnMessage.lastIndexOf(":")-1));
returnMessage = returnMessage.substring(0,returnMessage.lastIndexOf(":")-1);
}
else {
files.remove(i);
if(i!=0)
i--;//https://it4all.blog.csdn.net/article/details/77915981
}
}
//同步上传mysql
System.out.println(filePath);
for(int i=0 ; i
File file = new File();
file.setFileName(files.get(availableFile.get(i)).getOriginalFilename());
log.info(files.get(availableFile.get(i)).getOriginalFilename());
file.setFullPath(filePath.get(num-i-1));
file.setFileId(0);
file.setCustomerId(customerId);
service.insertOneFile(file);
}
System.out.println(files.size());
//打印Mysql
List file2 = service.getAllFiles();
map.addAttribute("files",file2);
return "redirect:/customers/fileList?customerId="+customerId;
}
多文件上传逻辑就复杂一些。我希望当用户选择多文件上传时,最多一次传5个文件。项目给我的接口的文件数量远大于我要的5。我需要实现用户传的文件数小于5时,将空文件删除。并且将返回的字符串整理成我需要的线上文件地址。并且同步到Mysql中。
MultipartFile:
A representation of an uploaded file received in a multipart request.
文件删除
这里的删除并不是在项目服务器上把文件删除,而是在Mysql表中删除数据。因为项目没给相应的删除文件的接口。
@RequestMapping("delFile")
public String delFile(ModelMap map, @RequestParam(value = "fileId") Integer fileId,
@RequestParam(value = "customerId") Integer customerId){
service.deleteFile(fileId);
List file2 = service.getAllFiles();
System.out.println(file2);
map.addAttribute("files",file2);
System.out.println("ok");
return "redirect:/customers/fileList?customerId="+customerId;
}
查看文件
用户之前都要先上传文件,才能看到文件列表。现在我希望能直接有个查看文件列表的接口。
@GetMapping(value = "fileList")
public String fileList(ModelMap map,
@RequestParam(value = "customerId") Integer customerId){
List file2 = service.getByCustomerId(customerId);
map.addAttribute("files",file2);
return "customer/fileList";
}
Mysql表同步
我这里通过mybatis里的mapper实现与mysql的信息交互。
@Insert("insert into files(file_name,full_path,customer_id) values(#{fileName},#{fullPath},#{customerId})")
@Options(useGeneratedKeys = true,keyColumn = "file_id",keyProperty = "fileId")//展示id只有insert时要
void insertOneFile(File file);
@Select("select * from files where customer_id = #{customerId}")
@Results( value = { //!!!!
@Result(property = "fileId", column = "file_id"),
@Result(property = "fileName", column = "file_name"),
@Result(property = "fullPath", column = "full_path"),
@Result(property = "customerId", column = "customer_id")})
List getByCustomerId(@Param("customerId") int customerId);
@Select("select * from files where file_id = #{fileId}")
@Results( value = { //!!!!
@Result(property = "fileId", column = "file_id"),
@Result(property = "fileName", column = "file_name"),
@Result(property = "fullPath", column = "full_path"),
@Result(property = "customerId", column = "customer_id")})
File getByFileId(@Param("fileId") int fileId);
@Select("select * from files")
@Results( value = { //!!!!
@Result(property = "fileId", column = "file_id"),
@Result(property = "fileName", column = "file_name"),
@Result(property = "fullPath", column = "full_path"),
@Result(property = "customerId", column = "customer_id")})
List getAllFiles();
@Delete("delete from files where file_id = #{fileId}")
void delFile(@Param("fileId") int fileId);
file实体类(注意!!!这里的file不是jdk里的file,而是我自己设置的实体类!!!
@Entity(name = "files")
@Data
public class File implements Serializable {
private static final long serialVersionUID = 1L;
@Id
private Integer fileId;
private String fileName;
private String fullPath;
private Integer customerId;
}
我在Navicat中把fileId调成自增主键后,会出现一个问题:Mysql默认主键名为id,我们再用Mapper使用Mysql语句时,要将表中的file_id与fileId一一对应,否则就算在Navicat中看到file_id下的数据是存在的,在select时,会报错说fileId不能是空。
怎么一一对应呢?举个例子。
@Delete("delete from files where file_id = #{fileId}")
@Options(useGeneratedKeys = true,keyColumn = "fileId")
@Results( value = { //!!!!
@Result(property = "fileId", column = "file_id"),
@Result(property = "fileName", column = "file_name"),
@Result(property = "fullPath", column = "full_path"),
@Result(property = "customerId", column = "customer_id")})
void delFile(@Param("fileId") int fileId);
produces:它的作用是指定返回值类型,不但可以设置返回值类型还可以设定返回值的字符编码;
consumes: 指定处理请求的提交内容类型(Content-Type),例如application/json, text/html;
enctype 属性规定在发送到服务器之前应该如何对表单数据进行编码。
Service层对Mapper层的调用我就不写了。
同步到mysql的数据传输我使用了druid。
package com.mybatis_druid_mysql.demo.Druid;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.PropertySource;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
@SpringBootConfiguration
@PropertySource(value = "classpath:/application.properties")
@EnableTransactionManagement
public class Druid {
@Value("${spring.datasource.druid.username}")
private String userName;
@Value("${spring.datasource.druid.password}")
private String password;
@Value("${spring.datasource.druid.driver-class-name}")
private String driverName;
@Value("${spring.datasource.druid.url}")
private String url;
@Value("${spring.datasource.druid.initial-size}")
private int initialSize;
@Value("${spring.datasource.druid.max-active}")
private int maxActive;
@Value("${spring.datasource.druid.min-idle}")
private int minIdle;
@Value("${spring.datasource.druid.max-wait}")
private int maxWait;
@Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}")
private int timeBetweenEvictionRunsMillis;
@Value("${spring.datasource.druid.minEvictableIdleTimeMillis}")
private int minEvictableIdleTimeMillis;
@Value("${spring.datasource.druid.maxPoolPreparedStatementPerConnectionSize}")
private int maxPoolPreparedStatementPerConnectionSize;
@Bean(name = "datasource")
public DataSource dataSource(){//由druid-spring-boot-starter实现springboot提供的datasource接口
DruidDataSource dataSource = null;
try{
dataSource = new DruidDataSource();
dataSource.setUsername(userName);
dataSource.setPassword(password);
dataSource.setUrl(url);
dataSource.setDriverClassName(driverName);
dataSource.setInitialSize(initialSize);
dataSource.setMaxActive(maxActive);
dataSource.setMinIdle(minIdle);
dataSource.setMaxWait(maxWait);
dataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
dataSource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
dataSource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
}catch (Exception e){
e.printStackTrace();
}
return dataSource;
}
}
这串代码我在以前的文章大致讲过。
这样,就完成了。