Java注解

java注解

在Java中,注解就是给程序添加一些信息,用字符@开头,这些信息用于修饰它后面紧挨着的其他代码元素,比如类、接口、字段、方法、方法中的参数、构造方法等,注解可以被编译器、程序运行时和其他工具使用,用于增强或修改程序行为等。

内置注解

Java内置了一些常用注解,比如:@Override@Deprecated@SuppressWarnings

@Override修饰一个方法,表示该方法不是当前类首先声明的,而是在某个父类或实现的接口中声明的,当前类"重写"了该方法。

@Deprecated可以修饰的范围很广,包括类、方法、字段、参数等,它表示对应的代码已经过时了,程序员不应该使用它,不过,它是一种警告,而不是强制性的,在IDE如Eclipse中,会给Deprecated元素加一条删除线以示警告,比如,Date中很多方法就过时了:

@SuppressWarnings表示压制Java的编译警告,它有一个必填参数,表示压制哪种类型的警告,它也可以修饰大部分代码元素,在更大范围的修饰也会对内部元素起效,比如,在类上的注解会影响到方法,在方法上的注解会影响到代码行。对于上面Date方法的调用,如果不希望显示警告,可以这样:@SuppressWarnings({"deprecation","unused"})

框架和库的注解

各种框架和库定义了大量的注解,程序员使用这些注解配置框架和库,与它们进行交互,例如依赖注入容器Web应用框架

现代Java开发经常利用某种框架管理对象的生命周期及其依赖关系,这个框架一般称为DI(Dependency Injection)容器,DI是指依赖注入,流行的框架有Spring、Guice等,在使用这些框架时,程序员一般不通过new创建对象,而是由容器管理对象的创建,对于依赖的服务,也不需要自己管理,而是使用注解表达依赖关系。这么做的好处有很多,代码更为简单,也更为灵活,比如容器可以根据配置返回一个动态代理,实现AOP。

看个简单的例子,Guice定义了Inject注解,可以使用它表达依赖关系,比如像下面这样:

public class OrderService {
    
    @Inject
    UserService userService;
    
    @Inject
    ProductService productService;
}

在Web开发中,典型的架构都是MVC(Model-View-Controller),典型的需求是配置哪个方法处理哪个URL的什么HTTP方法,然后将HTTP请求参数映射为Java方法的参数,各种框架如Spring MVC 都支持使用注解进行配置,比如,使用Jersey的一个配置示例为:

@Path("/hello")
public class HelloResource {
    
    @GET
    @Path("test")
    @Produces(MediaType.APPLICATION_JSON)
    public Map<String, Object> test(
            @QueryParam("a") String a) {
        Map<String, Object> map = new HashMap<>();
        map.put("status", "ok");
        return map;
    }
}

类HelloResource将处理Jersey配置的根路径下/hello下的所有请求,而test方法将处理/hello/test的GET请求,响应格式为JSON,自动映射HTTP请求参数a到方法参数String a。

创建注解

框架和库是怎么实现注解的呢?我们来看注解的创建。

举例:@Override的定义

@Target(ElementType.METHOD)
//多个目标:@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

定义注解与定义接口有点类似,都用了interface,不过注解的interface前多了@,另外,它还有两个元注解@Target和@Retention,这两个注解专门用于定义注解本身

@Target

@Target表示注解的目标,@Override的目标是方法(ElementType.METHOD),ElementType是一个枚举,其他可选值有:

  • TYPE:表示类、接口(包括注解),或者枚举声明
  • FIELD:字段,包括枚举常量
  • METHOD:方法
  • PARAMETER:方法中的参数
  • CONSTRUCTOR:构造方法
  • LOCAL_VARIABLE:本地变量
  • ANNOTATION_TYPE:注解类型
  • PACKAGE:包

目标可以有多个,用{}表示,比如@SuppressWarnings的@Target就有多个,定义为:

如果没有声明@Target,默认为适用于所有类型。

@Retention

@Retention表示注解信息保留到什么时候,取值只能有一个,类型为RetentionPolicy,它是一个枚举,有三个取值:

  • SOURCE:只在源代码中保留,编译器将代码编译为字节码文件后就会丢掉
  • CLASS:保留到字节码文件中,但Java虚拟机将class文件加载到内存时不一定会在内存中保留
  • RUNTIME:一直保留到运行时

如果没有声明@Retention,默认为CLASS。

@Override和@SuppressWarnings都是给编译器用的,所以@Retention都是RetentionPolicy.SOURCE。

定义参数

可以为注解定义一些参数,定义的方式是在注解内定义一些方法,比如@SuppressWarnings内定义的方法value,返回值类型表示参数的类型,这里是String[],使用@SuppressWarnings时必须给value提供值,比如:

@SuppressWarnings(value={"deprecation","unused"})
@SuppressWarnings({"deprecation","unused"})//当只有一个参数,且名称为value时,提供参数值时可以省略"value="

注解内参数的类型不是什么都可以的,合法的类型有基本类型、String、Class、枚举、注解、以及这些类型的数组

参数定义时可以使用default指定一个默认值,比如,Guice中Inject注解的定义:

@Target({ METHOD, CONSTRUCTOR, FIELD })
@Retention(RUNTIME)
@Documented
public @interface Inject {
  boolean optional() default false;//参数定义时可以使用default指定一个默认值
}

它有一个参数optional,默认值为false。如果类型为String,默认值可以为"",但不能为null。如果定义了参数且没有提供默认值,在使用注解时必须提供具体的值,不能为null。

@Inject多了一个元注解@Documented,它表示注解信息包含到Javadoc中。

@Inherited

与接口和类不同,注解不能继承。不过注解有一个与继承有关的元注解@Inherited,它表示子类是否能够继承父类的该注解。

public class InheritDemo {
    @Inherited
    @Retention(RetentionPolicy.RUNTIME)
    static @interface Test {
    }
    
    @Test
    static class Base {
    }
    
    static class Child extends Base {
    }
    
    public static void main(String[] args) {
        System.out.println(Child.class.isAnnotationPresent(Test.class));//检查Child类是否有Test注解
    }
}
//输出为true,这是因为Test有注解@Inherited,如果去掉,输出就变成false了。

查看注解信息

创建了注解,就可以在程序中使用,注解指定的目标,提供需要的参数,但这还是不会影响到程序的运行。要影响程序,要先能查看这些信息。

我们主要考虑@Retention为RetentionPolicy.RUNTIME的注解,利用反射机制在运行时进行查看和利用这些信息。

反射相关类中与注解有关的方法,这里汇总说明下,Class、Field、Method、Constructor中都有如下方法:

//获取所有的注解
public Annotation[] getAnnotations()
//获取所有本元素上直接声明的注解,忽略inherited来的
public Annotation[] getDeclaredAnnotations()
//获取指定类型的注解,没有返回null
public <A extends Annotation> A getAnnotation(Class<A> annotationClass)
//判断是否有指定类型的注解
public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)

Annotation是一个接口,它表示注解,具体定义为:

public interface Annotation {
    boolean equals(Object obj);
    int hashCode();
    String toString();
    //返回真正的注解类型
    Class<? extends Annotation> annotationType();
}

实际上,所有的注解类型,内部实现时,都是扩展的Annotation。

对于Method和Contructor,它们都有方法参数,而参数也可以有注解,所以它们都有如下方法:

public Annotation[][] getParameterAnnotations()//获取方法参数的注解,返回值是一个二维数组,每个参数对应一个一维数组

返回值是一个二维数组,每个参数对应一个一维数组,我们看个简单的例子:

public class MethodAnnotations {
    @Target(ElementType.PARAMETER)
    @Retention(RetentionPolicy.RUNTIME)
    static @interface QueryParam {//注解1
        String value();
    }
    
    @Target(ElementType.PARAMETER)
    @Retention(RetentionPolicy.RUNTIME)
    static @interface DefaultValue {//注解2
        String value() default "";
    }
    
    public void hello(@QueryParam("action") String action,
            @QueryParam("sort") @DefaultValue("asc") String sort){
        // ...
    }
    
    public static void main(String[] args) throws Exception {
        Class<?> cls = MethodAnnotations.class;
        Method method = cls.getMethod("hello", new Class[]{String.class, String.class});
        
        Annotation[][] annts = method.getParameterAnnotations();//二维数组
        for(int i=0; i<annts.length; i++){
            System.out.println("annotations for paramter " + (i+1));
            Annotation[] anntArr = annts[i];
            for(Annotation annt : anntArr){
                if(annt instanceof QueryParam){//如果是QueryParam注解
                    QueryParam qp = (QueryParam)annt;
                    System.out.println(qp.annotationType().getSimpleName()+":"+ qp.value());
                }else if(annt instanceof DefaultValue){
                    DefaultValue dv = (DefaultValue)annt;
                    System.out.println(dv.annotationType().getSimpleName()+":"+ dv.value());
                }
            }
        }
    }
}
//输出
//annotations for paramter 1
//QueryParam:action
//annotations for paramter 2
//QueryParam:sort
//DefaultValue:asc

定义了注解,通过反射获取到注解信息,但具体怎么利用这些信息呢?我们看两个简单的示例,一个是定制序列化,另一个是DI容器。

应用注解 - 定制序列化

定义注解

我们实现一个简单的类SimpleFormatter,它有一个方法:

public static String format(Object obj)

我们定义两个注解,@Label和@Format,@Label用于定制输出字段的名称,@Format用于定义日期类型的输出格式,它们的定义如下:

@Retention(RUNTIME)
@Target(FIELD)
public @interface Label {
    String value() default "";
}

@Retention(RUNTIME)
@Target(FIELD)
public @interface Format {
    String pattern() default "yyyy-MM-dd HH:mm:ss";
    String timezone() default "GMT+8";
}

使用注解

可以用这两个注解来修饰要序列化的类字段,比如:

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

    public Student() {
    }

    public Student(String name, Date born, Double score) {
        super();
        this.name = name;
        this.born = born;
        this.score = score;
    }

    @Override
    public String toString() {
        return "Student [name=" + name + ", born=" + born + ", score=" + score + "]";
    }
}

我们可以这样来使用SimpleFormatter:

SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Student zhangsan = new Student("张三", sdf.parse("1990-12-12"), 80.9d);
System.out.println(SimpleFormatter.format(zhangsan));
//输出为:
//姓名:张三
//出生日期:1990/12/12
//分数:80.9

利用注解信息

可以看出,输出使用了自定义的字段名称和日期格式,SimpleFormatter.format()是怎么利用这些注解的呢?我们看代码:

public static String format(Object obj) {
    try {
        Class<?> cls = obj.getClass();
        StringBuilder sb = new StringBuilder();
        for (Field f : cls.getDeclaredFields()) {//获取属性
            if (!f.isAccessible()) {
                f.setAccessible(true);
            }
            Label label = f.getAnnotation(Label.class);//获取Label注解
            String name = label != null ? label.value() : f.getName();//很粗糙的代码,获取变量值
            Object value = f.get(obj);
            if (value != null && f.getType() == Date.class) {//如果是Data类型的变量,可以去format
                value = formatDate(f, value);
            }
            sb.append(name + ":" + value + "\n");
        }
        return sb.toString();
    } catch (IllegalAccessException e) {
        throw new RuntimeException(e);
    }
}
//对于日期类型的字段,调用了formatDate
private static Object formatDate(Field f, Object value) {
    Format format = f.getAnnotation(Format.class);
    if (format != null) {
        SimpleDateFormat sdf = new SimpleDateFormat(format.pattern());
        sdf.setTimeZone(TimeZone.getTimeZone(format.timezone()));
        return sdf.format(value);
    }
    return value;
}

应用注解 - DI容器

定义@SimpleInject

我们再来看一个简单的DI容器的例子,我们引入一个注解@SimpleInject,修饰类中字段,表达依赖关系,定义为:

//定义@SimpleInject
@Retention(RUNTIME)
@Target(FIELD)
public @interface SimpleInject {
}

//两个简单的服务ServiceA和ServiceB,ServiceA依赖于ServiceB
public class ServiceA {

    @SimpleInject
    ServiceB b;
    
    public void callB(){
        b.action();
    }
}

public class ServiceB {

    public void action(){
        System.out.println("I'm B");
    }
}

//DI容器的类为SimpleContainer,提供一个方法getInstance
class SimpleContainer {
  //SimpleContainer.getInstance会创建需要的对象,并配置依赖关系
  public static <T> T getInstance(Class<T> cls) {
    try {
        T obj = cls.newInstance();//构造新的实例
        Field[] fields = cls.getDeclaredFields();
        for (Field f : fields) {
            if (f.isAnnotationPresent(SimpleInject.class)) {//查找所有需要依赖注入的属性
                if (!f.isAccessible()) {
                    f.setAccessible(true);
                }
                Class<?> fieldCls = f.getType();
                f.set(obj, getInstance(fieldCls));//递归构造依赖变量
            }
        }
        return obj;
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
 }
}

应用程序使用该方法获取对象实例,而不是自己new,使用方法如下所示:

ServiceA a = SimpleContainer.getInstance(ServiceA.class);
a.callB();

定义@SimpleSingleton

在上面的代码中,每次获取一个类型的对象,都会新创建一个对象,实际开发中,这可能不是期望的结果,期望的模式可能是单例,即每个类型只创建一个对象,该对象被所有访问的代码共享,怎么满足这种需求呢?我们增加一个注解@SimpleSingleton,用于修饰类,表示类型是单例,定义如下:

@Retention(RUNTIME)
@Target(TYPE)
public @interface SimpleSingleton {
}

//修饰ServiceB
@SimpleSingleton
public class ServiceB {

    public void action(){
        System.out.println("I'm B");
    }
}

//SimpleContainer也需要做修改
class SimpleContainer {
  //增加一个静态变量,缓存创建过的单例对象
  private static Map<Class<?>, Object> instances = new ConcurrentHashMap<>();
  
  //
  public static <T> T getInstance(Class<T> cls) {
    try {
        boolean singleton = cls.isAnnotationPresent(SimpleSingleton.class);
        if (!singleton) {
            return createInstance(cls);
        }
        Object obj = instances.get(cls);
        if (obj != null) {
            return (T) obj;
        }
        synchronized (cls) {
            obj = instances.get(cls);//首先检查类型是否是单例,如果不是,就直接调用createInstance创建对象。否则,检查缓存,如果有,直接返回,没有的话,调用createInstance创建对象,并放入缓存中。
            if (obj == null) {
                obj = createInstance(cls);
                instances.put(cls, obj);
            }
        }
        return (T) obj;
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}
  //第一版的getInstance类似
  private static <T> T createInstance(Class<T> cls) throws Exception {
    T obj = cls.newInstance();
    Field[] fields = cls.getDeclaredFields();
    for (Field f : fields) {
        if (f.isAnnotationPresent(SimpleInject.class)) {
            if (!f.isAccessible()) {
                f.setAccessible(true);
            }
            Class<?> fieldCls = f.getType();
            f.set(obj, getInstance(fieldCls));
        }
    }
    return obj;
}
}

注解提升了Java语言的表达能力,有效地实现了应用功能和底层功能的分离,框架/库的程序员可以专注于底层实现,借助反射实现通用功能,提供注解给应用程序员使用,应用程序员可以专注于应用功能,通过简单的声明式注解与框架/库进行协作。

ref:Java编程的逻辑 (85) - 注解

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容

  • 什么是注解 注解对于开发人员来讲既熟悉又陌生,熟悉是因为只要你是做开发,都会用到注解(常见的@Override);...
    张明云阅读 25,107评论 6 154
  • java自定义注解 Java注解是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配...
    尼尔君阅读 515评论 0 0