编写java注解和注解处理器

2019-12-20  本文已影响0人  程序员吉森

注解是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对象动态获取类的属性,通过属性可以获取到属性上的注解及其属性,然后我们可以对获取到的注解及属性进行相应的处理。

上一篇下一篇

猜你喜欢

热点阅读