这篇博文是接着上篇《面试|你不了解的注解(一)》写的,这里简单回顾下上篇博文的主要内容:
- 注解的基本概念;
- 注解内的四种标准注解:@Override,@Deprecated,@SuppressWarnings和@Native;
- 注解内的五种元注解:@Target,@Retention,@Documented,@Inherited和@Repeatable;
分析上面九种注解的源码,解析每种注解的基本作用和使用场景。
这篇博文是基于元注解而自定义注解的例子,代码居多,所以还是建议自己运行下才能理解自定义注解的过程。主要内容包括两部分:
- 自定义类注解
- 自定义属性注解
- 自定义注解注意事项
自定义注解
如果你熟悉Spring框架,那么@Autowired,@Controller以及@RequestMapping等注解你肯定见过并使用过,如果查看这些注解的源码,你会发现它们都是基于元注解而定制的注解。
自定义注解很简单,可关键在于定义注解处理器。下面尝试定义一个类注解和一个属性注解,以此熟悉注解的定义过程和注意事项。这里先给出自定义注解的步骤:
- ①自定义注解;
- ②定义注解处理器;
- ③使用注解;
1.自定义类注解
注解功能:类根据注解的值确定分库策略,这里的类一般是指跟数据库对应的POJO类。
1.1 定义注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ShardMapping {
String value() default ""; // 默认值不能是null
}
根据元注解@Target可知道,该注解作用于类上;根据元注解@Retention可知,该注解一直保留到JVM内;元注解@Documented表明如果形成doc文档,该注解会记录在javadoc文档内。
通过命令javap -c ShardMapping查看注解ShardMapping的字节码:
可以知道以下几点:
- 自定义注解本质上是一个接口类型;
- 自定义注解都会自动继承java.lang.annotation.Annotation接口;猜测可能是@字符的作用;
- 自定义注解内的元素都是public的抽象方法;即只能用public和abstract修饰,可以省略不写;
- 自定义注解内的元素可以有抽象方法的默认返回值,用default关键字来表示;
到这里,我们其实还不明确注解@ShardMapping有什么作用,下面是注解处理器。
1.2 定义注解处理器
public class ShardMappingAnnotationHandler {
static Map<String, String> shardMap = new HashMap<String, String>(8);
void processAnnotation(Object object) {
if (object == null) {
return;
}
Class<?> classObject = object.getClass();
if (!classObject.isAnnotationPresent(ShardMapping.class)) {
return; // object对象没有被ShardMapping注解修饰
}
String className = classObject.getName();
ShardMapping shardMapping = classObject.getAnnotation(ShardMapping.class);
String value = shardMapping.value();
if ("".equals(value)) {
System.out.println(className + ": shardMapping value is default, go on...");
shardMap.put(className, "defaule");
} else if ("hashCode".equals(value)) {
System.out.println(className + ": shardMapping value is hashCode, go on...");
shardMap.put(className, "hashCode");
} else if ("shardRef".equals(value)) {
System.out.println(className + ": shardMapping value is shardRef, go on...");
shardMap.put(className, "shardRef");
} else {
throw new RuntimeException(className + ": shardMapping value illegal. game over...");
}
}
}
注解处理器的大致思路是:
- ①利用反射判断参数对象是否被@ShardMapping注解修饰;
- ②利用反射获取该对象上注解@ShardMapping注解内的值,即value()方法返回的值;
- ③建立类与不同策略的映射关系,我们这里把分库策略简单的用“default”,"hashCode"和“shardRef”等字符串来表示,等到使用的时候就可以根据这个映射关系来进行分库路由;
通过梳理注解处理器的编写思路,我们知道反射在其中起了很大的作用。关于反射的内容,可以看这篇博文。
1.3 使用自定义注解
下面用@ShardMapping注解修饰类:
@ShardMapping("hashCode") // 也可以写成 @ShardMapping(value="hashCode")
public class DeparementTable {
public String depID;
public String depName;
public String depRank;
}
@ShardMapping("shardRef") // 也可以写成 @ShardMapping(value="shardRef")
public class EmployeeTable {
public String EmpID;
public String EmpName;
public String EmpRank;
}
注意:此处可以直接使用@ShardMapping而不需要括号以及括号内内容,value()方法则返回默认值。
注解处理器解析注解:
public class Client {
public static void main(String[] args) {
DeparementTable deparementTable = new DeparementTable();
EmployeeTable employeeTable = new EmployeeTable();
ShardMappingAnnotationHandler handler = new ShardMappingAnnotationHandler();
handler.processAnnotation(deparementTable);
handler.processAnnotation(employeeTable);
}
}
从定义注解,到定义注解处理器,再到注解使用,可以知道注解处理器是自定义注解的重点,我们每自定义一个注解,就需要为其编写一个注解处理器。在某种程度上来说,这其实挺麻烦的,Java为了减少麻烦,内置了AnnotationProcessFactory工厂类,关于它的使用这里不作介绍,感兴趣的可以自行百度。
2.自定义属性注解
注解功能:根据注解值设置注解修饰的属性是否可以为空。
2.1 定义注解:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Nullable {
boolean value() default true;
}
元注解@Target表明该注解只能用在属性上;注解元素value()方法默认返回true。
2.2 注解@Nullable的注解处理器:
public class NullAbleAnnotationHandler<T> {
void processAnnotation(T object) {
if (null == object) {
return;
}
Class<? extends Object> classT = object.getClass();
Field[] fields = classT.getDeclaredFields();
for (Field field : fields) {
if (!field.isAnnotationPresent(Nullable.class)) {
continue; // 没有注解的属性直接跳过
}
Nullable nullable = field.getAnnotation(Nullable.class);
boolean value = nullable.value();
System.out.println(classT.getName() + " " + field.getName() + ": nullable is " + value);
if (value) {
continue; // 为true或者默认 则不检查是否有值
}
String fieldValue = null;
try {
fieldValue = field.get(object).toString();
} catch (Exception e) {
System.out.println(e);
}
if (fieldValue == null) {
// 属性不可以为空
System.out.println("属性不可以为空");
throw new RuntimeException("属性不可以为空");
}
}
}
}
注解处理器的主要思路:
- ①通过反射获取对象的所有属性;
- ②遍历属性,判断属性是否被@Nullable注解修饰;
- ③对于被注解@Nullable修饰的属性,通过反射获取该注解以及注解内的value值;
- ④如果value值为true,表明该属性可以为空;否则需要进一步判断;
- ⑤通过反射获取该对象当前属性的值,如果属性为空,则表明不符合注解内的要求,则抛异常;
注解处理器的逻辑稍微有点绕,其利用的还是反射原理。
2.3 使用@Nullable注解:
public class Student {
@Nullable(false)
String id;
String name;
String sex;
// 省略get set方法
}
表明id属性不能为空。
使用注解处理器处理注解:
public class Client {
public static void main(String[] args) {
Student xiaoLi = new Student();
xiaoLi.setId("001");
xiaoLi.setName("xiaoLi");
xiaoLi.setSex("man");
Student xiaoSi = new Student();
xiaoSi.setName("xiaoSi");
xiaoSi.setId(null); // 该对象的id为null
NullAbleAnnotationHandler<Student> handler = new NullAbleAnnotationHandler<Student>();
handler.processAnnotation(xiaoLi);
handler.processAnnotation(xiaoSi);
}
}
对象xiaoSi的id为null,这不符合@Nullable设置的值,因此运行过程中会抛出异常。
3 注意
这里有个点需要注意,定义注解内元素时,元素名称必须与使用时一致,如:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Nullable {
boolean name() default true; // 不是value(), 而是name()
}
使用:
public class Student {
@Nullable(name = false) // 使用时必须要写name=false
String id;
// 省略
}
如果采用自定义的元素(抽象方法)名称,则使用注解的时候要么采用默认的fefault值,即直接@Nullable,要么按照上面的方式写全,不能采用省略@Nullable(false)写法。所以,当只有一个元素时,建议直接使用value()而别再定义其他名称。