【Spring源碼分析】Bean載入流程概覽

来源:http://www.cnblogs.com/xrq730/archive/2017/02/03/6285358.html
-Advertisement-
Play Games

代碼入口 之前寫文章都會啰啰嗦嗦一大堆再開始,進入【Spring源碼分析】這個板塊就直接切入正題了。 很多朋友可能想看Spring源碼,但是不知道應當如何入手去看,這個可以理解:Java開發者通常從事的都是Java Web的工作,對於程式員來說,一個Web項目用到Spring,只是配置一下配置文件而 ...


代碼入口

之前寫文章都會啰啰嗦嗦一大堆再開始,進入【Spring源碼分析】這個板塊就直接切入正題了。

很多朋友可能想看Spring源碼,但是不知道應當如何入手去看,這個可以理解:Java開發者通常從事的都是Java Web的工作,對於程式員來說,一個Web項目用到Spring,只是配置一下配置文件而已,Spring的載入過程相對是不太透明的,不太好去找載入的代碼入口。

下麵有很簡單的一段代碼可以作為Spring代碼載入的入口:

 1 ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
 2 ac.getBean(XXX.class);

ClassPathXmlApplicationContext用於載入CLASSPATH下的Spring配置文件,可以看到,第二行就已經可以獲取到Bean的實例了,那麼必然第一行就已經完成了對所有Bean實例的載入,因此可以通過ClassPathXmlApplicationContext作為入口。為了後面便於代碼閱讀,先給出一下ClassPathXmlApplicationContext這個類的繼承關係:

大致的繼承關係是如上圖所示的,由於版面的關係,沒有繼續畫下去了,左下角的ApplicationContext應當還有一層繼承關係,比較關鍵的一點是它是BeanFactory的子介面。

最後聲明一下,本文使用的Spring版本為3.0.7,比較老,使用這個版本純粹是因為公司使用而已。

 

ClassPathXmlApplicationContext構造函數

看下ClassPathXmlApplicationContext的構造函數:

 1 public ClassPathXmlApplicationContext(String configLocation) throws BeansException {
 2     this(new String[] {configLocation}, true, null);
 3 }
1 public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent)
2         throws BeansException {
3 
4     super(parent);
5     setConfigLocations(configLocations);
6     if (refresh) {
7         refresh();
8     }
9 }

從第二段代碼看,總共就做了三件事:

  1、super(parent)

    沒什麼太大的作用,設置一下父級ApplicationContext,這裡是null

  2、setConfigLocations(configLocations)

    代碼就不貼了,一看就知道,裡面做了兩件事情:

    (1)將指定的Spring配置文件的路徑存儲到本地

    (2)解析Spring配置文件路徑中的${PlaceHolder}占位符,替換為系統變數中PlaceHolder對應的Value值,System本身就自帶一些系統變數比如class.path、os.name、user.dir等,也可以通過System.setProperty()方法設置自己需要的系統變數

  3、refresh()

    這個就是整個Spring Bean載入的核心了,它是ClassPathXmlApplicationContext的父類AbstractApplicationContext的一個方法,顧名思義,用於刷新整個Spring上下文信息,定義了整個Spring上下文載入的流程。

 

refresh方法

