Building your own Maven archetype
Github is easy to share codes with others, for example I created jakartaee9-starter-boilerplate as a project template for Jakarta EE developers. For those who are familiar with Github, it is easy to start their new projects by forking or cloning this project directly. But obviously for a general Jakarta EE application, you do not need the configuraitons of all application severs, eg. Glassfish/Payara, WildFly, OpenLiberty, Apache TomEE, etc. For most of Java developers esp. Maven users, a simple and clean Maven archetype is still the preferred option to generate the new project skeleton, and you can add the required configuration back later.
I had created some maven archetype like toys myself in the past years, but I never made them public through the Maven Central repository. In this post, I will replay the steps of creating the Maven Archetype for Jakarta EE 9 and the followed steps of publishing it to the Maven Central repository and making it public to the world.
A better formatted version is published on dev.to, https://dev.to/hantsy_26/building-your-own-maven-archetype-gm8
Generating the archetype project skeleton
Maven archetype is the Maven specific templates used to generate a new project in the Maven build system. Maven itself provides an official archetype (maven-archetype-archetype) for you to build your own Maven archetype.
The official archetype introduction page lists all official archetypes.
Run the following command to generate the archetype project in an interactive mode.
mvn archetype:generate
When you hit down the Enter key, it will take some seconds to list all available archetypes. Every archetype is attached to a number.
Type the number of maven-archetype-archetype , then it will ask you to fill groupId, archetypeId, package and version of your project. After all fields are filled as expected, it will generate the project in seconds.
Alternatively, you can generate the project in a single command aka the batch mode.
mvn archetype:generate -B \
-DarchetypeGroupId=org.apache.maven.archetypes \
-DarchetypeArtifactId=maven-archetype-archetype \
-DarchetypeVersion=1.4 \
-DgroupId=io.github.hantsy \
-DartifactId=maven-archetype-jakartaee9 \
-Dversion=1.0-SNAPSHOT
Note here we use the -B parameter in the command line to skip the interactive questionnaire steps and run the command in batch mode.
More info about Maven archetype and maven-archetype-plugin, please check Maven Archetype.
Exploring the project structure
Import the project into your IDEs, such as Apache NetBeans, IntelliJ IDEA, etc.
You will see the following project file structure.
├── pom.xml
└── src
├── main
│ └── resources
│ ├── archetype-resources
│ │ ├── pom.xml
│ │ └── src
│ │ ├── main
│ │ │ └── java
│ │ │ └── App.java
│ │ └── test
│ │ └── java
│ │ └── AppTest.java
│ └── META-INF
│ └── maven
│ └── archetype-metadata.xml
└── test
└── resources
└── projects
└── it-basic
├── archetype.properties
└── goal.txt
The structure is similar to a general Maven project, there are some difference.
- The src/main/resources/archetype-resources folder stores all template resources used to generate projects from this archetype.
- The src/main/resources/META-INF/maven-archetype-metadata.xml is the archetype description file, including the properties, file sets etc.
- The src/test/resources/projects includes resources to test this archetype, including the properties applied to generate new projects and the goal run in the host integration-test phase.
Preparing the archetype resources
To simplify the work, I reuse the sample codes from jakartaee9-starter-boilerplate.
- Copy GreetingMessage, GreetingService, GreetingService and JaxrsActivator to the src/main/resources/archetype-resources/src/main/java folder. Change the package declaration to the following.
package $package;
…
2. Similar to the last step, copy the GreetingResourceTest and GreetingServiceTest to the src/main/resources/archetype-resources/src/test/java folder. Remember changing the package declaration.
3. Add empty beans.xmlfile to the src/main/resources/archetype-resources/src/main/resources/META-INF/ folder.
<beans xmlns=”https://jakarta.ee/xml/ns/jakartaee" xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=”https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/beans_3_0.xsd" version=”3.0"> </beans>
4. Add a simple arquillian.xml to the src/main/resources/archetype-resources/src/test/resources folder.
<arquillian xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance" xmlns=”http://jboss.org/schema/arquillian" xsi:schemaLocation=”http://jboss.org/schema/arquillian http://jboss.org/schema/arquillian/arquillian_1_0.xsd"> <defaultProtocol type=”Servlet 5.0"/>
<engine>
<property name=”deploymentExportPath”>target/</property> </engine>
<container qualifier=”glassfish” default=”true”>
<configuration>
<property name=”adminHost”>localhost</property>
<property name=”adminPort”>4848</property>
<property name=”adminUser”>admin</property> <! — if https is enabled via `asadmin enable-secure-admin` on a remote server →
<! — <property name=”adminHttps”>true</property> →
<! — if admin password is changed via `asadmin change-admin-password` →
<! — <property name=”adminPassword”>adminadmin</property> →
<! — default is empty → <property name=”adminPassword”></property>
</configuration>
</container>
</arquillian>
5. Change the pom.xml template (src/main/resources/archetype-resources/pom.xml) to the following.
<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>${groupId}</groupId>
<artifactId>${artifactId}</artifactId>
<version>${version}</version>
<packaging>war</packaging>
<name>${artifactId}</name>
<description>A Jakarta EE starter boilerplate for Jakarta EE 9</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<!-- Official Maven Plugins -->
<maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
<maven-war-plugin.version>3.3.1</maven-war-plugin.version>
<maven-dependency-plugin.version>3.1.2</maven-dependency-plugin.version>
<maven-surefire-plugin.version>3.0.0-M5</maven-surefire-plugin.version>
<maven-failsafe-plugin.version>3.0.0-M5</maven-failsafe-plugin.version>
<maven-surefire-report-plugin.version>3.0.0-M5</maven-surefire-report-plugin.version>
<!-- Cargo maven plugin -->
<!-- Since 1.9.0, cargo-maven3-plugin is the default for cargo prefix. -->
<cargo-maven3-plugin.version>1.9.0</cargo-maven3-plugin.version>
<!-- Jakarta EE API -->
<jakartaee-api.version>9.0.0</jakartaee-api.version>
<!-- Arquillian BOM -->
<arquillian-bom.version>1.7.0.Alpha9</arquillian-bom.version>
<junit-jupiter.version>5.7.1</junit-jupiter.version>
<!-- Glassfish server -->
<glassfish.version>6.0.0</glassfish.version>
<arquillian-glassfish6.version>1.0.0.Alpha1</arquillian-glassfish6.version>
<jersey.version>3.0.1</jersey.version>
<!-- by default skip tests -->
<skipTests>true</skipTests>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-api</artifactId>
<version>${jakartaee-api.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.arquillian</groupId>
<artifactId>arquillian-bom</artifactId>
<version>${arquillian-bom.version}</version>
<scope>import</scope>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>${junit-jupiter.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-api</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.arquillian.junit5</groupId>
<artifactId>arquillian-junit5-container</artifactId>
<scope>test</scope>
</dependency>
<!-- see: https://github.com/arquillian/arquillian-core/issues/248 -->
<!-- and https://github.com/arquillian/arquillian-core/pull/246/files -->
<dependency>
<groupId>org.jboss.arquillian.protocol</groupId>
<artifactId>arquillian-protocol-servlet-jakarta</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.codehaus.cargo</groupId>
<artifactId>cargo-maven3-plugin</artifactId>
<version>${cargo-maven3-plugin.version}</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>${maven-war-plugin.version}</version>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>glassfish</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.cargo</groupId>
<artifactId>cargo-maven3-plugin</artifactId>
<configuration>
<container>
<containerId>glassfish6x</containerId>
<artifactInstaller>
<groupId>org.glassfish.main.distributions</groupId>
<artifactId>glassfish</artifactId>
<version>${glassfish.version}</version>
</artifactInstaller>
</container>
<configuration>
<!-- the configuration used to deploy -->
<home>${project.build.directory}/glassfish6x-home</home>
<properties>
<cargo.remote.password></cargo.remote.password>
</properties>
</configuration>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<!-- Jersey -->
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-sse</artifactId>
<version>${jersey.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-binding</artifactId>
<version>${jersey.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
<version>${jersey.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.core</groupId>
<artifactId>jersey-client</artifactId>
<version>${jersey.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.arquillian.container</groupId>
<artifactId>arquillian-glassfish-remote-6</artifactId>
<version>${arquillian-glassfish6.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</profile>
</profiles>
</project>
Note, the value of groupId, artifactId and version elements in this xml refers to a placeholder which can be intercepted in the generating stage.
6. Change the archetype descriptor (src/main/resources/META-INF/maven/archetype-metadata.xml) to the following.
<archetype-descriptor xmlns=”http://maven.apache.org/plugins/maven-archetype-plugin/archetype-descriptor/1.0.0" xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=”http://maven.apache.org/plugins/maven-archetype-plugin/archetype-descriptor/1.0.0 http://maven.apache.org/xsd/archetype-descriptor-1.0.0.xsd" name=”${artifactId}”> <fileSets>
<fileSet filtered=”true” packaged=”true”>
<directory>src/main/java</directory>
</fileSet>
<fileSet> <directory>src/main/resources</directory>
</fileSet>
<fileSet filtered=”true” packaged=”true”>
<directory>src/test/java</directory>
</fileSet>
<fileSet>
<directory>src/test/resources</directory>
</fileSet>
</fileSets>
</archetype-descriptor>
In the above codes, when the filtered attribute is set to true, it will replace the placeholders(eg. $package, etc.) you have set in the template files with the parameters when executing mvn archetype:generate to generate projects. And when the packaged attribute is set to true, it will move the generated file to corresponding packages.
To test the archetype resources, there is a archetype.properties and a goal.txt file prepared in the src/test/resources/projects/it-basic. In the integration-test phase, it will generate a test project using archetype resources and replace the placeholders using the properties defined in the archetype.properties and execute goals in the goal.txt against the generated project.
I created a Github Actions workflow to verify the generated projects.
Publishing to Maven Central Repository
Note, the steps of publishing your packages to the Maven Central repository is a little tedious, please be patient.
Once you have built the maven archetype project successfully, the next step is publishing it to Maven Central repository and make it public, there are some useful guides in Google results. I suggest you pick one or more from the following list and read it firstly.
- Publish JAR To Central Maven Repository
- How to Publish Your Artifacts to Maven Central
- Create your own Maven Archetype in 5 simple steps
- Register an account of the Sonatype issue tracker.
- File a new issue on Community Support to submit your request of publishing your artifact. Check my issue#OSSRH-66424 I created for hantsy/maven-archetype-jakartaee9 as an example.
- Follow the response of the issue, create a new empty repo(the name is the issue id) on Github for authentication requirement. eg. OSSRH-66424
- Generate GPG keys and send to key servers.
Under Windows 10 system, you have to install GPG tools firstly. Simply run the following command to install gpg4win if you are using Chocolatey.
choco install gpg4win
Refresh the environment.
refreshenv
Then generate GPG keys.
# gpg — full-gen-key
gpg (GnuPG) 2.2.27; Copyright © 2021 g10 Code GmbH This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law.
Please select what kind of key you want:
(1) RSA and RSA (default)
(2) DSA and Elgamal
(3) DSA (sign only)
(4) RSA (sign only)
(14) Existing key from card Your selection? 1
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (3072)
Requested keysize is 3072 bits Please specify how long the key should be valid. 0 = key does not expire <n> = key expires in n days <n>w = key expires in n weeks <n>m = key expires in n months <n>y = key expires in n years Key is valid for? (0) Key does not expire at all Is this correct? (y/N) y
GnuPG needs to construct a user ID to identify your key.
Real name: Hantsy Bai
Email address: hantsy@gmail.com
Comment:
You selected this USER-ID: “Hantsy Bai <hantsy@gmail.com>”
Change (N)ame, ©omment, (E)mail or (O)kay/(Q)uit? O
We need to generate a lot of random bytes. It is a good idea to perform some other action (type on the keyboard, move the mouse, utilize the disks) during the prime generation; this gives the random number generator a better chance to gain enough entropy. gpg: AllowSetForegroundWindow(9152) failed: Access is denied.
We need to generate a lot of random bytes. It is a good idea to perform some other action (type on the keyboard, move the mouse, utilize the disks) during the prime generation; this gives the random number generator a better chance to gain enough entropy. gpg: C:/Users/hantsy/AppData/Roaming/gnupg/trustdb.gpg: trustdb created gpg: key E5D9B4272348ADAE marked as ultimately trusted gpg: directory ‘C:/Users/hantsy/AppData/Roaming/gnupg/openpgp-revocs.d’ created gpg: revocation certificate stored as ‘C:/Users/hantsy/AppData/Roaming/gnupg/openpgp-revocs.d\03B4C55DDA964D942C756D79E5D9B4272348ADAE.rev’ public and secret key created and signed.
pub rsa3072 2021–03–30 [SC] 03B4C55DDA964D942C756D79E5D9B4272348ADAE uid Hantsy Bai <hantsy@gmail.com>
sub rsa3072 2021–03–30 [E]
Send the keys to the public key servers.
# gpg — send-keys 03B4C55DDA964D942C756D79E5D9B4272348ADAE gpg: sending key E5D9B4272348ADAE to hkps://hkps.pool.sks-keyservers.net
5. Follow the steps of Deploying to OSSRH with Apache Maven to publish your artifact.
I skip the detailed steps here, please read the Synatype’s article carefully.
6. Then back to the issue#OSSRH-66424, comment and notify the moderator to publish your artifact to the Maven Central repository. In one or two hours, your artifact will be available on Maven Central Repository Search index page.
Publishing to Github Package
The Configuring Apache Maven for use with GitHub Packages page provides detailed steps to publish a package to Github Packages.
- Following the steps of Authenticating to Github Packages to set the Github authentication info in your ~/.m2/settings.xml file.
- Deploy to Github Packages.
- In our case, we have set the distributionManagement to use the Maven Central repository. Use the following command instead.
mvn clean package deploy -DskipTests -DaltDeploymentRepository=github::https://maven.pkg.github.com/hantsy/maven-archetype-jakartaee9
- By default, Github Packages does not allow anyone to access your repository without an authentication. After consulting from Github community, there is a solution to resolve this issue gracefully, it is possible to make your repository accessible to the public.
- Generate a Personal Access Token with packages:read scope.
- Run the following command to generate the repository config.
# docker run ghcr.io/jcansdale/gpr encode <your PAT>
A NuGet `nuget.config` file:
<packageSourceCredentials>
<github>
<add key=”Username” value=”PublicToken” />
<add key=”ClearTextPassword” value=”028d0c8f723084e499eb87a6fe173b3af295d618" />
</github>
</packageSourceCredentials>
A Maven `pom.xml` file:
<repositories>
<repository>
<id>github-public</id> <url>https://public:028d0c8f723084e499eb87a6fe173b3af295d618@maven.pkg.github.com/<OWNER>/*</url>
</repository>
</repositories>
An npm `.npmrc` file:
@OWNER:registry=https://npm.pkg.github.com //npm.pkg.github.com/:_authToken=”\u003028d0c8f723084e499eb87a6fe173b3af295d618"
- As you see, now you can add the above repository to your Maven proxy server or global Maven settings(~/.m2/settings.xml) in your system.
Check the source codes from my Github.
Building your own Maven archetype was originally published in ITNEXT on Medium, where people are continuing the conversation by highlighting and responding to this story.