zoukankan      html  css  js  c++  java
  • springboot源码分析(六)-refresh方法分析

    概述

      在学习springboot时候,会牵涉到很多的知识,而refresh方法可以说其中的核心方法,为什么这么说,因为整个spring的核心aop和ioc都和这个方法有关,既然这个方法那么重要,那我们就分析一下这个方法到底干了什么。

    refresh()调用的核心方法预览

    看了上面这个图是不是都晕了,其实不用晕,这里面很多的方法只是在做一些准备的工作和监听的工作,真正核心的就两三个方法,那我就把核心的方法先列出来,

    • invokeBeanFactoryPostProcessors(beanFactory),核心方法,将程序中的所有bean放入到beanDefinitionMap中,注意这一步并没有实例化bean,而是获得bean的beanDefinition
    • finishBeanFactoryInitialization(beanFactory),核心方法,将上面放入到beanDefinitionMap中的非懒加载的bean都实例化,这两个方法可以说是让ioc起作用的核心方法

    其实核心的方法我觉得就这两个,剩下的都是一些准备性的工作,ok,下面我们就逐个分析一下每个方法,不重要的方法我就简单说一下,我觉得没必要每个方法都全部吃透,搞明白核心方法就可以了,spring代码那么多,又不是自己要去写一个spring,没必要全部吃透。

    prepareRefresh()

      这个方法,从名字就可以看出来,就是一个为了执行refresh()方法做准备的方法,下面我们看一下代码

    protected void prepareRefresh() {
         //设置开始时间
    this.startupDate = System.currentTimeMillis(); this.closed.set(false); this.active.set(true); if (this.logger.isDebugEnabled()) { if (this.logger.isTraceEnabled()) { this.logger.trace("Refreshing " + this); } else { this.logger.debug("Refreshing " + this.getDisplayName()); } }       this.initPropertySources();
         //校验一些关键的环境信息是否存在
    this.getEnvironment().validateRequiredProperties(); if (this.earlyApplicationListeners == null) { this.earlyApplicationListeners = new LinkedHashSet(this.applicationListeners); } else { this.applicationListeners.clear(); this.applicationListeners.addAll(this.earlyApplicationListeners); } this.earlyApplicationEvents = new LinkedHashSet(); }

    总结:这个方法总的来说就是设置了一下启动的时间,校验了一下环境参数。

    obtainFreshBeanFactory()

      这个方法从名称也可以看出来获取beanFactory的,至于beanFactory是什么,相信大家也应该清楚,整个spring的核心接口,用于生产bean,为他的实现类DefaultListableBeanFactory,ApplicationContext等提供了最基本的规范,下面看一下代码

    protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
            this.refreshBeanFactory();
            return this.getBeanFactory();
        }

    第一个方法refreshBeanFactory,这个方法就是未beanFactory设置了一个序列化id,我们主要看一下第二个,这里使用了模板模式,真是实现这个getBeanFactory()的是GenericApplicationContext(),我们看一下这个类的构造方法

        public GenericApplicationContext() {
            this.customClassLoader = false;
            this.refreshed = new AtomicBoolean();
         //这里new了一个beanFactory
    this.beanFactory = new DefaultListableBeanFactory(); }

    我们看一下this.getBeanFactory();方法代码

     public final ConfigurableListableBeanFactory getBeanFactory() {
            return this.beanFactory;
        }

    总结:obtainFreshBeanFactory()方法就是为了返回new DefaultListableBeanFactory();

    this.prepareBeanFactory(beanFactory);

      同样,从名字就可以看出来是为beanFactory做一些准备工作,因为上一步已经获取到beanFactory

     protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
            beanFactory.setBeanClassLoader(this.getClassLoader());
            beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader()));
            beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, this.getEnvironment()));
         //这里设置了一个beanPostProcessor,这个接口的作用后面再说,这个接口和AOP有关 beanFactory.addBeanPostProcessor(
    new ApplicationContextAwareProcessor(this)); beanFactory.ignoreDependencyInterface(EnvironmentAware.class); beanFactory.ignoreDependencyInterface(EmbeddedValueResolverAware.class); beanFactory.ignoreDependencyInterface(ResourceLoaderAware.class); beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware.class); beanFactory.ignoreDependencyInterface(MessageSourceAware.class); beanFactory.ignoreDependencyInterface(ApplicationContextAware.class); beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory); beanFactory.registerResolvableDependency(ResourceLoader.class, this); beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this); beanFactory.registerResolvableDependency(ApplicationContext.class, this); beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this)); if (beanFactory.containsBean("loadTimeWeaver")) { beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory)); beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader())); } if (!beanFactory.containsLocalBean("environment")) { beanFactory.registerSingleton("environment", this.getEnvironment()); } if (!beanFactory.containsLocalBean("systemProperties")) { beanFactory.registerSingleton("systemProperties", this.getEnvironment().getSystemProperties()); } if (!beanFactory.containsLocalBean("systemEnvironment")) { beanFactory.registerSingleton("systemEnvironment", this.getEnvironment().getSystemEnvironment()); } }

    总结:这个里面代码一大坨,基本就是向beanFactory中设置了一个属性,为了后面使用。

    postProcessBeanFactory(beanFactory);

      这个方法在这里并没有实现,而是交给了子类实现,这里就不分析了

    invokeBeanFactoryPostProcessors(beanFactory);

      这个就是核心方法了,看一下代码

     protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
            PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, this.getBeanFactoryPostProcessors());
            if (beanFactory.getTempClassLoader() == null && beanFactory.containsBean("loadTimeWeaver")) {
                beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
                beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
            }
    
        }

    进入PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, this.getBeanFactoryPostProcessors());

      这个代码很长,但是大家不要怕,里面很多个循环只是在做重复的事情

    public static void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) {
            Set<String> processedBeans = new HashSet();
            ArrayList regularPostProcessors;
            ArrayList registryProcessors;
            int var9;
            ArrayList currentRegistryProcessors;
            String[] postProcessorNames;
         //先判断beanFactory是不是实现了BeanDefinitionRegistry
    if (beanFactory instanceof BeanDefinitionRegistry) { BeanDefinitionRegistry registry = (BeanDefinitionRegistry)beanFactory; regularPostProcessors = new ArrayList(); registryProcessors = new ArrayList(); Iterator var6 = beanFactoryPostProcessors.iterator(); while(var6.hasNext()) { BeanFactoryPostProcessor postProcessor = (BeanFactoryPostProcessor)var6.next();
              //判断postProcessor是不是BeanDefinitionRegistryPostProcessor,这个接口继承了BeanFactoryPostProcessor
    if (postProcessor instanceof BeanDefinitionRegistryPostProcessor) { BeanDefinitionRegistryPostProcessor registryProcessor = (BeanDefinitionRegistryPostProcessor)postProcessor;
                //执行方法 registryProcessor.postProcessBeanDefinitionRegistry(registry); registryProcessors.add(registryProcessor); }
    else { regularPostProcessors.add(postProcessor); } } currentRegistryProcessors = new ArrayList();
           //获取beanFactory中实现了BeanDefinitionRegistryPostProcessor接口的bean postProcessorNames
    = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false); String[] var16 = postProcessorNames; var9 = postProcessorNames.length; int var10; String ppName; for(var10 = 0; var10 < var9; ++var10) { ppName = var16[var10]; if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) { currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class)); processedBeans.add(ppName); } }        //对postProcessor排序 sortPostProcessors(currentRegistryProcessors, beanFactory); registryProcessors.addAll(currentRegistryProcessors);
           //<1.1>,执行postProcessor的postProcessBeanDefinitionRegistry()方法 invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
           //执行完了之后,清空当前的后置处理器列表 currentRegistryProcessors.clear();
           //<1.2>这一步和上一步的过程一样,但是这里获取到的这个处理器非常重要,是ConfigurationClassPostProcessor,这个类会处理所有的bean,并把其beanDefinition放入beanDefinitionMap中 postProcessorNames
    = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false); var16 = postProcessorNames; var9 = postProcessorNames.length; for(var10 = 0; var10 < var9; ++var10) { ppName = var16[var10]; if (!processedBeans.contains(ppName) && beanFactory.isTypeMatch(ppName, Ordered.class)) { currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class)); processedBeans.add(ppName); } } sortPostProcessors(currentRegistryProcessors, beanFactory); registryProcessors.addAll(currentRegistryProcessors); invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry); currentRegistryProcessors.clear(); boolean reiterate = true; while(reiterate) { reiterate = false;

              //这里的过程依然是一样的,把所有的postProcessor都执行一遍 postProcessorNames
    = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false); String[] var19 = postProcessorNames; var10 = postProcessorNames.length; for(int var26 = 0; var26 < var10; ++var26) { String ppName = var19[var26]; if (!processedBeans.contains(ppName)) { currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class)); processedBeans.add(ppName); reiterate = true; } } sortPostProcessors(currentRegistryProcessors, beanFactory); registryProcessors.addAll(currentRegistryProcessors); invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry); currentRegistryProcessors.clear(); } invokeBeanFactoryPostProcessors((Collection)registryProcessors, (ConfigurableListableBeanFactory)beanFactory); invokeBeanFactoryPostProcessors((Collection)regularPostProcessors, (ConfigurableListableBeanFactory)beanFactory); } else { invokeBeanFactoryPostProcessors((Collection)beanFactoryPostProcessors, (ConfigurableListableBeanFactory)beanFactory); } String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanFactoryPostProcessor.class, true, false); regularPostProcessors = new ArrayList(); registryProcessors = new ArrayList(); currentRegistryProcessors = new ArrayList(); postProcessorNames = postProcessorNames; int var20 = postProcessorNames.length; String ppName; for(var9 = 0; var9 < var20; ++var9) { ppName = postProcessorNames[var9]; if (!processedBeans.contains(ppName)) { if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) { regularPostProcessors.add(beanFactory.getBean(ppName, BeanFactoryPostProcessor.class)); } else if (beanFactory.isTypeMatch(ppName, Ordered.class)) { registryProcessors.add(ppName); } else { currentRegistryProcessors.add(ppName); } } } sortPostProcessors(regularPostProcessors, beanFactory); invokeBeanFactoryPostProcessors((Collection)regularPostProcessors, (ConfigurableListableBeanFactory)beanFactory); List<BeanFactoryPostProcessor> orderedPostProcessors = new ArrayList(registryProcessors.size()); Iterator var21 = registryProcessors.iterator(); while(var21.hasNext()) { String postProcessorName = (String)var21.next(); orderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class)); } sortPostProcessors(orderedPostProcessors, beanFactory); invokeBeanFactoryPostProcessors((Collection)orderedPostProcessors, (ConfigurableListableBeanFactory)beanFactory); List<BeanFactoryPostProcessor> nonOrderedPostProcessors = new ArrayList(currentRegistryProcessors.size()); Iterator var24 = currentRegistryProcessors.iterator(); while(var24.hasNext()) { ppName = (String)var24.next(); nonOrderedPostProcessors.add(beanFactory.getBean(ppName, BeanFactoryPostProcessor.class)); } invokeBeanFactoryPostProcessors((Collection)nonOrderedPostProcessors, (ConfigurableListableBeanFactory)beanFactory); beanFactory.clearMetadataCache(); }

     这个方法中写了很多的循环,每个循环干了什么事情呢?

    • 找出beanFactory中所有的实现了BeanDefinitionRegistryPostProcessor接口和BeanFactoryPostProcessor接口的bean
    • 对找出来的postProcessor进行排序
    • 执行postProcessor中的postProcessBeanDefinitionRegistry()方法和postProcessBeanFactory()方法

    那估计大家都有疑问了,为什么要写那么多循环,一个循环不就ok了,我自己觉得写一个中间那个while(reiterate)就可以了,没搞明白spring为什么要在这个while之前还要搞两个循环

    <1.1>处代码

        private static void invokeBeanDefinitionRegistryPostProcessors(Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, BeanDefinitionRegistry registry) {
            Iterator var2 = postProcessors.iterator();
    
            while(var2.hasNext()) {
                BeanDefinitionRegistryPostProcessor postProcessor = (BeanDefinitionRegistryPostProcessor)var2.next();
           //就是调用每个postProcessor中的方法 postProcessor.postProcessBeanDefinitionRegistry(registry); } }

    <1.2>处代码,ConfigurationClassPostProcessor,我们看一下这个类的回调函数

     public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
            int registryId = System.identityHashCode(registry);
            if (this.registriesPostProcessed.contains(registryId)) {
                throw new IllegalStateException("postProcessBeanDefinitionRegistry already called on this post-processor against " + registry);
            } else if (this.factoriesPostProcessed.contains(registryId)) {
                throw new IllegalStateException("postProcessBeanFactory already called on this post-processor against " + registry);
            } else {
          
    this.registriesPostProcessed.add(registryId);
           //执行这里的代码
    this.processConfigBeanDefinitions(registry); } }

    进入this.processConfigBeanDefinitions(registry);

    public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
            List<BeanDefinitionHolder> configCandidates = new ArrayList();
            String[] candidateNames = registry.getBeanDefinitionNames();
            String[] var4 = candidateNames;
            int var5 = candidateNames.length;
    
            for(int var6 = 0; var6 < var5; ++var6) {
                String beanName = var4[var6];
                BeanDefinition beanDef = registry.getBeanDefinition(beanName);
                if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
                    if (this.logger.isDebugEnabled()) {
                        this.logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
                    }
                } else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
                    configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
                }
            }
    
            if (!configCandidates.isEmpty()) {
                configCandidates.sort((bd1, bd2) -> {
                    int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
                    int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
                    return Integer.compare(i1, i2);
                });
                SingletonBeanRegistry sbr = null;
                if (registry instanceof SingletonBeanRegistry) {
                    sbr = (SingletonBeanRegistry)registry;
                    if (!this.localBeanNameGeneratorSet) {
                        BeanNameGenerator generator = (BeanNameGenerator)sbr.getSingleton("org.springframework.context.annotation.internalConfigurationBeanNameGenerator");
                        if (generator != null) {
                            this.componentScanBeanNameGenerator = generator;
                            this.importBeanNameGenerator = generator;
                        }
                    }
                }
    
                if (this.environment == null) {
                    this.environment = new StandardEnvironment();
                }
    
                ConfigurationClassParser parser = new ConfigurationClassParser(this.metadataReaderFactory, this.problemReporter, this.environment, this.resourceLoader, this.componentScanBeanNameGenerator, registry);
                Set<BeanDefinitionHolder> candidates = new LinkedHashSet(configCandidates);
                HashSet alreadyParsed = new HashSet(configCandidates.size());
    
                do {
              //这里就是解析每个bean parser.parse(candidates); parser.validate(); Set
    <ConfigurationClass> configClasses = new LinkedHashSet(parser.getConfigurationClasses()); configClasses.removeAll(alreadyParsed); if (this.reader == null) { this.reader = new ConfigurationClassBeanDefinitionReader(registry, this.sourceExtractor, this.resourceLoader, this.environment, this.importBeanNameGenerator, parser.getImportRegistry()); } this.reader.loadBeanDefinitions(configClasses); alreadyParsed.addAll(configClasses); candidates.clear(); if (registry.getBeanDefinitionCount() > candidateNames.length) { String[] newCandidateNames = registry.getBeanDefinitionNames(); Set<String> oldCandidateNames = new HashSet(Arrays.asList(candidateNames)); Set<String> alreadyParsedClasses = new HashSet(); Iterator var12 = alreadyParsed.iterator(); while(var12.hasNext()) { ConfigurationClass configurationClass = (ConfigurationClass)var12.next(); alreadyParsedClasses.add(configurationClass.getMetadata().getClassName()); } String[] var23 = newCandidateNames; int var24 = newCandidateNames.length; for(int var14 = 0; var14 < var24; ++var14) { String candidateName = var23[var14]; if (!oldCandidateNames.contains(candidateName)) { BeanDefinition bd = registry.getBeanDefinition(candidateName); if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) && !alreadyParsedClasses.contains(bd.getBeanClassName())) { candidates.add(new BeanDefinitionHolder(bd, candidateName)); } } } candidateNames = newCandidateNames; } } while(!candidates.isEmpty()); if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) { sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry()); } if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) { ((CachingMetadataReaderFactory)this.metadataReaderFactory).clearCache(); } } }

    有兴趣的可以进入parse看一下,这个就是我们每次写程序写了很多的@Service,@Component,@Controller,都是这里解析的

    总结:这个方法我只分析了invokeBeanFactoryPostProcessors,没有详细分析剩下的一大堆,如果有兴趣可以看看:【Spring】简述@Configuration配置类注册BeanDefinition到Spring容器的过程

    finishBeanFactoryInitialization(beanFactory)

      由于中间几个方法都不重要,就不分析了,直接分析这个方法

    protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
            if (beanFactory.containsBean("conversionService") && beanFactory.isTypeMatch("conversionService", ConversionService.class)) {
                beanFactory.setConversionService((ConversionService)beanFactory.getBean("conversionService", ConversionService.class));
            }
    
            if (!beanFactory.hasEmbeddedValueResolver()) {
                beanFactory.addEmbeddedValueResolver((strVal) -> {
                    return this.getEnvironment().resolvePlaceholders(strVal);
                });
            }
    
            String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
            String[] var3 = weaverAwareNames;
            int var4 = weaverAwareNames.length;
    
            for(int var5 = 0; var5 < var4; ++var5) {
                String weaverAwareName = var3[var5];
                this.getBean(weaverAwareName);
            }
    
            beanFactory.setTempClassLoader((ClassLoader)null);
            beanFactory.freezeConfiguration();
         //这一步就是将所有的非懒加载的bean初始化的步骤 beanFactory.preInstantiateSingletons(); }
    进入beanFactory.preInstantiateSingletons();
    public void preInstantiateSingletons() throws BeansException {
            if (this.logger.isTraceEnabled()) {
                this.logger.trace("Pre-instantiating singletons in " + this);
            }
    
            List<String> beanNames = new ArrayList(this.beanDefinitionNames);
            Iterator var2 = beanNames.iterator();
    
            while(true) {
                String beanName;
                Object bean;
                do {
                    while(true) {
                        RootBeanDefinition bd;
                        do {
                            do {
                                do {
                                    if (!var2.hasNext()) {
                                        var2 = beanNames.iterator();
    
                                        while(var2.hasNext()) {
                                            beanName = (String)var2.next();
                                            Object singletonInstance = this.getSingleton(beanName);
                                            if (singletonInstance instanceof SmartInitializingSingleton) {
                                                SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton)singletonInstance;
                                                if (System.getSecurityManager() != null) {
                                                    AccessController.doPrivileged(() -> {
                                                        smartSingleton.afterSingletonsInstantiated();
                                                        return null;
                                                    }, this.getAccessControlContext());
                                                } else {
                                                    smartSingleton.afterSingletonsInstantiated();
                                                }
                                            }
                                        }
    
                                        return;
                                    }
    
                                    beanName = (String)var2.next();
                                    bd = this.getMergedLocalBeanDefinition(beanName);
                                } while(bd.isAbstract());
                            } while(!bd.isSingleton());
                        } while(bd.isLazyInit());
    
                        if (this.isFactoryBean(beanName)) {
                            bean = this.getBean("&" + beanName);
                            break;
                        }
    
                        this.getBean(beanName);
                    }
                } while(!(bean instanceof FactoryBean));
    
                FactoryBean<?> factory = (FactoryBean)bean;
                boolean isEagerInit;
                if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
                    SmartFactoryBean var10000 = (SmartFactoryBean)factory;
                    ((SmartFactoryBean)factory).getClass();
                    isEagerInit = (Boolean)AccessController.doPrivileged(var10000::isEagerInit, this.getAccessControlContext());
                } else {
                    isEagerInit = factory instanceof SmartFactoryBean && ((SmartFactoryBean)factory).isEagerInit();
                }
    
                if (isEagerInit) {
                    this.getBean(beanName);
                }
            }
        }
    View Code

    这个方法,真是出奇的长,写的非常神奇,我还从没有写过这么多的循环嵌套,这个方法总的来说其实就是调用beanFactory.getBean()方法进行实例化bean,细节我是真没看头大

    总结:这个方法非常重要,尤其是getBean方法,这个方法下一篇文章会分析


    最后

      一开始的打算是把每个方法仔细分析一遍,在分析的过程中发现确实没有多大必要,里面的逻辑写的太多,把核心的看一下就可以了,其实这个refresh()方法我写的比较简单,其实里面的逻辑还是挺多的。下一篇文章介绍getBean()方法

    参考文章

    SpringBoot源码分析之Spring容器的refresh过程

    Spring源码-refresh方法

    BeanFactory和FactoryBean的区别

  • 相关阅读:
    classpath多个包添加
    不错的博客
    ARCGIS10.1 GeoDatabase深入理解:客户端连接与退出地理数据库时系统表的初始化
    ArcGIS Geodatabase版本控制机制的学习总结
    ARCGIS 10.1 发布服务问题以及注意事项汇总
    关于项目外包的一些总结
    ArcGIS与SuperMap的使用比较(1)
    [译]关于JavaScript 作用域你想知道的一切
    Apache 配置ArcGIS server/portal 反向代理
    arcgis server 无法手动删除切片
  • 原文地址:https://www.cnblogs.com/gunduzi/p/13113463.html
Copyright © 2011-2022 走看看