SpringBoot基于函数替换的热重载

背景

SpringBoot项目每次启动都很慢,有时候调试仅仅是改一点点东西,就要重启工作效率太低,希望能修改完代码,执行快捷键后就把该类的修改生效。(仅限于Bean的修改生效)

原理

SpringBoot的逻辑基本都是集中在ServiceImpl中,然后调用的时候都是通过@Autowired注入后再调用或者是SpringUtil.getBean获取调用。我们只需要把修改后的类使用Groovy生成一个新对象,然后反向注入到@Autowired标记的变量还有放入到singletonObjects容器中即可。但是相同包名和类名的类不能重复,所以我们获取原有类名加上数字区分然后继承原有类再编译即可。例如:要重载XXX类,那就生成XXX2类并且XXX2继承XXX再new一个XXX2对象反向注入即可。

实现

package com.meal.utils;

import com.alibaba.excel.support.cglib.proxy.Enhancer;
import com.alibaba.excel.support.cglib.proxy.MethodInterceptor;
import com.alibaba.excel.support.cglib.proxy.MethodProxy;
import com.meal.system.entity.SysHotfix;
import groovy.lang.GroovyClassLoader;
import lombok.SneakyThrows;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;


public class HotfixUtil {
    public static Map<String, Object> hotfixMap = new ConcurrentHashMap<>();
    static int idAdd = 0;
    //开发使用
    @SneakyThrows
    public static void reloadFile(String filePath) {
        System.out.println("重载:"+filePath);

        File file = new File(filePath);
        // 获取文件名
        String fileNameWithExtension = file.getName();

        // 去掉后缀
        String fileNameWithoutExtension = fileNameWithExtension.substring(0, fileNameWithExtension.lastIndexOf("."));
        String beanName = getBeanName(fileNameWithoutExtension);

        String content = readFile(filePath,fileNameWithoutExtension);



        if(!SpringUtil.containsBean(beanName)){
            System.out.println("不存在bean="+beanName);
            return;
        }


        Class clazz = new GroovyClassLoader().parseClass(content);
        Object o = clazz.newInstance();

        SpringUtil.replaceBean(beanName, o);
    }

    private static String getBeanName(String fileNameWithoutExtension) {
        if(fileNameWithoutExtension.length() <= 2) {
            return Character.toLowerCase(fileNameWithoutExtension.charAt(0)) + fileNameWithoutExtension.substring(1);
        }
        if(Character.isLowerCase(fileNameWithoutExtension.charAt(0))){
            return fileNameWithoutExtension;
        }
        if(Character.isUpperCase(fileNameWithoutExtension.charAt(0)) && Character.isLowerCase(fileNameWithoutExtension.charAt(1))){//第一个大写 第二个小写
            return Character.toLowerCase(fileNameWithoutExtension.charAt(0)) + fileNameWithoutExtension.substring(1);
        }

        return fileNameWithoutExtension;
    }

    public static String readFile(String filePath,String fileNameWithoutExtension) throws IOException {
        StringBuilder content = new StringBuilder();
        try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
            String line;
            boolean isLog=false;
            while ((line = reader.readLine()) != null) {
                if(line.contains("@Slf4j")) {
                    isLog=true;
                }
                if(line.contains("class "+fileNameWithoutExtension)){
                    String newClazz = fileNameWithoutExtension+idAdd;
                    String newLine = line.substring(0,line.indexOf(fileNameWithoutExtension));
                    newLine += newClazz+" extends "+fileNameWithoutExtension;
                    idAdd++;
                    if(line.contains("{")){
                        newLine += " {";
                    }
                    content.append(newLine).append("\n");
                    if(isLog){
                        content.append("Logger log = LoggerFactory.getLogger("+newClazz+".class);").append("\n");
                    }


                } else if(line.contains("package ")){
                    content.append(line).append("\n");

                    String newLine = line.substring(line.indexOf("package ")+8);
                    newLine = newLine.replace(";","");
                    newLine ="import "+newLine+"."+fileNameWithoutExtension+";";
                    content.append(newLine).append("\n");
                    content.append("import org.slf4j.Logger;").append("\n");
                    content.append("import org.slf4j.LoggerFactory;").append("\n");
                }
                else{
                    content.append(line).append("\n");
                }

            }
        }
        return content.toString();
    }

    @SneakyThrows
    public static void reload() {
        synchronized (hotfixMap) {
            for (String beanName : hotfixMap.keySet()) {
                SpringUtil.replaceBean(beanName, hotfixMap.get(beanName));
            }

            List<SysHotfix> sysHotfixList = EntityCrudServiceUtil.list(SysHotfix.class, null);
            for (SysHotfix sysHotfix : sysHotfixList) {
                Class clazz = new GroovyClassLoader().parseClass(sysHotfix.getClazzCode());
                Object o = clazz.newInstance();

                if (!hotfixMap.containsKey(sysHotfix.getBeanName()))
                    hotfixMap.put(sysHotfix.getBeanName(), SpringUtil.getBean(sysHotfix.getBeanName()));


                SpringUtil.replaceBean(sysHotfix.getBeanName(), o);

            }
        }

    }




    public static Object createProxy(final Object target,Object bean) {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(target.getClass());
        enhancer.setCallback(new HotfixMethodInterceptor(bean));
        return enhancer.create();
    }

    public static class HotfixMethodInterceptor implements MethodInterceptor {

        private Object originalObject;

        public HotfixMethodInterceptor(Object originalObject) {
            this.originalObject = originalObject;
        }

        @Override
        public Object intercept(Object object, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            return proxy.invokeSuper(object, args);
        }
    }
}


