PayPay Open Payment API(OPA)とSpring Framework(旧)の相性問題

github.com

PayPay決済のSDKを追加したところ、Tomcatが起動しないトラブルが発生しました。

org.springframework.beans.factory.BeanDefinitionStoreException: Failed to read candidate component class: file [************.class]; nested exception is java.lang.ArrayIndexOutOfBoundsException: 3145
    at org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider.findCandidateComponents(ClassPathScanningCandidateComponentProvider.java:237)
    at org.springframework.context.annotation.ClassPathBeanDefinitionScanner.doScan(ClassPathBeanDefinitionScanner.java:204)
    at org.springframework.context.annotation.ComponentScanBeanDefinitionParser.parse(ComponentScanBeanDefinitionParser.java:84)
    at org.springframework.beans.factory.xml.NamespaceHandlerSupport.parse(NamespaceHandlerSupport.java:73)
    at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.parseCustomElement(BeanDefinitionParserDelegate.java:1335)
    at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.parseCustomElement(BeanDefinitionParserDelegate.java:1325)
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.parseBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:135)
    at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.registerBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:93)
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.registerBeanDefinitions(XmlBeanDefinitionReader.java:493)
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:390)
    at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:334)
    at org.springframework.beans.

原因としては、古いバージョンのSpringフレームワークが、Java8以降の新しい言語機能を使用して作られたものを解釈できないということ。

www.infoq.com

そして、PayPayのSDKのソースを調べてみたところ、依存しているライブラリhibernate-validatorの中で、Stream APIが使われているということ。

githubの履歴をさかのぼって調べてみると、hibernate-validatorのバージョン6以降から、使われ始めている気がする。

f:id:ryu-htn:20210221210406p:plain

そこで、以下のように、PayPay SDKの依存関係からhibernate-validatorを除外して、改めて、バージョン5系のhibernate-validatorを追加することで、エラーにならずに、Tomcatが起動しました。

でも、バージョン下げて、正常に動くのかどうか、それが怖いですね。

        <dependency>
            <groupId>jp.ne.paypay</groupId>
            <artifactId>paypayopa</artifactId>
            <version>1.0.1</version>
           <exclusions>
                <exclusion>
                    <groupId>org.hibernate.validator</groupId>
                    <artifactId>hibernate-validator</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.hibernate.validator</groupId>
                    <artifactId>
                        hibernate-validator-annotation-processor
                    </artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <!-- <groupId>org.hibernate.validator</groupId> -->
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>5.3.4.Final</version>

        </dependency>

        <dependency>
            <!-- <groupId>org.hibernate.validator</groupId> -->
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator-annotation-processor</artifactId>
            <version>5.3.4.Final</version>
        </dependency>

追記 正常に動かないことがわかりました。 hibernate-validator 5.3.4.Finalにはないクラスを使用しているので、実行時に、ClassNotFoundExceptionが発生します。 正攻法でいくなら、Spring Frameworkのバージョンアップ、または、APIを独自実装するしかありません。 しかし、あくまで裏の手でいきたいなら、PayPay決済のSDKを修正してしまうこともできます。 hibernate-validatorのSDK上での用途は、パラメータのバリデーション処理です。 パラメータを正しく設定できているなら、不要の処理なので、バリデーション処理ごと削除してしまっても動作上は問題ないと思われました。

PayPay Open Payment API(OPA)のWebhookをjacksonしてみた

PayPay決済では、PayPay Open Payment API(OPA)という、決済操作をするAPIを提供しています。 その一つに、PayPay側からイベント通知を行うWebhookを提供しており、以下ようなJSON形式のデータがPostされるようです。 そのJSON形式のデータをjacksonで、デシリアライズしてみました。

https://www.paypay.ne.jp/opa/doc/jp/v1.0/webcashier#section/Webhook

{
  "notification_type": "Transaction",
  "merchant_id": "123456789",
  "store_id": "123456",
  "pos_id": "123",
  "order_id": "123456789123456",
  "merchant_order_id": "A12345",
  "authorized_at": "2020-03-13T13:35:30Z",
  "expires_at": "2020-04-13T13:35:29Z",
  "paid_at": "2020-04-13T13:35:29Z",
  "order_amount": 12345,
  "state": "COMPLETED"
}

クラス名は、Notificationにしました。 また、今後、PayPay側でJSONの仕様変更があるかもしれません。 たとえば、パラメタの追加は、想定しなければいけませんので、 @JsonIgnoreProperties(ignoreUnknown = true)を指定し、新たなパラメタは無視しています。

package paypay.handling;

import com.fasterxml.jackson.annotation.JsonFormat;
import org.apache.commons.lang.builder.ToStringBuilder;
import org.apache.commons.lang3.StringUtils;
import org.codehaus.jackson.annotate.JsonCreator;
import org.codehaus.jackson.annotate.JsonIgnoreProperties;
import org.codehaus.jackson.annotate.JsonProperty;

import java.io.Serializable;
import java.util.Date;

@JsonIgnoreProperties(ignoreUnknown = true)
public class Notification implements Serializable {

    private static final long serialVersionUID = 1L;

    @JsonProperty("authorized_at")
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
    private Date authorizedAt;

    @JsonProperty("confirmation_expires_at")
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
    private Date confirmationExpiresAt;

    @JsonProperty("expires_at")
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
    private Date expiresAt;

    @JsonProperty("merchant_id")
    private String merchantId;

    @JsonProperty("notification_id")
    private String notificationId;

    @JsonProperty("fileType")
    private FileType fileType;

    @JsonProperty("path")
    private String path;

    @JsonProperty("requestedAt")
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
    private Date requestedAt;

    @JsonProperty("merchant_order_id")
    private String merchantOrderId;

    @JsonProperty("notification_type")
    private NotificationType notificationType;

    @JsonProperty("order_amount")
    private Integer orderAmount;

    @JsonProperty("order_id")
    private String orderId;

    @JsonProperty("paid_at")
    private String paidAt;

    @JsonProperty("pos_id")
    private String posId;

    @JsonProperty("state")
    private State state;

    @JsonProperty("store_id")
    private String storeId;

    public Notification() {
    }

    public String getNotificationId() {
        return notificationId;
    }

    public void setNotificationId(String notificationId) {
        this.notificationId = notificationId;
    }

    public FileType getFileType() {
        return fileType;
    }

    public void setFileType(FileType fileType) {
        this.fileType = fileType;
    }

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    public Date getRequestedAt() {
        return requestedAt;
    }

    public void setRequestedAt(Date requestedAt) {
        this.requestedAt = requestedAt;
    }

    public Date getAuthorizedAt() {
        return authorizedAt;
    }

    public void setAuthorizedAt(Date authorizedAt) {
        this.authorizedAt = authorizedAt;
    }

    public Date getConfirmationExpiresAt() {
        return confirmationExpiresAt;
    }

    public void setConfirmationExpiresAt(Date confirmationExpiresAt) {
        this.confirmationExpiresAt = confirmationExpiresAt;
    }

    public Date getExpiresAt() {
        return expiresAt;
    }

    public void setExpiresAt(Date expiresAt) {
        this.expiresAt = expiresAt;
    }

    public String getMerchantId() {
        return merchantId;
    }

    public void setMerchantId(String merchantId) {
        this.merchantId = merchantId;
    }

    public String getMerchantOrderId() {
        return merchantOrderId;
    }

    public void setMerchantOrderId(String merchantOrderId) {
        this.merchantOrderId = merchantOrderId;
    }

    public NotificationType getNotificationType() {
        return notificationType;
    }

    public void setNotificationType(NotificationType notificationType) {
        this.notificationType = notificationType;
    }

    public Integer getOrderAmount() {
        return orderAmount;
    }

    public void setOrderAmount(Integer orderAmount) {
        this.orderAmount = orderAmount;
    }

    public String getOrderId() {
        return orderId;
    }

    public void setOrderId(String orderId) {
        this.orderId = orderId;
    }

    public String getPaidAt() {
        return paidAt;
    }

    public void setPaidAt(String paidAt) {
        this.paidAt = paidAt;
    }

    public String getPosId() {
        return posId;
    }

    public void setPosId(String posId) {
        this.posId = posId;
    }

    public State getState() {
        return state;
    }

    public void setState(State state) {
        this.state = state;
    }

    public String getStoreId() {
        return storeId;
    }

    public void setStoreId(String storeId) {
        this.storeId = storeId;
    }

    @Override
    public String toString() {
        return ToStringBuilder.reflectionToString(this);
    }

