当前位置: 首页 > 知识库问答 >
问题:

Spring Boot |如何动态添加新的tomcat连接器?

井誉
2023-03-14

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

我能够使用ServletWebServerFactory和tomcatConnectorCustomizer添加连接器。但是这个bean只在Spring启动时加载。

@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连接器?比如说方法调用?

我设法在运行时添加了一个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();

我应该如何继续?

共有3个答案

裴姚石
2023-03-14

基于大家公认的“阿里尔·卡雷拉”的答案,我做了一些不同的事情(IMO cleaner):

  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{...}
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;
        }
    }
}
古明煦
2023-03-14

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

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

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

public class MySingletonBean {

    @Autowired
    private ApplicationContext applicationContext;

    public void showMessage(){
        MyPrototypeBean bean = applicationContext.getBean(MyPrototypeBean.class);
    }
}
呼延弘方
2023-03-14

嗨,这里是我的示例项目:示例项目

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

预期响应:添加端口:9090

3-尝试:

http://localhost:9090/alternative/hello

预期响应:hello测试2

4-尝试预期错误:

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

预期响应(允许上下文[alternative],但endpoint未在此上下文的控制器中注册):白标签错误页。。。

http://localhost:9090/any/hello

预期响应(不允许上下文[任何]):白标签错误页。。。

http://localhost:8888/any/hello

预期响应(无效端口号):ERR\u CONNECTION\u拒绝

http://localhost:8080/hello

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

5-尝试删除端口:

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

6-检查已移除的端口:

http://localhost:9090/alternative/hello

预期响应(端口关闭):ERR\u CONNECTION\u拒绝

我希望这有帮助。

 类似资料:
  • 在Spring Boot文档中,有一节描述了如何为tomcat启用多个连接器(http://docs.spring.io/spring-boot/docs/1.1.7.RELEASE/reference/htmlsingle/#howto-在tomcat中启用多个连接器)。 但是有没有一种方法可以简单地将连接器添加到现有的连接器(web和管理连接器)?并将它们绑定到一些mvc控制器?我想做的是创建

  • 本文向大家介绍SpringBoot如何取消内置Tomcat启动并改用外接Tomcat,包括了SpringBoot如何取消内置Tomcat启动并改用外接Tomcat的使用技巧和注意事项,需要的朋友参考一下 这篇文章主要介绍了SpringBoot如何取消内置Tomcat启动并改用外接Tomcat,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 1,修改

  • 在我的情况下,有可能,例如,一个新的设备被启动,因此必须处理另一个流。但是如何动态添加这个新流呢?

  • 我需要在我的表中添加一个新产品。这http://localhost:8080/product/create/在Postman上工作。当我发布并添加新产品时,它会返回id并自动将其添加到我的表中。我的问题是如何在角CLI上实现它。我不知道我该怎么做。有人能帮我吗?这是我的代码: 产品Controller.java 应用程序。组成部分ts 产品服务ts app.component.html

  • 问题内容: 我正在写一些演示Web服务器,提供静态html,css和javascript。服务器看起来像 我的客户端javascript使ajax调用到其他服务器。我该如何添加 对我的服务器响应,以便客户端javascript可以进行ajax调用? 问题答案: 自从Express宠坏了我以来,弄清楚这个问题有点麻烦。 看看enable cors。基本上,您需要做的是添加到要启用cors的域中。re

  • 我们有一个spring-boot应用程序,它使用嵌入式tomcat进行部署,并使用MySQL后端的默认tomcat-jdbc连接池,而没有为MySQL或tomcat端定制。 该应用程序有一些调度程序,它们主要在一天中的特定时间运行,即在昨天的最后一次cron运行和今天的第一次cron运行之间,有超过9个小时的间隙。然而,无论何时cron在早期运行,它都从未遇到过空闲连接问题。 现在我们看到一条错误