package com.meal.utils;

import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.yarn.webapp.hamlet.Hamlet;
import org.springframework.aop.framework.AopContext;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.DefaultSingletonBeanRegistry;
import org.springframework.cloud.openfeign.FeignContext;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.*;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;

/**
 * @Author cwj
 * @Version 20211225
 * @desc spring工具类
 */
@Component
@Lazy
public final class SpringUtil implements BeanFactoryPostProcessor, ApplicationContextAware {

    /**
     * Spring应用上下文环境
     */
    private static ConfigurableListableBeanFactory beanFactory;

    private static ApplicationContext applicationContext;

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        SpringUtil.beanFactory = beanFactory;
    }

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

    /**
     * 获取对象
     *
     * @param name
     * @return Object 一个以所给名字注册的bean的实例
     * @throws BeansException
     */
    @SuppressWarnings("unchecked")
    public static <T> T getBean(String name) throws BeansException {
        return (T) beanFactory.getBean(name);
    }

    /**
     * 获取类型为requiredType的对象
     *
     * @param clz
     * @return
     * @throws BeansException
     */
    public static <T> T getBean(Class<T> clz) throws BeansException {
        T result = (T) beanFactory.getBean(clz);
        return result;
    }

    /**
     * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true
     *
     * @param name
     * @return boolean
     */
    public static boolean containsBean(String name) {
        return beanFactory.containsBean(name);
    }

    /**
     * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException)
     *
     * @param name
     * @return boolean
     * @throws NoSuchBeanDefinitionException
     */
    public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException {
        return beanFactory.isSingleton(name);
    }

    /**
     * @param name
     * @return Class 注册对象的类型
     * @throws NoSuchBeanDefinitionException
     */
    public static Class<?> getType(String name) throws NoSuchBeanDefinitionException {
        return beanFactory.getType(name);
    }

    /**
     * 如果给定的bean名字在bean定义中有别名,则返回这些别名
     *
     * @param name
     * @return
     * @throws NoSuchBeanDefinitionException
     */
    public static String[] getAliases(String name) throws NoSuchBeanDefinitionException {
        return beanFactory.getAliases(name);
    }

    /**
     * 获取aop代理对象
     *
     * @param invoker
     * @return
     */
    @SuppressWarnings("unchecked")
    public static <T> T getAopProxy(T invoker) {
        return (T) AopContext.currentProxy();
    }

    /**
     * 获取当前的环境配置,无配置返回null
     *
     * @return 当前的环境配置
     */
    public static String[] getActiveProfiles() {
        return applicationContext.getEnvironment().getActiveProfiles();
    }

    /**
     * 获取当前的环境配置,当有多个环境配置时,只获取第一个
     *
     * @return 当前的环境配置
     */
    public static String getActiveProfile() {
        final String[] activeProfiles = getActiveProfiles();
        return StringUtils.isNoneBlank(activeProfiles) ? activeProfiles[0] : null;
    }

    public static <T> T getFeignBean(String beanName, Class<T> tClass) {
        FeignContext feignContext = applicationContext.getBean("feignContext", FeignContext.class);
        return feignContext.getInstance(beanName, tClass);
    }


    @SneakyThrows
    public static void replaceBean(String beanName, Object targetObj)  {

//        cn.hutool.extra.spring.SpringUtil.unregisterBean(beanName);
//        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(targetObj.getClass());
//        BeanDefinition beanDefinition = beanDefinitionBuilder.getRawBeanDefinition();
//        applicationContext.getAutowireCapableBeanFactory().applyBeanPostProcessorsAfterInitialization(beanDefinition, beanName);
//         ((DefaultListableBeanFactory)beanFactory).registerBeanDefinition(beanName, beanDefinition);
//        applicationContext.getAutowireCapableBeanFactory().autowireBean(targetObj);



//        System.out.println("cccccccccc="+map.size());
//
//        System.out.println("WWWWWWWWWWW="+map.size());

//        applicationContext.getAutowireCapableBeanFactory().autowireBean(targetObj);
        injectAutowiredFields(targetObj);
        //cn.hutool.extra.spring.SpringUtil.unregisterBean(beanName);


        //ConfigurableApplicationContext context = (ConfigurableApplicationContext)applicationContext;
        //DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) context.getBeanFactory();
        //反射获取Factory中的singletonObjects 将该名称下的bean进行替换
        Field singletonObjects = DefaultSingletonBeanRegistry.class.getDeclaredField("singletonObjects");
        singletonObjects.setAccessible(true);
        Map<String, Object> map = (Map<String, Object>) singletonObjects.get(beanFactory);
        //Object bean = map.get(beanName);
//        if(bean != null)
//            cn.hutool.extra.spring.SpringUtil.getConfigurableBeanFactory().destroyBean(bean);

       // cn.hutool.extra.spring.SpringUtil.registerBean(beanName, targetObj);
        //beanFactory.registerSingleton(beanName, targetObj);
        beanFactory.autowireBean(targetObj);

        map.put(beanName, targetObj);
    }

    static Object getTrueSingleton(Object target) {
        Object result = target;
        Object trueBean = AopProxyUtils.getSingletonTarget(target);//跳过代理获取真实对象
        for (int i = 0; i < 10; i++) {
            if(trueBean != null){
                result = trueBean;
                trueBean = AopProxyUtils.getSingletonTarget(trueBean);
            }
        }
        return result;
    }

    private static void injectAutowiredFields(Object target) {
        String[] beanNames = applicationContext.getBeanDefinitionNames();
        for (String beanName : beanNames) {
            Object bean = beanFactory.getBean(beanName);

            Object trueBean = getTrueSingleton(bean);//跳过代理获取真实对象

            Object finalTrueBean = trueBean;

            Object trueTarget = getTrueSingleton(target);//跳过代理获取真实对象


            Object finalTrueTarget = trueTarget;
            ReflectionUtils.doWithFields(trueBean.getClass(), field -> {
                if (field.isAnnotationPresent(Autowired.class)) {

                    if(field.getType().isAssignableFrom(finalTrueTarget.getClass())) {
//                        FactoryBean<?> factoryBean = (FactoryBean<?>) bean;
//                        try {
//                            Object originalObject = factoryBean.getObject();
//                            field.setAccessible(true);
//                            System.out.println("RRRRRRRRRRRRR0="+field.get(originalObject));
//
//                            field.set(originalObject, null);
//
//                            field.set(originalObject, (Object) target);
//                            System.out.println("RRRRRRRRRRRRR1="+field.get(originalObject));
//                        } catch (Exception e) {
//                            e.printStackTrace();
//                        }
                        field.setAccessible(true);
                        field.set(finalTrueBean, target);

                    }

                }
            });

//            if(beanName.contains("webSocketConfig")) {
//                System.out.println("AAAAAAAAA="+beanName);
//            }
            Method[] methods = trueBean.getClass().getMethods();
            for (Method method : methods) {
                if (method.isAnnotationPresent(Autowired.class) && method.getName().startsWith("set")) {
                    // 获取方法的参数类型
                    Class<?>[] parameterTypes = method.getParameterTypes();
                    if (parameterTypes.length == 1) {
                        // 查找相应类型的 bean
                        try {
                            Object autowiredBean = applicationContext.getBean(parameterTypes[0]);
//                            if(beanName.contains("webSocketConfig")) {
//                                System.out.println("BBBBBB="+autowiredBean);
//                            }
                            method.setAccessible(true);
                            // 调用 setter 方法注入 bean
                            method.invoke(finalTrueBean, target);
                        } catch (Exception e) {
//                            if(beanName.contains("webSocketConfig")) {
//                                e.printStackTrace();
//                            }
                        }
                    }
                }
            }


//            System.out.println("EEEEEEE="+beanName);
//            Class<?> beanClass = bean.getClass();
//            Field[] fields = beanClass.getDeclaredFields();
//            for (Field field : fields) {
//                Type fieldType1 = field.getGenericType();
//                // 判断对象是否是字段 field1 的泛型类型或其子类型
//                if (fieldType1 instanceof ParameterizedType) {
//                    ParameterizedType parameterizedType = (ParameterizedType) fieldType1;
//                    Type[] typeArguments = parameterizedType.getActualTypeArguments();
//                    // 这里假设 listField 是 List<String> 类型
//                    if (typeArguments.length > 0 && typeArguments[0] instanceof Class) {
//                        Class<?> genericType = (Class<?>) typeArguments[0];
//                        // 判断对象是否是字段 field1 的泛型类型或其子类型
//                        if (genericType.isAssignableFrom(target.getClass())) {
//                            System.out.println("对象是字段 field1 的泛型类型或其子类型");
//                        } else {
//                            System.out.println("对象不是字段 field1 的泛型类型或其子类型");
//                        }
//                    }
//                }

//                if (field.isAnnotationPresent(Autowired.class)) {
//
                field.setAccessible(true);
                field.set(bean, null);
//                    field.setAccessible(true);
//                    try {
//                        field.set(bean, target);
//                    } catch (IllegalAccessException e) {
//                        e.printStackTrace();
//                    }
//
//                }

        }
    }

}


使用

需要自己实现一个idea插件,用于监控快捷键触发,获取当前选中的文件路径,然后通过http请求发送到自己项目,项目再执行HotfixUtil.reloadFile即可热更新模块。

  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序资源库

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值