共计 6307 个字符,预计需要花费 16 分钟才能阅读完成。
1. 前言
在学习任何一种 java 框架之前,我们基本都要先了解这个框架的注解。例如:spring 框架中的 @Controller、@Bean、@Component、@EnableCaching 等;mybatis 框架中的 @Select、@Delete、@ResultMap 等;甚至于 jdk 本身也有 @Override 等注解。这些注解带给我们一种感受,注解本身代表了框架的一系列功能,我们按照框架规定的方式使用注解,就能实现对应的功能。java 注解想要表达的思想就是,约定大于配置。
就像我现在打字用的键盘,键盘本身不能写字,但它提供了 26 个字母以及其他符号,我在键盘上按照约定敲击对应的键位,电脑会生成相应的指令做出相关的处理。同样,注解本身并不会实现功能,是 java 框架读取用户通过注解写入的值,通过反射机制实现一系列功能。
本文会简单介绍一下 jdk 自带的注解,以及如何自定义注解。接着会通过“注解 + 反射”的例子,简单实现一个 orm 框架。
2.jdk 注解
介绍 jdk 内置的几个常用注解:@Override,@Deprecated,@SuppressWarnings。
@Override,很常见,表明这个方法是重写的父类的方法,当你把 @Override 放到一个方法上时,编译器会自动去父类中查找是否有相应的方法,如果没有,说明注解使用错误,或者重写的方法名、参数等写错了,那么编译器就会给出编译错误,让你去修改。
@Deprecated,表明这个属性被弃用,可以修饰的范围很广,包括类、方法、字段、参数等等。当你使用它的时候,编译器就会给出提醒。不过,它是一种警告,而不是强制性的,在 IDE 中会给 Deprecated 元素加一条删除线以示警告,在 声明元素为 @Deprecated 时,应该用 Java 文档注释的方式同时说明替代方案,就像 Date 中的 API 文档那样,在调用 @Deprecated 方法时,应该先考虑其 建议的替代方案。
@SuppressWarnings,表明这不是一个警告,那么编译器就不会把它当做警告给提示出来。参数,表示压制哪种类型的警告,它也可以修饰大部分代码元素,在更大范围的修饰也会对内部元素起效,比如,在类上的注解会影响到方法,在方法上的注解会影响到代码行。对于 Date 方法的调用,可以这样压制警告
@SuppressWarnings({"deprecation", "unused"}) | |
public static void main(String[] args) {Date date = new Date(2017, 4, 12); | |
int year = date.getYear();} |
3. 自定义注解
我们先看看 几个注解的例子。
jdk 中的 @Override 注解
@Target(ElementType.METHOD) | |
@Retention(RetentionPolicy.SOURCE) | |
public @interface Override {} |
spring 中的 @FeignClient 注解
@Target({ElementType.TYPE}) | |
@Retention(RetentionPolicy.RUNTIME) | |
@Documented | |
public @interface FeignClient {@AliasFor("name") | |
String value() default ""; | |
/** @deprecated */ | |
@Deprecated | |
String serviceId() default ""; | |
@AliasFor("value") | |
String name() default ""; | |
String qualifier() default ""; | |
String url() default ""; | |
boolean decode404() default false; | |
Class<?>[] configuration() default {}; | |
Class<?> fallback() default void.class; | |
Class<?> fallbackFactory() default void.class; | |
String path() default ""; | |
boolean primary() default true;} |
可以发现几个有意思的信息:
- 定义注解类的的关键字是 @interface,就是接口前面加了 @
- 需要用注解来修饰注解(元注解)
- 注解类里面的属性是以一种“没有参数的方法”来表示,而且可以有默认值。
3.1. 元注解
修饰注解的注解,叫做元注解。java 里面有下面四种元注解:
(1)@Target:用来修饰注解的作用域。
- ElementType.TYPE:允许被修饰的注解作用在类、接口和枚举上
- ElementType.FIELD:允许作用在属性字段上
- ElementType.METHOD:允许作用在方法上
- ElementType.PARAMETER:允许作用在方法参数上
- ElementType.CONSTRUCTOR:允许作用在构造器上
- ElementType.LOCAL_VARIABLE:允许作用在本地局部变量上
- ElementType.ANNOTATION_TYPE:允许作用在注解上
- ElementType.PACKAGE:允许作用在包上
(2)@Retention:用于指明当前注解的生命周期。
- RetentionPolicy.SOURCE:当前注解编译期可见,不会写入 class 文件
- RetentionPolicy.CLASS:类加载阶段丢弃,会写入 class 文件
- RetentionPolicy.RUNTIME:永久保存,可以反射获取
(3)@Documented:申明注解是否应当被包含在 JavaDoc 文档中。
(4)@Inherited:是否允许子类继承该注解。
3.2. 属性
注解的属性也叫做成员变量。注解只有成员变量,没有方法。注解的成员变量在注解的定义中以“无形参的方法”形式来声明,其方法名定义了该成员变量的名字,其返回值定义了该成员变量的类型。
成员变量的类型可以是 java 基本类型,加类、接口、枚举、注解,以及他们的数组。
注解中属性可以有默认值,默认值需要用 default 关键值指定。
还有一种约束,当注解类中只有一个成员变量时,约束成员变量名必须是 value,应用这个注解时可以直接接属性值填写到括号内。例如:@Select(“select from table”) 和 @Select(value=”select from table”)。
4.orm 框架示例
很多全自动的 orm 框架,为了建立 pojo 类和数据库表之间的映射关系,都通过注解的方式,对 pojo 类注入表名,对 pojo 里面的属性注入表字段名。以前的 hibernate 就是通过这种方式,框架后台基于 pojo 对应的表和字段,动态生成 jdbc 所需的 sql。mybatis 不用,以前我们介绍过,mybatis 是基于 sql 的半自动的 orm 框架,并不需要 pojo 类的映射。
前面说过,注解本身并没有功能,它是作为一种类似于键盘的约束,一般框架通过反射机制去赋予它相应的功能。那么这个示例就是通过 注解 + 反射,去模拟一个 orm 框架的功能。
4.1. 注解
这里创建两个注解类,分别是 @KTable 和 @KColumn。
KTable.java 作用在 pojo 类上,映射数据库的表,所以作用域是 ElementType.TYPE。
@Target(ElementType.TYPE) | |
@Retention(RetentionPolicy.RUNTIME) | |
public @interface KTable {String value(); | |
} |
KColumn.java 作用在 pojo 类上,映射数据库表里面的字段,所以作用域是 ElementType.FIELD。
@Target(ElementType.FIELD) | |
@Retention(RetentionPolicy.RUNTIME) | |
public @interface KColumn {String value(); | |
} |
4.2.pojo
预先在数据库里面创建了一张表 fnd_user
| 字段名 | 属性 |
| — | — |
| id| varchar |
| name| varchar |
| age| int|
| role_code| varchar |
那么现在创建 pojo 类(FndUser.java),加上我们之前定义好的注解。
首先是 pojo 加上表名 @KTable 的注解。pojo 类里面的属性,我们约束好,如果加了 @KColumn 的注解,则框架取注解里面的值作为表字段名,如果不加注解,则取 pojo 类的属性名作为表字段名。
@KTable("fnd_user") | |
public class FndUser { | |
private String id; | |
private String name; | |
private int age; | |
@KColumn("role_code") | |
private String roleCode; | |
public String getId() {return id;} | |
public void setId(String id) {this.id = id;} | |
public String getName() {return name;} | |
public void setName(String name) {this.name = name;} | |
public int getAge() {return age;} | |
public void setAge(int age) {this.age = age;} | |
public String getRoleCode() {return roleCode;} | |
public void setRoleCode(String roleCode) {this.roleCode = roleCode;} | |
} |
4.3. 框架服务
框架核心的代码来了,我们定义了一个方法,在传入 pojo 的对象后,会返回对应的查询 sql。因为这部分代码的注释比较完整了,就不在多说了,主要通过反射,拿到注解的表名,列名,以及属性值,然后拼接 sql。
KpaasQuery.java
@Component | |
public class KpaasQuery {public String bindQuerySql(Object pojo) {StringBuffer stringBuffer = new StringBuffer(); | |
Class c = pojo.getClass(); | |
// 拿到表名 | |
boolean isTableExist = c.isAnnotationPresent(KTable.class); | |
if (!isTableExist) {return null;} | |
KTable kTable = (KTable) c.getAnnotation(KTable.class); | |
String tableName = kTable.value(); | |
// 拼接表 sql | |
stringBuffer.append("select * from").append(tableName).append("where 1=1"); | |
// 拿到列名 | |
// getDeclaredFields() 拿到所有字段,getFields() 拿到 public 字段 | |
Field[] fields = c.getDeclaredFields(); | |
for (Field field : fields) { | |
// 拿到字段名(fieldName)、列名(columnName)、字段值(fieldValue)String fieldName = field.getName(); | |
String columnName = fieldName; | |
Object fieldValue = null; | |
boolean isColumnExist = field.isAnnotationPresent(KColumn.class); | |
if (isColumnExist) {KColumn kColumn = field.getAnnotation(KColumn.class); | |
columnName = kColumn.value();} | |
// 拿到字段值 | |
String getMethodName = "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1); | |
try {Method getMethod = c.getMethod(getMethodName); | |
fieldValue = getMethod.invoke(pojo); | |
} catch (Exception e) {e.printStackTrace(); | |
} | |
// 拼接列 sql | |
if (fieldValue == null || (fieldValue instanceof Integer && (Integer) fieldValue == 0)) {continue;} | |
stringBuffer.append("and" + columnName); | |
if (fieldValue instanceof String) {stringBuffer.append("='").append(fieldValue).append("'"); | |
}else if(fieldValue instanceof Integer){stringBuffer.append("=").append(fieldValue); | |
} | |
} | |
return stringBuffer.toString();} | |
} |
4.4. 功能测试
写一个 controller,看能不能按照我们的要求返回 sql。
@RestController | |
public class UserController { | |
@Autowired | |
private KpaasQuery kpaasQuery; | |
@RequestMapping(value = "/getSql", method = RequestMethod.GET) | |
public String getSql() {FndUser fndUser = new FndUser(); | |
fndUser.setName("kerry"); | |
fndUser.setAge(24); | |
fndUser.setRoleCode("admin"); | |
String sql = kpaasQuery.bindQuerySql(fndUser); | |
return sql; | |
} | |
} |
调用接口,返回的结果是:select * from fnd_user where 1=1 and name = ‘kerry’ and age = 24 and role_code = ‘admin’
OK,符合我们的预期,那么这个简单的 orm 功能就实现了。
5. 备注
本公司的同事,看完这些,我猜想你的脑海中应该会浮现出一个框架 – 倚天 。我也是最早在使用倚天的时候,对注解产生了莫大的兴趣。当然,倚天的 orm 部分代码实现高深的多,功能也丰富的多。
最早的时候我挺怀疑倚天框架的必要性,我想明明直接可以通过 mybatis 就能实现的功能,为什么非要再封装成全自动的 orm 框架?后来我明白了,倚天给我们带来最大的好处,不是实现 orm 的功能,而是框架的约定。而这些约定让我们的代码更规范,开发人员只需要考虑业务逻辑和核心代码,很多涉及到代码质量、基础性能的问题,框架默默的就实现了。
拿本文的注解而言,因为倚天 pojo 类里面 @RowID 注解,我们不用考虑根据主键删除的安全性,框架会转换成加密后的 rowId。
包括 @SystemColumn 的五个基础字段的主键,我们不用去写版本号的自增长,和创建人、创建时间、最后更新人、最后更新时间,这些重复而又枯燥无味的代码。
就像你写 pojo 类时,不想手动去敲那些 get、set 方法一样,你期待 ide 能自动生成。一个好的框架能够让你尽量少的浪费时间,集中精力,更好的提高自身能力。