As a Java developer, you may have an awesome artifact you want to share to the world. One of the best place to do that is probably Maven Central and this tutorial will help you go global 😉.

Why Use Maven Central ?

Assuming you know how Maven or Gradle works, you should know that most of the dependencies we use in a Java project are hosted in a repository. There are many available repositories like Jitpack and you can even create your own private one. But Maven Central is for repositories what Netscape was to browsers, an ancestor but one still living and widely used. In fact, Maven and Gradle by default lookup artifacts through Maven Central while in order to use other repositories, you need a little additional configuration and above all, it's totally free. So, using Maven Central which is well known, our artifacts will be available to anyone without further configuration. With this, you should agree that Maven Central is a good start right ?!

How ?

In order to push your artifact to Maven Central, you need to first push the code in a Github repository and add at least a SSH key in Github. I haven't tested if this method works with Gitlab or any other hoster but I guess it should do it too. After that, we need to sequentially compile, prepare the release before actually releasing it or deploying it if you prefer. We will use this workflow as the steps you should perform (except the "push your code to Github" part) since each one of them require some configuration. the examples I will give are based on the supposition that you are using Maven but feel free to adapt them to Gradle.
⚠️ If you don't own a custom domain, I strongly recommend using io.github.<your_github_username> as groupId for your artifact otherwise, you'll have trouble during the deployment step.

Compilation step

In the compilation step, we will use following plugins:

  • Maven Compiler Plugin handling the compilation.
  • Maven Surefire Plugin for automatically running unit tests after compilation. I can't talk enough about the importance of tests, so at least, write unit tests. Don't forget that you will release your artifact to the whole world so you should make sure it's bug free at most as possible.
  • Maven Failsafe Plugin for running integration tests. Note that this plugin is optional depending on the nature of your project but I strongly recommend it if your artifact is to be used in a web application.
  • Jacoco Maven Plugin for code coverage. You can also use Cobertura if you prefer.
  • Maven Surefire Report Plugin for generating reports from your tests in formats like HTML, etc.
  • Maven Source Plugin for creating a JAR archive containing the source code of your project. This is really helpful for other developers that will use your artifact. Of course modern IDEs will be able to decompile .class files for a peek into their code but it will be a lot easier if they can just download the original one.
  • Maven Javadoc Plugin for producing the javadoc pages 😂. Well, I'm laughing because I know most of us hate writing documentation and there is a lot of memes running wild on the Internet about it but you're going global (I love this term, it makes me feel like I'm doing something really big 🙃). So, you need to do as much as possible to help other developers use easily your artifact and producing a javadoc is part of that. Why not even go further by writing wiki pages in your repository ?!

To get started, add this to your pom.xml and adapt accordingly:

<properties>
    <java.version>1.8</java.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <version.maven-compiler-plugin>3.8.1</version.maven-compiler-plugin>
    <version.maven-surefire-plugin>2.22.2</version.maven-surefire-plugin>
    <version.maven-source-plugin>3.1.0</version.maven-source-plugin>
    <version.maven-javadoc-plugin>3.1.1</version.maven-javadoc-plugin>
    <version.jacoco-maven-plugin>0.8.4</version.jacoco-maven-plugin>
    <version.maven-surefire-report-plugin>2.22.2</version.maven-surefire-report-plugin>
    <maven-failsafe-plugin.version>3.0.0-M3</maven-failsafe-plugin.version>
    <jacoco.reportFolder>${project.build.directory}/jacoco</jacoco.reportFolder>
    <jacoco.utReportFile>${jacoco.reportFolder}/test.exec</jacoco.utReportFile>
    <jacoco.itReportFile>${jacoco.reportFolder}/integrationTest.exec</jacoco.itReportFile>
    <project.testresult.directory>${project.build.directory}/test-results</project.testresult.directory>
    <junit.utReportFolder>${project.testresult.directory}/test</junit.utReportFolder>
    <junit.itReportFolder>${project.testresult.directory}/integrationTest</junit.itReportFolder>
</properties>
...
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>${version.maven-compiler-plugin}</version>
            <configuration>
                <encoding>${project.build.sourceEncoding}</encoding>
                <source>${java.version}</source>
                <target>${java.version}</target>
                <showDeprecation>true</showDeprecation>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>${version.maven-surefire-plugin}</version>
            <configuration>
                <excludes>
                    <exclude>**/*IntegrationTest.java</exclude>
                </excludes>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-source-plugin</artifactId>
            <version>${version.maven-source-plugin}</version>
            <executions>
                <execution>
                    <id>attach-sources</id>
                    <goals>
                        <goal>jar</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-javadoc-plugin</artifactId>
            <version>${version.maven-javadoc-plugin}</version>
            <configuration>
                <encoding>UTF-8</encoding>
            </configuration>
            <executions>
                <execution>
                    <id>attach-javadoc</id>
                    <goals>
                        <goal>jar</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
        <plugin>
            <groupId>org.jacoco</groupId>
            <artifactId>jacoco-maven-plugin</artifactId>
            <version>${version.jacoco-maven-plugin}</version>
            <executions>
                <execution>
                    <id>pre-unit-tests</id>
                    <goals>
                        <goal>prepare-agent</goal>
                    </goals>
                    <configuration>
                        <!-- Sets the path to the file which contains the execution data. -->
                        <destFile>${jacoco.utReportFile}</destFile>
                    </configuration>
                </execution>
                <!-- Ensures that the code coverage report for unit tests is created after unit tests have been run -->
                <execution>
                    <id>post-unit-test</id>
                    <phase>test</phase>
                    <goals>
                        <goal>report</goal>
                    </goals>
                    <configuration>
                        <dataFile>${jacoco.utReportFile}</dataFile>
                        <outputDirectory>${jacoco.reportFolder}</outputDirectory>
                    </configuration>
                </execution>
                <execution>
                    <id>pre-integration-tests</id>
                    <goals>
                        <goal>prepare-agent-integration</goal>
                    </goals>
                    <configuration>
                        <!-- Sets the path to the file which contains the execution data. -->
                        <destFile>${jacoco.itReportFile}</destFile>
                    </configuration>
                </execution>
                <!-- Ensures that the code coverage report for integration tests is created after integration tests have been run -->
                <execution>
                    <id>post-integration-tests</id>
                    <phase>post-integration-test</phase>
                    <goals>
                        <goal>report-integration</goal>
                    </goals>
                    <configuration>
                        <dataFile>${jacoco.itReportFile}</dataFile>
                        <outputDirectory>${jacoco.reportFolder}</outputDirectory>
                    </configuration>
                </execution>
            </executions>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-failsafe-plugin</artifactId>
            <version>${maven-failsafe-plugin.version}</version>
            <configuration>
                <!-- Only if using Spring Boot -->
                <!-- Due to spring-boot repackage, without adding this property test classes are not found
                         See https://github.com/spring-projects/spring-boot/issues/6254 -->
                <classesDirectory>${project.build.outputDirectory}</classesDirectory>
                <!-- Force alphabetical order to have a reproducible build -->
                <runOrder>alphabetical</runOrder>
                <reportsDirectory>${junit.itReportFolder}</reportsDirectory>
                <includes>
                    <include>**/*IT*</include>
                    <include>**/*IntTest*</include>
                </includes>
            </configuration>
            <executions>
                <execution>
                    <id>integration-test</id>
                    <goals>
                        <goal>integration-test</goal>
                    </goals>
                </execution>
                <execution>
                    <id>verify</id>
                    <goals>
                        <goal>verify</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

Preparing the release

To prepare our release, we will use the Maven Release Plugin. As stated by it's documentation, the preparation will do the following:

  • Check that there are no uncommitted changes in the sources
  • Check that there are no SNAPSHOT dependencies
  • Change the version in the POMs from x-SNAPSHOT to a new version (you will be prompted for the versions to use)
  • Transform the SCM information in the POM to include the final destination of the tag
  • Run the project tests against the modified POMs to confirm everything is in working order
  • Commit the modified POMs
  • Tag the code in the SCM with a version name (this will be prompted for)
  • Bump the version in the POMs to a new value y-SNAPSHOT (these values will also be prompted for)
  • Commit the modified POMs

Looking at this, you surely noticed that Maven will interact a lot with our SCM, so we will include the plugin Maven SCM Git Provider Git Executable Impl. since we are using git.
You'll need to adapt and add this lines to your pom.xml:

<properties>
    ...
    <version.maven-release-plugin>2.5.3</version.maven-release-plugin>
    <version.maven-scm-provider-gitexe>1.11.2</version.maven-scm-provider-gitexe>
    ...
</properties>
...
<scm>
    <connection>scm:git:git://github.com/mlniang/spring-zabbix-api-client.git</connection>
    <developerConnection>scm:git:[email protected]:mlniang/spring-zabbix-api-client.git</developerConnection>
    <url>https://github.com/mlniang/spring-zabbix-api-client</url>
    <tag>HEAD</tag>
</scm>
...
<build>
    <plugins>
        ...
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-release-plugin</artifactId>
            <version>${version.maven-release-plugin}</version>
            <configuration>
                <localCheckout>true</localCheckout>
                <pushChanges>false</pushChanges>
                <mavenExecutorId>forked-path</mavenExecutorId>
                <!--arguments>-Dgpg.passphrase=${gpg.passphrase}</arguments-->
            </configuration>
            <dependencies>
                <dependency>
                    <groupId>org.apache.maven.scm</groupId>
                    <artifactId>maven-scm-api</artifactId>
                    <version>${version.maven-scm-provider-gitexe}</version>
                </dependency>
                <dependency>
                    <groupId>org.apache.maven.scm</groupId>
                    <artifactId>maven-scm-provider-gitexe</artifactId>
                    <version>${version.maven-scm-provider-gitexe}</version>
                </dependency>
            </dependencies>
        </plugin>
        ...
    </plugins>
</build>

Dont worry about the commented line, it will become useful in the last step. Now you can run mvn clean release:prepare to check if everything is in order.

Deploying it

Once you've prepared your release, it's time to sign it and deploy it. Yes, you just read sign it and if you wonder why you need that, read this.

Getting access to Maven Central

Currently, you don't have rights to push your artifact to Maven Central. To get them, we will open an issue to Sonatype Issues and let them configure us an access. Create an account here if you don't have one yet and create an issue for new project hosting using this model.

Project-Hosting-Request

The support will give you as soon as possible instructions you should follow in the comments. While you're waiting for them, let's start generating the GPG key for the signature.

GPG Key generation

If you already have a key linked to the mail you used for the Github account, you can skip this part.
Download GnuPG for you OS and if it comes with a GUI, feel free to use it to create a key and upload it to a public key server. If you prefer using the command line, here are some commands to help you:

$ gpg --version # Verify that GPG is installed and in your PATH
$ gpg --full-gen-key # Generate a key pair, follow the instructions.

During the generation of the key pair, it will prompt you for a passphrase, input one (strongly recommended) and remember it.
Let's go inform Maven now about where and how to deploy it to Maven Central.

Maven Configuration

As you may know, we will add a distributionManagement section in our pom that points to our distribution servers. The distribution servers will be the ones provided by Sonatype and they will need our Sonatype account credentials for access. Like we should not, in any way, put credentials in a pom, we will add to the servers section of our Maven settings.xml:

<servers>
    ...
    <server>
        <id>ossrh</id>
        <username>sonatype_username</username>
        <password>sonatype_password</password>
    </server>
    ...
</servers>

And in the pom:

<distributionManagement>
    <snapshotRepository>
        <id>ossrh</id>
        <url>https://oss.sonatype.org/content/repositories/snapshots</url>
    </snapshotRepository>
    <repository>
        <id>ossrh</id>
        <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
    </repository>
</distributionManagement>

We will add too a dependency for the Nexus staging plugin (you can read more about the Nexus staging plugin here) and a profile for automatically signing the artifact during the release.

<properties>
    ...
    <version.nexus-staging-maven-plugin>1.6.8</version.nexus-staging-maven-plugin>
    <version.maven-gpg-plugin>1.6</version.maven-gpg-plugin>
    ...
</properties>
...
<build>
    <plugins>
        ...
        <plugin>
            <groupId>org.sonatype.plugins</groupId>
            <artifactId>nexus-staging-maven-plugin</artifactId>
            <version>${version.nexus-staging-maven-plugin}</version>
            <extensions>true</extensions>
            <configuration>
                <serverId>ossrh</serverId>
                <nexusUrl>https://oss.sonatype.org/</nexusUrl>
                <autoReleaseAfterClose>true</autoReleaseAfterClose>
            </configuration>
        </plugin>
    </plugins>
</build>
...
<profiles>
    ...
    <!-- GPG Signature on release -->
    <profile>
        <id>release-sign-artifacts</id>
        <activation>
            <property>
                <name>performRelease</name>
                <value>true</value>
            </property>
        </activation>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-gpg-plugin</artifactId>
                    <version>${version.maven-gpg-plugin}</version>
                    <executions>
                        <execution>
                            <id>sign-artifacts</id>
                            <phase>verify</phase>
                            <goals>
                                <goal>sign</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

One last thing with the Maven configuration! Remember our commented line in the pom, go and uncomment it. You'll notice that it's about the passphrase of our GPG key and it should be stored in a property called gpg.passphrase. Like we can't put passphrases in the pom, we will add a global profile active by default in our settings.xml that will automatically set this property:

<profiles>
    ...
    <profile>
        <id>ossrh</id>
        <activation>
            <activeByDefault>true</activeByDefault>
        </activation>
        <properties>
            <gpg.passphrase>your_pgp_passphrase</gpg.passphrase>
        </properties>
    </profile>
</profiles>

💡 At this moment, in Windows, GPG installation doesn't add automatically it's bin directory to the PATH environment variable. So, the release may fail complaining about not finding the executable gpg.exe. Just add GPG bin directory to your PATH or in your settings.xml, add before the property gpg.passphrase a property specifying the gpg executable location:

<gpg.executable>C:\Program Files (x86)\GnuPG\bin\gpg.exe</gpg.executable>

Finalizing

After the support team of Sonatype informed you can promote your first release, do a mvn release:perform before pushing to your repo the created tag and the modified code. Once done, be nice and say thanks to the Sonatype support in a comment and close the ticket.

A full example of a repository with an artifact in Maven Central is available at sprng-zabbix-api-client.
Welcome to the world of "known" Java developers and have fun 😎.