Showing posts with label java. Show all posts
Showing posts with label java. Show all posts

Monday, May 19, 2025

GCP PubSub Receive Example

This is part two of the GCP (Google Cloud Platform) PubSub example. Part one is GCP PubSub Send Example. In part one, we learned how to set up GCP admin stuff (e.g. billing), GCP PubSub (e.g. topic), Google Cloud SDK Shell, and a lot more. So if you want to know how to make this example work, please read part one first. I am going straight into the GCP PubSub receive code and will not be discussing about how things are set up. That's covered in part one, please do have a read.

Tools and Materials

These are the tools I used to make this example:

  • IntelliJ IDEA 2023.3.4 (Community Edition)
  • openjdk-17.0.10
  • Spring Boot v3.4.5
  • Windows 11
  • Google account
  • Google Cloud SDK Shell
  • Git Bash

The example code is here, github.com/jpllosa/gcp-pubsub. You may grab a copy of it.

Spring Boot GCP PubSub Receive Code

As with part one, GCP PubSub Send Example. There are no changes to our pom.xml. It will have the same dependencies.

  • Spring Web (<artifactId>spring-boot-starter-web</artifactId>) - Build web, including RESTful, applications using Spring MVC. Uses Apache Tomcat as the default embedded container.
  • Spring Integration (<artifactId>spring-boot-starter-integration</artifactId>, <artifactId>spring-integration-http</artifactId>) - Adds support for Enterprise Integration Patterns. Enables lightweight messaging and supports integration with external systems via declarative adapters.
  • Google Cloud Messaging (<artifactId>spring-cloud-gcp-starter-pubsub</artifactId>) - Adds the Google Cloud Support entry and all the required dependencies so that the Google Cloud Pub/Sub integration work out of the box.

Our GCP PubSub message receive code is short and sweet. Just like the send code. We subscribe to a GCP PubSub topic and when a message is receive, we print it out to the console. Here's the code. Explanation follows after.

  
package com.blogspot.jpllosa;

// imports snipped...

@SpringBootApplication
@RestController
public class GcpPubsubApplication {

// some send code snipped...

	@PostMapping("/postMessage")
	public RedirectView postMessage(@RequestParam("message") String message) {
		this.messagingGateway.sendToGcpPubsub(message);
		return new RedirectView("/");
	}

	// Receive
	@Bean
	public MessageChannel gcpPubsubInputChannel() {
		return new DirectChannel();
	}

	@Bean
	public PubSubInboundChannelAdapter messageChannelAdapter(
			@Qualifier("gcpPubsubInputChannel") MessageChannel inputChannel,
			PubSubTemplate pubSubTemplate) {
		PubSubInboundChannelAdapter adapter =
				new PubSubInboundChannelAdapter(pubSubTemplate, "gcp-pubsub-example-sub");
		adapter.setOutputChannel(inputChannel);

		return adapter;
	}

	@ServiceActivator(inputChannel = "gcpPubsubInputChannel")
	public void messageReceiver(String message) {
		System.out.println("Message received: " + message);
	}

}
  

The same with part one, only the GCP PubSub receive bits will be explained. Incoming messages will arrive on the MessageChannel (DirectChannel) we created. Simliar to sending, we have an inbound channel adapter that receives messages from GCP PubSub and forwards it to gcpPubsubInputChannel. The inbound channel adapter is linked to the GCP PubSub subscription name. The adapter is bound and listens for new messages from GCP PubSub gcp-pubsub-example-sub subscription. The method annotated with @ServiceActivator is called when new messages arrive at gcpPubsubInputChannel and we simply print the message on the console. Short and sweet.

Spring Boot GCP PubSub Receive in Action

Let's see our code in action. Run the Spring Boot app (e.g. mvnw spring-boot:run) or thru IntelliJ. When the app is ready, hit the /postMessage endpoint and you should have something like below printed on the console.

  
$ curl --data "message=GCP Pubsub send and receive!" localhost:8080/postMessage
  

We won't be able to see any messages in the GCP PubSub messages section because this happens very quick. If you want to see the messages sent on the messages tab, click PULL to delay the message being consumed. The delay is about 5-ish seconds. It would look like the below.

Spring Boot GCP PubSub Receive Wrap Up

There you have it. Your travel to GCP PubSub has come full circle, assuming you read part one of course. As per usual, we give thanks to Spring for making it easy for us by abstracting whatever it is happening under the hood. Alternatively, you can use other client libaries to connect to GCP PubSub. We created an input channel and attached it to a channel adapter that has subscribed to GCP PubSub. Viola and that is all there is to it! Thank you for reading.

Monday, May 12, 2025

GCP PubSub Send Example

If you are working on Shopify and you need to monitor webhook events (e.g. an order has been created), chances are you'll be directed to use Google Pub/Sub messaging service. In this blog, I'll show you how to setup PubSub on the Google Cloud Platform (GCP), then send a message and check the message on GCP PubSub cloud console.

Tools and Materials

These are the tools I used to make this example:

  • IntelliJ IDEA 2023.3.4 (Community Edition)
  • openjdk-17.0.10
  • Spring Boot v3.4.5
  • Windows 11
  • Google account
  • Google Cloud SDK Shell
  • Git Bash

The example code is here, github.com/jpllosa/gcp-pubsub. You may grab a copy of it.

GCP PubSub Setup

On GCP, create a new project. Like so.

Assign your new project with billing. As of this writing, Google provides free credits to new customers and free usage limits. For Google PubSub, it is 10 GB of messages per month. Please verify for yourselves if this is still the case. Here's what creating a billing account looks like and it being connected to the project ID.

Now that the administrative stuff has all been set up, it's time to create a PubSub topic and subscription.

Google Cloud SDK Shell Setup

Install and initialize the Google Cloud SDK. For installing the Google Cloud SDK Shell, it is better to get it from the horses' mouth. Please follow the Google Cloud documentation on how to install and initialiaze it for your project. Once initialized, enable GCP PubSub. Here are a few checkpoints so you'll know you are heading in the right direction. First and foremost, you must log in with application default credentials using the following command, gcloud auth application-default login. When you are logged, you should have something like below.

Next is to set the shell to your project, gcloud config set project <PROJECT-NAME>. Here's the command to see the available projects, gcloud config projects list. When you have done those, you should have something like below.

Lastly, enable GCP PubSub, gcloud services enable pubsub.googleapis.com. Output would look like below.

For Windows, we can verify that our Google credentials are saved by looking under C:\Users\username\AppData\Roaming\gcloud\application_default_credentials.json. This file should exist.

Spring Boot GCP PubSub Send Code

I used Spring Initialzr to create the skeleton project. The following are the dependencies we need:

  • Spring Web - Build web, including RESTful, applications using Spring MVC. Uses Apache Tomcat as the default embedded container.
  • Spring Integration - Adds support for Enterprise Integration Patterns. Enables lightweight messaging and supports integration with external systems via declarative adapters.
  • Google Cloud Messaging - Adds the Google Cloud Support entry and all the required dependencies so that the Google Cloud Pub/Sub integration work out of the box.

Our pom.xml should at least have the following dependencies.

  
<!-- snipped -->

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
	<groupId>com.google.cloud</groupId>
	<artifactId>spring-cloud-gcp-starter-pubsub</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.integration</groupId>
	<artifactId>spring-integration-http</artifactId>
</dependency>

<!-- snipped -->
  

Our GCP PubSub message sender code is short and sweet. When an HTTP POST is received, it then submits the message to GCP PubSub. Here's the code. Explanation follows after.

  
package com.blogspot.jpllosa;

// imports snipped...

@SpringBootApplication
@RestController
public class GcpPubsubApplication {

	public static void main(String[] args) {
		SpringApplication.run(GcpPubsubApplication.class, args);
	}

	@MessagingGateway(defaultRequestChannel = "gcpPubsubOutputChannel")
	public interface GcpPubsubOutboundGateway {
		void sendToGcpPubsub(String text);
	}

	@Bean
	@ServiceActivator(inputChannel = "gcpPubsubOutputChannel")
	public MessageHandler messageSender(PubSubTemplate pubsubTemplate) {
		return new PubSubMessageHandler(pubsubTemplate, "gcp-pubsub-example");
	}

	@Autowired
	private GcpPubsubOutboundGateway messagingGateway;

	@PostMapping("/postMessage")
	public RedirectView postMessage(@RequestParam("message") String message) {
		this.messagingGateway.sendToGcpPubsub(message);
		return new RedirectView("/");
	}

}
  

Since this is a blog about GCP PubSub, I'll focus on explaining the code related to it. I won't be talking about the other annotations (e.g. @SpringBootApplication, @PostMapping, etc.). For example, @PostMapping is related to web and nothing to do with GCP PubSub. We just need that endpoint to trigger a send message to GCP PubSub. We can use other client libraries to communicate with GCP PubSub, not just Spring.

We use Spring Integration messaging gateway to write to a channel, gcpPubsubOutputChannel. This channel is still in Spring land. When a message is in this channel, it is then consumed by the MessageHandler (outbound channel adapter) which in turn publishes the message to GCP PubSub. The outbound channel adapter converts the Spring message into a GCP PubSub message and is published to the topic, gcp-pubsub-example. The @ServiceActivator annotation causes this MessageHandler to be applied to any new messages in inputChannel. Lastly, we got a REST endpoint listening for messages which then forward the message to GCP PubSub. Short and sweet.

Spring Boot GCP PubSub Send in Action

Alrighty, run the Spring Boot app. We can do it command line (e.g. mvn spring-boot:run) or thru IntelliJ. I would suggest running it first on Google Cloud SDK Shell. If it works there then I don't see any reason why it won't work on IntelliJ. When the app is ready, hit the /postMessage endpoint.

  
$ curl --data "message=Eagle has landed!" localhost:8080/postMessage
  

Now, head over to GCP PubSub messages section and we should see the messages appear. If not, try reloading the web page or do a PULL. It should look like below.

Spring Boot GCP PubSub Send Wrap Up

Did you enjoy playing around the GCP PubSub service? We now have an idea on how to send messages via the GCP PubSub messaging service. Spring has made it easy for us by abstracting whatever it is happening under the hood. Thank you for reading.

Friday, April 25, 2025

Managing HttpOnly Cookie Example

In my previous blog, Spring Boot HttpOnly Cookie Example, I demonstrated how to use the HttpOnly flag to protect your cookie. Since an HttpOnly cookie cannot be managed on the front-end, here is a demonstration on how to manage it via the back-end. For this example we'll just expire the cookie when the user clicks a button. Shouldn't be tricky so let's get right to it.

Demonstration

Before we begin, these are the tools I used to make this example:

  • IntelliJ IDEA 2023.3.4 (Community Edition)
  • openjdk-17.0.10
  • Spring Boot v3.4.1
  • Windows 11

The example code is here, github.com/jpllosa/httponly-cookie/tree/manage-httponly. Grab a copy of this branch or if you have done the Spring Boot HttpOnly Cookie Example, you can make updates to the code as you wish.

Code Changes

A couple of changes to the code. First, the endpoint to hit that then tells the browser to expire the cookie. Second, an update on the UI to trigger the cookie deletion.

LoginController.java

  
... code snipped ...
import org.springframework.web.bind.annotation.*;

@Controller
public class LoginController {

... code snipped ...

    @GetMapping("/clear-my-session")
    public @ResponseBody String clearMySession(HttpServletResponse response) {

        Cookie cookie = new Cookie("MY_SESSION", "deleted");
        cookie.setHttpOnly(true);
        cookie.setMaxAge(0);
        response.addCookie(cookie);

        return "";
    }
}

  

The /clear-my-session resource simply responds by setting the MaxAge of the cookie to zero for it to be deleted on the browser. In some other languages, in Go for example, you'll have to set the MaxAge to -1 to expire the cookie. So please read the API documentation if you're wondering why MaxAge zero doesn't work. Since we are just running on our local machhine, we don't provide the Path and Domain. When not running on your local machine, most likely you'll need to supply a Path (e.g. "/") and a Domain (e.g. example.com) to make things work.

welcome.html

  
... code snipped ...
<body>
... code snipped ...
<button id="clearMySession">JS Clear MY_SESSION</button>
<button id="beClearMySession">BE Clear MY_SESSION</button>
</body>

<script th:inline="javascript">
    $(document).ready(function() {
        $("#clearMySession").on("click", function() {
            console.log("JS removing MY_SESSION");
            document.cookie = "MY_SESSION=; expires=Thu, 01-Jan-70 00:00:01 GMT;";
        });

        $("#beClearMySession").on("click", function() {
            console.log("BE removing MY_SESSION");

            $.get("/clear-my-session", function() {
                console.log("MY_SESSION removed");
            });
        });
    });
</script>
</html>

  

Here, we added a button that hits the endpoint we created above. Simples.

Ready, Set, Go

Run the Spring Boot app. Go to the login page. Just type any username and password. You should have something like below. Open Web Tools and head over to the Storage tab (it could be called a different name on a different browser). Take note of the cookie named MY_SESSION.

