【Spring底层分析】Spring AOP基本使用+万字底层源码阅读分析
一、AOP基本使用
三步:
- 将业务逻辑组件和切面类都加入到容器中,告诉Spring哪个是切面类(@Aspect)
- 在切面类上的每一个通知方法上标注通知注解,告诉Spring何时(@Before、@After、@Around……)何地运行(切入点表达式)
- 开启基于注解的aop模式,@EnableAspectJAutoProxy
定义业务逻辑类、切面类:
public class MathCalculator {public int div(int i, int j){return i / j;}
}
@Aspect //需要标注此注解,spring才知道这是切面类
public class LogAspects {@Pointcut("execution(public int org.example.MathCalculator.*(..))")public void pointCut(){};//@Before(value = "public int org.example.MathCalculator.div(int, int)")//@Before("public int org.example.MathCalculator.*(..)")@Before("pointCut()")public void logStart(JoinPoint joinPoint){Object[] args = joinPoint.getArgs();System.out.println("除法方法:" + joinPoint.getSignature().getName() + "开始运行,参数列表是:" + Arrays.asList(args));}@After("pointCut()")public void logEnd(){System.out.println("除法方法结束运行");}@AfterReturning(value = "pointCut()", returning = "result")public void logReturn(Object result){System.out.println("除法正常运行并返回结果:" + result);}@AfterThrowing(value = "pointCut()", throwing = "exception")public void logException(Exception exception){System.out.println("除法异常运行,异常信息:" + exception);}}
将业务逻辑类和切面类注册到spring容器中(这里我用的方式是配置类方式):
@EnableAspectJAutoProxy
@Configuration
public class AopConfiguration {// 业务逻辑类加入容器中@Beanpublic MathCalculator mathCalculator(){return new MathCalculator();}// 切面类加入容器中@Beanpublic LogAspects logAspects(){return new LogAspects();}
}
测试:
public class AopTest {@Testpublic void test01(){AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AopConfiguration.class);//MathCalculator mathCalculator = new MathCalculator();不是自己创建对象,而是从spring拿对象,这样切面才会生效MathCalculator mathCalculator = applicationContext.getBean(MathCalculator.class);mathCalculator.div(4,2); //测试目标方法是否被增强applicationContext.close();}
}
二、AOP原理
总结:
1)、@EnableAspectJAutoProxy 开启AOP功能
2)、@EnableAspectJAutoProxy 会给容器中注册一个组件:AnnotationAwareAspectJAutoProxyCreator
3)、AnnotationAwareAspectJAutoProxyCreator是一个后置处理器;
4)、容器的创建流程:
1)、registerBeanPostProcessors()注册后置处理器;创建AnnotationAwareAspectJAutoProxyCreator后置处理器
2)、 finishBeanFactoryInitialization () 初始化剩下的单实例bean
1)、如创建业务逻辑类组件、切面类组件
2)、AnnotationAwareAspectJAutoProxyCreator会拦截以上组件的创建过程:组件创建完之后,判断组件是否需要增强?
是:切面的通知方法,包装成增强器(Advisor);给业务逻辑组件创建一个代理对象(cglib)
5)、执行目标方法
1)代理对象执行目标方法
1)CglibAopProxy.intercept()来拦截目标方法
2)得到目标方法的拦截器链(增强器包装成拦截器MethodInterceptor)
3)利用拦截器的链式机制,依次进入每一个拦截器进行执行
4)效果:
正常执行:前置通知->目标方法->后置通知->返回通知
出现异常:前置通知->目标方法->后置通知->异常通知
分析AOP原理时,我们从这个注解开始分析:
启示,如果想研究xx原理,就可以从@EnableXXX注解分析,分析是否给容器注入了什么组件,我们只需要搞清楚这个组件的作用是什么,就可以分析出原理。
一、从@EnableAspectJAutoProxy注解开始分析
1、@EnableAspectJAutoProxy
- AspectJAutoProxyRegistrar.class
- 跟进registerAspectJAnnotationAutoProxyCreatorIfNecessary方法中,一路跟进,会进入以下方法:
也就是说@EnableAspectJAutoProxy注解的作用就是给容器中注入bean定义,为后边往容器中注入组件做准备。
internalAutoProxyCreator(组件名),AnnotationAwareAspectJAutoProxyCreator(组件类型)
所以接下来我们就是探究AnnotationAwareAspectJAutoProxyCreator组件的是功能是什么,何时工作?
- AnnotationAwareAspectJAutoProxyCreator.class
- AspectJAwareAdvisorAutoProxyCreator.class
- AbstractAdvisorAutoProxyCreator.class
- AbstractAutoProxyCreator.class
可以得出结论,AbstractAutoProxyCreator是一个后置处理器,还实现了aware接口,以下是重写的方法:
在AbstractAutoProxyCreator类中重写了后置处理器的两个方法:
在AbstractAdvisorAutoProxyCreator类中重写了aware接口的方法:
- SmartInstantiationAwareBeanPostProcessor相较于普通的后置处理器,额外还有以下两个需要重写的方法XXXInstantiation,这两个方法的执行时机也与XXXInitialization方法执行时机(bean初始化前后执行)不同。我们之后会分析。
以上是与AOP有关的组件的大致介绍。接下来我们启动AOP的测试demo,从容器创建、创建单实例bean、调用目标方法来分析AOP是怎么起作用的。
这里启动的demo就是本文介绍AOP基本使用的那个demo。
二、启动容器
1)registerBeanPostProcessors():注册所有后置处理器到容器中
1、先获取ioc容器已经定义了的需要创建对象的所有BeanPostProcessor的名字。其中一个就是AbstractAutoProxyCreator后置处理器的,这个定义就是在通过@EnableAspectJAutoProxy创建的
2、注册后置处理器
2.1、优先注册实现了PriorityOrdered接口的BeanPostProcessor;
2.2、再给容器中注册实现了Ordered接口的BeanPostProcessor;这里注册的就是我们的AbstractAutoProxyCreator。因为AbstractAutoProxyCreator后置处理器实现了@order接口。
2.3、注册没实现优先级接口的BeanPostProcessor;
后置处理器注册到容器中的过程:
1、创建bean实例:createBeanInstance()
2、属性赋值:populateBean()
3、初始化bean:initializeBean(),进入initializeBean()
3.1.invokeAwareMethods() -> 如果该bean实现aware接口,则执行重写aware接口的方法
3.2.applyBeanPostProcessorsBeforeInitialization()->执行该bean实现后置处理器接口,重写的postProcessBeforeInitialization()
3.3.invokeInitMethods()->执行初始化方法
3.4.applyBeanPostProcessorsAfterInitialization()->执行该bean实现后置处理器接口,重写的postProcessAfterInitialization()
3、将后置处理器注册到beanFactory中。
以上,AbstractAutoProxyCreator后置处理器已经注册到容器中了,这个后置处理器会在注册单实例bean(MathCalculator)时起作用。
2)finishBeanFactoryInitialization:注册所有单实例bean到容器中
1、先尝试用后置处理器获取代理对象【只有当后置处理是InstantiationAwareBeanPostProcessor类型时才有效】
resolveBeforeInstantiation()
由于AbstractAutoProxyCreator后置处理器就是InstantiationAwareBeanPostProcessor类型的,因此就会执行AbstractAutoProxyCreator里重写的postProcessBeforeInstantiation方法。【我们主要关心MathCalculator、LogAspects】
1.1、applyBeanPostProcessorsBeforeInstantiation()->postProcessBeforeInstantiation()
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) {Object cacheKey = this.getCacheKey(beanClass, beanName);if (!StringUtils.hasLength(beanName) || !this.targetSourcedBeans.contains(beanName)) {if (this.advisedBeans.containsKey(cacheKey)) {return null;}if (this.isInfrastructureClass(beanClass) || this.shouldSkip(beanClass, beanName)) {this.advisedBeans.put(cacheKey, Boolean.FALSE);return null;}}TargetSource targetSource = this.getCustomTargetSource(beanClass, beanName);if (targetSource != null) {if (StringUtils.hasLength(beanName)) {this.targetSourcedBeans.add(beanName);}Object[] specificInterceptors = this.getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource);Object proxy = this.createProxy(beanClass, beanName, specificInterceptors, targetSource);this.proxyTypes.put(cacheKey, proxy.getClass());return proxy;} else {return null;}}===========================最后直接返回null
1.2、applyBeanPostProcessorsAfterInitialization()->postProcessAfterInitialization()
因为postProcessBeforeInstantiation返回null,因此这里这个方法不执行。
2、真正创建bean实例
doCreateBean()
createBeanInstance()
populateBean()
initializeBean()
applyBeanPostProcessorsAfterInitialization()->postProcessAfterInitialization()
wrapIfNecessary():如果需要则包装成代理对象
1)获取当前bean的且已排好序的增强方法:Object[] specificInterceptors
- 获取所有增强器(通知方法)
- 找哪些通知方法是需要切入当前bean方法的
- 给增强器排序
2)保存当前bean到advisedBeans中:this.advisedBeans.put(cacheKey, Boolean.TRUE);
3)创建当前bean的代理对象:this.createProxy()
-
获取所有增强器(通知方法)
-
保存到proxyFactory
-
创建代理对象:Spring自动决定
- JdkDynamicAopProxy(config)->jdk动态代理
- ObjenesisCglibAopProxy(config)->cglib的动态代理
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {if (NativeDetector.inNativeImage() || !config.isOptimize() && !config.isProxyTargetClass() && !this.hasNoUserSuppliedProxyInterfaces(config)) {return new JdkDynamicAopProxy(config);} else {Class<?> targetClass = config.getTargetClass();if (targetClass == null) {throw new AopConfigException("TargetSource cannot determine target class: Either an interface or a target is required for proxy creation.");} else {return (AopProxy)(!targetClass.isInterface() && !Proxy.isProxyClass(targetClass) && !ClassUtils.isLambdaClass(targetClass) ? new ObjenesisCglibAopProxy(config) : new JdkDynamicAopProxy(config));}}}
4)最后返回代理对象给容器:return proxy
注意,以下情况则返回的是原始对象:
- 核心基础设施类:
Advisor
,Advice
,Pointcut
的实现 - 切面类:被
@Aspect
注解的类(通过shouldSkip()
显式排除) - 无匹配的
Advisor
的类
解释:核心基础设施类是怎么得到的?
假设有以下切面:
@Aspect
public class SecurityAspect {@Around("@annotation(RequireAuth)")public Object checkAuth(ProceedingJoinPoint pjp) {// 权限检查逻辑}
}
Spring会创建以下基础设施对象:
// 1. 创建Pointcut
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("@annotation(RequireAuth)");// 2. 创建Advice
Advice advice = new MethodInterceptor() {public Object invoke(MethodInvocation mi) {// 包装checkAuth方法的逻辑}
};// 3. 创建Advisor
Advisor advisor = new DefaultPointcutAdvisor(pointcut, advice);
那么,Advice与Advisor的区别在哪?
-
Advisor:封装了
Pointcut
(切点) +Advice
(增强)。定义"在哪里增强"和"如何增强"的完整规则 -
Advice:只包含增强行为。定义"具体增强什么"
- @Around:AspectJAroundAdvice
- @Before:AspectJMethodBeforeAdvice
- @After:AspectJAfterAdvice
- @AfterReturning:AspectJAfterReturningAdvice
- @AfterThrowing:AspectJAfterThrowingAdvice
@Aspect
public class LogAspect {// 👇 被拆解为 Pointcut + Advice@Before("execution(* com.service.*.*(..))") public void logBefore() {System.out.println("Before method");}
}
转换过程:
@Before
注解 → 创建AspectJExpressionPointcut
(Pointcut)logBefore()
方法 → 创建MethodBeforeAdviceInterceptor
(Advice)- 组合两者 → 创建
InstantiationModelAwarePointcutAdvisor
(Advisor)
三、调用目标方法
前面我们分析完启动容器后,容器会生成代理对象。当代理对象调用目标方法时,就会走增强的逻辑,具体是怎样的过程,我们现在来分析。
👆容器中保存了组件的代理对象(cglib增强后的对象),这个对象里面保存了详细信息(比如增强器,目标对象,xxx)
1)CglibAopProxy.intercept()
1、根据ProxyFactory对象获取将要执行的目标方法拦截器链:List chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
getInterceptorsAndDynamicInterceptionAdvice()
-
List interceptorList = new ArrayList(advisors.length);interceptorList保存所有拦截器5:一个默认的ExposeInvocationInterceptor 和4个增强器;
-
遍历所有的增强器,将其转为Interceptor;
Interceptor[] interceptors = registry.getInterceptors(advisor)
interceptorList.addAll(Arrays.asList(interceptors));
-
将增强器转为List; 如果是MethodInterceptor,直接加入到集合中。如果不是,使用AdvisorAdapter将增强器转为MethodInterceptor; 转换完成返回MethodInterceptor数组。(统一转换为
MethodInterceptor
目的是标准化执行接口,实现调用链的通用处理机制)MethodInterceptor[] interceptors = registry.getInterceptors(advisor);
interceptorList.addAll(Arrays.asList(interceptors));
public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException {List<MethodInterceptor> interceptors = new ArrayList(3);Advice advice = advisor.getAdvice();if (advice instanceof MethodInterceptor) { // 如果增强器是MethodInterceptor类型,那么直接添加interceptors.add((MethodInterceptor)advice);}Iterator var4 = this.adapters.iterator();while(var4.hasNext()) {AdvisorAdapter adapter = (AdvisorAdapter)var4.next();if (adapter.supportsAdvice(advice)) { // 否则使用适配器进行转换interceptors.add(adapter.getInterceptor(advisor));}}if (interceptors.isEmpty()) {throw new UnknownAdviceTypeException(advisor.getAdvice());} else {return (MethodInterceptor[])interceptors.toArray(new MethodInterceptor[0]);}}===================================1、public class AspectJAfterReturningAdvice extends AbstractAspectJAdvice implements AfterReturningAdvice, AfterAdvice, Serializable {} --->未实现MethodInterceptor接口,因此需要使用对应适配器转换。1.1、转换方法:adapter.getInterceptor(advisor) :public MethodInterceptor getInterceptor(Advisor advisor) {AfterReturningAdvice advice = (AfterReturningAdvice)advisor.getAdvice();return new AfterReturningAdviceInterceptor(advice);} 也就是说,将AspectJAfterReturningAdvice-->AfterReturningAdviceInterceptor。1.2、还有AspectJMethodBeforeAdvice-->MethodBeforeAdviceInterceptor2、其他的advice本身就实现了MethodInterceptor接口,因此无需转变:public class AspectJAfterThrowingAdvice extends AbstractAspectJAdvice implements MethodInterceptor, AfterAdvice, Serializable {}public class AspectJAroundAdvice extends AbstractAspectJAdvice implements MethodInterceptor, Serializable {}public class AspectJAfterAdvice extends AbstractAspectJAdvice implements MethodInterceptor, AfterAdvice, Serializable {}=================================public class AfterReturningAdviceInterceptor implements MethodInterceptor, AfterAdvice, Serializable {private final AfterReturningAdvice advice;public AfterReturningAdviceInterceptor(AfterReturningAdvice advice) {Assert.notNull(advice, "Advice must not be null");this.advice = advice;}@Nullablepublic Object invoke(MethodInvocation mi) throws Throwable {Object retVal = mi.proceed();this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis());return retVal;} } =================================@FunctionalInterfacepublic interface MethodInterceptor extends Interceptor {@NullableObject invoke(@Nonnull MethodInvocation invocation) throws Throwable;}可以看到,MethodInterceptor接口里只包含一个invoke抽象方法,将advice都转换为MethodInterceptor类型,就是为了之后统一调用invoke方法。
-
得到的拦截器链如下:
2、如果没有拦截器链,直接执行目标方法。
3、如果有拦截器链,把需要执行的目标对象、目标方法、拦截器链等信息传入创建一个CglibMethodInvocation对象,
并调用 Object retVal = mi.proceed();
retVal = (new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy)).proceed();
有了拦截器链后,我们现在来分析proceed方法,探究是怎么个拦截法。
递归过程就是:
1、依次按顺序0-4遍历每个拦截器,然后执行invoke方法。
2、0-3号的拦截器中的invoke方法中,第一步都是直接递归调用proceed方法。
3、直到遍历到4号拦截器,也就是MethodBeforeAdviceInterceptor拦截器。
4、接着再一次进入proceed方法时,由于满足出递归的条件判断,因此执行目标方法。
5、目标方法执行完则返回到上一个拦截器的invoke方法中。3号拦截器就是AspectJAfterAdvice。
6、接着继续返回到上一层拦截器的invoke方法中。2号拦截器是AfterReturningAdviceInterceptor。执行对应的增强方法。(只有没有异常才会执行。)
7、接着继续返回到上一层拦截器的invoke方法中。1号拦截器是AspectJAfterThrowingAdvice。执行对应的增强方法。(有异常才会执行。)
以上,就是整个拦截器调用过程。
相关文章:

【Spring底层分析】Spring AOP基本使用+万字底层源码阅读分析
一、AOP基本使用 三步: 将业务逻辑组件和切面类都加入到容器中,告诉Spring哪个是切面类(Aspect)在切面类上的每一个通知方法上标注通知注解,告诉Spring何时(Before、After、Around……)何地运…...
Python数据分析及可视化中常用的6个库及函数(二)
Python数据分析及可视化中常用的6个库及函数(二) 摘要:以下是Python数据分析及可视化常用的6个库的详细介绍,包括它们的概述以及每个库中最常用的10个函数(如果某些库常用函数不足10个,则列出所有常用函数)。每个函数都附带功能描述、用法说明和使用示例。这些库…...

新德通科技:以创新驱动光通信一体化发展,赋能全球智能互联
在数字经济与AI技术高速发展的今天,光通信作为信息传输的核心基础设施,正迎来前所未有的升级浪潮。深圳新德通科技有限公司(以下简称“新德通科技”)凭借其深厚的技术积累与一体化产品布局,成为行业内的中坚力量。本文…...
Selenium的底层原理
Selenium 底层主要依赖于 WebDriver 协议(即 W3C WebDriver 规范,早期也有 JSON Wire Protocol)来实现对浏览器的远程控制,其核心架构可以分为以下几层: Selenium 客户端(Client Library) 支持多…...
PostgreSQL的扩展 auth_delay
PostgreSQL的扩展 auth_delay auth_delay 是 PostgreSQL 提供的一个安全相关扩展,主要用于防止暴力破解攻击。它通过在认证失败后引入人为延迟来增加暴力破解的难度。 一、扩展基础 功能:在认证失败后增加延迟目的:减缓暴力破解和字典攻击…...
[Java 基础]Java 是什么
Java 是一门编程语言。 查看编程语言热门排行:https://www.tiobe.com/tiobe-index/ Java 的特点: 面向对象:Java 是面向对象的语言,支持封装、继承和多态等特性。 平台无关性:Java 通过“一次编写,到处…...
Qt学习2
跟学视频 1.菜单栏和工具栏 //菜单栏最多只能有一个//菜单栏创建QMenuBar * bar menuBar();//将菜单栏放到窗口中setMenuBar(bar);//创建菜单QMenu * fileMenu bar->addMenu("开始");QMenu * editMenu bar->addMenu("编辑");//创建菜单项QAction…...

C++ 内存泄漏检测器设计
文章目录 1. C中的动态内存分配2. 什么是内存泄漏3. 内存泄漏的代码案例4. 内存泄漏检查器的设计模块1:位置信息捕获:模块2:内存分配跟踪:模块3:内存释放跟踪:模块4:泄漏记录存储:模…...
在 Linux 上安装 Nmap 工具
📦 在 Linux 上安装 Nmap 工具指南 Nmap(Network Mapper)是功能强大的网络扫描工具,以下是各种 Linux 发行版的安装方法: 🧩 通用安装方法 1. 使用包管理器安装(推荐) # Debian/…...
从零打造AI面试系统全栈开发
🤖 AI面试系统开发完整教程 📋 项目概述 本教程将带你从零开始构建一个完整的AI面试系统,包含前端、后端、AI集成和部署的全流程。 源码地址 技术栈 前端: React TypeScript Vite Vaadin Components后端: Spring Boot Spring Securi…...

破局与进阶:ueBIM 在国产 BIM 赛道的差距认知与创新实践
作为国产BIM领域的探索者,斯维尔ueBIM自诞生以来始终以追赶国际头部技术为目标,但不可否认的是,在核心功能覆盖、行业生态成熟度以及全球市场占有率等方面,我们与Autodesk Revit、Bentley Systems等国际巨头仍存在显著差距。这种差…...

分布式流处理与消息传递——向量时钟 (Vector Clocks) 算法详解
Java 实现向量时钟 (Vector Clocks) 算法详解 一、向量时钟核心原理 #mermaid-svg-JcZ1GT0r1ZNSy6W7 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-JcZ1GT0r1ZNSy6W7 .error-icon{fill:#552222;}#mermaid-svg-JcZ…...

20250603在荣品的PRO-RK3566开发板的Android13下的命令行查看RK3566的温度
20250603在荣品的PRO-RK3566开发板的Android13下的命令行查看RK3566的温度 2025/6/3 11:58 RK3566的cpu运行效率 top rk3566_t:/ # rk3566_t:/ # rk3566_t:/ # cd /sys/class/thermal/ rk3566_t:/sys/class/thermal # ls -l rk3566_t:/sys/class/thermal # cd thermal_zone0/ r…...

帝可得 - 设备管理
一. 需求说明 设备管理主要涉及到三个功能模块,业务流程如下: 新增设备类型: 允许管理员定义新的售货机型号,包括其规格和容量。 新增设备: 在新的设备类型定义后,系统应允许添加新的售货机实例,并将它们分配到特定的…...
FTXUI配置
对于 FTXUI 的安装与配置, 官方已经给出了三种方案. 第一种: 使用 FetchContent 远程拉取第二种: 在你本地安装 FTXUI 库, 然后通过 find_package 使用第三种: 使用 Git 子模块 FetchContent 无需手动下载安装 FTXUI, 通过 CMake 自动从 GitHub 拉取并编译依赖 include(Fet…...
Caliper压力测试
目前FISCO BCOS适配的Caliper版本为0.2.0,请在部署Caliper运行环境时确保Caliper的版本为0.2.0,如在部署或使用过程中遇到任何问题,请优先参考 https://github.com/FISCO-BCOS/FISCO-BCOS/issues/1248 中的解决方案进行排查。 1. 环境要求 …...

【iOS安全】使用LLDB调试iOS App | LLDB基本架构 | LLDB安装和配置
LLDB基本架构 参考: https://crifan.github.io/ios_re_dynamic_debug/website/debug_code/lldb_debugserver.html https://book.crifan.org/books/ios_re_debug_debugserver_lldb/website/ LLDB安装和配置 1. 让iPhone中出现/Developer/usr/bin/debugserver 最初…...
一、核心概念深入解析
一、核心概念深入解析 1. shared_ptr 的线程安全性澄清 引用计数是原子操作:shared_ptr 的引用计数(use_count)在多线程中递增 / 递减是安全的(原子操作),但对象本身的读写需额外同步(如 std:…...
python直方图
在Python中,绘制直方图(Histogram)是一项非常常见的任务,通常用于数据可视化,以展示数据的分布情况。Python中有多种库可以绘制直方图,其中最常用的两个库是Matplotlib和Seaborn。此外,Pandas库…...
[特殊字符] Unity 性能优化终极指南 — Text / TextMeshPro 组件篇
UGUI Text组件的不当使用及其性能瓶颈与优化 在Unity UGUI系统中,Text 组件(或其升级版 TextMeshPro)是显示文本信息的核心元素。然而,如果不当使用,它极易成为UI性能瓶颈的罪魁祸首,尤其是在预制体、属性…...

Idea 配置 Maven 环境
下载 Maven 官网:https://maven.apache.org/index.html 点击左侧 Downloads,然后选择 Files 中的 zip 包下载(下载慢可以使用迅雷) 配置 Maven 将压缩包解压,比如我解压后放到了 D:\developer\environment\apache-…...
git clone报错:SSL certificate problem: unable to get local issuer certificate
上述报错的完整信息是: Cloning into test... fatal: unable to access https://github.com/xxxx/xxxx.git/: SSL certificate problem: unable to get local issuer certificate 该报错表示 Git 在使用 HTTPS 协议克隆仓库时,无法验证 GitHub 的 SSL …...

Kafka 如何保证不重复消费
在消息队列的使用场景中,避免消息重复消费是保障数据准确性和业务逻辑正确性的关键。对于 Kafka 而言,保证不重复消费并非单一机制就能实现,而是需要从生产者、消费者以及业务层等多个维度协同配合。接下来,我们将结合图文详细解析…...
SpringBoot整合MyBatis完整实践指南
在Java企业级应用开发中,SpringBoot和MyBatis的组合已经成为主流的技术选型方案之一。本文将详细介绍如何从零开始搭建一个基于SpringBoot和MyBatis的项目,包括环境配置、数据库设计、实体类创建、Mapper接口编写以及实际应用等完整流程。 一、环境准备…...

RNN结构扩展与改进:从简单循环网络到时间间隔网络的技术演进
本文系统介绍 RNN 结构的常见扩展与改进方案。涵盖 简单循环神经网络(SRN)、双向循环神经网络(BRNN)、深度循环神经网络(Deep RNN) 等多种变体,解析其核心架构、技术特点及应用场景,…...
docker中,容器时间和宿机主机时间不一致问题
win11下的docker中有个mysql。今天发现插入数据的时间不正确。后来发现原来是docker容器中的时间不正确。于是尝试了各种修改,什么run -e TZ"${tzutil /g}",TZ"Asia/Shanghai",还有初始化时带--mysqld一类的,…...
Unity Shader编程】之高级纹理
一,立方体纹理 Cubemap 用途 用途说明反射贴图表面镜面高光或金属反射环境光采样模拟环境对物体的影响天空盒背景使用六张图拼接场景背景全景投影做360度相机渲染、投影等 二,创建立方体纹理 在 Unity 中创建和保存一个 立方体纹理(Cubema…...

类 Excel 数据填报
类 Excel 填报模式,满足用户 Excel 使用习惯 数据填报,可作为独立的功能模块,用于管理业务流程、汇总采集数据,以及开发各类数据报送系统,因此,对于报表工具而言,其典型场景之一就是利用报表模…...
vscode调试stm32,Cortex Debug的配置文件lanuch.json如何写,日志
https://blog.csdn.net/jiladahe1997/article/details/122046665 https://discuss.em-ide.com/blog/67-cortex-debug 第一版 {// 使用 IntelliSense 了解相关属性。 // 悬停以查看现有属性的描述。// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?li…...

Office文档图片批量导出工具
软件介绍 本文介绍一款专业的Office文档图片批量导出工具。 软件特点 这款软件能够批量导出Word、Excel和PPT中的图片,采用绿色单文件设计,体积小巧仅344KB。 基本操作流程 使用方法十分简单:直接将Word、Excel或PPT文件拖入软件…...