Exposing HTTP Restful API with Inbound Adapters. Part 2 (Java DSL)

In the previous part of this tutorial, we implemented an application exposing a Restful API using XML configuration. This part will re-implement this application using Spring Integration Java DSL.

The application is implemented with Java 8, but when Java 8 specific code is used (for example, when using lambdas), I will also show you how to do it in Java 7. Anyway, I shared both versions at Github in case you want to check it out:

Java 7 Java DSL example

Java 8 Java DSL example

This post is divided into the following sections

  1. Introduction
  2. Application configuration
  3. Get operation
  4. Put and post operations
  5. Delete operation
  6. Conclusion

 

1 Application configuration

In the web.xml file, the dispatcher servlet is configured to use Java Config:

<servlet>
    <servlet-name>springServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextClass</param-name>
        <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
    </init-param>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>xpadro.spring.integration.server.configuration</param-value>
    </init-param>
</servlet>
<servlet-mapping>
    <servlet-name>springServlet</servlet-name>
    <url-pattern>/spring/*</url-pattern>
</servlet-mapping>

 

In the pom.xml file, we include the Spring Integration Java DSL dependency:

<properties>
    <spring-version>4.1.3.RELEASE</spring-version>
    <spring-integration-version>4.1.0.RELEASE</spring-integration-version>
    <slf4j-version>1.7.5</slf4j-version>
    <junit-version>4.9</junit-version>
    <jackson-version>2.3.0</jackson-version>
</properties>

<dependencies>
    <!-- Spring Framework - Core -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring-version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>${spring-version}</version>
    </dependency>
    
    <!-- Spring Framework - Integration -->
    <dependency>
        <groupId>org.springframework.integration</groupId>
        <artifactId>spring-integration-core</artifactId>
        <version>${spring-integration-version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.integration</groupId>
        <artifactId>spring-integration-http</artifactId>
        <version>${spring-integration-version}</version>
    </dependency>
    
    <!-- Spring Integration - Java DSL -->
    <dependency>
        <groupId>org.springframework.integration</groupId>
        <artifactId>spring-integration-java-dsl</artifactId>
        <version>1.0.0.RELEASE</version>
    </dependency>
    
    <!-- JSON -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-core</artifactId>
        <version>${jackson-version}</version>
    </dependency>
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>${jackson-version}</version>
    </dependency>
    
    <!-- Testing -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>${spring-version}</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>${junit-version}</version>
        <scope>test</scope>
    </dependency>
    
    <!-- Logging -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>${slf4j-version}</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>${slf4j-version}</version>
    </dependency>
</dependencies>

 

InfrastructureConfiguration.java

The configuration class contains bean and flow definitions.

@Configuration
@ComponentScan("xpadro.spring.integration.server")
@EnableIntegration
public class InfrastructureConfiguration {
    
    @Bean
    public ExpressionParser parser() {
        return new SpelExpressionParser();
    }
    
    @Bean
    public HeaderMapper<HttpHeaders> headerMapper() {
        return new DefaultHttpHeaderMapper();
    }
    
    //flow and endpoint definitions
}

 

In order to parse payload expressions, we define a bean parser, using an SpELExpressionParser.

The header mapper will later be registered as a property of inbound gateways, in order to map HTTP headers from/to message headers.

The detail of the flows and endpoints defined in this configuration class is explained in each of the following sections.

2 Get operation

Our first step is to define the HTTP inbound gateway that will handle GET requests.

@Bean
public MessagingGatewaySupport httpGetGate() {
    HttpRequestHandlingMessagingGateway handler = new HttpRequestHandlingMessagingGateway();
    handler.setRequestMapping(createMapping(new HttpMethod[]{HttpMethod.GET}, "/persons/{personId}"));
    handler.setPayloadExpression(parser().parseExpression("#pathVariables.personId"));
    handler.setHeaderMapper(headerMapper());
    
    return handler;
}

private RequestMapping createMapping(HttpMethod[] method, String... path) {
    RequestMapping requestMapping = new RequestMapping();
    requestMapping.setMethods(method);
    requestMapping.setConsumes("application/json");
    requestMapping.setProduces("application/json");
    requestMapping.setPathPatterns(path);
    
    return requestMapping;
}

 

The createMapping method is the Java alternative to the request-mapping XML element seen in the previous part of the tutorial. In this case, we can also use it to define the request path and supported methods.

Now that we have our gateway set, let’s define the flow that will serve GET requests (remember you can check a diagram of the full flow in the previous part of the tutorial):

@Bean
public IntegrationFlow httpGetFlow() {
    return IntegrationFlows.from(httpGetGate()).channel("httpGetChannel").handle("personEndpoint", "get").get();
}

 

The flow works as follows:

Since we are using a gateway, the response of the personEndpoint will be sent back to the client.

I am showing the personEndpoint for convenience, since it’s actually the same as in the XML application:

@Component
public class PersonEndpoint {
    private static final String STATUSCODE_HEADER = "http_statusCode";
    
    @Autowired
    private PersonService service;
    
    public Message<?> get(Message<String> msg) {
        long id = Long.valueOf(msg.getPayload());
        ServerPerson person = service.getPerson(id);
        
        if (person == null) {
            return MessageBuilder.fromMessage(msg)
                .copyHeadersIfAbsent(msg.getHeaders())
                .setHeader(STATUSCODE_HEADER, HttpStatus.NOT_FOUND)
                .build(); 
        }
        
        return MessageBuilder.withPayload(person)
            .copyHeadersIfAbsent(msg.getHeaders())
            .setHeader(STATUSCODE_HEADER, HttpStatus.OK)
            .build();
    }
    
    //other operations
}

 

GetOperationsTest uses a RestTemplate to test the exposed HTTP GET integration flow:

@RunWith(BlockJUnit4ClassRunner.class)
public class GetOperationsTest {
    private static final String URL = "http://localhost:8081/int-http-dsl/spring/persons/{personId}";
    private final RestTemplate restTemplate = new RestTemplate();
    
    private HttpHeaders buildHeaders() {
        HttpHeaders headers = new HttpHeaders();
        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
        headers.setContentType(MediaType.APPLICATION_JSON); 
        
        return headers;
    }
    
    @Test
    public void getResource_responseIsConvertedToPerson() {
        HttpEntity<Integer> entity = new HttpEntity<>(buildHeaders());
        ResponseEntity<ClientPerson> response = restTemplate.exchange(URL, HttpMethod.GET, entity, ClientPerson.class, 1);
        assertEquals("John" , response.getBody().getName());
        assertEquals(HttpStatus.OK, response.getStatusCode());
    }
    
    //more tests
}

 

I won’t show the full class since it is the same as in the XML example.

3 Put and post operations

Continuing with our Restful API application example, we define a bean for the HTTP inbound channel adapter.  You may notice that we are creating a new Gateway. The reason is that inbound channel adapter is internally implemented as a gateway that is not expecting a reply.

@Bean
public MessagingGatewaySupport httpPostPutGate() {
    HttpRequestHandlingMessagingGateway handler = new HttpRequestHandlingMessagingGateway();
    handler.setRequestMapping(createMapping(new HttpMethod[]{HttpMethod.PUT, HttpMethod.POST}, "/persons", "/persons/{personId}"));
    handler.setStatusCodeExpression(parser().parseExpression("T(org.springframework.http.HttpStatus).NO_CONTENT"));
    handler.setRequestPayloadType(ServerPerson.class);
    handler.setHeaderMapper(headerMapper());
    
    return handler;
}

 

We are again using the parser to resolve the returned status code expression.

The former XML attribute request-payload-type of the inbound adapter is now set as a property of the gateway.

The flow that handles both PUT and POST operations uses a router to send the message to the appropriate endpoint, depending on the HTTP method received:

@Bean
public IntegrationFlow httpPostPutFlow() {
    return IntegrationFlows.from(httpPostPutGate()).channel("routeRequest").route("headers.http_requestMethod", 
        m -> m.prefix("http").suffix("Channel")
            .channelMapping("PUT", "Put")
            .channelMapping("POST", "Post")
    ).get();
}

@Bean
public IntegrationFlow httpPostFlow() {
    return IntegrationFlows.from("httpPostChannel").handle("personEndpoint", "post").get();
}

@Bean
public IntegrationFlow httpPutFlow() {
    return IntegrationFlows.from("httpPutChannel").handle("personEndpoint", "put").get();
}

 

The flow is executed the following way:

Subflows (httpPutFlow and httpPostFlow) will receive messages from the router and handle them in our personEndpoint.

@Component
public class PersonEndpoint {
    @Autowired
    private PersonService service;
    
    //Get operation
    
    public void put(Message<ServerPerson> msg) {
        service.updatePerson(msg.getPayload());
    }
    
    public void post(Message<ServerPerson> msg) {
        service.insertPerson(msg.getPayload());
    }
}

 

Since we defined an inbound adapter, no response from the endpoint is expected.

In the router definition we used Java 8 lambdas. I told you I would show the alternative in Java 7, so a promise is a promise:

@Bean
public IntegrationFlow httpPostPutFlow() {
    return IntegrationFlows.from(httpPostPutGate()).channel("routeRequest").route("headers.http_requestMethod", 
        new Consumer<RouterSpec<ExpressionEvaluatingRouter>>() {
            @Override
            public void accept(RouterSpec<ExpressionEvaluatingRouter> spec) {
                spec.prefix("http").suffix("Channel")
                    .channelMapping("PUT", "Put")
                    .channelMapping("POST", "Post");
            }
        }
    ).get();
}

 

A little bit longer, isn’t it?

The PUT flow is tested by the PutOperationsTest class:

@RunWith(BlockJUnit4ClassRunner.class)
public class PutOperationsTest {
    private static final String URL = "http://localhost:8081/int-http-dsl/spring/persons/{personId}";
    private final RestTemplate restTemplate = new RestTemplate();
    
    //build headers method
    
    @Test
    public void updateResource_noContentStatusCodeReturned() {
        HttpEntity<Integer> getEntity = new HttpEntity<>(buildHeaders());
        ResponseEntity<ClientPerson> response = restTemplate.exchange(URL, HttpMethod.GET, getEntity, ClientPerson.class, 4);
        ClientPerson person = response.getBody();
        person.setName("Sandra");
        HttpEntity<ClientPerson> putEntity = new HttpEntity<ClientPerson>(person, buildHeaders());
        
        response = restTemplate.exchange(URL, HttpMethod.PUT, putEntity, ClientPerson.class, 4);
        assertEquals(HttpStatus.NO_CONTENT, response.getStatusCode());
        
        response = restTemplate.exchange(URL, HttpMethod.GET, getEntity, ClientPerson.class, 4);
        person = response.getBody();
        assertEquals("Sandra", person.getName());
    }
}

 

The POST flow is tested by the PostOperationsTest class:

@RunWith(BlockJUnit4ClassRunner.class)
public class PostOperationsTest {
    private static final String POST_URL = "http://localhost:8081/int-http-dsl/spring/persons";
    private static final String GET_URL = "http://localhost:8081/int-http-dsl/spring/persons/{personId}";
    private final RestTemplate restTemplate = new RestTemplate();
    
    //build headers method
    
    @Test
    public void addResource_noContentStatusCodeReturned() {
        ClientPerson person = new ClientPerson(9, "Jana");
        HttpEntity<ClientPerson> entity = new HttpEntity<ClientPerson>(person, buildHeaders());
        
        ResponseEntity<ClientPerson> response = restTemplate.exchange(POST_URL, HttpMethod.POST, entity, ClientPerson.class);
        assertEquals(HttpStatus.NO_CONTENT, response.getStatusCode());
        
        HttpEntity<Integer> getEntity = new HttpEntity<>(buildHeaders());
        response = restTemplate.exchange(GET_URL, HttpMethod.GET, getEntity, ClientPerson.class, 9);
        person = response.getBody();
        assertEquals("Jana", person.getName());
    }
}

 

4 Delete operation

With this operation we complete our application. The entry point is defined by the following bean:

@Bean
public MessagingGatewaySupport httpDeleteGate() {
    HttpRequestHandlingMessagingGateway handler = new HttpRequestHandlingMessagingGateway();
    handler.setRequestMapping(createMapping(new HttpMethod[]{HttpMethod.DELETE}, "/persons/{personId}"));
    handler.setStatusCodeExpression(parser().parseExpression("T(org.springframework.http.HttpStatus).NO_CONTENT"));
    handler.setPayloadExpression(parser().parseExpression("#pathVariables.personId"));
    handler.setHeaderMapper(headerMapper());
    
    return handler;
}

The configuration is pretty similar to the PutPost gateway. I won’t explain it again.

The delete flow sends the deletion request to the personEndpoint:

@Bean
public IntegrationFlow httpDeleteFlow() {
    return IntegrationFlows.from(httpDeleteGate()).channel("httpDeleteChannel").handle("personEndpoint", "delete").get();
}

 

And our bean will request the service to delete the resource:

public void delete(Message<String> msg) {
    long id = Long.valueOf(msg.getPayload());
    service.deletePerson(id);
}

 

The test asserts that the resource no longer exists after deletion:

@RunWith(BlockJUnit4ClassRunner.class)
public class DeleteOperationsTest {
    private static final String URL = "http://localhost:8081/int-http-dsl/spring/persons/{personId}";
    private final RestTemplate restTemplate = new RestTemplate();
    
    //build headers method
    
    @Test
    public void deleteResource_noContentStatusCodeReturned() {
        HttpEntity<Integer> entity = new HttpEntity<>(buildHeaders());
        ResponseEntity<ClientPerson> response = restTemplate.exchange(URL, HttpMethod.DELETE, entity, ClientPerson.class, 3);
        assertEquals(HttpStatus.NO_CONTENT, response.getStatusCode());
        
        try {
            response = restTemplate.exchange(URL, HttpMethod.GET, entity, ClientPerson.class, 3);
            Assert.fail("404 error expected");
        } catch (HttpClientErrorException e) {
            assertEquals(HttpStatus.NOT_FOUND, e.getStatusCode());
        }
    }
}

 

5 Conclusion

This second part of the tutorial has shown us how to implement a Spring Integration application with no XML configuration, using the new Spring Integration Java DSL. Although flow configuration is more readable using Java 8 lambdas, we still have the option to use Java DSL with previous versions of the language.

I’m publishing my new posts on Google plus and Twitter. Follow me if you want to be updated with new content.