5

【深入浅出Spring原理及实战】「源码调试分析」结合DataSourceRegister深入分析Import...

 1 year ago
source link: https://www.cnblogs.com/liboware/p/17055173.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

人的一生中不可能会一帆风顺,总会遇到一些挫折,当你对生活失去了信心的时候,仔细的看一看、好好回想一下你所遇到的最美好的事情吧,那会让你感觉到生活的美好


注入案例代码

如何通过实现SpringBoot框架带有的ImportBeanDefinitionRegistrar注册器,注入我们想要注册的bean对象实例。只需要采用@Import的注解进行注入对应的一类相关的bean对象。

java
@Import({DataSourceRegister.class,A.class})
@SpringBootApplication
@ComponentScan("com.libo")
public class LiboApplication {
    public static void main(String[] args) {
        SpringApplication sa = new SpringApplication(LiboApplication.class);
        sa.run(args);
    }
}

DataSourceRegister的开发实现

  • 在springboot启动的时候,loader模块会根据“清单文件”加载该Application类,并反射调用psvm入口函数main,@Import注解也可以导入一个常规类,并且创建注入很多对象实例

  • DataSourceRegister类是用来进行初始化数据源和并提供了执行动态切换数据源的工具类。

DataSourceRegister注入主数据源和从数据源

这里DataSourceRegister继承的EnvironmentAware接口,没有真正意义上去用它的用途,本身可以通过这个setEnvironment方法,进行注入Environment对象,从而可以读取其他的配置信息,目前主要用作一个hook方法。

读取对应的环境变量

java
public final void setEnvironment(Environment environment) {
        DruidEntity druidEntity = FileUtil.
			readYmlByClassPath("db_info", DruidEntity.class);
        defaultTargetDataSource =
			DataSourceUtil.createMainDataSource(druidEntity);
    }

主要用于读取Druid的数据源模型信息。进行创建对应的数据源对象defaultTargetDataSource

注入Bean到Spring容器中

registerBeanDefinitions注册BeanDefinition对象模型

java
    public final void registerBeanDefinitions(AnnotationMetadata  annotationMetadata, BeanDefinitionRegistry  beanDefinitionRegistry) {
        // 0.将主数据源添加到数据源集合中
        DataSourceSet.putTargetDataSourcesMap(MAINDATASOURCE,defaultTargetDataSource);
        //1.创建DataSourceBean
        GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
        beanDefinition.setBeanClass(DataSource.class);
        beanDefinition.setSynthetic(true);
        MutablePropertyValues mpv = beanDefinition.getPropertyValues();
        //spring名称约定为defaultTargetDataSource和targetDataSources
        mpv.addPropertyValue("defaultTargetDataSource",defaultTargetDataSource);
        mpv.addPropertyValue("targetDataSources",DataSourceSet.getTargetDataSourcesMap());
        beanDefinitionRegistry.registerBeanDefinition("dataSource", beanDefinition);
    }

完整的DataSourceRegister的案例

java
public class DataSourceRegister<T> implements EnvironmentAware, ImportBeanDefinitionRegistrar {

    private javax.sql.DataSource defaultTargetDataSource;

    static final String MAINDATASOURCE = "mainDataSource";

    public final void setEnvironment(Environment environment) {
        DruidEntity druidEntity = FileUtil.
			readYmlByClassPath("db_info", DruidEntity.class);
        defaultTargetDataSource =
			DataSourceUtil.createMainDataSource(druidEntity);
    }

    public final void registerBeanDefinitions(AnnotationMetadata 
											  annotationMetadata, BeanDefinitionRegistry 
											  beanDefinitionRegistry) {
        // 0.将主数据源添加到数据源集合中
        DataSourceSet.putTargetDataSourcesMap(MAINDATASOURCE,
		defaultTargetDataSource);
        //1.创建DataSourceBean
        GenericBeanDefinition beanDefinition = new
			GenericBeanDefinition();
        beanDefinition.setBeanClass(DataSource.class);
        beanDefinition.setSynthetic(true);
        MutablePropertyValues mpv = beanDefinition.getPropertyValues();
        //spring名称约定为defaultTargetDataSource和targetDataSources
        mpv.addPropertyValue("defaultTargetDataSource",
		defaultTargetDataSource);
        mpv.addPropertyValue("targetDataSources",
		DataSourceSet.getTargetDataSourcesMap());
        beanDefinitionRegistry.registerBeanDefinition("dataSource", beanDefinition);
    }
}

