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')
}

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」というファイルを削除する。

Spring IOのport out of range -1エラー

Spring Tools Suite、またはそのプラグインを入れたEclipseで「Failed to fetch Generation from Spring IO: port out of range:-1」というエラーが表示される場合、以下の設定を変更することでエラー解消することが出来る。

Eclipseの設定を開き、一般>ネットワーク接続のアクティブ・プロバイダーを「直接」に変更する。

Tomcat 10をJava 21で動かす

UbuntuにTomcat 10をインストールしJava 21で動かしたいが、まだパッケージでは提供されていないため、手動でインストールする。

前提

Java 21がインストール済みであること。
インストールされていない場合はこちらの記事の「Java 21のインストール」を参照のこと。

$ java -version
openjdk version "21" 2023-09-19 LTS
OpenJDK Runtime Environment Temurin-21+35 (build 21+35-LTS)
OpenJDK 64-Bit Server VM Temurin-21+35 (build 21+35-LTS, mixed mode, sharing)

Tomcat 10のインストール

Apache Tomcatのページがら最新版をダウンロードして解凍する。今後のため、シンボリックリンクも作成しておく。

$ wget https://dlcdn.apache.org/tomcat/tomcat-10/v10.1.15/bin/apache-tomcat-10.1.15.tar.gz
$ tar -xvf apache-tomcat-10.1.15.tar.gz
$ sudo ln -s /opt/apache-tomcat-10.1.15 /opt/tomcat-10

Tomcat 10用の実行ユーザーを作成する。

$ sudo useradd -m -U -d /opt/tomcat-10 -s /bin/false tomcat10

ファイルのオーナーを先ほど作成したユーザーに変更する。

$ sudo chown -R tomcat10: /opt/apache-tomcat-10.1.15

シェルを実行可能にする。

$ sudo sh -c 'chmod +x /opt/tomcat-10/bin/*.sh'

SystemD用のファイルを作成する。

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

SystemDの設定ファイルを再読み込みする。

$ sudo systemctl daemon-reload

Tomcat 10を有効にし、起動する。

$ sudo systemctl enable --now tomcat10
Created symlink /etc/systemd/system/multi-user.target.wants/tomcat10.service  /lib/systemd/system/tomcat10.service.

正常起動を確認する。

$ sudo systemctl status tomcat10
 tomcat10.service - Apache Tomcat 10 Web Application Server
     Loaded: loaded (/lib/systemd/system/tomcat10.service; enabled; vendor pres>
     Active: active (running) since Wed 2023-10-25 05:58:29 UTC; 6s ago
    Process: 12815 ExecStart=/opt/tomcat-10/bin/startup.sh (code=exited, status>
   Main PID: 12822 (java)
      Tasks: 35 (limit: 18910)
     Memory: 101.2M
        CPU: 6.485s
     CGroup: /system.slice/tomcat10.service
             mq12822 /opt/jdk-21/bin/java -Djava.util.logging.config.file=/opt/>
10月 25 05:58:29 hostname systemd[1]: Starting Apache Tomcat 10 Web Applicati>
10月 25 05:58:29 hostname tomcat10[12815]: Tomcat started.
10月 25 05:58:29 hostname systemd[1]: Started Apache Tomcat 10 Web Applicatio>

JenkinsをJava 21で動かす

JenkinsでビルドしていたプロジェクトをJava 21に変更したため、JenkinsもJava 21で起動する必要がある。

前提

Ubuntu上で既にJenkinsがJava 17で稼働しているものとする。
Jenkinsは最新版を使用している。

Java 21のインストール

下記からLinux用JDKをダウンロードして、任意のフォルダに解凍する。
https://adoptium.net/temurin/releases/

$ cd /opt
$ sudo wget https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21%2B35/OpenJDK21U-jdk_x64_linux_hotspot_21_35.tar.gz
$ sudo tar -xvf OpenJDK21U-jdk_x64_linux_hotspot_21_35.tar.gz
$ sudo ln -s /opt/jdk-21+35 /opt/jdk-21

Javaの設定

$ sudo update-alternatives --install /usr/bin/java java /opt/jdk-21/bin/java 1
$ sudo update-alternatives --install /usr/bin/javac javac /opt/jdk-21/bin/javac 1
$ sudo update-alternatives --install /usr/bin/javadoc javadoc /opt/jdk-21/bin/javadoc 1

インストールしたJDKをデフォルトに設定する。

$ sudo update-alternatives --config java
$ sudo update-alternatives --config javac
$ sudo update-alternatives --config javadoc

Java 21に切り替わっていることを確認する。

$ java -version
openjdk version "21" 2023-09-19 LTS
OpenJDK Runtime Environment Temurin-21+35 (build 21+35-LTS)
OpenJDK 64-Bit Server VM Temurin-21+35 (build 21+35-LTS, mixed mode, sharing)

Jenkinsの起動と動作確認

Jenkinsを起動する。

$ sudo systemctl start jenkins
$ sudo systemctl status jenkins
 jenkins.service - Jenkins Continuous Integration Server
・・・
10月 19 04:37:40 systemd[1]: Started Jenkins Continuous Integration >

Java 21のプロジェクトをビルドし、正常終了することを確認する。