As you already know from the previous blog, clicking on "JS Clear MY_SESSION" (JS for JavaScript) will not do anything. Now, try clicking on "BE Clear MY_SESSION", BE for back-end :). The MY_SESSION cookie disappears right before our eyes and you should have something like below. You can also check the network and console tabs to see what's happening behind the scenes.

Managing HttpOnly Cookie Wrap Up

There you have it. A nice way of managing your cookie from the back-end. Having the HttpOnly flag set prevents thrid parties from accessing your very important cookie. Now, it's only the back-end that can manipulate it. Thank you for reading.

Sunday, January 12, 2025

Spring Boot HttpOnly Cookie Example

Want to protect your cookie? Well, not from Santa Claus. I'm talking about HTTP Cookie. Using the HttpOnly flag when generating a cookie helps mitigate the risk of a client side script accessing the protected cookie. In other words, it won't be accessible programmatically on the client side (e.g. JavaScript). The cookie will be driven by the backend.

Why do this? To mitigate cross-site scripting (XSS) attacks. If a cross-site scripting flaw exists, and a user accidentally accesses a link that exploits this flaw, the browser will not reveal the cookie to a third party. Currently, every major browser supports HttpOnly cookies. If a browser does not support HttpOnly and a website attempts to set an HttpOnly cookie, the HttpOnly flag will be ignored by the browser, thus creating a traditional, script accessible cookie. As a result, the cookie (typically your session cookie) becomes vulnerable to theft or modification by a malicious script. Majority of XSS attacks target theft of session cookies. A server could help mitigate this issue by setting the HttpOnly flag on a cookie it creates, indicating the cookie should not be accessible on the client. If a browser that supports HttpOnly detects a cookie containing the HttpOnly flag, and client side script code attempts to read the cookie, the browser returns an empty string as the result. This causes the attack to fail by preventing the malicious (usually XSS) code from sending the data to an attacker’s website.

Demonstration

Before we begin, these are the tools I used to make this example:

  • IntelliJ IDEA 2023.3.4 (Community Edition)
  • openjdk-17.0.10
  • Spring Boot v3.4.1
  • Windows 11

The example code is here, github.com/jpllosa/httponly-cookie. Download it as you please. Now, on with the show. Run the Spring Boot app. Go to the login page, you should see something like below. Please forgive the look, this demo is about HttpOnly and not about the UI.

Without HttpOnly

Log in. Just type any user name and password. You should have something like below. Open Web Tools and head over to the Storage tab (it could be called a different name on a different browser). Take note of the HttpOnly column of the cookie named MY_SESSION.

This is the code of the Controller serving the page. Take note that we have commented out the line that sets the HttpOnly flag.

  
package com.blogspot.jpllosa.httponly_cookie;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.CookieValue;

@Controller
public class LoginController {

    @GetMapping("/login")
    public String getLogin() {
        return "login";
    }

    @PostMapping("/login")
    public String postLogin(
            @RequestParam(name="username", required=true) String username,
            @RequestParam(name="password", required=true) String password,
            HttpServletResponse response,
            HttpSession session) {

        session.setAttribute("username", username);
        session.setAttribute("password", password);

        Cookie cookie = new Cookie("MY_SESSION", "supercalifragilisticexpialidocious");
//        cookie.setHttpOnly(true);
        response.addCookie(cookie);

        return "redirect:/welcome";
    }

    @GetMapping("/welcome")
    public String welcomeUser(
            @CookieValue(value = "MY_SESSION") String mySession,
            Model model,
            HttpSession session) {

        model.addAttribute("username", session.getAttribute("username"));
        model.addAttribute("password", session.getAttribute("password"));
        model.addAttribute("mySession", mySession);

        return "welcome";
    }

}
  

Now, click "Clear MY_SESSION" button. Notice that the cookie is gone as below.

With HttpOnly

Alright, this time, we'll put in the HttpOnly flag. Uncomment cookie.setHttpOnly(true); and restart the Spring Boot app. Log in again and as usual, have the Storage tab open. What happens when you click the "Clear MY_SESSION" button now? It's still there no matter how many times we click the button as shown on the console logs.

Spring Boot HttpOnly Cookie Conclusion

There you have it. A nice way of protecting your cookie. Having the HttpOnly flag set prevents thrid parties from accessing you very important cookie. It's now the task of the backend to manage the cookie. Thank you for reading.

Monday, January 1, 2024

XML to Java Class Example

Have you got a task to convert XML data to Java objects? Worry no more. Java 1.8 has got you covered.

For this example, we are going to use a subset of XML data from dictionary of medicines and devices. Making this as close as possible to real world use. The XML Schema Definition describes the structure of an XML document. The XML document is the data we are going to convert into Java objects. The files are:

  • f_vtm2_3301123.xml
  • vtm_v2_3.xsd

New Java Project

I'm using Spring Tool Suite 4 and Java 1.8 for this project. You can use whatever IDE you like. Create a new project like so:

XSD to Java Types

I've placed the above mentioned XML and XSD files under the data folder. I'm on Windows 10 so on the Command Prompt I go to the data folder. I have JDK configured on my PATH which makes me do the xjc command. For more information, run xjc -help. Below I have specified the -p option which specifies the target package.


D:\xml-to-java\data>xjc -p com.blogspot.jpllosa vtm_v2_3.xsd
parsing a schema...
compiling a schema...
com\blogspot\jpllosa\ObjectFactory.java
com\blogspot\jpllosa\VIRTUALTHERAPEUTICMOIETIES.java

A million thanks JDK for the xjc command. We now have Java classes created. Let's move the package to our src folder.

XML to Java

Let's create Main.java to drive the XML to Java object conversion. You should have something like below:


package com.blogspot.jpllosa;

import java.io.File;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;

public class Main {
    public static void main(String[] args) throws JAXBException {
        System.out.println("Starting...");
        File file = new File("data/f_vtm2_3301123.xml");
        JAXBContext jcVtm = JAXBContext.newInstance(VIRTUALTHERAPEUTICMOIETIES.class);
        Unmarshaller unmarshaller = jcVtm.createUnmarshaller();
        VIRTUALTHERAPEUTICMOIETIES vtms = (VIRTUALTHERAPEUTICMOIETIES) unmarshaller.unmarshal(file);
        
        int size = vtms.getVTM().size();
        if (size > 0) {
            System.out.printf("Virtual Therapeutic Moieties: %,d \r\n", size);
            for (int i = 0; i < 5; i++) {
                System.out.println(vtms.getVTM().get(i).getNM());
            }
        }
    }
}