执行流程解读

  • 动态数据源注册器类实现了ImportBeanDefinitionRegistrar接口,没错就是这个原因,由于实现了该接口让该类成为了拥有注册bean的能力。

  • 原理上也能说得通作为一个Bean的注册类是没有必要被注册为Spring容器的Bean对象

  • 虽然这样解释也不为过但我仍然想一探究竟,本来想大概找找spring涉及关键类如:ConfigurationClass,ConfigurationClassParser等,接下来我们需要看一下SpringBoot的总体加载流程。

SpringApplication类的run方法

SpringBoot启动时使用了SpringApplication类的run方法来牵引整个spring的初始化过程,源码如下。

java
public ConfigurableApplicationContext run(String... args) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ConfigurableApplicationContext context = null;
	   // 错误原因分析器
        FailureAnalyzers analyzers = null;
        this.configureHeadlessProperty();
	   // 重点分析:启动所有的运行的监听器
        SpringApplicationRunListeners listeners = this.getRunListeners(args);
        listeners.starting();
        try {
			// 解析ApplicationArgument数据
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
			// 通过监听器和传递的参数,实现相关的配置环境对象信息,预先
			// 进行配置Environment对象,设置全局环境变量容器
            ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
			// 输出相关的Banner控制,根据配置以及相关的Environment
            Banner printedBanner = this.printBanner(environment);
			// 创建Spring容器上下文。
            context = this.createApplicationContext();
			//创建和赋值错误解析器
            analyzers = new FailureAnalyzers(context);
			// 准备环境上下文进行配置相关的容器的上下文的参数。
            this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
			// 刷新上下文容器
            this.refreshContext(context);
			// 后置执行上下文操作
            this.afterRefresh(context, applicationArguments);
			// 完成监听器的后置完成处理操作
            listeners.finished(context, (Throwable)null);
            stopWatch.stop();
            if(this.logStartupInfo) {
                (new StartupInfoLogger(this.mainApplicationClass)).
					logStarted(this.getApplicationLog(), stopWatch);
            }
            return context;
        } catch (Throwable var9) {
            this.handleRunFailure(context, listeners, 
								  (FailureAnalyzers)analyzers, 
								  var9);
            throw new IllegalStateException(var9);
        }
    }

根据上面的源码流程可以分析重点的加载过程

  1. 重点分析:启动所有的运行的监听器,获取SpringApplicationRunListeners; 从类路径下META-INF/spring.factories。回调所有的获取SpringApplicationRunListener.starting() 方法。
java
 SpringApplicationRunListeners listeners = this.getRunListeners(args);
 listeners.starting();
  1. 解析ApplicationArgument数据以及封装命令行参数,通过program参数的部分进行解析,并且加载到PropertiesSourcePlaceHolder中的环境变量容器内部。
java
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

通过上面的监听器和传递的参数,实现相关的配置环境对象信息,预先进行配置Environment对象,设置全局环境变量容器

  1. 根据上面的全局变量容器进行配置以及初始化相关的Environment对象。
java
ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
  1. 创建Spring容器上下文,在createApplicationContext当中,由于我们是web项目,则spring默认给我们创建了一个
java
context = this.createApplicationContext();

up-2def7b2c881c5a02c53da2576fb1da9da98.png

AnnotationConfigEmbeddedWebApplicationContext,当然它也是继承GenericWebApplicationContext类和GenericApplicationContext类的,那么他默认会持有一个DefaultListableBeanFactory对象,这个对象可以用来创建Bean。

  1. 准备环境上下文进行配置相关的容器的上下文的参数
java
this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
  1. 刷新上下文容器,refreshContext就是在做spring运行后的初始化工作。
java
this.refreshContext(context);

接着往下走,进入refreshContext中会调用一系列的refresh方法,最终进入AbstractApplicationContext中,主要将SpringBoot的容器对象数据和原本基础的Spring Framework的框架对象进行加载到容器中。

java
   @Override
    public void refresh() throws BeansException, IllegalStateException {
		synchronized (this.startupShutdownMonitor) {
			// Prepare this context for refreshing.
            prepareRefresh();
            // Tell the subclass to refresh the internal bean factory.
            ConfigurableListableBeanFactory beanFactory =
				obtainFreshBeanFactory();
            // Prepare the bean factory for use in this context.
            prepareBeanFactory(beanFactory);
            try {
                // Allows post-processing of the bean factory in context subclasses.
                postProcessBeanFactory(beanFactory);

                // Invoke factory processors registered as beans in the context.
                invokeBeanFactoryPostProcessors(beanFactory);

                // Register bean processors that intercept bean creation.
                registerBeanPostProcessors(beanFactory);

                // Initialize message source for this context.
                initMessageSource();

                // Initialize event multicaster for this context.
                initApplicationEventMulticaster();

                // Initialize other special beans in specific context subclasses.
                onRefresh();

                // Check for listener beans and register them.
                registerListeners();

                // Instantiate all remaining (non-lazy-init) singletons.
                finishBeanFactoryInitialization(beanFactory);

                // Last step: publish corresponding event.
                finishRefresh();
            }

            catch (BeansException ex) {
                if (logger.isWarnEnabled()) {
                    logger.warn("Exception encountered during context 
								initialization - " +"cancelling refresh attempt: " + ex);
                }

                // Destroy already created singletons to avoid dangling resources.
                destroyBeans();

                // Reset 'active' flag.
                cancelRefresh(ex);

                // Propagate exception to caller.
                throw ex;
            }
            finally {
                // Reset common introspection caches in Spring's core, since we
                // might not ever need metadata for singleton beans anymore...
                resetCommonCaches();
            }
        }
    }

invokeBeanFactoryPostProcessors() 方法就是Bean在注册前期做的一系列数据收集工作,BeanDefinitionRegistry的容器注册BeanDefinition之前,调用相关的

跟着堆栈继续深入,会进入到这个方法中,这个方法就是初始化bean前的所有轨迹:

up-af84572fb41ca5dd4bb27c4003619ef99e9.png

在invokeBeanFactoryPostProcessors方法中继续跟进一系列方法就会看到在一开始的时候spring会初始化几个系统固有的Bean

up-bf1862d79b0e82006a60797d6206dc7c0c2.png

继续调试后的关键点出现在这个方法中:

java
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
        List<BeanDefinitionHolder> configCandidates = new ArrayList<BeanDefinitionHolder>();
        String[] candidateNames = registry.getBeanDefinitionNames();
        for (String beanName : candidateNames) {
            BeanDefinition beanDef = registry.getBeanDefinition(beanName);
            if (ConfigurationClassUtils.isFullConfigurationClass(beanDef) ||
                    ConfigurationClassUtils.isLiteConfigurationClass(beanDef)) {
                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));
            }
        }
        // Return immediately if no @Configuration classes were found
        if (configCandidates.isEmpty()) {
            return;
        }
        // Sort by previously determined @Order value, if applicable
        Collections.sort(configCandidates, new 
						 Comparator<BeanDefinitionHolder>() {
            @Override
            public int compare(BeanDefinitionHolder bd1, 
							   BeanDefinitionHolder bd2) {
                int i1 = 
					ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
                int i2 = 
					ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
                return (i1 < i2) ? -1 : (i1 > i2) ? 1 : 0;
            }
        });
        // Detect any custom bean name generation strategy supplied through the enclosing application context
        SingletonBeanRegistry sbr = null;
        if (registry instanceof SingletonBeanRegistry) {
            sbr = (SingletonBeanRegistry) registry;
            if (!this.localBeanNameGeneratorSet && sbr.containsSingleton(CONFIGURATION_BEAN_NAME_GENERATOR)) {
                BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(CONFIGURATION_BEAN_NAME_GENERATOR);
                this.componentScanBeanNameGenerator = generator;
                this.importBeanNameGenerator = generator;
            }
        }
        // Parse each @Configuration class
        ConfigurationClassParser parser = new ConfigurationClassParser(
                this.metadataReaderFactory, this.problemReporter, this.environment, this.resourceLoader, this.componentScanBeanNameGenerator, registry);
        Set<BeanDefinitionHolder> candidates = new LinkedHashSet<BeanDefinitionHolder>(configCandidates);
        Set<ConfigurationClass> alreadyParsed = new HashSet<ConfigurationClass>(configCandidates.size());
        do {
            parser.parse(candidates);
            parser.validate();
            Set<ConfigurationClass> configClasses = new LinkedHashSet<ConfigurationClass>(parser.getConfigurationClasses());
            configClasses.removeAll(alreadyParsed);
            // 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);
            alreadyParsed.addAll(configClasses);
            candidates.clear();
            if (registry.getBeanDefinitionCount() > candidateNames.length) {
                String[] newCandidateNames = registry.getBeanDefinitionNames();
                Set<String> oldCandidateNames = new HashSet<String>(Arrays.asList(candidateNames));
                Set<String> alreadyParsedClasses = new HashSet<String>();
                for (ConfigurationClass configurationClass : alreadyParsed) {
                  alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
                }
                for (String candidateName : newCandidateNames) {
                    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());

        // Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes
        if (sbr != null) {
            if (!sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
                sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
            }
        }

        if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) {
            ((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache();
        }
    }

而通过不断重复调试确定获得注册Bean的列表应该发生在配置的“剖析阶段”,也就是parser.parse(candidates);这个方法的内部,到了这里基本问题的答案已经要浮出水面了,我也不再粘贴无用的代码,如果你真的对这个问题比骄傲好奇可以自己跟踪并练习调试的源码技巧!

当然在ConfigurationClassParser这个类中parse方法也是不少,只要静下心来逐渐分析,马上就能准确的找到Override的parse方法。

java
protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
        if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
            return;
        }

        ConfigurationClass existingClass = this.configurationClasses.get(configClass);
        if (existingClass != null) {
            if (configClass.isImported()) {
                if (existingClass.isImported()) {
                    existingClass.mergeImportedBy(configClass);
                }
                // Otherwise ignore new imported config class; existing non-imported class overrides it.
                return;
            }
            else {
                // Explicit bean definition found, probably replacing an import.
                // Let's remove the old one and go with the new one.
                this.configurationClasses.remove(configClass);
                for (Iterator<ConfigurationClass> it = this.knownSuperclasses.values().iterator(); it.hasNext();) {
                    if (configClass.equals(it.next())) {
                        it.remove();
                    }
                }
            }
        }

        // Recursively process the configuration class and its superclass hierarchy.
        SourceClass sourceClass = asSourceClass(configClass);
        do {
            sourceClass = doProcessConfigurationClass(configClass, sourceClass);//处理定义的配置类
        }
        while (sourceClass != null);

        this.configurationClasses.put(configClass, configClass);
    }
