【spring如何扫描一个路径下被注解修饰的类】

文章目录

为什么需要用到扫描被注解修饰的类

我们利用Spring框架或者SpringBoot框架经常会自己写一些注解,来方便自己的业务使用,但是我们如何获取到被注解修饰的类或方法,是一个值得探讨的问题,下面我们会具体分析一些demo来给出路径下某些被注解修饰的类的方案。

demo1

一、核心方案:使用 ClassPathScanningCandidateComponentProvider

这是 Spring 官方提供的扫描类,用来:

1.按包扫描 class;

2.按注解过滤;

3.支持 classpath 路径;

4.返回每个匹配类的 BeanDefinition。

下面的case是我们获取org.apache.dubbo.springboot.demo下的被@Service修饰的类

java 复制代码
 public static void main(String[] args) {
                // 1️⃣ 创建扫描器
                ClassPathScanningCandidateComponentProvider scanner =
                        new ClassPathScanningCandidateComponentProvider(false); // false 表示不使用默认过滤器

                // 2️⃣ 添加注解过滤器(比如扫描 @Service 的类)
                scanner.addIncludeFilter(new AnnotationTypeFilter(org.springframework.stereotype.Service.class));
                // 可选:也可以加类型过滤器,比如所有实现 MyInterface 的类
                // scanner.addIncludeFilter(new AssignableTypeFilter(MyInterface.class));
                String name = SayController.class.getPackage().getName();
                System.out.println("name = " + name);
                // 3️⃣ 扫描指定包路径
                String basePackage = "org.apache.dubbo.springboot.demo";
                Set<BeanDefinition> candidates = scanner.findCandidateComponents(basePackage);
                // 4️⃣ 遍历结果
                for (BeanDefinition bd : candidates) {
                    String className = bd.getBeanClassName();
                    System.out.println("发现被 @Service 修饰的类: " + className);
                }

    }

输出的结果

ClassPathScanningCandidateComponentProvider

ClassPathScanningCandidateComponentProvider 底层执行的流程:

1.扫描PathMatchingResourcePatternResolver#getResources("classpath*:org/apache/dubbo/springboot/demo/**/*.class")

2.扫描出所有 .class 文件;

3.读取字节码(不加载类)→ MetadataReader;

4.判断类上是否包含指定注解;

5.符合条件则返回 ScannedGenericBeanDefinition。

demo2:

使用PathMatchingResourcePatternResolver.class来进行获取路径org.apache.dubbo.springboot.demo下被@RestController注解修饰的类

java 复制代码
public static void main(String[] args) throws IOException {
        String basePackage = "org.apache.dubbo.springboot.demo";
        String packageSearchPath = "classpath*:" + basePackage.replace('.', '/') + "/**/*.class";

        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        Resource[] resources = resolver.getResources(packageSearchPath);

        SimpleMetadataReaderFactory readerFactory = new SimpleMetadataReaderFactory();

        for (Resource resource : resources) {
            MetadataReader metadataReader = readerFactory.getMetadataReader(resource);
            AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();
            if (annotationMetadata.hasAnnotation(RestController.class.getName())) {
                Map<String, Object> annotationAttributes = annotationMetadata.getAnnotationAttributes(RestController.class.getName());
                System.out.println("发现 @RestController 类:" + metadataReader.getClassMetadata().getClassName());
                System.out.println("注解@RestController 类的属性:" + annotationAttributes);
            }
        }

    }

执行结果

PathMatchingResourcePatternResolver扫描文件

核心PathMatchingResourcePatternResolver.getResource()

