一文搞懂反射泛型和反射注解以及通过注解方式写一个BaseDao

1418

反射泛型和反射注解概括起来就三步:

  • 自定义注解
  • 通过反射获取注解值
  • 使用自定义注解

最终案例

通过自定义注解,将数据库表与Java对象映射,在不需要配置文件的情况下,查询出数据库的记录
分析:数据库表有数据库名,表名和字段,所以在定义注解时这些都是必须的,但是在查询时有通过主键查询的方式,那么如何知道哪个字段是主键呢,这就需要来声明一下,所以,还需要定义一个id注解用于标识某对象的某属性对应着数据库的主键。此时就需要来说明一下什么是注解。

注解

什么是注解

  • 语法:@注解名称
  • 注解的作用:替代xml配置文件
  • Servlet3.0中就可以不在使用web.xml文件,而是所有配置文件都使用注解。
  • 注解是由框架来读取的!

注解的使用!

  • 定义注解类(框架的工作)
  • 使用注解(我们的工作)
  • 读取注解(反射读取注解)(框架的工作)

注解其实也是一个类

如何定义注解类

class A{}定义类
interface A{}定义接口

@interface A{} 定义注解

  • 天下所有的注解都是Annotation的子类
    不用特意标记默认就是

如何使用注解

首先自定义一个注解:

//自定义注解类
package annotation;
@interface MyAnnotation {
//暂时不写内容,只是演示注解的使用
}

使用自定义MyAnnotation注解

package annotation;
@MyAnnotation//注解能放在类上
public class Demo1 {
	@MyAnnotation//注解能放在成员变量上
	private String name;
	
	@MyAnnotation//注解能放在构造器上
	public void Demo1(){
		
	}
	
	@MyAnnotation//注解能放在方法上
	public void test1(){
		
	}
	
	public void test2(@MyAnnotation String name){//注解能放在参数上
		//注解能放在局部变量上
		@MyAnnotation
		String username = "hello";
	}
	//但是注解不能放在方法的调用上
}

注解的作用目标

由上述使用注解的例子可以看出,注解的作用目标有一下几种:

  • 方法
  • 构造器
  • 参数
  • 局部变量

注解的属性(依赖文档了解)

  • 定义属性
    格式:类型 属性名()
    例如:
//注解类
@interface MyAnno1{
	int age();//这是属性不是方法
	String name();//括号后面不能跟{},也不能加参数,因为这不是方法,而是注解的属性
}
  • 使用注解时给属性赋值
//使用属性时,属性必须要求有值
@MyAnno1(age=100,name="zhangsan")
  • 注解属性的默认值
//注解类
@interface MyAnno2{
	int age() default 100;//这是属性不是方法
	String name();
}
使用时:@MyAnno2(name="zhangsan")
也可以在给一个age值如:
@MyAnno2(age="999",name="zhangsan")
此时值会覆盖默认值
  • 名为value的属性的特权
在使用注解时,如果只给名为value的属性赋值,那么可以不给出属性的名称直接给出值
例如:
//注解的特权
@interface MyAnno3{
	int value();
	String name() default "hello world";
}
使用注解时:
@MyAnno3(100)相当于给value的属性赋值不用指定value
  • 注解属性的类型
注解属性有8中基本类型:
- String
- Enum
- Class
- 注解类型(和循环体的内容是循环体是一个道理)
- 以上类型的一维数组类型 int[],(二维数组不行)
注意:Integer包装类型不能使用

如何使用:
@interface MyAnno4{
	int a();
	String b();
	MyEnum c();
	Class d();
	MyAnno2 e();
	String[] f();
}
enum MyEnum{
	A,B,C
}

@MyAnno4(
	a=100,
	b="hello",
	c=MyEnum.A,
	d=String.class,
	e=@MyAnno2(name = "zhangsan"),
	f={"hello","world"}
)
public class Demo3 {

}
当给数组类型的属性赋值时,可以省略大括号
如f="hello"

注解的作用目标限定以及保存策略限定

