实战项目|自定义注解实现POJO类到CRUD语句的映射

更新时间:2019-07-22 09:16:51点击次数:1173次
1.前言
前段时间一直在整理Java基础面试题,大大小小共有二十余篇,虽然不足市场上现有面试题的十分之一,但每一个面试题都是从最基础的概念、作用、原理和使用场景等方面来解释的,对我自己而言,这个过程很痛苦也很快乐;痛苦的是深夜撸代码之余还要撸文档,但学完之后能够加深对Java语言的理解并且能够收获各位博友的点赞,瞬间就会有满满的动力继续学下去。感觉写跑题了,回到正题。

最近学习的点主要有常用的运行时状态(RTTI),反射以及注解,在学完之余自己也在思考是否能够写个小demo来整合一下这些知识点;于是就寻找各种运用场景,经过几天的琢磨,逐渐有了思路。不过这个思路,也是从项目中来的,那么肯定有人已经做过,我这里按照自己的想法来实现一下,大佬勿喷呀。

2.解决一个什么问题
想必大家都了解MySql的逆向工程,它可以利用XML配置文件,根据POJO类生成常规的SQL语句,虽然很方便,但对于复杂的查询场景还是无能为力的;这里,我们要实现的功能其实差不多,不过我们使用的不是XML配置文件,而是注解。

在实际项目中,一个表动不动就几十甚至上百个字段,那么对于我们来讲,不仅要编写很长的Create和Select语句,还要在代码中编写POJO类;其实这是一种重复劳动,拉了我们的工作效率,为了减少这个重复劳动并提高工作效率,就有了这个实战项目。

所以,这个实战项目解决的问题就是:减少建表和写POJO类的重复劳动,提高工作效率。

3.解决问题的思路是什么
明确我们要解决的问题之后,就是理清解决思路。
减少建表和写POJO类的重复劳动,站在工程师的角度,肯定是想只写好POJO类就行,至于建表语句或者查询语句都自动生成吧。诶,这个自动生成是关键的一步。

3.1问题提炼:如何根据POJO类自动生成建表语句或者查询语句
对,如何自动生成?
或许你有很多种方法,但我能想到的只有注解!关于注解,前面已经花了两篇文章来讲解,不熟悉的可以看完本博文后去我的主页里面去找。

3.2思路概述:利用自定义注解根据POJO类自动生成建表语句或者查询语句
这里,我们分析下建表语句所需要的内容:

符合MySQL规范的表名:字母大写且每个单词用下划线连接起来,如STATUS_DATA;
符合MySQL规范的字段名:字母大写且每个单词用下划线连接起来,如STATUS_CODE;
字段的类型:varchar类型还是int类型等;
字段的长度:自定义长度;
字段是否为空:自定义是否为空;
字段备注:字段的中文解释;
是否有主键;
是否有索引;
表是否有备注:即表的中文解释;
这里虽然列了很多内容,但核心的还是表名和字段名,其他的内容都是基于这两项来建立的。
我们一般书写POJO类主要包含两部分:

类名
类属性名
那么这里,我们就根据类名生成表名,根据属性名生成字段名。其他的内容都根据自定义注解来实现。

3.3 详细步骤
自定义一个名为@Entity注解,其主要功能是:

根据类名生成符合一定格式的表名;
获取表的中文解释,即表的备注内容;
此表要生成哪类sql语句,是Create还是Select,还是都要;
@Entity注解定义comment()和operator()两个抽象方法元素,分别用于完成功能2和功能3;功能1在代码中处理。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Entity {
String comment() default "";
String[] operator() default "all";
}

根据定义可知,comment()方法默认返回空字符串,而operator()方法默认返回字符串“all”,这里表示都生成的意思,具体看下面一节。

自定义一个名为@Column注解,其主要功能是:

拥有该注解的属性就认定为表的列属性;
根据属性生成符合一定格式的列名;
获取列的系列属性,包括类型、长度、是否为空以及备注等;
@Column注解定义一系列方法元素,用于完成上述功能。
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Column {
String type();    // 列类型
String length() default "10";   // 长度
boolean nullAble() default true;  // 可否为空,默认为true
String comment() default "";   // 备注信息
}

根据定义可知,length()方法默认返回10,表明字段长度如果没有定义则为10;nullAble()方法默认返回true,表明字段可以为空,如何设置为false,则字段不能为空。

自定义一个名为@PrimaryKey注解,用于鉴定该属性字段是否为主键。

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PrimaryKey {
}

注意:该注解为标记注解。

自定义一个名为@Index注解,用于判断是否在该属性字段上建立索引。

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Index {

}

注意:该注解为标记注解。

好了,到这里注解要完成的功能基本已经明了,下面就是注解处理器的核心逻辑。

4.核心代码讲解
注解处理器是注解发挥作用的关键,由于代码量大,这里就不一一黏贴出来,有兴趣的可以去看我github上的项目源码,这里解释几个关键的地方便于理解。

4.1 如何生成规范的表名和列名
getEntityName()方法部分源码:

/**
* 根据类名获取表名,根据属性名生成列名
* @param entityName  (全路径)类名
* @return 符合mysql规范的表名
*/
public String getEntityName(String entityName) {

// .... 忽略
List<String> nameList = new LinkedList<String>();
Stack<Character> stack = new Stack<Character>();
for (int index = entityName.length() - 1; index >= 0; index--) {
char indexChar = entityName.charAt(index);
if (indexChar >= 65 && indexChar <= 90) {
stack.add(indexChar);
StringBuilder sBuilder = new StringBuilder();
while (!stack.isEmpty()) {
sBuilder.append(stack.pop());
}
nameList.add(sBuilder.toString().toUpperCase());
} else {
stack.add(indexChar);
}
}

// 处理field
if (!stack.isEmpty()) {
StringBuilder sBuilder = new StringBuilder();
while (!stack.isEmpty()) {
sBuilder.append(stack.pop());
}
nameList.add(sBuilder.toString().toUpperCase());
}

StringBuilder entityNameStr = new StringBuilder();
for (int index = nameList.size() - 1; index >= 0; index-- ) {
entityNameStr.append(nameList.get(index));
entityNameStr.append(CONNECTOR);
}

return entityNameStr.substring(0, entityNameStr.length()-1);
}

方法利用Stack数据结构的先进后出原则,从最后一个字母往前分别放入Stack容器,如果遇到大写字母则pop()出容器的字母并组成一个单词放入LinkedList,然后统一拼接生成表名或者列名。效果如下所示:

// 类名到表名
StatusData   --->   STATUS_DATA
Stundet  --->  STUDENT

// 属性名到字段名
id   --->   ID
statusCode  --->   STATUS_CODE

4.2 如何解析列字段的属性
List<String[]> statementList = new LinkedList<String[]>();
Field[] fields = entityClass.getDeclaredFields();
for (Field field : fields) {
// 如果有@Column注解则认为是表中的列
if (!field.isAnnotationPresent(Column.class)) {
continue;
}
String fieldName = getEntityName(field.getName());
// 判断是否有@PrimaryKey注解
if (field.isAnnotationPresent(PrimaryKey.class)) {
primaryKey = fieldName;
}
// 判断是否有@Index注解
if (field.isAnnotationPresent(Index.class)) {
indexList.add(fieldName);
}
// 下面代码都是用来获取字段属性,并且拼接成各部分放入columnStr数组
String[] columnStr = new String[4];
Column columnField = field.getAnnotation(Column.class);
columnStr[0] = fieldName;

    // 拼接列字段的类型和长度  格式:VARCHAR(15)
StringBuilder fieldType = new StringBuilder();
fieldType.append(columnField.type().toUpperCase());
fieldType.append(LEFT_BRACKETS);
fieldType.append(columnField.length());
fieldType.append(RIGHT_BRACKETS);
columnStr[1] = fieldType.toString();

// 拼接列字段是否为空  格式:NOT NULL 或者””
if (!columnField.nullAble()){
columnStr[2] = NOT_NULL;
} else {
columnStr[2] = STRING_EMPTY;
}

// 拼接列字段的备注   格式:COMMENT ‘备注内容’  或者 “”
if (STRING_EMPTY.equals(columnField.comment())) {
columnStr[3] = STRING_EMPTY;
} else {
StringBuilder fieldComment = new StringBuilder();
fieldComment.append(COMMENT);
fieldComment.append(STRING_BLANK);
fieldComment.append(SINGLE_QUOTATION);
fieldComment.append(columnField.comment());
fieldComment.append(SINGLE_QUOTATION);
columnStr[3] = fieldComment.toString();
}

statementList.add(columnStr);
}

