Skip to content

Commit

Permalink
INT-4436: Register HTTP endpoints at runtime
Browse files Browse the repository at this point in the history
JIRA: https://jira.spring.io/browse/INT-4436

* Fix `IntegrationRequestMappingHandlerMapping` and
`IntegrationRequestMappingHandlerMapping` to implement a
`DestructionAwareBeanPostProcessor` to allow to register and
destroy HTTP endpoints at runtime, e.g. via `IntegrationFlowContext`
with the dynamic `IntegrationFlow`s
  • Loading branch information
artembilan authored and garyrussell committed Jun 25, 2018
1 parent 07c32ae commit ee501c8
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2013-2017 the original author or authors.
* Copyright 2013-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -24,6 +24,8 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.core.annotation.AnnotationUtils;
Expand Down Expand Up @@ -60,13 +62,18 @@
* {@code org.springframework.stereotype.Controller} user-class may have their own
* {@link org.springframework.web.bind.annotation.RequestMapping}.
* On the other side, all Spring Integration HTTP Inbound Endpoints are configured on
* the basis of the same {@link HttpRequestHandlingEndpointSupport} class and there is no
* the basis of the same {@link BaseHttpInboundEndpoint} class and there is no
* single {@link RequestMappingInfo} configuration without
* {@link org.springframework.web.method.HandlerMethod} in Spring MVC.
* Accordingly {@link IntegrationRequestMappingHandlerMapping} is a
* {@link org.springframework.web.servlet.HandlerMapping}
* compromise implementation between method-level annotations and component-level
* (e.g. Spring Integration XML) configurations.
* <p>
* Starting with version 5.1, this class implements {@link DestructionAwareBeanPostProcessor} to
* register HTTP endpoints at runtime for dynamically declared beans, e.g. via
* {@link org.springframework.integration.dsl.context.IntegrationFlowContext}, and unregister
* them during the {@link BaseHttpInboundEndpoint} destruction.
*
* @author Artem Bilan
*
Expand All @@ -76,13 +83,35 @@
* @see RequestMappingHandlerMapping
*/
public final class IntegrationRequestMappingHandlerMapping extends RequestMappingHandlerMapping
implements ApplicationListener<ContextRefreshedEvent> {
implements ApplicationListener<ContextRefreshedEvent>, DestructionAwareBeanPostProcessor {

private static final Method HANDLE_REQUEST_METHOD = ReflectionUtils.findMethod(HttpRequestHandler.class,
"handleRequest", HttpServletRequest.class, HttpServletResponse.class);

private final AtomicBoolean initialized = new AtomicBoolean();

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (this.initialized.get() && isHandler(bean.getClass())) {
detectHandlerMethods(bean);
}

return bean;
}

@Override
@SuppressWarnings("unchecked")
public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException {
if (isHandler(bean.getClass())) {
unregisterMapping(getMappingForEndpoint((BaseHttpInboundEndpoint) bean));
}
}

@Override
public boolean requiresDestruction(Object bean) {
return isHandler(bean.getClass());
}

@Override
protected boolean isHandler(Class<?> beanType) {
return HttpRequestHandlingEndpointSupport.class.isAssignableFrom(beanType);
Expand All @@ -97,6 +126,7 @@ protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpSer
handler = bean;
}
}

return super.getHandlerExecutionChain(handler, request);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.dsl.context.IntegrationFlowContext;
import org.springframework.integration.http.outbound.HttpRequestExecutingMessageHandler;
import org.springframework.integration.security.channel.ChannelSecurityInterceptor;
import org.springframework.integration.security.channel.SecuredChannel;
Expand Down Expand Up @@ -80,6 +81,9 @@ public class HttpDslTests {
@Autowired
private HttpRequestExecutingMessageHandler serviceInternalGatewayHandler;

@Autowired
private IntegrationFlowContext integrationFlowContext;

private MockMvc mockMvc;

@Before
Expand Down Expand Up @@ -114,6 +118,37 @@ public void testHttpProxyFlow() throws Exception {
.isForbidden());
}