Pretty cool. In a few minutes we are able to read the XML document and map it to Java objects. As Java objects, we can now do whatever we like with it. The super simple steps are open the file, create a JAXB context then the unmarshaller does all the heavy lifting. For this example we just show the first 5 VTMs out of the 3,000 or so Virtual Therapeutic Moieties. Right click on the file then Run As Java Application. The output looks like below:


Starting...
Virtual Therapeutic Moieties: 3,117 
Acebutolol
Paracetamol
Acetazolamide
Abciximab
Acarbose

There you have it. A super quick example of converting XML data to Java objects. The complete project can be cloned from github.com/jpllosa/xml-to-java.

Sunday, September 17, 2023

GitHub Actions Java with Maven Example

What do you hate the most about pull requests? Is it the formatting changes that clearly add no value and just makes the diff confusing? Or a PR that breaks the build? Don't you just hate a PR that breaks the build? In this example we will automate the code review by checking for a broken build when a pull request is opened. How is this done? By the power of GitHub actions.

GitHub Actions

We'll utilize my old GitHub project, github.com/jpllosa/tdd-junit from Test-Driven Development with Spring Boot Starter Test. Clicking on the Actions tab, GitHub will offer GitHub actions if you haven't created any. If you already have an Action then hit the New workflow button. For our example repo, we should see somthing like below:

GitHub Actions Java with Maven Example
GitHub Actions Java with Maven

Clicking Configure will create a template maven.yml file in .github/workflows folder as shown below. Editing a yml file in GitHub is also a nice way to search for actions. Commit then pull to have a local copy. Let's create a development branch and a feature branch (github-action-java-maven). What we would like to happen is when a pull request into development, it would automatically trigger a build, test and create a review comment if it's a broken build.

GitHub Actions Java with Maven Example
Finding GitHub Actions

$ git pull
From https://github.com/jpllosa/tdd-junit
 * [new branch]      development -> origin/development
 * [new branch]      github-action-java-maven -> origin/github-action-java-maven
Already up to date.

$ git checkout github-action-java-maven

maven.yml

Let's edit the maven.yml template. If you're using Spring Tool Suite and can't find your file, you might have to untick *. resources. Then you should be able to see the yml file.

GitHub Actions Java with Maven Example
Spring Tool Suite Filter
GitHub Actions Java with Maven Example
Spring Tool Suite Java Element Filter
GitHub Actions Java with Maven Example
GitHub Workflow YML file

Your yml file should look like below:


name: Java CI with Maven

on:
  pull_request:
    branches:
      - 'development'

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v3.6.0
      - name: Set up JDK
        uses: actions/setup-java@v3.12.0
        with:
          java-version: '8'
          distribution: 'semeru'
          cache: maven
      - name: Build with Maven
        run: mvn -B clean package
      - name: View context attributes
        if: ${{ failure() }}
        uses: actions/github-script@v6.4.1
        with:
          script: |
            console.log(context);
          github-token: ${{ secrets.GITHUB_TOKEN }}
          debug: true
      - name: Create PR Comment
        if: ${{ failure() }}
        uses: actions/github-script@v6.4.1
        with:
          script: |
            github.rest.pulls.createReview({
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number: context.payload.pull_request.number,
              event: "COMMENT",
              body: "It is a mortal sin to PR a broken build! :rage:",
              comments: [],
            })
          github-token: ${{ secrets.GITHUB_TOKEN }}
          debug: true

I won't be explaining workflows in too much detail. I'll let the GitHub Docs do that for me. I will however explain this YAML file. Let's start from the top. Our workflow name is Java CI with Maven. This workflow is triggered when a pull request is opened on branch development. Our workflow run is made up of a single job, with a job ID of build. The type of machine that will process our job is the latest version of Ubuntu.

Steps, the place where all the grunt work is done. First is the Checkout. It uses the Checkout GitHub action to checkout our repository. Second it Set up JDK. It uses the Setup Java JDK GitHub action to download and set up Java version 8. Third is Build with Maven which runs the mvn -B clean package command. Fourth is the View context attributes. It uses the GitHub Script GitHub action to help us write scripts in our workflow that uses the GitHub API and the workflow run context. This step is ran when any of the previous steps has failed. The failure() is a status check function. This step is for debugging purposes only, hence the log to the console call. Fifth is the Create PR Comment action. It uses the same GitHub action as the previous one. If the Build with Maven step fails then this will create a review comment saying that it is a mortal sin to create a pull request with a broken build. LOL!

GitHub Actions in Action

Let's add a unit that fails in MathFunTest.java, push it then create a PR. Our source branch is github-action-java-maven and the target development so that the workflow gets triggered.


package com.blogspot.jpllosa;

// snipped...

@SpringBootTest
public class MathFunTest {
	// snipped...
	
	@Test
	public void testThatFails() {
		fail("Not yet implemented");
	}
}

What's the outcome? You should see something like below when creating a pull request with a broken build:

GitHub Actions Java with Maven Example
Pull Request Review Comment

Remove the failing test then push the code and you should have something like below:

GitHub Actions Java with Maven Example
GitHub Actions Success

GitHub Actions Java with Maven Summary

GitHub Actions did all the work for us. From setting up the machine with all the required Java tools. Checking out the repo and then building and running the tests. And finally creating the review comment via the GitHub Script Github action API. There you have it. An automated code review comment for those PRs breaking the build. Hope you had fun reading and applying a similar GitHub action. I know I did.

Sunday, August 13, 2023

React FE Spring Boot Example

In this article we will demonstrate a React Front-End app served through Spring Boot's embedded Tomcat. Why do it this way? We can serve a React app in different ways (e.g. Apache Web Server, Nginx, etc.). What is so special about serving a React app through Spring Boot's Tomcat? There is nothing special. A reason could be that the development team is already well versed in Spring Boot with Tomcat. They have mastered configuring Tomcat and investing in a new web server (e.g. Nginx) is of little value. Another could be the deployment pipeline is already set up to do Java/Maven deployment (e.g. Jenkins). Lastly, it could be that you are a new hire and the development team already do it that way. That is, we don't have a choice but to support an existing React FE app served through Spring Boot.

Our focus will be on building the React app so that it can be deployed through a Spring Boot fat jar. We will lightly touch on starting a Spring Boot and React apps. It is assumed that the reader has knowledge regarding Node, React, Java, Maven, Spring Boot, etc. The reader should know how to set up the mentioned technology stack.

If you have been reading my past blogs, then the previous paragraphs sound so familiar. That is because I have done a similar thing for an Angular app. Essentially this article is similar to Angular FE Spring Boot Example. But I added in an extra. In my Angular FE Spring Boot Example, we had to do two steps to build the fat jar. In this article, we will build the fat jar in one step. Sounds good? Let's do it!

Building

Same as in my Angular FE Spring Boot Example, there were two ways to build a React app so that it can be deployed as single Spring Boot fat jar. First one is to use the exec-maven-plugin. Second is to use the maven-resources-plugin.

But wait. There's more. We'll be using the maven-resource-plugin in combination with frontend-maven-plugin. With these two plugins working together, we only need to run one command to build the fat jar. This means, we have leveled up from Angular FE Spring Boot Example.

Spring Initializr

Head over to Spring Initializr and it should look like below:

React FE Sprint Boot Example
Spring Initializr

React Bit

Under the root directory, create a dev folder. This is where all the React stuff goes. Assuming you have Node, Node Version Manager and Node Package Manager ready, run npx create-react-app my-app to set up a web app. We should have something like below:


D:\workspace\react-fe-spring-boot\dev>npx create-react-app my-app
Need to install the following packages:
  create-react-app@5.0.1
Ok to proceed? (y) y

Creating a new React app in D:\workspace\react-fe-spring-boot\dev\my-app.

Installing packages. This might take a couple of minutes.
Installing react, react-dom, and react-scripts with cra-template...

Created git commit.

Success! Created my-app at D:\workspace\react-fe-spring-boot\dev\my-app
Inside that directory, you can run several commands:

  npm start
    Starts the development server.

  npm run build
    Bundles the app into static files for production.

  npm test
    Starts the test runner.

  npm run eject
    Removes this tool and copies build dependencies, configuration files
    and scripts into the app directory. If you do this, you can’t go back!

We suggest that you begin by typing:

  cd my-app
  npm start

Happy hacking!

We follow the bottom two commands (i.e. cd my-app, npm start) and we should have something like below. For more details, head over to Create React App. Thank you Create React App for creating our project. The React app should be available at localhost:3000.

React FE Sprint Boot Example
React app on port 3000

Build the React project, npm run build. This will create files in the build directory. Take note of this folder as we will need it in our POM file. We should see something like below after the build:


D:\workspace\react-fe-spring-boot\dev>npm run build

> my-app@0.1.0 build
> react-scripts build

Creating an optimized production build...
Compiled successfully.

File sizes after gzip:

  46.61 kB  build\static\js\main.46f5c8f5.js
  1.78 kB   build\static\js\787.28cb0dcd.chunk.js
  541 B     build\static\css\main.073c9b0a.css

The project was built assuming it is hosted at /.
You can control this with the homepage field in your package.json.

The build folder is ready to be deployed.
You may serve it with a static server:

  npm install -g serve
  serve -s build

Find out more about deployment here:

  https://cra.link/deployment

Maven Bit

We don't need to change any Java code. The key is in the POM. We will copy the build files over to the classes/static folder and then Maven will take care of making the fat jar. Add the maven-resources-plugin like below (make sure you get the diretories right ;) ):


<plugin> 
	<artifactId>maven-resources-plugin</artifactId>
	<executions>
		<execution>
			<id>copy-resources</id>
			<phase>validate</phase>
			<goals>
				<goal>copy-resources</goal>
			</goals> 
			<configuration>
				<outputDirectory>${build.directory}/classes/static/</outputDirectory>
				<resources>
					<resource>
						<directory>dev/my-app/build</directory>
					</resource>
				</resources>
			</configuration>
		</execution>
	</executions>
</plugin>

Running the Fat Jar

Do a mvn clean package. Now, run java -jar ./target/react-fe-spring-boot-0.0.1-SNAPSHOT.jar and we should be able to see the it running on localhost:8080. It's now being served by Tomcat.

React FE Sprint Boot Example
React app on port 8080

Wait? What?! That was 2 steps to build it into a fat jar! What gives?

One Command to Build Them All

To build it all in one command, we'll enlist the help of the frontend-maven-plugin and a minor change on maven-resource-plugin. Before we do that, let's review the goals bound to the package phase, run mvn help:describe -Dcmd=package.


D:\workspace\react-fe-spring-boot>mvn help:describe -Dcmd=package

[INFO] 'package' is a phase corresponding to this plugin:
org.apache.maven.plugins:maven-jar-plugin:2.4:jar

It is a part of the lifecycle for the POM packaging 'jar'. This lifecycle includes the following phases:
* validate: Not defined
* initialize: Not defined
* generate-sources: Not defined
* process-sources: Not defined
* generate-resources: Not defined
* process-resources: org.apache.maven.plugins:maven-resources-plugin:2.6:resources
* compile: org.apache.maven.plugins:maven-compiler-plugin:3.1:compile
* process-classes: Not defined
* generate-test-sources: Not defined
* process-test-sources: Not defined
* generate-test-resources: Not defined
* process-test-resources: org.apache.maven.plugins:maven-resources-plugin:2.6:testResources
* test-compile: org.apache.maven.plugins:maven-compiler-plugin:3.1:testCompile
* process-test-classes: Not defined
* test: org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test
* prepare-package: Not defined
* package: org.apache.maven.plugins:maven-jar-plugin:2.4:jar
* pre-integration-test: Not defined
* integration-test: Not defined
* post-integration-test: Not defined
* verify: Not defined
* install: org.apache.maven.plugins:maven-install-plugin:2.4:install
* deploy: org.apache.maven.plugins:maven-deploy-plugin:2.7:deploy

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 6.947 s
[INFO] Finished at: 2023-08-06T23:04:08+01:00
[INFO] ------------------------------------------------------------------------

First change is to make maven-resources-plugin execute on the generate-sources phase. Change validate like so.


<plugin> 
	<artifactId>maven-resources-plugin</artifactId>
	<executions>
		<execution>
			<id>copy-resources</id>
			<!-- <phase>validate</phase> -->
			<phase>generate-sources</phase>
			...snipped...

Next is add the frontend-maven-plugin like so.


<plugin>
	<groupId>com.github.eirslett</groupId>
	<artifactId>frontend-maven-plugin</artifactId>
	<version>1.11.3</version>
	<configuration>
		<workingDirectory>dev/my-app</workingDirectory>
		<nodeVersion>v18.10.0</nodeVersion>
		<yarnVersion>v1.22.17</yarnVersion>
	</configuration>
	<executions>
		<execution>
			<id>install-frontend-tools</id>
			<goals>
				<goal>install-node-and-yarn</goal>
			</goals>
			<phase>validate</phase>
		</execution>
		<execution>
			<id>build-frontend</id>
			<goals>
				<goal>yarn</goal>
			</goals>
			<phase>initialize</phase>
			<configuration>
				<arguments>build</arguments>
			</configuration>
		</execution>
	</executions>
</plugin>

Now, do you know why I had to show you the Maven package phases? During the package phase, the first thing that will be done by the frontend plugin will be to install node and yarn (i.e. validate phase). After that is building the React app, yarn build (i.e. initialize phase). And lastly, resources will copy the files over (i.e. generate-sources phase). Viola! We're ready to run the build again. Remove the built files (e.g. mvn clean or delete target directory, delete the React app build directory) and then do a mvn clean package. You should see something like below. After the build you should be able to run the jar like before.


D:\workspace\react-fe-spring-boot>mvn clean package
[INFO] Scanning for projects...
[INFO]
[INFO] -------------< com.blogspot.jpllosa:react-fe-spring-boot >--------------
[INFO] Building react-fe-spring-boot 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-clean-plugin:3.2.0:clean (default-clean) @ react-fe-spring-boot ---
[INFO]
[INFO] --- frontend-maven-plugin:1.11.3:install-node-and-yarn (install-frontend-tools) @ react-fe-spring-boot ---
[INFO] Installing node version v18.10.0
[INFO] Copying node binary from C:\Users\jpllosa\.m2\repository\com\github\eirslett\node\18.10.0\node-18.10.0-win-x64.exe to D:\workspace\react-fe-spring-boot\dev\my-app\node\node.exe
[INFO] Installed node locally.
[INFO] Installing Yarn version v1.22.17
[INFO] Unpacking C:\Users\jpllosa\.m2\repository\com\github\eirslett\yarn\1.22.17\yarn-1.22.17.tar.gz into D:\workspace\react-fe-spring-boot\dev\my-app\node\yarn
[INFO] Installed Yarn locally.
[INFO]
[INFO] --- frontend-maven-plugin:1.11.3:yarn (build-frontend) @ react-fe-spring-boot ---
[INFO] Running 'yarn build' in D:\workspace\react-fe-spring-boot\dev\my-app
[INFO] yarn run v1.22.17
[INFO] $ react-scripts build
[INFO] Creating an optimized production build...
[INFO] Compiled successfully.
[INFO]
[INFO] File sizes after gzip:
[INFO]
[INFO]   46.61 kB  build\static\js\main.46f5c8f5.js
[INFO]   1.78 kB   build\static\js\787.28cb0dcd.chunk.js
[INFO]   541 B     build\static\css\main.073c9b0a.css
[INFO]
[INFO] The project was built assuming it is hosted at /.
[INFO] You can control this with the homepage field in your package.json.
[INFO]
[INFO] The build folder is ready to be deployed.
[INFO] You may serve it with a static server:
[INFO]
[INFO]   npm install -g serve
[INFO]   serve -s build
[INFO]
[INFO] Find out more about deployment here:
[INFO]
[INFO]   https://cra.link/deployment
[INFO]
[INFO] Done in 8.49s.
[INFO]
[INFO] --- maven-resources-plugin:3.2.0:copy-resources (copy-resources) @ react-fe-spring-boot ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Using 'UTF-8' encoding to copy filtered properties files.
[INFO] Copying 15 resources
[INFO]
[INFO] --- maven-resources-plugin:3.2.0:resources (default-resources) @ react-fe-spring-boot ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Using 'UTF-8' encoding to copy filtered properties files.
[INFO] Copying 1 resource
[INFO] Copying 0 resource

There you have it. Serving a Rect app through Spring Boot's fat jar and embedded Tomcat. Plus building it in one command! Grab the full repo here, github.com/jpllosa/react-fe-spring-boot

Friday, July 28, 2023

Spring ResourceTransformer Example

Here's an example of how to handle static web resources in Spring. Let us pretend that one of the business requirement of our Angular FE Spring Boot app is A/B testing. A/B testing is a method of comparing two versions of an app or webpage against each other to determine which one performs better. In short, we'll need a control webpage and a variant webpage. For the control webpage, there will be no changes. For the variant webpage, we'll need to inject "ab-testing.js" in the head of our webpage.

We will not be talking about how to do A/B testing. Instead, we will focus on how to inject the script tag in the head of our webpage. Requirements clear enough? Let's begin. Below is our control webpage.

Spring ResourceTransformer Example
Control Page

We will utilize the code from my previous blog, Angular FE Spring Boot Example and you can clone the code here, github.com/jpllosa/angular-fe-spring-boot.

Two Strategies

As far as I know, there are two ways to fulfill this business requirement.

First one is to insert the script tag when ngOnInit is called in a Component class.

Second is to use Spring's ResourceTransformer. We will use this method because the script tag is injected server side. Prior to serving the HTML file, it is transformed. The first strategy happens on the client side which is why I think this is a better solution. This also seems better because the "ab-testing.js" is already in the head prior to DOM manipulation by Angular. So let's get to it.

POM

Update the POM file. We need the dependency below for IOUtils.


<dependency>
	<groupId>commons-io</groupId>
	<artifactId>commons-io</artifactId>
	<version>2.11.0</version>
</dependency>

index.html

Add a marker text in dev/my-app/src/index.html, like so:


<head>
  ... snipped ...
  <link rel="icon" type="image/x-icon" href="favicon.ico">
  <!-- INJECT_HERE -->
</head>

Java Code

Create a com.blogspot.jpllosa.transformer package and add the following files below:


package com.blogspot.jpllosa.transformer;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;


@Configuration
public class IndexTransformationConfigurer extends WebMvcConfigurerAdapter {
	@Override
	public void addResourceHandlers(ResourceHandlerRegistry registry) {
		registry.addResourceHandler("index.html")
			.addResourceLocations("classpath:/static/")
			.resourceChain(false)
			.addTransformer(new IndexTransformer());
	}
}

We create the class above so that we can add a handler when serving the index.html file. Here it specifies that the index.html file from the classpath:/static/ location will have to pass through a Transformer. Other static resources will not be affected. We are not caching the result of the resource resolution (i.e. resourceChain(false)).


package com.blogspot.jpllosa.transformer;

import org.apache.commons.io.IOUtils;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.resource.ResourceTransformer;
import org.springframework.web.servlet.resource.ResourceTransformerChain;
import org.springframework.web.servlet.resource.TransformedResource;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@Service
public class IndexTransformer implements ResourceTransformer {
	@Override
	public Resource transform(HttpServletRequest request, Resource resource, ResourceTransformerChain chain) throws IOException {
		String html = IOUtils.toString(resource.getInputStream(), "UTF-8");
		html = html.replace("<!-- INJECT_HERE -->", 
			"<script src=\"//third.party.com/ab-testing.js\"> </script>");
		return new TransformedResource(resource, html.getBytes());
	}
}

We've kept our transformer simple for this example. The code is readable enough as to what it is doing, isn't it? We are replacing the placeholder text with the script tag. Job done.

Demonstration

If you have read the Angular FE Spring Boot Example then you should know what to do next. In case you have not. I'll reiterate.

Build the Angular project, ng build on dev/my-app. This will create files in the dist directory.

Run mvn clean package on the project root directory. After that, run java -jar ./target/angular-fe-spring-boot-0.0.1-SNAPSHOT.jar

and we should be able to see something like below running on localhost:8080. Is "ab-testing.js" there?
Spring ResourceTransformer Example
Variant Page

There you have it. A quick example of how to handle or transform static web resources in Spring. Grab the full repo here, github.com/jpllosa/angular-fe-spring-boot

Monday, June 26, 2023

Angular FE Spring Boot Example

In this article we will demonstrate an Angular Front-End app served through Spring Boot's embedded Tomcat. Why do it this way? We can serve an Angular app in different ways (e.g. Apache Web Server, Nginx, etc.). What is so special about serving an Angular app through Spring Boot's Tomcat? There is nothing special. A reason could be that the development team is already well versed in Spring Boot with Tomcat. They have mastered configuring Tomcat and investing in a new web server (e.g. Nginx) is of little value. Another could be the deployment pipeline is already set up to do Java/Maven deployment (e.g. Jenkins). Lastly, it could be that you are a new hire and the development team already do it that way. That is, we don't have a choice but to support an existing Angular FE app served through Spring Boot.

Our focus will be on building the Angular app so that it can be deployed through a Spring Boot fat jar. We will lightly touch on starting a Spring Boot and Angular apps. It is assumed that the reader has knowledge regarding Node, Angular CLI, Java, Maven, Spring Boot, etc. The reader should know how to set up the mentioned technology stack.

Two Ways to Build

As far as I know, there are two ways to build an Angular app so that it can be deployed as single Spring Boot fat jar.

First one is to use the exec-maven-plugin. This plugin runs the ng build and the outputPath of the Angular app must point to src/main/resources/static so that Angular dist files are bundled in the jar during packaging.

Second is to use the maven-resources-plugin. This plugin copies the Angular dist files to the classes/static directory of the Spring Boot app. This is the method we will be demonstrating. This way seems to be cleaner because it doesn't populate any files under the Java section of the code. The Angular build code goes straight into the fat jar. This way, all Angular development code and work remain in a separate directory from the Java side of things. And only become one when we build the fat jar. So let's get to it.

Spring Initializr

Head over to Spring Initializr and it should look like below:

Spring Initialzr

Angular Bit

Under the root directory, create a dev folder. This is where all the Angular stuff goes. Assuming you have Node, Node Version Manager and Node Package Manager ready, run npm install -g @angular/cli to install the Angular CLI. Check your installation, ng -v. We should have something like below:

Angular Version

Create an Angualr starter app under the dev folder, ng new my-app. This will create the scaffolding. Change directory to my-app, cd my-app, then run g serve. Open localhost:4200, we should have something like below:

My-App on 4200

Build the Angular project, ng build. This will create files in the dist directory. Take note of this folder as we will need it in our POM file. We should see something like below after the build:

Built Files

These are the built files found in the dist folder.

dist Contents

Maven Bit

We don't need to change any Java code. The key is in the POM. We will copy the dist files over to the classes/static folder and then Maven (mvn clean package) will take care of making the fat jar. Add the maven-resources-plugin like below (make sure you get the diretories right ;) ):


<plugin> 
	<artifactId>maven-resources-plugin</artifactId>
	<executions>
		<execution>
			<id>copy-resources</id>
			<phase>validate</phase>
			<goals>
				<goal>copy-resources</goal>
			</goals> 
			<configuration>
				<outputDirectory>${build.directory}/classes/static/</outputDirectory>
				<resources>
					<resource>
						<directory>dev/my-app/dist/my-app</directory>
					</resource>
				</resources>
			</configuration>
		</execution>
	</executions>
</plugin>

Below is the key bit when running mvn clean package. This tells us the files copied from Angular to Java classes. The number of files should match!

mvn clean package

Demonstration

Now, run java -jar ./target/angular-fe-spring-boot-0.0.1-SNAPSHOT.jar

and we should be able to see the it running on localhost:8080. Did you notice that it's on a new port this time? It's now being served by Tomcat.
My-App Served Through Spring Boot's Tomcat

There you have it. A nice way of serving an Angular app through Spring Boot's fat jar and embedded Tomcat. Grab the full repo here, github.com/jpllosa/angular-fe-spring-boot

Monday, May 8, 2023

Spring Boot WebSocket Example

In this article we will demonstrate the benefit of using Spring Boot and WebSocket to create an interactive web application. WebSocket is layer above TCP (Transmission Control Protocol). We will do STOMP (Streaming Text Oriented Messaging Protocol) messaging between client and server. STOMP is the protocol operating on top of WebSocket.

In my previous article, Spring Boot JMS Example, we tried to solve the UI "freezing" problem by incorporating JMS into our web application. Incorporating WebSocket is another way to solve the UI "freezing" problem. As mentioned before, we would like to avoid "blocking" the user interface or experience. If we know a user request will take a long time then we'll need a mechanism to allow the UI to continue working and not appear to "freeze". Similar to what SwingUtilities.invokeLater does. An interactive web application is a common use of WebSocket.

We will build upon the code from Spring Boot JMS Example. We will not dive into much detail on how to build the project as our focus will be on the practical application of WebSocket in our app. If you want to create the project yourself, please look at my other blogs (e.g. Spring Boot MockMvc Example) where I used Spring Initializr to create one.

That said, you can clone the finished project here, github.com/jpllosa/jms-async. We'll just go straight into explaining the code.

Dependencies

The following are the dependencies added to the POM. We won't be using any Bootstrap but just in case you want to make it look pretty, it is ready for you.

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>webjars-locator-core</artifactId>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>sockjs-client</artifactId>
	<version>1.0.2</version>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>stomp-websocket</artifactId>
	<version>2.3.3</version>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>bootstrap</artifactId>
	<version>3.3.7</version>
</dependency>
<dependency>
	<groupId>org.webjars</groupId>
	<artifactId>jquery</artifactId>
	<version>3.1.1-1</version>
</dependency>

WebSocket Configuration


