亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

在Spring中如何注入動(dòng)態(tài)代理Bean

 更新時(shí)間:2025年03月26日 11:25:04   作者:還是轉(zhuǎn)轉(zhuǎn)  
這篇文章主要介紹了在Spring中如何注入動(dòng)態(tài)代理Bean問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教

在Spring中注入動(dòng)態(tài)代理Bean

在Springboot中我們可以通過(guò)內(nèi)置的注解如@Service,@Component,@Repository來(lái)注冊(cè)bean,也可以在配置類中通過(guò)@Bean來(lái)注冊(cè)bean。這些都是Spring內(nèi)置的注解。

除此之外,還可以用@WebFilter,@WebServlet,@WebListener注解結(jié)合@ServletComponentScan自動(dòng)注冊(cè)Bean。但這里的@WebFilter@WebServlet,@WebListener并不是Spring的注解,而是Servlet 3+ 的注解。為什么這些注解的類能自動(dòng)注冊(cè)為Spring的Bean,其實(shí)現(xiàn)原理是什么呢?

如果進(jìn)入@ServletComponentScan中查看可以發(fā)現(xiàn),該注解上有另外一個(gè)注解:@Import(ServletComponentScanRegistrar.class),進(jìn)一步查看可知:class ServletComponentScanRegistrar implements ImportBeanDefinitionRegistrar。這里的關(guān)鍵就是ImportBeanDefinitionRegistrar 接口。

ImportBeanDefinitionRegistrar

Spring中最經(jīng)典的設(shè)計(jì)就是AOP和IOC,使得Spring框架具有良好的擴(kuò)展性,而ImportBeanDefinitionRegistrar 就是其中用來(lái)擴(kuò)展的hook之一。

通常情況下,Spring中的bean就是通過(guò)XML配置文件,Spring中的注解或配置類來(lái)注冊(cè)的。但有時(shí)候,可能需要在運(yùn)行時(shí)根據(jù)某些條件動(dòng)態(tài)地注冊(cè)一些bean,這時(shí)就可以使用ImporterBeanDefinitionRegistrar接口來(lái)實(shí)現(xiàn)此功能。

具體來(lái)說(shuō),實(shí)現(xiàn)了ImporterBeanDefinitionRegistrar 接口的類可以在@Importer注解中被引入,Spring在初始化容器時(shí)會(huì)調(diào)用這個(gè)實(shí)現(xiàn)類的regisgterBeanDefinitions方法,以便在運(yùn)行時(shí)根據(jù)需要需要注冊(cè)一些額外的bean。

這個(gè)接口通常用于一些高級(jí)的場(chǎng)景,比如根據(jù)運(yùn)行時(shí)環(huán)境來(lái)動(dòng)態(tài)的注冊(cè)不同的bean,或者根據(jù)某些外部配置來(lái)決定是否注冊(cè)某些bean等。通過(guò)這種方式使得Spring應(yīng)用程序的配置更加靈活和動(dòng)態(tài)化。

動(dòng)態(tài)注冊(cè)Bean

下面通過(guò)ImportBeanDefinitionRegistrar 來(lái)動(dòng)態(tài)注冊(cè)Bean。

首先將@ServletComponentScan 抄過(guò)來(lái)改一下名字:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({MetaAutoConfigureRegistrar.class})
public @interface MetaComponentScan {
    @AliasFor("basePackages")
    String[] value() default {};
    @AliasFor("value")
    String[] basePackages() default {};
    Class<?>[] basePackageClasses() default {};
}

然后實(shí)現(xiàn)自定義注冊(cè)器:

public class MetaComponentScanRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {

    private ResourceLoader resourceLoader;

    private Environment environment;

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metaData, BeanDefinitionRegistry registry) {
        MetaBeanDefinitionScanner scanner = new MetaBeanDefinitionScanner(registry, this.environment,
                this.resourceLoader);
        Set<String> packagesToScan = this.getBasePackages(metaData);
        scanner.scan(packagesToScan.toArray(new String[]{}));
    }

    private static class MetaBeanDefinitionScanner extends ClassPathBeanDefinitionScanner {
        public MetaBeanDefinitionScanner(BeanDefinitionRegistry registry, Environment environment,
                                         ResourceLoader resourceLoader) {
            super(registry, false, environment, resourceLoader);
            registerFilters();
        }

        protected void registerFilters() {
            addIncludeFilter(new AnnotationTypeFilter(Meta.class));
        }
    }

    private Set<String> getBasePackages(AnnotationMetadata metadata) {
        AnnotationAttributes attributes = AnnotationAttributes
                .fromMap(metadata.getAnnotationAttributes(MetaComponentScan.class.getName()));
        String[] basePackages = attributes.getStringArray("basePackages");
        Class<?>[] basePackageClasses = attributes.getClassArray("basePackageClasses");
        Set<String> packagesToScan = new LinkedHashSet<>(Arrays.asList(basePackages));
        for (Class<?> basePackageClass : basePackageClasses) {
            packagesToScan.add(ClassUtils.getPackageName(basePackageClass));
        }
        if (packagesToScan.isEmpty()) {
            packagesToScan.add(ClassUtils.getPackageName(metadata.getClassName()));
        }
        return packagesToScan;
    }
}

自定義注冊(cè)器必須實(shí)現(xiàn)ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware這三個(gè)接口,然后覆寫(xiě)registerBeanDefinitions 方法,該方法在Spring容器初始化的時(shí)候被調(diào)用。

在該方法中,需要一個(gè)掃描器,該掃描器中有一個(gè)過(guò)濾器,用于過(guò)濾自定義的注解類。因此,需要一個(gè)自定義注解:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Meta {
}

所有使用該注解的類都將被掃描器掃到并注冊(cè)為bean。掃描時(shí)需要知道要掃描的路徑,通過(guò)getBasePackages 方法獲取。最后調(diào)用ClassPathBeanDefinitionScanner 的scan方法來(lái)掃描和注冊(cè)bean,這部分是Spring中的固有實(shí)現(xiàn)。

現(xiàn)在來(lái)創(chuàng)建一個(gè)通過(guò)@Meta注解的類,看一下是否被自動(dòng)注冊(cè)為bean:

@Meta
public class DemoBean {
    public  DemoBean() {
        System.out.println("DemoBean register!");
    }
}

啟動(dòng)SpringBootApplication,會(huì)發(fā)現(xiàn)控制臺(tái)日志中有如下輸出:

DemoBean register!

表明確實(shí)調(diào)用了DemoBean 的構(gòu)造方法,自動(dòng)注冊(cè)了一個(gè)bean。

注入動(dòng)態(tài)代理bean

如果不是在第三方框架中,正常情況下,普通的類完全沒(méi)必要自定義注冊(cè),直接用Spring內(nèi)置的注解如@Component即可。

那使用自定義注解來(lái)動(dòng)態(tài)注冊(cè)Spring中的bean還有什么使用場(chǎng)景呢?

Mapper注入原理

如果了解Feign或者mybatis的Mapper應(yīng)該知道,在通過(guò)feign調(diào)用遠(yuǎn)程接口或者通過(guò)mapper訪問(wèn)數(shù)據(jù)庫(kù)時(shí),是不需要實(shí)現(xiàn)類的,而是直接通過(guò)接口進(jìn)行調(diào)用的。

下面以Mapper為例(mapper-spring:4.3.0)看下是如何實(shí)現(xiàn)的。

同樣的,首先需要在Springboot的啟動(dòng)類上加上注解@MapperScan,該注解中通過(guò)@Importer引入了MapperScannerRegistrar,而這個(gè)注冊(cè)器實(shí)現(xiàn)了ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware接口,并覆寫(xiě)了registerBeanDefinitions方法。在該方法中,調(diào)用了ClassPathBeanDefinitionScanner的子類ClassPathMapperScannerdoScan方法來(lái)對(duì)符合條件的包進(jìn)行掃描并注冊(cè)bean,其代碼如下:

@Override
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
    Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);

    if (beanDefinitions.isEmpty()) {
        logger.warn("No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
    } else {
        processBeanDefinitions(beanDefinitions);
    }

    return beanDefinitions;
}

可以看到,該方法首先調(diào)用了父類的doScan方法,也就是Spring類ClassPathBeanDefinitionScanner 中的doScan方法,通過(guò)BeanDefinitionReaderUtils來(lái)注冊(cè)bean,代碼如下:

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);
		}
	}
}

reigstry有三個(gè)實(shí)現(xiàn),這里主要看DefaultListableBeanFactory,在該類的registerBeanDefinition方法里,從beanDefinitionMap中根據(jù)beanName來(lái)獲取beanDefinition,如果不存在,就將自定義的beanDefinition放到beanDefinitionMap 中。