    public enum State {
        COMPLETED("COMPLETED"),
        CANCELED("CANCELED"),
        EXPIRED("EXPIRED"),
        AUTHORIZED("AUTHORIZED"),
        FAILED("FAILED"),
        UNKNOWN("UNKNOWN");

        private final String name;

        State(final String name) {
            this.name = name;
        }

        public static State enumOf(final String name) {
            for (State value : values()) {
                if (StringUtils.equals(value.getName(), name)) {
                    return value;
                }
            }
            return UNKNOWN;
        }

        @JsonCreator // This is the factory method and must be static
        public static State fromString(String string) {
            return State.enumOf(string);
        }

        private String getName() {
            return this.name;
        }

    }

    public enum FileType {
        TRANSACTION_RECON("transaction_recon"),
        UNKNOWN("UNKNOWN");

        private final String name;

        FileType(final String name) {
            this.name = name;
        }

        public static FileType enumOf(final String name) {
            for (FileType value : values()) {
                if (StringUtils.equals(value.getName(), name)) {
                    return value;
                }
            }
            return UNKNOWN;
        }

        @JsonCreator // This is the factory method and must be static
        public static FileType fromString(String string) {
            return FileType.enumOf(string);
        }

        private String getName() {
            return this.name;
        }
    }

    public enum NotificationType {
        TRANSACTION("Transaction"),
        FILE_CREATED("file.created"),
        UNKNOWN("UNKNOWN");

        private final String name;

        NotificationType(final String name) {
            this.name = name;
        }

        public static NotificationType enumOf(final String name) {
            for (NotificationType value : values()) {
                if (StringUtils.equals(value.getName(), name)) {
                    return value;
                }
            }
            return UNKNOWN;
        }

        @JsonCreator // This is the factory method and must be static
        public static NotificationType fromString(String string) {
            return NotificationType.enumOf(string);
        }

        private String getName() {
            return this.name;
        }
    }

}

変数名が、 Java側はローワーキャメルケースに対してJSON側はスネークケースと異なるため、@JsonPropertyで、明示的にJSON側の変数名を指定しています。

余談ですが、JSON側のrequestedAtfileTypeだけが、ローワーキャメルケースになっていました。ドキュメント上の誤りなのか、実際、そうなのか不明です。

ついでに言うと、突合ファイルの仕様も、項目名や値が、残高ブロックありが英語で、残高ブロックなしは、日本語になってました。

PayPayの決済システムの初期実装は、アメリカとかインドとかでやって、途中から、日本で引き取った感じでしょうかね。

なんというか、一貫性の無さに一抹の不安を感じざるをえません。

f:id:ryu-htn:20210220213602p:plain

さて、デシリアライズできたか確認します。 SpringMVCなので、以下のような、handlerメソッドを作成して、引数で、Notificationを受けとるようにします。

    @RequestMapping(value = "/webhook.api", method = RequestMethod.POST)
    public void webhookHandler(@RequestBody final Notification notification, HttpServletRequest request, HttpServletResponse response) {
        log.debug("デバッグ: " + notification);
    }

テスト

f:id:ryu-htn:20210220205053p:plain

結果

Notification@4312a609[authorizedAt=Fri Mar 13 22:35:30 JST 2020,confirmationExpiresAt=<null>,expiresAt=Mon Apr 13 22:35:29 JST 2020,merchantId=123456789,notificationId=<null>,fileType=<null>,path=<null>,requestedAt=<null>,merchantOrderId=A12345,notificationType=TRANSACTION,orderAmount=12345,orderId=123456789123456,paidAt=<null>,posId=123,state=AUTHORIZED,storeId=123456]

知らんうちにmaven-eclipse-pluginが引退してた。

古いプロジェクトを、最新のEclipseで開いたのだけど、ClassNotFoundで、WTPが起動しない。 どうも、maven-eclipse-pluginが、最新のEclipseでは、うまく動かなくなっていた。 調べてみると、maven-eclipse-pluginは、とっくにEOLのようだ。

f:id:ryu-htn:20210210122332p:plain

pom.xmlから、以下を削除したら、起動した。 Eclipseも便利になったものだ。

                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-eclipse-plugin</artifactId>
                        <version>2.8</version>
                        <configuration>
                            <wtpapplicationxml>true</wtpapplicationxml>
                            <wtpversion>2.0</wtpversion>
                        </configuration>
                    </plugin>