【问题标题】:Spring Boot | How to dynamically add new tomcat connector?弹簧靴 |如何动态添加新的tomcat连接器?
【发布时间】:2019-12-20 19:21:26
【问题描述】:

我需要让我的 Spring Boot 应用程序动态启动/停止监听新端口。 我知道为此需要将一个新的 tomcat 连接器注入到 Spring 上下文中。

我可以使用ServletWebServerFactory bean 和tomcatConnectorCustomizer 添加连接器。但是这个 bean 仅在 Spring Bootup 期间加载。

@Bean
public ServletWebServerFactory servletContainer() {

    TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();
    TomcatConnectorCustomizer tomcatConnectorCustomizer = connector -> {
        connector.setPort(serverPort);

        connector.setScheme("https");
        connector.setSecure(true);

        Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();

        protocol.setSSLEnabled(true);
        protocol.setKeystoreType("PKCS12");
        protocol.setKeystoreFile(keystorePath);
        protocol.setKeystorePass(keystorePass);
        protocol.setKeyAlias("spa");
        protocol.setSSLVerifyClient(Boolean.toString(true));
        tomcat.addConnectorCustomizers(tomcatConnectorCustomizer);
        return tomcat;

    }
}

有什么方法可以在运行时添加 tomcat 连接器吗?在方法调用上说?

更新:连接器已创建,但对其的所有请求都返回 404:

我已经设法在运行时添加了一个 Tomcat 连接器。但是对该端口的请求不会发送到我的 RestController。

    TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();

    TomcatConnectorCustomizer tomcatConnectorCustomizer = connector -> {
        Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
        connector.setScheme("http");
        connector.setSecure(false);
        connector.setPort(8472);
        protocol.setSSLEnabled(false);
    };
    tomcat.addConnectorCustomizers(tomcatConnectorCustomizer);

    tomcat.getWebServer().start();

我应该如何继续?

【问题讨论】:

  • @SimonMartinelli 这是一个要求。不是我设置的
  • 请告诉我更多关于用例的信息。
  • @SimonMartinelli 我有一个场景,同一个 API 对多个实体的行为应该不同。并且不能使用基于用户的身份验证。因此,每个实体都将被分配一个 tomcat 连接器(端口),因此 API 将在请求来自的端口上做出相应的行为
  • 为什么不使用多个上下文根?
  • 定义“行为不同”?

标签: java spring spring-boot


【解决方案1】:

您好,这是我的示例项目:sample project

1- 主应用程序(DemoApplication.java):

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

2 - 配置文件(AppConfig.java):

@Configuration
public class AppConfig {

@Autowired
private ServletWebServerApplicationContext server;

private static FilterConfig filterConfig = new FilterConfig();

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

@Bean
@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 {
                    connector.stop();
                    connector.destroy();
                    filterConfig.removePortConfig(port);
                } catch (LifecycleException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

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

3 - 过滤器(NewPortFilter.java):

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

        @Override
        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()) {
                response.sendError(403);
            }
            boolean accepted = false;
            for (String value : config) {
                if (request.getPathInfo().startsWith(value)) {
                    accepted = true;
                    break;
                }
            }
            if (accepted) {
                filterChain.doFilter(request, response);
            } else {
                response.sendError(403);
            }
        }
    };
    FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<Filter>();
    filterRegistrationBean.setFilter(filter);
    filterRegistrationBean.setOrder(-100);
    filterRegistrationBean.setName("restrictFilter");
    return filterRegistrationBean;
}
}

4 - 过滤器配置(FilterConfig.java):

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);
            set.add(allowedContextUrl);
            acceptedContextsByPort.put(port, set);
        }
    }

    public void removePortConfig(int port) {
        if(port > 0) {
            acceptedContextsByPort.remove(port);
        }
    }

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

5 - 控制器(TestController.java):

@RestController
public class TestController {
@Autowired
AppConfig config;

@Autowired
FilterConfig filterConfig;

@GetMapping("/admin/hello")
String test() {
    return "hello test";
}

@GetMapping("/alternative/hello")
String test2() {
    return "hello test 2";
}

@GetMapping("/admin/addNewPort")
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);
}

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

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

如何测试?

在浏览器中:

1 - 尝试:

http://localhost:8080/admin/hello

预期响应:你好测试

2 - 尝试:

http://localhost:8080/admin/addNewPort?port=9090&context=alternative

预期响应:添加端口:9090

3 - 尝试:

http://localhost:9090/alternative/hello

预期响应:你好测试 2

4 - 尝试预期的错误:

http://localhost:9090/alternative/addNewPort?port=8181&context=alternative

预期响应(允许上下文 [替代],但端点未在控制器中为此上下文注册):Whitelabel 错误页面...

http://localhost:9090/any/hello

预期响应(不允许使用上下文 [任何]):Whitelabel 错误页面...

http://localhost:8888/any/hello

预期响应(无效端口号):ERR_CONNECTION_REFUSED

http://localhost:8080/hello

预期响应(不允许上下文 [/hello]):Whitelabel 错误页面...

5 - 尝试删除一个端口:

http://localhost:8080/admin/removePort?port=9090

6 - 检查删除的端口:

http://localhost:9090/alternative/hello

预期响应(端口关闭):ERR_CONNECTION_REFUSED

希望对你有帮助。

【讨论】:

  • 如何在运行时添加新的 tomcat 连接器?我所说的新 tomcat 连接器的意思是应用程序应该在运行时动态开始侦听新端口(比如在 API 调用上我想添加一个新的 tomcat 连接器)
  • 嗨,我更新了我的 github,检查一下。我现在会更新我的答案,给我几分钟,然后再看一遍......
  • 嗨,很好的答案!它按预期工作!但是有什么方法可以移除连接器吗?
  • 嗨@SapneshNaik,好的!回答完毕!从我的项目中提取更改并尝试新的端点 (removePort)。
  • 很棒的工作,帮了我很多!我想我改进了一点,看看我的回答
【解决方案2】:

您应该使用@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) 创建 ServletWebServerFactory bean 作为原型 bean。

现在在 bean 中,您需要将新的 tomcat 连接器注入 Spring 上下文(例如 MySingletonBean)自动装配应用程序上下文并从 getBean 方法获取 ServletWebServerFactory bean(例如 MyPrototypeBean)。这样,您将始终获得新的 tomcat 连接器 bean。

下面是一个简单的示例代码:-

public class MySingletonBean {

    @Autowired
    private ApplicationContext applicationContext;

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

【讨论】:

  • 请在问题中查看我的更新。你能帮忙吗?
【解决方案3】:

根据@ariel-carrera 的公认答案,我做了一点不同(IMO 清洁工):

  1. 定义一个普通的 Spring 控制器来处理任何动态端口上的请求
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 it's not picked up by component scanning as inner class
@IgnoredBean
@RestController
@Slf4j
@RequiredArgsConstructor
class DynamicController {

    static final Method HANDLER_METHOD = getMethodsWithAnnotation(DynamicController.class, RequestMapping.class)[0];
    
    private final String myContext;
    
    @RequestMapping
    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());
        allAttributes.putAll(body);
        allAttributes.putAll(pathVariables);
        requestParams.forEach((name, values) -> allAttributes.put(name, values.size() > 1 ? values : values.get(0)));
        log.info("Handling 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 spring's default
    @ExceptionHandler
    public ResponseEntity<?> onError(Exception e) {
        log.debug("something happened in '{}'", myContext, e);
        return ResponseEntity.status(500).body(Map.of("message", e.getMessage()));
    }
}
  • 必须在自己的文件中声明
  • 它不是 Spring Bean,我们将为每个端口手动实例化它,并为其提供一些与端口所有者相关的上下文对象。
  • 注意@IgnoredBean,一个自定义注解:
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

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

@Target(TYPE)
@Retention(RUNTIME)
public @interface IgnoredBean {
}
@SpringBootApplication
@ComponentScan(excludeFilters = @ComponentScan.Filter(IgnoredBean.class))
...
public class MyApplication{...}
  1. 其余部分
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 java.util.stream.Stream;

import static java.util.stream.Collectors.toUnmodifiableSet;

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

    @Autowired
    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());
        connector.setThrowOnFailure(true);
        //0 means it will pick any available port
        connector.setPort(Optional.ofNullable(givenPort).orElse(0));
        try {
            tomcat.setConnector(connector);
        } catch (IllegalArgumentException e) {
            // if it fails to start the connector, the object will still be left inside here
            tomcat.getService().removeConnector(connector);
            val rootCause = ExceptionUtils.getRootCause(e);
            throw new IllegalArgumentException(rootCause.getMessage(), rootCause);
        }
        int port = connector.getLocalPort();
        val mapping = RequestMappingInfo
                .paths(path)
                .methods(method)
                .customCondition(new PortRequestCondition(port))
                .build();
        requestHandlerMapper.registerMapping(
                mapping,
                new DynamicController("my context for port " + port),
                DynamicController.HANDLER_METHOD
        );
        mappingByPort.put(port, mapping);
        log.info("added mapping {} {} for port {}", method, path, port);
        return port;
    }

    public void removeMapping(Integer port) {
        Stream.of(tomcat.getService().findConnectors())
                .filter(connector -> connector.getPort() == port)
                .findFirst()
                .ifPresent(connector -> {
                    try {
                        tomcat.getService().removeConnector(connector);
                        connector.destroy();
                    } catch (IllegalArgumentException | LifecycleException e) {
                        val rootCause = ExceptionUtils.getRootCause(e);
                        throw new IllegalArgumentException(rootCause.getMessage(), rootCause);
                    }
                    val mapping = mappingByPort.get(port);
                    requestHandlerMapper.unregisterMapping(mapping);
                    log.info("removed mapping {} {} for port {}",
                            mapping.getMethodsCondition().getMethods(),
                            Optional.ofNullable(mapping.getPathPatternsCondition())
                                    .map(PathPatternsRequestCondition::getPatternValues)
                                    .orElse(Set.of()),
                            port
                    );
                });
    }

    @RequiredArgsConstructor
    private static class PortRequestCondition implements RequestCondition<PortRequestCondition> {

        private final Set<Integer> ports;

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

        @Override
        public PortRequestCondition combine(PortRequestCondition other) {
            return new PortRequestCondition(Stream.concat(ports.stream(), other.ports.stream()).collect(toUnmodifiableSet()));
        }

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

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

【讨论】:

    猜你喜欢
    • 2018-02-14
    • 2017-12-22
    • 1970-01-01
    • 2019-05-03
    • 2021-04-20
    • 1970-01-01
    • 2023-01-07
    • 2018-10-03
    • 2014-06-25
    相关资源
    最近更新 更多