Xenonite
Xenonite

Reputation: 1957

Running SpingBoot app on multiple ports with different controllers

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

Upvotes: 6

Views: 6181

Answers (3)

Sam
Sam

Reputation: 2049

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

Upvotes: 1

Simon
Simon

Reputation: 3284

Ö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.

Upvotes: 3

ozkanpakdil
ozkanpakdil

Reputation: 4612

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 :)

Upvotes: 6

Related Questions