21.6. 确定上传文件的名称
为了避免文件被覆盖,每个文件的路径或文件名应该不发生冲突,例如可以使用唯一的文件,要保证文件名唯一,可以考虑使用时间、随机数等数据作为文件名的一部分,也可以使用用户的唯一数据,例如用户的id、用户名等作为文件名的一部分,具体规则可自行决定。
关于文件的扩展名部分,可以通过MultipartFile
的String getOriginalFilename()
方法获取原始文件名,即用户上传的文件在客户端时使用的文件名,然后对文件名进行分析处理,得到原始的扩展名,例如:
String originalFilename = file.getOriginalFilename();
String suffix = "";
int beginIndex = originalFilename.lastIndexOf(".");
if (beginIndex != -1) {
suffix = originalFilename.substring(beginIndex);
}
21.7. 确定上传文件的文件夹
上传的文件必须在Tomcat部署项目的目录中,否则,上传的文件将无法通过http协议进行访问,可以HttpServletRequest
对象获取在webapp
下指定名称的文件夹的实际路径,然后创建出所需的文件夹,例如,先在处理请求的方法中添加HttpServletRequest
作为参数,然后:
String parentDir = request.getServletContext().getRealPath("upload");
File parent = new File(parentDir);
if (!parent.exists()) {
parent.mkdirs();
}
21.8. 关于MultipartFile中的常用方法
-
String getOriginalFilename()
:获取原始文件名,即用户上传的文件在客户端时使用的文件名; -
boolean isEmpty()
:判断上传的文件是否为空,如果用户没有选择文件就提交了上传请求,或选择的文件是0字节的,则视为空,将返回true
,否则返回false
; -
long getSize()
:获取文件的大小,以字节为单位; -
String getContentType()
:获取文件的MIME类型,例如返回image/jpeg
,关于扩展名与MIME的对应关系,可以上网查阅资料,也可以在Tomcat的conf目录下的web.xml中查找,文件的扩展名不同,得到的MIME类型可能是不同的; -
InputStream getInputStream()
:获取文件的输入字节流,用于需要自定义处理用户上传的数据的应用场景,例如上传的文件较大,在存储时需要自定义缓冲区等,该方法不可与transferTo()
方法同时使用; -
void transferTo(File dest)
:保存客户端上传的文件,该方法不可以与getInputStream()
方法同时使用。
21.9. 关于CommonsMultipartResolver的配置
在spring.xml中可以对CommonsMultipartResolver
的属性注入值:
-
maxUploadSize
:最大上传的数据量,以字节为单位,假设设置为10M,则单次请求的最大数据就是10M,可能这次上传过程中有2个文件,则2个文件的总和不允许超过10M; -
maxUploadSizePerFile
:上传的每个文件的最大大小,假设设置为10M,且这次上传过程中有2个文件,则每个文件都不可以超过10M,但是这次请求的数据总大小可能接近20M; -
defaultEncoding
:默认字符集,用于设置上传时同一个表单中可能提交的其它字符的编码。
以上设置请求数据大小的配置,检查的时间节点比较靠前,在控制器还没有处理请求时就会检查;这些设置并不能取代在控制器通过
getSize()
获取文件大小并进行的相关检查,在配置文件中的这些配置是项目的全局化配置,即同一个项目中,无论是上传头像,还是上传附件,还是上传商品图片等,所有的上传功能都必须符合这些配置,而控制器中的检查是单项功能的检查,毕竟每个控制器只处理某1种请求,所以,上传头像的方法里自定义头像的大小,而上传商品图片的方法里自定义商品图片的大小,是根据具体功能作出的限制。
21.10. 如何一次性上传多个文件
如果需要上传的文件的数量是固定的,且文件的定位是不同的,例如上传身份证的正面和反面照片,则在客户端可以使用多个<input type="file" />
控件,且在服务器处理请求的方法中添加多个MultipartFile
类型的参数即可。
另外,如果上传的文件的定位是相同的,且数量不固定,则在客户端使用控件,添加multiple="multiple
属性,即:
<input type="file" name="file" multiple="multiple" />
这样的文件浏览控件在操作时,可以按住Ctrl键,是可以一次性选中多个文件的。
然后,在服务器端的控制器中,将参数声明为MultipartFile[]
即可。
21.11. 存储方式
所有的上传,都应该将文件存储到服务器的硬盘中,另外,在数据库中记录下文件的存储路径,当需要使用文件时,可以查询数据库获取指定文件的路径,再对文件进行访问。
22. 用户-上传头像-持久层
(a) 规划SQL语句
执行上传头像的数据库操作部分,只是修改用户数据的avatar
字段的值,需要执行的SQL语句大致是:
update t_user set avatar=?,modified_user=?,modified_time=? where uid=?
在执行修改之前,还应该检查用户数据是否存在、是否标记为已删除,对应的功能已经存在,无需再次开发。
(b) 接口与抽象方法
在UserMapper.java
接口中添加抽象方法:
Integer updateAvatar(
@Param("uid") Integer uid,
@Param("avatar") String avatar,
@Param("modifiedUser") String modifiedUser,
@Param("modifiedTime") Date modifiedTime);
© 配置映射
配置映射:
<!-- 更新头像 -->
<!-- Integer updateAvatar(
@Param("uid") Integer uid,
@Param("avatar") String avatar,
@Param("modifiedUser") String modifiedUser,
@Param("modifiedTime") Date modifiedTime) -->
<update id="updateAvatar">
UPDATE
t_user
SET
avatar=#{avatar},
modified_user=#{modifiedUser},
modified_time=#{modifiedTime}
WHERE
uid=#{uid}
</update>
单元测试:
@Test
public void updateAvatar() {
Integer uid = 7;
String avatar ="1234";
String modifiedUser = "超级管理员";
Date modifiedTime = new Date();
Integer rows = mapper.updateAvatar(uid, avatar, modifiedUser, modifiedTime);
System.err.println("rows=" + rows);
}
################### 以上大纲步骤 1 ##########################
1.UserMapper.java中添加抽象方法
2.UserMapper.xml配置文件中添加sql配置代码
3.用户映射测试
a.更新测试效果为:控制台显示:
row=1
23. 用户-上传头像-业务层
(a) 规划异常
本次执行的主要是更新数据的操作,则可能出现UpdateException
。
在执行更新之前,还需要根据uid查询用户数据,检查数据是否存在,检查数据的is_delete,都可能出现UserNotFoundException
。
此次操作无需创建新的异常类。
(b) 接口与抽象方法
在IUserService
中添加“修改头像”的抽象方法:
void changeAvatar(Integer uid, String avatar, String modifiedUser)
throws UserNotFoundException, UpdateException;
© 实现抽象方法
在UserServiceImpl
实现类中添加新的抽象方法并实现:
public void changeAvatar(Integer uid, String avatar, String modifiedUser)
throws UserNotFoundException, UpdateException {
// 根据参数uid查询用户数据
// 判断查询结果是否为null:UserNotFoundException
// 判断查询结果中的isDelete是否为1:UserNotFoundException
// 执行更新,获取返回值(受影响的行数)
// 判断受影响的行数是否不为1:UpdateException
}
具体实现为:
@Override
public void changeAvatar(Integer uid, String avatar, String modifiedUser)
throws UserNotFoundException, UpdateException {
// 根据参数uid查询用户数据
User result = userMapper.findByUid(uid);
// 判断查询结果是否为null:UserNotFoundException
if (result == null) {
// 是:抛出UserNotFoundException
throw new UserNotFoundException(
"修改头像失败!用户数据不存在!");
}
// 判断查询结果中的isDelete是否为1:UserNotFoundException
if (result.getIsDelete() == 1) {
// 是:抛出UserNotFoundException
throw new UserNotFoundException(
"修改头像失败!用户数据不存在!");
}
// 执行更新,获取返回值(受影响的行数)
Integer rows = userMapper.updateAvatar(uid, avatar, modifiedUser, new Date());
// 判断受影响的行数是否不为1:UpdateException
if (rows != 1) {
throw new UpdateException(
"修改头像失败!更新数据时出现未知错误!");
}
}
单元测试:
@Test
public void changeAvatar() {
try {
Integer uid = 7;
String avatar = "88888888";
String modifiedUser = "系统管理员";
service.changeAvatar(uid, avatar, modifiedUser);
System.err.println("OK");
} catch (ServiceException e) {
System.err.println(e.getClass().getName());
System.err.println(e.getMessage());
}
}
################### 以上大纲步骤 2 ##########################
1.IuserService.java中添加抽象方法
2.UserServiceImpl.java中实现重写抽象方法
3.测试
a.更新测试效果为:控制台显示:
ok
b.用持久层的查询用户查询,用户头像是否插入成功。
24. 用户-上传头像-控制器层
(a) 统一处理异常
在处理上传文件之前,应该对用户选择并提交的文件数据进行检查,例如文件是否为空、文件大小是否超出限制、文件类型是否超出限制,如果不符合标准,应该抛出对应的异常:
cn.tedu.store.controller.ex.FileUploadException
cn.tedu.store.controller.ex.FileEmptyException
cn.tedu.store.controller.ex.FileSizeException
cn.tedu.store.controller.ex.FileTypeException
cn.tedu.store.controller.ex.FileUploadIOException
cn.tedu.store.controller.ex.FileStateException
这些都继承自cn.tedu.store.controller.ex.FileUploadException
,而该FileUploadException
继承自RuntimeException
。
另外,在执行保存文件时,调用的transferTo()
方法也是会抛出异常的:
try {
file.transferTo(dest);
} catch (IllegalStateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
当捕获到以上这些异常时,可以在捕获后抛出自定义异常FileStateException
和FileUploadIOException
,也是和以上异常在同一个包中,也都继承自FileUploadException
,然后,在BaseController
中对自定义异常进行统一处理!
(b) 设计请求
设计“上传头像”的请求方式:
请求路径:/users/change_avatar
请求参数:MultipartFile file, HttpServletRequest request
请求方式:POST
响应数据:JsonResult<String>
© 处理请求
1.在UserController
中添加处理请求的方法:
@GetMapping("get_by_uid")
public JsonResult<User> getByUid(HttpSession session) {
// 执行获取数据
Integer uid = getUidFromSession(session);
User data = userService.getByUid(uid);
// 返回成功与数据
return new JsonResult<>(SUCCESS, data);
}
/**
* 允许上传的头像文件的最大大小
*/
public static final long AVATAR_MAX_SIZE = 2 * 1024 * 1024;
/**
* 允许上传的头像文件的类型列表
*/
public static final List<String> AVATAR_CONTENT_TYPES = new ArrayList<String>();
static {
AVATAR_CONTENT_TYPES.add("image/jpeg");
AVATAR_CONTENT_TYPES.add("image/png");
}
@PostMapping("change_avatar")
public JsonResult<String> changeAvatar(
@RequestParam("file") MultipartFile file,
HttpServletRequest request) {
// 检查文件是否为空
if (file.isEmpty()) {
throw new FileEmptyException(
"上传头像失败!请选择有效的图片文件!");
}
// 检查文件大小是否超出限制
if (file.getSize() > AVATAR_MAX_SIZE) {
throw new FileSizeException(
"上传头像失败!不允许上传超过" + (AVATAR_MAX_SIZE / 1024) + "KB的图片文件!");
}
// 检查文件类型是否超出限制
if (!AVATAR_CONTENT_TYPES.contains(file.getContentType())) {
throw new FileSizeException(
"上传头像失败!选择的文件类型超出了限制!\r\r允许使用的文件类型有:"
+ AVATAR_CONTENT_TYPES);
}
// 确定文件夹
String parentPath = request.getServletContext().getRealPath("upload");
File parent = new File(parentPath);
if (!parent.exists()) {
parent.mkdirs();
}
// 确定文件名
String filename = UUID.randomUUID().toString();
String originalFilename = file.getOriginalFilename();
int beginIndex = originalFilename.lastIndexOf(".");
String suffix = originalFilename.substring(beginIndex);
String child = filename + suffix;
// 保存用户上传的文件
File dest = new File(parent, child);
try {
file.transferTo(dest);
} catch (IllegalStateException e) {
throw new FileStateException(
"上传文件失败!文件状态有误,请重新尝试!");
} catch (IOException e) {
throw new FileUploadIOException(
"上传文件失败!发生读写错误,请重新尝试!");
}
// 将文件的路径记录到数据库
String avatarPath = "/upload/" + child;
Integer uid = getUidFromSession(request.getSession());
String username = getUsernameFromSession(request.getSession());
userService.changeAvatar(uid, avatarPath, username);
// 响应结果
return new JsonResult<>(SUCCESS, avatarPath);
}
2.启动类中加上以下配置
@SpringBootApplication
@Configuration
@MapperScan(“cn.tedu.store.mapper”)
public class StoreApplication {
public static void main(String[] args) {
SpringApplication.run(StoreApplication.class, args);
}
@Bean
public MultipartConfigElement getMultipartConfigElement() {
MultipartConfigFactory factory
= new MultipartConfigFactory();
DataSize maxFileSize = DataSize.ofMegabytes(500);
factory.setMaxFileSize(maxFileSize);
DataSize maxRequestSize = DataSize.ofMegabytes(500);
factory.setMaxRequestSize(maxRequestSize);
return factory.createMultipartConfig();
}
}
3.login页面中加入以下代码(alert(登录成功)下) :
if (obj.state == 2000) {
alert(“登录成功!”);
// 将头像存到Cookie
$.cookie(“avatar”, obj.data.avatar, {“expires”:7});
console.log(“avatar=” + $.cookie(“avatar”));
} else {
alert(obj.message);
}
后端代码 调整部分
1.添加一系列的检查
a.UserController.java类中:
/**
* 允许上传的头像文件的最大大小
/
public static final long AVATAR_MAX_SIZE = 2 * 1024 * 1024;
/*
* 允许上传的头像文件的类型列表
*/
public static final List AVATAR_CONTENT_TYPES = new ArrayList();
static {
AVATAR_CONTENT_TYPES.add("image/jpeg");
AVATAR_CONTENT_TYPES.add("image/png");
}
@PostMapping("change_avatar")
public JsonResult<String> changeAvatar(
@RequestParam("file") MultipartFile file,
HttpServletRequest request) {
// 检查文件是否为空
if (file.isEmpty()) {
throw new FileEmptyException(
"上传头像失败!请选择有效的图片文件!");
}
// 检查文件大小是否超出限制
if (file.getSize() > AVATAR_MAX_SIZE) {
throw new FileSizeException(
"上传头像失败!不允许上传超过" + (AVATAR_MAX_SIZE / 1024) + "KB的图片文件!");
}
// 检查文件类型是否超出限制
if (!AVATAR_CONTENT_TYPES.contains(file.getContentType())) {
throw new FileSizeException(
"上传头像失败!选择的文件类型超出了限制!\r\r允许使用的文件类型有:
" + AVATAR_CONTENT_TYPES);
}
// 确定文件夹
String parentPath = request.getServletContext().getRealPath("upload");
File parent = new File(parentPath);
if (!parent.exists()) {
parent.mkdirs();
}
// 确定文件名
String filename = UUID.randomUUID().toString();
String originalFilename = file.getOriginalFilename();
int beginIndex = originalFilename.lastIndexOf(".");
String suffix = originalFilename.substring(beginIndex);
String child = filename + suffix;
// 保存用户上传的文件
File dest = new File(parent, child);
try {
file.transferTo(dest);
} catch (IllegalStateException e) {
throw new FileStateException(
"上传文件失败!文件状态有误,请重新尝试!");
} catch (IOException e) {
throw new FileUploadIOException(
"上传文件失败!发生读写错误,请重新尝试!");
}
// 将文件的路径记录到数据库
String avatarPath = "/upload/" + child;
Integer uid = getUidFromSession(request.getSession());
String username = getUsernameFromSession(request.getSession());
userService.changeAvatar(uid, avatarPath, username);
// 响应结果
return new JsonResult<>(SUCCESS, avatarPath);
}
b.BaseController.java类中:
@ExceptionHandler({ServiceException.class,FileUploadException.class})
@ResponseBody
public JsonResult handleException(Throwable e) {
JsonResult jr = new JsonResult<>(e);
jr.setMessage(e.getMessage());
if (e instanceof UsernameDuplicateException) {
jr.setState(4000);
} else if (e instanceof UserNotFoundException) {
jr.setState(4001);
} else if (e instanceof PasswordNotMatchException) {
jr.setState(4002);
} else if (e instanceof InsertException) {
jr.setState(5000);
} else if (e instanceof UpdateException) {
jr.setState(5001);
} else if (e instanceof FileEmptyException) {
jr.setState(6000);
} else if (e instanceof FileSizeException) {
jr.setState(6001);
} else if (e instanceof FileTypeException) {
jr.setState(6002);
} else if (e instanceof FileStateException) {
jr.setState(6003);
} else if (e instanceof FileUploadIOException) {
jr.setState(6004);
}
return jr;
}
25. 用户-上传头像-前端界面
-
当登录成功时,将用户的头像路径响应给客户端,且客户端将头像路径保存到Cookie中。首先,应该检查在
UserMapper.xml
的findByUsername()
查询的字段列表中是否包含avatar
字段;然后,在登录的前端界面中,当响应结果表示登录成功时,通过$.cookie("avatar", obj.data.avatar, {"expires":7});
将服务器端响应的头像路径保存到客户端的Cookie中。 -
打开上传头像页面时,通过Cookie中保存的头像路径,显示当前登录的用户的头像:
26. 用户-上传头像-自定义上传大小的限制
在SpringBoot项目中,所有的上传默认都不允许超过1M,否则就会报错!
先在启动类StoreApplication
之前添加@Configuration
注解,然后,在类中添加:
@Bean
public MultipartConfigElement getMultipartConfigElement() {
MultipartConfigFactory factory
= new MultipartConfigFactory();
DataSize maxFileSize = DataSize.ofMegabytes(500);
factory.setMaxFileSize(maxFileSize);
DataSize maxRequestSize = DataSize.ofMegabytes(500);
factory.setMaxRequestSize(maxRequestSize);
return factory.createMultipartConfig();
}
27. 收货地址-分析
关于“收货地址”数据的管理,涉及的功能有:增加,修改,删除,设为默认,显示列表。
以上功能的开发顺序应该是:增加 > 显示列表 > 设为默认 > 删除 > 修改。
28. 收货地址-创建数据表
创建收货地址数据表:
CREATE TABLE t_address (
aid INT AUTO_INCREMENT COMMENT '收货地址id',
uid INT COMMENT '用户id',
name VARCHAR(50) COMMENT '收货人姓名',
province_code CHAR(6) COMMENT '省代号',
province_name VARCHAR(50) COMMENT '省名称',
city_code CHAR(6) COMMENT '市代号',
city_name VARCHAR(50) COMMENT '市名称',
area_code CHAR(6) COMMENT '区代号',
area_name VARCHAR(50) COMMENT '区名称',
zip CHAR(6) COMMENT '邮编',
address VARCHAR(100) COMMENT '详细地址',
phone VARCHAR(20) COMMENT '手机',
tel VARCHAR(20) COMMENT '固话',
tag VARCHAR(30) COMMENT '地址类型',
is_default INT COMMENT '是否默认,0-非默认,1-默认',
created_user VARCHAR(50) COMMENT '创建人',
created_time DATETIME COMMENT '创建时间',
modified_user VARCHAR(50) COMMENT '最后修改人',
modified_time DATETIME COMMENT '最后修改时间',
PRIMARY KEY (aid)
) DEFAULT CHARSET=UTF8;
29. 收货地址-创建实体类
30. 收货地址-增加-持久层
(a) 规划SQL语句
(b) 接口与抽象方法
© 配置映射
31. 收货地址-增加-业务层
(a) 规划异常
(b) 接口与抽象方法
© 实现抽象方法
32. 收货地址-增加-控制器层
(a) 统一处理异常
(b) 设计请求
设计“xxx”的请求方式:
请求路径:/users/reg
请求参数:User user
请求方式:POST
响应数据:JsonResult<Void>
© 处理请求