让一个注解他的作用目标只能在类上不能在方法上,这就叫做目标的限定

  • 在定义注解时给注解添加注解叫做@Target
@Target(value={ElementType.TYPE,ElementType.ANNOTATION_TYPE,ElementType.METHOD,ElementType.FIELD})
@interface MyAnno1{
	
}

保留策略

  • 源代码文件(SOURCE):注解只在源代码中存在,在编译时就别忽略了(无法反射)
  • 字节码文件(CLASS):注解在源代码中存在,编译时会把注解信息放到class中存在,但JVM在类时会被忽略加载注解(无法反射)
  • JVM中(RUNTIME):注解在源代码,字节码文件中存在,并且在JVM加载类时会把注解加载到JVM内存中(它是唯一可以反射的注解)

限定注解的而保留策略

@Retention(RetentionPolicy.RUNTIME)//保留策略
@interface MyAnno1{
	
}

读取注解

反射泛型信息:

Class --> Type getGenericSuperclass()
Type --> ParameterizedType,把Type强转为ParameterizedType类型
ParameterizedType --> 参数化类型 = A<String>
ParameterizedType :Type[]  getActualTypeArguments(),A<String>中的String
Type[]就是Class[],我们就得到了类型参数了!

通过上述描述的步骤既可以获的类型参数

public class Demo1 {
	@Test
	public void fun1(){
		new B();//执行得到java.lang.String
	}
}
class A<T> {
	public A() {
		/*
		 * 在这里获取子类传递的泛型信息,要得到一个Class
		 */
//		Class clazz = this.getClass();//得到子类的类型
//		Type type = clazz.getGenericSuperclass();//获取传递给父类参数化类型
//		ParameterizedType pType=(ParameterizedType)type;//它就是A<String>
//		Type[] types = pType.getActualTypeArguments();//它就是一个Class数组
//		Class c = (Class)types[0];
//		System.out.println(c.getName());//String或Integer
		
		//将上面注释的内容变成一句话
		Class c = (Class)((ParameterizedType)(this.getClass().
				getGenericSuperclass())).getActualTypeArguments()[0];
		System.out.println(c.getName());
	}
}
class B extends A<String> {
	
}
class C extends A<Integer> {
	
}

反射注解

上面讲述了如何通过反射来得到类的类型,那么要通过反射得到注解,要求注解的保留策略必须是RUNTIME

反射注解需要从作用目标开始反射

  • 类上的注解,需要使用Class来获取
  • 方法上的注解需要Method来获取
  • 构造器上的注解需要Constructor来获取
  • 成员山谷的需要使用Field来获取

一下拥有可以获取注解的方法:

  • Class:
  • Method、Constructor、Field有共同的父类:AccessibleObject
    它们都有一个方法:
  • Annotation getAnnotation(Class),返回目标上指定类型的注解
  • Annotation[] getAnnoations(),返回目标注解上所有的注解

定义一个注解MyAnno1 ,在定义一个类A来使用注解

@MyAnno1(name="A类",age=20,sex="男")
class A{
	@MyAnno1(name="test1方法",age=10,sex="女")
	public void test(){
		
	}
}
@Retention(RetentionPolicy.RUNTIME)//注意声明保留策略,否则获取不到
@interface MyAnno1 {
	String name();
	int age();
	String sex();
}

通过下面的代码来演示如何获得作用在A类上的注解

public class Demo2 {
       @Test
	public void test1(){
		/*
		 * 1.得到作用目标
		 */
		Class<A> c = A.class;
		/*
		 * 2.获取指定类型的注解
		 */
		MyAnno1 myAnno1 = c.getAnnotation(MyAnno1.class);
		System.out.println(myAnno1);//结果@demo2.MyAnno1(name=A类, age=20, sex=男)
	}

    @Test
	public void test2() throws NoSuchMethodException, SecurityException{
		/*
		 * 1.得到作用目标
		 */
		Class<A> c = A.class;
		Method method = c.getMethod("test");
		
		/*
		 * 2.获取指定类型的注解(获取方法上的注解)
		 */
		MyAnno1 myAnno1 = method.getAnnotation(MyAnno1.class);
		System.out.println(myAnno1.name()+ ", " + myAnno1.age() + ", " +myAnno1.sex());
	}
}

