基于Java动态编译实现springboot项目动态加载class文件的一些经历和思考
因为技术都是为业务服务的,所有的功能开发出来都需要有他的用途;所以先说下需求是啥样子。这个sql查出来的结果需要经过java或者js或者字典或者三者随意组合,输出修改后的结果集。字典就不展示了。。。可以随意实现 这些就是基本的业务流程了,其实也不难。想要实现这个流程,动态加载js不是问题,很简单,自行百度,5分钟搞定。Java动态加载就没那么容易了。首先我想到的就是反射。大方向是对的,没问题,就用
因为技术都是为业务服务的,所有的功能开发出来都需要有他的用途;
所以先说下需求是啥样子。
这个sql查出来的结果需要经过java或者js或者字典或者三者随意组合,输出修改后的结果集。
字典就不展示了。。。可以随意实现
这些就是基本的业务流程了,其实也不难。
想要实现这个流程,动态加载js不是问题,很简单,自行百度,5分钟搞定。
Java动态加载就没那么容易了。
首先我想到的就是反射。
大方向是对的,没问题,就用反射来实现,
于是写了一版。
public class ClassUtil {
private static final Logger logger = LoggerFactory.getLogger(ClassUtil.class);
private static JavaCompiler compiler;
static{
compiler = ToolProvider.getSystemJavaCompiler();
}
/**
* 获取java文件路径
* @param file
* @return
*/
private static String getFilePath(String file){
int last1 = file.lastIndexOf('/');
int last2 = file.lastIndexOf('\\');
return file.substring(0, last1>last2?last1:last2)+File.separatorChar;
}
/**
* 编译java文件
* @param ops 编译参数
* @param files 编译文件
*/
private static void javac(List<String> ops, String... files){
StandardJavaFileManager manager = null;
try{
manager = compiler.getStandardFileManager(null, null, null);
Iterable<? extends JavaFileObject> it = manager.getJavaFileObjects(files);
JavaCompiler.CompilationTask task = compiler.getTask(null, manager, null, ops, null, it);
task.call();
if(logger.isDebugEnabled()){
for (String file:files){
logger.debug("Compile Java File:" + file);
}
}
}
catch(Exception e){
logger.error(e.getMessage());
}
finally{
if(manager!=null){
try {
manager.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 生成java文件
* @param file 文件名
* @param source java代码
* @throws Exception
*/
private static void writeJavaFile(String file,String source)throws Exception{
if(logger.isDebugEnabled()){
logger.debug("Write Java Source Code to:"+file);
}
BufferedWriter bw = null;
try{
File dir = new File(getFilePath(file));
if(!dir.exists()){
dir.mkdirs();
}
bw = new BufferedWriter(new FileWriter(file));
bw.write(source);
bw.flush();
}
catch(Exception e){
throw e;
}
finally{
if(bw!=null){
bw.close();
}
}
}
/**
* 加载类
* @param name 类名
* @return
*/
private static Class<?> load(String name){
Class<?> cls = null;
ClassLoader classLoader = null;
try{
classLoader = ClassUtil.class.getClassLoader();
cls = classLoader.loadClass(name);
if(logger.isDebugEnabled()){
logger.debug("Load Class["+name+"] by "+classLoader);
}
}
catch(Exception e){
logger.error(e.getMessage());
}
return cls;
}
/**
* 编译代码并加载类
* @param filePath java代码路径
* @param source java代码
* @param clsName 类名
* @param ops 编译参数
* @return
*/
public static Class<?> loadClass(String filePath,String source,String clsName,List<String> ops){
try {
logger.info("filePath:"+filePath);
writeJavaFile( filePath,source);
javac(ops,filePath);
return load(clsName);
}
catch (Exception e) {
logger.error(e.getMessage());
}
return null;
}
/**
* 调用类方法
* @param cls 类
* @param methodName 方法名
* @param paramsCls 方法参数类型
* @param params 方法参数
* @return
*/
public static Object invoke(Class<?> cls,String methodName,Class<?>[] paramsCls,Object[] params){
Object result = null;
try {
Method method = cls.getDeclaredMethod(methodName, paramsCls);
Object obj = cls.newInstance();
result = method.invoke(obj, params);
}
catch (Exception e) {
logger.error(e.getMessage());
}
return result;
}
/**
* 执行JS函数,参数和返回值都是String类型
* @param javaStr 脚本
* @param className 类名
* @param methodName 方法名
* @param parameter 参数
* @return
*/
public Object convertByJava(String javaStr, String className,String methodName, Object parameter) {
StringBuilder sb = new StringBuilder();
sb.append(javaStr);
//设置编译参数
ArrayList<String> ops = new ArrayList<String>();
ops.add("-Xlint:unchecked");
//编译代码,返回class
Class<?> cls = ClassUtil.loadClass(System.getProperty("user.dir")+"\\target\\classes\\"+className+".java", sb.toString(), className, ops);
//执行方法
Object result = ClassUtil.invoke(cls, methodName, new Class[]{List.class}, new Object[]{parameter});
//输出结果
logger.info("parameter:" + JSONObject.toJSONString(parameter));
logger.info("result:" + result);
return result;
}
}
大致的流程是:先生成.java文件---再编译为.class文件---然后loadClass---最后invoke
本地跑main方法,这种写法是没问题的,但是如果要是用idea启动springboot项目,这样就会出现问题。参考这个解决。
这样就解决了。但是新的问题来了,这时如果对java文件进行修改,会发现修改无效,不管怎么改,都不会重新加载重新编译的.class文件。
借此机会,研究了一下java类的加载机制,发现上面方法写的是双亲委派方式,每次加载.class文件的时候,发现字节码已经在jvm内存里存在了,上面那种写法是不行的。
所以如果不想违背双亲委派,就使用classLoader的loadclass方法,如果想违背双亲委派,就自定义classLoader重写findclass方法,改造后的方法如下:
自定义classLoader extends ClassLoader 重写findClass方法
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
public class MyClassLoader extends ClassLoader {
private String rootDir;
public MyClassLoader(String rootDir) {
this.rootDir = rootDir;
}
@Override
protected Class<?> findClass(String className){
Class clazz = this.findLoadedClass(className);
FileChannel fileChannel = null;
WritableByteChannel outChannel = null;
if (null == clazz) {
try {
String classFile = getClassFile(className);
FileInputStream fis = new FileInputStream(classFile);
fileChannel = fis.getChannel();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
outChannel = Channels.newChannel(baos);
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
while (true) {
int i = fileChannel.read(buffer);
if (i == 0 || i == -1) {
break;
}
buffer.flip();
outChannel.write(buffer);
buffer.clear();
}
byte[] bytes = baos.toByteArray();
clazz = defineClass(className, bytes, 0, bytes.length);
} catch (Exception e) {
// ...
}finally {
// ...
}
}
return clazz;
}
/**
* 类文件的完全路径
*/
private String getClassFile(String className) {
return rootDir + "\\" + className.replace('.', '\\') + ".class";
}
}
这样完美解决本地idea启动springboot项目,热加载.java文件。
然后信心满满的将项目打成jar包,发布到测试环境进行测试。新的问题出现了!!!!!
jar包方式运行后,根本无法识别 import com.alibaba.fastjson.JSONObject; 这种引用。
为什呢,因为这个.java文件没有办法用到springboot打承德jar包中的任何依赖,就是单纯的java文件,只能用jdk的基本引用比如java.util什么的。
现在就得研究如何让动态编译的java文件能引用到springboot包中的依赖。
于是我就研究了一下springboot jarinjar。
在IDE中运行springboot 程序 类加载器是JDK自带的,类加载器已经完成了依赖jar的加载,所以编译是没问题的,但是springboot 用的是springboot自己写的LaunchedURLClassLoader。经过查阅资料后,从JavaFileManager入手,因为Java编译器是通过JavaFileManager来加载相关依赖类的。 重写JavaFileManager,使用到了springboot的 jarFile 来读取嵌套jar。
-------------------------------------------------------正确写法分割线------------------------------------------------
下面的才是正确写法!!!!!!!!!!!!!!
springboot的 jarFile 来读取嵌套jar
import com.sun.tools.javac.file.BaseFileObject;
import com.sun.tools.javac.file.JavacFileManager;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.ListBuffer;
import com.sun.tools.javac.util.Log;
import org.springframework.boot.loader.jar.JarFile;
import org.springframework.boot.system.ApplicationHome;
import javax.tools.*;
import javax.tools.JavaFileObject.Kind;
import java.io.*;
import java.lang.reflect.Constructor;
import java.net.*;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.jar.JarEntry;
import java.util.stream.Collectors;
/**
* 把一段Java字符串变成类
*
*/
public class MemoryClassLoader extends URLClassLoader {
private Map<String, byte[]> classBytes = new ConcurrentHashMap<>();
/**
* 单利默认的
*/
private static final MemoryClassLoader defaultLoader = new MemoryClassLoader();
public MemoryClassLoader() {
super(new URL[0], MemoryClassLoader.class.getClassLoader());
}
/**
* 获取默认的类加载器
*
* @return 类加载器对象
*/
public static MemoryClassLoader getInstrance() {
return defaultLoader;
}
/**
* 注册Java 字符串到内存类加载器中
*
* @param className 类名字
* @param javaStr Java字符串
*/
public void registerJava(String className, String javaStr) {
try {
this.classBytes.putAll(compile(className, javaStr));
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 自定义Java文件管理器
*
* @param var1
* @param var2
* @param var3
* @return
*/
public static SpringJavaFileManager getStandardFileManager(DiagnosticListener<? super JavaFileObject> var1, Locale var2, Charset var3) {
Context var4 = new Context();
var4.put(Locale.class, var2);
if (var1 != null) {
var4.put(DiagnosticListener.class, var1);
}
PrintWriter var5 = var3 == null ? new PrintWriter(System.err, true) : new PrintWriter(new OutputStreamWriter(System.err, var3), true);
var4.put(Log.outKey, var5);
return new SpringJavaFileManager(var4, true, var3);
}
/**
* 编译Java代码
*
* @param className 类名字
* @param javaStr Java代码
* @return class 二进制
*/
private static Map<String, byte[]> compile(String className, String javaStr) {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager stdManager = getStandardFileManager(null, null, null);
try (MemoryJavaFileManager manager = new MemoryJavaFileManager(stdManager)) {
JavaFileObject javaFileObject = manager.makeStringSource(className, javaStr);
JavaCompiler.CompilationTask task = compiler.getTask(null, manager, null, null, null, Arrays.asList(javaFileObject));
if (task.call()) {
return manager.getClassBytes();
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
byte[] buf = classBytes.get(name);
if (buf == null) {
return super.findClass(name);
}
classBytes.remove(name);
return defineClass(name, buf, 0, buf.length);
}
/**
* 开放findClass 给外部使用
*
* @param name classname
* @return class对象
*/
public Class<?> getClass(String name) throws ClassNotFoundException {
return this.findClass(name);
}
/**
* 获取jar包所在路径
*
* @return jar包所在路径
*/
public static String getPath() {
ApplicationHome home = new ApplicationHome(MemoryJavaFileManager.class);
String path = home.getSource().getPath();
return path;
}
/**
* 判断是否jar模式运行
*
* @return
*/
public static boolean isJar() {
return getPath().endsWith(".jar");
}
}
/**
* 内存Java文件管理器
* 用于加载springboot boot info lib 下面的依赖资源
*/
class MemoryJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> {
// compiled classes in bytes:
final Map<String, byte[]> classBytes = new HashMap<String, byte[]>();
final Map<String, List<JavaFileObject>> classObjectPackageMap = new HashMap<>();
private JavacFileManager javaFileManager;
/**
* key 包名 value javaobj 主要给jdk编译class的时候找依赖class用
*/
public final static Map<String, List<JavaFileObject>> CLASS_OBJECT_PACKAGE_MAP = new HashMap<>();
private static final Object lock = new Object();
private static boolean isInit = false;
public void init() {
try {
String jarBaseFile = MemoryClassLoader.getPath();
JarFile jarFile = new JarFile(new File(jarBaseFile));
List<JarEntry> entries = jarFile.stream().filter(jarEntry -> {
return jarEntry.getName().endsWith(".jar");
}).collect(Collectors.toList());
JarFile libTempJarFile = null;
List<JavaFileObject> onePackgeJavaFiles = null;
String packgeName = null;
for (JarEntry entry : entries) {
libTempJarFile = jarFile.getNestedJarFile(jarFile.getEntry(entry.getName()));
if (libTempJarFile.getName().contains("tools.jar")) {
continue;
}
Enumeration<JarEntry> tempEntriesEnum = libTempJarFile.entries();
while (tempEntriesEnum.hasMoreElements()) {
JarEntry jarEntry = tempEntriesEnum.nextElement();
String classPath = jarEntry.getName().replace("/", ".");
if (!classPath.endsWith(".class") || jarEntry.getName().lastIndexOf("/") == -1) {
continue;
} else {
packgeName = classPath.substring(0, jarEntry.getName().lastIndexOf("/"));
onePackgeJavaFiles = CLASS_OBJECT_PACKAGE_MAP.containsKey(packgeName) ? CLASS_OBJECT_PACKAGE_MAP.get(packgeName) : new ArrayList<>();
onePackgeJavaFiles.add(new MemorySpringBootInfoJavaClassObject(jarEntry.getName().replace("/", ".").replace(".class", ""),
new URL(libTempJarFile.getUrl(), jarEntry.getName()), javaFileManager));
CLASS_OBJECT_PACKAGE_MAP.put(packgeName, onePackgeJavaFiles);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
isInit = true;
}
MemoryJavaFileManager(JavaFileManager fileManager) {
super(fileManager);
this.javaFileManager = (JavacFileManager) fileManager;
}
public Map<String, byte[]> getClassBytes() {
return new HashMap<String, byte[]>(this.classBytes);
}
@Override
public void flush() throws IOException {
}
@Override
public void close() throws IOException {
classBytes.clear();
}
public List<JavaFileObject> getLibJarsOptions(String packgeName) {
synchronized (lock) {
if (!isInit) {
init();
}
}
return CLASS_OBJECT_PACKAGE_MAP.get(packgeName);
}
@Override
public Iterable<JavaFileObject> list(Location location,
String packageName,
Set<Kind> kinds,
boolean recurse)
throws IOException {
if ("CLASS_PATH".equals(location.getName()) && MemoryClassLoader.isJar()) {
List<JavaFileObject> result = getLibJarsOptions(packageName);
if (result != null) {
return result;
}
}
Iterable<JavaFileObject> it = super.list(location, packageName, kinds, recurse);
if (kinds.contains(Kind.CLASS)) {
final List<JavaFileObject> javaFileObjectList = classObjectPackageMap.get(packageName);
if (javaFileObjectList != null) {
if (it != null) {
for (JavaFileObject javaFileObject : it) {
javaFileObjectList.add(javaFileObject);
}
}
return javaFileObjectList;
} else {
return it;
}
} else {
return it;
}
}
@Override
public String inferBinaryName(Location location, JavaFileObject file) {
if (file instanceof MemoryInputJavaClassObject) {
return ((MemoryInputJavaClassObject) file).inferBinaryName();
}
return super.inferBinaryName(location, file);
}
@Override
public JavaFileObject getJavaFileForOutput(Location location, String className, Kind kind,
FileObject sibling) throws IOException {
if (kind == Kind.CLASS) {
return new MemoryOutputJavaClassObject(className);
} else {
return super.getJavaFileForOutput(location, className, kind, sibling);
}
}
JavaFileObject makeStringSource(String className, final String code) {
String classPath = className.replace('.', '/') + Kind.SOURCE.extension;
return new SimpleJavaFileObject(URI.create("string:///" + classPath), Kind.SOURCE) {
@Override
public CharBuffer getCharContent(boolean ignoreEncodingErrors) {
return CharBuffer.wrap(code);
}
};
}
void makeBinaryClass(String className, final byte[] bs) {
JavaFileObject javaFileObject = new MemoryInputJavaClassObject(className, bs);
String packageName = "";
int pos = className.lastIndexOf('.');
if (pos > 0) {
packageName = className.substring(0, pos);
}
List<JavaFileObject> javaFileObjectList = classObjectPackageMap.get(packageName);
if (javaFileObjectList == null) {
javaFileObjectList = new LinkedList<>();
javaFileObjectList.add(javaFileObject);
classObjectPackageMap.put(packageName, javaFileObjectList);
} else {
javaFileObjectList.add(javaFileObject);
}
}
class MemoryInputJavaClassObject extends SimpleJavaFileObject {
final String className;
final byte[] bs;
MemoryInputJavaClassObject(String className, byte[] bs) {
super(URI.create("string:///" + className.replace('.', '/') + Kind.CLASS.extension), Kind.CLASS);
this.className = className;
this.bs = bs;
}
@Override
public InputStream openInputStream() {
return new ByteArrayInputStream(bs);
}
public String inferBinaryName() {
return className;
}
}
class MemoryOutputJavaClassObject extends SimpleJavaFileObject {
final String className;
MemoryOutputJavaClassObject(String className) {
super(URI.create("string:///" + className.replace('.', '/') + Kind.CLASS.extension), Kind.CLASS);
this.className = className;
}
@Override
public OutputStream openOutputStream() {
return new FilterOutputStream(new ByteArrayOutputStream()) {
@Override
public void close() throws IOException {
out.close();
ByteArrayOutputStream bos = (ByteArrayOutputStream) out;
byte[] bs = bos.toByteArray();
classBytes.put(className, bs);
makeBinaryClass(className, bs);
}
};
}
}
}
/**
* 用来读取springboot的class
*/
class MemorySpringBootInfoJavaClassObject extends BaseFileObject {
private final String className;
private URL url;
MemorySpringBootInfoJavaClassObject(String className, URL url, JavacFileManager javacFileManager) {
super(javacFileManager);
this.className = className;
this.url = url;
}
@Override
public Kind getKind() {
return JavaFileObject.Kind.valueOf("CLASS");
}
@Override
public URI toUri() {
try {
return url.toURI();
} catch (URISyntaxException e) {
e.printStackTrace();
}
return null;
}
@Override
public String getName() {
return className;
}
@Override
public InputStream openInputStream() {
try {
return url.openStream();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
@Override
public OutputStream openOutputStream() throws IOException {
return null;
}
@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
return null;
}
@Override
public Writer openWriter() throws IOException {
return null;
}
@Override
public long getLastModified() {
return 0;
}
@Override
public boolean delete() {
return false;
}
public String inferBinaryName() {
return className;
}
@Override
public String getShortName() {
return className.substring(className.lastIndexOf("."));
}
@Override
protected String inferBinaryName(Iterable<? extends File> iterable) {
return className;
}
@Override
public boolean equals(Object o) {
return false;
}
@Override
public int hashCode() {
return 0;
}
@Override
public boolean isNameCompatible(String simpleName, Kind kind) {
return false;
}
}
/**
* java 文件管理器 主要用来 重新定义class loader
*/
class SpringJavaFileManager extends JavacFileManager {
public SpringJavaFileManager(Context context, boolean b, Charset charset) {
super(context, b, charset);
}
@Override
public ClassLoader getClassLoader(Location location) {
nullCheck(location);
Iterable var2 = this.getLocation(location);
if (var2 == null) {
return null;
} else {
ListBuffer var3 = new ListBuffer();
Iterator var4 = var2.iterator();
while (var4.hasNext()) {
File var5 = (File) var4.next();
try {
var3.append(var5.toURI().toURL());
} catch (MalformedURLException var7) {
throw new AssertionError(var7);
}
}
return this.getClassLoader((URL[]) var3.toArray(new URL[var3.size()]));
}
}
@Override
protected ClassLoader getClassLoader(URL[] var1) {
ClassLoader var2 = this.getClass().getClassLoader();
try {
Class loaderClass = Class.forName("org.springframework.boot.loader.LaunchedURLClassLoader");
Class[] var4 = new Class[]{URL[].class, ClassLoader.class};
Constructor var5 = loaderClass.getConstructor(var4);
return (ClassLoader) var5.newInstance(var1, var2);
} catch (Throwable var6) {
}
return new URLClassLoader(var1, var2);
}
}
搞定。
最后说一点,服务器上跑jar包的时候需要加上 -Xbootclasspath/a:$toolspath/tools.jar 这个启动参数来支持,因为Java编译器是通过JavaFileManager来加载相关依赖类的,而JavaFileManager来自tools.jar,所以不加会无法编译哦。
示例:nohup java -jar -Xbootclasspath/a:$toolspath/tools.jar:$toolspath/rt.jar:$toolspath/jce.jar XXXX.jar
更多推荐
所有评论(0)