在项目开发中,常常会使用到注解,无论是原生Java还是所使用的框架比如Hibernate、Spring,注解都是不可避免会使用到的技术。它大大的简化了代码,减少了我们的工作量,简洁清晰。但是对于Java注解总是感觉理解的不清晰。这一次学习了Java注解的相关知识,希望对Java注解能够有个更加全面的认识。
1 概述
首先思考三个问题,为什么要使用注解?学习注解有什么好处?注解可以做什么?
- 能够读懂别人的代码,特别是框架使用相关的代码
在学习编程的过程中,我们常常需要去看别人的代码,不可避免的是在别人的代码中,有很多的注解,如果我们不懂注解,就没法更好的读懂代码。 - 让编程更加简洁,代码更加清晰
本来需要配置文件或者很多逻辑才能实现的内容,如果可以使用一个注解来进行替代,就会使得代码更加简洁清晰。 - 让别人高看一眼
如今注解的使用很广泛,无论是Spring还是Hibernate中都使用了大量的注解,只有懂得了注解,才能更好地理解并使用相关框架。我们学习注解的目的不仅仅是会使用,更要学会能够使用自定义注解来解决问题。这才是一个优秀程序员的标配。
注解:Java提供了一种原程序中的元素关联任何信息和任何元数据的途径和方法
这个概念初看之下会显得很懵逼,难以理解,不过我们继续往下走,梳理一遍之后回过头来在看这个注解,就会有种豁然开朗的感觉。
2 常见注解
2.1 JDK自带注解
示例代码:
public interface Person {
public String name();
//当觉得方法存在问题,可以使用@Deprecated宣告方法过时,所有此方法调用都会被划横线
@Deprecated
public void sing();
}
public class Child implements Person {
//因为Child类实现了Person接口,所以实现方法上都会存在@Override注解。
@Override
public String name() {
return null;
}
@Override
public void sing() {
}
}
public class Test {
@SuppressWarnings("deprecation")
public static void main(String[] args) {
Person p = new Child();
p.sing();//过时方法会报警告,使用@SuppressWarnings能忽略警告
}
}
这就是JDK自带的三个注解的使用方法。
2.2 常见第三方注解
这个就不写示例代码了,有兴趣的可以去看看Spring的相关资料。
3 注解的分类
3.1 按照运行机制分
- 源码注解——注解只在源码中存在,编译成.class文件就不存在了
- 编译时注解——注解在源码和.class文件中都存在
比如JDK自带的三个注解都是属于编译时注解,在编译的过程中起作用。如Override会在编译的时候告诉编译器应该覆盖父类的一个方法,如果没有覆盖,则会报一个编译错误。 - 运行时注解——在运行阶段还起作用,甚至会影响运行逻辑的注解
比如Spring中的自动注入注解@Autowired,在运行过程中会将你的成员变量自动进行注入。
3.2 按照来源分
- 来自JDK的注解
- 来自第三方的注解(常用)
- 自定义的注解
3.3 元注解——注解的注解
4 自定义注解
示例代码:
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Description {
String desc();//类似接口中的方法,它是一个成员
int age() default 18;
}
4.1 自定义注解的语法要求
public @interface Description {
String desc();//类似接口中的方法,它是一个成员
int age() default 18;
}
- @interface:
使用@interface关键字定义注解。不是class也不是interface。 - 成员变量
- 成员以无参数无异常的方式进行声明
- 可以用default为成员指定一个默认值
- 成员类型是受限的,合法的类型包括基本数据类型及String,Class,Annotation,Enumeration。
- 如果注解只有一个成员,则成员名必须取名为value(),在使用时可以忽略成员名和赋值号(=),如@SuppressWarnings
- 注解类可以没有成员,没有成员的注解称为标识注解,如@Override
4.2 注解的注解(元注解)
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
示例代码上面的四行就是元注解,他们在注解Description 注解。我们来逐条分析一下:
- @Target 注解的作用域(ElementType类)
- CONSTRUCTOR —— 构造方法声明
- FIELD —— 字段声明
- LOCAL_VARIABLE —— 局部变量声明
- METHOD —— 方法声明
- PACKAGE —— 包声明
- PARAMETER —— 参数声明
- TYPE —— 类接口
- @Retention 生命周期(RetentionPolicy类)
- SOURCE —— 只在源码显示,编译时会丢弃
- CLASS —— 编译时会记录到class中,运行时忽略
- RUNTIME —— 运行时存在,可以通过反射读取
- @Inherited 标识性元注解,此注解允许子注解继承
- @Documented 生成javadoc时会包含注解
4.3 使用自定义注解
使用注解的语法:
@注解名(<成员名1>=<成员值1>,<成员名2>=<成员值2>,…)
@Description(desc="Hello",age=17)
public void sing() {
}
这只是最简单的用法,依据成员的变化和元注解的变化,注解可以使用在不同的地方,有不同的表示方式。但大体与之相似。具体可以参照上面改变注解书写方式,自己尝试。
4.4 解析注解
概念:通过反射获取类、函数或成员上的运行时注解信息,从而实现动态控制程序运行的逻辑。
首先思考分析一下,我们要获取注解的信息,首先就要先知道类的信息,那么要知道类的信息,首先就要先获取类的类类型。所以整个思路有了以后,代码就很容易来编写,主要就是要熟悉API中的一些关于注解的方法。
示例代码:
public class Person {
public void say() {}
}
@Description("I'm class annotation")
public class Child extends Person {
@Override
@Description("I'm method annotation")
public void say() {
System.out.println("hello");
}
}
//进行注解解析
public class ParseAnn {
public static void main(String[] args) {
try {
//1.使用类加载器加载类
Class c = Class.forName("com.gai.ann.Child");
//2.找到类上面的注解
boolean isExist = c.isAnnotationPresent(Description.class);//判断类上是否存在xx注解
if (isExist) {
//3.拿到注解实例
Description d = (Description) c.getAnnotation(Description.class);
System.out.println(d.value());
}
//4.找到方法上的注解
Method[] ms = c.getMethods();
for (Method method : ms) {
//第一种方法
// boolean isMExist = method.isAnnotationPresent(Description.class);
// if (isMExist) {
// Description d = method.getAnnotation(Description.class);
// System.out.println(d.value());
// }
//第二种方法
Annotation[] as = method.getAnnotations();
for (Annotation annotation : as) {
if (annotation instanceof Description) {
Description d = (Description) annotation;
System.out.println(d.value());
}
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
运行结果:
I’m class annotation
I’m method annotation
- 更改@Retention参数
将参数改为SOURCE,重新运行,发现没有结果。因为注解只在源码中存在,编译成.class文件是就没有了。
将参数改为CLASS,重新运行,发现没有结果。虽然在编译时注解存在,但是反射是在运行时起作用,此时注解依旧不存在了。
只有当参数为RUNTIME时,运行才能得到结果。 - 验证@Inherited继承的情况 查看@Inherited的源码,我们可以知道,这个继承对于接口的实现无效 现在我们将Child类中的Description注解都删除,在Person类中添加注解
@Description("I'm parent annotation")
public class Person {
@Description("I'm parent method")
public void say() {}
}
- 运行结果:
I’m parent annotation
根据运行结果我们可以看出继承的是类上面的注解,而不会继承方法上的注解。
5 项目实战
5.1 需求
- 有一张用户表,字段包括用户ID,用户名,昵称,年龄,性别,所在城市,邮箱,手机号
- 方便的对每个字段或字段的组合条件进行检索,并打印出SQL
- 使用方式要足够简单
5.2 分析
这个项目是想要实现模拟数据库查询功能,类似于Hibernate的,所以为了足够简单,我们也要使用注解来实现功能。他的最终目标是根据我们的输入条件来打印SQL语句,为了实现这一目的,我们需要
- 自定义注解,标注Pojo类的类名@Table和变量名@Column,分别对应数据库的表名和字段名
- 获取当前类的类类型,运用反射的知识去解析注解,拿到表名tableName,获取类的信息。
- 遍历当前类的所有成员变量,找到包含注解的成员变量,并解析注解,获取字段名columnName。
- 使用方法的反射运用知识,getXXXX方法获取用户输入的的条件filedValue
- 拼装SQL语句,”select * from tableName where columnName=filedValue….”
5.3 实现
关键代码:
public static String query(Object f) {
StringBuilder sb = new StringBuilder();
//1.获取到class
Class c = f.getClass();
//2.获取到table的名字
boolean exist = c.isAnnotationPresent(Table.class);
if (!exist) {
return null;
}
Table table = (Table) c.getAnnotation(Table.class);
String tableName = table.value();//拿到表名
//1=1的目的是防止后面不存在条件报错
sb.append("select * from ").append(tableName).append(" where 1=1");
//3.遍历所有的字段
Field[] fs = c.getDeclaredFields();
for (Field field : fs) {
//4.处理每个字段对应的sql
//4.1.拿到字段名
boolean isExits = field.isAnnotationPresent(Column.class);
if (!isExits) {
continue;
}
Column column = field.getAnnotation(Column.class);
String columnName = column.value();//拿到数据库字段名
//4.2.拿到字段值
String name = field.getName();
String methodName = "get"+name.substring(0, 1).toUpperCase()+name.substring(1);
Object fieldValue = null;
try {
Method method = c.getMethod(methodName);
fieldValue = method.invoke(f);
} catch (Exception e) {
e.printStackTrace();
}
//4.3.拼装sql
if (fieldValue==null ||
(fieldValue instanceof Integer && (Integer)fieldValue==0)) {
continue;//去除掉没有值的字段
}
sb.append(" and ").append(columnName);
if (fieldValue instanceof String) {
if (((String) fieldValue).contains(",")) {//邮箱存在多个,,,范围in
String[] ss = ((String) fieldValue).split(",");
sb.append(" in (");
for (String string : ss) {
sb.append("'").append(string).append("'").append(",");
}
sb.deleteCharAt(sb.length()-1);
sb.append(")");
} else {
sb.append("=").append("'").append(fieldValue).append("'");
}
} else if(fieldValue instanceof Integer) {
sb.append("=").append(fieldValue);
}
}
return sb.toString();
}
6 总结
这一次系统的学习了关于注解的知识,其实注解也是反射的一个应用啊,因为反射,注解才有了实现的可能,而有了注解,就会让程序显得更加简洁。学会了注解之后,我们学习框架的时候,也会更加清楚地理解框架的作用,非常利于我们去学习框架的知识。
有兴趣的也可以去看看刘果国老师的视频全面解析Java注解,谢谢老师!