Compile Time VS Run Time Dependency - Java

When is a time to use Run-time over Compile-time dependencies in Java/Gradle?

A typical case involves dynamically creating classes via reflection. As a contrived example, consider this app:

package net.codetojoy;

public class App {
public static void main(String[] args) throws Exception {
Class c = Class.forName("org.apache.commons.lang3.StringUtils");
Object object = c.getConstructor().newInstance();
System.out.println("object is : " + object);
}
}

It will create an object of StringUtils from Apache Commons Lang. (This example is silly; consider a case where libA will effectively do this for classes in libB).

There is no compile-time dependency so there is no reason to burden the compile-time classpath with the jar. However at run-time, the jar is certainly required. The build.gradle file is below. It uses the application plugin which nicely bundles the dependencies into a runnable deliverable.

apply plugin: 'java'
apply plugin: 'application'

repositories {
jcenter()
}

dependencies {
runtime group: 'org.apache.commons', name: 'commons-lang3', version: '3.6'
}

mainClassName = 'net.codetojoy.App'

Example output:

$ gradle run -q
object is : org.apache.commons.lang3.StringUtils@4aa298b7

External jar dependcy in java, compile time vs run time

During compilation you need either the source file of the class (and compile everything together) or the .class file (compiled file) in your build path. That can be achieved without a .jar file, as long as your external .class file does not have other dependencies as well.

A jar file can be used for compile time, for runtime, or for both. It depends on what kind of dependency you have. For example, you need it during compilation, and not need it at runtime because you run your application in a managed environment (like an application server) which already has the jar (or a version of it). You needed it directly at runtime if, for example, your code does some dynamic class loading (for example java.sql.DriverManager does that when loading the SQL driver class).

And of course, the most common case, when is needed both at compile and run-time.

Run time dependency vs Compile time dependency

An example of compile vs runtime would be slf4j-api which is used on compile time and slf4j-log4j on runtime. In this example we are talking about static binding which is the fastest way to go.

Dynamic linking like commons-logging to log4j, would be your reflection, because the call is made dynamically on runtime. Worst way to go, but sometimes necessary and some times very elegant if you think about the commons-lang Equal and HashBuilder functions.

Refection gives you a neat power to call what you do not know at the time of building, but it should be confined to framework code. Some would call it the highest level of programmer's lazyness :-)

So. If you do know, what you are about to invoke, or you have a strong picture of what you want to call, use Interfaces instead. It's way faster, predictable and secure. Think about IoC.

To add to your confusion, there are also runtime dependencies, which are not met on runtime. For example the log4j context listener requires a servlet implementation. But if you are not invoking it on runtime, you must not bother. Hibernat has the dom4 dependency, but if you use annotation s only, you don't need it.
Then you can deliberately shadow classes to inject it into the package structure. This comes handy if your library what written with classes that use default visibility. But AFAIK this is static binding. Note: In a security context, this would be called the Mix-and-Match attack.

To come back to you question: use static binding whenever is is possible.

Why compile- and run-time dependency is important?

Why I should care about this?

To make your apps build but not ship with unnecessary stuff.

In gradle, why should I use compileOnly vs implementation/api?

The documentation for compileOnly gives one use case as an example:

Gradle adds the dependency to the compilation classpath only (it is not added to the build output). This is useful when you're creating an Android library module and you need the dependency during compilation, but it's optional to have present at runtime. That is, if you use this configuration, then your library module must include a runtime condition to check whether the dependency is available, and then gracefully change its behavior so it can still function if it's not provided. This helps reduce the size of the final APK by not adding transient dependencies that aren't critical. This configuration behaves just like provided (which is now deprecated).

source

For example, consider a push messaging library that supports both Firebase FCM and Amazon ADM but does not require either. It would unnecessarily bloat apps if it would ship with both as transitive dependencies. With compileOnly the library can still be built. Developers using the library can select which dependencies to actually use.

Another example could be compile-time annotations that do not need to ship with the application.

Can a program depend on a library during compilation but not runtime?

A compile-time dependency is generally required at runtime. In maven, a compile scoped dependency will be added to the classpath on runtime (e.g. in wars they will be copied to WEB-INF/lib).

It is not, however, strictly required; for instance, we may compile against a certain API, making it a compile-time dependency, but then at runtime include an implementation that also includes the API.

There may be fringe cases where the project requires a certain dependency to compile but then the corresponding code is not actually needed, but these will be rare.

On the other hand, including runtime dependencies that are not needed at compile-time is very common. For instance, if you're writing a Java EE 6 application, you compile against the Java EE 6 API, but at runtime, any Java EE container can be used; it's this container that provides the implementation.

Compile-time dependencies can be avoided by using reflection. For instance, a JDBC driver can be loaded with a Class.forName and the actual class loaded be configurable through a configuration file.

Difference between compile and runtime configurations in Gradle

In the most common case, the artifacts needed at compile time are a subset of those needed at runtime. For example, let's say that a program called app uses library foo, and library foo internally uses library bar. Then only foo is needed to compile app, but both foo and bar are needed to run it. This is why by default, everything that you put on Gradle's compile configuration is also visible on its runtime configuration, but the opposite isn't true.

Runtime vs. Compile time

The difference between compile time and run time is an example of what pointy-headed theorists call the phase distinction. It is one of the hardest concepts to learn, especially for people without much background in programming languages. To approach this problem, I find it helpful to ask

  1. What invariants does the program satisfy?
  2. What can go wrong in this phase?
  3. If the phase succeeds, what are the postconditions (what do we know)?
  4. What are the inputs and outputs, if any?

Compile time

  1. The program need not satisfy any invariants. In fact, it needn't be a well-formed program at all. You could feed this HTML to the compiler and watch it barf...
  2. What can go wrong at compile time:

    • Syntax errors
    • Typechecking errors
    • (Rarely) compiler crashes
  3. If the compiler succeeds, what do we know?

    • The program was well formed---a meaningful program in whatever language.
    • It's possible to start running the program. (The program might fail immediately, but at least we can try.)
  4. What are the inputs and outputs?

    • Input was the program being compiled, plus any header files, interfaces, libraries, or other voodoo that it needed to import in order to get compiled.
    • Output is hopefully assembly code or relocatable object code or even an executable program. Or if something goes wrong, output is a bunch of error messages.

Run time

  1. We know nothing about the program's invariants---they are whatever the programmer put in. Run-time invariants are rarely enforced by the compiler alone; it needs help from the programmer.
  2. What can go wrong are run-time errors:

    • Division by zero
    • Dereferencing a null pointer
    • Running out of memory

    Also there can be errors that are detected by the program itself:

    • Trying to open a file that isn't there
    • Trying find a web page and discovering that an alleged URL is not well formed
  3. If run-time succeeds, the program finishes (or keeps going) without crashing.
  4. Inputs and outputs are entirely up to the programmer. Files, windows on the screen, network packets, jobs sent to the printer, you name it. If the program launches missiles, that's an output, and it happens only at run time :-)


Related Topics



Leave a reply



Submit