Skip to content

Java code example standards

Scott Macdonald edited this page Oct 2, 2024 · 28 revisions

Java Code Examples Standards

This document summarizes important points for writing and reviewing code examples written for the AWS SDK for Java V2. For more information on tools and standards, see the complete list in TCX Code Examples Standards.

General Structure

Service folders with examples should follow the common structure that uses folders for Actions, Scenarios, and Tests, with a top level POM file.

For all scenarios, code examples should use the Java Async client. The following code demonstrates how to perform asynchronous service calls. The method returns a CompletableFuture object to the calling code where it gets the value or handles the error.

Example of a service call where the calling code does not use the result of the CompletableFuture

Use the the CompletableFuture#whenComplete() method as the final completion stage. Consume the results or throw the exception as a RuntimeException for the calling code to handle.

// snippet-start:[ec2.java2.create_key_pair.main]
    /**
     * Creates a new key pair asynchronously.
     *
     * @param keyName the name of the key pair to create
     * @param fileName the name of the file to write the key material to
     * @return a {@link CompletableFuture} that represents the asynchronous operation
     *         of creating the key pair and writing the key material to a file
     */
    public CompletableFuture<CreateKeyPairResponse> createKeyPairAsync(String keyName, String fileName) {
        CreateKeyPairRequest request = CreateKeyPairRequest.builder()
                .keyName(keyName)
                .build();

        return getAsyncClient().createKeyPair(request)
                .whenComplete((response, exception) -> {
                    if (response != null) {
                        try {
                            BufferedWriter writer = new BufferedWriter(new FileWriter(fileName));
                            writer.write(response.keyMaterial());
                            writer.close();
                        } catch (IOException e) {
                            throw new RuntimeException("Failed to write key material to file: " + e.getMessage(), e);
                        }
                    } else {
                        throw new RuntimeException("Failed to create key pair: " + exception.getMessage(), exception);
                    }
                });
    }
   // snippet-end:[ec2.java2.create_key_pair.main]

Example of a service call where the calling code uses the result of the CompletableFuture

Use the CompletableFuture#handle() as the final completion stage. If the CompletableFuture completes successfully, the result is returned, otherwise errors are detected and thrown as a RuntimeException that are handled by the calling code.

        return getAsyncClient().createDomain(domainRequest)
                .handle( (createResponse, throwable) -> {
                    if (createResponse != null) {
                        logger.info("Domain status is {}", createResponse.domainStatus().changeProgressDetails().configChangeStatusAsString());
                        logger.info("Domain Id is {}", createResponse.domainStatus().domainId());
                        return createResponse.domainStatus().domainId();
                    }
                    throw new RuntimeException("Failed to create domain", throwable);
                });

Here is the calling code that gets the return value and handles any exceptions. In this example, it handles the resource exists.

  try {
            CompletableFuture<CreateKeyPairResponse> future = ec2Actions.createKeyPairAsync(keyName, fileName);
            CreateKeyPairResponse response = future.join();
            logger.info("Key Pair successfully created. Key Fingerprint: " + response.keyFingerprint());

        } catch (RuntimeException rt) {
            Throwable cause = rt.getCause();
            if (cause instanceof Ec2Exception ec2Ex) {
                if (ec2Ex.getMessage().contains("already exists")) {
                    // Key pair already exists.
                    logger.info("The key pair '" + keyName + "' already exists. Moving on...");
                } else {
                    logger.info("EC2 error occurred: Error message: {}, Error code {}", ec2Ex.getMessage(), ec2Ex.awsErrorDetails().errorCode());
                    return;
                }
            } else {
                logger.info("An unexpected error occurred: " + (rt.getMessage()));
                return;
            }
        }

Note this is a common pattern. That is returning a CompletableFuture instance, getting the return value, and handling exceptions in the calling code.

ALso notice use of logger.info vs System.out.println(). Moving forward, the use of logger.info() is used.

When the AWS SDK for Java SDK support Waiters, use them.

public CompletableFuture<String> createSecurityGroupAsync(String groupName, String groupDesc, String vpcId, String myIpAddress) {
        CreateSecurityGroupRequest createRequest = CreateSecurityGroupRequest.builder()
            .groupName(groupName)
            .description(groupDesc)
            .vpcId(vpcId)
            .build();

        return getAsyncClient().createSecurityGroup(createRequest)
            .thenCompose(createResponse -> {
                String groupId = createResponse.groupId();

                // Waiter to wait until the security group exists
                Ec2AsyncWaiter waiter = getAsyncClient().waiter();
                DescribeSecurityGroupsRequest describeRequest = DescribeSecurityGroupsRequest.builder()
                    .groupIds(groupId)
                    .build();

                CompletableFuture<WaiterResponse<DescribeSecurityGroupsResponse>> waiterFuture =
                    waiter.waitUntilSecurityGroupExists(describeRequest);

                return waiterFuture.thenCompose(waiterResponse -> {
                    IpRange ipRange = IpRange.builder()
                        .cidrIp(myIpAddress + "/32")
                        .build();

                    IpPermission ipPerm = IpPermission.builder()
                        .ipProtocol("tcp")
                        .toPort(80)
                        .fromPort(80)
                        .ipRanges(ipRange)
                        .build();

                    IpPermission ipPerm2 = IpPermission.builder()
                        .ipProtocol("tcp")
                        .toPort(22)
                        .fromPort(22)
                        .ipRanges(ipRange)
                        .build();

                    AuthorizeSecurityGroupIngressRequest authRequest = AuthorizeSecurityGroupIngressRequest.builder()
                        .groupName(groupName)
                        .ipPermissions(ipPerm, ipPerm2)
                        .build();

                    return getAsyncClient().authorizeSecurityGroupIngress(authRequest)
                        .thenApply(authResponse -> groupId);
                });
            })
            .whenComplete((result, exception) -> {
                if (exception != null) {
                    if (exception instanceof CompletionException && exception.getCause() instanceof Ec2Exception) {
                        throw (Ec2Exception) exception.getCause();
                    } else {
                        throw new RuntimeException("Failed to create security group: " + exception.getMessage(), exception);
                    }
                }
            });
    }

Example usage of paginators as per the general standards.

public CompletableFuture<Void> listAllObjectsAsync(String bucketName) {
        ListObjectsV2Request initialRequest = ListObjectsV2Request.builder()
            .bucket(bucketName)
            .maxKeys(1)
            .build();

        ListObjectsV2Publisher paginator = getAsyncClient().listObjectsV2Paginator(initialRequest);
        return paginator.subscribe(response -> {
            response.contents().forEach(s3Object -> {
                logger.info("Object key: " + s3Object.key());
            });
        }).thenRun(() -> {
            logger.info("Successfully listed all objects in the bucket: " + bucketName);
        }).exceptionally(ex -> {
            throw new RuntimeException("Failed to list objects", ex);
        });
    }

Examples should use JDK 17+ and use a Java Text blocks for large amount of text. Single sentences can still use System.out.println().

logger.info("""
            The Amazon Elastic Container Registry (ECR) is a fully-managed Docker container registry 
            service provided by AWS. It allows developers and organizations to securely 
            store, manage, and deploy Docker container images. 
            ECR provides a simple and scalable way to manage container images throughout their lifecycle, 
            from building and testing to production deployment.\s
                        
            The `EcrAsyncClient` interface in the AWS SDK provides a set of methods to 
            programmatically interact with the Amazon ECR service. This allows developers to 
            automate the storage, retrieval, and management of container images as part of their application 
            deployment pipelines. With ECR, teams can focus on building and deploying their 
            applications without having to worry about the underlying infrastructure required to 
            host and manage a container registry.
            
           This scenario walks you through how to perform key operations for this service.  
           Let's get started...
          """);

Java methods should provide JavaDoc details that provides a summary, parameters, exceptions, and return type below the snippet tag.

[TODO - update this]

// snippet-start:[ecr.java2.create.repo.main]
/**
 * Creates an Amazon Elastic Container Registry (Amazon ECR) repository.
 *
 * @param repoName the name of the repository to create
 * @return the Amazon Resource Name (ARN) of the created repository, or an empty string if the operation failed
 * @throws IllegalArgumentException if the repository name is null or empty
 * @throws RuntimeException         if an error occurs while creating the repository
 */
public String createECRRepository(String repoName) {

Java Single Actions/Hello Service examples use the Async clients and where available, use Paginator method calls.

 public static List<JobSummary> listJobs(String jobQueue) {
        if (jobQueue == null || jobQueue.isEmpty()) {
            throw new IllegalArgumentException("Job queue cannot be null or empty");
        }

        ListJobsRequest listJobsRequest = ListJobsRequest.builder()
            .jobQueue(jobQueue)
            .jobStatus(JobStatus.SUCCEEDED)  // Filter jobs by status
            .build();

        List<JobSummary> jobSummaries = new ArrayList<>();
        ListJobsPublisher listJobsPaginator = getAsyncClient().listJobsPaginator(listJobsRequest);
        CompletableFuture<Void> future = listJobsPaginator.subscribe(response -> {
            jobSummaries.addAll(response.jobSummaryList());
        });

        future.join();
        return jobSummaries;
    }

When a service is added or updated, update the Java V2 Github repo so it will be included in the build/lint/format and Weathertop tests.

New scenarios should have two classes. One class that uses main() controls the flow of the program. The other class, which is the Action class contains SDK method calls that uses the Async Java client.

You can create Request objects separately for service method calls. You can also use lambda expressions if you feel that makes sense for the example.

Use parameterized values for any AWS database query operations. (See the Redshift scenario as an example).

Import statements should not include any unused imports, wildcards, and be ordered alphabetically.

All Java code examples should be lintered using Checkstyle.

Java tests must use @TestInstance and @TestMethodOrder JUNT annotations.

@TestInstance(TestInstance.Lifecycle.PER_METHOD)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class AWSPolly

Integration tests should be marked with @Tag("IntegrationTest"). Also, use @Order(1) for ordering tests.

    @Test
    @Tag("IntegrationTest")
    @Order(1)
    public void describeVoicesSample() {
        assertDoesNotThrow(() ->DescribeVoicesSample.describeVoice(polly));
        logger.info("describeVoicesSample test passed");
    }

Use Maven/POM for dependency management.

Recommend keeping dependencies up-to-date and following best practices for dependency management. That is, every 3-4 months - update the Java SDK build.

Metadata for Action examples should contain at minimum the following snippets.

  • A snippet to show the action itself within context.
  • If more than one variation of the Action is included, use descriptions in the metadata to explain the differences.

Metadata for scenario examples should contain the Action class and Scenario that contains the main() that contains the output of the program.

Clone this wiki locally