SpringBoot核心框架之AOP详解
SpringBoot核心框架之AOP详解
一、AOP基础
1.1 AOP概述
- AOP:Aspect Oriented Programming(面向切面编程,面向方面编程),其实就是面向特定方法编程。
- 场景:项目部分功能运行较慢,定位执行耗时较长的业务方法,此时就需要统计每一个业务的执行耗时。
- 思路:给每个方法在开始前写一个开始计时的逻辑,在方法结束后写一个计时结束的逻辑,然后相减得到运行时间。
思路是没问题的,但是有个问题,一个项目是有很多方法的,如果挨个增加逻辑代码,会相当繁琐,造成代码的臃肿,所以可以使用AOP编程,将计时提出成一个这样的模板:
- 获取方法运行开始时间
- 运行原始方法
- 获取方法运行结束时间,计算执行耗时
原始方法就是我们需要计算时间的方法,并且可以对原始方法进行增强,其实这个技术就是用到了我们在Java基础部分学习的动态代理技术。
实现:动态代理是面向切面编程最主流的实现。而SpringAOP是Spring框架的高级技术,旨在管理bean对象的过程中,主要是通过底层的动态代理机制,对特点的方法进行编程。
1.2 AOP快速入门
统计各个业务层方法执行耗时
- 导入依赖:在pom.xml中导入AOP的依赖。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- 编写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;}
}
- 查看结果
这样我们就完成了,一个AOP的小例子,但是AOP的功能远不能这些,他还有更多的实用的功能。比如:记录操作日志:可以记录谁什么时间操作了什么方法,传了什么参数,返回值是什么都可以很方便的实现。还有比如权限控制,事务管理等等。
我们来总结一下AOP的优势
- 代码无侵入
- 减少重复代码
- 提高开发效率
- 维护方便
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支持的通知类型
通知类型:
- 环绕通知(Around Advice)
重点
!!!:
- 使用
@Around
注解来定义。 - 包围目标方法的执行,可以在方法执行前后执行自定义逻辑,并且可以控制目标方法的执行。
- 通过
ProceedingJoinPoint
参数的proceed()
方法来决定是否执行目标方法。
- 前置通知(Before Advice):
- 使用
@Before
注解来定义。 - 在目标方法执行之前执行,无论方法是否抛出异常,都会执行。
- 不能阻止目标方法的执行。
- 后置通知(After Advice) 也叫最终通知:
- 使用
@After
注解来定义。 - 在目标方法执行之后执行,无论方法是否抛出异常,都会执行。
- 通常用于资源清理工作
- 返回通知(After Returning Advice)
了解
: - 使用
@AfterReturning
注解来定义。 - 在目标方法成功执行之后执行,即没有抛出异常时执行。
- 可以获取方法的返回值。
- 异常通知(After Advice)
了解
:
- 使用
@AfterThrowing
注解来定义。 - 在目标方法抛出异常后执行。
- 可以获取抛出的异常对象。
注意事项:
- 环绕通知需要自己调用
joinPoint.proceed()
来让原始方法执行,其他通知则不需要。 - 环绕通知的返回值必须是
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; //IDprivate Integer operateUser; //操作人IDprivate 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 {@Autowiredprivate HttpServletRequest request;@Autowiredprivate 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@PostMappingpublic 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
完全符合要求!!!!!!
相关文章:

SpringBoot核心框架之AOP详解
SpringBoot核心框架之AOP详解 一、AOP基础 1.1 AOP概述 AOP:Aspect Oriented Programming(面向切面编程,面向方面编程),其实就是面向特定方法编程。 场景:项目部分功能运行较慢,定位执行耗时…...

Linux: network: ifconfig已经过时,建议使用ip addr相关命令
最近有一个同事在问网络的问题,在debug的过程中还在使用ifconfig命令查看IP的相关信息。 但是这个ifconfig已经不推荐使用了,最好使用ip 相关的命令做操作。 有些信息使用ifconfig显示不出来 ifconfig eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500ine…...

Flutter 鸿蒙next中的路由使用详解【基础使用】
✅近期推荐:求职神器 https://bbs.csdn.net/topics/619384540 🔥欢迎大家订阅系列专栏:flutter_鸿蒙next 💬淼学派语录:只有不断的否认自己和肯定自己,才能走出弯曲不平的泥泞路,因为平坦的大路…...

基于SSM+小程序民宿短租管理系统(民宿1)
👉文末查看项目功能视频演示获取源码sql脚本视频导入教程视频 1、项目介绍 基于SSM小程序民宿短租管理系统实现了管理员、用户及房主 1、管理员可以管理民宿信息和订单信息用户管理、房主管理、房间类型管理、预定管理等。 2、房主可以管理自己的民宿和订单 3、…...

SQL LIKE 操作符
SQL LIKE 操作符 在SQL中,LIKE 操作符用于在查询中搜索列中的特定模式。它通常与 % 和 _ 通配符一起使用,分别代表任意数量的字符和单个字符。LIKE 操作符在数据过滤和模式匹配方面非常有用,尤其是在处理大量文本数据时。 LIKE 操作符的基本…...

七款主流图纸加密软件强力推荐|2024年CAD图纸加密保护指南
在当今信息化的设计行业,保护CAD图纸的知识产权和数据安全变得尤为重要。随着越来越多的企业采用数字化设计和共享文件,如何防止CAD图纸被未经授权的访问和窃取成为了许多设计师和企业关注的焦点。为此,选用合适的图纸加密软件是保护CAD文件安…...

【STM32】单片机ADC原理详解及应用编程
本篇文章主要详细讲述单片机的ADC原理和编程应用,希望我的分享对你有所帮助! 目录 一、STM32ADC概述 1、ADC(Analog-to-Digital Converter,模数转换器) 2、STM32工作原理 二、STM32ADC编程实战 (一&am…...

C# 委托简述
1.委托 1.1什么是委托 委托委托 官网解释: 委托是安全封装方法的类型,类似于 C 和 C 中的函数指针。 与 C 函数指针不同的是,委托是面向对象的、类型安全的和可靠的。 委托的类型由委托的名称确定。 个人理解:委托就是一个方法的模板。它可以接收…...

瑞吉外卖项目
目录 Day01业务开发 一、项目总体介绍与展示 二、软件开发整体介绍 (一)软件开发流程 三、瑞吉外卖项目介绍 (一)项目介绍 (二)技术选型功能架构 1.技术选型—— 编辑2.功能架构—— 编辑 &a…...

Docker:4、龙晰(Anolis OS 8.8)宝塔面板安装
接上文Docker:1、基于龙晰 (Anolis OS 8.8 )的基础镜像制作,本节我们介绍:基于Docker的龙晰(Anolis OS 8.8 )宝塔安装。 在第一节中由于我们对 Docker 容器进行了SSH设置,这为我们这…...

多端项目开发全流程详解 - 从需求分析到多端部署
引言 在当今互联网时代,一个完整的产品常常需要覆盖多个终端,包括小程序、Web端(后台管理系统)、App端等。本文将详细介绍一个采用前后端分离架构的多端项目开发流程,重点分析各个终端的特点、功能定位及其开发要点。…...

4.5KB原生html+js+css实现图片打印位置的坐标和尺寸获取
一般用于图片打印文字或图片的坐标获取,代码来自AI有改动。 功能:本地图选择后不上传直接可比划线条作为对角线得到矩形,动态显示坐标 按下鼠标开始松开鼠标结束。有细微BUG但不影响坐标获取。 <!DOCTYPE html> <html lang"en">…...

智诊小助手-记录模式选择
记录模式总共有连续记录、硬件触发、软件触发、错误触发四种模式选择,并且在选择完记录模式后还可以设置保留触发点前报文条数、存储时间、录制通道、保存类型 配置过程如下: 点击下面右图中模式选择即可进入到左图中的参数配置界面 如上图选择的配置…...

JDBC: Java数据库连接的桥梁
什么是JDBC? Java数据库连接(Java Database Connectivity,简称JDBC)是Java提供的一种API,允许Java应用程序与各种数据库进行交互。JDBC提供了一组标准的接口,开发者可以利用这些接口执行SQL语句、处理结果集…...

英伟达GPU算力【自用】
GPU(图形处理单元)算力的提升是驱动当代科技革命的核心力量之一,尤其在人工智能、深度学习、科学计算和超级计算机领域展现出了前所未有的影响力。2024年的GPU技术发展,不仅体现在游戏和图形处理的传统优势上,更在跨行…...

「C/C++」C++11 之 智能指针
✨博客主页何曾参静谧的博客📌文章专栏「C/C」C/C程序设计📚全部专栏「VS」Visual Studio「C/C」C/C程序设计「UG/NX」BlockUI集合「Win」Windows程序设计「DSA」数据结构与算法「UG/NX」NX二次开发「QT」QT5程序设计「File」数据文件格式「PK」Parasoli…...

算法面试小抄
第一章:算法与数据结构要点速学 1.时间复杂度 (大 O) 首先,我们来谈谈常用操作的时间复杂度,按数据结构/算法划分。然后,我们将讨论给定输入大小的合理复杂性。 数组(动态数组/列表) 规定 n arr.length, 注意: &am…...

当有违法数据时,浏览器不解析,返回了undefined,导致数据不解析
现象:页面上没有看到数据 排查:断点到线上的源码里:1、协议回调确实没有拿到数据是个undefined 2、network里看服务确实响应了数据 3、控制台没有任何报错。 心情:莫名其妙的现象 我本地有json格式化工具,copy进去后&…...

在MySQL中ORDER BY使用的那种排序算法
在 MySQL 中,ORDER BY 子句的排序算法通常根据场景、数据量和表的索引情况而有所不同。MySQL 常用的排序算法包括: 文件排序(File Sort):MySQL 没有使用索引排序的情况下,会进行文件排序,这可以…...

学习threejs,使用粒子实现雨滴特效
👨⚕️ 主页: gis分享者 👨⚕️ 感谢各位大佬 点赞👍 收藏⭐ 留言📝 加关注✅! 👨⚕️ 收录于专栏:threejs gis工程师 文章目录 一、🍀前言1.1 ☘️THREE.Points简介1.11 ☘️…...

分布式-单元化架构1
一 两地三中心 1.1 两地三中心* 两地指的是两个城市:同城,异地。三中心指的是三个数据中心:生产中心、同城容灾中心、异地容灾中心。 在同一个城市或者临近的城市建设两个相同的系统,双中心具备相当的业务处理能力,…...

C++模板、STL
目录 一、模板 1、函数模板 (1)、基本语法和使用 (2)、函数模板注意事项 (3)、普通函数与函数模板的区别 (4)、普通函数与函数模板的调用规则 (5)、模板的局限性 2、类模板 (1)、基本语法 (2)、类模板与函数模板区别 (3)、类模板中成员函数创建时机 (4)、类模板对象…...

计算机视觉中的点算子:从零开始构建
Hey小伙伴们!今天我们要聊的是一个非常基础但极其重要的计算机视觉技术——点算子(Point Operators)。点算子主要用于对图像的每个像素进行独立的处理,比如亮度调整、对比度增强、灰度化等。通过这些简单的操作,我们可…...

国际中文教育知识图谱问答
你还在为毕业设计头疼么?想快速搭建一个智能化系统,展示数据又能精准回答问题?那你绝对不能错过这个超实用的 知识图谱问答系统,特别适用于需要整合复杂数据关系、交互性强的项目! 这个系统基于 Neo4j图数据库 开发&a…...

酒店大板轻触开关与传统的开关有什么区别
酒店大板轻触开关与传统的开关在功能、设计、使用方式以及安装维护等多个方面都存在显著的差异。以下是对这些差异的详细分析: 功能差异 酒店大板轻触开关: 多功能性:酒店大板轻触开关通常集成了多种功能,如控制照明、窗帘、夜灯、…...

【蓝桥杯选拔赛真题78】python电话号码 第十五届青少年组蓝桥杯python选拔赛真题 算法思维真题解析
目录 python电话号码 一、题目要求 1、编程实现 2、输入输出 二、算法分析 三、程序编写 四、程序说明 五、运行结果 六、考点分析 七、 推荐资料 1、蓝桥杯比赛 2、考级资料 3、其它资料 python电话号码 第十五届蓝桥杯青少年组python比赛选拔赛真题 一、题目要…...

对比两个json串的diff,支持map的深度递归
背景 项目重构,对老接口进行技术改造。动代码后,难免会有些bug,我们需要对比改造前后接口的返回,来判断逻辑是否有问题,这就涉及两个json的对比。 常规的diff文本工具是按行对比,无法处理复杂的map。本文通…...

【我的创作纪念日1024】
我的创作纪念日1024 机缘成就明年的规划 机缘 过去的1024个日子里,我在专业发展、职场和发展、科技创新创业、软件开发、人工智能、虚拟现实、区块链等栏目分享了一些工作和学习的建议和体会。尤其是在2022年,我连续发布100篇的博文,不仅仅是…...

萤石设备视频接入平台EasyCVR私有化视频平台变电站如何实现远程集中监控?
一、方案背景 随着城市经济的发展和电力系统的改造,变电站的数量和规模逐渐增加,对变电站的安全管理和监控需求也越来越高。视频监控系统作为重要的安全管理手段,在变电站中起到了关键的作用。 目前青犀视频研发的萤石设备视频接入平台EasyC…...

什么是多线程?请描述 Java 中实现多线程的基本方式?
今天和大家探讨一下 Java 中的多线程,包括它的基本概念、实现方式以及一些实际开发中的注意事项。 什么是多线程? 多线程是指在一个程序中存在多个执行流,每个执行流都可以独立于其他执行流执行。 在 Java 中,多线程允许开发者…...