CLOVER🍀

That was when it all began.

Reflections、Scannotation(とSpring)で、クラスパス上から特定のアノテーションが付与されたクラスを探し出す

少し前に試したエントリで、クラスをスキャンして自動登録…ができたらいいなぁみたいなところがあったのですが、以前それに近いことはエントリとして起こしたことがあります。

Javaで特定のパッケージ配下のクラスを検索する
http://d.hatena.ne.jp/Kazuhira/20120311/1331461906

ちなみに、ムダにScala版、Groovy版、Clojure版のエントリもあります…。

とはいえ、こういうのを自分であまり作りたいとは思わないので、できればライブラリなどを使いたいところです。

というわけで、ちょっと調べてみました。

Scanning Java annotations at runtime
http://stackoverflow.com/questions/259140/scanning-java-annotations-at-runtime

ここで挙がっているのは、Reflections、ClassIndex、Spring、infomas-aslですね。

ClassIndexは、Annotation Processing Toolで、ちょっと毛色が違いますが今回はパス。

ClassIndex
https://github.com/atteo/classindex

とはいえ、実行時にスキャンをかけるものに比べるとかなり早いそうな。

infomas-aslもちょっとパス。

infomas-asl
https://github.com/rmuller/infomas-asl

(足跡だけは残しておく…)

残ったものを、ちょっとずつ試してみたいと思います。

対象のコード

試すにも、スキャンして探し出す対象がなければ話が始まりません。今回は、対象としてこういうコードを用意しました。

//////////////////////////////////////////////////////////////////
// src/main/java/com/example/rest/TopResource.java
package com.example.rest;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("top")
public class TopResource {
    @GET
    @Path("hello")
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "Hello World";
    }
}

//////////////////////////////////////////////////////////////////
// src/main/java/com/example/rest/sub/SubResource.java 
package com.example.rest.sub;

import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("sub")
public class SubResource {
    @POST
    @Path("hello")
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "Hello World";
    }
}

//////////////////////////////////////////////////////////////////
// src/main/java/com/example/entity/TopEntity.java
package com.example.entity;

import javax.persistence.Entity;

@Entity
public class TopEntity { }


//////////////////////////////////////////////////////////////////
// src/main/java/com/example/entity/sub/SubEntity.java
package com.example.entity.sub;

import javax.persistence.Entity;

@Entity
public class SubEntity { }

JAX-RSJPA関連のアノテーションを付けたクラスを用意しました。前述のライブラリを使って、この中からクラスに@Entityが付いているもの、メソッドに@POSTが付いているものを探すようなコードを書いてみます。

Reflections

なんとなく良さそうかな?と思ったのが、こちらのReflections。

Reflections
https://github.com/ronmamo/reflections

Javadoc
http://reflections.googlecode.com/svn/trunk/reflections/javadoc/apidocs/index.html

特にアノテーションに限らず、クラスの継承関係やメソッドの引数、戻り値の型などでいろいろ検索できそうな感じです。

Maven依存関係はこちら。

    <dependency>
      <groupId>org.reflections</groupId>
      <artifactId>reflections</artifactId>
      <version>0.9.9</version>
    </dependency>

あ、確認用のコードには、JUnitとAssertJを使います。

使用したimport文はこちら。

import static org.assertj.core.api.Assertions.*;

import java.util.Set;
import java.util.stream.Collectors;

import org.reflections.Reflections;
import org.reflections.scanners.MethodAnnotationsScanner;
import org.reflections.util.ClasspathHelper;
import org.reflections.util.ConfigurationBuilder;
import org.reflections.util.FilterBuilder;

import com.example.rest.*;
import com.example.rest.sub.*;
import com.example.entity.*;
import com.example.entity.sub.*;

import org.junit.Test;

「com.example.〜」は、スキャン対象のコードですね。

@Entityが付与されたクラスを探すコード。

        Reflections reflections = new Reflections("com.example");

        Set<Class<?>> classesWithEntity =
            reflections.getTypesAnnotatedWith(javax.persistence.Entity.class);

        assertThat(classesWithEntity)
            .containsOnly(TopEntity.class, SubEntity.class);

かなりわかりやすいですね。パッケージ名を渡してその配下から取得するようにしています。他にも、基準となるオブジェクトを渡すなどの方法もあります。

@POSTが付与されたメソッドを持つクラスを探すコード。

        Reflections reflections =
            new Reflections(new ConfigurationBuilder()
                            .setUrls(ClasspathHelper.forPackage("com.example"))
                            .addScanners(new MethodAnnotationsScanner()));
            // 以下でも可
            // new Reflections("com.example", new MethodAnnotationsScanner());

        Set<Class<?>> classesHasMethodWithPost =
            reflections
            .getMethodsAnnotatedWith(javax.ws.rs.POST.class)
            .stream()
            .map(java.lang.reflect.Method::getDeclaringClass)
            .collect(Collectors.toSet());

        assertThat(classesHasMethodWithPost)
            .containsOnly(SubResource.class);

