2013-11-11 4 views
3

Чтобы уменьшить зависимость от внешних библиотек (и в основном как учебное упражнение), я решил добавить ServletContextListener в педагогический webapp, который я разрабатываю. Он будет создавать реестр имен классов (хранится как строки), которые имеют определенную аннотацию, сканируя каталог «WEB-INF/classes».Реализация пользовательского класса ClassLoader для сканирования/WEB-INF/classes directory

В качестве части этого я написал пользовательский класс ClassLoader, который я могу выбросить так часто, чтобы предотвратить заполнение пермгена при загрузке контекста из-за злоупотребления WebappClassLoader, с которого я начинаю работать.

К сожалению, я получаю неприятное исключение java.lang.NoClassDefFoundError: javax/websocket/server/ServerEndpointConfig$Configurator, когда он пытается загрузить один из моих ServerEndpointConfigurators.

public class MetascanClassLoader extends ClassLoader 
{ 
    private final String myBaseDir; 

    public MetascanClassLoader(final String baseDir) 
    { 
     if(!baseDir.endsWith(File.separator)) 
     { 
      myBaseDir = baseDir + File.separator; 
     } 
     else 
     { 
      myBaseDir = baseDir; 
     } 
    } 

    @Override 
    protected Class<?> loadClass(final String name, final boolean resolve) 
    throws ClassNotFoundException 
    { 
     synchronized(getClassLoadingLock(name)) 
     { 
      Class<?> clazz = findLoadedClass(name); 
      if (clazz == null) 
      { 
       try 
       { 
        final byte[] classBytes = 
         getClassBytesByName(name); 
        clazz = defineClass(
         name, classBytes, 0, classBytes.length); 
       } 
       catch (final ClassNotFoundException e) 
       { 
        if (getParent() != null) 
        { 
         clazz = getParent().loadClass(name); 
        } 
        else 
        { 
         throw new ClassNotFoundException(
          "Could not load class from MetascanClassloader's " + 
           "parent classloader", 
          e); 
        } 
       } 
      } 
      if(resolve) 
      { 
       resolveClass(clazz); 
      } 
      return clazz; 
     } 
    } 

    private byte[] getClassBytesByName(final String name) 
    throws ClassNotFoundException 
    { 
     final String pathToClass = 
      myBaseDir + name.replace(
       '.', File.separatorChar) + ".class"; 
     final ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
     try(final InputStream stream = new FileInputStream(pathToClass)) 
     { 
      int b; 
      while((b = stream.read()) != -1) 
      { 
       baos.write(b); 
      } 
     } 
     catch(final FileNotFoundException e) 
     { 
      throw new ClassNotFoundException(
       "Could not load class in MetascanClassloader.", e); 
     } 
     catch(final IOException e) 
     { 
      throw new RuntimeException(e); 
     } 
     return baos.toByteArray(); 
    } 
} 

Некоторая часть кода была подвержена влиянию реализации ClassLoader по умолчанию.

Вот общий поток, что я пытаюсь сделать:

  1. захватить замок для имени класса я пытаюсь загрузить, чтобы убедиться, что другие потоки (если я пытаюсь ударить этого ClassLoader в параллельное выполнение в какой-то момент в будущем) не пытайтесь загрузить один и тот же класс дважды.
  2. Проверьте, не загружен ли я этот класс с помощью метода findLoadedclass();
  3. Если он еще не загружен, попытайтесь извлечь класс из моего каталога «WEB-INF/classes».
  4. Если это не удается, передайте родительский загрузчик классов.
  5. Если это все еще не удается, взорвите (выбросьте).

Я могу гарантировать, что я передаю в именах классов корректно после сканирования для файлов класса - это отлично работает, если я заменю MetascanClassLoader.load (имя класса) звонки с Class.forName (Classname), но, как упоминалось ранее, Я не хочу забивать перген.

Похоже, что он падает только при попытке загрузить классы, содержащие ссылку на классы, которые могут быть найдены только в комплекте с Tomcat. У него вообще нет проблем с Java SE.

Сообщите мне, если вы также заметите что-либо особенно коварное/неприятное, что я делаю, кроме как заставить его не работать.

UPDATE: Похоже, что использование конструктора по умолчанию для суперкласса устанавливает родительский загрузчик классов в системный загрузчик классов, что объясняет недостающие классы, найденные в Tomcat.

я добавил следующую строку в качестве первой строки в моем конструкторе:

super(Thread.currentThread().getContextClassLoader()); 

К сожалению, я до сих пор работаю с проблемами, так как все объекты класса, возвращаемые моим ClassLoader не пустые, то с не информацию, за исключением имени класса. (Я проверил внутренние поля с помощью Eclipse, чтобы обнаружить это.)

