Azure基本

Azure App ServiceからAzure FunctionsにPost送信してみた(ソースコード編)

今回も引き続き、Azure App ServiceからPost通信によってAzure Functionsを呼び出す処理の実装について述べる。ここでは、具体的なサンプルプログラムのソースコードを共有する。

前提条件

下記記事を参照のこと。

Azure App ServiceからAzure FunctionsにPost送信してみた(前提条件と実行結果)Azure App ServiceからAzure Functionsを呼び出す際、Get通信だけでなく、リクエストパラメータを含むPos...

作成したサンプルプログラム(App Service側)の内容

作成したサンプルプログラム(App Service側)の構成は以下の通り。
サンプルプログラムの構成_appservice
なお、上記の赤枠は、前提条件のプログラムから追加・変更したプログラムである。

コントローラクラスの内容は以下の通りで、searchメソッド内でAzure FunctionsのgetUserDataList関数を呼び出すようにしている。

package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.client.RestTemplate;

import com.fasterxml.jackson.databind.ObjectMapper;

@Controller
public class DemoController {

    /** RestTemplateオブジェクト */
    @Autowired
    private RestTemplate restTemplate;

    /** ObjectMapperオブジェクト */
    @Autowired
    private ObjectMapper objectMapper;

    /** application.propertiesからdemoAzureFunc.urlBaseの値を取得 */
    @Value("${demoAzureFunc.urlBase}")
    private String demoAzureFuncBase;

    /**
     * 検索一覧画面を初期表示する.
     * @param model Modelオブジェクト
     * @return 検索一覧画面
     */
    @GetMapping("/")
    public String index(Model model) {
        SearchForm searchForm = new SearchForm();
        model.addAttribute("searchForm", searchForm);
        return "list";
    }

    /**
     * 検索条件に合うユーザーデータを取得し、一覧に表示する
     * @param searchForm 検索条件Form
     * @param model      Modelオブジェクト
     * @return 検索一覧画面
     */
    @PostMapping("/search")
    public String search(SearchForm searchForm, Model model) {
        // Azure FunctionsのgetUserDataList関数を呼び出すためのヘッダー情報を設定する
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);

        // Azure FunctionsのgetUserDataList関数を呼び出すための引数を設定する
        MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
        try {
            map.add("searchName", searchForm.getSearchName());
            map.add("searchSex", searchForm.getSearchSex());
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
        HttpEntity<MultiValueMap<String, String>> entity 
            = new HttpEntity<>(map, headers);

        // Azure FunctionsのgetUserDataList関数を呼び出す
        ResponseEntity<String> response = restTemplate.exchange(
              demoAzureFuncBase + "getUserDataList"
            , HttpMethod.POST, entity, String.class);

        // Azure Functionsの呼出結果のユーザーデータ一覧を、検索条件Formに設定する
        try {
            SearchResult searchResult = objectMapper.readValue(
                response.getBody(), SearchResult.class);
            searchForm.setUserDataList(searchResult.getUserDataList());
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
        model.addAttribute("searchForm", searchForm);
        return "list";
    }

}

また、Azure FunctionsのgetUserDataList関数の戻り値となるクラスと、そのクラスで参照するUserDataクラスの内容は以下の通り。

package com.example.demo;

import java.util.ArrayList;
import lombok.Data;

@Data
public class SearchResult {

    /** 検索結果リスト */
    private ArrayList<UserData> userDataList = new ArrayList<>();

}
package com.example.demo;

import lombok.Data;

@Data
public class UserData {

    /** ID */
    private String id;

    /** 名前 */
    private String name;

    /** 生年月日_年 */
    private String birthYear;

    /** 生年月日_月 */
    private String birthMonth;

    /** 生年月日_日 */
    private String birthDay;

    /** 性別 */
    private String sex;

}

さらに、検索用のFormクラスの内容は以下の通り。

package com.example.demo;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.Map;

import lombok.Data;

@Data
public class SearchForm {

    /** 検索用名前 */
    private String searchName;

    /** 検索用性別 */
    private String searchSex;

    /** 検索結果リスト */
    private ArrayList<UserData> userDataList = new ArrayList<>();

    /** 性別のMapオブジェクト */
    public Map<String, String> getSexItems() {
        Map<String, String> sexMap = new LinkedHashMap<String, String>();
        sexMap.put("1", "男");
        sexMap.put("2", "女");
        return sexMap;
    }
}

また、DemoConfigBeanクラスの内容は以下の通りで、ObjectMapperオブジェクトを作成するメソッドを追加している。

package com.example.demo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

import com.fasterxml.jackson.databind.ObjectMapper;

@Configuration
public class DemoConfigBean {

    /**
     * RestTemplateオブジェクトを作成する
     * @return RestTemplateオブジェクト
     */
    @Bean
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }

    /**
     * ObjectMapperオブジェクトを作成する
     * @return ObjectMapperオブジェクト
     */
    @Bean
    public ObjectMapper getObjectMapper() {
        return new ObjectMapper();
    }
}

さらに、pom.xmlの内容は以下の通りで、Lombokの設定を追加している。

<?xml version="1.0" encoding="UTF-8"?>
 
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">  
  <modelVersion>4.0.0</modelVersion>  
  <parent> 
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-parent</artifactId>  
    <version>2.4.0</version>  
    <relativePath/>  
    <!-- lookup parent from repository --> 
  </parent>  
  <groupId>com.example</groupId>  
  <artifactId>demoAzureApp</artifactId>  
  <version>0.0.1-SNAPSHOT</version>  
  <packaging>war</packaging>  
  <name>demoAzureApp</name>  
  <description>Demo project for Spring Boot</description>  
  <properties> 
    <java.version>1.8</java.version> 
  </properties>  
  <dependencies> 
    <dependency> 
      <groupId>org.springframework.boot</groupId>  
      <artifactId>spring-boot-starter-thymeleaf</artifactId> 
    </dependency>  
    <dependency> 
      <groupId>org.springframework.boot</groupId>  
      <artifactId>spring-boot-starter-web</artifactId> 
    </dependency>
    <!-- lombokの設定 -->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <scope>provided</scope>
    </dependency>
    <dependency> 
      <groupId>org.springframework.boot</groupId>  
      <artifactId>spring-boot-starter-tomcat</artifactId>  
      <scope>provided</scope> 
    </dependency>  
    <dependency> 
      <groupId>org.springframework.boot</groupId>  
      <artifactId>spring-boot-starter-test</artifactId>  
      <scope>test</scope> 
    </dependency> 
  </dependencies>  
  <build> 
    <plugins> 
      <plugin> 
        <groupId>org.springframework.boot</groupId>  
        <artifactId>spring-boot-maven-plugin</artifactId> 
      </plugin>  
      <plugin>
        <groupId>com.microsoft.azure</groupId>
        <artifactId>azure-webapp-maven-plugin</artifactId>
        <version>1.12.0</version>
        <configuration>
          <schemaVersion>v2</schemaVersion>
          <subscriptionId>(ログインユーザーのサブスクリプションID)</subscriptionId>
          <resourceGroup>azureAppDemo</resourceGroup>
          <appName>azureAppDemoService</appName>
          <pricingTier>B1</pricingTier>
          <region>japaneast</region>
          <appServicePlanName>ASP-azureAppDemo-8679</appServicePlanName>
          <appServicePlanResourceGroup>azureAppDemo</appServicePlanResourceGroup>
          <runtime>
            <os>Linux</os>
            <javaVersion>Java 8</javaVersion>
            <webContainer>Tomcat 8.5</webContainer>
          </runtime>
          <deployment>
            <resources>
              <resource>
                <directory>${project.basedir}/target</directory>
                <includes>
                  <include>*.war</include>
                </includes>
              </resource>
            </resources>
          </deployment>
        </configuration>
      </plugin>
    </plugins> 
  </build> 
</project>

また、HTMLファイルの内容は以下の通りで、検索一覧画面の内容となっている。

<!DOCTYPE html>
<html lang="ja" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>検索一覧画面</title>
</head>
<body>
    <form method="post" th:action="@{/search}" th:object="${searchForm}">
    <table border="1" cellpadding="5">
        <tr>
            <th>名前</th>
            <td><input type="text" th:value="*{searchName}" th:field="*{searchName}" /></td>
        </tr>
        <tr>
            <th>性別</th>
            <td>
                <select th:field="*{searchSex}">
                    <option value=""></option>
                    <option th:each="item : *{getSexItems()}"
                            th:value="${item.key}" th:text="${item.value}"/>
                </select>
            </td>
        </tr>
    </table>
    <br/><br/>
    <input type="submit" value="検索" />
    <br/><br/><br/><br/>
    <table border="1" cellpadding="5">
        <tr>
            <th width="40">ID</th>
            <th width="180">名前</th>
            <th width="180">生年月日</th>
            <th width="40">性別</th>
        </tr>
        <tr th:each="obj : *{userDataList}">
            <td th:text="${obj.id}" align="center"></td>
            <td th:text="${obj.name}" align="center"></td>
            <td th:text="|${obj.birthYear}年 ${obj.birthMonth}月 ${obj.birthDay}日|" align="center"></td>
            <td th:text="${obj.sex}" align="center"></td>
        </tr>
    </table>
    </form>
</body>
</html>

その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/azure/tree/master/call-azure-functions-by-post-2/demoAzureApp



作成したサンプルプログラム(Azure Functions側)の内容

作成したサンプルプログラム(Azure Functions側)の構成は以下の通り。
サンプルプログラムの構成_AzureFunctions
なお、上記の赤枠は、前提条件のプログラムから追加・変更したプログラムである。

<2021年4月13日 追記>
spring-cloud-function-dependenciesのバージョンは、2021年3月16日にリリースしたバージョン3.1.2を利用すると、1つのAzure Functions内に複数のファンクションを含む場合の不具合が解消できている。


その場合、Handlerクラスの継承するクラスを「AzureSpringBootRequestHandler」クラスから「FunctionInvoker」クラスに変更する。


spring-cloud-function-dependenciesの3.1.2を利用した実装サンプルは、以下の記事を参照のこと。

spring-cloud-function-dependenciesのバージョンを最新(3.1.2)にしてみたこれまでこのブログで取り上げてきたAzure Functionsのサンプルプログラムでは、spring-cloud-function-d...

ハンドラークラスの内容は以下の通りで、Azure App Serviceのコントローラクラスから呼ばれるgetUserDataList関数である。

package com.example;

import java.util.Optional;

import org.springframework.cloud.function.adapter.azure.AzureSpringBootRequestHandler;

import com.example.model.SearchForm;
import com.example.model.SearchResult;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.microsoft.azure.functions.ExecutionContext;
import com.microsoft.azure.functions.HttpMethod;
import com.microsoft.azure.functions.HttpRequestMessage;
import com.microsoft.azure.functions.HttpResponseMessage;
import com.microsoft.azure.functions.HttpStatus;
import com.microsoft.azure.functions.annotation.AuthorizationLevel;
import com.microsoft.azure.functions.annotation.FunctionName;
import com.microsoft.azure.functions.annotation.HttpTrigger;

public class GetUserDataListHandler 
    extends AzureSpringBootRequestHandler<SearchForm, SearchResult> {

    /**
     * HTTP要求に応じて、DemoAzureFunctionクラスのgetUserDataListメソッドを呼び出し、
     * その戻り値をボディに設定したレスポンスを返す
     * @param request リクエストオブジェクト
     * @param context コンテキストオブジェクト
     * @return レスポンスオブジェクト
     */
    @FunctionName("getUserDataList")
    public HttpResponseMessage execute(
         @HttpTrigger(name = "request", methods = HttpMethod.POST
            , authLevel = AuthorizationLevel.ANONYMOUS) 
                HttpRequestMessage<Optional<String>> request,
        ExecutionContext context) {

        // リクエストオブジェクトからパラメータ値を取得し、検索条件Formに設定する
        ObjectMapper mapper = new ObjectMapper();
        String jsonParam = request.getBody().get();
        jsonParam = jsonParam.replaceAll("\\[", "").replaceAll("\\]", "");

        SearchForm searchForm = new SearchForm();
        try {
             searchForm = mapper.readValue(
                 jsonParam, new TypeReference<SearchForm>() {});
        } catch (Exception ex) {
             throw new RuntimeException(ex);
        }

        // handleRequestメソッド内でDemoAzureFunctionクラスのgetUserDataListメソッドを
        // 呼び出し、その戻り値をボディに設定したレスポンスを、JSON形式で返す
        return request.createResponseBuilder(HttpStatus.OK)
            .body(handleRequest(searchForm, context))
            .header("Content-Type", "text/json").build();
    }
}

また、Azure Functionsのメインクラスは以下の通りで、先ほどのハンドラークラスのhandleRequestメソッドで呼び出される。

package com.example;

import java.util.function.Function;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import com.example.model.SearchForm;
import com.example.model.SearchResult;
import com.example.service.SearchService;

@SpringBootApplication
public class DemoAzureFunction {

    /** ユーザーデータ取得サービスクラスのオブジェクト */
    @Autowired
    private SearchService searchService;

    public static void main(String[] args) throws Exception {
        SpringApplication.run(DemoAzureFunction.class, args);
    }

    /**
     * DBから検索条件に合うユーザーデータを取得し結果を返却する関数
     * @return ユーザーデータ取得サービスクラスの呼出結果
     */
    @Bean
    public Function<SearchForm, SearchResult> getUserDataList() {
        return searchForm -> searchService.getUserDataList(searchForm);
    }
}

さらに、Azure FunctionsのメインクラスのgetUserDataListメソッドの引数・戻り値は以下のクラスで定義している。

package com.example.model;

import lombok.Data;

@Data
public class SearchForm {

    /** 検索用名前 */
    private String searchName;

    /** 検索用性別 */
    private String searchSex;

}
package com.example.model;

import java.util.ArrayList;
import com.example.mybatis.model.UserData;
import lombok.Data;

@Data
public class SearchResult {

    /** 検索結果リスト */
    private ArrayList<UserData> userDataList = new ArrayList<>();

}

また、検索サービスクラスの内容は以下の通りで、UserDataMapperクラスのデータベース検索処理を呼び出している。

package com.example.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.example.model.SearchForm;
import com.example.model.SearchResult;
import com.example.mybatis.UserDataMapper;

@Service
public class SearchService {

    /** DBからユーザーデータを取得するMapperオブジェクト */
    @Autowired
    private UserDataMapper userDataMapper;

    /**
     * DBから検索条件に合うユーザーデータを取得し結果を返却する
     * @param searchForm 検索条件Form
     * @return 結果情報オブジェクト
     */
    public SearchResult getUserDataList(SearchForm searchForm) {
        SearchResult searchResult = new SearchResult();
        searchResult.setUserDataList(userDataMapper.searchUserDataList(searchForm));
        return searchResult;
    }

}

さらに、USER_DATAテーブルからデータ検索するクラス、XMLファイルの内容は以下の通り。

package com.example.mybatis;

import java.util.ArrayList;
import org.apache.ibatis.annotations.Mapper;
import com.example.model.SearchForm;
import com.example.mybatis.model.UserData;

@Mapper
public interface UserDataMapper {

    /**
     * 引数の検索条件に合うユーザーデータを取得し結果を返却する
     * @param searchForm 検索条件Form
     * @return ユーザーデータリスト
     */
    ArrayList<UserData> searchUserDataList(SearchForm searchForm);

}
package com.example.mybatis.model;

import lombok.Data;

@Data
public class UserData {

    /** ID */
    private String id;

    /** 名前 */
    private String name;

    /** 生年月日_年 */
    private String birthYear;

    /** 生年月日_月 */
    private String birthMonth;

    /** 生年月日_日 */
    private String birthDay;

    /** 性別 */
    private String sex;

}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mybatis.UserDataMapper">
    <resultMap id="userDataResultMap" type="com.example.mybatis.model.UserData" >
        <result column="id" property="id" jdbcType="VARCHAR" />
        <result column="name" property="name" jdbcType="VARCHAR" />
        <result column="birthYear" property="birthYear" jdbcType="VARCHAR" />
        <result column="birthMonth" property="birthMonth" jdbcType="VARCHAR" />
        <result column="birthDay" property="birthDay" jdbcType="VARCHAR" />
        <result column="sex" property="sex" jdbcType="VARCHAR" />
    </resultMap>
    <select id="searchUserDataList" parameterType="com.example.model.SearchForm" resultMap="userDataResultMap">
        SELECT 
              u.id
            , u.name
            , u.birth_year AS birthYear
            , u.birth_month AS birthMonth
            , u.birth_day AS birthDay
            , s.sex_value AS sex
        FROM user_data u, m_sex s
        WHERE u.sex = s.sex_cd
        <if test="searchName != null and searchName != ''">
            AND u.name like CONCAT('%', #{searchName}, '%')
        </if>
        <if test="searchSex != null and searchSex != ''">
            AND u.sex = #{searchSex}
        </if>
    </select>
</mapper>

また、メインクラス(DemoAzureFunction.java)の名前が変更になった影響で、以下の定義ファイルの内容を変更している。

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "java",
    "MAIN_CLASS":"com.example.DemoAzureFunction",
    "AzureWebJobsDashboard": ""
  }
}
<properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <azure.functions.maven.plugin.version>1.9.0</azure.functions.maven.plugin.version>

        <!-- customize those properties. The functionAppName should be unique across Azure -->
        <functionResourceGroup>azureAppDemo</functionResourceGroup>
        <functionAppName>azureFuncDemoApp</functionAppName>
        <functionAppServicePlan>ASP-azureAppDemo-8679</functionAppServicePlan>
        <functionPricingTier>B1</functionPricingTier>

        <functionAppRegion>japaneast</functionAppRegion>
        <stagingDirectory>${project.build.directory}/azure-functions/${functionAppName}</stagingDirectory>
        <start-class>com.example.DemoAzureFunction</start-class>
        <spring.boot.wrapper.version>1.0.25.RELEASE</spring.boot.wrapper.version>
    </properties>

その他のソースコード内容は、以下のサイトを参照のこと。
https://github.com/purin-it/azure/tree/master/call-azure-functions-by-post-2/demoAzureFunc

要点まとめ

  • Azure App ServiceからPost通信によってAzure Functionsを呼び出す際も、Get通信の場合と同様、RestTemplateクラスを利用すればよい。
  • Post通信でリクエストボディに受け渡すパラメータを設定するには、LinkedMultiValueMapクラスのMapオブジェクトにパラメータを設定後、それをHttpEntityオブジェクトに追加する。