拨开荷叶行,寻梦已然成。仙女莲花里,翩翩白鹭情。
IMG-LOGO
主页 文章列表 Java11 中基于嵌套关系的访问控制优化

Java11 中基于嵌套关系的访问控制优化

白鹭 - 2022-01-23 2030 0 0

Java11 中基于嵌套的权限控制

你好,我是看山,

Java 语言很强大,但是,有人的地方就有江湖,有猿的地方就有 bug,Java 的核心代码并非十全十美,比如在 JDK 中居然也有反模式界面常量 中介绍的反模式实作,以及本文说到的这个技术债务:嵌套关系(NestMate)呼叫方式,

在 Java 语言中,类和界面可以相互嵌套,这种组合之间可以不受限制的彼此访问,包括访问彼此的建构式、栏位、方法等,即使是private私有的,也可以彼此访问,比如下面这样定义:

public class Outer {
    private int i;

    public void print1() {
        print11();
        print12();
    }

    private void print11() {
        System.out.println(i);
    }

    private void print12() {
        System.out.println(i);
    }

    public void callInnerMethod() {
        final Inner inner = new Inner();
        inner.print4();
        inner.print5();
        System.out.println(inner.j);
    }

    public class Inner {
        private int j;

        public void print3() {
            System.out.println(i);
            print1();
        }

        public void print4() {
            System.out.println(i);
            print11();
            print12();
        }

        private void print5() {
            System.out.println(i);
            print11();
            print12();
        }
    }
}

上例中,Outer类中的栏位i、方法print11print12都是私有的,但是可以在Inner类中直接访问,Inner类的栏位j、方法print5是私有的,也可以在Outer类中使用,这种设计是为了更好的封装,在用户看来,这几个彼此嵌套的类/界面是一体的,分开定义是为了更好的封装自己,隔离不同特性,但是有因为彼此是一体,所以私有元素也应该是共有的,

Java11 之前的实作方式

我们使用 Java8 编译,然后借助javap -c命令分别查看OuterInner的结果,

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

  public void print1();
    Code:
       0: aload_0
       1: invokespecial #2                  // Method print11:()V
       4: aload_0
       5: invokespecial #1                  // Method print12:()V
       8: return

  public void callInnerMethod();
    Code:
       0: new           #7                  // class cn/howardliu/tutorials/java8/nest/Outer$Inner
       3: dup
       4: aload_0
       5: invokespecial #8                  // Method cn/howardliu/tutorials/java8/nest/Outer$Inner."<init>":(Lcn/howardliu/tutorials/java8/nest/Outer;)V
       8: astore_1
       9: aload_1
      10: invokevirtual #9                  // Method cn/howardliu/tutorials/java8/nest/Outer$Inner.print4:()V
      13: aload_1
      14: invokestatic  #10                 // Method cn/howardliu/tutorials/java8/nest/Outer$Inner.access$000:(Lcn/howardliu/tutorials/java8/nest/Outer$Inner;)V
      17: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
      20: aload_1
      21: invokestatic  #11                 // Method cn/howardliu/tutorials/java8/nest/Outer$Inner.access$100:(Lcn/howardliu/tutorials/java8/nest/Outer$Inner;)I
      24: invokevirtual #6                  // Method java/io/PrintStream.println:(I)V
      27: return

  static int access$200(cn.howardliu.tutorials.java8.nest.Outer);
    Code:
       0: aload_0
       1: getfield      #3                  // Field i:I
       4: ireturn

  static void access$300(cn.howardliu.tutorials.java8.nest.Outer);
    Code:
       0: aload_0
       1: invokespecial #2                  // Method print11:()V
       4: return

  static void access$400(cn.howardliu.tutorials.java8.nest.Outer);
    Code:
       0: aload_0
       1: invokespecial #1                  // Method print12:()V
       4: return
}

再来看看Inner的编译结果,这里需要注意的是,内部类会使用特殊的命名方式定义Inner类,最侄训将编译结果存盘在两个档案中:

$ javap -c Outer\$Inner.class
Compiled from "Outer.java"
public class cn.howardliu.tutorials.java8.nest.Outer$Inner {
  final cn.howardliu.tutorials.java8.nest.Outer this$0;

  public cn.howardliu.tutorials.java8.nest.Outer$Inner(cn.howardliu.tutorials.java8.nest.Outer);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #3                  // Field this$0:Lcn/howardliu/tutorials/java8/nest/Outer;
       5: aload_0
       6: invokespecial #4                  // Method java/lang/Object."<init>":()V
       9: return

  public void print3();
    Code:
       0: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: aload_0
       4: getfield      #3                  // Field this$0:Lcn/howardliu/tutorials/java8/nest/Outer;
       7: invokestatic  #6                  // Method cn/howardliu/tutorials/java8/nest/Outer.access$200:(Lcn/howardliu/tutorials/java8/nest/Outer;)I
      10: invokevirtual #7                  // Method java/io/PrintStream.println:(I)V
      13: aload_0
      14: getfield      #3                  // Field this$0:Lcn/howardliu/tutorials/java8/nest/Outer;
      17: invokevirtual #8                  // Method cn/howardliu/tutorials/java8/nest/Outer.print1:()V
      20: return

  public void print4();
    Code:
       0: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: aload_0
       4: getfield      #3                  // Field this$0:Lcn/howardliu/tutorials/java8/nest/Outer;
       7: invokestatic  #6                  // Method cn/howardliu/tutorials/java8/nest/Outer.access$200:(Lcn/howardliu/tutorials/java8/nest/Outer;)I
      10: invokevirtual #7                  // Method java/io/PrintStream.println:(I)V
      13: aload_0
      14: getfield      #3                  // Field this$0:Lcn/howardliu/tutorials/java8/nest/Outer;
      17: invokestatic  #9                  // Method cn/howardliu/tutorials/java8/nest/Outer.access$300:(Lcn/howardliu/tutorials/java8/nest/Outer;)V
      20: aload_0
      21: getfield      #3                  // Field this$0:Lcn/howardliu/tutorials/java8/nest/Outer;
      24: invokestatic  #10                 // Method cn/howardliu/tutorials/java8/nest/Outer.access$400:(Lcn/howardliu/tutorials/java8/nest/Outer;)V
      27: return

  static void access$000(cn.howardliu.tutorials.java8.nest.Outer$Inner);
    Code:
       0: aload_0
       1: invokespecial #2                  // Method print5:()V
       4: return

  static int access$100(cn.howardliu.tutorials.java8.nest.Outer$Inner);
    Code:
       0: aload_0
       1: getfield      #1                  // Field j:I
       4: ireturn
}

我们可以看到,OuterInner中多出了几个方法,方法名格式是access$*00

Outer中的access$200方法回传了属性iaccess$300access$400分别呼叫了print11print12方法,这些新增的方法都是静态方法,作用域是默认作用域,即包内可用,这些方法最终被Inner类中的print3print4呼叫,相当于间接呼叫Outer中的私有属性或方法,

我们称这些生成的方法为“桥”方法(Bridge Method),是一种实作嵌套关系内部互相访问的方式,

在编译的时候,Java 为了保持类的单一特性,会将嵌套类编译到多个 class 档案中,同时为了保证嵌套类能够彼此访问,自动创建了呼叫私有方法的“桥”方法,这样,在保持原有定义不变的情况下,又实作了嵌套语法,

技术债务

“桥”方法的实作是比较巧妙的,但是这会造成原始码与编译结果访问控制权限不一致,比如,我们可以在Inner中呼叫Outer中的私有方法,按照道理来说,我们可以在Inner中通过反射呼叫Outer的方法,但实际上不行,会抛出IllegalAccessException例外,我们验证一下:

public class Outer {
    // 省略其他方法
    public void callInnerReflectionMethod()
            throws InvocationTargetException, NoSuchMethodException, IllegalAccessException {
        final Inner inner = new Inner();
        inner.callOuterPrivateMethod(this);
    }

    public class Inner {
        // 省略其他方法
        public void callOuterPrivateMethod(Outer outer)
                throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
            final Method method = outer.getClass().getDeclaredMethod("print12");
            method.invoke(outer);
        }
    }
}

定义测验用例:

@Test
void gotAnExceptionInJava8() {
    final Outer outer = new Outer();

    final Exception e = assertThrows(IllegalAccessException.class, outer::callInnerReflectionMethod);
    e.printStackTrace();

    assertDoesNotThrow(outer::callInnerMethod);
}

打印的例外信息是:

java.lang.IllegalAccessException: class cn.howardliu.tutorials.java8.nest.Outer$Inner cannot access a member of class cn.howardliu.tutorials.java8.nest.Outer with modifiers "private"
  at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:361)
  at java.base/java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:591)
  at java.base/java.lang.reflect.Method.invoke(Method.java:558)
  at cn.howardliu.tutorials.java8.nest.Outer$Inner.callOuterPrivateMethod(Outer.java:62)
  at cn.howardliu.tutorials.java8.nest.Outer.callInnerReflectionMethod(Outer.java:36)

通过反射直接呼叫私有方法会失败,但是可以直接的或者通过反射访问这些“桥”方法,这样就比较奇怪了,所以提出 JEP181 改进,修复这个技术债务的同时,为后续的改进铺路,

Java11 中的实作

我们再来看看 Java11 编译之后的结果:

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

  public void print1();
    Code:
       0: aload_0
       1: invokevirtual #2                  // Method print11:()V
       4: aload_0
       5: invokevirtual #3                  // Method print12:()V
       8: return

  public void callInnerMethod();
    Code:
       0: new           #7                  // class cn/howardliu/tutorials/java11/nest/Outer$Inner
       3: dup
       4: aload_0
       5: invokespecial #8                  // Method cn/howardliu/tutorials/java11/nest/Outer$Inner."<init>":(Lcn/howardliu/tutorials/java11/nest/Outer;)V
       8: astore_1
       9: aload_1
      10: invokevirtual #9                  // Method cn/howardliu/tutorials/java11/nest/Outer$Inner.print4:()V
      13: aload_1
      14: invokevirtual #10                 // Method cn/howardliu/tutorials/java11/nest/Outer$Inner.print5:()V
      17: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      20: aload_1
      21: getfield      #11                 // Field cn/howardliu/tutorials/java11/nest/Outer$Inner.j:I
      24: invokevirtual #6                  // Method java/io/PrintStream.println:(I)V
      27: return
}

是不是很干凈,与Outer类的原始码结构是一致的,我们再看看Inner有没有什么变化:

$ javap -c Outer\$Inner.class
Compiled from "Outer.java"
public class cn.howardliu.tutorials.java11.nest.Outer$Inner {
  final cn.howardliu.tutorials.java11.nest.Outer this$0;

  public cn.howardliu.tutorials.java11.nest.Outer$Inner(cn.howardliu.tutorials.java11.nest.Outer);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #1                  // Field this$0:Lcn/howardliu/tutorials/java11/nest/Outer;
       5: aload_0
       6: invokespecial #2                  // Method java/lang/Object."<init>":()V
       9: return

  public void print3();
    Code:
       0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: aload_0
       4: getfield      #1                  // Field this$0:Lcn/howardliu/tutorials/java11/nest/Outer;
       7: getfield      #4                  // Field cn/howardliu/tutorials/java11/nest/Outer.i:I
      10: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
      13: aload_0
      14: getfield      #1                  // Field this$0:Lcn/howardliu/tutorials/java11/nest/Outer;
      17: invokevirtual #6                  // Method cn/howardliu/tutorials/java11/nest/Outer.print1:()V
      20: return

  public void print4();
    Code:
       0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: aload_0
       4: getfield      #1                  // Field this$0:Lcn/howardliu/tutorials/java11/nest/Outer;
       7: getfield      #4                  // Field cn/howardliu/tutorials/java11/nest/Outer.i:I
      10: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
      13: aload_0
      14: getfield      #1                  // Field this$0:Lcn/howardliu/tutorials/java11/nest/Outer;
      17: invokevirtual #7                  // Method cn/howardliu/tutorials/java11/nest/Outer.print11:()V
      20: aload_0
      21: getfield      #1                  // Field this$0:Lcn/howardliu/tutorials/java11/nest/Outer;
      24: invokevirtual #8                  // Method cn/howardliu/tutorials/java11/nest/Outer.print12:()V
      27: return
}

同样干凈,

我们在通过测验用例验证一下反射呼叫:

@Test
void doesNotGotAnExceptionInJava11() {
    final Outer outer = new Outer();

    assertDoesNotThrow(outer::callInnerReflectionMethod);
    assertDoesNotThrow(outer::callInnerMethod);
}

结果是正常运行,

这就是 JEP181 期望的结果,原始码和编译结果一致,访问控制一致,

Nestmate 新增的 API

在 Java11 中还新增了几个 API,用于嵌套关系的验证:

getNestHost

这个方法是回传嵌套主机(NestHost),转成普通话就是找到嵌套类的外层类,对于非嵌套类,直接回传自身(其实也算是回传外层类),

我们看下用法:

@Test
void checkNestHostName() {
    final String outerNestHostName = Outer.class.getNestHost().getName();
    assertEquals("cn.howardliu.tutorials.java11.nest.Outer", outerNestHostName);

    final String innerNestHostName = Inner.class.getNestHost().getName();
    assertEquals("cn.howardliu.tutorials.java11.nest.Outer", innerNestHostName);

    assertEquals(outerNestHostName, innerNestHostName);

    final String notNestClass = NotNestClass.class.getNestHost().getName();
    assertEquals("cn.howardliu.tutorials.java11.nest.NotNestClass", notNestClass);
}

对于OuterInner都是回传了cn.howardliu.tutorials.java11.nest.Outer

getNestMembers

这个方法是回传嵌套类的嵌套成员阵列,下标是 0 的元素确定是 NestHost 对应的类,其他元素顺序没有给出排序规则,我们看下使用:

@Test
void getNestMembers() {
    final List<String> outerNestMembers = Arrays.stream(Outer.class.getNestMembers())
            .map(Class::getName)
            .collect(Collectors.toList());

    assertEquals(2, outerNestMembers.size());
    assertTrue(outerNestMembers.contains("cn.howardliu.tutorials.java11.nest.Outer"));
    assertTrue(outerNestMembers.contains("cn.howardliu.tutorials.java11.nest.Outer$Inner"));

    final List<String> innerNestMembers = Arrays.stream(Inner.class.getNestMembers())
            .map(Class::getName)
            .collect(Collectors.toList());

    assertEquals(2, innerNestMembers.size());
    assertTrue(innerNestMembers.contains("cn.howardliu.tutorials.java11.nest.Outer"));
    assertTrue(innerNestMembers.contains("cn.howardliu.tutorials.java11.nest.Outer$Inner"));
}

isNestmateOf

这个方法是用于判断两个类是否是彼此的 NestMate,彼此形成嵌套关系,判断依据还是嵌套主机,只要相同,两个就是 NestMate,我们看下使用:

@Test
void checkIsNestmateOf() {
    assertTrue(Inner.class.isNestmateOf(Outer.class));
    assertTrue(Outer.class.isNestmateOf(Inner.class));
}

后续的改进

嵌套关系是作为 Valhalla 项目的一部分,这个项目的主要目标之一是改进 JAVA 中的值型别和泛型,后续会有更多的改进:

  • 在泛型特化(generic specialization)中,每个特化型别(specialized type)可被创建为泛型的一个 Nestmate,
  • 支持对Unsafe.defineAnonymousClass() API 的安全替换,实作将新类创建为已有类的 Nestmate,
  • 可能会影响“密封类”(sealed classes),仅允许 Nestmate 的子类作为密封类,
  • 可能会影响私有嵌套型别,私有嵌套型别当前定义为包内可访问(package-access),

文末总结

本文阐述了基于嵌套关系的访问控制优化,其中涉及NestMateNestHostNestMember等概念,这次优化是 Valhalla 项目中一部分,主要改进 Java 中的值型别和泛型等,文中涉及原始码都上传在 GitHub 上,关注公号「看山的小屋」回复“java”获取原始码,

青山不改,绿水长流,咱们下次见,

推荐阅读

  • 一文掌握 Java8 Stream 中 Collectors 的 24 个操作
  • 一文掌握 Java8 的 Optional 的 6 种操作
  • 使用 Lambda 表达式实作超强的排序功能
  • Java8 的时间库(1):介绍 Java8 中的时间类及常用 API
  • Java8 的时间库(2):Date 与 LocalDate 或 LocalDateTime 互相转换
  • Java8 的时间库(3):开始使用 Java8 中的时间类
  • Java8 的时间库(4):检查日期字符串是否合法
  • Java8 的新特性
  • Java9 的新特性

你好,我是看山,游于码界,戏享人生,如果文章对您有帮助,请点赞、收藏、关注,我还整理了一些精品学习资料,关注公众号「看山的小屋」,回复“资料”即可获得,

个人主页:https://www.howardliu.cn
个人博文:Java11 中基于嵌套关系的访问控制优化
CSDN 主页:https://kanshan.blog.csdn.net/
CSDN 博文:Java11 中基于嵌套关系的访问控制优化

标签:

0 评论

发表评论

您的电子邮件地址不会被公开。 必填的字段已做标记 *