1. CONFIGURING TOMCAT TO LISTEN ON MULTIPLE PORTS USING SPRING BOOT

Today I landed in a stackoverflow question about how to access the JavaMelody UI using a port other than the one used by the APIs. I went ahead I answered the question with some initial code I had for another blog post I’m working on but then decided to make it its own post.

While this could certainly be accomplished adding a couple of Spring Boot configuration properties: management.context-path and management.port to application.yml for instance, and reuse the same path used for actuator endpoints (/admin/info, /admin/health, …), there might be some cases where having the servlet container listening on a port other that the two mentioned earlier might make sense, maybe for monitoring, traffic analysis, firewall rules, etc..

In this post I’ll add support for configuring embedded Tomcat to listen on multiple ports and configure JavaMelody to exclusively use one of those ports to display its reports using Spring Boot.

2. REQUIREMENTS

  • Java 8 or Java 7. For Java 7, java.version property inside pom.xml needs to be updated accordingly.
  • Maven 3.3.x
  • Familiarity with Spring Framework.

3. CREATE THE SPRING BOOT DEMO APPLICATION

curl "https://start.spring.io/starter.tgz" -d bootVersion=1.4.2.RELEASE -d dependencies=actuator,web -d language=java -d type=maven-project -d baseDir=springboot-tomcat-multiple-ports -d groupId=com.asimio.demo.api -d artifactId=springboot-tomcat-multiple-ports -d version=0-SNAPSHOT | tar -xzvf -

This command will create a Maven project in a folder named springboot-tomcat-multiple-ports with most of the dependencies used in the accompanying source code.

Alternatively, it can also be generated using Spring Initializr tool then selecting Actuator and Web dependencies.

Application.java, the entry point to the application looks like:

...
@SpringBootApplication(scanBasePackages = { "com.asimio.api.demo.rest" })
@Import({ EmbeddedTomcatConfiguration.class, JavaMelodyConfiguration.class, FiltersConfiguration.class })
public class Application {

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

It scans for @Component-based annotated classes in the specified package and more interesting, will instantiate the beans defined in the imported classes via @Import annotation.

3.1 CONFIGURING TOMCAT

EmbeddedTomcatConfiguration.java, imported in Application.java is where embedded Tomcat is added support to listen on multiple and configurable ports:

 1 ...
 2 @Configuration
 3 public class EmbeddedTomcatConfiguration {
 4 
 5   @Value("${server.port}")
 6   private String serverPort;
 7 
 8   @Value("${management.port:${server.port}}")
 9   private String managementPort;
10 
11   @Value("${server.additionalPorts:null}")
12   private String additionalPorts;
13 
14   @Bean
15   public EmbeddedServletContainerFactory servletContainer() {
16     TomcatEmbeddedServletContainerFactory tomcat = new TomcatEmbeddedServletContainerFactory();
17     Connector[] additionalConnectors = this.additionalConnector();
18     if (additionalConnectors != null && additionalConnectors.length > 0) {
19       tomcat.addAdditionalTomcatConnectors(additionalConnectors);
20     }
21    return tomcat;
22   }
23 
24   private Connector[] additionalConnector() {
25     if (StringUtils.isBlank(this.additionalPorts)) {
26       return null;
27     }
28     Set<String> defaultPorts = Sets.newHashSet(this.serverPort, this.managementPort);
29     String[] ports = this.additionalPorts.split(",");
30     List<Connector> result = new ArrayList<>();
31     for (String port : ports) {
32       if (!defaultPorts.contains(port)) {
33         Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
34         connector.setScheme("http");
35         connector.setPort(Integer.valueOf(port));
36         result.add(connector);
37       }
38     }
39     return result.toArray(new Connector[] {});
40   }
41 }

This class adds an additional Tomcat connector for each port from server.additionalPorts configuration property excluding ports that were set in server.port and management.port.

The relevant configuration properties used in conjunction with EmbeddedTomcatConfiguration.java are included in application.yml:

server:
  port: ${appPort:8880}
  additionalPorts: 8882,8883

# Spring MVC actuator endpoints available via /admin/info, /admin/health, ...
server.servlet-path: /
management:
  context-path: /admin
  port: 8881
...

The embedded Tomcat instance powering this application will listen on 8880 for all the requests except for Spring Boot actuator endpoints which will be exposed on 8881 through /admin path. Notice the custom property server.additionalPorts, this is used in EmbeddedTomcatConfiguration.java to add more connectors to Tomcat, in fact, the endpoints reachable using port 8880 will be reachable through ports 8882 and 8883.

This is a sample log once this application is started:

...
2016-12-14 21:07:03 INFO  TomcatEmbeddedServletContainer:87 - Tomcat initialized with port(s): 8880 (http) 8882 (http) 8883 (http)
...
2016-12-14 21:07:04 INFO  TomcatEmbeddedServletContainer:87 - Tomcat initialized with port(s): 8881 (http)
...
2016-12-14 21:07:04 INFO  Http11NioProtocol:179 - Initializing ProtocolHandler ["http-nio-8881"]
2016-12-14 21:07:04 INFO  Http11NioProtocol:179 - Starting ProtocolHandler [http-nio-8881]
2016-12-14 21:07:04 INFO  NioSelectorPool:179 - Using a shared selector for servlet write/read
2016-12-14 21:07:04 INFO  TomcatEmbeddedServletContainer:185 - Tomcat started on port(s): 8881 (http)
...
2016-12-14 21:07:04 INFO  Http11NioProtocol:179 - Initializing ProtocolHandler ["http-nio-8880"]
2016-12-14 21:07:04 INFO  Http11NioProtocol:179 - Starting ProtocolHandler [http-nio-8880]
2016-12-14 21:07:04 INFO  NioSelectorPool:179 - Using a shared selector for servlet write/read
2016-12-14 21:07:04 INFO  Http11NioProtocol:179 - Initializing ProtocolHandler ["http-nio-8882"]
2016-12-14 21:07:04 INFO  NioSelectorPool:179 - Using a shared selector for servlet write/read
2016-12-14 21:07:04 INFO  Http11NioProtocol:179 - Starting ProtocolHandler [http-nio-8882]
2016-12-14 21:07:04 INFO  Http11NioProtocol:179 - Initializing ProtocolHandler ["http-nio-8883"]
2016-12-14 21:07:04 INFO  NioSelectorPool:179 - Using a shared selector for servlet write/read
2016-12-14 21:07:04 INFO  Http11NioProtocol:179 - Starting ProtocolHandler [http-nio-8883]
2016-12-14 21:07:04 INFO  TomcatEmbeddedServletContainer:185 - Tomcat started on port(s): 8880 (http) 8882 (http) 8883 (http)
...
3.2 CONFIGURING JAVAMELODY

JavaMelodyConfiguration.java, also imported in Application.java was gotten from here and here and is used to configure the JavaMelody servlet filter and which data would be collected using AOP:

...
@Configuration
public class JavaMelodyConfiguration implements ServletContextInitializer {
...
  @Bean(name = "javaMelodyFilter")
  public FilterRegistrationBean javaMelodyFilter() {
    final FilterRegistrationBean javaMelody = new FilterRegistrationBean();
    javaMelody.setFilter(new MonitoringFilter());
    javaMelody.setOrder(1);
    javaMelody.setAsyncSupported(true);
    javaMelody.setName("javaMelody");
    javaMelody.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC);
    javaMelody.addUrlPatterns("/*");
    javaMelody.addInitParameter("monitoring-path", "/monitoring");
    return javaMelody;
  }

  @Bean
  public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
    return new DefaultAdvisorAutoProxyCreator();
  }

  // Monitoring JDBC datasources
  @Bean
  public SpringDataSourceBeanPostProcessor monitoringDataSourceBeanPostProcessor() {
    final SpringDataSourceBeanPostProcessor processor = new SpringDataSourceBeanPostProcessor();
    processor.setExcludedDatasources(null);
    return processor;
  }

  // Monitoring of beans or methods annotated with @MonitoredWithSpring
  @Bean
  public MonitoringSpringAdvisor monitoringAdvisor() {
    final MonitoringSpringAdvisor interceptor = new MonitoringSpringAdvisor();
    interceptor.setPointcut(new MonitoredWithAnnotationPointcut());
    return interceptor;
  }

  // Monitoring of all services and controllers (even without having
  // @MonitoredWithSpring annotation)
  @Bean
  public MonitoringSpringAdvisor springServiceMonitoringAdvisor() {
    final MonitoringSpringAdvisor interceptor = new MonitoringSpringAdvisor();
    interceptor.setPointcut(new AnnotationMatchingPointcut(Service.class));
    return interceptor;
  }

  @Bean
  public MonitoringSpringAdvisor springControllerMonitoringAdvisor() {
    final MonitoringSpringAdvisor interceptor = new MonitoringSpringAdvisor();
    interceptor.setPointcut(new AnnotationMatchingPointcut(Controller.class));
    return interceptor;
  }

  @Bean
  public MonitoringSpringAdvisor springRestControllerMonitoringAdvisor() {
    final MonitoringSpringAdvisor interceptor = new MonitoringSpringAdvisor();
    interceptor.setPointcut(new AnnotationMatchingPointcut(RestController.class));
    return interceptor;
  }
}

But this setup would expose the JavaMelody UI on ports 8880, 8882 and 8883, lets fix that by adding another servlet filter and a configuration property to application.yml:

javaMelodyPort: 8883

FiltersConfiguration.java imported in Application.java too looks like:

...
public class FiltersConfiguration {

  @Value("${javaMelodyPort:${server.port}}")
  private Integer javaMelodyPort;

  @Value("${javaMelodyPortOnly:true}")
  private Boolean javaMelodyPortOnly;

  @Bean(name = "javaMelodyRestrictingFilter")
  public FilterRegistrationBean javaMelodyRestrictingFilter(FilterRegistrationBean javaMelodyFilter) {
    Filter filter = new OncePerRequestFilter() {

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

        if (!javaMelodyPortOnly || request.getLocalPort() == javaMelodyPort) {
          filterChain.doFilter(request, response);
        } else {
          response.sendError(404);
        }
      }
    };
    FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
    filterRegistrationBean.setFilter(filter);
    filterRegistrationBean.setOrder(-100);
    filterRegistrationBean.setName("javaMelodyPortRestriction");
    filterRegistrationBean.addUrlPatterns(javaMelodyFilter.getInitParameters().get("monitoring-path"));
    return filterRegistrationBean;
  }
}

All this servlet filter does is to verify if requests to the JavaMelody UI should be exclusively accepted using the configured port, if so, and the request comes from a different port, it sends back a 404, otherwise it chains the request to the next servlet filter.

4. RUNNING THE DEMO APP

mvn spring-boot:run

Sending a request to a resource implemented in DemoResource.java (not reviewed here but included in the source code):

curl http://localhost:8880/demo
demo

Sending a request to a Spring Boot actuator endpoint on the management port:

curl http://localhost:8881/admin/info
{"app":{"name":"springboot-tomcat-multiple-ports"},"build":{"version":"0-SNAPSHOT"}}

Sending a couple of requests on non-allowed ports to JavaMelody:

curl http://localhost:8880/monitoring
{"timestamp":1481786965410,"status":404,"error":"Not Found","message":"No message available","path":"/monitoring"}
curl http://localhost:8882/monitoring
{"timestamp":1481787018786,"status":404,"error":"Not Found","message":"No message available","path":"/monitoring"}

Loading JavaMelody UI in a browser using configured port 8883:

JavaMelody on an additional Tomcat port

As it can be seen JavaMelody UI can only be accessed through port 8883, a similar approach might be taken to restrict access to the API, which wasn’t done in this demo.

Hopefully you enjoyed reading this post, thanks and feedback is always appreciated. If you found this post helpful and would like to receive updates when content like this is published, sign up to the newsletter.

5. SOURCE CODE

Accompanying source code for this blog post can be found at:

6. REFERENCES