SpringBoot核心框架之AOP详解

SpringBoot核心框架之AOP详解

一、AOP基础

1.1 AOP概述
  • AOP:Aspect Oriented Programming(面向切面编程,面向方面编程),其实就是面向特定方法编程。
    • 场景:项目部分功能运行较慢,定位执行耗时较长的业务方法,此时就需要统计每一个业务的执行耗时。
    • 思路:给每个方法在开始前写一个开始计时的逻辑,在方法结束后写一个计时结束的逻辑,然后相减得到运行时间。

思路是没问题的,但是有个问题,一个项目是有很多方法的,如果挨个增加逻辑代码,会相当繁琐,造成代码的臃肿,所以可以使用AOP编程,将计时提出成一个这样的模板:

  1. 获取方法运行开始时间
  2. 运行原始方法
  3. 获取方法运行结束时间,计算执行耗时

原始方法就是我们需要计算时间的方法,并且可以对原始方法进行增强,其实这个技术就是用到了我们在Java基础部分学习的动态代理技术

实现:动态代理是面向切面编程最主流的实现。而SpringAOP是Spring框架的高级技术,旨在管理bean对象的过程中,主要是通过底层的动态代理机制,对特点的方法进行编程。

1.2 AOP快速入门

统计各个业务层方法执行耗时

  1. 导入依赖:在pom.xml中导入AOP的依赖。
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
  1. 编写AOP程序:针对于特定方法根据业务需要进行编程。
@Slf4j // 日志
@Component // 将当前类交给spring管理
@Aspect // 声明这是一个AOP类
public class TimeAspect {

   @Around("execution(* com.example.service.*.*(..))")
   // @Around:表示这是一个环绕通知。
   // "execution(* com.example.service.*.*(..))":切入点表达式,它定义了哪些方法会被这个环绕通知所拦截。这个后面会详细讲解。
   // execution(* ...):表示拦截执行的方法。
   // * com.example.service.*.*(..):表示拦截 com.example.service 包下所有类的所有方法(* 表示任意字符的通配符)。
   // ..:表示方法可以有任意数量和类型的参数。
   public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
       // ProceedingJoinPoint是 Spring AOP 中的一个接口,在使用环绕通知时需要
       // 它继承自 JoinPoint 接口,并添加了 proceed() 方法。
       // 这个方法是 AOP 代理链执行的关键部分,它允许你在切面中执行自定义逻辑后继续执行原始方法。

       // 1. 记录开始时间
       long start = System.currentTimeMillis();

       // 2. 调用原始方法
       Object result = joinPoint.proceed(); // 执行被通知的方法。如果不调用 proceed(),被通知的方法将不会执行。

       // 3. 记录结束时间,计算耗时
       long end = System.currentTimeMillis();

       // getSignature():返回当前连接点的签名。
       log.info(joinPoint.getSignature()+"方法执行耗时:{}ms",end - start);

       return result;
   }
}
  1. 查看结果在这里插入图片描述

这样我们就完成了,一个AOP的小例子,但是AOP的功能远不能这些,他还有更多的实用的功能。比如:记录操作日志:可以记录谁什么时间操作了什么方法,传了什么参数,返回值是什么都可以很方便的实现。还有比如权限控制,事务管理等等。

我们来总结一下AOP的优势

  1. 代码无侵入
  2. 减少重复代码
  3. 提高开发效率
  4. 维护方便
1.3. AOP核心概念

连接点:JoinPoint,可以被连接点控制的方法(暗含方法执行时的信息)。 在此例中就是需要被计算耗时的业务方法。
通知:Advice,指那些重复的逻辑,也就是共性功能(最终体现为一个方法)。在此例中就是计算耗时的逻辑代码。
切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用。在此例中就是com.example.service 包下所有类的所有方法。
切面:Aspect,描述通知与切入点的对应关系(通知+切入点)。在此例中就是TimeAspect方法。
目标对象:Target,通知所应用的对象。在此例中就是通知com.example.service 包下所有类的所有方法。

