详解Java注解机制

上篇详细研究了Java中的反射操作以及Class类相关内容,但在Java开发过程中,除了反射,往往还有泛型、
注解等相关特性操作组合使用来实现一些高级技术,如Spring中就大量使用了反射和注解,实现了诸如Bean
容器管理机制等操作,SpringMvc框架中大量使用了注解,实现了servlet容器的简易操作等,现在我们开始详
细的学习Java中的注解机制

注解是什么

日常开发中经常提到注解,那么注解是什么呢?在Java中,注解就是给程序添加一些信息,用字符@开头,可以用来修饰后续的其他代码元素,比如类、接口、字段、方法、参数、构造方法等,往往注解还搭配编译器、程序运行时以及其他工具或者插件使用,用于实现代码功能的增强或者修改程序的行为等操作

Java内置注解

在Java中内置了一些注解,用来在类、方法申明使用,从而实现编译器检查、避免编译器检查等操作,同时也提高了java逻辑的严谨性,而在Java中内置的常见注解莫过于@Override、@Deprecated、@SuppressWarnings三个,下面分别介绍这三个常见的java内置注解的作用

@Override

@Override注解修饰于方法上,表明当前方法不是当前类首先申明的,而是由父类或者接口中继承来的方法,并且当前类进行了方法重写操作,比如:

public class Base {
    public void action() {};
}

现在有一个父类Base,申明一个方法--action,现在有一个子类继承了Base类,并且重写了action方法的内部实现,如下:

public class Child extends Base {
    @Override
    public void action(){
        System.out.println("child action");
    }
}

可以看到在Child类的action方法上有一个@Override注解,代表着此方法是Child类重新实现并且继承于Base类,但是当我们把这个注解删除,发现工程无论是编译还是运行,和之前一般无二,那么编译器为什么还要给我们默认添加这个注解呢?其实我们可以反过来思考,如果我们的类中有一个方法,并且使用了@Override注解进行修饰,但是当前类并没有从别的类或者接口继承来这个方法,或者此类本身就是独立的类,这个时候会发生什么呢?

public class Child extends Base {
    @Override
    public void action(){
        System.out.println("child action");
    }
    //action1方法在Base中不存在
    @Override
    public void action1(){
        System.out.println("child class");
    }
}

很明显当我们加上注解的瞬间,编译器就会直接提示异常,要求我们删除当前注解。从这可以看出来,@Override注解有助于帮助编译器检查语法错误,执行更严格的代码检查。

@Deprecated

@Deprecated注解可以修饰的范围很广,不仅可以作用在方法上,还可以修饰类、字段以及参数等所有注解可以修饰的范围,此注解代表被修饰的元素已经过时,并且警告使用者建议使用其他方法,不再使用当前方法。例如,Date类中就有很多方法被标识已经过时了:

@Deprecated
public Date(int year, int month, int date){
    .....
}
    
@Deprecated
public int getYear(){
    .....
}

当我们在使用这些方法的时候,ide往往会给我们加上一个删除线用来辅助提示调用者此方法已经过时,随时都可能在下个版本删除,不建议使用。而在Java9中,Deprecated注解又多了可填的属性,分别是sinceforRemoval,since属性类型为String类型,表示被修饰的元素从哪一个版本号开始过时,不建议使用,而forRemoval属性为Boolean类型,表示将来在过时的时候是否会删除当前元素,标记为true代表将来会删除此元素,使用者请慎用。而在Java9中,Integer包装类的构造方法中就有使用此特性标识的,如下:

@Deprecated(since="9")
public Integer(int value) {
    this.value = value;
}

@SuppressWarnings

@SuppressWarnings注解则是表示压制Java中的编译警告措施,使得编译器放弃被修饰元素的编译警告,此注解有一个必填参数,表示压制的是哪种类型的警告,此注解也可以使用在几乎所有元素中。比如我们在开发中使用了Date类中的一些过期或者有异常抛出风险的方法,ide往往就会添加一个警告的黄线,而我们在方法上添加@SuppressWarnings注解,则会发现警告消失不见,如下:

//添加在方法上,取消掉编译器的严格检查警告机制
@SuppressWarnings({"deprecation","unused"})
public static void main(String[] args) {
    Date date = new Date(2019, 10, 12);
    int year = date.getYear();
}

常见的库中的注解

日常开发使用的库中也有着大量的注解,例如Jackson、SpringMvc等,下面就简单介绍下常见库中的常见注解使用

