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 findClassAnnotations(String scanPackages, Class<? extends Annotation> annotation) throws ClassNotFoundException {
        //获取所有的类
        Set clazzSet = findPackageClass(scanPackages);
        Set methods = new HashSet<>();
        //遍历类,查询相应的annotation方法
        for (String clazz : clazzSet) {
            Set 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 findClassAnnotationMethods(String scanPackages, Class<? extends Annotation> annotation) {
        //获取所有的类
        Set clazzSet = findPackageClass(scanPackages);
        Set methods = new HashSet<>();
        //遍历类,查询相应的annotation方法
        for (String clazz : clazzSet) {
            try {
                Set ms = findAnnotationMethods(clazz, annotation);
                if (ms != null) {
                    methods.addAll(ms);
                }
            } catch (ClassNotFoundException ignore) {
            }
        }
        return methods;
    }

    /**
     * 根据扫描包的,查询下面的所有类
     *
     * @param scanPackages 扫描的package路径
     * @return
     */
    public static Set findPackageClass(String scanPackages) {
        if (StringUtils.isEmptyOrNull(scanPackages)) {
            return Collections.EMPTY_SET;
        }
        //验证及排重包路径,避免父子路径多次扫描
        Set packages = checkPackage(scanPackages);
        ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
        MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resourcePatternResolver);
        Set clazzSet = new HashSet();
        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 checkPackage(String scanPackages) {
        if (StringUtils.isEmptyOrNull(scanPackages)) {
            return Collections.EMPTY_SET;
        }
        Set 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 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 findAnnotationMethods(String fullClassName, Class<? extends Annotation> anno) throws ClassNotFoundException {
        Set 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 findAnnotations(String fullClassName, Class<? extends Annotation> anno) throws ClassNotFoundException {
        Set 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 classAnnotationMethods = findClassAnnotations(packages, BizPermission.class);
        classAnnotationMethods.forEach(set->{
            BizPermission annotation = (BizPermission)set;
            System.out.println(annotation.code()+" "+annotation.name());
        });
    }

}

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

  • 版本2 classgraph

需要引入classgraph maven依赖

        
            io.github.classgraph
            classgraph
            4.8.132
        

查询方法注解

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 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