システム開発で思うところ

Javaで主にシステム開発をしながら思うところをツラツラを綴る。主に自分向けのメモ。EE関連の情報が少なく自分自身がそういう情報があったら良いなぁということで他の人の参考になれば幸い

ログインユーザーの管理

ログインしたユーザー情報を管理するにあたって必要そうなことの整理。

順不同で、あれこれ。

各種部品を作ったりするための整理。

EEとかSpringの認証ライブラリは?

とりあえず、各種ライブラリを 使わないで 自前のForm認証から試してみようかと考えています。
今回のテーマは、認証認可をするための仕組み というよりも、ログイン後の権限に対する仕組み を考えてみようと思っているからです。

イントラネット(プライベート)だったら 自前のものや、Firewallの後ろだからBasic認証でも良いかなと思うし、インターネット(パブリック)だったら、自前でログインによる認証・認可を管理するよりも Googleとかのサービスを使うことになると思っています。
つまり、仕組みとして注力するのは ライフサイクルや アクセス権限のことを考える方を優先した方が良いかな?と。

名前付け

ログインユーザーとか、セッションユーザーとか、そんな感じ。
操作者の情報という意味を鑑みて「ClientAgent」にしようと思っています。

ライフサイクル

Scopedアノテーションを付与したクラスと 振る舞いを行うクラスは別にして、ライフサイクルだけを管理するものにしておく。

今回はセッションスコープ

リクエストスコープにして、都度 永続化情報やキャッシュへ問い合わせるというやり方もあると思います。
(可用性の高い仕組みを設けたい場合は、こっちかな?)
アクセスキーは、ログインIDか、そこから生成したハッシュ値を使うイメージ。

ログアウト

もしくは、セッション破棄。
これはクライアントが関係するので、JavaScript による close をトリガーとして サーバーサイドの操作をします。

セッションハイジャック対策

ログインしたところで、セッションIDを書き換えます。
自前のForm認証じゃない場合でも、使えるような仕組みにしたいところですが、そのためには Basic認証とか コンテナによる認証機構を使うとか、そのあたりも調べないといけないので 後回しにするかも。

パスワードのハッシュ化

生パスワードでチェックをするのではなく、ハッシュ化したもので判定をする。
SALTとか使って云々をするところとか、そのあたりもかな。
一応、自前のForm認証用のUtilityとして作るところを最低限と言う感じかなぁ。
この機能は ユーザー登録みたいなところでも使えるような可搬性も踏まえた作りにしておきたいところ。

認証と認可を分ける

若干YAGNIな気もするけど、OAuth2を使った仕組みへの連携とか、そのあたりを ある程度 考慮した仕組みも考えておきたいところ

権限判定

アクションに対するロールとか、そのあたりの仕組み。
悩んでいるところとしては、アクションに対するロールという整理が良いのか、アクターとユースケースとする整理が良いのか、というところ。

これについては、デブサミで 偶然 良い視座をいただきました。

リダイレクト

ログイン画面へのリダイレクトさせるケース

未ログイン

操作に対して、そもそもログインしているか、していないか、というレベルの話。
権限が付与されていない状態での操作、というものも これに含まれるかな。
OAuth2の発想が参考になりそうな気がします。

タイムアウト

セッションタイムアウトしていたら、再ログインを促します。
未ログインとの違いは、長時間の操作をしていなかったことを ユーザーに通知してからログイン画面に遷移させるか、いきなりログイン画面に遷移させるか、というところ。
自分の観測範囲では、遷移パターンはサービスによって異なるように思います。

  • ログイン画面へタイムアウトメッセージを添えて遷移する
  • タイムアウトした旨を伝える画面に遷移して、そこからログイン画面へ遷移する

と思ったけど、機能としては タイムアウトを検知時に 遷移する先の遷移先を指定するだけだから、実質一緒かな?

権限の異なる操作

異なる権限のユーザーで、URLを直接実行するケース。
悪意が無ければ普通は無さそうなケースだけど、だからこそ外せないところ。
通常なら URL Filterで制御するようなところです。
なので、Serviceで権限制御をしようとすると、この点については 対処できないケースがあるような気がしています。 ただ、適切に権限指定すれば 大丈夫な気もしています。

ログイン前後の情報喪失を防ぐ

具体的には、ログイン前にカートに入れた商品を いざ購入しようとしたら ログインを促されて ログインしたらカートの中身が空になるみたいなケース。
なんだけど、これは そもそも可用性の話であって権限とは話が違うので除外。

考えようとしたことのメモとしては

  • カートに商品を入れる という操作は 商品購入というユースケースとして 購入者権限が必要(=ログインしないといけない)として、ログイン画面を介在させる
  • 購入する という操作前後で カートの情報が喪失しないように引き継ぐ

セッションハイジャックのことを考えると*1、そもそも後者って どうやってやっているんだろう?という感じです。
セッションではなく、別のトークンみたいなものをもたせて、セッションとは異なる キャッシュで管理している?
クライアントに保持しておいて 毎回 全部引き回す?

参考情報

以前の自分の認証認可のメモのリンクは、一通り 改めて 読み直しておきたいところ

vermeer.hatenablog.jp

*1:セッションIDを書き換えるだけだから使えるのかな?

デバッグ用のログを出力させる

vermeer.hatenablog.jp

で、基本編的なところを整理しました。

今回は、アイディアネタ的な実装である デバッグ用のログ出力機能について メモを残します。

やりたいこと

  • 実行時例外が発生したときにだけ、アクションの開始時点の情報を出力したい
  • 出力ログファイルも別にしたい

実行時例外になったときに、アクションの開始時点*1の詳細情報を確認できる仕組みがあれば良いかもしれない、かといって、つねにアクションに関する情報をログへ出力したいわけではないというのがアイディアです。

開始時点の情報ですが、ActionクラスのtoStringメソッドの戻り値を出力します。 独自のインターフェースを設けることも考えましたが、 とりあえず toStringはオブジェクトの文字列情報を返却する という責務があるのだから、 あまり深く考えずに toStringをそのまま使うことを前提にしました。
当然ですが、何もしないと クラス名とHashCodeを出力するだけなので、有効な情報はありません。 lombokを使うとか 自分で実装するとか、まぁ そういう手間は必要になります。 とりあえず、仕組みを作るところまでを目標としました。

データを保持する

RequestScoped で、開始時点の情報を保持して(setUp)、実行時例外により処理が終了する際(tearDown)に出力させます。

@RequestScoped
public class LoggerStore {

    private Logger logger;
    private LoggerStoreItems items;
    private Class<?> triggerActionClass;
    private String startTitle;
    private String tearDownTitle;

    @PostConstruct
    public void init() {
        this.items = new LoggerStoreItems();
        this.startTitle = ">> Trace Logging Start >>";
        this.tearDownTitle = "<< Trace Logging  End  <<";
    }

    public void setUp(InvocationContext ic) {
        this.triggerActionClass = ic.getTarget().getClass().getSuperclass();
        this.logger = LoggerFactory.getLogger(LoggerName.LOGGER_STORE_SUB_NAME + "." + triggerActionClass.getName());
        this.append(ic, startTitle);
        this.append(ic, ic.getTarget().toString());
    }

    public void tearDown(Throwable throwable) {
        if (throwable != null) {
            Class<?> throwableClass = throwable.getClass();
            this.items.add(throwableClass, null, throwable, "");
        }
        this.items.add(triggerActionClass, null, null, tearDownTitle);
        this.items.logBy(logger);
    }

    public void append(InvocationContext ic, String message) {
        Class<?> actionClass = ic.getTarget().getClass().getSuperclass();
        Method actionMethod = ic.getMethod();
        this.items.add(actionClass, actionMethod, null, message);
    }

}

Interceptor

@Action
@Interceptor
@Priority(Interceptor.Priority.APPLICATION + 5)
public class ActionInterceptor {

    @Inject
    private LoggerStore loggerStore;

    @AroundInvoke
    public Object invoke(InvocationContext ic) throws Exception {
        Action action = ic.getMethod().getAnnotation(Action.class);

        loggerStore.setUp(ic);

        if (action != null && action.value().equals(Action.Ignore.ON)) {
            return null;
        }

        InvocationContextLogger logger = InvocationContextLogger.getLogger(ic);
        try {
            logger.fine(() -> "start");
            return ic.proceed();
        } finally {
            logger.fine(() -> "end");
        }

    }
}

ExceptionHandler

public class CustomExceptionHandler extends ExceptionHandlerWrapper {

    private final ExceptionHandler exceptionHandler;
    private final ThrowableHandlerFactory throwableHandlerFactory;
    private final ErrorPageNavigator errorPageNavigator;
    private final LoggerStore loggerStore;

    CustomExceptionHandler(ExceptionHandler exceptionHandler, ThrowableHandlerFactory throwableHandlerFactory,
                           ErrorPageNavigator errorPageNavigator, LoggerStore loggerStore) {
        this.exceptionHandler = exceptionHandler;
        this.throwableHandlerFactory = throwableHandlerFactory;
        this.errorPageNavigator = errorPageNavigator;
        this.loggerStore = loggerStore;
    }

    @Override
    public ExceptionHandler getWrapped() {
        return this.exceptionHandler;
    }

    @Override
    public void handle() {

        final Iterator<ExceptionQueuedEvent> it = getUnhandledExceptionQueuedEvents().iterator();

        while (it.hasNext()) {

            ExceptionQueuedEventContext eventContext = (ExceptionQueuedEventContext) it.next().getSource();

            Throwable causeThrowable = getRootCause(eventContext.getException()).getCause();
            Throwable throwable = causeThrowable != null
                                  ? causeThrowable
                                  : eventContext.getException().getCause();

            ThrowableHandler throwableHandler = this.throwableHandlerFactory.createThrowableHandler(throwable, eventContext);
            try {
                throwableHandler.execute();

            } catch (Exception ex) {
                this.errorPageNavigator.navigate(ex);

            } finally {
                this.loggerStore.tearDown(throwable);

                // 未ハンドリングキューから削除する
                it.remove();
            }

            getWrapped().handle();
        }

    }
}

