wifi___ 2024-04-01 11:29 采纳率: 0%
浏览 156
已结题

两个依赖的全限定类名相同,导致出现NoSuchMethodErr

依赖包poi-ooxml-lite-5.2.3.jar和ooxml-schemas-1.1.jar有全限定名相同的interface:org.openxmlformats.schemas.wordprocessingml.x2006.main.DocumentDocument,导致出现如下错误:

 java.lang.NoSuchFieldError: Factory
        at org.apache.poi.xwpf.usermodel.XWPFDocument.onDocumentRead(XWPFDocument.java:197)
        at org.apache.poi.ooxml.POIXMLDocument.load(POIXMLDocument.java:169)
        at org.apache.poi.xwpf.usermodel.XWPFDocument.<init>(XWPFDocument.java:146)
        at com.duxiu.readfile.operate.AbstractReadFile.readDocx(AbstractReadFile.java:375)
        at com.duxiu.readfile.operate.AbstractReadFile.readFileByTypeV1(AbstractReadFile.java:111)

img


注:报错的地方在poi-ooxml-5.2.3.jar内部的XWPFDocument.java中。

目前进度:

1.确定是类加载器只加载了ooxml-schemas-1.1.jar中的DocumentDocument.class,而没有加载poi-ooxml-lite-5.2.3.jar中的DocumentDocument.class,导致DocumentDocument.class缺少方法(ooxml-schemas-1.1.jar中的DocumentDocument.class没有Factory,poi-ooxml-lite-5.2.3.jar中的DocumentDocument.class有Factory);
2.项目中poi-ooxml-lite-5.2.3.jar是直接引的依赖包,而ooxml-schemas-1.1.jar是引用的其他依赖包间接引入的依赖包。已经查询过优先级:先引用的大于后引用的、直接引用的大于间接引用的。所以尝试过:在配置Project Settings -> Moudles -> depency中移动依赖的位置,让poi-ooxml-5.2.3.jar以及相关jar包移动到最前方,依赖包含ooxml-schemas-1.1.jar的依赖以及ooxml-schemas-1.1.jar自己放在后方、pom.xml中也是同样处理,结果还是没用,依旧报NoSuchFieldError。
3.自定义类加载器加载正确的类:(由于接触这块儿不多,借鉴了网上的写法)

/**
 * 自定义的类加载器
 */
public class CustomizeClassLoader extends ClassLoader{

    private String classPath;

    public CustomizeClassLoader(String classPath) {
        this.classPath = classPath;
    }

    /**
     * 重写findClass方法
     *
     * 如果不会写, 可以参考URLClassLoader中是如何加载AppClassLoader和ExtClassLoader的
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] data = loadBytes(name);
            return defineClass(name, data, 0, data.length);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }


    private byte[] loadBytes(String name) throws Exception {
        InputStream is = null;
        byte[] data = null;
        ByteArrayOutputStream baos = null;
        try {
            name = name.replace(".", "\\");
            // 如果path是jar包的路径,那么上述方法不能加载成功,抛出异常后执行此处的方法。
            JarFile jar = new JarFile(this.classPath);
            is = jar.getInputStream(jar.getEntry(name.replace("\\", "/") + ".class"));
            baos = new ByteArrayOutputStream();
            int ch = 0;
            while (-1 != (ch = is.read())) {
                baos.write(ch);
            }
            data = baos.toByteArray();
        } catch (Exception ex) {
            throw ex;
        } finally {
            try {
                if(is != null)
                    is.close();
                if(baos != null)
                    baos.close();
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
        return data;
    }

    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                /**
                 * 直接执行findClass()...什么意思呢? 首先会使用自定义类加载器加载类, 不在向上委托, 直接由
                 * 自己执行
                 *
                 * jvm自带的类还是需要由引导类加载器自动加载
                 */
                if (!name.equals("org.openxmlformats.schemas.wordprocessingml.x2006.main.DocumentDocument")) {
                    c = this.getParent().loadClass(name);
                } else {
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

    public static void main(String[] args) throws Exception {
        // 获取所有TestUse得到路径
        ClassLoader classloader = Thread.currentThread().getContextClassLoader();
//        Enumeration<URL> urls = classloader.getResources("org/openxmlformats/schemas/wordprocessingml/x2006/main/DocumentDocument.class");
//        Enumeration<URL> urls = classloader.getResources("com/duxiu/readfile/utils/ReadFileUtils.class");
        Enumeration<URL> urls = classloader.getResources("org/apache/poi/xwpf/usermodel/XWPFDocument.class");
        // 循环加载两个jar包下的
        while (urls.hasMoreElements()) {
            URL url = (URL) urls.nextElement();
            String fullPath = url.getPath();
            System.out.println(fullPath);
            String[] strs = fullPath.split("!");
            // 获取文件的路径以及文件名
            String path = strs[0].replace("file:/","");
            String fileName = strs[1].substring(1).replace(".class", "").replace("/", ".");
            CustomizeClassLoader loader = new CustomizeClassLoader(path);
            // 加载类
            Class<?> clazz = loader.loadClass(fileName);
            // 反射创建实体
            Object obj = clazz.getDeclaredConstructor().newInstance();
            Method method = clazz.getMethod("方法名", null);
            // 执行
            method.invoke(obj, null);
        }
    }

}

结果:能够使用自定义类加载器成功加载XWPFDocument.class,未成功加载DocumentDocument.class,也许是因为DocumentDocument.class是接口?
4.搜索到:如果当前类用的类加载器是AClassLoader,那么它内部的类也会使用AClassLoader。所以,在SpringMVC项目调用涉及到依赖的方法中,使用3中的自定义类加载器,那么到 DocumentDocument.Factory.parse(stream, DEFAULT_XML_OPTIONS);这一行也会使用自定义类加载器去加载DocumentDocument.class,如下:

public static String readFileNotFilter(String filePath){
        String articleContent = "";
        try{
            ClassLoader classloader = Thread.currentThread().getContextClassLoader();  // ParallelWebappClassLoader加载器
            Enumeration<URL> urls = classloader.getResources("com/duxiu/readfile/utils/ReadFileUtils.class");
            if (urls.hasMoreElements()) {
                URL url = (URL) urls.nextElement();
                String fullPath = url.getPath();
                System.out.println(fullPath);
                String[] strs = fullPath.split("!");
                // 获取文件的路径以及文件名
                String path = strs[0].replace("file:/", "");
                String fileName = strs[1].substring(1).replace(".class", "").replace("/", ".");
                CustomizeClassLoader loader = new CustomizeClassLoader(path);
                // 加载类
                Class<?> clazz = loader.loadClass(fileName);
                // 反射创建实体
                Object obj = clazz.getDeclaredConstructor().newInstance();
                articleContent = ((ReadFileUtils)obj).readFileByTypeV1(filePath, null);
            }
        }catch (Exception e){
            e.printStackTrace();
        }

//  原引用了依赖的地方
//        articleContent = ReadFileUtils.readFileByTypeV1(filePath,null);
        articleContent = OperateContent.removeContent(articleContent, filePath, null);
        return articleContent;
    }

5.如果4中能够取出对应的类的话,预计会遍历两个相同的DocumentDocument.class,取出正确的那一个。项目中自定义类加载器需要加载的类:public class ReadFileUtils extends AbstractReadFile {.}。目前自定义类加载器的main中加载ReadFileUtils.class失败,项目代码中加载ReadFileUtils.class也同样失败。

触发错误的条件:
依赖包poi-ooxml-lite-5.2.3.jar和ooxml-schemas-1.1.jar;

public static String readDocx(String path, ArticleProp p) {
        OPCPackage opcPackage = null;

        try {
            opcPackage = POIXMLDocument.openPackage(path);
            XWPFDocument document = new XWPFDocument(opcPackage);  // 触发错误的行
            List<XWPFHeader> headers = document.getHeaderList();
        } finally {
            if (opcPackage != null) {
                try {
                    opcPackage.close();
                } catch (Exception var29) {
                    LOGGER.error("not affect the use--->AbstractReadFile--->readDocx()-->opcPackage close(): path={}, error msg={}, ", path, var29.getMessage());
                    opcPackage = null;
                }
            }

        }
    }

手上太忙了,很多地方来不及仔细思考,所以打算先用maven-shade-plugin给重名的部分再重新取一个不重复的名字。
但是,还是对能否使用类加载器解决保持疑问,如果各位有能够正确执行的类加载器解法务必告知~,越关键越详细越好,必有重金酬谢。

  • 写回答

26条回答 默认 最新

  • 专家-赤兔[在线] 全栈领域优质创作者 2024-04-01 11:29
    关注
    获得1.32元问题酬金

    引自免费微信小程序:皆我百晓生

    针对你所描述的问题,当多个jar包中存在同名类且方法不一致时,在Java的类加载机制下,可能会因为类加载器加载顺序导致预期类的方法找不到。根据你提供的信息,ooxml-schemas-1.1.jar 中的 DocumentDocument 类没有 Factory 方法,而 poi-ooxml-lite-5.2.3.jar 中有。尽管你试图调整Maven依赖顺序,但由于Maven对于依赖冲突的解决策略可能并不按照你期望的方式来加载,因此你需要更精细地控制类加载行为。

    这里提供一种基于类加载器的解决方案,但需要注意的是,这种方式较为复杂且可能导致与容器环境或其他类加载器机制产生兼容性问题。你可以考虑通过定制类加载器并设置其为线程上下文类加载器(Thread.currentThread().setContextClassLoader()),确保解析 XWPFDocument 类时加载的是正确的 DocumentDocument 实现类。

    首先,创建一个新的自定义类加载器,并覆盖 findClassloadClass 方法,确保当加载指定类时从特定jar包中加载:

    import java.io.ByteArrayOutputStream;
    import java.io.InputStream;
    import java.net.URL;
    import java.net.URLClassLoader;
    import java.security.SecureClassLoader;
    import java.util.Enumeration;
    
    public class CustomOoxmlClassLoader extends SecureClassLoader {
    
        private final String poiOoxmlLiteJarPath;
    
        public CustomOoxmlClassLoader(ClassLoader parent, String poiOoxmlLiteJarPath) {
            super(parent);
            this.poiOoxmlLiteJarPath = poiOoxmlLiteJarPath;
        }
    
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            if (name.equals("org.openxmlformats.schemas.wordprocessingml.x2006.main.DocumentDocument")) {
                byte[] classData = loadClassData(name);
                if (classData != null) {
                    return defineClass(name, classData, 0, classData.length);
                }
            }
            return super.findClass(name);
        }
    
        private byte[] loadClassData(String className) throws Exception {
            try (JarFile jar = new JarFile(poiOoxmlLiteJarPath)) {
                String entryName = className.replace(".", "/") + ".class";
                InputStream is = jar.getInputStream(jar.getEntry(entryName));
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                int ch;
                while ((ch = is.read()) != -1) {
                    baos.write(ch);
                }
                return baos.toByteArray();
            }
        }
    
        @Override
        protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
            if (name.startsWith("org.apache.poi.xwpf.usermodel.")) {
                // 当加载Apache POI相关的类时,优先使用自定义类加载器
                return findClass(name);
            }
            return super.loadClass(name, resolve);
        }
    
        public static void setCustomClassLoaderIfNecessary(String poiOoxmlLiteJarPath) {
            ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
            if (!(currentClassLoader instanceof CustomOoxmlClassLoader)) {
                CustomOoxmlClassLoader customClassLoader = new CustomOoxmlClassLoader(currentClassLoader, poiOoxmlLiteJarPath);
                Thread.currentThread().setContextClassLoader(customClassLoader);
            }
        }
    }
    

    然后,在初始化或使用 XWPFDocument 之前,设置自定义类加载器为线程上下文类加载器:

    String poiOoxmlLiteJarPath = "your/path/to/poi-ooxml-lite-5.2.3.jar"; // 替换为实际路径
    CustomOoxmlClassLoader.setCustomClassLoaderIfNecessary(poiOoxmlLiteJarPath);
    
    // 现在可以安全地初始化 XWPFDocument,因为它会使用自定义类加载器加载依赖
    OPCPackage opcPackage = POIXMLDocument.openPackage(filePath);
    XWPFDocument document = new XWPFDocument(opcPackage);
    

    此方案的核心思想是在加载 XWPFDocument 之前,强制将线程上下文类加载器设置为自定义加载器,这样在解析相关类时会优先使用包含正确 Factory 方法的 DocumentDocument 类。

    另外,请注意,虽然这种方法理论上可行,但在生产环境中仍需谨慎对待,尤其是当应用部署在容器环境下(如Tomcat、Jetty等),可能需要额外处理与容器内置类加载器的兼容性问题。如果有可能,最优方案仍然是找出依赖冲突的原因,并尽可能通过合理管理Maven依赖关系解决此类问题。

    评论 编辑记录

报告相同问题?

问题事件

  • 系统已结题 4月9日
  • 创建了问题 4月1日

悬赏问题

  • ¥15 有偿求苍穹外卖环境配置
  • ¥15 代码在keil5里变成了这样怎么办啊,文件图像也变了,
  • ¥20 Ue4.26打包win64bit报错,如何解决?(语言-c++)
  • ¥15 clousx6整点报时指令怎么写
  • ¥30 远程帮我安装软件及库文件
  • ¥15 关于#自动化#的问题:如何通过电脑控制多相机同步拍照或摄影(相机或者摄影模组数量大于60),并将所有采集的照片或视频以一定编码规则存放至规定电脑文件夹内
  • ¥20 深信服vpn-2050这台设备如何配置才能成功联网?
  • ¥15 Arduino的wifi连接,如何关闭低功耗模式?
  • ¥15 Android studio 无法定位adb是什么问题?
  • ¥15 C#连接不上服务器,