Java Annotation 再度历险
Java Annotation 再度历险
在云端-看世间变幻 发表于4年前
Java Annotation 再度历险
  • 发表于 4年前
  • 阅读 132
  • 收藏 0
  • 点赞 0
  • 评论 0

新睿云服务器60天免费使用,快来体验!>>>   

本文是对 Java Annotation 基本知识的总结和梳理

Java annotation 官方说明

Annotations, a form of metadata ( "meta data" 我的理解是编程时就能确切知道的能够提供给运行时的信息), provide data about a program that is not part of the program itself. Annotations have no direct effect on the operation of the code they annotate.

从上面定义的话大概体会是这么个意思:

Annotaion 是对目标代码的标签性注释,或者叫说明,用来给Annotation所标记的对象打上标签,就好比人的名字,某种意义来说就是标签。比如某场活动中要找出所有叫 “张三” 的人,这个时候 “张三” 这个标签就起到标识的作用了。

比如Spring 提供的 Annotation, @Autowired, @Transaction 等表明了注解标注的对象在某种场景下会具有标识性。

Annotation 的用法

首先来看 Annotation 可以提供的一些比较重要的作用:

  • Information for the compiler — Annotations can be used by the compiler to detect errors or suppress warnings. (为编译器提供有用的信息来发现错误或者忽略警告,例如 @SuppressWarnings)

  • Compile-time and deployment-time processing — Software tools can process annotation information to generate code, XML files, and so forth. (在编译和部署期间生成代码、XML文件等)

  • Runtime processing — Some annotations are available to be examined at runtime. (提供运行时的信息)

下面就来定义个简单的 Annotation 试试

:::Java
/**
 * 定义一个用来生成代码的作者信息的 Annotation。 定义方法和 
 * Interface 类似,只是在 Interface 之前添加了一个 @ 符号
 */
public @interface Author {

    String name();
    String date();
}

/**
 * 然后用定义好的类来修饰一个Class
 */
@Author(name="Falcon", date="2013-11-02")
public class HelloWorld {
    
    
    @Author(name="Matt", date="2013-11-01")
    public static String var = "";


    @Author(name="Newton", date="2013-11-03")
    public static void main(String[] args) {

        System.out.println("Hello World!");

    }
}

这样就可以了,但是如果只标记 Class 而什么事都不做的话岂不是太无聊了。所以呢继续替 Autor 注解添加一个 jdk 预定义的 Annotation @Documented, 这样就可以使用Javadoc 在生成的代码文档中添加 Author 的注解信息了。(没错,注解可以修饰注解,这叫原注解 - meta annotation)

@Documented
public @interface Author {

    String name();
    String date();
}

接下来用 Javadoc 生成文档看看。

javadoc org.cgfalcon.annotation

命令将生成 org.cgfalcon.annotation 包下所有源文件的文档。

先补个不加 @Documented 注解时生成的图

annotation_nodoc

加了@Documented 注解的样子

annotation_doc

Annotation 定义

Annotation - 注解

注解类似被修饰对象的标签,可以在类(包括interface、enum)方法字段声明时使用。 你可以使用jdk 在 java.langjava.lang.annotation 中定义的 Annotation,也可以像前面一样自己定义 Annotation。 定义的语法如下:

@interface <NameOfAnnotation> {
   <TYPE> <FIELD_NAME>() [default <DEFAULT_VALUE>];
}

<TYPE> 可以是String, int, String[], Class<?> 等
<DEFAULT_VALUE> 给出字段的默认值

另外在 Java SE 8 中将扩展 Annotation 的使用范围,不但可以在声明时使用注解,而且可以在程序体中使用注解,这类注解叫 Type Annotation,例如

  • 创建实例时使用注解

      new @Interned MyObject();
    
  • 类型转换

      myString = (@NonNull String) str;
    
  • implements 子句

      class UnmodifiableList<T> implements @Readonly List<@Readonly T> {...}
    
  • 抛出异常时的声明

      void monitorTemperature() throws @Critical TemperatureException {...}
    

Meta Annotation - 元注解

Annotations that apply to other annotations are called meta-annotations.

也就是修饰注解的注解称为 元注解

Java SE 内置的 Annotation

JDK内置了三个预定义的Annotaion,分别是 @SuppressWarnings, @Override, @Deprecated。除此之外还有一类 元注解, 就是之前说的 修饰注解的注解,就比如之前的 @Documented 一样

三个预定义注解

  • @Deprecated, indicates that the marked element is deprecated and should no longer be used. (表示被标记的方法、类、字段不应当再使用了,可能会在未来的版本中删除)
  • @Override, informs the compiler that the element is meant to override an element declared in a superclass. (这个注解告诉编译器,被标记的对象将会覆盖父类中声明的相同的对象)
  • @SuppressWarnings, tells the compiler to suppress specific warnings that it would otherwise generate (这个注解告诉编译器,忽略被标记对象中可能存在的警告信息)

