JFinal使用技巧-多个JFinalConfig扫描加载

RT 实现多个JFinalConfig扫描加载的功能,目前没有使用场景,主要是拿JF新出的Scanner练练手,最近比较忙都没有分享有意思的东西了,回家前撸了一下,感觉还行,贴🐴 吧~

思路:
做一个主服务MainConfig,然后模仿@Path写个@JFConfig扫描加载项目内的其他MyConfig到一个LIst集合中,再MainConfig的每个回调接口中,都循环调用LIst里面的其他Config对应方法即可达到这个效果。社友反馈需求https://jfinal.com/feedback/7980


先建立一个MainConfig.java为JF配置入口

package com.demo.common.config;

import com.jfinal.config.*;
import com.jfinal.server.undertow.UndertowServer;
import com.jfinal.template.Engine;

import java.util.List;

public class MainConfig extends JFinalConfig {
    /**
     * 启动入口,运行此 main 方法可以启动项目,此 main 方法可以放置在任意的 Class 类定义中,不一定要放于此
     */
    public static void main(String[] args) {
        UndertowServer.start(MainConfig.class);
    }

    protected List<JFinalConfig> configs;
    public MainConfig(){
        this.configs = new JFinalConfigScanner("com.demo.common.config").scan();
        System.out.println("装载JFinalConfigs>" + configs);
    }

    @Override
    public void configConstant(Constants me) {
        for (JFinalConfig config : configs) {
            config.configConstant(me);
        }
    }

    @Override
    public void configRoute(Routes me) {
        for (JFinalConfig config : configs) {
            config.configRoute(me);
        }
    }

    @Override
    public void configEngine(Engine me) {
        for (JFinalConfig config : configs) {
            config.configEngine(me);
        }
    }

    @Override
    public void configPlugin(Plugins me) {
        for (JFinalConfig config : configs) {
            config.configPlugin(me);
        }
    }

    @Override
    public void configInterceptor(Interceptors me) {
        for (JFinalConfig config : configs) {
            config.configInterceptor(me);
        }
    }

    @Override
    public void configHandler(Handlers me) {
        for (JFinalConfig config : configs) {
            config.configHandler(me);
        }
    }

    @Override
    public void onStart() {
        for (JFinalConfig config : configs) {
            config.onStart();
        }
    }

    @Override
    public void onStop() {
        for (JFinalConfig config : configs) {
            config.onStop();
        }
    }
}


JFinalConfigs.java 注解用于配置 JFinalConfig

package com.demo.common.config;

import java.lang.annotation.*;

/**
 * JFinalConfigs 注解用于配置 JFinalConfig
 * 搭配 JFinalConfigScanner 实现多个JFinalConfig扫描加载功能
 */
@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface JFinalConfigs {
int NULL_ORDER = 0;

/**
 * 加载顺序
 * @return
 */
int order() default NULL_ORDER;
}


FinalConfigScanner.java 扫描 @JFinalConfigs 注解,实现多个JFinalConfig扫描加载功能

package com.demo.common.config;

import com.jfinal.config.JFinalConfig;
import com.jfinal.core.PathScanner;
import com.jfinal.kit.ReflectKit;
import com.jfinal.kit.StrKit;
import com.jfinal.log.Log;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.util.*;
import java.util.function.Predicate;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * JFinalConfigScanner 扫描 @JFinalConfigs 注解,实现多个JFinalConfig扫描加载功能
 */
public class JFinalConfigScanner {
    // 存放已被扫描过的 JFinalConfig
    private final List<JFinalConfig> scannedJFinalConfig = new ArrayList<>();

    // 过滤被扫描的资源
    private Predicate<URL> resourceFilter = null;

    // 扫描的基础 package,只扫描该包及其子包之下的类
    private String basePackage;

    // 跳过不需要被扫描的类
    private Predicate<String> classSkip;

    private ClassLoader classLoader;

    public JFinalConfigScanner(String basePackage, Predicate<String> classSkip) {
        if (StrKit.isBlank(basePackage)) {
            throw new IllegalArgumentException("basePackage can not be blank");
        }

        String bp = basePackage.replace('.', '/');
        bp = bp.endsWith("/") ? bp : bp + '/';          // 添加后缀字符 '/'
        bp = bp.startsWith("/") ? bp.substring(1) : bp;       // 删除前缀字符 '/'

        this.basePackage = bp;
        this.classSkip = classSkip;
    }

    public JFinalConfigScanner(String basePackage) {
        this(basePackage, null);
    }

    /**
     * 过滤被扫描的资源,提升安全性
     *
     * <pre>
     * 例子:
     *  PathScanner.filter(url -> {
     *      String res = url.toString();
     *
     *      // 如果资源在 jar 包之中,并且 jar 包文件名不包含 "my-project" 则过滤掉
     *      // 避免第三方 jar 包中的 Controller 被扫描到,提高安全性
     *      if (res.contains(".jar") && !res.contains("my-project")) {
     *          return false;  // return false 表示过滤掉当前资源
     *      }
     *
     *      return true;      // return true 表示保留当前资源
     *  });
     * </pre>
     */
    public void filter(Predicate<URL> resourceFilter) {
        this.resourceFilter = resourceFilter;
    }

    public List<JFinalConfig> scan() {
        try {
            classLoader = getClassLoader();
            List<URL> urlList = getResources();
            scanResources(urlList);
            order();
            return scannedJFinalConfig;
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    
    public void order(){
        Collections.sort(scannedJFinalConfig, new Comparator<JFinalConfig>() {
            @Override
            public int compare(JFinalConfig j1, JFinalConfig j2) {
                return Integer.compare(//
                        j1.getClass().getAnnotation(JFinalConfigs.class).order(),//
                        j2.getClass().getAnnotation(JFinalConfigs.class).order());
            }
        });
    }
    
    private ClassLoader getClassLoader() {
        ClassLoader ret = Thread.currentThread().getContextClassLoader();
        return ret != null ? ret : PathScanner.class.getClassLoader();
    }

    private List<URL> getResources() throws IOException {
        List<URL> ret = new ArrayList<>();

        // 用于去除重复
        Set<String> urlSet = new HashSet<>();
        // ClassLoader.getResources(...) 参数只支持包路径分隔符为 '/',而不支持 '\'
        Enumeration<URL> urls = classLoader.getResources(basePackage);
        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();

            // 过滤不需要扫描的资源
            if (resourceFilter != null && !resourceFilter.test(url)) {
                continue ;
            }

            String urlStr = url.toString();
            if ( ! urlSet.contains(urlStr) ) {
                urlSet.add(urlStr);
                ret.add(url);
            }
        }
        return ret;
    }

    private void scanResources(List<URL> urlList) throws IOException {
        for (URL url : urlList) {
            String protocol = url.getProtocol();
            if ("jar".equals(protocol)) {
                scanJar(url);
            } else if ("file".equals(protocol)) {
                scanFile(url);
            }
        }
    }

    private void scanJar(URL url) throws IOException {
        URLConnection urlConn = url.openConnection();
        if (urlConn instanceof JarURLConnection) {
            JarURLConnection jarUrlConn = (JarURLConnection)urlConn;
            try (JarFile jarFile = jarUrlConn.getJarFile()) {
                Enumeration<JarEntry> jarFileEntries = jarFile.entries();
                while (jarFileEntries.hasMoreElements()) {
                    JarEntry jarEntry = jarFileEntries.nextElement();
                    String en = jarEntry.getName();
                    // 只扫描 basePackage 之下的类
                    if (en.endsWith(".class") && en.startsWith(basePackage)) {
                        // JarEntry.getName() 返回值中的路径分隔符在所有操作系统下都是 '/'
                        en = en.substring(0, en.length() - 6).replace('/', '.');
                        scanJFinalConfig(en);
                    }
                }
            }
        }
    }

    private void scanFile(URL url) {
        String path = url.getPath();
        path = decodeUrl(path);
        File file = new File(path);
        String classPath = getClassPath(file);
        scanFile(file, classPath);
    }

    private void scanFile(File file, String classPath) {
        if (file.isDirectory()) {
            File[] files = file.listFiles();
            if (files != null) {
                for (File fi : files) {
                    scanFile(fi, classPath);
                }
            }
        }
        else if (file.isFile()) {
            String fullName = file.getAbsolutePath();
            if (fullName != null && fullName.endsWith(".class")) {
                String className = fullName.substring(classPath.length(), fullName.length() - 6).replace(File.separatorChar, '.');
                scanJFinalConfig(className);
            }
        }
    }

    private String getClassPath(File file) {
        String ret = file.getAbsolutePath();
        // 添加后缀,以便后续的 indexOf(bp) 可以正确获得下标值,因为 bp 确定有后缀
        if ( ! ret.endsWith(File.separator) ) {
            ret = ret + File.separator;
        }

        // 将 basePackage 中的路径分隔字符转换成与 OS 相同,方便处理路径
        String bp = basePackage.replace('/', File.separatorChar);
        int index = ret.lastIndexOf(bp);
        if (index != -1) {
            ret = ret.substring(0, index);
        }

        return ret;
    }

    @SuppressWarnings("unchecked")
    private void scanJFinalConfig(String className) {
        // 跳过不需要被扫描的 className
        if (classSkip != null && classSkip.test(className)) {
            return;
        }
        Class<?> c = loadClass(className);
        if (c == null){
            return;
        }
        if(!JFinalConfig.class.isAssignableFrom(c)){
            return;
        }
        JFinalConfigs configs = c.getAnnotation(JFinalConfigs.class);
        if(configs == null){
            return;
        }
        JFinalConfig config = (JFinalConfig)ReflectKit.newInstance(c);
        if (config != null && !scannedJFinalConfig.contains(config)) {
            scannedJFinalConfig.add(config);
        }
    }

    private Class<?> loadClass(String className) {
        try {
            return classLoader.loadClass(className);
        }
        // 此处不能 catch Exception,否则抓不到 NoClassDefFoundError,因为它是 Error 的子类
        catch (Throwable t) {
            Log.getLog(PathScanner.class).debug("PathScanner can not load the class \"" + className + "\"");

            /**
             * 由于扫描是一种主动行为,所以 pom.xml 中的 provided 依赖会在此被 loadClass,
             * 从而抛出 NoClassDefFoundError、UnsupportedClassVersionError、
             * ClassNotFoundException 异常。return null 跳过这些 class 不处理
             *
             * 如果这些异常并不是 provided 依赖的原因而引发,也会在后续实际用到它们时再次抛出异常,
             * 所以 return null 并不会错过这些异常
             */
            return null;
        }
    }

    /**
     * 支持路径中存在空格百分号等等字符
     */
    private String decodeUrl(String url) {
        try {
            return URLDecoder.decode(url, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }
}

代码是从JF的 @Path中拷贝出来,稍改改OK,再写俩测试类试试

package com.demo.common.config;

import com.jfinal.config.*;
import com.jfinal.kit.LogKit;
import com.jfinal.template.Engine;

@JFinalConfigs
public class My1Config extends JFinalConfig {
    @Override
    public void configConstant(Constants me) {
        System.out.println("My1Config>configConstant");
    }

    @Override
    public void configRoute(Routes me) {
        System.out.println("My1Config>configRoute");
    }

    @Override
    public void configEngine(Engine me) {
        System.out.println("My1Config>configEngine");
    }

    @Override
    public void configPlugin(Plugins me) {
        System.out.println("My1Config>configPlugin");
    }

    @Override
    public void configInterceptor(Interceptors me) {
        System.out.println("My1Config>configInterceptor");
    }

    @Override
    public void configHandler(Handlers me) {
        System.out.println("My1Config>configHandler");
    }

    @Override
    public void onStart() {
        System.out.println("My1Config>onStart");
    }

    @Override
    public void onStop() {
        System.out.println("My1Config>onStop");
    }
}
package com.demo.common.config;

import com.jfinal.config.*;
import com.jfinal.kit.LogKit;
import com.jfinal.template.Engine;

@JFinalConfigs(order = 1)
public class My2Config extends JFinalConfig {
    @Override
    public void configConstant(Constants me) {
        System.out.println("My2Config>configConstant");
    }

    @Override
    public void configRoute(Routes me) {
        System.out.println("My2Config>configRoute");
    }

    @Override
    public void configEngine(Engine me) {
        System.out.println("My2Config>configEngine");
    }

    @Override
    public void configPlugin(Plugins me) {
        System.out.println("My2Config>configPlugin");
    }

    @Override
    public void configInterceptor(Interceptors me) {
        System.out.println("My2Config>configInterceptor");
    }

    @Override
    public void configHandler(Handlers me) {
        System.out.println("My2Config>configHandler");
    }

    @Override
    public void onStart() {
        System.out.println("My2Config>onStart");
    }

    @Override
    public void onStop() {
        System.out.println("My2Config>onStop");
    }
}

开始测试!

运行MainConfig.java中的main方法

public static void main(String[] args) {
    UndertowServer.start(MainConfig.class);
}

效果:

image.png

Scanner很快啊,我一点,唰的就执行完了。。。 
回家~

评论区

JFinal

2021-01-15 22:57

举一反三,灵活运用,赞

jfinal 的 Scanner 精心设计过,性能、场景适应性极好

zhangtianxiao

2021-01-16 04:54

@JFinal 我把所有的BaseModel Model以静态内部类的方式分别塞在了两个文件中, 以杜绝测试表删除, model重新生成, 文件残留, 开发人员vsc不同步, 静态内部类是Sann不到的, 最终的结果长这样sByj41.png

JFinal

2021-01-16 15:28

@zhangtianxiao 你的 Model 已经生成了 baseModel ,为啥还要:
SerializeConfig.getGlobalInstance().put(type, value);

fastjson 不走 getter 方法,你的 model 中的字段的下划线怎么处理?

JFinal

2021-01-16 15:29

@zhangtianxiao PathScanner 只扫描 controller,不支持内部类的扫描

zhangtianxiao

2021-01-16 16:59

@JFinal 我数据和前端用下划线, 后端用驼峰

风满楼

2021-01-17 11:43

针不戳~

海哥

2021-01-19 13:17

不错 这样的话 在团队开发里 或者 多 maven 模块里,每个模块都可以有自己的 JFinalConfig 而不冲突了

AlienJunX

2021-01-25 13:54

@海哥 jboot中早就有了,^_^

杜福忠

2021-01-25 16:10

@AlienJunX 那必须的,海哥做了好多有意思的东西,我大学的时候就用过Afinal,工作后又用了JPress老爽了

steven_lhcb_9527

2021-02-01 10:00

太优秀了,实际上@JFinalConfigs相当于Spring里面的容器注解。

steven_lhcb_9527

2021-02-02 11:38

设计扫描类的设计思想是什么啊

杜福忠

2021-02-02 11:47

@steven_lhcb_9527 在帖子开头有说思路,和需求来源。简单来说就是社友反馈需求 + 想试试Scanner 。设计思想:尽量保持JFinal的风格原味 + 让使用者写更少代码

steven_lhcb_9527

2021-02-02 14:54

第205行
if(!c.isAnnotationPresent(JFinalConfigs.class)){
return;
}

杜福忠

2021-02-02 15:49

@steven_lhcb_9527 功能都是可以用的,但是波总写的Scanner之所以快如闪电,原因基本也是在这个位置了。可以自己试试 这两个方法的性能对比,当项目类文件越多的时候,扫描的耗时差距就越大!当有上千万个类文件时,差距基本在1000倍左右

steven_lhcb_9527

2021-02-02 16:23

@杜福忠 上千万个文件扫描直接递归应该就栈溢出了吧。

杜福忠

2021-02-02 16:44

@steven_lhcb_9527 文件scanFile模式的时候,一般也就是开发的时候是散的class文件,递归scanFile的时候,也就是说一层包,一次递归,没见过谁会把包名建的特别深层级的操作,其次是file.listFiles()创建的File对象也就是一个包下面的class数,这个也不会出现一个包里面放几千上万个类的吧,所以递归释放对象也是非常快的。

steven_lhcb_9527

2021-02-03 09:40

Enumeration urls = classLoader.getResources(basePackage);
枚举的封装类的hasMoreElements有毒吧,里面有两个元素,但是总是返回false。为什么不能用迭代器,用枚举?

杜福忠

2021-02-03 10:18

@steven_lhcb_9527 写JavaJDK这帮人那都是大佬中的大佬中的大佬级,肯定都是老严谨了,做的东西不是说改就改的。ClassLoader多底层啊第一个版本就有了,Iterator在1.2才出世了。。。再说它当前这个业务用Enumeration完全满足需求啊,没必要改呗

steven_lhcb_9527

2021-02-03 10:56

我找着你的代码,执行的时候hasMoreElements直接跳出了,明明有两个元素。不知道该怎么弄了

steven_lhcb_9527

2021-02-03 13:50

按理说,没有在while里面删除元素,怎么会计数器和枚举容器的大小不一致呢

热门分享

扫码入社