SpringIOC源碼解析(上)

来源:https://www.cnblogs.com/zhixiang-org-cn/archive/2019/08/04/11300644.html
-Advertisement-
Play Games

註意,看完這篇文章需要很長很長很長時間。。。 準備工作 本文會分析Spring的IOC模塊的整體流程,分析過程需要使用一個簡單的demo工程來啟動Spring,demo工程我以備好,需要的童鞋自行在下方鏈接下載: 1 https://github.com/shiyujun/spring-framew ...


註意,看完這篇文章需要很長很長很長時間。。。

準備工作

本文會分析Spring的IOC模塊的整體流程,分析過程需要使用一個簡單的demo工程來啟動Spring,demo工程我以備好,需要的童鞋自行在下方鏈接下載:

1
https://github.com/shiyujun/spring-framework
Demo工程示例代碼

本文源碼分析基於Spring5.0.0,所以pom文件中引入5.0的依賴

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.0.RELEASE</version>
</dependency>
</dependencies>

然後寫一個簡單的介面和實現類

1
2
3
4
5
6
7
8
9
public interface IOCService {
public String hollo();
}

public class IOCServiceImpl implements IOCService {
public String hollo() {
return "Hello,IOC";
}
}

新建一個application-ioc.xml

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd" default-autowire="byName">

<bean id="iocservice" class="cn.shiyujun.service.impl.IOCServiceImpl"/>
</beans>

啟動Spring

1
2
3
4
5
6
7
public class IOCDemo {
public static void main (String args[]){
ApplicationContext context = new ClassPathXmlApplicationContext("classpath:application-ioc.xml");
IOCService iocService=context.getBean(IOCService.class);
System.out.println(iocService.hollo());
}
}

上方一個簡單的demo工程相信各位童鞋在剛剛學習Spring的時候就已經玩的特別6了。我就不詳細的說明瞭,直接開始看源碼吧

ClassPathXmlApplicationContext

背景調查

在文章開始的demo工程中,我選擇使用了一個xml文件來配置了介面和實現類之間的關係,然後使用了ClassPathXmlApplicationContext這個類來載入這個配置文件。現在我們就先來看一下這個類到底是個什麼東東
首先看一下繼承關係圖(只保留了跟本文相關的,省略了很多其他的繼承關係)
1

可以看到左下角的就是我們今天的主角ClassPathXmlApplicationContext、然後它的旁邊是一個同門師兄弟FileSystemXmlApplicationContext。看名字就可以知道它們哥倆都是通過載入配置文件來啟動Spring的,只不過一個是從程式內載入一個是從系統內載入。

除了這兩個還有一個類AnnotationConfigApplicationContext比較值得我們關註,這個類是用來處理註解式編程的。

而最上邊的ApplicationContext則是大名鼎鼎的Spring核心上下文了

源碼分析

看一下這個類的源代碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ClassPathXmlApplicationContext extends AbstractXmlApplicationContext {
//配置文件數組
private Resource[] configResources;

// 指定ApplicationContext的父容器
public ClassPathXmlApplicationContext(ApplicationContext parent) {
super(parent);
}

public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, ApplicationContext parent)
throws BeansException {

super(parent);
// 根據提供的路徑,處理成配置文件數組(以分號、逗號、空格、tab、換行符分割)
setConfigLocations(configLocations);

if (refresh) {
refresh();
}
}
}

可以看到整體來看源碼比較簡單,只有setConfigLocationsrefresh兩個方法沒有看到具體的實現。但是如果你因為這個而小巧了Spring那可就大錯特錯了,setConfigLocations只是一個開胃小菜,refresh才是我們本文的重點

setConfigLocations

setConfigLocations方法的主要工作有兩個:創建環境對象ConfigurableEnvironment和處理ClassPathXmlApplicationContext傳入的字元串中的占位符

跟著setConfigLocations方法一直往下走

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void setConfigLocations(String... locations) {
if (locations != null) {
Assert.noNullElements(locations, "Config locations must not be null");
this.configLocations = new String[locations.length];
for (int i = 0; i < locations.length; i++) {
//往下看
this.configLocations[i] = resolvePath(locations[i]).trim();
}
}
else {
this.configLocations = null;
}
}


protected String resolvePath(String path) {
return getEnironment().resolveRequiredPlaceholders(path);
}

這裡getEnironment()就涉及到了創建環境變數相關的操作了

獲取環境變數
1
2
3
4
5
6
public ConfigurableEnvironment getEnvironment() {
if (this.environment == null) {
this.environment = createEnvironment();
}
return this.environment;
}

看一下ConfigurableEnvironment這個介面的繼承圖(1張沒能截全,兩張一塊看)
1
1
這個介面比較重要的就是兩部分內容了,一個是設置Spring的環境就是我們經常用的spring.profile配置。另外就是系統資源Property

接著看createEnvironment()方法,發現它返回了一個StandardEnvironment類,而這個類中的customizePropertySources方法就會往資源列表中添加Java進程中的變數和系統的環境變數

1
2
3
4
protected void customizePropertySources(MutablePropertySources propertySources) {
propertySources.addLast(new MapPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
propertySources.addLast(new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
}
處理占位符

再次回到 resolvePath方法後跟進通過上方獲取的ConfigurableEnvironment介面的resolveRequiredPlaceholders方法,終點就是下方的這個方法。這個方法主要就是處理所有使用${}方式的占位符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
protected String parseStringValue(
String value, PlaceholderResolver placeholderResolver, Set<String> visitedPlaceholders) {

StringBuilder result = new StringBuilder(value);
int startIndex = value.indexOf(this.placeholderPrefix);
while (startIndex != -1) {
int endIndex = findPlaceholderEndIndex(result, startIndex);
if (endIndex != -1) {
String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);
String originalPlaceholder = placeholder;
if (!visitedPlaceholders.add(originalPlaceholder)) {
throw new IllegalArgumentException(
"Circular placeholder reference '" + originalPlaceholder + "' in property definitions");
}
placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders);
// Now obtain the value for the fully resolved key...
String propVal = placeholderResolver.resolvePlaceholder(placeholder);
if (propVal == null && this.valueSeparator != null) {
int separatorIndex = placeholder.indexOf(this.valueSeparator);
if (separatorIndex != -1) {
String actualPlaceholder = placeholder.substring(0, separatorIndex);
String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());
propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
if (propVal == null) {
propVal = defaultValue;
}
}
}
if (propVal != null) {
// Recursive invocation, parsing placeholders contained in the
// previously resolved placeholder value.
propVal = parseStringValue(propVal, placeholderResolver, visitedPlaceholders);
result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
if (logger.isTraceEnabled()) {
logger.trace("Resolved placeholder '" + placeholder + "'");
}
startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length());
}
else if (this.ignoreUnresolvablePlaceholders) {
// Proceed with unprocessed value.
startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
}
else {
throw new IllegalArgumentException("Could not resolve placeholder '" +
placeholder + "'" + " in value \"" + value + "\"");
}
visitedPlaceholders.remove(originalPlaceholder);
}
else {
startIndex = -1;
}
}

return result.toString();
}

refresh

配置文件名稱解析完畢後,就到了最關鍵的一步refresh方法。這個方法,接下來會用超級長的篇幅來解析這個方法

先看一下這個方法里大致內容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();

// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);

try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);

// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);

// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);

// Initialize message source for this context.
initMessageSource();

// Initialize event multicaster for this context.
initApplicationEventMulticaster();

// Initialize other special beans in specific context subclasses.
onRefresh();

// Check for listener beans and register them.
registerListeners();

// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);

// Last step: publish corresponding event.
finishRefresh();
}

catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}

// Destroy already created singletons to avoid dangling resources.
destroyBeans();

// Reset 'active' flag.
cancelRefresh(ex);

// Propagate exception to caller.
throw ex;
}

finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}

是不是看著有點懵,不要著急,一行一行往下看,不研究明白誓不罷休

1. synchronized

為了避免refresh() 還沒結束,再次發起啟動或者銷毀容器引起的衝突

2. prepareRefresh()

做一些準備工作,記錄容器的啟動時間、標記“已啟動”狀態、檢查環境變數等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protected void prepareRefresh() {
this.startupDate = System.currentTimeMillis();
this.closed.set(false);
this.active.set(true);

if (logger.isInfoEnabled()) {
logger.info("Refreshing " + this);
}

// 初始化載入配置文件方法,並沒有具體實現,一個留給用戶的擴展點
initPropertySources();

// 檢查環境變數
getEnvironment().validateRequiredProperties();

this.earlyApplicationEvents = new LinkedHashSet<>();
}

其中檢查環境變數的核心方法為,簡單來說就是如果存在環境變數的value為空的時候就拋異常,然後停止啟動Spring

1
2
3
4
5
6
7
8
9
10
11
public void validateRequiredProperties() {
MissingRequiredPropertiesException ex = new MissingRequiredPropertiesException();
for (String key : this.requiredProperties) {
if (this.getProperty(key) == null) {
ex.addMissingRequiredProperty(key);
}
}
if (!ex.getMissingRequiredProperties().isEmpty()) {
throw ex;
}
}

基於這個特性我們可以做一些擴展,提前在集合requiredProperties中放入我們這個項目必須存在的一些環境變數。假說我們的生產環境資料庫地址、用戶名和密碼都是使用環境變數的方式註入進去來代替測試環境的配置,那麼就可以在這裡添加這個校驗,在程式剛啟動的時候就能發現問題

