从dubbo到javassist
从dubbo到 javassist
众所周知,dubbo是一个微内核+插件的架构。其中运用到了SPI机制,达到外部插件动态扩展功能的作用,但是dubbo并没有使用java原有的SPI,而是自己实现了一套更高效、功能更强大的SPI。网上有很多介绍Dubbo SPI的帖子,这里不做赘述。
自适应扩展机制 之 Adaptive 标注在接口方法上
(还有其它自适应方式,这里不做介绍)
Dubbo SPI 功能中有个有意思的功能:自适应扩展机制中,可以使用Adaptive注解,标识在接口方法上,然后Dubbo就会根据注解、参数等信息生成一个代理。默认采用javassist生成代理。
重点就是在 javassist 生成代理的方式 非常有意思,是先拼接好java代码字符串,然后编译为class,在构建实例。
例如:给如下接口生成代理:
会先根据已有信息使用StringBuilder 拼接一个java文件代码如下
然后使用javassist 编译,编译为Class后,实例化为一个代理对象。
所以javassist 可以运行时编译字符串,加载到内存,使其成为Class对象
不使用javassist,使用JDK自带的工具竟然也可以!
在dubbo源码中,我们也可以看见相关测试用例
@Test
public void testCompileJavaClass() throws Exception {
JavassistCompiler compiler = new JavassistCompiler();
Class<?> clazz = compiler.compile(getSimpleCode(), JavassistCompiler.class.getClassLoader());
// Because javassist compiles using the caller class loader, we should't use HelloService directly
Object instance = clazz.newInstance();
Method sayHello = instance.getClass().getMethod("sayHello");
Assertions.assertEquals("Hello world!", sayHello.invoke(instance));
}
String getSimpleCode() {
StringBuilder code = new StringBuilder();
code.append("package org.apache.dubbo.common.compiler.support;");
code.append("public class HelloServiceImpl" + SUFFIX.getAndIncrement() + " implements HelloService {");
code.append(" public String sayHello() { ");
code.append(" return \"Hello world!\"; ");
code.append(" }");
code.append("}");
return code.toString();
}
那在 B/S 网站项目中,是不是可以用户在浏览器输入代码,直接影响到服务器的代码执行效果呢?
我复制修改了dubbo的一些代码试了一下,确实是可以的
JavassistController
package run.wolfgang.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import run.wolfgang.config.JavassistCompiler;
import run.wolfgang.entity.req.CompileReq;
import java.lang.reflect.Method;
@RestController
@Slf4j
public class JavassistController {
private Class<?> aClass;
@PostMapping("/compile")
@ResponseBody
public String compile(@RequestBody CompileReq compileReq) {
JavassistCompiler javassistCompiler = new JavassistCompiler();
try {
aClass = javassistCompiler.doCompile(compileReq.getName(), compileReq.getSource());
return "ok";
} catch (Throwable e) {
log.error("",e);
}
return "error";
}
@GetMapping("/run")
public String run() {
try {
Object instance = aClass.newInstance();
Method sayHello = instance.getClass().getMethod("sayHello");
Object invoke = sayHello.invoke(instance);
return (String) invoke;
} catch (Throwable e) {
log.error("",e);
}
return "error";
}
}
JavassistCompiler
package run.wolfgang.config;
import javassist.CtClass;
import run.wolfgang.config.compiler.ClassUtils;
import run.wolfgang.config.compiler.CtClassBuilder;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class JavassistCompiler {
public static final String NAME = "javassist";
private static final Pattern IMPORT_PATTERN = Pattern.compile("import\\s+([\\w\\.\\*]+);\n");
private static final Pattern EXTENDS_PATTERN = Pattern.compile("\\s+extends\\s+([\\w\\.]+)[^\\{]*\\{\n");
private static final Pattern IMPLEMENTS_PATTERN = Pattern.compile("\\s+implements\\s+([\\w\\.]+)\\s*\\{\n");
private static final Pattern METHODS_PATTERN = Pattern.compile("\n(private|public|protected)\\s+");
private static final Pattern FIELD_PATTERN = Pattern.compile("[^\n]+=[^\n]+;");
public Class<?> doCompile(String name, String source) throws Throwable {
CtClassBuilder builder = new CtClassBuilder();
builder.setClassName(name);
// process imported classes
Matcher matcher = IMPORT_PATTERN.matcher(source);
while (matcher.find()) {
builder.addImports(matcher.group(1).trim());
}
// process extended super class
matcher = EXTENDS_PATTERN.matcher(source);
if (matcher.find()) {
builder.setSuperClassName(matcher.group(1).trim());
}
// process implemented interfaces
matcher = IMPLEMENTS_PATTERN.matcher(source);
if (matcher.find()) {
String[] ifaces = matcher.group(1).trim().split("\\,");
Arrays.stream(ifaces).forEach(i -> builder.addInterface(i.trim()));
}
// process constructors, fields, methods
String body = source.substring(source.indexOf('{') + 1, source.length() - 1);
String[] methods = METHODS_PATTERN.split(body);
String className = ClassUtils.getSimpleClassName(name);
Arrays.stream(methods).map(String::trim).filter(m -> !m.isEmpty()).forEach(method -> {
if (method.startsWith(className)) {
builder.addConstructor("public " + method);
} else if (FIELD_PATTERN.matcher(method).matches()) {
builder.addField("private " + method);
} else {
builder.addMethod("public " + method);
}
});
// compile
ClassLoader classLoader = ClassUtils.getCallerClassLoader(getClass());
CtClass cls = builder.build(classLoader);
return cls.toClass(classLoader, JavassistCompiler.class.getProtectionDomain());
}
}
ClassUtils
package run.wolfgang.config.compiler;
/**
* ClassUtils. (Tool, Static, ThreadSafe)
*/
public class ClassUtils {
private static final int JIT_LIMIT = 5 * 1024;
private ClassUtils() {
}
public static Class<?> forName(String[] packages, String className) {
try {
return classForName(className);
} catch (ClassNotFoundException e) {
if (packages != null && packages.length > 0) {
for (String pkg : packages) {
try {
return classForName(pkg + "." + className);
} catch (ClassNotFoundException ignore) {
}
}
}
throw new IllegalStateException(e.getMessage(), e);
}
}
public static ClassLoader getCallerClassLoader(Class<?> caller) {
return caller.getClassLoader();
}
public static Class<?> forName(String className) {
try {
return classForName(className);
} catch (ClassNotFoundException e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
public static Class<?> classForName(String className) throws ClassNotFoundException {
switch (className) {
case "boolean":
return boolean.class;
case "byte":
return byte.class;
case "char":
return char.class;
case "short":
return short.class;
case "int":
return int.class;
case "long":
return long.class;
case "float":
return float.class;
case "double":
return double.class;
case "boolean[]":
return boolean[].class;
case "byte[]":
return byte[].class;
case "char[]":
return char[].class;
case "short[]":
return short[].class;
case "int[]":
return int[].class;
case "long[]":
return long[].class;
case "float[]":
return float[].class;
case "double[]":
return double[].class;
default:
}
try {
return arrayForName(className);
} catch (ClassNotFoundException e) {
// try to load from java.lang package
if (className.indexOf('.') == -1) {
try {
return arrayForName("java.lang." + className);
} catch (ClassNotFoundException ignore) {
// ignore, let the original exception be thrown
}
}
throw e;
}
}
private static Class<?> arrayForName(String className) throws ClassNotFoundException {
return Class.forName(className.endsWith("[]")
? "[L" + className.substring(0, className.length() - 2) + ";"
: className, true, Thread.currentThread().getContextClassLoader());
}
/**
* get simple class name from qualified class name
*/
public static String getSimpleClassName(String qualifiedName) {
if (null == qualifiedName) {
return null;
}
int i = qualifiedName.lastIndexOf('.');
return i < 0 ? qualifiedName : qualifiedName.substring(i + 1);
}
}
CtClassBuilder
package run.wolfgang.config.compiler;
import javassist.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* CtClassBuilder is builder for CtClass
* <p>
* contains all the information, including:
* <p>
* class name, imported packages, super class name, implemented interfaces, constructors, fields, methods.
*/
public class CtClassBuilder {
private String className;
private String superClassName = "java.lang.Object";
private final List<String> imports = new ArrayList<>();
private final Map<String, String> fullNames = new HashMap<>();
private final List<String> ifaces = new ArrayList<>();
private final List<String> constructors = new ArrayList<>();
private final List<String> fields = new ArrayList<>();
private final List<String> methods = new ArrayList<>();
public String getClassName() {
return className;
}
public void setClassName(String className) {
this.className = className;
}
public String getSuperClassName() {
return superClassName;
}
public void setSuperClassName(String superClassName) {
this.superClassName = getQualifiedClassName(superClassName);
}
public List<String> getImports() {
return imports;
}
public void addImports(String pkg) {
int pi = pkg.lastIndexOf('.');
if (pi > 0) {
String pkgName = pkg.substring(0, pi);
this.imports.add(pkgName);
if (!pkg.endsWith(".*")) {
fullNames.put(pkg.substring(pi + 1), pkg);
}
}
}
public List<String> getInterfaces() {
return ifaces;
}
public void addInterface(String iface) {
this.ifaces.add(getQualifiedClassName(iface));
}
public List<String> getConstructors() {
return constructors;
}
public void addConstructor(String constructor) {
this.constructors.add(constructor);
}
public List<String> getFields() {
return fields;
}
public void addField(String field) {
this.fields.add(field);
}
public List<String> getMethods() {
return methods;
}
public void addMethod(String method) {
this.methods.add(method);
}
/**
* get full qualified class name
*
* @param className super class name, maybe qualified or not
*/
protected String getQualifiedClassName(String className) {
if (className.contains(".")) {
return className;
}
if (fullNames.containsKey(className)) {
return fullNames.get(className);
}
return ClassUtils.forName(imports.toArray(new String[0]), className).getName();
}
/**
* build CtClass object
*/
public CtClass build(ClassLoader classLoader) throws NotFoundException, CannotCompileException {
ClassPool pool = new ClassPool(true);
pool.appendClassPath(new LoaderClassPath(classLoader));
// create class
CtClass ctClass = pool.makeClass(className, pool.get(superClassName));
// add imported packages
imports.forEach(pool::importPackage);
// add implemented interfaces
for (String iface : ifaces) {
ctClass.addInterface(pool.get(iface));
}
// add constructors
for (String constructor : constructors) {
ctClass.addConstructor(CtNewConstructor.make(constructor, ctClass));
}
// add fields
for (String field : fields) {
ctClass.addField(CtField.make(field, ctClass));
}
// add methods
for (String method : methods) {
ctClass.addMethod(CtNewMethod.make(method, ctClass));
}
return ctClass;
}
}
然后调用controller 编辑
curl --location 'localhost:8080/compile' \
--header 'Content-Type: application/json' \
--data '{
"name": "Test1",
"source": "package run.wolfgang;\npublic class Test1 {\npublic String sayHello(){\nreturn \"hello2\";\n}}"
}
'
运行发现
curl --location 'localhost:8080/run'
返回"hello2"
但是会有一个问题 -- 无法重复编译同一个class
重复调用上面的 /compile api会报错
解决方式:自定义一个ClassLoader,每次编译时重新实例化一个自定义ClassLoader。将上述代码进行修改。
- 添加自定义ClassLoader
package run.wolfgang.config;
public class MyClassLoader extends ClassLoader{
}
- 修改 JavassistCompiler 的 doCompile方法
仅仅将
ClassLoader classLoader = ClassUtils.getCallerClassLoader(getClass());
修改为:
MyClassLoader classLoader = new MyClassLoader();
public Class<?> doCompile(String name, String source) throws Throwable {
CtClassBuilder builder = new CtClassBuilder();
builder.setClassName(name);
// process imported classes
Matcher matcher = IMPORT_PATTERN.matcher(source);
while (matcher.find()) {
builder.addImports(matcher.group(1).trim());
}
// process extended super class
matcher = EXTENDS_PATTERN.matcher(source);
if (matcher.find()) {
builder.setSuperClassName(matcher.group(1).trim());
}
// process implemented interfaces
matcher = IMPLEMENTS_PATTERN.matcher(source);
if (matcher.find()) {
String[] ifaces = matcher.group(1).trim().split("\\,");
Arrays.stream(ifaces).forEach(i -> builder.addInterface(i.trim()));
}
// process constructors, fields, methods
String body = source.substring(source.indexOf('{') + 1, source.length() - 1);
String[] methods = METHODS_PATTERN.split(body);
String className = ClassUtils.getSimpleClassName(name);
Arrays.stream(methods).map(String::trim).filter(m -> !m.isEmpty()).forEach(method -> {
if (method.startsWith(className)) {
builder.addConstructor("public " + method);
} else if (FIELD_PATTERN.matcher(method).matches()) {
builder.addField("private " + method);
} else {
builder.addMethod("public " + method);
}
});
// compile
//ClassLoader classLoader = ClassUtils.getCallerClassLoader(getClass());
MyClassLoader classLoader = new MyClassLoader();
CtClass cls = builder.build(classLoader);
return cls.toClass(classLoader, JavassistCompiler.class.getProtectionDomain());
}
完成!!!
现在可以重复编译相同的Class了。
这样修改的原因是
- ClassLoader在加载一个class后很难移除或者更新(本人网上搜了半天没有找到可行的方案)。
- 但是可以新建ClassLoader实例,然后用新的ClassLoader实例可以重复加载Class(因为JVM判断是否是同一个类的标准就是:相同的ClassLoader && 相同的class文件)
Tomcat 动态加载类是怎么做的呢
在搜索使用ClassLoader如何动态更新或者移除一个class时,我就在想,tomcat动态加载war包及其依赖是怎么做的呢?
我能不能学习人家怎么解决这个问题的呢?
结果人家似乎也是采用了上述的第二种方法:新建ClassLoader实例,重新加载class。
我没有去翻Tomcat源码,但是看见一个相关的帖子写的挺像那么回事的,先贴在这里。
引用帖子
https://cloud.tencent.com/developer/article/1685613
https://www.cnblogs.com/TomSnail/p/4372564.html
如有侵权,请联系本人,本人会在第一时间做出调整