Skip to content

Commit f17c223

Browse files
committed
Restructure the way new-style functions are loaded.
This shares more code between the HTTP and Background flavours, and improves the logic for guessing the flavour when it is not specified. It also gives a better exception when the named target class does not exist. Also replace some uses of deprecated methods in one of the test functions. PiperOrigin-RevId: 293924171
1 parent b627f28 commit f17c223

File tree

5 files changed

+117
-101
lines changed

5 files changed

+117
-101
lines changed

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

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -53,34 +53,25 @@ private NewBackgroundFunctionExecutor(FunctionExecutor<?> functionExecutor) {
5353
}
5454

5555
/**
56-
* Make a {@link NewBackgroundFunctionExecutor} for the class named by the given {@code target}.
57-
* If the class cannot be loaded, we currently assume that this is an old-style function
58-
* (specified as package.Class.method instead of package.Class) and return
59-
* {@code Optional.empty()}.
56+
* Makes a {@link NewHttpFunctionExecutor} for the given class.
6057
*
61-
* @throws RuntimeException if we succeed in loading the class named by {@code target} but then
62-
* either the class does not implement {@link RawBackgroundFunction} or we are unable to
63-
* construct an instance using its no-arg constructor.
58+
* @throws RuntimeException if either the class does not implement one of
59+
* {@link BackgroundFunction} or {@link RawBackgroundFunction},
60+
* or we are unable to construct an instance using its no-arg constructor.
6461
*/
65-
public static Optional<NewBackgroundFunctionExecutor> forTarget(
66-
String target, ClassLoader loader) {
67-
Class<?> c;
68-
try {
69-
c = loader.loadClass(target);
70-
} catch (ClassNotFoundException e) {
71-
return Optional.empty();
72-
}
73-
if (!BackgroundFunction.class.isAssignableFrom(c)
74-
&& !RawBackgroundFunction.class.isAssignableFrom(c)) {
62+
public static NewBackgroundFunctionExecutor forClass(Class<?> functionClass) {
63+
if (!BackgroundFunction.class.isAssignableFrom(functionClass)
64+
&& !RawBackgroundFunction.class.isAssignableFrom(functionClass)) {
7565
throw new RuntimeException(
76-
"Class " + c.getName() + " implements neither " + BackgroundFunction.class
66+
"Class " + functionClass.getName() + " implements neither " + BackgroundFunction.class
7767
.getName() + " nor " + RawBackgroundFunction.class.getName());
7868
}
7969
Object instance;
8070
try {
81-
instance = c.getConstructor().newInstance();
71+
instance = functionClass.getConstructor().newInstance();
8272
} catch (ReflectiveOperationException e) {
83-
throw new RuntimeException("Could not construct an instance of " + target + ": " + e, e);
73+
throw new RuntimeException(
74+
"Could not construct an instance of " + functionClass.getName() + ": " + e, e);
8475
}
8576
FunctionExecutor<?> executor;
8677
if (instance instanceof RawBackgroundFunction) {
@@ -99,7 +90,7 @@ public static Optional<NewBackgroundFunctionExecutor> forTarget(
9990
}
10091
executor = new TypedFunctionExecutor<>(maybeTargetType.get(), backgroundFunction);
10192
}
102-
return Optional.of(new NewBackgroundFunctionExecutor(executor));
93+
return new NewBackgroundFunctionExecutor(executor);
10394
}
10495

10596
/**

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

Lines changed: 11 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import com.google.cloud.functions.invoker.http.HttpRequestImpl;
1919
import com.google.cloud.functions.invoker.http.HttpResponseImpl;
2020
import java.io.IOException;
21-
import java.util.Optional;
2221
import java.util.logging.Level;
2322
import java.util.logging.Logger;
2423
import javax.servlet.http.HttpServlet;
@@ -36,42 +35,24 @@ private NewHttpFunctionExecutor(HttpFunction function) {
3635
}
3736

3837
/**
39-
* Make a {@link NewHttpFunctionExecutor} for the class named by the given {@code target}.
40-
* If the class cannot be loaded, we currently assume that this is an old-style function
41-
* (specified as package.Class.method instead of package.Class) and return
42-
* {@code Optional.empty()}.
38+
* Makes a {@link NewHttpFunctionExecutor} for the given class.
4339
*
44-
* @throws RuntimeException if we succeed in loading the class named by {@code target} but then
45-
* either the class does not implement {@link HttpFunction} or we are unable to construct an
46-
* instance using its no-arg constructor.
40+
* @throws RuntimeException if either the given class does not implement {@link HttpFunction}
41+
* or we are unable to construct an instance using its no-arg constructor.
4742
*/
48-
public static Optional<NewHttpFunctionExecutor> forTarget(String target, ClassLoader loader) {
49-
Class<?> c;
50-
while (true) {
51-
try {
52-
c = loader.loadClass(target);
53-
break;
54-
} catch (ClassNotFoundException e) {
55-
// This might be a nested class like com.example.Foo.Bar. That will actually appear as
56-
// com.example.Foo$Bar as far as Class.forName is concerned. So we try to replace every dot
57-
// from the last to the first with a $ in the hope of finding a class we can load.
58-
int lastDot = target.lastIndexOf('.');
59-
if (lastDot < 0) {
60-
return Optional.empty();
61-
}
62-
target = target.substring(0, lastDot) + '$' + target.substring(lastDot + 1);
63-
}
64-
}
65-
if (!HttpFunction.class.isAssignableFrom(c)) {
43+
public static NewHttpFunctionExecutor forClass(Class<?> functionClass) {
44+
if (!HttpFunction.class.isAssignableFrom(functionClass)) {
6645
throw new RuntimeException(
67-
"Class " + c.getName() + " does not implement " + HttpFunction.class.getName());
46+
"Class " + functionClass.getName() + " does not implement "
47+
+ HttpFunction.class.getName());
6848
}
69-
Class<? extends HttpFunction> httpFunctionClass = c.asSubclass(HttpFunction.class);
49+
Class<? extends HttpFunction> httpFunctionClass = functionClass.asSubclass(HttpFunction.class);
7050
try {
7151
HttpFunction httpFunction = httpFunctionClass.getConstructor().newInstance();
72-
return Optional.of(new NewHttpFunctionExecutor(httpFunction));
52+
return new NewHttpFunctionExecutor(httpFunction);
7353
} catch (ReflectiveOperationException e) {
74-
throw new RuntimeException("Could not construct an instance of " + target + ": " + e, e);
54+
throw new RuntimeException(
55+
"Could not construct an instance of " + functionClass.getName() + ": " + e, e);
7556
}
7657
}
7758

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

Lines changed: 79 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
import com.beust.jcommander.JCommander;
2020
import com.beust.jcommander.Parameter;
2121
import com.beust.jcommander.ParameterException;
22+
import com.google.cloud.functions.BackgroundFunction;
23+
import com.google.cloud.functions.HttpFunction;
24+
import com.google.cloud.functions.RawBackgroundFunction;
2225
import com.google.cloud.functions.invoker.BackgroundCloudFunction;
2326
import com.google.cloud.functions.invoker.BackgroundFunctionExecutor;
2427
import com.google.cloud.functions.invoker.BackgroundFunctionSignatureMatcher;
@@ -48,6 +51,7 @@
4851
import java.util.logging.Level;
4952
import java.util.logging.LogManager;
5053
import java.util.logging.Logger;
54+
import java.util.stream.Stream;
5155
import javax.servlet.http.HttpServlet;
5256
import org.eclipse.jetty.server.Server;
5357
import org.eclipse.jetty.servlet.ServletContextHandler;
@@ -163,12 +167,13 @@ static Optional<Invoker> makeInvoker(Map<String, String> environment, String...
163167
.stream()
164168
.filter(Objects::nonNull)
165169
.findFirst();
170+
ClassLoader functionClassLoader = makeClassLoader(functionClasspath);
166171
Invoker invoker =
167172
new Invoker(
168173
port,
169174
functionTarget,
170175
environment.get("FUNCTION_SIGNATURE_TYPE"),
171-
functionClasspath);
176+
functionClassLoader);
172177
return Optional.of(invoker);
173178
}
174179

@@ -185,20 +190,29 @@ private static boolean isLocalRun() {
185190
return System.getenv("K_SERVICE") == null;
186191
}
187192

193+
private static ClassLoader makeClassLoader(Optional<String> functionClasspath) {
194+
ClassLoader runtimeLoader = Invoker.class.getClassLoader();
195+
if (functionClasspath.isPresent()) {
196+
ClassLoader parent = new OnlyApiClassLoader(runtimeLoader);
197+
return new URLClassLoader(classpathToUrls(functionClasspath.get()), parent);
198+
}
199+
return runtimeLoader;
200+
}
201+
188202
private final Integer port;
189203
private final String functionTarget;
190204
private final String functionSignatureType;
191-
private final Optional<String> functionClasspath;
205+
private final ClassLoader functionClassLoader;
192206

193207
public Invoker(
194208
Integer port,
195209
String functionTarget,
196210
String functionSignatureType,
197-
Optional<String> functionClasspath) {
211+
ClassLoader functionClassLoader) {
198212
this.port = port;
199213
this.functionTarget = functionTarget;
200214
this.functionSignatureType = functionSignatureType;
201-
this.functionClasspath = functionClasspath;
215+
this.functionClassLoader = functionClassLoader;
202216
}
203217

204218
Integer getPort() {
@@ -213,8 +227,8 @@ String getFunctionSignatureType() {
213227
return functionSignatureType;
214228
}
215229

216-
Optional<String> getFunctionClasspath() {
217-
return functionClasspath;
230+
ClassLoader getFunctionClassLoader() {
231+
return functionClassLoader;
218232
}
219233

220234
public void startServer() throws Exception {
@@ -224,58 +238,34 @@ public void startServer() throws Exception {
224238
context.setContextPath("/");
225239
server.setHandler(context);
226240

227-
ClassLoader runtimeLoader = getClass().getClassLoader();
228-
ClassLoader classLoader;
229-
if (functionClasspath.isPresent()) {
230-
ClassLoader parent = new OnlyApiClassLoader(runtimeLoader);
231-
classLoader = new URLClassLoader(classpathToUrls(functionClasspath.get()), parent);
232-
} else {
233-
classLoader = runtimeLoader;
234-
}
241+
Optional<Class<?>> functionClass = loadFunctionClass();
235242

236243
HttpServlet servlet;
237244
if ("http".equals(functionSignatureType)) {
238-
Optional<NewHttpFunctionExecutor> newExecutor =
239-
NewHttpFunctionExecutor.forTarget(functionTarget, classLoader);
240-
if (newExecutor.isPresent()) {
241-
servlet = newExecutor.get();
245+
if (functionClass.isPresent()) {
246+
servlet = NewHttpFunctionExecutor.forClass(functionClass.get());
242247
} else {
243-
FunctionLoader<HttpCloudFunction> loader =
244-
new FunctionLoader<>(functionTarget, classLoader, new HttpFunctionSignatureMatcher());
248+
FunctionLoader<HttpCloudFunction> loader = new FunctionLoader<>(
249+
functionTarget, functionClassLoader, new HttpFunctionSignatureMatcher());
245250
HttpCloudFunction function = loader.loadUserFunction();
246251
servlet = new HttpFunctionExecutor(function);
247252
}
248253
} else if ("event".equals(functionSignatureType)) {
249-
Optional<NewBackgroundFunctionExecutor> newExecutor =
250-
NewBackgroundFunctionExecutor.forTarget(functionTarget, classLoader);
251-
if (newExecutor.isPresent()) {
252-
servlet = newExecutor.get();
254+
if (functionClass.isPresent()) {
255+
servlet = NewBackgroundFunctionExecutor.forClass(functionClass.get());
253256
} else {
254257
FunctionLoader<BackgroundCloudFunction> loader =
255258
new FunctionLoader<>(
256-
functionTarget, classLoader, new BackgroundFunctionSignatureMatcher());
259+
functionTarget, functionClassLoader, new BackgroundFunctionSignatureMatcher());
257260
BackgroundCloudFunction function = loader.loadUserFunction();
258261
servlet = new BackgroundFunctionExecutor(function);
259262
}
260263
} else if (functionSignatureType == null) {
261-
Optional<NewHttpFunctionExecutor> httpExecutor =
262-
NewHttpFunctionExecutor.forTarget(functionTarget, classLoader);
263-
if (httpExecutor.isPresent()) {
264-
servlet = httpExecutor.get();
264+
if (functionClass.isPresent()) {
265+
servlet = servletForDeducedSignatureType(functionClass.get());
265266
} else {
266-
Optional<NewBackgroundFunctionExecutor> backgroundExecutor =
267-
NewBackgroundFunctionExecutor.forTarget(functionTarget, classLoader);
268-
if (backgroundExecutor.isPresent()) {
269-
servlet = backgroundExecutor.get();
270-
} else {
271-
String error = String.format(
272-
"Could not determine function signature type from target %s. Either this should be"
273-
+ " a class implementing one of the interfaces in com.google.cloud.functions, or the"
274-
+ " environment variable FUNCTION_SIGNATURE_TYPE should be set to \"http\" or"
275-
+ " \"event\".",
276-
functionTarget);
277-
throw new RuntimeException(error);
278-
}
267+
throw new RuntimeException(
268+
"Class " + functionTarget + " does not exist or could not be loaded");
279269
}
280270
} else {
281271
String error = String.format(
@@ -290,28 +280,71 @@ public void startServer() throws Exception {
290280
server.join();
291281
}
292282

293-
static URL[] classpathToUrls(String classpath) throws IOException {
283+
private Optional<Class<?>> loadFunctionClass() {
284+
String target = functionTarget;
285+
while (true) {
286+
try {
287+
return Optional.of(functionClassLoader.loadClass(target));
288+
} catch (ClassNotFoundException e) {
289+
// This might be a nested class like com.example.Foo.Bar. That will actually appear as
290+
// com.example.Foo$Bar as far as Class.forName is concerned. So we try to replace every dot
291+
// from the last to the first with a $ in the hope of finding a class we can load.
292+
int lastDot = target.lastIndexOf('.');
293+
if (lastDot < 0) {
294+
return Optional.empty();
295+
}
296+
target = target.substring(0, lastDot) + '$' + target.substring(lastDot + 1);
297+
}
298+
}
299+
}
300+
301+
private HttpServlet servletForDeducedSignatureType(Class<?> functionClass) {
302+
if (HttpFunction.class.isAssignableFrom(functionClass)) {
303+
return NewHttpFunctionExecutor.forClass(functionClass);
304+
}
305+
if (BackgroundFunction.class.isAssignableFrom(functionClass)
306+
|| RawBackgroundFunction.class.isAssignableFrom(functionClass)) {
307+
return NewBackgroundFunctionExecutor.forClass(functionClass);
308+
}
309+
String error = String.format(
310+
"Could not determine function signature type from target %s. Either this should be"
311+
+ " a class implementing one of the interfaces in com.google.cloud.functions, or the"
312+
+ " environment variable FUNCTION_SIGNATURE_TYPE should be set to \"http\" or"
313+
+ " \"event\".",
314+
functionTarget);
315+
throw new RuntimeException(error);
316+
}
317+
318+
static URL[] classpathToUrls(String classpath) {
294319
String[] components = classpath.split(File.pathSeparator);
295320
List<URL> urls = new ArrayList<>();
296321
for (String component : components) {
297322
if (component.endsWith(File.separator + "*")) {
298323
urls.addAll(jarsIn(component.substring(0, component.length() - 2)));
299324
} else {
300325
Path path = Paths.get(component);
301-
if (Files.exists(path)) {
326+
try {
302327
urls.add(path.toUri().toURL());
328+
} catch (MalformedURLException e) {
329+
throw new UncheckedIOException(e);
303330
}
304331
}
305332
}
306333
return urls.toArray(new URL[0]);
307334
}
308335

309-
private static List<URL> jarsIn(String dir) throws IOException {
336+
private static List<URL> jarsIn(String dir) {
310337
Path path = Paths.get(dir);
311338
if (!Files.isDirectory(path)) {
312339
return Collections.emptyList();
313340
}
314-
return Files.list(path)
341+
Stream<Path> stream;
342+
try {
343+
stream = Files.list(path);
344+
} catch (IOException e) {
345+
throw new UncheckedIOException(e);
346+
}
347+
return stream
315348
.filter(p -> p.getFileName().toString().endsWith(".jar"))
316349
.map(p -> {
317350
try {

invoker/core/src/test/java/com/google/cloud/functions/invoker/runner/InvokerTest.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,20 @@
33
import static com.google.common.truth.Truth.assertThat;
44
import static com.google.common.truth.Truth.assertWithMessage;
55
import static com.google.common.truth.Truth8.assertThat;
6+
import static java.util.stream.Collectors.joining;
67

78
import java.io.ByteArrayOutputStream;
89
import java.io.File;
910
import java.io.IOException;
1011
import java.io.PrintStream;
1112
import java.net.URL;
13+
import java.net.URLClassLoader;
1214
import java.nio.charset.StandardCharsets;
1315
import java.util.Arrays;
1416
import java.util.Collections;
1517
import java.util.Map;
1618
import java.util.Optional;
19+
import java.util.stream.Collectors;
1720
import org.junit.Test;
1821
import org.junit.runner.RunWith;
1922
import org.junit.runners.JUnit4;
@@ -71,7 +74,8 @@ public void explicitSignatureType() {
7174
@Test
7275
public void defaultClasspath() {
7376
Optional<Invoker> invoker = Invoker.makeInvoker();
74-
assertThat(invoker.get().getFunctionClasspath()).isEmpty();
77+
assertThat(invoker.get().getClass().getClassLoader())
78+
.isSameInstanceAs(Invoker.class.getClassLoader());
7579
}
7680

7781
private static final String FAKE_CLASSPATH =
@@ -81,13 +85,20 @@ public void defaultClasspath() {
8185
public void explicitClasspathViaEnvironment() {
8286
Map<String, String> env = Collections.singletonMap("FUNCTION_CLASSPATH", FAKE_CLASSPATH);
8387
Optional<Invoker> invoker = Invoker.makeInvoker(env);
84-
assertThat(invoker.get().getFunctionClasspath()).hasValue(FAKE_CLASSPATH);
88+
assertThat(invokerClasspath(invoker.get())).isEqualTo(FAKE_CLASSPATH);
8589
}
8690

8791
@Test
8892
public void explicitClasspathViaOption() {
8993
Optional<Invoker> invoker = Invoker.makeInvoker("--classpath", FAKE_CLASSPATH);
90-
assertThat(invoker.get().getFunctionClasspath()).hasValue(FAKE_CLASSPATH);
94+
assertThat(invokerClasspath(invoker.get())).isEqualTo(FAKE_CLASSPATH);
95+
}
96+
97+
private static String invokerClasspath(Invoker invoker) {
98+
URLClassLoader urlClassLoader = (URLClassLoader) invoker.getFunctionClassLoader();
99+
return Arrays.stream(urlClassLoader.getURLs())
100+
.map(URL::getPath)
101+
.collect(joining(File.pathSeparator));
91102
}
92103

93104
@Test

0 commit comments

Comments
 (0)