总览
本文通过自定义classload实现加载指定的插件类,并运行的功能。通过使用java的security机制,降低加载的插件对系统造成破坏性的可能,比如需要限制插件调用System.exit(),调用获取jvm系统参数,读写文件和建立远程连接等。
插件示例
插件接口定义
/**
* Description 公开的插件接口,有插件提供方实现
* Author
* Date 2022/7/14 13:53
*/
public interface PlugOpenApi {
/**
*插件的启动方法
* @return
*/
int start();
/**
* 插件的停机方法
* @return
*/
int shutdown();
}
上游使用者给定的实现示例,其中包含系统推出的调用
/**
* Description 插件的示例实现
* Author
* Date 2022/7/14 13:55
*/
public class HelloWordPlugDemo implements PlugOpenApi {
ScheduledExecutorService service = new ScheduledThreadPoolExecutor(1);
/**
* 插件实现,延迟一秒打印hello word 然后调用系统 GC 和 关闭系统(模拟对系统的破坏)
*
* @return
*/
@Override
public int start() {
service.schedule(() -> System.out.println("hello word")
, 1L, TimeUnit.SECONDS);
System.gc();
System.out.println("gc调用完成");
System.exit(3);
System.out.println("系统关机调用完成");
return 1;
}
@Override
public int shutdown() {
service.shutdown();
return 0;
}
}
简化实现的类加载器
/**
* Description
* Author
* Date 2022/7/14 14:05
*/
public class PlugClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> aClass = null;
try {
aClass = this.getParent().loadClass(name);
} catch (ClassNotFoundException e) {
}
if (aClass != null) {
return aClass;
}
int len = 0;
byte[] data = null;
String path = name;
if (name.contains(".")){
path = name.replace(".","/");
}
try (FileInputStream fis = new FileInputStream("D://tmp" + "/" + path
+ ".class")) {
len = fis.available();
data = new byte[len];
fis.read(data);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return defineClass(name, data, 0, data.length);
}
}
简化的插件指定加载和运行机制
/**
* 运行插件的一个入口
* Description
* Author
* Date 2022/7/14 13:52
*/
public class Demo {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, InterruptedException {
PlugClassLoader plugClassLoader = new PlugClassLoader();
Class<PlugOpenApi> helloWordPlugDemo = (Class<PlugOpenApi>) plugClassLoader.loadClass("experiment.classload.plug.HelloWordPlugDemo");
PlugOpenApi plugOpenApi = helloWordPlugDemo.newInstance();
plugOpenApi.start();
TimeUnit.SECONDS.sleep(10);
plugOpenApi.shutdown();
System.out.println("插件运行结束");
}
}
单是这样的机制,直接运行导致整个jvm由于插件的调用exit(3)而退出。主要是在生产环境会是一个灾难的后果
启用java安全管理security
jvm的安全管理,默认是关闭。通过添加jvm参数启用,参数如下:
-Djava.security.manager
然而默认的java安全策略,不允许程序创建classload,不允许操作文件系统(应该是不能任一一个目录)
调整默认权限
修改jre\lib\security\java.policy文件,添加授权。如果不清楚文件中都能怎么设置。可通过jre/bin/policytool.exe 的图形界面程序辅助生成你需要的权限语句,将生成的条件语句添加到java.policy中指定的位置就好。
比如我要求jvm能够创建classload 能够读取文件系统,追加的权限内容如下:
然后重写插件的运行机制:
/**
* 运行插件的一个入口
* Description
* Author
* Date 2022/7/14 13:52
*/
public class DemoPermission {
public static void main(String[] args) throws InstantiationException, IllegalAccessException, InterruptedException, ClassNotFoundException {
CodeSource nullSource = new CodeSource(null, (CodeSigner[]) null);
PermissionCollection noPerms = new Permissions();
ProtectionDomain domain = new ProtectionDomain(nullSource, noPerms);
AccessControlContext safeContext = new AccessControlContext(new ProtectionDomain[]{domain});
PlugClassLoader plugClassLoader = new PlugClassLoader();
Class<PlugOpenApi> helloWordPlugDemo = (Class<PlugOpenApi>) plugClassLoader.loadClass("experiment.classload.plug.HelloWordPlugDemo");
PlugOpenApi plugOpenApi = helloWordPlugDemo.newInstance();
try {
AccessController.doPrivileged((PrivilegedAction) () -> {
try {
plugOpenApi.start();
} catch (Exception e) {
e.printStackTrace();
}
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
plugOpenApi.shutdown();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}, safeContext);
} catch (Exception e) {
e.printStackTrace();
}
System.exit(2);
System.out.println("插件运行结束");
}
}
运行结果如下:
插件中的exit(3),和线程池的关闭方法被拒绝,而main中的system.exit(2)能够正常执行。
实现了只限制了插件的权限,二不限制插件之外的权限。
注
本文只是一个简单演示,对里面涉及的理论知识没有阐述。