go语言后端开发学习(四) —— 在go项目中使用Zap日志库

一.前言

在之前的文章中我们已经介绍过如何使用logrus包来作为我们在gin框架中使用的日志中间件,而今天我们要介绍的就是我们如何在go项目中如何集成Zap来作为日志中间件

二.Zap的安装与快速使用

和安装其他第三方包没什么区别,我们下载Zap包只需要执行以下命令

go get -u go.uber.org/zap

在Zap的矿方说明中,给出了两种类型的日志记录器——LoggerSugared Logger,在不同场景下我们可以选择使用不同的日志记录器:

Logger:性能比较好,但是仅支持强类型输出的日志,适合在每一微秒和每一次内存分配都很重要的上下文中,使用Logger
Sugared Logger:它支持结构化和printf风格的日志记录。适合在性能很好但不是很关键的上下文中,使用SugaredLogger

接下来我将用两个简单的demo来展示一下我们如何使用这两种日志记录器:

//Logger
package mainimport ("go.uber.org/zap""net/http"
)var logger *zap.Loggerfunc main() {InitLogger()defer logger.Sync() //等到全部日志写入,将缓冲区中的日志写入磁盘SimpleHttpGet("www.baidu.com") //这行代码会报错,仅做展示错误日志信息的打印SimpleHttpGet("https://www.kugou.com")
}func InitLogger() {logger, _ = zap.NewProduction()
}func SimpleHttpGet(url string) {re, err := http.Get(url)if err != nil {logger.Error("Error fetching url", zap.String("url", url), zap.Error(err))} else {logger.Info("Success fetching url", zap.String("statusCode", re.Status), zap.String("url", url))}re.Body.Close()
}//SugarLogger
package mainimport ("go.uber.org/zap""net/http"
)var sugarlogger *zap.SugaredLoggerfunc main() {InitLogger()defer sugarlogger.Sync() //等到全部日志写入,将缓冲区中的日志写入磁盘SimpleHttpGet("www.baidu.com")SimpleHttpGet("https://www.kugou.com")
}func InitLogger() {logger, _ := zap.NewProduction() sugarlogger = logger.Sugar()
}func SimpleHttpGet(url string) {re, err := http.Get(url)if err != nil {sugarlogger.Error("Error fetching url", zap.String("url", url), zap.Error(err))} else {sugarlogger.Info("Success fetching url", zap.String("statusCode", re.Status), zap.String("url", url))}re.Body.Close()
}

三.Zap的配置

Zap的使用其实是比较简单的,但是如何去配置出一个适合我们自己项目的日志中间件其实也是比较困难,下面博主将一步步的实现的一个简单的日志中间件示例,下面开始吧!

1.让日志输入到文件中

在我们日常开发模式时,我们一般会将日志的错误信息打印在控制台上,这样可以方便我们去调试错误,但是在生产模式下,让错误信息打印在控制台上无疑是不大可能得了,我们一般会选择将日志信息录入到文件中,接下来让我们来尝试一下修改日志信息的输入路径。

为了修改日志信息的输出路径,我们这里就不会在通过NewProduction来自动创建logger对象了,而是我们自己通过New这一函数来手动传递配置了,在开始之前我们来看一下New这一函数:

func New(core zapcore.Core, options ...Option)

而这里我们所要配置的就是corezapcore.Core需要三个配置:

  • Encoder:它决定了我们以何种形式写入日志,比如我们可以使用Json格式来作为我们书写日志的格式
  • WriteSyncer:它决定了我们要将日志写到什么地方去
  • LogLevel:它决定了哪些级别的日志会被写入到日志文件中去

接下来我们尝试将日志打印在test.log中,并且用Jsontext两种格式来打印到文件中:

package mainimport ("go.uber.org/zap""go.uber.org/zap/zapcore""net/http""os"
)var sugarlogger *zap.SugaredLoggerfunc main() {InitLogger()defer sugarlogger.Sync() //等到全部日志写入,将缓冲区中的日志写入磁盘SimpleHttpGet("www.baidu.com")SimpleHttpGet("https://www.kugou.com")
}func InitLogger() {encoder := InitEncoder()level := InitLevel()writer := InitWriter()core := zapcore.NewCore(encoder, writer, level)sugarlogger = zap.New(core).Sugar()
}func InitEncoder() zapcore.Encoder {return zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig())
}func InitWriter() zapcore.WriteSyncer {file, _ := os.Create("G:\\bluebell\\src\\demo\\log\\test.log")return zapcore.AddSync(file)
}func InitLevel() zapcore.Level {return zapcore.ErrorLevel
}
func SimpleHttpGet(url string) {re, err := http.Get(url)if err != nil {sugarlogger.Error("Error fetching url", zap.String("url", url), zap.Error(err))} else {sugarlogger.Info("Success fetching url", zap.String("statusCode", re.Status), zap.String("url", url))}re.Body.Close()
}

