spring boot项目运行过程中,动态更新class文件,简单实践

在平时学习和开发过程中,如果java源文件发生了修改会重新编译成新的class文件,这时一般都需要重新启动项目,加载最新的class文件,改动才会生效。那么能不能在不重新启动项目的情况下,动态更新掉class文件,使最新的改动生效呢,下面我们简单实践一下:

首先创建一个简单的类文件:

package learnbymaven.string;

/**
 * @author jinghx
 * @date 2020/06/03
 */
public class Hello {

    public String sayHello(String content) {
        return "hello " + content;
    }

}

将生成的class文件放到E盘根目录下:
在这里插入图片描述
创建一个spring boot工程,项目结构如下:
在这里插入图片描述
导入maven依赖:

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

既然涉及到类加载,这里先自己定义一个类加载器:

public class MyClassLoader extends ClassLoader {
	/**
	 * 需要加载类的路径
	 */
	private String classPath;

	public MyClassLoader() {
	}

	public MyClassLoader(String classPath) {
		super();
		this.classPath = classPath;
	}

	@Override
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		Class<?> clazz = null;
		// 获取class文件字节码数组
		byte[] clazzByteArr = getData();

		if (clazzByteArr != null) {
			// 将class的字节码数组转换成class类的实例
			clazz = defineClass(name, clazzByteArr, 0, clazzByteArr.length);
		}
		return clazz;
	}

	/**
	 * 获取class文件字节数组
	 * 
	 * @return
	 */
	private byte[] getData() {
		File file = new File(this.classPath);
		if (file.exists()) {
			FileInputStream in = null;
			ByteArrayOutputStream out = null;
			try {
				in = new FileInputStream(file);
				out = new ByteArrayOutputStream();

				byte[] buffer = new byte[1024];
				int size = 0;
				while ((size = in.read(buffer)) != -1) {
					out.write(buffer, 0, size);
				}

			} catch (IOException e) {
				e.printStackTrace();
			} finally {
				try {
					in.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			return out.toByteArray();
		} else {
			return null;
		}
	}

	public String getClassPath() {
		return classPath;
	}

	public void setClassPath(String classPath) {
		this.classPath = classPath;
	}

}

测试一下自定义类加载器是否可以使用:

	@Test
	void contextLoads() throws Exception {
		MyClassLoader myClassLoader = new MyClassLoader();
		myClassLoader.setClassPath("E:\\Hello.class");
		Class<?> clazz = myClassLoader.loadClass("learnbymaven.string.Hello");
		System.out.println("当前类加载器:" + clazz.getClassLoader());
		Object instance = clazz.newInstance();
		Method method = clazz.getMethod("sayHello", String.class);
		Object result = method.invoke(instance, "李四");
		System.out.println(result);
	}

运行结果:

当前类加载器:com.learn.util.MyClassLoader@2f1ea80d
hello 李四

创建一个class文件加载的工具类:

public class UserUtil {

	/**
	 * 字节码对象
	 */
	private static Class<?> clazz;

	/**
	 * 实例对象
	 */
	private static Object userObject;

	/**
	 * 默认class文件路径
	 */
	private static final String DEFAULT_CLASS_PATH = "E:\\Hello.class";

	/**
	 * 默认全限定类名称
	 */
	private static final String DEFAULT_CLASS_NAME = "learnbymaven.string.Hello";

	/**
	 * 加载class对象
	 * 
	 * @param classPath
	 * @param className
	 * @return
	 * @throws Exception
	 */
	public synchronized static void loadClass(String classPath, String className) throws Exception {
		MyClassLoader myClassLoader = new MyClassLoader();
		myClassLoader.setClassPath(StringUtils.isEmpty(classPath) ? DEFAULT_CLASS_PATH : classPath);
		clazz = myClassLoader.loadClass(StringUtils.isEmpty(className) ? DEFAULT_CLASS_NAME : className);
		initUserObject();
	}

	/**
	 * 初始化userObject对象
	 * 
	 * @throws Exception
	 */
	public static void initUserObject() throws Exception {

		userObject = clazz == null ? null : clazz.newInstance();
	}

	/**
	 * sayHello方法
	 * 
	 * @param name
	 * @return
	 * @throws Exception
	 */
	public static String sayHello(String name) throws Exception {
		Method method = clazz.getMethod("sayHello", String.class);
		return (String) method.invoke(userObject, name);
	}

}

这个工具类主要是实现加载class文件和运行指定的sayHello()方法。

创建一个UserService类,这里只是简单模拟业务,就不再严格遵守开发规范了:

@Service
public class UserService {

	public String sayHello(String name) throws Exception {
		return UserUtil.sayHello(name);
	}

}

创建UserController:

@RestController
@RequestMapping("/user")
public class UserController {

	@Autowired
	private UserService userService;

	@GetMapping("/sayhello")
	public String sayHello(String name) {
		String result = null;
		try {
			result = userService.sayHello(name);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return result;
	}

}

为了实现在网页前端能够动态更新class文件,再创建一个ClassController:

@RestController
@RequestMapping("/class")
public class ClassController {

	/**
	 * 刷新加载类class文件
	 * 
	 * @param classPath
	 * @param className
	 * @return
	 */
	@GetMapping("/flushClass")
	public String flushClass(String classPath, String className) {
		String flag = "succes";
		try {
			UserUtil.loadClass(classPath, className);
		} catch (Exception e) {
			e.printStackTrace();
			flag = "error";
		}
		return flag;
	}

}

为了实现spring boot项目第一次启动时,Hello类就能够实现加载,需要在创建一个InitClassLoad类,InitClassLoad类实现了CommandLineRunner接口:

@Component
@Order(2)
public class InitClassLoad implements CommandLineRunner {

	@Override
	public void run(String... args) throws Exception {
		UserUtil.loadClass(null, null);// 加载默认class文件
	}

}

到这里,代码编写基本完成了,下面进行测试,运行项目,访问:http://127.0.0.1:8080/user/sayhello?name=王麻子

运行结果:
在这里插入图片描述
这时,假设增加了一个需求,需要你额外显示每次问好的时间,Hello类代码如下:

public class Hello {

    public String sayHello(String content) {
        StringBuilder hello = new StringBuilder();
        hello.append("hello, ").append(content).append(" Time:")
            .append(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        return hello.toString();
    }

}

此时,你只需要用最新的class文件替换掉E盘下面老的class文件,然后访问http://127.0.0.1:8080/class/flushClass?classPath=E:%2F%2FHello.class&className=learnbymaven.string.Hello,动态刷新一下class文件的加载就行了。

注意:使用GET方式提交时需要注意一些特殊字符,\ 表示目录路径 使用%2F替换一下!

然后重新访问:http://127.0.0.1:8080/user/sayhello?name=王麻子

运行结果:
在这里插入图片描述
可以看到运行结果已经发生了改变,而整个过程中,我们都没有重启spring boot项目。

到这里,就简单实现了在spring boot项目运行过程中动态替换class文件了,当然这个项目十分简陋,还有很大的提升空间。

posted @ 2020-06-03 15:38  三分魔系  阅读(1299)  评论(0编辑  收藏  举报