续 开发学生发布问题功能
实现讲师缓存
上次课我们成功的查询了所有讲师的信息显示在了讲师列表中
但是每次访问问题发布页,都需要到数据库查询所有讲师的话
连库次数较多,而且每次查询都是重复的结果
所以讲师和标签一样,也可以缓存起来
而且后面我们也需要根据讲师昵称获得讲师对象,所以也缓存一个包含所有讲师的Map
这样和标签缓存结构是一样的
IUserSevice接口中添加获得所有讲师Map的方法
// 查询所有讲师Map的业务逻辑层方法
Map<String,User> getTeacherMap();
UserServiceImpl实现代码如下
//声明两个讲师缓存的集合
private List<User> teachers=new CopyOnWriteArrayList<>();
private Map<String,User> teacherMap=new ConcurrentHashMap<>();
@Override
public List<User> getTeachers() {
if(teachers.isEmpty()){
synchronized (teachers){
teachers.clear();
teacherMap.clear();
List<User> list=userMapper.findTeachers();
teachers.addAll(list);
for(User u:list){
teacherMap.put(u.getNickname(),u);
}
}
}
// 最后别忘了返回teachers
return teachers;
}
@Override
public Map<String, User> getTeacherMap() {
if(teacherMap.isEmpty()){
getTeachers();
}
return teacherMap;
}
学生发布问题的业务逻辑详解
和注册相比
发布问题不需要注册一样去判断各种信息
但是新增问题这个业务有更多的关联关系
举例示意
学生发布问题到数据库的核心逻辑步骤
1.根据学生提交的表单信息,新增Question对象到数据库
2.再新增question_tag表中的问题和标签的关系
3.最后新增user_question表中问题和讲师的关系
编写发布问题的业务逻辑层代码
本次功能开发Mapper也是不需要新的方法的
IQuestionService接口中新增发布问题的方法
// 用户发布问题的业务逻辑层方法
void saveQuestion(QuestionVO questionVO,String username);
QuestionServiceImpl实现类
@Autowired
private IUserService userService;
@Autowired
private QuestionTagMapper questionTagMapper;
@Autowired
private UserQuestionMapper userQuestionMapper;
@Override
public void saveQuestion(QuestionVO questionVO, String username) {
// 1.根据用户名获得用户信息
User user=userMapper.findUserByUsername(username);
// 2.将用户选中的标签数组拼接为标签名称字符串
// {"Java基础","Java SE","面试题"}
// 目标"Java基础,Java SE,面试题"
StringBuilder builder=new StringBuilder();
for(String tagName:questionVO.getTagNames()){
builder.append(tagName).append(",");
}
// "Java基础,Java SE,面试题," 多了个"," 要删除
builder.deleteCharAt(builder.length()-1);
// builder转换成字符串
String tagNames=builder.toString();
// 3.收集Question信息,实例化并赋值
Question question=new Question()
.setTitle(questionVO.getTitle())
.setContent(questionVO.getContent())
.setUserNickName(user.getNickname())
.setUserId(user.getId())
.setCreatetime(LocalDateTime.now())
.setStatus(0)
.setPageViews(0)
.setPublicStatus(0)
.setDeleteStatus(0)
.setTagNames(tagNames);
// 4.新增question到数据库
int num=questionMapper.insert(question);
if(num!=1){
throw new ServiceException("数据库忙");
}
// 5.新增question_tag关系表
Map<String,Tag> tagMap=tagService.getTagMap();
// 遍历用户选中的所有标签
for(String tagName : questionVO.getTagNames()){
// 根据当前标签名称获得对应的标签对象
Tag t=tagMap.get(tagName);
// 实例化QuestionTag实体类,并赋值
QuestionTag questionTag=new QuestionTag()
.setQuestionId(question.getId())
.setTagId(t.getId());
// 执行新增流程
num=questionTagMapper.insert(questionTag);
if(num!=1){
throw new ServiceException("数据库忙!");
}
log.debug("新增问题和标签的关系:{}",questionTag);
}
// 6.新增user_question关系表
Map<String,User> teacherMap=userService.getTeacherMap();
for(String nickname:questionVO.getTeacherNicknames()){
User teacher=teacherMap.get(nickname);
UserQuestion userQuestion=new UserQuestion()
.setQuestionId(question.getId())
.setUserId(teacher.getId())
.setCreatetime(LocalDateTime.now());
num=userQuestionMapper.insert(userQuestion);
if(num!=1){
throw new ServiceException("数据库忙");
}
log.debug("新增了问题和讲师的关系:{}",userQuestion);
}
}
编写发布问题的控制层代码
上次课我们已经完成了QuestionController类中控制器方法接收表单信息的功能
下面要完成的其实就是对业务逻辑层方法的调用
要新加对当前登录用户信息的获取
createQuestion方法具体代码如下
@PostMapping("")
public String createQuestion(
@Validated QuestionVO questionVO,
BindingResult result,
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
@AuthenticationPrincipal UserDetails user
){
log.debug("接收表单信息:{}",questionVO);
if(result.hasErrors()){
String msg=result.getFieldError().getDefaultMessage();
return msg;
}
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
try {
// 这里调用业务逻辑层方法运行新增问题
questionService.saveQuestion(questionVO, user.getUsername());
return "ok";
} catch (ServiceException e){
log.error("业务异常",e);
return e.getMessage();
}
}
改写提交完成的路径
上传我们测试提交表单时,发生了404错误
原因是js代码中,编写了在提交完成后跳转到/index.html的代码
因为/index.html页面不存在,所以发生了404错误
我们要修改一下
createQuestion.js文件的28行
if(r.status==OK){
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
location.href="/index_student.html";
}
重启服务,测试发布问题的效果
如果成功,页面会跳转到学生首页
学生首页会显示你刚刚新增成功的问题
Spring声明式事务
什么是事务
一般程序中提到"事务"指数据库事务
数据库事务:是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成
我们开发的用户发布问题的功能整体就是一个数据库事务
如果我们发布问题程序运行过程中,发生了异常,我们对数据库各个信息和关系的新增操作可能并没有完成,但是程序就结束了,运行完的程序对数据库的修改已经生效,这种情况被称之为"数据库完整性缺失"
数据库完整性缺失:简单来说就是数据库中包含了错误的数据,或没有意义上的数据
数据库完整性缺失会导致我们的程序埋下隐患,会导致程序bug
我们要防止这种数据库操作运行一半就停止的情况发送
如果我们在程序中使用Spring声明式事务就可以解决这个问题了
为什么使用事务
如果我们将数据库的操作保存在一个事务中,
在代码运行过程中,如果发生异常,那么数据会恢复为事务运行之前的状态
这种操作叫撤销也叫回滚(roll back)
如果事务运行正常,那么所有数据库操作结果会一起提交到数据库
简单来说
保存在一个事务中的所有数据库操作要么都成功,要么都失败
常见面试题
数据库事务的四大特性(ACID特性)
-
原子性(Atomicity):
事务是一个整体,是操作数据库的最小单位,其中对数据库操作的指令不可再分,也就是这个事务中的数据库操作要么都执行要么都不执行
-
一致性(Consistency):
事务运行前后,数据库状态是一致的
-
隔离性(Isolation):
数据中多个事务可以并发,事务之间互不影响
-
持久性(Durability):
也译作"永久性",意思是数据库事务对数据的影响是永久的,不是临时的,不会随意退回之前的状态
SpringBoot项目使用声明式事务
如果我们想在SpringBoot框架中开启事务功能
方式数据库完整性缺失的现象,是非常简单的
我们只需要在需要为事务来支持数据库操作的业务逻辑层方法上添加一个注解,这个业务逻辑层方法中所有对数据库的操作就会自动保存在一个事务中
要么都执行,要么都不执行
如果业务逻辑层运行一切正常,最终事务会提交,全部数据库操作都执行
如果业务逻辑层运行发生异常(无论是系统异常还是自定义异常),最终事务会回滚(撤销),全部数据库操作都不执行
@Override
// Spring声明式事务
// 标记之后,当前被标记的方法中所有数据库操作保存在一个事务中
// 方法运行正常,事务提交,数据库操作生效,方法运行发生异常,事务回滚,数据库操作撤销
// 今后只要是包含两个以及两个以上的数据库增删改操作的方法,都必须添加该注解
@Transactional
public void saveQuestion(QuestionVO questionVO, String username) {
//......
}
一个业务逻辑层方法中包含两个以及两个以上的增删改操作时
需要在方法前添加@Transactional注解
我们编写的注册功能,也是应该添加事务注解的
统一异常处理
我们编写的注册和发布问题的控制层代码,都需要通过编写try-catch结构来调用业务逻辑层方法,以便捕获业务逻辑层代码可能发生的异常
这个try-catch结构会给程序代码带来一定的冗余,造成维护的不便
try {
questionService.saveQuestion(questionVO, user.getUsername());
return "ok";
} catch (ServiceException e){
log.error("业务异常",e);
return e.getMessage();
}
我们可以利用SpringMvc框架提供的"统一异常处理"的功能,简化控制器方法对业务逻辑层方法的调用和异常的捕获
免去编写try-catch结构和冗余提高代码的维护性
因为我们下面要编写的统一异常处理类的功能再控制层,所以我们可以将统一异常处理来创建在controller包中
创建一个类名为(推荐名称)ExceptionControllerAdvice代码如下
// 这个注解表示指定控制器方法运行到特殊时间节点时
// 可以运行的额外代码的方法,我们这里只考虑控制器方法发生异常时运行的代码
// ↓↓↓↓↓↓
@RestControllerAdvice
@Slf4j
public class ExceptionControllerAdvice {
// 下面编写一个注解,表示该方法是专门处理控制器发生的异常的
@ExceptionHandler
// 返回值为String,设计为当发生异常时,将异常信息返回为axios
// 方法名称随意,标准是handleXXXXXException
// 参数指定控制器发生什么异常类型时运行这个方法
public String handleServiceException(ServiceException e){
// 这个方法中的代码等价于控制器方法中try-catch结构中的catch
log.error("发生业务异常",e);
return e.getMessage();
}
// 再编写一个处理其他异常的方法
// 直接声明为异常的父类类型即可
@ExceptionHandler
public String handleException(Exception e){
log.error("发生其他异常",e);
return e.getMessage();
}
}
我们可以将当前注册和发布问题的控制器中的try-catch结构都删除了
测试注册功能是否还能正常提供错误提示
实现文件上传
创建文件上传的页面
给大家提供上传页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>文件上载演示</title>
</head>
<body>
<form id="demoForm" method="post"
enctype="multipart/form-data"
action="/upload/file" >
<div>
<label>上传文件
<input id="imageFile" type="file" name="imageFile">
</label>
</div>
<button type="submit">上传文件</button>
</form>
<img id="image" src="" alt="">
</body>
</html>
实现同步上传功能
所谓上传就是用户将客户端的文件\图片保存到服务器端的过程
我们编写的是服务器端的代码
在SystemController类中编写一个支持文件上传的代码
// 上传图片必须是post
@PostMapping("/upload/file")
public String upload(MultipartFile imageFile) throws IOException {
// 我们要确定文件\图片保存的位置
// 文件保存的路径是F:/upload/年/月/日
// 先获得当前日期的字符串做路径
// path: 2022/03/11
String path= DateTimeFormatter.ofPattern("yyyy/MM/dd")
.format(LocalDateTime.now());
// 确定要上传的文件夹
// folder= F:/upload/2022/03/11
File folder=new File("F:/upload/"+path);
// 创建这个文件夹
folder.mkdirs(); //mkdirsssssssss!!!!!!
// 下面要确定要上传文件的文件名
// 获得用户原始文件名,以便获得后缀名
String filename=imageFile.getOriginalFilename();
// kas.jd.kk.jpg
// 0123456789
// ext= .jpg
String ext=filename.substring(filename.lastIndexOf("."));
// 随机生成文件名
// jhsdjf-uwey-udsf-jahdjkfhajdskf.jpg
String name= UUID.randomUUID().toString()+ext;
// 最终文件位置(路径名+文件名)
// F:/upload/2022/03/11/jhsdjf-uwey-udsf-jahdjkfhajdskf.jpg
File file=new File(folder,name);
// 利用日志将实际上传文件的路径输出到控制台
log.debug("文件上传到:{}",file.getAbsolutePath());
// 执行上传
imageFile.transferTo(file);
// 返回成功提示
return "ok";
}
因为upload.html页面内容已经编写好
我们直接重启服务访问upload.html页面
选中文件后提交,检查目标位置时候出现上传的文件
实现异步上传
实际开发中,上传文件或图片都是使用异步的方式
我们修改upload.html页面,在页面中编写js代码实现异步上传功能
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>文件上载演示</title>
<script src="bower_components/jquery/dist/jquery.min.js" ></script>
<script src="bower_components/bootstrap/dist/js/bootstrap.min.js" ></script>
<script src="bower_components/vue/dist/vue.js"></script>
<!--引入CDN服务器的框架文件-->
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.21.1/axios.min.js"></script>
</head>
<body>
<form id="demoForm" method="post"
enctype="multipart/form-data"
action="/upload/file" >
<div>
<label>上传文件
<input id="imageFile" type="file" name="imageFile">
</label>
</div>
<button type="submit">上传文件</button>
</form>
<img id="image" src="" alt="">
<script>
// 我们编写jQuery代码,在表单提交时触发编写好的代码,并阻止表单原有的提交效果
$("#demoForm").submit(function(){
// 这个方法会在id为demoForm的表单提交时运行
// 编写代码获得用户选中的文件\图片
let files=document.getElementById("imageFile").files;
// 判断是否选中了文件
if(files.length>0){
// 将用户选中的文件传给自定义方法完成上传
uploadFile(files[0]);
}else{
alert("请选择要上传的文件\图片");
}
// jQuery的功能,返回false表示阻止表单提交
return false;
})
function uploadFile(file){
let form=new FormData();
form.append("imageFile",file);
axios({
url:"/upload/file",
method:"post",
data:form
}).then(function (response){
console.log(response.data);
})
}
</script>
</body>
</html>
重启服务,进行上传测试
如果点击"上传文件"按钮之后,F12控制台中出现ok
指定的路径也出现了图片,那么异步上传就成功了!
的功能分离开,设计为多个服务器,有的服务器专门负责一般业务,有的服务器专门负责静态资源,二者各司其职互不干扰,而且都会有更好的性能
下面我们就来创建管理静态资源的项目,这个项目负责显示上传的图片
创建子项目knows-resource
不用勾选任何选项直接下一步
父子相认
父项目pom.xml文件
<module>knows-resource</module>
子项目pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.tedu</groupId>
<artifactId>knows</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.tedu</groupId>
<artifactId>knows-resource</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>knows-resource</name>
<description>Demo project for Spring Boot</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
因为我们删除了测试依赖,所以直接删除test文件夹
knows-resource项目的application.properties添加配置如下
# 配置当前静态资源服务器端口号
# 因为静态资源服务器和portal项目要同时启动,所以不能再使用8080
server.port=8899
# 因为我们静态资源服务器要显示的图片都在F:/upload目录下
# 所以我们重新定位当前项目的静态资源路径
# 重新定位后,我们实现访问localhost:8899/a.jpg
# 相当于在访问F:/upload/a.jpg
spring.resources.static-locations=file:F:/upload
英文
Transactional:事务
Handle:处理
Handler:处理者(处理器)
Original:原始