BIO中的stream是单向的,例如FileInputStream对象只能进行读取数据的操作,而NIO中的Channel是双向的,可以读操作,也可以写操作
常见的Channel实现类
FileChannel:用于读取、写入、映射和操作文件的通道
DatagramChannel:通过UDP读写网络中的数据通道
SocketChannel:通过TCP读写网络中的数据
ServerSocketChannel:可以监听新进来的TCP连接,对每一个新进来的连接都会创建一个SocketChannel(ServerSocketChannel类似ServerSocket,SocketChannel类似Socket)
Selector(选择器)
概述
Selector是NIO中用于多路复用的核心组件,它可以用一个线程管理多个Channel。Selector可以检测多个注册的通道上是否有事件发生(可设置选择器监听通道的某某事件),例如连接就绪、数据可读、数据可写等。实现了一个I/O线程可以并发处理N个客户端连接和读写的操作。
可监听的事件类型
- 读:SelectionKey.OP_READ
- 写:SelectionKey.OP_WRITE
- 连接:SelectionKey.OP_CONNECT
- 接收:SelectionKey.OP_ACCEPT
若注册时不止监听一个事件,则可以使用位或操作符连接。每个通道注册到 Selector 时都会关联一个或多个事件
例如SelectionKey.OP_READ |SelectionKey.OP_WRITE
在Java中," | " 符号是位运算中的按位或操作符。通过使用 " | " 运算符将多个事件合并成一个整数值,我们可以将多个事件注册到同一个 SelectionKey 上。在后续的处理中,可以通过 SelectionKey 的 readyOps() 方法来判断发生了哪些事件。通过使用 “|” 运算符将多个事件合并在一个 SelectionKey 上,可以更方便地处理多种不同类型的事件。可以通过 selectedKeys() 方法获取到发生事件的 Channel(已就绪的监听事件)。
AIO
概述
异步非阻塞,通过异步的方式来进行I/O操作,当I/O操作完成时,会通知应用程序进行处理,不需要等待。
网络应用程序框架
Netty
概述
Netty是一个异步事件驱动的网络应用程序框架,用于快速开发可维护、可拓展的网络服务器和客户端。
Reactor模型
概念
Reactor是在NIO多路复用的基础上提出的一个高性能的IO设计模式,核心思想是把响应IO事件和业务处理进行分离,通过一个或者多个线程去处理IO事件,再把就绪的事件分发给业务线程进行异步处理,Reactor模型有三个重要的组件:Reactor、Acceptor、Handlers,以及三种模型:单线程Reactor、多线程Reactor、主从Reactor(多Reactor多线程模型,即Master-Worker模式)。
Reactor模型是一种常用的事件驱动的分发处理模型,基于I/O多路复用模型与线程池复用线程资源,并将输入事件和处理事件的逻辑解耦。不同的角色职责有:Reactor负责事件分发、Acceptor负责处理客户端连接、Handler处理非连接事件。
Reactor的核心
- 事件源:负责产生事件,例如网络连接、IO请求。
- Reactor:负责监听和分发事件。
- 处理器(Handlers):负责处理分发的事件,是实际业务逻辑的实现,对收到的事件进行相应的处理。
- 事件处理器注册表:用于管理事件源和处理器的注册关系,它将事件源和相应的处理器进行绑定。
单Reactor单线程
多个客户端连接请求时,Reactor通过在Selector中的通道轮询到有事件发生,判断事件是连接事件则使用Acceptor建立连接,如果是其他读写事件,则分发给相应的Handler进行处理。缺点是Reactor和Handler在一个线程里,如果处理逻辑阻塞了,那整个Reactor程序就阻塞了。
单Reactor多线程
Reactor通过Selector轮询监听客户端事件,如果是连接事件,则由Acceptor处理并创建一个Handler对象绑定。如果是读写请求,则交给对应的Handler处理,Handler只响应事件,不做具体业务处理,将read方法读到的数据分发给线程池的线程去进行业务处理。
相对比单线程Reactor,在分发这一块异步了,交给了不同线程去处理,发挥了多核CPU的性能,但是Reactor只有一个,所有事件的监听和响应,都由一个Reactor来完成,并发性不好。
主从Reactor
相比于单Reactor多线程,多了一个主Reactor专门用于处理连接事件,如果不是连接事件,则分发给从Reactor去处理。
Netty模型
简介
Netty有两个线程组,分为Boss NioEventLoopGroup和Worker NioEventLoopGroup,这两个NioEventLoopGroup包含多个NioEventLoop,NioEventLoop表示一个不断循环执行处理任务的线程,每个NioEventLoop中都包含一个Selector,用于监听绑定在其上的SocketChannel的网络通讯。
Boss NioEventLoopGroup负责与客户端建立连接,生成一个NioSocketChannel,并将其注册到Worker NioEventLoopGroup的某个NioEventLoop的Selector下;Worker NioEventLoopGroup负责读写请求,在对应的NioSocketChannel处理,在处理NioSocketChannel业务时,会使用pipeline,管道中维护了很多handler处理器来处理Channel中的数据。
组件
NioEventLoop
NioEventLoop中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用NioEventLoop的run方法,执行I/O任务和非I/O任务。
I/O 任务,即 selectionKey 中 ready 的事件,如 accept、connect、read、write 等,由 processSelectedKeys 方法触发。
非 IO 任务,添加到 taskQueue 中的任务,如 register0、bind0 等任务,由 runAllTasks 方法触发。
NioEventLoopGroup
维护了一组NioEventLoop,每个NioEventLoop维护一个Selector实例。每个NioEventLoop对应一个线程,处理绑定在Selector上的Channel的事件,NioEventLoopGroup则相当于这一组线程的组成。
Bootstrap、ServerBootstrap
一个Netty应用通常由一个Bootstrap开始,主要作用是配置整个Netty程序,串联各个组件,Bootstrap类是客户端程序的启动引导类,ServerBootstrap类是服务端启动引导类。
一般使用启动引导类的设置项有:
- 设置EventLoopGroup线程组
一个线程组默认的线程数为CPU核数的两倍,可使用自定义线程数
- 设置Channel通道类型
NioSocketChannel: 异步非阻塞的客户端 TCP Socket 连接。
NioServerSocketChannel: 异步非阻塞的服务器端 TCP Socket 连接。
OioSocketChannel: 同步阻塞的客户端 TCP Socket 连接。
OioServerSocketChannel: 同步阻塞的服务器端 TCP Socket 连接。
- 设置Option参数
用于设置连接配置参数
childOption()常用的参数: SO_RCVBUF Socket参数,TCP数据接收缓冲区大小。 TCP_NODELAY TCP参数,立即发送数据,默认值为Ture。 SO_KEEPALIVE Socket参数,连接保活,默认值为False。启用该功能时,TCP会主动探测空闲连接的有效性。
option()常用参数:SO_BACKLOG Socket参数,服务端接受连接的队列长度,如果队列已满,客户端连接将被拒绝。默认值,Windows为200,其他为128
- 设置handler流水线
- 进行端口绑定
ChannelFuture
Netty的I/O操作都是异步的,这些操作会返回一个ChannelFuture对象,通过对该对象的监听,可以获取IO操作的结果。
Channel
用于执行网络I/O操作。
下面是一些常用的 Channel 类型:
NioSocketChannel,异步的客户端 TCP Socket 连接。
NioServerSocketChannel,异步的服务器端 TCP Socket 连接。
NioDatagramChannel,异步的 UDP 连接。
NioSctpChannel,异步的客户端 Sctp 连接。
NioSctpServerChannel,异步的 Sctp 服务器端连接。 这些通道涵盖了 UDP 和 TCP 网络 IO 以及文件 IO。
Selector
Netty基于Selector来实现I/O多路复用,通过Selector一个线程可以监听多个连接的Channel事件,Selector会不断地轮询这些Channel是否已有已就绪的I/O事件。这样就可以做到一个线程高效管理多个Channel。
ChannelHandler
用于处理网络事件和数据的传递,负责数据从一个地方传递到另一个地方时对数据进行处理和转换,ChannelHandler在ChannelPipeline中形成一条处理链,每个ChannelHandler可以对出站和入站数据进行处理。
ChannelHandler 接口定义了一系列的回调方法,当发生相应的事件时,Netty 会自动调用这些方法。ChannelHandler 可以分为两大类:
- 入站处理器(Inbound Handler):处理入站数据,包括接收、解码、处理接收到的数据。入站处理器的回调方法包括:
- channelRegistered:通道注册事件
- channelActive:通道激活事件
- channelRead:通道读事件
- channelReadComplete:通道读取完成事件
- exceptionCaught:异常捕获事件
- 出站处理器(Outbound Handler):处理出站数据,包括编码、发送数据到网络。出站处理器的回调方法包括:
- bind:通道绑定事件
- connect:通道连接事件
- write:通道写事件
- flush:通道刷新事件
- disconnect:通道断开连接事件
- close:通道关闭事件
ChannelHandlerContext
代表了ChannelHandler在ChannelPipeline中的上下文,通过ChannelHandlerContext,ChannelHandler可以与其所在的ChannelPipeline和其他ChannelHandler进行交互,还可以拿到Channel、ChannelPipeline等对象。当数据从一个 ChannelHandler 传递到下一个 ChannelHandler 时,实际上是通过 ChannelHandlerContext 来实现的。
ChannelPipeline
ChannelPipeline是一条拦截和处理数据的处理链。ChannelPipeline相当于处理器的容器,初始化Channel时,将ChannelHandler桉顺序装在ChannelPipeline中,就可以实现桉顺序执行ChannelHandler。
一个Channel包含了一个ChannelPipeline,而ChannelPipeline中又维护了一个由ChannelHandlerContext组成的双向链表,并且每个ChannelHandlerContext中又关联着一个ChannelHandler。
传递过程:
Channel接收到数据时,即读事件(入站事件),会从ChannelPipeline的头部向尾部传递,在这个过程中会触发ChannelHandler中相应的事件处理方法。同样,当向Channel写数据时,即write事件(出站),会从ChannelPipeline的尾部向前传递直到头部,在这个过程中也会触发ChannelHandler中相应的事件处理方法。
装配方法:
实例化ChannelInitializer,重写initChannel()初始化通道的方法,获得SocketChannel进行装配流水线。将上面的实现类加入到Bootstrap中childHandler()方法,之后与客户端建立连接后每一个生成的NioSocketChannel都会调用这个initChannel()方法进行装配。
粘包拆包问题
粘包和拆包
TCP是一个流协议,它发送的数据是一个没有界限的长串的二进制数据。操作系统在发送TCP数据时,底层会有一个缓冲区,如果一次请求发送的数据量比较小,没达到缓冲区大小,则TCP会将多个请求合并为一个请求发送,这就是粘包;如果依次发送的数据量比较大,超过了缓冲区大小,则TCP会将其拆分为多次发送,这就是拆包,也就是将一个大的包分为多个小的包进行发送。
既然知道了TCP是无界的数据流,且协议本身无法避免粘包、拆包的发生。那我们只能在应用层数据协议上加以控制。
常见的解决方案
- 客户端在发送数据包的时候,每个数据包的长度固定,不足长度则使用空格来补充,服务端读取既定长度的内容作为一条完整的消息;
- 客户端在每个包末尾使用固定的分隔符,例如\r\n,服务端利用这个分隔符分离出每一条消息的内容。如果一个包被拆分了,则等待下一个包发送过来后找到其中的\r\n,然后进行合并,得到一个完整的包;
- 将消息分为头部和消息体,头部保存当前消息的长度,这样服务端就可以根据长度来判断数据是否接收完毕,只有读取到足够长的消息之后才算读到一个完整的消息;
Netty提供的解决方案
在确定了上面使用哪种方案来解决后我们就可以使用对应的解码器了。
FixedLengthFrameDecoder
解码固定长度的消息帧,将接收到的数据按照固定长度进行拆分,将连续的数据流拆分成固定大小的消息帧。
构造函数frameLength用于指定消息帧长度,如果客户端发送的消息不足frameLength,服务端的解码器会等积累到frameLength长度后再解码。
DelimiterBasedFrameDecoder
解码以特定分隔符为界的消息帧,将接收到的数据按照指定的分隔符进行拆分,从而将连续的数据流拆分为多个消息帧。
DelimiterBasedFrameDecoder的maxFrameLength参数表示个消息的最大长度。如果接收到的消息长度超过此值,DelimiterBasedFrameDecoder 将抛出 TooLongFrameException 异常。
LengthFieldBasedFrameDecoder和LengthFieldPrepender结合
LengthFieldBasedFrameDecoder用于从接收的字节流中提取包含消息长度的字段,并根据该长度对字节流进行拆分,进而得到完整的消息帧。
LengthFieldPrepender用于在发送的消息前添加表示消息长度的字段。
LengthFieldBasedFrameDecoder构造函数
- maxFrameLength: 表示单个帧(消息)的最大长度。如果接收到的消息长度超过此值,LengthFieldBasedFrameDecoder 将抛出 TooLongFrameException 异常,用于防止过长的消息导致内存溢出。
- lengthFieldOffset: 表示长度字段的偏移量,即长度字段的起始位置。长度字段是指用于表示帧长度的字段,它是一个固定长度的字段,用于标识整个帧的长度(包括长度字段本身和消息内容)。
- lengthFieldLength: 表示长度字段的长度,即表示帧长度的字段所占用的字节数。通常情况下,长度字段的长度是固定的,可以是 1、2、3、4、8 等字节。
- lengthAdjustment: 表示长度字段的值需要进行调整的值。在某些情况下,帧的长度字段不包含长度字段本身的长度,因此需要进行调整。例如,如果长度字段的值不包含长度字段本身的 4 个字节,而是表示剩余消息内容的长度,那么 lengthAdjustment 就应该设置为 -4。
- initialBytesToStrip: 表示从解码帧中跳过的字节数。在解码出完整的帧之后,可能帧的前面有一些不需要的字节,可以通过 initialBytesToStrip 来指定跳过这些字节。
- failFast: 表示是否在发现超过 maxFrameLength 的帧长度时立即抛出异常。如果设置为 true,则会立即抛出异常并关闭连接;如果设置为 false,则会继续接收数据,直到找到下一个帧的长度或达到 maxFrameLength,然后再拆分帧。
LengthFieldPrepender构造函数参数说明:
- lengthFieldLength: 长度字段的长度,即表示消息长度的字段将占用的字节数。
- lengthIncludesLengthFieldLength: 是否将长度字段的长度包含在总长度中。如果设置为 true,表示总长度 = 消息长度 + 长度字段的长度;如果设置为 false,表示总长度 = 消息长度
零拷贝
传统IO模式
用户应用程序通过read()函数,从用户态切换到内核态,再通过DMA控制器将磁盘(硬件设备)中的数据拷贝到内核缓冲区,接着read()调用返回,从内核态切换到用户态,CPU将内核缓冲区的数据拷贝到用户缓冲区;
用户应用程序通过write()函数,从用户态切换到内核态,接着CPU将数据从用户缓冲区拷贝到内核缓冲区的socket缓冲区,然后write()调用返回,触发从内核态切换为用户态,最后通过异步DMA拷贝socket缓冲区的数据到网卡。
在上面的IO情形中,发生了四次用户态内核态的上下文切换、四次数据拷贝(两次CPU拷贝、两次DMA拷贝)。
DMA代表“Direct Memory Access”(直接内存访问),它是计算机系统中用于实现高速数据传输的一种技术。传统上,CPU负责将数据从一个设备(如硬盘、网络卡等)读取到内存中或从内存中写入到设备中。这个过程需要CPU参与,进行数据的拷贝和传输,会占用CPU的时间和处理能力。
DMA技术通过引入专门的DMA控制器(DMA 本质上是一块主板上独立的芯片),使得外部设备可以直接访问系统内存,无需CPU的干预,从而实现高速数据传输。
概念
零拷贝是一种优化技术,减少数据在内核态和用户态之间的拷贝与上下文切换,从而提高数据传输效率,减少CPU和内存开销。
实现
mmap(Memory Mapping)(内存映射)
通过将内核缓冲区与应用程序共享,将用户空间内存映射到内核空间,这样用户对内存区域的修改可以直接反映到内核空间,同样内核空间的修改也可以直接反映到用户空间,省去了内核缓冲区与用户缓冲区之间的数据拷贝。
用户通过mmap,从用户态切换到内核态,然后通过DMA将磁盘数据拷贝到内核缓冲区,mmap调用返回,从内核态切换回用户态,用户应用程序通过write()向操作系统发起IO调用,从用户态转为内核态,接着CPU将数据从内核缓冲区拷贝到内核缓冲区里的socket缓冲区,wirte()调用返回,并从内核态转回用户态。DMA异步将socket缓冲区拷贝到网卡。
上面的mmap零拷贝IO中发生了四次用户态内核态的上下文切换,三次数据拷贝(两次DMA拷贝、一次CPU拷贝)。
sendfile
只要执行了read()和write()都会分别发生两次的上下文切换,首先从用户态切换到内核态,待内核态完成任务后,切换回用户态交由进程代码执行。
sendfile将read()和write()合并为一个操作,用户应用程序发出sendfile系统调用,从用户态切换到内核态,再通过DMA将数据从磁盘拷贝到内核缓冲区,然后CPU直接将数据从内核缓冲区拷贝到socket缓冲区,接着sendfile调用返回,从内核态切换到用户态,DMA再异步将内核空间socket缓冲区中的数据传递到网卡。
上面的sendfile实现的零拷贝IO发生了两次用户态内核态的上下文切换、三次数据拷贝(两次DMA拷贝和一次CPU拷贝)。
SG-DMA+sendfile
SG-DMA可以直接从内核空间缓冲区将数据拷贝到网卡,无需先复制一份到socket缓冲区,省去了一次CPU拷贝。
用户应用程序发出sendfile系统调用,从用户态切换到内核态,然后通过DMA器将数据从磁盘拷贝到内核缓冲区中,接下来不需要CPU将数据从内核缓冲区拷贝到socket缓冲区,而是将相应的文件描述符信息复制到socket缓冲区(该信息包含内核缓冲区的内存地址和内核缓冲区的偏移量),接着sendfile调用返回,从内核态切换回用户态,DMA根据socket缓冲区中的描述符信息将内核缓冲区中的数据复制到网卡。
上面带有SG-DMA的sendfile实现的零拷贝IO发生了两次用户态和内核态的上下文切换、两次数据拷贝(两次DMA拷贝),实现了最理想的零拷贝IO传输,不需要任何CPU拷贝。
应用实例
- FileChannel的map()使用mmap零拷贝技术,该方法返回MappedByteBuffer,该方法可以在一个打开的文件和MappedByteBuffer之间建立一个虚拟内存映射,MappedByteBuffer是一个文件的共享映射区域,用户空间和内核空间共享该缓冲区。
- FileChannel的transferTo、transferFrom使用sendfile零拷贝技术。
- ByteBuffer采用的直接内存,使用堆外内存直接进行socket读写。使用堆内存则多了一次将堆内存Buffer拷贝到直接内存的操作,然后才写入socket。
- Netty的FileRegion包装的FileChannel.transfer实现直接将文件缓冲区的数据发送到目标Channel(目标文件),避免通过write方法导致的内存拷贝。
- Netty的Unpooled的CompositeByteBuf可以将多个ByteBuf合并为一个逻辑上的ByteBuf,避免了各个ByteBuf之间的拷贝。合并是通过拷贝字节数组的引用来解决问题的。而不是拷贝字节数组内容。这两个 ByteBuf 在CompositeByteBuf 内部都是单独存在的。
- Netty的Unpooled的wrap操作(底层封装了CompositeByteBuf操作),可以将byte[]数组、ByteBuf、ByteBuffer包装成一个Netty ByteBuf对象,避免了拷贝操作。(生成的ByteBuf对象和原始数据共用了同一个存储空间,对原对象的修改也会反映到ByteBuf对象中)
- Netty的ByteBuf的slice操作,可以将一个ByteBuf切片成多个共享一个存储区域的ByteBuf对象,其实内部就是共享了原ByteBuf存储空间的不同部分而已。
Selector 空轮询BUG
BUG
若Selector的轮询结果为空,也没有wakeup或者新消息处理,则发生空轮询,最后导致CPU使用率满。
Netty中的解决思路
对Selector()方法中的阻塞定时 select(timeMIllinois)操作的 次数进行统计,每完成一次select操作进行一次计数,若在循环周期内,发生N次空轮询,如果N值大于BUG阈值(默认为512),就进行空轮询BUG处理。重建Selector,判断是否是其他线程发起的重建请求,若不是则将原SocketChannel从旧的Selector上去除注册,重新注册到新的Selector上,并将原来的Selector关闭。
实时通信
WebSocket协议
通讯协议
TCP/IP
三次握手
四次挥手
UDP
HTTP
设计模式
策略模式
概念
项目里怎么用
单例模式
单例模式是一种设计模式,用于确保一个类只有一个实例,并提供一个全局访问点来访问该实例。
饿汉
class Single{
private static Single instance = new Single();
public static Single getSingle() {
return instance;
}
private Single() {
}
}
懒汉
单线程版本
class Singleton{
private static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
private Singleton() {
}
}
多线程版本
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
双重检测锁
在实现单例模式时,使用双重检查锁(Double-Checked Locking)是一种常见的方式,结合使用volatile关键字来确保线程安全。
双重检查锁的方式可以在多线程环境下保证只有一个实例被创建,同时也能避免每次获取实例时都需要进行同步操作,提高了性能。它的基本思想是在获取实例时先检查实例是否已经被创建,如果没有被创建,则进入同步块进行实例的创建,这样可以减少多个线程同时进入同步块的情况。而第二次检查是为了在多个线程同时通过了第一个检查,只有一个线程能够创建实例,其他线程则直接返回已经创建的实例。
然而,双重检查锁在没有使用volatile关键字的情况下可能会引发线程安全问题。在Java中,由于指令重排序的存在,一个对象的创建过程可能被重排序,导致其他线程在第一个检查通过后,访问到一个未完全初始化的实例。为了解决这个问题,需要在实例的声明处使用volatile关键字,它的作用是禁止指令重排序,保证实例的创建过程是按照顺序进行的,从而避免了线程安全问题。
因此,使用双重检查锁方式实现单例模式时,需要同时使用volatile关键字,以确保线程安全和正确的对象创建顺序。
当一个对象被创建时,其实例化的过程通常会分为三个步骤:1)分配内存空间,2)初始化对象,3)将内存空间的引用赋值给对应的变量。
然而,在多线程环境下,由于编译器和处理器的优化,这些步骤可能会被重排序,即执行顺序可能不同于程序中的顺序。这种重排序在单线程环境下不会产生问题,因为对于单线程来说,重排序对最终结果没有影响。
但是,在多线程环境下,如果某个线程在执行双重检查锁时遇到了重排序,就可能会导致其他线程在第一个检查通过后,访问到一个尚未完全初始化的对象。这会引发严重的线程安全问题,因为其他线程可能会使用到未初始化完成的对象,导致程序出现意料之外的行为。
下面通过一个简单的例子来说明这个问题:
public class Singleton {
private static Singleton instance;
private Singleton() {
// 构造函数
}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 重排序可能导致问题
}
}
}
return instance;
}
}
在上述代码中,当线程A通过第一个检查时,发现instance为null,于是进入同步块并执行instance = new Singleton()。然而,由于指令重排序,这个过程可能被重排为1)分配内存空间,2)将内存空间的引用赋值给instance,3)初始化对象。如果此时线程B也通过了第一个检查,那么它会发现instance不为null,并返回一个未完全初始化的对象,导致出现异常或不正确的行为。
为了解决这个问题,我们需要使用volatile关键字修饰instance变量,将其声明为volatile Singleton instance;。这样一来,volatile关键字会禁止指令重排序,确保实例化过程的顺序是按照程序中的顺序进行的,从而避免了其他线程获取到未完全初始化的对象。
责任链模式
模板方法模式
观察者模式
代理模式
Web开发框架
Spring
控制反转(IoC)
控制反转是一种设计原则,指将应用程序中对象的创建和管理交由Spring容器负责,而不需要我们在程序代码中手动去实例化它。
依赖注入(DI)
依赖注入是控制反转的一种实现方式,指的是在创建对象时,将对象所依赖的其他对象注入到它的属性或构造函数中。通过依赖注入,对象无需自己去查找或创建依赖的其他对象,只需要定义好类和依赖关系的配置,由框架负责将依赖的对象注入进来。
创建Bean的方式
- 构造函数注入
- Setter方法注入
- 静态/实例工厂方法注入
Bean的作用域
Singleton
从加载IOC容器开始,Spring IOC容器中同一种类仅会存在一个Bean实例,无论是否从容器中使用了该Bean。
Prototype
每次中容器种取用Bean时,都会创建一个新的Bean,如果不取出Bean,则不会创建Bean对象。
Request
每次HTTP请求都会创建一个新的Bean实例,该作用域仅在Web应用程序上下文中有效(该 bean 仅在当前 HTTP request 内有效)。