Raspberry pi 2 Clusteringで遊ぶ

発売当時くらいに買ったRaspberry pi 2を一通り遊んだ後、放置して使っていなかったので、またちょっと遊んでみる。

ゴール

Raspberry piが手元に7台あるので、Load Balancer、 Reverse Proxy、Application Server、DB Serverで冗長構成を作成してみる。

Raspberry pi 2はメインメモリが2GB、CPUも32bitと今の基準からすると非力なので、なるべく軽い構成にする。Kubernetesとか使いたかったが、スペック的に無理だった。

Load Balancer: Nginx
Reverse Proxy: Nginx ※無くても良い
Application Server: Apache Tomcat 10 (Java 21)
DB Server: MariaDB

Ubuntu 22のインストール

Raspberry Pi Imagerを使ってUbuntuをSDカードにインストールする。

OSでOther general-purpose OS > Ubuntu > Ubuntu Server 22.04.4 LTS (32bit)を選択する。

初期設定

適当に選んだRaspberry Piで起動し、共通の設定を行ってから、SDカードのイメージを取り、他のSDカードにコピーしていく。

sudo apt update
sudo apt upgrade
sudo apt install net-tools


ローカルで使うだけなので、SSH接続でパスワード認証出来るようにしてしまう。

sudo vi /etc/ssh/sshd_config.d/60-cloudimg-settings.conf
下記の通り修正

PasswordAuthentication yes


静的なIPを割り当てる。
イメージを他の機器にコピーするときはIPを変更する。

sudo vi /etc/netplan/50-cloud-init.yaml
下記の通り修正

network:
    ethernets:
        eth0:
            dhcp4: false
            dhcp6: false
            addresses: [ローカルIP/24] #設定したIPは機器ごとに後で変更すること
            routes:
                - to: default
                  via: ゲートウェイIP #無いならroutesの設定いらない
            nameservers:
                addresses: [8.8.8.8, 8.8.4.4]
    version: 2


ホスト名を変更可能にする。

sudo netplan try
sudo vi /etc/cloud/cloud.cfg
下記の通り修正

preserve_hostname: true


ホスト名を設定する。
イメージを他の機器にコピーするときはホスト名を変更する。

sudo hostnamectl set-hostname rp1 #設定したホスト名は機器ごとに後で変更すること

Load Balancer

初期設定したイメージから作業を始める。

nginxをインストールする。

sudo apt install nginx
sudo systemctl start nginx
sudo systemctl enable nginx
sudo vi /etc/nginx/nginx.conf
下記の通り修正

worker_connections 16;


Web Serverとしては使用しないのでdefaultの設定は読み込まないようにし、Load Balancer用の設定ファイルを作成する。

sudo rm /etc/nginx/sites-enabled/default
sudo vi /etc/nginx/conf.d/loadbalancer.conf
下記の通り新規作成

upstream backend {
    #least_conn;
    #ip_hash; #設定しても良いが、ラウンドロビンされていることを直ぐに確認したいので今回はなし
    server Reverse Proxy Server No.1のIP:80;
    server Reverse Proxy Server No.2のIP:80;
}

server {
    listen 80 default_server;
    server_name _;

    location / {
        proxy_pass http://backend;
    }
}


設定ファイルに誤りがないことを確認し、nginxを再起動する。

sudo nginx -t
sudo systemctl restart nginx

Reverse Proxy

初期設定したイメージから作業を始める。
Reverse Proxy Serverは2台作成する。

nginxをインストールする。

sudo apt install nginx
sudo systemctl start nginx
sudo systemctl enable nginx
sudo vi /etc/nginx/nginx.conf
下記の通り修正

worker_connections 16;


Web Serverとしては使用しないのでdefaultの設定は読み込まれないようにし、Reverse Proxy用の設定ファイルを作成する。

sudo rm /etc/nginx/sites-enabled/default
sudo vi /etc/nginx/conf.d/proxy.conf
下記の通り新規作成

server {
    listen 80 default_server;
    server_name _;

    location / {
        proxy_pass http://Application Server1のIP:8080;
    }
}


もう1台のReverse Proxy Serverも同様に設定する。

Application Serverも2台構成にする予定のため、
proxy_pass http://Application Server1のIP:8080;のIPはそれぞれ別のIPになる。

設定ファイルに誤りがないことを確認し、nginxを再起動する。

sudo nginx -t
sudo systemctl restart nginx

Application Server

Java 21のインストール
Tomcat 10のインストール

Tomcat 10はインストールパッケージがなかったため、Apache Tomcatのページからダウンロードして解凍する。実行ユーザー、シンボリックリンクも作っておく。

sudo apt install openjdk-21-jdk

cd /opt
sudo tar xvfz /mnt/nfs/apache-tomcat-10.1.20.tar.gz
sudo ln -s /opt/apache-tomcat-10.1.20 tomcat
sudo useradd -m -U -d /opt/tomcat -s /bin/false tomcat
sudo chown -R tomcat: /opt/apache-tomcat-10.1.20


起動用のスクリプトを作成する。

sudo vi /usr/lib/systemd/system/tomcat.service
下記の通り新規作成

#
# Systemd unit file for Apache Tomcat
#
[Unit]
Description=Apache Tomcat 10 Web Application Server
After=network.target
[Service]
# Configuration
Environment="JAVA_OPTS=-Djava.awt.headless=true"
Environment="CATALINA_BASE=/opt/tomcat"
Environment="CATALINA_HOME=/opt/tomcat"
Environment="CATALINA_OPTS=-Xmx1024m -server"
Environment="CATALINA_PID=/opt/tomcat/temp/tomcat.pid"
# Lifecycle
Type=forking
ExecStart=/opt/tomcat/bin/startup.sh
ExecStop=/opt/tomcat/bin/shutdown.sh
Restart=always
PIDFile=/opt/tomcat/temp/tomcat.pid
# Logging
SyslogIdentifier=tomcat
# Security
User=tomcat
Group=tomcat
[Install]
WantedBy=multi-user.target


自動起動は設定しない。

sudo systemctl daemon-reload
sudo systemctl start tomcat


もう1台のApplication Serverも同様に設定する。

起動確認が終わったら一度Tomcatを停止して、レプリケーションの設定をserver.xmlに追記する。

今回は簡単に確認するため、SimpleTcpClusterを使用している。別の機会にSpring Sessionを使用した方法を試してみたい。

<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"
         channelSendOptions="6">
  <Manager className="org.apache.catalina.ha.session.BackupManager"
           expireSessionsOnShutdown="false"
           notifyListenersOnReplication="true"
           mapSendOptions="6"/>
  <Channel className="org.apache.catalina.tribes.group.GroupChannel">
    <Membership className="org.apache.catalina.tribes.membership.McastService"
                address="228.0.0.4"
                port="45564"
                frequency="500"
                dropTime="3000"/>
    <Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
              address="auto"
              port="5000"
              selectorTimeout="100"
              maxThreads="6"/>

    <Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
      <Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/>
    </Sender>
    <Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/>
    <Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatchInterceptor"/>
    <Interceptor className="org.apache.catalina.tribes.group.interceptors.ThroughputInterceptor"/>
  </Channel>

  <Valve className="org.apache.catalina.ha.tcp.ReplicationValve" filter=""/>

  <ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/>
</Cluster>


もう1台のApplication Serverも同様に設定する。

DB Server

まずは3台に対してMariaDBをインストールして初期設定を実行する。

sudo apt install mariadb-server
sudo systemctl stop mariadb
sudo mkdir /mnt/ssd/mariadb


データの保存先を変更し、リモートアクセス可能にする。

sudo vi /etc/mysql/mariadb.conf.d/50-server.cnf
下記の通り修正

datadir                 = /mnt/ssd/mariadb
#bind-address            = 127.0.0.1


MariaDBが外部フォルダにアクセス出来るようにApplication Armorのセキュリティ設定を変更する。

sudo vi /etc/apparmor.d/tunables/alias
下記の通り修正

alias /var/lib/mysql/ -> /mnt/ssd/mariadb/,


Application Armorを再起動し、MariaDBのデータフォルダをコピーする。

sudo systemctl restart apparmor
sudo rsync -avuz /var/lib/mysql/ /mnt/ssd/mariadb


MariaDBを起動し、初期設定する。

sudo systemctl start mariadb
sudo mysql_secure_installation
Enter current password for root (enter for none):
Switch to unix_socket authentication [Y/n] n
Change the root password? [Y/n] 
New password:
Re-enter new password:
Remove anonymous users? [Y/n] y
Disallow root login remotely? [Y/n] n
Remove test database and access to it? [Y/n] y
Reload privilege tables now? [Y/n] y

mysql -u root -p
grant all privileges on *.* to root@"%" identified by 'password' with grant option;
exit;

sudo systemctl restart mariadb


接続確認を行ったら、一度停止する。

MariaDBをインストールすると自動的にGalera Clusterがインストールされているはず。

自動起動を無効にし、バイナリロギングを有効にする。

sudo systemctl disable mariadb
sudo mkdir -p /mnt/ssd/log/mariadb
sudo chown -R mysql: /mnt/ssd/log/mariadb
sudo vi /etc/mysql/mariadb.conf.d/50-server.cnf
下記の通り修正

server-id               = 1 # サーバー毎に変更
log_bin                 = /mnt/ssd/log/mariadb/mariadb-bin.log


Galera Clusterの設定を行う。

sudo vi /etc/mysql/mariadb.conf.d/60-galera.cnf
下記の通り修正

[galera]
# Mandatory settings
wsrep_on                 = ON
wsrep_cluster_name       = "MariaDB Galera Cluster"
wsrep_cluster_address    = gcomm://サーバーIP1,サーバーIP2,サーバーIP3
binlog_format            = row
default_storage_engine   = InnoDB
innodb_autoinc_lock_mode = 2
wsrep_provider           = /usr/lib/galera/libgalera_smm.so

# Allow server to accept connections on all interfaces.
bind-address = 0.0.0.0


最初の1台は下記コマンドで実行する。以降は通常の起動コマンドで起動する。

sudo galera_new_cluster
※再起動時の注意

データが保存されているフォルダにgrastate.datというファイルがあるので、safe_to_bootstrap: 1となっているサーバーからgalera_new_clusterで起動する。

sudo less /mnt/ssd/mariadb/grastate.dat

# GALERA saved state
version: 2.1
uuid:    xxx
seqno:   -1
safe_to_bootstrap: 1


それ以外だと、エラー(WSREP: It may not be safe to bootstrap the cluster from this node. It was not the last one to leave the cluster and may not contain all the updates. To force cluster bootstrap with this node, edit the grastate.dat)となって起動できない。

動作確認

Javaアプリを作りクラスタリングされているか確認する。

Plugin could not be resolved. Ensure the plugin’s groupId, artifactId and version are present.が表示される

問題点

Eclipseでpom.xml開くと下記artifactIdの行にワーニングが表示されていた。

<reporting>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-javadoc-plugin</artifactId>
==省略==
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-project-info-reports-plugin</artifactId>
==省略==

Plugin could not be resolved. Ensure the plugin's groupId, artifactId and version are present.
Additional information: Unable to resolve org.apache.maven.plugins:maven-javadoc-plugin
Plugin could not be resolved. Ensure the plugin's groupId, artifactId and version are present.
Additional information: Unable to resolve org.apache.maven.plugins:maven-project-info-reports-plugin

Maven Repository上に存在はしている。
https://mvnrepository.com/artifact/org.apache.maven.plugins/maven-javadoc-plugin
https://mvnrepository.com/artifact/org.apache.maven.plugins/maven-project-info-reports-plugin

修正点

原因は<reporting>のみ記述していたこと。下記のように<pluginManagement>にも記述したらワーニングは表示されなくなった。

<pluginManagement>
    <plugins>
        <plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-javadoc-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-project-info-reports-plugin</artifactId>
            </plugin>

Jxls 2から3にアップグレードする

<!-- https://mvnrepository.com/artifact/org.jxls/jxls-poi -->
<dependency>
    <groupId>org.jxls</groupId>
    <artifactId>jxls-poi</artifactId>
    <version>3.0.0</version>
</dependency>

メジャーバージョンが3に上がり、Javaコードを殆ど書き直すことになったため、そのメモを残す。

参考URL:https://jxls.sourceforge.net/migration-to-v3-0.html

Jxls 2

ほぼ全てが変わったので、比較対象にならないが、変更前のコードは下記の通りとなっている。

