Escape from JAR Hell
|
1 |
<strong>What is JAR Hell?</strong> |
|
1 |
<strong><span style="font-size: medium;">Solution</span></strong> |
Problem Statement
package com.myorg.pantry;
public interface CoffeeMachine {
public String prepareCoffee(String coffeeType);
}
package com.myorg.pantry;
import com.myorg.pantry.rawmaterial.CoffeeIngredientSupplier;
public class CoffeeMachineImpl implements CoffeeMachine{
@Override
public String prepareCoffee(String coffeeType) {
String coffee = null;
if(coffeeType.equalsIgnoreCase("powdered-milk-coffee")){
CoffeeIngredientSupplier ingSupplier = new CoffeeIngredientSupplier();
String milk = ingSupplier.getMilk();
ingSupplier.addCoffee();
ingSupplier.addSugar();
coffee = milk + " Coffee";
}
return coffee;
}
}
package com.myorg.pantry.rawmaterial;
public class CoffeeIngredientSupplier {
boolean isMilkAvailable = false;
/**
* Loads powdered milk and returns it to calling machine @return
*/
public String getMilk(){
if(isMilkAvailable){
loadPowderedMilk();
}
return "Powdered Milk";
}
private void loadPowderedMilk() {
System.out.println("Powdered Milk Loaded");
}
public void addSugar(){
System.out.println("Sugar Added...");
}
public void addCoffee(){
System.out.println("Coffee Added...");
}
}
package com.myorg.pantry;
import com.myorg.pantry.rawmaterial.CoffeeIngredientSupplier;
public class CoffeeMachineImpl implements CoffeeMachine{
@Override
public String prepareCoffee(String coffeeType) {
String coffee = null;
if(coffeeType.equalsIgnoreCase("powdered-milk-coffee")){
CoffeeIngredientSupplier ingSupplier = new CoffeeIngredientSupplier();
String milk = ingSupplier.getMilk();
ingSupplier.addCoffee();
ingSupplier.addSugar();
coffee = milk + " Coffee";
}
else if(coffeeType.equalsIgnoreCase("bottled-milk-coffee")){
CoffeeIngredientSupplier ingSupplier = new CoffeeIngredientSupplier();
String milk = ingSupplier.getMilk();
ingSupplier.addCoffee();
ingSupplier.addSugar();
coffee = milk + " Coffee";
}
return coffee;
}
}
Great...all set now!!! Somebody from admin team informed all employees about the latest coffee machine and some excited employees rushed to cafeteria to taste bottled milk coffee. After sorting out that who will inagorate the new coffee button, an employee got the new coffee in his mug. The moment he tasted the coffee...... the reaction was.. "Yaakkk.. it's the same old coffee".
What happened here? Management asked engineer to debug the problem and he realized he is in a Jar Hell. He expected that his code will resolve dependencies during run time as both the jars are on classpath, but he forgot that Java is not that smart. Incidently, java classloader was loading the first jar "ingredient-powdered-milk.jar" from classpath and ingSupplier.getMilk() was loading powdered milk only in both the scenarios.
Now that, he knew the problem, it was time to fix it. He wrote new SmartCoffeeMachineImpl which looked like this:
package com.myorg.pantry;
import java.lang.reflect.Method;
import com.myorg.pantry.rawmaterial.CoffeeIngredientSupplier;
import com.myorg.util.JarClassLoader;
public class SmartCoffeeMachineImpl implements CoffeeMachine {
String pathForBottledMilkIngLib;
String pathForPowderedMilkIngLib;
@Override
public String prepareCoffee(String coffeeType) {
resolveLibPaths();
String coffee = null;
if (coffeeType.equalsIgnoreCase("powdered-milk-coffee")) {
coffee = getRequiredCoffee(pathForPowderedMilkIngLib);
}
else if (coffeeType.equalsIgnoreCase("bottled-milk-coffee")) {
coffee = getRequiredCoffee(pathForBottledMilkIngLib);
}
return coffee;
}
private void resolveLibPaths() {
String classpath = System.getProperty("java.class.path");
System.out.println(classpath);
String[] libsOnClasspath = classpath.split(";");
for (String lib : libsOnClasspath) {
if (lib.endsWith("-milk.jar")) {
if (lib.indexOf("bottled") > -1) {
pathForBottledMilkIngLib = lib;
} else if (lib.indexOf("powdered") > -1) {
pathForPowderedMilkIngLib = lib;
}
}
}
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private String getRequiredCoffee(String ingPath) {
String coffee = null;
try {
JarClassLoader classLoader = new JarClassLoader(
ingPath);
Class ingSuppClass = classLoader
.findClass("com.myorg.pantry.rawmaterial.CoffeeIngredientSupplier");
String milk = null;
try {
CoffeeIngredientSupplier ingSupplier = (CoffeeIngredientSupplier) ingSuppClass
.newInstance();
milk = ingSupplier.getMilk();
ingSupplier.addCoffee();
ingSupplier.addSugar();
} catch (ClassCastException ex) {
Object ingObject = ingSuppClass.newInstance();
Method getMilkMethod = ingSuppClass.getMethod("getMilk", (Class[])null);
milk = (String) getMilkMethod.invoke(ingObject, (Object[])null);
Method addSugarMethod = ingSuppClass
.getMethod("addSugar", (Class[]) null);
addSugarMethod.invoke(ingObject, (Object[])null);
Method addCoffeeMethod = ingSuppClass.getMethod("addCoffee",
(Class[])null);
addCoffeeMethod.invoke(ingObject, (Object[])null);
}
coffee = milk + " Coffee";
} catch (Exception e) {
e.printStackTrace();
}
return coffee;
}
}
All worked well after this as employees enjoyed bottled milk coffee which they were by now desperately looking for.
What did the magic? If we closely look into the implementation of SmartCoffeeMachineImpl, we will find two new methods resolveLibPaths() and getRequiredCoffee(String ingPath) and yes, these new methods did the trick.
To be able to solve JAR Hell, first we must be able to distinguish two jars clearly. Then only we can take decision about what to use when. resolveLibPaths() analyzes classpath and finds out the path of the two required jar files.
Now that we know the path of both the jar files, we can use JarClassLoader to load the required jar. But it might be possible that the Jar which you are trying to use is not the one which got loaded by default [default java class loader]. In that case you will get a ClassCastException, as the class you are looking for is different than the class which got loaded. So we will have to use Java Reflections to instantiate and invoke methods of the CoffeeIngredientSupplier class.
One more point to emphasise on, JarClassLoader is a custom implementation which appears like this:
package com.myorg.util;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
public final class JarClassLoader extends ClassLoader {
private final JarFile file;
public JarClassLoader(String filename) throws IOException {
this.file = new JarFile(filename);
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
JarEntry entry = file.getJarEntry(name.replace('.', '/') + ".class");
if (entry == null) {
throw new ClassNotFoundException(name);
}
try {
byte[] array = new byte[1024];
InputStream in = file.getInputStream(entry);
ByteArrayOutputStream out = new ByteArrayOutputStream(array.length);
int length = in.read(array);
while (length > 0) {
out.write(array, 0, length);
length = in.read(array);
}
return defineClass(name, out.toByteArray(), 0, out.size());
}
catch (IOException exception) {
throw new ClassNotFoundException(name, exception);
}
}
}
Working code for this problem is attached with the post.
That's it.. you have mastered the art of escaping from Jar Hell. This may not confirm your place in heaven but atleast it might save you some extra time to do some spiritual work to achieve a heavenly spot [;)]
And by the way keep on enjoying your office coffees..... !!




