QuickFIX/Jのカスタムフィールドについて考えてみる

QuickFIX/Jのメッセージでのカスタムフィールドについて考えてみました。動作検証はQuickFIX/Jのソースのサンプルに含まれるbanzaiとexecutorを動かしています。
https://github.com/quickfix-j/quickfixj/tree/master/quickfixj-examples

まず、banzai側でNewOrderSingleのメッセージを作成して送信する部分を修正し、9000のフィールドを追加します。
https://github.com/quickfix-j/quickfixj/blob/QFJ_RELEASE_2_2_0/quickfixj-examples/banzai/src/main/java/quickfix/examples/banzai/BanzaiApplication.java#L372

    public void send50(Order order) {
        quickfix.fix50.NewOrderSingle newOrderSingle = new quickfix.fix50.NewOrderSingle(
                new ClOrdID(order.getID()), sideToFIXSide(order.getSide()),
                new TransactTime(), typeToFIXType(order.getType()));
        newOrderSingle.set(new OrderQty(order.getQuantity()));
        newOrderSingle.set(new Symbol(order.getSymbol()));
        newOrderSingle.set(new HandlInst('1'));
        newOrderSingle.setString(9000, "CustomField");
        send(populateOrder(order, newOrderSingle), order.getSessionID());
    }

それからexecutor側のメッセージを受ける部分が以下になるのでここで追加した9000のフィールドが取得できる方法について調べてみます。
https://github.com/quickfix-j/quickfixj/blob/QFJ_RELEASE_2_2_0/quickfixj-examples/executor/src/main/java/quickfix/examples/executor/Application.java#L379

ただこれをそのまま動かそうとするとNewOrderSingleに存在しないフィールドがあるのでエラーが発生します。このエラーを発生させなくするためにはValidateIncomingMessage=Nの設定をしておきます。
https://github.com/quickfix-j/quickfixj/blob/QFJ_RELEASE_2_2_0/quickfixj-examples/executor/src/main/resources/quickfix/examples/executor/executor.cfg

ValidateIncomingMessage=N

まずQuickFIX/Jがメッセージを受け取ってパースするところを追ってみます。メッセージを受け取るところは以下になります。
https://github.com/quickfix-j/quickfixj/blob/QFJ_RELEASE_2_2_0/quickfixj-core/src/main/java/quickfix/mina/AbstractIoHandler.java#L126

AbstractIoHandler.java

    @Override
    public void messageReceived(IoSession ioSession, Object message) throws Exception {
        String messageString = (String) message;
        SessionID remoteSessionID = MessageUtils.getReverseSessionID(messageString);
        Session quickFixSession = findQFSession(ioSession, remoteSessionID);
        if (quickFixSession != null) {
            final boolean rejectGarbledMessage = quickFixSession.isRejectGarbledMessage();
            final Log sessionLog = quickFixSession.getLog();
            sessionLog.onIncoming(messageString);
            try {
                Message fixMessage = parse(quickFixSession, messageString);
                processMessage(ioSession, fixMessage);
            } catch (InvalidMessage e) {
                if (rejectGarbledMessage) {
                    final Message fixMessage = e.getFixMessage();
                    if ( fixMessage != null ) {
                        sessionLog.onErrorEvent("Processing garbled message: " + e.getMessage());
                        processMessage(ioSession, fixMessage);
                        return;
                    }
                }
                if (MessageUtils.isLogon(messageString)) {
                    sessionLog.onErrorEvent("Invalid LOGON message, disconnecting: " + e.getMessage());
                    ioSession.closeNow();
                } else {
                    sessionLog.onErrorEvent("Invalid message: " + e.getMessage());
                }
            }
        } else {
            log.error("Disconnecting; received message for unknown session: {}", messageString);
            ioSession.closeNow();
        }
    }

これを見るとMessage fixMessage = parse(quickFixSession, messageString); でメッセージをパースし、processMessage(ioSession, fixMessage);でパース後のメッセージで処理に移っているようです。

parseする部分は以下になります。
https://github.com/quickfix-j/quickfixj/blob/QFJ_RELEASE_2_2_0/quickfixj-core/src/main/java/quickfix/MessageUtils.java#L129

MessageUtils.java

    public static Message parse(Session session, String messageString) throws InvalidMessage {
        final String beginString = getStringField(messageString, BeginString.FIELD);
        final String msgType = getMessageType(messageString);
        final MessageFactory messageFactory = session.getMessageFactory();
        final DataDictionaryProvider ddProvider = session.getDataDictionaryProvider();
        final ApplVerID applVerID;
        final DataDictionary sessionDataDictionary = ddProvider == null ? null : ddProvider
                .getSessionDataDictionary(beginString);
        final quickfix.Message message;
        final DataDictionary payloadDictionary;

        if (!isAdminMessage(msgType) || isLogon(messageString)) {
            if (FixVersions.BEGINSTRING_FIXT11.equals(beginString)) {
                applVerID = getApplVerID(session, messageString);
            } else {
                applVerID = toApplVerID(beginString);
            }
            final DataDictionary applicationDataDictionary = ddProvider == null ? null : ddProvider
                    .getApplicationDataDictionary(applVerID);
            payloadDictionary = MessageUtils.isAdminMessage(msgType)
                    ? sessionDataDictionary
                    : applicationDataDictionary;
        } else {
            applVerID = null;
            payloadDictionary = sessionDataDictionary;
        }

        final boolean doValidation = payloadDictionary != null;
        final boolean validateChecksum = session.isValidateChecksum();

        message = messageFactory.create(beginString, applVerID, msgType);
        message.parse(messageString, sessionDataDictionary, payloadDictionary, doValidation,
                validateChecksum);

        return message;
    }

MessageFactoryを使ってMessageを作成し受け取ったセットしています。

        message = messageFactory.create(beginString, applVerID, msgType);
        message.parse(messageString, sessionDataDictionary, payloadDictionary, doValidation,
                validateChecksum);

FIXプロトコルの50の場合MessageFactoryのパッケージはquickfix.fix50になるのですが、ソースを見てみるとresourceファイルしか含まれていないようです。
https://github.com/quickfix-j/quickfixj/tree/QFJ_RELEASE_2_2_0/quickfixj-messages/quickfixj-messages-fix50

ですがjarの中を覗いてみると確かにclassがあるのが確認できます。

MessageFactory .java

public class MessageFactory implements quickfix.MessageFactory {
    public MessageFactory() {
    }