import org.jxls.area.Area;
import org.jxls.builder.AreaBuilder;
import org.jxls.builder.xls.XlsCommentAreaBuilder;
import org.jxls.common.CellRef;
import org.jxls.common.Context;
import org.jxls.util.TransformerFactory;

// 出力部分抜粋
try (InputStream is = getTemplateSource(getUrl(), request);
        OutputStream os = response.getOutputStream()) {
    var transformer = TransformerFactory.createTransformer(is, os);
    AreaBuilder areaBuilder = new XlsCommentAreaBuilder(transformer);
    List<Area> xlsAreaList = areaBuilder.build();
    var xlsArea = xlsAreaList.get(0);
    var context = new Context();

    model.entrySet().forEach(mode -> context.putVar(mode.getKey(), mode.getValue()));

    xlsArea.applyAt(new CellRef("Sheet1!A1"), context);
    transformer.write();
}

※getTemplateSourceはテンプレート用のエクセルファイルを読み込んでいるメソッド。

Jxls 3

Java部分は下記の通りかなりシンプルになった。テンプレートファイル自体は変更の必要はなかった。

import org.jxls.builder.JxlsStreaming;
import org.jxls.transform.poi.JxlsPoiTemplateFillerBuilder;

// 出力部分抜粋
try (InputStream is = getTemplateSource(getUrl(), request);
        OutputStream os = response.getOutputStream()) {
    JxlsPoiTemplateFillerBuilder.newInstance()
        .withStreaming(JxlsStreaming.STREAMING_ON)
        .withTemplate(is)
        .buildAndFill(model, new JxlsOutputStream(os));
}

ファイル出力用のクラスはファイルをローカルに保存するJxlsOutputFile.classのみ提供されているため、必要に応じて自分で実装する。今回はHttpResponseのOutputStreamに直接乗せたかったので、下記の通り実装した。

import java.io.IOException;
import java.io.OutputStream;

import org.jxls.builder.JxlsOutput;

/**
 * Excelを書き込むためのOutputStreamを提供するクラス.
 */
public class JxlsOutputStream implements JxlsOutput {

    private OutputStream os;

    public JxlsOutputStream(OutputStream os) {
        this.os = os;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public OutputStream getOutputStream() throws IOException {
        return os;
    }
}

Switch Expressionsに書き換える

しばらく放置していたが、Java 14から正式に追加されたSwitch Expressions、Java 21から追加されたPattern Matching for switchで既存の処理を書き換えることにした。

Java 11から21までの間のSwitch文の大きな変更は2つあって、分けて考えた方がわかりやすい。

Java 14で追加されたSwitch Expressions

これに書き換えることのメリットは2つと考えている。

1.breakが不要

Switch文から抜けるためのbreakが不要になった。breakがなくても同じように機能する。

例)変更前
switch (code) {
case 401, 403:
    model.addAttribute("errorTitle", "403");
    break;
case 404:
    model.addAttribute("errorTitle", "404");
    break;
default:
    model.addAttribute("errorTitle", "error");
}
変更後
switch (code) {
case 401, 403 -> model.addAttribute("errorTitle", "403");
case 404 -> model.addAttribute("errorTitle", "404");
default -> model.addAttribute("errorTitle", "error");
};

2.無駄な記述が減る

breakが不要になったことも含めて、無駄な記述を減らせる。

例)変更前
int multiple = 0;

switch (unitSign) {
case M_BYTE:
    multiple = 1024 * 1024;
    break;
case K_BYTE:
    multiple = 1024;
    break;
case BYTE:
    multiple = 1;
}
変更後
int multiple = switch (unitSign) {
case M_BYTE -> 1024 * 1024;
case K_BYTE -> 1024;
case BYTE -> 1;
};

Java 21で追加されたPattern Matching for switch

パターンマッチが使えるようになったが、正直すぐ使いそうなのはnullのチェックくらいだろうか。

例)変更前
public static FileConverter createConverter(FileType fileType) {
    if (fileType == null) {
        throw new FileException("errors.fileType");
    }

    switch (fileType) {
    case XML:
        return createXmlConverter();
    case EXCEL:
        return createExcelConverter();
    case CSV:
        return createCsvConverter();
    default:
        throw new FileException("errors.fileType");
    }
}
変更後
public static FileConverter createConverter(FileType fileType) {
    return switch (fileType) {
    case XML -> createXmlConverter();
    case EXCEL -> createExcelConverter();
    case CSV -> createCsvConverter();
    case null, default -> throw new FileException("errors.fileType");
    };
}

Gradle 7から8にアップグレードする

Gradle 7から8にアップデートした時のメモ。

非推奨になった箇所を修正した。
参考URL
https://docs.gradle.org/current/userguide/upgrading_version_8.html#changes_8.2

sourceCompatibility = '21'

java {
    sourceCompatibility = JavaVersion.VERSION_21
}

他にもpluginsで読み込んだものは、war{}のように囲んでから、プロパティを記述する必要がありそう。

例)

plugins {
    id 'war'
}

war {
    webAppDirectory = file('src/main/webapp')
}

PyTorch 2.2にアップグレードする

環境

Windows 11
Microsoft Store版 Python 3.10

動機

1年ほど前からバージョンアップしていなかったので、なんとなく更新することにした。

python -c "import torch; print( torch.__version__ )"
2.0.1+cu118

必要となるCUDAのバージョンの確認

PyTorchのページにアクセスし、使用可能なCUDAのバージョンを確認する。PyTorch 2.2.1では、11.8と12.1となっている。今回は12.1を使用してみる。

CUDA Toolkit 12.1.1のインストール

NVIDIA DeveloperのページからCUDA Toolkit 12.1.1をダウンロードし、インストールする。

なぜか、高速インストールだとエラーになってしまったので、カスタムを選択する。

CUDAのRuntime、Documentation、Developmentのみチェックして、次へをクリックすると正常にインストールが始まった。Nsightは後で入れることにする。

正常にインストール完了した。

cuDNN v8のインストール

NVIDIA DeveloperのページからcuDNN v8.9.7 (December 5th, 2023), for CUDA 12.xをダウンロードし、任意のファルダに解凍、binフォルダにパスを通す。

PyTorch 2.2のインストール

1.旧バージョンを削除

pip uninstall torch torchvision torchaudio

2.新バージョンをインストール

pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121

3.確認

インストール完了後、下記のコマンドを実行しバージョンを確認する。

python -c "import torch; print( torch.__version__ )"
2.2.1+cu121

import torch

print(torch.__version__)
print(torch.cuda.is_available())
print(torch.cuda.get_device_name())

2.2.1+cu121
True
NVIDIA GeForce RTX 3060 Ti

OAuth 2.0を使用して Office365のSMTPでメール送信したい

注意

Microsoft Exchangeのアカウント(例:@の後ろが企業名とか独自のドメインになっているもの)でないとSMTPのOAuth認証を有効に出来ないような。アカウント持っていないので詳細はわからない。

個人アカウントでも、Microsoft Entraでアプリの登録が出来るので、トークン取得部分だけ動作確認した。Microsoft Exchangeのアカウントがあれば、通しでも多分動くはず。

build.gradle
plugins {
	id 'java'
}

group = 'oauth2hotmail'
version = '0.0.1-SNAPSHOT'

java {
	sourceCompatibility = '17'
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
    implementation 'com.microsoft.azure:msal4j:1.14.2'
    implementation 'com.sun.mail:jakarta.mail:2.0.1'
}

tasks.named('test') {
	useJUnitPlatform()
}
HotmailOAuth.java
public class HotmailOAuth {

    private static final String SCOPE = "https://graph.microsoft.com/.default";
    // アプリケーション(クライアント) ID
    private static final String CLIENT_ID = "";
    // クライアント シークレット
    private static final String CLIENT_SECRET = "";
    // OAuth 2.0 トークン エンドポイント (v2)
    private static final String AUTHORITY = "https://login.microsoftonline.com/ディレクトリ(テナント) ID/oauth2/v2.0/token";
    private static final String MAIL_FROM = "sender@foo.bar";
    private static final String MAIL_TO = "receiver@foo.bar";

