spring boot 练手实战项目说明

码神之路网站所使用的博客,项目简单,需求明确,容易上手,非常适合做为练手级项目。

最终成品

blog.mszlu.com

项目讲解说明:

提供前端工程,只需要实现后端接口即可
项目以单体架构入手,先快速开发,不考虑项目优化,降低开发负担
开发完成后,开始优化项目,提升编程思维能力
比如页面静态化,缓存,云存储,日志等
docker部署上线
云服务器购买,域名购买,域名备案等
项目使用技术 :

springboot + mybatisplus+redis+mysql

基础知识
Spring常用注解,注解 IOC ,AOP,MVC 的理解
mybatisDao层 Mapper层 controller层 service层 model层 entity层 简介
mall商场学习文档
mybatisplus学习文档
mybatisplus配套代码
easycode搭配mybatisplus巨爽
@Autowired 的时候为什么注入接口不是实现类
@Service注解为什么是标注在实现类上的
mapper接口需要加注解吗?通过MapperScan减少注解
@Mapper与@MapperScan注解的作用是什么?

工程搭建

前端的工程:

npm install
npm run build
npm run dev

新建Maven工程

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.mszlu</groupId><artifactId>blog-parent</artifactId><version>1.0-SNAPSHOT</version><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.5.0</version><relativePath/></parent><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><java.version>1.8</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId><!-- 排除 默认使用的logback  --><exclusions><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-logging</artifactId></exclusion></exclusions></dependency><!-- log4j2 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-log4j2</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-mail</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.76</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId></dependency><dependency><groupId>commons-collections</groupId><artifactId>commons-collections</artifactId><version>3.2.2</version></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.3</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><!-- https://mvnrepository.com/artifact/joda-time/joda-time --><dependency><groupId>joda-time</groupId><artifactId>joda-time</artifactId><version>2.10.10</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

application配置文件配置

server.port=8888
spring.application.name=komorebi_blog
#数据库配置
spring.datasource.url=jdbc:mysql://localhost:3306/blog?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=747699
#mybatis
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#标识表名均为ms_前缀,后续操作可以不用定义这个前缀
mybatis-plus.global-config.db-config.table-prefix=ms_

MybatisPlus配置

创建配置类,设置分页查询(一般项目都会用到,所以提前配置好),注意@MapperScan(“com.komorebi.mapper”)注解。
配置类一定要加@Configuration

@Configuration
@MapperScan("com.komorebi.mapper")
public class MybatisPlusConfig {//   分页插件@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor(){MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new PaginationInnerInterceptor());return interceptor;}
}

跨域问题解决

创建WebMVCConfig配置类,解决不同端口之间的跨域问题
配置类一定要加@Configuration

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
public class WebMVCConfig implements WebMvcConfigurer {@Overridepublic void addCorsMappings(CorsRegistry registry) {//    跨域设置registry.addMapping("/**").allowCredentials(true).allowedOrigins("http://localhost:8080").allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE").allowedHeaders("*").maxAge(3600);}
}

二、首页配置

首页分页显示文章信息

控制类

@RestController//JSON数据交互
@RequestMapping("articles")
public class ArticleController {@AutowiredArticleService articleService;//分页显示文章列表@PostMappingpublic Result listArticle(@RequestBody PageParams pageParams){return articleService.listArticle(pageParams);}
}

vo类
Vo包中的类才是前端实际拿到的数据。
文章显示控制类返回的是一个Result对象,参数是PageParams对象,这两个类都放在vo包中,vo中的类都是前端显示数据的类,一般前端中只显示数据库表中部分数据。

Result类,定义了两个静态方法,分别表示请求成功、请求失败。

package com.komorebi.vo;import lombok.AllArgsConstructor;
import lombok.Data;@Data
@AllArgsConstructor
public class Result {private boolean success;private int code;private String msg;private Object data;public static Result success(Object data){return new Result(true,200,"success",data);}//请求失败public static Result fail(int code,String msg){return new Result(false,code,msg,null);}
}

前端传给后端接口的json数据,我们都封装为param类,方便操作。
PageParams 类定义了分页查询的page和pageSize,分别对应分页查询中的start和size。

package com.komorebi.vo;import lombok.Data;@Data
public class PageParams {private int page = 1;private int pageSize = 1;
}

ArticleVo 类是前端显示文章信息的类,所以在查询文章列表时,就要做数据库中Article类向ArticleVo 类的转换。

package com.komorebi.vo;import lombok.Data;import java.util.List;@Data
public class ArticleVo {private Long id;private String title;private String summary;  //简介private int commentCounts;private int ViewCounts;private int weight;   //置顶private String createDate;  //创建时间private String author;//暂时不需要
//    private ArticleBodyVo body;private List<TagVo> tags;//暂时不需要
//    private CategoryVo category;}

TagVo 类是前端显示标签信息的类,所以在查询文章列表时,就要做数据库中Tag类向TagVo 类的转换。

package com.komorebi.vo;import lombok.Data;@Data
public class TagVo {private Long id;private String tagName;
}

mapper接口

此处定义了三个mapper接口,分别为ArticleMapper、SysUserMapper、TagMapper。

package com.komorebi.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.komorebi.pojo.Article;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;@Repository
public interface ArticleMapper extends BaseMapper<Article> {
}
--------------------------------------------------------------
@Repository
public interface TagMapper extends BaseMapper<Tag> {List<Tag> findTagsByArticleId(Long articleId);
}
-------------------------------------------------------------
@Repository
public interface SysUserMapper extends BaseMapper<SysUser> {
}

因为tag和article有一张对应的表,所以要查询article对应的tag时,需要设计到多表的查询,但是,mybatisplus不支持多表查询,所以需要自己写mapper.xml文件。

注意:mapper.xml文件所在目录要和mapper对应,本次工程都在com.komorebi.mapper下。
可以在application.properties中mybatis-plus开启驼峰命名
mybatis-plus.configuration.map-underscore-to-camel-case=true
这样SQL语句就不需要as别名。
TagMapper.xml

<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.komorebi.mapper.TagMapper"><select id="findTagsByArticleId" parameterType="long" resultType="com.komorebi.pojo.Tag"># 可以在application.properties中mybatis-plus开启驼峰命名# mybatis-plus.configuration.map-underscore-to-camel-case=true# 这样SQL语句就不需要as别名select id,avatar,tag_name as tagName from ms_tagwhere id in(select tag_id from ms_article_tag where article_id=#{articleId})</select>
</mapper>

service层部分

该阶段定义了三个service接口类。

package com.komorebi.service;import com.komorebi.vo.PageParams;
import com.komorebi.vo.Result;public interface ArticleService {//分页查询文章列表Result listArticle(PageParams pageParams);
}
--------------------------------------------------------
package com.komorebi.service;import com.komorebi.pojo.SysUser;public interface SysUserService {SysUser findUserById(Long id);
}
--------------------------------------------------------
package com.komorebi.service;import com.komorebi.vo.TagVo;import java.util.List;public interface TagService {//通过文章id查询ui赢得标签,有一张表专门映射文章id和标签idList<TagVo> findTagsByArticleId(Long articleId);
}

serviceImpl类

ArticleServiceImpl

该实现类目前实现了文章分页查询。

知识点:
1)Page类定义分页对象;
2)LambdaQueryWrapper定义查询wrapper;
3)selectPage()函数返回的是Page对象,通过getRecords获得Article对象列表。
4)copy和copyList函数实现Article到ArticleVo的转换
5)BeanUtils.copyProperties(article,articleVo),可以实现对象之间的复制,相同属性名复制,不同属性名为null。

package com.komorebi.service;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.komorebi.mapper.ArticleMapper;
import com.komorebi.pojo.Article;
import com.komorebi.vo.ArticleVo;
import com.komorebi.vo.PageParams;
import com.komorebi.vo.Result;
import org.joda.time.DateTime;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.List;@Service
public class ArticleServiceImpl implements ArticleService{@AutowiredArticleMapper articleMapper;@AutowiredTagService tagService;@AutowiredSysUserService sysUserService;//分页查询@Overridepublic Result listArticle(PageParams pageParams) {//分页Page<Article> page = new Page<>(pageParams.getPage(),pageParams.getPageSize());LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<>();//先置顶排序由属性weight决定,后按照时间排序wrapper.orderByDesc(Article::getWeight,Article::getCreateDate);Page<Article> articlePage = articleMapper.selectPage(page, wrapper);//文章列表List<Article> records = articlePage.getRecords();//因为页面展示出来的数据不一定和数据库一i杨没所以我们要做一个抓安环//将查出的数据复制到articleVo中实现解耦,vo和页面数据交换List<ArticleVo> articleVo = copyList(records,true,true);return Result.success(articleVo);}//copyList实现private List<ArticleVo> copyList(List<Article> records,boolean isTag,boolean isAuthor) {ArrayList<ArticleVo> articleVos = new ArrayList<>();for(Article article:records){articleVos.add(copy(article,isTag,isAuthor));}return articleVos;}//这个方法是主要点是BeanUtils,又Spring提供的,专门用来拷贝的,想Article和articlevo相同属性的拷贝过来返回private ArticleVo copy(Article article,boolean isTag,boolean isAuthor){ArticleVo articleVo = new ArticleVo();BeanUtils.copyProperties(article,articleVo);//joda包中的DataTime.toString方法将Article的Long日期属性转为ArticleVo中的字符串日期属性articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));//是否显示标签和作者if(isTag){articleVo.setTags(tagService.findTagsByArticleId(article.getId()));}if(isAuthor){articleVo.setAuthor(sysUserService.findUserById(article.getAuthorId()).getNickname());}return articleVo;}
}

TagMapperServiceImpl
此处也会涉及对象之间的复制,原理同ArticleServiceImpl,这里实现的是Tag类复制为TagVo类。

package com.komorebi.service;import com.komorebi.mapper.TagMapper;
import com.komorebi.pojo.Article;
import com.komorebi.pojo.Tag;
import com.komorebi.vo.ArticleVo;
import com.komorebi.vo.TagVo;
import org.joda.time.DateTime;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.List;@Service
public class TagServiceImpl implements TagService{@AutowiredTagMapper tagMapper;@Overridepublic List<TagVo> findTagsByArticleId(Long articleId) {List<Tag> tags = tagMapper.findTagsByArticleId(articleId);return copyList(tags);}//copyList实现private List<TagVo> copyList(List<Tag> tags) {ArrayList<TagVo> tagVos = new ArrayList<>();for(Tag tag : tags){tagVos.add(copy(tag));}return tagVos;}private TagVo copy(Tag tag){TagVo tagVo = new TagVo();//BeanUtils,copyProperties用于类之间的复制,相同字段复制,不同字段为nullBeanUtils.copyProperties(tag,tagVo);return tagVo;}}

SysUserServiceImpl

package com.komorebi.service;import com.komorebi.mapper.SysUserMapper;
import com.komorebi.pojo.SysUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;@Service
public class SysUserServiceImpl implements SysUserService{@AutowiredSysUserMapper sysUserMapper;@Overridepublic SysUser findUserById(Long id) {return sysUserMapper.selectById(id);}
}

首页

首页最热标签

思路:
1)首先在ms_article_tag表操作,通过tag_id分组并排序获得前几名,返回一个tag_id列表。
2)然后根据tag_id列表查询ms_tag表中对应的id和tagName将查询结果返回为TagVo对象(作者使用的是返回为Tag对象,但是由于前端展示的都是vo类,所以我们转换为TagVo)。

接下来的就是编码环节
TagController类

@RestController
@RequestMapping("/tags")
public class TagsController {@AutowiredTagService tagService;//tags/hot相应最热标签tag对象@RequestMapping("/hot")public Result hot(){int limit = 6;//最热六个return tagService.hots(limit);}
}

TagService

Result hots(int limit);

TagMapper

import com.komorebi.vo.Result;
import com.komorebi.vo.TagVo;
import org.springframework.stereotype.Repository;import java.util.List;@Repository
public interface TagMapper extends BaseMapper<Tag> {List<Tag> findTagsByArticleId(Long articleId);//查询最热标签前limit条List<Long> findHotsTagId(int limit);//通过最热标签tagid查询最热tagsList<TagVo> findTagsByIds(List<Long> tagIds);
}

TagMapper对应的Mapper文件
重点:
1)findHotsTagId涉及分组并排序
2)findTagsByIds涉及到foreach标签

<!--    查询最热标签id,提取前limit个--><select id="findHotsTagId" parameterType="int" resultType="long">select tag_id from ms_article_taggroup by tag_idorder by count(*) limit #{limit}</select><!--    根据最热标签id查询对应tag对象--><select id="findTagsByIds" resultType="com.komorebi.vo.TagVo" parameterType="list">select id, tag_name from ms_tagwhere id in<foreach collection="tagIds" item="tagId" separator="," open="(" close=")">#{tagId}</foreach></select>

TagServiceImpl

 //查询若干个最热标签功能public Result hots(int limit){List<Long> hotsTagId = tagMapper.findHotsTagId(limit);//判断hotsTagId是否为空if(CollectionUtils.isEmpty(hotsTagId)){return Result.success(Collections.emptyList());}List<TagVo> tagsList = tagMapper.findTagsByIds(hotsTagId);return Result.success(tagsList);}

统一异常处理
定义Handler包,设置统一异常处理类AllExceptionHandler ,
1)@ResponseBody
2)@ControllerAdvice
3)@ExceptionHandler(Exception.class)

package com.komorebi.handler;import com.komorebi.vo.Result;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;//对加了@Controller的方法进行拦截处理,AOP实现
@ControllerAdvice
public class AllExceptionHandler {//进行异常处理,处理Exception.class异常@ExceptionHandler(Exception.class)//返回json数据@ResponseBodypublic Result doExceptionHandler(Exception e) {return Result.fail(-999,"系统异常,抱歉!");}
}

最热文章

原理同最热标签查询。根据view_count排序,选择出最热文章。

Controller

//显示最热文章@PostMapping("/hot")public Result hotArticle(){int limit = 3;return articleService.hotArticle(limit);}

Service

Result hotArticle(int limit);

ServiceImpl

//根据view_counts字段查询最热文章@Overridepublic Result hotArticle(int limit) {LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<>();wrapper.orderByDesc(Article::getViewCounts).select(Article::getId,Article::getTitle).last("limit "+limit);List<Article> articles = articleMapper.selectList(wrapper);return Result.success(copyList(articles,false,false));}

最新文章显示

原理和最热文章完全相同,只是通过create_date字段排序,选择出最新
Controller

/

/显示最新文章@PostMapping("/new")public Result newArticle(){int limit = 3;return articleService.newArticle(limit);}

Service

Result newArticle(int limit);

ServiceImpl

//最新文章@Overridepublic Result newArticle(int limit) {LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<>();wrapper.orderByDesc(Article::getCreateDate).select(Article::getId,Article::getTitle).last("limit "+limit);List<Article> articles = articleMapper.selectList(wrapper);return Result.success(copyList(articles,false,false));}

文章归档显示

由于这个归档查询涉及到数据库内部函数Year、Month,所以MybatisPlus不能实现,需要通过mapper.xml文件实现。

注意:新建dos包,do对象也是数据库中查询出的对象,但它并不需要一些持久化的对象,我们把这些对象放在do包中,由于do是一个关键词,所以在命名是加了s,即dos。
文章归档返回的对象为Archives(档案)类。
思路可通过sql语句了解。

Archives类

package com.komorebi.dos;import lombok.Data;@Data
public class Archives {private Integer year;private Integer month;private Long count;}

Controller

//文章归档@PostMapping("/listArchives")public Result listArchives(){return articleService.listArchives();}

Mapper

List<Archives> listArchives();
Mapper.xml
数据库中create_date 为bigint 13位,直接year()不行,需要先转date型后year()。
完整sql语句

select year(FROM_UNIXTIME(create_date/1000)) as year,month(FROM_UNIXTIME(create_date/1000)) as month, count(*) countfrom ms_articlegroup by year,month;<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.komorebi.mapper.ArticleMapper"><select id="listArchives" resultType="com.komorebi.dos.Archives">select YEAR(FROM_UNIXTIME(create_date/1000)) as year,MONTH(FROM_UNIXTIME(create_date/1000)) as month,count(*) as countfrom ms_articlegroup by year,month;</select>
</mapper>

Service

Result listArchives();

ServiceImpl

//文章归档@Overridepublic Result listArchives() {List<Archives> archivesList = articleMapper.listArchives();return Result.success(archivesList);}

三、登录功能实现

登录接口返回给浏览器一个token
因为每次登录都有错误验证,所以自己定义了一个ErrorCode类
ErrorCode.class

package com.komorebi.vo;public enum ErrorCode {PARAMS_ERROR(10001,"参数有误"),ACCOUNT_PWD_NOT_EXIST(10002,"用户密码不存在喔!"),TOKEN_ERROR(10003,"Token不合法"),ACCOUNT_EXIST(10004,"账号已存在"),NO_PERMISSION(70001,"无访问权限"),SESSION_TIME_OUT(90001,"会话超时"),NO_LOGIN(90002,"未登录"),;private int code;private String msg;ErrorCode(int code, String msg) {this.code = code;this.msg = msg;}public int getCode() {return code;}public void setCode(int code) {this.code = code;}public String getMsg() {return msg;}public void setMsg(String msg) {this.msg = msg;}
}

Controller

import com.komorebi.vo.PageParams;
import com.komorebi.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/login")
public class LoginController {@Autowiredprivate LoginService loginService;//登录@PostMappingpublic Result login(@RequestBody LoginParam loginParam){return loginService.login(loginParam);}
}

LoginService

  Result login(LoginParam loginParam);

LoginServiceImpl
导入依赖Commons-codec实现MD5加密

<dependency><groupId>commons-codec</groupId><artifactId>commons-codec</artifactId><version>1.13</version>
</dependency>

登录功能的核心步骤

1、检查参数是否合法
2、根据用户名和密码检查ms_sys_user表中对应的account、password字段
3、如果不存在,登录失败
4、因为数据库中password字段存放的是经过MD5加密过的密码,所以在查数据库表前,要先对password进行加密
5、如果存在,使用jwt,生成token,返回给前端
6、token放在redis中,redis映射token和user信息设置过期时间,先认证token是否合法,再认证redis是否存在

package com.komorebi.service;import com.alibaba.fastjson.JSON;
import com.komorebi.Utils.JWTUtils;
import com.komorebi.pojo.SysUser;
import com.komorebi.vo.ErrorCode;
import com.komorebi.vo.LoginParam;
import com.komorebi.vo.Result;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.apache.commons.codec.digest.DigestUtils;import java.util.concurrent.TimeUnit;@Service
public class LoginServiceImpl implements LoginService{@Autowiredprivate SysUserService sysUserService;@Autowiredprivate RedisTemplate<String,String> redisTemplate;//md5加密使用到的盐private static final String salt="mszlu!@#";@Overridepublic Result login(LoginParam loginParam) {/** 1、检查参数是否合法* 2、根据用户名和密码检查ms_sys_user表中对应的account、password字段* 3、如果不存在,登录失败* 4、如果存在,使用jwt,生成token,返回给前端* 5、token放在redis中,redis映射token和user信息* 设置过期时间,先认证token是否合法,再认证redis是否存在* */String account = loginParam.getAccount();String password = loginParam.getPassword();//用户名或者密码为空if(StringUtils.isBlank(account)||StringUtils.isBlank(password)){//提前写好的错误编码类,方便使用return Result.fail(ErrorCode.PARAMS_ERROR.getCode(),ErrorCode.PARAMS_ERROR.getMsg());}//这里使用的DigestUtils是来自于commons-codec包的,需要外部导入依赖//密码加盐,因为数据库中的密码是经过盐加密的System.out.println(password);SysUser user = sysUserService.findUser(account, password);//用户名密码错误if(user == null){return Result.fail(ErrorCode.ACCOUNT_PWD_NOT_EXIST.getCode(),ErrorCode.ACCOUNT_PWD_NOT_EXIST.getMsg());}//用户名密码正确String token = JWTUtils.createToke(user.getId());//存入redis,要确保已开启redisredisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(user),1, TimeUnit.DAYS);return Result.success(token);}
}

代码分析:
1)先定义一个盐加密字符串,在对password进行加密

private static final String salt="mszlu!@#";
password = DigestUtils.md5Hex(password+salt);

2)验证时需要通过sysUserService查询到对应用户并返回部分用户信息

SysUser user = sysUserService.findUser(account, password);

sysUserService.findUser方法实现

 @Overridepublic SysUser findUser(String account, String password) {LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();wrapper.eq(SysUser::getAccount,account).eq(SysUser::getPassword,password).select(SysUser::getAccount,SysUser::getId,SysUser::getAvatar,SysUser::getNickname).last("limit 1");return sysUserMapper.selectOne(wrapper);}

3)查到用户后,给用户返回一个JWT (token)

//用户名密码正确
String token = JWTUtils.createToke(user.getId());
4)用户token存入redis,要确保已开启redis

 @Autowiredprivate RedisTemplate<String,String> redisTemplate;redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(user),1, TimeUnit.DAYS);

5)由于要用到redis,在配置文件进行配置

#redis配置
spring.redis.host=localhost
spring.redis.port=6379

因为登录方法参数为LoginParam类,该类将前端传给后端的参数封装。
LoginParam

package com.komorebi.vo;import lombok.Data;@Data
public class LoginParam {private String account;private String password;
}

登陆后获取用户信息

登陆后用户的token会存放在浏览器本地,当用户登陆时会在请求头携带token,token中含有用户的id,用户的信息实际存放在redis中。

controller
token放在请求头中,所以要获取token
通过@RequestHeader(“Authorization”)注解获取

package com.komorebi.controller;import com.komorebi.service.SysUserService;
import com.komorebi.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/users")
public class UserController {@AutowiredSysUserService sysUserService;//用户信息展示请求//token放在请求头中,所以要获取token需要@RequestHeader("Authorization")注解@GetMapping("/currentUser")public Result currentUser(@RequestHeader("Authorization") String token){return sysUserService.findUserByToken(token);}
}

SysUserService

Result findUserByToken(String token);

SysUserServiceImpl
该类用于返回用户信息,由于页面展示用户部分信息,所以创建Vo用户类LoginUserVo

package com.komorebi.vo;import lombok.Data;@Data
public class LoginUserVo {private  Long id;private String account;private String nickname;private String avatar; //头像
}

findUserByToken方法

@Overridepublic Result findUserByToken(String token) {//获取用户展示信息SysUser sysUser = loginService.checkToken(token);if(sysUser==null)return Result.fail(ErrorCode.TOKEN_ERROR.getCode(),ErrorCode.TOKEN_ERROR.getMsg());//sysUser转为前端显示对象LoginUserVoLoginUserVo loginUserVo = new LoginUserVo();loginUserVo.setAccount(sysUser.getAccount());loginUserVo.setAvatar(sysUser.getAvatar());loginUserVo.setId(sysUser.getId());loginUserVo.setNickname(sysUser.getNickname());return Result.success(loginUserVo);}

findUserByToken会调用LoginServiceImp中的checkToken方法,检查token的合法性并返回SysUser对象。

LoginServiceImpl

@Overridepublic SysUser checkToken(String token) {//token为空if(StringUtils.isBlank(token))return null;//解析tokenMap<String, Object> checkToken = JWTUtils.checkToken(token);//解析为空if(checkToken==null)return null;//redis不存在token,user信息存放在redis中String userJson = redisTemplate.opsForValue().get("TOKEN_" + token);if(StringUtils.isBlank(userJson))return null;//token解析成功,并且redis存在//JSON.parseObject将json对象转为SysUser对象SysUser sysUser = JSON.parseObject(userJson,SysUser.class);return sysUser;}

退出登录

退出登录就是删除redis中的toke
Controller

@RestController
@RequestMapping("/logout")
public class LoginOutController {@AutowiredLoginService loginService;@GetMappingpublic Result logout(@RequestHeader("Authorization") String token){return loginService.logout(token);}
}

LoginServiceImpl

 //退出登录@Overridepublic Result logout(String token) {redisTemplate.delete("TOKEN_"+token);return Result.success(null);}

注册

注册和登录的功能有点类似
controller

@RestController
@RequestMapping("/register")
public class RegisterController {@AutowiredLoginService loginService;//注册功能,返回数据为token@PostMappingpublic Result register(@RequestBody LoginParam loginParam){return loginService.register(loginParam);}
}

本项目的注册功能在LoginService实现
LoginService

Result register(LoginParam loginParam);

LoginServiceImpl
思路:
1)判断参数是否合法;
2)判断账户是否已经存在;
3)若合法并且不存在,则创建新用户;
4)生成token;
5)token即用户信息存入redis;
6)注意:在SysServiceImpl中设置事务,一旦出现问题,就回滚;
7)返回给前端token.

@Overridepublic Result register(LoginParam loginParam) {String account = loginParam.getAccount();String password = loginParam.getPassword();String nickname = loginParam.getNickname();//用户参数为空if(StringUtils.isBlank(account)||StringUtils.isBlank(password)||StringUtils.isBlank(nickname)){return Result.fail(ErrorCode.PARAMS_ERROR.getCode(), ErrorCode.PARAMS_ERROR.getMsg());}SysUser sysUser = sysUserService.findUserByAccount(account);//用户已经存在if(sysUser!=null){return Result.fail(ErrorCode.ACCOUNT_EXIST.getCode(), "账号已经被注册");}//创建用户,ID默认为自增sysUser =new SysUser();sysUser.setAccount(account);                                   //账户名sysUser.setNickname(nickname);                                  //昵称sysUser.setPassword(DigestUtils.md5Hex(password+salt));  //密码加盐md5sysUser.setCreateDate(System.currentTimeMillis());              //创建时间sysUser.setLastLogin(System.currentTimeMillis());               //最后登录时间sysUser.setAvatar("/static/img/logo.b3a48c0.png");              //头像sysUser.setAdmin(1);                                             //管理员权限sysUser.setDeleted(0);                                             //假删除sysUser.setSalt("");                                                //盐sysUser.setStatus("");                                              //状态sysUser.setEmail("");                                               //邮箱this.sysUserService.save(sysUser);//生成tokenString token = JWTUtils.createToke(sysUser.getId());//token存入redisredisTemplate.opsForValue().set("TOKEN_"+token,JSON.toJSONString(sysUser),1,TimeUnit.DAYS);return Result.success(token);}

上面代码中涉及到两个函数
sysUserService.findUserByAccount(account)查询用户
sysUserService.save(sysUser)创建用户

findUserByAccount方法

   @Overridepublic SysUser findUserByAccount(String account) {LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper<>();wrapper.eq(SysUser::getAccount,account).last("limit 1");return this.sysUserMapper.selectOne(wrapper);}

save方法

   @Overridepublic void save(SysUser sysUser) {//保存用户id会自动生成//默认生成分布式id,采用雪花算法//mybatis-plussysUserMapper.insert(sysUser);}

登录拦截器

定义拦截器LoginInterceptor

package com.komorebi.handler;import com.alibaba.fastjson.JSON;
import com.komorebi.Utils.JWTUtils;
import com.komorebi.pojo.SysUser;
import com.komorebi.service.LoginService;
import com.komorebi.vo.ErrorCode;
import com.komorebi.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {@Autowiredpublic LoginService loginService;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {/*1、需要判断请求的接口上是否是HandleMethod即controller方法* 2、判断token是否为空,为空未登录* 3、不为空,登陆验证(通过LoginServiceImpl中的checkToken方法)* 4、如果认证成功,则放行* */if(!(handler instanceof HandlerMethod)){//拦截器是拦截的controller中的方法,controller的方法其实就是一个Handler//handler可能是RequestResourceHandle(访问资源handle),即可能是访问静态资源的方法//解释:controller对应HandlerMethod,所以拦截器只拦截HandlerMethodreturn true;}//获取tokenString token = request.getHeader("Authorization");//日志问题,需要导入lombok下的@slf4log.info("=============request start=================");String requestURI = request.getRequestURI();log.info("request uri:{}",requestURI);log.info("request method:{}",request.getMethod());log.info("token:{}",token);log.info("=============request end===================");//token为空,不放行if(StringUtils.isBlank(token)){Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());//设置返回消息格式response.setContentType("application/json;charset=utf8");//返回json信息response.getWriter().print(JSON.toJSONString(result));return false;}//token不为空,去做认证SysUser sysUser = loginService.checkToken(token);//用户不存在,即认证失败if(sysUser == null){System.out.println("没有该用户");Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());response.setContentType("application/json;charset=utf8");response.getWriter().print(JSON.toJSONString(result));return false;}//登陆验证成功,放行return true;}
}

配置将拦截器

这里拦截器默认拦截的/test,用于测试拦截器

@Overridepublic void addInterceptors(InterceptorRegistry registry) {//配置拦截接口,此处配置为test,用于测试registry.addInterceptor(loginInterceptor).addPathPatterns("/test");}

ThreadLocal引用

ThreadLocal简介:

多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一乐ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。

本次项目引入ThreadLocal的目的是为实现在Controller层获得用户信息。
原理:
1)在登录拦截放行前将用户信息存入ThreadLocal中;
2)在Controller中获取用户信息;
3)在Controller方法执行完后删除用户信息;
其中
1)对应拦截器中preHandle方法和UserThreadLocal的set方法
2)对应UserThreadLocal的get方法
3)对应拦截器afterCompletion方法和UserThreadLocal的remove方法
所以要创建UserThreadLocal类

package com.komorebi.UserThreadLocal;import com.komorebi.pojo.SysUser;public class UserThreadLocal {
//    声明为私有,即每个线程有自己的ThreadLocal变量private UserThreadLocal(){}
//    实例化一个ThreadLocal的类,即启用private static final ThreadLocal<SysUser> LOCAL = new ThreadLocal<>();//存入用户信息public static void put(SysUser sysUser){LOCAL.set(sysUser);}//获取用户信息public static SysUser get(){return LOCAL.get();}//删除用户信息public static void remove(){LOCAL.remove();}}

UserThreadLocal中set方法在登录拦截器中应用,即登陆时set存入用户信息。

 //为了实现在Controller中获取user用户信息,我们使用ThreadLocal//将用户信息存入ThreadLocal中UserThreadLocal.put(sysUser);//登陆验证成功,放行return true;

UserThreadLocal中get方法在Controller中应用,即登陆后获取用户信息。

  public Result test(){//测试ThreadLocalSysUser sysUser = UserThreadLocal.get();System.out.println(sysUser);return Result.success(null);}

UserThreadLocal中remove方法在登录拦截器afterCompletion方法中应用,即Controller执行完后删除用户信息。

   //controller方法处理完之后的操作,要将ThreadLocal释放掉,否则会内存泄漏@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {UserThreadLocal.remove();}

UserThreadLocal中remove方法在登录拦截器afterCompletion方法中应用,即Controller执行完后删除用户信息。

   //controller方法处理完之后的操作,要将ThreadLocal释放掉,否则会内存泄漏@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {UserThreadLocal.remove();}

ThreadLocal(本地的线程)到底有什么用

比如我们发出一个请求,当你启动某一个进程的时候,你让他和你对应的进程进行绑定的话,会深入的绑定到一起(以达到绑定用户信息的目的)。
为什么在那个后面一定要删除,因为一旦内存泄漏是很严重的

一个线程可以存在多个ThreadLocal每一个Thread维护一个ThreadLocalMap,
key为使用弱引用的ThreadLocal实例,value为线程变量的副本。
强引用,是最普遍的引用,一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。
弱引用,JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。
上面的那个key为使用弱引用的ThreadLocal实例,当我们的线程中的那个ThreadLocal被垃圾回收机制干掉之后,是不是这个弱引用的Key不存在了,但是这个是Map集合呀,Value会永远的存在,所有要手动的删除

四、文章详情

内容及相关信息展示

该模块实现的主要功能是,点击文章标题进入到文章内容显示页面
请求链接为http://localhost:8080/articles/view/{id}
此时文章内容增加了分类(category)和内容(body)两个部分,所以ArticleVo代码要增加两个字段
ArticleVo

package com.komorebi.vo;import lombok.Data;import java.util.List;@Data
public class ArticleVo {private Long id;private String title;private String summary;  //简介private int commentCounts;private int ViewCounts;private int weight;   //置顶private String createDate;  //创建时间private String author;private List<TagVo> tags;//内容属性private ArticleBodyVo body;//分类属性private CategoryVo category;
}

Articlecontroller

  //文章详情@PostMapping("/view/{id}")public Result findArticleById(@PathVariable("id") Long id){return articleService.findArticleById(id);}

findArticleById方法

   @Overridepublic Result findArticleById(Long id) {/** 1、根据id获得article对象* 2、根据bodyId和categoryId去做关联查询* */Article article = this.articleMapper.selectById(id);return Result.success(copy(article,true,true,true,true));}

copy函数改写
之前的copy函数只有三个参数,此时还要加两个参数内容分类

 private ArticleVo copy(Article article,boolean isTag,boolean isAuthor,boolean isBody,boolean isCategory){ArticleVo articleVo = new ArticleVo();BeanUtils.copyProperties(article,articleVo);//joda包中的DataTime.toString方法将Article的Long日期属性转为ArticleVo中的字符串日期属性articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));//是否显示标签和作者if(isTag){articleVo.setTags(tagService.findTagsByArticleId(article.getId()));}if(isAuthor){articleVo.setAuthor(sysUserService.findUserById(article.getAuthorId()).getNickname());}if(isBody){articleVo.setBody(findArticleBodyById(article.getBodyId()));}if(isCategory){articleVo.setCategory(categoryService.findCategoryById(article.getCategoryId()));}return articleVo;}

由于copy被修改,所以copyList也需要修改,未来不改变copyList原有代码,使用重载

//copyList实现,用于将Article列表转换为ArticleVo列表private List<ArticleVo> copyList(List<Article> records,boolean isTag,boolean isAuthor) {ArrayList<ArticleVo> articleVos = new ArrayList<>();for(Article article:records){articleVos.add(copy(article,isTag,isAuthor,false,false));}return articleVos;}//重载private List<ArticleVo> copyList(List<Article> records,boolean isTag,boolean isAuthor,boolean isBody,boolean isCategory) {ArrayList<ArticleVo> articleVos = new ArrayList<>();for(Article article:records){articleVos.add(copy(article,isTag,isAuthor,isBody,isCategory));}return articleVos;}

copy方法中为articleVo设置了body和categories两个属性。
所以要实现两个方法,获取文章内容: findArticleBodyById
获取文章分类:findCategoryById
findArticleBodyById方法(该方法在ArticleServiceImpl中)

 //获得文章body内容private ArticleBodyVo findArticleBodyById(Long bodyId) {ArticleBody articleBody = articleBodyMapper.selectById(bodyId);ArticleBodyVo articleBodyVo = new ArticleBodyVo();articleBodyVo.setContent(articleBody.getContent());return articleBodyVo;}

该方法的返回类型是ArticleBodyVo,所以要创建对应的ArticleBody和ArticleBodyVo类以及ArticleBodyMapper。

ArticleBodyMapper

@Repository
public interface ArticleBodyMapper extends BaseMapper<ArticleBody> {
}

ArticleBody

package com.komorebi.pojo;import lombok.Data;//文章详情内容存放在ms_article_body表中,需要创建一个对应的实体类
@Data
public class ArticleBody {private Long id;private String content;private String contentHtml;private Long articleId;
}

ArticleBodyVo

package com.komorebi.vo;import lombok.Data;@Data
public class ArticleBodyVo {private String content;
}

findCategoryById是由CategoryMapper实现的,所以要创建CategoryMapper、Category、CategoryVo、CategoryService、CategoryServiceImpl。

CategoryMapper

@Repository
public interface CategoryMapper extends BaseMapper<Category> {
}

Category

package com.komorebi.pojo;import lombok.Data;@Data
public class Category {private Long id;private String avatar;private String categoryName;private String description;
}

CategoryVo

package com.komorebi.vo;import lombok.Data;@Data
public class CategoryVo {private Long id;private String avatar;private String categoryName;}

CategoryService

public interface CategoryService {CategoryVo findCategoryById(Long categoryId);
}

CategoryServiceImpl

@Service
public class CategoryServiceImpl implements CategoryService{@AutowiredCategoryMapper categoryMapper;@Overridepublic CategoryVo findCategoryById(Long categoryId) {Category category = categoryMapper.selectById(categoryId);CategoryVo categoryVo = new CategoryVo();BeanUtils.copyProperties(category,categoryVo);return categoryVo;}
}

问题总结

由于数据库中id都是使用的Long,并且所有的Long在数据库中数值位数都设置为20,当数据以json形式传给前端时,前端JavaScript解析20位数的Long会出现溢出,因为JavaScript无法解析过长的数值。

原理:
后端使用64位存储长整数(long),最大支持9223372036854775807
前端的JavaScript使用53位来存放,最大支持9007199254740992,超过最大值的数,可能会出现问题(得到的溢出后的值)
解决方法:降低数据库中Long表示的Id字段的位数,由20降为15,并且将id的数值也修改为位数小于或等于15位的数值。

线程池实现更新评论数

查完文章了,新增阅读数,有没有问题呢?
答案是是有的,本应该直接返回数据,这时候做了一个更新操作,更新时间时加写锁,阻塞其他的读操作,新能就会比较低,
而且更新增加了此次接口的耗时,一旦更新出问题,不能影响我们其他的如:看文章呀什么的
那要怎么样去优化呢?,---->所有想到了线程池
可以把更新操作扔到线程池里面,就不会影响了,和主线程就不相关了

AOP日志

通过注解实现AOP。
Springboot 自定义注解+AOP
1)创建注解

package com.komorebi.common.aop;import org.apache.catalina.startup.SetContextPropertiesRule;import java.lang.annotation.*;@Target({ElementType.METHOD})//使用在方法上的注解
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogAnnotation {//注解的两个属性值String module() default "";String operator() default "";
}

1)AOP面向切面
实现日志输出
定义切入点:此处为自定义的注解
@Pointcut("@annotation(com.komorebi.common.aop.LogAnnotation)")
public void pt(){}
环绕通知:@Around(“pt()”)
return joinPoint.proceed(): 这个是从切点的环绕增强里面脱离出来
joinPoint.getSignature:获取被增强目标对象
getMethod:获得方法对象
getAnnotation:获得注解

package com.komorebi.common.aop;import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;import java.lang.reflect.Method;@Component//注入到ioc容器
@Aspect//这是一个增强类
@Slf4j
public class LogAspect {@Pointcut("@annotation(com.komorebi.common.aop.LogAnnotation)")public void pt(){}//环绕通知@Around("pt()")public Object log(ProceedingJoinPoint joinPoint) throws Throwable {long beginTime = System.currentTimeMillis();//执行方法Object result = joinPoint.proceed();//执行时长(毫秒)long time = System.currentTimeMillis() - beginTime;//保存日志recordLog(joinPoint, time);return result;}private void recordLog(ProceedingJoinPoint joinPoint, long time) {MethodSignature signature = (MethodSignature) joinPoint.getSignature();Method method = signature.getMethod();LogAnnotation logAnnotation = method.getAnnotation(LogAnnotation.class);log.info("=====================log start================================");log.info("module:{}",logAnnotation.module());log.info("operation:{}",logAnnotation.operator());//请求的方法名String className = joinPoint.getTarget().getClass().getName();String methodName = signature.getName();log.info("request method:{}",className + "." + methodName + "()");//        //请求的参数Object[] args = joinPoint.getArgs();String params = JSON.toJSONString(args[0]);log.info("params:{}",params);log.info("excute time : {} ms",time);log.info("=====================log end================================");}}

五、文章分类页面

展示所有的分类信息,并且点击进入相应的分类后,可以显示对应分类的文章。
CategoryController

//文章分类页面@GetMapping("/detail")public Result findAllDetail(){return categoryService.findAllDetail();}//某个分类对应的文章@GetMapping("/detail/{id}")public Result categoryDetailById(@PathVariable("id") Long id){return categoryService.categoryDetailById(id);}

因为要获取文章,所以我们只需要在原有的获取文章列表方法中,增加一个查询条件即可。
ArticleServiceImpl

//文章分类页面,按照分类CategoryId展示文章if(pageParams.getCategoryId()!=null){//加入分类 条件查询wrapper.eq(Article::getCategoryId,pageParams.getCategoryId());}

六、文章标签页面

展示所有的标签信息,并且点击进入相应的标签后,可以显示对应标签的文章。
TagController

//标签页面展示所有标签@GetMapping("/detail")public Result findAllDetail(){return tagService.findAllDetail();}//标签页面,某个标签对应的所有文章@GetMapping("/detail/{id}")public Result tagsDetailById(@PathVariable("id") Long id){return tagService.tagsDetailById(id);}

TagsServiceImpl

//文章标签页面获取所有标签@Overridepublic Result findAllDetail() {List<Tag> tags = tagMapper.selectList(null);return Result.success(tags);}
//文章标签页面根据标签获取文章@Overridepublic Result tagsDetailById(Long id) {Tag tag = tagMapper.selectById(id);return Result.success(tag);}

因为要获取文章,所以我们只需要在原有的获取文章列表方法中,增加一个查询条件即可。
ArticleServiceImpl

//文章标签页面,按照分类TagId展示文章List<Long> articleIdList = new ArrayList<>();if(pageParams.getTagId()!=null){//加入标签 条件查询//article表中没有tag字段,因为一篇文章有多个标签//映射为一张新表article_tag :article_id 1:n tag_id//1、查询标签id对应的文章id列表LambdaQueryWrapper<ArticleTag> articleTagLambdaQueryWrapper = new LambdaQueryWrapper<>();articleTagLambdaQueryWrapper.eq(ArticleTag::getTagId,pageParams.getTagId());List<ArticleTag> articleTags = articleTagMapper.selectList(articleTagLambdaQueryWrapper);for (ArticleTag articleTag : articleTags){articleIdList.add(articleTag.getArticleId());}if(articleIdList.size()>0){wrapper.in(Article::getId,articleIdList);}}

七、文章归档页面

由于文章归档涉及到年,月的属性,所以PageParams类需要添加一些属性
PageParams

package com.komorebi.vo;import lombok.Data;@Data
public class PageParams {private int page = 1;private int pageSize = 10;private Long categoryId;private Long tagId;private String year;private String month;//将个位月数改为双位数,例如:6月-》06月public String getMonth(){if(this.month != null && this.month.length() ==1){return "0"+this.month;}return this.month;}
}

因为涉及到年月的计算,mybatis_plus不能实现,所以只能使用sql语句实现,又因为文章归档归根结底也是文章查询,只是添加了一些查询条件。
所以将文章查询整体修改为mapper.xml形式,即将以前通过mybatis_plus实现的文章查询注释掉。
ArticleMapper

package com.komorebi.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.komorebi.dos.Archives;
import com.komorebi.pojo.Article;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;import java.util.List;@Repository
public interface ArticleMapper extends BaseMapper<Article> {List<Archives> listArchives();IPage<Article> listArticle(Page<Article> page,Long categoryId,Long tagId,String year,String month);
}

ArticleMapper.xml

<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.komorebi.mapper.ArticleMapper"><select id="listArchives" resultType="com.komorebi.dos.Archives">select YEAR(FROM_UNIXTIME(create_date/1000)) as year,MONTH(FROM_UNIXTIME(create_date/1000)) as month,count(*) as countfrom ms_articlegroup by year,month;</select>
<!--    文章显示--><select id="listArticle" resultType="com.komorebi.pojo.Article">select * from ms_article<where>1=1<if test="categoryId != null">and category_id=#{categoryId}</if><if test="tagId != null">and tag_id=#{tagId}</if><if test="year != null and year.length>0 and month!=null and month.length>0">and (From_UNIXTIME(create_date/1000,'%Y') = #{year} and From_UNIXTIME(create_date/1000,'%m') = #{month})</if></where>order by weight desc,create_date desc</select>
</mapper>

八、统一缓存处理

登陆时我们将用户信息存入了缓存,可以提高响应速度,我们首页加载的东西每次都会去访问数据库,我们可以把它们都加入到缓存中,加快存取速度。
为什么用redis,因为redis是在内存中的,所以响应速度会很快。
如何在不改变原有代码的基础上,加入缓存呢!AOP。
创建Cache注解

package com.komorebi.common.aop;import java.lang.annotation.*;@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Cache {//缓存过期时间long expire() default 1*60*1000;//名称String name() default "";
}

CacheAOP

package com.komorebi.common.aop;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.komorebi.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;import java.lang.reflect.Method;
import java.time.Duration;@Aspect
@Component
@Slf4j
public class CacheAspect {@AutowiredRedisTemplate<String,String> redisTemplate;//切入点为注解Cache@Pointcut("@annotation(com.komorebi.common.aop.Cache)")public void pt(){}@Around("pt()")public Object around(ProceedingJoinPoint joinPoint){try{Signature signature = joinPoint.getSignature();//获得类名String className = joinPoint.getTarget().getClass().getSimpleName();//获得方法名String methodName = signature.getName();//存取方法参数类型Class[] parameterTypes = new Class[joinPoint.getArgs().length];//拿到参数Object[] args = joinPoint.getArgs();//将所有参数拼接成字符串String params = "";for(int i=0; i<args.length; i++){if(args[i] != null){params += JSON.toJSONString(args[i]);parameterTypes[i] = args[i].getClass();}else{parameterTypes[i] = null;}}if(StringUtils.isNotEmpty(params)){//md5参数加密,用于设置redis keyparams = DigestUtils.md5Hex(params);}//通过parameterTypes拿到对应的方法Method method = joinPoint.getSignature().getDeclaringType().getMethod(methodName, parameterTypes);//获取cache注解Cache annotation = method.getAnnotation(Cache.class);//获取过期时间long expire = annotation.expire();//缓存名称String name = annotation.name();//创建redis Key,保证key的唯一性String redisKey = name+"::"+className+"::"+methodName+"::"+params;//1、先从redis中获取要查询的信息String redisValue = redisTemplate.opsForValue().get(redisKey);//如果redis中有if(StringUtils.isNotEmpty(redisValue)){log.info("走了缓存---,{},{}",className,methodName);return JSON.parseObject(redisValue,Result.class);}//2、redis中没有,访问查询方法,然后将结果存入redis//proceed()即代表执行了Controller中的方法,// 如果有返回值就返回,如果没有就不用返回,在这里有返回值,为文章信息Object proceed = joinPoint.proceed();//JSON.toJSONString将对象转为json字符串//JSON.parseObject将json字符串转为对象redisTemplate.opsForValue().set(redisKey,JSON.toJSONString(proceed), Duration.ofMillis(expire));log.info("存入缓存---{},{}",className,methodName);return proceed;} catch (Throwable throwable) {throwable.printStackTrace();}return Result.fail(-999,"系统错误");}
}

在想要添加缓存的方法上添加Cache注解

//分页显示文章列表@PostMapping@Cache(expire = 5*60*1000,name="listArticle")@LogAnnotation(module = "文章",operator = "获取文章列表")public Result listArticle(@RequestBody PageParams pageParams){return articleService.listArticle(pageParams);}

八、技术总结

1)jwt+redis:token令牌的登陆方式、访问速度快、安全性高,redis做了对token和用户信息的管理,用户登录做了缓存。
2)使用ThreadLocal保存用户信息,在请求的线程内可以直接获取用户信息,不需要再次查缓存或者数据库。
3)ThreadLocal使用结束后,做了value的删除,防止了内存泄漏。
4)线程池应用,对于文章浏览数的更新操作,将其加入线程池,异步任务实现浏览数量更新操作,提高页面响应速度,并且保证了线程安全。
5)AOP实现统一缓存处理,以项目首页内容为例,自定义注解Cache,以注解为切入点,第一次访问首页时,将首页信息存储在redis中,再次访问时,直接在redis中获取,无需再次查询。
6)AOP实现统一日志记录,自定义注解LogAnnotation,以注解为切入点,每次接口调用结束后台打印日志。
7)权限系统,通过Security实现认证和授权

码神之路项目——总结相关推荐

  1. 码神之路项目总结说明

    前言:码神的博客项目我做了两个星期,今天正好是第三个星期的星期一,于是我打算把这两星期所学做一个总结,以及把开发部署过程中的遇到的一些问题跟大家分享一下,既巩固了所学知识,又能帮助到后来的小伙伴们少踩 ...

  2. springboot+vue博客项目(码神之路博客项目)

    写在最前:b站中博客项目除了这个还有一个三更草堂的博客项目也是不错的,三更草堂会比这个完善些,但是码神这个项目也非常好,看完这个再去看三更的,互相补充.(这两个博客项目应该是目前b站最好的了,而且时间 ...

  3. 码神之路博客部署教程【完整版】|基于Linux的Docker部署教程|非常详细

    说明 前记:最近跟着哔站码神之路做了一个SpringBoot练手项目,第一次操作碰到了很多困难和问题,尤其是在部署部分,走了很多弯路,这里写下自己的部署过程,供大家参考,也欢迎大家提出宝贵的意见. 哔 ...

  4. 码神之路博客部署教程【完整版】基于Linux的Docker部署教程非常详细

    说明 前记:最近跟着哔站码神之路做了一个SpringBoot练手项目,第一次操作碰到了很多困难和问题,尤其是在部署部分,走了很多弯路,这里写下自己的部署过程,供大家参考,也欢迎大家提出宝贵的意见. 哔 ...

  5. 码神之路博客项目构建记录

    个人博客项目 Blog 一.项目搭建(2021.10.6) pom文件导入相关依赖 application配置文件配置 Mybatis Plus配置 跨域问题解决 二.首页配置 首页分页显示文章信息 ...

  6. 博客项目搭建(码神之路)

    文章目录 一.资源下载和项目搭建 二.功能 1.首页文章列表页- - -1 2. 首页文章列表页----2 3.首页-最热标签 4.统一异常处理 5.首页-最热文章 6.首页-最新文章 7.首页-文章 ...

  7. 从码奴到码神之路--初级到中级再到高级程序员的进化之路

    一.初级->中级 通常在初级这个阶段,是进步空间最大的阶段,这个阶段是不存在天花板. 编码:编码是初级阶段最需要经常做的事情,古话说孰能生巧,写代码也是同样的道理. 写什么样的代码 在公司要多做 ...

  8. 码神之路博客系统更换个人卡片图标(iconfont 阿里巴巴矢量图标库的使用)

    目录 个人卡片图标 更换步骤 1.前往iconfont 阿里巴巴矢量图标库官网,并且注册账号: 2.点击资源管理下的我的项目: 3.点击新建项目: 4.新建项目之后,在搜索框搜索想要的图标素材(例如Q ...

  9. 阿里巴巴开发手册最新版、码出高效、性能调优实战、成神之路

    清幽现云山, 虚静出内功 <阿里巴巴Java开发手册>是阿里内部Java工程师所遵循的开发规范,涵盖编程规约.单元测试规约.异常日志规约.MySQL规约.工程规约.安全规约等,这是近万名阿 ...

最新文章

  1. 【转】mac os、linux及unix之间的关系
  2. 工厂设计模式----python版本
  3. 安装mysql 5.1 详细步骤
  4. POJ2262 ZOJ1951 UVa543 Goldbach's Conjecture
  5. android学习资料免费下载
  6. 中科院分区发布2021年期刊重大调整(生信期刊调整为生物学大类)
  7. EMC相关标准 GB IEC EN对照(持续添加中……)
  8. 安卓逆向 -- 防抓包破解(JustTrustMe)
  9. 动态获取图片色值,黑点坐标
  10. python贴吧签到-基于Python3+Requests的贴吧签到助手
  11. 第10章 项目干系人管理
  12. 为什么有些大公司的技术弱爆了?
  13. 网工浓缩笔记以及考点(第四章 无线通信网)
  14. 【模电】第十章、信号处理与信号产生电路(振荡电路)
  15. python爬虫--爬取-猫眼电影-代码
  16. 算法笔记之狄克斯特拉算法
  17. 本地虚拟机搭建nginx web服务器
  18. 基于 PIR 的运动检测:传感器解决方案
  19. android版本 51,51星变手机版
  20. 死亡搁浅运送系统服务器,死亡搁浅图文攻略 主线流程+订单系统+运送流程+建筑搭建 操作介绍-游侠网...

热门文章

  1. 腾讯面了五轮,面委挂了,挂的原因让大家唏嘘...
  2. BIOS 与 CMOS 概述
  3. 管理类联考 综合考试 - 逻辑缺陷
  4. MySql实现联合查询
  5. 富贵包可不富贵,一个原则尽早消除
  6. 首次使用windows10安装DB2_10.5数据库踩坑全教程,附加问题解决目录思路汇总
  7. ADMM算法及其放缩形式,在压缩快照成像重建的图像重建论文中的公式推导
  8. 旋转矩阵R、平移向量t以及变换矩阵T的定义及其下标的含义
  9. 解决USB设备有时无法安全卸载的问题
  10. windows10桌面小箭头去掉后任务栏用不了