Loading...

March 23, 2017

Ratpacked: Add Ratpack To Spring Boot Application

In a previous post we saw how we can use Spring Boot in a Ratpack application. But the integration can also be the other way around: using Ratpack in a Spring Boot application. This way we can use Ratpack's power to handle requests sent to our Spring Boot application and still use all Spring Boot features in our application. The easiest way to add Ratpack to a Spring Boot application is adding a Ratpack dependency and use the @EnableRatpack annotation. With this annotation a RatpackServer instance is created and started along with configuration options.

Let's see an example Spring Boot application with Ratpack enabled. First we add Ratpack as dependency to our Spring Boot application. In our example we also add Ratpack's Dropwizard module as dependency. We use Gradle in our example:

// File: build.gradle
plugins {
    id 'groovy'
    id 'idea'
    id 'org.springframework.boot' version '1.5.2.RELEASE'
}

repositories {
    jcenter()
}

ext {
    ratpackVersion = '1.4.5'
}
dependencies {
    compile 'org.springframework.boot:spring-boot-starter'
    compile 'org.springframework.boot:spring-boot-devtools'
    
    // Add Ratpack for Spring Boot dependency.
    compile "io.ratpack:ratpack-spring-boot-starter:$ratpackVersion"
    // Add Dropwizard for Ratpack dependency.
    compile "io.ratpack:ratpack-dropwizard-metrics:$ratpackVersion"
    
    runtime 'ch.qos.logback:logback-classic:1.2.2'
    
    testCompile "org.spockframework:spock-core:1.0-groovy-2.4" 
}

springBoot {
    mainClass = 'mrhaki.sample.SampleApp'    
}

Now we look at our example application. We use the annotation @EnableRatpack to have Ratpack in our Spring Boot application. We add a Spring bean that implements Action<Chain>. Beans of this type are recognised by Spring Boot and are added to the Ratpack configuration. We also add a Spring bean that is a Ratpack module. This bean is also automatically added to the Ratpack configuration.

// File: src/main/java/mrhaki/sample/SampleApp.java
package mrhaki.sample;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import ratpack.dropwizard.metrics.DropwizardMetricsConfig;
import ratpack.dropwizard.metrics.DropwizardMetricsModule;
import ratpack.func.Action;
import ratpack.handling.Chain;
import ratpack.handling.RequestLogger;
import ratpack.spring.config.EnableRatpack;
import ratpack.spring.config.RatpackServerCustomizer;

import java.time.Duration;

// Add Ratpack configuration for Spring Boot
@EnableRatpack
@EnableConfigurationProperties
@SpringBootApplication
public class SampleApp {

    /**
     * Start application.
     * 
     * @param args
     */
    public static void main(String[] args) {
        SpringApplication.run(SampleApp.class, args);
    }

    /**
     * Implementation for {@link MessageService} with pirate accent.
     * 
     * @return {@link MessageService} Pirate speak.
     */
    @Bean
    MessageService pirateMessage() {
        return name -> String.format("Arr, matey %s", name);
    }

    /**
     * Create Ratpack chain to handle requests to {@code /message} endpoint.
     * 
     * @return Ratpack chain.
     */
    @Bean
    Action<Chain> messageHandler() {
        return chain -> chain
                // Add logging for requests.
                .all(RequestLogger.ncsa())
                .get("message/:name?", ctx -> {
                    final String name = ctx.getPathTokens().getOrDefault("name", "mrhaki");
                    // Use MessageService implementation added to Spring context.
                    final String message = ctx.get(MessageService.class).message(name);
                    ctx.render(message);
                });
    }

    /**
     * Configuration properties to configure {@link DropwizardMetricsModule}.
     * Properties can be set via default Spring Boot mechanism like
     * environment variables, system properties, configuration files, etc.
     * 
     * @return Configuration for {@link DropwizardMetricsModule}
     */
    @Bean
    MetricsProperties metricsProperties() {
        return new MetricsProperties();
    }

