记录每一次进步

工作生活中的每一次坑,都是程序员的一块勋章

Java虚拟机除了上一篇文章当中提到的内存管理外,另外一个重要的知识便是垃圾收集。在进行收集垃圾之前,我们首先要考虑一下三个问题:

  • 哪些内存需要回收
  • 什么时候回收
  • 如何回收

哪些内存是垃圾

一块内存是垃圾,也就是说在该内存上的对象已经不再使用了,需要去进行回收,那么,如何来判别对象是否已经不再用了,需要进行回收呢?这里有两个方法:

  • 引用计数算法
    给对象添加一个引用计数器,当每有一个地方引用它时,引用器值加1,当引用失效时,计数器减1。当计数器为0的时候,也就表示对象不可能再被使用了。这个方法实现简单,判定效率也很高。但是存在一个问题,就是它很难解决对喜爱那个之间互相循环引用的问题。比如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class ReferenceGc {
    public Object instance = null;
    private static final int _1MB = 1024*1024;
    }

    ...

    public static void testGC() {
    ReferenceGc objA = new ReferenceGc();
    ReferenceGc objB = new ReferenceGc();
    objA.instance = objB;
    objB.instance = objA;
    }

    在函数testGC创建了两个对象,在函数退出之后,objA和objB已经不会再被使用了,但是由于它们之间存在着引用,故引用计数算法得出的结论是objA与objB还在被引用,也就不会将这两个对象作为垃圾。

  • 可达性分析算法
    算法的基本思想如下:选择一些GC Roots作为起点,根据引用关系遍历所有能够到达的对象,遍历的路经称作引用链,当一个对象与GC Roots没有引用链的时,则证明了该对象是不可用的,也就是需要回收的垃圾。
    GC Roots的选择如下:

    • 虚拟机栈(栈帧中的本地变量表)中引用的对象
    • 方法区中类静态属性引用的对对象
    • 方法区中产量引用的对象
    • 本地方法栈中JNI引用的对象

如对象到GC Roots之间存在引用链,则根据引用的类型不同,是否进行回收也存在着区别,四个引用强度一次逐渐减弱:
强引用
即Object obj = new Object();即为强引用。

软引用
描述一些没有用但是非必需的对象,对于软引用,除非系统将要发生内存溢出异常,才会去进行回收。

弱引用
被弱引用引用的对象会被进行回收

虚引用
一个对象是否有虚引用,完全不会对其生存时间构成影响,也无法通过虚引用老获得一个对象实例。为对象设置虚引用的唯一目的就是能在对象被收集器回收时收到一个系统通知。

垃圾收集算法

几种主要的垃圾收集算法:

  • 标记-清除算法
    对垃圾进行标记,然后清除垃圾,但是由于垃圾不一定是连续的,会存在碎片
  • 复制算法
    将内存分为成两个部分,在一起一块上分配内存创建对象,进行垃圾收集时,将存活的对象复制到另外一块内存上。
  • 标记-整理算法
    对垃圾进行标记,然后清除垃圾,然后整理内存,去除碎片
  • 分代收集算法
    将内存分成新生代和老生代,对不同的代使用不同的收集算法

当前商业虚拟机的垃圾收集都采用“分代收集”算法。

垃圾收集器

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。Java虚拟机规范中没有对垃圾和搜集其应该如何实现做规定,因此不同的厂商和不同版本的虚拟机所提供的垃圾收集器都可能会有很大的差别。这里讨论的收集器是基于JDK1.7 Update14之后的HotSpot虚拟机器。
GC 分类
上图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器。

几个概念

并发和并行

  • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行。而垃圾收集程序运行在另一个CPU上。

Minor GC 和 Full GC

  • 新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。具体原理见上一篇文章。
  • 老年代GC(Major GC / Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。

新生代收集器

Serial收集器

Serial收集器是最基本、发展历史最悠久的新生代收集器,也是原理最简单的收集器。它是一个采用复制算法的单线程收集器,只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直至Serial收集器收集结束为止。
SerialNew收集器
该收集器目前是HotSpot虚拟机运行在Client模式下的默认的新生代收集器。它也有着优于其他收集器的地方:简单而高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得更高的单线程收集效率。

ParNew 收集器

ParNew收集器就是Serial收集器的多线程版本,它也是一个新生代收集器。除了使用多线程进行垃圾收集外,其余行为包括Serial收集器可用的所有控制参数、收集算法(复制算法)、Stop The World、对象分配规则、回收策略等与Serial收集器完全相同。
ParNew收集器
该虚拟机是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关的重要原因是,除了Serial收集器外,目前只有它能和CMS收集器(Concurrent Mark Sweep)配合工作。它默认开启的收集线程数与CPU的数量相同,在CPU非常多的情况下可使用-XX:ParallerGCThreads参数设置。

Parallel Scavenge 收集器

Parallel Scavenge收集器也是一个并行的多线程新生代收集器,它也使用复制算法。Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

Parallel Scavenge收集器除了会显而易见地提供可以精确控制吞吐量的参数,还提供了一个参数-XX:+UseAdaptiveSizePolicy,这是一个开关参数,打开参数后,就不需要手工指定新生代的大小(-Xmn)、Eden和Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为GC自适应的调节策略(GC Ergonomics)。自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。

老年代收集器

Serial Old收集器

Serial Old 是 Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”(Mark-Compact)算法。收集器的主要意义也是在于给Client模式下的虚拟机使用。Serial Old收集器的工作过程如图所示:
SerialOld收集器

Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。Parallel Old收集器的工作过程如图所示:
Parallel Old收集器

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它非常符合那些集中在互联网站或者B/S系统的服务端上的Java应用,这些应用都非常重视服务的响应速度。从名字上(“Mark Sweep”)就可以看出它是基于“标记-清除”算法实现的。
CMS收集器工作的整个流程分为以下4个步骤:

  • 初始标记(CMS initial mark):仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。
  • 并发标记(CMS concurrent mark):进行GC Roots Tracing的过程,在整个过程中耗时最长。
  • 重新标记(CMS remark):为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要“Stop The World”。
  • 并发清除(CMS concurrent sweep)

CMS收集器的工作过程如图所示:
CMS收集器

优点

CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停顿,因此CMS收集器也被称为并发低停顿收集器(Concurrent Low Pause Collector)。

缺点

  • 对CPU资源非常敏感 其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个时(比如2个),CMS对用户程序的影响就可能变得很大,如果本来CPU负载就比较大,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了50%,其实也让人无法接受。
  • 无法处理浮动垃圾(Floating Garbage) 可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生。这一部分垃圾出现在标记过程之后,CMS无法再当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就被称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。
  • 标记-清除算法导致的空间碎片 CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象。

G1收集器

G1(Garbage-First)是一款面向服务端应用的垃圾收集器,HotSpot开发团队赋予它的使命是(在比较长期的)未来可以替换掉JDK 1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点:

  • 并行与并发 G1 能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短“Stop The World”停顿时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
  • 分代收集 与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次GC的旧对象来获取更好的收集效果。
  • 空间整合 G1从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。这意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。此特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
  • 可预测的停顿 这是G1相对CMS的一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

G1收集器的内存模型

在G1之前的其他收集器进行收集的范围都是整个新生代或者老生代,而G1不再是这样。G1在使用时,Java堆的内存布局与其他收集器有很大区别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,而都是一部分Region(不需要连续)的集合。

工作过程

如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:

  • 初始标记(Initial Marking) 仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的Region中创建对象,此阶段需要停顿线程,但耗时很短。
  • 并发标记(Concurrent Marking) 从GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。
  • 最终标记(Final Marking) 为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。
  • 筛选回收(Live Data Counting and Evacuation) 首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率,流程图如下:
    G1收集器

总结

根据上述几种垃圾收集器的工作原理及工作区域,进行一个归总:

收集器 串行、并行or并发 新生代/老年代 算法 目标 适用场景
Serial 串行 新生代 复制算法 响应速度优先 单CPU环境下的Client模式
Serial Old 串行 老年代 标记-整理 响应速度优先 单CPU环境下的Client模式、CMS的后备预案
ParNew 并行 新生代 复制算法 响应速度优先 多CPU环境时在Server模式下与CMS配合
Parallel Scavenge 并行 新生代 复制算法 吞吐量优先 在后台运算而不需要太多交互的任务
Parallel Old 并行 老年代 标记-整理 吞吐量优先 在后台运算而不需要太多交互的任务
CMS 并发 老年代 标记-清除 响应速度优先 集中在互联网站或B/S系统服务端上的Java应用
G1 并发 both 标记-整理+复制算法 响应速度优先 面向服务端应用,将来替换CMS

参考资料 : 深入理解Java虚拟机(第2版)

欢迎关注个人公众号:
个人公号

虚拟机运行时数据区域分布及使用

博主之前有一年多的时间使用过C++进行编程,内存的管理是个很让人头大的问题,内存的释放需要自己去delete,不然会发生内存泄漏的问题。开发过程当中内存操作不当导致的bug占了很大的比例,更不用说说,在多线程情况下对内存的管理了。自从开始使用Java之后,深感Java虚拟机自动内存管理的便利性。最近重读《深入理解Java虚拟机》一书,对本书内容进行一些记录。首先我们就来看看Java虚拟机的内存分布以及使用。

虚拟机的运行时数据区域

虚拟机运行时数据区域

虚拟机各内存区域的使用

虚拟机分配的内存大致可以分为两个部分,一部分是线程私有的内存区域,一部分是进程内线程共有的内存区域。此外,还有一块直接内存,这部分内存不是虚拟机运行是数据区的一部分,也不是Java虚拟机规范中所定义的内存区域,但是这部分内存也会被使用到,比如NIO时会在该区域进行内存分配。

程序计数器

相信学习过计算机组成原理与汇编知识的读者都知道,在指令的执行过程当中,存在一个计数器,用来指示当前执行到了哪一条指令,其实程序计数器可以看作是当前线程执行的字节码的行号指示器,保存着当前指令的地址。由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,个线程之间程序计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

虚拟机栈

虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧用于存储局部变量表,操作数栈,动态链接,方法出口等信息。每一个方法从调用直到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈和出栈的过程。
在Java虚拟机规范中,对该区域规定了两种异常。如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常;如果虚拟机可以动态国展,如果扩展是无法申请到足够的内存,就会抛出OutOfMemoryEror异常。

本地方法栈

与虚拟机栈所发挥的作用是非常相似的,只不过虚拟机栈执行为虚拟机执行Java服务,而本地方法栈则为虚拟机使用到的Native方法服务。有的虚拟机(如Sun的HotSpot需积极)直接将本地方法栈和虚拟机栈合二为一。

方法区

方法区与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。该部分内存可以成为“永生区”。根据Java虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError异常。

运行时常量池

在方法区中,存在一块内存区域——运行时常量池,用于存放编译期间生成的各种字面量和符号引用,这部分内容将在类加载进入方法去的运行时常量池中存放。

堆区

Java对是所有线程共享的一筐内存区域,也是Java虚拟机所管理的内存中最大的一块,所有对象实例及数组都要在对上分配内存。Java堆是垃圾收集器管理的主要区域,根据Java虚拟机规范的规定,Java堆可以处于物理上不联系的内存空间,只要逻辑上是连续的即可。
从内存回收的角度看,Java对中可以细分为:

  • 新生代
    • Eden空间
    • From Survivor空间
    • To Survivor空间
  • 老生代

该部分内容将在下一篇博客《虚拟机垃圾收集算法及垃圾收集器》中介绍

从内存分配的角度看,线程共享的Java队中可能划分出多个线程私有的分配缓冲区(TLAB)。
如果在堆中没有内存完成实例的分配,并且堆无法再扩展时,将会抛出OutOfMemoryError异常。

直接内存

直接内存并不是虚拟及运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。在JDK1.4中加入的NIO类,引入了一种基于通道与缓冲区的I/O方式,它何以通过Native函数库直接分配堆外内存,也就是直接内存部分。

欢迎关注个人公众号:
个人公号

Spring Boot 启动流程详解

    使用SpringBoot已经有好几个月了,一直对框架当中Bean的实例化过程有很感兴趣,比如Spring是如何去加载.class文件,并解析其中我们通过注解@Configuration, @PropertySource,@ComponentScan,@Import,@Bean等产生我们自定义的Bean的。下面我们就来看看,框架当中是如何实现Bean的加载与创建的。

下面是SpringApplication函数的run函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
FailureAnalyzers analyzers = null;
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
analyzers = new FailureAnalyzers(context);
prepareContext(context, environment, listeners, applicationArguments,
printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
listeners.finished(context, null);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass)
.logStarted(getApplicationLog(), stopWatch);
}
return context;
}
catch (Throwable ex) {
handleRunFailure(context, listeners, analyzers, ex);
throw new IllegalStateException(ex);
}
}

    注意,在完成了context的创建之后,会调用refreshContext函数,refreshContext函数中会调用refresh函数,在该函数中又会先调用祖先类AbstractApplicationContext类的refresh函数。该函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();

// Tell the subclass to refresh the internal bean factory.
// 初始化BeanFactory
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.
//在子类中注册BeanFactory后处理器
postProcessBeanFactory(beanFactory);

// Invoke factory processors registered as beans in the context.
//调用工厂后处理器
invokeBeanFactoryPostProcessors(beanFactory);

// Register bean processors that intercept bean creation.
//注册Bean后处理器
registerBeanPostProcessors(beanFactory);

// Initialize message source for this context.
// 初始化信息源
initMessageSource();

// Initialize event multicaster for this context.
// 初始化应用上下文时间广播
initApplicationEventMulticaster();

// Initialize other special beans in specific context subclasses.
// 初始化其他特殊的Bean,有具体的子类来实现
onRefresh();

// Check for listener beans and register them.
// 注册事件监听器
registerListeners();

// Instantiate all remaining (non-lazy-init) singletons.
// 初始化所有非懒加载的单例Bean
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,在该函数的调用流程当中,调用链为PostProcessorRegistrationDelegate ->ConfigurationClassPostProcessor -> ConfigurationClassParser,在类ConfigurationClassParser类的doProcessConfigurationClass函数中,将启动类(即@SpringBootApplication标注的类,主要是@SpringBootApplication注解中的@Configuration注解的作用)做为入口,读取我们所定义的Bean,将其转化为一个个的BeanDefinition。下面,我们来看一下该函数的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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;
}

    在该代码块中我们可以看到,函数首先处理由注解@PropertySource,该注解用于将我们自定义的属性文件加载到上下文环境当中。然后对@ComponentScan注解定义的包结构进行扫描,将启动类所在包下的子包中Bean文件(不包括子文件夹中的Bean文件)定义转化为BeanDefinition,如果该Bean定义的文件中也使用了@Configuration对其进行了注解,那么将会进行递归。之后,解析由注解@Import和@ImportResource注解导入的Bean定义文件,最后是直接由@Bean注解的成员函数。之后所有需要实例化的Bean都已经完成了解析,都转化为BeanDefinition,将由refresh函数中的finishBeanFactoryInitialization根据这些信息对Bean进行实例化。

欢迎关注个人公众号:
个人公号

    前两天在使用spring Bean的时候,发现跑出来的数据总是存在着一些奇怪的值,这些值在当前的处理当中本该不会出现,找了半天,发现时上一个线程使用该Bean后的遗留的值。后面查看了spring Bean的相关知识后,找到了问题的原因。 使用了ThreadLocal——线程本地变量的方式解决了问题。对问题产生的原因做一个分析和总结。

问题原因

    Spring框架里的Bean,默认为单例模式,这是在多线程开发的时候要尤其注意的地方。
    当多用户同时请求一个服务时,容器会给每一个请求分配一个线程,这是多个线程会并发执行该请求多对应的业务逻辑(成员方法),此时就要注意了,如果该处理逻辑中有对该单例Bean状态的修改(体现为该单例Bean的成员属性),则必须考虑线程同步问题。

使用ThreadLocal解决线程安全问题

    ThreadLocal和线程同步机制相比有什么优势呢?ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。
    由于ThreadLocal中可以持有任何类型的对象,低版本JDK所提供的get()返回的是Object对象,需要强制类型转换。但JDK 5.0通过泛型很好的解决了这个问题,在一定程度地简化ThreadLocal的使用,概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
    我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如TransactionSynchronizationManager、LocaleContextHolder、RequestContextHolder、Hibernate的AnnotationSessionFactoryBean等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的Bean就可以在多线程中共享了。
    一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程
    ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
    如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。 或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。  线程安全问题都是由全局变量及静态变量引起的。
    若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

线程安全的情况

  1. 常量始终是线程安全的,因为只存在读操作。
  2. 每次调用方法前都新建一个实例是线程安全的,因为不会访问共享的资源。
  3. 局部变量是线程安全的。因为每执行一个方法,都会在独立的空间创建局部变量,它不是共享的资源。局部变量包括方法的参数变量和方法内变量。

有状态与无状态

    有状态就是有数据存储功能。有状态对象(Stateful Bean),就是有实例变量的对象 ,可以保存数据,是非线程安全的。在不同方法调用间不保留任何状态。
    无状态就是一次操作,不能保存数据。无状态对象(Stateless Bean),就是没有实例变量的对象 .不能保存数据,是不变类,是线程安全的。
    无状态的Bean适合用不变模式,技术就是单例模式,这样可以共享实例,提高性能。
    有状态的Bean,多线程环境下不安全,那么适合用Prototype原型模式。Prototype: 每次对bean的请求都会创建一个新的Bean实例。

欢迎关注个人公众号:
个人公号

基本概念

  1. 文档
    文档是有序集合里面的一个键值对的有序集合。文档的键是字符串(除了少数情况,键可以用任何的UTF-8)。相当于关系型数据库当中的表table。
  2. 集合
    集合就是一组文档,相当于关系型数据库当中的一行数据。

数据类型

  1. null
  2. 布尔型true or false
  3. 数值型
        默认为64位的浮点型数值,如果要使用整形值,可以使用NumberInt类(表示4字节带符号整数)或NumberLong类(表示8字符带符号整数),如
         {"x":NumberInt("3")}
        {"x":NumberLong("3")}
  4. 字符串
  5. 日期
        如{“x”:new Date()}
  6. 正则表达式,语法于JavaScript的正则表达式语法相同。
  7. 数组
         数组列表或数据集可以表示为数组:
         {"x":["a","b","c"]}
  8. 内嵌文档
        文档可以嵌套其他文档,被嵌套的文档作为父文档的值。如:
         {"x":{"y":NumberInt("3")}}
  9. 对象id
        对象id是一个12字节的ID,是文档的唯一标识。
         {"x",ObjectId()}
  10. 二进制数据
  11. 代码
         查询和文档可以包含任意的JavaScript代码:
         {"x":function(){/* ... */}}

创建、更新和删除文档

操作

  • 插入文档
       插入一个文档
    db.foo.insert({"bar":"baz"})
      批量插入
    db.foo.batchInsert({"_id":0},{"_id":1},{"_id":2})

  • 删除文档
      删除文档
    db.foo.remove()
    db.foo.remove({"opt_out":true})
      如果要删除全部的文档,使用drop直接删除集合会更快。

  • 更新文档
        文档存入数据库后,就可以使用update方法来更新文档。update有两个参数,一个是查询文档,用于定位需要更新的目标文档;另一个是修改器文档,用于说明要对找到的文档进行那些修改。
        文档的修改可以分为两种类型,一种是文档的替换,就是将文档内容整个地替换掉。另外一种是文档的部分字段的更新。在进行文档的部分字段的更新时,必须使用修改器,否则文档会被全部替换。

    1
    2
    db.user.update({"name":"joe"},{"address":"12345"})
    db.user.update({"name":"joe"},{"$set":{"address":"12345"}})

        第一个更新操作会将name为Joe的文档替换掉,第二个更新操作会将name为joe的文档的address内容替换为12345。因此,在修改文档的字段内容的时候,必须使用修改器。
        update函数的第三个参数为一个布尔型的值,指示update函数是否为upset。当为true(即upset)时,要是没有找到符合更新条件的文档,就会以这个条件和更新文档为基础创建一个新的文档。如果找到了匹配的文档,则进行正常的更新。
        update函数的第四个参数为一个布尔型的值,用于指示是否更新所有匹配的文档。update默认情况下只更新第一个匹配到的文档。因此,如果要更新所有匹配的文档,则需要将改参数设置为true。

常用的修改器

  • $set 用于设置文档中某一个键对应的值,如果对应的键不存在,则会新增加一个键值对。

  • $inc 用于增加已有键的值,如果对应的键不存在那就创建一个。$set只能用于整形,长整形或者双精度浮点型的值。

  • $push 用于操作数组。如果数组已经存在,$push会向已有的数组末尾加入一个元素,要是没有则会创建一个数组。

    1
    db.stocker.update({"_id":"GOOD"},{"$push":{"comment":"123"}})

        如果_id为GOOD的文档中不存在comment键,则新创建一个,如果存在的话,则将123添加到comment键的值当中,成为一个数组。
        也可以和$each一起使用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    db.stocker.update({"_id":"GOOD"},{"$push":{"comment":{"$each":["123","456","789"]}}})
    ```
    &ensp;&ensp;&ensp;&ensp;这样,三个字符串就都被添加到comment键对应的数组当中了。

    - $ne $andToSet 当我们将数组座位数据集使用的时候,需要保证数组内的元素不会重复,可以使用$ne $andToSet。

    - $pull 用于删除数组当中的元素。

    #四. 查询
    ## find函数
    &ensp;&ensp;&ensp;&ensp;MongoDB数据查询使用find函数,其功能与SQL中的select函数相同,可提供与关系型数据库类似的许多功能,包含映射、排序等。

    &ensp;&ensp;&ensp;&ensp;find函数语法
    >db.COLLECTION_NAME.find(query,fields,limit,skip)

    &ensp;&ensp;&ensp;&ensp;find() 方法以非结构化的方式来显示所有文档。
    &ensp;&ensp;&ensp;&ensp;如果你需要以易读的方式来读取数据,可以使用 pretty() 方法,语法格式如下:
    >db.COLLECTION_NAME.find().pretty()

    &ensp;&ensp;&ensp;&ensp;pretty() 方法以格式化的方式来显示所有文档。
    <font color='red'>**参数说明**</font>:

    - query:指明查询条件,相当于SQL中的where语句。
    `db.account.find({name:"lewesyang",age:{$it:22}})`
    - fields:用于字段映射,指定是否返回该字段,0代表不返回,1代表返回
    `db.account.find({name:"lewesyang",age:{$it:22}},{"age":0})`
    - limit:限制查询结果集的文档数量,指定查询返回结果数量的上限
    `db.account.find({name:"lewesyang",age:{$it:22}},{"age":0},5) `
    - skip:跳过一定数据量的结果,设置第一条返回文档的偏移量
    `db.account.find({name:"lewesyang",age:{$it:22}},{"age":0},5,10) `

    &ensp;&ensp;&ensp;&ensp;单独使用limit和skip语法:<br>
    &ensp;&ensp;&ensp;&ensp;&ensp;`db.account.find().limit(5).skip(10)`


    <font color='red'>**注意事项**</font>:

    - MongoDB不支持多集合间的连接查询,find函数一次查询只能针对一个集合。
    - find参数为空或者查询条件为空文档时,会返回集合中的所有文档。
    - 除了将limit和skip作为find函数的参数外,还可以单独使用limit和skip函数来修饰查询结果。
    - 返回的查询结果集默认情况下是无序的,如果需要对结果进行排序,可以使用sort函数:
    `db.account.find().sort({age:-1})`
    - db.collection.findOne()只会返回第一条数据。
    - 当查询的集合文档数量很大时,为了加快数据的查询速度可以创建索引。
    - 除了使用find函数实现基本查询外,MongoDB还提供了聚合框架,用于复杂查询。

    ##2. 查询条件

    - 比较操作符
    $gte,$gt,$lte,$lt,$ne,特别对应>=、>、<=、<。可以将其组合起来以方便查找一个范围的值。
    `db.user.find("age":{"$gte":18,"$lte":25,"$ne":22})`
    - OR查询
    MongoDB当中有两种方式进行OR查询,$in可以用来查询一个键的多个值,$or更通用一些,可以在多个键当中查询任意的给定值,$nin的作用域$in相反。
    `db.raffle.find({"age":{"$in":[1,2,3]}})`
    `db.raffle.find({"$or":[{"age":12},{"sex":"man"}]})`
    - $not
    $not时原条件句,即可以用在任何其他条件上。
    `db.raffle.find({"age":{"not":{"$in":[1,2,3]}}})`
    作用和
    `db.raffle.find({"age":{"$nin":[1,2,3]}})`
    相同
    - 条件语义
    我们会发现以$开头的键位于不同的位置。在查询当中,"$lte"在内层文档,在更新当中,"$inc"位于外层文档。基本可以肯定:条件语句是内层文档的键,而修改器则是外层文档的键。有一些“元操作符”也位于外层文档中,比如“$and”,“$or”,“$nor”等。一个键可以有任意多个条件语句,但是一个健不能对应多个更新修改器。

    ##3. 特定类型的查询
    ###3.1 null
    &ensp;&ensp;&ensp;&ensp;null类型有些奇怪,它不仅能够匹配自身,也能够匹配不包含自己的键的值。例如:

    {“_id”:ObjectId(“123”),”y”:null}
    {“_id”:ObjectId(“123”),”y”:1}
    {“_id”:ObjectId(“123”),”y”:2}

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    当我们使用查询语句:
    `db.c.find({"y":null})`
    时,不仅能够查询到第一条数据,也能够查询到第二条数据。因此,为了解决该问题,应该使用查询语句:
    `db.c.find({"y":{"$in":[null],"$exists":true}})`
    来进行查询。
    ###3.2 正则表达式
    可以使用
    `db.user.find({"name":/joe/i})`
    来匹配name键值包含joe字串的文档。在使用正则表达式时,需要注意的是,正则表达式不区分大小写。MongDB使用Perl兼容的正则表达式(PCRE)库来匹配正则表达式,任何PCRE支持的正则表达式语法都能被Perl接受。

    ###3.3 查询数组
    &ensp;&ensp;&ensp;&ensp;查询数组很容易,对于数组,我们可以这样理解:数组中每一个元素都是这个键值对键的一个有效值,如下面的例子:我们要查询出售apple的水果店:

    db.fruitshop.find();
    { “_id” : ObjectId(“5022518d09248743250688e0”), “name” : “big fruit”, “fruits” : [ “apple”, “pear”, “orange” ] }
    { “_id” : ObjectId(“502251a309248743250688e1”), “name” : “good fruit”, “fruits” : [ “banana”, “pear”, “orange” ] }
    { “_id” : ObjectId(“502251c109248743250688e2”), “name” : “good fruit”, “fruits” : [ “banana”, “apple”, “tomato” ] }

    db.fruitshop.find({“fruits”:”apple”});
    { “_id” : ObjectId(“5022518d09248743250688e0”), “name” : “big fruit”, “fruits” : [ “apple”, “pear”, “orange” ] }
    { “_id” : ObjectId(“502251c109248743250688e2”), “name” : “good fruit”, “fruits” : [ “banana”, “apple”, “tomato” ] }

    1
    &ensp;&ensp;&ensp;&ensp;我们发现只要包含苹果的数组都能被查询出来。如果要通过多个元素来匹配数组,就需要条件操作符"$all",比如我们要查询既卖apple又卖banana的水果店:

    db.fruitshop.find();
    { “_id” : ObjectId(“5022518d09248743250688e0”), “name” : “big fruit”, “fruits” : [ “apple”, “pear”, “orange” ] }
    { “_id” : ObjectId(“502251a309248743250688e1”), “name” : “good fruit”, “fruits” : [ “banana”, “pear”, “orange” ] }
    { “_id” : ObjectId(“502251c109248743250688e2”), “name” : “good fruit”, “fruits” : [ “banana”, “apple”, “tomato” ] }

    db.fruitshop.find({“fruits”:{“$all”:[“apple”,”banana”]}});
    { “_id” : ObjectId(“502251c109248743250688e2”), “name” : “good fruit”, “fruits” : [ “banana”, “apple”, “tomato” ] }

    1
    &ensp;&ensp;&ensp;&ensp;我们看,使用“$all”对数组内元素的顺序没有要求,只要全部包含的数组都能查询出来。数组查询也可以使用精确匹配的方式,即查询条件文档中键值对的值也是数组,如:

    { “_id” : ObjectId(“5022518d09248743250688e0”), “name” : “big fruit”, “fruits” : [ “apple”, “pear”, “orange” ] }
    { “_id” : ObjectId(“5022535109248743250688e4”), “name” : “fruit king”, “fruits” : [ “apple”, “orange”, “pear” ] }
    { “_id” : ObjectId(“502253c109248743250688e5”), “name” : “good fruit”, “fruits” : [ “apple”, “orange”, “pear”, “banana” ] }

    db.fruitshop.find({“fruits”:[“apple”,”orange”,”pear”]});
    { “_id” : ObjectId(“5022535109248743250688e4”), “name” : “fruit king”, “fruits” : [ “apple”, “orange”, “pear” ] }

    1
    2
    3
    &ensp;&ensp;&ensp;&ensp;如果是精确匹配的方式,MongoDB的处理方式是完全相同的匹配,即顺序与数量都要一致,上述中第一条文档和查询条件的顺序不一致,第三条文档比查询条件文档多一个元素,都没有被匹配成功!

    &ensp;&ensp;&ensp;&ensp;对于数组的匹配,还有一种形式是精确指定数组中某个位置的元素匹配,我们前面提到,数组中的索引可以作为键使用,如我们要匹配水果店售第二种水果是orange 的水果店:

    db.fruitshop.find();
    { “_id” : ObjectId(“5022518d09248743250688e0”), “name” : “big fruit”, “fruits” : [ “apple”, “pear”, “orange” ] }
    { “_id” : ObjectId(“5022535109248743250688e4”), “name” : “fruit king”, “fruits” : [ “apple”, “orange”, “pear” ] }
    { “_id” : ObjectId(“502253c109248743250688e5”), “name” : “good fruit”, “fruits” : [ “apple”, “orange”, “pear”, “banana” ] }

    db.fruitshop.find({“fruits.1”:”orange”});
    { “_id” : ObjectId(“5022535109248743250688e4”), “name” : “fruit king”, “fruits” : [ “apple”, “orange”, “pear” ] }
    { “_id” : ObjectId(“502253c109248743250688e5”), “name” : “good fruit”, “fruits” : [ “apple”, “orange”, “pear”, “banana” ] }

    1
    2
    3
    &ensp;&ensp;&ensp;&ensp;数组索引从0开始,我们匹配第二种水果就用furits.1作为键。

    &ensp;&ensp;&ensp;&ensp;$size条件操作符,可以用来查询特定长度的数组的,如我们要查询卖3种水果的水果店:

    db.fruitshop.find();
    { “_id” : ObjectId(“5022518d09248743250688e0”), “name” : “big fruit”, “fruits” : [ “apple”, “pear”, “orange” ] }
    { “_id” : ObjectId(“5022535109248743250688e4”), “name” : “fruit king”, “fruits” : [ “apple”, “orange”, “pear” ] }
    { “_id” : ObjectId(“502253c109248743250688e5”), “name” : “good fruit”, “fruits” : [ “apple”, “orange”, “pear”, “banana” ] }

    db.fruitshop.find({“fruits”:{“$size”:3}});
    { “_id” : ObjectId(“5022518d09248743250688e0”), “name” : “big fruit”, “fruits” : [ “apple”, “pear”, “orange” ] }
    { “_id” : ObjectId(“5022535109248743250688e4”), “name” : “fruit king”, “fruits” : [ “apple”, “orange”, “pear” ] }

1
&ensp;&ensp;&ensp;&ensp;但条件操作符"$size"不能和其他操作符连用如“$gt”等,这是这个操作符的一个缺陷。使用这个操作符我们只能精确查询某个长度的数组。如果实际中,在查询某个数组时,需要按其长度范围进行查询,这里推荐的做法是:在这个文档中额外增加一个“size”键,专门记录其中数组的大小,在对数组进行"$push"操作同时,将这个“size”键值加1。如下所示:

db.fruitshop.find({“name”:”big fruit”});
{ “_id” : ObjectId(“5022518d09248743250688e0”), “fruits” : [ “apple”, “pear”, “orange”, “strawberry” ], “name” : “big fruit”, “size” : 4 }
db.fruitshop.update({“name”:”big fruit”},
… {“$push”:{“fruits”:”banana”}, “$inc”:{“size”:1}}, false, false);

db.fruitshop.find({“name”:”big fruit”});
{ “_id” : ObjectId(“5022518d09248743250688e0”), “fruits” : [ “apple”, “pear”, “orange”, “strawberry”, “banana” ], “name” : “big fruit”, “size” : 5 }

1
&ensp;&ensp;&ensp;&ensp;find函数的第二个参数用于查询返回哪些键,他还可以控制查询返回数组的一个子数组,如下例:我只想查询水果店售卖说过数组的前两个:

db.fruitshop.find();
{ “_id” : ObjectId(“5022518d09248743250688e0”), “fruits” : [ “apple”, “pear”, “orange”, “strawberry”, “banana” ], “name” : “big fruit” }
{ “_id” : ObjectId(“5022535109248743250688e4”), “fruits” : [ “apple”, “orange”, “pear” ], “name” : “fruit king” }
{ “_id” : ObjectId(“502253c109248743250688e5”), “fruits” : [ “apple”, “orange”, “pear”, “banana” ], “name” : “good fruit” }

db.fruitshop.find({}, {“fruits”:{“$slice”:2}});
{ “_id” : ObjectId(“5022518d09248743250688e0”), “fruits” : [ “apple”, “pear” ], “name” : “big fruit” }
{ “_id” : ObjectId(“5022535109248743250688e4”), “fruits” : [ “apple”, “orange” ], “name” : “fruit king” }
{ “_id” : ObjectId(“502253c109248743250688e5”), “fruits” : [ “apple”, “orange” ], “name” : “good fruit” }

1
&ensp;&ensp;&ensp;&ensp;“$slice”也可以从后面截取,用复数即可,如-1表明截取最后一个;还可以截取中间部分,如[2,3],即跳过前两个,截取3个,如果剩余不足3个,就全部返回!

db.fruitshop.find();
{ “_id” : ObjectId(“5022518d09248743250688e0”), “fruits” : [ “apple”, “pear”, “orange”, “strawberry”, “banana” ], “name” : “big fruit” }
{ “_id” : ObjectId(“5022535109248743250688e4”), “fruits” : [ “apple”, “orange”, “pear” ], “name” : “fruit king” }
{ “_id” : ObjectId(“502253c109248743250688e5”), “fruits” : [ “apple”, “orange”, “pear”, “banana” ], “name” : “good fruit” }

db.fruitshop.find({}, {“fruits”:{“$slice”:-1}});
{ “_id” : ObjectId(“5022518d09248743250688e0”), “fruits” : [ “banana” ], “name” : “big fruit” }
{ “_id” : ObjectId(“5022535109248743250688e4”), “fruits” : [ “pear” ], “name” : “fruit king” }
{ “_id” : ObjectId(“502253c109248743250688e5”), “fruits” : [ “banana” ], “name” : “good fruit” }
db.fruitshop.find({}, {“fruits”:{“$slice”:[3,6]}});
{ “_id” : ObjectId(“5022518d09248743250688e0”), “fruits” : [ “strawberry”, “banana” ], “name” : “big fruit” }
{ “_id” : ObjectId(“5022535109248743250688e4”), “fruits” : [ ], “name” : “fruit king” }
{ “_id” : ObjectId(“502253c109248743250688e5”), “fruits” : [ “banana” ], “name” : “good fruit” }

1
&ensp;&ensp;&ensp;&ensp;如果第二个参数中有个键使用了条件操作符"$slice",则默认查询会返回所有的键,如果此时你要忽略哪些键,可以手动指明!如:

db.fruitshop.find({}, {“fruits”:{“$slice”:[3,6]}, “name”:0, “_id”:0});
{ “fruits” : [ “strawberry”, “banana” ] }
{ “fruits” : [ ] }
{ “fruits” : [ “banana” ] }

1
2
3

###3.4 查询内嵌文档
&ensp;&ensp;&ensp;&ensp;查询文档有两种方式,一种是完全匹查询,另一种是针对键值对查询!内嵌文档的完全匹配查询和数组的完全匹配查询一样,内嵌文档内键值对的数量,顺序都必须一致才会匹配,如下例:

db.staff.find();
{ “_id” : ObjectId(“50225fc909248743250688e6”), “name” : { “first” : “joe”, “middle” : “bush”, “last” : “Schmoe” }, “age” : 45 }
{ “_id” : ObjectId(“50225fe209248743250688e7”), “name” : { “first” : “joe”, “middle” : “bush” }, “age” : 35 }
{ “_id” : ObjectId(“50225fff09248743250688e8”), “name” : { “middle” : “bush”, “first” : “joe” }, “age” : 25 }

db.staff.find({“name”:{“first”:”joe”,”middle”:”bush”}});
{ “_id” : ObjectId(“50225fe209248743250688e7”), “name” : { “first” : “joe”, “middle” : “bush” }, “age” : 35 }

1
&ensp;&ensp;&ensp;&ensp;针对内嵌文档特定键值对的查询是最常用的!通过点表示法来精确表示内嵌文档的键:

db.staff.find();
{ “_id” : ObjectId(“50225fc909248743250688e6”), “name” : { “first” : “joe”, “middle” : “bush”, “last” : “Schmoe” }, “age” : 45 }
{ “_id” : ObjectId(“50225fe209248743250688e7”), “name” : { “first” : “joe”, “middle” : “bush” }, “age” : 35 }
{ “_id” : ObjectId(“50225fff09248743250688e8”), “name” : { “middle” : “bush”, “first” : “joe” }, “age” : 25 }

db.staff.find({“name.first”:”joe”, “name.middle”:”bush”});
{ “_id” : ObjectId(“50225fc909248743250688e6”), “name” : { “first” : “joe”, “middle” : “bush”, “last” : “Schmoe” }, “age” : 45 }
{ “_id” : ObjectId(“50225fe209248743250688e7”), “name” : { “first” : “joe”, “middle” : “bush” }, “age” : 35 }
{ “_id” : ObjectId(“50225fff09248743250688e8”), “name” : { “middle” : “bush”, “first” : “joe” }, “age” : 25 }

1
2
&ensp;&ensp;&ensp;&ensp;我们看,这样查询,所有有效文档均被查询到了!通过点表示法,可以表示深入到内嵌文档内部的键!利用“点表示法”来查询内嵌文档,这也约束了在插入文档时,任何键都不能包含“.” !!
&ensp;&ensp;&ensp;&ensp;当内嵌文档变得复杂后,如键的值为内嵌文档的数组,这种内嵌文档的匹配需要一些技巧,如下例:

db.blogs.findOne();
{
“_id” : ObjectId(“502262ab09248743250688ea”),
“content” : “…..”,
“comment” : [
{
“author” : “joe”,
“score” : 3,
“comment” : “just so so!”
},
{
“author” : “jimmy”,
“score” : 5,
“comment” : “cool! good!”
}
]
}
db.blogs.find({“comment.author”:”joe”, “comment.score”:{“$gte”:5}});
{ “_id” : ObjectId(“502262ab09248743250688ea”), “content” : “…..”, “comment” : [ { “author” : “joe”, “score” : 3, “comment” : “j
ust so so!” }, { “author” : “jimmy”, “score” : 5, “comment” : “cool! good!” } ] }

1
&ensp;&ensp;&ensp;&ensp;我们想要查询评论中有叫“joe”并且其给出的分数超过5分的blog文档,但我们利用“点表示法”直接写是有问题的,因为这条文档有两条评论,一条的作者名字叫“joe”但分数只有3,一条作者名字叫“jimmy”,分数却给了5!也就是这条查询条件和数组中不同的文档进行了匹配!这不是我们想要的,我们这里是要使用一组条件而不是单个指明每个键,使用条件操作符“$elemMatch”即可!他能将一组条件限定到数组中单条文档的匹配上:

db.blogs.findOne();
{
“_id” : ObjectId(“502262ab09248743250688ea”),
“content” : “…..”,
“comment” : [
{
“author” : “joe”,
“score” : 3,
“comment” : “just so so!”
},
{
“author” : “jimmy”,
“score” : 5,
“comment” : “cool! good!”
}
]
}
db.blogs.find({“comment”:{“$elemMatch”:{“author”:”joe”, “score”:{“$gte”:5}}}});
db.blogs.find({“comment”:{“$elemMatch”:{“author”:”joe”, “score”:{“$gte”:3}}}});
{ “_id” : ObjectId(“502262ab09248743250688ea”), “content” : “…..”, “comment” : [ { “author” : “joe”, “score” : 3, “comment” : “j
ust so so!” }, { “author” : “jimmy”, “score” : 5, “comment” : “cool! good!” } ] }

&ensp;&ensp;&ensp;&ensp;这样做,结果是正确的!利用条件操作符“$elemMatch”可以组合一组条件,并且还能达到的“点表示法”的模糊查询的效果!


欢迎关注个人公众号:
![个人公号](/images/个人公号.jpg)
0%