ALPNとは

アプリケーションによっては、TLSハンドシェークが完了する前に、共有アプリケーション・レベル値のネゴシエーションが必要な場合があります。 たとえば、HTTP/2では、特定のTCPまたはUDPポートで使用されるか使用できるHTTPバージョン(h2、spdy/3、http/1.1)の確立に役立つように、アプリケーション層プロトコル・ネゴシエーションのメカニズムが使用されます。 ALPN (RFC 7301)では、クライアントとサーバーとの間のネットワーク・ラウンドトリップを増やすことなく、これが行われます。 HTTP/2の場合、クライアントとサーバーが通信を開始する前に使用するHTTPのバージョンを知る必要があるため、接続がネゴシエートされる前にプロトコルを確立する必要があります。 ALPNがなければ、同じポートにアプリケーション・プロトコルHTTP/1とHTTP/2を持つことはできません。

クライアントは、TLSハンドシェイクの最初にあるALPN拡張機能を使用して、サポートされているアプリケーション・プロトコルのリストをClientHelloの一部としてサーバーに送信します。 サーバーは、ClientHello内のサポートされているアプリケーション・プロトコルのリストを読み取り、サポートされるプロトコルのうちどれを優先するかを決定します。 次に、ネゴシエーション結果とともにServerHelloメッセージをクライアントに送り返します。 メッセージには、選択されているプロトコルの名前、またはプロトコルが選択されていないことが示されている場合があります。

したがって、ネットワークのラウンドトリップを追加することなく、TLSハンドシェイク内でアプリケーション・プロトコル・ネゴシエーションを達成することができ、必要に応じてサーバーが各証明書を各アプリケーション・プロトコルに関連付けることができます。

他の多くのTLS拡張機能とは異なり、この拡張機能はセッションのプロパティを確立せず、接続のプロパティのみを確立します。 そのため、ネゴシエートされた値は、SSLSessionではなく、SSLSocket/ SSLEngineにあります。 セッション再開またはセッション・チケットが使用される場合(サーバー側の状態なしでのTLSセッション再開を参照)、前にネゴシエーションした値は無関係であり、新しいハンドシェーク・メッセージ内の値のみが考慮されます。

クライアントでのALPNの設定

クライアントがサポートするApplication Layer Protocol Negotiation (ALPN)の値を設定します。 サーバーとのハンドシェーク中に、サーバーは、クライアントのアプリケーション・プロトコル・リストを読み取り、どれが最も適切かを判断します。

クライアントの場合は、SSLParameters.setApplicationProtocols(String[])メソッドを使用し、その後にSSLSocketまたはSSLEnginesetSSLParametersメソッドを使用して、サーバーに送信するアプリケーション・プロトコルを設定します。

たとえば、クライアントで"three"および"two"のALPN値を設定するステップを次に示します。

コードを実行するには、プロパティjavax.net.ssl.trustStoreに有効なルート証明書を設定する必要があります。 (これはコマンドラインで行うことができます)。

import java.io.*; 
import java.util.*;
import javax.net.ssl.*; 
public class SSLClient {
    public static void main(String[] args) throws Exception {

        // Code for creating a client side SSLSocket
        SSLSocketFactory sslsf = (SSLSocketFactory) SSLSocketFactory.getDefault();
        SSLSocket sslSocket = (SSLSocket) sslsf.createSocket("localhost", 9999);

        // Get an SSLParameters object from the SSLSocket
        SSLParameters sslp = sslSocket.getSSLParameters();

        // Populate SSLParameters with the ALPN values
        // On the client side the order doesn't matter as
        // when connecting to a JDK server, the server's list takes priority
        String[] clientAPs = {"three", "two"};
        sslp.setApplicationProtocols(clientAPs);

        // Populate the SSLSocket object with the SSLParameters object
        // containing the ALPN values
        sslSocket.setSSLParameters(sslp);

        sslSocket.startHandshake();

        // After the handshake, get the application protocol that has been negotiated
        String ap = sslSocket.getApplicationProtocol();
        System.out.println("Application Protocol client side: \"" + ap + "\"");

        // Do simple write/read
        InputStream sslIS = sslSocket.getInputStream();
        OutputStream sslOS = sslSocket.getOutputStream();
        sslOS.write(280);
        sslOS.flush();
        sslIS.read();
        sslSocket.close();
    }
}

