昨晚服务在发布的时候, 出现如下异常
Caused by: java.lang.NoSuchMethodError: …
Dubbo在暴露服务的时候, 需要启动Netty服务端, 在启动服务端的过程中, 根据Reactor模型, 它需要创建IO线程.会涉及到使用Netty中的io.netty.util.concurrent.SingleThreadEventExecutor类, 根据错误提示, 在构造SingleThreadEventExecutor对象的时候, 找不到符合的构造器方法.
查看下应用依赖的Netty包
虽然有2个3.x版本的Netty包, 但是3.x版本的Netty包名都是 org.jboss.netty, 4.x版本的包名都是io.netty, 根据错误提示的包名, 因此排除3.x版本的嫌疑.
剩下的就是4.1.43版本和4.1.29版本, 版本不一致, 很可能就是因为这个原因造成的.
io.netty.util.concurrent.SingleThreadEventExecutor 这个类出现在两个包里.netty-all-4.1.43.Final.jar 和 netty-common-4.1.29.Final.jar 包中都有SingleThreadEventExecutor 类.
写了一个简单的测试案例
// Example.java
package com.infuq;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.IdleStateHandler;
public class Example {
public static void main(String[] args) throws Exception {
// 加载SingleThreadEventExecutor类
Class.forName("io.netty.util.concurrent.SingleThreadEventExecutor");
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup(8);
EventLoopGroup businessGroup = new NioEventLoopGroup(8);
ServerBootstrap serverBootstrap = new ServerBootstrap();
try {
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel ch) {
ChannelPipeline channelPipeline = ch.pipeline();
channelPipeline.addLast(new StringEncoder());
channelPipeline.addLast(new StringDecoder());
channelPipeline.addLast("idleEventHandler", new IdleStateHandler(0, 10, 0));
channelPipeline.addAfter("idleEventHandler","loggingHandler",new LoggingHandler(LogLevel.INFO));
}
});
ChannelFuture channelFuture = serverBootstrap.bind("127.0.0.1", 8080).sync();
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
以上代码会使用Netty创建一个服务端, 也是在模拟Dubbo使用Netty创建服务端, 本质是一样的. 只是在我的代码中, 使用
Class.forName("io.netty.util.concurrent.SingleThreadEventExecutor");
手动提前加载SingleThreadEventExecutor类.
编译程序
javac -d . -classpath ".:./netty-all-4.1.43.Final.jar:./netty-common-4.1.29.Final.jar" Example.java
在这里我们手动指定了jar包的加载顺序
运行程序
java -cp ".:./netty-all-4.1.43.Final.jar:./netty-common-4.1.29.Final.jar" com.infuq.Example
服务正常启动了…
接下来改变一下运行时加载Jar包的顺序, 让类加载器在加载SingleThreadEventExecutor类的时候, 先从netty-common-4.1.29.Final.jar包中查找加载.
出现了与文章一开始一样的错误. 因为提前加载了netty-common-4.1.29.Final.jar版本中的SingleThreadEventExecutor类, 而接下来创建Netty服务端的时候, 在构造SingleThreadEventExecutor对象的时候, 传入的参数格式是按照netty-all-4.1.43.Final.jar包中的SingleThreadEventExecutor类传参. netty-common-4.1.29.Final.jar 和 netty-all-4.1.43.Final.jar 中关于SingleThreadEventExecutor类构造器的确不同, 如下
netty-all-4.1.43.Final.jar 包中的SingleThreadEventExecutor类构造器比netty-common-4.1.29.Final.jar包中的SingleThreadEventExecutor类构造器多一个, 而且就是错误中提示的`缺失`那个构造器.
使用mvn dependency:tree > tmp.txt命令导出来依赖关系, 查看了下, netty-common-4.1.29.Final.jar 和 netty-all-4.1.43.Final.jar 这两个包分别是被架构组A和团队B使用, 而作为使用方的我们, 需要手动解决版本不一样的问题, 否则就会出现许多莫名其妙错误.
在这之前应用没有出现过类似错误, 所以感觉很奇怪, 为什么最近突然出现了这样的错误, 原来是我们最近代码中接入了团队B的一个能力框架, 它的底层间接依赖了Netty, 只是版本与我们代码中依赖架构组A使用的Netty版本不一致引起的.
世界大同, 版本一致是原则.
应用加载jar包的顺序颠倒, 导致应用启动报错. 而重点就在于加载jar包顺序.
接下来我们简单验证下, 在Linux系统中, 读取目录下的文件, 它的顺序是怎样的.
当我们使用ll 命令查看目录下文件的时候, 默认是按照字母排序的, 这个依据在man手册中可以查找到, 如下
man ls
描述中已经说明, ls默认按照字母次序排序文件
如果使用ll -r 查看目录内容, 又会看到另一种排序结果, 如下图, netty-common-4.1.29.Final.jar排在netty-all-4.1.43.Final.jar前面了
那么我们平时写的Java程序, 在加载某个目录下的Jar文件时, 比如Tomcat读取WEB-INF/lib目录下的jar文件时, 先读取哪个后读取哪个总该有个顺序吧, 它的底层不会像ls命令排序那样的, 那么它的底层是依据什么呢? 往下看
这里写了一个C程序(read_dir.c), 它的功能就是读取当前目录下的文件
// read_dir.c
/* Defines DT_* constants */
struct linux_dirent {
unsigned long d_ino;
off_t d_off;
unsigned short d_reclen;
char d_name[];
};
int
main(int argc, char *argv[])
{
int fd;
long nread;
char buf[BUF_SIZE];
struct linux_dirent *d;
char d_type;
fd = open(argc > 1 ? argv[1] : ".", O_RDONLY | O_DIRECTORY);
if (fd == -1)
handle_error("open");
for (;;) {
// 调用系统函数getdents()
nread = syscall(SYS_getdents, fd, buf, BUF_SIZE);
if (nread == -1)
handle_error("getdents");
if (nread == 0)
break;
printf("--------------- nread=%ld ---------------\n", nread);
printf("inode# file type d_reclen d_off d_name\n");
for (long bpos = 0; bpos < nread;) {
d = (struct linux_dirent *) (buf + bpos);
printf("%18ld ", d->d_ino);
d_type = *(buf + bpos + d->d_reclen - 1);
printf("%-10s ", (d_type == DT_REG) ? "regular" :
(d_type == DT_DIR) ? "directory" :
(d_type == DT_FIFO) ? "FIFO" :
(d_type == DT_SOCK) ? "socket" :
(d_type == DT_LNK) ? "symlink" :
(d_type == DT_BLK) ? "block dev" :
(d_type == DT_CHR) ? "char dev" : "???");
printf("%4d %10jd %s\n", d->d_reclen,
(intmax_t) d->d_off, d->d_name);
bpos += d->d_reclen;
}
}
exit(EXIT_SUCCESS);
}
编译这个C程序
gcc -o read_dir read_dir.c
执行生成的read_dir, 输出结果如下
【第一列inode】在Linux文件系统中, 标识一个文件并不是根据它的名称, 而是根据这个inode值. 不同文件的inode值不同.
比如在tmp目录下有三个文件,分别是-not,1.txt,2.txt
如果要删除1.txt , 可以使用rm 1.txt把文件删除掉. 但是当使用rm -not删除-not文件时, 它就会提示错误
rm 命令会把中划线-后面当成命令参数, 而rm没有-n的命令参数,因此报错了. 这个时候我们就可以使用inode值删除文件.查看文件的inode值
-not文件的inode值是317158, 于是使用rm `find . -inum 317158`;命令就可以删除-not文件.
【第二列file type】表示文件类型
【第三列d_reclen】表示文件长度
【第四列d_off】可以理解成这个文件在目录中的偏移, 具体含义在它的结构体中有说明, 上面输出的每行记录都使用下面的结构体表示
【第五列d_name】表示文件名
而我们读取目录下的文件就是根据d_off值排序的.
我们再次使用Python语言程序验证下
#! /usr/bin/env python
import os
r = os.listdir(".")
print(r)
输出的结果与C程序一致, 毕竟Python语言底层也是调用相同的C库函数.
对应的底层系统调用API是getdents, 可以参考 https://man7.org/linux/man-pages/man2/getdents.2.html 或man getdents 查看下相关的介绍.
附录: 本篇文章的实验代码地址
https://github.com/infuq/infuq-others/tree/master/Lab/2022-3-16
个人站点
语雀
公众号