被问麻了,Spring如何处理循环依赖?
前言
Spring如何处理循环依赖?这是最近较为频繁被问到的一个面试题,在前面Bean实例化流程中,对属性注入一文多多少少对循环依赖有过介绍,这篇文章详细讲一下Spring中的循环依赖的处理方案。 什么是循环依赖
依赖指的是Bean与Bean之间的依赖关系,循环依赖指的是两个或者多个Bean相互依赖,如:
构造器循环依赖
代码示例: public class BeanA { private BeanB beanB; public BeanA(BeanB beanB){ this.beanB = beanB; } } public class BeanB { private BeanA beanA; public BeanB(BeanA beanA){ this.beanA = beanA; } }
配置文件 Setter循环依赖
代码示例 public class BeanA { private BeanB beanB; public void setBeanB(BeanB beanB){ this.beanB = beanB; } } @Data public class BeanB { private BeanA beanA; public void setBeanA(BeanA beanA){ this.beanA = beanA; } }
配置文件
循环依赖包括: 构造器注入循环依赖 set , 注入循环依赖 和 prototype模式Bean的循环依赖。Spring只解决了单例Bean的 setter 注入循环依赖,对于构造器循环依赖,和 prototype模式的循环依赖是无法解决的,在创建Bean的时候就会抛出异常 :" BeanCurrentlyInCreationException " ,
循环依赖控制开关在 AbstractRefreshableApplicationContext 容器工厂类中有定义:public abstract class AbstractRefreshableApplicationContext extends AbstractApplicationContext { @Nullable private Boolean allowBeanDefinitionOverriding; //是否允许循环依赖 @Nullable private Boolean allowCircularReferences; //设置循环依赖 public void setAllowCircularReferences(boolean allowCircularReferences) { this.allowCircularReferences = allowCircularReferences; }
默认情况下是允许Bean之间的循环依赖的,在依赖注入时Spring会尝试处理循环依赖。如果将该属性配置为"false"则关闭循环依赖,当在Bean依赖注入的时遇到循环依赖时抛出异常。可以通过如下方式关闭,但是一般都不这么做 ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("bean.xml"); //禁用循环依赖 applicationContext.setAllowCircularReferences(false); //刷新容器 applicationContext.refresh(); ...构造器循环依赖处理
构造器是不允许循环依赖的,动动你的小脑瓜想一想,比如:A 依赖 B ,B依赖C,C依赖A,在实例化A的时候,构造器需要注入B,然后Spirng会实例化B,此时的A属于"正在创建"的状态。当实例化B的时候,发现构造器需要注入C,然后去实例化C,然而实例化C的时候又需要注入A的实例,这样就造成了一个死循环,永远无法先实例化出某一个Bean,所以Spring遇到这里构造器循环依赖会直接抛出异常。 那么Spring到底是如何做的呢?首先Spring会走Bean的实例化流程尝试创建 A 的实例 ,在创建实例之间先从 "正在创建Bean池" (一个缓存Map而已)中去查找A 是否正在创建,如果没找到,则将 A 放入 "正在创建Bean池"中,然后准备实例化构造器参数 B。 Spring会走Bean的实例化流程尝试创建 B 的实例 ,在创建实例之间先从 "正在创建Bean池" (一个缓存Map而已)中去查找B 是否正在创建,如果没找到,则将 B 放入 "正在创建Bean池"中,然后准备实例化构造器参数 A。 Spring会走Bean的实例化流程尝试创建 A 的实例 ,在创建实例之间先从 "正在创建Bean池" (一个缓存Map而已)中去查找A 是否正在创建。 此时:Spring发现 A 正处于"正在创建Bean池",表示出现构造器循环依赖,抛出异常:" BeanCurrentlyInCreationException "DefaultSingletonBeanRegistry#getSingleton
下面我们以 BeanA 构造参数依赖BeanB, BeanB 构造参数依赖BeanA 为例来分析。
当Spring的IOC容器启动,尝试对单利的BeanA进行初始化,根据之前的分析我们知道,单利Bean的创建入口是 AbstractBeanFactory#doGetBean 在该方法中会先从单利Bean缓存中获取,如果没有代码会走到:DefaultSingletonBeanRegistry#getSingleton(jString beanName, ObjectFactory<?> singletonFactory) 方法中 ,在该方法中会先对把创建的Bean加入 一个名字为 singletonsCurrentlyInCreation 的 ConcurrentHashMap中 ,意思是该Bean正在创建中,然后调用 ObjectFactory.getObject() 实例化Bean , 假设 BeanA 进入了该方法进行实例化://正在创建中的Bean private final Set singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16)); public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) { ...省略... //把该Bean的名字加入 singletonsCurrentlyInCreation 正在创建池 中 beforeSingletonCreation(beanName); boolean newSingleton = false; boolean recordSuppressedExceptions = (this.suppressedExceptions == null); if (recordSuppressedExceptions) { this.suppressedExceptions = new LinkedHashSet<>(); } try { //调用ObjectFactory创建Bean的实例 singletonObject = singletonFactory.getObject(); newSingleton = true; } ...省略... //如果singletonsCurrentlyInCreation中没该Bean,就把该Bean存储到singletonsCurrentlyInCreation中, //如果 singletonsCurrentlyInCreation 中有 该Bean,就报错循环依赖异常BeanCurrentlyInCreationException //也就意味着同一个beanName进入该方法2次就会抛异常 protected void beforeSingletonCreation(String beanName) { if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) { throw new BeanCurrentlyInCreationException(beanName); } }
beforeSingletonCreation 方法非常关键 ,它会把beanName加入 singletonsCurrentlyInCreation ,一个代表"正在创建中的Bean"的ConcurrentHashMap 中。
如果 singletonsCurrentlyInCreation 中没该beanName,就把该Bean存储到singletonsCurrentlyInCreation 中, 如果 singletonsCurrentlyInCreation 中有 该Bean,就报错循环依赖异常BeanCurrentlyInCreationException
【注意】也就意味着同一个beanName进入该方法2次就会抛异常 , 现在BeanA已经加入了 singletonsCurrentlyInCreation AbstractAutowireCapableBeanFactory#autowireConstructor
我们前面分析过 ObjectFactory.getObject 实例化Bean的详细流程,这里我只是大概在复盘一下就行了。因为我们的BeanA的构造器注入了一个BeanB,所以 代码最终会走到AbstractAutowireCapableBeanFactory#autowireConstructor ,通过构造器来实例化BeanA(在属性注入那一章有讲到 ) 。
在 autowireConstructor 方法中会通过 ConstructorResolver#resolveConstructorArguments 来解析构造参数,调用 BeanDefinitionValueResolver 去把 ref="beanB" 这种字符串的引用变成一个实实在在的Bean,即BeanB,所以在 BeanDefinitionValueResolver 属性值解析器中又会去实例化BeanB,同样会走到 DefaultSingletonBeanRegistry#getSingleton 中把BeanB加入 singletonsCurrentlyInCreation "正在创建Bean池"中,然后调用ObjectFactory.getObject 实例化BeanB。
低于BeanB而已同样需要通过构造器创建,BeanB构造器参数依赖了BeanA,也就意味着又会调用 BeanDefinitionValueResolver 去把 ref="beanA" 这种字符串引用变成容器中的BeanA的Bean实例,然后代码又会走到 DefaultSingletonBeanRegistry#getSingleton 。然后再一次的尝试把BeanA加入singletonsCurrentlyInCreation "正在创建Bean池"。
此时问题就来了,在最开始创建BeanA的时候它已经加入过一次"正在创建Bean" 池,这会儿实例化BeanB的时候,由于构造器参数依赖了BeanA,导致BeanA又想进入"正在创建Bean" 池 ,此时 Spring抛出循环依赖异常:
Error creating bean with name ‘beanA’: Requested bean is currently in creation: Is there an unresolvable circular reference?
到这,Spring处理构造器循环依赖的源码分析完毕。 setter循环依赖处理
setter循环依赖是可以允许的。Spring是通过提前暴露未实例化完成的Bean的 ObjectFactory 来实现循环依赖的,这样做的目的是其他的Bean可以通过 ObjectFactory 引用到该Bean。
实现流程如下: Spring创建BeanA,通过无参构造实例化,把BeanA添加到"正在创建Bean池"中,并暴露当前实例的 ObjectFactory ,即把ObjectFactory 添加到singletonFactories (三级缓存)中,该ObjectFactory 用来获取创建中的BeanA,然后,然后通过setter注入BeanBSpring创建BeanB,通过无参构造实例化,把BeanB添加到"正在创建Bean池"中,并暴露一个 ObjectFactory ,然后,然后通过setter注入BeanA在BeanB通过setter注入BeanA时,由于BeanA 提前暴露了 ObjectFactory ,通过它返回一个提前暴露一个创建中的BeanA。然后完成BeanB的依赖注入
这里补张图:
获取Bean的时候走三级缓存 protected Object getSingleton(String beanName, boolean allowEarlyReference) { //一级缓存,存储实例化好的Bean Object singletonObject = this.singletonObjects.get(beanName); //如果单利缓存池中没有,但是beanName正在创建 if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { synchronized (this.singletonObjects) { //获取二级缓存,这个里面存储的是正在创建的Bean,半成品 singletonObject = this.earlySingletonObjects.get(beanName); //如果也为空,但是允许循环依赖 if (singletonObject == null && allowEarlyReference) { //从三级缓存获取Bean的创建工厂, ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { //创建Bean的实例 singletonObject = singletonFactory.getObject(); //把Bean存储到二级缓存 this.earlySingletonObjects.put(beanName, singletonObject); //移除三级缓存中的创建工厂 this.singletonFactories.remove(beanName); } } } } return (singletonObject != NULL_OBJECT ? singletonObject : null); }AbstractAutowireCapableBeanFactory#doCreateBean
我们以BeanA 通过settter依赖BeanB,BeanB通过setter 依赖BeanA为例来分析一下源码,在之前的Bean实例化流程分析过程中我们了解到,Bean的实例化会走 AbstractBeanFactory#doGetBean ,然后查找单利缓存中是否有该Bean ,如果没有就调用 DefaultSingletonBeanRegistry#getSingleton ,方法会把BeanA加入 singletonsCurrentlyInCreation "创建中的Bean池",然后调用ObjectFactory.getObject 创建Bean.
org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean 源码:protected T doGetBean(final String name, @Nullable final Class requiredType, @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException { final String beanName = transformedBeanName(name); Object bean; // Eagerly check singleton cache for manually registered singletons. //缓存中获取Bean,解决了循环依赖问题 Object sharedInstance = getSingleton(beanName); ...缓存中没有走下面... if (mbd.isSingleton()) { //走 DefaultSingletonBeanRegistry#getSingleton ,方法会把bean加入"正在创建bean池" //然后调用ObjectFactory实例化Bean sharedInstance = getSingleton(beanName, () -> { try { return createBean(beanName, mbd, args); } catch (BeansException ex) { // Explicitly remove instance from singleton cache: It might have been put there // eagerly by the creation process, to allow for circular reference resolution. // Also remove any beans that received a temporary reference to the bean. destroySingleton(beanName); throw ex; } }); bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); }
第一次进来,缓存中是没有BeanA的,所有会走 getSingleton 方法,然后代码最终会走到 AbstractAutowireCapableBeanFactory#doCreateBean 方法中 。
AbstractAutowireCapableBeanFactory#doCreateBean 源码:protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) throws BeanCreationException { // Instantiate the bean. BeanWrapper instanceWrapper = null; if (mbd.isSingleton()) { instanceWrapper = this.factoryBeanInstanceCache.remove(beanName); } if (instanceWrapper == null) { //实例化Bean instanceWrapper = createBeanInstance(beanName, mbd, args); } ...省略... //如果是单利 ,如果是允许循环依赖,如果 beanName 出于创建中,已经被添加到"创建中的bean池" boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName)); if (earlySingletonExposure) { if (logger.isDebugEnabled()) { logger.debug("Eagerly caching bean "" + beanName + "" to allow for resolving potential circular references"); } //把ObjectFactory 添加到 singletonFactories 中。 addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); } try { //走依赖注入流程 populateBean(beanName, mbd, instanceWrapper); exposedObject = initializeBean(beanName, exposedObject, mbd); } //缓存单利Bean的创建工厂,用于解决循环依赖 protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) { Assert.notNull(singletonFactory, "Singleton factory must not be null"); synchronized (this.singletonObjects) { //singletonObjects单利缓存中是否包含Bean if (!this.singletonObjects.containsKey(beanName)) { //提前暴露ObjectFactory,把ObjectFactory放到singletonFactories中, //后面解决循环依赖,获取Bean实例的时候会用到 this.singletonFactories.put(beanName, singletonFactory); //早期单利bean缓存中移除Bean this.earlySingletonObjects.remove(beanName); //把注册的Bean加入registeredSingletons中 this.registeredSingletons.add(beanName); } } }
该方法中把BeanA实例化好之后,会把ObjectFactory存储到一个 singletonFactories (HashMap)中来提前暴露Bean的创建工厂,用于解决循环依赖【重要】,然后调用 populateBean 走属性注入流程。
属性注入会通过BeanDefinition得到bean的依赖属性,然后调用 AbstractAutowireCapableBeanFactory#applyPropertyValues ,把属性应用到对象上。在applyPropertyValues 方法中最终调用 BeanDefinitionValueResolver#resolveValueIfNecessary 解析属性值,比如:ref="beanB" 这种字符串引用变成 对象实例的引用。
在 BeanDefinitionValueResolver 解析依赖的属性值即:BeanB的时候,同样会触发BeanB的实例化,代码会走到AbstractBeanFactory#doGetBean ,然后走方法 DefaultSingletonBeanRegistry#getSingleton 中把BeanB加入 singletonsCurrentlyInCreation "创建中的Bean池",然后代码会走到AbstractAutowireCapableBeanFactory#doCreateBean 方法中创建BeanB,
该方法中会先实例化BeanB,接着会把BeanB的 ObjectFactory 存储到 singletonFactories (HashMap) 中来提前暴露Bean的创建工厂,用于解决循环依赖,然后调用 populateBean 走属性注入流程。
同样因为BeanB通过Setter 注入了 A,所以在 populateBean 属性注入流程中会解析 ref="beanA" 为容器中的 BeanA 的实例。
然后会走到 AbstractBeanFactory#doGetBean 中获取BeanA的实例。这个时候流程就不一样了,我们先看一下 AbstractBeanFactory#doGetBean 中的代码protected T doGetBean(final String name, @Nullable final Class requiredType, @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException { final String beanName = transformedBeanName(name); Object bean; // Eagerly check singleton cache for manually registered singletons. //从缓存中获取Bean Object sharedInstance = getSingleton(beanName); ...省略... //如果缓存中没有Bean,就创建Bean if (mbd.isSingleton()) { sharedInstance = getSingleton(beanName, () -> { try { return createBean(beanName, mbd, args); } catch (BeansException ex) { // Explicitly remove instance from singleton cache: It might have been put there // eagerly by the creation process, to allow for circular reference resolution. // Also remove any beans that received a temporary reference to the bean. destroySingleton(beanName); throw ex; } }); bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); }
在获取单利Bean的实例的时候是会先去单利Bean的缓存中去查看Bean是否已经存在,如果不存在,才会走 DefaultSingletonBeanRegistry#getSingleton 方法创建Bean。
问题是:此刻单利Bean缓存中已经有BeanA了,因为在最开始BeanA已经出于"正在创建Bean池"中了。我们先来看一下是如何从缓存获取Bean的。
DefaultSingletonBeanRegistry#getSingleton(java.lang.String, boolean) 源码如下://allowEarlyReference :是否创建早期应用,主要用来解决循环依赖 @Nullable protected Object getSingleton(String beanName, boolean allowEarlyReference) { // Quick check for existing instance without full singleton lock //从Map中 singletonObjects = new ConcurrentHashMap<>(256); 获取单利Bean //【一级缓存】singletonObject缓存中是否有Bean , 它存储的是已经实例化好的Bean Object singletonObject = this.singletonObjects.get(beanName); //如果singletonObjects中没有Bean,但是Bean出于正在创建池中,即:Set singletonsCurrentlyInCreation中有Bean, if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { //【二级缓存】从早期单例对象的缓存 earlySingletonObjects 中获取 singletonObject = this.earlySingletonObjects.get(beanName); //早期单利对象缓存中也没有,但是允许循环依赖 if (singletonObject == null && allowEarlyReference) { synchronized (this.singletonObjects) { // Consistent creation of early reference within full singleton lock singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null) { singletonObject = this.earlySingletonObjects.get(beanName); if (singletonObject == null) { //【三级缓存】获取ObjectFactory , 对象创建工厂,得到Bean创建过程中提前暴露的工厂。 ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName); if (singletonFactory != null) { //通过工厂ObjectFactory 获取对象实例 singletonObject = singletonFactory.getObject(); //把对象存储到早期缓存中 this.earlySingletonObjects.put(beanName, singletonObject); //把ObjectFactory移除 this.singletonFactories.remove(beanName); } } } } } } return singletonObject; }
这里就是经典的三级缓存解决Spring循环依赖。你看到了,这里会先从 singletonObjects 单利Bean缓存集合中获取Bean(该缓存是实例化完成了的Bean),如果没有,就从earlySingletonObjects 早期对象缓存中获取Bean(该缓存中存放的是还未实例化完成的早期Bean),如果还是没有,就从singletonFactories 中得到暴露的ObjectFactory 来获取依赖的Bean。然后放入早期缓存中。并把ObjectFactory 从singletonFactories 中移除。最后返回Bean的实例。
由于在实例化BeanA的时候已经把BeanA的 ObjectFactory 添加到了 singletonFactories 缓存中,那么这里就会走到 singletonFactory.getObject(); 方法得到BeanA的实例,并且会把BeanA存储到 earlySingletonObjects 早期单利Bean缓存中。
BeanA的实例成功返回,那么BeanB的 setter注入成功,代表BeanB实例化完成,那么BeanA的setter方法注入成功,BeanA实例化完成。 prototype模式的循环依赖
对于prototype模式下的Bean不允许循环依赖,因为 这种模式下Bean是不做缓存的,所以就没法暴露 ObjectFactory ,也就没办法实现循环依赖。总结
不知道你有没有看晕,反正我但是在源码时的过程是比较辛苦的,这里需要你对前面Bean的实例化流程和属性注入流程比较熟悉,否则就会晕菜。
这里总结一下:
构造器循环依赖是不允许的,主要通过 singletonsCurrentlyInCreation "正在创建Bean池" 把创建中的Bean缓存起来,如果循环依赖,同一个Bean势必会尝试进入该缓存2次,抛出循环依赖异常。
setter循环依赖是可以允许的。Spring是通过提前暴露未实例化完成的Bean的 ObjectFactory 来实现循环依赖的,这样做的目的是其他的Bean可以通过 ObjectFactory 引用到该Bean 。在获取依赖的Bean的时候使用到了三级缓存。下面的面试题你会答了吗?Spirng支持那种模式下的循环依赖(构造器?,setter?, prototype?) Spring是如何处理构造器注入循环依赖的? Spring是如何处理Setter注入循环依赖的?
第一次去上海城市沙滩每次出发都精彩!去沙滩玩,离自己家最近的地方就是启东,可我却去了上海市金山区的城市沙滩玩。买好了门票,进入景区,首先引入眼帘的是停靠在草坪地上的一架小飞机。飞机上刻着泊鹭通航B10
中国内地地铁开通48个城市城市名字首次通地铁运行线路正在建设线路北京1971年通地铁,24条,15条天津1984年通地铁,7条,7条上海1993年通地铁,19条,7条广州1997年通地铁,14条,11条深圳
2006年6月16日,我第一次去北京去北京市天安门看升国旗仪式爬八达岭长城好汉坡在北京游乐园玩过山车逛北京西单广场等等都是另我特别怀念的事情。我第一次去北京,老规矩先是旅游,后去打工。那天晚上八点多到了北京火车站,我
1997年到2021年江苏省南通市最低工资和小时工价格最低工资标准是2003年12月30日颁布,2004年3月1日实行。1997年和1998年南通市都是280元钱1999年南通市320元钱2000年南通市390元钱,小时工3。6元钱2
2021年7月25日,南通地铁一号线首次热滑动调试成功启动一条地铁线路的建成,有6个月左右的跨度综合调试。这长达6个月周期跨度中,同时联动调试参与单位很多。包括建设运营设计监督管理施工单位。有的调试科目一次就有150人以上参与。时间跨度大
我的童年(1987年到1996年)我是1981年出生的,在我有印象时,那是1987年,我在读幼儿园,幼儿园里面的玩具可多了,下课时间或者是游戏时间,我玩旋转木马,旋转飞机(坦克),我开心坐在里面,可以坐8个小朋友,
支持智能佩戴检测的TWS真无线蓝牙耳机荣耀Earbuds2SE说实话,到目前为止我用过的真无线蓝牙耳机也已经非常多了,所以普通的蓝牙耳机基本对我没有太大的吸引力了。而荣耀Earbuds2SE的出现还是让我充满了惊喜。我们首先来看一下它的外观吧
那些年我们使用过的手机号码你还记得你第一个手机号码是哪个号段?我第一次使用的是中国移动1391439号码归属地江苏南通139号码成立于上个世纪90年代,那个时候,手机号码是10位数,不是现在的11位数。时间
C怎么判断大小端模式大小端模式大端模式先存放最高有效字节,表现为最高有效字节存储在低地址小端模式先存放最低有效字节,表现为最低有效字节存储在低地址小端模式便于机器处理,大端模式方便人阅读。测试平台的字
有了它,我再也不用亮一宿灯了领普智能蓝牙开关双开套装因为我睡觉对光线不敏感,所以经常会不知不觉睡着了,早上睡醒一看灯亮了一宿。这无论是站在我个人的角度还是国家的角度都属于一种资源的浪费。2021年了,现在都在提碳中和碳达峰的概念,所
社畜的高颜值办公饮水补给站大宇彩虹杯,饮水就这么简单前段时间不知道大家有没有看到B站上一个up主的测评视频,传统立式的饮水机中,水污染的情况特别严重。尤其是长时间不清洗的饮水机,情况更为糟糕。那么我们有没有什么办法可以不喝饮水机里的
40004999元手机性价比排名小米11Pro第三如今,用户在选择智能手机的时候,不仅关注智能手机的外观造型,硬件配置,好评率等因素,还比较重视手机的性价比评分。比如在相同的价格区间内,用户自然希望可以购买到配置更高的机型。而这,
手机这么设置,既省电还不伤眼这是我们经常见到的头条首页其实头条首页也可以是这样。两个对比起来,我们会发现白色的更刺眼,黑色的会看起来更舒服一点。这其实就是现在手机都有的一个功能深色模式。打开了深色模式之后,常
2021年,想换手机,又怕被火龙坑,该怎么办?2021年,对于手机市场,消费者的购买力与往年相同,但是这一年的手机产品却让小伙伴们很是挠头。毕竟曾经火龙810和火龙820仍然让消费者记忆犹新,这一届的骁龙888成为了新一代的火
5万多账号被封亚马逊严打跨境电商祸起小卡片在亚马逊搜索中国某电商后显示页面。您涉嫌在商品包装内或商品包装上使用夹页传单优惠券小册子或类似材料,要求买家发表正面评论评分或提供奖励让买家发表评论评分。一家被封号的跨境电商收到的
手机的4大机身材质,每一种都各有优势和缺点,知道的人却不多如今,大家在网上买手机,通常会去对比芯片摄像头屏幕等参数,而对于手机的外观一般只是看它的设计美不美,却很少会去注意它的机身材质。可是大家有没有发现,如今智能手机的外观逐渐同质化,机
细节满满实测解读惠普战X锐龙版的商务质感一位优秀的商务人士,需要一台怎样的电脑?我想,一定是踏踏实实的,稳定优秀而且超长待机的,但是与此同时呢,体验上也要达到极致的。作为一款根植于商务办公需求几十年的产品,惠普的战系列多
比特币重回5万美元,币价升温之下显卡跟涨,游戏玩家何解?包括比特币在内的加密货币正在急速回升,再次点燃了币圈情绪,也重新推高了显卡价格。8月23日,比特币上行突破5万美元关口,为5月11日以来首次。而除了币圈玩家高度重视币价走势外,有显
质用车纯电动车不怕水?电池防水很重要随着新能源车,特别是纯电动汽车市场占有率不断提升,城市道路中纯电动车型的出镜率愈加频繁,尤其在北上广深等一线城市,大街上基本随处都可见纯电动汽车的身影。不过,消费者在使用纯电动车时
腾讯的游戏广告税收和500亿之前说过最近一直在考察互联网ETF替换顺丰仓位的事情。在这接近两个月的行情发展中,情况出现了少许变化。我决定把观察目标从互联网ETF变更到腾讯控股。原因也很简单,之前的考量主要是互
京东居家客服招聘标准前两天报名了京东居家客服,被拉进这个群里,大概了解了一下,招聘要求就是下面图片里这些,都满足的话就可以报名了。(主要针对在家看孩子的宝妈)咨询了一下,面试成功后,先培训,刚入职一小
小米红米K50realmeGT大师探索版参数大全参数大全小米红米K50Pro价格暂无价格产品特性快速充电,VoLTE高清语音,双扬声器,USBTypeC接口出品地区中国realmeGT大师探索版价格暂无价格上市时间2021年屏幕尺寸6。