人人都能看懂的Spring原理,绝对不会懵逼

  • 为什么要使用Spring?
  • Spring的核心组件
  • Spring是如何实现IOC和DI的?
    • 定义了BeanDefinition
    • 扫描加载BeanDefinition
    • 根据BeanDefinition进行Bean的实例化和初始化
      • 实例化
      • 属性赋值
      • 保存到单例缓冲池
  • 一个Bean从创建到销毁都会经历哪些步骤?
  • Spring怎么完成实例化的?
    • 推断构造
    • 实例化
  • Spring怎么完成依赖注入?
    • byName
    • byType
      • byType的初始处理
      • byType的核心处理
    • @Autowired
  • 循环依赖怎么解决?
    • 什么是循环依赖?
    • 三级缓存解决循环依赖
  • Spring怎么实现AOP的?
    • AOP是什么?为什么会有AOP?
    • AOP的核心组件
    • AOP处理
    • Spring中的AOP实现
  • Spring怎么实现事务的?
  • Spring提供了哪些扩展点?

为什么要使用Spring?

其实回答这个问题,可以反过来思考,能不能不使用Spring,不使用Spring会怎么样?

如果只是写些小Demo代码,或者开发一个小工具,那真的没必要使用Spring。

但如果是公司级的项目或系统,里面的代码量是非常庞大的,工程里面会有很多的类很多的模块系统的运行会创建很多对象,而对象与对象之间又有错综复杂的依赖关系。在没有Spring的管理下,这些对象的创建,依赖关系的维护,都需要通过硬编码进行管理,那么久而久之代码就会变得非常的难以维护,类与类之间,模块与模块之间的耦合度会非常的高,一个小改动,就有可能影响很多地方。

其实不使用Spring也不是不可以,很多开源框架也没有依赖Spring进行开发,但是它们也在进行版本的迭代和扩展,那是因为它们使用了各种设计模式优化了它们代码的设计,使其变得更好维护和扩展,或者可以说是不得不使用设计模式去优化,因为不这么做的话,它们的项目也会慢慢的变得无法再继续维护和扩展

而我们基本上都是做业务开发的,都是以实现功能为主,功能可用基本上就不会再想着去优化,所以如果不使用Spring,那么10个项目可能就是10座祖传屎山。

其实问题主要就是以下两点:

  1. 所有需要用到的对象,都需要手动创建,这些都是硬编码
  2. 对象与对象间的依赖关系,需要人为管理,对象的属性需要手动赋值,一个对象里面依赖到另外10个对象,那连带的要创建这10个对象

使用Spring,就是为了降低类与类之间,模块与模块之间的耦合度,使得代码更易于维护。

Spring的核心功能,就是管理对象的生命周期,特别是对象的创建,以及对象依赖关系的管理,也就是所谓的IOC(控制反转)和DI(依赖注入)。而其他的功能(例如AOP、事务)都相当于是扩展功能。

Spring的核心组件

先看看Spring有哪些比较核心的组件,没有列全,只列了一些主要的,混个眼熟。


其中DefaultListableBeanFactory是Bean工厂的最终实现类,最终使用到的Bean工厂就是它,而它同时又实现了BeanDefinitionRegistry(bean定义注册表),所以它也是一个bean定义注册表。

Spring是如何实现IOC和DI的?

定义了BeanDefinition

首先,Spring把我们交给他管理的对象,称为Bean,然后又定义了BeanDefinition(Bean定义)去描述这些Bean

那为什么要弄一个BeanDefinition呢?

  1. BeanDefinition相当于是对Class类元数据信息的一种扩展

  2. BeanDefinition就像是造房子前先要有的设计图纸,然后根据设计图纸来造房子

    因为Spring不是简单的帮我们创建一个对象就完事,他需要知道如何创建这个对象如何给它的属性赋值注入哪些值有哪些生命周期回调函数等等,单凭一个Class对象,不足以描述一个Bean的所有属性。
    因此,Spring把Bean的作用域,是否懒加载,属性依赖信息,自动装配模型等描述信息,封装到了BeanDefinition中。
    有了BeanDefinition之后,Spring就可以根据BeanDefinition去实例化和初始化Bean

扫描加载BeanDefinition

先有BeanDefinition,再有bean。

接下来Spring就要扫描我们的配置文件或注解,加载BeanDefinition,注册到Spring容器里面,正确来说应该是放到一个Map中,这个Map的名字叫beanDefinitionMap,key就是beanName,value就是BeanDefinition。

