1. Panoramica
In questo codelab, impareremo a conoscere il progetto Spring Native, a creare un'app che lo utilizza e a eseguirne il deployment su Google Cloud.
Esamineremo i suoi componenti, la cronologia recente del progetto, alcuni casi d'uso e, naturalmente, i passaggi necessari per utilizzarlo nei tuoi progetti.
Il progetto Spring Native è attualmente in fase sperimentale, pertanto per iniziare è necessaria una configurazione specifica. Tuttavia, come annunciato in occasione di SpringOne 2021, Spring Native verrà integrato in Spring Framework 6.0 e Spring Boot 3.0 con supporto di prima classe, quindi questo è il momento perfetto per dare un'occhiata più da vicino al progetto qualche mese prima del suo rilascio.
Sebbene la compilazione JIT sia stata ottimizzata molto bene per processi a esecuzione prolungata, esistono determinati casi d'uso in cui le applicazioni compilate AOT funzionano ancora meglio, di cui parleremo durante il codelab.
Imparerai a utilizzare
- Utilizzo di Cloud Shell
- Abilita l'API Cloud Run
- Crea ed esegui il deployment di un'app nativa Spring
- Esegui il deployment di un'app di questo tipo in Cloud Run
Che cosa ti serve
- Un progetto Google Cloud Platform con un account di fatturazione GCP attivo.
- gcloud CLI installata o accesso a Cloud Shell
- Competenze di base in Java e XML
- Conoscenza pratica dei comandi Linux più comuni
Sondaggio
Come utilizzerai questo tutorial?
Come valuteresti la tua esperienza con Java?
Come valuti la tua esperienza di utilizzo dei servizi Google Cloud?
2. Sfondo
Il progetto Spring Native utilizza diverse tecnologie per offrire agli sviluppatori prestazioni delle applicazioni native.
Per comprendere appieno Spring Native, è utile conoscere alcune di queste tecnologie dei componenti, cosa ci consentono di fare e come funzionano insieme.
Compilazione AOT
Quando gli sviluppatori eseguono javac normalmente in tempo di compilazione, il nostro codice sorgente .java viene compilato in file .class scritti in bytecode. Questo bytecode è destinato a essere compreso solo dalla Java Virtual Machine, quindi la JVM dovrà interpretare questo codice su altre macchine per poter eseguire il nostro codice.
Questo processo ci offre la portabilità tipica di Java, che ci consente di "scrivere una volta ed eseguire ovunque ", ma è costoso rispetto all'esecuzione di codice nativo.
Fortunatamente, la maggior parte delle implementazioni della JVM utilizza la compilazione just-in-time per ridurre questo costo di interpretazione. A questo scopo, vengono conteggiate le chiamate per una funzione e,se viene chiamata abbastanza spesso da superare una soglia ( 10.000 per impostazione predefinita), viene compilata in codice nativo in fase di runtime per evitare ulteriori interpretazioni costose.
La compilazione AOT adotta l'approccio opposto, compilando tutto il codice raggiungibile in un eseguibile nativo in fase di compilazione. In questo modo, la portabilità viene scambiata con l'efficienza della memoria e altri miglioramenti delle prestazioni in fase di esecuzione.

Si tratta ovviamente di un compromesso che non sempre vale la pena accettare. Tuttavia, la compilazione AOT può essere utile in determinati casi d'uso, ad esempio:
- Applicazioni di breve durata in cui il tempo di avvio è importante
- Ambienti con vincoli di memoria elevati in cui la compilazione JIT potrebbe essere troppo costosa
Come curiosità, la compilazione AOT è stata introdotta come funzionalità sperimentale in JDK 9, anche se questa implementazione era costosa da mantenere e non ha mai preso piede, quindi è stata rimossa silenziosamente in Java 17 a favore dell'utilizzo di GraalVM da parte degli sviluppatori.
GraalVM
GraalVM è una distribuzione JDK open source altamente ottimizzata che vanta tempi di avvio estremamente rapidi, compilazione di immagini native AOT e funzionalità poliglotte che consentono agli sviluppatori di combinare più linguaggi in un'unica applicazione.
GraalVM è in fase di sviluppo attivo, acquisisce nuove funzionalità e migliora quelle esistenti in continuazione, quindi invito gli sviluppatori a rimanere aggiornati.
Alcuni traguardi recenti sono:
- Un nuovo output di compilazione dell'immagine nativa di facile utilizzo ( 18/01/2021)
- Supporto di Java 17 ( 18/01/2022)
- Abilitazione della compilazione a più livelli per impostazione predefinita per migliorare i tempi di compilazione multilingue ( 20/04/2021)
Spring Native
In parole semplici, Spring Native consente di utilizzare il compilatore native-image di GraalVM per trasformare le applicazioni Spring in eseguibili nativi.
Questo processo prevede l'esecuzione di un'analisi statica dell'applicazione in tempo di compilazione per trovare tutti i metodi dell'applicazione raggiungibili dal punto di ingresso.
In questo modo viene creata una concezione "a mondo chiuso" dell'applicazione, in cui si presume che tutto il codice sia noto al tempo di compilazione e non è consentito caricare nuovo codice in fase di runtime.
È importante notare che la generazione di immagini native è un processo che richiede molta memoria, richiede più tempo della compilazione di un'applicazione normale e impone limitazioni a determinati aspetti di Java.
In alcuni casi, non sono necessarie modifiche al codice per far funzionare un'applicazione con Spring Native. Tuttavia, alcune situazioni richiedono una configurazione nativa specifica per funzionare correttamente. In queste situazioni, Spring Native spesso fornisce suggerimenti nativi per semplificare la procedura.
3. Configurazione/Preparazione
Prima di iniziare a implementare Spring Native, dobbiamo creare e distribuire la nostra app per stabilire una base di riferimento delle prestazioni che potremo confrontare in un secondo momento con la versione nativa.
1. Creazione del progetto
Iniziamo scaricando la nostra app da start.spring.io:
curl https://start.spring.io/starter.zip -d dependencies=web \
-d javaVersion=11 \
-d bootVersion=2.6.4 -o io-native-starter.zip
Questa app di base utilizza Spring Boot 2.6.4, l'ultima versione supportata dal progetto spring-native al momento della stesura.
Tieni presente che, a partire dalla release di GraalVM 21.0.3, puoi utilizzare anche Java 17 per questo esempio. Per questo tutorial continueremo a utilizzare Java 11 per ridurre al minimo la configurazione.
Una volta che abbiamo il file zip nella riga di comando, possiamo creare una sottodirectory per il nostro progetto e decomprimere la cartella al suo interno:
mkdir spring-native cd spring-native unzip ../io-native-starter.zip
2. Modifiche al codice
Una volta aperto il progetto, aggiungeremo rapidamente un segnale di vita e mostreremo le prestazioni di Spring Native dopo l'esecuzione.
Modifica DemoApplication.java in modo che corrisponda a questo:
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.Duration;
import java.time.Instant;
@RestController
@SpringBootApplication
public class DemoApplication {
private static Instant startTime;
private static Instant readyTime;
public static void main(String[] args) {
startTime = Instant.now();
SpringApplication.run(DemoApplication.class, args);
}
@GetMapping("/")
public String index() {
return "Time between start and ApplicationReadyEvent: "
+ Duration.between(startTime, readyTime).toMillis()
+ "ms";
}
@EventListener(ApplicationReadyEvent.class)
public void ready() {
readyTime = Instant.now();
}
}
A questo punto, l'app di base è pronta, quindi puoi creare un'immagine ed eseguirla localmente per farti un'idea del tempo di avvio prima di convertirla in un'applicazione nativa.
Per creare la nostra immagine:
mvn spring-boot:build-image
Puoi anche utilizzare docker images demo per farti un'idea delle dimensioni dell'immagine di base: 
Per eseguire la nostra app:
docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT
3. Esegui il deployment dell'app di base
Ora che abbiamo la nostra app, la implementeremo e prenderemo nota dei tempi, che confronteremo in un secondo momento con i tempi di avvio della nostra app nativa.
A seconda del tipo di applicazione che stai creando, esistono diversi tipi di hosting per i tuoi contenuti.
Tuttavia, poiché il nostro esempio è un'applicazione web molto semplice e diretta, possiamo semplificare le cose e fare affidamento su Cloud Run.
Se stai seguendo la procedura sulla tua macchina, assicurati di aver installato e aggiornato lo strumento gcloud CLI.
Se utilizzi Cloud Shell, tutto verrà gestito e potrai semplicemente eseguire il seguente comando nella directory di origine:
gcloud run deploy
4. Configurazione applicazione
1. Configurazione dei nostri repository Maven
Poiché questo progetto è ancora in fase sperimentale, dovremo configurare la nostra app in modo che possa trovare artefatti sperimentali, che non sono disponibili nel repository centrale di Maven.
Ciò comporta l'aggiunta dei seguenti elementi al file pom.xml, che puoi fare nell'editor che preferisci.
Aggiungi le seguenti sezioni repositories e pluginRepositories al nostro pom:
<repositories>
<repository>
<id>spring-release</id>
<name>Spring release</name>
<url>https://repo.spring.io/release</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-release</id>
<name>Spring release</name>
<url>https://repo.spring.io/release</url>
</pluginRepository>
</pluginRepositories>
2. Aggiunta delle dipendenze
Successivamente, aggiungi la dipendenza spring-native, necessaria per eseguire un'applicazione Spring come immagine nativa. Nota: questo passaggio non è necessario se utilizzi Gradle
<dependencies>
<!-- ... -->
<dependency>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-native</artifactId>
<version>0.11.2</version>
</dependency>
</dependencies>
3. Aggiunta/attivazione dei nostri plug-in
Ora aggiungi il plug-in AOT per migliorare la compatibilità e l'ingombro delle immagini native ( scopri di più):
<plugins>
<!-- ... -->
<plugin>
<groupId>org.springframework.experimental</groupId>
<artifactId>spring-aot-maven-plugin</artifactId>
<version>0.11.2</version>
<executions>
<execution>
<id>generate</id>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
Ora aggiorniamo spring-boot-maven-plugin per attivare il supporto delle immagini native e utilizziamo il builder paketo per creare la nostra immagine nativa:
<plugins>
<!-- ... -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<builder>paketobuildpacks/builder:tiny</builder>
<env>
<BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
</env>
</image>
</configuration>
</plugin>
</plugins>
Tieni presente che l'immagine del piccolo costruttore è solo una delle tante opzioni. È una buona scelta per il nostro caso d'uso perché ha pochissime librerie e utilità aggiuntive, il che contribuisce a ridurre al minimo la nostra superficie di attacco.
Se, ad esempio, stai creando un'app che deve accedere ad alcune librerie C comuni o non hai ancora la certezza dei requisiti della tua app, il full-builder potrebbe essere più adatto.
5. Crea ed esegui l'app nativa
Una volta configurato tutto, dovremmo essere in grado di creare la nostra immagine ed eseguire la nostra app nativa compilata.
Prima di eseguire la build, ecco alcuni aspetti da tenere a mente:
- Questa operazione richiederà più tempo di una build normale (alcuni minuti)

