Importar archivos de Amazon S3 en Java
Para importar un objeto de Amazon S3 a un archivo local, usa getObject de
AWS SDK para Java v2 con un transformador de respuesta a archivo. El importador que se muestra a
continuación descarga a una ubicación temporal, verifica una suma de comprobación SHA-256 de
confianza y publica el archivo de destino solo después de que la descarga completa supere la
verificación. También admite el cliente asíncrono estándar, el cliente AWS CRT y S3 Transfer Manager.
Introducción a Amazon S3 y Java
Este ejemplo recupera una clave conocida de un bucket de S3 de uso general. Necesitas la región del bucket, permiso para leer ese objeto y una suma SHA-256 proporcionada por quien lo publica o calculada a partir del archivo original antes de subirlo. Mantén el objeto sin cambios durante la descarga. La suma de comprobación identifica los bytes que quieres importar, incluso si se trata de un objeto vacío o un archivo binario.
Configurar AWS SDK v2
Los comandos usan Bash en Linux, la configuración regional C.UTF-8 y un JDK y
Maven instalados por separado. La configuración regional UTF-8 mantiene intactos los argumentos de
claves y nombres de archivo con caracteres no ASCII. Las versiones probadas son OpenJDK 21.0.12.1,
Maven 3.10.0, AWS SDK v2 2.55.13 y AWS CRT 0.48.5.
Las versiones de las dependencias que aparecen a continuación están fijadas para que las pruebas
sean reproducibles; consulta las versiones de AWS SDK al actualizarlas.
Crea un proyecto nuevo fuera de una copia de trabajo de Maven que tenga un directorio
.mvn. Maven puede heredar las opciones de la JVM, las extensiones y las
opciones de comandos de ese directorio. Este bloque comprueba las herramientas y rechaza esa
configuración heredada antes de crear archivos; mantiene tu shell en su directorio actual.
Si s3-import ya existe, usa otro directorio nuevo en lugar de borrar sus archivos.
(
for tool in java javac mvn; do
command -v "$tool" >/dev/null || {
printf 'Missing prerequisite: %s\n' "$tool" >&2
exit 1
}
done
parent="$PWD"
while :; do
[ ! -e "$parent/.mvn" ] || {
printf 'Use a directory outside the enclosing Maven configuration.\n' >&2
exit 1
}
[ "$parent" != / ] || break
parent="${parent%/*}"
[ -n "$parent" ] || parent=/
done
mkdir s3-import && mkdir -p s3-import/src/main/java
)
Guarda este pom.xml completo en s3-import/. El BOM mantiene
los módulos del SDK en la misma versión; CRT tiene su propia versión. El plugin de dependencias
copia los JAR necesarios en tiempo de ejecución para el lanzador de Java.
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>example</groupId>
<artifactId>s3-import</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>bom</artifactId>
<version>2.55.13</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3-transfer-manager</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk.crt</groupId>
<artifactId>aws-crt</artifactId>
<version>0.48.5</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>1.7.36</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.8.1</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>copy-dependencies</goal></goals>
<configuration><includeScope>runtime</includeScope></configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Configurar las credenciales de AWS
Configura las credenciales mediante el inicio de sesión de AWS o el rol de ejecución que ya use tu
organización. Este programa usa la
cadena de proveedores de credenciales predeterminada del SDK,
que comprueba las propiedades del sistema de Java, las variables de entorno, los perfiles
compartidos y los roles de ejecución. Las credenciales temporales del entorno requieren los tres
valores: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY y
AWS_SESSION_TOKEN. No los incluyas en el código fuente de Java ni en los archivos de Maven.
Las dependencias del ejemplo admiten credenciales del entorno y perfiles compartidos básicos.
Un perfil que obtiene credenciales mediante SSO o AssumeRole también necesita los módulos del SDK
correspondientes, como sso, ssooidc o
sts, en la versión del BOM. Completa ese inicio de sesión antes de ejecutar
el importador.
Configurar la región de AWS
Pasa la región del bucket como segundo argumento del programa, por ejemplo,
us-east-1. Cada cliente usa Region.of(regionName), por lo que no depende
de una región configurada en tu shell o perfil compartido. El argumento de la clave es la clave
literal de S3, no una URL s3://; no codifiques por tu cuenta los espacios ni
los signos de porcentaje para una URL.
Importar archivos de S3
Guarda lo siguiente como s3-import/src/main/java/S3FileImporter.java:
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.util.HexFormat;
import java.util.Set;
import java.util.concurrent.CompletionException;
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
import software.amazon.awssdk.core.sync.ResponseTransformer;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3AsyncClient;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.S3Exception;
import software.amazon.awssdk.transfer.s3.S3TransferManager;
import software.amazon.awssdk.transfer.s3.model.DownloadFileRequest;
public class S3FileImporter {
public static void main(String[] args) {
try {
run(args);
} catch (Exception error) {
Throwable cause = error;
while (cause instanceof CompletionException && cause.getCause() != null) {
cause = cause.getCause();
}
if (cause instanceof S3Exception s3Error) {
System.err.println("S3 download failed (HTTP " + s3Error.statusCode() + ").");
} else if (cause instanceof IOException || cause instanceof IllegalArgumentException) {
System.err.println(cause.getMessage());
} else {
System.err.println("Download failed; check credentials, connectivity, and local disk space.");
}
System.exit(1);
}
}
private static void run(String[] args) throws Exception {
if (args.length != 6 || !Set.of("sync", "async", "crt", "transfer").contains(args[0])) {
throw new IllegalArgumentException(
"Usage: S3FileImporter <sync|async|crt|transfer> <region> <bucket> <key> <destination> <sha256>");
}
String expectedHash = args[5];
if (!expectedHash.matches("[0-9a-fA-F]{64}")) {
throw new IllegalArgumentException("Supply the original object's 64-digit SHA-256.");
}
Path destination = Path.of(args[4]).toAbsolutePath();
if (Files.exists(destination, LinkOption.NOFOLLOW_LINKS)) {
throw new FileAlreadyExistsException("Destination already exists: " + destination);
}
GetObjectRequest request = GetObjectRequest.builder().bucket(args[2]).key(args[3]).build();
Region region = Region.of(args[1]);
Path stagingDirectory = Files.createTempDirectory(destination.getParent(), ".s3-import-");
Path stagedFile = stagingDirectory.resolve("object.bin");
try {
download(args[0], region, request, stagedFile);
if (!sha256(stagedFile).equalsIgnoreCase(expectedHash)) {
throw new IOException("SHA-256 mismatch; destination was not published.");
}
Files.move(stagedFile, destination);
System.out.println("Downloaded " + Files.size(destination) + " bytes to " + destination);
} finally {
Files.deleteIfExists(stagedFile);
Files.delete(stagingDirectory);
}
}
private static void download(String mode, Region region, GetObjectRequest request, Path file) {
if (mode.equals("sync")) {
try (S3Client client = S3Client.builder().region(region)
.overrideConfiguration(c -> c.retryStrategy(r -> r.maxAttempts(1))).build()) {
client.getObject(request, ResponseTransformer.toFile(file));
}
return;
}
if (mode.equals("async")) {
try (S3AsyncClient client = S3AsyncClient.builder().region(region)
.overrideConfiguration(c -> c.retryStrategy(r -> r.maxAttempts(1))).build()) {
client.getObject(request, AsyncResponseTransformer.toFile(file)).join();
}
return;
}
try (S3AsyncClient client = S3AsyncClient.crtBuilder().region(region)
.minimumPartSizeInBytes(8L * 1024 * 1024).maxConcurrency(2)
.retryConfiguration(r -> r.numRetries(0)).build()) {
if (mode.equals("crt")) {
client.getObject(request, AsyncResponseTransformer.toFile(file)).join();
} else {
try (S3TransferManager manager = S3TransferManager.builder().s3Client(client).build()) {
manager.downloadFile(DownloadFileRequest.builder()
.getObjectRequest(request).destination(file).build()).completionFuture().join();
}
}
}
}
private static String sha256(Path file) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
try (InputStream input = Files.newInputStream(file)) {
byte[] buffer = new byte[8192];
int count;
while ((count = input.read(buffer)) != -1) {
digest.update(buffer, 0, count);
}
}
return HexFormat.of().formatHex(digest.digest());
}
}
Desde s3-import/, compila y ejecuta la modalidad síncrona. Sustituye el bucket,
la clave, la región y la suma de comprobación por los valores de tu objeto. El directorio padre del
destino debe existir y el destino debe ser nuevo. Las comillas del classpath conservan el comodín
para que Java lo expanda.
mvn -B package &&
LC_ALL=C.UTF-8 java -cp 'target/classes:target/dependency/*' S3FileImporter \
sync us-east-1 your-bucket-name 'path/to/your/file.bin' \
'./downloaded-file.bin' 'REPLACE_WITH_ORIGINAL_SHA256'
Un comando que termina correctamente muestra la cantidad de bytes descargados y el destino, y
finaliza con estado cero. La verificación de la suma de comprobación se ejecuta después de que el
SDK complete el archivo y antes de que Files.move lo publique. Lee el archivo
completo, por lo que no basta con que exista, tenga un prefijo no vacío o se haya completado un
futuro. No obtengas la suma de comprobación esperada calculando el hash del propio archivo descargado.
Gestionar las excepciones
El comando finaliza con estado uno si se rechaza el destino, la suma de comprobación es incorrecta, el SDK falla o se produce un error de E/S local. Un fallo de descarga o de suma de comprobación capturado elimina el archivo temporal y no deja ningún destino final nuevo. Un destino existente se conserva. Ejecuta las descargas de forma secuencial para un mismo destino; este ejemplo no coordina publicaciones concurrentes.
Si falta una clave, S3 devuelve 404 cuando quien hace la solicitud también tiene
s3:ListBucket, y puede devolver 403 si no tiene ese permiso. Por lo tanto, un 403
no permite distinguir entre una clave inexistente y un acceso denegado. Comprueba la clave exacta
y los permisos con las reglas de permisos de GetObject.
Si la suma de comprobación no coincide, confirma el hash de quien publica el objeto y que este no
haya cambiado antes de volver a intentarlo con un destino nuevo. Si fallan las credenciales o la
red, corrige ese requisito previo y repite el comando. No interpretes una excepción mostrada como
una importación correcta.
Tipos de clientes de S3
Elige el modo cambiando solo el primer argumento del programa y usando un destino nuevo.
Cliente síncrono (S3Client)
sync bloquea la ejecución dentro de getObject hasta
que se completa el transformador de respuesta a archivo. Es un punto de partida útil para un
comando que importa un objeto.
Cliente asíncrono (S3AsyncClient)
async usa el cliente asíncrono estándar de Java. En una aplicación, su
futuro permite que otras tareas continúen mientras se ejecuta la descarga. Este comando llama a
join() antes de comprobar el archivo, cerrar el cliente o finalizar;
de lo contrario, el proceso podría finalizar antes de que termine la importación.
Cliente de rendimiento mejorado (AWS CRT)
crt usa el
cliente de S3 basado en AWS CRT, una implementación de
S3AsyncClient que puede recuperar objetos grandes por rangos de bytes. Su
dependencia nativa se incluye en la compilación con Maven. Tiene menos opciones de configuración
que el cliente estándar. Mide tu carga de trabajo antes de elegirlo por su rendimiento; estos
comandos no prometen una velocidad de transferencia específica.
Optimizar el rendimiento
Transferir directamente al disco
Los cuatro modos usan archivos de destino en lugar de cargar todo el objeto en un array de bytes
de Java. La pasada de SHA-256 también lee en búferes de tamaño fijo. Reserva espacio en disco para
el objeto completo y usa un directorio de destino con permisos de escritura. Si el proceso se
termina a la fuerza, inspecciona y elimina el directorio .s3-import-* que haya
quedado antes de volver a intentarlo; el bloque finally de Java no puede
realizar la limpieza de un proceso terminado a la fuerza.
Descargas por partes
transfer usa S3 Transfer Manager
con el cliente CRT proporcionado explícitamente. Las descargas grandes pueden usar solicitudes
paralelas de rangos de bytes; el ejemplo establece un tamaño mínimo de parte de 8 MiB y un máximo
de dos conexiones. Se trata de rangos de descarga, no de subidas multiparte de S3. Se espera a que
se complete el futuro del gestor antes de verificar la suma de comprobación, y el gestor se cierra
antes que su cliente.
Los reintentos están deshabilitados en esta demostración para que las solicitudes fallidas se notifiquen a quien las realiza. En una aplicación, elige una política de reintentos limitada y límites de tiempo para el cliente seleccionado en lugar de suponer que los constructores del cliente estándar y de CRT aceptan configuraciones idénticas. De lo contrario, una descarga puede quedar a la espera de una conexión lenta; este programa no establece un plazo máximo para toda la transferencia.
Permisos de IAM
Para la versión actual de una clave conocida en un bucket de uso general, concede
s3:GetObject sobre el ARN del objeto. No es necesario listar el bucket para este
importador. Por ejemplo, pide a tu administrador de AWS que adapte esta política al objeto que
necesitas:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-bucket-name/path/to/your/file.bin"
}
]
}
Este tutorial supone que el objeto se puede recuperar de inmediato y está cifrado con claves gestionadas por S3. Los objetos archivados pueden necesitar una restauración, y el cifrado con KMS añade requisitos de permisos sobre las claves. Consulta la correspondencia de permisos de la guía del usuario de S3 para esos casos. Este programa no solicita un ID de versión específico ni gestiona claves SSE-C.
Buenas prácticas
Conserva una suma de comprobación de confianza junto a cada objeto que quieras importar. Si más adelante integras este comando en una tarea, devuelve su estado distinto de cero al planificador y permite que el procesamiento posterior lea solo el destino final verificado. Selecciona claves inmutables o añade recuperación con reconocimiento de versiones cuando quien publica los objetos los sustituya en la misma ubicación.
Para importar objetos de S3 a un flujo de trabajo gestionado de procesamiento de archivos, consulta el S3 Import Robot y el SDK de Java de Transloadit.
