zoukankan      html  css  js  c++  java
  • Spring源码解析之ConfigurationClassPostProcessor(三)

    在上一章笔者介绍了ConfigurationClassParser.doProcessConfigurationClass(...)方法,在这个方法里调用了processImports(...)方法处理配置类的@Import注解,getImports(sourceClass)能从一个配置类上获取@Import注解配置的所有类形成一个集合,如果集合不为空则会在下面代码的<1>处开始遍历处理。

    如果一个类是ImportSelector接口的实现类,会进入<2>处的分支,并在<3>处创建实现类的实例,在<4>处调用实例的selectImports(...)方法获取配置类列表,之后在<5>处以递归的方式调用doProcessConfigurationClass(...)将获取到的配置类传入。

    如果一个类是ImportBeanDefinitionRegistrar接口的实现类,则会进入<6>处的分支,这里依旧生成一个实现类的实例,只是这里不立马回调该接口的方法,而是暂存在配置类中,待从ConfigurationClassParser.processImports(...)逐层返回到ConfigurationClassPostProcessor.processConfigBeanDefinitions(...)的do-while循环后会执行ImportBeanDefinitionRegistrar实例的回调方法。

    如果一个类仅仅是平平无奇的配置类,即不是ImportSelector的实现类,也不是ImportBeanDefinitionRegistrar的实现类,则会进入<7>处的分支,这里会将配置类传给processConfigurationClass(...)方法,这里我们也可以认为是递归调用,这个方法笔者之前讲过,会将配置类上的@ComponentScans、@ComponentScan注解指定的类路径解析出来,根据类路径扫描BeanDefinition,再判断扫描出来的类是否有资格成为配置类,如果可以的话会再进一步解析。

    class ConfigurationClassParser {
    	……
    	protected final SourceClass doProcessConfigurationClass(
    			ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
    			throws IOException {
    		……
    		processImports(configClass, sourceClass, getImports(sourceClass), filter, true);
    		……
    	}
    	private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
    			Collection<SourceClass> importCandidates, Predicate<String> exclusionFilter,
    			boolean checkForCircularImports) {
    
    		if (importCandidates.isEmpty()) {
    			return;
    		}
    
    		if (checkForCircularImports && isChainedImportOnStack(configClass)) {
    			this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
    		}
    		else {
    			this.importStack.push(configClass);
    			try {
    				for (SourceClass candidate : importCandidates) {//<1>
    					if (candidate.isAssignable(ImportSelector.class)) {//<2>
    						// Candidate class is an ImportSelector -> delegate to it to determine imports
    						Class<?> candidateClass = candidate.loadClass();
    						ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
    								this.environment, this.resourceLoader, this.registry);//<3>
    						Predicate<String> selectorFilter = selector.getExclusionFilter();
    						if (selectorFilter != null) {
    							exclusionFilter = exclusionFilter.or(selectorFilter);
    						}
    						if (selector instanceof DeferredImportSelector) {
    							this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
    						}
    						else {
    							String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());//<4>
    							Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames, exclusionFilter);
    							processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false);//<5>
    						}
    					}
    					else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {//<6>
    						// Candidate class is an ImportBeanDefinitionRegistrar ->
    						// delegate to it to register additional bean definitions
    						Class<?> candidateClass = candidate.loadClass();
    						ImportBeanDefinitionRegistrar registrar =
    								ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
    										this.environment, this.resourceLoader, this.registry);
    						configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
    					}
    					else {//<7>
    						// Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
    						// process it as an @Configuration class
    						this.importStack.registerImport(
    								currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
    						processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter);
    					}
    				}
    			}
    			catch (BeanDefinitionStoreException ex) {
    				throw ex;
    			}
    			catch (Throwable ex) {
    				throw new BeanDefinitionStoreException(
    						"Failed to process import candidates for configuration class [" +
    						configClass.getMetadata().getClassName() + "]", ex);
    			}
    			finally {
    				this.importStack.pop();
    			}
    		}
    	}
    	……
    }
    

      

    那么我们来总结一下ConfigurationClassParser.processImports(...)完成的工作,这个方法主要用于处理配置类上的@Import注解,@Import注解允许我们引入多个配置类,在这个方法会遍历处理这些配置类,如果配置类是ImportSelector的实现类,执行实现类的回调方法能立马拿到待引入的配置类列表,我们可以递归调用processImports(...)方法将拿到的配置类列表传入,在这个方法会处理来自上层传入的配置类。如果配置类是ImportBeanDefinitionRegistrar接口的实现类,这里仅仅是实例化这个类的实例并暂存到配置类,并不立即调用实现类的回调方法,因为spring不能肯定开发者会往传入的registrar对象注册多少个配置类,甚至有的开发者一个都不会注册。如果一个类不是ImportSelector或ImportBeanDefinitionRegistrar接口的实现类,会调用processConfigurationClass(...)方法将配置类传入,我们可以认为这里是递归调用,因为processConfigurationClass(...)方法最终会执行到processImports(...)方法。processConfigurationClass(...)方法会解析配置类指定的类路径,并将可以成为BeanDefinition的类注册进spring容器,在尝试将扫描出来的类作为配置类进行解析。

    在了解ConfigurationClassParser.parse(...)方法完成的工作后,我们来整体过一下ConfigurationClassPostProcessor.processConfigBeanDefinitions(...)方法,这个方法在最开始的时候会获取spring容器所有的beanName,遍历这些beanName拿到开发者传给容器的配置类,因为开发者可能传入多个配置类,所以这里会行程一个配置类列表,如果列表为空则退出该方法,如果列表不为空,则对列表中的配置类进行排序。

    在对配置类列表排序完毕后,会进入一个do-while循环解析配置类。解析配置类是一项非常复杂的工作,解析配置类的时候不但会扫描配置类指定的类路径,还会尝试将扫描出来的BeanDefinition当做一个配置类再次进行解析,如果扫描出来的BeanDefinition可以作为配置类会再次进行二次解析,这里又会重复之前所说的解析工作,在解析完毕后,才会处理@Import注解。

    在了解完spring是如何解析配置类后,我们来再来整体过下ConfigurationClassPostProcessor.processConfigBeanDefinitions(...)的逻辑,在<1>处会先用candidateNames这个数组存放spring容器目前所有的beanName。

    candidateNames有以下两点作用:

    1. spring会在<2>处先根据candidateNames中存放的beanName过滤出可以成为配置类的的BeanDefinition。
    2. candidateNames可以用于判断解析配置类时是否有新的BeanDefinition被注册进spring容器,可以认为candidateNames是解析配置类前的快照,如果解析完配置类后发现spring容器的BeanDefinition数量大于快照中beanName的数量,表示在解析的时候有引入新的BeanDefinition。

    在<2>处完成遍历后会将配置类的BeanDefinition和beanName包装成一个BeanDefinitionHolder对象存进configCandidates列表,之后再<3>处会根据configCandidates列表生成一个配置类集合candidates,<4>处同样会根据配置类的长度生成一个集合alreadyParsed,这个集合会存放已经解析的配置类。

    之后会进入一个do-while循环,在循环中会解析、校验配置类。<5>处会获取所有解析出来的配置类,这里笔者所说的不单单是我们平常用@Configuration注解标记的配置类,在解析方法里会把类路径下描到的BeanDefinition尝试作为配置类进行解析,所以<5>处的方法还会将类路径下可以作为配置类的BeanDefinition返回。我们可以在配置类上用@Import注解引入一个ImportBeanDefinitionRegistrar实现类,也可以用@ImportResource注解引入一个XML配置文件,ImportBeanDefinitionRegistrar实现类和XML文件在解析的时候会暂存在配置类中,等到从解析方法回到do-while循环时就会在<7>处配置类中暂存的ImportBeanDefinitionRegistrar实例和XML文件,这里可能会向spring容器注册新的BeanDefinition。

    在执行完<7>处的代码后,配置类才算完全被解析,在<8>会将已经被完全解析的配置类存放进alreadyParsed集合,同时我们往回看,每次执行do-while循环时,<5>处总会把目前获取到的配置类返回,configClasses可能存在已经被完全解析的配置类,所以在<6>处会把已经完全解析的配置类移除,<7>处仅处理目前需要处理尚未回调ImportBeanDefinitionRegistrar接口的实例和XML文件。

    在<8>处将完全处理完毕的配置类放到alreadyParsed集合后,会清空candidates集合,如果在<9>处判断目前spring容器拥有的BeanDefinition数量大于解析配置类前beanName的数量,会进入分支<9>看看是否有需要再解析的配置类,如果有配置类添加到candidates集合则开启新的一轮的循环。如果<9>处判断解析完配置类后spring容器的BeanDefinition数量等于原先解析前beanName的数量,则candidates集合为空退出do-while循环。

    那么在分支<9>中又是如何判断是否应该往candidates集合添加新的配置类呢?在分支<9>内会先把spring容器目前所有的beanName存放到newCandidateNames这个数组中,oldCandidateNames用于存放解析前的beanName,alreadyParsedClasses用于存放解析后的所有配置类。之后会在<10>处遍历spring容器目前所有的beanName,如果beanName在oldCandidateNames集合中,表示其BeanDefinition在本次循环或之前的循环已经被解析,这里不会进入<11>处的分支。如果beanName不在oldCandidateNames集合中,那么这个beanName对应的BeanDefinition有可能是在解析时注册进spring容器,也可能是解析配置类完毕后在<7>处注册进spring容器的,如果是在解析时引入,那么alreadyParsedClasses会包含这个配置类,这里不会进入<12>处的分支。如果是在<7>处处理暂存在配置类的ImportBeanDefinitionRegistrar实例和XML文件,那么alreadyParsedClasses不会包含BeanDefinition对应的类,这里只要ConfigurationClassUtils.checkConfigurationClassCandidate(...)判断BeanDefinition对应的类是配置类就会把BeanDefinition和beanName包装成BeanDefinitionHolder对象存进candidates。最后在<13>处将spring目前所有的beanName存进数组candidateNames,作为下一轮循环解析前的快照。如果下一轮循环在<9>处判断spring容器中的BeanDefinition数量等于上一轮的快照数量,就会退出循环。另外进入了<9>处的分支并不意味着一定会有下个循环,do-while退出循环的条件是candidates为空,只要新引入的BeanDefinition都是解析出来的配置类,或者新引入的BeanDefinition都不是配置类,则不会进入分支<12>,则candidates为空,会退出do-while循环。

    public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor,
    		PriorityOrdered, ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware {
    	……
    	public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
    		List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
    		String[] candidateNames = registry.getBeanDefinitionNames();//<1>
    
    		for (String beanName : candidateNames) {//<2>
    			BeanDefinition beanDef = registry.getBeanDefinition(beanName);
    			if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
    				if (logger.isDebugEnabled()) {
    					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));
    			}
    		}
    		……
    		// Parse each @Configuration class
    		ConfigurationClassParser parser = new ConfigurationClassParser(
    				this.metadataReaderFactory, this.problemReporter, this.environment,
    				this.resourceLoader, this.componentScanBeanNameGenerator, registry);
    
    		Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);//<3>
    		Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());//<4>
    		do {
    			parser.parse(candidates);
    			parser.validate();
    
    			Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());//<5>
    			configClasses.removeAll(alreadyParsed);//<6>
    
    			// Read the model and create bean definitions based on its content
    			if (this.reader == null) {
    				this.reader = new ConfigurationClassBeanDefinitionReader(
    						registry, this.sourceExtractor, this.resourceLoader, this.environment,
    						this.importBeanNameGenerator, parser.getImportRegistry());
    			}
    			this.reader.loadBeanDefinitions(configClasses);//<7>
    			alreadyParsed.addAll(configClasses);//<8>
    
    			candidates.clear();
    			if (registry.getBeanDefinitionCount() > candidateNames.length) {//<9>
    				String[] newCandidateNames = registry.getBeanDefinitionNames();
    				Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));
    				Set<String> alreadyParsedClasses = new HashSet<>();
    				for (ConfigurationClass configurationClass : alreadyParsed) {
    					alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
    				}
    				for (String candidateName : newCandidateNames) {//<10>
    					if (!oldCandidateNames.contains(candidateName)) {//<11>
    						BeanDefinition bd = registry.getBeanDefinition(candidateName);
    						if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
    								!alreadyParsedClasses.contains(bd.getBeanClassName())) {//<12>
    							candidates.add(new BeanDefinitionHolder(bd, candidateName));
    						}
    					}
    				}
    				candidateNames = newCandidateNames;//<13>
    			}
    		}
    		while (!candidates.isEmpty());
    		……
    	}
    	……
    }
    

      

    至此,我们了解在执行ConfigurationClassPostProcessor.processConfigBeanDefinitions(...)的时候是如何完成扫描类路径,笔者再介绍在上面的方法是如何完成配置类的校验,如何执行配置类中暂存的ImportBeanDefinitionRegistrar实例引入新的BeanDefinition,就结束本章对ConfigurationClassPostProcessor的介绍。

    首先我们来看配置类是如何完成校验的,在执行ConfigurationClassParser.validate()方法的时候会遍历已经获取到的所有配置类ConfigurationClass.validate(...)方法,在配置类校验的时候,会检查如果配置类是full模式(有标记@Configuration注解且proxyBeanMethods为true),这个full模式的配置类是否有使用fina修饰符,如果是full模式的配置类,那么类中是否有同时使用了@Bean注解和fina修饰符的方法?之所以做这样的校验:是因为一个full模式的配置类是要使用cglib技术代理的,如果配置类本身使用了final修饰符是无法被继承的,则不能通过校验。如果full模式的配置类有用@Bean注解标记的方法,方法本身也使用了final修饰符,这个方法是不能被重写的,就不允许通过校验。

    但如果大家将@Configuration、@Bean和final修饰符搭配使用,会发现java会出现编译错误,既然连编译这关都过不了,为何还要校验呢?spring这里是防止编译出来的字节码被篡改,有人特意在编译后的full模式配置类字节码加上final修饰符,在在这个方法里再次对配置类进行校验。

    class ConfigurationClassParser {
    	……
    	public void validate() {
    		for (ConfigurationClass configClass : this.configurationClasses.keySet()) {
    			configClass.validate(this.problemReporter);
    		}
    	}
    	……
    }
    
    final class ConfigurationClass {
    	……
    	public void validate(ProblemReporter problemReporter) {
    		// A configuration class may not be final (CGLIB limitation) unless it declares proxyBeanMethods=false
    		Map<String, Object> attributes = this.metadata.getAnnotationAttributes(Configuration.class.getName());
    		if (attributes != null && (Boolean) attributes.get("proxyBeanMethods")) {
    			if (this.metadata.isFinal()) {//校验full模式的配置类是否使用final修饰符
    				problemReporter.error(new FinalConfigurationProblem());
    			}
    			for (BeanMethod beanMethod : this.beanMethods) {//校验full模式配置类的方法
    				beanMethod.validate(problemReporter);
    			}
    		}
    	}
    	……
    }
    
    final class BeanMethod extends ConfigurationMethod {
    	……
    	public void validate(ProblemReporter problemReporter) {
    		if (getMetadata().isStatic()) {
    			// static @Bean methods have no constraints to validate -> return immediately
    			return;
    		}
    
    		if (this.configurationClass.getMetadata().isAnnotated(Configuration.class.getName())) {
    			if (!getMetadata().isOverridable()) {//校验方法是否允许被重写
    				// instance @Bean methods within @Configuration classes must be overridable to accommodate CGLIB
    				problemReporter.error(new NonOverridableMethodError());
    			}
    		}
    	}
    	……
    }
    

      

    最后,我们来了解下ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(...)是如何在解析完配置类后,又从配置类加载新的BeanDefinition到spring容器里。这个方法会调用loadBeanDefinitionsForConfigurationClass(...)方法遍历传入的配置类,在代码<1>处会遍历配置类中加了@Bean注解的方法,这里会调用loadBeanDefinitionsForBeanMethod(...)将@Bean方法包装成一个BeanDefinition注册进spring容器,因为我们知道spring会根据@Bean方法实例化一个bean对象,而BeanDefinition是生产bean对象的原料。

    在根据@Bean方法生成BeanDefinition时,会先在<2>处获取方法名,然后在<3>处获取bean对象的别名,并在<4>处建立beanName和别名的映射。之后会在<5>处根据配置类、元数据和方法名创建一个ConfigurationClassBeanDefinition对象,创建bean对象的方法是否是静态决定了控制流是进入<6>处还是<7>处的分支。如果是用静态方法来创建bean对象,在<6>处就设置静态方法对应的类以及方法名。如果是实例方法创建bean对象则进入<7>处的分支,在<7>处的分支会设置可以创建当前bean对象的工厂beanName,即配置类的beanName,再设置创建bean对象的方法名。

    在<8>处会设置@Bean方法的BeanDefinition的默认自动装配模型为AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR,但@Bean注解默认的自动装配模型为Autowire.NO,在<9>处会获取@Bean注解配置的自动装配模型并存放到BeanDefinition。在<10>处会获取@Bean注解设置的是否自动参与候选,默认为true。之后在<11>处获取初始化方法,在<12>处获取销毁方法,如果这两个字段有设置的话。最后在<13>处将beanName和BeanDefinition的映射注册进spring容器中。这里我们又看到一个BeanDefinition的实现类型。

    在<1>处的方法遍历完所有的beanMethod将其BeanDefinition注册进spring容器后,会分别在<14>和<15>处处理暂存在配置类的XML配置文件和ImportBeanDefinitionRegistrar实例。在<15>处loadBeanDefinitionsFromRegistrars(...)方法的实现我们也看到在这个方法内会遍历配置类中所有的ImportBeanDefinitionRegistrar实例,回调其registerBeanDefinitions(...)方法,在这个方法中开发者可以向spring容器注册新的BeanDefinition。

    class ConfigurationClassBeanDefinitionReader {
    	……
    	public void loadBeanDefinitions(Set<ConfigurationClass> configurationModel) {
    		TrackedConditionEvaluator trackedConditionEvaluator = new TrackedConditionEvaluator();
    		for (ConfigurationClass configClass : configurationModel) {
    			loadBeanDefinitionsForConfigurationClass(configClass, trackedConditionEvaluator);
    		}
    	}
    	private void loadBeanDefinitionsForConfigurationClass(
    			ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {
    		……
    		for (BeanMethod beanMethod : configClass.getBeanMethods()) {//<1>
    			loadBeanDefinitionsForBeanMethod(beanMethod);
    		}
    
    		loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());//<14>
    		loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());//<15>
    	}
    	……
    	private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) {
    		ConfigurationClass configClass = beanMethod.getConfigurationClass();
    		MethodMetadata metadata = beanMethod.getMetadata();
    		String methodName = metadata.getMethodName();//<2>
    		……
    		AnnotationAttributes bean = AnnotationConfigUtils.attributesFor(metadata, Bean.class);
    		Assert.state(bean != null, "No @Bean annotation attributes");
    
    		// Consider name and any aliases
    		List<String> names = new ArrayList<>(Arrays.asList(bean.getStringArray("name")));//<3>
    		String beanName = (!names.isEmpty() ? names.remove(0) : methodName);
    
    		// Register aliases even when overridden
    		for (String alias : names) {//<4>
    			this.registry.registerAlias(beanName, alias);
    		}
    
    		……
    		ConfigurationClassBeanDefinition beanDef = new ConfigurationClassBeanDefinition(configClass, metadata, beanName);//<5>
    		beanDef.setSource(this.sourceExtractor.extractSource(metadata, configClass.getResource()));
    
    		if (metadata.isStatic()) {//<6>
    			// static @Bean method
    			if (configClass.getMetadata() instanceof StandardAnnotationMetadata) {
    				beanDef.setBeanClass(((StandardAnnotationMetadata) configClass.getMetadata()).getIntrospectedClass());
    			}
    			else {
    				beanDef.setBeanClassName(configClass.getMetadata().getClassName());
    			}
    			beanDef.setUniqueFactoryMethodName(methodName);
    		}
    		else {//<7>
    			// instance @Bean method
    			beanDef.setFactoryBeanName(configClass.getBeanName());
    			beanDef.setUniqueFactoryMethodName(methodName);
    		}
    
    		if (metadata instanceof StandardMethodMetadata) {
    			beanDef.setResolvedFactoryMethod(((StandardMethodMetadata) metadata).getIntrospectedMethod());
    		}
    
    		beanDef.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR);//<8>
    		beanDef.setAttribute(org.springframework.beans.factory.annotation.RequiredAnnotationBeanPostProcessor.
    				SKIP_REQUIRED_CHECK_ATTRIBUTE, Boolean.TRUE);
    
    		AnnotationConfigUtils.processCommonDefinitionAnnotations(beanDef, metadata);
    
    		Autowire autowire = bean.getEnum("autowire");//<9>
    		if (autowire.isAutowire()) {
    			beanDef.setAutowireMode(autowire.value());
    		}
    
    		boolean autowireCandidate = bean.getBoolean("autowireCandidate");//<10>
    		if (!autowireCandidate) {
    			beanDef.setAutowireCandidate(false);
    		}
    
    		String initMethodName = bean.getString("initMethod");//<11>
    		if (StringUtils.hasText(initMethodName)) {
    			beanDef.setInitMethodName(initMethodName);
    		}
    
    		String destroyMethodName = bean.getString("destroyMethod");//<12>
    		beanDef.setDestroyMethodName(destroyMethodName);
    		……
    		BeanDefinition beanDefToRegister = beanDef;
    		……
    		this.registry.registerBeanDefinition(beanName, beanDefToRegister);//<13>
    	}
    	……
    	private void loadBeanDefinitionsFromRegistrars(Map<ImportBeanDefinitionRegistrar, AnnotationMetadata> registrars) {
    		registrars.forEach((registrar, metadata) ->
    				registrar.registerBeanDefinitions(metadata, this.registry, this.importBeanNameGenerator));
    	}
    	……
    }
    

      

  • 相关阅读:
    CAS在tomcat6.0.18下的SSO
    CAS在tomcat6.0.18下的SSO
    PL/SQL 0.几秒出结果,SQL效率一定高吗?
    优化大型复杂SQL
    C++ ProtoBuf小结
    protobuf c++入门
    Oracle_inner join,left join,right join,full join 的区别,用例子说明
    Linux_查看修改SWAP大小
    Oracle 优化器
    Oracle 行转列两种方法
  • 原文地址:https://www.cnblogs.com/beiluowuzheng/p/15181944.html
Copyright © 2011-2022 走看看