今回も引き続き、Azure App ServiceからPost通信によってAzure Functionsを呼び出す処理の実装について述べる。ここでは、具体的なサンプルプログラムのソースコードを共有する。
前提条件
下記記事を参照のこと。
作成したサンプルプログラム(App Service側)の内容
作成したサンプルプログラム(App Service側)の構成は以下の通り。

なお、上記の赤枠は、前提条件のプログラムから追加・変更したプログラムである。
コントローラクラスの内容は以下の通りで、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側)の構成は以下の通り。

なお、上記の赤枠は、前提条件のプログラムから追加・変更したプログラムである。
<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を利用した実装サンプルは、以下の記事を参照のこと。
ハンドラークラスの内容は以下の通りで、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オブジェクトに追加する。





