likes
comments
collection
share

asm扫描原理

作者站长头像
站长
· 阅读数 16

asm扫描原理

asm扫描原理

对于spring是如何扫描scan(包名),来注册到BeanDefinitionMap中的:

asm扫描原理

spring的做法:

asm扫描原理

asm扫描原理

asm扫描原理

asm扫描原理

asm扫描原理

asm扫描原理

asm扫描原理

for (Resource rootDirResource : rootDirResources) {
			rootDirResource = resolveRootDirResource(rootDirResource);
			URL rootDirUrl = rootDirResource.getURL();
			if (equinoxResolveMethod != null && rootDirUrl.getProtocol().startsWith("bundle")) {
				URL resolvedUrl = (URL) ReflectionUtils.invokeMethod(equinoxResolveMethod, null, rootDirUrl);
				if (resolvedUrl != null) {
					rootDirUrl = resolvedUrl;
				}
				rootDirResource = new UrlResource(rootDirUrl);
			}
            //判断当前的你传进来的路径是什么类型的?
			if (rootDirUrl.getProtocol().startsWith(ResourceUtils.URL_PROTOCOL_VFS)) {//VFS
				result.addAll(VfsResourceMatchingDelegate.findMatchingResources(rootDirUrl, subPattern, getPathMatcher()));
			}
			else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) {//jar
				result.addAll(doFindPathMatchingJarResources(rootDirResource, rootDirUrl, subPattern));
			}
			else {
				result.addAll(doFindPathMatchingFileResources(rootDirResource, subPattern));
			}
		}

asm扫描原理

asm扫描原理

asm扫描原理

通过上面一连串的代码来看,spring是通过Java中的流技术来把指定的包下的资源进行加载,但是我们回到真正扫描完成的方法

asm扫描原理

看到他获取的是一个Resource数组,这里很奇怪了为什么他存一个这样的数组,他直接通过反射得到一个class数组不好吗?

这里个人的看法是,由于很多类,其实会包含有些特殊的逻辑或者代码,比如静态代码块,这个静态代码块的是要该类调用过后才会被加载到JVM中的,并且下次不再被调用,生成新的实例,所以这里他这么做的道理是为不干扰用户行为。

asm扫描原理

再从这张图来看,他会把获取到resource数组转换为一个metadataReader,为什么?原因很简单,你上面都自己加载了一个resource了,那下面就不是普通的class了,所以需要通过spring定制的class的描述方式来描述当前的class。

下面来讲解spring使用ASM字节码编译技术:

