Статьи

Изучение магии внедрения зависимости с использованием аннотаций — часть 2

Это второй из короткой серии блогов, в которой я представляю реализацию внедрения зависимостей с использованием аннотаций. Если вы знакомы с Spring или EJB3, вы поймете, о чем я говорю: вы пишете урок; добавьте несколько аннотаций, например @Autowired или @Ejb; разверните его на своем веб-сервере, а затем и еще раз, все это склеено и работает, как по волшебству …

Этот блог не собирается писать полностью сформированную фабрику DI с использованием аннотаций, но даст вам несколько советов по к техникам, которые использовали ребята в Spring и команда EJB3.

Чтобы написать DI-фабрику, вам нужно выполнить несколько конкретных задач. Это включает в себя выяснение, какие классы на вашем пути к классам аннотированы, создание экземпляров этих классов и склеивание их всех вместе. Вчерашний блог продемонстрировал, как найти все файлы .class, которые находятся на вашем пути к классам и хранятся в вашей файловой системе. Сегодняшний блог делает еще один шаг вперед и исследует, как найти классы, которые хранятся в файлах JAR, расположенных в вашей файловой системе

. Сценарий, который я использую, такой же, как и вчерашний: учитывая имя пакета, найдите и отобразите все имена классов в этом пакет. Это означает, что приведенный ниже код будет довольно похож на:

public class JarFileSample {

  private static final String JAR_FILE_PACKAGE = "org.slf4j";

  private static final Pattern pattern = Pattern.compile("^" + JAR_FILE_PACKAGE + ".*");

  public static void main(String[] args) throws IOException, ClassNotFoundException {

    System.out.println("Finding all classes in jarfile package...");
    JarFileSample instance = new JarFileSample();

    List<Class<?>> result = instance.getClasses(JAR_FILE_PACKAGE);
    listResults(result);

    System.out.println("-- END --");
  }

  private List<Class<?>> getClasses(String packageName) throws ClassNotFoundException, IOException {

    String path = packageName.replace('.', '/'); // Convert the package name
    List<File> directories = getPackageDirectories(path);
    return walkJars(directories);
  }

  private List<File> getPackageDirectories(String path) throws IOException {

    List<File> files = new ArrayList<File>();
    ClassLoader classLoader = getClassLoader();
    Enumeration<URL> resources = classLoader.getResources(path);
    while (resources.hasMoreElements()) {
      URL resource = resources.nextElement();
      File file = getNextFile(resource);
      files.add(file);
    }
    return files;
  }

  private File getNextFile(URL resource) throws UnsupportedEncodingException {
    String fileNameDecoded = URLDecoder.decode(resource.getFile(), "UTF-8");
    fileNameDecoded = fileNameDecoded.substring(fileNameDecoded.indexOf(":") + 1, fileNameDecoded.indexOf("!"));
    return new File(fileNameDecoded);
  }

  private ClassLoader getClassLoader() {
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    assert classLoader != null;
    return classLoader;
  }

  private List<Class<?>> walkJars(List<File> directories) throws IOException, ClassNotFoundException {

    List<Class<?>> classes = new ArrayList<Class<?>>();
    for (File directory : directories) {
      classes.addAll(walkJar(directory));
    }

    return classes;
  }

  private List<Class<?>> walkJar(File directory) throws IOException, ClassNotFoundException {

    List<Class<?>> classes = new ArrayList<Class<?>>();
    JarFile jarFile = new JarFile(directory);

    Enumeration<JarEntry> jarEntries = jarFile.entries();

    while (jarEntries.hasMoreElements()) {
      JarEntry jarEntry = jarEntries.nextElement();
      addClassFromJar(jarEntry, classes);
    }

    return classes;
  }

  private void addClassFromJar(JarEntry jarEntry, List<Class<?>> classes) {
    if (isMatchingClass(jarEntry)) {
      String fileName = jarEntry.getName();
      if (isValidClassName(fileName)) {
        Class<?> clazz = createClass(fileName);
        if (isNotNull(clazz)) {
          classes.add(clazz);
        }
      }
    }
  }

  private boolean isMatchingClass(JarEntry jarEntry) {

    boolean retVal = false;
    if (!jarEntry.isDirectory()) {
      String name = jarEntry.getName();
      Matcher matcher = pattern.matcher(name);
      retVal = matcher.matches();
    }
    return retVal;
  }

  private boolean isValidClassName(String fileName) {
    return fileName.endsWith(".class") && !fileName.contains("$");
  }

  private Class<?> createClass(String fileName) {

    try {
      String className = getClassName(fileName);
      return Class.forName(className);
    } catch (Throwable e) {
      e.printStackTrace();
      return null;
    }
  }

  private String getClassName(final String fileName) {

    String retVal = fileName.substring(0, fileName.length() - 6);
    retVal = retVal.replaceAll("/", ".");

    return retVal;
  }

  private boolean isNotNull(Object obj) {
    return obj != null;
  }

  private static void listResults(List<Class<?>> clazzes) {

    int i = 1;
    for (Class<?> clazz : clazzes) {
      System.out.println(i++ + ") Found: " + clazz.getName());
    }
  }
}

Будучи образцом, я жестко закодировал название пакета. Затем он используется ClassLoader для поиска всех связанных с ним ресурсов URL. Говоря
обо всех ресурсах URL, я принимаю во внимание, что классы, принадлежащие к одному и тому же пакету, можно разделить на несколько разных файлов JAR. Например, в приведенном ниже коде я использую имя пакета начальной точки
org.slf4j , которое по своей конструкции распространяет свои классы на несколько файлов JAR.

При опросе ClassLoader мы получим старомодное перечисление, содержащее ресурсы URL с именами, которые выглядят примерно так:

file:/Users/Roger/.m2/repository/org/slf4j/slf4j-log4j12/1.6.1/slf4j-log4j12-1.6.1.jar!/org/slf4j

Очевидно, что нам нужен доступ к реальному файлу JAR, поэтому это имя должно быть исправлено так:

/Users/Roger/.m2/repository/org/slf4j/slf4j-log4j12/1.6.1/slf4j-log4j12-1.6.1.jar

Обратите внимание, что я предполагаю, что анализируемые файлы JAR находятся в той же файловой системе, что и мой код. Для наших целей это не плохое предположение; это просто то, что нужно знать.

Как только у нас будет список классов файлов JAR-файла, следующая задача — открыть их и заглянуть внутрь. JDK предоставляет вам целую кучу классов для этой цели, начиная с «
Jar ». В этом примере я использую: JarFile и JarEntry, так как все, что мне нужно, это полностью определенные имена классов. Если вы посмотрите на код, то увидите, что мне не пришлось делать ничего сложного, как вчера, когда я использовал рекурсию для обхода структуры каталогов. Это потому, что классы JarFile и JarEntry предоставляют вам всю необходимую информацию о пути.

Из кода вы можете видеть, что JarFile предоставляет вам перечисление классов JarEntry. Каждый JarEntry предоставляет вам достаточно информации, чтобы определить, соответствует ли это записи класса, соответствующей имени пакета начальной точки. Если это так, то все, что вы делаете, это создаете класс Class и добавляете его в список вывода.

В этой мази есть ложка, которую можно изменить, если вы измените имя пути начальной точки и запустите этот код. Это происходит, когда класс в файле JAR зависит от другого класса, который хранится в другом файле JAR, и этот файл недоступен. Когда это происходит, вы получите NoClassDefFoundError, подобный приведенному ниже, который сообщает вам, что класс JmsAppender в Log4J зависит от отсутствующего исключения JMSException:

java.lang.NoClassDefFoundError: javax/jms/JMSException
 at java.lang.Class.forName0(Native Method)
 at java.lang.Class.forName(Class.java:169)
 at marin.io.JarFileSample.createClass(JarFileSample.java:169)

В нашем случае это не проблема, так как мы не можем создавать экземпляры классов в фабрике DI, когда отсутствуют JAR-файлы, поэтому все, что нам нужно сделать, — сказать разработчику добавить отсутствующий JAR-файл …

Вместе со вчерашним блогом мы Теперь у нас есть две части головоломки: мы можем найти классы как в файлах JAR, так и в файловой системе. Следующим шагом будет выяснить, как проверить соответствующие аннотации, которые будут предметом для другого дня.

 

С http://www.captaindebug.com/2011/09/looking-into-magic-of-dependency_30.html