面试官:聊聊注解?

理解java注解

​ 注解是java中十分重要的一部分,我们无时无刻都在使用它,尤其是使用Spring框架开发的时候,会在项目中使用到很多奇奇怪怪的注解(@DataScope、@Log、@Override等),很多时候我们只是会使用这些注解,但是对于注解背后的逻辑、工作原理都知之甚少,更别提我们自己开发一个注解了。所以今天就把我对注解的了解以及如何自定义一个注解记录在此与大家一起交流。

​ 首先注解是Java1.5时引入的概念,属于一种类型。注解提供了一系列数据用来修饰程序代码(类、方法、字段),但是注解并不是所修饰代码的一部分,即它对代码的运行没有直接影响,由编译器决定该执行那些操作

​ 其次Annatation(注解)是一个接口,重新可以通过反射来获取指定程序中元素的Annatation对象,然后通过该对象来获取注解中的元数据信息

从@Override开始

想必在平时的编码中对于@Override来说,我们是最熟悉不过的了。知道其实用于方法的重写,子类覆盖父类方法用到的注解。但是其又是如何实现这个功能的呢,让我们点开源码看一下。

import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

可以看到其源码十分的简短,无非是导入一个jar包,然后使用了另外两个注解,然后使用@interface声明了这个注解。

点开@Target和@Retention我们会发现,他们始终在@Documented、@Retention、@Target三个注解之间疯狂的套娃,其实这三个注解是注解中的元注解。

元注解

下面我们分别来看下这三个元注解

@Retention(生命周期)

表示如何存储被标记的注解(指定存储级别),通俗的理解就是生命周期

  • SOURCE:源码级别,注解将会被编译器丢弃,不会保留在编译好的class文件中
  • CLASS:(默认级别)类文件级别,注解在class文件中可用,但是会被JVM丢弃,在执行的时候不会加载到虚拟机中。
  • RUNTIME:运行时级别,将在运行期(JVM)也保留,因此可用通过反射机制去读取注解的内容。如SpringMvc中的@Controller、@Autowired、@RequestMapping等。

@Target

表示被标记的注解可以用于那种java元素(类、接口、属性、方法)。一共有一下八种。

其中ElementType是枚举类型其定义如下:表示可能的取值范围

public enum ElementType{
     // 标明该注解可以用于类、接口(包括注解类型)或enum声明
    TYPE,

    // 标明该注解可以用于字段(域)声明,包括enum实例 
    FIELD,

    // 标明该注解可以用于方法声明 
    METHOD,

    // 标明该注解可以用于参数声明 
    PARAMETER,

    // 标明注解可以用于构造函数声明 
    CONSTRUCTOR,

    // 标明注解可以用于局部变量声明 
    LOCAL_VARIABLE,

    // 标明注解可以用于注解声明(应用于另一个注解上)
    ANNOTATION_TYPE,

    // 标明注解可以用于包声明 
    PACKAGE,

    //标明注解可以用于类型参数声明(1.8新加入)
    TYPE_PARAMETER,

    // 类型使用声明(1.8新加入)
    TYPE_USE
}

当注解为指定Target值时,此注解可以用于任何元素之上,多个值使用{}包含并用逗号隔开。

@Documented

无论何时使用指定的注解,都应使用Javadoc工具记录这些元素。(即会在生成的javadoc中加入注解说明)

@Inherited

可以从超类继承注释类型,仅用于类的声明(接口不会继承)

@Repeatable

在Java SE 8中引入的,表示标记的注释可以多次应用于相同的声明或类型使用。

注解的分类

通过对元注解的了解,我明白了一个注解都是由这些元注解修饰而来,而且我们也收获了一个重要信息——注解可以修饰注解

这样无限的套娃,就会有各种各样的注解,那么到底有哪些注解呢?常见的注解大致分为以下四类:

  • 元注解:一种有五种
  • jdk注解:常见的有我们的@Override、@Deprecated:用于标明以及过时的方法或者类等
  • 第三方注解:就是我们熟悉的第三方框架Spring等的注解,@Autowired等
  • 自定义注解:即开发人员根据项目需求自定义的注解,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的作用,在文章的最后我们还要实际编写一个注解。

注解处理器

使用注解的过程中很重要的一部分就是创建于使用注解处理器

  • 示例:

    • 定义注解:
import java.lang.annotation.*;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FruitProvider{
    public int id() default -1;
    public String name() default "";
    public String address() default "";
}
    • 注解使用:
public class Apple {
    @FruitProvider(id=1,name="zhonghu",address = "china")
    private String appleProvider;

    public String getAppleProvider() {
        return appleProvider;
    }

    public void setAppleProvider(String appleProvider) {
        this.appleProvider = appleProvider;
    }
}
    • 注解处理器:
import java.lang.reflect.Field;


public class FruitInfoUtil {
    public static void getFruitInfo(Class<?> clazz){
        String strFruitProvicer = "供应商信息:";
        Field[] fields = clazz.getDeclaredFields();//通过反射获取处理注解
        for (Field field : fields) {
            if (field.isAnnotationPresent(FruitProvider.class)) {
                FruitProvider fruitProvider = (FruitProvider) field.getAnnotation(FruitProvider.class);
                //注解信息的处理地方
                strFruitProvicer = " 供应商编号:" + fruitProvider.id() + " 供应商名称:"
                        + fruitProvider.name() + " 供应商地址:"+ fruitProvider.address();
                System.out.println(strFruitProvicer);
            }
        }
    }
}
    • 输出
public class FruitRun {
    public static void main(String[] args) {
        FruitInfoUtil.getFruitInfo(Apple.class);
    }
}
  • 结果:
    image

注解的作用

  • 在编译时进行格式检查。如@Override
  • 跟踪代码依赖性,实现替代配置文件功能。通过处理注解信息生成代码、XML文件。
  • 一些注释可以在运行时进行检查

开始撸注解

在实战开始前,我们还需要了解一下编写自定义注解的规则

规则

  • 注解的定义为@interface,所有的注解会自动继承java.lang.Annotation这个接口,并且不能再去继承别的类或者接口

注解不支持继承

  • 参数成员只能用publicdefault(默认)访问权限符修饰
  • 参数成员只能用八大基本数据类型、StringEnumClassannotations等数据类型,以及这些类型的数组

不允许使用任何包装类型,注解也是可以作为元素的类型,也就是嵌套注解

  • 要获取类方法和字段的注解信息,必须通过java反射机制来获取

同时为了运行时能准确获取到注解的相关信息,java在java.lang.reflect反射包下新增了AnnotatedElement接口,用于表示目前正在JVM运行的程序中已使用注解的元素,通过此接口提供的方法可以利用反射技术,读取注解的信息。

  • 注解也可以没有定义成员(只起到标识作用)

编写注解

​ 上面我们介绍了注解的使用以及注解的信息下面,我们来撸一个字段的注解,用来标记对象在序列化成JSON的时候要不要包含这个字段

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface JsonField {
    public String value() default "";
}
  • 解释:

    • JsonField注解的生命周期是RUNTIME,即运行时有效
    • JsonField注解修饰的目标是FIELD,也就是针对字段的
    • 创建注释需要用到@interface关键字
    • JsonField注解只有一个参数,名字为value,类型是String,默认值为一个空字符串

参数名为value,允许注解的使用者提供一个无需指定名字的参数。即我们可以在一个字段上使用@JsonField(value=“冢狐”),也可以把value=省略,变成@JsonField(“冢狐”)

    • default “ ”允许我们在一个字段上直接使用@JsonField,而无需指定参数的名和值

使用注解

​ 创建一个类,包含三个字段:age、name、address,后两个是必选序列化的字段

public class People {
    private  int age ;

    @JsonField("writeName")
    private String name;

    @JsonField
    private String address;

    public People(int age,String name,String address){
        this.age=age;
        this.name=name;
        this.address=address;
    }

    @Override
    public String toString() {
        return "People{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", address='" + address + '\'' +
                '}';
    }
}

其中:

  • name上的@JsonField注解提供了显示的字符串值
  • address上的@JsonField注解使用了缺省值

接下来我们编写序列化类JsonSerializer:

public class JsonSerializer {
    public static String serialize(Object object) throws IllegalAccessException {
        Class<?> objectClass = object.getClass();
        Map<String, String> jsonElements = new HashMap<>();
        for (Field field : objectClass.getDeclaredFields()) {
            field.setAccessible(true);
            if (field.isAnnotationPresent(JsonField.class)) {
                jsonElements.put(getSerializedKey(field), (String) field.get(object));
            }
        }
        return toJsonString(jsonElements);
    }

    private static String getSerializedKey(Field field) {
        String annotationValue = field.getAnnotation(JsonField.class).value();
        if (annotationValue.isEmpty()) {
            return field.getName();
        } else {
            return annotationValue;
        }
    }

    private static String toJsonString(Map<String, String> jsonMap) {
        String elementsString = jsonMap.entrySet()
                .stream()
                .map(entry -> "\"" + entry.getKey() + "\":\"" + entry.getValue() + "\"")
                .collect(Collectors.joining(","));
        return "{" + elementsString + "}";
    }
}

下面我们看一下各自的含义以及作用:

  • serialize() 方法是用来序列化对象的,它接收一个 Object 类型的参数。objectClass.getDeclaredFields() 通过反射的方式获取对象声明的所有字段,然后进行 for 循环遍历。在 for 循环中,先通过 field.setAccessible(true) 将反射对象的可访问性设置为 true,供序列化使用(如果没有这个步骤的话,private 字段是无法获取的,会抛出 IllegalAccessException 异常);再通过 isAnnotationPresent() 判断字段是否装饰了 JsonField 注解,如果是的话,调用 getSerializedKey() 方法,以及获取该对象上由此字段表示的值,并放入 jsonElements 中。
  • getSerializedKey() 方法用来获取字段上注解的值,如果注解的值是空的,则返回字段名。
  • toJsonString() 方法借助 Stream 流的方式返回格式化后的 JSON 字符串。

测试注解

public class JsonFileTest {
    public static void main(String[] args) throws IllegalAccessException{
        People cmower = new People(18,"冢狐","中国");
        System.out.println(JsonSerializer.serialize(cmower));
    }
}

  • 结果:

{“writeName”:“冢狐”,“address”:“中国”}

  • 分析

    • 首先age字段没有被@JsonField注解所以没有序列化
    • name修饰了@JsonField注解,并且显示指定了字符串writerName,所以序列化后变成了writeName
    • address字段修饰了@JsonField注解,但是没有显示指定值,所以序列化后还是address

最后

  • 如果觉得看完有收获,希望能给我点个赞,这将会是我更新的最大动力,感谢各位的支持
  • 欢迎各位关注我的公众号【java冢狐】,专注于java和计算机基础知识,保证让你看完有所收获,不信你打我
  • 如果看完有不同的意见或者建议,欢迎多多评论一起交流。感谢各位的支持以及厚爱。

——我是冢狐,和你一样热爱编程。

image

猜你喜欢

转载自blog.csdn.net/issunmingzhi/article/details/112000502