第一章:前言
1.本文解决了POI读取xls和xlsx的各种问题
2.本文提供了完善的下载库,并且CSDN下载只要2积分。
3.本文解决了安卓上的适配问题
第二章:介绍
我们都知道,可以利用poi库实现在java中读取excel表格
excel有两种格式,xls和xlsx,对应分别需要使用HSSFWorkbook和XSSFWorkbook去读取。
如果只导入poi.jar的话,那么只能读取xls格式的excel,如果读取xlsx格式的就是提示问题一的中错误。
具体POI库的用法就不讲了,本文主要是使用poi库读取xlsx格式文件的踩坑记录,读了本篇文章,可以少踩好多坑。
文章底部附有本身涉及到的所有jar的下载链接。
如果只是想最终解决问题的话,直接看第四章即可。
第三章:排查解决流程
问题一:eg XSSF instead of HSSF
这个问题就是仅poi不支持xlsx格式文件读取,需要导入poi-ooxml库才可以。
问题二:error: java.lang.NoClassDefFoundError: Failed resolution of: Lorg/apache/xmlbeans/XmlOptions;
导入poi-ooxml.jar后,抛这个异常,原因是xmlbeans库没有导入。导入xmlbeans库2.0.0的版本。
问题三:java.lang.NoClassDefFoundError: Failed resolution of: Lorg/dom4j/Namespace;
导入xmlbeans.jar后提示这个,是没有导入log4j库导致。
问题四:org.apache.poi.POIXMLException: java.lang.reflect.InvocationTargetException
这个我看网上的说法,是xmlbean版本太高导致。于是改低版本到1.0.0。
问题五:3.java.lang.NoSuchMethodError: No virtual method setSaveAggressiveNamespaces()Lorg/apache/xmlbeans/XmlOptions;
导入xmlbean1.0.0的版本,提示方法不存在,确实1.0.0的jar里面没有,2.0.0有但又有别的错误。成了死循环了。
于是抛开现状,想直接把poi升级到最新版本也许可以。JAVA环境下,这问题确实解决了。解决方案详见4.1。
问题六:The 'namespace-prefix' feature is not supported while the 'namespaces' feature is enabled.
java环境跑通后,发现安卓设备上跑还是不行,提示这个错误。而且无论怎么百度谷歌都找不到解决方案,无奈只能自己尝试去解决。
1.首先找到poi的官方git地址,下载源码:GitHub - apache/poi: Mirror of Apache POI
2.官方库是gradle的形式编译的。所以可以使用android studio打开。poi下有很多项目,我们选择poi-examples打开。
3.由于不是安卓项目,所以不会有默认可运行项目,需要我们手动去配置。点击项目,添加application,选择运行环境和主类就可以了,如下图所示。
4.然后点击运行,项目就可以跑起来了。我这里配置的main函数入口类是:
org.apache.poi.examples.xssf.usermodel.AligningCells
5.同时跑poi项目和安卓项目,读取同一份xlsx文件,果然,poi项目成功,安卓项目失败。
6.我们一层一层的追寻不一样的地方,终于找到了区别。
java里面,SAXParserFactory的实现类是com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl,
而安卓里面SAXParserFactory的实现类被谷歌改掉了,是org.apache.harmony.xml.parsers.SAXParserFactoryImpl。
而setFeature添加属性的时候,两者实现方式不一样。
一共要添加六个属性:
saxFactory.setValidating(false);//1
saxFactory.setNamespaceAware(true);//2
trySetSAXFeature(saxFactory, "http://javax.xml.XMLConstants/feature/secure-processing", true);//3
trySetSAXFeature(saxFactory, "http://apache.org/xml/features/nonvalidating/load-dtd-grammar", options.isLoadDTDGrammar());//4
trySetSAXFeature(saxFactory, "http://apache.org/xml/features/nonvalidating/load-external-dtd", options.isLoadExternalDTD());//5
trySetSAXFeature(saxFactory, "http://apache.org/xml/features/disallow-doctype-decl",options.disallowDocTypeDeclaration());//6
JAVA:
public void setFeature(String name, boolean value) throws ParserConfigurationException, SAXNotRecognizedException, SAXNotSupportedException {
if (name == null) {
throw new NullPointerException();
} else if (name.equals("http://javax.xml.XMLConstants/feature/secure-processing")) {
if (System.getSecurityManager() != null && !value) {
throw new ParserConfigurationException(SAXMessageFormatter.formatMessage((Locale)null, "jaxp-secureprocessing-feature", (Object[])null));
} else {
this.fSecureProcess = value;
this.putInFeatures(name, value);
}
} else {
this.putInFeatures(name, value);
try {
this.newSAXParserImpl();
} catch (SAXNotSupportedException var4) {
this.features.remove(name);
throw var4;
} catch (SAXNotRecognizedException var5) {
this.features.remove(name);
throw var5;
}
}
}
安卓:
@Override
public void setFeature(String name, boolean value) throws SAXNotRecognizedException {
if (name == null) {
throw new NullPointerException("name == null");
}
if (!name.startsWith("http://xml.org/sax/features/")) {//安卓就是这行抛异常的
throw new SAXNotRecognizedException(name);
}
if (value) {
features.put(name, Boolean.TRUE);
} else {
// This is needed to disable features that are enabled by default.
features.put(name, Boolean.FALSE);
}
}
所以安卓只能添加这六个中的其中2个,从而导致了问题的发生。
问题七:解决安卓中替换SAXParserFactoryImpl类的问题
方法1:反射改变SAXParserFactoryImpl的实现类。
走不通,安卓中是每次new出来的,没有成员变量。
public static SAXParserFactory newInstance() {
// instantiate the class directly rather than using reflection
return new SAXParserFactoryImpl();
}
方法2:XMLHelper中是通过
SAXParserFactory factory = SAXParserFactory.newInstance();
的方式生成SAXParserFactoryImpl的,这里直接new一个
com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl是不是就可以了呢?
但是需要改POI源码,改了之后打包jar,发现报错。
Forbidden class/interface use: com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl [non-public internal runtime class in Java 1.8]
in org.apache.poi.util.XMLHelper (XMLHelper.java:154)
Scanned 1633 class file(s) for forbidden API invocations (in 3.46s), 1 error(s).
提示SAXParserFactoryImpl属于非公共内部运行时类,直白点说就是不能直接new。
方法3:自己写一个一摸一样的SAXParserFactoryImpl。
也不行,SAXParserFactoryImpl中有引用到SAXParserImpl,SAXParserImpl的构造方法是protected的,只允许同一包名下使用。
方法4:那我反射生成SAXParserFactoryImpl是否可以呢?实验了下,发现安卓上竟然没有
com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl这个类。
只有org.apache.harmony.xml.parsers.SAXParserFactoryImpl
方法5:参照网上的开源库poi-on-android
问题八:processNamespacePrefixes问题
参照网上的开源库poi-on-android,发现其实大多数代码都是POI的代码,这个项目是可以跑在安卓上的。
所以感觉还是得找出根本的报错问题点,于是一行一行的代码调试。最终发现其实报错的是ExpatReader类的parse方法。
public void parse(InputSource input) throws IOException, SAXException {
if (processNamespacePrefixes && processNamespaces) {
/*
* Expat has XML_SetReturnNSTriplet, but that still doesn't
* include xmlns attributes like this feature requires. We may
* have to implement namespace processing ourselves if we want
* this (not too difficult). We obviously "support" namespace
* prefixes if namespaces are disabled.
*/
throw new SAXNotSupportedException("The 'namespace-prefix' " +
"feature is not supported while the 'namespaces' " +
"feature is enabled.");
}
...
}
这里我们发现有两个判断条件,两个条件都符合的时间才会报错。第一个应该是是否开启检查,第二个应该是是否检查通过的意思。所以我们的解决思路就是不开启namespacePrefixes检查。
我们继续看下哪里设置的这个值,在Locale类种SaxLoader类中。
private static abstract class SaxLoader
extends SaxHandler
implements ErrorHandler
{
SaxLoader(XMLReader xr, Locator startLocator)
{
super(startLocator);
_xr = xr;
try
{
//_xr.setFeature("http://xml.org/sax/features/namespace-prefixes", true);//就是这里
_xr.setFeature("http://xml.org/sax/features/namespaces", true);
_xr.setFeature("http://xml.org/sax/features/validation", false);
_xr.setProperty(
"http://xml.org/sax/properties/lexical-handler", this);
_xr.setContentHandler(this);
_xr.setDTDHandler(this);
_xr.setErrorHandler(this);
}
catch (Throwable e)
{
throw new RuntimeException(e.getMessage(), e);
}
try
{
_xr.setProperty("http://xml.org/sax/properties/declaration-handler", this);
}
catch (Throwable e)
{
logger.log(XBLogger.WARN, "SAX Declaration Handler is not supported", e);
}
}
Locale类在xmlbeans这个库里面。所以说如果我们干掉这行设置,是有可能通过验证的。所以我先断点调试改值试了下一把,果然把processNamespacePrefixes改为false就可以了。
所以下一步我们的任务就是,如何把这个检查去掉。
问题八:Locale类替换问题。
一开始的思路,自然是重新发布一个jar替换掉原来的xmlbeans。但是发现POI库中并不包含xmlbeans这个库。
所以还是参考poi-on-android这个框架,想到了类加载机制。如果我们创建一个同包名同类名的类,让classLoader优先加载这个,那么自然就不会去加载jar中的了。而我们可以对这个自己创建的Locale类进行修改。
说干就干,找到原来的Locale类,然后拷贝到应用目录下,创建相同的包名和类名。做完了之后,编译发现提示这三个类找不到:
import javax.xml.stream.Location;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
网上搜了下,大多数都是推荐直接依赖rt.jar的,这方案不好。一样是参照poi-on-android这框架吧。发现它依赖了
implementation 'stax:stax-api:1.0.1'
导入这个依赖,果然也可以了。
所以成功解决安卓上利用POI读取xlsx格式excel的问题了。完整解决方案参照4.2。
第四章:解决方案
4.1。解决java环境下poi读取xlsx的问题
1.下载最新的5.1.0的版本(PS:下载链接在文章底部)。
2.导入如下图所示的所有jar包。
3.对应的CellType类型改一下,int改为了枚举类型。
4.这时候XSSFWorkbook应该就可以正常使用了。
我的测试代码如下:
try {
FileInputStream fis = new FileInputStream(new File("demo.xlsx"));
Workbook wk = new XSSFWorkbook(fis);
Sheet sheet = wk.getSheetAt(0);
String sheetName = sheet.getSheetName();
System.out.print(sheetName);
} catch (Exception e) {
e.printStackTrace();
}
4.2。解决安卓上运行的问题。
在4.1的基础上进行如下的操作。
1.拷贝原有的Locale类到应用目录下,注释掉
this._xr.setFeature("http://xml.org/sax/features/namespace-prefixes", true);//todo 删掉这行是核心
这行代码
2.build文件中添加依赖:
implementation 'stax:stax-api:1.0.1'
3.这时候运行项目,应该就不会报错了。
具体可以参考我的一个开源项目中的使用:ExcelView/ExcelViewLib at main · September26/ExcelView · GitHub
具体使用类是:
ExcelViewLib/src/main/java/com/xt/excelviewlib/util/PoiUtil.java
五:下载链接
1.POI所有的jar下载: