Migrate to servlet binder for web features
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
|
||||
<properties>
|
||||
<spring-cloud-deployer-thin.version>1.0.8.RELEASE</spring-cloud-deployer-thin.version>
|
||||
<spring-cloud-stream-servlet.version>1.0.0.BUILD-SNAPSHOT</spring-cloud-stream-servlet.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -25,8 +26,12 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-function-web</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<artifactId>spring-cloud-function-stream</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-stream-binder-servlet</artifactId>
|
||||
<version>${spring-cloud-stream-servlet.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2016-2017 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.cloud.function.deployer;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
*
|
||||
*/
|
||||
@Component
|
||||
public class DeployedApplicationFilter extends WebMvcConfigurerAdapter
|
||||
implements HandlerInterceptor {
|
||||
|
||||
private final FunctionExtractingFunctionCatalog deployer;
|
||||
|
||||
@Autowired
|
||||
public DeployedApplicationFilter(FunctionExtractingFunctionCatalog deployer) {
|
||||
this.deployer = deployer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
|
||||
Object handler) throws Exception {
|
||||
String path = (String) request
|
||||
.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
|
||||
if (path != null) {
|
||||
// TODO: extract /stream to config property
|
||||
if (path.startsWith("/stream")) {
|
||||
String name = path.substring("/stream/".length());
|
||||
if (name.contains("/")) {
|
||||
name = name.substring(0, name.indexOf("/"));
|
||||
}
|
||||
if (deployer.deployed().containsKey(name)) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
response.setStatus(HttpStatus.NOT_FOUND.value());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postHandle(HttpServletRequest request, HttpServletResponse response,
|
||||
Object handler, ModelAndView modelAndView) throws Exception {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
|
||||
Object handler, Exception ex) throws Exception {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -17,7 +17,7 @@ package org.springframework.cloud.function.deployer;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.cloud.function.context.ContextFunctionCatalogAutoConfiguration;
|
||||
import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
|
||||
@@ -31,13 +31,16 @@ import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.loader.thin.ArchiveUtils;
|
||||
import org.springframework.cloud.deployer.spi.app.AppDeployer;
|
||||
import org.springframework.cloud.deployer.spi.core.AppDefinition;
|
||||
import org.springframework.cloud.deployer.spi.core.AppDeploymentRequest;
|
||||
import org.springframework.cloud.deployer.thin.ThinJarAppDeployer;
|
||||
import org.springframework.cloud.function.context.FunctionInspector;
|
||||
import org.springframework.cloud.function.context.catalog.FunctionInspector;
|
||||
import org.springframework.cloud.function.core.FunctionCatalog;
|
||||
import org.springframework.cloud.function.stream.config.SupplierInvokingMessageProducer;
|
||||
import org.springframework.cloud.stream.binder.servlet.RouteRegistrar;
|
||||
import org.springframework.context.support.LiveBeansView;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
@@ -49,6 +52,10 @@ public class FunctionExtractingFunctionCatalog
|
||||
private static Log logger = LogFactory
|
||||
.getLog(FunctionExtractingFunctionCatalog.class);
|
||||
|
||||
private RouteRegistrar routes;
|
||||
|
||||
private SupplierInvokingMessageProducer<?> producer;
|
||||
|
||||
private ThinJarAppDeployer deployer;
|
||||
|
||||
private Map<String, String> deployed = new LinkedHashMap<>();
|
||||
@@ -65,6 +72,16 @@ public class FunctionExtractingFunctionCatalog
|
||||
deployer = new ThinJarAppDeployer(name, profiles);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setRouteRegistrar(RouteRegistrar routes) {
|
||||
this.routes = routes;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setProducer(SupplierInvokingMessageProducer<?> producer) {
|
||||
this.producer = producer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() throws Exception {
|
||||
for (String name : new HashSet<>(names.keySet())) {
|
||||
@@ -156,6 +173,7 @@ public class FunctionExtractingFunctionCatalog
|
||||
this.deployed.put(id, path);
|
||||
this.names.put(name, id);
|
||||
this.ids.put(id, name);
|
||||
register(name);
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -165,6 +183,7 @@ public class FunctionExtractingFunctionCatalog
|
||||
// TODO: Convert to 404
|
||||
throw new IllegalStateException("No such app");
|
||||
}
|
||||
unregister(name);
|
||||
this.deployer.undeploy(id);
|
||||
String path = this.deployed.remove(id);
|
||||
this.names.remove(name);
|
||||
@@ -172,6 +191,39 @@ public class FunctionExtractingFunctionCatalog
|
||||
return new DeployedArtifact(name, id, path);
|
||||
}
|
||||
|
||||
private void register(String name) {
|
||||
Set<String> names = getSupplierNames(name);
|
||||
if (routes != null) {
|
||||
logger.info("Registering routes: " + names);
|
||||
routes.registerRoutes(getSupplierNames(name));
|
||||
}
|
||||
if (producer != null) {
|
||||
// Need an ApplicationEvent that we can react to in the producer?
|
||||
for (String supplier : names) {
|
||||
producer.start(supplier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Set<String> getSupplierNames(String name) {
|
||||
String id = this.names.get(name);
|
||||
return (Set<String>) invoke(id, FunctionCatalog.class, "getSupplierNames");
|
||||
}
|
||||
|
||||
private void unregister(String name) {
|
||||
Set<String> names = getSupplierNames(name);
|
||||
if (routes != null) {
|
||||
logger.info("Unregistering routes: " + names);
|
||||
routes.unregisterRoutes(names);
|
||||
}
|
||||
if (producer != null) {
|
||||
for (String supplier : names) {
|
||||
producer.stop(supplier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Object inspect(Object arg, String method) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Inspecting " + method);
|
||||
@@ -195,53 +247,90 @@ public class FunctionExtractingFunctionCatalog
|
||||
|
||||
private Object invoke(Class<?> type, String method, Object... arg) {
|
||||
Set<Object> results = new LinkedHashSet<>();
|
||||
Object fallback = null;
|
||||
for (String id : this.deployed.keySet()) {
|
||||
Object catalog = this.deployer.getBean(id, type);
|
||||
if (catalog == null) {
|
||||
Object result = invoke(id, type, method, arg);
|
||||
if (result instanceof Collection) {
|
||||
results.addAll((Collection<?>) result);
|
||||
continue;
|
||||
}
|
||||
String name = this.ids.get(id);
|
||||
String prefix = name + "/";
|
||||
if (arg.length == 1) {
|
||||
if (arg[0] instanceof String) {
|
||||
String specific = arg[0].toString();
|
||||
if (specific.startsWith(prefix)) {
|
||||
arg[0] = specific.substring(prefix.length());
|
||||
}
|
||||
else {
|
||||
continue;
|
||||
}
|
||||
if (result != null) {
|
||||
if (result == Object.class) {
|
||||
// Type fallback is Object
|
||||
fallback = Object.class;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
try {
|
||||
MethodInvoker invoker = new MethodInvoker();
|
||||
invoker.setTargetObject(catalog);
|
||||
invoker.setTargetMethod(method);
|
||||
invoker.setArguments(arg);
|
||||
invoker.prepare();
|
||||
Object result = invoker.invoke();
|
||||
if (result != null) {
|
||||
if (result instanceof Collection) {
|
||||
for (Object value : (Collection<?>) result) {
|
||||
results.add(prefix + value);
|
||||
}
|
||||
}
|
||||
else if (result instanceof String) {
|
||||
return prefix + result;
|
||||
}
|
||||
|
||||
else {
|
||||
return result;
|
||||
}
|
||||
if (result instanceof Boolean && !((Boolean) result)) {
|
||||
// Boolean fallback is false
|
||||
fallback = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new IllegalStateException("Cannot extract catalog", e);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
if (fallback != null) {
|
||||
return fallback;
|
||||
}
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Results: " + results);
|
||||
}
|
||||
return arg.length > 0 ? null : results;
|
||||
}
|
||||
|
||||
private Object invoke(String id, Class<?> type, String method, Object... arg) {
|
||||
Object catalog = this.deployer.getBean(id, type);
|
||||
if (catalog == null) {
|
||||
return null;
|
||||
}
|
||||
String name = this.ids.get(id);
|
||||
String prefix = name + "/";
|
||||
if (arg.length == 1) {
|
||||
if (arg[0] instanceof String) {
|
||||
String specific = arg[0].toString();
|
||||
if (specific.startsWith(prefix)) {
|
||||
arg[0] = specific.substring(prefix.length());
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
MethodInvoker invoker = new MethodInvoker();
|
||||
invoker.setTargetObject(catalog);
|
||||
invoker.setTargetMethod(method);
|
||||
invoker.setArguments(arg);
|
||||
invoker.prepare();
|
||||
Object result = invoker.invoke();
|
||||
if (result != null) {
|
||||
if (result instanceof Collection) {
|
||||
Set<String> results = new LinkedHashSet<>();
|
||||
for (Object value : (Collection<?>) result) {
|
||||
results.add(prefix + value);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
else if (result instanceof String) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Prefixed (from \" + name + \"): " + result);
|
||||
}
|
||||
return prefix + result;
|
||||
}
|
||||
|
||||
else {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Result (from " + name + "): " + result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new IllegalStateException("Cannot extract catalog", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Map<String, Object> deployed() {
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
for (String name : this.names.keySet()) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
exclusions.spring-web-reactive: org.springframework:spring-web-reactive
|
||||
exclusions.spring-cloud-function-web: org.springframework.cloud:spring-cloud-function-web
|
||||
exclusions.reator-netty: io.projectreactor.ipc:reactor-netty
|
||||
exclusions.spring-cloud-stream: org.springframework.cloud:spring-cloud-stream
|
||||
exclusions.spring-cloud-stream-reactive: org.springframework.cloud:spring-cloud-stream-reactive
|
||||
exclusions.spring-cloud-stream-binder-servlet: org.springframework.cloud:spring-cloud-stream-binder-servlet
|
||||
exclusions.spring-cloud-stream-binder-rabbit: org.springframework.cloud:spring-cloud-stream-binder-rabbit
|
||||
exclusions.spring-cloud-stream-binder-kafka: org.springframework.cloud:spring-cloud-stream-binder-kafka
|
||||
exclusions.spring-boot-starter-web: org.springframework.boot:spring-boot-starter-web
|
||||
|
||||
@@ -27,8 +27,9 @@ import org.junit.runners.Suite.SuiteClasses;
|
||||
* @author Dave Syer
|
||||
*/
|
||||
@RunWith(Suite.class)
|
||||
@SuiteClasses({ FunctionExtractingFunctionCatalogIntegrationTests.class,
|
||||
FunctionExtractingFunctionCatalogTests.class })
|
||||
@SuiteClasses({ FunctionAppDeployerTests.class,
|
||||
FunctionExtractingFunctionCatalogTests.class,
|
||||
FunctionExtractingFunctionCatalogIntegrationTests.class })
|
||||
@Ignore
|
||||
public class AdhocTestSuite {
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@ public class FunctionAppDeployerTests {
|
||||
@Test
|
||||
public void stream() throws Exception {
|
||||
String first = deploy("maven://io.spring.sample:function-sample:1.0.0.BUILD-SNAPSHOT",
|
||||
"spring.cloud.deployer.thin.profile=stream",
|
||||
"spring.cloud.deployer.thin.profile=rabbit",
|
||||
"--spring.cloud.function.stream.supplier.enabled=false", "--debug=true");
|
||||
// Deployment is blocking so it either failed or succeeded.
|
||||
assertThat(deployer.status(first).getState()).isEqualTo(DeploymentState.deployed);
|
||||
|
||||
@@ -43,9 +43,9 @@ public class FunctionExtractingFunctionCatalogIntegrationTests {
|
||||
public static void open() throws Exception {
|
||||
port = SocketUtils.findAvailableTcpPort();
|
||||
// System.setProperty("debug", "true");
|
||||
context = new ApplicationRunner().start("--server.port=" + port,
|
||||
"--spring.cloud.stream.enabled=false");
|
||||
deploy("sample", "maven://io.spring.sample:function-sample:1.0.0.BUILD-SNAPSHOT");
|
||||
context = new ApplicationRunner().start("--server.port=" + port, "--debug",
|
||||
"--logging.level.org.springframework.cloud.function=DEBUG");
|
||||
deploy("sample", "maven://com.example:function-sample-pojo:1.0.0.BUILD-SNAPSHOT");
|
||||
}
|
||||
|
||||
private static void deploy(String name, String path) throws Exception {
|
||||
@@ -79,23 +79,31 @@ public class FunctionExtractingFunctionCatalogIntegrationTests {
|
||||
|
||||
@Test
|
||||
public void words() {
|
||||
assertThat(new TestRestTemplate()
|
||||
.getForObject("http://localhost:" + port + "/sample/words", String.class))
|
||||
assertThat(new TestRestTemplate().getForObject(
|
||||
"http://localhost:" + port + "/stream/sample/words", String.class))
|
||||
.isEqualTo("[{\"value\":\"foo\"},{\"value\":\"bar\"}]");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missing() throws Exception {
|
||||
ResponseEntity<String> result = new TestRestTemplate().exchange(RequestEntity
|
||||
.get(new URI("http://localhost:" + port + "/stream/missing/words"))
|
||||
.build(), String.class);
|
||||
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void uppercase() {
|
||||
assertThat(new TestRestTemplate().postForObject(
|
||||
"http://localhost:" + port + "/sample/uppercase", "{\"value\":\"foo\"}",
|
||||
String.class)).isEqualTo("[{\"value\":\"FOO\"}]");
|
||||
"http://localhost:" + port + "/stream/sample/uppercase",
|
||||
"{\"value\":\"foo\"}", String.class)).isEqualTo("{\"value\":\"FOO\"}");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void another() throws Exception {
|
||||
deploy("strings", "maven://io.spring.sample:function-sample:1.0.0.BUILD-SNAPSHOT");
|
||||
assertThat(new TestRestTemplate().getForObject(
|
||||
"http://localhost:" + port + "/strings/words", String.class))
|
||||
"http://localhost:" + port + "/stream/strings/words", String.class))
|
||||
.isEqualTo("[\"foo\",\"bar\"]");
|
||||
}
|
||||
|
||||
@@ -106,13 +114,13 @@ public class FunctionExtractingFunctionCatalogIntegrationTests {
|
||||
assertThat(undeploy.contains(
|
||||
"\"path\":\"maven://io.spring.sample:function-sample-pojo:1.0.0.BUILD-SNAPSHOT\""));
|
||||
ResponseEntity<String> result = new TestRestTemplate().exchange(RequestEntity
|
||||
.get(new URI("http://localhost:" + port + "/sample/words")).build(),
|
||||
String.class);
|
||||
.get(new URI("http://localhost:" + port + "/stream/sample/words"))
|
||||
.build(), String.class);
|
||||
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
||||
deploy("sample", "maven://io.spring.sample:function-sample-pojo:1.0.0.BUILD-SNAPSHOT");
|
||||
assertThat(new TestRestTemplate().postForObject(
|
||||
"http://localhost:" + port + "/sample/uppercase", "{\"value\":\"foo\"}",
|
||||
String.class)).isEqualTo("[{\"value\":\"FOO\"}]");
|
||||
"http://localhost:" + port + "/stream/sample/uppercase",
|
||||
"{\"value\":\"foo\"}", String.class)).isEqualTo("{\"value\":\"FOO\"}");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user