zoukankan      html  css  js  c++  java
  • 不会吧,有人用了两年Spring, 居然不知道包扫描是怎么实现的

    全栈的自我修养: 0004 Java 包扫描实现和应用(File篇)

    I may not be able to change the past, but I can learn from it.

    我也许不能改变过去发生的事情,但能向过去学习。

    Table of Contents

    如果你曾经使用过 Spring, 那你已经配过 包扫描路径吧,那包扫描是怎么实现的呢?让我们自己写个包扫描

    用途

    基于Java 的反射机制,我们很容易根据 class 去创建一个实例对象,但如果我们根本不知道某个包下有多少对象时,我们应该怎么做呢?

    在使用Spring框架时,会根据包扫描路径来找到所有的 class, 并将其实例化后存入容器中。

    在我们的项目中也会遇到这样的场景,比如某个包为 org.example.plugins, 这个里面放着所有的插件,为了不每次增减插件都要手动修改代码,我们可能会想到用扫描的方式去动态获知 org.example.plugins 到底有多少 class, 当然应用场景很有很多

    思路

    在一开始的我们为了上传文件和下载文件这种需求,请求会在程序运行的时候去获取当前项目运行的父路径是什么,比如下面的代码 使用Class类的getResource("").getPath()获取当前.class文件所在的路径, 或者使用 File 来实现

    //实例化一个File对象。参数不同时,获取的最终结果也不同, 这里可以将 path 替换为要扫描的包路劲 例如 org/example
    String path = "";
    
    File directory = new File(path); 
    //获取标准路径。该方法需要放置在try/catch块中,或声明抛出异常
    directory.getCanonicalPath();
    //获取绝对路径
    directory.getAbsolutePath();
    

    其中传入指定路径

    Enumeration<URL> resources = Thread.currentThread().getContextClassLoader().getResources("org/example");
    
    while (resources.hasMoreElements()) {
      URL url = resources.nextElement();
      System.out.println(url.toString());
    }
    

    输出为

    file:/Users/zhangyunan/project/spring-demo/java8-demo/target/test-classes/org/example
    file:/Users/zhangyunan/project/spring-demo/java8-demo/target/classes/org/example
    
    

    一些小功能

    通过上面的代码,我们可以大概知道使用 File 遍历方式可以简单实现一部分包扫描,那我们定义个扫描器应该有的功能和特定吧

    1. 可以根据指定的包进行扫描
    2. 可以排除一些类或者包名
    3. 可以过滤一些包或者类

    关于过滤可以使用 Java8Predicate 来实现,

    简要设计

    /**
     * class 扫描器
     * 
     * @author zhangyunan
     */
    public class ClassScanner {
    
    
      /**
       * Instantiates a new Class scanner.
       *
       * @param basePackage      the base package
       * @param recursive        是否递归扫描
       * @param packagePredicate the package predicate
       * @param classPredicate   the class predicate
       */
      public ClassScanner(String basePackage, boolean recursive, Predicate<String> packagePredicate, Predicate<Class> classPredicate) {
    
      }
    
      /**
       * Do scan all classes set.
       *
       * @return the set
       */
      public Set<Class<?>> doScanAllClasses() {
        return null;
      }
    }
    

    具体实现

    1. 将包路径转换为文件路径

    当我们要扫描一个 org.example 包时,首先将其转换为文件格式 org/example, 来使用File 遍历方式

    String basePackage = "org.example";
    // 如果最后一个字符是“.”,则去掉
    if (basePackage.endsWith(".")) {
      basePackage = basePackage.substring(0, basePackage.lastIndexOf('.'));
    }
    // 将包名中的“.”换成系统文件夹的“/”
    String basePackageFilePath = basePackage.replace('.', '/');
    

    2. 获取真实的路径

    Enumeration<URL> resources = Thread.currentThread().getContextClassLoader().getResources(basePackageFilePath);
    
    while (resources.hasMoreElements()) {
      URL resource = resources.nextElement();
    
    }
    

    这里需要关注下 resource 的类型, 如果是 FileJar 则进行解析,这篇文章主要进行 File 操作

    3. 识别文件,并进行递归遍历

    String protocol = resource.getProtocol();
    if ("file".equals(protocol)) {
      String filePath = URLDecoder.decode(resource.getFile(), "UTF-8");
      // 扫描文件夹中的包和类
      doScanPackageClassesByFile(classes, packageName, filePath, recursive);
    }
    
    

    测试

    项目结构

    在这里插入图片描述

    @Test
    public void testGetPackageAllClasses() throws IOException, ClassNotFoundException {
    
      Predicate<String> packagePredicate = s -> true;
    
      ClassScanner scanner = new ClassScanner("org.example", true, packagePredicate, null);
      Set<Class<?>> packageAllClasses = scanner.doScanAllClasses();
      packageAllClasses.forEach(it -> {
        System.out.println(it.getName());
      });
    }
    

    结果

    org.example.ClassScannerTest
    org.example.mapper.UserMapper
    org.example.App
    org.example.ClassScanner
    

    完整代码

    
    import java.io.File;
    import java.io.FileFilter;
    import java.io.IOException;
    import java.net.URL;
    import java.net.URLDecoder;
    import java.util.Enumeration;
    import java.util.LinkedHashSet;
    import java.util.Set;
    import java.util.function.Predicate;
    
    /**
     * class 扫描器
     *
     * @author zhangyunan
     */
    public class ClassScanner {
    
      private final String basePackage;
      private final boolean recursive;
      private final Predicate<String> packagePredicate;
      private final Predicate<Class> classPredicate;
    
    
      /**
       * Instantiates a new Class scanner.
       *
       * @param basePackage      the base package
       * @param recursive        是否递归扫描
       * @param packagePredicate the package predicate
       * @param classPredicate   the class predicate
       */
      public ClassScanner(String basePackage, boolean recursive, Predicate<String> packagePredicate,
        Predicate<Class> classPredicate) {
        this.basePackage = basePackage;
        this.recursive = recursive;
        this.packagePredicate = packagePredicate;
        this.classPredicate = classPredicate;
      }
    
      /**
       * Do scan all classes set.
       *
       * @return the set
       * @throws IOException            the io exception
       * @throws ClassNotFoundException the class not found exception
       */
      public Set<Class<?>> doScanAllClasses() throws IOException, ClassNotFoundException {
    
        Set<Class<?>> classes = new LinkedHashSet<Class<?>>();
    
        String packageName = basePackage;
    
        // 如果最后一个字符是“.”,则去掉
        if (packageName.endsWith(".")) {
          packageName = packageName.substring(0, packageName.lastIndexOf('.'));
        }
    
        // 将包名中的“.”换成系统文件夹的“/”
        String basePackageFilePath = packageName.replace('.', '/');
    
        Enumeration<URL> resources = Thread.currentThread().getContextClassLoader().getResources(basePackageFilePath);
        while (resources.hasMoreElements()) {
          URL resource = resources.nextElement();
          String protocol = resource.getProtocol();
          if ("file".equals(protocol)) {
            String filePath = URLDecoder.decode(resource.getFile(), "UTF-8");
            // 扫描文件夹中的包和类
            doScanPackageClassesByFile(classes, packageName, filePath, recursive);
          }
        }
    
        return classes;
      }
    
      /**
       * 在文件夹中扫描包和类
       */
      private void doScanPackageClassesByFile(Set<Class<?>> classes, String packageName, String packagePath,
        boolean recursive) throws ClassNotFoundException {
        // 转为文件
        File dir = new File(packagePath);
        if (!dir.exists() || !dir.isDirectory()) {
          return;
        }
        final boolean fileRecursive = recursive;
        // 列出文件,进行过滤
        // 自定义文件过滤规则
        File[] dirFiles = dir.listFiles((FileFilter) file -> {
          String filename = file.getName();
    
          if (file.isDirectory()) {
            if (!fileRecursive) {
              return false;
            }
    
            if (packagePredicate != null) {
              return packagePredicate.test(packageName + "." + filename);
            }
            return true;
          }
    
          return filename.endsWith(".class");
        });
    
        if (null == dirFiles) {
          return;
        }
    
        for (File file : dirFiles) {
          if (file.isDirectory()) {
            // 如果是目录,则递归
            doScanPackageClassesByFile(classes, packageName + "." + file.getName(), file.getAbsolutePath(), recursive);
          } else {
            // 用当前类加载器加载 去除 fileName 的 .class 6 位
            String className = file.getName().substring(0, file.getName().length() - 6);
            Class<?> loadClass = Thread.currentThread().getContextClassLoader().loadClass(packageName + '.' + className);
            if (classPredicate == null || classPredicate.test(loadClass)) {
              classes.add(loadClass);
            }
          }
        }
      }
    }
    
  • 相关阅读:
    生活中总结出来的各种定律
    python中os.system()的返回值
    使用ldap登录服务器
    使用VirtualBox + Vagrant打造统一的开发环境
    Laravel 虚拟开发环境 Homestead
    win7 powershell版本过低问题
    Vagrant安装教程及常见问题
    vagrant up提示“Couldn‘t open file /path/to/base“的错误解决方法
    一起谈.NET技术,ASP.NET MVC的全球化方案 狼人:
    一起谈.NET技术,.Net4.0 Parallel编程(二)Data Parallelism 中 狼人:
  • 原文地址:https://www.cnblogs.com/zyndev/p/13301925.html
Copyright © 2011-2022 走看看