Java

更新时间:2025.05.28

一、概述

本工具类 WXPayUtility 为使用 Java 接入微信支付的开发者提供了一系列实用的功能,包括 JSON 处理、密钥加载、加密签名、请求头构建、响应验证等。通过使用这个工具类,开发者可以更方便地完成与微信支付相关的开发工作。

二、安装(引入依赖的第三方库)

本工具类依赖以下第三方库:

  1. Google Gson:用于 JSON 数据的序列化和反序列化。

  2. OkHttp:用于 HTTP 请求处理。

你可以通过 Maven 或 Gradle 来引入这些依赖。

如果你使用的 Gradle,请在 build.gradle 中加入:

1implementation 'com.google.code.gson:gson:${VERSION}'
2implementation 'com.squareup.okhttp3:okhttp:${VERSION}'

如果你使用的 Maven,请在 pom.xml 中加入:

1<!-- Google Gson -->
2<dependency>
3    <groupId>com.google.code.gson</groupId>
4    <artifactId>gson</artifactId>
5    <version>${VERSION}</version>
6</dependency>
7<!-- OkHttp -->
8<dependency>
9    <groupId>com.squareup.okhttp3</groupId>
10    <artifactId>okhttp</artifactId>
11    <version>${VERSION}</version>
12</dependency>

三、必需的证书和密钥

运行 SDK 必需以下的商户身份信息,用于构造请求的签名和验证应答的签名:

、工具类代码

1package com.java.utils;
2
3import com.google.gson.ExclusionStrategy;
4import com.google.gson.FieldAttributes;
5import com.google.gson.Gson;
6import com.google.gson.GsonBuilder;
7import com.google.gson.JsonElement;
8import com.google.gson.JsonObject;
9import com.google.gson.JsonSyntaxException;
10import com.google.gson.annotations.Expose;
11import com.google.gson.annotations.SerializedName;
12import okhttp3.Headers;
13import okhttp3.Response;
14import okio.BufferedSource;
15
16import javax.crypto.BadPaddingException;
17import javax.crypto.Cipher;
18import javax.crypto.IllegalBlockSizeException;
19import javax.crypto.NoSuchPaddingException;
20import javax.crypto.spec.GCMParameterSpec;
21import javax.crypto.spec.SecretKeySpec;
22import java.io.IOException;
23import java.io.UncheckedIOException;
24import java.io.UnsupportedEncodingException;
25import java.net.URLEncoder;
26import java.nio.charset.StandardCharsets;
27import java.nio.file.Files;
28import java.nio.file.Paths;
29import java.security.InvalidAlgorithmParameterException;
30import java.security.InvalidKeyException;
31import java.security.KeyFactory;
32import java.security.NoSuchAlgorithmException;
33import java.security.PrivateKey;
34import java.security.PublicKey;
35import java.security.SecureRandom;
36import java.security.Signature;
37import java.security.SignatureException;
38import java.security.spec.InvalidKeySpecException;
39import java.security.spec.PKCS8EncodedKeySpec;
40import java.security.spec.X509EncodedKeySpec;
41import java.time.DateTimeException;
42import java.time.Duration;
43import java.time.Instant;
44import java.util.Base64;
45import java.util.Map;
46import java.util.Objects;
47
48public class WXPayUtility {
49    private static final Gson gson = new GsonBuilder()
50            .disableHtmlEscaping()
51            .addSerializationExclusionStrategy(new ExclusionStrategy() {
52                @Override
53                public boolean shouldSkipField(FieldAttributes fieldAttributes) {
54                    final Expose expose = fieldAttributes.getAnnotation(Expose.class);
55                    return expose != null && !expose.serialize();
56                }
57
58                @Override
59                public boolean shouldSkipClass(Class<?> aClass) {
60                    return false;
61                }
62            })
63            .addDeserializationExclusionStrategy(new ExclusionStrategy() {
64                @Override
65                public boolean shouldSkipField(FieldAttributes fieldAttributes) {
66                    final Expose expose = fieldAttributes.getAnnotation(Expose.class);
67                    return expose != null && !expose.deserialize();
68                }
69
70                @Override
71                public boolean shouldSkipClass(Class<?> aClass) {
72                    return false;
73                }
74            })
75            .create();
76    private static final char[] SYMBOLS =
77            "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
78    private static final SecureRandom random = new SecureRandom();
79
80    /**
81     * 将 Object 转换为 JSON 字符串
82     */
83    public static String toJson(Object object) {
84        return gson.toJson(object);
85    }
86
87    /**
88     * 将 JSON 字符串解析为特定类型的实例
89     */
90    public static <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {
91        return gson.fromJson(json, classOfT);
92    }
93
94    /**
95     * 从公私钥文件路径中读取文件内容
96     *
97     * @param keyPath 文件路径
98     * @return 文件内容
99     */
100    private static String readKeyStringFromPath(String keyPath) {
101        try {
102            return new String(Files.readAllBytes(Paths.get(keyPath)), StandardCharsets.UTF_8);
103        } catch (IOException e) {
104            throw new UncheckedIOException(e);
105        }
106    }
107
108    /**
109     * 读取 PKCS#8 格式的私钥字符串并加载为私钥对象
110     *
111     * @param keyString 私钥文件内容,以 -----BEGIN PRIVATE KEY----- 开头
112     * @return PrivateKey 对象
113     */
114    public static PrivateKey loadPrivateKeyFromString(String keyString) {
115        try {
116            keyString = keyString.replace("-----BEGIN PRIVATE KEY-----", "")
117                    .replace("-----END PRIVATE KEY-----", "")
118                    .replaceAll("\\s+", "");
119            return KeyFactory.getInstance("RSA").generatePrivate(
120                    new PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyString)));
121        } catch (NoSuchAlgorithmException e) {
122            throw new UnsupportedOperationException(e);
123        } catch (InvalidKeySpecException e) {
124            throw new IllegalArgumentException(e);
125        }
126    }
127
128    /**
129     * 从 PKCS#8 格式的私钥文件中加载私钥
130     *
131     * @param keyPath 私钥文件路径
132     * @return PrivateKey 对象
133     */
134    public static PrivateKey loadPrivateKeyFromPath(String keyPath) {
135        return loadPrivateKeyFromString(readKeyStringFromPath(keyPath));
136    }
137
138    /**
139     * 读取 PKCS#8 格式的公钥字符串并加载为公钥对象
140     *
141     * @param keyString 公钥文件内容,以 -----BEGIN PUBLIC KEY----- 开头
142     * @return PublicKey 对象
143     */
144    public static PublicKey loadPublicKeyFromString(String keyString) {
145        try {
146            keyString = keyString.replace("-----BEGIN PUBLIC KEY-----", "")
147                    .replace("-----END PUBLIC KEY-----", "")
148                    .replaceAll("\\s+", "");
149            return KeyFactory.getInstance("RSA").generatePublic(
150                    new X509EncodedKeySpec(Base64.getDecoder().decode(keyString)));
151        } catch (NoSuchAlgorithmException e) {
152            throw new UnsupportedOperationException(e);
153        } catch (InvalidKeySpecException e) {
154            throw new IllegalArgumentException(e);
155        }
156    }
157
158    /**
159     * 从 PKCS#8 格式的公钥文件中加载公钥
160     *
161     * @param keyPath 公钥文件路径
162     * @return PublicKey 对象
163     */
164    public static PublicKey loadPublicKeyFromPath(String keyPath) {
165        return loadPublicKeyFromString(readKeyStringFromPath(keyPath));
166    }
167
168    /**
169     * 创建指定长度的随机字符串,字符集为[0-9a-zA-Z],可用于安全相关用途
170     */
171    public static String createNonce(int length) {
172        char[] buf = new char[length];
173        for (int i = 0; i < length; ++i) {
174            buf[i] = SYMBOLS[random.nextInt(SYMBOLS.length)];
175        }
176        return new String(buf);
177    }
178
179    /**
180     * 使用公钥按照 RSA_PKCS1_OAEP_PADDING 算法进行加密
181     *
182     * @param publicKey 加密用公钥对象
183     * @param plaintext 待加密明文
184     * @return 加密后密文
185     */
186    public static String encrypt(PublicKey publicKey, String plaintext) {
187        final String transformation = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding";
188
189        try {
190            Cipher cipher = Cipher.getInstance(transformation);
191            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
192            return Base64.getEncoder().encodeToString(cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)));
193        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
194            throw new IllegalArgumentException("The current Java environment does not support " + transformation, e);
195        } catch (InvalidKeyException e) {
196            throw new IllegalArgumentException("RSA encryption using an illegal publicKey", e);
197        } catch (BadPaddingException | IllegalBlockSizeException e) {
198            throw new IllegalArgumentException("Plaintext is too long", e);
199        }
200    }
201
202    public static String aesAeadDecrypt(byte[] key, byte[] associatedData, byte[] nonce,
203                                        byte[] ciphertext) {
204        final String transformation = "AES/GCM/NoPadding";
205        final String algorithm = "AES";
206        final int tagLengthBit = 128;
207
208        try {
209            Cipher cipher = Cipher.getInstance(transformation);
210            cipher.init(
211                    Cipher.DECRYPT_MODE,
212                    new SecretKeySpec(key, algorithm),
213                    new GCMParameterSpec(tagLengthBit, nonce));
214            if (associatedData != null) {
215                cipher.updateAAD(associatedData);
216            }
217            return new String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8);
218        } catch (InvalidKeyException
219                 | InvalidAlgorithmParameterException
220                 | BadPaddingException
221                 | IllegalBlockSizeException
222                 | NoSuchAlgorithmException
223                 | NoSuchPaddingException e) {
224            throw new IllegalArgumentException(String.format("AesAeadDecrypt with %s Failed",
225                    transformation), e);
226        }
227    }
228
229    /**
230     * 使用私钥按照指定算法进行签名
231     *
232     * @param message    待签名串
233     * @param algorithm  签名算法,如 SHA256withRSA
234     * @param privateKey 签名用私钥对象
235     * @return 签名结果
236     */
237    public static String sign(String message, String algorithm, PrivateKey privateKey) {
238        byte[] sign;
239        try {
240            Signature signature = Signature.getInstance(algorithm);
241            signature.initSign(privateKey);
242            signature.update(message.getBytes(StandardCharsets.UTF_8));
243            sign = signature.sign();
244        } catch (NoSuchAlgorithmException e) {
245            throw new UnsupportedOperationException("The current Java environment does not support " + algorithm, e);
246        } catch (InvalidKeyException e) {
247            throw new IllegalArgumentException(algorithm + " signature uses an illegal privateKey.", e);
248        } catch (SignatureException e) {
249            throw new RuntimeException("An error occurred during the sign process.", e);
250        }
251        return Base64.getEncoder().encodeToString(sign);
252    }
253
254    /**
255     * 使用公钥按照特定算法验证签名
256     *
257     * @param message   待签名串
258     * @param signature 待验证的签名内容
259     * @param algorithm 签名算法,如:SHA256withRSA
260     * @param publicKey 验签用公钥对象
261     * @return 签名验证是否通过
262     */
263    public static boolean verify(String message, String signature, String algorithm,
264                                 PublicKey publicKey) {
265        try {
266            Signature sign = Signature.getInstance(algorithm);
267            sign.initVerify(publicKey);
268            sign.update(message.getBytes(StandardCharsets.UTF_8));
269            return sign.verify(Base64.getDecoder().decode(signature));
270        } catch (SignatureException e) {
271            return false;
272        } catch (InvalidKeyException e) {
273            throw new IllegalArgumentException("verify uses an illegal publickey.", e);
274        } catch (NoSuchAlgorithmException e) {
275            throw new UnsupportedOperationException("The current Java environment does not support" + algorithm, e);
276        }
277    }
278
279    /**
280     * 根据微信支付APIv3请求签名规则构造 Authorization 签名
281     *
282     * @param mchid               商户号
283     * @param certificateSerialNo 商户API证书序列号
284     * @param privateKey          商户API证书私钥
285     * @param method              请求接口的HTTP方法,请使用全大写表述,如 GET、POST、PUT、DELETE
286     * @param uri                 请求接口的URL
287     * @param body                请求接口的Body
288     * @return 构造好的微信支付APIv3 Authorization 头
289     */
290    public static String buildAuthorization(String mchid, String certificateSerialNo,
291                                            PrivateKey privateKey,
292                                            String method, String uri, String body) {
293        String nonce = createNonce(32);
294        long timestamp = Instant.now().getEpochSecond();
295
296        String message = String.format("%s\n%s\n%d\n%s\n%s\n", method, uri, timestamp, nonce,
297                body == null ? "" : body);
298
299        String signature = sign(message, "SHA256withRSA", privateKey);
300
301        return String.format(
302                "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",signature=\"%s\"," +
303                        "timestamp=\"%d\",serial_no=\"%s\"",
304                mchid, nonce, signature, timestamp, certificateSerialNo);
305    }
306
307    /**
308     * 对参数进行 URL 编码
309     *
310     * @param content 参数内容
311     * @return 编码后的内容
312     */
313    public static String urlEncode(String content) {
314        try {
315            return URLEncoder.encode(content, StandardCharsets.UTF_8.name());
316        } catch (UnsupportedEncodingException e) {
317            throw new RuntimeException(e);
318        }
319    }
320
321    /**
322     * 对参数Map进行 URL 编码,生成 QueryString
323     *
324     * @param params Query参数Map
325     * @return QueryString
326     */
327    public static String urlEncode(Map<String, Object> params) {
328        if (params == null || params.isEmpty()) {
329            return "";
330        }
331
332        int index = 0;
333        StringBuilder result = new StringBuilder();
334        for (Map.Entry<String, Object> entry : params.entrySet()) {
335            result.append(entry.getKey())
336                    .append("=")
337                    .append(urlEncode(entry.getValue().toString()));
338            index++;
339            if (index < params.size()) {
340                result.append("&");
341            }
342        }
343        return result.toString();
344    }
345
346    /**
347     * 从应答中提取 Body
348     *
349     * @param response HTTP 请求应答对象
350     * @return 应答中的Body内容,Body为空时返回空字符串
351     */
352    public static String extractBody(Response response) {
353        if (response.body() == null) {
354            return "";
355        }
356
357        try {
358            BufferedSource source = response.body().source();
359            return source.readUtf8();
360        } catch (IOException e) {
361            throw new RuntimeException(String.format("An error occurred during reading response body. " +
362                    "Status: %d", response.code()), e);
363        }
364    }
365
366    /**
367     * 根据微信支付APIv3应答验签规则对应答签名进行验证,验证不通过时抛出异常
368     *
369     * @param wechatpayPublicKeyId 微信支付公钥ID
370     * @param wechatpayPublicKey   微信支付公钥对象
371     * @param headers              微信支付应答 Header 列表
372     * @param body                 微信支付应答 Body
373     */
374    public static void validateResponse(String wechatpayPublicKeyId, PublicKey wechatpayPublicKey,
375                                        Headers headers,
376                                        String body) {
377        String timestamp = headers.get("Wechatpay-Timestamp");
378        String requestId = headers.get("Request-ID");
379        try {
380            Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp));
381            // 拒绝过期请求
382            if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) {
383                throw new IllegalArgumentException(
384                        String.format("Validate response failed, timestamp[%s] is expired, request-id[%s]",
385                                timestamp, requestId));
386            }
387        } catch (DateTimeException | NumberFormatException e) {
388            throw new IllegalArgumentException(
389                    String.format("Validate response failed, timestamp[%s] is invalid, request-id[%s]",
390                            timestamp, requestId));
391        }
392        String serialNumber = headers.get("Wechatpay-Serial");
393        if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) {
394            throw new IllegalArgumentException(
395                    String.format("Validate response failed, Invalid Wechatpay-Serial, Local: %s, Remote: " +
396                            "%s", wechatpayPublicKeyId, serialNumber));
397        }
398
399        String signature = headers.get("Wechatpay-Signature");
400        String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"),
401                body == null ? "" : body);
402
403        boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey);
404        if (!success) {
405            throw new IllegalArgumentException(
406                    String.format("Validate response failed,the WechatPay signature is incorrect.%n"
407                                    + "Request-ID[%s]\tresponseHeader[%s]\tresponseBody[%.1024s]",
408                            headers.get("Request-ID"), headers, body));
409        }
410    }
411
412    /**
413     * 根据微信支付APIv3通知验签规则对通知签名进行验证,验证不通过时抛出异常
414     * @param wechatpayPublicKeyId 微信支付公钥ID
415     * @param wechatpayPublicKey 微信支付公钥对象
416     * @param headers 微信支付通知 Header 列表
417     * @param body 微信支付通知 Body
418     */
419    public static void validateNotification(String wechatpayPublicKeyId,
420                                            PublicKey wechatpayPublicKey, Headers headers,
421                                            String body) {
422        String timestamp = headers.get("Wechatpay-Timestamp");
423        try {
424            Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp));
425            // 拒绝过期请求
426            if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) {
427                throw new IllegalArgumentException(
428                        String.format("Validate notification failed, timestamp[%s] is expired", timestamp));
429            }
430        } catch (DateTimeException | NumberFormatException e) {
431            throw new IllegalArgumentException(
432                    String.format("Validate notification failed, timestamp[%s] is invalid", timestamp));
433        }
434        String serialNumber = headers.get("Wechatpay-Serial");
435        if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) {
436            throw new IllegalArgumentException(
437                    String.format("Validate notification failed, Invalid Wechatpay-Serial, Local: %s, " +
438                                    "Remote: %s",
439                            wechatpayPublicKeyId,
440                            serialNumber));
441        }
442
443        String signature = headers.get("Wechatpay-Signature");
444        String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"),
445                body == null ? "" : body);
446
447        boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey);
448        if (!success) {
449            throw new IllegalArgumentException(
450                    String.format("Validate notification failed, WechatPay signature is incorrect.\n"
451                                    + "responseHeader[%s]\tresponseBody[%.1024s]",
452                            headers, body));
453        }
454    }
455
456    /**
457     * 对微信支付通知进行签名验证、解析,同时将业务数据解密。验签名失败、解析失败、解密失败时抛出异常
458     * @param apiv3Key 商户的 APIv3 Key
459     * @param wechatpayPublicKeyId 微信支付公钥ID
460     * @param wechatpayPublicKey   微信支付公钥对象
461     * @param headers              微信支付应答 Header 列表
462     * @param body                 微信支付应答 Body
463     * @return 解析后的通知内容,解密后的业务数据可以使用 Notification.getPlaintext() 访问
464     */
465    public static Notification parseNotification(String apiv3Key, String wechatpayPublicKeyId,
466                                                 PublicKey wechatpayPublicKey, Headers headers,
467                                                 String body) {
468        validateNotification(wechatpayPublicKeyId, wechatpayPublicKey, headers, body);
469        Notification notification = gson.fromJson(body, Notification.class);
470        notification.decrypt(apiv3Key);
471        return notification;
472    }
473
474    /**
475     * 微信支付API错误异常,发送HTTP请求成功,但返回状态码不是 2XX 时抛出本异常
476     */
477    public static class ApiException extends RuntimeException {
478        private static final long serialVersionUID = 2261086748874802175L;
479
480        private final int statusCode;
481        private final String body;
482        private final Headers headers;
483        private final String errorCode;
484        private final String errorMessage;
485
486        public ApiException(int statusCode, String body, Headers headers) {
487            super(String.format("微信支付API访问失败,StatusCode: [%s], Body: [%s], Headers: [%s]", statusCode,
488                    body, headers));
489            this.statusCode = statusCode;
490            this.body = body;
491            this.headers = headers;
492
493            if (body != null && !body.isEmpty()) {
494                JsonElement code;
495                JsonElement message;
496
497                try {
498                    JsonObject jsonObject = gson.fromJson(body, JsonObject.class);
499                    code = jsonObject.get("code");
500                    message = jsonObject.get("message");
501                } catch (JsonSyntaxException ignored) {
502                    code = null;
503                    message = null;
504                }
505                this.errorCode = code == null ? null : code.getAsString();
506                this.errorMessage = message == null ? null : message.getAsString();
507            } else {
508                this.errorCode = null;
509                this.errorMessage = null;
510            }
511        }
512
513        /**
514         * 获取 HTTP 应答状态码
515         */
516        public int getStatusCode() {
517            return statusCode;
518        }
519
520        /**
521         * 获取 HTTP 应答包体内容
522         */
523        public String getBody() {
524            return body;
525        }
526
527        /**
528         * 获取 HTTP 应答 Header
529         */
530        public Headers getHeaders() {
531            return headers;
532        }
533
534        /**
535         * 获取 错误码 (错误应答中的 code 字段)
536         */
537        public String getErrorCode() {
538            return errorCode;
539        }
540
541        /**
542         * 获取 错误消息 (错误应答中的 message 字段)
543         */
544        public String getErrorMessage() {
545            return errorMessage;
546        }
547    }
548
549    public static class Notification {
550        @SerializedName("id")
551        private String id;
552        @SerializedName("create_time")
553        private String createTime;
554        @SerializedName("event_type")
555        private String eventType;
556        @SerializedName("resource_type")
557        private String resourceType;
558        @SerializedName("summary")
559        private String summary;
560        @SerializedName("resource")
561        private Resource resource;
562        private String plaintext;
563
564        public String getId() {
565            return id;
566        }
567
568        public String getCreateTime() {
569            return createTime;
570        }
571
572        public String getEventType() {
573            return eventType;
574        }
575
576        public String getResourceType() {
577            return resourceType;
578        }
579
580        public String getSummary() {
581            return summary;
582        }
583
584        public Resource getResource() {
585            return resource;
586        }
587
588        /**
589         * 获取解密后的业务数据(JSON字符串,需要自行解析)
590         */
591        public String getPlaintext() {
592            return plaintext;
593        }
594
595        private void validate() {
596            if (resource == null) {
597                throw new IllegalArgumentException("Missing required field `resource` in notification");
598            }
599            resource.validate();
600        }
601
602        /**
603         * 使用 APIv3Key 对通知中的业务数据解密,解密结果可以通过 getPlainText 访问。
604         * 外部拿到的 Notification 一定是解密过的,因此本方法没有设置为 public
605         * @param apiv3Key 商户APIv3 Key
606         */
607        private void decrypt(String apiv3Key) {
608            validate();
609
610            plaintext = aesAeadDecrypt(
611                    apiv3Key.getBytes(StandardCharsets.UTF_8),
612                    resource.associatedData.getBytes(StandardCharsets.UTF_8),
613                    resource.nonce.getBytes(StandardCharsets.UTF_8),
614                    Base64.getDecoder().decode(resource.ciphertext)
615            );
616        }
617
618        public static class Resource {
619            @SerializedName("algorithm")
620            private String algorithm;
621
622            @SerializedName("ciphertext")
623            private String ciphertext;
624
625            @SerializedName("associated_data")
626            private String associatedData;
627
628            @SerializedName("nonce")
629            private String nonce;
630
631            @SerializedName("original_type")
632            private String originalType;
633
634            public String getAlgorithm() {
635                return algorithm;
636            }
637
638            public String getCiphertext() {
639                return ciphertext;
640            }
641
642            public String getAssociatedData() {
643                return associatedData;
644            }
645
646            public String getNonce() {
647                return nonce;
648            }
649
650            public String getOriginalType() {
651                return originalType;
652            }
653
654            private void validate() {
655                if (algorithm == null || algorithm.isEmpty()) {
656                    throw new IllegalArgumentException("Missing required field `algorithm` in Notification" +
657                            ".Resource");
658                }
659                if (!Objects.equals(algorithm, "AEAD_AES_256_GCM")) {
660                    throw new IllegalArgumentException(String.format("Unsupported `algorithm`[%s] in " +
661                            "Notification.Resource", algorithm));
662                }
663
664                if (ciphertext == null || ciphertext.isEmpty()) {
665                    throw new IllegalArgumentException("Missing required field `ciphertext` in Notification" +
666                            ".Resource");
667                }
668
669                if (associatedData == null || associatedData.isEmpty()) {
670                    throw new IllegalArgumentException("Missing required field `associatedData` in " +
671                            "Notification.Resource");
672                }
673
674                if (nonce == null || nonce.isEmpty()) {
675                    throw new IllegalArgumentException("Missing required field `nonce` in Notification" +
676                            ".Resource");
677                }
678
679                if (originalType == null || originalType.isEmpty()) {
680                    throw new IllegalArgumentException("Missing required field `originalType` in " +
681                            "Notification.Resource");
682                }
683            }
684        }
685    }
686}