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;
}
}
}
4 ответа
Класс во время выполнения идентифицируется как его полностью определенным именем, так и его 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;
}
}
Вот пастбина. Пара замечаний: я указал null
родительский для URLClassLoader, чтобы избежать проблемы, перечисленной выше, и я манипулировал строками для достижения цели - но не знаю, насколько надежным может быть этот подход в других сценариях развертывания. Кроме того, URLCLassLoader, который я использовал, ищет только в двух каталогах, чтобы найти определения классов, а не все записи, перечисленные в CLASSPATH.
Ответ:
в sun.reflect.Reflection
Пакет есть метод, называемый isSameClassPackage
(фактическая подпись private static boolean isSameClassPackage(ClassLoader arg0, String arg1, ClassLoader arg2, String arg3);
). Этот метод отвечает за решение, принадлежат ли два класса одному и тому же пакету или нет.
Первая проверка этого метода заключается в том, что он сравнивает arg0 и arg2 (два загрузчика классов), если они различаются, возвращает false.
Таким образом, если вы используете разные загрузчики классов для двух классов, они не будут совпадать.
РЕДАКТИРОВАТЬ: Полная цепочка вызовов (по запросу):
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,'s OK, otherwise check further
Reflection.isSameClassPackage(Class, Class) -- get the class loaders of 2 classes
Reflection.isSameClassPackage(ClassLoader, String, ClassLoader, String)
Я нашел объяснение в спецификации 5.4.4 JVM (выделено мое):
Поле или метод R доступны классу или интерфейсу D тогда и только тогда, когда выполняется любое из следующих условий:
- [...]
- R либо защищен, либо имеет доступ по умолчанию (то есть ни открытый, ни защищенный, ни частный) и объявлен классом в том же пакете времени выполнения, что и D.
И пакет времени выполнения определен в спецификации # 5.3:
Пакет времени выполнения класса или интерфейса определяется по имени пакета и определению загрузчика класса класса или интерфейса.
Итог: это ожидаемое поведение.
Добавлять c:\temp
в Java путь к классам и загрузить Formula.class с тем же ClassLoader, что и Test1.class
Class<?> formulaClass = Class.forName(className);
это решит вашу проблему.