SpringBoot聚合项目:达内知道(七)-声明式事务、统一异常处理、文件上传
1.声明式事务
1.1 新增问题的程序漏洞
如果在新增问题的业务逻辑层运行过程中发生了异常,就会导致问题可能新增完毕,但是问题和关系(标签、讲师)新增失败的情况,这样就可能出现一个问题没有关联任何标签,或者没有关联任何讲师的情况。这种情况是不好的,在企业开发过程中,必须避免这种情况的发生。因为这样会引发数据安全性问题,可能导致程序运行过程中发生异常,要想避免这个问题就需要使用到事务。
1.2 什么是事务?
定义:是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。
常见面试题
数据库事务的四个特性,简称ACID特性
-
原子性(Atomicity):事务作为一个整体被执行,事务中的操作要么都成功,要么都失败(要执行都执行,要不执行都不执行);
-
一致性(Consistency):事务运行前后,一致状态保持不变,例如:转账前后银行中的总金额不变;
-
隔离性(Isolation):数据库支持多个事务并发执行,互不干扰;
-
持久性(Durability):已经提交的事务对数据库的影响是持久的,不可临时退回。
1.3 Spring Boot 声明式事务
Spring Boot框架中整合了Spring事务的管理功能,允许在业务逻辑层方法上添加事务管理的注解,一旦添加该注解,此方法中的所有数据库操作被管理为一个事务,即要么都执行,要么都不执行,就能解决上面的问题了。在这个方法运行过程中,如果没有发生异常,事务中数据库的操作就会正常提交生效;如果发生了异常,该方法中所有数据库操作全部取消,回滚(恢复)为运行之前的状态。
在实现类QuestionServiceImpl的saveQuestion方法上添加@Transactional注解:
//新增用户发布的问题
为了安全起见,可以在前面接收注册表单数据的vo类RegisterVo上也加上事务管理的注解。
2.统一异常处理
2.1 为什么需要统一异常处理?
在注册和发布问题的控制层代码中,需要对业务逻辑层进行异常处理,这样的异常处理在以后的功能中会不断出现,这样异常处理的代码就会形成冗余,代码显得臃肿,不方便维护。
2.2 Spring Mvc的统一异常处理
由于开发过程中大多是由控制器承担异常处理的,所以SpringMvc框架提供了一套异常处理的机制,能够自动处理控制层中发生的各种异常,在控制层代码中就不需要编写try-catch了。我们只需要根据SpringMvc提供的格式编写一个类即可,在controller包中创建类ExceptionControllerAdvice,代码如下:
package cn.tedu.knows.portal.controller;
import cn.tedu.knows.portal.exception.ServiceException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
//@RestControllerAdvice:表示当前类是给其他控制器类新增功能的
//我们这里的功能指异常处理
统一异常处理类运行流程:
3.文件上传
3.1 什么是文件上传?
即客户端将文件复制到服务器的过程,我们在static文件夹下创建一个可以上传文件的页面如下:upload.html
3.2 实现简单的同步上传
在SystemController类中添加如下方法:
启动服务,访问路径:http://localhost:8888/upload.html,按照学生身份进行登录,登录后显示效果如下:
选择文件上传后效果如下:
(1)浏览器
(2)控制台
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>文件上载演示</title>
<script src="bower_components/jquery/dist/jquery.min.js"></script>
<!--引入axios框架-->
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.21.1/axios.min.js"></script>
</head>
<body>
<form id="demoForm">
<div>
<label>上传文件
<input id="imageFile" type="file" name="imageFile">
</label>
</div>
<button type="submit">上传文件</button>
</form>
<img id="image" src="" alt="">
<script src="js/utils.js"></script>
<script>
$("#demoForm").submit(function () {
console.log("上传文件方法运行");
return false;//阻止表单提交,测试时点击提交按钮后无反应,不跳转页面
})
</script>
</body>
</html>
启动服务进行测试,登录后,访问http://localhost:8888/upload.html,上传图像文件,上传时页面无跳转,打开F12控制台输出“上传文件方法运行”,表示上传文件方法运行了。
注意:点击提交后页面跳转了,或者浏览器控制台没有输出都不行。
在上面的基础上继续编写代码,进行异步表单提交数据:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>文件上载演示</title>
<!--添加引用-->
<script src="bower_components/jquery/dist/jquery.min.js"></script>
<!--引入axios框架-->
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.21.1/axios.min.js"></script>
</head>
<body>
<form id="demoForm">
<div>
<label>上传文件
<input id="imageFile" type="file" name="imageFile"><!--对应下面上传文件的名字-->
</label>
</div>
<button type="submit">上传文件</button>
</form>
<img id="image" src="" alt="">
<!--引入js文件-->
<script src="js/utils.js"></script>
<script>
//提交文件的方法
$("#demoForm").submit(function () {
console.log("上传文件方法运行");
//根据用户设置的id获得用户选中的文件(数组),files表示文件域,代表所有文件
//可以一次选多个文件,但是现在还未设置,每次只能选中一个文件,故上传文件返回值类型files是数组
let files = document.getElementById("imageFile").files;//获取选中所有标签赋值到files中
//判断是否得到了选择的文件
if(files.length>0){
//对用户选中的文件执行上传,单独编写uploadImage方法执行上传
uploadImage(files[0]);//每次只能上传数组中的第一张图片
}else{
//没有得到文件,提示上传
alert("请选择文件");
}
//阻止表单提交
return false;
})
//上传文件的方法
function uploadImage(file) {
//上面阻止了表单提交,此处构建新的表单
let form = new FormData();
//在表单中添加文件
form.append("imageFile",file);//两个参数分别对应文件名key和值value
//利用axios异步提交数据
axios({
url:"/upload/image",
method:"post", //上传文件要用post
data:form //提交的内容不要加上“ ”,否则变成字符串
}).then(function (response) {
if(response.data=="success"){
console.log("上传成功!");
}else{
console.log("上传失败!");
}
})
}
</script>
</body>
</html>
先在application.properties配置文件中配置上传文件保存的路径,后续会调用:
# 配置上传文件保存的路径
knows.resource.path=file:C:/upload
# 配置当前项目访问路径,后续子项目用
knows.resource.host=http://localhost:8899
在SystemController控制器中新增一个方法用于异步上传数据:
// 这个属性的值会从application.properties文件中获得
// 获得的是knows.resource.path对应的值,注意此处是{}而不是()
@Value("${knows.resource.path}")
private File resourcePath;//File类型
// 获得的是knows.resource.host对应的值
@Value("${knows.resource.host}")
private String resourceHost;//String类型
//接收上传图片的表单数据
@PostMapping("/upload/image")
public String uploadImage(MultipartFile imageFile) throws IOException {//与form表单中imageFile对应
//按日期创建文件夹的路径 LocalDatetime对应年月日时分秒 LocalDatetime对应年月日
String path = DateTimeFormatter.ofPattern("yyyy/MM/dd")
.format(LocalDate.now());//年月日
// path: 2021/08/31
File folder=new File(resourcePath,path);//C:/upload, 2021/08/31,resourcePath已经在配置文件中配置,不用直接写绝对路径,路径可在配置文件中直接修改
// folder: F:/upload/2021/08/31
folder.mkdirs(); //mkdirsssssssssssssss!!!!!
log.debug("上传的文件夹为:{}",folder.getAbsolutePath());
//获得用户上传文件的原始后缀
// a.gif -> uuid.gif
// 01234
//获取文件名
String fileName=imageFile.getOriginalFilename();
//截取文件后缀名
String ext=fileName.substring(fileName.lastIndexOf("."));
// ext: .gif
//生成uuid确定文件名称
String name= UUID.randomUUID().toString() + ext;
//生成文件完整路径:name: jkha-sdjf-haj-sdhfj.gif
File file=new File(folder,name);//C:/upload/2021/08/31,xxx.xxx
// F:/upload/2021/08/31/jkha-sdjf-haj-sdhfj.gif
log.debug("最终上传的路径:{}",file.getAbsolutePath());
//执行上传
imageFile.transferTo(file);
return "success";
}
重启服务进行测试,访问路径为:http://localhost:8888/upload.html,登录后,进行上传图片,上传后没有反应,在浏览器上F12打开控制台,可以看到“上传成功!”,电脑磁盘指定文件夹内保存该图片,表示上传成功。
(1)浏览器
(2)IDEA
(3)电脑磁盘保存路径
如果报错,检查浏览器控制台和IDEA控制台报错信息,进行修改。