調(diào)用完父類的doScan方法之后,接下來(lái)調(diào)用processBeanDefinitions方法對(duì)beanDefinitions進(jìn)行處理。在該方法中,將beanClassmapper接口類變成了MapperFactoryBean,而MapperFactoryBean 實(shí)現(xiàn)了FactoryBean接口。這將使得最終生成的bean為代理對(duì)象。

當(dāng)Spring容器啟動(dòng)時(shí),它會(huì)掃描應(yīng)用程序中的所有Bean定義,并實(shí)例化那些需要實(shí)例化的Bean。如果遇到實(shí)現(xiàn)了FactoryBean接口的Bean定義,Spring將會(huì)為該Bean創(chuàng)建一個(gè)特殊的代理對(duì)象,以便在需要時(shí)調(diào)用FactoryBean的方法來(lái)創(chuàng)建實(shí)際的Bean實(shí)例。

當(dāng)需要使用由FactoryBean創(chuàng)建的Bean時(shí),Spring將會(huì)調(diào)用代理對(duì)象的getObject()方法來(lái)獲取實(shí)際的Bean實(shí)例。有需要的話,Spring還會(huì)調(diào)用代理對(duì)象的getObjectType()方法來(lái)確定實(shí)際Bean實(shí)例的類型。

如果FactoryBean創(chuàng)建的Bean是單例模式,那么Spring將在第一次調(diào)用getObject()方法時(shí)創(chuàng)建實(shí)例,并將其緩存起來(lái)。以后每次調(diào)用getObject()方法時(shí),都會(huì)返回同一個(gè)實(shí)例。如果FactoryBean創(chuàng)建的Bean不是單例模式,則每次調(diào)用getObject()方法時(shí)都會(huì)創(chuàng)建一個(gè)新的實(shí)例。

至此,Mapper接口注入到Spring中的過(guò)程就比較清晰了。

自定義注入

下面仿照Mapper的實(shí)現(xiàn)原理來(lái)自定義注解和代理工廠,實(shí)現(xiàn)自定義注入動(dòng)態(tài)代理Bean。

同樣地,先定義基礎(chǔ)注解,通過(guò)該注解引入Registrar:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({MapperScanRegistrar.class})
public @interface MapperScan {
    @AliasFor("basePackages")
    String[] value() default {};
    @AliasFor("value")
    String[] basePackages() default {};
    Class<?>[] basePackageClasses() default {};
}

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Mapper {
}

public class MapperScanRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {

    private ResourceLoader resourceLoader;

    private Environment environment;

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false, environment) {
            @Override
            protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
                return beanDefinition.getMetadata().isIndependent() && !beanDefinition.getMetadata().isAnnotation();
            }
        };
        scanner.setResourceLoader(this.resourceLoader);
        scanner.addIncludeFilter(new AnnotationTypeFilter(Mapper.class));

        Set<String> basePackages = getBasePackages(metadata);
        for (String pkg : basePackages) {
            Set<BeanDefinition> beanDefinitions = scanner.findCandidateComponents(pkg);
            for (BeanDefinition candidate : beanDefinitions) {
                if (candidate instanceof AnnotatedBeanDefinition annotatedBeanDefinition) {
                    AnnotationMetadata annotationMetadata = annotatedBeanDefinition.getMetadata();
                    String className = annotationMetadata.getClassName();
                    Class<?> beanClass = ClassUtils.resolveClassName(className, ClassUtils.getDefaultClassLoader());
                    String beanName = ClassUtils.getShortName(className);
                    BeanDefinitionBuilder definitionBuilder = BeanDefinitionBuilder
                            .genericBeanDefinition(MapperBeanFactory.class)
                            .addPropertyValue("type", beanClass);
                    registry.registerBeanDefinition(beanName, definitionBuilder.getBeanDefinition());
                }
            }
        }

    }

    private Set<String> getBasePackages(AnnotationMetadata metadata) {
        AnnotationAttributes attributes = AnnotationAttributes
                .fromMap(metadata.getAnnotationAttributes(MapperScan.class.getName()));
        String[] basePackages = attributes.getStringArray("basePackages");
        Class<?>[] basePackageClasses = attributes.getClassArray("basePackageClasses");
        Set<String> packagesToScan = new LinkedHashSet<>(Arrays.asList(basePackages));
        for (Class<?> basePackageClass : basePackageClasses) {
            packagesToScan.add(ClassUtils.getPackageName(basePackageClass));
        }
        if (packagesToScan.isEmpty()) {
            packagesToScan.add(ClassUtils.getPackageName(metadata.getClassName()));
        }
        return packagesToScan;
    }
}

