スプリングブートの動的読み込み
背景と実装のアイデア
ステーターを設計する場合は、独立して実行できる Springboot 単一の jar パッケージを簡単にロードできます。サービス全体を再起動せずに、実行されたサービスの機能をすばやく拡張したり、実行後のコードをすばやくプレビューしたりすることができます。コードプラットフォームはコードを生成します。
ジャーをロードするためのテクノロジースタック
- スプリングブート 2.2.6.RELEASE
- mybatis-plus 3.4.1
ローディングを実現
クラスのロードを完了したい場合は、Spring のクラス ロード メカニズムと Java のクラスローダーの親委任メカニズムに精通している必要があります。
ロードは 2 つのステップに分かれており、
最初のステップでは、対応する jar 内のクラス ファイルを現在実行中のメモリにロードし、次のステップでは、対応する Bean を Spring に登録し、管理のために Spring に渡します。
ロードクラス
ロード クラスは主に jdk の URLClassLoader ツール クラスを使用しますが、クラスローダーを構築するとき、コンストラクターは親クラス ローダーを指定できることに注意してください。指定されている場合、Java は 2 つのクラスローダーによってロードされた同じクラスを、指定しない場合は「com.demo.A can not Cast to com.demo.A」と表示されます。
ただし、次の理由により、ここではまだ親クラス ローダーを指定していません。
- ロードしたい jar はすべて独立して実行できるため、他のプロジェクトのファイルに依存する必要はありません。
- 親クラスローダーが指定されている場合、このクラスローダーはリサイクルできず、ローダーは常にメモリ内に存在します。
jarをロードするコード
/**
* 加载jar包
*
* @param jarPath jar路径
* @param packageName 扫面代码的路径
* @return
*/
public boolean loadJar(String jarPath, String packageName) {
try {
File file = FileUtil.file(jarPath);
URLClassLoader classloader = new URLClassLoader(new URL[]{
file.toURI().toURL()}, this.applicationContext.getClassLoader());
JarFile jarFile = new JarFile(file);
// 获取jar包下所有的classes
String pkgPath = packageName.replace(".", "/");
Enumeration<JarEntry> entries = jarFile.entries();
Class<?> clazz = null;
List<JarEntry> xmlJarEntry = new ArrayList<>();
List<String> loadedAliasClasses = new ArrayList<>();
List<String> otherClasses = new ArrayList<>();
// 首先加载model
while (entries.hasMoreElements()) {
JarEntry jarEntry = entries.nextElement();
String entryName = jarEntry.getName();
if (entryName.charAt(0) == '/') {
entryName = entryName.substring(1);
}
if (entryName.endsWith("Mapper.xml")) {
xmlJarEntry.add(jarEntry);
} else {
if (jarEntry.isDirectory() || !entryName.contains(pkgPath) || !entryName.endsWith(".class")) {
continue;
}
String className = entryName.substring(0, entryName.length() - 6);
otherClasses.add(className.replace("/", "."));
log.info("load class : " + className.replace("/", "."));
// 将变量首字母置小写
String beanName = StringUtils.uncapitalize(className);
if (beanName.contains(LoaderConstant.MODEL)) {
// 加载所有的class
clazz = classloader.loadClass(className.replace("/", "."));
SqlSessionFactory sqlSessionFactory = applicationContext.getBean(SqlSessionFactory.class);
sqlSessionFactory.getConfiguration().getTypeAliasRegistry().registerAlias(beanName.replace("/", "."), clazz);
loadedAliasClasses.add(beanName.replace("/", ".").toLowerCase());
doMap.put(className.replace("/", "."), clazz);
}
}
}
// 再加载其他class
for (String otherClass : otherClasses) {
// 加载所有的class
clazz = classloader.loadClass(otherClass.replace("/", "."));
log.info("load class : " + otherClass.replace("/", "."));
// 将变量首字母置小写
String beanName = StringUtils.uncapitalize(otherClass);
if (beanName.endsWith(LoaderConstant.MAPPER)) {
mapperMap.put(beanName, clazz);
} else if (beanName.endsWith(LoaderConstant.CONTROLLER)) {
controllerMap.put(beanName, clazz);
} else if (beanName.endsWith(LoaderConstant.SERVICE_IMPL)) {
serviceImplMap.put(beanName, clazz);
} else if (beanName.endsWith(LoaderConstant.SERVICE)) {
serviceMap.put(beanName, clazz);
}
}
// 加载所有XML
for (JarEntry jarEntry : xmlJarEntry) {
SqlSessionFactory sqlSessionFactory = applicationContext.getBean(SqlSessionFactory.class);
mybatisXMLLoader.xmlReload(sqlSessionFactory, jarFile, jarEntry, jarEntry.getName());
}
Jar jar = new Jar();
jar.setName(jarPath);
jar.setJarFile(jarFile);
jar.setLoader(classloader);
jar.setLoadedAliasClasses(loadedAliasClasses);
// 开始加载bean
registerBean(jar);
registry.registerJar(jarPath, jar);
} catch (Exception e) {
log.error(e.getLocalizedMessage());
return false;
}
return true;
}
通常、Bean の登録プロセス
ホットロードを実現するには、Spring のクラスのロードメカニズムを理解する必要があります。一般に、Spring が @Component アノテーションが付けられたクラスをスキャンすると、そのクラスに従って対応する BeanDefinition が生成され、BeanDefinitionRegistry に登録されます (これはインターフェース 、最終的には DefaultListableBeanFactory によって実装されます)。これは、参照によってインスタンスに挿入されるとき (つまり getBean) にインスタンス化され、DefaultSingletonBeanRegistry に登録されます。後続のシングルトンは DefaultSingletonBeanRegistry によって管理されます。
コントローラのロード
コントローラーのローディング機構
コントローラーの特別な点は、Spring がコントローラーを RequestMappingHandlerMapping に登録することです。したがって、コントローラーをホットロードするには 3 つの手順が必要です。
- BeanDefinitionの生成と登録
- インスタンスを作成して登録する
- 登録 RequestMappingHandlerMapping
コードは以下のように表示されます
// 获取bean工厂并转换为DefaultListableBeanFactory
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) ((ConfigurableApplicationContext)
applicationContext).getBeanFactory();
// 定义BeanDefinition
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
GenericBeanDefinition beanDefinition = (GenericBeanDefinition) beanDefinitionBuilder.getRawBeanDefinition();
//设置当前bean定义对象是单利的
beanDefinition.setScope("singleton");
// 将变量首字母置小写
beanName = StringUtils.uncapitalize(beanName);
// 将构建的BeanDefinition交由Spring管理
beanFactory.registerBeanDefinition(beanName, beanDefinition);
// 手动构建实例,并注入base service 防止卸载之后不再生成
Object obj = clazz.newInstance();
beanFactory.registerSingleton(beanName, obj);
log.info("register Singleton :" + beanName);
final RequestMappingHandlerMapping requestMappingHandlerMapping =
applicationContext.getBean(RequestMappingHandlerMapping.class);
if (requestMappingHandlerMapping != null) {
String handler = beanName;
Object controller = null;
try {
controller = applicationContext.getBean(handler);
} catch (Exception e) {
e.printStackTrace();
}
if (controller == null) {
return beanName;
}
// 注册Controller
Method method = requestMappingHandlerMapping.getClass().getSuperclass().getSuperclass().
getDeclaredMethod("detectHandlerMethods", Object.class);
// 将private改为可使用
method.setAccessible(true);
method.invoke(requestMappingHandlerMapping, handler);
}
IOCについて
実はBeanDefinitionを登録しておけばgetBean時に@Autowired @Resouceや構築メソッドのインジェクションまでSpringが自動で完了してくれるのですが、ここではカスタムインジェクションなどの業務処理を完了させるために自分でインスタンス化を完了させています。一部のプロキシ クラスの。
AOPについて
この書き方のデメリットとしては、getBean使用時に3層キャッシュにAOPが生成されるため、AOPが使用できないことですが、この方法でインジェクトしたい場合は、Springのソースコードを参照し、プロキシを構築してくださいクラスを作成してからそれを注入します。
サービスの読み込み
サービスの読み込み サービスに対応する実装クラスを直接インスタンス化して読み込むことができます 特別な処理は必要ないのでここではコードは載せません 読み込みの最初のステップはコントローラーと同じです
マッパーのロード
マッパーのロードは最も複雑な部分です。まず、マッパーには 2 種類あり、1 つは純粋な Mapper インターフェイス ファイルのロード、もう 1 つは XML ファイルのロードです。そして、マッパーをメモリに完全にロードできるように、Mybatis がどのようにロードされるかを分析する必要があります。ここでは、手順を次の手順に分けて説明します
- エイリアスの登録(主にXML用途)
- XMLファイルを解析する
- マッパーインターフェイスを解析し、マッパーを登録し、登録します
エイリアスを登録する
mybatis によるエイリアスの管理は、SqlSessionFactory の Configuration オブジェクトの TypeAliasRegistry に存在します (このオブジェクトは非常に重要で、mybatis によって読み込まれたリソースはこのオブジェクトで管理されます)。TypeAliasRegistry は HashMap を使用してエイリアスを管理します。ここでは registerAliases メソッドを直接呼び出すだけです。
SqlSessionFactory sqlSessionFactory = applicationContext.getBean(SqlSessionFactory.class);
sqlSessionFactory.getConfiguration().getTypeAliasRegistry().registerAlias(beanName.replace("/", "."), clazz);
XMLファイルを解析する
XML ファイルの解析は実際には比較的簡単です。XMLMapperBuilder を呼び出して解析するだけです。XMLMapperBuilder.parse メソッドは XML ファイルを解析し、resultMap、sqlFragments、およびmappedStatements を登録します。ただし、ここで注意が必要なのは、解析する際に、以前読み込んだデータを判断して削除する必要があることです。同様に、resultMaps、sqlFragments、mappedStatements もすべて SqlSessionFactory の Configuration に保持されます。リフレクションを通じてこれらのオブジェクトを取得すると、それを変更できます。コードは次のとおりです
/**
* 解析加载XML
*
* @param sqlSessionFactory
* @param jarFile jar对象
* @param jarEntry jar包中的XML对象
* @param name XML名称
* @throws IOException
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
public void xmlReload(SqlSessionFactory sqlSessionFactory, JarFile jarFile, JarEntry jarEntry, String name) throws IOException, NoSuchFieldException, IllegalAccessException {
// 2. 取得Configuration
Configuration targetConfiguration = sqlSessionFactory.getConfiguration();
Class<?> aClass = targetConfiguration.getClass();
if (targetConfiguration.getClass().getSimpleName().equals("MybatisConfiguration")) {
aClass = Configuration.class;
}
Set<String> loadedResources = (Set<String>) ObjectUtil.getFieldValue(targetConfiguration, aClass, "loadedResources");
loadedResources.remove(name);
// 3. 去掉之前加载的数据
Map<String, ResultMap> resultMaps = (Map<String, ResultMap>) ObjectUtil.getFieldValue(targetConfiguration, aClass, "resultMaps");
Map<String, XNode> sqlFragmentsMaps = (Map<String, XNode>) ObjectUtil.getFieldValue(targetConfiguration, aClass, "sqlFragments");
Map<String, MappedStatement> mappedStatementMaps = (Map<String, MappedStatement>) ObjectUtil.getFieldValue(targetConfiguration, aClass, "mappedStatements");
XPathParser parser = new XPathParser(jarFile.getInputStream(jarEntry), true, targetConfiguration.getVariables(), new XMLMapperEntityResolver());
XNode mapperXNode = parser.evalNode("/mapper");
List<XNode> resultMapNodes = mapperXNode.evalNodes("/mapper/resultMap");
String namespace = mapperXNode.getStringAttribute("namespace");
for (XNode xNode : resultMapNodes) {
String id = xNode.getStringAttribute("id", xNode.getValueBasedIdentifier());
resultMaps.remove(namespace + "." + id);
}
List<XNode> sqlNodes = mapperXNode.evalNodes("/mapper/sql");
for (XNode sqlNode : sqlNodes) {
String id = sqlNode.getStringAttribute("id", sqlNode.getValueBasedIdentifier());
sqlFragmentsMaps.remove(namespace + "." + id);
}
List<XNode> msNodes = mapperXNode.evalNodes("select|insert|update|delete");
for (XNode msNode : msNodes) {
String id = msNode.getStringAttribute("id", msNode.getValueBasedIdentifier());
mappedStatementMaps.remove(namespace + "." + id);
}
try {
// 4. 重新加载和解析被修改的 xml 文件
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(jarFile.getInputStream(jarEntry),
targetConfiguration, name, targetConfiguration.getSqlFragments());
xmlMapperBuilder.parse();
} catch (Exception e) {
log.error(e.getMessage(), e);
}
log.info("Parsed mapper file: '" + name + "'");
}
その他のクラスのロード
他のクラスのロードは比較的簡単です。クラスローダーを使用してこれらのクラスを直接ロードするだけです。Spring で管理する必要があるシングルトンの場合は、registerBeanDefinition で問題ありません。