简体   繁体   中英

Java `InvocationTargetException` with class instantiation by reflection

The problem I'm experiencing is thus: I have a series of classes that are collected via annotations. They all reside in the same folder, and if they have the particular annotation, they are instantiated via the Reflections library . While these classes are being instantiated, there is a static initializer that calls a static factory, which builds some structure. Java will throw the InvocationTargetException error while trying to obtain the factory created object. More specifically when I output the stacktrace for the ITE it points directly to the static initializer that asks the factory for the object.

Below is the code I use to replicate the issue.

I have an annotation: InferenceRule.java

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface funRule {
    String ruleName();
    String ruleType();
    String analyze() default "node";
}

I then apply that annotation to some number of classes in the package inference.rules :

@InferenceRule(ruleName = "assign", ruleType = "term")
public class Assign extends NodeAnalyzer {
    public Assign() {super();}
    public Assign(String... args) { super(args); }
    public Rule gatherAllCOnstraints(InstructionNode node) {
        // use the Identifier object here.
    }
    // rest of class here
}

The NodeAnalyzer class, the super of the Assign class above:

public abstract class NodeAnalyzer {
    protected Identifier identifier;

    protected NodeAnalyzer() {
        // Construct things here
    }

    protected NodeAnalyzer(String... args) {
        // Construct other things here
    }

    // Construct common things here
    {
        this.identifier = IdentifierFactory.getIdentifier();
    }
    // rest of class here
}

The Assign class is instantiated in the Inference class, as described below:

public class Inference {
    public final String NODE_ANALYSIS = "NODE";
    public static final String INFERENCE_PACKAGE = "inference.rules";
    private final Map<String, NodeAnalyzer> nodeAnalyzer = new HashMap<>();
    private final Map<String, EdgeAnalyzer> edgeAnalyzer = new HashMap<>();
    public Inference() {

    }
    // other non-interesting things here

    private void loadRules() {
        Reflections reflection = new Reflections(INFERENCE_PACKAGE);
        Set<Class<?>> annotated = reflection.getTypesAnnotatedWith(InferenceRule.class);

        for(Class<?> clazz : annotated) {
            try {
                String name = clazz.getAnnotation(InferenceRule.class).ruleName();
                String type = clazz.getAnnotation(InferenceRule.class).ruleType();
                String analyze = clazz.getAnnotation(InferenceRule.class).analyze();
                if (StringUtils.equalsIgnoreCase(analyze, NODE_ANALYSIS)) {
                    final NodeAnalyzer newInstance = (NodeAnalyzer) clazz.getConstructor(InferenceType.class).newInstance(InferenceType.valueOf(type));
                    this.nodeAnalyzer.put(name, newInstance);
                }
                // handle other cases...
            } catch(InvocationTargetException ite) {
                // For debugging, only
                ite.printStackTrace();
                logger.error(ite.getCause.getMessage());
                logger.error(ite.getTargetException.getMessage());
            }
        }
    }
}

As you can see, from the instantiation path in Assign and NodeAnalyzer , it must call the IdentifierFactory class:

public class IdentifierFactory {
    private static final Identifier identifier;
    static {
        if (ConfigFactory.getConfig().isDebEnabled()) {
            identifier = new DBIdentifier();
        } else {
            identifier = new NaiveIdentifier();
        }
    }

    public static Identifier getIdentifier() {
        return identifier;
    }
}

The NaiveIdentifier class:

