SpringBoot+vue练手项目---博客系统

SpringBoot+vue练手项目---博客系统


项目使用技术 :

springboot + mybatisplus+redis+mysql+jwt

项目讲解:https://www.bilibili.com/video/BV1Gb4y1d7zb?p=1

1. 工程搭建


前端的工程地址:

链接:https://pan.baidu.com/s/1cg_11ctsbbq_WM9BnpcOaQ 提取码:nrun

npm install

npm run build

npm run dev

1.1 新建maven工程

pom.xml(blog-parent)

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 " target="_blank">http://maven.apache.org/xsd/maven-4.0.0.xsd">

4.0.0

com.jihu

blog-parent

1.0-SNAPSHOT

blog-api

pom

org.springframework.boot

spring-boot-starter-parent

2.5.0

UTF-8

UTF-8

1.8

com.alibaba

fastjson

1.2.76

commons-collections

commons-collections

3.2.2

com.baomidou

mybatis-plus-boot-starter

3.4.3

joda-time

joda-time

2.10.10

org.springframework.boot

spring-boot-maven-plugin

pom.xml(blog-api)

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 " target="_blank">http://maven.apache.org/xsd/maven-4.0.0.xsd">

blog-parent

com.jihu

1.0-SNAPSHOT

4.0.0

blog-api

org.springframework.boot

spring-boot-starter

org.springframework.boot

spring-boot-starter-logging

org.springframework.boot

spring-boot-starter-log4j2

org.springframework.boot

spring-boot-starter-aop

org.springframework.boot

spring-boot-starter-mail

org.springframework.boot

spring-boot-starter-web

org.springframework.boot

spring-boot-starter-test

test

org.springframework.boot

spring-boot-starter-data-redis

com.alibaba

fastjson

1.2.76

mysql

mysql-connector-java

org.springframework.boot

spring-boot-configuration-processor

true

org.apache.commons

commons-lang3

commons-collections

commons-collections

3.2.2

com.baomidou

mybatis-plus-boot-starter

3.4.3

org.projectlombok

lombok

joda-time

joda-time

2.10.10

1.2 application.yml

server:

port: 8888

spring:

application:

name: jihu

#数据库的配置

datasource:

url: jdbc:mysql://localhost:3306/blog?useUnicode=true&characterEncoding=UTF-8&serverTimeZone=UTC

username: root

password: 123456

driver-class-name: com.mysql.cj.jdbc.Driver

#mybatis-plus

mybatis-plus:

configuration:

log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #打印日志 如sql语句

global-config:

db-config:

table-prefix: ms_ #标识表的前缀为ms_

#指定mapper文件的位置

mybatis-plus:

config-location: classpath:mapper/*.xml

1.3 配置 分页 和跨域

分页

package com.jihu.blog.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;

import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;

import org.mybatis.spring.annotation.MapperScan;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

@Configuration

//扫包,将此包下的接口生成代理实现类,并且注册到spring容器中

@MapperScan("com.jihu.blog.mapper")

public class MybatisPlusConfig {

//分页插件

@Bean

public MybatisPlusInterceptor mybatisPlusInterceptor(){

MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

interceptor.addInnerInterceptor(new PaginationInnerInterceptor());

return interceptor;

}

}

跨域

package com.jihu.blog.config;

import org.springframework.context.annotation.Configuration;

import org.springframework.web.servlet.config.annotation.CorsRegistry;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration

public class WebMVCConfig implements WebMvcConfigurer {

//实现跨域请求

@Override

public void addCorsMappings(CorsRegistry registry) {

registry.addMapping("/**")

.allowedOriginPatterns("*")

.allowedMethods("GET","HEAD","POST","PUT","DELETE","OPTIONS")

.allowCredentials(true)

.maxAge(3600)

.allowedHeaders("*");

}

}

1.4启动类

package com.jihu.blog;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication

public class BlogApp {

public static void main(String[] args) {

SpringApplication.run(BlogApp.class,args);

}

}

2.首页-文章列表


2.1 接口说明

接口url:/articles

请求方式:POST

请求参数:

参数名称

参数类型

说明

page

int

当前页数

pageSize

int

每页显示的数量

返回数据:

{

"success": true,

"code": 200,

"msg": "success",

"data": [

{

"id": 1,

"title": "springboot介绍以及入门案例",

"summary": "通过Spring Boot实现的服务,只需要依靠一个Java类,把它打包成jar,并通过`java -jar`命令就可以运行起来。\r\n\r\n这一切相较于传统Spring应用来说,已经变得非常的轻便、简单。",

"commentCounts": 2,

"viewCounts": 54,

"weight": 1,

"createDate": "2609-06-26 15:58",

"author": "12",

"body": null,

"tags": [

{

"id": 5,

"avatar": null,

"tagName": "444"

},

{

"id": 7,

"avatar": null,

"tagName": "22"

},

{

"id": 8,

"avatar": null,

"tagName": "11"

}

],

"categorys": null

}

]

}

2.2 编码

Spring基于注解的开发每个注解的作用

2.2.1 表结构

文章表

CREATE TABLE `blog`.`ms_article` (

`id` bigint(0) NOT NULL AUTO_INCREMENT,

`comment_counts` int(0) NULL DEFAULT NULL COMMENT '评论数量',

`create_date` bigint(0) NULL DEFAULT NULL COMMENT '创建时间',

`summary` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '简介',

`title` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '标题',

`view_counts` int(0) NULL DEFAULT NULL COMMENT '浏览数量',

`weight` int(0) NOT NULL COMMENT '是否置顶',

`author_id` bigint(0) NULL DEFAULT NULL COMMENT '作者id',

`body_id` bigint(0) NULL DEFAULT NULL COMMENT '内容id',

`category_id` int(0) NULL DEFAULT NULL COMMENT '类别id',

PRIMARY KEY (`id`) USING BTREE

) ENGINE = InnoDB AUTO_INCREMENT = 25 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

标签表

id,文章id,标签id,通过文章id可以间接查到标签id

CREATE TABLE `blog`.`ms_tag` (

`id` bigint(0) NOT NULL AUTO_INCREMENT,

`article_id` bigint(0) NOT NULL,

`tag_id` bigint(0) NOT NULL,

PRIMARY KEY (`id`) USING BTREE,

INDEX `article_id`(`article_id`) USING BTREE,

INDEX `tag_id`(`tag_id`) USING BTREE

) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

用户表

CREATE TABLE `blog`.`ms_sys_user` (

`id` bigint(0) NOT NULL AUTO_INCREMENT,

`account` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '账号',

`admin` bit(1) NULL DEFAULT NULL COMMENT '是否管理员',

`avatar` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '头像',

`create_date` bigint(0) NULL DEFAULT NULL COMMENT '注册时间',

`deleted` bit(1) NULL DEFAULT NULL COMMENT '是否删除',

`email` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邮箱',

`last_login` bigint(0) NULL DEFAULT NULL COMMENT '最后登录时间',

`mobile_phone_number` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '手机号',

`nickname` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '昵称',

`password` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '密码',

`salt` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '加密盐',

`status` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '状态',

PRIMARY KEY (`id`) USING BTREE

) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

2.2.2 entity层

Article 文章实体类

package com.jihu.blog.dao.pojo;

import lombok.Data;

@Data

public class Article {

public static final int Article_TOP = 1;

public static final int Article_Common = 0;

private Long id;

private String title;

private String summary;

private int commentCounts;

private int viewCounts;

/**

* 作者id

*/

private Long authorId;

/**

* 内容id

*/

private Long bodyId;

/**

*类别id

*/

private Long categoryId;

/**

* 置顶

*/

private int weight = Article_Common;

/**

* 创建时间

*/

private Long createDate;

}

SysUser 用户实体类

package com.jihu.blog.dao.pojo;

import lombok.Data;

@Data

public class SysUser {

private Long id;

private String account;

private Integer admin;

private String avatar;

private Long createDate;

private Integer deleted;

private String email;

private Long lastLogin;

private String mobilePhoneNumber;

private String nickname;

private String password;

private String salt;

private String status;

}

Tag 标签实体类

package com.jihu.blog.dao.pojo;

import lombok.Data;

@Data

public class Tag {

private Long id;

private String avatar;

private String tagName;

}

2.2.3 Controller层

ArticleController

package com.jihu.blog.controller;

import com.jihu.blog.service.ArticleService;

import com.jihu.blog.vo.Result;

import com.jihu.blog.vo.params.PageParams;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.web.bind.annotation.RequestBody;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

@RestController

@RequestMapping("articles")

public class ArticleController {

@Autowired

private ArticleService articleService;

//首页 文章列表

@PostMapping

public Result listArticle(@RequestBody PageParams pageParams){

//ArticleVo 页面接收的数据

return articleService.listArticle(pageParams);

}

}

2.2.4 Service层

ArticleService

package com.jihu.blog.service;

import com.jihu.blog.vo.Result;

import com.jihu.blog.vo.params.PageParams;

public interface ArticleService {

Result listArticle(PageParams pageParams);

}

ArticleServiceImpl

package com.jihu.blog.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;

import com.baomidou.mybatisplus.core.metadata.IPage;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;

import com.jihu.blog.dao.mapper.ArticleMapper;

import com.jihu.blog.dao.mapper.TagMapper;

import com.jihu.blog.dao.pojo.Article;

import com.jihu.blog.service.ArticleService;

import com.jihu.blog.service.SysUserService;

import com.jihu.blog.service.TagService;

import com.jihu.blog.vo.ArticleVo;

import com.jihu.blog.vo.Result;

import com.jihu.blog.vo.params.PageParams;

import org.joda.time.DateTime;

import org.springframework.beans.BeanUtils;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Service;

import java.util.ArrayList;

import java.util.List;

@Service

public class ArticleServiceImpl implements ArticleService {

@Autowired

private ArticleMapper articleMapper;

@Autowired

private TagService tagService;

@Autowired

private SysUserService sysUserService;

@Override

public Result listArticle(PageParams pageParams) {

Page

page = new Page<>(pageParams.getPage(), pageParams.getPageSize());

LambdaQueryWrapper

queryWrapper = new LambdaQueryWrapper<>();

//是否置顶进行排序

queryWrapper.orderByDesc(Article::getWeight,Article::getCreateDate);

Page

articlePage = articleMapper.selectPage(page, queryWrapper);

List

records = articlePage.getRecords();

//能直接返回吗 肯定不行 所以需要进行如下转换

List articleVoList = copyList(records,true,true);

return Result.success(articleVoList);

}

private List copyList(List

records,boolean isTag,boolean isAuthor) {

List articleVoList = new ArrayList<>();

for (Article record : records) {

articleVoList.add(copy(record,isTag,isAuthor));

}

return articleVoList;

}

private ArticleVo copy(Article article,boolean isTag,boolean isAuthor){

ArticleVo articleVo = new ArticleVo();

BeanUtils.copyProperties(article,articleVo);

articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));

//并不是所有的接口,都需要标签,作者信息

if (isTag){

Long articleId = article.getId();

articleVo.setTags(tagService.findTagsByrticleId(articleId));

}

if (isAuthor){

Long authorId = article.getAuthorId();

articleVo.setAuthor(sysUserService.findUserById(authorId).getNickname());

}

return articleVo;

}

}

SysUserService

package com.jihu.blog.service;

import com.jihu.blog.dao.pojo.SysUser;

public interface SysUserService {

SysUser findUserById(Long id);

}

SysUserServiceImpl

package com.jihu.blog.service.impl;

import com.jihu.blog.dao.mapper.SysUserMapper;

import com.jihu.blog.dao.pojo.SysUser;

import com.jihu.blog.service.SysUserService;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Service;

@Service

public class SysUserServiceImpl implements SysUserService {

@Autowired

private SysUserMapper sysUserMapper;

@Override

public SysUser findUserById(Long id) {

SysUser sysUser = sysUserMapper.selectById(id);

//防止空指针出现 加一个判断

if (sysUser == null){

sysUser = new SysUser();

sysUser.setNickname("马神之路");

}

return sysUser;

}

}

TagService

package com.jihu.blog.service;

import com.jihu.blog.vo.TagVo;

import java.util.List;

public interface TagService {

List findTagsByrticleId(Long articleId);

}

TagServiceImpl

package com.jihu.blog.service.impl;

import com.jihu.blog.dao.mapper.TagMapper;

import com.jihu.blog.dao.pojo.Tag;

import com.jihu.blog.service.TagService;

import com.jihu.blog.vo.TagVo;

import org.springframework.beans.BeanUtils;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Service;

import java.util.ArrayList;

import java.util.List;

@Service

public class TagServiceImpl implements TagService {

@Autowired

private TagMapper tagMapper;

@Override

public List findTagsByrticleId(Long articleId) {

//mybatisplus 无法进行多表查询

List tags = tagMapper.findTagsByrticleId(articleId);

return copyList(tags);

}

private List copyList(List tags) {

List tagVoList = new ArrayList<>();

for (Tag tag : tags) {

tagVoList.add(copy(tag));

}

return tagVoList;

}

private TagVo copy(Tag tag) {

TagVo tagVo = new TagVo();

BeanUtils.copyProperties(tag,tagVo);

return tagVo;

}

}

2.2.5 Mapper层

ArticleMapper

package com.jihu.blog.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;

import com.jihu.blog.dao.pojo.Article;

//BaseMapper mybatisplus中提供的可以让我们很方便的查询这张表

public interface ArticleMapper extends BaseMapper

{

}

SysUserMapper

package com.jihu.blog.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;

import com.jihu.blog.dao.pojo.SysUser;

public interface SysUserMapper extends BaseMapper {

}

TagMapper

package com.jihu.blog.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;

import com.jihu.blog.dao.pojo.Tag;

import java.util.List;

public interface TagMapper extends BaseMapper {

/**

* 根据文章id 查询标签列表

* @param articleId

* @return

*/

List findTagsByrticleId(Long articleId);

}

TagMapper.xml

http://mybatis.org/dtd/mybatis-3-mapper.dtd">

id,avatar,tag_name as tagName

2.2.6 Vo层

Result(统一最后的结果)

package com.jihu.blog.vo;

import lombok.AllArgsConstructor;

import lombok.Data;

@Data

@AllArgsConstructor

public class Result {

private boolean success;

private int code ;

private String msg;

private Object data;

public static Result success(Object data) {

return new Result(true, 200, "success", data);

}

public static Result fail(int code,String msg) {

return new Result(false, code, msg, null);

}

}

ArticleVo 建立与前端交互的Vo文件

package com.jihu.blog.vo;

import lombok.Data;

import java.util.List;

//建立与前端交互的Vo文件

@Data

public class ArticleVo {

private Long id;

private String title;

private String summary;

private int commentCounts;

private int viewCounts;

private int weight;

/**

* 创建时间

*/

private String createDate;

private String author;

// private ArticleBodyVo body;

private List tags;

// private List categorys;

}

新建TagVo

package com.jihu.blog.vo;

import lombok.Data;

@Data

public class TagVo {

private Long id;

private String tagName;

}

新建PageParams

package com.jihu.blog.vo.params;

import lombok.Data;

@Data

public class PageParams {

private int Page =1; //当前页数

private int PageSize =10; //每页显示的数量

}

2.2.7 测试:

3.首页- 最热标签


3.1接口说明

接口url:/tags/hot

请求方式:GET

请求参数:无

id: 标签名称 ,我们期望点击标签关于文章的所有列表都显示出来

返回数据:

{

"success": true,

"code": 200,

"msg": "success",

"data": [

{

"id":1,

"tagName":"4444"

}

]

}

3.2编码

3.2.1Controller层

package com.jihu.blog.controller;

import com.jihu.blog.service.TagService;

import com.jihu.blog.vo.Result;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

@RestController

@RequestMapping("tags")

public class TagsController {

@Autowired

private TagService tagService;

@GetMapping("hot")

public Result hot(){

int limit = 6;

return tagService.hots(limit);

}

}

3.2.2 Service层

建立service接口

TagService

package com.jihu.blog.service;

import com.jihu.blog.vo.Result;

import com.jihu.blog.vo.TagVo;

import java.util.List;

public interface TagService {

Result hots(int limit);

}

建立serviceimpl实现类

TagServiceImpl

@Service

public class TagServiceImpl implements TagService {

@Autowired

private TagMapper tagMapper;

@Override

public Result hots(int limit) {

/*

1.标签所拥有的文章数量最多 即为最热标签

2. 查询 根据tag_id 分组 计数,从大到小 排列 取前 limit个

*/

List tagIds = tagMapper.findHotsIds(limit);

//判断一下是否为空

if (tagIds == null){

return Result.success(Collections.emptyList());

}

//需求的是 tagId 和 tagName tag对象

//需要的是这样的一个sql语句 select * from tag where id in (1,2,3...)

List tagList = tagMapper.findTagdByTagIds(tagIds);

return Result.success(tagList);

}

}

3.2.3 Mapper层

TagMapper

package com.jihu.blog.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;

import com.jihu.blog.dao.pojo.Tag;

import java.util.List;

public interface TagMapper extends BaseMapper {

/**

* 查询最热的标签 前limit条

* @param limit

* @return

*/

List findHotsIds(int limit);

/*

根据最热标签查询 最热文章名字

*/

List findTagdByTagIds(List tagIds);

}

TagMapper

http://mybatis.org/dtd/mybatis-3-mapper.dtd">

id,avatar,tag_name as tagName

3.2.4 测试

4.统一异常处理


不管是controller层还是service,dao层,都有可能报异常,如果是预料中的异常,可以直接捕获处理,如果是意料之外的异常,需要统一进行处理,进行记录,并给用户提示相对比较友好的信息。

AllExceptionHandler

package com.jihu.blog.handler;

import com.jihu.blog.vo.Result;

import org.springframework.web.bind.annotation.ControllerAdvice;

import org.springframework.web.bind.annotation.ExceptionHandler;

import org.springframework.web.bind.annotation.ResponseBody;

//对加了 @Controller 注解方法进行拦截处理 AOP的实现

@ControllerAdvice

public class AllExceptionHandler {

//进行异常处理, 处理Exception.class的异常

@ExceptionHandler(Exception.class)

@ResponseBody //返回json数据

public Result doException(Exception ex){

ex.printStackTrace();

return Result.fail(-999,"系统异常");

}

}

5.首页-最热文章


5.1 接口说明

接口url:/articles/hot

请求方式:POST

请求参数:无

返回数据:

{

"success": true,

"code": 200,

"msg": "success",

"data": [

{

"id": 1,

"title": "springboot介绍以及入门案例",

},

{

"id": 9,

"title": "Vue.js 是什么",

},

{

"id": 10,

"title": "Element相关",

}

]

}

5.2 Controller层

ArticleController

@RestController

@RequestMapping("articles")

public class ArticleController {

@Autowired

private ArticleService articleService;

//首页 最热文章

@PostMapping("hot")

public Result hotArticle(){

int limit = 5; //取前5条

return articleService.hotArticle(limit);

}

}

5.3 Service层

ArticleService

package com.jihu.blog.service;

import com.jihu.blog.vo.Result;

import com.jihu.blog.vo.params.PageParams;

public interface ArticleService {

Result listArticle(PageParams pageParams);

Result hotArticle(int limit);

}

ArticleServiceImpl

@Service

public class ArticleServiceImpl implements ArticleService {

@Autowired

private ArticleMapper articleMapper;

/**

* 最热文章查询

* @param limit

* @return

*/

@Override

public Result hotArticle(int limit) {

LambdaQueryWrapper

lambdaQueryWrapper = new LambdaQueryWrapper<>();

lambdaQueryWrapper.orderByDesc(Article::getViewCounts);

lambdaQueryWrapper.select(Article::getId,Article::getTitle);

lambdaQueryWrapper.last("limit "+ limit);

//SELECT id, title from ms_article ORDER BY view_counts DESC limit 5

List

articles = articleMapper.selectList(lambdaQueryWrapper);

return Result.success(copyList(articles,false,false));

}

}

5.4测试

6.首页-最新文章


和最热文章非常类似,一个是根据浏览量来选择,一个是根据最新创建时间来选择

6.1 接口说明

接口url:/articles/new

请求方式:POST

请求参数:无

返回数据:

{

"success": true,

"code": 200,

"msg": "success",

"data": [

{

"id": 1,

"title": "springboot介绍以及入门案例",

},

{

"id": 9,

"title": "Vue.js 是什么",

},

{

"id": 10,

"title": "Element相关",

}

]

}

6.2 Controller层

在com.jihu.blog.controller.ArticleController中添加

//首页 最新文章

@PostMapping("new")

public Result newArticle(){

int limit = 5; //取前5条

return articleService.newArticle(limit);

}

6.3ArticleService

Result newArticle(int limit);

6.4ArticleServiceImpl

/**

* 最新文章查询

* @param limit

* @return

*/

@Override

public Result newArticle(int limit) {

LambdaQueryWrapper

lambdaQueryWrapper = new LambdaQueryWrapper<>();

lambdaQueryWrapper.orderByDesc(Article::getCreateDate);

lambdaQueryWrapper.select(Article::getId,Article::getTitle);

lambdaQueryWrapper.last("limit "+limit);

//SELECT id, title from ms_article ORDER BY create_data DESC limit 5

List

articles = articleMapper.selectList(lambdaQueryWrapper);

return Result.success(copyList(articles,false,false));

}

6.5测试

7.首页-文章归档


每一篇文章根据创建时间某年某月发表多少篇文章

7.1接口说明

接口url:/articles/listArchives

请求方式:POST

请求参数:无

返回数据:

{

"success": true,

"code": 200,

"msg": "success",

"data": [

{

"year": "2021",

"month": "6",

"count": 2

}

]

}

7.2 Controller层

com.jihu.blog.controller.ArticleController

//首页 文章归档

@PostMapping("listArchives")

public Result listArchives(){

return articleService.listArchives();

}

7.3 ArticleService

Result listArchives();

7.4 ArticleServiceImpl

//文章归档

@Override

public Result listArchives() {

List archivesList = articleMapper.listArchives();

return Result.success(archivesList);

}

7.5 ArticleMapper

package com.jihu.blog.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;

import com.jihu.blog.dao.dos.Archives;

import com.jihu.blog.dao.pojo.Article;

import com.jihu.blog.vo.Result;

import java.util.List;

//BaseMapper mybatisplus中提供的可以让我们很方便的查询这张表

public interface ArticleMapper extends BaseMapper

{

List listArchives();

}

7.6 ArticleMapper.xml

http://mybatis.org/dtd/mybatis-3-mapper.dtd">

7.7测试

8.登录


8.1 接口说明

接口url:/login

请求方式:POST

请求参数:

参数名称

参数类型

说明

account

string

账号

password

string

密码

返回数据:

{

"success": true,

"code": 200,

"msg": "success",

"data": "token"

}

8.2 JWT

登录使用JWT技术。

jwt 可以生成 一个加密的token,做为用户登录的令牌,当用户登录成功之后,发放给客户端。

请求需要登录的资源或者接口的时候,将token携带,后端验证token是否合法。

jwt 有三部分组成:A.B.C

A:Header,{“type”:“JWT”,“alg”:“HS256”} 固定

B:playload,存放信息,比如,用户id,过期时间等等,可以被解密,不能存放敏感信息

C: 签证,A和B加上秘钥 加密而成,只要秘钥不丢失,可以认为是安全的。

jwt 验证,主要就是验证C部分 是否合法。

导入依赖包

依赖包:

io.jsonwebtoken

jjwt

0.9.1

JWTUtils(工具类):

com.jihu.blog.utils.JWTUtils

package com.jihu.blog.utils;

import io.jsonwebtoken.Jwt;

import io.jsonwebtoken.JwtBuilder;

import io.jsonwebtoken.Jwts;

import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;

import java.util.HashMap;

import java.util.Map;

public class JWTUtils {

private static final String jwtToken = "123456Mszlu!@#$$";

public static String createToken(Long userId){

Map claims = new HashMap<>();

claims.put("userId",userId);

JwtBuilder jwtBuilder = Jwts.builder()

.signWith(SignatureAlgorithm.HS256, jwtToken) // 签发算法,秘钥为jwtToken

.setClaims(claims) // body数据,要唯一,自行设置

.setIssuedAt(new Date()) // 设置签发时间

.setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 60 * 1000));// 一天的有效时间

String token = jwtBuilder.compact();

return token;

}

public static Map checkToken(String token){

try {

Jwt parse = Jwts.parser().setSigningKey(jwtToken).parse(token);

return (Map) parse.getBody();

}catch (Exception e){

e.printStackTrace();

}

return null;

}

//测验一下

public static void main(String[] args) {

String token = JWTUtils.createToken(100L);

System.out.println(token);

Map map = JWTUtils.checkToken(token);

System.out.println(map.get("userId"));

}

}

8.3 LoginController

package com.jihu.blog.controller;

import com.jihu.blog.service.LoginService;

import com.jihu.blog.vo.Result;

import com.jihu.blog.vo.params.LoginParams;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.web.bind.annotation.RequestBody;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

@RestController

@RequestMapping("login")

public class LoginController {

@Autowired

private LoginService loginService;

@RequestBody主要用来接收前端传递给后端的json字符串中的数据的(请求体中的数据的);

// 而最常用的使用请求体传参的无疑是POST请求了,所以使用@RequestBody接收数据时,一般都用POST方式进行提交。

@PostMapping

public Result Login(@RequestBody LoginParams loginParams){

//登录 验证用户 访问用户表

return loginService.login(loginParams);

}

}

8.4 LoginParam 登录参数

构造LoginParam也就是我们的请求数据com.jihu.blog.vo.params.LoginParams

package com.jihu.blog.vo.params;

import lombok.Data;

@Data

public class LoginParams {

private String account;

private String password;

}

8.5 LoginService

package com.jihu.blog.service;

import com.jihu.blog.dao.pojo.SysUser;

import com.jihu.blog.vo.Result;

import com.jihu.blog.vo.params.LoginParams;

public interface LoginService {

/**

* 登录功能

* @param loginParams

* @return

*/

Result login(LoginParams loginParams);

}

导入依赖包md5加密的依赖包:

commons-codec

commons-codec

8.6 LoginServiceImpl

// @Component – 指示自动扫描组件。

//@Repository – 表示在持久层DAO组件。

//@Service – 表示在业务层服务组件。

//@Controller – 表示在表示层控制器组件。

@Service

public class LoginServiceImpl implements LoginService {

@Autowired

private SysUserService sysUserService;

@Autowired

private RedisTemplate redisTemplate;

//加密盐用于加密

private static final String salt = "mszlu!@#";

@Override

public Result login(LoginParams loginParams) {

/**

* 1.检查参数是否合法

* 2.根据用户名和密码去user表中查询是否存在

* 3.如果不存在 登录失败

* 4. 如果存在 ,使用jwt生成token 返回给前端

* 5.把token放入redis中,redis 存储 token user这个信息,设置过期时间

* (登录认证时 先认证token字符串是否合法,再去redis认证是否存在)

*/

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());

}

//把密码加密一下

String pwd = DigestUtils.md5Hex(password + salt);

SysUser sysUser = sysUserService.findUser(account,pwd);

if (sysUser == null){

return Result.fail(ErrorCode.ACCOUNT_PWD_NOT_EXIST.getCode(),ErrorCode.ACCOUNT_PWD_NOT_EXIST.getMsg());

}

//登录成功,使用JWT生成token,返回token和redis中

String token = JWTUtils.createToken(sysUser.getId());

//JSON.toJSONString :是把 sysUser对象转换为对应的json字符串 (参考:https://blog.csdn.net/antony9118/article/details/71023009)

//设置过期时间为1天

redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(sysUser),1, TimeUnit.DAYS);

return Result.success(token);

}

//生成我们想要的密码,放于数据库用于登陆

public static void main(String[] args) {

System.out.println(DigestUtils.md5Hex("admin"+salt));

}

}

8.7 SysUserService

SysUser findUser(String account, String password);

8.8 SysUserServiceImpl

@Override

public SysUser findUser(String account, String password) {

LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>();

lambdaQueryWrapper.eq(SysUser::getAccount,account);

lambdaQueryWrapper.eq(SysUser::getPassword,password);

lambdaQueryWrapper.select(SysUser::getAccount,SysUser::getId,SysUser::getAvatar,SysUser::getNickname);

lambdaQueryWrapper.last("limit 1"); //保证一下查询效率 要不然可能还会一直往下查询

return sysUserMapper.selectOne(lambdaQueryWrapper);

}

8.9 redis配置

#整合redis

spring.redis.host=192.168.56.130

spring.redis.port=6379

8.10 统一错误码

com.jihu.blog.vo.ErrorCode

package com.jihu.blog.vo;

public enum ErrorCode {

PARAMS_ERROR(10001,"参数有误"),

ACCOUNT_PWD_NOT_EXIST(10002,"用户名或密码不存在"),

TOKEN_ERROR(10003,"token不合法"),

NO_PERMISSION(70001,"无访问权限"),

SESSION_TIME_OUT(90001,"会话超时"),

NO_LOGIN(90002,"未登录"),;

private int code;

private String msg;

ErrorCode(int code, String msg){

this.code = code;

this.msg = msg;

}

public int getCode() {

return code;

}

public void setCode(int code) {

this.code = code;

}

public String getMsg() {

return msg;

}

public void setMsg(String msg) {

this.msg = msg;

}

}

8.11 测试

9.获取用户信息


为什么实现完获取用户信息才能登陆测试呢?

token前端获取到之后,会存储在 storage中 h5 ,本地存储,存储好后,拿到storage中的token去获取用户信息,如果这个接口没实现,他就会一直请求陷入死循环

9.1 接口说明

得从http的head里面拿到这个参数,这样传参相对来说安全一些,返回是数据是我们用户相关的数据,id,账号、昵称和头像

接口url:/users/currentUser

请求方式:GET

请求参数:

参数名称

参数类型

说明

Authorization

string

头部信息(TOKEN)

返回数据:

{

"success": true,

"code": 200,

"msg": "success",

"data": {

"id":1,

"account":"1",

"nickaname":"1",

"avatar":"ss"

}

}

9.2 UsersController

package com.jihu.blog.controller;

import com.jihu.blog.service.SysUserService;

import com.jihu.blog.vo.Result;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.bind.annotation.*;

//浅谈@RequestMapping @ResponseBody 和 @RequestBody 注解的用法与区别?

//https://blog.csdn.net/ff906317011/article/details/78552426?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-2.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-2.no_search_link

@RestController

@RequestMapping("users")

public class UsersController {

@Autowired

private SysUserService sysUserService;

@GetMapping("currentUser")

public Result currentUser(@RequestHeader("Authorization") String token){

return sysUserService.findUserByToken(token);

}

}

9.3 SysUserService

package com.jihu.blog.service;

import com.jihu.blog.dao.pojo.SysUser;

import com.jihu.blog.vo.Result;

public interface SysUserService {

/**

* 根据token查询用户信息

* @param token

* @return

*/

Result findUserByToken(String token);

}

9.4 SysUserServiceImpl

@Override

public Result findUserByToken(String token) {

/**

* 1.token合法性效验

* 是否为空,解析是否成功 redis是否存在

* 2.如果效验失败 返回错误

* 3.如果成功,返回对应的结果 LoginUserVo

*/

//去loginservice中去校验token

SysUser sysUser = loginService.checkToken(token);

if (sysUser == null){

return 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);

}

9.5 LoginService

SysUser checkToken(String token);

9.6 LoginServiceImpl

@Override

public SysUser checkToken(String token) {

//token为空返回null

if (StringUtils.isBlank(token)){

return null;

}

Map stringObjectMap = JWTUtils.checkToken(token);

//解析失败

if (stringObjectMap == null){

return null;

}

//如果成功

String userJson = redisTemplate.opsForValue().get("TOKEN_" + token);

if (StringUtils.isBlank(userJson)){

return null;

}

SysUser sysUser = JSON.parseObject(userJson, SysUser.class); //解析为json

return sysUser;

}

9.7 LoginUserVo

package com.jihu.blog.vo;

import lombok.Data;

@Data

public class LoginUserVo {

//与页面交互

private Long id;

private String account;

private String nickname;

private String avatar;

}

9.8 测试

10. 退出登录


登陆一个的对token进行认证,一个是在redis中进行注册,token字符串没法更改掉,只能由前端进行清除,后端能做的就是把redis进行清除

10.1 接口说明

接口url:/logout

请求方式:GET

请求参数:

参数名称

参数类型

说明

Authorization

string

头部信息(TOKEN)

返回数据:

{

"success": true,

"code": 200,

"msg": "success",

"data": null

}

10.2 LogoutController

package com.jihu.blog.controller;

import com.jihu.blog.service.LoginService;

import com.jihu.blog.vo.Result;

import com.jihu.blog.vo.params.LoginParams;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RequestHeader;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

@RestController

@RequestMapping("logout")

public class LogoutController {

@Autowired

private LoginService loginService;

//获取头部信息这样一个参数

@GetMapping

public Result logout(@RequestHeader("Authorization") String token){

return loginService.logout(token);

}

}

10.3 LoginService

/**

* 退出登陆

* @param token

* @return

*/

Result logout(String token);

10.4 LoginServiceImpl

@Override

public Result logout(String token) {

//后端直接删除redis中的token

redisTemplate.delete("TOKEN_"+token);

return Result.success(null);

}

10.5 测试

11. 注册用户


11.1 接口说明

接口url:/register

请求方式:POSTpost传参意味着请求参数是按照json方式传具体可以看这篇post和@Requestbody

请求参数:

参数名称

参数类型

说明

account

string

账号

password

string

密码

nickname

string

昵称

返回数据:

{

"success": true,

"code": 200,

"msg": "success",

"data": "token"

}

11.2 RegisterController

package com.jihu.blog.controller;

import com.jihu.blog.service.LoginService;

import com.jihu.blog.vo.Result;

import com.jihu.blog.vo.params.LoginParams;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.web.bind.annotation.RequestBody;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

@RestController

@RequestMapping("register")

public class RegisterController {

@Autowired

private LoginService loginService;

@PostMapping

public Result register(@RequestBody LoginParams loginParams){

//sso 叫做 单点登录, 后期如果把登录注册功能 提出去(单独的服务,可以独立提供接口服务)

return loginService.register(loginParams);

}

}

11.3 LoginService

/**

* 注册

* @param loginParams

* @return

*/

Result register(LoginParams loginParams);

11.4 LoginServiceImpl

@Override

public Result register(LoginParams loginParams) {

/**

* 1.判断参数是否合法

* 2.判断账户是否存在,存在 返回账户已经被注册

* 3.如果账户不存在,注册用户

* 4.生成token

* 5. 存入redis 并返回

* 6. 注意 加上事务,一旦中间任何过程出现问题,注册的用户 需要回滚

*/

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 = sysUserService.findUserByAccount(account);

if (sysUser != null){

return Result.fail(ErrorCode.ACCOUNT_EXIST.getCode(),"账号已经被注册了");

}

sysUser = new SysUser();

sysUser.setNickname(nickname);

sysUser.setAccount(account);

sysUser.setPassword(DigestUtils.md5Hex(password + salt));

sysUser.setCreateDate(System.currentTimeMillis());

sysUser.setLastLogin(System.currentTimeMillis());

sysUser.setAvatar("/static/img/logo.b3a48c0.png");

sysUser.setAdmin(1); //1为true

sysUser.setDeleted(0); //0为false

sysUser.setEmail("");

sysUser.setSalt("");

sysUser.setStatus("");

sysUserService.save(sysUser);

String token = JWTUtils.createToken(sysUser.getId());

redisTemplate.opsForValue().set("TOKEN_"+token,JSON.toJSONString(sysUser) ,1,TimeUnit.DAYS);

return Result.success(token);

}

11.5 ErrorCode

ACCOUNT_EXIST(10004,"账号已存在"),

11.6 SysUserService

/**

* 根据账号查找用户

* @param account

* @return

*/

SysUser findUserByAccount(String account);

/**

* 保存用户

* @param sysUser

*/

void save(SysUser sysUser);

11.7 SysUserServiceImpl

@Override

public SysUser findUserByAccount(String account) {

LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>();

lambdaQueryWrapper.eq(SysUser::getAccount,account);

lambdaQueryWrapper.last("limit 1");

return sysUserMapper.selectOne(lambdaQueryWrapper);

}

@Override

public void save(SysUser sysUser) {

//保存用户这 id会自动生成

//注意: 这个地方 默认生成的id 是分布式id 采用了雪花算法

sysUserMapper.insert(sysUser);

}

11.8 加事务

出现错误就进行回滚防止添加异常

增加@Transactional注解

com.jihu.blog.service.LoginService

@Service

@Transactional

public class LoginServiceImpl implements LoginService {}

当然 一般建议将事务注解@Transactional加在 接口上,通用一些。

package com.jihu.blog.service;

import com.jihu.blog.dao.pojo.SysUser;

import com.jihu.blog.vo.Result;

import com.jihu.blog.vo.params.LoginParams;

import org.springframework.transaction.annotation.Transactional;

@Transactional

public interface LoginService {

/**

* 登录功能

* @param loginParams

* @return

*/

Result login(LoginParams loginParams);

SysUser checkToken(String token);

/**

* 退出登陆

* @param token

* @return

*/

Result logout(String token);

/**

* 注册

* @param loginParams

* @return

*/

Result register(LoginParams loginParams);

}

11.9 测试

12. 登录拦截器


每次访问需要登录的资源的时候,都需要在代码中进行判断,一旦登录的逻辑有所改变,代码都得进行变动,非常不合适。

那么可不可以统一进行登录判断呢?

springMVC中拦截器

可以,使用拦截器,进行登录拦截,如果遇到需要登录才能访问的接口,如果未登录,拦截器直接返回,并跳转登录页面。Javas三大器:过滤器-监听器-拦截器

12.1 拦截器实现

com.jihu.blog.handler.LoginInterceptor

package com.jihu.blog.handler;

import com.alibaba.fastjson.JSON;

import com.fasterxml.jackson.annotation.JsonAlias;

import com.jihu.blog.dao.pojo.SysUser;

import com.jihu.blog.service.LoginService;

import com.jihu.blog.vo.ErrorCode;

import com.jihu.blog.vo.Result;

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang3.StringUtils;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Component;

import org.springframework.web.method.HandlerMethod;

import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

@Component

@Slf4j

public class LoginInterceptor implements HandlerInterceptor {

@Autowired

private LoginService loginService;

@Override

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

//在执行controller方法(handler)之前进行执行

/**

* 1.需要判断 请求的接口路径 是否为 HandlerMethod (controller方法),不是的话,放行

* 2.判断token 是否为空, 如果为空 未登录

* 3.如果token不为空,登录验证 loginService checkToken

* 4.如果认证成功,放行即可

*/

if (!(handler instanceof HandlerMethod)){

return true;

}

String token = request.getHeader("Authorization");

log.info("=================request start===========================");

String requestURI = request.getRequestURI();

log.info("request uri:{}",requestURI);

log.info("request method:{}",request.getMethod());

log.info("token:{}", token);

log.info("=================request end===========================");

if (StringUtils.isBlank(token)){

Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());

response.setContentType("application/json;charset=utf-8");

response.getWriter().print(JSON.toJSONString(result));

return false;

}

SysUser sysUser = loginService.checkToken(token);

if (sysUser == null){

Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());

response.setContentType("application/json;charset=utf-8");

response.getWriter().print(JSON.toJSONString(result));

return false;

}

//登录验证成功,放行

return true;

}

}

12.2 使拦截器生效

com.jihu.blog.config.WebMVCConfig

package com.jihu.blog.config;

import com.jihu.blog.handler.LoginInterceptor;

import com.jihu.blog.service.LoginService;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.Configuration;

import org.springframework.web.servlet.config.annotation.CorsRegistry;

import org.springframework.web.servlet.config.annotation.InterceptorRegistry;

import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration

public class WebMVCConfig implements WebMvcConfigurer {

@Autowired

private LoginInterceptor loginInterceptor;

//实现跨域请求

@Override

public void addCorsMappings(CorsRegistry registry) {

registry.addMapping("/**")

.allowedOriginPatterns("*")

.allowedMethods("GET","HEAD","POST","PUT","DELETE","OPTIONS")

.allowCredentials(true)

.maxAge(3600)

.allowedHeaders("*");

}

@Override

public void addInterceptors(InterceptorRegistry registry) {

//拦截test接口,后续实际遇到需要拦截的接口时,在配置为真正的拦截接口

registry.addInterceptor(loginInterceptor)

.addPathPatterns("/test");

}

}

12.3测试

com.jihu.blog.controller.TestController

@RestController

@RequestMapping("test")

public class TestController {

@RequestMapping

public Result test(){

//得到用户的信息

SysUser sysUser = UserThreadLocal.get();

System.out.println(sysUser);

return Result.success(null);

}

}

用postman进行测试,先登录找到token,然后在验证,才能成功

13.ThreadLocal保存用户信息


redis中只放了token我们希望直接获取用户信息好处和如何使用的使用ThreadLocal保存用户登录信息使用ThreadLocal替代Session完成保存用户登录信息功能

使用ThreadLocal替代Session的好处:

可以在同一线程中很方便的获取用户信息,不需要频繁的传递session对象。

具体实现流程:

在登录业务代码中,当用户登录成功时,生成一个登录凭证存储到redis中,

将凭证中的字符串保存在cookie中返回给客户端。

使用一个拦截器拦截请求,从cookie中获取凭证字符串与redis中的凭证进行匹配,获取用户信息,

将用户信息存储到ThreadLocal中,在本次请求中持有用户信息,即可在后续操作中使用到用户信息。

相关问题Session原理COOKIE和SESSION有什么区别?

com.jihu.blog.utils.UserThreadLocal

package com.jihu.blog.utils;

import com.jihu.blog.dao.pojo.SysUser;

public class UserThreadLocal {

private UserThreadLocal(){}

//ThreadLocal 做线程变量隔离的

private static final ThreadLocal LOCAL = new ThreadLocal<>();

public static void put(SysUser sysUser){

LOCAL.set(sysUser);

}

public static SysUser get(){

return LOCAL.get();

}

public static void remove(){

LOCAL.remove();

}

}

com.jihu.blog.handler.LoginInterceptor

package com.jihu.blog.handler;

import com.alibaba.fastjson.JSON;

import com.fasterxml.jackson.annotation.JsonAlias;

import com.jihu.blog.dao.pojo.SysUser;

import com.jihu.blog.service.LoginService;

import com.jihu.blog.utils.UserThreadLocal;

import com.jihu.blog.vo.ErrorCode;

import com.jihu.blog.vo.Result;

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang3.StringUtils;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Component;

import org.springframework.web.method.HandlerMethod;

import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

@Component

@Slf4j

public class LoginInterceptor implements HandlerInterceptor {

@Autowired

private LoginService loginService;

@Override

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

//在执行controller方法(handler)之前进行执行

/**

* 1.需要判断 请求的接口路径 是否为 HandlerMethod (controller方法),不是的话,放行

* 2.判断token 是否为空, 如果为空 未登录

* 3.如果token不为空,登录验证 loginService checkToken

* 4.如果认证成功,放行即可

*/

if (!(handler instanceof HandlerMethod)){

return true;

}

String token = request.getHeader("Authorization");

log.info("=================request start===========================");

String requestURI = request.getRequestURI();

log.info("request uri:{}",requestURI);

log.info("request method:{}",request.getMethod());

log.info("token:{}", token);

log.info("=================request end===========================");

if (StringUtils.isBlank(token)){

Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());

response.setContentType("application/json;charset=utf-8");

response.getWriter().print(JSON.toJSONString(result));

return false;

}

SysUser sysUser = loginService.checkToken(token);

if (sysUser == null){

Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());

response.setContentType("application/json;charset=utf-8");

response.getWriter().print(JSON.toJSONString(result));

return false;

}

//登录验证成功,放行

//我希望在controller中 直接获取用户的信息 怎么获取呢?

UserThreadLocal.put(sysUser);

return true;

}

@Override

public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

//如果不删除 ThreadLocal中用完的信息 会有内存泄漏的风险

UserThreadLocal.remove();

}

}

com.jihu.blog.controller.TestController

package com.jihu.blog.controller;

import com.jihu.blog.dao.pojo.SysUser;

import com.jihu.blog.utils.UserThreadLocal;

import com.jihu.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);

}

}

测试

在postman中重新发送一次http://localhost:8888/test请求,

可以看到能拿到我们的信息

14.ThreadLocal内存泄漏


ThreadLocal原理及内存泄露预防

实线表强引用,虚线代表弱引用

每一个Thread维护一个ThreadLocalMap, key为使用弱引用的ThreadLocal实例,value为线程变量的副本。

强引用,使用最普遍的引用,一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。

如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。

弱引用,JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。

13.文章详情


13.1接口说明

接口url:/articles/view/{id}

请求方式:POST

请求参数:

参数名称

参数类型

说明

id

long

文章id(路径参数)

返回数据:

{success: true, code: 200, msg: "success",…}

code: 200

data: {id: "1405916999732707330", title: "SpringBoot入门案例", summary: "springboot入门案例", commentCounts: 0,…}

msg: "success"

success: true

13.2涉及到的表

内容表

content存放makedown格式的信息content_html存放html格式的信息

CREATE TABLE `blog`.`ms_article_body` (

`id` bigint(0) NOT NULL AUTO_INCREMENT,

`content` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL,

`content_html` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL,

`article_id` bigint(0) NOT NULL,

PRIMARY KEY (`id`) USING BTREE,

INDEX `article_id`(`article_id`) USING BTREE

) ENGINE = InnoDB AUTO_INCREMENT = 38 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

类别表avata分类图标路径category_name图标分类的名称description分类的描述

CREATE TABLE `blog`.`ms_category` (

`id` bigint(0) NOT NULL AUTO_INCREMENT,

`avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,

`category_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,

`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,

PRIMARY KEY (`id`) USING BTREE

) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

13.3 pojo层

Category

package com.jihu.blog.dao.pojo;

import lombok.Data;

@Data

public class Category {

private Long id;

private String avatar;

private String categoryName;

private String description;

}

ArticleBody

package com.jihu.blog.dao.pojo;

import lombok.Data;

//内容表

@Data

public class ArticleBody {

private Long id;

private String content;

private String contentHtml;

private Long articleId;

}

13.4 Controller

com.jihu.blog.controller.ArticleController

//文章详情

@PostMapping("view/{id}")

public Result findArticleById(@PathVariable("id") Long articleId){

return articleService.findArticleById(articleId);

}

13.5 Service层

文章表里面只有tiltle以及一些简介ms_article 中body_id对应第二张表ms_article_body上的idms_category会映射到ms_article 中的category_id需要做一些相对的关联查询

ArticleService

/**

* 查看文章详情

* @param articleId

* @return

*/

Result findArticleById(Long articleId);

ArticleServiceImpl

private List copyList(List

records,boolean isTag,boolean isAuthor) {

List articleVoList = new ArrayList<>();

for (Article record : records) {

articleVoList.add(copy(record,isTag,isAuthor,false,false));

}

return articleVoList;

}

//方法重载

private List copyList(List

records,boolean isTag,boolean isAuthor,boolean isBody, boolean isCategory) {

List articleVoList = new ArrayList<>();

for (Article record : records) {

articleVoList.add(copy(record,isTag,isAuthor,isBody,isCategory));

}

return articleVoList;

}

private ArticleVo copy(Article article,boolean isTag,boolean isAuthor,boolean isBody, boolean isCategory){

ArticleVo articleVo = new ArticleVo();

BeanUtils.copyProperties(article,articleVo);

articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));

//并不是所有的接口,都需要标签,作者信息

if (isTag){

Long articleId = article.getId();

articleVo.setTags(tagService.findTagsByArticleId(articleId));

}

if (isAuthor){

Long authorId = article.getAuthorId();

articleVo.setAuthor(sysUserService.findUserById(authorId).getNickname());

}

if (isBody){

Long bodyId = article.getBodyId();

articleVo.setBody(findArticleBodyById(bodyId));

}

if (isCategory){

Long categoryId = article.getCategoryId();

articleVo.setCategory(categoryService.findCategoryById(categoryId));

}

return articleVo;

}

private ArticleBodyVo findArticleBodyById(Long bodyId) {

ArticleBody articleBody = articleBodyMapper.selectById(bodyId);

ArticleBodyVo articleBodyVo = new ArticleBodyVo();

articleBodyVo.setContent(articleBody.getContent());

return articleBodyVo;

}

@Override

public Result findArticleById(Long articleId) {

/**

* 1.根据id查询 文章信息

* 2.根据bodyId和categoryid 去做关联查询

*/

Article article = articleMapper.selectById(articleId);

ArticleVo articleVo = copy(article,true,true,true,true);

//查看完文章了,新增阅读数,有没有问题呢?

//查看完文章之后,本应该直接返回数据了,这时候做了一个更新操作,更新时加写锁,阻塞其他的读操作,性能就会比较低

// 更新 增加了此次接口的 耗时 如果一旦更新出问题,不能影响 查看文章的操作

//线程池 可以把更新操作 扔到线程池中去执行,和主线程就不相关了

threadService.updateArticleViewCount(articleMapper,article);

return Result.success(articleVo);

}

CategoryService

package com.jihu.blog.service;

import com.jihu.blog.vo.CategoryVo;

public interface CategoryService {

CategoryVo findCategoryById(Long id);

}

CategoryServiceImpl

package com.jihu.blog.service.impl;

import com.jihu.blog.dao.mapper.CategoryMapper;

import com.jihu.blog.dao.pojo.Category;

import com.jihu.blog.service.CategoryService;

import com.jihu.blog.vo.CategoryVo;

import org.springframework.beans.BeanUtils;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Service;

@Service

public class CategoryServiceImpl implements CategoryService {

@Autowired

private CategoryMapper categoryMapper;

@Override

public CategoryVo findCategoryById(Long id) {

Category category = categoryMapper.selectById(id);

CategoryVo categoryVo = new CategoryVo();

//因为category,categoryVo属性一样所以可以使用 BeanUtils.copyProperties

BeanUtils.copyProperties(category,categoryVo);

return categoryVo;

}

}

13.6 Vo层

ArticleVo

package com.jihu.blog.vo;

import lombok.Data;

import java.util.List;

//建立与前端交互的Vo文件

@Data

public class ArticleVo {

private Long id;

private String title;

private String summary;

private int commentCounts;

private int viewCounts;

private int weight;

/**

* 创建时间

*/

private String createDate;

private String author;

private ArticleBodyVo body;

private List tags;

private CategoryVo category;

}

ArticleBodyVo

package com.jihu.blog.vo;

import lombok.Data;

@Data

public class ArticleBodyVo {

//内容

private String content;

}

CategoryVo

package com.jihu.blog.vo;

import lombok.Data;

@Data

public class CategoryVo {

//id,图标路径,图标名称

private Long id;

private String avatar;

private String categoryName;

}

13.7 mapper层

ArticleBodyMapper

package com.jihu.blog.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;

import com.jihu.blog.dao.pojo.ArticleBody;

public interface ArticleBodyMapper extends BaseMapper {

}

CategoryMapper

package com.jihu.blog.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;

import com.jihu.blog.dao.pojo.Category;

public interface CategoryMapper extends BaseMapper {

}

13.8 测试

14.使用线程池 更新阅读次数


/查看完文章了,新增阅读数,有没有问题呢?//查看完文章之后,本应该直接返回数据了,这时候做了一个更新操作,更新时加写锁,阻塞其他的读操作,性能就会比较低(没办法解决,增加阅读数必然要加锁)//更新增加了此次接口的耗时(考虑减少耗时)如果一旦更新出问题,不能影响查看操作想到了一个技术 线程池可以把更新操作扔到 线程池中去执行和主线程就不相关了

什么是乐观锁,什么是悲观锁CAS原理分析

14.1线程池配置

做一个线程池的配置来开启线程池

com.jihu.blog.config.ThreadPoolConfig

package com.jihu.blog.config;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.scheduling.annotation.EnableAsync;

import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

//https://www.jianshu.com/p/0b8443b1adc9 关于@Configuration和@Bean的用法和理解

@Configuration

@EnableAsync

public class ThreadPoolConfig {

@Bean("taskExecutor")

public Executor asyncServiceExecutor(){

ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

// 设置核心线程数

executor.setCorePoolSize(5);

// 设置最大线程数

executor.setCorePoolSize(20);

//配置队列大小

executor.setQueueCapacity(Integer.MAX_VALUE);

// 设置线程活跃时间(秒)

executor.setKeepAliveSeconds(60);

// 设置默认线程名称

executor.setThreadNamePrefix("码神之路博客项目");

// 等待所有任务结束后再关闭线程池

executor.setWaitForTasksToCompleteOnShutdown(true);

//执行初始化

executor.initialize();

return executor;

}

}

14.2 使用

com.jihu.blog.service.ThreadService

package com.jihu.blog.service;

import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;

import com.jihu.blog.dao.mapper.ArticleMapper;

import com.jihu.blog.dao.pojo.Article;

import org.springframework.scheduling.annotation.Async;

import org.springframework.stereotype.Component;

@Component

public class ThreadService {

//期望此操作在线程池执行不会影响原有主线程

//这里线程池不了解可以去看JUC并发编程

@Async("taskExecutor")

public void updateArticleViewCount(ArticleMapper articleMapper, Article article) {

int viewCounts = article.getViewCounts();

Article articleupdate = new Article();

articleupdate.setViewCounts(viewCounts+1);

LambdaUpdateWrapper

updatewrapper = new LambdaUpdateWrapper<>();

//根据id更新

updatewrapper.eq(Article::getId,article.getId());

//设置一个为了在多线程的环境下线程安全

//改之前再确认这个值有没有被其他线程抢先修改,类似于CAS操作 cas加自旋,加个循环就是cas

updatewrapper.eq(Article::getViewCounts,viewCounts);

// update article set view_count=100 where view_count=99 and id =111

//实体类加更新条件

articleMapper.update(articleupdate,updatewrapper);

try {

Thread.sleep(5000);

System.out.println("更新完成了");

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

com.jihu.blog.service.impl.ArticleServiceImpl

@Autowired

private ThreadService threadService;

@Override

public ArticleVo findArticleById(Long id) {

Article article = articleMapper.selectById(id);

//线程池

threadService.updateViewCount(articleMapper,article);

return copy(article,true,true,true,true);

}

14.3测试

睡眠 ThredService中的方法 5秒,不会影响主线程的使用,即文章详情会很快的显示出来,不受影响

Bug修正之前Article中的commentCounts,viewCounts,weight 字段为int,会造成更新阅读次数的时候,将其余两个字段设为初始值0mybatisplus在更新文章阅读次数的时候虽然只设立了articleUpdate.setviewsCounts(viewCounts+1),但是int默认基本数据类型为0,mybatisplus但凡不是null就会生成到sql语句中进行更新。会出现

理想中应该是只有views_counts改变但是因为mybatisplus规则所以会出现这个现象所以将int改为Integer就不会出现这个问题。

15.评论列表


评论表id评论idcontent评论内容create_date评论时间article_id评论文章author_id谁评论的parent_id盖楼功能对评论的评论进行回复to_uid给谁评论level评论的是第几层(1级表示最上层的评论,2表示对评论的评论)

CREATE TABLE `blog`.`ms_comment` (

`id` bigint(0) NOT NULL AUTO_INCREMENT,

`content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,

`create_date` bigint(0) NOT NULL,

`article_id` int(0) NOT NULL,

`author_id` bigint(0) NOT NULL,

`parent_id` bigint(0) NOT NULL,

`to_uid` bigint(0) NOT NULL,

`level` varchar(1) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,

PRIMARY KEY (`id`) USING BTREE,

INDEX `article_id`(`article_id`) USING BTREE

) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

15.1接口说明

接口url:/comments/article/{id}

请求方式:GET

请求参数:

参数名称

参数类型

说明

id

long

文章id(路径参数)

返回数据

{

"success": true,

"code": 200,

"msg": "success",

"data": [

{

"id": 53,

"author": {

"nickname": "李四",

"avatar": "http://localhost:8080/static/img/logo.b3a48c0.png",

"id": 1

},

"content": "写的好",

"childrens": [

{

"id": 54,

"author": {

"nickname": "李四",

"avatar": "http://localhost:8080/static/img/logo.b3a48c0.png",

"id": 1

},

"content": "111",

"childrens": [],

"createDate": "1973-11-26 08:52",

"level": 2,

"toUser": {

"nickname": "李四",

"avatar": "http://localhost:8080/static/img/logo.b3a48c0.png",

"id": 1

}

}

],

"createDate": "1973-11-27 09:53",

"level": 1,

"toUser": null

}

]

}

15.2 CommentsController

@RequestMapping("comments")

@RestController

public class CommentsController {

@Autowired

private CommentsService commentsService;

/**

* 评论列表

* @param id

* @return

*/

@GetMapping("article/{id}")

public Result comments(@PathVariable Long id){

return commentsService.commentsByArticleId(id);

}

}

15.3 Service层

15.3.1 CommentsService

public interface CommentsService {

/**

* 根据文章id查询所有的评论列表

* @param id

* @return

*/

Result commentsByArticleId(Long id);

}

15.3.2 CommentsServiceImpl

package com.jihu.blog.service.impl;

@Service

public class CommentsServiceImpl implements CommentsService {

@Autowired

private CommentMapper commentMapper;

@Autowired

private SysUserService sysUserService;

@Override

public Result commentsByArticleId(Long id) {

/**

* 1.根据文章id 查询 评论列表,从comment表中查询

* 2.根据作者的id 查询作者的信息

* 3.判断如果level=1,要去查询它有没有子评论

* 4.如果有 根据评论id 进行查询(parent_id)

*/

LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();

queryWrapper.eq(Comment::getArticleId,id);

queryWrapper.eq(Comment::getLevel,1);

List comments = commentMapper.selectList(queryWrapper);

List commentVoList = copyList(comments);

return Result.success(commentVoList);

}

private List copyList(List comments) {

List commentVoList = new ArrayList<>();

for (Comment comment : comments) {

commentVoList.add(copy(comment));

}

return commentVoList;

}

private CommentVo copy(Comment comment) {

CommentVo commentVo = new CommentVo();

// 相同属性copy

BeanUtils.copyProperties(comment,commentVo);

//作者信息

Long authorId = comment.getAuthorId();

UserVo userVo= sysUserService.findUserVoById(authorId);

commentVo.setAuthor(userVo);

//子评论

Integer level = comment.getLevel();

if (level == 1){

Long id = comment.getId();

List commentVoList = findCommentByParentId(id);

commentVo.setChildrens(commentVoList);

}

//to user 给谁评论

if (level>1){

Long toUid = comment.getToUid();

UserVo toUserVo = sysUserService.findUserVoById(toUid);

commentVo.setToUser(toUserVo);

}

return commentVo;

}

//子评论的查询

private List findCommentByParentId(Long id) {

LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();

queryWrapper.eq(Comment::getParentId,id);

queryWrapper.eq(Comment::getLevel,2);

List comments = commentMapper.selectList(queryWrapper);

return copyList(comments);

}

}

15.3.3 SysUserService

/**

* 查询UserVo的信息

* 查询用户信息的服务:

* @param id

* @return

*/

UserVo findUserVoById(Long id);

15.3.4 SysUserServiceImpl

@Override

public UserVo findUserVoById(Long id) {

SysUser sysUser = sysUserMapper.selectById(id);

if (sysUser == null){

sysUser = new SysUser();

sysUser.setId(1L);

sysUser.setAvatar("/static/img/logo.b3a48c0.png");

sysUser.setNickname("马神之路");

}

UserVo userVo = new UserVo();

BeanUtils.copyProperties(sysUser,userVo);

return userVo;

}

15.4 Vo层

用于返回的数据:

CommentVo

com.jihu.blog.vo.CommentVo

package com.jihu.blog.vo;

import com.fasterxml.jackson.databind.annotation.JsonSerialize;

import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;

import lombok.Data;

import java.util.List;

@Data

public class CommentVo {

//防止前端 精度损失 把id转为string

//把 id转化为string类型的 要不然会丢精度 导致错误

@JsonSerialize(using = ToStringSerializer.class)

private Long id;

private UserVo author;

private String content;

private List childrens;

private String createDate;

private Integer level;

private UserVo toUser;

}

UserVo

com.jihu.blog.vo.UserVo

package com.jihu.blog.vo;

import lombok.Data;

@Data

public class UserVo {

private Long id;

private String avatar;

private String nickname;

}

15.5 测试

16.评论


16.1 接口说明

接口url:/comments/create/change

请求方式:POST

请求参数:

参数名称

参数类型

说明

articleId

long

文章id

content

string

评论内容

parent

long

父评论id

toUserId

long

被评论的用户id

返回数据:

{

"success": true,

"code": 200,

"msg": "success",

"data": null

}

16.2加入到登录拦截器中

com.jihu.blog.config.WebMVCConfig

@Override

public void addInterceptors(InterceptorRegistry registry) {

//拦截test接口,后续实际遇到需要拦截的接口时,在配置为真正的拦截接口

registry.addInterceptor(loginInterceptor)

.addPathPatterns("/test").addPathPatterns("/comments/create/change");

}

16.3 新建CommentParam

构建评论参数对象:

com.jihu.blog.vo.params.CommentParam

package com.jihu.blog.vo.params;

import lombok.Data;

@Data

public class CommentParam {

private Long articleId;

private String content;

private Long parent;

private Long toUserId;

}

16.4 修改CommentsController

/**

* 评论

* @param commentParam

* @return

*/

@PostMapping("create/change")

public Result comment(@RequestBody CommentParam commentParam){

return commentsService.comment(commentParam);

}

16.5 修改CommentsService

// 评论

Result comment(CommentParam commentParam);

16.6 修改CommentsServiceImpl

@Override

public Result comment(CommentParam commentParam) {

//拿到当前用户

SysUser sysUser = UserThreadLocal.get();

Comment comment = new Comment();

comment.setAuthorId(sysUser.getId());

comment.setArticleId(commentParam.getArticleId());

comment.setContent(commentParam.getContent());

comment.setCreateDate(System.currentTimeMillis());

Long parent = commentParam.getParent();

if (parent == null || parent == 0 ){

comment.setLevel(1);

}else {

comment.setLevel(2);

}

//如果是空,parent就是0

comment.setParentId(parent == null ? 0 : parent);

Long toUserId = commentParam.getToUserId();

comment.setToUid(toUserId == null ? 0 : toUserId);

commentMapper.insert(comment);

return Result.success(null);

}

修改 com.jihu.blog.vo.CommentVo

//防止前端 精度损失 把id转为string

// 分布式id 比较长,传到前端 会有精度损失,必须转为string类型 进行传输,就不会有问题了

@JsonSerialize(using = ToStringSerializer.class)

private Long id;

16.7 测试

17、写文章


写文章由 三部分组成:

  1. 获取所有文章类别

  1. 获取所有标签

  1. 发布文章

17.1 文章分类

17.1.1接口说明

接口url:/categorys

请求方式:GET

请求参数:无

返回数据:

{

"success":true,

"code":200,

"msg":"success",

"data":

[

{"id":1,"avatar":"/category/front.png","categoryName":"前端"},

{"id":2,"avatar":"/category/back.png","categoryName":"后端"},

{"id":3,"avatar":"/category/lift.jpg","categoryName":"生活"},

{"id":4,"avatar":"/category/database.png","categoryName":"数据库"},

{"id":5,"avatar":"/category/language.png","categoryName":"编程语言"}

]

}

17.1.2 CategoryController

package com.jihu.blog.controller;

import com.jihu.blog.service.CategoryService;

import com.jihu.blog.vo.Result;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.bind.annotation.GetMapping;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RestController;

@RestController

@RequestMapping("categorys")

public class CategoryController {

@Autowired

private CategoryService categoryService;

@GetMapping

public Result categories(){

return categoryService.findAll();

}

}

17.1.3 CategoryService

Result findAll();

17.1.4 CategoryServiceImpl

@Override

public Result findAll() {

// 没有任何参数,所有一个空的LambdaQueryWrapper即可

List categories = categoryMapper.selectList(new LambdaQueryWrapper<>());

//页面交互的对象

return Result.success(copyList(categories));

}

private List copyList(List categories) {

ArrayList categoryVoList = new ArrayList<>();

for (Category category : categories) {

categoryVoList.add(copy(category));

}

return categoryVoList;

}

private CategoryVo copy(Category category) {

CategoryVo categoryVo = new CategoryVo();

BeanUtils.copyProperties(category,categoryVo);

//id不一致要重新设立

// categoryVo.setId(String.valueOf(category.getId()));

return categoryVo;

}

17.1.5 测试

17.2 获取所有标签

17.2.1 接口说明

接口url:/tags

请求方式:GET

请求参数:无

返回数据:

{

"success": true,

"code": 200,

"msg": "success",

"data": [

{

"id": 5,

"tagName": "springboot"

},

{

"id": 6,

"tagName": "spring"

},

{

"id": 7,

"tagName": "springmvc"

},

{

"id": 8,

"tagName": "11"

}

]

}

17.2.2 TagsController

@Autowired

private TagService tagService;

@GetMapping

public Result findAll(){

/**

* 查询所有的文章标签

* @return

*/

return tagService.findAll();

}

17.2.3 TagService

/**

* 查询所有文章标签

* @return

*/

Result findAll();

17.2.4 TagServiceImpl

@Override

public Result findAll() {

List tags = this.tagMapper.selectList(new LambdaQueryWrapper<>());

return Result.success(copyList(tags));

}

17.2.5 测试

17.3 发布文章

17.3.1接口说明

请求内容是object({content: “ww”, contentHtml: “ww↵”})是因为本身为makedown的编辑器id指的是文章id

接口url:/articles/publish

请求方式:POST

请求参数:

返回数据:

{

"success": true,

"code": 200,

"msg": "success",

"data": {"id":12232323}

}

17.3.2修改ArticleController

@PostMapping("publish")

public Result publish(@RequestBody ArticleParam articleParam){

return articleService.publish(articleParam);

}

17.3.3 参数 param

ArticleParam

package com.jihu.blog.vo.params;

import com.jihu.blog.vo.CategoryVo;

import com.jihu.blog.vo.TagVo;

import lombok.Data;

import java.util.List;

@Data

public class ArticleParam {

private Long id;

private ArticleBodyParam body;

private CategoryVo category;

private String summary;

private List tags;

private String title;

}

ArticleBodyParam

package com.jihu.blog.vo.params;

import lombok.Data;

@Data

public class ArticleBodyParam {

private String content;

private String contentHtml;

}

17.3.4 Service层

ArticleService

/**

* 文章发布服务

* @param articleParam

* @return

*/

Result publish(ArticleParam articleParam);

ArticleServiceImpl

@Override

public Result publish(ArticleParam articleParam) {

//注意想要拿到数据必须将接口加入到登录拦截当中

SysUser sysUser = UserThreadLocal.get();

/**

* 1.发布文章 目的 构建Article对象

* 2.作者id 当前的登录用户

*3.标签 要将标签加入到 关联表当中

* 4.body 内容存储 article bodyId

*/

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);

//插入之后 会生成一个文章id(因为新建的文章没有文章id所以要insert一下

//官网解释:"insert后主键会自动'set到实体的ID字段。所以你只需要"getid()就好

//利用主键自增,mp的insert操作后id值会回到参数对象中

//https://blog.csdn.net/HSJ0170/article/details/107982866

articleMapper.insert(article);

//tags

List tags = articleParam.getTags();

if ( tags != null){

for (TagVo tag : tags) {

ArticleTag articleTag = new ArticleTag();

articleTag.setArticleId(article.getId());

articleTag.setTagId(tag.getId());

articleTagMapper.insert(articleTag);

}

}

//body

ArticleBody articleBody = new ArticleBody();

articleBody.setContent(articleParam.getBody().getContent());

articleBody.setContentHtml(articleParam.getBody().getContentHtml());

articleBody.setArticleId(article.getId());

articleBodyMapper.insert(articleBody);

//插入完之后再给一个id

article.setBodyId(articleBody.getId());

//MybatisPlus中的save方法什么时候执行insert,什么时候执行update

// https://www.cxyzjd.com/article/Horse7/103868144

//只有当更改数据库时才插入或者更新,一般查询就可以了

articleMapper.updateById(article);

Map map = new HashMap<>();

map.put("id",article.getId().toString());

//或者这样也行

// ArticleVo articleVo = new ArticleVo();

// articleVo.setId(article.getId());

// return Result.success(articleVo);

return Result.success(map);

}

17.3.5 修改 WebMVCConfig

当然登录拦截器中,需要加入发布文章的拦截:

@Override

public void addInterceptors(InterceptorRegistry registry) {

//拦截test接口,后续实际遇到需要拦截的接口时,在配置为真正的拦截接口

registry.addInterceptor(loginInterceptor)

.addPathPatterns("/test")

.addPathPatterns("/comments/create/change")

.addPathPatterns("/articles/publish");

}

17.3.6 mapper层

ArticleBodyMapper

package com.jihu.blog.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;

import com.jihu.blog.dao.pojo.ArticleBody;

public interface ArticleBodyMapper extends BaseMapper {

}

ArticleTagMapper

package com.jihu.blog.dao.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;

import com.jihu.blog.dao.pojo.ArticleTag;

public interface ArticleTagMapper extends BaseMapper {

}

17.3.7 ArticleVo

package com.jihu.blog.vo;

import com.fasterxml.jackson.databind.annotation.JsonSerialize;

import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;

import lombok.Data;

import java.util.List;

//建立与前端交互的Vo文件

@Data

public class ArticleVo {

@JsonSerialize(using = ToStringSerializer.class)

private Long id;

private String title;

private String summary;

private int commentCounts;

private int viewCounts;

private int weight;

/**

* 创建时间

*/

private String createDate;

private String author;

private ArticleBodyVo body;

private List tags;

private CategoryVo category;

}

17.3.8 ArticleTag

package com.jihu.blog.dao.pojo;

import lombok.Data;

@Data

public class ArticleTag {

private Long id;

private Long articleId;

private Long tagId;

}

17.3.9 测试

18.AOP日志


IOC是spring的两大核心概念之一,IOC给我们提供了一个IOCbean容器,这个容器会帮我们自动去创建对象,不需要我们手动创建,IOC实现创建的通过DI(Dependency Injection 依赖注入),我们可以通过写Java注解代码或者是XML配置方式,把我们想要注入对象所依赖的一些其他的bean,自动的注入进去,他是通过byName或byType类型的方式来帮助我们注入。正是因为有了依赖注入,使得IOC有这非常强大的好处,解耦。

可以举个例子,JdbcTemplate 或者 SqlSessionFactory 这种bean,如果我们要把他注入到容器里面,他是需要依赖一个数据源的,如果我们把JdbcTemplate 或者 Druid 的数据源强耦合在一起,会导致一个问题,当我们想要使用jdbctemplate必须要使用Druid数据源,那么依赖注入能够帮助我们在Jdbc注入的时候,只需要让他依赖一个DataSource接口,不需要去依赖具体的实现,这样的好处就是,将来我们给容器里面注入一个Druid数据源,他就会自动注入到JdbcTemplate如果我们注入一个其他的也是一样的。比如说c3p0也是一样的,这样的话,JdbcTemplate和数据源完全的解耦了,不强依赖与任何一个数据源,在spring启动的时候,就会把所有的bean全部创建好,这样的话,程序在运行的时候就不需要创建bean了,运行速度会更快,还有IOC管理bean的时候默认是单例的,可以节省时间,提高性能,

Spring IOC ,AOP,MVC 的理解

Springboot AOP日志相关讲解

在不改变原有方法基础上对原有方法进行增强

1. 新建LogAnnotation

com.jihu.blog.commom.aop.LogAnnotation

package com.jihu.blog.commom.aop;

import java.lang.annotation.*;

//type代表可以放在类上面 ,method代表可以放在方法上

@Target({ElementType.METHOD})

@Retention(RetentionPolicy.RUNTIME)

@Documented

public @interface LogAnnotation {

String module() default "";

String operator() default "";

}

2.修改ArticleController

com.jihu.blog.controller.ArticleController

//首页 文章列表

@PostMapping

//加上此注解,代表要对此接口记录日志

@LogAnnotation(module="文章",operator="获取文章列表")

public Result listArticle(@RequestBody PageParams pageParams){

// int i =10/0;

//ArticleVo 页面接收的数据

return articleService.listArticle(pageParams);

}

3.新建LogAspect

com.jihu.blog.commom.aop.LogAspect

package com.jihu.blog.commom.aop;

import com.alibaba.fastjson.JSON;

import com.jihu.blog.utils.HttpContextUtils;

import com.jihu.blog.utils.IpUtils;

import lombok.extern.slf4j.Slf4j;

import org.aspectj.lang.ProceedingJoinPoint;

import org.aspectj.lang.annotation.Around;

import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.annotation.Pointcut;

import org.aspectj.lang.reflect.MethodSignature;

import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

import java.lang.reflect.Method;

@Component

@Aspect //切面 定义了通知和切点的关系

@Slf4j

public class LogAspect {

@Pointcut("@annotation(com.jihu.blog.commom.aop.LogAnnotation)")

public void pt(){}

//环绕通知

@Around("pt()")

public Object log(ProceedingJoinPoint joinPoint) throws Throwable {

long beginTime = System.currentTimeMillis();

//执行方法

Object result = joinPoint.proceed();

//执行时长(毫秒)

long time = System.currentTimeMillis() - beginTime;

//保存日志

recordLog(joinPoint,time);

return result;

}

private void recordLog(ProceedingJoinPoint joinPoint, long time) {

MethodSignature signature = (MethodSignature) joinPoint.getSignature();

Method method = signature.getMethod();

LogAnnotation logAnnotation = method.getAnnotation(LogAnnotation.class);

log.info("=====================log start================================");

log.info("module:{}",logAnnotation.module());

log.info("operation:{}",logAnnotation.operator());

//请求的方法名

String className = joinPoint.getTarget().getClass().getName();

String methodName = signature.getName();

log.info("request method:{}",className + "." + methodName + "()");

// //请求的参数

Object[] args = joinPoint.getArgs();

String params = JSON.toJSONString(args[0]);

log.info("params:{}",params);

//获取request 设置IP地址

HttpServletRequest request = HttpContextUtils.getHttpServletRequest();

log.info("ip:{}", IpUtils.getIpAddr(request));

log.info("excute time : {} ms",time);

log.info("=====================log end================================");

}

}

用到的方法类

4.新建HttpContextUtils

com.jihu.blog.utils.HttpContextUtils

package com.jihu.blog.utils;

import org.springframework.web.context.request.RequestContextHolder;

import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**

* HttpServletRequest

*

*/

public class HttpContextUtils {

public static HttpServletRequest getHttpServletRequest() {

return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

}

}

5.新建IpUtils

com.jihu.blog.utils.IpUtils

package com.jihu.blog.utils;

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.lang3.StringUtils;

import javax.servlet.http.HttpServletRequest;

/**

* 获取Ip

*

*/

@Slf4j

public class IpUtils {

/**

* 获取IP地址

*

* 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址

* 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址

*/

public static String getIpAddr(HttpServletRequest request) {

String ip = null, unknown = "unknown", seperator = ",";

int maxLength = 15;

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) {

log.error("IpUtils ERROR ", e);

}

// 使用代理,则获取第一个IP地址

if (StringUtils.isEmpty(ip) && ip.length() > maxLength) {

int idx = ip.indexOf(seperator);

if (idx > 0) {

ip = ip.substring(0, idx);

}

}

return ip;

}

/**

* 获取ip地址

*

* @return

*/

public static String getIpAddr() {

HttpServletRequest request = HttpContextUtils.getHttpServletRequest();

return getIpAddr(request);

}

}

6.测试

bug修正


因为数据库中的create_date类型是bigint ,不是date类型的 所有需要进行如下操作。

防止拿到的值是null值,因为拿到的是毫秒值,需要对其进行转化,Y表示年,m表示月,对时间进行重写。

相关函数说明

修改ArticleMapper.xml

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

19.文章图片上传


19.1接口说明

接口url:/upload

请求方式:POST

请求参数:

参数名称

参数类型

说明

image

file

上传的文件名称

返回数据:

{

"success":true,

"code":200,

"msg":"success",

"data":"https://static.mszlu.com/aa.png"

}

修改pom文件引入七牛云的sdkpom.xml

com.qiniu

qiniu-java-sdk

[7.7.0, 7.7.99]

19.2 新建UploadController

package com.jihu.blog.controller;

import com.mszlu.blog.utils.QiniuUtils;

import com.mszlu.blog.vo.Result;

import org.apache.commons.lang3.StringUtils;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.bind.annotation.PostMapping;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestParam;

import org.springframework.web.bind.annotation.RestController;

import org.springframework.web.multipart.MultipartFile;

import java.util.UUID;

@RestController

@RequestMapping("upload")

public class UploadController {

@Autowired

private QiniuUtils qiniuUtils;

//https://blog.csdn.net/justry_deng/article/details/80855235 MultipartFile介绍

@PostMapping

public Result upload(@RequestParam("image")MultipartFile file){

//原始文件名称 比如说aa.png

String originalFilename = file.getOriginalFilename();

//唯一的文件名称

String fileName = UUID.randomUUID().toString()+"."+StringUtils.substringAfterLast(originalFilename, ".");

//上传文件上传到那里呢? 七牛云 云服务器

//降低我们自身应用服务器的带宽消耗

boolean upload = qiniuUtils.upload(file, fileName);

if (upload) {

return Result.success(QiniuUtils.url+fileName);

}

return Result.fail(20001,"上传失败");

}

19.3 使用七牛云

注意七牛云测试域名 https://static.mszlu.com/ 一个月一回收,记得去修改。springboot默认只上传1M的图片大小所以修改文件配置src/main/resources/application.properties

# 上传文件总的最大值

spring.servlet.multipart.max-request-size=20MB

# 单个文件的最大值

spring.servlet.multipart.max-file-size=2MB

七牛云建立存储空间教程

19.4新建QiniuUtils

com.jihu.blog.utils.QiniuUtils.java

package com.mszlu.blog.utils;

import com.alibaba.fastjson.JSON;

import com.qiniu.http.Response;

import com.qiniu.storage.Configuration;

import com.qiniu.storage.Region;

import com.qiniu.storage.UploadManager;

import com.qiniu.storage.model.DefaultPutRet;

import com.qiniu.util.Auth;

import org.apache.commons.lang3.StringUtils;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.stereotype.Component;

import org.springframework.web.multipart.MultipartFile;

@Component

public class QiniuUtils {

public static final String url = "https://static.mszlu.com/";

//修改以下两个值放到proprietarties中,在密钥管理中获取

@Value("${qiniu.accessKey}")

private String accessKey;

@Value("${qiniu.accessSecretKey}")

private String accessSecretKey;

public boolean upload(MultipartFile file,String fileName){

//构造一个带指定 Region 对象的配置类

Configuration cfg = new Configuration(Region.huabei());

//...其他参数参考类注释

UploadManager uploadManager = new UploadManager(cfg);

//...生成上传凭证,然后准备上传,修改上传名称为自己创立空间的空间名称(是你自己的)

String bucket = "mszlu";

//默认不指定key的情况下,以文件内容的hash值作为文件名

try {

byte[] uploadBytes = file.getBytes();

Auth auth = Auth.create(accessKey, accessSecretKey);

String upToken = auth.uploadToken(bucket);

Response response = uploadManager.put(uploadBytes, fileName, upToken);

//解析上传成功的结果

DefaultPutRet putRet = JSON.parseObject(response.bodyString(), DefaultPutRet.class);

return true;

} catch (Exception ex) {

ex.printStackTrace();

}

return false;

}

}

20.导航-文章分类


20.1 查询所有的文章分类

20.1.1接口说明

接口url:/categorys/detail

请求方式:GET

请求参数:无

返回数据:

{

"success": true,

"code": 200,

"msg": "success",

"data": [

{

"id": 1,

"avatar": "/static/category/front.png",

"categoryName": "前端",

"description": "前端是什么,大前端"

},

{

"id": 2,

"avatar": "/static/category/back.png",

"categoryName": "后端",

"description": "后端最牛叉"

}

]

}

20.1.2 修改CategoryVo

@Data

public class CategoryVo {

private Long id;

private String avatar;

private String categoryName;

private String description;

}

20.1.3 修改CategoryController

@GetMapping("detail")

public Result categorydetail(){

return categoryService.findCategoryByDetail();

}

20.1.4 修改CategoryService

/**

* 查询所有文章分类

* @return

*/

Result findCategoryByDetail();

20.1.5 修改CategoryServiceImpl

@Override

public Result findCategoryByDetail() {

LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();

List categories = categoryMapper.selectList(queryWrapper);

return Result.success(copyList(categories));

}

20.1.6 文章分类显示

20.2 查询所有的标签

20.2.1 接口说明

接口url:/tags/detail

请求方式:GET

请求参数:无

返回数据:

{

"success": true,

"code": 200,

"msg": "success",

"data": [

{

"id": 5,

"tagName": "springboot",

"avatar": "/static/tag/java.png"

},

{

"id": 6,

"tagName": "spring",

"avatar": "/static/tag/java.png"

}

]

}

20.2.2 修改TagVo

@Data

public class TagVo {

private Long id;

private String tagName;

private String avatar;

}

20.2.3 修改TagsController

@GetMapping("detail")

public Result findTagDetail(){

return tagService.findTagDetail();

}

20.2.4 修改TagService

/**

* 查询所有的标签

* @return

*/

Result findTagDetail();

20.2.5 修改TagServiceImpl

@Override

public Result findTagDetail() {

LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();

List tags = tagMapper.selectList(queryWrapper);

return Result.success(copyList(tags));

}

20.2.6 显示结果:

21.分类文章列表


21.1接口说明

接口url:/category/detail/{id}

请求方式:GET

请求参数:

参数名称

参数类型

说明

id

分类id

路径参数

返回数据:

{

"success": true,

"code": 200,

"msg": "success",

"data":

{

"id": 1,

"avatar": "/static/category/front.png",

"categoryName": "前端",

"description": "前端是什么,大前端"

}

}

21.2修改CategoryController

@GetMapping("detail/{id}")

public Result categorydetailById(@PathVariable("id") Long id){

return categoryService.categoryDetailById(id);

}

21.3修改CategoryService

Result categoryDetailById(Long id);

21.4修改CategoryServiceImpl

@Override

public Result categoryDetailById(Long id) {

// LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();

Category category = categoryMapper.selectById(id);

return Result.success(copy(category));

}

完成上面这些只能说是可以显示文章分类的图标了

但是如果想显示后端所有的归属内容得在文章查询列表出进行queryWrapper查找,当文章分类标签不是null时,加入文章分类标签这个查询元素进行分类修改。

21.5修改ArticleServiceImpl

@Override

public Result listArticle(PageParams pageParams) {

Page

page = new Page<>(pageParams.getPage(), pageParams.getPageSize());

LambdaQueryWrapper

queryWrapper = new LambdaQueryWrapper<>();

//查询文章的参数 加上分类id,判断不为空 加上分类条件

if (pageParams.getCategoryId() != null){

//相当于 category_id = #{category_id}

queryWrapper.eq(Article::getCategoryId,pageParams.getCategoryId());

}

//是否置顶进行排序

queryWrapper.orderByDesc(Article::getWeight,Article::getCreateDate);

Page

articlePage = articleMapper.selectPage(page, queryWrapper);

List

records = articlePage.getRecords();

//能直接返回吗 肯定不行 所以需要进行如下转换

List articleVoList = copyList(records,true,true);

return Result.success(articleVoList);

}

21.6修改PageParams

@Data

public class PageParams {

private int page = 1;

private int pageSize = 10;

private Long categoryId;

private Long tagId;

}

最后就可以显示所有文章分类的每个标签下的内容了

22.标签文章列表


22.1接口说明

接口url:/tags/detail/{id}

请求方式:GET

请求参数:

参数名称

参数类型

说明

id

标签id

路径参数

返回数据:

{

"success": true,

"code": 200,

"msg": "success",

"data":

{

"id": 5,

"tagName": "springboot",

"avatar": "/static/tag/java.png"

}

}

22.2 修改TagsController

/**

* 查询所有文章标签下所有的文章

* @return

*/

@GetMapping("detail/{id}")

public Result findTagDetailById(@PathVariable("id") Long id){

return tagService.findTagDetailById(id);

}

22.3修改TagService

Result findTagDetailById(Long id);

22.4修改TagServiceImpl

@Override

public Result findTagDetailById(Long id) {

Tag tag = tagMapper.selectById(id);

return Result.success(copy(tag));

}

完成上面这些这保证了文章标签显示出来了我们需要重写文章查询接口,保证当遇到标签查询时我们可以做到正确查询文章标签所对应的内容,要不每一个标签查出来的内容都是一样的。

22.5修改ArticleServiceImpl

@Override

public Result listArticle(PageParams pageParams) {

Page

page = new Page<>(pageParams.getPage(), pageParams.getPageSize());

LambdaQueryWrapper

queryWrapper = new LambdaQueryWrapper<>();

//相当于 category_id = #{category_id}

//根据文章分类获取文章

if (pageParams.getCategoryId() != null){

queryWrapper.eq(Article::getCategoryId,pageParams.getCategoryId());

}

-----------添加的代码开始------------

//根据标签获取文章

ArrayList articleIdList = new ArrayList<>();

if (pageParams.getTagId() != null){

//加入标签条件查询

//article表中并没有tag字段 一篇文章有多个标签

//articie_tog article_id 1:n tag_id

//我们需要利用一个全新的属于文章标签的queryWrapper将这篇文章的article_Tag查出来,保存到一个list当中。

// 然后再根据queryWrapper的in方法选择我们需要的标签即可。

LambdaQueryWrapper queryWrapper1 = new LambdaQueryWrapper<>();

queryWrapper1.eq(ArticleTag::getTagId, pageParams.getTagId());

List articleTags = articleTagMapper.selectList(queryWrapper1);

for (ArticleTag articleTag : articleTags) {

articleIdList.add(articleTag.getArticleId());

}

if (articleIdList.size() > 0){

// and id in (1,2,3)

queryWrapper.in(Article::getId,articleIdList);

}

}

---------添加的代码结束-----------------

//是否置顶进行排序

queryWrapper.orderByDesc(Article::getWeight,Article::getCreateDate);

Page

articlePage = articleMapper.selectPage(page, queryWrapper);

List

records = articlePage.getRecords();

//能直接返回吗 肯定不行 所以需要进行如下转换

List articleVoList = copyList(records,true,true);

return Result.success(articleVoList);

}

22.6测试

最终的结果如下,每一个标签下都对应着该标签所对应的文章

23. 归档文章列表


23.1接口说明

接口url:/articles

请求方式:POST

请求参数:

参数名称

参数类型

说明

year

string

month

string

返回数据

{

"success": true,

"code": 200,

"msg": "success",

"data": [文章列表,数据同之前的文章列表接口]

}

mybatisplus驼峰命名和mapper.xml使用

23.2修改PageParams

package com.jihu.blog.vo.params;

import lombok.Data;

@Data

public class PageParams {

private int Page =1; //当前页数

private int PageSize =10; //每页显示的数量

private Long categoryId;

private Long tagId;

private String year;

private String month;

//为了让传递的值为6变成06

public String getMonth(){

if (month != null && this.month.length()==1){

return "0"+this.month;

}

return this.month;

}

}

23.3修改ArticleServiceImpl

@Override

public Result listArticle(PageParams pageParams) {

Page

page = new Page<>(pageParams.getPage(),pageParams.getPageSize());

IPage

articleIPage = articleMapper.listArticle(page, pageParams.getCategoryId(), pageParams.getTagId(), pageParams.getYear(), pageParams.getMonth());

List

records = articleIPage.getRecords();

return Result.success(copyList(records,true,true));

}

23.4修改ArticleMapper

IPage

listArticle(Page
page,

Long categoryId,

Long tagId,

String year,

String month);

23.5修改ArticleMapper.xml

resultMap和resultType区别 https://blog.csdn.net/xushiyu1996818/article/details/89075069?spm=1001.2101.3001.6650.4&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-4.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7Edefault-4.no_search_link驼峰命名法 https://blog.csdn.net/A_Java_Dog/article/details/107006391?spm=1001.2101.3001.6650.6&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-6.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-6.no_search_linkmybatis中xml文件用法 https://blog.csdn.net/weixin_43882997/article/details/85625805动态sql https://www.jianshu.com/p/e309ae5e4a77驼峰命名 https://zoutao.blog.csdn.net/article/details/82685918?spm=1001.2101.3001.6650.18&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-18.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-18.no_search_link

23.6测试

24、统一缓存处理(优化)


内存的访问速度 远远大于 磁盘的访问速度 (1000倍起)Spring Cache介绍

Cache

@Target({ElementType.METHOD})

@Retention(RetentionPolicy.RUNTIME)

@Documented

public @interface Cache {

long expire() default 1 * 60 * 1000;

String name() default "";

}

CacheAspect

package com.jihu.blog.commom.cache;

import com.alibaba.fastjson.JSON;

import com.jihu.blog.vo.Result;

import lombok.extern.slf4j.Slf4j;

import org.apache.commons.codec.digest.DigestUtils;

import org.apache.commons.lang3.StringUtils;

import org.aspectj.lang.ProceedingJoinPoint;

import org.aspectj.lang.Signature;

import org.aspectj.lang.annotation.Around;

import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.annotation.Pointcut;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

import java.time.Duration;

@Component

@Aspect //切面 定义了通知和切点的关系

@Slf4j

public class CacheAspect {

@Autowired

private RedisTemplate redisTemplate;

@Pointcut("@annotation(com.jihu.blog.commom.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

if(args[i] != null) {

params += JSON.toJSONString(args[i]);

parameterTypes[i] = args[i].getClass();

}else {

parameterTypes[i] = null;

}

}

if (StringUtils.isNotEmpty(params)) {

//加密 以防出现key过长以及字符转义获取不到的情况

params = DigestUtils.md5Hex(params);

}

Method method = pjp.getSignature().getDeclaringType().getMethod(methodName, parameterTypes);

//获取Cache注解

Cache annotation = method.getAnnotation(Cache.class);

//缓存过期时间

long expire = annotation.expire();

//缓存名称

String name = annotation.name();

//先从redis获取

String redisKey = name + "::" + className+"::"+methodName+"::"+params;

String redisValue = redisTemplate.opsForValue().get(redisKey);

if (StringUtils.isNotEmpty(redisValue)){

log.info("走了缓存~~~,{},{}",className,methodName);

return JSON.parseObject(redisValue, Result.class);

}

Object proceed = 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,"系统错误");

}

}

使用:

在需要的地方加上就可以了

测试

思考别的优化


mongodbredis incr

1.文章可以放人es当中,便于后续中文分词搜索。

2.评论数据,可以考虑放到mongodb当中,电商系统当中 评论数据放入mongdb中

3.阅读数和评论数,考虑把阅读数和评论数 增加的时候放入 redis incr 自增,使用定时任务 定时把数据固化到数据库当中

4.为了加快访问速度,部署的时候,可以把图片,js,css等放入七牛云存储,加快网站访问速度。

25.管理后台


25.1搭建项目

25.1.1 新建maven工程 blog-admin

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 " target="_blank">http://maven.apache.org/xsd/maven-4.0.0.xsd">

blog-parent

com.jihu

1.0-SNAPSHOT

4.0.0

blog-admin

org.springframework.boot

spring-boot-starter

org.springframework.boot

spring-boot-starter-logging

org.springframework.boot

spring-boot-starter-log4j2

org.springframework.boot

spring-boot-starter-aop

org.springframework.boot

spring-boot-starter-mail

org.springframework.boot

spring-boot-starter-web

org.springframework.boot

spring-boot-starter-test

test

com.alibaba

fastjson

1.2.76

mysql

mysql-connector-java

org.springframework.boot

spring-boot-configuration-processor

true

org.apache.commons

commons-lang3

commons-collections

commons-collections

3.2.2

commons-codec

commons-codec

com.baomidou

mybatis-plus-boot-starter

3.4.3

org.projectlombok

lombok

joda-time

joda-time

2.10.10

25.1.2 application.properties:

#server

server.port= 8889

spring.application.name=mszlu_admin_blog

# datasource

spring.datasource.url=jdbc:mysql://localhost:3306/blog1?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_

#指定mapper文件的位置

mybatis-plus.mapper-locations=classpath:mapper/*.xml

25.1.3 mybatis-plus配置:

com.jihu.blog.admin.config.MybatisPlusConfig

package com.jihu.blog.admin.config;

import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;

import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;

import org.mybatis.spring.annotation.MapperScan;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

@Configuration

//扫包,将此包下的接口生成代理实现类,并且注册到spring容器中

@MapperScan("com.jihu.blog.admin.mapper")

public class MybatisPlusConfig {

//分页插件

@Bean

public MybatisPlusInterceptor mybatisPlusInterceptor(){

MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

interceptor.addInnerInterceptor(new PaginationInnerInterceptor());

return interceptor;

}

}

25.1.4 启动类

package com.jihu.blog.admin;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication

public class AdminApp {

public static void main(String[] args) {

SpringApplication.run(AdminApp.class,args);

}

}

25.1.5导入前端工程

放入resources下的static目录中

25.1.6新建表

后台管理用户表

CREATE TABLE `blog`.`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;

权限表

CREATE TABLE `blog`.`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;

用户和权限的关联表

CREATE TABLE `blog`.`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;

权限管理页面展示

25.1.7AdminController

package com.jihu.blog.admin.controller;

import com.jihu.blog.admin.model.params.PageParam;

import com.jihu.blog.admin.pojo.Permission;

import com.jihu.blog.admin.service.PermissionService;

import com.jihu.blog.admin.vo.Result;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.bind.annotation.*;

@RestController

@RequestMapping("admin")

public class AdminController {

@Autowired

private PermissionService permissionService;

@PostMapping("permission/permissionList")

public Result listpermission(@RequestBody PageParam pageParam){

return permissionService.listpermission(pageParam);

}

@PostMapping("permission/add")

public Result permissionadd(@RequestBody Permission permission){

return permissionService.permissionadd(permission);

}

@PostMapping("permission/update")

public Result permissionupdate(@RequestBody Permission permission){

return permissionService.permissionupdate(permission);

}

@GetMapping("permission/delete/{id}")

public Result permissiondeleteById(@PathVariable("id") Long id){

return permissionService.permissiondeleteById(id);

}

}

25.1.8 新建Permission

com.jihu.blog.admin.pojo.Permission

package com.jihu.blog.admin.pojo;

import com.baomidou.mybatisplus.annotation.IdType;

import com.baomidou.mybatisplus.annotation.TableId;

import lombok.Data;

@Data

public class Permission {

@TableId(type =IdType.AUTO)

private Long id;

private String name;

private String path;

private String description;

}

25.1.9 新建PageParam

com.jihu.blog.admin.model.params.PageParam

package com.jihu.blog.admin.model.params;

import lombok.Data;

@Data

public class PageParam {

//当前页

private Integer currentPage;

//页面个数

private Integer pageSize;

//查询条件

private String queryString;

}

25.1.10 新建PageResult

package com.jihu.blog.admin.vo;

import lombok.Data;

import java.util.List;

@Data

public class PageResult {

private List list;

private Long total;

}

25.1.11 新建Result

package com.jihu.blog.admin.vo;

import lombok.AllArgsConstructor;

import lombok.Data;

@Data

@AllArgsConstructor

public class Result {

private boolean success;

private int code ;

private String msg;

private Object data;

public static Result success(Object data) {

return new Result(true, 200, "success", data);

}

public static Result fail(int code,String msg) {

return new Result(false, code, msg, null);

}

}

25.1.12新建PermissionService

package com.jihu.blog.admin.service;

import com.jihu.blog.admin.model.params.PageParam;

import com.jihu.blog.admin.pojo.Permission;

import com.jihu.blog.admin.vo.Result;

public interface PermissionService {

/**

* 查询所有权限列表

* @param pageParam

* @return

*/

Result listpermission(PageParam pageParam);

/**

* 添加权限

* @param permission

* @return

*/

Result permissionadd(Permission permission);

/**

* 更新权限

* @param permission

* @return

*/

Result permissionupdate(Permission permission);

/**

* 删除权限

* @param id

* @return

*/

Result permissiondeleteById(Long id);

}

25.1.13新建PermissionServiceImpl

package com.jihu.blog.admin.service.Impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;

import com.jihu.blog.admin.mapper.Permissionmapper;

import com.jihu.blog.admin.model.params.PageParam;

import com.jihu.blog.admin.pojo.Permission;

import com.jihu.blog.admin.service.PermissionService;

import com.jihu.blog.admin.vo.PageResult;

import com.jihu.blog.admin.vo.Result;

import org.apache.commons.lang3.StringUtils;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Service;

@Service

public class PermissionServiceImpl implements PermissionService {

@Autowired

private Permissionmapper permissionmapper;

@Override

public Result listpermission(PageParam pageParam) {

/**

* 要的数据,管理台,表的所有的字段 permission

* 分页查询

*/

Page page = new Page<>(pageParam.getCurrentPage(),pageParam.getPageSize());

LambdaQueryWrapper lambdaQueryWrapper = new LambdaQueryWrapper<>();

if (StringUtils.isNoneBlank(pageParam.getQueryString())){

lambdaQueryWrapper.eq(Permission::getName,pageParam.getQueryString());

}

Page permissionPage = permissionmapper.selectPage(page, lambdaQueryWrapper);

PageResult pageResult = new PageResult<>();

pageResult.setList(permissionPage.getRecords());

pageResult.setTotal(permissionPage.getTotal());

return Result.success(pageResult);

}

@Override

public Result permissionadd(Permission permission) {

int result = permissionmapper.insert(permission);

return Result.success(result);

}

@Override

public Result permissionupdate(Permission permission) {

int result = permissionmapper.updateById(permission);

return Result.success(result);

}

@Override

public Result permissiondeleteById(Long id) {

int result = permissionmapper.deleteById(id);

return Result.success(result);

}

}

25.1.14测试:

26、Security集成


26.1添加依赖

org.springframework.boot

spring-boot-starter-security

26.2 配置

package com.jihu.blog.admin.config;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import org.springframework.security.config.annotation.web.builders.WebSecurity;

import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration;

import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration

public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Bean

public BCryptPasswordEncoder bCryptPasswordEncoder(){

return new BCryptPasswordEncoder();

}

public static void main(String[] args) {

//加密策略 MD5 不安全 彩虹表 MD5 加盐

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("/user/findAll").hasRole("admin") //访问接口需要admin的角色

.antMatchers("/css/**").permitAll()

.antMatchers("/img/**").permitAll()

.antMatchers("/js/**").permitAll()

.antMatchers("/plugins/**").permitAll()

.antMatchers("/admin/**").access("@authService.auth(request,authentication)") //自定义service 来去实现实时的权限认证

.antMatchers("/pages/**").authenticated()

.and().formLogin()

.loginPage("/login.html") //自定义的登录页面

.loginProcessingUrl("/login") //登录处理接口

.usernameParameter("username") //定义登录时的用户名的key 默认为username

.passwordParameter("password") //定义登录时的密码key,默认是password

.defaultSuccessUrl("/pages/main.html")

.failureUrl("/login.html")

.permitAll() //通过 不拦截,更加前面配的路径决定,这是指和登录表单相关的接口 都通过

.and().logout() //退出登录配置

.logoutUrl("/logout") //退出登录接口

.logoutSuccessUrl("/login.html")

.permitAll() //退出登录的接口放行

.and()

.httpBasic()

.and()

.csrf().disable() //csrf关闭 如果自定义登录 需要关闭

.headers().frameOptions().sameOrigin(); //支持iframe 页面嵌套

}

}

26.3登录认证

Admin

package com.jihu.blog.admin.pojo;

import com.baomidou.mybatisplus.annotation.IdType;

import com.baomidou.mybatisplus.annotation.TableId;

import lombok.Data;

@Data

public class Admin {

@TableId(type = IdType.AUTO)

private Long id;

private String username;

private String password;

}

SecurityUserService

package com.jihu.blog.admin.service;

import com.jihu.blog.admin.pojo.Admin;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.security.core.userdetails.User;

import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.security.core.userdetails.UserDetailsService;

import org.springframework.security.core.userdetails.UsernameNotFoundException;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import org.springframework.stereotype.Component;

import java.util.ArrayList;

@Component

public class SecurityUserService implements UserDetailsService {

@Autowired

private AdminServilce adminServilce;

@Override

public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

//登录的时候,把username传递到这里

//通过username查询 admin表,如果admin存在 将密码告诉spring security

//如果不存在 返回null 认证失败

Admin admin = adminServilce.findAdminByUsername(username);

if (admin == null){

//登录失败

return null;

}

UserDetails userDetails = new User(username,admin.getPassword(),new ArrayList<>());

//剩下的认证 就由框架帮我们完成

return userDetails;

}

public static void main(String[] args) {

System.out.println(new BCryptPasswordEncoder().encode("123456"));

}

}

AdminServilce

package com.jihu.blog.admin.service;

import com.jihu.blog.admin.pojo.Admin;

import com.jihu.blog.admin.pojo.Permission;

import java.util.List;

public interface AdminServilce {

Admin findAdminByUsername(String username);

/**

* 根据用户id查询用户的权限

* @param id

* @return

*/

List findPermissionsByAdminId(Long id);

}

AdminServilceImpl

package com.jihu.blog.admin.service.Impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;

import com.jihu.blog.admin.mapper.AdminMapper;

import com.jihu.blog.admin.mapper.Permissionmapper;

import com.jihu.blog.admin.pojo.Admin;

import com.jihu.blog.admin.pojo.Permission;

import com.jihu.blog.admin.service.AdminServilce;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Service;

import java.util.List;

@Service

public class AdminServilceImpl implements AdminServilce {

@Autowired

private AdminMapper adminMapper;

@Autowired

private Permissionmapper permissionmapper;

@Override

public Admin findAdminByUsername(String username){

LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();

queryWrapper.eq(Admin::getUsername,username);

queryWrapper.last("limit 1");

Admin admin = adminMapper.selectOne(queryWrapper);

return admin;

}

@Override

public List findPermissionsByAdminId(Long id) {

return permissionmapper.findPermissionsByAdminId(id);

}

}

AdminMapper

package com.jihu.blog.admin.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;

import com.jihu.blog.admin.pojo.Admin;

public interface AdminMapper extends BaseMapper {

}

Permissionmapper

package com.jihu.blog.admin.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;

import com.jihu.blog.admin.pojo.Permission;

import java.util.List;

public interface Permissionmapper extends BaseMapper {

List findPermissionsByAdminId(Long adminId);

}

26.4 权限认证

AuthService

package com.jihu.blog.admin.service;

import com.jihu.blog.admin.pojo.Admin;

import com.jihu.blog.admin.pojo.Permission;

import org.apache.commons.lang3.StringUtils;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.security.core.Authentication;

import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;

import java.util.List;

@Service

@Slf4j

public class AuthService {

@Autowired

private AdminServilce adminServilce;

public boolean auth(HttpServletRequest request, Authentication authentication){

//权限认证

//请求路径

String requestURI = request.getRequestURI();

log.info("request url:{}", requestURI);

Object principal = authentication.getPrincipal();

//true代表放行 false 代表拦截

if (principal == null ||"anonymousUser".equals(principal)){

//未登录

return false;

}

UserDetails userDetails = (UserDetails) principal;

String username = userDetails.getUsername();

Admin admin = adminServilce.findAdminByUsername(username);

if (admin == null){

return false;

}

if (admin.getId() == 1){

//认为是超级管理员

return true;

}

List permissions = adminServilce.findPermissionsByAdminId(admin.getId());

requestURI = StringUtils.split(requestURI,'?')[0];

for (Permission permission : permissions) {

if (requestURI.equals(permission.getPath())){

//代表有权限

return true;

}

}

return false;

}

}

27.作业


添加角色,用户拥有多个角色,一个角色拥有多个权限

28.总结技术亮点


1、jwt + redis

token令牌的登录方式,访问认证速度快,session共享,安全性

redis做了令牌和用户信息的对应管理,

1,进一步增加了安全性

2、登录用户做了缓存

3、灵活控制用户的过期(续期,踢掉线等)

2、threadLocal使用了保存用户信息,请求的线程之内,可以随时获取登录的用户,做了线程隔离

3、在使用完ThreadLocal之后,做了value的删除,防止了内存泄漏(这面试说强引用。弱引用。不是明摆着让面试官间JVM嘛)

4·、线程安全-update table set value = newValue where id=1 and value=oldValue

5、线程池应用非常广,面试7个核心参数(对当前的主业务流程无影响的操作,放入线程池执行)

1.登录,记录日志

6·权限系统重点内容

7·统一日志记录,统一缓存处理

你可能感兴趣的:(java,java,vue)