spring boot运行过程中动态加载Controller

发布于:2024-05-01 ⋅ 阅读:(24) ⋅ 点赞:(0)

1.被加载的jar代码

package com.dl;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}
package com.dl;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class JarController {


    @RequestMapping("/jar")
    String jar() {
        return "i  am  a  jar";
    }


}

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.dl</groupId>
    <artifactId>jar</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>Spring Boot Blank Project (from https://github.com/making/spring-boot-blank)</name>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.12</version>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>com.dl.App</start-class>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <excludes>
                        <!-- 去除指定的类-->
                        <exclude>**/App.java</exclude>
                    </excludes>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>2.6</version>
                <configuration>
                    <archive>
                        <addMavenDescriptor>false</addMavenDescriptor>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

工程结构
在这里插入图片描述

2.实现代码

package com.dl;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class, args);
    }
}
package com.dl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 测试
 */
@RestController
public class HelloController {

    private final TestDynamicLoad testDynamicLoad;

    @Autowired
    public HelloController(TestDynamicLoad testDynamicLoad) {
        this.testDynamicLoad = testDynamicLoad;
    }

    @RequestMapping("/")
    String hello() {
        return "Hello";
    }

    /**
     *
     * @param path  jar文件的路径
     * @param fileName  jar文件的名称
     * @return  加载结果
     */
    @RequestMapping("/load")
    String load(@RequestParam String path, @RequestParam String fileName) {
        try {
            testDynamicLoad.loadJar(path,fileName);
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
            return "失败:"+e.getMessage();
        }
        return "加载成功";
    }


    /**
     *
     * @param name 卸载jar的名称
     * @return
     */
    @RequestMapping("/unload")
    String unload(@RequestParam String name) {
        try {
            testDynamicLoad.unloadJar(name);
        } catch (IllegalAccessException | NoSuchFieldException e) {
            e.printStackTrace();
            return "失败:"+e.getMessage();
        }
        return "卸载成功";
    }

}

package com.dl;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 自定义类加载器
 */
public class TestClassLoader  extends URLClassLoader{
    private Map<String, Class<?>> loadedClasses = new ConcurrentHashMap<>();

    public Map<String, Class<?>> getLoadedClasses() {
        return loadedClasses;
    }

    public TestClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }

    //加载
    @Override
    protected Class<?> findClass(String name) {
        // 从已加载的类集合中获取指定名称的类
        Class<?> clazz = loadedClasses.get(name);
        if (clazz != null) {
            return clazz;
        }
        try {
            // 调用父类的findClass方法加载指定名称的类
            clazz = super.findClass(name);
            // 将加载的类添加到已加载的类集合中
            loadedClasses.put(name, clazz);
            return clazz;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            return null;
        }
    }

    //卸载
    public void unload() {
        try {
            for (Map.Entry<String, Class<?>> entry : loadedClasses.entrySet()) {
                // 从已加载的类集合中移除该类
                String className = entry.getKey();
                loadedClasses.remove(className);
                try{
                    // 调用该类的destory方法,回收资源
                    Class<?> clazz = entry.getValue();
                    Method destory = clazz.getDeclaredMethod("destory");
                    destory.invoke(clazz);
                } catch (Exception e ) {
                    // 表明该类没有destory方法
                }
            }
            // 从其父类加载器的加载器层次结构中移除该类加载器
            close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

package com.dl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

@Component
public class TestDynamicLoad {

    @Autowired
    private ApplicationContext applicationContext;

    private Map<String, TestClassLoader> myClassLoaderCenter = new ConcurrentHashMap<>();



    /**
     * 动态加载指定路径下指定jar包
     * @param path
     * @param fileName
     */
    public void loadJar(String path, String fileName) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        //获取jar文件
        File file = new File(path +"/" + fileName);
        // 获取beanFactory
        DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();

        try {
            //创建URLConnection
            URL url = new URL("jar:file:" + file.getAbsolutePath() + "!/");
            URLConnection urlConnection = url.openConnection();
            JarURLConnection jarURLConnection = (JarURLConnection)urlConnection;
            // 获取jar文件
            JarFile jarFile = jarURLConnection.getJarFile();
            Enumeration<JarEntry> entries = jarFile.entries();
            // 创建自定义类加载器,并加到map中方便管理
            TestClassLoader myClassloader = new TestClassLoader(new URL[] { url }, ClassLoader.getSystemClassLoader());
            myClassLoaderCenter.put(fileName, myClassloader);
            // 遍历文件
            while (entries.hasMoreElements()) {
                JarEntry jarEntry = entries.nextElement();
                if (jarEntry.getName().endsWith(".class")) {
                    // 1. 加载类到jvm中
                    // 获取类的全路径名
                    String className = jarEntry.getName().replace('/', '.').substring(0, jarEntry.getName().length() - 6);
                    // 1.1进行反射获取
                    myClassloader.loadClass(className);
                }
            }
            Map<String, Class<?>> loadedClasses = myClassloader.getLoadedClasses();
            for(Map.Entry<String, Class<?>> entry : loadedClasses.entrySet()){
                String className = entry.getKey();
                Class<?> clazz = entry.getValue();
                // 此处beanName使用全路径名是为了防止beanName重复
                String packageName = className.substring(0, className.lastIndexOf(".") + 1);
                String beanName = className.substring(className.lastIndexOf(".") + 1);
                beanName = packageName + beanName.substring(0, 1).toLowerCase() + beanName.substring(1);
                // 2. 将有@spring注解的类交给spring管理
                // 2.1 判断类的类型
                String flag = hasSpringAnnotation(clazz);
                if(!flag.equals("pass")){
                    // 2.2交给spring管理
                    BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
                    AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
                    // 2.3注册到spring的beanFactory中
                    beanFactory.registerBeanDefinition(beanName, beanDefinition);
                    // 2.4允许注入和反向注入
                    beanFactory.autowireBean(clazz);
                    beanFactory.initializeBean(clazz, beanName);
                    // 2.5手动构建实例,并注入base service 防止卸载之后不再生成
                    Object obj = clazz.newInstance();
                    beanFactory.registerSingleton(beanName, obj);
                    //3.特殊处理
                    //3.1不同的spring核心类不同的处理,实例中只是写了contrller
                    handle(flag,beanName);
                }

            }
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("读取jar文件异常: " + fileName);
        }
    }


    /**
     * 判断一个类是具体类型,如果是spring核心类需要交给spring管理
     * @param clazz 要检查的类
     * @return string 如果该类上添加了相应的 Spring 注解返回对应标识;否则返回 pass
     */
    private  String hasSpringAnnotation(Class<?> clazz) {
        if (clazz == null) {
            return "pass";
        }
        //是否是接口
        if (clazz.isInterface()) {
            return "pass";
        }
        //是否是抽象类
        if (Modifier.isAbstract(clazz.getModifiers())) {
            return "pass";
        }
        //常规注解效验和处理
        try {
            if (clazz.getAnnotation(Component.class) != null ) {
                return "Component";
            }
            if (clazz.getAnnotation(Repository.class) != null) {
                return "Repository";
            }
            if (clazz.getAnnotation(Service.class) != null ) {
                return "Service";
            }
            if (clazz.getAnnotation(Configuration.class) != null ) {
                return "Configuration";
            }
            if (clazz.getAnnotation(Controller.class) != null || clazz.getAnnotation(RestController.class) != null) {
                return "Controller";
            }

        }catch (Exception e){
            e.printStackTrace();
        }
        return "pass";
    }
    /**
     * 处理类
     * @param type 类型标识
     * @param name bean名称
     */
    private  void handle(String type ,String name){
        //这里只做了contrller类型标识的处理
        if(type.equals("Controller")){
            RequestMappingHandlerMapping handlerMapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
            // 注册Controller
            Method method = null;
            try {
                method = handlerMapping.getClass().getSuperclass().getSuperclass().
                        getDeclaredMethod("detectHandlerMethods", Object.class);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            }
            // 将private改为可使用
            assert method != null;
            method.setAccessible(true);
            try {
                method.invoke(handlerMapping, name);
            } catch (IllegalAccessException | InvocationTargetException e) {
                e.printStackTrace();
            }
        }

    }

    /**
     * 动态卸载
     * @param name  卸载jar的名称
     */
    public void unloadJar(String name) throws IllegalAccessException, NoSuchFieldException {
        // 获取加载当前jar的类加载器
        TestClassLoader myClassLoader = myClassLoaderCenter.get(name);
        // 获取beanFactory,准备从spring中卸载
        DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
        Map<String, Class<?>> loadedClasses = myClassLoader.getLoadedClasses();
        Set<String> beanNames = new HashSet<>();
        for (Map.Entry<String, Class<?>> entry: loadedClasses.entrySet()) {
            // 截取beanName
            String key = entry.getKey();
            String packageName = key.substring(0, key.lastIndexOf(".") + 1);
            String beanName = key.substring(key.lastIndexOf(".") + 1);
            beanName = packageName + beanName.substring(0, 1).toLowerCase() + beanName.substring(1);

            // 获取bean,如果获取失败,表名这个类没有加到spring容器中,则跳出本次循环
            Object bean = null;
            try{
                bean = applicationContext.getBean(beanName);
            }catch (Exception e){
                // 异常说明spring中没有这个bean
                continue;
            }
            // 从spring中移除,这里的移除是仅仅移除的bean,并未移除bean定义
            beanNames.add(beanName);
            beanFactory.destroyBean(beanName, bean);
        }
        // 移除bean定义
        Field mergedBeanDefinitions = beanFactory.getClass()
                .getSuperclass()
                .getSuperclass().getDeclaredField("mergedBeanDefinitions");
        mergedBeanDefinitions.setAccessible(true);
        Map<String, RootBeanDefinition> rootBeanDefinitionMap = ((Map<String, RootBeanDefinition>) mergedBeanDefinitions.get(beanFactory));
        for (String beanName : beanNames) {
            beanFactory.removeBeanDefinition(beanName);
            // 父类bean定义去除
            rootBeanDefinitionMap.remove(beanName);
        }
        // 从类加载中移除
        try {
            // 从类加载器底层的classes中移除连接
            Field field = ClassLoader.class.getDeclaredField("classes");
            field.setAccessible(true);
            Vector<Class<?>> classes = (Vector<Class<?>>) field.get(myClassLoader);
            classes.removeAllElements();
            // 移除类加载器的引用
            myClassLoaderCenter.remove(name);
            // 卸载类加载器
            myClassLoader.unload();
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }

}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.dl</groupId>
    <artifactId>li</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>Spring Boot Blank Project (from https://github.com/making/spring-boot-blank)</name>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.12</version>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>com.dl.App</start-class>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.6.0</version>
            </plugin>
        </plugins>
    </build>

</project>

3.测试

①启动项目
在这里插入图片描述

②测试url
在这里插入图片描述

③没有加载jar前
在这里插入图片描述

④加载jar
在这里插入图片描述

⑤加载后验证
在这里插入图片描述

⑥卸载jar
在这里插入图片描述

⑦验证
在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到