Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
PMG-VascoSaavedra opened this issue Mar 20, 2024 · 2 comments

Comments

@PMG-VascoSaavedra
Copy link

PMG-VascoSaavedra commented Mar 20, 2024

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
Copy link
Author

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:
#1085

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

@PMG-VascoSaavedra
Copy link
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant