java 注解扫描

最近要实现一个项目启动时进行注解扫描的功能, 用于实现方法的动态加载. 实际实现版本有两个版本, 第一个版本是直接百度的现成工具类, 可以基本实现功能, 但是实现的效率和安全性都存在未知性, 所以改进了第二个版本, 通过类库: classgraph 来实现.

  • 版本 1 自定义工具类
package a.custom.utils;

import a.custom.annotation.BizPermission;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.SystemPropertyUtils;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

/**

  • @author 123

  • @Description

  • @create 2021/11/3 11:12
    */
    @Component
    public class PackageUtils {

    private final static Log log = LogFactory.getLog(PackageUtils.class);
    // 扫描 scanPackages 下的文件的匹配符
    protected static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";

    /**

    • 查询指定注解信息
    • @param scanPackages
    • @param annotation
    • @return
    • @throws ClassNotFoundException
      */
      public static Set<Annotation> findClassAnnotations(String scanPackages, Class<? extends Annotation> annotation) throws ClassNotFoundException {
      // 获取所有的类
      Set<String> clazzSet = findPackageClass(scanPackages);
      Set<Annotation> methods = new HashSet<>();
      // 遍历类,查询相应的 annotation 方法
      for (String clazz : clazzSet) {
      Set<Annotation> ms = findAnnotations(clazz, annotation);
      if (ms != null) {
      methods.addAll(ms);
      }
      }
      return methods;
      }

    /**

    • 结合 spring 的类扫描方式
    • 根据需要扫描的包路径及相应的注解,获取最终测 method 集合
    • 仅返回 public 方法,如果方法是非 public 类型的,不会被返回
    • 可以扫描工程下的 class 文件及 jar 中的 class 文件
    • @param scanPackages
    • @param annotation
    • @return
      */
      public static Set<Method> findClassAnnotationMethods(String scanPackages, Class<? extends Annotation> annotation) {
      // 获取所有的类
      Set<String> clazzSet = findPackageClass(scanPackages);
      Set<Method> methods = new HashSet<>();
      // 遍历类,查询相应的 annotation 方法
      for (String clazz : clazzSet) {
      try {
      Set<Method> ms = findAnnotationMethods(clazz, annotation);
      if (ms != null) {
      methods.addAll(ms);
      }
      } catch (ClassNotFoundException ignore) {
      }
      }
      return methods;
      }

    /**

    • 根据扫描包的, 查询下面的所有类

    • @param scanPackages 扫描的 package 路径

    • @return
      */
      public static Set<String> findPackageClass(String scanPackages) {
      if (StringUtils.isEmptyOrNull(scanPackages)) {
      return Collections.EMPTY_SET;
      }
      // 验证及排重包路径, 避免父子路径多次扫描
      Set<String> packages = checkPackage(scanPackages);
      ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
      MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
      Set<String> clazzSet = new HashSet<String>();
      for (String basePackage : packages) {
      if (StringUtils.isEmptyOrNull(basePackage)) {
      continue;
      }
      String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
      org.springframework.util.ClassUtils.convertClassNameToResourcePath(SystemPropertyUtils.resolvePlaceholders(basePackage)) + "/" + DEFAULT_RESOURCE_PATTERN;
      try {
      Resource[] resources = resourcePatternResolver.getResources(packageSearchPath);
      for (Resource resource : resources) {
      // 检查 resource,这里的 resource 都是 class
      String clazz = loadClassName(metadataReaderFactory, resource);
      clazzSet.add(clazz);
      }
      } catch (Exception e) {
      log.error("获取包下面的类信息失败,package:" + basePackage, e);
      }

      }
      return clazzSet;
      }

    /**

    • 排重、检测 package 父子关系,避免多次扫描
    • @param scanPackages
    • @return 返回检查后有效的路径集合
      */
      private static Set<String> checkPackage(String scanPackages) {
      if (StringUtils.isEmptyOrNull(scanPackages)) {
      return Collections.EMPTY_SET;
      }
      Set<String> packages = new HashSet<>();
      // 排重路径
      Collections.addAll(packages, scanPackages.split(","));
      String[] strings = packages.toArray(new String[packages.size()]);
      for (String pInArr : strings) {
      if (StringUtils.isEmptyOrNull(pInArr) || pInArr.equals(".") || pInArr.startsWith(".")) {
      continue;
      }
      if (pInArr.endsWith(".")) {
      pInArr = pInArr.substring(0, pInArr.length() - 1);
      }
      Iterator<String> packageIte = packages.iterator();
      boolean needAdd = true;
      while (packageIte.hasNext()) {
      String pack = packageIte.next();
      if (pInArr.startsWith(pack + ".")) {
      // 如果待加入的路径是已经加入的 pack 的子集,不加入
      needAdd = false;
      } else if (pack.startsWith(pInArr + ".")) {
      // 如果待加入的路径是已经加入的 pack 的父集,删除已加入的 pack
      packageIte.remove();
      }
      }
      if (needAdd) {
      packages.add(pInArr);
      }
      }
      return packages;
      }

    /**

    • 加载资源,根据 resource 获取 className
    • @param metadataReaderFactory spring 中用来读取 resource 为 class 的工具
    • @param resource 这里的资源就是一个 Class
    • @throws IOException
      */
      private static String loadClassName(MetadataReaderFactory metadataReaderFactory, Resource resource) {
      try {
      if (resource.isReadable()) {
      MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(resource);
      if (metadataReader != null) {
      return metadataReader.getClassMetadata().getClassName();
      }
      }
      } catch (Exception e) {
      log.error("根据 resource 获取类名称失败", e);
      }
      return null;
      }

    /**

    • 把 action 下面的所有 method 遍历一次,标记他们是否需要进行敏感词验证
    • 如果需要,放入 cache 中
    • @param fullClassName
      */
      public static Set<Method> findAnnotationMethods(String fullClassName, Class<? extends Annotation> anno) throws ClassNotFoundException {
      Set<Method> methodSet = new HashSet<>();
      Class<?> clz = Class.forName(fullClassName);
      Method[] methods = clz.getDeclaredMethods();
      for (Method method : methods) {
      if (method.getModifiers() != Modifier.PUBLIC) {
      continue;
      }
      Annotation annotation = method.getAnnotation(anno);
      if (annotation != null) {
      methodSet.add(method);
      }
      }
      return methodSet;
      }

    /**

    • 查询指定注解信息
    • @param fullClassName
    • @param anno
    • @return
    • @throws ClassNotFoundException
      */
      public static Set<Annotation> findAnnotations(String fullClassName, Class<? extends Annotation> anno) throws ClassNotFoundException {
      Set<Annotation> methodSet = new HashSet<>();
      Class<?> clz = Class.forName(fullClassName);
      Method[] methods = clz.getDeclaredMethods();
      for (Method method : methods) {
      if (method.getModifiers() != Modifier.PUBLIC) {
      continue;
      }
      Annotation annotation = method.getAnnotation(anno);
      if (annotation != null) {
      if(methodSet.contains(annotation)){
      log.error("注解不存在");
      }
      methodSet.add(annotation);
      }
      }
      return methodSet;
      }

    public static void main(String[] args) throws ClassNotFoundException {
    String packages = "scan.package";
    Set<Annotation> classAnnotationMethods = findClassAnnotations(packages, BizPermission.class);
    classAnnotationMethods.forEach(set->{
    BizPermission annotation = (BizPermission)set;
    System.out.println(annotation.code()+" "+annotation.name());
    });
    }

}

