今回も引き続き、Azure App ServiceからPost通信によってAzure Functionsを呼び出す処理の実装について述べる。ここでは、具体的なサンプルプログラムのソースコードを共有する。
前提条件
下記記事を参照のこと。
作成したサンプルプログラム(App Service側)の内容
作成したサンプルプログラム(App Service側)の構成は以下の通り。
なお、上記の赤枠は、前提条件のプログラムから追加・変更したプログラムである。
コントローラクラスの内容は以下の通りで、searchメソッド内でAzure FunctionsのgetUserDataList関数を呼び出すようにしている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | 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クラスの内容は以下の通り。
1 2 3 4 5 6 7 8 9 10 11 12 | package com.example.demo; import java.util.ArrayList; import lombok.Data; @Data public class SearchResult { /** 検索結果リスト */ private ArrayList<UserData> userDataList = new ArrayList<>(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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クラスの内容は以下の通り。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | 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オブジェクトを作成するメソッドを追加している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 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の設定を追加している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | <?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ファイルの内容は以下の通りで、検索一覧画面の内容となっている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | <!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関数である。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | 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メソッドで呼び出される。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | 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メソッドの引数・戻り値は以下のクラスで定義している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | package com.example.model; import lombok.Data; @Data public class SearchForm { /** 検索用名前 */ private String searchName; /** 検索用性別 */ private String searchSex; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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クラスのデータベース検索処理を呼び出している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | 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ファイルの内容は以下の通り。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | <?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)の名前が変更になった影響で、以下の定義ファイルの内容を変更している。
1 2 3 4 5 6 7 8 9 | { "IsEncrypted": false, "Values": { "AzureWebJobsStorage": "", "FUNCTIONS_WORKER_RUNTIME": "java", "MAIN_CLASS":"com.example.DemoAzureFunction", "AzureWebJobsDashboard": "" } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <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オブジェクトに追加する。