Loggerの初期化

通常のログと違って、出力先の異なる 別のLogger(storeLogger)に出力します。
FileHandlerを 新たに設けることで実装しました。

[Java]Java SE標準ロギングのプロパティ設定で複数のファイルにログを分けて出力する - torutkのブログ
を参考にしました。

@ApplicationScoped
public class LoggerLifecycleHandler {

    private static final Logger rootLogger = Logger.getLogger(LoggerName.ROOT_NAME);
    private static final Logger storeLogger = Logger.getLogger(LoggerName.ROOT_NAME + "." + LoggerName.LOGGER_STORE_SUB_NAME);

    public void startUp(@Observes @Initialized(ApplicationScoped.class) Object event) {
        System.out.println(">> Startup:Initialize RootLogger >>");
        LoggerInitializer.builder()
                .rootLogger(rootLogger)
                .propertiesFilePath("/logging.properties")
                .consoleHandlerClass(LogConsoleHandler.class, LogConsoleFormatter.class)
                .fileHandlerClass(LogFileHandler.class, LogFileFormatter.class)
                .execute();

        LoggerInitializer.builder()
                .rootLogger(storeLogger)
                .propertiesFilePath("/logging.properties")
                .consoleHandlerClass(LogConsoleHandler.class, LogConsoleFormatter.class)
                .fileHandlerClass(LogStoreFileHandler.class, LogFileFormatter.class)
                .execute();

    }

    public void shutdown(@Observes @Destroyed(ApplicationScoped.class) Object event) {
        System.out.println("<< Cleanup:Closing logging file <<");
        LogFileCloser logFileCloser = new LogFileCloser();

        Collections.list(LogManager.getLogManager().getLoggerNames()).stream()
                .filter(name -> name.startsWith(LoggerName.ROOT_NAME))
                .forEach(logFileCloser::close);

    }

}

Code

vermeer_etc / jsf-ddd / source / — Bitbucket

さいごに

開始時点の実装はしましたが、処理中の情報ををストックする実装は一旦 保留としています。 必須な気もしましたが、アイディア実装みたいなところもあるので とりあえずベースとなる仕組みまでで止めておいて、有効活用するケースが具体的にあった時に拡張していけばよいかな と考えたためです。 また、できればクラス名などをLoggerクラスのように リクレクションで取得出来たら便利かな?と思って、実際の実装を見てみたのですが Java8以前と Java9以降では 使っているライブラリが大きく違っていたというのも、一旦保留とした理由の1つです。*2


*1:今回は実装するのを保留にしましたが、必要であれば途中の状態も

*2:まだ EEに関する技術参考になりうるのは Java8に留めておいた方が良いだろうという考えもあっての判断でもあります。

Jakarta EEでjava.util.logging.Loggerを使ったLogging

vermeer.hatenablog.jp

で学んだことを駆使しつつ、自分なりの Jakarta EEでのLoggingの実装が大体 整理できましたので、至る経緯と考察を残すためのメモ。

生成と破棄

これは、この記事のちょっとした続きです。

vermeer.hatenablog.jp

肝心の LogFileCloserの実装が漏れていました。
また、ちょっと追記もあります。
追記したのは、logger.removeHandler(h);のところです。
removeHandlerをしないと、ConsoleHandlerが残っていて 開発中だと LoggerにHandlerがどんどん追加付与されてしまって、同じログが連続して出力されてしまいます。
FileHandlerの方は、closeだけでイイ感じにしてくれるみたいで 重複出力は無かったんですけどね。

Java EE インジェクト可能なロガーの作り方と注意点 - Qiita
を参考にさせていただきました。

public class LogFileCloser {

    public void close(String rootLoggerName) {
        Logger logger = Logger.getLogger(rootLoggerName);
        for (Handler h : logger.getHandlers()) {
            h.close();
            logger.removeHandler(h);
        }
    }
}

また呼出し側も ちょっと変えたので そこに追記。
参考したものだと、RootとなるLoggerだけにHandlerを付与するので問題なかったのですが、複数のHandlerをもったLoggerをRoot配下に設けたい場合には、機能として不足していたので ちょっと追記。

@ApplicationScoped
public class LoggerLifecycleHandler {

(略)

    public void shutdown(@Observes @Destroyed(ApplicationScoped.class) Object event) {
        System.out.println("<< Cleanup:Closing logging file <<");
        LogFileCloser logFileCloser = new LogFileCloser();

        Collections.list(LogManager.getLogManager().getLoggerNames()).stream()
                .filter(name -> name.startsWith(LoggerName.ROOT_NAME))
                .forEach(logFileCloser::close);
    }
}

フォーマット