而这个工作是通过BeanDefinitionReader完成的。
如果是XML的配置方式,则通过XmlBeanDefinitionReader完成BeanDefinition的加载和注册,
如果是注解的配置方式,则通过AnnotatedBeanDefinitionReader完成BeanDefinition的加载和注册。

BeanDefinition最后会被注册到DefaultListableBeanFactory(也就是我们常说的的Bean工厂)的beanDefinitionMap中,DefaultListableBeanFactory实现了BeanDefinitionRegistry接口,所以拥有了BeanDefinition注册表的功能。

根据BeanDefinition进行Bean的实例化和初始化

有了BeanDefinition,就可以根据BeanDefinition进行Bean的实例化和初始化了。

实例化

首先是根据BeanDefinition进行Bean的实例化,实例化是通过反射完成的,会通过反射获取Constructor构造器对象,然后调用newInstance方法进行实例化。

但是一个类中可能有多个构造函数,具体要用哪个呢?
这就要看BeanDefinition中关于构造器参数的描述信息constructorArgumentValues,获取最匹配的构造器。这个步骤叫做推断构造
如果没有指定构造器参数,则使用的是默认的构造器。

属性赋值

Bean实例化之后,就要对Bean进行属性赋值,也就是依赖注入

Spring会根据BeanDefinition中记录的autowireMode自动注入模型,使用对应的策略进行依赖注入。

不同的自动注入模型,对应使用不同的策略为该Bean的属性找到匹配的值或对象:

  • byName:BeanFactory#getBean(属性名)
  • byType:根据类型从Spring容器中获取beanName,再通过BeanFactory#getBean(beanName)获取bean,这里如果获取到多个相同类型的bean,还要推断出一个最匹配的
  • constructor:构造器注入,不在这里处理,在实例化的时候已经处理了
  • no:不使用自动注入,使用我们手动指定的值(也就是配置文件指定要注入的属性值),手动指定的值保存在BeanDefinition的propertyValues中

为该Bean的属性找到匹配的值后,并不是马上进行属性赋值,而是收集到一个PropertyValues中,后面再根据PropertyValues为该Bean的所有属性进行赋值。PropertyValues相当于是一个对bean的不同属性要赋哪些值的描述对象。
BeanDefinition中的propertyValues也是一个PropertyValues对象,保存的是我们手动指定的属性值,这里会合并为一个PropertyValues对象。

属性赋值当然也是通过反射进行,通过setXXX方法的方法对象Method的 Method#invoke(…) 方法进行属性赋值。

保存到单例缓冲池

初始化完成后的Bean,就会放入到单例缓存池中。

这个单例缓存池的名字叫singletonObjects,也是一个Map,key是beanName,value是对应的Bean,当我们需要某个Bean时,就从该缓存池中返回。

一个Bean从创建到销毁都会经历哪些步骤?

上面这个流程是比较粗糙的,一个Bean在Spring中是有一个完整的生命周期的,也就是在实例化、初始化、初始化完成、再到销毁,有哪些步骤,要干哪些事情。

以上就是bean的生命周期的详细过程。

xxxAware接口是用于获取Spring自身的bean的,比如实现了BeanFactoryAware接口,Spring会回调setBeanFactory方法,我们就可以获取到BeanFactory,通过实现BeanNameAware接口获取beanName也是同理。

BeanPostProcessor是Spring提供的扩展点,该接口有两个方法postProcessBeforeInitializationpostProcessAfterInitialization(也就是before方法和after方法),会在bean初始化的时候被回调。用于在执行beab的初始化方法的前后,对该bean进行一些特殊处理,比如可以动态代理返回一个代理对象,替换掉原来的bean。
我们可以定义自己的BeanPostProcessor实现,然后注册到Spring容器中。

bean的初始化方法和销毁回调方法,我们应该比较熟悉。我们可以通过接口的方式xml注解,配置初始化方法和销毁回调方法。

生命周期阶段 接口 xml 注解
初始化 InitializingBean#afterPropertiesSet init-method @PostConstruct
销毁 DisposableBean#destroy destroy-method @PreDestroy

Spring怎么完成实例化的?

首先,推断构造 并且 通过构造器反射实例化 bean对象的这个过程,是在AbstractAutowireCapableBeanFactory的createBeanInstance方法里面进行的,AbstractAutowireCapableBeanFactory是DefaultListableBeanFactory的父类。

推断构造

而推断构造的过程,是由一个bean后置处理器完成的,它就是AutowiredAnnotationBeanPostProcessor,在createBeanInstance方法里面,Spring会获取所有的bean后置处理器,判断是否是SmartInstantiationAwareBeanPostProcessor类型的bean后置处理,如果是,则回调它的determineCandidateConstructors(beanClass, beanName)方法,这里就会回调AutowiredAnnotationBeanPostProcessor的determineCandidateConstructors(beanClass, beanName)方法

AutowiredAnnotationBeanPostProcessor的determineCandidateConstructors(beanClass, beanName)方法又是怎么处理的呢?

首先会通过beanClass.getDeclaredConstructors()获取所有的构造器,beanClass就是Class对象,然后遍历所有构造器对象,看构造器是否带@Autowired注解,注解required属性是否为true,是否无参构造等等,然后保存遍历结果

遍历完成后对遍历结果做处理

返回的其实不是一个构造器,而是构造器数组

其实推断构造这里还有一个名为primaryConstructor(主构造器)的东西,这个东西只有时Kotlin类才有值,非Kotlin类则为null,所以我们直接忽略。

实例化

因为推断构造返回的是一个构造器数组,所以还要从数组中筛选出最合适的构造器

  • 返回的构造器数组,如果是空,Spring会使用无参构造器进行实例化,如果没有无参构造器,就会报错
  • 构造器数组不是空,那么就要从数组中挑一个最合适的

最后就是通过InstantiationStrategy进行反射实例化,InstantiationStrategy顾名思义就是实例化策略,默认实现类是
SimpleInstantiationStrategy,它的instantiate方法,又调用了BeanUtils.instantiateClass(ctor, args),里面会通过ctor.newInstance(…)反射实例化,ctor就是最合适的构造器,args就是构造器参数。

那么,这个最合适的构造器,怎么挑选呢?

以下逻辑稍微有点复杂,如果对这一块不感兴趣的小伙伴,可以忽略

这里先记住两个概念:argsToUse将要使用的参数,constructorToUse将要使用的构造器

  1. 首先,如果我们主动传参(手动getBean并传入了参数),则argsToUse使用我们传入的参数
  2. 如果没有我们没有传参,则看BeanDefinition中是否缓存了曾经处理过的构造器和构造参数,如果有则用缓存的
  3. 缓存也没有,则要在构造器数组中挑选
  4. 如果构造器数组只有一个无参构造,并且我们没有主动传参,使用无参构造,并缓存到BeanDefinition
  5. 否则,先确定最少需要的参数个数minNrOfArgs,我们主动参数了,则minNrOfArgs就是我们传的参数个数,如果没有,但是我们在配置中指定了构造参数,则minNrOfArgs就是我们指定的参数的个数,再否则,就是0
  6. 对构造器进行排序,public优先,构造器参数多的优先
  7. 遍历构造器数组
  8. 如果已经挑选出了构造器和构造参数(constructorToUse != null && argsToUse != null),并且当前构造器所需参数更少,那么就break,也就是后面的构造器都不会比已经挑选出的更合适
  9. 如果当前构造器参数少于 minNrOfArgs,忽略
  10. 如果我们没有主动传参,则Spring要获取传给构造器的参数,如果我们在配置中指定了构造参数,则取我们在配置中指定的,如果没有,则要通过getBean去取(自动注入)
  11. 计算取得参数与当前构造器所需参数的差值
  12. 保留差值更小的
  13. 遍历结束
  14. 如果非主动传参,则把推断结果缓存到BeanDefinition中
  15. constructorToUse和argsToUse交给InstantiationStrategy,反射实例化

Spring怎么完成依赖注入?

以下是Spring对依赖注入的具体处理逻辑。

byName

byName的依赖注入比较简单,就是以属性名作为beanName,调用BeanFactory#getBean(属性名),从Spring容器中获取对应的bean,然后通过反射进行注入。

byType

byType稍微复杂一点。
byType使用到了DefaultListableBeanFactory的resolveDependency方法,这个方法顾名思义就是解决依赖关系

byType的初始处理

  1. 首先会根据属性名propertyName,生成一个属性描述符PropertyDescriptor,这个属性描述符是对当前bean的某个属性的描述。
  2. 然后根据属性描述符,生成MethodParameter对象,顾名思义,就是方法参数对象,里面包装了属性对应的setXXX方法,和set方法的参数信息。
  3. 然后根据MethodParameter对象,生成一个DependencyDescriptor依赖描述符。


PropertyDescriptor就是对bean的某个属性的一个描述对象。

MethodParameter就是对方法和方法参数等信息的封装。

DependencyDescriptor是对set方法的描述。这个描述符对象,包含了set方法所在类的Class,set方法名称,参数类型,set方法对应的属性名等。
DependencyDescriptor在后续DefaultListableBeanFactory的resolveDependency方法寻找符合依赖的bean时使用。

以上PropertyDescriptor、MethodParameter、DependencyDescriptor三个对象,其实也不是重点,如果记不住,可以直接忽略。

byType的核心处理

生成的DependencyDescriptor,然后就交给DefaultListableBeanFactory的resolveDependency方法。
然后,DefaultListableBeanFactory#resolveDependency有两步处理:

  1. 回调AutowireCandidateResolver的getSuggestedValue(DependencyDescriptor)方法,根据DependencyDescriptor依赖描述符获取对应的值。
  2. 如果AutowireCandidateResolver的getSuggestedValue方法没有返回有效的值。则通过bean的类型获取所有符合该类型的beanName,然后获取生成一个Map,key是beanName,value是beanName对应的bean,然后推断哪个bean更符合,则使用哪个bean注入。


这里出现了一个比较陌生的东西-AutowireCandidateResolver(自动装配候选解析器),其实它就是Spring定义的一个接口,里面有个getSuggestedValue方法。我们可以通过自定义一个AutowireCandidateResolver然后实现getSuggestedValue方法,实现自定义的依赖注入,例如为某些特定类型的属性,注入我们自己给定的值或者对象。比如Spring自己就实现了一个QualifierAnnotationAutowireCandidateResolver类型的AutowireCandidateResolver,getSuggestedValue方法处理@value注解修饰的属性的依赖注入,返回@Value注解上给定的值或者@Value指定的配置项对应的值。

如果AutowireCandidateResolver的getSuggestedValue方法返回了null,那么就要走接下来的步骤:通过bean类型获取所有类型匹配的beanName,进而获取所有类型匹配的bean,然后推断一个最合适的。

那如何推断最符合的bean呢?


其实就是四步:

  1. 如果被 @Primary 注解修饰的,优先使用,但是发现有多个被@Primary修饰的,则报错
  2. 如果没有@Primary注解修饰的,则看有没有被 @Priority ,如果有,则使用,发现多个被@Priority注解修饰的,则报错
  3. 如果也没有,则寻找beanName和属性名匹配的bean
  4. 如何还没有,如果该属性是必须要注入的,则报错,否则返回null

@Autowired

@Autowired注解修饰的属性的处理,比较特别。
Spring自定义了一个BeanPostProcessor实现类AutowiredAnnotationBeanPostProcessor,再进行依赖注入时会进行回调,扫描被被@Autowired注解修饰的属性,然后最终会调用DefaultListableBeanFactory的resolveDependency方法,所以最后的处理逻辑是跟byType一致的。

@Autowired如果在字段上,则通过Field#set(…)进行反射注入
@Autowired如果在方法上,则通过set方法的Method对象的Method.invoke(…)方法进行反射注入

循环依赖怎么解决?

什么是循环依赖?

就是你中有我,我中有你~

这种情况如果不做特殊处理,就会造成死循环,也就是初始化A时,发现依赖了B,就去初始化B,而初始化B时,发现依赖了A,又去初始化A…

那Spring如何解决循环依赖呢?答案就是三级环境。

三级缓存解决循环依赖

第一级缓存就是单例缓存池singletonObjects,这个是在bean初始化完成后才放入进去的,而依赖注入是在bean初始化的时候进行的,显然不能解决循环依赖。
所以应该多加一个二级缓存,在bean实例化后,初始化前,提前放入到二级缓存中,这叫提前暴露。然后当进行依赖注入时,发现属性所依赖的bean又依赖到自己,那就直接从缓存中拿

那看来搞两级缓存就可以了,为什么要搞三级呢?
那是因为Spring还要处理AOP处理,假如一个bean后面要进行AOP处理,返回一个代理对象,那这里提前暴露的又是原对象,就会出现注入的类型和最后放入到单例缓存池的类型不一致

所以再多搞一级缓存,也就是第三级缓存
第三级缓存存放什么呢?存放的是把bean包装一层后的ObjectFactory对象ObjectFactory#getObject()返回里面的bean。为什么要包装一层呢?因为不知道这个bean需不需要进行AOP,如果不需要,则返回原来的bean,如果需要,那么就要提前进行AOP处理,返回代理对象,这样依赖的bean和最后放入单例池的都是同一个对象(代理对象)。

然后依赖注入时获取依赖的bean就变成这样的流程:

三级缓存中返回的对象,放入二级缓存,然后从二级缓存删除。
实例化完成后,从二级缓存删除,放入一级缓存。

到这里,Spring最核心的原理基本上就差不多了,如果这些都能弄懂的话,可以说是对Spring有一个比较深入的理解。

当然Spring还有一些扩展的东西,比如AOP,事务,还有其他的扩展点。

Spring怎么实现AOP的?

AOP是什么?为什么会有AOP?

AOP的意思就是面向切面编程,所谓切面,可以简单理解为横跨了多个方法,就像一个切面一样,在各方法前(或者后)都要执行的代码。

比如处理请求前打印日志,进行权限校验等,这些代码在每个请求方法前都要执行,但是不可能每个方法都重复写一遍,所以通过AOP,就可以把这些每个地方都要执行的代码,提取到切面中去

AOP的核心组件

先看一下AOP的核心组件

有了解过AOP的应该对这些都很熟悉。

AOP处理

AOP处理要做的事情,就是在被Pointcut命中的方法前后(具体是前是后,要看Advice类型),织入Advice定义的增强逻辑,织入的方式就是创建一个代理对象在执行方法前(或者后),执行增强逻辑

经过代理增强之后,目标方法就会被包裹起来,根据不同的Advice(通知)类型,在它的前前后后,会有不同的增强逻辑:

Spring中的AOP实现

Spring首先实现了一个BeanPostProcessor(bean后置处理器),AnnotationAwareAspectJAutoProxyCreator,它的祖先类AbstractAutoProxyCreator在after方法定义了AOP的入口,在after方法中检查是否要对bean进行AOP处理,如果需要,则进行AOP处理。

检查一个bean是否需要进行AOP处理,其实就是拿到所有我们定义的Pointcut,看bean里面的方法有没有被Pointcut切中的,有的话,就要进行AOP处理。
Pointcut是通过MethodMatcher方法匹配器的matches方法,与方法进行匹配,匹配结果返回true或者false。

如果需要进行AOP处理,则要进行动态代理,生成代理对象。
代理方式有JDK动态代理和CGLib两种,如果bean实现了接口,则使用JDK动态代理,否则使用CGLib
生成的代理对象里,会保存一个Advisor[] 增强器数组,在方法调用时会触发Advisor里的Advice增强逻辑的调用。

在进行方法调用时,就会从Advisor中筛选出需要执行的Advisor(一个类中有多个方法,可能被不同的Pointcut切中,就要进行不同的增强逻辑),这个筛选也是通过Pointcut(一个Advisor会有一个Pointcut与之对应) 进行匹配。从筛选出的各个Advisor中取出Advice,与目标方法一起,封装为一个执行器链。然后就是责任链模式的链式调用

Spring怎么实现事务的?

Spring是通过AOP实现事务的,也就是通过AOP处理,把被@Transactional注解修饰的目标方法包裹在try-catch块里面,在进入try-catch之前,先开启事务,然后在try块中执行目标方法,如果目标方法报错,则在catch块中回滚事务,如果没有报错,则在try-catch块之后提交事务。

Spring提供了哪些扩展点?

这里就简单罗列一下Spring的扩展点、扩展点的用途、以及例子,具体细节就不展开说明了。

人人都能看懂的Spring底层原理,看完绝对不会懵逼相关推荐

  1. 小学生也能看懂的ArrayList底层原理

    阅读指引:本文包含源码,如果不想阅读源码,建议跳过 "ArrayList 的主要方法" 中的源码分析部分,直接看每一部分的小总结. 简单介绍 ArrayList是 Java 集合框 ...

  2. 10分钟看懂 Java NIO 底层原理

    写在前面 很多的小伙伴,被java IO 模型,搞得有点儿晕,一会儿是4种模型,一会儿又变成了5种模型. 很多的小伙伴,也被nio这个名词搞晕了,一会儿java 的nio 不叫 非阻塞io,一会儿ja ...

  3. 人人都能看懂的Spring源码解析,Spring如何解决循环依赖

    人人都能看懂的Spring源码解析,Spring如何解决循环依赖 原理解析 什么是循环依赖 循环依赖会有什么问题? 如何解决循环依赖 问题的根本原因 如何解决 为什么需要三级缓存? Spring的三级 ...

  4. 关于Spring底层原理面试的那些问题,你是不是真的懂Spring?

    转载自  关于Spring底层原理面试的那些问题,你是不是真的懂Spring? 1.什么是 Spring 框架?Spring 框架有哪些主要模块? Spring 框架是一个为 Java 应用程序的开发 ...

  5. 人人都能读懂的react源码解析(大厂高薪必备)

    人人都能读懂的react源码解析(大厂高薪必备) 1.开篇(听说你还在艰难的啃react源码) ​ 本教程目标是打造一门严谨(严格遵循react17核心思想).通俗易懂(提供大量流程图解,结合demo ...

  6. 一文看懂各种传感器工作原理!

    传感器是能够感受规定的被测量并按一定规律转换成可用输出信号的器件或装置的总称,通常被测量是非电物理量,输出信号一般为电量.当今世界正面临一场新的技术革命,这场革命的主要基础是信息技术,而传感器技术被认 ...

  7. 一文看懂RPA的技术原理、产品形态、设计与构建

    一文看懂RPA的技术原理.产品形态.设计与构建 过去的一年,RPA机器人流程自动化行业迎来了一个快速发展的机遇.RPA创业者得到了国内投资人的认可,一些RPA公司也接连拿到千万美金级别的融资,这在当下 ...

  8. 画图用计算机显卡,1分钟看懂显卡显卡绘图原理!

    原标题:1分钟看懂显卡显卡绘图原理! 熟悉电脑的人都知道计算机里面有个东西叫做'显卡',无论是性能爆炸的独立显卡还是羸弱的核心显卡,其中之一是家用电脑必不可少的硬件之一. 其实在电脑里面所有的数据都是 ...

  9. 手机麦克风结构原理图_让你一次看懂手机芯片的工作原理

    原标题:让你一次看懂手机芯片的工作原理 在<解析通讯技术(上)>与<解析通讯技术(下)>中,我们了解到无线通讯的频谱有限,分配非常严格,相同频宽的电磁波只能使用一次,为了解决僧 ...

最新文章

  1. 关于年长程序员的5个误传
  2. 基于图形剪切的图像分割
  3. 猴子会照镜子吗?科学家的这一研究意义非凡!
  4. Linux执行命令时遇到的些问题
  5. Python 技术篇-不使用os模块遍历文件夹,pathlib库获取直接下级文件和所有下级文件
  6. mysql drivermanager_MYSQL 之 JDBC(二): 数据库连接(二)通过DriverManager获取数据库连接...
  7. 【Antlr】识别常见的词法结构
  8. 想做Python自动化,这些Python常用知识你都掌握了吗?
  9. linux 下 Apache自启动 配置方法
  10. JAVA实训心得体会(精选4篇)
  11. 安卓反编译smali代码注入第三方广告
  12. 大数据薪水大概多少_大数据工程师工资待遇一般多少?高吗
  13. 俄罗斯方块(Tetris)
  14. 实名认证API“智慧数据”打造企业服务新入口
  15. oeasy教您玩转linux010206toilet
  16. ftt传感器_锥形量热仪FTT和CONE型号差异分析
  17. 基于sanic打造python web框架
  18. php操作mysql数据_PHP - 操作MySQL数据库
  19. php繁体网站配置,php网页是繁体字怎么办
  20. 手推Logistic Regression

热门文章

  1. Windows下用ndk编译ffmpeg
  2. 电脑桌面计算机打开很慢,我的电脑打开很慢怎么办
  3. Eclipse 开源详细介绍
  4. Scale 编程基础 A(变量声明、基本数据类型、运算符、文件读写、分支与循环、数据结构)
  5. JAVA开发与运维(Nginx配置详解)
  6. s.length什么意思
  7. 路由环路的产生及解决
  8. AVAudioPlayer常用属性
  9. Intel超线程的沉沦与逆袭
  10. Shapefile格式数据去除投影坐标系