Java注解应用案例--实现.class文件到sql建表语句的转换过程

项目Demo地址:点击这里
整体的思路:利用自定义的注解处理器(IProcessor及其实现),配合Class的Annotation相关API,对目标的.class文件进行解析,最终处理得到SQL建表语句(这期间用到注解解析辅助bean类:TableInfo和ColumnInfo,以及工具类:ClassFileUtils)

Java注解应用案例--实现.class文件到sql建表语句的转换过程_第1张图片

一、注解的定义

说了是Java注解的应用,当然得先自定义注解。

///@Entity:用于修饰类,表示此类需要转换为数据库表
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Entity {
    String value() default "";
}
///@Column:用于修饰属性,用于映射到数据库表字段
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Column {
    String value() default "";// 字段值
    boolean nullable() default true;///是否可空
    boolean autoIncrement() default false;//是否自增
    int length() default -1;///列(字段) 长度
}
///@ID:用于修饰属性,表示此属性将映射为数据库表主键
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ID {
    String value() default "";///表示映射成的注解名称
}

二、根据目的(输入对应文件目录,得到需要的SQL建表语句),创建自定义的注解处理器:

public interface IProcessor {
    String process(String url) throws Exception;
}
public class TableProcessor implements IProcessor {

    @Override
    public String process(String url) throws Exception {
        ///① 利用工具类,深度优先遍历的形式获取所有的文件
        List classFiles = ClassFileUtils.getClassFiles(url);
        StringBuilder sql = new StringBuilder();
        ///② 对每一个文件都进行类的获取,转成类列表
        for (File file : classFiles){
            Class clazz = ClassFileUtils.loadClass(file);
            ///③ 分析Class类中的注解,并生成SQL语句,存储到StringBuilder中
            TableInfo tableInfo = new TableInfo();
            tableInfo = tableInfo.parse(clazz);
            if (tableInfo!=null){
                sql.append(tableInfo.toString());
            }
        }
        return sql.toString();
    }
}

三、在写自定义的注解处理器时,我们发现,以上代码的①、②、③的过程,是需要比较复杂的逻辑的,那么,就一步步去实现。

  • 首先, 遍历目录下的所有.class文件:
      /**
       * 获取目录下所有.class文件
       * @param url 查找目录的绝对路径
       * @return 所有Class文件(类文件)
       */
      public static List getClassFiles(String url){
          allClassFile = new ArrayList<>();
          File file = new File(url);
          //如果是目录,则递归搜索该目录
          if (file.isDirectory()){
              fillClassFiles(file);
          }else{
              ///否则,如果是
              if (isClassFile(file)){
                  allClassFile.add(file);
              }
          }
          return allClassFile;
      }
    
      /**
       * 递归函数:递归搜索目录
       * @param directory 目录
       */
      private static void fillClassFiles(File directory) {
          File[] list = directory.listFiles();
          for(File file: list){
              if (file.isDirectory()){
                  fillClassFiles(file);
              }else{
                  if (isClassFile(file)){
                      allClassFile.add(file);
                  }
              }
          }
      }
    
      /**
       * 是否是class文件
       * @param file 目标文件
       * @return 是否匹配
       */
      private static boolean isClassFile(File file) {
          return getFileType(file).equals("class");
      }
    
      /**
       * 获取文件类型
       * @param file 目标文件(编译之后的.class文件)
       * @return 文件后缀名
       */
      private static String getFileType(File file) {
          String fileName=  file.getName();
          return fileName.substring(fileName.lastIndexOf(".")+1);
      }
    
  • 然后,对每一个.class文件都获取到它的Class类:
     /**
       * .class文件解析出 Class对象的过程(重点)
       * 关键点:读取.class文件中的内容,找到类名,并通过Class.forName(类名)得到Class对象
       * @param file .class文件
       * @return 类类型对象
       * @throws Exception 解析过程中的异常
       */
      public static Class loadClass(File file) throws Exception {
          Map offsetMap = new HashMap();
          Map classNameMap = new HashMap();
          DataInputStream data = new DataInputStream(new FileInputStream(file));///读取原始数据类型
          int magic = data.readInt(); // 0xcafebabe
          int minorVersion = data.readShort();
          int majorVersion = data.readShort();
          int constant_pool_count = data.readShort();
          int[] constant_pool = new int[constant_pool_count];
          for (int i = 1; i < constant_pool_count; i++) {
              int tag = data.read();
              int tableSize;
              switch (tag) {
                  case 1: // UTF
                      int length = data.readShort();
                      char[] bytes = new char[length];
                      for (int k = 0; k < bytes.length; k++)
                          bytes[k] = (char) data.read();
                      String className = new String(bytes);
                      classNameMap.put(i, className);
                      break;
                  case 5: // LONG
                  case 6: // DOUBLE
                      data.readLong(); // discard 8 bytes
                      i++; // Special skip necessary
                      break;
                  case 7: // CLASS
                      int offset = data.readShort();
                      offsetMap.put(i, offset);
                      break;
                  case 8: // STRING
                      data.readShort(); // discard 2 bytes
                      break;
                  case 3: // INTEGER
                  case 4: // FLOAT
                  case 9: // FIELD_REF
                  case 10: // METHOD_REF
                  case 11: // INTERFACE_METHOD_REF
                  case 12: // NAME_AND_TYPE
                      data.readInt(); // discard 4 bytes;
                      break;
                  default:
                      throw new RuntimeException("Bad tag " + tag);
              }
          }
          short access_flags = data.readShort();
          int this_class = data.readShort();
          int super_class = data.readShort();
          int classNameOffset = offsetMap.get(this_class);
          String className = classNameMap.get(classNameOffset).replace("/", ".");
          Class clazz = Class.forName(className);
          return clazz;
      }
    
  • 最后,对Class类中的注解的分析和生成对应的SQL建表语句:
    前两步,我们可以用一个工具类ClassFileUtils来封装,而最后一步,除了事先创建一个临时数据存储的StringBuilder对象来准备SQL语句的存储工作之外,我们用两个类:①ColumnInfo ②TableInfo 来分别对【类<->数据库表】和【类属性<->数据库表字段】两个过程来做处理:
    • ColumnInfo.java:

public class ColumnInfo {
private String columnName;///字段名称
private Class type;//字段类型
private boolean isID = false;//主键?
private boolean nullable = true;//非空?
private boolean isAutoIncrement = false;//自增?
private int length = 32;//字段长度(默认)
private boolean needPersist = false; ///是否保存本字段到数据库中

public ColumnInfo parse(Field field) {
    this.columnName = field.getName();
    this.type = field.getType();
    Annotation[] annotations = field.getAnnotations();

    ////做注解的两个判断:①如果是@Column 则说明有字段别名,需要更新字段名 ②如果是@ID,说明是主键
    for (Annotation annotation:annotations){
        if (annotation.annotationType().equals(Column.class)){
            this.needPersist = true;
            Column column = (Column)annotation;//强转赋值
            if (!column.value().equals("")) this.columnName=column.value();
            this.nullable = column.nullable();
            this.isAutoIncrement = column.autoIncrement();
            if (column.length()!=-1) this.length = column.length();
        }else if (annotation.annotationType().equals(ID.class)){
            this.needPersist = true;
            ID id = (ID)annotation;
            isID = true;
            if (!id.value().equals("")){
                this.columnName = id.value();
            }
        }
    }
    if (this.needPersist)
        return this;
    else
        return null;
}

@Override
public String toString() {

    StringBuilder sql = new StringBuilder(columnName);
    if (this.type.equals(String.class)){
        sql.append(" VARCHAR(").append(this.length).append(")");
    }else if (this.type.equals(Integer.class)){
        sql.append(" INT");
    }
    if (this.isID){
        sql.append(" PRIMARY KEY");
    }
    if (this.isAutoIncrement){
        sql.append(" AUTO INCREMENT");
    }
    if (!this.nullable){
        sql.append(" NOT NULL");
    }
    sql.append(";");
    return sql.toString();
}

}
```

  • TableInfo.java:

public class TableInfo {
private String tableName;
private Class clazz;
private boolean needPersist = false;
private Map columnInfos = new HashMap<>();

public TableInfo parse(Class clazz){
    this.clazz = clazz;
    this.tableName = this.clazz.getSimpleName();
    Annotation[] annotations = this.clazz.getAnnotations();
    for (Annotation annotation:annotations){
        if (annotation.annotationType().equals(Entity.class)){
            this.needPersist = true;
            Entity entity = (Entity)annotation;
            if (!entity.value().equals("")){
                this.tableName = entity.value();
            }
            break;
        }
    }
    if (this.needPersist){//说明有 @Entity 注解
        Field[] fields = this.clazz.getDeclaredFields();
        for (Field field:fields){
            ColumnInfo columnInfo = new ColumnInfo();
            columnInfo = columnInfo.parse(field);
            if (columnInfo!=null){
                this.columnInfos.put(field.getName(),columnInfo);
            }
        }
        return this;
    }else{
        return null;
    }
}

@Override
public String toString() {
    StringBuilder sql = new StringBuilder();
    sql.append("\n");
    sql.append("CREATE TABLE ");
    sql.append(this.tableName).append(" (");
    for (ColumnInfo columnInfo: this.columnInfos.values()){
        sql.append("\n    ");
        sql.append(columnInfo.toString());
    }
    sql.append("\n);");
    return sql.toString();
}

}
```

四、测试类

@Entity("person")
public class Person {
     @ID("pid")
     @Column(autoIncrement=true,length = 10)
     public Integer id;
     @Column(value = "pname",length = 255)
     public String name;
}

开始测试:

public class Main {

    public static void main(String[] args) throws Exception {
        Person person = new Person();
        ///注解处理器(内部use了 TableInfo 和 ColumnInfo),将类转成SQL语句
        TableProcessor processor = new TableProcessor();
        String sql = processor.process(System.getProperty("user.dir"));
        System.out.println(sql);
    }
}

得到:

CREATE TABLE person (
    pname VARCHAR(255);
    pid INT PRIMARY KEY AUTO_INCREMENT;
);

你可能感兴趣的:(Java注解应用案例--实现.class文件到sql建表语句的转换过程)