0

Tomcat architecture is comprised of the following elements: Server => Service => Engine => Host => Context tomcat architecture

When configuring a standard Tomcat server, we can configure a custom thread pool by specifying the following in our server.xml file: (below is pseudo-code)

<Server>
  <Service name="Catalina">
    <Connector port="8080"/>
    <Executor name="custom-pool" className="my.package.poolImplementation" />
    <Engine name="Catalina" defaultHost="localhost">  
      <Here be more elements />
    </Engine>
  </Service>
</Server>

(specifically, the Executor name="custom-pool" className="my.package.poolImplementation")

How do I configure Spring Boot to allow the same behaviour programmatically ?
(WITHOUT using Spring configuration files)

No matter where i searched, or how hard I tried, I couldn't find any answer or example.
Thanks in advance

Yonatan Alon
  • 118
  • 1
  • 9
  • see if u can set properties like this https://stackoverflow.com/questions/51703746/setting-relaxedquerychars-for-embedded-tomcat/51703955#51703955 – pvpkiran Mar 30 '20 at 13:39
  • @pvpkiran I have already tried, to no avail. You can refer to the tomcat instance in the way specified in that question, but you can only provide an internal tomcat executor this way: TomcatWebServer server = (TomcatWebServer)factory.getWebServer(); server.getTomcat().getService().addExecutor(/*Only allows org.apache.catalina.Executor*/); – Yonatan Alon Mar 30 '20 at 14:01

3 Answers3

2

I looked up some source code (see TomcatServletWebServerFactory.java/ServletWebServerFactoryConfiguration.java) and found a way to do that.

@Bean
public TomcatProtocolHandlerCustomizer<?> tomcatProtocolHandlerCustomizer() {
    return protocolHandler -> {
        protocolHandler.setExecutor(...);
    };
}
Joseph
  • 111
  • 7
1

Considering two and a half years have passed since I originally asked this question, I think it is time that I shared our solution for the benefit of anyone that might read this in the future.

We ended up writing a custom component that implements WebServerFactoryCustomizer. Spring Boot will scan for all beans before starting its embedded Tomcat server. If it detects a bean that implements this interface, it will invoke the customize() method and pass the server factory as an argument to the function.

From there, it was straightforward:

package your.pack.age.name;

import org.apache.catalina.core.StandardThreadExecutor;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.embedded.tomcat.TomcatWebServer;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.stereotype.Component;

@Component
public class TomcatServerConfig implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
    private final StandardThreadExecutor customExecutor;

    public TomcatServerConfig() {
        this.customExecutor = YourExecutorImplementation();
    }

    @Override
    public void customize(TomcatServletWebServerFactory factory) {
        /*This web server is the Tomcat server embedded in Spring Boot*/
        TomcatWebServer webServer = (TomcatWebServer)factory.getWebServer()
        webServer.getTomcat().getService().addExecutor(this.customExecutor);
    }
}

(The actual code we used was simplified here, for the sake of a clear answer)

It is also worth noting that similar code needs to be written for the Tomcat Connectors, using TomcatConnectorCustomizer:

package your.pack.age.name;

import org.apache.catalina.connector.Connector;
import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.stereotype.Component;

@Component
public class TomcatConnectorConfig implements TomcatConnectorCustomizer {
    private final StandardThreadExecutor customExecutor;

    public TomcatConnectorConfig() {
        this.customExecutor = YourExecutorImplementation();
    }

    @Override
    public void customize(Connector connector) {
         connector.getProtocolHandler().setExecutor(this.customExecutor);
    }
}

For convenience, I am adding a skeletal custom implementation of a thread executor:

import org.apache.catalina.LifecycleException;
import org.apache.catalina.core.StandardThreadExecutor;

public class HTTPThreadExecutor extends StandardThreadExecutor {
    public HTTPThreadExecutor() {
        super();

        /* Custom constructor code here */
    }

    @Override
    protected void startInternal() throws LifecycleException {
        super.startInternal();

        /* Any work you need done upon startup */
    }
}
Yonatan Alon
  • 118
  • 1
  • 9
  • I did copy `org.apache.catalina.core.StandardThreadExecutor` into a demo app, changed its name. Followed instructions here and am receiving a `Caused by: org.apache.catalina.LifecycleException: An invalid Lifecycle transition was attempted ([STARTING]) for component [com.example.demo.tomcat.MyStandardThreadExecutor@778db1fc] in state [NEW]` any idea? – davidmpaz Aug 28 '23 at 19:38
  • @davidmpaz I added an example implementation to my answer, so that you can compare yours against it. – Yonatan Alon Aug 29 '23 at 13:03
  • @davidmpaz It is also worth checking the following resources: https://tomcat.apache.org/tomcat-8.5-doc/api/org/apache/catalina/core/StandardThreadExecutor.html#startInternal(), https://tomcat.apache.org/tomcat-8.5-doc/api/org/apache/catalina/util/LifecycleBase.html#startInternal(), https://tomcat.apache.org/tomcat-8.5-doc/api/org/apache/catalina/Lifecycle.html#START_EVENT – Yonatan Alon Aug 29 '23 at 13:05
  • thanks for the infos! I cecked them all, also I created a repo here: https://github.com/davidmpaz/spring-tomcat-executor, which reproduce my issue, do you think we could take a look there together? Thanks! – davidmpaz Aug 29 '23 at 18:28
  • @davidmpaz After checking your repo, notice that your implementing class is defined as `public class CustomThreadExecutor extends LifecycleMBeanBase implements Executor, ResizableExecutor`, and has an `@Slf4j` annotation above it. Please try and convert it to `public class CustomThreadExecutor extends StandardThreadExecutor`, as per the example provided, and remove the logger annotation. – Yonatan Alon Aug 31 '23 at 09:55
  • Please also remove the copy-paste code of the method implementations, which should now be defined in the `StandardThreadExecutor` super class you will be inheriting from – Yonatan Alon Aug 31 '23 at 10:01
  • thanks! How do you see it different. I mean the implementation is the same, I assume Tomcat expect only that the class implement required interfaces and extends from LifecycleMBeanBase, it should make no difference... I just updated the code as you told me, it is on branch: testing-inheritance, unfortunately I am getting same error. Thanks in advance! – davidmpaz Sep 01 '23 at 10:33
0

I needed to customize Tomcat also. I ended up with a code like this:

@Component
public class TomcatCustomizer extends TomcatServletWebServerFactory {
  @Override
  protected void postProcessContext(Context context) {
    Engine engine = (Engine) context.getParent().getParent();
    Service service = engine.getService();
    Server server = service.getServer();
    Connector connector = service.findConnectors()[0];
  }
}

You can then set different properties of the server, service, engine, connector. From the object service you can also access the executor and change it. This part I never tried. Whatever you change it will override and complete the spring-boot configuration, you will not loose the spring-boot config.

As Yonatan mentioned one can add an executor with service.addExecutor(...) there is also a method from removing an executor.

I needed this kind of detailed access to the configuration because I needed to configure the server.getGlobalNamingResources(). Also to add a JAASRealm and a Valve. I do not see how one could achieve complete config access with just the customize method.

danidacila
  • 147
  • 1
  • 3
  • I really like your solution, because it gives programmatic access to all Tomcat components. However, it is worth mentioning that an executor can be added to a Tomcat Service by calling `service.addExecutor(executorImpl)`. If you can kindly amend your code example, I will mark this as the accepted answer :) – Yonatan Alon Jan 29 '23 at 01:27
  • Do note that `postProcessContext()` gets called AFTER Tomcat is started, while 'customize()' get called before Tomcat is started. (see https://github.com/spring-projects/spring-boot/blob/390892f11beecfa2c24cfba344451433ab27837d/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java) – Yonatan Alon Jan 29 '23 at 01:44