Java の DNS キャッシュの確認方法とサンプルコード

Java ではホスト名の解決を行ったときに JVM にその結果がキャッシュされます。

ググってみるとどうやらそういう仕様らしいということは分かるのですが、
実際にキャッシュされているホスト名、IPアドレスを確認する方法がすぐには分からなかったので整理してみました。

キャッシュ保持期間の設定方法などは [こちらの記事](http://qiita.com/arai-wa/items/492d91d85a7473007e37) が詳しいです。



#キャッシュが保持されている箇所#
java.net.InetAddress の private フィールド addressCache（名前解決に失敗した場合は negativeCache）に格納されます。

```java
Field acf = InetAddress.class.getDeclaredField("addressCache");
acf.setAccessible(ture);
Object addressCache = acf.get(InetAddress.class);
```

この addressCache の型は InetAddress のインナークラスである Cache 型であり、実際のアドレス自体はさらにその中の cache という private フィールドに Map として格納されています。

```java
Field cf = Class.forName("java.net.InetAddress$Cache").getDeclaredField("cache");
cf.setAccessible(true);
Map<String, Object> cacheList = (Map<String, Object>)cf.get(addressCache);
```

この Map の key がホスト名なんですが、value は InetAddress のインナークラスである CacheEntry 型であり、これもそのままでは参照できません。名前解決後のIPアドレスはCacheEntryクラスの private フィールド addresses に格納されています。

```java
Field f = Class.forName("java.net.InetAddress").getDeclaredField("addresses");
f.setAccessible(true);
InetAddress[] addresses = (InetAddress[])f.get(cacheList);
```

これでようやくキャッシュされた IPアドレスにたどり着くことができます。

#キャッシュされたアドレスの出力用サンプルコード#

[GitHub](https://github.com/ksugimori/DNSCacheTest) にも置いています。
JDK-1.6, 1.7 では動作確認済。

```java
    public static void main(String[] args) {
        try {
            InetAddress.getByName("localhost");
            InetAddress.getByName("www.example.com");
            InetAddress.getByName("unknown.example.com"); // 名前解決できないはず
        } catch (UnknownHostException e) {
            // 名前解決できなかった場合は InetAddress#negativeCache にホスト名が格納される
        }

        printCacheList(CacheType.Positive);
        printCacheList(CacheType.Negative);
    }

    public enum CacheType {
        Positive("addressCache"), Negative("negativeCache");
        private String fieldName;
        private CacheType(String name) {
            this.fieldName = name;
        }
        public String getFieldName() {
            return this.fieldName;
        }
    }

    /**
     * キャッシュされたホスト名とアドレス（名前解決に失敗した場合はキャッシュの有効期限）を出力します
     * 
     * @param type Positive/Negative 
     */
    @SuppressWarnings("unchecked")
    public static void printCacheList(CacheType type)  {
        System.out.println(type.getFieldName());

        // Cache 型の private フィールドを取得
        Object addressCache = getField(InetAddress.class, "java.net.InetAddress", type.getFieldName());

        // キャッシュの一覧を取得
        Map<String, Object> cacheMap = (Map<String, Object>)getField(addressCache, "java.net.InetAddress$Cache", "cache");

        // 一覧表示
        for (Entry<String, Object> entry : cacheMap.entrySet()) {
            String hostname = entry.getKey();
            Object cacheEntry = entry.getValue();
            switch(type) {
            case Positive:
                // 名前解決後の InetAddress オブジェクトを取得（0番目は IPv4, 1番目は IPv6）
                InetAddress[] addresses = (InetAddress[])getField(cacheEntry, "java.net.InetAddress$CacheEntry", "addresses");
                System.out.printf("  %-20s => %s%n", hostname, addresses[0].getHostAddress());
                break;
            case Negative:
                // キャッシュの有効期限を取得
                long expiration = (long)getField(cacheEntry, "java.net.InetAddress$CacheEntry", "expiration");
                System.out.printf("  %-20s (Expiration Date: %s)%n", hostname, new Date(expiration));
                break;
            }
        }
    }

    /**
     * オブジェクト内のprivateフィールドを取得します
     * 
     * @param obj Object
     * @param className クラス名
     * @param fieldName フィールド名
     * @return obj 内の fieldName 項目
     */
    public static Object getField(Object obj, String className, String fieldName) {
        try {
            Field field = Class.forName(className).getDeclaredField(fieldName);
            field.setAccessible(true);
            return field.get(obj);
        } catch (Exception e) {
            e.getStackTrace();
            return null;
        }
    }

```