编写java注解和注解处理器
注解是java语言中一种重要的特性。本文介绍如何编写注解、如果使用注解以及如何编写注解处理器。
注解在java开发中一直具有举足轻重的地位。在Spring Boot大行其道的今天,注解更显重要,框架提供的各种注解让我们开发更加方便。注解就像生活中的标签纸一样,可以为我们标注的类、方法、属性等补充额外的信息,也可以用于声明类、方法、属性的额外特性。除了使用框架提供的注解,有时候我们也可以编写自定义注解。
如何编写注解
首先,让我们以spring提供的RestController注解为例,看一下注解是由哪些部分组成的。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
@AliasFor(annotation = Controller.class)
String value() default "";
}
注解的声明
在java中,注解的声明与类的声明类似,除了使用@interface关键字代替类的class关键字,注意不要与接口的声明(interface,不带@)混淆。
注解的属性
注解中可以包含多个属性。与类的属性不同,注解中的属性需要在属性名后加一对小括号。这有点像无参数的方法声明,事实上在注解处理器中调用注解的属性时也与调用无参方法相同,这一点我们在下文中将进一步介绍。
注解的属性可以使用default关键字赋默认值。注意如果一个属性没有默认值,那么在使用注解时必须为这个属性赋值。
注解的元信息
注解需要使用在类、方法还是属性上?注解是在源文件中生效,还是在class文件中生效,还是可以在运行时调用?这些都属于注解的元信息。注解通过一系列元注解(就是标注在注解上的注解)来说明这些元信息。常用的元注解包括@Target、@Retention、@Documented、@Inherited以及@Repeatable(jdk8之后新增)。
@Target通过指定一个ElementType枚举类型的数组来表明注解的作用范围。常用的范围包括TYPE(类和注解)、FIELD(属性)、METHOD(方法)、PARAMETER(方法的参数)、CONSTRUCTOR(构造器)和LOCAL_VARIABLE(局部变量 )等 。
@Retention通过指定一个RetentionPolicy类型的枚举值来表明注解的保留策略,它有三个枚举值:SOURCE(源代码)、CLASS(class文件)和RUNTIME(运行时)。SOURCE表明注解只在源代码中保留,在编译时会被编译器丢弃。CLASS表明注解可以被编译到class文件中,但不会被加载到jvm中。RUNTIME表明注解可以被加载到jvm中,供运行时使用。
@Documented表明该注解(指被@Documented标注的注解)会进入javadoc文档中,即被该注解标注的类、方法、属性等的javadoc中会显示出这个注解的情况。
@Inherited表明该注解(指被@Inherited标注的注解)标注的类、方法、属性的会传递到它的子类中。但是该注解不会在子类的javadoc文档中显示。
@Repeatable表明该注解(指被@Repeatable标注的注解)可以被重复使用,@Repeatable注解内要传入能容纳当前注解的容器类,例如
@Repeatable(RestControllers.class)
// RestControllers为能容纳@RestController注解的容器
public @interface RestControllers{
RestController[] value();
}
通常情况下,我们应该为注解添加@Target、@Retention和@Documented元注解。
如何使用注解
我们可以在@Retention元注解定义的注解作用范围内使用注解。例如,如果注解被 @Retention(RetentionType.TYPE)标注,那么该注解可以在类上使用。这里我们还是以@RestController注解为例来说明如何使用注解:
@RestController(value = "userController")
public class UserController{}
我们可以通过属性名=属性值的方式为注解的属性进行赋值,没有默认值的属性必须要赋值。需要注意,如果要为名为value的注解属性赋值,且不需要为其他属性赋值时,value可以省略。即上面的代码可以简化为:
@RestController("userController")
public class UserController{}
如果我们要为数组类型的属性赋值,而且要赋的值为单元素的数组,那么数组的大括号可以省略,如:
@Target(ElementType.TYPE)
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Test{
String[] value();
}
// 完整的写法
@Test(value={"abc"})
public class MyTest{}
// 上面的代码可以简化为这种形式
@Test("abc")
public class MyTest{}
如何编写注解处理器
注解本身的作用有限,通常需要配合独立的处理代码来使用。注解的处理代码可以存在于单独的类中,也可以存在于其他类的某段逻辑代码当中。注解处理器通常通过java的反射机制来实现。这里以将实体类对象的集合导出为EXCEL为例:
首先我们先编写一个@Excel注解:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Excel {
// 字段在EXCEL列中的名称
String value() default "";
// 字段值的映射,如性别映射可以写为"0-女,1-男"
String map() default "";
}
假设有一个名为Person的实体类如下:
public class Person {
@Excel("id")
private Integer id;
/**
* 姓名
*/
@Excel("姓名")
private String name;
/**
* 年龄
*/
@Excel("年龄")
private Integer age;
/**
* 性别
*/
@Excel(value="性别", map="0-女,1-男")
private Integer gender;
}
我们用@Excel注解标注了各个属性的中文名称,对于性别属性,我们还给出了相应的映射值。我们希望模拟Excel输出,将对象输出为以下格式:
id 姓名 年龄 性别
1 张三 25 男
2 李四 30 女
下面我们来编写处理类:
public class ExcelHandler {
// 缓存字段的映射
Map<String, Map<String, String>> cache = new HashMap<>();
public <T> void handle(List<T> list, Class<T> clazz) throws IllegalAccessException {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
// 遍历字段,如果字段上存在注解,就进行相应处理
if (field.isAnnotationPresent(Excel.class)) {
// 获取注解对象
Excel annotation = field.getAnnotation(Excel.class);
// 打印出对象的value属性,即中文名称
System.out.print(annotation.value() + " ");
// 如果注解有map属性,将map属性的字符串解析为HashMap,存到缓存中,供下面解析字段值使用
if (!"".equals(annotation.map())){
cache.put(field.getName(), convertToMap(annotation.map()));
}
}
}
System.out.println();
for (T t : list) {
for (Field field : fields) {
// 设置可以获取对象中属性的值
field.setAccessible(true);
Object value = field.get(t);
if (field.isAnnotationPresent(Excel.class)) {
Excel annotation = field.getAnnotation(Excel.class);
String map = annotation.map();
if ("".equals(map)) {
System.out.print(value);
} else {
System.out.print(cache.get(field.getName()).get(String.valueOf(value)));
}
} else {
System.out.print(value);
}
System.out.print(" ");
}
System.out.println();
}
}
/**
* 将映射属性转化为HashMap
* @param fieldMap 注解中的映射属性,如"0-女,1-男"
* @return 转化为HashMap
*/
private Map<String, String> convertToMap(String fieldMap) {
Map<String, String> map = new HashMap<>();
String[] arr = fieldMap.split(",");
for (String s : arr) {
String[] arr1 = s.split("-");
map.put(arr1[0], arr1[1]);
}
return map;
}
public static void main(String[] args) throws IllegalAccessException {
Person zhangsan = new Person();
zhangsan.setGender(1);
zhangsan.setId(1);
zhangsan.setAge(25);
zhangsan.setName("张三");
Person lisi = new Person();
lisi.setGender(1);
lisi.setId(1);
lisi.setAge(25);
lisi.setName("张三");
List<Person> personList = new ArrayList<>();
personList.add(zhangsan);
personList.add(lisi);
new ExcelHandler().handle(personList, Person.class);
}
}
这里主要的思路就是通过反射机制,根据class对象动态获取类的属性,通过属性可以获取到属性上的注解及其属性,然后我们可以对获取到的注解及属性进行相应的处理。