码神之路网站所使用的博客,项目简单,需求明确,容易上手,非常适合作为练手几项目。
成品预览: blog.mszlu.com
项目搭建思路:
1. 提供前端工程,只需要实现后端接口即可
2. 项目以单体架构入手,先快速开发,不考虑项目优化,降低开发负担
3. 开发完成后,开始优化项目,提升编程思维能力(页面静态化,缓存,云存储,日志等等…)
4. docker部署上线
5. 云服务器购买,域名购买,域名备案等…
项目使用技术:springboot + mybatisplus + redis + mysql
工程搭建 1. 前端工程用vscode打开,然后编译启动,具体命令如下:
npm install npm run build npm run dev
BUT,我们在这里不用这种方式,而是采用另外一种方式去启动前端。
下面给出具体命令:
yarn yarn start
看在这里,我们可能会有一个大写的问号:npm和yarn有什么区别呢?
npm和yarn的区别请点这里
总的来说就是,yarn更快,更简洁(真是忘金油式的回答啊!)
总结一下为什么yarn比npm更好吧:
- 速度快: 采用并行安装和离线安装(如果之前安装过yarn,安装时就可以从缓存中离线安装)
- 安装版本统一: (确保每次拉取项目使用的依赖版本都是相同的)
- 更简洁的输出
- 多注册来源处理
- 更好的语义化(命令更简洁)
2.2 maven依赖飘红问题4.0.0 com.raxcl blog-parent 1.0-SNAPSHOT blog-api pom org.springframework.boot spring-boot-starter-parent 2.5.0 8 8 com.alibaba fastjson 1.2.76 commons-collections commons-collections 3.2.2 com.baomidou mybatis-plus-boot-starter 3.4.3 joda-time joda-time 2.10.10 org.springframework.boot spring-boot-maven-plugin
过程中可能会有一些依赖飘红,推测是由于版本号和父项目不一致导致的,先不用管(有强迫症的孩子可以尝试解决) 。 是由于maven版本过低导致的,maven官网推荐大家更新至3.8.3版本。飘红问题即可解决(如果还飘红,可以先输入版本号,刷新pom.xml文件后再删除版本号即可)
3.2 配置信息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
application.properties
#server server.port=8888 spring.application.name=raxcl # datasource spring.datasource.url=jdbc:mysql://106.54.170.191: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_
MybatisPlusConfig
package com.raxcl.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.beans.factory.annotation.Configurable;
@Configuration
@MapperScan("com.raxcl.blog.mapper")
public class MybatisPlusConfig {
//分页插件
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}
WebMVCConfig
package com.raxcl.config;
import org.springframework.beans.factory.annotation.Configurable;
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("
private int commentCounts;
private Long createDate;
private String summary;
private String title;
private int viewCounts;
private int weight;
private Long authorId;
private Long bodyId;
private int category_id;
}
package com.raxcl.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.raxcl.blog.dao.pojo;
import lombok.Data;
@Data
public class Tag {
private Long id;
private String avatar;
private String tagName;
}
1.2.2.2 mapper接口
package com.raxcl.blog.dao.mapper; import com.baomidou.mybatisplus.core.mapper.baseMapper; import com.raxcl.blog.dao.pojo.Article; import org.apache.ibatis.annotations.Mapper; @Mapper public interface ArticleMapper extends baseMapper{ }
package com.raxcl.blog.dao.mapper; import com.baomidou.mybatisplus.core.mapper.baseMapper; import com.raxcl.blog.dao.pojo.SysUser; import org.apache.ibatis.annotations.Mapper; @Mapper public interface SysUserMapper extends baseMapper{ }
package com.raxcl.blog.dao.mapper; import com.baomidou.mybatisplus.core.mapper.baseMapper; import com.raxcl.blog.dao.pojo.Tag; import org.apache.ibatis.annotations.Mapper; import java.util.List; @Mapper public interface TagMapper extends baseMapper1.2.3 vo层{ List findTagByArticleId(Long articleId); }
package com.raxcl.blog.vo.param;
import lombok.Data;
@Data
public class PageParams {
private int page = 1;
private int pageSize = 10;
}
package com.raxcl.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 List categorys;
private Long authorId;
private Long bodyId;
private int category_id;
}
package com.raxcl.blog.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@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.raxcl.blog.vo;
import lombok.Data;
@Data
public class TagVo {
private Long id;
private String tagName;
}
1.2.4 controller层
package com.raxcl.blog.api;
import com.raxcl.blog.service.ArticleService;
import com.raxcl.blog.vo.param.PageParams;
import com.raxcl.blog.vo.Result;
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("articles")
public class ArticleController {
private final ArticleService articleService;
public ArticleController(ArticleService articleService) {
this.articleService = articleService;
}
//Result是统一结果返回(VO类)
@PostMapping
public Result articles(@RequestBody PageParams pageParams){
return articleService.listArticle(pageParams);
}
}
1.2.5 service层
package com.raxcl.blog.service;
import com.raxcl.blog.vo.param.PageParams;
import com.raxcl.blog.vo.Result;
import org.springframework.stereotype.Service;
public interface ArticleService {
Result listArticle(PageParams pageParams);
}
package com.raxcl.blog.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.raxcl.blog.dao.mapper.ArticleMapper;
import com.raxcl.blog.dao.pojo.Article;
import com.raxcl.blog.service.ArticleService;
import com.raxcl.blog.service.SysUserService;
import com.raxcl.blog.service.TagService;
import com.raxcl.blog.vo.ArticleVo;
import com.raxcl.blog.vo.param.PageParams;
import com.raxcl.blog.vo.Result;
import org.joda.time.DateTime;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class ArticleServiceImpl implements ArticleService {
private final ArticleMapper articleMapper;
private final TagService tagService;
private final SysUserService sysUserService;
public ArticleServiceImpl(ArticleMapper articleMapper, TagService tagService, SysUserService sysUserService) {
this.articleMapper = articleMapper;
this.tagService = tagService;
this.sysUserService = sysUserService;
}
@Override
public Result listArticle(PageParams pageParams) {
Page page = new Page<>(pageParams.getPage(),pageParams.getPageSize());
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
//是否置顶进行排序
queryWrapper.orderByDesc(Article::getWeight, Article::getCreateDate);
Page articlePage = articleMapper.selectPage(page, queryWrapper);
//能直接返回吗? 很明显不能
List records = articlePage.getRecords();
List articleVoList = copyList(records, true, true);
return Result.success(articleVoList);
}
private List copyList(List records, boolean isTag, boolean isAuthor) {
List articleVoList = new ArrayList<>();
for (Article record : records){
articleVoList.add(copy(record,isTag, isAuthor));
}
return articleVoList;
}
private ArticleVo copy(Article article, boolean isTag, boolean isAuthor) {
ArticleVo articleVo = new ArticleVo();
BeanUtils.copyProperties(article, articleVo);
articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));
if (isTag){
articleVo.setTags(tagService.findTagByArticleId(article.getId()));
}
if (isAuthor){
articleVo.setAuthor(sysUserService.findUserById(article.getId()).getNickname());
}
return articleVo;
}
}
package com.raxcl.blog.service;
import com.raxcl.blog.dao.pojo.SysUser;
public interface SysUserService {
SysUser findUserById(Long id);
}
package com.raxcl.blog.service.impl;
import com.raxcl.blog.dao.mapper.SysUserMapper;
import com.raxcl.blog.dao.pojo.SysUser;
import com.raxcl.blog.service.SysUserService;
import org.springframework.stereotype.Service;
import java.util.Objects;
@Service
public class SysUserServiceImpl implements SysUserService {
private final SysUserMapper sysUserMapper;
public SysUserServiceImpl(SysUserMapper sysUserMapper) {
this.sysUserMapper = sysUserMapper;
}
@Override
public SysUser findUserById(Long id) {
SysUser sysUser = sysUserMapper.selectById(id);
if (Objects.isNull(sysUser)){
sysUser = new SysUser();
sysUser.setNickname("raxcl学博客");
}
return sysUser;
}
}
package com.raxcl.blog.service;
import com.raxcl.blog.vo.TagVo;
import java.util.List;
public interface TagService {
List findTagByArticleId(Long articleId);
}
package com.raxcl.blog.service.impl;
import com.raxcl.blog.dao.mapper.TagMapper;
import com.raxcl.blog.dao.pojo.Article;
import com.raxcl.blog.dao.pojo.Tag;
import com.raxcl.blog.service.TagService;
import com.raxcl.blog.vo.ArticleVo;
import com.raxcl.blog.vo.TagVo;
import org.joda.time.DateTime;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class TagServiceImpl implements TagService {
private final TagMapper tagMapper;
public TagServiceImpl(TagMapper tagMapper) {
this.tagMapper = tagMapper;
}
@Override
public List findTagByArticleId(Long articleId) {
//mybatisplus 无法进行多表查询?
List tags = tagMapper.findTagByArticleId(articleId);
return copyList(tags);
}
private List copyList(List records) {
List tagVoList = new ArrayList<>();
for (Tag record : records){
tagVoList.add(copy(record));
}
return tagVoList;
}
private TagVo copy(Tag tag) {
TagVo tagVo = new TagVo();
BeanUtils.copyProperties(tag, tagVo);
return tagVo;
}
}
1.2.6 mapper实现层
2. 首页-最热标签 2.1 接口说明
接口url:/tags/hot
请求方式:GET
请求参数:无
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": [
{
"id":1,
"tagName":"4444"
}
]
}
2.2 编码
2.2.1 controller层
package com.raxcl.blog.api;
import com.raxcl.blog.service.TagService;
import com.raxcl.blog.vo.Result;
import com.raxcl.blog.vo.TagVo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("tags")
public class TagsController {
private final TagService tagService;
public TagsController(TagService tagService) {
this.tagService = tagService;
}
@GetMapping("/hot")
public Result listHotTags() {
int limit = 6;
List tagVoList = tagService.hot(limit);
return Result.success(tagVoList);
}
}
2.2.2 service层
package com.raxcl.blog.service;
import com.raxcl.blog.vo.TagVo;
import java.util.List;
public interface TagService {
List hot(int limit);
}
package com.raxcl.blog.service.impl;
import com.raxcl.blog.dao.mapper.TagMapper;
import com.raxcl.blog.dao.pojo.Article;
import com.raxcl.blog.dao.pojo.Tag;
import com.raxcl.blog.service.TagService;
import com.raxcl.blog.vo.ArticleVo;
import com.raxcl.blog.vo.TagVo;
import org.joda.time.DateTime;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class TagServiceImpl implements TagService {
@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);
}
}
2.2.3 mapper层
package com.raxcl.blog.dao.mapper; import com.baomidou.mybatisplus.core.mapper.baseMapper; import com.raxcl.blog.dao.pojo.Tag; import com.raxcl.blog.vo.TagVo; import org.apache.ibatis.annotations.Mapper; import java.util.List; @Mapper public interface TagMapper extends baseMapper{ List findHotsTagIds(int limit); List findTagsByTagIds(List tagIds); }
3. 统一处理异常id,avatar,tag_name
package com.raxcl.blog.handler;
import com.raxcl.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
public Result doException(Exception e){
e.printStackTrace();
return Result.fail(-999,"系统异常");
}
}
4. 首页-最热文章&最新文章
4.1 接口说明
最热文章:
接口url:/articles/hot
请求方式:POST
请求参数:
最新文章:
接口url:/articles/new
请求方式:POST
请求参数:
package com.raxcl.blog.api;
import com.raxcl.blog.service.ArticleService;
import com.raxcl.blog.vo.param.PageParams;
import com.raxcl.blog.vo.Result;
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("articles")
public class ArticleController {
@PostMapping("hot")
public Result hotArticle(){
int limit = 5;
return articleService.hotArticle(limit);
}
@PostMapping("new")
public Result newArticle(){
int limit = 5;
return articleService.newArticle(limit);
}
}
4.3 service层
package com.raxcl.blog.service;
import com.raxcl.blog.vo.param.PageParams;
import com.raxcl.blog.vo.Result;
public interface ArticleService {
Result hotArticle(int limit);
Result newArticle(int limit);
}
package com.raxcl.blog.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.raxcl.blog.dao.mapper.ArticleMapper;
import com.raxcl.blog.dao.pojo.Article;
import com.raxcl.blog.service.ArticleService;
import com.raxcl.blog.service.SysUserService;
import com.raxcl.blog.service.TagService;
import com.raxcl.blog.vo.ArticleVo;
import com.raxcl.blog.vo.param.PageParams;
import com.raxcl.blog.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 {
@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));
}
@Override
public Result newArticle(int limit) {
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.orderByDesc(Article::getCreateDate);
queryWrapper.select(Article::getId,Article::getTitle);
queryWrapper.last("limit "+ limit);
List articles = articleMapper.selectList(queryWrapper);
return Result.success(copyList(articles,false,false));
}
}
5. 首页-文章归档
5.1 接口说明
接口url:/articles/listArchives
请求方式:POST
5.2 dto层package com.raxcl.blog.dao.dto;
import lombok.Data;
@Data
public class Archives {
private Integer year;
private Integer month;
private Integer count;
}
5.3 controller层
package com.raxcl.blog.api;
import com.raxcl.blog.service.ArticleService;
import com.raxcl.blog.vo.param.PageParams;
import com.raxcl.blog.vo.Result;
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("articles")
public class ArticleController {
@PostMapping("listArchives")
public Result listArchives(){
return articleService.listArchives();
}
}
5.3 service层
package com.raxcl.blog.service;
import com.raxcl.blog.vo.param.PageParams;
import com.raxcl.blog.vo.Result;
public interface ArticleService {
Result listArchives();
}
package com.raxcl.blog.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.raxcl.blog.dao.mapper.ArticleMapper;
import com.raxcl.blog.dao.pojo.Article;
import com.raxcl.blog.service.ArticleService;
import com.raxcl.blog.service.SysUserService;
import com.raxcl.blog.service.TagService;
import com.raxcl.blog.vo.ArticleVo;
import com.raxcl.blog.vo.param.PageParams;
import com.raxcl.blog.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 {
@PostMapping("listArchives")
public Result listArchives(){
return articleService.listArchives();
}
}
5.4 mapper层
package com.raxcl.blog.dao.mapper; import com.baomidou.mybatisplus.core.mapper.baseMapper; import com.raxcl.blog.dao.dto.Archives; import com.raxcl.blog.dao.pojo.Article; import org.apache.ibatis.annotations.Mapper; import java.util.List; @Mapper public interface ArticleMapper extends baseMapper{ List listArchives(); }
6. 登录功能 6.1 接口说明select year(create_date) year, month(create_date) month, count(*) count from ms_article group by year, month
接口url:/login
请求方式:POST
请求参数:
| 参数名称 | 参数类型 | 说明 |
|---|---|---|
| account | string | 账号 |
| password | string | 密码 |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": "token"
}
6.2 pom.xml
commons-codec
commons-codec
6.3 vo层
package com.raxcl.blog.vo.param;
import lombok.Data;
@Data
public class LoginParam {
private String account;
private String password;
}
package com.raxcl.blog.vo;
public enum ErrorCode {
PARAMS_ERROR(10001,"参数有误"),
ACCOUNT_PWD_NOT_EXIST (10002,"用户名或密码不存在"),;
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;
}
}
6.4 utils层
package com.raxcl.blog.utils;
import io.jsonwebtoken.*;
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;
}
}
6.5 controller层
package com.raxcl.blog.controller;
import com.raxcl.blog.service.LoginService;
import com.raxcl.blog.vo.Result;
import com.raxcl.blog.vo.param.LoginParam;
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 {
private final LoginService loginService;
public LoginController(LoginService loginService) {
this.loginService = loginService;
}
@PostMapping
public Result login(@RequestBody LoginParam loginParam){
return loginService.login(loginParam);
}
}
6.6 service层
package com.raxcl.blog.service;
import com.raxcl.blog.vo.Result;
import com.raxcl.blog.vo.param.LoginParam;
public interface LoginService {
Result login(LoginParam loginParam);
}
package com.raxcl.blog.service.impl;
import com.alibaba.fastjson.JSON;
import com.raxcl.blog.dao.pojo.SysUser;
import com.raxcl.blog.service.LoginService;
import com.raxcl.blog.service.SysUserService;
import com.raxcl.blog.utils.JWTUtils;
import com.raxcl.blog.vo.ErrorCode;
import com.raxcl.blog.vo.Result;
import com.raxcl.blog.vo.param.LoginParam;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@Service
public class LoginServiceImpl implements LoginService {
private static final String slat = "raxcl!@#";
private final SysUserService sysUserService;
private final RedisTemplate redisTemplate;
public LoginServiceImpl(SysUserService sysUserService, RedisTemplate redisTemplate) {
this.sysUserService = sysUserService;
this.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 (Objects.isNull(sysUser)){
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);
}
}
package com.raxcl.blog.service;
import com.raxcl.blog.dao.pojo.SysUser;
public interface SysUserService {
SysUser findUser(String account, String pwd);
}
package com.raxcl.blog.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.raxcl.blog.dao.mapper.SysUserMapper;
import com.raxcl.blog.dao.pojo.SysUser;
import com.raxcl.blog.service.SysUserService;
import org.springframework.stereotype.Service;
import java.util.Objects;
@Service
public class SysUserServiceImpl implements SysUserService {
@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;
}
}
7. 获取用户信息
7.1 接口说明
接口url:/users/currentUser
请求方式:GET
请求参数:
| 参数名称 | 参数类型 | 说明 |
|---|---|---|
| Authorization | string | 头部信息(TOKEN) |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": {
"id":1,
"account":"1",
"nickaname":"1",
"avatar":"ss"
}
}
7.2 controller层
package com.raxcl.blog.controller;
import com.raxcl.blog.service.SysUserService;
import com.raxcl.blog.vo.Result;
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 {
private final SysUserService sysUserService;
public UserController(SysUserService sysUserService) {
this.sysUserService = sysUserService;
}
@GetMapping("currentUser")
public Result currentUser (@RequestHeader("Authorization") String token){
return sysUserService.getUserInfoByToken(token);
}
}
7.3 vo层
package com.raxcl.blog.vo;
import lombok.Data;
@Data
public class LoginUserVo {
private Long id;
private String account;
private String nickname;
private String avatar;
}
7.4 service层
package com.raxcl.blog.service;
import com.raxcl.blog.dao.pojo.SysUser;
import com.raxcl.blog.vo.Result;
public interface SysUserService {
Result getUserInfoByToken(String token);
}
package com.raxcl.blog.service.impl;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.raxcl.blog.dao.mapper.SysUserMapper;
import com.raxcl.blog.dao.pojo.SysUser;
import com.raxcl.blog.service.SysUserService;
import com.raxcl.blog.utils.JWTUtils;
import com.raxcl.blog.vo.ErrorCode;
import com.raxcl.blog.vo.LoginUserVo;
import com.raxcl.blog.vo.Result;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.Objects;
@Service
public class SysUserServiceImpl implements SysUserService {
private final SysUserMapper sysUserMapper;
private final RedisTemplate redisTemplate;
public SysUserServiceImpl(SysUserMapper sysUserMapper, RedisTemplate redisTemplate) {
this.sysUserMapper = sysUserMapper;
this.redisTemplate = redisTemplate;
}
@Override
public Result getUserInfoByToken(String token) {
Map map = JWTUtils.checkToken((token));
if (Objects.isNull(map)){
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);
}
}
8. 登出
接口url:/logout
请求方式:GET
请求参数:
| 参数名称 | 参数类型 | 说明 |
|---|---|---|
| Authorization | string | 头部信息(TOKEN) |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": null
}
8.1 controller层
package com.raxcl.blog.controller;
import com.raxcl.blog.service.LoginService;
import com.raxcl.blog.vo.Result;
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("logout")
public class LogoutController {
private final LoginService loginService;
public LogoutController(LoginService loginService) {
this.loginService = loginService;
}
@GetMapping
public Result logout(@RequestHeader("Authorization") String token){
return loginService.logout(token);
}
}
8.2 service层
package com.raxcl.blog.service;
import com.raxcl.blog.vo.Result;
import com.raxcl.blog.vo.param.LoginParam;
public interface LoginService {
Result logout(String token);
}
package com.raxcl.blog.service.impl;
import com.alibaba.fastjson.JSON;
import com.raxcl.blog.dao.pojo.SysUser;
import com.raxcl.blog.service.LoginService;
import com.raxcl.blog.service.SysUserService;
import com.raxcl.blog.utils.JWTUtils;
import com.raxcl.blog.vo.ErrorCode;
import com.raxcl.blog.vo.Result;
import com.raxcl.blog.vo.param.LoginParam;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@Service
public class LoginServiceImpl implements LoginService {
@Override
public Result logout(String token) {
redisTemplate.delete("TOLEN_"+token);
return Result.success(null);
}
}
9. 注册
9.1 接口说明
接口url:/register
请求方式:POST
请求参数:
| 参数名称 | 参数类型 | 说明 |
|---|---|---|
| account | string | 账号 |
| password | string | 密码 |
| nickname | string | 昵称 |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": "token"
}
9.2 vo层
package com.raxcl.blog.vo.param;
import lombok.Data;
@Data
public class LoginParam {
private String account;
private String password;
private String nickname;
}
package com.raxcl.blog.vo;
public enum ErrorCode {
PARAMS_ERROR(10001,"参数有误"),
ACCOUNT_PWD_NOT_EXIST (10002,"用户名或密码不存在"),
NO_PERMISSION(70001,"无访问权限"),
SESSION_TIME_OUT(90001,"会话超时"),
NO_LOGIN(90002,"未登录"),
ACCOUNT_EXIST(10004,"账号已存在");
}
9.3 controller层
package com.raxcl.blog.controller;
import com.raxcl.blog.service.LoginService;
import com.raxcl.blog.vo.Result;
import com.raxcl.blog.vo.param.LoginParam;
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 {
private final LoginService loginService;
public RegisterController(LoginService loginService) {
this.loginService = loginService;
}
@PostMapping
public Result register(@RequestBody LoginParam loginParam){
return loginService.register(loginParam);
}
}
9.4 service层
package com.raxcl.blog.service;
import com.raxcl.blog.vo.Result;
import com.raxcl.blog.vo.param.LoginParam;
public interface LoginService {
Result register(LoginParam loginParam);
}
package com.raxcl.blog.service.impl;
import com.alibaba.fastjson.JSON;
import com.raxcl.blog.dao.pojo.SysUser;
import com.raxcl.blog.service.LoginService;
import com.raxcl.blog.service.SysUserService;
import com.raxcl.blog.utils.JWTUtils;
import com.raxcl.blog.vo.ErrorCode;
import com.raxcl.blog.vo.Result;
import com.raxcl.blog.vo.param.LoginParam;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@Service
@Transactional
public class LoginServiceImpl implements LoginService {
private static final String slat = "raxcl!@#";
private final SysUserService sysUserService;
private final RedisTemplate redisTemplate;
public LoginServiceImpl(SysUserService sysUserService, RedisTemplate redisTemplate) {
this.sysUserService = sysUserService;
this.redisTemplate = redisTemplate;
}
@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);
}
}
package com.raxcl.blog.service;
import com.raxcl.blog.dao.pojo.SysUser;
import com.raxcl.blog.vo.Result;
public interface SysUserService {
SysUser findUserByAccount(String account);
void save(SysUser sysUser);
}
package com.raxcl.blog.service.impl;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.raxcl.blog.dao.mapper.SysUserMapper;
import com.raxcl.blog.dao.pojo.SysUser;
import com.raxcl.blog.service.SysUserService;
import com.raxcl.blog.utils.JWTUtils;
import com.raxcl.blog.vo.ErrorCode;
import com.raxcl.blog.vo.LoginUserVo;
import com.raxcl.blog.vo.Result;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.Objects;
@Service
public class SysUserServiceImpl implements SysUserService {
private final SysUserMapper sysUserMapper;
private final RedisTemplate redisTemplate;
public SysUserServiceImpl(SysUserMapper sysUserMapper, RedisTemplate redisTemplate) {
this.sysUserMapper = sysUserMapper;
this.redisTemplate = redisTemplate;
}
@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 采用了雪花算法
this.sysUserMapper.insert(sysUser);
}
}
10. 登录拦截器
每次访问需要登录的资源的时候,都需要在代码中进行判断,一旦登录的逻辑有所改变,代码都得进行变动,非常不合适。
那么可不可以统一进行登录判断呢?
可以,使用拦截器,进行登录拦截,如果遇到需要登录才能访问的接口,如果未登录,拦截器直接返回,并跳转登录页面。
10.1 hander层package com.raxcl.blog.handler;
import com.alibaba.fastjson.JSON;
import com.raxcl.blog.dao.pojo.SysUser;
import com.raxcl.blog.service.LoginService;
import com.raxcl.blog.vo.ErrorCode;
import com.raxcl.blog.vo.Result;
import lombok.extern.slf4j.Slf4j;
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;
import java.io.IOException;
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
private final LoginService loginService;
public LoginInterceptor(LoginService loginService) {
this.loginService = loginService;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if(!(handler instanceof HandlerMethod)){
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;
}
//是登录状态,放行
return true;
}
}
10.2 service层
package com.raxcl.blog.service;
import com.raxcl.blog.dao.pojo.SysUser;
import com.raxcl.blog.vo.Result;
import com.raxcl.blog.vo.param.LoginParam;
public interface LoginService {
SysUser checkToken(String token);
}
package com.raxcl.blog.service.impl;
import com.alibaba.fastjson.JSON;
import com.raxcl.blog.dao.pojo.SysUser;
import com.raxcl.blog.service.LoginService;
import com.raxcl.blog.service.SysUserService;
import com.raxcl.blog.utils.JWTUtils;
import com.raxcl.blog.vo.ErrorCode;
import com.raxcl.blog.vo.Result;
import com.raxcl.blog.vo.param.LoginParam;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
@Service
@Transactional
public class LoginServiceImpl implements LoginService {
@Override
public SysUser checkToken(String token) {
if (StringUtils.isBlank(token)){
return null;
}
Map stringObjectMap = JWTUtils.checkToken(token);
if (stringObjectMap == null){
return null;
}
String userJson = redisTemplate.opsForValue().get("TOKEN_"+ token);
if (StringUtils.isBlank(userJson)){
return null;
}
SysUser sysUser = JSON.parseObject(userJson, SysUser.class);
return sysUser;
}
}
10.3 config层(使拦截器生效)
package com.raxcl.blog.config;
import com.raxcl.blog.handler.LoginInterceptor;
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 {
private final LoginInterceptor loginInterceptor;
public WebMVCConfig(LoginInterceptor loginInterceptor) {
this.loginInterceptor = loginInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry){
//拦截test接口,后续实际遇到需要拦截的接口时,在配置为真正的拦截接口
registry.addInterceptor(loginInterceptor).addPathPatterns("/test");
}
}
10.4 测试层(controller)
package com.raxcl.blog.controller;
import com.raxcl.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(){
return Result.success(null);
}
}
11. ThreadLocal保存用户信息
11.1 utils层
package com.raxcl.blog.utils;
import com.raxcl.blog.dao.pojo.SysUser;
public class UserThreadLocal {
private UserThreadLocal(){}
private static final ThreadLocal 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();
}
}
11.2 handler层
package com.raxcl.blog.handler;
import com.alibaba.fastjson.JSON;
import com.raxcl.blog.dao.pojo.SysUser;
import com.raxcl.blog.service.LoginService;
import com.raxcl.blog.utils.UserThreadLocal;
import com.raxcl.blog.vo.ErrorCode;
import com.raxcl.blog.vo.Result;
import lombok.extern.slf4j.Slf4j;
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;
import java.io.IOException;
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
private final LoginService loginService;
public LoginInterceptor(LoginService loginService) {
this.loginService = loginService;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//在执行controller方法之前进行执行
//在执行controller方法(Handler)之前进行执行
if(!(handler instanceof HandlerMethod)){
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;
}
//是登录状态,放行
UserThreadLocal.put(sysUser);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex){
UserThreadLocal.remove();
}
}
11.3 controller层
package com.raxcl.blog.controller;
import com.raxcl.blog.utils.UserThreadLocal;
import com.raxcl.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(){
System.out.println("哈哈哈哈哈哈");
System.out.println(UserThreadLocal.get());
return Result.success(null);
}
}
12. ThreadLocal内存泄漏
实线代表强引用,虚线代表弱引用
每一个Thread维护一个ThreadLocalMap, key为使用弱引用的ThreadLocal实例,value为线程变量的副本。
强引用,使用最普遍的引用,一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。
弱引用,JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。