上面已經說了,refresh()方法是整個Spring Bean載入的核心,因此看一下整個refresh()方法的定義:

 1 public void refresh() throws BeansException, IllegalStateException {
 2         synchronized (this.startupShutdownMonitor) {
 3             // Prepare this context for refreshing.
 4             prepareRefresh();
 5 
 6             // Tell the subclass to refresh the internal bean factory.
 7             ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
 8 
 9             // Prepare the bean factory for use in this context.
10             prepareBeanFactory(beanFactory);
11 
12             try {
13                 // Allows post-processing of the bean factory in context subclasses.
14                 postProcessBeanFactory(beanFactory);
15 
16                 // Invoke factory processors registered as beans in the context.
17                 invokeBeanFactoryPostProcessors(beanFactory);
18 
19                 // Register bean processors that intercept bean creation.
20                 registerBeanPostProcessors(beanFactory);
21 
22                 // Initialize message source for this context.
23                 initMessageSource();
24 
25                 // Initialize event multicaster for this context.
26                 initApplicationEventMulticaster();
27 
28                 // Initialize other special beans in specific context subclasses.
29                 onRefresh();
30 
31                 // Check for listener beans and register them.
32                 registerListeners();
33 
34                 // Instantiate all remaining (non-lazy-init) singletons.
35                 finishBeanFactoryInitialization(beanFactory);
36 
37                 // Last step: publish corresponding event.
38                 finishRefresh();
39             }
40 
41             catch (BeansException ex) {
42                 // Destroy already created singletons to avoid dangling resources.
43                 destroyBeans();
44 
45                 // Reset 'active' flag.
46                 cancelRefresh(ex);
47 
48                 // Propagate exception to caller.
49                 throw ex;
50             }
51         }
52     }

每個子方法的功能之後一點一點再分析,首先refresh()方法有幾點是值得我們學習的:

  1、方法是加鎖的,這麼做的原因是避免多線程同時刷新Spring上下文

  2、儘管加鎖可以看到是針對整個方法體的,但是沒有在方法前加synchronized關鍵字,而使用了對象鎖startUpShutdownMonitor,這樣做有兩個好處:

    (1)refresh()方法和close()方法都使用了startUpShutdownMonitor對象鎖加鎖,這就保證了在調用refresh()方法的時候無法調用close()方法,反之亦然,避免了衝突

    (2)另外一個好處不在這個方法中體現,但是提一下,使用對象鎖可以減小了同步的範圍,只對不能併發的代碼塊進行加鎖,提高了整體代碼運行的效率

  3、方法裡面使用了每個子方法定義了整個refresh()方法的流程,使得整個方法流程清晰易懂。這點是非常值得學習的,一個方法裡面幾十行甚至上百行代碼寫在一起,在我看來會有三個顯著的問題:

    (1)擴展性降低。反過來講,假使把流程定義為方法,子類可以繼承父類,可以根據需要重寫方法

    (2)代碼可讀性差。很簡單的道理,看代碼的人是願意看一段500行的代碼,還是願意看10段50行的代碼?

    (3)代碼可維護性差。這點和上面的類似但又有不同,可維護性差的意思是,一段幾百行的代碼,功能點不明確,不易後人修改,可能會導致“牽一發而動全身”

 

prepareRefresh方法

下麵挨個看refresh方法中的子方法,首先是prepareRefresh方法,看一下源碼:

 1 /**
 2  * Prepare this context for refreshing, setting its startup date and
 3  * active flag.
 4  */
 5 protected void prepareRefresh() {
 6     this.startupDate = System.currentTimeMillis();
 7         synchronized (this.activeMonitor) {
 8         this.active = true;
 9     }
10 
11     if (logger.isInfoEnabled()) {
12         logger.info("Refreshing " + this);
13     }
14 }

這個方法功能比較簡單,顧名思義,準備刷新Spring上下文,其功能註釋上寫了:

1、設置一下刷新Spring上下文的開始時間

2、將active標識位設置為true

另外可以註意一下12行這句日誌,這句日誌列印了真正載入Spring上下文的Java類。

 

obtainFreshBeanFactory方法

obtainFreshBeanFactory方法的作用是獲取刷新Spring上下文的Bean工廠,其代碼實現為:

1 protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
2     refreshBeanFactory();
3     ConfigurableListableBeanFactory beanFactory = getBeanFactory();
4     if (logger.isDebugEnabled()) {
5         logger.debug("Bean factory for " + getDisplayName() + ": " + beanFactory);
6     }
7     return beanFactory;
8 }

其核心是第二行的refreshBeanFactory方法,這是一個抽象方法,有AbstractRefreshableApplicationContext和GenericApplicationContext這兩個子類實現了這個方法,看一下上面ClassPathXmlApplicationContext的繼承關係圖即知,調用的應當是AbstractRefreshableApplicationContext中實現的refreshBeanFactory,其源碼為:

 1 protected final void refreshBeanFactory() throws BeansException {
 2     if (hasBeanFactory()) {
 3         destroyBeans();
 4         closeBeanFactory();
 5     }
 6     try {
 7         DefaultListableBeanFactory beanFactory = createBeanFactory();
 8         beanFactory.setSerializationId(getId());
 9         customizeBeanFactory(beanFactory);
10         loadBeanDefinitions(beanFactory);
11         synchronized (this.beanFactoryMonitor) {
12             this.beanFactory = beanFactory;
13         }
14     }
15     catch (IOException ex) {
16         throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
17     }
18 }

這段代碼的核心是第7行,這行點出了DefaultListableBeanFactory這個類,這個類是構造Bean的核心類,這個類的功能會在這一篇文章中詳細解讀,首先給出DefaultListableBeanFactory的繼承關係圖:

AbstractAutowireCapableBeanFactory這個類的繼承層次比較深,版面有限,就沒有繼續畫下去了,本圖基本上清楚地展示了DefaultListableBeanFactory的層析結構。

 

XML文件解析

另外一個核心是第10行,loadBeanDefinitions(beanFactory)方法,為什麼我們配置的XML文件最後能轉成Java Bean,首先就是由這個方法處理的。該方法最終的目的是將XML文件進行解析,以Key-Value的形式,Key表示BeanName,Value為BeanDefinition,最終存入DefaultListableBeanFactory中:

1 /** Map of bean definition objects, keyed by bean name */
2 private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<String, BeanDefinition>();
3 
4 /** List of bean definition names, in registration order */
5 private final List<String> beanDefinitionNames = new ArrayList<String>();

最終DefaultListableBeanFactory會先遍歷beanDefinitionNames,從beanDefinitionMap中拿到對應的BeanDefinition,最終轉為具體的Bean對象。BeanDefinition本身是一個介面,AbstractBeanDefinition這個抽象類存儲了Bean的屬性,看一下AbstractBeanDefinition這個抽象類的定義:

這個類的屬性與方法很多,這裡就列舉了一些最主要的方法和屬性,可以看到包含了bean標簽中的所有屬性,之後就是根據AbstractBeanDefinition中的屬性值構造出對應的Bean對象。

Spring沒有直接拿到XML中的bean定義就直接轉為具體的Bean對象,就是給Spring開發者留下了擴展點,比如之前BeanPostProcessor,在最後一部分會簡單提及。接著看一下XML是如何轉為Bean的,首先在AbstractXmlApplicationContext中將DefaultListableBeanFactory轉換為XmlBeanDefinitionReader:

 1 protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
 2     // Create a new XmlBeanDefinitionReader for the given BeanFactory.
 3     XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
 4 
 5     // Configure the bean definition reader with this context's
 6     // resource loading environment.
 7     beanDefinitionReader.setResourceLoader(this);
 8     beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));
 9 
10     // Allow a subclass to provide custom initialization of the reader,
11     // then proceed with actually loading the bean definitions.
12     initBeanDefinitionReader(beanDefinitionReader);
13     loadBeanDefinitions(beanDefinitionReader);
14 }

XmlBeanDefinitionReader顧名思義,一個XML文件中讀取Bean定義的工具,然後追蹤13行的代碼,先追蹤到DefaultBeanDefinitionDocumentReader的parseDefaultElement方法:

 1 private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) {
 2     if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) {
 3         importBeanDefinitionResource(ele);
 4     }
 5     else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) {
 6         processAliasRegistration(ele);
 7     }
 8     else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) {
 9         processBeanDefinition(ele, delegate);
10     }
11 }

XML文件的節點import、alias、bean分別有自己對應的方法去處理,以最常見的Bean為例,即第9行:

 1 protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
 2     BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
 3     if (bdHolder != null) {
 4         bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
 5         try {
 6             // Register the final decorated instance.
 7             BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
 8         }
 9         catch (BeanDefinitionStoreException ex) {
10             getReaderContext().error("Failed to register bean definition with name '" +
11                     bdHolder.getBeanName() + "'", ele, ex);
12         }
13         // Send registration event.
14         getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
15     }
16 }

核心的解析bean節點的代碼為第2行,這裡已經調用到了BeanDefinitionParserDelegate類的parseBeanDefinitionElement方法,看下是怎麼做的:

 1 public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, BeanDefinition containingBean) {
 2     String id = ele.getAttribute(ID_ATTRIBUTE);
 3     String nameAttr = ele.getAttribute(NAME_ATTRIBUTE);
 4 
 5     List<String> aliases = new ArrayList<String>();
 6     if (StringUtils.hasLength(nameAttr)) {
 7         String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, BEAN_NAME_DELIMITERS);
 8         aliases.addAll(Arrays.asList(nameArr));
 9     }
10 
11     String beanName = id;
12     if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) {
13         beanName = aliases.remove(0);
14         if (logger.isDebugEnabled()) {
15             logger.debug("No XML 'id' specified - using '" + beanName +
16                     "' as bean name and " + aliases + " as aliases");
17         }
18     }
19 
20     if (containingBean == null) {
21         checkNameUniqueness(beanName, aliases, ele);
22     }
23 
24     AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);
25     if (beanDefinition != null) {
26         if (!StringUtils.hasText(beanName)) {
27             try {
28                 if (containingBean != null) {
29                     beanName = BeanDefinitionReaderUtils.generateBeanName(
30                             beanDefinition, this.readerContext.getRegistry(), true);
31                 }
32                 else {
33                     beanName = this.readerContext.generateBeanName(beanDefinition);
34                     // Register an alias for the plain bean class name, if still possible,
35                     // if the generator returned the class name plus a suffix.
36                     // This is expected for Spring 1.2/2.0 backwards compatibility.
37                     String beanClassName = beanDefinition.getBeanClassName();
38                     if (beanClassName != null &&
39                                 beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() &&
40                                 !this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) {
41                         aliases.add(beanClassName);
42                     }
43                 }
44                 if (logger.isDebugEnabled()) {
45                     logger.debug("Neither XML 'id' nor 'name' specified - " +
46                             "using generated bean name [" + beanName + "]");
47                 }
48             }
49             catch (Exception ex) {
50                 error(ex.getMessage(), ele);
51                 return null;
52             }
53         }
54         String[] aliasesArray = StringUtils.toStringArray(aliases);
55         return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray);
56     }
57 
58     return null;
59 }

總結一下代碼邏輯:

(1)第2行和第3行,獲取id屬性和name屬性

(2)第5行~第9行,如果填寫了name屬性的話,將name屬性以",;",分割出來的字元串全部認為這個bean的別名,這裡我們可以學到Spring的StringUtils的tokenizeToStringArray方法,可以將字元串按照指定分割符分割為字元串數組

(3)第11行~第18行,預設beanName為id屬性,如果bean有配置別名(即上面的name屬性的話),以name屬性的第一個值作為beanName,發現很多人不知道beanName是什麼,這幾行代碼就表示了容器是如何定義beanName的

(4)第20行~第22行,這段用於保證beanName的唯一性的,BeanDefinitionParserDelegate中有一個屬性usedNames,這是一個Set,強制性地保證了beanName的唯一性

