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文件了,当然这个项目十分简陋,还有很大的提升空间。