Migrate to servlet binder for web features

This commit is contained in:
Dave Syer
2017-08-08 08:27:04 +01:00
parent 540b4d378e
commit 1af0d451cf
107 changed files with 4055 additions and 2010 deletions

View File

@@ -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>

View File

@@ -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 {
}
}

View File

@@ -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;

View File

@@ -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()) {

View File

@@ -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

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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\"}");
}
}