本次主要是使用SpringBoot+Mybatis+MySQL完成用户的头像上传。
在数据库中t_user表中的字段avator类型为varchar(50),这么设计有以下原因:
将对象文件保存到操作系统上,然后再把这个文件路径记录下来,因为记录路径是非常便捷和方便的,如果要打开这个文件,可以依据这个路径找到这个文件,所以在数据库中需要保存这个文件的路径即可。大点的公司会将所有的静态资源,比如图片、文件、其他资源文件存放到某台电脑上,再把这台电脑作为一台单独的服务器使用。
故对应的是一个更新用户avator字段的SQL语句:
update t_user set avatar=?.modified_user=?,modified_time? where uid = ?
UserMapper接口中来定义个抽象方法用于修改用户的头像。
/**
* @Param("SQL映射文件中#{}占位符的变量名") 解决的问题,当SQL语句的占位符和映射的接口方法参数名不一致时,
* 需要将某个参数强行注入到某个占位符变量上时,可以使用@Param这个注解来标注映射的关系
* 根据用户uid值来修改用户的头像
* @param uid
* @param avatar
* @param modifiedUser
* @param modifiedTime
* @return
*/
Integer updateAvatarByUid(
@Param("uid") Integer uid,
@Param("avatar") String avatar,
@Param("modifiedUser") String modifiedUser,
@Param("modifiedTime") Date modifiedTime);
UserMapper.xml文件中编写映射的SQL语句。
<update id="updateAvatarByUid">
UPDATE t_user
SET
avatar=#{avatar},
modified_user = #{modifiedUser},
modified_time = #{modifiedTime}
WHERE uid = #{uid}
</update>
在测试类中编写测试的方法
@Test
public void updateAvatarByUid() {
userMapper.updateAvatarByUid(13,"/upload/avatar.png","管理员", new Date());
}
运行test,运行成功后去数据库进行结果验证。
三个参数,uid和modified_user可以从HttpSession中获取。
/**
* 修改用户的头像
* @param uid 用户的id
* @param avatar 用户头像的路径
* @param username 用户的名称
*/
void changeAvatar(Integer uid, String avatar, String username);
编写业务层的更新用户头像的方法
@Override
public void changeAvatar(Integer uid, String avatar, String username) {
// 查询当前的用户数据是否存在
User result = userMapper.findByUid(uid);
if (result == null || result.getIsDelete() == 1) {
throw new UserNotFoundException("用户数据不存在");
}
Integer rows = userMapper.updateAvatarByUid(uid, avatar, username, new Date());
if (rows != 1) {
throw new UpdateException("更新用户头像时产生未知的异常");
}
}
测试业务层方法的执行。
@Test
public void changeAvatar() {
userService.changeAvatar(13, "/upload/test.png", "admin01");
}
文件异常的父类:
FileUploadException 泛指文件上传的异常(父类)继承RuntimeException
父类是:FileUploadException
FileEmptyException 文件为空的异常
FileSizeException 文件大小超出限制
FileStateException 文件状态的异常
FileTypeException 文件类型异常
FileUploadIoException 文件读写的异常
5个构造方法显式的声明出来,再去继承相关的父类。
在基类BaseController类中进行编写和统一处理。
else if (e instanceof FileEmptyException) {
result.setState(5003);
result.setMessage("文件为空的异常");
} else if (e instanceof FileSizeException) {
result.setState(6000);
result.setMessage("文件大小限制的异常");
} else if (e instanceof FileTypeException) {
result.setState(6001);
result.setMessage("文件类型的异常");
} else if (e instanceof FileStateException) {
result.setState(6002);
result.setMessage("文件状态的异常");
} else if (e instanceof FileUploadIOException) {
result.setState(6003);
result.setMessage("文件读写的异常");
}
在异常统一处理方法的参数列表上增加新的异常处理作为它的参数。
@ExceptionHandler({ServiceException.class, FileUploadException.class})
/users/change_avatar
POST (get请求提交数据2KB)
HttpSession session, MultipartFile file
JsonResult<String>
UserController.java中
/* 设置上传文件的最大值10M */
public static final int AVATAR_MAX_SIZE = 10 * 1024 * 1024;
/* 限制上传文件的类型 */
public static final List<String> AVATAR_TYPE = new ArrayList<>();
static { // 初始化
AVATAR_TYPE.add("image/jpeg");
AVATAR_TYPE.add("image/png");
AVATAR_TYPE.add("image/bmp");
AVATAR_TYPE.add("image/gif");
}
/**
* MultiPartFile 接口式SpringMVC提供的一个接口,这个接口为我们包装了获取文件类型的数据(任何类型的file都可以接收)
* SpringBoot它又整合了SpringMVC,只需要在处理请求的方法参数列表上声明一个参数类型为MultiPartFile的参数,
* 然后SpringBoot自动将传递给服务的文件数据赋值给这个参数。
* @RequestParam,表示请求中的参数,将请求中的参数注入请求处理方法的某个参数上,
* 如果名称不一致可以使用@RequestParam注解进行标记和映射
* @param session
* @param file
* @return
*/
@RequestMapping("change_avatar")
public JsonResult<String> changeAvatar(HttpSession session, MultipartFile file) {
// 判断文件是否为null
if (file.isEmpty()) {
throw new FileEmptyException("文件为空");
}
if (file.getSize() > AVATAR_MAX_SIZE) {
throw new FileSizeException("文件超出限制");
}
// 判断文件的类型是否是规定的后缀类型
String contentType = file.getContentType();
// 如果集合包含某个元素则返回值true
if (!AVATAR_TYPE.contains(contentType)) {
throw new FileTypeException("文件类型不支持");
}
// 上传的文件.../upload/文件.png
String parent = session.getServletContext().getRealPath("upload");
// File对象指向这个路径,File是否存在
File dir = new File(parent);
//检测目录是否存在
if (!dir.exists()) {
// 创建目录
dir.mkdirs();
}
// 上传的文件名称随机生成,获取到这个文件名称,UUID工具生成一个新的字符串作为文件名
// 例如:avatar01.png,保存后缀
String originalFileName = file.getOriginalFilename();
System.out.println("originalFileName = " + originalFileName);
int index = originalFileName.lastIndexOf(".");
String suffix = originalFileName.substring(index);
String filename = UUID.randomUUID().toString().toUpperCase() + suffix;
// 是一个空文件
File dest = new File(dir, filename);
// 参数file中数据写入到这个空文件中
try {
// 将file文件中的数据写入到dest文件中
file.transferTo(dest);
} catch (FileStateException e) {
throw new FileStateException("文件状态异常");
} catch (IOException e) {
throw new FileUploadIOException("文件读写异常");
}
Integer uid = getuidFromSession(session);
String username = getUsernameFromSession(session);
// 返回头像的路径,返回相对路径即可 /upload/test.png
String avatar = "/upload/" + filename;
userService.changeAvatar(uid, avatar, username);
// 返回用户头像的路径给前端页面,用于头像的展示使用
return new JsonResult<>(OK, avatar);
}
在upload.html页面中编写上传头像的代码。
说明:如果直接使用表单进行文件的上传,需要给表单显式的添加一个属性enctype=”multipart/form-data“声明出来,不会将目标文件的数据结构做修改再上传,不同于字符串。
form表单中增加:
action="/users/change_avatar"
method="post"
enctype="multipart/form-data"
参考下图:
SpringMVC默认为1MB文件可以进行上传。手动修改SpringMVC默认上传文件的大小。
方式1:直接可以在配置文件中(application.properties)进行配置。
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=15MB
方式2:采用java代码的形式来来设置文件的上传大小的限制。
在主类中进行配置,因为主类的加载时间是最早的。
可以定义一个方法,必须用@Bean修饰,在类的前面添加@Configuration注解进行修饰;返回值是MultipartConfigElement类型。
@Configuration //表示配置类
@SpringBootApplication
// MapperScan注解指定当前项目中的Mapper接口路径的位置,在项目启动时会自动加载所有的接口
@MapperScan("com.cy.store.mapper")
public class StoreApplication {
public static void main(String[] args) {
SpringApplication.run(StoreApplication.class, args);
}
@Bean
public MultipartConfigElement getMultipartConfigElement() {
// 创建一个配置的工厂类对象
MultipartConfigFactory factory = new MultipartConfigFactory();
// 设置需要创建的对象的相关信息
factory.setMaxFileSize(DataSize.of(10, DataUnit.MEGABYTES));
factory.setMaxRequestSize(DataSize.of(15, DataUnit.MEGABYTES));
// 通过工厂类来创建MultipartConfigElement对象
return factory.createMultipartConfig();
}
}
上面的处理方式虽然可以上传成功,实际页面上的图片在返回后是没有更新的,针对这个问题,需要在upload.html文件中做一些改动,通过ajax请求来提交文件,提交完成后返回了json串,并解析出data中的数据,再设置到img头像标签的属性上就可以了。
对upload.html做的修改
删除上面添加的:
action="/users/change_avatar"
method="post"
enctype="multipart/form-data"
增加id属性:
id="form-change-avator"
将submit改为button,并增加id属性:
<input id="btn-change-avatar" type="button" class="btn btn-primary" value="上传" />
之前在前端页面一直使用serialize()解析,但他不能解析文件中的数据,下面做一些说明:
new FormData($("#form")[0]); // 文件类型的数据可以使用FormData对象进行存储
ajax默认处理数据时是按照字符串的形式进行处理,以及默认会采用字符串的形式提交数据,所以需要关闭这两个默认的功能。
上面的完成后,如果跳转到其他页面再到上传头像页签,之前上传的头像还是不显示的。
可以在上传头像成功后,将服务器返回的头像路径保存到客户端cookie对象中,每次检测到用户打开上传头像页面,可以通过ready()方法来自动检测并读取cookie中的头像设置到src属性上。
① 设置cookie中的值
需要在login.html中设置,导入cookie.js文件
<script src="../bootstrap3/js/jquery.cookie.js" type="text/javascript" charset="utf-8"></script>
调用cookie方法
$.cookie(key, value, time); // cookie的存活时间单位是:天
② 在upload.html页面先引入cookie.js文件
<script src="../bootstrap3/js/jquery.cookie.js" type="text/javascript" charset="utf-8"></script>
③ 在upload.html页面通过ready()函数自动读取cookie中的数据
$(document).ready(function () {
let avatar = $.cookie("avatar");
console.log(avatar);
// 将cookie值获取出来设置到头像的src属性上
$("#img-avatar").attr("src", avatar);
});
在更改完头像后,将最新的头像地址,再次保存到cookie,同名保存会覆盖原有cookie中的值。
$.cookie("avatar", json.data, {expires: 7});