這里的注冊(cè)邏輯是重點(diǎn)。

其中Scanner不是繼承自ClassPathBeanDefinitionScanner 的,而是與其同級(jí)的,需要覆寫(xiě)isCandidateComponent 方法。
ClassPathBeanDefinitionScanner是直接用于掃描Bean并注冊(cè)的類,它繼承了ClassPathScanningCandidateComponentProvider,并添加了注冊(cè)Bean定義的功能。
ClassPathScanningCandidateComponentProvider是掃描候選組件的provider,它負(fù)責(zé)識(shí)別符合條件的類,但不負(fù)責(zé)注冊(cè)這些類。換句話說(shuō),注冊(cè)Bean定義的功能需要自己實(shí)現(xiàn)。

注冊(cè)Bean定義的代碼如下:

if (candidate instanceof AnnotatedBeanDefinition annotatedBeanDefinition) {
    AnnotationMetadata annotationMetadata = annotatedBeanDefinition.getMetadata();
    String className = annotationMetadata.getClassName();
    Class<?> beanClass = ClassUtils.resolveClassName(className, ClassUtils.getDefaultClassLoader());
    String beanName = ClassUtils.getShortName(className);
    BeanDefinitionBuilder definitionBuilder = BeanDefinitionBuilder
            .genericBeanDefinition(MapperBeanFactory.class)
            .addPropertyValue("type", beanClass);
    registry.registerBeanDefinition(beanName, definitionBuilder.getBeanDefinition());
}

先獲取bean定義的元數(shù)據(jù),這其中包含bean的類名,可以借此通過(guò)反射來(lái)獲取類對(duì)象。

然后更新bean定義,主要是更新beanClass,將其由原始的接口類更改為MapperBeanFactory。同時(shí),還添加了一個(gè)type字段,值為原始的接口類。這樣實(shí)例化bean時(shí)就能生成代理對(duì)象了,且代理對(duì)象的類型為接口類。

最終看下MapperBeanFactory的實(shí)現(xiàn):

public class MapperBeanFactory<T> implements FactoryBean<T> {

    private Class<T> type;

    public MapperBeanFactory() {
    }

    public MapperBeanFactory(Class<T> type) {
        this.type = type;
    }

    @Override
    public Class<T> getObjectType() {
        return type;
    }

    @Override
    public T getObject() {
        return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[]{type}, (proxy, method, args) -> {
            System.out.printf("Class %s, execute %s method, parameters=%s%n",
                    method.getDeclaringClass().getName(), method.getName(), args[0]);
            return switch (method.getName()) {
                case "sayHello" -> "hello, " + args[0];
                case "sayHi" -> "hi, " + args[0];
                default -> "hello, world!";
            };
        });
    }

    public void setType(Class<T> type) {
        this.type = type;
    }
}

這里的setType方法是必須的,添加的"type"屬性就是通過(guò)此set方法設(shè)置進(jìn)來(lái)的。getObject方法用于生成實(shí)際的代理對(duì)象,具體是由Proxy.newProxyInstance來(lái)生成的。該方法需要三個(gè)參數(shù),分別是: 代理類的加載器,代理類要實(shí)現(xiàn)的接口列表,代理類handler(InvocationHandler接口的實(shí)現(xiàn)類)。其中,第三個(gè)參數(shù)是一個(gè)匿名類對(duì)象(這里用lambda表達(dá)式進(jìn)行了簡(jiǎn)化),該匿名類實(shí)現(xiàn)了InvocationHandler 接口,并覆寫(xiě)了invoke代理方法。在代理方法中,根據(jù)原始調(diào)用方法的不同返回不同的值。

接下來(lái)看一下Mapper注解的接口和接口controller:

@Mapper
public interface UserMapper {

    String sayHello(String userName);

    String sayHi(String userName);
}

@RestController
@RequestMapping("/sample")
public class HelloController {

    @Resource
    private UserMapper userMapper;

    @RequestMapping("/hello")
    public String sayHello(@RequestParam String userName) {
        return userMapper.sayHello(userName);
    }

