Spring源碼:bean的生命周期(一)

来源:https://www.cnblogs.com/guoxiaoyu/archive/2023/05/01/17365456.html
-Advertisement-
Play Games

Spring的Bean定義環節是Spring IoC容器中的核心流程之一。在這個過程中,Spring會掃描指定的包路徑,找到符合條件的Bean,並將其轉換為Bean定義。在這個過程中,Spring使用了ASM技術來解析類的註解信息,判斷當前類是否符合要求。然後,Spring將符合條件的Bean定義加... ...


前言

本節將正式介紹Spring源碼細節,將講解Bean生命周期。請註意,雖然我們不希望過於繁瑣地理解Spring源碼,但也不要認為Spring源碼很簡單。在本節中,我們將主要講解Spring 5.3.10版本的源代碼。如果您看到的代碼與我講解的不同,也沒有關係,因為其中的原理和業務邏輯基本相同。為了更好地理解,我們將先講解Bean的生命周期,再講解Spring的啟動原理和流程,因為啟動是準備工作的一部分。

題外話

目前在該版本中,引入了一個名為jfr的JDK技術,類似於Java飛行日誌(JFL),也稱為飛行數據記錄器(Black Box)技術。具體作用不再詳細闡述,讀者可以參考此文:JFR介紹
如果您看到以下代碼,請直接跳過,因為它並沒有太大的作用:

    public AnnotationConfigApplicationContext() {
        StartupStep createAnnotatedBeanDefReader = this.getApplicationStartup().start("spring.context.annotated-bean-reader.create");
        // 額外會創建StandardEnvironment
        this.reader = new AnnotatedBeanDefinitionReader(this);
        createAnnotatedBeanDefReader.end();
        this.scanner = new ClassPathBeanDefinitionScanner(this);
    }

需要註意的是,其中的 StartupStep 關於預設實現並沒有什麼實際作用。但是,還有一種實現方式是 FlightRecorderStartupStep,它是JDK的JFR技術。

Bean的生成過程

生成BeanDefinition

BeanDefinition的作用大家基本通過前面的文章也知道了大概,就是用來描述bean的。
那麼它是如何載入的呢?首先我們看一下 ClassPathBeanDefinitionScanner 類,它是用於掃描的。其中有一個屬性是 BeanDefinitionRegistry,即Bean定義的註冊類。預設實現是 DefaultListableBeanFactory,但是在 ClassPathBeanDefinitionScanner 類中並沒有直接使用該類作為屬性,而是使用了它的父介面 BeanDefinitionRegistry。這是因為 ClassPathBeanDefinitionScanner 類實際上並沒有使用 BeanDefinitionRegistry 介面中的許多方法來註冊Bean定義。

接下來,我們來分析 ClassPathBeanDefinitionScanner 類的 scan 方法:

protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
        Assert.notEmpty(basePackages, "At least one base package must be specified");
        Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
        for (String basePackage : basePackages) {

            Set<BeanDefinition> candidates = findCandidateComponents(basePackage);

            for (BeanDefinition candidate : candidates) {
                ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
                candidate.setScope(scopeMetadata.getScopeName());

                String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);

                if (candidate instanceof AbstractBeanDefinition) {
                    postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
                }
                if (candidate instanceof AnnotatedBeanDefinition) {
                    // 解析@Lazy、@Primary、@DependsOn、@Role、@Description
                    AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
                }

                // 檢查Spring容器中是否已經存在該beanName
                if (checkCandidate(beanName, candidate)) {
                    BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
                    definitionHolder =
                            AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
                    beanDefinitions.add(definitionHolder);

                    // 註冊
                    registerBeanDefinition(definitionHolder, this.registry);
                }
            }
        }
        return beanDefinitions;
    }

