spring-cloud / spring-cloud-function

Apache License 2.0
1.03k stars 615 forks source link

Spring Cloud function - MultiValueMap Request - Jetty [mvn function:run] fails - Tomcat [mvn test] OK #1125

Open PMG-VascoSaavedra opened 6 months ago

PMG-VascoSaavedra commented 6 months ago

I have a simple Spring cloud function, which was developed according to the guidelines provided by Spring Cloud documentation.

Step 1: Add the spring-cloud-function-adapter-gcp dependency:

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-function-adapter-gcp</artifactId>
    </dependency>
</dependencies>

Step 2: Add the spring-boot-maven-plugin which will build the JAR of the function to deploy

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <outputDirectory>target/deploy</outputDirectory>
    </configuration>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-function-adapter-gcp</artifactId>
        </dependency>
    </dependencies>
</plugin>

Step 3: Add the Maven plugin provided as part of the Google Functions Framework for Java. This allows to test locally via mvn function:run.

<plugin>
    <groupId>com.google.cloud.functions</groupId>
    <artifactId>function-maven-plugin</artifactId>
    <version>0.9.1</version>
    <configuration>
        <functionTarget>org.springframework.cloud.function.adapter.gcp.GcfJarLauncher</functionTarget>
        <port>8080</port>
    </configuration>
</plugin>

Step 4: The Spring Cloud Function Code

@SpringBootApplication
public class CloudFunctionMain {

    private static final Logger log = LoggerFactory.getLogger(CloudFunctionMain.class);

    public static void main(String[] args) {
    SpringApplication.run(CloudFunctionMain.class, args);
    }

    @Bean
    public Function<MultiValueMap<String, Object>, ResponseEntity<Object>> function() {
    return this::handleNotify;
    }

    private ResponseEntity<Object> handleNotify(final MultiValueMap<String, Object> request) {

    for (final String key : request.keySet()) {
        log.info("Key: " + key + " Value: " + request.getFirst(key));
    }
    return new ResponseEntity<>(null, new HttpHeaders(), HttpStatus.OK);
    }
}

Step 5: Create Unit test for startup

@SpringBootTest(classes = CloudFunctionMain.class, webEnvironment = WebEnvironment.RANDOM_PORT)
public class CloudFunctionMainTest {

    @Autowired
    private TestRestTemplate restTemplate;
    private final URI functionUri = URI.create("/function");

    @Test
    void testStartUp() throws URISyntaxException {

    MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();

    // Adding values
    map.add("key1", "value1");
    map.add("key2", "value2");

    final ResponseEntity<Object> result = restTemplate.exchange(RequestEntity.post(functionUri).body(map),
        Object.class);

    assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
    }
}

Step 6: Execute unit test:

»» mvn clean test
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running CloudFunctionMainTest
 :: Spring Boot ::                (v3.2.3)

2024-03-20T21:13:03.478Z  INFO  --- [           main] c.p.c.email.tests.CloudFunctionMainTest  : Starting CloudFunctionMainTest using Java 22 with PID 
2024-03-20T21:13:05.106Z  INFO  --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 53070 (http) with context path ''
2024-03-20T21:13:05.106Z  INFO  --- [           main] c.p.c.email.tests.CloudFunctionMainTest  : Started CloudFunctionMainTest in 2.129 seconds (process running for 3.465)
2024-03-20T21:13:05.891Z  INFO  --- [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2024-03-20T21:13:05.891Z  INFO  --- [o-auto-1-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 0 ms
2024-03-20T21:13:05.982Z  INFO  --- [o-auto-1-exec-1] c.p.cloud.email.CloudFunctionMain        : Key: key1 Value: value1
2024-03-20T21:13:05.982Z  INFO  --- [o-auto-1-exec-1] c.p.cloud.email.CloudFunctionMain        : Key: key2 Value: value2
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.858 s -- in CloudFunctionMainTest

Step 7: Run the function locally

»» mvn clean function:run
[INFO] Calling Invoker with [--classpath,
21:24:26.419 [main] INFO org.springframework.cloud.function.adapter.gcp.FunctionInvoker -- Initializing: class CloudFunctionMain
======> SOURCE: class CloudFunctionMain
2024-03-20T21:24:26.781Z  INFO 32664 --- [           main] c.g.c.functions.invoker.runner.Invoker   : Starting Invoker using Java 22 with PID [INFO] jetty-9.4.51.v20230217; built: 2023-02-17T08:19:37.309Z; git: b45c405e4544384de066f814ed42ae3dceacdd49; jvm 22+36-2370
[INFO] Started o.e.j.s.ServletContextHandler@2842ef02{/,null,AVAILABLE}
[INFO] Started ServerConnector@6b2aafbc{HTTP/1.1, (http/1.1)}{0.0.0.0:8080}
[INFO] Started @4452ms
2024-03-20T21:24:27.577Z  INFO 32664 --- [           main] c.g.c.functions.invoker.runner.Invoker   : Serving function...
2024-03-20T21:24:27.577Z  INFO 32664 --- [           main] c.g.c.functions.invoker.runner.Invoker   : Function: org.springframework.cloud.function.adapter.gcp.GcfJarLauncher
2024-03-20T21:24:27.577Z  INFO 32664 --- [           main] c.g.c.functions.invoker.runner.Invoker   : URL: http://localhost:8080/

Step 8: Use CURL to send a x-www-form-urlencoded Request:

curl -i -X POST http://localhost:8080/ -H "Content-Type: application/x-www-form-urlencoded" -d "param1=value1&param2=value2"

Which generates the following error:

2024-03-20T21:28:59.107Z ERROR 32664 --- [qtp343722304-76] com.google.cloud.functions.invoker       : Failed to execute org.springframework.cloud.function.adapter.gcp.GcfJarLauncher

java.lang.ClassCastException: class org.eclipse.jetty.server.Request$1 cannot be cast to class org.springframework.util.MultiValueMap (org.eclipse.jetty.server.Request$1 is in unnamed module of loader org.codehaus.plexus.classworlds.realm.ClassRealm @374c40ba; org.springframework.util.MultiValueMap is in unnamed module of loader com.google.cloud.functions.invoker.runner.Invoker$FunctionClassLoader @4f2ac7e0)
        at org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry$FunctionInvocationWrapper.invokeFunctionAndEnrichResultIfNecessary(SimpleFunctionRegistry.java:958) ~[spring-cloud-function-context-4.1.1-SNAPSHOT.jar:4.1.1-SNAPSHOT]
        at org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry$FunctionInvocationWrapper.invokeFunction(SimpleFunctionRegistry.java:904) ~[spring-cloud-function-context-4.1.1-SNAPSHOT.jar:4.1.1-SNAPSHOT]
        at org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry$FunctionInvocationWrapper.doApply(SimpleFunctionRegistry.java:740) ~[spring-cloud-function-context-4.1.1-SNAPSHOT.jar:4.1.1-SNAPSHOT]
        at org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry$FunctionInvocationWrapper.apply(SimpleFunctionRegistry.java:580) ~[spring-cloud-function-context-4.1.1-SNAPSHOT.jar:4.1.1-SNAPSHOT]
        at org.springframework.cloud.function.adapter.gcp.FunctionInvoker.service(FunctionInvoker.java:120) ~[spring-cloud-function-adapter-gcp-4.1.1-SNAPSHOT.jar:4.1.1-SNAPSHOT]
        at org.springframework.cloud.function.adapter.gcp.GcfJarLauncher.service(GcfJarLauncher.java:53) ~[spring-cloud-function-adapter-gcp-4.1.1-SNAPSHOT.jar:4.1.1-SNAPSHOT]
        at com.google.cloud.functions.invoker.HttpFunctionExecutor.service(HttpFunctionExecutor.java:68) ~[na:na]
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:790) ~[java-function-invoker-1.3.0.jar:na]
        at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:799) ~[na:na]
        at org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:554) ~[na:na]
        at org.eclipse.jetty.server.handler.ScopedHandler.nextHandle(ScopedHandler.java:233) ~[na:na]
        at org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java:1440) ~[na:na]
        at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:188) ~[na:na]
        at org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:505) ~[na:na]
        at org.eclipse.jetty.server.handler.ScopedHandler.nextScope(ScopedHandler.java:186) ~[na:na]
        at org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:1355) ~[na:na]
        at org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:141) ~[na:na]
        at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127) ~[na:na]
        at com.google.cloud.functions.invoker.runner.Invoker$NotFoundHandler.handle(Invoker.java:474) ~[na:na]
        at org.eclipse.jetty.server.handler.HandlerWrapper.handle(HandlerWrapper.java:127) ~[na:na]
        at org.eclipse.jetty.server.Server.handle(Server.java:516) ~[na:na]
        at org.eclipse.jetty.server.HttpChannel.lambda$handle$1(HttpChannel.java:487) ~[na:na]
        at org.eclipse.jetty.server.HttpChannel.dispatch(HttpChannel.java:732) ~[na:na]
        at org.eclipse.jetty.server.HttpChannel.handle(HttpChannel.java:479) ~[na:na]
        at org.eclipse.jetty.server.HttpConnection.onFillable(HttpConnection.java:277) ~[na:na]
        at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:311) ~[na:na]
        at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:105) ~[na:na]
        at org.eclipse.jetty.io.ChannelEndPoint$1.run(ChannelEndPoint.java:104) ~[na:na]
        at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:883) ~[na:na]
        at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1034) ~[na:na]
        at java.base/java.lang.Thread.run(Thread.java:1570) ~[na:na]

Question: It seems that the error is due to the Tests using Tomcat, and when the function runs, it uses Jetty via spring-cloud-function-adapter-gcp.

Why does this happen, and what can i do to overcome this error?

The project can be downloaded here: cloud-function.zip

PMG-VascoSaavedra commented 6 months ago

1 - If running through Eclipse, it works as expected:

image

2 - I had to downgrade to spring-boot 3.1.6 due to this issue: https://github.com/spring-cloud/spring-cloud-function/issues/1085

After 1) and 2), i still get the same error when the Cloud Function is deployed in GCP: image

PMG-VascoSaavedra commented 5 months ago

I was able to sort this out, with the help of a friend.

Basically, i would have to change the signature to receive an Object, and then cast to a BufferedReader. Afterwards, i would have to read from it line by line, parse the lines and create a Key/Value Map.

I tested this and it worked.

@SpringBootApplication
public class CloudFunctionMain {

    private static final Logger log = LoggerFactory.getLogger(CloudFunctionMain.class);

    public static void main(String[] args) {
        SpringApplication.run(CloudFunctionMain.class, args);
    }

    @Bean
    public Function<Object, ResponseEntity<Object>> function() {
    return this::handleNotify;
    }

    private ResponseEntity<Object> handleNotify(final Object values) {

        BufferedReader request = ((BufferedReader) values);

        //Read BufferedReader, parse the lines and add values to a Map.

        return new ResponseEntity<>(null, new HttpHeaders(), HttpStatus.OK);
    }
}

In the end, i opted to use Quarkus.