在上篇文章(Netty 入门 — ChannelHandler, Netty 的数据加工厂)提到 ChannelHandler 虽然是一个好的打工人,但是在我们实际业务线中,他不可能一个人干所有的活啊,毕竟都 21 世纪了,我们是要讲究分工的。所以 Netty 就需要一个好的组织者将这些 ChannelHandler 组织起来,形成一个条完整,高效的业务线,这个组织者就是 ChannelPipeline。
ChannelPipeline 概述
pipeline 翻译为管道、流水线,在 Netty 这个大工厂中,ChannelPipeline 就像一条流水线,数据流过 ChannelPipeline,被一步一步地加工,最后得到一个成熟的工艺品。
在 Netty 中,ChannelPipeline 是 Netty 的核心处理链,用于实现网络时间的动态编排和有序传播。它负责组织和编排各种 ChannelHandler,使他们能够有序地组织在一起,但实际的数据加工还是由 ChannelHandler 处理。
ChannelPipeline 的内部结构
ChannelPipeline 可以看作是 ChannelHandler 的容器载体,它是由一组 ChannelHandler 有序地组成的双向拦截链。每当新建一个 Channel 时都会新建一个 ChannelPipeline 与之绑定,而且这种绑定关系是永久的,当该 Channel 有 I/O 读写事件发生时,数据会贯穿整个 ChannelPipeline ,由里面的 ChannelHandler 依次拦截和处理。
我们知道 ChannelHandler 分为出站 handler 和入站 handler ,但是 ChannelPipeline 并没有将他们分开,而是将出站 handler 和入站handler 混编在一起的,当一个入站事件从 ChannelPipeline 的头部向尾部开始传播的时候,每一个 ChannelHandler 都会判断下一个 ChannelHandler 的类型是否与当前 ChannelHandler 的类型相同,如果是则将事件传播给他,不是则跳过该 ChannelHandler 传递下一个,直到找到跟他相同类型的 ChannelHandler。下图是一个入站的传播路径:
我们一般都是选择入站节点作为头部,出站节点作为尾部的。
ChannelPipeline 也提供了一些 API 用于维护该双向链。
api | 描述 |
addLast() |
将该 ChannelHandler 添加到 ChannelPipeline 的末尾 |
addBefore() |
将该ChannelHandler 添加在指定名称的 ChannelHandler 之前 |
addAfter() |
将该ChannelHandler 添加在指定名称的 ChannelHandler 之后 |
addFirst() |
将该 ChannelHandler 添加到 ChannelPipeline 的第一个位置 |
remove() |
删除指定的 ChannelHandler |
replace() |
替换指定的 ChannelHandler |
ChannelHandlerContext
ChannelHandlerContext 用于保存 ChannelHandler 的上下文,它包含了 ChannelHandler 生命周期中的所有事件,如 connect
、bind
、read
、write
。
为什么会有一个 ChannelHandlerContext 呢?其实这是一种编程思想,我认为是单一职责,一个类只做一件事。试想下 ChannelHandler 是数据加工厂,但是我们现在又要他来维护它与周边 ChannelHandler 的关系,要负责事件的传播,还要维护其生命周期,累不累啊,功能严重耦合。所以我们需要 ChannelHandlerContext 来帮助他更好地工作,它只需要做事,其余的交给 ChannelHandlerContext 了(我靠,立刻脑补了我们的 996)。
ChannelHandlerContext 他代表了 ChannelHandler 和 ChannelPipeline 之间的关联,每当有一个 ChannelHandler 被添加到 ChannelPipeline 中时都会创建一个 ChannelHandlerContext 与之关联,它维护着该 ChannelHandler 与其他 ChannelHandler(同一个 ChannelPipeline) 的之间交互。
我们在初始化 ChannelPipeline 的时候会发现,ChannelPipeline 的双向链表其实是有特定的首尾节点的,其中首节点 HeadContext,尾节点 TailContext,我们所有自定义的 ChannelHandler 节点都是唯一这两个节点的中间。
从上图我们可以看出 HeadContext 既是 InboundHandler,也是 OutboundHandler,所以读事件则是从 HeadContext 开始,写事件也是在 HeadContext 结束。而 TailContext 则只是 OutboundHandler,它会在 ChannelPipeline 调用链的最后一步执行,用于终止 Inbound 的事件传播。TailContext 作为 Outbound 事件传播的第一站,它仅仅只是将 Outbound 事件进行传递。
加入 ChannelHandlerContext 的完整图如下:
ChannelPipeline 的 API
ChannelPipeline 的 API 不仅仅有对 ChannelHandler 的维护功能,还有一些入站和出站的方法。
- 入站 API
方法 | 描述 |
---|---|
fireChannelRegistered |
调用 ChannelPipeline 中下一个 ChannelInboundHandler 的 channelRegistered 方法 |
fireChannelUnregistered |
调用 ChannelPipeline 中下一个 ChannelInboundHandler 的 channelUnregistered 方法 |
fireChannelActive |
调用 ChannelPipeline 中下一个 ChannelInboundHandler 的 channelActive 方法 |
fireChannelInactive |
调用 ChannelPipeline 中下一个 ChannelInboundHandler 的 channelInactive 方法 |
fireExceptionCaught |
调用 ChannelPipeline 中下一个 ChannelInboundHandler 的 exceptionCaught 方法 |
fireUserEventTriggered |
调用 ChannelPipeline 中下一个 ChannelInboundHandler 的 userEventTriggered 方法 |
fireChannelRead |
调用 ChannelPipeline 中下一个 ChannelInboundHandler 的 channelRead 方法 |
fireChannelReadComplete |
调用 ChannelPipeline 中下一个 ChannelInboundHandler 的 channelReadComplete 方法 |
从这里就可以看出,所有的 fireXx()
方法其实就是将消息传递给下一个节点
- 出站 API
方法 | 描述 |
---|---|
bind |
调用 ChannelPipeline 中下一个 ChannelOutboundHandler 的bind 方法,将 Channel 与本地地址绑定 |
connect |
调用 ChannelPipeline 中下一个 ChannelOutboundHandler 的connect 方法,将 Channel 连接到远程节点 |
disconnect |
调用 ChannelPipeline 中下一个 ChannelOutboundHandler 的disconnect 方法,将 Channel 与远程连接断开 |
close |
调用 ChannelPipeline 中下一个 ChannelOutboundHandler 的close 方法,将 Channel 关闭 |
deregister |
调用 ChannelPipeline 中下一个 ChannelOutboundHandler 的deregister 方法,将 Channel 从其对应的 EventLoop 注销 |
flush |
调用 ChannelPipeline 中下一个 ChannelOutboundHandler 的flush 方法,将 Channel 的数据冲刷到远程节点 |
read |
调用 ChannelPipeline 中下一个 ChannelOutboundHandler 的 read 方法,从 Channel 中读取数据 |
write |
调用 ChannelPipeline 中下一个 ChannelOutboundHandler 的 write 方法,将数据写入 Channel |
writeAndFlush |
先调用 write 方法,然后调用flush方法,将数据写入并刷回远程节点 |
出站类的方法都是与 Channel 相关的。
ChannelPipeline 事件传播机制
记住:InboundHandler顺序执行,OutboundHandler逆序执行。
ChannelPipeline 将 ChannelHandler 编排好后,就响应等待 I/O 事件了,但是这个事件是如何传播的呢?大明哥通过一个例子来跟你细说。首先我们需要先构造一个如下图的传播链。
代码如下:
public class ChannelPipelineTest_01_server {
public static void main(String[] args) {
new ServerBootstrap()
.group(new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 这里一定要按照顺序来添加
pipeline.addLast(new InboundHandler("InboundHandler-1",false));
pipeline.addLast(new InboundHandler("InboundHandler-2",false));
pipeline.addLast(new OutboundHandler("OutboundHandler-1"));
pipeline.addLast(new OutboundHandler("OutboundHandler-2"));
pipeline.addLast(new InboundHandler("InboundHandler-3",true));
}
})
.bind(8081);
}
private static class InboundHandler extends ChannelInboundHandlerAdapter {
// handler 的名称
private String handlerName;
// 是否写数据
private Boolean flushFlag;
public InboundHandler(String handlerName,Boolean flushFlag) {
this.handlerName = handlerName;
this.flushFlag = flushFlag;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("InboundHandler :" + handlerName);
if (!flushFlag) {
// 不需要写数据,传递给下一个节点
ctx.fireChannelRead(msg);
} else {
// 写数据,则调用 channel.writeAndFlush()
System.out.println("==============================");
ctx.channel().writeAndFlush(msg);
}
}
}
private static class OutboundHandler extends ChannelOutboundHandlerAdapter {
// handler 的名称
private String handlerName;
public OutboundHandler(String handlerName) {
this.handlerName = handlerName;
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
System.out.println("OutboundHandler :" + handlerName);
super.write(ctx,msg,promise);
}
}
}
执行结果如下:
InboundHandler :InboundHandler-1
InboundHandler :InboundHandler-2
InboundHandler :InboundHandler-3
==============================
OutboundHandler :OutboundHandler-2
OutboundHandler :OutboundHandler-1
由执行结果可见,Inbound 事件是有 Head —> Tail,而 Outbound 事件则是由 Tail —> Head,两者传播方向恰好相反。上面例子的运行流程如下:
红色为 Inbound 事件的响应路径,紫色为 Outbound 事件的响应路径。
在 InboundHandler 中,有段代码我们需要注意:
if (!flushFlag) {
// 不需要写数据,传递给下一个节点
ctx.fireChannelRead(msg);
} else {
// 写数据,则调用 channel.writeAndFlush()
System.out.println("==============================");
ctx.channel().writeAndFlush(msg);
}
这了有小伙伴会有疑问,为什么传递到下一个节点的时候是调用 ChannelHandlerContext.fireChannelRead()
,而写数据的时候调用的是 Channel.writeAndFlush()
,其实如果小伙伴去看 API 的时候会发现 ChannelHandlerContext 也是有 writeAndFlush()
的,但是为什么不使用 ChannelHandlerContext 的,而使用 的呢?其实这正是 ChannelHandlerContext 与 Channel 或者 ChannelPipeline 的区别:
- Channel 或 ChannelPipeline 的方法其影响是会沿着整个 ChannelPipeline 进行传播。
- 而 ChannelHandlerContext 方法则是从与其相关联的 ChannelHandler 开始,且只会传播给该 ChannelPipeline 种下一个能处理该事件的 ChannelHandler。
有兴趣的小伙伴,可以将代码调整下:
ctx.channel().writeAndFlush(msg);
调整为
ctx.writeAndFlush(msg);
然后再运行下,大明哥就不再演示了。
ChannelPipeline 异常传播机制
任何 ChannelHandler 都有可能会产生异常,如果某一个 ChannelHandler 的处理逻辑出现了异常,会有什么情况呢?我们对上面的代码进行简单调整下:
public class ChannelPipelineTest_02_server {
// 省略代码
private static class InboundHandler extends ChannelInboundHandlerAdapter {
// 省略代码
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("InboundHandler :" + handlerName);
if ("InboundHandler-2".equals(handlerName)) {
throw new RuntimeException("InboundHandler Exception");
}
if (!flushFlag) {
// 不需要写数据,传递给下一个节点
ctx.fireChannelRead(msg);
} else {
// 写数据,则调用 channel.writeAndFlush()
System.out.println("==============================");
ctx.channel().writeAndFlush(msg);
}
}
}
// 省略代码
}
当 handler 为 InboundHandler-2 时,就抛出异常。运行结果:
InboundHandler :InboundHandler-1
InboundHandler :InboundHandler-2
2022-06-25 22:29:22.224 [nioEventLoopGroup-2-2] WARN io.netty.channel.DefaultChannelPipeline - An exceptionCaught() event was fired, and it reached at the tail of the pipeline. It usually means the last handler in the pipeline did not handle the exception.
java.lang.RuntimeException: InboundHandler Exception
at com.sike.netty.rumen.channelpipeline.ChannelPipelineTest_02_server$InboundHandler.channelRead(ChannelPipelineTest_02_server.java:48) ~[classes/:?]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
at com.sike.netty.rumen.channelpipeline.ChannelPipelineTest_02_server$InboundHandler.channelRead(ChannelPipelineTest_02_server.java:53) [classes/:?]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:722) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:658) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:584) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:496) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:995) [netty-common-4.1.77.Final.jar:4.1.77.Final]
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) [netty-common-4.1.77.Final.jar:4.1.77.Final]
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) [netty-common-4.1.77.Final.jar:4.1.77.Final]
at java.lang.Thread.run(Thread.java:748) [?:1.8.0_201]
日志中我们可以看出,事件仅仅只传播到了 InboundHandler-2 节点就没有传播下去了,同时还有一条 WARN io.netty.channel.DefaultChannelPipeline...
的告警日志,这是 TailContext 节点打印出来的,它的出现表明了用户没有对异常进行拦截和处理,最后只能有 TailContext 节点来处理了。
我们再来改造下:
public class ChannelPipelineTest_03_server {
// 省略代码
private static class InboundHandler extends ChannelInboundHandlerAdapter {
// 省略代码
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("InboundHandler :" + handlerName);
if ("InboundHandler-2".equals(handlerName)) {
throw new RuntimeException("InboundHandler Exception");
}
if (!flushFlag) {
// 不需要写数据,传递给下一个节点
ctx.fireChannelRead(msg);
} else {
// 写数据,则调用 channel.writeAndFlush()
System.out.println("==============================");
ctx.channel().writeAndFlush(msg);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("InBoundHandler Exception: " + handlerName);
// 将异常传播下去
ctx.fireExceptionCaught(cause);
}
}
// 省略代码
}
InboundHandler 重写了 exceptionCaught()
,在 Netty 入门 — ChannelHandler, Netty 的数据加工厂 中讲到 exceptionCaught()
是当 ChannelHandler 在处理过程中出现异常时调用。ctx.fireExceptionCaught(cause);
表示将 Exception 传播下去。运行结果:
InboundHandler :InboundHandler-1
InboundHandler :InboundHandler-2
InBoundHandler Exception: InboundHandler-2
InBoundHandler Exception: InboundHandler-3
2022-06-25 22:40:26.371 [nioEventLoopGroup-2-2] WARN io.netty.channel.DefaultChannelPipeline - An exceptionCaught() event was fired, and it reached at the tail of the pipeline. It usually means the last handler in the pipeline did not handle the exception.
java.lang.RuntimeException: InboundHandler Exception
at com.sike.netty.rumen.channelpipeline.ChannelPipelineTest_03_server$InboundHandler.channelRead(ChannelPipelineTest_03_server.java:48) ~[classes/:?]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
at com.sike.netty.rumen.channelpipeline.ChannelPipelineTest_03_server$InboundHandler.channelRead(ChannelPipelineTest_03_server.java:53) [classes/:?]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:722) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:658) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:584) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:496) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:995) [netty-common-4.1.77.Final.jar:4.1.77.Final]
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) [netty-common-4.1.77.Final.jar:4.1.77.Final]
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) [netty-common-4.1.77.Final.jar:4.1.77.Final]
at java.lang.Thread.run(Thread.java:748) [?:1.8.0_201]
从运行结果中可以看出,ctx.fireExceptionCaught
将异常从当前节点传播到了 TailContext 节点。异常信息依然由 TailContext 进行统一处理。
虽然 Netty 在 TailContext 中做了最后的兜底,但是这种情况并不满足我们实际业务的处理逻辑,对于具体的业务而言,响应方不仅仅需要对异常进行拦截和处理,还需要根据具体的异常类型做出不同的异常处理。
ChannelPipeline 异常的最佳实践
如上所描述的,在实际的业务场景中我们需要根据具体的异常类型做出不同的异常处理,不能简单粗暴地丢给 Netty 来做兜底处理,所以最好的方法是我们需要对异常进行统一拦击,然后从实际业务场景触发做出不同的异常处理机制。所以最好的方式是我们需要添加一个自定义的异常处理 Handler:ExceptionHandler。如下:
伪代码如下:
public class ExceptionHandler extends ChannelDuplexHandler {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
if (cause instanceof BusinessException) {
// dosomething
}
if (cause instanceof SystemException) {
// dosomething
}
.....
}
}
ChannelDuplexHandler 是一个比较特殊的 ChannelHandler ,它同时实现了 ChannelInboundHandler 和 ChannelOutboundHandler,所以它能同时响应入站事件和出站事件,而对于我们统一异常处理是不需要区分入站事件和出站事件的。
总结
最后,大明哥来做一个总结,加深各位小伙伴们的理解。
- ChannelPipeline 是一个有多个 ChannelHandler 组成的双向链表。
- 在创建 Channel 的时候会随之创建一个与其绑定的 ChannelPipeline,且这种绑定关系是持久的。
- ChannelHandlerContext 是 ChannelHandler 的封装,每一个 ChannelHandler 都对应一个 ChannelHandlerContext,ChannelHandlerContext 里面保存着与之对应的 ChannelHandler 的上下文,它维护着该 ChannelHandler 与其他 ChannelHandler 的关系。
- 事件传播
- Inbound 事件的传播方向是 Head —> Tail。
- Outbound 事件的传播方向是 Tail —> Head。
- 每个节点都有可能会有异常发生,我们对异常的处理方式是在 ChannelPipeline 链路上增加一个 ExceptionHandler 用来统一拦截处理异常。
示例代码:http://suo.nz/1vasBW
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。