How to Create a Java library: From Scratch to Maven Central
Introduction
If you usually need to rewrite or copy similar code between different projects, it may be time to stop replicating it and create a library.
You can also share it as open-source so other people can use it and help you improve it.
To use your library in different projects, you have to publish it on a repository like Maven Central Repository.
So let's run through the entire process and publish a library for padding Strings. We'll start by creating our project from scratch.
NOTE
If you want to skip the project creation:
- You can use your own project and jump ahead to Preparing pom.xml to Deploy on Maven Central; or
-
Download my project from GitHub and jump ahead to Requesting Access to Maven Central.
β οΈ IF YOU USE MY PROJECT, DON'T FORGET TO CHANGE ITS GROUP ID.
Creating the Project
Run the following command on your terminal:
mvn archetype:generate -DgroupId=com.thegreatapi.demolibrary -DartifactId=demolibrary -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4 -DinteractiveMode=false
NOTE
Use your own group ID on the command. If you use com.thegreat.api.demolibrary
you won't be able to publish to Maven Central.
If you're not sure about what group ID to use, look at this article.
That command will create a project with the following pom.xml
:
<?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>com.thegreatapi.demolibrary</groupId>
<artifactId>demolibrary</artifactId>
<version>1.0-SNAPSHOT</version>
<name>demolibrary</name>
<!-- FIXME change it to the project's website -->
<url>http://www.example.com</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
<plugins>
<!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.1</version>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
<!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->
<plugin>
<artifactId>maven-site-plugin</artifactId>
<version>3.7.1</version>
</plugin>
<plugin>
<artifactId>maven-project-info-reports-plugin</artifactId>
<version>3.0.0</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
Let's change it to use Java 11 instead of 1.7.
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
And then we create a LICENSE
file. I'll use Apache 2 License, but you can use any license you want. To use Apache 2 License, you have to copy the content from http://www.apache.org/licenses/LICENSE-2.0.txt and paste it into your LICENSE
file.
Implementing the Library
Now let's create an interface StringPadder
in the package com.thegreatapi.demolibrary.stringpadder
. That will be the interface that clients will use to pad Strings.
package com.thegreatapi.demolibrary.stringpadder;
/**
* Pads a {@link String}.
* <p>
* The instances of classes that implement this interface are thread-safe and immutable.
*/
public interface StringPadder {
/**
* Returns a new {@link String} that right-aligns the characters in the specified String by padding them with spaces
* on the left, for a specified total length.
*
* @param stringToPad the {@link String} to be padded
* @param totalLength total length of the new {@link String}
* @return the padded {@link String}
*/
String padLeft(String stringToPad, int totalLength);
/**
* Returns a new {@link String} that right-aligns the characters in the specified String by padding them with the
* specified char on the left, for a specified total length.
*
* @param stringToPad the {@link String} to be padded
* @param totalLength total length of the new {@link String}
* @return the padded {@link String}
*/
String padLeft(String stringToPad, int totalLength, char paddingCharacter);
/**
* Returns a new {@link String} that left-aligns the characters in the specified String by padding them with spaces
* on the left, for a specified total length.
*
* @param stringToPad the {@link String} to be padded
* @param totalLength total length of the new {@link String}
* @return the padded {@link String}
*/
String padRight(String stringToPad, int totalLength);
/**
* Returns a new {@link String} that left-aligns the characters in the specified String by padding them with the
* specified char on the left, for a specified total length.
*
* @param stringToPad the {@link String} to be padded
* @param totalLength total length of the new {@link String}
* @return the padded {@link String}
*/
String padRight(String stringToPad, int totalLength, char paddingCharacter);
}
Now let's create the implementation of the StringPadder
interface.
package com.thegreatapi.demolibrary.stringpadder;
class StringPadderImpl implements StringPadder {
StringPadderImpl() {
}
@Override
public String padLeft(String stringToPad, int totalLength) {
return padLeft(stringToPad, totalLength, ' ');
}
@Override
public String padLeft(String stringToPad, int totalLength, char paddingCharacter) {
return getStringToBeAdded(stringToPad, totalLength, paddingCharacter) + stringToPad;
}
@Override
public String padRight(String stringToPad, int totalLength) {
return padRight(stringToPad, totalLength, ' ');
}
@Override
public String padRight(String stringToPad, int totalLength, char paddingCharacter) {
return stringToPad + getStringToBeAdded(stringToPad, totalLength, paddingCharacter);
}
private String getStringToBeAdded(String stringToPad, int totalLength, char paddingCharacter) {
int quantity = totalLength - stringToPad.length();
if (quantity > 0) {
return Character.toString(paddingCharacter).repeat(quantity);
} else {
return "";
}
}
}
Note that the class is package-private, so the clients can't use it in their code.
Now we're going to create a factory for clients to create instances of StringPadder
.
package com.thegreatapi.demolibrary.stringpadder;
/**
* Factory for creating instances of {@link StringPadder}.
*/
public final class StringPadderFactory {
private StringPadderFactory() {
}
/**
* Creates an instance of {@link StringPadder}.
*
* @return the new instance
*/
public static StringPadder createStringPadder() {
return new StringPadderImpl();
}
}
Creating Tests
Let's replace JUnit 4 with JUnit 5 and add AssertJ dependency to our pom.xml
.
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.7.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.19.0</version>
<scope>test</scope>
</dependency>
</dependencies>
Now we're ready to implement our tests.
package com.thegreatapi.demolibrary.stringpadder;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class StringPadderImplTest {
private final StringPadderImpl stringPadder = new StringPadderImpl();
@Test
void padLeft() {
assertThat(stringPadder.padLeft("thegreatapi.com", 20))
.isEqualTo(" thegreatapi.com");
}
@Test
void padLeftWithZeros() {
assertThat(stringPadder.padLeft("thegreatapi.com", 20, '0'))
.isEqualTo("00000thegreatapi.com");
}
@Test
void padRight() {
assertThat(stringPadder.padRight("thegreatapi.com", 20))
.isEqualTo("thegreatapi.com ");
}
@Test
void padRightWithZeros() {
assertThat(stringPadder.padRight("thegreatapi.com", 20, '0'))
.isEqualTo("thegreatapi.com00000");
}
@Test
void padLeftWithInvalidTotalLength() {
assertThat(stringPadder.padLeft("thegreatapi.com", 3))
.isEqualTo("thegreatapi.com");
}
@Test
void padLeftWithZerosInvalidTotalLength() {
assertThat(stringPadder.padLeft("thegreatapi.com", 3, '0'))
.isEqualTo("thegreatapi.com");
}
@Test
void padRightInvalidTotalLength() {
assertThat(stringPadder.padRight("thegreatapi.com", 3))
.isEqualTo("thegreatapi.com");
}
@Test
void padRightWithZerosInvalidTotalLength() {
assertThat(stringPadder.padRight("thegreatapi.com", 3, '0'))
.isEqualTo("thegreatapi.com");
}
}
If we run mvn verify
, we should see an output similar to the following:
/Library/Java/JavaVirtualMachines/adoptopenjdk-11.jdk/Contents/Home/bin/java -Dmaven.multiModuleProjectDirectory=/Users/helber/Desktop/demolibrary -Dmaven.home=/Applications/IntelliJ IDEA.app/Contents/plugins/maven/lib/maven3 -Dclassworlds.conf=/Applications/IntelliJ IDEA.app/Contents/plugins/maven/lib/maven3/bin/m2.conf -Dmaven.ext.class.path=/Applications/IntelliJ IDEA.app/Contents/plugins/maven/lib/maven-event-listener.jar -javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=61185:/Applications/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Applications/IntelliJ IDEA.app/Contents/plugins/maven/lib/maven3/boot/plexus-classworlds.license:/Applications/IntelliJ IDEA.app/Contents/plugins/maven/lib/maven3/boot/plexus-classworlds-2.6.0.jar org.codehaus.classworlds.Launcher -Didea.version=2020.3.3 verify
[INFO] Scanning for projects...
[INFO]
[INFO] --------------< com.thegreatapi.demolibrary:demolibrary >---------------
[INFO] Building demolibrary 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-resources-plugin:3.0.2:resources (default-resources) @ demolibrary ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /Users/helber/Desktop/demolibrary/src/main/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.8.0:compile (default-compile) @ demolibrary ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 2 source files to /Users/helber/Desktop/demolibrary/target/classes
[INFO]
[INFO] --- maven-resources-plugin:3.0.2:testResources (default-testResources) @ demolibrary ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /Users/helber/Desktop/demolibrary/src/test/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.8.0:testCompile (default-testCompile) @ demolibrary ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to /Users/helber/Desktop/demolibrary/target/test-classes
[INFO]
[INFO] --- maven-surefire-plugin:2.22.1:test (default-test) @ demolibrary ---
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.thegreatapi.demolibrary.stringpadder.StringPadderImplTest
[INFO] Tests run: 8, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.07 s - in com.thegreatapi.demolibrary.stringpadder.StringPadderImplTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 8, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO]
[INFO] --- maven-jar-plugin:3.0.2:jar (default-jar) @ demolibrary ---
[INFO] Building jar: /Users/helber/Desktop/demolibrary/target/demolibrary-1.0-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.378 s
[INFO] Finished at: 2021-04-05T06:36:12-03:00
[INFO] ------------------------------------------------------------------------
Process finished with exit code 0
Our implementation is ready. The next step is to deploy our library to Maven Central.
Preparing pom.xml to Deploy on Maven Central {#preparing-pom.xml-to-deploy-on-maven-central}
There are some requirements we need to satisfy in order to upload our library to Maven Central. We can find those requirements on https://central.sonatype.org/pages/requirements.html.
The first thing we have to change in our pom.xml
is to define a non-SNAPSHOT version for our library. To do that, we just need to remove the -SNAPSHOT
suffix. Let's define our version as 0.0.1
.
<version>0.0.1</version>
Next, we have to add a description and a URL to our project. In my case, the URL should be http://thegreatapi.com. Your URL must differ from mine because you have to own the domain. You can also use a GitHub URL if you prefer.
<description>
This project is a library for padding Strings in Java.
DON'T USE THIS IN PRODUCTION. IT WAS CREATED FOR DEMO PURPOSES ONLY.
</description>
<url>http://thegreatapi.com</url>
After that, we add the license information. Note that I'm using Apache 2 license. Use the same license that you defined in your LICENSE
file.
<licenses>
<license>
<name>The Apache License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
</license>
</licenses>
The next step is to add information about the developers.
<developers>
<developer>
<name>Helber Belmiro</name>
<email>your@email.com</email>
<organization>The Great API</organization>
<organizationUrl>https://thegreatapi.com</organizationUrl>
</developer>
</developers>
And then, add the information about the Source Code Management (SCM). The following information is for my project. Replace it with your project's information.
<scm>
<connection>scm:git:git@github.com:hbelmiro/demo-library.git</connection>
<developerConnection>scm:git:ssh://github.com:hbelmiro/demo-library.git</developerConnection>
<url>https://github.com/hbelmiro/demo-library/tree/master</url>
</scm>
Maven Central demands you to send the Javadoc and source code. So you have to create it when building your artifacts. You must also sign the artifacts you are sending to Maven Central.
Since we need to do it only when deploying to Maven Central, it might be a good idea to create a profile for this.
<profiles>
<profile>
<id>release</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>1.6</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
<configuration>
<javadocExecutable>${java.home}/bin/javadoc</javadocExecutable>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
You also have to install a GPG client and put it on your command line path. Follow the instructions on https://central.sonatype.org/pages/working-with-pgp-signatures.html to install the GPG client.
The next step is to add the Distribution Management to your pom.xml
.
<distributionManagement>
<snapshotRepository>
<id>ossrh</id>
<url>https://s01.oss.sonatype.org/content/repositories/snapshots</url>
</snapshotRepository>
<repository>
<id>ossrh</id>
<url>https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/</url>
</repository>
</distributionManagement>
Also, add nexus-staging-maven-plugin.
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<version>1.6.7</version>
<extensions>true</extensions>
<configuration>
<serverId>ossrh</serverId>
<nexusUrl>https://s01.oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>true</autoReleaseAfterClose>
</configuration>
</plugin>
...
</plugins>
</pluginManagement>
<build>
Your library is ready to be published to Maven Central.
Requesting Access to Maven Central
Using the OSS Repository Hosting (OSSRH), provided by Sonatype for any open-source project, is the easiest way to publish your project.
The first step is to create a JIRA account.
After that, you have to create a New Project ticket. As an example, you can use the ticket I opened to publish demolibrary.
Officially, they ask you for 2 working days to complete the process, but it normally takes one or two hours.
NOTE
Use your own group ID. If you use com.thegreat.api.demolibrary
you won't be able to publish to Maven Central.
They will ask if you own the domain specified on groupId and if so, you'll have to verify the ownership. On my ticket they commented:
In my case, I added a TXT record to my DNS. When I did that, I commented on the ticket, and they set the ticket to "Resolved".
Releasing the Library
After proving the domain ownership, you're ready to upload your artifacts. To do that, you first release it to staging.
mvn clean deploy -P release
At this point, your artifacts are stored in a private repository, so you can verify them before releasing them. So log in to https://s01.oss.sonatype.org/ using your JIRA credentials.
On the left menu, click on "Staging Repositories" and you'll see your library.
If everything is OK with your library, you can close it, otherwise, you can drop it.
When you close it, on the low section, you can check if it was closed successfully by clicking on the "Activity" tab. You should see something similar to this:
If the repository was successfully closed, now you're able to promote it to release.
Run the following command using your Repository ID. On the image above, it's comthegreatapidemolibrary-1005
. Don't forget to replace it with your own ID.
mvn nexus-staging:release -DstagingRepositoryId=comthegreatapidemolibrary-1005
NOTE
Use your own group ID. If you use com.thegreat.api.demolibrary
you won't be able to publish to Maven Central.
You should see an output similar to:
[INFO] * Connected to Nexus at https://s01.oss.sonatype.org:443/, is version 2.14.20-02 and edition "Professional"
[INFO] Releasing staging repository with IDs=[comthegreatapidemolibrary-1005]
Waiting for operation to complete...
.........
[INFO] Released
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 28.583 s
[INFO] Finished at: 2021-04-07T07:53:50-03:00
[INFO] ------------------------------------------------------------------------
Now your library is published. On this very first deployment, you have to comment on the JIRA ticket, so they can activate the sync to Maven Central.
After Sonatype activates Maven Central sync for your library, when you successfully release new components, they will be published to Central https://repo1.maven.org/maven2/, typically within 10 minutes, though updates to https://search.maven.org can take up to two hours.
Start Your Library Now
You are now ready to start your own library. It will be great for you and the Java community.
To help you with that, I wrote an article about the principles that sustain great frameworks and libraries.
If you still don't feel prepared, what's preventing you from taking the next step? Comment here or send me a message on my social media. I'll be happy to help you.
Top comments (0)