advanced java (九) 注解和反射

advanced java (九) 注解和反射

注解是一种 元数据(matadata),元数据为描述数据的数据,主要是描述数据属性的信息。

一般来说注解提供了四个注解对自定义注解进行配置,分别是

在 java当中一般会使用反射来对注解过的对象做处理,比如说 Spring 框架里面的 @Bean

同样 java 的反射机制很强大,不仅可以获取到类,类加载器,方法名,私有变量等等。同样还可以通过 invoke 来调用方法,甚至是一些 private 修饰的方法。

java 中使用的是 双亲委派模型 (parents delegate model),实现对 class 的加载。

最后会说一下 Spring 框架中的 BeanFactoryFactoryBean 是通过反射来注册 Bean 的,以及 Spring 框架中 aop 和 ioc 思想。

元注解

当声明使用 @interface ,就可以自定义注解,注解本身没有意义,一般和反射搭配使用,常见的就是 Spring 框架。

@Documented

一般用于文档,会被 javadoc 之类的工具处理。

@Target

ElementType 范围
TYPE 用于描述接口、类、枚举、注解
FIELD 用于描述字段、枚举的常量
METHOD 用于描述方法
PARAMETER 用于描述方法参数
CONSTRUCTOR 用于描述构造器
LOCAL_VARIABLE 用于描述局部变量
ANNOTATION_TYPE 用于描述注解
PACKAGE 用于描述包
TYPE_PARAMETER ( java8 ) 用于描述泛型参数
TYPE_USE ( java8 ) 只要是形态名称,都可以进行标注

说明了注解所修饰的对象范围

@Retention

RetentionPolicy 范围
SOURCE 注解仅存在于源码中,在class字节码文件中不包含
CLASS (默认) 注解会在 class 字节码文件中存在,但 Class Loader 不会加载,运行时无法获得
RUNTIME 注解会在 class 字节码文件中存在,在运行时可以通过反射获取

注解有三种生命周期,比如说 @Override 一般用于检查重载,所以它的生命周期是 SOURCE 。而CLASS 一般会和注解处理器(Annotation Processor) 一起使用,它是 javac 的一个工具,它用来在编译时扫描和处理注解。而大多数需要使用反射接口的注解,如 Spring 框架里面的大量注解,会使用RUNTIME

@Inherited

Inherited作用是,使用此注解声明出来的自定义注解,在使用此自定义注解时,如果注解在类上面时,子类会自动继承此注解,否则的话,子类不会继承此注解。只对类有效。

使用注解

注解不支持继承,在注解中可以声明各种类型的变量。可以在注解中声明枚举类型,可以使用泛型,也可以对注解嵌套。可以声明类型或者类型的数组。

@Target({ElementType.LOCAL_VARIABLE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface AwesomeAnnotation{
    enum Status {FIXED,NORMAL};
    String[] value();
    String desc() default "";
    Class<? extends Annotation> annotation() default  Annotation.class;
    Status status() default  Status.NORMAL;
    Retention retention() default @Retention(RetentionPolicy.RUNTIME);
}

如果没有设定默认值,那么使用注解的时候必须要提供元素的值。 value 可以省略 key=value 的语法,这被称作快捷方式。

@AwesomeAnnotation({"awesome","good"})
String s1;
@AwesomeAnnotation(value = "awesome",desc = "s2",annotation = AwesomeAnnotation.class)
String s2;
@AwesomeAnnotation(value = "awesome",status = AwesomeAnnotation.Status.FIXED)
String s3;
@AwesomeAnnotation(value = "awesome",retention = @Retention(RetentionPolicy.CLASS))
String s4;

在 java8 中,新增了对类型注解和重复注解的支持。

类型注解也就是 TYPE_PARAMETERTYPE_USE

重复注解可以在同一方法、属性、类等类型中多次使用同一个注解。这样会比之前的写法更简单方便,可读性更强。

之前的写法

public @interface SubAnnotation {
    String value();
}

数组保存注解

public @interface RepeatableAnnotations {
    SubAnnotation[] value();
}

使用多个注解

@RepeatableAnnotations({@SubAnnotation("sub1"),@SubAnnotation("sub2")})
String s1;

而 java8 就简化了这种写法

使用 @Repeatable

@Repeatable(RepeatableAnnotations.class)
public @interface SubAnnotation {
    String value();
}

使用多个注解

@SubAnnotation("sub1")
@SubAnnotation("sub2")
String s1;

反射

反射是 java 中一个十分强大的特性,运行时可以检测和修改它本身状态或行为,也可以获取元数据信息。可以用来简化代码,或者一些强大的功能,使代码更加灵活。不过反射的运行效率并不高。

反射其实是语义同像(Semantic Homoiconicity)的一种体现。反射将对程序以透明的机制,暴露给了开发者,让我们有能力去操纵它,能够更灵活地完成工作。虽然一定程度上破坏了封装,但是任何事物都有两面性。封装建立抽象屏障,降低程序的复杂度。事实上通过反射,将很多难以预测的问题留到运行时,动态地去解决。也就是说,给我们关闭了正面的大门,却留了一条后路。

获取 Class 类有三种方法

Class aClass = String.class;
String str = new String("Hello");
Class getClass = str.getClass();
Class forNameClass = Class.forName("java.lang.String");

可以通过 Class 获取构造方法对象 ,并新建对象

可以获取相应 Class 的局部变量

可以获取对象的方法,并调用对象上相应方法

可以获取运行时的注解

class Apple implements Serializable{
    public Apple(int wight, String color) {
        this.wight = wight;
        this.color = color;
    }
    protected static String type = "Fruit";
    private int wight = 100;
    @NotNull // RetentionPolicy.CLASS
    @Resource(name = "color") // RetentionPolicy.RUNTIME
    public String color = "red";
    private void setWight(int wight) {
        this.wight = wight;
    }

    @Override
    public String toString() {
        return "Apple{" +
                "wight=" + wight +
                ", color='" + color + '\'' +
                '}';
    }
}

调用反射接口创建对象

Class aClass = Apple.class;

Apple apple = (Apple) aClass.getConstructor(int.class,String.class).newInstance(50 ,"green");
System.out.println(apple);
//Apple{wight=50, color='green'}

获取局部变量,需要注意的是 getFields() 只会获取 public 修饰的字段

Field[] fields = aClass.getDeclaredFields();
Arrays.asList(fields).forEach(f -> {
    System.out.println(f.getType() +  ": [" + f.getName() + "]  modifier:" + f.getModifiers() );
});

//int: [wight]  modifier:2
//class java.lang.String: [color]  modifier:1
//class java.lang.String: [type]  modifier:12

System.out.println(aClass.getFields().length);
// 1

modifier 表示该字段的修饰符,规则如下

当然 强大的 java 反射机制甚至可以调用方法,在某些场景下,可以绕开 java 的限制,调用私有方法

其中 getMethods() 是获取本类和父类,父接口的所有 public 修饰的方法,而 getDeclaredMethods() 获取本类中 privateprotected 、默认以及 public 修饰的方法。

需要注意的是,如果要调用私有方法,需要修改 showPrivate 权限,不然会抛出 IllegalAccessException: Class hello can not access a member of class Apple with modifiers "private" 的异常

Class aClass = Apple.class;
Apple apple = new Apple(70,"yellow");
Method method = aClass.getDeclaredMethod("setWight",int.class);
method.setAccessible(true);
method.invoke(apple,120);
System.out.println(apple.toString());
// Apple{wight=120, color='yellow'}

同时反射可以获取运行时的注解,需要注意的是,只有运行时的注解( RetentionPolicy.RUNTIME )才会存在 runtime 阶段,而注解里面的局部变量可以继续通过反射获得。

getDeclaredAnnotations()getAnnotations() 的区别在于 getDeclaredAnnotations() 会忽略继承的注解,也就是 父类存在的 @Inherited 修饰的注解。

Class aClass = Apple.class;
Field color = aClass.getDeclaredField("color");
System.out.println(color.getAnnotations().length); //1  因为只有 @Resource 是运行时注解, 而 @NotNull 不是
Resource resource =  color.getAnnotation(Resource.class);
System.out.println(resource.name()); //color

同样可以获取实现了哪些接口

Class aClass = Apple.class;
Arrays.asList(aClass.getInterfaces()).forEach(i -> {
    System.out.println(i.getName());
});
// java.io.Serializable

Enclosing 修饰的反射方法的使用对象是内部类或匿名内部类,内部类或匿名内部类可以通过 getEnclosingClass() 获取外部类, 通过 getEnclosingConstructor() 获取被使用的构造方法, getEnclosingMethod() 获取被哪个方法定义。

当然,反射还有处理数组和枚举类的API。

还可以通过 getClassLoader() 获取类加载器。

通过反射,就可以通过输入一些字段,达到构建一个对象的目的。这也就是说,通过一个 xml 配置文件,就能够生成一个 Bean 实例,这也就是 Spring 框架的一部分原理。

双亲委派模型

类加载过程

在编译成字节码后,虚拟机将 CLASS 文件加载到内存,然后验证、准备,解析(这三个阶段统称为连接)和初始化,最后形成能被虚拟机使用的 java 类型,这就是虚拟机的类加载机制。

类加载的时机

类加载的生命周期

类加载器

在 java 中 ,有三个Class Loader类型

双亲委派模式 (parents delegation model)就是说在类加载请求的时候,总是由父类加载器去完成加载任务,只有父类加载器无法完成时,子类加载器才会尝试自己去加载。

采用双亲委派模式的是好处是 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载。

aop和ioc

aop和ioc思想

依赖注入(Dependency Injection) 和 控制反转(Inversion of Control)是 Spring 框架中很重要的两个知识点。依赖注入是种实现控制反转用于解决依赖性的设计模式。目的是解耦。

依赖注入是一种设计模式,目的是解除程序组件之间的一些直接耦合。也就是依赖注入到 IOC 容器,把有依赖关系的类放入容器中。

在 Spring 框架当中,具体的做法是使用 BeanFactoryBeanBeanFactoryBean 就是由 Spring 容器初始化、装配及管理的对象。将原本的 Bean 类依赖 Bean 的具体实现 ,改成 Bean 类依赖一个接口和一个 IOC 容器。IOC 容器也就是 BeanFactory ,用于实例化、定位、配置应用程序中的对象及建立这些对象间的依赖,同时 Bean 的具体实现内容 ,也就是 FactoryBean , 实现了 Bean 类的接口。

然后将 Bean 的具体实现的创建工厂 ( FactoryBean ) 注册到 IOC 容器 ( BeanFactory ),最后 Bean 只需要给IOC 容器 ( BeanFactory ) 一个标识索取 Bean ,IOC 容器将回调 Bean 的具体实现的创建工厂 ( FactoryBean ) ,生产出 BeanFactory 的实例之后交给开发者。

控制反转是一种设计代码的思路。

举一个例子来说,就是框架(Framework)和库(Library)的区别。在我们使用库的时候,会去主动调用库里面的方法,来达到我们的目的。而 Spring 框架做到了控制反转。也就是说,库是一种 “正向” 控制,而框架是反过来调用开发者的代码。

BeanFactory和FactoryBean的区别

BeanFactory 是接口 , 一般使用的高级容器 ApplicationContext 接口继承了这个接口( 其实是继承了多个接口,其中 ListableBeanFactory, HierarchicalBeanFactory 都继承了 BeanFactory ),所有Bean 由它来管理。

FactoryBean 在IOC容器的基础上给 Bean 的实现加上了一个简单工厂模式和装饰模式,让我们可以获取 Bean ,是通过反射机制实现的,很多 Bean 类都通过 FactoryBean 生产出实例。

ApplicationContext 继承的接口,如 EnvironmentCapableMessageSourceApplicationEventPublisherResourcePatternResolver 等描述了它的意义。 同样在 Spring 基础框架里面,有很多不同类型的 FactoryBean 实现不同的功能,通过注入 Bean 的形式。同时 IOC 容器 还会管理 Bean 类之间的依赖关系。