java 修改源码_一个Java运行时修改代码的框架

0x0 背景

项目开发中,经常会遇到项目运行中出现问题,此时如果想要紧急修复或者bug排查,往往束手无策;

为了解决以上问题,作者基于spring和javassist开发了一款可以在运行时修改业务逻辑的框架,实现了在项目运行中修改指定的方法、在方法中插入代码、替换指定的class。

0x1 用法

项目已发布在maven中央仓库

com.github.gx304419380

spring-hook

1.0.0

0x2 web页面

访问/spring-hook路径,进入dashboard,可以看到当前项目中所有的bean

1753acb06fa9

20210304092939727.png

选择要修改的bean,可以进入bean详情,进行替换bean或者修改指定方法

1753acb06fa9

20210304092954343.png

点击指定方法,进入方法修改页面

1753acb06fa9

20210304093004903.png

0x3 原理

核心原理是使用javassist生成新的class,然后反射生成新的bean替换掉spring容器中原有的bean,代码如下:

//查找原始的bean信息

BeanInfo beanInfo = getBeanInfo(beanName);

//根据传入的新class生成一个老class的子类

Class> subClass = generateClassByFile(beanInfo, classInputStream);

//替换bean

replaceBean(beanName, subClass);

其中,generateClassByFile方法如下:

/**

* 将新的类继承要替换的类

*

* @param beanInfo bean信息

* @param classInputStream 类文件输入流

* @return 新类

*/

private Class> generateClassByFile(BeanInfo beanInfo, InputStream classInputStream)

throws NotFoundException, IOException, CannotCompileException {

String name = beanInfo.getTargetClass().getName();

if (name.contains(CLASS_POSTFIX)) {

name = name.substring(0, name.indexOf(CLASS_POSTFIX));

}

// 类库池, jvm中所加载的class

ClassPool pool = ClassPool.getDefault();

CtClass father = pool.get(name);

CtClass sub = pool.makeClass(classInputStream);

validateClass(sub, father);

sub.setName(name + CLASS_POSTFIX + COUNTER.getAndIncrement());

sub.setSuperclass(father);

handleConstructors(sub);

return sub.toClass(beanInfo.getBeanClassLoader(), null);

}

replaceBean方法如下:

/**

* 调用spring框架替换bean

*

* @param beanName beanName

* @param subClass 新的bean类型

*/

private void replaceBean(String beanName, Class> subClass) {

//移除旧的bean

DefaultListableBeanFactory beanFactory = applicationContext.getDefaultListableBeanFactory();

beanFactory.removeBeanDefinition(beanName);

//定义新的bean definition并注册到spring

AbstractBeanDefinition bd = BeanDefinitionBuilder.genericBeanDefinition(subClass).getBeanDefinition();

beanFactory.registerBeanDefinition(beanName, bd);

//生成bean

Object bean = beanFactory.getBean(beanName);

log.info("refresh bean finish: {}", bean);

//对于controller需要特殊处理,生成request mapping

handleRequestMapping(beanName);

//刷新缓存

CACHE.remove(beanName);

}

还可以修改指定的方法,根据方法生成新的class,代码如下:

/**

* 根据传入的方法字符串,生成一个新的bean class

*

* @param beanInfo bean信息

* @param dto hook信息

* @return 新class

*/

private Class> generateClassByMethod(BeanInfo beanInfo, HookMethodDto dto) throws NotFoundException, CannotCompileException {

String name = beanInfo.getTargetClass().getName();

if (name.contains(CLASS_POSTFIX)) {

name = name.substring(0, name.indexOf(CLASS_POSTFIX));

}

// 类库池, jvm中所加载的class

ClassPool pool = ClassPool.getDefault();

CtClass father = pool.get(name);

CtClass sub = pool.getAndRename(name, name + CLASS_POSTFIX + COUNTER.getAndIncrement());

sub.setSuperclass(father);

handleConstructors(sub);

CtMethod ctMethod = findMethod(sub, dto.getMethodName(), dto.getArgClassList());

switch (dto.getHookMethodType()) {

case REPLACE:

sub.removeMethod(ctMethod);

//生成新的方法

CtMethod method = CtNewMethod.make(dto.getMethodCode(), sub);

sub.addMethod(method);

break;

case BEFORE:

ctMethod.insertBefore(dto.getMethodCode());

break;

case AFTER:

ctMethod.insertAfter(dto.getMethodCode());

break;

case FINALLY:

ctMethod.insertAfter(dto.getMethodCode(), true);

break;

default:

break;

}

return sub.toClass(beanInfo.getBeanClassLoader(), null);

}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值