java
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
            throws IOException {

        // Recursively process any member (nested) classes first
        processMemberClasses(configClass, sourceClass);

        // Process any @PropertySource annotations
        for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
                sourceClass.getMetadata(), PropertySources.class,
                org.springframework.context.annotation.PropertySource.class)) {
            if (this.environment instanceof ConfigurableEnvironment) {
                processPropertySource(propertySource);
            }
            else {
                logger.warn("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
                        "]. Reason: Environment must implement ConfigurableEnvironment");
            }
        }

        // Process any @ComponentScan annotations        Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
                sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
        if (!componentScans.isEmpty() &&
                !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
            for (AnnotationAttributes componentScan : componentScans) {
                // The config class is annotated with @ComponentScan -> perform the scan immediately
                Set<BeanDefinitionHolder> scannedBeanDefinitions =
                        this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
                // Check the set of scanned definitions for any further config classes and parse recursively if needed
                for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
                    if (ConfigurationClassUtils.checkConfigurationClassCandidate(
                            holder.getBeanDefinition(), this.metadataReaderFactory)) {
                        parse(holder.getBeanDefinition().getBeanClassName(), holder.getBeanName());
                    }
                }
            }
        }

        // Process any @Import annotations
        processImports(configClass, sourceClass, getImports(sourceClass), true);//处理注解导入的类型

        // Process any @ImportResource annotations
        if (sourceClass.getMetadata().isAnnotated(ImportResource.class.getName())) {
            AnnotationAttributes importResource =
                    AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
            String[] resources = importResource.getStringArray("locations");
            Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
            for (String resource : resources) {
                String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
                configClass.addImportedResource(resolvedResource, readerClass);
            }
        }

        // Process individual @Bean methods
        Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
        for (MethodMetadata methodMetadata : beanMethods) {
            configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
        }

        // Process default methods on interfaces
        processInterfaces(configClass, sourceClass);

        // Process superclass, if any
        if (sourceClass.getMetadata().hasSuperClass()) {
            String superclass = sourceClass.getMetadata().getSuperClassName();
            if (!superclass.startsWith("java") && !this.knownSuperclasses.containsKey(superclass)) {
                this.knownSuperclasses.put(superclass, configClass);
                // Superclass found, return its annotation metadata and recurse
                return sourceClass.getSuperClass();
            }
        }

        // No superclass -> processing is complete
        return null;
    }

