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

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包:

111
复制到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:谢谢你的阅读,如果你觉得对你帮助,麻烦点个攒哦~

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值