- spring boot 练手实战项目说明
- 基础知识
- 1. 工程搭建
- 1.1 新建maven工程
- 1.2 配置
- 1.3 启动类
- 2. 首页-文章列表
- 2.1 接口说明
- 2.2 编码
- 2.2.1 表结构
- 2.2.2 Controller
- 2.2.3 Service
- 2.2.4 Dao
- 2.2.5 测试
- 3. 首页-最热标签
- 3.1 接口说明
- 3.2 编码
- 3.2.1 Controller
- 3.2.2 Service
- 3.2.3 Dao
- 3.2.4 测试
- 1. 统一异常处理
- 2. 首页-最热文章
- 2.1 接口说明
- 2.2 Controller
- 2.3 Service
- 2.4 测试
- 3. 首页-最新文章
- 3.1 接口说明
- 3.1 Controller
- 3.2 Service
- 4. 首页-文章归档
- 4.1接口说明
- 4.1 Controller
- 4.2 Service
- 4.3 Dao
- 4.4 测试
- 1. 登录
- 1.1 接口说明
- 1.2 JWT
- 1.3 Controller
- 1.4 Service
- 1.5 登录参数,redis配置,统一错误码
- 1.6 测试
- 2. 获取用户信息
- 2.1 接口说明
- 2.2 Controller
- 2.3 Service
- 2.4 LoginUserVo
- 2.5 测试
- 3. 退出登录
- 3.1 接口说明
- 3.2 Controller
- 3.3 Service
- 3.4 测试
- 1. 注册
- 1.1 接口说明
- 1.2 Controller
- 1.3 Service
- 1.4 加事务
- 1.5 测试
- 2. 登录拦截器
- 2.1 拦截器实现
- 2.2 使拦截器生效
- 2.3 测试
- 3. ThreadLocal保存用户信息
- 1. ThreadLocal内存泄漏
- 2. 文章详情
- 2.1 接口说明
- 2.2 涉及到的表
- 2.3 Controller
- 2.4 Service
- 2.5 测试
- 3. 使用线程池 更新阅读次数
- 3.1 线程池配置
- 3.1 使用
- 3.3 测试
- Bug修正
- 1. 评论列表
- 1.1 接口说明
- 1.2 Controller
- 1.3 Service
- 2. 评论
- 2.1 接口说明
- 2.2 加入到登录拦截器中
- 2.3 Controller
- 2.4 Service
视频链接
码神之路网站所使用的博客,项目简单,需求明确,容易上手,非常适合做为练手级项目。
最终成品
blog.mszlu.com
项目讲解说明:
- 提供前端工程,只需要实现后端接口即可
- 项目以单体架构入手,先快速开发,不考虑项目优化,降低开发负担
- 开发完成后,开始优化项目,提升编程思维能力
- 比如页面静态化,缓存,云存储,日志等
- docker部署上线
- 云服务器购买,域名购买,域名备案等
项目使用技术 :
springboot + mybatisplus+redis+mysql
基础知识mybatisDao层 Mapper层 controller层 service层 model层 entity层 简介
mall商场学习文档
mybatisplus学习文档
mybatisplus配套代码
easycode搭配mybatisplus巨爽
前端的工程:
npm install npm run build npm run dev1.1 新建maven工程
1.2 配置4.0.0 com.mszlu blog-parent 1.0-SNAPSHOT org.springframework.boot spring-boot-starter-parent 2.5.0 UTF-8 UTF-8 1.8 org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-logging org.springframework.boot spring-boot-starter-log4j2 org.springframework.boot spring-boot-starter-aop org.springframework.boot spring-boot-starter-mail org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-starter-data-redis com.alibaba fastjson 1.2.76 mysql mysql-connector-java org.springframework.boot spring-boot-configuration-processor true org.apache.commons commons-lang3 commons-collections commons-collections 3.2.2 com.baomidou mybatis-plus-boot-starter 3.4.3 org.projectlombok lombok joda-time joda-time 2.10.10 org.springframework.boot spring-boot-maven-plugin
#server server.port= 8888 spring.application.name=mszlu_blog # datasource spring.datasource.url=jdbc:mysql://localhost:3306/blog?useUnicode=true&characterEncoding=UTF-8&serverTimeZone=UTC spring.datasource.username=root spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver #mybatis-plus mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl mybatis-plus.global-config.db-config.table-prefix=ms_
package com.mszlu.blog.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
//扫包,将此包下的接口生成代理实现类,并且注册到spring容器中
@MapperScan("com.mszlu.blog.dao")
public class MybatisPlusConfig {
//分页插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}
package com.mszlu.blog.config;
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 WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
//跨域配置,不可设置为*,不安全, 前后端分离项目,可能域名不一致
//本地测试 端口不一致 也算跨域
registry.addMapping("
private Long authorId;
private Long bodyId;
private Long categoryId;
private int weight = Article_Common;
private Long createDate;
}
package com.mszlu.blog.dao.pojo;
import lombok.Data;
@Data
public class SysUser {
private Long id;
private String account;
private Integer admin;
private String avatar;
private Long createDate;
private Integer deleted;
private String email;
private Long lastLogin;
private String mobilePhoneNumber;
private String nickname;
private String password;
private String salt;
private String status;
}
package com.mszlu.blog.dao.pojo;
import lombok.Data;
@Data
public class Tag {
private Long id;
private String avatar;
private String tagName;
}
2.2.2 Controller
package com.mszlu.blog.api;
import com.mszlu.blog.dao.pojo.Article;
import com.mszlu.blog.service.ArticleService;
import com.mszlu.blog.vo.Archive;
import com.mszlu.blog.vo.ArticleVo;
import com.mszlu.blog.vo.Result;
import com.mszlu.blog.vo.params.PageParams;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("articles")
public class ArticleController {
@Autowired
private ArticleService articleService;
//Result是统一结果返回
@PostMapping
public Result articles(@RequestBody PageParams pageParams) {
//ArticleVo 页面接收的数据
List articles = articleService.listArticlesPage(pageParams);
return Result.success(articles);
}
}
package com.mszlu.blog.vo;
import com.mszlu.blog.dao.pojo.Article;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result {
private boolean success;
private Integer code;
private String msg;
private Object data;
public static Result success(Object data) {
return new Result(true,200,"success",data);
}
public static Result fail(Integer code, String msg) {
return new Result(false,code,msg,null);
}
}
package com.mszlu.blog.vo;
import com.mszlu.blog.dao.pojo.ArticleBody;
import com.mszlu.blog.dao.pojo.Category;
import com.mszlu.blog.dao.pojo.SysUser;
import com.mszlu.blog.dao.pojo.Tag;
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 tags;
private List categorys;
}
2.2.3 Service
package com.mszlu.blog.service;
import com.mszlu.blog.vo.Archive;
import com.mszlu.blog.vo.ArticleVo;
import com.mszlu.blog.vo.params.PageParams;
import java.util.List;
public interface ArticleService {
List listArticlesPage(PageParams pageParams);
}
package com.mszlu.blog.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.mszlu.blog.dao.ArticleMapper;
import com.mszlu.blog.dao.SysUserMapper;
import com.mszlu.blog.dao.pojo.Article;
import com.mszlu.blog.dao.pojo.SysUser;
import com.mszlu.blog.dao.pojo.Tag;
import com.mszlu.blog.service.ArticleService;
import com.mszlu.blog.service.SysUserService;
import com.mszlu.blog.service.TagsService;
import com.mszlu.blog.vo.ArticleBodyVo;
import com.mszlu.blog.vo.ArticleVo;
import com.mszlu.blog.vo.TagVo;
import com.mszlu.blog.vo.params.PageParams;
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
private ArticleMapper articleMapper;
@Autowired
private SysUserService sysUserService;
@Autowired
private TagsService tagsService;
public ArticleVo copy(Article article,boolean isAuthor,boolean isBody,boolean isTags){
ArticleVo articleVo = new ArticleVo();
BeanUtils.copyProperties(article, articleVo);
if (isAuthor) {
SysUser sysUser = sysUserService.findSysUserById(article.getAuthorId());
articleVo.setAuthor(sysUser.getNickname());
}
articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));
if (isTags){
List tags = tagsService.findTagsByArticleId(article.getId());
articleVo.setTags(tags);
}
return articleVo;
}
private List copyList(List records,boolean isAuthor,boolean isBody,boolean isTags) {
List articleVoList = new ArrayList<>();
for (Article article : records) {
ArticleVo articleVo = copy(article,isAuthor,isBody,isTags);
articleVoList.add(articleVo);
}
return articleVoList;
}
@Override
public List listArticlesPage(PageParams pageParams) {
QueryWrapper queryWrapper = new QueryWrapper<>();
Page page = new Page<>(pageParams.getPage(),pageParams.getPageSize());
Page articlePage = articleMapper.selectPage(page, queryWrapper);
List articleVoList = copyList(articlePage.getRecords(),true,false,true);
return articleVoList;
}
}
package com.mszlu.blog.service;
import com.mszlu.blog.dao.pojo.SysUser;
public interface UserService {
SysUser findUserById(Long userId);
}
package com.mszlu.blog.service.impl;
import com.mszlu.blog.dao.SysUserMapper;
import com.mszlu.blog.dao.pojo.SysUser;
import com.mszlu.blog.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private SysUserMapper sysUserMapper;
@Override
public SysUser findUserById(Long userId) {
SysUser sysUser = sysUserMapper.selectById(userId);
if (sysUser == null) {
sysUser = new SysUser();
sysUser.setNickname("码神之路");
}
return sysUser;
}
}
package com.mszlu.blog.service;
import com.mszlu.blog.dao.pojo.Tag;
import com.mszlu.blog.vo.TagVo;
import java.util.List;
public interface TagsService {
List findTagsByArticleId(Long id);
}
package com.mszlu.blog.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.mszlu.blog.dao.TagMapper;
import com.mszlu.blog.dao.pojo.Tag;
import com.mszlu.blog.service.TagsService;
import com.mszlu.blog.vo.TagVo;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@Service
public class TagsServiceImpl implements TagsService {
@Autowired
private TagMapper tagMapper;
public TagVo copy(Tag tag){
TagVo tagVo = new TagVo();
BeanUtils.copyProperties(tag,tagVo);
return tagVo;
}
public List copyList(List tagList){
List tagVoList = new ArrayList<>();
for (Tag tag : tagList) {
tagVoList.add(copy(tag));
}
return tagVoList;
}
@Override
public List findTagsByArticleId(Long id) {
List tags = tagMapper.findTagsByArticleId(id);
return copyList(tags);
}
}
2.2.4 Dao
package com.mszlu.blog.dao; import com.baomidou.mybatisplus.core.mapper.baseMapper; import com.mszlu.blog.dao.pojo.Article; import com.mszlu.blog.vo.ArticleVo; import java.util.List; public interface ArticleMapper extends baseMapper{ }
package com.mszlu.blog.dao; import com.baomidou.mybatisplus.core.mapper.baseMapper; import com.mszlu.blog.dao.pojo.Tag; import java.util.List; public interface TagMapper extends baseMapper{ List findTagsByArticleId(Long articleId); }
package com.mszlu.blog.dao; import com.baomidou.mybatisplus.core.mapper.baseMapper; import com.mszlu.blog.dao.pojo.SysUser; public interface SysUserMapper extends baseMapper{ }
2.2.5 测试 3. 首页-最热标签 3.1 接口说明id,avatar,tag_name as tagName
接口url:/tags/hot
请求方式:GET
请求参数:无
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": [
{
"id":1,
"tagName":"4444"
}
]
}
3.2 编码
3.2.1 Controller
package com.mszlu.blog.api;
import com.mszlu.blog.service.ArticleService;
import com.mszlu.blog.service.TagsService;
import com.mszlu.blog.vo.Archive;
import com.mszlu.blog.vo.ArticleVo;
import com.mszlu.blog.vo.Result;
import com.mszlu.blog.vo.TagVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("tags")
public class TagsController {
@Autowired
private TagsService tagsService;
@GetMapping("/hot")
public Result listHotTags() {
int limit = 6;
List tagVoList = tagsService.hot(limit);
return Result.success(tagVoList);
}
}
package com.mszlu.blog.vo;
import lombok.Data;
@Data
public class TagVo {
private Long id;
private String tagName;
}
3.2.2 Service
package com.mszlu.blog.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.mszlu.blog.dao.TagMapper;
import com.mszlu.blog.dao.pojo.Tag;
import com.mszlu.blog.service.TagsService;
import com.mszlu.blog.vo.TagVo;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@Service
public class TagsServiceImpl implements TagsService {
@Autowired
private TagMapper tagMapper;
public TagVo copy(Tag tag){
TagVo tagVo = new TagVo();
BeanUtils.copyProperties(tag,tagVo);
return tagVo;
}
public List copyList(List tagList){
List tagVoList = new ArrayList<>();
for (Tag tag : tagList) {
tagVoList.add(copy(tag));
}
return tagVoList;
}
@Override
public List hot(int limit) {
List hotsTagIds = tagMapper.findHotsTagIds(limit);
if (CollectionUtils.isEmpty(hotsTagIds)){
return Collections.emptyList();
}
List tagList = tagMapper.findTagsByTagIds(hotsTagIds);
return copyList(tagList);
}
}
package com.mszlu.blog.service;
import com.mszlu.blog.vo.TagVo;
import java.util.List;
public interface TagsService {
List hot(int limit);
}
3.2.3 Dao
package com.mszlu.blog.dao; import com.baomidou.mybatisplus.core.mapper.baseMapper; import com.mszlu.blog.dao.pojo.Tag; import java.util.List; public interface TagMapper extends baseMapper{ List findTagsByTagIds(List tagIds); List findHotsTagIds(int limit); }
3.2.4 测试 1. 统一异常处理id,avatar,tag_name as tagName
不管是controller层还是service,dao层,都有可能报异常,如果是预料中的异常,可以直接捕获处理,如果是意料之外的异常,需要统一进行处理,进行记录,并给用户提示相对比较友好的信息。
package com.mszlu.blog.handler;
import com.mszlu.blog.vo.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
//对加了@Controller注解的方法进行拦截处理 AOP的实现
@ControllerAdvice
public class AllExceptionHandler {
//进行异常处理,处理Exception.class的异常
@ExceptionHandler(Exception.class)
@ResponseBody //返回json数据
public Result doException(Exception ex){
ex.printStackTrace();
return Result.fail(-999,"系统异常");
}
}
2. 首页-最热文章
2.1 接口说明
接口url:/articles/hot
请求方式:POST
请求参数:
| 参数名称 | 参数类型 | 说明 |
|---|---|---|
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": [
{
"id": 1,
"title": "springboot介绍以及入门案例",
},
{
"id": 9,
"title": "Vue.js 是什么",
},
{
"id": 10,
"title": "Element相关",
}
]
}
2.2 Controller
@PostMapping("hot")
public Result hotArticle(){
int limit = 5;
return articleService.hotArticle(limit);
}
2.3 Service
Result hotArticle(int limit);
@Override
public Result hotArticle(int limit) {
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.orderByDesc(Article::getViewCounts);
queryWrapper.select(Article::getId,Article::getTitle);
queryWrapper.last("limit " + limit);
List articles = articleMapper.selectList(queryWrapper);
return Result.success(copyList(articles,false,false));
}
2.4 测试
3. 首页-最新文章
3.1 接口说明
接口url:/articles/new
请求方式:POST
请求参数:
| 参数名称 | 参数类型 | 说明 |
|---|---|---|
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": [
{
"id": 1,
"title": "springboot介绍以及入门案例",
},
{
"id": 9,
"title": "Vue.js 是什么",
},
{
"id": 10,
"title": "Element相关",
}
]
}
3.1 Controller
@PostMapping("new")
public Result newArticles(){
int limit = 5;
return articleService.newArticles(limit);
}
3.2 Service
Result newArticles(int limit);
@Override
public Result newArticles(int limit) {
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.orderByDesc(Article::getCreateDate);
queryWrapper.select(Article::getId,Article::getTitle);
queryWrapper.last("limit "+limit);
//select id,title from article order by create_date desc limit 5
List articles = articleMapper.selectList(queryWrapper);
return Result.success(copyList(articles,false,false));
}
4. 首页-文章归档
4.1接口说明
接口url:/articles/listArchives
请求方式:POST
请求参数:
| 参数名称 | 参数类型 | 说明 |
|---|---|---|
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": [
{
"year": "2021",
"month": "6",
"count": 2
}
]
}
select year(create_date) as year,month(create_date) as month,count(*) as count from ms_article group by year,month
4.1 Controller
@PostMapping("listArchives")
public Result listArchives(){
return articleService.listArchives();
}
package com.mszlu.blog.dao.dos;
import lombok.Data;
@Data
public class Archives {
//档案类的
private Integer year;
private Integer month;
private Integer count;
}
4.2 Service
ArticleService.java
Result listArchives();
ArticleServiceImpl.java
@Override
public Result listArchives() {
List archivesList = articleMapper.listArchives();
return Result.success(archivesList);
}
4.3 Dao
package com.mszlu.blog.dao.mapper; import com.baomidou.mybatisplus.core.mapper.baseMapper; import com.mszlu.blog.dao.pojo.Article; import java.util.List; import java.util.Map; public interface ArticleMapper extends baseMapper{ List listArchives(); }
4.4 测试select year(create_date) as year,month(create_date) as month,count(*) as count from ms_article group by year,month
注意:前端工程 需使用当天资料下的app
1. 登录 1.1 接口说明接口url:/login
请求方式:POST
请求参数:
| 参数名称 | 参数类型 | 说明 |
|---|---|---|
| account | string | 账号 |
| password | string | 密码 |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": "token"
}
1.2 JWT
登录使用JWT技术。
jwt 可以生成 一个加密的token,做为用户登录的令牌,当用户登录成功之后,发放给客户端。
请求需要登录的资源或者接口的时候,将token携带,后端验证token是否合法。
jwt 有三部分组成:A.B.C
A:Header,{“type”:“JWT”,“alg”:“HS256”} 固定
B:playload,存放信息,比如,用户id,过期时间等等,可以被解密,不能存放敏感信息
C: 签证,A和B加上秘钥 加密而成,只要秘钥不丢失,可以认为是安全的。
jwt 验证,主要就是验证C部分 是否合法。
依赖包:
io.jsonwebtoken jjwt 0.9.1
工具类:
package com.mszlu.blog.utils;
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class JWTUtils {
private static final String jwtToken = "123456Mszlu!@#$$";
public static String createToken(Long userId){
Map claims = new HashMap<>();
claims.put("userId",userId);
JwtBuilder jwtBuilder = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, jwtToken) // 签发算法,秘钥为jwtToken
.setClaims(claims) // body数据,要唯一,自行设置
.setIssuedAt(new Date()) // 设置签发时间
.setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 60 * 1000));// 一天的有效时间
String token = jwtBuilder.compact();
return token;
}
public static Map checkToken(String token){
try {
Jwt parse = Jwts.parser().setSigningKey(jwtToken).parse(token);
return (Map) parse.getBody();
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}
1.3 Controller
package com.mszlu.blog.controller;
import com.mszlu.blog.service.LoginService;
import com.mszlu.blog.vo.Result;
import com.mszlu.blog.vo.params.LoginParam;
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);
}
}
1.4 Service
package com.mszlu.blog.service;
import com.mszlu.blog.vo.Result;
import com.mszlu.blog.vo.params.LoginParam;
public interface LoginService {
Result login(LoginParam loginParam);
}
md5加密的依赖包:
commons-codec commons-codec
package com.mszlu.blog.service.impl;
import com.alibaba.fastjson.JSON;
import com.mszlu.blog.dao.pojo.SysUser;
import com.mszlu.blog.service.LoginService;
import com.mszlu.blog.service.SysUserService;
import com.mszlu.blog.utils.JWTUtils;
import com.mszlu.blog.vo.ErrorCode;
import com.mszlu.blog.vo.Result;
import com.mszlu.blog.vo.params.LoginParam;
import org.apache.commons.codec.digest.DigestUtils;
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 java.util.concurrent.TimeUnit;
@Service
public class LoginServiceImpl implements LoginService {
//加密盐用于加密
private static final String slat = "mszlu!@#";
@Autowired
private SysUserService sysUserService;
@Autowired
private RedisTemplate redisTemplate;
@Override
public Result login(LoginParam loginParam) {
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());
}
String pwd = DigestUtils.md5Hex(password + slat);
SysUser sysUser = sysUserService.findUser(account,pwd);
if (sysUser == null){
return Result.fail(ErrorCode.ACCOUNT_PWD_NOT_EXIST.getCode(),ErrorCode.ACCOUNT_PWD_NOT_EXIST.getMsg());
}
//登录成功,使用JWT生成token,返回token和redis中
String token = JWTUtils.createToken(sysUser.getId());
redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(sysUser),1, TimeUnit.DAYS);
return Result.success(token);
}
//生成我们想要的密码,放于数据库用于登陆
public static void main(String[] args) {
System.out.println(DigestUtils.md5Hex("admin"+slat));
}
}
SysUserServiceImpl.java
@Override
public SysUser findUser(String account, String pwd) {
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SysUser::getAccount,account);
queryWrapper.eq(SysUser::getPassword,pwd);
queryWrapper.select(SysUser::getId,SysUser::getAccount,SysUser::getAvatar,SysUser::getNickname);
queryWrapper.last("limit 1");
SysUser sysUser = sysUserMapper.selectOne(queryWrapper);
return sysUser;
}
SysUserService.java
SysUser findUser(String account, String pwd);1.5 登录参数,redis配置,统一错误码
package com.mszlu.blog.vo.params;
import lombok.Data;
@Data
public class LoginParam {
private String account;
private String password;
}
spring.redis.host=localhost spring.redis.port=6379
package com.mszlu.blog.vo;
public enum ErrorCode {
PARAMS_ERROR(10001,"参数有误"),
ACCOUNT_PWD_NOT_EXIST(10002,"用户名或密码不存在"),
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;
}
}
1.6 测试
使用postman测试,因为登录后,需要跳转页面,进行token认证,有接口未写,前端会出现问题。
token前端获取到之后,会存储在 storage中 h5 ,本地存储
2. 获取用户信息 2.1 接口说明接口url:/users/currentUser
请求方式:GET
请求参数:
| 参数名称 | 参数类型 | 说明 |
|---|---|---|
| Authorization | string | 头部信息(TOKEN) |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": {
"id":1,
"account":"1",
"nickaname":"1",
"avatar":"ss"
}
}
2.2 Controller
package com.mszlu.blog.controller;
import com.mszlu.blog.service.SysUserService;
import com.mszlu.blog.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
private SysUserService sysUserService;
@GetMapping("currentUser")
public Result currentUser(@RequestHeader("Authorization") String token){
return sysUserService.findUserByToken(token);
}
}
2.3 Service
Result findUserByToken(String token);
@Override
public Result findUserByToken(String token) {
Map map = JWTUtils.checkToken(token);
if (map == null){
return Result.fail(ErrorCode.NO_LOGIN.getCode(),ErrorCode.NO_LOGIN.getMsg());
}
String userJson = redisTemplate.opsForValue().get("TOKEN_" + token);
if (StringUtils.isBlank(userJson)){
return Result.fail(ErrorCode.NO_LOGIN.getCode(),ErrorCode.NO_LOGIN.getMsg());
}
SysUser sysUser = JSON.parseObject(userJson, SysUser.class);
LoginUserVo loginUserVo = new LoginUserVo();
loginUserVo.setAccount(sysUser.getAccount());
loginUserVo.setAvatar(sysUser.getAvatar());
loginUserVo.setId(sysUser.getId());
loginUserVo.setNickname(sysUser.getNickname());
return Result.success(loginUserVo);
}
2.4 LoginUserVo
package com.mszlu.blog.vo;
import lombok.Data;
@Data
public class LoginUserVo {
//与页面交互
private Long id;
private String account;
private String nickname;
private String avatar;
}
2.5 测试
3. 退出登录
3.1 接口说明
接口url:/logout
请求方式:GET
请求参数:
| 参数名称 | 参数类型 | 说明 |
|---|---|---|
| Authorization | string | 头部信息(TOKEN) |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": null
}
3.2 Controller
package com.mszlu.blog.controller;
import com.mszlu.blog.service.LoginService;
import com.mszlu.blog.vo.Result;
import com.mszlu.blog.vo.params.LoginParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("logout")
public class LogoutController {
@Autowired
private LoginService loginService;
@GetMapping
public Result logout(@RequestHeader("Authorization") String token){
return loginService.logout(token);
}
}
3.3 Service
@Override
public Result logout(String token) {
//后端直接删除redis中的token
redisTemplate.delete("TOKEN_"+token);
return Result.success(null);
}
3.4 测试
1. 注册
1.1 接口说明
接口url:/register
请求方式:POST
请求参数:
| 参数名称 | 参数类型 | 说明 |
|---|---|---|
| account | string | 账号 |
| password | string | 密码 |
| nickname | string | 昵称 |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": "token"
}
1.2 Controller
package com.mszlu.blog.controller;
import com.mszlu.blog.service.LoginService;
import com.mszlu.blog.vo.Result;
import com.mszlu.blog.vo.params.LoginParam;
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("register")
public class RegisterController {
@Autowired
private LoginService loginService;
@PostMapping
public Result register(@RequestBody LoginParam loginParam){
//sso 单点登录,后期如果把登录注册功能 提出去(单独的服务,可以独立提供接口服务)
return loginService.register(loginParam);
}
}
参数LoginParam中 添加新的参数nickname。
package com.mszlu.blog.vo.params;
import lombok.Data;
@Data
public class LoginParam {
private String account;
private String password;
private String nickname;
}
1.3 Service
@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 = this.sysUserService.findUserByAccount(account);
if (sysUser != null){
return Result.fail(ErrorCode.ACCOUNT_EXIST.getCode(),ErrorCode.ACCOUNT_EXIST.getMsg());
}
sysUser = new SysUser();
sysUser.setNickname(nickname);
sysUser.setAccount(account);
sysUser.setPassword(DigestUtils.md5Hex(password+slat));
sysUser.setCreateDate(System.currentTimeMillis());
sysUser.setLastLogin(System.currentTimeMillis());
sysUser.setAvatar("/static/img/logo.b3a48c0.png");
sysUser.setAdmin(1); //1 为true
sysUser.setDeleted(0); // 0 为false
sysUser.setSalt("");
sysUser.setStatus("");
sysUser.setEmail("");
this.sysUserService.save(sysUser);
//token
String token = JWTUtils.createToken(sysUser.getId());
redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(sysUser),1, TimeUnit.DAYS);
return Result.success(token);
}
在ErrorCode.java中添加一条
ACCOUNT_EXIST(10004,"账号已存在"),
SysUserService:
SysUser findUserByAccount(String account);
void save(SysUser sysUser);
SysUserServiceImpl :
@Override
public SysUser findUserByAccount(String account) {
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SysUser::getAccount,account);
queryWrapper.last("limit 1");
return sysUserMapper.selectOne(queryWrapper);
}
@Override
public void save(SysUser sysUser) {
//保存用户这 id会自动生成
//这个地方 默认生成的id是 分布式id 雪花算法
//mybatis-plus
this.sysUserMapper.insert(sysUser);
}
1.4 加事务
@Service
@Transactional
public class LoginServiceImpl implements LoginService {}
当然 一般建议加在 接口上,通用一些。
测试的时候 可以将redis 停掉,那么redis连接异常后,新添加的用户 应该执行回滚操作。
1.5 测试 2. 登录拦截器每次访问需要登录的资源的时候,都需要在代码中进行判断,一旦登录的逻辑有所改变,代码都得进行变动,非常不合适。
那么可不可以统一进行登录判断呢?
可以,使用拦截器,进行登录拦截,如果遇到需要登录才能访问的接口,如果未登录,拦截器直接返回,并跳转登录页面。
2.1 拦截器实现package com.mszlu.blog.handler;
import com.alibaba.fastjson.JSON;
import com.mszlu.blog.dao.pojo.SysUser;
import com.mszlu.blog.service.LoginService;
import com.mszlu.blog.vo.ErrorCode;
import com.mszlu.blog.vo.Result;
import lombok.extern.slf4j.Slf4j;
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
private LoginService loginService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//在执行controller方法(Handler)之前进行执行
if (!(handler instanceof HandlerMethod)){
//handler 可能是 RequestResourceHandler springboot 程序 访问静态资源 默认去classpath下的static目录去查询
return true;
}
String token = request.getHeader("Authorization");
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===========================");
if (token == null){
Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), "未登录");
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(JSON.toJSONString(result));
return false;
}
SysUser sysUser = loginService.checkToken(token);
if (sysUser == null){
Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), "未登录");
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(JSON.toJSONString(result));
return false;
}
//是登录状态,放行
//登录验证成功,放行
//我希望在controller中 直接获取用户的信息 怎么获取?
return true;
}
}
2.2 使拦截器生效
package com.mszlu.blog.config;
import com.mszlu.blog.handler.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMVCConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addCorsMappings(CorsRegistry registry) {
//跨域配置
registry.addMapping("
if (!(handler instanceof HandlerMethod)){
//handler 可能是 RequestResourceHandler springboot 程序 访问静态资源 默认去classpath下的static目录去查询
return true;
}
String token = request.getHeader("Authorization");
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===========================");
if (StringUtils.isBlank(token)){
Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), "未登录");
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(JSON.toJSONString(result));
return false;
}
SysUser sysUser = loginService.checkToken(token);
if (sysUser == null){
Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), "未登录");
response.setContentType("application/json;charset=utf-8");
response.getWriter().print(JSON.toJSONString(result));
return false;
}
//登录验证成功,放行
//我希望在controller中 直接获取用户的信息 怎么获取?
UserThreadLocal.put(sysUser);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//如果不删除 ThreadLocal中用完的信息 会有内存泄漏的风险
UserThreadLocal.remove();
}
}
package com.mszlu.blog.controller;
import com.mszlu.blog.dao.pojo.SysUser;
import com.mszlu.blog.utils.UserThreadLocal;
import com.mszlu.blog.vo.Result;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("test")
public class TestController {
@RequestMapping
public Result test(){
// SysUser
SysUser sysUser = UserThreadLocal.get();
System.out.println(sysUser);
return Result.success(null);
}
}
1. ThreadLocal内存泄漏
实线代表强引用,虚线代表弱引用
每一个Thread维护一个ThreadLocalMap, key为使用弱引用的ThreadLocal实例,value为线程变量的副本。
强引用,使用最普遍的引用,一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。
弱引用,JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。
2. 文章详情 2.1 接口说明接口url:/articles/view/{id}
请求方式:POST
请求参数:
| 参数名称 | 参数类型 | 说明 |
|---|---|---|
| id | long | 文章id(路径参数) |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": "token"
}
2.2 涉及到的表
content存放makedown格式的信息
content_html存放html格式的信息
CREATE TABLE `blog`.`ms_article_body` ( `id` bigint(0) NOT NULL AUTO_INCREMENT, `content` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL, `content_html` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL, `article_id` bigint(0) NOT NULL, PRIMARY KEY (`id`) USING BTREE, INDEX `article_id`(`article_id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 38 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
package com.mszlu.blog.dao.pojo;
import lombok.Data;
@Data
public class ArticleBody {
private Long id;
private String content;
private String contentHtml;
private Long articleId;
}
avata分类图标路径
category_name图标分类的名称
description分类的描述
CREATE TABLE `blog`.`ms_category` ( `id` bigint(0) NOT NULL AUTO_INCREMENT, `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, `category_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
package com.mszlu.blog.dao.pojo;
import lombok.Data;
@Data
public class Category {
private Long id;
private String avatar;
private String categoryName;
private String description;
}
2.3 Controller
@PostMapping("view/{id}")
public Result findArticleById(@PathVariable("id") Long articleId){
return articleService.findArticleById(articleId);
}
2.4 Service
ArticleService.java
Result findArticleById(Long articleId);
body_id对应第二张表ms_article_body上的id
ArticleServiceImpl.java
@Override
public Result findArticleById(Long articleId) {
Article article = this.articleMapper.selectById(articleId);
ArticleVo articleVo = copy(article, true, true,true,true);
//查看完文章了,新增阅读数,有没有问题呢?
//查看完文章之后,本应该直接返回数据了,这时候做了一个更新操作,更新时加写锁,阻塞其他的读操作,性能就会比较低
// 更新 增加了此次接口的 耗时 如果一旦更新出问题,不能影响 查看文章的操作
//线程池 可以把更新操作 扔到线程池中去执行,和主线程就不相关了
threadService.updateArticleViewCount(articleMapper,article);
return Result.success(articleVo);
}
package com.mszlu.blog.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 tags;
private CategoryVo category;
}
ArticleVo中的属性填充:
private List copyList(List records, boolean isTag, boolean isAuthor) {
List articleVoList = new ArrayList<>();
for (Article record : records) {
articleVoList.add(copy(record,isTag,isAuthor,false,false));
}
return articleVoList;
}
private List copyList(List records, boolean isTag, boolean isAuthor,boolean isBody) {
List articleVoList = new ArrayList<>();
for (Article record : records) {
articleVoList.add(copy(record,isTag,isAuthor,isBody,false));
}
return articleVoList;
}
private List copyList(List records, boolean isTag, boolean isAuthor,boolean isBody,boolean isCategory) {
List articleVoList = new ArrayList<>();
for (Article record : records) {
articleVoList.add(copy(record,isTag,isAuthor,isBody,isCategory));
}
return articleVoList;
}
private ArticleVo copy(Article article, boolean isTag, boolean isAuthor, boolean isBody, boolean isCategory){
ArticleVo articleVo = new ArticleVo();
BeanUtils.copyProperties(article,articleVo);
articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));
//并不是所有的接口 都需要标签 ,作者信息
if (isTag){
Long articleId = article.getId();
articleVo.setTags(tagService.findTagsByArticleId(articleId));
}
if (isAuthor){
Long authorId = article.getAuthorId();
articleVo.setAuthor(sysUserService.findUserById(authorId).getNickname());
}
if (isBody){
ArticleBodyVo articleBody = findArticleBody(article.getId());
articleVo.setBody(articleBody);
}
if (isCategory){
CategoryVo categoryVo = findCategory(article.getCategoryId());
articleVo.setCategory(categoryVo);
}
return articleVo;
}
@Autowired
private CategoryService categoryService;
private CategoryVo findCategory(Long categoryId) {
return categoryService.findCategoryById(categoryId);
}
@Autowired
private ArticleBodyMapper articleBodyMapper;
private ArticleBodyVo findArticleBody(Long articleId) {
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ArticleBody::getArticleId,articleId);
ArticleBody articleBody = articleBodyMapper.selectOne(queryWrapper);
ArticleBodyVo articleBodyVo = new ArticleBodyVo();
articleBodyVo.setContent(articleBody.getContent());
return articleBodyVo;
}
package com.mszlu.blog.vo;
import lombok.Data;
@Data
public class CategoryVo {
private Long id;
private String avatar;
private String categoryName;
}
package com.mszlu.blog.vo;
import lombok.Data;
@Data
public class ArticleBodyVo {
private String content;
}
package com.mszlu.blog.service;
import com.mszlu.blog.vo.CategoryVo;
public interface CategoryService {
CategoryVo findCategoryById(Long id);
}
package com.mszlu.blog.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.mszlu.blog.dao.mapper.CategoryMapper;
import com.mszlu.blog.dao.pojo.Category;
import com.mszlu.blog.service.CategoryService;
import com.mszlu.blog.vo.CategoryVo;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class CategoryServiceImpl implements CategoryService {
@Autowired
private CategoryMapper categoryMapper;
@Override
public CategoryVo findCategoryById(Long id){
Category category = categoryMapper.selectById(id);
CategoryVo categoryVo = new CategoryVo();
BeanUtils.copyProperties(category,categoryVo);
return categoryVo;
}
}
package com.mszlu.blog.dao.mapper; import com.baomidou.mybatisplus.core.mapper.baseMapper; import com.mszlu.blog.dao.pojo.ArticleBody; public interface ArticleBodyMapper extends baseMapper{ }
package com.mszlu.blog.dao.mapper; import com.baomidou.mybatisplus.core.mapper.baseMapper; import com.mszlu.blog.dao.pojo.Category; public interface CategoryMapper extends baseMapper2.5 测试 3. 使用线程池 更新阅读次数 3.1 线程池配置{ }
package com.mszlu.blog.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("码神之路博客项目");
// 等待所有任务结束后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
//执行初始化
executor.initialize();
return executor;
}
}
3.1 使用
package com.mszlu.blog.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.mszlu.blog.dao.mapper.ArticleMapper;
import com.mszlu.blog.dao.pojo.Article;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Component
public class ThreadService {
@Async("taskExecutor")
public void updateViewCount(ArticleMapper articleMapper,Article article){
Article articleUpdate = new Article();
articleUpdate.setViewCounts(article.getViewCounts() + 1);
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Article::getId,article.getId());
//设置一个 为了在多线程的环境下 线程安全(改之前确认值有没有被其他线程抢先修改)
queryWrapper.eq(Article::getViewCounts,article.getViewCounts());
//类似于 update article set view_count=100 where view_count=99 and id=11
articleMapper.update(articleUpdate,queryWrapper);
try {
//睡眠5秒 证明不会影响主线程的使用
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Autowired
private ThreadService threadService;
@Override
public ArticleVo findArticleById(Long id) {
Article article = articleMapper.selectById(id);
threadService.updateViewCount(articleMapper,article);
return copy(article,true,true,true,true);
}
3.3 测试
睡眠 ThredService中的方法 5秒,不会影响主线程的使用,即文章详情会很快的显示出来,不受影响
Bug修正之前Article中的commentCounts,viewCounts,weight 字段为int,会造成更新阅读次数的时候,将其余两个字段设为初始值0
mybatisplus但凡不是null就会生成到sql语句中进行更新
package com.mszlu.blog.dao.pojo;
import lombok.Data;
@Data
public class Article {
public static final int Article_TOP = 1;
public static final int Article_Common = 0;
private Long id;
private String title;
private String summary;
private Integer commentCounts;
private Integer viewCounts;
private Long authorId;
private Long bodyId;
private Long categoryId;
private Integer weight;
private Long createDate;
}
1. 评论列表
id评论id
content评论内容
create_date评论时间
article_id评论文章
author_id谁评论的
parent_id盖楼功能对评论的评论进行回复
to_uid给谁评论
level评论的是第几层(1级表示最上层的评论,2表示对评论的评论)
CREATE TABLE `blog`.`ms_comment` ( `id` bigint(0) NOT NULL AUTO_INCREMENT, `content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, `create_date` bigint(0) NOT NULL, `article_id` int(0) NOT NULL, `author_id` bigint(0) NOT NULL, `parent_id` bigint(0) NOT NULL, `to_uid` bigint(0) NOT NULL, `level` varchar(1) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, PRIMARY KEY (`id`) USING BTREE, INDEX `article_id`(`article_id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
package com.mszlu.blog.dao.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;
}
1.1 接口说明
接口url:/comments/article/{id}
请求方式:GET
请求参数:
| 参数名称 | 参数类型 | 说明 |
|---|---|---|
| id | long | 文章id(路径参数) |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": [
{
"id": 53,
"author": {
"nickname": "李四",
"avatar": "http://localhost:8080/static/img/logo.b3a48c0.png",
"id": 1
},
"content": "写的好",
"childrens": [
{
"id": 54,
"author": {
"nickname": "李四",
"avatar": "http://localhost:8080/static/img/logo.b3a48c0.png",
"id": 1
},
"content": "111",
"childrens": [],
"createDate": "1973-11-26 08:52",
"level": 2,
"toUser": {
"nickname": "李四",
"avatar": "http://localhost:8080/static/img/logo.b3a48c0.png",
"id": 1
}
}
],
"createDate": "1973-11-27 09:53",
"level": 1,
"toUser": null
}
]
}
1.2 Controller
package com.mszlu.blog.controller;
import com.mszlu.blog.service.CommentsService;
import com.mszlu.blog.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("comments")
public class CommentsController {
@Autowired
private CommentsService commentsService;
@GetMapping("article/{id}")
public Result comments(@PathVariable("id") Long articleId){
return commentsService.commentsByArticleId(articleId);
}
}
1.3 Service
package com.mszlu.blog.service;
import com.mszlu.blog.vo.Result;
public interface CommentsService {
Result commentsByArticleId(Long articleId);
}
package com.mszlu.blog.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.mszlu.blog.dao.mapper.CommentMapper;
import com.mszlu.blog.dao.pojo.Comment;
import com.mszlu.blog.service.CommentsService;
import com.mszlu.blog.service.SysUserService;
import com.mszlu.blog.vo.CommentVo;
import com.mszlu.blog.vo.Result;
import com.mszlu.blog.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 CommentsServiceImpl implements CommentsService {
@Autowired
private CommentMapper commentMapper;
@Autowired
private SysUserService sysUserService;
@Override
public Result commentsByArticleId(Long articleId) {
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Comment::getArticleId,articleId);
queryWrapper.eq(Comment::getLevel,1);
List comments = commentMapper.selectList(queryWrapper);
return Result.success(copyList(comments));
}
private CommentVo copy(Comment comment) {
CommentVo commentVo = new CommentVo();
// 相同属性copy
BeanUtils.copyProperties(comment,commentVo);
commentVo.setId(String.valueOf(comment.getId()));
//作者信息
Long authorId = comment.getAuthorId();
UserVo userVo = this.sysUserService.findUserVoById(authorId);
commentVo.setAuthor(userVo);
//子评论
Integer level = comment.getLevel();
if (1 == level){
Long id = comment.getId();
List commentVoList = findCommentsByParentId(id);
commentVo.setChildrens(commentVoList);
}
//to User 给谁评论
if (level > 1){
Long toUid = comment.getToUid();
UserVo toUserVo = this.sysUserService.findUserVoById(toUid);
commentVo.setToUser(toUserVo);
}
return commentVo;
}
private List findCommentsByParentId(Long id) {
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Comment::getParentId,id);
queryWrapper.eq(Comment::getLevel,2);
List comments = this.commentMapper.selectList(queryWrapper);
return copyList(comments);
}
public List copyList(List commentList){
List commentVoList = new ArrayList<>();
for (Comment comment : commentList) {
commentVoList.add(copy(comment));
}
return commentVoList;
}
}
返回的数据:
package com.mszlu.blog.vo;
import com.mszlu.blog.dao.pojo.SysUser;
import lombok.Data;
import java.util.List;
@Data
public class CommentVo {
private Long id;
private UserVo author;
private String content;
private List childrens;
private String createDate;
private Integer level;
private UserVo toUser;
}
package com.mszlu.blog.vo;
import lombok.Data;
@Data
public class UserVo {
private String nickname;
private String avatar;
private Long id;
}
在SysUserService中提供 查询用户信息的服务:
UserVo findUserVoById(Long id);
@Override
public UserVo findUserVoById(Long id) {
SysUser sysUser = sysUserMapper.selectById(id);
if (sysUser == null){
sysUser = new SysUser();
sysUser.setId(1L);
sysUser.setAvatar("/static/img/logo.b3a48c0.png");
sysUser.setNickname("码神之路");
}
UserVo userVo = new UserVo();
userVo.setAvatar(sysUser.getAvatar());
userVo.setNickname(sysUser.getNickname());
userVo.setId(sysUser.getId());
return userVo;
}
2. 评论
2.1 接口说明
接口url:/comments/create/change
请求方式:POST
请求参数:
| 参数名称 | 参数类型 | 说明 |
|---|---|---|
| articleId | long | 文章id |
| content | string | 评论内容 |
| parent | long | 父评论id |
| toUserId | long | 被评论的用户id |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": null
}
2.2 加入到登录拦截器中
@Override
public void addInterceptors(InterceptorRegistry registry) {
//拦截test接口,后续实际遇到需要拦截的接口时,在配置为真正的拦截接口
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/test").addPathPatterns("/comments/create/change");
}
2.3 Controller
构建评论参数对象:
package com.mszlu.blog.vo.params;
import lombok.Data;
@Data
public class CommentParam {
private Long articleId;
private String content;
private Long parent;
private Long toUserId;
}
@PostMapping("create/change")
public Result comment(@RequestBody CommentParam commentParam){
return commentsService.comment(commentParam);
}
2.4 Service
Result comment(CommentParam commentParam);
@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());
Long parent = commentParam.getParent();
if (parent == null || parent == 0) {
comment.setLevel(1);
}else{
comment.setLevel(2);
}
comment.setParentId(parent == null ? 0 : parent);
Long toUserId = commentParam.getToUserId();
comment.setToUid(toUserId == null ? 0 : toUserId);
this.commentMapper.insert(comment);
return Result.success(null);
}
//防止前端 精度损失 把id转为string
// 分布式id 比较长,传到前端 会有精度损失,必须转为string类型 进行传输,就不会有问题了
@JsonSerialize(using = ToStringSerializer.class)
private Long id;



