Linux: Spring Boot as a service on port 80

Well, that’s a mouth full. This blog shows how to run your Spring Boot application on port 80 when your linux server starts. We are going to use Ubuntu or Linux Mint for this, and we’re going to assume that Java is installed.

Setup Spring Boot

The first thing we need to do is tell Maven to make our Spring Boot application runnable. We’ll configure the spring-boot-maven-plugin to make the jar that’s going to be built executable.

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<executable>
						true
					</executable>
				</configuration>
			</plugin>
		</plugins>
	</build>

By default Spring Boot runs on port 8080. There’s no need to change that here, we’ll deal with that later. But if you want, you could start the application on a different port by configuring it in application.properties

server.port=8079

Run Application as a Service

Set permissions

The first thing we need to do is to copy the application to its final destination, and create a user to run the app. By creating a separate user for this application, we limit the damage that can be done in case there is a security breach. I’m going to call the application “website” and I’m going to place it under /var/website, to make things easy. Once the application is in its place, change directory to /var/website, create a new user and transfer ownership to the new user. We’re going to allow the newly created user to read and execute the file, but not to write to the file. This, again, limits the damage that can be done when there’s a security breach.

sudo useradd website
sudo passwd website
sudo chown website:website website.jar
sudo chmod 500 website.jar

Setup systemd service

To manage starting and stopping our application, we’re going to turn it into a service maintained by systemd. We need to tell systemd how to run our service by creating a configuration file.

create /etc/systemd/system/website.service

[Unit]
Description=My spring-boot application

Wants=network.target
After=syslog.target

[Service]
User=website
Type=simple
ExecStart=/var/website/website.jar
Restart=on-failure
RestartSec=60
KillMode=mixed

[Install]
WantedBy=multi-user.target

This is what the options mean:
Description: A text description of what the service is for.
Wants: The services that should, but are not required to, be started before this service.
After: The services that should be started (if they are not already running) after this service has been started.
User: The user that should be used to start this service.
Type: This indicates the way the service is started. We’ll set it to simple.
ExecStart: The command with parameters that needs to be executed to start this service.
Restart: When the service should be restarted.
RestartSec: If the service needs to be restarted, how many seconds systemd should wait.
KillMode: How the process will be stopped by systemd. We’ll set it to mixed, which sends a SIGTERM signal to our service which allows the service to shut down itself.
WantedBy: We want our service to be started when the system is up and running, and accepting connections from users.

For a list of options, look here.
KillMode is described here.
For more info on WantedBy, see this.

For the security reasons mentioned above, we’ll change the permissions of this file:

sudo chmod 640 /etc/systemd/system/website.service

Enable service to start on boot

The next thing we want to do is to indicate that this service should be started when the server is starting.

sudo systemctl enable website

Setup nginx

Now we have our application installed as a service, and it’s started when the server starts up. But the application is running on port 8079, which is not what we want. Linux only allows the root user to open TCP ports below 1000. To fix this, we’re going to use nginx to forward port 80 to our application.

create /etc/nginx/sites-enabled/website.conf with the following content:

server {
        listen  80;
        server_name     _;
        location /  {
            proxy_pass          http://localhost:8079/;
            proxy_redirect      off;

            proxy_set_header   Host             $host;
            proxy_set_header   X-Real-IP        $remote_addr;
            proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
        }
}

This tells nginx to listen to port 80, for incoming connections on any hostname, and forward it to our application.

Multiple Full-Screens in Java

Working with Java’s Full-Screen Exclusive Mode is a bit different than working with AWT, Swing or JavaFX. The tutorial describes how, and why, this works. The information, however, is a bit spread out. Also, it doesn’t mention how to work with multiple full screens at the same time. Not that it’s very different from working with only one screen, but it’s at least fun to try out.

The Frame

