Web Push notifications allow web applications to receive messages pushed from a server, even when the app isn't actively open in a browser. Here's a comprehensive guide to implementing Web Push in Java.
Architecture Overview
- Service Worker - Handles push events in the browser
- VAPID - Voluntary Application Server Identification for secure messaging
- Web Push Protocol - Standard for sending push messages
- Java Backend - Handles subscription storage and message sending
Dependencies
Maven Dependencies
<dependency> <groupId>nl.martijndwars</groupId> <artifactId>web-push</artifactId> <version>5.1.1</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.0</version> </dependency> <dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.70</version> </dependency>
Gradle Dependencies
implementation 'nl.martijndwars:web-push:5.1.1' implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.0' implementation 'org.bouncycastle:bcprov-jdk15on:1.70'
Core Implementation
Example 1: VAPID Key Generation
import nl.martijndwars.webpush.Utils; import org.bouncycastle.jce.ECNamedCurveTable; import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec; import org.bouncycastle.jce.interfaces.ECPrivateKey; import org.bouncycastle.jce.interfaces.ECPublicKey; import java.security.*; import java.util.Base64; public class VapidKeyGenerator { public static class VapidKeys { private final String publicKey; private final String privateKey; public VapidKeys(String publicKey, String privateKey) { this.publicKey = publicKey; this.privateKey = privateKey; } public String getPublicKey() { return publicKey; } public String getPrivateKey() { return privateKey; } } public static VapidKeys generateVapidKeys() throws Exception { KeyPair keyPair = generateKeyPair(); ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic(); ECPrivateKey privateKey = (ECPrivateKey) keyPair.getPrivate(); String publicKeyBase64 = Base64.getUrlEncoder().withoutPadding() .encodeToString(publicKey.getQ().getEncoded(true)); String privateKeyBase64 = Base64.getUrlEncoder().withoutPadding() .encodeToString(privateKey.getD().toByteArray()); return new VapidKeys(publicKeyBase64, privateKeyBase64); } private static KeyPair generateKeyPair() throws Exception { ECNamedCurveParameterSpec parameterSpec = ECNamedCurveTable.getParameterSpec("prime256v1"); KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("ECDH", "BC"); keyPairGenerator.initialize(parameterSpec); return keyPairGenerator.generateKeyPair(); } public static void main(String[] args) throws Exception { VapidKeys keys = generateVapidKeys(); System.out.println("=== VAPID Keys Generated ==="); System.out.println("Public Key: " + keys.getPublicKey()); System.out.println("Private Key: " + keys.getPrivateKey()); System.out.println("============================"); // Save these keys for your application // Public key goes to your frontend // Private key stays secure on your server } } Example 2: Push Subscription Management
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Base64; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class PushSubscriptionManager { public static class PushSubscription { @JsonProperty("endpoint") private String endpoint; @JsonProperty("keys") private Keys keys; public static class Keys { @JsonProperty("p256dh") private String p256dh; @JsonProperty("auth") private String auth; public Keys() {} public Keys(String p256dh, String auth) { this.p256dh = p256dh; this.auth = auth; } // Getters and setters public String getP256dh() { return p256dh; } public void setP256dh(String p256dh) { this.p256dh = p256dh; } public String getAuth() { return auth; } public void setAuth(String auth) { this.auth = auth; } } public PushSubscription() {} public PushSubscription(String endpoint, Keys keys) { this.endpoint = endpoint; this.keys = keys; } // Getters and setters public String getEndpoint() { return endpoint; } public void setEndpoint(String endpoint) { this.endpoint = endpoint; } public Keys getKeys() { return keys; } public void setKeys(Keys keys) { this.keys = keys; } } private final Map<String, PushSubscription> userSubscriptions; private final ObjectMapper objectMapper; public PushSubscriptionManager() { this.userSubscriptions = new ConcurrentHashMap<>(); this.objectMapper = new ObjectMapper(); } public void saveSubscription(String userId, String subscriptionJson) { try { PushSubscription subscription = objectMapper.readValue(subscriptionJson, PushSubscription.class); userSubscriptions.put(userId, subscription); System.out.println("Subscription saved for user: " + userId); } catch (JsonProcessingException e) { throw new RuntimeException("Invalid subscription JSON", e); } } public void removeSubscription(String userId) { userSubscriptions.remove(userId); System.out.println("Subscription removed for user: " + userId); } public PushSubscription getSubscription(String userId) { return userSubscriptions.get(userId); } public Map<String, PushSubscription> getAllSubscriptions() { return new ConcurrentHashMap<>(userSubscriptions); } // Example usage public static void main(String[] args) { PushSubscriptionManager manager = new PushSubscriptionManager(); String subscriptionJson = """ { "endpoint": "https://fcm.googleapis.com/fcm/send/...", "keys": { "p256dh": "BHc4aJ...", "auth": "8e0wXR..." } } """; manager.saveSubscription("user123", subscriptionJson); PushSubscription subscription = manager.getSubscription("user123"); System.out.println("Retrieved subscription: " + subscription.getEndpoint()); } } Example 3: Web Push Service Implementation
import nl.martijndwars.webpush.Notification; import nl.martijndwars.webpush.PushService; import nl.martijndwars.webpush.Subscription; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.jose4j.lang.JoseException; import java.security.GeneralSecurityException; import java.security.Security; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class WebPushService { private final PushService pushService; private final ExecutorService executorService; private final PushSubscriptionManager subscriptionManager; static { // Add BouncyCastle provider for cryptographic operations Security.addProvider(new BouncyCastleProvider()); } public WebPushService(String publicKey, String privateKey) throws GeneralSecurityException { this.pushService = new PushService(publicKey, privateKey); this.executorService = Executors.newFixedThreadPool(10); this.subscriptionManager = new PushSubscriptionManager(); // Configure push service (optional) this.pushService.setSubject("mailto:[email protected]"); } public CompletableFuture<Void> sendNotificationAsync( String userId, String title, String message) { return CompletableFuture.runAsync(() -> { try { sendNotification(userId, title, message); } catch (Exception e) { throw new RuntimeException("Failed to send notification to user: " + userId, e); } }, executorService); } public void sendNotification(String userId, String title, String message) throws GeneralSecurityException, JoseException, Exception { PushSubscriptionManager.PushSubscription subscription = subscriptionManager.getSubscription(userId); if (subscription == null) { System.out.println("No subscription found for user: " + userId); return; } // Convert to web-push library's Subscription format Subscription.Keys keys = new Subscription.Keys( subscription.getKeys().getP256dh(), subscription.getKeys().getAuth() ); Subscription pushSubscription = new Subscription( subscription.getEndpoint(), keys ); // Create notification payload String payload = createNotificationPayload(title, message); // Create and send notification Notification notification = new Notification(pushSubscription, payload); pushService.send(notification); System.out.println("Notification sent to user: " + userId); } public void broadcastNotification(String title, String message) { subscriptionManager.getAllSubscriptions().keySet().forEach(userId -> { sendNotificationAsync(userId, title, message); }); } private String createNotificationPayload(String title, String message) { // Create JSON payload for the notification return String.format(""" { "title": "%s", "body": "%s", "icon": "/icons/icon-192x192.png", "badge": "/icons/badge-72x72.png", "vibrate": [200, 100, 200], "requireInteraction": true, "actions": [ { "action": "view", "title": "View Details" }, { "action": "dismiss", "title": "Close" } ], "data": { "url": "/notifications", "timestamp": "%d" } } """, title, message, System.currentTimeMillis()); } public void shutdown() { executorService.shutdown(); } // Getters public PushSubscriptionManager getSubscriptionManager() { return subscriptionManager; } } Spring Boot Integration
Example 4: Spring Boot REST Controller
import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.*; @RestController @RequestMapping("/api/push") public class PushNotificationController { private final WebPushService pushService; public PushNotificationController(WebPushService pushService) { this.pushService = pushService; } @PostMapping("/subscribe") public ResponseEntity<?> subscribe( @RequestHeader("X-User-ID") String userId, @RequestBody String subscriptionJson) { try { pushService.getSubscriptionManager().saveSubscription(userId, subscriptionJson); return ResponseEntity.ok().body(Map.of("message", "Subscription saved successfully")); } catch (Exception e) { return ResponseEntity.badRequest().body(Map.of("error", e.getMessage())); } } @PostMapping("/unsubscribe") public ResponseEntity<?> unsubscribe(@RequestHeader("X-User-ID") String userId) { pushService.getSubscriptionManager().removeSubscription(userId); return ResponseEntity.ok().body(Map.of("message", "Unsubscribed successfully")); } @PostMapping("/send") public ResponseEntity<?> sendNotification( @RequestBody SendNotificationRequest request) { try { pushService.sendNotificationAsync( request.getUserId(), request.getTitle(), request.getMessage() ); return ResponseEntity.ok().body(Map.of("message", "Notification sent")); } catch (Exception e) { return ResponseEntity.internalServerError() .body(Map.of("error", "Failed to send notification")); } } @PostMapping("/broadcast") public ResponseEntity<?> broadcastNotification( @RequestBody BroadcastNotificationRequest request) { try { pushService.broadcastNotification(request.getTitle(), request.getMessage()); return ResponseEntity.ok().body(Map.of("message", "Broadcast notification sent")); } catch (Exception e) { return ResponseEntity.internalServerError() .body(Map.of("error", "Failed to send broadcast notification")); } } @GetMapping("/public-key") public ResponseEntity<?> getPublicKey() { // In a real application, this would come from configuration return ResponseEntity.ok().body(Map.of("publicKey", "YOUR_PUBLIC_VAPID_KEY_HERE")); } // Request DTOs public static class SendNotificationRequest { private String userId; private String title; private String message; // Getters and setters public String getUserId() { return userId; } public void setUserId(String userId) { this.userId = userId; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } } public static class BroadcastNotificationRequest { private String title; private String message; // Getters and setters public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } } } Example 5: Spring Configuration
import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.security.Security; @Configuration public class WebPushConfig { static { Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); } @Value("${vapid.public.key}") private String vapidPublicKey; @Value("${vapid.private.key}") private String vapidPrivateKey; @Value("${vapid.subject:mailto:[email protected]}") private String vapidSubject; @Bean public WebPushService webPushService() throws Exception { WebPushService pushService = new WebPushService(vapidPublicKey, vapidPrivateKey); return pushService; } @Bean public PushSubscriptionManager pushSubscriptionManager() { return new PushSubscriptionManager(); } } Advanced Features
Example 6: Notification with Custom Actions and Data
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; public class AdvancedNotificationService { private final WebPushService webPushService; private final ObjectMapper objectMapper; public AdvancedNotificationService(WebPushService webPushService) { this.webPushService = webPushService; this.objectMapper = new ObjectMapper(); } public void sendOrderConfirmation(String userId, String orderId, double amount) { try { String payload = createOrderConfirmationPayload(orderId, amount); // We'd need to modify WebPushService to accept raw payload sendRawNotification(userId, payload); } catch (Exception e) { System.err.println("Failed to send order confirmation: " + e.getMessage()); } } public void sendChatMessage(String userId, String fromUser, String message) { try { String payload = createChatMessagePayload(fromUser, message); sendRawNotification(userId, payload); } catch (Exception e) { System.err.println("Failed to send chat message: " + e.getMessage()); } } public void sendUrgentAlert(String userId, String alertType, String details) { try { String payload = createUrgentAlertPayload(alertType, details); sendRawNotification(userId, payload); } catch (Exception e) { System.err.println("Failed to send urgent alert: " + e.getMessage()); } } private String createOrderConfirmationPayload(String orderId, double amount) throws Exception { ObjectNode payload = objectMapper.createObjectNode(); payload.put("title", "Order Confirmed"); payload.put("body", String.format("Your order #%s for $%.2f has been confirmed", orderId, amount)); payload.put("icon", "/icons/shopping-cart.png"); payload.put("badge", "/icons/notification-badge.png"); ObjectNode data = payload.putObject("data"); data.put("type", "ORDER_CONFIRMATION"); data.put("orderId", orderId); data.put("amount", amount); data.put("url", "/orders/" + orderId); data.put("timestamp", System.currentTimeMillis()); ObjectNode actions = payload.putArray("actions"); ObjectNode trackAction = objectMapper.createObjectNode(); trackAction.put("action", "track"); trackAction.put("title", "Track Order"); actions.add(trackAction); ObjectNode viewAction = objectMapper.createObjectNode(); viewAction.put("action", "view"); viewAction.put("title", "View Details"); actions.add(viewAction); return objectMapper.writeValueAsString(payload); } private String createChatMessagePayload(String fromUser, String message) throws Exception { ObjectNode payload = objectMapper.createObjectNode(); payload.put("title", "New message from " + fromUser); payload.put("body", message.length() > 100 ? message.substring(0, 100) + "..." : message); payload.put("icon", "/icons/chat.png"); payload.put("badge", "/icons/message-badge.png"); payload.put("requireInteraction", true); ObjectNode data = payload.putObject("data"); data.put("type", "CHAT_MESSAGE"); data.put("fromUser", fromUser); data.put("message", message); data.put("url", "/chat/" + fromUser); data.put("timestamp", System.currentTimeMillis()); return objectMapper.writeValueAsString(payload); } private String createUrgentAlertPayload(String alertType, String details) throws Exception { ObjectNode payload = objectMapper.createObjectNode(); payload.put("title", "URGENT: " + alertType); payload.put("body", details); payload.put("icon", "/icons/alert.png"); payload.put("badge", "/icons/alert-badge.png"); payload.put("requireInteraction", true); payload.putArray("vibrate").add(1000).add(500).add(1000); ObjectNode data = payload.putObject("data"); data.put("type", "URGENT_ALERT"); data.put("alertType", alertType); data.put("details", details); data.put("url", "/alerts"); data.put("timestamp", System.currentTimeMillis()); return objectMapper.writeValueAsString(payload); } private void sendRawNotification(String userId, String payload) { // This would require extending WebPushService to handle raw payloads // For now, we'll use the existing method with a simplified approach try { // Extract title and body for basic notification ObjectNode notification = objectMapper.readValue(payload, ObjectNode.class); String title = notification.get("title").asText(); String body = notification.get("body").asText(); webPushService.sendNotificationAsync(userId, title, body); } catch (Exception e) { System.err.println("Failed to send raw notification: " + e.getMessage()); } } } Example 7: Subscription Analytics and Management
import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; public class PushAnalytics { private final Map<String, UserSubscriptionStats> userStats; private final AtomicLong totalNotificationsSent; private final AtomicLong totalNotificationsFailed; public PushAnalytics() { this.userStats = new ConcurrentHashMap<>(); this.totalNotificationsSent = new AtomicLong(0); this.totalNotificationsFailed = new AtomicLong(0); } public void recordNotificationSent(String userId) { userStats.computeIfAbsent(userId, k -> new UserSubscriptionStats()) .incrementSent(); totalNotificationsSent.incrementAndGet(); } public void recordNotificationFailed(String userId) { userStats.computeIfAbsent(userId, k -> new UserSubscriptionStats()) .incrementFailed(); totalNotificationsFailed.incrementAndGet(); } public void recordSubscription(String userId) { userStats.computeIfAbsent(userId, k -> new UserSubscriptionStats()) .setSubscribed(true); } public void recordUnsubscription(String userId) { UserSubscriptionStats stats = userStats.get(userId); if (stats != null) { stats.setSubscribed(false); } } public AnalyticsSnapshot getSnapshot() { long activeSubscriptions = userStats.values().stream() .filter(UserSubscriptionStats::isSubscribed) .count(); long totalSent = totalNotificationsSent.get(); long totalFailed = totalNotificationsFailed.get(); double successRate = totalSent > 0 ? (double) (totalSent - totalFailed) / totalSent * 100 : 0; return new AnalyticsSnapshot(activeSubscriptions, totalSent, totalFailed, successRate); } public Map<String, UserSubscriptionStats> getUserStats() { return new HashMap<>(userStats); } public static class UserSubscriptionStats { private boolean subscribed; private long notificationsSent; private long notificationsFailed; private Date lastNotificationSent; public void incrementSent() { notificationsSent++; lastNotificationSent = new Date(); } public void incrementFailed() { notificationsFailed++; } // Getters and setters public boolean isSubscribed() { return subscribed; } public void setSubscribed(boolean subscribed) { this.subscribed = subscribed; } public long getNotificationsSent() { return notificationsSent; } public long getNotificationsFailed() { return notificationsFailed; } public Date getLastNotificationSent() { return lastNotificationSent; } public double getSuccessRate() { long total = notificationsSent + notificationsFailed; return total > 0 ? (double) notificationsSent / total * 100 : 0; } } public static class AnalyticsSnapshot { private final long activeSubscriptions; private final long totalNotificationsSent; private final long totalNotificationsFailed; private final double successRate; private final Date timestamp; public AnalyticsSnapshot(long activeSubscriptions, long totalSent, long totalFailed, double successRate) { this.activeSubscriptions = activeSubscriptions; this.totalNotificationsSent = totalSent; this.totalNotificationsFailed = totalFailed; this.successRate = successRate; this.timestamp = new Date(); } // Getters public long getActiveSubscriptions() { return activeSubscriptions; } public long getTotalNotificationsSent() { return totalNotificationsSent; } public long getTotalNotificationsFailed() { return totalNotificationsFailed; } public double getSuccessRate() { return successRate; } public Date getTimestamp() { return timestamp; } } } Frontend Integration Example
Example 8: Service Worker (JavaScript)
// service-worker.js self.addEventListener('push', function(event) { if (!event.data) return; const data = event.data.json(); const options = { body: data.body, icon: data.icon || '/icons/icon-192x192.png', badge: data.badge || '/icons/badge-72x72.png', vibrate: data.vibrate || [200, 100, 200], requireInteraction: data.requireInteraction || false, data: data.data || {}, actions: data.actions || [] }; event.waitUntil( self.registration.showNotification(data.title, options) ); }); self.addEventListener('notificationclick', function(event) { event.notification.close(); if (event.action === 'view' && event.notification.data.url) { event.waitUntil( clients.openWindow(event.notification.data.url) ); } else if (event.action === 'dismiss') { // Notification dismissed } else { // Default action - open app event.waitUntil( clients.openWindow('/') ); } }); Example 9: Frontend Subscription (JavaScript)
// push-notifications.js class PushNotificationManager { constructor() { this.publicKey = 'YOUR_PUBLIC_VAPID_KEY'; this.isSupported = 'serviceWorker' in navigator && 'PushManager' in window; } async init() { if (!this.isSupported) { console.log('Push notifications not supported'); return false; } try { // Register service worker const registration = await navigator.serviceWorker.register('/service-worker.js'); console.log('Service Worker registered'); // Check current subscription this.subscription = await registration.pushManager.getSubscription(); return true; } catch (error) { console.error('Service Worker registration failed:', error); return false; } } async subscribe() { if (!this.isSupported) return null; try { const registration = await navigator.serviceWorker.ready; // Convert VAPID public key to Uint8Array const applicationServerKey = this.urlBase64ToUint8Array(this.publicKey); // Subscribe to push notifications this.subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: applicationServerKey }); // Send subscription to server await this.sendSubscriptionToServer(this.subscription); console.log('Push notification subscription successful'); return this.subscription; } catch (error) { console.error('Failed to subscribe to push notifications:', error); return null; } } async unsubscribe() { if (!this.subscription) return; try { await this.subscription.unsubscribe(); await this.removeSubscriptionFromServer(); this.subscription = null; console.log('Push notification subscription removed'); } catch (error) { console.error('Failed to unsubscribe from push notifications:', error); } } async sendSubscriptionToServer(subscription) { const response = await fetch('/api/push/subscribe', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-User-ID': 'current-user-id' // Replace with actual user ID }, body: JSON.stringify(subscription) }); if (!response.ok) { throw new Error('Failed to save subscription on server'); } } async removeSubscriptionFromServer() { const response = await fetch('/api/push/unsubscribe', { method: 'POST', headers: { 'X-User-ID': 'current-user-id' // Replace with actual user ID } }); if (!response.ok) { throw new Error('Failed to remove subscription from server'); } } urlBase64ToUint8Array(base64String) { const padding = '='.repeat((4 - base64String.length % 4) % 4); const base64 = (base64String + padding) .replace(/\-/g, '+') .replace(/_/g, '/'); const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; ++i) { outputArray[i] = rawData.charCodeAt(i); } return outputArray; } getSubscriptionState() { return this.subscription ? 'subscribed' : 'unsubscribed'; } } // Usage const pushManager = new PushNotificationManager(); // Initialize when page loads document.addEventListener('DOMContentLoaded', async () => { await pushManager.init(); // Update UI based on subscription state updateSubscriptionUI(pushManager.getSubscriptionState()); }); function updateSubscriptionUI(state) { const subscribeBtn = document.getElementById('subscribe-btn'); const unsubscribeBtn = document.getElementById('unsubscribe-btn'); const status = document.getElementById('subscription-status'); if (state === 'subscribed') { subscribeBtn.style.display = 'none'; unsubscribeBtn.style.display = 'block'; status.textContent = 'Subscribed to push notifications'; } else { subscribeBtn.style.display = 'block'; unsubscribeBtn.style.display = 'none'; status.textContent = 'Not subscribed to push notifications'; } } // Button event handlers document.getElementById('subscribe-btn').addEventListener('click', async () => { await pushManager.subscribe(); updateSubscriptionUI(pushManager.getSubscriptionState()); }); document.getElementById('unsubscribe-btn').addEventListener('click', async () => { await pushManager.unsubscribe(); updateSubscriptionUI(pushManager.getSubscriptionState()); }); Best Practices and Security
Security Considerations
- VAPID Keys: Keep private key secure, never expose it to clients
- HTTPS: Web Push requires HTTPS in production
- User Consent: Always get explicit user permission
- Rate Limiting: Implement rate limiting to prevent abuse
- Input Validation: Validate all incoming subscription data
Performance Optimization
- Batch Operations: Send multiple notifications in batches
- Async Processing: Use async processing for non-blocking operations
- Connection Pooling: Reuse HTTP connections for push services
- Caching: Cache frequently accessed subscription data
Error Handling and Monitoring
Example 10: Comprehensive Error Handling
import java.util.concurrent.CompletableFuture; public class RobustWebPushService extends WebPushService { private final PushAnalytics analytics; public RobustWebPushService(String publicKey, String privateKey, PushAnalytics analytics) throws GeneralSecurityException { super(publicKey, privateKey); this.analytics = analytics; } @Override public CompletableFuture<Void> sendNotificationAsync(String userId, String title, String message) { return super.sendNotificationAsync(userId, title, message) .thenRun(() -> analytics.recordNotificationSent(userId)) .exceptionally(throwable -> { analytics.recordNotificationFailed(userId); handleNotificationError(userId, throwable); return null; }); } private void handleNotificationError(String userId, Throwable throwable) { System.err.println("Notification failed for user " + userId + ": " + throwable.getMessage()); // Check if subscription is invalid and should be removed if (isInvalidSubscriptionError(throwable)) { System.out.println("Removing invalid subscription for user: " + userId); getSubscriptionManager().removeSubscription(userId); analytics.recordUnsubscription(userId); } } private boolean isInvalidSubscriptionError(Throwable throwable) { String message = throwable.getMessage().toLowerCase(); return message.contains("gone") || message.contains("not found") || message.contains("invalid") || message.contains("expired"); } } Conclusion
Web Push notifications in Java provide a powerful way to engage users with real-time updates. Key takeaways:
- VAPID Authentication: Essential for secure push notifications
- Subscription Management: Properly store and manage user subscriptions
- Async Processing: Use async operations for better performance
- Error Handling: Robust error handling for failed deliveries
- Analytics: Track delivery success rates and user engagement
This implementation provides a solid foundation for integrating Web Push notifications into your Java applications, with features for security, scalability, and maintainability.