个人博客

看到别人搭建的自己的博客,想想自己的服务器买了很久也没怎么用过,于是就想搭建一个自己的博客,上传一些自己的学习笔记心得,一些笔记方便自己查看,项目是一个前后端分离的项目,后端springboot、mybatis-plus、shiro、jwt、maven,前端采用 vue +element-ui搭建,欢迎大家访问博客!!!

博客源码

https://gitee.com/ease-i/blog

博客地址

http://www.ease.center

博客图片

  • 主页
  • 详情页
  • 个人主页

    此系统参考诸多大佬的GitHub搭建,后续继续完善功能!

后端:

  • Springboot
  • Shiro
  • Jwt
  • Mybatis-plus
  • Lombok
  • Redis
  • Mysql
  • Maven

前端:

  • Vue
  • Element
  • Vue-Router
  • VueX
  • Axios
  • Mavon-editor

请求API

用户模块

请求地址

请求方式

请求参数

操作

user/login

post

accout,password

登录

user/layout

get

/

退出

user/me

get

/

获取本人信息

博客模块

() 中是默认值

请求地址

请求方式

请求参数

操作

/articles

get

pageNo(1),limit(5)

获取文章

/articles/{id}

get

id

根据id获取文章

/articles/update

post

article实体

编辑文章根据id获取文章

/articles/{id}

delete

id

删除文章根据id删除文章

/articles/pulish

post

article实体

添加文章

/articles/hot

get

/

获取最热门博客

/articles/category/{id}

get

id(tag的id)

根据分类获取文章

/articles//likeAdd/{id}

get

id

增加点赞

/articles/likeSub/{id}

get

id

减少点赞

/articles/upload/image

post

file

上传图片

后端框架搭建

步骤:
  1. Springboot整合Shiro和Jwt实现登录模块
  2. 导入sql文件生成表
  3. Mybatis-plus 生成实体类、Service、Mapper、Mappper.xml
  4. 配置请求跨域
  5. 定制统一返回模板
  6. 定制全局错误拦截
  7. 编写具体业务
主要实现功能
  1. 登录功能,由于是个人博客,所有没有添加注册功能
  2. Springboot整合markdown-it,实现markdown渲染,编辑
  3. Springboot整合Redis 实现查看和点赞功能,相比于mysql提高显示效率
1. Springboot整合Shiro和Jwt实现登录模块

ShiroConfig

package com.anyi.config;

import com.anyi.shiro.AccountRealm;
import com.anyi.shiro.JwtFilter;
import io.jsonwebtoken.Jwt;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.mgt.SessionsSecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @author 安逸i
 * @version 1.0
 */
@Configuration
public class ShiroConfig {
    @Autowired
    RedisSessionDAO redisSessionDAO;
    @Autowired
    JwtFilter jwtFilter;

    @Autowired
    RedisCacheManager redisCacheManager;
    @Bean
    public SessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();

        // inject redisSessionDAO
        sessionManager.setSessionDAO(redisSessionDAO);
        // other stuff...
        return sessionManager;
    }

    @Bean
    public SessionsSecurityManager securityManager(AccountRealm accountRealm, SessionManager sessionManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(accountRealm);

        //inject sessionManager
        securityManager.setSessionManager(sessionManager);

        // inject redisCacheManager
        securityManager.setCacheManager(redisCacheManager);

        // other stuff...

        return securityManager;
    }
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/**", "jwt"); // 主要通过注解方式校验权限
        chainDefinition.addPathDefinitions(filterMap);
        return chainDefinition;
    }
    @Bean("shiroFilterFactoryBean")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
                                                         ShiroFilterChainDefinition shiroFilterChainDefinition) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);
        Map<String, Filter> filters = new HashMap<>();
        filters.put("jwt", jwtFilter);
        shiroFilter.setFilters(filters);
        Map<String, String> filterMap = shiroFilterChainDefinition.getFilterChainMap();
        shiroFilter.setFilterChainDefinitionMap(filterMap);
        return shiroFilter;
    }

}

AccountRealm 用于验证登录

package com.anyi.shiro;

import com.anyi.entity.SysUser;
import com.anyi.service.SysUserService;
import com.anyi.util.JwtUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @author 安逸i
 * @version 1.0
 */
@Component
public class AccountRealm extends AuthorizingRealm {

    @Autowired
    JwtUtils jwtUtils;
    @Autowired
    SysUserService sysUserService;
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }


    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        JwtToken jwtToken = (JwtToken) token;
        String  principal = (String)jwtToken.getPrincipal();
        String userId = jwtUtils.getClaimByToken(principal).getSubject();
        SysUser sysUser = sysUserService.getById(userId);
        if(sysUser == null){
            throw new UnknownAccountException("账户不存在");
        }
        if ((sysUser.getStatus()).equals("-1")){
            throw new LockedAccountException("账户已被锁定");
        }
        AccountProfile accountProfile = new AccountProfile();
        BeanUtils.copyProperties(sysUser,accountProfile);
        return new SimpleAuthenticationInfo(accountProfile,jwtToken.getCredentials(),getName());
    }
}

Jwt 工具类 用于生产 token

package com.anyi.shiro;

import org.apache.shiro.authc.AuthenticationToken;

/**
 * 将传进来的 jwt 封装成 jwtToken
 */

public class JwtToken implements AuthenticationToken {

    private String token;

    public JwtToken(String jwt) {
        this.token = jwt;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}

JwtFilter 对 Token 进行拦截验证

package com.anyi.shiro;

import cn.hutool.json.JSONUtil;
import com.anyi.common.lang.Result;
import com.anyi.util.JwtUtils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import io.jsonwebtoken.Claims;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.ExpiredCredentialsException;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author 安逸i
 * @version 1.0
 */
@Component
public class JwtFilter extends AuthenticatingFilter {
    @Autowired
    private JwtUtils jwtUtils;

    /**
     * 重写方法,将 jwt 封装成自己的token
     * @param servletRequest
     * @param servletResponse
     * @return
     * @throws Exception
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String jwt = request.getHeader("Authorization");
        if (StringUtils.isEmpty(jwt)){
            return null;
        }
        return new JwtToken(jwt);
    }

    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String jwt = request.getHeader("Authorization");
        if (StringUtils.isEmpty(jwt)){
            return true;
        }else {
            Claims claimByToken = jwtUtils.getClaimByToken(jwt);
            if(claimByToken == null || jwtUtils.isTokenExpired(claimByToken.getExpiration())){
                throw new ExpiredCredentialsException("token已经失效,请重新登录");
            }
        }
        return executeLogin(servletRequest,servletResponse);
    }

    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        Throwable throwable = e.getCause() == null ? e:e.getCause();
        Result fail = Result.fail(throwable.getMessage());
        String s = JSONUtil.toJsonStr(fail);
        try {
            httpServletResponse.getWriter().print(s);
        } catch (IOException ex) {
        }
        return false;
    }

    /**
     * 对跨域提供支持
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        // 跨域时会首先发送一个OPTIONS请求,这里我们给OPTIONS请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }
}
2. 导入Sql文件,生成数据库表
/*
SQLyog Ultimate v12.14 (64 bit)
MySQL - 5.7.27 : Database - blog
*********************************************************************
*/


/*!40101 SET NAMES utf8 */;

/*!40101 SET SQL_MODE=''*/;

/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`blog` /*!40100 DEFAULT CHARACTER SET utf8 */;

USE `blog`;

/*Table structure for table `m_article` */

DROP TABLE IF EXISTS `m_article`;

CREATE TABLE `m_article` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `like_counts` int(11) DEFAULT '0',
  `create_date` datetime DEFAULT NULL,
  `summary` varchar(100) DEFAULT NULL,
  `title` varchar(64) DEFAULT NULL,
  `view_counts` int(11) DEFAULT '0',
  `author_id` bigint(20) DEFAULT NULL,
  `context` longtext,
  `category_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `FKndx2m69302cso79y66yxiju4h` (`author_id`),
  KEY `FKjrn3ua4xmiulp8raj7m9d2xk6` (`category_id`),
  CONSTRAINT `FKjrn3ua4xmiulp8raj7m9d2xk6` FOREIGN KEY (`category_id`) REFERENCES `m_category` (`id`),
  CONSTRAINT `FKndx2m69302cso79y66yxiju4h` FOREIGN KEY (`author_id`) REFERENCES `m_user` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8;

/*Data for the table `m_article` */

insert  into `m_article`(`id`,`like_counts`,`create_date`,`summary`,`title`,`view_counts`,`author_id`,`context`,`category_id`) values 

(7,0,'2022-04-15 15:56:58','线程入门','JAVA线程入门',184,1,'## Java学习-09-韩顺平老师\n\n### Java-线程入门01\n\n<h2 id=\"1\">线程相关概念及基本使用</h2>\n\n**线程的相关概念:**\n\n- **进程**\n\n```tex\n1.进程是指运行中的程序,比如我们使用qq,就会启动一个进程,操作系统就\n  会为改进程分配内存空间,当我们使用迅雷的,又启动了一个进程,操作系统\n  将为迅雷分配空间。\n2.进程是程序的执行过程,或是正在运行的一个程序。是一个动态过程:有它自身\n  的残生、存在和消亡的过程。\n```\n\n- **线程**\n\n```tex\n1.线程是进程创建的,是进程的一个实体。\n2.一个进程可以右多个进程。\n3.线程也可以载创建线程。\n举例:当我们使用迅雷下载一个文件的时候,进程就会创建一个线程来下载,\n    如果又有一个文件要用迅雷下载,那么迅雷就会在创建一个线程下载。\n4.单线程:同一个时刻,只允许执行一个线程。\n5.多线程:同一个时刻,可以执行多个线程,比如qq可以同时打开多个聊天窗口.\n6.并发:同一时刻,多个任务交给一个CPU来交替执行,造成一种\"貌似同时\"的错觉,\n       简单来说,单核Cpu实现多任务就是并发。\n7.并行:同一时刻,多个任务由多个Cpu来执行,称为并行。\n```\n\n**线程的基本使用**\n\n- **创建线程的两种基本方式**\n  **1.继承Thread类,重写run方法**\n\n```java\npublic class ThreadWays {\n    public static void main(String[] args) {\n        Thread01 thread01 = new Thread01();\n        Thread01 thread02 = new Thread01();\n        thread01.start();\n        thread02.start();\n    }\n}\n\nclass Thread01 extends Thread{\n    @Override\n    public void run() {\n        for (int i = 0; i < 10; i++) {\n            try {\n                Thread.sleep(1000);\n            } catch (InterruptedException e) {\n                e.printStackTrace();\n            }\n            System.out.println(\"线程\" +Thread.currentThread() + \"正在打印i = \" + i);\n            if (i == 8){\n                break;\n            }\n        }\n    }\n}\n```\n\n**2.实现Runnable接口重写方法**\n\n```java\npublic class ThreadWays {\n    public static void main(String[] args) {\n        Thread01 thread01 = new Thread01();\n        Thread01 thread02 = new Thread01();\n        // 但是这里不能使用start来启动线程\n        // 通过给Thread对象,传入一个 线程在调用start方法\n        new Thread(thread01).start();\n        new Thread(thread02).start();\n    }\n}\n\nclass Thread01 implements Runnable{\n    @Override\n    public void run() {\n        for (int i = 0; i < 10; i++) {\n            try {\n                Thread.sleep(1000);\n            } catch (InterruptedException e) {\n                e.printStackTrace();\n            }\n            System.out.println(\"线程\" +Thread.currentThread() + \"正在打印i = \" + i);\n            if (i == 8){\n                break;\n            }\n        }\n    }\n}\n```\n\n**关于为什么线程调用start方法,而不是调用run方法:**\n\n```tex\n1.调用start方法实际上底层是调用了start0() private native void start0();\n  这个方法是原生方法,是由JVM机调用的,可以理解为,由JVM机来调用我们定义的\n  run方法。\n2.如果直接调用run方法,将不是创建线程来执行,会等run函数执行完,在执行其他代码。\n```\n\n**线程终止:**\n\n```tex\n1.当线程完成任务后,会自动退出。\n2.还可以通过变量来控制run方法退出的方式俩停止线程,即通知方式。\n```\n\n**线程的常用方法:**\n\n```tex\n1.setName:设置线程名称\n2.getName:返回线程的名称。\n3.start:使该线程开始执行,Java虚拟机底层调用该线程的start0()方法。\n4.run:调用线程对象的run 方法。\n5.setPriority:设置线程的优先级。\n6.getPriority:获取线程的优先级。\n7.sleep:在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)。\n8.interrupt:中断线程,不是退出线程。\n9.yield:线程礼让,让出cpu,让其他线程执行,让的时间不确定,所以礼让不一定成功,\n        当cpu空间充足的时候,一般都是礼让失败.\n10.join:线程插队,插队的线程一旦插队成功,则肯定先执行完插入线程的任务,再回来\n        当前线程的任务。\n```\n\n演示代码:\n\n```java\npublic class ThreadMethod {\n    public static void main(String[] args) {\n        Thread02 thread02 = new Thread02();\n        thread02.setName(\"线程1\");\n        // Thread.MIN_PRIORITY  = 1\n        // Thread.NORM_PRIORITY = 5\n        // Thread.MAX_PRIORITY  = 10\n        thread02.setPriority(Thread.MIN_PRIORITY);\n        thread02.start();\n    }\n}\n\nclass Thread02 extends Thread{\n    @Override\n    public void run() {\n        while (true){\n            System.out.println(this.getName()); // 线程1\n            System.out.println(this.getPriority()); // 1\n            try {\n                Thread.sleep(1000); // 休眠一秒钟\n            } catch (InterruptedException e) {\n                e.printStackTrace();\n            }\n            try {\n                this.interrupt();// 中断当前线程,会进入下一个循环\n            } catch (Exception e) {\n                e.printStackTrace();\n            }\n        }\n    }\n}\n\n```\n\n**join插队演示**\n\n```java\npublic class YieldAndJoin {\n    public static void main(String[] args) {\n        Brother brother = new Brother();\n        Thread thread = new Thread(brother);\n        for (int i = 0; i < 10; i++) {\n            System.out.println(Thread.currentThread() + \"正在运行\" + i);\n            try {\n                Thread.sleep(1000);\n            } catch (InterruptedException e) {\n                e.printStackTrace();\n            }\n            if(i == 5){ // 当i=5的时候,让brother线程插队\n                try {\n                    thread.start(); // 启动线程\n                    thread.join(); // 采用join插队,一定成功,要等Brother线程执行完,才会回到主线程\n                } catch (InterruptedException e) {\n                    e.printStackTrace();\n                }\n            }\n        }\n    }\n}\n\nclass Brother implements Runnable{\n    @Override\n    public void run() {\n        for (int i = 0; i < 10; i++) {\n            System.out.println(Thread.currentThread() + \"正在运行\" + i);\n            try {\n                Thread.sleep(1000);\n            } catch (InterruptedException e) {\n                e.printStackTrace();\n            }\n        }\n    }\n}\n```\n\n**方法使用细节:**\n\n```tex\n1.start底层会创建新的线程,调用run,run就是一个简单的方法调用,不会启动新的线程。\n2.interupt 终端线程,但并没有真正的结束线程,所以一般用于中断正在休眠的线程。\n3.sleep线程的静态方法,使当前线程休眠。\n```\n\n**用户线程和线程守护**\n\n```tex\n1.用户线程,也叫工作线程,当线程任务执行完或通知方式结束。\n2.守护线程:一般为工作线程服务的,当所有用于线程结束,守护线程自动结束。\n3.常见的守护线程:垃圾回收机制。\n```\n\n```java\npublic class Daemon_ {\n    public static void main(String[] args) {\n        DaemonThread daemonThread = new DaemonThread();\n        daemonThread.setDaemon(true); // 将daemonThread设置为守护线程\n        daemonThread.start();\n        for (int i = 0; i < 10; i++) {\n            System.out.println(\"主线程正在运行\");\n            try {\n                Thread.sleep(1000);\n            } catch (InterruptedException e) {\n                e.printStackTrace();\n            }\n        }\n        System.out.println(\"主线程退出啦\"); // 当输出主线程退出啦,守护线程就会立马退出。\n    }\n}\n\nclass DaemonThread extends Thread{\n    @Override\n    public void run() {\n        for (int i = 0; i < 20; i++) {\n            System.out.println(\"守护线程正在运行!\");\n            try {\n                Thread.sleep(1000);\n            } catch (InterruptedException e) {\n                e.printStackTrace();\n            }\n        }\n    }\n}\n```',1),

(8,0,'2022-04-14 14:42:21','测试5','测试5',25,1,'测试5',1),

(13,0,'2022-04-15 12:59:38','操作系统-王道老师','操作系统-王道老师',71,1,'## 操作系统-王道老师\n### 第二章04-管程和死锁\n#### 目录:\n[1.管程](#1)\n[2.死锁的概念](#2)\n     [2.1 死锁的概念](#2)\n     [2.2 死锁、饥饿、死循环的区别](#2)\n     [2.3 死锁产生的必要条件](#2)\n     [2.4 什么时候会发生死锁](#2)\n[3.死锁的处理策略](#3)\n     [3.1 预防死锁](#2)\n     [3.2 避免死锁](#2)\n     [3.3 死锁的检测](#2)\n     [3.4 死锁的解除](#2)\n\n<h3 id=\"1\">1.管程<h3>\n\n#### 1.1 为什么要引入管程:\n为了解决信号量机制编程的麻烦,易出错的问题\n\n#### 1.2 管程的组成:\n1.局部于管程的**共享数据结构**说明。\n2.对该数据结构进行操作的**一个过程**。\n3.对局部于管程的共享数据设置初始值的语句。\n4.管程只有一个名字。\n\n#### 1.3 基本特征:\n1.局部于管程的数据只能被局部于管程的过程访问。\n2.一个进程只有通过调用管程内的过程才能进入管程访问共享数据。\n3.每次仅允许一个线程进入管程内执行某个内部过程。\n#### 1.4 管程解决消费者问题:\n图片\n\n<h3 id=\"2\">2.死锁的概念</h3>\n\n#### 2.1 什么是死锁:\n在并发环境下,各进程因竞争资源而造成的**一种互相等待对方手里的资源,导致个进程都堵塞,都无法向前推进**,就是“**死锁**”。发生死锁后若无外力干涉,这些进程都将无法向前推进。\n#### 2.2 死锁、饥饿、死循环的区别\n**死锁:**\n>各进程互相等待对方手里的资源,导致各进程都堵塞,无法向前推进的现象。\n\n**饥饿:**\n>由于长期得不到想要的资源,某进程无法向前推进的现象。\n\n**死循环:**\n>某进程执行过程中一直跳不出某个循环的现象。有时是陈虚谷元逻辑Bug所致,有时是程序员故意设计。\n\n**三者异同点:**\n图片\n#### 2.3 死锁产生的必要条件\n**产生死锁必须同时满足以下四个条件,只要其中一个不成,死锁就不会发生。**\n\n**互斥条件:**\n>只有对必须互斥使用的资源的争抢才会导致死锁(如哲学家的筷子、打印机设备)像内存、扬声器这样可以同时让多个进程使用的资源是不会导致死锁的(因为进程不用阻塞等待这种资源)。\n\n**不剥夺条件:**\n>进程所获得的资源在未使用完之前,不能由其他进程强行夺走,只能主动释放。\n\n**请求和保持条件:**\n>进程己经保持了至少一个资源,但又提出了新的资源请求,而该资源又被其他进程占有,此时请求进程被阻塞,但又对自己己有的资源保持不放。\n\n**循环等待条件:**\n>存在一种进程资源的循环等待链,链中的每一个进程已获得的资源同时被下一个进程所请求。\n\n**注意:** \n>**发生死锁时一定有循环等待,但发生循环等待时未必死锁**(循环等待是死锁的必要不充分条件)\n#### 2.4 什么时候会发生死锁\n1.对**系统资源的竞争**。各进程对**不可剥夺的资源**(如打印机)的竟争可能引起死锁,对可剥夺的资源(CPU)的竞年是不会引起死锁的。\n2.**进程推进顺序非法**。请求和释放资源的顺序不当,也同样会导致死锁。例如,并发执行的进程P1、P2分别申请并占有了资源 R1、R2,之后进程P1又紧接着申请资源R2,而进程P2又申请资源R1,两者会因为申请的资源被对方占有而阻塞,从而发生死锁。\n3.**信号量的使用不当也会造成死锁**。如生产者-消费者问题中,如果实现互斥的P操作在实现同步的p操作之前,就有可能导致死锁。(可以把互斥信号量、同步信号量也看做是一种抽象的系统资源。\n\n总之,对于**不可剥夺资源的不合理分配**,可能导致死锁。\n<h3 id=\"3\">死锁的处理策略</h3>\n\n#### 3.1 预防死锁\n**基本概念:** 破坏死锁产生的四个必要条件中的一个或多个。\n\n**3.1.1 破坏互斥条件:**\n**互斥条件:**只有必须互斥使用的资源争抢才会导致死锁。\n**基本方法:**\n>只有对必须互斥使用的资源的争抢才会导致死锁,如果把只能互斥使用的资源改造为允许共享使用,则系统不会进入死锁状态。比如:SPOOLing技术。操作系统可以采用**SPOOLing技术**把独占设备在逻辑上改造成共享设备。比如,用SPOOLing技术将打印机改造为共享设备。\n\n图片\n\n**缺点:**\n>并不是所有的资源都可以改造成可共享使用的资源。并且为了系统安全,很多地方还必须保护这种互斥性。因此,**很多时候都无法破坏互斥条件**。\n\n**3.1.2 破坏不剥夺条件:** \n**不剥夺资源:**进程所获得资源在未使用之前,不能由其他进程抢夺走,只能主动释放。\n**基本方法:**\n>**方案一:**\n>当某个进程请求新的资源得不到满足时,它必须立即释放保持的所有资源,待以后需要时再重新申请。也就是说,即使某些资源尚未使用完,也需要主动释放,从而破坏了不可剥夺条件。\n>**方案二:**\n>当某个进程需要的资源被其他进程所占有的时候,可以由操作系统协助,将想要的资源强行剥夺。,这种方式一般需要考虑各进程的优先级(比如:剥夺调度方式,就是将处理机资源强行剥夺给优先级更高的进程。\n\n**缺点:**\n>1.实现起来比较复杂\n>2.释放已获得的资源可能造成前一阶段工作的失效。因此这种方法一般只适用于易保存和恢复状态的资源,如CPU。\n\n**3.1.3 破坏请求和保持条件:** \n**请求和保持条件:**进程己经保持了至少一个资源,但又提出了新的资源请求,而该资源又被其他进\n程占有,此时请求进程被阻塞,但又对自己己有的资源保持不放。\n**基本方法:**\n>可以采用静态分配方法,即进程在运行前一次申请完它所需要的全部资源,在它的资源未满足前,不让它投入运行。一旦投入运行后,这些资源就一直归它所有,该进程就不会再请求别的任何资源了。\n\n图片\n\n**缺点:**\n>有些资源可能只需要用很短的时间,因此如果进程的整个运行期间都一直保持着所有资源,就会造成严重的资源浪费,资源利用率极低。另外,该策略也有可能导致某些进程饥饿。\n\n**3.1.3破环循环等待条件:**\n**循环等待条件:** 存在一种进程资源的循环等待链,链中的每一个进程已获得的资源同时被下一个进程所请求。可采用顺序资源分配法。首先给系统中的资源编号,规定每个进程必须按编号递增的顺序请求资源,同类资源(即编号相同的资源)一次申请完。\n**基本方法:**\n>一 个进程只有己占有小编号的资源时,才有资格申请更大编号的资源。按此规则,已特有大编号资源的进程不可能逆向地回来申请小编号的资源,从而就不会产生循环等待的现象。\n\n图片\n**缺点:**\n>1.不方便增加新的设备,因为可能需要重新分配所有的编号。\n>2.进程实际使用资源的顺序可能和编号递增顺序不一致,会导致资源浪费。\n>3.必须按规定次序申请资源,用户编程麻烦。\n\n#### 3.2 避免死锁\n**基本概念:** 用某种方法防止系统进入不安全状态,从而避免死锁(银行家算法)\n\n**3.2.1 安全序列、不安全序列、死锁的联系:**\n>1.所谓安全序列,就是指如果系统按照这种序列分配资源,则每个进程都能顺利完成。只要能找出一个安全序列,系统就是安全状态。当然,安全序列可能有多个。\n>2.如果分配了资源之后,系统中找不出任何一个安全序列,系统就进入了不安全状态。这就意味着之后可能所有进程都无法顺利的执行下去。当然,如果有进程提前归还了一些资源,那系统也有可能重新回到安全状态,不过我们在分配资源之前总是要考虑到最坏的情况。\n>3.如果系统处于安全状态,就一定不会发生死锁。如果系统进入不安全状态,就可能发生死锁(处于不安全状态未必就是发生了死锁,但发生死锁时一定是在不安全状态)\n>4.如果系统处于**安全状态**,就**一定不会**发生**死锁**。如果系统进入**不安全状态**,就**可能**发生**死锁**(处于不安全状态未必就是发生了死锁,但发生死锁时一定是在不安全状态)因此可以在**资源分配之前预先判断这次分配足否会导致系统进入不安全状态**,以此决定是否答应资源分配请求。这也是**“银行家算法”**的核心思想。\n\n\n图片\n**3.2.2 银行家算法:**\n\n**找得到安全序列:**\n图片\n**找不到安全序列:**\n图片\n**知识回顾:**\n图片\n#### 3.3 死锁的检测\n**3.3.1 基本概念:** 允许死锁的发生,不过操作系统会负责检测除死锁的发生,然后采取某种措施解除死锁。\n\n**3.3.2 数据结构:** 资源分配图\n>1.两种结点:\n	进程结点:对应一个进程(下面的小圆圈)。\n	资源结点:对应一类资源,一类资源结点可能有多个(对应下面的长方形)。\n>2.两条边:\n	进程结点-->资源结点:表示进程申请了几个资源。\n	资源结点-->进程结点:表示已经为进程分配了几个资源(每条边代表一个)\n\n**3.3.3 死锁检测算法**\n>1.在资源分配图中,找出既不阻塞又不是孤点的进程pi(即找出一条有向边与它相连,且该有向边对应资源的申请数量小于等于系统中已有空闲资源数量。如下图中,R1没有空闲资源,R2有一个空闲资源。若所有的连接该进程的边均满足上述条件,则这个进程能继续运行直至完成,然后释放它所占有的所有资源)消去它所有的请求边和分配边,使之成为孤立的结点,在下图中。p1是满足这一条件的进程结点,于是将p1的所有边消去\n>2.进程pi 所释放的资源,可以唤醒某些因等待这些资源而阻塞的进程,原来的阻塞进程可能变为非阻塞进程。在下图中,p2就满足这样的条件。根据1)中的方法进行一系列简化后,若能消去途中所有的边,则称该图是可完全简化的。\n\n图片前\n\n图片后\n\n\n#### 3.4 死锁的解除\n注:并不是系统中所有的进程都是死锁状态,用死锁检测算法化简资源分配图后,还连着边的**那些进程就是死锁进程**。\n**3.4.1 资源剥夺法:**\n>挂起(暂时放到外存上)某些死锁进程,并抢占它的资源,将这些资源分配给其他的死锁进程。但是应防止被挂起的进程长时间得不到资源而饥饿。\n>\n**3.4.2 撤销进程法(或称为终止进程法):**\n>强制撤销部分、甚至全部死锁进程,并剥夺这些进程的资源。这种方式的优点是实现简单,但所付出的代价可能会很大。因为有些进程可能已经运行了很长时间,已经接近结束了,一旦被终止可谓功亏一篑,以后还得从头再来。\n\n**3.4.3 进程回退法:**\n>让一个或多个死锁进程回退到足以避免死锁的地步。这就要求系统要记录进程的历史信息,设置还原点。\n\n**第二章小结,欢迎大家交流学习!**',1);

/*Table structure for table `m_category` */

DROP TABLE IF EXISTS `m_category`;

CREATE TABLE `m_category` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `avatar` varchar(255) DEFAULT NULL,
  `categoryname` varchar(255) DEFAULT NULL,
  `description` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;

/*Data for the table `m_category` */

insert  into `m_category`(`id`,`avatar`,`categoryname`,`description`) values 

(1,'iconfont icon-java','JAVA','vue相关文章'),

(2,'iconfont icon-Vue','VUE','Java相关文章'),

(3,'iconfont icon-shujuku','SQL','SQL相关文章');

/*Table structure for table `m_comment` */

DROP TABLE IF EXISTS `m_comment`;

CREATE TABLE `m_comment` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `content` varchar(255) DEFAULT NULL,
  `create_date` datetime DEFAULT NULL,
  `article_id` int(11) DEFAULT NULL,
  `author_id` bigint(20) DEFAULT NULL,
  `parent_id` int(11) DEFAULT NULL,
  `to_uid` bigint(20) DEFAULT NULL,
  `level` varchar(1) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `FKecq0fuo9k0lnmea6r01vfhiok` (`article_id`),
  KEY `FKkvuyh6ih7dt1rfqhwsjomsa6i` (`author_id`),
  KEY `FKaecafrcorkhyyp1luffinsfqs` (`parent_id`),
  KEY `FK73dgr23lbs3ebex5qvqyku308` (`to_uid`),
  CONSTRAINT `FK73dgr23lbs3ebex5qvqyku308` FOREIGN KEY (`to_uid`) REFERENCES `m_user` (`id`),
  CONSTRAINT `FKaecafrcorkhyyp1luffinsfqs` FOREIGN KEY (`parent_id`) REFERENCES `m_comment` (`id`),
  CONSTRAINT `FKecq0fuo9k0lnmea6r01vfhiok` FOREIGN KEY (`article_id`) REFERENCES `m_article` (`id`),
  CONSTRAINT `FKkvuyh6ih7dt1rfqhwsjomsa6i` FOREIGN KEY (`author_id`) REFERENCES `m_user` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=53 DEFAULT CHARSET=utf8;

/*Data for the table `m_comment` */

/*Table structure for table `m_user` */

DROP TABLE IF EXISTS `m_user`;

CREATE TABLE `m_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `account` varchar(64) DEFAULT NULL,
  `avatar` varchar(255) DEFAULT NULL,
  `create_date` datetime DEFAULT NULL,
  `email` varchar(128) DEFAULT NULL,
  `last_login` datetime DEFAULT NULL,
  `username` varchar(255) DEFAULT NULL,
  `password` varchar(64) DEFAULT NULL,
  `status` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `UK_awpog86ljqwb89aqa1c5gvdrd` (`account`),
  UNIQUE KEY `UK_ahtq5ew3v0kt1n7hf1sgp7p8l` (`email`)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8;

/*Data for the table `m_user` */

insert  into `m_user`(`id`,`account`,`avatar`,`create_date`,`email`,`last_login`,`username`,`password`,`status`) values 

(1,'anyi','http://www.ease.center/images/avatar.jpg','2022-04-14 11:59:26','1961741003@qq.com','2022-04-01 11:59:42','安逸','e10adc3949ba59abbe56e057f20f883e','1');

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
3. Mybatis-plus 生成实体类、Service、Mapper、Mappper.xml

CodeGenerator

package com.anyi;

import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

// 演示例子,执行 main 方法控制台输入模块表名回车自动生成对应项目目录中
public class CodeGenerator {

    /**
     * <p>
     * 读取控制台内容
     * </p>
     */
    public static String scanner(String tip) {
        Scanner scanner = new Scanner(System.in);
        StringBuilder help = new StringBuilder();
        help.append("请输入" + tip + ":");
        System.out.println(help.toString());
        if (scanner.hasNext()) {
            String ipt = scanner.next();
            if (StringUtils.isNotEmpty(ipt)) {
                return ipt;
            }
        }
        throw new MybatisPlusException("请输入正确的" + tip + "!");
    }

    public static void main(String[] args) {
        // 代码生成器
        AutoGenerator mpg = new AutoGenerator();

        // 全局配置
        GlobalConfig gc = new GlobalConfig();
        String projectPath = System.getProperty("user.dir");
        gc.setOutputDir(projectPath + "/src/main/java");
//        gc.setOutputDir("D:\\test");
        gc.setAuthor("anyi");
        gc.setOpen(false);
        // gc.setSwagger2(true); 实体属性 Swagger2 注解
        gc.setServiceName("%sService");
        mpg.setGlobalConfig(gc);

        // 数据源配置
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl("jdbc:mysql://localhost:3306/blog?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=UTC");
        // dsc.setSchemaName("public");
        dsc.setDriverName("com.mysql.cj.jdbc.Driver");
        dsc.setUsername("root");
        dsc.setPassword("root");
        mpg.setDataSource(dsc);

        // 包配置
        PackageConfig pc = new PackageConfig();
        pc.setModuleName(null);
        pc.setParent("com.anyi");
        mpg.setPackageInfo(pc);

        // 自定义配置
        InjectionConfig cfg = new InjectionConfig() {
            @Override
            public void initMap() {
                // to do nothing
            }
        };

        // 如果模板引擎是 freemarker
        String templatePath = "/templates/mapper.xml.ftl";
        // 如果模板引擎是 velocity
        // String templatePath = "/templates/mapper.xml.vm";

        // 自定义输出配置
        List<FileOutConfig> focList = new ArrayList<>();
        // 自定义配置会被优先输出
        focList.add(new FileOutConfig(templatePath) {
            @Override
            public String outputFile(TableInfo tableInfo) {
                // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
                return projectPath + "/src/main/resources/mapper/"
                        + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
            }
        });

        cfg.setFileOutConfigList(focList);
        mpg.setCfg(cfg);

        // 配置模板
        TemplateConfig templateConfig = new TemplateConfig();

        templateConfig.setXml(null);
        mpg.setTemplate(templateConfig);

        // 策略配置
        StrategyConfig strategy = new StrategyConfig();
        strategy.setNaming(NamingStrategy.underline_to_camel);
        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
        strategy.setEntityLombokModel(true);
        strategy.setRestControllerStyle(true);
        strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
        strategy.setControllerMappingHyphenStyle(true);
        strategy.setTablePrefix("m_");
        mpg.setStrategy(strategy);
        mpg.setTemplateEngine(new FreemarkerTemplateEngine());
        mpg.execute();
    }
4. 配置请求跨域

CorsConfig

package com.anyi.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author 安逸i
 * @version 1.0
 */
@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                .allowCredentials(true)
                .maxAge(3600)
                .allowedHeaders("*");
    }
}
5.定制统一返回模板

Result

package com.anyi.common.lang;

import lombok.Data;

import java.io.Serializable;
import java.net.UnknownServiceException;

/**
 * @author 安逸i
 * @version 1.0
 */
@Data
public class Result implements Serializable {
    private String code;
    private String msg;
    private Object data;

    public static Result success(String code ,String msg ,Object data){
        Result result = new Result();
        result.setCode(code);
        result.setMsg(msg);
        result.setData(data);
        return result;
    }
    public static Result success(Object data){
        Result result = new Result();
        result.setCode("200");
        result.setMsg("操作成功");
        result.setData(data);
        return result;
    }

    public static Result success(String msg,Object data){
        Result result = new Result();
        result.setCode("200");
        result.setMsg(msg);
        result.setData(data);
        return result;
    }
    public static Result fail(String code,String msg, Object data){
        Result result = new Result();
        result.setCode(code);
        result.setMsg(msg);
        result.setData(data);
        return result;
    }
    public static Result fail(String msg){
        Result result = new Result();
        result.setCode("-1");
        result.setMsg(msg);
        result.setData(null);
        return result;
    }
}
  1. 定制全局错误拦截

GlobalExceptionHandler

package com.anyi.exception;

import com.anyi.common.lang.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.ShiroException;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * @author 安逸i
 * @version 1.0
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(value = ShiroException.class)
    public Result handler(ShiroException e){
        log.error("未授权异常:----------{}",e);
        return Result.fail(e.getMessage());
    }

    /**
     * 处理运行时异常
     * @param e
     * @return
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = RuntimeException.class)
    public Result handler(RuntimeException e){
        log.error("运行时异常:----------{}",e);
        return Result.fail(e.getMessage());
    }

    /**
     * 处理不合法参数异常
     * @param e
     * @return
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = IllegalArgumentException.class)
    public Result handler(IllegalArgumentException e){
        log.error("断言异常:----------{}",e);
        return Result.fail(e.getMessage());
    }


    /**
     * 处理实体校验 使用 spring-boot-start-validation
     * @param e
     * @return
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public Result handler(MethodArgumentNotValidException e){
        BindingResult bindingResult = e.getBindingResult();
        ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get();
        log.error("参数验证时异常:----------{}",e);
        return Result.fail(objectError.getDefaultMessage());
    }
}