FCM Integration with Spring Boot To Send Push Notification From Server Side.


Hey Guys, recently i have integrated FCM( Firebase Cloud Messaging) with one of my spring boot application. So I would like to share my experience with you.

Some Basics Steps You Need To Follow:
1) Please make sure client application registered under Firebase.

2) Now we need to generate Firebase SDK admin key. This is basically JSON file and the purpose of this file is for server side authorization.You can get more details under https://firebase.google.com/docs/cloud-messaging/auth-server .

3) To get Admin SDK go to Project Setting -> Service Accounts -> Check Java radio button -> Finally click on Generate new private key and save it.


4) Now we need to use generated private JSON file in our spring boot application.So for this I am assuming you have already Spring Boot application , if you don't have then you can create from here https://start.spring.io/

5) Create one folder under src/main/resources in your spring boot application. In my case I have created fcm folder. Now put you private key under this folder.

6) In your application.properties file create key/value like app.firebase-configuration-file=fcm/dummy-d7bd4-firebase-adminsdk-l3psc-8c2aa79daa.json
Note : value key contains location of your private JSON file.

7) Add Firebase dependency in pom.xml file.

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

8) Now we need to initialize Firebase application , for this we need private key location so to access the location we can use @Value annotation with key name. Create class FCMInitializer with file name FCMInitializer.java

package com.pushnotification.fcm.service;

import java.io.IOException;

import javax.annotation.PostConstruct;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;

import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;

@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());
        }
    }

}

In the above code snippets , initialize() method is being called on application start up due to @PostConstruct annotaction.

9) Create Class FCMService with @Service annotation which contains some different types of methods. Create class with file name FCMService.java

package com.pushnotification.fcm.service;

import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ExecutionException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import com.google.firebase.messaging.AndroidConfig;
import com.google.firebase.messaging.AndroidNotification;
import com.google.firebase.messaging.ApnsConfig;
import com.google.firebase.messaging.Aps;
import com.google.firebase.messaging.FirebaseMessaging;
import com.google.firebase.messaging.Message;
import com.google.firebase.messaging.Notification;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.pushnotification.fcm.model.PushNotificationRequest;

@Service
public class FCMService {

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

    public void sendMessage(Map<String, String> data, PushNotificationRequest request)
            throws InterruptedException, ExecutionException {
        Message message = getPreconfiguredMessageWithData(data, request);
        Gson gson = new GsonBuilder().setPrettyPrinting().create();
        String jsonOutput = gson.toJson(message);
        String response = sendAndGetResponse(message);
        logger.info("Sent message with data. Topic: " + request.getTopic() + ", " + response+ " msg "+jsonOutput);
    }

    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);
    }

    public void sendMessageToToken(PushNotificationRequest request)
            throws InterruptedException, ExecutionException {
        Message message = getPreconfiguredMessageToToken(request);
        Gson gson = new GsonBuilder().setPrettyPrinting().create();
        String jsonOutput = gson.toJson(message);
        String response = sendAndGetResponse(message);
        logger.info("Sent message to token. Device token: " + request.getToken() + ", " + response+ " msg "+jsonOutput);
    }

    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 getPreconfiguredMessageToToken(PushNotificationRequest request) {
        return getPreconfiguredMessageBuilder(request).setToken(request.getToken())
                .build();
    }

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

    private Message getPreconfiguredMessageWithData(Map<String, String> data, PushNotificationRequest request) {
        return getPreconfiguredMessageBuilder(request).putAllData(data).setToken(request.getToken())
                .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()));
    }
}

10) Now I am going to add one more service layer. Create class PushNotificationService with file name PushNotificationService.java

package com.pushnotification.fcm.service;

import java.util.HashMap;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import com.pushnotification.fcm.model.PushNotificationRequest;

@Service
public class PushNotificationService {

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

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

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

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

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

    private Map<String, String> getSamplePayloadData() {
        Map<String, String> pushData = new HashMap<>();
        pushData.put("messageId", "msgid");
        pushData.put("text", "txt");
        pushData.put("user", "pankaj singh");
        return pushData;
    }
}

Note : Create these all three above files under the service layer.

11) Now create two files under model layer PushNotificationRequest.java , PushNotificationResponse.java

package com.pushnotification.fcm.model;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Setter
@Getter
@NoArgsConstructor
public class PushNotificationRequest {

    private String title;
    private String message;
    private String topic;
    private String token;  
}
package com.pushnotification.fcm.model;
public class PushNotificationResponse {

    private int status;
    private String message;

    public PushNotificationResponse() {
    }

    public PushNotificationResponse(int status, String message) {
        this.status = status;
        this.message = message;
    }

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }
}

12) Create controller class to test push notification.Create file PushNotificationController.java

package com.pushnotification.fcm.controller;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import com.pushnotification.fcm.model.PushNotificationRequest;
import com.pushnotification.fcm.model.PushNotificationResponse;
import com.pushnotification.fcm.service.PushNotificationService;

@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);
    }

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

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

Finally we have configured everything for push notification.So our code is ready to send the push notification.


We have mainly three use cases:
a) Push notification based on topics. In this kind of notification we don't required any device token. So based on topic configuration push notification will work.

For this you can use end point : /notification/topic

b) Push notification with token.With this end point we can send only title and body part.

For this you can use end point : /notification/token

c) Push notification with additional payload data. With this end point we can send title,body and some additional key/pair data.

For this you can use end point : /notification/data

If you want to test please use postman and pass JSON body like :

{
	"title":"Put the push notification title here",
	"message":"Put here push notification body here",
	"token":"f65ewWeHOUs:APA91bH2mVv-cVpjVv4mJZxkugeNoJJ_AL-Bl_Kfosz26rk1nTJREp-_b9eEzojh3GgvGxtu1VXlyXTxc5j_jQNMl0oBEEw2oe_knHVIpvdAKMG6qya6"
}

Note : You can download full working source code from my repository link https://github.com/firststepitsolution/push-notification-with-fcm-and-spring-boot I have tested on both platform (IOS and Android) and its working as expected.

Data Type In Java

There are two data types in JAVA. Lets take a graphical representation to understand it.
Data Type In Java

Lets take graphical representation to understand the primitive data type memory required , value holding capacity range and their default value.

Primitive data type memory, range and their default value

Lets take a example to understand the primitive data type:

1) boolean primitive data type example:

package primitiveDataType;

/**
 * 
 * boolean is a primitive data type and it takes 1 bits of memory
 * Its default value is false
 */
public class BooleanPrimitiveDataType {

	public static void main(String[] args) {
	boolean booleanVariable=true;
	System.out.println("Boolean value is : "+booleanVariable);
	}
}

2) byte primitive data type example:

package primitiveDataType;

/**
 * 
 * byte is a primitive data type and takes 8 bits(1 byte) of memory
 * Its value-range lies between -128 to 127 (inclusive)
 * Its minimum value is -128 and maximum value is 127
 * Its default value is 0 
 *
 */
public class BytePrimitiveDataType {

 public static void main(String[] args) {
	byte byteVariable=127;
	System.out.println(byteVariable);
	byteVariable='p';
	System.out.println((char)byteVariable);
 }
}

3) char primitive data type example:
package primitiveDataType;

/**
 * 
 * short is a primitive data type and takes 16 bits(2 byte) of memory
 * Its value range lies between 
 * '\u0000' (or 0) to '\uffff' (or 65,535 inclusive)
 * Its default value is '\u0000' 
 *
 */
public class CharPrimitiveDataType {

 public static void main(String[] args) {
	char name='p';
	System.out.println(name);
 }
}

4) short primitive data type example:

package primitiveDataType;

/**
 * 
 * short is a primitive data type and takes 16 bits(2 byte) of memory
 * Its value-range lies between -32,768 to 32,767 (inclusive).
 * Its minimum value is -32,768 and maximum value is 32,767. Its default value is 0. 
 *
 */
public class ShortPrimitiveDataType {

 public static void main(String[] args) {
	//It will accept only max value 32767
	short shortVariable=32767;
	System.out.println(shortVariable);
	shortVariable=-32768;
	System.out.println(shortVariable);
		
 }
}