The first part of this exercise is to create a Frame that should be displayed. Since the OS is managing the video memory, we should guard against it. We could lose our drawing at any time, because the OS can simply reclaim the memory. The draw method looks a bit complex because of this, with its double loop structure. But on the positive side, we can just ignore any hint by the OS that the frame should be repainted.

package nl.ghyze.fullscreen;

import java.awt.Color;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.image.BufferStrategy;

public class TestFrame extends Frame {
    private final String id;

    public TestFrame(String id) {
        this.id = id;

        // ignore OS initiated paint events
        this.setIgnoreRepaint(true);
    }

    public void draw() {
        BufferStrategy strategy = this.getBufferStrategy();
        do {
            // The following loop ensures that the contents of the drawing buffer
            // are consistent in case the underlying surface was recreated
            do {
                // Get a new graphics context every time through the loop
                // to make sure the strategy is validated
                Graphics graphics = strategy.getDrawGraphics();

                int w = this.getWidth();
                int h = this.getHeight();

                // clear screen
                graphics.setColor(Color.black);
                graphics.fillRect(0,0,w,h);

                // draw screen
                graphics.setColor(Color.ORANGE);
                graphics.drawString("Screen: " + this.id, w / 2, h / 2);

                // Dispose the graphics
                graphics.dispose();

                // Repeat the rendering if the drawing buffer contents
                // were restored
            } while (strategy.contentsRestored());

            // Display the buffer
            strategy.show();

            // Repeat the rendering if the drawing buffer was lost
        } while (strategy.contentsLost());
    }
}

The ScreenFactory

This is just a simple utility class to figure out which screens are available, and what quality images we can show on these screens.

package nl.ghyze.fullscreen;

import java.awt.DisplayMode;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;

public class ScreenFactory {

    private final GraphicsDevice[] graphicsDevices;

    /**
     * Constructor. Finds all available GraphicsDevices.
     */
    public ScreenFactory(){
        GraphicsEnvironment localGraphicsEnvironment = GraphicsEnvironment.getLocalGraphicsEnvironment();
        graphicsDevices = localGraphicsEnvironment.getScreenDevices();
    }

    /**
     * Get a list of the IDs of all available GraphicsDevices
     * @return the list of the IDs of all available GraphicsDevices
     */
    public List<String> getGraphicsDeviceIds(){
        return Arrays.stream(graphicsDevices).map(GraphicsDevice::getIDstring).collect(Collectors.toList());
    }

    /**
     * Get a single GraphicsDevice, by ID. Return an empty optional if none is found.
     * @param graphicsDeviceId the ID of the requested GraphicsDevice
     * @return an optional which contains the GraphicsDevice, if found.
     */
    public Optional<GraphicsDevice> getGraphicsDevice(String graphicsDeviceId){
        return Arrays.stream(graphicsDevices)
                .filter(graphicsDevice -> graphicsDevice.getIDstring().equals(graphicsDeviceId))
                .findAny();
    }

    /**
     * Get all available DisplayModes for the selected GraphicsDevice
     * @param graphicsDeviceId the ID of the GraphicsDevice
     * @return a list of DisplayModes
     */
    public List<DisplayMode> getDisplayModes(String graphicsDeviceId){
        GraphicsDevice gd = Arrays.stream(graphicsDevices)
                .filter(graphicsDevice -> graphicsDevice.getIDstring().equals(graphicsDeviceId))
                .findFirst()
                .orElseThrow(IllegalArgumentException::new);

        return Arrays.stream(gd.getDisplayModes()).collect(Collectors.toList());
    }

    /**
     * Get the best DisplayMode for the selected GraphicsDevice.
     * Best is defined here as the most pixels, highest bit-depth and highest refresh-rate.
     * @param graphicsDeviceId the ID of the GraphicsDevice
     * @return the best DisplayMode for this GraphicsDevice
     */
    public DisplayMode getBestDisplayMode(String graphicsDeviceId){
        List<DisplayMode> displayModes = getDisplayModes(graphicsDeviceId);
        DisplayMode best = null;
        for (DisplayMode displayMode : displayModes){
            if (best == null){
                best = displayMode;
            } else {
                if (isScreensizeBetterOrEqual(best, displayMode) ){
                    best = displayMode;
                } else if (isScreensizeBetterOrEqual(best, displayMode)
                        && isBitDepthBetterOrEqual(best, displayMode)){
                    best = displayMode;
                } else if (isScreensizeBetterOrEqual(best, displayMode)
                        && isBitDepthBetterOrEqual(best, displayMode)
                && isRefreshRateBetterOrEqual(best, displayMode)){
                    best = displayMode;
                }
            }
        }
        return best;
    }

    private boolean isScreensizeBetterOrEqual(DisplayMode current, DisplayMode potential){
        return potential.getHeight() * potential.getWidth() >= current.getHeight() * current.getWidth();
    }

    private boolean isBitDepthBetterOrEqual(DisplayMode current, DisplayMode potential){
        if (current.getBitDepth() == DisplayMode.BIT_DEPTH_MULTI) {
            return false;
        } else if (potential.getBitDepth()  == DisplayMode.BIT_DEPTH_MULTI){
            return true;
        }
        return potential.getBitDepth() >= current.getBitDepth();
    }

    private boolean isRefreshRateBetterOrEqual(DisplayMode current, DisplayMode potential){
        if (current.getRefreshRate() == DisplayMode.REFRESH_RATE_UNKNOWN) {
            return false;
        } else if (potential.getRefreshRate()  == DisplayMode.REFRESH_RATE_UNKNOWN){
            return true;
        }
        return potential.getRefreshRate() >= current.getRefreshRate();
    }
}

Bringing it together

For every screen that we have, we’re going to make a Frame. If the screen supports a Full Screen mode, we’re going to use it. Otherwise, we’re just going to use a maximized Frame. Once we’ve setup the screens, we’re going to loop over the frames, and draw each of them. We’ll do this in an infinite loop until we reach some stop condition. In this case, we’re going to stop after two seconds, but you can implement it in any way you’d like. I’ve found that if you don’t implement a stop condition, and just use an infinite loop, it can be quite challenging to actually stop the program. When the program should shut down, we’re going to reset the screens to normal and dispose of the Frames. Once the Frames are disposed, the program stops.

package nl.ghyze.fullscreen;

import java.awt.DisplayMode;
import java.awt.GraphicsDevice;
import java.util.ArrayList;
import java.util.List;

public class MultiFullScreen {
    private final List<TestFrame> frames = new ArrayList<>();

    private final long startTime = System.currentTimeMillis();
    private final ScreenFactory screenFactory = new ScreenFactory();

    public MultiFullScreen(){
        try {
            for (String graphicsDeviceId : screenFactory.getGraphicsDeviceIds()) {
                GraphicsDevice graphicsDevice = screenFactory.getGraphicsDevice(graphicsDeviceId).orElseThrow(IllegalStateException::new);
                DisplayMode best = screenFactory.getBestDisplayMode(graphicsDeviceId);

                TestFrame tf = new TestFrame(graphicsDeviceId);
                // remove borders, if supported. Not needed, but looks better.
                tf.setUndecorated(graphicsDevice.isFullScreenSupported());

                // first set fullscreen window, then set display mode
                graphicsDevice.setFullScreenWindow(tf);
                graphicsDevice.setDisplayMode(best);

                // can only be called after it has been set as a FullScreenWindow
                tf.createBufferStrategy(2);

                frames.add(tf);
            }
            run();
            shutDown();
        }catch (Exception e){
            e.printStackTrace();
        } finally {
            shutDown();
        }
    }