1.4. AOP的执行流程

因为SpringAOP是基于动态代理实现的,所有在方法运行时就会先为目标对象基于动态代理生成一个代理对象,为什么说AOP可以增强方法,就是因为有一个代理方法,然后在AOP执行时,Spring就会将通知添加到代理对象的方法前面,也就是记录开始时间的那个逻辑代码,然后调用原始方法,也就是需要计时的那个方法,此时代理对象已经把原始方法添加到代理对象里面了,然后执行调用原始方法下面的代码,在此例中就是计算耗时的那部分,AOP会把这部分代码添加到代理对象的执行方法的下面,这样代理对象就完成了对目标方法的增强,也就是添加了计时功能,最后在程序运行时自动注入的也就不是原来的对象,而是代理对象了,不过这些都是AOP自动完成,我们只需要编写AOP代码即可。

二、AOP进阶

2.1. AOP支持的通知类型

通知类型:

  1. 环绕通知(Around Advice) 重点!!!:
  • 使用 @Around 注解来定义。
  • 包围目标方法的执行,可以在方法执行前后执行自定义逻辑,并且可以控制目标方法的执行。
  • 通过 ProceedingJoinPoint 参数的 proceed() 方法来决定是否执行目标方法。
  1. 前置通知(Before Advice)
  • 使用 @Before 注解来定义。
  • 在目标方法执行之前执行,无论方法是否抛出异常,都会执行。
  • 不能阻止目标方法的执行。
  1. 后置通知(After Advice) 也叫最终通知
  • 使用 @After 注解来定义。
  • 在目标方法执行之后执行,无论方法是否抛出异常,都会执行。
  • 通常用于资源清理工作
  • 返回通知(After Returning Advice) 了解
  • 使用 @AfterReturning 注解来定义。
  • 在目标方法成功执行之后执行,即没有抛出异常时执行。
  • 可以获取方法的返回值。
  1. 异常通知(After Advice)了解
  • 使用 @AfterThrowing 注解来定义。
  • 在目标方法抛出异常后执行。
  • 可以获取抛出的异常对象。

注意事项:

  1. 环绕通知需要自己调用joinPoint.proceed()来让原始方法执行,其他通知则不需要。
  2. 环绕通知的返回值必须是Object,来接受原始方法的返回值。
@Slf4j
@Component
@Aspect
public class MyAspect {

    // 因为示例中的切入点都是一样的,所以不用写多次切入表达式,创建一个方法即可。
    // 此方法也可在其他AOP需要切入点的地方使用。
    @Pointcut("execution(* com.example.service.impl.DeptServiceImpl.*(..))")
    public void pt(){}

    // 前置通知
    @Before("pt()")
    public void Before(){
        log.info("before ...");
    }

    // 环绕通知
    @Around("pt()")
    public Object Around(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("around after ...");
        // 调用原始方法
        Object proceed = joinPoint.proceed();
        log.info("around after ...");
        return proceed;
    }

    // 后置通知
    @After("pt()")
    public void After(){
        log.info("after ...");
    }

    // 返回通知
    @AfterReturning("pt()")
    public void Returning(){
        log.info("returning ...");
    }

    // 异常通知
    @AfterThrowing("pt()")
    public void Throwing(){
        log.info("throwing ...");
    }
}
2.2. 多个通知之间的执行顺序

当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会执行。那么顺序是怎么的呢?
我们先创建三个AOP程序,分别给他们创建一个前置通知和后置通知,然后启动程序观察他们的输出情况。

// MyAspect2
@Slf4j
@Component
@Aspect
public class MyAspect2 {

    @Before("execution(* com.example.service.impl.DeptServiceImpl.*(..))")
    public void befor(){
        log.info("befor2 ...");
    }

    @After("execution(* com.example.service.impl.DeptServiceImpl.*(..))")
    public void after(){
        log.info("after2 ...");
    }
}
// MyAspect3
@Slf4j
@Component
@Aspect
public class MyAspect3 {