四个元注解(meta annotation)

  • @Target, marks another annotation to restrict what kind of Java elements the annotation can be applied to. (表示注解可以在那些类型上使用) 可取的值有:

    • ElementType.ANNOTATION_TYPE
    • ElementType.CONSTRUCTOR
    • ElementType.FIELD
    • ElementType.LOCAL_VARIABLE
    • ElementType.METHOD
    • ElementType.PACKAGE
    • ElementType.PARAMETER
    • ElementType.TYPE
  • @Retention, specifies how the marked annotation is stored. (表示注解可以存在于哪些阶段)可取的值有:

    • RetentionPolicy.SOURCE, 源码级的注解,将会被编译器忽略
    • RetentionPolicy.CLASS, 编译级注解,对编译器有用,但是会被JVM忽略
    • RetentionPolicy.RUNTIME,运行时级注解,只有这类注解才能在运行时通过反射获取到
  • @Documented, indicates that whenever the specified annotation is used those elements should be documented using the Javadoc tool. (告诉javadoc 是否需要在文档中生成)

  • @Inherited, indicates that the annotation type can be inherited from the super class. (This is not true by default.) (这个注解允许,当用户对当前 class 查询的注解不存在时,能够向该 class 的父类class查询该注解。另外,这个注解只能修饰 类)

  • @Repeatable, Java SE 8新增Annotatoin, 表明相同名称的注解可以出现超过1次

Annotation 行为的控制

Annotation 如果只是定义出来,放在 方法字段上来起个简单的标签作用,那其实功能就大大减弱了。因此在自定义注解时,通常会定义和自定义Annotation 相匹配的一组行为来进行对代码增强。

下面的代码演示通过定义 Bean,@Inject Annotation 来完成实例注入。例子中定义 EmailValidator 是对Email地址的检测工具, EmailService 是对外提供的Email名称检测服务。

/**
 * Bean Annotation.  被 @Bean 修饰的对象表示需要被容器加载
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Bean {

}

/**
 * @Inject 用来修饰字段,说明该字段需要由容器注入。
 * @Inject 提供了属性 target 用来指定要注入的 Class
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {
    Class<?> target();
}

接下来是 Email服务 的定义

/**
 * Email 实例,只提供 address字段
 */
public class Email {

    private String address;

    public Email(String address) {
        this.address = address;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    @Override
    public String toString() {
        return "Email{" +
                "address='" + address + '\'' +
                '}';
    }
}

/**
 * EmailValidator 提供对邮箱地址的检测功能
 */
@Bean
public class EmailValidator {

    /**
     * 使用正则表达式来检测邮箱地址是否合法
     */
    private Pattern emailPattern = Pattern.compile("[\\w!#$%&'*+/=?^_`{|}~-]+(?:\\.[\\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\\w](?:[\\w-]*[\\w])?\\.)+[\\w](?:[\\w-]*[\\w])?");

    public boolean validate(Email email) {
        if (email == null || email.getAddress() == null) {
            return false;
        }
        Matcher matcher = emailPattern.matcher(email.getAddress());
        if (matcher.find()) {
            return true;
        } else {
            return false;
        }
    }
}


/**
 * EmailService Bean,同样EmailService会由容器初始化并维护
 */
@Bean
public class EmailService {

    /**
     * 此处使用 @Inject Annotation,表明字段 emailValidator需要由容器注入
     */
    @Inject(target = EmailValidator.class)
    private EmailValidator emailValidator;

    public void setEmailValidator(EmailValidator emailValidator) {
        this.emailValidator = emailValidator;
    }

    public void validateEmail(Email email) {
        if(emailValidator.validate(email)) {
            System.out.println("Eamil valid");
        } else {
            System.out.println("Illegal Email Address Format");
        }
    }
}

这样 @Bean, @Inject 就定义完了,使用也很简单,只需要在需要的字段或方法上加上注解即可。但是只标记不做解析的话就只能当花瓶了,根本毫无作用。因此我们还得自己定义对注解的解析逻辑。

下面的代码 BeanContext 实现一个简单的容器功能,主要做两个工作:

  1. 负责搜索指定 Package 下被 @Bean 标记的类并将之实例化
  2. 对于已经实例化的类,遍历其字段,查找出含有 @Inject 的字段,并使用合适的Class 实例给字段注入值。

代码如下:

/**
 * BeanContext 充当一个简单Bean容器,负责查找 LOAD_PACKAGE 包下的类,加载
 * 被 @Bean 修饰的类,然后对含有 @Inject 的字段进行注入
 */
public class BeanContext {

