Java基础篇--反射和注解

目录

前言

Java的反射机制

反射机制的概念

***:什么场合需要用到反射?

Java反射API

反射的步骤

***:获取Class对象的3种方法

***:Class.forName()和ClassLoader.loadClass()有什么区别?

***:程序判断题(forName和loadClass执行,与static代码块的执行关系)

***:反射创建对象的两种方法

java注解

注解的概念

四种标准元注解

***:如何实现自定义注解?

***:(实战)模拟spring框架,简单实现IOC


前言

带着问题学java系列博文之java基础篇。从问题出发,学习java知识。


Java的反射机制

反射机制的概念

反射机制的概念

Java中的反射机制是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。

***:动态语言

动态语言,是指程序在运行时可以改变其结构:新的函数可以引进,已有的函数可以被删除等结构上的变化。比如常见的 JavaScript 就是动态语言,除此之外 Ruby,Python 等也属于动态语言,而 C、 C++则不属于动态语言。 从反射角度说 JAVA 属于半动态语言。

***:什么场合需要用到反射?

java程序中对象在运行时总是会出现两种类型:编译时类型和运行时类型。编译时类型由声明对象的类型来决定,运行时类型则由实际赋值给对象的类型决定。例如:Person p = new Student(); 其编译时类型为Person,运行时类型为Student。很显然,从Person类型是无法获取Student类型的具体方法。此外,有时候程序在运行时还可能接收到外部传入的对象,该对象的编译时类型为 Object,但是程序又需要调用该对象的运行时类型的方法。为了解决这些问题, 程序需要在运行时发现对象和类的真实信息。然而,如果编译时根本无法预知该对象和类属于哪些类,程序只能依靠运行时信息来发现该对象和类的真实信息,此时就必须使用到反射了。

编程中实际使用反射的案例主要有:数据库连接时根据全类名加载驱动;动态代理时通过反射获取被代理对象的方法;注解处理器获取注解的内容;诸多框架(spring的ioc)等

Java反射API

class类:反射的核心类,可以获取类的属性、方法等信息;

Field类:表示类的成员变量,可以用来获取和设置类之中的属性值;

Method类:表示类的方法,可以用来获取类中的方法信息或者执行方法;

Constructor类:表示类的构造方法,可以用来初始化对象;

getDeclaredFields():Class对象的方法,获取该类的所有属性(包含私有属性),注意配合setAccessible()开放安全限制;

getFields():Class对象的方法,获取该类的属性(仅public修饰的属性);

getDeclaredMethods():Class对象的方法,获取该类的所有方法(包含私有方法);

getMethods():Class对象的方法,获取该类的方法(仅public修饰的方法);

getConstructor(可选参数):Class对象的方法,获取该类的指定参数的构造方法;

反射的步骤

  1. 获取想要操作的类的 Class 对象,他是反射的核心,通过 Class 对象我们可以任意调用类的方法;
  2. 调用 Class 类中的方法(反射的使用阶段);
  3. 使用反射 API 来操作这些信息

***:获取Class对象的3种方法

在讲三种方法之前,我们先了解一下java的三大阶段:

  1. Source源代码阶段:javac编辑类文件为字节码, 其中成员变量是一类,构造方法一类,成员方法一类;
  2. Class类对象阶段:进入内存,封装成class对象:成员变量Field【】;构造方法 Constructor【】;成员方法 Method【】;
  3. Runtime运行时阶段:解析为具体的对象实例;

