I’m getting the following SOAR error when trying to implement a singleton using the static class holder pattern to get the thread safe and lazy loading.
The singleton use a logger which is also a singleton.
public final class Config {
private final Properties props = new Properties();
private Config() {
try{
// load configuration into props from a file
} catch ( Exception e) {
MyLoggerProvider.instance.error("error", e);
}
}
private static class Holder {
public static final Config instance = new Config();
}
public static Config getInstance() { return Holder.instance; }
public String getLoggerLevel() {
return this.props.getProperty("logger.level", "INFO");
}
}
public final class MyLoggerProvider{
public static final Logger instance = new SimpleLogger();
private MyLoggerProvider() {}
}
public final class SimpleLogger implements Logger{
SimpleLogger(){
String slevel = Config.getInstance().getLoggerLevel();
//...
}
}
The execution order of static initializers (also called “clinit”) is computed by SOAR from the application entry points, as described at SOAR — MicroEJ Documentation.
From you code snippet, I don’t see how MyLogger touches Config$Holder (likely you simplified the code)
To go further, I suggest to have a look to the .clinitmap generated in the soar folder
Please share the reason why Config$Holder requires MyLogger and why MyLogger requires Config$Holder.
I have updated the code snippet to show the relation between the two classes.
Actually there is an init cycle between the 2 classes:
Config is using LoggerProvider in constructor to log errors if any
LoggerProvider instantiate SimpleLogger which uses Config in constructor to get the config level
This behavior is OK on SIM, and only throws an error when the simulator option Use target characteristics is selected.
The issue is always reproductible on device.
Thanks for the .clinitmap file, it helps finding the cycle:
On Simulator, static initializers are executed on demand by default, whereas the SOAR static analysis is applied when the Use target characteristics is enabled.
However, the cycle may not be detected on Simulator if MyLoggerProvider.instance is only accessed in the catch block but not executed.
To go further:
what is the effect on Simulator if you access MyLoggerProvider.instance at the beginning of the constructor ?
private Config() {
MyLoggerProvider.instance.info("test"); // test in normal path
try{
// load configuration into props from a file
} catch ( Exception e) {
MyLoggerProvider.instance.error("error", e);
}
}
please also share the initialization code of MyLoggerProvider (especially the initialization of static field named config)
When moving the MyLoggerProvider.instance.info("test") outside of the catch without selecting the option Use target characteristics I get.
Exception in thread "main" java.lang.ExceptionInInitializerError: java.lang.NullPointerException
at java.lang.Throwable.fillInStackTrace(Throwable.java:82)
at java.lang.Throwable.<init>(Throwable.java:32)
at java.lang.Exception.<init>(Exception.java:14)
at java.lang.RuntimeException.<init>(RuntimeException.java:14)
at java.lang.NullPointerException.<init>(NullPointerException.java:6)
The class MyLoggerProvider doesn’t have a constructor, it simply create an instance of SimpleLogger inline with the field declaration