private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
		//该candidates存放被扫描出来的beanDefinition
		Set<BeanDefinition> candidates = new LinkedHashSet<>();
		try {
			//拼接PackageUrl路径
			String packageSearchPath =
					ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
							resolveBasePackage(basePackage) +
							'/' +
							this.resourcePattern;
			//开始用spring自己使用的流工具完成文件的扫描,得到指定包中的文件
			Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
			boolean traceEnabled = logger.isTraceEnabled();
			boolean debugEnabled = logger.isDebugEnabled();
			for (Resource resource : resources) {
				if (traceEnabled) {
					logger.trace("Scanning " + resource);
				}
				try {
					//这里为什么spring不转换为class?
					/*原因:
					 * 1.由于有些类中是会存在静态代码块,对于用户而言这些静态代码块是按需加载的,当我不需要的时候我就不会去加载
					 * 2.那如果在这里spring使用class的形式来提前或者是自说自话的进行class加载,那是不是就间接的把class中static代码中的内容加载了
					 * 3.是不是对spring的使用者很不友好
					 * 4.那么当前,我们对于metadataReader就可以理解spring为了防止上述的情况所创建的"spring版本的class"
					 * 5.我们可以看到getMetadataReaderFactory()方法的返回对象是MetadataReaderFactory->SimpleMetadataReaderFactory-ASM->MetadataReader
					 */
					MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
					//判断这个类是否符合要求,扫描器有属于自己的规则,isCandidateComponent(metadataReader)这个方法就是判断当前的metadataReader是否符合spring对于scanner的要求
					/*
					 * 1.当前的被加载的metadataReader是否要被剔除
					 * 2.当前的metadataReader是否需要被转化
					 * 3.由于扫描器没有添加excludeFilters所以当前isCandidateComponent方法中根本不会去过滤这个集合中的元素
					 * 4.是否需要转化,扫描器添加了includeFilters 3个过滤规则
					 */
					//总结:一句话,spring得到包路径后,会去扫描,得到属于spring特有的Class,然后判断是否有3个过滤规则,有就转化为BeanDefinition

					// 由于mybatis在扩展spring的时候没有添任何的规则所以这里永远为true,
					// 那么作为mybatis作者的角度来看,其实他想说的是我认为只要你指定了包,
					// 那么这个包下的所有的类都是可以Mapper
					if (isCandidateComponent(metadataReader)) {
						ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
						sbd.setSource(resource);
						// 在默认为spring的情况下不会调用isCandidateComponent()方法进行判断当前类是否是一个正常类
						// 在mybatis扩展spring的情况下,会调用mybatis扩展重写子类,并且重写方法isCandidateComponent()方法
						// mybatis获取到当前的BeanDefinition的class进行判断是否为interface
						if (isCandidateComponent(sbd)) {
							if (debugEnabled) {
								logger.debug("Identified candidate component class: " + resource);
							}
							//如果是一个正常类就加入到candidates集合中
							candidates.add(sbd);
						} else {
							if (debugEnabled) {
								logger.debug("Ignored because not a concrete top-level class: " + resource);
							}
						}
					} else {
						if (traceEnabled) {
							logger.trace("Ignored because not matching any filter: " + resource);
						}
					}
				} catch (FileNotFoundException ex) {
					if (traceEnabled) {
						logger.trace("Ignored non-readable " + resource + ": " + ex.getMessage());
					}
				} catch (Throwable ex) {
					throw new BeanDefinitionStoreException(
							"Failed to read candidate component class: " + resource, ex);
				}
			}
		} catch (IOException ex) {
			throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
		}
		return candidates;
	}

可以看到在ClassPathScanningCandidateComponentProvider类中,把resource传入到getMetadataReader方法中构建出来一个metaDateReader对象。

MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);

我们点进getMetadataReaderFactory方法中发现他的返回类型是一个什么什么工厂,再通过,getMetadataReader方法构建出来了一个metaDateReader对象。

/**
 * Return the MetadataReaderFactory used by this component provider.
 * 返回此组件提供程序使用的元数据读取器工厂。
 */
public final MetadataReaderFactory getMetadataReaderFactory() {
    if (this.metadataReaderFactory == null) {
        this.metadataReaderFactory = new CachingMetadataReaderFactory();
    }
    return this.metadataReaderFactory;
}

再看后面的方法,我们想要点进去系统提示有两个实现,说明该方法是一个接口,但是这次我们进入到SimpleMetadataReaderFactory中。

asm扫描原理

@Override
public MetadataReader getMetadataReader(Resource resource) throws IOException {
    return new SimpleMetadataReader(resource, this.resourceLoader.getClassLoader());
}
SimpleMetadataReader(Resource resource, @Nullable ClassLoader classLoader) throws IOException {
    SimpleAnnotationMetadataReadingVisitor visitor = new SimpleAnnotationMetadataReadingVisitor(classLoader);
    getClassReader(resource).accept(visitor, PARSING_OPTIONS);
    this.resource = resource;
    this.annotationMetadata = visitor.getMetadata();
}

关键点来了spring先调用getMetadataReader()方法,然后实例化SimpleMetadataReader方法,这里我们可以看见第一行是new了一个SimpleAnnotationMetadataReadingVisitor()对象,传入一个类加载器。我们回忆一下设计模式中的访问者模式。然后再说一下市面上面的ASM框架很多,其实万变不离其宗都是基于最原始的ASM框架开发出来的,像JDK原生的ASM框架,所以Spring自己也有一套Spring的ASM框架。

下面我们来模仿spring将一个类通过ASM技术来将一个类解析为MetaDataClass的:

public class Application {
	//定义全局变量,指定ASM要处理那些类属性
	private static final int PARSING_OPTIONS = ClassReader.SKIP_DEBUG | ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES;

	public static void main(String[] args) throws IOException {
		//1.创建一个ASM解析器
		ClassParse4Asm parse = new ClassParse4Asm();
		//2.调用spring的类解析器来完成解析其实就是反射那套只不过spring进行了封装
		ClassReader classReader = new ClassReader("com.lukp.live.utils.ClassParse4ASm");
		//3.由于spring在类解析的时候是采用的访问者模式来进行解析的,所以这个方法就是接受一个classVisitor,并且你也可以看到ClassParse4Asm继承了classVisitor
		classReader.accept(parse, PARSING_OPTIONS);
		//4.将当前的类解析完成后返回当前我们自定义封装的类的信息
		MetaDataLuKP metaDataLuKP = parse.getMetaDataLuKP();
		System.out.println("我自定义生成完成的MetaDataLuKP:" + metaDataLuKP);
	}
}
/**
 * @User lukuangpeng
 * @Date 2023/8/9 22:09
 * 该类是模拟了spring的MetaDataReader
 **/
@Setter
@Getter
public class MetaDataLuKP {
	//该list存放着一个类的注解信息
	List<AnnotationDataLuKP> list = new ArrayList<>();
	//当前解释的类
	String className;
}
/**
 * @User lukuangpeng
 * @Date 2023/8/9 22:09
 * 该类用来存放注解相关的信息
 **/
@Builder
@Setter
@Getter
public class AnnotationDataLuKP {
	//注解的类型
	Class annotationType;
	//来着那个类
	String source;
	//注解的名称
	String name;
}
/**
 * @User lukuangpeng
 * @Date 2023/8/9 22:26
 * 模仿spring中对类中的注解的解析器
 **/
public class AnnotationVisitorParse4Asm extends AnnotationVisitor {

	List<MergedAnnotation> list;
	String sourceName;

	//在构造器中完成当前类信息的解析
	public AnnotationVisitorParse4Asm(String descriptor, List list, String sourceName) {
		super(SpringAsmInfo.ASM_VERSION);
		this.list = list;
		this.sourceName = sourceName;
		//使用spring提供的类信息解析器,来解析出当前的类的className是什么
		String className = Type.getType(descriptor).getClassName();
		try {
			//在通过反射得到类的信息
			Class<?> aClass = ClassUtils.forName(className, AnnotationVisitorParse4Asm.class.getClassLoader());
			//创建我们定义的ClassAnnotation描述类
			AnnotationDataLuKP.AnnotationDataLuKPBuilder annotationDataLuKP = AnnotationDataLuKP
					.builder()
					.annotationType(aClass)
					.source(sourceName);
			list.add(annotationDataLuKP);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

	@Override
	public void visit(String name, Object value) {
		super.visit(name, value);
	}

	@Override
	public void visitEnd() {
		super.visitEnd();
	}
}
**
 * @User lukuangpeng
 * @Date 2023/8/8 22:54
 * 模仿spring中对类的解析器
 **/
@Getter
@Setter
public class ClassParse4Asm extends ClassVisitor {

	private MetaDataLuKP metaDataLuKP;
	private String className;
	private List<AnnotationDataLuKP> list;

	public ClassParse4Asm() {
		super(SpringAsmInfo.ASM_VERSION);
	}

	public MetaDataLuKP getMetaDataLuKP() {
		return metaDataLuKP;
	}

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

	@Override
	public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
		//在该方法中只负责解析当前的类中有哪些注解,至于真正的当前类的解析其实是调用了AnnotationVisitor来完成的
		System.out.println("visitAnnotation:" + descriptor);
		return new AnnotationVisitorParse4Asm(descriptor, list, className);
	}

	@Override
	public void visitEnd() {
		//在该方法中完成对类的信息的封装-(MetaDataLuKP)
		metaDataLuKP = new MetaDataLuKP();
		metaDataLuKP.setClassName(className);
		metaDataLuKP.setList(list);
	}

}

下面我们回过头来看看spring是怎么做的,是如何对一个类进行解析的:

  1. 首先也还是进行指定包下的扫描使用的就是前面我们说过的scan()方法来扫描。
  2. 拼接好路径后,进行解析转换为一个类Resource。
  3. 当前肯定是一个集合存储着Resource,然后遍历的去加载这个集合。
  4. ok,现在开始解析第一个Resource,我们会调用spring提供的ASM解析能力,其实也就是一个方法getMetadataReader(),在他的构造器中放入当前的resource。
  5. 下面其实就用到观察者模式来完成对于一个类的spring形式的解析,比如我们上面的实现:
  6. 先解析一个类,然后定义描述一个类的字段信息,描述一个类的注解信息,描述一个类的type等等等的,都是spring对于一个类的描述。
  7. 然后通过一层一层的拨丝抽茧,完成spring对于类的信息的完成描述,最后返回一个metadataReader,提供给spring进行实例化Bean。
  8. Tips:spring在调用解析方法的时候会先创建一个CachingMetadataReaderFactory(),其实原因也很简单,看一个cache中有没有?有就直接拿来,没有就自己解析一个出来,并且put到缓存中,这个缓存其实是一个Map,并且是一个CurrentHashMap。

asm扫描原理

@Override
public MetadataReader getMetadataReader(Resource resource) throws IOException {
    if (this.metadataReaderCache instanceof ConcurrentMap) {
        // No synchronization necessary... 先去Cache中拿一下
        MetadataReader metadataReader = this.metadataReaderCache.get(resource);
        if (metadataReader == null) {
            //如果没有的话就会调用父类的getMetadataReader()方法去会将当前resource,传入getMetadataReader方法获取metadataReader
            metadataReader = super.getMetadataReader(resource);
            //并且put到Cache中
            this.metadataReaderCache.put(resource, metadataReader);
        }
        //不为空就直接返回
        return metadataReader;
    }
    else if (this.metadataReaderCache != null) {
        synchronized (this.metadataReaderCache) {
            MetadataReader metadataReader = this.metadataReaderCache.get(resource);
            if (metadataReader == null) {
                metadataReader = super.getMetadataReader(resource);
                this.metadataReaderCache.put(resource, metadataReader);
            }
            return metadataReader;
        }
    }
    else {
        return super.getMetadataReader(resource);
    }
}

我们走进父类的方法中:

@Override
public MetadataReader getMetadataReader(Resource resource) throws IOException {
    //如果在cache中没有就会调用到SimpleMetadataReader的解析器来解析
    return new SimpleMetadataReader(resource, this.resourceLoader.getClassLoader());
}
SimpleMetadataReader(Resource resource, @Nullable ClassLoader classLoader) throws IOException {
		//SimpleAnnotationMetadataReadingVisitor:该类用于将封装类解析之后的数据,
    	//我们可以看到下面有很多的Visitor其实就类似于我们自己简单实现了Annotation的解析
		SimpleAnnotationMetadataReadingVisitor visitor = new SimpleAnnotationMetadataReadingVisitor(classLoader);
		getClassReader(resource).accept(visitor, PARSING_OPTIONS);
		this.resource = resource;
		this.annotationMetadata = visitor.getMetadata();
	}
转载自:https://juejin.cn/post/7383100103001407498
评论
请登录