如何使用 Groovy 动态加载 Java 代码为 class 并注册 Spring的bean

616 阅读2分钟

最近在看极客时间的dubbo,大佬写的东西自然而然完善了我对dubbo的认知,但最要命的是让我学到了一些我没有见识到的思路,真的颇有感慨:别学了,越学越发现自己的无知

今天总结一下Groovy 动态加载 Java 代码为 class 并注册 Spring的bean

1. 代码

/**
 * @Author: dingyawu
 * @Description: TODO
 * @Date: Created in 21:31 2023/1/24
 */
public class ExtLoadInfo {

    private String beanName;

    private String scriptBase64;

    public String getBeanName() {
        return beanName;
    }

    public void setBeanName(String beanName) {
        this.beanName = beanName;
    }

    public String getScriptBase64() {
        return scriptBase64;
    }

    public void setScriptBase64(String scriptBase64) {
        this.scriptBase64 = scriptBase64;
    }
}



@Component
public class GroovyLoader implements ApplicationContextAware {

    private static final GroovyClassLoader groovyClassLoader;
    private ApplicationContext ctx;

    public static final String UTF_8 = "UTF-8";

    private static Map<String, String> extLoadMap = new ConcurrentHashMap<>(16);

    static {
        CompilerConfiguration config = new CompilerConfiguration();
        config.setSourceEncoding(UTF_8);
        groovyClassLoader = new GroovyClassLoader(Thread.currentThread().getContextClassLoader(), config);
    }


    public Boolean existBeanName(String beanName, String scriptBase64){
        if (!extLoadMap.containsKey(beanName)){
            return false;
        }
        String scriptBase64Value = extLoadMap.get(beanName);
        if (Objects.equals(scriptBase64, scriptBase64Value)){
            return false;
        }
        return true;
    }

    public Object getBean(String beanName, String scriptBase64){
        // 1. 尝试看 beanName 是否已经加载过,从 spring 容器中看
        Object bean = getBeanInner(beanName);
        if (bean != null) {
            return bean;
        }

        // 2. 针对java code进行编译,主要得到class返参结果
        Class<?> clz = complie(scriptBase64);

        // 3. 将class创建bean定义交给spring容器来实例化
        applyClz2Spring(beanName, clz);

        // 4. 再次从容器中获取beanName对应的实例返回
        bean = getBeanInner(beanName);
        extLoadMap.put(beanName, scriptBase64);
        return bean;
    }

    private Object getBeanInner(String beanName) {
        try {
            return this.ctx.getBean(beanName);
        } catch (BeansException e) {
            e.printStackTrace();
        }
        return null;
    }

    private Class<?> complie(String scriptBase64) {
        String script = new String(Base64.decodeBase64(scriptBase64), Charsets.UTF_8);
        return groovyClassLoader.parseClass(script);
    }

    private void applyClz2Spring(String beanName, Class<?> clz) {
        AbstractBeanDefinition definition = BeanDefinitionBuilder.genericBeanDefinition(clz).getRawBeanDefinition();
        ((BeanDefinitionRegistry)((AbstractApplicationContext)ctx).getBeanFactory()).registerBeanDefinition(beanName, definition);
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.ctx = applicationContext;
    }
}


@RestController
public class GroovyController {

    @Autowired
    private GroovyLoader groovyLoader;

    @RequestMapping("/groovy")
    public String test(@RequestBody ExtLoadInfo extLoadInfo){
        String beanName = extLoadInfo.getBeanName();
        String scriptBase64 = extLoadInfo.getScriptBase64();
        Boolean existBeanName = groovyLoader.existBeanName(beanName, scriptBase64);
        if (existBeanName){
            throw new RuntimeException("beanName 已存在,请重新传参beanName:" + beanName);
        }
        Object instance = groovyLoader.getBean(beanName, scriptBase64);
        ILoader loader = (ILoader) instance;
        return loader.getLoaderName();
    }
}


public interface ILoader {

    public String getLoaderName();
}


2. 测试代码


postman 调用:http://localhost:8080/groovy
{
    "beanName": "roy1",
    "scriptBase64":"cGFja2FnZSBjb20uaG1pbHkuc3ByaW5nLnNhbXBsZXMubG9hZGVyOwoKCnB1YmxpYyBjbGFzcyBFeHRDbGFzc0xvYWRlciBpbXBsZW1lbnRzIElMb2FkZXJ7CgoKICAgIEBPdmVycmlkZQogICAgcHVibGljIFN0cmluZyBnZXRMb2FkZXJOYW1lKCkgewogICAgICAgIHJldHVybiAi5oiR6Ieq5bex5YaZ55qE5LiA5Liq5pS55Yqo55qEQ2xhc3NMb2FkZXIhISEiOwogICAgfQp9"

}


result:我自己写的一个改动的ClassLoader!!!

base64在线编码地址:https://base64.us/
选取整个Iloader的实现类,然后去base64 编码



/**
 * 我写的测试用例
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class BootConsumerAPPTests {

   @Autowired
   private ApplicationContext ac;

   @Autowired
   private GroovyController groovyController;

   @Test
   public void contextLoads() {
      Person person = ac.getBean("person", Person.class);
      System.out.println(person.getName());
      Map<String, ILoader> map = ac.getBeansOfType(ILoader.class);
      if (map.isEmpty()){
         ExtLoadInfo extLoadInfo = new ExtLoadInfo();
         extLoadInfo.setBeanName("roy1");
         extLoadInfo.setScriptBase64("cGFja2FnZSBjb20ucm95LmxvYWRlcjsKCgpwdWJsaWMgY2xhc3MgRXh0Q2xhc3NMb2FkZXIgaW1wbGVtZW50cyBJTG9hZGVyewoKICAgIEBPdmVycmlkZQogICAgcHVibGljIFN0cmluZyBnZXRMb2FkZXJOYW1lKCkgewogICAgICAgIHJldHVybiAi5oiR6Ieq5bex5YaZ55qE5LiA5Liq5pS55Yqo55qEQ2xhc3NMb2FkZXIhISEiOwogICAgfQp9Cg==");
         String test = groovyController.test(extLoadInfo);
         Assert.assertTrue(Objects.equals(test, "我自己写的一个改动的ClassLoader!!!"));
      } else {
         Assert.fail();
      }



   }

}

3. 思路

1. 先判断入参的bean有没有被占用

2. groovy动态加载成为字节码

3. 加载到容器中

4. 获取bean强转为接口,进行调用