20190719のJavaに関する記事は5件です。

ApplicationInsightsのjavaagentを仕込んで実行されたSQLをトレース

ドキュメントにある内容をやっただけです。

https://docs.microsoft.com/ja-jp/azure/azure-monitor/app/java-agent

こんな感じになります。

image.png

黒塗りばかりのこの画像は所要時間が5分かかった謎のリクエストを調べたときのものです。
4回SQLクエリが実行されている様子と、2個ほどクソクエリがある様子が分かります。クエリのどれかをクリックすると、右のコマンドにSQLが出てきます。

SQLの内容はPreparedStatementを使っているためかPreparedなSQLになっており、パラメータは「?」になっているまでは見れないようです。(逆に埋められても機密情報が見えかねないのでこれはこれで良いと思います)
呼び出されたコードの位置もわかりません。でも実装者なら十分役立つことでしょう。

もちろん、DBクエリの情報をApplicationInsightsに送るため、転送量がかかるのでその点は注意してください。

前提

  • Javaアプリ
  • ApplicationInsightsは導入済み

仕組み

JavaAgentを使って、クラスローダでクラスを読み込む際にクラスファイルに手を加え、テレメトリを書き出す仕組みになっているようです。

JavaAgentを組み込むだけでよいので、動いているコードには手を加えずに利用できます。

導入

  • javaagentファイルをダウンロード
  • 同じ場所にAI-Agent.xmlを作成
  • JVM引数に-javaagent:...を追加
  • アプリケーション再起動

導入手順はAzure WebApp上を想定していますが、JavaのWebアプリケーションならAzure WebAppに限らずちょっと読み替えるだけで導入できます。

ApplicationInsightsのjavaagentを導入する

https://github.com/microsoft/ApplicationInsights-Java/releases/ で公開されているので、導入済みのApplicationInsightsのバージョンにあったagentを入れます。

Azure WebAppなら、kudu上でこんな感じに操作。D:\home\data 辺りなら邪魔にならないかと思いますが、適宜変更してください。

cd D:\home\data
curl -LO https://github.com/microsoft/ApplicationInsights-Java/releases/download/2.1.1/applicationinsights-agent-2.1.1.jar

AI-Agent.xmlファイルを作成

AI-Agent.xmlというファイルを先ほどダウンロードしたjarファイルがある場所と同じ場所に作成してください。

内容は以下をコピペで良いです。

AI-Agent.xml
<?xml version="1.0" encoding="utf-8"?>
<ApplicationInsightsAgent>
    <Instrumentation>
        <BuiltIn enabled="true"> <!-- ここのenabled属性も見ているため忘れずに。 -->
            <JDBC enabled="true" /> <!-- JDBCでクエリを投げた時にクエリの実行時間とPreparedなSQLのテレメトリを送信 -->
        </BuiltIn>
    </Instrumentation>
</ApplicationInsightsAgent>

今回はJDBCのみですが、設定でいくつかのHTTP呼び出しなどもトレースできるようです。

JVM引数に-javaagent:...を追加

JVM引数に以下を追加します。ファイルパスは適宜置き換えます。

-javaagent:D:/home/data/applicationinsights-agent-2.1.1.jar

Tomcatで動かすアプリなら環境変数JAVA_OPTSCATALINA_OPTSでよいです。

Azure WebAppならアプリケーション設定かkudu上からweb.configで。ただし、アプリケーション設定の方が優先されるので、その点を考慮してください。

アプリケーション再起動

javaagentを読み込ませるために再起動が必要です。

以上。

その他

冗長なテレメトリを抑制したい

現状、時間がかかった場合だけ送る、といったことは出来ないようです。

例えば、十分に早いことが分かっているクエリや、コネクションプーリングを使っているとプールから取り出したときに腐ってないかチェックするときのSELECT 1でもテレメトリが送られてしまいます。
もし防ぎたい場合は、フィルタを書く必要があります。以下のIssueにTelemetryProcessorの実装例があります。

参考: https://github.com/microsoft/ApplicationInsights-Java/issues/837#issuecomment-471610584

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaでLambda関数を書いてSAMでデプロイしてみた

