续 Spring验证框架
上次课我们已经将实体类验证规则编写在属性上了
下面要启动SpringValidation框架的验证了
控制器中启动验证功能
我们的注册功能的控制器是SystemController
打开这个类,找到注册方法
修改代码如下
@PostMapping("/register")
public String register(
// 我们可以通过添加@Validated注解启动SpringValidation的验证
// 一旦在控制器方法参数前添加@Validated,表示控制器方法运行前
// 先由SpringValidation框架按照RegisterVO类中编写的验证规则进行验证
@Validated RegisterVO registerVO,
// 下面的参数就是SpringValidation框架验证的结果对象
// 对registerVO对象属性的验证信息会自动保存到result对象中
BindingResult result
) {
// 使用@Slf4j提供的log对象,将registerVO信息输出到日志
log.debug("接收到表单信息:{}", registerVO);
// 判断result对象中有没有验证失败的信息
if(result.hasErrors()){
// 进入if表示registerVO对象中有属性没有通过验证
// 下面来获取这个信息(信息就是验证未通过的message的值)
String msg=result.getFieldError().getDefaultMessage();
return msg;
}
try {
userService.registerStudent(registerVO);
return "ok";
} catch (ServiceException e) {
// 将错误信息输出到日志的error级别
log.error("注册失败", e);
// 控制器返回的字符串就是业务逻辑层中的错误信息文本
return e.getMessage();
}
}
下面可以进行测试
但是测试之前,要先删除html页面中的html5的表单验证代码
建议在测试成功之后,回复删除掉的html5验证代码
开发显示首页标签
首页标签效果
显示首页标签的开发流程
显示首页标签列表的开发流程
1.学生访问首页,在页面加载完毕时利用Vue调用axios请求
2.axios请求到TagController控制器中的方法
3.控制器方法调用TagService业务逻辑层中获得所有标签的方法
4.TagService会使用Mapper连接数据库查询所有标签并返回给控制器
5.控制器得到所有标签后显示在页面上
之前完成的注册时有表单提交的
下面要完成的查询所有标签的功能是没有表单的,普通的get请求
实际开发中,程序员一般会从底层编写代码
数据访问层->业务逻辑层->控制层
本次功能查询的所有标签的数据访问层方法,已经有MybatisPlus框架提供了,无需我们编写,所以直接从业务逻辑层开始
开发业务逻辑层
开发业务逻辑层先编写接口
查询所有表单明显是ITagService接口,添加方法如下
public interface ITagService extends IService<Tag> {
// 定义全查所有标签的业务逻辑层方法
List<Tag> getTags();
}
TagServiceImpl实现类编写代码如下
@Service
public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements ITagService {
@Autowired
private TagMapper tagMapper;
@Override
public List<Tag> getTags() {
// 调用查询所有标签的方法
List<Tag> tags=tagMapper.selectList(null);
// 千万别忘了返回tags
return tags;
}
}
编写控制层代码
TagController类编写调用业务逻辑层方法,获得所有标签
并将所有标签返回给前端页面
@RestController
@RequestMapping("/v1/tags")
public class TagController {
// 添加业务逻辑层的依赖注入
@Autowired
private ITagService tagService;
// @GetMapping("")写法的含义就是只使用类上定义的路径作为当前控制方法的路径
// localhost:8080/v1/tags
@GetMapping("")
public List<Tag> tags(){
List<Tag> tags=tagService.getTags();
return tags;
}
}
重启portal项目
我们想测试一下这个控制器方法是否能够正确运行,返回所有标签
我们可以打开浏览器,在浏览器地址栏直接输入localhost:8080/v1/tags
如果看到所有标签的json格式返回值表示一切正确
我们将这样的操作称之为"浏览器同步测试"
特别适合测试支持Get请求的控制器方法,今后我们可以多使用这种方法测试我们的java代码,尤其是报错的时候,可以定位错误出现的位置
显示结果如下
编写Vue绑定和Js代码
这次我们的页面换为了index_student.html
要想让这个页面显示出所有标签
我们要按下面步骤依次完成
1.添加axios的引用
<!--引入CDN服务器的框架文件-->
<script src="https://cdn.bootcdn.net/ajax/libs/axios/0.21.1/axios.min.js"></script>
</head>
2.在页面尾部添加js引用
<script src="js/utils.js"></script>
<script src="js/tags_nav.js"></script>
</body>
</html>
3.index_student.html的163行附近进行Vue的绑定操作
<!--引入标签的导航栏-->
<div class="container-fluid" th:fragment="tags_nav" >
<div class="nav font-weight-light" id="tagsApp">
<a href="tag/tag_question.html" class="nav-item nav-link text-info"><small>全部</small></a>
<a href="tag/tag_question.html"
class="nav-item nav-link text-info"
v-for="tag in tags">
<small v-text="tag.name">Java基础</small>
</a>
</div>
</div>
重启服务测试
发现如果不登录直接访问学生首页,有大面积空白,因为学生首页放行,但是/v1/tags没有放行导致的
必须在登录页登录后,再访问学生首页才正常
要解决这个问题我们采用取消学生首页放行的办法
原因有二
1:显示所有标签的控制器方法没有放行
2:更重要的是今后我们实现显示当前登录用户所有问题时,必须知道当前登录用户是谁
打开SecurityConfig类
注释或删除学生首页放行的行
.antMatchers(// 指定路径
//"/index_student.html",
"/css/*",
"/js/*",
"/img/**",
"/bower_components/**",
"/login.html",
"/register.html", // 放行注册页
"/register" // 放行注册控制器路径
)
再次重启服务,访问学生首页就必须先登录了
实现标签的缓存
什么是缓存
缓存指计算机(服务器)内存保存的数据,一般用于支持快速访问
简单来说,就是将一些经常被使用的数据,保存在内存中,以提高访问效率和速度
为什么需要缓存
因为如果一个数据保存在数据库中,经常被使用时反复连接数据库,效率低
如果将这个数据保存在内存中,在使用时直接从内存获取,提高访问效率,所以使用缓存来保存这些经常被访问的数据
缓存的使用情景
什么时候使用缓存
要结合我们计算机内存的优缺点
内存
优点:快
缺点:
- 相对硬盘容量小,不能将硬盘所有数据都保存,一旦内存空间不足,运行明显变慢
- 内存中的数据都是"易失"的,一旦断电,信息就没了
综合内存优缺点
行业中,一般满足下面3中情况时,这样的数据建议保存在缓存中
- 数据量不能太大
- 数据经常被访问或使用
- 数据库缓存的数据不应该被频繁修改,或对修改并不敏感
实现缓存全部标签列表
经过分析,我们的标签列表tags就适合保存在缓存中
以提高访问效率
下面我们就修改TagServiceImpl类,实现标签列表的缓存
// 声明缓存对象
private List<Tag> tags=new CopyOnWriteArrayList<>();
// tags属性用于充当保存所有标签的缓存对象
// 因为TagServiceImpl默认是单例作用域的,所以tags属性也只有一份
// 在今后任何方法的访问中,不会出现新的对象,重复占用内存
// CopyOnWriteArrayList是jdk1.8开始支持的线程安全的集合对象
@Override
public List<Tag> getTags() {
// 要获得所有标签
// 先判断缓存对象是不是没有元素
// 3
if(tags.isEmpty()){
synchronized (tags) {
// 2
tags.clear();
List<Tag> list = tagMapper.selectList(null);
tags.addAll(list);
System.out.println("tags加载完成");
}
// 1
}
// 千万别忘了返回tags
return tags;
}
开发显示学生问题列表
显示学生问题列表业务流程
怎么去查询一个用户的所有问题呢?
我们可以先编写一个sql语句
select * from question
where user_id=11 and delete_status=0
sql语句比较简单,而且只有在学生首页时需要查询
所以可以使用QueryWrapper来完成
所以直接开发业务逻辑层
开发业务逻辑层
先写接口方法
IQuestionService添加方法如下
public interface IQuestionService extends IService<Question> {
// 根据登录用户的用户名查询问题列表
List<Question> getMyQuestions(String username);
}
转到QuestionServiceImpl类编写实现代码如下
@Service
public class QuestionServiceImpl extends ServiceImpl<QuestionMapper, Question> implements IQuestionService {
@Autowired
private QuestionMapper questionMapper;
@Autowired
private UserMapper userMapper;
@Override
public List<Question> getMyQuestions(String username) {
// 先根据用户名查询用户信息(用户对象)
User user=userMapper.findUserByUsername(username);
// 再使用QueryWrapper完成该用户的问题列表的查询
QueryWrapper<Question> query=new QueryWrapper<>();
query.eq("user_id",user.getId());
query.eq("delete_status",0);
query.orderByDesc("createtime");
// 执行查询
List<Question> list=questionMapper.selectList(query);
// 别忘了返回list
return list;
}
}
编写控制层代码
QuestionController类添加方法
@RestController
@RequestMapping("/v1/questions")
public class QuestionController {
@Autowired
private IQuestionService questionService;
// localhost:8080/v1/questions/my
@GetMapping("/my")
public List<Question> my(
//@AuthenticationPrincipal注解效果
// 从Spring-Security框架获得当前登录用户的UserDetails对象
// 赋值给注解之后的参数
@AuthenticationPrincipal UserDetails user
){
List<Question> questions=questionService
.getMyQuestions(user.getUsername());
// 返回业务逻辑层查询出的所有问题列表
return questions;
}
}
重启服务,发送同步请求
localhost:8080/v1/questions/my
一定要求我们登录才能访问,注意,登录的必须是在数据库中有问题的用户
例如st2,xiaom,不要登录自己注册的用户,因为没有问题数据
Vue绑定和js代码
要想将我们查询出的问题列表信息显示在页面上,还需要编写对应的VUE绑定
首先在页面尾部添加引用
<script src="js/utils.js"></script>
<script src="js/tags_nav.js"></script>
<!-- ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ -->
<script src="js/index.js"></script>
</body>
</html>
然后开始编写html代码的Vue绑定
index_student.html的182行附近
<div id="questionsApp">
<div class="row" style="display: none">
<div class="alert alert-warning w-100" role="alert">
抱歉您还没有提问内容, <a href="question/create.html" class="alert-link">您可以点击此处提问</a>,或者点击标签查看其它问答
</div>
</div>
<div class="media bg-white m-2 p-3"
v-for="question in questions">
<!-- ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ -->
<div class="media-body w-50">
<div class="row">
<div class="col-md-12 col-lg-2">
<span class="badge badge-pill badge-warning" style="display: none">未回复</span>
<span class="badge badge-pill badge-info" style="display: none">已回复</span>
<span class="badge badge-pill badge-success">已解决</span>
</div>
<div class="col-md-12 col-lg-10">
<h5 class="mt-0 mb-1 text-truncate">
<a class="text-dark"
href="question/detail.html"
v-text="question.title">
<!-- ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ -->
eclipse 如何导入项目?
</a>
</h5>
</div>
</div>
<div class="font-weight-light text-truncate text-wrap text-justify mb-2" style="height: 70px;">
<p v-html="question.content">
<!-- ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ -->
eclipse 如何导入项目?
</p>
</div>
<div class="row">
<div class="col-12 mt-1 text-info">
<i class="fa fa-tags" aria-hidden="true"></i>
<a class="text-info badge badge-pill bg-light" href="tag/tag_question.html"><small >Java基础 </small></a>
</div>
</div>
<div class="row">
<div class="col-12 text-right">
<div class="list-inline mb-1 ">
<small class="list-inline-item"
v-text="question.userNickName">风继续吹</small>
<!-- ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ -->
<small class="list-inline-item">
<span v-text="question.pageViews">12</span>
<!-- ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ -->
浏览</small>
<small class="list-inline-item" >13分钟前</small>
</div>
</div>
</div>
</div>
<!-- / class="media-body"-->
<img src="img/tags/example0.jpg" class="ml-3 border img-fluid rounded" alt="" width="208" height="116">
</div>
因为js代码都是写好的
所以直接重启服务,就能显示所有问题列表内容了!
显示持续时间
所谓持续时间,就是问题发布时间距离现在的时间差
页面上固定值"13分钟前"
我们要将这个值修改为真实的时间
我们设计将持续时间的显示分为4个分段
- 时间差不足1分钟,显示"刚刚"
- 时间差不足1小时,显示"XX分钟前"
- 时间差在1天以内,显示"XX小时前"
- 时间差在1天以上,显示"XX天前"
实现的逻辑就是用当前时间减去问题发布的时间
在判断这个时间差的范围,代码已经在index.js文件中编写完毕
index_student.html页面的226行附近,添加
<small class="list-inline-item"
v-text="question.duration">13分钟前</small>
重启服务就能显示持续时间了
显示当前问题关联的标签
上面的图片就是一个问题关联了多个标签的显示效果
先明确数据库中标签和问题的关系
因为question和tag是多对多的关系,一旦需要根据问题id查询该问题对应的标签就需要一个3表的关联查询
这个3表关联查询,性能差,程序员不愿意写,带来各种问题
所以我们希望避免关联查询
实际开发中,我们可以将经常需要关联查询的内容直接保存在当前数据表中,在需要时直接查询当前表,避免关联查询
但是我们要知道,这样做会出现数据库表的数据冗余,占用更多数据库空间同时增加数据库维护难度,开发中需要斟酌利弊才能确定的
为了实现一个问题对象中包含多个标签的表示
我们修改Question实体类,添加一个标签集合的属性
/**
* 当前问题包含的所有标签的集合
*/
// 声明当前属性不对应数据库表中的任何列
@TableField(exist = false)
private List<Tag> tags;
下面我们就要完成将tagNames属性中例如
"Java基础,Java SE,面试题"的字符串
转换为一个List<Tag>类型的对象,其中包含
- Java基础标签对象
- Java SE标签对象
- 面试题标签对象
具体思路在文档末尾
按照上面思路,我们需要先到TagService业务逻辑层中,声明包含所有标签的Map类型缓存对象
先声明接口中的方法
ITagService添加方法如下
// 定义返回包含所有标签对象Map的方法
Map<String,Tag> getTagMap();
TagServiceImpl类实现代码如下
添加tagMap缓存对象
@Service
public class TagServiceImpl extends ServiceImpl<TagMapper, Tag> implements ITagService {
@Autowired
private TagMapper tagMapper;
// 声明缓存对象
private List<Tag> tags=new CopyOnWriteArrayList<>();
// tags属性用于充当保存所有标签的缓存对象
// 因为TagServiceImpl默认是单例作用域的,所以tags属性也只有一份
// 在今后任何方法的访问中,不会出现新的对象,重复占用内存
// CopyOnWriteArrayList是jdk1.8开始支持的线程安全的集合对象
private Map<String,Tag> tagMap=new ConcurrentHashMap<>();
// ConcurrentHashMap是一个线程安全的Map集合类型对象,从jdk1.8开始
@Override
public List<Tag> getTags() {
// 要获得所有标签
// 先判断缓存对象是不是没有元素
// 3
if(tags.isEmpty()){
synchronized (tags) {
// 2
tags.clear();
tagMap.clear();
List<Tag> list = tagMapper.selectList(null);
tags.addAll(list);
// 遍历list对tagMap赋值
for(Tag t:list){
tagMap.put(t.getName(),t);
}
System.out.println("tags加载完成");
}
// 1
}
// 千万别忘了返回tags
return tags;
}
@Override
public Map<String, Tag> getTagMap() {
// 判断tagMap是不是empty
if(tagMap.isEmpty()){
// 如果tagMap是empty,证明getTags方法也没有运行过
// 调用getTags方法为tagMap赋值即可
getTags();
}
// 千万别忘了返回
return tagMap;
}
}
有了上面提供的缓存的Map对象
我们就可以在Question的业务逻辑层中进行转了
在QuestionServiceImpl类中编写一个转换方法
代码如下
// 编写TagNames转换为List<Tag>的方法
// 需要获得ITagService业务逻辑层中的缓存Map
@Autowired
private ITagService tagService;
private List<Tag> tagNamesToTags(String tagNames){
//tagNames:"Java基础,Java SE,面试题"
String[] names=tagNames.split(",");
// names:{"Java基础","Java SE","面试题"}
Map<String,Tag> tagMap=tagService.getTagMap();
// 循环遍历之前声明一个用于接收返回值的List
List<Tag> tags=new ArrayList<>();
// 遍历当前的names数组
for(String name:names){
// 根据标签名称获得标签对象
Tag t=tagMap.get(name);
// 将获得的标签对象赋值到tags中
tags.add(t);
}
// 返回tags
return tags;
}
英文
Principal:当事人
tagNames转换为List<Tag>的思路
以106号问题为例
tagNames的值为:“Java基础,Java SE,面试题”
我们先利用String类的方法split将这个字符串拆分为String的数组
String[] names=tagNames.split(",");
names:{“Java基础”,“Java SE”,“面试题”}
下面最重要的环节就是怎么将"Java基础"这样的标签名称转换为对应的标签对象
我们知道通过一个名称寻找对应对象最快的数据结构是Map
所以为了进一步提高从内存中通过标签名称获得标签对象的效率
我们先在TagServiceImpl类中再定义一个包含所有标签对象的Map集合
以便我们在这个转换过程中通过标签名称获得标签对象
假设这个缓存的Map对象为tagMap
获得对应标签对象的代码为
Tag t=tagMap.get(names[i]);
最后将这个t对象保存在一个List<Tag>中即可完成转换
list.add(t);
假设缓存对象名称为tags
for(Tag t:tags){
if(t.getName.equals("Java基础")){
// 进这个if的t对象就是"Java基础"对应的标签对象
}
}