少し前に試したエントリで、クラスをスキャンして自動登録…ができたらいいなぁみたいなところがあったのですが、以前それに近いことはエントリとして起こしたことがあります。
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-RS、JPA関連のアノテーションを付けたクラスを用意しました。前述のライブラリを使って、この中からクラスに@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、Javassist、FindBugs、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);