    @RequestMapping("/hi")
    public String sayHi(@RequestParam String userName) {
        return userMapper.sayHi(userName);
    }
}

當(dāng)系統(tǒng)啟動(dòng)后,訪問(wèn)http://localhost:8080/sample/hello?userName=testhttp://localhost:8080/sample/hi?userName=test會(huì)返回不同的結(jié)果。

這里UserMapper接口中的方法并沒(méi)有實(shí)現(xiàn),真正的實(shí)現(xiàn)邏輯是在代理方法中根據(jù)方法名做的。

可以做一下合理的推測(cè),除了Mapper之外,Spring Data JPA中的接口訪問(wèn)數(shù)據(jù)庫(kù)的具體邏輯,也是在代理方法中實(shí)現(xiàn)的。

總結(jié)

以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • Java實(shí)例化一個(gè)抽象類對(duì)象的方法教程

    Java實(shí)例化一個(gè)抽象類對(duì)象的方法教程

    大家都知道抽象類無(wú)法實(shí)例化,就無(wú)法創(chuàng)建對(duì)象。所以下面這篇文章主要給大家介紹了關(guān)于Java實(shí)例化一個(gè)抽象類對(duì)象的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。
    2017-12-12
  • SpringBoot項(xiàng)目微信云托管入門(mén)部署實(shí)踐

    SpringBoot項(xiàng)目微信云托管入門(mén)部署實(shí)踐

    本文主要介紹了SpringBoot項(xiàng)目微信云托管入門(mén)部署實(shí)踐,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-03-03
  • 詳解Java8 新特性之日期API

    詳解Java8 新特性之日期API

    Java 8 在包java.time下包含了一組全新的時(shí)間日期API。下面通過(guò)示例給大家講解java8 新特征日期api的相關(guān)知識(shí),感興趣的朋友一起看看吧
    2017-07-07
  • Spring中FactoryBean的高級(jí)用法實(shí)戰(zhàn)教程

    Spring中FactoryBean的高級(jí)用法實(shí)戰(zhàn)教程

    FactoryBean是Spring框架的高級(jí)特性,允許自定義對(duì)象的創(chuàng)建過(guò)程,適用于復(fù)雜初始化邏輯,本文給大家介紹Spring中FactoryBean的高級(jí)用法實(shí)戰(zhàn),感興趣的朋友跟隨小編一起看看吧
    2024-09-09
  • Spring boot定時(shí)任務(wù)的原理及動(dòng)態(tài)創(chuàng)建詳解

    Spring boot定時(shí)任務(wù)的原理及動(dòng)態(tài)創(chuàng)建詳解

    這篇文章主要給大家介紹了關(guān)于Spring boot定時(shí)任務(wù)的原理及動(dòng)態(tài)創(chuàng)建的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-03-03
  • SpringMVC實(shí)現(xiàn)用戶登錄全過(guò)程

    SpringMVC實(shí)現(xiàn)用戶登錄全過(guò)程

    這篇文章主要介紹了SpringMVC實(shí)現(xiàn)用戶登錄全過(guò)程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2024-09-09
  • SpringBoot  jdbctemplate使用方法解析

    SpringBoot jdbctemplate使用方法解析

    這篇文章主要介紹了SpringBoot jdbctemplate使用方法解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-05-05
  • JAVA PDF操作之實(shí)現(xiàn)截取N頁(yè)和多個(gè)PDF合并

    JAVA PDF操作之實(shí)現(xiàn)截取N頁(yè)和多個(gè)PDF合并

    這篇文章主要為大家詳細(xì)介紹了java關(guān)于PDF的一些操作,例如截取N頁(yè)并生成新文件,轉(zhuǎn)圖片以及多個(gè)PDF合并,文中的示例代碼講解詳細(xì),感興趣的可以了解下
    2025-01-01
  • MyBatis動(dòng)態(tài)<if>標(biāo)簽的使用

    MyBatis動(dòng)態(tài)<if>標(biāo)簽的使用

    本文主要介紹了MyBatis動(dòng)態(tài)<if>標(biāo)簽的使用,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2023-05-05
  • java實(shí)現(xiàn)科研信息管理系統(tǒng)

    java實(shí)現(xiàn)科研信息管理系統(tǒng)

    這篇文章主要為大家詳細(xì)介紹了java科研信息管理系統(tǒng),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2018-02-02

最新評(píng)論