それは私のせいではありません。Springでこの素晴らしいネットワークツールライブラリを使用したことはないと思います。

1.はじめに

今日のITプロジェクトでは、サーバーが外部へのネットワーク要求を開始するシーンは基本的にどこにでもあります。

従来、サーバー側のコードでhttpサービスにアクセスする場合、通常はJDKのHttpURLConnectionまたはApacheのHttpClientを使用しますが、この方法は使用するのが面倒であり、APIの使用は非常に複雑であり、リソースのリサイクルについて心配する必要があります。

例としてファイルのダウンロードを取り上げ、ApacheのHttpClientメソッドを使用してファイルをダウンロードします。以下は、前にカプセル化したコードロジックです。これがどれほど複雑かを確認してください。

実際、Springは、操作するためのシンプルで便利なテンプレートクラスであるRestTemplateを提供してくれました。

RestTemplateは、HTTPリクエストを実行する同期ブロッキングツールクラスであり、HTTPクライアントライブラリ(JDK HttpURLConnection、Apache HttpComponents、okHttpなど)に基づいた、よりシンプルで使いやすいテンプレートメソッドAPIをカプセル化するだけです。使用するプログラマー提供されているテンプレートメソッドは、ネットワークリクエストと処理を開始します。これにより、開発効率が大幅に向上します。

さて、これ以上BBはありません、コードを始めましょう!

2.環境構成

2.1。Spring以外の環境でのRestTemplateの使用

現在のプロジェクトがSpringプロジェクトでない場合は、Spring-Webパッケージを追加してRestTemplateクラスを導入します

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-web</artifactId>
  <version>5.2.6.RELEASE</version>
</dependency>

RestTemplateを使用してGETリクエストを送信し、プログラムが正しく実行されるかどうかを確認する単体テストクラスを作成します

@Test
public void simpleTest() {
    RestTemplate restTemplate = new RestTemplate();
    String url = "http://jsonplaceholder.typicode.com/posts/1";
    String str = restTemplate.getForObject(url, String.class);
    System.out.println(str);
}

2.2。Spring環境でのRestTemplateの使用

現在のプロジェクトがSpringBootの場合は、次の依存関係インターフェイスを追加してください。

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

同時に、RestTemplate構成をBeanとして初期化します。

@Configuration
public class RestTemplateConfig {

    /**
     * 没有实例化RestTemplate时,初始化RestTemplate
     * @return
     */
    @ConditionalOnMissingBean(RestTemplate.class)
    @Bean
    public RestTemplate restTemplate(){
        RestTemplate restTemplate = new RestTemplate();
        return restTemplate;
    }
}

この初期化メソッドは、基盤となるHTTPクライアント実装としてJDK独自のHttpURLConnectionを使用することに注意してください。

もちろん、次のように、RestTemplateのデフォルトクライアントを変更することもできます。たとえば、HttpClientクライアントに変更することもできます。

@Configuration
public class RestTemplateConfig {


    /**
     * 没有实例化RestTemplate时,初始化RestTemplate
     * @return
     */
    @ConditionalOnMissingBean(RestTemplate.class)
    @Bean
    public RestTemplate restTemplate(){
        RestTemplate restTemplate = new RestTemplate(getClientHttpRequestFactory());
        return restTemplate;
    }

    /**
     * 使用HttpClient作为底层客户端
     * @return
     */
    private ClientHttpRequestFactory getClientHttpRequestFactory() {
        int timeout = 5000;
        RequestConfig config = RequestConfig.custom()
                .setConnectTimeout(timeout)
                .setConnectionRequestTimeout(timeout)
                .setSocketTimeout(timeout)
                .build();
        CloseableHttpClient client = HttpClientBuilder
                .create()
                .setDefaultRequestConfig(config)
                .build();
        return new HttpComponentsClientHttpRequestFactory(client);
    }

}

RestTemplateを使用する必要がある場合は、それを挿入して使用してください。

@Autowired
private RestTemplate restTemplate;

開発者のフィードバック、およびインターネット上でのさまざまなHTTPクライアントのパフォーマンスと使いやすさの評価から、OkHttpはApacheのHttpClientよりも優れており、ApacheのHttpClientはHttpURLConnectionよりも優れています。

したがって、次の方法で、基盤となるhttpクライアントをOkHttpに置き換えることもできます。

/**
 * 使用OkHttpClient作为底层客户端
 * @return
 */
private ClientHttpRequestFactory getClientHttpRequestFactory(){
    OkHttpClient okHttpClient = new OkHttpClient.Builder()
            .connectTimeout(5, TimeUnit.SECONDS)
            .writeTimeout(5, TimeUnit.SECONDS)
            .readTimeout(5, TimeUnit.SECONDS)
            .build();
    return new OkHttp3ClientHttpRequestFactory(okHttpClient);
}

3.APIプラクティス

RestTemplateの最大の機能は、さまざまなネットワークリクエストメソッドをラップすることです。これにより、開発者の作業負荷が大幅に簡素化されます。GET、POST、PUT、DELETE、ファイルのアップロードとダウンロードを例として、各APIの使用法を紹介しましょう。

3.1。GETリクエスト

RestTemplateを介してHTTPGETプロトコルリクエストを送信するために一般的に使用される2つの方法があります。

  • getForObject()
  • getForEntity()

2つの主な違いは、getForObject()の戻り値がHTTPプロトコルの応答本体であるということです。

getForEntity()は、HTTP応答をカプセル化するResponseEntityを返します。応答本文に加えて、HTTPステータスコード、contentType、contentLength、ヘッダー、およびその他の情報も含まれます。

Spring Boot環境で単体テストケースを作成するには、最初にApiインターフェイスを作成してから、サービステスト用の単体テストを作成します。

  • パラメータなしでリクエストを取得
@RestController
public class TestController {

    /**
     * 不带参的get请求
     * @return
     */
    @RequestMapping(value = "testGet", method = RequestMethod.GET)
    public ResponseBean testGet(){
        ResponseBean result = new ResponseBean();
        result.setCode("200");
        result.setMsg("请求成功,方法:testGet");
        return result;
    }
}
public class ResponseBean {

    private String code;

    private String msg;

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    @Override
    public String toString() {
        return "ResponseBean{" +
                "code='" + code + '\'' +
                ", msg='" + msg + '\'' +
                '}';
    }
}
@Autowired
private RestTemplate restTemplate;

/**
 * 单元测试(不带参的get请求)
 */
@Test
public void testGet(){
    //请求地址
    String url = "http://localhost:8080/testGet";

    //发起请求,直接返回对象
    ResponseBean responseBean = restTemplate.getForObject(url, ResponseBean.class);
    System.out.println(responseBean.toString());
}
  • パラメータを使用してリクエストを取得する(RESTfulスタイル)
@RestController
public class TestController {

    /**
     * 带参的get请求(restful风格)
     * @return
     */
    @RequestMapping(value = "testGetByRestFul/{id}/{name}", method = RequestMethod.GET)
    public ResponseBean testGetByRestFul(@PathVariable(value = "id") String id, @PathVariable(value = "name") String name){
        ResponseBean result = new ResponseBean();
        result.setCode("200");
        result.setMsg("请求成功,方法:testGetByRestFul,请求参数id:" +  id + "请求参数name:" + name);
        return result;
    }
}
@Autowired
private RestTemplate restTemplate;


 /**
 * 单元测试(带参的get请求)
 */
@Test
public void testGetByRestFul(){
    //请求地址
    String url = "http://localhost:8080/testGetByRestFul/{1}/{2}";

    //发起请求,直接返回对象(restful风格)
    ResponseBean responseBean = restTemplate.getForObject(url, ResponseBean.class, "001", "张三");
    System.out.println(responseBean.toString());
}
  • パラメーターを使用してリクエストを取得する(プレースホルダーを使用してパラメーターを渡す)
@RestController
public class TestController {

    /**
     * 带参的get请求(使用占位符号传参)
     * @return
     */
    @RequestMapping(value = "testGetByParam", method = RequestMethod.GET)
    public ResponseBean testGetByParam(@RequestParam("userName") String userName,
                                             @RequestParam("userPwd") String userPwd){
        ResponseBean result = new ResponseBean();
        result.setCode("200");
        result.setMsg("请求成功,方法:testGetByParam,请求参数userName:" +  userName + ",userPwd:" + userPwd);
        return result;
    }
}
@Autowired
private RestTemplate restTemplate;

 /**
 * 单元测试(带参的get请求)
 */
@Test
public void testGetByParam(){
    //请求地址
    String url = "http://localhost:8080/testGetByParam?userName={userName}&userPwd={userPwd}";

    //请求参数
    Map<String, String> uriVariables = new HashMap<>();
    uriVariables.put("userName", "唐三藏");
    uriVariables.put("userPwd", "123456");

    //发起请求,直接返回对象(带参数请求)
    ResponseBean responseBean = restTemplate.getForObject(url, ResponseBean.class, uriVariables);
    System.out.println(responseBean.toString());
}

上記のgetForObjectリクエストパラメータ転送メソッド、getForEntityのすべてを使用でき、使用方法はほぼ同じですが、受け取る結果にはわずかな違いがあります。

ResponseEntity <T> responseEntityを使用して、応答結果を受け取ります。responseEntity.getBody()を使用して応答本文を取得します。

 /**
 * 单元测试
 */
@Test
public void testAllGet(){
    //请求地址
    String url = "http://localhost:8080/testGet";

    //发起请求,返回全部信息
    ResponseEntity<ResponseBean> response = restTemplate.getForEntity(url, ResponseBean.class);

    // 获取响应体
    System.out.println("HTTP 响应body:" + response.getBody().toString());

    // 以下是getForEntity比getForObject多出来的内容
    HttpStatus statusCode = response.getStatusCode();
    int statusCodeValue = response.getStatusCodeValue();
    HttpHeaders headers = response.getHeaders();

    System.out.println("HTTP 响应状态:" + statusCode);
    System.out.println("HTTP 响应状态码:" + statusCodeValue);
    System.out.println("HTTP Headers信息:" + headers);
}

3.2、POSTリクエスト

実際、POSTリクエストメソッドはGETリクエストメソッドに似ています。RestTemplateのPOSTリクエストには、次の2つの主要なメソッドも含まれています。

  • postForObject()
  • postForEntity()

postForEntity()はすべての情報を返し、postForObject()メソッドはbodyオブジェクトを返します。具体的な使用法は次のとおりです。

  • フォームリクエストのシミュレーション、メソッドテスト後
@RestController
public class TestController {

    /**
     * 模拟表单请求,post方法测试
     * @return
     */
    @RequestMapping(value = "testPostByForm", method = RequestMethod.POST)
    public ResponseBean testPostByForm(@RequestParam("userName") String userName,
                                        @RequestParam("userPwd") String userPwd){
        ResponseBean result = new ResponseBean();
        result.setCode("200");
        result.setMsg("请求成功,方法:testPostByForm,请求参数userName:" + userName + ",userPwd:" + userPwd);
        return result;
    }
}
@Autowired
private RestTemplate restTemplate;

/**
 * 模拟表单提交,post请求
 */
@Test
public void testPostByForm(){
    //请求地址
    String url = "http://localhost:8080/testPostByForm";

    // 请求头设置,x-www-form-urlencoded格式的数据
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

    //提交参数设置
    MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
    map.add("userName", "唐三藏");
    map.add("userPwd", "123456");

    // 组装请求体
    HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);

    //发起请求
    ResponseBean responseBean = restTemplate.postForObject(url, request, ResponseBean.class);
    System.out.println(responseBean.toString());
}
  • フォームリクエストのシミュレーション、postメソッドテスト(オブジェクトが受け入れられました)
@RestController
public class TestController {

    /**
     * 模拟表单请求,post方法测试
     * @param request
     * @return
     */
    @RequestMapping(value = "testPostByFormAndObj", method = RequestMethod.POST)
    public ResponseBean testPostByForm(RequestBean request){
        ResponseBean result = new ResponseBean();
        result.setCode("200");
        result.setMsg("请求成功,方法:testPostByFormAndObj,请求参数:" + JSON.toJSONString(request));
        return result;
    }
}
public class RequestBean {


    private String userName;


    private String userPwd;


    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getUserPwd() {
        return userPwd;
    }

    public void setUserPwd(String userPwd) {
        this.userPwd = userPwd;
    }
}
@Autowired
private RestTemplate restTemplate;

/**
 * 模拟表单提交,post请求
 */
@Test
public void testPostByForm(){
    //请求地址
    String url = "http://localhost:8080/testPostByFormAndObj";

    // 请求头设置,x-www-form-urlencoded格式的数据
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

    //提交参数设置
    MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
    map.add("userName", "唐三藏");
    map.add("userPwd", "123456");

    // 组装请求体
    HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);

    //发起请求
    ResponseBean responseBean = restTemplate.postForObject(url, request, ResponseBean.class);
    System.out.println(responseBean.toString());
}
  • JSONリクエストのモック、メソッドテスト後
@RestController
public class TestController {

    /**
     * 模拟JSON请求,post方法测试
     * @param request
     * @return
     */
    @RequestMapping(value = "testPostByJson", method = RequestMethod.POST)
    public ResponseBean testPostByJson(@RequestBody RequestBean request){
        ResponseBean result = new ResponseBean();
        result.setCode("200");
        result.setMsg("请求成功,方法:testPostByJson,请求参数:" + JSON.toJSONString(request));
        return result;
    }
}
@Autowired
private RestTemplate restTemplate;

/**
 * 模拟JSON提交,post请求
 */
@Test
public void testPostByJson(){
    //请求地址
    String url = "http://localhost:8080/testPostByJson";

    //入参
    RequestBean request = new RequestBean();
    request.setUserName("唐三藏");
    request.setUserPwd("123456789");

    //发送post请求,并打印结果,以String类型接收响应结果JSON字符串
    ResponseBean responseBean = restTemplate.postForObject(url, request, ResponseBean.class);
    System.out.println(responseBean.toString());
}
  • ページリダイレクトのシミュレーション、リクエストの投稿
@Controller
public class LoginController {

    /**
     * 重定向
     * @param request
     * @return
     */
    @RequestMapping(value = "testPostByLocation", method = RequestMethod.POST)
    public String testPostByLocation(@RequestBody RequestBean request){
        return "redirect:index.html";
    }
}

@Autowired
private RestTemplate restTemplate;

/**
 * 重定向,post请求
 */
@Test
public void testPostByLocation(){
    //请求地址
    String url = "http://localhost:8080/testPostByLocation";

    //入参
    RequestBean request = new RequestBean();
    request.setUserName("唐三藏");
    request.setUserPwd("123456789");

    //用于提交完成数据之后的页面跳转,返回跳转url
    URI uri = restTemplate.postForLocation(url, request);
    System.out.println(uri.toString());
}

出力は次のとおりです。

http://localhost:8080/index.html

3.3.PUTリクエスト

put requestメソッドは、多くの人が使用することはありません。既存のリソースを変更したり、リソースを挿入したりすることを指します。このメソッドは、URLで表されるリソースにHTTPPUTメソッドリクエストを送信します。例は次のとおりです。

@RestController
public class TestController {

    /**
     * 模拟JSON请求,put方法测试
     * @param request
     * @return
     */
    @RequestMapping(value = "testPutByJson", method = RequestMethod.PUT)
    public void testPutByJson(@RequestBody RequestBean request){
        System.out.println("请求成功,方法:testPutByJson,请求参数:" + JSON.toJSONString(request));
    }
}
@Autowired
private RestTemplate restTemplate;

/**
 * 模拟JSON提交,put请求
 */
@Test
public void testPutByJson(){
    //请求地址
    String url = "http://localhost:8080/testPutByJson";

    //入参
    RequestBean request = new RequestBean();
    request.setUserName("唐三藏");
    request.setUserPwd("123456789");

    //模拟JSON提交,put请求
    restTemplate.put(url, request);
}

3.4.DELETEリクエスト

これに対応するのは、既存のリソースを削除することを意味するdeleteメソッドプロトコルです。このメソッドは、URLで表されるリソースにHTTPDELETEメソッド要求を送信します。

@RestController
public class TestController {

    /**
     * 模拟JSON请求,delete方法测试
     * @return
     */
    @RequestMapping(value = "testDeleteByJson", method = RequestMethod.DELETE)
    public void testDeleteByJson(){
        System.out.println("请求成功,方法:testDeleteByJson");
    }
}
@Autowired
private RestTemplate restTemplate;

/**
 * 模拟JSON提交,delete请求
 */
@Test
public void testDeleteByJson(){
    //请求地址
    String url = "http://localhost:8080/testDeleteByJson";

    //模拟JSON提交,delete请求
    restTemplate.delete(url);
}

3.5、一般的なリクエスト方法交換方法

上記の方法が要件を満たしていない場合。RestTemplateツールクラスには、GET、POST、DELETE、PUT、OPTIONS、PATCHなどのHTTPメソッドリクエストを送信できる交換一般プロトコルリクエストメソッドもあります。

ソースコードを開くと、これがはっきりとわかります。

交換方式を使用することで、さまざまなシナリオでリクエスト操作に対応できます!

3.6。ファイルのアップロードとダウンロード

頻繁に使用されるgetおよびpostリクエストに加えて、ファイルのアップロードとダウンロードという、よく発生する別のシナリオがあります。RestTemplateを使用する場合、どのように使用しますか?

ケースは次のとおりです。具体的な実装の詳細はコードコメントを参照してください。

  • ファイルのアップロード
@RestController
public class FileUploadController {


    private static final String UPLOAD_PATH = "/springboot-frame-example/springboot-example-resttemplate/";

    /**
     * 文件上传
     * @param uploadFile
     * @return
     */
    @RequestMapping(value = "upload", method = RequestMethod.POST)
    public ResponseBean upload(@RequestParam("uploadFile") MultipartFile uploadFile,
                               @RequestParam("userName") String userName) {
        // 在 uploadPath 文件夹中通过用户名对上传的文件归类保存
        File folder = new File(UPLOAD_PATH + userName);
        if (!folder.isDirectory()) {
            folder.mkdirs();
        }

        // 对上传的文件重命名,避免文件重名
        String oldName = uploadFile.getOriginalFilename();
        String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("."));

        //定义返回视图
        ResponseBean result = new ResponseBean();
        try {
            // 文件保存
            uploadFile.transferTo(new File(folder, newName));
            result.setCode("200");
            result.setMsg("文件上传成功,方法:upload,文件名:" + newName);
        } catch (IOException e) {
            e.printStackTrace();
            result.setCode("500");
            result.setMsg("文件上传失败,方法:upload,请求文件:" + oldName);
        }
        return result;
    }
}

@Autowired
private RestTemplate restTemplate;

/**
 * 文件上传,post请求
 */
@Test
public void upload(){
    //需要上传的文件
    String filePath = "/Users/panzhi/Desktop/Jietu20220205-194655.jpg";

    //请求地址
    String url = "http://localhost:8080/upload";

    // 请求头设置,multipart/form-data格式的数据
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.MULTIPART_FORM_DATA);

    //提交参数设置
    MultiValueMap<String, Object> param = new LinkedMultiValueMap<>();
    param.add("uploadFile", new FileSystemResource(new File(filePath)));
    //服务端如果接受额外参数,可以传递
    param.add("userName", "张三");

    // 组装请求体
    HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(param, headers);

    //发起请求
    ResponseBean responseBean = restTemplate.postForObject(url, request, ResponseBean.class);
    System.out.println(responseBean.toString());
}
  • ドキュメントのダウンロード
@RestController
public class FileUploadController {


    private static final String UPLOAD_PATH = "springboot-frame-example/springboot-example-resttemplate/";


    /**
     * 带参的get请求(restful风格)
     * @return
     */
    @RequestMapping(value = "downloadFile/{userName}/{fileName}", method = RequestMethod.GET)
    public void downloadFile(@PathVariable(value = "userName") String userName,
                             @PathVariable(value = "fileName") String fileName,
                             HttpServletRequest request,
                             HttpServletResponse response) throws Exception {

        File file = new File(UPLOAD_PATH + userName + File.separator + fileName);
        if (file.exists()) {
            //获取文件流
            FileInputStream fis = new FileInputStream(file);
            //获取文件后缀(.png)
            String extendFileName = fileName.substring(fileName.lastIndexOf('.'));
            //动态设置响应类型,根据前台传递文件类型设置响应类型
            response.setContentType(request.getSession().getServletContext().getMimeType(extendFileName));
            //设置响应头,attachment表示以附件的形式下载,inline表示在线打开
            response.setHeader("content-disposition","attachment;fileName=" + URLEncoder.encode(fileName,"UTF-8"));
            //获取输出流对象(用于写文件)
            OutputStream os = response.getOutputStream();
            //下载文件,使用spring框架中的FileCopyUtils工具
            FileCopyUtils.copy(fis,os);
        }
    }
}

@Autowired
private RestTemplate restTemplate;

/**
 * 小文件下载
 * @throws IOException
 */
@Test
public void downloadFile() throws IOException {
    String userName = "张三";
    String fileName = "c98b677c-0948-46ef-84d2-3742a2b821b0.jpg";
    //请求地址
    String url = "http://localhost:8080/downloadFile/{1}/{2}";

    //发起请求,直接返回对象(restful风格)
    ResponseEntity<byte[]> rsp = restTemplate.getForEntity(url, byte[].class, userName,fileName);
    System.out.println("文件下载请求结果状态码:" + rsp.getStatusCode());

    // 将下载下来的文件内容保存到本地
    String targetPath = "/Users/panzhi/Desktop/"  + fileName;
    Files.write(Paths.get(targetPath), Objects.requireNonNull(rsp.getBody(), "未获取到下载文件"));
}

このダウンロード方法では、実際にダウンロードしたファイルをクライアントのローカルメモリに一度ロードしてから、ファイルをメモリからディスクに書き込みます。この方法は、小さなファイルのダウンロードに適しています。ファイルが比較的大きい場合、またはファイルの同時ダウンロード数が比較的多い場合、大量のメモリを占有しやすくなり、アプリケーションの運用効率が低下します。

  • 大きなファイルのダウンロード
@Autowired
private RestTemplate restTemplate;

/**
 * 大文件下载
 * @throws IOException
 */
@Test
public void downloadBigFile() throws IOException {
    String userName = "张三";
    String fileName = "c98b677c-0948-46ef-84d2-3742a2b821b0.jpg";
    //请求地址
    String url = "http://localhost:8080/downloadFile/{1}/{2}";

    //定义请求头的接收类型
    RequestCallback requestCallback = request -> request.getHeaders()
            .setAccept(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM, MediaType.ALL));

    //对响应进行流式处理而不是将其全部加载到内存中
    String targetPath = "/Users/panzhi/Desktop/"  + fileName;
    restTemplate.execute(url, HttpMethod.GET, requestCallback, clientHttpResponse -> {
        Files.copy(clientHttpResponse.getBody(), Paths.get(targetPath));
        return null;
    }, userName, fileName);
}

このダウンロード方法の違いは次のとおりです。

  • リクエストヘッダーAPPLICATION_OCTET_STREAMが設定され、データがストリームの形式でロードされることを示します
  • RequestCallbackをFile.copyと組み合わせると、ファイルコンテンツの一部が確実に受信され、コンテンツの一部がディスクに書き込まれます。すべてをメモリにロードして、最後にディスクに書き込む代わりに。

Excel、PDF、Zipなどの大きなファイルをダウンロードするときに特に便利です。

4.まとめ

この章の説明を通じて、読者はRestTemplateを使用してRestfulインターフェイスに簡単かつ迅速にアクセスする方法を事前に理解している必要があります。実際、RestTemplateの機能は非常に強力であり、作成者はほんの少ししか学んでいません。この記事に不明な点があると感じた場合、または他に知りたいことがある場合は、以下にメッセージを残してください。今後、補足および改善するために最善を尽くします。

おすすめ

転載: blog.csdn.net/Trouvailless/article/details/124219411