This code shows a Java REST client using Jersey with Jakarta RESTful web services.

The RESTful server is not shown here - it could be anything which:

  • generates JSON
  • uses a self-signed certificate

There are plenty of examples of this around, but several of them are out-of-date and others do not handle a self-signed certificate. The significance of the self-signed certificate is this: For normal (legitimate) certificates issued by certificate authorities, the CA root certificate will (should) already exist in Java’s cacerts file, as part of the Java installation. You don’t need to handle the certificate explicitly in these cases.

That’s obviously not going to be true for your self-signed certificate - so you need to provide that to the REST client, instead of relying on Java’s cacerts file.

Danger!

This is all fine for non-production, private purposes.

Don’t use self-signed certificates in production.

Additionally, I needed to do the following:

  • generate more detailed debugging information
  • use a JSON deserializer (Jackson, in this case)

In this case, the self-signed certificate is stored in a Java keystore (JKS) file - although it could be handled as a PEM certificate (not shown here).

The Maven dependencies:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<dependencies>
    <dependency>
        <groupId>jakarta.ws.rs</groupId>
        <artifactId>jakarta.ws.rs-api</artifactId>
        <version>3.1.0</version>
    </dependency>
    <dependency>
        <groupId>org.glassfish.jersey.core</groupId>
        <artifactId>jersey-client</artifactId>
        <version>3.1.0</version>
    </dependency>
    <dependency>
        <groupId>org.glassfish.jersey.inject</groupId>
        <artifactId>jersey-hk2</artifactId>
        <version>3.1.0</version>
    </dependency>
    <dependency>
        <groupId>org.glassfish.jersey.media</groupId>
        <artifactId>jersey-media-json-jackson</artifactId>
        <version>3.1.0</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.14.1</version>
    </dependency>
</dependencies>

The Jersey client:

This assumes that the JSON can be deserialized into a Java class called MyPojo, with a field called foo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.core.Feature;
import jakarta.ws.rs.core.MediaType;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.glassfish.jersey.logging.LoggingFeature;

public class MyJerseyClientConnector {

    static final String KEYSTORE_FILE = "your_keystore.jks";
    static final String KEYSTORE_PASS = "the_password";

    static final Logger LOGGER = Logger.getLogger(MyJerseyClientConnector.class.getName());

    static final Feature debugger = new LoggingFeature(
            Logger.getLogger(App.class.getName()),
            Level.INFO,
            LoggingFeature.Verbosity.PAYLOAD_ANY,
            8192);

    public void doRestCall(String url) throws IOException {
        MyPojo pojo = ClientBuilder.newBuilder()
                .trustStore(loadKeyStore()) // Self-signed cert
                .register(debugger)
                .register(new MyObjectMapperProvider())
                .build() // the Client
                .target(path).request()
                .accept(MediaType.APPLICATION_JSON)
                .get() // the Response
                .readEntity(MyPojo.class);

        System.out.println(pojo.getFoo());
    }

    private KeyStore loadKeyStore() {
        KeyStore keyStore = null;
        try (InputStream inputStream = new FileInputStream(KEYSTORE_FILE)) {
            keyStore = KeyStore.getInstance("JKS");
            keyStore.load(inputStream, KEYSTORE_PASS.toCharArray());
        } catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException ex) {
            LOGGER.log(Level.SEVERE, "Error during keystore load.", ex);
        }
        return keyStore;
    }
}

The integration with Jackson uses a custom object mapper class MyObjectMapperProvider defined as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.ext.ContextResolver;
import jakarta.ws.rs.ext.Provider;

@Provider
@Produces(MediaType.APPLICATION_JSON)
public class MyObjectMapperProvider implements ContextResolver<ObjectMapper> {

    @Override
    public ObjectMapper getContext(Class<?> objectType) {
        final ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        return objectMapper;
    }

}

In my case, I chose to customize this by adding the Jackson FAIL_ON_UNKNOWN_PROPERTIES = false setting. There are other ways of doing this.

Some general points:

  1. The debugger logging feature is purely optional. It shows the basic request/response exchange - plus any error messages, which may otherwise not be captured.

  2. The keystore data is loaded using the .trustStore(...) method - not the keyStore(...) method. This is telling the client to treat the self-signed cert as trusted - as if it were part of the Java cacerts file.

  3. the doRestCall method rolls all the processing into one sequence of “fluent” methods - but there is certainly no need to do that. It’s just done here as a demo.

Danger!

This is all fine for non-production, private purposes.

Don’t use self-signed certificates in production.