このコードを実行して、ALPN値onetwoおよびthreeを設定したJavaサーバーにClientHelloを送信すると、出力は次のようになります:

Application Protocol client side: two

ハンドシェイク中にネゴシエーションの結果を確認することもできます。 ハンドシェーク中のネゴシエーション済ALPN値の特定を参照してください。

サーバーでのデフォルトのALPNの設定

サーバーにALPN値を設定して、適切なアプリケーション・プロトコルを判別するには、デフォルトのALPNメカニズムを使用します。

サーバーでALPNのデフォルト・メカニズムを使用するには、設定するALPN値をSSLParametersオブジェクトに移入し、このSSLParametersオブジェクトを使用して、クライアント(「クライアントでのALPNの設定」セクションを参照してください)でALPNを設定したときと同様に、SSLSocketオブジェクトまたはSSLEngineオブジェクトにこれらのパラメータを移入します。 ClientHelloに含まれるALPN値のいずれかと一致する、サーバーに設定されたALPN値の最初の値が選択され、ServerHelloの一部としてクライアントに返されます。

ここでは、プロトコル・ネゴシエーションのデフォルト・アプローチを使用するJavaサーバーのコードを示します。 コードを実行するには、プロパティjavax.net.ssl.keyStoreを有効なキーストアに設定する必要があります。 (これはコマンドラインで行うことができます。「JSSEで使用するキーストアの作成」を参照してください。)。

import java.util.*; 
import javax.net.ssl.*; 
public class SSLServer {
    public static void main(String[] args) throws Exception {

        // Code for creating a server side SSLSocket
        SSLServerSocketFactory sslssf = 
            (SSLServerSocketFactory) SSLServerSocketFactory.getDefault();
        SSLServerSocket sslServerSocket = 
            (SSLServerSocket) sslssf.createServerSocket(9999);
        SSLSocket sslSocket = (SSLSocket) sslServerSocket.accept();

        // Get an SSLParameters object from the SSLSocket
        SSLParameters sslp = sslSocket.getSSLParameters();

        // Populate SSLParameters with the ALPN values
        // As this is server side, put them in order of preference
        String[] serverAPs ={ "one", "two", "three" };
        sslp.setApplicationProtocols(serverAPs);

        // If necessary at any time, get the ALPN values set on the 
        // SSLParameters object with:
        // String serverAPs = sslp.setApplicationProtocols();

        // Populate the SSLSocket object with the ALPN values
        sslSocket.setSSLParameters(sslp);

        sslSocket.startHandshake();

        // After the handshake, get the application protocol that 
        // has been negotiated

        String ap = sslSocket.getApplicationProtocol();
        System.out.println("Application Protocol server side: \"" + ap + "\"");

        // Continue with the work of the server
        InputStream sslIS = sslSocket.getInputStream();
        OutputStream sslOS = sslSocket.getOutputStream();
        sslIS.read();
        sslOS.write(85);
        sslOS.flush();
        sslSocket.close();
    }
}
このコードが実行され、JavaクライアントがALPN値threeおよびtwoを持つClientHelloを送信すると、出力は次のようになります:
Application Protocol server side: two

ハンドシェイク中にネゴシエーションの結果を確認することもできます。 ハンドシェーク中のネゴシエーション済ALPN値の特定を参照してください。

サーバーでのカスタムALPNの設定

カスタムALPNメカニズムを使用して、コールバック・メソッドを設定して適切なアプリケーション・プロトコルを決定します。

サーバーのデフォルトのネゴシエーション・プロトコルを使用しない場合は、SSLEngineまたはSSLSocketsetHandshakeApplicationProtocolSelectorメソッドを使用して、これまでのハンドシェイク状態を調べることができるBiFunction (ラムダ)コールバックを登録し、クライアントのアプリケーション・プロトコルのリストおよびその他の関連情報に基づいて選択できます。 たとえば、推奨されている暗号化方式群、または選択に当たって取得できるServer Name Indication (SNI)やその他のデータの使用を検討できます。 カスタム・ネゴシエーションを使用する場合、setApplicationProtocolsメソッド(デフォルト・ネゴシエーション)で設定された値は無視されます。

次に、プロトコル・ネゴシエーションにカスタム・メカニズムを使用するJavaサーバー用コードを示します。 コードを実行するには、プロパティjavax.net.ssl.keyStoreに有効な証明書を設定する必要があります。 (これはコマンドラインで行うことができます。「JSSEで使用するキーストアの作成」を参照してください。)。

import java.util.*; 
import javax.net.ssl.*; 
public class SSLServer {
    public static void main(String[] args) throws Exception {

        // Code for creating a server side SSLSocket
        SSLServerSocketFactory sslssf =
            (SSLServerSocketFactory) SSLServerSocketFactory.getDefault();
        SSLServerSocket sslServerSocket = 
            (SSLServerSocket) sslssf.createServerSocket(9999);
        SSLSocket sslSocket = (SSLSocket) sslServerSocket.accept();

        // Code to set up a callback function
        // Pass in the current SSLSocket to be inspected and client AP values
        sslSocket.setHandshakeApplicationProtocolSelector(
            (serverSocket, clientProtocols) -> {
                SSLSession handshakeSession = serverSocket.getHandshakeSession();
                // callback function called with current SSLSocket and client AP values
                // plus any other useful information to help determine appropriate
                // application protocol. Here the protocol and ciphersuite are also
                // passed to the callback function.
                return chooseApplicationProtocol(
                    serverSocket,
                    clientProtocols,
                    handshakeSession.getProtocol(),
                    handshakeSession.getCipherSuite());
         }); 

        sslSocket.startHandshake();

        // After the handshake, get the application protocol that has been
        // returned from the callback method.

        String ap = sslSocket.getApplicationProtocol();
        System.out.println("Application Protocol server side: \"" + ap + "\"");

        // Continue with the work of the server
        InputStream sslIS = sslSocket.getInputStream();
        OutputStream sslOS = sslSocket.getOutputStream();
        sslIS.read();
        sslOS.write(85);
        sslOS.flush();
        sslSocket.close();
    }

    // The callback method. Note how the parameters match the call within 
    // the setHandshakeApplicationProtocolSelector method above.
    public static String chooseApplicationProtocol(SSLSocket serverSocket,
            List<String> clientProtocols, String protocol, String cipherSuite ) {
        // For example, check the cipher suite and return an application protocol
        // value based on that.
        if (cipherSuite.equals("<--a_particular_ciphersuite-->")) { 
            return "three";
        } else {
            return "";
        }
    } 
}

暗号スイートがこのコードの実行時に条件文で指定する暗号スイートと一致する場合、値threeが返されます。 それ以外の場合は、空の文字列が返されます。

BiFunctionオブジェクトの戻り値は、アプリケーション・プロトコル名であるString、または通知された名前のいずれかが受け入れられないことを示すNULLであることに注意してください。 戻り値が空のStringの場合、アプリケーション・プロトコル・インジケータは使用されません。 戻り値がnull (値が選択されていない)またはピアによって宣言されていない値である場合、基になるプロトコルは、実行するアクションを決定します。 (たとえば、サーバー・コードでno_application_protocolアラートが送信され、接続が終了されます。)

クライアントとサーバーの両方でハンドシェイクが完了したら、SSLSocketオブジェクトまたはSSLEngineオブジェクトのgetApplicationProtocolメソッドをコールして、ネゴシエーションの結果を確認できます。

ハンドシェイク中の交渉されたALPN値の決定

ハンドシェーク中にネゴシエートされたALPN値を決定するには、カスタムのKeyManagerクラスまたはTrustManagerクラスを作成し、このカスタム・クラスにgetHandshakeApplicationProtocolメソッドへのコールを含めます。

選択したALPNおよびSNI値が、KeyManagerまたはTrustManagerによる選択に影響を与えるユースケースもあります。 たとえば、アプリケーションが、サーバーの属性および選択されたALPN/SNI/暗号化方式群値に応じて、異なる証明書/公開キーのセットを選択する必要がある場合があります。

このサンプル・コードは、KeyManagerオブジェクトとして作成および登録したカスタムX509ExtendedKeyManager内からgetHandshakeApplicationProtocolメソッドをコールする方法を示しています。

この例では、X509ExtendedKeyManagerを拡張するカスタムKeyManagerのコード全体を示します。 ほとんどのメソッドは、このMyX509ExtendedKeyManagerクラスによってラップされているKeyManagerクラスから返される値を単に返します。 ただし、chooseServerAliasメソッドは、SSLSocketオブジェクトでgetHandshakeApplicationProtocolをコールするため、現在のネゴシエーション済ALPN値を決定できます。

