SpringBoot+AOP+自定义注解,优雅实现日志记录

news2024/11/22 4:27:23

文章目录

    • 前言
    • 准备阶段
    • 1、数据库日志表
    • 2、自定义注解编写
    • 3、AOP切面类编写
    • 4、业务层
      • 4.1、Service 层:
      • 4.2 Service 实现层:
    • 5、测试

前言

首先我们看下传统记录日志的方式是什么样的:

@DeleteMapping("/deleteUserById/{userId}")
public JSONResult deleteUserById(@PathVariable("userId") Long userId){
    //调用Service实现类方法做删除操作
    userService.deleteUserById(userId);
    //记录操作日志
    LogUtils.addLog("用户模块", "删除用户操作", "12");
    return JSONResult.success();
}
  1. 日志记录代码与业务代码强耦合,万一哪天需要多记录一个字段到数据库的话,所有调用的地方都需要修改
  2. 许多参数需要花费很大代价才能记录到数据库,比如:请求方法全路径、请求方式(get还是post等)、方法执行耗时、入参、出参、方法执行状态等
  3. 非常不优雅,难维护

接下来给大家分享一种非常优雅的方式记录日志,就是采用自定义注解+AOP切面编程技术,实现日志记录,现在记录日志的方式就是这样了:

@PostMapping("/save")
@MyLog(title = "用户模块", content = "新增用户信息")
public JSONResult save(@RequestBody UserDto dto){
    //业务逻辑代码这里,省略
    return JSONResult.success(dto);
}

可以看到,直接使用自定义注解@MyLog完成日志记录即可,与业务代码没有任何耦合,是不是看着非常优雅呢?

好了,废话不多说,接下来跟着下面的步骤,将这个功能集成到你的项目中的

准备阶段

1、数据库日志表

我们数据库先准备一张记录日志信息的表,建表语句如下:

CREATE TABLE `sys_oper_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '日志主键',
  `title` varchar(50) DEFAULT '' COMMENT '模块标题',
  `content` varchar(100) DEFAULT NULL COMMENT '日志内容',
  `method` varchar(100) DEFAULT '' COMMENT '方法名称',
  `request_method` varchar(10) DEFAULT '' COMMENT '请求方式',
  `oper_name` varchar(50) DEFAULT '' COMMENT '操作人员',
  `request_url` varchar(255) DEFAULT '' COMMENT '请求URL',
  `ip` varchar(128) DEFAULT '' COMMENT '请求IP地址',
  `ip_location` varchar(255) DEFAULT '' COMMENT 'IP归属地',
  `request_param` varchar(2000) DEFAULT '' COMMENT '请求参数',
  `response_result` varchar(2000) DEFAULT '' COMMENT '方法响应参数',
  `status` int(1) DEFAULT NULL COMMENT '操作状态(0正常 1异常)',
  `error_msg` varchar(2000) DEFAULT NULL COMMENT '错误消息',
  `oper_time` datetime DEFAULT NULL COMMENT '操作时间',
  `take_time` bigint(20) DEFAULT NULL COMMENT '方法执行耗时(单位:毫秒)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='操作日志记录';

2、自定义注解编写

好,表已经准备好了,下面是下载到本地到项目:

在这里插入图片描述

这里对项目结构就不多做介绍了,在此基础上,我们新建一个包,用来写自定义注解,代码如下:

package org.js.annotation;import java.lang.annotation.*;/**
 * 自定义注解记录系统操作日志
 */
//Target注解决定 MyLog 注解可以加在哪些成分上,如加在类身上,或者属性身上,或者方法身上等成分
@Target({ ElementType.PARAMETER, ElementType.METHOD })
//Retention注解括号中的"RetentionPolicy.RUNTIME"意思是让 MyLog 这个注解的生命周期一直程序运行时都存在
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog
{
    /**
     * 模块标题
     */
    String title() default "";
    /**
     * 日志内容
     */
    String content() default "";
}

OK,到目前为止,我们就新增了一个自定义注解类,现在项目结构变成这样了:

在这里插入图片描述

3、AOP切面类编写

好,自定义注解写好后,我们开始写AOP切面类,需要先导入AOP相关依赖jar包,所以需要在pom.xml中加入下面依赖

<!-- aop切面 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

然后切面类代码如下:

package org.js.aop;import com.alibaba.fastjson.JSON;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.js.annotation.MyLog;
import org.js.domain.OperLog;
import org.js.service.IOperLogService;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;/**
 * 切面处理类,记录操作日志到数据库
 */
@Aspect
@Component
public class OperLogAspect {@Autowired
    private IOperLogService operLogService;//为了记录方法的执行时间
    ThreadLocal<Long> startTime = new ThreadLocal<>();/**
     * 设置操作日志切入点,这里介绍两种方式:
     * 1、基于注解切入(也就是打了自定义注解的方法才会切入)
     *    @Pointcut("@annotation(org.js.annotation.MyLog)")
     * 2、基于包扫描切入
     *    @Pointcut("execution(public * org.js.controller..*.*(..))")
     */
    @Pointcut("@annotation(org.js.annotation.MyLog)")//在注解的位置切入代码
    //@Pointcut("execution(public * org.js.controller..*.*(..))")//从controller切入
    public void operLogPoinCut() {
    }@Before("operLogPoinCut()")
    public void beforMethod(JoinPoint point){
        startTime.set(System.currentTimeMillis());
    }/**
     * 设置操作异常切入点记录异常日志 扫描所有controller包下操作
     */
    @Pointcut("execution(* org.js.controller..*.*(..))")
    public void operExceptionLogPoinCut() {
    }
​
​
    /**
     * 正常返回通知,拦截用户操作日志,连接点正常执行完成后执行, 如果连接点抛出异常,则不会执行
     *
     * @param joinPoint 切入点
     * @param result      返回结果
     */
    @AfterReturning(value = "operLogPoinCut()", returning = "result")
    public void saveOperLog(JoinPoint joinPoint, Object result) {
        // 获取RequestAttributes
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        // 从获取RequestAttributes中获取HttpServletRequest的信息
        HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);
        try {
            // 从切面织入点处通过反射机制获取织入点处的方法
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            // 获取切入点所在的方法
            Method method = signature.getMethod();
            // 获取操作
            MyLog myLog = method.getAnnotation(MyLog.class);OperLog operlog = new OperLog();
            if (myLog != null) {
                operlog.setTitle(myLog.title());//设置模块名称
                operlog.setContent(myLog.content());//设置日志内容
            }
            // 将入参转换成json
            String params = argsArrayToString(joinPoint.getArgs());
            // 获取请求的类名
            String className = joinPoint.getTarget().getClass().getName();
            // 获取请求的方法名
            String methodName = method.getName();
            methodName = className + "." + methodName + "()";
            operlog.setMethod(methodName); //设置请求方法
            operlog.setRequestMethod(request.getMethod());//设置请求方式
            operlog.setRequestParam(params); // 请求参数
            operlog.setResponseResult(JSON.toJSONString(result)); // 返回结果
            operlog.setOperName("张三"); // 获取用户名(真实环境中,肯定有工具类获取当前登录者的账号或ID的,或者从token中解析而来)
            operlog.setIp(getIp(request)); // IP地址
            operlog.setIpLocation("湖北武汉"); // IP归属地(真是环境中可以调用第三方API根据IP地址,查询归属地)
            operlog.setRequestUrl(request.getRequestURI()); // 请求URI
            operlog.setOperTime(new Date()); // 时间
            operlog.setStatus(0);//操作状态(0正常 1异常)
            Long takeTime = System.currentTimeMillis() - startTime.get();//记录方法执行耗时时间(单位:毫秒)
            operlog.setTakeTime(takeTime);
            //插入数据库
            operLogService.insert(operlog);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }/**
     * 异常返回通知,用于拦截异常日志信息 连接点抛出异常后执行
     */
    @AfterThrowing(pointcut = "operExceptionLogPoinCut()", throwing = "e")
    public void saveExceptionLog(JoinPoint joinPoint, Throwable e) {
        // 获取RequestAttributes
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        // 从获取RequestAttributes中获取HttpServletRequest的信息
        HttpServletRequest request = (HttpServletRequest) requestAttributes.resolveReference(RequestAttributes.REFERENCE_REQUEST);OperLog operlog = new OperLog();
        try {
            // 从切面织入点处通过反射机制获取织入点处的方法
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            // 获取切入点所在的方法
            Method method = signature.getMethod();
            // 获取请求的类名
            String className = joinPoint.getTarget().getClass().getName();
            // 获取请求的方法名
            String methodName = method.getName();
            methodName = className + "." + methodName + "()";
            // 获取操作
            MyLog myLog = method.getAnnotation(MyLog.class);
            if (myLog != null) {
                operlog.setTitle(myLog.title());//设置模块名称
                operlog.setContent(myLog.content());//设置日志内容
            }
            // 将入参转换成json
            String params = argsArrayToString(joinPoint.getArgs());
            operlog.setMethod(methodName); //设置请求方法
            operlog.setRequestMethod(request.getMethod());//设置请求方式
            operlog.setRequestParam(params); // 请求参数
            operlog.setOperName("张三"); // 获取用户名(真实环境中,肯定有工具类获取当前登录者的账号或ID的,或者从token中解析而来)
            operlog.setIp(getIp(request)); // IP地址
            operlog.setIpLocation("湖北武汉"); // IP归属地(真是环境中可以调用第三方API根据IP地址,查询归属地)
            operlog.setRequestUrl(request.getRequestURI()); // 请求URI
            operlog.setOperTime(new Date()); // 时间
            operlog.setStatus(1);//操作状态(0正常 1异常)
            operlog.setErrorMsg(stackTraceToString(e.getClass().getName(), e.getMessage(), e.getStackTrace()));//记录异常信息
            //插入数据库
            operLogService.insert(operlog);
        } catch (Exception e2) {
            e2.printStackTrace();
        }
    }/**
     * 转换异常信息为字符串
     */
    public String stackTraceToString(String exceptionName, String exceptionMessage, StackTraceElement[] elements) {
        StringBuffer strbuff = new StringBuffer();
        for (StackTraceElement stet : elements) {
            strbuff.append(stet + "\n");
        }
        String message = exceptionName + ":" + exceptionMessage + "\n\t" + strbuff.toString();
        message = substring(message,0 ,2000);
        return message;
    }/**
     * 参数拼装
     */
    private String argsArrayToString(Object[] paramsArray)
    {
        String params = "";
        if (paramsArray != null && paramsArray.length > 0)
        {
            for (Object o : paramsArray)
            {
                if (o != null)
                {
                    try
                    {
                        Object jsonObj = JSON.toJSON(o);
                        params += jsonObj.toString() + " ";
                    }
                    catch (Exception e)
                    {
                        e.printStackTrace();
                    }
                }
            }
        }
        return params.trim();
    }//字符串截取
    public static String substring(String str, int start, int end) {
        if (str == null) {
            return null;
        } else {
            if (end < 0) {
                end += str.length();
            }if (start < 0) {
                start += str.length();
            }if (end > str.length()) {
                end = str.length();
            }if (start > end) {
                return "";
            } else {
                if (start < 0) {
                    start = 0;
                }if (end < 0) {
                    end = 0;
                }
                return str.substring(start, end);
            }
        }
    }/**
     * 转换request 请求参数
     * @param paramMap request获取的参数数组
     */
    public Map<String, String> converMap(Map<String, String[]> paramMap) {
        Map<String, String> returnMap = new HashMap<>();
        for (String key : paramMap.keySet()) {
            returnMap.put(key, paramMap.get(key)[0]);
        }
        return returnMap;
    }//根据HttpServletRequest获取访问者的IP地址
    public static String getIp(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
}

代码里面的逻辑我就不赘述了,里面的注释写的非常全,大家应该看得懂,不懂的评论区留言即可

现在项目结构如下:

在这里插入图片描述

4、业务层

4.1、Service 层:

package cn.js.service;

import cn.js.domain.SysOperLog;
import cn.js.query.SysOperLogQuery;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;


/**
 * <p>
 * 操作日志记录 服务类
 * </p>
 *
 * @author js
 * @date 2023-11-02
 */
public interface SysOperLogService extends IService<SysOperLog> {

     IPage<SysOperLog> selectMyPage(SysOperLogQuery query);

     Page<SysOperLog> selectMySqlPage(SysOperLogQuery query);

}

4.2 Service 实现层:

package cn.js.service.impl;

import cn.hutool.core.util.StrUtil;
import cn.js.Mapper.SysOperLogMapper;
import cn.js.domain.SysOperLog;
import cn.js.query.SysOperLogQuery;
import cn.js.service.SysOperLogService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * <p>
 * 操作日志记录 服务实现类
 * </p>
 *
 * @author js
 * @date 2023-11-02
 */
@Transactional
@Service
@Slf4j
public class SysOperLogServiceImpl extends ServiceImpl<SysOperLogMapper, SysOperLog> implements SysOperLogService {

    @Autowired
    private SysOperLogMapper sysOperLogMapper;

    //查询分页列表数据
    public IPage<SysOperLog> selectMyPage(SysOperLogQuery query) {
        QueryWrapper<SysOperLog> wrapper = new QueryWrapper<>();
        if (StrUtil.isNotEmpty(query.getKeyword())) {
            //下面条件根据实际情况修改
            wrapper.and(
                i -> i.like("user_name", query.getKeyword())
                     .or().like("login_name", query.getKeyword())
            );
        }
        //排序(默认根据主键ID降序排序,根据实际情况修改)
        wrapper.orderByDesc("id");
        Page<SysOperLog> page = new Page<>(query.getCurrent(), query.getSize());
        return super.page(page, wrapper);
    }

    //查询分页列表数据(自己写SQL)
    public Page<SysOperLog> selectMySqlPage(SysOperLogQuery query) {
        Page<SysOperLog> page = new Page<>(query.getCurrent(), query.getSize());
        List<SysOperLog> list = sysOperLogMapper.selectMySqlPage(page, query);
        return page.setRecords(list);
    }
}

5、测试

接下来我们就可以测试了,在Controller接口中直接用自定义注解开始记录日志,如下方法使用:

@GetMapping("/deleteUserById/{userId}")
@MyLog(title = "用户模块", content = "删除用户操作")
public JSONResult deleteUserById(@PathVariable("userId") Long userId){
    //这里具体删除用户代码 省略.....
    return JSONResult.success();
}

然后启动项目,浏览器输入地址:http://localhost:8001/deleteUserById/123

显示结果如下:

在这里插入图片描述

说明接口调用成功,看下数据库是否记录了日志:

在这里插入图片描述

数据库已经新增了一条日志记录,而且里面记录到信息非常全

OK,至此,我们以后项目中再记录日志就非常方便了,只需要在方法上面打一个注解就可以了,在AOP里负责往数据库写,方便日后维护

完整代码

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

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

相关文章

【C语言:函数栈帧的创建与销毁】

文章目录 前言一、前期准备1.寄存器2.汇编指令3.测试代码 二、解开函数栈帧的神秘面纱1.栈帧大体轮廓2.main函数栈帧的创建3.main函数内执行有效代码4.烫烫烫5.函数参数的传递6.add函数栈帧的创建7.add函数内执行有效代码8.add是如何获得参数的9. add函数栈帧的销毁10.main函数…

IDEA中如何移除未使用的import

&#x1f468;&#x1f3fb;‍&#x1f4bb; 热爱摄影的程序员 &#x1f468;&#x1f3fb;‍&#x1f3a8; 喜欢编码的设计师 &#x1f9d5;&#x1f3fb; 擅长设计的剪辑师 &#x1f9d1;&#x1f3fb;‍&#x1f3eb; 一位高冷无情的编码爱好者 大家好&#xff0c;我是全栈工…

@Slf4j将日志记录到磁盘和数据库

文章目录 1、背景介绍2、存本地2.1、配置文件2.2、使用 3、存数据库3.1、配置文件改造3.2、过滤器编写3.3、表准备3.4、添加依赖3.5、测试 4、优化4.1、日志定期删除 1、背景介绍 现在我一个SpringBoot项目想记录日志&#xff0c;大概可以分为下面这几种&#xff1a; 用户操作…

速学数据结构 | 链表实现队列究竟有什么优势?

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏:《速学数据结构》 《C语言进阶篇》 ⛺️生活的理想&#xff0c;就是为了理想的生活! &#x1f4cb; 前言 &#x1f308;hello&#xff01; 各位宝子们大家好啊&#xff0c;栈区的实现我们前面已经讲了&#…

0005Java安卓程序设计-ssm基于Android的网店系统

文章目录 **摘要**目录系统设计开发环境 编程技术交流、源码分享、模板分享、网课教程 &#x1f427;裙&#xff1a;776871563 摘要 随着Internet的发展&#xff0c;人们的日常生活已经离不开网络。未来人们的生活与工作将变得越来越数字化&#xff0c;网络化和电子化。网上管…

系统提示缺少或找不到emp.dll文件的详细解决方案

我今天打开一款《游戏》。然而&#xff0c;在游戏中遇到了一个非常棘手的问题&#xff1a;游戏报错找不到emp.dll,无法继续执行代码。这让我们非常苦恼&#xff0c;因为这个问题严重影响了我们的游戏体验。 在经过一番努力之后&#xff0c;我终于找到了4个解决方法&#xff0c…

要讨个公道,要分辨真假

这里写自定义目录标题 欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants 创建一个自定义列表如何创建一个…

Qt利用VCPKG和CMake和OpenCV和Tesseract实现中英文OCR

文章目录 1. 开发平台2. 下载文件2.1 下载安装 OpenCV 库2.2 下载安装 Tesseract-OCR库2.3 下载训练好的语言包 3. CMakeLists.txt 内容4. Main.cpp4.1 中英文混合OCR 5. 在Qt Creator 中设置 CMake vcpkg5.1 在初始化配置文件里修改5.2 在构建配置里修改 说明&#xff1a;在Q…

C语言--判断一个年份是否是闰年(详解)

一.闰年的定义 闰年是指在公历&#xff08;格里高利历&#xff09;中&#xff0c;年份可以被4整除但不能被100整除的年份&#xff0c;或者可以被400整除的年份。简单来说&#xff0c;闰年是一个比平年多出一天的年份&#xff0c;即2月有29天。闰年的目的是校准公历与地球公转周…

Git 的基本操作 ——命令行

Git 的工作流程 详解如下&#xff1a; 本地仓库&#xff1a;是在开发人员自己电脑上的Git仓库,存放我们的代码(.git 隐藏文件夹就是我们的本地仓库) 远程仓库&#xff1a;是在远程服务器上的Git仓库,存放代码(可以是github.com或者gitee.com 上的仓库,或者自己该公司的服务器…

【小白专用】PHP基本语法 23.11.04

PHP基本语法 PHP是超文本预处理器 由服务器解析执行 可以与 html 进行混编(嵌入) ,PHP是一种弱类型语言 1.1 PHP标记 PHP和其他Web语言一样&#xff0c;都是用一对标记将PHP代码包含起来&#xff0c;以便和HTML代码区分开来。PHP支持4种风格的标记&#xff0c;如表所示。 标…

王道p18 6.从有序顺序表中删除所有其值重复的元素,使表中所有元素的值均不同(c语言代码实现)

视频讲解在这里&#xff1a;&#x1f447; 顺序表p18 第6题wd数据结构课后代码题&#xff08;c语言代码实现&#xff09;_哔哩哔哩_bilibili 本题代码如下 void deleterepeat(struct sqlist* L) {if (L->length 0)printf("表空");int i 0;int k 0;for (i 1…

Vue3项目嵌套企业微信扫码登录

企业微信登录流程 企业微信提供了OAuth的授权登录方式&#xff0c;可以让从企业微信终端打开的网页获取成员的身份信息&#xff0c;从而免去登录的环节。 整个流程采用的是OAuth2&#xff0c;流程如下&#xff1a; 前端操作思路 配置一些参数&#xff0c;渲染登录模板也就是…

K8s:部署 CNI 网络组件+k8s 多master集群部署+负载均衡及Dashboard k8s仪表盘图像化展示

目录 1 部署 CNI 网络组件 1.1 部署 flannel 1.2 部署 Calico 1.3 部署 CoreDNS 2 负载均衡部署 3 部署 Dashboard 1 部署 CNI 网络组件 1.1 部署 flannel K8S 中 Pod 网络通信&#xff1a; ●Pod 内容器与容器之间的通信 在同一个 Pod 内的容器&#xff08;Pod 内的容…

https://aip.baidubce.com/oauth/2.0/token报错blocked by CORS policy

还是跟以前一样&#xff0c;我们先看报错点&#xff1a;&#xff08;注意小编这里是H5解决跨域的&#xff0c;不过解决跨域的原理都差不多&#xff09; Access to XMLHttpRequest at https://aip.baidubce.com/oauth/2.0/token from origin http://localhost:8000 has been blo…

[C++进阶篇]STL中vector的使用

一、vector的介绍 1.vector的介绍 vector是表示可变大小数组的序列容器。vector也采用的连续存储空间来存储元素&#xff0c;就是可以采用下标对vector的元素进行访问&#xff0c;和数组一样。它的大小是可以动态改变的。 2.重要的接口组成 二、 vector迭代器的使用 2.1 ve…

[SSD综述1.6] SSD固态硬盘参数图文解析_选购固态硬盘就像买衣服?

依公知及经验整理,原创保护,禁止转载。 专栏 《SSD入门到精通系列》 <<<< 返回总目录 <<<< ​ 传统的 HDD 是“马达+磁头+磁盘”的机械结构,而 SSD 则是“闪存介质+主控”的纯半导体芯片存储结构,两者在数据存储介质和读写方式上有着本质区别,这…

S4.2.4.5 Lane Polarity Inversion

一 本章节主讲知识点 1.1 Polarity Inversion 极性反转 1.2 Lane Reversal 通道翻转 二 本章节原文翻译 2.1 极性反转 原文摘录&#xff1a; PCIe 协议规定&#xff0c;必须支持该特性。该特性的目标也是为了简化 PCB 的布线。每个 lane 都包含一组发送&#xff08;Tx&…

Datawhale-AIGC实践

Datawhale-AIGC实践 部署ChatGLM3-6B平台 clone 项目&#xff0c;配置环境 git clone https://github.com/THUDM/ChatGLM3.git cd ChatGLM3 pip install -r requirement.txt修改web_demo.py, web_demo2.py 设置加载模型的路径修改启动代码: demo.queue().launch(shareFalse…

4.6找出字符串中第一个匹配的下标(还是不太会KMP)

算法&#xff1a;用了KMP算法节省时间、空间复杂度 不过代码还是不太会&#xff0c;只能解读正确代码 正确代码&#xff1a; class Solution:def getNext(self, next, s):j -1next[0] jfor i in range(1, len(s)):while j > 0 and s[i] ! s[j1]:j next[j]if s[i] s[j…