Console用と、File用のフォーマットをそれぞれ準備しました。
とはいえ、基本的な出力は同じなので、LogRecordConverterとして、編集ロジック自体は共通化しました。

JUL を少しマシにする - A Memorandum
を参考にさせていただきました。

手を加えたところのポイント

  • System.lineSeparator()の位置
  • 短縮形の要否

Fileと違って Consoleは出力都度 自動で改行が入るので タイミングを変えています。
また、Consoleは 開発時に出力ウィンドウに出力された内容を確認するために使うので クラス名が分かれば 十分なので短縮形で良いのですが、Fileは運用時とか より正確な情報が欲しいと思ったので短縮形表記を止めました。
このあたりは設計思想というか そのあたりによって考え方が変わると思います。

public class LogRecordConverter {

    private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss.SSS");

    private static final Map<Level, String> levelMsgMap = Collections.unmodifiableMap(new HashMap<Level, String>() {
        private static final long serialVersionUID = 1L;

        {
            put(Level.SEVERE, "SEVERE");
            put(Level.WARNING, "WARN");
            put(Level.INFO, "INFO");
            put(Level.CONFIG, "CONF");
            put(Level.FINE, "FINE");
            put(Level.FINER, "FINE");
            put(Level.FINEST, "FINE");
        }
    });

    private static final AtomicInteger nameColumnWidth = new AtomicInteger(32);

    private final LocalDateTime dateTime;
    private final Level logLevel;
    private final String className;
    private final String methodName;
    private final Throwable throwable;
    private final String message;

    private LogRecordConverter(LocalDateTime dateTime, Level logLevel, String className, String methodName, Throwable throwable, String message) {
        this.dateTime = dateTime;
        this.logLevel = logLevel;
        this.className = className;
        this.methodName = methodName;
        this.throwable = throwable;
        this.message = message;
    }

    public static LogRecordConverter of(LocalDateTime dateTime, Level logLevel, String className, String methodName, Throwable throwable, String message) {
        return new LogRecordConverter(dateTime, logLevel, className, methodName, throwable, message);
    }

    public String toConsole() {
        StringBuilder sb = new StringBuilder(300);
        this.appendCommonRecord(sb);
        this.appendShorteningCategoryAndMessage(sb);

        if (this.throwable != null) {
            sb.append(System.lineSeparator());
            this.appendThrown(sb);
        }
        return sb.toString();
    }

    public String toFile() {
        StringBuilder sb = new StringBuilder(300);
        this.appendCommonRecord(sb);
        this.appendCategoryAndMessage(sb);

        sb.append(System.lineSeparator());
        if (this.throwable != null) {
            this.appendThrown(sb);
        }
        return sb.toString();
    }

    private void appendCommonRecord(StringBuilder sb) {
        sb.append(this.timeStamp());
        sb.append(" ");

        sb.append(levelMsgMap.get(this.logLevel));
        sb.append(" ");
    }

    private void appendShorteningCategoryAndMessage(StringBuilder sb) {
        int width = nameColumnWidth.intValue();
        String category = adjustCategoryLength(baseCategory(), width);
        sb.append("[[");
        sb.append(category);
        sb.append("]] ");
        this.updateNameColumnWidth(width, category.length());

        sb.append(message);
    }

    private void appendCategoryAndMessage(StringBuilder sb) {
        sb.append("[[");
        sb.append(baseCategory());
        sb.append("]] ");
        sb.append(message);
    }

    private void appendThrown(StringBuilder sb) {
        StringWriter sw = new StringWriter();
        try (PrintWriter pw = new PrintWriter(sw)) {
            this.throwable.printStackTrace(pw);
        }
        sb.append(sw.toString());
    }

    private String timeStamp() {
        return formatter.format(dateTime);
    }

    private String baseCategory() {
        return className == null
               ? ""
               : methodName == null
                 ? className
                 : className + "#" + methodName;
    }

    private String adjustCategoryLength(String packageName, int aimLength) {

        int overflowWidth = packageName.length() - aimLength;

        String[] fragment = packageName.split(Pattern.quote("."));
        for (int i = 0; i < fragment.length - 1; i++) {
            if (1 < fragment[i].length() && 0 < overflowWidth) {

                int cutting = (fragment[i].length() - 1) - overflowWidth;
                cutting = (cutting < 0) ? (fragment[i].length() - 1) : overflowWidth;

                fragment[i] = fragment[i].substring(0, fragment[i].length() - cutting);
                overflowWidth -= cutting;
            }
        }

        String result = String.join(".", fragment);

        int cnt = aimLength - result.length();
        if (cnt <= 0) {
            return result;
        }
        String blank = new String(new char[cnt]).replace("\0", " ");
        return result + blank;
    }

    private void updateNameColumnWidth(int width, int categoryLength) {
        if (width < categoryLength) {
            nameColumnWidth.compareAndSet(width, categoryLength);
        }
    }
}


Handlerへの設定は、コンテナ起動時に一度だけ実施すれば良いので、以下のクラスを作りました。*1

Java EEアプリケーションで起動時になにかしらの処理をする方法 — 裏紙
を参考にして、Serveletに依存させず、CDIだけで 初期化処理を行っています。

@ApplicationScoped
public class LoggerLifecycleHandler {

    private static final Logger rootLogger = Logger.getLogger(LoggerName.ROOT_NAME);
(略)

    public void startUp(@Observes @Initialized(ApplicationScoped.class) Object event) {
        System.out.println(">> Startup:Initialize RootLogger >>");
        LoggerInitializer.builder()
                .rootLogger(rootLogger)
                .propertiesFilePath("/logging.properties")
                .consoleHandlerClass(LogConsoleHandler.class, LogConsoleFormatter.class)
                .fileHandlerClass(LogFileHandler.class, LogFileFormatter.class)
                .execute();

  (略)
    }

    public void shutdown(@Observes @Destroyed(ApplicationScoped.class) Object event) {
        System.out.println("<< Cleanup:Closing logging file <<");
        LogFileCloser logFileCloser = new LogFileCloser();

        Collections.list(LogManager.getLogManager().getLoggerNames()).stream()
                .filter(name -> name.startsWith(LoggerName.ROOT_NAME))
                .forEach(logFileCloser::close);
    }
}


初期設定するクラスLoggerInitializerにおけるポイントは
setUseParentHandlersです。

これも、ConsoleとFileで扱いが微妙に違ったというか、そういう理由です。

Fileだと、setUseParentHandlers(false)を指定しなくても問題ないのですが、Consoleは デフォルトのLoggerに伝播しているか、EEサーバ側が内部的に伝播しているか 良く分かりませんが、2行ログが出力されるために必要な実装でした。

public class LoggerInitializer {

(略)

    public static class Builder implements RootLoggerBuilder, PropertiesFileBuilder {
        public void execute() {

            this.initConsoleFormatter();
            this.setConsoleHander();

            this.initFileFormatter();
            this.setFileHander();

            this.rootLogger.setUseParentHandlers(false);
        }

(略)

}

Interceptorとの連携

Java EEのCDIで定義しておくと便利なプロデューサーとインターセプタ - きしだのはてな を参考にさせていただきました。

InvocationContext を使うことで、実行元の情報を取得する事が出来ます。

ただ、このやり方だとGCのタイミングで問題が起こる可能性があるかもしれないです。

参考
ログレベルが突然変わる謎の事象を追う ~ あるOSSサポートエンジニアの1日 - Qiita

ということで、上述で示した 強参照の RootLogger配下に強制するように Loggerの生成は、簡単なクラス(LogFactory)を作りました。
ただし、直接使うのではなく、InterceptorとかProducer経由でLoggerを生成する前提にしようと思っていたので、このクラスについては default*2にしました。

class LoggerFactory {

    private LoggerFactory() {
    }

    static Logger getLogger(String name) {
        return Logger.getLogger(LoggerName.ROOT_NAME + "." + name);
    }

}

加えて、InvocationContextからLoggerに必要な情報を取得できるようなクラスを作りました。

public class InvocationContextLogger {

    private final String actionClass;
    private final String actionMethod;
    private final Logger logger;

    private InvocationContextLogger(Logger logger, String actionClass, String actionMethod) {
        this.logger = logger;
        this.actionClass = actionClass;
        this.actionMethod = actionMethod;
    }

    public static InvocationContextLogger getLogger(InvocationContext ic) {
        String actionClass = ic.getTarget().getClass().getSuperclass().getName();
        String actionMethod = ic.getMethod().getName();
        Logger logger = LoggerFactory.getLogger(actionClass);
        return new InvocationContextLogger(logger, actionClass, actionMethod);
    }

    public void severe(Supplier<String> msgSupplier) {
        logger.logp(Level.SEVERE, actionClass, actionMethod, msgSupplier);
    }

(省略)

}


実際に使用しているところ(抜粋)。
当初、LogeerFactoryを使うやり方で考えていたのですが、戻り値を InvocationContextLoggerとすることを鑑みて、別クラスにしました。
ちなみに、Loggerがインターフェースだったら 方式を変えた可能性があります。

@Action
@Interceptor
@Priority(Interceptor.Priority.APPLICATION + 5)
public class ActionInterceptor {

(略)
    @AroundInvoke
    public Object invoke(InvocationContext ic) throws Exception {

     (略)

        InvocationContextLogger logger = InvocationContextLogger.getLogger(ic);
        try {
            logger.fine(() -> "start");
            return ic.proceed();
        } finally {
            logger.fine(() -> "end");
        }

    }
}

Code

vermeer_etc / jsf-ddd / source / — Bitbucket

さいごに

いったん、基本的な実装まで。 次にデバッグ用にアイディアネタ的な仕組みも実装したので、それについて書きたいと思います。


*1:過去の記事でも 取り上げ済みの再掲です

*2:パッケージプライベート

Loggerファイルの初期生成と破棄

vermeer.hatenablog.jp

ちょっとだけ 忘れないようにしておきたいトピックがあったので そこだけの抜粋です。

アプリケーションを経由してログ出力をすると hoge.log.lckというファイルが残ってしまうので 能動的にCloseする必要があります。

参考としては こちらが詳しいです。

Java EE インジェクト可能なロガーの作り方と注意点 - Qiita

これで目的は十分達成できるのですが

  • DIを管理するクラスに直接ロジックを書くことを避けたかった
  • 開始・終了制御は CDIで完結させたかった

ということで、あれこれやりました。

差分コード

vermeer_etc / jsf-ddd / commit / 006cd7411230 — Bitbucket

抜粋するのは生成と破棄のところです。*1

@ApplicationScoped
public class LoggerLifecycleHandler {

    private static final Logger rootLogger = Logger.getLogger("root");

    public void startUp(@Observes @Initialized(ApplicationScoped.class) Object event) {
        System.out.println(">> Startup:Initialize RootLogger >>");
        LoggerInitializer.builder()
                .rootLogger(rootLogger)
                .propertiesFilePath("/logging.properties")
                .consoleHandlerClass(LogConsoleHandler.class, LogConsoleFormatter.class)
                .fileHandlerClass(LogFileHandler.class, LogFileFormatter.class)
                .execute();
    }

    public void shutdown(@Observes @Destroyed(ApplicationScoped.class) Object event) {
        System.out.println("<< Cleanup:Closing logging file <<");
        new LogFileCloser().close("root");
    }

}

@Observes @Destroyedは メソッドアノテーション@PreDestroyでも同じことが出来るのは確認したのですが、開始と終了との実装のバランスを鑑みてあわせました。

参考資料

Java EEアプリケーションで起動時になにかしらの処理をする方法 — 裏紙

java - CDI 1.1: Is @Observes @Initialized(TransactionScoped.class) supposed to work? - Stack Overflow

Loggerについての補足

Payaraで試しましたが、CosoleLogHandlerに何かしらフォーマットを適用しようとしたら、標準エラーログになってしまいます。
Loggerとして logger.infoとしてもエラーになります。
残念ながら、この解決方法は分かりませんでした。


*1:初期起動時と終了時の破棄に関して同じようなことを調べる可能性があると思っての抜粋

Loggingのメモ

参考資料


まずは、この記事を きちんと読む
java.util.loggingの使い方 - Qiita

そのあとで、この記事を読む
JUL を少しマシにする - A Memorandum


入門から実践までJavaで学べる「ログ」の常識 (1/4):プログラマーの常識をJavaで身につける(10) - @IT

[Java] 標準APIのLoggerを使用してログを外部ファイルに出力する。 | DevelopersIO

7.1. ロギング — TERASOLUNA Server Framework for Java (5.x) Development Guideline 5.2.1.RELEASE documentation

CDIのInjectionPointを使ってみる - CLOVER🍀

JavaEE7をはじめよう(18) - CDI インターセプターとステレオタイプ - エンタープライズギークス (Enterprise Geeks)

Java EEのCDIで定義しておくと便利なプロデューサーとインターセプタ - きしだのはてな

Java TM ロギングの概要

アプリへのslf4j + logback 導入時の java.util.logging 向け対処 - Qiita

[Java]Java SE標準ロギングのプロパティ設定で複数のファイルにログを分けて出力する - torutkのブログ

Java EE インジェクト可能なロガーの作り方と注意点 - Qiita

ログレベルが突然変わる謎の事象を追う ~ あるOSSサポートエンジニアの1日 - Qiita

Payara のログ・ビューワの制限事項 (ja) - notepad

java - 複数のパラメータによるロギング - 答えた

java.util.logging - FileHandler.patternで指定したディレクトリを作成する - Kazzzの日記

logging - Java - configure custom loggers for use - Stack Overflow

メモ

JavaのLoggerは実装ライブラリを、DIなどで置き換えられるように インターフェースと実装を分離させるようなことはしない方が良い気がする。
理由は、既に十分に抽象化で混とんとしているから。
とりあえず標準Loggerをインターフェースとして標準Loggerは実装クラスなので実装置き換えということは出来ませんが、slf4jを使うことにしたときに一気に書き換えをするという割り切りをしておけばいいかなと今は考えています。
それに基本的なLoggingについては、Interceptor経由で出力する前提で考えているから影響範囲も かなり限定的だと思っています。

メモ2

資料を参考に注意することなどの整理*1

  • Loggerはstatic final なフィールドで保持して強参照にする*2
  • Logger生成時に強制的にRootLogger配下に属するようにするべき?
  • Formatterを作って出力情報をきちんとする
  • FileHandlerは独自のFileHandlerを作って出力先を振り分けできる
  • WebApplicationの場合はLoggerのCloseをする
  • 設定情報はpropertiesを参照する仕組みとして独自のpropertiesがない場合の考慮も忘れずに
  • Formatterは共通実装で良い(が良い)
  • Handlerで出力制御が確定する
  • EEサーバー固有のpropertiesと衝突するのでLogManager.getLogManager().readConfiguration()を使ってはいけない
  • 各種制御をするのは独自パーツのみと割り切る(各種環境設定との衝突を避ける)
  • ログ出力制御はpropertiesもあるけど、Filterもある。独自制御の場合はFilterの方が良いかもしれない

メモ(Payara)

