Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ If you are using Maven without the BOM, add this to your dependencies:
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-spanner</artifactId>
<version>6.110.0</version>
<version>6.111.1</version>
</dependency>

```
Expand Down Expand Up @@ -381,6 +381,7 @@ Samples are in the [`samples/`](https://github.com/googleapis/java-spanner/tree/
| List Databases Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/ListDatabasesSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/ListDatabasesSample.java) |
| List Instance Config Operations Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/ListInstanceConfigOperationsSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/ListInstanceConfigOperationsSample.java) |
| List Instance Configs Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/ListInstanceConfigsSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/ListInstanceConfigsSample.java) |
| Mutable Credentials Example | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/MutableCredentialsExample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/MutableCredentialsExample.java) |
| Pg Alter Sequence Sample | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/PgAlterSequenceSample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/PgAlterSequenceSample.java) |
| Pg Async Query To List Async Example | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/PgAsyncQueryToListAsyncExample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/PgAsyncQueryToListAsyncExample.java) |
| Pg Async Runner Example | [source code](https://github.com/googleapis/java-spanner/blob/main/samples/snippets/src/main/java/com/example/spanner/PgAsyncRunnerExample.java) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/java-spanner&page=editor&open_in_editor=samples/snippets/src/main/java/com/example/spanner/PgAsyncRunnerExample.java) |
Expand Down
2 changes: 1 addition & 1 deletion samples/install-without-bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
<dependency>
<groupId>com.google.cloud</groupId>
<artifactId>google-cloud-spanner</artifactId>
<version>6.110.0</version>
<version>6.111.1</version>
</dependency>
<!-- [END spanner_install_without_bom] -->

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.spanner;

// [START spanner_mutable_credentials]

import com.google.auth.oauth2.ServiceAccountCredentials;
import com.google.cloud.spanner.MutableCredentials;
import com.google.cloud.spanner.Spanner;
import com.google.cloud.spanner.SpannerOptions;
import com.google.cloud.spanner.admin.database.v1.DatabaseAdminClient;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.FileTime;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

public class MutableCredentialsExample {

static void createClientWithMutableCredentials() throws IOException {
final String credentialsPath = "location_of_service_account_credential_json";
Path path = Paths.get(credentialsPath);
// Use an array to hold the mutable lastModifiedTime so it can be accessed in the lambda
FileTime[] lastModifiedTime = new FileTime[] {Files.getLastModifiedTime(path)};
Comment on lines +42 to +43
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better clarity and to use modern Java concurrency utilities, consider using java.util.concurrent.atomic.AtomicReference instead of a single-element array to hold a mutable reference. This makes the intent of sharing a mutable object with the lambda more explicit. This change should be applied along with an update to the lambda that uses this variable.

Suggested change
// Use an array to hold the mutable lastModifiedTime so it can be accessed in the lambda
FileTime[] lastModifiedTime = new FileTime[] {Files.getLastModifiedTime(path)};
// Use an AtomicReference to hold the mutable lastModifiedTime so it can be accessed in the lambda
final java.util.concurrent.atomic.AtomicReference<FileTime> lastModifiedTime =
new java.util.concurrent.atomic.AtomicReference<>(Files.getLastModifiedTime(path));


// 1 - create service account credentials
ServiceAccountCredentials serviceAccountCredentials;
try (FileInputStream is = new FileInputStream(credentialsPath)) {
serviceAccountCredentials = ServiceAccountCredentials.fromStream(is);
}

// 2 - wrap credentials from step 1 in a MutableCredentials instance
MutableCredentials mutableCredentials = new MutableCredentials(serviceAccountCredentials);

// 3 - set credentials on your SpannerOptions builder to your mutableCredentials
SpannerOptions options = SpannerOptions.newBuilder().setCredentials(mutableCredentials).build();

// 4 - include logic for when/how to update your mutableCredentials
// In this example we'll use a SchedulerExecutorService to periodically check for updates
ThreadFactory daemonThreadFactory =
runnable -> {
Thread thread = new Thread(runnable, "spanner-mutable-credentials-rotator");
thread.setDaemon(true);
return thread;
};
ScheduledExecutorService executorService =
Executors.newSingleThreadScheduledExecutor(daemonThreadFactory);
executorService.scheduleAtFixedRate(
() -> {
try {
FileTime currentModifiedTime = Files.getLastModifiedTime(path);
if (currentModifiedTime.compareTo(lastModifiedTime[0]) > 0) {
lastModifiedTime[0] = currentModifiedTime;
Comment on lines +71 to +72
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

As part of refactoring to use AtomicReference for lastModifiedTime, you'll need to update this code to use get() and set() to access and modify the value.

Suggested change
if (currentModifiedTime.compareTo(lastModifiedTime[0]) > 0) {
lastModifiedTime[0] = currentModifiedTime;
if (currentModifiedTime.compareTo(lastModifiedTime.get()) > 0) {
lastModifiedTime.set(currentModifiedTime);

try (FileInputStream is = new FileInputStream(credentialsPath)) {
ServiceAccountCredentials credentials = ServiceAccountCredentials.fromStream(is);
mutableCredentials.updateCredentials(credentials);
}
}
} catch (IOException e) {
System.err.println("Failed to check or update credentials: " + e.getMessage());
}
},
15,
15,
TimeUnit.MINUTES);

// 5. Use the client
try (Spanner spanner = options.getService();
DatabaseAdminClient databaseAdminClient = spanner.createDatabaseAdminClient()) {
// Perform operations...
// long running client operations will always use the latest credentials wrapped in
// mutableCredentials
} finally {
// Ensure the executor is shut down when the application exits or the client is closed
executorService.shutdown();
}
Comment on lines +87 to +95
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This example is intended to demonstrate credential updates for a long-running client, but the Spanner client and the ScheduledExecutorService are shut down immediately after creation because the try-with-resources block is empty. This prevents the credential rotation logic from ever running in a meaningful way.

To make the example more illustrative, consider adding a TimeUnit.sleep() within the try block to simulate a long-running process. This will keep the client and the background thread alive long enough to demonstrate the credential update mechanism.

    // 5. Use the client
    try (Spanner spanner = options.getService();
        DatabaseAdminClient databaseAdminClient = spanner.createDatabaseAdminClient()) {
      // Perform operations...
      // long running client operations will always use the latest credentials wrapped in
      // mutableCredentials.
      // This example demonstrates this by keeping the client alive for 60 seconds.
      System.out.println("Client created. It will remain active for 60 seconds.");
      try {
        TimeUnit.SECONDS.sleep(60);
      } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
      }
      System.out.println("60 seconds passed. Closing client.");
    } finally {
      // Ensure the executor is shut down when the application exits or the client is closed
      executorService.shutdown();
    }

}
}
// [END spanner_mutable_credentials]
Loading