3. obtainFreshBeanFactory()

乍一看這個方法也沒幾行代碼,但是這個方法負責了BeanFactory的初始化、Bean的載入和註冊等事件

1
2
3
4
5
6
7
8
9
10
11
12

protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
// 核心
refreshBeanFactory();

// 返回剛剛創建的 BeanFactory
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
if (logger.isDebugEnabled()) {
logger.debug("Bean factory for " + getDisplayName() + ": " + beanFactory);
}
return beanFactory;
}
BeanFactory

先看refreshBeanFactory()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

protected final void refreshBeanFactory() throws BeansException {
// 判斷當前ApplicationContext是否存在BeanFactory,如果存在的話就銷毀所有 Bean,關閉 BeanFactory
// 註意,一個應用可以存在多個BeanFactory,這裡判斷的是當前ApplicationContext是否存在BeanFactory
if (hasBeanFactory()) {
destroyBeans();
closeBeanFactory();
}
try {
// 初始化DefaultListableBeanFactory
DefaultListableBeanFactory beanFactory = createBeanFactory();
beanFactory.setSerializationId(getId());

// 設置 BeanFactory 的兩個配置屬性:是否允許 Bean 覆蓋、是否允許迴圈引用
customizeBeanFactory(beanFactory);

// 載入 Bean 到 BeanFactory 中
loadBeanDefinitions(beanFactory);
synchronized (this.beanFactoryMonitor) {
this.beanFactory = beanFactory;
}
}
catch (IOException ex) {
throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex);
}
}

這裡一開始就實例化了一個DefaultListableBeanFactory,先看一下這個類的繼承關係
2
可以看到這個哥們的背景相當大,所有關於容器的介面、抽象類他都繼承了。再看他的方法

2
2
2
2
這方法簡直多的嚇人,妥妥的Spring家族超級富二代。看他的方法名稱相信就可以猜出他大部分的功能了

BeanDefinition

在看loadBeanDefinitions()這個方法之前,就必須瞭解一個東西了。那就是:BeanDefinition

我們知道BeanFactory是一個Bean容器,而BeanDefinition就是Bean的一種形式(它裡面包含了Bean指向的類、是否單例、是否懶載入、Bean的依賴關係等相關的屬性)。BeanFactory中就是保存的BeanDefinition。

看BeanDefinition的介面定義

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement {

// Bean的生命周期,預設只提供sington和prototype兩種,在WebApplicationContext中還會有request, session, globalSession, application, websocket 等
String SCOPE_SINGLETON = ConfigurableBeanFactory.SCOPE_SINGLETON;
String SCOPE_PROTOTYPE = ConfigurableBeanFactory.SCOPE_PROTOTYPE;


// 設置父Bean
void setParentName(String parentName);

// 獲取父Bean
String getParentName();

// 設置Bean的類名稱
void setBeanClassName(String beanClassName);

// 獲取Bean的類名稱
String getBeanClassName();


// 設置bean的scope
void setScope(String scope);

String getScope();

// 設置是否懶載入
void setLazyInit(boolean lazyInit);

boolean isLazyInit();

// 設置該Bean依賴的所有Bean
void setDependsOn(String... dependsOn);

// 返回該Bean的所有依賴
String[] getDependsOn();

// 設置該Bean是否可以註入到其他Bean中
void setAutowireCandidate(boolean autowireCandidate);

// 該Bean是否可以註入到其他Bean中
boolean isAutowireCandidate();

// 同一介面的多個實現,如果不指定名字的話,Spring會優先選擇設置primary為true的bean
void setPrimary(boolean primary);

// 是否是primary的
boolean isPrimary();

// 指定工廠名稱
void setFactoryBeanName(String factoryBeanName);
// 獲取工廠名稱
String getFactoryBeanName();
// 指定工廠類中的工廠方法名稱
void setFactoryMethodName(String factoryMethodName);
// 獲取工廠類中的工廠方法名稱
String getFactoryMethodName();

// 構造器參數
ConstructorArgumentValues getConstructorArgumentValues();

// Bean 中的屬性值,後面給 bean 註入屬性值的時候會說到
MutablePropertyValues getPropertyValues();

// 是否 singleton
boolean isSingleton();

// 是否 prototype
boolean isPrototype();

// 如果這個 Bean 是被設置為 abstract,那麼不能實例化,常用於作為 父bean 用於繼承
boolean isAbstract();

int getRole();
String getDescription();
String getResourceDescription();
BeanDefinition getOriginatingBeanDefinition();
}
讀取配置文件

現在可以看loadBeanDefinitions()方法了,這個方法會根據配置,載入各個 Bean,然後放到 BeanFactory 中

1
2
3
4
5
6
7
8
9
10
11
12
@Overrideprotected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException {
// 實例化XmlBeanDefinitionReader
XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory);
beanDefinitionReader.setEnvironment(this.getEnvironment());
beanDefinitionReader.setResourceLoader(this);
beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this));

// 初始化 BeanDefinitionReader
initBeanDefinitionReader(beanDefinitionReader);
// 接著往下看
loadBeanDefinitions(beanDefinitionReader);
}
1
2
3
4
5
6
7
8
9
10
11

protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException {
Resource[] configResources = getConfigResources();
if (configResources != null) {
reader.loadBeanDefinitions(configResources);
}
String[] configLocations = getConfigLocations();
if (configLocations != null) {
reader.loadBeanDefinitions(configLocations);
}
}

第一個if是看有沒有系統指定的配置文件,如果沒有的話就走第二個if載入我們最開始傳入的classpath:application-ioc.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException {
Assert.notNull(resources, "Resource array must not be null");
int counter = 0;
// 迴圈,處理所有配置文件,咱們這裡就傳了一個
for (Resource resource : resources) {
// 繼續往下看
counter += loadBeanDefinitions(resource);
}
// 最後返回載入的所有BeanDefinition的數量
return counter;
}

@Override
public int loadBeanDefinitions(String location) throws BeanDefinitionStoreException {
return loadBeanDefinitions(location, null);
}


public int loadBeanDefinitions(String location, @Nullable Set<Resource> actualResources) throws BeanDefinitionStoreException {
ResourceLoader resourceLoader = getResourceLoader();
if (resourceLoader == null) {
throw new BeanDefinitionStoreException(
"Cannot import bean definitions from location [" + location + "]: no ResourceLoader available");
}

if (resourceLoader instanceof ResourcePatternResolver) {
try {
//將配置文件轉換為Resource對象
Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location);
//接著往下看
int loadCount = loadBeanDefinitions(resources);
if (actualResources != null) {
for (Resource resource : resources) {
actualResources.add(resource);
}
}
if (logger.isDebugEnabled()) {
logger.debug("Loaded " + loadCount + " bean definitions from location pattern [" + location + "]");
}
return loadCount;
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
"Could not resolve bean definition resource pattern [" + location + "]", ex);
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 架構組件:基於Shard Jdbc分庫分表,資料庫擴容方案 一、資料庫擴容 1、業務場景 互聯網項目中有很多“數據量大,業務複雜度高,需要分庫分表”的業務場景。 這樣分層的架構 (1)上層是業務層biz,實現業務邏輯封裝; (2)中間是服務層service,封裝數據訪問; (3)下層是數據層db,存 ...
  • 一、單例模式 1、什麼是單例模式 採取一定的方法,使程式中的某個類只存在一個實例對象,且該類對外提供一個獲取該對象的方法(一般為靜態方法)。 2、單例模式分類 (1)餓漢式(2種寫法,線程安全) 靜態變數 靜態代碼塊 (2)懶漢式(3種寫法) 線程不安全 線程安全,同步方法 線程安全,同步代碼塊(不 ...
  • 單一職責原則 單一職責原則:一個類應該只有一個原因引起改變,即一個類應該只負責一個業務邏輯。 問題由來:類T負責t1, t2兩個職責,當因為t1j對類T修改的時候,可能導致類T出現問題而影響職責t2。 解決方案:遵循單一職責原則,將類T進行改寫,確保一個類負責一個職責。 demo: 有一個類Anim ...
  • 1、線程的狀態 1.1 新生狀態 新生狀態是指new創建了一個新線程,但還沒有調用他的start方法 1.2 可運行狀態 調用了start方法之後,線程就有了運行的機會,處於可運行狀態。 此時線程對象可能正在運行,也可能尚未運行。 為什麼這麼說呢?因為線程的運行方式是一種搶占的運行方式,在同一個時刻 ...
  • 零基礎php開發工程師視頻教程全套,基礎+進階+項目實戰(80G) 下載地址 ...
  • 從小白到python開發工程師,只需這套系統教程就夠了,其它的垃圾教程全部丟掉,丟掉!!! 【零基礎python開發工程師視頻教程全套,基礎+進階+項目實戰,包含課件和源碼】此套教程共154天,共130G,價值13000元, 學完這154天,你的等級就從0(python小白)晉級到5(python中 ...
  • 接著上一篇博客 【RocketMQ中Broker的啟動源碼分析(一)】 在完成準備工作後,調用start方法: 這裡最主要的是通過BrokerController 的start方法來完成啟動 BrokerController的start方法: 首先通過messageStore啟動messageSto ...
  • Java8 新增了 Optional 類,可以更加優雅地解決空指針的問題。 構造器 Optional 的構造器是私有的,不能通過 new 的方式來創建 Optional 對象,因此,Optional 提供了三個靜態方法創建 Optional 對象,分別為 /`of(T value) ofNullab ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...