该版本功能上只提供了方法注解的查询, 类注解的需要自己再完善; 优点是原生实现, 不需要额外的包依赖

  • 版本 2 classgraph

需要引入 classgraph maven 依赖

        <dependency>
            <groupId>io.github.classgraph</groupId>
            <artifactId>classgraph</artifactId>
            <version>4.8.132</version>
        </dependency>

查询方法注解

package a.custom.utils;

import io.github.classgraph.AnnotationParameterValueList;
import io.github.classgraph.ClassGraph;
import io.github.classgraph.ClassInfoList;
import io.github.classgraph.ScanResult;

import java.lang.annotation.Annotation;
import java.util.List;
import java.util.stream.Collectors;

/**

  • @author 123

  • @Description 类工具

  • @create 2021/11/18 9:56
    */
    public class ClassUtils {

    /**

    • 扫描指定方法注解
    • @param pkg 扫描包
    • @param annotation 获取的注解类型
    • @return 返回注解参数 [{name:name,value:value}]
      */
      public static List<AnnotationParameterValueList> methodAnnotionScan(String pkg, Annotation annotation) {
      try (ScanResult scanResult = // Assign scanResult in try-with-resources
      new ClassGraph() // Create a new ClassGraph instance
      .enableAllInfo() // Scan classes, methods, fields, annotations
      .acceptPackages(pkg) // Scan com.xyz and subpackages
      .scan()) { // Perform the scan and return a ScanResult
      // 获取类里指定方法注解
      ClassInfoList ciList = scanResult.getClassesWithMethodAnnotation(annotation.getClass());
      // 指定方法注解内容提取, 提取流程: ClassInfoList -> ClassInfo -> MethodInfo -> AnnotationInfo -> ParameterValues -> AnnotationParameterValue
      return ciList.stream().flatMap(ci->ci.getMethodInfo().stream().filter(me->me.getAnnotationInfo(annotation.getClass())!=null)
      .map(me->me.getAnnotationInfo(annotation.getClass()).getParameterValues())).collect(Collectors.toList());
      }
      }
      }

classgraph 是一个基于 jvm 语言进行类路径和包扫描的开源工具包. 基于 jvm 语言, 它拥有基于分析或响应其他代码属性而编写代码的能力. 拥有了更灵活的扩展性.

根据类的层级关系, 它的数据提取层级如下:

ClassInfoList -> ClassInfo -> MethodInfo -> AnnotationInfo -> ParameterValues -> AnnotationParameterValue

  • 常用的反射工具类库
Reflections
Corn Classpath Scanner
annotation-detector
Scannotation
Sclasner
Annovention
ClassIndex (compiletime annotation scanner/processor)
Jandex (Java annotation indexer, part of Wildfly)
Spring has built-in classpath scanning
Hibernate has the class org.hibernate.ejb.packaging.Scanner.
extcos -- the Extended Component Scanner
Javassist
ObjectWeb ASM
QDox, a fast Java source parser and indexer
bndtools, which is able to "crawl"/parse the bytecode of class files to find all imports/dependencies, among other things.
coffea, a command line tool and Python library for analyzing static dependences in Java bytecode
org.clapper.classutil.ClassFinder
com.google.common.reflect.ClassPath
jdependency
Burningwave Core
  • 参考资料:

classgraph