    public static void main(String[] args) throws MalformedURLException, AddressException, MessagingException {
        // アクセス トークンを取得
        var token = getAccessTokenFromSecret();

        // SMTP セッションを作成
        var props = new Properties();
        props.put("mail.smtp.host", "それぞれのSMTPホスト名");
        props.put("mail.smtp.port", "587");
        props.put("mail.smtp.auth", "true");
        props.put("mail.smtp.auth.mechanisms", "XOAUTH2");
        props.put("mail.smtp.starttls.enable", "true");
        var session = Session.getDefaultInstance(props);

        // メールを作成
        var message = new MimeMessage(session);
        message.setFrom(new InternetAddress(MAIL_FROM));
        message.setRecipient(Message.RecipientType.TO, new InternetAddress(MAIL_TO));
        message.setSubject("OAuth 2.0認証を使って送信したメール");
        message.setText("このメールは、OAuth 2.0 認証を使って送信しました。");

        // メールを送信
        var transport = session.getTransport("smtp");
        transport.connect(MAIL_FROM, token);
        transport.sendMessage(message, message.getAllRecipients());
        transport.close();

        System.out.println("メールを送信しました。");
    }

    public static String getAccessTokenFromSecret() throws MalformedURLException {
      var credential = ClientCredentialFactory.createFromSecret(CLIENT_SECRET);

      var cca = ConfidentialClientApplication
              .builder(CLIENT_ID, credential)
              .authority(AUTHORITY)
              .build();

      var parameters = ClientCredentialParameters
              .builder(Set.of(SCOPE))
              .build();

      var result = cca.acquireToken(parameters).join();

      return result.accessToken();
    }
}

Hibernate Search 7.0 Migration

Hibernate Search 7.0がリリースされていたので、アップグレードした。今回プログラムの修正は必要なかった。

pom.xmlの変更点

  • lucene-analyzers-kuromojiをlucene-analysis-kuromojiに変更
  • hibernate-search-mapper-orm-orm6をhibernate-search-mapper-ormに変更
<!-- Hibernate Search & Lucene -->
<dependency>
    <groupId>org.apache.lucene</groupId>
    <artifactId>lucene-analysis-kuromoji</artifactId>
    <version>9.8.0</version>
</dependency>
<dependency>
    <groupId>org.hibernate.search</groupId>
    <artifactId>hibernate-search-mapper-orm</artifactId>
    <version>7.0.0.Final</version>
</dependency>
<dependency>
   <groupId>org.hibernate.search</groupId>
   <artifactId>hibernate-search-backend-lucene</artifactId>
   <version>7.0.0.Final</version>
</dependency>

バージョンを変えるだけでなく、artifactIdを変更する必要がある。luceneを使用している場合はそちらも変更する必要がある。

OAuth 2.0を使用して GmailのSMTPでメール送信したい

前提条件

Gmailのアカウントを持っていること。

Google Cloudの設定

https://console.cloud.google.com/にアクセスして、プロジェクトを作成する。

わかりやすい名前を付ける。

User Typeは「外部」を選択する。

テスト用なのでアプリ情報は適当に入力する。

プロジェクトを作成したら、「認証情報」タブの「認証情報を作成」をクリックする。

「OAuthクライアントID」を選択する。

アプリケーションの種類には「デスクトップアプリ」を選択する。メールを送信するだけなので、ライブラリからGmail APIを追加する必要はない。

作成が完了したら、クライアント ID、クライアント シークレットが発行されるので、メモしておく。

プログラムの例

build.gradle
plugins {
	id 'java'
}

group = 'oauth2gmail'
version = '0.0.1-SNAPSHOT'

java {
	sourceCompatibility = '17'
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
    implementation 'com.google.api-client:google-api-client:2.2.0'
    implementation 'com.sun.mail:jakarta.mail:2.0.1'
}

tasks.named('test') {
	useJUnitPlatform()
}
GmailOAuth.java

メモしておいたクライアント ID、クライアント シークレットを、CLIENT_ID、CLIENT_SECRET変数にそれぞれ設定する。

今回リダイレクト処理は必要ないので、リダイレクトURLにはurn:ietf:wg:oauth:2.0:oobを設定する。これを設定するとリダイレクトせずに直接画面に認証コードが表示される。

MAIL_FROMにGmailのアドレスを設定する。MAIL_TOにはテストメールを送っても大丈夫なメールアドレスを設定する。

public class GmailOAuth {

    private static final String SCOPE = "https://mail.google.com/";
    private static final String CLIENT_ID = "CLIENT_ID";
    private static final String CLIENT_SECRET = "CLIENT_SECRET";
    private static final String REDIRECT_URI = "urn:ietf:wg:oauth:2.0:oob";
    private static final String MAIL_FROM = "sender@foo.bar";
    private static final String MAIL_TO = "receiver@foo.bar";

    public static void main(String[] args) throws IOException, MessagingException {
        // 取得した認証コードを保存するフォルダを指定
        var fileDataStoreFactory = new FileDataStoreFactory(new File(System.getProperty("java.io.tmpdir")));

        // OAuth 2.0 の認証フローを作成
        var flow = new GoogleAuthorizationCodeFlow.Builder(new NetHttpTransport(),
                GsonFactory.getDefaultInstance(), CLIENT_ID, CLIENT_SECRET, Set.of(SCOPE))
                        .setCredentialDataStore(StoredCredential.getDefaultDataStore(fileDataStoreFactory)).build();

        // アクセス トークンを取得
        var credential = flow.loadCredential("user");

        // 取得済みの認証情報があるか、またそれは有効か確認
        if (credential == null || (credential.getExpiresInSeconds() < 100 && !credential.refreshToken())) {
            // 有効なコードがなかった場合、新たに認可コードを取得
            var url = flow.newAuthorizationUrl().setRedirectUri(REDIRECT_URI).build();
            System.out.println("Please open the following URL in your browser then type the authorization code:");
            System.out.println("  " + url);
            System.out.println("Please enter your authentication code:");

            try (var s = new Scanner(System.in)) {
                var code = s.nextLine();
                var tokenResponse = flow.newTokenRequest(code).setRedirectUri(REDIRECT_URI).execute();
                credential = flow.createAndStoreCredential(tokenResponse, "user");
            }
        }

        // SMTP セッションを作成
        var props = new Properties();
        props.put("mail.smtp.host", "smtp.gmail.com");
        props.put("mail.smtp.port", "587");
        props.put("mail.smtp.auth", "true");
        props.put("mail.smtp.auth.mechanisms", "XOAUTH2");
        props.put("mail.smtp.starttls.enable", "true");
        var session = Session.getDefaultInstance(props);

        // メールを作成
        var message = new MimeMessage(session);
        message.setFrom(new InternetAddress(MAIL_FROM));
        message.setRecipient(Message.RecipientType.TO, new InternetAddress(MAIL_TO));
        message.setSubject("Gmail から送信したメール");
        message.setText("このメールは、Gmail の SMTP で OAuth 2.0 認証を使って送信しました。");

        // メールを送信
        var transport = session.getTransport("smtp");
        transport.connect(MAIL_FROM, credential.getAccessToken());
        transport.sendMessage(message, message.getAllRecipients());
        transport.close();

        System.out.println("メールを送信しました。");
    }
}

実行

初回実行時、下記のメッセージが表示されるようになっている。表示されているURLにブラウザでアクセスし、アクセス権を付与する。

Please open the following URL in your browser then type the authorization code:
  ここに表示されるURLにブラウザでアクセスする
Please enter your authentication code:

ChromeブラウザでGmailにログイン済みの状態であれば、下記の通りアカウントの選択に表示される。もちろん別のアカウントを選択しても良い。

テスト用のプロジェクトのため、注意事項が表示されるがそのまま続行する。

アクセス権の付与が間違いないことを確認して、続行する。

表示された認証コードをコピーして、「Please enter your authentication code:」の下に貼り付けEnterを押下する。

「メールを送信しました。」と表示されたら、送信先のメールアドレスにメールが送信されたことを確認する。

2回目以降は、認証コードの有効期限内であれば、上記の手順無しでメール送信できる。逆にもう一度最初から実行したい場合は、「System.getProperty(“java.io.tmpdir”)」フォルダ内の「StoredCredential」というファイルを削除する。

ChromeでYouTube視聴中マウスを動かすとノイズが表示される

ChromeでYouTube視聴中マウスを動かすと、黒と透過の市松模様のノイズが画面に表示されることがある。

Chromeの設定変更で直ると記載があったので、下記の通り変更してみた。

1.Chromeのアドレスバーに「chrome://flags/」と入力してEnter

2.検索ボックスに「Choose ANGLE graphics backend」と入力

3.プルダウンを選択し、DefaultからOpenGLに変更

4.Chromeを再起動