深入理解Netty(第二部分):关键接口与类的剖析与解读
1. 开篇
这部分是我想唠叨的一些话,想直接了解 Netty 的同学可以跳过这一节
上文我们说到了介绍 Netty 的原因和重要性以及 Netty 的基础 —— Java NIO 的一些知识。接下来本文将对 Netty 的重要的类做一个介绍。
在正式开始之前先多唠叨几句。我写 Netty 这一系列(目前只有两篇,斗胆叫系列吧)文章的主要目的还是为了自我学习,为了更深入地了解 Netty。
学一个技术,看一篇文章,读一本书,并不是看完了,自认为读懂了就结束了。看完读懂只是第一步,对于需要熟练掌握的技术,深入理解的知识,一个善于学习的人接下来要做的是尝试用自己的语言去表达自己所学到的东西。通过表达可以检验自己是否真的掌握技术、理解知识。如果有条件,最好能去通过演讲的形式去讲给别人。通过与他人交流的方式进一步检验自己的掌握程度。同时还能锻炼口才和交流能力,这是很多技术人员,包括一些顶尖技术人员所缺乏的。
我对于 Netty 来说只是一个初学者。因为产品的特点,我们需要为遗留系统提供更多协议的支持、负载均衡组件需要支持一些老的协议,安全方面也要有全面的掌控,这使得我们要在 TCP 层面开发很多东西,最终 Netty 便成为了我们的选择。
在使用 Netty 的过程中发现,想要用好它并不是很容易,遇到一些奇怪的问题是家常便饭。再加上 Netty 不像 Spring 那样文档详细到可以当书看,所以便开始考虑去看 Netty 的源代码。
另一方面,作为一个在 Java 服务器端开发领域工作了8年的,收入平平的,但尚有理想的一个”老“程序员,我是不甘把完成一些简单的业务功能代码作为一生的工作内容。我想去能够去实现技术复杂的系统是多数有理想的程序员的目标。所以,去理解一个复杂的框架的源代码便成为我的学习计划中的一项。
好了,唠叨了这么多该开始正式内容了。
2. Netty 简介
Netty 是一个 Java NIO 库,基于 Reactor 模式。Reactor 直译为反应器、反应堆。这个翻译容易误导人。其实将 Reactor 意译为分发器更为恰当。Reactor 模式本质上是一个事件机制,通过一个或一组线程检查事件,发现事件之后交由另一组事件处理线程执行该事件所对应的事件处理器(回调),从而实现高响应的程序。
Reactor 模式和 Java NIO 的结合,变为 Java 世界带来了一批高性能 IO 实现,Netty 便是其中一个。当使用 Netty 实现服务器端,Netty 会使用两个线程池,一个线程池用于不断轮询 Selector 上的事件;另一个线程池用于处理 IO 事件。
接下来的内容是基于 Netty 4.0.30。
3. 对学习 Netty 源代码重要性的补充
在 Netty 中,异步调用被大量使用。虽然 Java(包括其它很多语言)写成的程序大部分还是同步调用,但现在为了提高系统响应能力,越来越多的系统开始引入了 Reactor 设计思想和异步风格的代码调用。在语言层面,从 Java 5 开始引入线程池,Java 7 引入 ForkJoin,Java 8 引入了 CompletableFuture 和在 Stream 中加入了异步并行的特性。专注并发的 Scala 和 Go 语言出现并流行。在框架领域,Spring 5 计划引入 spring-reactive (Juergen Hoeller在SpringOne2GX大会上宣布Spring 4.3与5.0的总体规划) 以支持响应式的编程。Akka 的出现。这些都说明了异步和 Reactive 编程越来越重要。通过了解 Netty 的源代码,了解 Netty 的实现原理,我们也可以通过一个被广泛使用的真实技术去了解在 Java 编程领域,异步编程如何被实现和使用。进一步,在我们自己的日常工作中,在服务器端开发上,要经常思考是同步调用还是异步。
3.1 异步编程
可能有人会问异步编程有什么好处?我们先来看同步编程。在同步编程中,一个个方法的执行必须按照先来后到的顺序,后面的方法必须等前面的方法执行完之后才能执行(这里我们就不考虑指令重排)。不管后面的方法是否真的需要等前面方法执行完毕后再执行。同时这些方法也都是在一个线程里执行的,如果前一个方法出问题,抛出异常,后面的方法便无法执行。或者前面的方法耗时过程或者因为某些原因挂起线程,后面的方法也不会执行。
另外,各个方法之间也有轻重缓急之分,有些方法需要被优先执行,有些方法的优先级就比较低。这种情况,不同优先级的方法最好被不同调度逻辑的线程执行。
异步编程虽然会写起来复杂一些,但是可以解决上述这些问题。这对构建一个高可用的服务器端应用是很重要的。
上述原因也导致了各种 Reactive 框架和 Go 等新语言的出现。
总结一下,同步编程对方法的调度产生限制,而异步编程可以打破这些限制,帮助构建可用性和响应度更好的系统。
4. Netty 源码中重要的接口和类
Netty 主要由这么几个主要的组件组成:Channel、ChannelPipeline、ChannelHandler、EventLoopGroup、EventLoop、Bootstrap
4.1 Channel
Channel 是 Netty 中最主要的概念之一。在 Netty 中,有一系列的定义和实现 Channel 概念的接口和类。
如同 JDK 中对 Socket Channel 的定义一样,Netty 中的 Socket Channel 也被分为两大类:SocketChannel
和 ServerSocketChannel
。其中,对于服务器端开发最为常用的 Socket Channel 实现类是 NioSocketChannel
和 NioServerSocketChannel
。下面我们来看这两个类的类图:
NioSocketChannel 类图
NioServerSocketChannel 类图
NioSocketChannel
和 NioServerSocketChannel
的底层实现基于 JDK 的 SocketChannel
和 ServerSocketChannel
。这两个类(其实主要是其父类),通过其 Unsafe
内部类,实现了大量的底层实际操作。例如,AbstractNioChannel
通过其内部类 AbstractNioUnsafe
实现了很多底层的操作。在后面的文章中,我们会经常看到 Unsafe
内部类的源码。
Channel
接口
在 Netty 的 Channel
接口定义了很多的方法。除了大量和 IO 操作相关的方法外,这里重点介绍两个方法:
EventLoop eventLoop();
ChannelPipeline pipeline();
EventLoop eventLoop()
在 Netty 中,大多数 Channel
都有一个 EventLoop
处理它(少数没有 EventLoop
是尚未完成注册工作)。在 NIO 场景中,Channel
通过注册到 Selector
而与一个 NioEventLoop
关联上。这时,当调用到这个 Channel
的 EventLoop eventLoop()
方法时,这个 NioEventLoop
便会被返回。
ChannelPipeline pipeline()
一个 Channel
包含一个 ChannelPipeline
的引用,进而引用了一组 ChannelHandler
。
4.2 ChannelPipeline
ChannelPipeline
类图
在 Netty 中,Channel
是通讯的载体,而 ChannelPipeline
则是数据处理器的载体。虽然 Netty 是基于事件机制实现的,但在 4.0 中并没有名字包含 Event 的接口(在 3.0 版本中存在一个 ChannelEvent
)。ChannelPipeline 上各种事件的传递是直接通过传递数据进行的,或者不包含数据(例如 Channel Active 事件)。
从上图中可以看到,ChannelPipeline
是一个接口,其有一个默认的实现类 DefaultChannelPipeline
。在 DefaultChannelPipeline
中有两个属性:head 和 tail。这两者都扩展了 AbstractChannelHandlerContext
,所以他们都是 ChannelHandlerContext
。同时,它们两个还是 ChannelHandler
。这两者就是 ChannelPipeline
中 ChannelHandler
链的头和尾。后续文章会对此详细介绍。
ChannelPipeline
的 一个重要作用就是承载 ChannelHandler
。向 ChannelPipeline
中添加 ChannelHandler
非常简单,可以使用下列方法:
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(aChannelHandler);
pipeline.addFirst(aChannelHandler);
pipeline.addLast(anEventExecutorGroup, aChannelHandler);
pipeline.addLast("theNameOftheChannelHandler", aChannelHandler);
ChannelPipeline
所提供的添加 ChannelHandler
的方法还有很多,我就不一一列举了,它们的形式和功能大体类似。
ChannelPipeline
可以在运行时动态进行动态修改其中的 ChannelHandler
。
4.3 ChannelHandler
ChannelHandler
是承载业务处理逻辑的地方,是开发人员使用最多的 Netty 接口。ChannelHandler
可分为两大类:ChannelInboundHandler
和 ChannelOutboundHandler
。故名思维,这两接口分别对应入站和出站消息的处理。(Netty 5 不再区分 ChannelInboundHandler
和 ChannelOutboundHandler
)
开发人员通常不直接实现上面提到的三个接口,而通常是继承 ChannelInboundHandlerAdapter
或 ChannelOutboundHandlerAdapter
。
需要注意的是,不建议在 ChannelHandler
中直接实现耗时或阻塞的操作,因为这可能会阻塞 Netty 工作线程,导致 Netty 无法及时响应 IO 处理。
4.4 ChannelHandlerContext
ChannelHandlerContext
是一个接口,被定义用于在 ChannelHandler
的 Inbound 和 Outbound 业务处理方法中传递上下文信息和传播事件。所谓传播事件指的是调用某个方法会导致相应的事件在 ChannelPipeline
的 ChannelHandler
链中按照规定的顺序传播。规定顺序指的是 inbound 事件是从 head -> tail 的顺序传播,outbound 事件是按照 tail -> head 的顺序传播,这在后面会有详细介绍。(Netty 5 还是会区分 inbound 和 outbound 事件)
上下文信息包括 Channel
、EventExecutor
等属性。可触发的事件包括 Channel 的注册、激活、读、写等,这些事件被分为了 inbound 和 outbound 两大类。因为事件比较多,所以不一一列举相关的事件触发方法。简单来说,fire 开头的事件触发方法都是用来触发 inbound 事件,例如 ChannelHandlerContext fireChannelRegistered()
、ChannelHandlerContext fireChannelActive()
和 ChannelHandlerContext fireChannelRead()
等。其它的事件触发方法都是触发 outbound 事件的,例如 ChannelFuture bind(SocketAddress, ChannelPromise)
、ChannelFuture connect(SocketAddress, SocketAddress, ChannelPromise)
、ChannelFuture write(Object, ChannelPromise)
。关于这些事件传播方法的完整介绍可以在 ChannelHandlerContext
和 ChannelPipeline
的 Javadoc 中找到。
需要注意的一点是在 inbound 和 outbound 事件中都有 read,这是容易让初学者困惑的地方(至少我困惑了)。Inbound 事件中的 read 事件所对应的方法为 fireChannelRead(Object)
,用于读到的消息继续在 ChannelHandler
链中传播。Outbound 事件中的 read 所对应的方法为 read()
,用于将当前 Channel
设置为可读消息的状态。如果这个 Channel
为 AbstractNioChannel
时,即调用 selectionKey.interestOps(interestOps | readInterestOp)
。
ChannelHandlerContext
是 Netty 中一个很重要的概念,正确理解这个接口所定义的方法的使用,深入理解该接口实现类的原理对于正确使用 Netty 至关重要。
4.5 ChannelFuture & Promise
Netty 源码中大量使用了异步编程,从代码实现角度看就是大量使用了线程池和 Future
。对于 Netty 的线程池部分我们稍后介绍,这里先介绍 Future
。熟悉 Java 5 的同学一定对 Future
不陌生。简单来说就是其代表了一个异步任务,任务将在未来某个时刻完成,而 Future
这个接口就是用来提供例如获取接口、查看任务状态等功能。
Netty 扩展了 Java 5 引入的 Future 机制。从下面的类图我们可以看到相关类的关系:
DefaultChannelPromise
类图
4.5.1 Netty 的 Future
接口
需要注意的是,上面类图中有两个 Future
,最上面的是 java.util.concurrent.Future
,而其下面的则是 io.netty.util.concurrent.Future
。后面如果提到 Future
,不加说明,指的就是 Netty 中的 Future
。Netty 的 Future
对 JDK Future
的一个主要改变是增加了用于表示任务是否成功的方法 —— boolean isSuccess()
。因为 JDK Future
的 boolean isDone()
方法不区分任务是正常结束,还是因为异常或被取消。ChannelFuture
的 Javadoc 详细说明了任务未完成、正常完成、异常完成和被取消四种状态下相关方法的返回值,这里就不重复介绍了。
除了 boolean isSuccess()
方法外,Future
还定义了下列方法:
Future<V> addListener(GenericFutureListener<? extends Future<? super V>> listener)
Future<V> addListeners(GenericFutureListener<? extends Future<? super V>>... listeners)
Future<V> removeListener(GenericFutureListener<? extends Future<? super V>> listener)
Future<V> removeListeners(GenericFutureListener<? extends Future<? super V>>... listeners)
Future<V> await() throws InterruptedException
Future<V> awaitUninterruptibly()
Future<V> sync() throws InterruptedException
Future<V> syncUninterruptibly()
前四个方法是顾名思义,是用来添加删除 GenericFutureListener
,这里就不过多做介绍了。本系列后续文章会提到某些实现了对这几个方法的实现。
这里稍微说明一下 await()
和 sync()
这两个用于线程同步的方法。简单来说 sync()
就是会抛出异常的 await()
。
4.5.2 Promise
什么是 Promise?
用 Javascript 的同学可能对 Promise 这个词不陌生,但是 Java 程序员刚看到这个词可能会丈二和尚摸不着头脑。所以这里我先简单介绍一下我对 Promise 的理解。
从尚在制定中的 ECMAScript 6 引入 Promise 机制,Promise 是一个用来定义异步调用链的东东。举个栗子:
Promise.all([
fetchPromised("http://backend/foo.txt", 500),
fetchPromised("http://backend/bar.txt", 500),
fetchPromised("http://backend/baz.txt", 500)
]).then((data) => {
let [ foo, bar, baz ] = data
console.log(`success: foo=${foo} bar=${bar} baz=${baz}`)
}, (err) => {
console.log(`error: ${err}`)
})
在 Java 8 中被引入的 CompletableFuture
也有类似的 API,这在我博客里的一篇阅读总结中有提到。在这篇阅读总结里面,我所读到的关于 CompletableFuture
文章里有一句对 Promise 的总结我觉得很恰当 —— “future without underlying task or thread pool”。但这句话说的还不是太明白。我的理解是 Promise 不想 Future 那样代表一个已经提交的任务。Promise 反映的是对未来的“允诺”,即它反映了将来会被提交的异步任务的行为。虽然现在这个任务尚未被提交,刚未开始执行。从代码的形式上看,Promise 机制提供了一些 API 用来编排组织异步任务。
但是 Netty 的 Promise
和上面提到的 Javascript 和 Java 8 的 Promise 不太一样。Netty 中的 Promise
,如同它的 Javadoc 里的解释一样,是一个可写的 Future。这个还不是太好理解,但是看完我对 Netty 回调机制便会明白。
回调
在介绍 Future
接口的时候我们提到了其定义了添加和删除 GenericFutureListener
的方法,那这些监听器在添加之后如何被调用呢。这就涉及到了 Promise
所定义的方法了:
/**
* Marks this future as a success and notifies all
* listeners.
*
* If it is success or failed already it will throw an {@link IllegalStateException}.
*/
Promise<V> setSuccess(V result);
/**
* Marks this future as a success and notifies all
* listeners.
*
* @return {@code true} if and only if successfully marked this future as
* a success. Otherwise {@code false} because this future is
* already marked as either a success or a failure.
*/
boolean trySuccess(V result);
从其 Javadoc 就可以看出,这两个方法不仅会标记任务成功完成,同时还会通知所有的监听器。在本系列后续文章会介绍其实现类是具体如何实现通知功能的。
4.5.3 ChannelFuture
除了 Promise
,Netty 中另一个继承自 Future
的接口是 ChannelFuture
。这个接口增加的内容不多,和 Future
相比只是增加了 Channel channel()
方法,用来返回与之对应的 Channel
。
4.6 EventLoop & EventLoopGroup
NioEventLoop
和 NioEventLoopGroup
是常用的 EventLoop
和 EventLoopGroup
的实现类。当我们使用 ServerBootstrap
去构建 Netty 服务端组件的时候,我们通常会这么写:
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup);
NioEventLoop
没有在上面的代码中体现出来,但实际也是被使用了的。所以我们从这两个实现类来简单了解一下相关的接口和类。
4.6.1 NioEventLoopGroup
我们先从 NioEventLoopGroup
开始,先来看看类图:
NioEventLoopGroup
类图
从上图中我们可以看出,Netty 的 EventLoopGroup
是通过继承 EventExecutorGroup
接口继承了 JDK 的 Iterable
和 ScheduledExecutorService
接口,即它是一个可以被迭代的可调度线程池服务。EventExecutorGroup
所增加的方法主要用于任务调度,这里不做介绍。EventLoopGroup
中定义了三个新方法:
EventLoop next();
ChannelFuture register(Channel channel);
ChannelFuture register(Channel channel, ChannelPromise promise);
接下来分别对它们做一下解释
EventLoop next()
其实这个方法是覆写了其父接口中的方法,但是我们在这里介绍一下。
EventLoopGroup
顾名思义是 EventLoop
的 group,即包含了一组 EventGroup
。在实际的业务处理中,EventLoopGroup
会通过 EventLoop next()
方法选择一个 EventLoop
,然后将实际的业务处理交给这个被选出的 EventLoop
去做。
对于 NioEventLoopGroup
来说,其真实功能都会交给 EventLoopGroup
去实现。
ChannelFuture register(Channel channel)
此方法的功能是将一个 Channel
注册到这个 EventLoopGroup
上。这样,当和这个 Channel
相关的事件发生时,EventLoopGroup
就能进行处理。从这个方法的返回值可以看出,这是一个异步方法。
ChannelFuture register(Channel channel, ChannelPromise promise)
此方法同上面的方法类似,只是增加了对回调功能的支持。
4.6.2 NioEventLoop
我们依旧先来看类图:
NioEventLoop 类图
从上图可以看出,EventLoop
继承自 EventLoopGroup
,即 EventLoop
也是一种 EventLoopGroup
。这样可以形成多层级的 EventLoopGroup
。同样,EventLoop
和 EventLoopGroup
彼此的父接口 EventExecutor
和 EventExecutorGroup
也是这样的关系。
因为同 EventExecutor
相比,EventLoop
几乎没有新加方法(只是覆写了一个方法),所以我们这里就只看 EventExecutor
里新定义的方法
EventExecutor
EventExecutorGroup parent();
boolean inEventLoop();
boolean inEventLoop(Thread thread);
<V> Promise<V> newPromise();
<V> ProgressivePromise<V> newProgressivePromise();
<V> Future<V> newSucceededFuture(V result);
<V> Future<V> newFailedFuture(Throwable cause);
这里说一下 boolean inEventLoop()
。其它的方法 EventExecutorGroup parent()
顾名思义就好了,不用怎么解释。几个 newXXX()
方法通常的实现就是 new 一个相应的对象,所以这里也不做介绍了。
boolean inEventLoop()
和 boolean inEventLoop(Thread thread)
方法在 Netty 源码中的被经常使用。当 Netty 准备执行一个异步任务,即准备向 EventLoop
提交一个新任务的时候,会先调用 boolean inEventLoop()
方法判断当前线程(提交任务的线程,而非执行任务的线程)是否属于 EventLoop
的执行线程。如果是,通常的做法就是直接调用,否则才是调用 EventLoop
的 execute
之类的方法提交任务。
NioEventLoop
这个实现类实现了很多具体的工作。我们前面提到 EventLoopGroup
的 ChannelFuture register(Channel channel)
方法,在 NioEventLoop
的实现中便是将 Channel
注册到 NioEventLoop
中的 Selector
上(实际工程不是这么简单直接,在后续文章会详细介绍)
4.6.3 EventLoopGroup 的其它一些事儿
以 NioEventLoopGroup
为例,其中的每个线程都有自己的任务队列。和 JDK 的 ThreadPoolExecutor
和 ScheduledThreadPoolExecutor
线程池实现不同,Netty 的 NioEventLoopGroup
每一个执行线程都有自己的任务队列,而不是像 JDK 的线程池那样,执行线程共享一个任务队列(ForkJoinPool
除外,其每个执行线程都有自己的任务队列,并可以从其它执行线程的任务队列获取任务,即工作盗取 Work stealing 算法)。这种方式避免了线程之间为获取任务而产生的竞争和线程同步带来的开销。
在 Netty 中,一个 ChannelHandler
实例从始至终都是被一个线程调用。所以一个 ChannelHandler
实例肯定也不会被多个线程同时调用。这避免了内存可见性等的问题,简化了线程安全方面的设计。(Netty 5 目前不保证 ChannelHandler
实例会始终被一个线程调用,但是保证不会被多个线程同时调用)
在 Netty 4 和 5 中,Inbound 和 outbound 事件处理都是在工作线程中执行的。(Netty 3 中 outbound 会在用户发起的线程中执行)
5. 调用关系
在了解完 Netty 中主要接口和类的功能和作用之后,我们来看一下它们直接是如何协调配合的。下面是一个简化过的,当有 IO 事件发生时的时序图,反映了 Netty 接口和类之间大致的调用关系。
Netty 简化调用关系图
简单来说,当有 IO 事件发生时,NioEventLoop
会通过不断轮询 Selector
获得相应的 SelectionKey
。进而通过,SelectionKey
的 Object attachment()
方法获得底层连接对应的对象,通常是一个 AbstractNioChannel
。进而调用 Channel
上相应的方法。然后是调用 ChannelPipeline
上的事件触发方法,最终调用到用户自定义的 ChannelHandler
上。
6. 小结
写到这里,啰啰嗦嗦地把 Netty 简单介绍了一下。介绍这么多代码层面的东西是为了后续正是开始介绍 Netty 的源码做铺垫。
其实 OSChina、ITeye 等国内的技术社区已经有很多篇介绍 Netty 源码的文章。我在这里继续造车轮的原因主要是为了自己更好的学习 Netty,同时也为了能和大家交流。同时,我希望我的文章能够补全一些别的文章没涉及的内容,包含一些我的理解和想法。
软件和其它技术一样,最核心、最有价值的部分是思想。通过阅读源码,在帮助更好地使用软件、设计软件的同时,理解、吸收好的思想,并产生自己的有价值的思想,这才是最重要的。
推荐阅读
-
深入理解Netty(第二部分):关键接口与类的剖析与解读
-
【2022新手指南】Java编程进阶之路 - 六、技术架构篇 ### MySQL索引底层解析与优化实战 - 你会讲解MySQL索引的数据结构吗?性能调优技巧知多少? - Redis深度揭秘:你知道多少?从基础到哨兵、主从复制全梳理 - Redis持久化及哨兵模式详解,还有集群搭建和Leader选举黑箱打开 - Zookeeper是个啥?特性和应用场景大公开 - ZooKeeper集群搭建攻略及 Leader选举、读写一致性、共享锁实现细节 - 探究ZooKeeper中的Leader选举机制及其在分布式环境中的作用 - Zab协议深入剖析:原理、功能与在Zookeeper中的核心地位 - RabbitMQ全方位解读:工作模式、消费限流、可靠投递与配置策略 - 设计者视角:RabbitMQ过期时间、死信队列与延时队列实践指南 - RocketMQ特性和应用场景揭示:理解其精髓与差异化优势 - Kafka详细介绍:特性及广泛应用于实时数据处理的场景解析 - ElasticSearch实力揭秘:特性概述与作为搜索引擎的广泛应用 - MongoDB认知升级:非关系型数据库的优势阐述,安装与使用实战教学 - BIO/NIO/AIO网络模型对比:掌握它们的区别与在网络编程中的实际应用 - Netty带你飞:理解其超快速度背后的秘密,包括线程模型分析 - 网络通信黑科技:Netty编解码原理与常用编解码器的应用,Protostuff实战演示 - 解密Netty粘包与拆包现象,怎样有效应对这一常见问题 - 自定义Netty心跳检测机制,轻松调整检测间隔时间的艺术 - Dubbo轻骑兵介绍:核心特性概览,服务降级实战与其实现益处 - Dubbo三大神器解读:本地存根与本地伪装的实战运用与优势呈现 ----------------------- 七、结语与回顾
-
SSM三大框架基础面试题-一、Spring篇 什么是Spring框架? Spring是一种轻量级框架,提高开发人员的开发效率以及系统的可维护性。 我们一般说的Spring框架就是Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发。这些模块是核心容器、数据访问/集成、Web、AOP(面向切面编程)、工具、消息和测试模块。比如Core Container中的Core组件是Spring所有组件的核心,Beans组件和Context组件是实现IOC和DI的基础,AOP组件用来实现面向切面编程。 Spring的6个特征: 核心技术:依赖注入(DI),AOP,事件(Events),资源,i18n,验证,数据绑定,类型转换,SpEL。 测试:模拟对象,TestContext框架,Spring MVC测试,WebTestClient。 数据访问:事务,DAO支持,JDBC,ORM,编组XML。 Web支持:Spring MVC和Spring WebFlux Web框架。 集成:远程处理,JMS,JCA,JMX,电子邮件,任务,调度,缓存。 语言:Kotlin,Groovy,动态语言。 列举一些重要的Spring模块? Spring Core:核心,可以说Spring其他所有的功能都依赖于该类库。主要提供IOC和DI功能。 Spring Aspects:该模块为与AspectJ的集成提供支持。 Spring AOP:提供面向切面的编程实现。 Spring JDBC:Java数据库连接。 Spring JMS:Java消息服务。 Spring ORM:用于支持Hibernate等ORM工具。 Spring Web:为创建Web应用程序提供支持。 Spring Test:提供了对JUnit和TestNG测试的支持。 谈谈自己对于Spring IOC和AOP的理解 IOC(Inversion Of Controll,控制反转)是一种设计思想: 在程序中手动创建对象的控制权,交由给Spring框架来管理。IOC在其他语言中也有应用,并非Spring特有。IOC容器实际上就是一个Map(key, value),Map中存放的是各种对象。 将对象之间的相互依赖关系交给IOC容器来管理,并由IOC容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。IOC容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。在实际项目中一个Service类可能由几百甚至上千个类作为它的底层,假如我们需要实例化这个Service,可能要每次都搞清楚这个Service所有底层类的构造函数,这可能会把人逼疯。如果利用IOC的话,你只需要配置好,然后在需要的地方引用就行了,大大增加了项目的可维护性且降低了开发难度。 Spring中的bean的作用域有哪些? 1.singleton:该bean实例为单例 2.prototype:每次请求都会创建一个新的bean实例(多例)。 3.request:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP request内有效。 4.session:每一次HTTP请求都会产生一个新的bean,该bean仅在当前HTTP session内有效。 5.global-session:全局session作用域,仅仅在基于Portlet的Web应用中才有意义,Spring5中已经没有了。Portlet是能够生成语义代码(例如HTML)片段的小型Java Web插件。它们基于Portlet容器,可以像Servlet一样处理HTTP请求。但是与Servlet不同,每个Portlet都有不同的会话。 Spring中的单例bean的线程安全问题了解吗? 概念用于理解:大部分时候我们并没有在系统中使用多线程,所以很少有人会关注这个问题。单例bean存在线程问题,主要是因为当多个线程操作同一个对象的时候,对这个对象的非静态成员变量的写操作会存在线程安全问题。 有两种常见的解决方案(用于回答的点): 1.在bean对象中尽量避免定义可变的成员变量(不太现实)。 2.在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在ThreadLocal(线程本地化对象)中(推荐的一种方式)。 ThreadLocal解决多线程变量共享问题(参考博客):https://segmentfault.com/a/1190000009236777 Spring中Bean的生命周期: 1.Bean容器找到配置文件中Spring Bean的定义。 2.Bean容器利用Java Reflection API创建一个Bean的实例。 3.如果涉及到一些属性值,利用set方法设置一些属性值。 4.如果Bean实现了BeanNameAware接口,调用setBeanName方法,传入Bean的名字。 5.如果Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader方法,传入ClassLoader对象的实例。 6.如果Bean实现了BeanFactoryAware接口,调用setBeanClassFacotory方法,传入ClassLoader对象的实例。 7.与上面的类似,如果实现了其他*Aware接口,就调用相应的方法。 8.如果有和加载这个Bean的Spring容器相关的BeanPostProcessor对象,执postProcessBeforeInitialization方法。 9.如果Bean实现了InitializingBean接口,执行afeterPropertiesSet方法。 10.如果Bean在配置文件中的定义包含init-method属性,执行指定的方法。 11.如果有和加载这个Bean的Spring容器相关的BeanPostProcess对象,执行postProcessAfterInitialization方法。 12.当要销毁Bean的时候,如果Bean实现了DisposableBean接口,执行destroy方法。 13.当要销毁Bean的时候,如果Bean在配置文件中的定义包含destroy-method属性,执行指定的方法。 Spring框架中用到了哪些设计模式? 1.工厂设计模式:Spring使用工厂模式通过BeanFactory和ApplicationContext创建bean对象。 2.代理设计模式:Spring AOP功能的实现。 3.单例设计模式:Spring中的bean默认都是单例的。 4.模板方法模式:Spring中的jdbcTemplate、hibernateTemplate等以Template结尾的对数据库操作的类,它们就使用到了模板模式。 5.包装器设计模式:我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。 6.观察者模式:Spring事件驱动模型就是观察者模式很经典的一个应用。 7.适配器模式:Spring AOP的增强或通知(Advice)使用到了适配器模式、Spring MVC中也是用到了适配器模式适配Controller。 还有很多。。。。。。。 @Component和@Bean的区别是什么 1.作用对象不同。@Component注解作用于类,而@Bean注解作用于方法。 2.@Component注解通常是通过类路径扫描来自动侦测以及自动装配到Spring容器中(我们可以使用@ComponentScan注解定义要扫描的路径)。@Bean注解通常是在标有该注解的方法中定义产生这个bean,告诉Spring这是某个类的实例,当我需要用它的时候还给我。 3.@Bean注解比@Component注解的自定义性更强,而且很多地方只能通过@Bean注解来注册bean。比如当引用第三方库的类需要装配到Spring容器的时候,就只能通过@Bean注解来实现。 @Configuration public class AppConfig { @Bean public TransferService transferService { return new TransferServiceImpl; } } <beans> <bean id="transferService" class="com.kk.TransferServiceImpl"/> </beans> @Bean public OneService getService(status) { case (status) { when 1: return new serviceImpl1; when 2: return new serviceImpl2; when 3: return new serviceImpl3; } } 将一个类声明为Spring的bean的注解有哪些? 声明bean的注解: @Component 组件,没有明确的角色 @Service 在业务逻辑层使用(service层) @Repository 在数据访问层使用(dao层) @Controller 在展现层使用,控制器的声明 注入bean的注解: @Autowired:由Spring提供 @Inject:由JSR-330提供 @Resource:由JSR-250提供 *扩:JSR 是 java 规范标准 Spring事务管理的方式有几种? 1.编程式事务:在代码中硬编码(不推荐使用)。 2.声明式事务:在配置文件中配置(推荐使用),分为基于XML的声明式事务和基于注解的声明式事务。 Spring事务中的隔离级别有哪几种? 在TransactionDefinition接口中定义了五个表示隔离级别的常量:ISOLATION_DEFAULT:使用后端数据库默认的隔离级别,Mysql默认采用的REPEATABLE_READ隔离级别;Oracle默认采用的READ_COMMITTED隔离级别。ISOLATION_READ_UNCOMMITTED:最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。ISOLATION_READ_COMMITTED:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生ISOLATION_REPEATABLE_READ:对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。ISOLATION_SERIALIZABLE:最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。 Spring事务中有哪几种事务传播行为? 在TransactionDefinition接口中定义了八个表示事务传播行为的常量。 支持当前事务的情况:PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。PROPAGATION_MANDATORY: 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)。 不支持当前事务的情况:PROPAGATION_REQUIRES_NEW: 创建一个新的事务,如果当前存在事务,则把当前事务挂起。PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。 其他情况:PROPAGATION_NESTED: 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于PROPAGATION_REQUIRED。 二、SpringMVC篇 什么是Spring MVC ?简单介绍下你对springMVC的理解? Spring MVC是一个基于Java的实现了MVC设计模式的请求驱动类型的轻量级Web框架,通过把Model,View,Controller分离,将web层进行职责解耦,把复杂的web应用分成逻辑清晰的几部分,简化开发,减少出错,方便组内开发人员之间的配合。 Spring MVC的工作原理了解嘛? image.png Springmvc的优点: (1)可以支持各种视图技术,而不仅仅局限于JSP; (2)与Spring框架集成(如IoC容器、AOP等); (3)清晰的角色分配:前端控制器(dispatcherServlet) , 请求到处理器映射(handlerMapping), 处理器适配器(HandlerAdapter), 视图解析器(ViewResolver)。 (4) 支持各种请求资源的映射策略。 Spring MVC的主要组件? (1)前端控制器 DispatcherServlet(不需要程序员开发) 作用:接收请求、响应结果,相当于转发器,有了DispatcherServlet 就减少了其它组件之间的耦合度。 (2)处理器映射器HandlerMapping(不需要程序员开发) 作用:根据请求的URL来查找Handler (3)处理器适配器HandlerAdapter 注意:在编写Handler的时候要按照HandlerAdapter要求的规则去编写,这样适配器HandlerAdapter才可以正确的去执行Handler。 (4)处理器Handler(需要程序员开发) (5)视图解析器 ViewResolver(不需要程序员开发) 作用:进行视图的解析,根据视图逻辑名解析成真正的视图(view) (6)视图View(需要程序员开发jsp) View是一个接口, 它的实现类支持不同的视图类型(jsp,freemarker,pdf等等) springMVC和struts2的区别有哪些? (1)springmvc的入口是一个servlet即前端控制器(DispatchServlet),而struts2入口是一个filter过虑器(StrutsPrepareAndExecuteFilter)。 (2)springmvc是基于方法开发(一个url对应一个方法),请求参数传递到方法的形参,可以设计为单例或多例(建议单例),struts2是基于类开发,传递参数是通过类的属性,只能设计为多例。 (3)Struts采用值栈存储请求和响应的数据,通过OGNL存取数据,springmvc通过参数解析器是将request请求内容解析,并给方法形参赋值,将数据和视图封装成ModelAndView对象,最后又将ModelAndView中的模型数据通过reques域传输到页面。Jsp视图解析器默认使用jstl。 SpringMVC怎么样设定重定向和转发的? (1)转发:在返回值前面加"forward:",譬如"forward:user.do?name=method4" (2)重定向:在返回值前面加"redirect:",譬如"redirect:http://www.baidu.com" SpringMvc怎么和AJAX相互调用的? 通过Jackson框架就可以把Java里面的对象直接转化成Js可以识别的Json对象。具体步骤如下 : (1)加入Jackson.jar (2)在配置文件中配置json的映射 (3)在接受Ajax方法里面可以直接返回Object,List等,但方法前面要加上@ResponseBody注解。 如何解决POST请求中文乱码问题,GET的又如何处理呢? (1)解决post请求乱码问题: 在web.xml中配置一个CharacterEncodingFilter过滤器,设置成utf-8; <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> (2)get请求中文参数出现乱码解决方法有两个: ①修改tomcat配置文件添加编码与工程编码一致,如下: <ConnectorURIEncoding="utf-8" connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/> ②另外一种方法对参数进行重新编码: String userName = new String(request.getParamter("userName").getBytes("ISO8859-1"),"utf-8") ISO8859-1是tomcat默认编码,需要将tomcat编码后的内容按utf-8编码。 Spring MVC的异常处理 ? 统一异常处理: Spring MVC处理异常有3种方式: (1)使用Spring MVC提供的简单异常处理器SimpleMappingExceptionResolver; (2)实现Spring的异常处理接口HandlerExceptionResolver 自定义自己的异常处理器; (3)使用@ExceptionHandler注解实现异常处理; 统一异常处理的博客:https://blog.csdn.net/ctwy291314/article/details/81983103 SpringMVC的控制器是不是单例模式,如果是,有什么问题,怎么解决? 是单例模式,所以在多线程访问的时候有线程安全问题,不要用同步,会影响性能的,解决方案是在控制器里面不能写成员变量。(此题目类似于上面Spring 中 第5题 有两种解决方案) SpringMVC常用的注解有哪些? @RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用于类上,则表示类中的所有响应请求的方法都是以该地址作为父路径。 @RequestBody:注解实现接收http请求的json数据,将json转换为java对象。 @ResponseBody:注解实现将conreoller方法返回对象转化为json对象响应给客户。 SpingMvc中的控制器的注解一般用那个,有没有别的注解可以替代? 一般用@Controller注解,也可以使用@RestController,@RestController注解相当于@ResponseBody + @Controller,表示是表现层,除此之外,一般不用别的注解代替。 如果在拦截请求中,我想拦截get方式提交的方法,怎么配置? 可以在@RequestMapping注解里面加上method=RequestMethod.GET。 怎样在方法里面得到Request,或者Session? 直接在方法的形参中声明request,SpringMVC就自动把request对象传入。 如果想在拦截的方法里面得到从前台传入的参数,怎么得到? 直接在形参里面声明这个参数就可以,但必须名字和传过来的参数一样。 如果前台有很多个参数传入,并且这些参数都是一个对象的,那么怎么样快速得到这个对象? 直接在方法中声明这个对象,SpringMVC就自动会把属性赋值到这个对象里面。 SpringMVC中函数的返回值是什么? 返回值可以有很多类型,有String, ModelAndView。ModelAndView类把视图和数据都合并的一起的。 SpringMVC用什么对象从后台向前台传递数据的? 通过ModelMap对象,可以在这个对象里面调用put方法,把对象加到里面,前台就可以拿到数据。 怎么样把ModelMap里面的数据放入Session里面? 可以在类上面加上@SessionAttributes注解,里面包含的字符串就是要放入session里面的key。 SpringMvc里面拦截器是怎么写的: 有两种写法,一种是实现HandlerInterceptor接口,另外一种是继承适配器类,接着在接口方法当中,实现处理逻辑;然后在SpringMvc的配置文件中配置拦截器即可: <!-- 配置SpringMvc的拦截器 --> <mvc:interceptors> <!-- 配置一个拦截器的Bean就可以了 默认是对所有请求都拦截 --> <bean id="myInterceptor" class="com.zwp.action.MyHandlerInterceptor"></bean> <!-- 只针对部分请求拦截 --> <mvc:interceptor> <mvc:mapping path="/modelMap.do" /> <bean class="com.zwp.action.MyHandlerInterceptorAdapter" /> </mvc:interceptor> </mvc:interceptors> 注解原理: 注解本质是一个继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象。通过代理对象调用自定义注解的方法,会最终调用AnnotationInvocationHandler的invoke方法。该方法会从memberValues这个Map中索引出对应的值。而memberValues的来源是Java常量池 三、Mybatis篇 什么是MyBatis? MyBatis是一个可以自定义SQL、存储过程和高级映射的持久层框架。 讲下MyBatis的缓存 MyBatis的缓存分为一级缓存和二级缓存,一级缓存放在session里面,默认就有, 二级缓存放在它的命名空间里,默认是不打开的,使用二级缓存属性类需要实现Serializable序列化接口, 可在它的映射文件中配置<cache/> Mybatis是如何进行分页的?分页插件的原理是什么? 1)Mybatis使用RowBounds对象进行分页,也可以直接编写sql实现分页,也可以使用Mybatis的分页插件。 2)分页插件的原理:实现Mybatis提供的接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql。 举例:select * from student,拦截sql后重写为:select t.* from (select * from student)t limit 0,10 简述Mybatis的插件运行原理,以及如何编写一个插件? 1)Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、 Executor这4种接口的插件,Mybatis通过动态代理, 为需要拦截的接口生成代理对象以实现接口方法拦截功能, 每当执行这4种接口对象的方法时,就会进入拦截方法, 具体就是InvocationHandler的invoke方法,当然, 只会拦截那些你指定需要拦截的方法。 2)实现Mybatis的Interceptor接口并复写intercept方法, 然后在给插件编写注解,指定要拦截哪一个接口的哪些方法即可, 记住,别忘了在配置文件中配置你编写的插件。 Mybatis动态sql是做什么的?都有哪些动态sql?能简述一下动态sql的执行原理不? 1)Mybatis动态sql可以让我们在Xml映射文件内, 以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能。 2)Mybatis提供了9种动态sql标签:trim|where|set|foreach|if|choose|when|otherwise|bind。 3)其执行原理为,使用OGNL从sql参数对象中计算表达式的值, 根据表达式的值动态拼接sql,以此来完成动态sql的功能。 #{}和${}的区别是什么? 1)#{}是预编译处理,${}是字符串替换。 2)Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值(有效的防止SQL注入); 3)Mybatis在处理${}时,就是把${}替换成变量的值。 为什么说Mybatis是半自动ORM映射工具?它与全自动的区别在哪里? Hibernate属于全自动ORM映射工具, 使用Hibernate查询关联对象或者关联集合对象时, 可以根据对象关系模型直接获取,所以它是全自动的。 而Mybatis在查询关联对象或关联集合对象时, 需要手动编写sql来完成,所以,称之为半自动ORM映射工具。 Mybatis是否支持延迟加载?如果支持,它的实现原理是什么? 1)Mybatis仅支持association关联对象和collection关联集合对象的延迟加载, association指的就是一对一,collection指的就是一对多查询。 在Mybatis配置文件中, 可以配置是否启用延迟加载lazyLoadingEnabled=true|false。 2)它的原理是,使用CGLIB创建目标对象的代理对象, 当调用目标方法时,进入拦截器方法, 比如调用a.getB.getName, 拦截器invoke方法发现a.getB是null值, 那么就会单独发送事先保存好的查询关联B对象的sql, 把B查询上来,然后调用a.setB(b), 于是a的对象b属性就有值了, 接着完成a.getB.getName方法的调用。 这就是延迟加载的基本原理。 MyBatis与Hibernate有哪些不同? 1)Mybatis和hibernate不同,它不完全是一个ORM框架, 因为MyBatis需要程序员自己编写Sql语句, 不过mybatis可以通过XML或注解方式灵活配置要运行的sql语句, 并将java对象和sql语句映射生成最终执行的sql, 最后将sql执行的结果再映射生成java对象。 2)Mybatis学习门槛低,简单易学,程序员直接编写原生态sql, 可严格控制sql执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发, 例如互联网软件、企业运营类软件等,因为这类软件需求变化频繁, 一但需求变化要求成果输出迅速。但是灵活的前提是mybatis无法做到数据库无关性, 如果需要实现支持多种数据库的软件则需要自定义多套sql映射文件,工作量大。 3)Hibernate对象/关系映射能力强,数据库无关性好, 对于关系模型要求高的软件(例如需求固定的定制化软件) 如果用hibernate开发可以节省很多代码,提高效率。 但是Hibernate的缺点是学习门槛高,要精通门槛更高, 而且怎么设计O/R映射,在性能和对象模型之间如何权衡, 以及怎样用好Hibernate需要具有很强的经验和能力才行。 总之,按照用户的需求在有限的资源环境下只要能做出维护性、 扩展性良好的软件架构都是好架构,所以框架只有适合才是最好。 MyBatis的好处是什么? 1)MyBatis把sql语句从Java源程序中独立出来,放在单独的XML文件中编写, 给程序的维护带来了很大便利。 2)MyBatis封装了底层JDBC API的调用细节,并能自动将结果集转换成Java Bean对象, 大大简化了Java数据库编程的重复工作。 3)因为MyBatis需要程序员自己去编写sql语句, 程序员可以结合数据库自身的特点灵活控制sql语句, 因此能够实现比Hibernate等全自动orm框架更高的查询效率,能够完成复杂查询。 简述Mybatis的Xml映射文件和Mybatis内部数据结构之间的映射关系? Mybatis将所有Xml配置信息都封装到All-In-One重量级对象Configuration内部。 在Xml映射文件中,<parameterMap>标签会被解析为ParameterMap对象, 其每个子元素会被解析为ParameterMapping对象。 <resultMap>标签会被解析为ResultMap对象, 其每个子元素会被解析为ResultMapping对象。 每一个<select>、<insert>、<update>、<delete> 标签均会被解析为MappedStatement对象, 标签内的sql会被解析为BoundSql对象。 什么是MyBatis的接口绑定,有什么好处? 接口映射就是在MyBatis中任意定义接口,然后把接口里面的方法和SQL语句绑定, 我们直接调用接口方法就可以,这样比起原来了SqlSession提供的方法我们可以有更加灵活的选择和设置. 接口绑定有几种实现方式,分别是怎么实现的? 接口绑定有两种实现方式,一种是通过注解绑定,就是在接口的方法上面加 上@Select@Update等注解里面包含Sql语句来绑定, 另外一种就是通过xml里面写SQL来绑定,在这种情况下, 要指定xml映射文件里面的namespace必须为接口的全路径名. 什么情况下用注解绑定,什么情况下用xml绑定? 当Sql语句比较简单时候,用注解绑定;当SQL语句比较复杂时候,用xml绑定,一般用xml绑定的比较多 MyBatis实现一对一有几种方式?具体怎么操作的? 有联合查询和嵌套查询,联合查询是几个表联合查询,只查询一次, 通过在resultMap里面配置association节点配置一对一的类就可以完成; 嵌套查询是先查一个表,根据这个表里面的结果的外键id, 去再另外一个表里面查询数据,也是通过association配置, 但另外一个表的查询通过select属性配置。 Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别? 能,Mybatis不仅可以执行一对一、一对多的关联查询, 还可以执行多对一,多对多的关联查询,多对一查询, 其实就是一对一查询,只需要把selectOne修改为selectList即可; 多对多查询,其实就是一对多查询,只需要把selectOne修改为selectList即可。 关联对象查询,有两种实现方式,一种是单独发送一个sql去查询关联对象, 赋给主对象,然后返回主对象。另一种是使用嵌套查询,嵌套查询的含义为使用join查询, 一部分列是A对象的属性值,另外一部分列是关联对象B的属性值, 好处是只发一个sql查询,就可以把主对象和其关联对象查出来。 MyBatis里面的动态Sql是怎么设定的?用什么语法? MyBatis里面的动态Sql一般是通过if节点来实现,通过OGNL语法来实现, 但是如果要写的完整,必须配合where,trim节点,where节点是判断包含节点有 内容就插入where,否则不插入,trim节点是用来判断如果动态语句是以and 或or 开始,那么会自动把这个and或者or取掉。 Mybatis是如何将sql执行结果封装为目标对象并返回的?都有哪些映射形式? 第一种是使用<resultMap>标签,逐一定义列名和对象属性名之间的映射关系。 第二种是使用sql列的别名功能,将列别名书写为对象属性名, 比如T_NAME AS NAME,对象属性名一般是name,小写, 但是列名不区分大小写,Mybatis会忽略列名大小写,
-
windows下进程间通信的(13种方法)-摘 要 本文讨论了进程间通信与应用程序间通信的含义及相应的实现技术,并对这些技术的原理、特性等进行了深入的分析和比较。 ---- 关键词 信号 管道 消息队列 共享存储段 信号灯 远程过程调用 Socket套接字 MQSeries 1 引言 ---- 进程间通信的主要目的是实现同一计算机系统内部的相互协作的进程之间的数据共享与信息交换,由于这些进程处于同一软件和硬件环境下,利用操作系统提供的的编程接口,用户可以方便地在程序中实现这种通信;应用程序间通信的主要目的是实现不同计算机系统中的相互协作的应用程序之间的数据共享与信息交换,由于应用程序分别运行在不同计算机系统中,它们之间要通过网络之间的协议才能实现数据共享与信息交换。进程间通信和应用程序间通信及相应的实现技术有许多相同之处,也各有自己的特色。即使是同一类型的通信也有多种的实现方法,以适应不同情况的需要。 ---- 为了充分认识和掌握这两种通信及相应的实现技术,本文将就以下几个方面对这两种通信进行深入的讨论:问题的由来、解决问题的策略和方法、每种方法的工作原理和实现、每种实现方法的特点和适用的范围等。 2 进程间的通信及其实现技术 ---- 用户提交给计算机的任务最终都是通过一个个的进程来完成的。在一组并发进程中的任何两个进程之间,如果都不存在公共变量,则称该组进程为不相交的。在不相交的进程组中,每个进程都独立于其它进程,它的运行环境与顺序程序一样,而且它的运行环境也不为别的进程所改变。运行的结果是确定的,不会发生与时间相关的错误。 ---- 但是,在实际中,并发进程的各个进程之间并不是完全互相独立的,它们之间往往存在着相互制约的关系。进程之间的相互制约关系表现为两种方式: ---- (1) 间接相互制约:共享CPU ---- (2) 直接相互制约:竞争和协作 ---- 竞争——进程对共享资源的竞争。为保证进程互斥地访问共享资源,各进程必须互斥地进入各自的临界段。 ---- 协作——进程之间交换数据。为完成一个共同任务而同时运行的一组进程称为同组进程,它们之间必须交换数据,以达到协作完成任务的目的,交换数据可以通知对方可以做某事或者委托对方做某事。 ---- 共享CPU问题由操作系统的进程调度来实现,进程间的竞争和协作由进程间的通信来完成。进程间的通信一般由操作系统提供编程接口,由程序员在程序中实现。UNIX在这个方面可以说最具特色,它提供了一整套进程间的数据共享与信息交换的处理方法——进程通信机制(IPC)。因此,我们就以UNIX为例来分析进程间通信的各种实现技术。 ---- 在UNIX中,文件(File)、信号(Signal)、无名管道(Unnamed Pipes)、有名管道(FIFOs)是传统IPC功能;新的IPC功能包括消息队列(Message queues)、共享存储段(Shared memory segment)和信号灯(Semapores)。 ---- (1) 信号 ---- 信号机制是UNIX为进程中断处理而设置的。它只是一组预定义的值,因此不能用于信息交换,仅用于进程中断控制。例如在发生浮点错、非法内存访问、执行无效指令、某些按键(如ctrl-c、del等)等都会产生一个信号,操作系统就会调用有关的系统调用或用户定义的处理过程来处理。 ---- 信号处理的系统调用是signal,调用形式是: ---- signal(signalno,action) ---- 其中,signalno是规定信号编号的值,action指明当特定的信号发生时所执行的动作。 ---- (2) 无名管道和有名管道 ---- 无名管道实际上是内存中的一个临时存储区,它由系统安全控制,并且独立于创建它的进程的内存区。管道对数据采用先进先出方式管理,并严格按顺序操作,例如不能对管道进行搜索,管道中的信息只能读一次。 ---- 无名管道只能用于两个相互协作的进程之间的通信,并且访问无名管道的进程必须有共同的祖先。 ---- 系统提供了许多标准管道库函数,如: pipe——打开一个可以读写的管道; close——关闭相应的管道; read——从管道中读取字符; write——向管道中写入字符; ---- 有名管道的操作和无名管道类似,不同的地方在于使用有名管道的进程不需要具有共同的祖先,其它进程,只要知道该管道的名字,就可以访问它。管道非常适合进程之间快速交换信息。 ---- (3) 消息队列(MQ) ---- 消息队列是内存中独立于生成它的进程的一段存储区,一旦创建消息队列,任何进程,只要具有正确的的访问权限,都可以访问消息队列,消息队列非常适合于在进程间交换短信息。 ---- 消息队列的每条消息由类型编号来分类,这样接收进程可以选择读取特定的消息类型——这一点与管道不同。消息队列在创建后将一直存在,直到使用msgctl系统调用或iqcrm -q命令删除它为止。 ---- 系统提供了许多有关创建、使用和管理消息队列的系统调用,如: ---- int msgget(key,flag)——创建一个具有flag权限的MQ及其相应的结构,并返回一个唯一的正整数msqid(MQ的标识符); ---- int msgsnd(msqid,msgp,msgsz,msgtyp,flag)——向队列中发送信息; ---- int msgrcv(msqid,cmd,buf)——从队列中接收信息; ---- int msgctl(msqid,cmd,buf)——对MQ的控制操作; ---- (4) 共享存储段(SM) ---- 共享存储段是主存的一部分,它由一个或多个独立的进程共享。各进程的数据段与共享存储段相关联,对每个进程来说,共享存储段有不同的虚拟地址。系统提供的有关SM的系统调用有: ---- int shmget(key,size,flag)——创建大小为size的SM段,其相应的数据结构名为key,并返回共享内存区的标识符shmid; ---- char shmat(shmid,address,flag)——将当前进程数据段的地址赋给shmget所返回的名为shmid的SM段; ---- int shmdr(address)——从进程地址空间删除SM段; ---- int shmctl (shmid,cmd,buf)——对SM的控制操作; ---- SM的大小只受主存限制,SM段的访问及进程间的信息交换可以通过同步读写来完成。同步通常由信号灯来实现。SM非常适合进程之间大量数据的共享。 ---- (5) 信号灯 ---- 在UNIX中,信号灯是一组进程共享的数据结构,当几个进程竞争同一资源时(文件、共享内存或消息队列等),它们的操作便由信号灯来同步,以防止互相干扰。 ---- 信号灯保证了某一时刻只有一个进程访问某一临界资源,所有请求该资源的其它进程都将被挂起,一旦该资源得到释放,系统才允许其它进程访问该资源。信号灯通常配对使用,以便实现资源的加锁和解锁。 ---- 进程间通信的实现技术的特点是:操作系统提供实现机制和编程接口,由用户在程序中实现,保证进程间可以进行快速的信息交换和大量数据的共享。但是,上述方式主要适合在同一台计算机系统内部的进程之间的通信。 3 应用程序间的通信及其实现技术 ---- 同进程之间的相互制约一样,不同的应用程序之间也存在竞争和协作的关系。UNIX操作系统也提供一些可用于应用程序之间实现数据共享与信息交换的编程接口,程序员可以通过自己编程来实现。如远程过程调用和基于TCP/IP协议的套接字(Socket)编程。但是,相对普通程序员来说,它们涉及的技术比较深,编程也比较复杂,实现起来困难较大。 ---- 于是,一种新的技术应运而生——通过将有关通信的细节完全掩盖在某个独立软件内部,即底层的通讯工作和相应的维护管理工作由该软件内部来实现,用户只需要将通信任务提交给该软件去完成,而不必理会它的具体工作过程——这就是所谓的中间件技术。 ---- 我们在这里分别讨论这三种常用的应用程序间通信的实现技术——远程过程调用、会话编程技术和MQSeries消息队列技术。其中远程过程调用和会话编程属于比较低级的方式,程序员参与的程度较深,而MQSeries消息队列则属于比较高级的方式,即中间件方式,程序员参与的程度较浅。 ---- 4.1 远程过程调用(RPC)
-
深度学习中的不确定性量化:2020年实用技术与应用大解析 - 61页精华解读" 这份报告深入剖析了近年来深度学习领域中不确定性量化(UQ)技术的最新发展,包括其在强化学习(RL)中的运用实例。探讨了贝叶斯近似和集成学习等主流UQ方法在各个具体场景中的广泛应用,比如自动驾驶、目标识别、图像修复、医疗影像分析(如分类和分割)、文本理解(如文本分类和风险评估)、以及生物信息学等多个领域。 报告进一步梳理了UQ方法在深度学习领域的关键应用案例,并针对当前面临的挑战及未来研究方向进行了概览和展望,为这一领域的研究人员和实践者提供了有价值的参考指南。
-
第二章:深入理解Linux编程 - 进程工作原理:剖析进程定义与特性、状态转换、关键数据结构、从创建到结束的过程、睡眠与唤醒机制、暂停与重启操作,以及处理器调度的核心概念
-
F#探险之旅(二):函数式编程(上)-函数式编程范式简介 F#主要支持三种编程范式:函数式编程(Functional Programming,FP)、命令式编程(Imperative Programming)和面向对象(Object-Oriented,OO)的编程。回顾它们的历史,FP是最早的一种范式,第一种FP语言是IPL,产生于1955年,大约在Fortran一年之前。第二种FP语言是Lisp,产生于1958,早于Cobol一年。Fortan和Cobol都是命令式编程语言,它们在科学和商业领域的迅速成功使得命令式编程在30多年的时间里独领风骚。而产生于1970年代的面向对象编程则不断成熟,至今已是最流行的编程范式。有道是“*代有语言出,各领风骚数十年”。 尽管强大的FP语言(SML,Ocaml,Haskell及Clean等)和类FP语言(APL和Lisp是现实世界中最成功的两个)在1950年代就不断发展,FP仍停留在学院派的“象牙塔”里;而命令式编程和面向对象编程则分别凭着在商业领域和企业级应用的需要占据领先。今天,FP的潜力终被认识——它是用来解决更复杂的问题的(当然更简单的问题也不在话下)。 纯粹的FP将程序看作是接受参数并返回值的函数的集合,它不允许有副作用(side effect,即改变了状态),使用递归而不是循环进行迭代。FP中的函数很像数学中的函数,它们都不改变程序的状态。举个简单的例子,一旦将一个值赋给一个标识符,它就不会改变了,函数不改变参数的值,返回值是全新的值。 FP的数学基础使得它很是优雅,FP的程序看起来往往简洁、漂亮。但它无状态和递归的天性使得它在处理很多通用的编程任务时没有其它的编程范式来得方便。但对F#来说这不是问题,它的优势之一就是融合了多种编程范式,允许开发人员按照需要采用最好的范式。 关于FP的更多内容建议阅读一下这篇文章:Why Functional Programming Matters(中文版)。F#中的函数式编程 从现在开始,我将对F#中FP相关的主要语言结构逐一进行介绍。标识符(Identifier) 在F#中,我们通过标识符给值(value)取名字,这样就可以在后面的程序中引用它。通过关键字let定义标识符,如: let x = 42 这看起来像命令式编程语言中的赋值语句,两者有着关键的不同。在纯粹的FP中,一旦值赋给了标识符就不能改变了,这也是把它称为标识符而非变量(variable)的原因。另外,在某些条件下,我们可以重定义标识符;在F#的命令式编程范式下,在某些条件下标识符的值是可以修改的。 标识符也可用于引用函数,在F#中函数本质上也是值。也就是说,F#中没有真正的函数名和参数名的概念,它们都是标识符。定义函数的方式与定义值是类似的,只是会有额外的标识符表示参数: let add x y = x + y 这里共有三个标识符,add表示函数名,x和y表示它的参数。关键字和保留字关键字是指语言中一些标记,它们被编译器保留作特殊之用。在F#中,不能用作标识符或类型的名称(后面会讨论“定义类型”)。它们是: abstract and as asr assert begin class default delegate do donedowncast downto elif else end exception extern false finally forfun function if in inherit inline interface internal land lazy letlor lsr lxor match member mod module mutable namespace new nullof open or override private public rec return sig static structthen to true try type upcast use val void when while with yield 保留字是指当前还不是关键字,但被F#保留做将来之用。可以用它们来定义标识符或类型名称,但编译器会报告一个警告。如果你在意程序与未来版本编译器的兼容性,最好不要使用。它们是: atomic break checked component const constraint constructor continue eager event external fixed functor global include method mixinobject parallel process protected pure sealed trait virtual volatile 文字值(Literals) 文字值表示常数值,在构建计算代码块时很有用,F#提供了丰富的文字值集。与C#类似,这些文字值包括了常见的字符串、字符、布尔值、整型数、浮点数等,在此不再赘述,详细信息请查看F#手册。 与C#一样,F#中的字符串常量表示也有两种方式。一是常规字符串(regular string),其中可包含转义字符;二是逐字字符串(verbatim string),其中的(")被看作是常规的字符,而两个双引号作为双引号的转义表示。下面这个简单的例子演示了常见的文字常量表示: let message = "Hello World"r"n!" // 常规字符串let dir = @"C:"FS"FP" // 逐字字符串let bytes = "bytes"B // byte 数组let xA = 0xFFy // sbyte, 16进制表示let xB = 0o777un // unsigned native-sized integer,8进制表示let print x = printfn "%A" xlet main = print message; print dir; print bytes; print xA; print xB; main Printf函数通过F#的反射机制和.NET的ToString方法来解析“%A”模式,适用于任何类型的值,也可以通过F#中的print_any和print_to_string函数来完成类似的功能。值和函数(Values and Functions) 在F#中函数也是值,F#处理它们的语法也是类似的。 let n = 10let add a b = a + blet addFour = add 4let result = addFour n printfn "result = %i" result 可以看到定义值n和函数add的语法很类似,只不过add还有两个参数。对于add来说a + b的值自动作为其返回值,也就是说在F#中我们不需要显式地为函数定义返回值。对于函数addFour来说,它定义在add的基础上,它只向add传递了一个参数,这样对于不同的参数addFour将返回不同的值。考虑数学中的函数概念,F(x, y) = x + y,G(y) = F(4, y),实际上G(y) = 4 + y,G也是一个函数,它接收一个参数,这个地方是不是很类似?这种只向函数传递部分参数的特性称为函数的柯里化(curried function)。 当然对某些函数来说,传递部分参数是无意义的,此时需要强制提供所有参数,可是将参数括起来,将它们转换为元组(tuple)。下面的例子将不能编译通过: let sub(a, b) = a - blet subFour = sub 4 必须为sub提供两个参数,如sub(4, 5),这样就很像C#中的方法调用了。 对于这两种方式来说,前者具有更高的灵活性,一般可优先考虑。 如果函数的计算过程中需要定义一些中间值,我们应当将这些行进行缩进: let halfWay a b = let dif = b - a let mid = dif / 2 mid + a 需要注意的是,缩进时要用空格而不是Tab,如果你不想每次都按几次空格键,可以在VS中设置,将Tab字符自动转换为空格;虽然缩进的字符数没有限制,但一般建议用4个空格。而且此时一定要用在文件开头添加#light指令。作用域(Scope)作用域是编程语言中的一个重要的概念,它表示在何处可以访问(使用)一个标识符或类型。所有标识符,不管是函数还是值,其作用域都从其声明处开始,结束自其所处的代码块。对于一个处于最顶层的标识符而言,一旦为其赋值,它的值就不能修改或重定义了。标识符在定义之后才能使用,这意味着在定义过程中不能使用自身的值。 let defineMessage = let message = "Help me" print_endline message // error 对于在函数内部定义的标识符,一般而言,它们的作用域会到函数的结束处。 但可使用let关键字重定义它们,有时这会很有用,对于某些函数来说,计算过程涉及多个中间值,因为值是不可修改的,所以我们就需要定义多个标识符,这就要求我们去维护这些标识符的名称,其实是没必要的,这时可以使用重定义标识符。但这并不同于可以修改标识符的值。你甚至可以修改标识符的类型,但F#仍能确保类型安全。所谓类型安全,其基本意义是F#会避免对值的错误操作,比如我们不能像对待字符串那样对待整数。这个跟C#也是类似的。 let changeType = let x = 1 let x = "change me" let x = x + 1 print_string x 在本例的函数中,第一行和第二行都没问题,第三行就有问题了,在重定义x的时候,赋给它的值是x + 1,而x是字符串,与1相加在F#中是非法的。 另外,如果在嵌套函数中重定义标识符就更有趣了。 let printMessages = let message = "fun value" printfn "%s" message; let innerFun = let message = "inner fun value" printfn "%s" message innerFun printfn "%s" message printMessages 打印结果: fun value inner fun valuefun value 最后一次不是inner fun value,因为在innerFun仅仅将值重新绑定而不是赋值,其有效范围仅仅在innerFun内部。递归(Recursion)递归是编程中的一个极为重要的概念,它表示函数通过自身进行定义,亦即在定义处调用自身。在FP中常用于表达命令式编程的循环。很多人认为使用递归表示的算法要比循环更易理解。 使用rec关键字进行递归函数的定义。看下面的计算阶乘的函数: let rec factorial x = match x with | x when x < 0 -> failwith "value must be greater than or equal to 0" | 0 -> 1 | x -> x * factorial(x - 1) 这里使用了模式匹配(F#的一个很棒的特性),其C#版本为: public static long Factorial(int n) { if (n < 0) { throw new ArgumentOutOfRangeException("value must be greater than or equal to 0"); } if (n == 0) { return 1; } return n * Factorial (n - 1); } 递归在解决阶乘、Fibonacci数列这样的问题时尤为适合。但使用的时候要当心,可能会写出不能终止的递归。匿名函数(Anonymous Function) 定义函数的时候F#提供了第二种方式:使用关键字fun。有时我们没必要给函数起名,这种函数就是所谓的匿名函数,有时称为lambda函数,这也是C#3.0的一个新特性。比如有的函数仅仅作为一个参数传给另一个函数,通常就不需要起名。在后面的“列表”一节中你会看到这样的例子。除了fun,我们还可以使用function关键字定义匿名函数,它们的区别在于后者可以使用模式匹配(本文后面将做介绍)特性。看下面的例子: let x = (fun x y -> x + y) 1 2let x1 = (function x -> function y -> x + y) 1 2let x2 = (function (x, y) -> x + y) (1, 2) 我们可优先考虑fun,因为它更为紧凑,在F#类库中你能看到很多这样的例子。 注意:本文中的代码均在F# 1.9.4.17版本下编写,在F# CTP 1.9.6.0版本下可能不能通过编译。 F#系列随笔索引页面