    @Before("execution(* com.example.service.impl.DeptServiceImpl.*(..))")
    public void befor(){
        log.info("befor3 ...");
    }

    @After("execution(* com.example.service.impl.DeptServiceImpl.*(..))")
    public void after(){
        log.info("after3 ...");
    }
}
// MyAspect4
@Slf4j
@Component
@Aspect
public class MyAspect4 {

    @Before("execution(* com.example.service.impl.DeptServiceImpl.*(..))")
    public void befor(){
        log.info("befor4 ...");
    }

    @After("execution(* com.example.service.impl.DeptServiceImpl.*(..))")
    public void after(){
        log.info("after4 ...");
    }
}

// 输出结果
 com.example.aop.MyAspect2                : befor2 ...
 com.example.aop.MyAspect3                : befor3 ...
 com.example.aop.MyAspect4                : befor4 ...

 com.example.aop.MyAspect4                : after4 ...
 com.example.aop.MyAspect3                : after3 ...
 com.example.aop.MyAspect2                : after2 ...

// 然后我们把MyAspect2改成MyAspect5,但输出内容不变,我们来看一下输出结果
 com.example.aop.MyAspect3                : befor3 ...
 com.example.aop.MyAspect4                : befor4 ...
 com.example.aop.MyAspect5                : befor2 ...

 com.example.aop.MyAspect5                : after2 ...
 com.example.aop.MyAspect4                : after4 ...
 com.example.aop.MyAspect3                : after3 ...
2.2.1 默认情况:

执行顺序是和类名有关系的,对于目标方法前的通知字母越靠前的越先执行,目标方法后的通知则相反,字母越靠前的越晚执行,这和Filter拦截器的规则是一样的。

2.2.2 也可以使用注解的方式指定顺序。使用@Order(数字)加在切面类上来控制顺序。

目标方法前的通知:数字小的先执行。
目标方法后的通知:数字小的后执行。

@Slf4j
@Component
@Aspect

@Order(10)

public class MyAspect3 {
   ...
}
2.3. 切入点表达式

切入点表达式:描述切入点方法的一种表达式。
作用:主要决定项目中哪些方法需要加入通知。
常见形式:

  • execution(…):根据方法的签名来匹配。
  • @annotation:根据注解匹配。
2.3.1 execution(…)

execution主要是通过方法的返回值,类名,包名,方法名,方法参数等信息来匹配,语法为:
execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常)

其中带 ? 的表示可以省略的部分

  • 访问修饰符:可省略(比如:public private …)
  • 包名.类名:可省略 但不推荐
  • throws 异常:可省略 (注意是方法上声明可抛出的异常,不是实际抛出的异常)
// 完整的写法:
@Before("execution(public void com.example.service.impl.DeptServiceImpl.add(java.lang.Integer))")
public void befor(){
   ...
}

可以使用通配符描述切入点

  • 单个独立的任意符号,可以通配任意返回值,包括包名,类名,方法名,任意一个参数,也可以通配包,类,方法名的一部分。
@After("execution(* com.*.service.*.add*(*))")
  • 多个连续的任意符号,可以通配任意层级的包,或任意类型,任意个数的参数。
@After("execution(* com.example..DeptService.*(..))")
  • 根据业务的需要,也可以使用 且(&&),或(||),非(!)来组合切入点表达式。
@After("execution(* com.example..DeptService.*(..)) || execution(* com.example.service.DeptService.*(..))")
2.3.2 @annotation:用于匹配标识有特定注解的方法

语法:@annotation(注解的全类名)

先新建一个注解:

@Retention(RetentionPolicy.RUNTIME)  // 用来描述有效时间,RUNTIMW:在运行时有效
@Target(ElementType.METHOD) // 用来说明这个注解可以运行在哪里, METHOD:方法上
public @interface MyLog {
}

在目标方法上添加注解

@MyLog
@Override
public void delete(Integer id) {
    deptMapper.delect(id); // 根据id删除部门
}
@MyLog
@Override
public void add(Dept dept) {
    dept.setCreateTime(LocalDateTime.now());
    dept.setUpdateTime(LocalDateTime.now());
    deptMapper.add(dept);
}

在切入点表达式以注解的方式进行

@After("@annotation(com.example.aop.MyLog)")
public void after(){
    ...
}
3.3. 连接点

在Spring中使用JoinPoint抽象了连接点,用它可以获取方法执行时的相关信息,如目标类目,方法名,方法参数等。

  • 对于环绕通知(@around),获取连接点信息只能使用ProceedingJoinPoint
  • 对于其他四种通知,获取连接点信息只能使用JoinPoint,他是ProceedingJoinPoint的父类型。
// 我们只在环绕通知中演示,因为API都是相同的
@Component
@Aspect
@Slf4j
public class MyAspect5 {

    @Pointcut("@annotation(com.example.aop.MyLog)")
    public void pt(){}

    @Before("pt()")
    public void before(JoinPoint joinPoint){
        log.info("before ...");
    }
    @Around("pt()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("around ... before");
        // 1. 获取目标对象的类名
        log.info("目标对象的类名:"+joinPoint.getTarget().getClass().getName());
        // 2. 获取目标方法的方法名
        log.info("目标方法的方法名"+joinPoint.getSignature().getName());
        // 3. 目标方法运行时传入的参数
        log.info("目标方法运行时传入的参数"+ Arrays.toString(joinPoint.getArgs())); // 数组不能直接输出
        // 4. 放行,目标方法执行
        Object object = joinPoint.proceed();
        // 5. 获取目标方法的返回值
        log.info("目标方法的返回值"+ object);

        log.info("around ... after");
        return object;
    }
}
// 查看结果
com.example.aop.MyAspect5                : around ... before
com.example.aop.MyAspect5                : 目标对象的类名:com.example.service.impl.DeptServiceImpl
com.example.aop.MyAspect5                : 目标方法的方法名select
com.example.aop.MyAspect5                : 目标方法运行时传入的参数[1]
com.example.aop.MyAspect5                : before ...
com.example.aop.MyAspect5                : 目标方法的返回值[Dept(id=1, name=学工部, createTime=2023-11-30T13:55:55, updateTime=2023-11-30T13:55:55)]
com.example.aop.MyAspect5                : around ... after

三、AOP案例

3.1. 分析

需求:将项目中的增、删、改、相关接口的操作日志记录到数据库表中

  • 操作日志包含:操作人,操作时间,执行方法的全类名,执行方法名,方法运行时的参数,返回值,方法运行时长。
    思路分析:
  • 需要对方法添加统一的功能,使用AOP最方便,并且需要计算运行时长,所以使用 环绕通知
  • 因为增删改的方法名没有规则,所以使用注解的方式写切入表达式
    步骤:
    • 准备:
      • 案例中引入AOP的起步依赖
      • 设计数据表结构,并且引入对应的实体类
    • 编码:
      • 自定义注解:@Log
      • 定义切面类,完成记录操作日志的逻辑代码
3.2. 开始干活

3.2.1. 创建数据库:

create table operate_log
(
   id            int unsigned primary key auto_increment comment 'ID',
   operate_user  int unsigned comment '操作人ID',
   operate_time  datetime comment '操作时间',
   class_name    varchar(100) comment '操作的类名',
   method_name   varchar(100) comment '操作的方法名',
   method_params varchar(1000) comment '方法参数',
   return_value  varchar(2000) comment '返回值',
   cost_time     bigint comment '方法执行耗时, 单位:ms'
) comment '操作日志表';

3.2.2. 引入依赖

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

<!-- fastJSON  阿里巴巴提供的转JSON的工具-->
<!-- 因为返回值是一个json的,但数据库表需要的是字符串,所以使用此工具将json转换成String -->
<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>fastjson</artifactId>
   <version>2.0.7</version>
</dependency>

3.2.3. 新建实体类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperateLog {
    private Integer id; //ID
    private Integer operateUser; //操作人ID
    private LocalDateTime operateTime; //操作时间
    private String className; //操作类名
    private String methodName; //操作方法名
    private String methodParams; //操作方法参数
    private String returnValue; //操作方法返回值
    private Long costTime; //操作耗时
}

3.2.4. 新建Mapper层

@Mapper
public interface OperateLogMapper {
    //插入日志数据
    @Insert("insert into operate_log (operate_user, operate_time, class_name, method_name, method_params, return_value, cost_time) " +
            "values (#{operateUser}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime});")
    void insert(OperateLog log);
}

3.2.5. 新建注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Log {
}

3.2.6. 定义切面类,完成记录操作日志的逻辑代码

@Component
@Aspect
@Slf4j
public class LogAspect {

    @Autowired
    private HttpServletRequest request;

    @Autowired
    private OperateLogMapper operateLogMapper;

    @Around("@annotation(com.example.anno.Log)")
    public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
        //操作人ID    因为jwt令牌有登录人信息,所以解析jwt令牌就可以
//        String token = request.getHeader("token");
//        Claims claims = JwtUtils.parseJWT(token);
//        Integer user = (Integer) claims.get("id");
        //  使用链式编程   ↓↓↓
        Integer user = (Integer) JwtUtils.parseJWT(request.getHeader("token")).get("id");
        //操作时间
        LocalDateTime optionTime = LocalDateTime.now();
        //操作类名
        String className = joinPoint.getTarget().getClass().getName();
        //操作方法名
        String methodName = joinPoint.getSignature().getName();
        //操作方法参数
        String args = Arrays.toString(joinPoint.getArgs());
        long start = System.currentTimeMillis(); // 记录方法开始运行时间
        // 调用原始方法
        Object result = joinPoint.proceed();
        long end = System.currentTimeMillis(); // 记录方法结束运行时间
        //操作方法返回值
        String returnValue = JSONObject.toJSONString(result);
        //操作耗时
        long costTime = end - start;
        // 记录操作日志
        OperateLog operateLog = new OperateLog(null, user, optionTime, className, methodName, args, returnValue, costTime);
        operateLogMapper.insert(operateLog);
        log.info("AOP记录操作日志:{}", operateLog);
        return result;
    }
}

3.2.7. 给需要记录的方法上面添加自定义的注解

// 这里就不一一展示了    
/**
     * 根据id删除部门
     */
    @Log
    @DeleteMapping("/{id}")
    public Result delete(@PathVariable Integer id){
        log.info("根据id删除部门:{}",id);
        deptService.delete(id);
        return Result.success();
    }
    /**
     * 添加部门
     */
    @Log
    @PostMapping
    public Result add(@RequestBody Dept dept){
        log.info("添加部门{}",dept);
        deptService.add(dept);
        return Result.success();
    }
3.3. 查看结果

刚刚进行了部门的增删改以及员工的增删改操作,我们查看数据库,看有没有被记录。

1,1,2024-10-27 20:20:23,com.example.controller.DeptController,delete,[15],"{""code"":1,""msg"":""success""}",40
2,1,2024-10-27 20:20:45,com.example.controller.DeptController,add,"[Dept(id=null, name=测试部, createTime=null, updateTime=null)]","{""code"":1,""msg"":""success""}",5
3,1,2024-10-27 20:21:00,com.example.controller.EmpController,sava,"[Emp(id=null, username=测试, password=null, name=测试, gender=1, image=, job=1, entrydate=2024-10-20, deptId=16, createTime=null, updateTime=null)]","{""code"":1,""msg"":""success""}",6
4,1,2024-10-27 20:23:01,com.example.controller.DeptController,add,"[Dept(id=null, name=1, createTime=null, updateTime=null)]","{""code"":1,""msg"":""success""}",8
5,1,2024-10-27 20:23:18,com.example.controller.DeptController,delete,[17],"{""code"":1,""msg"":""success""}",12

在这里插入图片描述

完全符合要求!!!!!!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【万户软件-注册安全分析报告-无验证方式导致安全隐患】

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 1. 暴力破解密码&#xff0c;造成用户信息泄露 2. 短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉 3. 带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造…

HBuilder X 中Vue.js基础使用4->表单输入绑定(三)

表单绑定是实现动态数据双向绑定的重要部分&#xff0c;它让开发者可以轻松地管理和响应用户输入。本文将详细介绍如何在Vue 3中利用v-model指令以及一些特定修饰符来处理不同类型的表单输入。 v-model双向数据绑定 Vue的 v-model 指令提供了双向绑定的功能&#xff0c;key在…

动态规划-回文串问题——647.回文子串

1.题目解析 题目解析&#xff1a;647.回文子串——力扣 测试用例 2.算法原理 1.状态表示 本题需要判断一段字符串是否为回文子串&#xff0c;因此最简单的方法就是保存起开始位置与结束位置&#xff0c;那么就需要一个二维的dp表来保存一段字符串是否为回文子串&#xff0c;…

AI绘画王者归来!SD恐怖如斯!Facebook最强人体分割大师Sapiens 吊打SAM2,亦可Pose,Depth,Normal,ComfyUI

在AI绘画领域&#xff0c;SD恐怖如斯和Facebook的Sapiens模型一直是业界关注的焦点。而最近&#xff0c;Sapiens模型凭借其强大的人体分割能力&#xff0c;再次成为AI绘画领域的佼佼者。 SD恐怖如斯和Sapiens模型的优势 SD恐怖如斯是一款基于深度学习的AI绘画模型&#xff0c;…

Redis-06 Redis复制

主&#xff1a; 192.168.248.132 6379 从1&#xff1a; 192.168.248.140 6380 从2&#xff1a; 192.168.248.139 6381 1.三大命令 拷贝一个新的redis.conf&#xff08;出厂默认&#xff0c;没修改的&#xff09;的文件 2.配置详情 2.1 改为yes 2.2 87行注释掉 2.3 改为no …

什么是成品系统源码,哪里有成品源码,成品源码二次开发需要多久?

成品系统源码指的是已经开发完成、可以立即部署或根据需求进行二次开发的软件系统源代码。这些源码通常包括但医疗信息化软件&#xff08;如HIS、LIS、PACS等&#xff09;、智慧工地源码、家政预约上门系统、实验室管理系统、定位系统源码以及生产管理系统等。 1、医疗信息化软…

[OceanBase-不止于记录]:揭秘双引擎战略,共探AI时代数据架构未来

前言 又到了一年一度大家最爱的探会文章&#xff0c;非常荣幸收到OceanBase官方的邀请参加2024 OceanBase 年度发布会&#xff0c;作为一个经常参加线下探会的博主&#xff0c;每一次体验都有所不同&#xff0c;每一次新技术的突破都让人感到无比兴奋。同时&#xff0c;作为数…

ELK之路第三步——日志收集筛选logstash和filebeat

logstash和filebeat&#xff08;偷懒版&#xff09; 前言logstash1.下载2.修改配置文件3.测试启动4.文件启动 filebeat1.下载2.配置3.启动 前言 上一篇&#xff0c;我们说到了可视化界面Kibana的安装&#xff0c;这一篇&#xff0c;会简单介绍logstash和filebeat的安装和配置。…

终于完工! ffmpeg 视频滤镜:添加文本-drawtext

滤镜描述 drawtext 官网链接 》 FFmpeg Filters Documentation 这个滤镜可以给视频添加上文本&#xff0c;可以给文本加边框、颜色、阴影。注意不是字幕功能&#xff0c;因为这个滤镜不能精准的控制开始和结束的时间。 滤镜使用 参数 fontfile <string> …

【模型学习之路】手写+分析Transformer

手写分析transformer 目录 前言 positional encoding 注意力机制 多头注意力 高维度乘法 多头注意力机制 多头注意力层的实现 Encoder FeedForwardNet EncoderLayer Encoder Decoder DecoderLayer Decoder 组装Trasformer! 后话 测试一下 mask 前言 Attenti…

Z 检验和 T 检验之间的区别

