0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

如何在實(shí)際的代碼中使Spring組件的特性?

OSC開源社區(qū) ? 來源:OSCHINA 社區(qū) ? 2023-08-11 09:52 ? 次閱讀

首先,我們將探討一些Spring框架中IOC(Inversion of Control)的高級特性,特別是組件掃描的相關(guān)知識。組件掃描是Spring框架中一個(gè)重要的特性,它可以自動(dòng)檢測并實(shí)例化帶有特定注解(如@Component,@Service,@Controller等)的類,并將它們注冊為Spring上下文中的bean。這里,我們會通過一些詳細(xì)的例子來闡明這些概念,并且展示如何在實(shí)際的代碼中使用這些特性。

1. 組件掃描路徑

@ComponentScan注解是用于指定Spring在啟動(dòng)時(shí)需要掃描的包路徑,從而自動(dòng)發(fā)現(xiàn)并注冊組件。 我們設(shè)置組件掃描路徑包括兩種方式:

直接指定包名:如@ComponentScan("com.example.demo"),等同于@ComponentScan(basePackages = {"com.example.demo"}),Spring會掃描指定包下的所有類,并查找其中帶有@Component、@Service、@Repository等注解的組件,然后將這些組件注冊為Spring容器的bean。

指定包含特定類的包:如@ComponentScan(basePackageClasses = {ExampleService.class}),Spring會掃描ExampleService類所在的包以及其所有子包。

接下來,給出了一個(gè)完整的例子,說明如何使用第二種方式來設(shè)置組件掃描路徑。這可以通過設(shè)置@ComponentScan的basePackageClasses屬性來實(shí)現(xiàn)。例如:

@Configuration
@ComponentScan(basePackageClasses = {ExampleService.class})
public class BasePackageClassConfiguration {
}
以上代碼表示,Spring會掃描ExampleService類所在的包以及其所有子包。 全部代碼如下: 首先,我們創(chuàng)建一個(gè)名為ExampleService的服務(wù)類
package com.example.demo.service;

import org.springframework.stereotype.Service;

@Service
public class ExampleService {
}
接著在bean目錄下創(chuàng)建DemoDao
package com.example.demo.bean;

import org.springframework.stereotype.Component;

@Component
public class DemoDao {
}
然后在配置類中設(shè)置組件掃描路徑
package com.example.demo.configuration;

import com.example.demo.service.ExampleService;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackageClasses = ExampleService.class)
public class BasePackageClassConfiguration {
}
我們還會創(chuàng)建一個(gè)名為DemoApplication的類,這個(gè)類的作用是啟動(dòng)Spring上下文并打印所有注冊的bean的名稱。
package com.example.demo;

import com.example.demo.configuration.BasePackageClassConfiguration;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import java.util.Arrays;

public class DemoApplication {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new
                AnnotationConfigApplicationContext(BasePackageClassConfiguration.class);
        String[] beanDefinitionNames = ctx.getBeanDefinitionNames();
        Arrays.stream(beanDefinitionNames).forEach(System.out::println);
    }
}
運(yùn)行上述DemoApplication類的main方法,就會在控制臺上看到所有注冊的bean的名稱,包括我們剛剛創(chuàng)建的ExampleService。

fc7dfe12-3770-11ee-9e74-dac502259ad0.png

現(xiàn)在,如果我們在ExampleService類所在的包或者其任意子包下創(chuàng)建一個(gè)新的類(例如TestService),那么這個(gè)組件類也會被自動(dòng)注冊為一個(gè)bean。這就是basePackageClasses屬性的作用:它告訴Spring要掃描哪些包以及其子包。
package com.example.demo.service;

import org.springframework.stereotype.Service;

@Service
public class TestService {
}
如果再次運(yùn)行DemoApplication類的main方法,就會看到TestService也被打印出來,說明它也被成功注冊為了一個(gè)bean。

fcc2351e-3770-11ee-9e74-dac502259ad0.png

我們可以看到這個(gè)DemoDao始終沒有被掃描到,我們看一下@ComponentScan注解的源碼

fcea8212-3770-11ee-9e74-dac502259ad0.png

可以看到basePackageClasses屬性這是一個(gè)數(shù)組類型的,有人會疑問了,剛剛我們寫的@ComponentScan(basePackageClasses = ExampleService.class),這沒有用到數(shù)組啊,為什么這里還能正常運(yùn)行呢? 如果數(shù)組只包含一個(gè)元素,可以在賦值時(shí)省略數(shù)組的大括號{},這是Java的一種語法糖。在這種情況下,編譯器會自動(dòng)把該元素包裝成一個(gè)數(shù)組。 例如,以下兩種寫法是等價(jià)的:
@ComponentScan(basePackageClasses = {ExampleService.class})

@ComponentScan(basePackageClasses = ExampleService.class)
在上面兩種情況下,ExampleService.class都會被包裝成一個(gè)只包含一個(gè)元素的數(shù)組。這是Java語法的一個(gè)便利特性,使得代碼在只有一個(gè)元素的情況下看起來更加簡潔。 那么為了DemoDao組件被掃描到,我們可以在basePackageClasses屬性加上DemoDao類,這樣就可以掃描DemoDao組件所在的包以及它的子包。
package com.example.demo.configuration;

import com.example.demo.bean.DemoDao;
import com.example.demo.service.ExampleService;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackageClasses = {ExampleService.class, DemoDao.class})
public class BasePackageClassConfiguration {
}
運(yùn)行結(jié)果

fd037b64-3770-11ee-9e74-dac502259ad0.png

@ComponentScan注解的源碼還有不少,后面我們用到再說

2. 按注解過濾組件(包含)

除了基本的包路徑掃描,Spring還提供了過濾功能,允許我們通過設(shè)定過濾規(guī)則,只包含或排除帶有特定注解的類。

這個(gè)過濾功能對于大型項(xiàng)目中的模塊劃分非常有用,可以精細(xì)控制Spring的組件掃描范圍,優(yōu)化項(xiàng)目啟動(dòng)速度。 在Spring中可以通過@ComponentScan的includeFilters屬性來實(shí)現(xiàn)注解包含過濾,只包含帶有特定注解的類。

在下面這個(gè)例子中,我們將創(chuàng)建一些帶有特定注解的組件,并設(shè)置一個(gè)配置類來掃描它們。 全部代碼如下: 創(chuàng)建一個(gè)新的注解Species:

package com.example.demo.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Species {
}
接下來,我們將創(chuàng)建三個(gè)不同的組件,其中兩個(gè)包含Species注解:
package com.example.demo.bean;

import com.example.demo.annotation.Species;
import org.springframework.stereotype.Component;

@Species
public class Elephant {
}
Elephant類被@Species修飾,沒有@Component修飾。
package com.example.demo.bean;

import org.springframework.stereotype.Component;

@Component
public class Monkey {
}
Monkey只被@Component修飾
package com.example.demo.bean;

import com.example.demo.annotation.Species;
import org.springframework.stereotype.Component;

@Component
@Species
public class Tiger {
}
如上所示,Tiger有@Component和@Species修飾。 接著,我們需要?jiǎng)?chuàng)建一個(gè)配置類,用于掃描和包含有Species注解的組件:
package com.example.demo.configuration;

import com.example.demo.annotation.Species;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;

@Configuration
@ComponentScan(basePackages = "com.example.demo",
        includeFilters = @Filter(type = FilterType.ANNOTATION, classes = Species.class),
        useDefaultFilters = false)
public class FilterConfiguration {
}
在這個(gè)配置類中,我們設(shè)置了@ComponentScan注解的includeFilters屬性,F(xiàn)ilterType.ANNOTATION代表按注解過濾,這里用于掃描包含所有帶有Species注解的組件。注意,useDefaultFilters = false表示禁用了默認(rèn)的過濾規(guī)則,只會包含標(biāo)記為Species的組件。

有人可能會疑問了,useDefaultFilters = false表示禁用了默認(rèn)的過濾規(guī)則,什么是默認(rèn)的過濾規(guī)則? 在Spring中,當(dāng)使用@ComponentScan注解進(jìn)行組件掃描時(shí),Spring提供了默認(rèn)的過濾規(guī)則。這些默認(rèn)規(guī)則包括以下幾種類型的注解:

@Component

@Repository

@Service

@Controller

@RestController

@Configuration

默認(rèn)不寫useDefaultFilters屬性的情況下,useDefaultFilters屬性的值為true,Spring在進(jìn)行組件掃描時(shí)會默認(rèn)包含以上注解標(biāo)記的組件,如果將useDefaultFilters設(shè)置為false,Spring就只會掃描明確指定過濾規(guī)則的組件,不再包括以上默認(rèn)規(guī)則的組件。

所以,useDefaultFilters = false是在告訴Spring我們只想要自定義組件掃描規(guī)則。 最后,我們創(chuàng)建一個(gè)主程序,來實(shí)例化應(yīng)用上下文并列出所有的Bean名稱:

package com.example.demo;

import com.example.demo.configuration.FilterConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class DemoApplication {

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(FilterConfiguration.class);
        String[] beanNames = ctx.getBeanDefinitionNames();
        for (String beanName : beanNames) {
            System.out.println(beanName);
        }
    }
}
當(dāng)我們運(yùn)行這個(gè)程序時(shí),會看到輸出的Bean名字只包括被Species注解標(biāo)記的類。在這個(gè)例子中會看到Tiger和Elephant,但不會看到Monkey,因?yàn)槲覀兊呐渲弥话吮籗pecies注解的類。 運(yùn)行結(jié)果:

fd2c2a5a-3770-11ee-9e74-dac502259ad0.png

如果useDefaultFilters屬性設(shè)置為true,那么運(yùn)行程序時(shí)輸出的Bean名字將會包括Monkey。 總結(jié):上面介紹了如何使用Spring的@ComponentScan注解中的includeFilters屬性和useDefaultFilters屬性來過濾并掃描帶有特定注解的類。

通過自定義注解和在配置類中設(shè)置相關(guān)屬性,可以精確地控制哪些類被Spring容器掃描和管理。如果設(shè)置useDefaultFilters為false,則Spring只掃描被明確指定過濾規(guī)則的組件,不再包含默認(rèn)規(guī)則(如@Component、@Service等)的組件。

3. 按注解過濾組件(排除)

在Spring框架中,我們不僅可以通過@ComponentScan注解的includeFilters屬性設(shè)置包含特定注解的類,還可以通過excludeFilters屬性來排除帶有特定注解的類。這個(gè)功能對于我們自定義模塊的加載非常有用,我們可以通過這種方式,精確控制哪些組件被加載到Spring的IOC容器中。

下面我們將通過一個(gè)具體的示例來說明如何使用@ComponentScan的excludeFilters屬性來排除帶有特定注解的類。 全部代碼如下: 首先,創(chuàng)建一個(gè)注解Exclude:

package com.example.demo.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Exclude {
}
定義三個(gè)類Elephant、Monkey、Tiger
package com.example.demo.bean;

import com.example.demo.annotation.Exclude;
import org.springframework.stereotype.Component;

@Component
@Exclude
public class Elephant {
}
package com.example.demo.bean;

import org.springframework.stereotype.Component;

@Component
public class Monkey {
}
package com.example.demo.bean;

import org.springframework.stereotype.Component;

@Component
public class Tiger {
}
注意,這幾個(gè)類都標(biāo)記為@Component,Elephant類上有@Exclude注解。 接下來,我們創(chuàng)建配置類FilterConfiguration,在其中使用@ComponentScan注解,并通過excludeFilters屬性排除所有標(biāo)記為@Exclude的類:
package com.example.demo.configuration;

import com.example.demo.annotation.Exclude;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;