- Questa procedura di compilazione può richiedere molta memoria (alcuni gigabyte)

- Questo processo di compilazione richiede che il daemon Docker sia raggiungibile
- In questo esempio, la procedura viene eseguita manualmente, ma puoi anche configurare le fasi di build per attivare automaticamente un profilo di build nativo.
Per creare la nostra immagine:
mvn spring-boot:build-image
Una volta creata, possiamo vedere l'app nativa in azione.
Per eseguire la nostra app:
docker run --rm -p 8080:8080 demo:0.0.1-SNAPSHOT
A questo punto, siamo in una posizione ideale per vedere entrambi i lati dell'equazione dell'applicazione nativa.
Abbiamo rinunciato a un po' di tempo di compilazione e a un maggiore utilizzo della memoria in fase di compilazione, ma in cambio otteniamo un'applicazione che può avviarsi molto più rapidamente e consumare molta meno memoria utilizzata (a seconda del carico di lavoro).
Se eseguiamo docker images demo per confrontare le dimensioni dell'immagine nativa con l'originale, possiamo notare una riduzione drastica:

Va inoltre osservato che nei casi d'uso più complessi sono necessarie modifiche aggiuntive per comunicare al compilatore AOT cosa farà la tua app in fase di runtime. Per questo motivo, alcuni carichi di lavoro prevedibili (come i job batch) potrebbero essere molto adatti, mentre altri potrebbero essere più difficili.
6. Deployment della nostra app nativa
Per eseguire il deployment della nostra app in Cloud Run, dobbiamo inserire la nostra immagine nativa in un gestore di pacchetti come Artifact Registry.
1. Preparazione del repository Docker
Possiamo iniziare questa procedura creando un repository:
gcloud artifacts repositories create native-image-docker-repo --repository-format=docker \
--location=us-central1 --description="Repository for our native images"
Successivamente, vogliamo assicurarci di essere autenticati per eseguire il push nel nostro nuovo registro.
gcloud CLI può semplificare notevolmente questa procedura:
gcloud auth configure-docker us-central1-docker.pkg.dev
2. Esegui il push dell'immagine in Artifact Registry
Ora tagghiamo la nostra immagine:
export PROJECT_ID=$(gcloud config list --format 'value(core.project)')
docker tag demo:0.0.1-SNAPSHOT \
us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2
Poi possiamo utilizzare docker push per inviarlo ad Artifact Registry:
docker push us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2
3. Deployment in Cloud Run
Ora siamo pronti per eseguire il deployment in Cloud Run dell'immagine che abbiamo archiviato in Artifact Registry:
gcloud run deploy --image us-central1-docker.pkg.dev/$PROJECT_ID/native-image-docker-repo/quickstart-image:tag2
Poiché abbiamo creato e implementato la nostra app come immagine nativa, possiamo essere certi che la nostra applicazione utilizzi in modo eccellente i costi dell'infrastruttura durante l'esecuzione.
Non esitare a confrontare i tempi di avvio della nostra app di base con questa nuova app nativa.

