Conditional Compilation

Conditional compilation is a technique in which lines of code are not compiled into the class file based on a particular condition. This can be used to remove tons of debugging code in a production build. To understand the power of conditional compilation, consider Example 2-22, which demonstrates a method that does a complex transaction and logs it using Log4J.

Example 2-22. A method with traces
package oracle.hcj.finalstory;
import org.apache.log4j.Logger;
public class ConditionalCompile {
 private static final Logger LOGGER = Logger.getLogger(ConditionalCompile.class);
 public static void someMethod( ) {
 // Do some set up code. LOGGER.debug("Set up complete, beginning phases.");
 // do first part. LOGGER.debug("phase1 complete");
 // do second part. LOGGER.debug("phase2 complete");
 // do third part. LOGGER.debug("phase3 complete");
 // do finalization part. LOGGER.debug("phase4 complete");
 // Operation Completed
 LOGGER.debug("All phases completed successfully");
 }
}


If you assume that there is a lot of code in each phase of the method, the logging shown in this example could be essential to finding business logic errors. However, when you deploy this app in a production environment, you have to go back through the code and eliminate all the logging, or this method will run like a three-legged dog in quicksand. Even if Log4j is set to a higher error level, every logging statement requires a call to another method, a lookup in a configuration table, and so on.

Screenshot

Leaving extensive logging in your program is just not a viable option. I remember going to a new company and working on some code written by biologists. I fired up the GUI, which was one of those "typical slow Java GUIs," and immediately noticed something odd. In my console window, there was so much stuff being written that the word "spam" hardly does it justice. In just initializing the app, the program wrote in the neighborhood of 6,000 lines of tracing information. "No wonder this GUI is slow," I thought. Writing out traces is an extremely CPU-expensive activity, and you should avoid it in a production system whenever possible.


To reduce the overhead, you could try to turn off the logging using a variable:

package oracle.hcj.finalstory;
import org.apache.log4j.Logger;
public class ConditionalCompile {
 private static final Logger LOGGER = Logger.getLogger(ConditionalCompile.class);
 private static boolean doLogging = false;
 public static void someMethodBetter( ) {
 // Do some set up code. if (doLogging) {
 LOGGER.debug("Set up complete, beginning phases.");
 }
 // do first part. if (doLogging) {
 LOGGER.debug("phase1 complete");
 }
 // do second part.  if (doLogging) {
 LOGGER.debug("phase2 complete");
 }
 // do third part. if (doLogging) {
 LOGGER.debug("phase3 complete");
 }
 // do finalization part. if (doLogging) {
 LOGGER.debug("phase4 complete");
 }
 // Operation Completed
 if (doLogging) {
 LOGGER.debug("All phases completed successfully");
 }
 }
}


In this code, you used the doLogging variable to shut off the logging information at runtime. Using if statements such as this improves your runtime performance and allows you to turn the logging on and off whenever you want. When doLogging is true, you get all of the logging along with the performance hit. When doLogging is false, you get no logging, and somewhat less of a performance hit. This may be a good idea in some cases. However, if your logging is printing out extensive information, this technique will still slow things down. When the variable is false, instead of printing out the debugging information, the CPU performs one comparison each time it hits a logging statement.

Screenshot

When using the isDebugEnabled( ) method in Log4J, the cost is much more than one comparison per if statement.


Also, you can't afford to forget that in a method run thousands of times, a few comparisons per method can add up quickly. You need the ability to compile conditionally, depending on your build paradigm. In development mode, you want all of the debugging code. In deployment mode, you don't. C++ refugees will undoubtedly recognize this ability in the #ifdef directive. Although Java doesn't have this directive, the final keyword can be used to accomplish similar results:

package oracle.hcj.finalstory;
import org.apache.log4j.Logger;
public class ConditionalCompile {
 private static final Logger LOGGER = Logger.getLogger(ConditionalCompile.class);
 private final static boolean doLogging = false;
 public static void someMethodBetter( ) {
 // Do some set up code. if (doLogging) {
 LOGGER.debug("Set up complete, beginning phases.");
 }
 // do first part. if (doLogging) {
 LOGGER.debug("phase1 complete");
 }
 // do second part. if (doLogging) {
 LOGGER.debug("phase2 complete");
 }
 // do third part. if (doLogging) {
 LOGGER.debug("phase3 complete");
 }
 // do finalization part. if (doLogging) {
 LOGGER.debug("phase4 complete");
 }
 // Operation Completed
 if (doLogging) {
 LOGGER.debug("All phases completed successfully");
 }
 }
}


By converting the doLogging attribute into a final attribute, you have told the compiler that whenever it sees doLogging, it should replace it with false as per the compile-time substitution rules from earlier in the chapter. The first pass of the compiler changes the code to something like this:

package oracle.hcj.finalstory;
import org.apache.log4j.Logger;
public class ConditionalCompile {
 private static final Logger LOGGER = Logger.getLogger(ConditionalCompile.class);
 private static boolean doLogging = false;
 public static void someMethodBetter( ) {
 // Do some set up code.  if (false) {
 LOGGER.debug("Set up complete, beginning phases.");
 }
 // do first part.  if (false) {
 LOGGER.debug("phase1 complete");
 }
 // do second part.  if (false) {
 LOGGER.debug("phase2 complete");
 }
 // do third part.  if (false) {
 LOGGER.debug("phase3 complete");
 }
 // do finalization part.  if (false) {
 LOGGER.debug("phase4 complete");
 }
 // Operation Completed
 if (false) {
 LOGGER.debug("All phases completed successfully");
 }
 }
}


Once this is done, the compiler takes another look at it and sees that there are unreachable statements in the code. Since you are working with a top-quality compiler, it doesn't like all those unreachable byte codes. So it removes them, and you end up with this:

package oracle.hcj.finalstory;
import org.apache.log4j.Logger;
public class ConditionalCompile {
 private static final Logger LOGGER = Logger.getLogger(ConditionalCompile.class);
 private static boolean doLogging = false;
 public static void someMethodBetter( ) {
 // Do some set up code. // do first part. // do second part. // do third part. // do finalization part. // Operation Completed
 }
}


This is perfect! By setting the value of the doLogging to true or false and recompiling the class, you can turn on and off inclusion of code in your program. When you build for development, you alter the code to set doLogging to true. When you build a production release, set it to false. You now have the best of both worlds.

Screenshot

Remember that if you change a primitive final static logging variable, you'll have to change the class that contains that variable and recompile all the classes that reference it.


Conditional Compilation Variable Location

When implementing conditional compilation, the question of where to put these compilation variables always comes up. There are many potential solutions to the problem, but you need to watch out for a couple of pitfalls. First of all, putting a variable in the top-level package of your product is probably not a good idea. The problem is that there may be other classes in this package as well. Since all classes will be referencing this package, you could accidentally create circular package dependencies.

Screenshot

Circular dependencies arise when Package A depends on Package B, which in turn depends on Package A. Circular dependencies make it very difficult to separate code into components, and circular references make code fragile by allowing bugs to migrate across the dependencies into other packages.


Your best bet is to create a new package in each of your major products. If you are an employee of a Sun, for example, each major product would be defined by the package directly under com.sun. I like to create a package named _development under the major packages of my clients' products. The leading underscore helps me remember that this package is not part of the product base but is instead a container for things such as these variables. Inside this new package, place a class named DevelopmentMode. In this class, install one variable for each package in your product. Then you can simply import the class and access the appropriate variable. For the Hardcore Java example code, the class would look something like the following.

package oracle.hcj._development;
public final class DevelopmentMode {
 /** Development mode constant for package oracle.hcj.bankdata. */
 public static final boolean hcj_bankdata = true;
 /** Development mode constant for package oracle.hcj.bankdata. */
 public static final boolean hcj_collections = true;
 /** Development mode constant for package oracle.hcj.bankdata. */
 public static final boolean hcj_constants = true;
 /** Development mode constant for package oracle.hcj.bankdata. */
 public static final boolean hcj_datamodeling = true;
 /** Development mode constant for package oracle.hcj.bankdata. */
 public static final boolean hcj_exceptions = true;
 /** Development mode constant for package oracle.hcj.bankdata. */
 public static final boolean hcj_finalstory = true;
 /** Development mode constant for package oracle.hcj.bankdata. */
 public static final boolean hcj_immutable = true;
 /** Development mode constant for package oracle.hcj.bankdata. */
 public static final boolean hcj_nested = true;
 /** Development mode constant for package oracle.hcj.bankdata. */
 public static final boolean hcj_proxies = true;
 /** Development mode constant for package oracle.hcj.bankdata. */
 public static final boolean hcj_references = true;
 /** Development mode constant for package oracle.hcj.bankdata. */
 public static final boolean hcj_review = true;
 private DevelopmentMode( ) {
 assert false: "DevelopmentMode is a Singleton.";
 }
}


In this class, you don't specify the oracle package because it would be a bit redundant, since all the code you would be writing for your company would be in that package. However, the variable naming here is a matter of taste. The important fact is that all packages can be turned from development to deployment mode in one place. Using the constant is easy:

package oracle.hcj.finalstory;
import oracle.hcj._development.DevelopmentMode;
public class ConditionalCompile {
 public void projectVariables( ) {
 if (DevelopmentMode.hcj_finalstory) {
 // . . . do conditional code.
 }
 }
}


You merely use the constant in the same way it was used in the last section. This allows you to take advantage of conditional compilation in a maintainable manner. However, be careful not to place any malfunctioning code into the _development package (or whatever you named your package), or you may create circular package dependencies. The _development package shouldn't depend on anything other than third-party libraries and the JDK itself.

      
Comments