URLClassLoader и доступность пакетов-частных методов - программирование
Подтвердить что ты не робот

URLClassLoader и доступность пакетов-частных методов

У меня есть класс Formula, расположенный в пакете javaapplication4, который загружается с помощью URLClassLoader. Однако, когда я вызываю его из другого класса Test1, находящегося в том же пакете, я не могу получить доступ к его методам, которые имеют модификатор доступа по умолчанию (я могу получить доступ к общедоступным методам).

Я получаю следующее исключение:

java.lang.IllegalAccessException: класс javaapplication4.Test1 не может получить доступ к члену класса javaapplication4.Formula с модификаторами ""

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

Я полагаю, что это проблема с использованием другого загрузчика классов, но не уверен, почему (я установил родительский элемент URLClassLoader).

SSCCE воспроизводит проблему (пути к Windows). Я полагаю, что проблема заключается в методе loadClass:

public class Test1 {

    private static final Path TEMP_PATH = Paths.get("C:/temp/");

    public static void main(String[] args) throws Exception {
        String thisPackage = Test1.class.getPackage().getName();
        String className = thisPackage + ".Formula"; //javaapplication4.Formula
        String body = "package " + thisPackage + ";   "
                    + "public class Formula {         "
                    + "    double calculateFails() {  "
                    + "        return 123;            "
                    + "    }                          "
                    + "    public double calculate() {"
                    + "        return 123;            "
                    + "    }                          "
                    + "}                              ";

        compile(className, body, TEMP_PATH);
        Class<?> formulaClass = loadClass(className, TEMP_PATH);

        Method calculate = formulaClass.getDeclaredMethod("calculate");
        double value = (double) calculate.invoke(formulaClass.newInstance());
        //next line prints 123
        System.out.println("value = " + value);

        Method calculateFails = formulaClass.getDeclaredMethod("calculateFails");
        //next line throws exception:
        double valueFails = (double) calculateFails.invoke(formulaClass.newInstance());
        System.out.println("valueFails = " + valueFails);
    }

    private static Class<?> loadClass(String className, Path path) throws Exception {
        URLClassLoader loader = new URLClassLoader(new URL[]{path.toUri().toURL()}, Test1.class.getClassLoader());
        return loader.loadClass(className);
    }

    private static void compile(String className, String body, Path path) throws Exception {
        List<JavaSourceFromString> sourceCode = Arrays.asList(new JavaSourceFromString(className, body));

        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
        fileManager.setLocation(StandardLocation.CLASS_OUTPUT, Arrays.asList(path.toFile()));
        boolean ok = compiler.getTask(null, fileManager, null, null, null, sourceCode).call();

        System.out.println("compilation ok = " + ok);
    }

    public static class JavaSourceFromString extends SimpleJavaFileObject {
        final String code;

        JavaSourceFromString(String name, String code) {
            super(URI.create("string:///" + name.replace('.', '/') + JavaFileObject.Kind.SOURCE.extension),
                    JavaFileObject.Kind.SOURCE);
            this.code = code;
        }

        @Override
        public CharSequence getCharContent(boolean ignoreEncodingErrors) {
            return code;
        }
    }
}
4b9b3361

Ответ 1

Класс во время выполнения идентифицируется как с его полным именем, так и с его ClassLoader.

Например, когда вы тестируете два объекта Class<T> для равенства, если они имеют одинаковое каноническое имя, но были загружены из разных классов ClassLoaders, они не будут равны.

Для двух классов, принадлежащих к одному и тому же пакету (и, в свою очередь, для доступа к приватным частным процедурам), они также должны быть загружены из одного класса ClassLoader, что здесь не так. Фактически Test1 загружается системным загрузчиком классов, а Формула загружается с помощью URLClassLoader, созданного внутри loadClass().

Если вы указываете родительский загрузчик для вашего URLClassLoader, чтобы он загружал Test1, все же используются два разных загрузчика (вы можете проверить его, утверждая равенство загрузчиков).

Я не думаю, что вы можете сделать класс Formula, загруженный тем же самым Test1 ClassLoader (вам нужно будет использовать известный путь и поместить его на CLASSPATH), но я нашел способ сделайте обратное: загрузите другой экземпляр Test1 в ClassLoader, используемый для загрузки формулы. Это макет в псевдокоде:

class Test1 {

  public static void main(String... args) {
    loadClass(formula);
  }

  static void loadClass(location) {
    ClassLoader loader = new ClassLoader();
    Class formula = loader.load(location);
    Class test1 = loader.load(Test1);
    // ...
    Method compute = test1.getMethod("compute");
    compute.invoke(test1, formula);
  }

  static void compute(formula) {
    print formula;
  }
}

Вот pastebin. Несколько примечаний: я указал родительский элемент null для URLClassLoader, чтобы избежать проблемы, перечисленной выше, и я манипулировал строками для достижения этой цели, но не знаю, насколько надежным может быть этот подход в других сценариях развертывания. Кроме того, URLCLassLoader я использовал только поиск в двух каталогах, чтобы найти определения классов, а не все записи, перечисленные в CLASSPATH

Ответ 2

Ответ:

В пакете sun.reflect.Reflection существует метод под названием isSameClassPackage (фактическая подпись private static boolean isSameClassPackage(ClassLoader arg0, String arg1, ClassLoader arg2, String arg3);). Этот метод отвечает за принятие решения о том, принадлежат ли два класса к одному и тому же пакету или нет.

Первая проверка этого метода делает, что он сравнивает arg0 и arg2 (два загрузчика классов), если они разные, он возвращает false.

Итак, если вы используете разные классы-загрузчики для двух классов, это не будет соответствовать.

EDIT: Полная цепочка вызовов (по запросу):

Method.invoke()
Method.checkAccess() -- fallback to the real check
Method.slowCheckMemberAccess()   -- first thing to do to call
Reflection.ensureMemberAccess()  -- check some nulls, then
Reflection.verifyMemberAccess()  -- if public, it, OK, otherwise check further
Reflection.isSameClassPackage(Class, Class) -- get the class loaders of 2 classes
Reflection.isSameClassPackage(ClassLoader, String, ClassLoader, String) 

Ответ 3

Я нашел объяснение в спецификации JVM 5.4.4 (выделено мной):

Поле или метод R доступно для класса или интерфейса D тогда и только тогда, когда выполняется одно из следующих условий:

  • [...]
  • R либо защищен, либо имеет доступ по умолчанию (то есть ни публичный, ни защищенный, ни закрытый) и объявляется классом в том же пакете времени выполнения, что и D.

И пакет времени выполнения определен в спецификации № 5.3:

Пакет времени выполнения класса или интерфейса определяется именем пакета и определяющим загрузчиком класса класса или интерфейса.

Нижняя строка: это ожидаемое поведение.

Ответ 4

Добавьте c:\temp в java classpath и загрузите Formula.class с тем же ClassLoader, что и Test1.class

Class<?> formulaClass = Class.forName(className);

это решит вашу проблему.