はじめに

 これまで、AWS Lambdaを主にPython、時々、Node.jsで作ってきましたが、気分転換にJavaでの開発方法を調べたのでまとめます。

 今回は、Javaで書いたLambda関数のソースコードをGradleでビルドしてAWS SAMでデプロイしてみました。
 Eclipseでの開発用に AWS Toolkit for Eclipse も提供されていますが、普段はVimで開発してCLIでデプロイしているので、そちらに合わせます。

検証環境

  • OS: macOS High Sierra 10.13.6
  • aws-cli: 1.15.38
  • aws-sam: 0.11.0
  • Java 1.8.0_221
  • JDK: OpenJDK 1.8.0_212 (Amazon Corretto 8.212)
  • Gradle 5.5.1

JavaでLambdaを開発する上でのポイント

ランタイムはJava 8のみ

 下の公式ドキュメントにあるように、提供されているJavaのランタイムはJava 8 (JDK 8)のみです。
 それ以外のバージョンを使用する場合は、サポートされるまで待つかカスタムランタイムを使う必要があります。

Java による Lambda 関数のビルド - AWS Lambda

zipファイルかjarファイルにしてでデプロイ

 Javaで書かれたアプリケーションを、zipファイルまたはスタンドアロンjarにパッケージしてデプロイします。

 公式ドキュメントではMavenを使ってスタンドアロンjarに、Gradleを使ってzipファイルにしてデプロイする方法が紹介されてます。
 今回は、Gradleでzipファイルにパッケージングしてデプロイする方法で試しました。

Java の AWS Lambda デプロイパッケージ - AWS Lambda

ハンドラー関数のリクエストの受け方・返し方が3種類

 ハンドラー関数のリクエストの受け取り方、そして、レスポンスの返し方に、以下のように複数の方式があります。

  1. Java のシンプルな型
  2. POJO (Plain Old Java Object) 型
  3. ストリーム型

 それぞれでハンドラー関数の書き方が変わります。つまり、ハンドラー関数の書き方に3種類あるということです。

 「このイベントトリガーを使うならこのタイプ」というような決め方というよりも、どういったデータが送られてくるかという視点で使い分けるようです。
 この状況にはこれという明確なものはないようなので、実際にJavaでLambda関数を開発する際はどの方式にするか要検討です。

 今回は、「POJO型」を採用しました。

ハンドラーの入出力タイプ (Java) - AWS Lambda

今回作ったもの

 Lambda関数のトリガーとして API Gateway を使った、簡単なWebアプリケーションを作成しました。
 POSTリクエストを送ると、"Hello"と返してくるだけのシンプルなAPIです。

サンプルコード

 作成したコードはここに置いてます。
https://github.com/mmclsntr/awslambda-javagradle

プロジェクト構成

.
├── build/
│   ├── distributions/
│   │   └── awslambda-javagradle.zip   # ビルドで生成されるデプロイパッケージ
│   └── ...
├── build.gradle                    # Gradleビルド設定ファイル
├── src/main/java/awslambda/javagradle
│   ├── Greeting.java            # アプリの中核となる部分
│   ├── Handler.java             # Lambdaのハンドラ関数を格納
│   └── Response.java            # Lambdaのレスポンスを整形
└── template.yml                    # CloudFormationテンプレートファイル
└── その他Gradle用ファイル

コーディング

ハンドラクラス

Handler.java
package awslambda.javagradle;

import java.util.Collections;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;

public class Handler implements RequestHandler<Map<String, Object>, Response> {

    private static final Logger LOG = Logger.getLogger(Handler.class.getName());

    @Override
    public Response handleRequest(Map<String, Object> input, Context context) {
        LOG.info("received: " + input);
        LOG.setLevel(Level.INFO);

        Greeting greetingBody = new Greeting("Hello");

        return Response.builder()
                .setStatusCode(200)
                .setObjectBody(greetingBody)
                .build();
    }
}

レスポンスクラス