大致邏輯如下:

  1. 獲取掃描包路徑
  2. findCandidateComponents:獲取符合條件的bean
  3. 遍歷candidate(候選bean),由於第二部使用了ASM技術,所以並沒有真正獲取beanclass而是使用了beanname替代所以,遍歷的做法就是將符合條件的bean定義進行註冊。
  4. scopeMetadata解析scope註解。
  5. beanNameGenerator構建當前bean的唯一名字。
  6. postProcessBeanDefinition這裡其實就是進行預設值賦值。
  7. processCommonDefinitionAnnotations進行解析@Lazy、@Primary、@DependsOn、@Role、@Description
  8. checkCandidate(beanName, candidate)再次檢查是否該beanName已經註冊過。
  9. registerBeanDefinition,註冊到我們的DefaultListableBeanFactory的BeanDefinitionMap中。

其實這裡基本就已經大概瞭解 的差不多了,然後再繼續講解下每一個流程裡面都走了那些邏輯:

findCandidateComponents

其主要邏輯會進入如下源碼:

private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
        Set<BeanDefinition> candidates = new LinkedHashSet<>();
        try {
            // 獲取basePackage下所有的文件資源
            String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
                    resolveBasePackage(basePackage) + '/' + this.resourcePattern;

            Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);

            boolean traceEnabled = logger.isTraceEnabled();
            boolean debugEnabled = logger.isDebugEnabled();
            for (Resource resource : resources) {
                if (traceEnabled) {
                    logger.trace("Scanning " + resource);
                }
                if (resource.isReadable()) {
                    try {
                        MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
                        // excludeFilters、includeFilters判斷
                        if (isCandidateComponent(metadataReader)) { // @Component-->includeFilters判斷
                            ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
                            sbd.setSource(resource);

                            if (isCandidateComponent(sbd)) {
                                if (debugEnabled) {
                                    logger.debug("Identified candidate component class: " + resource);
                                }
                                candidates.add(sbd);
                            }
                            //此處省略部分代碼
                            ......
        }
        return candidates;
    }

讓我們來深入瞭解一下Bean掃描的具體細節。以下是主要流程:

  1. 獲取basePackage下所有的文件資源。以下分析註解信息等都是用到了ASM技術,並沒有真正的去載入這個類。
  2. isCandidateComponent(metadataReader),進行判斷是否當前類具有@component註解。
  3. isCandidateComponent(sbd),進行判斷是否當前類屬於內部類、介面、抽象類
  4. 符合上述條件則會加入到bean定義候選集合中。

ASM技術這裡不做多解釋,主要看下Spring是如何進行判斷校驗當前bean是否符合條件的,第一個 isCandidateComponent(metadataReader)方法:

	protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
		for (TypeFilter tf : this.excludeFilters) {
			if (tf.match(metadataReader, getMetadataReaderFactory())) {
				return false;
			}
		}
		// 符合includeFilters的會進行條件匹配,通過了才是Bean,也就是先看有沒有@Component,再看是否符合@Conditional
		for (TypeFilter tf : this.includeFilters) {
			if (tf.match(metadataReader, getMetadataReaderFactory())) {
				return isConditionMatch(metadataReader);
			}
		}
		return false;
	}

那麼includeFilters預設會在啟動AnnotationConfigApplicationContext時就會預設註冊一個解析Component註解的filter,代碼如下:

protected void registerDefaultFilters() {  
  
   // 註冊@Component對應的AnnotationTypeFilter  
  this.includeFilters.add(new AnnotationTypeFilter(Component.class));  
  
   ClassLoader cl = ClassPathScanningCandidateComponentProvider.class.getClassLoader();  
  
   try {  
      this.includeFilters.add(new AnnotationTypeFilter(  
            ((Class<? extends Annotation>) ClassUtils.forName("javax.annotation.ManagedBean", cl)), false));  
      logger.trace("JSR-250 'javax.annotation.ManagedBean' found and supported for component scanning");  
   }  
   catch (ClassNotFoundException ex) {  
      // JSR-250 1.1 API (as included in Java EE 6) not available - simply skip.  
  }  
  
   try {  
      this.includeFilters.add(new AnnotationTypeFilter(  
            ((Class<? extends Annotation>) ClassUtils.forName("javax.inject.Named", cl)), false));  
      logger.trace("JSR-330 'javax.inject.Named' annotation found and supported for component scanning");  
   }  
   catch (ClassNotFoundException ex) {  
      // JSR-330 API not available - simply skip.  
  }  
}

如果符合過濾條件,那麼他就會開始生成最初的bean定義:

public ScannedGenericBeanDefinition(MetadataReader metadataReader) {  
   Assert.notNull(metadataReader, "MetadataReader must not be null");  
   this.metadata = metadataReader.getAnnotationMetadata();  
   // 這裡只是把className設置到BeanDefinition中  
  setBeanClassName(this.metadata.getClassName());  
   setResource(metadataReader.getResource());  
}

那麼就剩下最後的校驗了:isCandidateComponent(sbd);再來看看他的作用子什麼:

protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {  
   AnnotationMetadata metadata = beanDefinition.getMetadata();  
   return (metadata.isIndependent() && (metadata.isConcrete() ||  
         (metadata.isAbstract() && metadata.hasAnnotatedMethods(Lookup.class.getName()))));  
}

看完一臉懵,那我們就好好解釋一下每個判斷都是什麼意思吧:

  1. metadata.isIndependent():是否當前類為內部類,眾說周知java語言編譯內部類的時候會產生兩個class文件,比如下麵這樣:
    image
    那麼這個Member內部類是不會被Spring作為單獨的類去掃描的。除非也加上@component註解,並且為static內部類
  2. metadata.isConcrete():這個類就是判斷下是否是介面還是抽象類
  3. metadata.hasAnnotatedMethods(Lookup.class.getName()):判斷是否有方法是帶有Lookup註解的,Lookup註解工作中用到的確實有些少,我在這裡簡要說明下:比如我們註冊給Spring的bean都是單例,但是如果我們有多例的bean被一個單例的bean所依賴的話,一次屬性只能註入一次,也打不到多例的效果,這時候就可以用Lookup註解實現了,比如這樣:
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);  
 //UserService 為單例