5) int primitive data type example:

package primitiveDataType;

/**
 * 
 * short is a primitive data type and takes 32 bits(4 byte) of memory
 * Its value range lies between - 2,147,483,648  
 * to 2,147,483,647  (inclusive)
 * Its minimum value is -32,768 and maximum value is 32,767
 * Its default value is 0
 *
 */
public class IntPrimitiveDataType {

 public static void main(String[] args) {
 /**
  * It will accept only max value 2,147,483,647, 
  * if you will try to put above the mentioned range
  * it will throw error
  */
 int intVariable=2147483647;
 System.out.println("Max acceptable value is "+intVariable);
 intVariable=-2147483648;
 System.out.println("Min acceptable value is :"+intVariable);
		
 }
}

6) long  primitive data type example:

package primitiveDataType;

/**
 * 
 * short is a primitive data type and takes 64 bits(8 byte) of memory
 * Its value range lies between
 * -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807(inclusive)
 * Its minimum value is -9,223,372,036,854,
 * 775,808 and maximum value is 9,223,372,036,854,775,807
 * Its default value is 0L 
 *
 */
public class LongPrimitiveDataType {

 public static void main(String[] args) {
/**
 * It will accept only max value 9,223,372,036,854,775,807, 
 * if you will try to put above the mentioned 
 * range it will throw error
 * All literal  numbers in java are by default ints,
 * which has range -2147483648 to 2147483647 inclusive.
 * Your literals are outside this range, 
 * so to  make this compile you need to indicate they're
 * long literals (ie suffix with L):
 * You must use L to say to the compiler that it is a long literal.
 */
 long longVariable=9223372036854775807L;
 System.out.println("Max acceptable value is "+longVariable);
 longVariable=-9223372036854775808L;
 System.out.println("Min acceptable value is :"+longVariable);
		
 }
}

7) float  primitive data type example:

package primitiveDataType;

/**
 * 
 * float is a primitive data type and takes 32 bits(4 byte) of memory
 * The smallest decimal is 1.40239846 x 10^-45, 
 * and the largest value is 3.40282347 x 10^38.
 * Float range is much difference than the other primitive data type.
 * Its default value is 0.0f
 */
public class FloatPrimitiveDataType {

 public static void main(String[] args) {
	float floatVariable=9223.5f;
	System.out.println("Float value is : "+floatVariable);
		
 }
}

8) double primitive data type example:

package primitiveDataType;

/**
 * 
 * double is a primitive data type and takes 64 bits(8 byte) of memory
 * The range is 4.9406564584124654 x 10^-324 
 * to 1.7976931348623157 x 10^308. That range can
 * also be positive or negative.
 * Its default value is 0.0d
 */
public class DoublePrimitiveDataType {

 public static void main(String[] args) {
  double doubleVariable=9223.50006d;
  System.out.println("Double value is : "+doubleVariable);
 }
}

Note : You can download full source code from git repository https://github.com/firststepitsolution/java-core

Java Variable Naming Conventions.

Some basic convention rules for java variable:
1. Variable name always start with letter rather than number,$(dollar) and _(underscore).
Valid Variables Name Based On Convention:
int marks, int sumOfTwoNumbers, int listOfStudents etc
Variables Name Without Naming Convention:
int $marks, int _marks, int sum_Of_Two_Numbers, int list_of_students etc ( these all variables will work but i will suggest to not use these kind of variables and you must follow the proper naming convention for proper understanding of code and their meaning.
Invalid Variable:
int 1marks, int sum&Of, int list%Of etc
2. Name should be meaningful full word. If your variable name contains combination of two or more words then first word should be start with lower letter and then remaining words first letter should be start with capital.
3. If variables stores constant value then variable name should be in capital and if more then two words then separate words with underscore(_).
Example:
static final int RECORDS_PER_PAGE = 10;
static final int MAX_RECORDS_PER_PAGE=100;
4. Special characters are not allowed like #$%^&*@ etc
5. Variable are case-sensitive.