目次
Java は Groovy のクラスとメソッドを呼び出します。
Groovy は Java クラスとメソッドを呼び出します。
GroovyShell を使用して Groovy スクリプトを実行します。
GroovyClassLoader を使用して Groovy スクリプトをロードして実行します。
最初のステップは、SpringBoot と統合し、依存関係を導入することです。
3 番目のステップは、テスト クラスを作成し、GroovyShell を使用してデモンストレーションを行うことです。
5 番目のステップは、SpringBoot を起動し、Groovy スクリプトの SpringContextUtil を通じて SpringBoot コンテナ内の Bean を取得することです。
最初のステップは、Groovy スクリプトを作成し、GroovyClassLoader を使用して実装することです。
2 番目のステップは、Groovy 呼び出し Bean を作成することです。
3 番目のステップは、クラスをロードする GroovyClassLoader を作成することです。
前回 2 回の記事では、Groovy の基本文法と GDK を体系的に学習しましたが、今回は、Groovy と Java を統合する方法と、それを SpringBoot プロジェクトに統合する方法を学習します。Groovy と Java は適切に統合されており、互いのコードや機能を呼び出して使用できます。Groovy と Java の統合により、両方の利点が最大限に活用され、開発がより柔軟かつ効率的になります。
I. 概要
1. Java で Groovy を使用する:
- Groovy 依存関係を Java プロジェクトに追加します。
- Java コードで Groovy クラスとスクリプトを使用します。Groovy コードは Java で直接実行でき、Groovy クラスのメソッドを呼び出したり、そのプロパティにアクセスしたりすることができます。Groovy スクリプトは、GroovyShell または GroovyClassLoader を使用して実行できます。
2. Groovy で Java を使用します。
- Groovy は Java をネイティブにサポートしており、Java クラスを直接使用したり、Java メソッドを呼び出したりすることができます。Groovy コードは Java コードと混合して作成できます。
- Groovy コードで Java クラスを使用する場合、追加のインポートは必要なく、直接使用するだけです。
- Groovy は、より簡潔な構文と、コードの記述を容易にするクロージャ、拡張メソッド、動的型などのより強力な機能も提供します。
より良い統合を実現するには、次の点に注意してください。
- 同じ依存関係管理ツールを使用して、Java プロジェクトと Groovy プロジェクトが同じバージョンの依存関係を使用するようにします。
- Java クラスがコンパイルされ、生成されたバイトコード ファイルと Groovy コードが相互にアクセスできるように同じクラスパスに配置されていることを確認してください。
- GroovyShell または GroovyClassLoader を使用して Groovy スクリプトを実行する場合は、Groovy コードが Java クラスにアクセスできるように、クラス パスに Java クラスのパスを含めます。
3. いくつかのパラダイムの違い、利点、欠点
Groovy と Java を統合するにはさまざまな方法がありますが、以下ではいくつかの一般的な方法、その違い、利点と欠点について説明します。
Java は Groovy のクラスとメソッドを呼び出します。
- 説明: Java は、クラスパスを通じて Groovy クラスおよびメソッドに直接アクセスし、Groovy コードを Java コードの一部として扱うことができます。Groovy クラスのメソッドを呼び出したり、そのプロパティにアクセスしたりできます。
- 異なる点: Java は、Java コードを呼び出すのと同じように、Groovy のクラスとメソッドをシームレスに呼び出すことができます。
- 利点: シンプルかつ直接的なので、Groovy と Java の混合コードを記述するのに非常に便利です。
- 欠点: クロージャ、動的型付けなど、Groovy に固有の機能については、Java が完全には理解できない可能性があります。
Groovy は Java クラスとメソッドを呼び出します。
- 説明: Groovy は Java を当然サポートしており、Java クラスを直接使用したり、Java メソッドを呼び出したりすることができます。Groovy コードは、追加のインポートを行わずに Java コードと混合して作成できます。
- 違い: Groovy と Java の統合は非常に調和的であり、Java クラスを自動的にインポートし、Java 構文を直接使用できます。
- 利点: シームレスな統合により、Java エコシステムと既存のライブラリを最大限に活用できます。
- 欠点: Groovy は、いくつかの点で Java よりも「動的」である可能性があります。これは、場合によってはパフォーマンスと型の安全性が失われる可能性があることを意味します。
GroovyShell を使用して Groovy スクリプトを実行します。
- 説明: GroovyShell を使用して、Java コード内の Groovy スクリプト コードのブロックを実行します。Groovy スクリプトは動的にロードして実行できます。
- 異なる点: GroovyShell の Evaluate メソッドを通じて、動的な Groovy スクリプト コードを Java で実行できます。
- 利点: 実行時に Groovy スクリプトを動的に実行できるため、柔軟性が高く、便利で、高速です。
- 欠点: スクリプトを動的に実行すると、パフォーマンスに一定の影響が出る可能性があり、追加の構文チェックが必要になります。
GroovyClassLoader を使用して Groovy スクリプトをロードして実行します。
- 説明: Java の GroovyClassLoader を介して Groovy スクリプトをロードして実行します。これにより、より柔軟なスクリプト実行が実現します。
- 異なります。GroovyClassLoader を介して Groovy スクリプトをロードすると、対応する Class オブジェクトが取得され、インスタンス化され、必要に応じて呼び出されます。
- 利点: Groovy スクリプトは柔軟にロードして実行でき、Java コードと対話できます。欠点: GroovyShell と比較して、GroovyClassLoader を使用すると、読み込みと実行を実装するためにより多くのコードが必要になります。
要約すると、Groovy と Java の統合方法が異なれば、それぞれ長所と短所も異なります。特定のニーズに応じて適切な方法を選択できます。Java を使用して Groovy のクラスとメソッドを呼び出し、Groovy を使用して Java のクラスとメソッドを呼び出すのが、最も直接的でシームレスな統合方法です。GroovyShell または GroovyClassLoader を使用して Groovy スクリプトを実行すると、より柔軟であり、スクリプトの動的な実行が必要なシナリオに適しています。
2、実戦
次に、SpringBoot がどのように Groovy スクリプトを統合し、実際の開発に適用するかを紹介します。
最初のステップは、SpringBoot と統合し、依存関係を導入することです。
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>3.0.17</version>
<type>pom</type>
</dependency>
第 2 ステップ、魅力的なスクリプトを作成する
package script
import com.example.groovy.GroovyInvokeJavaDemo
import com.example.groovy.groovyshell.ShellGroovyDTO
import com.example.groovy.utils.SpringContextUtil
/**
* @Author: lly
* @Date: 2023/7/1
*/
def helloWord() {
return "hello groovy"
}
helloWord()
def cal(int a, int b) {
ShellGroovyDTO dto = new ShellGroovyDTO()
dto.setA(a)
dto.setB(b)
if (b > 0) {
dto.setNum(a + b)
} else {
dto.setNum(a)
}
return dto
};
cal(a, b)
/** 定义静态变量 **/
class Globals {
static String PARAM1 = "静态变量"
static int[] arrayList = [1, 2]
}
def groovyInvokeJavaMethod(int a, int b) {
GroovyInvokeJavaDemo groovyInvokeJavaDemo = SpringContextUtil.getBean("groovyInvokeJavaDemo")
// return groovyInvokeJavaDemo.groovyInvokeJava();
return groovyInvokeJavaDemo.groovyInvokeJavaParam(a, b);
}
groovyInvokeJavaMethod(a, b)
3 番目のステップは、テスト クラスを作成し、GroovyShell を使用してデモンストレーションを行うことです。
* // GroovyShell インスタンスを作成する
* // パラメーターを渡し、結果を受け取るための Binding オブジェクトを作成します
* // パラメータの設定
* // Groovy スクリプトを実行します
* // 結果を取得する
package com.example.groovy.groovyshell;
import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
/**
* @Author: lly
* @Date: 2023/7/1
* <p>
* 下面这段测试类包含两个,一个是有参数的调用,一个是无参数的调用
* // 创建GroovyShell实例
* // 创建Binding对象,用于传递参数和接收结果
* // 设置参数
* // 执行Groovy脚本
* // 获取结果
*/
public class GroovyShellApp {
/**
* GroovyShell 无参数 demo
**/
public static void main(String[] args) {
String groovyStr = "package script\n" +
"\n" +
"import com.example.groovy.groovyshell.ShellGroovyDTO\n" +
"\n" +
"/**\n" +
" * @Author: lly\n" +
" * @Date: 2023/7/1\n" +
" */\n" +
"\n" +
"def helloWord() {\n" +
" return \"hello groovy\"\n" +
"}\n" +
"\n" +
"helloWord()\n" +
"\n" +
"def cal(int a, int b) {\n" +
" ShellGroovyDTO dto = new ShellGroovyDTO()\n" +
" dto.setA(a)\n" +
" dto.setB(b)\n" +
" if (b > 0) {\n" +
" dto.setNum(a + b)\n" +
" } else {\n" +
" dto.setNum(a)\n" +
" }\n" +
" return dto\n" +
"};\n" +
"\n" +
"cal(a , b)";
// 创建GroovyShell实例
GroovyShell shell = new GroovyShell();
Script script = shell.parse(groovyStr);
Object helloWord = script.invokeMethod("helloWord", null);
System.out.println(helloWord);
}
/** GroovyShell 有参数 demo **/
// public static void main(String[] args) {
//
// String groovyStr = "package script\n" +
// "\n" +
// "import com.example.groovy.groovyshell.ShellGroovyDTO\n" +
// "\n" +
// "/**\n" +
// " * @Author: lly\n" +
// " * @Date: 2023/7/1\n" +
// " */\n" +
// "def cal(int a, int b) {\n" +
// " ShellGroovyDTO dto = new ShellGroovyDTO()\n" +
// " dto.setA(a)\n" +
// " dto.setB(b)\n" +
// " if (b > 0) {\n" +
// " dto.setNum(a + b)\n" +
// " } else {\n" +
// " dto.setNum(a)\n" +
// " }\n" +
// " return dto\n" +
// "};\n" +
// "\n" +
// "cal(a, b)";
//
// // 创建Binding对象,用于传递参数和接收结果
// Binding binding = new Binding();
//
// // 创建GroovyShell实例
// GroovyShell shell = new GroovyShell(binding);
//
//
// // 设置参数
// binding.setVariable("a", 10);
// binding.setVariable("b", 20);
//
// // 执行Groovy脚本
// Object result = shell.evaluate(groovyStr);
//
// // 获取结果
// ShellGroovyDTO dto = (ShellGroovyDTO) result;
// System.out.println(dto);
// }
}
package com.example.groovy.groovyshell;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @Author: lly
* @Date: 2023/7/1
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class ShellGroovyDTO {
private Integer a;
private Integer b;
private Integer num;
}
4 番目のステップでは、実行結果を表示します。
5 番目のステップは、SpringBoot を起動し、Groovy スクリプトの SpringContextUtil を通じて SpringBoot コンテナ内の Bean を取得することです。
上記の手順は Pure Java コードで Groovy を呼び出す手順です。実際、開発プロセスでは通常、Groovy コードと Java コードを相互調整します。次に、Groovy の SpringContextUtil を通じて SpringBoot コンテナ内の Bean を取得し、ターゲットを呼び出す方法を見てみましょう。方法。
package com.example.groovy.utils;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;
/**
* @Author: lly
* @Date: 2023/7/2
*/
@Service
public class SpringContextUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringContextUtil.applicationContext = applicationContext;
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
/**
* 通过name获取 Bean.
*
* @param name
* @return
*/
public static Object getBean(String name) {
return getApplicationContext().getBean(name);
}
/**
* 通过class获取Bean.
*
* @param clazz
* @param <T>
* @return
*/
public static <T> T getBean(Class<T> clazz) {
return getApplicationContext().getBean(clazz);
}
/**
* 通过name,以及Clazz返回指定的Bean
*
* @param name
* @param clazz
* @param <T>
* @return
*/
public static <T> T getBean(String name, Class<T> clazz) {
return getApplicationContext().getBean(name, clazz);
}
}
ステップ6. コンテナ内にBeanを作成する
「GroovyInvokeJavaDemo」Bean を作成し、管理のために Spring に渡します。ターゲット メソッドは 2 つあり、1 つはパラメータを必要とし、もう 1 つはパラメータを必要としません。Java 経由で Groovy を呼び出すときに必要なパラメータは、Groovy で Java メソッドが呼び出されるときに渡されます。
package com.example.groovy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* @Author: lly
* @Date: 2023/7/2
*/
@Service
@Slf4j
public class GroovyInvokeJavaDemo {
public String groovyInvokeJava() {
List<String> lits = new ArrayList<>();
log.info("this is SpringBoot class, groovy script invoke this method ...");
return "this is SpringBoot class, groovy script invoke this method ...";
}
public String groovyInvokeJavaParam(int a, int b) {
List<String> lits = new ArrayList<>();
log.info("this is SpringBoot class, groovy script invoke this method ,param is a:{}, b:{}", a, b);
return "this is SpringBoot class, groovy script invoke this method , a:" + a + ", b:" + b;
}
}
ステップ 7. アクセスコード
package com.example.groovy;
import com.example.groovy.classloader.GroovyClassLoaderRule;
import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* @Author: lly
* @Date: 2023/7/2
*/
@RestController
@RequestMapping("/groovy")
public class GroovyInvokeJavaSpringController {
@Resource
private GroovyClassLoaderRule groovyClassLoaderRule;
@RequestMapping("/groovy-shell/spring")
public String groovyInvokeJavaMethodTest() {
String groovyStr = "package script\n" +
"\n" +
"import com.example.groovy.GroovyInvokeJavaDemo\n" +
"import com.example.groovy.groovyshell.ShellGroovyDTO\n" +
"import com.example.groovy.utils.SpringContextUtil\n" +
"\n" +
"/**\n" +
" * @Author: lly\n" +
" * @Date: 2023/7/1\n" +
" */\n" +
"\n" +
"def helloWord() {\n" +
" return \"hello groovy\"\n" +
"}\n" +
"\n" +
"helloWord()\n" +
"\n" +
"def cal(int a, int b) {\n" +
" ShellGroovyDTO dto = new ShellGroovyDTO()\n" +
" dto.setA(a)\n" +
" dto.setB(b)\n" +
" if (b > 0) {\n" +
" dto.setNum(a + b)\n" +
" } else {\n" +
" dto.setNum(a)\n" +
" }\n" +
" return dto\n" +
"};\n" +
"\n" +
"cal(a , b)\n" +
"\n" +
"/** 定义静态变量 **/\n" +
"class Globals {\n" +
" static String PARAM1 = \"静态变量\"\n" +
" static int[] arrayList = [1, 2]\n" +
"}\n" +
"\n" +
"def groovyInvokeJavaMethod(int a, int b) {\n" +
" GroovyInvokeJavaDemo groovyInvokeJavaDemo = SpringContextUtil.getBean(\"groovyInvokeJavaDemo\")\n" +
"// return groovyInvokeJavaDemo.groovyInvokeJava();\n" +
" return groovyInvokeJavaDemo.groovyInvokeJavaParam(a, b);\n" +
"}\n" +
"\n" +
"groovyInvokeJavaMethod(a, b)";
Binding binding = new Binding();
binding.setVariable("a", 100);
binding.setVariable("b", 100);
GroovyShell groovyShell = new GroovyShell(binding);
Object evaluate = groovyShell.evaluate(groovyStr);
groovyShell.getClassLoader().clearCache();
return (String) evaluate;
}
}
8番目のステップ、開始、インターフェイステスト
効果を確認するには、「http://localhost:8080/groovy/groovy-shell/spring 」にアクセスしてください。
3. 最適化
上記では Groovy を Java に統合しましたが、上記のコードには大きな問題があり、それは主に次の 2 つの側面に反映されています。
第 1 の側面: 5 番目のステップを通じて、Groovy で SpringBoot コンテナ オブジェクトを取得でき、コンテナ オブジェクトを取得することでコンテナ内のすべてのものを取得できることがわかります。これは非常に便利で柔軟性がありますが、非常に危険です。権限制御が適切に行われていない場合、Groovy スクリプトはシステムを攻撃する最も強力な武器になります。
2 番目の側面: Groovy スクリプトが適切に使用されないと、OOM が発生し、最終的にはサーバーがダウンします。このメソッドが呼び出されるたびにGroovyShellやScriptなどのインスタンスが生成されるため、呼び出し回数が増えると必然的にOOMが発生します。
解決策は、GroovyClassLoader の clearCache() 関数を使用して、呼び出し後に GroovyShell、Script、およびその他のインスタンスを破棄することですが、実際にはこれだけでは十分ではありません。OOM の理由は、GroovyShell と Script のインスタンスが多すぎるだけではありません。スクリプト内の Java コードでもオブジェクトまたは新しいインスタンスが作成される場合、たとえ GroovyShell が破棄されても、スクリプト内のオブジェクトは破棄されないことがわかります。したがって、Groovy コードではキャッシュ管理を使用するのが最適です。
最初のステップは、Groovy スクリプトを作成し、GroovyClassLoader を使用して実装することです。
GroovyClassLoad_1.groovy
package script
import com.example.groovy.GroovyInvokeJavaDemo
import com.example.groovy.groovyshell.ShellGroovyDTO
import com.example.groovy.utils.SpringContextUtil
/**
* @Author: lly
* @Date: 2023/7/1
*/
def groovyInvokeJavaMethod(int a, int b) {
GroovyInvokeJavaDemo groovyInvokeJavaDemo = SpringContextUtil.getBean("groovyInvokeJavaDemo")
// return groovyInvokeJavaDemo.groovyInvokeJava();
return groovyInvokeJavaDemo.groovyInvokeJavaParam(a, b)
}
groovyInvokeJavaMethod(a, b)
GroovyClassLoad_2.groovy
package script
import com.example.groovy.GroovyInvokeJavaDemo
import com.example.groovy.groovyshell.ShellGroovyDTO
import com.example.groovy.utils.SpringContextUtil
/**
* @Author: lly
* @Date: 2023/7/1
*/
def groovyInvokeJavaMethod(int a, int b) {
GroovyInvokeJavaDemo groovyInvokeJavaDemo = SpringContextUtil.getBean("groovyInvokeJavaDemo")
// return groovyInvokeJavaDemo.groovyInvokeJava();
return groovyInvokeJavaDemo.groovyInvokeJavaParam(a, b)
}
groovyInvokeJavaMethod(a, b)
2 番目のステップは、Groovy 呼び出し Bean を作成することです。
package com.example.groovy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* @Author: lly
* @Date: 2023/7/2
*/
@Service
@Slf4j
public class GroovyInvokeJavaDemo {
public String groovyInvokeJava() {
List<String> lits = new ArrayList<>();
log.info("this is SpringBoot class, groovy script invoke this method ...");
return "this is SpringBoot class, groovy script invoke this method ...";
}
public String groovyInvokeJavaParam(int a, int b) {
List<String> lits = new ArrayList<>();
log.info("this is SpringBoot class, groovy script invoke this method ,param is a:{}, b:{}", a, b);
return "this is SpringBoot class, groovy script invoke this method , a:" + a + ", b:" + b;
}
}
3 番目のステップは、クラスをロードする GroovyClassLoader を作成することです。
package com.example.groovy.classloader;
/**
* @Author: lly
* @Date: 2023/7/1
*
* 定义 Groovy 执行的接口
*/
public interface GroovyClassLoaderRule {
String run();
}
package com.example.groovy.classloader;
import groovy.lang.Binding;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyObject;
import groovy.lang.Script;
import lombok.extern.slf4j.Slf4j;
import org.apache.groovy.parser.antlr4.util.StringUtils;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
/**
* @Author: lly
* @Date: 2023/7/1
*/
@Slf4j
@Service
public class GroovyClassLoaderRuleImpl implements GroovyClassLoaderRule {
/**
* 脚本容器 :缓存Script,避免创建太多
**/
private static final Map<String, GroovyObject> SCRIPT_MAP = new HashMap<>();
private static final GroovyClassLoader CLASS_LOADER = new GroovyClassLoader();
public static GroovyObject loadScript(String key, String rule) {
if (SCRIPT_MAP.containsKey(key)) {
return SCRIPT_MAP.get(key);
}
GroovyObject groovyObject = loadScript(rule);
SCRIPT_MAP.put(key, groovyObject);
return groovyObject;
}
public static GroovyObject loadScript(String rule) {
if (StringUtils.isEmpty(rule)) {
return null;
}
try {
Class ruleClazz = CLASS_LOADER.parseClass(rule);
if (ruleClazz != null) {
log.info("load rule:" + rule + " success!");
GroovyObject groovyObject = (GroovyObject) ruleClazz.newInstance();
return groovyObject;
}
} catch (Exception e) {
log.error(e.getMessage(), e);
} finally {
CLASS_LOADER.clearCache();
}
log.error("load rule error, can not load Script");
return null;
}
@Override
public String run() {
// 业务逻辑执行,方便配置
String groovyClassLoader1 = "package script\n" +
"\n" +
"import com.example.groovy.GroovyInvokeJavaDemo\n" +
"import com.example.groovy.groovyshell.ShellGroovyDTO\n" +
"import com.example.groovy.utils.SpringContextUtil\n" +
"\n" +
"/**\n" +
" * @Author: lly\n" +
" * @Date: 2023/7/1\n" +
" */\n" +
"\n" +
"def groovyInvokeJavaMethod(int a, int b) {\n" +
" GroovyInvokeJavaDemo groovyInvokeJavaDemo = SpringContextUtil.getBean(\"groovyInvokeJavaDemo\")\n" +
"// return groovyInvokeJavaDemo.groovyInvokeJava();\n" +
"\n" +
" return groovyInvokeJavaDemo.groovyInvokeJavaParam(a, b)\n" +
"}\n" +
"\n" +
"groovyInvokeJavaMethod(a, b)";
String groovyClassLoader2 = "package script\n" +
"\n" +
"import com.example.groovy.GroovyInvokeJavaDemo\n" +
"import com.example.groovy.groovyshell.ShellGroovyDTO\n" +
"import com.example.groovy.utils.SpringContextUtil\n" +
"\n" +
"/**\n" +
" * @Author: lly\n" +
" * @Date: 2023/7/1\n" +
" */\n" +
"\n" +
"def groovyInvokeJavaMethod(int a, int b) {\n" +
" GroovyInvokeJavaDemo groovyInvokeJavaDemo = SpringContextUtil.getBean(\"groovyInvokeJavaDemo\")\n" +
"// return groovyInvokeJavaDemo.groovyInvokeJava();\n" +
"\n" +
" return groovyInvokeJavaDemo.groovyInvokeJavaParam(a, b)\n" +
"}\n" +
"\n" +
"groovyInvokeJavaMethod(a, b)";
Binding binding = new Binding();
binding.setVariable("a", 300);
binding.setVariable("b", 400);
// Script classLoader1 = loadScript("groovyClassLoader1", groovyClassLoader1, binding);
GroovyObject groovyObject = loadScript("groovyClassLoader2", groovyClassLoader2);
Object groovyInvokeJavaMethod = groovyObject.invokeMethod("groovyInvokeJavaMethod", new Object[]{100, 200});
return (String) groovyInvokeJavaMethod;
}
}
4 番目のステップでは、リクエスト API を作成します。
@RequestMapping("/groovy-class-loader/spring")
public String groovyClassLoaderRuleTest() {
String result = groovyClassLoaderRule.run();
return result;
}
5番目のステップ、検証の開始
この時点で、Groovy シリーズは終了です。コードが必要な場合は、私の gitHub Web サイトにアクセスして入手するか、メッセージを残してください。お役に立てればと思い、プライベート メッセージをお送りします。