一 需求背景

每天需要定时的进行各种姿势的数据校验,而这些姿势的叠加层出不穷,如果每增加一个小姿势都要进行测试部署上线,十分不值得。
于是我们决定将代码搬到数据库里面,可以随时随地增加不同的“校验姿势”。
注意:这样的作法虽然可以很便捷的上代码,但是生产环境上还是不建议这样做,不安全。

二 步骤

  1. 在项目中先定义一个checker接口,这个接口便是我们动态代码class的父类。
  2. 定义一个数据库表,形式如下:
-- 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';
  1. 有了代码存放的位置,就可以开始加载啦,我定义了两个核心方法,loadBean即使用javaCode和className动态编译加载并load到spring容器中;invoke则是提供了一个操作该bean的方式(这个不要也罢),具体可看下方DynamicBeanHandle/DynamicBeanHandlerImplr。
  2. 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),可以参考。
  3. loadBean已经介绍完了,什么时候进行loadBean则是看大家的需求,我这里直接用定时任务来获取dcc_class表的所有数据,判断不存在时加载。
  4. 关于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;
    }
Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