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元素属性也挺费时间的;
- 目前的功能不适用于实际项目,后续需要改进;
总之,通过这个实战项目我们能切实理解注解的使用场景,加深对反射的理解,以后项目中如果有需求就能通过自定义注解来实现啦。
项目Github地址:https://github.com/nobodyboy/SqlAnnotation
后续有时间,该实战项目还会继续维护,增加一些额外的功能,如生成insert语句或者加入分表的功能,如果有需要可以star一下,后续有变动也会通知到你。
PS:谢谢你的阅读,如果你觉得对你帮助,麻烦点个攒哦~