Comment déployer une application web en Java et Angular dans un seul artéfact JAR ou WAR
UPDATED Je viens de faire une mise à jour de la solution, avec une architecture plus elegante, pour Spring Boot 3 avec Angular 15, actuellement uniquement en Anglais:
https://marmo.dev/angular-with-java
Vieille version
Je suis en train de mettre à jour le projet avec Angular 13 et Spring Boot 2.6.2, il est encore en ‘test’, vous pouvez trouver les sources ici:
https://github.com/marco76/java-angular-basic/tree/origin/angular-13-spring-boot-262
si vous allez le tester, merci de me laisser un commentaire.
Dans votre projet, vous pouvez déployer votre application Angular et Java dans le même artefact (.jar ou .war) ou dans des artefacts séparés.
Si les équipes frontend et backend travaillent indépendamment et que l'équipe frontend utilise un monorépo, des artefacts séparés peuvent présenter certains avantages.
Un tutoriel vidéo se trouve sur ici (pour le français envoyer un commentaire) :
Un JAR avec Spring Boot ou Java EE / Jakarta EE
Ce projet contient 3 modules, le parent et 2 enfants (frontend et backend).
Dans cet exemple nous utilisons Spring Boot, la même chose s'applique pour un projet Java EE / Jakarta EE.
Nous construisons un fat JAR Spring qui contient le backend et le frontend.
Vous pouvez trouver le projet sur GitHub ici : https://github.com/marco76/java-angular-basic
Vous pouvez voir ici comment le projet est structuré :
L'artefact du projet backend contiendra les classes compilées en Java et l'application Angular :
Pour construire et démarrer le projet localement :
git clone https://github.com/marco76/java-angular-basic.git
cd ./java-angular-basic.git
mvn clean package
cd ./backend/target
java -jar backend-0.1-SNAPSHOT.jar
pom.xml parent
Le fichier parent pom.xml
déclare les 2 modules à construire. frontend
pour Angular et backend
pour Java.
Les 2 modules peuvent être démarrés séparément pendant le développement. L'application Angular utilisera le port 4200, l'application Java utilisera le port 8080.
<?xml version="1.0" encoding="UTF-8"?>
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dev.marco</groupId>
<artifactId>java-angular-example</artifactId>
<packaging>pom</packaging>
<version>0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.3</version>
</parent>
<modules>
<module>frontend</module>
<module>backend</module>
</modules>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.3.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.4.3</version>
</dependency>
</dependencies>
</project>
Frontend pom.xml
Le module frontend contient une application Angular CLI standard.
Pendant le développement, vous pouvez démarrer l'application avec un ng serve
classique.
Le gros du travail est fait par le plugin https://github.com/eirslett/frontend-maven-plugin. Ce plugin télécharge node
, installe les bibliothèques et construit le projet.
Le résultat est un fichier jar
qui contient l'application Angular compilée.
<?xml version="1.0" encoding="UTF-8"?>
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>dev.marco</groupId>
<artifactId>java-angular-example</artifactId>
<version>0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>frontend</artifactId>
<packaging>jar</packaging>
<build>
<plugins>
<!-- clean the dist directory used by Angular -->
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<filesets>
<fileset>
<directory>dist</directory>
</fileset>
</filesets>
</configuration>
</plugin>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.11.2</version>
<executions>
<!-- Install node and npm -->
<execution>
<id>Install Node and NPM</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
<configuration>
<nodeVersion>v14.16.0</nodeVersion>
</configuration>
</execution>
<!-- clean install -->
<execution>
<id>npm install</id>
<goals>
<goal>npm</goal>
</goals>
</execution>
<!-- build app -->
<execution>
<id>npm run build</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>run build --prod</arguments>
</configuration>
</execution>
<!-- code validation -->
<execution>
<id>npm run lint</id>
<goals>
<goal>npm</goal>
</goals>
<phase>test</phase>
<configuration>
<arguments>run lint</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<resources>
<resource>
<!-- we copy the content of the frontend directory in the final artifact -->
<directory>dist/frontend</directory>
</resource>
</resources>
</build>
</project>
Le backend Java pom.xml
Vous devez importer le module frontend, le code du frontend sera inclus dans votre paquet final.
Il y a une dépendance avec le module frontend. Cette solution n'est pas idéale car le module backend n'est pas complètement indépendant.
Nous utilisons le maven-dependency-plugin
pour extraire le contenu du frontend et copier le contenu dans notre artefact backend.
L'application Angular est copiée dans le dossier /classes/static
, ce dossier est utilisé pour servir du contenu statique dans Spring Boot.
<?xml version="1.0" encoding="UTF-8"?>
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>dev.marco</groupId>
<artifactId>java-angular-example</artifactId>
<version>0.1-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.build.outputEncoding>UTF-8</project.build.outputEncoding>
</properties>
<artifactId>backend</artifactId>
<dependencies>
<dependency>
<groupId>dev.marco</groupId>
<artifactId>frontend</artifactId>
<version>0.1-SNAPSHOT</version>
<type>jar</type>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.4.3</version>
<configuration>
<mainClass>dev.marco.example.springboot.Application</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.1.2</version>
<executions>
<execution>
<id>merge</id>
<phase>initialize</phase>
<goals>
<goal>unpack</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>dev.marco</groupId>
<artifactId>frontend</artifactId>
<type>jar</type>
<overWrite>true</overWrite>
<outputDirectory>${project.build.directory}/classes/static</outputDirectory>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Spring
Dans notre exemple, nous avons ajouté un controller REST pour déployer et exécuter l'application de base.
Pendant la phase de développement, nous démarrons l'application Spring backend et l'application Angular dans des instances séparées.
Pour permettre la communication entre le HttpClient
d'Angular et le RestController
de Spring, nous devons activer un Cross Origin Resource Sharing dans Spring Boot ou nos requêtes seront refusées.
@RestController
// we allow localhost:4200 for testing purposes
@CrossOrigin(origins = "http://localhost:4200")
public class HelloController {
@RequestMapping(value = "/message", produces = MediaType.APPLICATION_JSON_VALUE)
public Map<String, String> index() {
return Collections.singletonMap("message", "Greetings from Spring Boot!");
}
}
Angular
Notre application Angular ne fait rien de spécial, nous appelons simplement le service backend et nous affichons le message reçu :
export class AppComponent implements OnInit{
title = 'frontend';
message = '';
constructor(private http: HttpClient) { }
ngOnInit(): void {
this.http.get('http://localhost:8080/message').pipe(
first(),
tap(result => console.log('Message received from the server: ', result)),
map(result => this.message = (result as any).message)
).subscribe();
}
}
Déployer un WAR avec Tomcat
Si vous devez déployer un WAR avec Tomcat, vous pouvez utiliser la branche suivante
https://github.com/marco76/java-angular-basic/tree/feature/war-for-tomcat
Vous devez
-
mvn clean package
-
Dans le backend il y aura un fichier : angular-spring-example.war
-
Déployer le fichier, il devrait être visible sous : localhost:8080/angular-spring-example, si vous voulez l'exécuter en tant que / vous devez le renommer dans ROOT avant le déploiement.
Changements majeurs par rapport à la version JAR :
backend/pom.xml
Nous changeons le type de packaging et nous ajoutons une dépendance à spring-boot-starter-tomcat
<packaging>war</packaging>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
backend/src/main/java/dev/marco/example/springboot/Application.java
@SpringBootApplication
// we extend SpringBootServletInitializer
public class Application extends SpringBootServletInitializer {