凌晨四点的洛杉矶


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 搜索

error、exception、throwable

发表于 2019-03-09 | 分类于 java面试准备 , java基础 |

前言

在一个方法的运行过程中,如果发生了异常,则这个方法(或者是Java虚拟机)生成一个代表该异常的对象(它包含了异常的详细信息),并把它交给运行时系统,运行时系统寻找相应的代码来处理这一异常。我们把生成异常对象并把它提交给运行时系统的过程称为抛出(throw)异常。

当Java运行时系统得到一个异常对象时,它将会沿着方法的调用栈逐层回溯,寻找处理这一异常的代码。找到能够处理这类异常的方法后,运行时系统把当前异常对象交给这个方法进行处理,这一过程称为捕获(catch)异常。

错误和异常

Throwable类是 Java 语言中所有错误或异常的超类。

Throwable有两个子类:Error和Exception

  • Exception(异常):是应用程序中可能的可预测、可恢复问题。一般大多数异常表示中度到轻度的问题。异常一般是在特定环境下产生的,通常出现在代码的特定方法和操作中。在 EchoInput 类中,当试图调用 readLine方法时,可能出现 IOException 异常。

    Exception 类有一个重要的子类 RuntimeException。RuntimeException 类及其子类表示“JVM常用操作”引发的错误。例如,若试图使用空值对象引用、除数为零或数组越界,则分别引发运行时异常(NullPointerException、ArithmeticException)和 ArrayIndexOutOfBoundException。

  • Error(错误):表示运行应用程序中较严重问题,不可恢复。大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟机)出现的问题。例如,当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。

ex5

可控制异常和不可控制异常的划分

Java中的异常分为两大类:

  1. Checked Exception(非Runtime Exception)

    Java认为Checked异常都是可以被处理的异常,所以Java程序必须显式的处理Checked异常,如果程序没有处理checked异常,程序在编译时候将发生错误。
    我们比较熟悉的Checked异常有:

    • java.lang.ClassNotFoundException
  • java.lang.NoSuchMetodException
    • java.io.IOException
  1. Unchecked Exception(Runtime Exception)

    Runtime Exception如除数是0、数组下标越界等,其产生频繁,处理麻烦,若显示申明或者捕获将会对程序的可读性和运行效率影响很大。所以由系统自动检测并将它们交给缺省的异常处理程序。当然如果你有处理要求也可以显示捕获它们。
    我们比较熟悉的RumtimeException类的子类有:

    • java.lang.ArithmeticException
    • java.lang.ArrayStoreExcetpion
    • java.lang.ClassCastException
  • java.lang.IndexOutOfBoundsException
    • java.lang.NullPointerException

ex4

java.lang.Error:Throwable的子类,用于标记严重错误。合理的应用程序不应该去try/catch这种错误。绝大多数的错误都是非正常的,就根本不该出现的。

java.lang.Exception:Throwable的子类,用于指示一种合理的程序想去catch的条件。即它仅仅是一种程序运行条件,而非严重错误,并且鼓励用户程序去catch它。

Error和RuntimeException 及其子类都是不可控制的异常(unchecked exceptions),而所有其他的Exception类都是可控制的异常(checked exceptions)。

Checked Exceptions:通常是从一个可以恢复的程序中抛出来的,并且最好能够从这种异常中使用程序恢复。比如FileNotFoundException, ParseException等。可控制的异常发生在编译阶段,必须要使用try…catch(或者throws)进行处理,否则编译不通过。

Unchecked Exceptions:通常是如果一切正常的话本不该发生的异常,但是的确发生了。发生在运行期,具有不确定性,主要是由于程序的逻辑问题所引起的。比如ArrayIndexOutOfBoundException, ClassCastException等。从语言本身的角度讲,程序不该去catch这类异常。虽然能够从诸如RuntimeException这样的异常中catch并恢复,但是并不鼓励终端程序员这么做。 因为这类错误本身就是bug,应该被修复,出现此类错误时程序就应该立即停止执行。 因此,面对Errors和Unchecked Exceptions应该让程序自动终止执行,程序员不该做诸如try/catch这样的事情,而是应该查明原因,修改代码逻辑。

处理方法

Java中凡是继承自Exception,而不继承自RuntimeException类的异 常都是非运行时异常。

对于非运行时异常(Checked Exception),必须要对其进行处理,否则无法通过编译。
处理方式有两种:

  1. 使用try..catch..finally进行捕获;
  2. 在产生异常的方法声明后面写上throws 某一个Exception类型,如throws Exception,将异常抛出到外面一层去。

对于运行时异常(Runtime exception),可以对其进行处理,也可以不处理。推荐不对运行时异常进行处理。

  • 一般而言,Checked Exception 表示这个Exception 必须要被处理,也就是说程序设计者应该已经知道可能会收到某个Exception(因为要try catch住) ,所以程序设计者应该能针对这些不同的Checked Exception 做出不同的处理。
  • 而Runtime Exception 通常会暗示着程序上的错误,这种错误会导致程序设计者无法处理,而造成程序无法继续执行下去。

浅谈进程和线程

发表于 2019-03-06 | 分类于 java面试准备 , 并发编程 |

浅谈进程和线程

定义

  • 进程定义:一般是对于系统来讲,进程(process)是具有一定独立功能的程序,操作系统利用进程把工作划分为一些功能单元。进程是进行资源分配和调度的一个独立单位。它还拥有一个私有的虚拟地址空间,该空间仅能被它所包含的线程访问。
  • 线程定义:线程(thread)是进程中所包含的一个或多个执行单元。它只能归属于一个进程并且只能访问该进程所拥有的资源。线程是CPU调度和分派的基本单位,没有单独地址空间,有独立的栈,局部变量,寄存器,程序计数器等。

关系

  1. 一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行。
  2. 线程属于进程,不能独立执行;每个进程至少要有一个线程,成为主线程。
  3. 一个进程无法直接访问另一个进程的资源;同一进程内的多个线程共享进程的资源。

区别

进程和线程的主要差别在于它们是不同的操作系统资源管理方式。

进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

  1. 线程作为调度和分配的基本单位,进程作为拥有资源的基本单位。
  2. 不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行。
  3. 线程的划分尺度小于进程,使得多线程程序的并发性高。
  4. 进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。
  5. 在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。
  6. 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
  7. 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。

补充:粗略谈下synchronized关键字

synchronized 关键字,代表这个方法加锁,相当于不管哪一个线程(例如线程A),运行到这个方法时,都要检查有没有其它线程B(或者C、 D等)正在用这个方法(或者该类的其他同步方法),有的话要等正在使用synchronized方法的线程B(或者C 、D)运行完这个方法后再运行此线程A,没有的话,锁定调用者,然后直接运行。

它包括两种用法:synchronized 方法和 synchronized 块。

Java语言的关键字,可用来给对象和方法或者代码块加锁。当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。当两个并发线程访问同一个对象object中的这个加锁同步代码块时,一个时间内只能有一个线程得到执行。另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块。然而,当一个线程访问object的一个加锁代码块时,另一个线程仍可以访问该object中的非加锁代码块。

几种进程间的通信方式

  1. 管道(pipe):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有血缘关系的进程间使用。进程的血缘关系通常指父子进程关系。
  2. 有名管道(named pipe):有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间通信。
  3. 信号量(semophore):信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它通常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
  4. 消息队列(message queue):消息队列是由消息组成的链表,存放在内核中,并由消息队列标识符标识。消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。
  5. 信号(signal):信号是一种比较复杂的通信方式,用于通知接收进程某一事件已经发生。
  6. 共享内存(shared memory):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的IPC方式,它是针对其他进程间的通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量配合使用,来实现进程间的同步和通信。
  7. 套接字(socket):套接口也是一种进程间的通信机制,与其他通信机制不同的是它可以用于不同及其间的进程通信。

几种线程间的通信机制

1、锁机制

  • 互斥锁:提供了以排它方式阻止数据结构被并发修改的方法。
  • 读写锁:允许多个线程同时读共享数据,而对写操作互斥。
  • 条件变量:可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件测试是在互斥锁的保护下进行的。条件变量始终与互斥锁一起使用。

2、信号量机制:包括无名线程信号量与有名线程信号量

3、信号机制:类似于进程间的信号处理。

线程间通信的主要目的是用于线程同步,所以线程没有像进程通信中用于数据交换的通信机制。

线程的调度

  1. sleep():在睡眠的时候,会释放cpu让给其他线程执行。即使没有其他线程抢占cpu,也需要等待睡眠时间到了以后才能真正的指定。

  2. yield():执行的时候会让出cpu , 但是会立马同其他的线程抢占cpu。注意:yield()只会让位给优先级一样或者比它优先级高的线程,而且不能由用户指定暂停多长时间。

    当线程执行了sleep方法之后,线程将转入到睡眠状态,直到时间结束,而执行yield方法,直接转入到就绪状态。

  3. join():在哪个线程被调用,就插入到哪个线程前面。

java锁的种类和区别

发表于 2019-03-04 | 分类于 java面试准备 , 并发编程 |

java中锁有很多种,本文将主要介绍各种锁的分类和区别,主要如下:

  • 公平锁/非公平锁
  • 可重入锁
  • 独享锁/共享锁
  • 互斥锁/读写锁
  • 乐观锁/悲观锁
  • 分段锁
  • 偏向锁/轻量级锁/重量级锁
  • 自旋锁

上面是很多锁的名词,这些分类并不是全是指锁的状态,有的指锁的特性,有的指锁的设计。

下面总结的内容是对每个锁的名词进行一定的解释。

公平锁/非公平锁

公平锁是指多个线程按照申请锁的顺序来获取锁。
非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。
对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。

非公平锁的优点在于吞吐量比公平锁大。
对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

可重入锁

可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。
对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁,其名字是Re entrant Lock重新进入锁。
对于Synchronized而言,也是一个可重入锁,可重入锁的一个好处是可一定程度避免死锁。

1
2
3
4
5
6
7
synchronized void setA() throws Exception{
Thread.sleep(1000);
setB();
}
synchronized void setB() throws Exception{
Thread.sleep(1000);
}

上面的代码就是一个可重入锁的一个特点。

如果不是可重入锁的话,当前线程就不会获得setB方法的锁,因此setB可能不会被当前线程执行,可能造成死锁。

独享锁/共享锁

独享锁是指该锁一次只能被一个线程所持有。
共享锁是指该锁可被多个线程所持有。

对于Java ReentrantLock而言,其是独享锁。

对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。
读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。
独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。
对于Synchronized而言,当然是独享锁。

互斥锁/读写锁

注意:上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。
互斥锁在Java中的具体实现就是ReentrantLock
读写锁在Java中的具体实现就是ReadWriteLock

乐观锁/悲观锁

乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。

悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。
乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。

  • 悲观锁适合写操作非常多的场景
  • 乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升
  • 悲观锁在Java中的使用,就是利用各种锁。
  • 乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

分段锁

分段锁其实是一种锁的设计,并不是具体的一种锁。

对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。
我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表,同时又是一个ReentrantLock。(Segment继承了ReentrantLock)
当需要put元素的时候,并不是对整个HashMap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。
在统计size的时候,就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。
分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

偏向锁/轻量级锁/重量级锁

这三种锁是指锁的状态,并且是针对Synchronized。

在Java 5通过引入锁升级的机制来实现高效Synchronized。

这三种锁的状态是通过对象监视器在对象头中的字段来表明的。
偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价。
轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。
重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

自旋锁

在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

参考:https://blog.csdn.net/qq_35181209/article/details/77652278

CAS算法

发表于 2019-03-04 | 分类于 java面试准备 , 并发编程 |

什么是CAS?

Compare And Set(或Compare And Swap),CAS是解决多线程并行情况下使用锁造成性能损耗的一种机制,采用这种无锁的原子操作可以实现线程安全,避免加锁的笨重性。

JDK5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronized同步锁的一种乐观锁。

JDK5之前Java语言是靠synchronized关键字保证同步的,这是一种独占锁,也是是悲观锁。

CAS 利用 CPU指令 保证了操作的原子性,以达到锁的效果,循环这个指令,直到成功为止。java提供的CAS原子操作类AtomicInteger等,核心就是CAS(CompareAndSwap)。

注意:原子操作和锁是一样的一种可以保证线程安全的方式,如何让线程安全就看如何使用锁或者如何使用原子操作。CAS使用了正确的原子操作,所以保证了线程安全。

CAS算法理解

对CAS的理解,CAS是一种无锁算法。

CAS有3个操作数:

  • 内存值V
  • 旧的预期值A
  • 要修改的新值B

当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

CAS比较与交换的伪代码可以表示为:

do{

​ 备份旧数据;

​ 基于旧数据构造新数据;

}while(!CAS( 内存地址,备份的旧数据,新数据 ))

场景理解

cas1

注:t1,t2线程是同时更新同一变量56的值

因为t1和t2线程都同时去访问同一变量56,所以他们会把主内存的值完全拷贝一份到自己的工作内存空间,所以t1和t2线程的预期值都为56。

假设t1在与t2线程竞争中线程t1能去更新变量的值,而其他线程都失败。(失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次发起尝试)。t1线程去更新变量值改为57,然后写到内存中。此时对于t2来说,内存值变为了57,与预期值56不一致,就操作失败了(想改的值不再是原来的值)。

(上图通俗的解释是:CPU去更新一个值,但如果想改的值不再是原来的值,操作就失败,因为很明显,有其它操作先改变了这个值。)

当两者进行比较时,如果相等,则证明共享数据没有被修改,替换成新值,然后继续往下运行;如果不相等,说明共享数据已经被修改,放弃已经所做的操作,然后重新执行刚才的操作。

容易看出 CAS 操作是基于共享数据不会被修改的假设,采用了类似于数据库的commit-retry 的模式。当同步冲突出现的机会很少时,这种假设能带来较大的性能提升。

原子操作类

当高并发的情况下,对于基本数据类型或者引用数据类型的操作,可能会产生线程安全问题,为了避免多线程问题的处理方式一般有加锁,但是加锁会影响性能,所以这个时候可以考虑使用原子操作类。CAS由于是在硬件方面保证的原子性,不会锁住当前线程,所以执行效率是很高的。
常见的原子操作类:

cas4

CAS算法在JDK中的应用

在原子类变量中,如java.util.concurrent.atomic中的AtomicXXX,都使用了这些底层的JVM支持为数字类型的引用类型提供一种高效的CAS操作,而在java.util.concurrent中的大多数类在实现时都直接或间接的使用了这些原子变量类。

Java 1.7中AtomicInteger.incrementAndGet()的实现源码为:

cas2

这段代码是一个无限循环,也就是CAS的自旋,循环体中做了三件事:

  1. 获取当前值
  2. 当前值+1,计算出目标值
  3. 进行CAS操作,如果成功则跳出循环,如果失败则重复上述步骤

Java 1.8中AtomicInteger.compareAndSet(int expect, int update)的实现源码:

cas5

compareAndSet方法的实现很简单,只有一行代码。

这里涉及到两个重要的对象,一个是unsafe,一个是valueOffset。

什么是unsafe呢?Java语言不像C,C++那样可以直接访问底层操作系统,但是JVM为我们提供了一个后门,这个后门就是unsafe。unsafe为我们提供了硬件级别的原子操作。

至于valueOffset对象,是通过unsafe.objectFiledOffset方法得到,所代表的是AtomicInteger对象value成员变量在内存中的偏移量。我们可以简单的把valueOffset理解为value变量的内存地址。

我们上面说过,CAS机制中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

而unsafe的compareAndSwapInt方法的参数包括了这三个基本元素:valueOffset参数代表了V,expect参数代表了A,update参数代表了B。

正是unsafe的compareAndSwapInt方法保证了Compare和Swap操作之间的原子性操作。

注意:其中current为从内存中拷贝的旧的预期值,next为想要修改的新值。

由此可见,AtomicInteger.incrementAndGet的实现用了乐观锁技术,调用了类sun.misc.Unsafe库里面的 CAS算法,用CPU指令来实现无锁自增。

所以,AtomicInteger.incrementAndGet的自增比用synchronized的锁效率倍增。

CAS的缺点

CAS虽然很高效的解决了原子操作问题,但是CAS仍然存在三大问题:

  1. 循环时间长开销很大
  2. 只能保证一个共享变量的原子操作。
  3. ABA问题。

循环时间长开销很大:
如果并发量很高,我们可以看到getAndAddInt方法(JDK1.8)执行时,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。

自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。

只能保证一个共享变量的原子操作:

CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5开始,提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作。

所以我们可以使用锁或者利用AtomicReference类把多个共享变量合并成一个共享变量来操作,以此保证原子性。

什么是ABA问题?ABA问题怎么解决?
如果内存地址V初次读取的值是A,并且在准备赋值的时候检查到它的值仍然为A,那我们就能说它的值没有被其他线程改变过了吗?

如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。

JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的compareAndSet方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

Java并发包为了解决这个问题,提供了一个带有标记的原子引用类AtomicStampedReference,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚 ABA 问题是否会影响程序并发的正确性。如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。

解决ABA问题:
AtomicMarkableReference:内部是一个boolean类型的版本号,可以记录是否被更改过
AtomicStampedReference:内部是一个int类型的版本号,可以记录被更改的次数

例如:使用AtomicStampedReference,避免ABA问题,查看内部是int类型的版本号

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
public class AtomicStampedReferenceDemo {

public static void main(String[] args) throws InterruptedException {
//设置初始化版本号是0
AtomicStampedReference<String> atomicStampedReference = new AtomicStampedReference("a1", 0);

//初始的值和版本号
String reference = atomicStampedReference.getReference();
int stamp = atomicStampedReference.getStamp();

Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("目前的值:" + reference + "............版本号:" + stamp
+ ",修改结果:" + atomicStampedReference.compareAndSet(reference, "a2", stamp, stamp + 1));
}
});

Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
String reference = atomicStampedReference.getReference();
System.out.println("目前的值:" + reference + "............版本号:" + atomicStampedReference.getStamp()
+ ",修改结果:" + atomicStampedReference.compareAndSet(reference, "a2", stamp, stamp + 1));
}
});

thread1.start();
thread1.join();
thread2.start();
thread2.join();
}

}

运行结果:

cas6

拓展-乐观锁和悲观锁

乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

乐观锁常见的两种实现方式

1. 版本号机制

一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

举一个简单的例子: 假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。

  1. 操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除 $50( $100-$50 )。
  2. 在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。
  3. 操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。
  4. 操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。

这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。

2. CAS算法

即compare and swap(比较与交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数

  • 需要读写的内存值 V
  • 进行比较的值 A
  • 拟写入的新值 B

当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。

面试必备之深入理解自旋锁

什么是自旋锁?

自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环。

获取锁的线程一直处于活跃状态,但是并没有执行任何有效的任务,使用这种锁会造成busy-waiting。

它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。

Java如何实现自旋锁

下面是个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SpinLock {
private AtomicReference<Thread> cas = new AtomicReference<Thread>();
public void lock() {
Thread current = Thread.currentThread();
// 利用CAS
while (!cas.compareAndSet(null, current)) {
// DO nothing
}
}
public void unlock() {
Thread current = Thread.currentThread();
cas.compareAndSet(current, null);
}
}

简单分析:lock()方法利用的CAS,当第一个线程A获取锁的时候,能够成功获取到,不会进入while循环。如果此时线程A没有释放锁,另一个线程B又来获取锁,此时由于不满足CAS,所以就会进入while循环,不断判断是否满足CAS,直到A线程调用unlock()方法释放了该锁。

自旋锁存在的问题

  1. 如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
  2. 上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。

自旋锁的优点

  1. 自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
  2. 非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。 (线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)

可重入的自旋锁和不可重入的自旋锁

的那段代码,仔细分析一下就可以看出,它是不支持重入的,即当一个线程第一次已经获取到了该锁,在锁释放之前又一次重新获取该锁,第二次就不能成功获取到。由于不满足CAS,所以第二次获取会进入while循环等待,而如果是可重入锁,第二次也是应该能够成功获取到的。而且,即使第二次能够成功获取,那么当第一次释放锁的时候,第二次获取到的锁也会被释放,而这是不合理的。

为了实现可重入锁,我们需要引入一个计数器,用来记录获取锁的线程数。

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
public class ReentrantSpinLock {
private AtomicReference<Thread> cas = new AtomicReference<Thread>();
private int count;
public void lock() {
Thread current = Thread.currentThread();
if (current == cas.get()) { // 如果当前线程已经获取到了锁,线程数增加一,然后返回
count++;
return;
}
// 如果没获取到锁,则通过CAS自旋
while (!cas.compareAndSet(null, current)) {
// DO nothing
}
}
public void unlock() {
Thread cur = Thread.currentThread();
if (cur == cas.get()) {
if (count > 0) {// 如果大于0,表示当前线程多次获取了该锁,释放锁通过count减一来模拟
count--;
} else {// 如果count==0,可以将锁释放,这样就能保证获取锁的次数与释放锁的次数是一致的了。
cas.compareAndSet(cur, null);
}
}
}
}

关于自旋锁,大家可以看一下这篇文章,非常不错:《 面试必备之深入理解自旋锁》

参考:

https://www.jianshu.com/p/21be831e851e

https://blog.csdn.net/qq_28822933/article/details/83341633

https://blog.csdn.net/qq_32998153/article/details/79529704

https://juejin.im/post/5b4977ae5188251b146b2fc8

https://blog.csdn.net/qq_34337272/article/details/81252853

Spring常见面试题总结

发表于 2019-03-03 | 分类于 java面试准备 , Spring |

Spring是什么?

Spring是一个轻量级的IoC和AOP容器框架。目的是解决企业应用开发的复杂性,使用基本的JavaBean来完成以前只可能由EJB完成的事情,并提供了更多的企业应用功能,Spring的用途不仅限于服务器端的开发,从简单性、可测试性和松耦合的角度而言,任何Java应用都可以从Spring中受益。

Spring 的优点?

(1)spring属于低侵入式设计,代码的污染极低;

(2)spring的DI机制降低了业务对象替换的复杂性;

(3)容器提供了AOP技术,利用它很容易实现如权限拦截,运行期监控等功能;

(4)降低了组件之间的耦合性 ,实现了软件各层之间的解耦;

(5)容器提供单例模式支持;

(6)可以使用容器提供的众多服务,如事务管理,消息服务等;

(7)容器提供了众多的辅助类,能加快应用的开发;

(8)spring对于主流的应用框架提供了集成支持,如hibernate,JPA,Struts等

(9)独立于各种应用服务器

(10)Spring的高度开放性,并不强制应用完全依赖于Spring,开发者可以自由选择spring的部分或全部。

Spring的AOP理解

AOP,一般称为面向方面(切面)编程,作为面向对象的一种补充,用于解剖封装好的对象内部,找出其中对多个对象产生影响的公共行为,并将其封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),切面将那些与业务无关,却被业务模块共同调用的逻辑提取并封装起来,减少了系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。可用于权限认证、日志、事务处理。

AOP实现的关键在于AOP框架自动创建的AOP代理,AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ;动态代理则以Spring AOP为代表。

(1)AspectJ是静态代理的增强,所谓静态代理,就是AOP框架会在编译阶段生成AOP代理类,因此也称为编译时增强,他会在编译阶段将AspectJ织入到Java字节码中,运行的时候就是增强之后的AOP对象。

(2)Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。

Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理:

①JDK动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。JDK动态代理的核心是InvocationHandler接口和Proxy类。生成的代理对象的方法调用都会委托到InvocationHandler.invoke()方法,当我们调用代理类对象的方法时,这个“调用”会转送到invoke方法中,代理类对象作为proxy参数传入,参数method标识了我们具体调用的是代理类的哪个方法,args为这个方法的参数。

②如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法,覆盖方法时可以添加增强代码,从而实现AOP。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。

(3)静态代理与动态代理区别在于生成AOP代理对象的时机不同,相对来说AspectJ的静态代理方式具有更好的性能,但是AspectJ需要特定的编译器进行处理,而Spring AOP则无需特定的编译器处理。

Spring的IoC理解

(1)IOC就是控制反转。就是对象的创建权反转交给Spring,由容器控制程序之间的依赖关系,作用是实现了程序的解耦合,而非传统实现中,由程序代码直接操控。(依赖)控制权由应用代码本身转到了外部容器,由容器根据配置文件去创建实例并管理各个实例之间的依赖关系,控制权的转移,是所谓反转,并且由容器动态的将某种依赖关系注入到组件之中。BeanFactory 是Spring IoC容器的具体实现与核心接口,提供了一个先进的配置机制,使得任何类型的对象的配置成为可能,用来包装和管理各种bean。

(2)最直观的表达就是,IOC让对象的创建不用去new了,可以由spring自动生产,这里用的就是java的反射机制,通过反射在运行时动态的去创建、调用对象。spring就是根据配置文件在运行时动态的去创建对象,并调用对象的方法的。

(3)Spring的IOC有三种注入方式 :
第一是根据属性注入,也叫set方法注入;
第二种是根据构造方法进行注入;
第三种是根据注解进行注入。

详细的说:

(4)IoC,控制反转:将对象交给容器管理,你只需要在spring配置文件总配置相应的bean,以及设置相关的属性,让spring容器生成类的实例对象以及管理对象。在spring容器启动的时候,spring会把你在配置文件中配置的bean都初始化以及装配好,然后在你需要调用的时候,就把它已经初始化好的那些bean分配给你需要调用这些bean的类。就是将对象的控制权反转给spring容器管理。

(5)DI机制(Dependency Injection,依赖注入):可以说是IoC的其中一个内容,在容器实例化对象的时候主动的将被调用者(或者说它的依赖对象)注入给调用对象。比如对象A需要操作数据库,以前我们总是要在A中自己编写代码来获得一个Connection对象,有了 spring我们就只需要告诉spring,A中需要一个Connection,至于这个Connection怎么构造,何时构造,A不需要知道。在系统运行时,spring会在适当的时候制造一个Connection,然后像打针一样,注射到A当中,这样就完成了对各个对象之间关系的控制。

总结:IoC让相互协作的组件保持松散的耦合,而AOP编程允许你把遍布于应用各层的功能分离出来形成可重用的功能组件。

BeanFactory和ApplicationContext有什么区别?

BeanFactory和ApplicationContext是Spring的两大核心接口,而其中ApplicationContext是BeanFactory的子接口。它们都可以当做Spring的容器,生成Bean实例的,并管理容器中的Bean。

(1)BeanFactory:是Spring里面最底层的接口,提供了最简单的容器的功能,负责读取bean配置文档,管理bean的加载与实例化,维护bean之间的依赖关系,负责bean的生命周期,但是无法支持spring的aop功能和web应用。

(2)ApplicationContext接口作为BeanFactory的派生,因而具有BeanFactory所有的功能。而且ApplicationContext还在功能上做了扩展,以一种更面向框架的方式工作以及对上下文进行分层和实现继承,相较于BeanFactorty,ApplicationContext还提供了以下的功能:

①默认初始化所有的Singleton,也可以通过配置取消预初始化。

②继承MessageSource,因此支持国际化。

③资源访问,比如访问URL和文件。

④事件机制。

⑤同时加载多个配置文件。

⑥以声明式方式启动并创建Spring容器。

⑦载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的层次,比如应用的web层。

(3)

①BeanFactroy采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化,这样,我们就不能发现一些存在的Spring的配置问题。如果Bean的某一个属性没有注入,BeanFacotry加载后,直至第一次使用调用getBean方法才会抛出异常。
②而ApplicationContext则相反,它是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入。 ApplicationContext启动后预载入所有的单实例Bean,通过预载入单实例bean ,确保当你需要的时候,你就不用等待,因为它们已经创建好了。

③相对于基本的BeanFactory,ApplicationContext 唯一的不足是占用内存空间。当应用程序配置Bean较多时,程序启动较慢。

(4)BeanFactory通常以编程的方式被创建,ApplicationContext还能以声明的方式创建,如使用ContextLoader。

(5)BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用,但两者之间的区别是:BeanFactory需要手动注册,而ApplicationContext则是自动注册。

解释Spring支持的几种bean的作用域

Spring容器中的bean可以分为5个范围:

(1)singleton:这种bean范围是默认的,这种范围确保不管接受到多少个请求,每个容器中只有一个bean的实例,单例的模式由bean factory自身来维护。
(2)prototype:原形范围与单例范围相反,为每一个bean请求提供一个实例。
(3)request:在请求bean范围内会每一个来自客户端的网络请求创建一个实例,在请求完成以后,bean会失效并被垃圾回收器回收。
(4)Session:与请求范围类似,确保每个session中有一个bean的实例,在session过期后,bean会随之失效。

(5)global-session:global-session和Portlet应用相关。当你的应用部署在Portlet容器中工作时,它包含很多portlet。如果你想要声明让所有的portlet共用全局的存储变量的话,那么这全局变量需要存储在global-session中。全局作用域与Servlet中的session作用域效果相同。

请解释Spring Bean的生命周期?

其他地址:https://www.cnblogs.com/zrtqsk/p/3735273.html

首先说一下Servlet的生命周期:实例化,初始init,接收请求service,销毁destroy;

Spring上下文中的Bean生命周期也类似,如下:

(1)实例化一个Bean--也就是我们常说的new;

(2)按照Spring上下文对实例化的Bean进行配置--也就是IOC注入;

(3)如果这个Bean已经实现了BeanNameAware接口,会调用它实现的setBeanName(String)方法,此处传递的就是Spring配置文件中Bean的id值;

(4)如果这个Bean已经实现了BeanFactoryAware接口,会调用它实现的setBeanFactory(setBeanFactory(BeanFactory)传递的是Spring工厂自身(可以用这个方式来获取其它Bean,只需在Spring配置文件中配置一个普通的Bean就可以);

(5)如果这个Bean已经实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文(同样这个方式也可以实现步骤4的内容,但比4更好,因为ApplicationContext是BeanFactory的子接口,有更多的实现方法);

(6)如果这个Bean关联了BeanPostProcessor接口,将会调用postProcessBeforeInitialization(Object obj, String s)方法,BeanPostProcessor经常被用作是Bean内容的更改,并且由于这个是在Bean初始化结束时调用那个的方法,也可以被应用于内存或缓存技术;

(7)如果Bean在Spring配置文件中配置了init-method属性会自动调用其配置的初始化方法。

(8)如果这个Bean关联了BeanPostProcessor接口,将会调用postProcessAfterInitialization(Object obj, String s)方法、;

注:以上工作完成以后就可以应用这个Bean了,那这个Bean是一个Singleton的,所以一般情况下我们调用同一个id的Bean会是在内容地址相同的实例,当然在Spring配置文件中也可以配置非Singleton。

(9)当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用那个其实现的destroy()方法;

(10)最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法。

注意:我们这里描述的是应用Spring上下文Bean的生命周期,如果应用Spring的工厂也就是BeanFactory的话去掉第5步就Ok了。

Spring中bean的加载过程

(1)获取配置文件资源;

(2)对获取的xml资源进行一定的处理检验;

(3)处理包装资源;

(4)解析处理包装过后的资源;

(5)加载提取bean并注册(添加到beanDefinitionMap中)

Spring框架中的单例Beans是线程安全的么?

Spring框架并没有对单例bean进行任何多线程的封装处理。

关于单例bean的线程安全和并发问题需要开发者自行去搞定。

但实际上,大部分的Spring bean并没有可变的状态(比如Serview类和DAO类),所以在某种程度上说Spring的单例bean是线程安全的。如果你的bean有多种状态的话(比如 View Model 对象),就需要自行保证线程安全。

最浅显的解决办法就是将多态bean的作用域由“singleton”变更为“prototype”。

Spring如何处理线程并发问题?

Spring使用ThreadLocal解决线程安全问题。

我们知道在一般情况下,只有有状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的Bean就可以在多线程中共享了。

ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。

(1)在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。

(2)而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。

(3)概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

请解释Spring自动装配模式的区别?

在Spring框架中共有5种自动装配:

(1)no:这是Spring框架的默认设置,在该设置下自动装配是关闭的,开发者需要自行在bean定义中用标签明确的设置依赖关系。

(2)byName:该选项可以根据bean名称设置依赖关系。当向一个bean中自动装配一个属性时,容器将根据bean的名称自动在配置文件中查询一个匹配的bean。如果找到的话,就装配这个属性,如果没找到的话就报错。

(3)byType:该选项可以根据bean类型设置依赖关系。当向一个bean中自动装配一个属性时,容器将根据bean的类型自动在在配置文件中查询一个匹配的bean。如果找到的话,就装配这个属性,如果没找到的话就报错。

(4)constructor:构造器的自动装配和byType模式类似,但是仅仅适用于与有构造器相同参数的bean,如果在容器中没有找到与构造器参数类型一致的bean,那么将会抛出异常。

(5)autodetect:该模式自动探测使用构造器自动装配或者byType自动装配。首先,首先会尝试找合适的带参数的构造器,如果找到的话就是用构造器自动装配,如果在bean内部没有找到相应的构造器或者是无参构造器,容器就会自动选择byTpe的自动装配方式。

Spring 控制器的加载过程:(XML版)

(1)Web容器创建;

(2)上下文创建,但未初始化;

(3)监听器创建,并注册到Context上;

(4)上下文初始化;

(5)通知到监听者,Spring配置文件/@Configuration加载;

(6)Load-on-startup>0的ServletConfig创建,springMVC的DispatcherServlet此时创建。

PS:Spring容器时SpringMVC的父容器。Spring的AOP在Spring的上下文创建时就会创建;如果想要代理SpringMVC的控制层,需要将配置写到SpringMVC的配置文件下。

Spring 框架中都用到了哪些设计模式?

(1)代理模式—在AOP和remoting中被用的比较多。

(2)单例模式—在spring配置文件中定义的bean默认为单例模式。

(3)工厂模式—BeanFactory用来创建对象的实例。

(4)模板方法—用来解决代码重复的问题。比如. RestTemplate, JmsTemplate, JpaTemplate。

(5)前端控制器—Spring提供了DispatcherServlet来对请求进行分发。

(6)视图帮助(View Helper )—Spring提供了一系列的JSP标签,高效宏来辅助将分散的代码整合在视图里。

(7)依赖注入—贯穿于BeanFactory / ApplicationContext接口的核心理念。

Spring事务的种类和各自的区别

spring支持编程式事务管理和声明式事务管理两种方式:

(1)编程式事务管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理,spring推荐使用TransactionTemplate。

(2)声明式事务管理建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过基于@Transactional注解的方式),便可以将事务规则应用到业务逻辑中。

(3)显然声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式。声明式事务管理使业务代码不受污染,一个普通的POJO对象,只要加上注解就可以获得完全的事务支持。和编程式事务相比,声明式事务唯一不足地方是,后者的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。

Spring的事务传播行为

Spring事务的传播行为说的是当一个方法调用另一个方法时,事务该如何操作。
(1)PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。

(2)PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。‘

(3)PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。

(4)PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。

(5)PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

(6)PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。

(7)PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。

Spring事务的实现方式和实现原理

(1)划分处理单元——IOC:

由于spring解决的问题是对单个数据库进行局部事务处理的,具体的实现首相用spring中的IOC划分了事务处理单元。并且将对事务的各种配置放到了ioc容器中(设置事务管理器,设置事务的传播特性及隔离机制)。

(2)AOP拦截需要进行事务处理的类:

Spring事务处理模块是通过AOP功能来实现声明式事务处理的,具体操作(比如事务实行的配置和读取,事务对象的抽象),用TransactionProxyFactoryBean接口来使用AOP功能,生成proxy代理对象,通过TransactionInterceptor完成对代理方法的拦截,将事务处理的功能编织到拦截的方法中。

读取ioc容器事务配置属性,转化为spring事务处理需要的内部数据结构(TransactionAttributeSourceAdvisor),转化为TransactionAttribute表示的数据对象。

(3)对事物处理实现(事务的生成、提交、回滚、挂起):

spring委托给具体的事务处理器实现。实现了一个抽象和适配。适配的具体事务处理器:DataSource数据源支持、hibernate数据源事务处理支持、JDO数据源事务处理支持,JPA、JTA数据源事务处理支持。这些支持都是通过设计PlatformTransactionManager、AbstractPlatforTransaction一系列事务处理的支持。为常用数据源支持提供了一系列的TransactionManager。

(4)结合:

PlatformTransactionManager实现了TransactionInterception接口,让其与TransactionProxyFactoryBean结合起来,形成一个Spring声明式事务处理的设计体系。

解释一下Spring AOP里面的几个名词

(1)切面(Aspect):一个关注点的模块化,这个关注点可能会横切多个对象。事务管理是J2EE应用中一个关于横切关注点的很好的例子。 在Spring AOP中,切面可以使用通用类(基于模式的风格) 或者在普通类中以 @Aspect 注解(@AspectJ风格)来实现。

(2)连接点(Joinpoint):在程序执行过程中某个特定的点,比如某方法调用的时候或者处理异常的时候。 在Spring AOP中,一个连接点 总是 代表一个方法的执行。 通过声明一个org.aspectj.lang.JoinPoint类型的参数可以使通知(Advice)的主体部分获得连接点信息。

(3)通知(Advice):在切面的某个特定的连接点(Joinpoint)上执行的动作。通知有各种类型,其中包括“around”、“before”和“after”等通知。 通知的类型将在后面部分进行讨论。许多AOP框架,包括Spring,都是以拦截器做通知模型, 并维护一个以连接点为中心的拦截器链。

(4)切入点(Pointcut):匹配连接点(Joinpoint)的断言。通知和一个切入点表达式关联,并在满足这个切入点的连接点上运行(例如,当执行某个特定名称的方法时)。 切入点表达式如何和连接点匹配是AOP的核心:Spring缺省使用AspectJ切入点语法。

(5)引入(Introduction):(也被称为内部类型声明(inter-type declaration))。声明额外的方法或者某个类型的字段。 Spring允许引入新的接口(以及一个对应的实现)到任何被代理的对象。例如,你可以使用一个引入来使bean实现 IsModified 接口,以便简化缓存机制。

(6)目标对象(Target Object): 被一个或者多个切面(aspect)所通知(advise)的对象。也有人把它叫做 被通知(advised) 对象。 既然Spring AOP是通过运行时代理实现的,这个对象永远是一个 被代理(proxied) 对象。

(7)织入(Weaving):把切面(aspect)连接到其它的应用程序类型或者对象上,并创建一个被通知(advised)的对象。 这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。 Spring和其他纯Java AOP框架一样,在运行时完成织入。

切入点(pointcut)和连接点(join point)匹配的概念是AOP的关键,这使得AOP不同于其它仅仅提供拦截功能的旧技术。 切入点使得定位通知(advice)可独立于OO层次。 例如,一个提供声明式事务管理的around通知可以被应用到一组横跨多个对象中的方法上(例如服务层的所有业务操作)。

Spring1

通知有哪些类型?

(1)前置通知(Before advice):在某连接点(join point)之前执行的通知,但这个通知不能阻止连接点前的执行(除非它抛出一个异常)。

(2)返回后通知(After returning advice):在某连接点(join point)正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回。
(3)抛出异常后通知(After throwing advice):在方法抛出异常退出时执行的通知。
(4)后通知(After (finally) advice):当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。
(5)环绕通知(Around Advice):包围一个连接点(join point)的通知,如方法调用。这是最强大的一种通知类型。 环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行。

环绕通知是最常用的一种通知类型。大部分基于拦截的AOP框架,例如Nanning和JBoss4,都只提供环绕通知。

Spring IOC和Spring AOP

发表于 2019-03-02 | 分类于 java面试准备 , Spring |

Spring IOC

IOC原理

IOC的意思是控件反转也就是由容器控制程序之间的关系,这也是spring的优点所在,把控件权交给了外部容器。之前的写法,由程序代码直接操控,而现在控制权由应用代码中转到了外部容器。控制权的转移是所谓反转。换句话说之前用new的方式获取对象,现在由Spring给你,至于怎么给你就是DI了。

什么是DI机制(依赖注入)

这里说DI又要说到IOC,依赖注入(Dependecy Injection)和控制反转(Inversion of Control)是同一个概念。具体的讲:当某个角色需要另外一个角色协助的时候,在传统的程序设计过程中,通常由调用者来创建被调用者的实例。但是在Spring中 创建被调用者的工作不再由调用者来完成,因此称为控制反转。在Spring中创建被调用者的工作由Spring来完成,然后注入调用者 因此也称为依赖注入。

Spring以动态灵活的方式来管理对象 , 注入的四种方式:

  1. 构造方法注入

  2. 注解注入

  3. setter注入

    注意:如果通过set方法注入属性,那么spring会通过默认的空参构造方法来实例化对象,所以如果在类中写了一个带有参数的构造方法,一定要把空参数的构造方法写上,否则spring没有办法实例化对象,导致报错。

IOC的实现原理

Java反射机制

概念

我们考虑一个场景,如果我们在程序运行时,一个对象想要检视自己所拥有的成员属性,该如何操作?

再考虑另一个场景,如果我们想要在运行期获得某个类的Class信息如它的属性、构造方法、一般方法后再考虑是否创建它的对象,这种情况该怎么办呢?这就需要用到反射!

对于反射,官方给出的概念:反射是Java语言的一个特性,它允许程序在运行时(注意不是编译的时候)来进行自我检查并且对内部的成员进行操作。例如它允许一个Java类获取它所有的成员变量和方法并且显示出来。

反射主要是指程序可以访问,检测和修改它本身状态或行为的一种能力,并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义。在Java中,只要给定类的名字,那么就可以通过反射机制来获得类的所有信息。

反射是Java中一种强大的工具,能够使我们很方便的创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代码链接。但是如果反射使用不当会导致高成本!类中有什么信息,利用反射机制就能可以获得什么信息,不过前提是得知道类的名字。

作用

  1. 在运行时判断任意一个对象所属的类;
  2. 在运行时获取类的对象;
  3. 在运行时访问java对象的属性,方法,构造方法等。

首先要搞清楚为什么要用反射机制?直接创建对象不就可以了吗,这就涉及到了动态与静态的概念。

静态编译:在编译时确定类型,绑定对象,即通过。

动态编译:运行时确定类型,绑定对象。动态编译最大限度发挥了Java的灵活性,体现了多态的应用,有以降低类之间的藕合性。

反射机制的深度解析

Java的反射机制的实现要借助于4个类:class,Constructor,Field,Method;

Class:类对象,
Constructor-类的构造器对象,
Field-类的属性对象,
Method-类的方法对象。

Class反射对象描述类语义结构,可以从Class对象中获取构造函数、成员变量、方法类等类元素的反射对象,并以编程的方式通过这些反射对象对目标类对象进行操作。这些反射对象类在java.reflect包中定义,下面是最主要的三个反射类:

  • Constructor:类的构造函数反射类

    通过Class.getDeclaredConstructors()方法可以获得类的所有构造函数反射对象数组。

    在JDK5.0中,还可以通过getDeclaredConstructor(Class… parameterTypes)获取拥有特定入参的构造函数反射对象。

    Constructor的一个主要方法是newInstance(Object[] initargs),通过该方法可以创建一个对象类的实例,相当于new关键字。在JDK5.0中该方法演化为更为灵活的形式:newInstance (Object… initargs)。

  • Method:类方法的反射类

    通过Class.getDeclaredMethods()方法可以获取类的所有方法反射类对象数组Method[]。

    在JDK5.0中可以通过getDeclaredMethod(String name, Class… parameterTypes)获取特定方法,name为方法名,Class…为方法入参类型列表。

    Method最主要的方法是invoke(Object obj, Object[] args),obj表示操作的目标对象,args为方法入参。

  • Field:类的成员变量的反射类

    通过Class.getDeclaredFields()方法可以获取类的成员变量反射对象数组

    通过Class.getDeclaredField(String name)则可获取某个特定名称的成员变量反射对象。

    Field类最主要的方法是set(Object obj, Object value),obj表示操作的目标对象,通过value为目标对象的成员变量设置值。如果成员变量为基础类型,用户可以使用Field类中提供的带类型名的值设置方法,比如setBoolean(Object obj, boolean value)、setInt(Object obj, int value)等。

基本运用

  1. 获取Class对象

    • 使用 Class 类的 forName 静态方法:

      1
      2
      3
      4
      public static Class<?> forName(String className)
      ```
      比如在 JDBC 开发中常用此方法加载数据库驱动:
      Class.forName(driver);
    • 直接获取某一个对象的 class,比如:

      1
      2
      Class<?> klass = int.class;
      Class<?> classInt = Integer.TYPE;
    • 调用某个对象的 getClass() 方法,比如:

      1
      2
      StringBuilder str = new StringBuilder("123");
      Class<?> klass = str.getClass();
  2. 判断是否为某个类的实例

    一般地,我们用 instanceof 关键字来判断是否为某个类的实例。同时我们也可以借助反射中 Class 对象的 isInstance() 方法来判断是否为某个类的实例,它是一个 native 方法:

    1
    public native boolean isInstance(Object obj);
  3. 创建实例对象

    • 使用Class对象的newInstance()方法来创建Class对象对应类的实例

      1
      2
      Class<?> c = String.class;
      Object str = c.newInstance();
    • 先通过Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建实例。这种方法可以用指定的构造器构造类的实例

      1
      2
      3
      4
      5
      6
      7
      //获取String所对应的Class对象
      Class<?> c = String.class;
      //获取String类带一个String参数的构造器
      Constructor constructor = c.getConstructor(String.class);
      //根据构造器创建实例
      Object obj = constructor.newInstance("23333");
      System.out.println(obj);
  4. 获取方法

    获取某个Class对象的方法集合,主要有以下几个方法:

    • getDeclaredMethods 方法返回类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法。
    1
    public Method[] getDeclaredMethods() throws SecurityException
    • getMethods 方法返回某个类的所有公用(public)方法,包括其继承类的公用方法。
    1
    public Method[] getMethods() throws SecurityException
    • getMethod 方法返回一个特定的方法,其中第一个参数为方法名称,后面的参数为方法的参数对应Class的对象。
    1
    public Method getMethod(String name, Class<?>... parameterTypes)

    例子如下:

    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
    package org.ScZyhSoft.common;
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    public class test1 {
    public static void test() throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
    Class<?> c = methodClass.class;
    Object object = c.newInstance();
    Method[] methods = c.getMethods();
    Method[] declaredMethods = c.getDeclaredMethods();
    //获取methodClass类的add方法
    Method method = c.getMethod("add", int.class, int.class);
    //getMethods()方法获取的所有方法
    System.out.println("getMethods获取的方法:");
    for(Method m:methods)
    System.out.println(m);
    //getDeclaredMethods()方法获取的所有方法
    System.out.println("getDeclaredMethods获取的方法:");
    for(Method m:declaredMethods)
    System.out.println(m);
    }
    }
    class methodClass {
    public final int fuck = 3;
    public int add(int a,int b) {
    return a+b;
    }
    public int sub(int a,int b) {
    return a+b;
    }
    }

    程序的运行结果如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    getMethods获取的方法:
    public int org.ScZyhSoft.common.methodClass.add(int,int)
    public int org.ScZyhSoft.common.methodClass.sub(int,int)
    public final void java.lang.Object.wait() throws java.lang.InterruptedException
    public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
    public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
    public boolean java.lang.Object.equals(java.lang.Object)
    public java.lang.String java.lang.Object.toString()
    public native int java.lang.Object.hashCode()
    public final native java.lang.Class java.lang.Object.getClass()
    public final native void java.lang.Object.notify()
    public final native void java.lang.Object.notifyAll()
    getDeclaredMethods获取的方法:
    public int org.ScZyhSoft.common.methodClass.add(int,int)
    public int org.ScZyhSoft.common.methodClass.sub(int,int)
  5. 获取构造器信息

    获取类构造器的用法与上述获取方法的用法类似。主要是通过Class类的getConstructor方法得到Constructor类的一个实例,而Constructor类有一个newInstance方法可以创建一个对象实例:

    1
    public T newInstance(Object ... initargs)
  6. 获取类的成员变量信息

    主要是这几个方法,在此不再赘述:

    • getFiled:访问公有的成员变量
    • getDeclaredField:所有已声明的成员变量,但不能得到其父类的成员变量

    getFileds 和 getDeclaredFields 方法用法同上(参照 Method)

  7. 调用方法

    当我们从类中获取了一个方法后,我们就可以用 invoke() 方法来调用这个方法。invoke 方法的原型为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public Object invoke(Object obj, Object... args)
    throws IllegalAccessException, IllegalArgumentException,
    InvocationTargetException
    {
    if (!override) {
    if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
    Class<?> caller = Reflection.getCallerClass();
    checkAccess(caller, clazz, obj, modifiers);
    }
    }
    MethodAccessor ma = methodAccessor; // read volatile
    if (ma == null) {
    ma = acquireMethodAccessor();
    }
    return ma.invoke(obj, args);
    }

    例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class test1 {
    public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
    Class<?> klass = methodClass.class;
    //创建methodClass的实例
    Object obj = klass.newInstance();
    //获取methodClass类的add方法
    Method method = klass.getMethod("add",int.class,int.class);
    //调用method对应的方法 => add(1,4)
    Object result = method.invoke(obj,1,4);
    System.out.println(result);
    }
    }
    class methodClass {
    public final int fuck = 3;
    public int add(int a,int b) {
    return a+b;
    }
    public int sub(int a,int b) {
    return a+b;
    }
    }

IOC容器技术剖析

IOC中最基本的技术就是“反射(Reflection)”编程,通俗来讲就是根据给出的类名(字符串方式)来动态地生成对象,这种编程方式可以让对象在生成时才被决定到底是哪一种对象。只是在Spring中要生产的对象都在配置文件中给出定义,目的就是提高灵活性和可维护性。

目前C#、Java和PHP5等语言均支持反射,其中PHP5的技术书籍中,有时候也被翻译成“映射”。有关反射的概念和用法,大家应该都很清楚。反射的应用是很广泛的,很多的成熟的框架,比如像Java中的Hibernate、Spring框架,.Net中NHibernate、Spring.NET框架都是把”反射“做为最基本的技术手段。

我们可以把IOC容器的工作模式看做是工厂模式的升华,可以把IOC容器看作是一个工厂,这个工厂里要生产的对象都在配置文件中给出定义,然后利用编程语言提供的反射机制,根据配置文件中给出的类名生成相应的对象。从实现来看,IOC是把以前在工厂方法里写死的对象生成代码,改变为由配置文件来定义,也就是把工厂和对象生成这两者独立分隔开来,目的就是提高灵活性和可维护性。

工厂模式

Spring AOP

什么是AOP?

面向切面编程(AOP)完善Spring的依赖注入(DI),面向切面编程在spring中主要表现为两个方面:

  1. 面向切面编程提供声明式事务管理
  2. Spring支持用户自定义的切面

面向切面编程(aop)是对面向对象编程(oop)的补充。

面向对象编程将程序分解成各个层次的对象,面向切面编程将程序运行过程分解成各个切面。

AOP从程序运行角度考虑程序的结构,提取业务处理过程的切面,oop是静态的抽象,aop是动态的抽象, 是对应用执行过程中的步骤进行抽象,从而获得步骤之间的逻辑划分。

具体使用场景:

  1. Authentication:权限
  2. Caching:缓存
  3. Context passing:内容传递
  4. Error handling:错误处理
  5. Lazy loading:懒加载
  6. Debugging:调试
  7. logging, tracing, profiling and monitoring:记录跟踪 优化 校准
  8. Performance optimization:性能优化
  9. Persistence:持久化
  10. Resource pooling:资源池
  11. Synchronization:同步
  12. Transactions:事务

使用AOP需要的一些概念

  • Aspect(切面):Aspect 声明类似于 Java 中的类声明,在 Aspect 中会包含着一些 Pointcut 以及相应Advice。
  • Joint point(连接点):表示在程序中明确定义的点,典型的包括方法调用,对类成员的访问以及异常处理程序块的执行等等,它自身还可以嵌套其它 joint point。
  • Pointcut(切点):表示一组 joint point,这些 joint point 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方。
  • Advice(增强):Advice 定义了在 Pointcut 里面定义的程序点具体要做的操作,它通过 before、after 和around 来区别是在每个 joint point 之前、之后还是代替执行的代码。
  • Target(目标对象):织入 Advice 的目标对象.,即被代理类。
  • Weaving(织入):将 Aspect 和其他对象连接起来, 并创建 Adviced object 的过程。

1.通知(Advice)

通知定义了在切入点代码执行时间点附近需要做的工作。

Spring支持五种类型的通知:

  1. Before(前) org.apringframework.aop.MethodBeforeAdvice
  2. After-returning(返回后) org.springframework.aop.AfterReturningAdvice
  3. After-throwing(抛出后) org.springframework.aop.ThrowsAdvice
  4. Arround(周围) org.aopaliance.intercept.MethodInterceptor
  5. Introduction(引入) org.springframework.aop.IntroductionInterceptor

2.连接点(Joinpoint)

程序能够应用通知的一个“时机”,这些“时机”就是连接点,例如方法调用时、异常抛出时、方法返回后等等。

3.切入点(Pointcut)

通知定义了切面要发生的“故事”,连接点定义了“故事”发生的时机,那么切入点就定义了“故事”发生的地点。例如某个类或方法的名称,Spring中允许我们方便的用正则表达式来指定。

4.切面(Aspect)

通知、连接点、切入点共同组成了切面:时间、地点和要发生的“故事”。

5.引入(Introduction)

引入允许我们向现有的类添加新的方法和属性(Spring提供了一个方法注入的功能)。

6.目标(Target)

即被通知的对象,如果没有AOP,那么通知的逻辑就要写在目标对象中,有了AOP之后它可以只关注自己要做的事,解耦合!

7.代理(proxy)

应用通知的对象,详细内容参见设计模式里面的动态代理模式。

8.织入(Weaving)

把切面应用到目标对象来创建新的代理对象的过程,织入一般发生在如下几个时机:

(1)编译时:当一个类文件被编译时进行织入,这需要特殊的编译器才可以做的到,例如AspectJ的织入编译器;

(2)类加载时:使用特殊的ClassLoader在目标类被加载到程序之前增强类的字节代码;

(3)运行时:切面在运行的某个时刻被织入,SpringAOP就是以这种方式织入切面的,原理应该是使用了JDK的动态代理技术。

Spring AOP的实现机制

aop1

Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理。

JDK动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。JDK动态代理的核心是InvocationHandler接口和Proxy类。如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。

CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意,CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。

下面我们来了解一下什么是代理模式?

代理模式介绍

什么是代理模式?就是用一个新的对象来伪装原来的对象,从而实现一些“不可告人”的动作。

什么情况下会使用代理模式?简单来说,就是不能或者不想直接引用一个对象。什么是不能?比如我在内网中想访问外网的资源,但是因为网关的控制,访问不了。那什么是不想呢?比如我在网页上要显示一张图片,但是图片太大了,会拉慢页面的加载速度,我想用一张小一点的图片代替。

来看一张类结构图:

aop2

  • Subject:原对象的抽象
  • RealSubject:原对象的实现
  • Proxy: 代理对象

通过代理模式,客户端访问时同原来一样,但访问的前后已经做了额外的操作(可能你的信息和数据就被窃取了)。

好了,来看一个正常点的例子。做IT的一般都需要翻墙,比如去YouTube上看点MV啥的(说好的正常呢),但是正常访问肯定是要被屏蔽的,所以就要通过一些工具去穿过重重防守的GTW。一般的方式就是本地的工具将你的访问信息加密后,交给一个未被屏蔽的国外的服务器,然后服务器解密这些访问信息,去请求原始的访问地址,再将请求得到的资源和信息回传给你自己的本地。我们以浏览器来举例。

浏览器接口:

1
2
3
4
public interface Browser {

void visitInternet();
}

Chrome的实现类:

1
2
3
4
5
6
7
public class ChromeBrowser implements Browser{

public void visitInternet() {
System.out.println("visit YouTube");
}

}

如果直接访问肯定是要挂掉的,我们通过解密和加密的两个方法简单模拟翻墙的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ChromeBrowser implements Browser{

public void visitInternet() {
encrypt();
System.out.println("visit YouTube");
decrypt();
}

// 加密
private void encrypt(){
System.out.println("encrypt ...");
}

// 解密
private void decrypt(){
System.out.println("decrypt ...");
}
}

总结:虽然这样就可以访问成功了,但直接将加密和解密的方式写死在原对象里,不仅侵入了原有的代码结构且会显得很LOW。

静态代理

根据上面的代理模式的类图,最简单的方式就是写一个静态代理,为ChromeBrowser写一个代理类。

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
public class ChromeBrowserProxy implements Browser{

private ChromeBrowser browser;

public ChromeBrowserProxy(ChromeBrowser chromeBrowser) {
this.browser = chromeBrowser;
}

public void visitInternet() {
encrypt();
browser.visitInternet();
decrypt();
}

// 加密
private void encrypt(){
System.out.println("encrypt ...");
}

// 解密
private void decrypt(){
System.out.println("decrypt ...");
}

}

ChromeBrowserProxy同样实现Browser接口,客户端访问时不再直接访问ChromeBrowser,而是通过它的代理类。

下面是静态代理的测试类:

1
2
3
4
5
6
7
public class StaticProxyTest {

public static void main(String[] args) {
Browser browser = new ChromeBrowserProxy(new ChromeBrowser());
browser.visitInternet();
}
}

总结:这种方式解决了对原对象的代码侵入,但是出现了另一个问题。如果我有好几个浏览器,难道每个浏览器的实现类都要写一个代理类吗?太LOW太LOW。我们需要更牛B的方式:JDK动态代理。

JDK动态代理

在JDK中提供了一种代理的实现方式,可以动态地创建代理类,就是java.lang.reflect包中的Proxy类提供的newProxyInstance方法。

1
Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
  • classLoader是创建代理类的类加载器
  • interfaces是原对象实现的接口
  • InvocationHandler是回调方法的接口

真正的代理过程通过InvocationHandler接口中的invoke方法来实现:

1
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
  • proxy是代理对象
  • method是执行的方法
  • args是执行方法的参数数组

还是以Chrome浏览器举例,JdkBrowserProxy实现InvocationHandler接口,并通过构造方法传入被代理的对象,然后在invoke方法中实现代理的过程。

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
public class JdkBrowserProxy implements InvocationHandler{

private Browser browser;

public JdkBrowserProxy(Browser browser) {
this.browser = browser;
}

public Browser getProxy(){
return (Browser) Proxy.newProxyInstance(browser.getClass().getClassLoader(),
browser.getClass().getInterfaces(), this);
}

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
encrypt();// 程序执行前加入逻辑,MethodBeforeAdviceInterceptor
Object retVal = method.invoke(browser, args);
decrypt();// 程序执行后加入逻辑,MethodAfterAdviceInterceptor
return retVal;
}

/**
* 加密
*/
private void encrypt(){
System.out.println("encrypt ...");
}

/**
* 解密
*/
private void decrypt(){
System.out.println("decrypt ...");
}
}

来看测试方法:

1
2
3
4
5
6
7
public class JdkDynamicProxyTest {

public static void main(String[] args) {
Browser browser = new JdkBrowserProxy(new ChromeBrowser()).getProxy();
browser.visitInternet();
}
}

总结:JDK的动态代理基本能够解决大部分的需求,唯一的缺点就是它只能代理接口中的方法。如果被代理对象没有实现接口,或者想代理没在接口中定义的方法,JDK的动态代理就无能为力了,此时就需要CGLIB动态代理。

CGLIB动态代理

cglib是一种强大的,高性能高品质的代码生成库,用来在运行时扩展JAVA的类以及实现指定接口。

通过cglib提供的Enhancer类的create静态方法来创建代理类。

1
Enhancer.create(Class type, Callback callback)
  • type是原对象的Class对象
  • callback是回调方法接口

cglib中的callback通过实现它的MethodInterceptor接口的intercept方法来进行回调:

1
public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args, MethodProxy proxy) throws Throwable;
  • obj是被代理的对象
  • method是执行的方法
  • args是执行方法的参数数组
  • proxy用来执行未被拦截的原方法

cglib代理类不局限于上面的浏览器的例子,而是通过泛型来实现通用,并且使用单例模式减少代理类的重复创建。

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
public class CglibBrowserProxy implements MethodInterceptor{

private static CglibBrowserProxy proxy = new CglibBrowserProxy();

private CglibBrowserProxy(){

}

//获取静态实例的方法
public static CglibBrowserProxy getInstance(){
return proxy;
}

public <T> T getProxy(Class<T> clazz){
return (T) Enhancer.create(clazz, this);
}

public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
encrypt();// 程序执行前加入逻辑,MethodBeforeAdviceInterceptor
Object retVal = proxy.invokeSuper(obj, args);
decrypt();// 程序执行后加入逻辑,MethodAfterAdviceInterceptor
return retVal;
}

/**
* 加密
*/
private void encrypt(){
System.out.println("encrypt ...");
}

/**
* 解密
*/
private void decrypt(){
System.out.println("decrypt ...");
}
}

然后在ChromeBrowser添加一个听音乐的方法,它并未在Browser接口定义:

1
2
3
public void listenToMusic(){
System.out.println("listen to Cranberries");
}

来看下客户端测试:

1
2
3
4
5
6
7
8
public class CglibDynamicProxyTest {

public static void main(String[] args) {
ChromeBrowser browser = CglibBrowserProxy.getInstance().getProxy(ChromeBrowser.class);
browser.visitInternet();
browser.listenToMusic();
}
}

总结:可以发现没有使用Browser接口来接受代理对象,而是直接使用ChromeBrowser对象。这样的方式就可以代理ChromeBrowser中未在Chrome接口中的方法。

抛出问题:如果想让一个对象调用它未实现的接口中的方法,即后面AOP里所说的引用增强,原生的cglib怎么实现呢?

CGLIB引入增强

引入增强听上去很高大上,其实它的实现原理就以下几步:

  1. 通过CGLIB创建代理对象,并使其实现指定接口
  2. 在MethodIntercept的回调方法中,判断执行方法是否为接口中的方法,如果是,则通过反射调用接口的实现类。

创建一个新接口Game,它定义了开始的方法:

1
2
3
4
public interface Game {

void start();
}

让代理类实现Game接口,并在intercept方法中判断执行方法是接口方法还是原对象的方法:

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
public class CglibIntroductionBrowserProxy implements MethodInterceptor,Game{

private static CglibIntroductionBrowserProxy proxy = new CglibIntroductionBrowserProxy();

private CglibIntroductionBrowserProxy(){

}

public static CglibIntroductionBrowserProxy getInstance(){
return proxy;
}

public <T> T getProxy(Class<T> clazz){
return (T) Enhancer.create(clazz, new Class[]{ Game.class }, this);
}

public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
// 程序执行前加入逻辑,MethodBeforeAdviceInterceptor
Object retVal;
if(method.getDeclaringClass().isInterface()){
method.setAccessible(true);
retVal = method.invoke(this, args);
}else{
retVal = proxy.invokeSuper(obj, args);
}
// 程序执行后加入逻辑,MethodAfterAdviceInterceptor
return retVal;
}

public void start() {
System.out.println("start a game");
}

}

来看测试类:

1
2
3
4
5
6
7
8
9
10
public class CglibIntroductionDynamicProxyTest {

public static void main(String[] args) {
Browser browser = CglibIntroductionBrowserProxy.getInstance().getProxy(ChromeBrowser.class);
browser.visitInternet();

Game game = (Game) browser;
game.start();
}
}

总结:可以发现执行接口方法时,通过jdk的反射机制来实现的。而调用其自身方法,则是通过cglib来触发的。

最后补充几点

  1. JDK动态代理的代理对象只能通过接口去接收,如果用原对象接收,会报类型转换异常。
  2. cglib不能拦截final修饰的方法,调用时只会执行原有方法。
  3. cglib是在运行时通过操作字节码来完成类的扩展和改变,除了代理,还支持很多强大的操作,比如bean的生成和属性copy,动态创建接口以及融合多个对象等,具体见https://github.com/cglib/cglib/wiki/Tutorial。

参考:

https://my.oschina.net/u/2377110/blog/1504596

https://my.oschina.net/u/2377110/blog/1504596

https://blog.csdn.net/dreamrealised/article/details/12885739

Spring Bean

发表于 2019-02-28 | 分类于 java面试准备 , Spring |

前言

在 Spring 中,那些组成应用程序的主体及由 Spring IOC 容器所管理的对象,被称之为 bean。简单地讲,bean 就是由 IOC 容器初始化、装配及管理的对象,除此之外,bean 就与应用程序中的其他对象没有什么区别了。而 bean 的定义以及 bean 相互间的依赖关系将通过配置元数据来描述。

Spring中的bean默认都是单例的,这些单例Bean在多线程程序下如何保证线程安全呢? 例如对于Web应用来说,Web容器对于每个用户请求都创建一个单独的Sevlet线程来处理请求,引入Spring框架之后,每个Action都是单例的,那么对于Spring托管的单例Service Bean,如何保证其安全呢? Spring的单例是基于BeanFactory也就是Spring容器的,单例Bean在此容器内只有一个,Java的单例是基于 JVM,每个 JVM 内只有一个实例。

在大多数情况下,单例 bean 是很理想的方案。不过,有时候你可能会发现你所使用的类是易变的,它们会保持一些状态,因此重用是不安全的。在这种情况下,将 class 声明为单例的就不是那么明智了。因为对象会被污染,稍后重用的时候会出现意想不到的问题,所以 Spring 定义了多种作用域的bean。

bean的作用域

创建一个bean定义,其实质是用该bean定义对应的类来创建真正实例的“配方”。把bean定义看成一个配方很有意义,它与class很类似,只根据一张“处方”就可以创建多个实例。不仅可以控制注入到对象中的各种依赖和配置值,还可以控制该对象的作用域。这样可以灵活选择所建对象的作用域,而不必在Java Class级定义作用域。

Spring Framework支持五种作用域,分别阐述如下表:

bean1

五种作用域中,request、session 和 global session 三种作用域仅在基于web的应用中使用(不必关心你所采用的是什么web应用框架),只能用在基于 web 的 Spring ApplicationContext(另外一种IOC容器)环境。

  1. singleton——唯一 bean 实例

    当一个 bean 的作用域为 singleton,那么Spring IoC容器中只会存在一个共享的 bean 实例,并且所有对 bean 的请求,只要 id 与该 bean 定义相匹配,则只会返回bean的同一实例。 singleton 是单例类型(对应于单例模式),就是在创建起容器时就同时自动创建了一个bean的对象,不管你是否使用,但我们可以指定Bean节点的 lazy-init=”true” 来延迟初始化bean**,这时候,只有在第一次获取bean时才会初始化bean,即第一次请求该bean时才初始化。 每次获取到的对象都是同一个对象。注意,singleton 作用域是Spring中的缺省作用域。

    要在XML中将 bean 定义成 singleton ,可以这样配置:

    1
    <bean id="ServiceImpl" class="cn.csdn.service.ServiceImpl" scope="singleton">

    也可以通过 @Scope 注解(它可以显示指定bean的作用范围)的方式:

    1
    2
    3
    4
    5
    @Service
    @Scope("singleton")
    public class ServiceImpl{

    }
  2. prototype——每次请求都会创建一个新的 bean 实例

    当一个bean的作用域为 prototype,表示一个 bean 定义对应多个对象实例。 prototype 作用域的 bean 会导致在每次对该 bean 请求(将其注入到另一个 bean 中,或者以程序的方式调用容器的 getBean() 方法)时都会创建一个新的 bean 实例。prototype 是原型类型,它在我们创建容器的时候并没有实例化,而是当我们获取bean的时候才会去创建一个对象,而且我们每次获取到的对象都不是同一个对象。

    根据经验,对有状态的 bean 应该使用 prototype 作用域,而对无状态的 bean 则应该使用 singleton 作用域。

    在 XML 中将 bean 定义成 prototype ,可以这样配置:

    1
    2
    3
    <bean id="account" class="com.foo.DefaultAccount" scope="prototype"/>  
    或者
    <bean id="account" class="com.foo.DefaultAccount" singleton="false"/>

    通过 @Scope 注解的方式实现就不做演示了。

  3. request——每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效

    request只适用于Web程序,每一次 HTTP 请求都会产生一个新的bean,同时该bean仅在当前HTTP request内有效,当请求结束后,该对象的生命周期即告结束。

    在 XML 中将 bean 定义成 request ,可以这样配置:

    1
    <bean id="loginAction" class=cn.csdn.LoginAction" scope="request"/>
  4. session——每一次HTTP请求都会产生一个新的 bean,该bean仅在当前 HTTP session 内有效

    session只适用于Web程序,session 作用域表示该针对每一次 HTTP 请求都会产生一个新的 bean,同时该 bean 仅在当前 HTTP session 内有效.与request作用域一样,可以根据需要放心的更改所创建实例的内部状态,而别的 HTTP session 中根据 userPreferences 创建的实例,将不会看到这些特定于某个 HTTP session 的状态变化。当HTTP session最终被废弃的时候,在该HTTP session作用域内的bean也会被废弃掉。

    在 XML 中将 bean 定义成 session ,可以这样配置:

    1
    <bean id="userPreferences" class="com.foo.UserPreferences" scope="session"/>
  5. globalSession

    globalSession作用域类似于标准的 HTTP session 作用域,不过仅仅在基于 portlet 的 web 应用中才有意义。Portlet 规范定义了全局 Session 的概念,它被所有构成某个 portlet web 应用的各种不同的 portle t所共享。在global session 作用域中定义的 bean 被限定于全局portlet Session的生命周期范围内。

    在 XML 中将 bean 定义成 globalSession ,可以这样配置:

    1
    <bean id="user" class="com.foo.Preferences "scope="globalSession"/>

bean的生命周期

概述

Spring的IOC容器功能非常强大,负责Spring的Bean的创建和管理等功能。而Spring bean是整个Spring应用中很重要的一部分,了解Spring Bean的生命周期对我们了解整个Spring框架会有很大的帮助。

BeanFactory、ApplicationContext是Spring两种很重要的容器。前者提供了最基本的依赖注入的支持,而后者在继承前者的基础进行了功能的拓展,例如增加了事件传播,资源访问和国际化的消息访问等功能。本文主要介绍了ApplicationContext和BeanFactory两种容器的Bean的生命周期。

Spring Bean的生命周期总的来说是这样子的:

bean4

  • Bean容器找到配置文件中 Spring Bean 的定义。
  • Bean容器利用Java Reflection API创建一个Bean的实例。
  • 如果涉及到一些属性值 利用set方法设置一些属性值。
  • 如果Bean实现了BeanNameAware接口,调用setBeanName()方法,传入Bean的名字。
  • 如果Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例。
  • 如果Bean实现了BeanFactoryAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例。
  • 与上面的类似,如果实现了其他*Aware接口,就调用相应的方法。
  • 如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象,执行postProcessBeforeInitialization()方法
  • 如果Bean实现了InitializingBean接口,执行afterPropertiesSet()方法。
  • 如果Bean在配置文件中的定义包含init-method属性,执行指定的方法。
  • 如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象,执行postProcessAfterInitialization()方法
  • 当要销毁Bean的时候,如果Bean实现了DisposableBean接口,执行destroy()方法。
  • 当要销毁Bean的时候,如果Bean在配置文件中的定义包含destroy-method属性,执行指定的方法。

ApplicationContext Bean生命周期

bean2

上图修正为“调用disposableBean的destroy()方法”

ApplicationContext容器中,Bean的生命周期流程如上图所示,流程大致如下:

1.首先容器启动后,会对scope为singleton且非懒加载的bean进行实例化。

2.按照Bean定义信息配置信息,注入所有的属性。

3.如果Bean实现了BeanNameAware接口,会回调该接口的setBeanName()方法,传入该Bean的id,此时该Bean就获得了自己在配置文件中的id。

4.如果Bean实现了BeanFactoryAware接口,会回调该接口的setBeanFactory()方法,传入该Bean的BeanFactory,这样该Bean就获得了自己所在的BeanFactory。

5.如果Bean实现了ApplicationContextAware接口,会回调该接口的setApplicationContext()方法,传入该Bean的ApplicationContext,这样该Bean就获得了自己所在的ApplicationContext。

6.如果有Bean实现了BeanPostProcessor接口,则会回调该接口的postProcessBeforeInitialzation()方法。

7.如果Bean实现了InitializingBean接口,则会回调该接口的afterPropertiesSet()方法。

8.如果Bean配置了init-method方法,则会执行init-method配置的方法。

9.如果有Bean实现了BeanPostProcessor接口,则会回调该接口的postProcessAfterInitialization()方法。

10.经过流程9之后,就可以正式使用该Bean了。对于scope为singleton的Bean,Spring的ioc容器中会缓存一份该bean的实例,而对于scope为prototype的Bean,每次被调用都会new一个新的对象,其生命周期就交给调用方管理了,不再是Spring容器进行管理了。

11.容器关闭后,如果Bean实现了DisposableBean接口,则会回调该接口的destroy()方法。

12.如果Bean配置了destroy-method方法,则会执行destroy-method配置的方法。至此,整个Bean的生命周期结束。

示例:

我们定义了一个Person类,该类实现了BeanNameAware,BeanFactoryAware,ApplicationContextAware,InitializingBean,DisposableBean五个接口。在applicationContext.xml文件中配置了该Bean的id为person1,并且配置了init-method和destroy-method,为该Bean配置了属性name为jack的值,然后定义了一个MyBeanPostProcessor方法,该方法实现了BeanPostProcessor接口,在applicationContext.xml文件中配置了该方法的Bean,其代码如下所示:

applicationContext.xml文件,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.2.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.2.xsd">

<bean id="person1" destroy-method="myDestroy"
init-method="myInit" class="com.test.spring.life.Person">
<property name="name">
<value>jack</value>
</property>
</bean>

<!-- 配置自定义的后置处理器 -->
<bean id="postProcessor" class="com.pingan.spring.life.MyBeanPostProcessor" />
</beans>

Person类,代码如下所示:

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
public class Person implements BeanNameAware, BeanFactoryAware,
ApplicationContextAware, InitializingBean, DisposableBean {

private String name;

public Person() {
System.out.println("PersonService类构造方法");
}


public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
System.out.println("set方法被调用");
}

//自定义的初始化函数
public void myInit() {
System.out.println("myInit被调用");
}

//自定义的销毁方法
public void myDestroy() {
System.out.println("myDestroy被调用");
}

public void destroy() throws Exception {
// TODO Auto-generated method stub
System.out.println("destory被调用");
}

public void afterPropertiesSet() throws Exception {
// TODO Auto-generated method stub
System.out.println("afterPropertiesSet被调用");
}

public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException {
// TODO Auto-generated method stub
System.out.println("setApplicationContext被调用");
}

public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
// TODO Auto-generated method stub
System.out.println("setBeanFactory被调用,beanFactory");
}

public void setBeanName(String beanName) {
// TODO Auto-generated method stub
System.out.println("setBeanName被调用,beanName:" + beanName);
}

public String toString() {
return "name is :" + name;
}

MyBeanPostProcessor类,实现了BeanPostProcessor接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyBeanPostProcessor implements BeanPostProcessor {

public Object postProcessBeforeInitialization(Object bean,
String beanName) throws BeansException {
// TODO Auto-generated method stub

System.out.println("postProcessBeforeInitialization被调用");
return bean;
}

public Object postProcessAfterInitialization(Object bean,
String beanName) throws BeansException {
// TODO Auto-generated method stub
System.out.println("postProcessAfterInitialization被调用");
return bean;
}

}

测试类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class AcPersonServiceTest {

public static void main(String[] args) {
// TODO Auto-generated method stub

System.out.println("开始初始化容器");
ApplicationContext ac = new ClassPathXmlApplicationContext("com/test/spring/life/applicationContext.xml");

System.out.println("xml加载完毕");
Person person1 = (Person) ac.getBean("person1");
System.out.println(person1);
System.out.println("关闭容器");
((ClassPathXmlApplicationContext)ac).close();

}

}

我们启动容器,可以看到整个初始化的过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
开始初始化容器
九月 25, 2016 10:44:50 下午 org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh
信息: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@b4aa453: startup date [Sun Sep 25 22:44:50 CST 2016]; root of context hierarchy
九月 25, 2016 10:44:50 下午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
信息: Loading XML bean definitions from class path resource [com/test/spring/life/applicationContext.xml]
Person类构造方法
set方法被调用
setBeanName被调用,beanName:person1
setBeanFactory被调用,beanFactory
setApplicationContext被调用
postProcessBeforeInitialization被调用
afterPropertiesSet被调用
myInit被调用
postProcessAfterInitialization被调用
xml加载完毕
name is :jack
关闭容器
九月 25, 2016 10:44:51 下午 org.springframework.context.support.ClassPathXmlApplicationContext doClose
信息: Closing org.springframework.context.support.ClassPathXmlApplicationContext@b4aa453: startup date [Sun Sep 25 22:44:50 CST 2016]; root of context hierarchy
destory被调用
myDestroy被调用

BeanFactory Bean生命周期

bean3

上图修正为“调用disposableBean的destroy()方法”

BeanFactoty容器中, Bean的生命周期如上图所示,与ApplicationContext相比,有如下几点不同:

1.BeanFactory容器中,不会调用ApplicationContextAware接口的setApplicationContext()方法。

2.BeanPostProcessor接口的postProcessBeforeInitialzation()方法和postProcessAfterInitialization()方法不会自动调用,必须自己通过代码手动注册。

3.BeanFactory容器启动的时候,不会去实例化所有Bean,包括所有scope为singleton且非懒加载的Bean也是一样,而是在调用的时候去实例化。

示例:

我们还是使用前面定义好的Person类和MyBeanPostProcessor类,以及ApplicationContext.xml文件。

BfPersonServiceTest测试类的main函数实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class BfPersonServiceTest {

public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println("开始初始化容器");
ConfigurableBeanFactory bf = new XmlBeanFactory(new ClassPathResource("com/pingan/spring/life/applicationContext.xml"));
System.out.println("xml加载完毕");
//beanFactory需要手动注册beanPostProcessor类的方法
bf.addBeanPostProcessor(new MyBeanPostProcessor());
Person person1 = (Person) bf.getBean("person1");
System.out.println(person1);
System.out.println("关闭容器");
bf.destroySingletons();
}

}

启动容器,我们可以看到整个调用流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
开始初始化容器
九月 26, 2016 12:27:05 上午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
信息: Loading XML bean definitions from class path resource [com/pingan/spring/life/applicationContext.xml]
xml加载完毕
PersonService类构造方法
set方法被调用
setBeanName被调用,beanName:person1
setBeanFactory被调用,beanFactory
postProcessBeforeInitialization被调用
afterPropertiesSet被调用
myInit被调用
postProcessAfterInitialization被调用
name is :jack
关闭容器
destory被调用
myDestroy被调用

initialization 和 destroy

有时我们需要在Bean属性值set好之后和Bean销毁之前做一些事情,比如检查Bean中某个属性是否被正常的设置好值了。Spring框架提供了多种方法让我们可以在Spring Bean的生命周期中执行initialization和pre-destroy方法。

  1. 实现InitializingBean和DisposableBean接口

    这两个接口都只包含一个方法。

    通过实现InitializingBean接口的afterPropertiesSet()方法可以在Bean属性值设置好之后做一些操作,实现DisposableBean接口的destroy()方法可以在销毁Bean之前做一些操作。

    例子如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class GiraffeService implements InitializingBean,DisposableBean {
    @Override
    public void afterPropertiesSet() throws Exception {
    System.out.println("执行InitializingBean接口的afterPropertiesSet方法");
    }
    @Override
    public void destroy() throws Exception {
    System.out.println("执行DisposableBean接口的destroy方法");
    }
    }

    注意:这种方法比较简单,但是不建议使用。因为这样会将Bean的实现和Spring框架耦合在一起。

  2. 在bean的配置文件中指定init-method和destroy-method方法

    Spring允许我们创建自己的 init 方法和 destroy 方法,只要在 Bean 的配置文件中指定 init-method 和 destroy-method 的值就可以在 Bean 初始化时和销毁之前执行一些操作。

    例子如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class GiraffeService {
    //通过<bean>的destroy-method属性指定的销毁方法
    public void destroyMethod() throws Exception {
    System.out.println("执行配置的destroy-method");
    }
    //通过<bean>的init-method属性指定的初始化方法
    public void initMethod() throws Exception {
    System.out.println("执行配置的init-method");
    }
    }

    配置文件中的配置:

    1
    2
    <bean name="giraffeService" class="com.giraffe.spring.service.GiraffeService" init-method="initMethod" destroy-method="destroyMethod">
    </bean>

    注意:自定义的init-method和post-method方法可以抛异常但是不能有参数。这种方式比较推荐,因为可以自己创建方法,无需将Bean的实现直接依赖于spring的框架。

  3. 使用@PostConstruct和@PreDestroy注解

    除了xml配置的方式,Spring 也支持用 @PostConstruct和 @PreDestroy注解来指定 init 和 destroy 方法。这两个注解均在javax.annotation 包中。为了注解可以生效,需要在配置文件中定义org.springframework.context.annotation.CommonAnnotationBeanPostProcessor或context:annotation-config

    例子如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class GiraffeService {
    @PostConstruct
    public void initPostConstruct(){
    System.out.println("执行PostConstruct注解标注的方法");
    }
    @PreDestroy
    public void preDestroy(){
    System.out.println("执行preDestroy注解标注的方法");
    }
    }

    配置文件中的配置:

    1
    <bean class="org.springframework.context.annotation.CommonAnnotationBeanPostProcessor" />

实现*Aware接口,在Bean中使用Spring框架的一些对象

有些时候我们需要在 Bean 的初始化中使用 Spring 框架自身的一些对象来执行一些操作。比如获ServletContext 的一些参数,获取 ApplicaitionContext 中的 BeanDefinition 的名字,获取 Bean 在容器中的名字等等。为了让 Bean可以获取到框架自身的一些对象,Spring 提供了一组名为*Aware的接口。

这些接口均继承于org.springframework.beans.factory.Aware标记接口,并提供一个将由 Bean 实现的set*方法,Spring通过基于setter的依赖注入方式使相应的对象可以被Bean使用。

介绍一些重要的Aware接口:

  • ApplicationContextAware: 获得ApplicationContext对象,可以用来获取所有Bean definition的名字。
  • BeanFactoryAware:获得BeanFactory对象,可以用来检测Bean的作用域。
  • BeanNameAware:获得Bean在配置文件中定义的名字。
  • ResourceLoaderAware:获得ResourceLoader对象,可以获得classpath中某个文件。
  • ServletContextAware:在一个MVC应用中可以获取ServletContext对象,可以读取context中的参数。
  • ServletConfigAware: 在一个MVC应用中可以获取ServletConfig对象,可以读取config中的参数。
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
public class GiraffeService implements   ApplicationContextAware,
ApplicationEventPublisherAware, BeanClassLoaderAware, BeanFactoryAware,
BeanNameAware, EnvironmentAware, ImportAware, ResourceLoaderAware{
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
System.out.println("执行setBeanClassLoader,ClassLoader Name = " + classLoader.getClass().getName());
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
System.out.println("执行setBeanFactory,setBeanFactory:: giraffe bean singleton=" + beanFactory.isSingleton("giraffeService"));
}
@Override
public void setBeanName(String s) {
System.out.println("执行setBeanName:: Bean Name defined in context="
+ s);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
System.out.println("执行setApplicationContext:: Bean Definition Names="
+ Arrays.toString(applicationContext.getBeanDefinitionNames()));
}
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
System.out.println("执行setApplicationEventPublisher");
}
@Override
public void setEnvironment(Environment environment) {
System.out.println("执行setEnvironment");
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
Resource resource = resourceLoader.getResource("classpath:spring-beans.xml");
System.out.println("执行setResourceLoader:: Resource File Name="
+ resource.getFilename());
}
@Override
public void setImportMetadata(AnnotationMetadata annotationMetadata) {
System.out.println("执行setImportMetadata");
}
}

BeanPostProcessor

上面的*Aware接口是针对某个实现这些接口的Bean定制初始化的过程, Spring同样可以针对容器中的所有Bean,或者某些Bean定制初始化过程,只需提供一个实现BeanPostProcessor接口的类即可。 该接口中包含两个方法,postProcessBeforeInitialization和postProcessAfterInitialization。 postProcessBeforeInitialization方法会在容器中的Bean初始化之前执行, postProcessAfterInitialization方法在容器中的Bean初始化之后执行。

例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class CustomerBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("执行BeanPostProcessor的postProcessBeforeInitialization方法,beanName=" + beanName);
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("执行BeanPostProcessor的postProcessAfterInitialization方法,beanName=" + beanName);
return bean;
}
}

要将BeanPostProcessor的Bean像其他Bean一样定义在配置文件中:

1
<bean class="com.giraffe.spring.service.CustomerBeanPostProcessor"/>

单例管理的对象和非单例管理的对象

单例管理的对象

当scope=”singleton”,即默认情况下,会在启动容器时(即实例化容器时)时实例化。但我们可以指定Bean节点的lazy-init=”true”来延迟初始化bean,这时候,只有在第一次获取bean时才会初始化bean,即第一次请求该bean时才初始化。如下配置:

1
<bean id="ServiceImpl" class="cn.csdn.service.ServiceImpl" lazy-init="true"/>

如果想对所有的默认单例bean都应用延迟初始化,可以在根节点beans设置default-lazy-init属性为true,如下所示:

1
<beans default-lazy-init="true" …>

默认情况下,Spring 在读取 xml 文件的时候,就会创建对象。在创建对象的时候先调用构造器,然后调用 init-method 属性值中所指定的方法。在对象在被销毁的时候,会调用 destroy-method 属性值中所指定的方法。

非单例管理的对象

当scope=”prototype”时,容器也会延迟初始化 bean,Spring 读取xml 文件的时候,并不会立刻创建对象,而是在第一次请求该 bean 时才初始化(如调用getBean方法时)。在第一次请求每一个 prototype 的bean 时,Spring容器都会调用其构造器创建这个对象,然后调用init-method属性值中所指定的方法。在对象被销毁的时候,Spring 容器不会帮我们调用任何方法,因为是非单例,这个类型的对象有很多个,Spring容器一旦把这个对象交给你之后,就不再管理这个对象了。

总结

可以发现,对于作用域为 prototype 的 bean ,其destroy方法并没有被调用。如果 bean 的 scope 设为prototype时,当容器关闭时,destroy 方法不会被调用。

对于 prototype 作用域的 bean,有一点非常重要,那就是 Spring不能对一个 prototype bean 的整个生命周期负责:容器在初始化、配置、装饰或者是装配完一个prototype实例后,将它交给客户端,随后就对该prototype实例不闻不问了。

不管何种作用域,容器都会调用所有对象的初始化生命周期回调方法。但对prototype而言,任何配置好的析构生命周期回调方法都将不会被调用。清除prototype作用域的对象并释放任何prototype bean所持有的昂贵资源,都是客户端代码的职责(让Spring容器释放被prototype作用域bean占用资源的一种可行方式是,通过使用Bean的后置处理器BeanPostProcessor,该处理器持有要被清除的bean的引用)。

谈及prototype作用域的bean时,在某些方面你可以将Spring容器的角色看作是Java new操作的替代者,任何迟于该时间点的生命周期事宜都得交由客户端来处理。

Spring 容器可以管理 singleton 作用域下 bean 的生命周期,在此作用域下,Spring 能够精确地知道bean何时被创建,何时初始化完成,以及何时被销毁。而对于 prototype 作用域的bean,Spring只负责创建,当容器创建了 bean 的实例后,bean 的实例就交给了客户端的代码管理,Spring容器将不再跟踪其生命周期,并且不会管理那些被配置成prototype作用域的bean的生命周期。

参考:

https://github.com/Snailclimb/JavaGuide/blob/master/%E4%B8%BB%E6%B5%81%E6%A1%86%E6%9E%B6/SpringBean.md

https://www.jianshu.com/p/3944792a5fff

SpringMVC工作原理详解

发表于 2019-02-28 | 分类于 java面试准备 , Spring |

先谈下什么是MVC模式

MVC是一种设计模式,分为Model、View、Controller三层

Spring MVC的简单原理图如下:

mvc1

Spring MVC的简单介绍:

SpringMVC 框架是以请求为驱动,围绕 Servlet 设计,将请求发给控制器,然后通过模型对象,分派器来展示请求结果视图。其中核心类是 DispatcherServlet,它是一个 Servlet,顶层是实现的Servlet接口。

Spring MVC的使用:

在 web.xml 中配置 DispatcherServlet ,并且需要配置 Spring 监听器ContextLoaderListener:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<!-- 如果不设置init-param标签,则必须在/WEB-INF/下创建xxx-servlet.xml文件,其中xxx是servlet-name中配置的名称。 -->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/springmvc-servlet.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

Spring MVC的工作原理:

  • 简单来说:

    1. 客户端(浏览器)发送请求,直接请求到 DispatcherServlet。
    2. DispatcherServlet根据请求信息调用 HandlerMapping,解析请求对应的 Handler。解析到对应的 Handler(也就是我们平常说的 Controller 控制器)后,开始由 HandlerAdapter 适配器处理。
    3. HandlerAdapter会根据 Handler 来调用真正的处理器来处理请求,并处理相应的业务逻辑。
    4. 处理器处理完业务后,会返回一个 ModelAndView 对象,Model 是返回的数据对象,View 是个逻辑上的 View。
    5. ViewResolver 会根据逻辑 View 查找实际的 View。
    6. DispaterServlet 把返回的 Model 传给 View(视图渲染)。
    7. 把 View 返回给请求者(浏览器)
  • 如下图所示:

    mvc2

    上图的一个笔误的小问题:Spring MVC 的入口函数也就是前端控制器 DispatcherServlet 的作用是接收请求,响应结果。

  • 详细流程:(一次请求的过程)
    1、在整个 Spring MVC 框架中,DispatcherServlet 处于核心位置,它负责协调和组织不同组件完成请求处理并返回响应工作。在看 DispatcherServlet 类之前,我们先来看一下请求处理的大致流程:
    Tomcat 启动,对 DispatcherServlet 进行实例化,然后调用它的init() 方法进行初始化,在这个初始化过程中完成了:对 web.xml 中初始化参数的加载;建立WebApplicationContext (SpringMVC的IOC容器);进行组件的初始化;
    2、客户端发出请求,由 Tomcat 接收到这个请求,如果匹配 DispatcherServlet 在 web.xml 中配置的映射路径,Tomcat 就将请求转交给 DispatcherServlet 处理;
    3、DispatcherServlet 从容器中取出所有 HandlerMapping 实例(每个实例对应一个 HandlerMapping 接口的实现类)并遍历,每个 HandlerMapping 会根据请求信息,通过自己实现类中的方式去找到处理该请求的 Handler (执行程序,如Controller中的方法),并且将这个 Handler 与一堆 HandlerInterceptor (拦截器) 封装成一个 HandlerExecutionChain 对象,一旦有一个 HandlerMapping 可以找到 Handler 则退出循环;
    4、DispatcherServlet 取出 HandlerAdapter 组件,根据已经找到的 Handler,再从所有 HandlerAdapter 中找到可以处理该 Handler 的 HandlerAdapter 对象;
    5、执行 HandlerExecutionChain 中所有拦截器的 preHandler() 方法,然后再利用HandlerAdapter 执行 Handler ,执行完成得到 ModelAndView,再依次调用拦截器的 postHandler() 方法;
    6、利用 ViewResolver 将 ModelAndView 或是 Exception(可解析成 ModelAndView)解析成 View,然后 View 会调用 render() 方法再根据 ModelAndView 中的Model(数据)渲染出页面;
    7、最后再依次调用拦截器的 afterCompletion() 方法,这一次请求就结束了。

  • Spring MVC重要组件说明

    1、前端控制器DispatcherServlet(不需要工程师开发),由框架提供(重要)

    作用:Spring MVC 的入口函数。接收请求,响应结果,相当于转发器,中央处理器。有了 DispatcherServlet 减少了其它组件之间的耦合度。用户请求到达前端控制器,它就相当于mvc模式中的c,DispatcherServlet是整个流程控制的中心,由它调用其它组件处理用户的请求,DispatcherServlet的存在降低了组件之间的耦合性。

    2、处理器映射器HandlerMapping(不需要工程师开发),由框架提供

    作用:根据请求的url查找Handler。HandlerMapping负责根据用户请求找到Handler即处理器(Controller),SpringMVC提供了不同的映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等。

    3、处理器适配器HandlerAdapter

    作用:按照特定规则(HandlerAdapter要求的规则)去执行Handler。 通过HandlerAdapter对处理器进行执行,这是适配器模式的应用,通过扩展适配器可以对更多类型的处理器进行执行。

    4、处理器Handler(需要工程师开发)

    注意:编写Handler时按照HandlerAdapter的要求去做,这样适配器才可以去正确执行Handler。Handler 是继DispatcherServlet前端控制器的后端控制器,在DispatcherServlet的控制下Handler对具体的用户请求进行处理。 由于Handler涉及到具体的用户业务请求,所以一般情况需要工程师根据业务需求开发Handler。

    5、视图解析器View resolver(不需要工程师开发),由框架提供

    作用:进行视图解析,根据逻辑视图名解析成真正的视图(view)。 View Resolver负责将处理结果生成View视图,View Resolver首先根据逻辑视图名解析成物理视图名即具体的页面地址,再生成View视图对象,最后对View进行渲染将处理结果通过页面展示给用户。 springmvc框架提供了很多的View视图类型,包括:jstlView、freemarkerView、pdfView等。 一般情况下需要通过页面标签或页面模版技术将模型数据通过页面展示给用户,需要由工程师根据业务需求开发具体的页面。

    6、视图View(需要工程师开发)

    View是一个接口,实现类支持不同的View类型(jsp、freemarker、pdf…)

    注意:处理器Handler(也就是我们平常说的Controller控制器)以及视图层view都是需要我们自己手动开发的。其他的一些组件比如:前端控制器DispatcherServlet、处理器映射器HandlerMapping、处理器适配器HandlerAdapter等等都是框架提供给我们的,不需要自己手动开发。

DispatcherServlet详细解析

  • 首先看下源码:

    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
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    package org.springframework.web.servlet;

    @SuppressWarnings("serial")
    public class DispatcherServlet extends FrameworkServlet {

    public static final String MULTIPART_RESOLVER_BEAN_NAME = "multipartResolver";
    public static final String LOCALE_RESOLVER_BEAN_NAME = "localeResolver";
    public static final String THEME_RESOLVER_BEAN_NAME = "themeResolver";
    public static final String HANDLER_MAPPING_BEAN_NAME = "handlerMapping";
    public static final String HANDLER_ADAPTER_BEAN_NAME = "handlerAdapter";
    public static final String HANDLER_EXCEPTION_RESOLVER_BEAN_NAME = "handlerExceptionResolver";
    public static final String REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME = "viewNameTranslator";
    public static final String VIEW_RESOLVER_BEAN_NAME = "viewResolver";
    public static final String FLASH_MAP_MANAGER_BEAN_NAME = "flashMapManager";
    public static final String WEB_APPLICATION_CONTEXT_ATTRIBUTE = DispatcherServlet.class.getName() + ".CONTEXT";
    public static final String LOCALE_RESOLVER_ATTRIBUTE = DispatcherServlet.class.getName() + ".LOCALE_RESOLVER";
    public static final String THEME_RESOLVER_ATTRIBUTE = DispatcherServlet.class.getName() + ".THEME_RESOLVER";
    public static final String THEME_SOURCE_ATTRIBUTE = DispatcherServlet.class.getName() + ".THEME_SOURCE";
    public static final String INPUT_FLASH_MAP_ATTRIBUTE = DispatcherServlet.class.getName() + ".INPUT_FLASH_MAP";
    public static final String OUTPUT_FLASH_MAP_ATTRIBUTE = DispatcherServlet.class.getName() + ".OUTPUT_FLASH_MAP";
    public static final String FLASH_MAP_MANAGER_ATTRIBUTE = DispatcherServlet.class.getName() + ".FLASH_MAP_MANAGER";
    public static final String EXCEPTION_ATTRIBUTE = DispatcherServlet.class.getName() + ".EXCEPTION";
    public static final String PAGE_NOT_FOUND_LOG_CATEGORY = "org.springframework.web.servlet.PageNotFound";
    private static final String DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties";
    protected static final Log pageNotFoundLogger = LogFactory.getLog(PAGE_NOT_FOUND_LOG_CATEGORY);
    private static final Properties defaultStrategies;
    static {
    try {
    ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);
    defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
    }
    catch (IOException ex) {
    throw new IllegalStateException("Could not load 'DispatcherServlet.properties': " + ex.getMessage());
    }
    }

    /** Detect all HandlerMappings or just expect "handlerMapping" bean? */
    private boolean detectAllHandlerMappings = true;

    /** Detect all HandlerAdapters or just expect "handlerAdapter" bean? */
    private boolean detectAllHandlerAdapters = true;

    /** Detect all HandlerExceptionResolvers or just expect "handlerExceptionResolver" bean? */
    private boolean detectAllHandlerExceptionResolvers = true;

    /** Detect all ViewResolvers or just expect "viewResolver" bean? */
    private boolean detectAllViewResolvers = true;

    /** Throw a NoHandlerFoundException if no Handler was found to process this request? **/
    private boolean throwExceptionIfNoHandlerFound = false;

    /** Perform cleanup of request attributes after include request? */
    private boolean cleanupAfterInclude = true;

    /** MultipartResolver used by this servlet */
    private MultipartResolver multipartResolver;

    /** LocaleResolver used by this servlet */
    private LocaleResolver localeResolver;

    /** ThemeResolver used by this servlet */
    private ThemeResolver themeResolver;

    /** List of HandlerMappings used by this servlet */
    private List<HandlerMapping> handlerMappings;

    /** List of HandlerAdapters used by this servlet */
    private List<HandlerAdapter> handlerAdapters;

    /** List of HandlerExceptionResolvers used by this servlet */
    private List<HandlerExceptionResolver> handlerExceptionResolvers;

    /** RequestToViewNameTranslator used by this servlet */
    private RequestToViewNameTranslator viewNameTranslator;

    private FlashMapManager flashMapManager;

    /** List of ViewResolvers used by this servlet */
    private List<ViewResolver> viewResolvers;

    public DispatcherServlet() {
    super();
    }

    public DispatcherServlet(WebApplicationContext webApplicationContext) {
    super(webApplicationContext);
    }
    @Override
    protected void onRefresh(ApplicationContext context) {
    initStrategies(context);
    }

    protected void initStrategies(ApplicationContext context) {
    initMultipartResolver(context);
    initLocaleResolver(context);
    initThemeResolver(context);
    initHandlerMappings(context);
    initHandlerAdapters(context);
    initHandlerExceptionResolvers(context);
    initRequestToViewNameTranslator(context);
    initViewResolvers(context);
    initFlashMapManager(context);
    }
    }

    DispatcherServlet类中的属性beans:

    • HandlerMapping:用于handlers映射请求和一系列的对于拦截器的前处理和后处理,大部分用@Controller注解。
    • HandlerAdapter:帮助DispatcherServlet处理映射请求处理程序的适配器,而不用考虑实际调用的是哪个处理程序。
    • ViewResolver:根据实际配置解析实际的View类型。
    • ThemeResolver:解决Web应用程序可以使用的主题,例如提供个性化布局。
    • MultipartResolver:解析多部分请求,以支持从HTML表单上传文件。-
    • FlashMapManager:存储并检索可用于将一个请求属性传递到另一个请求的input和output的FlashMap,通常用于重定向。

    在Web MVC框架中,每个DispatcherServlet都拥自己的WebApplicationContext,它继承了ApplicationContext。WebApplicationContext包含了其上下文和Servlet实例之间共享的所有的基础框架beans。

  • HandlerMapping:

    mvc3

    HandlerMapping接口处理请求的映射HandlerMapping接口的实现类:

    • SimpleUrlHandlerMapping类通过配置文件把URL映射到Controller类。
    • DefaultAnnotationHandlerMapping类通过注解把URL映射到Controller类。
  • HandlerAdapter:

    mvc4

    HandlerAdapter接口->处理请求映射

    AnnotationMethodHandlerAdapter:通过注解,把请求URL映射到Controller类的方法上。

  • HandlerExceptionResolver:
    mvc5

    HandlerExceptionResolver接口-异常处理接口

    • SimpleMappingExceptionResolver类通过配置文件进行异常处理。
    • AnnotationMethodHandlerExceptionResolver类通过注解进行异常处理。
  • ViewResolver:

    mvc6

    ViewResolver接口->解析View视图。

    UrlBasedViewResolver类通过配置文件,把一个视图名交给到一个View来处理。

参考:

https://github.com/Snailclimb/JavaGuide/blob/master/%E4%B8%BB%E6%B5%81%E6%A1%86%E6%9E%B6/SpringMVC%20%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86%E8%AF%A6%E8%A7%A3.md

https://blog.csdn.net/jianyuerensheng/article/details/51258942

Mysql常见面试题

发表于 2019-02-24 | 分类于 java面试准备 , Mysql |

数据库索引原理

MyISAM和InnoDB索引的底层实现原理

MyISAM 和 InnoDB 是MySQL的两代搜索引擎,区别在于,对于辅助索引的实现原理不一样,并且MyISAM是索引和文件分离的,而InnoDB不是。

一般以主键为索引的叫做主索引,而以其他键为索引的叫做辅助索引;

MyISAM的实现原理,利用B+树实现:

下面为主键索引:

mysql1

由上图可以看出,col1是主键,而叶子结点存储的数据是一个地址,通过地址找到数据;

下面是辅助索引(和主索引不同的是辅助索引的key是可以重复的) :

mysql2

注意:上图的叶子节点34下面的地址值应为0x07

InnoDB的实现原理,利用B+树实现:

下面是主键索引:

mysql3

这是主索引,即利用主键构造的B+树;

注意,和MyISAM不同的是叶子结点的数据域保存的是全部数据;

下面在看辅助索引:

mysql4

仔细看辅助索引和主索引的区别,辅助索引的叶子结点保存的是主键。

这就是MyISAM和InnoDB最大的不同。

既然MyISAM和InnoDB是MySQL的两代引擎,肯定会有一个提升,而InnoDB是最新一代,那么它到底优在哪里?

试想,MyISAM和InnoDB都是以B+树为基础实现的,相对于B树的不同其实前面已经讲过,即数据域和结点分离。而MyISAM更是索引和文件分离,B+树的叶子结点的数据域存放的是文件内容的地址,主索引和辅助索引的B+树都是如此。那么如果我改变了一个地址,是不是所有的索引树都得改变,正如前面我们讲的在磁盘上频繁的读写操作是效率很低的,而这块又不适用局部原理,因为逻辑上相邻的结点,物理上不一定相邻,那么这样就会造成效率上的降低;

于是乎,InnoDB就产生了,它让除了主索引以外的辅助索引的叶子结点的数据域都保存主键,先通过辅助索引找到主键,然后通过主键找到叶子结点的所有数据,听起来貌似很麻烦,遍历了两棵树,但是,这样如果有了修改的话,改变的只是主索引,其它辅助索引都不用动。而且,数据库中的树的每一个结点的key可不是咱们给的那么少。试想如果一个结点有1024个key,那么高度为2的B+树都有1024*1024个key,所以一般树的高度都很低,所以,遍历树的消耗几乎忽略不计!

另外,我们也就很容易明白为什么不建议使用过长的字段作为主键,因为所有辅助索引都引用主索引,过长的主索引会令辅助索引变得过大。

为什么使用B+Tree作为数据库索引?

红黑树等数据结构也可以用来实现索引,但是文件系统及数据库系统普遍采用B-/+Tree作为索引结构。

一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个数量级,所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘I/O操作次数的渐进复杂度。换句话说,索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数。

这样我们对比上面的B+树和红黑树,比如查找节点21,红黑树要磁盘IO5次,而B+树只要2次,也就是说磁盘IO次数大致为树的高度,这样B+树就脱颖而出了,成为实现索引的不二选择。

实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree的高度一般都在2~4层。MySQL的InnoDB存储引擎在设计时是将根节点常驻内存的,也就是说查找某一键值的行记录时最多只需要1~3次磁盘I/O操作。

数据库中的B+Tree索引可以分为聚集索引(clustered index)和辅助索引(secondary index)。

上面的B+Tree示例图在数据库中的实现即为聚集索引,聚集索引的B+Tree中的叶子节点存放的是整张表的行记录数据。辅助索引与聚集索引的区别在于辅助索引的叶子节点并不包含行记录的全部数据,而是存储相应行数据的聚集索引键,即主键。当通过辅助索引来查询数据时,InnoDB存储引擎会遍历辅助索引找到主键,然后再通过主键在聚集索引中找到完整的行记录数据。(InnoDB的索引实现原理)

聚簇索引(聚集索引):并不是一种单独的索引类型,而是一种数据存储方式。具体细节取决于不同的实现,InnoDB的聚簇索引其实就是在同一个结构中保存了B-Tree索引(技术上来说是B+Tree)和数据行。

数据库索引采用B+树而不是B树或红黑树的主要原因:

  1. 方便扫库:B+树只要遍历叶子节点就可以实现整棵树的遍历,而且在数据库中基于范围的查询是非常频繁的,而B树只能中序遍历所有节点,效率太低。
  2. 文件与数据库都是需要较大的存储,也就是说,它们都不可能全部存储在内存中,故需要存储到磁盘上。而所谓索引,则为了数据的快速定位与查找,那么索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数,因此B+树相比B树更为合适。数据库系统巧妙利用了局部性原理与磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入,而红黑树这种结构,高度明显要深的多,并且由于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性。

为什么数据库选B-tree或B+tree而不是二叉查找树作为索引结构?

  1. 磁盘IO与预读

    磁盘读取依靠的是机械运动,分为寻道时间、旋转延迟、传输时间三个部分,这三个部分耗时相加就是一次磁盘IO的时间,大概9ms左右。这个成本是访问内存的十万倍左右;正是由于磁盘IO是非常昂贵的操作,所以计算机操作系统对此做了优化:预读

    预读:每一次IO时,不仅仅把当前磁盘地址的数据加载到内存,同时也把相邻数据也加载到内存缓冲区中。因为局部预读原理说明,当访问一个地址数据的时候,与其相邻的数据很快也会被访问到。每次磁盘IO读取的数据我们称之为一页(page)。一页的大小与操作系统有关,一般为4k或者8k。这也就意味着读取一页内数据的时候,实际上发生了一次磁盘IO。

  2. B-Tree与二叉查找树的对比

    我们知道二叉查找树查询的时间复杂度是O(logN),查找速度最快和比较次数最少,既然性能已经如此优秀,但为什么实现索引是使用B-Tree而不是二叉查找树,关键因素是磁盘IO的次数。

    数据库索引是存储在磁盘上,当表中的数据量比较大时,索引的大小也跟着增长,达到几个G甚至更多。当我们利用索引进行查询的时候,不可能把索引全部加载到内存中,只能逐一加载每个磁盘页,这里的磁盘页就对应索引树的节点。

    一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个数量级,所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘I/O操作次数的渐进复杂度。换句话说,索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数。而磁盘IO的次数和树的高度有关,因此,减少磁盘IO的次数就必须要压缩树的高度,让瘦高的树尽量变成矮胖的树,所以B-Tree就在这样伟大的时代背景下诞生了。

    索引是存在于索引文件中,是存在于磁盘中的。因为索引通常是很大的,因此无法一次将全部索引加载到内存当中,因此每次只能从磁盘中读取一个磁盘页的数据到内存中。而这个磁盘的读取的速度较内存中的读取速度而言是差了好几个级别。

    注意,我们说的平衡二叉树结构,指的是逻辑结构上的平衡二叉树,其物理实现是数组。然后由于在逻辑结构上相近的节点在物理结构上可能会差很远。因此,每次读取的磁盘页的数据中有许多是用不上的。因此,查找过程中要进行许多次的磁盘读取操作。

    而适合作为索引的结构应该是尽可能少的执行磁盘IO操作,因为执行磁盘IO操作非常的耗时。因此,平衡二叉树并不适合作为索引结构。

为什么说B+树比B树更适合数据库索引?

mysql1

  1. B+树的磁盘读写代价更低:B+树的内部节点并没有指向关键字具体信息的指针,因此其内部节点相对B树更小。如果把所有同一内部节点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多,一次性读入内存的需要查找的关键字也就越多,相对IO读写次数就降低了。

  2. B+树的查询效率更加稳定:由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。

  3. 由于B+树的数据都存储在叶子结点中,分支结点均为索引,方便扫库,只需要扫一遍叶子结点即可,但是B树因为其分支结点同样存储着数据,我们要找到具体的数据,需要进行一次中序遍历按序来扫,所以B+树更加适合在区间查询的情况,所以通常B+树用于数据库索引。

  4. PS:我在知乎上看到有人是这样说的,我感觉说的也挺有道理的:

    他们认为数据库索引采用B+树的主要原因是:B树在提高了IO性能的同时并没有解决元素遍历的我效率低下的问题,正是为了解决这个问题,B+树应用而生。B+树只需要去遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作或者说效率太低。

参考:https://blog.csdn.net/qq_35571554/article/details/82759668

数据库索引类型

索引的优点:

  1. 减少了服务器需要扫描的数据行数。
  2. 帮助服务器避免进行排序和创建临时表(B+Tree 索引是有序的,可以用来做 ORDER BY 和 GROUP BY 操作);
  3. 将随机 I/O 变为顺序 I/O(B+Tree 索引是有序的,也就将相邻的数据都存储在一起)。

首先要明白索引(index)是在存储引擎(storage engine)层面实现的,而不是server层面。不是所有的存储引擎都支持所有的索引类型。

即使多个存储引擎支持某一索引类型,它们的实现和行为也可能有所差别。

MySQL里的索引类型主要有以下几种:

1. B-Tree索引

最常见的索引类型,基于B-Tree数据结构。B-Tree的基本思想是,所有值(被索引的列)都是排过序的,每个叶节点到跟节点距离相等。所以B-Tree适合用来查找某一范围内的数据,而且可以直接支持数据排序(ORDER BY)。但是当索引多列时,列的顺序特别重要,需要格外注意。InnoDB和MyISAM都支持B-Tree索引。InnoDB用的是一个变种B+Tree,而MyISAM为了节省空间对索引进行了压缩,从而牺牲了性能。

2. Hash索引

基于hash表。所以这种索引只支持精确查找,不支持范围查找,不支持排序。这意味着范围查找或ORDER BY都要依赖server层的额外工作。目前只有Memory引擎支持显式的hash索引(但是它的hash是nonunique的,冲突太多时也会影响查找性能)。Memory引擎默认的索引类型即是Hash索引,虽然它也支持B-Tree索引。

例子:

1
2
3
4
5
CREATE TABLE testhash (
fname VARCHAR(50) NOT NULL,
lname VARCHAR(50) NOT NULL,
KEY USING HASH(fname)
) ENGINE =MEMORY;

3. Spatial (R-Tree)(空间)索引

只有MyISAM引擎支持,并且支持的不好。可以忽略。

4. Full-text索引

主要用来查找文本中的关键字,而不是直接与索引中的值相比较。Full-text索引跟其它索引大不相同,它更像是一个搜索引擎,而不是简单的WHERE语句的参数匹配。你可以对某列分别进行full-text索引和B-Tree索引,两者互不冲突。Full-text索引配合MATCH AGAINST操作使用,而不是一般的WHERE语句加LIKE。

从数据结构的角度

  1. B+树索引(O(logn))
  2. 哈希索引
    • 仅仅能满足”=”,”IN”和”<>”查询,不能使用范围查询。
    • 其检索效率非常高,索引的检索可以一次定位,不像B-Tree 索引需要从根节点到枝节点,最后才能访问到页节点这样多次的IO访问,所以 Hash 索引的查询效率要远高于 B-Tree 索引。
    • 只有Memory存储引擎显示支持hash索引。
  3. FULLTEXT索引(现在MyISAM和InnoDB引擎都支持了)
  4. R-Tree索引(用于对GIS数据类型创建SPATIAL索引)

从物理存储角度

  1. 聚集索引(clustered index)
  2. 非聚集索引(non-clustered index)

MyISAM的是非聚簇索引,InnoDB使用的是聚簇索引。

从逻辑角度

  1. 主键索引:主键索引是一种特殊的唯一索引,不允许有空值。

  2. 普通索引或者单列索引。

  3. 多列索引(复合索引):复合索引指多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。注意:使用复合索引时遵循最左前缀。

  4. 唯一索引或者非唯一索引。

  5. 空间索引:空间索引是对空间数据类型的字段建立的索引,MYSQL中的空间数据类型有4种,分别是GEOMETRY、POINT、LINESTRING、POLYGON。
    MYSQL使用SPATIAL关键字进行扩展,使得能够用于创建正规索引类型的语法创建空间索引。创建空间索引的列,必须将其声明为NOT NULL,空间索引只能在存储引擎为MYISAM的表中创建。

    1
    2
    CREATE TABLE table_name[col_name data type]
    [unique|fulltext|spatial][index|key][index_name](col_name[length])[asc|desc]

注意:

1、unique|fulltext|spatial为可选参数,分别表示唯一索引、全文索引和空间索引;

2、index和key为同义词,两者作用相同,用来指定创建索引;

3、col_name为需要创建索引的字段列,该列必须从数据表中该定义的多个列中选择;

4、index_name指定索引的名称,为可选参数,如果不指定,MYSQL默认col_name为索引值;

5、length为可选参数,表示索引的长度,只有字符串类型的字段才能指定索引长度;

6、asc或desc指定升序或降序的索引值存储。

索引的使用场景

  1. 匹配全值(match the full value)

    对索引中所有列都指定具体值,即是对索引中的所有列都有等值匹配的条件。
    例如,租赁表 rental 中通过指定出租日期 rental_date + 库存编号 inventory_id + 客户编号 customer_id 的组合条件进行查询,执行计划的 key和extra两字段的值看到优化器选择了复合索引 idx_rental_date:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    MySQL [sakila]> explain select * from rental where rental_date='2005-05-25 17:22:10' and inventory_id=373 and customer_id=343 \G
    *************************** 1. row ***************************
    id: 1
    select_type: SIMPLE
    table: rental
    partitions: NULL
    type: const
    possible_keys: rental_date,idx_fk_inventory_id,idx_fk_customer_id
    key: rental_date
    key_len: 10
    ref: const,const,const
    rows: 1
    filtered: 100.00
    Extra: NULL
    1 row in set, 1 warning (0.00 sec)

    explain 输出结果中字段 type 的值为 const,表示是常量;字段 key 的值为 rental_date, 表示优化器选择索引 rental_date 进行扫描。

  2. 匹配值的范围查询(match a range of values)

    对索引的值能够进行范围查找。
    例如,检索租赁表 rental 中客户编号 customer_id 在指定范围内的记录:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    MySQL [sakila]> explain select * from rental where customer_id >= 373 and customer_id < 400 \G
    *************************** 1. row ***************************
    id: 1
    select_type: SIMPLE
    table: rental
    partitions: NULL
    type: range
    possible_keys: idx_fk_customer_id
    key: idx_fk_customer_id
    key_len: 2
    ref: NULL
    rows: 718
    filtered: 100.00
    Extra: Using index condition
    1 row in set, 1 warning (0.05 sec)

    类型 type 为 range 说明优化器选择范围查询,索引 key 为 idx_fk_customer_id 说明优化器选择索引 idx_fk_customer_id 来加速访问,注意到这个列子中 extra 列为 using index codition ,表示 mysql 使用了 ICP(using index condition) 来进一步优化查询。

  3. 匹配最左前缀(match a leftmost prefix)

    最左匹配原则可以算是 MySQL 中 B-Tree 索引使用的首要原则。

  4. 仅仅对索引进行查询(index only query),也叫索引覆盖

    当查询的列都在索引的字段中时,查询的效率更高。

    所以应该尽量避免使用 select *,需要哪些字段,就只查哪些字段。

  5. 匹配列前缀(match a column prefix)

    仅仅使用索引中的第一列,并且只包含索引第一列的开头一部分进行查找。
    例如,现在需要查询出标题 title 是以 AFRICAN 开头的电影信息,从执行计划能够清楚看到,idx_title_desc_part 索引被利用上了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    MySQL [sakila]> create index idx_title_desc_part on film_text(title (10), description(20));
    Query OK, 0 rows affected (0.07 sec)
    Records: 0 Duplicates: 0 Warnings: 0

    MySQL [sakila]> explain select title from film_text where title like 'AFRICAN%'\G
    *************************** 1. row ***************************
    id: 1
    select_type: SIMPLE
    table: film_text
    partitions: NULL
    type: range
    possible_keys: idx_title_desc_part,idx_title_description
    key: idx_title_desc_part
    key_len: 32
    ref: NULL
    rows: 1
    filtered: 100.00
    Extra: Using where
    1 row in set, 1 warning (0.00 sec)

    extra 值为 using where 表示优化器需要通过索引回表查询数据。

  6. 能够实现索引匹配部分精确而其他部分进行范围匹配(match one part exactly and match a range on another part)

    例如,需要查询出租日期 rental_date 为指定日期且客户编号 customer_id 为指定范围的库存:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    MySQL [sakila]> MySQL [sakila]> explain select inventory_id from rental where rental_date='2006-02-14 15:16:03' and customer_id >= 300 and customer_id <=400\G
    *************************** 1. row ***************************
    id: 1
    select_type: SIMPLE
    table: rental
    partitions: NULL
    type: ref
    possible_keys: rental_date,idx_fk_customer_id
    key: rental_date
    key_len: 5
    ref: const
    rows: 182
    filtered: 16.85
    Extra: Using where; Using index
    1 row in set, 1 warning (0.00 sec)
  7. 如果列名是索引,那么使用 column_name is null 就会使用索引

    例如,查询支付表 payment 的租赁编号 rental_id 字段为空的记录就用到了索引:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    MySQL [sakila]> explain select * from payment where rental_id is null \G
    *************************** 1. row ***************************
    id: 1
    select_type: SIMPLE
    table: payment
    partitions: NULL
    type: ref
    possible_keys: fk_payment_rental
    key: fk_payment_rental
    key_len: 5
    ref: const
    rows: 5
    filtered: 100.00
    Extra: Using index condition
    1 row in set, 1 warning (0.00 sec)

索引失效的情况

  1. 用 or 分割开的条件,如果 or 前的条件中的列有索引,而后面的列中没有索引,那么涉及的索引都不会被用到。
  2. 复合索引的情况下,假如查询条件不包含索引列最左边部分,即不满足最左前缀原则 ,是不会使用复合索引的。
  3. like查询是以%开头。
  4. 如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引
  5. 如果mysql估计使用全表扫描要比使用索引快,则不使用索引

最左前缀匹配原则

  • 定义:就是最左优先,在创建多列索引时,要根据业务需求,where子句中使用最频繁的一列放在最左边

  • 生效原则:组合索引的生效原则:从前往后依次使用生效,如果中间某个索引没有使用,那么断点前面的索引部分起作用,断点后面的索引没有起作用。
    比如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    where a=3 and b=45 and c=5 .... 这种三个索引顺序使用中间没有断点,全部发挥作用;
    where a=3 and c=5... 这种情况下b就是断点,a发挥了效果,c没有效果
    where b=3 and c=4... 这种情况下a就是断点,在a后面的索引都没有发挥作用,这种写法联合索引没有发挥任何效果;
    where b=45 and a=3 and c=5 .... 这个跟第一个一样,全部发挥作用,abc只要用上了就行,跟写的顺序无关

    (0) select * from mytable where a=3 and b=5 and c=4;
    abc三个索引都在where条件里面用到了,而且都发挥了作用
    (1) select * from mytable where c=4 and b=6 and a=3;
    这条语句列出来只想说明 mysql没有那么笨,where里面的条件顺序在查询之前会被mysql自动优化,效果跟上一句一样
    (2) select * from mytable where a=3 and c=7;
    a用到索引,b没有用,所以c是没有用到索引效果的
    (3) select * from mytable where a=3 and b>7 and c=3;
    a用到了,b也用到了,c没有用到,这个地方b是范围值,也算断点,只不过自身用到了索引
    (4) select * from mytable where b=3 and c=4;
    因为a索引没有使用,所以这里 bc都没有用上索引效果
    (5) select * from mytable where a>4 and b=7 and c=9;
    a用到了 b没有使用,c没有使用
    (6) select * from mytable where a=3 order by b;
    a用到了索引,b在结果排序中也用到了索引的效果,前面说了,a下面任意一段的b是排好序的
    (7) select * from mytable where a=3 order by c;
    a用到了索引,但是这个地方c没有发挥排序效果,因为中间断点了,使用 explain 可以看到 filesort
    (8) select * from mytable where b=3 order by a;
    b没有用到索引,排序中a也没有发挥索引效果

MyISAM和InnoDB的区别

mysql3

MyISAM简介

MyISAM是MySQL的默认数据库引擎(5.5版之前),由早期的 ISAM (Indexed Sequential Access Method:有索引的顺序访问方法)所改良。虽然性能极佳,而且提供了大量的特性,包括全文索引、压缩、空间函数等,但MyISAM不支持事务和行级锁,而且最大的缺陷就是崩溃后无法安全恢复。不过,5.5版本之后,MySQL引入了InnoDB(另一种数据库引擎)。

下面这张图只是想表达的意思是现在大多数时候我们使用的都是InnoDB存储引擎,但是在某些情况下使用MyISAM更好,比如:MyISAM更适合读密集的表,而InnoDB更适合写密集的的表。 在数据库做主从分离的情况下,经常选择MyISAM作为主库的存储引擎。

mysql5

MyISAM特点

  • 不支持行锁(MyISAM只有表锁),读取时对需要读到的所有表加锁,写入时则对表加排他锁;
  • 不支持事务
  • 不支持外键
  • 不支持崩溃后的安全恢复

  • 在表有读取查询的同时,支持往表中插入新纪录

  • 支持BLOB和TEXT的前500个字符索引,支持全文索引

  • 支持延迟更新索引,极大地提升了写入性能

  • 对于不会进行修改的表,支持压缩表 ,极大地减少了磁盘空间的占用

InnoDB的简介

InnoDB是MySQL的默认数据库引擎(5.5版之后),2006年五月时由甲骨文公司并购。与传统的ISAM与

MyISAM相比,InnoDB的最大特色就是支持了ACID兼容的事务(Transaction)功能。

InnoDB的特点

  • 支持行锁,采用MVCC来支持高并发,有可能死锁
  • 支持事务
  • 支持外键
  • 支持崩溃后的安全恢复
  • 不支持全文索引
  • 读写阻塞与事务隔离级别相关
  • 具有非常高效的缓存特性,能缓存索引,也能缓存数据
  • 支持分区、表空间,类似Oracle数据库

关于二者的对比与总结

3.1 二者的常见对比

1) count运算上的区别: 因为MyISAM缓存有表meta-data(行数等),因此在做COUNT(*)时对于一个结构很好的查询是不需要消耗多少资源的。而对于InnoDB来说,则没有这种缓存。

2) 是否支持事务和崩溃后的安全恢复: MyISAM强调的是性能,每次查询具有原子性,其执行数度比InnoDB类型更快,但是不提供事务支持。InnoDB提供事务支持事务,外部键等高级数据库功能,具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。

3)是否支持外键: MyISAM不支持,而InnoDB支持。

3.2 总结

MyISAM更适合读密集的表,而InnoDB更适合写密集的的表。 在数据库做主从分离的情况下,经常选择MyISAM作为主库的存储引擎。

一般来说,如果需要事务支持,并且有较高的并发读取频率(MyISAM的表锁的粒度太大,所以当该表写并发量较高时,要等待的查询就会很多了),InnoDB是不错的选择。如果你的数据量很大(MyISAM支持压缩特性可以减少磁盘的空间占用),而且不需要支持事务时,MyISAM是最好的选择。

参考:https://juejin.im/post/5b1685bef265da6e5c3c1c34

字符集及校对规则

字符集指的是一种从二进制编码到某类字符符号的映射。

校对规则则是指某种字符集下的排序规则。

Mysql中每一种字符集都会对应一系列的校对规则。

Mysql采用的是类似继承的方式指定字符集的默认值,每个数据库以及每张数据表都有自己的默认值,他们逐层继承。比如:某个库中所有表的默认字符集将是该数据库所指定的字符集(这些表在没有指定字符集的情况下,才会采用默认字符集)

PS:整理自《Java工程师修炼之道》

查询缓存的使用

my.cnf加入以下配置,重启Mysql开启查询缓存

1
2
query_cache_type=1
query_cache_size=600000

Mysql执行以下命令也可以开启查询缓存

1
2
set global  query_cache_type=1;
set global query_cache_size=600000;

如上,开启查询缓存后在同样的查询条件以及数据情况下,会直接在缓存中返回结果。这里的查询条件包括查询本身、当前要查询的数据库、客户端协议版本号等一些可能影响结果的信息。因此任何两个查询在任何字符上的不同都会导致缓存不命中。此外,如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、Mysql库中的系统表,其查询结果也不会被缓存。

缓存建立之后,Mysql的查询缓存系统会跟踪查询中涉及的每张表,如果这些表(数据或结构)发生变化,那么和这张表相关的所有缓存数据都将失效。

缓存虽然能够提升数据库的查询性能,但是缓存同时也带来了额外的开销,每次查询后都要做一次缓存操作,失效后还要销毁。 因此,开启缓存查询要谨慎,尤其对于写密集的应用来说更是如此。如果开启,要注意合理控制缓存空间大小,一般来说其大小设置为几十MB比较合适。此外,还可以通过sql_cache和sql_no_cache来控制某个查询语句是否需要缓存:

1
select sql_no_cache count(*) from usr;

数据库并发控制

Mysql的大多数事务型存储引擎实现都不是简单的行级锁,基于并发性能考虑,一般都实现了MVCC多版本并发控制。

Mysql的并发控制是为了实现事务的隔离性,实现隔离性就要解决脏读、不可重复读、幻读问题。

Mysql的并发控制主要有两种方式,一种是多版本的并发控制(MVCC),一种是基于锁的并发控制。

最为常见的三种并发控制机制:

  • 悲观并发控制,悲观并发控制其实是最常见的并发控制机制,也就是锁;
  • 乐观并发控制,乐观并发控制其实也有另一个名字:乐观锁,乐观锁其实并不是一种真实存在的锁;
  • 多版本并发控制,与前两者对立的命名不同,MVCC 可以与前两者中的任意一种机制结合使用,以提高数据库的读性能。

并发控制要解决的问题:未提交的修改数据

  • 脏读:一个事务读到了另一个事务尚未提交的数据。
  • 不可重复读:同一个事务中两次读取的数据发生改变,这种改变是由另一个事务修改了对应记录引起的。
  • 幻读:同一事务多次查询进行的时候,由于其他插入或删除操作的事务提交,导致每次返回不同的结果集(查到的数据增多或者减少)

小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

mysql2

MVCC多版本并发控制

MVCC是通过保存数据在某个时间点的快照来实现的。不管事务执行多长时间,事务看到的数据都是一致的。

MVCC最大的优点就是:读不加锁,读写不冲突。

MVCC的实现原理

为了实现MVCC,InnoDB对每一行都加上了两个隐藏的列,其中一列存储行被修改的时系统的修改版本号,另外一列存储行被删除时的系统的删除版本号。每当一个事务开始的时候,InnoDB都会给这个事务分配一个递增的版本号,事务开始时的系统版本号会作为事务的版本号。

mysql4

实际上,这个描述是很不严格的,问题有以下几点:

  1. 每条记录含有的隐藏列不是两个而是三个

    它们分别是:

    DB_TRX_ID, 6byte, 创建这条记录/最后一次更新这条记录的事务ID

    DB_ROLL_PTR, 7byte,回滚指针,指向这条记录的上一个版本(存储于rollback segment里)

    DB_ROW_ID, 6byte,隐含的自增ID,如果数据表没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引

    另外,每条记录的头信息(record header)里都有一个专门的bit(deleted_flag)来表示当前记录是否已经被删除

  2. 记录的历史版本是放在专门的rollback segment里(undo log)

    UPDATE非主键语句的效果是:

    老记录被复制到rollback segment中形成undo log,DB_TRX_ID和DB_ROLL_PTR不动

    新记录的DB_TRX_ID = 当前事务ID,DB_ROLL_PTR指向老记录形成的undo log

    这样就能通过DB_ROLL_PTR找到这条记录的历史版本。如果对同一行记录执行连续的update操作,新记录与undo log会组成一个链表,遍历这个链表可以看到这条记录的变迁)

  3. Mysql的一致性读,是通过一个叫做read view的结构来实现的,下面介绍相关概念和可见性比较算法:

    read_view中维护了系统中活跃事务集合的快照,这些活跃事务ID的最小值为up_limit_id,最大值为low_limit_id。

    设要读取的行的最后提交事务id(即当前数据行的稳定事务id)为 trx_id_current,
    当前新开事务id为 new_id,当前新开事务创建的快照read view 中最早的事务id为up_limit_id, 最迟的事务id为low_limit_id(注意这个low_limit_id=未开启的事务id=当前最大事务id+1)

    • 1.trx_id_current < up_limit_id, 这种情况比较好理解, 表示新事务在读取该行记录时, 该行记录的稳定事务ID是小于系统当前所有活跃的事务, 所以当前行稳定数据对新事务可见, 跳到步骤5.
    • 2.trx_id_current >= trx_id_last, 这种情况也比较好理解, 表示该行记录的稳定事务ID是在本次新事务创建之后才开启的, 但是却在本次新事务执行第二个select前就commit了,所以该行记录的当前值不可见, 跳到步骤4。
    • 3.trx_id_current <= trx_id_current <= trx_id_last, 表示该行记录所在事务在本次新事务创建的时候处于活动状态,从up_limit_id到low_limit_id进行遍历,如果trx_id_current等于他们之中的某个事务id的话,那么不可见,调到步骤4,否则表示可见,调到步骤5。
    • 4.从该行记录的 DB_ROLL_PTR 指针所指向的回滚段中取出最新的undo-log的版本号, 将它赋值该 trx_id_current,然后跳到步骤1重新开始判断。
    • 5.将该可见行的值返回。
  4. 辅助索引实现MVCC

    刚才讲的内容是基于聚簇索引的,只有聚簇索引中含有DB_TRX_ID与DB_ROLL_PTR隐藏列,可以比较容易的实现MVCC,但是辅助中并不含有这几个隐藏列,只含有1个bit的deleted flag。

  5. purge线程

    从前面的分析可以看出,为了实现InnoDB的MVCC机制,更新或者删除操作都只是设置一下老记录的deleted_bit,并不真正将过时的记录删除。

    为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bit为true的记录。

    为了不影响MVCC的正常工作,purge线程自己也维护了一个read view(这个read view相当于系统中最老活跃事务的read view)

    如果某个记录的deleted_bit为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。

补充概念

多版本控制: 指的是一种提高并发的技术。最早的数据库系统,只有读读之间可以并发,读写,写读,写写都要阻塞。引入多版本之后,只有写写之间相互阻塞,其他三种操作都可以并行,这样大幅度提高了InnoDB的并发度。在内部实现中,与Postgres在数据行上实现多版本不同,InnoDB是在undolog中实现的,通过undolog可以找回数据的历史版本。找回的数据历史版本可以提供给用户读(按照隔离级别的定义,有些读请求只能看到比较老的数据版本),也可以在回滚的时候覆盖数据页上的数据。在InnoDB内部中,会记录一个全局的活跃读写事务数组,其主要用来判断事务的可见性(up_limit_id和low_limit_id)。

read view:主要是用来做可见性判断的, 比较普遍的解释便是”本事务不可见的当前其他活跃事务”。

对于read view快照的生成时机, 也非常关键, 正是因为生成时机的不同, 造成了RC,RR两种隔离级别的不同可见性;

  • 在innodb中(默认repeatable read级别), 事务在begin/start transaction之后的第一条select读操作后, 会创建一个快照(read view), 将当前系统中活跃的其他事务记录记录起来;
  • 在innodb中(默认repeatable committed级别), 事务中每条select语句都会创建一个快照(read view);

undo_log

  • Undo log是InnoDB MVCC事务特性的重要组成部分。当我们对记录做了变更操作时就会产生undo记录,Undo记录默认被记录到系统表空间(ibdata)中,但从5.6开始,也可以使用独立的Undo 表空间。
  • Undo记录中存储的是老版本数据,当一个旧的事务需要读取数据时,为了能读取到老版本的数据,需要顺着undo链找到满足其可见性的记录。当版本链很长时,通常可以认为这是个比较耗时的操作(例如bug#69812)。
  • 大多数对数据的变更操作包括INSERT/DELETE/UPDATE,其中INSERT操作在事务提交前只对当前事务可见,因此产生的Undo日志可以在事务提交后直接删除(谁会对刚插入的数据有可见性需求呢!!),而对于UPDATE/DELETE则需要维护多版本信息,在InnoDB里,UPDATE和DELETE操作产生的Undo日志被归成一类,即update_undo
  • 另外, 在回滚段中的undo logs分为: insert undo log 和 update undo log
    • insert undo log : 事务对insert新记录时产生的undolog, 只在事务回滚时需要, 并且在事务提交后就可以立即丢弃。
    • update undo log : 事务对记录进行delete和update操作时产生的undo log, 不仅在事务回滚时需要, 一致性读也需要,所以不能随便删除,只有当数据库所使用的快照中不涉及该日志记录,对应的回滚日志才会被purge线程删除。

读操作分成两类:快照读和当前读。

  • 快照读:简单的select操作属于快照读,不加锁。

    1
    select * from table where ?
  • 当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。

    1
    2
    3
    4
    select * from table where ? lock in share mode ;
    select * from table where ? for update ;
    update table set ? where ? ;
    delete from table where ? ;

乐观并发控制

一般采用以下方式:使用版本号(version)机制来实现,版本号就是为数据添加一个版本标志,一般在表中添加一个version字段;当读取数据的时候把version也取出来,然后version+1,更新数据库的时候对比第一次取出来的version和数据库里面的version是否一致,如果一致则更新成功,否则失败进入重试,具体使用大致如下:

1
2
3
4
5
begin;
select id,name,version from test_lock where id=1;
....
update test_lock set name='xxx',version=version+1 where id=1 and version=${version};
commit;

先查询后更新,需要保证原子性,要么使用悲观锁的方式,对整个事务加锁;要么使用乐观锁的方式,如果在读多写少的系统中,乐观锁性能更好;

事务机制

关系型数据库需要遵循ACID原则,具体内容如下:

mysql6

  1. 原子性: 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
  2. 一致性: 执行事务前后,数据库从一个一致性状态转换到另一个一致性状态。
  3. 隔离性: 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
  4. 持久性: 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

为了达到上述事务特性,数据库定义了几种不同的事务隔离级别:

  • READ_UNCOMMITTED(读未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读

  • READ_COMMITTED(读已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生

  • REPEATABLE_READ(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。

  • SERIALIZABLE(串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。

    Mysql 默认采用的 REPEATABLE_READ隔离级别 Oracle 默认采用的 READ_COMMITTED隔离级别。

锁机制与InnoDB锁算法

MyISAM和InnoDB存储引擎使用的锁:

  • MyISAM采用表级锁(table-level locking)
  • InnoDB支持行级锁(row-level locking)和表级锁,默认为行级锁

按照锁的粒度把数据库锁分为表级锁和行级锁

  • 表级锁: Mysql中锁定 粒度最大 的一种锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。其锁定粒度最大,触发锁冲突的概率最高,并发度最低,MyISAM和 InnoDB引擎都支持表级锁。
  • 行级锁: Mysql中锁定 粒度最小 的一种锁,只针对当前操作的行进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。InnoDB支持的行级锁,包括如下几种:
    • Record Lock: 对索引项加锁,锁定符合条件的行。其他事务不能修改和删除加锁项;
    • Gap Lock: 对索引项之间的“间隙”加锁,锁定记录的范围(对第一条记录前的间隙或最后一条将记录后的间隙加锁),不包含索引项本身。其他事务不能在锁范围内插入数据,这样就防止了别的事务新增幻影行。
    • Next-key Lock: 锁定索引项本身和索引范围。即Record Lock和Gap Lock的结合,可解决幻读问题。

虽然使用行级索具有粒度小、并发度高等特点,但是表级锁有时候也是非常必要的:

  • 事务更新大表中的大部分数据直接使用表级锁效率更高;
  • 事务比较复杂,使用行级索很可能引起死锁导致回滚。

补充:

页级锁: MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。页级进行了折衷,一次锁定相邻的一组记录。BDB支持页级锁。开销和加锁时间界于表锁和行锁之间,会出现死锁。锁定粒度界于表锁和行锁之间,并发度一般。

按照是否可写进一步划分为共享锁(S锁)和排他锁(X锁)

表级锁和行级锁可以进一步划分为共享锁(S)和排他锁(X)

  • 共享锁(S)

    共享锁(Share Locks,简记为S)又被称为读锁,其他用户可以并发读取数据,但任何事务都不能获取数据上的排他锁,直到已释放所有共享锁。

    共享锁(S锁)又称为读锁,若事务T对数据对象A加上S锁,则事务T只能读A;其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这就保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。

  • 排他锁(X):

    排它锁((Exclusive lock,简记为X锁))又称为写锁,若事务T对数据对象A加上X锁,则只允许T读取和修改A,其它任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。它防止任何其它事务获取资源上的锁,直到在事务的末尾将资源上的原始锁释放为止。在更新操作(INSERT、UPDATE 或 DELETE)过程中始终应用排它锁。

两者之间的区别:

  1. 共享锁(S锁):如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,而不能加排他锁。获取共享锁的事务只能读数据,不能修改数据。

    排他锁(X锁):如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获取排他锁的事务既能读数据,又能修改数据。

  2. 共享锁下其它用户可以并发读取,查询数据,但不能修改,增加,删除数据,即资源共享。

另外两种表级锁:IS和IX

当一个事务需要给自己需要的某个资源加锁的时候,如果遇到一个共享锁正锁定着自己需要的资源的时候,自己可以再加一个共享锁,不过不能加排他锁。但是,如果遇到自己需要锁定的资源已经被一个排他锁占有之后,则只能等待该锁定释放资源之后自己才能获取锁定资源并添加自己的锁定。而意向锁的作用就是当一个事务在需要获取的资源锁定的时候,该事务可以需要锁定行的表上面添加一个合适的意向锁。如果自己需要一个共享锁,那么就在表上面添加一个意向共享锁。而如果自己需要的是某行(或者某些行)上面添加一个排他锁的话,则先在表上面添加一个意向排他锁。意向共享锁可以同时并存多个,但是意向排他锁同时只能有一个存在。

InnoDB另外的两个表级锁:

  • 意向共享锁(IS): 表示事务准备给数据行记入共享锁,事务在一个数据行加共享锁前必须先取得该表的IS锁。
  • 意向排他锁(IX): 表示事务准备给数据行加入排他锁,事务在一个数据行加排他锁前必须先取得该表的IX锁。

注意:

  1. 这里的意向锁是表级锁,表示的是一种意向,仅仅表示事务正在读或写某一行记录,在真正加行锁时才会判断是否冲突。意向锁是InnoDB自动加的,不需要用户干预。
  2. IX,IS是表级锁,不会和行级的X,S锁发生冲突,只会和表级的X,S发生冲突。

InnoDB的锁机制兼容情况如下:

mysql5

当一个事务请求的锁模式与当前的锁兼容,InnoDB就将请求的锁授予该事务;反之如果请求不兼容,则该事物就等待锁释放。

死锁和避免死锁

InnoDB的行级锁是基于索引实现的,如果查询语句为命中任何索引,那么InnoDB会使用表级锁。 此外,InnoDB的行级锁是针对索引加的锁,不针对数据记录,因此即使访问不同行的记录,如果使用了相同的索引键仍然会出现锁冲突。

另外,还需要注意,在通过以下情况使用锁的时候,如果表没有定义任何索引,那么InnoDB会创建一个隐藏的聚簇索引并使用这个索引来加记录锁。

1
2
SELECT ...LOCK IN SHARE MODE;
SELECT ...FOR UPDATE;

死锁产生:不同于MyISAM总是一次性获得所需的全部锁,InnoDB的锁是逐步获得的,当两个事务都需要获得对方持有的锁,导致双方都在等待,这就产生了死锁。

发生死锁后,InnoDB一般都可以检测到,并使一个事务释放锁回退,另一个则可以获取锁完成事务。

避免死锁:

  • 通过表级锁来减少死锁产生的概率;
  • 多个程序尽量约定以相同的顺序访问表(这也是解决并发理论中哲学家就餐问题的一种思路);
  • 同一个事务尽可能做到一次锁定所需要的所有资源。

大表优化

当MySQL单表记录数过大时,数据库的CRUD性能会明显下降,一些常见的优化措施如下:

  1. 限定数据的范围: 务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时候,我们可以控制在一个月的范围内。;

  2. 读/写分离: 经典的数据库拆分方案,主库负责写,从库负责读;

  3. 垂直分区:

    根据数据库里面数据表的相关性进行拆分。 例如,用户表中既有用户的登录信息又有用户的基本信息,可以将用户表拆分成两个单独的表,甚至放到单独的库做分库。

    简单来说垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表。 如下图所示,这样来说大家应该就更容易理解了。

    mysql7

    垂直拆分的优点: 可以使得行数据变小,在查询时减少读取的Block数,减少I/O次数。此外,垂直分区可以简化表的结构,易于维护。

    垂直拆分的缺点: 主键会出现冗余,需要管理冗余列,并会引起join操作,可以通过在应用层进行join来解决。此外,垂直分区会让事务变得更加复杂;

  4. 水平分区:

    保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达到了分布式的目的。 水平拆分可以支撑非常大的数据量。

    水平拆分是指数据表行的拆分,表的行数超过200万行时,就会变慢,这时可以把一张的表的数据拆成多张表来存放。举个例子:我们可以将用户信息表拆分成多个用户信息表,这样就可以避免单一表数据量过大对性能造成影响。

    mysql8

    水平拆分可以支持非常大的数据量。需要注意的一点是:分表仅仅是解决了单一表数据过大的问题,但由于表的数据还是在同一台机器上,其实对于提升MySQL并发能力没有什么意义,所以 水平拆分最好分库 。

    水平拆分能够 支持非常大的数据量存储,应用端改造也少,但 分片事务难以解决 ,跨界点Join性能较差,逻辑复杂。《Java工程师修炼之道》的作者推荐 尽量不要对数据进行分片,因为拆分会带来逻辑、部署、运维的各种复杂度 ,一般的数据表在优化得当的情况下支撑千万以下的数据量是没有太大问题的。如果实在要分片,尽量选择客户端分片架构,这样可以减少一次和中间件的网络I/O。

    下面补充一下数据库分片的两种常见方案:

    • 客户端代理: 分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。 当当网的 Sharding-JDBC 、阿里的TDDL是两种比较常用的实现。
    • 中间件代理: 在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。 我们现在谈的 Mycat 、360的Atlas、网易的DDB等等都是这种架构的实现。

计算机网络常见面试题

发表于 2019-02-23 | 分类于 java面试准备 , 计算机网络 |

常见状态码及原因短语

  • HTTP请求结构: 请求方式 + 请求URI + 协议及其版本
  • HTTP响应结构: 状态码 + 原因短语 + 协议及其版本
  • 1×× : 请求处理中,请求已被接受,正在处理
  • 2×× : 请求成功,请求被成功处理
    200 OK
  • 3×× :重定向,要完成请求必须进行进一步处理
    301 :永久性转移
    302 :暂时性转移
    304 :所请求的资源未修改,服务器返回此状态码时,不会返回任何资源
  • 4××:客户端错误,请求不合法
    400:Bad Request,请求有语法问题
    401:请求要求用户的身份认证
    403:服务器理解请求客户端的请求,但是拒绝执行此请求
    404:客户端所访问的页面不存在
  • 5××:服务器端错误,服务器不能处理合法请求
    500:服务器内部错误
    502:充当网关或代理的服务器,从远端服务器接收到了一个无效的请求
    503:服务器暂时的无法处理客户端的请求

计算机网络中各层传递的数据形式

  • 物理层:比特流

  • 数据链路层:帧

  • 网络层:数据报、分组

  • 传输层:数据段

  • 应用层:数据、消息

    总结:

    在每一层都有一个信息传输单元,叫信息分组(即上面提到的分组一般性的意思),每层具体的PDU叫法是由最初始的信息分组在各层经过封装后形成的。进入应用层加上应用层的报头后成为最初始的信息分组,叫信息,由传输层提供报文交付(下层为上层服务),应用层报文传到传输层。进入传输层,报文被分割固定长度,加上传输层报头,成为数据段。数据段向下传到网络层,加上网络层报头,包含路由信息,成为分组或数据报,网络层实现分组交付。继续向下传输,进入数据链路层,加上数据链路层头部和尾部,形成帧,将源自网络层来的数据可靠地传输到相邻节点的目标机网络层。进入物理层,将数据转换成物理链路中模拟量即bit进行传输。到达接收端是一个相反的过程。

TCP三次握手和四次挥手

三次握手

jw3

jw4

三次握手

  • 客户端–发送带有 SYN 标志的数据包–一次握手–服务端
  • 服务端–发送带有 SYN/ACK 标志的数据包–二次握手–客户端
  • 客户端–发送带有 ACK 标志的数据包–三次握手–服务端

四次挥手

jw5

四次挥手

断开一个 TCP 连接则需要“四次挥手”:

  • 客户端-发送一个 FIN,用来关闭客户端到服务器的数据传送。
  • 服务器-收到这个 FIN,它发回一 个 ACK,确认序号为收到的序号加1 。和 SYN 一样,一个 FIN 将占用一个序号。
  • 服务器-关闭与客户端的连接,发送一个FIN给客户端。
  • 客户端-发回 ACK 报文确认,并将确认序号设置为收到序号加1。

为什么TCP需要三次握手,四次挥手?

为什么需要三次握手?

“三次握手”的目的是“为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误”。

client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后

的某个时间才到达server。本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为

是client再次发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。假设不采用“三次握手”,

那么只要server发出确认,新的连接就建立了。由于现在client并没有发出建立连接的请求,因此不会理睬server

的确认,也不会向server发送数据。但server却以为新的运输连接已经建立,并一直等待client发来数据。这样,

server的很多资源就白白浪费掉了。采用“三次握手”的办法可以防止上述现象发生。例如刚才那种情况,client不

会向server的确认发出确认。server由于收不到确认,就知道client并没有要求建立连接。主要目的防止server端一直等待,浪费资源。

为什么需要四次挥手?

因为TCP有个半关闭状态,假设A.B要释放连接,那么A发送一个释放连接报文给B,B收到后发送确认,这个时候A不发数据,但是B如果发数据A还是要接受,这叫半关闭。然后B还要发给A连接释放报文,然后A发确认,所以是4次。

为什么在TCP连接握手时为何ACK是和SYN一起发送,这里ACK却没有和FIN一起发送呢?

因为tcp是全双工模式,接收到FIN时意味将没有数据再发来,但是还是可以继续发送数据。

为什么四次挥手最后需要2MSL的TIME-WAIT时间,才进入CLOSED状态?

第一、为了保证发送方发送的最后一个ACK报文段能够到达接收方。这个ACK报文段有可能丢失,因而使处在LAST-ACK状态的接收方收不到对已发送的FIN+ACK报文段的确认。接收方会超时重传这个FIN+ACK报文段,而A就能在2MSL时间内收到这个重传的FIN+ACK报文段。接着A重传一次确认,重新启动2MSL计时器。

第二、防止“已失效的连接请求报文段”出现在本连接中。A在发送完最后一个ACK报文段后,再经过时间2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样就可以使下一个新的连接中不会出现这种旧的连接请求报文段。

TCP和UDP有什么区别?

UDP的主要特点

  1. UDP是无连接的;
  2. UDP使用尽最大努力交付,即不保证可靠交付,因此主机不需要维持复杂的链接状态(这里面有许多参数);
  3. UDP是面向报文的;
  4. UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等);
  5. UDP支持一对一、一对多、多对一和多对多的交互通信;
  6. UDP的首部开销小,只有8个字节,比TCP的20个字节的首部要短。

TCP的主要特点

  1. TCP是面向连接的。(就好像打电话一样,通话前需要先拨号建立连接,通话结束后要挂机释放连接);
  2. 每一条TCP连接只能有两个端点,每一条TCP连接只能是点对点的(一对一);
  3. TCP提供可靠交付的服务。通过TCP连接传送的数据,无差错、不丢失、不重复、并且按序到达;
  4. TCP提供全双工通信。TCP允许通信双方的应用进程在任何时候都能发送数据。TCP连接的两端都设有发送缓存和接收缓存,用来临时存放双方通信的数据;
  5. TCP是面向字节流的。TCP中的“流”(stream)指的是流入进程或从进程流出的字节序列。“面向字节流”的含义是:虽然应用程序和TCP的交互是一次一个数据块(大小不等),但TCP把应用程序交下来的数据仅仅看成是一连串的无结构的字节流。

jw1

1). TCP对应的应用层协议

  • FTP:定义了文件传输协议,使用21端口。常说某某计算机开了FTP服务便是启动了文件传输服务。下载文件,上传主页,都要用到FTP服务。
  • Telnet:它是一种用于远程登陆的端口,用户可以以自己的身份远程连接到计算机上,通过这种端口可以提供一种基于DOS模式下的通信服务。如以前的BBS是-纯字符界面的,支持BBS的服务器将23端口打开,对外提供服务。
  • SMTP:定义了简单邮件传送协议,现在很多邮件服务器都用的是这个协议,用于发送邮件。如常见的免费邮件服务中用的就是这个邮件服务端口,所以在电子邮件设置-中常看到有这么SMTP端口设置这个栏,服务器开放的是25号端口。
  • POP3:它是和SMTP对应,POP3用于接收邮件。通常情况下,POP3协议所用的是110端口。也是说,只要你有相应的使用POP3协议的程序(例如Fo-xmail或Outlook),就可以不以Web方式登陆进邮箱界面,直接用邮件程序就可以收到邮件(如是163邮箱就没有必要先进入网易网站,再进入自己的邮-箱来收信)。
  • HTTP:从Web服务器传输超文本到本地浏览器的传送协议。

2). UDP对应的应用层协议

  • DNS:用于域名解析服务,将域名地址转换为IP地址。DNS用的是53号端口。
  • SNMP:简单网络管理协议,使用161号端口,是用来管理网络设备的。由于网络设备很多,无连接的服务就体现出其优势。
  • TFTP(Trival File Transfer Protocal):简单文件传输协议,该协议在熟知端口69上使用UDP服务。

交换机和路由器有什么区别?

①工作所处的OSI层次不一样,交换机工作在OSI第二层数据链路层,路由器工作在OSI第三层网络层

②寻址方式不同:交换机根据MAC地址寻址,路由器根据IP地址寻址

③转发速不同:交换机的转发速度快,路由器转发速度相对较慢

交换机、路由器、网关、网桥的概念及各自用途?

  • 交换机用于局域网,利用主机的 MAC 地址进行数据传输,而不需要关心 IP 数据包中的 IP 地址,它工作于数据链路层。
  • 路由器识别网络是通过 IP 数据包中 IP 地址的网络号进行的,所以为了保证数据包路由的正确性,每个网络都必须有一个唯一的网络号。路由器通过 IP 数据包的 IP 地址进行路由选择的(将数据包递交给哪个下一跳路由器),路由器工作于网络层。
  • 网关就是连接两个网络的设备,能在不同协议间移动数据。作用于网络层以上。
  • 网桥是一个局域网与另一个局域网之间建立连接的桥梁。属于数据链路层的一种设备。

ARP是地址解析协议,简单语言解释一下工作原理。

ARP协议:通过目的的IP地址来获取MAC地址。

  1. 首先,每个主机都会在自己的ARP缓冲区中建立一个ARP列表,以表示IP地址和MAC地址之间的对应关系。
  2. 当源主机要发送数据时,首先检查ARP列表中是否有对应IP地址的目的主机的MAC地址,如果有,则直接发送数据,如果没有,就向本网段的所有主机发送ARP数据包,该数据包包括的内容有:源主机IP地址,源主机MAC地址,目的主机的IP地址。
  3. 当本网络的所有主机收到该ARP数据包时,首先检查数据包中的IP地址是否是自己的IP地址,如果不是,则忽略该数据包,如果是,则首先从数据包中取出源主机的IP和MAC地址写入到ARP列表中,如果已经存在,则覆盖,然后将自己的MAC地址写入ARP响应包中,告诉源主机自己是它想要找的MAC地址。
  4. 源主机收到ARP响应包后。将目的主机的IP和MAC地址写入ARP列表,并利用此信息发送数据。如果源主机一直没有收到ARP响应数据包,表示ARP查询失败。

注意:广播发送ARP请求,单播发送ARP响应。

ICMP协议?

ICMP是InternetControl Message Protocol,因特网控制报文协议。它是TCP/IP协议族的一个子协议,用于在IP主机、路由器之间传递控制消息。控制消息是指网络通不通、主机是否可达、路由器是否可用等网络本身的消息。这些控制消息虽然并不传输用户数据,但是对于用户数据的传递起着重要的作用。ICMP报文有两种:差错报告报文和询问报文。

DHCP协议?

动态主机配置协议,是一种让系统得以连接到网络上,并获取所需要的配置参数手段。通常被应用在大型的局域网络环境中,主要作用是集中的管理、分配IP地址,使网络环境中的主机动态的获得IP地址、Gateway地址、DNS服务器地址等信息,并能够提升地址的使用率。

HTTP的长连接和短连接?

HTTP的长连接和短连接本质上是TCP长连接和短连接。HTTP属于应用层协议。

短连接:浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。

长连接:当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的 TCP连接不会关闭。如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。实现长连接要客户端和服务端都支持长连接。

TCP短连接: client向server发起连接请求,server接到请求,然后双方建立连接。client向server发送消息,server回应client,然后一次读写就完成了,这时候双方任何一个都可以发起close操作,不过一般都是client先发起close操作。短连接一般只会在 client/server间传递一次读写操作。

TCP长连接: client向server发起连接,server接受client连接,双方建立连接。Client与server完成一次读写之后,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。

为什么TIME_WAIT状态还需要等2MSL(Max SegmentLifetime,最大分段生存期)秒之后才能返回到CLOSED状态呢?

因为虽然双方都同意关闭连接了,而且握手的4个报文也都发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SENT状态到ESTABLISH状态那样),但是我们必须假想网络是不可靠的,你无法保证你最后发送的ACK报文一定会被对方收到,就是说对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文。所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文。

IO中同步与异步,阻塞与非阻塞区别

同步和异步关注的是消息通信机制 (synchronous communication/asynchronous communication)
所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。
换句话说,就是由调用者主动等待这个调用的结果。
而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。

阻塞调用是指调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回。

非阻塞:不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。

IP地址的分类

IP地址根据网络号和主机号来分,分为A、B、C三类及特殊地址D、E。全0和全1的都保留不用。(全0表示网络地址,全1表示广播地址)
A类:第一个字节为网络号,后三个字节为主机号。该类IP地址的最前面为“0”,所以地址的网络号取值于1~126之间。一般用于大型网络。
B类:前两个字节为网络号,后两个字节为主机号。该类IP地址的最前面为“10”,所以地址的网络号取值于128~191之间。一般用于中等规模网络。
C类:前三个字节为网络号,最后一个字节为主机号。该类IP地址的最前面为“110”,所以地址的网络号取值于192~223之间。一般用于小型网络。
D类地址:以1110开头,第一个字节范围为224~239;多播地址
E类地址:以1111开头,保留今后使用
网络号(net-id):标记主机(或路由器)所连接的网络;主机号(host-id):标记主机(或路由器)。

IP地址与子网掩码相与得到网络号。

TCP/IP中,各层协议的简单介绍?

  • 数据链路层:PPP协议(点对点协议),SCMA/CD (带冲突检测的载波监听多路访问技术)

  • 网络层:IP协议、ICMP协议(网际控制报文协议)、IGMP(网际组管理协议)、ARP协议、RARP协议,OSPF(开放路径最短优先协议)。
    ICMP协议: 因特网控制报文协议。它是TCP/IP协议族的一个子协议,用于在IP主机、路由器之间传递控制消息。

  • 传输层:UDP协议、TCP协议。

  • 应用层:FTP(文件传送协议)、Telenet(远程登录协议)、DNS(域名解析协议)、SMTP(邮件传送协议),POP3协议(邮局协议),HTTP协议。
    TFTP协议: 是TCP/IP协议族中的一个用来在客户机与服务器之间进行简单文件传输的协议,提供不复杂、开销不大的文件传输服务。
    HTTP协议: 超文本传输协议,是一个属于应用层的面向对象的协议,由于其简捷、快速的方式,适用于分布式超媒体信息系统。
    NAT协议:网络地址转换属接入广域网(WAN)技术,是一种将私有(保留)地址转化为合法IP地址的转换技术。

    DHCP协议(动态主机配置协议):一个局域网的网络协议,使用UDP协议工作。用途:给内部网络或网络服务供应商自动分配IP地址,给用户或者内部网络管理员作为对所有计算机作中央管理的手段。

在浏览器中输入www.baidu.com后执行的全部过程?

  1. 浏览器查询DNS,获取域名对应的IP地址:具体过程包括浏览器搜索自身的DNS缓存、搜索操作系统的DNS缓存、读取本地的Host文件、检查路由器缓存和向本地DNS服务器进行查询(搜索本地DNS服务器缓存)等。对于向本地DNS服务器进行查询,如果要查询的域名由本地DNS服务器区域解析,则返回解析结果给客户机,完成域名解析(此解析具有权威性);如果要查询的域名不由本地DNS服务器区域解析,但该服务器已缓存了此网址映射关系,则调用这个IP地址映射,完成域名解析(此解析不具有权威性)。如果本地域名服务器并未缓存该网址映射关系,那么将根据其设置发起递归查询或者迭代查询;
  2. 浏览器获得域名对应的IP地址以后,浏览器向服务器请求建立链接,发起三次握手;
  3. TCP/IP链接建立起来后,浏览器向服务器发送HTTP请求;
  4. 服务器接收到这个请求,并根据路径参数映射到特定的请求处理器进行处理,并将处理结果及相应的视图返回给浏览器;
  5. 浏览器解析并渲染视图,如果遇到对js文件、css文件及图片等静态资源的引用,则重复上述步骤,向服务器请求这些资源;
  6. 浏览器根据其请求到的资源、数据渲染页面,最终向用户呈现一个完整的页面;
  7. 浏览器向服务器(目的主机)发起四次挥手,请求断开连接。

jw2

DNS的工作原理?

  1. 应用进程将待解析的域名放在DNS请求报文中,以UDP数据报的形式发送给本地域名服务器,本地域名服务器查找到相应域名的IP地址后(主机向本地域名服务器的查询一般都是采用递归查询),就将该域名的IP地址信息放入应答报文中返回给客户进程。
  2. 如果主机所询问的本地域名服务器不知道被查询域名的IP地址,那么本地域名服务器就以DNS客户的身份,向其他域名服务器继续发送查询请求报文(本地域名服务器向根域名服务器的查询通常采用迭代查询)。
  3. 当根域名服务器收到本地域名服务器的迭代查询请求报文时,要么给出所要查询的IP地址,要么告诉本地域名服务器:“你下一步应当向哪一个域名服务器查询”。然后让本地域名服务器进行后续的查询。

Cookie和Session的区别

概述

  1. 由于HTTP协议是无状态的协议,所以服务端需要记录用户的状态时,就需要用某种机制来识具体的用户,这个机制就是Session。典型的场景比如购物车,当你点击下单按钮时,由于HTTP协议无状态,所以并不知道是哪个用户操作的,所以服务端要为特定的用户创建了特定的Session,用于标识这个用户,并且跟踪用户,这样才知道购物车里面有几本书。这个Session是保存在服务端的,有一个唯一标识。在服务端保存Session的方法很多,内存、数据库、文件都有。集群的时候也要考虑Session的转移,在大型的网站,一般会有专门的Session服务器集群,用来保存用户会话,这个时候 Session 信息都是放在内存的,使用一些缓存服务比如Memcached之类的来放 Session。
  2. 思考一下服务端如何识别特定的客户?这个时候Cookie就登场了。每次HTTP请求的时候,客户端都会发送相应的Cookie信息到服务端。实际上大多数的应用都是用 Cookie 来实现Session跟踪的,第一次创建Session的时候,服务端会在HTTP协议中告诉客户端,需要在 Cookie 里面记录一个Session ID,以后每次请求把这个会话ID发送到服务器,我就知道你是谁了。有人问,如果客户端的浏览器禁用了 Cookie 怎么办?一般这种情况下,会使用一种叫做URL重写的技术来进行会话跟踪,即每次HTTP交互,URL后面都会被附加上一个诸如 sid=xxxxx 这样的参数,服务端据此来识别用户。
  3. Cookie其实还可以用在一些方便用户的场景下,设想你某次登陆过一个网站,下次登录的时候不想再次输入账号了,怎么办?这个信息可以写到Cookie里面,访问网站的时候,网站页面的脚本可以读取这个信息,就自动帮你把用户名给填了,能够方便一下用户。这也是Cookie名称的由来,给用户的一点甜头。
  4. 所以,总结一下:
    Session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中。
    Cookie是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现Session的一种方式。

总结

  1. session 在服务器端,cookie 在客户端(浏览器)
  2. session 默认被存在在服务器的一个文件里(不是内存)
  3. session 的运行依赖 session id,而 session id 是存在 cookie 中的,也就是说,如果浏览器禁用了 cookie,同时 session 也会失效(但是可以通过其它方式实现,比如在 url 中传递 session_id)
  4. session 可以放在文件、数据库、或内存中都可以。
  5. 用户验证这种场合一般会用 session

论述

一、cookie和session

cookie和session都是用来跟踪浏览器用户身份的会话方式。

区别:

1、保持状态:cookie保存在浏览器端,session保存在服务器端

2、使用方式:

(1)cookie机制:如果不在浏览器中设置过期时间,cookie被保存在内存中,生命周期随浏览器的关闭而结束,这种cookie简称会话cookie。如果在浏览器中设置了cookie的过期时间,cookie被保存在硬盘中,关闭浏览器后,cookie数据仍然存在,直到过期时间结束才消失。Cookie是服务器发给客户端的特殊信息,cookie是以文本的方式保存在客户端,由对服务器的请求来传递,每次都会携带在HTTP请求头中,如果使用cookie保存过多数据会带来性能问题。

(2)session机制:当服务器收到请求需要创建session对象时,首先会检查客户端请求中是否包含sessionid。如果有sessionid,服务器将根据该id返回对应session对象。如果客户端请求中没有sessionid,服务器会创建新的session对象,并把sessionid在本次响应中返回给客户端。通常使用cookie方式存储sessionid到客户端,在交互中浏览器按照规则将sessionid发送给服务器。如果用户禁用cookie,则要使用URL重写,可以通过response.encodeURL(url) 进行实现;API对encodeURL的结束为,当浏览器支持Cookie时,url不做任何处理;当浏览器不支持Cookie的时候,将会重写URL将SessionID拼接到访问地址后。

3、存储内容:cookie只能保存字符串类型,以文本的方式;session通过类似与Hashtable的数据结构来保存,能支持任何类型的对象(session中可含有多个对象)

4、存储的大小:cookie:单个cookie保存的数据不能超过4kb;session大小没有限制。

5、安全性:cookie:针对cookie所存在的攻击:Cookie欺骗,Cookie截获;session的安全性大于cookie。

原因如下:

(1)sessionID存储在cookie中,若要攻破session首先要攻破cookie;

(2)sessionID是要有人登录,或者启动session_start才会有,所以攻破cookie也不一定能得到sessionID;

(3)第二次启动session_start后,前一次的sessionID就是失效了,session过期后,sessionID也随之失效。

(4)sessionID是加密的

(5)综上所述,攻击者必须在短时间内攻破加密的sessionID,这很难。

6、应用场景:

cookie:

(1)判断用户是否登陆过网站,以便下次登录时能够实现自动登录(或者记住密码)。如果我们删除cookie,则每次登录必须从新填写登录的相关信息。

(2)保存上次登录的时间等信息。

(3)保存上次查看的页面

(4)浏览计数

jw11

session:Session用于保存每个用户的专用信息,变量的值保存在服务器端,通过SessionID来区分不同的客户。

(1)网上商城中的购物车

(2)保存用户登录信息

(3)将某些数据放入session中,供同一用户的不同页面使用

(4)防止用户非法登录

7、缺点:

cookie:

  • 大小受限

  • 用户可以操作(禁用)cookie,使功能受限

  • 安全性较低
  • 有些状态不可能保存在客户端。
  • 每次访问都要传送cookie给服务器,浪费带宽。
  • cookie数据有路径(path)的概念,可以限制cookie只属于某个路径下。

session:

  • Session保存的东西越多,就越占用服务器内存,对于用户在线人数较多的网站,服务器的内存压力会比较大。

  • 依赖于cookie(sessionID保存在cookie),如果禁用cookie,则要使用URL重写,不安全。

  • 创建Session变量有很大的随意性,可随时调用,不需要开发者做精确地处理,所以,过度使用session变量将会导致代码不可读而且不好维护。

下面是关于Cookie生命周期的介绍:

Cookie如果不设置过期时间(使用setMaxAge()方法),则表示这个cookie生命周期为浏览器会话期间,只要关闭浏览器窗口,cookie就消失了。这种生命期为浏览会话期的cookie被称为会话cookie。会话cookie一般不保存在硬盘上而是保存在内存里。
如果设置了过期时间,浏览器就会把cookie保存到硬盘上,关闭后再次打开浏览器,这些cookie依然有效直到超过设定的过期时间。存储在硬盘上的cookie可以在不同的浏览器进程间共享,比如两个IE窗口。而对于保存在内存的cookie,不同的浏览器还有不同的处理方式。

HTTPS的实现原理

废话一下:其实我也不知道自己理解的是不是对的,我觉得网上很多讲Https原理的文章其实本质上讲的都是SSL/TLS的原理。在我看来,Https指的是Http协议+ SSL/TLS协议,也就是说,Http协议本来定义了数据的包装方式(要包含请求头,请求方式等),现在在这个基础上,还要求行对数据进行加密,对通信双方身份进行验证(如何加密,如何验证由SSL决定)。 我也不知道我理解的对不对?求高人赐教。

jw6

下面上图:

jw7

jw8

补充:

非对称加密:

非对称加密(asymmetric cryptography),一种密码学算法类型,在这种密码学方法中,需要一对密钥,一个是私人密钥,另一个则是公开密钥。这两个密钥是数学相关,用某用户密钥加密后所得的信息,只能用该用户的解密密钥才能解密。

比如说: 我有一对密钥,一个是公钥(假设是’a’),一个是私钥(假设是’b’),现在我用’a’对一个内容加了密,那么,此时,,你只有用私钥’b’对它进行解密才能看到正确内容。同理,如果我用私钥’b’加了密,你只有用公钥’a’解密才能看到内容。

对称加密:

对称密钥加密(英语:Symmetric-key algorithm)又称为对称加密、私钥加密、共享密钥加密,是密码学中的一类加密算法。这类算法在加密和解密时使用相同的密钥,或是使用两个可以简单地相互推算的密钥。

对称加密其实就是说: 比如我用’a’作为密钥对内容加了密,你必须也得用’a’作为密钥对加密后的内容进行解密。

HTTP协议

特点

1.HTTP协议是无状态的

​ 就是说每次HTTP请求都是独立的,任何两个请求之间没有什么必然的联系。但是在实际应用当中并不是完全这样的,引入了Cookie和Session机制来关联请求。

2.多次HTTP请求

  在客户端请求网页时多数情况下并不是一次请求就能成功的,服务端首先是响应HTML页面,然后浏览器收到响应之后发现HTML页面还引用了其他的资源,例如,CSS,JS文件,图片等等,还会自动发送HTTP请求这些需要的资源。现在的HTTP版本支持管道机制,可以同时请求和响应多个请求,大大提高了效率。

3.基于TCP协议

  HTTP协议目的是规定客户端和服务端数据传输的格式和数据交互行为,并不负责数据传输的细节。底层是基于TCP实现的。现在使用的版本当中是默认持久连接的,也就是多次HTTP请求使用一个TCP连接。

HTTP报文

请求报文

jw9

1
2
3
4
5
6
7
8
9
10
11
GET /wxisme HTTP/1.1  
Host: www.cnblogs.com
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.0; zh-CN; rv:1.8.1) Gecko/20061010 Firefox/2.0
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: en-us,zh-cn;q=0.7,zh;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: gb2312,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Proxy-Connection: keep-alive
Cookie: ASP.NET_SessionId=ey5drq45lsomio55hoydzc45
Cache-Control: max-age=0

响应报文

jw10

1
2
3
4
5
6
7
8
9
10
HTTP/1.1 200 OK
Date: Tue, 12 Jul 2016 21:36:12 GMT
Content-Length: 563
Content-Type: text/html

<html>
<body>
Hello http!
</body>
</html>
1234
FoBoHuang

FoBoHuang

乌云后面依然是灿烂的晴天。——朗弗罗

40 日志
12 分类
11 标签
GitHub
© 2019 FoBoHuang
由 Hexo 强力驱动
|
主题 — NexT.Mist v5.1.4