Reflectionsのデフォルトでは、アノテーションがクラスに付与されているもの探すScannerと、指定されたクラスおよびサブクラスに対するScannerしか設定されていないのですが、今回はメソッドを対象にするのでScannerを追加します。

取得できるのはMethodなので、その後でクラスの定義を取得しています。

単体で使うには、なかなか使いやすそうだなと思いました。依存関係がちょっと多め(Guava、JavassistFindBugs、SLF4J、dom4j、Gson、Servlet-API、Commons-VFS)なのがちょっとだけ難点ですかね…。

Scannotation

エントリの最初の紹介では名前を出しませんでしたが、Reflectionsが後継となったもの?のようです(Reflectionsの説明に、「Java runtime metadata analysis, in the spirit of Scannotations」とあるので…)。

Scannotation
http://scannotation.sourceforge.net/

Javadoc
http://scannotation.sourceforge.net/apidocs/index.html

GitHub
https://github.com/jharting/scannotation

なぜにこれを紹介するのかというと、もともとこのエントリを書くきっかけになっていたRESTEasy 2.Xが使っていたからです…。

では、サンプルを。

Maven依存関係。

    <dependency>
      <groupId>org.scannotation</groupId>
      <artifactId>scannotation</artifactId>
      <version>1.0.3</version>
    </dependency>

import文。

import static org.assertj.core.api.Assertions.*;

import java.io.IOException;
import java.net.URL;
import java.util.Set;
import java.util.stream.Collectors;

import org.scannotation.AnnotationDB;
import org.scannotation.ClasspathUrlFinder;

import com.example.rest.*;
import com.example.rest.sub.*;
import com.example.entity.*;
import com.example.entity.sub.*;

import org.junit.Test;

@Entityが付与されたクラスを探すコード。

        AnnotationDB db = new AnnotationDB();
        URL url = ClasspathUrlFinder.findResourceBase("com/example");

        db.scanArchives(url);

        Set<Class<?>> classesWithEntity =
            db
            .getAnnotationIndex()
            .get(javax.persistence.Entity.class.getName())
            .stream()
            .map(cs -> {
                try {
                    return Class.forName(cs);
                } catch (ReflectiveOperationException e) {
                    throw new RuntimeException(e);
                }
            })
            .collect(Collectors.toSet());

        assertThat(classesWithEntity)
            .containsOnly(TopEntity.class, SubEntity.class);

Reflectionsより、少し長くなりました。

@POSTが付与されたメソッドを持つクラスを探すコード。

        AnnotationDB db = new AnnotationDB();
        URL url = ClasspathUrlFinder.findResourceBase("com/example");

        db.scanArchives(url);

        Set<Class<?>> classesHasMethodWithPost =
            db
            .getAnnotationIndex()
            .get(javax.ws.rs.POST.class.getName())
            .stream()
            .map(cs -> {
                try {
                    return Class.forName(cs);
                } catch (ReflectiveOperationException e) {
                    throw new RuntimeException(e);
                }
            })
            .collect(Collectors.toSet());

        assertThat(classesHasMethodWithPost)
            .containsOnly(SubResource.class);

Reflectionsより手間はかかりますが、機能的にもちょっと落ちそうな印象です。また、もう更新が止まっている気がします。

その代わり、依存関係は少ないところが良いです(Javassist)。

Spring Framework

最後、オマケ的にSpringを。調べると、「Springを使ってたら、その機能あるよー」みたいなエントリをちょこちょこと見かけたので。

Java Code Examples for org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider
http://www.programcreek.com/java-api-examples/index.php?api=org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider

こちらは簡単に。

Maven依存関係。

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>4.1.6.RELEASE</version>
    </dependency>

import文。

import static org.assertj.core.api.Assertions.*;

import java.util.Set;
import java.util.stream.Collectors;

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.util.ClassUtils;

import com.example.rest.*;
import com.example.rest.sub.*;
import com.example.entity.*;
import com.example.entity.sub.*;

import org.junit.Test;

こちらは、Filterを設定してスキャン対象を決めるのですが、どうもクラスが対象(TypeFilter)な感じ。

        ClassPathScanningCandidateComponentProvider scanner =
            new ClassPathScanningCandidateComponentProvider(true);

        scanner.addIncludeFilter(new AnnotationTypeFilter(javax.persistence.Entity.class));

        Set<Class<?>> classesWithEntity =
            scanner
            .findCandidateComponents("com.example")
            .stream()
            .map(BeanDefinition::getBeanClassName)
            .map(cn -> {
                try {
                    return ClassUtils.forName(cn, getClass().getClassLoader());
                } catch (ReflectiveOperationException e) {
                    throw new RuntimeException(e);
                }
            })
            .collect(Collectors.toSet());

        assertThat(classesWithEntity)
            .containsOnly(TopEntity.class, SubEntity.class);

とりあえず、自分のやりたかったことがだいたいできたので、OKとしましょう。とはいえ、フレームワークを何かしら使っている場合はその機能の範囲で使えるといいですね。SpringとかCDIとか。