UserService bean = applicationContext.getBean(UserService.class);  
bean.test();  
bean.test();
@Component  
public class UserService {  

   @Autowired  
  private User user;  
  
   public void test(){  
      System.out.println(user);  
   }  
}
 @Component  
@Scope("prototype")  
public class User {  
  
}

如果這樣執行的話,你永遠拿不到多例的User類,因為UserService 在屬性依賴註入的時候已經做完了賦值,每次調用拿到的都是同一個對象,那如果不這麼做可不可以,當然可以:比如這樣改下UserService 類:

@Component  
public class UserService {  
  
   @Autowired  
  private User user;  
  
   public void test(){  
      System.out.println(get());  
   }  
  
   @Lookup  
  public User get(){  
      return null;  
   }  
}

至於為什麼這裡返回null,後續在進行講解,只要知道他的註解的作用即可。到此我們終於解決完了findCandidateComponents方法找出了符合條件的bean定義。

generateBeanName

獲取當前bean的beanname,源碼如下:

public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {  
   if (definition instanceof AnnotatedBeanDefinition) {  
      // 獲取註解所指定的beanName  
  String beanName = determineBeanNameFromAnnotation((AnnotatedBeanDefinition) definition);  
      if (StringUtils.hasText(beanName)) {  
         // Explicit bean name found.  
  return beanName;  
      }  
   }  
   // Fallback: generate a unique default bean name.  
  return buildDefaultBeanName(definition, registry);  
}

buildDefaultBeanName構建註解我們寫 beanname這個不難理解,如果沒有那麼就會走預設的構建,那麼這裡是jdk提供的,有個點需要註意下如果首字母和第二個字母都是大寫那麼名字直接return,如果你寫的是ABtest,那麼beanname就是ABtest:

public static String decapitalize(String name) {  
    if (name == null || name.length() == 0) {  
        return name;  
    }  
    if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) &&  
                    Character.isUpperCase(name.charAt(0))){  
        return name;  
    }  
    char chars[] = name.toCharArray();  
    chars[0] = Character.toLowerCase(chars[0]);  
    return new String(chars);  
}

postProcessBeanDefinition

這裡主要是進行設置BeanDefinition的預設值,直接看源碼就能看懂:

public void applyDefaults(BeanDefinitionDefaults defaults) {  
   Boolean lazyInit = defaults.getLazyInit();  
   if (lazyInit != null) {  
      setLazyInit(lazyInit);  
   }  
   setAutowireMode(defaults.getAutowireMode());  
   setDependencyCheck(defaults.getDependencyCheck());  
   setInitMethodName(defaults.getInitMethodName());  
   setEnforceInitMethod(false);  
   setDestroyMethodName(defaults.getDestroyMethodName());  
   setEnforceDestroyMethod(false);  
}

processCommonDefinitionAnnotations

這一步主要時進行解析類上的註解,看源碼也可以基本看懂,沒有太多繞的邏輯,如下展示:

static void processCommonDefinitionAnnotations(AnnotatedBeanDefinition abd, AnnotatedTypeMetadata metadata) {  
   AnnotationAttributes lazy = attributesFor(metadata, Lazy.class);  
   if (lazy != null) {  
      abd.setLazyInit(lazy.getBoolean("value"));  
   }  
   else if (abd.getMetadata() != metadata) {  
      lazy = attributesFor(abd.getMetadata(), Lazy.class);  
      if (lazy != null) {  
         abd.setLazyInit(lazy.getBoolean("value"));  
      }  
   }  
  
   if (metadata.isAnnotated(Primary.class.getName())) {  
      abd.setPrimary(true);  
   }  
   AnnotationAttributes dependsOn = attributesFor(metadata, DependsOn.class);  
   if (dependsOn != null) {  
      abd.setDependsOn(dependsOn.getStringArray("value"));  
   }  
  
   AnnotationAttributes role = attributesFor(metadata, Role.class);  
   if (role != null) {  
      abd.setRole(role.getNumber("value").intValue());  
   }  
   AnnotationAttributes description = attributesFor(metadata, Description.class);  
   if (description != null) {  
      abd.setDescription(description.getString("value"));  
   }  
}

checkCandidate

這一步主要是檢查是否我們的bean定義map註冊中已經存在了,不過我們工作中基本上都會通過,如果存在多個那會拋異常:

protected boolean checkCandidate(String beanName, BeanDefinition beanDefinition) throws IllegalStateException {  
   if (!this.registry.containsBeanDefinition(beanName)) {  
      return true;  
   }  
   BeanDefinition existingDef = this.registry.getBeanDefinition(beanName);  
   BeanDefinition originatingDef = existingDef.getOriginatingBeanDefinition();  
   if (originatingDef != null) {  
      existingDef = originatingDef;  
   }  
   // 是否相容,如果相容返回false表示不會重新註冊到Spring容器中,如果不衝突則會拋異常。  
  if (isCompatible(beanDefinition, existingDef)) {  
      return false;  
   }  
   throw new ConflictingBeanDefinitionException("Annotation-specified bean name '" + beanName +  
         "' for bean class [" + beanDefinition.getBeanClassName() + "] conflicts with existing, " +  
         "non-compatible bean definition of same name and class [" + existingDef.getBeanClassName() + "]");  
}

可是你像我這樣下麵寫的話就不會出現異常,但是工作中肯定也不會這麼用,這裡只做展示,AppConfig和AppConfig1都是對同樣的配置,會對同一個包路徑掃描兩次:

AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();  
applicationContext.register(AppConfig.class);  
applicationContext.register(AppConfig1.class);  
applicationContext.refresh();  
UserService bean = applicationContext.getBean(UserService.class);  
bean.test();

registerBeanDefinition

那麼最後當前的bean定義正式生成,並註冊到我們之前經常說的DefaultListableBeanFactory的Map<String, BeanDefinition> beanDefinitionMap屬性。

public static void registerBeanDefinition(  
      BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry)  
      throws BeanDefinitionStoreException {  
  
   // Register bean definition under primary name.  
  String beanName = definitionHolder.getBeanName();  
   registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition());  
  
   // Register aliases for bean name, if any.  
  String[] aliases = definitionHolder.getAliases();  
   if (aliases != null) {  
      for (String alias : aliases) {  
         registry.registerAlias(beanName, alias);  
      }  
   }  
}

