注解的那点事儿

什么是注解?


注解是JDK1.5引入的一个语法糖,它主要用来当作元数据,简单的说就是用于解释数据的数据。在Java中,类、方法、变量、参数、包都可以被注解。很多开源框架都使用了注解,例如SpringMyBatisJunit。我们平常最常见的注解可能就是@Override了,该注解用来标识一个重写的函数。

注解的作用:

  • 配置文件:替代xml等文本文件格式的配置文件。使用注解作为配置文件可以在代码中实现动态配置,相比外部配置文件,注解的方式会减少很多文本量。但缺点也很明显,更改配置需要对代码进行重新编译,无法像外部配置文件一样进行集中管理(所以现在基本都是外部配置文件+注解混合使用)。

  • 数据的标记:注解可以作为一个标记(例如:被@Override标记的方法代表被重写的方法)。

  • 减少重复代码:注解可以减少重复且乏味的代码。比如我们定义一个@ValidateInt,然后通过反射来获得类中所有成员变量,只要是含有@ValidateInt注解的成员变量,我们就可以对其进行数据的规则校验。

定义一个注解非常简单,只需要遵循以下的语法规则:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
public @interface ValidateInt {
    // 它们看起来像是定义一个函数,但其实这是注解中的属性
    int maxLength();

    int minLength();

}

我们发现上面的代码在定义注解时也使用了注解,这些注解被称为元注解。作用于注解上的注解称为元注解(元注解其实就是注解的元数据)Java中一共有以下元注解。

  • @Target:用于描述注解的使用范围(注解可以用在什么地方)。

    • ElementType.CONSTRUCTOR:构造器。

    • ElementType.FIELD:成员变量。

    • ElementType.LOCAL_VARIABLE:局部变量。

    • ElementType.PACKAGE:包。

    • ElementType.PARAMETER:参数。

    • ElementType.METHOD:方法。

    • ElementType.TYPE:类、接口(包括注解类型) 或enum声明。

  • @Retention:注解的生命周期,用于表示该注解会在什么时期保留。

    • RetentionPolicy.RUNTIME:运行时保留,这样就可以通过反射获得了。

    • RetentionPolicy.CLASS:在class文件中保留。

    • RetentionPolicy.SOURCE:在源文件中保留。

  • @Documented:表示该注解会被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。

  • @Inherited:表示该注解是可被继承的(如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个annotation将被用于该class的子类)。

了解了这些基础知识之后,接着完成上述定义的@ValidateInt,我们定义一个Cat类然后在它的成员变量中使用@ValidateInt,并通过反射进行数据校验。

public class Cat {

    private String name;

    @ValidateInt(minLength = 0, maxLength = 10)
    private int age;

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public static void main(String[] args) throws IllegalAccessException {
        Cat cat = new Cat();
        cat.setName("楼楼");
        cat.setAge(11);

        Class<? extends Cat> clazz = cat.getClass();
        Field[] fields = clazz.getDeclaredFields();
        if (fields != null) {
            for (Field field : fields) {
                ValidateInt annotation = field.getDeclaredAnnotation(ValidateInt.class);
                if (annotation != null) {
                    field.setAccessible(true);
                    int value = field.getInt(cat);
                    if (value < annotation.minLength()) {
                        // ....
                    } else if (value > annotation.maxLength()) {
                        // ....
                    }
                }
            }
        }
    }

}

本文作者为:SylvanasSun(sylvanas.sun@gmail.com),首发于SylvanasSun's Blog
原文链接:https://sylvanassun.github.io/2017/10/15/2017-10-15-JavaAnnotation/
(转载请务必保留本段声明,并且保留超链接。)

注解的实现


注解其实只是Java的一颗语法糖(语法糖是一种方便程序员使用的语法规则,但它其实并没有表面上那么神奇的功能,只不过是由编译器帮程序员生成那些繁琐的代码)。在Java中这样的语法糖还有很多,例如enum、泛型、forEach等。

通过阅读JLS(Java Language Specification(当你想了解一个语言特性的实现时,最好的方法就是阅读官方规范)发现,注解是一个继承自java.lang.annotation.Annotation接口的特殊接口,原文如下:

An annotation type declaration specifies a new annotation type, a special kind of interface type. To distinguish an annotation type declaration from a normal interface declaration, the keyword interface is preceded by an at-sign (@).

Note that the at-sign (@) and the keyword interface are distinct tokens. It is possible to separate them with whitespace, but this is discouraged as a matter of style.

The rules for annotation modifiers on an annotation type declaration are specified in §9.7.4 and §9.7.5.

The Identifier in an annotation type declaration specifies the name of the annotation type.

It is a compile-time error if an annotation type has the same simple name as any of its enclosing classes or interfaces.

The direct superinterface of every annotation type is java.lang.annotation.Annotation.
package java.lang.annotation;

/**
 * The common interface extended by all annotation types.  Note that an
 * interface that manually extends this one does <i>not</i> define
 * an annotation type.  Also note that this interface does not itself
 * define an annotation type.
 *
 * More information about annotation types can be found in section 9.6 of
 * <cite>The Java&trade; Language Specification</cite>.
 *
 * The {@link java.lang.reflect.AnnotatedElement} interface discusses
 * compatibility concerns when evolving an annotation type from being
 * non-repeatable to being repeatable.
 *
 * @author  Josh Bloch
 * @since   1.5
 */
public interface Annotation {
    ...
}

我们将上节定义的@ValidateInt注解进行反编译来验证这个说法。

  Last modified Oct 14, 2017; size 479 bytes
  MD5 checksum 2d9dd2c169fe854db608c7950af3eca7
  Compiled from "ValidateInt.java"
public interface com.sun.annotation.ValidateInt extends java.lang.annotation.Annotation
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
Constant pool:
   #1 = Class              #18            // com/sun/annotation/ValidateInt
   #2 = Class              #19            // java/lang/Object
   #3 = Class              #20            // java/lang/annotation/Annotation
   #4 = Utf8               maxLength
   #5 = Utf8               ()I
   #6 = Utf8               minLength
   #7 = Utf8               SourceFile
   #8 = Utf8               ValidateInt.java
   #9 = Utf8               RuntimeVisibleAnnotations
  #10 = Utf8               Ljava/lang/annotation/Retention;
  #11 = Utf8               value
  #12 = Utf8               Ljava/lang/annotation/RetentionPolicy;
  #13 = Utf8               RUNTIME
  #14 = Utf8               Ljava/lang/annotation/Target;
  #15 = Utf8               Ljava/lang/annotation/ElementType;
  #16 = Utf8               FIELD
  #17 = Utf8               Ljava/lang/annotation/Documented;
  #18 = Utf8               com/sun/annotation/ValidateInt
  #19 = Utf8               java/lang/Object
  #20 = Utf8               java/lang/annotation/Annotation
{
  public abstract int maxLength();
    descriptor: ()I
    flags: ACC_PUBLIC, ACC_ABSTRACT

  public abstract int minLength();
    descriptor: ()I
    flags: ACC_PUBLIC, ACC_ABSTRACT
}
SourceFile: "ValidateInt.java"
RuntimeVisibleAnnotations:
  0: #10(#11=e#12.#13)
  1: #14(#11=[e#15.#16])
  2: #17()

public interface com.sun.annotation.ValidateInt extends java.lang.annotation.Annotation,很明显ValidateInt继承自java.lang.annotation.Annotation

那么,如果注解只是一个接口,又是如何实现对属性的设置呢?这是因为Java使用了动态代理对我们定义的注解接口生成了一个代理类,而对注解的属性设置其实都是在对这个代理类中的变量进行赋值。所以我们才能用反射获得注解中的各种属性。

为了证实注解其实是个动态代理对象,接下来我们使用CLHSDB(Command-Line HotSpot Debugger)来查看JVM的运行时数据。如果有童鞋不了解怎么使用的话,可以参考R大的文章借HSDB来探索HotSpot VM的运行时数据 - Script Ahead, Code Behind - ITeye博客

0x000000000257f538 com/sun/proxy/$Proxy1

注解的类型为com/sun/proxy/$Proxy1,这正是动态代理生成代理类的默认类型,com/sun/proxy为默认包名,$Proxy是默认的类名,1为自增的编号。

实践-包扫描器


我们在使用Spring的时候,只需要指定一个包名,框架就会去扫描该包下所有带有Spring中的注解的类。实现一个包扫描器很简单,主要思路如下:

  • 先将传入的包名通过类加载器获得项目内的路径。

  • 然后遍历并获得该路径下的所有class文件路径(需要处理为包名的格式)。

  • 得到了class文件的路径就可以使用反射生成Class对象并获得其中的各种信息了。

定义包扫描器接口:

public interface PackageScanner {

    List<Class<?>> scan(String packageName);

    List<Class<?>> scan(String packageName, ScannedClassHandler handler);

}

函数2需要传入一个ScannedClassHandler接口,该接口是我们定义的回调函数,用于在扫描所有类文件之后执行的处理操作。

@FunctionalInterface // 这个注解表示该接口为一个函数接口,用于支持Lambda表达式
public interface ScannedClassHandler {

    void execute(Class<?> clazz);

}

我想要包扫描器可以识别和支持不同的文件类型,定义一个枚举类ResourceType

public enum ResourceType {

    JAR("jar"),
    FILE("file"),
    CLASS_FILE("class"),
    INVALID("invalid");

    private String typeName;

    public String getTypeName() {
        return this.typeName;
    }

    private ResourceType(String typeName) {
        this.typeName = typeName;
    }

}

PathUtils是一个用来处理路径和包转换等操作的工具类:

public class PathUtils {

    private static final String FILE_SEPARATOR = System.getProperty("file.separator");

    private static final String CLASS_FILE_SUFFIX = ".class";

    private static final String JAR_PROTOCOL = "jar";

    private static final String FILE_PROTOCOL = "file";

    private PathUtils() {
    }
    
    // 去除后缀名
    public static String trimSuffix(String filename) {
        if (filename == null || "".equals(filename))
            return filename;

        int dotIndex = filename.lastIndexOf(".");
        if (-1 == dotIndex)
            return filename;
        return filename.substring(0, dotIndex);
    }

    public static String pathToPackage(String path) {
        if (path == null || "".equals(path))
            return path;

        if (path.startsWith(FILE_SEPARATOR))
            path = path.substring(1);
        return path.replace(FILE_SEPARATOR, ".");
    }

    public static String packageToPath(String packageName) {
        if (packageName == null || "".equals(packageName))
            return packageName;
        return packageName.replace(".", FILE_SEPARATOR);
    }

    /**
     * 根据URL的协议来判断资源类型
     */
    public static ResourceType getResourceType(URL url) {
        String protocol = url.getProtocol();
        switch (protocol) {
            case JAR_PROTOCOL:
                return ResourceType.JAR;
            case FILE_PROTOCOL:
                return ResourceType.FILE;
            default:
                return ResourceType.INVALID;
        }
    }

    public static boolean isClassFile(String path) {
        if (path == null || "".equals(path))
            return false;
        return path.endsWith(CLASS_FILE_SUFFIX);
    }

    /**
     * 抽取URL中的主要路径.
     * Example:
     * "file:/com/example/hello" to "/com/example/hello"
     * "jar:file:/com/example/hello.jar!/" to "/com/example/hello.jar"
     */
    public static String getUrlMainPath(URL url) throws UnsupportedEncodingException {
        if (url == null)
            return "";
        
        // 如果不使用URLDecoder解码的话,路径会出现中文乱码问题
        String filePath = URLDecoder.decode(url.getFile(), "utf-8");
        // if file is not the jar
        int pos = filePath.indexOf("!");
        if (-1 == pos)
            return filePath;

        return filePath.substring(5, pos);
    }

    public static String concat(Object... args) {
        if (args == null || args.length == 0)
            return "";

        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < args.length; i++)
            stringBuilder.append(args[i]);

        return stringBuilder.toString();
    }

}

定义了这些辅助类之后,就可以去实现包扫描器了。

public class SimplePackageScanner implements PackageScanner {

    protected String packageName;

    protected String packagePath;

    protected ClassLoader classLoader;

    private Logger logger;

    public SimplePackageScanner() {
        this.classLoader = Thread.currentThread().getContextClassLoader();
        this.logger = LoggerFactory.getLogger(SimplePackageScanner.class);
    }

    @Override
    public List<Class<?>> scan(String packageName) {
        return this.scan(packageName, null);
    }

    @Override
    public List<Class<?>> scan(String packageName, ScannedClassHandler handler) {
        this.initPackageNameAndPath(packageName);
        if (logger.isDebugEnabled())
            logger.debug("Start scanning package: {} ....", this.packageName);
        URL url = this.getResource(this.packagePath);
        if (url == null)
            return new ArrayList<>();
        return this.parseUrlThenScan(url, handler);
    }

    private void initPackageNameAndPath(String packageName) {
        this.packageName = packageName;
        this.packagePath = PathUtils.packageToPath(packageName);
    }
    
}   

函数getResource()会根据包名来通过类加载器获得当前项目下的URL对象,如果这个URL为空则直接返回一个空的ArrayList

    protected URL getResource(String packagePath) {
        URL url = this.classLoader.getResource(packagePath);
        if (url != null)
            logger.debug("Get resource: {} success!", packagePath);
        else
            logger.debug("Get resource: {} failed,end of scan.", packagePath);
        return url;
    }

函数parseUrlThenScan()会解析URL对象并进行扫描,最终返回一个类列表。

    protected List<Class<?>> parseUrlThenScan(URL url, ScannedClassHandler handler) {
        String urlPath = "";
        try {
            // 先提取出URL中的路径(不含协议名等信息)
            urlPath = PathUtils.getUrlMainPath(url);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            logger.debug("Get url path failed.");
        }

        // 判断URL的类型
        ResourceType type = PathUtils.getResourceType(url);
        List<Class<?>> classList = new ArrayList<>();

        try {
            switch (type) {
                case FILE:
                    classList = this.getClassListFromFile(urlPath, this.packageName);
                    break;
                case JAR:
                    classList = this.getClassListFromJar(urlPath);
                    break;
                default:
                    logger.debug("Unsupported file type.");
            }
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
            logger.debug("Get class list failed.");
        }

        // 执行回调函数
        this.invokeCallback(classList, handler);
        logger.debug("End of scan <{}>.", urlPath);
        return classList;
    }

函数getClassListFromFile()会扫描路径下的所有class文件,并拼接包名生成Class对象。

    protected List<Class<?>> getClassListFromFile(String path, String packageName) throws ClassNotFoundException {
        File file = new File(path);
        List<Class<?>> classList = new ArrayList<>();

        File[] listFiles = file.listFiles();
        if (listFiles != null) {
            for (File f : listFiles) {
                if (f.isDirectory()) {
                    // 如果是一个文件夹,则继续递归调用,注意传递的包名
                    List<Class<?>> list = getClassListFromFile(f.getAbsolutePath(),
                            PathUtils.concat(packageName, ".", f.getName()));
                    classList.addAll(list);
                } else if (PathUtils.isClassFile(f.getName())) {
                    // 我们不添加名字带有$的class文件,这些都是JVM动态生成的
                    String className = PathUtils.trimSuffix(f.getName());
                    if (-1 != className.lastIndexOf("$"))
                        continue;

                    String finalClassName = PathUtils.concat(packageName, ".", className);
                    classList.add(Class.forName(finalClassName));
                }
            }
        }

        return classList;
    }

函数getClassListFromJar()会扫描Jar中的class文件。

    protected List<Class<?>> getClassListFromJar(String jarPath) throws IOException, ClassNotFoundException {
        if (logger.isDebugEnabled())
            logger.debug("Start scanning jar: {}", jarPath);

        JarInputStream jarInputStream = new JarInputStream(new FileInputStream(jarPath));
        JarEntry jarEntry = jarInputStream.getNextJarEntry();
        List<Class<?>> classList = new ArrayList<>();

        while (jarEntry != null) {
            String name = jarEntry.getName();
            if (name.startsWith(this.packageName) && PathUtils.isClassFile(name))
                classList.add(Class.forName(name));
            jarEntry = jarInputStream.getNextJarEntry();
        }

        return classList;
    }

函数invokeCallback()遍历类对象列表,然后执行回调函数。

    protected void invokeCallback(List<Class<?>> classList, ScannedClassHandler handler) {
        if (classList != null && handler != null) {
            for (Class<?> clazz : classList) {
                handler.execute(clazz);
            }
        }
    }

本节中实现的包扫描器源码地址:https://gist.github.com/SylvanasSun/6ab31dcfd9670f29a46917decdba36d1

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

推荐阅读更多精彩内容

  • 什么是注解(Annotation):Annotation(注解)就是Java提供了一种元程序中的元素关联任何信息和...
    九尾喵的薛定谔阅读 3,080评论 0 2
  • 本文章涉及代码已放到github上annotation-study 1.Annotation为何而来 What:A...
    zlcook阅读 28,934评论 15 116
  • 一、什么是注解? 注解对于开发人员来讲既熟悉又陌生,熟悉是因为只要你是做开发,都会用到注解(常见的@Overrid...
    _Justin阅读 1,321评论 0 10
  • 整体Retrofit内容如下: 1、Retrofit解析1之前哨站——理解RESTful 2、Retrofit解析...
    隔壁老李头阅读 6,209评论 4 31
  • 2017年11月3日红源悟语今日成长对自己对他人有帮助的事持续做下去的结果是为了成为自身良好习惯!今日感悟美好的事...
    红源随笔阅读 147评论 0 0