import java.net.Socket;
import java.security.*;
import javax.net.ssl.*;

public class MyX509ExtendedKeyManager extends X509ExtendedKeyManager {

    // X509ExtendedKeyManager is an abstract class so your new class 
    // needs to implement all the abstract methods in this class. 
    // The easiest way to do this is to wrap an existing KeyManager
    // and call its methods for each of the methods you need to implement.   

    X509ExtendedKeyManager akm;
    
    public MyX509ExtendedKeyManager(X509ExtendedKeyManager akm) {
        this.akm = akm;
    }

    @Override
    public String[] getClientAliases(String keyType, Principal[] issuers) {
        return akm.getClientAliases(keyType, issuers);
    }

    @Override
    public String chooseClientAlias(String[] keyType, Principal[] issuers, 
        Socket socket) {
        return akm.chooseClientAlias(keyType, issuers, socket);
    }

    @Override
    public String chooseServerAlias(String keyType, Principal[] issuers, 
        Socket socket) {
        
        // This method has access to a Socket, so it is possible to call the
        // getHandshakeApplicationProtocol method here. Note the cast from 
        // a Socket to an SSLSocket
        String ap = ((SSLSocket) socket).getHandshakeApplicationProtocol();
        System.out.println("In chooseServerAlias, ap is: " + ap);
        return akm.chooseServerAlias(keyType, issuers, socket);
    }

    @Override
    public String[] getServerAliases(String keyType, Principal[] issuers) {
        return akm.getServerAliases(keyType, issuers);
    }

    @Override
    public X509Certificate[] getCertificateChain(String alias) {
        return akm.getCertificateChain(alias);
    }

    @Override
    public PrivateKey getPrivateKey(String alias) {
        return akm.getPrivateKey(alias);
    }
}
このコードをJavaサーバーのKeyManagerとして登録し、JavaクライアントがALPN値を含むClientHelloを送信すると、出力は次のようになります:
In chooseServerAlias, ap is: <negotiated value>

この例では、デフォルトのALPNネゴシエーション戦略とカスタムKeyManagerMyX509ExtendedKeyManagerを使用する単純なJavaサーバーを示します(前のコード・サンプルを参照)。

import java.io.*;
import java.util.*;
import javax.net.ssl.*;
import java.security.KeyStore;

public class SSLServerHandshake {
    
    public static void main(String[] args) throws Exception {
        SSLContext ctx = SSLContext.getInstance("TLS");

        // You need to explicitly create a create a custom KeyManager

        // Keystores
        KeyStore keyKS = KeyStore.getInstance("PKCS12");
        keyKS.load(new FileInputStream("serverCert.p12"), 
            "password".toCharArray());

        // Generate KeyManager
        KeyManagerFactory kmf = KeyManagerFactory.getInstance("PKIX");
        kmf.init(keyKS, "password".toCharArray());
        KeyManager[] kms = kmf.getKeyManagers();

        // Code to substitute MyX509ExtendedKeyManager
        if (!(kms[0] instanceof X509ExtendedKeyManager)) {
            throw new Exception("kms[0] not X509ExtendedKeyManager");
        }

        // Create a new KeyManager array and set the first index 
        // of the array to an instance of MyX509ExtendedKeyManager.
        // Notice how creating this object is done by passing in the 
        // existing default X509ExtendedKeyManager 
        kms = new KeyManager[] { 
            new MyX509ExtendedKeyManager((X509ExtendedKeyManager) kms[0])};

        // Initialize SSLContext using the new KeyManager
        ctx.init(kms, null, null);

        // Instead of using SSLServerSocketFactory.getDefault(), 
        // get a SSLServerSocketFactory based on the SSLContext
        SSLServerSocketFactory sslssf = ctx.getServerSocketFactory();
        SSLServerSocket sslServerSocket = 
            (SSLServerSocket) sslssf.createServerSocket(9999);
        SSLSocket sslSocket = (SSLSocket) sslServerSocket.accept();
        SSLParameters sslp = sslSocket.getSSLParameters();
        String[] serverAPs ={"one","two","three"};
        sslp.setApplicationProtocols(serverAPs);
        sslSocket.setSSLParameters(sslp);
        sslSocket.startHandshake();

        String ap = sslSocket.getApplicationProtocol();
        System.out.println("Application Protocol server side: \"" + ap + "\"");

        InputStream sslIS = sslSocket.getInputStream();
        OutputStream sslOS = sslSocket.getOutputStream();
        sslIS.read();
        sslOS.write(85);
        sslOS.flush();

        sslSocket.close();
        sslServerSocket.close();
    }
}

カスタムX509ExtendedKeyManagerを指定した場合、ハンドシェーク中にchooseServerAliasが呼び出されると、KeyManagerはネゴシエートされたアプリケーション・プロトコル値を調べることができます。 示されている例の場合、この値はコンソールに出力されます。

たとえば、このコードが実行され、JavaクライアントがALPN値threeおよびtwoを持つClientHelloを送信すると、出力は次のようになります:
Application Protocol server side: two

SunJSSEプロバイダを使用したALPN値の読取りおよび書込み

ALPNは、バイト配列でデータを転送します。これは、US-ASCIIなどのシングルバイト文字エンコーディングでのテキストのエンコードが期待されることを意味します。 Java ALPN APIはテキストにStringクラスを使用しますが、Java SE 16/11.0.2/8u301,より前では、SunJSSEプロバイダはStringインスタンスをUTF-8でバイト配列に変換します。 ただし、UTF-8は可変幅文字エンコーディングです。 U+007Fよりも上にある文字を、複数のバイトでエンコードします。これはALPNピアで予期されない可能性があります。

Java SE 16/11.0.2/8u301以降では、SunJSSEプロバイダはString文字を8ビットのISO_8859_1/LATIN-1文字としてエンコードおよびデコードします。

ALPN値は、ピアが期待するネットワーク・バイト表現を使用して表されるようになりました。このため、標準の7ビットASCIIベースのStringインスタンスを変更する必要はありません。

javax.net.ssl.SSLSocketおよびjavax.net.ssl.SSLEngineのメソッドは、ピアによって送信されたネットワーク・バイト表現のApplicationProtocol String値を返します。

ただし、U+007Fより上の文字を含むUnicodeデータがある場合、アプリケーションは、SunJSSEプロバイダに依存してUnicode文字を自動的にエンコードまたはデコードするのではなく、送信または受信する前にUnicodeデータをバイト配列に正しくエンコードまたはデコードする必要があります。 または、セキュリティ・プロパティjdk.tls.alpnCharsetUTF-8に設定して、前の動作に戻すことができます。

ALPN値と予想される値を比較するには、それらをバイト配列に変換して比較できます。

次の例で予想されるALPN値は、文字列http/1.1およびUTF-8でエンコードされた文字列(16進数) 0xABCD0xABCE0xABCF (Meetei Mayekの文字は"HUK UN I"です)です。 この例では、ALPN値をISO-8859-1のバイト配列に変換し、http/1.1をUTF-8のバイト配列に変換し、0xABCD0xABCE0xABCFのバイト配列表現を手動で指定します。

    // Get the ALPN value negotiated by the TLS handshake currently
    // in progress

    String networkString = sslEngine.getHandshakeApplicationProtocol();
    
    // Encode the ALPN value into a byte array with the ISO-8859-1
    // character encoding
        
    byte[] bytes = networkString.getBytes(StandardCharsets.ISO_8859_1);
  
    String HTTP1_1 = "http/1.1";
    
    // Encode the String "http/1.1" into a byte array with the
    // UTF-8 character set
    
    byte[] HTTP1_1_BYTES = HTTP1_1.getBytes(StandardCharsets.UTF_8);
    
    // Create a byte array representing the Unicode characters 0xABCD,
    // 0xABCE, and 0xABCF, which are the Meetei Mayek letters "HUK UN I"

    byte[] HUK_UN_I_BYTES = new byte[] {
        (byte) 0xab, (byte) 0xcd,
        (byte) 0xab, (byte) 0xce,
        (byte) 0xab, (byte) 0xcf};
        
    // Test whether the APLN value is equal to "http/1.1" or
    // 0xABCD0xABCE0xABCF

    if ((Arrays.compare(bytes, HTTP1_1_BYTES) == 0 ) ||
        Arrays.compare(bytes, HUK_UN_I_BYTES) == 0) {
        // ...
    }

