同じJavaロジックなのに違う環境で違う動きがした話

最近XML変換関連のJavaロジックを修正する時に、違う環境で違う動きをしたことがありました。
主に2つの問題に遭遇し、色々調査して結構勉強になりました。
この文章で問題解決した経緯を紹介したいと思います。

# ケース1: TransformerFactory

## 事象
XML Documentをbytesやstringに変換するため、TransformerFactoryを使っています。
実装の中に以下の一文があります。

``` Java
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
```
こちらの1文は特に修正していませんが、環境が変わることによってXMLの結果の差異はありました。
修正後のXMLにstandalone="no"が出るという差分です。

## 調査
なんで？と思い、TransformerFactoryなどに何が入っているのか調べました。
以下のようにSystemOutします。

``` Java
System.out.println(transformerFactory.getClass().getCanonicalName());
System.out.println(transformer.getClass().getCanonicalName());
```

以下のような結果が出ています。
変更前
``` Java
transformerFactory : org.apache.xalan.processor.TransformerFactoryImpl			
transformer : org.apache.xalan.transformer.TransformerIdentityImpl
```
変更後
``` Java
transformerFactory : com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl
transformer : com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl
```

`org.apache.xalan.processor.TransformerFactoryImpl`は`xalan-${version}.jar`にあります。
変更前は`xalan-${version}.jar`を参照しています。
変更後の環境には特にxalanがなく、Java標準のものが参照
このjarは従前の環境に存在し、且つ参照されていることが今回の差異の元というのがわかりました。

## 修正
新しい環境には`xalan-${version}.jar`はありませんでしたので、それを出荷しました。
ちょっと小ネタですが、`xalan-${version}.jar`は実行時`serializer-${version}.jar`も必要ですので、それも出荷しました。

TransformerFactoryをxalanのjarのものに固定すれば既存の動きを互換できますので、以下のように実装を変えました。
``` Java
            TransformerFactory transformerFactory = TransformerFactory.newInstance(
                    TransformerFactoryImpl.class.getCanonicalName(), null);
            Transformer transformer = transformerFactory.newTransformer();
```
TransformerFactoryImpl.class.getCanonicalName()の結果は`org.apache.xalan.processor.TransformerFactoryImpl`になり、既存の処理結果と同じようになることを確認できました。

## Sonarqubeの指摘
TransformerFactoryを固定することによって、処理の問題は解消できていますが、社内で導入されたSonarqubeに脆弱性の指摘をされました。
```
XML parsers should not be vulnerable to XXE attacks (java:S2755)
```
修正方法も提示されています。
```Java
TransformerFactory factory = javax.xml.transform.TransformerFactory.newInstance();
// to be compliant, prohibit the use of all protocols by external entities:
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
```
ただし、これ通りに実装すると、Exceptionが発生しました。
```Java
Caused by: java.lang.IllegalArgumentException: サポートされていません: http://javax.xml.XMLConstants/property/accessExternalDTD
at org.apache.xalan.processor.TransformerFactoryImpl.setAttribute(TransformerFactoryImpl.java:571)
```
xalanのjarを使っているからエラーになったようです。
Java標準にすればエラーはなくなりますが、修正前後の互換を優先したいためxalanを使うようにしました。

これで一旦落ち着いた？と思ったら別のところで同類の問題が起きました。

# ケース2: DocumentBuilderFactory

## 事象
まず発端としてはTransformerFactoryの件と別の環境でエラーが出ました。（特に修正していない部分）
``` Java
ライセンスエラー (Error[0xc800100d]: The license for Feature "SolarNativeRuntime" is no longer valid because the license expiration date has been reached.)

java.lang.AbstractMethodError: org.apache.xerces.dom.DeferredElementImpl.setIdAttribute(Ljava/lang/String;Z)V
	at 
```
そしてエラーが発生した箇所は以下の実装と関係している模様です。

``` Java
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
```
（TransformerFactoryと似たDocumentBuilderFactoryですね。）

## 調査

## 異常終了の原因調査
`org.apache.xerces.dom.DeferredElementImpl`はxercesImpl.jarにあるもので、xercesImpl.jarと関係しているようです。

変更前の環境にはjarが存在していますが、変更後の環境にはxercesImpl.jarはありますが、クラスパスにはありません。
そのため、参照されていない可能性が高いと思いました。
TransformerFactoryと似た調べ方で、DocumentBuilderFactoryに何が入っているのか確認しました。
以下のようにSystemOutします。
``` Java
System.out.println(documentBuilderFactory.getClass().getCanonicalName());
System.out.println(documentBuilder.getClass().getCanonicalName());
```
以下の結果を得ました。

変更前
``` Java
DocumentBuilderFactory : org.apache.xerces.jaxp.DocumentBuilderFactoryImpl
DocumentBuilder : org.apache.xerces.jaxp.DocumentBuilderImpl
```
変更後
``` Java
DocumentBuilderFactory : org.apache.xerces.jaxp.DocumentBuilderFactoryImpl
DocumentBuilder : org.apache.xerces.jaxp.DocumentBuilderFactoryImpl
```
変更前後のどちらもxercesImpl.jarが参照されているように見えます。
でも変更後の環境のクラスパスがないですけどね。じゃどうやって参照されているのでしょう？（謎）
とにかくxercesImpl.jarを消したらどうなるのか確認しました。
元々異常終了だったのですが、消したら正常終了しました。
そして以下のようにDocumentBuilderFactoryの参照クラスが変わりました。（xercesImpl.jarはもう参照されていない）
``` Java
DocumentBuilderFactory : com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl
DocumentBuilder : com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderImpl
```
ということでクラスパスを経ずにどうやって参照されているはわかりませんが、xercesImpl.jarが参照され、異常終了となった原因というところまでわかりました。

## xercesImpl.jarはなぜ参照されている？

なぜ参照されているかについて、問題が起きた処理のJavaDocを確認しました。(DocumentBuilderFactory#newInstance)
英語の方は原文ですのでそちらを確認しました。

https://docs.oracle.com/javase/8/docs/api/javax/xml/parsers/DocumentBuilderFactory.html#newInstance

> Obtain a new instance of a DocumentBuilderFactory. This static method creates a new factory instance. This method uses the following ordered lookup procedure to determine the DocumentBuilderFactory implementation class to load:
> - Use the javax.xml.parsers.DocumentBuilderFactory system property.
> - Use the properties file "lib/jaxp.properties" in the JRE directory. This configuration file is in standard java.util.Properties format and contains the fully qualified name of the implementation class with the key being the system property defined above. The jaxp.properties file is read only once by the JAXP implementation and it's values are then cached for future use. If the file does not exist when the first attempt is made to read from it, no further attempts are made to check for its existence. It is not possible to change the value of any property in jaxp.properties after it has been read for the first time.
> - Uses the service-provider loading facilities, defined by the ServiceLoader class, to attempt to locate and load an implementation of the service using the default loading mechanism: the service-provider loading facility will use the current thread's context class loader to attempt to load the service. If the context class loader is null, the system class loader will be used.
> - Otherwise, the system-default implementation is returned.
Once an application has obtained a reference to a DocumentBuilderFactory it can use the factory to configure and obtain parser instances.

上記の記述通り、DocumentBuilderFactoryはシステムプロパティなどを順番的に確認して何をロードするか決めています。
3つ目の記述ところは外部Jar関連のようです。
ServiceLoaderの話があり、そこの情報を確認しました。

https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html

> If com.example.impl.StandardCodecs is an implementation of the CodecSet service then its jar file also contains a file named
> META-INF/services/com.example.CodecSet

パスがMETA-INF/servicesの設定ファイルがあるようで、そこの設定が参照されている模様です。
xercesImpl.jarを確認すると、META-INF\services\javax.xml.parsers.DocumentBuilderFactoryのファイルがあり、そこに`org.apache.xerces.jaxp.DocumentBuilderFactoryImpl`の記述がありました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3335050/defdec44-929e-59f8-5149-af601deda7cb.png)

なるほど、クラスパスにはないが、jar自体はサーバーにあるので、その中にある設定ファイルが参照されている模様です。
謎は少し解けました。

## TransformerFactoryの問題が起きた環境での調査

TransformerFactoryの問題が起きた環境ではなぜDocumentBuilderFactoryの問題が起きていないでしょう？というのが謎で調べました。

同様にDocumentBuilderFactoryに何が入っているのか確認しました。

変更前後の環境はどちらもxercesImpl.jarはありません。
ただし、以下のような結果が出てびっくりしました。

変更前
``` Java
DocumentBuilderFactory : org.apache.xerces.jaxp.DocumentBuilderFactoryImpl
DocumentBuilder : org.apache.xerces.jaxp.DocumentBuilderImpl
```
変更後
``` Java
DocumentBuilderFactory : com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl
DocumentBuilder : com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderImpl
```
'com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl'は標準Javaにあるもののようです。

あれ、xercesImpl.jarがないのになぜ変更前の環境はxercesImpl.jarが参照されているでしょう。
pomは一瞬xercesImplがあるように見えますが、scopeがtestなので、ビルド時は参照されません。
```Java
            <dependency>
                <groupId>xerces</groupId>
                <artifactId>xercesImpl</artifactId>
                <version>xxxx</version>
                <type>jar</type>
                <scope>test</scope>
            </dependency>
```
他の方が調べて分かったのですが、変更前の環境はIBM Javaを使っていて、jre/libフォルダ以下にあるxml.jarにorg.apache.xerces.jaxp.DocumentBuilderFactoryImplが内包されていました。そのクラスが参照されていると思われます。

## 修正
DocumentBuilderFactoryをxercesImpl.jarなどにある`org.apache.xerces.jaxp.DocumentBuilderFactoryImpl`ではなく、Java標準の
`com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl`にすれば解決できました。
```Java
        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(
                "com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl", null);
        DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
```

## Sonarqubeの指摘
TransformerFactoryの場合と同様に、脆弱性の指摘がありました。（`XML parsers should not be vulnerable to XXE attacks`）

修正方法も提示されています。
```Java
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// to be compliant, completely disable DOCTYPE declaration:
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
// or completely disable external entities declarations:
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
// or prohibit the use of all protocols by external entities:
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
```
どちらにすべき？と思い、以下の文章のJAXP DocumentBuilderFactory, SAXParserFactory and DOM4Jの章で最初に推奨された実装でやりました。

https://cheatsheetseries.owasp.org/cheatsheets/XML_External_Entity_Prevention_Cheat_Sheet.html#java

つまり、最終的に実装は以下になっています。
```Java
        DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(
                "com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl", null);
        documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);

        DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
```

# 再度TransformerFactoryを確認

TransformerFactoryの件を解決した際は従前の動きを互換しただけで、なぜ`xalan-${version}.jar`が参照されているかそこまで見ていませんでした。
DocumentBuilderFactoryの件を経て見当がついていますので、この節で少し再度確認するようにします。

まずはJavaDocです。（TransformerFactory#newInstance()）

https://docs.oracle.com/javase/8/docs/api/javax/xml/transform/TransformerFactory.html

> Use the service-provider loading facilities, defined by the ServiceLoader class, to attempt to locate and load an implementation of the service using the default loading mechanism: the service-provider loading facility will use the current thread's context class loader to attempt to load the service. If the context class loader is null, the system class loader will be used.

DocumentBuilderFactoryと似た記述がありますので、jarの中にある設定ファイルが参照されていると思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3335050/972db7b1-209f-da2f-f927-97c01f3ed1bd.png)

想定通り、`META-INF/services/javax.xml.transform.TransformerFactory`というファイルに`org.apache.xalan.processor.TransformerFactoryImpl`の記述があり、それが参照されていると思います。

# まとめ
今回の件は難しく色々勉強になりました。勉強になったポイントは以下です。
- instanceを生成する処理は外部jarやJavaの種類によって動作が変わるケースがあります。
  そのため、複数種類の環境の動作を担保したい場合、固定にした方がいいと思います
- Jarの動きを調査する際には公式のドキュメントを見た方がよさそうです

# 最後に

外部jarやJavaの種類によって動作が違う問題の調査や解決は難しく、自分も一部しかわかっていないという状態です。
できるだけ何の問題に遭遇し、どう調査し解決したのか、経緯を具体的に記載しました。
皆さんが似た問題に遭遇した際の調査の参考になったら幸いです！