[关闭]
@liayun 2016-09-20T06:24:22.000000Z 字数 9579 阅读 1567

注解(二)——解析注解案例

java基础加强


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

解析注解的简单案例

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

  1. @Retention(RetentionPolicy.RUNTIME)
  2. public @interface DbInfo {
  3. String driver();
  4. String url();
  5. String username();
  6. String password();
  7. }

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

  1. public class JdbcUtils {
  2. @DbInfo(driver="com.mysql.jdbc.Driver", url="jdbc:mysql://localhost:3306/bookstore", username="root", password="yezi")
  3. public static Connection getConnection() {
  4. try {
  5. Method method = JdbcUtils.class.getMethod("getConnection", null); // 反射出getConnection()方法
  6. DbInfo info = method.getAnnotation(DbInfo.class); // 得到@DbInfo(...)注解
  7. String driver = info.driver();
  8. String url = info.url();
  9. String username = info.username();
  10. String password = info.password();
  11. System.out.println(driver);
  12. System.out.println(url);
  13. } catch (Exception e) {
  14. throw new RuntimeException(e);
  15. }
  16. }
  17. public static void main(String[] args) {
  18. JdbcUtils.getConnection();
  19. }
  20. }

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

  1. public class JdbcUtils {
  2. private static String driver;
  3. private static String url;
  4. private static String username;
  5. private static String password;
  6. static {
  7. try {
  8. // 解析注解,获取注解配置的信息
  9. Method method = JdbcUtils.class.getMethod("getConnection", null); // 反射出getConnection()方法
  10. DbInfo info = method.getAnnotation(DbInfo.class); // 得到@DbInfo(...)注解
  11. driver = info.driver();
  12. url = info.url();
  13. username = info.username();
  14. password = info.password();
  15. } catch (Exception e) {
  16. throw new RuntimeException(e);
  17. }
  18. }
  19. @DbInfo(driver="com.mysql.jdbc.Driver", url="jdbc:mysql://localhost:3306/bookstore", username="root", password="yezi")
  20. public static Connection getConnection() {
  21. System.out.println(driver);
  22. System.out.println(url);
  23. return null;
  24. }
  25. public static void main(String[] args) {
  26. JdbcUtils.getConnection();
  27. }
  28. }

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

解析注解的复杂案例

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

解析方法上的注解

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

  1. public class Book implements Serializable {
  2. blabla...
  3. }

那么我们就要编写其对应的Dao——BookDao.java去操作数据库,为了提升程序的数据库访问性能,我们决定在应用程序中加入C3P0连接池,所以在该工程中应导入如下Jar包:

在编写BookDao类的过程中,为了简化对JDBC的编写,我们就不可避免地要使用Apache组织提供的一个开源JDBC工具类库——commons-dbutils。那么这时BookDao类的代码可以写成:

  1. public class BookDao {
  2. public void add(Book book) {
  3. QueryRunner runner = new QueryRunner(...);
  4. blabla...
  5. }
  6. }

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

  1. public class BookDao {
  2. private ComboPooledDataSource ds;
  3. public void setDs(ComboPooledDataSource ds) {
  4. this.ds = ds;
  5. }
  6. public ComboPooledDataSource getDs() {
  7. return ds;
  8. }
  9. public void add(Book book) {
  10. QueryRunner runner = new QueryRunner(ds);
  11. blabla...
  12. }
  13. }

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

  1. @Retention(RetentionPolicy.RUNTIME)
  2. public @interface Inject {
  3. String driverClass() default "com.mysql.jdbc.Driver";
  4. String jdbcUrl() default "jdbc:mysql://localhost:3306/bookstore";
  5. String user() default "root";
  6. String password() default "yezi";
  7. }

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

  1. public class BookDao {
  2. /*
  3. * 任何类都是Object的孩子,也即BookDao这个类从Object类还继承了class属性
  4. */
  5. private ComboPooledDataSource ds;
  6. @Inject
  7. public void setDs(ComboPooledDataSource ds) {
  8. this.ds = ds;
  9. }
  10. public ComboPooledDataSource getDs() {
  11. return ds;
  12. }
  13. public void add(Book book) {
  14. QueryRunner runner = new QueryRunner(ds);
  15. blabla...
  16. }
  17. }

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

  1. public class DaoFactory {
  2. public static BookDao createBookDao() {
  3. BookDao dao = new BookDao();
  4. // 向dao注入一个连接池
  5. blabla......
  6. return dao;
  7. }
  8. }

如何向dao注入一个连接池呢?——我的思路是这样的:我首先会反射出BookDao类的所有属性,我看哪个属性的set方法上有注解,并且判断它这个方法上是不是要一个Inject注解,若是就用这个注解配置的信息来创建一个连接池,并注入进来。
大家可能有一个疑问,那就是为什么要反射出BookDao类的所有属性呢,我们只需要反射出ds这个属性,看该属性的set方法上有没注解即可了吧?原因是我们写的DaoFactory类要具有通用性,试想如果还有一个CategoryDao类。

  1. public class CategoryDao {
  2. private ComboPooledDataSource combods;
  3. @Inject
  4. public void setCombods(ComboPooledDataSource combods) {
  5. this.combods = combods;
  6. }
  7. }

该CategoryDao类的combods属性的set方法上才有注解。若想我们编写的DaoFactory类具有通用性,那么必须得反射出BookDao类的所有属性。
注意虽然我们要反射出BookDao类的所有属性,但是父类的属性我们是不要的哟,因为任何类都是Object的孩子,也即BookDao这个类从Object类还继承了class属性,所以该class属性是没必要解析出来的
走到这一步,接下来我们就要编写出完整的DaoFactory类了。

  1. public class DaoFactory {
  2. public static BookDao createBookDao() {
  3. BookDao dao = new BookDao();
  4. // 向dao注入一个连接池
  5. try {
  6. // 解析出dao所有的属性,父类(Object)的属性我不要(用内省技术)
  7. BeanInfo info = Introspector.getBeanInfo(dao.getClass(), Object.class);
  8. PropertyDescriptor[] pds = info.getPropertyDescriptors();
  9. for (int i = 0; pds != null && i < pds.length; i++) {
  10. // 得到bean的每一个属性描述器
  11. PropertyDescriptor pd = pds[i];
  12. Method setMethod = pd.getWriteMethod(); // 得到属性对应的set方法
  13. // 看set方法上有没有Inject注解
  14. Inject inject = setMethod.getAnnotation(Inject.class);
  15. if (inject == null) {
  16. continue;
  17. }
  18. // 若方法上有Inject注解,则用注解配置的信息创建一个连接池
  19. DataSource ds = createDataSourceByInject(inject, new ComboPooledDataSource());
  20. setMethod.invoke(dao, ds); // 用注解配置的信息创建出一个连接池之后,往dao注入进去
  21. }
  22. } catch (Exception e) {
  23. throw new RuntimeException(e);
  24. }
  25. return dao;
  26. }
  27. // 用注解的信息,为连接池配置属性
  28. private static DataSource createDataSourceByInject(Inject inject, DataSource ds) { // 传递进来的有可能是DBCP池,也有可能是C3P0池,这两个池的属性是不一样的,但是我们的代码要通用!
  29. // 获取到注解所有属性相应的方法,driverClass、url、equals、hashCode方法
  30. Method[] methods = inject.getClass().getMethods();
  31. for (Method m : methods) {
  32. String methodName = m.getName(); // 得到方法名,如equals、url
  33. PropertyDescriptor pd = null;
  34. try {
  35. /*
  36. * ds池上面有没有这个方法名对应的属性,又要通过内省技术
  37. * 现在用属性描述器去描述ds.getClass()这个Class上面有没有这个方法名对应的属性,
  38. * 若没有,就会抛异常,否则要继续下一轮循环。
  39. */
  40. pd = new PropertyDescriptor(methodName, ds.getClass()); // getEquals、getUrl
  41. Object value = m.invoke(inject, null); // 得到注解属性的值
  42. pd.getWriteMethod().invoke(ds, value); // 得到注解属性的值之后,要把这个值set到连接池相对应的属性上
  43. } catch (Exception e) {
  44. continue;
  45. }
  46. }
  47. return ds;
  48. }
  49. }

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

  1. public class TestFactory {
  2. public static void main(String[] args) throws SQLException {
  3. BookDao dao = DaoFactory.createBookDao();
  4. DataSource ds = dao.getDs();
  5. Connection conn = ds.getConnection();
  6. System.out.println(conn);
  7. }
  8. }

测试通过,没问题。其实如果我们足够细心的话,可以发现我们上面编写的DaoFactory类依然不够通用,问题出在代码

  1. DataSource ds = createDataSourceByInject(inject, new ComboPooledDataSource());

处,因为现在我们是自己new了一个连接池,但是我们是不应该new的,做的好的话,应该是我这个BookDao内省出所有的属性,内省出所有的属性之后,我看这个属性类型是什么,就应该创建一个什么样的连接池。这样真正具有通用性的DaoFactory类的代码为:

  1. public class DaoFactory {
  2. public static BookDao createBookDao() {
  3. BookDao dao = new BookDao();
  4. // 向dao注入一个连接池
  5. try {
  6. // 解析出dao所有的属性,父类(Object)的属性我不要(用内省技术)
  7. BeanInfo info = Introspector.getBeanInfo(dao.getClass(), Object.class);
  8. PropertyDescriptor[] pds = info.getPropertyDescriptors();
  9. for (int i = 0; pds != null && i < pds.length; i++) {
  10. // 得到bean的每一个属性描述器
  11. PropertyDescriptor pd = pds[i];
  12. Method setMethod = pd.getWriteMethod(); // 得到属性对应的set方法
  13. // 看set方法上有没有Inject注解
  14. Inject inject = setMethod.getAnnotation(Inject.class);
  15. if (inject == null) {
  16. continue;
  17. }
  18. // 若方法上有Inject注解,则用注解配置的信息创建一个连接池
  19. Class propertyType = pd.getPropertyType(); // 获取属性描述器描述的那个属性的类型
  20. Object datasource = propertyType.newInstance(); // 创建出这个属性需要内省的那个对象,即整出了一个连接池
  21. DataSource ds = (DataSource) createDataSourceByInject(inject, datasource);
  22. setMethod.invoke(dao, ds); // 用注解配置的信息创建出一个连接池之后,往dao注入进去
  23. }
  24. } catch (Exception e) {
  25. throw new RuntimeException(e);
  26. }
  27. return dao;
  28. }
  29. // 用注解的信息,为连接池配置属性
  30. private static DataSource createDataSourceByInject(Inject inject, DataSource ds) { // 传递进来的有可能是DBCP池,也有可能是C3P0池,这两个池的属性是不一样的,但是我们的代码要通用!
  31. // 获取到注解所有属性相应的方法,driverClass、url、equals、hashCode方法
  32. Method[] methods = inject.getClass().getMethods();
  33. for (Method m : methods) {
  34. String methodName = m.getName(); // 得到方法名,如equals、url
  35. PropertyDescriptor pd = null;
  36. try {
  37. /*
  38. * ds池上面有没有这个方法名对应的属性,又要通过内省技术
  39. * 现在用属性描述器去描述ds.getClass()这个Class上面有没有这个方法名对应的属性,
  40. * 若没有,就会抛异常,否则要继续下一轮循环。
  41. */
  42. pd = new PropertyDescriptor(methodName, ds.getClass()); // getEquals、getUrl
  43. Object value = m.invoke(inject, null); // 得到注解属性的值
  44. pd.getWriteMethod().invoke(ds, value); // 得到注解属性的值之后,要把这个值set到连接池相对应的属性上
  45. } catch (Exception e) {
  46. continue;
  47. }
  48. }
  49. return ds;
  50. }
  51. }

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

解析字段上的注解

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

  1. public class BookDao {
  2. /*
  3. * 任何类都是Object的孩子,也即BookDao这个类从Object类还继承了class属性
  4. */
  5. @Inject private ComboPooledDataSource ds; // 字段
  6. @Inject
  7. public void setDs(ComboPooledDataSource ds) {
  8. this.ds = ds;
  9. }
  10. public ComboPooledDataSource getDs() {
  11. return ds;
  12. }
  13. public void add(Book book) {
  14. QueryRunner runner = new QueryRunner(ds);
  15. blabla......
  16. }
  17. }

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

  1. public class DaoFactory {
  2. public static BookDao createBookDao() {
  3. BookDao dao = new BookDao();
  4. Field[] fields = dao.getClass().getDeclaredFields();
  5. for (int i = 0; fields != null && i < fields.length; i++) {
  6. Field f = fields[i];
  7. f.setAccessible(true);
  8. Inject inject = f.getAnnotation(Inject.class);
  9. if (inject == null) {
  10. continue;
  11. }
  12. // 代表当前获取到的字段上有Inject这个注解,则用注解的配置信息,创建一个连接池赋到字段上
  13. try {
  14. DataSource ds = (DataSource) f.getType().newInstance(); // 获取字段的类型,创建字段需要的连接池
  15. // 用注解的信息,配置上面创建的连接池
  16. inject2Datasource(inject, ds);
  17. f.set(dao, ds); // 调用字段的set方法把这个连接池设置到dao上去
  18. } catch (Exception e) {
  19. e.printStackTrace();
  20. }
  21. }
  22. return dao;
  23. }
  24. // 用注解的信息,配置连接池
  25. private static void inject2Datasource(Inject inject, DataSource ds) {
  26. Method[] methods = inject.getClass().getMethods();
  27. for (Method method : methods) {
  28. String name = method.getName(); // 得到注解的每一个方法,例如(jdbcUrl()、user()、password()、toString()、equals()、hashCode()...)
  29. // 获取ds上与方法名相对应的属性
  30. try {
  31. PropertyDescriptor pd = new PropertyDescriptor(name, ds.getClass());
  32. Object value = method.invoke(inject, null); // 得到注解属性的值
  33. // 把值赋到ds的属性上
  34. pd.getWriteMethod().invoke(ds, value);
  35. } catch (Exception e) {
  36. continue;
  37. }
  38. }
  39. }
  40. }

总结

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

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注