    private void shutDown() {
        // unset full screen windows
        for (String graphicsDeviceId : screenFactory.getGraphicsDeviceIds()) {
            try {
                GraphicsDevice graphicsDevice = screenFactory.getGraphicsDevice(graphicsDeviceId).orElseThrow(IllegalStateException::new);
                graphicsDevice.setFullScreenWindow(null);
            } catch (Exception e){
                e.printStackTrace();
            }
        }

        // Dispose frames, so the application can exit.
        for (TestFrame frame : frames){
            frame.dispose();
        }
    }

    public void run(){
        while(shouldRun()){
            for(TestFrame tf : frames){
                tf.draw();
            }
        }
    }

    private boolean shouldRun(){
        final long runningTime = System.currentTimeMillis() - startTime;
        return runningTime < 2000L;
    }

    public static void main(String[] args) {
        new MultiFullScreen();
    }
}

Jackson, JAXB, OpenAPI and Spring

Imagine that you have a service that enriches some data. The model for the data that needs to be enriched is generated from an XSD. The model for the enriching data (the wrapper) is generated from OpenAPI specs. Those two separate models need to be combined and sent out via Spring’s RestTemplate. The problem is that the generated models interfere with each other, and the combination of models doesn’t serialize correctly.

To solve the problem of interference, we’re going to transform the XSD-generated model to a generic JSON model (using the org.json library). That way we eliminate the XML annotations, and we can just send the OpenAPI model.

XML model to JSONObject

Our wrapper model accepts any Object as payload. We can set our XmlModel there, but we can also set a JSONObject. However, we need to transform our XML model to JSONObject. We can use Jackson for this, when we use the right AnnotationInspector. We’re not going to transform the model directly to JSONObject, but we use a String representation as an intermediate step. Once we have converted our XmlModel to JSONObject, we can set that as our payload.

private JSONObject toJSONObject(XmlModel xmlModel) {
  try {
    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.setAnnotationIntrospector(
    new JaxbAnnotationIntrospector(TypeFactory.defaultInstance()));
    String content = objectMapper.writeValueAsString(xmlModel);
    return new JSONObject(content);
  } catch (JsonProcessingException e) {
    // Shouldn't happen.
    log.severe("Error translating to JSONObject");
    throw new IllegalStateException(
      "Error translating to JSONObject");
  }
}

Configure RestTemplate

Now we have a model that can serialize, but our RestTemplate can’t handle it yet. Specifically, The ObjectMapper that the RestTemplate uses can’t handle the JSONObject. But we can configure our custom ObjectMapper and tell the RestTemplate to use that one. To serialize the JSONObject, we need to add the JsonOrgModule. And while we’re at it, we’re going to add the JavaTimeModule so we can serialize dates and times. We can’t set the ObjectMapper directly, we need to set it on a MessageConverter. Then we need to explicitly set our MessageConverter as the first to be sure that it’s going to be used.

@Bean
public RestTemplate myRestTemplate() {
  // Create and configurae
  ObjectMapper objectMapper = new ObjectMapper();
  objectMapper.registerModule(new JsonOrgModule());
  objectMapper.registerModule(new JavaTimeModule());

  // Configure MessageConverter
  MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
  converter.setObjectMapper(objectMapper);

  // Configure RestTemplate
  RestTemplate restTemplate =
     // create the RestTemplate;
     restTemplate.getMessageConverters()
       .add(0, outputgeneratiebeheerMessageConverter);

  return restTemplate;
}

Maven dependencies

The artifact jackson-module-jaxb-annotations contains the JaxbAnnotationIntrospector that we used to serialize the XML model. The jackson-datatype-json-org contains the JsonOrgModule that we used to serialize the JsonObjects. These dependencies need to be added, in addition to jackson

<properties>
  <jackson.version>2.12.1</jackson.version>
</properties>

<dependencies>
  <dependency>
    <groupId>com.fasterxml.jackson.module</groupId>
    <artifactId>jackson-module-jaxb-annotations</artifactId>
    <version>${jackson.version}</version>
  </dependency>
  <dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-json-org</artifactId>
    <version>${jackson.version}</version>
  </dependency>
</dependencies>