Android SQLite反射封装

Java的源代码通过javac工具编译后生成.class文件,在运行Java应用时虚拟机会把.class文件加载到内存中,在JVM中会生成对应的Class类型对象。开发者可以通过Class对象查看类中定义的属性(Field)、方法(Method)和构造函数(Constructor)等成员对象,也可以利用Field属性对象访问类实例中定义的属性值,Method方法对象调用类实例的方法,Constructor构造函数用来创建类实例对象。像这种能够在运行时动态获取类成员并且调用它们的功能就是Java的反射机制,有了反射机制开发者完全可以不知道具体类型却能够构造它的对象、设置属性以及调用方法。

// Student反射简析
public class Student {
	private int id;
	private String name;
	private int age;
	public Student(int id) {
		this.id = id;
	}
	public Student() {} // 没有参数的构造函数就是默认构造函数
// 省略所有的getters和setters方法和toString()方法
}

// 构造函数有两个,注意构造函数和方法是不同的类对象
// public com.example.Student(int)  带id的构造函数
// public com.example.Student()  默认构造函数
Constructor[] constructors = Student.class.getConstructors();

// 属性有三个
// private int com.example.Student.id
// private java.lang.String com.example.Student.name
// private int com.example.Student.age
Field[] fields = Student.class.getDeclaredFields();

// 方法有7个这里只列举了2个,注意构造函数和方法是不同的类对象
// public java.lang.String com.example.Student.getName()
// public int com.example.Student.getId() 等等...
Method[] methods = Student.class.getDeclaredMethods();

上诉代码中通过反射机制获取到了Student类的构造方法,内部的三个属性值和访问/设置方法对象,同样可以通过反射机制获取Teacher类的构造方法属性值和方法,Student类和Teacher类就能够通过相同的反射接口来处理。
在简单数据库封装中直接用源代码的方式创建对象,再设置对象属性,为了避免再使用样板代码,有必要对创建对象和设置属性操作做封装,先来看不使用new使用反射该如何创建对象呢?在Class类中有一个newInstance()的方法就能够创建对象,该方法内部会使用默认构造函数也就是没有参数的构造函数来创建对象。Java语言中类如果没有声明任何构造函数,编译器就会为它添加一个默认构造函数,一旦类中声明了带参数的构造函数,类中就不会再添加默认的构造函数需要开发者手动添加默认构造函数,使用Class.newInstance()的类通常建议手动声明默认构造函数。

Student student = Student.class.newInstance();
Constructor defaultConstructor = Student.class.getConstructor(null);
student = (Student) defaultConstructor.newInstance(null);
Constructor trivialConstructor = Student.class.getConstructor(int.class);
student = (Student) trivialConstructor.newInstance(100);

上面通过Student.class获取到了Student实体类的Class对象,使用Class.newInstance()方法还有获取其他的构造方法都可以创建创建新的对象,不同的构造函数要求传入的参数类型是不同的。想要设置对象的属性值,可以通过前面的Field属性对象设置,也可以用Method方法设置。

Method method = Student.class.getDeclaredMethod("setName", String.class);
method.invoke(student, "zhangsan");
Field field = Student.class.getDeclaredField("age");
field.setAccessible(true);
field.set(student, 20);
// Student [id=100, name=zhangsan, age=20]
System.out.println(student.toString());  

在使用Costructor、Method和Field时如果在源代码中被声明为private私有权限,直接调用会抛出无法访问异常,想要访问私有对象就需要调用setAceesible(true)设置可访问。在属性赋值读取时如果用Method方法对象需要getter和setter两个方法的Method,使用很不方便,通常访问属性就直接使用Field来做处理。

BaseDao接口

构造对象和设置属性值使用反射机制可以很好的实现封装,前面的示例代码中需要明确指定Student.class代表当前构造和设置的都是Student对象,如果有新的Teacher实体类需要构造和设置就需要把Student.class替换成Teacher.class,既然这里已经考虑到要复用反射的构造对象和设置属性逻辑,可以引入类继承机制来实现Dao对象。

// 简单BaseDao接口
public abstract class BaseDao {
	public abstract void save(Object t);
public abstract Object load(Object key);
    public Object create(Cursor cursor) {
Class class = getEntityClass();
// 反射生成对象并设置属性
}
    public abstract Class getEntityClass();
}

BaseDao.create()方法会传入查询到的Cursor对象,根据getEntityClass()获取到当前Dao操作的实体类,getEntityClass()由具体的子类负责实现。仔细查看上面的BaseDao就会发现它的所有操作针对的类型都需要是Object,每次用户load()加载对象后都需要强制类型转换成需要的类型。在Java中对这种编译时无法确认的类型提供了泛型机制,泛型采用类型变量来代替实际使用的类型,在实际调用的地方会要求初始化类型变量,编译器就能够判断实际操作的对象类型并为开发者提供强制类型转换。Java的泛型机制并不是运行时的,它只在编译时存在,在实际运行时对象类型中擦除了类型变量;不过有些开发场景下需要知道用户提供的泛型类到底是什么,Java编译器会将实际的泛型保存在.class文件中,用户需要的时候可以通过Class.getGenericSuperClass()接口获取到泛型父类。

// 使用泛型实现BaseDao<T>
ArrayList<User> users = new ArrayList<User>();
ArrayList objects = new ArrayList();
// true,由于泛型擦除,ArrayList和ArrayList<User>其实是一个类
System.out.println(users.getClass() == objects.getClass()); 
// 现在使用泛型变量来实现之前的BaseDao基础类,它的方法处理的
// 参数类型不再是Object而是T类型。
public abstract class BaseDao<T> {
	public abstract void save(T t);
    public abstract T load(Object key);
    public T create(Cursor cursor) {
Class clazz = getEntityClass();
// 反射生成对象并设置属性
}
    public abstract Class<?> getEntityClass();
}

public class StudentDao extends BaseDao<Student> {
public  void save(Student t) {  }
    public Student load(Object key) { return null; } 
public Class<?> getEntityClass() { return Student.class; }
}

public class TeacherDao extends BaseDao<Teacher> {
public  void save(Teacher t) {  }
    public Teacher load(Object key){  return null;  } 
public Class<?> getEntityClass() { return Teacher.class; }
}

上诉代码用了泛型的继承实现返回个用户直接就是对应的类型,用户获取到对象后不再需要执行强制类型转换,可见泛型的实现比直接使用Object类型更加用户友好。在BaseDao中获取当前实体类型的getEntityClass()方法返回的类型和类声明的BaseDao尖括号参数化类是相同的,泛型的类型变量实际值可以通过Class.getGenericSuperClass()查找到,它返回的是Type类型,可以称呼该返回结果为泛型父类 ;在Class对象里还有一个getSuperClass()方法负责返回类的父类型,它返回的结果还是Class对象,可以称作是运行时父类。

// getGenericSuperClass()调用示例
//定义一个StudentDao的子类,没有什么用,只为查看运行结果
// getGenericSuperClass()和getSuperClass()区别
public class MyStudentDao extends StudentDao { }
// BaseDao的泛型父类和运行时父类比较
BaseDao.class.getGenericSuperclass() == BaseDao.class.getSuperclass()  // true 
// BaseDao泛型父类和运行时父类相同都是Class类型
System.out.println(BaseDao.class.getSuperclass());// class java.lang.Object
System.out.println(BaseDao.class.getGenericSuperclass());// class java.lang.Object

// false,比较Student Dao泛型父类和运行时父类是否相同
System.out.println(StudentDao.class.getGenericSuperclass() == StudentDao.class.getSuperclass());
// StudentDao的泛型父类和运行时父类不同,泛型父类时ParameterizedType类型
// 运行时父类时Class类型
// com.example.BaseDao<com.example.Student>
System.out.println(StudentDao.class.getGenericSuperclass()); // StudentDao泛型父类
// class com.example.BaseDao
System.out.println(StudentDao.class.getSuperclass()); // StudentDao运行时父类

// MyStudentDao的泛型父类和运行时父类相同都是Class类型
// true 
System.out.println(MyStudentDao.class.getGenericSuperclass() == MyStudentDao.class.getSuperclass());
// class StudentDao 
System.out.println(MyStudentDao.class.getGenericSuperclass()); // MyStudentDao泛型父类
// class StudentDao 
System.out.println(MyStudentDao.class.getSuperclass()); // MyStudentDao运行时父类

从示例代码运行结果可以看出StudentDao泛型父类中包含Student类,这种类型在Java中使用ParameterizedType参数化类型,它是由原生类型(RawType)和类型参数(TypeArgument)两种类型组成,类型参数是个数组类型,按照代码中声明的顺序先后排列,泛型父类BaseDao里BaseDao就是原生类型,Student是第一个类型参数。BaseDao和MyStudentDao两个类它们的运行时父类和泛型父类返回的类型是相同的而且都是Class对象,二者在代码中分别继承自Object和StudentDao两个普通类,它们在运行时父类和泛型父类是相同的。StudentDao类型在源代码中继承的是BaseDao参数化类型,由于Java中的泛型在运行时被擦除,BaseDao在运行时继承体系中实际是BaseDao.class对象,也就是StudentDao.getSuperClass()返回的Class对象,BaseDao类型在编译时被作为字符串保存在.class文件中,开发者通过getGenericSuperClass()获取代码泛型继承类型虚拟机就会读取.class文件中的泛型字符串并且作为ParameterizedType对象返回给开发者。
在运行过程中想要获取BaseDao里的Student类,需要从当前类开始一直向上遍历到Object类,查看其中泛型父类类型是ParameterizedType参数化类型,之后从中获取第一个类型变量的类型,这种查找方式对TeacherDao同样适用,可以把实现直接放到BaseDao类中。

// 代码获取实体类型
public Class<?> getEntityClass() {
    if (entityClass == null) {
        Class<?> current = getClass();
        // 查看源码父类是否是参数化类型
        while (!(current.getGenericSuperclass() instanceof ParameterizedType)) {
            current = current.getSuperclass(); // 继续遍历运行时父类
        }

        ParameterizedType type = (ParameterizedType) current.getGenericSuperclass();
        entityClass = (Class<?>) type.getActualTypeArguments()[0];
    }
return entityClass;
}

数据库表与Java对象

在应用开发中通常会用到User用户类、Group群组类和Order订单类,通常定义好实体类之后创建数据库表就是创建失败,创建数据库表默认表名使用的是实体类名,user、group通常在数据库管理系统中用来管理用户账号,order在SQL语句中有order by的类型,也就是说它们的表名其实是数据库的关键字,用来创建数据库表一定会导致异常失败。为了避免出现这种情况需要能够指定实体创建的表名,创建表的时候通常都要求指定主键,定义的实体类有多个属性哪个属性是主键呢?Java里有注解Annotation对象用来在代码中做标记,注解可以在编译时运行时读取配置的数据,表名和主键都需要在运行时读取,就需要使用运行时的注解。

// 数据表主键和表名注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Id {
    String value() default "";
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Table {
    String name();
}

注解使用@interface定义,注解的属性定义与方法很类似,属性类型后面跟着属性名,属性名后面有一个”()“,属性还可以指定默认的值,如果注解里没有定义任何属性,它就会有一个value的属性。自定义注解还需要定义留存类型@Retention和目标类型@Target,留存类型分为Source源代码保留,Class编译的类文件保留,Runtime运行时保留,在JVM将类加载到虚拟机中时Source和Class保留都会被删除掉,Runtime保留的注解将会被加载到内存中;目标类型表明注解能够标注的类型,Table目标类型是Type就代表能标注类、接口,Id的目标类型是字段,也就能标注属性值。

@Table(name = "t_student")
public class Student {
         @Id
         private int id;
        ......
}

在Class、Field、Method等类型上都会有isAnnotationPresented()方法用来判断它们有没有注解,判断有注解后可以调用getAnnotation()获取注解对象,调用注解对象的属性方法就能够获取到开发者设置的注解值。