package com.blogspot.jpllosa.websocket;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

	@Override
	public void configureMessageBroker(MessageBrokerRegistry config) {
		config.enableSimpleBroker("/topic");
		config.setApplicationDestinationPrefixes("/app");
	}
	
	@Override
	public void registerStompEndpoints(StompEndpointRegistry registry) {
		registry.addEndpoint("/fibonacci-websocket").withSockJS();
	}
}

The above code enables WebSocket and STOMP messaging in Spring. This is a Spring configuration class as the descriptive annotation says @Configuration. This class also enables WebSocket message handling, backed by a message broker (@EnableWebSocketMessageBroker).

In WebSocketConfig, we override two default methods. First, the configureMessageBroker() method which implements the default method in WebSocketMessageBrokerConfigurer to configure the message broker. The enableSimpleBroker() enables a simple memory-based message broker to carry the messages back to the client on destinations prefixed with /topic. It also designates the /app prefix for messages that are bound for methods annotated with @MessageMapping. This prefix will be used to define all the message mappings. For example, /app/fibonacci-id is the endpoint that the FibonacciController.fibonacciCompute() method is mapped to handle.

The registerStompEndpoints() method registers the /fibonacci-websocket endpoint, enabling SockJS fallback options so that alternate transports can be used if WebSocket is not available. The SockJS client will attempt to connect to /fibonacci-websocket and use the best available transport (websocket, xhr-streaming, xhr-polling, etc.).

Message-handling Controller


package com.blogspot.jpllosa.controller;

// ...imports snipped...

@Controller
public class FibonacciController {

	@Autowired
	JmsTemplate jmsTemplate;
	
	@Autowired
	HashMap myMap;
	
	private Fibonacci fib = new Fibonacci();
	
	// ...code snipped...
	
	@GetMapping("/stomp-fib")
	public String stompFib(@RequestParam(name="numbers", required=true) String numbers, Model model) {
		final String PENDING = "pending";
		
		UUID uuid = UUID.randomUUID();
		
		FibonacciMessage fibMsg = new FibonacciMessage(uuid.toString(),
				Integer.parseInt(numbers),
				PENDING);
		
		myMap.put(fibMsg.getId(), fibMsg);

		model.addAttribute("numbers", fibMsg.getNumbers());
		model.addAttribute("result", fibMsg.getResult());
		model.addAttribute("id", fibMsg.getId());
		
		return "stomp-fib";
	}
	
	@MessageMapping("/fibonacci-id")
	@SendTo("/topic/fibonacci-result")
	public FibonacciMessage fibonacciCompute(StompMessage message) {
		FibonacciMessage fibMsg = myMap.get(message.getId());
		
		if (fibMsg.getResult().equals("pending")) {
			String result = fib.fibonacci(fibMsg.getNumbers());
			fibMsg.setResult(result);
			System.out.println("stomp websocket: " + fibMsg);
			
			myMap.put(fibMsg.getId(), fibMsg);
		}
		
		return fibMsg;	
	}
}

In Spring, STOMP messages can be routed to @Controller classes. Tha above code is added into our controller. The fibonacciCompute() method is called when a message is sent to the fibonacci-id destination. The payload is automatically bound to StompMessage. The FibonacciMessage is then broadcasted to all subscribers of /topic/fibonacci-result when the method is finished.

This methods just checks the map if it contains the ID of the fibonacci compute request. If the compute request is still pending, it then computes the fibonacci sequence then places it back into the map. Otherwise, it returns with a result. It will print the result on the console as well (e.g. stomp websocket: FibonacciMessage [id=0170491e-b44b-4831-9e2f-29422fc60482, numbers=5, result=1, 1, 2, 3, 5]) when the fibonacci sequence has been computed.

The stompFib simply serves the stomp-fib.html web content.

Message Model


package com.blogspot.jpllosa.websocket;

public class StompMessage {
	String id;
	
	public StompMessage() {
	}
	
	public StompMessage(String id) {
		this.id = id;
	}

	public String getId() {
		return id;
	}

	public void setId(String id) {
		this.id = id;
	}
}

As explained earlier, the STOMP payload is automatically bound to StompMessage. Spring handles the JSON marshalling automatically, we just need to provide the model. This models the message that carries the ID.

JavaScript Client


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head> 
    <title>Fibonacci Result via WebSocket</title> 
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <script src="/webjars/jquery/jquery.min.js"></script>
    <script src="/webjars/sockjs-client/sockjs.min.js"></script>
    <script src="/webjars/stomp-websocket/stomp.min.js"></script>
</head>
<body>
	<h1>Fibonacci Result via WebSocket</h1>
	<p th:text="'Numbers: ' + ${numbers}" />
    <p id="result" th:text="'Result: ' + ${result}" />
    <p>The result will update in a few seconds (<span id="sec"></span>).</p>
</body>

<script th:inline="javascript">
$(document).ready(function() {
	var socket = new SockJS("/fibonacci-websocket");
	var stompClient = Stomp.over(socket);
	var $result = $("#result");
	var $sec = $("#sec");
	var timer;
	var counter = 1;
	
	stompClient.connect({}, function(frame) {
		console.log("connected");
		stompClient.subscribe("/topic/fibonacci-result", function(message) {
			var fibMsg = JSON.parse(message.body);
			$result.text("Result: " + fibMsg.result);
			clearInterval(timer);
			
			setTimeout(function() {
				if (stompClient !== null) {
			        stompClient.disconnect();
			        console.log("disconnected");
			    }
			}, 5000);
		});
		
		setTimeout(function() {
			stompClient.send("/app/fibonacci-id", {}, JSON.stringify({
				id: [[${id}]],
			}));
			
			timer = setInterval(function() {
				$sec.text(counter);
				counter++;
			}, 1000);
		}, 1000);
	});
});
</script>
</html>

Our stomp-fib.html has a lot more code compared to the HTMLs in Spring Boot JMS Example. Here we import the sockjs.min.js and stomp.min.js libraries that will be used to communicated with our server through STOMP over WebSocket.

Here is what the JavaScript code section does. When the HTML document is fully loaded it instantiates a SockJS object which opens a connection to /fibonacci-websocket then use STOMP over it. We then subscribe for messages to the /topic/fibonacci-result destination. Whenever a message is received, it will update the DOM and clear the timer, then disconnect. Upon connection, we wait for a second then send a STOMP message to trigger the fibonacci computation and then interactively update the DOM showing the elapsed time in seconds.

Demonstration

To demonstrate, open the /stomp-fib endpoint (e.g. http://localhost:8080/stomp-fib?numbers=5). Don't forget to run the Spring Boot App! You should have something like below:

There you have it. Another way of solving the UI "freezing" problem with Spring Boot WebSocket.