JAVAOOP-21 反射
XML
什么是XML
XML是一种可扩展的标记语言
,标准通用标记语言的子集 ,从 HTML —> XHTML —> XML 发展而来。
使用的时候是以文件的形式存在,扩展名为xml。
XML文件的用途
数据交换
:作为与编程语言、操作系统无关的数据格式,通过网络进行数据的交换。数据存储
:通常作为超小型数据库来看待,可以存储数据。配置文件
:配置文件本质上就是数据存储的一种应用,主要用于应用程序的配置存储。文件格式
:office文件也是使用xml技术进行了数据的存储,这种应用方式比较特殊。
在JavaEE中,会使用XML类型的文件进行配置信息的存储;在后期的框架学习,也会大量使用XML进行框架的配置。
XML文件的格式
xml文件通常会在第一行写入 声明
:
<?xml version="1.0" encoding="UTF-8"?>
version是版本号,encoding是当前XML文件使用的编码。
在声明下,可以由自定义的标签组合。标签名可以用中文。第一行的标签称为根元素
<书店>
</书店>
在根节点里面可以添加子元素,每个元素里面可以添加属性,然后为属性赋值。
这种结构和HTML其实是完全一样的,只不过所有的标签都是自己定义的
示例
<书店>
<书 书名="从入门到放弃" price="33" 作者="喵先生"/>
</书店>
四种解析方式及优缺点
在Java中解析XML有4种方案:
- DOM:优点是直观好理解,缺点是DOM解析需要一次性的解析整个xml文件,容易出现内存溢出
- SAX:采用事件驱动,内存消耗小,无法同时访问多处不同的数据
- DOM4J:操作XML的一个工具包,本次案例使用的就是DOM4J进行演示
- JDOM:可以操作XML的一个工具包
DOM4J 的基础使用
DOM4J是 dom4j.org 出品的一个开源 XML 解析包。DOM4J应用于 Java 平台,采用了 Java 集合框架并完全支持 DOM,SAX 和 JAXP。
读取常用操作
-
读取XML文件
Document document = new SAXReader().read(new File("beans.xml"));
-
获取根节点
Element rootElement = document.getRootElement();
-
获取某个子节点
Element memberElm=root.element("member");// "member"是节点名
-
获取节点的文本
String text=memberElm.getText(); String text=root.elementText("name"); //这个是取得根节点下的name字节点的文字
-
取得某节点下指定名称的所有节点并进行遍历
List nodes = rootElm.elements("member"); for (Iterator it = nodes.iterator(); it.hasNext();) { Element elm = (Element) it.next(); // do something }
删除操作
parentElm.remove(childElm); // childElm是待删除的节点,parentElm是其父节点
添加操作
-
创建根节点
//创建一个文档对象 Document document = DocumentHelper.createDocument(); //跟节点 Element rootElement = document.addElement("beans");
-
创建子节点
Element bean = rootElement.addElement("bean");
属性操作
-
取得节点的指定的属性
Element root = document.getRootElement(); Attribute attribute=root.attribute("size"); // 属性名name
-
取得属性的文字
String text=attribute.getText(); String text2=root.element("name").attributeValue("firstname"); //这个是取得根节点下name字节点的firstname属性的值.
-
设置某节点的属性和文字
newMemberElm.addAttribute("name", "sitinspring");
-
设置属性的文字
Attribute attribute = root.attribute("name"); attribute.setText("sitinspring");
-
删除某属性
Attribute attribute=root.attribute("size");// 属性名name root.remove(attribute);
将文档写入XML文件
文档中含有中文,设置编码格式再写入
OutputFormat format = OutputFormat.createPrettyPrint();
format.setEncoding("GBK"); // 指定XML编码
XMLWriter writer = new XMLWriter(new FileWriter("output.xml"),format);
writer.write(document);
writer.close();
字符串与XML的转换
-
将字符串转化为XML
String text = "<members> <member>sitinspring</member> </members>"; Document document = DocumentHelper.parseText(text);
-
将文档或节点的XML转化为字符串
SAXReader reader = new SAXReader(); Document document = reader.read(new File("input.xml")); Element root=document.getRootElement(); String docXmlText=document.asXML(); String rootXmlText=root.asXML(); Element memberElm=root.element("member"); String memberXmlText=memberElm.asXML();
反射:框架设计的灵魂
生活中我们经常使用镜子,在镜子中可以看到现实世界的映射。反射机制有点类似镜子,可以在没有加载类的前提下,得到一个类的 结构,并可以为其创建对象,调用方法!
当我们开发程序时,在一个类中需要使用多个类,比如20个,但是这20个类是作为参数传递进来的,不清楚会传递哪个类,这个时候我们会用什么技术呢 ?当然是多态!
我们使用接口为这20个类都提供相同的方法就可以解决这个问题。但是如果类更多,当前接口也没有办法呢?使用接口继承么?如果200个类呢?如果使用的类都已经打 jar 包了,不能直接修改代码怎么办?
使用反射可以在代码没有侵入性的前提下,完成对目标对象的创建和使用。
要想理解反射的原理,首先要了解什么是类型信息。Java让我们在运行时识别对象和类的信息,主要有两种方式
- RTTI(Run-Time Type Identification),它假定我们在编译时已经知道了所有的类型信息;
- 反射机制,它允许我们在运行时发现和使用类的信息。
使用反射的前提条件:必须先得到代表的字节码的 Class,Class类用于表示.class文件(字节码)
反射的概述
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。
要想解剖一个类,必须先要获取到该类的字节码文件对象。而解剖使用的就是Class类中的方法。所以先要获取到每一个字节码文件对应的Class类型的对象.。反射就是把Java类中的各种成分映射成一个个的Java对象。
例如:
一个类有:成员变量、方法、构造方法、包等等信息,利用反射技术可以对一个类进行解剖,把各个组成部分映射成一个个对象。 (其实:一个类中这些成员方法、构造方法,在加入类中都有一个类来描述)
反射的理解
反射之中包含了一个「反」字,所以想要解释反射就必须先从「正」开始解释。
一般情况下,我们使用某个类时必定知道它是什么类,是用来做什么的。于是我们直接对这个类进行实例化,之后使用这个类对象进行操作。
示例:
Phone phone = new Phone(); //直接初始化,「正」
phone.setPrice(4);
上面这样子进行类对象的初始化,我们可以理解为「正」。
而反射则是一开始并不知道我要初始化的类对象是什么,自然也无法使用 new 关键字来创建对象了。
这时候,我们使用 JDK 提供的反射 API 进行反射调用:
Class clz = Class.forName("com.melrin.demo.reflect.Phone");
Method method = clz.getMethod("setPrice", int.class);
Constructor constructor = clz.getConstructor();
Object object = constructor.newInstance();
method.invoke(object, 4);
上面两段代码的执行结果,其实是完全一样的。但是其思路完全不一样,第一段代码在未运行时就已经确定了要运行的类(Phone),而第二段代码则是在运行时通过字符串值才得知要运行的类。
所以说,反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法
完整示例:
public class Phone {
private int price;
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public static void main(String[] args) throws Exception{
//正常的调用
Phone phone = new Phone();
phone.setPrice(5000);
System.out.println("Phone Price:" + phone.getPrice());
//使用反射调用
Class clz = Class.forName("com.melrin.demo.reflect.Phone");
Method setPriceMethod = clz.getMethod("setPrice", int.class);
Constructor phoneConstructor = clz.getConstructor();
Object phoneObj = phoneConstructor.newInstance();
setPriceMethod.invoke(phoneObj, 6000);
Method getPriceMethod = clz.getMethod("getPrice");
System.out.println("Phone Price:" + getPriceMethod.invoke(phoneObj));
}
}
从代码中可以看到我们使用反射调用了 setPrice 方法,并传递了 6000 的值。之后使用反射调用了 getPrice 方法,输出其价格。上面的代码整个的输出结果是:
Phone Price:5000
Phone Price:6000
从这个简单的例子可以看出,一般情况下我们使用反射获取一个对象的步骤:
//获取类的 Class 对象实例
Class clz = Class.forName("com.melrin.demo.api.Phone");
//根据 Class 对象实例获取 Constructor 对象
Constructor phoneConstructor = clz.getConstructor();
//使用 Constructor 对象的 newInstance 方法获取反射类对象
Object phoneObj = phoneConstructor.newInstance();
而如果要调用某一个方法,则需要经过下面的步骤:
//获取方法的 Method 对象
Method setPriceMethod = clz.getMethod("setPrice", int.class);
//利用 invoke 方法调用方法
setPriceMethod.invoke(phoneObj, 6000);
到这里,我们已经能够掌握反射的基本使用。但如果要进一步掌握反射,还需要对反射的常用 API 有更深入的理解。
反射的基础
在 JDK 中,反射相关的 API 可以分为下面几个方面:获取反射的 Class 对象、通过反射创建类对象、通过反射获取类属性方法及构造器。
获取反射中的Class对象
在反射中,要获取一个类或调用一个类的方法,我们首先需要获取到该类的 Class 对象。在 Java API 中,获取 Class 类对象有三种方法:
-
使用 Class.forName 静态方法。当知道某类的全路径名时,可以使用此方法获取 Class 类对象。用的最多,但可能抛出 ClassNotFoundException 异常。
Class c1 = Class.forName(“java.lang.String”);
-
直接通过 类名.class 的方式得到,该方法最为安全可靠,程序性能更高。这说明任何一个类都有一个隐含的静态成员变量 class。这种方法只适合在编译前就知道操作的 Class。\
会有类的导入操作
Class c2 = String.class;
-
通过对象调用 getClass() 方法来获取,通常应用在:比如你传过来一个 Object类型的对象,而我不知道你具体是什么类用这种方法。
会有类的导入操作
String str = new String("Hello"); Class c3 = str.getClass();
需要注意的是:一个类在 JVM 中只会有一个 Class 实例,即我们对上面获取的 c1、c2和c3进行 equals 比较,发现都是 true。
通过反射创建类对象
通过反射创建类对象主要有两种方式:
-
通过 Class 对象的 newInstance() 方法
Class clz = Phone.class; Phone phone = (Phone)clz.newInstance();
-
通过 Constructor 对象的 newInstance() 方法。
Class clz = Phone.class; Constructor constructor = clz.getConstructor(); Phone phone= (Phone)constructor.newInstance();
通过 Constructor 对象创建类对象可以选择特定构造方法,而通过 Class 对象则只能使用默认的无参数构造方法。下面的代码就调用了一个有参数的构造方法进行了类对象的初始化。
```java
Class clz = Phone.class;
Constructor constructor = clz.getConstructor(String.class, int.class);
Phone phone = (Phone)constructor.newInstance("华为",6666);
通过反射获取类属性、方法、构造器
我们通过 Class 对象的 getFields() 方法可以获取 Class 类的属性,但无法获取私有属性。
Class clz = Phone.class;
Field[] fields = clz.getFields();
for (Field field : fields) {
System.out.println(field.getName());
}
输出结果是:
price
而如果使用 Class 对象的 getDeclaredFields() 方法则可以获取包括私有属性在内的所有属性:
Class clz = Phone.class;
Field[] fields = clz.getDeclaredFields();
for (Field field : fields) {
System.out.println(field.getName());
}
输出结果是:
name
price
与获取类属性一样,当我们去获取类方法、类构造器时,如果要获取私有方法或私有构造器,则必须使用有 declared 关键字的方法。
反射常用方法
-
类相关的方法
方法 用途 asSubclass(Class clazz) 把传递的类的对象转换成代表其子类的对象 Cast 把对象转换成代表类或是接口的对象 getClassLoader() 获得类的加载器 getClasses() 返回一个数组,数组中包含该类中所有公共类和接口类的对象 getDeclaredClasses() 返回一个数组,数组中包含该类中所有类和接口类的对象 forName(String className) 根据类名返回类的对象 getName() 获得类的完整路径名字 newInstance() 创建类的实例 getPackage() 获得类的包 getSimpleName() 获得类的名字 getSuperclass() 获得当前类继承的父类的名字 getInterfaces() 获得当前类实现的类或是接口 -
类中属性相关的方法
方法 用途 getField(String name) 获得某个公有的属性对象 getFields() 获得所有公有的属性对象 getDeclaredField(String name) 获得类的一个属性。包括private 声明的和继承类 getDeclaredFields() 获得类的所有属性。包括private 声明的和继承类 -
类中注解相关的方法
方法 | 用途 |
---|---|
getAnnotation(Class annotationClass) | 返回该类中与参数类型匹配的公有注解对象 |
getAnnotations() | 返回该类所有的公有注解对象 |
getDeclaredAnnotation(Class annotationClass) | 返回该类中与参数类型匹配的所有注解对象 |
getDeclaredAnnotations() | 返回该类所有的注解对象 |
- 类中构造器相关的方法
方法 | 用途 |
---|---|
getConstructor(Class…<?> parameterTypes) | 获得该类中与参数类型匹配的公有构造方法 |
getConstructors() | 获得该类的所有公有构造方法 |
getDeclaredConstructor(Class…<?> parameterTypes) | 获得该类中与参数类型匹配的构造方法 |
getDeclaredConstructors() | 获得该类所有构造方法 |
- 类中方法相关的方法
方法 | 用途 |
---|---|
getMethod(String name, Class…<?> parameterTypes) | 获得该类某个公有的方法 |
getMethods() | 获得该类所有公有的方法 |
getDeclaredMethod(String name, Class…<?> parameterTypes) | 获得该类某个方法,包括private 声明的和继承类 |
getDeclaredMethods() | 获得该类所有方法,包括private 声明的和继承类 |
-
其他重要的方法
方法 用途 isAnnotation() 如果是注解类型则返回true isAnnotationPresent(Class<? extends Annotation> annotationClass) 如果是指定类型注解类型则返回true isAnonymousClass() 如果是匿名类则返回true isArray() 如果是一个数组类则返回true isEnum() 如果是枚举类则返回true isInstance(Object obj) 如果obj是该类的实例则返回true isInterface() 如果是接口类则返回true isLocalClass() 如果是局部类则返回true isMemberClass() 如果是内部类则返回true
反射源码解析
当我们懂得了如何使用反射后,今天我们就来看看 JDK 源码中是如何实现反射的。或许大家平时没有使用过反射,但是在开发 Web 项目的时候会遇到过下面的异常:
java.lang.NullPointerException
...
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:369)
可以看到异常堆栈指出了异常在 Method 的第 369 的 invoke 方法中,其实这里指的 invoke 方法就是我们反射调用方法中的 invoke。
Method method = clz.getMethod("setPrice", int.class);
method.invoke(object, 6); //就是这里的invoke方法
例如我们经常使用的 Spring 配置中,经常会有相关 Bean 的配置:
<bean class="com.melrin.demo.api.Phone"></bean>
当我们在 XML 文件中配置了上面这段配置之后,Spring 便会在启动的时候利用反射去加载对应的 Phone类。而当 Apple 类不存在或发生启发异常时,异常堆栈便会将异常指向调用的 invoke 方法。
从这里可以看出,我们平常很多框架都使用了反射,而反射中最重要的就是 Method 类的 invoke 方法了。
反射总结
我们知道反射机制允许程序在运行时取得任何一个已知名称的class的内部信息,包括包括fields(属性),methods(方法)等,并可于运行时改变fields内容或调用methods。那么我们便可以更灵活的编写代码,代码可以在运行时装配,无需在组件之间进行源代码链接,降低代码的耦合度;还有动态代理的实现;JDBC原生代码注册驱动;hibernate 的实体类;Spring的AOP等等。但是凡事都有两面性,反射使用不当会造成很高的资源消耗
new对象和反射得到对象的区别
- 在使用反射的时候,必须确保这个类已经加载并已经连接了。使用new的时候,这个类可以没有被加载,也可以已经被加载。
- new关键字可以调用任何public构造方法,而反射只能调用无参构造方法。
- new关键字是强类型的,效率相对较高。 反射是弱类型的,效率低。
- 反射提供了一种更加灵活的方式创建对象,得到对象的信息。如Spring 中 AOP等的使用,动态代理的使用,都是基于反射的。解耦。
综合案例
现在我们产生对象的方式是使用 new 关键字:
Object obj = new Object();
new Object() 是创建一个对象,通过赋值符号由变量 obj 进行引用。这种关系维护是由开发者手动进行维护,但是在开发中有时候难免会遇到实现的对象会在使用中切换的情况。
引用对象的改变就需要重新进行编译,产生新的class文件才能由JRE重新加载执行。
这种开发方式带来一些问题:
- 如果引用的对象频繁切换,每次都需要重新编码,重新编译,然后载入执行,步骤不仅繁琐而且可能会出错。(比如很多地方同时都需要做这种操作,越多越容易出错)
- 项目上线运行时,牵连的项目很多,如果修改的文件非常多,就意味着系统停止运行的时间越长,会带来经济上的损失。
所以我们希望能够有一种方式,能够尽可能避免以上问题。
解决的方式之一可以将声明与实现的关系写入到XML中,代码中仅有类型的声明,但没有实现。需要使用的对象通过读取配置文件得到所在位置,然后使用反射技术进行对象的创建。再通过代码进行赋值。
这种方式我们成为「控制反转」
开发思路:
- 声明与实现的关系写入到XML文件中
- 开发XML的读取工具类
- 开发使用反射创建对象的工具类
- 验证
创建XML文件
在创建XML之前,先确定XML内部数据的格式,我们将根标签设置为 beans,子标签为bean,用于存储一个类的信息。
- id 是唯一的标签,不可以重复,相当于个人的身份证。作用是用于取出对应的class信息
- class 属性用于配置类的全包名和类名
类:User,包是:org.merlin.demo.bean
<beans>
<bean id="user" class="org.merlin.demo.bean.User"></bean>
</beans>
每个类都有自己的属性,可以在创建对象后为属性赋值,比如User中有属性
public class User{
private String name;
private int age;
}
XML中可以在bean标签中添加子标签 property ,将属性和数据进行关联
<beans>
<bean id="user" class="org.merlin.demo.bean.User">
<property name="name" value="张三"/>
<property name="age" value="33"/>
</bean>
</beans>
XML的工具类
工具类的开发需要关注几个地方:
- 能够正确查找到XML配置文件的位置
- 正确分解XML的内容
- 将得到的数据有组织的组装起来
- 对外提供方法,供调用
步骤
-
项目添加 dom4j 工具包的支持
-
创建能够组装XML信息的类
public class Prop { private String name; private String value; public Prop() {} public Prop(String name, String value) { super(); this.name = name; this.value = value; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } @Override public String toString() { return "Prop [name=" + name + ", value=" + value + "]"; } } import java.util.List; public class Bean { private String id; private String clazz; private List<Prop> props; public Bean() {} public Bean(String id, String clazz) { super(); this.id = id; this.clazz = clazz; } public Bean(String id, String clazz, List<Prop> props) { super(); this.id = id; this.clazz = clazz; this.props = props; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getClazz() { return clazz; } public void setClazz(String clazz) { this.clazz = clazz; } public List<Prop> getProps() { return props; } public void setProps(List<Prop> props) { this.props = props; } @Override public String toString() { return "Bean [id=" + id + ", clazz=" + clazz + "]"; } }
-
创建XML的解析类:ReaderXML
-
创建 open 方法
private Document document = null; /** * 打开文件 , 演示方法的连续调用,可以改为私有方法 * @param fileName * @return ReaderXML */ public ReaderXML open(String fileName){ try { document = new SAXReader().read(new File(fileName)); } catch (DocumentException e) { e.printStackTrace(); } //this指的是?ReaderXML return this; }
这里使用了一个全局变量, document
-
创建解析方法 parseXML
这个方法中会通过Bean 和 Prop 2个类组装XML中得到的信息,同时方法会将解析后的 Map 对象返回给方法的调用者
/** * 解析XML , 返回Bean对象列表 */ public Map<String,Bean> parseXML(){ Map<String,Bean> beans = new HashMap<String,Bean>(); Element rootElement = document.getRootElement(); //得到bean标签列表 Iterator<Element> elementIterator = rootElement.elementIterator(); while( elementIterator.hasNext() ){ //得到当前循环的bean子标签 Element element = elementIterator.next(); String id = element.attributeValue("id"); Bean bean = new Bean( id , element.attributeValue("class" ) ); //接着取出bean标签里面的prop标签,这又是一个集合 Iterator<Element> props = element.elementIterator(); List<Prop> propList = new ArrayList<>(); while( props.hasNext() ){ Element prop = props.next(); propList.add(new Prop(prop.attributeValue("name") , prop.attributeValue("value"))); } bean.setProps(propList); beans.put(id , bean); } return beans; }
-
-
创建 BeanFactory类,用于提取信息并创建对象进行数据绑定
-
构造器
private Map<String,Bean> beans = null; public BeanFactory(String fileName) { openXML(fileName); }
-
读取XML提取后的数据,这里使用了ReaderXML类
private BeanFactory openXML(String fileName){ ReaderXML reader = new ReaderXML(); beans = reader.open(fileName).parseXML(); return this; }
-
创建对象,绑定数据
public Object getBean(String beanName) throws Exception{ Bean deptBean = beans.get(beanName); Class<?> clazz = Class.forName( deptBean.getClazz() ); //创建一个新的实例 Object obj = clazz.newInstance(); Field[] fields = clazz.getDeclaredFields(); List<Prop> props = deptBean.getProps(); //循环Bean标签的prop列表 for (Prop prop : props) { //循环的是Dept类的所有属性 for (Field field : fields) { if( prop.getName().equals(field.getName() ) ){ //给属性设置值 //设置属性为可以访问状态 field.setAccessible(true); if(field.getType().getName().equals("java.lang.Integer")){ field.set(obj, Integer.valueOf(prop.getValue()) ); } else { field.set(obj, prop.getValue() ); } break; } } } return obj; }
-
检验
此处暂时不写内容,当上面的代码讲解完成后再进行补充。
Java 注解
元注解
Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制 。 可以用来将任何的信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联。
元注解的作用就是负责注解其他注解。Java5.0定义了4个标准的meta-annotation类型,它们被用来提供对其它 annotation类型作说明。
截止到JDK8,共有以下10个元注解:
- @Override - 检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
- @Deprecated - 标记过时方法。如果使用该方法,会报编译警告。
- @SuppressWarnings - 指示编译器去忽略注解中声明的警告。
作用在其他注解的注解(或者说 元注解)是:
- @Retention - 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。
- @Documented - 标记这些注解是否包含在用户文档中。
- @Target - 标记这个注解应该是哪种 Java 成员。
- @Inherited - 标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)
从 Java 7 之后,又添加了 3 个注解:
- @SafeVarargs - Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
- @FunctionalInterface - Java 8 开始支持,标识一个匿名函数或函数式接口。
- @Repeatable - Java 8 开始支持,标识某注解可以在同一个声明上使用多次。
以上元注解仅讲解下面2个
@Retention
- Retention 英文意思有保留、保持的意思,它表示注解存在阶段是保留在源码(编译期),字节码(类加载)或者运行期(JVM中运行)。在@Retention注解中使用枚举RetentionPolicy来表示注解保留时期
- @Retention(RetentionPolicy.SOURCE),注解仅存在于源码中,在class字节码文件中不包含
- @Retention(RetentionPolicy.CLASS), 默认的保留策略,注解会在class字节码文件中存在,但运行时无法获得
- @Retention(RetentionPolicy.RUNTIME), 注解会在class字节码文件中存在,在运行时可以通过反射获取到
- 如果我们是自定义注解,则通过前面分析,我们自定义注解如果只存着源码中或者字节码文件中就无法发挥作用,而在运行期间能获取到注解才能实现我们目的,所以自定义注解中肯定是使用 @Retention(RetentionPolicy.RUNTIME)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTestAnnotation {}
@Target
- Target的英文意思是目标,这也很容易理解,使用@Target元注解表示我们的注解作用的范围就比较具体了,可以是类,方法,方法参数变量等,同样也是通过枚举类ElementType表达作用类型
- @Target(ElementType.TYPE) 作用接口、类、枚举、注解
- @Target(ElementType.FIELD) 作用属性字段、枚举的常量
- @Target(ElementType.METHOD) 作用方法
- @Target(ElementType.PARAMETER) 作用方法参数
- @Target(ElementType.CONSTRUCTOR) 作用构造函数
- @Target(ElementType.LOCAL_VARIABLE)作用局部变量
- @Target(ElementType.ANNOTATION_TYPE)作用于注解(@Retention注解中就使用该属性)
- @Target(ElementType.PACKAGE) 作用于包
- @Target(ElementType.TYPE_PARAMETER) 作用于类型泛型,即泛型方法、泛型类、泛型接口 (jdk1.8加入)
- @Target(ElementType.TYPE_USE) 类型使用.可以用于标注任意类型除了 class (jdk1.8加入)
- 一般比较常用的是ElementType.TYPE类型
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyTestAnnotation {}
自定义注解
JDK自带的元注解功能有限,所以更多的时候我们会自定义注解来方便程序开发。比如框架自带的注解。
创建自定义注解需要使用关键字 @interface
public @interface MyAnnotation{}
定义一个自定义注解(使用元注解,每个元注解都是可选,非必选)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.METHOD})
public @interface MyAnnotation{
public String name() default "hello";
}
注解中可以声明成员方法,声明的成员方法为最终的注解里面的参数,成员方法可以使用default关键字设置默认值。上面的注解使用如:
@MyAnnotation1(name="miao")
自定义注解演示
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Target({ElementType.FIELD,ElementType.METHOD})
@interface MyAnno{
public String name() default "miao";
public String email() default "miao@example.com";
}
定义一个User类,来使用自定义注解
class User{
@MyAnno(name = "喵先生")
private String name;
@MyAnno(name = "miao@example.com")
private String email;
@MyAnno(name = "sayHelloWorld")
public String sayHello(){
return "";
}
}
注解的本质
注解的本质就是一个Annotation接口
/**Annotation接口源码*/
public interface Annotation {
boolean equals(Object obj);
int hashCode();
Class<? extends Annotation> annotationType();
}
- 通过以上源码,我们知道注解本身就是Annotation接口的子接口,也就是说注解中其实是可以有属性和方法,但是接口中的属性都是static final的,对于注解来说没什么意义,而我们定义接口的方法就相当于注解的属性,也就对应了前面说的为什么注解只有属性成员变量,其实他就是接口的方法,这就是为什么成员变量会有括号,不同于接口我们可以在注解的括号中给成员变量赋值。
注解属性类型
注解属性类型可以有以下列出的类型
- 基本数据类型
- String
- 枚举类型
- 注解类型
- Class类型
注解成员变量赋值
如果注解又多个属性,则可以在注解括号中用“,”号隔开分别给对应的属性赋值,如下例子,注解在父类中赋值属性
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyTestAnnotation {
String name() default "miao";
int age() default 18;
}
@MyTestAnnotation(name = "father",age = 50)
public class Father {
}
获取注解属性
- 前面我们说了很多注解如何定义,放在哪,现在我们可以开始学习注解属性的提取了,这才是使用注解的关键,获取属性的值才是使用注解的目的。
- 如果获取注解属性,当然是反射啦,主要有三个基本的方法
/**是否存在对应 Annotation 对象*/
public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {
return GenericDeclaration.super.isAnnotationPresent(annotationClass);
}
/**获取 Annotation 对象*/
public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {
Objects.requireNonNull(annotationClass);
return (A) annotationData().annotations.get(annotationClass);
}
/**获取所有 Annotation 对象数组*/
public Annotation[] getAnnotations() {
return AnnotationParser.toArray(annotationData().annotations);
}
下面结合前面的例子,我们来获取一下注解属性,在获取之前我们自定义的注解必须使用元注解@Retention(RetentionPolicy.RUNTIME)
public class test {
public static void main(String[] args) throws NoSuchMethodException {
/**
* 获取类注解属性
*/
Class<Father> fatherClass = Father.class;
boolean annotationPresent = fatherClass.isAnnotationPresent(MyTestAnnotation.class);
if(annotationPresent){
MyTestAnnotation annotation = fatherClass.getAnnotation(MyTestAnnotation.class);
System.out.println(annotation.name());
System.out.println(annotation.age());
}
/**
* 获取方法注解属性
*/
try {
Field age = fatherClass.getDeclaredField("age");
boolean annotationPresent1 = age.isAnnotationPresent(Age.class);
if(annotationPresent1){
Age annotation = age.getAnnotation(Age.class);
System.out.println(annotation.value());
}
Method play = PlayGame.class.getDeclaredMethod("play");
if (play!=null){
People annotation2 = play.getAnnotation(People.class);
Game[] value = annotation2.value();
for (Game game : value) {
System.out.println(game.value());
}
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
}