public class NaiveIdentifier {
    private Set<Integer> unknowns = new HashSet<Integer>() {{
        unknowns.add(0);
        // add more here.
    };

    public NaiveIdentifier() {} // empty default constructor
}

The ConfigFactory class follows a similar pattern as the IdentifierFactory class. It builds a config based on certain input.

The exact exception thrown looks like:

java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at phases.inference.Inference.loadRules(Inference.java:197)
    at phases.inference.Inference.<init>(Inference.java:76)
    at phases.PhaseFacade$PHASES.getPhase(PhaseFacade.java:27)
    at phases.PhaseFacade.<init>(PhaseFacade.java:42)
    at compilation.Compiler.runPhases(Compiler.java:126)
    at compilation.Compiler.runAllOps(Compiler.java:118)
    at Main.main(Main.java:45)
Caused by: java.lang.ExceptionInInitializerError
    at phases.inference.rules.NodeAnalyzer.<init>(NodeAnalyzer.java:35)
    at phases.inference.rules.Assign.<init>(Assign.java:22)
    ... 11 more
Caused by: java.lang.NullPointerException
    at typesystem.identification.NaiveIdentifier$1.<init>(NaiveIdentifier.java:23)
    at typesystem.identification.NaiveIdentifier.<init>(NaiveIdentifier.java:22)
    at typesystem.identification.IdentifierFactory.<clinit>(IdentifierFactory.java:25)
    ... 13 more

and:

java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at phases.inference.Inference.loadRules(Inference.java:197)
    at phases.inference.Inference.<init>(Inference.java:76)
    at phases.PhaseFacade$PHASES.getPhase(PhaseFacade.java:27)
    at phases.PhaseFacade.<init>(PhaseFacade.java:42)
    at compilation.Compiler.runPhases(Compiler.java:126)
    at compilation.Compiler.runAllOps(Compiler.java:118)
    at Main.main(Main.java:45)
Caused by: java.lang.NoClassDefFoundError: Could not initialize class typesystem.identification.IdentifierFactory
    at phases.inference.rules.NodeAnalyzer.<init>(NodeAnalyzer.java:35)
    at phases.inference.rules.Assign.<init>(Assign.java:18)
    ... 11 more

From these, I cannot adequately discern what the root cause is. To further complicate this, I have tried to run this using other input files and it works just fine on those.

This code

public class NaiveIdentifier {
    private Set<Integer> unknowns = new HashSet<Integer>() {{
        unknowns.add(0);
        // add more here.
    }}; // ( <- added missing brace here)

    public NaiveIdentifier() {} // empty default constructor
}

is using the “Double Curly Brace Initialization” anti-pattern. Usually, this anti-pattern is used to save some typing in source code:

public class NaiveIdentifier {
    private Set<Integer> unknowns = new HashSet<Integer>() {{
        // yeah, we saved writing the nine characters "unknowns."
        add(0);
        // add more here.
    }};

    public NaiveIdentifier() {} // empty default constructor
}

at the expense of creating a new subclass of the collection class and potentially creating memory leaks as inner classes hold references to their outer class instance, as discussed in this Q&A .

As an ironic twist, you have not omitted the characters unknowns. , thus not only not taken any advantage of this anti-pattern, but created this bug, as you are accessing the field supposed to be initialized with the constructed set instance from within the set's constructor. In other words, your code is equivalent to the following code:

public class NaiveIdentifier {
    private Set<Integer> unknowns;
    {
      Set<Integer> temp = new HashSet<Integer>() {{
        unknowns.add(0);
        // add more here.
      }};
      unknowns = temp;
    }

    public NaiveIdentifier() {} // empty default constructor
}

which makes it clear, why this code fails with a NullPointerException .

You could fix this by using the anti-pattern consistently, ie remove the unknowns. characters to change the outer instance field access to a superclass invocation (like in the second code example above), however, now that the characters are there, you could easily change the code to use a clean initializer without the anti-pattern:

public class NaiveIdentifier {
    private Set<Integer> unknowns = new HashSet<Integer>();
    {
        unknowns.add(0);
        // add more here.
    }

    public NaiveIdentifier() {} // empty default constructor
}

When using single curly braces, you are not creating an inner class subclass of HashSet , but just defining an initializer that will be added to the constructor of NaiveIdentifier , executed in the expected program text order, first, the initializer unknowns = new HashSet<Integer>() , then the unknowns.add(…); statements.

For simple initialization statements, you may consider the alternative

public class NaiveIdentifier {
    private Set<Integer> unknowns = new HashSet<>(Arrays.asList(0, 1, 2, 3 …));

    public NaiveIdentifier() {} // empty default constructor
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM