Invoking listeners although there were errors during field injection leads to misleading stacktrace
- 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
Assessment
This issue has not been assessed yet.