Node.js | 基于 MongoDB 的简易用户管理系统

news2025/2/23 13:17:06

在这里插入图片描述


🖥️ NodeJS专栏:Node.js从入门到精通
🖥️ 博主的前端之路(源创征文一等奖作品):前端之行,任重道远(来自大三学长的万字自述)
🖥️ TypeScript知识总结:TypeScript 学习笔记(十万字超详细知识点总结)
🧑‍💼 个人简介:大三学生,一个不甘平庸的平凡人🍬
👉 你的一键三连是我更新的最大动力❤️!


📑目录

  • 🔽 前言
  • 1️⃣ 项目最终效果
  • 2️⃣ 搭建项目
  • 3️⃣ 连接MongoDB
    • 🔹 配置MongoDB
    • 🔹 创建用户模型
  • 4️⃣ 创建API接口
    • 🔹 接口规范
    • 🔹 添加用户信息
    • 🔹 删除用户信息
    • 🔹 修改用户信息
    • 🔹 查询用户信息
  • 5️⃣ 搭建前端页面
    • 🔹 页面结构
    • 🔹 业务代码
  • 🔼 结语


🔽 前言

上一节我们介绍了MongoDBMongoose,这一节为了巩固Nodejs操作MongoDB数据库实现增删改查的功能,本文将带领大家制作一个简易的用户管理系统项目,让我们开始吧!

1️⃣ 项目最终效果

在带领大家搭建项目之前,先向大家展示一下项目最终实现的效果:

在这里插入图片描述

因为写这个项目的目的是巩固Nodejs操作数据库,重点在于功能的实现,所以我们并不会过多的去写CSS来美化页面。

这个简易的用户管理系统包含以下功能:

  • 添加用户信息
  • 分页查询用户信息(不查询密码字段)
  • 修改用户信息
  • 删除用户信息

2️⃣ 搭建项目

我们将使用express 应用程序生成器来搭建项目骨架,控制台输入:

express 简易用户管理系统 --view=ejs

上面的命令将创建一个使用ejs模板的名为简易用户管理系统express项目。

express 应用程序生成器的介绍可以查看我的这篇文章:Node.js | 深入讲解 express 应用程序生成器

使用VS Code打开项目,找到package.json,修改下图所示的地方:

在这里插入图片描述

这里修改的目的是:在启动项目时使用nodemon指令来启动,这样当我们项目代码更改时它能够自动重新运行(前提是你安装了nodemon指令,没有安装的可以控制台输入npm i nodemon -g全局安装nodemon)。

之后安装mongoose,在项目根目录下打开终端执行下面这行代码进行安装:

npm i mongoose

再执行以下代码运行项目:

npm run start

浏览器打开http://localhost:3000/,出现以下页面表示运行成功:

在这里插入图片描述

3️⃣ 连接MongoDB

🔹 配置MongoDB

项目根目录下创建config文件夹(此文件夹用来存放配置文件),并在此文件夹内创建db.config.js文件(DB数据的配置文件):

// db.config.js
const mongoose = require("mongoose");

// 连接数据库
// 前缀mongodb:是固定的,后面是你的mongodb的运行端口
// user_test代表数据库名称
mongoose.connect("mongodb://127.0.0.1:27017/user_test");
// 当你插入集合和数据时,数据库user_test会自动创建

user_test是我定义的数据库的名称,大家根据需要可以自行更改。

之后在app.js文件中引入我们创建的这个db.config.js文件:

// app.js中添加以下代码
// 引入数据库模块
require("./config/db.config");

🔹 创建用户模型

项目根目录下创建model文件夹(此文件夹用来存放模型),并在此文件夹内创建UserModel.js文件(用户模型):

// UserModel.js
const mongoose = require("mongoose");

// 字段类型
const UserType = {
    username: String,
    password: String,
    age: Number,
};

// 创建一个模型(user),对应数据库中的集合(表)(users)
const UserModel = mongoose.model("user", new mongoose.Schema(UserType));
// 注意:创建的mongodb的集合名称是加s的
// mongoose.model第二个参数可以通过mongoose.Schema生成的实例来限制集合字段类型
// 因为mongodb过于自由,对类型没有限制,我们在开发中往往需要使用mongoose.Schema来手动限制数据库各种字段类型

// 导出模型
module.exports = UserModel;

根据这个用户模型,Mongoose能够自动帮我们在数据库中创建一个users集合,集合内的每条数据包含的字段有:usernamepasswordage、以及自动生成的_id

之后运行MongoDB数据库,再启动项目,MongoDB的运行窗口出现下图所示就代码我们连接成功了:

在这里插入图片描述

4️⃣ 创建API接口

app.js中的usersRouter的路由前缀修改为/api

// app.use("/users", usersRouter); 
app.use("/api", usersRouter); // 使用/api前缀

routes目录下的users.js中引入我们上面创建的用户模型:

// users.js
// 引入用户模型
const UserModel = require("../model/UserModel");

再将users.js中的下述代码删除:

/* GET users listing. */
router.get("/", function (req, res, next) {
    res.send("respond with a resource");
});

🔹 接口规范

我们常使用RES Tful架构来规范接口的定义,RES Tful特点包括:

  1. 每一个URI代表1种资源;
  2. 客户端使用GETPOSTPUTDELETE4个表示操作方式的动词对服务端资源进行操作:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源;
  3. 通过操作资源的表现形式来操作资源;
  4. 资源的表现形式是XML或者HTML
  5. 客户端与服务端之间的交互在请求之间是无状态的,从客户端到服务端的每个请求都必须包含理解请求所必需的信息。

使用方式:

请求方式请求地址效果
GEThttp://localhost:3000/api/user获取用户列表(所有用户信息)
GEThttp://localhost:3000/api/user/{id}获取指定id的用户信息
POSThttp://localhost:3000/api/user添加用户信息
PUThttp://localhost:3000/api/user/{id}修改指定id的用户信息
DELETEhttp://localhost:3000/api/user/{id}删除指定id的用户信息

过滤信息:

过滤信息常用于GET请求,通过指定一些规则来获取对应的信息。

通用字段说明例子
limit返回记录的数量http://localhost:3000/api/user/?limit=10 获取10条数据
offset返回记录的开始位置http://localhost:3000/api/user/?offset=10 获取数据库中第10条之后的数据
page指定第几页常配合per_page使用
per_page每页的记录数http://localhost:3000/api/user/?page=2&per_page=10 按照每页10条数来算,获取第二页数据
sortby指定返回结果按照哪个属性排序常配合order使用
order指定排序顺序http://localhost:3000/api/user/?sortby=name&order=asc 获取按照name字段升序排序的数据
state指定筛选条件http://localhost:3000/api/user/?state=close 获取state为close的数据

多个过滤字段可以相互结合使用(通过&分割),并以URL参数的形式出现在请求地址之后。

🔹 添加用户信息

添加用户信息我们使用post接口,users.js中添加以下代码:

// 添加数据
router.post("/user", function (req, res, next) {
    // 获取请求体中的参数
    const { username, password, age } = req.body;
    // 插入数据库
    UserModel.create({ username, password, age })
        .then((data) => {
            res.send(data);
        })
        .catch((error) => {
            console.log(error);
            res.send({ mag: "添加信息出错!" });
        });
});

使用Api调试工具测试一下接口是否正常运行,这里使用Apifox进行测试:

Apifox的使用教程

先将测试环境的前置URL设置为我们项目运行的地址:

在这里插入图片描述

http://127.0.0.1:3000就是http://localhost:3000/

定义接口运行接口
在这里插入图片描述在这里插入图片描述

结果显示接口运行成功,在MongoDB可视化工具中能够查看到新添加的数据:

在这里插入图片描述

🔹 删除用户信息

删除用户信息我们使用delete接口,users.js中添加以下代码:

// 删除数据
router.delete("/user/:userId", function (req, res, next) {
	// 获取动态路由参数
    const { userId } = req.params;
    // 删除数据
    UserModel.deleteOne({ _id: userId })
        .then((data) => {
            res.send({ msg: "删除成功!", ...data });
        })
        .catch((error) => {
            console.log(error);
            res.send({ msg: "删除失败!" });
        });
});

使用Apifox测试:

定义接口运行接口效果
在这里插入图片描述在这里插入图片描述在这里插入图片描述

🔹 修改用户信息

修改用户信息我们使用put接口,users.js中添加以下代码:

// 修改数据
// 动态路由,获取前端传来的id
router.put("/user/:userId", function (req, res, next) {
	// 获取请求体中的参数
    const { username, password, age } = req.body;
    // 获取动态路由参数
    const { userId } = req.params;
    // 更新数据
    UserModel.updateOne(
        { _id: userId },
        {
            username,
            password,
            age,
        }
    )
        .then((data) => {
            res.send({ msg: "更新成功!", ...data });
        })
        .catch((error) => {
            console.log(error);
            res.send({ msg: "更新失败!" });
        });
});

使用Apifox测试,定义接口:

定义接口运行接口效果
在这里插入图片描述在这里插入图片描述在这里插入图片描述

🔹 查询用户信息

查询用户信息使用get接口,users.js中添加以下代码:

这里的查询先是获取到数据的总数量,然后再根据前端传来的pagelimit字段查询到指定数据,之后将查询的数据和数据总数量一并返回给前端,从而实现分页查询的功能。

// 查询数据:分页查询的接口
router.get("/user", function (req, res, next) {
	// 获取路由参数(过滤信息)
    const { page, per_page} = req.query;
    UserModel.find()
        .count() // count方法,获取数据的总数量
        .then((dataCount) => {
            // find查询,第二个参数数组指定获取的字段,这里是获取username和age以及id(默认具有),不获取password
            UserModel.find({}, ["username", "age"])
                // sort排序,按照age:1(正序)排序,age:-1为倒叙
                .sort({ age: 1 })
                // skip方法,代表跳过几条数据开始获取
                .skip((page - 1) * per_page)
                // limit方法,代表取多少条数据
                .limit(per_page)
                .then((data) => {
                    res.send({ data, dataCount });
                });
        });
});
定义接口运行接口
电脑在这里插入图片描述

至此,整个项目需要用到的Api接口就都定义好了,下面我们开始搭建前端页面。

5️⃣ 搭建前端页面

我们在views目录下的index.ejs文件中创建我们的前端页面:

注意:虽然我们这个项目是在ejs模板中搭建页面,但我们并不需要使用ejs语法,只需使用html语法即可。

🔹 页面结构

<body>
	<h1>NodeJS操作mongodb:简易用户管理系统</h1>
	
	<div>
	  用户名:<input type="text" id="username">
	</div>
	<div>
	  密码:<input type="text" id="password">
	</div>
	<div>
	  年龄:<input type="number" id="age">
	</div>
	<button id="addBtn">增加用户</button>
	
	<p>提示:在输入框中输入信息可选择点击增加用户,也可点击表格中更新按钮来更新指定数据</p>
	
	<hr>
	
	<table border="2">
	  <thead>
	    <tr>
	      <td>id</td>
	      <td>用户名</td>
	      <td>年龄</td>
	      <td>操作</td>
	    </tr>
	  </thead>
	  <tbody>
	  </tbody>
	</table>
	
	<!-- 存放分页按钮 -->
	<div id="pageBtn">
	</div>
	
	<!-- 存放业务代码 -->
	<script></script>
</body>

🔹 业务代码

获取DOM元素:

const username = document.getElementById('username')
const password = document.getElementById('password')
const age = document.getElementById('age')
const pageBtn = document.getElementById('pageBtn')
const addBtn = document.getElementById('addBtn')

定义变量:

// 数据总条数
let dataCount = 0
// 每页显示条目个数
let pageSize = 2
// 当前页数
let pageNum = 1
// 总页数
let pageCount = 1

渲染列表数据:

// 渲染列表数据
function renderTable(data) {
  const tbody = document.querySelector('tbody')
  tbody.innerHTML = data.map(item => `
    <tr>
      <td>${item._id}</td>
      <td>${item.username}</td>
      <td>${item.age}</td>
      <td>
        <button οnclick="deleteFn('${item._id}')">删除</button>
        <button οnclick="updateFn('${item._id}')">更新</button>
      </td>
  
    </tr>
  `).join('')
}

renderTable函数接收的是一个用户数据的数组集合,用来将此数据渲染到tbody中。

渲染分页按钮:

// 渲染分页按钮
function renderPageBtn(page) {
  let str = ''
  for (let i = 1; i <= page; i++) {
    if (i === pageNum) {
      // 添加红色背景
      str += `<button οnclick="getList(${i})" style="background-color: red;">第${i}页</button>`
    } else {
      str += `<button οnclick="getList(${i})">第${i}页</button>`
    }
  }
  pageBtn.innerHTML = str
}

renderPageBtn函数接收的参数表示总页数,有多少页就渲染多少个分页按钮,同时对当前所处的页对应的分页按钮做背景变红的处理,并且每个分页按钮都绑定了一个点击事件。

获取数据:

// 查询数据api
function getList(page) {
  fetch(`/api/user/?page=${page}&per_page=${pageSize}`).then(res => res.json()).then(res => {
    // 设置数据总条数
    dataCount = res.dataCount
    // 设置总页数
    pageCount = Math.ceil(dataCount / pageSize)
    // 设置当前页数
    pageNum = page
    // 渲染列表数据
    renderTable(res.data)
    // 渲染分页按钮
    renderPageBtn(pageCount)
  })
}

getList函数接收的参数表示页数,用来获取指定页的数据,并调用renderTable将数据渲染,同时调用renderPageBtn更新分页按钮。

添加数据:

// 添加数据
addBtn.onclick = () => {
  if (!(username.value && password.value && age.value)) {
    alert('请正确输入输入框信息!')
    return
  }
  fetch('/api/user', {
    method: 'post',
    body: JSON.stringify({
      username: username.value,
      password: password.value,
      age: age.value
    }),
    headers: {
      "Content-Type": "application/json"
    }
  }).then(res => res.json()).then(res => {
    // 添加完成后获取数据库中最新数据
    getList(pageNum);
    alert('添加成功!')
    // 清空表单
    username.value = ''
    password.value = ''
    age.value = ''
  })
}

更新数据:

// 更新数据api
function updateFn(id) {
  if (!(username.value && password.value && age.value)) {
    alert('请正确输入输入框信息!')
    return
  }
  fetch(`/api/user/${id}`, {
    method: 'put',
    body: JSON.stringify({
      username: username.value,
      password: password.value,
      age: age.value
    }),
    headers: {
      "Content-Type": "application/json"
    }
  }).then(res => res.json()).then(res => {
    // 添加完成后获取数据库中最新数据
    getList(pageNum);
    alert('更新成功!')
    // 清空表单
    username.value = ''
    password.value = ''
    age.value = ''
  })
}

删除数据:

// 删除数据api
function deleteFn(id) {
  fetch(`/api/user/${id}`, {
    method: 'delete',
  }).then(res => res.json()).then(res => {
    // 删除完成后获取数据库中最新数据
    // newPageCount删除完数据后最新的总页数
    let newPageCount = dataCount > 1 ? Math.ceil((dataCount - 1) / pageSize) : 1
    if (pageNum > newPageCount) {
      // 如果当前页数大于总页数则获取最后一页数据
      getList(newPageCount)
    } else {
      getList(pageNum)
    }
    alert('删除成功!')
  })
}

至此项目就全部完成啦!项目效果与文章开头展示的一样,项目最终的目录结构如下:

在这里插入图片描述

🔼 结语

经过上面一系列的操作,我们的简易用户管理系统总算是写好了,但这时你会发现我们的这个项目写的好乱,比如routes目录本该存放的是单纯的路由文件,但现在它还包含了操作数据库的代码:

在这里插入图片描述

并且这些操作数据库的代码也不是单纯的操作数据库,还掺杂了处理数据,返回数据的代码(如res.send),这就使整个项目的业务变得特别混乱。

各部分之间相互掺杂,导致耦合度太高,这势必为之后的维护种下了风险的种子。

我们可以使用MVC架构,实现业务分层来解决这些问题,下一篇我们就将深入去学习MVC架构,并使用它来重构这个简易用户管理系统,敬请期待!

博主的Node.js从入门到精通专栏正在持续更新中,关注博主订阅专栏学习Node不迷路!

如果本篇文章对你有所帮助,还请客官一件四连!❤️

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2856.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++秋招经验贴

文章目录一、个人背景及秋招情况1.个人背景2.秋招情况二、求职C强相关开发岗位的准备过程以及一些建议1. 八股2. 力扣刷题3. 实习4. 项目三、总结一、个人背景及秋招情况 1.个人背景 本科&#xff1a;二本&#xff0c;材料专业   硕士&#xff1a;211硕&#xff0c;光学工程…

TI IWR1642毫米波雷达使用串口原始数据采集与分析

本文编辑&#xff1a;调皮哥的小助理 1.引言 如果文章能够给你带来价值&#xff0c;希望能够关注我。 如果文章能够让你学习到知识&#xff0c;希望你能够点个赞&#xff01; 好了下面开始今天的学习内容吧。 今天给大家分享的是 《TI 的IWR1642毫米波雷达使用串口原始数据…

深度学习入门(十五)环境和分布偏移(了解)

深度学习入门&#xff08;十五&#xff09;环境和分布偏移前言环境和分布偏移教材1 分布偏移的类型1.1 协变量偏移1.2 标签偏移1.3 概念偏移2 分布偏移示例2.1医学诊断2.2 自动驾驶汽车2.3 非平稳分布2.4 更多轶事3 分布偏移纠正3.1 经验风险与实际风险3.2 协变量偏移纠正3.3 标…

MATLAB | 一起来感受数学之美叭

前两天去观摩了MATHWORKS官方举办的Mathematics is beautiful数学之美投票比赛&#xff0c;见到了很多非常惊艳的作品&#xff0c;在这里分享给大家让大家一同感受大神们的创造力&#xff0c;接下来由我来做全程解说。 虽然看起来代码都写好了&#xff0c;&#xff0c;&#x…

程序员眼中看到的网页是如何制作出来的?

一、认识网页 在学习之初&#xff0c;我们需要认识一下网页的概念&#xff0c;因为网页与我们的 html是息息相关的。 那么接下来我们来看一下&#xff0c;我们经常去通过浏览器查看的网页&#xff0c;它的本质是什么&#xff1f;在此我们需要去做一个对比。我们眼中看到的网页…

聚类算法概要及相关知识准备

聚类的概念 聚类分析是在数据中发现数据对象之间的关系&#xff0c;将数据进行分组&#xff0c;组内的相似性越大&#xff0c;组间的差别越大&#xff0c;则聚类效果越好。 将物理或抽象对象的集合分成由类似对象组成的多个类或簇&#xff08;cluster&#xff09;的过程被称为…

SpringBoot+Vue的社区疫情防控管理系统|基于Python+Django的社区物资采购系统

&#x1f496;&#x1f496;作者&#xff1a;IT跃迁谷毕设展 &#x1f499;&#x1f499;个人简介&#xff1a;曾长期从事计算机专业培训教学&#xff0c;本人也热爱上课教学&#xff0c;语言擅长Java、微信小程序、Python、Golang、安卓Android等。平常会做一些项目定制化开发…

Cookie使用详解

Cookie使用详解 目录Cookie使用详解理论知识前言创建Cookiecookie 的属性介绍name 、valuedomainpathExpires 、Max-AgeSameSiteSecure&#xff0c;HttpOnlyCookie与跨域、安全知识点小结实践相关配置修改代码实践实验过程记录其它小结理论知识 前言 HTTP Cookie&#xff08;…

使用Charles和iPhone进行微信小程序抓包详解

基于工作原因&#xff0c;需要对一款微信小程序进行测试。本次任务是纯黑盒方式&#xff0c;所以只有通过抓包的方式找到接口及参数列表&#xff0c;再逐一进行功能和性能测试。 一、使用工具 网络抓包工具&#xff1a;Charles 设备&#xff1a;iPhone6s&#xff0c;iPhone1…

数据分析 | Pandas 200道练习题,每日10道题,学完必成大神(8)

文章目录前期准备1. 将收盘价5日均线&#xff0c;20日均线与原始数据绘制在同一个图上2. 按周为采样规则&#xff0c;取一周收盘价的最大值3. 绘重制采样数据与原始数据4. 将数据往后移动5天、5. 将数据向前移动5天6. 使用expending函数计算开盘价的移动窗口的均值7. 绘制上一题…

牛客刷题系列(汽水瓶,跳台阶扩展问题,斐波那契凤尾)

牛客刷题系列一&#xff1a;汽水瓶题目链接常规写法简便写法二.跳台阶扩展问题三&#xff1a;斐波那契凤尾很多小伙伴为了刷题发愁 今天为大家推荐一款刷题神奇哦&#xff1a;刷题面试神器牛客 各大互联网大厂面试真题。从基础到入阶乃至原理刨析类面试题 应有尽有&#xff0c;…

云IDE介绍——CSDN开发云

云IDE产品介绍云IDE使用教程 免费使用地址&#xff1a;点击【云IDE】&#xff0c;即可开始创建工作空间啦~ 作者简介&#xff1a;一名在校云计算网络运维学生、每天分享网络运维的学习经验、和学习笔记。 座右铭&#xff1a;低头赶路&#xff0c;敬事如仪 个人主页&#xf…

【学习笔记之数据结构】时间复杂度与空间复杂度

一、算法效率 算法在编写成可执行程序后&#xff0c;运行时需要耗费时间资源和空间&#xff08;内存&#xff09;资源。因此衡量一个算法的好坏&#xff0c;一般是从时间和空间两个维度来衡量的&#xff0c;即时间复杂度和空间复杂度。   时间复杂度主要衡量一个算法的运行快…

2022年音视频面试题 C/C++/Linux/FFmpeg/webRTC/rtmp/hls/rtsp/ffplay/srs

1&#xff09;OpenGL 是按照什么架构设计的&#xff1f; OpenGL 的渲染架构是 Client/Server 模式&#xff1a;Client&#xff08;客户端&#xff09;指的是我们在 CPU 上运行的一些代码&#xff0c;比如我们会编写 OC/C/Java 代码调用 OpenGL 的一些 API&#xff1b;而 Server…

mybatis学习(1)

使用mybatis也是有一段时间了&#xff0c;但是一直没有系统了解和学习&#xff0c;最近正好有空&#xff0c;索性花点时间熟悉下。 为什么是mybatis&#xff1f; 了解mybatis之前&#xff0c;需要知道什么是"数据库持久层"&#xff0c;我的理解&#xff0c;就是将数…

Decoder与Encoder重要组件

Decoder与Encoder重要组件 大家知道&#xff0c;Netty从底层Java通道读到ByteBuf二进制数据&#xff0c;传入Netty通道的流水线&#xff0c;随后开始入站处理。在入站处理过程中&#xff0c;需要将ByteBuf二进制类型&#xff0c;解码成Java POJO对象。这个解码过程&#xff0c…

【Python基础篇020】网络编程初识

文章目录 &#x1f9a0;一、前言 &#x1f9a0;二、软件开发架构 &#x1f340;2.1、C/S架构 &#x1f340;2.2、B/S架构 &#x1f340;2.3、服务端与客户端 &#x1f9a0;三、ip与端口号 &#x1f340;3.1、IP地址与端口号常识 &#x1f340;3.2、MAC和IP的概念与不同…

实际应用效果不佳?来看看提升深度神经网络泛化能力的核心技术(附代码)

目录 数据增强 &#x1f4cc; 技术介绍 &#x1f4cc; 手动数据处理&增强 &#x1f4cc; 基于 TensorFlow 的数据增强 Dropout 随机失活 &#x1f4cc; 技术介绍 &#x1f4cc; 基于TensorFlow应用Dropout &#x1f4a1; L1 和 L2 正则化 &#x1f4cc; 技术介绍 …

Mybatis日志框架

文章目录一、 用日志打印代替sout1、sout有什么问题①问题1&#xff1a;I/O影响性能②问题2&#xff1a;无法统一管理③问题3&#xff1a;显得你很low2、使用日志框架的好处①设定级别&#xff0c;统一管理②灵活指定输出位置③自定义日志格式④基于日志分析问题二、最佳用法1、…

设计模式之【单例模式】全解,单例模式实现方式,暴力打破单例模式与解决方案,你真的认识单例模式吗?

文章目录什么是单例模式单例模式的应用场景处理有线程冲突的资源表示全局唯一类单例模式的实现方式1、饿汉式之静态常量2、饿汉式之静态代码块3、懒汉式之线程不安全方式&#xff08;不推荐&#xff09;4、懒汉式之加锁方式&#xff08;不推荐&#xff09;5、懒汉式之双重锁检查…