什么?还能这样操作 ---Jvm类加载器系统和运行时修改字节码最近开发过程中遇到一个老代码优化的需求,为了提高代码的可
大家好,我是Alioth
前言
最近开发过程中遇到一个老代码优化的需求,为了提高代码的可读性和减少一些冗余的类。我负责改动ORM相关的的内容,在优化过程中发现了很多关于XXXJsonTypeHandler的类,都是用于自动序列化和反序列化的处理类
@Data
@TableName(autoResultMap = true)
public class User {
@TableId
private Long id;
// 用于处理Json类型的TypeHandler
@TableField(typeHandler = UserDetailTypeHandler.class)
private List<UserDetail> details;
@TableField(typeHandler = UserDetailTypeHandler.class)
private Info info;
}
泛型TypeHandler
但是有很多带泛型的类比如List<T> Map<K,V>,在反序列化的时候就会出现问题,因为Java泛型的类型擦除,运行时类型都会变成Object类
需要通反射获取 GenericType 或者 GenericSuperclass 拿到真正的运行时类型,正确的完成反序列化,不然虽然看起来反序列成功了,但是存的值却是Map,一取出来就抛出ClassCastException所以才会出现大量这种特化的TypeHandler,均是继承ListTypeHandler实现的,Map泛型的也相差无几
public abstract class ListTypeHandler<T> extends BaseTypeHandler<List<T>> {
private static final ObjectMapper OBJECT_MAPPER = SpringUtil.getBean(ObjectMapper.class);
public static <T> T parseObject(String text, TypeReference<T> typeReference) {
if (StringUtils.isBlank(text)) {
return null;
}
try {
return OBJECT_MAPPER.readValue(text, typeReference);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, List<T> parameter, JdbcType jdbcType)
throws SQLException {
String content = CollUtil.isEmpty(parameter) ? null : JSON.toJSONString(parameter);
ps.setString(i, content);
}
@Override
public List<T> getNullableResult(ResultSet rs, String columnName) throws SQLException {
return this.getListByJsonArrayString(rs.getString(columnName));
}
@Override
public List<T> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return this.getListByJsonArrayString(rs.getString(columnIndex));
}
@Override
public List<T> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return this.getListByJsonArrayString(cs.getString(columnIndex));
}
private List<T> getListByJsonArrayString(String content) {
return StrUtil.isBlank(content) ? new ArrayList<>() : parseObject(content, specificType());
}
/**
* 具体类型,由子类提供
*
* @return 具体类型
*/
protected abstract TypeReference<List<T>> specificType();
}
List<UserDetail> TypeHandler这样实现是为了传递父类的泛型信息用于正确实现JSON反序列化
public class UserDetailHandler extends ListTypeHandler<UserDetail> {
@Override
protected TypeReference<List<UserDetail>> specificType() {
return new TypeReference<List<UserDetail>>() {
};
}
}
统一处理
要进行自动反序列化泛型对象的就得拿到他的Field对象然后调用getGenericType获取到实际的ParameterizedTypeImpl交给Jackson或者FastJson进行正确的解析了
TableFieldInfo的修改
MP中负责实体类映射会在TableFieldInfo中,特化查询的ResultMap下次再讲,我这只是用一部分进行解释
我们首先对Mybatis-Plus的TableFieldInfo类快速浏览一遍,发现获取TypeHandler的方法主要是在getResultMapping方法中
ResultMapping getResultMapping(final Configuration configuration) {
ResultMapping.Builder builder = new ResultMapping.Builder(configuration, property,
StringUtils.getTargetColumn(column), propertyType);
TypeHandlerRegistry registry = configuration.getTypeHandlerRegistry();
if (jdbcType != null && jdbcType != JdbcType.UNDEFINED) {
builder.jdbcType(jdbcType);
}
if (typeHandler != null && typeHandler != UnknownTypeHandler.class) {
TypeHandler<?> typeHandler = registry.getMappingTypeHandler(this.typeHandler);
if (typeHandler == null) {
// 懒加载获取TypeHandler
typeHandler = registry.getInstance(propertyType, this.typeHandler);
// 我们需要在此加入获取字段信息的逻辑
}
builder.typeHandler(typeHandler);
}
return builder.build();
}
突然我们发现需要加的逻辑会对修改库的class文件,我们当然可以把lib jar里的class改了再重新打包然后引入我们fix后的版本,但是我并没有这么操作,因为因为我们后续如果解决其他问题后还是要对MP的版本进行无感升级的,只需修改版本号而不是修改依赖坐标
类加载系统
要想实现运行时修改字节码只能从类加载系统着手了,我们都知道类加载是基于双亲委派机制实现的,上下级关系如下图所示,常见的Tomcat就是打破双亲委派机制实现对不同WebApp的类隔离的
TableFieldInfo类加载的拦截
要想实现修改类信息我们就得拦截其加载过程,避免我们的类直接从Classpath中加载,而是从我们规定的地方获取对应的字节码,这就少不了自定义类加载器了,我的实现如下 Patcher的接口定义,用于实现策略模式拦截指定类
public interface Patcher {
boolean support(String clazz);
Class<?> patch(String clazz, Definer definer);
@FunctionalInterface
interface Definer {
Class<?> defineClass(String name, byte[] b, int off, int len);
}
}
自定义的类加载器PatchClassLoader
用于Patch字节码的类加载器
@Slf4j
public class PatchClassLoader extends ClassLoader {
public PatchClassLoader(ClassLoader parent) {
super(parent);
System.err.println("Load Parent Classloader: " + parent); // 打印日志信息
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
for (Patcher p : patchers) { // patchers 来源于另外的启动类的静态变量 后续会提到
if (p.support(name)) { //利用策略模式实现
try {
return p.patch(name, this::defineClass);
} catch (Exception e) {
log.error("{} Error:", "Err Load Patcher " + name, e);
break;
}
}
}
// 对于没有需要被修改的类需要由当前类加载器进行修改
// 不然不会讲修改后的类进行正确加载和执行的
try {
if (isNeedPatchClass(name)) {
Class<?> c = findLoadedClass(name);
if (c == null) {
String clazz = getInnerName(name);
InputStream is = getParent().getResourceAsStream(clazz);
if (is == null) {
return super.loadClass(name, resolve);
}
byte[] buf = IoUtil.readBytes(is);
c = defineClass(name, buf, 0, buf.length);
}
return c;
}
} catch (Exception e) {
System.err.println("Break Error " + e);
}
// 共享类和java lang类由上级类加载器进行加载就可以了
return super.loadClass(name, resolve);
}
// 对指定的包下的类打破双亲委派机制 由自定义类加载器进行加载
private boolean isNeedPatchClass(String clazz) {
// 这些包均对要修改的类由使用关系才需打破双亲委派 反过来的就不需要
return clazz.startsWith("com.xxx.app") || clazz.startsWith("com.baomidou.mybatisplus");
}
// 获取resource路径名称
private String getInnerName(String clazz) {
return clazz.replaceAll("\.", "/") + ".class";
}
}
启动类实现
在这我使用了SPI获取接口下对应的实现,进行了一个简单的分离,SPI的使用我就不多赘述
@Slf4j
public class PatchLauncher {
public static PatchClassLoader loader;
static final ArrayList<Patcher> patchers = new ArrayList<>();
public void addPatcher(Patcher p) {
patchers.add(p);
}
public static void run(String clazz, String[] args) {
try {
//
setupSpiPatchers(); // 加载SPI实现
//
initJarMode();
Class<?> clz = Class.forName(clazz, true, loader);
invokeMainMethod(clz, args);
} catch (Exception e) {
rethrow(e);
}
}
private static Method getMainMethod(Class<?> clazz) throws NoSuchMethodException {
return clazz.getMethod("main", String[].class);
}
// 替代主方法的启动开始,用于修改启动类的类加载器
private static void invokeMainMethod(Class<?> clazz, String[] args) {
try {
Method method = getMainMethod(clazz);
method.setAccessible(true);
method.invoke(null, new Object[]{args});
} catch (Exception e) {
rethrow(e);
}
}
// 获取完SPI实现后再修改线程上下文类加载器
// 设置上级类加载器
private static void initJarMode() {
ClassLoader tx = Thread.currentThread().getContextClassLoader();
ClassLoader parent = tx == null ? PatchLauncher.class.getClassLoader() : tx;
loader = new PatchClassLoader(parent);
Thread.currentThread().setContextClassLoader(loader);
}
// 获取对应SPI实现 SPI实现类的类加载器由线程上下文类加载器确定
private static void setupSpiPatchers() {
ServiceLoader<Patcher> loader = ServiceLoader.load(Patcher.class);
for (Patcher patcher : loader) {
patchers.add(patcher);
}
}
@SuppressWarnings("unchecked")
private static <T extends Exception, R> R rethrow(Exception e) throws T {
throw (T) e;
}
}
Patcher的实现
对于Patcher的实现 利用ASM框架和Javassist进行字节码的修改
public class TableFieldInfoPatcher implements Patcher, Opcodes {
@Override
public boolean support(String clazz) {
return clazz.equals("com.baomidou.mybatisplus.core.metadata.TableFieldInfo");
}
@Override
public Class<?> patch(String clazz, Definer definer) {
ClassLoader loader = getClass().getClassLoader();
try (InputStream is = loader.getResourceAsStream(clazz.replace(".", "/") + ".class")) {
byte[] bytes = process(clazz, is);
return definer.defineClass(clazz, bytes, 0, bytes.length);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private byte[] process(String clazz, InputStream is) throws IOException {
ClassReader reader = new ClassReader(is);
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
String clz = clazz.replace(".", "/");
// injectPatchField 的实现
reader.accept(new ClassVisitor(ASM9, writer) {
@Override
public void visitEnd() {
writer.visitField(Opcodes.ACC_PRIVATE, "fieldSetter", "Ljava/lang/reflect/Method;", null, null).visitEnd();
MethodVisitor setter = writer.visitMethod(ACC_PRIVATE, "injectPatchField", "(Lorg/apache/ibatis/type/TypeHandler;)V", "(Lorg/apache/ibatis/type/TypeHandler<*>;)V", null);
setter.visitCode();
Label label0 = new Label();
Label label1 = new Label();
Label label2 = new Label();
setter.visitTryCatchBlock(label0, label1, label2, "java/lang/Exception");
setter.visitLabel(label0);
setter.visitVarInsn(ALOAD, 1);
setter.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
setter.visitVarInsn(ASTORE, 2);
Label label3 = new Label();
setter.visitLabel(label3);
setter.visitVarInsn(ALOAD, 2);
setter.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getName", "()Ljava/lang/String;", false);
setter.visitLdcInsn("IGenericJsonHandler");
setter.visitMethodInsn(INVOKEVIRTUAL, "java/lang/String", "endsWith", "(Ljava/lang/String;)Z", false);
setter.visitJumpInsn(IFEQ, label1);
Label label4 = new Label();
setter.visitLabel(label4);
setter.visitVarInsn(ALOAD, 0);
setter.visitFieldInsn(GETFIELD, clz, "fieldSetter", "Ljava/lang/reflect/Method;");
Label label5 = new Label();
setter.visitJumpInsn(IFNONNULL, label5);
Label label6 = new Label();
setter.visitLabel(label6);
setter.visitVarInsn(ALOAD, 0);
setter.visitVarInsn(ALOAD, 2);
setter.visitLdcInsn("setFieldInfo");
setter.visitInsn(ICONST_1);
setter.visitTypeInsn(ANEWARRAY, "java/lang/Class");
setter.visitInsn(DUP);
setter.visitInsn(ICONST_0);
setter.visitLdcInsn(Type.getType("Ljava/lang/reflect/Field;"));
setter.visitInsn(AASTORE);
setter.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Class", "getMethod", "(Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;", false);
setter.visitFieldInsn(PUTFIELD, clz, "fieldSetter", "Ljava/lang/reflect/Method;");
setter.visitLabel(label5);
setter.visitFrame(Opcodes.F_APPEND, 1, new Object[]{"java/lang/Class"}, 0, null);
setter.visitVarInsn(ALOAD, 0);
setter.visitFieldInsn(GETFIELD, clz, "fieldSetter", "Ljava/lang/reflect/Method;");
setter.visitVarInsn(ALOAD, 1);
setter.visitInsn(ICONST_1);
setter.visitTypeInsn(ANEWARRAY, "java/lang/Object");
setter.visitInsn(DUP);
setter.visitInsn(ICONST_0);
setter.visitVarInsn(ALOAD, 0);
setter.visitFieldInsn(GETFIELD, clz, "field", "Ljava/lang/reflect/Field;");
setter.visitInsn(AASTORE);
setter.visitMethodInsn(INVOKEVIRTUAL, "java/lang/reflect/Method", "invoke", "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;", false);
setter.visitInsn(POP);
setter.visitLabel(label1);
setter.visitFrame(Opcodes.F_CHOP, 1, null, 0, null);
Label label7 = new Label();
setter.visitJumpInsn(GOTO, label7);
setter.visitLabel(label2);
setter.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[]{"java/lang/Exception"});
setter.visitVarInsn(ASTORE, 2);
Label label8 = new Label();
setter.visitLabel(label8);
setter.visitTypeInsn(NEW, "java/lang/RuntimeException");
setter.visitInsn(DUP);
setter.visitVarInsn(ALOAD, 2);
setter.visitMethodInsn(INVOKESPECIAL, "java/lang/RuntimeException", "<init>", "(Ljava/lang/Throwable;)V", false);
setter.visitInsn(ATHROW);
setter.visitLabel(label7);
setter.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
setter.visitInsn(RETURN);
Label label9 = new Label();
setter.visitLabel(label9);
setter.visitLocalVariable("clz", "Ljava/lang/Class;", "Ljava/lang/Class<*>;", label3, label1, 2);
setter.visitLocalVariable("e", "Ljava/lang/Exception;", null, label8, label7, 2);
setter.visitLocalVariable("this", "L" + clz + ";", null, label0, label9, 0);
setter.visitLocalVariable("handler", "Lorg/apache/ibatis/type/TypeHandler;", "Lorg/apache/ibatis/type/TypeHandler<*>;", label0, label9, 1);
setter.visitMaxs(7, 3);
setter.visitEnd();
super.visitEnd();
}
}, 0);
byte[] buf = writer.toByteArray();
try {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(buf));
CtMethod ctMethod = ctClass.getDeclaredMethod("getResultMapping");
ctMethod.setBody("{ " +
" org.apache.ibatis.mapping.ResultMapping.Builder builder = new org.apache.ibatis.mapping.ResultMapping.Builder($1, this.property, com.baomidou.mybatisplus.core.toolkit.StringUtils.getTargetColumn(this.column), this.propertyType);\n" +
" org.apache.ibatis.type.TypeHandlerRegistry registry = $1.getTypeHandlerRegistry();\n" +
" if (this.jdbcType != null && this.jdbcType != org.apache.ibatis.type.JdbcType.UNDEFINED) {\n" +
" builder.jdbcType(this.jdbcType);\n" +
" }\n" +
"\n" +
" if (this.typeHandler != null && this.typeHandler != org.apache.ibatis.type.UnknownTypeHandler.class) {\n" +
" org.apache.ibatis.type.TypeHandler typeHandler = registry.getMappingTypeHandler(this.typeHandler);\n" +
" if (typeHandler == null) {\n" +
" typeHandler = registry.getInstance(this.propertyType, this.typeHandler);\n" +
" this.injectPatchField(typeHandler);\n" +
" }\n" +
"\n" +
" builder.typeHandler(typeHandler);\n" +
" }\n" +
"\n" +
" return builder.build();" +
"}");
return ctClass.toBytecode();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
翻译成可阅读的实现就是这样 新增的injectPatchField方法
private Method fieldSetter;
private void injectPatchField(TypeHandler<?> handler) {
try {
Class<?> clz = handler.getClass();
if (clz.getName().endsWith("IJsonHandler")) { // 针对我业务场景中专有的处理器
if (this.fieldSetter == null) {
this.fieldSetter = clz.getMethod("setFieldInfo", Field.class);
}
this.fieldSetter.invoke(handler, this.field);
}
} catch (Exception var3) {
Exception e = var3;
throw new RuntimeException(e);
}
}
修改后的getResultMapping方法
ResultMapping getResultMapping(final Configuration configuration) {
ResultMapping.Builder var2 = new ResultMapping.Builder(configuration, this.property, StringUtils.getTargetColumn(this.column), this.propertyType);
TypeHandlerRegistry var3 = configuration.getTypeHandlerRegistry();
if (this.jdbcType != null && this.jdbcType != JdbcType.UNDEFINED) {
var2.jdbcType(this.jdbcType);
}
if (this.typeHandler != null && this.typeHandler != UnknownTypeHandler.class) {
TypeHandler var4 = var3.getMappingTypeHandler(this.typeHandler);
if (var4 == null) {
var4 = var3.getInstance(this.propertyType, this.typeHandler);
this.injectPatchField(var4);
}
var2.typeHandler(var4);
}
return var2.build();
}
启动类的编写
public class Application {
public static void main(String[] args) {
// 由PatchClassLoader加载待执行的类 实现类加载器的传递
PatchLauncher.run("com.alioth.xxx.SpringApplication", args);
}
}
根据上面的一个简单小Demo啊,就实现了一个在运行时修改字节码加载的过程,这是非常的Amazing的啊,熟悉和利用好JVM类加载系统的过程就能更加灵活的运用到生产中去,目前运行还没有出现问题,后续的话这样也不是长久之计,目前MP的新版本已经是支持泛型字段的JSON反序列的,只是我们当前版本下升级带来的影响会大于上述方案,后续修复其他问题后就不会存在上述这种写法了
预期结果
经过测试后发现与预期一致,能够在实例化TypeHandler后执行注入字段的方法代码,并能完成正确的泛型化类型
总结
通过上述的例子我们就能打破双亲委派从而加载出我们魔改过后的类而不是直接从ClassPath中读取然后加载,缺点也是很明显的,有一些特殊的类会出现问题,这种方法只是提供了一种思路去实现加载时修改字节码实现
本人水平有限,文中若有纰漏还望交流指正
参考实现
转载自:https://juejin.cn/post/7420343712872284201