註解的那點事兒

什麼是註解?


註解是JDK1.5引入的一個語法糖,它主要用來看成元數據,簡單的說就是用於解釋數據的數據。在Java中,類、方法、變量、參數、包均可以被註解。不少開源框架都使用了註解,例如SpringMyBatisJunit。咱們日常最多見的註解可能就是@Override了,該註解用來標識一個重寫的函數。html

註解的做用:java

  • 配置文件:替代xml等文本文件格式的配置文件。使用註解做爲配置文件能夠在代碼中實現動態配置,相比外部配置文件,註解的方式會減小不少文本量。但缺點也很明顯,更改配置須要對代碼進行從新編譯,沒法像外部配置文件同樣進行集中管理(因此如今基本都是外部配置文件+註解混合使用)。git

  • 數據的標記:註解能夠做爲一個標記(例如:被@Override標記的方法表明被重寫的方法)。程序員

  • 減小重複代碼:註解能夠減小重複且乏味的代碼。好比咱們定義一個@ValidateInt,而後經過反射來得到類中全部成員變量,只要是含有@ValidateInt註解的成員變量,咱們就能夠對其進行數據的規則校驗。github

定義一個註解很是簡單,只須要遵循如下的語法規則:oracle

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
public @interface ValidateInt {
    // 它們看起來像是定義一個函數,但其實這是註解中的屬性
    int maxLength();

    int minLength();

}複製代碼

咱們發現上面的代碼在定義註解時也使用了註解,這些註解被稱爲元註解。做用於註解上的註解稱爲元註解(元註解其實就是註解的元數據)Java中一共有如下元註解。app

  • @Target:用於描述註解的使用範圍(註解能夠用在什麼地方)。框架

    • ElementType.CONSTRUCTOR:構造器。ide

    • 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
原文連接:sylvanassun.github.io/2017/10/15/…
(轉載請務必保留本段聲明,而且保留超連接。)

註解的實現


註解其實只是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);
            }
        }
    }複製代碼

本節中實現的包掃描器源碼地址:gist.github.com/SylvanasSun…

相關文章
相關標籤/搜索