个人博客
看到别人搭建的自己的博客,想想自己的服务器买了很久也没怎么用过,于是就想搭建一个自己的博客,上传一些自己的学习笔记心得,一些笔记方便自己查看,项目是一个前后端分离的项目,后端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
用户模块
请求地址 | 请求方式 | 请求参数 | 操作 |
| post | accout,password | 登录 |
| get | / | 退出 |
| get | / | 获取本人信息 |
博客模块
()
中是默认值
请求地址 | 请求方式 | 请求参数 | 操作 |
| get | pageNo(1),limit(5) | 获取文章 |
| get | id | 根据id获取文章 |
| post | article实体 | 编辑文章根据id获取文章 |
| delete | id | 删除文章根据id删除文章 |
| post | article实体 | 添加文章 |
| get | / | 获取最热门博客 |
| get | id(tag的id) | 根据分类获取文章 |
| get | id | 增加点赞 |
| get | id | 减少点赞 |
| post | file | 上传图片 |
后端框架搭建
步骤:
- Springboot整合Shiro和Jwt实现登录模块
- 导入sql文件生成表
- Mybatis-plus 生成实体类、Service、Mapper、Mappper.xml
- 配置请求跨域
- 定制统一返回模板
- 定制全局错误拦截
- 编写具体业务
主要实现功能
- 登录功能,由于是个人博客,所有没有添加注册功能
- Springboot整合markdown-it,实现markdown渲染,编辑
- 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;
}
}
- 定制全局错误拦截
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());
}
}