はじめに
この記事はJava Advent Calendar 2024の第20日目のエントリーです。そして一日目のエントリーの続きです。
なお19日目のエントリーはmikoto2000さんによるSpring Data JPA の JPQL エラー備忘録(LocalDate がエラーになる) でした。そちらもご覧ください
しつこく、こんにちは、世界。
続きです。AdventMessageProviderFactory
に細工して、jp.hatano.advent2024.providers
パッケージに新しいAdventMessageProvider
を実装したクラスを追加すると、自動的に読み込まれて実行される、というところまで行きました。
まずはAdventApp.java
の中身。
package jp.hatano.advent2024.main;
import java.io.*;
public class AdventApp {
public static void main(String[] args) {
AdventMessageProviderFactory factory = new AdventMessageProviderFactory("jp.hatano.advent2024.providers");
for ( AdventMessageProvider provider : factory ) {
AdventApp application = new AdventApp(provider);
application.sayHello(System.out);
}
}
private final AdventMessageProvider provider;
public AdventApp(AdventMessageProvider provider) {
this.provider = provider;
}
public void sayHello(PrintStream out) {
out.println(provider.getHello());
}
}
そしてAdventMessageProvider
インターフェース。
package jp.hatano.advent2024.main;
public interface AdventMessageProvider {
String getHello();
}
さらにAdventMessageProviderFactory.java
の部分。
package jp.hatano.advent2024.main;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.nio.file.Paths;
public class AdventMessageProviderFactory implements Iterable<AdventMessageProvider>, Iterator<AdventMessageProvider> {
private final List<AdventMessageProvider> listProviders = new ArrayList<>();
public AdventMessageProviderFactory(String packageName) {
String currentClassPath;
String thisPackageName = getClass().getPackageName().replace(".",File.separator);
String targetPackageName = packageName.replace(".",File.separator);
try {
URL classPath = getClass().getResource(".");
if ( classPath != null ) {
currentClassPath = Paths.get(classPath.toURI()).toAbsolutePath().toString().replace(thisPackageName,targetPackageName);
File[] providerFiles = new File(currentClassPath).listFiles();
if ( providerFiles != null ) {
for (File providerFile : providerFiles) {
String providerAbsolutePath = providerFile.getAbsolutePath();
String filename = providerAbsolutePath.substring(providerAbsolutePath.lastIndexOf(File.separatorChar)+1);
String className = packageName + "." + filename.replace(".class","");
Class<?> aClass = Class.forName(className);
listProviders.add((AdventMessageProvider) aClass.getConstructor(new Class[]{}).newInstance(new Object[]{}));
}
}
}
} catch (URISyntaxException | NoSuchMethodException | IllegalAccessException | InstantiationException |
InvocationTargetException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
public int size() {
return listProviders.size();
}
public AdventMessageProvider get(int i) {
return listProviders.get(i);
}
public AdventMessageProvider get(String name) {
for ( AdventMessageProvider provider : listProviders ) {
if ( provider.getClass().getName().equals(name) ) {
return provider;
}
}
return null;
}
@Override
public Iterator<AdventMessageProvider> iterator() {
return listProviders.iterator();
}
@Override
public boolean hasNext() {
return iterator().hasNext();
}
@Override
public AdventMessageProvider next() {
return iterator().next();
}
}
ここまではいいですよね…。(いいですよ…ね?)
家鴨は家鴨、でも呼べるよ
ここで、AdventMessageProviderContainer
というクラスを用意します。
package jp.hatano.advent2024.main;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class AdventMessageProviderContainer implements AdventMessageProvider {
private final Class<?> aClass;
private Object anObject = null;
private boolean instantiateSuccessful = true;
public AdventMessageProviderContainer(Class<?> aClass) {
this.aClass = aClass;
try {
anObject = aClass.getConstructor(new Class<?>[] {}).newInstance(new Object[] {});
} catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException
| NoSuchMethodException | SecurityException e) {
instantiateSuccessful = false;
}
}
private Method methodGetHello = null;
public boolean applicable() {
if ( !instantiateSuccessful ) {
return false;
}
if ( methodGetHello != null ) {
return true;
}
try {
methodGetHello = aClass.getMethod("getHello",new Class<?>[] {});
} catch (NoSuchMethodException | SecurityException e) {
return false;
}
return true;
}
@Override
public String getHello() {
if ( methodGetHello == null && !applicable()) {
return null;
}
return getHello0();
}
private String getHello0() {
if ( methodGetHello != null ) {
try {
return (String) methodGetHello.invoke(anObject,new Object[] {});
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
return null;
}
}
return null;
}
}
コンストラクタはひとまず受け取ったクラスオブジェクトを格納するだけにとどめ、getHello()
を呼び出す場合その前にapplicable()
というメソッドを呼ぶ必要があるようにしました。ここで本当に渡されたクラスがgetHello()
というメソッドを保持しているかどうかをチェックするようにしました。ここでは渡されたクラスがAdventMessageProvider
インターフェースを実装しているかどうかはチェックしていません。単純にgetHello()
というシグネチャのメソッドを抱えているかしか見ていません。あればgetHello()
を外から呼ばれたときに実際のgetHello()
実装に繋ぐだけしかしていないのです。
さらにAdventMessageProviderFactory
の方も(長くなるので抜粋にしましょう…。)
Class<?> aClass = Class.forName(className);
Class<?>[] interfaces = aClass.getInterfaces();
boolean interfaceFound = false;
for ( Class<?> anInterface : interfaces ) {
if ( anInterface.getName().equals("jp.hatano.advent2024.main.AdventMessageProvider") ) {
listProviders.add((AdventMessageProvider) aClass.getConstructor(new Class[] {}).newInstance(new Object[] {}));
interfaceFound = true;
break;
}
}
if ( !interfaceFound ) {
AdventMessageProviderContainer container = new AdventMessageProviderContainer(aClass);
if ( container.applicable() ) {
listProviders.add(container);
}
}
インターフェースにAdventMessageProvider
が含まれていればlistProviders
へそのままadd()
しておき、なかった場合にはそのクラスをAdventMessageProviderContainer
で包んでやはりlistProviders
へadd()
するという動作をしているわけです。
わざとAdventMessageProvider
インターフェースを実装していないItalianMessageProvider
クラスを用意してやります。
package jp.hatano.advent2024.providers;
public class ItalianMessageProvider {
public String getHello() {
return "Ciao mondo.";
}
}
これを、jp/hatano/advent2024/providers
の下に置いてやってAdventApp
を実行してやると、こうなります。
% java -cp out/production/advent2024 jp.hatano.advent2024.main.AdventApp
Hello, World!
Bonjour le monde.
Ciao mondo.
こんにちは、世界
はい、EnglishMessageProvider
、FrenchMessageProvider
、JapaneseMessageProvider
と並んで、ItalianMessageProvider
もちゃんと呼び出せていますね。
こういう「想定した一連のメソッドを抱えていることが確認できれば想定したクラスのオブジェクトだとみなしてしまってもかまわない」というやり方を家鴨型付と言うとか言わないとか。まあでも、Javaの場合きちんとインターフェースを実装すればすむ話なので、あえてこんなことをする価値はないんですよね。やればできるというところをやってみたかっただけなのですよね。
おわりに
相変わらずオチはありません。コードは引き続きgithubに公開しています。