Spring--三级缓存机制

一、什么是三级缓存

就是在Bean生成流程中保存Bean对象三种形态的三个Map集合,如下:

// 一级缓存Map 存放完整的Bean(流程跑完的)
private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);// 二级缓存Map 存放不完整的Bean(只实例化完,还没属性赋值、初始化)
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap(16);// 三级缓存Map 存放一个Bean的lambda表达式(也是刚实例化完)
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16);

用来解决什么问题?

这个大家应该熟知了,就是循环依赖

什么是循环依赖?

就像下面这样,AService 中注入了BService ,而BService 中又注入了AService ,这就是循环依赖

@Service
public class AService {@Resourceprivate BService bService;
}@Service
public class BService {@Resourceprivate AService aService;
}

二、Bean的加载源码

我们先通过getBean()流程图,来了解Spring的getBean()方法的工作流程,接着根据这个工作流程一步一步的阅读源码
在这里插入图片描述

//在spring中我们平时用到的getbean()这个方法实际上是调用的AbstractBeanFactory这个抽象工厂中得getbean方法
public Object getBean(String name) throws BeansException {
//看源码 我们首先看其返回值   如下返回得是doGetBean这个方法return this.doGetBean(name, null, null, false);}
//接下来我们看这个doGetBean这个方法
protected <T> T doGetBean(
final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly) throws BeansException {//获取name对应的真正beanName
//有这么几种情况 传入得参数有可能是某个参数得别名,也有可能是FactoryBean的name  //根据具体得实例去解析最终的得name
final String beanName = this.transformedBeanName(name);Object bean;//   在创建单例bean的时候会存在依赖注入的情况,而在创建依赖的时候为了避免循环依赖//  Spring创建bean的原则是不等bean创建完成就会将创建bean的ObjectFactory提前曝光(将对应的ObjectFactory加入到缓存)//   一旦下一个bean创建需要依赖上一个bean,则直接使用ObjectFactory对象// 获取单例Object sharedInstance = this.getSingleton(beanName);if (sharedInstance != null && args == null) {// 实例已经存在if (logger.isDebugEnabled()) {if (this.isSingletonCurrentlyInCreation(beanName)) {logger.debug("Returning eagerly cached instance of singleton bean '" + beanName + "' that is not fully initialized yet - a consequence of a circular reference");} else {logger.debug("Returning cached instance of singleton bean '" + beanName + "'");}}// 返回对应的实例bean = this.getObjectForBeanInstance(sharedInstance, name, beanName, null);} else {// 单例实例不存在if (this.isPrototypeCurrentlyInCreation(beanName)) {// 只有在单例模式下才会尝试解决循环依赖问题// 对于原型模式,如果存在循环依赖,也就是满足this.isPrototypeCurrentlyInCreation(beanName),抛出异常throw new BeanCurrentlyInCreationException(beanName);}// 获取parentBeanFactory实例BeanFactory parentBeanFactory = this.getParentBeanFactory();// 如果在beanDefinitionMap中(即所有已经加载的类中)不包含目标bean,则尝试从parentBeanFactory中获取if (parentBeanFactory != null && !this.containsBeanDefinition(beanName)) {String nameToLookup = this.originalBeanName(name);  // 获取name对应的真正beanName,如果是factoryBean,则加上“&”前缀if (args != null) {// 递归到BeanFactory中寻找return (T) parentBeanFactory.getBean(nameToLookup, args);} else {return parentBeanFactory.getBean(nameToLookup, requiredType);}}// 如果不仅仅是做类型检查,标记bean的状态已经创建,即将beanName加入alreadyCreated集合中if (!typeCheckOnly) {this.markBeanAsCreated(beanName);}try {//将存储XML配置的GenericBeanDefinition实例转换成RootBeanDefinition实例,方便后续处理// 如果存在父bean,则同时合并父bean的相关属性final RootBeanDefinition mbd = this.getMergedLocalBeanDefinition(beanName);// 检查bean是否是抽象的,如果是则抛出异常this.checkMergedBeanDefinition(mbd, beanName, args);// 加载当前bean依赖的beanString[] dependsOn = mbd.getDependsOn();if (dependsOn != null) {// 存在依赖,递归实例化依赖的beanfor (String dep : dependsOn) {if (this.isDependent(beanName, dep)) {// 检查dep是否依赖beanName,从而导致循环依赖throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Circular depends-on relationship between '" + beanName + "' and '" + dep + "'");}// 缓存依赖调用this.registerDependentBean(dep, beanName);this.getBean(dep);}}// 完成加载依赖的bean后,实例化mbd自身if (mbd.isSingleton()) {// scope == singletonsharedInstance = this.getSingleton(beanName, new ObjectFactory<Object>() {
@Override
public Object getObject() throws BeansException {try {return createBean(beanName, mbd, args);} catch (BeansException ex) {// 清理工作,从单例缓存中移除destroySingleton(beanName);throw ex;}}});bean = this.getObjectForBeanInstance(sharedInstance, name, beanName, mbd);} else if (mbd.isPrototype()) {// scope == prototypeObject prototypeInstance;try {// 设置正在创建的状态this.beforePrototypeCreation(beanName);// 创建beanprototypeInstance = this.createBean(beanName, mbd, args);} finally {this.afterPrototypeCreation(beanName);}// 返回对应的实例bean = this.getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);} else {// 其它范围得实例String scopeName = mbd.getScope();
final Scope scope = this.scopes.get(scopeName);if (scope == null) {throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'");}try {Object scopedInstance = scope.get(beanName, new ObjectFactory<Object>() {@Overridepublic Object getObject() throws BeansException {beforePrototypeCreation(beanName);try {return createBean(beanName, mbd, args);} finally {afterPrototypeCreation(beanName);}}});// 返回对应的实例bean = this.getObjectForBeanInstance(scopedInstance, name, beanName, mbd);} catch (IllegalStateException ex) {throw new BeanCreationException(beanName, "Scope '" + scopeName + "' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton", ex);}}} catch (BeansException ex) {cleanupAfterBeanCreationFailure(beanName);throw ex;}}// 检查需要的类型是否符合bean的实际类型,对应getBean时指定的需要类型if (requiredType != null && bean != null && !requiredType.isAssignableFrom(bean.getClass())) {try {// 执行类型转换,转换成对应的类型return this.getTypeConverter().convertIfNecessary(bean, requiredType);} catch (TypeMismatchException ex) {if (logger.isDebugEnabled()) {logger.debug("Failed to convert bean '" + name + "' to required type '" + ClassUtils.getQualifiedName(requiredType) + "'", ex);}throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());}}到这里才是真正意义上返回一个bean而已return (T) bean;}

三、三级缓存详解

不管你了不了解源码,我们先看一下Bean的生成流程,看看三级缓存是在什么地方有调用,就三个地方:

  1. Bean实例化前会先查询缓存,判断Bean是否已经存在
  2. Bean属性赋值前会先向三级缓存中放入一个lambda表达式,该表达式执行则会生成一个半成品Bean放入二级缓存
  3. Bean初始化完成后将完整的Bean放入一级缓存,同时清空二、三级缓存

接下来我们一个一个看!
在这里插入图片描述

3.1 Bean实例化前

AbstractBeanFactory.doGetBean

Bean实例化前会从缓存里面获取Bean,防止重复实例化
在这里插入图片描述

DefaultSingletonBeanRegistry.getSingleton(String beanName, boolean allowEarlyReference)

我们看看这个获取的方法逻辑:

  • 从一级缓存获取,获取到了,则返回
  • 从二级缓存获取,获取到了,则返回
  • 从三级缓存获取,获取到了,则执行三级缓存中的lambda表达式,将结果放入二级缓存,清除三级缓存
public Object getSingleton(String beanName) {return this.getSingleton(beanName, true);
}@Nullable
protected Object getSingleton(String beanName, boolean allowEarlyReference) {// 从一级缓存中获取Bean 获取到了则返回 没获取到继续Object singletonObject = this.singletonObjects.get(beanName);if (singletonObject == null && this.isSingletonCurrentlyInCreation(beanName)) {// 从二级缓存中获取Bean  获取到了则返回 没获取到则继续singletonObject = this.earlySingletonObjects.get(beanName);if (singletonObject == null && allowEarlyReference) {// 加一把锁防止 线程安全 双重获取校验synchronized(this.singletonObjects) {// 从一级缓存中获取Bean 获取到了则返回 没获取到继续singletonObject = this.singletonObjects.get(beanName);if (singletonObject == null) {// 从二级缓存中获取Bean  获取到了则返回 没获取到则继续singletonObject = this.earlySingletonObjects.get(beanName);if (singletonObject == null) {// 从三级缓存中获取 没获取到则返回ObjectFactory<?> singletonFactory = (ObjectFactory)this.singletonFactories.get(beanName);if (singletonFactory != null) {// 获取到了 执行三级缓存中的lambda表达式singletonObject = singletonFactory.getObject();// 并将结果放入二级缓存this.earlySingletonObjects.put(beanName, singletonObject);// 从三级缓存中移除this.singletonFactories.remove(beanName);}}}}}}return singletonObject;
}

3.2 属性赋值/注入前

AbstractAutowireCapableBeanFactory.doCreateBean
在这里插入图片描述
DefaultSingletonBeanRegistry.addSingletonFactory

这里就是将一个lambda表达式放入了三级缓存,我们需要去看一下这个表达式是干什么的!!

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {Assert.notNull(singletonFactory, "Singleton factory must not be null");synchronized(this.singletonObjects) {// 一级缓存中不存在的话 if (!this.singletonObjects.containsKey(beanName)) {// 将lambda表达式放入三级缓存this.singletonFactories.put(beanName, singletonFactory);// 清除二级缓存 this.earlySingletonObjects.remove(beanName);this.registeredSingletons.add(beanName);}}
}

AbstractAutowireCapableBeanFactory.getEarlyBeanReference

该方法说白了就是会判断该Bean是否需要被动态代理,两种返回结果:

  • 不需要代理,返回未属性注入、未初始化的半成品Bean
  • 需要代理,返回未属性注入、未初始化的半成品Bean的代理对象
protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {Object exposedObject = bean;if (!mbd.isSynthetic() && this.hasInstantiationAwareBeanPostProcessors()) {Iterator var5 = this.getBeanPostProcessors().iterator();// 遍历后置处理器while(var5.hasNext()) {BeanPostProcessor bp = (BeanPostProcessor)var5.next();// 找到实现SmartInstantiationAwareBeanPostProcessor接口的// 该接口getEarlyBeanReference方法什么时候会执行?// AOP动态代理的时候 该方法执行就是判断该Bean是否需要被代理// 需要代理则会创建代理对象返回if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor)bp;exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);}}}// 这个Object有两种情况,一是实例化后的半成品Bean,二是半成品Bean动态代理后的代理对象return exposedObject;
}

注意:这里只是把lambda表达式放入了三级缓存,如果不从三级缓存中获取,这个表达式是不执行的,一旦执行了,就会把半成品Bean或者半成品Bean的代理对象放入二级缓存中了

3.3初始化后

AbstractBeanFactory.doGetBean

这里注意啊,这个getSingleton方法传参传了个lambda表达式,这个表达式内部就是Bean的实例化过程,初始化完成后,是要需要执行这个getSingleton方法的
在这里插入图片描述
DefaultSingletonBeanRegistry.getSingleton(beanName, singletonFactory)

这个方法与上面那个不一样,重载了

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {synchronized(this.singletonObjects) {// 第一次进来这里获取肯定为nullObject singletonObject = this.singletonObjects.get(beanName);if (singletonObject == null) {// 省略................try {// 注意啊,这个就是执行外面那个传参的lambda表达式// 所以这里才会跳到createBean方法那里去执行singletonObject = singletonFactory.getObject();newSingleton = true;} // 省略................finally {if (recordSuppressedExceptions) {this.suppressedExceptions = null;}this.afterSingletonCreation(beanName);}// 到了这说明Bean创建完了if (newSingleton) {// 这里就会把Bean放入一级缓存中了 同时清除二、三级缓存this.addSingleton(beanName, singletonObject);}}return singletonObject;}}

DefaultSingletonBeanRegistry.addSingleton

protected void addSingleton(String beanName, Object singletonObject) {synchronized(this.singletonObjects) {// 放入一级缓存  this.singletonObjects.put(beanName, singletonObject);// 清除二、三级缓存this.singletonFactories.remove(beanName);this.earlySingletonObjects.remove(beanName);this.registeredSingletons.add(beanName);}
}

总结

整个过程就三个地方跟缓存有关,我们假设现在要实例化A这个Bean,看看缓存是怎么变化的:

  • 实例化前,获取缓存判断(三个缓存中肯定没有A,获取为null,进入实例化流程)
  • 实例化完成,属性注入前(往三级缓存中放入了一个lambda表达式,一、二级为null)
  • 初始化完成(将A这个Bean放入一级缓存,清除二、三级缓存)

以上则是单个Bean生成过程中缓存的变化!!

四、怎么解决的循环依赖

上面我们把Bean流程中利用缓存的三个重要的点都找出来了,也分析了会带来什么变化,接下来看看是怎么解决的循环依赖,我们看个图就懂了:

以A注入B,B注入A为例:

A属性注入前就把lambda表达式放入了第三级缓存,所以B再注入A的时候会从第三级缓存中找到A的lambda表达式并执行,然后将半成品Bean放入第二级缓存,所以此时B注入的只是半成品的A对象,B创建完成后返回给A注入,A继续初始化,完成创建。

注意: B注入的半成品A对象只是一个引用,所以之后A初始化完成后,B这个注入的A就随之变成了完整的A
在这里插入图片描述
从上述看第三级缓存是用来提前暴露Bean对象引用的,所以解决了循环依赖,但是第二级缓存的这个半成品Bean对象干嘛的呢?

假设A同时注入了B和C,B和C又都注入了A,这时A注入B,实例化B的过程和上述是一样的,但随后还会注入C,那这个C在注入A的时候还会有第三级缓存用吗?没了吧,所以它就只能用第二级缓存的半成品Bean对象了,同样也是引用而已

五、不用三级缓存不行吗

可能很多小伙伴得到的答案就是不行,而且答案是因为不确定这个Bean是不是代理对象,所以搞了个lambda表达式?答案真的是这样吗??

我们分析一下:AOP动态代理在没有循环依赖的时候是在哪里执行的?Bean初始化后!有循环依赖的时候是在属性赋值前,中间就间隔了一个属性注入对吧,没错,在属性注入的时候注入的是原始对象的引用还是代理对象的引用这个很重要,但是属性注入会影响AOP的结果吗?是否AOP创建代理对象和切面有关,和属性注入无关,所以我们完全可以在属性注入之前就知道这个Bean是代理对象还是非代理对象,就像下面这样,我不将表达式放入第三级缓存了,而是直接执行,将结果放入第二级缓存
在这里插入图片描述
这样可不可以?可以吧,这样用二级缓存就解决了,但是在一个对象没有属性赋值、初始化前就创建代理对象是有风险的!像这么做不管有没有产生循环依赖,只要有AOP动态代理对象的产生就有一分风险,这么做是得不偿失的,所以有了三级缓存,三级缓存是只有在循环依赖以及AOP动态代理同时产生时才会有风险。可以说是因为存在循环依赖所以被迫的导致Bean对象提前的暴露了引用!!! 所以这下懂了吧

至于为什么多例、构造器注入这两种情况解决不了循环依赖就很简单了:

循环依赖的解决原理是在对象实例化后提前暴露了引用,而这两种情况都还没实例化呢

六、总结

  • 一级缓存:用于存储被完整创建了的bean。也就是完成了初始化之后,可以直接被其他对象使用的bean。
  • 二级缓存:用于存储半成品的Bean。也就是刚实例化但是还没有进行初始化的Bean
  • 三级缓存:三级缓存存储的是工厂对象(lambda表达式)。工厂对象可以产生Bean对象提前暴露的引用(半成品的Bean或者半成品的代理Bean对象),执行这个lambda表达式,就会将引用放入二级缓存中

经过以上的分析,现在应该懂了吧:

循环依赖是否一定需要三级缓存来解决? 不一定,但三级缓存会更合适,风险更小

二级缓存能否解决循环依赖? 可以,但风险比三级缓存更大

第二级缓存用来干嘛的? 存放半成品的引用,可能产生多对象循环依赖,第三级缓存产生引用后,后续的就可以直接注入该引用

多例、构造器注入为什么不能解决循环依赖? 因为循环依赖的原理的实例化后提前暴露的引用,这两种情况还没实例化

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

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

相关文章

USB3.2 摘录(九)

系列文章目录 USB3.2 摘录&#xff08;一&#xff09; USB3.2 摘录&#xff08;二&#xff09; USB3.2 摘录&#xff08;三&#xff09; USB3.2 摘录&#xff08;四&#xff09; USB3.2 摘录&#xff08;五&#xff09; USB3.2 摘录&#xff08;六&#xff09; USB3.2 摘录&…

页面设计任务 商品详情页(带评论区)

目录 效果图&#xff1a; 任务描述 源码&#xff1a; 详细讲解&#xff1a; 1.产品信息部分 2.用户评论区域 效果图&#xff1a; 任务描述 页面结构: 页面应包括一个标题部分、一个产品展示区和一个客户评价区。使用图片展示产品&#xff0c;并添加描述。客户评价区展示一…

IP代理池学习记录

免责声明 本文仅供学习和研究目的使用。所提供的信息和技术仅限于合规和合法的使用场景。请读者在应用相关技术时遵守法律法规&#xff0c;尊重他人的数据隐私和网站使用条款。本文作者对因使用本文信息而产生的任何法律责任或损失不承担责任。 1、初识IP代理池 概述&#xff…

如何使用ssm实现基于SSM的社区物业管理系统的设计与实现+vue

TOC ssm223基于SSM的社区物业管理系统的设计与实现vue 绪论 1.1 研究背景 现在大家正处于互联网加的时代&#xff0c;这个时代它就是一个信息内容无比丰富&#xff0c;信息处理与管理变得越加高效的网络化的时代&#xff0c;这个时代让大家的生活不仅变得更加地便利化&…

Redis—缓存机制

Redis 缓存机制 1. 缓存三兄弟1.1 缓存击穿1.2 缓存穿透1.3 缓存雪崩 2. 布隆过滤器3. 缓存和数据库数据一致性3.1 缓存更新策略3.2 缓存不一致处理 4. 热点 key4.1 热点 key 处理4.2 热点 key 重建 5. 缓存预热 Redis&#xff0c;一个轻量级的开源内存数据结构存储系统&#x…

Redis计数器:数字的秘密

文章目录 Redis计数器incr 指令用户计数统计用户统计信息查询缓存一致性 小结 技术派项目源码地址 : Gitee :技术派 - https://gitee.com/itwanger/paicodingGithub :技术派 - https://github.com/itwanger/paicoding 用户的相关统计信息 文章数&#xff0c;文章总阅读数&am…

go设计模式——单例模式

概念 单例是一种创建型设计模式&#xff0c;它确保一个类在整个程序运行期间只有一个实例&#xff0c;并提供一个全局访问点来使用该实例。虽然单例模式在某些情况下非常有用&#xff0c;例如管理全局配置、日志记录或资源共享&#xff0c;但它也带来了与全局变量相似的问题。…

redis面试(二十三)写锁释放

先加了写锁&#xff0c;后面再次加写锁或者读锁 anyLock: { “mode”: “write”, “UUID_01:threadId_01:write”: 2, “UUID_01:threadId_01”: 1 } 写锁的释放lua脚本在这里 RedissonWriteLock.unlockInnerAsync() 比如说现在的参数是这 KEYS[1] anyLock KEYS[2] redi…

SQL手工注入漏洞测试(MongoDB数据库)靶场通关攻略

构造数据回显 });return ({title:1,content:2 成功回显1,2&#xff0c;接下来我们开始尝试查询数据库 });return({title:tojson(db),content:2 得到之后我们就可以继续查询他的表名了 });return({title:tojson(db.getCollectionNames()),content:2 最后我们就可以爆出他表里的数…

宝塔面板配置FTP服务并安装内网穿透实现无公网IP远程连接

文章目录 前言1. Linux安装Cpolar2. 创建FTP公网地址3. 宝塔FTP服务设置4. FTP服务远程连接小结 5. 固定FTP公网地址6. 固定FTP地址连接 前言 本文主要介绍宝塔FTP文件传输服务如何搭配内网穿透工具&#xff0c;实现随时随地远程连接局域网环境搭建的宝塔FTP文件服务并进行文件…

ssrf实现.SSH未创建写shell

一、介绍SSRF漏洞 SSRF (Server-Side Request Forgery,服务器端请求伪造)是一种由攻击者构造请求&#xff0c;由服务端发起请求的安全漏洞。一般情况下&#xff0c;SSRF攻击的目标是外网无法访问的内部系统(正因为请求是由服务端发起的&#xff0c;所以服务端能请求到与自身相…

C语言基础——函数详解

目录 函数的概述 1 函数的概念 2 函数的意义 函数的定义和使用 1 函数的定义 2 函数的调用 2.1 在同一文件中函数定义后函数调用 2.2 在同一文件中函数定义前函数调用 2.3 调用其它文件中定义的函数 2.3.1 在函数调用文件中进行声明 2.3.2 在头文件中进行函数的声明 函…

图片工具箱:一键批量加水印,守护创意,提升效率!

前言 你是否曾在处理海量图片时&#xff0c;被繁琐的步骤和漫长的等待时间折磨得苦不堪言&#xff1f;是否梦想过拥有一款神器&#xff0c;能让你的图片处理工作变得轻松愉快&#xff0c;从此告别加班的烦恼&#xff0c;迎接升职加薪的曙光&#xff1f;那么&#xff0c;让我向…

有限差分学习笔记

有限差分介绍 ​ 在数学中&#xff0c;有限差分法&#xff08;finite-difference methods&#xff0c;简称FDM&#xff09;&#xff0c;是一种微分方程数值方法&#xff0c;是通过有限差分来近似导数&#xff0c;从而寻求微分方程的近似解。 由泰勒展开式的推导 显式方…

Web应用加密数据传输方案

目录 概述 最初的方案 改进后的方案 秘钥的过期时间 概述 介于公司最近发布了一个面向C端用户的Web系统&#xff0c;为防止前端调用后端的API接口时&#xff0c;数据传输的内容轻易的被黑客获取&#xff0c;而设计的一个前后端数据加密传输方案 最初的方案 在最开始&#xf…

2 种方式申请免费 SSL 证书,阿里云 Certbot

如何使用免费的 SSL 证书&#xff0c;有时在项目中需要使用免费的 SSL 证书&#xff0c;Aliyun 提供免费证书&#xff0c;三个月有效期&#xff0c;可以直接在aliyun 申请&#xff0c;搜索 SSL 证书&#xff0c;选择测试证书。 Aliyun 证书需要每三月来来换一次&#xff0c;页…

<数据集>车内视角行人识别数据集<目标检测>

数据集格式&#xff1a;VOCYOLO格式 图片数量&#xff1a;6470张 标注数量(xml文件个数)&#xff1a;6470 标注数量(txt文件个数)&#xff1a;6470 标注类别数&#xff1a;1 标注类别名称&#xff1a;[pedestrian] 序号类别名称图片数框数1pedestrian647029587 使用标注…

奔驰S迈巴赫S480升级动态按摩座椅效果怎么样

在迈巴赫 S480 的尊崇之旅中&#xff0c;舒适从未有尽头。现在&#xff0c;为您呈现前排动态按摩座椅的升级&#xff0c;将舒适体验提升至全新境界。 迈巴赫 S480 已然是舒适的代名词&#xff0c;但前排动态按摩座椅的升级&#xff0c;将为您带来前所未有的放松与享受。 当您…

网络UDP报文详细解析

目录 一、简介二、详细介绍三、其他相关链接1、TCP报文段的详细图总结2、TCP三次握手和四次挥手详解3、socket通信原理及相关函数详细总结4、网络包IP首部详细解析 一、简介 本文主要介绍UDP报文格式。 二、详细介绍 UDP是一种无连接、不可靠的用户数据报协议&#xff0c;其…

【注解】反序列化时匹配多个 JSON 属性名 @JsonAlias 详解

JsonAlias 注解是 Jackson 提供的一个功能强大的注解&#xff0c;允许一个字段在反序列化时匹配多个 JSON 属性名。它适用于在处理多种输入数据格式时&#xff0c;或当 JSON 数据的键名可能变化时。 一、JsonAlias 的作用 多种别名&#xff1a;JsonAlias 允许你为一个字段定义…