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.