google / google/guice

Invoking listeners although there were errors during field injection leads to misleading stacktrace

Open
#990 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Java
Stars
12.7k
Forks
1.7k
Avg merge
11m
Merged PRs (30d)
2

Description

I am currently facing a problem, which gave me headaches to find the source of it.

Here's the story:

I am using a jsr250 implementation (as attachment). In the following (simplified) scenario
a misleading error message is printed as the @PostConstruct listener is called
although one of the injections of the class have thrown a NullPointerException
during creation.

Here's the code:

```
public class BrokenCar {

@Inject
private BrokenEngine engine;

@PostConstruct
public void afterPropertiesSet() {
/** Will throw a NullPointerException. */
engine.check();
}

}
```

```
public class BrokenEngine {

public BrokenEngine() {
throw new NullPointerException();
}

public void check() {
}

}
```

```
@Test
public void testBrokenCar() {
injector = Guice.createInjector(new Jsr250Module());
injector.getInstance(BrokenCar.class);
}
```

Running the test generates the output:

```
com.google.inject.ProvisionException: Unable to provision, see the following errors:

1) Error calling method afterPropertiesSet annotated with @PostConstruct.
while locating de.jaculon.jsr250.cars.BrokenCar
Caused by: java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
...
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
Caused by: java.lang.NullPointerException
at de.jaculon.jsr250.cars.BrokenCar.afterPropertiesSet(BrokenCar.java:15)
... 38 more

2) Error injecting constructor, java.lang.NullPointerException
at de.jaculon.jsr250.cars.BrokenEngine.(BrokenEngine.java:5)
while locating de.jaculon.jsr250.cars.BrokenEngine
for field at de.jaculon.jsr250.cars.BrokenCar.engine(BrokenCar.java:7)
while locating de.jaculon.jsr250.cars.BrokenCar
Caused by: java.lang.NullPointerException
at de.jaculon.jsr250.cars.BrokenEngine.(BrokenEngine.java:6)
...
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)

2 errors
at com.google.inject.internal.InjectorImpl$2.get(InjectorImpl.java:1025)
...
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)

```

As you can see in the output the afterPropertiesSet() method is called although there was an
error in that class.

I would suggest to not invoking the listeners in this case. Then the stacktrace would be
smaller and the NullPointerException easier to recognize. Note that this is only a simplified
example, i had another real life case where finding this kind of bug was much harder.

Here's the jsr250 @Postconstruct code:

```
package de.jaculon.jsr250;

import static com.google.inject.matcher.Matchers.any;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import javax.annotation.PostConstruct;

import com.google.inject.AbstractModule;
import com.google.inject.ProvisionException;
import com.google.inject.TypeLiteral;
import com.google.inject.spi.InjectionListener;
import com.google.inject.spi.TypeEncounter;
import com.google.inject.spi.TypeListener;

public class Jsr250Module extends AbstractModule {

private String postconstruct;

@Override
protected void configure() {
postconstruct = PostConstruct.class.getName();

binder().bindListener(any(), new TypeListener() {
@Override
public void hear(TypeLiteral injectableType,
TypeEncounter encounter) {
cleanHear(injectableType, encounter);
}
});
}

private void cleanHear(TypeLiteral injectableType,
TypeEncounter encounter) throws SecurityException {
Class rawType = injectableType.getRawType();

Method[] methods = rawType.getDeclaredMethods();
for (final Method method : methods) {
boolean annotatedWithPostConstruct = isAnnotatedWithPostConstruct(
method);
if (!annotatedWithPostConstruct) {
continue;
}

registerListener(encounter, method);
}
}

private void registerListener(TypeEncounter encounter,
final Method method) {
encounter.register(new InjectionListener() {

@Override
public void afterInjection(I injectee) {
try {
method.setAccessible(true);
method.invoke(injectee);
} catch (Exception e) {
String msg = String.format(
"Error calling method %s annotated with @PostConstruct.",
method.getName());
throw new ProvisionException(msg, e);
}
}
});
}

private boolean isAnnotatedWithPostConstruct(Method method) {

Annotation[] annotations = method.getAnnotations();
for (Annotation annotation : annotations) {
Class annotationType = annotation.annotationType();
String name = annotationType.getName();
if (name.equals(postconstruct)) {
return true;
}
}

/**
* Note: If you use:
*
* method.getAnnotation(PostConstruct.class);
*
* then it will return null if PostConstruct is referenced by two
* different jars in the classpath (e.g from rt.jar since java 1.7 and
* javax.annotation_1.2.0.jar). The solution is to use the fully
* qualified name as string like above.
*/

return false;
}

}

```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.