(5)第24行用於解析bean的其他屬性,後面的代碼不太重要,看一下parseBeanDefinitionElement的實現

 1 public AbstractBeanDefinition parseBeanDefinitionElement(
 2         Element ele, String beanName, BeanDefinition containingBean) {
 3 
 4     this.parseState.push(new BeanEntry(beanName));
 5 
 6     String className = null;
 7     if (ele.hasAttribute(CLASS_ATTRIBUTE)) {
 8         className = ele.getAttribute(CLASS_ATTRIBUTE).trim();
 9     }
10 
11     try {
12         String parent = null;
13         if (ele.hasAttribute(PARENT_ATTRIBUTE)) {
14             parent = ele.getAttribute(PARENT_ATTRIBUTE);
15         }
16         AbstractBeanDefinition bd = createBeanDefinition(className, parent);
17 
18         parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);
19         bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT));
20 
21         parseMetaElements(ele, bd);
22         parseLookupOverrideSubElements(ele, bd.getMethodOverrides());
23         parseReplacedMethodSubElements(ele, bd.getMethodOverrides());
24 
25         parseConstructorArgElements(ele, bd);
26         parsePropertyElements(ele, bd);
27         parseQualifierElements(ele, bd);
28         bd.setResource(this.readerContext.getResource());
29         bd.setSource(extractSource(ele));
30 
31         return bd;
32     }
33     catch (ClassNotFoundException ex) {
34         error("Bean class [" + className + "] not found", ele, ex);
35     }
36     catch (NoClassDefFoundError err) {
37         error("Class that bean class [" + className + "] depends on not found", ele, err);
38     }
39     catch (Throwable ex) {
40         error("Unexpected failure during bean definition parsing", ele, ex);
41     }
42     finally {
43         this.parseState.pop();
44     }
45 
46     return null;
47 }

這裡會取class屬性、parent屬性,18行的代碼可以跟進去看一下這裡就不貼了,會取得scope、lazy-init、abstract、depends-on屬性等等,設置到BeanDefinition中,這樣大致上,一個Bean的定義就被存入了BeanDefinition中。最後一步追溯到之前DefaultBeanDefinitionDocumentReader的processBeanDefinition方法:

1 BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());

這句語句將BeanDefinition存入DefaultListableBeanFactory的beanDefinitionMap中,追蹤一下代碼最終到DefaultListableBeanFactory的registerBeanDefinition方法中:

 1 public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
 2     throws BeanDefinitionStoreException {
 3 
 4     Assert.hasText(beanName, "Bean name must not be empty");
 5     Assert.notNull(beanDefinition, "BeanDefinition must not be null");
 6 
 7     if (beanDefinition instanceof AbstractBeanDefinition) {
 8         try {
 9             ((AbstractBeanDefinition) beanDefinition).validate();
10         }
11         catch (BeanDefinitionValidationException ex) {
12             throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
13                     "Validation of bean definition failed", ex);
14         }
15     }
16 
17     synchronized (this.beanDefinitionMap) {
18         Object oldBeanDefinition = this.beanDefinitionMap.get(beanName);
19         if (oldBeanDefinition != null) {
20             if (!this.allowBeanDefinitionOverriding) {
21                 throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName,
22                         "Cannot register bean definition [" + beanDefinition + "] for bean '" + beanName +
23                         "': There is already [" + oldBeanDefinition + "] bound.");
24             }
25             else {
26                 if (this.logger.isInfoEnabled()) {
27                     this.logger.info("Overriding bean definition for bean '" + beanName +
28                             "': replacing [" + oldBeanDefinition + "] with [" + beanDefinition + "]");
29                 }
30             }
31         }
32         else {
33             this.beanDefinitionNames.add(beanName);
34             this.frozenBeanDefinitionNames = null;
35         }
36         this.beanDefinitionMap.put(beanName, beanDefinition);
37 
38         resetBeanDefinition(beanName);
39     }
40 }

大致上就是beanDefinitionNames中增加一個beanName,beanDefinitionMap將老的BeanDefinition替換(假如不允許BeanDefinition重寫的話會拋出異常)。這樣一個漫長的流程過後,XML文件中的各個bean節點被轉換為BeanDefinition,存入了DefaultListableBeanFactory中,後續DefaultListableBeanFactory可以根據BeanDefinition,構造對應的Bean對象出來。

 

其他部分

上面分析了Spring上下文載入的代碼入口以及refresh方法中的一些重點部分,下麵還有一些子方法沒有分析,這裡簡單分析一下:

1、prepareBeanFactory方法

  prepareBeanFactory用於配置BeanFactory上下文的標準特征,比如載入Spring上下文使用的ClassLoader以及一些前置處理器

2、postProcessBeanFactory方法

  在標準初始化之後修改應用上下文的內部BeanFactory,所有Bean的定義已經被載入完畢了,但是Bean還沒有被初始化,此時允許在特定的ApplicationContext上下文中註冊特定的BeanPostProcessor

3、invokeBeanFactoryPostProcessors方法

  初始化並且調用所有已經註冊的BeanProcessor介面,按照BeanProcessor介面給定的順序調用。其實最終就是調用BeanProcessor介面的postProcessBeanFactory方法,傳入BeanFactory

4、registerBeanPostProcessors方法

  註冊所有的BeanPostProcessor方法

5、initMessageSource方法

  初始化上下文的MessageSource

6、initApplicationEventMulticaster方法

  初始化上下文的多播事件

7、onRefresh方法

  在指定的上下文子類中初始化其他特殊的Bean

8、finishBeanFactoryInitialization方法

  初始化所有非懶載入單例Bean,標紅加粗表示這是重點,所有單例Bean通過這個方法載入,後文也是圍繞這個方法展開

9、finishRefresh方法

  最後一步,結束刷新上下文,發佈正確的事件

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 使用Python的turtle(海龜)模塊畫圖 第一步:讓Python引入turtle模塊,引入模塊就是告訴Python你想要用它。 第二步:創建畫布。調用turtle中的Pen函數。 第三步:移動海龜。 forward的中文意思是“向前地;促進”。所以這行代碼的意思是海龜向前移動50個像素: 讓海 ...
  • 有時候,我們也會有在C++中調用.NET的需求,C++/CLI就是這樣一種技術,它能夠與本地代碼混合編程,從而提供強大的功能,本文將介紹如何使用反射的一些實踐。 ...
  • 代碼入口 上文【Spring源碼分析】Bean載入流程概覽,比較詳細地分析了Spring上下文載入的代碼入口,並且在AbstractApplicationContext的refresh方法中,點出了finishBeanFactoryInitialization方法完成了對於所有非懶載入的Bean的初 ...
  • 最近要從一個http上下載個文件,差點就直接telnet了,突然發現了這個: 但是還得讀取它,用什麼呢? 直接用 但是在類里,不能用Application.StartupPath代表程式所在目錄呀,這怎麼辦呢?遂baidu了一下,發現可以這樣: 然後,就可以讀取了。 完整代碼:(目標文件地址http ...
  • 本文演示環境為python2.7,主要介紹了python模塊之re正則表達式 ...
  • 1. 位運算符,如果運算對象是帶符號的且它的值為負,那麼位運算符如何處理運算對象的“符號位”依賴於機器。此時左移操作可能會改變符號位的值,因此是一種UB。 Best Practices: 關於符號位如何處理沒有明確的規定,所以強烈建議僅將位運算用於處理無符號類型。 Bitwise Operators ...
  • 點我去Python官網下載 往下翻幾頁就能看到各種版本的Python,當前最新的是Python3.6,也沒多大區別,我選擇的是3.5.2 64位的,點擊download 根據自己的電腦配置,我選擇的是64位的 一路預設下去吧! 安裝成功!下麵打開命令提示符,輸入python,回車如下圖一樣就安裝成功 ...
  • hello world 好。就這樣! ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...