Repository: jchambers/pushy
Pull request #754 (2020-02-29 23:30:25)

Use JSR-310 date/time types
As a follow-on to #746, this moves everything (I think!) to the date/time data types introduced in Java 8 (JSR-310).


Patch:
diff --git a/pushy/src/main/java/com/eatthepath/pushy/apns/ApnsChannelFactory.java b/pushy/src/main/java/com/eatthepath/pushy/apns/ApnsChannelFactory.java
index ff6b9e7a..ab2dc06b 100644
--- a/pushy/src/main/java/com/eatthepath/pushy/apns/ApnsChannelFactory.java
+++ b/pushy/src/main/java/com/eatthepath/pushy/apns/ApnsChannelFactory.java
@@ -45,6 +45,7 @@ import io.netty.util.concurrent.PromiseNotifier;
 import java.io.Closeable;
 import java.net.InetSocketAddress;
 import java.net.SocketAddress;
+import java.time.Duration;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicLong;
@@ -70,9 +71,9 @@ class ApnsChannelFactory implements PooledObjectFactory<Channel>, Closeable {
     static final AttributeKey<Promise<Channel>> CHANNEL_READY_PROMISE_ATTRIBUTE_KEY =
             AttributeKey.valueOf(ApnsChannelFactory.class, "channelReadyPromise");
 
-    ApnsChannelFactory(final SslContext sslContext, final ApnsSigningKey signingKey, final long tokenExpirationMillis,
-                       final ProxyHandlerFactory proxyHandlerFactory, final int connectTimeoutMillis,
-                       final long idlePingIntervalMillis, final long gracefulShutdownTimeoutMillis,
+    ApnsChannelFactory(final SslContext sslContext, final ApnsSigningKey signingKey, final Duration tokenExpiration,
+                       final ProxyHandlerFactory proxyHandlerFactory, final Duration connectTimeout,
+                       final Duration idlePingInterval, final Duration gracefulShutdownTimeout,
                        final Http2FrameLogger frameLogger, final InetSocketAddress apnsServerAddress,
                        final EventLoopGroup eventLoopGroup) {
 
@@ -92,8 +93,8 @@ class ApnsChannelFactory implements PooledObjectFactory<Channel>, Closeable {
         this.bootstrapTemplate.remoteAddress(apnsServerAddress);
         this.bootstrapTemplate.resolver(this.addressResolverGroup);
 
-        if (connectTimeoutMillis > 0) {
-            this.bootstrapTemplate.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeoutMillis);
+        if (connectTimeout != null) {
+            this.bootstrapTemplate.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) connectTimeout.toMillis());
         }
 
         this.bootstrapTemplate.handler(new ChannelInitializer<SocketChannel>() {
@@ -111,13 +112,13 @@ class ApnsChannelFactory implements PooledObjectFactory<Channel>, Closeable {
                     if (signingKey != null) {
                         clientHandlerBuilder = new TokenAuthenticationApnsClientHandler.TokenAuthenticationApnsClientHandlerBuilder()
                                 .signingKey(signingKey)
-                                .tokenExpirationMillis(tokenExpirationMillis)
+                                .tokenExpiration(tokenExpiration)
                                 .authority(authority)
-                                .idlePingIntervalMillis(idlePingIntervalMillis);
+                                .idlePingInterval(idlePingInterval);
                     } else {
                         clientHandlerBuilder = new ApnsClientHandler.ApnsClientHandlerBuilder()
                                 .authority(authority)
-                                .idlePingIntervalMillis(idlePingIntervalMillis);
+                                .idlePingInterval(idlePingInterval);
                     }
 
                     if (frameLogger != null) {
@@ -126,8 +127,8 @@ class ApnsChannelFactory implements PooledObjectFactory<Channel>, Closeable {
 
                     apnsClientHandler = clientHandlerBuilder.build();
 
-                    if (gracefulShutdownTimeoutMillis > 0) {
-                        apnsClientHandler.gracefulShutdownTimeoutMillis(gracefulShutdownTimeoutMillis);
+                    if (gracefulShutdownTimeout != null) {
+                        apnsClientHandler.gracefulShutdownTimeoutMillis(gracefulShutdownTimeout.toMillis());
                     }
                 }
 
@@ -139,7 +140,7 @@ class ApnsChannelFactory implements PooledObjectFactory<Channel>, Closeable {
 
                 pipeline.addLast(sslHandler);
                 pipeline.addLast(new FlushConsolidationHandler(FlushConsolidationHandler.DEFAULT_EXPLICIT_FLUSH_AFTER_FLUSHES, true));
-                pipeline.addLast(new IdleStateHandler(idlePingIntervalMillis, 0, 0, TimeUnit.MILLISECONDS));
+                pipeline.addLast(new IdleStateHandler(idlePingInterval.toMillis(), 0, 0, TimeUnit.MILLISECONDS));
                 pipeline.addLast(apnsClientHandler);
             }
         });
diff --git a/pushy/src/main/java/com/eatthepath/pushy/apns/ApnsClient.java b/pushy/src/main/java/com/eatthepath/pushy/apns/ApnsClient.java
index 1a046511..5ce9d469 100644
--- a/pushy/src/main/java/com/eatthepath/pushy/apns/ApnsClient.java
+++ b/pushy/src/main/java/com/eatthepath/pushy/apns/ApnsClient.java
@@ -37,6 +37,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.net.InetSocketAddress;
+import java.time.Duration;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicLong;
 
@@ -130,9 +131,9 @@ public class ApnsClient {
     }
 
     protected ApnsClient(final InetSocketAddress apnsServerAddress, final SslContext sslContext,
-                         final ApnsSigningKey signingKey, final long tokenExpirationMillis,
-                         final ProxyHandlerFactory proxyHandlerFactory,  final int connectTimeoutMillis,
-                         final long idlePingIntervalMillis, final long gracefulShutdownTimeoutMillis,
+                         final ApnsSigningKey signingKey, final Duration tokenExpiration,
+                         final ProxyHandlerFactory proxyHandlerFactory,  final Duration connectTimeout,
+                         final Duration idlePingInterval, final Duration gracefulShutdownTimeout,
                          final int concurrentConnections,  final ApnsClientMetricsListener metricsListener,
                          final Http2FrameLogger frameLogger, final EventLoopGroup eventLoopGroup) {
 
@@ -146,8 +147,8 @@ public class ApnsClient {
 
         this.metricsListener = metricsListener != null ? metricsListener : new NoopApnsClientMetricsListener();
 
-        final ApnsChannelFactory channelFactory = new ApnsChannelFactory(sslContext, signingKey, tokenExpirationMillis,
-                proxyHandlerFactory,  connectTimeoutMillis, idlePingIntervalMillis, gracefulShutdownTimeoutMillis,
+        final ApnsChannelFactory channelFactory = new ApnsChannelFactory(sslContext, signingKey, tokenExpiration,
+                proxyHandlerFactory, connectTimeout, idlePingInterval, gracefulShutdownTimeout,
                 frameLogger, apnsServerAddress, this.eventLoopGroup);
 
         final ApnsChannelPoolMetricsListener channelPoolMetricsListener = new ApnsChannelPoolMetricsListener() {
diff --git a/pushy/src/main/java/com/eatthepath/pushy/apns/ApnsClientBuilder.java b/pushy/src/main/java/com/eatthepath/pushy/apns/ApnsClientBuilder.java
index e47de61b..b7e06ef9 100644
--- a/pushy/src/main/java/com/eatthepath/pushy/apns/ApnsClientBuilder.java
+++ b/pushy/src/main/java/com/eatthepath/pushy/apns/ApnsClientBuilder.java
@@ -43,7 +43,7 @@ import java.security.KeyStoreException;
 import java.security.PrivateKey;
 import java.security.cert.Certificate;
 import java.security.cert.X509Certificate;
-import java.util.concurrent.TimeUnit;
+import java.time.Duration;
 
 /**
  * An {@code ApnsClientBuilder} constructs new {@link ApnsClient} instances. Callers must specify the APNs server to
@@ -62,7 +62,7 @@ public class ApnsClientBuilder {
     private String privateKeyPassword;
 
     private ApnsSigningKey signingKey;
-    private long tokenExpirationMillis = TimeUnit.MINUTES.toMillis(50);
+    private Duration tokenExpiration = Duration.ofMinutes(50);
 
     private File trustedServerCertificatePemFile;
     private InputStream trustedServerCertificateInputStream;
@@ -76,9 +76,9 @@ public class ApnsClientBuilder {
 
     private ProxyHandlerFactory proxyHandlerFactory;
 
-    private int connectionTimeoutMillis;
-    private long idlePingIntervalMillis = DEFAULT_PING_IDLE_TIME_MILLIS;
-    private long gracefulShutdownTimeoutMillis;
+    private Duration connectionTimeout;
+    private Duration idlePingInterval = DEFAULT_IDLE_PING_INTERVAL;
+    private Duration gracefulShutdownTimeout;
 
     private Http2FrameLogger frameLogger;
 
@@ -87,7 +87,7 @@ public class ApnsClientBuilder {
      *
      * @since 0.11
      */
-    public static final int DEFAULT_PING_IDLE_TIME_MILLIS = 60_000;
+    public static final Duration DEFAULT_IDLE_PING_INTERVAL = Duration.ofMinutes(1);
 
     /**
      * The hostname for the production APNs gateway.
@@ -289,15 +289,14 @@ public class ApnsClientBuilder {
      * expiration duration for clients using token-based authentication is 50 minutes. Callers <em>should not</em> set a
      * non-default value unless the upstream behavior changes.</p>
      *
-     * @param duration the magnitude of the expiration duration in the given time units
-     * @param timeUnit the time units in which the given {@code duration} are measured
+     * @param tokenExpiration the duration after which authentication tokens should expire
      *
      * @return a reference to this builder
      *
      * @since 0.13.11
      */
-    public ApnsClientBuilder setTokenExpiration(final long duration, final TimeUnit timeUnit) {
-        this.tokenExpirationMillis = timeUnit.toMillis(duration);
+    public ApnsClientBuilder setTokenExpiration(final Duration tokenExpiration) {
+        this.tokenExpiration = tokenExpiration;
         return this;
     }
 
@@ -442,32 +441,30 @@ public class ApnsClientBuilder {
      * Sets the maximum amount of time, in milliseconds, that the client under construction will wait to establish a
      * connection with the APNs server before the connection attempt is considered a failure.
      *
-     * @param connectionTimeout the maximum amount of time to wait for a connection attempt to complete
-     * @param timeoutUnit the time unit for the given timeout
+     * @param timeout the maximum amount of time to wait for a connection attempt to complete
      *
      * @return a reference to this builder
      *
      * @since 0.8
      */
-    public ApnsClientBuilder setConnectionTimeout(final long connectionTimeout, final TimeUnit timeoutUnit) {
-        this.connectionTimeoutMillis = (int) timeoutUnit.toMillis(connectionTimeout);
+    public ApnsClientBuilder setConnectionTimeout(final Duration timeout) {
+        this.connectionTimeout = timeout;
         return this;
     }
 
     /**
      * Sets the amount of idle time (in milliseconds) after which the client under construction will send a PING frame
-     * to the APNs server. By default, clients will send a PING frame after
-     * {@value com.eatthepath.pushy.apns.ApnsClientBuilder#DEFAULT_PING_IDLE_TIME_MILLIS} milliseconds of inactivity.
+     * to the APNs server. By default, clients will send a PING frame after an idle period of
+     * {@link com.eatthepath.pushy.apns.ApnsClientBuilder#DEFAULT_IDLE_PING_INTERVAL}.
      *
-     * @param pingInterval the amount of idle time after which the client will send a PING frame
-     * @param pingIntervalUnit the time unit for the given idle time
+     * @param idlePingInterval the amount of idle time after which the client will send a PING frame
      *
      * @return a reference to this builder
      *
      * @since 0.10
      */
-    public ApnsClientBuilder setIdlePingInterval(final long pingInterval, final TimeUnit pingIntervalUnit) {
-        this.idlePingIntervalMillis = pingIntervalUnit.toMillis(pingInterval);
+    public ApnsClientBuilder setIdlePingInterval(final Duration idlePingInterval) {
+        this.idlePingInterval = idlePingInterval;
         return this;
     }
 
@@ -477,7 +474,6 @@ public class ApnsClientBuilder {
      *
      * @param gracefulShutdownTimeout the amount of time to wait for in-progress requests to complete before closing a
      * connection
-     * @param timeoutUnit the time unit for the given timeout
      *
      * @return a reference to this builder
      *
@@ -485,8 +481,8 @@ public class ApnsClientBuilder {
      *
      * @since 0.8
      */
-    public ApnsClientBuilder setGracefulShutdownTimeout(final long gracefulShutdownTimeout, final TimeUnit timeoutUnit) {
-        this.gracefulShutdownTimeoutMillis = timeoutUnit.toMillis(gracefulShutdownTimeout);
+    public ApnsClientBuilder setGracefulShutdownTimeout(final Duration gracefulShutdownTimeout) {
+        this.gracefulShutdownTimeout = gracefulShutdownTimeout;
         return this;
     }
 
@@ -565,8 +561,8 @@ public class ApnsClientBuilder {
         }
 
         final ApnsClient client = new ApnsClient(this.apnsServerAddress, sslContext, this.signingKey,
-                this.tokenExpirationMillis,  this.proxyHandlerFactory, this.connectionTimeoutMillis,
-                this.idlePingIntervalMillis, this.gracefulShutdownTimeoutMillis, this.concurrentConnections,
+                this.tokenExpiration, this.proxyHandlerFactory, this.connectionTimeout,
+                this.idlePingInterval, this.gracefulShutdownTimeout, this.concurrentConnections,
                 this.metricsListener, this.frameLogger, this.eventLoopGroup);
 
         if (sslContext instanceof ReferenceCounted) {
diff --git a/pushy/src/main/java/com/eatthepath/pushy/apns/ApnsClientHandler.java b/pushy/src/main/java/com/eatthepath/pushy/apns/ApnsClientHandler.java
index fd04774a..1e5eda40 100644
--- a/pushy/src/main/java/com/eatthepath/pushy/apns/ApnsClientHandler.java
+++ b/pushy/src/main/java/com/eatthepath/pushy/apns/ApnsClientHandler.java
@@ -22,10 +22,10 @@
 
 package com.eatthepath.pushy.apns;
 
+import com.eatthepath.pushy.apns.util.InstantAsTimeSinceEpochTypeAdapter;
 import com.eatthepath.uuid.FastUUID;
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
-import com.eatthepath.pushy.apns.util.DateAsTimeSinceEpochTypeAdapter;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
 import io.netty.channel.Channel;
@@ -48,7 +48,8 @@ import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
-import java.util.Date;
+import java.time.Duration;
+import java.time.Instant;
 import java.util.Map;
 import java.util.Objects;
 import java.util.UUID;
@@ -64,7 +65,7 @@ class ApnsClientHandler extends Http2ConnectionHandler implements Http2FrameList
 
     private final String authority;
 
-    private final long pingTimeoutMillis;
+    private final Duration pingTimeout;
     private ScheduledFuture<?> pingTimeoutFuture;
 
     private Throwable connectionErrorCause;
@@ -84,7 +85,7 @@ class ApnsClientHandler extends Http2ConnectionHandler implements Http2FrameList
             new IOException("Stream closed before a reply was received");
 
     private static final Gson GSON = new GsonBuilder()
-            .registerTypeAdapter(Date.class, new DateAsTimeSinceEpochTypeAdapter(TimeUnit.MILLISECONDS))
+            .registerTypeAdapter(Instant.class, new InstantAsTimeSinceEpochTypeAdapter(TimeUnit.MILLISECONDS))
             .create();
 
     private static final Logger log = LoggerFactory.getLogger(ApnsClientHandler.class);
@@ -92,7 +93,7 @@ class ApnsClientHandler extends Http2ConnectionHandler implements Http2FrameList
     public static class ApnsClientHandlerBuilder extends AbstractHttp2ConnectionHandlerBuilder<ApnsClientHandler, ApnsClientHandlerBuilder> {
 
         private String authority;
-        private long idlePingIntervalMillis;
+        private Duration idlePingInterval;
 
         ApnsClientHandlerBuilder authority(final String authority) {
             this.authority = authority;
@@ -103,12 +104,12 @@ class ApnsClientHandler extends Http2ConnectionHandler implements Http2FrameList
             return this.authority;
         }
 
-        long idlePingIntervalMillis() {
-            return idlePingIntervalMillis;
+        Duration idlePingInterval() {
+            return idlePingInterval;
         }
 
-        ApnsClientHandlerBuilder idlePingIntervalMillis(final long idlePingIntervalMillis) {
-            this.idlePingIntervalMillis = idlePingIntervalMillis;
+        ApnsClientHandlerBuilder idlePingInterval(final Duration idlePingIntervalMillis) {
+            this.idlePingInterval = idlePingIntervalMillis;
             return this;
         }
 
@@ -136,7 +137,7 @@ class ApnsClientHandler extends Http2ConnectionHandler implements Http2FrameList
         public ApnsClientHandler build(final Http2ConnectionDecoder decoder, final Http2ConnectionEncoder encoder, final Http2Settings initialSettings) {
             Objects.requireNonNull(this.authority(), "Authority must be set before building an ApnsClientHandler.");
 
-            final ApnsClientHandler handler = new ApnsClientHandler(decoder, encoder, initialSettings, this.authority(), this.idlePingIntervalMillis());
+            final ApnsClientHandler handler = new ApnsClientHandler(decoder, encoder, initialSettings, this.authority(), this.idlePingInterval());
             this.frameListener(handler);
             return handler;
         }
@@ -147,7 +148,7 @@ class ApnsClientHandler extends Http2ConnectionHandler implements Http2FrameList
         }
     }
 
-    ApnsClientHandler(final Http2ConnectionDecoder decoder, final Http2ConnectionEncoder encoder, final Http2Settings initialSettings, final String authority, final long idlePingIntervalMillis) {
+    ApnsClientHandler(final Http2ConnectionDecoder decoder, final Http2ConnectionEncoder encoder, final Http2Settings initialSettings, final String authority, final Duration idlePingInterval) {
         super(decoder, encoder, initialSettings);
 
         this.authority = authority;
@@ -158,7 +159,7 @@ class ApnsClientHandler extends Http2ConnectionHandler implements Http2FrameList
 
         this.connection().addListener(this);
 
-        this.pingTimeoutMillis = idlePingIntervalMillis / 2;
+        this.pingTimeout = idlePingInterval.dividedBy(2);
     }
 
     @Override
@@ -236,7 +237,7 @@ class ApnsClientHandler extends Http2ConnectionHandler implements Http2FrameList
                 .authority(this.authority)
                 .path(APNS_PATH_PREFIX + pushNotification.getToken())
                 .scheme(HttpScheme.HTTPS.name())
-                .addInt(APNS_EXPIRATION_HEADER, pushNotification.getExpiration() == null ? 0 : (int) (pushNotification.getExpiration().getTime() / 1000));
+                .addInt(APNS_EXPIRATION_HEADER, pushNotification.getExpiration() == null ? 0 : (int) pushNotification.getExpiration().getEpochSecond());
 
         if (pushNotification.getCollapseId() != null) {
             headers.add(APNS_COLLAPSE_ID_HEADER, pushNotification.getCollapseId());
@@ -277,7 +278,7 @@ class ApnsClientHandler extends Http2ConnectionHandler implements Http2FrameList
             this.pingTimeoutFuture = context.channel().eventLoop().schedule(() -> {
                 log.debug("Closing channel due to ping timeout.");
                 context.channel().close();
-            }, pingTimeoutMillis, TimeUnit.MILLISECONDS);
+            }, pingTimeout.toMillis(), TimeUnit.MILLISECONDS);
 
             this.flush(context);
         }
diff --git a/pushy/src/main/java/com/eatthepath/pushy/apns/ApnsPushNotification.java b/pushy/src/main/java/com/eatthepath/pushy/apns/ApnsPushNotification.java
index 3fadecf3..549d3647 100644
--- a/pushy/src/main/java/com/eatthepath/pushy/apns/ApnsPushNotification.java
+++ b/pushy/src/main/java/com/eatthepath/pushy/apns/ApnsPushNotification.java
@@ -24,7 +24,7 @@ package com.eatthepath.pushy.apns;
 
 import com.eatthepath.pushy.apns.util.ApnsPayloadBuilder;
 
-import java.util.Date;
+import java.time.Instant;
 import java.util.UUID;
 
 /**
@@ -73,7 +73,7 @@ public interface ApnsPushNotification {
      *
      * @since 0.5
      */
-    Date getExpiration();
+    Instant getExpiration();
 
     /**
      * Returns the priority with which this push notification should be sent to the receiving device. If {@code null},
diff --git a/pushy/src/main/java/com/eatthepath/pushy/apns/ErrorResponse.java b/pushy/src/main/java/com/eatthepath/pushy/apns/ErrorResponse.java
index 500d79ca..cd13f6c2 100644
--- a/pushy/src/main/java/com/eatthepath/pushy/apns/ErrorResponse.java
+++ b/pushy/src/main/java/com/eatthepath/pushy/apns/ErrorResponse.java
@@ -22,13 +22,13 @@
 
 package com.eatthepath.pushy.apns;
 
-import java.util.Date;
+import java.time.Instant;
 
 class ErrorResponse {
     private final String reason;
-    private final Date timestamp;
+    private final Instant timestamp;
 
-    public ErrorResponse(final String reason, final Date timestamp) {
+    public ErrorResponse(final String reason, final Instant timestamp) {
         this.reason = reason;
         this.timestamp = timestamp;
     }
@@ -37,7 +37,7 @@ class ErrorResponse {
         return this.reason;
     }
 
-    Date getTimestamp() {
+    Instant getTimestamp() {
         return this.timestamp;
     }
 }
diff --git a/pushy/src/main/java/com/eatthepath/pushy/apns/PushNotificationResponse.java b/pushy/src/main/java/com/eatthepath/pushy/apns/PushNotificationResponse.java
index ca54aeb2..7ad29b1e 100644
--- a/pushy/src/main/java/com/eatthepath/pushy/apns/PushNotificationResponse.java
+++ b/pushy/src/main/java/com/eatthepath/pushy/apns/PushNotificationResponse.java
@@ -22,7 +22,7 @@
 
 package com.eatthepath.pushy.apns;
 
-import java.util.Date;
+import java.time.Instant;
 import java.util.UUID;
 
 /**
@@ -86,5 +86,5 @@ public interface PushNotificationResponse<T extends ApnsPushNotification> {
      *
      * @since 0.5
      */
-    Date getTokenInvalidationTimestamp();
+    Instant getTokenInvalidationTimestamp();
 }
diff --git a/pushy/src/main/java/com/eatthepath/pushy/apns/SimplePushNotificationResponse.java b/pushy/src/main/java/com/eatthepath/pushy/apns/SimplePushNotificationResponse.java
index 12ae73e5..6586b6e4 100644
--- a/pushy/src/main/java/com/eatthepath/pushy/apns/SimplePushNotificationResponse.java
+++ b/pushy/src/main/java/com/eatthepath/pushy/apns/SimplePushNotificationResponse.java
@@ -24,7 +24,7 @@ package com.eatthepath.pushy.apns;
 
 import com.eatthepath.uuid.FastUUID;
 
-import java.util.Date;
+import java.time.Instant;
 import java.util.UUID;
 
 /**
@@ -37,9 +37,9 @@ class SimplePushNotificationResponse<T extends ApnsPushNotification> implements
     private final boolean success;
     private final UUID apnsId;
     private final String rejectionReason;
-    private final Date tokenExpirationTimestamp;
+    private final Instant tokenExpirationTimestamp;
 
-    SimplePushNotificationResponse(final T pushNotification, final boolean success, final UUID apnsId, final String rejectionReason, final Date tokenExpirationTimestamp) {
+    SimplePushNotificationResponse(final T pushNotification, final boolean success, final UUID apnsId, final String rejectionReason, final Instant tokenExpirationTimestamp) {
         this.pushNotification = pushNotification;
         this.success = success;
         this.apnsId = apnsId;
@@ -68,7 +68,7 @@ class SimplePushNotificationResponse<T extends ApnsPushNotification> implements
     }
 
     @Override
-    public Date getTokenInvalidationTimestamp() {
+    public Instant getTokenInvalidationTimestamp() {
         return this.tokenExpirationTimestamp;
     }
 
diff --git a/pushy/src/main/java/com/eatthepath/pushy/apns/TokenAuthenticationApnsClientHandler.java b/pushy/src/main/java/com/eatthepath/pushy/apns/TokenAuthenticationApnsClientHandler.java
index ca6f73f9..783f632b 100644
--- a/pushy/src/main/java/com/eatthepath/pushy/apns/TokenAuthenticationApnsClientHandler.java
+++ b/pushy/src/main/java/com/eatthepath/pushy/apns/TokenAuthenticationApnsClientHandler.java
@@ -37,7 +37,8 @@ import org.slf4j.LoggerFactory;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 import java.security.SignatureException;
-import java.util.Date;
+import java.time.Duration;
+import java.time.Instant;
 import java.util.Objects;
 import java.util.concurrent.TimeUnit;
 
@@ -48,7 +49,7 @@ class TokenAuthenticationApnsClientHandler extends ApnsClientHandler {
     private AuthenticationToken authenticationToken;
     private int mostRecentStreamWithNewToken = 0;
 
-    private final long tokenExpirationMillis;
+    private final Duration tokenExpiration;
     private ScheduledFuture<?> expireTokenFuture;
 
     private static final AsciiString APNS_AUTHORIZATION_HEADER = new AsciiString("authorization");
@@ -59,7 +60,7 @@ class TokenAuthenticationApnsClientHandler extends ApnsClientHandler {
 
     public static class TokenAuthenticationApnsClientHandlerBuilder extends ApnsClientHandlerBuilder {
         private ApnsSigningKey signingKey;
-        private Long tokenExpirationMillis;
+        private Duration tokenExpiration;
 
         public TokenAuthenticationApnsClientHandlerBuilder signingKey(final ApnsSigningKey signingKey) {
             this.signingKey = signingKey;
@@ -70,33 +71,33 @@ class TokenAuthenticationApnsClientHandler extends ApnsClientHandler {
             return this.signingKey;
         }
 
-        public TokenAuthenticationApnsClientHandlerBuilder tokenExpirationMillis(final long tokenExpirationMillis) {
-            this.tokenExpirationMillis = tokenExpirationMillis;
+        public TokenAuthenticationApnsClientHandlerBuilder tokenExpiration(final Duration tokenExpiration) {
+            this.tokenExpiration = tokenExpiration;
             return this;
         }
 
-        public Long tokenExpirationMillis() {
-            return this.tokenExpirationMillis;
+        public Duration tokenExpiration() {
+            return this.tokenExpiration;
         }
 
         @Override
         public ApnsClientHandler build(final Http2ConnectionDecoder decoder, final Http2ConnectionEncoder encoder, final Http2Settings initialSettings) {
             Objects.requireNonNull(this.authority(), "Authority must be set before building a TokenAuthenticationApnsClientHandler.");
             Objects.requireNonNull(this.signingKey(), "Signing key must be set before building a TokenAuthenticationApnsClientHandler.");
-            Objects.requireNonNull(this.tokenExpirationMillis(), "Token expiration duration must be set before building a TokenAuthenticationApnsClientHandler.");
+            Objects.requireNonNull(this.tokenExpiration(), "Token expiration duration must be set before building a TokenAuthenticationApnsClientHandler.");
 
-            final ApnsClientHandler handler = new TokenAuthenticationApnsClientHandler(decoder, encoder, initialSettings, this.authority(), this.idlePingIntervalMillis(), this.signingKey(), this.tokenExpirationMillis());
+            final ApnsClientHandler handler = new TokenAuthenticationApnsClientHandler(decoder, encoder, initialSettings, this.authority(), this.idlePingInterval(), this.signingKey(), this.tokenExpiration());
             this.frameListener(handler);
             return handler;
         }
     }
 
-    protected TokenAuthenticationApnsClientHandler(final Http2ConnectionDecoder decoder, final Http2ConnectionEncoder encoder, final Http2Settings initialSettings, final String authority, final long idlePingIntervalMillis, final ApnsSigningKey signingKey, final long tokenExpirationMillis) {
-        super(decoder, encoder, initialSettings, authority, idlePingIntervalMillis);
+    protected TokenAuthenticationApnsClientHandler(final Http2ConnectionDecoder decoder, final Http2ConnectionEncoder encoder, final Http2Settings initialSettings, final String authority, final Duration idlePingInterval, final ApnsSigningKey signingKey, final Duration tokenExpiration) {
+        super(decoder, encoder, initialSettings, authority, idlePingInterval);
 
         Objects.requireNonNull(signingKey, "Signing key must not be null for token-based client handlers.");
         this.signingKey = signingKey;
-        this.tokenExpirationMillis = tokenExpirationMillis;
+        this.tokenExpiration = tokenExpiration;
     }
 
     @Override
@@ -107,13 +108,13 @@ class TokenAuthenticationApnsClientHandler extends ApnsClientHandler {
             try {
                 log.debug("Generating new token for stream {}.", streamId);
 
-                this.authenticationToken = new AuthenticationToken(signingKey, new Date());
+                this.authenticationToken = new AuthenticationToken(signingKey, Instant.now());
                 this.mostRecentStreamWithNewToken = streamId;
 
                 this.expireTokenFuture = context.executor().schedule(() -> {
                     log.debug("Proactively expiring authentication token.");
                     TokenAuthenticationApnsClientHandler.this.authenticationToken = null;
-                }, tokenExpirationMillis, TimeUnit.MILLISECONDS);
+                }, tokenExpiration.toMillis(), TimeUnit.MILLISECONDS);
             } catch (final NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
                 // This should never happen because we check the key/algorithm at signing key construction time.
                 log.error("Failed to generate authentication token.", e);
diff --git a/pushy/src/main/java/com/eatthepath/pushy/apns/auth/AuthenticationToken.java b/pushy/src/main/java/com/eatthepath/pushy/apns/auth/AuthenticationToken.java
index 4eaaffce..09b98107 100644
--- a/pushy/src/main/java/com/eatthepath/pushy/apns/auth/AuthenticationToken.java
+++ b/pushy/src/main/java/com/eatthepath/pushy/apns/auth/AuthenticationToken.java
@@ -22,10 +22,10 @@
 
 package com.eatthepath.pushy.apns.auth;
 
+import com.eatthepath.pushy.apns.util.InstantAsTimeSinceEpochTypeAdapter;
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
 import com.google.gson.annotations.SerializedName;
-import com.eatthepath.pushy.apns.util.DateAsTimeSinceEpochTypeAdapter;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
 import io.netty.handler.codec.base64.Base64;
@@ -37,7 +37,7 @@ import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 import java.security.Signature;
 import java.security.SignatureException;
-import java.util.Date;
+import java.time.Instant;
 import java.util.Objects;
 import java.util.concurrent.TimeUnit;
 
@@ -90,9 +90,9 @@ public class AuthenticationToken {
         private final String issuer;
 
         @SerializedName("iat")
-        private final Date issuedAt;
+        private final Instant issuedAt;
 
-        AuthenticationTokenClaims(final String teamId, final Date issuedAt) {
+        AuthenticationTokenClaims(final String teamId, final Instant issuedAt) {
             this.issuer = teamId;
             this.issuedAt = issuedAt;
         }
@@ -101,14 +101,14 @@ public class AuthenticationToken {
             return this.issuer;
         }
 
-        Date getIssuedAt() {
+        Instant getIssuedAt() {
             return this.issuedAt;
         }
     }
 
     private static final Gson GSON = new GsonBuilder()
             .disableHtmlEscaping()
-            .registerTypeAdapter(Date.class, new DateAsTimeSinceEpochTypeAdapter(TimeUnit.SECONDS))
+            .registerTypeAdapter(Instant.class, new InstantAsTimeSinceEpochTypeAdapter(TimeUnit.SECONDS))
             .create();
 
     private final AuthenticationTokenHeader header;
@@ -130,7 +130,7 @@ public class AuthenticationToken {
      * @throws InvalidKeyException if the given key was invalid for any reason
      * @throws SignatureException if the given key could not be used to sign the token
      */
-    public AuthenticationToken(final ApnsSigningKey signingKey, final Date issuedAt) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
+    public AuthenticationToken(final ApnsSigningKey signingKey, final Instant issuedAt) throws NoSuchAlgorithmException, InvalidKeyException, SignatureException {
         this.header = new AuthenticationTokenHeader(signingKey.getKeyId());
         this.claims = new AuthenticationTokenClaims(signingKey.getTeamId(), issuedAt);
 
@@ -185,7 +185,7 @@ public class AuthenticationToken {
      *
      * @return the time at which this token was issued
      */
-    public Date getIssuedAt() {
+    public Instant getIssuedAt() {
         return this.claims.getIssuedAt();
     }
 
diff --git a/pushy/src/main/java/com/eatthepath/pushy/apns/server/MockApnsServerHandler.java b/pushy/src/main/java/com/eatthepath/pushy/apns/server/MockApnsServerHandler.java
index 02891711..1632a51d 100644
--- a/pushy/src/main/java/com/eatthepath/pushy/apns/server/MockApnsServerHandler.java
+++ b/pushy/src/main/java/com/eatthepath/pushy/apns/server/MockApnsServerHandler.java
@@ -22,10 +22,10 @@
 
 package com.eatthepath.pushy.apns.server;
 
+import com.eatthepath.pushy.apns.util.InstantAsTimeSinceEpochTypeAdapter;
 import com.eatthepath.uuid.FastUUID;
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
-import com.eatthepath.pushy.apns.util.DateAsTimeSinceEpochTypeAdapter;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
 import io.netty.channel.ChannelFuture;
@@ -39,7 +39,7 @@ import io.netty.util.concurrent.PromiseCombiner;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.Date;
+import java.time.Instant;
 import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 
@@ -56,7 +56,7 @@ class MockApnsServerHandler extends Http2ConnectionHandler implements Http2Frame
     private static final int MAX_CONTENT_LENGTH = 4096;
 
     private static final Gson GSON = new GsonBuilder()
-            .registerTypeAdapter(Date.class, new DateAsTimeSinceEpochTypeAdapter(TimeUnit.MILLISECONDS))
+            .registerTypeAdapter(Instant.class, new InstantAsTimeSinceEpochTypeAdapter(TimeUnit.MILLISECONDS))
             .create();
 
     private static final Logger log = LoggerFactory.getLogger(MockApnsServerHandler.class);
@@ -120,9 +120,9 @@ class MockApnsServerHandler extends Http2ConnectionHandler implements Http2Frame
 
     private static class RejectNotificationResponse extends ApnsResponse {
         private final RejectionReason errorReason;
-        private final Date timestamp;
+        private final Instant timestamp;
 
-        RejectNotificationResponse(final int streamId, final UUID apnsId, final RejectionReason errorReason, final Date timestamp) {
+        RejectNotificationResponse(final int streamId, final UUID apnsId, final RejectionReason errorReason, final Instant timestamp) {
             super(streamId, apnsId);
 
             this.errorReason = errorReason;
@@ -133,7 +133,7 @@ class MockApnsServerHandler extends Http2ConnectionHandler implements Http2Frame
             return this.errorReason;
         }
 
-        Date getTimestamp() {
+        Instant getTimestamp() {
             return this.timestamp;
         }
     }
@@ -141,9 +141,9 @@ class MockApnsServerHandler extends Http2ConnectionHandler implements Http2Frame
     @SuppressWarnings({"unused", "FieldCanBeLocal"})
     private static class ErrorPayload {
         private final String reason;
-        private final Date timestamp;
+        private final Instant timestamp;
 
-        ErrorPayload(final String reason, final Date timestamp) {
+        ErrorPayload(final String reason, final Instant timestamp) {
             this.reason = reason;
             this.timestamp = timestamp;
         }
@@ -156,7 +156,7 @@ class MockApnsServerHandler extends Http2ConnectionHandler implements Http2Frame
         }
 
         @Override
-        public void handlePushNotificationRejected(final Http2Headers headers, final ByteBuf payload, final RejectionReason rejectionReason, final Date deviceTokenExpirationTimestamp) {
+        public void handlePushNotificationRejected(final Http2Headers headers, final ByteBuf payload, final RejectionReason rejectionReason, final Instant deviceTokenExpirationTimestamp) {
         }
     }
 
@@ -278,7 +278,7 @@ class MockApnsServerHandler extends Http2ConnectionHandler implements Http2Frame
             this.write(context, new AcceptNotificationResponse(stream.id(), apnsId), writePromise);
             this.listener.handlePushNotificationAccepted(headers, payload);
         } catch (final RejectedNotificationException e) {
-            final Date deviceTokenExpirationTimestamp = e instanceof UnregisteredDeviceTokenException ?
+            final Instant deviceTokenExpirationTimestamp = e instanceof UnregisteredDeviceTokenException ?
                     ((UnregisteredDeviceTokenException) e).getDeviceTokenExpirationTimestamp() : null;
 
             this.write(context, new RejectNotificationResponse(stream.id(), apnsId, e.getRejectionReason(), deviceTokenExpirationTimestamp), writePromise);
diff --git a/pushy/src/main/java/com/eatthepath/pushy/apns/server/MockApnsServerListener.java b/pushy/src/main/java/com/eatthepath/pushy/apns/server/MockApnsServerListener.java
index b06ead6e..51a5d02c 100644
--- a/pushy/src/main/java/com/eatthepath/pushy/apns/server/MockApnsServerListener.java
+++ b/pushy/src/main/java/com/eatthepath/pushy/apns/server/MockApnsServerListener.java
@@ -25,7 +25,7 @@ package com.eatthepath.pushy.apns.server;
 import io.netty.buffer.ByteBuf;
 import io.netty.handler.codec.http2.Http2Headers;
 
-import java.util.Date;
+import java.time.Instant;
 
 /**
  * <p>Mock APNs server listeners are notified when push notifications are accepted or rejected by a
@@ -57,12 +57,11 @@ public interface MockApnsServerListener {
 
     /**
      * Indicates that a push notification has been rejected by the mock server.
-     *
-     * @param headers the notification's HTTP/2 headers
+     *  @param headers the notification's HTTP/2 headers
      * @param payload the notification's payload
      * @param rejectionReason the reason the push notification was rejected by the mock server
      * @param deviceTokenExpirationTimestamp the time at which the push notification's destination device token expired;
-     * may be {@code null} if the token has not expired
+* may be {@code null} if the token has not expired
      */
-    void handlePushNotificationRejected(Http2Headers headers, ByteBuf payload, RejectionReason rejectionReason, Date deviceTokenExpirationTimestamp);
+    void handlePushNotificationRejected(Http2Headers headers, ByteBuf payload, RejectionReason rejectionReason, Instant deviceTokenExpirationTimestamp);
 }
diff --git a/pushy/src/main/java/com/eatthepath/pushy/apns/server/ParsingMockApnsServerListenerAdapter.java b/pushy/src/main/java/com/eatthepath/pushy/apns/server/ParsingMockApnsServerListenerAdapter.java
index 1e6fc044..b5447650 100644
--- a/pushy/src/main/java/com/eatthepath/pushy/apns/server/ParsingMockApnsServerListenerAdapter.java
+++ b/pushy/src/main/java/com/eatthepath/pushy/apns/server/ParsingMockApnsServerListenerAdapter.java
@@ -22,10 +22,10 @@
 
 package com.eatthepath.pushy.apns.server;
 
-import com.eatthepath.uuid.FastUUID;
 import com.eatthepath.pushy.apns.ApnsPushNotification;
 import com.eatthepath.pushy.apns.DeliveryPriority;
 import com.eatthepath.pushy.apns.PushType;
+import com.eatthepath.uuid.FastUUID;
 import io.netty.buffer.ByteBuf;
 import io.netty.handler.codec.http2.Http2Headers;
 import io.netty.util.AsciiString;
@@ -33,7 +33,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.nio.charset.StandardCharsets;
-import java.util.Date;
+import java.time.Instant;
 import java.util.UUID;
 
 /**
@@ -54,14 +54,14 @@ public abstract class ParsingMockApnsServerListenerAdapter implements MockApnsSe
     private static class LenientApnsPushNotification implements ApnsPushNotification {
         private final String token;
         private final String payload;
-        private final Date invalidationTime;
+        private final Instant invalidationTime;
         private final DeliveryPriority priority;
         private final PushType pushType;
         private final String topic;
         private final String collapseId;
         private final UUID apnsId;
 
-        private LenientApnsPushNotification(final String token, final String topic, final String payload, final Date invalidationTime, final DeliveryPriority priority, final PushType pushType, final String collapseId, final UUID apnsId) {
+        private LenientApnsPushNotification(final String token, final String topic, final String payload, final Instant invalidationTime, final DeliveryPriority priority, final PushType pushType, final String collapseId, final UUID apnsId) {
             this.token = token;
             this.payload = payload;
             this.invalidationTime = invalidationTime;
@@ -83,7 +83,7 @@ public abstract class ParsingMockApnsServerListenerAdapter implements MockApnsSe
         }
 
         @Override
-        public Date getExpiration() {
+        public Instant getExpiration() {
             return this.invalidationTime;
         }
 
@@ -146,26 +146,24 @@ public abstract class ParsingMockApnsServerListenerAdapter implements MockApnsSe
     /**
      * Parses a push notification rejected by a mock APNs server into an {@link ApnsPushNotification} instance for
      * further processing by
-     * {@link ParsingMockApnsServerListenerAdapter#handlePushNotificationRejected(ApnsPushNotification, RejectionReason, Date)}.
-     *
-     * @param headers the notification's HTTP/2 headers
+     * {@link ParsingMockApnsServerListenerAdapter#handlePushNotificationRejected(ApnsPushNotification, RejectionReason, Instant)}.
+     *  @param headers the notification's HTTP/2 headers
      * @param payload the notification's payload
      * @param rejectionReason the reason the push notification was rejected by the mock server
      * @param deviceTokenExpirationTimestamp the time at which the push notification's destination device token expired;
      */
-    public void handlePushNotificationRejected(final Http2Headers headers, final ByteBuf payload, final RejectionReason rejectionReason, final Date deviceTokenExpirationTimestamp) {
+    public void handlePushNotificationRejected(final Http2Headers headers, final ByteBuf payload, final RejectionReason rejectionReason, final Instant deviceTokenExpirationTimestamp) {
         this.handlePushNotificationRejected(parsePushNotification(headers, payload), rejectionReason, deviceTokenExpirationTimestamp);
     }
 
     /**
      * Handles a parsed push notification accepted by a mock server. Note that any field of the parsed push notification
      * may be {@code null}.
-     *
-     * @param pushNotification the push notification rejected by the server
+     *  @param pushNotification the push notification rejected by the server
      * @param rejectionReason the reason the push notification was rejected by the mock server
      * @param deviceTokenExpirationTimestamp the time at which the push notification's destination device token expired;
      */
-    public abstract void handlePushNotificationRejected(final ApnsPushNotification pushNotification, final RejectionReason rejectionReason, final Date deviceTokenExpirationTimestamp);
+    public abstract void handlePushNotificationRejected(final ApnsPushNotification pushNotification, final RejectionReason rejectionReason, final Instant deviceTokenExpirationTimestamp);
 
     private static ApnsPushNotification parsePushNotification(final Http2Headers headers, final ByteBuf payload) {
         final UUID apnsId;
@@ -233,10 +231,10 @@ public abstract class ParsingMockApnsServerListenerAdapter implements MockApnsSe
             pushType = pushTypeFromHeader;
         }
 
-        final Date expiration;
+        final Instant expiration;
         {
             final Integer expirationTimestamp = headers.getInt(APNS_EXPIRATION_HEADER);
-            expiration = expirationTimestamp != null ? new Date(expirationTimestamp * 1000) : null;
+            expiration = expirationTimestamp != null ? Instant.ofEpochSecond(expirationTimestamp) : null;
         }
 
         final String collapseId;
diff --git a/pushy/src/main/java/com/eatthepath/pushy/apns/server/TlsAuthenticationValidatingPushNotificationHandler.java b/pushy/src/main/java/com/eatthepath/pushy/apns/server/TlsAuthenticationValidatingPushNotificationHandler.java
index 642202fc..524862c6 100644
--- a/pushy/src/main/java/com/eatthepath/pushy/apns/server/TlsAuthenticationValidatingPushNotificationHandler.java
+++ b/pushy/src/main/java/com/eatthepath/pushy/apns/server/TlsAuthenticationValidatingPushNotificationHandler.java
@@ -25,6 +25,7 @@ package com.eatthepath.pushy.apns.server;
 import io.netty.handler.codec.http2.Http2Headers;
 import io.netty.util.AsciiString;
 
+import java.time.Instant;
 import java.util.*;
 
 class TlsAuthenticationValidatingPushNotificationHandler extends ValidatingPushNotificationHandler {
@@ -33,7 +34,7 @@ class TlsAuthenticationValidatingPushNotificationHandler extends ValidatingPushN
 
     private static final AsciiString APNS_TOPIC_HEADER = new AsciiString("apns-topic");
 
-    TlsAuthenticationValidatingPushNotificationHandler(final Map<String, Set<String>> deviceTokensByTopic, final Map<String, Date> expirationTimestampsByDeviceToken, final String baseTopic) {
+    TlsAuthenticationValidatingPushNotificationHandler(final Map<String, Set<String>> deviceTokensByTopic, final Map<String, Instant> expirationTimestampsByDeviceToken, final String baseTopic) {
         super(deviceTokensByTopic, expirationTimestampsByDeviceToken);
 
         Objects.requireNonNull(baseTopic, "Base topic must not be null for mock server handlers using TLS-based authentication.");
diff --git a/pushy/src/main/java/com/eatthepath/pushy/apns/server/TokenAuthenticationValidatingPushNotificationHandler.java b/pushy/src/main/java/com/eatthepath/pushy/apns/server/TokenAuthenticationValidatingPushNotificationHandler.java
index ed51880e..93d14094 100644
--- a/pushy/src/main/java/com/eatthepath/pushy/apns/server/TokenAuthenticationValidatingPushNotificationHandler.java
+++ b/pushy/src/main/java/com/eatthepath/pushy/apns/server/TokenAuthenticationValidatingPushNotificationHandler.java
@@ -32,10 +32,10 @@ import org.slf4j.LoggerFactory;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 import java.security.SignatureException;
-import java.util.Date;
+import java.time.Duration;
+import java.time.Instant;
 import java.util.Map;
 import java.util.Set;
-import java.util.concurrent.TimeUnit;
 
 class TokenAuthenticationValidatingPushNotificationHandler extends ValidatingPushNotificationHandler {
 
@@ -47,11 +47,11 @@ class TokenAuthenticationValidatingPushNotificationHandler extends ValidatingPus
     private static final AsciiString APNS_TOPIC_HEADER = new AsciiString("apns-topic");
     private static final AsciiString APNS_AUTHORIZATION_HEADER = new AsciiString("authorization");
 
-    private static final long AUTHENTICATION_TOKEN_EXPIRATION_MILLIS = TimeUnit.HOURS.toMillis(1);
+    private static final Duration AUTHENTICATION_TOKEN_EXPIRATION_DURATION = Duration.ofHours(1);
 
     private static final Logger log = LoggerFactory.getLogger(TokenAuthenticationValidatingPushNotificationHandler.class);
 
-    TokenAuthenticationValidatingPushNotificationHandler(final Map<String, Set<String>> deviceTokensByTopic, final Map<String, Date> expirationTimestampsByDeviceToken, final Map<String, ApnsVerificationKey> verificationKeysByKeyId, final Map<ApnsVerificationKey, Set<String>> topicsByVerificationKey) {
+    TokenAuthenticationValidatingPushNotificationHandler(final Map<String, Set<String>> deviceTokensByTopic, final Map<String, Instant> expirationTimestampsByDeviceToken, final Map<String, ApnsVerificationKey> verificationKeysByKeyId, final Map<ApnsVerificationKey, Set<String>> topicsByVerificationKey) {
         super(deviceTokensByTopic, expirationTimestampsByDeviceToken);
 
         this.verificationKeysByKeyId = verificationKeysByKeyId;
@@ -119,7 +119,7 @@ class TokenAuthenticationValidatingPushNotificationHandler extends ValidatingPus
             throw new RejectedNotificationException(RejectionReason.INVALID_PROVIDER_TOKEN);
         }
 
-        if (authenticationToken.getIssuedAt().getTime() + AUTHENTICATION_TOKEN_EXPIRATION_MILLIS < System.currentTimeMillis()) {
+        if (Instant.now().isAfter(authenticationToken.getIssuedAt().plus(AUTHENTICATION_TOKEN_EXPIRATION_DURATION))) {
             throw new RejectedNotificationException(RejectionReason.EXPIRED_PROVIDER_TOKEN);
         }
 
diff --git a/pushy/src/main/java/com/eatthepath/pushy/apns/server/UnregisteredDeviceTokenException.java b/pushy/src/main/java/com/eatthepath/pushy/apns/server/UnregisteredDeviceTokenException.java
index 92169d60..7dd4b0d2 100644
--- a/pushy/src/main/java/com/eatthepath/pushy/apns/server/UnregisteredDeviceTokenException.java
+++ b/pushy/src/main/java/com/eatthepath/pushy/apns/server/UnregisteredDeviceTokenException.java
@@ -22,7 +22,7 @@
 
 package com.eatthepath.pushy.apns.server;
 
-import java.util.Date;
+import java.time.Instant;
 import java.util.Objects;
 
 /**
@@ -33,14 +33,14 @@ import java.util.Objects;
  * @since 0.12
  */
 public class UnregisteredDeviceTokenException extends RejectedNotificationException {
-    private final Date deviceTokenExpirationTimestamp;
+    private final Instant deviceTokenExpirationTimestamp;
 
     /**
      * Constructs a new unregistered device token exception indicating that the device token expired at the given time.
      *
      * @param deviceTokenExpirationTimestamp the time at which the destination device token expired
      */
-    public UnregisteredDeviceTokenException(final Date deviceTokenExpirationTimestamp) {
+    public UnregisteredDeviceTokenException(final Instant deviceTokenExpirationTimestamp) {
         super(RejectionReason.UNREGISTERED);
 
         Objects.requireNonNull(deviceTokenExpirationTimestamp, "Device token expiration timestamp must not be null.");
@@ -48,7 +48,7 @@ public class UnregisteredDeviceTokenException extends RejectedNotificationExcept
         this.deviceTokenExpirationTimestamp = deviceTokenExpirationTimestamp;
     }
 
-    Date getDeviceTokenExpirationTimestamp() {
+    Instant getDeviceTokenExpirationTimestamp() {
         return deviceTokenExpirationTimestamp;
     }
 }
diff --git a/pushy/src/main/java/com/eatthepath/pushy/apns/server/ValidatingPushNotificationHandler.java b/pushy/src/main/java/com/eatthepath/pushy/apns/server/ValidatingPushNotificationHandler.java
index 789f2cc9..f4404974 100644
--- a/pushy/src/main/java/com/eatthepath/pushy/apns/server/ValidatingPushNotificationHandler.java
+++ b/pushy/src/main/java/com/eatthepath/pushy/apns/server/ValidatingPushNotificationHandler.java
@@ -22,16 +22,16 @@
 
 package com.eatthepath.pushy.apns.server;
 
-import com.eatthepath.uuid.FastUUID;
 import com.eatthepath.pushy.apns.DeliveryPriority;
 import com.eatthepath.pushy.apns.PushType;
+import com.eatthepath.uuid.FastUUID;
 import io.netty.buffer.ByteBuf;
 import io.netty.handler.codec.http.HttpMethod;
 import io.netty.handler.codec.http2.Http2Headers;
 import io.netty.util.AsciiString;
 
 import java.nio.charset.StandardCharsets;
-import java.util.Date;
+import java.time.Instant;
 import java.util.Map;
 import java.util.Set;
 import java.util.regex.Matcher;
@@ -40,7 +40,7 @@ import java.util.regex.Pattern;
 abstract class ValidatingPushNotificationHandler implements PushNotificationHandler {
 
     private final Map<String, Set<String>> deviceTokensByTopic;
-    private final Map<String, Date> expirationTimestampsByDeviceToken;
+    private final Map<String, Instant> expirationTimestampsByDeviceToken;
 
     private static final String APNS_PATH_PREFIX = "/3/device/";
     private static final AsciiString APNS_TOPIC_HEADER = new AsciiString("apns-topic");
@@ -54,7 +54,7 @@ abstract class ValidatingPushNotificationHandler implements PushNotificationHand
     private static final int MAX_PAYLOAD_SIZE = 4096;
     private static final int MAX_COLLAPSE_ID_SIZE = 64;
 
-    ValidatingPushNotificationHandler(final Map<String, Set<String>> deviceTokensByTopic, final Map<String, Date> expirationTimestampsByDeviceToken) {
+    ValidatingPushNotificationHandler(final Map<String, Set<String>> deviceTokensByTopic, final Map<String, Instant> expirationTimestampsByDeviceToken) {
         this.deviceTokensByTopic = deviceTokensByTopic;
         this.expirationTimestampsByDeviceToken = expirationTimestampsByDeviceToken;
     }
@@ -136,7 +136,7 @@ abstract class ValidatingPushNotificationHandler implements PushNotificationHand
                         throw new RejectedNotificationException(RejectionReason.BAD_DEVICE_TOKEN);
                     }
 
-                    final Date expirationTimestamp = this.expirationTimestampsByDeviceToken.get(deviceToken);
+                    final Instant expirationTimestamp = this.expirationTimestampsByDeviceToken.get(deviceToken);
 
                     if (expirationTimestamp != null) {
                         throw new UnregisteredDeviceTokenException(expirationTimestamp);
diff --git a/pushy/src/main/java/com/eatthepath/pushy/apns/server/ValidatingPushNotificationHandlerFactory.java b/pushy/src/main/java/com/eatthepath/pushy/apns/server/ValidatingPushNotificationHandlerFactory.java
index ff5b4d96..f44d5114 100644
--- a/pushy/src/main/java/com/eatthepath/pushy/apns/server/ValidatingPushNotificationHandlerFactory.java
+++ b/pushy/src/main/java/com/eatthepath/pushy/apns/server/ValidatingPushNotificationHandlerFactory.java
@@ -26,8 +26,8 @@ import com.eatthepath.pushy.apns.auth.ApnsVerificationKey;
 
 import javax.net.ssl.SSLPeerUnverifiedException;
 import javax.net.ssl.SSLSession;
+import java.time.Instant;
 import java.util.Collections;
-import java.util.Date;
 import java.util.Map;
 import java.util.Set;
 import java.util.regex.Matcher;
@@ -51,7 +51,7 @@ import java.util.regex.Pattern;
 public class ValidatingPushNotificationHandlerFactory implements PushNotificationHandlerFactory {
 
     private final Map<String, Set<String>> deviceTokensByTopic;
-    private final Map<String, Date> expirationTimestampsByDeviceToken;
+    private final Map<String, Instant> expirationTimestampsByDeviceToken;
 
     private final Map<String, ApnsVerificationKey> verificationKeysByKeyId;
     private final Map<ApnsVerificationKey, Set<String>> topicsByVerificationKey;
@@ -70,7 +70,7 @@ public class ValidatingPushNotificationHandlerFactory implements PushNotificatio
      * authentication tokens; only needed for token authentication, and may be {@code null} in which case all
      * notifications sent with token authentication will be rejected
      */
-    public ValidatingPushNotificationHandlerFactory(final Map<String, Set<String>> deviceTokensByTopic, final Map<String, Date> expirationTimestampsByDeviceToken, final Map<String, ApnsVerificationKey> verificationKeysByKeyId, final Map<ApnsVerificationKey, Set<String>> topicsByVerificationKey) {
+    public ValidatingPushNotificationHandlerFactory(final Map<String, Set<String>> deviceTokensByTopic, final Map<String, Instant> expirationTimestampsByDeviceToken, final Map<String, ApnsVerificationKey> verificationKeysByKeyId, final Map<ApnsVerificationKey, Set<String>> topicsByVerificationKey) {
         this.deviceTokensByTopic = deviceTokensByTopic != null ?
                 deviceTokensByTopic : Collections.emptyMap();
 
diff --git a/pushy/src/main/java/com/eatthepath/pushy/apns/util/DateAsTimeSinceEpochTypeAdapter.java b/pushy/src/main/java/com/eatthepath/pushy/apns/util/InstantAsTimeSinceEpochTypeAdapter.java
similarity index 73%
rename from pushy/src/main/java/com/eatthepath/pushy/apns/util/DateAsTimeSinceEpochTypeAdapter.java
rename to pushy/src/main/java/com/eatthepath/pushy/apns/util/InstantAsTimeSinceEpochTypeAdapter.java
index 59a6dde5..436cbfe0 100644
--- a/pushy/src/main/java/com/eatthepath/pushy/apns/util/DateAsTimeSinceEpochTypeAdapter.java
+++ b/pushy/src/main/java/com/eatthepath/pushy/apns/util/InstantAsTimeSinceEpochTypeAdapter.java
@@ -25,16 +25,16 @@ package com.eatthepath.pushy.apns.util;
 import com.google.gson.*;
 
 import java.lang.reflect.Type;
-import java.util.Date;
+import java.time.Instant;
 import java.util.Objects;
 import java.util.concurrent.TimeUnit;
 
 /**
- * Converts {@link java.util.Date} instances in JSON objects to or from timestamps since the epoch.
+ * Converts {@link java.time.Instant} instances in JSON objects to or from timestamps since the epoch.
  *
  * @author <a href="https://github.com/jchambers">Jon Chambers</a>
  */
-public class DateAsTimeSinceEpochTypeAdapter implements JsonSerializer<Date>, JsonDeserializer<Date> {
+public class InstantAsTimeSinceEpochTypeAdapter implements JsonSerializer<Instant>, JsonDeserializer<Instant> {
 
     private final TimeUnit timeUnit;
 
@@ -43,32 +43,32 @@ public class DateAsTimeSinceEpochTypeAdapter implements JsonSerializer<Date>, Js
      *
      * @param timeUnit the time unit in which to express timestamps
      */
-    public DateAsTimeSinceEpochTypeAdapter(final TimeUnit timeUnit) {
+    public InstantAsTimeSinceEpochTypeAdapter(final TimeUnit timeUnit) {
         Objects.requireNonNull(timeUnit);
         this.timeUnit = timeUnit;
     }
 
     @Override
-    public Date deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context) throws JsonParseException {
-        final Date date;
+    public Instant deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context) throws JsonParseException {
+        final Instant instant;
 
         if (json.isJsonPrimitive()) {
-            date = new Date(this.timeUnit.toMillis(json.getAsLong()));
+            instant = Instant.ofEpochMilli(this.timeUnit.toMillis(json.getAsLong()));
         } else if (json.isJsonNull()) {
-            date = null;
+            instant = null;
         } else {
             throw new JsonParseException("Dates represented as time since the epoch must either be numbers or null.");
         }
 
-        return date;
+        return instant;
     }
 
     @Override
-    public JsonElement serialize(final Date src, final Type typeOfSrc, final JsonSerializationContext context) {
+    public JsonElement serialize(final Instant src, final Type typeOfSrc, final JsonSerializationContext context) {
         final JsonElement element;
 
         if (src != null) {
-            element = new JsonPrimitive(this.timeUnit.convert(src.getTime(), TimeUnit.MILLISECONDS));
+            element = new JsonPrimitive(this.timeUnit.convert(src.toEpochMilli(), TimeUnit.MILLISECONDS));
         } else {
             element = JsonNull.INSTANCE;
         }
diff --git a/pushy/src/main/java/com/eatthepath/pushy/apns/util/SimpleApnsPushNotification.java b/pushy/src/main/java/com/eatthepath/pushy/apns/util/SimpleApnsPushNotification.java
index 159f452c..136ef28f 100644
--- a/pushy/src/main/java/com/eatthepath/pushy/apns/util/SimpleApnsPushNotification.java
+++ b/pushy/src/main/java/com/eatthepath/pushy/apns/util/SimpleApnsPushNotification.java
@@ -22,15 +22,15 @@
 
 package com.eatthepath.pushy.apns.util;
 
-import com.eatthepath.uuid.FastUUID;
 import com.eatthepath.pushy.apns.ApnsPushNotification;
 import com.eatthepath.pushy.apns.DeliveryPriority;
 import com.eatthepath.pushy.apns.PushType;
+import com.eatthepath.uuid.FastUUID;
 
-import java.util.Date;
+import java.time.Duration;
+import java.time.Instant;
 import java.util.Objects;
 import java.util.UUID;
-import java.util.concurrent.TimeUnit;
 
 /**
  * A simple and immutable implementation of the {@link ApnsPushNotification} interface.
@@ -43,7 +43,7 @@ public class SimpleApnsPushNotification implements ApnsPushNotification {
 
     private final String token;
     private final String payload;
-    private final Date invalidationTime;
+    private final Instant invalidationTime;
     private final DeliveryPriority priority;
     private final PushType pushType;
     private final String topic;
@@ -53,7 +53,7 @@ public class SimpleApnsPushNotification implements ApnsPushNotification {
     /**
      * The default expiration period for push notifications (one day).
      */
-    public static final long DEFAULT_EXPIRATION_PERIOD_MILLIS = TimeUnit.DAYS.toMillis(1);
+    public static final Duration DEFAULT_EXPIRATION_PERIOD = Duration.ofDays(1);
 
     /**
      * Constructs a new push notification with the given token, topic, and payload. A default expiration time is set for
@@ -67,10 +67,11 @@ public class SimpleApnsPushNotification implements ApnsPushNotification {
      * @param payload the payload to include in this push notification
      *
      * @see DeliveryPriority#IMMEDIATE
-     * @see #DEFAULT_EXPIRATION_PERIOD_MILLIS
+     * @see #DEFAULT_EXPIRATION_PERIOD
      */
     public SimpleApnsPushNotification(final String token, final String topic, final String payload) {
-        this(token, topic, payload, new Date(System.currentTimeMillis() + DEFAULT_EXPIRATION_PERIOD_MILLIS), DeliveryPriority.IMMEDIATE, null, null, null);
+
+        this(token, topic, payload, Instant.now().plus(DEFAULT_EXPIRATION_PERIOD), DeliveryPriority.IMMEDIATE, null, null, null);
     }
 
     /**
@@ -86,7 +87,7 @@ public class SimpleApnsPushNotification implements ApnsPushNotification {
      *
      * @see DeliveryPriority#IMMEDIATE
      */
-    public SimpleApnsPushNotification(final String token, final String topic, final String payload, final Date invalidationTime) {
+    public SimpleApnsPushNotification(final String token, final String topic, final String payload, final Instant invalidationTime) {
         this(token, topic, payload, invalidationTime, DeliveryPriority.IMMEDIATE, null, null, null);
     }
 
@@ -101,7 +102,7 @@ public class SimpleApnsPushNotification implements ApnsPushNotification {
      * {@code null}, no delivery attempts beyond the first will be made
      * @param priority the priority with which this notification should be delivered to the receiving device
      */
-    public SimpleApnsPushNotification(final String token, final String topic, final String payload, final Date invalidationTime, final DeliveryPriority priority) {
+    public SimpleApnsPushNotification(final String token, final String topic, final String payload, final Instant invalidationTime, final DeliveryPriority priority) {
         this(token, topic, payload, invalidationTime, priority, null, null, null);
     }
 
@@ -117,7 +118,7 @@ public class SimpleApnsPushNotification implements ApnsPushNotification {
      * @param pushType the type of notification to be sent
      * @param priority the priority with which this notification should be delivered to the receiving device
      */
-    public SimpleApnsPushNotification(final String token, final String topic, final String payload, final Date invalidationTime, final DeliveryPriority priority, final PushType pushType) {
+    public SimpleApnsPushNotification(final String token, final String topic, final String payload, final Instant invalidationTime, final DeliveryPriority priority, final PushType pushType) {
         this(token, topic, payload, invalidationTime, priority, pushType, null, null);
     }
 
@@ -134,7 +135,7 @@ public class SimpleApnsPushNotification implements ApnsPushNotification {
      * @param collapseId the "collapse identifier" for this notification, which allows it to supersede or be superseded
      * by other notifications with the same collapse identifier
      */
-    public SimpleApnsPushNotification(final String token, final String topic, final String payload, final Date invalidationTime, final DeliveryPriority priority, final String collapseId) {
+    public SimpleApnsPushNotification(final String token, final String topic, final String payload, final Instant invalidationTime, final DeliveryPriority priority, final String collapseId) {
         this(token, topic, payload, invalidationTime, priority, null, collapseId, null);
     }
 
@@ -152,7 +153,7 @@ public class SimpleApnsPushNotification implements ApnsPushNotification {
      * @param collapseId the "collapse identifier" for this notification, which allows it to supersede or be superseded
      * by other notifications with the same collapse identifier
      */
-    public SimpleApnsPushNotification(final String token, final String topic, final String payload, final Date invalidationTime, final DeliveryPriority priority, final PushType pushType, final String collapseId) {
+    public SimpleApnsPushNotification(final String token, final String topic, final String payload, final Instant invalidationTime, final DeliveryPriority priority, final PushType pushType, final String collapseId) {
         this(token, topic, payload, invalidationTime, priority, pushType, collapseId, null);
     }
 
@@ -171,7 +172,7 @@ public class SimpleApnsPushNotification implements ApnsPushNotification {
      * @param apnsId the unique identifier for this notification; may be {@code null}, in which case the APNs server
      * will assign a unique identifier automatically
      */
-    public SimpleApnsPushNotification(final String token, final String topic, final String payload, final Date invalidationTime, final DeliveryPriority priority, final String collapseId, final UUID apnsId) {
+    public SimpleApnsPushNotification(final String token, final String topic, final String payload, final Instant invalidationTime, final DeliveryPriority priority, final String collapseId, final UUID apnsId) {
         this(token, topic, payload, invalidationTime, priority, null, collapseId, apnsId);
     }
 
@@ -191,7 +192,7 @@ public class SimpleApnsPushNotification implements ApnsPushNotification {
      * @param apnsId the unique identifier for this notification; may be {@code null}, in which case the APNs server
      * will assign a unique identifier automatically
      */
-    public SimpleApnsPushNotification(final String token, final String topic, final String payload, final Date invalidationTime, final DeliveryPriority priority, final PushType pushType, final String collapseId, final UUID apnsId) {
+    public SimpleApnsPushNotification(final String token, final String topic, final String payload, final Instant invalidationTime, final DeliveryPriority priority, final PushType pushType, final String collapseId, final UUID apnsId) {
         this.token = Objects.requireNonNull(token, "Destination device token must not be null.");
         this.topic = Objects.requireNonNull(topic, "Destination topic must not be null.");
         this.payload = Objects.requireNonNull(payload, "Payload must not be null.");
@@ -202,7 +203,6 @@ public class SimpleApnsPushNotification implements ApnsPushNotification {
         this.apnsId = apnsId;
     }
 
-
     /**
      * Returns the token of the device to which this push notification should be delivered.
      *
@@ -229,7 +229,7 @@ public class SimpleApnsPushNotification implements ApnsPushNotification {
      * @return the time at which this push notification is no longer valid and should no longer be delivered
      */
     @Override
-    public Date getExpiration() {
+    public Instant getExpiration() {
         return this.invalidationTime;
     }
 


Test patch:
diff --git a/pushy/src/test/java/com/eatthepath/pushy/apns/AbstractClientServerTest.java b/pushy/src/test/java/com/eatthepath/pushy/apns/AbstractClientServerTest.java
index 4740ee4c..aa5f43c8 100644
--- a/pushy/src/test/java/com/eatthepath/pushy/apns/AbstractClientServerTest.java
+++ b/pushy/src/test/java/com/eatthepath/pushy/apns/AbstractClientServerTest.java
@@ -42,6 +42,7 @@ import java.io.InputStream;
 import java.security.KeyPair;
 import java.security.interfaces.ECPrivateKey;
 import java.security.interfaces.ECPublicKey;
+import java.time.Instant;
 import java.util.*;
 
 public class AbstractClientServerTest {
@@ -68,7 +69,7 @@ public class AbstractClientServerTest {
     protected static final Map<String, Set<String>> DEVICE_TOKENS_BY_TOPIC =
             Collections.singletonMap(TOPIC, Collections.singleton(DEVICE_TOKEN));
 
-    protected static final Map<String, Date> EXPIRATION_TIMESTAMPS_BY_DEVICE_TOKEN = Collections.emptyMap();
+    protected static final Map<String, Instant> EXPIRATION_TIMESTAMPS_BY_DEVICE_TOKEN = Collections.emptyMap();
 
     protected static final int TOKEN_LENGTH = 32; // bytes
 
diff --git a/pushy/src/test/java/com/eatthepath/pushy/apns/ApnsClientTest.java b/pushy/src/test/java/com/eatthepath/pushy/apns/ApnsClientTest.java
index 4fd91352..8818c6ec 100644
--- a/pushy/src/test/java/com/eatthepath/pushy/apns/ApnsClientTest.java
+++ b/pushy/src/test/java/com/eatthepath/pushy/apns/ApnsClientTest.java
@@ -32,8 +32,8 @@ import org.junit.runner.RunWith;
 
 import javax.net.ssl.SSLHandshakeException;
 import java.io.InputStream;
+import java.time.Instant;
 import java.util.ArrayList;
-import java.util.Date;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -183,7 +183,7 @@ public class ApnsClientTest extends AbstractClientServerTest {
         }
 
         @Override
-        public void handlePushNotificationRejected(final ApnsPushNotification pushNotification, final RejectionReason rejectionReason, final Date deviceTokenExpirationTimestamp) {
+        public void handlePushNotificationRejected(final ApnsPushNotification pushNotification, final RejectionReason rejectionReason, final Instant deviceTokenExpirationTimestamp) {
             synchronized (this.rejectedNotifications) {
                 this.rejectedNotifications.add(pushNotification);
                 this.rejectedNotifications.notifyAll();
@@ -482,7 +482,7 @@ public class ApnsClientTest extends AbstractClientServerTest {
     @Test
     @Parameters({"true", "false"})
     public void testSendNotificationWithExpiredDeviceToken(final boolean useTokenAuthentication) throws Exception {
-        final Date expiration = new Date();
+        final Instant expiration = Instant.now();
 
         final PushNotificationHandlerFactory handlerFactory = sslSession -> (headers, payload) -> {
             throw new UnregisteredDeviceTokenException(expiration);
diff --git a/pushy/src/test/java/com/eatthepath/pushy/apns/DateAsTimeSinceEpochTypeAdapterTest.java b/pushy/src/test/java/com/eatthepath/pushy/apns/DateAsTimeSinceEpochTypeAdapterTest.java
deleted file mode 100644
index d5fa1d57..00000000
--- a/pushy/src/test/java/com/eatthepath/pushy/apns/DateAsTimeSinceEpochTypeAdapterTest.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Copyright (c) 2020 Jon Chambers
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
-
-package com.eatthepath.pushy.apns;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-
-import java.util.Date;
-import java.util.concurrent.TimeUnit;
-
-import com.eatthepath.pushy.apns.util.DateAsTimeSinceEpochTypeAdapter;
-import org.junit.Test;
-
-import com.google.gson.JsonNull;
-import com.google.gson.JsonPrimitive;
-
-public class DateAsTimeSinceEpochTypeAdapterTest {
-
-    @Test(expected = NullPointerException.class)
-    public void testDateAsTimeSinceEpochTypeAdapterNullUnits() {
-        new DateAsTimeSinceEpochTypeAdapter(null);
-    }
-
-    @Test
-    public void testDeserialize() {
-        {
-            final DateAsTimeSinceEpochTypeAdapter adapter = new DateAsTimeSinceEpochTypeAdapter(TimeUnit.MILLISECONDS);
-            assertNull(adapter.deserialize(JsonNull.INSTANCE, Date.class, null));
-        }
-
-        {
-            final DateAsTimeSinceEpochTypeAdapter adapter = new DateAsTimeSinceEpochTypeAdapter(TimeUnit.MILLISECONDS);
-
-            final long timestampInMilliseconds = System.currentTimeMillis();
-            final Date dateFromTimestamp = new Date(timestampInMilliseconds);
-
-            assertEquals(dateFromTimestamp, adapter.deserialize(new JsonPrimitive(timestampInMilliseconds), Date.class, null));
-        }
-
-        {
-            final DateAsTimeSinceEpochTypeAdapter adapter = new DateAsTimeSinceEpochTypeAdapter(TimeUnit.SECONDS);
-
-            final long timestampInSeconds = System.currentTimeMillis() / 1000;
-            final Date dateFromTimestamp = new Date(timestampInSeconds * 1000);
-
-            assertEquals(dateFromTimestamp, adapter.deserialize(new JsonPrimitive(timestampInSeconds), Date.class, null));
-        }
-
-    }
-
-    @Test
-    public void testSerialize() {
-        {
-            final DateAsTimeSinceEpochTypeAdapter adapter = new DateAsTimeSinceEpochTypeAdapter(TimeUnit.MILLISECONDS);
-            assertEquals(JsonNull.INSTANCE, adapter.serialize(null, Date.class, null));
-        }
-
-        {
-            final DateAsTimeSinceEpochTypeAdapter adapter = new DateAsTimeSinceEpochTypeAdapter(TimeUnit.MILLISECONDS);
-
-            final long timestampInMilliseconds = System.currentTimeMillis();
-            final Date dateFromTimestamp = new Date(timestampInMilliseconds);
-
-            assertEquals(new JsonPrimitive(timestampInMilliseconds), adapter.serialize(dateFromTimestamp, Date.class, null));
-        }
-
-        {
-            final DateAsTimeSinceEpochTypeAdapter adapter = new DateAsTimeSinceEpochTypeAdapter(TimeUnit.SECONDS);
-
-            final long timestampInSeconds = System.currentTimeMillis() / 1000;
-            final Date dateFromTimestamp = new Date(timestampInSeconds * 1000);
-
-            assertEquals(new JsonPrimitive(timestampInSeconds), adapter.serialize(dateFromTimestamp, Date.class, null));
-        }
-    }
-}
diff --git a/pushy/src/test/java/com/eatthepath/pushy/apns/InstantAsTimeSinceEpochTypeAdapterTest.java b/pushy/src/test/java/com/eatthepath/pushy/apns/InstantAsTimeSinceEpochTypeAdapterTest.java
new file mode 100644
index 00000000..cf8b02ce
--- /dev/null
+++ b/pushy/src/test/java/com/eatthepath/pushy/apns/InstantAsTimeSinceEpochTypeAdapterTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2020 Jon Chambers
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package com.eatthepath.pushy.apns;
+
+import com.eatthepath.pushy.apns.util.InstantAsTimeSinceEpochTypeAdapter;
+import com.google.gson.JsonNull;
+import com.google.gson.JsonPrimitive;
+import org.junit.Test;
+
+import java.time.Instant;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+public class InstantAsTimeSinceEpochTypeAdapterTest {
+
+    @Test(expected = NullPointerException.class)
+    public void testInstantAsTimeSinceEpochTypeAdapterNullUnits() {
+        new InstantAsTimeSinceEpochTypeAdapter(null);
+    }
+
+    @Test
+    public void testDeserialize() {
+        {
+            final InstantAsTimeSinceEpochTypeAdapter adapter = new InstantAsTimeSinceEpochTypeAdapter(TimeUnit.MILLISECONDS);
+            assertNull(adapter.deserialize(JsonNull.INSTANCE, Instant.class, null));
+        }
+
+        {
+            final InstantAsTimeSinceEpochTypeAdapter adapter = new InstantAsTimeSinceEpochTypeAdapter(TimeUnit.MILLISECONDS);
+
+            final long timestampInMilliseconds = Instant.now().toEpochMilli();
+            final Instant instantFromTimestamp = Instant.ofEpochMilli(timestampInMilliseconds);
+
+            assertEquals(instantFromTimestamp, adapter.deserialize(new JsonPrimitive(timestampInMilliseconds), Instant.class, null));
+        }
+
+        {
+            final InstantAsTimeSinceEpochTypeAdapter adapter = new InstantAsTimeSinceEpochTypeAdapter(TimeUnit.SECONDS);
+
+            final long timestampInSeconds = Instant.now().getEpochSecond();
+            final Instant instantFromTimestamp = Instant.ofEpochSecond(timestampInSeconds);
+
+            assertEquals(instantFromTimestamp, adapter.deserialize(new JsonPrimitive(timestampInSeconds), Instant.class, null));
+        }
+
+    }
+
+    @Test
+    public void testSerialize() {
+        {
+            final InstantAsTimeSinceEpochTypeAdapter adapter = new InstantAsTimeSinceEpochTypeAdapter(TimeUnit.MILLISECONDS);
+            assertEquals(JsonNull.INSTANCE, adapter.serialize(null, Instant.class, null));
+        }
+
+        {
+            final InstantAsTimeSinceEpochTypeAdapter adapter = new InstantAsTimeSinceEpochTypeAdapter(TimeUnit.MILLISECONDS);
+
+            final long timestampInMilliseconds = Instant.now().toEpochMilli();
+            final Instant instantFromTimestamp = Instant.ofEpochMilli(timestampInMilliseconds);
+
+            assertEquals(new JsonPrimitive(timestampInMilliseconds), adapter.serialize(instantFromTimestamp, Instant.class, null));
+        }
+
+        {
+            final InstantAsTimeSinceEpochTypeAdapter adapter = new InstantAsTimeSinceEpochTypeAdapter(TimeUnit.SECONDS);
+
+            final long timestampInSeconds = Instant.now().getEpochSecond();
+            final Instant instantFromTimestamp = Instant.ofEpochSecond(timestampInSeconds);
+
+            assertEquals(new JsonPrimitive(timestampInSeconds), adapter.serialize(instantFromTimestamp, Instant.class, null));
+        }
+    }
+}
diff --git a/pushy/src/test/java/com/eatthepath/pushy/apns/auth/AuthenticationTokenTest.java b/pushy/src/test/java/com/eatthepath/pushy/apns/auth/AuthenticationTokenTest.java
index c8f7af23..93401f36 100644
--- a/pushy/src/test/java/com/eatthepath/pushy/apns/auth/AuthenticationTokenTest.java
+++ b/pushy/src/test/java/com/eatthepath/pushy/apns/auth/AuthenticationTokenTest.java
@@ -28,7 +28,7 @@ import org.junit.Test;
 import java.security.KeyPair;
 import java.security.interfaces.ECPrivateKey;
 import java.security.interfaces.ECPublicKey;
-import java.util.Date;
+import java.time.Instant;
 import java.util.regex.Pattern;
 
 import static org.junit.Assert.*;
@@ -52,12 +52,12 @@ public class AuthenticationTokenTest {
     @Test
     public void testAuthenticationTokenFromSigningKey() throws Exception {
         // We're happy here as long as nothing explodes
-        new AuthenticationToken(this.signingKey, new Date());
+        new AuthenticationToken(this.signingKey, Instant.now());
     }
 
     @Test
     public void testAuthenticationTokenFromString() throws Exception {
-        final String base64EncodedToken = new AuthenticationToken(this.signingKey, new Date()).toString();
+        final String base64EncodedToken = new AuthenticationToken(this.signingKey, Instant.now()).toString();
 
         // We're happy here as long as nothing explodes
         new AuthenticationToken(base64EncodedToken);
@@ -65,7 +65,7 @@ public class AuthenticationTokenTest {
 
     @Test
     public void testGetIssuedAt() throws Exception {
-        final Date now = new Date();
+        final Instant now = Instant.now();
         final AuthenticationToken token = new AuthenticationToken(this.signingKey, now);
 
         assertEquals(token.getIssuedAt(), now);
@@ -73,7 +73,7 @@ public class AuthenticationTokenTest {
 
     @Test
     public void testVerifySignature() throws Exception {
-        final AuthenticationToken token = new AuthenticationToken(this.signingKey, new Date());
+        final AuthenticationToken token = new AuthenticationToken(this.signingKey, Instant.now());
 
         assertTrue(token.verifySignature(this.verificationKey));
 
@@ -105,7 +105,7 @@ public class AuthenticationTokenTest {
 
     @Test
     public void testToString() throws Exception {
-        final AuthenticationToken token = new AuthenticationToken(this.signingKey, new Date());
+        final AuthenticationToken token = new AuthenticationToken(this.signingKey, Instant.now());
 
         assertTrue(Pattern.matches("^[a-zA-Z0-9_\\-]+\\.[a-zA-Z0-9_\\-]+\\.[a-zA-Z0-9_\\-]+$", token.toString()));
     }
diff --git a/pushy/src/test/java/com/eatthepath/pushy/apns/server/MockApnsServerTest.java b/pushy/src/test/java/com/eatthepath/pushy/apns/server/MockApnsServerTest.java
index 2348480f..51d41957 100644
--- a/pushy/src/test/java/com/eatthepath/pushy/apns/server/MockApnsServerTest.java
+++ b/pushy/src/test/java/com/eatthepath/pushy/apns/server/MockApnsServerTest.java
@@ -28,7 +28,7 @@ import io.netty.channel.nio.NioEventLoopGroup;
 import io.netty.util.concurrent.Future;
 import org.junit.Test;
 
-import java.util.Date;
+import java.time.Instant;
 import java.util.UUID;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -44,7 +44,7 @@ public class MockApnsServerTest extends AbstractClientServerTest {
 
         private ApnsPushNotification mostRecentPushNotification;
         private RejectionReason mostRecentRejectionReason;
-        private Date mostRecentDeviceTokenExpiration;
+        private Instant mostRecentDeviceTokenExpiration;
 
         @Override
         public void handlePushNotificationAccepted(final ApnsPushNotification pushNotification) {
@@ -68,7 +68,7 @@ public class MockApnsServerTest extends AbstractClientServerTest {
         }
 
         @Override
-        public void handlePushNotificationRejected(final ApnsPushNotification pushNotification, final RejectionReason rejectionReason, final Date deviceTokenExpirationTimestamp) {
+        public void handlePushNotificationRejected(final ApnsPushNotification pushNotification, final RejectionReason rejectionReason, final Instant deviceTokenExpirationTimestamp) {
             this.mostRecentPushNotification = pushNotification;
             this.mostRecentRejectionReason = rejectionReason;
             this.mostRecentDeviceTokenExpiration = deviceTokenExpirationTimestamp;
@@ -217,7 +217,7 @@ public class MockApnsServerTest extends AbstractClientServerTest {
     @Test
     public void testListenerRejectedNotificationWithExpiration() throws Exception {
         final TestMockApnsServerListener listener = new TestMockApnsServerListener();
-        final Date expiration = new Date();
+        final Instant expiration = Instant.now();
 
         final MockApnsServer server = this.buildServer(sslSession -> (headers, payload) -> {
             throw new UnregisteredDeviceTokenException(expiration);
diff --git a/pushy/src/test/java/com/eatthepath/pushy/apns/server/ParsingMockApnsServerListenerAdapterTest.java b/pushy/src/test/java/com/eatthepath/pushy/apns/server/ParsingMockApnsServerListenerAdapterTest.java
index 57ac2eed..7a5f6cb7 100644
--- a/pushy/src/test/java/com/eatthepath/pushy/apns/server/ParsingMockApnsServerListenerAdapterTest.java
+++ b/pushy/src/test/java/com/eatthepath/pushy/apns/server/ParsingMockApnsServerListenerAdapterTest.java
@@ -32,7 +32,7 @@ import io.netty.handler.codec.http2.Http2Headers;
 import io.netty.util.AsciiString;
 import org.junit.Test;
 
-import java.util.Date;
+import java.time.Instant;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
@@ -49,7 +49,7 @@ public class ParsingMockApnsServerListenerAdapterTest {
 
         private ApnsPushNotification mostRecentPushNotification;
         private RejectionReason mostRecentRejectionReason;
-        private Date mostRecentDeviceTokenExpiration;
+        private Instant mostRecentDeviceTokenExpiration;
 
         @Override
         public void handlePushNotificationAccepted(final ApnsPushNotification pushNotification) {
@@ -59,7 +59,7 @@ public class ParsingMockApnsServerListenerAdapterTest {
         }
 
         @Override
-        public void handlePushNotificationRejected(final ApnsPushNotification pushNotification, final RejectionReason rejectionReason, final Date deviceTokenExpirationTimestamp) {
+        public void handlePushNotificationRejected(final ApnsPushNotification pushNotification, final RejectionReason rejectionReason, final Instant deviceTokenExpirationTimestamp) {
             this.mostRecentPushNotification = pushNotification;
             this.mostRecentRejectionReason = rejectionReason;
             this.mostRecentDeviceTokenExpiration = deviceTokenExpirationTimestamp;
@@ -116,9 +116,9 @@ public class ParsingMockApnsServerListenerAdapterTest {
         }
 
         {
-            final Date expiration = new Date(1_000_000_000);
+            final Instant expiration = Instant.ofEpochMilli(1_000_000_000);
 
-            final Http2Headers headers = new DefaultHttp2Headers().addInt(APNS_EXPIRATION_HEADER, (int) (expiration.getTime() / 1000));
+            final Http2Headers headers = new DefaultHttp2Headers().addInt(APNS_EXPIRATION_HEADER, (int) expiration.getEpochSecond());
 
             listener.handlePushNotificationAccepted(headers, null);
 
@@ -181,7 +181,7 @@ public class ParsingMockApnsServerListenerAdapterTest {
 
         {
             final RejectionReason rejectionReason = RejectionReason.BAD_PRIORITY;
-            final Date deviceTokenExpiration = new Date();
+            final Instant deviceTokenExpiration = Instant.now();
 
             listener.handlePushNotificationRejected(new DefaultHttp2Headers(), null, rejectionReason, deviceTokenExpiration);
             assertEquals(rejectionReason, listener.mostRecentRejectionReason);
diff --git a/pushy/src/test/java/com/eatthepath/pushy/apns/server/TlsAuthenticationValidatingPushNotificationHandlerTest.java b/pushy/src/test/java/com/eatthepath/pushy/apns/server/TlsAuthenticationValidatingPushNotificationHandlerTest.java
index e36db2ff..e1c66633 100644
--- a/pushy/src/test/java/com/eatthepath/pushy/apns/server/TlsAuthenticationValidatingPushNotificationHandlerTest.java
+++ b/pushy/src/test/java/com/eatthepath/pushy/apns/server/TlsAuthenticationValidatingPushNotificationHandlerTest.java
@@ -24,14 +24,14 @@ package com.eatthepath.pushy.apns.server;
 
 import io.netty.handler.codec.http2.Http2Headers;
 
-import java.util.Date;
+import java.time.Instant;
 import java.util.Map;
 import java.util.Set;
 
 public class TlsAuthenticationValidatingPushNotificationHandlerTest extends ValidatingPushNotificationHandlerTest {
 
     @Override
-    protected ValidatingPushNotificationHandler getHandler(final Map<String, Set<String>> deviceTokensByTopic, final Map<String, Date> expirationTimestampsByDeviceToken) {
+    protected ValidatingPushNotificationHandler getHandler(final Map<String, Set<String>> deviceTokensByTopic, final Map<String, Instant> expirationTimestampsByDeviceToken) {
         return new TlsAuthenticationValidatingPushNotificationHandler(deviceTokensByTopic, expirationTimestampsByDeviceToken, TOPIC);
     }
 
diff --git a/pushy/src/test/java/com/eatthepath/pushy/apns/server/TokenAuthenticationValidatingPushNotificationHandlerTest.java b/pushy/src/test/java/com/eatthepath/pushy/apns/server/TokenAuthenticationValidatingPushNotificationHandlerTest.java
index bd0e3de8..25700965 100644
--- a/pushy/src/test/java/com/eatthepath/pushy/apns/server/TokenAuthenticationValidatingPushNotificationHandlerTest.java
+++ b/pushy/src/test/java/com/eatthepath/pushy/apns/server/TokenAuthenticationValidatingPushNotificationHandlerTest.java
@@ -34,8 +34,8 @@ import org.junit.Test;
 import java.security.KeyPair;
 import java.security.interfaces.ECPrivateKey;
 import java.security.interfaces.ECPublicKey;
+import java.time.Instant;
 import java.util.Collections;
-import java.util.Date;
 import java.util.Map;
 import java.util.Set;
 
@@ -61,7 +61,7 @@ public class TokenAuthenticationValidatingPushNotificationHandlerTest extends Va
     }
 
     @Override
-    protected TokenAuthenticationValidatingPushNotificationHandler getHandler(final Map<String, Set<String>> deviceTokensByTopic, final Map<String, Date> expirationTimestampsByDeviceToken) {
+    protected TokenAuthenticationValidatingPushNotificationHandler getHandler(final Map<String, Set<String>> deviceTokensByTopic, final Map<String, Instant> expirationTimestampsByDeviceToken) {
         final Map<String, ApnsVerificationKey> verificationKeysByKeyId =
                 Collections.singletonMap(KEY_ID, this.verificationKey);
 
@@ -73,7 +73,7 @@ public class TokenAuthenticationValidatingPushNotificationHandlerTest extends Va
 
     @Override
     protected void addAcceptableCredentialsToHeaders(final Http2Headers headers) throws Exception {
-        final AuthenticationToken authenticationToken = new AuthenticationToken(this.signingKey, new Date());
+        final AuthenticationToken authenticationToken = new AuthenticationToken(this.signingKey, Instant.now());
 
         headers.set(APNS_AUTHORIZATION_HEADER, authenticationToken.getAuthorizationHeader());
     }
@@ -139,7 +139,7 @@ public class TokenAuthenticationValidatingPushNotificationHandlerTest extends Va
                 new TokenAuthenticationValidatingPushNotificationHandler(
                         DEVICE_TOKENS_BY_TOPIC, Collections.emptyMap(), verificationKeysByKeyId, topicsByVerificationKey);
 
-        final AuthenticationToken authenticationToken = new AuthenticationToken(signingKey, new Date());
+        final AuthenticationToken authenticationToken = new AuthenticationToken(signingKey, Instant.now());
 
         this.headers.set(APNS_AUTHORIZATION_HEADER, authenticationToken.getAuthorizationHeader());
 
@@ -155,7 +155,7 @@ public class TokenAuthenticationValidatingPushNotificationHandlerTest extends Va
         final KeyPair keyPair = KeyPairUtil.generateKeyPair();
 
         final ApnsSigningKey unverifiedKey = new ApnsSigningKey(KEY_ID, TEAM_ID, (ECPrivateKey) keyPair.getPrivate());
-        final AuthenticationToken unverifiedToken = new AuthenticationToken(unverifiedKey, new Date());
+        final AuthenticationToken unverifiedToken = new AuthenticationToken(unverifiedKey, Instant.now());
 
         this.headers.set(APNS_AUTHORIZATION_HEADER, unverifiedToken.getAuthorizationHeader());
 
@@ -168,7 +168,7 @@ public class TokenAuthenticationValidatingPushNotificationHandlerTest extends Va
 
     @Test
     public void testHandleNotificationWithWithExpiredAuthenticationToken() throws Exception {
-        final AuthenticationToken expiredToken = new AuthenticationToken(this.signingKey, new Date(0));
+        final AuthenticationToken expiredToken = new AuthenticationToken(this.signingKey, Instant.ofEpochMilli(0));
 
         this.headers.set(APNS_AUTHORIZATION_HEADER, expiredToken.getAuthorizationHeader());
 
@@ -190,7 +190,7 @@ public class TokenAuthenticationValidatingPushNotificationHandlerTest extends Va
                 new TokenAuthenticationValidatingPushNotificationHandler(
                         DEVICE_TOKENS_BY_TOPIC, Collections.emptyMap(), verificationKeysByKeyId, topicsByVerificationKey);
 
-        final AuthenticationToken authenticationToken = new AuthenticationToken(signingKey, new Date());
+        final AuthenticationToken authenticationToken = new AuthenticationToken(signingKey, Instant.now());
 
         this.headers.set(APNS_AUTHORIZATION_HEADER, authenticationToken.getAuthorizationHeader());
 
diff --git a/pushy/src/test/java/com/eatthepath/pushy/apns/server/ValidatingPushNotificationHandlerTest.java b/pushy/src/test/java/com/eatthepath/pushy/apns/server/ValidatingPushNotificationHandlerTest.java
index f3c4be43..a1ac4bb2 100644
--- a/pushy/src/test/java/com/eatthepath/pushy/apns/server/ValidatingPushNotificationHandlerTest.java
+++ b/pushy/src/test/java/com/eatthepath/pushy/apns/server/ValidatingPushNotificationHandlerTest.java
@@ -34,6 +34,7 @@ import org.junit.Before;
 import org.junit.Test;
 
 import java.nio.charset.StandardCharsets;
+import java.time.Instant;
 import java.util.*;
 
 import static org.junit.Assert.assertEquals;
@@ -59,7 +60,7 @@ public abstract class ValidatingPushNotificationHandlerTest {
     protected ByteBuf payload;
 
     @SuppressWarnings("SameParameterValue")
-    protected abstract ValidatingPushNotificationHandler getHandler(final Map<String, Set<String>> deviceTokensByTopic, final Map<String, Date> expirationTimestampsByDeviceToken);
+    protected abstract ValidatingPushNotificationHandler getHandler(final Map<String, Set<String>> deviceTokensByTopic, final Map<String, Instant> expirationTimestampsByDeviceToken);
 
     protected abstract void addAcceptableCredentialsToHeaders(Http2Headers headers) throws Exception;
 
@@ -121,7 +122,7 @@ public abstract class ValidatingPushNotificationHandlerTest {
 
     @Test
     public void testHandleNotificationWithExpirationDate() throws Exception {
-        this.headers.setInt(APNS_EXPIRATION_HEADER, (int) (new Date().getTime() / 1000));
+        this.headers.setInt(APNS_EXPIRATION_HEADER, (int) Instant.now().getEpochSecond());
 
         this.getHandler(DEVICE_TOKENS_BY_TOPIC, Collections.emptyMap())
                 .handlePushNotification(this.headers, this.payload);
@@ -197,8 +198,8 @@ public abstract class ValidatingPushNotificationHandlerTest {
 
     @Test
     public void testHandleNotificationWithExpiredDeviceToken() {
-        final Map<String, Date> deviceTokenExpirationDates =
-                Collections.singletonMap(TOKEN, new Date(System.currentTimeMillis() - 1));
+        final Map<String, Instant> deviceTokenExpirationDates =
+                Collections.singletonMap(TOKEN, Instant.now().minusMillis(1));
 
         this.testWithExpectedRejection("Push notifications with an expired device token should be rejected.",
                 this.getHandler(DEVICE_TOKENS_BY_TOPIC, deviceTokenExpirationDates),
diff --git a/pushy/src/test/java/com/eatthepath/pushy/apns/util/SimpleApnsPushNotificationTest.java b/pushy/src/test/java/com/eatthepath/pushy/apns/util/SimpleApnsPushNotificationTest.java
index 5e6c1f11..84763ad9 100644
--- a/pushy/src/test/java/com/eatthepath/pushy/apns/util/SimpleApnsPushNotificationTest.java
+++ b/pushy/src/test/java/com/eatthepath/pushy/apns/util/SimpleApnsPushNotificationTest.java
@@ -26,7 +26,7 @@ import com.eatthepath.pushy.apns.DeliveryPriority;
 import com.eatthepath.pushy.apns.PushType;
 import org.junit.Test;
 
-import java.util.Date;
+import java.time.Instant;
 import java.util.UUID;
 
 import static org.junit.Assert.*;
@@ -39,7 +39,7 @@ public class SimpleApnsPushNotificationTest {
         final String topic = "test-topic";
         final String payload = "{\"test\": true}";
 
-        final Date now = new Date();
+        final Instant now = Instant.now();
 
         final SimpleApnsPushNotification pushNotification =
                 new SimpleApnsPushNotification(token, topic, payload);
@@ -47,7 +47,7 @@ public class SimpleApnsPushNotificationTest {
         assertEquals(token, pushNotification.getToken());
         assertEquals(topic, pushNotification.getTopic());
         assertEquals(payload, pushNotification.getPayload());
-        assertTrue(pushNotification.getExpiration().after(now));
+        assertTrue(pushNotification.getExpiration().isAfter(now));
         assertEquals(DeliveryPriority.IMMEDIATE, pushNotification.getPriority());
         assertNull(pushNotification.getPushType());
         assertNull(pushNotification.getCollapseId());
@@ -73,7 +73,7 @@ public class SimpleApnsPushNotificationTest {
         final String token = "test-token";
         final String topic = "test-topic";
         final String payload = "{\"test\": true}";
-        final Date expiration = new Date();
+        final Instant expiration = Instant.now();
 
         final SimpleApnsPushNotification pushNotification =
                 new SimpleApnsPushNotification(token, topic, payload, expiration);
@@ -93,7 +93,7 @@ public class SimpleApnsPushNotificationTest {
         final String token = "test-token";
         final String topic = "test-topic";
         final String payload = "{\"test\": true}";
-        final Date expiration = new Date();
+        final Instant expiration = Instant.now();
         final DeliveryPriority priority = DeliveryPriority.CONSERVE_POWER;
 
         final SimpleApnsPushNotification pushNotification =
@@ -114,7 +114,7 @@ public class SimpleApnsPushNotificationTest {
         final String token = "test-token";
         final String topic = "test-topic";
         final String payload = "{\"test\": true}";
-        final Date expiration = new Date();
+        final Instant expiration = Instant.now();
         final DeliveryPriority priority = DeliveryPriority.CONSERVE_POWER;
         final PushType pushType = PushType.BACKGROUND;
 
@@ -136,7 +136,7 @@ public class SimpleApnsPushNotificationTest {
         final String token = "test-token";
         final String topic = "test-topic";
         final String payload = "{\"test\": true}";
-        final Date expiration = new Date();
+        final Instant expiration = Instant.now();
         final DeliveryPriority priority = DeliveryPriority.CONSERVE_POWER;
         final String collapseId = "test-collapse-id";
 
@@ -158,7 +158,7 @@ public class SimpleApnsPushNotificationTest {
         final String token = "test-token";
         final String topic = "test-topic";
         final String payload = "{\"test\": true}";
-        final Date expiration = new Date();
+        final Instant expiration = Instant.now();
         final DeliveryPriority priority = DeliveryPriority.CONSERVE_POWER;
         final PushType pushType = PushType.BACKGROUND;
         final String collapseId = "test-collapse-id";
@@ -181,7 +181,7 @@ public class SimpleApnsPushNotificationTest {
         final String token = "test-token";
         final String topic = "test-topic";
         final String payload = "{\"test\": true}";
-        final Date expiration = new Date();
+        final Instant expiration = Instant.now();
         final DeliveryPriority priority = DeliveryPriority.CONSERVE_POWER;
         final PushType pushType = PushType.BACKGROUND;
         final String collapseId = "test-collapse-id";