if (entityClass.isAnnotationPresent(Table.class)) {
         Table table = entityClass.getAnnotation(Table.class);
         mTableName = table.name();
}

到目前为止已经清楚如何获取数据库表和主键,通过反射来封装创建和设置对象,使用泛型机制封装不同的实体类,现在开始实现查找基类的查询操作,查询操作需要首先生成Select SQL语句。在Java开发中使用反射获取信息比较耗时,为了避免每次操作都要查找,通常都会使用缓存机制在第一次查找成功之后将信息保存起来,后面再需要获取信息可以直接使用的缓存对象。

// BaseDao<T>基础实现
public class BaseDao<T> {
    private static final String TAG = "BaseDao";
    private SQLiteDatabase mDb;
    protected Class<?> entityClass;  // 实体类缓存对象
    private String mTableName; // 数据库表对应的表名
    // 主键相关内容
    private String mIdName; // 主键名称
    private Field mIdField; // 主键对应的属性
    // 获取数据库表名
   private String getTableName() {
// 如果之前没有缓存过表名
        if (TextUtils.isEmpty(mTableName)) {
            Class<?> entityClass = getEntityClass();
            // 如果定义过Table注解,获取注解里的表名
            if (entityClass.isAnnotationPresent(Table.class)) {
                Table table = entityClass.getAnnotation(Table.class);
                mTableName = table.name();
            } else {
 // 否则默认使用实体类名作为表名
                mTableName = entityClass.getName();
            }
        }

        return mTableName;
    }
    // 获取主键名
    private String getIdName() {
parseIdInfo();
        return mIdName;
}

    // 获取主键对应的属性对象
    private Field getIdField() {
        parseIdInfo();
        return mIdField;
    }

    private void parseIdInfo() {
        if (mIdField == null) {
            Class<?> entityClass = getEntityClass();
            Field[] fields = entityClass.getDeclaredFields();
            // 遍历所有的属性对象
            for (Field field : fields) {
                // 如果包含Id注解,就代表该属性是主键
                if (field.isAnnotationPresent(Id.class)) {
                    mIdField = field;
                    Id id = field.getAnnotation(Id.class);
                    mIdName = id.value();
                    if (TextUtils.isEmpty(mIdName)) {
                        mIdName = field.getName();
                    }
                    break;
                }
            }

            if (mIdField == null) {
                throw new RuntimeException("primary key must define");
            }
            mIdField.setAccessible(true);
        }
    }
.....
}

代码BaseDao在成功获取到泛型实体类型的基础上,通过反射机制解析注解在实体类型上的@Table表名注解和@Id主键注解,@Table是直接注解在实体类上的通过Class.getAnnotation()可以直接获取到注解实例,@Id是注解在字段Field上的,需要遍历Class. getDeclaredFields()返回的所有字段对象,通过Field.getAnnotation()接口获取到注解实例,解析出这两个注解元素就实现了数据库表名和主键名与Java实体类型和主键字段的对应,其他的字段为了方便都统一认定实体字段名和数据库表列名一一对应。

数据访问封装

上面的代码已经将表名、主键名和属性都已经准备好了,现在可以自动生成通过主键加载一条Student记录并且生成Student对象返回的逻辑,下面的代码通过前面解析出来的表名和主键名生成根据主键加载实体的SQL语句。

// 加载SQL语句生成实现
// 使用StringBuilder生成SELECT * FROM tableName  where 主键名 = ?的SQL语句
private void generateLoadSQL(Field[] fields) {
    StringBuilder builder = new StringBuilder("SELECT * FROM ");
    builder.append(getTableName());
    builder.append(" WHERE ");
    builder.append(getIdName());
    builder.append(" = ?;");
    mLoadSql = builder.toString();
    Log.e(TAG, mLoadSql);
}

在BaseDao.load(Object key)方法中先生成加载实体类的SQL语句,接着调用SQLiteDatabase.rawQuery()方法查询并且将查询到的Cursor对象传递给createEntity()方法并生成实体对象返回。

// 加载数据库对象实现
public T load(Object key) {
// 首先生成加载加载SQL语句
    if (TextUtils.isEmpty(mLoadSql)) {
        generateLoadSQL(fields); 
    }
    Cursor cursor = null;
    try {
        // 执行加载操作,如果请求到了对应主键的记录就生成对象
        cursor = mDb.rawQuery(mLoadSql, new String[]{String.valueOf(key)});
        if (cursor.moveToNext()) {
            return createEntity(cursor);
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        IOUtils.close(cursor);
    }
    return null;
}

createEntity()方法内部会调用getEntityClass()获取实体类的Class对象,之后调用Class.newInstance()方法创建实体对象,接着遍历实体类的所有属性字段,为创建的新实体对象设置所有的属性值,这样就封装了创建实体对象和设置属性值的操作。

// Cursor转换成Java对象实现
private T createEntity(Cursor cursor) throws InstantiationException, IllegalAccessException {
    Field[] fields = getEntityClass().getDeclaredFields(); // 获取数据类型所有字段
    if (!CollectionUtils.isEmpty(fields)) {
        return null;
    }
T entity = (T) getEntityClass().newInstance(); // 反射生成对象
    for (Field field : fields) {
        field.setAccessible(true);
        field.set(entity, getFieldValue(field, cursor)); // 设置对象所有属性值
    }
    return entity;
}

// 这里由于篇幅限制就删除了short byte float等其他基础类型的判断
private Object getFieldValue(Field field, Cursor cursor) {
    Class<?> fieldType = field.getType();
    Object value = null;
    if (fieldType == int.class || fieldType == Integer.class) {
        // 如果属性是int类型就调用cursor.getInt()获取值
        value = cursor.getInt(cursor.getColumnIndex(field.getName()));
    } else if (fieldType == char.class || fieldType == Character.class) {
        // 如果是Character类型使用cursor.getString()获取值后只取第一个字符
        String str = cursor.getString(cursor.getColumnIndex(field.getName()));
        if (str.length() > 0) {
            value = str.charAt(0);
        }
    } else if (fieldType == boolean.class || fieldType == Boolean.class) {
       String str = cursor.getString(cursor.getColumnIndex(field.getName()));
       value = "true".equals(str);
    } else if (fieldType == double.class || fieldType == Double.class) {
        value = cursor.getDouble(cursor.getColumnIndex(field.getName()));
    } else if (fieldType == String.class) {
        value = cursor.getString(cursor.getColumnIndex(field.getName()));
    }
    return value;
}

属性值是从数据库中查询得到的Cursor对象获取的,Cursor需要根据数据类型得到某个特定值,这里就根据属性的类型通过多个if判断来决定获取调用Cursor对应的方法,由于数据库表列名和实体类的字段名相互对应,getColumnIndex()方法传入列名直接使用Field.getName()即可。还要注意int.class和Integer.class是不同的,如果在实体类中使用int定义的属性那么它的类型就是int.class,如果定义时用的是Integer那么它的类型就是Integer.class。由于数据库表中有时候返回的数据可能是空对象,使用Integer定义的属性调用时很可能会抛出NullPointerException导致应用退出,因此推荐在实体定义时使用原生数据类型。前面的加载封装对任意类型的实体数据访问对象都是通用的,定义在BaseDao就可以了,其他的实体Dao继承了BaseDao就会自动拥有按主键加载的方法;类似的向插入、按主键删除、更新和列表查询都可以使用相同的方法封装在BaseDao中。
对反射比较熟悉的开发者都知道反射在性能上是有缺陷的,频繁的使用反射会降低应用的性能,在大多数据的应用中都不推荐大量的使用反射机制。如果不使用反射封装数据库操作的样板代码,还是需要像简单封装一样为每个数据实体都生成单独的Dao对象,好在Java中提供了注解处理工具,它能够在编译时读取开发者在源代码中标注的注解,获取注解信息后就可以做处理操作生成对应的代码,这样用户就只需要标注注解就能够自动生成Dao对象。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值