手动加载jar包并生成独立的spring bean

该博客介绍了如何在Java框架中动态加载用户上传的jar包,创建独立的classLoader,初始化Spring上下文,并执行jar包内的业务逻辑。主要内容包括加载jar包、创建classLoader、创建Spring上下文以及框架中使用jar包的bean。
摘要由CSDN通过智能技术生成

1. 加载jar包

这一步主要是作为框架应用,加载用户上传的jar包

Path basePath = "/home/admin/apps/";

// 加载主jar包,即框架定义的方法入口jar
Path appJarPath = basePath.resolve("app");

Stream<Path> appJarPathList= Files.list(appJarPath);
Optional<Path> appJarPathResult =  appJarPathList.filter(path ->path.toFile().getName().endsWith(".jar")).findFirst();

File appJar = appJarPathResult.get().toFile();


// 加载主jar的依赖jar(由maven打包生成)
Path dependencyJarPath = basePath.resolve("dependencies");

Stream<Path> dependencyPathList= Files.list(dependencyJarPath);
dependencyJars = dependencyPathList.map(path ->path.toFile()).collect(Collectors.toList());

// 将所有jar转换成classLoader能使用的URL列表
List<File> allJars = new ArrayList<>();
allJars.add(mainJar);
allJars.addAll(dependencyJars);

ArrayList<URL> urlList = new ArrayList<>();
allJars.forEach(jarFile ->{
    try {
        urlList.add(new URL("file:"+jarFile.getAbsolutePath()));
    } catch (MalformedURLException e) {
    }
});

2. 创建classLoader

创建一个单独的classLoader,并且以当前classLoader为parent,这样就把jar和框架隔离开来了。

URL[] urlArr = new URL[urlList.size()];
urlList.toArray(urlArr);
JarFile appJarFile = new JarFile(appJar);

// 这里的manifest, 是jar包里面手写的一个,目录在resources/META-INF/MANIFEST.MF
// 
// MF文件内容是:
// Manifest-Version: 1.0
// APP-Main-Class: com.xxx.yyy.MyApp
// 
// 注意上面的MyApp是用户程序实现的框架的入口接口(MainApp)的一个实现,便于框架调用用户jar的方法
Manifest manifest = appJarFile.getManifest();
String mainClassName = manifest.getMainAttributes().getValue("APP-Main-Class");

// 创建新的classLoader,以当前程序(AppFrame)的classLoader为parent
URLClassLoader classLoader = new URLClassLoader(urlArr, AppFrame.class.getClassLoader());
Class mainClass = classLoader.loadClass(mainClassName);

3. 创建spring上下文

创建spring上下文,把jar包里面的bean配置扫描解析成框架可用的bean

AnnotationConfigApplicationContext newContext = new AnnotationConfigApplicationContext();

// 设置刚刚新建的classLoader,是之能找到jar包的class和框架本身的class
newContext.setResourceLoader(new DefaultResourceLoader(classLoader));

// 扫描APP中入口类所在的包(潜规则是入口类要放到jar包的最外层,保证jar包中的其它类都属于这个入口类的包路径之下)
newContext.scan(mainClass.getPackage().getName());

newContext.registerShutdownHook();

// 另外,如果jar包使用到了框架的bean,可用在这里进行注入
// 注意:
// 1. 这里的applicationContext是框架的spring上下文,通过实现ApplicationContextAware接口注入的
// 2. 通过名称获取bean,为了不出问题,在服务发布的时候,尽量指定精确的名称:@Service("frameServiceOne")
FrameServiceOne frameServiceOne = (FrameServiceOne)applicationContext.getBean("frameServiceOne");

newContext.getBeanFactory().registerSingleton("frameServiceOne", frameServiceOne);


// 同时,也可以注册一些spring支持的processer,进行注解、属性等的处理
// 这里frameLogAnnoInjector可以实现BeanPostProcessor接口
// 在postProcessBeforeInitialization里面处理自定义注解
// @Override
// public Object postProcessBeforeInitialization(final Object bean, String //beanName) throws BeansException {
//     ReflectionUtils.doWithFields(bean.getClass(), field -> {
//         ReflectionUtils.makeAccessible(field);
//         if (field.getAnnotation(FrameLogAnno.class) != null) {
//             Logger log = LoggerFactory.getLogger(getLoggerName());
//             field.set(bean, log);
//           }
//        });
//        return bean;
//    }
//
// 然后就可以在框架和jar包里直接通过注解使用日志:
// @FrameLogAnno
// Logger logger;
FrameLogAnnoInjector frameLogAnnoInjector = new FrameLogAnnoInjector();
newContext.getBeanFactory().addBeanPostProcessor(frameLogAnnoInjector);

// 当然也可以注入BeanFactoryPostProcessor,实现属性占位符替换
// 其中PropertyPlaceholderConfigurer是spring自带的占位符处理器
Properties properties = new Properties();
properties.setProperty("xxx","yyy");
PropertyPlaceholderConfigurer configurer = new PropertyPlaceholderConfigurer();
configurer.setProperties(properties);
newContext.addBeanFactoryPostProcessor(configurer);


// 最后,刷新spring上下文,将bean定义实例化成bean
// 注意这里是在主线程直接刷新的,jar包用户多了以后,可以起线程池,用Future或countDownLatch等待
// 需要注意把新的classLoader传给子线程,并设置到子线程的ContextClassLoader就行

// 保留当前主线程的classLoader
ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
// 用新的classLoader去刷新(新的classLoader包含jar包里面的class)
Thread.currentThread().setContextClassLoader(classLoader);
newContext.refresh();
// 恢复当前主线程的classLoader
Thread.currentThread().setContextClassLoader(currentClassLoader);

// 最后,共享一下active profile
Arrays.asList(applicationContext.getEnvironment().getActiveProfiles()).stream().forEach(profile -> newContext.getEnvironment().addActiveProfile(profile));

4. 框架中使用jar包的bean

app初始化并缓存起来

// 加载app,可以放到框架的启动方法里面,也可以同时放到一个加载接口方法里面
public void loadApp(String appName) {

    // 通过app名称初始化spring上下文,具体方法就是前面几节的内容
    ConfigurableApplicationContext newContext = initSpringContext(appName);

    // 找到jar包中的入口接口MainApp的实例化bean
    MainApp mainApp = newContext.getBeanFactory().getBean("mainApp");

    // 入口方法初始化
    mainApp.init();

    // 将app信息缓存起来,后续使用
    AppInfo appInfo = new AppInfo();
    appInfo.setAppName(appName);
    appInfo.setMainClass(mainClass); // 想办法传进来,保存起来,可能有用
    appInfo.setClassLoader(classLoader); // 想办法传进来,保存起来,可能有用
    appInfo.setMainApp(mainApp);
    appInfo.setApplicationContext(newContext);
    this.saveApp(appInfo);
}

有请求流量进来以后,在框架调用app的bean执行逻辑

AppInfo appInfo = this.getApp(appName);

// pre方法
appInfo.getMainApp().preHandle();


// 找到handler
AppHandler handler = appInfo.getApplicationContext().getBean(handlerName, AppHandler.class);

if (null == handler) {
    Map<String, AppHandler> handlers = appInfo.getApplicationContext().getBeansOfType(AppHandler.class);
    handler = handlers.values().stream().filter(h -> h.getClass().getName().equalsIgnoreCase(handlerClassName)).findFirst().orElse(null);
}


// 执行方法
handler.handle();


// post方法
appInfo.getMainApp().postHandle();

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值