Java基础加强第四讲 注解(下)——解析注解案例

注解入门后,还不趁火打铁,将注解的应用弄得炉火纯青,更待何时。在本讲中,我们将通过3个例子来详解注解在实际开发中的应用。

解析注解的简单案例

我们首先关注一个解析注解的简单案列,由简入难,循序渐进,最后过渡到非常复杂的案例中。在实际项目中,我们通常需要编写一个JdbcUtils的工具类,用于得到与数据库的连接,而与数据库相关的基本配置信息我们通常是用一个配置文件来存储的,但现在我们希望用一个注解来替代配置文件。所以,接下来我就讲解一个解析注解的简单案列(即通过注解来给类注入一些基本信息进去)。
首先,我们编写一个DbInfo注解:

package cn.liayun.annotation2;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface DbInfo {

	String driver();

	String url();

	String username();

	String password();
	
}

千万要注意:当一个Annotation类型被定义为运行时Annotation后,该注释才是运行时可见,当class文件被载入时保存在class文件中的Annotation才会被虚拟机读取。所以对于注解DbInfo来说,@Retention(RetentionPolicy.RUNTIME)元注解不能丢失,否则会报空指针异常(java.lang.NullPointerException)。
接着,我们就编写JdbcUtils工具类,由于是第一次编写,代码可能会是这样:

package cn.liayun.annotation2;

import java.lang.reflect.Method;
import java.sql.Connection;

public class JdbcUtils {
	
	/*
	 * 任务,解析@DbInfo这个注解,来获取信息,来获取连接。
	 */
	@DbInfo(driver = "com.mysql.jdbc.Driver", url = "jdbc:mysql://localhost:3306/bookstore", username = "root", password = "liayun")
	public static Connection getConnection() {
		try {
			//反射出JdbcUtils类中的getConnection方法
			Method method = JdbcUtils.class.getMethod("getConnection", null);
			//一个方法可能有多个注解,得到一个DbInfo注解
			DbInfo info = method.getAnnotation(DbInfo.class);
			
			//获取DbInfo注解上的属性
			String driver = info.driver();
			String url = info.url();
			String username = info.username();
			String password = info.password();
			
			System.out.println(driver);
			System.out.println(url);
			
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		return null;
	}
	
	public static void main(String[] args) {
		JdbcUtils.getConnection();
	}
	
}

虽然上面的程序运行没问题,但还是不够优雅,因为注解DbInfo只需要解析一次就行了,所以我们可以在JdbcUtils工具类被加载时就解析该注解。这样该工具类的代码就应为:

package cn.liayun.annotation2;

import java.lang.reflect.Method;
import java.sql.Connection;

public class JdbcUtils {
	
	private static String driver;
	private static String url;
	private static String username;
	private static String password;
	
	//如何解析注解,获取注解配置的信息
	static {
		try {
			//反射出JdbcUtils类中的getConnection方法
			Method method = JdbcUtils.class.getMethod("getConnection", null);
			//一个方法可能有多个注解,得到一个DbInfo注解
			DbInfo info = method.getAnnotation(DbInfo.class);
			
			//获取DbInfo注解上的属性
			driver = info.driver();
			url = info.url();
			username = info.username();
			password = info.password();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	/*
	 * 任务,解析@DbInfo这个注解,来获取信息,来获取连接。
	 */
	@DbInfo(driver = "com.mysql.jdbc.Driver", url = "jdbc:mysql://localhost:3306/bookstore", username = "root", password = "liayun")
	public static Connection getConnection() {
		System.out.println(driver);
		System.out.println(url);
		return null;
	}
	
	public static void main(String[] args) {
		JdbcUtils.getConnection();
	}
	
}

这样代码看起来还算优雅吧!!!接下来,我们就要昂首踏步地进入解析注解的超复杂的案例中了。

解析注解的复杂案例

我们在做开发的时候,经常会碰到注解加到字段上或者方法上,结果这个类就会自动拥有某个对象。我们不禁要问,这是怎么一回事呢?为了弄明白,我们宁可花费大量的精力去深度剖析其内部的原理,这将对我们以后学习框架具有极大的帮助。我们首先关注加到方法上的注解。

解析方法上的注解

一个项目中会有很多实体类型,那么我们就要编写多个相对应的XxxDao,比如在工程中有这样一个实体类型(Book.java):

public class Book implements Serializable {
    //blabla...
}

那么我们就要编写与其相对应的BookDao去操作数据库,为了提升程序的数据库访问性能,我们决定在应用程序中加入C3P0连接池,所以在该工程中应导入如下Jar包:
在这里插入图片描述
在编写BookDao类的过程中,为了简化对JDBC的编写,我们就不可避免地要使用Apache组织提供的一个开源JDBC工具类库——commons-dbutils。那么这时BookDao类的代码可以写成:

public class BookDao {
    public void add(Book book) {
        QueryRunner runner = new QueryRunner(...);

        //blabla...
    }
}

在编写该类的过程中,我们发现QueryRunner类需要一个javax.sql.DataSource来作参数的构造方法。要想得到QueryRunner类的一个实例对象,必须传递一个数据库连接池进去。这样BookDao类的代码就应是这样:

public class BookDao {
    private ComboPooledDataSource ds;
    public void setDs(ComboPooledDataSource ds) {
        this.ds = ds;
    }
    public ComboPooledDataSource getDs() {
        return ds;
    }
    public void add(Book book) {
        QueryRunner runner = new QueryRunner(ds);

        //blabla...
    }
}

一般来说,我们在应用程序中加入C3P0连接池后,都要在类目录下加入C3P0的配置文件——c3p0-config.xml,里面配置的是与数据库相关的信息。但是我们已经学过注解了,而注解就是用于替代配置文件的,所以在该工程中我们打算用注解。
BookDao类在工作的时候需要一个连接池ds,那我就要在public void setDs(ComboPooledDataSource ds)方法上加入一个注解,注解起的作用是注入拥有些许属性的连接池进来,即通过注解注入对象。这样,我们编写的注解就应该是:

package cn.liayun.annotation2;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {

	String driverClass() default "com.mysql.jdbc.Driver";

	String jdbcUrl() default "jdbc:mysql://localhost:3306/bookstore";

	String user() default "root";

	String password() default "liayun";

}

Inject注解写完之后,BookDao类的代码就要改成:

public class BookDao {
    /*
     * 任何类都是Object的孩子,也即BookDao这个类还从Object类中继承了一个class属性
     */
    private ComboPooledDataSource ds;
    
    //使用@Inject注解注入一个数据源
    @Inject
    public void setDs(ComboPooledDataSource ds) {
        this.ds = ds;
    }
    
    public ComboPooledDataSource getDs() {
        return ds;
    }
    
    public void add(Book book) {
        QueryRunner runner = new QueryRunner(ds);

        //blabla...
    }
}

现在我们就要写一个解析程序来解析这个注解了,通过注解的配置信息来配置一个连接池进来。那这个解析程序的代码写在哪儿呢?BookDao类是由service层来调用的,一般service层会通过一个工厂去创建dao,那么在由工厂创建dao的时候,负责解析这个注解,给创建的dao配置一个连接池进去。也即这时我们要编写一个DaoFactory类。

public class DaoFactory {
    public static BookDao createBookDao() {
        BookDao dao = new BookDao();

        //向dao注入一个连接池
        //blabla......

        return dao;     
    }
}

如何向dao中注入一个连接池呢?我的思路是这样的:我首先会反射出BookDao类的所有属性,我看哪个属性相对应的set方法上有注解,并且判断它这个方法上是不是有一个Inject注解,若是就用这个注解配置的信息来创建一个连接池,并注入进来。大家可能有一个疑问,那就是为什么要反射出BookDao类的所有属性呢,我们只需要反射出ds这个属性,看该属性的set方法上有没注解即可了吧?原因是我们编写的DaoFactory类要具有通用性,试想如果还有一个CategoryDao类,如下:
在这里插入图片描述
该CategoryDao类的combods属性的set方法上才有注解。若想我们编写的DaoFactory类具有通用性,那么必须得反射出BookDao类的所有属性。温馨提示:虽然我们要反射出BookDao类的所有属性,但是父类的属性我们是不要的哟,因为任何类都是Object的孩子,也即BookDao这个类还从Object类中继承了一个class属性,所以该class属性是没必要解析出来的
走到这一步,接下来我们就要编写出完整的DaoFactory类了。

package cn.liayun.annotation2;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;

import javax.sql.DataSource;

import com.mchange.v2.c3p0.ComboPooledDataSource;

public class DaoFactory {
	
	public static BookDao createBookDao() {
		BookDao dao = new BookDao();
		
		//向dao中注入一个连接池
		/*
		 * 做法:我会反射出BookDao所有的属性,我看哪个属性相对应的set方法上有@Inject注解,
		 *     如果有这个注解的话,我就用这个注解配置的信息,创建出一个连接池,并注入进来。
		 */
		try {                                                        
			//反射出dao所有的属性,父类(Object)的属性我不要(用内省技术)
																	 //不要从Object类中继承的class属性
			BeanInfo info = Introspector.getBeanInfo(dao.getClass(), Object.class);
			PropertyDescriptor[] pds = info.getPropertyDescriptors();
			for (int i = 0; pds != null && i < pds.length; i++) {
				//得到bean的每一个属性描述器
				PropertyDescriptor pd = pds[i];
				Method setMethod = pd.getWriteMethod();//得到属性相对应的set方法
				
				//看set方法上有没有@Inject注解
				Inject inject = setMethod.getAnnotation(Inject.class);
				if (inject == null) {
					continue;
				}
				
				//方法上有@Inject注解,则用该注解配置的信息,创建一个连接池
				DataSource ds = createDataSourceByInject(inject, new ComboPooledDataSource());
				//用注解配置的信息创建出一个连接池之后,往dao上注入进去
				setMethod.invoke(dao, ds);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		return dao;
	}
	
	//用@Inject注解的配置信息,为连接池配置属性
	private static DataSource createDataSourceByInject(Inject inject, DataSource ds) {
		/*
		 * 获取到注解所有属性相对应的方法,
		 * 例如有driverClass()、jdbcUrl()等方法,但也有equals()和hashCode()方法
		 */
		Method[] methods = inject.getClass().getMethods();
		for (Method m : methods) {
			String methodName = m.getName();//得到方法的名称,例如equals、jdbcUrl
			//反射ds连接池上有没有方法名对应的属性,如果这个池上面有这个方法名对应的属性的话,那么这个必定就是一个属性
			PropertyDescriptor pd = null;
			try {
				/*
                 * ds池上面有没有这个方法名相对应的属性,又要通过内省技术
                 * 现在用属性描述器去描述ds.getClass()这个Class上面有没有这个方法名相对应的属性,
                 * 若没有,就会抛异常,要继续下一轮循环。
                 */
				pd = new PropertyDescriptor(methodName, ds.getClass());//getEquals()、getUrl()
				Object value = m.invoke(inject, null);//得到注解属性的值
				pd.getWriteMethod().invoke(ds, value);//得到注解属性的值之后,要把值给连接池相对应的属性上
			} catch (Exception e) {
				continue;
			}
		}
		return ds;
	}
	
}

编写好了上面的DaoFactory类的代码之后,我们就要测试其好不好使了。

package cn.liayun.annotation2;

import java.sql.Connection;
import java.sql.SQLException;

import javax.sql.DataSource;

public class TestFactory {

	public static void main(String[] args) throws SQLException {
		BookDao dao = DaoFactory.createBookDao();
		DataSource ds = dao.getDs();
		Connection conn = ds.getConnection();
		System.out.println(conn);
	}

}

测试通过,没问题。其实,如果我们足够细心的话,可以发现我们上面编写的DaoFactory类依然不够通用,问题出在下面这句代码:
在这里插入图片描述
因为现在我们是自己new了一个连接池,但是我们是不应该new的,做的好的话,应该是我这个DaoFactory内省出这个BookDao里面所有的属性,内省出所有的属性之后,我看这个属性的类型是什么,就应该创建一个什么样的连接池,如果属性的类型是DBCP,就创建出一个DBCP连接池来;反之,则创建出一个C3P0连接池来。这样真正具有通用性的DaoFactory类的代码为:

package cn.liayun.annotation2;

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;

import javax.sql.DataSource;

public class DaoFactory {
	
	public static BookDao createBookDao() {
		BookDao dao = new BookDao();
		
		//向dao中注入一个连接池
		/*
		 * 做法:我会反射出BookDao所有的属性,我看哪个属性相对应的set方法上有@Inject注解,
		 *     如果有这个注解的话,我就用这个注解配置的信息,创建出一个连接池,并注入进来。
		 */
		try {                                                        
			//反射出dao所有的属性,父类(Object)的属性我不要(用内省技术)
																	 //不要从Object类中继承的class属性
			BeanInfo info = Introspector.getBeanInfo(dao.getClass(), Object.class);
			PropertyDescriptor[] pds = info.getPropertyDescriptors();
			for (int i = 0; pds != null && i < pds.length; i++) {
				//得到bean的每一个属性描述器
				PropertyDescriptor pd = pds[i];
				Method setMethod = pd.getWriteMethod();//得到属性相对应的set方法
				
				//看set方法上有没有@Inject注解
				Inject inject = setMethod.getAnnotation(Inject.class);
				if (inject == null) {
					continue;
				}
				
				Class propertyTye = pd.getPropertyType();//获取属性描述器描述的那个属性的类型
				Object datasource = propertyTye.newInstance();//创建出这个属性需要内省的那个对象,即整出了一个连接池
				
				//方法上有@Inject注解,则用该注解配置的信息,创建一个连接池
				DataSource ds = (DataSource) createDataSourceByInject(inject, datasource);
				//用注解配置的信息创建出一个连接池之后,往dao上注入进去
				setMethod.invoke(dao, ds);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		
		return dao;
	}
	
	/*
	 * 为了更加通用,用@Inject注解的配置信息创建一个什么池,你把这个池传递给我。因为你有可能传递过来DBCP连接池,也有可能是C3P0连接池,
	 * 而这两个池的属性是不一样的。
	 * 
	 */
	//用@Inject注解的配置信息,为连接池配置属性
	private static Object createDataSourceByInject(Inject inject, Object ds) {
		/*
		 * 获取到注解所有属性相对应的方法,
		 * 例如有driverClass()、jdbcUrl()等方法,但也有equals()和hashCode()方法
		 */
		Method[] methods = inject.getClass().getMethods();
		for (Method m : methods) {
			String methodName = m.getName();//得到方法的名称,例如equals、jdbcUrl
			//反射ds连接池上有没有方法名对应的属性,如果这个池上面有这个方法名对应的属性的话,那么这个必定就是一个属性
			PropertyDescriptor pd = null;
			try {
				/*
                 * ds池上面有没有这个方法名相对应的属性,又要通过内省技术
                 * 现在用属性描述器去描述ds.getClass()这个Class上面有没有这个方法名相对应的属性,
                 * 若没有,就会抛异常,要继续下一轮循环。
                 */
				pd = new PropertyDescriptor(methodName, ds.getClass());//getEquals()、getUrl()
				Object value = m.invoke(inject, null);//得到注解属性的值
				pd.getWriteMethod().invoke(ds, value);//得到注解属性的值之后,要把值给连接池相对应的属性上
			} catch (Exception e) {
				continue;
			}
		}
		return ds;
	}
	
}

接下来,我们就来关注加到字段上的注解了。

解析字段上的注解

我们在做开发的时候,也有会碰到注解加到字段上的情况。如:

public class BookDao {
    /*
     * 任何类都是Object的孩子,也即BookDao这个类还从Object类中继承了一个class属性
     */
    //在字段上加入注解
    @Inject private ComboPooledDataSource ds; // 字段

    @Inject
    public void setDs(ComboPooledDataSource ds) {
        this.ds = ds;
    }

    public ComboPooledDataSource getDs() {
        return ds;
    }

    public void add(Book book) {
        QueryRunner runner = new QueryRunner(ds);

        //blabla......
    }
}

同理,现在我们就要写一个解析程序来解析字段上的注解,通过注解的配置信息来配置一个连接池进来。这样我们的DaoFactory类的代码可以写为:

package cn.liayun.annotation2;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

import javax.sql.DataSource;

public class DaoFactory {
	
	public static BookDao createBookDao() {
		BookDao dao = new BookDao();
		
		//拿到类上声明的所有的字段(包括私有)
		Field[] fields = dao.getClass().getDeclaredFields();
		for (int i = 0; fields != null && i < fields.length; i++) {
			Field f = fields[i];
			f.setAccessible(true);
			Inject inject = f.getAnnotation(Inject.class);
			if (inject == null) {
				continue;
			}
			
			//代表当前获取到的字段上有注入@Inject这个注解,则使用该注解信息,创建一个池赋值到字段上
			
			try {
				//获取字段的类型,然后创建什么类型的连接池
				DataSource ds = (DataSource) f.getType().newInstance();//获取字段的类型,创建字段需要的连接池
				
				//使用注解的信息,配置上面创建的连接池
				inject2DataSource(inject, ds);
				
				//调用字段的set方法把这个连接池设置到dao上去
				f.set(dao, ds);
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}
		
		return dao;
	}

	//使用注解的信息,配置连接池
	private static void inject2DataSource(Inject inject, DataSource ds) {
		Method[] methods = inject.getClass().getMethods();
		for (Method m : methods) {
			String name = m.getName();//得到注解的每一个方法,例如jdbcUrl()、user()、password()、toString()、hashCode()等方法
			
			//获取ds上与方法名相对应的属性
			try {
				PropertyDescriptor pd = new PropertyDescriptor(name, ds.getClass());
				Object value = m.invoke(inject, null);//得到注解属性的值
				pd.getWriteMethod().invoke(ds, value);//把值赋值到ds相对应的属性上
			} catch (Exception e) {
				continue;
			}
		}
	}
	
}

总结

将来在做开发的时候,经常会发现你写好一个类,只需要往字段上或方法上加上一个注解,再把这个类交给某个容器(例如Spring)管理,结果那个容器就会自动帮你注入一个对象,你那个时候就不需要自己傻逼兮兮地创建对象了。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李阿昀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值