合併bean定義

這個是非常重要的一個步驟,所以單獨拿出來說下,這個步驟也是獲取bean之前的最後一個對bean定義做修改的地方,getMergedLocalBeanDefinition(beanName);通過beanname來獲取合併後的bean定義,這是什麼意思呢?看下源碼:

protected RootBeanDefinition getMergedBeanDefinition(  
      String beanName, BeanDefinition bd, @Nullable BeanDefinition containingBd)  
      throws BeanDefinitionStoreException {  
  
   synchronized (this.mergedBeanDefinitions) {  
      RootBeanDefinition mbd = null;  
      RootBeanDefinition previous = null;  
  
      // Check with full lock now in order to enforce the same merged instance.  
  if (containingBd == null) {  
         mbd = this.mergedBeanDefinitions.get(beanName);  
      }  
  
      if (mbd == null || mbd.stale) {  
         previous = mbd;  
         if (bd.getParentName() == null) {  
            // Use copy of given root bean definition.  
  if (bd instanceof RootBeanDefinition) {  
               mbd = ((RootBeanDefinition) bd).cloneBeanDefinition();  
            }  
            else {  
               mbd = new RootBeanDefinition(bd);  
            }  
         }  
         else {  
            // Child bean definition: needs to be merged with parent.  
 // pbd表示parentBeanDefinition  
  BeanDefinition pbd;  
            try {  
               String parentBeanName = transformedBeanName(bd.getParentName());  
               if (!beanName.equals(parentBeanName)) {  
                  pbd = getMergedBeanDefinition(parentBeanName);  
               }  
               else {  
                  BeanFactory parent = getParentBeanFactory();  
                  if (parent instanceof ConfigurableBeanFactory) {  
                     pbd = ((ConfigurableBeanFactory) parent).getMergedBeanDefinition(parentBeanName);  
                  }  
                  else {  
                     ......
                    }  
               }  
            }  
            catch (NoSuchBeanDefinitionException ex) {  
               ......
            }  
  
            // Deep copy with overridden values.  
 // 子BeanDefinition的屬性覆蓋父BeanDefinition的屬性,這就是合併  
  mbd = new RootBeanDefinition(pbd);  
            mbd.overrideFrom(bd);  
         }  
  
         // Set default singleton scope, if not configured before.  
  if (!StringUtils.hasLength(mbd.getScope())) {  
            mbd.setScope(SCOPE_SINGLETON);  
         }  
  
  if (containingBd != null && !containingBd.isSingleton() && mbd.isSingleton()) {  
            mbd.setScope(containingBd.getScope());  
         }  
  
		 if (containingBd == null && isCacheBeanMetadata()) {  
            this.mergedBeanDefinitions.put(beanName, mbd);  
         }  
      }  
      if (previous != null) {  
         copyRelevantMergedBeanDefinitionCaches(previous, mbd);  
      }  
      return mbd;  
   }  
}

為什麼需要進行合併bean定義,因為每個bean都會可能被其他聲明bean所引用,因為我們在工作中用到的都是註解形式,所以很少註意,我們看下Spring是xml時代時寫的聲明bean:

<bean id="user1" class="com.xiaoyu.service.User" scope="prototype"/>  
  
<bean id="user2" class="com.xiaoyu.service.User" parent="user1"/>

然後我們在看下合併bean定義的源碼邏輯:

  1. 判斷是否有parent,沒有的話就正常包裝原始bean定義為RootBeanDefinition。
  2. 如果有parent,那麼在判斷其父類是否還有父類,如果有則遞歸合併bean定義方法。
  3. 最關鍵的其實是這個代碼:先以父類的bean定義生成RootBeanDefinition,然後如果子類定義了某個屬性的話那就覆蓋父類的bean定義。
    mbd = new RootBeanDefinition(pbd); mbd.overrideFrom(bd);
public void overrideFrom(BeanDefinition other) {  
   if (StringUtils.hasLength(other.getBeanClassName())) {  
      setBeanClassName(other.getBeanClassName());  
   }  
   if (StringUtils.hasLength(other.getScope())) {  
      setScope(other.getScope());  
   }  
   setAbstract(other.isAbstract());  
   if (StringUtils.hasLength(other.getFactoryBeanName())) {  
      setFactoryBeanName(other.getFactoryBeanName());  
   }  
   if (StringUtils.hasLength(other.getFactoryMethodName())) {  
      setFactoryMethodName(other.getFactoryMethodName());  
   }  
   setRole(other.getRole());  
   setSource(other.getSource());  
   copyAttributesFrom(other);  
  //此處省略部分代碼
   ......
}
  1. 最後將包裝的bean定義放入mergedBeanDefinitions合併定義的map中。

現在我們的的Spring中 了兩個bean定義Map,那麼在啟動時進行創建bean時用到的都是合併後的bean定義map。

結語

那麼現在Spring的Bean定義環節就基本講解完畢了。其實最主要的是Spring是如何判斷是否將Bean定義加入,並生成Bean定義,以及最後如何使用合併的Bean定義來包裝原始的Bean定義。下一節我們將開始講解Spring的Bean實例化。
公眾號

ps:以上內容,純屬個人見解,有任何問題下方評論!關註博主公眾號,源碼專題、面試精選、AI最新擴展等你來看!原創編寫不易,轉載請說明出處!
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • (初探MySQL) 前言 周所周知MySQL已成為全世界最受歡迎的資料庫之一。無論你用的何種編程語言在開發系統,資料庫基本上都是必不可少的。 無論是小型項目開發如我們開發一個個人博客系統,還是構建那些聲名顯赫的網站如某寶、某訊等,MySQL都有著穩定、可靠、快速等優點。可以勝任數據存儲的業務需求。 ...
  • 1. 消滅NULL 1.1. NULL惹人討厭的原因 1.1.1. 進行SQL編碼時,必須考慮違反人類直覺的三值邏輯 1.1.2. 指定IS NULL、IS NOT NULL的時候,不會用到索引,SQL語句執行起來性能低下 1.1.2.1. 1 + NULL = NULL 2- NULL = NUL ...
  • 前言 地址:https://www.cnblogs.com/FReQuenter5156/p/setblog.html/ 如題,使用的是 Simple Memory 主題。 Github 連接:https://github.com/BNDong/Cnblogs-Theme-SimpleMemory。 ...
  • 嘿嘿嘿、嘿嘿,俺又回來了! github代碼地址 https://github.com/Tom-shushu/work-study 介面文檔有道雲 https://note.youdao.com/s/GShGsYE8 介面文檔離線版本 https://files.cnblogs.com/files/ ...
  • FactoryBean 和 BeanFactory 是兩個不同的概念。前者是一個介面,我們可以在實現該介面時通過調用 getObject 方法來返回實例,同時 FactoryBean 本身也是一個實例。後者是 Spring 容器的工廠,通過其中的 bean 定義 Map 一個一個地實例化我們通過註解... ...
  • Springboot的優點 內置servlet容器,不需要在伺服器部署 tomcat。只需要將項目打成 jar 包,使用 java -jar xxx.jar一鍵式啟動項目 SpringBoot提供了starter,把常用庫聚合在一起,簡化複雜的環境配置,快速搭建spring應用環境 可以快速創建獨立 ...
  • 列印 print("hello world") 註釋 單行註釋 多行註釋 -- 這是單行註釋 --[[ 這是多行註釋 ]] 賦值 s="Hello World" -- 多重賦值 a,b="String a","String b" -- 交換值,類似python a,b="String a","Str ...
  • Redis連環40問,絕對夠全! Redis是什麼? Redis(Remote Dictionary Server)是一個使用 C 語言編寫的,高性能非關係型的鍵值對資料庫。與傳統資料庫不同的是,Redis 的數據是存在記憶體中的,所以讀寫速度非常快,被廣泛應用於緩存方向。Redis可以將數據寫入磁碟 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...