または、ALPN値が特定の文字セット(UTF-8など)を使用してStringからエンコードされたことがわかっている場合は、ALPN値とメソッドString.equals()を比較できます。 比較する前に、ALPN値をUnicode Stringにデコードする必要があります。

    String unicodeString = new String(bytes, StandardCharsets.UTF_8);
    if (unicodeString.equals(HTTP1_1) ||
        unicodeString.equals("\uabcd\uabce\uabcf")) {
        // ...
    }

メソッドjavax.net.ssl.SSLParameters.setApplicationProtocols(String[] protocols)の場合、そのString引数を、ピアが期待するネットワーク・バイト表現に変換する必要があります。 たとえば、ピアがUTF-8でALPN値を期待する場合、UTF-8でバイト配列に変換してから、バイト指向の文字列として格納する必要があります:

// Convert Meetei Mayek letters "HUK UN I" (in hexadecimal, 0xABCD0xABCE0xABCF)
// to a byte array with UTF-8
byte[] bytes = "\uabcd\uabce\uabcf".getBytes(StandardCharsets.UTF_8);

// Create a byte-oriented String with ISO-8859-1
String HUK_UN_I = new String(bytes, StandardCharsets.ISO_8859_1);

// GREASE value {0x8A, 0x8A}
String rfc7301Grease8A = "\u008A\u008A";
SSLParameters p = sslSocket.getSSLParameters();
p.setApplicationProtocols(new String[] {"h2", "http/1.1", rfc7301Grease8A, HUK_UN_I});
sslSocket.setSSLParameters(p);

TLSハンドシェイクの開始時に、クライアントがALPN値のリストをサーバーに送信します。サーバーは、使用できる値を選択し、認識できない値を無視します。 ただし、欠陥のあるTLS実装では、認識されないALPN値が拒否されることがあり、これによりハンドシェイクが続行できなくなる可能性がありますが、ALPN値が認識されているクライアントやサーバーは引き続き接続できるため、開発者または管理者はこの欠陥に気付かない場合があります。

そのため、TLS仕様では、Generate Random Extensions And Sustain Extensibility (GREASE)値が導入されました。これは、認識されない値をピアが正しく処理できるようにTLS実装がランダムにアドバタイズできる、予約済みの一連のTLSプロトコル値です。

前述の例では、メソッドsetApplicationProtocolsrfc7301Grease8Aに渡される値の1つがGREASE値です。 ピアはそれを拒否するのではなく無視します。

関連するクラスとメソッド

これらのクラスとメソッドは、Application Layer Protocol Negotiation (ALPN)で作業する場合に使用されます。

SSLEngineおよびSSLSocketには、同じALPN関連メソッドがあり、同じ機能があります。

クラス メソッド 目的
SSLParameters public String[] getApplicationProtocols(); クライアント側とサーバー側: このメソッドを使用して、各プロトコル・セットを含むString配列を返します。
SSLParameters public void setApplicationProtocols([] protocols);

Client-side: このメソッドを使用して、サーバーで選択できるプロトコルを設定します。

Server-side: このメソッドを使用して、サーバーが使用できるプロトコルを設定します。 String配列には、プロトコルが優先度順に含まれている必要があります。

SSLEngine SSLSocket public String getApplicationProtocol(); クライアント側とサーバー側: TLSプロトコル・ネゴシエーションが完了した後、このメソッドを使用して、接続に選択されたプロトコルを含むStringを返します。
SSLEngine SSLSocket public String getHandshakeApplicationProtocol(); クライアント側とサーバー側: 期間ハンドシェイク・メソッドを使用して、接続用に選択されたプロトコルを含むStringを返します。 このメソッドがハンドシェークの前または後に呼び出された場合、nullを返します。 このメソッドの呼出し方法の詳細は、 ハンドシェーク中のネゴシエーション済ALPN値の特定を参照してください。
SSLEngine SSLSocket public void setHandshakeApplicationProtocolSelector(BiFunction,String> selector) Server-side: メソッドを使用してコールバック関数を登録します。 アプリケーション・プロトコル値は、プロトコルや暗号スイートなどの利用可能な情報に基づいてコールバックに設定できます。 このメソッドの使い方については、「サーバーでのカスタムALPNの設定」を参照してください。

Copyright © 1993, 2025, Oracle and/or its affiliates. All rights reserved.