目录 一、说明 二、什么是假设检验&#xff1f; 三、假设检验基础 3.1 假设检验的基本概念 3.2 、执行假设验证的步骤 3.3 临界值、P 值 3.4 方向假设 3.5 非方向假设检验s 四、什么是 Z 检验统计量&#xff1f; 五、Z 检验示例 5.1 单样本 Z 检验 5.2 双样本 Z 检…

动态规划 —— 路径问题-下降路径最小和

1. 下降路径最小和 题目链接&#xff1a; 931. 下降路径最小和 - 力扣&#xff08;LeetCode&#xff09;https://leetcode.cn/problems/minimum-falling-path-sum/description/ 2. 算法原理 状态表示&#xff1a;以莫一个位置位置为结尾 dp[i&#xff0c;j]表示&#xff1a;到…

大模型是怎么训练的 微调vsRAG

模型训练的关键 在理解提示工程、RAG和微调时&#xff0c;我们首先需明白大模型的训练依托于海量多样数据&#xff0c;使其具备跨领域的综合能力。以一个具体案例为例&#xff0c;当面对问题解答失败的情况时&#xff0c;需从三方面分析&#xff1a;一、提问者表述不清&#x…

SAP ABAP开发学习——第一代增强(包含增强演示)

​​​​​​SAP ABAP开发学习——第二代增强&#xff08;包含增强演示&#xff09;-CSDN博客 SAP ABAP开发学习——第三代增强&#xff08;BADI)-CSDN博客 概念 第一代增强(增强嵌入标准程序中) 第一代出口-User exit 以SD用户出口为例 SD及MM较多的程序都是基于源码控制来…

基础IO -- 标准错误输出stderr

目录 1&#xff09;为什么要有 fd 为 2 的 stderr 2&#xff09;使2和1重定向到一个文件中 这里我们谈一下以前只是了解过的stderr 通过两段代码&#xff0c;显然&#xff0c;我们可以知道两个FILE*都是指向显示器的 对于重定向&#xff0c;只有stdout才会将打印的数据重定向…

Cursor 写一个 Flutter Unsplash 壁纸工具 | 从零开始

Cursor 写一个 Flutter Unsplash 壁纸工具 | 从零开始 视频 https://space.bilibili.com/404904528/channel/collectiondetail?sid4106380 https://www.youtube.com/watch?v-ecvMPs5vN4&listPL274L1n86T835KIPMBSwWMy1At6XCJDVR 前言 原文 用Cursor和Flutter构建动态图…

十分钟Linux中的epoll机制

epoll机制 epoll是Linux内核提供的一种高效I/O事件通知机制&#xff0c;用于处理大量文件描述符的I/O操作。它适合高并发场景&#xff0c;如网络服务器、实时数据处理等&#xff0c;是select和poll的高效替代方案。 1. epoll的工作原理 epoll通过内核中的事件通知接口和文件…

【每日刷题】Day147

【每日刷题】Day147 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;每日刷题&#x1f34d; &#x1f33c;文章目录&#x1f33c; 1. 神奇数_牛客笔试题_牛客网 2. DNA序列__牛客网 3. I-十字爆破_牛客小白月赛25 1. 神奇数_牛客笔…

干部出国境管理系统:规范管理,确保安全

在全球化的时代背景下&#xff0c;干部因工作需要或个人原因出国境的情况日益增多。为了加强对干部出国境的管理&#xff0c;确保干部出国境活动规范、有序、安全&#xff0c;干部出国境管理系统应运而生。 一、干部出国境管理系统的重要性 规范管理流程 干部出国境管理系统…

基于Qt的多线程并行和循序运行实验Demo

致谢&#xff08;Acknowledgement&#xff09;&#xff1a; 感谢Youtube博主Qt With Ketan与KDAB精心录制的Qt多线程处理应用教程&#xff0c;感谢Bilibili博主爱编程的大丙对Qt多线程与线程池内容深入浅出的讲解。 一、计算机线程相关概念 线程概念[1]&#xff1a; 在计算机科…