Skip to content

Commit 809d05a

Browse files
Keep ClassNotFoundException as cause when reporting function not found. (GoogleCloudPlatform#26)
The code is complicated by the support for legacy "ClassName.method" way of specifying function targets, which we will delete when we delete java8 support from GCF. Under the assumption that the user did mean to use the new form, we remember what the ClassNotFoundException was that caused us to fail, and we include that in the cause chain of the final exception. That way the user can see a possible ExceptionInInitializerError or the like that might be the underlying reason for the failure.
1 parent 0907814 commit 809d05a

File tree

4 files changed

+78
-30
lines changed

4 files changed

+78
-30
lines changed

invoker/core/src/main/java/com/google/cloud/functions/invoker/FunctionLoader.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,17 @@ public class FunctionLoader<T extends CloudFunction> {
2222
private final String functionTarget;
2323
private final ClassLoader classLoader;
2424
private final FunctionSignatureMatcher<T> matcher;
25+
private final ClassNotFoundException newStyleException;
2526

2627
public FunctionLoader(
2728
String functionTarget,
2829
ClassLoader classLoader,
29-
FunctionSignatureMatcher<T> matcher) {
30+
FunctionSignatureMatcher<T> matcher,
31+
ClassNotFoundException newStyleException) {
3032
this.functionTarget = functionTarget;
3133
this.classLoader = classLoader;
3234
this.matcher = matcher;
35+
this.newStyleException = newStyleException;
3336
}
3437

3538
/**
@@ -40,7 +43,7 @@ public FunctionLoader(
4043
public T loadUserFunction() throws Exception {
4144
int lastDotIndex = functionTarget.lastIndexOf(".");
4245
if (lastDotIndex == -1) {
43-
throw new ClassNotFoundException(functionTarget);
46+
throw newStyleException;
4447
}
4548
String targetClassName = functionTarget.substring(0, lastDotIndex);
4649
String targetMethodName = functionTarget.substring(lastDotIndex + 1);
@@ -50,7 +53,8 @@ public T loadUserFunction() throws Exception {
5053
} catch (ClassNotFoundException e) {
5154
throw new RuntimeException(
5255
"Could not load either " + functionTarget + " (new form) or "
53-
+ targetClassName + " (old form)");
56+
+ targetClassName + " (old form)",
57+
newStyleException);
5458
}
5559

5660
Object targetInstance = targetClass.getDeclaredConstructor().newInstance();

invoker/core/src/main/java/com/google/cloud/functions/invoker/runner/Invoker.java

Lines changed: 65 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.beust.jcommander.JCommander;
2020
import com.beust.jcommander.Parameter;
2121
import com.beust.jcommander.ParameterException;
22+
import com.google.auto.value.AutoOneOf;
2223
import com.google.cloud.functions.BackgroundFunction;
2324
import com.google.cloud.functions.HttpFunction;
2425
import com.google.cloud.functions.RawBackgroundFunction;
@@ -250,34 +251,48 @@ public void startServer() throws Exception {
250251
servletContextHandler.setContextPath("/");
251252
server.setHandler(NotFoundHandler.forServlet(servletContextHandler));
252253

253-
Optional<Class<?>> functionClass = loadFunctionClass();
254+
ClassOrException functionClass = loadFunctionClass();
254255

255256
HttpServlet servlet;
256257
if ("http".equals(functionSignatureType)) {
257-
if (functionClass.isPresent()) {
258-
servlet = NewHttpFunctionExecutor.forClass(functionClass.get());
259-
} else {
260-
FunctionLoader<HttpCloudFunction> loader = new FunctionLoader<>(
261-
functionTarget, functionClassLoader, new HttpFunctionSignatureMatcher());
262-
HttpCloudFunction function = loader.loadUserFunction();
263-
servlet = new HttpFunctionExecutor(function);
258+
switch (functionClass.getKind()) {
259+
case LOADED_CLASS:
260+
servlet = NewHttpFunctionExecutor.forClass(functionClass.loadedClass());
261+
break;
262+
default:
263+
FunctionLoader<HttpCloudFunction> loader =
264+
new FunctionLoader<>(
265+
functionTarget,
266+
functionClassLoader,
267+
new HttpFunctionSignatureMatcher(),
268+
functionClass.exception());
269+
HttpCloudFunction function = loader.loadUserFunction();
270+
servlet = new HttpFunctionExecutor(function);
264271
}
265272
} else if ("event".equals(functionSignatureType)) {
266-
if (functionClass.isPresent()) {
267-
servlet = NewBackgroundFunctionExecutor.forClass(functionClass.get());
268-
} else {
269-
FunctionLoader<BackgroundCloudFunction> loader =
270-
new FunctionLoader<>(
271-
functionTarget, functionClassLoader, new BackgroundFunctionSignatureMatcher());
272-
BackgroundCloudFunction function = loader.loadUserFunction();
273-
servlet = new BackgroundFunctionExecutor(function);
273+
switch (functionClass.getKind()) {
274+
case LOADED_CLASS:
275+
servlet = NewBackgroundFunctionExecutor.forClass(functionClass.loadedClass());
276+
break;
277+
default:
278+
FunctionLoader<BackgroundCloudFunction> loader =
279+
new FunctionLoader<>(
280+
functionTarget,
281+
functionClassLoader,
282+
new BackgroundFunctionSignatureMatcher(),
283+
functionClass.exception());
284+
BackgroundCloudFunction function = loader.loadUserFunction();
285+
servlet = new BackgroundFunctionExecutor(function);
274286
}
275287
} else if (functionSignatureType == null) {
276-
if (functionClass.isPresent()) {
277-
servlet = servletForDeducedSignatureType(functionClass.get());
278-
} else {
279-
throw new RuntimeException(
280-
"Class " + functionTarget + " does not exist or could not be loaded");
288+
switch (functionClass.getKind()) {
289+
case LOADED_CLASS:
290+
servlet = servletForDeducedSignatureType(functionClass.loadedClass());
291+
break;
292+
default:
293+
throw new RuntimeException(
294+
"Class " + functionTarget + " does not exist or could not be loaded",
295+
functionClass.exception());
281296
}
282297
} else {
283298
String error = String.format(
@@ -294,18 +309,44 @@ public void startServer() throws Exception {
294309
server.join();
295310
}
296311

297-
private Optional<Class<?>> loadFunctionClass() {
312+
@AutoOneOf(ClassOrException.Kind.class)
313+
abstract static class ClassOrException {
314+
enum Kind {LOADED_CLASS, EXCEPTION}
315+
316+
abstract Kind getKind();
317+
318+
abstract Class<?> loadedClass();
319+
320+
abstract ClassNotFoundException exception();
321+
322+
static ClassOrException of(Class<?> c) {
323+
return AutoOneOf_Invoker_ClassOrException.loadedClass(c);
324+
}
325+
326+
static ClassOrException of(ClassNotFoundException e) {
327+
return AutoOneOf_Invoker_ClassOrException.exception(e);
328+
}
329+
}
330+
331+
private ClassOrException loadFunctionClass() {
298332
String target = functionTarget;
333+
ClassNotFoundException firstException = null;
299334
while (true) {
300335
try {
301-
return Optional.of(functionClassLoader.loadClass(target));
336+
return ClassOrException.of(functionClassLoader.loadClass(target));
302337
} catch (ClassNotFoundException e) {
338+
if (firstException == null) {
339+
firstException = e;
340+
}
303341
// This might be a nested class like com.example.Foo.Bar. That will actually appear as
304342
// com.example.Foo$Bar as far as Class.forName is concerned. So we try to replace every dot
305343
// from the last to the first with a $ in the hope of finding a class we can load.
306344
int lastDot = target.lastIndexOf('.');
307345
if (lastDot < 0) {
308-
return Optional.empty();
346+
// We're going to try to load the function target using the old form, classname.method.
347+
// But it's at least as likely that the class does exist, but we failed to load it for
348+
// some other reason. So ensure that we keep the exception to show to the user.
349+
return ClassOrException.of(firstException);
309350
}
310351
target = target.substring(0, lastDot) + '$' + target.substring(lastDot + 1);
311352
}

invoker/core/src/test/java/com/google/cloud/functions/invoker/BackgroundFunctionTest.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,11 @@ public void adder() throws Exception {
8484
HttpServletResponse res = Mockito.mock(HttpServletResponse.class);
8585
String fullTarget = "com.google.cloud.functions.invoker.BackgroundFunctionTest$" + target;
8686
FunctionLoader<BackgroundCloudFunction> loader =
87-
new FunctionLoader<>(fullTarget, getClass().getClassLoader(),
88-
new BackgroundFunctionSignatureMatcher());
87+
new FunctionLoader<>(
88+
fullTarget,
89+
getClass().getClassLoader(),
90+
new BackgroundFunctionSignatureMatcher(),
91+
null);
8992
BackgroundCloudFunction function = loader.loadUserFunction();
9093
BackgroundFunctionExecutor executor = new BackgroundFunctionExecutor(function);
9194
Mockito.when(req.getReader())

invoker/core/src/test/java/com/google/cloud/functions/invoker/HttpFunctionTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public void adder() throws Exception {
4545
"com.google.cloud.functions.invoker.HttpFunctionTest$HttpWriter.writeResponse";
4646
String requestData = "testData";
4747
FunctionLoader<HttpCloudFunction> loader = new FunctionLoader<>(
48-
fullTarget, getClass().getClassLoader(), new HttpFunctionSignatureMatcher());
48+
fullTarget, getClass().getClassLoader(), new HttpFunctionSignatureMatcher(), null);
4949
HttpCloudFunction function = loader.loadUserFunction();
5050
HttpFunctionExecutor executor = new HttpFunctionExecutor(function);
5151
Mockito.when(req.getParameter("data")).thenReturn(requestData);

0 commit comments

Comments
 (0)