20211205のJavaに関する記事は7件です。

小ネタ:IntelliJでCSVファイルをテーブル形式で参照する

この記事は ZOZO #3 Advent Calendar 2021 10日目の記事になります。 小ネタです。 DBUnitなどでテスト用のDBテーブルのレコードをCSVで用意することありますよね。 そのレコードを作るのが面倒だったりするのですが、IntelliJにはCSVをテーブル形式で参照できる機能があります。 たとえばこんなCSV。タイトルと行が位置あっておらず編集しずらいです。 右クリックで上記のようにテーブル形式に表示するメニューが出てきます。 こんな感じで編集、行追加も可能です。 画面左下から切り替えも可能です。 以上小ネタでした。 明日は@satto_sannの記事になります!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

azure batch 使った機能を自社アプリケーションに組み込んでみた

基礎知識 azureバッチの概要はこちら↓ azure Batch ってなんなん? はじめに 私が管理しているアプリケーションで同じ処理を大量に並行で実行したい要件があり、Azure Batchを使って、計算する機構をつくりました。 アプリ概要 あるWebアプリで指定された複数のパラメータを元に各パラメータごとに計算用アプリを実行します。 仕込み 元々Spring で作った自社Webアプリケーションがあります。 今回計算用にSpringでCliアプリケーションを用意しました。(引数を2つ受け取って計算結果をDBに保存する) 仕組み 1.計算用のCliアプリケーションをazure storageに配置(batchCli.jar) 2.自社アプリ本体から、Azure Batchのリソースを作成するように指示 3.指示を受けて、プール、ノード、ジョブ、タスクが作られる 4.作られたノード上でタスクが実行され、DBに結果書き出し、実行ログがazure storageに保存される コード説明 こちらを参考に 上記の仕組みを実現するコードを作りました。 executeメソッドにて、 1.計算必要なパラメータを取得 2.プールを作成 3.タスクを実行 4.結果を保存 というシンプルなものですが、 ネット上にサンプルが少なく単純に作るのに時間かかったのと 本記事末尾にあるような認証まわりのハマりポイントにハマりかなり苦戦しました。 /** * Azure Batchを利用して複数パラメータの計算を実行する * */ @Slf4j @Component @RequiredArgsConstructor public class BatchExecTask { //実行に必要な設定を持ってるクラス private final @NonNull BatchConfig batchConfig; //計算対象を撮ってくるサービス private final @NonNull BatchService batchService; //結果を書き込むサービス private final @NonNull BatchResultService batchResultService; //1ノードで実行できる最大タスク private static Integer MAX_TASKS_PER_NODE = 4; //パラメータ埋めるためのクラス @Value(staticConstructor = "of") public static class BatchExecParameter { private Long batchId; private Long taskId; private int order; private String taskKey; private BigDecimal parameter1; private BigDecimal parameter2; } //実行用のメインメソッド @Transactional(rollbackFor = Exception.class) public void execute(Long taskId, Long batchId) { log.info(taskId, "計算リソースの準備を開始します")); // Batch関連リソースの後処理設定 // 基本は削除 Boolean shouldDeleteJob = true; Boolean shouldDeletePool = true; var TASK_COMPLETE_TIMEOUT = Duration.ofMinutes(30); var cred = getCredentials(); BatchClient client = BatchClient.open(cred); //作った時間でpoolとjobの名前を決める var timeKey = DateTime.now().toString("yyyy-MM-dd-HH-mm-ss"); var poolId = "pool-" + timeKey; var jobId = String.format("job-%s", timeKey); try { log.info(taskId, "計算条件を取得します")); var params = getParameters(batchId, taskId); log.info(taskId, "計算リソースを作成します。数分お待ち下さい")); var sharedPool = createPoolIfNotExists(client, poolId, params.size()); log.info(taskId, "計算処理を開始します。数分お待ち下さい")); submitJobAndAddTask(client, sharedPool.id(), jobId, params, taskId); //計算実行したパラメータとタスクの名前のマップを記録しておく var paramMap = Seq.seq(params).toMap(p -> p.getTaskKey()); if (!waitForTasksToComplete(client, jobId, TASK_COMPLETE_TIMEOUT)) { throw new TimeoutException("計算処理がタイムアウトしました。"); } log.info(taskId, "計算が完了しました。結果を保存します。")); //結果保存 batchResultService.store(batchId, taskId, paramMap); log.info(taskId, "保存処理が完了しました。")); } catch (BatchErrorException err) { log.info(taskId, "エラーが発生しました。")); printBatchException(err); } catch (ErrorException err) { log.info(taskId, "エラーが発生しました。")); throw err; } catch (Exception ex) { log.info(taskId, "エラーが発生しました。")); ex.printStackTrace(); throw new ErrorException("計算処理中に予期せぬエラーが発生しました。"); } finally { // 必要に応じてリソースを削除する if (shouldDeleteJob) { try { client.jobOperations().deleteJob(jobId); } catch (BatchErrorException err) { printBatchException(err); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if (shouldDeletePool) { try { client.poolOperations().deletePool(poolId); } catch (BatchErrorException err) { printBatchException(err); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } /** * 実行用のパラメータを取得する */ private List<BatchExecParameter> getParameters( long batchId, long taskId) { //実行パラメータのリストを取得する //実行するparameter1,parameter2のマトリックスで決まる。 //5×4なら20個のパラメータのリストを返す   var detail = batchService.getDeatil(batchId); List<BatchExecParameter> params = new ArrayList<>(); var keyId = 1; for (var i = 0; i < detail.getParameter1List().size(); i++) { for (var j = 0; j < detail.getParameter2List().size(); j++) { //タスクの並び順が文字列純だとばらつくので0埋めにする var taskKey = String.format("task%03d", keyId); var parameter1 = detail.getParameter1List().get(i); var parameter2 = detail.getParameter2List().get(j); var param = BatchExecParameter.of(batchId, taskId, keyId, taskKey, parameter1, parameter2); params.add(param); keyId++; } } return params; } /** * Azure Batchの資格情報を取得する */ private BatchCredentials getCredentials() { //Azure ADで認証する(そうしないと仮想ネットワークが使えない) var batchEndpoint = batchConfig.getBatchUri(); //ActiveDirectoryのアプリケーションのクライアントID var clientId = batchConfig.getClientId(); //アプリケーションの認証シークレット var applicationSecret = batchConfig.getSecret(); //このアプリケーションを含むドメインまたはテナントID。 var tenantId = batchConfig.getTenantId(); //エンドポイントはnullにしとくとコンストラクタでセットされる var cred = new BatchApplicationTokenCredentials(batchEndpoint, clientId, applicationSecret, tenantId, null, null); return cred; } /** * azure batchのプールにジョブとタスクを紐づける */ private void submitJobAndAddTask(BatchClient client, String poolId, String jobId, List<BatchExecParameter> params, Long taskId) throws BatchErrorException, IOException, StorageException, InvalidKeyException, URISyntaxException { // プールにジョブを追加する var poolInfo = new PoolInformation(); poolInfo.withPoolId(poolId); client.jobOperations().createJob(jobId, poolInfo); // タスクにファイルを紐付ける String sas = retrieveJarUriWithSas(); var file = new ResourceFile(); file.withFilePath(batchConfig.getCliJarName()).withHttpUrl(sas); List<ResourceFile> files = new ArrayList<>(); files.add(file); for (var param : params) { // タスクを作る var taskToAdd = new TaskAddParameter(); taskToAdd.withId(param.getTaskKey()).withCommandLine(createCommandLine(param)); taskToAdd.withResourceFiles(files); //タスクログとストレージの紐付け var outputFiles = getOutputFiles(taskId, jobId, param.getTaskKey()); taskToAdd.withOutputFiles(outputFiles); // ジョブにタスクを追加する client.taskOperations().createTask(jobId, taskToAdd); } } /** * 標準出力と標準エラー出力のファイルを作る */ private List<OutputFile> getOutputFiles(Long taskId, String jobId, String taskKey) throws IOException { var stdout = createOutputFile(taskId, jobId, taskKey, "stdout"); var stderr = createOutputFile(taskId, jobId, taskKey, "stderr"); return List.of(stdout, stderr); } /** * 標準出力と標準エラー出力のファイルを作る */ private OutputFile createOutputFile(Long taskId, String jobId, String taskKey, String fileName) throws IOException { var output = new OutputFile(); var destination = new OutputFileDestination(); var storage = new OutputFileBlobContainerDestination(); var containerUrl = retrieveLogContainerUriWithSas(); var path = String.format("/%s-%s/%s/%s.txt", jobId, taskId.toString(), taskKey, fileName); storage.withContainerUrl(containerUrl).withPath(path); destination.withContainer(storage); var uploadOptions = new OutputFileUploadOptions() .withUploadCondition(OutputFileUploadCondition.TASK_COMPLETION); output.withDestination(destination) //ログファイルができる階層は実行ディレクトリの一つ上にできる .withFilePattern("../" + fileName + ".txt") .withUploadOptions(uploadOptions); return output; } /** * プールが存在しなかったら作ります * * @param Batchクライアントのインスタンス * @param プールID * @return プールのインスタンス * @throws Exception */ private CloudPool createPoolIfNotExists(BatchClient client, String poolId, Integer paramSize) throws Exception { //プールが安定するまで待つ時間:5分 Duration POOL_STEADY_TIMEOUT = Duration.ofMinutes(5); //VMが準備されるまでに待つ時間:20分 Duration VM_READY_TIMEOUT = Duration.ofMinutes(20); // Batch poolが存在するかどうか確認。しなければ作る。 if (!client.poolOperations().existsPool(poolId)) { var configuration = createVmConfiguration(client); // ノード数(並列数を超える分は別ノードで実行) int poolVMCount = (int) Math .ceil(paramSize.doubleValue() / MAX_TASKS_PER_NODE.doubleValue()); var startTask = new StartTask(); PoolAddParameter poolAddParameter = new PoolAddParameter(); poolAddParameter.withId(poolId); poolAddParameter.withVmSize(batchConfig.getVmSize()); // 標準のイメージではjavaがインストールされていないため、開始タスクでインストール startTask.withCommandLine( "/bin/bash -c \"apt-get update && apt-get install -y openjdk-11-jdk\""); //プール autouser 管理者相当 var userIdentity = (new UserIdentity()).withAutoUser(new AutoUserSpecification() .withElevationLevel(ElevationLevel.ADMIN).withScope(AutoUserScope.POOL)); startTask.withUserIdentity(userIdentity); poolAddParameter.withStartTask(startTask); poolAddParameter.withVirtualMachineConfiguration(configuration); poolAddParameter.withTargetDedicatedNodes(poolVMCount); if (batchConfig.getSubnetId() != null) { //仮想ネットワークの設定 //※設定する仮想ネットワークは同一サブスクリプションである必要がある //https://docs.microsoft.com/ja-jp/java/api/com.microsoft.azure.batch.protocol.models.networkconfiguration.withsubnetid?view=azure-java-stable#com_microsoft_azure_batch_protocol_models_NetworkConfiguration_withSubnetId_java_lang_String_ var networkConfig = new NetworkConfiguration(); networkConfig.withSubnetId(batchConfig.getSubnetId()); poolAddParameter.withNetworkConfiguration(networkConfig); } //タスクの並列実行数(コア数の4倍が最大) poolAddParameter.withMaxTasksPerNode(MAX_TASKS_PER_NODE); client.poolOperations().createPool(poolAddParameter); } boolean steady = false; // プールが安定するまで待機する var steadyStopWatch = Stopwatch.createStarted(); while (steadyStopWatch.elapsed().toMillis() < POOL_STEADY_TIMEOUT.toMillis()) { var pool = client.poolOperations().getPool(poolId); if (pool.allocationState() == AllocationState.STEADY) { steady = true; var creationElapsedSeconds = Seconds.secondsBetween(pool.creationTime(), pool.allocationStateTransitionTime()); log.info("プールが安定状態になりました。所要時間:" + creationElapsedSeconds.getSeconds() + "秒"); break; } log.info("プールが安定するまで10秒待機します..."); Thread.sleep(10 * 1000); } if (!steady) { throw new TimeoutException("プールの割当がタイムアウトしました"); } // VMがアイドル状態になるまで待機する boolean hasIdleVM = false; var readyStopWatch = Stopwatch.createStarted(); while (readyStopWatch.elapsed().toMillis() < VM_READY_TIMEOUT.toMillis()) { List<ComputeNode> nodeCollection = client.computeNodeOperations().listComputeNodes( poolId, new DetailLevel.Builder().withSelectClause("id, state") .withFilterClause("state eq 'idle'") .build()); if (!nodeCollection.isEmpty()) { hasIdleVM = true; log.info("仮想マシンがアイドル状態になりました。所要時間:" + readyStopWatch.elapsed(TimeUnit.SECONDS) + "秒"); break; } log.info("仮想マシンが開始するまで10秒待機します..."); Thread.sleep(10 * 1000); } if (!hasIdleVM) { throw new TimeoutException("仮想マシンの開始がタイムアウトしました"); } return client.poolOperations().getPool(poolId); } /** * VMイメージの定義を作る */ private VirtualMachineConfiguration createVmConfiguration(BatchClient client) throws Exception { //ubuntu 18.04を指定。必要に応じて変える String osPublisher = "canonical"; String osOffer = "ubuntuserver"; String imageVersion = "18.04-lts"; // sku image の参照を取得する List<ImageInformation> skus = client.accountOperations().listSupportedImages(); //SKUの取得 var skuOpt = Seq.seq(skus) .filter(sku -> sku.osType() == OSType.LINUX) .filter(sku -> sku.verificationType() == VerificationType.VERIFIED) .filter(sku -> sku.imageReference().publisher().equalsIgnoreCase(osPublisher) && sku.imageReference().offer().equalsIgnoreCase(osOffer) && sku.imageReference().sku().equals(imageVersion)) .findFirst(); if (!skuOpt.isPresent()) { throw new Exception("image not found"); } var sku = skuOpt.get(); var imageRef = sku.imageReference(); var skuId = sku.nodeAgentSKUId(); // イメージを指定してpoolを作る var configuration = new VirtualMachineConfiguration(); configuration.withNodeAgentSKUId(skuId).withImageReference(imageRef); return configuration; } /** * ジョブ内のタスクが終わるまで待機します。 * * @param client azure batchクライアント * @param jobId ジョブID * @param expiryTime タイムアウトまでの時間 * @return 時間内に全てのタスクが完了したらtrue, そうでない場合はfalseを返します * @throws BatchErrorException * @throws IOException * @throws InterruptedException */ private boolean waitForTasksToComplete(BatchClient client, String jobId, Duration expiryTime) throws BatchErrorException, IOException, InterruptedException { var stopWatch = Stopwatch.createStarted(); while (stopWatch.elapsed().toMillis() < expiryTime.toMillis()) { List<CloudTask> taskCollection = client.taskOperations().listTasks(jobId, new DetailLevel.Builder().withSelectClause("id, state, executionInfo").build()); // 全てのタスクが完了したかどうか var allComplete = Seq.seq(taskCollection) .allMatch(t -> t.state() == TaskState.COMPLETED); if (allComplete) { //全部完了したら抜ける。 StringBuilder errorDetailBuilder = new StringBuilder(); for (var task : taskCollection) { var failureInfo = task.executionInfo().failureInfo(); if (failureInfo != null) { errorDetailBuilder.append(System.lineSeparator()); errorDetailBuilder.append( String.format("処理名:%s エラー内容:%s", task.id(), failureInfo.message())); } } log.info("タスクが全て完了しました。実行時間:" + stopWatch.elapsed(TimeUnit.SECONDS) + "秒"); if (errorDetailBuilder.length() > 0) { var erroMessage = "計算処理でエラーが発生しました" + System.lineSeparator(); throw new ErrorException(erroMessage + errorDetailBuilder.toString()); } //タスクからメッセージが帰ってくるまで10秒待つ Thread.sleep(10 * 1000); return true; } log.debug("タスクが完了するまで10秒待ちます.."); // 10秒ごとのチェックする Thread.sleep(10 * 1000); } // タイムアウトしたら抜ける。 return false; } /** * 実行用のコマンドライン文字列を生成します * @param parameter1 * @param parameter2 * @return */ private String createCommandLine(BatchExecParameter param) { // 出来上がりの文字列イメージ // java -jar // -Dspring.profiles.active=xxx,xxx-local // -Duser.language=ja // -Duser.country=JP // -Duser.timezone=Asia/Tokyo // -Dfile.encoding=UTF-8 // batchCli.jar BatchExec -p1 0.35 -p2 123456 return "java -jar " + "-Dspring.profiles.active=" + batchConfig.getExecuteProfile() + " " + "-Dspring.cloud.config.label=" + batchConfig.getConfigLabel() + " " + "-Duser.language=ja -Duser.country=JP -Duser.timezone=Asia/Tokyo " + "-Dfile.encoding=UTF-8 " + //同一ノードでアプリを実行する場合、tomcatのportが重複するためタスクごとに変更する "-Dserver.port=" + (8080 + param.order) + " " + //アクチュエーターのportが重複するためタスクごとに変更する "-Dmanagement.server.port=" + (9999 - param.order) + " " + batchConfig.getCliJarName() + " " + "BatchExec " + "-p1 " + param.getParameter1().toString() + " " + "-p2 " + param.getParameter2().toString() + " "; } /** * jarの格納先blobへのsas付きパスを返却する * @return * @throws IOException */ private String retrieveJarUriWithSas() throws IOException { try { var container = getContainerReference(batchConfig.getStorageContainerName()); var blob = container.getBlockBlobReference(batchConfig.getCliJarPath()); //一日だけ読み取り権限 var accessPolicy = StorageUtil.createSharedAccessPolicy( EnumSet.of(SharedAccessBlobPermissions.READ), 60 * 24); return blob.getUri() + "?" + blob.generateSharedAccessSignature(accessPolicy, null); } catch (URISyntaxException | StorageException | InvalidKeyException e) { throw new IOException(e); } } /** * ログファイルの格納先blobへのsas付きパスを返却する * @return * @throws IOException */ private String retrieveLogContainerUriWithSas() throws IOException { try { var container = getContainerReference("tasklogs"); container.createIfNotExists(); //一日だけ書き込み権限 var accessPolicy = StorageUtil.createSharedAccessPolicy( EnumSet.of(SharedAccessBlobPermissions.WRITE), 60 * 24); return container.getUri() + "?" + container.generateSharedAccessSignature(accessPolicy, null); } catch (URISyntaxException | StorageException | InvalidKeyException e) { throw new IOException(e); } } /** * コンテナへの参照を取得する * * @param storageAccountName ストレージアカウント名 * @param storageAccountKey ストレージアカウントキー * @return コンテナへの参照 * @throws URISyntaxException * @throws StorageException */ private CloudBlobContainer getContainerReference(String containerName) throws URISyntaxException, StorageException { // ストレージの資格情報を生成する var credentials = new StorageCredentialsAccountAndKey(batchConfig.getStorageAccount(), batchConfig.getStorageAccountKey()); // https接続でストレージアカウントを生成する var storageAccount = new CloudStorageAccount(credentials, true); // blobクライアントを生成する var blobClient = storageAccount.createCloudBlobClient(); // コンテナへの参照を取得する return blobClient.getContainerReference(containerName); } /** * バッチエラーを出力します * * @param err バッチエラーの内容 */ private static void printBatchException(BatchErrorException err) { var builder = new StringBuilder(); builder.append(String.format("計算中にエラーが発生しました %s", err.toString())); if (err.body() != null) { builder.append(System.lineSeparator()); builder.append(String.format("エラーコード = %s, message = %s", err.body().code(), err.body().message().value())); if (err.body().values() != null) { for (var detail : err.body().values()) { builder.append(System.lineSeparator()); builder.append(String.format("エラー詳細 %s=%s", detail.key(), detail.value())); } } } var errMsg = builder.toString(); log.error(errMsg); throw new ErrorException(errMsg); } private TaskMessage createMessage(long taskId, String message) { var taskMesssage = new TaskMessage(); taskMesssage.setTaskId(taskId); taskMesssage.setMessage(message); taskMesssage.setPublishedDateTime(LocalDateTime.now()); return taskMesssage; } } ハマったポイント ・ノードからSQLサーバーのアクセスが弾かれてエラー ・batch poolに仮想ネットワーク設定しようとしたらAD認証じゃないとだめでエラー ・各リソースのIAM設定しないとazure batchからリソース操作できないエラー ・アプリのポート重複により起動できないエラー
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

String Boot環境構築

1.eclipseをインストール(Windows版のインストールをキャプチャしています)   1-1.https://mergedoc.osdn.jp/ にアクセス   1-2.Eclipse 2021をクリック(赤枠で囲っている箇所)   1-3.Windows64bit版 Full EditionのJavaをクリック(赤枠で囲っている箇所)  1-4.Eclipseダウンロード(ダウンロードが開始されない場合、赤枠のリンクをクリック) 1-5.Eclipseを解凍(7Zipで解凍, キャプチャでは、Qiitaファルダで解凍) 2.Spring Bootプロジェクト作成  2-1.Eclipse起動(workspaceは初期設定)  2-2.ファイル→新規→Springスターター・プロジェクトを選択  2-3.タイプ:Maven Project, パッケージング:Warで作成   2-3ー1.赤枠の箇所を変更して、次へボタンをクリック   2-3-2.完了ボタンをクリック、ビルドが完了するまで待つ  2-4.パッケージエクスプローラーでプロジェクトを選択して右クリック→実行を選択→Maven installをクリック   (ERRORになった場合、プロジェクトのプロパティのMavenでアクティブMavenプロファイルを空白で再度、上記を実行)  2-5.index.htmlを作成   2-5-1.src/main/resources/templatesを右クリック→新規→HTMLファイルをクリック   2-5-2.ファイル名をindex.htmlに変更して完了ボタンをクリック  2-6.プロジェクト実行      プロジェクトを右クリック→実行→Spring Bootアプリケーションをクリック 3.ブラウザで確認   http://localhost:8080/ にアクセス(正常に表示されている場合、Insert title hereのタイトルが表示される)      
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Android Studio】Gradleでの Jackson の追加方法(Java)

Android Studioを使用したJacksonの使用方法について調べたのですが、実装までに四苦八苦したので、メモを残しておきます。 リポジトリ Jacksonが公開されているページです。 ページ内の取得したいバージョンの「Gradle」の部分をコピーします。 jackson-core https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core jackson-databind https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind jackson-annotations https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-annotations 使用するバージョンの選択 build.gradleに貼り付ける箇所のコピー build.gradle ライブラリの管理はbuild.gradleで行われているので、先ほどコピーした内容をbuild.gradleのdependenciesに追加します。 build.gradle plugins { id 'com.android.application' } android { // 省略 } dependencies { // ここに追加する // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core implementation group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: '2.13.0' // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.13.0' // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-annotations implementation group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.13.0' // 以下はデフォルトの内容 implementation 'androidx.appcompat:appcompat:1.3.1' implementation 'com.google.android.material:material:1.4.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.1' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' } 追加した後に再ビルドをすれば、読み込み完了です。 あとは、Javaのコードで、下記の様にjacksonのライブラリをimportすればandroid studioで、Jacksonが使用できるようになります。 (私の場合は、ObjectMapperのreadValueとwriteValueAsStringぐらいしか使わないため、ObjectMapperをimportしています。) import com.fasterxml.jackson.databind.ObjectMapper;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Javaを使った関数型プログラミング

Javaのカレンダー | Advent Calendar 2021の6日目の記事です 概要 手続き型のこれを @GetMapping("/items") public List<ItemDto> get(@RequestParam(name = "name", defaultValue = "") String name) { // 入力チェック if (name.length() <= 10) { throw new IllegalArgumentException("name"); } // 商品をDBから検索 List<ItemData> itemDataList = repository.findByNameLike("%" + name + "%"); List<ItemDto> result = new ArrayList<>(); for (var itemData : itemDataList) { var itemDto = new ItemDto(); itemDto.setId(itemData.getId()); itemDto.setCategory(itemData.getCategory()); itemDto.setName(itemData.getName()); itemDto.setPriceIncludingTax(itemData.getPrice() * 110 / 100); // 消費税を計算 result.add(itemDto); } return result; } 関数型のこれにする話 @GetMapping("/items") public List<ItemDto> get(@RequestParam(name = "name", defaultValue = "") String name) { return validate(name).stream() // 入力チェック .flatMap(this::getItems) // 商品をDBから検索 .map(mapper::toDto) // DTOに変換 .collect(Collectors.toList()); // List に変換 } // ... Javaを関数型で書くと、行数が短くなるという話がありますが、今回のサンプルではそうはならなかったので、一部抜き出して、短く見せている部分もあります (なので記事の最後に全体像を貼り付けておきます) 説明 Spring Boot を使った REST API を関数型で書いたらこうなるという例(上のコード)です Java8以降であれば、関数型インターフェイスと Stream API を組み合わせることで、例のような関数型プログラミングができます メリットは、何でしょうか? アプリケーションの要求と書き手のスキルがマッチすれば、参照透過性の高い堅牢なプログラムになると信じていますが... 既存の冗長な手続き型のプログラムを関数型にリファクタリングするというだけでも新しい発見がありそうです 最近では、Kotlin やりたい。 WebFlux やりたい。Scala / Clojure を業務で使ってみたい、みたいな声も聞きますが(実際には聞いたことはありません)、まずは手元のいい意味でレガシーな業務コードを関数型に書き換えて、キリッっとしてみていはいかがでしょうか? 自分も Javaや関数型に詳しいというわけでなく、できればいいな程度の気持ちでこの記事を書いています また、実戦経験にも乏しいため、中途半端なコードになってしまっていたら申し訳ないです ポイント 関数型でプログラミングするために気をつけること(これをやれば必ず関数型になるというものではない)をあげてみます 1対1の変換にはmap、1対Nの変換にはflatMapを使ってメソッドをつなげる メソッドの抽出を繰り返して、メソッドを最小単位に分解し、可能な限りメソッド参照を使ってアクセスする メソッドの行数は5行という目安があるようです。が、すぐに破られるでしょう 例外は、非検査例外(RuntimeExceptionの派生)を使う 検査例外はラムダ式では扱えないので、@SneakyThrows を使って回避する ラムダでの例外の扱いには諸説ありますが、@SneakyThrows は javac のチェックをすり抜けて、元の例外を投げているという話もあるので、邪道といわれてしまうかもしれません これが参考になるかも Hide Checked Exceptions with SneakyThrows 例外は、@RestControllerAdvice を使って REST API の大元でハンドリングする DTOにロジックを書かない DTO(Data Transfer Object) を本来の意味のものにしてあげる 書いても static メソッドの置き場所に困ったときにする 可能であれば メソッドを static にする static であれば、少なくともメンバ変数との依存がないことが保証され、コードが理解しやすい 妥協ポイント 副作用のない関数が必ずしも書けると思わない どうしてもメソッド間でデータを持ちわまりたいときに、引数を変更して返却してしまう自分がいる 例外の扱いは雑になる 検査例外の投げ直し(@SneakyThrows)、呼び出し元でのエラーハンドリング(@RestControllerAdvice)がオススメなので、回復可能な例外などを個別に手当しない(してもよい)が第一となる 多少の手続き型は残す(る) やっぱり、手続き型で書くと作業が捗るよね、なんてこともある 付録 概要で省略したコードの全体像です @RestController @RequiredArgsConstructor public class SampleFunctionalController { private final ItemRepository repository; private final ItemMapper mapper; @GetMapping("/items") public List<ItemDto> get(@RequestParam(name = "name", defaultValue = "") String name) { return validate(name).stream() // 入力チェック .flatMap(this::getItems) // 商品をDBから検索 .map(mapper::toDto) // DTOに変換 .collect(Collectors.toList()); // List に変換 } // 入力チェック public Optional<String> validate(String name) { if (name.length() > 10) { throw new IllegalArgumentException("name"); } return Optional.of(name); } // 商品をDBから検索 public Stream<ItemData> getItems(String name) { return repository.findByNameLike("%" + name + "%"); } } @Component class ItemMapper { public ItemDto toDto(ItemData itemData) { var itemDto = new ItemDto(); itemDto.setId(itemData.getId()); itemDto.setCategory(itemData.getCategory()); itemDto.setName(itemData.getName()); itemDto.setPriceIncludingTax(itemData.getPrice() * 110 / 100); // 消費税を計算 return itemDto; } }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Retrofit+OkHttpでファイルダウンロードするAPIを呼び出す

この記事はラクスアドベントカレンダーの5日目の記事です。 なぜ3日連続書くなんて宣言してしまったのか激しく後悔していますが、今日も書いていきたいと思います。 はじめに 今日はRetrofit+OkHttpでファイルダウンロードするAPI呼び出しについて書いていきたいと思います。 検索すると古いバージョンの話やAndroidに特化した内容の記事が多くヒットしたのですが、今回紹介するのはWebサイトで同期的にAPIを呼び出す前提のやり方です。 バージョン retrofit:2.9.0 okhttp:3.14.9 実装方法 Retrofitのインスタンス生成 レスポンスをバイト列で受け取りたいので今回はコンバーターを設定しません。 Retrofit retrofit = new Retrofit.Builder() .baseUrl("http://localhost:8080") // コンバーターを指定しない //.addConverterFactory(JacksonConverterFactory.create()) .build(); インターフェイスの実装 サービスから戻り値を変換せずにokhttp3.ResponseBodyで受け取るように宣言します。また@Streamingをつけておかないとダウンロードしたファイルが一気にメモリに展開されてOutOfMemoryErrorが起きる可能性があるので注意してください。 public interface DownloadService { @Streaming @GET("/download") Call<ResponseBody> download(); } 呼び出し方 あとはAPIを呼び出して取得したレスポンスに対してresponse.body().byteStream()するとダウンロードしたファイルがInputStreamで取得できるのでファイルに保存するなり好きにしてください。 // API呼び出し DownloadService service = retrofit.create(DownloadService.class); Response<ResponseBody> response = service.download().execute(); InputStream is = new BufferedInputStream(response.body().byteStream()); OutputStream os = new BufferedOutputStream( new FileOutputStream(new File("/tmp/test.gif"))); try (is; os) { int read = 0; byte[] bytes = new byte[1024]; while ((read = is.read(bytes)) != -1) { os.write(bytes, 0, read); } } おしまい 以上、最近仕事で実装しているRetrofitについての3日間連続投稿でした。 明日は@FlatMountainさんのiOSアプリ開発のお話です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Retrofit+OkHttpでmultipart/form-dataなAPIの呼び出してハマった

この記事はラクスアドベントカレンダーの4日目の記事です。 空いているので昨日に引き続き書いていきたいと思います。 はじめに multipart/form-dataなAPIを呼び出す実装が一番面倒だと思っているのですが、自前でゴリゴリと実装している人が意外と多い印象です。 Retrofit+OkHttpを使ったらどれぐらい簡単なのか試してみたのですがハマり所もあったので記事していきます。 ハマりどころ @retrofit2.http.Multipart @retrofit2.http.Part @retrofit2.http.Body okhttp3.MultipartBody okhttp3.MultipartBody.Part okhttp3.RequestBody あたりを組み合わせて実装するのですが、まずい組み合わせでハマったり、必要なリクエストヘッダーが指定できそうで出来なかったりと使い方が分かるまで結構大変でした。 StackOverFlowでも反応が多いのでハマっている人の多さが伺い知れます。 まずやっておいた方が良いこと どんなリクエストデータが作成されたのか見れないとデバッグもままならないのでリクエストボディをログに出すようにInterceptorを仕込んでおくことをお勧めします。 HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(); loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); OkHttpClient client = new OkHttpClient.Builder() .addInterceptor(loggingInterceptor) .build(); これによって、生のリクエスト内容がログに出るので何が起きているか把握できるようになります。 ちなみにmultipart/form-dataのバウンダリーや改行がどう入るのかを見れるので勉強にもなるかと思います。 2021-12-04 22:43:08.014 INFO 43159 --- [nio-8080-exec-1] okhttp3.OkHttpClient : --> POST http://localhost:8080/upload3 2021-12-04 22:43:08.014 INFO 43159 --- [nio-8080-exec-1] okhttp3.OkHttpClient : Content-Type: multipart/form-data; boundary=1f2d4b84-5503-43b1-aa79-2e46bd64074f 2021-12-04 22:43:08.014 INFO 43159 --- [nio-8080-exec-1] okhttp3.OkHttpClient : Content-Length: 343 2021-12-04 22:43:08.015 INFO 43159 --- [nio-8080-exec-1] okhttp3.OkHttpClient : 2021-12-04 22:43:08.015 INFO 43159 --- [nio-8080-exec-1] okhttp3.OkHttpClient : --1f2d4b84-5503-43b1-aa79-2e46bd64074f Content-Disposition: form-data; name="json" Content-Length: 14 {"param": "1"} --1f2d4b84-5503-43b1-aa79-2e46bd64074f Content-Disposition: form-data; name="file"; filename="test.csv" Content-Type: text/csv Content-Length: 24 no,name 1,taro 2,hanako --1f2d4b84-5503-43b1-aa79-2e46bd64074f-- 2021-12-04 22:43:08.015 INFO 43159 --- [nio-8080-exec-1] okhttp3.OkHttpClient : --> END POST (343-byte body) 実装方法その1 MutliPartBodyオブジェクトを自分で組み立てて@Bodyで渡すやり方。 後で出てくる@Multipartと@Bodyは組み合わせられないのですが、最初はそれをやろうとしてハマりました。 interface UploadService { @POST("upload") Call<Map<String, String>> upload(@Body MultipartBody multipartBody); } 次に呼び出し方。 MutliPartBody.Builder.addFormDataPartを使ってマルチパートのボディ部分を作るのが一番良さそうでした。 それに気づくまではaddPartを使ったり試行錯誤したのですが、思った通りのリクエスト内容にならなくて結構ハマりました。 MultipartBody multipartBody = new MultipartBody.Builder() .setType(MultipartBody.FORM) //デフォルトはmultipart/mixedなのでmultipart/form-dataに設定し直す // Content-Typeを指定したくて試行錯誤した結果がこちら // filenameにnullを渡さないといけないのが気持ち悪いけど他に丁度いいインターフェイスがない .addFormDataPart("json", null, RequestBody.create(MediaType.get("application/json"), "{\"param\": \"1\"}")) .addFormDataPart("file", "test.csv", RequestBody.create(MediaType.get("text/csv"), new File("/tmp/test.csv"))) .build(); service.upload(multipartBody).execute(); 実装方法その2 今度は@Multipartを使うパターンです。 前述した通り@Bodyとは組み合わせられないので@Partを使います。 interface UploadService2 { @Multipart @POST("upload3") Call<Map<String, String>> upload(@Part MultipartBody.Part json, @Part MultipartBody.Part file); } MultipartBody.Partを作ってAPIに渡します。作り方はその1と同じような感じでOKです。 MultipartBody.Part json = MultipartBody.Part.createFormData("json", null, RequestBody.create(MediaType.get("application/json"), "{\"param\": \"1\"}")); MultipartBody.Part file = MultipartBody.Part.createFormData("file", "test.csv", RequestBody.create(MediaType.get("text/csv"), new File("/tmp/test.csv"))); UploadService2 service2 = retrofit.create(UploadService2.class); service2.upload(json, file).execute(); おわり 以上、やりたいことが出来ました。 結局、かなり試行錯誤しないと実装出来ませんでしたが、やり方が分かってしまえばスッキリ短く書けました!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む