    /**
     * Spring beans that are {@link com.google.inject.Module} objects are
     * automatically added to Ratpack's registry.
     * 
     * @param metricsProperties Configuration for module.
     * @return Module to add Dropwizard to Ratpack.
     */
    @Bean
    DropwizardMetricsModule metricsModule(final MetricsProperties metricsProperties) {
        // Create Dropwizard configuration.
        final DropwizardMetricsConfig config = new DropwizardMetricsConfig();
        if (metricsProperties.isJmx()) {
            config.jmx();
        }
        if (metricsProperties.getSlf4j().isEnabled()) {
            config.slf4j(slf4jConfig -> slf4jConfig
                    .enable(true)
                    .reporterInterval(Duration.ofSeconds(metricsProperties.getSlf4j().getInterval())));
        }

        // Create Dropwizard module.
        final DropwizardMetricsModule metricsModule = new DropwizardMetricsModule();
        metricsModule.setConfig(config);

        return metricsModule;
    }
}

We create a bean pirateMessage that implements the following interface:

package mrhaki.sample;

public interface MessageService {
    String message(final String name);
}

We also need the supporting class MetricsProperties to allow for configuration of the Dropwizard module.

// File: src/main/java/mrhaki/sample/MetricsProperties.java
package mrhaki.sample;

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * Configuration for {@link ratpack.dropwizard.metrics.DropwizardMetricsModule}.
 */
@ConfigurationProperties(prefix = "dropwizard")
public class MetricsProperties {
    private boolean jmx;
    private Slf4Config slf4j = new Slf4Config();

    public boolean isJmx() {
        return jmx;
    }

    public void setJmx(final boolean jmx) {
        this.jmx = jmx;
    }

    public Slf4Config getSlf4j() {
        return slf4j;
    }

    public static class Slf4Config {
        private boolean enabled;
        private long interval = 30;

        public boolean isEnabled() {
            return enabled;
        }

        public void setEnabled(final boolean enabled) {
            this.enabled = enabled;
        }

        public long getInterval() {
            return interval;
        }

        public void setInterval(final long interval) {
            this.interval = interval;
        }
    }
}

To complete our application we also add a configuration file where we can change several aspects of our application:

# File: src/main/resources/application.yml
ratpack:
  port: 9000
---
dropwizard:
  jmx: true
  slf4j:
    enabled: true
    interval: 10

Let's start the application and we can see already in the logging output Ratpack is started:

$ ./gradlew bootRun
:compileJava
:compileGroovy NO-SOURCE
:processResources UP-TO-DATE
:classes
:findMainClass
:bootRun
06:38:45.137 [main] DEBUG org.springframework.boot.devtools.settings.DevToolsSettings - Included patterns for restart : []
06:38:45.139 [main] DEBUG org.springframework.boot.devtools.settings.DevToolsSettings - Excluded patterns for restart : [/spring-boot-starter/target/classes/, /spring-boot-autoconfigure/target/classes/, /spring-boot-starter-[\w-]+/, /spring-boot/target/classes/, /spring-boot-actuator/target/classes/, /spring-boot-devtools/target/classes/]
06:38:45.140 [main] DEBUG org.springframework.boot.devtools.restart.ChangeableUrls - Matching URLs for reloading : [file:/Users/mrhaki/Projects/mrhaki.com/blog/posts/samples/ratpack/springboot/build/classes/main/, file:/Users/mrhaki/Projects/mrhaki.com/blog/posts/samples/ratpack/springboot/build/resources/main/]

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.5.2.RELEASE)

2017-03-23 06:38:45.531  INFO 25302 --- [  restartedMain] mrhaki.sample.SampleApp                  : Starting SampleApp on mrhaki-laptop-2015.fritz.box with PID 25302 (/Users/mrhaki/Projects/mrhaki.com/blog/posts/samples/ratpack/springboot/build/classes/main started by mrhaki in /Users/mrhaki/Projects/mrhaki.com/blog/posts/samples/ratpack/springboot)
2017-03-23 06:38:45.532  INFO 25302 --- [  restartedMain] mrhaki.sample.SampleApp                  : No active profile set, falling back to default profiles: default
2017-03-23 06:38:45.609  INFO 25302 --- [  restartedMain] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@701a7feb: startup date [Thu Mar 23 06:38:45 CET 2017]; root of context hierarchy
2017-03-23 06:38:46.023  INFO 25302 --- [  restartedMain] f.a.AutowiredAnnotationBeanPostProcessor : JSR-330 'javax.inject.Inject' annotation found and supported for autowiring
2017-03-23 06:38:46.687  INFO 25302 --- [  restartedMain] o.s.b.d.a.OptionalLiveReloadServer       : LiveReload server is running on port 35729
2017-03-23 06:38:46.714  INFO 25302 --- [  restartedMain] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2017-03-23 06:38:46.726  INFO 25302 --- [  restartedMain] ratpack.server.RatpackServer             : Starting server...
2017-03-23 06:38:46.963  INFO 25302 --- [  restartedMain] ratpack.server.RatpackServer             : Building registry...
2017-03-23 06:38:47.618  INFO 25302 --- [  restartedMain] ratpack.server.RatpackServer             : Initializing 1 services...
2017-03-23 06:38:47.711  INFO 25302 --- [  restartedMain] ratpack.server.RatpackServer             : Ratpack started for http://localhost:9000
2017-03-23 06:38:47.716  INFO 25302 --- [  restartedMain] mrhaki.sample.SampleApp                  : Started SampleApp in 2.556 seconds (JVM running for 2.992)

To add extra server configuration properties we must add a Spring bean that implements the ratpack.spring.config.RatpackServerCustomizer interface. The Spring Boot Ratpack configuration uses all beans found in the context that implement this interface. The interface has three methods we need to implement: getHandlers, getBindings and getServerConfig. The easiest way to implement the interface is by extending the class RatpackServerCustomizerAdapter. This class already provides empty implementations for the three methods. We only need to override the method we need in our application.

We rewrite our previous example application. We create a new class RatpackServerConfig that extends RatpackServerCustomizerAdapter. We override the method getServerConfig to set the development mode property of our Ratpack server configuration:

// File: src/main/java/mrhaki/sample/RatpackServerConfig.java
package mrhaki.sample;

import org.springframework.beans.factory.annotation.Autowired;
import ratpack.func.Action;
import ratpack.server.ServerConfigBuilder;
import ratpack.spring.config.RatpackProperties;
import ratpack.spring.config.RatpackServerCustomizerAdapter;

/**
 * Spring beans that implement {@link ratpack.spring.config.RatpackServerCustomizer}
 * interface our used for configuring Ratpack. The class
 * {@linly onk RatpackServerCustomizerAdapter} is a convenience class we can 
 * extend and only override the methods we need to.
 */
public class RatpackServerConfig extends RatpackServerCustomizerAdapter {

    /**
     * {@link RatpackProperties} configuration properties 
     * for Ratpack configuration.
     */
    private final RatpackProperties ratpack;

    public RatpackServerConfig(final RatpackProperties ratpack) {
        this.ratpack = ratpack;
    }

    /**
     * Extra configuration for the default Ratpack server configuration.
     * 
     * @return Extra server configuration.
     */
    @Override
    public Action<ServerConfigBuilder> getServerConfig() {
        return serverConfigBuilder -> serverConfigBuilder
                .development(ratpack.isDevelopment());
    }
    
}

We change SampleApp and add RatpackServerConfig as Spring bean:

// File: src/main/java/mrhaki/sample/SampleApp.java
...
    /**
     * Extra Ratpack server configuration.
     * 
     * @param ratpackProperties Properties for Ratpack server configuration.
     * @return Bean with extra Ratpack server configuration.
     */
    @Bean
    RatpackServerCustomizer ratpackServerSpec(final RatpackProperties ratpackProperties) {
        return new RatpackServerConfig(ratpackProperties);
    }
...

Another rewrite of our application could be to move all Ratpack configuration like handlers and bindings in the RatpackServerConfig class. We simply need to override the other two methods: getHandlers and getBindings. This way we have all the configuration together.

// File: src/main/java/mrhaki/sample/RatpackServerConfig.java
package mrhaki.sample;

import ratpack.dropwizard.metrics.DropwizardMetricsConfig;
import ratpack.dropwizard.metrics.DropwizardMetricsModule;
import ratpack.func.Action;
import ratpack.guice.BindingsSpec;
import ratpack.handling.Chain;
import ratpack.handling.RequestLogger;
import ratpack.server.ServerConfigBuilder;
import ratpack.spring.config.RatpackProperties;
import ratpack.spring.config.RatpackServerCustomizerAdapter;

import java.time.Duration;
import java.util.Arrays;
import java.util.List;

/**
 * Spring beans that implement {@link ratpack.spring.config.RatpackServerCustomizer}
 * interface our used for configuring Ratpack. The class
 * {@linly onk RatpackServerCustomizerAdapter} is a convenience class we can 
 * extend and only override the methods we need to.
 */
public class RatpackServerConfig extends RatpackServerCustomizerAdapter {

    /**
     * {@link RatpackProperties} configuration properties 
     * for Ratpack configuration. 
     */
    private final RatpackProperties ratpack;

    /**
     * Configuration properties for {@link DropwizardMetricsModule}.
     */
    private final MetricsProperties metrics;

    public RatpackServerConfig(
            final RatpackProperties ratpack, 
            final MetricsProperties metrics) {
        this.ratpack = ratpack;
        this.metrics = metrics;
    }

    /**
     * Extra configuration for the default Ratpack server configuration.
     * 
     * @return Extra server configuration.
     */
    @Override
    public Action<ServerConfigBuilder> getServerConfig() {
        return serverConfigBuilder -> serverConfigBuilder
                .development(ratpack.isDevelopment());
    }

    /**
     * Configure Ratpack handlers.
     * 
     * @return List of Ratpack chain configurations.
     */
    @Override
    public List<Action<Chain>> getHandlers() {
        return Arrays.asList(messageHandler()); 
    }

    /**
     * Create Ratpack chain to handle requests to {@code /message} endpoint.
     *
     * @return Ratpack chain.
     */
    private Action<Chain> messageHandler() {
        return chain -> chain
                // Add logging for requests.
                .all(RequestLogger.ncsa())
                .get("message/:name?", ctx -> {
                    final String name = ctx.getPathTokens().getOrDefault("name", "mrhaki");
                    // Use MessageService implementation added to Spring context.
                    final String message = ctx.get(MessageService.class).message(name);
                    ctx.render(message);
                });
    }

    /**
     * Add {@link DropwizardMetricsModule} to the Ratpack bindings.
     * 
     * @return Ratpack bindings.
     */
    @Override
    public Action<BindingsSpec> getBindings() {
        return bindings -> {
            bindings.module(DropwizardMetricsModule.class, dropwizardMetricsConfig());
        };
    }

    /**
     * Configuration for {@link DropwizardMetricsModule}.
     * 
     * @return Configuration action for configuring {@link DropwizardMetricsModule}.
     */
    private Action<DropwizardMetricsConfig> dropwizardMetricsConfig() {
        return config -> {
            if (metrics.isJmx()) {
                config.jmx();
            }
            if (metrics.getSlf4j().isEnabled()) {
                config.slf4j(slf4jConfig -> slf4jConfig
                        .enable(true)
                        .reporterInterval(Duration.ofSeconds(metrics.getSlf4j().getInterval())));
            }
        };
    }
}

Written with Ratpack 1.4.5 and Spring Boot 1.5.2.RELEASE.