通过上述内容,我们了解了什么是如何通过反射获取类型参数,以及什么是注解,注解该如何通过反射获取,那么作为练习,下面就完成通过注解来写一个BaseDao的实验

完成注解案例

首先我们根据实验的分析,创建注解类

/**  
* <p>Title: Table</p>  
* <p>Description: Table 注解类用于标识哪个对象对应数据库的哪一张表,作用在类上</p>  
* @author guqin  
* @date 2018年9月23日  
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface Table {
	String value();
}
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**  
* <p>Title: Column</p>  
* <p>Description: 该注解类用于将数据库字段映射到java对象属性上</p>  
* @author guqin  
* @date 2018年9月23日  
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
	String value();
}
/**  
* <p>Title: ID</p>  
* <p>Description: ID注解类用于在对象属性上标识哪一个字段是数据库主键</p>  
* @author guqin  
* @date 2018年9月23日  
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface ID {
	String value();
}

为什么没有数据库名的注解呢,众所周知,连接数据库需要指定库名,用户名和密码才能访问,这些信息都是配置在外部的配置文件中的,比如JDBC、Hibernate或者其他,于此同时还会使用数据库连接池比如DBCP、C3P0连接池等,那么数据库名及用户名密码就会配置在这些的配置文件当中,比如可以写一个jdbc.properties,所以是不需要指定库名的注解的而且这中信息还是多个对象公用的。
下面以查询user表为例来映射一个User对象:

import guqing.basedao.Column;
import guqing.basedao.ID;
import guqing.basedao.Table;

/**  
* <p>Title: User</p>  
* <p>Description: </p>  
* @author guqin  
* @date 2018年9月23日  
*/
@Table("user")//它的值表示当前类对应的表
public class User {
	@ID("u_id")//当前属性对应的列明,而且说明这个列是主键列
	private String uid;
	
	@Column("uname")
	private String username;
	
	@Column("password")
	private String password;
	
	@Column("state")
	private boolean state;
	
	@Column("price")
	private double price;
	
	//get set方法省略
}

完成了以上工作之后,现在就需要:

  • 获取注解的信息
  • 拼接sql语句查询数据库
  • 映射结果集

获取注解信息

package guqing.basedao;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

/**  
* <p>Title: MatchField</p>  
* <p>Description: 该类用于字段匹配,将数据库字段与java对象属性映射 </p>  
* @author guqin  
* @date 2018年9月26日  
*/
public class MatchField {
	private static String primary;//数据库主键
	private static String primaryName;//标有@ID注解的对象属性名
	
	//通过单例设计模式,返回唯一实例
	private static MatchField matchField = new MatchField();
	//私有构造方法,对外提供获取该对象的方法来拿到唯一静态实例
	private MatchField(){}
	
	/**
	* @Title: getMatchFieldInstance  
	* @Description: 获取该对象的句柄  
	* @param @return
	* @return MatchField
	* @throws
	 */
	public static MatchField getMatchFieldInstance() {
		return matchField;
	}
	