java 复制代码
   public Resource[] getResources(String locationPattern) throws IOException {
   			//校验路径不为null
        Assert.notNull(locationPattern, "Location pattern must not be null");
        //判断路径的开头是否是classpath*:
        if (locationPattern.startsWith("classpath*:")) {
        			//AntPathMatcher.isPattern()方法是校验路径中是否含有*等字符,true执行findPathMatchingResources()方法,false执行findAllClassPathResources()方法
            return this.getPathMatcher().isPattern(locationPattern.substring("classpath*:".length())) ? this.findPathMatchingResources(locationPattern) : this.findAllClassPathResources(locationPattern.substring("classpath*:".length()));
        } else {
            int prefixEnd = locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 : locationPattern.indexOf(58) + 1;
            return this.getPathMatcher().isPattern(locationPattern.substring(prefixEnd)) ? this.findPathMatchingResources(locationPattern) : new Resource[]{this.getResourceLoader().getResource(locationPattern)};
        }
    }

findPathMatchingResources方法

java 复制代码
protected Resource[] findPathMatchingResources(String locationPattern) throws IOException {
				//获取我们的根路径
        String rootDirPath = this.determineRootDir(locationPattern);
        //将*后面的字段去掉
        String subPattern = locationPattern.substring(rootDirPath.length());
        //获取目录下的所有的文件 递归走getResource()
        Resource[] rootDirResources = this.getResources(rootDirPath);
        Set<Resource> result = new LinkedHashSet(16);

        for(Resource rootDirResource : rootDirResources) {
            rootDirResource = this.resolveRootDirResource(rootDirResource);
            URL rootDirUrl = rootDirResource.getURL();
            if (equinoxResolveMethod != null && rootDirUrl.getProtocol().startsWith("bundle")) {
                URL resolvedUrl = (URL)ReflectionUtils.invokeMethod(equinoxResolveMethod, (Object)null, new Object[]{rootDirUrl});
                if (resolvedUrl != null) {
                    rootDirUrl = resolvedUrl;
                }

                rootDirResource = new UrlResource(rootDirUrl);
            }

            if (rootDirUrl.getProtocol().startsWith("vfs")) {
                result.addAll(PathMatchingResourcePatternResolver.VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, this.getPathMatcher()));
            } else if (!ResourceUtils.isJarURL(rootDirUrl) && !this.isJarResource(rootDirResource)) {
                result.addAll(this.doFindPathMatchingFileResources(rootDirResource, subPattern));
            } else {
                result.addAll(this.doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern));
            }
        }

        if (logger.isTraceEnabled()) {
            logger.trace("Resolved location pattern [" + locationPattern + "] to resources " + result);
        }

        return (Resource[])result.toArray(new Resource[0]);
    }

findAllClassPathResources()方法

java 复制代码
    protected Resource[] findAllClassPathResources(String location) throws IOException {
        String path = location;
        if (location.startsWith("/")) {
            path = location.substring(1);
        }
				//核心点doFindAllClassPathResources()
        Set<Resource> result = this.doFindAllClassPathResources(path);
        if (logger.isTraceEnabled()) {
            logger.trace("Resolved classpath location [" + location + "] to resources " + result);
        }

        return (Resource[])result.toArray(new Resource[0]);
    }
   protected Set<Resource> doFindAllClassPathResources(String path) throws IOException {
   		//创建一个结果集合
		   Set<Resource> result = new LinkedHashSet(16);
		   //DefaultResourceLoade创建,为什么考虑用DefaultResourceLoader?
		   //JVM 会在以下路径中查找:当前 classpath 目录;所有依赖的 JAR 包;父类加载器的路径(如 AppClassLoader → ExtClassLoader)。这也是为什么即使 com/example/MyService.class 在不同 JAR 中都有,Spring 也能找到多个匹配。
		   ClassLoader cl = this.getClassLoader();
		   //获取路径下的所有的类
		   Enumeration<URL> resourceUrls = cl != null ? cl.getResources(path) : ClassLoader.getSystemResources(path);
		   //循环遍历获取到的resourceUrls
		   while(resourceUrls.hasMoreElements()) {
		       URL url = (URL)resourceUrls.nextElement();
		       result.add(this.convertClassLoaderURL(url));
		   }
		
		   if (!StringUtils.hasLength(path)) {
		       this.addAllClassLoaderJarRoots(cl, result);
		   }
		
		   return result;
}

PathMatchingResourcePatternResolver#doFindPathMatchingFileResources()方法

java 复制代码
    protected Set<Resource> doFindPathMatchingFileResources(Resource rootDirResource, String subPattern) throws IOException {
        File rootDir;
        try {
        			//获取根路径
            rootDir = rootDirResource.getFile().getAbsoluteFile();
        } catch (FileNotFoundException ex) {
            if (logger.isDebugEnabled()) {
                logger.debug("Cannot search for matching files underneath " + rootDirResource + " in the file system: " + ex.getMessage());
            }

            return Collections.emptySet();
        } catch (Exception ex) {
            if (logger.isInfoEnabled()) {
                logger.info("Failed to resolve " + rootDirResource + " in the file system: " + ex);
            }

            return Collections.emptySet();
        }
				//执行核心方法
        return this.doFindMatchingFileSystemResources(rootDir, subPattern);
    }

PathMatchingResourcePatternResource#rerrieveMatchingFiles()方法

java 复制代码
 protected Set<File> retrieveMatchingFiles(File rootDir, String pattern) throws IOException {
 				//校验路径是否存在
        if (!rootDir.exists()) {
            if (logger.isDebugEnabled()) {
                logger.debug("Skipping [" + rootDir.getAbsolutePath() + "] because it does not exist");
            }
					//	不存在直接返回
            return Collections.emptySet();
            //判断是否是目录
        } else if (!rootDir.isDirectory()) {
            if (logger.isInfoEnabled()) {
                logger.info("Skipping [" + rootDir.getAbsolutePath() + "] because it does not denote a directory");
            }
						//不是直接返回
            return Collections.emptySet();
         //判断目录是否可读
        } else if (!rootDir.canRead()) {
            if (logger.isInfoEnabled()) {
                logger.info("Skipping search for matching files underneath directory [" + rootDir.getAbsolutePath() + "] because the application is not allowed to read the directory");
            }
						//不可读直接返回
            return Collections.emptySet();
        } else {
        			//重新定义路径
            String fullPattern = StringUtils.replace(rootDir.getAbsolutePath(), File.separator, "/");
            if (!pattern.startsWith("/")) {
                fullPattern = fullPattern + "/";
            }

            fullPattern = fullPattern + StringUtils.replace(pattern, File.separator, "/");
            Set<File> result = new LinkedHashSet(8);
            this.doRetrieveMatchingFiles(fullPattern, rootDir, result);
            return result;
        }
    }

PathMatchingResourcePatternResource#doRetriveMatchingFiles()方法

java 复制代码
 protected void doRetrieveMatchingFiles(String fullPattern, File dir, Set<File> result) throws IOException {
        if (logger.isTraceEnabled()) {
            logger.trace("Searching directory [" + dir.getAbsolutePath() + "] for files matching pattern [" + fullPattern + "]");
        }
			//获取路径下的所有的文件,并进行循环遍历,将匹配到的文件放入集合中,如果是文件夹,继续循环遍历。
        for(File content : this.listDirectory(dir)) {
            String currPath = StringUtils.replace(content.getAbsolutePath(), File.separator, "/");
            if (content.isDirectory() && this.getPathMatcher().matchStart(fullPattern, currPath + "/")) {
                if (!content.canRead()) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Skipping subdirectory [" + dir.getAbsolutePath() + "] because the application is not allowed to read the directory");
                    }
                } else {
                		//递归继续循环遍历
                    this.doRetrieveMatchingFiles(fullPattern, content, result);
                }
            }
						//判断是否匹配
            if (this.getPathMatcher().match(fullPattern, currPath)) {
                result.add(content);
            }
        }

    }

解析二进制文件的类

java 复制代码
final class SimpleAnnotationMetadataReadingVisitor extends ClassVisitor {
    @Nullable
    private final ClassLoader classLoader;
    private String className = "";
    private int access;
    @Nullable
    private String superClassName;
    private String[] interfaceNames = new String[0];
    @Nullable
    private String enclosingClassName;
    private boolean independentInnerClass;
    private Set<String> memberClassNames = new LinkedHashSet(4);
    private List<MergedAnnotation<?>> annotations = new ArrayList();
    private List<SimpleMethodMetadata> annotatedMethods = new ArrayList();
    @Nullable
    private SimpleAnnotationMetadata metadata;
    @Nullable
    private Source source;

    SimpleAnnotationMetadataReadingVisitor(@Nullable ClassLoader classLoader) {
        super(17432576);
        this.classLoader = classLoader;
    }
		//获取类名称,接口名称,父类名称
    public void visit(int version, int access, String name, String signature, @Nullable String supername, String[] interfaces) {
        this.className = this.toClassName(name);
        this.access = access;
        if (supername != null && !this.isInterface(access)) {
            this.superClassName = this.toClassName(supername);
        }

        this.interfaceNames = new String[interfaces.length];

        for(int i = 0; i < interfaces.length; ++i) {
            this.interfaceNames[i] = this.toClassName(interfaces[i]);
        }

    }

    public void visitOuterClass(String owner, String name, String desc) {
        this.enclosingClassName = this.toClassName(owner);
    }

    public void visitInnerClass(String name, @Nullable String outerName, String innerName, int access) {
        if (outerName != null) {
            String className = this.toClassName(name);
            String outerClassName = this.toClassName(outerName);
            if (this.className.equals(className)) {
                this.enclosingClassName = outerClassName;
                this.independentInnerClass = (access & 8) != 0;
            } else if (this.className.equals(outerClassName)) {
                this.memberClassNames.add(className);
            }
        }

    }
		//获取类筑基期
    @Nullable
    public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
        ClassLoader var10000 = this.classLoader;
        Source var10001 = this.getSource();
        List var10004 = this.annotations;
        var10004.getClass();
        return MergedAnnotationReadingVisitor.get(var10000, var10001, descriptor, visible, var10004::add);
    }
		//获取方法名称
    @Nullable
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        if (this.isBridge(access)) {
            return null;
        } else {
            ClassLoader var10002 = this.classLoader;
            String var10003 = this.className;
            List var10007 = this.annotatedMethods;
            var10007.getClass();
            return new SimpleMethodMetadataReadingVisitor(var10002, var10003, access, name, descriptor, var10007::add);
        }
    }
		//汇总为 SimpleAnnotationMetadata
    public void visitEnd() {
        String[] memberClassNames = StringUtils.toStringArray(this.memberClassNames);
        MethodMetadata[] annotatedMethods = (MethodMetadata[])this.annotatedMethods.toArray(new MethodMetadata[0]);
        MergedAnnotations annotations = MergedAnnotations.of(this.annotations);
        this.metadata = new SimpleAnnotationMetadata(this.className, this.access, this.enclosingClassName, this.superClassName, this.independentInnerClass, this.interfaceNames, memberClassNames, annotatedMethods, annotations);
    }

    public SimpleAnnotationMetadata getMetadata() {
        Assert.state(this.metadata != null, "AnnotationMetadata not initialized");
        return this.metadata;
    }

    private Source getSource() {
        Source source = this.source;
        if (source == null) {
            source = new Source(this.className);
            this.source = source;
        }

        return source;
    }

    private String toClassName(String name) {
        return ClassUtils.convertResourcePathToClassName(name);
    }

    private boolean isBridge(int access) {
        return (access & 64) != 0;
    }

    private boolean isInterface(int access) {
        return (access & 512) != 0;
    }

    private static final class Source {
        private final String className;

        Source(String className) {
            this.className = className;
        }

        public int hashCode() {
            return this.className.hashCode();
        }

        public boolean equals(@Nullable Object obj) {
            if (this == obj) {
                return true;
            } else {
                return obj != null && this.getClass() == obj.getClass() ? this.className.equals(((Source)obj).className) : false;
            }
        }

        public String toString() {
            return this.className;
        }
    }
}

ASM 读取类注解信息解析

ASM 是一个字节码操作框架(来自 ObjectWeb),它可以在不加载类到 JVM 的情况下,直接解析 .class 文件的二进制结构。

类似于我们平常用 Class.forName("xxx") 是加载 + 解析。ASM 则是 只解析 class 文件内容,不加载类,直接读取文件的二进制流。

Spring 在启动时要扫描大量类(比如 @Component、@Configuration),如果每个类都用反射加载,会极慢且占内存。

于是 Spring 用 ASM 读取 .class 文件里的元数据(如类名、父类、接口、注解、方法名等),而不创建 Class 对象。

ASM获取类加载信息

java 复制代码
public static void main(String[] args) throws IOException {


        // 1 读取 class 文件(这里从 classpath 获取)
        InputStream in = SayController.class.getClassLoader()
                .getResourceAsStream("org/apache/dubbo/springboot/demo/service/SayService.class");

        // 2 创建 ASM 的 ClassReader
        ClassReader classReader = new ClassReader(in);

        // 3 调用 accept() 让 ASM 解析并回调自定义的 ClassVisitor
        classReader.accept(new ClassVisitor(Opcodes.ASM9) {

            @Override
            public void visit(int version, int access, String name,
                              String signature, String superName, String[] interfaces) {
                System.out.println("类名:" + name);
                System.out.println("父类:" + superName);
            }

            @Override
            public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
                System.out.println("类注解:" + descriptor);
                return super.visitAnnotation(descriptor, visible);
            }

            @Override
            public MethodVisitor visitMethod(int access, String name,
                                             String descriptor, String signature, String[] exceptions) {
                System.out.println("方法名:" + name + " | 描述符:" + descriptor);
                return new MethodVisitor(Opcodes.ASM9) {
                    @Override
                    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
                        System.out.println("  方法注解:" + desc);
                        return super.visitAnnotation(desc, visible);
                    }
                };
            }

            @Override
            public void visitEnd() {
                System.out.println("=== 解析结束 ===");
            }
        }, 0);

        in.close();
    }

Spring 中 ASM 的使用路径:

当 Spring 扫描包时(如 @ComponentScan("com.xxx")):

1.SimpleMetadataReader(Resource, ClassLoader) 打开 class 文件的输入流

2.new ClassReader(is) 解析 class 文件字节流

3.AnnotationMetadataReadingVisitor visitor = new AnnotationMetadataReadingVisitor(classLoader) 创建一个 ASM 的 Visitor

4.classReader.accept(visitor, 2) 让 ASM 开始扫描 class 文件并回调 visitor

5.AnnotationMetadataReadingVisitor 收集所有类、方法、注解等元数据

6.最终生成 AnnotationMetadata 对象,供 Spring 上层逻辑(如 BeanDefinitionScanner)使用

相关推荐
百锦再3 小时前
对前后端分离与前后端不分离(通常指服务端渲染)的架构进行全方位的对比分析
java·开发语言·python·架构·eclipse·php·maven
间彧3 小时前
Java双亲委派模型的具体实现原理是什么?
后端
间彧3 小时前
Java类的加载过程
后端
DokiDoki之父4 小时前
Spring—注解开发
java·后端·spring
提笔了无痕4 小时前
什么是Redis的缓存问题,以及如何解决
数据库·redis·后端·缓存·mybatis
浪里行舟4 小时前
国产OCR双雄对决?PaddleOCR-VL与DeepSeek-OCR全面解析
前端·后端
CodeCraft Studio4 小时前
【能源与流程工业案例】KBC借助TeeChart 打造工业级数据可视化平台
java·信息可视化·.net·能源·teechart·工业可视化·工业图表
lang201509284 小时前
Spring Boot缓存机制全解析
spring boot·后端·缓存
摇滚侠4 小时前
Spring Boot 3零基础教程,WEB 开发 默认页签图标 Favicon 笔记29
java·spring boot·笔记