Exposing HTTP Restful API with Inbound Adapters. Part 2 (Java DSL)
Integration, Spring ·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:
This post is divided into the following sections
- Introduction
- Application configuration
- Get operation
- Put and post operations
- Delete operation
- 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:
- from(httpGetGate()): Get messages received by the HTTP Inbound Gateway.
- channel(“httpGetChannel”): Register a new DirectChannel bean and send the message received to it.
- handle(“personEndpoint”, “get”): Messages sent to the previous channel will be consumed by our personEndpoint bean, invoking its get method.
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:
- from(httpPostPutGate()):Get messages received by the HTTP Inbound adapter.
- channel(“routeRequest”): Register a DirectChannel bean and send the message received to it.
- route(…): Messages sent to the previous channel will be handled by a router, which will redirect them based on the HTTP method received (http_requestMethod header). The destination channel is resolved applying the prefix and suffix. For example, if the HTTP method is PUT, the resolved channel will be httpPutChannel, which is a bean also defined in this configuration class.
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.