上面是构建Create语句的逻辑,后续只需要遍历statementList容器即可。

以上是理解注解处理器的两个核心点,完整的项目请移步github。

5.使用实例
为了项目中使用,需要把其打成jar包便于引用。可以点击这里下载jar包,CSDN不能设置0积分下载,自动设置了5积分,没有积分下载的可以去github项目中下载。

引入jar包:

复制到lib目录下,右击-Build Path…-add build path…,结果如图所示。

新建StatusData类:
package com.starry.annotation;

@Entity(comment="状态数据表")
public class StatusData {
@PrimaryKey
@Column(type="varchar", length="20", nullAble=false, comment="编号")
String id;

@Index
@Column(type="varchar", length="15", nullAble=false, comment="状态码")
String statusCode;

@Column(type="varchar", length="225", comment="状态描述")
String statusDesc;

@Index
@Column(type="char", nullAble=false, comment="操作人编号")
String operatorId;

@Column(type="varchar", length="15", comment="操作人姓名")
String operatorName;

@Column(type="varchar", length="255", comment="备注")
String mark;
}

POJO类表明id为主键,并且在statusCode和operatorId上构建索引,id、statusCode和operatorId不能为空。
注意,这个@Entity注解内operator()元素采用默认值“all”,表示生成Create语句,Select语句和Drop语句;否则按照配置值来生成,处理器中定义能配置的值由“create”,“select”和“drop”三种,可是是任何组合的数组。

客户端处理并生成结果:
public class Client {
public static void main(String[] args) {
EntityAnnotaitonHandler handler = new EntityAnnotaitonHandler();
List<String> list= null;
try {
list = handler.process(new StatusData());
} catch (Exception e) {
System.out.println("发生错误: " + e.getMessage());
}

if (list == null) {
return ;
}

for (String string : list) {
System.out.println(string);
System.out.println();
}
}
}

注解处理器内往外抛出了两个异常,所以需要进行捕获,异常具体可以看源码。

生成结果:

CREATE TABLE STATUS_DATA(ID VARCHAR(20) NOT NULL COMMENT '编号',STATUS_CODE VARCHAR(15) NOT NULL COMMENT '状态码',STATUS_DESC VARCHAR(225) COMMENT '状态描述',OPERATOR_ID CHAR(10) NOT NULL COMMENT '操作人编号',OPERATOR_NAME VARCHAR(15) COMMENT '操作人姓名',MARK VARCHAR(255) COMMENT '备注',PRIMARY KEY (ID),INDEX INDEX_STATUS_CODE (STATUS_CODE),INDEX INDEX_OPERATOR_ID (OPERATOR_ID))ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='状态数据表';

DROP TABLE IS EXISTS STATUS_DATA

SELECT ID AS id,STATUS_CODE AS statusCode,STATUS_DESC AS statusDesc,OPERATOR_ID AS operatorId,OPERATOR_NAME AS operatorName,MARK AS mark FROM STATUS_DATA


或许你会觉得这种使用方法很low,我也承认确实挺low的,哈哈哈哈…
实际项目中也不会这么用,基本是采用包扫描的时候解析注解以及注解的类,然后自动执行生成我们想要的内容,咱们这里项目小,也是为了不需要手动编写大表的Create语句或者Select语句,权当一个小工具喽。

6.总结
这个实战项目基本达到了我们想要的功能,但还有很多不足之处。比如:

大表创建的话,编写@Column元素属性也挺费时间的;
目前的功能不适用于实际项目,后续需要改进;

本站文章版权归原作者及原出处所有 。内容为作者个人观点, 并不代表本站赞同其观点和对其真实性负责,本站只提供参考并不构成任何投资及应用建议。本站是一个个人学习交流的平台,网站上部分文章为转载,并不用于任何商业目的,我们已经尽可能的对作者和来源进行了通告,但是能力有限或疏忽,造成漏登,请及时联系我们,我们将根据著作权人的要求,立即更正或者删除有关内容。本站拥有对此声明的最终解释权。

  • 项目经理 点击这里给我发消息
  • 项目经理 点击这里给我发消息
  • 项目经理 点击这里给我发消息
  • 项目经理 点击这里给我发消息