@PostConstruct虽好,请勿乱用

1.问题说明

在日常的业务开发中,有时会利用@PostConstruct在容器启动时执行一些任务。例如:

@PostConstruct
public void init(){System.out.println("service 初始化...............");
}

一般情况这没什么问题,但最近一个同事在做一个数据结转的任务中使用这个注解进行测试的时候却出现了问题,大概的伪代码如下:

@Component
public class TreePrune{@PostConstructpublic void init() {System.out.println("初始化开始...............");CompletableFuture<Void> voidCompletableFuture =  CompletableFuture.runAsync(this::process);try {voidCompletableFuture.get();} catch (Exception e) {throw new RuntimeException(e);}System.out.println("初始化成功...............");
}private void process() {SpringContextHolder.getBean(Tree.class).test(null);}
}@Component
public class Tree {public TreeNode test(TreeNode root) {System.out.println("测试Tree");return root;}
}

启动项目,控制台输出:

"初始化成功...............

控制台并没有继续输出测试Tree初始化成功...............这两句,看起来程序似乎处于中止的状态,没有继续向下执行。

为了查看线程的执行状态,使用jstack -l pid命令打印堆栈,查看输出的日志,发现线程确实处于BLOCKED状态,而且仔细看堆栈信息的话可以发现是在执行DefaultSingletonBeanRegistry.getSingleton方法时等待获取monitor锁。

在这里插入图片描述

我们先找到相关源码,Spring的版本是5.2.11.RELEASE,在org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:179)

	protected Object getSingleton(String beanName, boolean allowEarlyReference) {// Quick check for existing instance without full singleton lockObject singletonObject = this.singletonObjects.get(beanName);if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {singletonObject = this.earlySingletonObjects.get(beanName);if (singletonObject == null && allowEarlyReference) {// singletonObjects就是一个ConcurrentHashMapsynchronized (this.singletonObjects) {// Consistent creation of early reference within full singleton locksingletonObject = this.singletonObjects.get(beanName);if (singletonObject == null) {singletonObject = this.earlySingletonObjects.get(beanName);if (singletonObject == null) {ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);if (singletonFactory != null) {singletonObject = singletonFactory.getObject();this.earlySingletonObjects.put(beanName, singletonObject);this.singletonFactories.remove(beanName);}}}}}}return singletonObject;}

对Spring创建bean相关源码有一定了解的同学应该对这个方法比较熟悉,Spring在创建bean的时候会先尝试从一级缓存里获取,如果获取到直接返回,如果没有获取到会先获取锁然后继续尝试从二级缓存、三级缓存中获取。CompletableFuture里执行任务的线程在获取singletonObjects对象的monitor锁时被阻塞了也就是说有其它线程已经提前获取了这个锁并且没有释放。根据锁对象的地址0x00000005c4e76198在日志中搜索,果然有发现。

在这里插入图片描述

可以看到持有对象0x00000005c4e76198monitor锁的线程就是main线程,也就是Springboot项目启动的主线程,也就是执行被@PostConstruct修饰的init方法的线程,同时main线程在执行get方法等待获取任务执行结果时切换为WAITING状态。看堆栈的话,main线程是在启动时创建TreePrune对象时获取的锁,相关源码如下,在org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222):

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {Assert.notNull(beanName, "Bean name must not be null");// 获取singletonObjects的monitor锁synchronized (this.singletonObjects) {Object singletonObject = this.singletonObjects.get(beanName);if (singletonObject == null) {......beforeSingletonCreation(beanName);boolean newSingleton = false;boolean recordSuppressedExceptions = (this.suppressedExceptions == null);if (recordSuppressedExceptions) {this.suppressedExceptions = new LinkedHashSet<>();}try {// 创建对象,后续会执行到TreePrune类中的init方法singletonObject = singletonFactory.getObject();newSingleton = true;}.......if (newSingleton) {addSingleton(beanName, singletonObject);}}return singletonObject;}}

因此,整个流程就是main线程在创建TreePrune对象时,先获取singletonObjectsmonitor锁然后执行到init方法,在init方法里异步开启CompletableFuture任务,使用get方法获取任务结果,在结果返回之前main线程处于WAITING状态,并且不释放锁。与此同时CompletableFuture内的异步线程从容器中获取bean也需要获取singletonObjectsmonitor锁,由于main线程不释放锁,CompletableFuture内的异步线程一直处于BLOCKED状态无法返回结果,get方法也就一直处于WAITING状态,形成了一个类似死锁的局面。

tips:分析stack文件的时候,有一个比较好用的在线工具Online Java Thread Dump Analyzer,它能比较直观的展示锁被哪个线程获取,哪个线程又在等待获取锁。

在这里插入图片描述

2.问题解决

根据上面的分析解决办法也很简单,既然问题是由于main线程在获取锁后一直不释放导致的,而没有释放锁主要是因为一直在get方法处等待,那么只需要从get方法入手即可。

  • 方法一,如果业务允许,干脆不调用get方法获取结果;

  • 方法二,get方法添加等待超时时间,这样其实也无法获取到异步任务执行结果:

    voidCompletableFuture.get(1000L)
    
  • 方法三,get方法放在异步线程执行:

        new Thread(){@Overridepublic void run(){try {voidCompletableFuture.get();} catch (Exception e) {throw new RuntimeException(e);} }}.start();
    
  • 方法四,CompletableFuture里的异步任务改为同步执行

    @PostConstruct
    public void init() {System.out.println("初始化开始...............");process();System.out.println("初始化成功...............");
    }
    

单纯就上面这个伪代码例子来说,除了上面几种方法,其实还有一种方法也可以解决,那就是修改process方法,将手动从容器中获取tree改为自动注入,至于原因将在后文进行分析,可以提示一下与@PostConstruct执行的时机有关。前面的例子之所以要写成手动从容器获取是因为原始代码process方法里是调用Mapper对象操作数据库,为了复现问题做了类似的处理。

@Component
public class TreePrune{@AutowiredTree tree;@PostConstructpublic void init() {System.out.println("初始化开始...............");CompletableFuture<Void> voidCompletableFuture = 				CompletableFuture.runAsync(this::process);try {voidCompletableFuture.get();} catch (Exception e) {throw new RuntimeException(e);}System.out.println("初始化成功...............");
}private void process() {tree.test(null);}
}@Component
public class Tree {public TreeNode test(TreeNode root) {System.out.println("测试Tree");return root;}
}

3.问题拓展

问题看起来是解决了,但对于问题形成的根本原因以及@PostConstruct的原理还没有过多的讲解,下面就简单介绍下。

@PostConstruct注解是在javax.annotation包下的,也就是java拓展包定义的注解,并不是Spring定义的,但Spring对它的功能做了实现。与之类似的还有@PreDestroy@Resource等注解。

package javax.annotation;
....
@Documented
@Retention (RUNTIME)
@Target(METHOD)
public @interface PostConstruct {
}

Spring提供了一个CommonAnnotationBeanPostProcessor来处理这几个注解,看名字就知道这是一个bean的后置处理器,它能介入bean创建过程。

	public CommonAnnotationBeanPostProcessor() {setOrder(Ordered.LOWEST_PRECEDENCE - 3);setInitAnnotationType(PostConstruct.class);setDestroyAnnotationType(PreDestroy.class);ignoreResourceType("javax.xml.ws.WebServiceContext");}

这个后置处理器会在容器启动时进行注册

		// Check for JSR-250 support, and if present add the CommonAnnotationBeanPostProcessor.if (jsr250Present && !registry.containsBeanDefinition(COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)) {RootBeanDefinition def = new RootBeanDefinition(CommonAnnotationBeanPostProcessor.class);def.setSource(source);beanDefs.add(registerPostProcessor(registry, def, COMMON_ANNOTATION_PROCESSOR_BEAN_NAME));}

首先我们看Spring创建bean的一个核心方法,只保留一些核心的代码,源码在org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBeann(AbstractAutowireCapableBeanFactory.java:547)。

protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)throws BeanCreationException {// Instantiate the bean.BeanWrapper instanceWrapper = null;.....if (instanceWrapper == null) {// 创建对象instanceWrapper = createBeanInstance(beanName, mbd, args);}Object bean = instanceWrapper.getWrappedInstance();Class<?> beanType = instanceWrapper.getWrappedClass();if (beanType != NullBean.class) {mbd.resolvedTargetType = beanType;}....// Initialize the bean instance.Object exposedObject = bean;try {// 注入属性populateBean(beanName, mbd, instanceWrapper);// 初始化exposedObject = initializeBean(beanName, exposedObject, mbd);}......return exposedObject;}

我们主要看初始化的initializeBean方法

	protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {if (System.getSecurityManager() != null) {AccessController.doPrivileged((PrivilegedAction<Object>) () -> {invokeAwareMethods(beanName, bean);return null;}, getAccessControlContext());}else {// 处理Aware接口invokeAwareMethods(beanName, bean);}Object wrappedBean = bean;if (mbd == null || !mbd.isSynthetic()) {//后置处理器的before方法wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);}try {//处理InitializingBean和init-methodinvokeInitMethods(beanName, wrappedBean, mbd);}catch (Throwable ex) {throw new BeanCreationException((mbd != null ? mbd.getResourceDescription() : null),beanName, "Invocation of init method failed", ex);}if (mbd == null || !mbd.isSynthetic()) {//后置处理器的after方法wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);}return wrappedBean;}@Overridepublic Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName)throws BeansException {Object result = existingBean;//遍历所有的后置处理器然后执行它的postProcessBeforeInitializationfor (BeanPostProcessor processor : getBeanPostProcessors()) {Object current = processor.postProcessBeforeInitialization(result, beanName);if (current == null) {return result;}result = current;}return result;}protected void invokeInitMethods(String beanName, Object bean, @Nullable RootBeanDefinition mbd)throws Throwable {boolean isInitializingBean = (bean instanceof InitializingBean);if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {if (logger.isTraceEnabled()) {logger.trace("Invoking afterPropertiesSet() on bean with name '" + beanName + "'");}if (System.getSecurityManager() != null) {try {AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () -> {((InitializingBean) bean).afterPropertiesSet();return null;}, getAccessControlContext());}catch (PrivilegedActionException pae) {throw pae.getException();}}else {// 处理处理InitializingBean((InitializingBean) bean).afterPropertiesSet();}}if (mbd != null && bean.getClass() != NullBean.class) {String initMethodName = mbd.getInitMethodName();if (StringUtils.hasLength(initMethodName) &&!(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) &&!mbd.isExternallyManagedInitMethod(initMethodName)) {// 处理init-method方法invokeCustomInitMethod(beanName, bean, mbd);}}}

applyBeanPostProcessorsBeforeInitialization方法里会遍历所有的后置处理器然后执行它的postProcessBeforeInitialization,前面说的CommonAnnotationBeanPostProcessor类继承了InitDestroyAnnotationBeanPostProcessor,所以执行的是下面这个方法。

	@Overridepublic Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {// 查找@PostConstruct、@PreDestroy注解修饰的方法LifecycleMetadata metadata = findLifecycleMetadata(bean.getClass());try {// 通过反射调用metadata.invokeInitMethods(bean, beanName);}catch (InvocationTargetException ex) {throw new BeanCreationException(beanName, "Invocation of init method failed", ex.getTargetException());}catch (Throwable ex) {throw new BeanCreationException(beanName, "Failed to invoke init method", ex);}return bean;}private LifecycleMetadata findLifecycleMetadata(Class<?> clazz) {if (this.lifecycleMetadataCache == null) {return buildLifecycleMetadata(clazz);}// 从缓存里获取LifecycleMetadata metadata = this.lifecycleMetadataCache.get(clazz);if (metadata == null) {synchronized (this.lifecycleMetadataCache) {metadata = this.lifecycleMetadataCache.get(clazz);if (metadata == null) {// 没有去创建metadata = buildLifecycleMetadata(clazz);this.lifecycleMetadataCache.put(clazz, metadata);}return metadata;}}return metadata;}

buildLifecycleMetadata方法里,会通过反射去获取方法上有initAnnotationTypedestroyAnnotationType类型方法,而initAnnotationTypedestroyAnnotationType的值就是前面创建CommonAnnotationBeanPostProcessor的构造方法里赋值的,也就是PostConstruct.classPreDestroy.class

	private LifecycleMetadata buildLifecycleMetadata(final Class<?> clazz) {if (!AnnotationUtils.isCandidateClass(clazz, Arrays.asList(this.initAnnotationType, this.destroyAnnotationType))) {return this.emptyLifecycleMetadata;}List<LifecycleElement> initMethods = new ArrayList<>();List<LifecycleElement> destroyMethods = new ArrayList<>();Class<?> targetClass = clazz;do {final List<LifecycleElement> currInitMethods = new ArrayList<>();final List<LifecycleElement> currDestroyMethods = new ArrayList<>();ReflectionUtils.doWithLocalMethods(targetClass, method -> {//initAnnotationType就是PostConstruct.classif (this.initAnnotationType != null && method.isAnnotationPresent(this.initAnnotationType)) {LifecycleElement element = new LifecycleElement(method);currInitMethods.add(element);}//destroyAnnotationType就是PreDestroy.classif (this.destroyAnnotationType != null && method.isAnnotationPresent(this.destroyAnnotationType)) {currDestroyMethods.add(new LifecycleElement(method));}});initMethods.addAll(0, currInitMethods);destroyMethods.addAll(currDestroyMethods);targetClass = targetClass.getSuperclass();}while (targetClass != null && targetClass != Object.class);return (initMethods.isEmpty() && destroyMethods.isEmpty() ? this.emptyLifecycleMetadata :new LifecycleMetadata(clazz, initMethods, destroyMethods));}

获取到方法上有initAnnotationTypedestroyAnnotationType类型方法后,后续就是通过反射进行调用,就不赘述了。完整的流程其实还是相对比较简单的,下面有个大致的流程图,感兴趣的同学可以自己打个断点跟着走一走。

在这里插入图片描述

根据源码的执行流程我们可以知道,在一个bean 创建的过程中@PostConstruct的执行在属性注入populateBean方法之后的initializeBean方法即初始化bean的方法中。现在你知道为什么我们前面说将process方法中手动从容器中获取tree改为自动注入也可以解决问题了吗?

改为自动注入后获取tree对象就是在populateBean方法中执行,也就是说是main线程在执行,当它尝试去获取singletonObjectsmonitor锁时,由于Sychronized是可重入锁,它不会被阻塞,等执行到CompletableFuture的异步任务时,由于并不需要去容器中获取bean,也就不会尝试去获取singletonObjectsmonitor锁,即不会被阻塞,那么get方法自然就能获取到结果,程序也就能正常的执行下去。

此外,通过源码我们也可以知道在Bean初始化的执行三种常见方法的执行顺序,即

1.注解@PostConstruct

2.InitializingBean接口的afterPropertiesSet方法

3.<bean>或者@Bean注入bean,它的init-method的属性

4.结论

通过上述的分析,可以做几个简单的结论:

1.@PostConstruct 修饰的方法是在bean初始化的时候执行,并且相比其它初始化方法,它们的顺序是@PostConstruct > InitializingBean > init-method

2.不要在@PostConstruct 中执行耗时任务,它会影响程序的启动速度,如果实在有这样的需求可以考虑异步执行或者使用定时任务。

3.程序中如果有类似future.get获取线程执行结果的代码,尽量使用有超时时间的get方法。

参考:Spring 框架中 @PostConstruct 注解详解

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

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

相关文章

ui5使用echart

相关的代码已经发布到github上。 展示下相关的实现功能 1、柱状图-1 2、柱状图-2 3.折线图 4.饼状图 如何使用&#xff1a; 使用git clone项目到本地 git clone https://github.com/linhuang0405/com.joker.Zechart找到index.html。在vscode里右键选择Open with Live Serve…

BLE通用广播包

文章目录 1、蓝牙广播数据格式2、扫描响应数据 1、蓝牙广播数据格式 蓝牙广播包的最大长度是37个字节&#xff0c;其中设备地址占用了6个字节&#xff0c;只有31个字节是可用的。这31个可用的字节又按照一定的格式来组织&#xff0c;被分割为n个AD Structure。如下图所示&…

VS Code 如何搭建C/C++环境

目录 一、VS Code是什么&#xff1f; 二、VS Code下载和安装 2.1下载 2.2安装 2.3环境介绍 三、Vs Code配置C/C环境 3.1下载和配置MinGW-w64编译器套件 3.1.1下载 3.1.2配置 一、VS Code是什么&#xff1f; 跨平台&#xff0c;免费且开源的现代轻量级代码编辑器 Vis…

【MATLAB源码-第85期】基于farrow结构的滤波器仿真,截止频率等参数可调。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 Farrow结构是一种用于实现可变数字滤波器的方法&#xff0c;尤其适用于数字信号处理中的采样率转换和时变滤波。它通过多项式近似来实现对滤波器系数的平滑变化&#xff0c;使得滤波器具有可变的群延时或其他参数。 Farrow结…

mysql中数据是如何被用B+树查询到的

innoDB是按照页为单位读写的 那页中有很多行数据&#xff0c;是怎么执行查询的呢&#xff0c;首先我们肯定&#xff0c;是以单向列表形式存储的&#xff0c;提高了增删的效率&#xff0c;但是查询效率低。所以实际上对页中的行数据进行了优化&#xff0c;能以二分的方式进行查…

基于北方苍鹰算法优化概率神经网络PNN的分类预测 - 附代码

基于北方苍鹰算法优化概率神经网络PNN的分类预测 - 附代码 文章目录 基于北方苍鹰算法优化概率神经网络PNN的分类预测 - 附代码1.PNN网络概述2.变压器故障诊街系统相关背景2.1 模型建立 3.基于北方苍鹰优化的PNN网络5.测试结果6.参考文献7.Matlab代码 摘要&#xff1a;针对PNN神…

Java面试-框架篇-Mybatis

Java面试-框架篇-Mybatis MyBatis执行流程延迟加载使用及原理一, 二级缓存来源 MyBatis执行流程 读取MyBatis配置文件: mybatis-config.xml加载运行环境和映射文件构造会话工厂SqlSessionFactory会话工厂创建SqlSession对象(包含了执行SQL语句的所有方法)操作数据库的接口, Ex…

IP地址定位技术发展与未来趋势

随着互联网的快速发展&#xff0c;人们对网络的需求和依赖程度越来越高。在海量的网络数据传输中&#xff0c;IP地址定位技术作为网络安全与信息追踪的重要手段&#xff0c;其精准度一直备受关注。近年来&#xff0c;随着技术的不断进步&#xff0c;IP地址定位的精准度得到了显…

【wireshark】基础学习

TOC 查询tcp tcp 查询tcp握手请求的代码 tcp.flags.ack 0 确定tcp握手成功的代码 tcp.flags.ack 1 确定tcp连接请求的代码 tcp.flags.ack 0 and tcp.flags.syn 1 3次握手后确定发送成功的查询 tcp.flags.fin 1 查询某IP对外发送的数据 ip.src_host 192.168.73.134 查询某…

485 实验

485(一般称作 RS485/EIA-485)隶属于 OSI 模型物理层&#xff0c;是串行通讯的一种。电气特性规定 为 2 线&#xff0c;半双工&#xff0c;多点通信的类型。它的电气特性和 RS-232 大不一样。用缆线两端的电压差值 来表示传递信号。RS485 仅仅规定了接受端和发送端的电气特性。它…

python趣味编程-5分钟实现一个太空大战游戏(含源码、步骤讲解)

飞机战争游戏系统项目是使用Python编程语言开发的,是一个简单的桌面应用程序。 Python 中的飞机战争游戏使用pygame导入和随机导入。 Pygame 是一组跨平台的 Python 模块,专为编写视频游戏而设计。它包括设计用于 Python 编程语言的计算机图形和声音库。

【word技巧】Word制作试卷,ABCD选项如何对齐?

使用word文件制作试卷&#xff0c;如何将ABCD选项全部设置对齐&#xff1f;除了一直按空格或者Tab键以外&#xff0c;还有其他方法吗&#xff1f;今天分享如何将ABCD选项对齐。 首先&#xff0c;我们打开【替换和查找】&#xff0c;在查找内容输入空格&#xff0c;然后点击全部…

结构体打印

打印输出 通过注解来派生Debug trait&#xff0c;才可以通过println!进行打印。默认的占位符是{}&#xff0c;底层是按照std::fmt::Display具体实现进行格式化输出。 {}、{:?}、{#?}是格式化的几种形式&#xff0c;{#?}是更加易读的JSON话格式。 方法 结构体声明方法&…

【应用前沿】索托斯平台:个性化推荐变身SaaS 服务

随着互联网技术和人工智能的迅速发展&#xff0c;面对海量的数据和资源&#xff0c;如何快速准确地为每个用户提供其感兴趣的内容&#xff0c;成为我们亟待解决的问题。个性化推荐系统正是为了解决这一问题而诞生的&#xff0c;它能够通过对用户行为的分析和挖掘&#xff0c;为…

5-6求1-20的阶乘和

#include<stdio.h> //求阶乘 int main(){int n;double sum0;//求和&#xff1a;一点一点加int t1;for (n1;n<15;n){tt*n;sumsumt;}printf("结果是&#xff1a;%22.15e \n",sum);return 0; }为啥最后是%22.15e呢&#xff1f; 因为这个求和的结果太大了 所以转…

【VScode】安装配置、插件及远程SSH连接

一、VSCode安装 二、配置安装插件 三、配置远程连接SSH 四、MinGW 一、VSCode安装 VS官网 Visual Studio Code - Code Editing. Redefined下载安装包&#xff1a; 二、配置安装插件 安装中文插件 配置字体为20 配置文件–>首选项->设置->Font Size为20 设置 VSC…

【libGDX】使用Mesh绘制圆形

1 前言 使用Mesh绘制三角形 中介绍了绘制三角形的方法&#xff0c;使用Mesh绘制矩形 中介绍了绘制矩形的方法&#xff0c;本文将介绍绘制圆形的方法。 libGDX 以点、线段、三角形为图元&#xff0c;没有提供绘制圆形的接口。要绘制圆形边框&#xff0c;必须通过割圆法逼近圆形&…

【中间件】服务化中间件理论intro

中间件middleware 内容管理 intro服务化middleware架构注册中心intro服务治理系统intro 本文主要intro服务化中间件的探讨 去年cfeng写了一篇博客走马观花般阐述了Spring Cloud下面的各种中间件&#xff0c;连深入使用都谈不上&#xff0c;只能说intro&#xff0c;在实际work中…

数字孪生助力污水处理升级

随着科技的发展&#xff0c;数字孪生技术在各行各业中得到了广泛应用。在污水处理领域&#xff0c;数字孪生技术为流程监控、效率提升、问题诊断等提供了强有力的支持。本文就借用山海鲸可视化软件的污水处理解决方案为大家介绍数字孪生在污水处理领域的作用。 一、实时监控 …

VsCode学习

一、在VsCode 上编写第一个C语言 在VsCode上写代码都是先打开文件夹&#xff0c;这样也方便管理代码和编译器产生的可执行程序&#xff0c;VsCode生成的配置文件等。 1.1打开文件夹 写代码前&#xff0c;首先创立一个文件夹存储以后我们写的VsCode代码&#xff0c;便于管理。…