    public Message create(String beginString, String msgType) {
        byte var4 = -1;
        switch(msgType.hashCode()) {
        case 54:
            if (msgType.equals("6")) {
                var4 = 92;
            }
            break;
        case 55:
            if (msgType.equals("7")) {
                var4 = 3;
            }
            break;
        case 56:
           ~省略~
        }

        switch(var4) {
        case 0:
            return new BusinessMessageReject();
        case 1:
            return new UserRequest();
        case 2:
            return new UserResponse();
        case 3:
            return new Advertisement();
            ~省略~
        }
    }
    ~省略~

これよりMsgTypeをもとにインスタンスを生成しMessageにアップキャストして返しています。関数が返すのはMessage型ですが実際のインスタンスは各メッセージの型になっているので、FixApplication実装クラスのonMessageの引数の型を各メッセージの実装クラス(quickfix.fix50.NewOrderSingleなど)にすることでそのonMessage関数が呼び出されます。

Message周りのクラスについてもう少し見てみるとquickfixj-codegeneratorというサブプロジェクトがありこれがビルド時にメッセージ系のクラスを生成しているようです。
https://github.com/quickfix-j/quickfixj/tree/master/quickfixj-codegenerator

これより、QuickFIX/Jのソースに含まれるメッセージ定義のxmlを修正してQuickFIX/Jのソース自体をビルドするとカスタムフィールドがクラスに含まれるようになります。
https://github.com/quickfix-j/quickfixj/blob/master/quickfixj-messages/quickfixj-messages-fix50/src/main/resources/FIX50.xml

ただ、この方法をする場合は取引先毎でQuickFIX/Jを分けて管理する必要が出てくるので面倒な気がするので別の方法も考えておきます。

quickfixj-codegeneratorが吐き出したNewOrderSingleクラスの各フィールドのgetterを見てみるとFieldMap.javaのメンバ変数から値を取得していることが確認できます。

NewOrderSingle.java

public Symbol getSymbol() throws FieldNotFound {
    return this.get(new Symbol());
}

public Symbol getSymbol() throws FieldNotFound {
    return this.get(new Symbol());
}

public Symbol get(Symbol value) throws FieldNotFound {
    this.getField(value);
    return value;
}

FieldMap.java

public StringField getField(StringField field) throws FieldNotFound {
    return (StringField)updateValue(field, this.getString(field.getField()));
}

public String getString(int field) throws FieldNotFound {
    return (String)this.getField(field).getObject();
}

StringField getField(int field) throws FieldNotFound {
    StringField f = (StringField)this.fields.get(field);
    if (f == null) {
        throw new FieldNotFound(field);
    } else {
        return f;
    }
}

NewOrderSingleとFieldMapの継承関係については以下のようになっています。
NewOrderSingle -> quickfix.fix50.Message -> quickfix.Message -> FieldMap

FieldMapクラスのメンバ変数を見てみると以下になっています。

    private final int[] fieldOrder;
    private final TreeMap<Integer, Field<?>> fields;
    private final TreeMap<Integer, List<Group>> groups;

この内fieldOrder以外はsetterがあるので以下のようにNewOrderSingleを拡張したクラスを定義してみます。

public class MyNewOrderSingle extends NewOrderSingle {

    private final Message message;

    public MyNewOrderSingle(NewOrderSingle message) {
        super();
        this.setFields(message);
        this.setGroups(message);
        this.message = message;

        // カスタムフィールドのバリデートはここでやる。コンストラクタの引数はNewOrderSingleにしておくことで標準フィールドのバリデートが済のことの担保を取る
    }

    public String getCustomFiledValue() throws FieldNotFound {
        return this.message.getString(9000);
    }
    
    // Setterも用意しておく
    public void setCustomField(String value) {
        this.message.setString(9000, value);
    }

    public Optional<String> getOptFieldValue() {
        try {
            return Optional.of(this.message.getString(9001));
        } catch (FieldNotFound e) {
            return Optional.empty();
        }
    }

    public void setOptField(String value) {
        this.message.setString(9001, value);
    }

    // toStringをオーバーライドしメンバ変数のmessageのtoStringを呼び出す
    public String toString() {
        return this.message.toString();
    }
}

これで標準のフィールドについてはsetFields, setGroupsを呼び出しているのでNewOrderSingleに定義されているgetterから取得できます。カスタムフィールドの値を参照したい場合はMessageから直接取得するための関数を定義しておきます。Messageを直接扱うので必須でないフィールドのgetterについてはOptinalにすることもできます。

このメッセージを受け取るためにFactoryメソッドが生成するインスタンス自体はNewOrderSingleなのでNewOrderSingleを受け取る関数の中でMyNewOrderSingleに変換します。

    public void onMessage(quickfix.fix50.NewOrderSingle order, SessionID sessionID)
            throws FieldNotFound, UnsupportedMessageType, IncorrectTagValue {
        try {
            MyNewOrderSingle myNewOrderSingle = new MyNewOrderSingle(order);

ちゃんとやるのであればメッセージの定義を変換したうえでQuickFIX/J自体をビルドするのが良いのでしょうが、実際に問題がないかは動作を見る必要はありますが確認して問題なければこれでも良さそうです。