title: 01.实现ResourceResolver
tag: 笔记 手写SSM IOC
扫描指定包下的所有Class文件
IOC管理对象
我们知道IOC容器最主要的内容是管理对象的创建和依赖关系的维护,因此我们在实现IOC容器之前我们需要知道哪些类的对象是需要IOC容器管理的。我们学过spring知道将对象交由IOC容器管理通常有两种方式:
- 通过XML配置文件,使用< beans > 等标签将对象注册到IOC容器中进行管理。
这样的方式我们可以通过将XML文件传入ApplicationContext
核心类(也可以是BeanFactory)中,即可通过getBean方法拿到对应创建完成的类:
ApplicationContext ctx=new ClassPathXmlApplicationContext("applicationContext.xml");
Object bean = ctx.getBean("beanName");
- Java引入注解后,我们可以完全依赖注解来实现Bean的注册:
@Configuration
@ComponentScan("com.duan")
public class SpringConfig {
}
我们可以创建一个spring的配置类来配置扫描Bean的包,spring就会把包下带有特定注解的类加入到IOC容器中管理:
@Component("BookService")
public class BookServiceImpl implements BookService {
}
这样我们就可以在将spring
的配置类传入ApplicationContext
中,同样可以实现IOC
对bean
的管理。
目前注解开发是最常用的方式,因此我们选择注解来实现注册bean到容器,因此我们首先需要实现扫描包下的所有类文件。
扫描包下的类文件
Java
的类加载器ClassLoader
可以通过指定类名来加载指定的Class
文件,但不能根据包名来加载包下的所有Class
文件。
我们知道Class文件是Java源代码编译后的文件,经过maven
编译后存在target
目录中,这个路径我们叫做classpath
,也就是存放class文件的路径。因此我们需要的就是去classpath中去扫描符合扫描包路径的class文件。
例如,Classpath
中搜索的文件org/example/Hello.class
就符合包名org.example
,我们需要根据文件路径把它变为org.example.Hello
,就相当于获得了类名。因此,搜索Class变成了搜索文件。
实现ResourceResolver
Resource类
我们先定义一个Resource
类型表示文件:
public record Resource(String path, String name) {
}
ResourceResolver类
再定义一个ResourceResolver
来对传入的路径进行扫描。
public class ResourceResolver {
String basePackage;
public ResourceResolver(String basePackage) {
this.basePackage = basePackage;
}
//扫描Resourse
public <R> List<R> scan(Function<Resource, R> mapper) {
String basePackagePath = this.basePackage.replace(".", "/");
try {
List<R> collector = new ArrayList<>();
scan0(basePackagePath, collector, mapper);
return collector;
} catch (IOException e) {
throw new UncheckedIOException(e);
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
}
- 我们先将传入的包路径中的".“替换为”/"。
- 我们通过传入的
Function<Resource, R> mapper
函数式接口来对扫描到的Resource
进行映射。这样,ResourceResolver
只负责扫描并列出所有文件,由客户端决定是找出.class
文件,还是找出.properties
文件。 - 我们使用
collector
集合来收集扫描出的资源。
下面我们看scan0
函数的实现:
<R> void scan0(String basePackagePath,List<R> collector, Function<Resource, R> mapper)
throws IOException, URISyntaxException {
logger.info("scan path:{}" , basePackagePath);
//获取
Enumeration<URL> resources = getContextClassLoader().getResources(basePackagePath);
while (resources.hasMoreElements()){
URI uri = resources.nextElement().toURI();
//处理uri为字符串
String uriStr = removeTrailingSlash(uriToString(uri));
//拿到存放class文件的目录的路径
String classPath = uriStr.substring(0, uriStr.length() - basePackagePath.length());
//将“file:删除”
if (classPath.startsWith("file:")) {
classPath = classPath.substring(5);
}
//jar和file做不同处理
if (uriStr.startsWith("jar:")) {
scanFile(true, classPath, jarUriToPath(basePackagePath, uri), collector, mapper);
} else {
scanFile(false, classPath, Paths.get(uri), collector, mapper);
}
}
}
ClassLoader getContextClassLoader() {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
if(classLoader == null) classLoader = getClass().getClassLoader();
return classLoader;
}
- ClassLoader首先从
Thread.getContextClassLoader()
获取,如果获取不到,再从当前Class获取,因为Web应用的ClassLoader不是JVM提供的基于Classpath的ClassLoader,而是Servlet容器提供的ClassLoader,它不在默认的Classpath搜索,而是在/WEB-INF/classes
目录和/WEB-INF/lib
的所有jar包搜索,从Thread.getContextClassLoader()
可以获取到Servlet容器专属的ClassLoader; - 拿到
ClassLoader
后我们就可以通过ClassLoader
调用getResources(basePackagePath)
,就可以获取到存放basePackagePath
包Class
文件的URI
。 - 我们再将URI转换为字符串后将
basePackagePath
截去之后就可以得到classpath
。 - 接下来我们就可以根据文件是file或者jar来进行文件扫描
scanFile
。
下面是scanFile
方法的实现,该类用于遍历指定包root下的所有class文件:
<R> void scanFile(boolean isJar, String classPath, Path root, List<R> collector,
Function<Resource, R> mapper) throws IOException {
logger.info("classPath:{},root:{}", classPath, root);
//去除最后的斜杠
classPath = removeTrailingSlash(classPath);
//try-with-resource保证pathStream资源释放
try(Stream<Path> pathStream = Files.walk(root)) {
String finalClassPath = classPath;
pathStream.filter(Files::isRegularFile)
.forEach(file -> {
Resource resource;
//实例化Resource
if(isJar){
resource = new Resource(finalClassPath, removeTrailingSlash(file.toString()));
}else {
String path = file.toString();
String name = removeLeadingSlash(path.substring(finalClassPath.length()));
resource = new Resource("file:" + path, name);
}
logger.atDebug().log("found resource: {}", resource);
//通过传入的函数式表达式来传入想要获取的文件
R r = mapper.apply(resource);
if (r != null) {
collector.add(r);
}
});
}
}
Files.walk(root)
:会生成一个file的stream流,来对路径下的全部文件进行遍历。
测试
编写测试就对刚才编写的两个类的包进行扫描
public static void main(String[] args) {
ResourceResolver rr = new ResourceResolver("com.duan.framework.io");
List<String> classList = rr.scan(res -> {
String name = res.name(); // 资源名称"com/duan/framework/io/Resource.class"
if (name.endsWith(".class")) { // 如果以.class结尾
// 把"org/example/Hello.class"变为"org.example.Hello":
return name.substring(0, name.length() - 6).replace("/", ".").replace("\\", ".");
}
// 否则返回null表示不是有效的Class Name:
return null;
});
}
输出:
22:31:59.000 [main] INFO com.duan.framework.io.ResourceResolver -- scan path:com/duan/framework/io
22:31:59.004 [main] INFO com.duan.framework.io.ResourceResolver -- classPath:/E:/JAVACODE/Spring-Simple/target/classes/,root:E:\JAVACODE\Spring-Simple\target\classes\com\duan\framework\io
22:31:59.009 [main] DEBUG com.duan.framework.io.ResourceResolver -- found resource: Resource[path=file:E:\JAVACODE\Spring-Simple\target\classes\com\duan\framework\io\Resource.class, name=com\duan\framework\io\Resource.class]
22:31:59.022 [main] DEBUG com.duan.framework.io.ResourceResolver -- found resource: Resource[path=file:E:\JAVACODE\Spring-Simple\target\classes\com\duan\framework\io\ResourceResolver.class, name=com\duan\framework\io\ResourceResolver.class]
[com.duan.framework.io.Resource, com.duan.framework.io.ResourceResolver]
从日志的最后一行来看,我们确实获取到了该包下的类的包路径。这样我们就可以通过反射的forName
来获取该类的Class对象,就可以进行下面的操作了。
an\framework\io\ResourceResolver.class]
[com.duan.framework.io.Resource, com.duan.framework.io.ResourceResolver]
从日志的最后一行来看,我们确实获取到了该包下的类的包路径。这样我们就可以通过**反射**的`forName`来获取该类的Class对象,就可以进行下面的操作了。