読者です 読者をやめる 読者になる 読者になる

この日記は私的なものであり所属会社の見解とは無関係です。 GitHub: takahashikzn

package-infoをJavadoc用途以外で使う

以前の日記でも書きましたが、
package-info.javaはインタフェースとしてコンパイルされます。
で、"package-info"という名前だけあって、パッケージ自身のメタ情報を含めるのにちょうど良い。


今やっている仕事で、『特定の処理を全てのクラスに適用する』というオーダーがありました。
コードで書くと、まあこんな感じの内容。

public class DoSomethingToAllClasses {

    // 指定したディレクトリ配下のすべてのクラスに対して処理を行う
    public void execute(File rootDir) {
    
        for (final Class<?> clazz : findAllClasses(rootDir)) {
            this.doSomething(clazz);
        }
    }


    // 何かの処理
    private void doSomething(final Class<?> clazz) { ... }


    // 指定したディレクトリ配下のクラスを検索する
    private Set<Class<?>> findAllClasses(final File dir) {

        final Set<Class<?>> classes = new HashSet<Class<?>>();

        for (final File f : dir.listFiles()) {
            if (f.isDirectory()) {
                classes.addAll(this.findAllClasses(f));
            } else if (f.getName().endsWith(".class")) {
                classes.add(this.resolveClassFromClassFile(f));
            }
        }

        return classes;
    }


    private Class<?> resolveClassFromClassFile(final File classFile) { ... }
}

これで、要求は満たせました。ところが。。。

一部のパッケージだけは除外したい

という処理を追加することに。
で、一番単純なのがこういうやり方。

public class DoSomethingToAllClasses {

    // 無視するパッケージ
    private static final Set<Package> IGNORE_PACKAGES = new HashSet<Package>(Arrays.asList(
        Package.getPackage("foo.aaa"),
        Package.getPackage("foo.bbb")
    ));


    //...(変更がない箇所は省略 )


    private Set<Class<?>> findAllClasses(final File dir) {

        final Set<Class<?>> classes = new HashSet<Class<?>>();

        outer:
        for (final File f : dir.listFiles()) {
            if (f.isDirectory()) {
                classes.addAll(this.findAllClasses(f));
                continue;
            } else if (!f.getName().endsWith(".class")) {
                continue;
            }

            final Class<?> clazz = this.resolveClassFromClassFile(f);
            
            // 無視パッケージに含まれるクラスなら無視する
            for (final Package ignorePackage : IGNORE_PACKAGES) {
                if (this.contains(ignorePackage, clazz)) {
                    continue outer;
                }
            }

            classes.add(clazz);
        }

        return classes;
    }


    // クラスが、指定したパッケージまたはそのサブパッケージに含まれるか否か
    private boolean contains(Package p, Class<?> clazz) {

        // "p==null"→デフォルトパッケージ
        if (p == null)
            return true; 
        else if (clazz.getPackage() == p)
            return true;
        
        final Package parent = Package.getPackage(p.getName().replaceFirst("[.]\\w+$", ""));
        return this.contains(parent, clazz);
    }

}


これで問題ないのですが、無視対象のパッケージが増えるたびに
DoSomethingToAllClassesを修正する(IGNORE_PACKAGESのエントリを増やす)必要があります。


それがイヤならば、
『DoSomethingToAllClasses#executeの引数に、無視パッケージ一覧を渡してやればいいのでは?』
という話になるのですが、とある事情があり、それもできない。さてどうするか。

パッケージにアノテーションを付ける

最終的に実施したのが、
『package-info.javaを用意し、パッケージにアノテーションを付けて無視パッケージか否かを判定する』
という方法。具体的には次の通りです。


/**
 * 無視するパッケージであることを示すアノテーション
 */
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PACKAGE)
public @interface Ignore {}


public class DoSomethingToAllClasses {

    //...(変更がない箇所は省略 )

    private Set<Class<?>> findAllClasses(final File dir) {

        final Set<Class<?>> classes = new HashSet<Class<?>>();

        outer:
        for (final File f : dir.listFiles()) {
            if (f.isDirectory()) {
                classes.addAll(this.findAllClasses(f));
            } else if (f.getName().endsWith(".class")) {
                final Class<?> clazz = this.resolveClassFromClassFile(f);
                
                if (!isIgnoringPackage(clazz.getPackage())) {
                    classes.add(clazz);
                }
            }
        }

        return classes;
    }

    private boolean isIgnoringPackage(Package p) {
        if (p == null)
            return false;
        else if (p.isAnnotationPresent(Ignore.class))
            return true;
        else {
            final Package parent = ...;
            return isIgnoringPackage(parent);
        }
    }
}


package-info.javaはこんな感じ。(foo.barパッケージ)

@Ignore
package foo.bar;


これで、無視パッケージが追加されてもpackage-info.javaを追加すればよいだけ、になります。

この他にも

package-info.javaの中にはpublicでないクラスやアノテーションをフツーに宣言できます。
パッケージ共通の実装をこの中に含めておく、というのもアリかもしれません。