Дополнительная информация: Классы Java SE по-прежнему загружаются правильно, путем осмотра объектов.Когда я проверяю один из классов, который живет в классах WEB-INF \, это то поведение, которое я получаю, когда пытаюсь проверить объект класса в любой момент после вызова loadClass() (Я скрыл имена пакетов для предотвращения совместного использования ненужной информации о проекте). Eclipse inspection.

Я также попытался обеспечить, что resolveClass() вызывается решением hardcoding loadClass (name, resolve) для true, но это не имеет значения.

ОБНОВЛЕНО СНОВА: Благодаря Хольгер отличный «пощечина-с-форелью» момент ниже, я был чрезвычайно глуп, чтобы думать, что я мог бы сделать вывод, смысл частного переменным в Class объектах возвращаются.

Я бросил несколько System.out.print() S внутри моей ClassLoader (synchronized конечно - первый раз, когда я попытался его без синхронизации был очень грязный!) Я воткнул следующую строку непосредственно перед обратным заявлением loadClass()

System.out.print(clazz.getName() + " annotations:"); 
for(final Annotation a : clazz.getAnnotations()) 
{ 
    System.out.print(" " + a.annotationType().getName() + ";"); 
} 
System.out.println(); 

Это дало мне результаты, которые я ожидал, напечатав что-то по строкам:

org.fun.MyClass annotations: org.fun.MyAnnotationOne; org.fun.MyAnnotationsTwo; 

Но подождите минутку - это работает?

System.out.print(clazz.getName() + " annotations:"); 
for(final Annotation a : clazz.getAnnotations()) 
{ 
    System.out.print(" " + a.annotationType().getName() + ";"); 
} 
System.out.print("HAS_ANNOTATION:"); 
if(clazz.getAnnotation(MyAnnotationOne.class) != null) 
{ 
    System.out.print("true"); 
} 
else 
{ 
    System.out.print("false"); 
} 
System.out.println(); 

Результат:

org.fun.MyClass annotations: org.fun.MyAnnotationOne; org.fun.MyAnnotationsTwo;HAS_ANNOTATION:false 

Хлоп! Но потом это ударило меня. Проверьте мой ответ ниже.

+1

«(Я проверил внутренние поля, используя Eclipse, чтобы обнаружить это.)« Это ничего не значит. Существует причина для инкапсуляции в ООП. Просто просмотр частных полей объекта ничего не говорит о семантике. Держу пари, если вы позвоните, например. 'getDeclaredMethods()', в экземпляре класса вместо поиска с отладчиком все будет выглядеть совершенно иначе. – Holger

+0

Отличный комментарий. Заглядывание появилось, как и для других классов, эти поля были установлены, и я ошибочно предположил, что это связано с проблемой. Я обновляю вопрос своими новыми выводами. – studro

+0

Для всех, что я мог знать, эти поля могли просто хранить ранее выкопанные значения. – studro

ответ

0

Я помню, что читал что-то интересное о сегодняшнем равенстве, которое полностью пропустило мой разум, когда я вернулся к попытке отладить эту вещь. Jon Skeet mentioned in the answer to another question:

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

Обратите внимание, что ваш код учитывает точное совпадение, однако - он не будет предоставлять вид «совместимости присваивания», который (скажем) instanceof делает, когда видит, ссылается ли значение на объект, являющийся экземпляром данного класс. Для этого вам нужно посмотреть Class.isAssignableFrom.

Как java.lang.Class не отменяет java.lang.Object.equals(), и каждый Class объекта хранит ссылку на свой ClassLoader, даже если два Class эсов имеют одинаковый класс, данные и имя, они не будут равны, если они пришли из два разных ClassLoaders. Итак, как это применимо здесь?

Проблематика строка кода

if(clazz.getAnnotation(MyAnnotationOne.class) != null) 

Вызов clazz.getAnnotations(), как указано выше, в окончательном обновлении вопроса будет список аннотаций с использованием полного имени класса, который равен полного имени класса класса, на который ссылается литерал MyAnnotationOne.class в приведенной выше инструкции if.Тем не менее, я предполагаю, что clazz.getAnnotation() использует некоторый внутренний вызов equals(), чтобы проверить, являются ли классы аннотаций одинаковыми.

Класс, на который ссылается MyAnnotationOne.class, загружается пользовательским классом класса (который в большинстве случаев будет его родителем). Однако класс MyAnnotationOne, который прикреплен к clazz, загружается самим пользователем ClassLoader. В результате метод equals() возвращает false, и мы получаем null назад.

Большое спасибо Хольгеру за то, что он оттолкнул меня в правильном направлении и отвел меня в ответ.

Смежные вопросы