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