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: 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¶m2=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?
EDIT: Some new info on this.
1 - If running through Eclipse, it works as expected:
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:

Opened an issue on Spring cloud function github: https://github.com/spring-cloud/spring-cloud-function/issues/1125

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.
In the end, i opted to use Quarkus.