@Test
public void testDynamicHttpEndpoint() throws Exception {
IntegrationFlow flow =
IntegrationFlows.from(Http.inboundGateway("/dynamic")
.requestMapping(r -> r.params("name"))
.payloadExpression("#requestParams.name[0]"))
.<String, String>transform(String::toLowerCase)
.get();

IntegrationFlowContext.IntegrationFlowRegistration flowRegistration =
this.integrationFlowContext.registration(flow).register();

this.mockMvc.perform(
get("/dynamic")
.with(httpBasic("admin", "admin"))
.param("name", "BAR"))
.andExpect(
content()
.string("bar"));

flowRegistration.destroy();

this.mockMvc.perform(
get("/dynamic")
.with(httpBasic("admin", "admin"))
.param("name", "BAZ"))
.andExpect(
status()
.isNotFound());
}

@Configuration
@EnableWebSecurity
@EnableIntegration
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2017 the original author or authors.
* Copyright 2017-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -19,6 +19,8 @@
import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicBoolean;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.integration.http.config.HttpContextUtils;
Expand Down Expand Up @@ -66,6 +68,11 @@
* {@link org.springframework.web.reactive.HandlerMapping}
* compromise implementation between method-level annotations and component-level
* (e.g. Spring Integration XML) configurations.
* <p>
* Starting with version 5.1, this class implements {@link DestructionAwareBeanPostProcessor} to
* register HTTP endpoints at runtime for dynamically declared beans, e.g. via
* {@link org.springframework.integration.dsl.context.IntegrationFlowContext}, and unregister
* them during the {@link WebFluxInboundEndpoint} destruction.
*
* @author Artem Bilan
*
Expand All @@ -75,13 +82,35 @@
* @see RequestMappingHandlerMapping
*/
public class WebFluxIntegrationRequestMappingHandlerMapping extends RequestMappingHandlerMapping
implements ApplicationListener<ContextRefreshedEvent> {
implements ApplicationListener<ContextRefreshedEvent>, DestructionAwareBeanPostProcessor {

private static final Method HANDLER_METHOD = ReflectionUtils.findMethod(WebHandler.class,
"handle", ServerWebExchange.class);

private final AtomicBoolean initialized = new AtomicBoolean();

@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (this.initialized.get() && isHandler(bean.getClass())) {
detectHandlerMethods(bean);
}

return bean;
}

@Override
@SuppressWarnings("unchecked")
public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException {
if (isHandler(bean.getClass())) {
unregisterMapping(getMappingForEndpoint((WebFluxInboundEndpoint) bean));
}
}

@Override
public boolean requiresDestruction(Object bean) {
return isHandler(bean.getClass());
}

@Override
protected boolean isHandler(Class<?> beanType) {
return WebFluxInboundEndpoint.class.isAssignableFrom(beanType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.dsl.context.IntegrationFlowContext;
import org.springframework.integration.http.HttpHeaders;
import org.springframework.integration.http.dsl.Http;
import org.springframework.integration.support.MessageBuilder;
Expand Down Expand Up @@ -109,6 +110,9 @@ public class WebFluxDslTests {
@Autowired
private WebApplicationContext wac;

@Autowired
private IntegrationFlowContext integrationFlowContext;

@Autowired
@Qualifier("webFluxWithReplyPayloadToFlux.handler")
private WebFluxRequestExecutingMessageHandler webFluxWithReplyPayloadToFlux;
Expand Down Expand Up @@ -246,6 +250,33 @@ public void testSse() {
.verifyComplete();
}

@Test
public void testDynamicHttpEndpoint() throws Exception {
IntegrationFlow flow =
IntegrationFlows.from(WebFlux.inboundGateway("/dynamic")
.requestMapping(r -> r.params("name"))
.payloadExpression("#requestParams.name[0]"))
.<String, String>transform(String::toLowerCase)
.get();

IntegrationFlowContext.IntegrationFlowRegistration flowRegistration =
this.integrationFlowContext.registration(flow).register();

this.webTestClient.get().uri("/dynamic?name=BAR")
.attributes(basicAuthenticationCredentials("guest", "guest"))
.exchange()
.expectBody(String.class)
.isEqualTo("bar");

flowRegistration.destroy();

this.webTestClient.get().uri("/dynamic?name=BAZ")
.attributes(basicAuthenticationCredentials("guest", "guest"))
.exchange()
.expectStatus()
.isNotFound();
}

@Configuration
@EnableWebFlux
@EnableWebSecurity
Expand Down

0 comments on commit ee501c8

Please sign in to comment.