@Configuration
@ComponentScan(basePackages = "com.example.demo",
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Exclude.class))
public class FilterConfiguration {
}
這樣,在Spring IOC容器中,只有Tiger和Monkey類會被掃描并實(shí)例化,因?yàn)镋lephant類被@Exclude注解標(biāo)記,而我們在FilterConfiguration類中排除了所有被@Exclude注解標(biāo)記的類。 主程序?yàn)椋?
package com.example.demo;

import com.example.demo.configuration.FilterConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class DemoApplication {

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(FilterConfiguration.class);
        String[] beanNames = ctx.getBeanDefinitionNames();
        for (String beanName : beanNames) {
            System.out.println(beanName);
        }
    }
}
運(yùn)行結(jié)果:

fd681ee8-3770-11ee-9e74-dac502259ad0.png

總結(jié):這小節(jié)主要講解了如何在Spring框架中通過@ComponentScan注解的excludeFilters屬性進(jìn)行注解過濾,以精確控制加載到Spring IOC容器中的組件。在本小節(jié)的示例中,我們首先創(chuàng)建了一個(gè)名為Exclude的注解,然后定義了三個(gè)類Elephant、Monkey、和Tiger,它們都被標(biāo)記為@Component,其中Elephant類上還有一個(gè)@Exclude注解。

接下來,我們創(chuàng)建了一個(gè)配置類FilterConfiguration,其中使用了@ComponentScan注解,并通過excludeFilters屬性排除了所有標(biāo)記為@Exclude的類。因此,當(dāng)程序運(yùn)行時(shí),Spring IOC容器中只有Tiger和Monkey類會被掃描并實(shí)例化,因?yàn)镋lephant類被@Exclude注解標(biāo)記,所以被排除了。

4. 通過正則表達(dá)式過濾組件

在Spring框架中,除了可以通過指定注解來進(jìn)行包含和排除類的加載,我們還可以利用正則表達(dá)式來對組件進(jìn)行過濾。這種方式提供了一種更靈活的方式來選擇需要被Spring IOC容器管理的類。具體來說,可以利用正則表達(dá)式來包含或者排除名稱符合某個(gè)特定模式的類。

下面,我們將通過一個(gè)具體的例子來展示如何使用正則表達(dá)式過濾來只包含類名以特定字符串結(jié)尾的類。 下面的例子將演示如何只包含類名以Tiger結(jié)尾的類。 全部代碼如下: 定義三個(gè)類Tiger、Elephant、Monkey

package com.example.demo.bean;

import org.springframework.stereotype.Component;

@Component
public class Elephant {
}
package com.example.demo.bean;

import org.springframework.stereotype.Component;

@Component
public class Monkey {
}
package com.example.demo.bean;

import org.springframework.stereotype.Component;

@Component
public class Tiger {
}
接著我們創(chuàng)建配置類FilterConfiguration,使用@ComponentScan注解,并通過includeFilters屬性來包含類名以Tiger結(jié)尾的類:
package com.example.demo.configuration;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;

@Configuration
@ComponentScan(basePackages = "com.example.demo",
        useDefaultFilters = false,
        includeFilters = @ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*Tiger"))
public class FilterConfiguration {
}
在上述示例中,我們使用FilterType.REGEX過濾類型,并指定要包含的正則表達(dá)式模式".*Tiger"。結(jié)果只會有Tiger類會被Spring的IOC容器掃描并實(shí)例化,因?yàn)橹挥蠺iger類的類名滿足正則表達(dá)式".*Tiger"。這里.代表任意單個(gè)字符 (除了換行符),*代表前一個(gè)字符重復(fù)任意次,.*組合起來表示匹配任意個(gè)任意字符。 主程序:
package com.example.demo;

import com.example.demo.configuration.FilterConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class DemoApplication {

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(FilterConfiguration.class);
        String[] beanNames = ctx.getBeanDefinitionNames();
        for (String beanName : beanNames) {
            System.out.println(beanName);
        }
    }
}
運(yùn)行結(jié)果

fdb5eab0-3770-11ee-9e74-dac502259ad0.png

總結(jié):本小節(jié)介紹了如何在Spring框架中使用正則表達(dá)式對組件進(jìn)行過濾,以選擇哪些類應(yīng)被Spring IOC容器管理。在所給示例中,首先定義了三個(gè)類Elephant、Monkey和Tiger。然后創(chuàng)建了一個(gè)配置類FilterConfiguration,使用了@ComponentScan注解,并通過includeFilters屬性設(shè)置了一個(gè)正則表達(dá)式" .*Tiger",用于選擇類名以"Tiger"結(jié)尾的類。所以在運(yùn)行主程序時(shí),Spring的IOC容器只會掃描并實(shí)例化Tiger類,因?yàn)橹挥蠺iger類的類名滿足正則表達(dá)式" .*Tiger"。

5. Assignable 類型過濾組件

"Assignable類型過濾 " 是Spring框架在進(jìn)行組件掃描時(shí)的一種過濾策略,該策略允許我們指定一個(gè)或多個(gè)類或接口,然后Spring會包含或排除所有可以賦值給這些類或接口的類。如果我們指定了一個(gè)特定的接口,那么所有實(shí)現(xiàn)了這個(gè)接口的類都會被包含(或者排除)。同樣,如果指定了一個(gè)具體的類,那么所有繼承自這個(gè)類的類也會被包含(或者排除)。 在下面的例子中,我們將使用 “Assignable類型過濾” 來包含所有實(shí)現(xiàn)了Animal接口的類。 全部代碼如下: 首先,我們定義一個(gè)Animal接口

package com.example.demo.bean;

public interface Animal {
}
接著定義三個(gè)類:Elephant、Monkey和Tiger,其中Tiger沒有實(shí)現(xiàn)Animal接口
package com.example.demo.bean;

import org.springframework.stereotype.Component;

@Component
public class Elephant implements Animal {
}
package com.example.demo.bean;

import org.springframework.stereotype.Component;

@Component
public class Monkey implements Animal {
}
package com.example.demo.bean

import org.springframework.stereotype.Component;

