Java 17から21にアップグレードする

Java 21がリリースされたので、自前のプログラムをJava 21に変更してみる。

コードの変更部分

Java 21に変更したら下記のコードが非推奨になっていた。

new Locale(locale)
new URL(referer)

それぞれ下記の通り修正した。

Locale.of(locale)
URI.create(referer).toURL()

newするようなコードは今後もなくなっていくのだろうか。個人的には変更後のコードの方が好きだ。

2023/10/17 追記

pom.xmlに下記設定を追記した。

-proc:full

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.11.0</version>
    <configuration>
        <compilerArgs>
            <arg>-parameters</arg>
            <arg>-proc:full</arg>
        </compilerArgs>
    </configuration>
</plugin>

-XX:+EnableDynamicAgentLoading

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.1.2</version>
    <configuration>
        <reuseForks>false</reuseForks>
        <argLine>${argLine} -XX:+EnableDynamicAgentLoading -Dcatalina.base=${project.build.directory}</argLine>
    </configuration>
</plugin>
2023/10/18 追記

Java 21にコンパイルの設定を変更すると、なぜか下記11行目ががコンパイルエラーになる。(一部抜粋)

import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class NovelProcessTest {

    @Mock
    private MessageSourceAccessor messages;

    @Test
    void testExecute() {
        when(messages.getMessage(anyString())).thenReturn(

下記のように修正するとコンパイルエラーは解消する。理由はわからない。

import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class NovelProcessTest {

    @Mock
    private MessageSourceAccessor messages;

    @Test
    void testExecute() {
        String msg = messages.getMessage(anyString());
        when(msg).thenReturn(

下記でもコンパイルエラーは解消するので、whenがメソッドの最初にあるのが駄目なのだろうか。

    @Test
    void testExecute() {
        String msg = "";
        when(messages.getMessage(anyString())).thenReturn(

jjwt 0.11.5から0.12.1にアップグレードする

以下のメソッドを変更した。

- SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS512);
+ SecretKey key = Jwts.SIG.HS512.key().build();

- Claims claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
+ Claims claims = Jwts.parser().verifyWith(key).build().parseSignedClaims(token).getPayload();

Jwts.builder()
-        .setClaims(claims)
-        .setSubject(username)
-        .setIssuedAt(createdDate)
-        .setExpiration(expirationDate)
+        .claims(claims)
+        .subject(username)
+        .issuedAt(createdDate)
+        .expiration(expirationDate)
        .signWith(key)
        .compact();

0.12.0はバグがあるので注意!
https://github.com/jwtk/jjwt/issues/854

class io.jsonwebtoken.impl.lang.OptionalMethodInvoker cannot access class sun.security.util.KeyUtil (in module java.base) because module java.base does not export sun.security.util to unnamed module

Eclipse上でSVNのURLを切り替える

Subversion(SVN)サーバーのIPやURLが変わった場合に、開発環境の設定を変更する覚書。

プロジェクト単位ではなく、レポジトリーで切り替える。

1.SVNレポジトリーブラウザでロケーションのプロパティを開く

2.レポジトリー・ロケーションの編集のURLの部分を変更する

※ID、パスワードの再入力が必要になるので、用意しておくこと。

Hibernate 6.3.0にアップデートしたらNullPointerExceptionが発生した

アップデートしたら下記のエラーが発生するようになった。

java.lang.NullPointerException: Cannot invoke "org.hibernate.boot.spi.MetadataImplementor.getEntityBindings()" because "this.metadata" is null

Hibernate 6.3.0のバグっぽい。下記で修正中。

HHH-17154 Fix NullPointerException is thrown when constructing Entity…

同日にリリースされた6.2.8に変更したところ、NullPointerExceptionは出なくなったため、しばらくこちらを利用することにした。

依存関係にあるJarファイルを1つのフォルダにまとめる

理由があってJarファイルをまとめてコピーしておく必要があるとき、Mavenビルドのプラグインに以下を追加すると、指定したフォルダに依存関係のあるJarファイルを全て出力することが出来る。

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-dependency-plugin</artifactId>
            <executions>
                <execution>
                    <id>copy-dependencies</id>
                    <phase>package</phase>
                    <goals>
                        <goal>copy-dependencies</goal>
                    </goals>
                    <configuration>
                        <outputDirectory>${project.build.directory}/lib</outputDirectory>
                        <excludeGroupIds>org.apache.maven.surefire</excludeGroupIds>
                    </configuration>
                </execution>
            </executions>
        </plugin>

Hibernate ORM 6.2とHibernate Search 6.1とjandexについて

Hibernate ORM 6.2とHibernate Search 6.1を同時に使用する場合、それぞれの依存関係に設定されているjandexのバージョンが異なるため、method not foundエラーが発生する。(実験的な互換性らしい)

エラーが発生した場合は、以下の通りHibernate Searchの方にjandexを除外する設定を追記する。

<dependency>
    <groupId>org.hibernate.search</groupId>
    <artifactId>hibernate-search-mapper-orm-orm6</artifactId>
    <version>${hibernate-search.version}</version>
    <exclusions>
        <exclusion>
            <groupId>org.jboss</groupId>
            <artifactId>jandex</artifactId>
        </exclusion>
    </exclusions>
</dependency>

2023/7/11 追記:Hibernate Search 6.2がリリースされ、上記の対応は不要になった。

Spring Boot 3.1にアップグレードする

Spring Bootの3.1がリリースされたため、早速アップグレードしたところ、Spring Securityのバージョンが6.1になっていた。

その結果、このクラスでは以下のメソッドが非推奨になった。
.exceptionHandling()
.cors()
.csrf()
.formLogin()
.httpBasic()
.authorizeExchange()
.and()

@Bean
public SecurityWebFilterChain securitygWebFilterChain(ServerHttpSecurity http) {
    return http
            .exceptionHandling()
            .authenticationEntryPoint((swe, e) -> {
                return Mono.fromRunnable(() -> {
                    swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
                });
            }).accessDeniedHandler((swe, e) -> {
                return Mono.fromRunnable(() -> {
                    swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
                });
            }).and()
            .cors().configurationSource(corsConfigurationSource())
            .and().csrf().disable()
            .formLogin().disable()
            .httpBasic().disable()
            .authenticationManager(authenticationManager)
            .securityContextRepository(securityContextRepository)
            .authorizeExchange()
            .pathMatchers(HttpMethod.OPTIONS).permitAll()
            .pathMatchers("/crawler-api/login").permitAll()
            .pathMatchers("/crawler-api/signup").permitAll()
            .pathMatchers("/crawler-api/users*").hasAuthority("ROLE_USER")
            .pathMatchers("/crawler-api/novels*").hasAuthority("ROLE_USER")
            .anyExchange().authenticated()
            .and().build();
}

非推奨になったメソッドには新たにラムダ式で記述出来る同名のメソッドが追加されている。今後はそちらを使用する。

新しいメソッドに置き換えると下記の通りとなる。

.exceptionHandling(exceptionHandling -> exceptionHandling
        .authenticationEntryPoint((swe, e) -> {
            return Mono.fromRunnable(() -> {
                swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            });
        }).accessDeniedHandler((swe, e) -> {
            return Mono.fromRunnable(() -> {
                swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
            });
        }))
.csrf(csrf -> csrf.disable())
.formLogin(formLogin -> formLogin.disable())
.httpBasic(httpBasic -> httpBasic.disable())
.authenticationManager(authenticationManager)
.securityContextRepository(securityContextRepository)
.authorizeExchange(exchanges -> exchanges
        .pathMatchers(HttpMethod.OPTIONS).permitAll()
        .pathMatchers("/crawler-api/login").permitAll()
        .pathMatchers("/crawler-api/signup").permitAll()
        .pathMatchers("/crawler-api/users*").hasAuthority("ROLE_USER")
        .pathMatchers("/crawler-api/novels*").hasAuthority("ROLE_USER")
        .anyExchange().authenticated())
.build();

cors()についてはCorsConfigurationSourceのBeanが定義されていれば自動的に読み込まれるとのことなので、SecurityWebFilterChainからは削除した。
また、新しいメソッドを置き換えた結果、.and()は不要になった。

JunitでMock使用時、エラーになる場合がある

mockito-core-5.2.0.jar

JunitでMockを使用しているとき、下記のエラーが出る。何度も実行していると、たまに正常に終了するときもある。

Error creating bean with name 'userDao' defined in class path resource [common/service/applicationContext-test.xml]: Failed to instantiate [java.lang.Object]: Factory method 'mock' threw exception with message: Please don't pass any values here. Java will detect class automagically.

調べてみたら下記に理由が書いてあった。
automatically detect class to mock #2779

修正内容は以下の通り。

    <bean id="userDao" class="org.mockito.Mockito" factory-method="mock">
-        <constructor-arg value="common.dao.jpa.UserDao" />
+        <constructor-arg>
+            <value type="java.lang.Class">common.dao.jpa.UserDao</value>
+       </constructor-arg>
    </bean>

Java 8のOptionalクラスの考察

動機

Optionalについて、うまく使えていない気がするため、改めて整理してみる。

実装例

Nullチェック

nullの一番の問題点は、nullチェックを行わなくともコンパイルエラーにならないこと。そして実行時にNullPointerExceptionが発生する可能性があること。

例として、nullチェックを行う場合について考えてみる。

public String getFruit() {
    return 果物の名前または、nullを返却する処理とする;
}

public void fruitCheck() {
    var fruit = getFruit();

    if (fruit != null) {
        System.out.println(fruit + " are delicious.");            
    }
}

これを単純にOptionalに置き換えると下記のようになる。

public Optional<String> getFruit() {
    String name = 果物の名前または、nullを返却する処理とする;
    return Optional.ofNullable(name);
}

public void fruitCheck() {
    var fruit = getFruit();

    if (fruit.isPresent()) {
        System.out.println(fruit.get() + " are delicious.");
    }
}

これだと、nullチェックを行っているときと変わらない。この処理を典型的な関数型プログラミングのスタイルに書き換えてみる。

public void fruitCheck() {
    var fruit = getFruit();

    fruit.ifPresent(name -> System.out.println(name + " are delicious."));
}

elseの処理が必要な場合は、下記のように書ける。

fruit.ifPresentOrElse(name -> System.out.println(name + " are delicious."),
        () -> System.out.println("I'm hungry."));

Nullの場合にデフォルト値を設定する処理

getFruit()のメソッドでnullを返すのではなく、nullの場合デフォルトの値を返す場合を考えてみる。

public String getFruit() {
    String name = 果物の名前または、nullを返却する処理とする;

    if (name != null) {
        return name;
    } else {
        return "Apple";
    }
}

public void fruitCheck() {
    var fruit = getFruit();

    System.out.println(fruit + " are delicious.");            
}

Optionalに置き換えると下記のようになる。

public String getFruit() {
    String name = 果物の名前または、nullを返却する処理とする;

    return Optional.ofNullable(name).orElse("Apple");
}

関数型プログラミングのスタイルになって、Nullチェックのミスも減るだろうか。

Nullチェックするとともに値のチェックも行う場合

public void fruitCheck() {
    var fruit = getFruit();

    if (fruit != null && fruit.equals("Banana")) {
        System.out.println(fruit + " are very delicious.");            
    }
}
public void fruitCheck() {
    var fruit = getFruit();

    fruit.filter(name -> name.equals("Banana"))
            .ifPresent(name -> System.out.println(name + " are delicious."));
}

Javaでファイル内容を比較し、結果を別ファイルに出力する

目的

キーが1対1の関係になっている2つのファイルを1行ずつ読みだし内容を比較する。その比較結果を別のファイルに出力するプログラムをJavaで作成する。

要件

〇入力ファイル
 ・在庫ファイル
   商品コード
   在庫数
 ・注文ファイル
   商品コード
   注文数
 ※それぞれ、商品コードで一意、昇順になっている。

〇出力ファイル
 ・新在庫ファイル
   商品コード
   在庫数
 ・エラーファイル
   商品コード
   注文数
 新在庫ファイル・・・入力ファイルの在庫と注文を突合して、在庫数から注文数を引いた数量を記録する
 エラーファイル・・・注文に対して、在庫がない、在庫が足りない場合はエラーとし、当ファイルに出力する

〇その他
 入力ファイルのサイズが大きいため、1行ずつ読み込んで処理していく必要がある。

実装例

public void matching() {
    try (var stockFile = Files.newBufferedReader(Paths.get("src/stock.txt"));
            var orderFile = Files.newBufferedReader(Paths.get("src/order.txt"));
            var newStockFile = Files.newBufferedWriter(Paths.get("src/newStock.txt"));
            var errorOrderFile = Files.newBufferedWriter(Paths.get("src/errorOrder.txt"));) {
        // 入力ファイルの1行目を読み込む
        // 文字列からオブジェクトに変換する
        Stock stock = Stock.createFromLine(stockFile.readLine());
        Order order = Order.createFromLine(orderFile.readLine());

        // どちらかのファイルの行があるうちは処理を続ける
        while (stock.getKey() < Integer.MAX_VALUE || order.getKey() < Integer.MAX_VALUE) {
            // 比較するコードを取得。(この例ではコードを数値としている)
            // コードで昇順になっているのでコードの大小を比較すれば、
            // 一方のファイルにしかない商品もわかる
            int stockKey = stock.getKey();
            int orderKey = order.getKey();

            if (stockKey - orderKey == 0) {
                // コードが一致
                int qty = stock.getQuantity() - order.getQuantity();

                if (qty < 0) {
                    // 在庫不足
                    // エラーファイル出力
                    errorOrderFile.write(order.toErrorOrder("01").toLine());
                    errorOrderFile.newLine();
                } else {
                    stock.setQuantity(qty);
                }

                // オブジェクトから1行分の文字列を生成し、書き出す
                newStockFile.write(stock.toNewStock().toLine());
                newStockFile.newLine();
                // 次の行を読み込む
                stock = Stock.createFromLine(stockFile.readLine());
                order = Order.createFromLine(orderFile.readLine());
            } else if (stockKey - orderKey > 0) {
                // 商品なし
                // エラーファイル出力
                errorOrderFile.write(order.toErrorOrder("02").toLine());
                errorOrderFile.newLine();
                // 次の行を読み込む
                order = Order.createFromLine(orderFile.readLine());
            } else {
                // 注文なし
                newStockFile.write(stock.toNewStock().toLine());
                newStockFile.newLine();
                // 次の行を読み込む
                stock = Stock.createFromLine(stockFile.readLine());
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

※他にも「var」宣言を使える箇所があるが、わかりやすくするため、あえて通常の宣言にしている。

@Builder
@Data
public class Order {

    private String category;
    private String code;
    private Integer quantity;

    public Integer getKey() {
        if (category == null && code == null) {
            return Integer.MAX_VALUE;
        } else {
            return Integer.valueOf(category + code);
        }
    }

    public ErrorOrder toErrorOrder(String errorCode) {
        return ErrorOrder.builder()
                .category(category)
                .code(code)
                .quantity(quantity)
                .errorCode(errorCode)
                .build();
    }

    public static Order createFromLine(String line) {
        if (line == null) {
            return Order.builder().build();
        } else if (line.length() < 35) {
            throw new IllegalArgumentException();
        }

        return Order.builder()
                .category(line.substring(4, 5))
                .code(line.substring(6, 7))
                .quantity(Integer.valueOf(line.substring(8, 9)))
                .build();
    }
}