    public static Map<Class<?>, Object> beanMap = new HashMap<Class<?>, Object>();
    /**
     * 加载类的起始包
     */
    public static final String LOAD_PACKAGE = "org.cgfalcon.annotation";

    static {

        List<Class<?>> clazzList = new ArrayList<Class<?>>();
        try {
            Enumeration<URL> urls = Thread.currentThread().getContextClassLoader().getResources(LOAD_PACKAGE.replace(".", "/"));
            while (urls.hasMoreElements()) {
                URL url = urls.nextElement();
                if (url != null) {
                    String protocol = url.getProtocol();
                    if (protocol.equals("file")) {
                        String cPath = url.getPath();
                        loadBeans(clazzList, cPath, LOAD_PACKAGE);
                    }
                }
            }

            /**
             * 将查找完的被 @Bean 修饰的类实例化放入 beanMap中
             */
            for (Class<?> clazz : clazzList) {
                Object instance = clazz.newInstance();
                beanMap.put(clazz, instance);
            }

            /*
             * Dependencies inject
             *
             */
            iocInject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    /**
     * 查找实例化好的实例,对其中需要 Inject 的字段注入合适的字段
     */
    private static void iocInject() {
        for (Map.Entry<Class<?>, Object> entry : beanMap.entrySet()) {
            Class<?> clazz = entry.getKey();
            Object instance = entry.getValue();
            Field[] fields = clazz.getDeclaredFields();
            if (fields != null && fields.length > 0) {
                for (Field eachField : fields) {
                    if (eachField.isAnnotationPresent(Inject.class)) {
                        /* 在我的定义中 @Inject 包含一个字段 target, 用来制定要注入的实力类型,
                         * 此处通过该类型来从 beanMap中查找相应的实例
                          * */
                        Class<?> injectClass = eachField.getAnnotation(Inject.class).target();
                        Object injectInstance = beanMap.get(injectClass);
                        if (injectInstance != null) {
                            try {
                                /*
                                 * 查找出来的实例注入instance 的指定字段中
                                 */
                                eachField.setAccessible(true);
                                eachField.set(instance, injectInstance);
                            } catch (IllegalAccessException e) {
                                e.printStackTrace();
                            }
                        }

                    }
                }
            }
        }
    }

    public static void loadBeans(List<Class<?>> clazzList, String packagePath, String loadPackage) {
        File packageDir = new File(packagePath);
        if (packageDir.isDirectory()) {
            getSubpackageClasses(clazzList, packageDir, loadPackage);
        }
    }

    /**
     * 查找 packageDir 下的class文件,将含有 @Bean 注解的类取出来
     *
     * @param clazzList
     * @param packageDir
     * @param loadPackage
     */
    private static void getSubpackageClasses(List<Class<?>> clazzList, File packageDir, String loadPackage) {
        File[] subfiles = packageDir.listFiles();
        for (File subFile : subfiles) {
            if (subFile.isFile() && subFile.getName().endsWith(".class")) {
                String fileName = subFile.getName();
                String className = loadPackage + "." + fileName.substring(0, fileName.indexOf("."));

                try {
                    Class<?> clazz = Class.forName(className);
                    if (clazz.isAnnotationPresent(Bean.class)) {

                        clazzList.add(clazz);
                    }
                } catch (ClassNotFoundException e) {
                    System.out.printf("Failed to load class: <%s>\n", className);
                }
            } else if (subFile.isDirectory()) {
                getSubpackageClasses(clazzList, subFile, loadPackage + "." + subFile.getName());
            }
        }
    }

    public static <T> T getBean(Class<T> clazz) {
        if (beanMap == null) {
            return null;
        }
        return (T) beanMap.get(clazz);
    }
}

然后来实际使用看看

public class Main {

    public static void main(String[] args) {
        EmailService emailService = BeanContext.getBean(EmailService.class);

        emailService.validateEmail(new Email("ddd"));
        emailService.validateEmail(new Email("hotmail@gmail.com"));
    }
}

输出结果为:

Email{address='ddd'} - illegal
Email{address='hotmail@gmail.com'} - valid

由上观之,自定义Annotation的使用通常需要经历: 自定义Annotation, 解析Annotation 两个步骤, 通常比较麻烦的地方就是在解析的步骤上。因此但凡说 Annotation,必定要涉及到使用的特定环境,假设脱离的语境,那么也就像花瓶一样,只能做摆设了。

博客链接: http://typeorigin.com

参考资料

  1. Oracle | The Java Tutorials | Lesson: Annotations
  2. InfoQ | Java深度历险(六)——Java注解
  • 打赏
  • 点赞
  • 收藏
  • 分享
共有 人打赏支持
在云端-看世间变幻
粉丝 21
博文 18
码字总数 12940
×
在云端-看世间变幻
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: