AndroidとJ2SEの両方で処理を共有するためのログラッパー

小ネタ。
ComponentとかModelとか、別にAndroidに依存しない部分はJ2SE用と共通で作りたいわけですが(・ω・)
テストとかも、エミュレータでやると遅いので、PC上で出来るところはしちゃいたいですし。*1


っで、そこでちょっと面倒なのが、ログ出力。
AndroidのLogはstaticなメソッドで実装されていたりするし(´д`;)


っということで、ログラッパークラスを作って、PCでもAndroidでも下記の様にログ出力を書けるようにする方法について。

public class MyComponent {

    private static final Logger log = LoggerFactory.getLogger(MyComponent.class);

    public void hoge() {
        log.trace("うさ☆うさだよもん");
    }
}

なお、内部的には、Android用の実装はLogクラスを使い、PC用実装はSLF4Jを使う形にします。

共通

まず、ログを抽象化するインターフェースとそのファクトリーインターフェースを下記のような感じで用意します(・ω・)

public interface Logger {

    boolean isTraceEnabled();
...
    boolean isErrorEnabled();
    void trace(String msg);
    void trace(String msg, Throwable t);
...
    void error(String msg);
    void error(String msg, Throwable t);
}
public interface LoggerFactoryAdapter {

    Logger getLogger(final Class<?> clazz);
}

っで、ファクトリーを取得するクラスを下記の様な感じで作成。
ANDROID_LOGGER_FACTORY_ADAPTER、STANDARD_LOGGER_FACTORY_ADAPTERはそれぞれAndroidとSLF4用のLoggerFactoryAdapter実装クラスのクラス名になります。

public final class LoggerFactory {

    private static final String ANDROID_LOGGER_FACTORY_ADAPTER = "os.android.log.AndroidLoggerFactoryAdapter";

    private static final String STANDARD_LOGGER_FACTORY_ADAPTER = "os.standard.log.SLF4JLoggerFactoryAdapter";

    private static final Map<String, LoggerFactoryAdapter> adopters = new ConcurrentHashMap<String, LoggerFactoryAdapter>();

    public static Logger getLogger(final Class<?> clazz) {
        if (Environment.isAndroid()) {
            return getAdapter(ANDROID_LOGGER_FACTORY_ADAPTER).getLogger(clazz);
        }
        return getAdapter(STANDARD_LOGGER_FACTORY_ADAPTER).getLogger(clazz);
    }

    private static LoggerFactoryAdapter getAdapter(final String loggerName) {
        LoggerFactoryAdapter adopter = adopters.get(loggerName);
        if (adopter == null) {
            try {
                adopter = (LoggerFactoryAdapter)Class.forName(loggerName).newInstance();
            } catch (InstantiationException e) {
                throw new WrapRuntimeException(e);
            } catch (IllegalAccessException e) {
                throw new WrapRuntimeException(e);
            } catch (ClassNotFoundException e) {
                throw new WrapRuntimeException(e);
            }
            adopters.put(loggerName, adopter);
        }
        return adopter;
    }

    private LoggerFactory() {
    }
}

ちなみに、Androidか否かの実行環境の判定は下記の様にしてみましたが、どんなもんでしょう(・ω・)?

public final class Environment {

    public static boolean isAndroid() {
        String vmName = System.getProperty("java.vm.name");
        return vmName.toLowerCase().contains("dalvik");
    }
}

Android用ログ実装

そしてAndroid用のログ実装としては、下記の様なクラスを用意。
内容としては、クラス名をTAGとして扱いLogの処理をラップするだけ(・ω・)

public class AndroidLogger implements Logger {

    private final String tag;

    public AndroidLogger(final String tag) {
        this.tag = tag;
    }

    @Override
    public boolean isTraceEnabled() {
        return Log.isLoggable(tag, Log.VERBOSE);
    }
...
    @Override
    public boolean isErrorEnabled() {
        return Log.isLoggable(tag, Log.ERROR);
    }

    @Override
    public void trace(final String msg) {
        Log.v(tag, msg);
    }
...
    @Override
    public void error(final String msg, final Throwable t) {
        Log.e(tag, msg, t);
    }
}
public class AndroidLoggerFactoryAdapter implements LoggerFactoryAdapter {

    @Override
    public Logger getLogger(final Class<?> clazz) {
        return new AndroidLogger(clazz.getSimpleName());
    }
}

Android用(SLF4J版)ログ実装

次はPC用のログ実装について、SLF4J版を使ってこんな感じで。
内容的には、SLF4Jが他のログライブラリ(Log4j、commons-loggingとか)のブリッジでやっているのと同じような事ですだ(・ω・)

public class SLF4JLogger implements Logger {

    private final org.slf4j.Logger logger;

    public SLF4JLogger(final org.slf4j.Logger logger) {
        this.logger = logger;
    }

    @Override
    public boolean isTraceEnabled() {
        return logger.isTraceEnabled();
    }
...
    @Override
    public boolean isErrorEnabled() {
        return logger.isErrorEnabled();
    }

    @Override
    public void trace(final String msg) {
        logger.trace(msg);
    }
...
    @Override
    public void error(final String msg, final Throwable t) {
        logger.error(msg, t);
    }
}
public class SLF4JLocationAwareLogger implements Logger {

    private static final String FQCN = SLF4JLocationAwareLogger.class.getName();

    private final LocationAwareLogger logger;

    public SLF4JLocationAwareLogger(final LocationAwareLogger logger) {
        this.logger = logger;
    }

    @Override
    public boolean isTraceEnabled() {
        return logger.isTraceEnabled();
    }
...
    @Override
    public boolean isErrorEnabled() {
        return logger.isErrorEnabled();
    }

    @Override
    public void trace(final String msg) {
        logger.log(null, FQCN, LocationAwareLogger.TRACE_INT, msg, null, null);
    }
...
    @Override
    public void error(final String msg, final Throwable t) {
        logger.log(null, FQCN, LocationAwareLogger.ERROR_INT, msg, null, t);
    }
}
public class SLF4JLoggerFactoryAdapter implements LoggerFactoryAdapter {

    @Override
    public Logger getLogger(final Class<?> clazz) {
        org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(clazz);
        if (logger instanceof LocationAwareLogger) {
            return new SLF4JLocationAwareLogger((LocationAwareLogger)logger);
        } else {
            return new SLF4JLogger(logger);
        }
    }
}

ビルド環境

上記のライブラリをビルドするプロジェクトではandroid.jarの参照が必要ですが、PC用の実行環境ではandroid.jarは不要です。
同様に、Android環境でもSLF4Jのjarは不要です。
LoggerFactoryはリフレクションでログファクトリーを作成することで、実行環境では不要なライブラリを参照しなくても済むようにしているという話です。


っで、これでPCでもAndroidでも同じ書き方で使えるログ処理が用意できたので、共有できる部分はこの方法を使って、テストもPCベースで走らせられるものはそうして楽ちんぽん(・∀・)、っと行きたいところです。

*1:みんなJenkinsでAndroidエミュレータプラグイン使って、複数バージョンのテストとかまでしているのかしら(・ω・)? 自分は遅いのが嫌なので、PCベースでテストできるものはPCベースで or 2.3エミュオンリーとかですが。