Dynamic compilation and loading of JAVA code, and then instantiated beans are injected into the Spring container

A demand background

It is necessary to check the data of various postures regularly every day, and the superposition of these postures is endless. It is not worthwhile to test and deploy every time a small posture is added.
So we decided to move the code to the database, and we can add different "check postures" anytime and anywhere.
Note: Although this approach can be very convenient to upload the code, it is not recommended in the production environment, and it is not safe.

Two steps

  1. First define a checker interface in the project, this interface is the parent class of our dynamic code class.
  2. Define a database table in the following form:
-- 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. Once the code is stored, you can start loading. I defined two core methods. LoadBean uses javaCode and className to dynamically compile and load and load into the spring container; invoke provides a way to manipulate the bean (this Don't worry), see DynamicBeanHandle/DynamicBeanHandlerImplr below for details.
  2. The loadBean method is the key point. There are some details in it. Next, expand:
    a. FileUtils.createTempFileWithFileNameAndContent is a custom tool class, which actually only creates a file with a specified file name in the temporary directory and writes javacode. The reason for this is that the java compiler tool used next only supports the path, and cannot directly pass in javaCode, and the random file name is not used here because the public class name in java must be the same as the file name
    b. JavaCompiler is The real compiler is, compiler.run(...) essentially executes a javac command with cmd. (It should be noted here that com.sun.tools.jar should be introduced at runtime, this package belongs to jdk but is not included by jre )
    c. Now the .java file is compiled into .class, and then you need to use classLoader to load it, but you should pay attention here The several classloaders that come with the jvm itself cannot load our custom class because it is not in the loading directory of any classloader, so we need to customize one. I used MyClassLoader, as follows.
    d. The loadClass is successful, we get the Class object, the CLass object is converted to BeanDefinition, and then loaded with Spring's DefaultListableBeanFactory.
    I just wrote an article before ( Spring reload bean ), you can refer to it.
  3. LoadBean has been introduced. When to loadBean is based on your needs, I will directly use timing tasks to get all the data in the dcc_class table, and load it when it is judged that it does not exist.
  4. Regarding the invoke method, it relies on Spring's ApplicationContext container, because we have injected the code into the container before, here we can directly take and execute the method based on the beanName.

Three code sets

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;
    }