一、反射机制
①、类对象概念: 所有的类,都存在一个类对象,这个类对象用于提供类本身的信息,比如有几种构造方法, 有多少属性,有哪些普通方法。
②、获取类对象有3种方式
1. Class.forName
2. Hero.class
3. new Hero().getClass()
在一个JVM中,一种类,只会有一个类对象存在。所以以上三种方式取出来的类对象,都是一样的。
准确的讲是一个ClassLoader下,一种类,只会有一个类对象存在。通常一个JVM下,只会有一个ClassLoader。
③、获取类对象的时候,会导致类属性被初始化,而且只会执行一次。(除了直接使用 Class c = Hero.class 这种方式,这种方式不会导致静态属性被初始化):
在对象方法前,加上修饰符synchronized ,同步对象是当前实例。
当synchronized修饰静态方法的时候, 同步对象就是这个类的类对象。
换句话说,静态方法被修饰为synchronized的时候,其同步对象就是当前类的类对象。
④、常见对象:
与传统的通过new 来获取对象的方式不同
反射机制,会先拿到Hero的“类对象”,然后通过类对象获取“构造器对象”
再通过构造器对象创建一个对象
public class TestReflection {
public static void main(String[] args) {
//传统的使用new的方式创建对象
Hero h1 =new Hero();
h1.name = "teemo";
System.out.println(h1);
try {
//使用反射的方式创建对象
String className = "charactor.Hero";
//类对象
Class pClass=Class.forName(className);
//构造器
Constructor c= pClass.getConstructor();
//通过构造器实例化
Hero h2= (Hero) c.newInstance();
h2.name="gareen";
System.out.println(h2);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
通过配置文件获取对象:
读取hero.config的数据,取出其中的类名,根据类名实例化出对象,然后返回对象。
public class GetReflectionTest {
APHero aph = new APHero();
ADHero adh = new ADHero();
public static void main(String[] args) {
File f = new File("/Users/yuqian/Desktop/hero.config.txt");
try(FileOutputStream fos = new FileOutputStream(f);DataOutputStream dos = new DataOutputStream(fos);){
dos.writeUTF("Charactor.Hero.ADHero");
dos.writeUTF("Charactor.Hero.APHero");
}catch (IOException e){
e.printStackTrace();
}
try(FileInputStream fis = new FileInputStream(f);DataInputStream dis = new DataInputStream(fis);){
String name1 = dis.readUTF();
String name2 = dis.readUTF();
try {
ADHero hero1 = (ADHero) Class.forName(name1).getConstructor().newInstance();
APHero hero2 = (APHero) Class.forName(name2).getConstructor().newInstance();
hero1.physicAttack();
hero2.magicAttack();
System.out.println(hero1);
System.out.println(hero2);
}catch(Exception e){
e.printStackTrace();
}
}catch(IOException e) {
e.printStackTrace();
}
}
}
⑤、访问属性:
为了访问属性,把name修改为public。
对于private修饰的成员,需要使用setAccessible(true)才能访问和修改。
通过反射修改属性的值:
public static void main(String[] args) {
Hero h =new Hero();
//使用传统方式修改name的值为garen
h.name = "garen";
try {
//获取类Hero的名字叫做name的字段
Field f1= h.getClass().getDeclaredField("name");
//修改这个字段的值
f1.set(h, "teemo");
//打印被修改后的值
System.out.println(h.name);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
⑥、getField和getDeclaredField的区别
这两个方法都是用于获取字段
getField 只能获取public的,包括从父类继承来的字段。
getDeclaredField 可以获取本类所有的字段,包括private的,但是不能获取继承来的字段。 (注: 这里只能获取到private的字段,但并不能访问该private字段的值,除非加上setAccessible(true))
⑦、通过反射机制,调用一个对象的方法:
public class TestReflection {
public static void main(String[] args) {
Hero h = new Hero();
try {
// 获取这个名字叫做setName,参数类型是String的方法
Method m = h.getClass().getMethod("setName", String.class);
// 对h对象,调用这个方法
m.invoke(h, "盖伦");
// 使用传统的方式,调用getName方法
System.out.println(h.getName());
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
⑧、在学习了Spring 的依赖注入,反转控制之后,才会对反射有更好的理解
当需要从第一个业务方法切换到第二个业务方法的时候,使用非反射方式,必须修改代码,并且重新编译运行,才可以达到效果
new
Service2().doService2();
使用反射方式,首先准备一个配置文件,就叫做spring.txt吧, 放在src目录下。 里面存放的是类的名称,和要调用的方法名。
在测试类Test中,首先取出类名称和方法名,然后通过反射去调用这个方法。
当需要从调用第一个业务方法,切换到调用第二个业务方法的时候,不需要修改一行代码,也不需要重新编译,只需要修改配置文件spring.txt,再运行即可。
这也是Spring框架的最基本的原理,只是它做的更丰富,安全,健壮。
public class Test {
@SuppressWarnings({ "rawtypes", "unchecked" })
public static void main(String[] args) throws Exception {
//从spring.txt中获取类名称和方法名称
File springConfigFile = new File("e:\\project\\j2se\\src\\spring.txt");
Properties springConfig= new Properties();
springConfig.load(new FileInputStream(springConfigFile));
String className = (String) springConfig.get("class");
String methodName = (String) springConfig.get("method");
//根据类名称获取类对象
Class clazz = Class.forName(className);
//根据方法名称,获取方法对象
Method m = clazz.getMethod(methodName);
//获取构造器
Constructor c = clazz.getConstructor();
//根据构造器,实例化出对象
Object service = c.newInstance();
//调用对象的指定方法
m.invoke(service);
}
}
二、注解
反射用于解析注解中的信息
1、基本内置注解:
①、@Override 用在方法上,表示这个方法重写了父类的方法,如toString()。
如果父类没有这个方法,那么就无法编译通过
②、@Deprecated 表示这个方法已经过期,不建议开发者使用。(暗示在将来某个不确定的版本,就有可能会取消掉)
③、@SuppressWarnings Suppress英文的意思是抑制的意思,这个注解的用处是忽略警告信息。
比如大家使用集合的时候,有时候为了偷懒,会不写泛型,像这样:
List heros = new ArrayList();
@SuppressWarnings 有常见的值,分别对应如下意思
1.deprecation:使用了不赞成使用的类或方法时的警告(使用@Deprecated使得编译器产生的警告);
2.unchecked:执行了未检查的转换时的警告,例如当使用集合时没有用泛型 (Generics) 来指定集合保存的类型; 关闭编译器警告
3.fallthrough:当 Switch 程序块直接通往下一种情况而没有 Break 时的警告;
4.path:在类路径、源文件路径等中有不存在的路径时的警告;
5.serial:当在可序列化的类上缺少 serialVersionUID 定义时的警告;
6.finally:任何 finally 子句不能正常完成时的警告;
7.rawtypes 泛型类型未指明
8.unused 引用定义了,但是没有被使用
9.all:关于以上所有情况的警告。
④、@SafeVarargs 这是1.7 之后新加入的基本注解. 如例所示,当使用可变数量的参数的时候,而参数的类型又是泛型T的话,就会出现警告。 这个时候,就使用@SafeVarargs来去掉这个警告
@SafeVarargs注解只能用在参数长度可变的方法或构造方法上,且方法必须声明为static或final,否则会出现编译错误。一个方法使用@SafeVarargs注解的前提是,开发人员必须确保这个方法的实现中对泛型类型参数的处理不会引发类型安全问题。
⑤、@FunctionalInterface这是Java1.8 新增的注解,用于约定函数式接口。
函数式接口概念: 如果接口中只有一个抽象方法(可以包含多个默认方法或多个static方法),该接口称为函数式接口。函数式接口其存在的意义,主要是配合Lambda 表达式 来使用。
2、自定义注解:
①、非注解方式DBUtil:
在一个基于JDBC开发的项目里,都会有一个DBUtil这么一个类,在这个类里统一提供连接数据库的IP地址,端口,数据库名称, 账号,密码,编码方式等信息。如例所示,在这个DBUtil类里,这些信息,就是以属性的方式定义在类里的。
大家可以运行试试,运行结果是获取一个连接数据库test的连接Connection实例。
package util;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class DBUtil {
static String ip = "127.0.0.1";
static int port = 3306;
static String database = "test";
static String encoding = "UTF-8";
static String loginName = "root";
static String password = "admin";
static{
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static Connection getConnection() throws SQLException {
String url = String.format("jdbc:mysql://%s:%d/%s?characterEncoding=%s", ip, port, database, encoding);
return DriverManager.getConnection(url, loginName, password);
}
public static void main(String[] args) throws SQLException {
System.out.println(getConnection());
}
}
②、自定义注解@JDBCConfig:
把DBUtil这个类改造成为支持自定义注解的方式。 首先创建一个注解JDBCConfig
1. 创建注解类型的时候即不使用class也不使用interface,而是使用@interface
public @interface JDBCConfig
2. 元注解
@Target({METHOD,TYPE}) 表示这个注解可以用用在类/接口上,还可以用在方法上
@Retention(RetentionPolicy.RUNTIME) 表示这是一个运行时注解,即运行起来之后,才获取注解中的相关信息,而不像基本注解如@Override 那种不用运行,在编译时eclipse就可以进行相关工作的编译时注解。
@Inherited 表示这个注解可以被子类继承
@Documented 表示当执行javadoc的时候,本注解会生成相关文档
3. 注解元素,这些注解元素就用于存放注解信息,在解析的时候获取出来
package anno;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import java.lang.annotation.Documented;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({METHOD,TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface JDBCConfig {
String ip();
int port() default 3306;
String database();
String encoding();
String loginName();
String password();
}
③、注解方式DBUtil
数据库相关配置信息本来是以属性的方式存放的,现在改为了以注解的方式,提供这些信息了。
目前只是以注解的方式提供这些信息,但是还没有解析
package util;
import anno.JDBCConfig;
@JDBCConfig(ip = "127.0.0.1", database = "test", encoding = "UTF-8", loginName = "root", password = "admin")
public class DBUtil {
static {
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
④、解析注解:
接下来就通过反射,获取这个DBUtil这个类上的注解对象
JDBCConfig config = DBUtil.class.getAnnotation(JDBCConfig.class);
拿到注解对象之后,通过其方法,获取各个注解元素的值:
String ip = config.ip();
int port = config.port();
String database = config.database();
String encoding = config.encoding();
String loginName = config.loginName();
String password = config.password();
后续就一样了,根据这些配置信息得到一个数据库连接Connection实例
package util;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import anno.JDBCConfig;
@JDBCConfig(ip = "127.0.0.1", database = "test", encoding = "UTF-8", loginName = "root", password = "admin")
public class DBUtil {
static {
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static Connection getConnection() throws SQLException, NoSuchMethodException, SecurityException {
JDBCConfig config = DBUtil.class.getAnnotation(JDBCConfig.class);
String ip = config.ip();
int port = config.port();
String database = config.database();
String encoding = config.encoding();
String loginName = config.loginName();
String password = config.password();
String url = String.format("jdbc:mysql://%s:%d/%s?characterEncoding=%s", ip, port, database, encoding);
return DriverManager.getConnection(url, loginName, password);
}
public static void main(String[] args) throws NoSuchMethodException, SecurityException, SQLException {
Connection c = getConnection();
System.out.println(c);
}
}
3、元注解概念:
在讲解元注解概念之前,我们先建立元数据的概念。 元数据在英语中对应单词 metadata, metadata在wiki中的解释是:
Metadata is data [information] that provides information about other data
为其他数据提供信息的数据
这样元注解就好理解了,元注解 meta annotation用于注解 自定义注解 的注解。
元注解有这么几种:
@Target
@Retention
@Inherited
@Documented
@Repeatable (java1.8 新增)
①、@Target 表示这个注解能放在什么位置上,是只能放在类上?还是即可以放在方法上,又可以放在属性上。自定义注解@JDBCConfig 这个注解上的@Target是:@Target({METHOD,TYPE}),表示他可以用在方法和类型上(类和接口),但是不能放在属性等其他位置。 可以选择的位置列表如下:
ElementType.TYPE:能修饰类、接口或枚举类型
ElementType.FIELD:能修饰成员变量
ElementType.METHOD:能修饰方法
ElementType.PARAMETER:能修饰参数
ElementType.CONSTRUCTOR:能修饰构造器
ElementType.LOCAL_VARIABLE:能修饰局部变量
ElementType.ANNOTATION_TYPE:能修饰注解
ElementType.PACKAGE:能修饰包
②、@Retention 表示生命周期,自定义注解@JDBCConfig 上的值是 RetentionPolicy.RUNTIME, 表示可以在运行的时候依然可以使用。 @Retention可选的值有3个:
RetentionPolicy.SOURCE: 注解只在源代码中存在,编译成class之后,就没了。@Override 就是这种注解。
RetentionPolicy.CLASS: 注解在java文件编程成.class文件后,依然存在,但是运行起来后就没了。@Retention的默认值,即当没有显式指定@Retention的时候,就会是这种类型。
RetentionPolicy.RUNTIME: 注解在运行起来之后依然存在,程序可以通过反射获取这些信息,自定义注解@JDBCConfig 就是这样。
③、@Inherited 表示该注解具有继承性。如例,设计一个DBUtil的子类,其getConnection2方法,可以获取到父类DBUtil上的注解信息。
④、@Documented 如图所示, 在用javadoc命令生成API文档后,DBUtil的文档里会出现该注解说明。
注: 使用eclipse把项目中的.java文件导成API文档步骤:
1. 选中项目
2. 点开菜单File
3. 点击Export
4. 点开java->javadoc->点next
5. 点finish
⑤、当没有@Repeatable修饰的时候,注解在同一个位置,只能出现一次,如例所示:
@JDBCConfig(ip = "127.0.0.1", database = "test", encoding = "UTF-8", loginName = "root", password = "admin")
@JDBCConfig(ip = "127.0.0.1", database = "test", encoding = "UTF-8", loginName = "root", password = "admin")
重复做两次就会报错了。
使用@Repeatable之后,再配合一些其他动作,就可以在同一个地方使用多次了。
比如在练习练习-查找文件内容 中有一个要求,即查找文件后缀名是.java的文件,我们把部分代码修改为注解,并且使用@Repeatable 这个元注解来表示,文件后缀名的范围可以是java, html, css, js 等等。
为了紧凑起见,把注解作为内部类的形式放在一个文件里。
1. 注解FileTypes,其value()返回一个FileType数组
2. 注解FileType,其@Repeatable的值采用FileTypes
3. 运用注解:在work方法上重复使用多次@FileType注解
4. 解析注解: 在work方法内,通过反射获取到本方法上的FileType类型的注解数组,然后遍历本数组
package annotation;
import static java.lang.annotation.ElementType.METHOD;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
public class FindFiles {
@Target( METHOD)
@Retention( RetentionPolicy.RUNTIME )
public @interface FileTypes {
FileType[] value();
}
@Target( METHOD )
@Retention( RetentionPolicy.RUNTIME )
@Repeatable( FileTypes.class )
public @interface FileType {
String value();
};
@FileType( ".java" )
@FileType( ".html" )
@FileType( ".css" )
@FileType( ".js" )
public void work(){
try {
FileType[] fileTypes= this.getClass().getMethod("work").getAnnotationsByType(FileType.class);
System.out.println("将从如下后缀名的文件中查找文件内容");
for (FileType fileType : fileTypes) {
System.out.println(fileType.value());
}
System.out.println("查找过程略。。。");
} catch (NoSuchMethodException | SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void main(String[] args) {
new FindFiles().work();
}
}
4、仿Hibernate注解:
①、hibernate有两种配置方式,分别是*.hbm.xml 配置方式 和注解方式。 虽然方式不一样,但是都是用于解决如下问题:
1. 当前类是否实体类
2. 对应的表名称
3. 主键对应哪个属性, 自增长策略是什么,对应字段名称是什么
4. 非主键属性对应字段名称是什么
②、自定义hibernate注解
参考hibernate的 注解配置方式 ,自定义5个注解,分别对应hibernate中用到的注解:
hibernate_annotation.MyEntity 对应 javax.persistence.Entity
hibernate_annotation.MyTable 对应 javax.persistence.Table
hibernate_annotation.MyId 对应 javax.persistence.Id
hibernate_annotation.MyGeneratedValue 对应 javax.persistence.GeneratedValue
hibernate_annotation.MyColumn 对应 javax.persistence.Column
③、运用在Hero对象上
像以注解方式配置Product类 那样,在Hero类上运用这些自定义注解:
当注解的方法是value的时候,给这个注解赋值时,本来应该是:
@MyColumn(value="name_")
现在可以简略一点,写为
@MyColumn("name_")
只有当名称是value的时候可以这样,其他名称如name,stratgy等不行
package pojo;
import hibernate_annotation.MyColumn;
import hibernate_annotation.MyEntity;
import hibernate_annotation.MyGeneratedValue;
import hibernate_annotation.MyId;
import hibernate_annotation.MyTable;
@MyEntity
@MyTable(name="hero_")
public class Hero {
private int id;
private String name;
private int damage;
private int armor;
@MyId
@MyGeneratedValue(strategy = "identity")
@MyColumn("id_")
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
@MyColumn("name_")
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@MyColumn("damage_")
public int getDamage() {
return damage;
}
public void setDamage(int damage) {
this.damage = damage;
}
@MyColumn("armor_")
public int getArmor() {
return armor;
}
public void setArmor(int armor) {
this.armor = armor;
}
}
④、解析注解
创建一个解析类ParseHibernateAnnotation ,获取Hero类上配置的注解信息,
思路如下:
1. 首先获取Hero.class类对象
2. 判断本类是否进行了MyEntity 注解
3. 获取注解 MyTable
4. 遍历所有的方法,如果某个方法有MyId注解,那么就记录为主键方法primaryKeyMethod
5. 把主键方法的自增长策略注解MyGeneratedValue和对应的字段注解MyColumn 取出来,并打印
6. 遍历所有非主键方法,并且有MyColumn注解的方法,打印属性名称和字段名称的对应关系。
package test;
import java.lang.reflect.Method;
import hibernate_annotation.MyColumn;
import hibernate_annotation.MyEntity;
import hibernate_annotation.MyGeneratedValue;
import hibernate_annotation.MyId;
import hibernate_annotation.MyTable;
import pojo.Hero;
public class ParseHibernateAnnotation {
public static void main(String[] args) {
Class<Hero> clazz = Hero.class;
MyEntity myEntity = (MyEntity) clazz.getAnnotation(MyEntity.class);
if (null == myEntity) {
System.out.println("Hero类不是实体类");
} else {
System.out.println("Hero类是实体类");
MyTable myTable= (MyTable) clazz.getAnnotation(MyTable.class);
String tableName = myTable.name();
System.out.println("其对应的表名是:" + tableName);
Method[] methods =clazz.getMethods();
Method primaryKeyMethod = null;
for (Method m: methods) {
MyId myId = m.getAnnotation(MyId.class);
if(null!=myId){
primaryKeyMethod = m;
break;
}
}
if(null!=primaryKeyMethod){
System.out.println("找到主键:" + method2attribute( primaryKeyMethod.getName() ));
MyGeneratedValue myGeneratedValue =
primaryKeyMethod.getAnnotation(MyGeneratedValue.class);
System.out.println("其自增长策略是:" +myGeneratedValue.strategy());
MyColumn myColumn = primaryKeyMethod.getAnnotation(MyColumn.class);
System.out.println("对应数据库中的字段是:" +myColumn.value());
}
System.out.println("其他非主键属性分别对应的数据库字段如下:");
for (Method m: methods) {
if(m==primaryKeyMethod){
continue;
}
MyColumn myColumn = m.getAnnotation(MyColumn.class);
//那些setter方法上是没有MyColumn注解的
if(null==myColumn)
continue;
System.out.format("属性: %s\t对应的数据库字段是:%s%n",method2attribute(m.getName()),myColumn.value());
}
}
}
private static String method2attribute(String methodName) {
String result = methodName; ;
result = result.replaceFirst("get", "");
result = result.replaceFirst("is", "");
if(result.length()<=1){
return result.toLowerCase();
}
else{
return result.substring(0,1).toLowerCase() + result.substring(1,result.length());
}
}
}
5、注解分类:
按照作用域分:
根据注解的作用域@Retention,注解分为
RetentionPolicy.SOURCE: Java源文件上的注解
RetentionPolicy.CLASS: Class类文件上的注解
RetentionPolicy.RUNTIME: 运行时的注解
按照来源分:
1. 内置注解 如@Override ,@Deprecated 等等
2. 第三方注解,如Hibernate, Struts等等
3. 自定义注解,如仿hibernate的自定义注解
在工作中,大部分都是使用第三方注解, 当然第三方注解本身就是自定义注解。