sayi.github.com icon indicating copy to clipboard operation
sayi.github.com copied to clipboard

运行时扫描Java注解(一)之探讨

Open Sayi opened this issue 8 years ago • 0 comments

本文参考英文原文Scanning Java Annotations at Runtime进行了转述,但是也并非完全一字一句的翻译过来。

当你在开发一个基于注解的框架时,你肯定需要扫描所有使用了特定注解的类,从而初始化你的框架。比如JPA,你需要找到一系列使用了@Entity注解的类,从而定义ORM映射。可以使用很多技术在运行时扫描Java注解,本文将对此作探讨。

指定扫描目录

我们需要找到从哪个目录开始扫描,很多框架可以让用户指定目录,比如SpringMVC,通过以下配置(base-package)指定扫描的包名。

<context:annotation-config />
<context:component-scan base-package="com.xxx.xx" />

根据开发环境,我们可以有不同的方法获取扫描目录。

1.Java classpath

通过系统属性java.class.path指定扫描目录。

2.classloader

通过ClassLoader.getResource()ClassLoader.getResources()获取指定资源,从而解析出扫描目录。

ClassLoader cl = Thread.currentThread().getContextClassLoader();
Enumeration<URL> urls = cl.getResources("com/deepoove/dubbo");

3.Web Applications

对于Web应用,扫描目录在WEB-INF/lib或者WEB-INF/classes下,我们可以通过ServletContext 类获取lib下的资源:

List<URL> urls = new ArrayList<URL>();

Set libJars = servletContext.getResourcePaths("/WEB-INF/lib");
for (Object jar : libJars){
    try{
       urls.add(servletContext.getResource((String) jar));
    }
    catch (MalformedURLException e){
       throw new RuntimeException(e);
    }
}

获取classes下的资源会额外采取一点小措施:

URL classesPath = null;
Set libJars = servletContext.getResourcePaths("/WEB-INF/classes");
for (Object jar : libJars){
   try{
      URL url = servletContext.getResource((String) jar);
      String urlString = url.toString();
      int index = urlString.lastIndexOf("/WEB-INF/classes/");
      urlString = urlString.substring(0, index + "/WEB-INF/classes/".length());
      classesPath = new URL(urlString);       
      //do somethings   
    }catch (MalformedURLException e){
      throw new RuntimeException(e);
   }
}

至于如何获取到servletContext,在web框架中,可以通过监听器ServletContextListener 获取。

开始扫描目录

遍历目录是简单的,值得一提的是我们可以通过JarInputStream扫描jar文件。

找到指定注解的类

至此,我们已经可以获得所有扫描的类路径了,通过ClassLoader加载这些类,然后通过Java反射API去扫描特定注解是一个糟糕的方法:

  1. Java反射API只能扫描运行时可见的注解,注解类型有Source、Class、Runtime,仅仅只有Runtime类型的注解才能被反射API获取到。
  2. 通常你不会使用每一个你扫描的类,所以通过ClassLoader加载这些类,将填充JVM永久代且浪费资源。

所以,如果不通过ClassLoader去加载这些类,那么我们又如何去扫描特定注解呢??? 答案是通过字节码操作库 Javassist或者ASM。

在Javassist中,你可以获取InputStream从而实例化ClassFile类,ClassFile对象无需加载这个类就可以获取到特定注解。

DataInputStream dstream = new DataInputStream(new BufferedInputStream(bits));

ClassFile cf =  new ClassFile(dstream);
String className = cf.getName();
AnnotationsAttribute visible = (AnnotationsAttribute) cf.getAttribute(AnnotationsAttribute.visibleTag);
AnnotationsAttribute invisible = (AnnotationsAttribute) cf.getAttribute(AnnotationsAttribute.invisibleTag);
for (javassist.bytecode.Annotation ann : visible.getAnnotations()){
     System.out.println("@" + ann.getTypeName());
}

visible对象对应Runtime类型的注解,invisible对象对象Class类型的注解。Javassist同样提供了遍历方法和属性的方法,获取方法和属性上的注解也是同理。

反射框架

Reflections,一个java 运行时的元数据解析框架,下一篇文章,我们将从Reflections源码层面探讨更多关于反射获取注解的议题。

Sayi avatar Sep 20 '17 09:09 Sayi