Showing posts with label spring boot. Show all posts
Showing posts with label spring boot. Show all posts

Sunday, October 13, 2024

Securing a Secure Web Application Example

How do you secure an already secure web application? Well, the title is that because we are going to build upon Spring's Securing a Web Application guide. You can follow the Spring guide to build your secure web app and then come back here to make it more secure. Or you can just keep on reading, I'm going to share the repo anyway.

Tools

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.3.3
  • Windows 11
  • Ubuntu 22.04.3 LTS (GNU/Linux 5.15.153.1-microsoft-standard-WSL2)
  • OpenSSL 3.0.2 15 Mar 2022 (Library: OpenSSL 3.0.2 15 Mar 2022)

Prerequisite

Alright. I'm assuming you followed the Spring guide to securing a web app and got it running. What you did was add Spring Security on your Spring Boot app which then protects your resource endpoints. You have secured it with a login page and only authorized users can access certain resources. You should have something like below.

I have added 127.0.0.1 jpllosa.tech on the Windows hosts (C:\Windows\System32\drivers\etc) file. We can't use localhost for our certificate later.

Requirement

And now the problem we are going to solve. Imagine this secure web app is runnning on our company network. As it stands anyone can see the website on the browser (e.g. by going to http://jpllosa.tech:8080). They would see the welcome and login pages. Now, we don't want just anybody to see the website. We only want, let's say level 1 clearance personnel and machine to see it. What do we do? Any ideas? Yes, we are going to serve the web app over HTTPS (Hypertext Transfer Protocol Secure) and then what's next? Yes, install our own certificate authority as a trusted certificate on the browser of the level 1 personnel's laptop (e.g. an IT services job). Is the requirement clear enough?

Create Self-signed Certificate

We're going to do PEM (Privacy Enhanced Mail) certificates. Thankfully, Spring Boot supports this. We don't have to deal with the Java specific JKS (Java KeyStore) format, which can be tricky to configure sometimes.

Let's start by creating a private key. This is the most important component of our certificate and helps to enable encryption. So open your WSL and generate a private key like so.

  
$ openssl genrsa -out jpllosa.tech.key 2048
$ ls
jpllosa.tech.key
  

Second, create a certificate signing request. A certificate signing request (CSR) includes the public key and some additional information like organization and country. We need this because we want our certificate signed. Create the CSR with our private key like below. I've left the challenge password and optional company name blank. The important field is Common Name, which should be the fully qualified domain name (FQDN) of our domain.

  
$ openssl req -key jpllosa.tech.key -new -out jpllosa.tech.csr
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:AU
State or Province Name (full name) [Some-State]:Some-State
Locality Name (eg, city) []:City
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Internet Widgets
Organizational Unit Name (eg, section) []:section
Common Name (e.g. server FQDN or YOUR name) []:jpllosa.tech
Email Address []:webmaster@jpllosa.tech

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:
$ ls
jpllosa.tech.csr  jpllosa.tech.key
  

Next, we create a self-signed certificate with our private key and CSR. This is a certificate signed with its own private key. This certificate isn't trusted since it is self-signed. Create a self-signed certificate like so.

  
$ openssl x509 -signkey jpllosa.tech.key -in jpllosa.tech.csr -req -days 365 -out jpllosa.tech.crt
Certificate request self-signature ok
subject=C = AU, ST = Some-State, L = City, O = Internet Widgets, OU = section, CN = jpllosa.tech, emailAddress = webmaster@jpllosa.tech
$ ls
jpllosa.tech.crt  jpllosa.tech.csr  jpllosa.tech.key
  

Now we need something for the browser (i.e. client side) as we will need to install a trusted certificate for it. We need a self-signed root certificate authority (CA) certificate. We can do this by being our own certificate authority. Let's create a self-signed root CA like so.

  
$ openssl req -x509 -sha256 -days 1825 -newkey rsa:2048 -keyout myCA.key -out myCA.crt
.....+...+...+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*.+....+........+...+...+.+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*....+..+.............+.................+..........+..+............+.+........+.+..............+....+..+......+.......+..+.............+..+.........+............+....+..............+......+...+...............+...+....+...........+..........+..............+....+...............+..+....+..............+......+.+...+.........+.....+...+.........+....+...........+...+.+.........+..+......+...+....+.....+.+...........+...................+..+...+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.................+..........+.....+.......+...+...+..+..........+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*..+..+.............+..+.+...+..+.......+..+...+.+...........+....+.....+.........+.+.....+.......+..+.......+......+..+.+.....+....+.....+...+.......+..+....+.....+.......+.....+....+........+......+....+.....+...+.......+......+.....+.......+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*.+...+...+......+...........+.......+...+..+...+.......+..+....+..+.......+........+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Enter PEM pass phrase: password
Verifying - Enter PEM pass phrase: password
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:AU
State or Province Name (full name) [Some-State]:Some-State
Locality Name (eg, city) []:City
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Internet Widgets
Organizational Unit Name (eg, section) []:Section
Common Name (e.g. server FQDN or YOUR name) []:my.com
Email Address []:webmaster@my.com
$ ls
jpllosa.tech.crt  jpllosa.tech.csr  jpllosa.tech.key  myCA.crt  myCA.key
  

Next, we sign our CSR with our root CA. The result will be, the CA-signed certificate will be in the jpllosa.tech.crt file. This is a working certificate. Create like so.

  
$ openssl x509 -req -CA myCA.crt -CAkey myCA.key -in jpllosa.tech.csr -out jpllosa.tech.crt -days 365 -CAcreateserial
Certificate request self-signature ok
subject=C = AU, ST = Some-State, L = City, O = Internet Widgets, OU = section, CN = jpllosa.tech, emailAddress = webmaster@jpllosa.tech
Enter pass phrase for myCA.key: password
$ ls
jpllosa.tech.crt  jpllosa.tech.csr  jpllosa.tech.key  myCA.crt  myCA.key
  

Even though we have created a working certificate, it will still be flagged by the browser. What we need is to add the subjectAltName. We need a SAN extension. X.509 certificates need information about the domain for which this particular certificate is issued for. To align with SAN extension stadards, we need to create a configuration text file then add the configuration to the certificate. Like so. How's your vi skills?

  
$ vi jpllosa.tech.ext
$ cat jpllosa.tech.ext
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:false
subjectAltName = @alt_names
[alt_names]
DNS.1 = jpllosa.tech
$ openssl x509 -req -CA myCA.crt -CAkey myCA.key -in jpllosa.tech.csr -out jpllosa.tech.crt -days 365 -CAcreateserial -extfile jpllosa.tech.ext
Certificate request self-signature ok
subject=C = AU, ST = Some-State, L = City, O = Internet Widgets, OU = section, CN = jpllosa.tech, emailAddress = webmaster@jpllosa.tech
Enter pass phrase for myCA.key: password
$ ls
jpllosa.tech.crt  jpllosa.tech.csr  jpllosa.tech.ext  jpllosa.tech.key  myCA.crt  myCA.key
  

Finally, we got a working certificate that meets all the SAN requirements. SAN makes the certificates more secure and it allows the definition of several domains or IP addresses and we can use a single certificate across multiple domains. View the certificate like so. Spot the important bit, Subject Alternative Name.

  
$ openssl x509 -text -noout -in jpllosa.tech.crt
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            78:db:cd:5a:18:8d:46:0f:74:55:16:86:dc:f2:74:95:1b:c4:58:0e
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = AU, ST = Some-State, L = City, O = Internet Widgets, OU = Section, CN = my.com, emailAddress = webmaster@my.com
        Validity
            Not Before: Oct  5 16:41:02 2024 GMT
            Not After : Oct  5 16:41:02 2025 GMT
        Subject: C = AU, ST = Some-State, L = City, O = Internet Widgets, OU = section, CN = jpllosa.tech, emailAddress = webmaster@jpllosa.tech
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    00:ac:70:3c:57:da:fc:f4:b4:f6:4f:f4:3d:64:9f:
                    27:ca:09:d9:b6:4e:4d:89:2f:db:4b:6e:7c:18:4b:
                    af:5c:b4:80:cc:42:0a:cd:1e:15:1e:2d:be:71:e4:
                    6c:59:40:82:c5:ac:29:3b:fa:51:0b:6b:20:11:e5:
                    7d:1e:92:f7:e9:9d:f4:15:31:47:64:ec:1b:a5:14:
                    00:6e:c0:98:76:be:71:9d:c6:97:14:47:aa:30:b1:
                    ef:c4:b4:6b:b0:31:22:25:65:21:ab:35:21:ac:7b:
                    6a:f7:c0:78:d2:90:7d:33:d0:3c:dc:db:21:8e:75:
                    ff:04:83:bd:e6:9a:5d:79:70:a2:59:21:ff:51:20:
                    ea:74:d1:78:89:61:49:f6:6c:87:85:e2:0f:0c:f7:
                    b4:be:2b:79:88:28:fc:f7:50:ef:c1:e6:63:3e:a4:
                    0e:3c:71:18:97:55:5c:76:18:80:67:af:84:0a:16:
                    98:79:aa:00:00:77:a4:1b:97:bd:9c:41:50:13:89:
                    0c:63:29:51:84:7f:95:67:b7:f0:94:2b:b4:bb:50:
                    5e:6f:66:d1:06:4a:97:d6:3a:ac:6e:90:59:22:2c:
                    d3:09:a1:4b:e7:a1:3c:96:f6:b5:9c:25:5f:5b:cb:
                    be:a5:41:11:da:dc:a5:1b:cd:86:4d:a1:bd:44:1c:
                    44:c5
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Authority Key Identifier:
                17:B0:3C:87:D6:06:3C:54:21:F9:0B:8D:94:46:EF:F9:FA:A9:5C:8C
            X509v3 Basic Constraints:
                CA:FALSE
            X509v3 Subject Alternative Name:
                DNS:jpllosa.tech
            X509v3 Subject Key Identifier:
                93:B7:A9:CB:E3:56:A7:38:C1:8A:E6:6E:0A:0B:4B:4C:FE:1A:B7:FA
    Signature Algorithm: sha256With
...snipped...
  

Configure Spring Boot with PEM Certificates for TLS/SSL Communication

And now the easy part. Thanks to Spring Boot. All we have to do is add some properties on the application.properties file to enable SSL communication. Update the properties file like so.

  
server.port=8443
server.ssl.certificate=classpath:jpllosa.tech.crt
server.ssl.certificate-private-key=classpath:jpllosa.tech.key
  

Place jpllosa.tech.crt and jpllosa.tech.key under /src/main/resources so it can be picked up in the classpath. Try running the web app and go to the site. What do you see? Do you see something like below?

Our HTTPS site is still getting a warning. You could advance and accept the risk but what we want is for the browser to trust the site.

Browser Configuration

We need to import the root CA into the browser so our site can be trusted. These steps are specific to Firefox. Go to Settings > Privacy & Security > View Certificates.

Import myCA.crt and trust it to identify websites. You should be able to see Internet Widgets installed.

Let's try accessing the web app again. What does it say now? The warning is gone and you can go to the welcome and login pages.

Securing a Secure Web Application Summary

What a journey. To secure a Spring Boot app with PEM certificates for SSL communication was the easy bit. Just a few lines in the properties file. The arduous bit was the creation of the PEM certificates. Lastly we had to import our root CA to the browser so our site can be trusted. There you have it. We got to secure a secured web app. Was the requirement satisfied? . As usual, entire code is available at github.com/jpllosa/cert-web-app. Thank you for reading.

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.

Tuesday, April 18, 2023

Spring Boot JMS Example

In this article we will demonstrate the benefit of using Spring Boot Java Message Service (JMS) for asynchronous processing. JMS is used as a way to allow asynchronous request processing in our web application. One reason you would to like do this is to avoid "blocking" the user interface or experience. If you know a user request will take a long time then you'll need a mechanism to allow the UI to continue working and not appear to "freeze". Similar to what SwingUtilities.invokeLater does. There are also other uses for JMS but generally and most common is queueing requests that take too long to process.

We will not dive into much detail on how to build the project as our focus will be on the practical application of JMS 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 start one. Here's what your Spring Initializr should look like:

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

JMS Configuration


package com.blogspot.jpllosa;

import java.util.HashMap;

import javax.jms.ConnectionFactory;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jms.DefaultJmsListenerContainerFactoryConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.jms.annotation.EnableJms;
import org.springframework.jms.config.DefaultJmsListenerContainerFactory;
import org.springframework.jms.config.JmsListenerContainerFactory;
import org.springframework.jms.support.converter.MappingJackson2MessageConverter;
import org.springframework.jms.support.converter.MessageConverter;
import org.springframework.jms.support.converter.MessageType;

import com.blogspot.jpllosa.messaging.FibonacciMessage;

@SpringBootApplication
@EnableJms
public class JmsAsynchronousProcessingApplication {

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

	@Bean
	public JmsListenerContainerFactory myFactory(ConnectionFactory connectionFactory,
			DefaultJmsListenerContainerFactoryConfigurer configurer) {
		DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();

		configurer.configure(factory, connectionFactory);

		return factory;
	}

	@Bean
	public MessageConverter jacksonJmsMessageConverter() {
		MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
		converter.setTargetType(MessageType.TEXT);
		converter.setTypeIdPropertyName("_type");
		return converter;
	}
	
	@Bean
	public HashMap myMap() {
		return new HashMap();
	}
}

In the above code, we have configured JMS with Spring to send and receive messages. The @SpringBootApplication is a convenience annotation that does the following:

  • Tags the class a source of beand definitions akin to @Configuration.
  • Tells Spring Boot to add beans based on the classpath settings, other beans and property settings.
  • Tells Spring Boot to find other components, configurations and services in the package akin to @ComponentScan.
The @EnableJms triggers the discovery of methods annotated with @JmsListener and then Spring Boot creates the message listener for this. The myFactory is referenced in the JmsListener of the receiver, FibonacciCompute.receiveFibonacciMessage(). We use Jackson to serialize the content in text format. Spring Boot detects the MessageConverter and associates it to both the default JmsTemplate and any JmsListenerContainerFactory.

Last but not the least is the so called "database". Could have created a static map here but opted instead to make it a bean so I can just auto wire it.

Message Reveiver


package com.blogspot.jpllosa.messaging;

import java.util.HashMap;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;

import com.blogspot.jpllosa.service.Fibonacci;

@Component
public class FibonacciCompute {
	
	@Autowired
	HashMap myMap;
	
	private Fibonacci fib = new Fibonacci();
	
	@JmsListener(destination = "fibonacciCompute", containerFactory = "myFactory")
	public void receiveFibonacciMessage(FibonacciMessage fibMsg) {
		
		String result = fib.fibonacci(fibMsg.getNumbers());
		fibMsg.setResult(result);
		System.out.println(fibMsg);
		
		myMap.put(fibMsg.getId(), fibMsg);
	}

}

The JmsListener annotation defines the name of the destination that this method should listen to and the reference to the JmsListenerContainerFactory to use. When a message is received, it computes the fibonacci sequence. Once it has a result, it is stored in the map "database". This map will then be accessed later as you will see. It will print the result on the console as well (e.g. FibonacciMessage [id=f2f175e3-05d9-4ea2-89ae-02777242c7de, numbers=8, result=1, 1, 2, 3, 5, 8, 13, 21]) when the fibonacci sequence has been computed.

Controller and Service


package com.blogspot.jpllosa.controller;

import java.util.HashMap;
import java.util.UUID;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;

import com.blogspot.jpllosa.messaging.FibonacciMessage;
import com.blogspot.jpllosa.service.Fibonacci;

@Controller
public class FibonacciController {
	
	@Autowired
	JmsTemplate jmsTemplate;
	
	@Autowired
	HashMap myMap;

	@GetMapping("/fib")
	public String fib(@RequestParam(name="numbers", required=true) String numbers, Model model) {
		Fibonacci fib = new Fibonacci();
		model.addAttribute("numbers", numbers);
		model.addAttribute("result", fib.fibonacci(Integer.parseInt(numbers)));
		
		return "fib";
	}
	
	@GetMapping("/jms-fib")
	public String JmsFib(@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);
		
		jmsTemplate.convertAndSend("fibonacciCompute", fibMsg);

		model.addAttribute("numbers", fibMsg.getNumbers());
		model.addAttribute("result", fibMsg.getResult());
		model.addAttribute("id", fibMsg.getId());
		
		return "jms-fib";
	}
	
	@GetMapping("/fib-updates/{id}")
	public String FibUpdates(@PathVariable String id, Model model) {
		
		FibonacciMessage fibMsg = myMap.get(id);

		if (fibMsg != null) {
			model.addAttribute("numbers", fibMsg.getNumbers());
			model.addAttribute("result", fibMsg.getResult());
		} else {
			model.addAttribute("numbers", "");
			model.addAttribute("result", "");
		}
		
		return "fib-updates";
	}
}

package com.blogspot.jpllosa.service;

public class Fibonacci {

	public String fibonacci(int numbers) {
		try {
			Thread.sleep(5000);
		} catch (InterruptedException e) {
			// don't care
		}
		
		if (numbers < 0) return "";
		if (numbers == 0) return "0";
		if (numbers == 1) return "1";
		
		StringBuilder sb = new StringBuilder();
		int num1 = 0;
		int num2 = 1;
		sb.append(num2);
		for (int i = 1; i < numbers; i++) {
			if (sb.length() > 0) {
				sb.append(", ");
			}
			
			int sumOfPreviousTwo = num1 + num2;
			sb.append(sumOfPreviousTwo);
			num1 = num2;
			num2 = sumOfPreviousTwo;
		}
		
		return sb.toString().trim();
	}
	
	private int fibonacciCompute(int numbers) {
		if (numbers == 1 || numbers == 2) return 1;

		return fibonacciCompute(numbers - 2) + fibonacciCompute(numbers - 1);
	}
	
	public String fibonacciRecursion(int numbers) {
		if (numbers == 0) return "0";
		
		StringBuilder sb = new StringBuilder();
		for (int i = 1; i <= numbers; i ++) {
			if (sb.length() > 0) {
				sb.append(", ");
			}
			
			sb.append(fibonacciCompute(i));
		}
		
		return sb.toString().trim();
	}
	
}

I'll explain the code along with the demonstration below.

Demonstration

First off, let's see what happens when we don't use JMS and a long process is going to take place. To demonstrate this, hit the /fib endpoint (e.g. http://localhost:8080/fib?numbers=5). I've intentionally added the 5 second delay to simulate a long process. As you will notice, the user inteface took a long while to render the page. A user might assume that the web page has frozen. We don't want that. Opening web tools + Network tab, we see the turle icon when we did the request. The turtle means it had a slow server response time (x secs). The recommended limit is 500ms.

Now let's see what happens when we use JMS and a long process is going to take place. To demonstrate this, hit the /jms-fib endpoint (e.g. http://localhost:8080/jms-fib?numbers=8). As you might have noticed, there is a little difference in the implementation. We had to create a unique ID associating the request. We also returned a "pending" message right away along with a link the user can click for updates of the fibonacci computation request. When we received the request it was simple call to convertAndSend. With the destination specified and plain old Java object, of course. In contrast to the previous /fib call, we got a web page straight away and it took like 5ms.

For the results, we click the link below (e.g. http://localhost:8080/fib-updates/) and if it is still pending we can reload the page. Then we see something like below:

Now you've seen JMS in action. We have demonstrated the benefit of Spring Boot Java Message Service for asynchronous processing. There you have it. A simple example of how to use JMS to allow asynchronous request processing in our web application and not "freezing" the UI.

Wednesday, March 1, 2023

Spring Session with Redis Example

In this article, we will talk about Spring session with Redis. We will demonstrate the difference between maintaining a session with HttpSession versus a session with Redis. Why would we want our session to be backed with Redis? Read on and we'll find out why. By the way, Spring session can also persist data using JDBC, Gemfire, or MongoDB.

Overview

Offloading session management from the servlet container (e.g. Tomcat's HTTP session) because of its limitations and eating up of server memory are some reasons we want to persist session data somewhere else. The most likely reason you'ld want to use Spring session with Redis is when you are scaling up. For example, you have multiple instances of a microservice running behind a load balancer and you'ld want to keep track of session state. Why have multiple instances? Because you want zero downtime of your service. With Redis, our session data can be shared between multiple microservices. By persisting sessions, multiple application instances can serve the same session and individual instances can crash without impacting other application instances.

Redis

I will not be providing instructions on how to set up Redis on your local machine. For that, I'll point you to Redis and make your way to the Get Started section. I'm using Windows 10 so I followed the Install on Windows instructions. I also had to set up my Windows Subsystem for Linux and got Ubuntu running on my local machine. In the end and by default, your Redis server should be running on port 6379 and you should be able to ping it. You should have something like below:

Spring Session with Redis
Redis Running

Spring Session with Redis Project

I will not provide instructions on how to create this project as well. My focus will be demostrating HttpSession versus Spring session with Redis. Anyway, if you want to create the project, you can follow the steps from my previous blogs (e.g. Spring Boot MockMvc Example). Here is what your Spring Initialzr would look like:

Spring Session with Redis
Spring Initializr

Here is the finished project, clone it from github.com/jpllosa/spring-session-redis.

@RestController


package com.blogspot.jpllosa.controller;

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

import javax.servlet.http.HttpSession;

import com.blogspot.jpllosa.model.UserData;

@RestController
public class HttpSessionController {

	@PostMapping("/save-to-http-session")
	public HttpEntity saveToHttpSession(@RequestBody UserData userData, HttpSession httpSession) {
		httpSession.setAttribute("userData", userData);
		return new ResponseEntity<>("", HttpStatus.ACCEPTED);
	}
	
	@GetMapping("/get-from-http-session")
	public HttpEntity getFromHttpSession(HttpSession httpSession) {
		return new ResponseEntity<>((UserData) httpSession.getAttribute("userData"), HttpStatus.OK);
	}
}

This is where all the action happens. Our microservice is pretty simple really. It accepts user data via an HTTP POST then we can send it back via an HTTP GET if it was saved. The response to a successful POST is a 202, meaning it was accepted.

Background

In our demonstration, we will run two instances of our microservice. MS 80, microservice on port 8080 and MS 81, microservice on port 8081. We'll pretend both are behind a load balancer. A network load balancer will decide where in coming requests go to which microservice. But we won't actually have a load balancer.

Tomcat HTTP Session

We'll need to comment out a few bits in the configuration. And just to be sure, we will stop the Redis server. To stop the server execute sudo service redis-server stop then you should see this message "Stopping redis-server: redis-server.". Comment out spring.session.store-type=redis in your application.properties and comment out the two Redis dependencies in you POM, like so:


<!--
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.session</groupId>
	<artifactId>spring-session-data-redis</artifactId>
</dependency>
-->

Open two command prompts/terminals and run MS 80 on one and MS 81 on the other. So that's mvn spring-boot:run -Dspring-boot.run.arguments=--server.port=8080 for MS 80 and mvn spring-boot:run -Dspring-boot.run.arguments=--server.port=8081 for MS 81. You should see something like 2023-02-23 16:27:13.547 INFO 25148 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8081 (http). Of course 8080 will show for MS 80. Now, let's POST some user data. Let's send a POST via Postman to MS 80 like so:

Spring Session with Redis
HTTP POST to MS 80

Now, let's check if our user data has persisted on both microservices. Let's do a GET on MS 80 and 81, like so:

Spring Session with Redis
HTTP GET from MS 80
Spring Session with Redis
HTTP GET from MS 81

As you can see, only MS 80 returned the user data. Now, do you see the problem if we had to maintain session state between multiple instances? Only one MS maintained the state. Imagine if we were going to shutdown or restart MS 80 to update it or something, the session data will be lost. What if the session data was banking transaction? Oh dear!

Spring Session with Redis

Now the fun part. Start the Redis server, sudo service redis-server start. Then undo the comments you made on your POM and application.properties. This should make your app Redis ready. With all that done, restart MS 80 and MS 81. So that's Ctrl+C then execute the maven command again as mentioned above. Let's do the POST again on MS 80 and do a GET on MS 80 and 81. Now the GET response will show the user data like so:

Spring Session with Redis
HTTP POST to MS 80 with Redis
Spring Session with Redis
HTTP GET from MS 80 with Redis
Spring Session with Redis
HTTP GET from MS 81 with Redis

There you have it. MS 80 and MS 81 are sending back the user data information. The magic that caused it all is spring.session.store-type=redis in your application.properties. Thanks to Spring Boot, a lot of steps have been done for us with just a single line in the application properties file. What did Spring Boot do for us? It applied a configuration that is equivalent to adding an @EnableRedisHttpSession to a class. This created a Spring bean with the name of springSessionRepositoryFilter that implements Filter. The filter is in charge of replacing the HttpSession implementation to be backed by Spring Session. Then Spring Boot automatically created a RedisConnectionFactory that connects Spring Session to a Redis Server on localhost on port 6379 (default port).

In a production environment, I would recommend to add more configuration and/or create a configuration class annotated with @EnableRedisHttpSession for more fine grained control over a session. Here are some of the configurations you can add:

  1. server.servlet.session.timeout= # Session timeout. If a duration suffix is not specified, seconds is used.
  2. spring.session.redis.flush-mode=on_save # Sessions flush mode.
  3. spring.session.redis.namespace=spring:session # Namespace for keys used to store sessions.
  4. spring.redis.host=localhost # Redis server host.
  5. spring.redis.password= # Login password of the redis server.
  6. spring.redis.port=6379 # Redis server port.

There you have it. Hope you had much fun playing with Spring Session with Redis, I know I did.