最近在看极客时间的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强转为接口,进行调用