	/**
	* @Title: MappingField2Map  
	* @Description: 字段映射方法,返回一个Map,key=对象属性名 ,value=数据库字段名
	* @param @param beanClass
	* @param @return
	* @return Map<String,String>
	* @throws
	 */
	public Map<String,String> MappingField2Map(Class<?> beanClass){
		Map<String,String> fieldMapping = new HashMap<String,String>();
		
		//反射获取到主键的值
		Field[] fields = beanClass.getDeclaredFields();
		for(Field field : fields){//遍历之
			ID id = field.getAnnotation(ID.class);
			Column column = field.getAnnotation(Column.class);
			if(id!=null) {
				//拿到至关重要的数据库主键字段与@ID注解的对象属性名,将其放到Map中形成映射键值对
				primary = id.value();
				primaryName = field.getName();
				fieldMapping.put(primaryName, primary);//对象的成员变量名-->表字段名
				
			} else if(column!=null) {
				//拿到其他普通数据库字段与普通字段对应的java对象属性,同样放到Map中形成映射关联
				fieldMapping.put(field.getName(), column.value());
				
			} else {
				//如果都不是那么默认就是字段名-->对应数据库字段名
				//也就是对象属性可以不标识注解表示与数据库字段同名
				//也可以自己再改进一下比如设置默认值或者驼峰命名等
				//甚至还可以写一个xml配置文件用于配置数据库字段与对象属性的映射
				//然后读取配置文件的内容,这就比较类似于hibernate,总之自由发挥吧
				fieldMapping.put(field.getName(), field.getName());
			}
		}
		
		//如果用户忘记注解主键那么抛出异常
		if(primaryName==null||primary==null){
			throw new RuntimeException("syntax error:unknown primary key columns,"
					+ " try again after annotating the primary key in the Javabean with @ID(value)");
		}
		//返回数据库字段与对象属性映射关系的Map
		return fieldMapping;
	}
	
	/**
	* @Title: precursorSelectSql  
	* @Description: 通过上面的MappingField2Map方法拼凑查询语句的前半部分 
	* @param @param beanClass
	* @param @return
	* @return String
	* @throws
	 */
	public String precursorSelectSql(Class<?> beanClass){
		//通过beanClass得到字段映射Map
		Map<String,String> mappingField = MappingField2Map(beanClass);
		
		//拿到javaBean所有成员变量
		Field[] fields = beanClass.getDeclaredFields();
		
		//创建查询语句前驱
		StringBuilder sb = new StringBuilder("select ");
		//拼凑sql语句
		for(int i=0;i<fields.length;i++){
  			sb.append("`"+mappingField.get(fields[i].getName())+"`");//通过键获得表的字段名称
  			sb.append(" as "+"`"+fields[i].getName()+"`");
  			if(i<fields.length-1){
  				sb.append(",");//最后一个不加逗号
  			}
  		}
		sb.append(" ");
		return sb.toString();
	}

	/**
	* @Title: getBeanPrimaryName  
	* @Description: 返回javabean中作为主键的成员变量名称  
	* @param @return
	* @return String
	* @throws
	 */
	public String getBeanPrimaryName(){
		return primaryName;
	}
}

其实有了数据库字段与对象属性的映射就可以比较方便的完成查询更新删除添加等操作了,都是写平凑sql语句的活。缺点是只能进行简单属性的映射。
下面就是通过上面的映射方式写的BaseDao可以参考下一:

package guqing.basedao;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanHandler;
import org.apache.commons.dbutils.handlers.BeanListHandler;

import cn.itcast.jdbc.TxQueryRunner;

/**  
* <p>Title: BaseDao1</p>  
* <p>Description: </p>  
* @author guqin  
* @date 2018年9月23日  
*/
public class BaseDao<T> {
	private QueryRunner qr = new TxQueryRunner();
	private Class<T> beanClass;//通过父类获取子类类型
	private String primaryName;//Dao主键字段名称
	private String tablename;//表名称
	private Map<String,String> mappingField;//存储字段映射
	//获取到MappingField对象
	private MatchField matchField = MatchField.getMatchFieldInstance();
	
	//构造方法
	@SuppressWarnings("unchecked")
	public BaseDao(){
		//通过父类获取子类类型
		beanClass = (Class<T>)((ParameterizedType)this.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
		
		//先获取字段映射Map
		mappingField = matchField.MappingField2Map(beanClass);
		//再获取主键名称
		primaryName = matchField.getBeanPrimaryName();
		
		//获取到表名称
		Table table = beanClass.getAnnotation(Table.class);
		if(table!=null){
			tablename = table.value();
			
		} else {
			String clazzSimpleName = beanClass.getSimpleName();
			tablename = clazzSimpleName.substring(0, 1).toLowerCase() + clazzSimpleName.substring(1);
		}
	}
	
	public void save(T bean) throws SQLException{
		//String sql ="insert into 表名 values(几个?)";
		
		//通过反射将类中属性的个数清楚就是问号的个数
		Field[] fields = beanClass.getDeclaredFields();

		//获取成员变量上的注解
		List<Object> params = new LinkedList<Object>();
		
		String sql ="insert into " + "`"+tablename +"`"+" values(";
		//拼凑sql字符串
		for(int i=0;i<fields.length;i++){
			sql += "?";
			//拼凑参数
			try {
				params.add(beanClass.getMethod(getGetMethodString(fields[i].getName())).invoke(bean));
			} catch (Exception e) {
				
				try {
					Object param = beanClass.getMethod(getIsMethodString(fields[i].getName())).invoke(bean);
					if(param=="false"){
						params.add(0);
					}else if(param=="true"){
						params.add(1);
					}else{
						params.add(param);
					}
					
				} catch (Exception e1) {
					e1.printStackTrace();
				}
			}
			if(i < fields.length-1){
				sql +=",";
			}
		}
		sql += ")";
		
		qr.update(sql,params.toArray());
	}
	
	
	/**
	* @Title: update  
	* @Description: 更新方法  
	* @param @param bean
	* @return void
	* @throws
	 */
	public void update(T bean){
		try {
			Map<String,Object[]> updateSqlParam = bean2UpdateSql(bean);
			
			/*
			 * update car_number_track set ...+where cid=cid子句
			 */
			qr.update((String) updateSqlParam.get("sql")[0], updateSqlParam.get("params"));
		} catch (SQLException e) {
			throw new RuntimeException(e);
		}
	}

	
	/**
	* @Title: baseDelete  
	* @Description: 根据主键删除  
	* @param @param id
	* @param @return
	* @return Boolean
	* @throws
	 */
     public void deleteById(Object id){
         try{
     		String sql = "delete from " + "`" + tablename +"`"+ " where " + mappingField.get(primaryName) +"=?";
     		
     		qr.update(sql,id);
         } catch (Exception e) {
             e.printStackTrace();
            throw new RuntimeException(e);
         }
     }     
		
     
     /**
     * @Title: get  
     * @Description: 通过主键查询  
     * @param @param id
     * @param @return
     * @return T
     * @throws
      */
     public T get(Object id) {
    	 try {
    		String preSelectSql =  matchField.precursorSelectSql(beanClass);
    		preSelectSql = preSelectSql + "from " + "`" + tablename +"`" + " where " + mappingField.get(primaryName)+"=?";
    		
			return qr.query(preSelectSql, new BeanHandler<T>(beanClass),id);
		} catch (SQLException e) {
			throw new RuntimeException(e);
		}
     }
     
     /**
     * @Title: list  
     * @Description: 查询方法  
     * @param @return
     * @return List<T>
     * @throws
      */
     public List<T> list(){
    	try {
    		//查询语句的前半部分 + 用户自定义的后半部分查询语句
    		String querySql = matchField.precursorSelectSql(beanClass) + Query.getThatSql();
    		
    		if(querySql.contains(":")||querySql.contains("?")){
    			throw new RuntimeException("Syntax error: the number of parameters is insufficient."
    					+ " Please try again after checking[大哥参数个数没给够]");
    		}
    		
			return qr.query(querySql, new BeanListHandler<T>(beanClass));
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
     }

	/**
	* @Title: getGetMethodString  
	* @Description: 通过成员变量属性名称获取到set方法  
	* @param @param fieldName
	* @param @return
	* @return String
	* @throws
	 */
	private String getGetMethodString(String fieldName) {
		String getMethodString = "get" + fieldName.substring(0, 1).toUpperCase() +
				fieldName.substring(1);
		return getMethodString;
	}
	
	private String getIsMethodString(String fieldName) {
		String getMethodString = "is" + fieldName.substring(0, 1).toUpperCase() +
				fieldName.substring(1);
		return getMethodString;
	}

	private Map<String,Object[]> bean2UpdateSql(T bean){
		List<Object> params = new ArrayList<Object>();
		Map<String,Object[]> updateSqlParams = new HashMap<String,Object[]>();
		
		//给出sql语句的前半部分
		StringBuilder firstHalfSql = new StringBuilder("update "+"`"+tablename+"`");//前缀
		StringBuilder setSql = new StringBuilder();//后缀
		
		Object primaryValue = null;
		//先在第一个占位符也就是cid出添加参数
		setSql.append(" set "+"`"+mappingField.get(primaryName)+"`"+"=?");
		try {
			primaryValue = beanClass.getMethod(getGetMethodString(primaryName)).invoke(bean); 
			params.add(primaryValue);
		} catch (Exception e) {
			throw new RuntimeException(e);
		} 
		
		/*
		 * 1.判断条件,完成sql中追加where子句
		 * 2.创建ArrayList,保存参数
		 */
		Field[] fields = beanClass.getDeclaredFields();
		for(Field field : fields){
			if(field.getName().equals(primaryName)){
				continue;//略过主键
			}
			Column column = field.getAnnotation(Column.class);
			try {
				String returnType = beanClass.getMethod(getGetMethodString(field.getName())).getGenericReturnType().getTypeName();
				Object getMethod = beanClass.getMethod(getGetMethodString(field.getName())).invoke(bean);
			
				if(returnType.contains("String")||returnType.contains("Integer")){
					String param = (String) getMethod;
					if(param!=null && !param.trim().isEmpty() && param!="null" && column!=null){
						setSql.append(","+"`"+column.value()+"`"+"=?");
						params.add(param);
						
					} else if(param!=null && !param.trim().isEmpty() && param!="null" && column==null) {
						setSql.append(","+"`"+ mappingField.get(field.getName()) +"`"+"=?");
						params.add(param);
					}
				}else if(returnType.contains("double")){
					double param =  (double)getMethod;
					if(param!=0.0 && column!=null){
						setSql.append(","+"`"+column.value()+"`"+"=?");
						params.add(param);
						
					} else if(column==null) {
						setSql.append(","+"`"+ mappingField.get(field.getName()) +"`"+"=?");
						params.add(param);
					}
				}else if(returnType.contains("int")){
					int param = (int) getMethod;
					if(param!=0 && column!=null){
						setSql.append(","+"`"+column.value()+"`"+"=?");
						params.add(param);
						
					} else if(column==null) {
						setSql.append(","+"`"+ mappingField.get(field.getName()) +"`"+"=?");
						params.add(param);
					}
				} else if(returnType.contains("boolean")){
					Object param =  (Object)getMethod;
					if(column!=null && param!=null && !param.equals("null")){
						setSql.append(","+"`"+column.value()+"`"+"=?");
						params.add((boolean)param);
						
					} else if(param==null){
						continue;
					}else{
						setSql.append(","+"`"+ mappingField.get(field.getName()) +"`"+"=?");
						params.add((boolean)param);
					}
				}
			} catch (Exception e) {
				try {
					String returnType = beanClass.getMethod(getIsMethodString(field.getName())).getGenericReturnType().getTypeName();
					Object getMethod = beanClass.getMethod(getIsMethodString(field.getName())).invoke(bean);
					
					if(returnType.contains("boolean")){
						Object param =  (Object)getMethod;
						if(column!=null && param!=null && !param.equals("null")){
							setSql.append(","+"`"+column.value()+"`"+"=?");
							params.add((boolean)param);
							
						} else if(param==null){
							continue;
						}else{
							setSql.append(","+"`"+ mappingField.get(field.getName()) +"`"+"=?");
							params.add((boolean)param);
						}
					}
				} catch (Exception e1) {
					e1.printStackTrace();
				}
			}
			
		}
		/*
		 * 追加where语句的参数
		 */
		String finalSql = firstHalfSql.append(setSql).append(" where "+
						"`"+mappingField.get(primaryName)+"`"+"=?").toString();
		params.add(primaryValue);
		
		String[] sql = {finalSql};
		
		updateSqlParams.put("sql",sql);
		updateSqlParams.put("params", params.toArray());
		
		return updateSqlParams;
	}
	
}