A little programming allows you to take complete control of your Java environment.
In my previous article, I explained the complexities of the Java environment, especially as it relates to interaction with ILE languages. I introduced the three major components of a Java environment—the Java version, the classpath, and the runtime properties—and explained how to manage them non-programmatically through environment variables and properties files. In this article, I'll provide a program that does all of that, and in so doing I'll also show you a couple of cool techniques that can be used in other situations.
Quick Recap of Java Environments
Remember that the first component of the Java environment is the Java version that you're running. That ties to the actual binary code for the Java Virtual Machine (JVM) and is determined by the JAVA_HOME environment variable, which contains an absolute path to the JVM. You have a default JVM, which depends on the version of the operating system you're running. For my original case, I was running on IBM i 7.3, which defaults to IT4J 8.0 32 bit, but I needed the 64-bit version, and that requirement led to this project.
The next part of a Java environment is the classpath. The classpath acts as the library list for Java and contains a list of locations where prewritten Java code exists. This value is set using the CLASSPATH environment variable.
So far, we've only used environment variables, and if that was all it took, we wouldn't need a dedicated program. However, it's never that easy, so now it's time to address the last piece of the environment, the runtime properties. These properties are usually set on the command line by the script that invokes the JVM, but since we're calling this from ILE RPG, we instead make use of the JNI_CreateJavaVM procedure. The trick here is that the procedure expects ASCII values (more on that in a moment).
Let's Get to the Code!
That's really all the introduction we need, so without further ado, let's see the code. This will be broken into four primary sections: the data definitions, the mainline, and the three subprocedures. It's really that simple.
ctl-opt actgrp(*new) option(*srcstmt:*nodebugio);
dcl-c C_JDK_80_64 '/QOpenSys/QIBM/ProdData/JavaVM/jdk80/64bit';
dcl-ds initArgs likeds(JavaVMInitArgs);
dcl-ds options likeds(JavaVMOption) occurs(5);
dcl-s jvm like(JavaVM_p);
dcl-s env like(JNIEnv_P) inz(*null);
dcl-s nVMs int(10);
dcl-pr system int(10) extproc('system');
command pointer value options(*string);
iFunc char(10) const;
oRC int(10) options(*nopass);
iJVM char(10) const options(*nopass);
iOpt1 char(50) const options(*nopass);
iOpt2 char(50) const options(*nopass);
iOpt3 char(50) const options(*nopass);
iOpt4 char(50) const options(*nopass);
iOpt5 char(50) const options(*nopass);
This part of the code is very simple. It starts with my normal control specification, in which I create a new activation group for the program. Next are JNI-specific definitions. We haven't really had a chance to touch on that acronym: JNI stands for Java Native Interface and is the industry standard way for non-Java programs to access the Java environment. The IBM i uses this interface to access Java directly from ILE. To keep it brief, this section of code basically sets up a JVM startup parameter that allows up to five runtime properties but that could be expanded easily.
Next, I have the prototype for the system function, which I use to execute IBM i commands, and finally the prototype for this program itself. Let me take a quick review of this:
- iFunc char(10) - Key to the CLSPTH file
- oRC int(10) - Return code (0 is success)
- iJVM char(10) - Override Java version (*64)
- iOpt1-5 char(50) - Additional JVM startup parameters
Only iFunc is required; that's the key to the CLSPTH file and identifies the Java function we're performing (which would in turn determine which pieces of Java code we'd want to include in our classpath). You'd call the program with only one parameter if you're happy with the system defaults for Java (which are usually fine for 99 percent of the cases) and just want to define the CLASSPATH. This syntax doesn't even start the JVM; you'll do that when you invoke Java.
If you specify any other parameters, then the JVM is started, and the result code is returned in the oRC parameter. Any additional functionality depends on the parameters you pass. The next parameter allows you to change the Java version. This program only has one override, which is selected by specifying either *64 or *64BIT as the third parameter. Specifying anything else (including blank) just uses the default JVM.
Finally, you have up to five additional 50-character parameters. Any additional parameters specified are plugged into the startup parameter structure before the JVM is invoked. The tricky part about this last piece is that the parameters must be converted to ASCII before being passed to the JNI_CreateJavaVM procedure.
// Always set environment variable CLASSPATH
// This will be used by default JVM startup
// If JVM parameters are passed, use them
if %parms > 1;
// See if a VM is already running; if so, exit with error
if (JNI_GetCreatedJavaVMs(jvm : 1 : nVMs) = 0) and (nVMs > 0);
oRC = -99;
*inlr = *on;
// Initialize JVM properties
// Ignore any initial settings and overwrite
initArgs = *allx'00';
initArgs.version = JNI_VERSION_1_2;
// First, check for a JVM ID
if %parms > 2;
// If selecting 64-bit, set JAVA_HOME to reflect it
if iJVM = '*64' or iJVM = '*64BIT';
setEnvVar( 'JAVA_HOME': C_JDK_80_64);
// Now process optional parameters
if %parms > 3;
if %parms > 4;
if %parms > 5;
if %parms > 6;
if %parms > 7;
// Set the address of options (have to occur to first element
%occur(options) = 1;
initArgs.options = %addr(options);
// Start the VM and return the result
oRC = JNI_CreateJavaVM (jvm : env : %addr(initArgs));
*inlr = *on;
The only thing that will always be done is the classpath will be set. This is done by a call to the setClasspath procedure, which will follow shortly. If only one parameter is passed, then the program ends here. Otherwise, the next step is to get ready to start the JVM. The first part of that task is to see if there already is a JVM running, since the IBM i architecture limits you to one JVM in a job. Once a JVM is started for a job, you can never start another one. A quick check determines whether a JVM is running and returns an error if so.
The first task is to select the Java version. We do this only if the default JVM won't work. To do it, we have to put a long, rather cumbersome folder path into the JAVA_HOME environment variable. In this program, there is only one alternate JVM, and it is selected by passing in *64 or *64BIT as the second parameter. Finally, we add each additional parameter that is passed using the addOption procedure.
Once all the setup work is complete, we update the initArgs structure and call JNI_CreateJavaVM. We return the result.
Adding an Option
iOption varchar(50) const;
dcl-s wOptionAscii varchar(50) ccsid(819);
dcl-s wOptionHex varchar(50) ccsid(*hex);
// Increment option count
initArgs.noptions += 1;
// Change occur, clear option and allocate string
%occur(options) = initArgs.noptions;
options = *allx'00';
options.optionString = %alloc(%len(iOption) + 1);
// Convert to ASCII, then hex (so %str won't convert back)
wOptionAscii = iOption;
wOptionHex = wOptionAscii;
// Copy hex and null-terminate it for Java
%str(options.optionString: %len(iOption) + 1) = wOptionHex;
This is actually one of my favorite parts of the program. I've written many different versions of EBCDIC-to-ASCII conversions (and vice versa), but the time that the compiler team put into string definition really pays off here. Watch how easy this is. First, we pass in an option; this comes in as an EBCDIC varying character string. I have to move it to the next entry in the array. The first part of the procedure changes the occurrence of the data structure. Yes, I probably could have used a data structure array instead of a multi-occurrence data structure, but this came from the original IBM example code and I didn't feel like straying too far from it. Note that I also have to allocate the space for the variable. Again, that's just the way the code was written, but it makes sense when you're talking about an arbitrary number of arbitrary-length strings.
Anyway, it's the next part that is so very cool. By defining the variable wOptionASCII with the keyword CCSID(819), the simple act of setting wOptionASCII equal to iOption automatically converts it from EBCDIC to ASCII! Then I simply use %str to make a zero-terminated copy in the new variable I allocated, and I'm done! Oh, you probably noticed a second move from wOptionASCII to wOptionHex. That's because %str likes to convert things itself. The first time I did this code without the extra move, the value that ended up in options.optionString was actually in EBDIC; %str saw that wOptionASCII was CCSID(819) and (not so) helpfully converted it back to my job CCSID.
Setting the CLASSPATH
// Aggregate CLSPTH records into path and set CLASSPATH
dcl-s wPath varchar(2000);
exec sql set :wPath =
(select listagg(trim(CPPATH), ':') within group(order by CPSEQ)
from CLSPTH where CPFUNC = :iFunc);
SetEnvVar( 'CLASSPATH': wPath);
And while the auto-conversion in addOption is indeed the coolest thing in the program, this procedure is also pretty nifty. The file CLSPTH is simple: it has a function (CPFUNC) that is the key, a sequence (CPSEQ), and a 50-character path variable (CPPATH). To format that into the CLASSPATH variable, you have to concatenate all of them with colons in between each entry. This is typically done in a loop in RPG using concatenation, being careful to not include a colon either at the beginning or end of the aggregated string. Well, since we do that about a million times in our programming careers, the fine folks in the SQL world added a way to do that, called LISTAGG.
Without going into detail (you can find the IBM documentation here), the syntax used in the SQL statement above aggregates all the CPPATH values for the specified CPPATH key, in order by the CPSEQ variable, delimited by colons and stuffs that entire value into the wPath variable.
Setting an Environment Variable
iVar varchar(10) const;
iVal varchar(2000) const;
system('RMVENVVAR ' + iVar + ' LEVEL(*JOB)');
system('ADDENVVAR ' + iVar + ' VALUE(''' + iVal + ''') LEVEL(*J
The last procedure is very simple. It just removes and then sets an environment variable. It’s hardcoded to do this at the *JOB level (as opposed to the *SYSTEM level) and since we're using the system function, any errors are ignored. It's not very robust, but it works.
And That’s All!
That's it for the entire program. You can copy this source code into a SQLRPGLE member and away you go! Let me know if you encounter any issues, but this should give you complete control over your environment. Just to give you an example, this is how I used it in my actual application code.
SYCLSPTH( '*EXCEL': wRC: '*64BIT': '-Xmx4G': '-Xms1G');
The value *EXCEL is the key to the CLSPTH file, and it selects all of the POI JAR files and all of their prerequisites, along with JT400 and a couple of miscellaneous pieces. I needed a 4G heap, so I not only specified that, but since a 4G heap also requires a 64-bit JVM, I overrode that as well.