21. 用户-上传头像-持久层
(a) 规划所需要执行的SQL语句
在持久层处理“上传头像”时,也只是将数据表中的“头像”字段的值进行更新即可,至于值应该是多少,不由持久层来决定,只需要解决“更新头像的值”的功能即可,所以,需要执行的SQL语句大致是:
update t_user set avatar=?, modified_user=?, modified_time=? where uid=?
在执行更新之前,依然检查用户数据是否存在及状态,在持久层并不需要开发新的功能!
(b) 接口与抽象方法
在UserMapper
接口中添加:
Integer updateAvatarByUid(
@Param("uid") Integer uid,
@Param("avatar") String avatar,
@Param("modifiedUser") String modifiedUser,
@Param("modifiedTime") Date modifiedTime
);
(c) 配置映射
在UserMapper.xml中配置映射:
<!-- 修改某用户的头像 -->
<!-- Integer updateAvatarByUid(
@Param("uid") Integer uid,
@Param("avatar") String avatar,
@Param("modifiedUser") String modifiedUser,
@Param("modifiedTime") Date modifiedTime) -->
<update id="updateAvatarByUid">
UPDATE
t_user
SET
avatar=#{avatar},
modified_user=#{modifiedUser},
modified_time=#{modifiedTime}
WHERE
uid=#{uid}
</update>
在UserMapperTests
中测试:
@Test
public void updateAvatarByUid() {
Integer uid = 18;
String avatar = "jgfbjghgkuygv";
String modifiedUser = "密码管理员";
Date modifiedTime = new Date();
Integer rows = mapper.updateAvatarByUid(uid, avatar, modifiedUser, modifiedTime);
System.err.println("rows=" + rows);
}
22. 用户-上传头像-业务层
(a) 规划可能出现的异常
在执行更新头像之前,应该检查用户数据是否存在及状态是否正常,可能抛出UserNotFoundException
,在执行更新时,可能抛出UpdateException
。
(b) 业务接口及抽象方法
在IUserService
接口中添加抽象方法:
/**
* 修改头像
* @param uid 用户的id
* @param avatar 新的头像路径
* @param username 用户名
*/
void changeAvatar(Integer uid, String avatar, String username);
(c) 实现抽象方法
在UserServiceImpl
中实现以上抽象方法,实现过程可参考“修改密码”,将其简化即可:
@Override
public void changeAvatar(Integer uid, String avatar, String username) {
// 基于参数uid调用userMapper的findByUid()查询用户数据
User result = userMapper.findByUid(uid);
// 判断查询结果是否为null
if (result == null) {
// 是:抛出UserNotFoundException
throw new UserNotFoundException(
"修改用户头像失败!用户数据不存在!");
}
// 判断查询结果中的isDelete是否为1
if (result.getIsDelete() == 1) {
// 是:抛出UserNotFoundException
throw new UserNotFoundException(
"修改用户头像失败!用户数据不存在!");
}
// 调用userMapper的updateAvatarByUid()执行更新密码,并获取返回的受影响的行数
Integer rows = userMapper.updateAvatarByUid(uid, avatar, username, new Date());
// 判断受影响的行数是否不为1
if (rows != 1) {
// 是:UpdateException
throw new UpdateException(
"修改用户头像失败!执行更新用户头像时出现未知错误!请联系系统管理员!");
}
}
在UserServiceTests
中测试:
@Test
public void changeAvatar() {
try {
Integer uid = 1800;
String avatar = "1234";
String username = "密码管理员";
service.changeAvatar(uid, avatar, username);
System.err.println("OK.");
} catch (ServiceException e) {
System.err.println(e.getClass().getName());
System.err.println(e.getMessage());
}
}
23. 用户-上传头像-控制器层
(a) 处理新创建的异常
此次控制器层需要自行抛出异常!在处理上传的过程中,可能出现相关错误,这些错误都应该使用抛出异常的方式来表示,另外,再在统一处理异常的代码中进行处理!
如果客户端上传的文件为空,则抛出FileEmptyException
;
如果客户端上传的文件大小超出了限制,则抛出FileSizeException
;
如果客户端上传的文件类型超出了限制,则抛出FileTypeException
;
在调用transferTo()
方法保存客户端上传的文件时,该方法还会抛出IllegalStateException
和IOException
,但是,并不适合在控制器中将这2种异常直接抛出,交给统一处理异常的代码进行处理,因为后续其它的功能也许也会出现IOException
,处理方式可能应该不同!所以,在调用tranferTo()
方法时,应该使用try...catch
语法捕获这2种异常,并在处理时抛出自定义异常FileUploadStateException
和FileUploadIOException
!
另外,还应该创建以上异常类的基类FileUploadException
!
这些异常应该创建在cn.tedu.store.controller.ex
包中,其中,FileUploadException
是另5个异常的基类,它应该继承自RuntimeException
!
所以,现在需要创建6个异常类,其继承结构是:
RuntimeException
FileUploadException
FileEmptyException
FileSizeException
FileTypeException
FileUploadStateException
FileUploadIOException
在处理异常的方法之前,先将方法之前的注解改为:
@ExceptionHandler({ServiceException.class, FileUploadException.class})
即:使得该方法能够处理ServiceException
和FileUploadException
这2大类型的异常!
然后,再在处理异常的方法中,添加更多的else if
语句进行处理!
(b) 设计所需要处理的请求
请求路径:
请求参数:
请求方式:
响应结果:JsonResult<?>
(c) 处理请求
关于处理上传的基本代码如下:
@PostMapping("avatar/change")
public JsonResult<String> changeAvatar(
MultipartFile file, HttpSession session) throws Exception {
// 日志
System.err.println("UserController.changeAvatar()");
// 判断文件是否为空
boolean isEmpty = file.isEmpty();
System.err.println("\tisEmpty=" + isEmpty);
// 获取文件大小
long size = file.getSize();
System.err.println("\tsize=" + size);
// 获取文件的MIME类型
String contentType = file.getContentType();
System.err.println("\tcontentType=" + contentType);
// 获取原始文件名(客户端设备中的文件名)
String originalFilename
= file.getOriginalFilename();
System.err.println("\toriginalFilename=" + originalFilename);
// 将文件上传到哪个文件夹
String parent = session
.getServletContext().getRealPath("upload");
System.err.println("\tupload path=" + parent);
File dir = new File(parent);
if (!dir.exists()) {
dir.mkdirs();
}
// 保存上传的文件时使用的文件名
String filename = ""
+ System.currentTimeMillis()
+ System.nanoTime();
String suffix = "";
int beginIndex = originalFilename
.lastIndexOf(".");
if (beginIndex >= 1) {
suffix = originalFilename
.substring(beginIndex);
}
String child = filename + suffix;
// 将客户端上传的文件保存到服务器端
File dest = new File(parent, child);
file.transferTo(dest);
// 将保存的文件的路径记录到数据库中
String avatar = "/upload/" + child;
System.err.println("\tavatar path=" + avatar);
Integer uid = getUidFromSession(session);
String username = getUsernameFromSession(session);
userService.changeAvatar(uid, avatar, username);
// 返回
return new JsonResult<>(OK, avatar);
}
24. 用户-上传头像-前端页面
------------------------------------
附1:关于文件上传
在实现“文件上传”时,如果使用传统的表单提交方式,首先,必须使用<input type="file" />
类型的控件,用于客户端浏览并选择所需要上传的文件,并且,在<form>
标签中,必须配置method="post"
和enctype="multipart/form-data"
!为了保证表单能提交,还应该在<form>
标签中设置action
属性,在上传控件中添加name
属性,并保证按钮是submit
类型的!
所以,在传统方式提交时,上传所需的最小化代码:
<form action="/users/avatar/change" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" value="上传" />
</form>
在服务器端,就需要使用控制器接收上传的请求,则在控制器类中添加处理请求的方法:
@PostMapping("avatar/change")
public JsonResult<Void> changeAvatar(MultipartFile file) throws Exception {
// 将客户端上传的文件保存到服务器端
File dest = new File("d:/chengheng/1.png");
file.transferTo(dest);
return null;
}
在处理上传时,需要注意几个常规问题:
-
可以在处理请求的方法中添加
HttpServletRequest
参数,调用其getServletContext().getRealPath()
方法获取某个文件夹的实际路径,该文件夹应该是文件上传到的位置; -
应该自行制定策略,保证上传的文件不会覆盖此前上传的文件;
-
调用
MultipartFile
的getOriginalFilename()
可以获取原始文件名,从中得到原始文件的扩展名,用于保存上传的文件时指定扩展名。
SpringBoot框架设置了默认上传文件的限制,如果需要自定义限制值,可以在application.properties中进行配置!根据使用不同的SpringBoot框架的版本,对应的设置值不一样
Spring Boot 1.3.x and earlier
multipart.maxFileSize
multipart.maxRequestSize
Spring Boot 1.4.x and 1.5.x
spring.http.multipart.maxFileSize
spring.http.multipart.maxRequestSize
Spring Boot 2.x
spring.servlet.multipart.maxFileSize
spring.servlet.multipart.maxRequestSize
如果,直接在启动类的声明之前添加@Configuration
注解,并在启动类的内部添加方法进行配置:
@Bean
public MultipartConfigElement getMultipartConfigElement() {
MultipartConfigFactory factory = new MultipartConfigFactory();
factory.setMaxFileSize(DataSize.ofMegabytes(100));
factory.setMaxRequestSize(DataSize.ofMegabytes(100));
return factory.createMultipartConfig();
}
在服务器端的控制器处理上传时,关于MultipartFile
接口的API有:
-
String getOriginalFilename()
:获取客户端上传的文件的原始文件名; -
boolean isEmpty()
:判断上传的文件是否为空,如果在表单中没有选择文件,或选择的文件是0字节则视为空,如果为空,则返回true
,如果不为空,则为false
; -
long getSize()
:获取文件的大小,以字节为单位; -
String getContentType()
:获取文件的MIME类型; -
InputStream getInputStream()
:获取文件的输入流,通常用于自定义处理客户端提交的文件的数据,该方法不可以与tranferTo()
同时使用; -
void transferTo(File dest)
:保存客户端上传的文件,该方法不可以与getInputStream()
同时使用;