概述
一、背景
算法RD以前用python实现特征预处理,改完代码重新运行就生效了,现在改成java(为了借助java更强大生态,同时融入公司java体系),每次修改,都要打包、上线,迭代效率特别低。
二、需求分析
python编程,不需要显式编译,现改现运行,python解释器会先把源代码编译成.pyc,然后解释执行,两步一起做了,用户无感知。而java源代码需要先在一个地方编译成.class字节码,然后丢到另一个地方的JVM去解释执行,两步是分开的。
需求很简单:java插件修改完代码,重新打包上传到插件目录,线上代码自动生效。
需求可分解为:
- 插件
- 热加载
三、需求拆解
在实现这个需求时,我做了很多调研,先梳理一些知识点。
双亲委派
- 子类先委托父类加载
- 父类加载器有自己的加载范围,范围内没有找到,则不加载,并返回给子类
- 子类在收到父类无法加载的时候,才会自己去加载
类加载器体系
- 启动类加载器(BootstrapClassLoader):C++实现,在java里无法获取,只负责加载<JAVA_HOME>/lib下的类。
- 扩展类加载器(ExtClassLoader): Java实现,可以在java里获取,只负责加载<JAVA_HOME>/lib/ext下的类。
- 系统类加载器/应用程序类加载器(AppClassLoader):是与我们接触对多的类加载器,我们写的代码默认就是由它来加载,ClassLoader.getSystemClassLoader返回的就是它。
- 自定义类加载器(CustomClassLoader)
双亲委派最重要的作用是为了安全,保证核心API的.class不被篡改,比如jdk的String类只有BootstrapClassLoader能加载,如果AppClassLoader也能加载,JVM中会有多个String类的class实例,有很大安全隐患。
SPI
java做插件,标准做法基本是用SPI(Service Provider Interface)实现,由框架层制定接口,允许第三方为这些接口提供实现。比如
- JDBC:java.sql.Driver是jdk定义的,mysql产商提供mysql-connector-java具体实现。
- dubbo:extension机制,协议扩展、路由扩展等。
双亲委派的可见性原则
- AppClassLoader是可以读取到由ExtClassLoader和BootstrapClassLoader加载进来的Class的;
- ExtClassLoader是可以读取到由BootstrapClassLoader加载进来的Class的;
为什么SPI要打破双亲委派?
如果接口定义A和接口实现B,都是被AppClassLoader加载,自然没必要打破。但有些情况下,A和B不是同一个类加载器加载,比如第一个例子,JDBC接口定义是在<JAVA_HOME>/lib包,它只能被BootstrapClassLoader加载的,而BootstrapClassLoader不能加载mysql的实现类,只能由AppClassLoader加载。根据双亲委派的可见性原则,BootstrapClassLoader加载的DriverManager是不可能拿到AppClassLoader加载的实现类,所以要想办法打破。
怎么打破双亲委派?
- 上下文类加载器
- 自定义类加载器
SPI打破双亲委派,一般通过上下文类加载器
eg1:jdk的ServiceLoader
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
eg2:dubbo扩展获取classloader源码
public static ClassLoader getClassLoader(Class<?> clazz) {
ClassLoader cl = null;
try {
cl = Thread.currentThread().getContextClassLoader();
} catch (Throwable ex) {
// Cannot access thread context ClassLoader - falling back to system class loader...
}
if (cl == null) {
// No thread context class loader -> use class loader of this class.
cl = clazz.getClassLoader();
if (cl == null) {
// getClassLoader() returning null indicates the bootstrap ClassLoader
try {
cl = ClassLoader.getSystemClassLoader();
}
catch (Throwable ex) {
// Cannot access system ClassLoader - oh well, maybe the caller can live with null...
}
}
}
return cl;
}
要实现热加载,一般需要自定义类加载器
因为热加载的本质是引用不同的class实例,而唯一确定一个class实例唯一性是通过如下2点,只要一致,就是同一个class实例。
- 类全限定名
- 类加载器
类全限定名很好理解,比如:java.sql.Driver,通常它不被反复修改。
所以只能从类加载器下手,每次热部署时,new一个自定义类加载器来重新加载类,这样在方法区有2个class实例,使用新的class代替旧的class使用。
四、实现代码
原理:每次插件jar上传,DefaultTransformFactory自动监控到jar包更新,重新new一个TransformClassLoader来加载新的类放在pluginClassMap,插件使用方只需要调用getTransformerClassMap获取pluginClassMap来使用。
待改进:
- 这里没考虑插件的卸载,理论上存在class实例和自定义类加载器实例会无限创建,引起元空间内存溢出(方法区)的隐患。不过我们在调用方做到了对class引用不扩散,以及每次重启jvm会重置,影响不大。
闲话:
- 这里只有部分核心代码,实现上只用了自定义类加载器,就解决了插件+热加载的需求,上面分析SPI的篇幅,只是调研总结的资料。tomcat应用部署隔离,其实就是自定义类加载器+SPI结合使用的场景,我们这个需求没那么复杂。
- 热部署确实很热,延伸到OSGi(Java动态化模块化系统)、蚂蚁金服JarsLink,据说蚂蚁金服做到了上线无需发版,着实厉害。
- 刚看到github上专门有做插件的开源项目pf4j,后面有空也要研究一下。
接入方调用代码块
//通过插件name找到class
Map<String, Class<? extends BaseTransformer>> transformPluginMap = transformFactory.getTransformerClassMap();
if (transformPluginMap != null && transformPluginMap.size() > 0) {
Iterator<Map.Entry<String, Class<? extends BaseTransformer>>> iterator = transformPluginMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Class<? extends BaseTransformer>> entry = iterator.next();
if (name2Transformer.containsKey(entry.getKey())) {
log.info("name={}`transformer={} already in name2Transformer", entry.getKey(), entry.getValue());
continue;
}
name2Transformer.put(entry.getKey(), entry.getValue());
log.info("name={}`transformer={} inserted into name2Transformer", entry.getKey(), entry.getValue());
}
}
自定义类加载器TransformClassLoader
/**
* Created by itbasketplayer on 2020/7/16.
*/
public class TransformClassLoader extends ClassLoader {
private final static Logger logger = LoggerFactory.getLogger(TransformClassLoader.class);
private HashSet dynaclazns; // 需要由该类加载器直接加载的类全限定名
public TransformClassLoader(String jarfileDir) {
super(null); // 指定父类加载器为 null
if (StringUtils.isEmpty(jarfileDir)) {
throw new IllegalArgumentException("basePath can not be empty!");
}
File dir = new File(jarfileDir);
if (!dir.exists()) {
throw new IllegalArgumentException("basePath not exists:" + jarfileDir);
}
if (!dir.isDirectory()) {
throw new IllegalArgumentException("basePath must be a directory:" + jarfileDir);
}
dynaclazns = new HashSet();
loadClassFromJarfileDir(jarfileDir);
}
private void loadClassFromJarfileDir(String jarfileDir) {
File[] files = new File(jarfileDir).listFiles();
if (files != null) {
for (File file : files) {
scanJarFile(file);
}
}
}
private void scanJarFile(File file) {
if (file.exists()) {
if (file.isFile() && file.getName().endsWith(".jar")) {
try {
readJAR(new JarFile(file));
} catch (IOException e) {
logger.error("", e);
}
} else if (file.isDirectory()) {
for (File f : file.listFiles()) {
scanJarFile(f);
}
}
}
}
private void readJAR(JarFile jar) throws IOException {
Enumeration<JarEntry> en = jar.entries();
while (en.hasMoreElements()) {
JarEntry je = en.nextElement();
je.getName();
String name = je.getName();
if (name.endsWith(".class")) {
String className = name.replace("\", ".")
.replace("/", ".")
.replace(".class", "");
InputStream input = null;
ByteArrayOutputStream baos = null;
try {
input = jar.getInputStream(je);
baos = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = input.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
defineClass(className, baos.toByteArray(), 0, baos.toByteArray().length);
dynaclazns.add(className);
} catch (Exception e) {
logger.error("", e);
} finally {
if (baos != null) {
baos.close();
}
if (input != null) {
input.close();
}
}
}
}
}
protected Class loadClass(String name, boolean resolve)
throws ClassNotFoundException {
Class cls = null;
cls = findLoadedClass(name);
if (!this.dynaclazns.contains(name) && cls == null)
cls = getSystemClassLoader().loadClass(name);
if (cls == null)
throw new ClassNotFoundException(name);
if (resolve)
resolveClass(cls);
return cls;
}
}
默认插件工厂DefaultTransformFactory
/**
* 默认插件工厂
* Created by itbasketplayer on 2020/7/16.
*/
public class DefaultTransformFactory extends AbstractTransformFactory {
private final static Logger logger = LoggerFactory.getLogger(DefaultTransformFactory.class);
private TransformClassLoader classLoader;
private AppConfig config;
private volatile boolean reloading = false;
private int pluginHistoryCount = 0;
private int classLoaderHistoryCount = 0;
public DefaultTransformFactory(final String pluginPath, final PluginDbInfo pluginDbInfo) {
if (pluginPath == null || pluginPath.length() == 0) {
throw new IllegalArgumentException("pluginPath can not be empty!");
}
final MysqlConfigParser mysqlConfigParser = new MysqlConfigParser(pluginDbInfo);
try {
this.config = mysqlConfigParser.parseConfig();
} catch (Exception e) {
throw new IllegalArgumentException("parseConfig error!");
}
this.config.setPluginPath(pluginPath);
init(this.config);
//文件监控,实现热更新
ExecutorService schedulerExecuter = Executors.newSingleThreadExecutor(new ThreadFactory() {
@Override
public Thread newThread(Runnable runnable) {
Thread thread = Executors.defaultThreadFactory().newThread(runnable);
thread.setName("TransformFileListener-Thread");
thread.setDaemon(false);
return thread;
}
});
schedulerExecuter.submit(new Runnable() {
@Override
public void run() {
TransformFileWatch transformFileWatch = new TransformFileWatch(pluginPath, 5);
logger.info("begin transformFileWatch...");
transformFileWatch.monitor(new TransformFileListener(new TransformFileListenerCallback() {
@Override
public void onFileChange(File file) {
try {
if (reloading) {
logger.info("正在更新jar,请勿太频繁更新");
return;
}
reloading = true;
config = mysqlConfigParser.parseConfig();
config.setPluginPath(pluginPath);
init(config);
} catch (Exception e) {
logger.error("", e);
} finally {
reloading = false;
}
}
}));
}
});
}
private void init(AppConfig config) {
long startTime = System.currentTimeMillis();
ConcurrentHashMap<String, PluginDefinition> newPluginDefinitionMap = new ConcurrentHashMap();
//加载pluginPath下的jar
classLoader = new TransformClassLoader(config.getPluginPath());
classLoaderHistoryCount++;
//注册pluginName,此时并未初始化plugin实例,plugin实例将在getPlugin时初始化
List<PluginDefinition> pds = config.getPlugins();
if (pds != null && pds.size() > 0) {
for (PluginDefinition pd : pds) {
newPluginDefinitionMap.put(pd.getPluginName(), pd);
}
}
pluginDefinitionMap.putAll(newPluginDefinitionMap);
Map<String, Class<? extends BaseTransformer>> newPluginClassMap = initPluginClassMap(newPluginDefinitionMap);
pluginHistoryCount += newPluginClassMap.size();
pluginClassMap.putAll(newPluginClassMap);
logger.info("init spends={}ms classLoader={} `classLoaderHistoryCount={} `pluginHistoryCount={}", System.currentTimeMillis() - startTime, classLoader.toString(), classLoaderHistoryCount, pluginHistoryCount);
logger.info("pluginDefinitionMapSize={} `pluginClassSize={} `pluginClassMap={}", pluginDefinitionMap.size(), pluginClassMap.size(), pluginClassMap);
}
private Map<String, Class<? extends BaseTransformer>> initPluginClassMap(Map<String, PluginDefinition> newPluginDefinitionMap) {
ConcurrentHashMap<String, Class<? extends BaseTransformer>> newPluginClassMap = new ConcurrentHashMap<>();
for (Map.Entry<String, PluginDefinition> entry : newPluginDefinitionMap.entrySet()) {
Class<? extends BaseTransformer> plugin = createPlugin(entry.getValue().getPluginClass());
if (plugin == null) {
logger.error("initPluginClassMap pluginName={} can not create a plugin", entry.getKey());
continue;
}
newPluginClassMap.put(entry.getKey(), plugin);
}
return newPluginClassMap;
}
@Override
protected Class<? extends BaseTransformer> loadPluginClass(String clazz) throws ClassNotFoundException {
return (Class<? extends BaseTransformer>) classLoader.loadClass(clazz);
}
}
插件工厂接口TransformFactory
/**
* Created by itbasketplayer on 2020/7/10.
*/
public interface TransformFactory {
/**
* 通过插件全限定名获取插件实例
* 注意使用规范,每次获取实例都从此接口获取,并使用final
* 1、保证jar更新后能使用新的实例
* 2、旧引用不扩散,避免引发内存泄露
*
* @param transformName
* @return
*/
Class<? extends BaseTransformer> getTransformerClass(String transformName);
Map<String, Class<? extends BaseTransformer>> getTransformerClassMap();
}
插件工厂抽象类AbstractTransformFactory
/**
* 插件工厂抽象类
* Created by itbasketplayer on 2020/7/10.
*/
public abstract class AbstractTransformFactory implements TransformFactory {
private static Logger logger = LoggerFactory.getLogger(AbstractTransformFactory.class);
protected ConcurrentHashMap<String, PluginDefinition> pluginDefinitionMap = new ConcurrentHashMap<>();
protected ConcurrentHashMap<String, Class<? extends BaseTransformer>> pluginClassMap = new ConcurrentHashMap<>();
public Class<? extends BaseTransformer> getTransformerClass(String pluginName) {
//必须在插件注册表,或者之前在注册表,后下线的
PluginDefinition definition = pluginDefinitionMap.get(pluginName);
if (definition == null) {
logger.error("not found pluginName={} in db configs", pluginName);
return null;
}
return pluginClassMap.get(pluginName);
}
public Map<String, Class<? extends BaseTransformer>> getTransformerClassMap() {
return pluginClassMap;
}
protected Class<? extends BaseTransformer> createPlugin(String pluginClass) {
try {
Class<? extends BaseTransformer> clazz = loadPluginClass(pluginClass);
return clazz;
} catch (Exception e) {
throw new PluginException("not found transformer class:" + pluginClass, e);
}
}
protected abstract Class<? extends BaseTransformer> loadPluginClass(String clazz) throws ClassNotFoundException;
}
最后
以上就是风趣纸飞机为你收集整理的java自定义插件实现的全部内容,希望文章能够帮你解决java自定义插件实现所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复