通过SpringBoot框架实现的博客后端模块,对前端接口进行规范实现以及文档编写。
项目需求分析
使用的技术 SpringBoot+MybatisPlus+Redis+MySQL
1、项目环境搭建 思路 1、我们先搭建父工程(blog-parent),然后在里面创建子工程后端模块(blog-api)。
正常情况下我们可以选择SpringInitiarazer来创建SpringBoot项目,但是此时我们选择的是另一种较为复杂的方法去熟悉一下SpringBoot项目从头搭建的过程:通过创建Maven项目继承SpringBoot项目。
在将Maven项目变成SpringBoot项目之后可以进行启动测试
在我们要添加Web依赖之后,添加了依赖之后我们就不能轻易启动项目了,要在配置文件中配置(端口,名称,数据库)之后才能启动项目。
思考:这种方式和创建SpringInitializer究竟有什么区别呢?
我们只需要讨论怎么将Maven项目转化为SpringBoot项目:
1.1、数据库创建 1.1.1、先创建数据库
1.2、创建父工程 1.2.1、创建Maven项目 一般来说我们会创建一个parent项目。(是为了之后创建项目的时候能够直接继承吗?)
1.2.2、pom文件完善 在pom文件中,我们完善四个大点
properties:文件中原来就有peoperties,在里面添加字符集和jdk版本的定义。
parent:此处定义Maven的父项目,我们选择在此处继承SpringBoot2.5.0
dependencies:在此处我们导入想要使用的相关依赖
build:在此处我们引入插件构建SpringBoot
dependencymanagement:在父工程的依赖外面加上dependencymanagement可以方便子项目导入依赖时可以不用管理版本,直接导入即可。
子工程添加依赖的时候可以不用写版本号,因为父工程已经版本号进行了控制
如果子工程想添加别的版本的依赖直接声明版本或者直接添加就行,并不会被真正的限制
注意:如果父项目不用依赖管理,那么依赖会随着项目的继承而自动继承,可如果使用了项目管理,子项目自己就还要再导入一遍(只是不用再进行版本输入了。)
packaging设置为pom代表此工程是一个父工程(设置于最外层)<packaging>pom</packaging>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 <properties > <maven.compiler.source > 8</maven.compiler.source > <maven.compiler.target > 8</maven.compiler.target > <project.build.sourceEncoding > UTF-8</project.build.sourceEncoding > <project.reporting.outputEncoding > UTF-8</project.reporting.outputEncoding > <java.version > 1.8</java.version > </properties > <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 2.5.0</version > <relativePath /> </parent > <dependencies > <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 > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-log4j2</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-aop</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-mail</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 1.2.76</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-configuration-processor</artifactId > <optional > true</optional > </dependency > <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-lang3</artifactId > </dependency > <dependency > <groupId > commons-collections</groupId > <artifactId > commons-collections</artifactId > <version > 3.2.2</version > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.4.3</version > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > </dependency > <dependency > <groupId > joda-time</groupId > <artifactId > joda-time</artifactId > <version > 2.10.10</version > </dependency > </dependencies > <build > <plugins > <plugin > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > </plugin > </plugins > </build >
1.2.3、删除src文件
1.3、创建子工程 1.3.1、创建模块
1.3.2、完善pom依赖 由于我们上文并没有使用dependencymanagement对父项目依赖进行管理,所以我们此时创建了子项目之后,依赖就不用进行再次导入的了。
1.3.3、application配置文件 我们选择的是application.peoperties配置文件。进行基础的配置。由于前端会默认将文件部署到8080,那么我们后台就要换个端口进行部署,部署能用就行,但是要与前端的后端接口地址进行匹配,所以我们就干脆部署在前端要求的8888端口下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # server server.port=8888 # 应用名称 spring.application.name=wahoyu_blog # 数据库的配置datasource(字符编码以及时区) spring.datasource.url=jdbc:mysql://localhost:3306/boot_blog?useUnicode=true&characterEncoding=UTF-8&serverTimeZone=UTC spring.datasource.username=root spring.datasource.password=123456 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver #配置MybatisPlus ## 日志实现 mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl ## 设置数据库表的前缀(本来数据库表和实体类之间是应该对应的,但是我们设置了数据库表的前缀) ## 设置前缀之后,数据库表名就可以有一个前缀 mybatis-plus.global-config.db-config.table-prefix=ms_
1.3.4、MybatisPlusConfig配置类 由于我们上面在配置文件中设置了MybatisPlus,所以我们还要设置一个启动类。
1.3.5、创建启动类并测试
1 2 3 4 5 6 @SpringBootApplication public class BlogApp { public static void main (String[] args) { SpringApplication.run(BlogApp.class,args); } }
点击小三角测试成功,oh玛德,一点一点搭建SpringBoot项目简直不要更麻烦~(加油,以后就会更快了)
1.3.6、MybatisPlus分页插件注册 前端的访问是进行分页的,那么我们在这里进行一下分页配置。(MybatisPlusConfig.java)
1 2 3 4 5 6 7 @Bean public MybatisPlusInterceptor mybatisPlusInterceptor () { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor (); interceptor.addInnerInterceptor(new PaginationInnerInterceptor ()); return interceptor; }
1.3.7、跨域配置 由于前端部署在8080,我们后端部署在8888,所以涉及到要跨域名访问的问题。我们在配置文件中已经设置了后端部署的域名并与前端的接口链接进行了对应,现在我们要对后端进行一个跨域拦截,让域名为8080的前端可以通过。
1 2 3 4 5 6 7 8 9 10 @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addCorsMappings (CorsRegistry registry) { registry.addMapping("/**" ).allowedOrigins("http://localhost:8080" ); } }
1.4、前端项目运行 可以使用控制台进入文件夹输入命令,也可使用IDE终端进行命令输入。
1.4.1、创建Vue项目 如果我们没有Vue项目,我们可以使用命令
1 2 vue init webpack firstVue
如果已经有一个Vue项目,那么我们可以进入项目的目录
1.4.2、查看package.json文件
此时我们应该查看项目中的package.json
的scripts 配置
1 2 3 4 5 "scripts" : { "dev" : "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js" , "start" : "npm run dev" , "build" : "node build/build.js" } ,
1 2 3 4 5 "scripts" : { "serve" : "vue-cli-service serve" , "build" : "vue-cli-service build" , "lint" : "vue-cli-service lint" } ,
1.4.3、添加项目依赖install
1.4.4、打包项目build
生成的文件夹不能直接运行,需要安装serve:npm install serve -S -g
(一定要全局安装不然也会报错)。
必须要在dist文件夹的上一级运行部署命令。
或者可以将dist文件拷贝到其他地方,比如使用hbuilder开启服务访问,或者复制到xampp的服务中访问
1.4.5、本地部署dev/server
查看其中的具体配置
2、首页内容与标签 2.1、首页-文章列表 在我们部署成功之后,下面我们主要进行实现的就是进行前端的接口实现。首先进行的就是实现首页文章列表的分页查询
(包括标题,主要内容,作者等),这一步中的关键点在于:
将前端的参数进行实体类Vo封装,传入Controller,对返回值进行总体Result封装
使用MybatisPlus查询出Page(包括Page对象,对Wrapper的限制)
使用MybatisPlus查询出Page(包括Page对象,对Wrapper的限制)
通过getRecords()将Page对象转化为文章List对象
查询出的文章List→适合前端输出的文章List
将List转化成Vo实体类对象,再将Vo实体类对象作为data参数传入Result进行前端页面返回
2.1.1、接口说明 接口url:/articles
请求方式:POST
请求参数:
参数名称
参数类型
说明
page
int
当前页数
pageSize
int
每页显示的数量
返回数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 { "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.1.2、文章列表主体内容 2.1.2.1、表架构 boot_blog.sql
2.1.2.2、vo.Result 作为给前端生成返回值的实体类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Data public class ArticleVo { private Long id; private String title; private String summary; private int commentCounts; private int viewCounts; private int weight; private String createDate; private String author; private List<TagVo> tags; }
2.1.2.3、实体类(entity、pojo) 创建Article、Tag、SysUser的实体类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @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 int commentCounts; private int viewCounts; private Long authorId; private Long bodyId; private Long categoryId; private int weight = Article_Common; private Long createDate; }
1 2 3 4 5 6 7 8 @Data public class Tag { private Long id; private String avatar; private String tagName; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Data public class SysUser { private Long id; private String account; private Integer admin; private String avatar; private Long createDate; private Integer deleted; private String email; private Long lastLogin; private String mobilePhoneNumber; private String nickname; private String password; private String salt; private String status; }
2.1.2.4、Mapper(Dao) mapper继承MybatisPlusa中的BaseMapper<实体类>,相当于自动实现众多方法。
1 2 3 4 5 @Mapper public interface ArticleMapper extends BaseMapper <Article> { }
1 2 3 4 5 @Mapper public interface SysUserMapper extends BaseMapper <SysUser> { }
1 2 3 4 5 @Mapper public interface TagMapper extends BaseMapper <Tag> { }
2.1.2.5、Controller 负责将读取参数并将传入的参数进一步传递到Service中进行逻辑处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @RestController @RequestMapping("articles") public class ArticleController { @Autowired ArticleService articleService; @PostMapping public Result listArticle (@RequestBody PageParams pageParams) { return articleService.listArticle(pageParams); } }
2.1.2.6、Service 1 2 3 4 5 6 7 public interface ArticleService { Result listArticle (PageParams pageParams) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 @Service public class ArticleServiceImpl implements ArticleService { @Autowired ArticleMapper articleMapper; @Override public Result listArticle (PageParams pageParams) { Page<Article> page = new Page <>(pageParams.getPage(), pageParams.getPageSize()); LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper <>(); queryWrapper.orderByDesc(Article::getCreateDate,Article::getWeight); 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; } }
2.1.2.7、Vo对象(给前端传送的数据的封装实体类) 传入参数Vo
1 2 3 4 5 6 7 @Data public class PageParams { private int page = 1 ; private int pageSize = 10 ; }
返回结果Vo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Data public class ArticleVo { private Long id; private String title; private String summary; private int commentCounts; private int viewCounts; private int weight; private String createDate; private String author; private List<TagVo> tags; }
2.1.2.8、测试 运行失败,重新部署后成功。可能是因为重启了项目?
目前,我们实现了简单的,将后端的文章列表数据库一条线衔接到前端接口。
目前我们实现了文章列表的简单实现,现在我们还要对队列中的众多细节进行实现:比如标签、作者等……
但是我们依旧要注意以下几点:
2.1.3、文章列表中标签的显示 不是每一个文章都会有标签的,所以我们在进行文章copy的时候会进行tag和author的信息和判断👇
首先我们将文章的Tag列表赋给文章列表中的每个文章,这就涉及到对每个文章进行copy函数中(具体实现再ArticleService中的Article to ArticleVo的操作)
我们发现文章和标签的格式是一对一进行的,我们再输出文章列表的时候可以通过articleid对tags进行查询,将查询到的tags列表填入到articleVo实体类对象中。
articleService→tagService→tagMapper(xml书写查询规则)。在Tag部分,我们依旧使用copy原则将查询值进行返回。(将查询到的List<Tag>转化成tagList)
2.1.3.1、ArtivleService 在copy成ArticleVo的过程中将内部进行填补。再将author也进行填充之后,再对Article进行完善。
1 2 3 4 5 6 if (isTag) { Long articleId = article.getId(); articleVo.setTags(tagService.findTagsByArticleId(articleId)); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Service public class TagServiceImpl implements TagService { @Autowired private TagMapper tagMapper; @Override public List<TagVo> findTagsByArticleId (Long id) { List<Tag> tags = tagMapper.findTagsByArticleId(id); return copyList(tags); } public List<TagVo> copyList (List<Tag> tagList) { List<TagVo> tagVoList = new ArrayList <>(); for (Tag tag : tagList) { tagVoList.add(copy(tag)); } return tagVoList; } public TagVo copy (Tag tag) { TagVo tagVo = new TagVo (); BeanUtils.copyProperties(tag,tagVo); return tagVo; } }
2.1.3.3、TagMapper 1 2 3 4 5 @Mapper public interface TagMapper extends BaseMapper <Tag> { List<Tag> findTagsByArticleId (Long atricleId) ; }
2.1.3.4、TagMapper.xml 1 2 3 4 5 6 7 8 9 10 11 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="com.blog.mapper.TagMapper" > <select id ="findTagsByArticleId" parameterType ="long" resultType ="com.blog.entity.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.1.4、文章列表中作者的显示 对于User我们决定暂时不设置Vo类,因为我们可以直接通过两个表之间的数值进行查询。上面的标签是使用了另一个表将文章列表和标签页表进行连接(这是因为一个文章只能有一个作者,但是可以有许多个标签)
2.1.4.1、ArticleService 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 private List<ArticleVo> copyList (List<Article> records, boolean isTag , boolean isAuthor) { 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 authorId = article.getAuthorId(); articleVo.setAuthor(sysUserService.findUserById(authorId).getNickname()); } return articleVo; }
2.1.4.2、UserService 1 2 3 4 public interface SysUserService { SysUser findUserById (Long id) ; }
2.1.4.3、UserServiceImpl 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @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("Wahoyu" ); } return sysUser; } }
2.1.5、测试
2.2、首页-最热标签 我们想要在主页显示出被使用的数目最多的标签并进行显示,主要思路是:
2.2.1、接口说明 接口url:/tags/hot
请求方式:GET
请求参数:无
返回数据:
1 2 3 4 5 6 7 8 9 10 11 { "success" : true , "code" : 200 , "msg" : "success" , "data" : [ { "id" : 1 , "tagName" : "4444" } ] }
2.2.2、编码 2.2.2.1、Controller 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @RestController @RequestMapping("tags") public class TagsController { @Autowired TagService tagService; @GetMapping("hot") public Result hot () { int limit = 6 ; return tagService.hots(limit); } }
2.2.2.2、Service 1 2 3 4 5 6 public interface TagService { List<TagVo> findTagsByArticleId (Long articleId) ; Result hots (int limit) ; }
2.2.2.3、ServiceImpl 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Override public Result hots (int limit) { List<Long> tagIds = tagMapper.findHotsTagIds(limit); if (CollectionUtils.isEmpty(tagIds)){ return Result.success(Collections.emptyList()); } List<Tag> tagList = tagMapper.findTagsByIds(tagIds); return Result.success(tagList); }
2.2.2.4、TagMapper 1 2 3 4 5 6 7 8 9 @Mapper public interface TagMapper extends BaseMapper <Tag> { List<Tag> findTagsByArticleId (Long atricleId) ; List<Long> findHotsTagIds (int limit) ; List<Tag> findTagsByIds (List<Long> tagIds) ; }
2.2.2.5、TagMapper.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <select id ="findHotsTagIds" parameterType ="int" resultType ="java.lang.Long" > SELECT tag_id FROM `ms_article_tag` GROUP BY tag_id ORDER BY COUNT(*) DESC LIMIT #{limit}</select > <select id ="findTagsByIds" parameterType ="list" resultType ="com.blog.entity.Tag" > select id , tag_name as tagName from ms_tag where id in <foreach collection ="tagIds" item ="tagId" separator ="," open ="(" close = ")" > #{tagId} </foreach > </select >
2.2.3、测试
2.3、首页-最热文章 最热文章是通过article的浏览量进行判断的。所以我们要去article表中找一下view_count字段,根据他进行排序。并不是复杂查询,所以使用的是Wrapper进行筛选。
2.3.1 接口说明 接口url:/articles/hot
请求方式:POST
请求参数:
返回数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 { "success" : true , "code" : 200 , "msg" : "success" , "data" : [ { "id" : 1 , "title" : "springboot介绍以及入门案例" , } , { "id" : 9 , "title" : "Vue.js 是什么" , } , { "id" : 10 , "title" : "Element相关" , } ] }
2.3.2 编码 2.3.2.1、ArticleController 1 2 3 4 5 6 7 8 @PostMapping("hot") public Result hotArticle () { int limit = 5 ; return articleService.hotArticle(limit); }
2.3.2.2、ArticleService 1 2 Result hotArticle (int limit) ;
2.3.2.3、ArticleServiceImpl 1 2 3 4 5 6 7 8 9 10 11 @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 )); }
2.3.3测试
2.4、首页-最新文章 最新文章与最热文章类似,只不过最热文章是按照浏览量进行排序,最新文章是按照创建时间进降序排序。
2.4.1 接口说明 接口url:/articles/new
请求方式:POST
请求参数:
返回数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 { "success" : true , "code" : 200 , "msg" : "success" , "data" : [ { "id" : 1 , "title" : "springboot介绍以及入门案例" , } , { "id" : 9 , "title" : "Vue.js 是什么" , } , { "id" : 10 , "title" : "Element相关" , } ] }
2.4.2 编码 2.4.2.1、ArticleController 1 2 3 4 5 6 7 8 @PostMapping("new") public Result newArticle () { int limit = 5 ; return articleService.newArticle(limit); }
2.4.2.2、ArticleService 1 2 Result newArticle (int limit) ;
2.4.2.3、ArticleServiceImpl 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @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 )); }
2.4.3 测试
2.5、首页-文章归档 每一个文章创建的时候都有时间,我们将其分为年月的形式。某年某月你发表了多少文章就是文章归档。相当于是你某段时间发表文章的计数。
具体的思路以及重点如下:
分层领域模型规约:
DO( Data Object):与数据库表结构一一对应,通过DAO层向上传输数据源对象。
DTO( Data Transfer Object):数据传输对象,Service或Manager向外传输的对象。
BO( Business Object):业务对象。 由Service层输出的封装业务逻辑的对象。
AO( Application Object):应用对象。 在Web层与Service层之间抽象的复用对象模型,极为贴近展示层,复用度不高。
VO( View Object):显示层对象,通常是Web向模板渲染引擎层传输的对象。
POJO( Plain Ordinary Java Object):在本手册中, POJO专指只有setter/getter/toString的简单类,包括DO/DTO/BO/VO等。
Vo与Do的区别:
2.5.1接口说明 接口url:/articles/listArchives
请求方式:POST
请求参数:
返回数据:
1 2 3 4 5 6 7 8 9 10 11 12 { "success" : true , "code" : 200 , "msg" : "success" , "data" : [ { "year" : "2021" , "month" : "6" , "count" : 2 } ] }
2.5.2 编码 2.5.2.1、ArticleController 1 2 3 4 5 @PostMapping("listArchives") public Result listArchives () { return articleService.listArchives(); }
2.5.2.2、dos.Archives 1 2 3 4 5 6 7 8 9 10 @Data public class Archives { private Integer year; private Integer month; private Integer count; }
2.5.2.3、ArticleService
2.5.2.4、ArticleServiceImpl 1 2 3 4 5 6 @Override public Result listArchives () { List<Archives> archivesList = articleMapper.listArchives(); return Result.success(archivesList); }
2.5.2.5、ArticleMapper 1 2 3 4 5 6 @Mapper public interface ArticleMapper extends BaseMapper <Article> { List<Archives> listArchives () ; }
2.5.2.6、ArticleMapper.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="com.blog.mapper.ArticleMapper" > <select id ="listArchives" resultType ="com.blog.dos.Archives" > select year(create_date) as year,month(create_date) as month,count(*) as count from ms_article group by year,month </select > </mapper >
2.5.3 测试
2.6、统一异常处理 不管是controller层还是service,dao层,都有可能报异常。如果是预料中的异常,可以直接捕获处理;如果是意料之外的异常,需要统一进行异常处理,进行记录,并给用户提示相对比较友好的信息。
1 2 3 4 5 6 7 8 9 10 11 12 @ControllerAdvice public class AllExceptionHandler { @ExceptionHandler(Exception.class) @ResponseBody public Result doException (Exception ex) { ex.printStackTrace(); return Result.fail(-999 ,"系统异常" ); } }
当我们在任意一个层出现了奇怪的报错的时候,会进行报错。虽然前端会显示200,但是实际上获取到的信息是-999报错。但是实际上我们获取到的信息是-999。
3、用户认证相关操作 3.1、用户登陆 在此部分我们首先导入两个依赖:
同时我们还要对程序进行简单的一些实体类/工具类优化:
前端登录参数类
生成token工具类
统一异常代码枚举
代码思路:(我们主要使用LoginService)
(获取用户信息时)访问权限页面时,会直接进行检验你有没有token(先验证你的token是否正确,再去redis中查找有没有),如果检验成功则获取用户信息,检验失败则进行登陆环节。
在Service中我们首先检查前端传入的参数进行控制判断,如果参数为空则报错
如果参数都存在,则直接对密码进行加密
此处调用SysUserService,根据用户名和加密密码去user表中查询找用户,如果没有用户则报错
如果存在用户,则利用JWT生成一个token
将token放入Redis数据库当中 (token:存放用户id) 设置过期时间;
将token信息return给前端,剩下的就交给前端吧。
3.1.1 接口说明 接口url:/login
请求方式:POST
请求参数:
参数名称
参数类型
说明
account
string
账号
password
string
密码
返回数据:
1 2 3 4 5 6 { "success" : true , "code" : 200 , "msg" : "success" , "data" : "token" }
3.1.2 JWT技术 登录使用JWT技术。(Json Web Token)
jwt 可以生成 一个加密的token,做为用户登录的令牌,当用户登录成功之后,发放给客户端。
请求需要登录的资源或者接口的时候,将token携带,后端验证token是否合法。
jwt 有三部分组成:A.B.C
A:Header,{“type”:”JWT”,”alg”:”HS256”} 固定
B:playload,存放信息,比如,用户id,过期时间等等,可以被解密,不能存放敏感信息
C: 签证,A+B+秘钥
加密而成,只要秘钥不丢失,可以认为是安全的。
jwt 验证,主要就是验证C部分 是否合法。
依赖包:
1 2 3 4 5 <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt</artifactId > <version > 0.9.1</version > </dependency >
工具类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package com.mszlu.blog.utils;import io.jsonwebtoken.Jwt;import io.jsonwebtoken.JwtBuilder;import io.jsonwebtoken.Jwts;import io.jsonwebtoken.SignatureAlgorithm;import java.util.Date;import java.util.HashMap;import java.util.Map;public class JWTUtils { private static final String jwtToken = "123456Mszlu!@#$$" ; public static String createToken (Long userId) { Map<String,Object> claims = new HashMap <>(); claims.put("userId" ,userId); JwtBuilder jwtBuilder = Jwts.builder() .signWith(SignatureAlgorithm.HS256, jwtToken) .setClaims(claims) .setIssuedAt(new Date ()) .setExpiration(new Date (System.currentTimeMillis() + 24 * 60 * 60 * 60 * 1000 )); String token = jwtBuilder.compact(); return token; } public static Map<String, Object> checkToken (String token) { try { Jwt parse = Jwts.parser().setSigningKey(jwtToken).parse(token); return (Map<String, Object>) parse.getBody(); }catch (Exception e){ e.printStackTrace(); } return null ; } }
3.1.3 JWT测试 在此写一个SpringBootTest测试类对JWT工具类进行测试
1 2 3 4 5 6 7 8 9 10 @SpringBootTest public class BlogAppTest { @Test public static void main (String[] args) { String token = JWTUtils.createToken(100L ); System.out.println(token); Map<String, Object> map = JWTUtils.checkToken(token); System.out.println(map.get("userId" )); } }
3.1.4 编码 3.1.4.1、导入依赖 导入MD5加密算法的依赖和JWT生成token的依赖
1 2 3 4 5 6 7 8 9 10 11 <dependency > <groupId > commons-codec</groupId > <artifactId > commons-codec</artifactId > </dependency > <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt</artifactId > <version > 0.9.1</version > </dependency >
3.1.4.2、创建Vo及工具类 1、统一异常代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 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; } }
2、JWT 生成token工具类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public class JWTUtils { private static final String jwtToken = "123456Mszlu!@#$$" ; public static String createToken (Long userId) { Map<String,Object> claims = new HashMap <>(); claims.put("userId" ,userId); JwtBuilder jwtBuilder = Jwts.builder() .signWith(SignatureAlgorithm.HS256, jwtToken) .setClaims(claims) .setIssuedAt(new Date ()) .setExpiration(new Date (System.currentTimeMillis() + 24 * 60 * 60 * 60 * 1000 )); String token = jwtBuilder.compact(); return token; } public static Map<String, Object> checkToken (String token) { try { Jwt parse = Jwts.parser().setSigningKey(jwtToken).parse(token); return (Map<String, Object>) parse.getBody(); }catch (Exception e){ e.printStackTrace(); } return null ; } }
3、前端登录参数类
1 2 3 4 5 6 @Data public class LoginParams { private String account; private String password; }
3.1.4.2、Controller 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @RestController @RequestMapping("login") public class LoginController { @Autowired private LoginService loginService; @PostMapping public Result login (@RequestBody LoginParams loginParams) { return loginService.login(loginParams); } }
3.1.4.3、LoginService和SysUserService 1 2 3 4 5 6 public interface LoginService { Result login (LoginParams loginParams) ; }
1 2 3 4 5 SysUser findUser (String account, String password) ;
3.1.4.4、LoginServiceImpl和SysUserServiceImpl 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 @Service public class LoginServiceImpl implements LoginService { @Autowired private SysUserService sysUserService; @Autowired private RedisTemplate<String,String> redisTemplate; private static final String salt = "wahoyu!@#" ; @Override public Result login (LoginParams loginParams) { String account = loginParams.getAccount(); String password = loginParams.getPassword(); if (StringUtils.isBlank(account) || StringUtils.isBlank(password)){ return Result.fail(ErrorCode.PARAMS_ERROR.getCode(),ErrorCode.PARAMS_ERROR.getMsg()); } password = DigestUtils.md5Hex(password + salt); SysUser sysUser = sysUserService.findUser(account,password); if (sysUser == null ){ return Result.fail(ErrorCode.ACCOUNT_PWD_NOT_EXIST.getCode(),ErrorCode.ACCOUNT_PWD_NOT_EXIST.getMsg()); } String token = JWTUtils.createToken(sysUser.getId()); redisTemplate.opsForValue().set("TOKEN_" +token, JSON.toJSONString(sysUser),1 , TimeUnit.DAYS); return Result.success(token); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Override public SysUser findUser (String account, String password) { LambdaQueryWrapper<SysUser> queryWrapper = new LambdaQueryWrapper <>(); queryWrapper.eq(SysUser::getAccount,account); queryWrapper.eq(SysUser::getPassword, password); queryWrapper.select(SysUser::getAccount, SysUser::getId, SysUser::getAvatar, SysUser::getNickname); queryWrapper.last("limit 1" ); return sysUserMapper.selectOne(queryWrapper); }
3.1.5、测试
在测试类中输出加密后的password,手动存储在数据库中。
1 2 3 4 5 6 7 8 9 10 @SpringBootTest public class BlogAppTest { @Test public static void main (String[] args) { String password = "123456" ; String salt = "wahoyu!@#" ; password = DigestUtils.md5Hex(password + salt); System.out.println(password); } }
运行Redis服务器,和SpringBoot项目
Postman对请求进行书写
3.2、获取用户信息 参数并不是直接传输到后端的,而是要我们直接从HttpHeader中获取。所以前端发的是GET请求不是POST。
获取用户信息思路步骤:
Controller将header中的token取出传给后端
Service通过JWT对token进行合法性校验,不合格则报错
取出Redis数据库中,token对应的json格式的sysUser对象
将json格式的信息转化成SysUser格式的对象
将sysUser对象复制成loginUserVo格式的对象,发给前端
后期优化:
将验证token的步骤从sysUserService转移到了LoginService中方便后面,拦截器对token进行检查
3.2.1 接口说明 接口url:/users/currentUser
请求方式:GET
请求参数:
参数名称
参数类型
说明
Authorization
string
头部信息(TOKEN)
返回数据:
1 2 3 4 5 6 7 8 9 10 11 { "success" : true , "code" : 200 , "msg" : "success" , "data" : { "id" : 1 , "account" : "admin" , "nickaname" : "Wahoyu" , "avatar" : "ss" } }
3.2.2、编码 3.2.2.1、LoginUserVo 1 2 3 4 5 6 7 8 9 10 11 @Data public class LoginUserVo { private Long id; private String account; private String nickname; private String avatar; }
3.2.2.2、Controller 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @RestController @RequestMapping("users") public class UserController { @Autowired private SysUserService sysUserService; @GetMapping("currentUser") public Result currentUser (@RequestHeader("Authorization") String token) { return sysUserService.getUserInfoByToken(token); } }
3.2.2.3、统一异常码 1 TOKEN_ERROR(10001 ,"用户名或密码不存在" ),
3.2.2.4、LoginService 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @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); if (StringUtils.isBlank(userJson)){ return null ; } SysUser sysUser = JSON.parseObject(userJson, SysUser.class); return sysUser; }
3.2.2.5、SysUserService 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Override public Result getUserInfoByToken (String token) { SysUser sysUser = loginService.checkToken(token); if (sysUser == null ){ Result.fail(ErrorCode.TOKEN_ERROR.getCode(),ErrorCode.TOKEN_ERROR.getMsg()); } LoginUserVo loginUserVo = new LoginUserVo (); loginUserVo.setAccount(sysUser.getAccount()); loginUserVo.setAvatar(sysUser.getAvatar()); loginUserVo.setId(sysUser.getId()); loginUserVo.setNickname(sysUser.getNickname()); return Result.success(loginUserVo); }
3.2.3、测试 打开Redis +SpringBoot+HBuilder服务器(一定要打开redis)
如图显示登陆成功
3.3、退出登陆 此时我们只需要做一件事,那就是把token从Redis数据库中删除。(我们的Redis服务器就相当于Session,我们将redis清空之后,获取不到用户数据自然就退出了。我们能够操作的也只有后端Redis。)
3.3.1 接口说明 接口url:/logout
请求方式:GET
请求参数:
参数名称
参数类型
说明
Authorization
string
头部信息(TOKEN)
返回数据:
1 2 3 4 5 6 { "success" : true , "code" : 200 , "msg" : "success" , "data" : null }
3.3.2 编码 3.3.2.1、Controller 1 2 3 4 5 6 7 8 9 10 11 12 @RestController @RequestMapping("logout") public class LogoutController { @Autowired private LoginService loginService; @GetMapping public Result logout (@RequestHeader("Authorization") String token) { return loginService.logout(token); } }
3.3.2.2、Service 1 2 3 4 5 6 7 public interface LoginService { Result login (LoginParams loginParams) ; Result logout (String token) ; }
3.3.2.3、ServiceImpl 1 2 3 4 5 6 7 @Override public Result logout (String token) { redisTemplate.delete("TOKEN_" +token); return Result.success(null ); }
3.3.3 测试 记得开启Redis服务器进行测试。退出登陆成功
3.4、注册 关于文件:
由于注册功能与登陆功能类似,我们借用登陆的LoginParams,借用LoginService写注册主逻辑,借用SysUserService写关于用户查询和用户保存相关逻辑。
关于id:
我们默认的id生成算法时分布式(雪花算法)。如果我们想实现逐步递增
,可以在sysuser的实体类id字段上添加注解@TableIdId(type = IdType.AUTO)
。但是此时我们不进行修改,就是用分布式。以后用户多了之后,要进行分表操作 id就需要使用分布式id。
注册功能实现思路:
判断参数是否有空值
判断账户是否已经存在(返回账户已经存在)
注册用户(调用SysUserService的save方法)
生成token (直接让登陆完的用户回主页)
将token存入redis
将token返回给前端路程
给LoginService接口类 加上事务注解 一旦中间出现任何问题 需要回滚  
3.4.1 接口说明 接口url:/register
请求方式:POST
请求参数:
参数名称
参数类型
说明
account
string
账号
password
string
密码
nickname
string
昵称
返回数据:
1 2 3 4 5 6 { "success" : true , "code" : 200 , "msg" : "success" , "data" : "token" }
3.4.2 编码 3.4.2.1 LoginParams 参数LoginParam中 添加新的参数nickname。
1 2 3 4 5 6 @Data public class LoginParams { private String account; private String password; private String nickname; }
3.4.2.2 RegisterController 借用登陆参数Vo来实现注册功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @RestController @RequestMapping("register") public class RegisterController { @Autowired private LoginService loginService; @PostMapping public Result register (@RequestBody LoginParams loginParams) { return loginService.register(loginParams); } }
3.4.2.3 LoginService 1 2 Result register (LoginParams loginParams) ;
3.4.2.4 LoginServiceImpl 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 @Override public Result register (LoginParams loginParams) { String account = loginParams.getAccount(); String password = loginParams.getPassword(); String nickname = loginParams.getNickname(); if (StringUtils.isBlank(account) || StringUtils.isBlank(password) || StringUtils.isBlank(nickname)){ return Result.fail(ErrorCode.PARAMS_ERROR.getCode(),ErrorCode.PARAMS_ERROR.getMsg()); } SysUser sysUser = this .sysUserService.findUserByAccount(account); if (sysUser != null ){ return Result.fail(ErrorCode.ACCOUNT_EXIST.getCode(),ErrorCode.ACCOUNT_EXIST.getMsg()); } sysUser = new SysUser (); sysUser.setNickname(nickname); sysUser.setAccount(account); sysUser.setPassword(DigestUtils.md5Hex(password+salt)); sysUser.setCreateDate(System.currentTimeMillis()); sysUser.setLastLogin(System.currentTimeMillis()); sysUser.setAvatar("/static/img/logo.b3a48c0.png" ); sysUser.setAdmin(1 ); sysUser.setDeleted(0 ); sysUser.setSalt("" ); sysUser.setStatus("" ); sysUser.setEmail("" ); this .sysUserService.save(sysUser); String token = JWTUtils.createToken(sysUser.getId()); redisTemplate.opsForValue().set("TOKEN_" +token, JSON.toJSONString(sysUser),1 , TimeUnit.DAYS); return Result.success(token); }
3.4.2.5 ErrorCode账号存在 1 ACCOUNT_EXIST(10004 ,"账号已存在" ),
3.4.2.6 SysUserService 1 2 3 4 5 SysUser findUserByAccount (String account) ;void save (SysUser sysUser) ;
3.4.2.7 SysUserServiceImpl 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @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) { this .sysUserMapper.insert(sysUser); }
3.4.2.8 事务注解 1 2 3 4 5 6 7 8 9 10 @Transactional public interface LoginService { Result login (LoginParams loginParams) ; Result logout (String token) ; Result register (LoginParams loginParams) ; }
3.4.3 测试 打开三个服务器进行测试。
3.5、优化-滚去登陆 初始的登录逻辑时将登陆验证放在每一个文件中,利用代码进行判断。可如果登陆的逻辑发生了改变,我们岂不是要每一个代码都要发生改变?
所以我们要进行统一登陆判断,使用拦截器进行登录拦截。遇到需要登陆才能访问的接口,我们的拦截器将会进行登陆验证,如果没登陆,会把登陆页面交给你,让你滚去登录!
我们在SpringMVC中有一个功能叫拦截器
。
总的思路:
配置拦截器interceptor
在拦截器中添加日志(方便查看相关信息)
在WebMvcConfig中对拦截器进行具体的路径配置
拦截器具体思路:
需要判断 请求的接口路径 是否为 HandlerMethod(controller方法),如果是访问静态资源的RequestResourceHandler,应该放行
判断 token 是否为空(如果为空:未登录)
如果token不为空:进行登陆验证checkToken)
如果验证成功 运行即可
关于拦截路径:
由于我们的日志系统读文章不需要什么用户权限,所以我们现在知识对test路径下的controller方法进行限制
1 2 3 4 5 6 7 @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns("/test" ); }
如果是其他日志系统,我们可以进行全局封存并进行部分放行(如登陆页面的放行)
1 2 3 4 5 6 7 8 9 10 11 12 @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns("/**" ) .excludePathPatterns("/login" ) .excludePathPatterns("/register" ); }
3.5.1 拦截器实现(添加日志) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 @Component @Slf4j public class LoginInterceptor implements HandlerInterceptor { @Autowired private LoginService loginService; @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)){ return true ; } String token = request.getHeader("Authorization" ); log.info("=================request start===========================" ); String requestURI = request.getRequestURI(); log.info("request uri:{}" ,requestURI); log.info("request method:{}" ,request.getMethod()); log.info("token:{}" , token); log.info("=================request end===========================" ); if (StringUtils.isBlank(token)){ Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(),"未登录" ); response.setContentType("application/json;charset=utf-8" ); response.getWriter().print(JSON.toJSONString(result)); return false ; } SysUser sysUser = loginService.checkToken(token); if (sysUser == null ){ Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(),"未登录" ); response.setContentType("application/json;charset=utf-8" ); response.getWriter().print(JSON.toJSONString(result)); return false ; } return true ; } }
3.5.2在WebMvcConfig中注册拦截器 1 2 3 4 5 6 7 @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns("/test" ); }
3.5.3 测试 token属于头部信息,需要使用API调试工具进行测试,直接在浏览器访问不可以。
而且我们不能对此博客系统进行全面封禁 ,因为本项目前端设置的请求文章列表和标签的时候,头部不会携带token信息。拦截器从头部中获取不到token,自然就不能通过拦截器。
1、设置TestController
2、头部未携带token进行访问时(失败):
3、携带token(一定要放在头部里!!!!!)进行访问(访问成功):
但是我们现在出现了一个问题:如果我们将全局进行封锁,只放行login和register。我们获取不到众多标签和文章,原因是我们的请求头中没有携带token
3.6、优化-ThreadLocal保存用户信息 我们现在,在登陆了之后,如果我们想直接从前端获取当前用户的信息
(注意:获取用户信息是获取数据库中存储的对象,并不是获取token),我们该怎么办呢?
我们选择使用ThreadLocal技术进行数据存储。此方法的原理涉及多线程。简言之就是:往一个线程里面存数据,这个数据只能由这个线程获取,其他线程无法获取这个数据
3.6.1 ThreadLocal工具类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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(); } }
3.6.2 拦截器配置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 @Component @Slf4j public class LoginInterceptor implements HandlerInterceptor { @Autowired private LoginService loginService; @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)){ return true ; } String token = request.getHeader("Authorization" ); log.info("=================request start===========================" ); String requestURI = request.getRequestURI(); log.info("request uri:{}" ,requestURI); log.info("request method:{}" ,request.getMethod()); log.info("token:{}" , token); log.info("=================request end===========================" ); if (StringUtils.isBlank(token)){ Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), "未登录" ); response.setContentType("application/json;charset=utf-8" ); response.getWriter().print(JSON.toJSONString(result)); return false ; } SysUser sysUser = loginService.checkToken(token); if (sysUser == null ){ Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), "未登录" ); response.setContentType("application/json;charset=utf-8" ); response.getWriter().print(JSON.toJSONString(result)); return false ; } UserThreadLocal.put(sysUser); return true ; } @Override public void afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserThreadLocal.remove(); } }
3.6.3 TestController 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.mszlu.blog.controller;import com.mszlu.blog.dao.pojo.SysUser;import com.mszlu.blog.utils.UserThreadLocal;import com.mszlu.blog.vo.Result;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController @RequestMapping("test") public class TestController { @RequestMapping public Result test () { SysUser sysUser = UserThreadLocal.get(); System.out.println(sysUser); return Result.success(null ); } }
3.6.4 断点测试 获取用户信息成功
4、文章-相关内容 4.1、文章-查看具体内容 大致流程:
我们想要查看单个文章的具体内容,需要同时获得文章的详细内容、文章的标签列表、文章的分类信息,并将以上信息同时封装进一个Vo中。
详细思路:
数据库添加文章内容表
、分类表
添加文章内容
和分类表
的实体类
写Controller,将前端获取到的文章id,将id传到Service以获取文章的全部信息
我们现在Service中调用一个总方法,此方法调用copy方法
我们决定需要在三个Vo用来放置参数(将三个Vo全部封装于ArticleVo中进行返回)
文章标签列表(之前写过)
文章body(文章详细内容)
文章分类Vo(分类id,分类的图标,分类的名称)
先模仿前面的Article对象copy转化成ArticleVo写一个逻辑demo
先通过id获取文章的基础信息(article表)
我们之前获取过文章对应的tag_list,此处可以直接调用方法
通过article表中的body_id对文章的具体内容进行查找
articleBody可以直接在ArticleService进行查询,并直接将值取过来进行返回(articleBody_id→content→转移给ArticleBodyVo)
通过article表中的category_id对文章的分类内容进行对应查找
category则需要在CategoryService中新建方法进行查询。(写一个空mapper→service→将查询到的直接copy到Vo中)
对上面的copyList方法进行一个重载
,原来的用于展示文章列表,新加的里面对Body和分类都进行了判断与内容添加
关于复制属性:
如果属性不是完全对应,可以使用articleBodyVo.setContent(articleBody.getContent());
方法对单个属性进行copy
如果属性完全对应(真的完全对应吗?)可以使用BeanUtils.copyProperties(A,B)
方法,将A中的属性完全复制到B中。
4.1.1 接口说明 接口url:/articles/view/{id}
请求方式:POST
请求参数:
参数名称
参数类型
说明
id
long
文章id(路径参数)
返回数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 { "success" : true , "code" : 200 , "msg" : "success" , "data" : { "id" : 1 , "title" : "springboot介绍以及入门案例" , "summary" : "通过Spring Boot实现的服务,只需要依靠一个Java类,把它打包成jar,并通过`java -jar`命令就可以运行起来。\r\n\r\n这一切相较于传统Spring应用来说,已经变得非常的轻便、简单。" , "commentCounts" : 16 , "viewCounts" : 125 , "weight" : 0 , "createDate" : "2610-10-10 15:10" , "author" : "Wahoyu" , "body" : { "content" : "# 1. Spring Boot介绍\r\n\r\n## 1.1 简介\r\n\r\n在您第1次接触和学习Spring框架的时候,是否因为其繁杂的配置而退却了?\r\n\r\n在你第n次使用Spring框架的时候,是否觉得一堆反复黏贴的配置有一些厌烦?\r\n\r\n那么您就不妨来试试使用Spring Boot来让你更易上手,更简单快捷地构建Spring应用!\r\n\r\nSpring Boot让我们的Spring应用变的更轻量化。\r\n\r\n我们不必像以前那样繁琐的构建项目、打包应用、部署到Tomcat等应用服务器中来运行我们的业务服务。\r\n\r\n通过Spring Boot实现的服务,只需要依靠一个Java类,把它打包成jar,并通过`java -jar`命令就可以运行起来。\r\n\r\n这一切相较于传统Spring应用来说,已经变得非常的轻便、简单。\r\n\r\n**总结一下Spring Boot的主要优点:**\r\n\r\n1. 为所有Spring开发者更快的入门\r\n2. 开箱即用,提供各种默认配置来简化项目配置\r\n3. 内嵌式容器简化Web项目\r\n4. 没有冗余代码生成和XML配置的要求\r\n5. 统一的依赖管理\r\n6. 自动装配,更易使用,更易扩展\r\n\r\n## 1.2 使用版本说明\r\n\r\nSpringboot版本:使用最新的2.5.0版本\r\n\r\n教程参考了官方文档进行制作,权威。\r\n\r\n其他依赖版本:\r\n\r\n\t1. Maven 需求:3.5+\r\n\r\n \t2. JDK 需求 8+\r\n \t3. Spring Framework 5.3.7以上版本\r\n \t4. Tomcat 9.0\r\n \t5. Servlet版本 4.0 但是可以部署到Servlet到3.1+的容器中\r\n\r\n# 2. 快速入门\r\n\r\n快速的创建一个Spring Boot应用,并且实现一个简单的Http请求处理。通过这个例子对Spring Boot有一个初步的了解,并体验其结构简单、开发快速的特性。\r\n\r\n教程使用的Idea版本:2019.3\r\n\r\n## 2.1 创建基础项目\r\n\r\n**第一步:** 创建maven项目\r\n\r\npom.xml :\r\n\r\n~~~xml\r\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\r\n xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\r\n xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd\">\r\n <modelVersion>4.0.0</modelVersion>\r\n\r\n <groupId>com.xiaopizhu</groupId>\r\n <artifactId>helloSpringBoot</artifactId>\r\n <version>1.0-SNAPSHOT</version>\r\n\t<!--springboot的父工程其中定义了常用的依赖,并且无依赖冲突-->\r\n <parent>\r\n <groupId>org.springframework.boot</groupId>\r\n <artifactId>spring-boot-starter-parent</artifactId>\r\n <version>2.5.0</version>\r\n </parent>\r\n</project>\r\n~~~\r\n\r\n注意上方的parent必须加,其中定义了springboot官方支持的n多依赖,基本上常用的已经有了,所以接下来导入依赖的时候,绝大部分都可以不加版本号。\r\n\r\n此时的工程结构为:\r\n\r\n\r\n\r\n**第二步:** 添加web依赖\r\n\r\n~~~xml\r\n<dependencies>\r\n <dependency>\r\n <groupId>org.springframework.boot</groupId>\r\n <artifactId>spring-boot-starter-web</artifactId>\r\n </dependency>\r\n</dependencies>\r\n~~~\r\n\r\n添加上方的web依赖,其中间接依赖了spring-web,spring-webmvc,spring-core等spring和springmvc的包,并且集成了tomcat。\r\n\r\n**第三步:** 编写启动类\r\n\r\n~~~java\r\npackage com.xiaopizhu.springboot;\r\n\r\nimport org.springframework.boot.SpringApplication;\r\nimport org.springframework.boot.autoconfigure.SpringBootApplication;\r\n\r\n@SpringBootApplication\r\npublic class HelloApp {\r\n\r\n public static void main(String[] args) {\r\n SpringApplication.run(HelloApp.class,args);\r\n }\r\n}\r\n\r\n~~~\r\n\r\n@SpringBootApplication注解标识了HelloApp为启动类,也是Spring Boot的核心。\r\n\r\n**第四步:** 运行启动类的main方法\r\n\r\n\r\n\r\n看到如上配置,证明启动成功,tomcat端口号默认为8080。\r\n\r\n**第五步:** 如果想要修改端口号,可以在resources目录下新建application.properties\r\n\r\n~~~properties\r\nserver.port=8082\r\n~~~\r\n\r\n**第六步:** 重新运行\r\n\r\n\r\n\r\n此时的项目结构为:\r\n\r\n\r\n\r\n**src/main/java :** 编写java代码,注意启动类需要放在项目的根包下。\r\n\r\n**src/main/resources:** 放置资源的目录,比如springboot的配置文件,静态文件,mybatis配置,日志配置等。\r\n\r\n**src/test/java:** 测试代码\r\n\r\n## 2.2 编写一个Http接口\r\n\r\n**第一步:**创建`HelloController`类,内容如下:\r\n\r\n~~~java\r\npackage com.xiaopizhu.springboot.controller;\r\n\r\nimport org.springframework.web.bind.annotation.GetMapping;\r\nimport org.springframework.web.bind.annotation.RequestMapping;\r\nimport org.springframework.web.bind.annotation.RestController;\r\n\r\n@RestController\r\n@RequestMapping(\"hello\")\r\npublic class HelloController {\r\n\r\n @GetMapping(\"boot\")\r\n public String hello(){\r\n return \"hello spring boot\";\r\n }\r\n\r\n}\r\n\r\n~~~\r\n\r\n**注意包名,必须在启动类所在的包名下。**\r\n\r\n**第二步:**重启程序,使用postman或者直接在浏览器输入http://localhost:8082/hello/boot\r\n\r\n得到结果:hello spring boot\r\n\r\n## 2.3 编写单元测试用例\r\n\r\n**第一步:**添加spring boot测试依赖\r\n\r\n~~~xml\r\n\t\t<dependency>\r\n <groupId>org.springframework.boot</groupId>\r\n <artifactId>spring-boot-starter-test</artifactId>\r\n <scope>test</scope>\r\n </dependency>\r\n~~~\r\n\r\n**第二步:**在src/test 下,编写测试用例\r\n\r\n~~~java\r\npackage com.xiaopizhu.springboot.controller;\r\n\r\nimport org.junit.jupiter.api.BeforeAll;\r\nimport org.junit.jupiter.api.BeforeEach;\r\nimport org.junit.jupiter.api.Test;\r\nimport org.springframework.boot.test.context.SpringBootTest;\r\nimport org.springframework.http.MediaType;\r\nimport org.springframework.test.web.servlet.MockMvc;\r\nimport org.springframework.test.web.servlet.request.MockMvcRequestBuilders;\r\nimport org.springframework.test.web.servlet.setup.MockMvcBuilders;\r\n\r\nimport static org.hamcrest.Matchers.equalTo;\r\nimport static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;\r\nimport static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;\r\n\r\n@SpringBootTest\r\npublic class TestHelloController {\r\n\r\n private MockMvc mockMvc;\r\n\r\n @BeforeEach\r\n public void beforeEach(){\r\n mockMvc = MockMvcBuilders.standaloneSetup(new HelloController()).build();\r\n }\r\n @Test\r\n public void testHello() throws Exception {\r\n mockMvc.perform(MockMvcRequestBuilders.get(\"/hello/boot\")\r\n .accept(MediaType.APPLICATION_JSON))\r\n .andExpect(status().isOk())\r\n .andExpect(content().string(equalTo(\"hello spring boot\")));\r\n }\r\n}\r\n\r\n~~~\r\n\r\n上面的测试用例,是构建一个空的`WebApplicationContext`,并且在before中加载了HelloController,得以在测试用例中mock调用,模拟请求。\r\n\r\n## 2.4 打包为jar运行\r\n\r\n**第一步:**添加打包(maven构建springboot)插件\r\n\r\n~~~xml\r\n <build>\r\n <plugins>\r\n <plugin>\r\n <groupId>org.springframework.boot</groupId>\r\n <artifactId>spring-boot-maven-plugin</artifactId>\r\n </plugin>\r\n </plugins>\r\n </build>\r\n~~~\r\n\r\n在idea的右侧 maven中,使用package来打包程序,打包完成后,在target目录下生成helloSpringBoot-1.0-SNAPSHOT.jar\r\n\r\n\r\n\r\n**第二步:**打开cmd:找到jar对应的目录\r\n\r\n输入命令\r\n\r\n~~~shell\r\njava -jar helloSpringBoot-1.0-SNAPSHOT.jar\r\n~~~\r\n\r\n\r\n\r\n**第三步:**测试,使用postman或者直接在浏览器输入http://localhost:8082/hello/boot\r\n\r\n得到结果:hello spring boot\r\n\r\n## 2.5 查看jar包内容\r\n\r\n~~~shell\r\njar tvf helloSpringBoot-1.0-SNAPSHOT.jar\r\n~~~\r\n\r\n# 3. 小结\r\n\r\n1. 通过Maven构建了一个空白Spring Boot项目,再通过引入web模块实现了一个简单的请求处理。\r\n2. 通过修改配置文件,更改端口号\r\n3. 编写了测试用例\r\n4. 打包jar包运行\r\n\r\n" } , "tags" : [ { "id" : 1 , "tagName" : "springboot" } , { "id" : 2 , "tagName" : "spring" } , { "id" : 3 , "tagName" : "springmvc" } ] , "category" : { "id" : 2 , "avatar" : "/static/category/back.png" , "categoryName" : "后端" } } }
4.1.2 编码 4.1.2.1 数据库表 内容表
1 2 3 4 5 6 7 8 CREATE TABLE `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 ;
分类表
1 2 3 4 5 6 7 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 CHA RACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic ;
4.1.2.2 Entity 1 2 3 4 5 6 7 8 @Data public class ArticleBody { private Long id; private String content; private String contentHtml; private Long articleId; }
1 2 3 4 5 6 7 8 @Data public class Category { private Long id; private String avatar; private String categoryName; private String description; }
4.1.2.3 Controller 1 2 3 4 5 6 7 @PostMapping("view/{id}") public Result findArticleById (@PathVariable("id") Long articleId) { return articleService.findArticleById(articleId); }
4.1.2.4 梳理Vo之间的关系 我们之前也返回过ArticleVo,但是我们在其中加入了作者信息
和标签列表
。
现在我们对前端进行返回的是ArticleVo副本,里面添加了我们要加入的分类信息
和文章详细信息
。
4.1.2.7 Service与Copy重载
Contrller调用,用于展示文章详细信息的方法,并进行返回
1 2 3 4 5 6 7 8 9 10 11 12 13 @Override public Result findArticleById (Long articleId) { Article article = this .articleMapper.selectById(articleId); ArticleVo articleVo = copy(article,true ,true ,true ,true ); return Result.success(articleVo); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 authorId = article.getAuthorId(); articleVo.setAuthor(sysUserService.findUserById(authorId).getNickname()); } return articleVo; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @Autowired CategoryService categoryService;private ArticleVo copy (Article article , boolean isTag , boolean isAuthor,boolean isBody ,boolean isCategory) { ArticleVo articleVo = new ArticleVo (); BeanUtils.copyProperties(article,articleVo); articleVo.setCreateDate(new DateTime (article.getCreateDate()).toString("yyyy-MM-dd HH:MM" )); if (isTag) { Long articleId = article.getId(); articleVo.setTags(tagService.findTagsByArticleId(articleId)); } if (isAuthor){ Long authorId = article.getAuthorId(); articleVo.setAuthor(sysUserService.findUserById(authorId).getNickname()); } if (isBody){ Long bodyId = article.getBodyId(); articleVo.setBody(findArticleByBodyId(bodyId)); } if (isCategory){ Long categoryId = article.getCategoryId(); articleVo.setCategory(categoryService.findCategoryById(categoryId)); } return articleVo; }
ArticleBodyService借用ArticleService(将文章详细内容返回给第二版copy函数)
1 2 3 4 5 6 7 private ArticleBodyVo findArticleByBodyId (Long bodyId) { ArticleBody articleBody = articleBodyMapper.selectById(bodyId); ArticleBodyVo articleBodyVo = new ArticleBodyVo (); articleBodyVo.setContent(articleBody.getContent()); return articleBodyVo; }
CategoryService(将分类信息返回给第二版copy函数)
1 2 3 4 5 6 public interface CategoryService { CategoryVo findCategoryById (Long categoryId) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @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; } }
4.1.2.8 Mapper
1 2 3 @Mapper public interface ArticleBodyMapper extends BaseMapper <ArticleBody> { }
1 2 3 @Mapper public interface CategoryMapper extends BaseMapper <Category> { }
4.1.3 测试
4.2、阅读数更新-线程池(JUC) 我们看了文章之后,文章阅读数是不是应该增加!
我们第一时间想到的是在Service中添加操作,在查看完文章新增阅读数目。但是此处有问题,我们在查看完之后,本应该直接返回数据了
所以我们打算使用线程池:
使用线程池防止两个问题:
防止更新操作出现问题,导致后面阅读失败
减少部分等待更新的耗时
我们把更新操作扔到线程池中去执行,就和主线程不相关了。开启线程池的目的是为了防止更新操作出现问题,从而影响文章阅读,即使我们开启redis,也应该将redis的操作放在线程池中。
4.2.1、模拟不使用线程池 我们让主线程sleep五秒之后再进行活动,用来模拟我们原来的单线程更新操作。我们发现无论线程更新操作放在哪里,都不会影响我们最后返回ArticleVo的时间。我们打开页面之后过了五秒,页面的内容才开始加载。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Component public class ThreadService { public void updateArticleViewCount (ArticleMapper articleMapper , Article article) { try { Thread.sleep(5000 ); System.out.println("更新完成..." ); } catch (InterruptedException e) { e.printStackTrace(); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Autowired ThreadService threadService;@Override public Result findArticleById (Long articleId) { Article article = this .articleMapper.selectById(articleId); ArticleVo articleVo = copy(article,true ,true ,true ,true ); threadService.updateArticleViewCount(articleMapper,article); return Result.success(articleVo); }
4.2.2、线程池配置类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Configuration @EnableAsync public class ThreadPoolConfig { @Bean("taskExecutor") public Executor asyncServiceExecutor () { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor (); executor.setCorePoolSize(5 ); executor.setMaxPoolSize(20 ); executor.setQueueCapacity(Integer.MAX_VALUE); executor.setKeepAliveSeconds(60 ); executor.setThreadNamePrefix("码神之路博客项目" ); executor.setWaitForTasksToCompleteOnShutdown(true ); executor.initialize(); return executor; } }
4.2.3、模拟使用线程池 上面配置了线程池之后,此处加上注解,代表我们下面的操作将在线程池中进行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Component public class ThreadService { @Async("taskExecutor") public void updateArticleViewCount (ArticleMapper articleMapper , Article article) { try { Thread.sleep(5000 ); System.out.println("更新完成..." ); } catch (InterruptedException e) { e.printStackTrace(); } } }
4.2.4、真正的更新逻辑 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Component public class ThreadService { @Async("taskExecutor") public void updateArticleViewCount (ArticleMapper articleMapper , Article article) { int viewCounts = article.getViewCounts(); Article articleUpdate = new Article (); articleUpdate.setViewCounts(viewCounts +1 ); LambdaUpdateWrapper<Article> updateWrapper = new LambdaUpdateWrapper <>(); updateWrapper.eq(Article::getViewCounts,viewCounts); articleMapper.update(articleUpdate, updateWrapper); try { Thread.sleep(5000 ); System.out.println("更新完成..." ); } catch (InterruptedException e) { e.printStackTrace(); } } }
4.3、pojo-Bug修改 在我们使用AtricleVo对数据库表格进行数据更改时,我们只传入了View_Count,结果却导致我们我们的commentCounts和weight变成了0。这是因为在MybatisPlus中,只要我们传入的值不为null,那么就会在提交的时候传入默认值。然而int的默认值正是0。
解决办法就是将int都改成Integer,Integer的默认值是null。
并且将weight的默认值删除。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Data public class Article { public static final int Article_TOP = 1 ; public static final int Article_Common = 0 ; private Long id; private String title; private String summary; private Integer commentCounts; private Integer viewCounts; private Long authorId; private Long bodyId; private Long categoryId; private Integer weight; private Long createDate; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Data public class ArticleVo { 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; }
4.4、文章内-评论列表展示
思路:
在Controller中,我们获取了前端的文章id,调用commentService中的获取评论的总方法。
在此总方法中,我们通过level和文章id对评论进行了统一的筛查,获得了原生commentList
而后我们使用copyList方法,将原生commentList
转化成精修后的commentVoList
在copyList方法中,我们使用copy方法,对每个comment对象复制到commentVo对象中
使用copy方法对copyList中的每一个comment转化成commentVo
先将同类型的字段进行复制,然后再对子评论
和UserVo
进行操作
我们在copy方法中,会将一级评论者信息的vo进行返回
设置当前用户的作者信息和上一级的用户信息
获取children时,我们获取当前以及评论的id,再通过id和level对评论进行查找并调用copyLiat方法进行转换,再将commentVoList赋值类Children属性。
4.4.1 接口说明 接口url:/comments/article/{id}
请求方式:GET
请求参数:
参数名称
参数类型
说明
id
long
文章id(路径参数)
返回数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 { "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 } ] }
4.4.2 编码 4.4.2.1 数据库表 1 2 3 4 5 6 7 8 9 10 11 12 CREATE TABLE `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 ;
4.4.2.2 实体类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @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; }
4.4.2.3 Vos 1 2 3 4 5 6 7 8 9 10 11 12 @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; }
1 2 3 4 5 6 7 8 @Data public class UserVo { private String nickname; private String avatar; private Long id; }
4.4.2.4 Controller 1 2 3 4 5 6 7 8 9 10 11 12 13 @RestController @RequestMapping("comments") public class CommentsController { @Autowired private CommentsService commentsService; @GetMapping("article/{id}") public Result comments (@PathVariable("id") Long id) { return commentsService.commentsByArticle(id); } }
4.4.2.5 Service
1 2 3 4 public interface CommentsService { Result commentsByArticle (Long id) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 @Service public class CommentsServiceImpl implements CommentsService { @Autowired CommentMapper commentMapper; @Autowired SysUserService sysUserService; @Override public Result commentsByArticle (Long 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 = findCommentsByParentId(id); commentVo.setChildrens(commentVoList); } if (level > 1 ){ Long toUid = comment.getToUid(); UserVo toUserVo = this .sysUserService.findUserVoById(toUid); commentVo.setToUser(toUserVo); } return commentVo; } private List<CommentVo> findCommentsByParentId (Long id) { LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper <>(); queryWrapper.eq(Comment::getParentId,id); queryWrapper.eq(Comment::getLevel,2 ); return copyList(commentMapper.selectList(queryWrapper)); } }
1 2 UserVo findUserVoById (Long id) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @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("Wahoyu" ); } UserVo userVo = new UserVo (); BeanUtils.copyProperties(sysUser,userVo); return userVo; }
4.4.2.6 Mapper
1 2 3 @Mapper public interface CommentMapper extends BaseMapper <Comment> { }
1 2 3 @Mapper public interface SysUserMapper extends BaseMapper <SysUser> { }
4.4.3 测试
4.5、文章内-发表评论 4.5.1 接口说明 接口url:/comments/create/change
请求方式:POST
请求参数:
参数名称
参数类型
说明
articleId
long
文章id
content
string
评论内容
parent
long
父评论id
toUserId
long
被评论的用户id
返回数据:
1 2 3 4 5 6 { "success" : true , "code" : 200 , "msg" : "success" , "data" : null }
4.5.2 加入到登录拦截器中 1 2 3 4 5 6 @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns("/test" ).addPathPatterns("/comments/create/change" ); }
4.5.3 编码 4.5.3.1 Vo.param 1 2 3 4 5 6 7 8 9 10 11 @Data public class CommentParam { private Long articleId; private String content; private Long parent; private Long toUserId; }
4.5.3.2 Controller 1 2 3 4 5 6 @PostMapping("create/change") public Result comment (@RequestBody CommentParam commentParam) { return commentService.comment(commentParam); }
4.5.3.3 Service
1 Result comment (CommentParam commentParam) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @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 ); }
4.5.2.4 雪花精度损失 防止前端精度损失,把id转为string进行传输 分布式id 比较长,传到前端数值会有精度损失,必须转为string类型进行传输
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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 CommentVo { @JsonSerialize(using = ToStringSerializer.class) private Long id; private UserVo author; private String content; private List<CommentVo> childrens; private String createDate; private Integer level; private UserVo toUser; }
4.5.4 测试 前端不会自动刷新,需要自己进行刷新测试。
5、写文章与AOP日志 写文章需要 三个接口:
获取所有的tags供作者选择
获取所有的categorys供作者选择
发布文章
获取分类和标签信息比较简单,前面我们已经实现了获取tagList的过程,现在我们只要获取category就可以了。
获取分类流程的思路也是:findAll()方法→copyList()→copy()方法,其中并没有任何的复杂逻辑~
注意:获取分类和标签的方法
上面的地址@GetMapping
不能加地址,整个Controller的前缀就已经有了。
5.1. 获取所有文章分类 5.1.1 接口说明 接口url:/categorys
请求方式:GET
请求参数:
返回数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 { "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" : "编程语言" } ] }
5.1.2 Controller 1 2 3 4 5 6 7 8 9 10 11 12 13 @RestController @RequestMapping("categorys") public class CategoryController { @Autowired private CategoryService categoryService; @GetMapping public Result listCategory () { return categoryService.findAll(); } }
5.1.3 Service 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Override public Result findAll () { List<Category> categories = this .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; }
5.1.4 测试
5.2. 获取所有文章标签 5.2.1 接口说明 接口url:/tags
请求方式:GET
请求参数:
返回数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 { "success" : true , "code" : 200 , "msg" : "success" , "data" : [ { "id" : 5 , "tagName" : "springboot" } , { "id" : 6 , "tagName" : "spring" } , { "id" : 7 , "tagName" : "springmvc" } , { "id" : 8 , "tagName" : "11" } ] }
5.2.2 Controller 1 2 3 4 @GetMapping public Result findAll () { return tagService.findAll(); }
5.2.3 Service 1 2 3 4 5 @Override public Result findAll () { List<Tag> tags = this .tagMapper.selectList(new LambdaQueryWrapper <>()); return Result.success(copyList(tags)); }
5.2.4 测试
5.3. 发布文章
5.3.1 接口说明 接口url:/articles/publish
请求方式:POST
请求参数:
参数名称
参数类型
说明
id
long
文章id(编辑有值)
title
string
文章标题
summary
string
文章概述
body
object({content: “ww”, contentHtml: “ww↵”})
文章内容
category
{id: 2, avatar: “/category/back.png”, categoryName: “后端”}
文章类别
tags
[{id: 5}, {id: 6}]
文章标签
返回数据:
1 2 3 4 5 6 { "success" : true , "code" : 200 , "msg" : "success" , "data" : { "id" : 12232323 } }
5.3.2 登陆拦截器配置 当然登录拦截器中,需要加入发布文章的配置:
1 2 3 4 5 6 7 8 @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns("/test" ) .addPathPatterns("/comments/create/change" ) .addPathPatterns("/articles/publish" ); }
5.3.3 编码 5.3.2.1 Vo.Params 1 2 3 4 5 6 7 8 9 10 @Data public class ArticleParam { private Long id; private ArticleBodyParam body; private CategoryVo category; private String summary; private List<TagVo> tags; private String title; }
1 2 3 4 5 6 @Data public class ArticleBodyParam { private String content; private String contentHtml; }
5.3.2.2 Vos 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @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; }
5.3.2.3 Dos 1 2 3 4 5 6 7 @Data public class ArticleTag { private Long id; private Long articleId; private Long tagId; }
5.3.2.4 Controller 1 2 3 4 @PostMapping("publish") public Result publish (@RequestBody ArticleParam articleParam) { return articleService.publish(articleParam); }
5.3.2.5 Service 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 @Autowired ArticleTagMapper articleTagMapper;@Override @Transactional public Result publish (ArticleParam articleParam) { SysUser sysUser = UserThreadLocal.get(); Article article = new Article (); article.setAuthorId(sysUser.getId()); article.setCategoryId(articleParam.getCategory().getId()); article.setCreateDate(System.currentTimeMillis()); article.setCommentCounts(0 ); article.setSummary(articleParam.getSummary()); article.setTitle(articleParam.getTitle()); article.setViewCounts(0 ); article.setWeight(Article.Article_Common); article.setBodyId(-1L ); this .articleMapper.insert(article); List<TagVo> tags = articleParam.getTags(); if (tags != null ) { for (TagVo tag : tags) { ArticleTag articleTag = new ArticleTag (); articleTag.setArticleId(article.getId()); articleTag.setTagId(tag.getId()); this .articleTagMapper.insert(articleTag); } } ArticleBody articleBody = new ArticleBody (); articleBody.setContent(articleParam.getBody().getContent()); articleBody.setContentHtml(articleParam.getBody().getContentHtml()); articleBody.setArticleId(article.getId()); articleBodyMapper.insert(articleBody); article.setBodyId(articleBody.getId()); articleMapper.updateById(article); ArticleVo articleVo = new ArticleVo (); articleVo.setId(article.getId()); return Result.success(articleVo); }
5.3.2.6 Mapper 1 2 3 public interface ArticleTagMapper extends BaseMapper <ArticleTag> { }
5.3.4 测试
5.4. 使用AOP记录日志 我们使用AOP(面向切面编程)来记录日志,AOP可以在不改变原方法及基础上对方法进行增强。
5.4.1 创建一个注解 1 2 3 4 5 6 7 8 9 10 11 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface LogAnnotation { String module () default "" ; String operation () default "" ; }
5.4.2 开发AOP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 @Aspect @Component @Slf4j public class LogAspect { @Pointcut("@annotation(com.mszlu.blog.common.aop.LogAnnotation)") public void logPointCut () { } @Around("logPointCut()") public Object around (ProceedingJoinPoint point) throws Throwable { long beginTime = System.currentTimeMillis(); Object result = point.proceed(); long time = System.currentTimeMillis() - beginTime; recordLog(point, 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.operation()); 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); HttpServletRequest request = HttpContextUtils.getHttpServletRequest(); log.info("ip:{}" , IpUtils.getIpAddr(request)); log.info("excute time : {} ms" ,time); log.info("=====================log end================================" ); } }
5.4.3 工具类
1 2 3 4 5 6 public class HttpContextUtils { public static HttpServletRequest getHttpServletRequest () { return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public class IPUtils { private static Logger logger = LoggerFactory.getLogger(IPUtils.class); public static String getIpAddr (HttpServletRequest request) { String ip = null ; try { ip = request.getHeader("x-forwarded-for" ); if (StringUtils.isEmpty(ip) || "unknown" .equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP" ); } if (StringUtils.isEmpty(ip) || ip.length() == 0 || "unknown" .equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP" ); } if (StringUtils.isEmpty(ip) || "unknown" .equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_CLIENT_IP" ); } if (StringUtils.isEmpty(ip) || "unknown" .equalsIgnoreCase(ip)) { ip = request.getHeader("HTTP_X_FORWARDED_FOR" ); } if (StringUtils.isEmpty(ip) || "unknown" .equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } } catch (Exception e) { logger.error("IPUtils ERROR " , e); } if (StringUtils.isEmpty(ip) && ip.length() > 15 ) { if (ip.indexOf("," ) > 0 ) { ip = ip.substring(0 , ip.indexOf("," )); } } return ip; } }
5.4.4 测试 我们在Service方法上面添加日志注解,进行日志测试。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @LogAnnotation @Override public Result findArticleById (Long articleId) { Article article = this .articleMapper.selectById(articleId); ArticleVo articleVo = copy(article,true ,true ,true ,true ); threadService.updateArticleViewCount(articleMapper,article); return Result.success(articleVo); }
5.5. 文章标签 - Bug修复
ArticleMapper.xml中的时间格式没对上
把原来的时间提取格式
1 2 3 4 5 6 <select id ="listArchives" resultType ="com.blog.dos.Archives" > select year(create_date) as year,month(create_date) as month,count(*) as count from ms_article group by year,month</select >
转化成
1 2 3 4 5 6 <select id ="listArchives" resultType ="com.blog.dos.Archives" > 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</select >
5.6. 上传文章图片 原本的文字是小内容资源。如今要上传的图片是大资源,我们要对整体流量进行一定的考虑。
假如我们使用1M带宽的服务器,如果一个人访问的图片是100K,那么10个人就1000K ≈ 1M。带宽便被10个使用者占满了。如果还有第11个人来,便访问不到页面资源。想要解决这个办法,我们就要为图片资源额外增加一个图片服务器。
5.6.1 接口说明 接口url:/upload
请求方式:POST
请求参数:
参数名称
参数类型
说明
image
file
上传的文件名称
返回数据:
1 2 3 4 5 6 { "success" : true , "code" : 200 , "msg" : "success" , "data" : "https://static.mszlu.com/aa.png" }
5.6.2 编码 5.6.2.1 导入依赖 1 2 3 4 5 <dependency > <groupId > com.qiniu</groupId > <artifactId > qiniu-java-sdk</artifactId > <version > [7.7.0, 7.7.99]</version > </dependency >
5.6.2.2 Controller 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @RestController @RequestMapping("upload") public class UploadController { @Autowired private QiniuUtils qiniuUtils; @PostMapping public Result upload (@RequestParam("image") MultipartFile file) { String fileName = UUID.randomUUID().toString() + "." + StringUtils.substringAfterLast(file.getOriginalFilename(), "." ); boolean upload = qiniuUtils.upload(file, fileName); if (upload){ return Result.success(QiniuUtils.url + fileName); } return Result.fail(20001 ,"上传失败" ); } }
5.6.2.3 获取自己七牛云的一些参数
测试域名:rhf0n6ci3.hb***************
AccessKet:vPMtxZlbVHgqRF***************
AccessSecretKry:y8jFIlCwbdl6nW****************
5.6.2.4 properties配置七牛云属性 1 2 3 4 5 6 7 8 9 # 七牛云服务器配置 ## 上传文件总的最大值 spring.servlet.multipart.max-request-size=20MB ## 单个文件的最大值 spring.servlet.multipart.max-file-size=2MB ## 七牛ak qiniu.accessKey=vPMtxZlbVHgqRF*************** ## 七牛sk qiniu.accessSecretKey=y8jFIlCwbdl6nW****************
5.6.2.5 七牛云配置类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Component public class QiniuUtils { public static final String url = "http://*************clouddn.com/" ; @Value("${qiniu.accessKey}") private String accessKey; @Value("${qiniu.accessSecretKey}") private String accessSecretKey; public boolean upload (MultipartFile file,String fileName) { Configuration cfg = new Configuration (Region.huabei()); UploadManager uploadManager = new UploadManager (cfg); String bucket = "boo******25" ; 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 ; } }
5.6.5 测试 注意:
配置华南华北的函数(注意看七牛云的函数配置)
配置七牛云的域名
配置七牛云的仓库名
上传图片时候只能上传指定格式的图片
6、主页-其他页面 6.1. 查询所有的文章分类 6.1.1 接口说明 接口url:/categorys/detail
请求方式:GET
请求参数:
返回数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 { "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" : "好多语言,该学哪个?" } ] }
6.1.2 编码 6.1.2.1 Vo 1 2 3 4 5 6 7 8 9 10 11 12 import lombok.Data;@Data public class CategoryVo { private Long id; private String avatar; private String categoryName; private String description; }
6.1.2.2 Controller 1 2 3 4 @GetMapping("detail") public Result categoriesDetail () { return categoryService.findAllDetail(); }
6.1.2.3 Service 1 2 3 4 5 6 @Override public Result findAllDetail () { List<Category> categories = categoryMapper.selectList(new LambdaQueryWrapper <>()); return Result.success(copyList(categories)); }
6.1.3 测试
6.2. 查询所有的标签 6.2.1 接口说明 接口url:/tags/detail
请求方式:GET
请求参数:
返回数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 { "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" } ] }
6.2.2 编码 6.2.2.1 Vo 1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.mszlu.blog.vo;import lombok.Data;@Data public class TagVo { private Long id; private String tagName; private String avatar; }
6.2.2.3 Controller 1 2 3 4 @GetMapping("detail") public Result findAllDetail () { return tagService.findAllDetail(); }
6.2.2.4 Service 1 2 3 4 5 6 7 @Override public Result findAllDetail () { LambdaQueryWrapper<Tag> queryWrapper = new LambdaQueryWrapper <>(); List<Tag> tags = this .tagMapper.selectList(queryWrapper); return Result.success(copyList(tags)); }
6.2.3 测试
6.3. 某分类的全部文章 6.3.1 接口说明 接口url:/category/detail/{id}
请求方式:GET
请求参数:
参数名称
参数类型
说明
id
分类id
路径参数
返回数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 { "success" : true , "code" : 200 , "msg" : "success" , "data" : { "id" : 1 , "avatar" : "/static/category/front.png" , "categoryName" : "前端" , "description" : "前端是什么,大前端" } }
6.3.2 编码 6.3.2.1 Vo 1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.mszlu.blog.vo.params;import lombok.Data;@Data public class PageParams { private int page = 1 ; private int pageSize = 10 ; private Long categoryId; private Long tagId; }
6.3.2.2 Controller 1 2 3 4 5 @GetMapping("detail/{id}") public Result categoriesDetailById (@PathVariable("id") Long id) { return categoryService.categoriesDetailById(id); }
6.3.2.3 Service 1 2 3 4 5 6 @Override public Result categoriesDetailById (Long id) { Category category = categoryMapper.selectById(id); CategoryVo categoryVo = copy(category); return Result.success(categoryVo); }
6.3.2.4 ArticleService 实际上我们就是在展示文章列表,但是如果我们有分类id这个属性的话,我们就把这个属性进行一个筛选。在之前展示所有文章列表的情况下,展示分类id明确的id。
1 2 3 4 if (pageParams.getCategoryId() != null ) { queryWrapper.eq(Article::getCategoryId,pageParams.getCategoryId()); }
6.3.3 测试
6.4. 某标签的全部文章 6.4.1 接口说明 接口url:/tags/detail/{id}
请求方式:GET
请求参数:
参数名称
参数类型
说明
id
标签id
路径参数
返回数据:
1 2 3 4 5 6 7 8 9 10 11 12 { "success" : true , "code" : 200 , "msg" : "success" , "data" : { "id" : 5 , "tagName" : "springboot" , "avatar" : "/static/tag/java.png" } }
6.4.2 编码 6.4.2.1 Controller 1 2 3 4 @GetMapping("detail/{id}") public Result findDetailById (@PathVariable("id") Long id) { return tagService.findDetailById(id); }
6.4.2.2 Service 1 2 3 4 5 6 @Override public Result findDetailById (Long id) { Tag tag = tagMapper.selectById(id); TagVo copy = copy(tag); return Result.success(copy); }
6.4.2.3 修改ArticleService 此处我们要对所有文章列表的输出进行限制,限制某tag的文章才能够进行输出。开心的来讲,我们本来可以像category一样进行直接限制,皆大欢喜。但是我们突然发现了一个问题!我们的Article表中没有设置tag字段! 那么我们就只能进行多表查询:
我们先内嵌了一个查询,先根据tagId把所有的行(小对象)查出来形成一个对象列表。再循环将每一个行(对象)中的ArticleId单独提取出来,存放到列表中。最后的最后,千万别忘了我们这是在一个查询listArticle的wrapper中,我们获取到articleList之后,还要使用这个列表对Article的输出进行限制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @Override public Result listArticle (PageParams pageParams) { 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); } } 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); }
6.4.3 测试
6.5. 归档文章列表 归档页面的接口就是我们之前调用文章列表的接口,只不过它这个接口让我们多传了两个参数(year和month)。
在实体类中:
我们对月份进行了处理,如果月数只有一位的话,我们要在前面加上0。
在业务层和DAO层中:
因为我们要按照年和月去查询,所以我们要使用MySQL的一个函数,然而此函数MybatisPlus是不支持的,所以我们要自定义一个函数进行查询。
原本的listArticle:
我亲爱的逻辑,你是我在这个项目中写出的第一个复杂逻辑,经过不断的修改,你逐渐完善起来。可今天由于前端定义的重复接口,我即将要把你整个业务逻辑进行修改。在下面我将用漫长的逻辑说明笔记纪念你。
在原本的文章列表中,它主要包括:
获取前端传入的众多参数
通过前端的部分参数进行分页
通过时间和地位降序排列
如果我们要展示的是某分类的全部文章 ,或者某标签的全部文章 ,我们还可以对其进行条件限制 。
通过(当前页数,页表大小,查询条件) 生成文章页 -> 原生文章列表 -> 精修后文章列表
返回articleVoList
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 @Override public Result listArticle (PageParams pageParams) { Page<Article> page = new Page <>(pageParams.getPage(), pageParams.getPageSize()); LambdaQueryWrapper<Article> queryWrapper = new LambdaQueryWrapper <>(); queryWrapper.orderByDesc(Article::getCreateDate,Article::getWeight); 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); } } Page<Article> articlePage = articleMapper.selectPage(page, queryWrapper); List<Article> articleList = articlePage.getRecords(); List<ArticleVo> articleVoList = copyList(articleList,true ,true ); return Result.success(articleVoList); }
修改后的逻辑:
依旧是前端传入众多参数进入到
分页 逻辑留在Service中
!!将时间权重的降序 排列转移到了sql语句的最后
!!将某分类,某标签的文章列表逻辑,放在了sql语句的if函数中
!!将page(包含了当前页和页面大小),CategoryId,TagId,Year,Month传入mapper中进行查询 生成当前文章页对象。
剩下的和原来的逻辑相同:文章页 -> 原生文章列表 -> 精修后文章列表
返回articleVoList
6.5.1 接口说明 接口url:/articles
请求方式:POST
请求参数:
参数名称
参数类型
说明
year
string
年
month
string
月
返回数据:
1 2 3 4 5 6 7 8 { "success" : true , "code" : 200 , "msg" : "success" , "data" : [ 文章列表,数据同之前的文章列表接口] }
6.5.2 编码 6.5.2.1文章列表参数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @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; } }
6.5.2.2 ServiceImpl 1 2 3 4 5 6 @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 )); }
6.5.2.2 Mapper.xml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <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 > 1 = 1 <if test ="categoryId != null" > and category_id = #{categoryId} </if > <if test ="tagId != null" > and id in (select article_id from ms_article_tag where tag_id=#{tagId}) </if > <if test ="year != null and year.length>0 and month != null and month.length>0" > and ( FROM_UNIXTIME(create_date/1000,'%Y') = #{year} and FROM_UNIXTIME(create_date/1000,'%m') = #{month} ) </if > </where > order by weight desc , create_date desc</select >
7、后续优化 7.1、统一缓存处理 7.1.1 思路 我们提到优化最先想到的肯定是缓存方面的优化,内存的访问速度远远大于 磁盘的访问速度 (多少倍?至少是1000倍起)。所以我们一般情况下,第一步骤进行的优化,就是让他尽量的减少对磁盘的访问,尽可能让它从内存中拿取数据 。
对每一个接口进行优化肯定不太合适,那么我们接下来就是要想办法做一个统一缓存处理,做统一处理,我们自然而然就会想到了我们的AOP技术。
我们决定使用Annoation+AOP+Redis的方式进行缓存管理。
使用了此缓存处理会怎样,怎么开始,怎样直接结束?
统一缓存的原理是这样的:
自定义注解后,我们能遍地使用该注解
在AOP中
我们设置切点和通知可以让遍地的注解,成为我们逻辑的跑腿小弟
我们的业务主要逻辑:让前端发送页面请求时先不顾一切获取方法名+类名+传入参数+注解传入的name
,这几项进行加工就是我们规定好的key值。
当前端想要获取数据,我们就先用这个生成的key先进行查找,如果能查到,皆大欢喜,直接把你进行return。
如果没查到,那么不好意思,你没办法走捷径,必须访问磁盘。而且你回来路过的时候还得把你的数据留下~留着下次访问直接找到~
7.1.2 编码 7.1.2.1 注解自定义 注解也就是我们AOP当中的切点
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.blog.common.cache;@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Cache { long expire () default 1 * 60 * 1000 ; String name () default "" ; }
7.1.2.2 AOP配置类 定义一个切面,定义了我们的切点和通知的关系
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 @Aspect @Component @Slf4j public class CacheAspect { @Autowired private RedisTemplate<String, String> redisTemplate; @Pointcut("@annotation(com.blog.common.cache.Cache)") public void pt () {} @Around("pt()") public Object around (ProceedingJoinPoint pjp) { try { Signature signature = pjp.getSignature(); String className = pjp.getTarget().getClass().getSimpleName(); String methodName = signature.getName(); Class[] parameterTypes = new Class [pjp.getArgs().length]; Object[] args = pjp.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)) { params = DigestUtils.md5Hex(params); } Method method = pjp.getSignature().getDeclaringType().getMethod(methodName, parameterTypes); Cache annotation = method.getAnnotation(Cache.class); long expire = annotation.expire(); String name = annotation.name(); 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 = pjp.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 ,"系统错误" ); } }
7.1.3 测试 1 2 3 4 5 6 @PostMapping("hot") @Cache(expire = 5 * 60 * 1000,name = "hot_article") public Result hotArticle () { int limit = 5 ; return articleService.hotArticle(limit); }
8、admin模块以及Security集成 做一个后台,用SpringSecurity做一个权限系统(api模块是jwt进行密码验证,admin模块是使用Security进行安全控制)。有什么用呢?我们知道我们做出的软件,其实是面向三个方向。
我们前端页面中设置了用户管理
和权限管理
,我们这里主要对权限管理进行操作。此处也是Bug重灾区,因为权限
既是我们胡乱操作测试的对象,又是我们进行操作的权限判断依据。
①新建了blog-admin模块(后台管理系统)
②对权限(实现了增删改查)
③配置了Security登录功能。
④配置了不同用户的不同权限(将前端传入的权限与数据库具有的权限进行对比)
8.1. 搭建项目 8.1.1 新建maven工程 我们导入依赖并暂时关闭Security依赖,我们先通过上面的依赖,在后端做一个简单的增删改查功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 <dependencies > <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 > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-log4j2</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-aop</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-mail</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > <scope > test</scope > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 1.2.76</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-configuration-processor</artifactId > <optional > true</optional > </dependency > <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-lang3</artifactId > </dependency > <dependency > <groupId > commons-collections</groupId > <artifactId > commons-collections</artifactId > <version > 3.2.2</version > </dependency > <dependency > <groupId > commons-codec</groupId > <artifactId > commons-codec</artifactId > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.4.3</version > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > </dependency > <dependency > <groupId > joda-time</groupId > <artifactId > joda-time</artifactId > <version > 2.10.10</version > </dependency > </dependencies >
8.1.2 properties配置 application.properties:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 server.port=8889 spring.application.name=blog_admin #数据库的配置 # datasource spring.datasource.url=jdbc:mysql://localhost:3306/boot_blog?useUnicode=true&characterEncoding=UTF-8&serverTimeZone=UTC spring.datasource.username=root spring.datasource.password=123456 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver #mybatis-plus mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl mybatis-plus.global-config.db-config.table-prefix=ms_
mybatis-plus配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Configuration @MapperScan("com.mszlu.blog.admin.mapper") public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor () { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor (); interceptor.addInnerInterceptor(new PaginationInnerInterceptor ()); return interceptor; } }
8.1.3 创建启动类 1 2 3 4 5 6 7 8 @SpringBootApplication public class AdminApp { public static void main (String[] args) { SpringApplication.run(AdminApp.class,args); } }
启动成功
8.1.4 导入前端工程 我们的前端页面时Vue+ElementUI进行开发的。
放入resources下的static目录中,前端工程在资料中有
我们访问http://localhost:8889/pages/main.html ,打开前端管理页面。
8.1.5 新建表 管理员用户表
1 2 3 4 5 6 CREATE TABLE `ms_admin` ( `id` bigint (0 ) NOT NULL AUTO_INCREMENT, `username` varchar (255 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , `password` varchar (255 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic ;
权限表
1 2 3 4 5 6 7 CREATE TABLE `ms_permission` ( `id` bigint (0 ) NOT NULL AUTO_INCREMENT, `name` varchar (255 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , `path` varchar (255 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , `description` varchar (255 ) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL , PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic ;
管理员x权限表
1 2 3 4 5 6 CREATE TABLE `ms_admin_permission` ( `id` bigint (0 ) NOT NULL AUTO_INCREMENT, `admin_id` bigint (0 ) NOT NULL , `permission_id` bigint (0 ) NOT NULL , PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic ;
8.2. 权限的CRUD基础实现 我们对权限进行增删改查时(相当于学生x 图书的增删改查)。但是当我们要控制数据库的访问权限的时候,它既是我们进行判断的依据,优势我们进行修改的工具。(我的建议是,基础的展示和增删改查不要变动,因为这些既是图书又是权限;我们新建权限用来测试即可,他们只是普通的图书)
8.2.1 Vo+Params PageResult可放在Result中,用于返回展示列表给前端。
Param是接收前端传来的参数的实体类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @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 ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 @Data public class PageParam { private Integer currentPage; private Integer pageSize; private String queryString; }
用于给前端返回分页信息的封装类,此类被封装在Result中进行返回
1 2 3 4 5 6 7 8 9 @Data public class PageResult <T> { private List<T> list; private Long total; }
8.2.2 Entity(Permission) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Data public class Permission { @TableId(type = IdType.AUTO) private Long id; private String name; private String path; private String description; }
8.2.1 AdminController  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @RestController @RequestMapping("admin") public class AdminController { @Autowired private PermissionService permissionService; @PostMapping("permission/permissionList") public Result permissionList (@RequestBody PageParam pageParam) { return permissionService.listPermission(pageParam); } @PostMapping("permission/add") public Result add (@RequestBody Permission permission) { return permissionService.add(permission); } @PostMapping("permission/update") public Result update (@RequestBody Permission permission) { return permissionService.update(permission); } @GetMapping("permission/delete/{id}") public Result delete (@PathVariable("id") Long id) { return permissionService.delete(id); } }
8.2.2 PermissionService  1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 @Service public class PermissionServiceImpl implements PermissionService { @Autowired private PermissionMapper permissionMapper; @Override public Result listPermission (PageParam pageParam) { Page<Permission> page = new Page <>(pageParam.getCurrentPage(),pageParam.getPageSize()); LambdaQueryWrapper<Permission> queryWrapper = new LambdaQueryWrapper <>(); if (StringUtils.isNotBlank(pageParam.getQueryString())) { queryWrapper.eq(Permission::getName,pageParam.getQueryString()); } Page<Permission> permissionPage = this .permissionMapper.selectPage(page, queryWrapper); PageResult<Permission> pageResult = new PageResult <>(); pageResult.setList(permissionPage.getRecords()); pageResult.setTotal(permissionPage.getTotal()); return Result.success(pageResult); } @Override public Result add (Permission permission) { this .permissionMapper.insert(permission); return Result.success(null ); } @Override public Result update (Permission permission) { this .permissionMapper.updateById(permission); return Result.success(null ); } @Override public Result delete (Long id) { this .permissionMapper.deleteById(id); return Result.success(null ); } }
8.2.3 Mapper 1 2 3 4 @Mapper public interface PermissionMapper extends BaseMapper <Permission> { }
8.2.4 测试
8.3. Security集成-登陆实现 上面我们实现了权限表的增删改,现在我们就将Security集成进去,实现我们的权限管理系统
我们将需要拦截的接口都放在/admin/
下面,我们只需要对admin进行拦截即可,想要使用admin,必须先通过我们自定义的Service方法
8.3.1 添加依赖 1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-security</artifactId > </dependency >
8.3.2 Security配置类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public BCryptPasswordEncoder bCryptPasswordEncoder () { return new BCryptPasswordEncoder (); } public static void main (String[] args) { String mszlu = new BCryptPasswordEncoder ().encode("mszlu" ); System.out.println(mszlu); } @Override public void configure (WebSecurity web) throws Exception { super .configure(web); } @Override protected void configure (HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/css/**" ).permitAll() .antMatchers("/img/**" ).permitAll() .antMatchers("/js/**" ).permitAll() .antMatchers("/plugins/**" ).permitAll() .antMatchers("/admin/**" ).access("@authService.auth(request,authentication)" ) .antMatchers("/pages/**" ).authenticated() .and() .formLogin() .loginPage("/login.html" ) .loginProcessingUrl("/login" ) .usernameParameter("username" ) .passwordParameter("password" ) .defaultSuccessUrl("/pages/main.html" ) .failureUrl("/login.html" ) .permitAll() .and() .logout() .logoutUrl("/logout" ) .logoutSuccessUrl("/login.html" ) .permitAll() .and() .httpBasic() .and() .csrf().disable() .headers().frameOptions().sameOrigin(); } }
8.3.3 admin实体类 1 2 3 4 5 6 7 8 9 10 11 @Data public class Admin { @TableId(type = IdType.AUTO) private Long id; private String username; private String password; }
8.3.4 登陆功能Service 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Component @Slf4j public class SecurityUserService implements UserDetailsService { @Autowired private AdminService adminService; @Override public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException { log.info("username:{}" ,username); Admin adminUser = adminService.findAdminByUserName(username); if (adminUser == null ){ throw new UsernameNotFoundException ("用户名不存在" ); } ArrayList<GrantedAuthority> authorities = new ArrayList <>(); UserDetails userDetails = new User (username,adminUser.getPassword(), authorities); return userDetails; } public static void main (String[] args) { System.out.println(new BCryptPasswordEncoder ().encode("123456" )); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Service public class AdminService { @Autowired private AdminMapper adminMapper; @Autowired private PermissionMapper permissionMapper; public Admin findAdminByUserName (String username) { LambdaQueryWrapper<Admin> queryWrapper = new LambdaQueryWrapper <>(); queryWrapper.eq(Admin::getUsername,username).last("limit 1" ); Admin adminUser = adminMapper.selectOne(queryWrapper); return adminUser; } public List<Permission> findPermissionsByAdminId (Long adminId) { return permissionMapper.findPermissionsByAdminId(adminId); } }
1 2 3 4 @Mapper public interface AdminMapper extends BaseMapper <Admin> { }
1 2 3 4 5 6 @Mapper public interface PermissionMapper extends BaseMapper <Permission> { List<Permission> findPermissionsByAdminId (Long adminId) ; }
1 2 3 4 5 6 7 8 9 10 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace ="com.blog.admin.mapper.PermissionMapper" > <select id ="findPermissionsByAdminId" parameterType ="long" resultType ="com.mszlu.blog.admin.pojo.Permission" > select * from ms_permission where id in (select permission_id from ms_admin_permission where admin_id=#{adminId}) </select > </mapper >
8.3.5登录功能测试 登陆功能成功
8.4. 权限认证 根据权限中定义的权限来判断该用户是否能反过来对权限进行操作。(太生草了)
8.4.1 数据库表定义权限
🥕注意:此处的权限既是我们修改的对象,又是我们进行操作的依据
admin_permission(admin有资格进行修改,test没有)
8.4.2 Service 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 @Service @Slf4j public class AuthService { @Autowired private AdminService adminService; public boolean auth (HttpServletRequest request, Authentication authentication) { String requestURI = request.getRequestURI(); log.info("request url:{}" , requestURI); Object principal = authentication.getPrincipal(); if (principal == null || "anonymousUser" .equals(principal)){ return false ; } UserDetails userDetails = (UserDetails) principal; String username = userDetails.getUsername(); Admin admin = adminService.findAdminByUserName(username); if (admin == null ){ return false ; } if (admin.getId() == 1 ){ return true ; } List<Permission> permissions = adminService.findPermissionsByAdminId(admin.getId()); requestURI = StringUtils.split(requestURI,'?' )[0 ]; for (Permission permission : permissions) { if (requestURI.equals(permission.getPath())){ log.info("权限通过" ); return true ; } } return false ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class MySimpleGrantedAuthority implements GrantedAuthority { private String authority; private String path; public MySimpleGrantedAuthority () {} public MySimpleGrantedAuthority (String authority) { this .authority = authority; } public MySimpleGrantedAuthority (String authority,String path) { this .authority = authority; this .path = path; } @Override public String getAuthority () { return authority; } public String getPath () { return path; } }