All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.nutz.plugins.hotplug.Hotplug Maven / Gradle / Ivy

There is a newer version: 1.r.69.v20220215
Show newest version
package org.nutz.plugins.hotplug;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.nutz.ioc.Ioc;
import org.nutz.ioc.ObjectProxy;
import org.nutz.ioc.impl.NutIoc;
import org.nutz.ioc.impl.ScopeContext;
import org.nutz.ioc.loader.annotation.AnnotationIocLoader;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.ioc.loader.combo.ComboIocLoader;
import org.nutz.json.Json;
import org.nutz.lang.Files;
import org.nutz.lang.Lang;
import org.nutz.lang.Mirror;
import org.nutz.lang.Streams;
import org.nutz.lang.Strings;
import org.nutz.lang.util.Disks;
import org.nutz.lang.util.FileVisitor;
import org.nutz.log.Log;
import org.nutz.log.Logs;
import org.nutz.mvc.ActionChainMaker;
import org.nutz.mvc.Mvcs;
import org.nutz.mvc.NutConfig;
import org.nutz.mvc.Setup;
import org.nutz.mvc.UrlMapping;
import org.nutz.mvc.ViewMaker;
import org.nutz.mvc.annotation.IocBy;
import org.nutz.mvc.annotation.SetupBy;
import org.nutz.mvc.impl.NutLoading;
import org.nutz.mvc.impl.ServletValueProxyMaker;
import org.nutz.resource.NutResource;
import org.nutz.resource.Scans;
import org.nutz.resource.impl.JarResourceLocation;

/**
 * 动态增减web插件
 * 
 * 用法:  

* 主项目的MainModule标注@LoadingBy(HotPlug.class)

* 插件项目的格式要求: 需要一个XXXMainModule, 必须带一个hotplug.XXX.json文件,且文件格式如下

* {name:"cms", "version":"1.0", "base": "net.wendal.nutzbook.cms", "main":"net.wendal.nutzbook.cms.CmsMainModule"} * @author wendal * */ @SuppressWarnings("unchecked") public class Hotplug extends NutLoading { private static final Log log = Logs.get(); protected static Properties hpconf = new Properties(); protected HotplugClassLoader hpcl; protected static Hotplug me; public Hotplug() throws IOException { me = this; hpcl = new HotplugClassLoader(Thread.currentThread().getContextClassLoader()); InputStream ins = getClass().getClassLoader().getResourceAsStream("/hotplug.properties"); if (ins != null) { hpconf.load(ins); } Scans.me().addResourceLocation(new HotplugResourceLocation()); } /** * 保持已有的Ioc容器,映射插件的@At的时候需要用到 */ protected Ioc ioc; //----------------------------------------------------- // 为了动态增减ioc内的对象,需要hack一下NutIoc内的私有属性 protected ScopeContext scopeContext; //----------------------------------------------------- /** * 主项目的配置对象 */ protected NutConfig config; /** * 代理原有的映射关系,优先使用插件的映射关系 */ protected HotplugUrlMapping ump; /** * 插件列表,为了方便,这里直接用静态属性了 */ protected static Map _plugins = new LinkedHashMap(); /** * 主项目的@Ok/@Fail处理类,新增插件时需要用到. */ protected ViewMaker[] views; protected ActionChainMaker chainMaker; @Override public UrlMapping load(NutConfig config) { this.config = config; // 保存起来,后面会用到 UrlMapping um = super.load(config); ump = new HotplugUrlMapping(um, config.getServletContext()); // 代理之 return ump; } protected Ioc createIoc(NutConfig config, Class mainModule) throws Exception { NutIoc ioc = null; IocBy ib = mainModule.getAnnotation(IocBy.class); if (ib == null) throw new RuntimeException("Ioc is needed!"); List argList = new ArrayList(); String[] args = ib.args(); for (String arg : args) { argList.add(arg); } if (!argList.contains("*hotplug")) argList.add("*hotplug"); args = argList.toArray(new String[argList.size()]); if (log.isDebugEnabled()) log.debugf("@IocBy(type=%s, args=%s,init=%s)", ComboIocLoader.class, Json.toJson(args), Json.toJson(ib.init())); scopeContext = new ScopeContext("app"); ioc = new NutIoc(new ComboIocLoader(args), scopeContext, "app"); ioc.addValueProxyMaker(new ServletValueProxyMaker(config.getServletContext())); // 将自身放入ioc容器, 这样就能通过主项目的入口方法调用本类的方法 ((NutIoc)ioc).getIocContext().save("app", "hotplug", new ObjectProxy(this)); Mvcs.setIoc(ioc); return ioc; } protected ViewMaker[] createViewMakers(Class mainModule, Ioc ioc) throws Exception { if (views == null) //保持主项目的@Ok/@Fail处理器 views = super.createViewMakers(mainModule, ioc); return views; } @Override protected ActionChainMaker createChainMaker(NutConfig config, Class mainModule) { if (chainMaker == null) chainMaker = super.createChainMaker(config, mainModule); return chainMaker; } /** * 载入一个插件,必须符合特定的jar包 * @param f 插件jar文件,通过文件上传/数据库读取等方式保存到本地,然后调用本方法 * @return 插件信息 * @throws Exception */ public HotplugConfig enable(File f, HotplugConfig hc) throws Exception { if (hc == null) hc = checkHotplugFile(f); try { if (Hotplug._plugins.containsKey(hc.getName())) disable(hc.getName()); } catch (Exception e) { log.info("something happen when remove old hotplug", e); } // 首先,我们需要解析这个jar. Jar文件也是Zip. 解析完成前,还不会影响到现有系统的运行 ZipFile zf = new ZipFile(f); Enumeration en = (Enumeration) zf.entries(); HashMap assets = new HashMap(); HashMap tmpls = new HashMap(); while (en.hasMoreElements()) { ZipEntry ze = en.nextElement(); String name = ze.getName(); if (name.endsWith("/")) continue; // 解析资源文件 if (name.startsWith("assets/")) { byte[] buf = Streams.readBytes(zf.getInputStream(ze)); HotplugAsset asset = new HotplugAsset(buf); assets.put(name.substring("assets/".length()), asset); } else if (name.startsWith("templates/")) { tmpls.put(name.substring("templates/".length()), new String(Streams.readBytes(zf.getInputStream(ze)))); } } zf.close(); hc.assets = assets; hc.tmpls = tmpls; // 解析完成, 开始影响现有系统. // ----------------------------------------------------- // 先保存当前的类加载器 ClassLoader prevClassLoader = Thread.currentThread().getContextClassLoader(); try { // 构建URLClassLoader,开始加载这个jar包 URLClassLoader classLoader = new URLClassLoader(new URL[]{f.toURI().toURL()}, hpcl); hc.classLoader = classLoader; // 放入插件列表, 因为tmpls已经生效,所以会影响beetl的模板加载系统(其实嘛,一点问题没有...) _plugins.put(hc.getName(), hc); // 将其设置为线程上下文的ClassLoader, 这样才能是下面的资源扫描和类扫描生效 Thread.currentThread().setContextClassLoader(classLoader); // 添加资源扫描路径,为下面的类扫描打下基础, 这里开始影响"资源扫描子系统",其实也是没一点问题... hc.resourceLocation = new JarResourceLocation(f.getAbsolutePath()); abc(hc); new File(f.getParent(), f.getName() + ".enable").createNewFile(); hc.put("enable", true); } catch (Exception e) { disable(hc.getName()); throw e; } finally { // 还原ClassLoader Thread.currentThread().setContextClassLoader(prevClassLoader); } return hc; } public void disable(String key) { HotplugConfig hc = _plugins.get(key); if (hc == null) { return; } if ("file".equals(hc.getOrigin())) { try { new File(hc.getOriginPath() + ".enable").delete(); } catch (Throwable e) { } } // 移除URL映射, 对外服务停止. // 移除出插件列表, 同时移除静态资源和URL映射. hc.urlMapping = null; // 如果存在iocLoader,清理一下. 为空的可能性,只有初始化过程中抛出异常,但,还没写呢... if (hc.iocLoader != null) { // 变量所持有的ioc bean,逐一销毁 for (String beanName : hc.iocLoader.getName()) { try { ObjectProxy op = scopeContext.fetch(beanName); if (op == null) continue; op.depose(); } catch (Exception e) { log.debug("depose hotplug bean fail", e); } } // 移除ioc上下文中存在的ioc bean for (String beanName : hc.iocLoader.getName()) { scopeContext.remove("app", beanName); } } if ("file".equals(hc.getOrigin()) && hc.classLoader != null && hc.getClassLoader() instanceof URLClassLoader) { try { ((URLClassLoader)hc.classLoader).close(); } catch (Throwable e) { log.warn("something happen when close UrlClassLoader", e); } } _plugins.remove(key); } protected List setups = new ArrayList(); protected void setupInit(Class klass) { Setup setup; if (klass.getAnnotation(IocBean.class) != null) setup = (Setup) ioc.get(klass); else setup = (Setup) Mirror.me(klass).born(); setups.add(setup); setup.init(config); } public void setupInit() { List list = Scans.me().scan("hotplug/", ".+.(js|json)$"); List hclist = new ArrayList(); for (NutResource nr : list) { log.debug("Check " + nr.getName()); try { HotplugConfig hc = Json.fromJson(HotplugConfig.class, nr.getReader()); log.debugf("Found name=%s base=%s", hc.getName(), hc.getBase());hclist.add(hc); hc.put("origin", "embed"); } catch (Exception e) { throw new RuntimeException(e); } } //TODO 自定义顺序 sort(hclist); // 加载内置插件 for (HotplugConfig hc : hclist) { hc.put("enable", true); hc.classLoader = getClass().getClassLoader(); hc.assets = new HashMap(); hc.tmpls = new HashMap(); _plugins.put(hc.getName(), hc); log.debug("init hotplug name=" + hc.getName()); try { abc(hc); } catch (Exception e) { log.error("fail at hotplug name=" + hc.getName()); throw new RuntimeException(e); } } hclist = getHotPlugJarList(false); sort(hclist); // 加载外部插件 for (HotplugConfig hc : hclist) { log.infof("hotplug name=%s version=%s enable=%s", hc.getName(), hc.getVersion(), hc.isEnable()); if (!hc.isEnable()) { continue; } String path = hc.getOriginPath(); log.debugf("hotplug from dir path=%s", path); try { enable(new File(path), hc); } catch (Exception e) { log.error("load hotplug fail!!! path=" + path, e); throw Lang.wrapThrow(e); } } } public void sort(List hclist) { Collections.sort(hclist, new Comparator() { public int compare(HotplugConfig prev, HotplugConfig next) { if (prev.getName().equals(next.getName())) return 0; if ("core".equals(prev.getName())) return -1; else if ("core".equals(next.getName())) return 1; return prev.getName().compareTo(next.getName()); } }); } public void setupDestroy(){ for (Setup setup : setups) { setup.destroy(config); } } public void abc(HotplugConfig hc) throws Exception { String mainClass = hc.getMain(); if (Strings.isBlank(mainClass)) { mainClass = hc.getBase() + "." + Strings.upperFirst(hc.getName()) + "MainModule"; } Class klass = hc.getClassLoader().loadClass(mainClass); // 放入NutIoc的Ioc加载器列表,开始影响"Ioc子系统", 恩, 一般情况下很好. // 加载旗下的@IocBean IocBy iocBy = klass.getAnnotation(IocBy.class); if (iocBy == null) hc.iocLoader = new AnnotationIocLoader(hc.getBase()); else hc.iocLoader = new ComboIocLoader(iocBy.args()); // 生成插件的URL映射, 即@At的配置 UrlMapping um = evalUrlMapping(config, klass, ioc); // 看看有无setupby SetupBy setupBy = klass.getAnnotation(SetupBy.class); if (setupBy != null) { setupInit(setupBy.value()); } else { try { Class setupClass = hc.getClassLoader().loadClass(hc.getBase() + "." + Strings.upperFirst(hc.getName()) + "MainSetup"); setupInit(setupClass); } catch (ClassNotFoundException e) { } } // 赋值给hc,然后让ump添加映射表, 正式对外服务的开始, 插件相关的URL可以访问了 hc.urlMapping = um; } public static File find(String key) { String dir = hpconf.getProperty("hotplug.parent_projects"); if (dir != null) { if (key.contains("?")) { key = key.substring(0, key.indexOf('?')); } File parentProjectRoot = new File(dir); if (parentProjectRoot.isDirectory()) { for (String subprojectName : parentProjectRoot.list()) { if (!new File(parentProjectRoot, subprojectName).isDirectory()) continue; File f = new File(dir + "/" + subprojectName + "/src/main/resources/" + key); if (f.exists() && f.isFile()) { log.debugf("found %s", f.getAbsolutePath()); return f; } f = new File(dir + "/" + subprojectName + "/conf/" + key); if (f.exists() && f.isFile()) { log.debugf("found %s", f.getAbsolutePath()); return f; } } } } return null; } public static Properties getHpconf() { return hpconf; } public static String getLibPath() { return hpconf.getProperty("hotplug.localdir", "/var/lib/hotplug"); } public static List getHotPlugJarList(final boolean getAll) { final List list = new ArrayList(); String hcdir = getLibPath(); File f = new File(Disks.normalize(hcdir)); log.debug("check hotplug.localdir : " + f.getAbsolutePath()); if (f.exists() && f.isDirectory()) { Disks.visitFile(f, new FileVisitor() { public void visit(File file) { if (file.isDirectory()) return; HotplugConfig hc = checkHotplugFile(file); if (hc == null) { log.debug("not hotplug : "+ file.getAbsolutePath()); return; } if (log.isDebugEnabled()) log.debugf("found hotplug name=%s version=%s enable=%s", hc.getName(), hc.getVersion(), hc.isEnable()); list.add(hc); } }, new FileFilter() { public boolean accept(File f) { if (f.isDirectory()) return true; return f.getName().endsWith(".jar"); } }); } return list; } public static HotplugConfig checkHotplugFile(File f) { try { ZipFile zf = new ZipFile(f); Enumeration en = (Enumeration) zf.entries(); HotplugConfig hc = null; try { while (en.hasMoreElements()) { ZipEntry ze = en.nextElement(); String name = ze.getName(); if (name.endsWith("/")) continue; if (name.startsWith("hotplug/hotplug.") && name.endsWith(".json")) { String j = new String(Streams.readBytes(zf.getInputStream(ze))); hc = Json.fromJson(HotplugConfig.class, j); hc.put("origin", "file"); hc.put("origin_path", f.getAbsolutePath()); hc.put("sha1", Lang.sha1(f)); hc.put("enable", new File(f.getParentFile(), f.getName() + ".enable").exists()); return hc; } } } finally { zf.close(); } } catch (Exception e) { log.debug("bad hotplug file="+f.getAbsolutePath(), e); return null; } return null; } public boolean add(File f) { HotplugConfig hc = checkHotplugFile(f); if (hc == null) return false; String dst = String.format("%s/%s-%s.jar", getLibPath(), hc.getName(), hc.getVersion()); Files.createFileIfNoExists(new File(dst)); Files.copy(f, new File(dst)); return true; } public static Map getActiveHotPlug() { return new LinkedHashMap(_plugins); } public static List getActiveHotPlugList() { return new ArrayList(_plugins.values()); } public static Hotplug me() { return me; } public HotplugClassLoader getHpcl() { return hpcl; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy