一 需求背景
每天需要定时的进行各种姿势的数据校验,而这些姿势的叠加层出不穷,如果每增加一个小姿势都要进行测试部署上线,十分不值得。
于是我们决定将代码搬到数据库里面,可以随时随地增加不同的“校验姿势”。
注意:这样的作法虽然可以很便捷的上代码,但是生产环境上还是不建议这样做,不安全。
二 步骤
- 在项目中先定义一个checker接口,这个接口便是我们动态代码class的父类。
- 定义一个数据库表,形式如下:
-- Dynamic Code Compiler
DROP TABLE IF EXISTS `dcc_class`;
CREATE TABLE `dcc_class` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`bean_name` varchar(255) NOT NULL COMMENT '加载到spring的beanName,同时也是className(注:必须与javaCode中的classname保持一致)',
`java_code` text COMMENT '显而易见,这里就是具体的java代码,要注意的时,这里的代码时一个完整的class,并且是实现了我们上述Checker接口的class',
`method_name` varchar(255) DEFAULT NULL COMMENT '默认被执行的方法(其他方法也可以执行,但是更建议一个类一个方法)',
`updated_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`created_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_bean_name` (`bean_name`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT 'Dynamic Code Compiler';
- 有了代码存放的位置,就可以开始加载啦,我定义了两个核心方法,loadBean即使用javaCode和className动态编译加载并load到spring容器中;invoke则是提供了一个操作该bean的方式(这个不要也罢),具体可看下方DynamicBeanHandle/DynamicBeanHandlerImplr。
- loadBean方法是重点,在里面有一些细节,接下来展开:
a. FileUtils.createTempFileWithFileNameAndContent是自定义的工具类,作用其实只是在临时目录下创建一个指定文件名的文件,并且将javacode写入。这么做的原因是接下来使用的java编译工具只支持路径,而不能直接将javaCode传入,而这里不使用随机文件名是因为在java中public的class name要和文件名相同
b. JavaCompiler便是真正的编译器了,compiler.run(…)本质上,就是用cmd执行了一个javac命令。(这里需要注意,运行时要引入com.sun.tools.jar,这个包属于jdk却不被jre包含)
c. 现在.java文件编译成.class了,紧接着需要使用classLoader加载,但是这里要注意jvm本身自带的几种classloader都无法加载我们的自定义class的,因为它不处于任何一个classloader的加载目录,所以我们需要自定义一个,我使用了MyClassLoader,如下。
d. loadClass成功,我们便拿到了Class对象,CLass对象转BeanDefinition,再用Spring的DefaultListableBeanFactory加载。
之前刚好写过一篇(Spring重新加载bean),可以参考。 - loadBean已经介绍完了,什么时候进行loadBean则是看大家的需求,我这里直接用定时任务来获取dcc_class表的所有数据,判断不存在时加载。
- 关于invoke方法,依赖了Spring的ApplicationContext容器,因为我们之前已经将代码注入到了容器里面,这里就可以直接根据beanName来拿然后执行方法。
三 代码集
public interface DynamicBeanHandler {
/**
*
* @param javaCode java代码
* @param beanName beanName(同时也是classname),注意:beanName必须与javaCode中的className保持一致
* @throws Exception
*/
void loadBean(String javaCode, String beanName) throws Exception;
/**
* 无参方法执行
* @param beanName
* @param methodName
* @return
*/
Object invoke(String beanName, String methodName);
/**
* 有参方法执行
* @param beanName
* @param methodName
* @param args demo : new Object[]{value}
* @param parameterTypes demo : new Class[]{Object.class}
* @return
*/
Object invoke(String beanName, String methodName,Object[] args, Class<?>[] parameterTypes) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException;
}
@Slf4j
@Service
public class DynamicBeanHandlerImpl implements DynamicBeanHandler {
private ConcurrentHashMap<String, BeanTTL> cacheBean = new ConcurrentHashMap<>();
@Autowired
private DccClassService dccClassService;
@Autowired
private ApplicationContextUtil applicationContextUtil;
@SneakyThrows
@Override
public void loadBean(String javaCode, String beanName) {
// TODO 格式校验,检查javaCode是否合法,是否public class name与beanName一致等。
log.info("loadBean,compile {} start",beanName);
File file = FileUtils.createTempFileWithFileNameAndContent(beanName, ".java",javaCode.getBytes());
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
int result = compiler.run(null, null, null, file.getAbsolutePath());
if(result==0){
log.info("{} {}",beanName,"-编译成功");
}else{
throw new ClassCompilerException(String.format("动态编译失败,className %s", beanName));
}
log.info("loadBean,loadClass {} start, to {}",beanName,file.getParent());
URL[] urls = new URL[]{new URL("file:/"+file.getParent()+"/")};
URLClassLoader loader = new MyClassLoader(urls, Thread.currentThread().getContextClassLoader());
Class c = loader.loadClass(beanName);
log.info("loadBean,loadClass {} end",beanName);
log.info("loadBean,inject bean to IOC, {} start",beanName);
applicationContextUtil.injectBean(beanName,c);
log.info("loadBean,inject bean to IOC, {} end",beanName);
cacheBean.put(beanName,
BeanTTL.builder().updatedTime(Instant.now()).bean(ApplicationContextUtil.getBean(beanName)).build());
}
@Override
public Object invoke(String beanName, String methodName) {
if (find(beanName)) return null;
try {
Object bean = ApplicationContextUtil.getBean(beanName);
return MethodUtils.invokeExactMethod(bean,methodName);
} catch (NoSuchMethodException|IllegalAccessException| InvocationTargetException e) {
log.info("executeMethod failed,{}::{}",beanName,methodName);
log.info("executeMethod errMsg : {}",e);
}
return null;
}
@Override
public Object invoke(String beanName, String methodName, Object[] args, Class<?>[] parameterTypes) throws InvocationTargetException, NoSuchMethodException, IllegalAccessException {
if (find(beanName) && ApplicationContextUtil.getBean(beanName)==null) return null;
try {
Object bean = ApplicationContextUtil.getBean(beanName);
return MethodUtils.invokeExactMethod(bean,methodName,args,parameterTypes);
} catch (NoSuchMethodException|IllegalAccessException e) {
log.info("executeMethod failed,{}::{}",beanName,methodName);
log.info("executeMethod errMsg : {}",e);
throw e;
}
}
private boolean find(String beanName) {
if(!cacheBean.containsKey(beanName)){
String javaCode = getJavaCodeByBeanName(beanName);
if(StringUtils.isBlank(javaCode)){
log.info("executeMethod loadBean failed,javaCode not found by beanName {}",beanName);
return false;
}
try {
loadBean(javaCode,beanName);
} catch (Exception e) {
log.info("executeMethod loadBean failed,{}::{}",beanName);
log.info("executeMethod loadBean errMsg : {}",e);
throw new BeanNotFoundException(String.format("执行bean找不到,且无法加载,beanName %s", beanName));
}
}
return true;
}
private String getJavaCodeByBeanName(String beanName) {
DccClass dccClass = dccClassService.getByBeanName(beanName);
if(dccClass!=null){
return dccClass.getJavaCode();
}
return null;
}
@Scheduled(cron = "0/10 * * * * ?")
public void syncFromDB() {
// 一个select,就不放出来了
List<DccClass> dccClasses = dccClassService.getAll();
dccClasses.forEach(dccClass -> {
if(!cacheBean.containsKey(dccClass.getBeanName()) || (cacheBean.get(dccClass.getBeanName()).getUpdatedTime().isBefore(dccClass.getUpdatedTime()))){
loadBean(dccClass.getBeanName(),dccClass.getJavaCode());
}
});
}
}
public class MyClassLoader extends URLClassLoader {
public MyClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
}
public static File createTempFileWithFileNameAndContent(String beanName,String suffix,byte[] content) throws IOException {
String tempDir=System.getProperty("java.io.tmpdir");
File file = new File(tempDir+"/"+beanName+suffix);
OutputStream os = new FileOutputStream(file);
os.write(content, 0, content.length);
os.flush();
os.close();
return file;
}