Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save msievers/230dae52a6cfc739a75a01562082c134 to your computer and use it in GitHub Desktop.
Save msievers/230dae52a6cfc739a75a01562082c134 to your computer and use it in GitHub Desktop.
[Spring Boot, Kubernetes] Make a Spring Boot app expose apis on different ports utilizing HandlerInterceptor

Motivation

Assume you have a Spring Boot application which exposes multiple apis, for example one for dogs and one for cats. Let's further assume that the api base paths look something like this

  • /api/cats/v1/
  • /api/dogs/v1/

Especially within kubernetes, it would be slick to be able to expose these apis using dedicated services. Let's recap a simple kubernetes service definition.

kind: Service
apiVersion: v1
metadata:
  name: cat-service
spec:
  selector:
    app: my-spring-boot-app
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080

Now, it would be possible to access the cats api by a path like this cat-service/api/cats/v1, but the dogs api would be published via cat-service also as cat-service/api/dogs/v1.

It would be nice to make different apis listen/answer only on dedicated ports, for example

  • 8081 => cats api
  • 8082 => dogs api

Then it would be possible to have two services like this

kind: Service
apiVersion: v1
metadata:
  name: cat-service
spec:
  selector:
    app: my-spring-boot-app
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8081
kind: Service
apiVersion: v1
metadata:
  name: dog-service
spec:
  selector:
    app: my-spring-boot-app
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8082

Idea

First you need to instruct your servlet container to listen to multiple ports. Because now all apis would be exposed on every port you need a filter/interceptor in the second place, to only allow certain requests to hit their associated controllers if the port matches.

Solution

1. Make embedded tomcat listen to multiple ports

This is based on the official Spring Boot docs.

import org.apache.catalina.connector.Connector;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.server.ServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Arrays;

@Configuration
public class EmbeddedTomcatConfiguration {
    
    @Bean
    public ServletWebServerFactory servletContainer() {

        TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
        
        Arrays.asList(8081, 8082).forEach(port -> {
            Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
            connector.setPort(port);
            tomcat.addAdditionalTomcatConnectors(connector);
        });
        
        return tomcat;
    }
}

2. Decline all requests on a non matching port

The following approach avoids pattern/regex matching an paths but utilizes a neat feature of Spring MVCs HandlerInterceptorAdapter#preHandle method.

! Attention, magic move here !

Let's have a look at the signature of preHandle.

preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)

Like for traditional filters, you have HttpServletRequest and HttpServletResponse, but you also get the handler, which gives you access to the controller (class) which is assigned to handle the request.

You can now accept/decline requests for instance by asking for the package of the assigned controller. Let's assume our project has the following structure

com.github.msievers.app
- apis
  - cats/v1
    - CatsController.java
  - dogs/v1
    - DogsController.java

A simple HandlerInterceptor could look like this

import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

class HandlerInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {

        String handlerPackageName = ((HandlerMethod) handler)
            .getBean().getClass().getPackage().getName();

        if(request.getLocalPort() == 8081 &&
            handlerPackageName.equals("com.github.msievers.app.apis.cats.v1")) {
            return true;
        }

        if(request.getLocalPort() == 8082 &&
            handlerPackageName.equals("com.github.msievers.app.apis.dogs.v1")) {
            return true;
        }

        response.setStatus(404);
        return false;
    }
}

You can easily replace equals with startsWith to allow nested controllers etc. The intent of this snippet is just to demonstrate the basic idea.

To make this HandlerInterceptor actually being used, you need to add it to the so called InterceptorRegistry. The following demonstrates this within a complete example.

import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Configuration
public class CustomWebMvcConfigurer implements WebMvcConfigurer {
    
    class HandlerInterceptor extends HandlerInterceptorAdapter {

        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {

            String handlerPackageName = ((HandlerMethod) handler)
                .getBean().getClass().getPackage().getName();
            
            if(request.getLocalPort() == 8081 &&
                handlerPackageName.equals("com.github.msievers.app.apis.cats.v1")) {
                return true;
            }

            if(request.getLocalPort() == 8082 &&
                handlerPackageName.equals("com.github.msievers.app.apis.dogs.v1")) {
                return true;
            }

            response.setStatus(404);
            return false;
        }
    }

    @Override
    public void addInterceptors(final InterceptorRegistry registry) {
        registry.addInterceptor(new HandlerInterceptor());
    }
}

Resources

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment