权限和反射
概述
java中访问权限、注解。你自以为自己都滚瓜烂熟了,但你可能认为它们都是小儿科,记住了简单用法就行。真到了你写框架的时候,什么时候用public,什么时候用private,什么时候又用protect,就不是很清晰,随便用一个,框架也是照常运行。但你的框架很容易被攻击,或者缺少功能使用。因为不该暴露的暴露,该暴露的又没有暴露。注解又是如何被读取,怎么使用其带的值。
访问权限控制
访问权限,首先应该明白在哪访问(是否同包),通过谁访问(当前类对象、子孙类对象)、访问什么(方法、属性)。在哪访问应该优先考虑。
类访问
类修饰符public、abstract、final、缺省(没有修饰符)。
- public修饰:允许任何地方,创建该类的实例。至于属性方法的访问,要看它门头上的修饰符。
- abstract抽象类:允许该类有未实现的方法。不涉及访问权限。
- 默认:包外不可见,本包内,其他类可以创建它的实例,本包内继承。
- final类:包外不可见,本包内,其他类可以创建它的实例。不能被继承。
属性或方法访问权限
- public公开的,在任何地点,任何访问者都可以访问
- protected ,本包下,都可以访问。
- 默认,本包下,自己的对象才能访问。
- private ,本类中,自己对象才能访问。
作用域 | 当前类对象 | 同一个包(packege) | 子孙类对象 | 其他包 |
---|---|---|---|---|
public | √ | √ | √ | √ |
protected | √ | √ | √ | X |
默认 | √ | √ | X | X |
private | √ | X | X | X |
列表中每次应该取对象(本类、子孙类对象)状态和包(本包、外包)状态做与运算。
可变参数
Object... args
必须放在方法参数最后,如果放前面,后面的参数也会被认为是可变参数。调用Spring的getBean一定要分清,调用的四个方法中的哪一种,因为有可变长度的构造函数参数,一不小心就传成了就会出错。
factory.getBean("firstBean",null,"s",null)
本次传参混淆了doGetBean
的四参数,这样实际调用的是getBean(String name, @Nullable Class<T> requiredType, @Nullable Object... args)
,null和s被认为是可变长度参数。这样构造器覆盖构造参数时,由于传入的和构造器本身的参数长度不一致,会报错Error creating bean with name 'firstBean' defined in class path resource [beans.xml]: Could not resolve matching constructor (hint: specify index/type/name arguments for simple parameters to avoid type ambiguities)
。Idea默认集合中null不显示,怎么都发现不了为什么参数长度会是2。
基于嵌套的访问控制
Nest-Based Access Control是JDK11新引入的访问控制规则,在java11之前,classfile用InnerClasses和EnclosingMethod两种属性来帮助编译器确认源码的嵌套关系,每一个嵌套的类型会编译到自己的class文件中,在使用上述属性来连接其他class文件。这些属性对于jvm确定嵌套关系上已经足够了,但是它们不直接适用于访问控制,并且和java语言绑定的太紧了。为了提供一种更大的,更广泛的,不仅仅是java语言的嵌套类型,并且补足访问控制检测的不足,引入了两个新的class文件属性。一个是嵌套宿主NestHost
(也叫top-level class),将嵌套的宿主记录到当前类或接口声明中。另一个是嵌套成员NestMembers
,记录了被授权可以声明在嵌套宿主类或接着口中的类或接口。class文件结构的属性表不能同时包含NestMembers
属性和NestHost
属性。这个规则避免了一个嵌套宿主在不同的嵌套中声明成员。
当且仅当以下条件为真时,一个私有成员字段或方法R是可以被类或接口D访问的:
…
R是声明在另一个类或接口C中,同时 C 和 D 是嵌套伙伴。
由于C和D是嵌套同伴,那么他们一定有同一个嵌套宿主类,在这里是Test。如果C在自己的嵌套宿主的属性中可以列举出D,那么C类就会被D 断言为自己的嵌套成员。如果D也可以在自己的NestMembers 属性中列举出C,那么这个嵌套同伴关系就是有效的。D是自己的隐性嵌套成员。
public class NestHost {
class NestMemberOne {
private String R = "get到我了";
};
class NestMemberAnother {
private void privateMethod(){
System.out.println( (new NestMemberOne()).R);
}
};
public Object Ap(){
class LocalInnerClass {}
return new LocalInnerClass();
}
public static void main(String[] args) {
/**1.非宿主类必须通过宿主类对象创建宿主类的非静态内部类对象(
new NestHost().new NestMemberOne())。创建静态内部类对
象:NestHost.NestMemberOne mo =new NestHost.NestMemberOne()
2.宿主类可以提供非静态方法创建内部类对象(new NestMemberOne())。如
果是提供的是静态方法,创建非静态内部类对象,必须使用宿主类的对象来创
建内部类对象。如下:
*/
NestMemberOne mo = new NestHost().new NestMemberOne();
for(Class nb:mo.getClass().getNestMembers()) System.out.println(nb);
/**
NestMemberOne和NestMemberAnother是嵌套伙伴关系,所有嵌套成员属性
一致
class com.bean.anotation.ownanotation.NestHost
class com.bean.anotation.ownanotation.NestHost$NestMemberAnother
class com.bean.anotation.ownanotation.NestHost$NestMemberOne
*/
Class cl = (new NestHost()).Ap().getClass();
System.out.print("Enclosing Method :");
System.out.println(cl.getEnclosingMethod());
/**
当一个类为局部内部类或匿名嵌套类时才有EnclosingMethod属性,返回它的
外围方法。
Enclosing Method :public java.lang.Object
com.bean.anotation.ownanotation.NestHost.Ap()
*/
}
}
接口 Interface
接口中可以有final属性变量,默认方法,静态方法。
public interface IntefaceMmethod {
/**会默认public static final类型,它调用了静态方法来赋值。
* IntefaceMmethod在JVM初始化调用方法给final的HPQ赋值。外界通过接口
* 调用test(String...args)。所以测试类启动的时候会走两次。不要惊讶。
* 这样做的好处:我需要一个final常量,它有生成规则,而且这个规则也会给
* 变量使用(我们还要生成其他IntefaceMmethod对象,就可以调用test方法
* test方法中可以根据参数不同生成不同的IntefaceMmethod对象)。
*/
IntefaceMmethod HPQ = test("dalas");
static IntefaceMmethod test(String...args){
System.out.println(args);
return null;
}
}
注解Anotation
这是注解接口,但我们没见过它被extends,我们自定义的接口又是怎么继承它的。其中的@interface是一个关键字,这个关键字声明隐含了一个信息:这个接口必须继承java.lang.annotation.Annotation
接口,也就是说注解的本质还是接口。JDK动态代理对注解进行代理,这样注解就有了具体实现类。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {
String value();
}
//注解的class文件反编译之后
public interface Table extends java.lang.annotation.Annotation {
//注解元素成为抽象方法
public abstract java.lang.String value();
}
Annotation解析
运行时Annotation解析
运行时 Annotation 指 @Retention 为,RetentionPolicy.RUNTIME
的 Annotation,可调用Class的常用 API 解析。其实就是通过反射
getAnnotation(XXXAnnotation.class);
getAnnotations();
isAnnotationPresent(XXXAnnotation.class);
编译时Annotation解析
编译时Annotation指@Retention为RetentionPolicy.CLASS
的Annotation,由apt(Annotation Processing Tool) 自动解析。
- 自定义类继承自AbstractProcessor
- 重写其中的 process 方法
实际是 apt(Annotation Processing Tool) 在编译时自动查找所有继承自 AbstractProcessor 的类,然后调用他们的 process 方法去处理
public class TableProcessor extends AbstractProcessor {
@Override
public boolean process(Set<?extends TypeElement> annotations, RoundEnvironment env) {
HashMap<String, String> map = new HashMap<String, String>();
for (TypeElement te : annotations) {
for (Element element : env.getElementsAnnotatedWith(te)) {
Table table = element.getAnnotation(Table.class);
map.put(element.getEnclosingElement().toString(), table.value());
}
}
return false;
}
}
有篇博文对APT的运用很好的实践
@Repeatable
java8新增元注解,就是一语法糖。让一个注解可以在一个地方重复使用多次。JDK1.8以前:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FileType {
String fileSuffix() default ".java";
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FileTypes {
FileType[] value();
}
@FileTypes({@FileType(".java"),@FileType(".css"),@FileType(".html"),@FileType(".js")})
public void readFile(){
try {
FileType[] fileTypes= this.getClass().getMethod("readFile").getAnnotationsByType(FileType.class);
System.out.println("将从如下后缀名的文件中查找文件内容");
for (FileType fileType : fileTypes) {
System.out.println(fileType.value());
}
} catch (NoSuchMethodException | SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
这样解决了需求,但代码不宜读。用@Repeatable
```java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(FileTypes.class)
public @interface FileType {
String fileSuffix() default ".java";
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FileTypes {
FileType[] value();
}
@FileType(".java")
@FileType(".css")
@FileType(".html")
@FileType(".js")
public void readFile(){
try {
FileType[] fileTypes= this.getClass().getMethod("readFile").getAnnotationsByType(FileType.class);
System.out.println("将从如下后缀名的文件中查找文件内容");
for (FileType fileType : fileTypes) {
System.out.println(fileType.value());
}
} catch (NoSuchMethodException | SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
lamda表达式
@FunctionalInterface
有了这个注解注释的接口就可以完成lamda功能。例如执行注解value属性的策略API
@FunctionalInterface
interface ValueExtractor {
/**
* Extract the annotation attribute represented by the supplied {@link Method}
* from the supplied source {@link Object}.
*/
@Nullable
Object extract(Method attribute, @Nullable Object object);
}
ReflectionUtils::invokeMethod
由extract就知道该调用哪个invokeMethod
方法
JEP
这里的JEP是JDK增强计划书(JDK增强功能索引中涉及到编码层面的功能),详细内容查看官方JEP。
EdDSA
JEP 339:Edwards-Curve Digital Signature Algorithm (EdDSA)
爱德华兹曲线数字签名算法(EdDSA),和Schnorr,secp256k1, sm2 最大的区别在于没有使用随机数,这样产生的签名结果是确定性的,即每次对同一消息签名结果相同。一般说来随机数是安全措施中重要的一种方法,但是随机数的产生也是安全隐患,索尼的PS3 密钥泄露事件,就是随机数产生的问题导致的。签名过程中出现了这个相同的随机数 r,那么私钥将很容易被计算出来,造成泄露,当前很多兴起的区块链项目都在使用。
Sealed Classes(密封类)
JEP 360: Sealed Classes (Preview)
在 Java 语言中,代码的重用是通过类的继承实现的:多个子类可以继承同一个超类(并重用超类的方法)。但是重用代码并不是类层次结构的唯一目的,有时类层次结构仅仅是对某个领域的建模。以这种方式使用类层次结构时,限制子类集合可以简化建模。
比如在图形库中,Shape 类的开发者可能只希望有限个数的类扩展 Shape 类,开发者并不想为未知子类编写防御代码。以前的 Java 并不会限制 Shape 类的扩展属性,Shape 类可以拥有任意数量的子类。在封闭类(Sealed Classes)中,类层次结构是封闭的,但是代码仍然可以在有限范围内重用。
public sealed class Shape permits Circle, Rectangle, Square {
//类中内容
}
final class Circle extends Shape {}
//在子类数量很少时,在密封类的源文件中声明子类会很方便。密封类可以省略 permits子句
public sealed class Shape
{}
final class Circle extends Shape{
}
这个封闭接口的声明表明,只有 permits 关键字后面的类,包括 Circle, Rectangle, Square,能够实现这个接口。permits
关键字限定了封闭类或者封闭接口的可扩展范围。类修饰符 sealed 和 permits 关键字一起,使得封闭类和封闭接口只能在一个限定的、封闭的范围内,获得扩展性。
- 许可类可以声明为终极类(final),从而关闭扩展性;
- 许可类可以声明为封闭类(sealed),从而延续受限制的扩展性;
- 许可类可以声明为解封类(non-sealed), 从而支持不受限制的扩展性。
虚拟机层面支持
//InstanceKlass有成员变量
Array<jushort>* _permitted_subclasses;
尽管sealed关键字是类修饰符,但是ClassFile
中并没有 ACC_SEALED
标志。密封类的Class文件有PermittedSubclasses
属性(JVM规范中JDK15、16预览),该属性隐式指示密封修饰符,并显式指定允许的子类。
PermittedSubclasses_attribute {
//指向常量池中代表PermittedSubclasses字符串的索引
u2 attribute_name_index;
//属性长度,不包括最初的六个字节,也就是记录的是number_of_classes和classes长度
u4 attribute_length;
//封闭类允许子类个数,图中是2。和下一个属性classes中类的个数一致
u2 number_of_classes;
u2 classes[number_of_classes];
}
Records
JEP 359:Records (Preview)
数据载体Records
是Java的一种新的类型。同枚举一样,records也是对类的一种限制。records放弃了类通常享有的特性:将API和表示解耦。但是作为回报,records使数据类变得非常简洁。不能用abstract
修饰Record
类,可以用final
修饰Record
类,但是这没有必要,因为Record
类本身就是final
的。成员Record
类,还有本地Record
类,本身就是static
的,也可以用static
修饰,但是没有必要。
声明一个Records
- 声明顶级类:单独作为一个java文件
public record Student(long id, String name, int age) {}
- 作为一个成员内部类
public class ClassMates{
public record Student(long id, String name, int age) {}
}
- 作为局部内部类
public class ClassMates{
public void callTheRoll() {
record Student(long id, String name, int age) {}
}
声明内部元素
Record
类属性必须在头部声明,在Record
类体只能声明静态属性:
public record Student(long id, String name, int age) {
/**不是传参的,只能是字段,不包含在此类的Record属性中。作为参数传递进来
* 的会出现在Record属性,并且是final类型字段
*/
static long alaisId;
}
Record
类体可以声明方法(成员方法和静态方法都可以),和一般类一样。但是不能声明abstract
或者native
方法
public record Student(long id, String name, int age) {
public void test(){}
public static void test2(){}
}
- 类体也不能包含实例初始化块
public record Student(long id, String name, int age) {
//不能声明实列初始化块,但可以是静态块(初始化出来的实例都是它的成员变量,成员变量应该是static的,所以不能是非静态初始化块或者实例)
{
...
}
}
类体中只能声明静态字段,静态块。可以安全的发布对象,因为这些静态属性在类加载的准备阶段就已经初始化。如果非静态需要等到类初始化的时候才能被初始化。可能会影响对象的发布和使用。同时也是严格区分了Record
类和普通类。否则没有这些约束,和我们声明POJO
对象没有任何区别。静态字段可以作为一个Record
通用的属性,比如状态控制等。
protected List<JCTree> classOrInterfaceOrRecordBodyDeclaration(Name className, boolean isInterface, boolean isRecord) {
...
//标记
(token.kind == LBRACE &&
(mods.flags & Flags.StandardFlags & ~Flags.STATIC) == 0 &&
mods.annotations.isEmpty()) {
if (isInterface) {
log.error(DiagnosticFlag.SYNTAX, token.pos, Errors.InitializerNotAllowed);
} else if (isRecord && (mods.flags & Flags.STATIC) == 0) {
log.error(DiagnosticFlag.SYNTAX, token.pos, Errors.InstanceInitializerNotAllowedInRecords);
}
}