文章目录
博客项目
博客项目文件
链接: https://pan.baidu.com/s/1eKeb8GGV7UTMaZIGeSxq5Q
提取码: fgm9 复制这段内容后打开百度网盘手机App,操作更方便哦
项目讲解说明:
- 提供前端工程,只需要实现后端接口即可
- 项目以单体架构入手,先快速开发,不考虑项目优化,降低开发负担
- 开发完成后,开始优化项目,提升编程思维能力
- 比如页面静态化,缓存,云存储,日志等
- docker部署上线
- 云服务器购买,域名购买,域名备案等
项目使用技术 :
springboot + mybatisplus+redis+mysql
1. 工程搭建
前端的工程:
npm install
npm run build
npm run dev
创建父工程
pom.xml
<?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.hmx</groupId>
<artifactId>blog-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<modules>
<module>blog-api</module>
</modules>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.0</version>
</parent>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.5.4</version>
</plugin>
</plugins>
</build>
</project>
1.1 新建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">
<parent>
<artifactId>blog-parent</artifactId>
<groupId>com.hmx</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>blog-api</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--核心启动器,提供自动配置支持,日志和YAML-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--使用Log4j2进行日志记录的starter。替代方案spring-boot-starter-logging-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<!--使用Spring MVC构建Web (包括RESTful) 应用程序的starter。使用Tomcat作为默认的嵌入式容器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--使用Spring AOP和AspectJ进行面向编程的入门者-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!--使用 Java Mail 的 starter 和 Spring Framework 的电子邮件发送支持-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<!--用于使用包括JUnit Jupiter、Hamcrest和 Mockito在内的库测试SpringBoot应用程序的starter-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!--将Redis键值数据存储与Spring Data Redis 和 Lettuce 客户端一起使用的入门者-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--spring默认使用yml中的配置,但有时候要用传统的xml或properties配置,就需要使用spring-boot-configuration-processor了-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!--MySQL提供的JDBC驱动包,用JDBC连接MySQL数据库时必须使用该jar包(与数据库建立连接,发送SQL语句,获取执行结果)-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.25</version>
</dependency>
<!--mybatis增强版,支持JDBC,简化持久层开发-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<!--fastjson是由阿里巴巴工程师基于JAVA开发的一款JSON解析器和生成器,
可用于将Java对象转换为其JSON表示形式。它还可以用于将JSON字符串转换为等效的Java对象。-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
<!--Apache下的一些常用的工具包-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!--为Java标准的Collections API提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。保证性能的同时大大简化代码。 -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.2</version>
</dependency>
<!--Apache开源组织提供的用于摘要运算、编码解码的包。常见的编码解码工具Base64、MD5、Hex、SHA1、DES等-->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<!--七牛云软件开发工具包-->
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
<version>[7.7.0, 7.7.99]</version>
</dependency>
<!--jwt鉴权-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!--提供了一组Java类包用于处理包括ISO8601标准在内的date和time。
可以利用它把JDK Date和Calendar类完全替换掉,而且仍然能够提供很好的集成。-->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>2.10.10</version>
</dependency>
<!--优点:
1. 简化冗余的JavaBean代码;
2. 大大提高JavaBean中方法的执行效率
缺点:
1. 侵入性太强
2. 代码跟踪不友好
3. 版本兼容问题
4. 封装性问题-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
1.2 配置
#server
server:
port: 8888
servlet:
context-path: /api
#mybatis-plus
spring:
application:
name: hmx_blog
# datasource
datasource:
url: jdbc:mysql://localhost:3306/blog?useUnicode=true&characterEncoding=UTF-8&serverTimeZone=UTC
username: root
password: hmx123456
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: localhost
port: 6379
servlet:
multipart:
# 上传文件总的最大值
max-request-size: 20MB
# 单个文件的最大值
max-file-size: 2MB
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
table-prefix: ms_
qiniu:
accessKey: wLc1tIoxzJltwFfazfNFGZPPwkbHZFpCvHEhKWxV
accessSecretKey: IziNo_mUF_NwP_NSog95oVO6zb_m-7YPIc9jwTAD
package com.hmx.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.hmx.blog.dao")
public class MybatisPlusConfig {
//分页插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}
package com.hmx.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("/**").allowedOrigins("http://localhost:8080");
}
}
1.3 启动类
package com.hmx.blog;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class BlogApplication {
public static void main(String[] args) {
SpringApplication.run(BlogApplication.class, args);
}
}
2. 首页-文章列表
2.1 接口说明
接口url:/articles
请求方式:POST
请求参数:
参数名称 | 参数类型 | 说明 |
---|---|---|
page | int | 当前页数 |
pageSize | int | 每页显示的数量 |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": [
{
"id": 1,
"title": "springboot介绍以及入门案例",
"summary": "通过Spring Boot实现的服务,只需要依靠一个Java类,把它打包成jar,并通过`java -jar`命令就可以运行起来。\r\n\r\n这一切相较于传统Spring应用来说,已经变得非常的轻便、简单。",
"commentCounts": 2,
"viewCounts": 54,
"weight": 1,
"createDate": "2609-06-26 15:58",
"author": "12",
"body": null,
"tags": [
{
"id": 5,
"avatar": null,
"tagName": "444"
},
{
"id": 7,
"avatar": null,
"tagName": "22"
},
{
"id": 8,
"avatar": null,
"tagName": "11"
}
],
"categorys": null
},
{
"id": 9,
"title": "Vue.js 是什么",
"summary": "Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。",
"commentCounts": 0,
"viewCounts": 3,
"weight": 0,
"createDate": "2609-06-27 11:25",
"author": "12",
"body": null,
"tags": [
{
"id": 7,
"avatar": null,
"tagName": "22"
}
],
"categorys": null
},
{
"id": 10,
"title": "Element相关",
"summary": "本节将介绍如何在项目中使用 Element。",
"commentCounts": 0,
"viewCounts": 3,
"weight": 0,
"createDate": "2609-06-27 11:25",
"author": "12",
"body": null,
"tags": [
{
"id": 5,
"avatar": null,
"tagName": "444"
},
{
"id": 6,
"avatar": null,
"tagName": "33"
},
{
"id": 7,
"avatar": null,
"tagName": "22"
},
{
"id": 8,
"avatar": null,
"tagName": "11"
}
],
"categorys": null
}
]
}
2.2 编码
2.2.1 表结构
CREATE TABLE `blog`.`ms_article` (
`id` bigint(0) NOT NULL AUTO_INCREMENT,
`comment_counts` int(0) NULL DEFAULT NULL COMMENT '评论数量',
`create_date` bigint(0) NULL DEFAULT NULL COMMENT '创建时间',
`summary` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '简介',
`title` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '标题',
`view_counts` int(0) NULL DEFAULT NULL COMMENT '浏览数量',
`weight` int(0) NOT NULL COMMENT '是否置顶',
`author_id` bigint(0) NULL DEFAULT NULL COMMENT '作者id',
`body_id` bigint(0) NULL DEFAULT NULL COMMENT '内容id',
`category_id` int(0) NULL DEFAULT NULL COMMENT '类别id',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 25 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
CREATE TABLE `blog`.`ms_tag` (
`id` bigint(0) NOT NULL AUTO_INCREMENT,
`article_id` bigint(0) NOT NULL,
`tag_id` bigint(0) NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `article_id`(`article_id`) USING BTREE,
INDEX `tag_id`(`tag_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
CREATE TABLE `blog`.`ms_sys_user` (
`id` bigint(0) NOT NULL AUTO_INCREMENT,
`account` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '账号',
`admin` bit(1) NULL DEFAULT NULL COMMENT '是否管理员',
`avatar` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '头像',
`create_date` bigint(0) NULL DEFAULT NULL COMMENT '注册时间',
`deleted` bit(1) NULL DEFAULT NULL COMMENT '是否删除',
`email` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邮箱',
`last_login` bigint(0) NULL DEFAULT NULL COMMENT '最后登录时间',
`mobile_phone_number` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '手机号',
`nickname` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '昵称',
`password` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '密码',
`salt` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '加密盐',
`status` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '状态',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
package com.hmx.blog.dao.pojo;
import lombok.Data;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-09 16:22
**/
@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;
/**
* 作者id
*/
private Long authorId;
/**
* 内容id
*/
private Long bodyId;
/**
*类别id
*/
private Long categoryId;
/**
* 置顶
*/
private Integer weight = Article_Common;
/**
* 创建时间
*/
private Long createDate;
}
package com.hmx.blog.dao.pojo;
import lombok.Data;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-09 16:34
**/
@Data
public class SysUser {
//@TableId(type = IdType.ASSIGN_ID) // 默认id类型
//以后 用户多了以后,要进行分表操作,id就需要分布式id了
//@TableId(type = IdType.AUTO) 数据库自增
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.hmx.blog.dao.pojo;
import lombok.Data;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-09 16:35
**/
@Data
public class Tag {
private Long id;
private String avatar;
private String tagName;
}
2.2.2 Controller
package com.hmx.blog.controller;
import com.hmx.blog.service.ArticleService;
import com.hmx.blog.vo.Result;
import com.hmx.blog.vo.params.PageParams;
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("/article")
public class ArticleController {
@Autowired
private ArticleService articleService;
/**
* 首页 文章列表
* @param pageParams
* @return
*/
@PostMapping
public Result listArticle(@RequestBody PageParams pageParams){
return articleService.listArticle(pageParams);
}
}
package com.hmx.blog.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);
}
}
package com.hmx.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<TagVo> tags;
// private List<CategoryVo> categorys;
}
2.2.3 Service
package com.hmx.blog.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmx.blog.dao.mapper.ArticleMapper;
import com.hmx.blog.dao.pojo.Article;
import com.hmx.blog.vo.ArticleVo;
import com.hmx.blog.vo.Result;
import com.hmx.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;
@Override
public Result listArticle(PageParams pageParams) {
//1 分页查询 article数据库表
Page<Article> page = new Page<>(pageParams.getPage(), pageParams.getPageSize());
LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
//是否按置顶进行排序weight字段
//order by create_date desc
queryWrapper.orderByDesc(Article::getWeight,Article::getCreateDate);
Page<Article> articlePage = articleMapper.selectPage(page, queryWrapper);
List<Article> records = articlePage.getRecords();
//能直接返回吗? 很明显不能
List<ArticleVo> articleVoList = copyList(records);
return Result.success(articleVoList);
}
private List<ArticleVo> copyList(List<Article> records){
List<ArticleVo> articleVoList = new ArrayList<>();
for (Article record : records) {
articleVoList.add(copy(record));
}
return articleVoList;
}
private ArticleVo copy(Article article){
ArticleVo articleVo = new ArticleVo();
BeanUtils.copyProperties(article,articleVo);
articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));
return articleVo;
}
}
package com.hmx.blog.service;
import com.hmx.blog.dao.pojo.SysUser;
public interface SysUserService {
SysUser findUserById(Long id);
}
package com.hmx.blog.service.impl;
import com.hmx.blog.dao.mapper.SysUserMapper;
import com.hmx.blog.dao.pojo.SysUser;
import com.hmx.blog.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class SysUserServiceImpl implements SysUserService {
@Autowired
private SysUserMapper sysUserMapper;
@Override
public SysUser findUserById(Long id) {
SysUser sysUser = sysUserMapper.selectById(id);
if (sysUser == null) {
sysUser = new SysUser();
sysUser.setNickname("hmx");
}
return sysUser;
}
}
package com.hmx.blog.service;
import com.hmx.blog.dao.mapper.TagMapper;
import com.hmx.blog.vo.TagVo;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
public interface TagService {
List<TagVo> findTagsByArticleId(Long articleId);
}
package com.hmx.blog.service.impl;
import com.hmx.blog.dao.mapper.TagMapper;
import com.hmx.blog.dao.pojo.Article;
import com.hmx.blog.dao.pojo.Tag;
import com.hmx.blog.service.TagService;
import com.hmx.blog.vo.ArticleVo;
import com.hmx.blog.vo.TagVo;
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 TagServiceImpl implements TagService {
@Autowired
private TagMapper tagMapper;
@Override
public List<TagVo> findTagsByArticleId(Long articleId) {
// mybatisplus 无法进行多表查询
List<Tag> tags = tagMapper.findTagsByArticleId(articleId);
return copyList(tags);
}
private List<TagVo> copyList(List<Tag> records){
List<TagVo> 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;
}
}
2.2.4 Dao
package com.hmx.blog.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hmx.blog.dao.pojo.Article;
public interface ArticleMapper extends BaseMapper<Article> {
}
package com.hmx.blog.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hmx.blog.dao.pojo.Tag;
public interface TagMapper extends BaseMapper<Tag> {
List<Tag> findTagsByArticleId(Long articleId);
}
package com.hmx.blog.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hmx.blog.dao.pojo.SysUser;
public interface SysUserMapper extends BaseMapper<SysUser> {
}
2.2.5 mapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hmx.blog.dao.mapper.TagMapper">
<select id="findTagsByArticleId" parameterType="long" resultType="com.hmx.blog.dao.pojo.Tag">
select id,avatar,tag_name as tagName from ms_tag
where id in
(select tag_id from ms_article_tag where article_id=#{articleId})
</select>
</mapper>
2.2.6 vo
package com.hmx.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<TagVo> tags;
// private List<CategoryVo> categorys;
}
package com.hmx.blog.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);
}
}
package com.hmx.blog.vo;
import lombok.Data;
@Data
public class TagVo {
private Long id;
private String tagName;
}
package com.hmx.blog.vo.params;
import lombok.Data;
@Data
public class PageParams {
private int page = 1;
private int pageSize = 10;
}
2.2.7 config
package com.hmx.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.hmx.blog.dao.mapper")
public class MybatisPlusConfig {
//分页插件
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}
package com.hmx.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("/**").allowedOrigins("http://localhost:8080");
}
}
2.2.8 测试
3.首页-最热标签
3.1 接口说明
接口url:/tags/hot
请求方式:GET
请求参数:无
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": [
{
"id":1,
"tagName":"4444"
}
]
}
3.2 编码
3.2.1 Controller
package com.hmx.blog.controller;
import com.hmx.blog.service.TagService;
import com.hmx.blog.vo.Result;
import com.hmx.blog.vo.TagVo;
import org.springframework.beans.factory.annotation.Autowired;
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 TagController {
@Autowired
private TagService tagService;
@GetMapping("/hot")
public Result getHotTags(){
int limit = 4;
return tagService.getHostTags(limit);
}
}
3.2.2 Service
package com.hmx.blog.service.impl;
import com.hmx.blog.dao.mapper.TagMapper;
import com.hmx.blog.dao.pojo.Tag;
import com.hmx.blog.service.TagService;
import com.hmx.blog.vo.Result;
import com.hmx.blog.vo.TagVo;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@Service
public class TagServiceImpl implements TagService {
@Autowired
private TagMapper tagMapper;
@Override
public List<TagVo> findTagsByArticleId(Long articleId) {
// mybatisplus 无法进行多表查询
List<Tag> tags = tagMapper.findTagsByArticleId(articleId);
return copyList(tags);
}
@Override
public Result getHostTags(int i) {
List<Long> ids = tagMapper.findHotTagIds(i);
if (CollectionUtils.isEmpty(ids)) {
return Result.success(Collections.emptyList());
}
List<Tag> tags = tagMapper.findTagsByTagIds(ids);
System.out.println("tags = " + tags);
return Result.success(tags);
}
private List<TagVo> copyList(List<Tag> records){
List<TagVo> 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;
}
}
3.2.3 Dao
package com.hmx.blog.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hmx.blog.dao.pojo.Tag;
import java.util.List;
public interface TagMapper extends BaseMapper<Tag> {
List<Tag> findTagsByArticleId(Long articleId);
List<Long> findHotTagIds(int size);
List<Tag> findTagsByTagIds(List<Long> tagIds);
}
3.2.4 mapper.xml
TagMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hmx.blog.dao.mapper.TagMapper">
<select id="findTagsByArticleId" parameterType="long" resultType="com.hmx.blog.dao.pojo.Tag">
select id,avatar,tag_name as tagName from ms_tag
where id in
(select tag_id from ms_article_tag where article_id=#{articleId})
</select>
<select id="findTagsByTagIds" resultType="com.hmx.blog.dao.pojo.Tag">
select id,tag_name as tagName from ms_tag
where id in
<foreach collection="tagIds" open="(" close=")" item="tagId" separator=",">
#{tagId}
</foreach>
</select>
<select id="findHotTagIds" resultType="java.lang.Long">
select tag_id from ms_article_tag group by tag_id order by count(1) desc
limit #{size}
</select>
</mapper>
3.2.5 测试
4 统一异常处理
不管是controller层还是service,dao层,都有可能报异常,如果是预料中的异常,可以直接捕获处理,如果是意料之外的异常,需要统一进行处理,进行记录,并给用户提示相对比较友好的信息。
package com.hmx.blog.handler;
import com.hmx.blog.vo.Result;
import org.springframework.stereotype.Controller;
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的异常
* @param ex
* @return
*/
@ExceptionHandler(Exception.class)
@ResponseBody
public Result doException(Exception ex){
ex.printStackTrace();
return Result.fail(-999,"系统异常");
}
}
4.1 测试
4.1.1 在controller层的方法中添加一个异常
4.1.2 测试结果
5 首页-最热文章
5.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相关",
}
]
}
5.2 Controller
/**
* 首页 最热文章
* @param
* @return
*/
@PostMapping("hot")
public Result hotArticle(){
int limit = 5;
return articleService.hotArticle(limit);
}
5.3 Service
@Override
public Result hotArticle(int limit) {
LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.orderByDesc(Article::getViewCounts);
queryWrapper.select(Article::getId,Article::getTitle);
queryWrapper.last("limit " + limit);
List<Article> articles = articleMapper.selectList(queryWrapper);
System.out.println("articles = " + articles);
return Result.success(copyList(articles,false,false));
}
5.4 测试
6 首页-最新文章
6.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相关",
}
]
}
6.1 Controller
/**
* 首页 最新文章
* @return
*/
@PostMapping("new")
public Result newArticle(){
int limit = 5;
return articleService.newArticle(limit);
}
6.2 Service
@Override
public Result newArticle(int limit) {
LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.orderByDesc(Article::getCreateDate);
queryWrapper.select(Article::getId,Article::getTitle);
queryWrapper.last("limit " + limit);
List<Article> articles = articleMapper.selectList(queryWrapper);
return Result.success(copyList(articles,false,false));
}
6.3 测试
7 首页-文章归档
7.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
7.2 Controller
/**
* 首页-文章归档
* @return
*/
@PostMapping("listArchives")
public Result listArchives(){
return articleService.listArchives();
}
7.3 Service
@Override
public Result listArchives() {
List<Archive> archives = articleMapper.listArchives();
System.out.println(archives);
return Result.success(archives);
}
7.4 Dao
DO(Domain Object) 领域对象
- 从现实世界中抽象出来的有形或无形的业务实体。
package com.hmx.blog.dao.dos;
import lombok.Data;
@Data
public class Archive {
private Integer year;
private Integer month;
private Integer count;
}
7.5 mapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hmx.blog.dao.mapper.ArticleMapper">
<select id="listArchives" resultType="com.hmx.blog.dao.dos.Archive">
SELECT YEAR(FROM_UNIXTIME(create_date/1000)) AS YEAR,MONTH(FROM_UNIXTIME(create_date/1000)) AS MONTH,COUNT(*) AS COUNT
FROM ms_article
GROUP BY YEAR,MONTH
</select>
</mapper>
SELECT YEAR(FROM_UNIXTIME(create_date/1000)) AS YEAR,MONTH(FROM_UNIXTIME(create_date/1000)) AS MONTH,COUNT() AS COUNT
FROM ms_article
GROUP BY YEAR,MONTH
这里原up主使用的是
select year(create_date) as year,month(create_date) as month,count() as count from ms_article group by year,month
不知道为啥用不了,就改成上面那个sql语句了,但我不明白为啥使用下面的语句会报语法错误,暂时不去探究
7.6 测试
8 登录
当用户登陆后再次访问登录页面,会检查用户的token是否正确,正确就直接方形。
8.1 接口说明
接口url:/login
请求方式:POST
请求参数:
参数名称 | 参数类型 | 说明 |
---|---|---|
account | string | 账号 |
password | string | 密码 |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": "token"
}
8.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部分 是否合法。
依赖包:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
md5加密的依赖包:
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
工具类:
package com.hmx.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 {
//即JWT第三部分的秘钥secret
private static final String JWT_TOKEN = "Hmx123456P@ssw0rdMszlu!@#$";
public static String createToken(Long userId){
//第二部分(playload载荷)(进行一些声明)
Map<String,Object> claims = new HashMap<>();
claims.put("userId",userId);
JwtBuilder jwtBuilder = Jwts.builder()
//第一部分Header
// 签发算法,秘钥为jwtToken
.signWith(SignatureAlgorithm.HS256, JWT_TOKEN)
// body数据,要唯一,自行设置
.setClaims(claims)
// 设置签发时间
.setIssuedAt(new Date())
// 一天的有效时间
.setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000));
String token = jwtBuilder.compact();
return token;
}
public static Map<String, Object> checkToken(String token){
try {
Jwt parse = Jwts.parser().setSigningKey(JWT_TOKEN).parse(token);
return (Map<String, Object>) parse.getBody();
}catch (Exception e){
e.printStackTrace();
}
return null;
}
/*public static void main(String[] args) {
String token = JWTUtils.createToken(11L);
System.out.println(token);
Map<String, Object> stringObjectMap = JWTUtils.checkToken(token);
System.out.println("userId = " + stringObjectMap.get("userId"));
}*/
}
8.3 Controller
package com.hmx.blog.controller;
import com.hmx.blog.service.LoginService;
import com.hmx.blog.vo.Result;
import com.hmx.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);
}
}
8.4 Service
package com.hmx.blog.service.impl;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.hmx.blog.dao.mapper.SysUserMapper;
import com.hmx.blog.dao.pojo.SysUser;
import com.hmx.blog.service.LoginService;
import com.hmx.blog.service.SysUserService;
import com.hmx.blog.utils.JWTUtils;
import com.hmx.blog.vo.ErrorCode;
import com.hmx.blog.vo.Result;
import com.hmx.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;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-13 17:38
**/
@Service
public class LoginServiceImpl implements LoginService {
private static final String salt = "hmx123P@ssw0rd";
@Autowired
private SysUserService sysUserService;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private SysUserMapper sysUserMapper;
@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 + salt);
SysUser sysUser = sysUserService.findUser(account,pwd);
System.out.println("sysUser = " + sysUser);
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("hmx" + salt));
}
}
package com.hmx.blog.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.hmx.blog.dao.mapper.SysUserMapper;
import com.hmx.blog.dao.pojo.SysUser;
import com.hmx.blog.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-10 17:06
**/
@Service
public class SysUserServiceImpl implements SysUserService {
@Autowired
private SysUserMapper sysUserMapper;
@Override
public SysUser findUserById(Long id) {
SysUser sysUser = sysUserMapper.selectById(id);
if (sysUser == null) {
sysUser = new SysUser();
sysUser.setNickname("hmx");
}
return sysUser;
}
@Override
public SysUser findUser(String account, String pwd) {
LambdaQueryWrapper<SysUser> 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;
}
}
8.5 登录参数,redis配置,统一错误码
package com.hmx.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.hmx.blog.vo;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-13 17:58
**/
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;
}
}
8.6 测试
使用postman测试,因为登录后,需要跳转页面,进行token认证,有接口未写,前端会出现问题。
token前端获取到之后,会存储在 storage中 h5 ,本地存储
9 获取用户信息
9.1 接口说明
接口url:/users/currentUser
请求方式:GET
请求参数:
参数名称 | 参数类型 | 说明 |
---|---|---|
Authorization | string | 头部信息(TOKEN) |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": {
"id":1,
"account":"1",
"nickaname":"1",
"avatar":"ss"
}
}
9.2 Controlller
package com.hmx.blog.controller;
import com.hmx.blog.service.SysUserService;
import com.hmx.blog.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("users")
public class UserController {
@Autowired
private SysUserService sysUserService;
@GetMapping("currentUser")
public Result currentUser(@RequestHeader("Authorization") String token){
return sysUserService.findUserByToken(token);
}
}
9.3 Service
package com.hmx.blog.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.hmx.blog.dao.mapper.SysUserMapper;
import com.hmx.blog.dao.pojo.SysUser;
import com.hmx.blog.service.LoginService;
import com.hmx.blog.service.SysUserService;
import com.hmx.blog.vo.LoginUserVo;
import com.hmx.blog.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class SysUserServiceImpl implements SysUserService {
@Autowired
private SysUserMapper sysUserMapper;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private LoginService loginService;
@Override
public SysUser findUserById(Long id) {
SysUser sysUser = sysUserMapper.selectById(id);
if (sysUser == null) {
sysUser = new SysUser();
sysUser.setNickname("hmx");
}
return sysUser;
}
@Override
public SysUser findUser(String account, String pwd) {
LambdaQueryWrapper<SysUser> 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;
}
@Override
public Result findUserByToken(String token) {
/**
* 1. token合法性校验
* 是否为空,解析是否成功 redis是否存在
* 2. 如果校验失败 返回错误
* 3. 如果成功,返回对应的结果 LoginUserVo
*/
SysUser sysUser = loginService.checkToken(token);
LoginUserVo loginUserVo = new LoginUserVo();
loginUserVo.setAccount(sysUser.getAccount());
loginUserVo.setAvatar(sysUser.getAvatar());
loginUserVo.setId(sysUser.getId());
loginUserVo.setNickname(sysUser.getNickname());
return Result.success(loginUserVo);
}
}
package com.hmx.blog.service.impl;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.hmx.blog.dao.mapper.SysUserMapper;
import com.hmx.blog.dao.pojo.SysUser;
import com.hmx.blog.service.LoginService;
import com.hmx.blog.service.SysUserService;
import com.hmx.blog.utils.JWTUtils;
import com.hmx.blog.vo.ErrorCode;
import com.hmx.blog.vo.Result;
import com.hmx.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.Map;
import java.util.concurrent.TimeUnit;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-13 17:38
**/
@Service
public class LoginServiceImpl implements LoginService {
private static final String salt = "hmx123P@ssw0rd";
@Autowired
private SysUserService sysUserService;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private SysUserMapper sysUserMapper;
@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 + salt);
SysUser sysUser = sysUserService.findUser(account,pwd);
System.out.println("sysUser = " + sysUser);
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);
}
@Override
public SysUser checkToken(String token) {
if (StringUtils.isBlank(token)) {
return null;
}
Map<String, Object> map = JWTUtils.checkToken(token);
if (map == null) {
return null;
}
String userJson = redisTemplate.opsForValue().get("TOKEN_" + token).toString();
if ((StringUtils.isBlank(userJson))) {
return null;
}
SysUser sysUser = JSON.parseObject(userJson, SysUser.class);
return sysUser;
}
public static void main(String[] args) {
System.out.println(DigestUtils.md5Hex("hmx" + salt));
}
}
9.4 vo
package com.hmx.blog.vo;
import lombok.Data;
@Data
public class LoginUserVo {
private Long id;
private String account;
private String nickname;
private String avatar;
}
9.5 测试
若要清除header中的token,点击application下的storage下的clear site data按钮
10 退出登录
10.1 接口说明
接口url:/logout
请求方式:GET
请求参数:
参数名称 | 参数类型 | 说明 |
---|---|---|
Authorization | string | 头部信息(TOKEN) |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": null
}
10.2 controller
package com.hmx.blog.controller;
import com.hmx.blog.service.LoginService;
import com.hmx.blog.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("logout")
public class LoginOutController {
@Autowired
private LoginService loginService;
@GetMapping
public Result loginOut(@RequestHeader("Authorization") String token){
return loginService.loginOut(token);
}
}
10.3 service
package com.hmx.blog.service.impl;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.hmx.blog.dao.mapper.SysUserMapper;
import com.hmx.blog.dao.pojo.SysUser;
import com.hmx.blog.service.LoginService;
import com.hmx.blog.service.SysUserService;
import com.hmx.blog.utils.JWTUtils;
import com.hmx.blog.vo.ErrorCode;
import com.hmx.blog.vo.Result;
import com.hmx.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.Map;
import java.util.concurrent.TimeUnit;
@Service
public class LoginServiceImpl implements LoginService {
private static final String salt = "hmx123P@ssw0rd";
@Autowired
private SysUserService sysUserService;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private SysUserMapper sysUserMapper;
@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 + salt);
SysUser sysUser = sysUserService.findUser(account,pwd);
System.out.println("sysUser = " + sysUser);
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);
}
@Override
public SysUser checkToken(String token) {
if (StringUtils.isBlank(token)) {
return null;
}
Map<String, Object> map = JWTUtils.checkToken(token);
if (map == null) {
return null;
}
String userJson = redisTemplate.opsForValue().get("TOKEN_" + token).toString();
if ((StringUtils.isBlank(userJson))) {
return null;
}
SysUser sysUser = JSON.parseObject(userJson, SysUser.class);
return sysUser;
}
@Override
public Result loginOut(String token) {
redisTemplate.delete("TOKEN_" + token);
return Result.success(null);
}
public static void main(String[] args) {
System.out.println(DigestUtils.md5Hex("hmx" + salt));
}
}
10.4 测试
11 注册
11.1 接口说明
接口url:/register
请求方式:POST
请求参数:
参数名称 | 参数类型 | 说明 |
---|---|---|
account | string | 账号 |
password | string | 密码 |
nickname | string | 昵称 |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": "token"
}
11.2 Controller
package com.hmx.blog.controller;
import com.hmx.blog.service.LoginService;
import com.hmx.blog.vo.Result;
import com.hmx.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。
private String nickname;
11.3 Service
package com.hmx.blog.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.hmx.blog.dao.mapper.SysUserMapper;
import com.hmx.blog.dao.pojo.SysUser;
import com.hmx.blog.service.LoginService;
import com.hmx.blog.service.SysUserService;
import com.hmx.blog.vo.LoginUserVo;
import com.hmx.blog.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class SysUserServiceImpl implements SysUserService {
@Autowired
private SysUserMapper sysUserMapper;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private LoginService loginService;
@Override
public SysUser findUserById(Long id) {
SysUser sysUser = sysUserMapper.selectById(id);
if (sysUser == null) {
sysUser = new SysUser();
sysUser.setNickname("hmx");
}
return sysUser;
}
@Override
public SysUser findUser(String account, String pwd) {
LambdaQueryWrapper<SysUser> 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;
}
@Override
public Result findUserByToken(String token) {
/**
* 1. token合法性校验
* 是否为空,解析是否成功 redis是否存在
* 2. 如果校验失败 返回错误
* 3. 如果成功,返回对应的结果 LoginUserVo
*/
SysUser sysUser = loginService.checkToken(token);
LoginUserVo loginUserVo = new LoginUserVo();
loginUserVo.setAccount(sysUser.getAccount());
loginUserVo.setAvatar(sysUser.getAvatar());
loginUserVo.setId(sysUser.getId());
loginUserVo.setNickname(sysUser.getNickname());
return Result.success(loginUserVo);
}
@Override
public SysUser findUserByAccount(String account) {
LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(SysUser::getAccount,account);
queryWrapper.last("limit 1");
return this.sysUserMapper.selectOne(queryWrapper);
}
@Override
public void save(SysUser sysUser) {
/**
* 保存这个用户 id 会自动生成
* 这个地方 默认生成的id 是 分布式id 雪花算法
* mybatis-plus
*/
this.sysUserMapper.insert(sysUser);
}
}
ACCOUNT_EXIST(10004,"账号已存在"),
package com.hmx.blog.service.impl;
import com.alibaba.fastjson.JSON;
import com.hmx.blog.dao.mapper.SysUserMapper;
import com.hmx.blog.dao.pojo.SysUser;
import com.hmx.blog.service.LoginService;
import com.hmx.blog.service.SysUserService;
import com.hmx.blog.utils.JWTUtils;
import com.hmx.blog.vo.ErrorCode;
import com.hmx.blog.vo.Result;
import com.hmx.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 org.springframework.transaction.annotation.Transactional;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-13 17:38
**/
@Service
@Transactional
public class LoginServiceImpl implements LoginService {
private static final String SALT = "hmx123P@ssw0rd";
@Autowired
private SysUserService sysUserService;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private SysUserMapper sysUserMapper;
@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 + SALT);
SysUser sysUser = sysUserService.findUser(account,pwd);
System.out.println("sysUser = " + sysUser);
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);
}
@Override
public SysUser checkToken(String token) {
if (StringUtils.isBlank(token)) {
return null;
}
Map<String, Object> map = JWTUtils.checkToken(token);
if (map == null) {
return null;
}
String userJson = redisTemplate.opsForValue().get("TOKEN_" + token).toString();
if ((StringUtils.isBlank(userJson))) {
return null;
}
SysUser sysUser = JSON.parseObject(userJson, SysUser.class);
return sysUser;
}
@Override
public Result loginOut(String token) {
redisTemplate.delete("TOKEN_" + token);
return Result.success(null);
}
@Override
public Result register(LoginParam loginParam) {
String account = loginParam.getAccount();
String password = loginParam.getPassword();
String nickname = loginParam.getNickname();
//1. 判断参数 是否合法
if (StringUtils.isBlank(account) || StringUtils.isBlank(password) || StringUtils.isBlank(nickname)) {
return Result.fail(ErrorCode.PARAMS_ERROR.getCode(),ErrorCode.PARAMS_ERROR.getMsg());
}
//2. 判断账户是否存在,存在 返回账户一杯注册
SysUser sysUser = this.sysUserService.findUserByAccount(account);
if (sysUser != null) {
return Result.fail(ErrorCode.ACCOUNT_EXIST.getCode(),ErrorCode.ACCOUNT_EXIST.getMsg());
}
//3. 不存在,注册用户
sysUser = new SysUser();
sysUser.setNickname(nickname);
sysUser.setAccount(account);
sysUser.setPassword(DigestUtils.md5Hex(password + SALT));
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("hmx123P@ssw0rd");
sysUser.setStatus("");
sysUser.setEmail("");
this.sysUserService.save(sysUser);
//生成token
String token = JWTUtils.createToken(sysUser.getId());
//存入redis并返回
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("hmx" + SALT));
}
}
11.4 添加事务
@Service
@Transactional
public class LoginServiceImpl implements LoginService {}
当然 一般建议加在 接口上,通用一些。
测试的时候 可以将redis 停掉,那么redis连接异常后,新添加的用户 应该执行回滚操作。
11.5 测试
12 登录拦截器
每次访问需要登录的资源的时候,都需要在代码中进行判断,一旦登录的逻辑有所改变,代码都得进行变动,非常不合适。
那么可不可以统一进行登录判断呢?
可以,使用拦截器,进行登录拦截,如果遇到需要登录才能访问的接口,如果未登录,拦截器直接返回,并跳转登录页面。
12.1 拦截器实现
package com.hmx.blog.handler;
import com.alibaba.fastjson.JSON;
import com.hmx.blog.dao.pojo.SysUser;
import com.hmx.blog.service.LoginService;
import com.hmx.blog.utils.UserThreadLocal;
import com.hmx.blog.vo.ErrorCode;
import com.hmx.blog.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private LoginService loginService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//在执行controller方法(Handler)之前进行执行
/**
* 1. 需要判断 请求的接口路径 是否为 HandlerMethod (controller方法)
* 2. 判断 token是否为空, 如果为空 未登录
* 3. 如果token 不为空, 登录验证 loginService checkToken
* 4. 如果认证成功 放行即可
*/
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(), ErrorCode.NO_LOGIN.getMsg());
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(), ErrorCode.NO_LOGIN.getMsg());
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();
}
}
12.2 拦截器生效
package com.hmx.blog.config;
import com.hmx.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 WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//拦截test接口,后续遇到实际需要拦截的接口时,再配置为真正的拦截接口
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/test");
}
@Override
public void addCorsMappings(CorsRegistry registry) {
//跨域配置,不可设置为*,不安全,前后端分离项目,可能域名不一致
//本地测试 端口不一致 也算跨域
registry.addMapping("/**").allowedOrigins("http://localhost:8080");
}
}
12.3 测试
package com.hmx.blog.controller;
import com.hmx.blog.dao.pojo.SysUser;
import com.hmx.blog.utils.UserThreadLocal;
import com.hmx.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 = UserThreadLocal.get();
System.out.println("sysUser = " + sysUser);
return Result.success(null);
}
}
13 ThreadLocal保存用户信息
package com.hmx.blog.utils;
import com.hmx.blog.dao.pojo.SysUser;
public class UserThreadLocal {
private UserThreadLocal(){}
//线程变量隔离
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();
}
}
package com.hmx.blog.handler;
import com.alibaba.fastjson.JSON;
import com.hmx.blog.dao.pojo.SysUser;
import com.hmx.blog.service.LoginService;
import com.hmx.blog.utils.UserThreadLocal;
import com.hmx.blog.vo.ErrorCode;
import com.hmx.blog.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
private LoginService loginService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//在执行controller方法(Handler)之前进行执行
/**
* 1. 需要判断 请求的接口路径 是否为 HandlerMethod (controller方法)
* 2. 判断 token是否为空, 如果为空 未登录
* 3. 如果token 不为空, 登录验证 loginService checkToken
* 4. 如果认证成功 放行即可
*/
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(), ErrorCode.NO_LOGIN.getMsg());
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(), ErrorCode.NO_LOGIN.getMsg());
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.hmx.blog.controller;
import com.hmx.blog.dao.pojo.SysUser;
import com.hmx.blog.utils.UserThreadLocal;
import com.hmx.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 = UserThreadLocal.get();
System.out.println("sysUser = " + sysUser);
return Result.success(null);
}
}
14 ThreadLocal内存泄漏
实线代表强引用,虚线代表弱引用
每一个Thread维护一个ThreadLocalMap, key为使用弱引用的ThreadLocal实例,value为线程变量的副本。
强引用,使用最普遍的引用,一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。
弱引用,JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。
15 文章详情
15.1 接口说明
接口url:/articles/view/{id}
请求方式:POST
请求参数:
参数名称 | 参数类型 | 说明 |
---|---|---|
id | long | 文章id(路径参数) |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": "token"
}
15.2 涉及到的表
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.hmx.blog.dao.pojo;
import lombok.Data;
@Data
public class ArticleBody {
private Long id;
private String content;
private String contentHtml;
private Long articleId;
}
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.hmx.blog.dao.pojo;
import lombok.Data;
@Data
public class Category {
private Long id;
private String avatar;
private String categoryName;
private String description;
}
15.3 Controller
@PostMapping("view/{id}")
public Result findArticleById(@PathVariable("id") Long id) {
ArticleVo articleVo = articleService.findArticleById(id);
return Result.success(articleVo);
}
15.4 Service
@Override
public ArticleVo findArticleById(Long id) {
Article article = articleMapper.selectById(id);
return copy(article,true,true,true,true);
}
package com.hmx.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<TagVo> tags;
private CategoryVo category;
}
ArticleVo中的属性填充:
package com.hmx.blog.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmx.blog.dao.dos.Archive;
import com.hmx.blog.dao.mapper.ArticleBodyMapper;
import com.hmx.blog.dao.mapper.ArticleMapper;
import com.hmx.blog.dao.mapper.CategoryMapper;
import com.hmx.blog.dao.pojo.Article;
import com.hmx.blog.dao.pojo.ArticleBody;
import com.hmx.blog.service.ArticleService;
import com.hmx.blog.service.CategoryService;
import com.hmx.blog.service.SysUserService;
import com.hmx.blog.service.TagService;
import com.hmx.blog.vo.ArticleBodyVo;
import com.hmx.blog.vo.ArticleVo;
import com.hmx.blog.vo.Result;
import com.hmx.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 TagService tagService;
@Autowired
private SysUserService sysUserService;
@Autowired
private ArticleBodyMapper articleBodyMapper;
@Autowired
private CategoryMapper categoryMapper;
@Autowired
private CategoryService categoryService;
@Override
public Result listArticle(PageParams pageParams) {
//1 分页查询 article数据库表
Page<Article> page = new Page<>(pageParams.getPage(), pageParams.getPageSize());
LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
//是否按置顶进行排序weight字段
//order by create_date desc
queryWrapper.orderByDesc(Article::getWeight,Article::getCreateDate);
Page<Article> articlePage = articleMapper.selectPage(page, queryWrapper);
List<Article> records = articlePage.getRecords();
//能直接返回吗? 很明显不能
List<ArticleVo> articleVoList = copyList(records,true,true,false,false);
return Result.success(articleVoList);
}
@Override
public Result hotArticle(int limit) {
LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.orderByDesc(Article::getViewCounts);
queryWrapper.select(Article::getId,Article::getTitle);
queryWrapper.last("limit " + limit);
List<Article> articles = articleMapper.selectList(queryWrapper);
return Result.success(copyList(articles,false,false,false,false));
}
@Override
public Result newArticle(int limit) {
LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.orderByDesc(Article::getCreateDate);
queryWrapper.select(Article::getId,Article::getTitle);
queryWrapper.last("limit " + limit);
List<Article> articles = articleMapper.selectList(queryWrapper);
return Result.success(copyList(articles,false,false,false,false));
}
@Override
public Result listArchives() {
List<Archive> archives = articleMapper.listArchives();
System.out.println(archives);
return Result.success(archives);
}
@Override
public Result findArticleById(Long articleId) {
/**
* 1. 根据id查询文章信息
* 2. 根据bodyId和categoryId 去做关联查询
*
*/
ArticleVo articleVo = new ArticleVo();
Article article = this.articleMapper.selectById(articleId);
System.out.println("article = " + article);
articleVo = copy(article, true, true, true, true);
return Result.success(articleVo);
}
private List<ArticleVo> copyList(List<Article> records, boolean isTag,boolean isAuthor,boolean isBody,boolean isCategory){
List<ArticleVo> 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){
Long articleId = article.getId();
articleVo.setTags(tagService.findTagsByArticleId(articleId));
}
if (isAuthor) {
Long uid = article.getAuthorId();
articleVo.setAuthor(sysUserService.findUserById(uid).getNickname());
}
return articleVo;
}
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 uid = article.getAuthorId();
articleVo.setAuthor(sysUserService.findUserById(uid).getNickname());
}
if (isBody) {
Long bodyId = article.getBodyId();
articleVo.setBody(findArticleBodyById(bodyId));
}
if (isCategory) {
Long categoryId = article.getCategoryId();
articleVo.setCategory(categoryService.findCategoryById(categoryId));
}
return articleVo;
}
private ArticleBodyVo findArticleBodyById(Long bodyId) {
ArticleBody articleBody = articleBodyMapper.selectById(bodyId);
ArticleBodyVo articleBodyVo = new ArticleBodyVo();
articleBodyVo.setContent(articleBody.getContent());
return articleBodyVo;
}
}
package com.hmx.blog.vo;
import lombok.Data;
@Data
public class CategoryVo {
private Long id;
private String avatar;
private String categoryName;
}
package com.hmx.blog.vo;
import lombok.Data;
@Data
public class ArticleBodyVo {
private String content;
}
package com.hmx.blog.service.impl;
import com.hmx.blog.dao.mapper.CategoryMapper;
import com.hmx.blog.dao.pojo.Category;
import com.hmx.blog.service.CategoryService;
import com.hmx.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 categoryId) {
Category category = categoryMapper.selectById(categoryId);
CategoryVo categoryVo = new CategoryVo();
BeanUtils.copyProperties(category,categoryVo);
return categoryVo;
}
}
public interface ArticleBodyMapper extends BaseMapper<ArticleBody> {
}
public interface CategoryMapper extends BaseMapper<Category> {
}
15.5 测试
16 使用线程池 更新阅读次数
16.1 线程池配置
package com.hmx.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;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-16 09:16
**/
@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("HMX的博客项目");
//等待所有任务结束后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
//执行初始化
executor.initialize();
return executor
}
}
16.2 使用
package com.hmx.blog.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.hmx.blog.dao.mapper.ArticleMapper;
import com.hmx.blog.dao.pojo.Article;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-16 09:45
**/
@Component
public class ThreadService {
//期望此操作在线程池 执行 不会影响原有的主线程
@Async("taskExecutor")
public void updateArticleViewCount(ArticleMapper articleMapper, Article article) {
Article articleUpdate = new Article();
articleUpdate.setViewCounts(article.getViewCounts() + 1);
LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Article::getId,article.getId());
queryWrapper.eq(Article::getViewCounts, article.getViewCounts());
articleMapper.update(articleUpdate,queryWrapper);
/*try {
//睡眠5秒 证明不会影响主线程的使用
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
}
}
@Autowired
private ThreadService threadService;
@Override
public Result findArticleById(Long articleId) {
/**
* 1. 根据id查询文章信息
* 2. 根据bodyId和categoryId 去做关联查询
*
*/
ArticleVo articleVo = new ArticleVo();
Article article = this.articleMapper.selectById(articleId);
System.out.println("article = " + article);
articleVo = copy(article, true, true, true, true);
//查看完文章了,新增阅读数,有没有问题呢
//查看文章之后,本应该直接返回数据了,这时候做了一个更新操作,更新时加写锁,阻塞其他的读操作,性能就会比较低
//更新 增加了此次接口的 耗时 如果一旦更新出问题, 不能影响 查看文章的操作
//线程池 可以吧更新操作 扔到线程池中去执行,和主线程就相关了
threadService.updateArticleViewCount(articleMapper, article);
return Result.success(articleVo);
}
16.3 测试
睡眠 ThredService中的方法 5秒,不会影响主线程的使用,即文章详情会很快的显示出来,不受影响
17 BUG修正
之前Article中的commentCounts,viewCounts,weight 字段为int,会造成更新阅读次数的时候,将其余两个字段设为初始值0。
因为int 类型有默认值0,使用mybatis-plus时若被更新的对象的值不为Null就会被更新
package com.hmx.blog.dao.pojo;
import lombok.Data;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-09 16:22
**/
@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;
/**
* 作者id
*/
private Long authorId;
/**
* 内容id
*/
private Long bodyId;
/**
*类别id
*/
private Long categoryId;
/**
* 置顶
*/
private Integer weight = Article_Common;
/**
* 创建时间
*/
private Long createDate;
}
18 评论列表
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.hmx.blog.dao.pojo;
import lombok.Data;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-16 22:42
**/
@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;
}
18.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
}
]
}
18.2 Controller
package com.hmx.blog.controller;
import com.hmx.blog.service.CommentsService;
import com.hmx.blog.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-16 22:44
**/
@RestController
@RequestMapping("comments")
public class CommentsController {
@Autowired
private CommentsService commentsService;
@PostMapping("article/{id}")
public Result comments(@PathVariable("id") Long id){
return commentsService.commentsByArticleId(id);
}
}
18.3 Service
在SysUserService中提供 查询用户信息的服务:
@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;
}
package com.hmx.blog.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.hmx.blog.dao.mapper.CommentMapper;
import com.hmx.blog.dao.pojo.Article;
import com.hmx.blog.dao.pojo.Comment;
import com.hmx.blog.service.CommentsService;
import com.hmx.blog.service.SysUserService;
import com.hmx.blog.vo.CommentVo;
import com.hmx.blog.vo.Result;
import com.hmx.blog.vo.UserVo;
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;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-16 22:47
**/
@Service
public class CommentsServiceImpl implements CommentsService {
@Autowired
private CommentMapper commentMapper;
@Autowired
private SysUserService sysUserService;
@Override
public Result commentsByArticleId(Long id) {
/**
* 1. 根据文章id 查询 评论列表 从 comment 表中查询
* 2. 根据作者id 查询作者的信息
* 3. 判断 如果 level = 1 要去查询它有没有子评论
* 4. 如果有 根据评论id 进行查询 (parent_id)
*/
LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Comment::getArticleId,id);
queryWrapper.eq(Comment::getLevel,1);
List<Comment> comments = commentMapper.selectList(queryWrapper);
List<CommentVo> commentVoList = copyList(comments);
return Result.success(commentVoList);
}
private List<CommentVo> copyList(List<Comment> comments) {
List<CommentVo> commentVoList = new ArrayList<>();;
for (Comment comment : comments) {
commentVoList.add(copy(comment));
}
return commentVoList;
}
private CommentVo copy(Comment comment) {
CommentVo commentVo = new CommentVo();
BeanUtils.copyProperties(comment,commentVo);
//作者信息
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<CommentVo> commentVoList = findCommentsByParenetId(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<CommentVo> findCommentsByParenetId(Long id) {
LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Comment::getParentId,id);
queryWrapper.eq(Comment::getLevel, 2);
return copyList(commentMapper.selectList(queryWrapper));
}
}
18.4 vo
package com.hmx.blog.vo;
import lombok.Data;
import java.util.List;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-16 22:59
**/
@Data
public class CommentVo {
private Long id;
private UserVo author;
private String content;
private List<CommentVo> childrens;
private String createDate;
private Integer level;
private UserVo toUser;
}
package com.hmx.blog.vo;
import lombok.Data;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-16 22:59
**/
@Data
public class UserVo {
private String nickname;
private String avatar;
private Long id;
}
18.5 dao
package com.hmx.blog.dao.pojo;
import lombok.Data;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-16 22:42
**/
@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;
}
18.6 测试
19 评论
19.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
}
19.2 加入到登录拦截器中
@Override
public void addInterceptors(InterceptorRegistry registry) {
//拦截test接口,后续遇到实际需要拦截的接口时,再配置为真正的拦截接口
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/test").addPathPatterns("/comments/create/change");
}
19.3 Controller
构建评论参数对象:
package com.hmx.blog.vo.params;
import lombok.Data;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-17 08:57
**/
@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);
}
19.4 Service
//防止前端 精度损失 把 id转为string
//分布式id 比较长,传到前端 会有精度损失,必须转为string类型 进行传输,就不会有问题了
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
@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);
}
19.5 测试
20 写文章
写文章需要 三个接口:
- 获取所有文章类别
- 获取所有标签
- 发布文章
20.1 所有文章分类
20.1.1 接口说明
接口url:/categorys
请求方式:GET
请求参数:
参数名称 | 参数类型 | 说明 |
---|
返回数据:
{
"success":true,
"code":200,
"msg":"success",
"data":
[
{"id":1,"avatar":"/category/front.png","categoryName":"前端"},
{"id":2,"avatar":"/category/back.png","categoryName":"后端"},
{"id":3,"avatar":"/category/lift.jpg","categoryName":"生活"},
{"id":4,"avatar":"/category/database.png","categoryName":"数据库"},
{"id":5,"avatar":"/category/language.png","categoryName":"编程语言"}
]
}
20.1.2 Controller
package com.hmx.blog.controller;
import com.hmx.blog.service.CategoryService;
import com.hmx.blog.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-17 13:56
**/
@RestController
@RequestMapping("categorys")
public class CategoryController {
@Autowired
private CategoryService categoryService;
//categorys
@GetMapping
public Result categories(){
return categoryService.findAll();
}
}
20.1.3 Service
@Override
public Result findAll() {
List<Category> categories = categoryMapper.selectList(new LambdaQueryWrapper<>());
//页面交互的对象
return Result.success(copyList(categories));
}
public List<CategoryVo> copyList(List<Category> categoryList) {
List<CategoryVo> categoryVoList = new ArrayList<>();
for (Category category : categoryList) {
categoryVoList.add(copy(category));
}
return categoryVoList;
}
public CategoryVo copy(Category category) {
CategoryVo categoryVo = new CategoryVo();
BeanUtils.copyProperties(category,categoryVo);
return categoryVo;
}
21. 所有文章标签
21.1 接口说明
接口url:/tags
请求方式:GET
请求参数:
参数名称 | 参数类型 | 说明 |
---|
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": [
{
"id": 5,
"tagName": "springboot"
},
{
"id": 6,
"tagName": "spring"
},
{
"id": 7,
"tagName": "springmvc"
},
{
"id": 8,
"tagName": "11"
}
]
}
21.2 Controller
@GetMapping
public Result findAll(){
return tagService.findAll();
}
21.3 Service
@Override
public Result findAll() {
List<Tag> tags = this.tagMapper.selectList(new LambdaQueryWrapper<>());
return Result.success(copyList(tags));
}
22. 发布文章
22.1 接口说明
接口url:/articles/publish
请求方式:POST
请求参数:
参数名称 | 参数类型 | 说明 |
---|---|---|
title | string | 文章标题 |
id | long | 文章id(编辑有值) |
body | object({content: “ww”, contentHtml: “ ww ↵”}) | 文章内容 |
category | {id: 2, avatar: “/category/back.png”, categoryName: “后端”} | 文章类别 |
summary | string | 文章概述 |
tags | [{id: 5}, {id: 6}] | 文章标签 |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": {"id":12232323}
}
22.2 Controller
package com.hmx.blog.vo.params;
import com.hmx.blog.vo.CategoryVo;
import com.hmx.blog.vo.TagVo;
import lombok.Data;
import java.util.List;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-17 14:26
**/
@Data
public class ArticleParam {
private Long id;
private ArticleBodyParam body;
private CategoryVo category;
private String summary;
private List<TagVo> tags;
private String title;
}
package com.hmx.blog.vo.params;
import lombok.Data;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-17 14:27
**/
@Data
public class ArticleBodyParam {
private String content;
private String contentHtml;
}
@PostMapping("publish")
public Result publish(@RequestBody ArticleParam articleParam) {
return articleService.publish(articleParam);
}
22.3 Service
@Override
public Result publish(ArticleParam articleParam) {
//此接口 要加入到登录拦截中
SysUser sysUser = UserThreadLocal.get();
/**
* 1. 发布文章 目的 构建Article对象
* 2. 作者id 当前的登录用户
* 3. 标签 要将标签加入关联列表中
* 4. body 内容存储
*/
Article article = new Article();
article.setAuthorId(sysUser.getId());
article.setWeight(Article.Article_Common);
article.setViewCounts(0);
article.setTitle(articleParam.getTitle());
article.setSummary(articleParam.getSummary());
article.setCommentCounts(0);
article.setCreateDate(System.currentTimeMillis());
article.setCategoryId(articleParam.getCategory().getId());
this.articleMapper.insert(article);
//tag
List<TagVo> tags = articleParam.getTags();
if (tags != null) {
for (TagVo tag : tags) {
Long articleId = article.getId();
ArticleTag articleTag = new ArticleTag();
articleTag.setTagId(tag.getId());
articleTag.setArticleId(articleId);
articleTagMapper.insert(articleTag);
}
}
//body
ArticleBody articleBody = new ArticleBody();
articleBody.setArticleId(article.getId());
articleBody.setContent(articleParam.getBody().getContent());
articleBody.setContentHtml(articleParam.getBody().getContentHtml());
articleBodyMapper.insert(articleBody);
article.setBodyId(articleBody.getId());
articleMapper.updateById(article);
Map<String,String> map = new HashMap<>();
map.put("id",article.getId().toString());
return Result.success(map);
}
package com.hmx.blog.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hmx.blog.dao.pojo.ArticleTag;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-17 14:41
**/
public interface ArticleTagMapper extends BaseMapper<ArticleTag> {
}
package com.hmx.blog.dao.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hmx.blog.dao.pojo.ArticleBody;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-15 21:53
**/
public interface ArticleBodyMapper extends BaseMapper<ArticleBody> {
}
package com.hmx.blog.vo;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import java.util.List;
@Data
public class ArticleVo {
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
private String title;
private String summary;
private Integer commentCounts;
private Integer viewCounts;
private Integer weight;
/**
* 创建时间
*/
private String createDate;
private String author;
private ArticleBodyVo body;
private List<TagVo> tags;
private CategoryVo category;
}
package com.hmx.blog.dao.pojo;
import lombok.Data;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-17 14:40
**/
@Data
public class ArticleTag {
private Long id;
private Long articleId;
private Long tagId;
}
当然登录拦截器中,需要加入发布文章的配置:
@Override
public void addInterceptors(InterceptorRegistry registry) {
//拦截test接口,后续实际遇到需要拦截的接口时,在配置为真正的拦截接口
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/test")
.addPathPatterns("/comments/create/change")
.addPathPatterns("/articles/publish");
}
22.4 测试
这里用户必须有nickname功能才能正常
23. AOP日志
package com.hmx.blog.common.aop;
import java.lang.annotation.*;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-17 15:47
**/
//Type代表可以放在类上,METHOD代表可以放在方法上
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogAnnotation {
String module() default "";
String operator() default "";
}
package com.hmx.blog.common.aop;
import com.alibaba.fastjson.JSON;
import com.hmx.blog.utils.HttpContextUtils;
import com.hmx.blog.utils.IpUtils;
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 javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-17 15:52
**/
@Component
@Aspect //切面 定义了通知和切点的关系
@Slf4j
public class LogAspect {
@Pointcut("@annotation(com.hmx.blog.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);
//获取request 设置IP地址
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
log.info("ip:{}", IpUtils.getIpAddr(request));
log.info("excute time : {} ms",time);
log.info("=====================log end================================");
}
}
package com.hmx.blog.utils;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-17 16:01
**/
public class IpUtils {
private IpUtils() {
}
/**
* 获取当前网络ip
* @param request
* @return
*/
public static String getIpAddr(HttpServletRequest request){
String ipAddress = request.getHeader("x-forwarded-for");
if(ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if(ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if(ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if(ipAddress.equals("127.0.0.1") || ipAddress.equals("0:0:0:0:0:0:0:1")){
//根据网卡取本机配置的IP
InetAddress inet=null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
ipAddress= inet.getHostAddress();
}
}
//对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if(ipAddress!=null && ipAddress.length()>15){ //"***.***.***.***".length() = 15
if(ipAddress.indexOf(",")>0){
ipAddress = ipAddress.substring(0,ipAddress.indexOf(","));
}
}
return ipAddress;
}
/**
* 获得MAC地址
* @param ip
* @return
*/
public static String getMACAddress(String ip){
String str = "";
String macAddress = "";
try {
Process p = Runtime.getRuntime().exec("nbtstat -A " + ip);
InputStreamReader ir = new InputStreamReader(p.getInputStream());
LineNumberReader input = new LineNumberReader(ir);
for (int i = 1; i < 100; i++) {
str = input.readLine();
if (str != null) {
if (str.indexOf("MAC Address") > 1) {
macAddress = str.substring(str.indexOf("MAC Address") + 14, str.length());
break;
}
}
}
} catch (IOException e) {
e.printStackTrace(System.out);
}
return macAddress;
}
}
package com.hmx.blog.utils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-17 16:01
**/
public class HttpContextUtils {
public static HttpServletRequest getHttpServletRequest() {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
}
}
23.1 测试
Bug修正
文章归档:
select FROM_UNIXTIME(create_date/1000,'%Y') as year, FROM_UNIXTIME(create_date/1000,'%m') as month,count(*) as count from ms_article group by year,month
24 文章图片上传
24.1 接口说明
接口url:/upload
请求方式:POST
请求参数:
参数名称 | 参数类型 | 说明 |
---|---|---|
image | file | 上传的文件名称 |
返回数据:
{
"success":true,
"code":200,
"msg":"success",
"data":"https://static.mszlu.com/aa.png"
}
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
<version>[7.7.0, 7.7.99]</version>
</dependency>
24.2 Controller
package com.hmx.blog.controller;
import com.hmx.blog.utils.QiniuUtils;
import com.hmx.blog.vo.Result;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.util.UUID;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-17 16:27
**/
@RestController
@RequestMapping("upload")
public class UploadController {
@Autowired
private QiniuUtils qiniuUtils;
@PostMapping
public Result upload(@RequestParam("image") MultipartFile file) {
//原始文件名称 比如aa.png
String originalFilename = file.getOriginalFilename();
//唯一的文件名称
String fileName = UUID.randomUUID().toString() + "." + StringUtils.substringAfterLast(originalFilename, ".");
//上传文件,上传到哪呢? 七牛云 云服务器 按量付费 速度快 把图片发放到离用户最近的服务器上
//降低 我们自身应用服务器的带宽消耗
boolean upload = qiniuUtils.upload(file, fileName);
if (upload) {
return Result.success(QiniuUtils.url + fileName);
}
return Result.fail(20001, "上传失败");
}
}
24.3 使用七牛云
# 上传文件总的最大值
spring.servlet.multipart.max-request-size=20MB
# 单个文件的最大值
spring.servlet.multipart.max-file-size=2MB
package com.hmx.blog.utils;
import com.alibaba.fastjson.JSON;
import com.qiniu.http.Response;
import com.qiniu.storage.Configuration;
import com.qiniu.storage.Region;
import com.qiniu.storage.UploadManager;
import com.qiniu.storage.model.DefaultPutRet;
import com.qiniu.util.Auth;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-17 16:45
**/
@Component
public class QiniuUtils {
public static final String url = "http://qxzes9jq2.hn-bkt.clouddn.com/";
@Value("${qiniu.accessKey}")
private String accessKey;
@Value("${qiniu.accessSecretKey}")
private String accessSecretKey;
public boolean upload(MultipartFile file, String fileName){
//构造一个带指定 Region 对象的配置类
Configuration cfg = new Configuration(Region.huanan());
//...其他参数参考类注释
UploadManager uploadManager = new UploadManager(cfg);
//...生成上传凭证,然后准备上传
String bucket = "hmx1234";
//默认不指定key的情况下,以文件内容的hash值作为文件名
try {
byte[] uploadBytes = file.getBytes();
Auth auth = Auth.create(accessKey, accessSecretKey);
String upToken = auth.uploadToken(bucket);
Response response = uploadManager.put(uploadBytes, fileName, upToken);
//解析上传成功的结果
DefaultPutRet putRet = JSON.parseObject(response.bodyString(), DefaultPutRet.class);
return true;
} catch (Exception ex) {
ex.printStackTrace();
}
return false;
}
}
24.4 测试
25 导航-文章分类
25.1 查询所有的文章分类
25.1.1 接口说明
接口url:/categorys/detail
请求方式:GET
请求参数:
参数名称 | 参数类型 | 说明 |
---|
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": [
{
"id": 1,
"avatar": "/static/category/front.png",
"categoryName": "前端",
"description": "前端是什么,大前端"
},
{
"id": 2,
"avatar": "/static/category/back.png",
"categoryName": "后端",
"description": "后端最牛叉"
},
{
"id": 3,
"avatar": "/static/category/lift.jpg",
"categoryName": "生活",
"description": "生活趣事"
},
{
"id": 4,
"avatar": "/static/category/database.png",
"categoryName": "数据库",
"description": "没数据库,啥也不管用"
},
{
"id": 5,
"avatar": "/static/category/language.png",
"categoryName": "编程语言",
"description": "好多语言,该学哪个?"
}
]
}
package com.hmx.blog.vo;
import lombok.Data;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-15 21:45
**/
@Data
public class CategoryVo {
private Long id;
private String avatar;
private String categoryName;
private String description;
}
25.1.2 Controller
@GetMapping("detail")
public Result categoriesDetail(){
return categoryService.findAllDetail();
}
25.1.3 Service
@Override
public Result findAll() {
List<Category> categories = categoryMapper.selectList(new LambdaQueryWrapper<Category>().select(Category::getId,Category::getCategoryName));
//页面交互的对象
return Result.success(copyList(categories));
}
25.1.4 测试
25.2 查询所有的标签
25.2.1 接口说明
接口url:/tags/detail
请求方式:GET
请求参数:
参数名称 | 参数类型 | 说明 |
---|
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": [
{
"id": 5,
"tagName": "springboot",
"avatar": "/static/tag/java.png"
},
{
"id": 6,
"tagName": "spring",
"avatar": "/static/tag/java.png"
},
{
"id": 7,
"tagName": "springmvc",
"avatar": "/static/tag/java.png"
},
{
"id": 8,
"tagName": "11",
"avatar": "/static/tag/css.png"
}
]
}
25.2.3 Controller
package com.hmx.blog.vo;
import lombok.Data;
@Data
public class TagVo {
private Long id;
private String tagName;
private String avatar;
}
@GetMapping("detail")
public Result findAllDetail(){
return tagService.findAllDetail();
}
25.2.4 Service
@Override
public Result findAllDetail() {
LambdaQueryWrapper<Tag> queryWrapper = new LambdaQueryWrapper<>();
List<Tag> tags = this.tagMapper.selectList(queryWrapper);
return Result.success(copyList(tags));
}
25.2.5 测试
25.3 分类文章列表
25.3.1 接口说明
接口url:/category/detail/{id}
请求方式:GET
请求参数:
参数名称 | 参数类型 | 说明 |
---|---|---|
id | 分类id | 路径参数 |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data":
{
"id": 1,
"avatar": "/static/category/front.png",
"categoryName": "前端",
"description": "前端是什么,大前端"
}
}
25.3.2 Controller
@GetMapping("detail/{id}")
public Result categoriesDetailById(@PathVariable("id") Long id){
return categoryService.categoriesDetailById(id);
}
25.3.3 Service
@Override
public Result categoriesDetailById(Long id) {
Category category = categoryMapper.selectById(id);
CategoryVo categoryVo = copy(category);
return Result.success(categoryVo);
}
//查询文章的参数 加上分类id,判断不为空 加上分类条件
if (pageParams.getCategoryId() != null) {
queryWrapper.eq(Article::getCategoryId,pageParams.getCategoryId());
}
package com.hmx.blog.vo.params;
import lombok.Data;
@Data
public class PageParams {
private int page = 1;
private int pageSize = 10;
private Long categoryId;
private Long tagId;
}
25.4. 标签文章列表
25.4.1 接口说明
接口url:/tags/detail/{id}
请求方式:GET
请求参数:
参数名称 | 参数类型 | 说明 |
---|---|---|
id | 标签id | 路径参数 |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data":
{
"id": 5,
"tagName": "springboot",
"avatar": "/static/tag/java.png"
}
}
25.4.2 Controller
@GetMapping("detail/{id}")
public Result findDetailById(@PathVariable("id") Long id){
return tagService.findDetailById(id);
}
25.4.3 Service
@Override
public Result findDetailById(Long id) {
Tag tag = tagMapper.selectById(id);
TagVo copy = copy(tag);
return Result.success(copy);
}
25.4.4 修改原有的查询文章接口
@Override
public Result listArticle(PageParams pageParams) {
/**
* 1. 分页查询 article数据库表
*/
Page<Article> page = new Page<>(pageParams.getPage(),pageParams.getPageSize());
LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper<>();
if (pageParams.getCategoryId() != null) {
queryWrapper.eq(Article::getCategoryId,pageParams.getCategoryId());
}
List<Long> articleIdList = new ArrayList<>();
if (pageParams.getTagId() != null){
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){
queryWrapper.in(Article::getId,articleIdList);
}
}
//是否置顶进行排序
//order by create_date desc
queryWrapper.orderByDesc(Article::getWeight,Article::getCreateDate);
Page<Article> articlePage = articleMapper.selectPage(page, queryWrapper);
List<Article> records = articlePage.getRecords();
//能直接返回吗? 很明显不能
List<ArticleVo> articleVoList = copyList(records,true,true);
return Result.success(articleVoList);
}
25.4.5 测试
26. 归档文章列表
26.1 接口说明
接口url:/articles
请求方式:POST
请求参数:
参数名称 | 参数类型 | 说明 |
---|---|---|
year | string | 年 |
month | string | 月 |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": [文章列表,数据同之前的文章列表接口]
}
26.2 文章列表参数
package com.hmx.blog.vo.params;
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;
public String getMonth(){
if (this.month != null && this.month.length() == 1){
return "0"+this.month;
}
return this.month;
}
}
26.3 使用自定义sql 实现文章列表
@Override
public Result listArticle(PageParams pageParams) {
Page<Article> page = new Page<>(pageParams.getPage(),pageParams.getPageSize());
IPage<Article> articleIPage = this.articleMapper.listArticle(page,pageParams.getCategoryId(),pageParams.getTagId(),pageParams.getYear(),pageParams.getMonth());
return Result.success(copyList(articleIPage.getRecords(),true,true));
}
<resultMap id="articleMap" type="com.mszlu.blog.dao.pojo.Article">
<id column="id" property="id" />
<result column="author_id" property="authorId"/>
<result column="comment_counts" property="commentCounts"/>
<result column="create_date" property="createDate"/>
<result column="summary" property="summary"/>
<result column="title" property="title"/>
<result column="view_counts" property="viewCounts"/>
<result column="weight" property="weight"/>
<result column="body_id" property="bodyId"/>
<result column="category_id" property="categoryId"/>
</resultMap>
<select id="listArticle" resultMap="articleMap">
select * from ms_article
/*增加where前缀,自动忽略前缀*/
<trim prefixOverrides="AND|OR">
<if test="categoryId != null">
and category_id = #{categoryId}
</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>
<if test="tagId != null">
and id in (select article_id from ms_article_tag where tag_id=#{tagId})
</if>
</trim>
order by create_date desc
</select>
@Override
public Result listArticle(PageParams pageParams) {
Page<Article> page = new Page<>(pageParams.getPage(), pageParams.getPageSize());
// 传递的Page分页对象会被MybatisPlus自己处理
IPage<Article> articleIPage = articleMapper.listArticle(page, pageParams.getCategoryId(), pageParams.getTagId(), pageParams.getYear(), pageParams.getMonth());
List<Article> records = articleIPage.getRecords();
return Result.success(copyList(records, true, true,false,false));
}
26.4 测试
27. 统一缓存处理(优化)
内存的访问速度 远远大于 磁盘的访问速度 (1000倍起)
package com.hmx.blog.common.cache;
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Cache {
long expire() default 1 * 60 * 1000;
String name() default "";
}
package com.hmx.blog.common.cache;
import com.alibaba.fastjson.JSON;
import com.hmx.blog.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;
/**
* @program: blog-parent
* @description:
* @author: hmx
* @create: 2021-08-18 10:20
**/
//aop 定义一个切面,切面定义了切点和通知的关系
@Aspect
@Component
@Slf4j
public class CacheAspect {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Pointcut("@annotation(com.hmx.blog.common.cache.Cache)")
public void pt(){}
@Around("pt()")
public Object around(ProceedingJoinPoint point) throws Throwable {
try {
Signature signature = point.getSignature();
//类名
String className = point.getTarget().getClass().getSimpleName();
//调用的方法名
String methodName = signature.getName();
Class[] parameterTypes = new Class[point.getArgs().length];
Object[] args = point.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)) {
//加密以防出现key过长以及字符转义获取不到的情况
params = DigestUtils.md5Hex(params);
}
Method method = point.getSignature().getDeclaringType().getMethod(methodName, parameterTypes);
//获取Cache注解
Cache annotation = method.getAnnotation(Cache.class);
//缓存过期时间
long expire = annotation.expire();
//缓存名称
String name = annotation.name();
//先从redis中获取
String redisKey = name + "::" + className + "::" + methodName + "::" + params;
String redisValue = redisTemplate.opsForValue().get(redisKey);
if (StringUtils.isNotEmpty(redisValue)) {
log.info("走了缓存~~~,{},{}",className,methodName);
return JSON.parseObject(redisValue, Result.class);
}
Object proceed = point.proceed();
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, "系统错误");
}
}
使用:
@PostMapping("hot")
@Cache(expire = 5 * 60 * 1000,name = "hot_article")
public Result hotArticle(){
int limit = 5;
return articleService.hotArticle(limit);
}
@PostMapping("new")
@Cache(expire = 5 * 60 * 1000,name = "new_article")
public Result newArticle(){
int limit = 5;
return articleService.newArticle(limit);
}
思考别的优化
- 文章可以放入es当中,便于后续中文分词搜索。springboot教程有和es的整合
- 评论数据,可以考虑放入mongodb当中 电商系统当中 评论数据放入mongodb中
- 阅读数和评论数 ,考虑把阅读数和评论数 增加的时候 放入redis incr自增,使用定时任务 定时把数据持久化到数据库当中
- 为了加快访问速度,部署的时候,可以把图片,js,css等放入七牛云存储中,加快网站访问速度
做一个后台 用springsecurity 做一个权限系统,对工作帮助比较大
将域名注册,备案,部署相关
总结
1 jwt + redis
token令牌的登录方式,访问认证速度快,session共享,安全性
redis做了令牌和用户信息的对应管理,1.进一步加强了安全性 2. 登录用户做了缓存 3. 灵活控制用户的过期(续期,踢掉线等)
2. threadLocal使用了保存用户信息,请求的线程之内,可以随时获取登录的用户,做了线程隔离
3. 在使用完ThreadLcoal之后,做了value的删除,防止了内存泄漏
4. 线程安全- update table set value = newValue where id = 1 and value = oldValue
5. 线程池 应用非常广,7个参数 (对当前的主业务流程 无影响的操作,放入线程执行)
1. 登录,记录日志
6. 权限系统 重点内容
7. 统一日志记录,统一缓存处理