NoSuchMethodError 与 Linux系统读取目录内文件顺序

昨晚服务在发布的时候, 出现如下异常

在这里插入图片描述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>() {
                        @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();
        }
    }

}

以上代码会使用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

在这里插入图片描述
服务正常启动了…



在启动的时候, 可以添加一个-XX:+TraceClassLoading启动参数, 查看类是从哪个Jar包中加载的
在这里插入图片描述
执行以上命令, 根据打印信息可知, io.netty.util.concurrent.SingleThreadEventExecutor 是从 netty-all-4.1.43.Final.jar 包中加载的, 与指定的加载顺序一致.
在这里插入图片描述



接下来改变一下运行时加载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

#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 (;;) {
       // 调用系统函数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 查看下相关的介绍.

1.通过删除目录D下的A文件,可以改变目录D下的其他文件的d_off值
2.通过删除目录D下的A文件,再重新将A文件添加到目录D下,可以改变A文件的d_off值

附录: 本篇文章的实验代码地址
https://github.com/infuq/infuq-others/tree/master/Lab/2022-3-16


个人站点
语雀

公众号

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值