昨晚服務(wù)在發(fā)布的時(shí)候, 出現(xiàn)如下異常
Caused by: java.lang.NoSuchMethodError: ...
Dubbo在暴露服務(wù)的時(shí)候, 需要啟動(dòng)Netty服務(wù)端, 在啟動(dòng)服務(wù)端的過程中, 根據(jù)Reactor模型, 它需要?jiǎng)?chuàng)建IO線程.會(huì)涉及到使用Netty中的io.netty.util.concurrent.SingleThreadEventExecutor類, 根據(jù)錯(cuò)誤提示, 在構(gòu)造SingleThreadEventExecutor對(duì)象的時(shí)候, 找不到符合的構(gòu)造器方法.
查看下應(yīng)用依賴的Netty包
雖然有2個(gè)3.x版本的Netty包, 但是3.x版本的Netty包名都是 org.jboss.netty, 4.x版本的包名都是io.netty, 根據(jù)錯(cuò)誤提示的包名, 因此排除3.x版本的嫌疑.
剩下的就是4.1.43版本和4.1.29版本, 版本不一致, 很可能就是因?yàn)檫@個(gè)原因造成的.
io.netty.util.concurrent.SingleThreadEventExecutor 這個(gè)類出現(xiàn)在兩個(gè)包里.netty-all-4.1.43.Final.jar 和 netty-common-4.1.29.Final.jar 包中都有SingleThreadEventExecutor 類.
寫了一個(gè)簡(jiǎn)單的測(cè)試案例
// 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>() {
@Override
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();
}
}
}
以上代碼會(huì)使用Netty創(chuàng)建一個(gè)服務(wù)端, 也是在模擬Dubbo使用Netty創(chuàng)建服務(wù)端, 本質(zhì)是一樣的. 只是在我的代碼中, 使用
Class.forName("io.netty.util.concurrent.SingleThreadEventExecutor");
手動(dòng)提前加載SingleThreadEventExecutor類.
編譯程序
javac -d . -classpath ".:./netty-all-4.1.43.Final.jar:./netty-common-4.1.29.Final.jar" Example.java
在這里我們手動(dòng)指定了jar包的加載順序
運(yùn)行程序
java -cp ".:./netty-all-4.1.43.Final.jar:./netty-common-4.1.29.Final.jar" com.infuq.Example
服務(wù)正常啟動(dòng)了...
接下來改變一下運(yùn)行時(shí)加載Jar包的順序, 讓類加載器在加載SingleThreadEventExecutor類的時(shí)候, 先從netty-common-4.1.29.Final.jar包中查找加載.
出現(xiàn)了與文章一開始一樣的錯(cuò)誤. 因?yàn)樘崆凹虞d了netty-common-4.1.29.Final.jar版本中的SingleThreadEventExecutor類, 而接下來創(chuàng)建Netty服務(wù)端的時(shí)候, 在構(gòu)造SingleThreadEventExecutor對(duì)象的時(shí)候, 傳入的參數(shù)格式是按照netty-all-4.1.43.Final.jar包中的SingleThreadEventExecutor類傳參. netty-common-4.1.29.Final.jar 和 netty-all-4.1.43.Final.jar 中關(guān)于SingleThreadEventExecutor類構(gòu)造器的確不同, 如下
netty-all-4.1.43.Final.jar 包中的SingleThreadEventExecutor類構(gòu)造器比netty-common-4.1.29.Final.jar包中的SingleThreadEventExecutor類構(gòu)造器多一個(gè), 而且就是錯(cuò)誤中提示的`缺失`那個(gè)構(gòu)造器.
使用mvn dependency:tree > tmp.txt命令導(dǎo)出來依賴關(guān)系, 查看了下, netty-common-4.1.29.Final.jar 和 netty-all-4.1.43.Final.jar 這兩個(gè)包分別是被架構(gòu)組A和團(tuán)隊(duì)B使用, 而作為使用方的我們, 需要手動(dòng)解決版本不一樣的問題, 否則就會(huì)出現(xiàn)許多莫名其妙錯(cuò)誤.
在這之前應(yīng)用沒有出現(xiàn)過類似錯(cuò)誤, 所以感覺很奇怪, 為什么最近突然出現(xiàn)了這樣的錯(cuò)誤, 原來是我們最近代碼中接入了團(tuán)隊(duì)B的一個(gè)能力框架, 它的底層間接依賴了Netty, 只是版本與我們代碼中依賴架構(gòu)組A使用的Netty版本不一致引起的.
世界大同, 版本一致是原則.
應(yīng)用加載jar包的順序顛倒, 導(dǎo)致應(yīng)用啟動(dòng)報(bào)錯(cuò). 而重點(diǎn)就在于加載jar包順序.
接下來我們簡(jiǎn)單驗(yàn)證下, 在Linux系統(tǒng)中, 讀取目錄下的文件, 它的順序是怎樣的.
當(dāng)我們使用ll 命令查看目錄下文件的時(shí)候, 默認(rèn)是按照字母排序的, 這個(gè)依據(jù)在man手冊(cè)中可以查找到, 如下
man ls
描述中已經(jīng)說明, ls默認(rèn)按照字母次序排序文件
如果使用ll -r 查看目錄內(nèi)容, 又會(huì)看到另一種排序結(jié)果, 如下圖, netty-common-4.1.29.Final.jar排在netty-all-4.1.43.Final.jar前面了
那么我們平時(shí)寫的Java程序, 在加載某個(gè)目錄下的Jar文件時(shí), 比如Tomcat讀取WEB-INF/lib目錄下的jar文件時(shí), 先讀取哪個(gè)后讀取哪個(gè)總該有個(gè)順序吧, 它的底層不會(huì)像ls命令排序那樣的, 那么它的底層是依據(jù)什么呢? 往下看
這里寫了一個(gè)C程序(read_dir.c), 它的功能就是讀取當(dāng)前目錄下的文件
// read_dir.c
#define _GNU_SOURCE
#include <dirent.h> /* Defines DT_* constants */
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
struct linux_dirent {
unsigned long d_ino;
off_t d_off;
unsigned short d_reclen;
char d_name[];
};
#define BUF_SIZE 1024
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 (;;) {
// 調(diào)用系統(tǒng)函數(shù)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);
}
編譯這個(gè)C程序
gcc -o read_dir read_dir.c
執(zhí)行生成的read_dir, 輸出結(jié)果如下
【第一列inode】在Linux文件系統(tǒng)中, 標(biāo)識(shí)一個(gè)文件并不是根據(jù)它的名稱, 而是根據(jù)這個(gè)inode值. 不同文件的inode值不同.
比如在tmp目錄下有三個(gè)文件,分別是-not,1.txt,2.txt
如果要?jiǎng)h除1.txt , 可以使用rm 1.txt把文件刪除掉. 但是當(dāng)使用rm -not刪除-not文件時(shí), 它就會(huì)提示錯(cuò)誤
rm 命令會(huì)把中劃線-后面當(dāng)成命令參數(shù), 而rm沒有-n的命令參數(shù),因此報(bào)錯(cuò)了. 這個(gè)時(shí)候我們就可以使用inode值刪除文件.查看文件的inode值
-not文件的inode值是317158, 于是使用rm `find . -inum 317158`;命令就可以刪除-not文件.
【第二列file type】表示文件類型
【第三列d_reclen】表示文件長(zhǎng)度
【第四列d_off】可以理解成這個(gè)文件在目錄中的偏移, 具體含義在它的結(jié)構(gòu)體中有說明, 上面輸出的每行記錄都使用下面的結(jié)構(gòu)體表示
<font >而我們讀取目錄下的文件就是根據(jù)<font color=red size=5>d_off</font>值排序的.</font>
我們?cè)俅问褂肞ython語言程序驗(yàn)證下
#! /usr/bin/env python
import os
r = os.listdir(".")
print(r)
輸出的結(jié)果與C程序一致, 畢竟Python語言底層也是調(diào)用相同的C庫函數(shù).
對(duì)應(yīng)的底層系統(tǒng)調(diào)用API是getdents, 可以參考 https://man7.org/linux/man-pages/man2/getdents.2.html 或man getdents 查看下相關(guān)的介紹.
附錄: 本篇文章的實(shí)驗(yàn)代碼地址
https://github.com/infuq/infuq-others/tree/master/Lab/2022-3-16