java – Spring Boot | How to dynamically add new tomcat connector?

java – Spring Boot | How to dynamically add new tomcat connector?

Hi here is my sample project: sample project

1- Main Application (

public class DemoApplication {
    public static void main(String[] args) {, args);

2 – Config file (

public class AppConfig {

private ServletWebServerApplicationContext server;

private static FilterConfig filterConfig = new FilterConfig();

void init() {
    //setting default port config
    filterConfig.addNewPortConfig(8080, /admin);

@Scope(value = WebApplicationContext.SCOPE_APPLICATION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public FilterConfig createFilterConfig() {
    return filterConfig;

public void addPort(String schema, String domain, int port, boolean secure) {
    TomcatWebServer ts = (TomcatWebServer) server.getWebServer();
    synchronized (this) {
        ts.getTomcat().setConnector(createConnector(schema, domain, port, secure));

public void addContextAllowed(FilterConfig filterConfig, int port, String context) {
    filterConfig.addNewPortConfig(port, context);

 public void removePort(int port) {
    TomcatWebServer ts = (TomcatWebServer) server.getWebServer();
    Service service = ts.getTomcat().getService();
    synchronized (this) {
        Connector[] findConnectors = service.findConnectors();
        for (Connector connector : findConnectors) {
            if (connector.getPort() == port) {
                try {
                } catch (LifecycleException e) {

private Connector createConnector(String schema, String domain, int port, boolean secure) {
    Connector conn = new Connector(org.apache.coyote.http11.Http11NioProtocol);
    if (secure) {
        // config secure port...
    return conn;

3 – Filter (

public class NewPortFilter {
@Bean(name = restrictFilter)
public FilterRegistrationBean<Filter> retstrictFilter(FilterConfig filterConfig) {
    Filter filter = new OncePerRequestFilter() {

        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                FilterChain filterChain) throws ServletException, IOException {

            // get allowed url contexts
            Set<String> config = filterConfig.getConfig().get(request.getLocalPort());
            if (config == null || config.isEmpty()) {
            boolean accepted = false;
            for (String value : config) {
                if (request.getPathInfo().startsWith(value)) {
                    accepted = true;
            if (accepted) {
                filterChain.doFilter(request, response);
            } else {
    FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<Filter>();
    return filterRegistrationBean;

4 – Filter Config (

public class FilterConfig {

    private Map<Integer, Set<String>> acceptedContextsByPort = new ConcurrentHashMap<>();

    public void addNewPortConfig(int port, String allowedContextUrl) {
        if(port > 0 && allowedContextUrl != null) {
            Set<String> set = acceptedContextsByPort.get(port);
            if (set == null) {
                set = new HashSet<>();
            set = new HashSet<>(set);
            acceptedContextsByPort.put(port, set);

    public void removePortConfig(int port) {
        if(port > 0) {

    public Map<Integer, Set<String>> getConfig(){
        return acceptedContextsByPort;

5 – Controller (

public class TestController {
AppConfig config;

FilterConfig filterConfig;

String test() {
    return hello test;

String test2() {
    return hello test 2;

ResponseEntity<String> createNewPort(@RequestParam Integer port, @RequestParam String context) {
    if (port == null || port < 1) {
        return new ResponseEntity<>(Invalid Port + port, HttpStatus.BAD_REQUEST);
    config.addPort(http, localhost, port, false);
    if (context != null && context.length() > 0) {
        config.addContextAllowed(filterConfig, port, context);

    return new ResponseEntity<>(Added port: + port, HttpStatus.OK);

ResponseEntity<String> removePort(@RequestParam Integer port) {
    if (port == null || port < 1) {
        return new ResponseEntity<>(Invalid Port + port, HttpStatus.BAD_REQUEST);

    return new ResponseEntity<>(Removed port: + port, HttpStatus.OK);

How to test it?

In a browser:

1 – try:


Expected Response: hello test

2 – try:


Expected Response: Added port:9090

3 – try:


Expected Response: hello test 2

4 – try expected errors:


Expected Response (context [alternative] allowed but endpoint not registered in controller for this context): Whitelabel Error Page…


Expected Response (context [any] not allowed): Whitelabel Error Page…


Expected Response (invalid port number): ERR_CONNECTION_REFUSED


Expected Response (no context allowed [/hello]): Whitelabel Error Page…

5 – try remove a port:


6 – check removed port:


Expected Response (port closed): ERR_CONNECTION_REFUSED

I hope it helps.

You should create ServletWebServerFactory bean as a prototype bean using @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE).

Now in bean where you need new tomcat connector to be injected into Spring context(MySingletonBean in example) autowire the application context and get ServletWebServerFactory bean(MyPrototypeBean in example) from getBean method. In this way you will always get new tomcat connector bean.

Following is a simple sample code:-

public class MySingletonBean {

    private ApplicationContext applicationContext;

    public void showMessage(){
        MyPrototypeBean bean = applicationContext.getBean(MyPrototypeBean.class);

java – Spring Boot | How to dynamically add new tomcat connector?

Based on the accepted answer of @ariel-carrera, I did it a little differently (IMO cleaner):

  1. Define a normal Spring controller for handling the request on any dynamic port
import package.IgnoredBean;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import static org.apache.commons.lang3.reflect.MethodUtils.getMethodsWithAnnotation;

//must be declared in a separate java file so that its not picked up by component scanning as inner class
class DynamicController {

    static final Method HANDLER_METHOD = getMethodsWithAnnotation(DynamicController.class, RequestMapping.class)[0];
    private final String myContext;
    public Object handle(
            @RequestBody Map<String, Object> body,
            @RequestParam MultiValueMap<String, Object> requestParams,
            @PathVariable Map<String, Object> pathVariables
    ) {
        Map<String, Object> allAttributes = new HashMap<>(body.size() + requestParams.size() + pathVariables.size());
        requestParams.forEach((name, values) -> allAttributes.put(name, values.size() > 1 ? values : values.get(0))); request for {}: {}, myContext, allAttributes);
        return allAttributes;

    // this handler only affects this particular controller. Otherwise it will use any of your regular @ControllerAdvice beans or fall back to springs default
    public ResponseEntity<?> onError(Exception e) {
        log.debug(something happened in {}, myContext, e);
        return ResponseEntity.status(500).body(Map.of(message, e.getMessage()));
  • Must be declared in its own file
  • It is not a Spring Bean, we will instantiate it manually for each port and give it some context objects that are relevant to the port owner.
  • Note the @IgnoredBean, a custom annotation:
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

public @interface IgnoredBean {
@ComponentScan(excludeFilters = @ComponentScan.Filter(IgnoredBean.class))
public class MyApplication{...}
  1. The rest of it
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.startup.Tomcat;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.coyote.http11.Http11NioProtocol;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.embedded.tomcat.TomcatWebServer;
import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.mvc.condition.PathPatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import static;

class DynamicControllerService {
    private final RequestMappingHandlerMapping requestHandlerMapper;
    private final Map<Integer, RequestMappingInfo> mappingByPort = new ConcurrentHashMap<>();
    private Tomcat tomcat;

    void setTomcat(ServletWebServerApplicationContext context) {
        tomcat = ((TomcatWebServer) context.getWebServer()).getTomcat();

    public int addMapping(@Nullable Integer givenPort, RequestMethod method, String path, Object myContext) {
        val connector = new Connector(new Http11NioProtocol());
        //0 means it will pick any available port
        try {
        } catch (IllegalArgumentException e) {
            // if it fails to start the connector, the object will still be left inside here
            val rootCause = ExceptionUtils.getRootCause(e);
            throw new IllegalArgumentException(rootCause.getMessage(), rootCause);
        int port = connector.getLocalPort();
        val mapping = RequestMappingInfo
                .customCondition(new PortRequestCondition(port))
                new DynamicController(my context for port  + port),
        mappingByPort.put(port, mapping); mapping {} {} for port {}, method, path, port);
        return port;

    public void removeMapping(Integer port) {
                .filter(connector -> connector.getPort() == port)
                .ifPresent(connector -> {
                    try {
                    } catch (IllegalArgumentException | LifecycleException e) {
                        val rootCause = ExceptionUtils.getRootCause(e);
                        throw new IllegalArgumentException(rootCause.getMessage(), rootCause);
                    val mapping = mappingByPort.get(port);
           mapping {} {} for port {},

    private static class PortRequestCondition implements RequestCondition<PortRequestCondition> {

        private final Set<Integer> ports;

        public PortRequestCondition(Integer... ports) {
            this.ports = Set.of(ports);

        public PortRequestCondition combine(PortRequestCondition other) {
            return new PortRequestCondition(Stream.concat(,;

        public PortRequestCondition getMatchingCondition(HttpServletRequest request) {
            return ports.contains(request.getLocalPort()) ? this : null;

        public int compareTo(PortRequestCondition other, HttpServletRequest request) {
            return 0;

Leave a Reply

Your email address will not be published. Required fields are marked *