7. Riepilogo/Pulizia
Congratulazioni per aver creato ed eseguito il deployment di un'applicazione nativa Spring su Google Cloud.
Ci auguriamo che questo tutorial ti incoraggi a familiarizzare con il progetto Spring Native e a tenerlo presente se dovesse soddisfare le tue esigenze in futuro.
(Facoltativo) Libera spazio e/o disattiva il servizio
Che tu abbia creato un progetto Google Cloud per questo codelab o ne stia riutilizzando uno esistente, fai attenzione a evitare addebiti non necessari per le risorse che abbiamo utilizzato.
Puoi eliminare o disattivare i servizi Cloud Run che abbiamo creato, eliminare l'immagine che abbiamo ospitato o chiudere l'intero progetto.
8. Risorse aggiuntive
Sebbene il progetto Spring Native sia attualmente un progetto nuovo e sperimentale, esiste già una vasta gamma di risorse utili per aiutare i primi utenti a risolvere i problemi e a partecipare:
Risorse aggiuntive
Di seguito sono riportate risorse online che potrebbero essere pertinenti per questo tutorial:
- Scopri di più sui suggerimenti nativi
- Scopri di più su GraalVM
- Come partecipare
- Errore di memoria insufficiente durante la creazione di immagini native
- Errore di avvio dell'applicazione
Licenza
Questo lavoro è concesso in licenza ai sensi di una licenza Creative Commons Attribution 2.0 Generic.