@Component
public class Tiger {
}
然后,我們創(chuàng)建一個(gè)FilterConfiguration類并使用@ComponentScan來掃描所有實(shí)現(xiàn)了Animal接口的類。
package com.example.demo.configuration;

import com.example.demo.bean.Animal;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;

@Configuration
@ComponentScan(basePackages = "com.example.demo",
        useDefaultFilters = false,
        includeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = Animal.class))
public class FilterConfiguration {
}
這種過濾方式在@ComponentScan注解中通過FilterType.ASSIGNABLE_TYPE來使用,這里Spring將只掃描并管理所有實(shí)現(xiàn)了Animal接口的類。 最后,我們創(chuàng)建一個(gè)主程序來測試:
package com.example.demo;

import com.example.demo.configuration.FilterConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class DemoApplication {

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(FilterConfiguration.class);
        String[] beanNames = ctx.getBeanDefinitionNames();
        for (String beanName : beanNames) {
            System.out.println(beanName);
        }
    }
}
運(yùn)行結(jié)果:

fde44d06-3770-11ee-9e74-dac502259ad0.png

這里也可以看到,只有實(shí)現(xiàn)了Animal接口的類才會被Spring的IoC容器掃描并實(shí)例化,其他的@Component類沒有實(shí)現(xiàn)Animal接口的bean將不會被掃描和實(shí)例化。 總結(jié):本小節(jié)介紹了Spring框架中的 "Assignable類型過濾 ",這是一種可以指定一個(gè)或多個(gè)類或接口進(jìn)行組件掃描的過濾策略。

Spring會包含或排除所有可以賦值給這些類或接口的類。在本小節(jié)的例子中,首先定義了一個(gè)Animal接口,然后定義了三個(gè)類Elephant、Monkey和Tiger,其中Elephant和Monkey實(shí)現(xiàn)了Animal接口,而Tiger沒有。然后創(chuàng)建了一個(gè)FilterConfiguration類,使用了@ComponentScan注解,并通過includeFilters屬性和FilterType.ASSIGNABLE_TYPE類型來指定掃描所有實(shí)現(xiàn)了Animal接口的類。

因此,當(dāng)運(yùn)行主程序時(shí),Spring的IOC容器只會掃描并實(shí)例化實(shí)現(xiàn)了Animal接口的Elephant和Monkey類,未實(shí)現(xiàn)Animal接口的Tiger類不會被掃描和實(shí)例化。

6. 自定義組件過濾器

Spring也允許我們定義自己的過濾器來決定哪些組件將被Spring IoC容器掃描。為此,我們需要實(shí)現(xiàn)TypeFilter接口,并重寫match()方法。在match()方法中,我們可以自定義選擇哪些組件需要被包含或者排除。 全部代碼如下: 新增一個(gè)接口Animal

package com.example.demo.bean;

public interface Animal {
    String getType();
}
定義幾個(gè)類,實(shí)現(xiàn)Animal接口
package com.example.demo.bean;

import org.springframework.stereotype.Component;

@Component
public class Elephant implements Animal {
    public String getType() {
        return "This is a Elephant.";
    }
}
package com.example.demo.bean;

import org.springframework.stereotype.Component;

@Component
public class Monkey implements Animal {
    public String getType() {
        return "This is an Monkey.";
    }
}
package com.example.demo.bean;

import org.springframework.stereotype.Component;

@Component
public class Tiger implements Animal {
    public String getType() {
        return "This is a Tiger.";
    }
}
package com.example.demo.bean;

import org.springframework.stereotype.Component;

@Component
public class Tiger2 {
    public String getType() {
        return "This is a Tiger2.";
    }
}
Tiger2沒實(shí)現(xiàn)Animal接口,后面用來對比。 下面我們先個(gè)自定義一個(gè)過濾器CustomFilter,它實(shí)現(xiàn)了TypeFilter接口,這個(gè)過濾器會包含所有實(shí)現(xiàn)了Animal接口并且類名以"T"開頭的類:
package com.example.demo.filter;

import com.example.demo.bean.Animal;
import org.springframework.core.type.ClassMetadata;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.TypeFilter;

import java.io.IOException;
import java.util.Arrays;

public class CustomFilter implements TypeFilter {

    @Override
    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
        ClassMetadata classMetadata = metadataReader.getClassMetadata();

        // 如果全限定類名以 "T" 開頭并且實(shí)現(xiàn)了 "Animal" 接口
        return classMetadata.getClassName().startsWith("com.example.demo.bean.T") &&
                Arrays.asList(classMetadata.getInterfaceNames()).contains(Animal.class.getName());
    }
}
如果match方法返回true,那么Spring將把這個(gè)類視為候選組件,還需滿足其他條件才能創(chuàng)建bean,如果這個(gè)類沒有使用@Component、@Service等注解,那么即使過濾器找到了這個(gè)類,Spring也不會將其注冊為bean。因?yàn)镾pring依然需要識別類的元數(shù)據(jù)(如:@Component、@Service等注解)來確定如何創(chuàng)建和管理bean。反之,如果match方法返回false,那么Spring將忽略這個(gè)類。 在match方法中

metadataReader.getClassMetadata()返回一個(gè)ClassMetadata對象,它包含了關(guān)于當(dāng)前類的一些元數(shù)據(jù)信息,例如類名、是否是一個(gè)接口、父類名等。

classMetadata.getClassName()返回當(dāng)前類的全限定類名,也就是包括了包名的類名。

在match方法中,我們檢查了當(dāng)前類的全限定名是否以"com.example.demo.bean.T"開頭,并且當(dāng)前類是否實(shí)現(xiàn)了"Animal"接口。如果滿足這兩個(gè)條件,match方法就返回true,Spring會將這個(gè)類視為候選組件。如果這兩個(gè)條件有任何一個(gè)不滿足,match方法就返回false,Spring就會忽略這個(gè)類,不會將其視為候選組件。 然后,在我們的FilterConfiguration中,使用FilterType.CUSTOM類型,并且指定我們剛才創(chuàng)建的CustomFilter類:

package com.example.demo.configuration;

import com.example.demo.filter.CustomFilter;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;

@Configuration
@ComponentScan(basePackages = "com.example.demo",
        useDefaultFilters = false,
        includeFilters = @ComponentScan.Filter(type = FilterType.CUSTOM, classes = CustomFilter.class))
public class FilterConfiguration {
}
這樣,當(dāng)Spring IoC容器進(jìn)行掃描的時(shí)候,只有類名以"T"開頭并且實(shí)現(xiàn)了Animal接口的組件才會被包含。在我們的例子中,只有Tiger類會被包含,Tiger2、Elephant和Monkey類將被排除。 主程序:
package com.example.demo;

import com.example.demo.configuration.FilterConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class DemoApplication {

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(FilterConfiguration.class);
        String[] beanNames = ctx.getBeanDefinitionNames();
        for (String beanName : beanNames) {
            System.out.println(beanName);
        }
    }
}
運(yùn)行結(jié)果:

fe21dc20-3770-11ee-9e74-dac502259ad0.png

調(diào)試會發(fā)現(xiàn),match方法在不停的回調(diào)。其實(shí)match方法的調(diào)用次數(shù)和Spring應(yīng)用上下文中的Bean定義數(shù)量是相關(guān)的,當(dāng)我們使用@ComponentScan進(jìn)行包掃描時(shí),Spring會遍歷指定包(及其子包)下的所有類,對每個(gè)類進(jìn)行分析以決定是否需要?jiǎng)?chuàng)建對應(yīng)的Bean。

當(dāng)我們使用@ComponentScan.Filter定義自定義的過濾器時(shí),Spring會為每個(gè)遍歷到的類調(diào)用過濾器的match方法,以決定是否需要忽略這個(gè)類。因此,match方法被調(diào)用的次數(shù)等于Spring掃描到的類的數(shù)量,不僅包括最終被創(chuàng)建為Bean的類,也包括被過濾器忽略的類。

這個(gè)行為可能受到一些其他配置的影響。例如,如果Spring配置中使用了懶加載 (@Lazy),那么match方法的調(diào)用可能會被延遲到Bean首次被請求時(shí)。 總結(jié):本小節(jié)介紹了如何在Spring框架中創(chuàng)建和使用自定義過濾器,以決定哪些組件將被Spring IoC容器視為候選組件。

通過實(shí)現(xiàn)TypeFilter接口并重寫其match()方法,可以根據(jù)自定義的條件決定哪些類會被包含在候選組件的列表中。在這個(gè)例子中,我們創(chuàng)建了一個(gè)自定義過濾器,只有以"T"開頭且實(shí)現(xiàn)了Animal接口的類才會被標(biāo)記為候選組件。當(dāng)Spring進(jìn)行包掃描時(shí),會遍歷所有的類,并對每個(gè)類調(diào)用過濾器的match()方法,這個(gè)方法的調(diào)用次數(shù)等于Spring掃描到的類的數(shù)量。

然后,只有那些同時(shí)滿足過濾器條件并且被Spring識別為組件的類(例如,使用了@Component或@Service等注解),才會被實(shí)例化為Bean并被Spring IoC容器管理。如果配置了懶加載,那么Bean的實(shí)例化可能會被延遲到Bean首次被請求時(shí)。

7. 組件掃描的其他特性

Spring的組件掃描機(jī)制提供了一些強(qiáng)大的特性,我們來逐一講解。

7.1 組合使用組件掃描

Spring提供了@ComponentScans注解,讓我們能夠組合多個(gè)@ComponentScan使用,這樣可以讓我們在一次操作中完成多次包掃描。 @ComponentScans的主要使用場景是當(dāng)需要對Spring的組件掃描行為進(jìn)行更精細(xì)的控制時(shí),可以在同一個(gè)應(yīng)用程序中掃描兩個(gè)完全獨(dú)立的包,也可以在應(yīng)用多個(gè)獨(dú)立的過濾器來排除或包含特定的組件。

fe376e64-3770-11ee-9e74-dac502259ad0.png

可以看到@ComponentScans注解接收了一個(gè)ComponentScan數(shù)組,也就是一次性組合了一堆@ComponentScan注解。 讓我們通過一個(gè)例子來看看如何使用@ComponentScans來組合多個(gè)@ComponentScan。 全部代碼如下: 首先,我們定義兩個(gè)簡單的類,分別在com.example.demo.bean1和com.example.demo.bean2包中:

package com.example.demo.bean1;

import org.springframework.stereotype.Component;

@Component
public class BeanA {
}
package com.example.demo.bean2;

import org.springframework.stereotype.Component;

@Component
public class BeanB {
}
然后,我們在配置類中使用@ComponentScans來一次性掃描這兩個(gè)包:
package com.example.demo.configuration;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScans;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScans({
        @ComponentScan("com.example.demo.bean1"),
        @ComponentScan("com.example.demo.bean2")
})
public class AppConfig {
}

最后,我們可以測試一下是否成功地掃描到了這兩個(gè)類:

package com.example.demo;

import com.example.demo.bean1.BeanA;
import com.example.demo.bean2.BeanB;
import com.example.demo.configuration.AppConfig;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class DemoApplication {

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        BeanA beanA = ctx.getBean(BeanA.class);
        BeanB beanB = ctx.getBean(BeanB.class);
        System.out.println("beanA = " + beanA);
        System.out.println("beanB = " + beanB);
    }
}
運(yùn)行上述main方法,BeanA和BeanB就成功地被掃描并注入到了Spring的ApplicationContext中。 運(yùn)行結(jié)果:

fe5317c2-3770-11ee-9e74-dac502259ad0.png

總結(jié):本小節(jié)介紹了Spring包掃描機(jī)制的一個(gè)重要特性,即能夠使用@ComponentScans注解進(jìn)行組合包掃描。這個(gè)特性允許在一次操作中完成多次包掃描,實(shí)現(xiàn)對Spring組件掃描行為的精細(xì)控制。例如,可以同時(shí)掃描兩個(gè)完全獨(dú)立的包,或者應(yīng)用多個(gè)獨(dú)立的過濾器來排除或包含特定的組件。在本小節(jié)的示例中,使用@ComponentScans一次性掃描了com.example.demo.bean1和com.example.demo.bean2兩個(gè)包,成功地將BeanA和BeanB掃描并注入到Spring的ApplicationContext中。

8. 組件掃描的組件名稱生成

當(dāng)我們在Spring中使用注解進(jìn)行bean的定義和管理時(shí),通常會用到@Component,@Service,@Repository,@Controller等注解。在使用這些注解進(jìn)行bean定義的時(shí)候,如果我們沒有明確指定bean的名字,那么Spring會根據(jù)一定的規(guī)則為我們的bean生成一個(gè)默認(rèn)的名字。 這個(gè)默認(rèn)的名字一般是類名的首字母小寫。例如,對于一個(gè)類名為MyService的類,如果我們像這樣使用@Service注解:

@Service
public class MyService {
}
那么Spring會為我們的bean生成一個(gè)默認(rèn)的名字myService。我們可以在應(yīng)用的其他地方通過這個(gè)名字來引用這個(gè)bean。例如,我們可以在其他的bean中通過@Autowired注解和這個(gè)名字來注入這個(gè)bean:
@Autowired
private MyService myService;
這個(gè)默認(rèn)的名字是通過BeanNameGenerator接口的實(shí)現(xiàn)類AnnotationBeanNameGenerator來生成的。AnnotationBeanNameGenerator會檢查我們的類是否有明確的指定了bean的名字,如果沒有,那么它就會按照類名首字母小寫的規(guī)則來生成一個(gè)默認(rèn)的名字。

8.1 Spring 是如何生成默認(rèn) bean 名稱的(源碼分析)

為了解釋這個(gè)過程,讓我們看一下AnnotationBeanNameGenerator類的源碼,以下源碼對應(yīng)的Spring版本是5.3.7。 先給出源碼圖片,后面給出源碼分析

fe8d7d22-3770-11ee-9e74-dac502259ad0.png

代碼塊提出來分析:

public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
    if (definition instanceof AnnotatedBeanDefinition) { // 該行檢查BeanDefinition是否為AnnotatedBeanDefinition
        String beanName = this.determineBeanNameFromAnnotation((AnnotatedBeanDefinition)definition); // 該行調(diào)用方法來從注解獲取bean名稱
        if (StringUtils.hasText(beanName)) { // 檢查是否獲取到了有效的bean名稱
            return beanName; // 如果有,返回這個(gè)名稱
        }
    }

    return this.buildDefaultBeanName(definition, registry); // 如果沒有從注解中獲取到有效的名稱,調(diào)用方法生成默認(rèn)的bean名稱
}
再看看determineBeanNameFromAnnotation方法

fee58c38-3770-11ee-9e74-dac502259ad0.png

這段代碼很長,我們直接將代碼塊提出來分析:

@Nullable
protected String determineBeanNameFromAnnotation(AnnotatedBeanDefinition annotatedDef) {
    // 1. 獲取bean定義的元數(shù)據(jù),包括所有注解信息
    AnnotationMetadata amd = annotatedDef.getMetadata();
    
    // 2. 獲取所有注解類型
    Set types = amd.getAnnotationTypes();
    
    // 3. 初始化bean名稱為null
    String beanName = null;
    
    // 4. 遍歷所有注解類型
    Iterator var5 = types.iterator();
    while(var5.hasNext()) {
        // 4.1 獲取當(dāng)前注解類型
        String type = (String)var5.next();
        
        // 4.2 獲取當(dāng)前注解的所有屬性
        AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(amd, type);
        
        // 4.3 只有當(dāng)前注解的屬性不為null時(shí),才會執(zhí)行以下代碼
        if (attributes != null) {
            Set metaTypes = (Set)this.metaAnnotationTypesCache.computeIfAbsent(type, (key) -> {
                Set result = amd.getMetaAnnotationTypes(key);
                return result.isEmpty() ? Collections.emptySet() : result;
            });
            
            // 4.4 檢查當(dāng)前注解是否為帶有名稱的元注解
            if (this.isStereotypeWithNameValue(type, metaTypes, attributes)) {
                // 4.5 嘗試從注解的"value"屬性中獲取bean名稱
                Object value = attributes.get("value");
                if (value instanceof String) {
                    String strVal = (String)value;
                    
                    // 4.6 檢查獲取到的名稱是否為有效字符串
                    if (StringUtils.hasLength(strVal)) {
                        // 4.7 如果已經(jīng)存在bean名稱并且與當(dāng)前獲取到的名稱不一致,則拋出異常
                        if (beanName != null && !strVal.equals(beanName)) {
                            throw new IllegalStateException("Stereotype annotations suggest inconsistent component names: '" + beanName + "' versus '" + strVal + "'");
                        }

                        // 4.8 設(shè)置bean名稱為獲取到的名稱
                        beanName = strVal;
                    }
                }
            }
        }
    }
    
    // 5. 返回獲取到的bean名稱,如果沒有找到有效名稱,則返回null
    return beanName;
}
最后看看buildDefaultBeanName方法,Spring是如何生成bean的默認(rèn)名稱的。

ff6294da-3770-11ee-9e74-dac502259ad0.png

拆成代碼塊分析:
protected String buildDefaultBeanName(BeanDefinition definition) {
    // 1. 從bean定義中獲取bean的類名
    String beanClassName = definition.getBeanClassName();

    // 2. 確保bean類名已設(shè)置,否則會拋出異常
    Assert.state(beanClassName != null, "No bean class name set");

    // 3. 使用Spring的ClassUtils獲取類的簡單名稱,即不帶包名的類名
    String shortClassName = ClassUtils.getShortName(beanClassName);

    // 4. 使用Java內(nèi)省工具(Introspector)將類名首字母轉(zhuǎn)換為小寫
    // 這就是Spring的默認(rèn)bean命名策略,如果用戶沒有通過@Component等注解顯式指定bean名,
    // 則會使用該類的非限定類名(即不帶包名的類名),并將首字母轉(zhuǎn)換為小寫作為bean名。
    return Introspector.decapitalize(shortClassName);
}
8.2 生成默認(rèn) bean 名稱的特殊情況 大家肯定知道UserService默認(rèn)bean名稱為userService,但如果類名為MService,bean名稱還是MService,不會首字母小寫。具體原因,我們來分析一下。 我們上面分析buildDefaultBeanName方法生成默認(rèn)bean名稱的時(shí)候,發(fā)現(xiàn)里面有調(diào)用decapitalize方法后再返回,我們來看看decapitalize方法。

ff8c6bac-3770-11ee-9e74-dac502259ad0.png

提出代碼塊分析一下

/**
 * 將字符串轉(zhuǎn)換為正常的 Java 變量名規(guī)則的形式。
 * 這通常意味著將第一個(gè)字符從大寫轉(zhuǎn)換為小寫,
 * 但在(不常見的)特殊情況下,當(dāng)有多個(gè)字符并且第一個(gè)和第二個(gè)字符都是大寫時(shí),我們將保持原樣。
 * 因此,“FooBah”變?yōu)椤癴ooBah”,“X”變?yōu)椤皒”,但“URL”保持為“URL”。
 * 這是 Java 內(nèi)省機(jī)制的一部分,因?yàn)樗婕?Java 對類名和變量名的默認(rèn)命名規(guī)則。
 * 根據(jù)這個(gè)規(guī)則,我們可以從類名自動(dòng)生成默認(rèn)的變量名。
 *
 * @param name 要小寫的字符串。
 * @return 小寫版本的字符串。
 */
public static String decapitalize(String name) {
    if (name == null || name.length() == 0) {
        return name;
    }
    // 如果字符串的前兩個(gè)字符都是大寫,那么保持原樣
    if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) && Character.isUpperCase(name.charAt(0))) {
        return name;
    }
    char chars[] = name.toCharArray();
    // 將第一個(gè)字符轉(zhuǎn)為小寫
    chars[0] = Character.toLowerCase(chars[0]);
    return new String(chars);
}
根據(jù)Java的命名規(guī)則,類名的首字母應(yīng)該大寫,而變量名的首字母應(yīng)該小寫,它告訴內(nèi)省機(jī)制如何從類名生成默認(rèn)的變量名(或者說bean名)。 這里可以看到,decapitalize方法接收一個(gè)字符串參數(shù),然后將這個(gè)字符串的首字母轉(zhuǎn)為小寫,除非這個(gè)字符串的前兩個(gè)字符都是大寫,這種情況下,字符串保持不變。 所以,在Java內(nèi)省機(jī)制中,如果類名的前兩個(gè)字母都是大寫,那么在進(jìn)行首字母小寫的轉(zhuǎn)換時(shí),會保持原樣不變。也就是說,對于這種情況,bean的名稱和類名是一樣的。 這種設(shè)計(jì)是為了遵守Java中的命名約定,即當(dāng)一個(gè)詞作為類名的開始并且全部大寫時(shí)(如URL,HTTP),應(yīng)保持其全部大寫的格式。

9. Java 的內(nèi)省機(jī)制在生成默認(rèn) bean 名稱中的應(yīng)用

Java內(nèi)省機(jī)制(Introspection)是Java語言對Bean類的一種自我檢查的能力,它屬于Java反射的一個(gè)重要補(bǔ)充。它允許Java程序在運(yùn)行時(shí)獲取Bean類的類型信息以及Bean的屬性和方法的信息。注意:“內(nèi)省” 發(fā)音是"nèi xǐng"。 內(nèi)省機(jī)制的目的在于提供一套統(tǒng)一的API,可以在運(yùn)行時(shí)動(dòng)態(tài)獲取類的各種信息,主要涵蓋以下幾個(gè)方面:

獲取類的類型信息:可以在運(yùn)行時(shí)獲取任意一個(gè)Bean對象所屬的類、接口、父類、修飾符等信息。

屬性信息:可以獲取Bean類的屬性的各種信息,如類型、修飾符等。

獲取方法信息:可以獲取Bean類的方法信息,如返回值類型、參數(shù)類型、修飾符等。

調(diào)用方法:可以在運(yùn)行時(shí)調(diào)用任意一個(gè)Bean對象的方法。

修改屬性值:可以在運(yùn)行時(shí)修改Bean的屬性值。

通過這些反射API,我們可以以一種統(tǒng)一的方式來操作任意一個(gè)對象,無需對對象的具體類進(jìn)行硬編碼。 在命名規(guī)則上,當(dāng)我們獲取一個(gè)Bean的屬性名時(shí),如果相應(yīng)的getter或setter方法的名稱除去"get"/"set"前綴后,剩余部分的第一個(gè)字母是大寫的,那么在轉(zhuǎn)換成屬性名時(shí),會將這個(gè)字母變?yōu)樾?。如果剩余部分的前兩個(gè)字母都是大寫的,屬性名會保持原樣不變,不會將它們轉(zhuǎn)換為小寫。 這個(gè)規(guī)則主要是為了處理一些類名或方法名使用大寫字母縮寫的情況。例如,對于一個(gè)名為 "getURL“的方法,我們會得到”URL“作為屬性名,而不是”uRL"。 雖然在日常開發(fā)中我們可能不會直接頻繁使用到Java的內(nèi)省機(jī)制,但在一些特定的場景和工具中,內(nèi)省機(jī)制卻發(fā)揮著重要作用:

IDE 和調(diào)試工具:這些工具需要利用內(nèi)省機(jī)制來獲取類的信息,如類的層次結(jié)構(gòu)、方法和屬性信息等,以便提供代碼補(bǔ)全、代碼檢查等功能。

測試框架:例如JUnit這樣的測試框架需要通過內(nèi)省機(jī)制來實(shí)例化測試類,獲取測試方法等信息以進(jìn)行測試的運(yùn)行。

依賴注入框架:比如Spring等依賴注入框架需要利用內(nèi)省機(jī)制來掃描類,獲取類中的依賴關(guān)系定義,并自動(dòng)裝配bean。

序列化 / 反序列化:序列化需要獲取對象的類型信息和屬性信息來實(shí)現(xiàn)對象狀態(tài)的持久化;反序列化需要利用類型信息來還原對象。

日志框架:很多日志框架可以通過內(nèi)省機(jī)制自動(dòng)獲取日志方法所在類、方法名等上下文信息。

訪問權(quán)限判斷:一些安全相關(guān)的框架需要通過內(nèi)省判斷一個(gè)成員的訪問權(quán)限是否合法。

面向接口編程:內(nèi)省機(jī)制使得在面向接口編程的時(shí)候可以不需要hardcode接口的實(shí)現(xiàn)類名,而是在運(yùn)行時(shí)定位。

簡言之,內(nèi)省機(jī)制的目的是實(shí)現(xiàn)跨類的動(dòng)態(tài)操作和信息訪問,提高運(yùn)行時(shí)的靈活性。這也使得框架在不知道具體類的情況下,可以進(jìn)行一些有用的操作。







審核編輯:劉清

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報(bào)投訴
  • 控制器
    +關(guān)注

    關(guān)注

    112

    文章

    16105

    瀏覽量

    177080
  • 編譯器
    +關(guān)注

    關(guān)注

    1

    文章

    1617

    瀏覽量

    49016
  • JAVA語言
    +關(guān)注

    關(guān)注

    0

    文章

    138

    瀏覽量

    20062
  • 過濾器
    +關(guān)注

    關(guān)注

    1

    文章

    427

    瀏覽量

    19521
  • 調(diào)試器
    +關(guān)注

    關(guān)注

    1

    文章

    300

    瀏覽量

    23668

原文標(biāo)題:解鎖Spring組件掃描的新視角

文章出處:【微信號:OSC開源社區(qū),微信公眾號:OSC開源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    何在實(shí)際電路中使用帶通濾波器

    在本教程中,我們將了解此帶通濾波器、其背后的理論以及如何在實(shí)際電路中使用它。
    的頭像 發(fā)表于 09-08 15:52 ?7326次閱讀
    如<b class='flag-5'>何在</b><b class='flag-5'>實(shí)際</b>電路<b class='flag-5'>中使</b>用帶通濾波器

    何在Linux中使用htop命令

    本文介紹如何在 Linux 中使用 htop 命令。
    的頭像 發(fā)表于 12-04 14:45 ?1831次閱讀
    如<b class='flag-5'>何在</b>Linux<b class='flag-5'>中使</b>用htop命令

    什么是java spring

    。在SSH項(xiàng)目中管理事務(wù)以及對象的注入Spring是非侵入式的:基于Spring開發(fā)的系統(tǒng)中的對象一般不依賴于Spring的類。組成 Spring 框架的每個(gè)模塊(或
    發(fā)表于 09-11 11:16

    何在verilog組件中使用內(nèi)存?

    你好。我正在嘗試(現(xiàn)在)為我的碩士論文寫簡單的Verilog組件,而在做短的LIFO堆棧時(shí),我遇到了問題,因?yàn)椋骸皹?gòu)建錯(cuò)誤:不支持內(nèi)存聲明”不管怎樣,為了避免這個(gè)問題,例如,你可以在PSoC 5LP中使用不知何故的RAM構(gòu)建,以使這個(gè)
    發(fā)表于 08-22 12:51

    Spring筆記分享

    ; 可以管理所有的組件(類)Spring的優(yōu)良特性1) 非侵入式:基于Spring開發(fā)的應(yīng)用中的對象可以不依賴于Spring的API2) 依
    發(fā)表于 11-04 07:51

    何在java代碼中使用HTTP代理IP

    何在java代碼中使用HTTP代理IP。
    的頭像 發(fā)表于 08-04 15:38 ?2177次閱讀

    何在python代碼中使用HTTP代理IP

    何在python代碼中使用HTTP代理IP。
    的頭像 發(fā)表于 08-04 15:46 ?1243次閱讀

    何在PHP代碼中使用HTTP代理IP

    何在PHP代碼中使用HTTP代理IP。
    的頭像 發(fā)表于 08-04 16:08 ?2399次閱讀

    go語言代碼中使用HTTP代理IP的方法

    何在go語言代碼中使用HTTP代理IP。
    的頭像 發(fā)表于 08-04 16:13 ?3125次閱讀

    何在易e語言代碼中使用HTTP代理IP

    何在易e語言代碼中使用HTTP代理IP,示例代碼demo直接可用(步驟注釋清晰)
    的頭像 發(fā)表于 08-05 16:29 ?6808次閱讀

    何在c語言代碼中使用HTTP代理IP

    何在c語言代碼中使用HTTP代理IP,示例代碼demo直接可用(步驟注釋清晰)
    的頭像 發(fā)表于 08-05 16:31 ?2302次閱讀

    何在c#語言代碼中使用HTTP代理IP

    何在c#語言代碼中使用HTTP代理IP,示例代碼demo直接可用(步驟注釋清晰)
    的頭像 發(fā)表于 08-05 16:33 ?2544次閱讀

    何在python代碼中使用HTTP代理IP

    如何再python代碼中使用HTTP代理IP。
    的頭像 發(fā)表于 09-13 09:25 ?968次閱讀

    何在Arduino中使用LDR

    電子發(fā)燒友網(wǎng)站提供《如何在Arduino中使用LDR.zip》資料免費(fèi)下載
    發(fā)表于 10-31 09:50 ?0次下載
    如<b class='flag-5'>何在</b>Arduino<b class='flag-5'>中使</b>用LDR

    何在測試中使用ChatGPT

    Dimitar Panayotov 在 2023 年 QA Challenge Accepted 大會 上分享了他如何在測試中使用 ChatGPT。
    的頭像 發(fā)表于 02-20 13:57 ?711次閱讀