Blog
2021/10/6 19:00开始这个项目,此片博客用于记录开发的点滴
一、项目搭建(2021.10.6)
pom文件导入相关依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<!-- 排除默认使用的logback-->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
<version>2.5.5</version>
</dependency>
<!-- log4j2-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.5.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
<version>2.5.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.78</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--可以看到json中信息-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>2.5.5</version>
<optional>true</optional>
</dependency>
<!--跟java.lang这个包的作用类似,Commons Lang这一组API也是提供一些基础的、通用的操作和处理,如自动生成toString()的结果、自动实现hashCode()和equals()方法、数组操作、枚举、日期和时间的处理等等。-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!--StringUtils就是这个提供的,用来有时候验证什么是否为空呀-->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.2</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10.10</version>
</dependency>
</dependencies>
application配置文件配置
server.port=8888
spring.application.name=komorebi_blog
#数据库配置
spring.datasource.url=jdbc:mysql://localhost:3306/blog?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=747699
#mybatis
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#标识表名均为ms_前缀,后续操作可以不用定义这个前缀
mybatis-plus.global-config.db-config.table-prefix=ms_
Mybatis Plus配置
创建配置类,设置分页查询(一般项目都会用到,所以提前配置好),注意@MapperScan(“com.komorebi.mapper”)注解。
配置类一定要加@Configuration。
@Configuration
@MapperScan("com.komorebi.mapper")
public class MybatisPlusConfig {
// 分页插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}
跨域问题解决
创建WebMVCConfig配置类,解决不同端口之间的跨域问题
配置类一定要加@Configuration。
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMVCConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 跨域设置
registry.addMapping("/**")
.allowCredentials(true)
.allowedOrigins("http://localhost:8080")
.allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")
.allowedHeaders("*")
.maxAge(3600);
}
}
二、首页配置
首页分页显示文章信息
控制类
@RestController//JSON数据交互
@RequestMapping("articles")
public class ArticleController {
@Autowired
ArticleService articleService;
//分页显示文章列表
@PostMapping
public Result listArticle(@RequestBody PageParams pageParams){
return articleService.listArticle(pageParams);
}
}
vo类
Vo包中的类才是前端实际拿到的数据。
文章显示控制类返回的是一个Result对象,参数是PageParams对象,这两个类都放在vo包中,vo中的类都是前端显示数据的类,一般前端中只显示数据库表中部分数据。
Result类,定义了两个静态方法,分别表示请求成功、请求失败。
package com.komorebi.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class Result {
private boolean success;
private int code;
private String msg;
private Object data;
public static Result success(Object data){
return new Result(true,200,"success",data);
}
//请求失败
public static Result fail(int code,String msg){
return new Result(false,code,msg,null);
}
}
前端传给后端接口的json数据,我们都封装为param类,方便操作。
PageParams 类定义了分页查询的page和pageSize,分别对应分页查询中的start和size。
package com.komorebi.vo;
import lombok.Data;
@Data
public class PageParams {
private int page = 1;
private int pageSize = 1;
}
ArticleVo 类是前端显示文章信息的类,所以在查询文章列表时,就要做数据库中Article类向ArticleVo 类的转换。
package com.komorebi.vo;
import lombok.Data;
import java.util.List;
@Data
public class ArticleVo {
private Long id;
private String title;
private String summary; //简介
private int commentCounts;
private int ViewCounts;
private int weight; //置顶
private String createDate; //创建时间
private String author;
//暂时不需要
// private ArticleBodyVo body;
private List<TagVo> tags;
//暂时不需要
// private CategoryVo category;
}
TagVo 类是前端显示标签信息的类,所以在查询文章列表时,就要做数据库中Tag类向TagVo 类的转换。
package com.komorebi.vo;
import lombok.Data;
@Data
public class TagVo {
private Long id;
private String tagName;
}
mapper接口
此处定义了三个mapper接口,分别为ArticleMapper、SysUserMapper、TagMapper。
package com.komorebi.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.komorebi.pojo.Article;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
@Repository
public interface ArticleMapper extends BaseMapper<Article> {
}
--------------------------------------------------------------
@Repository
public interface TagMapper extends BaseMapper<Tag> {
List<Tag> findTagsByArticleId(Long articleId);
}
-------------------------------------------------------------
@Repository
public interface SysUserMapper extends BaseMapper<SysUser> {
}
因为tag和article有一张对应的表,所以要查询article对应的tag时,需要设计到多表的查询,但是,mybatisplus不支持多表查询,所以需要自己写mapper.xml文件。
注意:mapper.xml文件所在目录要和mapper对应,本次工程都在com.komorebi.mapper下。
可以在application.properties中mybatis-plus开启驼峰命名
mybatis-plus.configuration.map-underscore-to-camel-case=true
这样SQL语句就不需要as别名。
TagMapper.xml
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.komorebi.mapper.TagMapper">
<select id="findTagsByArticleId" parameterType="long" resultType="com.komorebi.pojo.Tag">
# 可以在application.properties中mybatis-plus开启驼峰命名
# mybatis-plus.configuration.map-underscore-to-camel-case=true
# 这样SQL语句就不需要as别名
select id,avatar,tag_name as tagName from ms_tag
where id in
(select tag_id from ms_article_tag where article_id=#{articleId})
</select>
</mapper>
service层部分
该阶段定义了三个service接口类。
package com.komorebi.service;
import com.komorebi.vo.PageParams;
import com.komorebi.vo.Result;
public interface ArticleService {
//分页查询文章列表
Result listArticle(PageParams pageParams);
}
--------------------------------------------------------
package com.komorebi.service;
import com.komorebi.pojo.SysUser;
public interface SysUserService {
SysUser findUserById(Long id);
}
--------------------------------------------------------
package com.komorebi.service;
import com.komorebi.vo.TagVo;
import java.util.List;
public interface TagService {
//通过文章id查询ui赢得标签,有一张表专门映射文章id和标签id
List<TagVo> findTagsByArticleId(Long articleId);
}
serviceImpl类
- ArticleServiceImpl
该实现类目前实现了文章分页查询。
知识点:
1)Page类定义分页对象;
2)LambdaQueryWrapper定义查询wrapper;
3)selectPage()函数返回的是Page对象,通过getRecords获得Article对象列表。
4)copy和copyList函数实现Article到ArticleVo的转换
5)BeanUtils.copyProperties(article,articleVo),可以实现对象之间的复制,相同属性名复制,不同属性名为null。
package com.komorebi.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.komorebi.mapper.ArticleMapper;
import com.komorebi.pojo.Article;
import com.komorebi.vo.ArticleVo;
import com.komorebi.vo.PageParams;
import com.komorebi.vo.Result;
import org.joda.time.DateTime;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class ArticleServiceImpl implements ArticleService{
@Autowired
ArticleMapper articleMapper;
@Autowired
TagService tagService;
@Autowired
SysUserService sysUserService;
//分页查询
@Override
public Result listArticle(PageParams pageParams) {
//分页
Page<Article> page = new Page<>(pageParams.getPage(),pageParams.getPageSize());
LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<>();
//先置顶排序由属性weight决定,后按照时间排序
wrapper.orderByDesc(Article::getWeight,Article::getCreateDate);
Page<Article> articlePage = articleMapper.selectPage(page, wrapper);
//文章列表
List<Article> records = articlePage.getRecords();
//因为页面展示出来的数据不一定和数据库一i杨没所以我们要做一个抓安环
//将查出的数据复制到articleVo中实现解耦,vo和页面数据交换
List<ArticleVo> articleVo = copyList(records,true,true);
return Result.success(articleVo);
}
//copyList实现
private List<ArticleVo> copyList(List<Article> records,boolean isTag,boolean isAuthor) {
ArrayList<ArticleVo> articleVos = new ArrayList<>();
for(Article article:records){
articleVos.add(copy(article,isTag,isAuthor));
}
return articleVos;
}
//这个方法是主要点是BeanUtils,又Spring提供的,专门用来拷贝的,想Article和articlevo相同属性的拷贝过来返回
private ArticleVo copy(Article article,boolean isTag,boolean isAuthor){
ArticleVo articleVo = new ArticleVo();
BeanUtils.copyProperties(article,articleVo);
//joda包中的DataTime.toString方法将Article的Long日期属性转为ArticleVo中的字符串日期属性
articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));
//是否显示标签和作者
if(isTag){
articleVo.setTags(tagService.findTagsByArticleId(article.getId()));
}
if(isAuthor){
articleVo.setAuthor(sysUserService.findUserById(article.getAuthorId()).getNickname());
}
return articleVo;
}
}
- TagMapperServiceImpl
此处也会涉及对象之间的复制,原理同ArticleServiceImpl,这里实现的是Tag类复制为TagVo类。
package com.komorebi.service;
import com.komorebi.mapper.TagMapper;
import com.komorebi.pojo.Article;
import com.komorebi.pojo.Tag;
import com.komorebi.vo.ArticleVo;
import com.komorebi.vo.TagVo;
import org.joda.time.DateTime;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class TagServiceImpl implements TagService{
@Autowired
TagMapper tagMapper;
@Override
public List<TagVo> findTagsByArticleId(Long articleId) {
List<Tag> tags = tagMapper.findTagsByArticleId(articleId);
return copyList(tags);
}
//copyList实现
private List<TagVo> copyList(List<Tag> tags) {
ArrayList<TagVo> tagVos = new ArrayList<>();
for(Tag tag : tags){
tagVos.add(copy(tag));
}
return tagVos;
}
private TagVo copy(Tag tag){
TagVo tagVo = new TagVo();
//BeanUtils,copyProperties用于类之间的复制,相同字段复制,不同字段为null
BeanUtils.copyProperties(tag,tagVo);
return tagVo;
}
}
- SysUserServiceImpl
package com.komorebi.service;
import com.komorebi.mapper.SysUserMapper;
import com.komorebi.pojo.SysUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class SysUserServiceImpl implements SysUserService{
@Autowired
SysUserMapper sysUserMapper;
@Override
public SysUser findUserById(Long id) {
return sysUserMapper.selectById(id);
}
}
首页展示
此时已经是 2021/10/6 22:00,今天的任务到此结束了。
首页最热标签(10/7 12:17)
思路:
1)首先在ms_article_tag表操作,通过tag_id分组并排序获得前几名,返回一个tag_id列表。
2)然后根据tag_id列表查询ms_tag表中对应的id和tagName将查询结果返回为TagVo对象(作者使用的是返回为Tag对象,但是由于前端展示的都是vo类,所以我们转换为TagVo)。
接下来的就是编码环节
TagController类
@RestController
@RequestMapping("/tags")
public class TagsController {
@Autowired
TagService tagService;
//tags/hot相应最热标签tag对象
@RequestMapping("/hot")
public Result hot(){
int limit = 6;//最热六个
return tagService.hots(limit);
}
}
TagService
Result hots(int limit);
TagMapper
import com.komorebi.vo.Result;
import com.komorebi.vo.TagVo;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface TagMapper extends BaseMapper<Tag> {
List<Tag> findTagsByArticleId(Long articleId);
//查询最热标签前limit条
List<Long> findHotsTagId(int limit);
//通过最热标签tagid查询最热tags
List<TagVo> findTagsByIds(List<Long> tagIds);
}
TagMapper对应的Mapper文件
重点:
1)findHotsTagId涉及分组并排序
2)findTagsByIds涉及到foreach标签
<!-- 查询最热标签id,提取前limit个-->
<select id="findHotsTagId" parameterType="int" resultType="long">
select tag_id from ms_article_tag
group by tag_id
order by count(*) limit #{limit}
</select>
<!-- 根据最热标签id查询对应tag对象-->
<select id="findTagsByIds" resultType="com.komorebi.vo.TagVo" parameterType="list">
select id, tag_name from ms_tag
where id in
<foreach collection="tagIds" item="tagId" separator="," open="(" close=")">
#{tagId}
</foreach>
</select>
TagServiceImpl
//查询若干个最热标签功能
public Result hots(int limit){
List<Long> hotsTagId = tagMapper.findHotsTagId(limit);
//判断hotsTagId是否为空
if(CollectionUtils.isEmpty(hotsTagId)){
return Result.success(Collections.emptyList());
}
List<TagVo> tagsList = tagMapper.findTagsByIds(hotsTagId);
return Result.success(tagsList);
}
统一异常处理
定义Handler包,设置统一异常处理类AllExceptionHandler ,
1)@ResponseBody
2)@ControllerAdvice
3)@ExceptionHandler(Exception.class)
package com.komorebi.handler;
import com.komorebi.vo.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
//对加了@Controller的方法进行拦截处理,AOP实现
@ControllerAdvice
public class AllExceptionHandler {
//进行异常处理,处理Exception.class异常
@ExceptionHandler(Exception.class)
//返回json数据
@ResponseBody
public Result doExceptionHandler(Exception e) {
return Result.fail(-999,"系统异常,抱歉!");
}
}
最热文章
原理同最热标签查询。根据view_count排序,选择出最热文章。
Controller
//显示最热文章
@PostMapping("/hot")
public Result hotArticle(){
int limit = 3;
return articleService.hotArticle(limit);
}
Service
Result hotArticle(int limit);
ServiceImpl
//根据view_counts字段查询最热文章
@Override
public Result hotArticle(int limit) {
LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<>();
wrapper.orderByDesc(Article::getViewCounts)
.select(Article::getId,Article::getTitle)
.last("limit "+limit);
List<Article> articles = articleMapper.selectList(wrapper);
return Result.success(copyList(articles,false,false));
}
最新文章显示
原理和最热文章完全相同,只是通过create_date字段排序,选择出最新
Controller
//显示最新文章
@PostMapping("/new")
public Result newArticle(){
int limit = 3;
return articleService.newArticle(limit);
}
Service
Result newArticle(int limit);
ServiceImpl
//最新文章
@Override
public Result newArticle(int limit) {
LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<>();
wrapper.orderByDesc(Article::getCreateDate)
.select(Article::getId,Article::getTitle)
.last("limit "+limit);
List<Article> articles = articleMapper.selectList(wrapper);
return Result.success(copyList(articles,false,false));
}
文章归档显示
由于这个归档查询涉及到数据库内部函数Year、Month,所以MybatisPlus不能实现,需要通过mapper.xml文件实现。
注意:新建dos包,do对象也是数据库中查询出的对象,但它并不需要一些持久化的对象,我们把这些对象放在do包中,由于do是一个关键词,所以在命名是加了s,即dos。
文章归档返回的对象为Archives(档案)类。
思路可通过sql语句了解。
Archives类
package com.komorebi.dos;
import lombok.Data;
@Data
public class Archives {
private Integer year;
private Integer month;
private Long count;
}
Controller
//文章归档
@PostMapping("/listArchives")
public Result listArchives(){
return articleService.listArchives();
}
Mapper
List<Archives> listArchives();
Mapper.xml
数据库中create_date 为bigint 13位,直接year()不行,需要先转date型后year()。
完整sql语句
select year(FROM_UNIXTIME(create_date/1000)) as year,
month(FROM_UNIXTIME(create_date/1000)) as month,
count(*) count
from ms_article
group by year,month;
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.komorebi.mapper.ArticleMapper">
<select id="listArchives" resultType="com.komorebi.dos.Archives">
select YEAR(FROM_UNIXTIME(create_date/1000)) as year,
MONTH(FROM_UNIXTIME(create_date/1000)) as month,
count(*) as count
from ms_article
group by year,month;
</select>
</mapper>
Service
Result listArchives();
ServiceImpl
//文章归档
@Override
public Result listArchives() {
List<Archives> archivesList = articleMapper.listArchives();
return Result.success(archivesList);
}
展示
三、登录功能实现
登录接口返回给浏览器一个token
因为每次登录都有错误验证,所以自己定义了一个ErrorCode类
ErrorCode.class
package com.komorebi.vo;
public enum ErrorCode {
PARAMS_ERROR(10001,"参数有误"),
ACCOUNT_PWD_NOT_EXIST(10002,"用户密码不存在喔!"),
TOKEN_ERROR(10003,"Token不合法"),
ACCOUNT_EXIST(10004,"账号已存在"),
NO_PERMISSION(70001,"无访问权限"),
SESSION_TIME_OUT(90001,"会话超时"),
NO_LOGIN(90002,"未登录"),;
private int code;
private String msg;
ErrorCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
Controller
import com.komorebi.vo.PageParams;
import com.komorebi.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/login")
public class LoginController {
@Autowired
private LoginService loginService;
//登录
@PostMapping
public Result login(@RequestBody LoginParam loginParam){
return loginService.login(loginParam);
}
}
LoginService
Result login(LoginParam loginParam);
LoginServiceImpl
导入依赖Commons-codec实现MD5加密
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.13</version>
</dependency>
登录功能的核心步骤
1、检查参数是否合法
2、根据用户名和密码检查ms_sys_user表中对应的account、password字段
3、如果不存在,登录失败
4、因为数据库中password字段存放的是经过MD5加密过的密码,所以在查数据库表前,要先对password进行加密
5、如果存在,使用jwt,生成token,返回给前端
6、token放在redis中,redis映射token和user信息设置过期时间,先认证token是否合法,再认证redis是否存在
package com.komorebi.service;
import com.alibaba.fastjson.JSON;
import com.komorebi.Utils.JWTUtils;
import com.komorebi.pojo.SysUser;
import com.komorebi.vo.ErrorCode;
import com.komorebi.vo.LoginParam;
import com.komorebi.vo.Result;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.apache.commons.codec.digest.DigestUtils;
import java.util.concurrent.TimeUnit;
@Service
public class LoginServiceImpl implements LoginService{
@Autowired
private SysUserService sysUserService;
@Autowired
private RedisTemplate<String,String> redisTemplate;
//md5加密使用到的盐
private static final String salt="mszlu!@#";
@Override
public Result login(LoginParam loginParam) {
/*
* 1、检查参数是否合法
* 2、根据用户名和密码检查ms_sys_user表中对应的account、password字段
* 3、如果不存在,登录失败
* 4、如果存在,使用jwt,生成token,返回给前端
* 5、token放在redis中,redis映射token和user信息
* 设置过期时间,先认证token是否合法,再认证redis是否存在
* */
String account = loginParam.getAccount();
String password = loginParam.getPassword();
//用户名或者密码为空
if(StringUtils.isBlank(account)||StringUtils.isBlank(password)){
//提前写好的错误编码类,方便使用
return Result.fail(ErrorCode.PARAMS_ERROR.getCode(),ErrorCode.PARAMS_ERROR.getMsg());
}
//这里使用的DigestUtils是来自于commons-codec包的,需要外部导入依赖
//密码加盐,因为数据库中的密码是经过盐加密的
System.out.println(password);
SysUser user = sysUserService.findUser(account, password);
//用户名密码错误
if(user == null){
return Result.fail(ErrorCode.ACCOUNT_PWD_NOT_EXIST.getCode(),ErrorCode.ACCOUNT_PWD_NOT_EXIST.getMsg());
}
//用户名密码正确
String token = JWTUtils.createToke(user.getId());
//存入redis,要确保已开启redis
redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(user),1, TimeUnit.DAYS);
return Result.success(token);
}
}
代码分析:
1)先定义一个盐加密字符串,在对password进行加密
private static final String salt="mszlu!@#";
password = DigestUtils.md5Hex(password+salt);
2)验证时需要通过sysUserService查询到对应用户并返回部分用户信息
SysUser user = sysUserService.findUser(account, password);
sysUserService.findUser方法实现
@Override
public SysUser findUser(String account, String password) {
LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysUser::getAccount,account)
.eq(SysUser::getPassword,password)
.select(SysUser::getAccount,SysUser::getId,SysUser::getAvatar,SysUser::getNickname)
.last("limit 1");
return sysUserMapper.selectOne(wrapper);
}
3)查到用户后,给用户返回一个JWT (token)
//用户名密码正确
String token = JWTUtils.createToke(user.getId());
4)用户token存入redis,要确保已开启redis
@Autowired
private RedisTemplate<String,String> redisTemplate;
redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(user),1, TimeUnit.DAYS);
5)由于要用到redis,在配置文件进行配置
#redis配置
spring.redis.host=localhost
spring.redis.port=6379
因为登录方法参数为LoginParam类,该类将前端传给后端的参数封装。
LoginParam
package com.komorebi.vo;
import lombok.Data;
@Data
public class LoginParam {
private String account;
private String password;
}
登陆测试
这里的测试使用的是postman工具进行接口测试。
Redis中可以看到相应的Token
登陆后获取用户信息
登陆后用户的token会存放在浏览器本地,当用户登陆时会在请求头携带token,token中含有用户的id,用户的信息实际存放在redis中。
controller
token放在请求头中,所以要获取token
通过@RequestHeader(“Authorization”)注解获取
package com.komorebi.controller;
import com.komorebi.service.SysUserService;
import com.komorebi.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
SysUserService sysUserService;
//用户信息展示请求
//token放在请求头中,所以要获取token需要@RequestHeader("Authorization")注解
@GetMapping("/currentUser")
public Result currentUser(@RequestHeader("Authorization") String token){
return sysUserService.findUserByToken(token);
}
}
SysUserService
Result findUserByToken(String token);
SysUserServiceImpl
该类用于返回用户信息,由于页面展示用户部分信息,所以创建Vo用户类LoginUserVo
package com.komorebi.vo;
import lombok.Data;
@Data
public class LoginUserVo {
private Long id;
private String account;
private String nickname;
private String avatar; //头像
}
findUserByToken方法
@Override
public Result findUserByToken(String token) {
//获取用户展示信息
SysUser sysUser = loginService.checkToken(token);
if(sysUser==null)
return Result.fail(ErrorCode.TOKEN_ERROR.getCode(),ErrorCode.TOKEN_ERROR.getMsg());
//sysUser转为前端显示对象LoginUserVo
LoginUserVo loginUserVo = new LoginUserVo();
loginUserVo.setAccount(sysUser.getAccount());
loginUserVo.setAvatar(sysUser.getAvatar());
loginUserVo.setId(sysUser.getId());
loginUserVo.setNickname(sysUser.getNickname());
return Result.success(loginUserVo);
}
findUserByToken会调用LoginServiceImp中的checkToken方法,检查token的合法性并返回SysUser对象。
LoginServiceImpl
@Override
public SysUser checkToken(String token) {
//token为空
if(StringUtils.isBlank(token))
return null;
//解析token
Map<String, Object> checkToken = JWTUtils.checkToken(token);
//解析为空
if(checkToken==null)
return null;
//redis不存在token,user信息存放在redis中
String userJson = redisTemplate.opsForValue().get("TOKEN_" + token);
if(StringUtils.isBlank(userJson))
return null;
//token解析成功,并且redis存在
//JSON.parseObject将json对象转为SysUser对象
SysUser sysUser = JSON.parseObject(userJson,SysUser.class);
return sysUser;
}
展示:
请求成功
退出登录
退出登录就是删除redis中的toke
Controller
@RestController
@RequestMapping("/logout")
public class LoginOutController {
@Autowired
LoginService loginService;
@GetMapping
public Result logout(@RequestHeader("Authorization") String token){
return loginService.logout(token);
}
}
LoginServiceImpl
//退出登录
@Override
public Result logout(String token) {
redisTemplate.delete("TOKEN_"+token);
return Result.success(null);
}
结果
注册
注册和登录的功能有点类似
controller
@RestController
@RequestMapping("/register")
public class RegisterController {
@Autowired
LoginService loginService;
//注册功能,返回数据为token
@PostMapping
public Result register(@RequestBody LoginParam loginParam){
return loginService.register(loginParam);
}
}
本项目的注册功能在LoginService实现
LoginService
Result register(LoginParam loginParam);
LoginServiceImpl
思路:
1)判断参数是否合法;
2)判断账户是否已经存在;
3)若合法并且不存在,则创建新用户;
4)生成token;
5)token即用户信息存入redis;
6)注意:在SysServiceImpl中设置事务,一旦出现问题,就回滚;
7)返回给前端token.
@Override
public Result register(LoginParam loginParam) {
String account = loginParam.getAccount();
String password = loginParam.getPassword();
String nickname = loginParam.getNickname();
//用户参数为空
if(StringUtils.isBlank(account)||StringUtils.isBlank(password)||StringUtils.isBlank(nickname)){
return Result.fail(ErrorCode.PARAMS_ERROR.getCode(), ErrorCode.PARAMS_ERROR.getMsg());
}
SysUser sysUser = sysUserService.findUserByAccount(account);
//用户已经存在
if(sysUser!=null){
return Result.fail(ErrorCode.ACCOUNT_EXIST.getCode(), "账号已经被注册");
}
//创建用户,ID默认为自增
sysUser =new SysUser();
sysUser.setAccount(account); //账户名
sysUser.setNickname(nickname); //昵称
sysUser.setPassword(DigestUtils.md5Hex(password+salt)); //密码加盐md5
sysUser.setCreateDate(System.currentTimeMillis()); //创建时间
sysUser.setLastLogin(System.currentTimeMillis()); //最后登录时间
sysUser.setAvatar("/static/img/logo.b3a48c0.png"); //头像
sysUser.setAdmin(1); //管理员权限
sysUser.setDeleted(0); //假删除
sysUser.setSalt(""); //盐
sysUser.setStatus(""); //状态
sysUser.setEmail(""); //邮箱
this.sysUserService.save(sysUser);
//生成token
String token = JWTUtils.createToke(sysUser.getId());
//token存入redis
redisTemplate.opsForValue().set("TOKEN_"+token,JSON.toJSONString(sysUser),1,TimeUnit.DAYS);
return Result.success(token);
}
上面代码中涉及到两个函数
sysUserService.findUserByAccount(account)查询用户
sysUserService.save(sysUser)创建用户
findUserByAccount方法
@Override
public SysUser findUserByAccount(String account) {
LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysUser::getAccount,account)
.last("limit 1");
return this.sysUserMapper.selectOne(wrapper);
}
save方法
@Override
public void save(SysUser sysUser) {
//保存用户id会自动生成
//默认生成分布式id,采用雪花算法
//mybatis-plus
sysUserMapper.insert(sysUser);
}
注册
注册成功
登录拦截器
定义拦截器LoginInterceptor
package com.komorebi.handler;
import com.alibaba.fastjson.JSON;
import com.komorebi.Utils.JWTUtils;
import com.komorebi.pojo.SysUser;
import com.komorebi.service.LoginService;
import com.komorebi.vo.ErrorCode;
import com.komorebi.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
public LoginService loginService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
/*1、需要判断请求的接口上是否是HandleMethod即controller方法
* 2、判断token是否为空,为空未登录
* 3、不为空,登陆验证(通过LoginServiceImpl中的checkToken方法)
* 4、如果认证成功,则放行
* */
if(!(handler instanceof HandlerMethod)){
//拦截器是拦截的controller中的方法,controller的方法其实就是一个Handler
//handler可能是RequestResourceHandle(访问资源handle),即可能是访问静态资源的方法
//解释:controller对应HandlerMethod,所以拦截器只拦截HandlerMethod
return true;
}
//获取token
String token = request.getHeader("Authorization");
//日志问题,需要导入lombok下的@slf4
log.info("=============request start=================");
String requestURI = request.getRequestURI();
log.info("request uri:{}",requestURI);
log.info("request method:{}",request.getMethod());
log.info("token:{}",token);
log.info("=============request end===================");
//token为空,不放行
if(StringUtils.isBlank(token)){
Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());
//设置返回消息格式
response.setContentType("application/json;charset=utf8");
//返回json信息
response.getWriter().print(JSON.toJSONString(result));
return false;
}
//token不为空,去做认证
SysUser sysUser = loginService.checkToken(token);
//用户不存在,即认证失败
if(sysUser == null){
System.out.println("没有该用户");
Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());
response.setContentType("application/json;charset=utf8");
response.getWriter().print(JSON.toJSONString(result));
return false;
}
//登陆验证成功,放行
return true;
}
}
配置将拦截器
这里拦截器默认拦截的/test,用于测试拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
//配置拦截接口,此处配置为test,用于测试
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/test");
}
testController
@RestController
@RequestMapping("/test")
public class TestController {
@RequestMapping
public Result test(){
return Result.success(null);
}
}
postman测试拦截器
1)没有token情况下
由于没有token,postman中返回未登录
日志中也显示token为null
2)有token
前提设置:postman中选择Authorization的类型为No Auth。
测试
访问成功,有token,拦截器放行
日志输出
此时token不为空
2021/10/7 22:07 今天的学习任务结束!
ThreadLocal引用(10/8 10:30)
ThreadLocal简介:
多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一乐ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。
本次项目引入ThreadLocal的目的是为实现在Controller层获得用户信息。
原理:
1)在登录拦截放行前将用户信息存入ThreadLocal中;
2)在Controller中获取用户信息;
3)在Controller方法执行完后删除用户信息;
其中
1)对应拦截器中preHandle方法和UserThreadLocal的set方法
2)对应UserThreadLocal的get方法
3)对应拦截器afterCompletion方法和UserThreadLocal的remove方法
所以要创建UserThreadLocal类
package com.komorebi.UserThreadLocal;
import com.komorebi.pojo.SysUser;
public class UserThreadLocal {
// 声明为私有,即每个线程有自己的ThreadLocal变量
private UserThreadLocal(){}
// 实例化一个ThreadLocal的类,即启用
private static final ThreadLocal<SysUser> LOCAL = new ThreadLocal<>();
//存入用户信息
public static void put(SysUser sysUser){
LOCAL.set(sysUser);
}
//获取用户信息
public static SysUser get(){
return LOCAL.get();
}
//删除用户信息
public static void remove(){
LOCAL.remove();
}
}
UserThreadLocal中set方法在登录拦截器中应用,即登陆时set存入用户信息。
//为了实现在Controller中获取user用户信息,我们使用ThreadLocal
//将用户信息存入ThreadLocal中
UserThreadLocal.put(sysUser);
//登陆验证成功,放行
return true;
UserThreadLocal中get方法在Controller中应用,即登陆后获取用户信息。
public Result test(){
//测试ThreadLocal
SysUser sysUser = UserThreadLocal.get();
System.out.println(sysUser);
return Result.success(null);
}
UserThreadLocal中remove方法在登录拦截器afterCompletion方法中应用,即Controller执行完后删除用户信息。
//controller方法处理完之后的操作,要将ThreadLocal释放掉,否则会内存泄漏
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserThreadLocal.remove();
}
ThreadLocal(本地的线程)到底有什么用
比如我们发出一个请求,当你启动某一个进程的时候,你让他和你对应的进程进行绑定的话,会深入的绑定到一起(以达到绑定用户信息的目的)。
为什么在那个后面一定要删除,因为一旦内存泄漏是很严重的
一个线程可以存在多个ThreadLocal每一个Thread维护一个ThreadLocalMap,
key为使用弱引用的ThreadLocal实例,value为线程变量的副本。
强引用,是最普遍的引用,一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。
弱引用,JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。
上面的那个key为使用弱引用的ThreadLocal实例,当我们的线程中的那个ThreadLocal被垃圾回收机制干掉之后,是不是这个弱引用的Key不存在了,但是这个是Map集合呀,Value会永远的存在,所有要手动的删除
四、文章详情
内容及相关信息展示
该模块实现的主要功能是,点击文章标题进入到文章内容显示页面
请求链接为http://localhost:8080/articles/view/{id}
此时文章内容增加了分类(category)和内容(body)两个部分,所以ArticleVo代码要增加两个字段
ArticleVo
package com.komorebi.vo;
import lombok.Data;
import java.util.List;
@Data
public class ArticleVo {
private Long id;
private String title;
private String summary; //简介
private int commentCounts;
private int ViewCounts;
private int weight; //置顶
private String createDate; //创建时间
private String author;
private List<TagVo> tags;
//内容属性
private ArticleBodyVo body;
//分类属性
private CategoryVo category;
}
Articlecontroller
//文章详情
@PostMapping("/view/{id}")
public Result findArticleById(@PathVariable("id") Long id){
return articleService.findArticleById(id);
}
findArticleById方法
@Override
public Result findArticleById(Long id) {
/*
* 1、根据id获得article对象
* 2、根据bodyId和categoryId去做关联查询
* */
Article article = this.articleMapper.selectById(id);
return Result.success(copy(article,true,true,true,true));
}
copy函数改写
之前的copy函数只有三个参数,此时还要加两个参数内容和分类
private ArticleVo copy(Article article,boolean isTag,boolean isAuthor,boolean isBody,boolean isCategory){
ArticleVo articleVo = new ArticleVo();
BeanUtils.copyProperties(article,articleVo);
//joda包中的DataTime.toString方法将Article的Long日期属性转为ArticleVo中的字符串日期属性
articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));
//是否显示标签和作者
if(isTag){
articleVo.setTags(tagService.findTagsByArticleId(article.getId()));
}
if(isAuthor){
articleVo.setAuthor(sysUserService.findUserById(article.getAuthorId()).getNickname());
}
if(isBody){
articleVo.setBody(findArticleBodyById(article.getBodyId()));
}
if(isCategory){
articleVo.setCategory(categoryService.findCategoryById(article.getCategoryId()));
}
return articleVo;
}
由于copy被修改,所以copyList也需要修改,未来不改变copyList原有代码,使用重载
//copyList实现,用于将Article列表转换为ArticleVo列表
private List<ArticleVo> copyList(List<Article> records,boolean isTag,boolean isAuthor) {
ArrayList<ArticleVo> articleVos = new ArrayList<>();
for(Article article:records){
articleVos.add(copy(article,isTag,isAuthor,false,false));
}
return articleVos;
}
//重载
private List<ArticleVo> copyList(List<Article> records,boolean isTag,boolean isAuthor,boolean isBody,boolean isCategory) {
ArrayList<ArticleVo> articleVos = new ArrayList<>();
for(Article article:records){
articleVos.add(copy(article,isTag,isAuthor,isBody,isCategory));
}
return articleVos;
}
copy方法中为articleVo设置了body和categories两个属性。
所以要实现两个方法,获取文章内容: findArticleBodyById
获取文章分类:findCategoryById
findArticleBodyById方法(该方法在ArticleServiceImpl中)
//获得文章body内容
private ArticleBodyVo findArticleBodyById(Long bodyId) {
ArticleBody articleBody = articleBodyMapper.selectById(bodyId);
ArticleBodyVo articleBodyVo = new ArticleBodyVo();
articleBodyVo.setContent(articleBody.getContent());
return articleBodyVo;
}
该方法的返回类型是ArticleBodyVo,所以要创建对应的ArticleBody和ArticleBodyVo类以及ArticleBodyMapper。
ArticleBodyMapper
@Repository
public interface ArticleBodyMapper extends BaseMapper<ArticleBody> {
}
ArticleBody
package com.komorebi.pojo;
import lombok.Data;
//文章详情内容存放在ms_article_body表中,需要创建一个对应的实体类
@Data
public class ArticleBody {
private Long id;
private String content;
private String contentHtml;
private Long articleId;
}
ArticleBodyVo
package com.komorebi.vo;
import lombok.Data;
@Data
public class ArticleBodyVo {
private String content;
}
findCategoryById是由CategoryMapper实现的,所以要创建CategoryMapper、Category、CategoryVo、CategoryService、CategoryServiceImpl。
CategoryMapper
@Repository
public interface CategoryMapper extends BaseMapper<Category> {
}
Category
package com.komorebi.pojo;
import lombok.Data;
@Data
public class Category {
private Long id;
private String avatar;
private String categoryName;
private String description;
}
CategoryVo
package com.komorebi.vo;
import lombok.Data;
@Data
public class CategoryVo {
private Long id;
private String avatar;
private String categoryName;
}
CategoryService
public interface CategoryService {
CategoryVo findCategoryById(Long categoryId);
}
CategoryServiceImpl
@Service
public class CategoryServiceImpl implements CategoryService{
@Autowired
CategoryMapper categoryMapper;
@Override
public CategoryVo findCategoryById(Long categoryId) {
Category category = categoryMapper.selectById(categoryId);
CategoryVo categoryVo = new CategoryVo();
BeanUtils.copyProperties(category,categoryVo);
return categoryVo;
}
}
效果展示
文章内容(body)
文章底部分类(category)
文章详情展示这个模块业务逻辑比较简单,只是涉及了太多实体类和VO类,在这里画一个图用于理解它们之间的关系
问题总结
由于数据库中id都是使用的Long,并且所有的Long在数据库中数值位数都设置为20,当数据以json形式传给前端时,前端JavaScript解析20位数的Long会出现溢出,因为JavaScript无法解析过长的数值。
原理:
后端使用64位存储长整数(long),最大支持9223372036854775807
前端的JavaScript使用53位来存放,最大支持9007199254740992,超过最大值的数,可能会出现问题(得到的溢出后的值)
解决方法:降低数据库中Long表示的Id字段的位数,由20降为15,并且将id的数值也修改为位数小于或等于15位的数值。
长度修改为15
由于Article的表中id值改变了,所以要将所有涉及到该表id的表修改,否则后续会出现错误。
线程池实现更新评论数
查完文章了,新增阅读数,有没有问题呢?
答案是是有的,本应该直接返回数据,这时候做了一个更新操作,更新时间时加写锁,阻塞其他的读操作,新能就会比较低,
而且更新增加了此次接口的耗时,一旦更新出问题,不能影响我们其他的如:看文章呀什么的
那要怎么样去优化呢?,---->所有想到了线程池
可以把更新操作扔到线程池里面,就不会影响了,和主线程就不相关了
定义ThreadService
package com.komorebi.service;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.komorebi.mapper.ArticleMapper;
import com.komorebi.pojo.Article;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
//多线程实现文章阅读数++
@Component
public class ThreadService {
@Async("taskExecutor")
public void updateArticleViewCount(ArticleMapper articleMapper,Article article){
int viewCounts = article.getViewCounts();
Article articleUpdate = new Article();
articleUpdate.setViewCounts(viewCounts+1);
LambdaUpdateWrapper<Article> updateWrapper = new LambdaUpdateWrapper<>();
//更新的条件时id和viewCount要对应,即由id和viewCount确定唯一的文章
updateWrapper.eq(Article::getId,article.getId())
.eq(Article::getViewCounts,article.getViewCounts());
articleMapper.update(articleUpdate,updateWrapper);
//异步操作实现
try {
Thread.sleep(2000);
System.out.println("更新完成!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
设置线程池配置文件
ThreadPoolConfig
package com.komorebi.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
@EnableAsync//开启多线程
public class ThreadPoolConfig {
@Bean("taskExecutor")
public Executor asyncServiceExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 设置线程核心数
executor.setCorePoolSize(5);
// 设置最大线程数
executor.setMaxPoolSize(20);
// 配置队列大小
executor.setQueueCapacity(Integer.MAX_VALUE);
// 设置线程活跃时间
executor.setKeepAliveSeconds(60);
// 设置线程名称
executor.setThreadNamePrefix("Lum博客");
//等待所有任务及结束后关闭线程
executor.setWaitForTasksToCompleteOnShutdown(true);
//执行初始化
executor.initialize();
return executor;
}
}
在查看文章详情方法内加入更新viewCount操作
@Override
public Result findArticleById(Long id) {
/*
* 1、根据id获得article对象
* 2、根据bodyId和categoryId去做关联查询
* */
Article article = this.articleMapper.selectById(id);
//更新viewCount操作
threadService.updateArticleViewCount(articleMapper,article);
return Result.success(copy(article,true,true,true,true));
}
update更新实体类部分字段bug
sql在执行时update时,如果字段有默认值,那么更新就会涉及到该字段(即使我们没有设置更新该字段),比如:因为int的默认值为0,所以在更新viewCount时,其他的int类型的字段也会被更新,即使我们没有设置更新其他字段值,其他字段更新值默认为数据类型的默认值,int的默认值是0,解决这类bug最好的方法就是将int改为Integer。
评论展示
创建评论实体类comment、commentVo、评论中需要评论用户类UserVo
comment
package com.komorebi.pojo;
import lombok.Data;
@Data
public class Comment {
private Long id;
private String content;
private Long createDate;
private Long articleId;
private Long authorId;
private Long parentId;
private Long toUid;
private Integer level;
}
commentVo
package com.komorebi.vo;
import com.komorebi.pojo.Comment;
import lombok.Data;
import java.util.List;
@Data
public class CommentVo {
private Long id;
//评论作者
private UserVo author;
private String content;
private List<CommentVo> childrens;
private String createDate;
private Integer level;
//当为二级评论时,表示被评论人
private UserVo toUser;
}
UserVo
package com.komorebi.vo;
import lombok.Data;
@Data
public class UserVo {
private Long id;
private String nickName;
private String avatar;
}
评论显示模块实现
CommentController
@RestController
@RequestMapping("comments")
public class CommentController {
@Autowired
CommentService commentService;
@GetMapping("article/{id}")
public Result comments(@PathVariable("id") Long articleId){
return commentService.commonsByArticleId(articleId);
}
}
CommentMapper
@Repository
public interface CommentMapper extends BaseMapper<Comment> {
}
CommentService
public interface CommentService {
Result commonsByArticleId(Long articleId);
}
CommentServiceImpl
package com.komorebi.service.imp;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.komorebi.mapper.CommentMapper;
import com.komorebi.pojo.Comment;
import com.komorebi.pojo.SysUser;
import com.komorebi.service.CommentService;
import com.komorebi.service.SysUserService;
import com.komorebi.vo.CommentVo;
import com.komorebi.vo.Result;
import com.komorebi.vo.UserVo;
import org.joda.time.DateTime;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class CommentServiceImpl implements CommentService {
@Autowired
CommentMapper commentMapper;
@Autowired
SysUserService sysUserService;
@Override
public Result commonsByArticleId(Long articleId) {
/*
* 1、根据文章id查询评论列表,在comment表中查询
* 2、根据作者id查询作者信息
* 3、如果level=1,查询是否有子评论
* 4、如果有,根据评论id查询子评论
* */
LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Comment::getArticleId,articleId)
.eq(Comment::getLevel,1);
List<Comment> commentList = commentMapper.selectList(queryWrapper);
List<CommentVo> commentVos = copyList(commentList);
return Result.success(commentVos);
}
private List<CommentVo> copyList(List<Comment> commentList) {
List<CommentVo> commentVos = new ArrayList<>();
for(Comment comment : commentList){
commentVos.add(copy(comment));
}
return commentVos;
}
private CommentVo copy(Comment comment) {
CommentVo commentVo = new CommentVo();
//将相同属性进行copy
BeanUtils.copyProperties(comment,commentVo);
//时间格式化
commentVo.setCreateDate(new DateTime(comment.getCreateDate()).toString("yyyy-MM-dd HH:mm"));
//作者信息
commentVo.setAuthor(sysUserService.findUserVoById(comment.getAuthorId()));
//子评论
if(comment.getLevel()==1){
Long id = comment.getId();
List<CommentVo> commentVoList = findCommentByParentId(id);
commentVo.setChildrens(commentVoList);
}
//toUser:向谁评论
if(comment.getId()>1){
Long toUid = comment.getToUid();
UserVo userVo = sysUserService.findUserVoById(toUid);
commentVo.setToUser(userVo);
}
return commentVo;
}
private List<CommentVo> findCommentByParentId(Long id) {
LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Comment::getParentId,id)
.eq(Comment::getLevel,2);
List<Comment> comments = commentMapper.selectList(queryWrapper);
return copyList(comments);
}
}
结果展示
bug解决(10/9 11:30)
在评论显示功能实现后,打开博客首页,发现最新文章和最热文章无法显示,经过debug发现,在article到articleVo转换的过程中出现异常,所以无法显示,debug显示BeanUtils.copyProperties()函数在复制commentCounts属性时出错,说明sql语句没有错误,于是去查看了article、artileVo相应的属性,发现存在属性名相同,但是类型不同的属性,具体表现为commentCounts在article类中为Integer类型,在articleVo中却为int类型,于是将所有的int改为了Integer,bug修复,
具体原因应该是我们查询最热文章时,只是查询的id和标签,其它属性值没有查询,所以都为默认值。
在article中commentCounts默认值为null(Integer),但是articleVo中commentCounts默认值为0(int),如果进行复制,则int类型被赋予了null,一定会报错,因为int类型没有null这个属性值。
发表评论功能
CommentServiceImpl类
通过comment方法发布评论,
原理:获取前端的评论及用户数据,存入相应表中
前提:由于此处实在线程池中获取的用户信息,所以要将该请求接口加入到拦截器中,如果不加入到拦截器中,不仅不可以发布评论,而且没有办法获得用户信息,因为用户信息实在拦截器中加入到线程池的。
comment方法
//发表评论
//发表评论,是要将用户进行的评论存入到comment表中,所以要创建comment对象,而不是commentVo对象
@Override
public Result comment(CommentParam commentParam) {
//用户信息存在了本地线程,直接可以获取,前提将该请求接口加入到拦击器中
SysUser sysUser = UserThreadLocal.get();
Comment comment = new Comment();
comment.setArticleId(commentParam.getArticleId());
comment.setAuthorId(sysUser.getId());
comment.setContent(commentParam.getContent());
comment.setCreateDate(System.currentTimeMillis());
System.out.println(commentParam);
Long parentId = commentParam.getParentId();
if(parentId==null||parentId==0){
comment.setLevel(1);
}else{
comment.setLevel(2);
}
comment.setParentId(parentId == null ? 0: parentId);
Long toUserId = commentParam.getToUserId();
comment.setToUid(toUserId == null ? 0 : toUserId);
int insert = commentMapper.insert(comment);
System.out.println(insert);
return Result.success(null);
}
结果展示
刷新页面,评论成功
发布文章
发布文章要实现三个功能,文章分类获取、文章标签获取、发布功能实现。
如下图所示:实现三个功能后的发布页面
获取所有文章分类
文章分类在CategoryController中实现
CategoryController
@RestController
@RequestMapping("/categorys")
public class CategoryController {
@Autowired
CategoryService categoryService;
//查询所有文章分类
@GetMapping
public Result findAll(){
return categoryService.findAll();
}
}
对应的核心函数findAll
//获取所有文章分类
@Override
public Result findAll() {
List<Category> categories = categoryMapper.selectList(null);
return Result.success(copyList(categories));
}
private List<CategoryVo> copyList(List<Category> categories) {
ArrayList<CategoryVo> categoryVos = new ArrayList<>();
for(Category category:categories){
categoryVos.add(copy(category));
}
return categoryVos;
}
private CategoryVo copy(Category category) {
CategoryVo categoryVo = new CategoryVo();
BeanUtils.copyProperties(category,categoryVo);
return categoryVo;
}
获取所有标签
原理同上
TagsController
//获取文章所有标签
@GetMapping
public Result findAll(){
return tagService.findAll();
}
对应的核心函数findAll
public Result findAll() {
List<Tag> tags = tagMapper.selectList(null);
return Result.success(copyList(tags));
}
//copyList实现
private List<TagVo> copyList(List<Tag> tags) {
ArrayList<TagVo> tagVos = new ArrayList<>();
for(Tag tag : tags){
tagVos.add(copy(tag));
}
return tagVos;
}
private TagVo copy(Tag tag){
TagVo tagVo = new TagVo();
//BeanUtils,copyProperties用于类之间的复制,相同字段复制,不同字段为null
BeanUtils.copyProperties(tag,tagVo);
return tagVo;
}
文章发布实现
步骤:
1)从前端就收到文章相关参数,建立ArticleParam类;
2)article表中存入相关属性;
3)Article_tag表中存入标签和文章id数据;
4)Article_body表中存入文章内容。
注意:由于会存入用户信息,所以要将该接口加入到拦截器中,原理同发布评论相同。
ArticleController
//发布文章
@PostMapping("/publish")
public Result publish(@RequestBody ArticleParam articleParam){
return articleService.publish(articleParam);
}
核心方法publish
@Autowired
ArticleTagMapper articleTagMapper;
//发布文章
@Override
public Result publish(ArticleParam articleParam) {
/**
* 1.发布文章 目的构建Article对象
* 2. 作者id 当前登陆用户
* 3. 标签 将标签加入关联表中
* 4. body内容存储 article bodyId
* @param articleParam
* @return
* 此接口要加入登陆拦截中
*/
//获得用户信息,前提,将该请接口加入到拦截其中
SysUser sysUser = UserThreadLocal.get();
System.out.println("----------用户信息");
System.out.println(sysUser);
System.out.println("----------");
Article article = new Article();
article.setAuthorId(sysUser.getId());
article.setWeight(Article.Article_Common);
article.setViewCounts(0);
article.setTitle(articleParam.getTitle());
article.setSummary(articleParam.getSummary());
article.setCommentCounts(0);
article.setCreateDate(System.currentTimeMillis());
article.setCategoryId(articleParam.getCategory().getId());
System.out.println("----------");
System.out.println(article);
System.out.println("----------");
//插入之后会自动生成id
articleMapper.insert(article);
//tag插入数据
List<TagVo> tags = articleParam.getTags();
if(tags != null){
for(TagVo tag : tags){
Long articleId = article.getId();
ArticleTag articleTag = new ArticleTag();
articleTag.setTagId((tag.getId()));
articleTag.setArticleId(articleId);
articleTagMapper.insert(articleTag);
}
}
//articleBody插入数据
ArticleBody articleBody = new ArticleBody();
articleBody.setArticleId(article.getId());
articleBody.setContent(articleParam.getBody().getContent());
articleBody.setContentHtml(articleParam.getBody().getContentHtml());
articleBodyMapper.insert(articleBody);
//设置文章bodyId
article.setBodyId(articleBody.getId());
//更新article bodyId字段
articleMapper.updateById(article);
//将id转换成string放入map
Map<String,String> map = new HashMap<>();
map.put("id",article.getId().toString());
return Result.success(map);
}
发布文章成功
AOP日志(10/11)
通过注解实现AOP。
Springboot 自定义注解+AOP
1)创建注解
package com.komorebi.common.aop;
import org.apache.catalina.startup.SetContextPropertiesRule;
import java.lang.annotation.*;
@Target({ElementType.METHOD})//使用在方法上的注解
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogAnnotation {
//注解的两个属性值
String module() default "";
String operator() default "";
}
1)AOP面向切面
实现日志输出
定义切入点:此处为自定义的注解
@Pointcut("@annotation(com.komorebi.common.aop.LogAnnotation)")
public void pt(){}
环绕通知:@Around(“pt()”)
return joinPoint.proceed(): 这个是从切点的环绕增强里面脱离出来
joinPoint.getSignature:获取被增强目标对象
getMethod:获得方法对象
getAnnotation:获得注解
package com.komorebi.common.aop;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Component//注入到ioc容器
@Aspect//这是一个增强类
@Slf4j
public class LogAspect {
@Pointcut("@annotation(com.komorebi.common.aop.LogAnnotation)")
public void pt(){}
//环绕通知
@Around("pt()")
public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
long beginTime = System.currentTimeMillis();
//执行方法
Object result = joinPoint.proceed();
//执行时长(毫秒)
long time = System.currentTimeMillis() - beginTime;
//保存日志
recordLog(joinPoint, time);
return result;
}
private void recordLog(ProceedingJoinPoint joinPoint, long time) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
LogAnnotation logAnnotation = method.getAnnotation(LogAnnotation.class);
log.info("=====================log start================================");
log.info("module:{}",logAnnotation.module());
log.info("operation:{}",logAnnotation.operator());
//请求的方法名
String className = joinPoint.getTarget().getClass().getName();
String methodName = signature.getName();
log.info("request method:{}",className + "." + methodName + "()");
// //请求的参数
Object[] args = joinPoint.getArgs();
String params = JSON.toJSONString(args[0]);
log.info("params:{}",params);
log.info("excute time : {} ms",time);
log.info("=====================log end================================");
}
}
五、文章分类页面
展示所有的分类信息,并且点击进入相应的分类后,可以显示对应分类的文章。
CategoryController
//文章分类页面
@GetMapping("/detail")
public Result findAllDetail(){
return categoryService.findAllDetail();
}
//某个分类对应的文章
@GetMapping("/detail/{id}")
public Result categoryDetailById(@PathVariable("id") Long id){
return categoryService.categoryDetailById(id);
}
因为要获取文章,所以我们只需要在原有的获取文章列表方法中,增加一个查询条件即可。
ArticleServiceImpl
//文章分类页面,按照分类CategoryId展示文章
if(pageParams.getCategoryId()!=null){
//加入分类 条件查询
wrapper.eq(Article::getCategoryId,pageParams.getCategoryId());
}
展示
相应分类对应文章页面
六、文章标签页面
展示所有的标签信息,并且点击进入相应的标签后,可以显示对应标签的文章。
TagController
//标签页面展示所有标签
@GetMapping("/detail")
public Result findAllDetail(){
return tagService.findAllDetail();
}
//标签页面,某个标签对应的所有文章
@GetMapping("/detail/{id}")
public Result tagsDetailById(@PathVariable("id") Long id){
return tagService.tagsDetailById(id);
}
TagsServiceImpl
//文章标签页面获取所有标签
@Override
public Result findAllDetail() {
List<Tag> tags = tagMapper.selectList(null);
return Result.success(tags);
}
//文章标签页面根据标签获取文章
@Override
public Result tagsDetailById(Long id) {
Tag tag = tagMapper.selectById(id);
return Result.success(tag);
}
因为要获取文章,所以我们只需要在原有的获取文章列表方法中,增加一个查询条件即可。
ArticleServiceImpl
//文章标签页面,按照分类TagId展示文章
List<Long> articleIdList = new ArrayList<>();
if(pageParams.getTagId()!=null){
//加入标签 条件查询
//article表中没有tag字段,因为一篇文章有多个标签
//映射为一张新表article_tag :article_id 1:n tag_id
//1、查询标签id对应的文章id列表
LambdaQueryWrapper<ArticleTag> articleTagLambdaQueryWrapper = new LambdaQueryWrapper<>();
articleTagLambdaQueryWrapper.eq(ArticleTag::getTagId,pageParams.getTagId());
List<ArticleTag> articleTags = articleTagMapper.selectList(articleTagLambdaQueryWrapper);
for (ArticleTag articleTag : articleTags){
articleIdList.add(articleTag.getArticleId());
}
if(articleIdList.size()>0){
wrapper.in(Article::getId,articleIdList);
}
}
展示
标签对应文章
七、文章归档页面
由于文章归档涉及到年,月的属性,所以PageParams类需要添加一些属性
PageParams
package com.komorebi.vo;
import lombok.Data;
@Data
public class PageParams {
private int page = 1;
private int pageSize = 10;
private Long categoryId;
private Long tagId;
private String year;
private String month;
//将个位月数改为双位数,例如:6月-》06月
public String getMonth(){
if(this.month != null && this.month.length() ==1){
return "0"+this.month;
}
return this.month;
}
}
因为涉及到年月的计算,mybatis_plus不能实现,所以只能使用sql语句实现,又因为文章归档归根结底也是文章查询,只是添加了一些查询条件。
所以将文章查询整体修改为mapper.xml形式,即将以前通过mybatis_plus实现的文章查询注释掉。
ArticleMapper
package com.komorebi.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.komorebi.dos.Archives;
import com.komorebi.pojo.Article;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ArticleMapper extends BaseMapper<Article> {
List<Archives> listArchives();
IPage<Article> listArticle(Page<Article> page,
Long categoryId,
Long tagId,
String year,
String month);
}
ArticleMapper.xml
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.komorebi.mapper.ArticleMapper">
<select id="listArchives" resultType="com.komorebi.dos.Archives">
select YEAR(FROM_UNIXTIME(create_date/1000)) as year,
MONTH(FROM_UNIXTIME(create_date/1000)) as month,
count(*) as count
from ms_article
group by year,month;
</select>
<!-- 文章显示-->
<select id="listArticle" resultType="com.komorebi.pojo.Article">
select * from ms_article
<where>
1=1
<if test="categoryId != null">
and category_id=#{categoryId}
</if>
<if test="tagId != null">
and tag_id=#{tagId}
</if>
<if test="year != null and year.length>0 and month!=null and month.length>0">
and (From_UNIXTIME(create_date/1000,'%Y') = #{year} and From_UNIXTIME(create_date/1000,'%m') = #{month})
</if>
</where>
order by weight desc,create_date desc
</select>
</mapper>
八、统一缓存处理
登陆时我们将用户信息存入了缓存,可以提高响应速度,我们首页加载的东西每次都会去访问数据库,我们可以把它们都加入到缓存中,加快存取速度。
为什么用redis,因为redis是在内存中的,所以响应速度会很快。
如何在不改变原有代码的基础上,加入缓存呢!AOP。
创建Cache注解
package com.komorebi.common.aop;
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Cache {
//缓存过期时间
long expire() default 1*60*1000;
//名称
String name() default "";
}
CacheAOP
package com.komorebi.common.aop;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.komorebi.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.time.Duration;
@Aspect
@Component
@Slf4j
public class CacheAspect {
@Autowired
RedisTemplate<String,String> redisTemplate;
//切入点为注解Cache
@Pointcut("@annotation(com.komorebi.common.aop.Cache)")
public void pt(){}
@Around("pt()")
public Object around(ProceedingJoinPoint joinPoint){
try{
Signature signature = joinPoint.getSignature();
//获得类名
String className = joinPoint.getTarget().getClass().getSimpleName();
//获得方法名
String methodName = signature.getName();
//存取方法参数类型
Class[] parameterTypes = new Class[joinPoint.getArgs().length];
//拿到参数
Object[] args = joinPoint.getArgs();
//将所有参数拼接成字符串
String params = "";
for(int i=0; i<args.length; i++){
if(args[i] != null){
params += JSON.toJSONString(args[i]);
parameterTypes[i] = args[i].getClass();
}else{
parameterTypes[i] = null;
}
}
if(StringUtils.isNotEmpty(params)){
//md5参数加密,用于设置redis key
params = DigestUtils.md5Hex(params);
}
//通过parameterTypes拿到对应的方法
Method method = joinPoint.getSignature().getDeclaringType().getMethod(methodName, parameterTypes);
//获取cache注解
Cache annotation = method.getAnnotation(Cache.class);
//获取过期时间
long expire = annotation.expire();
//缓存名称
String name = annotation.name();
//创建redis Key,保证key的唯一性
String redisKey = name+"::"+className+"::"+methodName+"::"+params;
//1、先从redis中获取要查询的信息
String redisValue = redisTemplate.opsForValue().get(redisKey);
//如果redis中有
if(StringUtils.isNotEmpty(redisValue)){
log.info("走了缓存---,{},{}",className,methodName);
return JSON.parseObject(redisValue,Result.class);
}
//2、redis中没有,访问查询方法,然后将结果存入redis
//proceed()即代表执行了Controller中的方法,
// 如果有返回值就返回,如果没有就不用返回,在这里有返回值,为文章信息
Object proceed = joinPoint.proceed();
//JSON.toJSONString将对象转为json字符串
//JSON.parseObject将json字符串转为对象
redisTemplate.opsForValue().set(redisKey,JSON.toJSONString(proceed), Duration.ofMillis(expire));
log.info("存入缓存---{},{}",className,methodName);
return proceed;
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return Result.fail(-999,"系统错误");
}
}
在想要添加缓存的方法上添加Cache注解
//分页显示文章列表
@PostMapping
@Cache(expire = 5*60*1000,name="listArticle")
@LogAnnotation(module = "文章",operator = "获取文章列表")
public Result listArticle(@RequestBody PageParams pageParams){
return articleService.listArticle(pageParams);
}
查看日志
redis key查询
八、技术总结
1)jwt+redis:token令牌的登陆方式、访问速度快、安全性高,redis做了对token和用户信息的管理,用户登录做了缓存。
2)使用ThreadLocal保存用户信息,在请求的线程内可以直接获取用户信息,不需要再次查缓存或者数据库。
3)ThreadLocal使用结束后,做了value的删除,防止了内存泄漏。
4)线程池应用,对于文章浏览数的更新操作,将其加入线程池,异步任务实现浏览数量更新操作,提高页面响应速度,并且保证了线程安全。
5)AOP实现统一缓存处理,以项目首页内容为例,自定义注解Cache,以注解为切入点,第一次访问首页时,将首页信息存储在redis中,再次访问时,直接在redis中获取,无需再次查询。
6)AOP实现统一日志记录,自定义注解LogAnnotation,以注解为切入点,每次接口调用结束后台打印日志。
7)权限系统,通过Security实现认证和授权