  • 独自ログの出力先指定は絶対パス指定にする。
    相対パスだと C:\payara5\glassfish\domains\domain1\config配下に作成されるので注意すること。

  • 独自フォーマットは標準出力としては エラー扱いになる?
    多分、これが根拠になりそう。
    GlassFish 4.1 のロギング (ja) - notepad

こうなると もはや JULは ミドル用のLoggerで、LogBackなどのライブラリをアプリケーション用のLoggerとして使う として大きく振り切った方が 安全な気がしてきた。
欠点は、Payaraだと LogViewerが使えたりするけど、その恩恵も捨てるということになるところ。 まぁLog監視は そもそもPayaraでやらずにログ監視用サーバーでやるべきかもしれない。

FileHanderについては指定したフォーマットをそのまま使ってくれているようだということは確認が出来ているので及第点かなぁ。

Code

Loggerの実装についてコミットログ(雑に)

vermeer_etc / jsf-ddd / commit / 006cd7411230 — Bitbucket


*1:当たり前を当たり前として認識することも含めて

*2:そうしないとGCでLoggerインスタンスが破棄される可能性がある

Lambdaを使って内部リストを暴露しないで処理をする

最近、ちょっとずつStreamAPIだけでなく、Lambdaを使うことに少しずつ慣れてきつつあります。

Lambdaは無名クラスが簡単に使えるくらいの印象しか持っていなかったのですが、遅延評価の仕組みとして 徐々に使い方が分かってくると 何でもトンカチで殴りたくなるものです。 というわけではないのですが、自分で実装してみて「これは 良いかも」と思ったので ご紹介。

よくやる実装

ファーストクラスコレクションを作って構造と責務を閉じようとするのですが、どうしても内部コレクションを公開したいことはあります。
特に 副作用のある操作をしたい場合に、どうしても そういう実装をせざるを得ないケースです。 そういうときは Collections#unmodifiableList を使って不変なリストを返却するようなメソッドを実装して対応しています。

public class Callee {

    private List<String> items;

    public Callee() {
        this.items = Arrays.asList("aaa", "bbb", "ccc", "ddd");
    }

    public List<String> items() {
        return Collections.unmodifiableList(items);
    }
}
public class Caller {

    public void useList() {
        Callee callee = new Callee();
        List<String> items = callee.items();
        items.stream().forEachOrdered(System.out::println);
    }

}

Lambdaを使って構造公開しない実装

public class Callee {

    private List<String> items;

    public Callee() {
        this.items = Arrays.asList("aaa", "bbb", "ccc", "ddd");
    }

    public void forEachOrdered(Consumer<? super String> action) {
        this.items.stream().forEachOrdered(item -> action.accept(item));
    }

}


ポイントは List<String> items を外部公開することなく 処理が行えているところです。

public class Caller {

    public void useLambda() {
        Callee callee = new Callee();
        callee.forEachOrdered(System.out::println);
    }

}

補足

内部リストの公開以外にもドメインオブジェクト(ファーストクラスコレクション)側に外部依存が少なくなるのも嬉しいところです。

例えば DBアクセスのような処理です。 update(Connection conn, String userId)みたいなメソッドを準備すれば良いんじゃないの?というのが容易に想像できるわけですが、それだと ドメインオブジェクトにインフラ層のパッケージへの依存が生まれます。私としては それを回避できるのも嬉しいところです。*1

気をつけること

当たり前ですが なんでも、このやり方でやるのは良くないです。
Collections.unmodifiableList で不変を担保する代替にはなると思いますが、責務の放棄までしない方が良いです。
例えば、Caller側の情報を使うこともなく、副作用の伴う処理をしているわけでもない、ただ繰り返し処理をするだけというのであれば 目的としている使い方ではありません。それは Collections.unmodifiableListで公開していることと同じことになってしまうので、きちんとドメインオブジェクト側で メソッドとして準備をして、それを使うようにすべきだと思います。

さいごに

なんでも Lambda、なんでも StreamAPI、は正しいとは思いませんが、だからといって忌避することなく、少しずつ使ってみて 理解を深めておくことは大事かな、と思います。 私もまだまだ理解も実践も乏しいところがあります。
「おっ、おもしろそう」と何かしらの一助になれば幸いです。

*1:もちろん、緩衝材としてインターフェースを介すれば問題ないよね?というのもありますし、それはそれで有効な手段だと思います。

JSFでツールチップ

vermeer.hatenablog.jp

の続きと言うか、ついでに。

やりたいこと

エラーメッセージを入力領域にツールチップを出力する

やりかた

xhtmlのinput要素のtitleにエラーメッセージを出力する

前回の記事と ほぼ同じ発想です。

実装

Titleに出力メッセージを扱うクラス

BeanValidationExceptionのInterceptorで

  • 画面上のコンポーネントを取得して通常のIDとJSFのClinetIdを関連付けたクラス
  • エラーとなったClientIdとメッセージのリスト

をマージして、Titleに出力するメッセージ用のクラスを編集します。

@Action
@Interceptor
@Priority(Interceptor.Priority.APPLICATION)
@Dependent
public class BeanValidationExceptionInterceptor {

    private final CurrentViewContext context;
    private final MessageConverter messageConverter;
    private final MessageWriter messageWriter;
    private final ClientComplementManager clientComplementManager;
    private final ErrorStyle errorStyle;
    private final ErrorTooltip errorTooltip;

    @Inject
    public BeanValidationExceptionInterceptor(CurrentViewContext context,
                                              MessageConverter messageConverter, MessageWriter messageWriter,
                                              ClientComplementManager clientComplementManager,
                                              ErrorStyle errorStyle, ErrorTooltip errorTooltip) {
        this.context = context;
        this.messageConverter = messageConverter;
        this.messageWriter = messageWriter;
        this.clientComplementManager = clientComplementManager;
        this.errorStyle = errorStyle;
        this.errorTooltip = errorTooltip;
    }

    @AroundInvoke
    public Object invoke(InvocationContext ic) throws Exception {

        String currentViewId = context.currentViewId();

        try {
            return ic.proceed();
        } catch (BeanValidationException ex) {

            ClientIdsWithComponents clientIdsWithInputComponents = new InputComponentScanner().scan();

            ClientIdMessages clientidMessages
                             = messageConverter.toClientIdMessages(ex.getValidatedResults(),
                                                                   ic.getTarget().getClass().getSuperclass(),
                                                                   clientIdsWithInputComponents);

            ClientIdsWithComponents clientIdsWithHtmlMessages = new HtmlMessageScanner().scan();
            messageWriter.appendErrorMessageToComponent(clientidMessages.toClientIdMessagesForWriting(clientIdsWithHtmlMessages));

            FacesContext.getCurrentInstance().validationFailed();
            clientComplementManager.setClientidMessages(clientidMessages);

            this.errorStyle.set(clientIdsWithInputComponents, clientidMessages);

            //ツールチップ用のインスタンスに情報を渡します
            this.errorTooltip.set(clientIdsWithInputComponents, clientidMessages);
            return currentViewId;
        }

    }
}


情報を保持するクラス

@Named
@RequestScoped
public class ErrorTooltip {

    private ClientIdsWithComponents clientIdsWithComponents;
    private ClientIdMessages clientIdMessages;

    @PostConstruct
    private void init() {
        this.clientIdsWithComponents = new ClientIdsWithComponents();
        this.clientIdMessages = new ClientIdMessages();
    }

    public void set(ClientIdsWithComponents clientIdsWithInputComponents, ClientIdMessages clientIdMessages) {

        Set<String> clientIds = clientIdMessages.getList().stream()
                .map(ClientIdMessage::getClientId)
                .collect(Collectors.toSet());

        this.clientIdsWithComponents = clientIdsWithInputComponents.filter(clientIds);
        this.clientIdMessages = clientIdMessages;
    }

    /**
     * 指定したIDの項目が検証不正だった場合に適用する メッセージ を返却します.
     * <P>
     * xhtmlでのパラメータ指定時には、シングルクウォートで値を指定してください.
     *
     * @param id 対象となるコンポーネントのID(JSFのクライアントIDではありません)
     * @return 当該項目IDにエラーがない場合は 空文字を返却します.
     */
    public String byId(String id) {
        String clientId = this.clientIdsWithComponents.getOrNull(id);
        if (clientId == null) {
            return "";
        }
        return clientIdMessages.getMessage(clientId);
    }

    /**
     * 指定したClientId(フルパス)の項目が検証不正だった場合に適用する メッセージ を返却します.
     *
     * @param clientId 対象となるコンポーネントのID(JSFのクライアントIDではありません)
     * @return 当該項目IDにエラーがない場合は 空文字を返却します.
     */
    public String byClientId(String clientId) {
        return clientIdMessages.getMessage(clientId);
    }

}

xhtmlの記述

画面IDとクライアントID、いずれからでもメッセージを取得するメソッドを準備していますが、input要素に直接出力するので 通常は クライアントIDをパラメータとするメソッドを 使うことになると思います。
引数はcomponent.clientIdを使えば、当該コンポーネントのクライアントIDが取得できます。

<div class="field">
    <label>利用者ID</label>
    <input jsf:styleClass="short-input" type="text" placeholder="someone@example.com"
           jsf:id="email" jsf:value="#{userRegistrationPage.email}"
           title="#{errorTooltip.byClientId(component.clientId)}"/>

</div>

Code

vermeer_etc / jsf-ddd / source / — Bitbucket

さいご

まだ繰り返し領域に関する制御は保留だけど
とりあえず、これで メッセージ関連は一段落かな?