以上两个方法中标红的就是关键点。而且spring的大师们也把注释写的十分明显:”//Process any @Import annotations“,到这里已经彻底豁然开朗!

spring会先去处理scan,将你程序内部的所有要注册的Bean全部获得(自然包括那些configuration),这里统称为ConfigurationClass,scan全部整理完毕后才会去处理@Import注解时导入的类!

我们回到最初的问题 DataSourceRegister和A两个类为什么A成为了Bean但DataSourceRegister却未成为Bean呢?

在processImports方法中,很明显candidate.isAssignable(ImportBeanDefinitionRegistrar.class)时操作为:configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());而普通的类通过processConfigurationClass(candidate.asConfigClass(configClass));方法,最终会被放在ConfigurationClassParser类的成员变量configurationClasses中,最终被初始化为Bean。

java
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
            Collection<SourceClass> importCandidates, boolean checkForCircularImports) throws IOException {

        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) {
                    if (candidate.isAssignable(ImportSelector.class)) {
                        // Candidate class is an ImportSelector -> delegate to it to determine imports
                        Class<?> candidateClass = candidate.loadClass();
                        ImportSelector selector = BeanUtils.instantiateClass(candidateClass, ImportSelector.class);
                        ParserStrategyUtils.invokeAwareMethods(
                                selector, this.environment, this.resourceLoader, this.registry);
                        if (this.deferredImportSelectors != null && selector instanceof DeferredImportSelector) {
                            this.deferredImportSelectors.add(
                                    new DeferredImportSelectorHolder(configClass, (DeferredImportSelector) selector));
                        }
                        else {
                            String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
                            Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
                            processImports(configClass, currentSourceClass, importSourceClasses, false);
                        }
                    }
                    else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
                        // Candidate class is an ImportBeanDefinitionRegistrar ->
                        // delegate to it to register additional bean definitions
                        Class<?> candidateClass = candidate.loadClass();
                        ImportBeanDefinitionRegistrar registrar =
                                BeanUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class);
                        ParserStrategyUtils.invokeAwareMethods(
                                registrar, this.environment, this.resourceLoader, this.registry);
                        configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
                    }
                    else {
                        // 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));
                    }
                }
            }
            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();
            }
        }
    }

后置执行上下文操作

java
this.afterRefresh(context, applicationArguments);

完成监听器的后置完成处理操作

java
listeners.finished(context, (Throwable)null);

至此,总体的mportBeanDefinitionRegistrar的对象注入体系就基本介绍完了

__EOF__


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK