20220625_项目热部署学习笔记
1概述
本文主要是结合代码,学习一下spring中Bean的动态加载,具体实现看代码,主要分以下几种情况:
- 本地项目中的类加载到当前项目的spring 容器。
- 外部磁盘中的单个clazz文件基于自定义加载器的方式加载到当前项目spring 容器中。
- 外部磁盘中的单个clazz文件基于URLClassLoader加载器的方式加载到当前项目spring 容器中
- 外部磁盘jar中的clazz文件基于注解的方式加载到当前项目spring 容器中。
- 外部磁盘jar中的clazz文件基于反射的方式加载当前项目中。
2代码示例
2.1本地项目中的类加载到当前项目的spring 容器
/**
* @author kikop
* @version 1.0
* @project myspringhotdeploydemo
* @file DynamicInjectManager
* @desc 项目热部署示例学习
* @date 2022/06/25
* @time 8:30
* @by IDE: IntelliJ IDEA
*/
public class DynamicInjectManager {
/**
* 本地项目中的类加载到当前项目的spring 容器2
* @param defaultListableBeanFactory
*/
public static void dynamicInjectBeanByConstructor(DefaultListableBeanFactory defaultListableBeanFactory) {
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.rootBeanDefinition(CurrentAnimal.class);
// BeanDefinitionBuilder beanDefinitionBuilder2 = BeanDefinitionBuilder.genericBeanDefinition(CurrentAnimal.class);
// 1.方式1,by constructorArgIndex
beanDefinitionBuilder.addConstructorArgValue("北极熊")
.addConstructorArgValue("白色")
.addConstructorArgValue(3);
String beanName = "myAnimal";
defaultListableBeanFactory.registerBeanDefinition(beanName, beanDefinitionBuilder.getBeanDefinition());
Object bean = defaultListableBeanFactory.getBean(beanName);
CurrentAnimal animal = (CurrentAnimal) bean;
System.out.println("animal.getName():" + animal.getName());
}
/**
* 本地项目中的类加载到当前项目的spring 容器1
* @param defaultListableBeanFactory
*/
public static void dynamicInjectBeanByProperty(DefaultListableBeanFactory defaultListableBeanFactory) {
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.rootBeanDefinition(CurrentAnimal.class);
// 2.方式2
beanDefinitionBuilder.addPropertyValue("name", "北极熊")
.addPropertyValue("color", "白色")
.addPropertyValue("age", 3);
String beanName = "myAnimal";
defaultListableBeanFactory.registerBeanDefinition(beanName, beanDefinitionBuilder.getBeanDefinition());
Object bean = defaultListableBeanFactory.getBean(beanName);
CurrentAnimal animal = (CurrentAnimal) bean;
System.out.println("animal.getName():" + animal.getName());
}
2.2外部磁盘中的单个clazz文件基于自定义加载器反射的方式加载到当前项目spring 容器中
2.2.1MyClassLoader
package com.kikop.cloader;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
/**
* @author kikop
* @version 1.0
* @project myspringhotdeploydemo
* @file ICalculator
* @desc 项目热部署示例学习
* @date 2022/06/25
* @time 8:30
* @by IDE: IntelliJ IDEA
* @reference https://mp.weixin.qq.com/s/8JNecuJA9n07Ob7XzLPQTA
*/
public class MyClassLoader extends ClassLoader {
private String dirPath;
// 是否保留类的完整包名路径,
// true:保留时,类要放在原来的包路径下
// false:类直接放在 dirPath下
private boolean isKeepPacketName = true;
/**
* @param dirPath D:/mqexperiment/hotdeploy
*/
public MyClassLoader(String dirPath, boolean isKeepPacketName) {
if (!dirPath.endsWith("/") && !dirPath.endsWith("\\")) {
dirPath += "/";
}
this.dirPath = dirPath;
this.isKeepPacketName = isKeepPacketName;
}
/**
* 触发被 loadClass-->findClass-->loadClass
*
* @param name com.kikop.model.CurrentAnimal
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
String filePath; // 磁盘路径
if (isKeepPacketName) {
// 前提:包名要保留,因为是根据 className进行分割的
// file:///D:/mqexperiment/hotdeploy/com/kikop/model/CurrentAnimal.class
filePath = dirPath + name.replace('.', '/') + ".class";
} else {
// file:///D:/mqexperiment/hotdeploy/CurrentAnimal.class
filePath = dirPath + name.substring(name.lastIndexOf('.') + 1) + ".class";
}
byte[] b;
Path path;
try {
String strIgnore = "Customizer";
if (name.lastIndexOf(strIgnore) != -1) { // ignore for check beaninfo
return null;
}
path = Paths.get(new URI(filePath));
// b = MyClassLoaderUtil.getBytesByFilePath(filePath);
b = Files.readAllBytes(path);
// defineClass将字节数组转换成Class对象
return defineClass(name, b, 0, b.length);
} catch (IOException | URISyntaxException e) {
e.printStackTrace();
return null;
}
}
}
2.2.2dynamicInjectBeanByCustomCloader
/**
* @author kikop
* @version 1.0
* @project myspringhotdeploydemo
* @file DynamicInjectManager
* @desc 项目热部署示例学习
* @date 2022/06/25
* @time 8:30
* @by IDE: IntelliJ IDEA
*/
public class DynamicInjectManager {
public static void dynamicInjectBeanByCustomCloader(DefaultListableBeanFactory defaultListableBeanFactory
, Map reqParam) throws ClassNotFoundException, MalformedURLException {
// 1.解析
String beanName = reqParam.get("beanName");
try {
if (null != defaultListableBeanFactory.getBean(beanName)) {
System.out.println(String.format("%s 容器中已经存在", beanName));
return;
}
} catch (Exception ex) {
// ignore
}
String strLocation = reqParam.get("localtion");
String strClazz = reqParam.get("clazz");
String strPath = "file:///" + strLocation; // URL
MyClassLoader myClassLoader = new MyClassLoader(strPath, false);
Class> aClass = myClassLoader.loadClass(strClazz); // com.kikop.model.CurrentAnimal
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(aClass);
// 2.组装
for (int i = 0; i < aClass.getDeclaredFields().length; i++) {
String fieldName = aClass.getDeclaredFields()[i].getName();
if (null != reqParam.get(fieldName)) {
beanDefinitionBuilder.addPropertyValue(fieldName, reqParam.get(fieldName));
} else {
beanDefinitionBuilder.addPropertyValue(fieldName, "default" + fieldName);
}
}
defaultListableBeanFactory.registerBeanDefinition(beanName, beanDefinitionBuilder.getBeanDefinition());
// 3.输出字段
// beanName的加载器:myClassLoader
// type.getClassLoader():myClassLoader
// 默认会变相加载该类的 XCustomizer,肯定没有啊
Object animalBean = defaultListableBeanFactory.getBean(beanName);
List fields = Arrays.asList(aClass.getDeclaredFields());
fields.stream().forEach(field -> {
System.out.println(field);
});
}
2.3外部磁盘中的单个clazz文件基于URLClassLoader加载器的方式加载到当前项目spring 容器中
/**
* @author kikop
* @version 1.0
* @project myspringhotdeploydemo
* @file DynamicInjectManager
* @desc 项目热部署示例学习
* @date 2022/06/25
* @time 8:30
* @by IDE: IntelliJ IDEA
*/
public class DynamicInjectManager {
public static void dynamicInjectBeanByReflect(DefaultListableBeanFactory defaultListableBeanFactory
, Map reqParam) throws ClassNotFoundException, MalformedURLException {
// 1.解析
String beanName = reqParam.get("beanName");
try {
if (null != defaultListableBeanFactory.getBean(beanName)) {
System.out.println(String.format("%s 容器中已经存在", beanName));
return;
}
} catch (Exception ex) {
// ignore
}
String strLocation = reqParam.get("localtion");
// 使用file协议在本地寻找指定.class文件,file:///Users/fa1c0n/codeprojects/IdeaProjects/misc-classes/src/main/java/
// 使用http协议到远程地址寻找指定.class文件, http://127.0.0.1:8000/
String strPath = "file:///" + strLocation; // URL
String strClazz = reqParam.get("clazz");
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL(strPath)});
// 注意:
// 类名 com.kikop.model.User
Class> aClass = urlClassLoader.loadClass(strClazz);
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(aClass);
// 2.组装
for (int i = 0; i < aClass.getDeclaredFields().length; i++) {
String fieldName = aClass.getDeclaredFields()[i].getName();
if (null != reqParam.get(fieldName)) {
beanDefinitionBuilder.addPropertyValue(fieldName, reqParam.get(fieldName));
} else {
beanDefinitionBuilder.addPropertyValue(fieldName, "default" + fieldName);
}
}
defaultListableBeanFactory.registerBeanDefinition(beanName, beanDefinitionBuilder.getBeanDefinition());
// 3.输出字段
Object animalBean = defaultListableBeanFactory.getBean(beanName);
List fields = Arrays.asList(aClass.getDeclaredFields());
fields.stream().forEach(field -> {
System.out.println(field);
});
}
2.4外部磁盘jar中的clazz文件基于注解的方式加载到当前项目spring 容器中
package com.kikop.deploymethod;
import com.kikop.calculator.ICalculator;
import com.kikop.utils.MyJarUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Set;
/**
* @author kikop
* @version 1.0
* @project myspringhotdeploydemo
* @file DeployByAnnotation
* @desc 项目热部署示例学习
* @date 2022/06/25
* @time 8:30
* @by IDE: IntelliJ IDEA
* @reference https://mp.weixin.qq.com/s/8JNecuJA9n07Ob7XzLPQTA
*/
@Component
public class DeployByAnnotation implements ApplicationContextAware {
private static ApplicationContext applicationContext;
// jar:file:/
private static String jarAddress = "D:\\mqexperiment\\hotdeploy\\mycalculatorbusinesscomponent-0.0.1-SNAPSHOT.jar";
private static String jarPath = "file:/" + jarAddress;
/**
* 加入jar包后 动态注册bean到spring容器,包括bean的依赖
*/
public static void hotDeployWithSpring() throws Exception {
// com.kiko.calculator.impl.CalculatorImpl
Set classNameSet = MyJarUtils.readJarFile(jarAddress);
URLClassLoader urlClassLoader = new URLClassLoader(
new URL[]{new URL(jarPath)}, Thread.currentThread().getContextClassLoader());
for (String className : classNameSet) {
Class clazz = urlClassLoader.loadClass(className);
if (MyJarUtils.isSpringBeanClass(clazz)) { // 是需要的注解
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
((BeanDefinitionRegistry) applicationContext).registerBeanDefinition(
MyJarUtils.transformName(className), beanDefinitionBuilder.getBeanDefinition());
}
}
ICalculator calculator = applicationContext.getBean(ICalculator.class);
System.out.println(calculator.calculate(4, 5));
}
/**
* 删除jar包时 需要在spring容器删除注入
*/
public static void delete() throws Exception {
Set classNameSet = MyJarUtils.readJarFile(jarAddress);
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL(jarPath)}, Thread.currentThread().getContextClassLoader());
for (String className : classNameSet) {
Class clazz = urlClassLoader.loadClass(className);
if (MyJarUtils.isSpringBeanClass(clazz)) {
((BeanDefinitionRegistry) applicationContext).removeBeanDefinition(MyJarUtils.transformName(className));
}
}
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
2.5外部磁盘jar中的clazz文件基于反射的方式加载当前项目中
package com.kikop.deploymethod;
import com.kikop.calculator.ICalculator;
import java.net.URL;
import java.net.URLClassLoader;
/**
* @author kikop
* @version 1.0
* @project myspringhotdeploydemo
* @file DeployByReflect
* @desc 项目热部署示例学习
* @date 2022/06/25
* @time 8:30
* @by IDE: IntelliJ IDEA
* @reference https://mp.weixin.qq.com/s/8JNecuJA9n07Ob7XzLPQTA
*/
public class DeployByReflect {
// 位置目录,不能是文件
private static String jarAddress = "D:\\mqexperiment\\hotdeploy\\mycalculatorbusinesscomponent-0.0.1-SNAPSHOT.jar";
private static String jarPath = "file:/" + jarAddress;
// ClassLoader(最底层)-->SecureClassLoader(中间态)-->URLClassLoader(最简单)
// SecureClassLoader 默认: this(checkCreateClassLoader(), null, getSystemClassLoader());
/**
* 热加载Calculator接口的实现 反射方式
*
* @throws Exception
*/
public static void hotDeployWithReflect() throws Exception {
URLClassLoader urlClassLoader = new URLClassLoader(
new URL[]{new URL(jarPath)},
Thread.currentThread().getContextClassLoader());
Class clazz = urlClassLoader.loadClass("com.kikop.calculator.impl.CalculatorImpl");
ICalculator iCalculator = (ICalculator) clazz.newInstance();
int result = iCalculator.add(18, 1);
System.out.println(result);
}
}
2.6工具类
2.6.1MyJarUtils
package com.kikop.utils;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* @author kikop
* @version 1.0
* @project myspringhotdeploydemo
* @file MyJarUtils
* @desc 项目热部署示例学习
* @date 2022/06/25
* @time 8:30
* @by IDE: IntelliJ IDEA
* @reference https://mp.weixin.qq.com/s/8JNecuJA9n07Ob7XzLPQTA
*/
public class MyJarUtils {
/**
* 读取jar包中所有类文件
*/
public static Set readJarFile(String jarAddress) throws IOException {
Set classNameSet = new HashSet<>();
JarFile jarFile = new JarFile(jarAddress);
Enumeration entries = jarFile.entries();// 遍历整个jar文件
while (entries.hasMoreElements()) {
JarEntry jarEntry = entries.nextElement();
String name = jarEntry.getName();
if (name.endsWith(".class")) { // 过滤 .class
// com/kikop/calculator/impl/XxxImpl.class
// com/kikop/calculator/impl/XxxImpl
// 最终的classNamecom.kikop.calculator.impl.XxxImpl
String className = name.replace(".class", "").replaceAll("/", ".");
classNameSet.add(className);
}
}
return classNameSet;
}
/**
* 方法描述 判断class对象是否带有spring的注解
*/
public static boolean isSpringBeanClass(Class> cla) {
if (cla == null) {
return false;
}
//是否是接口
if (cla.isInterface()) {
return false;
}
// 是否是抽象类
if (Modifier.isAbstract(cla.getModifiers())) {
return false;
}
if (cla.getAnnotation(Component.class) != null) {
return true;
}
if (cla.getAnnotation(Repository.class) != null) {
return true;
}
if (cla.getAnnotation(Service.class) != null) {
return true;
}
return false;
}
/**
* 类名首字母小写 作为 spring容器beanMap的key
*/
public static String transformName(String className) {
String tmpstr = className.substring(className.lastIndexOf(".") + 1);
return tmpstr.substring(0, 1).toLowerCase() + tmpstr.substring(1);
}
}
2.6.2MyClassLoaderUtil
package com.kikop.utils;
import com.sun.xml.internal.ws.util.ByteArrayBuffer;
import java.io.*;
import java.net.URL;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* @author kikop
* @version 1.0
* @project myjdkclazzloader1demo
* @file MyClassLoaderUtil
* @desc 获取类的字节码
* @date 2021/6/13
* @time 18:00
* @by IDE: IntelliJ IDEA
*/
public class MyClassLoaderUtil {
private static String convertFilePath(String ignoreValue, String filePath) {
if (filePath.indexOf(ignoreValue) != -1) {
filePath = filePath.substring(filePath.indexOf(ignoreValue) + ignoreValue.length());
}
return filePath;
}
/**
* getBytesByFilePath
*
* 返回类的字节码
*
* @param filePath D:/test/com.kikop.model.User.class
* @return
*/
public static byte[] getBytesByFilePath(String filePath) {
filePath = convertFilePath("file:///", filePath);
filePath = convertFilePath("file:/", filePath);
byte[] resultBytes = null;
InputStream inputStream = null;
// 借助 byteArrayOutputStream暂存字节流
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
try {
File file = new File(filePath);
if (!file.exists()) {
return resultBytes;
}
inputStream = new FileInputStream(file);
int c = 0;
while ((c = inputStream.read()) != -1) {
byteArrayOutputStream.write(c);
}
resultBytes = byteArrayOutputStream.toByteArray();
} catch (FileNotFoundException ex) {
ex.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
byteArrayOutputStream.close();
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultBytes;
}
/**
* getBytesByJarFile
* 返回 Jar包中类的字节码
*
* @param jarDirectory D:\workdirectory\mqexperiment\clloader\
* @param jarName xx.jar
* @param classFullName java.lang.String
* @return
* @throws IOException
*/
public static byte[] getBytesByJarFile(String jarDirectory, String jarName, String classFullName) throws IOException {
byte[] resultBytes = null;
InputStream inputStream = null;
ByteArrayOutputStream byteArrayOutputStream = null;
try {
// replaceAll:要加斜杠转义,replace:不需要
// java.lang.String --> java/lang/String
String tmpClassFullName = classFullName.replaceAll("\\.", "/").concat(".class");
// com.kikop.AddressService-->com/kikop/service/AddressService
// String tmpClassFullName2 = classFullName.replace(".", "/").concat(".class");
String jarFullName = jarDirectory + "/" + jarName;
JarFile jar = new JarFile(jarFullName);
JarEntry entry = jar.getJarEntry(tmpClassFullName); // java/lang/String.class
if (null == entry) { // 增加异常判断,文件不存在
System.out.println("tmpClassFullName:" + tmpClassFullName);
return null;
}
inputStream = jar.getInputStream(entry);
byteArrayOutputStream = new ByteArrayOutputStream();
int nextValue = inputStream.read();
while (-1 != nextValue) {
byteArrayOutputStream.write(nextValue);
nextValue = inputStream.read();
}
// byte[] buffer=new byte[2048];
// int len=0;
// while((len=in.read(buffer))!=-1){
// out.write(buffer,0,len);
// }
resultBytes = byteArrayOutputStream.toByteArray();
} catch (Exception ex) {
ex.printStackTrace();
} finally {
try {
if (null != byteArrayOutputStream) {
byteArrayOutputStream.close();
}
if (null != inputStream) {
inputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return resultBytes;
}
/**
* getBytesByJarFile 返回 Jar包中类的字节码
*
* @param jarDirectory D:\workdirectory\mqexperiment\clloader\
* @param jarName xx.jar
* @param classFullName java.lang.String
* @return
* @throws IOException
*/
public static byte[] getBytesByJarFile2(String jarDirectory, String jarName, String classFullName) throws IOException {
// com.kikop.AddressService-->com/kikop/service/AddressService.class
String tmpClassFullName = classFullName.replace(".", "/").concat(".class");
InputStream inputStream;
ByteArrayBuffer byteArrayBuffer = new ByteArrayBuffer();
byte[] resultBytes = null;
int code;
URL fileUrl;
try {
// jar:file:\
// D:\workdirectory\mqexperiment\clloader\mymoduleva-1.0-SNAPSHOT.jar!/
// com/kikop/service/AddressService.class
String jarFullName = jarDirectory + "/" + jarName;
String strSpec = "jar:file:\\" + jarFullName + "!/" + tmpClassFullName;
fileUrl = new URL(strSpec);
inputStream = fileUrl.openStream();
while ((code = inputStream.read()) != -1) {
byteArrayBuffer.write(code);
}
resultBytes = byteArrayBuffer.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return resultBytes;
}
}
2.7测试
package com.kikop;
import com.kikop.cloader.MyClassLoader;
import com.kikop.deploymethod.DeployByAnnotation;
import com.kikop.deploymethod.DeployByReflect;
import com.kikop.deploymethod.DynamicInjectManager;
import com.kikop.model.CurrentAnimal;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author kikop
* @version 1.0
* @project myspringhotdeploydemo
* @file MySpringHotDeployDemoApplication
* @desc 项目热部署示例学习
* @date 2022/06/25
* @time 8:30
* @by IDE: IntelliJ IDEA
* @reference https://mp.weixin.qq.com/s/8JNecuJA9n07Ob7XzLPQTA
*/
@SpringBootApplication
public class MySpringHotDeployDemoApplication implements CommandLineRunner {
private static DefaultListableBeanFactory defaultListableBeanFactory;
public static void main(String[] args) throws ClassNotFoundException, MalformedURLException {
ConfigurableApplicationContext configurableApplicationContext = SpringApplication.run(MySpringHotDeployDemoApplication.class, args);
AutowireCapableBeanFactory autowireCapableBeanFactory = configurableApplicationContext.getAutowireCapableBeanFactory();
defaultListableBeanFactory = (DefaultListableBeanFactory) autowireCapableBeanFactory;
Map reqParam = new HashMap<>();
reqParam.put("name", "北极熊");
reqParam.put("color", "白色");
reqParam.put("age", "3");
reqParam.put("localtion", "D:/mqexperiment/hotdeploy/");
reqParam.put("clazz", "com.kikop.model.Animal");
reqParam.put("beanName", "myAnimal");
DynamicInjectManager.dynamicInjectBeanByCustomCloader(defaultListableBeanFactory, reqParam);
}
@Override
public void run(String... args) throws Exception {
// 1.基于JDK反射包热部署
DeployByReflect.hotDeployWithReflect();
// 2.基于Spring注解Jar包热部署
// DeployByAnnotation.hotDeployWithSpring();
}
}
总结
1.1JDKintrospector
当我们把这个类交给 spring 的时候,问题就出现了: spring 是一个已有的框架, 它并不知道 User
这个类,也并不知道它有哪些方法、哪些属性。
public void introspector(String clazz, Map properties) throws Exception {
//反射创建实例
Class target = Class.forName(clazz);
Object bean = target.newInstance();
BeanInfo beanInfo = Introspector.getBeanInfo(bean.getClass());
PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor pd : pds) {
Method setMethod = pd.getWriteMethod();
String fieldName = pd.getName();
if ("name".equalsIgnoreCase(fieldName)) {
setMethod.invoke(bean, properties.get(fieldName));
} else if ("age".equalsIgnoreCase(fieldName)){
setMethod.invoke(bean, properties.get(fieldName));
}
}
参考
1动态上传jar包热部署实战
https://mp.weixin.qq.com/s/8JNecuJA9n07Ob7XzLPQTA
2SpringBoot动态注入Bean
http://www.caotama.com/1398966.html
3SpringBoot动态注入bean (系统注入漏洞原理)
https://blog.csdn.net/weixin_45943597/article/details/124176226
4Java中动态加载字节码的方法 (持续补充)Good
https://blog.csdn.net/mole_exp/article/details/122768814
5java Introspector(内省) 的使用场景以及为什么使用
https://www.jianshu.com/p/418122d84e6e
6[深入理解Java:内省(Introspector)
https://www.cnblogs.com/peida/archive/2013/06/03/3090842.html)
public interface **Customizer**
customizer 类提供一个用来自定义目标 Java Bean 的完全自定义 GUI。
每个 customizer 都应该从 java.awt.Component 类继承,因此它们可以在 AWT 对话框或面板中被实例化。
每个 customizer 都应该有一个 null 构造方法。