Send push notifications from Spring Boot server-side application using FCM

As you probably noticed, I familiarize myself with the subject of Firebase Cloud Messaging push notifications recently.

This time I would like to share with you my push notifications server-side application. The app is made with Spring Boot framework.

Provided example covers most common notifications sending use cases such as: sending push notification to a topic, directly to the users’ device or sending messages with additional data payload.

This post is not a step-by-step tutorial. I’ll focus on the most important parts. Fully working example is available on GitHub.

If you don’t have a client-side application to receive push notifications yet, you should consider the use of Ionic app I described earlier. Have fun!

Let’s go

Let’s start from the Firebase integration.

First of all you have to generate your own Firebase SDK admin key. Basically it’s a JSON file with your Firebase project credentials. You’ll need it for server-side authorization (more information).

Login into your Firebase console. Go to Project settings -> Service accounts and then click Generate new private key button.

 

Generate and save the file. We’ll use it in the next steps.

Spring Boot application

Put generated Firebase Admin SDK JSON file inside your Spring Boot project files (if you don’t have one you can generate it here).

In my case I created google folder inside src/main/resources. Then, in my application.properties, I added new key/value pair containing file path.

app.firebase-configuration-file=google/push-notifications-example-firebase-adminsdk.json

 

Now we’ll need Firebase dependencies here so let’s add some. I was using Maven as a dependency manager. In my pom.xml I added:

        <dependency>
            <groupId>com.google.firebase</groupId>
            <artifactId>firebase-admin</artifactId>
            <version>6.8.1</version>
        </dependency>

Now we have to initialize our Firebase application. This is the time to use our app.firebase-configuration-file. I used @Value annotation to inject the path value to the String field.

...

@Service
public class FCMInitializer {

    @Value("${app.firebase-configuration-file}")
    private String firebaseConfigPath;

    Logger logger = LoggerFactory.getLogger(FCMInitializer.class);

    @PostConstruct
    public void initialize() {
        try {
            FirebaseOptions options = new FirebaseOptions.Builder()
                    .setCredentials(GoogleCredentials.fromStream(new ClassPathResource(firebaseConfigPath).getInputStream())).build();
            if (FirebaseApp.getApps().isEmpty()) {
                FirebaseApp.initializeApp(options);
                logger.info("Firebase application has been initialized");
            }
        } catch (IOException e) {
            logger.error(e.getMessage());
        }
    }

}
FCMInitializer.java

The initialize() method is being called on application start-up thanks to @PostConstruct annotation.

Sending push notifications

The next step is to prepare our push notifications service class.
But first of all I separated following use cases:

  • send push notification to topic using sample data (defined in application.properties)
  • send scheduled sample push notification to topic (every minute)
  • send push notification with additional data payload (user-defined key/value data object, look at the docs for more information)
  • send push notification without additional data payload
  • send push notification to a specific user

Sending push notification without data payload example from FCMService class:

@Service
public class FCMService {

    ...

    public void sendMessageWithoutData(PushNotificationRequest request)
            throws InterruptedException, ExecutionException {
        Message message = getPreconfiguredMessageWithoutData(request);
        String response = sendAndGetResponse(message);
        logger.info("Sent message without data. Topic: " + request.getTopic() + ", " + response);
    }

    ...

    private String sendAndGetResponse(Message message) throws InterruptedException, ExecutionException {
        return FirebaseMessaging.getInstance().sendAsync(message).get();
    }

    private AndroidConfig getAndroidConfig(String topic) {
        return AndroidConfig.builder()
                .setTtl(Duration.ofMinutes(2).toMillis()).setCollapseKey(topic)
                .setPriority(AndroidConfig.Priority.HIGH)
                .setNotification(AndroidNotification.builder().setSound(NotificationParameter.SOUND.getValue())
                        .setColor(NotificationParameter.COLOR.getValue()).setTag(topic).build()).build();
    }

    private ApnsConfig getApnsConfig(String topic) {
        return ApnsConfig.builder()
                .setAps(Aps.builder().setCategory(topic).setThreadId(topic).build()).build();
    }

    private Message getPreconfiguredMessageWithoutData(PushNotificationRequest request) {
        return getPreconfiguredMessageBuilder(request).setTopic(request.getTopic())
                .build();
    }

    ...

    private Message.Builder getPreconfiguredMessageBuilder(PushNotificationRequest request) {
        AndroidConfig androidConfig = getAndroidConfig(request.getTopic());
        ApnsConfig apnsConfig = getApnsConfig(request.getTopic());
        return Message.builder()
                .setApnsConfig(apnsConfig).setAndroidConfig(androidConfig).setNotification(
                        new Notification(request.getTitle(), request.getMessage()));
    }


}

If you really want to you can test it right now by calling proper methods. For clarity purposes I added another layer – PushNotificationService which will be used directly by PushNotificationController in the further steps.

Let’s take a look at PushNotificationService:

...

@Service
public class PushNotificationService {

    @Value("#{${app.notifications.defaults}}")
    private Map<String, String> defaults;

    private Logger logger = LoggerFactory.getLogger(PushNotificationService.class);
    private FCMService fcmService;

    public PushNotificationService(FCMService fcmService) {
        this.fcmService = fcmService;
    }

    @Scheduled(initialDelay = 60000, fixedDelay = 60000)
    public void sendSamplePushNotification() {
        try {
            fcmService.sendMessageWithoutData(getSamplePushNotificationRequest());
        } catch (InterruptedException | ExecutionException e) {
            logger.error(e.getMessage());
        }
    }

    public void sendPushNotification(PushNotificationRequest request) {
        try {
            fcmService.sendMessage(getSamplePayloadData(), request);
        } catch (InterruptedException | ExecutionException e) {
            logger.error(e.getMessage());
        }
    }

    public void sendPushNotificationWithoutData(PushNotificationRequest request) {
        try {
            fcmService.sendMessageWithoutData(request);
        } catch (InterruptedException | ExecutionException e) {
            logger.error(e.getMessage());
        }
    }


    public void sendPushNotificationToToken(PushNotificationRequest request) {
        try {
            fcmService.sendMessageToToken(request);
        } catch (InterruptedException | ExecutionException e) {
            logger.error(e.getMessage());
        }
    }


    private Map<String, String> getSamplePayloadData() {
        Map<String, String> pushData = new HashMap<>();
        pushData.put("messageId", defaults.get("payloadMessageId"));
        pushData.put("text", defaults.get("payloadData") + " " + LocalDateTime.now());
        return pushData;
    }


    private PushNotificationRequest getSamplePushNotificationRequest() {
        PushNotificationRequest request = new PushNotificationRequest(defaults.get("title"),
                defaults.get("message"),
                defaults.get("topic"));
        return request;
    }


}
PushNotificationService

As you see we have a @Value annotation again over here.
Why? Because we have bunch of default values to pass to our methods.

So instead of injecting single value from properties to particular String variable I decided to use Map type for simplicity. It still uses the same @Value annotation.

Again, we store the defaults in application.properties. So the app.notifications.defaults key looks like this:

app.notifications.defaults={topic: 'common', title: 'Common topic - Hello', message: 'Sending test message \uD83D\uDE42', token: 'ss22t03wz208eg:APA2idkkow223FE_0v5yHxqCLTyxAQafj6nWaqi4QzwZTW004q1PUux63UsFN', payloadMessageId: '123', payloadData: 'Hello. This is payload content.'}
application.properties

Moreover, you can see we’re using the FCMService directly, passing notifications requests objects to it.

For scheduling I used @Scheduled annotation with initial delay of 1min. (initialDelay parameter) and 1min. sending interval (fixedDelay parameter). Remember to annotate your Application class with @EnableScheduling.

Endpoints

Let’s interact with our services and build a controller.

For presentation purposes I exposed my service methods as REST API endpoints in order to send custom, direct messages (to topic or specific subscriber) with/without data payload or trigger the default notification sending.

...

@RestController
public class PushNotificationController {

    private PushNotificationService pushNotificationService;

    public PushNotificationController(PushNotificationService pushNotificationService) {
        this.pushNotificationService = pushNotificationService;
    }

    @PostMapping("/notification/topic")
    public ResponseEntity sendNotification(@RequestBody PushNotificationRequest request) {
        pushNotificationService.sendPushNotificationWithoutData(request);
        return new ResponseEntity<>(new PushNotificationResponse(HttpStatus.OK.value(), "Notification has been sent."), HttpStatus.OK);
    }

...

    @GetMapping("/notification")
    public ResponseEntity sendSampleNotification() {
        pushNotificationService.sendSamplePushNotification();
        return new ResponseEntity<>(new PushNotificationResponse(HttpStatus.OK.value(), "Notification has been sent."), HttpStatus.OK);
    }
}
PushNotificationController.java fragment

Test REST endpoints with cURL

Let’s test our endpoints. I used cURL for this task but you can use your favorite HTTP requests tools or write a small client app for this.

  • GET /notification – Trigger sample notification with default values sending
curl -H "Content-Type: application/json" -X GET http://localhost:8080/notification
  • POST /notification/topic – Send a message to a specific topic
curl -d '{"title":"Hello", "message":"The message...", "topic":"contactTopic"}' -H "Content-Type: application/json" -X POST http://localhost:8080/notification/topic
  • POST /notification/token – Send a message to a specific device (with token)
curl -d '{"title":"Hey you!", "message":"Watch out!", "token":"cct00ebz8eg:APA91bFcTkFE_0Qafj6nWv5yHxqCLTyxAaqi4QzwsFNLP5M9G78X8Z5UMZTW004q1PUux63Ut-1WMGVToMNTdB3ZfO8lCZlc4lGpxm7LBdWfkhaUxdbpQ5xIO5cAb-w9H2dBLNHT7i-U", "topic": ""}' -H "Content-Type: application/json" -X POST http://localhost:8080/notification/token
  • POST /notification/data – Send a message to a specific topic with additional payload data.
    Please note: in this case I took defaults from application.properties as sample payload. In your application you should probably use third-party API data or persistent data from your database.
curl -d '{"title":"Hello", "message":"Data message", "topic":"contactTopic"}' -H "Content-Type: application/json" -X POST http://localhost:8080/notification/data

 

If succeeded you should receive following JSON response with code 200:

{

    "status": 200,
    "message": "Notification has been sent."

}

 

Results

Well… If configured properly it just works. I used my Ionic app and Android phone as a client.

Take a look at this screenshot with received notifications:

In this case the client-side app doesn’t handle additional payload data.

Are you curious about the yellow colour of notifications? I set it in NotificationParameter enum along with sound option value (which is default in my case).
See the reference for more additional fields description.

Summary

In this post I covered basic Firebase Cloud Messaging integration with Spring Boot application. If you’re looking for server-side push notification implementation I think this is the way to go.

As I mentioned before, the fully working example is available on GitHub. If you’re looking for basic client-side mobile application please take a look at my previous Ionic push notifications app post.

Please note that provided example was tested with Android application. Although I provided some basic Apple Push Notification Service (APNS) configuration you have to deal with potential issues when developing an iOS application. If you managed to do it please let me know.

Good luck!

Leave a Reply

Your e-mail address will not be published. Required fields are marked *