Jackson

Jackson是一个通用的序列化库,程序员使用过程中可以使用它提供的注解机制对序列化进行定制化操作,比如:

.使用@JsonIgnore和@JsonIgnoreProperties配置序列化的过程中忽略部分字段

.使用@JsonManagedReference和@JsonBackReference可以配置实例之间的互相引用

.使用@JsonProperty和@JsonFormat配置序列化的过程中字段名称和属性字段的格式等

Servlet3.0

随着web开发技术的发展,Java web已经发展到了Servlet3.0,在早期使用Servlet的时候,我们只能在web.xml中配置,但是当我们使用Servlet3.0的时候开始,已经开始支持注解了,比如我们可以使用@WebServlet配置一个类为Servlet类,如下:

@WebServlet(urlPatterns = "/async", asyncSupported = true)
public class AsyncDemoServlet extends HttpServlet {
  //..............
}
SpringMvc

同样的,在web开发中,我们往往还会使用SpringMvc框架来简化开发,其框架的大量注解可以帮助我们减少大量的业务代码,例如一个请求的参数和字段/实例之间的映射关系,一个方法使用的是Http的什么请求方法,对应请求的某个路径,同样的请求如何解析,返回的响应报文格式定义等,这些都可以使用注解来简化实现,一个简单的Mvc操作如下:

@Controller
@RequestMapping("/hello")
public class HelloController {

    @GetMapping("/test")
    @ResponseBody
    public String test(){
        return "hello test";
    }
}

其中@Controller注解标明当前的类是SpringMvc接管的一个Bean实例,@RequestMapping("/hello")则是代表当前Bean的前置请求路径比如是/hello开头, @GetMapping("/test")则是表示test方法被访问必须是Http请求的get请求,并且路径必须是/hello/test为路径前置,@ResponseBody注解则是标明了当前请求的相应信息按照默认的格式返回(根据后缀名来确定格式)

注解的创建

从上面我们可以看到使用注解的确可以很方便的简化我们开发过程,因此很多库和开发过程中,也会使用大量的注解简化开发,那么这些注解我们如何实现呢?首先我们先看看最常见的注解@Override的创建:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

从上面我们可以看到,Override类中使用了@Target元注解和@Retention元注解来定义整个注解,@Target表示需要注解的目标,@Retention则是标明当前注解的信息可以保留到Java的什么阶段,而除了这两个元注解以外,还有两个元注解@Documented和@Inherited,@Documented用来表示当前的注解信息是否包含到文档中,@Inherited注解则是和注解之间的继承有对应的关系,那么这些元注解具体有什么作用,以及具体有哪些参数可以选择呢?接下来我们便分别学习一下

@Target

@Target注解表示当前注解可以使用在什么类型的元素上,这里的值可以多选,即一个注解可以作用在多种不同类型的元素上,具体的可选值在ElementType枚举类中,值如下:

取值 解释
TYPE 表示作用在类、接口上
FIELD 表示作用在字段,包括枚举常量中
METHOD 表示作用在方法中
PARAMETER 表示作用在方法中的参数中
CONSTRUCTOR 表示作用在构造方法中
LOCAL_VARIABLE 表示作用在本地常量中
MODULE 表示作用在部分模块中(Java9引入的概念)
ANNOTATION_TYPE 表示当前注解作用在定义其他注解中,即元注解
PACKAGE 表示当前注解使用在包的申明中
TYPE_PARAMETER 表明当前注解使用在类型参数的申明中(Java8新增)
TYPE_USE 表明当前注解使用在具体使用类型中(Java8新增)

当使用多个作用域范围的时候,使用{}包裹多个参数,比如@SuppressWarnings注解的Target就有多个,在Java7中的定义为:

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
    String[] value();
}

即代表SuppressWarnings注解均可以作用在这七种元素上

@Retention

@Retention注解则是表明了当前注解可以保留到Java多个阶段的哪一个阶段,参数类型为RetentionPolicy枚举类,可取值如下:

取值 解释
SOURCE 此注解仅在源代码阶段保留,编译后即丢失注解部分
CLASS 表示编译后依然保留在Class字节码中,但是加载时不一定会在内存中
RUNTIME 表示不仅保留在Class字节码中,一直到内存使用时仍然存在

此注解有默认值,即当我们没有申明@Retention的时候,默认则是Class取值范围

@Documented

@Documented注解没有具体的参数,使用此元注解,则表示带有类型的注解将由javadoc记录

@Inherited

@Inherited注解与注解的继承有关系,具体关系为如果使用了当前的元注解,则表示此注解可以被其他的注解的子类直接继承,但是需要注意的是对已实现接口上的注解将没有作用。

我们通过一个案例来了解@Inherited注解的作用,首先我们定义一个Test注解:

@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
}

Test注解上有@Inherited元注解修饰,则表明Test注解会被继承,接着我们在Base类上使用Test注解:

@Test
public class Base {
}

这个时候实现一个子类Child,我们来通过反射类Class中的isAnnotationPresent方法打印,看此类中的Test是否存在且是来自于父类继承而来:

public class Child extends Base {
}
-----------------------------------
 public static void main(String[] args) {
    System.out.println(Child.class.isAnnotationPresent(Test.class));//true
}

最后输出的结果为true,则表明Child类中存在@Test注解,并且是由继承父类而来

使用注解实现简单定制序列化

上面我们已经学习了注解定义和创建相关的内容,接下来我们利用注解简单实现通用格式化输出的类SimpleFormatter,类中有一个方法format,方法参数为Object类型的实例对象,即表明了对象的序列化方式,类定义如下:

/**
 * 通用格式转换输出类
 */
public class SimpleFormatter {
    /**
     * 通用格式化方法==>将obj对象输出为String
     * @param obj
     * @return
     */
    public static String format(Object obj){
        try{
            Class<?> cls = obj.getClass();
            StringBuilder builder = new StringBuilder();
            for (Field field : cls.getDeclaredFields()) {
                if(!field.isAccessible()){
                    field.setAccessible(true);//放弃java安全检测,设置可以访问私有字段
                }
                //获取Label注解-输出的字段名称
                Label label = field.getAnnotation(Label.class);
                String name = null == label ? field.getName() : label.value();
                //获取字段对应的value
                Object value = field.get(obj);
                //如果是Date类型,走时间格式化
                if(null != value && field.getType() == Date.class){
                    value = formatter(field,value);
                }
                builder.append(name + "?" + value + "\n");
            }
            return builder.toString();
        }catch (Exception e){
            e.printStackTrace();
            throw new RuntimeException("格式化输出失败:"+e.getMessage());
        }
    }

    /**
     * 针对时间类型字段进行格式化的方法
     */
    private static Object formatter(Field field, Object value) {
        Format format = field.getAnnotation(Format.class);
        if(null == format){
            return value;
        }
        String pattern = format.pattern();
        String timezone = format.timezone();
        SimpleDateFormat sdf = new SimpleDateFormat(pattern);
        sdf.setTimeZone(TimeZone.getTimeZone(timezone));
        return sdf.format(value);
    }
}

而除了格式化的类以外,我们还定义了两个注解,一个用来表明格式化的时候的字段名称,一个用来针对时间格式字段输出的格式,如下:

/**
 * Labl表明当前字段输出的名称,仅作用在字段上
 */
@Retention(RUNTIME)
@Target(FIELD)
public @interface Label {
    String value() default "";
}
--------------------------------
/**
 * Format注解作用在字段上,针对时间字段类型的输出格式
 */
@Retention(RUNTIME)
@Target(FIELD)
public @interface Format {
    String pattern() default "yyyy-MM-dd HH:mm:ss";
    String timezone() default "GMT+8";
}

除此之外,我们还需要一个实例类:

public class Student {
    @Label("姓名")
    private String name;
    @Label("出生日期")
    @Format(pattern="yyyy/MM/dd")
    private Date born;
    @Label("分数")
    private double score;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Date getBorn() {
        return born;
    }

    public void setBorn(Date born) {
        this.born = born;
    }

    public double getScore() {
        return score;
    }

    public void setScore(double score) {
        this.score = score;
    }
}

定义完毕后,我们可以这么来使用:

public static void main(String[] args) throws ParseException {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
        Student zhangsan = new Student();
        zhangsan.setName("张三");
        zhangsan.setBorn(format.parse("1990-12-12"));
        zhangsan.setScore(655);
        System.out.println(SimpleFormatter.format(zhangsan));
    }

输出的结果为:

姓名?张三
出生日期?1990/12/12
分数?655.0

至此,一个简单的格式化输出类就完成了

推荐阅读更多精彩内容