Lesson 3: File-based Configuration
Introduction
File-based configuration is a popular option for applications that might have to be run in more than one mode. Our configuration files are XML-based and support multiple sections of which only one is selected to be the active one. Consider the following scenario:
Your mixed-language application is happily deployed with a hard-coded Java configuration that works. Then
a user reports a crash that leaves a Java stacktrace behind. You want to run the application with the
-Xcheck:jni
option to make sure that all JNI calls are done properly (you don't do that for production
use because it has a performance impact) and you want to debug into the Java parts of the
application. But for that you need to set several JVM options to allow your Java debugger to attach to the JVM
that's been loaded into your native process. How can you do that?
With a file-based configuration that's really easy. You simply put two configuration sections into your configuration file, for example named "Production" and "Debug". Under normal circumstances, the application runs with the "Production" settings but when you need to do some debugging you simply switch to the "Debug" configuration that contains your debugger settings.
The configuration file elements are a bit awkward because we were constrained by elements supported by the early .NET configuration files (we wanted the configuration files to be usable by JuggerNET and JunC++ion applications.) Nevertheless, the files are easy to understand. Here's an example from the file-based configuration example bundled with the code generator:
<Codemesh> <Runtime> <!-- you can have many different configurations in your config file; They just have to end with the string ".JvmSettings", otherwise they can have any valid XML name you choose. This element specifies which one you wish to use by the name prefix. --> <Loader name="Production"/> <!-- a JVM configuration options for production use; there are many more, but most can be specified via corresponding -D or -X settings --> <Production.JvmSettings> <add key="Type" value="Codemesh.JuggerNET.SunJava2JvmLoader" /> <add key="ClassPath" value="../.." /> <add key="InitialHeapSize" value="64" /> <add key="CheckJNI" value="false" /> <add key="TraceLevel" value="TraceWarnings" /> </Production.JvmSettings> <!-- the -D options (system properties) for Production use --> <Production.Options> <add key="FOO" value="!!! foo !!!" /> <add key="BAR" value="!!! bar !!!" /> <add key="BAZ" value="!!! baz !!!" /> </Production.Options> <!-- the -X options for Production use --> <Production.XOptions> <add key="mx" value="256m" /> </Production.XOptions> <!-- a JVM configuration for debug use --> <Debug.JvmSettings> <add key="Type" value="Codemesh.JuggerNET.SunJava2JvmLoader" /> <add key="ClassPath" value="../.." /> <add key="InitialHeapSize" value="16" /> <add key="CheckJNI" value="true" /> <add key="TraceLevel" value="TraceInfo" /> </Debug.JvmSettings> <!-- the -D options (system properties) for Debug use --> <Debug.Options> <add key="FOO" value="!!! foo-debug !!!" /> <add key="BAR" value="!!! bar-debug !!!" /> <add key="BAZ" value="!!! baz-debug !!!" /> </Debug.Options> <!-- the -X options for Debug use --> <Debug.XOptions> <add key="mx" value="16m" /> </Debug.XOptions> </Runtime> </Codemesh>
To use this file to configure the application, you have three options:
- You can use the static
JvmLoader.SetConfigFile()
method before you callGetJvmLoader()
, or - You can use the
JvmLoader.GetJvmLoader()
factory method that takes a filename as its first argument, or - You can use
IJvmLoader.Read()
one or more times after you have created aJvmLoader
. This will aggregate the settings in the referenced configuration files into the configured effective settings.
The first method is particularly useful if you want to leave the creation of JvmLoader
instance to the application while setting the configuration file in a configuration hook inside a library.
Often you will wish to use a configuration file that is co-located with your binary executable. A common pattern for such use cases is to use an environment variable to define the application's home directory and then derive all paths, including the configuration file path, from that directory. The following code snippet illustrates this pattern.
Process p = Process.GetCurrentProcess(); string dir = new FileInfo(p.MainModule.FileName).DirectoryName; string config = dir + @"\config.xml"; if( new FileInfo(config).Exists ) { IJvmLoader loader = JvmLoader.GetJvmLoader( config ); ... // optionally make configuration API calls that override/check the config file loader.JvmPath = ... ; loader.DashDOption[ ... ] = ...; } else { // log the error and quit ... } ... try { IJvm jvm = loader.Load(); } catch( System.Exception e ) { ... }
We repeat the following section from the explicit lesson because it is important and applies to all configuration approaches.
When to Configure
As you can see in the above code snippet, we explicitly try to load the JVM after the configuration is complete. While this is not strictly necessary (the JDBC Courier runtime supports on-demand loading when Java is required) it is highly recommended. Doing it this way gives you one and only one point at which the JVM initialization could fail, making the debugging of configuration issues much easier.
You want to make sure that the configuration is complete before you make your first Java call or the JDBC Courier runtime will simply use its default settings to load a JVM when asked to do something in Java. Most JVM settings are
"frozen" in place once the JVM has been loaded and cannot be modified after the fact. Therefore, particularly
if you have a large codebase, it is important to perform the configuration as early as possible. We recommend that you do it
right at the beginning of your
program's Main()
.
Example
You can find an example in the code generator distribution at examples/dotnet/v3/configuration/filebased
.
The readme.html
file in that directory contains a description and the instructions on how to build and
run the example.