@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.pswp.cn/news/161745.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/161745.shtml
英文地址,請注明出處:http://en.pswp.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…

1

【任務 1】私有云服務搭建[10 分] 【題目 1】基礎環境配置[0.5 分] 【題目 2】Yum 源配置[0.5 分] 【題目 3】配置無秘鑰 ssh[0.5 分] 【題目 4】基礎安裝[0.5 分] 【題目 5】數據庫安裝與調優[0.5 分] 【題目 6】Keystone 服務安裝與使用[0.5 分] 【題目 7】Glance 安裝與使用…

BLE通用廣播包

文章目錄 1、藍牙廣播數據格式2、掃描響應數據 1、藍牙廣播數據格式 藍牙廣播包的最大長度是37個字節&#xff0c;其中設備地址占用了6個字節&#xff0c;只有31個字節是可用的。這31個可用的字節又按照一定的格式來組織&#xff0c;被分割為n個AD Structure。如下圖所示&…

npm命令

node -v --查看版本 npm install --安裝npm npm config get registry --查看npm當前鏡像 npm config set registry https://registry.npmmirror.com --設置淘寶鏡像 npm版本管理工具

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;能以二分的方式進行查…

Mac Goland無法調試

去github上下載golang的debug工具delve&#xff1a; go-delve/delve?github.com/go-delve/delve/blob/master/Documentation/installation/README.md?編輯 或者: go install github.com/go-delve/delve/cmd/dlvlatest按照他的安裝方式進行安裝&#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…

vue腳手架的基礎搭建過程

MVVM架構 Vue框架底層設計遵循MVVM架構。 Model層&#xff08;M&#xff09;模型層&#xff08;業務邏輯層&#xff09; View層&#xff08;V&#xff09;視圖層 主管UI ViewModel層&#xff08;VM&#xff09; 將項目代碼劃分清晰的層次結構后&#xff0c;非常有利于后期代…

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 編程語言的計算機圖形和聲音庫。

以jar包形式 部署Spring Boot項目

后端部署 當你將Spring Boot項目打包成JAR文件并上傳到服務器時&#xff0c;可以考慮在服務器上創建一些目錄來存放這個JAR文件以及相關的配置文件。以下是一些常見的目錄結構建議&#xff1a; /opt/your-project-name/&#xff1a; 在/opt目錄下創建一個與你的項目名稱相關的…

【word技巧】Word制作試卷,ABCD選項如何對齊?

使用word文件制作試卷&#xff0c;如何將ABCD選項全部設置對齊&#xff1f;除了一直按空格或者Tab鍵以外&#xff0c;還有其他方法嗎&#xff1f;今天分享如何將ABCD選項對齊。 首先&#xff0c;我們打開【替換和查找】&#xff0c;在查找內容輸入空格&#xff0c;然后點擊全部…

省市區編碼sql

CREATE TABLE area (id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主鍵,code varchar(64) COLLATE utf8mb4_bin DEFAULT NULL COMMENT 編碼,name varchar(255) COLLATE utf8mb4_bin DEFAULT NULL COMMENT 名稱,parent_code varchar(64) COLLATE utf8mb4_bin DEFAULT NULL CO…

20個CSS函數-釋放設計創造力和響應能力

20個CSS函數-釋放設計創造力和響應能力 CSS是網頁設計的核心&#xff0c;使開發者和設計者能夠制作出令人嘆為觀止和反應迅速的網頁布局。CSS函數通過引入動態性和多功能性提升了我們的設計能力。在本文中&#xff0c;我們將開始講解20個CSS函數。 1.rgba()&#xff1a;定義顏…

結構體打印

打印輸出 通過注解來派生Debug trait&#xff0c;才可以通過println!進行打印。默認的占位符是{}&#xff0c;底層是按照std::fmt::Display具體實現進行格式化輸出。 {}、{:?}、{#?}是格式化的幾種形式&#xff0c;{#?}是更加易讀的JSON話格式。 方法 結構體聲明方法&…