Java基础篇--反射和注解

it2023-02-19  84

目录

前言

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对象的方法,获取该类的指定参数的构造方法;

反射的步骤

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

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

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

Source源代码阶段:javac编辑类文件为字节码, 其中成员变量是一类,构造方法一类,成员方法一类;Class类对象阶段:进入内存,封装成class对象:成员变量Field【】;构造方法 Constructor【】;成员方法 Method【】;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()。


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

最新回复(0)