Перейти к основному содержимому

Java 11 Контроль доступа на основе Nest

· 5 мин. чтения

1. Введение

В этом руководстве мы рассмотрим вложения , новый контекст управления доступом, представленный в Java 11.

2. До Java 11

2.1. Вложенные типы

Java позволяет вкладывать классы и интерфейсы друг в друга . Эти вложенные типы имеют неограниченный доступ друг к другу, в том числе к закрытым полям, методам и конструкторам.

Рассмотрим следующий пример вложенного класса:

public class Outer {

public void outerPublic() {
}

private void outerPrivate() {
}

class Inner {

public void innerPublic() {
outerPrivate();
}
}
}

Здесь, хотя метод externalPrivate() является закрытым , он доступен из метода innerPublic() .

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

Таким образом, в приведенном выше примере Outer и Inner вместе образуют гнездо и являются соседями друг друга.

2.2. Мостовой метод

Правила доступа JVM не разрешают частный доступ между соседями. В идеале мы должны получить ошибку компиляции для приведенного выше примера. Однако компилятор исходного кода Java разрешает доступ, вводя уровень косвенности.

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

Это происходит за кулисами. Этот метод соединения немного увеличивает размер развернутого приложения и может запутать пользователей и инструменты.

2.3. Использование отражения

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

Например, если мы попытаемся вызвать externalPrivate() рефлексивно из класса Inner :

public void innerPublicReflection(Outer ob) throws Exception {
Method method = ob.getClass().getDeclaredMethod("outerPrivate");
method.invoke(ob);
}

Мы получим исключение:

java.lang.IllegalAccessException: 
Class com.foreach.Outer$Inner can not access a member of class com.foreach.Outer with modifiers "private"

Java 11 пытается решить эти проблемы.

3. Контроль доступа на основе гнезда

Java 11 привносит понятие соплеменников и связанных с ними правил доступа в JVM . Это упрощает работу компиляторов исходного кода Java.

Для этого формат файла класса теперь содержит два новых атрибута:

  1. Один член гнезда (обычно класс верхнего уровня) назначается хостом гнезда. Он содержит атрибут (NestMembers) для идентификации других статически известных членов гнезда.
  2. У каждого из других членов гнезда есть атрибут (NestHost), чтобы идентифицировать его узел гнезда.

Таким образом, чтобы типы C и D были соседями по гнезду, у них должен быть один и тот же хост гнезда. Тип C утверждает, что является членом гнезда, размещенного D , если он перечисляет D в своем атрибуте NestHost. Членство подтверждается, если D также перечисляет C в своем атрибуте NestMembers. Кроме того, тип D неявно является членом гнезда, которое он размещает.

Теперь компилятору не нужно генерировать мостовые методы .

Наконец, управление доступом на основе гнезда устраняет неожиданное поведение основного отражения. Поэтому метод innerPublicReflection() , показанный в предыдущем разделе, будет выполняться без каких-либо исключений.

4. API отражения Nestmate

Java 11 предоставляет средства для запроса новых атрибутов файла класса с использованием базового отражения . Класс java.lang.Class содержит следующие три новых метода.

4.1. получитьNestHost()

Это возвращает узел гнезда гнезда, к которому принадлежит этот объект класса :

@Test
public void whenGetNestHostFromOuter_thenGetNestHost() {
is(Outer.class.getNestHost().getName()).equals("com.foreach.Outer");
}

@Test
public void whenGetNestHostFromInner_thenGetNestHost() {
is(Outer.Inner.class.getNestHost().getName()).equals("com.foreach.Outer");
}

Оба класса Outer и Inner принадлежат вложенному хосту com.foreach.Outer .

4.2. isNestmateOf()

Это определяет, является ли данный класс родственным этому объекту класса :

@Test
public void whenCheckNestmatesForNestedClasses_thenGetTrue() {
is(Outer.Inner.class.isNestmateOf(Outer.class)).equals(true);
}

4.3. получитьNestMembers()

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

@Test
public void whenGetNestMembersForNestedClasses_thenGetAllNestedClasses() {
Set<String> nestMembers = Arrays.stream(Outer.Inner.class.getNestMembers())
.map(Class::getName)
.collect(Collectors.toSet());

is(nestMembers.size()).equals(2);

assertTrue(nestMembers.contains("com.foreach.Outer"));
assertTrue(nestMembers.contains("com.foreach.Outer$Inner"));
}

5. Детали компиляции

5.1. Метод моста до Java 11

Давайте углубимся в детали метода соединения, сгенерированного компилятором. Мы можем увидеть это, разобрав получившийся файл класса:

$ javap -c Outer
Compiled from "Outer.java"
public class com.foreach.Outer {
public com.foreach.Outer();
Code:
0: aload_0
1: invokespecial #2 // Method java/lang/Object."<init>":()V
4: return

public void outerPublic();
Code:
0: return

static void access$000(com.foreach.Outer);
Code:
0: aload_0
1: invokespecial #1 // Method outerPrivate:()V
4: return
}

Здесь, кроме конструктора по умолчанию и общедоступного метода externalPublic() , обратите внимание на метод access$000() . Компилятор генерирует это как метод соединения.

innerPublic () проходит через этот метод для вызова externalPrivate() :

$ javap -c Outer\$Inner
Compiled from "Outer.java"
class com.foreach.Outer$Inner {
final com.foreach.Outer this$0;

com.foreach.Outer$Inner(com.foreach.Outer);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:Lcom/foreach/Outer;
5: aload_0
6: invokespecial #2 // Method java/lang/Object."<init>":()V
9: return

public void innerPublic();
Code:
0: aload_0
1: getfield #1 // Field this$0:Lcom/foreach/Outer;
4: invokestatic #3 // Method com/foreach/Outer.access$000:(Lcom/foreach/Outer;)V
7: return
}

Обратите внимание на комментарий в строке №19. Здесь innerPublic() вызывает метод моста access$000() .

5.2. Соседи с Java 11

Компилятор Java 11 сгенерирует следующий дизассемблированный файл класса Outer :

$ javap -c Outer
Compiled from "Outer.java"
public class com.foreach.Outer {
public com.foreach.Outer();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public void outerPublic();
Code:
0: return
}

Обратите внимание, что нет метода соединения, сгенерированного компилятором. Кроме того, класс Inner теперь может напрямую вызывать метод externalPrivate() :

$ javap -c Outer\$Inner.class 
Compiled from "Outer.java"
class com.foreach.Outer$Inner {
final com.foreach.Outer this$0;

com.foreach.Outer$Inner(com.foreach.Outer);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:Lcom/foreach/Outer;
5: aload_0
6: invokespecial #2 // Method java/lang/Object."<init>":()V
9: return

public void innerPublic();
Code:
0: aload_0
1: getfield #1 // Field this$0:Lcom/foreach/Outer;
4: invokevirtual #3 // Method com/foreach/Outer.outerPrivate:()V
7: return
}

6. Заключение

В этой статье мы рассмотрели управление доступом на основе вложений, представленное в Java 11.

Как обычно, фрагменты кода можно найти на GitHub .