运行结果如下:
在这里插入图片描述
我们可以看到文件已经输入到json.log中了。

当然我们也可以使用正常的text格式

package mainimport ("go.uber.org/zap""go.uber.org/zap/zapcore""net/http""os"
)var sugarlogger *zap.SugaredLoggerfunc main() {InitLogger()defer sugarlogger.Sync() //等到全部日志写入,将缓冲区中的日志写入磁盘SimpleHttpGet("www.baidu.com")SimpleHttpGet("https://www.kugou.com")
}func InitLogger() {encoder := InitEncoder()level := InitLevel()writer := InitWriter()core := zapcore.NewCore(encoder, writer, level)sugarlogger = zap.New(core).Sugar()
}func InitEncoder() zapcore.Encoder {return zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig())
}func InitWriter() zapcore.WriteSyncer {file, _ := os.Create("G:\\bluebell\\src\\demo\\log\\text.log")return zapcore.AddSync(file)
}func InitLevel() zapcore.Level {return zapcore.ErrorLevel
}
func SimpleHttpGet(url string) {re, err := http.Get(url)if err != nil {sugarlogger.Error("Error fetching url", zap.String("url", url), zap.Error(err))} else {sugarlogger.Info("Success fetching url", zap.String("statusCode", re.Status), zap.String("url", url))}re.Body.Close()
}

运行结果:
在这里插入图片描述

2.修改时间编码,将调用函数信息记录在日志中

在上面日志输出中我们可以看到两个比较大的问题:

  • 时间是以非人类可读的方式展示,像1.7233697314262748e+09这样
  • 日志没有调用方的信息我们很难确定错误的位置

所以我们现在要做的就是以下修改:

  • 修改时间编码器
  • 让日志文件中存在调用者信息

首先是修改时间编码器:

func InitEncoder() zapcore.Encoder {encoderConfig := zap.NewProductionEncoderConfig()encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoderencoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder //在日志文件中使用大写字母记录日志级别return zapcore.NewConsoleEncoder(encoderConfig)
}

运行结果如下:

2024-08-11T18:14:02.328+0800	INFO	Success fetching url{statusCode 15 0 403 Forbidden <nil>} {url 15 0 https://www.kugou.com <nil>}

最后我们添加将调用函数信息记录到日志中的功能,这里我们需要修改一下代码:

func InitLogger() {encoder := InitEncoder()level := InitLevel()writer := InitWriter()core := zapcore.NewCore(encoder, writer, level)sugarlogger = zap.New(core,zap.AddCaller()).Sugar()
}

这样我们就能看到调用信息了:

2024-08-11T19:00:52.831+0800	INFO	main/main.go:47	Success fetching url{statusCode 15 0 403 Forbidden <nil>} {url 15 0 https://www.kugou.com <nil>}

拓展:AddCallerSkip
在日志记录过程中,我们通常希望在日志消息中包含准确的调用信息(即,记录日志的代码行)。例如,如果你在 main.go 文件的第 10 行调用了 logger.Info(“Message”),你希望日志记录显示调用发生在 main.go:10。
但是,如果你将日志记录封装到另一个函数中,例如:

func logInfo(msg string) {logger.Info(msg)
}

这样返回的值就不是准确的日志记录问题了,因为日志记录的调用栈就会增加一层,因为实际上 logger.Info 是由 logInfo 函数调用的。如果不调整调用栈深度,日志中可能会显示 logInfo 函数的调用位置,而不是实际的日志记录位置。

AddCallerSkip 函数用于调整日志记录库中记录调用信息的调用栈深度。它可以让你指定跳过多少层调用栈,从而准确获取实际的日志记录位置。

所以我们最后的InitLogger函数是这样的:

func InitLogger() {encoder := InitEncoder()level := InitLevel()writer := InitWriter()core := zapcore.NewCore(encoder, writer, level)sugarlogger = zap.New(core, zap.AddCaller(),zap.AddCallerSkip(1)).Sugar()
}

3.如何将日志输出到多个位置或将特定级别日志输入到单独文件

  • 将日志输出到多个位置
func InitWriter() zapcore.WriteSyncer {file, _ := os.Create("G:\\bluebell\\src\\demo\\log\\text.log")os := io.MultiWriter(os.Stdout, file)  //既输入到控制台也输入到日志文件中return zapcore.AddSync(os)
}
  • 将特定级别日志输入到单独文件(以error为例)
func InitLogger() {encoder := InitEncoder()level := InitLevel()writer := InitWriter()c1 := zapcore.NewCore(encoder, writer, level) //记录全部日志errF, _ := os.Create("G:\\bluebell\\src\\demo\\log\\err.log")c2 := zapcore.NewCore(encoder, zapcore.AddSync(errF), zap.ErrorLevel) //记录错误日志core := zapcore.NewTee(c1, c2)                                        // tee将日志输出到多个目的地sugarlogger = zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1)).Sugar()
}

以上完整代码如下:

package mainimport ("go.uber.org/zap""go.uber.org/zap/zapcore""io""net/http""os"
)var sugarlogger *zap.SugaredLoggerfunc main() {InitLogger()defer sugarlogger.Sync() //等到全部日志写入,将缓冲区中的日志写入磁盘SimpleHttpGet("https://www.kugou.com")SimpleHttpGet("www.baidu.com")
}func InitLogger() {encoder := InitEncoder()level := InitLevel()writer := InitWriter()c1 := zapcore.NewCore(encoder, writer, level) //记录全部日志errF, _ := os.Create("G:\\bluebell\\src\\demo\\log\\err.log")c2 := zapcore.NewCore(encoder, zapcore.AddSync(errF), zap.ErrorLevel) //记录错误日志core := zapcore.NewTee(c1, c2)                                        // tee将日志输出到多个目的地sugarlogger = zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1)).Sugar()
}func InitEncoder() zapcore.Encoder {encoderConfig := zap.NewProductionEncoderConfig()encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoderencoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder //在日志文件中使用大写字母记录日志级别return zapcore.NewConsoleEncoder(encoderConfig)
}func InitWriter() zapcore.WriteSyncer {file, _ := os.Create("G:\\bluebell\\src\\demo\\log\\text.log")os := io.MultiWriter(os.Stdout, file)return zapcore.AddSync(os)
}func InitLevel() zapcore.Level {return zapcore.DebugLevel
}
func SimpleHttpGet(url string) {re, err := http.Get(url)if err != nil {sugarlogger.Error("Error fetching url", zap.String("url", url), zap.Error(err))} else {sugarlogger.Info("Success fetching url", zap.String("statusCode", re.Status), zap.String("url", url))}re.Body.Close()
}

四.Zap实现日志分割

日志切割可以使用Lumberjack这一第三方包,可以按照下面这个命令下载:

go get gopkg.in/natefinch/lumberjack.v2

最后我们来开一下怎么加入支持:

func InitWriter() zapcore.WriteSyncer {lumberjackLogger := &lumberjack.Logger{Filename:   "G:\\bluebell\\src\\demo\\log\\app.log", //日志文件路径MaxSize:    1,                                       //每个日志文件保存的最大尺寸 单位:MBMaxBackups: 5,                                       //最多保存多少个日志文件MaxAge:     30,                                      //日志文件最多保存多少天Compress:   false,                                   //是否压缩}return zapcore.AddSync(lumberjackLogger)
}

这样就实现了一个简单的日志分割了。

五.在gin框架中集成Zap日志库

在很早之前博主就写过gin.Default会调用Logger(), Recovery()这两个中间件,,所我们想在gin框架中集成Zap日志库只需要重写一下这两个中间件就可以了:

func GinLogger() gin.HandlerFunc {return func(c *gin.Context) {start := time.Now()path := c.Request.URL.Pathquery := c.Request.URL.RawQueryc.Next()cost := time.Since(start)logger.Info(path,zap.Int("status", c.Writer.Status()),zap.String("method", c.Request.Method),zap.String("path", path),zap.String("query", query),zap.String("ip", c.ClientIP()),zap.String("user-agent", c.Request.UserAgent()),zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),zap.Duration("cost", cost),)}
}// GinRecovery recover掉项目可能出现的panic
func GinRecovery(stack bool) gin.HandlerFunc {return func(c *gin.Context) {defer func() {if err := recover(); err != nil {// Check for a broken connection, as it is not really a// condition that warrants a panic stack trace.var brokenPipe boolif ne, ok := err.(*net.OpError); ok {if se, ok := ne.Err.(*os.SyscallError); ok {if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {brokenPipe = true}}}httpRequest, _ := httputil.DumpRequest(c.Request, false)if brokenPipe {logger.Error(c.Request.URL.Path,zap.Any("error", err),zap.String("request", string(httpRequest)),)// If the connection is dead, we can't write a status to it.c.Error(err.(error)) // nolint: err checkc.Abort()return}if stack {logger.Error("[Recovery from panic]",zap.Any("error", err),zap.String("request", string(httpRequest)),zap.String("stack", string(debug.Stack())),)} else {logger.Error("[Recovery from panic]",zap.Any("error", err),zap.String("request", string(httpRequest)),)}c.AbortWithStatus(http.StatusInternalServerError)}}()c.Next()}
}

最后得到的就是我们的最终log文件代码:

package mainimport ("github.com/gin-gonic/gin""go.uber.org/zap""go.uber.org/zap/zapcore""gopkg.in/natefinch/lumberjack.v2""net""net/http""net/http/httputil""os""runtime/debug""strings""time"
)var logger *zap.Loggerfunc main() {r := gin.New()r.Use(GinLogger(), GinRecovery(true))
}func GinLogger() gin.HandlerFunc {return func(c *gin.Context) {start := time.Now()path := c.Request.URL.Pathquery := c.Request.URL.RawQueryc.Next()cost := time.Since(start)logger.Info(path,zap.Int("status", c.Writer.Status()),zap.String("method", c.Request.Method),zap.String("path", path),zap.String("query", query),zap.String("ip", c.ClientIP()),zap.String("user-agent", c.Request.UserAgent()),zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),zap.Duration("cost", cost),)}
}// GinRecovery recover掉项目可能出现的panic
func GinRecovery(stack bool) gin.HandlerFunc {return func(c *gin.Context) {defer func() {if err := recover(); err != nil {// Check for a broken connection, as it is not really a// condition that warrants a panic stack trace.var brokenPipe boolif ne, ok := err.(*net.OpError); ok {if se, ok := ne.Err.(*os.SyscallError); ok {if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {brokenPipe = true}}}httpRequest, _ := httputil.DumpRequest(c.Request, false)if brokenPipe {logger.Error(c.Request.URL.Path,zap.Any("error", err),zap.String("request", string(httpRequest)),)// If the connection is dead, we can't write a status to it.c.Error(err.(error)) // nolint: err checkc.Abort()return}if stack {logger.Error("[Recovery from panic]",zap.Any("error", err),zap.String("request", string(httpRequest)),zap.String("stack", string(debug.Stack())),)} else {logger.Error("[Recovery from panic]",zap.Any("error", err),zap.String("request", string(httpRequest)),)}c.AbortWithStatus(http.StatusInternalServerError)}}()c.Next()}
}func InitLogger() {encoder := InitEncoder()level := InitLevel()writer := InitWriter()c1 := zapcore.NewCore(encoder, writer, level) //记录全部日志errF, _ := os.Create("G:\\bluebell\\src\\demo\\log\\err.log")c2 := zapcore.NewCore(encoder, zapcore.AddSync(errF), zap.ErrorLevel) //记录错误日志core := zapcore.NewTee(c1, c2)                                        // tee将日志输出到多个目的地logger = zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1))
}func InitEncoder() zapcore.Encoder {encoderConfig := zap.NewProductionEncoderConfig()encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoderencoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder //在日志文件中使用大写字母记录日志级别return zapcore.NewConsoleEncoder(encoderConfig)
}func InitWriter() zapcore.WriteSyncer {lumberjackLogger := &lumberjack.Logger{Filename:   "G:\\bluebell\\src\\demo\\log\\app.log", //日志文件路径MaxSize:    1,                                       //每个日志文件保存的最大尺寸 单位:MBMaxBackups: 5,                                       //最多保存多少个日志文件MaxAge:     30,                                      //日志文件最多保存多少天Compress:   false,                                   //是否压缩}return zapcore.AddSync(lumberjackLogger)
}func InitLevel() zapcore.Level {return zapcore.DebugLevel
}

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

pod详解 list-watch机制 预选优选策略 如何指定节点调度pod

K8S是通过 list-watch 机制实现每个组件的协同工作 controller-manager、scheduler、kubelet 通过 list-watch 机制监听 apiserver 发出的事件&#xff0c;apiserver 也会监听 etcd 发出的事件 scheduler的调度策略&#xff1a; 预选策略&#xff08;Predicates&#xff09;…

Pytorch_cuda版本的在线安装命令

pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 运行效果如下&#xff1a; 这个方法是直接从pytorch官网进行在线下载和安装。 cu121&#xff0c;表示当前您安装的cuda版本是12.1

【Redis】持久化—RDB和AOF机制

目录 什么是持久化&#xff1f; RDB&#xff08;定期备份&#xff09; 触发机制 bgsave命令的运作流程 RDB文件的处理 RDB文件 RDB的优缺点 AOF&#xff08;实时备份&#xff09; AOF工作流程 AOF 缓冲区同步⽂件策略 AOF重写机制 AOF重写流程 Redis根据持久化文件…

2015款到18款奔驰GLC升级为2021款的HU6主机后,实现了触摸屏人机交互和Carplay功能

奔驰GLC是北京奔驰生产的一款中型SUV。有车主将2015款奔驰GLC升级为2021款的HU6主机后&#xff0c;实现了触摸屏人机交互和Carplay功能。该车主分享了使用体验&#xff1a; • Carplay功能&#xff1a;可以直接在车机大屏幕上显示导航、音乐和电话信息&#xff0c;让用户在开车…

联想SR650更换风扇后提示传感器异常“传感器Phy Presence Set已从正常状态转换至非紧急状态”

服务器型号&#xff1a;联想ThinkSystem SR650 故障现象&#xff1a;一台联想 ThinkSystem SR650服务器告警&#xff0c;面板和平台同时报风扇故障 告警信息如下图&#xff1a;&#xff08;面板和平台同时告警&#xff09; 接入bmc后查看发现是6号风扇告警 硬件这里已经无法识…

会C++了,想开始接触C#怎么办?|.Net 架构|从C++到C#的入门教学

前言 高质量博客汇总https://blog.csdn.net/yu_cblog/category_12379430.html成熟常用的开发工具和框架https://blog.csdn.net/yu_cblog/category_12737979.htmlDocker从认识到实践再到底层原理https://blog.csdn.net/yu_cblog/category_12424689.html操作系统和计算机网络从入…

[C++] 深入理解面向对象编程特性 : 继承

文章目录 继承的概念与定义继承的定义定义格式不同继承方式与继承的基类中访问限定符间的影响C中的继承和访问控制总结父类的private成员在子类中的访问限制protected成员的使用场景成员访问方式总结继承方式的默认值实际应用中的继承方式 示例代码 OOP中类之间的关系“is a” …

PLSQL导入导出ORACLE数据提示失败问题修改PLSQL配置

oracle中plsql导入提示无法导入问题 1.首先看下是否环境变量已经配置(具体配置看下面环境变量配置) 2.plsql数据导入中tools-->Preferences中配置如下框中的内容 3.设置 tnsnames.ora文件中看下是否设置有问题 4.PLSQL乱码问题 NLS_LANG SIMPLIFIED CHINESE_CHINA.ZHS16…

性能测试工具之JMeter

JMeter Apache JMeter应用程序是开源软件,是一个100%纯Java应用程序,旨在负载测试功能行为和衡量性能。它最初是为测试Web应用程序而设计的,但后来扩展到其他测试功能。 JMeter是一个免费、开源、跨平台的性能测试工具,于20世纪90年代后期面世。这是一个成熟、健全且具有…

湖南(市场调查)源点咨询 构建多元样本是如何改善调研结果的?

湖南&#xff08;市场调研&#xff09;源点咨询认为&#xff0c;大多数市场研究从业者更倾向于从单一数据源获取调研样本&#xff0c;然而在很多情况下&#xff0c;仅从单个数据源很难获得真正具有代表性的样本。 使用多元样本源有两大好处&#xff1a; ①能够捕获不愿加入样…

2024110读书笔记|《飞花令·月》——长安一片月,万户捣衣声,独出前门望野田,月明荞麦花如雪

2024110读书笔记|《飞花令月》——长安一片月&#xff0c;万户捣衣声&#xff0c;独出前门望野田&#xff0c;月明荞麦花如雪 《飞花令月》素心落雪 编著&#xff0c;飞花令得名于唐代诗人韩翃《寒食》中的名句“春城无处不飞花”&#xff0c;类似于行酒令&#xff0c;是文人们…

Java数组篇[5]:数组的排序和查找

哈喽&#xff0c;各位小伙伴们&#xff0c;你们好呀&#xff0c;我是喵手。运营社区&#xff1a;C站/掘金/腾讯云/阿里云/华为云/51CTO&#xff1b;欢迎大家常来逛逛 今天我要给大家分享一些自己日常学习到的一些知识点&#xff0c;并以文字的形式跟大家一起交流&#xff0c;互…

1960-2020中国1km分辨率年均气温数据

数据简介 中国1km分辨率年均气温数据是在中国大陆2400多个站点的气温年统计结果的基础上&#xff0c;融合了NOAA Gsod中包括港澳台在内亚洲地区1300个站点的数据&#xff0c;使用Ansuplin插值软件生成的1960-2020年0.01&#xff08;约1km&#xff09;的网格数据。 Ansuplin基…

shell外壳与Linux权限

&#x1f308;个人主页&#xff1a;Yui_ &#x1f308;Linux专栏&#xff1a;Linux &#x1f308;C语言笔记专栏&#xff1a;C语言笔记 &#x1f308;数据结构专栏&#xff1a;数据结构 文章目录 1.shell命令以及运行原理2. Linux权限的概念3.Linux权限管理3.1 文件访问者的分类…

vue3前端开发-小兔鲜项目-添加购物车操作第一步

首先&#xff0c;呢&#xff0c;告诉大家一个坏消息&#xff0c;官方媒体的案例代码已经被他们删除了。如图所示。 也就是说&#xff0c;大家已经看不到官方的代码文件了。 那么既然如此&#xff0c;我们自己写的这个博客记录日志&#xff0c;就显得尤为重要了。继续今天的内容…

SuccBI+低代码文档中心 — 可视化分析(仪表板)(下)

制作仪表板 引入数据模型 仪表板所需模型已经在数据模块中准备好&#xff0c;可以将对应模型表添加到数据模型中。提供了两种添加方式&#xff1a; 在数据栏中点击添加按钮&#xff0c;在弹出框中通过搜索或直接在其所在目录下选中该模型&#xff0c;点击确定。 点击数据按钮…

一篇讲清楚什么是密码加密和加盐算法 | 附Java代码实现

目录 前言&#xff1a; 一、密码加密 1. MD5介绍 2.彩虹表攻击 3.测试复杂密码是否能被攻破 二、加盐算法 1.对密码123456演示加盐算法 2.盐值的储存 3.密码加盐思想总结 三、Java代码实现 前言&#xff1a; 早些年&#xff0c;数据泄露屡见不鲜&#xff0c;每个班上总…

【Web前端】vue3整合eslint约束代码格式

一、整合eslint 整合eslint的两种方式&#xff1a; 在已有项目中整合eslint&#xff1a;# 安装eslint及其vue插件即可 npm i -D eslint eslint-plugin-vue创建项目时整合eslint&#xff1a; 提示 是否引入ESLint用于代码质量检测 时选择 是# 创建vue3项目 npx create-vue # 下…

.NET8使用VS2022打包Docker镜像

NET8使用VS2022打包Docker镜像 1. 项目中添加Docker支持文件2. 自定义镜像名称3. 发布Docker镜像3.1 安装Docker3.2 控制台切换到项目根目录,执行以下命令发布镜像 3.3 修改镜像名称4. 保存镜像到本地 1. 项目中添加Docker支持文件 2. 自定义镜像名称 项目文件PropertyGroup节…

vue3中 ref 和 reactive 的区别

相同&#xff1a;均是声明响应式对象。且声明的响应式对象是深层的 1. 数据类型不同&#xff1a;ref用于包装JavaScript基本类型的数据&#xff08;如字符串、数字、布尔值等&#xff09;&#xff0c;而reactive可以用于包装JavaScript对象和数组等复杂类型的数据。 2.访问方式…