其中从阶段1到阶段2就是反射的过程。获取Class对象的三种方法也是分别与这三个阶段对应的:

    //获取class对象的三种方式
    private static void testGetClass() {
        Class<Person> personClass = null;
        //Class.forName() 和 ClassLoader.loadClass() 的区别:
        // 1.前者的过程是:加载,连接,初始化  2.后者的过程是加载
        // 因此前者会进入类对象阶段,执行初始化类的静态变量和静态代码块;后者则不会
        try {
            //第一种方法,对应第一个阶段
            personClass = (Class<Person>) Class.forName("com.zst.javabasedemo.collection.Person");
            System.out.println(personClass);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        try {
            Class<?> aClass = ClassLoader.getSystemClassLoader().loadClass("com.zst.javabasedemo.collection.Person");
            System.out.println("classloader:"+aClass);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        //第二种方法,对应第二个阶段
        Class personClass1 = Person.class;
        System.out.println(personClass1);

        //第三种方法,对应第三阶段
        Person person = new Person("zhangsan",28);
        Class personClass2 = person.getClass();
        System.out.println(personClass2);

        //注意,通过对比,三种方式获取的对象是相同的,说明同一个字节码文件在一次程序运行中,加载且仅加载一次,内存中仅保存一份该class对象
        System.out.println("class1 == class2 == class3 ? "+(personClass == personClass1 && personClass == personClass2));
    }
  • 第一种方法:Class.forName(全类名)或者ClassLoader.loadClass(全类名)

这种方法对应的是三大阶段的第一个阶段,此时是类文件,需要进行加载类文件,获取class对象。这种方法是最安全、性能最好的,推荐使用。

  • 第二种方法:Person.class

这种方法对应的是第二个阶段,此时jvm已经加载了类文件进内存,已持有Person类对象,所以此时只需要直接获取类对象的class属性即可。

  • 第三种方法:person.getClass()

这种方法对应的是第三个阶段,此时jvm已经解析初始化完成,创建了具体的对象实例,所以只需要通过对象实例的getClass方法获取该对象实例的class属性即可。

如代码范例中的注释,三种方法获取到的class对象是完全相同的,这也说明了同一份字节码文件在一次程序运行过程中,jvm加载且仅加载一次,内存中仅有一份该class对象。

***:Class.forName()和ClassLoader.loadClass()有什么区别?

Class.forName()的执行会直接走完三大阶段,首先加载字节码文件,然后连接,再初始化(加载->连接->初始化 是java的类加载机制,详见《Java基础篇--JVM》);而初始化阶段,jvm会为该类对象分配内存空间,给变量赋默认值,执行静态代码块,类对象进入内存;

ClassLoader.loadClass()的执行只会加载字节码文件,最多由于参数resolve,再执行一步连接,没有类对象进入内存;

Class.forName(String,boolean,ClassLoader),可以通过参数指定类加载器;而ClassLoader.loadClass()就是由当前执行方法的类加载器加载。

***:程序判断题(forName和loadClass执行,与static代码块的执行关系)

public class Person {
    public static String name;
    public int age;

    static {
        name = "default";
        System.out.println("static is running");
    }
}

    public void testForName() throws Exception{
        Class<Person> person = (Class<Person>) Class.forName("com.zst.javabasedemo.test.Person");
        System.out.println("----------------------");
        Field name = person.getField("name");
        System.out.println(name.get(person));
    }

    public void testLoadClass() throws Exception {
        Class<Person> person2 = (Class<Person>) ClassLoader.getSystemClassLoader().loadClass("com.zst.javabasedemo.test.Person");
        System.out.println("----------------------");
        Field nameField = person2.getField("name");
        System.out.println(nameField.get(person2));
    }

testForName()方法执行的结果是什么?testLoadClass()方法执行的结果是什么?

出现上图的现象,就是因为forName()方法会直接走过加载、连接和初始化,静态代码块会执行,所以先输出static is running;

而loadClass()方法仅仅是加载(最多到连接),静态代码块不会执行,只有等到nameField.get(person2)时,才会执行初始化,所以先输出分隔线。

***:反射创建对象的两种方法

  • Class.newInstance()

获得Class对象后,直接调用Class对象的静态方法newInstance()创建对象实例。注意,该方法可以正确执行的前提是Class对象对应的类有默认的空构造器。

  • Constructor.newInstance()

通过Class对象获取对应类的构造器Constructor,然后再调用Constructor的静态方法newInstance()创建对象实例。这种方法的好处是可以选择带参数的构造器,直接在创建对象实例时给对应属性赋初值。注意,这种方法获取构造器时,要确保参数类型与类中的带参构造器完全一致,否则将抛出NoSuchMethodException异常;且在执行newInstance()方法时,也要传入相同类型的初值

 

java注解

注解的概念

Annotation(注解)是 Java 提供的一种对程序中元素关联信息和元数据(metadata)的途径和方法。 Annatation(注解)是一个接口,程序可以通过反射来获取指定程序中元素的 Annotation对象,然后通过该 Annotation 对象来获取注解中的元数据信息。

四种标准元注解

元注解的作用就是负责注解其他注解。Java5定义了4个标准的元注解类型,被用来提供对其它注解的类型作说明。

  • @Target:指定注解的作用范围

可设置的范围由枚举类ElementType限定,主要有TYPE(限定注解可用于类、接口、枚举、Annotation),FIELD(限定注解可用于属性),METHOD(限定注解可用于方法),PARAMETER(限定注解可用于参数),CONSTRUCTOR(限定注解可用于构造器),PACKAGE(限定注解可用于包)等

  • @Retention:指定注解的有效阶段

可设置的阶段由枚举类RetentionPolicy限定,主要有SOURCE(源代码阶段),CLASS(class类对象阶段)和RUNTIME(运行时阶段),该注解可以描述注解的生命周期,表明注解生命周期

  • @Documented:描述javadoc

该注解表明javadoc生成api文档的时候将保留注解信息

  • @Inherited:表明注解可以被继承

该注解表明子类可以继承父类的这个注解,通过反射同样可以拿到注解的元数据

***:如何实现自定义注解?

/**
 * 注解的本质是一个继承了java.lang.annotation.Annotation的接口
 * 它的抽象方法就是定义属性,支持8种基本类型,枚举,注解,(以上类型)数组
 * 如果只有一个方法,即默认一个属性,此时可以不用再写名称
 * 也可以指定方法默认值,则可以在注解使用时不需要都赋值
 * 注解中有一个重要的属性value,当只配置一个值时,可以默认不写名称“value”
 */
@Target(ElementType.METHOD) //元注解 ,标明注解的使用范围是方法
@Retention(RetentionPolicy.RUNTIME) //元注解,标明注解保留的时间范围是运行时(对应java3个阶段,source,class,runtime)
@Documented  //元注解,标注javadoc api文档将保留注解信息
@Inherited  //元注解,具有继承性(父类使用了该注解,子类将继承该注解,通过反射可以拿到)
public @interface MyAnno {
    String name();
    int age();
    String[] sports() default {"足球","篮球"};
}

如上代码,实现了一个自定义的注解MyAnno。其实自定义注解很简单,首先注解的本质是一个接口(继承了Annotation接口),我们只需要用@interface 注解修饰接口类即可,无需单独继承Annotation接口;然后就是利用四个元注解,限定自定义注解的作用范围、有效阶段、是否保留进api文档以及是否可以继承;最后就是根据需要,自定义抽象方法,抽象方法就是定义元数据。

那定义了注解,如何让注解发挥实际的作用呢?这时就需要注解处理器,自定义注解就需要我们自己实现自定义注解处理器。

***:(实战)模拟spring框架,简单实现IOC

1.首先自定义两个注解:Autowired和Bean,用于属性自动注入和自动初始化对象实例,并存入bean容器

/**
 * 自定义注入注解
 * 用于属性自动注入
 * 可被继承
 * 默认元数据name=“”
 * 当给name赋值时,则根据name自动注入;否则按照属性的名称(名称限定:属性的类型名、首字母小写)自动注入
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Autowired {
    String name() default "";
}


/**
 * 自定义bean初始化注解
 * 用于自动初始化bean实例
 * 该注解用于类,表示该类被bean工厂接管,会自动创建单例的实例
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Bean {
}

 

2.然后实现自定义注解处理器,用于处理自定义的两个注解。主要是两个方法:initBean和loadAnno。initBean用于bean实例化,并存入bean容器;loadAnno用于自动注入属性。

/**
 * 自定义注解处理器,用于处理自定义的注解Autowired和Bean
 */
public class MyAnnoLoad {

    //bean容器
    private static ConcurrentHashMap<String,Object> beanMap = new ConcurrentHashMap<>();

    /**
     * 属性注入
     * @param field 属性对象
     * @param bean 要注入的对象实例
     * @param name 注入的属性在bean容器中的名称
     */
    public static void loadAnno(Field field,Object bean,String name){
        try {
            //开放权限
            field.setAccessible(true);
            //如果指定bean名称,则按照指定的名称去注入,未指定则按照属性名来注入(所以要求属性名是类型名首字母小写)
            if (null == name || name.trim().length() == 0 ) {
                //容器中未找到,说明没有这个对象实例,先初始化一个
                if (!beanMap.containsKey(field.getName())) {
                    initBean(field.getType(), field.getName());
                }
                //将对象实例注入到属性
                field.set(bean, beanMap.get(field.getName()));
            } else {
                if (!beanMap.containsKey(name)){
                    initBean(field.getType(),name);
                }
                System.out.println("Autowired注解指定了name:"+name);
                field.set(bean,beanMap.get(name));
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    /**
     * 初始化bean
     * @param clazz Class对象
     * @param name bean在容器中的名称
     * @return
     */
    public static Object initBean(Class<?> clazz,String name){
        Object bean = null;
        try {
            //仅对使用Bean注解的类进行初始化对象
            if (clazz.isAnnotationPresent(Bean.class)){
                bean = clazz.newInstance();
                Field[] fields = clazz.getDeclaredFields();
                for (Field field : fields) {
                    //遍历该类的成员属性,如果发现存在Autowired注解的属性,则先注入该属性
                    if (field.isAnnotationPresent(Autowired.class)){
                        //得到注解类,获取注解的元数据
                        Autowired autowired = field.getAnnotation(Autowired.class);
                        loadAnno(field,bean,autowired.name());
                    }
                }
            }

            //未指定name参数,则默认类型名首字母小写作为key,保存进beanMap
            if (null == name || name.trim().length() == 0){
                name = toLowerCaseFirstOne(clazz.getSimpleName());
            }
            beanMap.put(name,bean);
            System.out.println("key:"+name+",bean:"+bean);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return bean;
    }

    /**
     * 对string字串的首字母进行小写转换
     * @param s
     * @return
     */
    public static String toLowerCaseFirstOne(String s){
        if(Character.isLowerCase(s.charAt(0)))
            return s;
        else
            return (new StringBuilder()).append(Character.toLowerCase(s.charAt(0))).append(s.substring(1)).toString();
    }
}

 

3.使用注解实现业务逻辑,这里完全模拟spring mvc,从mapper到service,再到controller,首先用bean注解,让框架自动初始化bean,然后使用Autowired注解实现属性的注入。代码范例简单的实现了spring的IOC,且支持根据自定义名称和默认类名两种方式注入。

/**
 * 模拟数据库操作类mapper
 */
@Bean
public class UserMapper {

    public List<String> getAll(){
        List<String> users = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            users.add("user"+i);
        }
        return users;
    }
}



/**
 * 模拟service
 * 利用注入的mapper实现具体的业务逻辑
 */
@Bean
public class MyService {

    //使用自定义注解,注入属性
    @Autowired
    private UserMapper userMapper;

    public List<String> findAll(){
        return userMapper.getAll();
    }
}



/**
 * 模拟controller
 * 利用注入的service,实现具体的业务逻辑
 */
@Bean
public class MyController {

    //使用自定义注解注入属性,且指定了name
    @Autowired(name = "service")
    private MyService myService;

    /**
     * 模拟服务启动,首先加载bean实例,注入属性
     * 再模拟业务方法调用
     * @param args
     */
    public static void main(String[] args) {
        MyController controller = (MyController) MyAnnoLoad.initBean(MyController.class,null);
        controller.findAll();
    }

    public void findAll(){
        List<String> userList = myService.findAll();
        for (String name : userList) {
            System.out.println(name);
        }
        System.out.println("执行成功");
    }
}

执行结果如下图:

可以看到框架自动初始化了三个对象实例,且按照键值对保存进了bean容器(ConcurrentHashMap);由于在contronller中指定了名称来注入,所以结果中也有所体现,存入容器时,使用的key就是指定的名称,否则就是该类型名的首字母小写作为key。然后通过autowired注解依次注入属性,所以controller调用service,service调用userMapper都是可以的,成功执行了userMapper.getAll()。


以上系个人理解,如果存在错误,欢迎大家指正。原创不易,转载请注明出处!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值