4

I am currently writing an application in Spring Boot 2.4.0 that is required to listen on multiple ports (3, to be specific - but might be 4 in the future). The idea is that each port makes a different API available for other services/apps to connect to it.

So, for a minimal working example, I'd say we have a SpringBootApp like this:

@SpringBootApplication
public class MultiportSpringBoot {

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

Now, I'd want to have this listening on 3 different ports, say 8080, 8081, and 8082. For all (!) requests to one of these ports, a specific controller should be "in charge". One of the reasons for this requirement is that one controller needs to handle a (regular) web frontend and another an API. In case an invalid request is received, the API-controller needs to give a different error message than the frontend should. Hence, the requirement given is a clear separation.

So I imagine multiple controllers for the different ports, such as:

@Controller
public class Controller8080
{
    @RequestMapping(value = "/", method = RequestMethod.GET)
    public ModelAndView test8080()
    {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("test8080");
        return modelAndView;
    }
}

with similar controllers for the other ports:

@Controller
public class Controller8081
{
    @RequestMapping(value = "/", method = RequestMethod.GET)
    public ResponseEntity test8081()
    {
        JSONObject stuff = doSomeStuffForPort8081();

        return new ResponseEntity<String>(stuff, HttpStatus.OK);
    }
}

I hoped for an annotation similar to @RequestMapping to be able to match and fix the port numbers for the controllers, but this seems to be no option as no such annotation seems to exist.

Now, this topic seems to be a bit specific, which is probably why you don't find all too much info on the web. I found Starting Spring boot REST controller in two ports, but I can also only have ONE instance running. I looked at https://tech.asimio.net/2016/12/15/Configuring-Tomcat-to-Listen-on-Multiple-ports-using-Spring-Boot.html, but this is outdated for Spring Boot 2.4.0 and a bit bloated with JavaMelody examples.

Anyone can provide a minimum working example for a solution for this?

--

EDIT:

To clarify a bit more: I need multiple, separate RESTControllers that each handle requests on different ports. I.e. a request to domain.com:8080/ should be handled by a different controller than a request to domain.com:8081/.

As an example, consider the two following controllers that should handle requests on ports 8080 and 8081 respectively:

//controller for port 8080
@RestController
public class ControllerA
{
    @GetMapping("/")
    String helloA(HttpServletRequest request)
    {
        return "ControllerA at port " + request.getLocalPort();
    }
}

and

//controller for port 8081
@RestController
public class ControllerB
{
    @GetMapping("/")
    String helloB(HttpServletRequest request)
    {
        return "ControllerB at port " + request.getLocalPort();
    }
}
Xenonite
  • 1,823
  • 4
  • 26
  • 39
  • As pointed out, there are multiple reasons for pursuing a design like this. The convenience of a clear separation of APIs is only one. Anyhow, I am not at liberty to overthrow the entire architecture. – Xenonite Dec 02 '20 at 13:54
  • How about you use a reverse proxy like nginx , to forward the calls at different ports to your app? One nginx instance to listen to ports and to forward to your apis and one instance of your app. – Shawrup Dec 05 '20 at 16:30
  • Thanks for the idea, but I am limited to SpringBoot 2.4.0 with embedded Tomcat, no other external services – Xenonite Dec 06 '20 at 14:59

3 Answers3

4

The tomcat class names changed a little bit so the link you provide has the old code but it is enough for the new code. Code below shows how you can open multiple ports in spring boot 2.4

@Bean
    public ServletWebServerFactory servletContainer() {
        TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
        tomcat.addAdditionalTomcatConnectors(additionalConnector());
        return tomcat;
    }

private Connector[] additionalConnector() {
    if (!StringUtils.hasLength(this.additionalPorts)) {
        return null;
    }
    String[] ports = this.additionalPorts.split(",");
    List<Connector> result = new ArrayList<>();
    for (String port : ports) {
        Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
        connector.setScheme("http");
        connector.setPort(Integer.valueOf(port));
        result.add(connector);
    }
    return result.toArray(new Connector[]{});
} 

And for responding to different ports with different controller you can implement the logic like check getLocalPort and respond it accordingly.

@GetMapping("/hello")
String hello(HttpServletRequest request) {
    return "hello from " + request.getLocalPort();
}

Or you can write a logical controller in filter. example code below

 @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain fc) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;
        if (req.getLocalPort() == 8882 && req.getRequestURI().startsWith("/somefunction")) {
            res.sendError(HttpServletResponse.SC_FORBIDDEN);
        } else {
            fc.doFilter(request, response);
        }
    }

You can find all running example here https://github.com/ozkanpakdil/spring-examples/tree/master/multiport

This is how it looks in my local enter image description here

In order to have same path with different controllers you can use @RequestMapping("/controllerNO") on top of the classes(check), NO should be number 1 , 2, otherwise spring will complain "you have same path" and will give you this exception

Caused by: java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'testController2' method 
com.mascix.multiport.TestController2#hello(HttpServletRequest)
to {GET [/hello]}: There is already 'testController1' bean method

Because from design spring will allow only one path to correspond to one controller, after requestmapping you can change the filter as this. Good thing about reflection you will learn very different exceptions. java.lang.NoSuchMethodException or java.lang.IllegalArgumentException

Latest code how it works in my local enter image description here

I must say this approach is not right and against the design of spring, in order to have different ports with different controllers, have multiple JVMs. If you mix the logic it will be harder for you to solve future problems and implement new features.

If you have to do it in one jvm, write a service layer and call the functions separately from one controller and write a logic like below

@GetMapping("/hello")
    String hello(HttpServletRequest request) {
        if (request.getLocalPort() == 8888) {
            return service.hellofrom8888();
        }
        if (request.getLocalPort() == 8889) {
            return service.hellofrom8889();
        }

        return "no repsonse ";
    }

At least this will be easy to maintain and debug. Still looks "ugly" though :)

ozkanpakdil
  • 3,199
  • 31
  • 48
  • Thank you for your detailed answer. Anyhow, I dont see how this is mapping requests to different controllers based on the port the request was received on What I need are separate controllers which are used for request on different ports. I.e. a request for "/x" incoming on port 8080 needs to be handled by a different (!) controller than the same request for "/x" being received on port 8081. I played around with the code provided by you, but I can't see how this is implementing the desired solution as all requests via all ports are handled by the same controller. Am I missing something? – Xenonite Dec 08 '20 at 11:57
  • Hi, again thank you for the detailed answer. Throws exceptions for me but I will look into this further. – Xenonite Dec 11 '20 at 10:48
  • Thanks for the suggested code; I notice that this.additionalPorts is not defined anywhere… is it an array, a string from properties, or what? May I ask if you drop a bean with class names like `Connector`, it'd help to summarize the imports you are referring to. – dlamblin Apr 28 '21 at 22:15
  • 1
    https://github.com/ozkanpakdil/spring-examples/blob/master/multiport/src/main/resources/application.properties looks like have it. I highly suggest the read the code under https://github.com/ozkanpakdil/spring-examples/tree/master/multiport – ozkanpakdil Apr 29 '21 at 10:26
3

Özkan has already provided detailed information on how to get Tomcat to listen to multiple ports by supplying your own ServletWebServerFactory @Bean based on TomcatServletWebServerFactory.

As for the mapping, how about this approach:

Add a @RequestMapping("/8080") to your controller (methods keep their specific @RequestMapping)

@Controller
@RequestMapping("/8080")
public class Controller8080
{
    @RequestMapping(value = "/", method = RequestMethod.GET)
    public ModelAndView test8080()
    {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("test8080");
        return modelAndView;
    }
}

Define your own RequestMappingHandlerMapping as

public class PortBasedRequestMappingHandlerMapping extends RequestMappingHandlerMapping {

    @Override
    protected HandlerMethod lookupHandlerMethod(final String lookupPath, final HttpServletRequest request) throws Exception {
        return super.lookupHandlerMethod(request.getLocalPort() + lookupPath, request);
    }
}

and use it by

@Bean
public WebMvcRegistrations webMvcRegistrationsHandlerMapping() {
    return new WebMvcRegistrations() {

        @Override
        public RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
            return new PortBasedRequestMappingHandlerMapping();
        }
    };
}

This will attempt to map a request to /foobar on port 8080 to /8080/foobar.

Simon
  • 2,994
  • 3
  • 28
  • 37
  • This looks like a better approach then filter. can you provide a POC ? you can just change my code here https://github.com/ozkanpakdil/spring-examples/tree/master/multiport and a PR :) – ozkanpakdil Dec 10 '20 at 19:18
  • this looks interesting! will try this out over the week end. thank you for the idea! – Xenonite Dec 11 '20 at 10:49
1

Another approach is by using org.springframework.web.servlet.mvc.condition.RequestCondition which I think is cleaner https://stackoverflow.com/a/69397870/6166627

Sam
  • 1,832
  • 19
  • 35