Response.java
package awslambda.javagradle;

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Collections;
import java.util.Map;
import org.apache.log4j.Logger;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class Response {

    private final int statusCode;
    private final String body;
    private final Map<String, String> headers;
    private final boolean isBase64Encoded;

    public Response(int statusCode, String body, Map<String, String> headers, boolean isBase64Encoded) {
        this.statusCode = statusCode;
        this.body = body;
        this.headers = headers;
        this.isBase64Encoded = isBase64Encoded;
    }
    ...

}

あいさつクラス

 サンプルとしてHelloと返してくれる超シンプルなアプリケーションを作ります。

Greeting.java
package awslambda.javagradle;

public class Greeting {
    private String greetings;

    public Greeting(String greetings) {
        this.greetings = greetings;
    }
    public String getGreetings() {
        return greetings;
    }

    public void setGreetings(String greetings) {
        this.greetings = greetings;
    }
}

CloudFormationテンプレート

template.yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
    AWS Lambda Java with Gradle

Globals:
    Function:
        Timeout: 20

Resources:
  PostGreetingFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: build/distributions/awslambda-javagradle.zip
      Handler: awslambda.javagradle.Handler::handleRequest
      Runtime: java8
      Events:
        GetOrder:
          Type: Api
          Properties:
            Path: /
            Method: post

Outputs:
    ApiEndpoint:
      Description: "API Gateway endpoint URL for Prod stage"
      Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/"

    PostGreetingFunction:
      Description: "PostGreeting Lambda Function ARN"
      Value: !GetAtt PostGreetingFunction.Arn

デプロイ

ビルド設定ファイル作成

build.gradle
apply plugin: 'java'

repositories {
    mavenCentral()
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

dependencies {
    compile (
        'com.amazonaws:aws-lambda-java-core:1.1.0',
        'com.amazonaws:aws-lambda-java-log4j:1.0.0',
        'com.amazonaws:aws-lambda-java-events:1.1.0',
        'com.fasterxml.jackson.core:jackson-core:2.8.5',
        'com.fasterxml.jackson.core:jackson-databind:2.8.5',
        'com.fasterxml.jackson.core:jackson-annotations:2.8.5'
    )
}

// Task for building the zip file for upload
task buildZip(type: Zip) {
    from compileJava
    from processResources
    into('lib') {
        from configurations.runtime
    }
}

build.dependsOn buildZip

ビルド

Gradle コマンドでビルドします。

gradle build

AWS SAMでデプロイ

デプロイ先S3バケット作成

aws s3 mb s3://<デプロイ先S3バケット> --aws-profile=<AWSプロファイル>

パッケージ

sam package で、上で作成したS3バケットへ実行ファイルをアップロード & デプロイ用テンプレートファイルを生成します。

sam package \
--s3-bucket <デプロイ先S3バケット名> \
--s3-prefix <デプロイ先S3フォルダ名(プレフィクス) ※任意> \
--output-template-file output.yml \
--profile <AWSプロファイル>

アウトプットとして、 output.yml ファイルが作られます。

デプロイ

sam deploy で、LambdaとAPI Gatewayをデプロイします。

sam deploy \
--template-file output.yml \
--stack-name awslambda-javagradle-greeting \
--capabilities CAPABILITY_IAM \
--profile <AWSプロファイル>

確認

AWS上にLambda関数とAPI Gatewayが作られたので、エンドポイントにPOSTリクエストを投げて見ます。

リクエスト
curl -X POST https://xxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/
レスポンス
{"greetings":"Hello"}

所感

 私自身、普段はJavaを使わないのでちょっと時間がかかりましたが、簡単に作れたなという印象です。Pythonなどのスクリプト言語で作るよりも当然ながらコード量は増えますが、厳格なサーバーレス開発ができることは魅力的です。

 ハンドラー関数の書き方に複数種類あるといったJavaで書く上での特有の仕様が、今回だけでは把握しきれませんでした。実際に使う際は、その辺りも考慮しながら詳細設計したいと思います。

 また、噂通り、初期起動が遅いです (レスポンスされるまで5秒くらい)。
 参照: https://cero-t.hatenadiary.jp/entry/20160106/1452090214

まとめ

 Javaを利用したLambda関数の開発の特徴やコーディング感覚をつかむことができました。
 もし、サーブレットから移行するならそこそこ大規模な改修が必要と思いますが、代わりとして使えなくもない印象です。

 ランタイムがこのままJava8のみなのか、そこのサポートが少し心配です。。
 API Gatewayをトリガーとしましたが、他のサービスも試して見たいと思います。

参考

https://qiita.com/kamata1729/items/8d88ea10dd3bb61fa6cc
https://qiita.com/riversun/items/7fcc06617b469aed8f27

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

テスト対象のクラスの一部のメソッドをモック化する方法

最近noteに書くか、Qiitaに書くか迷い中です。

いつも忘れる、いつも一部をモック化したいときの書き方。

  • mock化したいメソッド
@Slf4j
@Service
public class HighHolidayService {

    // 一月単位でまとめた休日リスト
    Map<String, List<LocalDate>> highHolidayCache = new HashMap<>();

    // テスト対象のメソッド
    public List<LocalDate> getHolidayList(LocalDate targetDate) {
        String key = String.format("%d%02d", targetDate.getYear(), targetDate.getMonthValue());
        if (highHolidayCache.containsKey(key)) {
            return highHolidayCache.get(key);
        }
        List<LocalDate> holidayList = selectRecords(targetDate);
        highHolidayCache.put(key, holidayList);
        return holidayList;
    }

    // このメソッドをモック化したい!!!!
    List<LocalDate> selectRecords(LocalDate targetDate) {
            return null;
    }
}

  • テストの書き方
package jp.co.hogehoge.service;

import static org.mockito.Matchers.anyObject;
import static org.mockito.Mockito.doReturn;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;

import org.junit.Before;
import org.junit.Test;
import org.mockito.MockitoAnnotations;
import org.springframework.boot.test.mock.mockito.SpyBean;

public class HighHolidayServiceTest {

    @SpyBean    // springboot の@spyの書き方
    HighHolidayService highHolidayService;

    @Before
    public void init() {
        MockitoAnnotations.initMocks(this);
    }
    @Test
    public void 対象の休日リストを取得する() {
        List<LocalDate> returnHolidayList = new ArrayList<>();
        returnHolidayList.add(LocalDate.of(2019, 07, 28));
        returnHolidayList.add(LocalDate.of(2019, 07, 21));
        returnHolidayList.add(LocalDate.of(2019, 07, 15));
        returnHolidayList.add(LocalDate.of(2019, 07, 14));
        returnHolidayList.add(LocalDate.of(2019, 07, 7));
        // doReturn(返却するオブジェクト).when(spyしたオブジェクト).method()
        // この書き方忘れる!!
        doReturn(returnHolidayList).when(highHolidayService).selectRecords(anyObject());

        LocalDate date = LocalDate.of(2019, 7, 15);
        List<LocalDate> holidayList = highHolidayService.getHolidayList(date);
        holidayList.forEach(System.out::println);
    }
}

クラスごとMock化するのはよくやるからいいんだけど、テスト対象の一部をモック化するのはたまにしか出てこないから忘れるんですよね。。
というか、クラス分けたほうがいいって話が多数で(そうなんだけど、そこまでするまでもないロジックだったりするし)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Java】ActiveDirectoryの参照/更新

はじめに

ADのユーザーをメンテナンスする機能を提供するため、ユーザー一覧を取得したり、更新できるようにしたい。
JavaではJNDIを使ってADへアクセスできる。

ユーザー一覧の取得

ユーザー一覧の取得
private static void getUserList() throws NamingException {

    Hashtable<String, String> env = new Hashtable<String, String>();
    env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
    env.put(Context.PROVIDER_URL, "ldap://xxxx"); // ADサーバーのアドレス
    env.put(Context.SECURITY_AUTHENTICATION, "simple");
    env.put(Context.SECURITY_PRINCIPAL, "user@domain"); // ADサーバーにログオンするためのユーザーID@ドメイン
    env.put(Context.SECURITY_CREDENTIALS, "password"); // パスワード

    DirContext context = null;
    try {
        context = new InitialDirContext(env);

        // 検索条件の指定
        String name = "OU=aaa, DC=bbb, DC=ccc"; // どの範囲を検索対象とするか
        String filter = "(objectCategory=user)";

        SearchControls control = new SearchControls();
        control.setSearchScope(SearchControls.SUBTREE_SCOPE);

        // 検索実行
        NamingEnumeration<SearchResult> results = context.search(name, filter, control);

        // 結果取得
        while (results.hasMoreElements()) {
            SearchResult result = results.next();
            NamingEnumeration<? extends Attribute> attributes = result.getAttributes().getAll();
            AdUserBean user = new AdUserBean();

            while (attributes.hasMoreElements()) {
                Attribute attribute = attributes.next();
                Object value = attribute.get();

                if ("name".equals(attribute.getID())) {
                    if (value instanceof String) {
                        user.setName((String) value);
                    }

                } else if ("displayName".equals(attribute.getID())) {
                    if (value instanceof String) {
                        user.setDisplayName((String) value);
                    }

                } else if ("cn".equals(attribute.getID())) {
                    if (value instanceof String) {
                        user.setCn((String) value);
                    }

                } else if ("userPrincipalName".equals(attribute.getID())) {
                    if (value instanceof String) {
                        user.setUserPrincipalName((String) value);
                    }

                }

            }

            System.out.println(user);
        }

    } finally {
        try {
            if (context != null) {
                context.close();
            }
        } catch (NamingException e) {
        }
    }
}

主な属性:

名前 説明
userPrincipalName ユーザーID@ドメイン
displayName 表示名

メールアドレスは単体項目だが、電話番号は複数指定可能な項目。

参考にしたサイト:
https://www.earthlink.co.jp/engineerblog/intra-mart-engineerblog/3336/
(ただしこちらのサンプルは、AD認証するためにユーザーIDを指定した検索)

Active Directory のユーザーの属性についての説明
https://docs.microsoft.com/en-us/windows/win32/ad/naming-properties
Active Directory の属性一覧
https://docs.microsoft.com/en-us/windows/win32/adschema/attributes-all

LDAP の検索フィルター構文
http://software.fujitsu.com/jp/manual/manualfiles/M050000/B1WN4911/01/idmgr07/idmgr447.htm
https://www.ibm.com/support/knowledgecenter/ja/SSYJ99_8.5.0/admin-system/rbug_ldapfltrxprns.html

ユーザー属性の変更

ユーザー属性の変更
private static void update() throws NamingException {

    Hashtable<String, String> env = new Hashtable<String, String>();
    env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
    env.put(Context.PROVIDER_URL, "ldap://server"); // ADサーバーのアドレス
    env.put(Context.SECURITY_AUTHENTICATION, "simple");
    env.put(Context.SECURITY_PRINCIPAL, "user@domain"); // ADサーバーにログオンするためのユーザーID@ドメイン
    env.put(Context.SECURITY_CREDENTIALS, "password"); // パスワード

    DirContext context = null;
    try {
        context = new InitialDirContext(env);

        Attributes attrs = new BasicAttributes();
        attrs.put(new BasicAttribute("pager", "123456"));
        attrs.put(new BasicAttribute("telephoneNumber", "1234567"));

        String name = "CN=username, OU=xxx, DC=yyy, DC=zzz"; // どの範囲を対象とするか

        context.modifyAttributes(name, DirContext.REPLACE_ATTRIBUTE, attrs);

    } finally {
        try {
            if (context != null) {
                context.close();
            }
        } catch (NamingException e) {
        }
    }
}

更新条件をちゃんとユーザー1人になるようにしないと、複数ユーザーが一気に更新されてしまいそう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Programming News】Qiitaまとめ記事 July 18, 2019 Vol.4

筆者が昨日2019/7/18(木)に気になったQiitaの記事をまとめました。昨日のまとめ記事はこちら

Java

Python

Node.js

Nuxt.js

Swift

Laravel

MySQL

Git

Visual Studio

AWS

Docker

Google Apps Script

Develop

Raspberry

UML

Go言語

R言語

awk

LaTex

Redmine

更新情報

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む