Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Introduction

In this tutorial, we’ll discuss Java’s JEP 418, which establishes a new Service Provider Interface (SPI) for Internet host and address resolution.

2. Internet Address Resolution

Any device connected to a computer network is assigned a numeric value or an IP (Internet Protocol) Address. IP addresses help identify the device on a network uniquely, and they also help in routing packets to and from the devices.

They are typically of two types. IPv4 is the fourth generation of the IP standard and is a 32-bit address. Due to the rapid growth of the Internet, a newer v6 of the IP standard has also been published, which is much larger and contains hexadecimal characters.

Additionally, there is another relevant type of address. Network devices, such as an Ethernet port or a Network Interface Card (NIC), have the MAC (Media Access Control) address. These are globally distributed, and all network interface devices can be uniquely identified with MAC addresses.

Internet Address Resolution broadly refers to converting a higher-level network address, such as a domain (e.g., baeldung) or a URL (https://www.baeldung.com), to a lower-level network address, such as an IP address or a MAC address.

3. Internet Address Resolution in Java

Java provides multiple ways to resolve internet addresses today using the java.net.InetAddress API. The API internally uses the operating system’s native resolver for DNS lookup.

The OS native address resolution, which the InetAddress API currently uses, involves multiple steps. A system-level DNS cache is involved, where commonly queried DNS mappings are stored. If a cache miss occurs in the local DNS cache, the system resolver configuration provides information about a DNS server to perform subsequent lookups.

The OS then queries the configured DNS server obtained in the previous step for the information. This step might happen a couple of more times recursively.

If a match and lookup is successful, the DNS address is cached at all the servers and returned to the original client. However, if there is no match, an iterative lookup process is triggered to a Root Server, providing information about the Authoritative Nave Servers (ANS). These Authoritative Name Servers (ANS) store information about Top Level Domains (TLD) such as the .org, .com, etc.

These steps ultimately match the domain to an Internet address if it is valid or returns failure to the client.

4. Using Java’s InetAddress API

The InetAddress API provides numerous methods for performing DNS query and resolution. These APIs are available as part of the java.net package.

4.1. getAllByName() API

The getAllByName() API tries to map a hostname to a set of IP addresses:

InetAddress[] inetAddresses =  InetAddress.getAllByName(host);
Assert.assertTrue(Arrays.stream(inetAddresses).map(InetAddress::getHostAddress).toArray(String[]::new) > 1);

This is also termed forward lookup.

4.2. getByName() API

The getByName() API is similar to the previous forward lookup API except that it only maps the host to the first matching IP Address:

InetAddress inetAddress = InetAddress.getByName("www.google.com");
Assert.assertNotNull(inetAddress.getHostAddress()); // returns an IP Address

4.3. getByAddress() API

This is the most basic API to perform a reverse lookup, wherein it takes an IP address as its input and tries to return a host associated with it:

InetAddress inetAddress =  InetAddress.getByAddress(ip);
Assert.assertNotNull(inetAddress.getHostName()); // returns a host (eg. google.com)

4.4. getCanonicalHostName() API and getHostName() API

These APIs perform a similar reverse lookup and try to return the Fully Qualified Domain Name(FQDN) associated with it:

InetAddress inetAddress = InetAddress.getByAddress(ip); 
Assert.assertNotNull(inetAddress.getCanonicalHostName()); // returns a FQDN
Assert.assertNotNull(inetAddress.getHostName());

5. Service Provider Interface (SPI)

The Service Provider Interface (SPI) pattern is an important design pattern used in software development. The purpose of this pattern is to allow for pluggable components and implementations for a specific service.

It allows developers to extend the capabilities of a system without modifying any of the core expectations of the service and use any of the implementations without being tied down to a single one.

5.1. SPI Components in InetAddress

Following the SPI design pattern, this JEP proposes a way to substitute the default system resolver with a custom one. The SPI is available from Java 18 onwards. A service locator is necessary to locate the provider to use. If the service locator fails to identify any provider service, it returns to the default implementation.

As with any SPI implementation, there are four main components:

  1. A Service is the first component, a collection of interfaces and classes that serve a specific functionality. In our case, we are dealing with the Internet Address Resolution as the Service
  2. A Service Provider Interface is an interface or an abstract class that serves as a proxy for the service. This interface delegates all operations it defines to its implementations. The InetAddressResolver Interface is the Service Provider Interface for our use case, and it defines operations for looking up host names and IP Addresses for resolution
  3. The third component is a Service Provider, which defines a concrete implementation of the Service Provider Interface. The InetAddressResolverProvider is an abstract class whose purpose is to serve as a factory for the many custom implementations of resolvers. We’ll define our implementation by extending this abstract class. The JVM maintains a single system-wide resolver, which is then used by InetAddress and is typically set during VM initialization
  4. The Service Loader component, the final component, ties all this together. The ServiceLoader mechanism will locate an eligible InetAddressResolverProvider provider implementation and set it as the default system-wide resolver. A fallback mechanism sets the default resolver system-wide if there is a failure

5.2. Custom Implementation of InetAddressResolverProvider

The changes put forth through this SPI are available in the java.net.spi package, and the following classes are newly added:

  • InetAddressResolverProvider
  • InetAddressResolver
  • InetAddressResolver.LookupPolicy
  • InetAddressResolverProvider.Configuration

In this section, we’ll try to write a custom resolver implementation for InetAddressResolver to substitute for the system default resolver. Before we write our custom resolver, we can define a small utility class that will load the registry of address mappings from a file onto memory (or cache).

Based on the registry entries, our custom address resolver will be able to resolve address hosts to IPs and vice versa.

First, we define our class CustomAddressResolverImpl by extending from the abstract class InetAddressResolverProvider. Doing this requires us to immediately provide implementations of two methods: get(Configuration configuration) and name(). 

We can use the name() to return the name of the current implementation class or any other relevant identifier:

@Override
public String name() {
    return "CustomInternetAddressResolverImpl";
}

Let’s now implement the get() method. The get() method returns an instance of the InetAddressResolver class, which we can define inline or separately. We’ll define it inline for simplicity.

An InetAddressResolver interface has two methods:

  • Stream<InetAddress> lookupByName(String host, LookupPolicy lookupPolicy) throws UnknownHostException
  • String lookupByAddress(byte[] addr) throws UnknownHostException

We can write any custom logic to map a host to its IP Address (in the form of InetAddress) and vice versa. In this example, we’ll let our Registry functionality take care of the same:

@Override
public InetAddressResolver get(Configuration configuration) {
    LOGGER.info("Using Custom Address Resolver :: " + this.name());
    LOGGER.info("Registry initialised");
    return new InetAddressResolver() {
        @Override
        public Stream<InetAddress> lookupByName(String host, LookupPolicy lookupPolicy) throws UnknownHostException {
            return registry.getAddressesfromHost(host);
        }

        @Override
        public String lookupByAddress(byte[] addr) throws UnknownHostException {
            return registry.getHostFromAddress(addr);
        }
    };
}

5.3. Registry Class Implementation

For this article, we’ll use a HashMap to store a list of IP Addresses and hostnames in memory. We can load the list from a file on the system as well.

The Map is of type Map<String, List<byte[]>> where the host names are stored as the keys and the IP address are stored as a List of byte[]. This data structure allows multiple IPs to be mapped against a single host. We can perform forward and backward lookups using this Map.

Forward Lookup, in this case, is when we pass the hostname as a parameter and expect to resolve it against its IP Address, for example, when we type www.baeldung.com:

public Stream<InetAddress> getAddressesfromHost(String host) throws UnknownHostException {
    LOGGER.info("Performing Forward Lookup for HOST : " + host);
    if (!registry.containsKey(host)) {
        throw new UnknownHostException("Missing Host information in Resolver");
    }
    return registry.get(host)
      .stream()
      .map(add -> constructInetAddress(host, add))
      .filter(Objects::nonNull);
}

We should note that the response is a Stream of InetAddress to accommodate multiple IPs.

An example of reverse lookup is when we want to know the hostname associated with an IP Address:

public String getHostFromAddress(byte[] arr) throws UnknownHostException {
    LOGGER.info("Performing Reverse Lookup for Address : " + Arrays.toString(arr));
    for (Map.Entry<String, List<byte[]>> entry : registry.entrySet()) {
        if (entry.getValue()
          .stream()
          .anyMatch(ba -> Arrays.equals(ba, arr))) {
            return entry.getKey();
        }
    }
    throw new UnknownHostException("Address Not Found");
}

Finally, the ServiceLoader module loads our custom implementation for InetAddress resolution.

To discover our Service Provider, we create a configuration under the resources/META-INF/services hierarchy named java.net.spi.InetAddressResolverProvider. The configuration file should maintain our provider’s fully qualified path as com.baeldung.inetspi.providers.CustomAddressResolverImpl.java. 

This tells the JVM to load the corresponding implementation of the provider following the SPI pattern.

6. Alternative Solutions

We have a few workarounds in case we don’t wish to add a custom implementation for address resolution:

  • Using JNDI and its DNS providers is an alternative to using InetAddress for resolution; however, we cannot leverage the rich APIs that InetAddress provides for easier access
  • We can use the OS’ native resolver via JNI of the Project Panama
  • Finally, we can directly modify JDK system property files, such as the jdk.net.hosts.file, to inform InetAddress to use a specific file for host matching. However, it is difficult to maintain an exhaustive list.

7. Conclusion

In this article, we examined how Internet Address Resolution works in Java using the InetAddress API. We also examined JEP 418 for the Service Provider Interface (SPI) for Internet host and address resolution. We implemented a custom Provider for Internet Address resolution and discussed some alternatives.

As always, the code for the examples is available over on GitHub.

Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – REST with Spring (eBook) (everywhere)
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.