Перезагрузить класс в Groovy - PullRequest
2 голосов
/ 14 октября 2019

У меня есть собственный ClassLoader, расширяющий GroovyClassLoader, который компилирует исходный код в .class файлы на диске и затем загружает полученный класс:

class MyClassLoader extends GroovyClassLoader {

  File cache = new File( './cache' )
  Compiler compiler

  MyClassLoader() {
    CompilerConfiguration cc = new CompilerConfiguration( targetDirectory:cache )
    compiler = new Compiler( cc )
    addClasspath cache.path
  }

  @Override
  Class findClass( name ) {
    try{
      parent.findClass name
    }catch( ClassNotFoundException e ){
      compiler.compile name, getBodySomehow()
      byte[] blob = loadFromFileSystem name
      Class c = defineClass name, blob, 0, blob.length
      setClassCacheEntry c
      c
    }
  }

  @Override
  void removeClassCacheEntry​(String name) {
    Class c = cache[ name ]
    super.removeClassCacheEntry​(name)
    GroovySystem.metaClassRegistry.removeMetaClass c
    deleteFiles name
  }
}

Class clazz = myClassLoader.loadClass 'some.pckg.SomeClass'

Теперь, если я изменю исходный код, вызовите myClassLoader.removeClassCacheEntry​(name) и попробуйте myClassLoader.loadClass() еще раз, я получаю:

java.lang.LinkageError: loader (экземпляр com / my / MyClassLoader): попытка дублировать определение класса для имени some / pckg / SomeClass

Я прочитал большую часть Интернета и нашел «решение» для инициализации загрузчика классов для каждого класса:

MyClassLoader myClassLoader = new MyClassLoader()
Class clazz = myClassLoader.loadClass 'some.pckg.SomeClass'

Это работает, но повышает производительностьмои проблемы ...

Как правильно перезагрузить классы? Как я могу использовать один и тот же загрузчик классов? Чего мне не хватает?

Ответы [ 2 ]

1 голос
/ 28 октября 2019

На самом деле есть хитрость, которую можно использовать

Первоначально, когда вы вызываете

classLoader.defineClass(className, classBytes, 0, classBytes.length)

Он вызывает нативный метод Java defineClass1, который фактически вызывает loadClass метод.

Таким образом, возможно перехватить этот метод и обработать его немного иначе, чем оригинал.

В папке, содержащей файлы кэшированных классов, у меня есть следующий groovy, скомпилированный в класс: A.class

println "Hello World!"

B.class для проверки загрузки зависимых классов

class B extends A {
    def run(){
        super.run()
        println "Hello from ${this.getClass()}!"
    }
}

и C.class для проверки загрузки многоуровневых классов

я использовал эту банку дляскомпилируйте следующий класс и выполните пример повторной загрузки класса

class C extends org.apache.commons.lang3.RandomUtils {
    def rnd(){ nextInt() }
}

следующий класс + код загружает и перезагружает тот же класс:

import java.security.PrivilegedAction;
import java.security.AccessController;
import org.codehaus.groovy.control.CompilationFailedException;

@groovy.transform.CompileStatic
class CacheClassLoader extends GroovyClassLoader{
    private File cacheDir = new File('/11/tmp/a__cache')

    private CacheClassLoader(){throw new RuntimeException("default constructor not allowed")}

    public CacheClassLoader(ClassLoader parent){
        super(parent)
    }
    public CacheClassLoader(Script parent){
        this(parent.getClass().getClassLoader())
    }

    @Override
    protected Class getClassCacheEntry(String name) {
        Class clazz = super.getClassCacheEntry(name)
        if( clazz ){
            println "getClassCacheEntry $name -> got from memory cache"
            return clazz
        }
        def cacheFile = new File(cacheDir, name.tr('.','/')+'.class')
        if( cacheFile.exists() ){
            println "getClassCacheEntry $name -> cache file exists, try to load it"
            //clazz = getPrivelegedLoader().defineClass(name, cacheFile.bytes)
            clazz = getPrivelegedLoader().defineClass(name, cacheFile.bytes)
            super.setClassCacheEntry(clazz)
        }
        return clazz
    }

    private PrivelegedLoader getPrivelegedLoader(){
        PrivelegedLoader loader = AccessController.doPrivileged(new PrivilegedAction<PrivelegedLoader>() {
            public PrivelegedLoader run() {
                return new PrivelegedLoader();
            }
        });
    }
    public class PrivelegedLoader extends CacheClassLoader {
        private final CacheClassLoader delegate

        public PrivelegedLoader(){ 
            super(CacheClassLoader.this)
            this.delegate = CacheClassLoader.this
        }

        public Class loadClass(String name, boolean lookupScriptFiles, boolean preferClassOverScript, boolean resolve) throws ClassNotFoundException, CompilationFailedException {
            Class c = findLoadedClass(name);
            if (c != null) return c;
            return delegate.loadClass(name, lookupScriptFiles, preferClassOverScript, resolve);
        }
    }
}

def c=null
//just to show intermediate class loaders could load some classes that will be used in CacheClassLoader
def cl_0 = new GroovyClassLoader(this.getClass().getClassLoader())
cl_0.addClasspath('/11/tmp/a__cache/commons-lang3-3.5.jar')
//create cache class loader
def cl = new CacheClassLoader(cl_0)

println "---1---"
c = cl.loadClass('A')
c.newInstance().run()

println "---2---"
c = cl.loadClass('A')
c.newInstance().run()

println "---3---"
cl.removeClassCacheEntry('A')
c = cl.loadClass('A')
c.newInstance().run()

println "---4---"
c = cl.loadClass('B')
c.newInstance().run()

println "---5---"
cl.removeClassCacheEntry('A')
cl.removeClassCacheEntry('B')
c = cl.loadClass('B')
c.newInstance().run()

println "---6---"
c = cl.loadClass('C')
println c.newInstance().rnd()

результат:

---1---
getClassCacheEntry A -> cache file exists, try to load it
Hello World!
---2---
getClassCacheEntry A -> got from memory cache
Hello World!
---3---
getClassCacheEntry A -> cache file exists, try to load it
Hello World!
---4---
getClassCacheEntry B -> cache file exists, try to load it
getClassCacheEntry A -> got from memory cache
Hello World!
Hello from class B!
---5---
getClassCacheEntry B -> cache file exists, try to load it
getClassCacheEntry A -> cache file exists, try to load it
Hello World!
Hello from class B!
---6---
getClassCacheEntry C -> cache file exists, try to load it
226399895

PS: не уверен, что требуется привилегированный доступ

1 голос
/ 28 октября 2019

JVM не позволяет просто выгрузить некоторый класс, единственный способ выгрузить класс - это GC его. И класс может быть GC, как и любой другой объект -> все достижимые ссылки должны быть удалены и GC запускается.
Сложная часть ... загрузчик классов содержит ссылки на все классы. Таким образом, единственный способ выгрузить класс - это избавиться от класса и загрузчика классов.

Более подробную информацию вы можете найти в спецификации языка: https://docs.oracle.com/javase/specs/jvms/se13/jvms13.pdf 12.7 Выгрузка классов и интерфейсов

Реализация языка программирования Java может выгружать классы. Класс или интерфейс могут быть выгружены, если и только если их определяющий загрузчик класса может быть восстановлен сборщиком мусора, как обсуждалось в §12.6. Классы и интерфейсы, загруженные загрузчиком начальной загрузки, не могут быть выгружены.

И выгрузка классов вообще не требуется реализовывать в некоторых реализациях JVM:

Выгрузка классовоптимизация, которая помогает уменьшить использование памяти. [...] система решает реализовать оптимизацию, такую ​​как выгрузка классов. [...] Следовательно, был ли класс или интерфейс выгружен или нет, должно быть прозрачно для программы.

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

Таким образом, каждый ваш скрипт должен просто использовать собственный загрузчик классов, поскольку это единственный способ фактически не тратить память , так что класс может быть позднее GC. Просто убедитесь, что вы не используете никаких библиотек, которые могут кэшировать ссылки на ваш класс - как многие библиотеки сериализации / ORM могут делать это для типов данных или некоторых других библиотек отражений.
Другим решением будет использование другого языка сценариев. это не создает Java-классы, а просто выполняет некую структуру AST.

Существует и еще одно решение этой проблемы, но оно очень сложное, и это не то, что вы должны использовать в рабочей среде, оно даже требует от вас предоставления специальных аргументов JVM или JVM из JDK, который содержит все необходимые модули. Поскольку java поддерживает API инструментария, который может позволить вам изменять байт-код класса во время выполнения, но если класс уже загружен, вы можете изменять только байт-код методов, вы не можете добавлять / удалять / редактировать метод / поле / подписи класса. Поэтому может быть очень плохой идеей использовать его для таких скриптов, поэтому я на этом остановлюсь.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...