Search code examples
javaclassloader

Java How Initialize Already Loaded Class


I am trying to develop a dynamic Factory Class, where Factory Class does not know the factories, I will put the code so that the question is clearer.

App.java:

package br.com.factory;

public class App {
    public static void main(String[] args) {
        Product product = ProductFactory.createProduct("Xiaomi", "MI XX");
        if (product != null) {
            System.out.println(product.getName());
        }
    }
}

Product.java:

package br.com.factory;

public interface Product {
   public String getName();   
}

ProductFactory.java:

package br.com.factory;

import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;

public class ProductFactory {
   
   private static HashMap<String, Map<String, Supplier<Product>>> registries = new HashMap<>();

   private ProductFactory(){}

   static {
      ClassLoaderInitializer.initialize(ProductSupplier.class);
   }

   public static Product createProduct(String manufacturer, String model) {
      Map<String, Supplier<Product>> manufacturers = registries.getOrDefault(manufacturer, null);
      if (manufacturers != null){
         Supplier<Product> suppliers = manufacturers.getOrDefault(model, null);
         if (suppliers != null) {
            return suppliers.get();
         }
      }
      return null;
   }

   public static void registerFactory(String manufacturer, String model, Supplier<Product> supplier) {
      registries
         .computeIfAbsent(manufacturer, p -> new HashMap<>())
         .putIfAbsent(model, supplier);
   }

}

ProductSupplier.java:

package br.com.factory;

import java.util.function.Supplier;

public interface ProductSupplier extends Supplier<Product> {

}

XiaomiFactory.java:

package br.com.factory.xiaomi;

import br.com.factory.Product;
import br.com.factory.ProductFactory;
import br.com.factory.ProductSupplier;

public class XiaomiFactory implements ProductSupplier {

   static {
      ProductFactory.registerFactory("Xiaomi", "MI XX", XiamoMiXX::new);
   }

   private XiaomiFactory() {
   }

   @Override
   public Product get() {
      return new XiamoMiXX();
   }

}

class XiamoMiXX implements Product {
   @Override
   public String getName() {
      return "Xiaomi Mi XX";
   }
}

ClassLoaderInitializer.java:

package br.com.factory;

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Enumeration;

public class ClassLoaderInitializer {

   private ClassLoaderInitializer() {
   }

   public static void initialize(Class<?> parentClass) {
      try {
         Enumeration<URL> resources = ClassLoaderInitializer.class.getClassLoader().getResources("");
         while (resources.hasMoreElements()) {
            URL nextElement = resources.nextElement();           

            try (URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { nextElement });) {               
               URL[] urLs = urlClassLoader.getURLs();

               for (URL u : urLs) {
                  try {
                     File file = new File(u.toURI());
                     initializeClass(file, file, urlClassLoader, parentClass);
                  } catch (URISyntaxException e) {
                     e.printStackTrace();
                  }
               }
            }
         }
      } catch (IOException e) {
         e.printStackTrace();
      }
   }

   private static void initializeClass(File root, File file, ClassLoader loader, Class<?> parentClass) {
      if (file.isDirectory()) {
         File[] listFiles = file.listFiles();
         for (File f : listFiles) {
            initializeClass(root, f, loader, parentClass);
         }
      } else {
         if (file.getName().toUpperCase().endsWith(".class".toUpperCase())) {
            try {
               String fileName = file.toString();
               String className = fileName.substring(root.toString().length() + 1,
                     fileName.toUpperCase().lastIndexOf(".class".toUpperCase())).replace(File.separator, ".");

               Class<?> clazz = Class.forName(className, false, loader);

               if (clazz.isAssignableFrom(parentClass)) {
                  Class.forName(className, true, loader);
               }

            } catch (ClassNotFoundException e) {
               e.printStackTrace();
            }
         }
      }
   }
}

The problem occurs in the initializeClass method,

more precisely in:

Class <?> Clazz = Class.forName (className, false, loader);

if (clazz.isAssignableFrom (parentClass)) {
   Class.forName (className, true, loader);
}

the idea of ​​the ClassLoaderInitializer class is to initialize the classes that inherit from a given class "Class <?> parentClass"

but when I call the method

Class.forName (className, true, loader); 

for the second time, passing true in the second parameter, the class is not initialized.

if I call:

Class.forName (className, true, loader); 

directly, the initializeClass method will initialize all classes, what I would not like to happen.

is there any outside of me explicitly initializing (forcing) the class specifies?


Solution

  • Use ServiceLoader

    ServiceLoader allows you to register services using metadata in the META-INF/services/ folder. It allows you to use a Java API to register all your services at once. It's better than trying to do it yourself, especially since it's standardized and doesn't require "magic" to get registered. Magic which might be broken in Java 9 and later with the introduction of modules.

    This is how you should use it:

    ProductSupplier.java

    public interface ProductSupplier extends Supplier<Product> {
      // Add these methods
      String getManufacturer();
      String getModel();
    }
    

    ProductFactory.java

    public class ProductFactory {
      private static final Map<List<String>, ProductSupplier> SUPPLIERS;
      static {
        var suppliers = new HashMap<List<String>, ProductSupplier>();
        for (var supplier : ServiceLoader.load(ProductSupplier.class)) { // Yes, it's as easy as this.
          var key = List.of(supplier.getManufacturer(), supplier.getModel());
          suppliers.put(key, supplier);
        }
        SUPPLIERS = Map.copyOf(suppliers);
      }
    
      public static Product createProduct(String manufacturer, String model) {
          var key = List.of(manufacturer, model);
          var supplier = suppliers.getOrDefault(key, () -> null);
          return supplier.get();
       }
    }
    

    XiaomiFactory.java

    public class XiaomiFactory implements ProductSupplier {
      @Override public String getManufacturer() { return "Xiaomi"; }
      @Override public String getModel() { return "Mi XX"; }
      @Override public Product get() { return new XiaomiMiXX(); }
    }
    

    In META-INF/services/com.br.factory.ProductSupplier:

    com.br.factory.xiaomi.XiaomiFactory
    com.br.factory.samsung.SamsungFactory # Need to create
    com.br.factory.apple.AppleFactory     # Need to create