CLOVERšŸ€

That was when it all began.

Spring Batch恧态Bean ValidationćØItemć®ć‚¹ć‚­ćƒƒćƒ—ć®å‹•ä½œć‚’ē¢ŗčŖć™ć‚‹

恓悌ćÆ态ćŖć«ć‚’ć—ćŸćć¦ę›øć„ćŸć‚‚ć®ļ¼Ÿ

Spring BatchćØBean Validation悒ēµ„ćæåˆć‚ć›ć¦ć€Item恮惐ćƒŖćƒ‡ćƒ¼ć‚·ćƒ§ćƒ³ć‚’ć—ćŸć‚Šć€NG恫ćŖć£ćŸItemć‚’ć‚¹ć‚­ćƒƒćƒ—ć§ććŸć‚Šć™ć‚‹ćØ恄恆恮恧
ć”ć‚‡ć£ćØ試恗恦ćæ悈恆恋ćŖćØ怂

Spring BatchćØBean Validation

Spring Batch恧Bean Validation恌ä½æ恈悋恓ćØćÆćƒ‰ć‚­ćƒ„ćƒ”ćƒ³ćƒˆć«ę›øć‹ć‚Œć¦ć„ć‚‹ć‚ć‘ć§ć™ćŒć€ItemProcessor恮ē”Øé€”ć®ć²ćØ恤ćØ恗恦ē™»å “ć—ć¾ć™ć€‚

Item processing / Validating Input

BeanValidatingItemProcessorćØ恄恆ItemProcessorćŒęä¾›ć•ć‚Œć¦ć„ć‚‹ć®ć§ć€ć“ć”ć‚‰ć‚’ä½æć£ć¦å®Ÿē¾ć™ć‚‹ć‚ˆć†ć§ć™ć­ć€‚

BeanValidatingItemProcessor (Spring Batch 4.3.5 API)

Spring恮惐ćƒŖćƒ‡ćƒ¼ć‚·ćƒ§ćƒ³ć‚’ä½æć†å “åˆćÆć€ć“ć”ć‚‰ć®ć‚ˆć†ć§ć™ć€‚

ValidatingItemProcessor (Spring Batch 4.3.5 API)

惐ćƒŖćƒ‡ćƒ¼ć‚·ćƒ§ćƒ³ć§NG恫ćŖ悋ćØä¾‹å¤–ćŒć‚¹ćƒ­ćƒ¼ć•ć‚Œć‚‹ć®ć§ć€ćć®ć¾ć¾ć ćØć‚¢ćƒ—ćƒŖć‚±ćƒ¼ć‚·ćƒ§ćƒ³ćŒåœę­¢ć™ć‚‹ć‚ˆć†ć§ć™ć€‚

原因ćØćŖć£ćŸItemć‚’ć‚¹ć‚­ćƒƒćƒ—ć™ć‚‹ć«ćÆ态Step恧čØ­å®šć™ć‚‹ć‚ˆć†ć§ć™ć€‚

Configuring a Step / Chunk-oriented Processing / Configuring Skip Logic

今回ćÆć€ć“ć®ć‚ćŸć‚Šć‚’č©¦ć—ć¦ćæć¾ć™ć€‚

ćŠé”Œ

ä»Šå›žć®ćŠé”ŒćÆ态仄äø‹ćØć—ć¾ć™ć€‚

  • ę›øē±ćƒ‡ćƒ¼ć‚æ恮CSV悒čŖ­ćæč¾¼ćæ态čŖ­ćæč¾¼ć‚“ć ćƒ‡ćƒ¼ć‚æ悒ęؙęŗ–å‡ŗåŠ›ć«ę›ø恍å‡ŗ恙Chunkå½¢å¼ć®JobćŠć‚ˆć³Stepć‚’ä½œęˆć™ć‚‹
  • Bean Validationć‚’č”Œć†itemProcessor悒čæ½åŠ ć—态ć‚Øćƒ©ćƒ¼ć«ćŖ悋Itemć‚’å«ć‚“ć CSVć‚’å…„åŠ›ć™ć‚‹
  • ć¾ćšćÆ惐ćƒŖćƒ‡ćƒ¼ć‚·ćƒ§ćƒ³NG恫ćŖć£ć¦åœę­¢ć™ć‚‹ćØ恓悍悒ē¢ŗčŖć—态ꬔ恫ć‚Øćƒ©ćƒ¼ć«ćŖć£ćŸItemć‚’ć‚¹ć‚­ćƒƒćƒ—ć™ć‚‹čØ­å®šć‚’č”Œć†

恓悓ćŖę„Ÿć˜ć§ć‚„ć£ć¦ć„ćć¾ć™ć€‚

ē’°å¢ƒ

ä»Šå›žć®ē’°å¢ƒćÆ态恓恔悉怂

$ java --version
openjdk 17.0.3 2022-04-19
OpenJDK Runtime Environment (build 17.0.3+7-Ubuntu-0ubuntu0.20.04.1)
OpenJDK 64-Bit Server VM (build 17.0.3+7-Ubuntu-0ubuntu0.20.04.1, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.5 (3599d3414f046de2324203b78ddcf9b5e4388aa0)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.3, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-109-generic", arch: "amd64", family: "unix"

JobRepositoryć®ćƒ‡ćƒ¼ć‚æćÆ态MySQLć«ę ¼ē“ć™ć‚‹ć“ćØć«ć—ć¾ć™ć€‚ćƒćƒ¼ć‚øćƒ§ćƒ³ćÆ仄äø‹ć§ć€172.17.0.2ć§å‹•ä½œć—ć¦ć„ć‚‹ć‚‚ć®ćØć—ć¾ć™ć€‚

$ mysql --version
mysql  Ver 8.0.28 for Linux on x86_64 (MySQL Community Server - GPL)

ęŗ–å‚™

恧ćÆ态Spring Boot惗惭ć‚ø悧ć‚Æćƒˆć‚’ä½œęˆć—ć¦ć„ćć¾ć™ć€‚

依存関äæ‚恫batch态validation态mysqlć‚’ęŒ‡å®šć—ć¦ć€ä½œęˆć€‚

$ curl -s https://start.spring.io/starter.tgz \
  -d bootVersion=2.6.7 \
  -d javaVersion=17 \
  -d name=batch-beanvalidation \
  -d groupId=org.littlewings \
  -d artifactId=batch-beanvalidation \
  -d version=0.0.1-SNAPSHOT \
  -d packageName=org.littlewings.spring.batch \
  -d dependencies=batch,validation,mysql \
  -d baseDir=batch-beanvalidation | tar zxvf -

惗惭ć‚ø悧ć‚Æćƒˆå†…ć«ē§»å‹•ć€‚

$ cd batch-beanvalidation

č‡Ŗ動ē”Ÿęˆć•ć‚ŒćŸć‚½ćƒ¼ć‚¹ć‚³ćƒ¼ćƒ‰ćÆć€å‰Šé™¤ć—ć¦ćŠćć¾ć™ć€‚

$ rm src/main/java/org/littlewings/spring/batch/BatchBeanvalidationApplication.java src/test/java/org/littlewings/spring/batch/BatchBeanvalidationApplicationTests.java

Mavenć®ä¾å­˜é–¢äæ‚ćŠć‚ˆć³ćƒ—ćƒ©ć‚°ć‚¤ćƒ³ć®čح定ćÆ态恓恔悉怂

 <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-batch</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.batch</groupId>
            <artifactId>spring-batch-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

ē¶šć„ć¦ć€ć‚½ćƒ¼ć‚¹ć‚³ćƒ¼ćƒ‰ć‚’ä½œęˆć—ć¦ć„ćć¾ć™ć€‚

Itemē›øå½“ć®ć‚Æćƒ©ć‚¹ć€‚

src/main/java/org/littlewings/spring/batch/Book.java

package org.littlewings.spring.batch;

import javax.validation.constraints.Min;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;

public class Book {
    @Size(min = 14, max = 14)
    @Pattern(regexp = "\\d{3}-\\d{10}")
    String isbn;

    @Size(max = 200)
    String title;

    @Min(1000)
    Integer price;

    // getterļ¼setterćÆēœē•„
}

謎恫态1,000円仄äøŠć®ęœ¬ć—ć‹å—ć‘ä»˜ć‘ćŖć„ć‚ˆć†ć«ćŖć£ć¦ć„ć¾ć™ć€‚

Jobć®å®šē¾©ć€‚

src/main/java/org/littlewings/spring/batch/BeanValidationJobConfig.java

package org.littlewings.spring.batch;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.step.skip.AlwaysSkipItemSkipPolicy;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;
import org.springframework.batch.item.validator.BeanValidatingItemProcessor;
import org.springframework.batch.item.validator.ValidationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

@Configuration
public class BeanValidationJobConfig {
    @Autowired
    JobBuilderFactory jobBuilderFactory;

    @Autowired
    StepBuilderFactory stepBuilderFactory;

    @Autowired
    LocalValidatorFactoryBean localValidatorFactoryBean;

    @Bean
    public Job beanValidationJob() {
        return jobBuilderFactory
                .get("beanValidationJob")
                .incrementer(new RunIdIncrementer())
                // å¾Œć§
                .build();
    }

    // å¾Œć§

    @StepScope
    @Bean
    public FlatFileItemReader<Book> flatFileItemReader(@Value("#{jobParameters['filePath']}") String filePath) {
        Resource fileResource = new FileSystemResource(filePath);

        return new FlatFileItemReaderBuilder<Book>()
                .name("flatFileItemReader")
                .resource(fileResource)
                .encoding("UTF-8")
                .delimited()
                .names(new String[]{"isbn", "title", "price"})
                .linesToSkip(1)
                .targetType(Book.class)
                .saveState(false)
                .build();
    }

    // å¾Œć§

    @StepScope
    @Bean
    public ItemWriter<Book> consoleItemWriter() {
        Logger logger = LoggerFactory.getLogger("consoleItemWriter");

        return books -> books.forEach(book ->
                logger.info("[writer] isbn = {}, title = {}, price = {}", book.getIsbn(), book.getTitle(), book.getPrice())
        );
    }
}

Jobć«å«ć¾ć‚Œć‚‹Stepć®ę§‹ęˆć‚„

    @Bean
    public Job beanValidationJob() {
        return jobBuilderFactory
                .get("beanValidationJob")
                .incrementer(new RunIdIncrementer())
                // å¾Œć§
                .build();
    }

    // å¾Œć§

Bean Validation悒ä½æ恆ItemProcessor恮čؘčæ°ćÆå¾Œå›žć—ć«ć—ć¦ć„ć¾ć™ć€‚

    // å¾Œć§

ćŖćŠć€ćƒ•ć‚”ć‚¤ćƒ«ć®čŖ­ćæč¾¼ćæćÆFlatFileItemReaderBuilderć§č”Œć„ć€

    @StepScope
    @Bean
    public FlatFileItemReader<Book> flatFileItemReader(@Value("#{jobParameters['filePath']}") String filePath) {
        Resource fileResource = new FileSystemResource(filePath);

        return new FlatFileItemReaderBuilder<Book>()
                .name("flatFileItemReader")
                .resource(fileResource)
                .encoding("UTF-8")
                .delimited()
                .names(new String[]{"isbn", "title", "price"})
                .linesToSkip(1)
                .targetType(Book.class)
                .saveState(false)
                .build();
    }

ItemWriterćÆęø”ć£ć¦ććŸćƒćƒ£ćƒ³ć‚Æ悒ęؙęŗ–å‡ŗåŠ›ć«ę›ø恍å‡ŗć™ć‚ˆć†ć«ä½œęˆć€‚

    @StepScope
    @Bean
    public ItemWriter<Book> consoleItemWriter() {
        Logger logger = LoggerFactory.getLogger("consoleItemWriter");

        return books -> books.forEach(book ->
                logger.info("[writer] isbn = {}, title = {}, price = {}", book.getIsbn(), book.getTitle(), book.getPrice())
        );
    }

mainć‚Æćƒ©ć‚¹ć€‚

src/main/java/org/littlewings/spring/batch/App.java

package org.littlewings.spring.batch;

import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@EnableBatchProcessing
public class App {
    public static void main(String... args) {
        SpringApplication.run(App.class, args);
    }
}

čح定ćÆ态恓悓ćŖę„Ÿć˜ć«ć—ć¦ćŠćć¾ć™ć€‚

src/main/resources/application.properties

spring.datasource.url=jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=UTF-8
spring.datasource.username=kazuhira
spring.datasource.password=password

spring.batch.jdbc.initialize-schema=always

CSVć‚‚ä½œęˆć—ć¾ć—ć‚‡ć†ć€‚

ć¾ćšćÆ态å…ØItemćŒå•é”ŒćŖć„ćƒ‡ćƒ¼ć‚æ恮CSV怂

src/main/resources/book.csv

isbn,title,price
978-4798142470,Springå¾¹åŗ•å…„門 Spring Frameworkć«ć‚ˆć‚‹Javać‚¢ćƒ—ćƒŖć‚±ćƒ¼ć‚·ćƒ§ćƒ³é–‹ē™ŗ,4400
978-4774182179,[ę”¹čØ‚ę–°ē‰ˆ]Springå…„é–€ ā€•ā€•Javaćƒ•ćƒ¬ćƒ¼ćƒ ćƒÆćƒ¼ć‚Æćƒ»ć‚ˆć‚Šč‰Æ恄čØ­č؈ćØć‚¢ćƒ¼ć‚­ćƒ†ć‚Æćƒćƒ£,4180
978-1492076988,Spring Boot: Up and Running: Building Cloud Native Java and Kotlin Applications,6265
978-4295000198,ć‚„ć•ć—ćå­¦ć¹ć‚‹MySQL運ē”Ø惻ē®”ē†å…„門怐5.7åƾåæœć€‘,2860
978-1484237236,The Definitive Guide to Spring Batch: Modern Finite Batch Processing in the Cloud,7361
978-4798147406,č©³č§£MySQL 5.7 ę­¢ć¾ć‚‰ć¬é€²åŒ–ć«ä¹—ć‚Šé…ć‚ŒćŖć„ćŸć‚ć®ćƒ†ć‚Æćƒ‹ć‚«ćƒ«ć‚¬ć‚¤ćƒ‰ (NEXT ONE),3960
978-4798161488,MySQLå¾¹åŗ•å…„門 ē¬¬4ē‰ˆ MySQL 8.0åƾåæœ,4180
978-4797393118,åŸŗē¤Žć‹ć‚‰ć®MySQL ē¬¬3ē‰ˆ (åŸŗē¤Žć‹ć‚‰ć‚·ćƒŖćƒ¼ć‚ŗ),6038
978-4873116389,å®Ÿč·µćƒć‚¤ćƒ‘ćƒ•ć‚©ćƒ¼ćƒžćƒ³ć‚¹MySQL ē¬¬3ē‰ˆ,5280
978-4774170206,MariaDB&MySQLå…Øę©Ÿčƒ½ćƒć‚¤ćƒ–ćƒ«,3850

å…Ø10ä»¶ć‚ć‚Šć€ćƒ˜ćƒƒćƒ€ćƒ¼å…„ć‚Šć§ć™ć€‚

ꬔ恫态ćØć“ć‚ć©ć“ć‚ćŠć‹ć—ćŖćƒ‡ćƒ¼ć‚æćŒå…„ć£ćŸCSV怂

src/main/resources/book_invalid.csv

isbn,title,price
978-4798142470,Springå¾¹åŗ•å…„門 Spring Frameworkć«ć‚ˆć‚‹Javać‚¢ćƒ—ćƒŖć‚±ćƒ¼ć‚·ćƒ§ćƒ³é–‹ē™ŗ,4400
xx-xxxxxxx,[ę”¹čØ‚ę–°ē‰ˆ]Springå…„é–€ ā€•ā€•Javaćƒ•ćƒ¬ćƒ¼ćƒ ćƒÆćƒ¼ć‚Æćƒ»ć‚ˆć‚Šč‰Æ恄čØ­č؈ćØć‚¢ćƒ¼ć‚­ćƒ†ć‚Æćƒćƒ£,4180
978-1492076988,Spring Boot: Up and Running: Building Cloud Native Java and Kotlin Applications,6265
978-4295000198,ć‚„ć•ć—ćå­¦ć¹ć‚‹MySQL運ē”Ø惻ē®”ē†å…„門怐5.7åƾåæœć€‘,860
978-1484237236,The Definitive Guide to Spring Batch: Modern Finite Batch Processing in the Cloud,7361
978-4798147406,č©³č§£MySQL 5.7 ę­¢ć¾ć‚‰ć¬é€²åŒ–ć«ä¹—ć‚Šé…ć‚ŒćŖć„ćŸć‚ć®ćƒ†ć‚Æćƒ‹ć‚«ćƒ«ć‚¬ć‚¤ćƒ‰ (NEXT ONE),960
978-4798161488,MySQLå¾¹åŗ•å…„門 ē¬¬4ē‰ˆ MySQL 8.0åƾåæœ,4180
978-4797393118,åŸŗē¤Žć‹ć‚‰ć®MySQL ē¬¬3ē‰ˆ (åŸŗē¤Žć‹ć‚‰ć‚·ćƒŖćƒ¼ć‚ŗ),6038
978-4873116389,å®Ÿč·µćƒć‚¤ćƒ‘ćƒ•ć‚©ćƒ¼ćƒžćƒ³ć‚¹MySQL ē¬¬3ē‰ˆ,5280
978-4774170206,MariaDB&MySQLå…Øę©Ÿčƒ½ćƒć‚¤ćƒ–ćƒ«,850

4件态ć‚Øćƒ©ćƒ¼ć«ćŖć‚‹ćƒ‡ćƒ¼ć‚æļ¼ˆISBNć®å½¢å¼čŖ¤ć‚Šć€ä¾”ę ¼ćŒ1,000円ęœŖęŗ€ļ¼‰ćŒå…„ć£ć¦ć„ć¾ć™ć€‚

ć“ć“ć¾ć§ć§ć€ęŗ–å‚™ćÆ完äŗ†ć§ć™ć€‚

BeanValidatingItemProcessor悒ä½æ恆

恧ćÆć€ä½œęˆć—ćŸJob恫Bean Validationć‚’č”Œć†ć‚ˆć†ć«Stepć‚’ę§‹ęˆć—ć¦ć„ćć¾ć—ć‚‡ć†ć€‚

恓恓恋悉ćÆć€å…ˆć»ć©ć€Œ// å¾Œć§ć€ćØć‚³ćƒ”ćƒ³ćƒˆć‚’ę›øć„ć¦ć„ćŸéƒØåˆ†ć‚’åŸ‹ć‚ćŸć‚Šć€å¤‰ę›“ć—ć¦ć„ć£ćŸć‚Šć—ć¾ć™ć€‚

ęœ€åˆćÆ态惐ćƒŖćƒ‡ćƒ¼ć‚·ćƒ§ćƒ³ćŖ恗恧

ć¾ćšćÆ态Bean Validationć‚’č”Œć‚ćŖć„ć‚ˆć†ć«Stepć‚’ę§‹ęˆć—ć¾ć™ć€‚

Stepć®å®šē¾©ć€‚

    @Bean
    public Step noBeanValidationStep() {
        return stepBuilderFactory
                .get("withBeanValidationStep")
                .<Book, Book>chunk(3)
                .reader(flatFileItemReader(null))
                .writer(consoleItemWriter())
                .build();
    }

Jobć®å®šē¾©ć€‚

    @Bean
    public Job beanValidationJob() {
        return jobBuilderFactory
                .get("beanValidationJob")
                .incrementer(new RunIdIncrementer())
                .start(noBeanValidationStep())
                .build();
    }

ć“ć‚Œć§ćƒ‘ćƒƒć‚±ćƒ¼ć‚øćƒ³ć‚°ć—ć¦

$ mvn package

å®Ÿč”Œć€‚ć¾ćšćÆć€ę­£ć—ć„ćƒ‡ćƒ¼ć‚æćŒå…„ć£ćŸCSV悒čŖ­ćæč¾¼ć›ć¦ćæć¾ć™ć€‚

$ java -Dspring.batch.job.names=beanValidationJob -jar target/batch-beanvalidation-0.0.1-SNAPSHOT.jar filePath=src/main/resources/book.csv

ēµęžœć€‚

2022-04-28 01:37:52.114  INFO 17646 --- [           main] o.s.b.a.b.JobLauncherApplicationRunner   : Running default command line with: [filePath=src/main/resources/book.csv]
2022-04-28 01:37:52.291  INFO 17646 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=beanValidationJob]] launched with the following parameters: [{run.id=5, filePath=src/main/resources/book.csv}]
2022-04-28 01:37:52.394  INFO 17646 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [withBeanValidationStep]
2022-04-28 01:37:52.510  INFO 17646 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4798142470, title = Springå¾¹åŗ•å…„門 Spring Frameworkć«ć‚ˆć‚‹Javać‚¢ćƒ—ćƒŖć‚±ćƒ¼ć‚·ćƒ§ćƒ³é–‹ē™ŗ, price = 4400
2022-04-28 01:37:52.511  INFO 17646 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4774182179, title = [ę”¹čØ‚ę–°ē‰ˆ]Springå…„é–€ ā€•ā€•Javaćƒ•ćƒ¬ćƒ¼ćƒ ćƒÆćƒ¼ć‚Æćƒ»ć‚ˆć‚Šč‰Æ恄čØ­č؈ćØć‚¢ćƒ¼ć‚­ćƒ†ć‚Æćƒćƒ£, price = 4180
2022-04-28 01:37:52.511  INFO 17646 --- [           main] consoleItemWriter                        : [writer] isbn = 978-1492076988, title = Spring Boot: Up and Running: Building Cloud Native Java and Kotlin Applications, price = 6265
2022-04-28 01:37:52.540  INFO 17646 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4295000198, title = ć‚„ć•ć—ćå­¦ć¹ć‚‹MySQL運ē”Ø惻ē®”ē†å…„門怐5.7åƾåæœć€‘, price = 2860
2022-04-28 01:37:52.540  INFO 17646 --- [           main] consoleItemWriter                        : [writer] isbn = 978-1484237236, title = The Definitive Guide to Spring Batch: Modern Finite Batch Processing in the Cloud, price = 7361
2022-04-28 01:37:52.540  INFO 17646 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4798147406, title = č©³č§£MySQL 5.7 ę­¢ć¾ć‚‰ć¬é€²åŒ–ć«ä¹—ć‚Šé… 悌ćŖć„ćŸć‚ć®ćƒ†ć‚Æćƒ‹ć‚«ćƒ«ć‚¬ć‚¤ćƒ‰ (NEXT ONE), price = 3960
2022-04-28 01:37:52.566  INFO 17646 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4798161488, title = MySQLå¾¹åŗ•å…„門 ē¬¬4ē‰ˆ MySQL 8.0åƾåæœ, price = 4180
2022-04-28 01:37:52.566  INFO 17646 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4797393118, title = åŸŗē¤Žć‹ć‚‰ć®MySQL ē¬¬3ē‰ˆ (åŸŗē¤Žć‹ć‚‰ć‚·ćƒŖćƒ¼ć‚ŗ), price = 6038
2022-04-28 01:37:52.567  INFO 17646 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4873116389, title = å®Ÿč·µćƒć‚¤ćƒ‘ćƒ•ć‚©ćƒ¼ćƒžćƒ³ć‚¹MySQL ē¬¬3ē‰ˆ, price = 5280
2022-04-28 01:37:52.594  INFO 17646 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4774170206, title = MariaDB&MySQLå…Øę©Ÿčƒ½ćƒć‚¤ćƒ–ćƒ«, price = 3850
2022-04-28 01:37:52.621  INFO 17646 --- [           main] o.s.batch.core.step.AbstractStep         : Step: [withBeanValidationStep] executed in 226ms
2022-04-28 01:37:52.675  INFO 17646 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=beanValidationJob]] completed with the following parameters: [{run.id=5, filePath=src/main/resources/book.csv}] and the following status: [COMPLETED] in 339ms

ꬔ恫态ć‚Øćƒ©ćƒ¼ć«ćŖ悋CSV悒čŖ­ćæč¾¼ć¾ć›ć¦ćæć¾ć™ć€‚

$ java -Dspring.batch.job.names=beanValidationJob -jar target/batch-beanvalidation-0.0.1-SNAPSHOT.jar filePath=src/main/resources/book_invalid.csv

恧恙恌态恝悂恝悂Bean Validationć‚’ć¾ć å…„ć‚Œć¦ć„ćŖ恄恮恧å…Øä»¶é€šéŽć—ć¾ć™ć€‚

2022-04-28 01:38:43.458  INFO 17727 --- [           main] o.s.b.a.b.JobLauncherApplicationRunner   : Running default command line with: [filePath=src/main/resources/book_invali
d.csv]
2022-04-28 01:38:43.638  INFO 17727 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=beanValidationJob]] launched with the following parameters: [{run.id=6, filePath=src/main/resources/book_invalid.csv}]
2022-04-28 01:38:43.768  INFO 17727 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [withBeanValidationStep]
2022-04-28 01:38:43.890  INFO 17727 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4798142470, title = Springå¾¹åŗ•å…„門 Spring Frameworkć«ć‚ˆć‚‹Javać‚¢ćƒ—ćƒŖć‚±ćƒ¼ć‚·ćƒ§ćƒ³é–‹ē™ŗ, price = 4400
2022-04-28 01:38:43.890  INFO 17727 --- [           main] consoleItemWriter                        : [writer] isbn = xx-xxxxxxx, title = [ę”¹čØ‚ę–°ē‰ˆ]Springå…„é–€ ā€•ā€•Javaćƒ•ćƒ¬ćƒ¼ćƒ ćƒÆćƒ¼ć‚Æćƒ»ć‚ˆć‚Šč‰Æ恄čØ­č؈ćØć‚¢ćƒ¼ć‚­ćƒ†ć‚Æćƒćƒ£, price = 4180
2022-04-28 01:38:43.890  INFO 17727 --- [           main] consoleItemWriter                        : [writer] isbn = 978-1492076988, title = Spring Boot: Up and Running: Building Cloud Native Java and Kotlin Applications, price = 6265
2022-04-28 01:38:43.920  INFO 17727 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4295000198, title = ć‚„ć•ć—ćå­¦ć¹ć‚‹MySQL運ē”Ø惻ē®”ē†å…„門怐5.7åƾåæœć€‘, price = 860
2022-04-28 01:38:43.921  INFO 17727 --- [           main] consoleItemWriter                        : [writer] isbn = 978-1484237236, title = The Definitive Guide to Spring Batch: Modern Finite Batch Processing in the Cloud, price = 7361
2022-04-28 01:38:43.921  INFO 17727 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4798147406, title = č©³č§£MySQL 5.7 ę­¢ć¾ć‚‰ć¬é€²åŒ–ć«ä¹—ć‚Šé… 悌ćŖć„ćŸć‚ć®ćƒ†ć‚Æćƒ‹ć‚«ćƒ«ć‚¬ć‚¤ćƒ‰ (NEXT ONE), price = 960
2022-04-28 01:38:43.948  INFO 17727 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4798161488, title = MySQLå¾¹åŗ•å…„門 ē¬¬4ē‰ˆ MySQL 8.0åƾåæœ, price = 4180
2022-04-28 01:38:43.948  INFO 17727 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4797393118, title = åŸŗē¤Žć‹ć‚‰ć®MySQL ē¬¬3ē‰ˆ (åŸŗē¤Žć‹ć‚‰ć‚·ćƒŖćƒ¼ć‚ŗ), price = 6038
2022-04-28 01:38:43.948  INFO 17727 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4873116389, title = å®Ÿč·µćƒć‚¤ćƒ‘ćƒ•ć‚©ćƒ¼ćƒžćƒ³ć‚¹MySQL ē¬¬3ē‰ˆ, price = 5280
2022-04-28 01:38:43.975  INFO 17727 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4774170206, title = MariaDB&MySQLå…Øę©Ÿčƒ½ćƒć‚¤ćƒ–ćƒ«, price = 850
2022-04-28 01:38:44.002  INFO 17727 --- [           main] o.s.batch.core.step.AbstractStep         : Step: [withBeanValidationStep] executed in 234ms
2022-04-28 01:38:44.061  INFO 17727 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=beanValidationJob]] completed with the following parameters: [{run.id=6, filePath=src/main/resources/book_invalid.csv}] and the following status: [COMPLETED] in 373ms
BeanValidatingItemProcessor悒čæ½åŠ ć™ć‚‹

恧ćÆ态BeanValidatingItemProcessor悒čæ½åŠ ć—恦ćæć¾ć—ć‚‡ć†ć€‚

Item processing / Validating Input

仄äø‹ć®å®šē¾©ć‚’čæ½åŠ ć—ć¾ć™ć€‚

    @StepScope
    @Bean
    public BeanValidatingItemProcessor<Book> beanValidatingItemProcessor() {
        return new BeanValidatingItemProcessor<>(localValidatorFactoryBean);
    }

ć‚³ćƒ³ć‚¹ćƒˆćƒ©ć‚Æć‚æć§ęŒ‡å®šć—ć¦ć„ć‚‹LocalValidatorFactoryBeanćÆ态Spring Boot恮Auto Configurationć§ć‚»ćƒƒćƒˆć‚¢ćƒƒćƒ—ć•ć‚ŒćŸć‚‚ć®ć§ć™ć€‚
ęŒ‡å®šć—ćŖćć¦ć‚‚å†…éƒØēš„恫LocalValidatorFactoryBean悒ē”Ÿęˆć™ć‚‹ć®ć§ć™ćŒć€ć›ć£ć‹ććŖ悉Spring Bootć§ć‚»ćƒƒćƒˆć‚¢ćƒƒćƒ—ć•ć‚ŒćŸć‚‚ć®ć‚’
ä½æć£ćŸę–¹ćŒć‚ˆć„ć‹ćŖćØę€ć„ć¾ć™ć€‚

Step定ē¾©ćÆ仄äø‹ć®ć‚ˆć†ć«ItemProcessor悒čæ½åŠ ć—恦

    @Bean
    public Step withBeanValidationStep() {
        return stepBuilderFactory
                .get("withBeanValidationStep")
                .<Book, Book>chunk(3)
                .reader(flatFileItemReader(null))
                .processor(beanValidatingItemProcessor())
                .writer(consoleItemWriter())
                .build();
    }

Jobć‚’å¤‰ę›“ć€‚

    @Bean
    public Job beanValidationJob() {
        return jobBuilderFactory
                .get("beanValidationJob")
                .incrementer(new RunIdIncrementer())
                .start(withBeanValidationStep())
                .build();
    }

ćƒ‘ćƒƒć‚±ćƒ¼ć‚øćƒ³ć‚°ć—ć¦å®Ÿč”Œć€‚

$ mvn package
$ java -Dspring.batch.job.names=beanValidationJob -jar target/batch-beanvalidation-0.0.1-SNAPSHOT.jar filePath=src/main/resources/book.csv

ć‚Øćƒ©ćƒ¼ć«ćŖ悉ćŖć„ę–¹ć®ēµęžœćÆ态ēœē•„ć—ć¾ć™ć€‚

ē¶šć„恦态ć‚Øćƒ©ćƒ¼ć«ćŖć‚‹ę–¹ć®CSV悒čŖ­ćæč¾¼ć¾ć›ć¾ć™ć€‚

$ java -Dspring.batch.job.names=beanValidationJob -jar target/batch-beanvalidation-0.0.1-SNAPSHOT.jar filePath=src/main/resources/book_invalid.csv

恙悋ćØ态ć‚Øćƒ©ćƒ¼ć«ćŖ悋Itemć«é­é‡ć—ćŸę™‚ē‚¹ć§ä¾‹å¤–ć‚’ć‚¹ćƒ­ćƒ¼ć—ć¦ć‚¢ćƒ—ćƒŖć‚±ćƒ¼ć‚·ćƒ§ćƒ³ćŒēµ‚äŗ†ć—ć¾ć™ć€‚

2022-04-28 01:42:46.217  INFO 17951 --- [           main] o.s.b.a.b.JobLauncherApplicationRunner   : Running default command line with: [filePath=src/main/resources/book_invalid.csv]
2022-04-28 01:42:46.432  INFO 17951 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=beanValidationJob]] launched with the following parameters: [{run.id=8, filePath=src/main/resources/book_invalid.csv}]
2022-04-28 01:42:46.530  INFO 17951 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [withBeanValidationStep]
2022-04-28 01:42:46.718 ERROR 17951 --- [           main] o.s.batch.core.step.AbstractStep         : Encountered an error executing step withBeanValidationStep in job beanValidationJob

org.springframework.batch.item.validator.ValidationException: Validation failed for org.littlewings.spring.batch.Book@11dee337:
Field error in object 'item' on field 'isbn': rejected value [xx-xxxxxxx]; codes [Pattern.item.isbn,Pattern.isbn,Pattern.java.lang.String,Pattern]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.isbn,isbn]; arguments []; default message [isbn],[Ljavax.validation.constraints.Pattern$Flag;@532a02d9,\d{3}-\d{10}]; default message [ę­£č¦č”Øē¾ "\d{3}-\d{10}" ć«ćƒžćƒƒćƒć•ć›ć¦ćć ć•ć„]
Field error in object 'item' on field 'isbn': rejected value [xx-xxxxxxx]; codes [Size.item.isbn,Size.isbn,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.isbn,isbn]; arguments []; default message [isbn],14,14]; default message [14 恋悉 14 恮間恮悵悤ć‚ŗć«ć—ć¦ćć ć•ć„]
        at org.springframework.batch.item.validator.SpringValidator.validate(SpringValidator.java:54) ~[spring-batch-infrastructure-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.item.validator.ValidatingItemProcessor.process(ValidatingItemProcessor.java:84) ~[spring-batch-infrastructure-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.item.validator.ValidatingItemProcessor$$FastClassBySpringCGLIB$$39980290.invoke(<generated>) ~[spring-batch-infrastructure-4.3.5.jar!/:4.3.5]
        at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:793) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.support.DelegatingIntroductionInterceptor.doProceed(DelegatingIntroductionInterceptor.java:137) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.support.DelegatingIntroductionInterceptor.invoke(DelegatingIntroductionInterceptor.java:124) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:708) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.batch.item.validator.BeanValidatingItemProcessor$$EnhancerBySpringCGLIB$$b5c9b6d8.process(<generated>) ~[spring-batch-infrastructure-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.item.SimpleChunkProcessor.doProcess(SimpleChunkProcessor.java:134) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.item.SimpleChunkProcessor.transform(SimpleChunkProcessor.java:319) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.item.SimpleChunkProcessor.process(SimpleChunkProcessor.java:210) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.item.ChunkOrientedTasklet.execute(ChunkOrientedTasklet.java:77) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.tasklet.TaskletStep$ChunkTransactionCallback.doInTransaction(TaskletStep.java:407) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.tasklet.TaskletStep$ChunkTransactionCallback.doInTransaction(TaskletStep.java:331) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:140) ~[spring-tx-5.3.19.jar!/:5.3.19]
        at org.springframework.batch.core.step.tasklet.TaskletStep$2.doInChunkContext(TaskletStep.java:273) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.scope.context.StepContextRepeatCallback.doInIteration(StepContextRepeatCallback.java:82) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.repeat.support.RepeatTemplate.getNextResult(RepeatTemplate.java:375) ~[spring-batch-infrastructure-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.repeat.support.RepeatTemplate.executeInternal(RepeatTemplate.java:215) ~[spring-batch-infrastructure-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.repeat.support.RepeatTemplate.iterate(RepeatTemplate.java:145) ~[spring-batch-infrastructure-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.tasklet.TaskletStep.doExecute(TaskletStep.java:258) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.AbstractStep.execute(AbstractStep.java:208) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.job.SimpleStepHandler.handleStep(SimpleStepHandler.java:152) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.job.AbstractJob.handleStep(AbstractJob.java:413) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.job.SimpleJob.doExecute(SimpleJob.java:136) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.job.AbstractJob.execute(AbstractJob.java:320) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.launch.support.SimpleJobLauncher$1.run(SimpleJobLauncher.java:149) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.core.task.SyncTaskExecutor.execute(SyncTaskExecutor.java:50) ~[spring-core-5.3.19.jar!/:5.3.19]
        at org.springframework.batch.core.launch.support.SimpleJobLauncher.run(SimpleJobLauncher.java:140) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
        at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
        at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:198) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.batch.core.configuration.annotation.SimpleBatchConfiguration$PassthruAdvice.invoke(SimpleBatchConfiguration.java:128) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at jdk.proxy2/jdk.proxy2.$Proxy58.run(Unknown Source) ~[na:na]
        at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.execute(JobLauncherApplicationRunner.java:199) ~[spring-boot-autoconfigure-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.executeLocalJobs(JobLauncherApplicationRunner.java:173) ~[spring-boot-autoconfigure-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.launchJobFromProperties(JobLauncherApplicationRunner.java:160) ~[spring-boot-autoconfigure-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.run(JobLauncherApplicationRunner.java:155) ~[spring-boot-autoconfigure-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.run(JobLauncherApplicationRunner.java:150) ~[spring-boot-autoconfigure-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:768) ~[spring-boot-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:758) ~[spring-boot-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:310) ~[spring-boot-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1312) ~[spring-boot-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1301) ~[spring-boot-2.6.7.jar!/:2.6.7]
        at org.littlewings.spring.batch.App.main(App.java:11) ~[classes!/:0.0.1-SNAPSHOT]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
        at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
        at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:49) ~[batch-beanvalidation-0.0.1-SNAPSHOT.jar:0.0.1-SNAPSHOT]
        at org.springframework.boot.loader.Launcher.launch(Launcher.java:108) ~[batch-beanvalidation-0.0.1-SNAPSHOT.jar:0.0.1-SNAPSHOT]
        at org.springframework.boot.loader.Launcher.launch(Launcher.java:58) ~[batch-beanvalidation-0.0.1-SNAPSHOT.jar:0.0.1-SNAPSHOT]
        at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:88) ~[batch-beanvalidation-0.0.1-SNAPSHOT.jar:0.0.1-SNAPSHOT]
Caused by: org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 2 errors
Field error in object 'item' on field 'isbn': rejected value [xx-xxxxxxx]; codes [Pattern.item.isbn,Pattern.isbn,Pattern.java.lang.String,Pattern]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.isbn,isbn]; arguments []; default message [isbn],[Ljavax.validation.constraints.Pattern$Flag;@532a02d9,\d{3}-\d{10}]; default message [ę­£č¦č”Øē¾ "\d{3}-\d{10}" ć«ćƒžćƒƒćƒć•ć›ć¦ćć ć•ć„]
Field error in object 'item' on field 'isbn': rejected value [xx-xxxxxxx]; codes [Size.item.isbn,Size.isbn,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.isbn,isbn]; arguments []; default message [isbn],14,14]; default message [14 恋悉 14 恮間恮悵悤ć‚ŗć«ć—ć¦ćć ć•ć„]
        ... 64 common frames omitted

2022-04-28 01:42:46.722  INFO 17951 --- [           main] o.s.batch.core.step.AbstractStep         : Step: [withBeanValidationStep] executed in 192ms
2022-04-28 01:42:46.778  INFO 17951 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=beanValidationJob]] completed with the following parameters: [{run.id=8, filePath=src/main/resources/book_invalid.csv}] and the following status: [FAILED] in 303ms

FAILED恫ćŖć£ć¦ć—ć¾ć„ć¾ć—ćŸć€‚

ć‚Øćƒ©ćƒ¼ć«ćŖć£ćŸItem悒čŖ­ćæé£›ć°ć™

ꬔ恫态ć‚Øćƒ©ćƒ¼ć«ćŖć£ćŸItemć‚’ć‚¹ć‚­ćƒƒćƒ—ć™ć‚‹ć‚ˆć†ć«čØ­å®šć—ć¦ćæć¾ć—ć‚‡ć†ć€‚

Configuring a Step / Chunk-oriented Processing / Configuring Skip Logic

faultTolerant悒ä½æć„ć€ę¬”ć«ć‚¹ć‚­ćƒƒćƒ—ć™ć‚‹ä¾‹å¤–ļ¼ˆskipļ¼‰ćØć‚¹ć‚­ćƒƒćƒ—åÆčƒ½ćŖꕰļ¼ˆskipLimitļ¼‰ć‚’ęŒ‡å®šć—ć¾ć™ć€‚

    @Bean
    public Step faultTolerantBeanValidationStep() {
        return stepBuilderFactory
                .get("withBeanValidationStep")
                .<Book, Book>chunk(3)
                .reader(flatFileItemReader(null))
                .processor(beanValidatingItemProcessor())
                .writer(consoleItemWriter())
                .faultTolerant()
                .skip(ValidationException.class)
                .skipLimit(5)
                .build();
    }

skipLimitćÆ态0ć‚ˆć‚Šå¤§ććŖå€¤ć‚’ęŒ‡å®šć™ć‚‹åæ…č¦ćŒć‚ć‚Šć¾ć™ć€‚ē„”åˆ¶é™ć€ćæ恟恄ćŖ恓ćØćÆ恧恍ćŖ恕恝恆恧恙怂

Job定ē¾©ć‚‚å¤‰ę›“怂

    @Bean
    public Job beanValidationJob() {
        return jobBuilderFactory
                .get("beanValidationJob")
                .incrementer(new RunIdIncrementer())
                .start(faultTolerantBeanValidationStep())
                .build();
    }

ćƒ‘ćƒƒć‚±ćƒ¼ć‚øćƒ³ć‚°ć—ć¦ć€å®Ÿč”Œć€‚ć“ć“ć‹ć‚‰å…ˆćÆ态ć‚Øćƒ©ćƒ¼ć«ćŖć‚‹ćƒ•ć‚”ć‚¤ćƒ«ć®ćæčŖ­ćæč¾¼ć¾ć›ć¾ć™ć€‚

$ mvn package
$ java -Dspring.batch.job.names=beanValidationJob -jar target/batch-beanvalidation-0.0.1-SNAPSHOT.jar filePath=src/main/resources/book_invalid.csv

惭悰怂

2022-04-28 01:48:14.347  INFO 18194 --- [           main] org.littlewings.spring.batch.App         : Started App in 1.941 seconds (JVM running for 2.392)
2022-04-28 01:48:14.348  INFO 18194 --- [           main] o.s.b.a.b.JobLauncherApplicationRunner   : Running default command line with: [filePath=src/main/resources/book_invalid.csv]
2022-04-28 01:48:14.522  INFO 18194 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=beanValidationJob]] launched with the following parameters: [{run.id=9, filePath=src/main/resources/book_invalid.csv}]
2022-04-28 01:48:14.649  INFO 18194 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [withBeanValidationStep]
2022-04-28 01:48:14.849  INFO 18194 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4798142470, title = Springå¾¹åŗ•å…„門 Spring Frameworkć«ć‚ˆć‚‹Javać‚¢ćƒ—ćƒŖć‚±ćƒ¼ć‚·ćƒ§ćƒ³é–‹ē™ŗ, price = 4400
2022-04-28 01:48:14.849  INFO 18194 --- [           main] consoleItemWriter                        : [writer] isbn = 978-1492076988, title = Spring Boot: Up and Running: Building Cloud Native Java and Kotlin Applications, price = 6265
2022-04-28 01:48:14.883  INFO 18194 --- [           main] consoleItemWriter                        : [writer] isbn = 978-1484237236, title = The Definitive Guide to Spring Batch: Modern Finite Batch Processing in the Cloud, price = 7361
2022-04-28 01:48:14.911  INFO 18194 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4798161488, title = MySQLå¾¹åŗ•å…„門 ē¬¬4ē‰ˆ MySQL 8.0åƾåæœ, price = 4180
2022-04-28 01:48:14.911  INFO 18194 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4797393118, title = åŸŗē¤Žć‹ć‚‰ć®MySQL ē¬¬3ē‰ˆ (åŸŗē¤Žć‹ć‚‰ć‚·ćƒŖćƒ¼ć‚ŗ), price = 6038
2022-04-28 01:48:14.911  INFO 18194 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4873116389, title = å®Ÿč·µćƒć‚¤ćƒ‘ćƒ•ć‚©ćƒ¼ćƒžćƒ³ć‚¹MySQL ē¬¬3ē‰ˆ, price = 5280
2022-04-28 01:48:14.965  INFO 18194 --- [           main] o.s.batch.core.step.AbstractStep         : Step: [withBeanValidationStep] executed in 316ms
2022-04-28 01:48:15.019  INFO 18194 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=beanValidationJob]] completed with the following parameters: [{run.id=9, filePath=src/main/resources/book_invalid.csv}] and the following status: [COMPLETED] in 448ms

今åŗ¦ćÆć€ę­£åøø恫ēµ‚äŗ†ć™ć‚‹ć‚ˆć†ć«ćŖć‚Šć¾ć—ćŸć€‚

ItemWriterćÆćć®ć¾ć¾å‡¦ē†ć•ć‚ŒćŸItem恮ćæ恌å‡ŗåŠ›ć•ć‚Œć€ć“ć”ć‚‰ćÆęƒ³å®šé€šć‚Šć§ć™ćŒć€ć©ć®ć‚ˆć†ćŖItem恌ć‚Øćƒ©ćƒ¼ć«ćŖć£ćŸć‹ćÆć‚ć‹ć‚Šć¾ć›ć‚“ć€‚

ć‚¹ć‚­ćƒƒćƒ—ć—ćŸItem悒惭悰å‡ŗåŠ›ć™ć‚‹

ć‚¹ć‚­ćƒƒćƒ—ć—ćŸItem悒惭悰å‡ŗåŠ›ć™ć‚‹ć‚ˆć†ć«ć—ć¦ćæć¾ć—ć‚‡ć†ć€‚

Common Batch Patterns / Logging Item Processing and Failures

SkipListenerć‚¤ćƒ³ć‚æćƒ¼ćƒ•ć‚§ćƒ¼ć‚¹ć‚’å®Ÿč£…ć™ć‚‹ć‹ć€@OnSkipInProcessć‚¢ćƒŽćƒ†ćƒ¼ć‚·ćƒ§ćƒ³ć‚’ä»˜äøŽć—ćŸćƒ”ć‚½ćƒƒćƒ‰ć‚’ęŒć¤ć‚Æćƒ©ć‚¹ć‚’ä½œęˆć™ć‚‹åæ…č¦ćŒć‚ć‚Šć¾ć™ć€‚

SkipListener (Spring Batch 4.3.5 API)

OnSkipInProcess (Spring Batch 4.3.5 API)

ć“ć®ć‚ˆć†ćŖListenerć‚’ä½œęˆć€‚

src/main/java/org/littlewings/spring/batch/RejectItemLoggingListener.java

package org.littlewings.spring.batch;

import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.annotation.OnSkipInProcess;
import org.springframework.batch.item.validator.ValidationException;
import org.springframework.validation.BindException;

public class RejectItemLoggingListener {
    Logger logger = LoggerFactory.getLogger(RejectItemLoggingListener.class);

    @OnSkipInProcess
    public void OnSkipInProcess(Book book, Throwable throwable) {
        if (throwable instanceof ValidationException) {
            ((BindException) throwable.getCause()).getMessage();
            logger.info(
                    "validation error, isbn = {}, title = {}, reject reason = {}",
                    book.getIsbn(),
                    book.getTitle(),
                    ((BindException) throwable.getCause())
                            .getBindingResult()
                            .getFieldErrors()
                            .stream().map(e -> e.getObjectName() + "#" + e.getField() + ":" + e.getDefaultMessage()).collect(Collectors.joining(", "))
            );
        } else {
            logger.error("error", throwable);
        }
    }
}

恓恔悉悒Bean定ē¾©ć—恦

    @StepScope
    @Bean
    public RejectItemLoggingListener rejectItemLoggingListener() {
        return new RejectItemLoggingListener();
    }

faultTolerantć®å¾Œć«ListenerćØ恗恦čæ½åŠ ć—ć¾ć™ć€‚

    @Bean
    public Step loggingInvalidItemBeanValidationStep() {
        return stepBuilderFactory
                .get("loggingInvalidItemBeanValidationStep")
                .<Book, Book>chunk(3)
                .reader(flatFileItemReader(null))
                .processor(beanValidatingItemProcessor())
                .writer(consoleItemWriter())
                .faultTolerant()
                .skip(ValidationException.class)
                .skipLimit(5)
                .listener(rejectItemLoggingListener())
                .build();
    }

Job悒꧋ꈐ怂

    @Bean
    public Job beanValidationJob() {
        return jobBuilderFactory
                .get("beanValidationJob")
                .incrementer(new RunIdIncrementer())
                .start(loggingInvalidItemBeanValidationStep())
                .build();
    }

å®Ÿč”Œć€‚

$ mvn package
$ java -Dspring.batch.job.names=beanValidationJob -jar target/batch-beanvalidation-0.0.1-SNAPSHOT.jar filePath=src/main/resources/book_invalid.csv

惭悰怂

2022-04-28 01:53:07.154  INFO 18429 --- [           main] o.s.b.a.b.JobLauncherApplicationRunner   : Running default command line with: [filePath=src/main/resources/book_invalid.csv]
2022-04-28 01:53:07.330  INFO 18429 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=beanValidationJob]] launched with the following parameters: [{run.id=10, filePath=src/main/resources/book_invalid.csv}]
2022-04-28 01:53:07.460  INFO 18429 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [loggingInvalidItemBeanValidationStep]
2022-04-28 01:53:07.693  INFO 18429 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4798142470, title = Springå¾¹åŗ•å…„門 Spring Frameworkć«ć‚ˆć‚‹Javać‚¢ćƒ—ćƒŖć‚±ćƒ¼ć‚·ćƒ§ćƒ³é–‹ē™ŗ, price = 4400
2022-04-28 01:53:07.693  INFO 18429 --- [           main] consoleItemWriter                        : [writer] isbn = 978-1492076988, title = Spring Boot: Up and Running: Building Cloud Native Java and Kotlin Applications, price = 6265
2022-04-28 01:53:07.697  INFO 18429 --- [           main] o.l.s.batch.RejectItemLoggingListener    : validation error, isbn = xx-xxxxxxx, title = [ę”¹čØ‚ę–°ē‰ˆ]Springå…„é–€ ā€•ā€•Java惕 ćƒ¬ćƒ¼ćƒ ćƒÆćƒ¼ć‚Æćƒ»ć‚ˆć‚Šč‰Æ恄čØ­č؈ćØć‚¢ćƒ¼ć‚­ćƒ†ć‚Æćƒćƒ£, reject reason = item#isbn:ę­£č¦č”Øē¾ "\d{3}-\d{10}" ć«ćƒžćƒƒćƒć•ć›ć¦ćć ć•ć„, item#isbn:14 恋悉 14 恮間恮悵悤ć‚ŗć«ć—ć¦ćć ć•ć„
2022-04-28 01:53:07.748  INFO 18429 --- [           main] consoleItemWriter                        : [writer] isbn = 978-1484237236, title = The Definitive Guide to Spring Batch: Modern Finite Batch Processing in the Cloud, price = 7361
2022-04-28 01:53:07.748  INFO 18429 --- [           main] o.l.s.batch.RejectItemLoggingListener    : validation error, isbn = 978-4295000198, title = ć‚„ć•ć—ćå­¦ć¹ć‚‹MySQL運ē”Ø惻 ē®”ē†å…„門怐5.7åƾåæœć€‘, reject reason = item#price:1000 仄äøŠć®å€¤ć«ć—ć¦ćć ć•ć„
2022-04-28 01:53:07.748  INFO 18429 --- [           main] o.l.s.batch.RejectItemLoggingListener    : validation error, isbn = 978-4798147406, title = č©³č§£MySQL 5.7 ę­¢ć¾ć‚‰ć¬é€²åŒ–ć«ä¹—ć‚Šé…ć‚ŒćŖć„ćŸć‚ć®ćƒ†ć‚Æćƒ‹ć‚«ćƒ«ć‚¬ć‚¤ćƒ‰ (NEXT ONE), reject reason = item#price:1000 仄äøŠć®å€¤ć«ć—ć¦ćć ć•ć„
2022-04-28 01:53:07.776  INFO 18429 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4798161488, title = MySQLå¾¹åŗ•å…„門 ē¬¬4ē‰ˆ MySQL 8.0åƾåæœ, price = 4180
2022-04-28 01:53:07.776  INFO 18429 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4797393118, title = åŸŗē¤Žć‹ć‚‰ć®MySQL ē¬¬3ē‰ˆ (åŸŗē¤Žć‹ć‚‰ć‚·ćƒŖćƒ¼ć‚ŗ), price = 6038
2022-04-28 01:53:07.776  INFO 18429 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4873116389, title = å®Ÿč·µćƒć‚¤ćƒ‘ćƒ•ć‚©ćƒ¼ćƒžćƒ³ć‚¹MySQL ē¬¬3ē‰ˆ, price = 5280
2022-04-28 01:53:07.805  INFO 18429 --- [           main] o.l.s.batch.RejectItemLoggingListener    : validation error, isbn = 978-4774170206, title = MariaDB&MySQLå…Øę©Ÿčƒ½ćƒć‚¤ćƒ– 惫, reject reason = item#price:1000 仄äøŠć®å€¤ć«ć—ć¦ćć ć•ć„
2022-04-28 01:53:07.833  INFO 18429 --- [           main] o.s.batch.core.step.AbstractStep         : Step: [loggingInvalidItemBeanValidationStep] executed in 372ms
2022-04-28 01:53:07.883  INFO 18429 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=beanValidationJob]] completed with the following parameters: [{run.id=10, filePath=src/main/resources/book_invalid.csv}] and the following status: [COMPLETED] in 514ms

ć‚Øćƒ©ćƒ¼ć«ćŖć£ćŸItemćŒć€ćƒ­ć‚°ć«å‡ŗåŠ›ć•ć‚Œć‚‹ć‚ˆć†ć«ćŖć‚Šć¾ć—ćŸć€‚

恔ćŖćæ恫态仄äø‹ć®ć‚ˆć†ć«faultTolerantć®å‰ć«Listener悒čæ½åŠ ć—ć¦ć‚‚ę©Ÿčƒ½ć—ć¾ć›ć‚“ć€‚

    @Bean
    public Step invalidSkipItemListenerBeanValidationStep() {
        return stepBuilderFactory
                .get("invalidSkipItemListenerBeanValidationStep")
                .<Book, Book>chunk(3)
                .reader(flatFileItemReader(null))
                .processor(beanValidatingItemProcessor())
                .writer(consoleItemWriter())
                .listener(rejectItemLoggingListener())
                .faultTolerant()
                .skip(ValidationException.class)
                .skipLimit(5)
                .build();
    }

listenerćƒ”ć‚½ćƒƒćƒ‰ć®å¼•ę•°ć®åž‹ćŒObjectć®ć‚‚ć®ćŒć‚ć‚‹ć®ć§ęø”ć›ć¦ć—ć¾ć†ć®ć§ć™ćŒć€SkipListenerć‚¤ćƒ³ć‚æćƒ¼ćƒ•ć‚§ćƒ¼ć‚¹ć‚’å®Ÿč£…ć—ć¦ć„ć‚‹å “åˆćÆ态
åž‹ćŒåˆć‚ćšć«ć‚³ćƒ³ćƒ‘ć‚¤ćƒ«ćŒé€šć‚‰ćŖ恏ćŖć‚‹ć®ć§ć“ć®ć‚ˆć†ćŖćƒŸć‚¹ćÆē™ŗē”Ÿć—ć¾ć›ć‚“ā€¦ć€‚

skipć§ęŒ‡å®šć—ćŸå€¤ć‚’äøŠå›žć£ćŸå “合

skipć§ęŒ‡å®šć—ćŸę•°ć‚’č¶…ćˆć¦Itemć‚’ć‚¹ć‚­ćƒƒćƒ—ć—ćŸå “åˆć«ć€ć©ć†ćŖ悋恋悒ē¢ŗčŖć—恦ćæć¾ć—ć‚‡ć†ć€‚

惐ćƒŖćƒ‡ćƒ¼ć‚·ćƒ§ćƒ³ćŒNG恫ćŖ悋Item恮ꕰćÆ4ćŖ恮恧态skip恫3ć‚’ęŒ‡å®šć—ć¦ćæć¾ć™ć€‚

    @Bean
    public Step loggingInvalidItemBeanValidationStopStep() {
        return stepBuilderFactory
                .get("loggingInvalidItemBeanValidationStep")
                .<Book, Book>chunk(3)
                .reader(flatFileItemReader(null))
                .processor(beanValidatingItemProcessor())
                .writer(consoleItemWriter())
                .faultTolerant()
                .skip(ValidationException.class)
                .skipLimit(3)
                .listener(rejectItemLoggingListener())
                .build();
    }

Jobć®ę§‹ęˆć€‚

    @Bean
    public Job beanValidationJob() {
        return jobBuilderFactory
                .get("beanValidationJob")
                .incrementer(new RunIdIncrementer())
                .start(loggingInvalidItemBeanValidationStopStep())
                .build();
    }

å®Ÿč”Œć€‚

$ mvn package
$ java -Dspring.batch.job.names=beanValidationJob -jar target/batch-beanvalidation-0.0.1-SNAPSHOT.jar filePath=src/main/resources/book_invalid.csv

ēµęžœć€‚

2022-04-28 01:57:38.529  INFO 18665 --- [           main] o.s.b.a.b.JobLauncherApplicationRunner   : Running default command line with: [filePath=src/main/resources/book_invalid.csv]
2022-04-28 01:57:38.734  INFO 18665 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=beanValidationJob]] launched with the following parameters: [{run.id=11, filePath=src/main/resources/book_invalid.csv}]
2022-04-28 01:57:38.827  INFO 18665 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [loggingInvalidItemBeanValidationStep]
2022-04-28 01:57:39.020  INFO 18665 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4798142470, title = Springå¾¹åŗ•å…„門 Spring Frameworkć«ć‚ˆć‚‹Javać‚¢ćƒ—ćƒŖć‚±ćƒ¼ć‚·ćƒ§ćƒ³é–‹ē™ŗ, price = 4400
2022-04-28 01:57:39.020  INFO 18665 --- [           main] consoleItemWriter                        : [writer] isbn = 978-1492076988, title = Spring Boot: Up and Running: Building Cloud Native Java and Kotlin Applications, price = 6265
2022-04-28 01:57:39.025  INFO 18665 --- [           main] o.l.s.batch.RejectItemLoggingListener    : validation error, isbn = xx-xxxxxxx, title = [ę”¹čØ‚ę–°ē‰ˆ]Springå…„é–€ ā€•ā€•Java惕 ćƒ¬ćƒ¼ćƒ ćƒÆćƒ¼ć‚Æćƒ»ć‚ˆć‚Šč‰Æ恄čØ­č؈ćØć‚¢ćƒ¼ć‚­ćƒ†ć‚Æćƒćƒ£, reject reason = item#isbn:14 恋悉 14 恮間恮悵悤ć‚ŗć«ć—ć¦ćć ć•ć„, item#isbn:ę­£č¦č”Øē¾ "\d{3}-\d{10}" ć«ćƒžćƒƒćƒć•ć›ć¦ćć ć•ć„
2022-04-28 01:57:39.055  INFO 18665 --- [           main] consoleItemWriter                        : [writer] isbn = 978-1484237236, title = The Definitive Guide to Spring Batch: Modern Finite Batch Processing in the Cloud, price = 7361
2022-04-28 01:57:39.056  INFO 18665 --- [           main] o.l.s.batch.RejectItemLoggingListener    : validation error, isbn = 978-4295000198, title = ć‚„ć•ć—ćå­¦ć¹ć‚‹MySQL運ē”Ø惻 ē®”ē†å…„門怐5.7åƾåæœć€‘, reject reason = item#price:1000 仄äøŠć®å€¤ć«ć—ć¦ćć ć•ć„
2022-04-28 01:57:39.057  INFO 18665 --- [           main] o.l.s.batch.RejectItemLoggingListener    : validation error, isbn = 978-4798147406, title = č©³č§£MySQL 5.7 ę­¢ć¾ć‚‰ć¬é€²åŒ–ć«ä¹—ć‚Šé…ć‚ŒćŖć„ćŸć‚ć®ćƒ†ć‚Æćƒ‹ć‚«ćƒ«ć‚¬ć‚¤ćƒ‰ (NEXT ONE), reject reason = item#price:1000 仄äøŠć®å€¤ć«ć—ć¦ćć ć•ć„
2022-04-28 01:57:39.082  INFO 18665 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4798161488, title = MySQLå¾¹åŗ•å…„門 ē¬¬4ē‰ˆ MySQL 8.0åƾåæœ, price = 4180
2022-04-28 01:57:39.082  INFO 18665 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4797393118, title = åŸŗē¤Žć‹ć‚‰ć®MySQL ē¬¬3ē‰ˆ (åŸŗē¤Žć‹ć‚‰ć‚·ćƒŖćƒ¼ć‚ŗ), price = 6038
2022-04-28 01:57:39.082  INFO 18665 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4873116389, title = å®Ÿč·µćƒć‚¤ćƒ‘ćƒ•ć‚©ćƒ¼ćƒžćƒ³ć‚¹MySQL ē¬¬3ē‰ˆ, price = 5280
2022-04-28 01:57:39.110 ERROR 18665 --- [           main] o.s.batch.core.step.AbstractStep         : Encountered an error executing step loggingInvalidItemBeanValidationStep in job beanValidationJob

org.springframework.batch.core.step.skip.SkipLimitExceededException: Skip limit of '3' exceeded
        at org.springframework.batch.core.step.skip.LimitCheckingItemSkipPolicy.shouldSkip(LimitCheckingItemSkipPolicy.java:133) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.skip.ExceptionClassifierSkipPolicy.shouldSkip(ExceptionClassifierSkipPolicy.java:70) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.item.FaultTolerantChunkProcessor.shouldSkip(FaultTolerantChunkProcessor.java:519) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.item.FaultTolerantChunkProcessor.access$500(FaultTolerantChunkProcessor.java:56) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.item.FaultTolerantChunkProcessor$2.recover(FaultTolerantChunkProcessor.java:289) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.retry.support.RetryTemplate.handleRetryExhausted(RetryTemplate.java:539) ~[spring-retry-1.3.3.jar!/:na]
        at org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:387) ~[spring-retry-1.3.3.jar!/:na]
        at org.springframework.retry.support.RetryTemplate.execute(RetryTemplate.java:255) ~[spring-retry-1.3.3.jar!/:na]
        at org.springframework.batch.core.step.item.BatchRetryTemplate.execute(BatchRetryTemplate.java:217) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.item.FaultTolerantChunkProcessor.transform(FaultTolerantChunkProcessor.java:308) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.item.SimpleChunkProcessor.process(SimpleChunkProcessor.java:210) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.item.ChunkOrientedTasklet.execute(ChunkOrientedTasklet.java:77) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.tasklet.TaskletStep$ChunkTransactionCallback.doInTransaction(TaskletStep.java:407) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.tasklet.TaskletStep$ChunkTransactionCallback.doInTransaction(TaskletStep.java:331) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.transaction.support.TransactionTemplate.execute(TransactionTemplate.java:140) ~[spring-tx-5.3.19.jar!/:5.3.19]
        at org.springframework.batch.core.step.tasklet.TaskletStep$2.doInChunkContext(TaskletStep.java:273) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.scope.context.StepContextRepeatCallback.doInIteration(StepContextRepeatCallback.java:82) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.repeat.support.RepeatTemplate.getNextResult(RepeatTemplate.java:375) ~[spring-batch-infrastructure-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.repeat.support.RepeatTemplate.executeInternal(RepeatTemplate.java:215) ~[spring-batch-infrastructure-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.repeat.support.RepeatTemplate.iterate(RepeatTemplate.java:145) ~[spring-batch-infrastructure-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.tasklet.TaskletStep.doExecute(TaskletStep.java:258) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.AbstractStep.execute(AbstractStep.java:208) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.job.SimpleStepHandler.handleStep(SimpleStepHandler.java:152) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.job.AbstractJob.handleStep(AbstractJob.java:413) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.job.SimpleJob.doExecute(SimpleJob.java:136) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.job.AbstractJob.execute(AbstractJob.java:320) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.launch.support.SimpleJobLauncher$1.run(SimpleJobLauncher.java:149) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.core.task.SyncTaskExecutor.execute(SyncTaskExecutor.java:50) ~[spring-core-5.3.19.jar!/:5.3.19]
        at org.springframework.batch.core.launch.support.SimpleJobLauncher.run(SimpleJobLauncher.java:140) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
        at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
        at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:198) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.batch.core.configuration.annotation.SimpleBatchConfiguration$PassthruAdvice.invoke(SimpleBatchConfiguration.java:128) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at jdk.proxy2/jdk.proxy2.$Proxy58.run(Unknown Source) ~[na:na]
        at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.execute(JobLauncherApplicationRunner.java:199) ~[spring-boot-autoconfigure-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.executeLocalJobs(JobLauncherApplicationRunner.java:173) ~[spring-boot-autoconfigure-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.launchJobFromProperties(JobLauncherApplicationRunner.java:160) ~[spring-boot-autoconfigure-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.run(JobLauncherApplicationRunner.java:155) ~[spring-boot-autoconfigure-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.autoconfigure.batch.JobLauncherApplicationRunner.run(JobLauncherApplicationRunner.java:150) ~[spring-boot-autoconfigure-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:768) ~[spring-boot-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:758) ~[spring-boot-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:310) ~[spring-boot-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1312) ~[spring-boot-2.6.7.jar!/:2.6.7]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1301) ~[spring-boot-2.6.7.jar!/:2.6.7]
        at org.littlewings.spring.batch.App.main(App.java:11) ~[classes!/:0.0.1-SNAPSHOT]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
        at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
        at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:49) ~[batch-beanvalidation-0.0.1-SNAPSHOT.jar:0.0.1-SNAPSHOT]
        at org.springframework.boot.loader.Launcher.launch(Launcher.java:108) ~[batch-beanvalidation-0.0.1-SNAPSHOT.jar:0.0.1-SNAPSHOT]
        at org.springframework.boot.loader.Launcher.launch(Launcher.java:58) ~[batch-beanvalidation-0.0.1-SNAPSHOT.jar:0.0.1-SNAPSHOT]
        at org.springframework.boot.loader.JarLauncher.main(JarLauncher.java:88) ~[batch-beanvalidation-0.0.1-SNAPSHOT.jar:0.0.1-SNAPSHOT]
Caused by: org.springframework.batch.item.validator.ValidationException: Validation failed for org.littlewings.spring.batch.Book@47428937:
Field error in object 'item' on field 'price': rejected value [850]; codes [Min.item.price,Min.price,Min.java.lang.Integer,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.price,price]; arguments []; default message [price],1000]; default message [1000 仄äøŠć®å€¤ć«ć—ć¦ćć ć•ć„]
        at org.springframework.batch.item.validator.SpringValidator.validate(SpringValidator.java:54) ~[spring-batch-infrastructure-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.item.validator.ValidatingItemProcessor.process(ValidatingItemProcessor.java:84) ~[spring-batch-infrastructure-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.item.validator.ValidatingItemProcessor$$FastClassBySpringCGLIB$$39980290.invoke(<generated>) ~[spring-batch-infrastructure-4.3.5.jar!/:4.3.5]
        at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:793) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.support.DelegatingIntroductionInterceptor.doProceed(DelegatingIntroductionInterceptor.java:137) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.support.DelegatingIntroductionInterceptor.invoke(DelegatingIntroductionInterceptor.java:124) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:708) ~[spring-aop-5.3.19.jar!/:5.3.19]
        at org.springframework.batch.item.validator.BeanValidatingItemProcessor$$EnhancerBySpringCGLIB$$b5c9b6d8.process(<generated>) ~[spring-batch-infrastructure-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.item.SimpleChunkProcessor.doProcess(SimpleChunkProcessor.java:134) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.batch.core.step.item.FaultTolerantChunkProcessor$1.doWithRetry(FaultTolerantChunkProcessor.java:239) ~[spring-batch-core-4.3.5.jar!/:4.3.5]
        at org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:329) ~[spring-retry-1.3.3.jar!/:na]
        ... 52 common frames omitted
Caused by: org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'item' on field 'price': rejected value [850]; codes [Min.item.price,Min.price,Min.java.lang.Integer,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [item.price,price]; arguments []; default message [price],1000]; default message [1000 仄äøŠć®å€¤ć«ć—ć¦ćć ć•ć„]
        ... 68 common frames omitted

2022-04-28 01:57:39.115  INFO 18665 --- [           main] o.s.batch.core.step.AbstractStep         : Step: [loggingInvalidItemBeanValidationStep] executed in 288ms
2022-04-28 01:57:39.158  INFO 18665 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=beanValidationJob]] completed with the following parameters: [{run.id=11, filePath=src/main/resources/book_invalid.csv}] and the following status: [FAILED] in 376ms

ć‚¹ć‚­ćƒƒćƒ—åÆčƒ½ćŖItemć®ę•°ć‚’č¶…ćˆćŸćØ恄恆恓ćØć§ć€ä¾‹å¤–ćŒć‚¹ćƒ­ćƒ¼ć•ć‚Œć‚¢ćƒ—ćƒŖć‚±ćƒ¼ć‚·ćƒ§ćƒ³ćŒåœę­¢ć—ć¾ć™ć€‚

org.springframework.batch.core.step.skip.SkipLimitExceededException: Skip limit of '3' exceeded
SkipPolicy悒čØ­å®šć™ć‚‹

ęœ€å¾Œć«ć€SkipPolicy悒čØ­å®šć—ć¦ćæć¾ć™ć€‚

SkipPolicy (Spring Batch 4.3.5 API)

SkipPolicyćÆć‚¹ć‚­ćƒƒćƒ—åÆčƒ½ćŖę”ä»¶ć‚’åˆ¤å®šć™ć‚‹ćŸć‚ć®ć‚¤ćƒ³ć‚æćƒ¼ćƒ•ć‚§ćƒ¼ć‚¹ć§ć€SkipPolicy#shouldSkip(java.lang.Throwable t, int skipCount)ćƒ”ć‚½ćƒƒćƒ‰ć§
åˆ¤å®šć‚’č”Œć„ć¾ć™ć€‚

ä»Šć¾ć§čØ­å®šć—ć¦ć„ćŸć‚¹ć‚­ćƒƒćƒ—ćØåˆ¤å®šć™ć‚‹ä¾‹å¤–ć‚„ć‚¹ć‚­ćƒƒćƒ—åÆčƒ½ćŖꕰćÆ态SkipPolicyć®å®Ÿč£…ć§ć‚ć‚‹ExceptionClassifierSkipPolicyćØ
LimitCheckingItemSkipPolicy恮ēµ„ćæåˆć‚ć›ć§ę©Ÿčƒ½ć—ć¦ć„ć¾ć—ćŸć€‚

ExceptionClassifierSkipPolicy (Spring Batch 4.3.5 API)

LimitCheckingItemSkipPolicy (Spring Batch 4.3.5 API)

https://github.com/spring-projects/spring-batch/blob/4.3.5/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilder.java#L575-L579

恓恓恧态仄äø‹ć®ć‚ˆć†ć«skipPolicyćƒ”ć‚½ćƒƒćƒ‰ć§ę˜Žē¤ŗēš„恫SkipPolicyć®ć‚¤ćƒ³ć‚¹ć‚æćƒ³ć‚¹ć‚’ęŒ‡å®šć™ć‚‹ć“ćØć§ć€ć‚¹ć‚­ćƒƒćƒ—ć®ę”ä»¶ć‚’ć‚«ć‚¹ć‚æ惞悤ć‚ŗ
恙悋恓ćØćŒć§ćć¾ć™ć€‚

    @Bean
    public Step loggingInvalidItemAlwaysSkipBeanValidationStep() {
        return stepBuilderFactory
                .get("loggingInvalidItemBeanValidationStep")
                .<Book, Book>chunk(3)
                .reader(flatFileItemReader(null))
                .processor(beanValidatingItemProcessor())
                .writer(consoleItemWriter())
                .faultTolerant()
                //.skip(ValidationException.class)
                .skipPolicy(new AlwaysSkipItemSkipPolicy())
                .listener(rejectItemLoggingListener())
                .build();
    }

ć“ć“ć§ęŒ‡å®šć—ć¦ć„ć‚‹AlwaysSkipItemSkipPolicyćÆć€ä¾‹å¤–ć‚„ć‚¹ć‚­ćƒƒćƒ—ć—ćŸItemć®ę•°ć«é–¢ć‚ć‚‰ćšåøøć«ć‚¹ć‚­ćƒƒćƒ—ć‚’čرåÆ恙悋ć‚Æćƒ©ć‚¹ć§ć™ć€‚

AlwaysSkipItemSkipPolicy (Spring Batch 4.3.5 API)

skipPolicyćƒ”ć‚½ćƒƒćƒ‰ć‚’ä½æć£ć¦SkipPolicyć‚’ęŒ‡å®šć™ć‚‹ćØ态CompositeSkipPolicyćØ恄恆SkipPolicyć‚’åˆęˆć™ć‚‹ć‚Æćƒ©ć‚¹ćŒä½æ悏悌态
悂ćØ悂ćØ利ē”Ø恕悌悋ExceptionClassifierSkipPolicyćØLimitCheckingItemSkipPolicy恮ēµ„ćæåˆć‚ć›ćØåˆęˆć•ć‚Œć¾ć™ć€‚

CompositeSkipPolicy (Spring Batch 4.3.5 API)

https://github.com/spring-projects/spring-batch/blob/4.3.5/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilder.java#L586

č©•ä¾”é †ćÆć€å…ˆć«ē™»éŒ²ć—ćŸSkipPolicyć‹ć‚‰é †ć«č”Œć‚ć‚Œć‚‹ć‚ˆć†ćŖ恮恧态AlwaysSkipItemSkipPolicy悒ä½æē”Ø恙悋ćØć‚¹ć‚­ćƒƒćƒ—åÆ¾č±”ć®ä¾‹å¤–ć‚’
ć©ć®ć‚ˆć†ć«ęŒ‡å®šć—ć¦ć‚‚ć‚¹ć‚­ćƒƒćƒ—ę•°ć‚’ć©ć®ć‚ˆć†ć«ęŒ‡å®šć—ć¦ć‚‚ć€åøøć«ć‚¹ć‚­ćƒƒćƒ—åÆčƒ½ćØåˆ¤å®šć•ć‚Œć¾ć™ć€‚

https://github.com/spring-projects/spring-batch/blob/4.3.5/spring-batch-core/src/main/java/org/springframework/batch/core/step/skip/CompositeSkipPolicy.java#L40-L44

恔ćŖćæ恫态他恮SkipPolicyć®å®Ÿč£…ćÆć‚¹ć‚­ćƒƒćƒ—ć—ćŖ恄NeverSkipItemSkipPolicy恌恂悋悈恆恧恙怂

NeverSkipItemSkipPolicy (Spring Batch 4.3.5 API)

Job定ē¾©ć‚’å¤‰ę›“恗恦

    @Bean
    public Job beanValidationJob() {
        return jobBuilderFactory
                .get("beanValidationJob")
                .incrementer(new RunIdIncrementer())
                .start(loggingInvalidItemAlwaysSkipBeanValidationStep())
                .build();
    }

ćƒ‘ćƒƒć‚±ćƒ¼ć‚øćƒ³ć‚°ć—ć¦å®Ÿč”Œć€‚

$ mvn package
$ java -Dspring.batch.job.names=beanValidationJob -jar target/batch-beanvalidation-0.0.1-SNAPSHOT.jar filePath=src/main/resources/book_invalid.csv

惭悰怂

2022-05-22 00:44:05.818  INFO 66977 --- [           main] org.littlewings.spring.batch.App         : Started App in 2.269 seconds (JVM running for 2.677)
2022-05-22 00:44:05.819  INFO 66977 --- [           main] o.s.b.a.b.JobLauncherApplicationRunner   : Running default command line with: [filePath=src/main/resources/book_invalid.csv]
2022-05-22 00:44:06.012  INFO 66977 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=beanValidationJob]] launched with the following parameters: [{run.id=4, filePath=src/main/resources/book_invalid.csv}]
2022-05-22 00:44:06.141  INFO 66977 --- [           main] o.s.batch.core.job.SimpleStepHandler     : Executing step: [loggingInvalidItemBeanValidationStep]
2022-05-22 00:44:06.379  INFO 66977 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4798142470, title = Springå¾¹åŗ•å…„門 Spring Frameworkć«ć‚ˆć‚‹Javać‚¢ćƒ—ćƒŖć‚±ćƒ¼ć‚·ćƒ§ćƒ³é–‹ē™ŗ, price = 4400
2022-05-22 00:44:06.379  INFO 66977 --- [           main] consoleItemWriter                        : [writer] isbn = 978-1492076988, title = Spring Boot: Up and Running: Building Cloud Native Java and Kotlin Applications, price = 6265
2022-05-22 00:44:06.387  INFO 66977 --- [           main] o.l.s.batch.RejectItemLoggingListener    : validation error, isbn = xx-xxxxxxx, title = [ę”¹čØ‚ę–°ē‰ˆ]Springå…„é–€ ā€•ā€•Java ćƒ•ćƒ¬ćƒ¼ćƒ ćƒÆćƒ¼ć‚Æćƒ»ć‚ˆć‚Šč‰Æ恄čØ­č؈ćØć‚¢ćƒ¼ć‚­ćƒ†ć‚Æćƒćƒ£, reject reason = item#isbn:ę­£č¦č”Øē¾ "\d{3}-\d{10}" ć«ćƒžćƒƒćƒć•ć›ć¦ćć ć•ć„, item#isbn:14 恋悉 14 恮間恮悵悤ć‚ŗć«ć—ć¦ćć ć•ć„
2022-05-22 00:44:06.510  INFO 66977 --- [           main] consoleItemWriter                        : [writer] isbn = 978-1484237236, title = The Definitive Guide to Spring Batch: Modern Finite Batch Processing in the Cloud, price = 7361
2022-05-22 00:44:06.510  INFO 66977 --- [           main] o.l.s.batch.RejectItemLoggingListener    : validation error, isbn = 978-4295000198, title = ć‚„ć•ć—ćå­¦ć¹ć‚‹MySQL運ē”Ø 惻ē®”ē†å…„門怐5.7åƾåæœć€‘, reject reason = item#price:1000 仄äøŠć®å€¤ć«ć—ć¦ćć ć•ć„
2022-05-22 00:44:06.511  INFO 66977 --- [           main] o.l.s.batch.RejectItemLoggingListener    : validation error, isbn = 978-4798147406, title = č©³č§£MySQL 5.7 ę­¢ć¾ć‚‰ć¬é€²åŒ–ć«ä¹—ć‚Šé…ć‚ŒćŖć„ćŸć‚ć®ćƒ†ć‚Æćƒ‹ć‚«ćƒ«ć‚¬ć‚¤ćƒ‰ (NEXT ONE), reject reason = item#price:1000 仄äøŠć®å€¤ć«ć—ć¦ćć ć•ć„
2022-05-22 00:44:06.543  INFO 66977 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4798161488, title = MySQLå¾¹åŗ•å…„門 ē¬¬4ē‰ˆ MySQL 8.0åƾåæœ, price = 4180
2022-05-22 00:44:06.543  INFO 66977 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4797393118, title = åŸŗē¤Žć‹ć‚‰ć®MySQL ē¬¬3ē‰ˆ (åŸŗē¤Žć‹ć‚‰ć‚·ćƒŖćƒ¼ć‚ŗ), price = 6038
2022-05-22 00:44:06.544  INFO 66977 --- [           main] consoleItemWriter                        : [writer] isbn = 978-4873116389, title = å®Ÿč·µćƒć‚¤ćƒ‘ćƒ•ć‚©ćƒ¼ćƒžćƒ³ć‚¹MySQL ē¬¬3ē‰ˆ, price = 5280
2022-05-22 00:44:06.576  INFO 66977 --- [           main] o.l.s.batch.RejectItemLoggingListener    : validation error, isbn = 978-4774170206, title = MariaDB&MySQLå…Øę©Ÿčƒ½ćƒć‚¤ ćƒ–ćƒ«, reject reason = item#price:1000 仄äøŠć®å€¤ć«ć—ć¦ćć ć•ć„
2022-05-22 00:44:06.607  INFO 66977 --- [           main] o.s.batch.core.step.AbstractStep         : Step: [loggingInvalidItemBeanValidationStep] executed in 465ms
2022-05-22 00:44:06.665  INFO 66977 --- [           main] o.s.b.c.l.support.SimpleJobLauncher      : Job: [SimpleJob: [name=beanValidationJob]] completed with the following parameters: [{run.id=4, filePath=src/main/resources/book_invalid.csv}] and the following status: [COMPLETED] in 605ms

ć‚¹ć‚­ćƒƒćƒ—åÆčƒ½ćŖä¾‹å¤–ć‚‚ć‚¹ć‚­ćƒƒćƒ—åÆčƒ½ćŖå›žę•°ć‚‚ć—ć¦ć„ć¾ć›ć‚“ćŒć€Itemć®å‡¦ē†äø­ć«ä¾‹å¤–ćŒć‚¹ćƒ­ćƒ¼ć•ć‚Œć¦ć‚‚ć‚¹ć‚­ćƒƒćƒ—ćØć—ć¦åˆ¤å®šć™ć‚‹ć‚ˆć†ć«ćŖć‚Šć¾ć—ćŸć€‚

AlwaysSkipItemSkipPolicy悒ä½æē”Ø恙悋ćØ态äø€č¦‹ć‚¹ć‚­ćƒƒćƒ—å›žę•°ć®åˆ¶é™ć‚’ę’¤å»ƒć§ćć‚‹ć‚ˆć†ć«ć‚‚č¦‹ćˆć¾ć™ćŒć€ä¾‹å¤–ć‚‚ć™ć¹ć¦ē„”視恙悋恮恧
ć”ć‚‡ć£ćØå¾®å¦™ć§ć™ć­ć€‚

ć‚‚ć†å°‘ć—å‡ć£ćŸć‚¹ć‚­ćƒƒćƒ—ę”ä»¶ć«ć—ćŸć„å “åˆćÆ态č‡Ŗåˆ†ć§SkipPolicyć‚’å®Ÿč£…ć™ć‚‹ć®ć‹ć‚‚ć—ć‚Œć¾ć›ć‚“ć€‚

ć¾ćØ悁

Spring Batch恫Bean Validationć‚’åŠ ćˆć‚‹ćØćØ悂恫态Itemć‚’ć‚¹ć‚­ćƒƒćƒ—ć—ćŸå “åˆćØćć®åˆ¶å¾”ć«ć¤ć„ć¦ē¢ŗčŖć—恦ćæć¾ć—ćŸć€‚

1åŗ¦Spring Batchć‚’ć—ć£ć‹ć‚Šč¦‹ć¦ćŠććØć€ć“ć®ć‚ćŸć‚ŠćÆ割ćØ恙悓ćŖ悊ćØå…„ć‚Œć¾ć—ćŸć­ć€‚

ęœ€å¾Œć«ć€Job悄Stepć‚’å®šē¾©ć—ć¦ć„ćŸć‚Æćƒ©ć‚¹ć®ć‚½ćƒ¼ć‚¹ć‚³ćƒ¼ćƒ‰å…Øä½“ć‚’č¼‰ć›ć¦ćŠćć¾ć™ć€‚

src/main/java/org/littlewings/spring/batch/BeanValidationJobConfig.java

package org.littlewings.spring.batch;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepScope;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.step.skip.AlwaysSkipItemSkipPolicy;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder;
import org.springframework.batch.item.validator.BeanValidatingItemProcessor;
import org.springframework.batch.item.validator.ValidationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

@Configuration
public class BeanValidationJobConfig {
    @Autowired
    JobBuilderFactory jobBuilderFactory;

    @Autowired
    StepBuilderFactory stepBuilderFactory;

    @Autowired
    LocalValidatorFactoryBean localValidatorFactoryBean;

    @Bean
    public Job beanValidationJob() {
        return jobBuilderFactory
                .get("beanValidationJob")
                .incrementer(new RunIdIncrementer())
                //.start(noBeanValidationStep())
                //.start(withBeanValidationStep())
                //.start(faultTolerantBeanValidationStep())
                //.start(invalidSkipItemListenerBeanValidationStep())
                .start(loggingInvalidItemBeanValidationStep())
                //.start(loggingInvalidItemBeanValidationStopStep())
                //.start(loggingInvalidItemAlwaysSkipBeanValidationStep())
                .build();
    }

    @Bean
    public Step noBeanValidationStep() {
        return stepBuilderFactory
                .get("withBeanValidationStep")
                .<Book, Book>chunk(3)
                .reader(flatFileItemReader(null))
                .writer(consoleItemWriter())
                .build();
    }

    @Bean
    public Step withBeanValidationStep() {
        return stepBuilderFactory
                .get("withBeanValidationStep")
                .<Book, Book>chunk(3)
                .reader(flatFileItemReader(null))
                .processor(beanValidatingItemProcessor())
                .writer(consoleItemWriter())
                .build();
    }

    @Bean
    public Step faultTolerantBeanValidationStep() {
        return stepBuilderFactory
                .get("withBeanValidationStep")
                .<Book, Book>chunk(3)
                .reader(flatFileItemReader(null))
                .processor(beanValidatingItemProcessor())
                .writer(consoleItemWriter())
                .faultTolerant()
                .skip(ValidationException.class)
                .skipLimit(5)
                .build();
    }

    @Bean
    public Step invalidSkipItemListenerBeanValidationStep() {
        return stepBuilderFactory
                .get("invalidSkipItemListenerBeanValidationStep")
                .<Book, Book>chunk(3)
                .reader(flatFileItemReader(null))
                .processor(beanValidatingItemProcessor())
                .writer(consoleItemWriter())
                .listener(rejectItemLoggingListener())
                .faultTolerant()
                .skip(ValidationException.class)
                .skipLimit(5)
                .build();
    }

    @Bean
    public Step loggingInvalidItemBeanValidationStep() {
        return stepBuilderFactory
                .get("loggingInvalidItemBeanValidationStep")
                .<Book, Book>chunk(3)
                .reader(flatFileItemReader(null))
                .processor(beanValidatingItemProcessor())
                .writer(consoleItemWriter())
                .faultTolerant()
                .skip(ValidationException.class)
                .skipLimit(5)
                .listener(rejectItemLoggingListener())
                .build();
    }

    @Bean
    public Step loggingInvalidItemBeanValidationStopStep() {
        return stepBuilderFactory
                .get("loggingInvalidItemBeanValidationStep")
                .<Book, Book>chunk(3)
                .reader(flatFileItemReader(null))
                .processor(beanValidatingItemProcessor())
                .writer(consoleItemWriter())
                .faultTolerant()
                .skip(ValidationException.class)
                .skipLimit(3)
                .listener(rejectItemLoggingListener())
                .build();
    }

    @Bean
    public Step loggingInvalidItemAlwaysSkipBeanValidationStep() {
        return stepBuilderFactory
                .get("loggingInvalidItemBeanValidationStep")
                .<Book, Book>chunk(3)
                .reader(flatFileItemReader(null))
                .processor(beanValidatingItemProcessor())
                .writer(consoleItemWriter())
                .faultTolerant()
                //.skip(ValidationException.class)
                .skipPolicy(new AlwaysSkipItemSkipPolicy())
                .listener(rejectItemLoggingListener())
                .build();
    }

    @StepScope
    @Bean
    public FlatFileItemReader<Book> flatFileItemReader(@Value("#{jobParameters['filePath']}") String filePath) {
        Resource fileResource = new FileSystemResource(filePath);

        return new FlatFileItemReaderBuilder<Book>()
                .name("flatFileItemReader")
                .resource(fileResource)
                .encoding("UTF-8")
                .delimited()
                .names(new String[]{"isbn", "title", "price"})
                .linesToSkip(1)
                .targetType(Book.class)
                .saveState(false)
                .build();
    }

    @StepScope
    @Bean
    public BeanValidatingItemProcessor<Book> beanValidatingItemProcessor() {
        return new BeanValidatingItemProcessor<>(localValidatorFactoryBean);
    }

    @StepScope
    @Bean
    public RejectItemLoggingListener rejectItemLoggingListener() {
        return new RejectItemLoggingListener();
    }

    @StepScope
    @Bean
    public ItemWriter<Book> consoleItemWriter() {
        Logger logger = LoggerFactory.getLogger("consoleItemWriter");

        return books -> books.forEach(book ->
                logger.info("[writer] isbn = {}, title = {}, price = {}", book.getIsbn(), book.getTitle(), book.getPrice())
        );
    }
}

MySQL Connector/JćØCharacter Setļ¼Character Set Resultsļ¼Connection CollationćØ恮čØ­å®šć€é–¢äæ‚ćŒć‚ˆććŒć‚ć‹ć‚‰ćŖć‹ć£ćŸć®ć§čŖæć¹ć¦ćæ悋

恓悌ćÆ态ćŖć«ć‚’ć—ćŸćć¦ę›øć„ćŸć‚‚ć®ļ¼Ÿ

MySQL恮JDBCćƒ‰ćƒ©ć‚¤ćƒćƒ¼ć€Connector/J恮characterEncoding态characterSetResults态connectionCollationć‚ćŸć‚Šć®čŖ¬ę˜Žć‚’見恦恄恦态
äøę€č­°ćŖę„Ÿć˜ćŒć—ćŸć®ć§čŖæć¹ć¦ćæ悋恓ćØć«ć—ć¾ć—ćŸć€‚

MySQL :: MySQL Connector/J 8.0 Developer Guide :: 6.3.3 Session

ć©ć†ęŒ‡å®šć—ćŸć‚‰ć„ć„ć‹ć€ć‚ˆćć‚ć‹ć‚‰ćŖ恏ćŖ悋悓恧恙悈恭怂

Connector/J恮čŖ¬ę˜Žć‚’čŖ­ć‚€

characterEncoding态characterSetResults态connectionCollation恮čŖ¬ę˜Žć‚’ć€ćć‚Œćžć‚Œč¦‹ć¦ćæć¾ć™ć€‚

MySQL :: MySQL Connector/J 8.0 Developer Guide :: 6.3.3 Session

ćŖćŠć€ć“ć®ćƒ‰ć‚­ćƒ„ćƒ”ćƒ³ćƒˆć‚’č¦‹ć¦ć„ć‚‹ę™‚ć®MySQL Connector/Jć®ćƒćƒ¼ć‚øćƒ§ćƒ³ćÆ态8.0.28恧恙怂

characterEncodingćÆ态character_set_clientćŠć‚ˆć³character_set_connectionć‚’ć€ŒęŒ‡å®šć•ć‚ŒćŸJava恮ć‚Øćƒ³ć‚³ćƒ¼ćƒ‡ć‚£ćƒ³ć‚°ć®ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆć®
Character Set恫čØ­å®šć—ć€collation_connection悒Character Setć®ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆć®Collation恫čØ­å®šć™ć‚‹ć€ćØę›øć‹ć‚Œć¦ć„ć¾ć™ć€‚

Instructs the server to set session system variables 'character_set_client' and 'character_set_connection' to the default character set for the specified Java encoding and set 'collation_connection' to the default collation for this character set.

characterEncoding悂connectionCollationć‚‚ęŒ‡å®šć•ć‚Œć¦ć„ćŖć„å “åˆćÆ态characterEncodingćØ恗恦ćÆ8.0.26仄降ćÆutf8mb4ćŒęŒ‡å®šć•ć‚Œć‚‹ćØ
ę›øć‹ć‚Œć¦ć„ć¾ć™ć€‚

If neither this property nor the property 'connectionCollation' is set:
For Connector/J 8.0.25 and earlier, the driver will try to use the server default character set; For Connector/J 8.0.26 and later, the driver will use "utf8mb4".

utf8mb4ćÆJava恮ć‚Øćƒ³ć‚³ćƒ¼ćƒ‡ć‚£ćƒ³ć‚°ć§ćÆć‚ć‚Šć¾ć›ć‚“ćŒā€¦ć€‚

恔ćŖćæ恫态ē¾åœØ恮MySQLćÆCharacter EncodingćÆutf8mb4ćŒćƒ‡ćƒ•ć‚©ćƒ«ćƒˆć§ć‚ć‚Šć€utf8mb4ć®ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆć®CollationćÆutf8mb4_0900_ai_ci恧恙怂

MySQL Server 恫ćÆć‚µćƒ¼ćƒćƒ¼ę–‡å­—ć‚»ćƒƒćƒˆćØć‚µćƒ¼ćƒćƒ¼ē…§åˆé †åŗćŒć‚ć‚Šć¾ć™ć€‚ ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆć§ćÆ态恓悌悉ćÆ utf8mb4 ćŠć‚ˆć³ utf8mb4_0900_ai_ci ć§ć™ćŒć€ć‚µćƒ¼ćƒćƒ¼ć®čµ·å‹•ę™‚ć«ć‚³ćƒžćƒ³ćƒ‰ćƒ©ć‚¤ćƒ³ć¾ćŸćÆć‚Ŗćƒ—ć‚·ćƒ§ćƒ³ćƒ•ć‚”ć‚¤ćƒ«ć§ę˜Žē¤ŗēš„恫čØ­å®šć—ć€å®Ÿč”Œę™‚ć«å¤‰ę›“ć§ćć¾ć™ć€‚

MySQL :: MySQL 8.0 リファレンスマニュアル :: 10.3.2 サーバー文字セットおよび照合順序

characterSetResultsć«ć¤ć„ć¦ć‚‚ć€ć€ŒęŒ‡å®šć•ć‚ŒćŸJava恮ć‚Øćƒ³ć‚³ćƒ¼ćƒ‡ć‚£ćƒ³ć‚°ć«åƾåæœć™ć‚‹Character Set恧ć‚Øćƒ³ć‚³ćƒ¼ćƒ‰ć•ć‚ŒćŸćƒ‡ćƒ¼ć‚æ悒čæ”ć™ć‚ˆć†ć«
ć‚µćƒ¼ćƒćƒ¼ć«ęŒ‡ē¤ŗć—ć¾ć™ć€ćØę›øć‹ć‚Œć¦ć„ć¾ć™ć€‚

Instructs the server to return the data encoded with the default character set for the specified Java encoding.

ęŒ‡å®šć—ćŖć„ć€ć¾ćŸćÆnullć®å “åˆć€ć‚µćƒ¼ćƒćƒ¼ćÆå…ƒć®Character Setć§ćƒ‡ćƒ¼ć‚æ悒送äæ”ć—ć€ćƒ‰ćƒ©ć‚¤ćƒćƒ¼ćÆēµęžœć®ćƒ”ć‚æćƒ‡ćƒ¼ć‚æć«å¾“ć£ć¦ćƒ‡ćƒ¼ć‚æ悒
ćƒ‡ć‚³ćƒ¼ćƒ‰ć—ć¾ć™ć€‚

If not set or set to "null", the server will send data in its original character set and the driver will decode it according to the result metadata.

connectionCollationćÆć€ć‚»ćƒƒć‚·ćƒ§ćƒ³ć‚·ć‚¹ćƒ†ćƒ å¤‰ę•°collation_connectionć‚’ęŒ‡å®šć•ć‚ŒćŸCollation恫čØ­å®šć—ć€character_set_clientćØ
character_set_connection悒åƾåæœć™ć‚‹Character Set恫čØ­å®šć™ć‚‹ć‚ˆć†ć«ć‚µćƒ¼ćƒćƒ¼ć«ęŒ‡ē¤ŗć—ć¾ć™ć€‚

Instructs the server to set session system variable 'collation_connection' to the specified collation name and set 'character_set_client' and 'character_set_connection' to the corresponding character set.

恓恮ēµęžœć€connectionCollationćÆcharacterEncodingć§ęŒ‡å®šć—ćŸå€¤ć‚’äøŠę›øćć™ć‚‹ęŒ™å‹•ć«ćŖ悋悈恆恧恙怂

This property overrides the value of 'characterEncoding' with the character set this collation belongs to.

ćć—ć¦connectionCollation悂characterEncodingć‚‚ęŒ‡å®šć•ć‚Œć¦ć„ćŖć„å “åˆćÆ态connectionCollationć®ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆć®Collation恫ćŖ悋ćØ
ę›øć‹ć‚Œć¦ć„ć¾ć™ć€‚

If neither this property nor the property 'characterEncoding' is set:
For Connector/J 8.0.25 and earlier, the driver will try to use the server default character set;
For Connector/J 8.0.26 and later, the driver will use "utf8mb4" default collation.

恓悌ćÆć€ć©ć†ęŒ‡å®šć™ć‚‹ć®ćŒé©åˆ‡ć§ć—ć‚‡ć†ć‹ļ¼Ÿć“恆ćŖ悋ćØć€å„å¤‰ę•°ćŒć‚¢ćƒ—ćƒŖć‚±ćƒ¼ć‚·ćƒ§ćƒ³ć®å‹•ä½œć«äøŽćˆć‚‹å½±éŸæ悒ē¢ŗčŖć—ć¦ćŠćåæ…č¦ćŒ
恂悊恝恆恧恙恭怂

ē¾åœØćÆCharacter EncodingćÆutf8mb4ć‚’ęŒ‡å®šć™ć‚‹ć®ćŒē„”é›£ć‹ćØę€ć„ć¾ć™ć®ć§ć€äø»ć«Collationć¾ć‚ć‚Šć«é–¢ć™ć‚‹č©±ćŒćƒć‚¤ćƒ³ćƒˆć‹ćŖćØćÆ
ę€ć„ć¾ć™ćŒć€‚

MySQL恮Collationć®ćƒ‰ć‚­ćƒ„ćƒ”ćƒ³ćƒˆć‚’čŖ­ć‚“恧ćæ悋

ę¬”ć®ćƒ‰ć‚­ćƒ„ćƒ”ćƒ³ćƒˆć‚’č¦‹ć¦ćæć¾ć™ć€‚

MySQL :: MySQL 8.0 リファレンスマニュアル :: 10.4 接続文字セットおよび照合順序

ć¾ćšćÆć€ć‚µćƒ¼ćƒćƒ¼ćŠć‚ˆć³ćƒ‡ćƒ¼ć‚æćƒ™ćƒ¼ć‚¹ćƒ¬ćƒ™ćƒ«ć®Character SetćØCollation恫恤恄恦怂

惻character_set_server ćŠć‚ˆć³ collation_server ć‚·ć‚¹ćƒ†ćƒ å¤‰ę•°ćÆć€ć‚µćƒ¼ćƒćƒ¼ć®ę–‡å­—ć‚»ćƒƒćƒˆćØē…§åˆé †åŗć‚’ē¤ŗć—ć¾ć™ć€‚
惻character_set_database ćŠć‚ˆć³ collation_database ć‚·ć‚¹ćƒ†ćƒ å¤‰ę•°ćÆć€ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆćƒ‡ćƒ¼ć‚æćƒ™ćƒ¼ć‚¹ć®ę–‡å­—ć‚»ćƒƒćƒˆćŠć‚ˆć³ē…§åˆé †åŗć‚’ē¤ŗć—ć¾ć™ć€‚

character_set_clientćÆ态ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćŒé€äæ”ć™ć‚‹ćƒ‡ćƒ¼ć‚æ恮ć‚Øćƒ³ć‚³ćƒ¼ćƒ‡ć‚£ćƒ³ć‚°ć«é–¢ć‚ć‚‹č©±ć«ćŖć‚Šć¾ć™ć€‚

ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć‹ć‚‰é›¢ć‚Œć‚‹ćØćć®ć‚¹ćƒ†ćƒ¼ćƒˆćƒ”ćƒ³ćƒˆć®ę–‡å­—ć‚»ćƒƒćƒˆćÆä½•ć§ć™ć‹ć€‚
ć‚µćƒ¼ćƒćƒ¼ćÆ态character_set_client ć‚·ć‚¹ćƒ†ćƒ å¤‰ę•°å€¤ć‚’ć€ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćŒé€äæ”ć™ć‚‹ć‚¹ćƒ†ćƒ¼ćƒˆćƒ”ćƒ³ćƒˆć®ę–‡å­—ć‚»ćƒƒćƒˆć«ć—ć¾ć™ć€‚

character_set_connectionćÆ态ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆćŒé€äæ”ć—ćŸćƒ‡ćƒ¼ć‚æć‚’å¤‰ę›ć™ć‚‹å…ˆć®ć‚Øćƒ³ć‚³ćƒ¼ćƒ‡ć‚£ćƒ³ć‚°ć‚’ęŒ‡å®šć™ć‚‹ć‚ˆć†ć§ć™ć€‚

ć‚µćƒ¼ćƒćƒ¼ćŒć‚¹ćƒ†ćƒ¼ćƒˆćƒ”ćƒ³ćƒˆć‚’å—äæ”恗恟恂ćØć€ć©ć®ę–‡å­—ć‚»ćƒƒćƒˆć«å¤‰ę›ć™ć‚‹ć¹ćć§ć™ć‹ć€‚
恓悌悒ē¢ŗčŖć™ć‚‹ćŸć‚ć«ć€ć‚µćƒ¼ćƒćƒ¼ćÆ character_set_connection ćŠć‚ˆć³ collation_connection ć‚·ć‚¹ćƒ†ćƒ å¤‰ę•°ć‚’ä½æē”Øć—ć¾ć™:
ć‚µćƒ¼ćƒćƒ¼ćÆ态ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć«ć‚ˆć£ć¦é€äæ”ć•ć‚ŒćŸć‚¹ćƒ†ćƒ¼ćƒˆćƒ”ćƒ³ćƒˆć‚’ character_set_client 恋悉 character_set_connection ć«å¤‰ę›ć—ć¾ć™ć€‚

äø€ę–¹ć§ć€collation_connectionćÆćƒŖćƒ†ćƒ©ćƒ«ę–‡å­—åˆ—ć®ęÆ”č¼ƒć«ä½æć‚ć‚Œć‚‹ć ć‘ć®ć‚ˆć†ć§ć™ć­ć€‚

collation_connection ćÆ态ćƒŖćƒ†ćƒ©ćƒ«ę–‡å­—åˆ—ć®ęÆ”č¼ƒć«é‡č¦ć§ć™ć€‚ ć‚«ćƒ©ćƒ å€¤ćØę–‡å­—åˆ—ć‚’ęÆ”č¼ƒć™ć‚‹å “åˆć€collation_connection ćÆ関äæ‚ć‚ć‚Šć¾ć›ć‚“ć€‚

ćØ恄恆恓ćØćÆ态collation_connection悒갗恫恙悋恓ćØćÆ恻ćØ悓恩ćŖ恕恝恆恧恙恭怂

character_set_resultsćÆć€ć‚µćƒ¼ćƒćƒ¼ć‹ć‚‰čæ”ć™ćƒ‡ćƒ¼ć‚æ恮ć‚Øćƒ³ć‚³ćƒ¼ćƒ‡ć‚£ćƒ³ć‚°ć«ä½æē”Ø恕悌悋悈恆恧恙怂

ć‚Æć‚ØćƒŖćƒ¼ēµęžœć‚’ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć«čæ”é€ć™ć‚‹å‰ć«ć€ć‚µćƒ¼ćƒćƒ¼ćÆć©ć®ę–‡å­—ć‚»ćƒƒćƒˆć«å¤‰ę›ć™ć‚‹åæ…č¦ćŒć‚ć‚Šć¾ć™ć‹ć€‚
character_set_results ć‚·ć‚¹ćƒ†ćƒ å¤‰ę•°å€¤ćÆć€ć‚µćƒ¼ćƒćƒ¼ćŒć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć«ć‚Æć‚ØćƒŖćƒ¼ēµęžœć‚’čæ”äæ”恙悋ćØćć«ä½æē”Øć™ć‚‹ę–‡å­—ć‚»ćƒƒćƒˆć‚’ē¤ŗć—ć¾ć™ć€‚ ć“ć‚Œć«ćÆć€ć‚«ćƒ©ćƒ å€¤ć€ēµęžœćƒ”ć‚æćƒ‡ćƒ¼ć‚æ (ć‚«ćƒ©ćƒ åćŖ恩)态ć‚Øćƒ©ćƒ¼ćƒ”ćƒƒć‚»ćƒ¼ć‚øćŖ恩恮ēµęžœćƒ‡ćƒ¼ć‚æćŒå«ć¾ć‚Œć¾ć™ć€‚

ē‰¹ć«å¤‰ę›ć‚’č”Œć„å “合ćÆ态čØ­å®šć—ćŖ恄恋binaryć«ęŒ‡å®šć™ć‚‹ć€ćØ怂

ēµęžœć‚»ćƒƒćƒˆć¾ćŸćÆć‚Øćƒ©ćƒ¼ćƒ”ćƒƒć‚»ćƒ¼ć‚øć®å¤‰ę›ć‚’å®Ÿč”Œć—ćŖć„ć‚ˆć†ć«ć‚µćƒ¼ćƒćƒ¼ć«ęŒ‡ē¤ŗ恙悋恫ćÆ态character_set_results 悒 NULL ć¾ćŸćÆ binary 恫čØ­å®šć—ć¾ć™:

character_set_server态character_set_database态character_set_client态character_set_connectionćÆutf8mb4恧ēµ±äø€ć—ć¦ć„ć‚Œć°
問锌ćŖ恕恝恆恧恙恗态恝恆恙悋ćØcharacter_set_resultsćÆ꘎ē¤ŗēš„ć«ęŒ‡å®šć—ćŖćć¦ć‚‚ć„ć„ć®ć§ćÆ态ćØ恄恆ꄟ恘恧恗悇恆恋怂

character_set_resultsć‚’ęŒ‡å®šć—ćŸćØ恗恦悂态utf8mb4恧恗悇恆恭怂

ćć‚Œćžć‚Œć®ć‚·ć‚¹ćƒ†ćƒ å¤‰ę•°ć®ćƒ‰ć‚­ćƒ„ćƒ”ćƒ³ćƒˆćØčŖ¬ę˜ŽćÆ态恓恔悉怂

  • character_set_server ā€¦ ć‚µćƒ¼ćƒćƒ¼ć®ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆć®ę–‡å­—ć‚»ćƒƒćƒˆ
  • character_set_database ā€¦ ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆćƒ‡ćƒ¼ć‚æćƒ™ćƒ¼ć‚¹ć§ä½æē”Øć•ć‚Œć‚‹ę–‡å­—ć‚»ćƒƒćƒˆ
    • ē¾åœØćÆ非ęŽØå„Ø恮čح定
  • character_set_client ā€¦ ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć‹ć‚‰åˆ°é”ć™ć‚‹ć‚¹ćƒ†ćƒ¼ćƒˆćƒ”ćƒ³ćƒˆć®ę–‡å­—ć‚»ćƒƒćƒˆ
  • character_set_connection ā€¦ ę–‡å­—ć‚»ćƒƒćƒˆć‚¤ćƒ³ćƒˆćƒ­ćƒ‡ćƒ„ćƒ¼ć‚µćŖć—ć§ęŒ‡å®šć•ć‚ŒćŸćƒŖćƒ†ćƒ©ćƒ«ćŠć‚ˆć³ę•°å€¤ć‹ć‚‰ę–‡å­—åˆ—ćøć®å¤‰ę›ć«ä½æē”Øć•ć‚Œć‚‹ę–‡å­—ć‚»ćƒƒćƒˆ
  • character_set_results ā€¦ ć‚Æć‚ØćƒŖćƒ¼ēµęžœć‚’ć‚Æćƒ©ć‚¤ć‚¢ćƒ³ćƒˆć«čæ”ć™ćŸć‚ć«ä½æē”Øć•ć‚Œć‚‹ę–‡å­—ć‚»ćƒƒćƒˆć€‚ ć“ć‚Œć«ćÆć€ć‚«ćƒ©ćƒ å€¤ć€ēµęžœćƒ”ć‚æćƒ‡ćƒ¼ć‚æ (ć‚«ćƒ©ćƒ åćŖ恩)态ć‚Øćƒ©ćƒ¼ćƒ”ćƒƒć‚»ćƒ¼ć‚øćŖ恩恮ēµęžœćƒ‡ćƒ¼ć‚æćŒå«ć¾ć‚Œć¾ć™ć€‚
  • collation_server ā€¦ ć‚µćƒ¼ćƒćƒ¼ć®ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆć®ē…§åˆé †åŗ
  • collation_database ā€¦ ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆćƒ‡ćƒ¼ć‚æćƒ™ćƒ¼ć‚¹ć§ä½æē”Ø恕悌悋ē…§åˆ
    • ē¾åœØćÆ非ęŽØå„Ø恮čح定
  • collation_connection ā€¦ ꎄē¶šę–‡å­—ć‚»ćƒƒćƒˆć®ē…§åˆé †åŗć€‚collation_connection ćÆ态ćƒŖćƒ†ćƒ©ćƒ«ę–‡å­—åˆ—ć®ęÆ”č¼ƒć«é‡č¦ć§ć™ć€‚ ć‚«ćƒ©ćƒ å€¤ćØę–‡å­—åˆ—ć‚’ęÆ”č¼ƒć™ć‚‹å “åˆć€collation_connection ćÆ関äæ‚ć‚ć‚Šć¾ć›ć‚“ć€‚ć“ć‚ŒćÆć€ć‚«ćƒ©ćƒ ć«ćÆē…§åˆå„Ŗ先åŗ¦ć®é«˜ć„ē‹¬č‡Ŗ恮ē…§åˆćŒć‚ć‚‹ćŸć‚ć§ć™

MySQL Connector/Jć«č©±ć‚’ęˆ»ć™ćØ

ć“ć“ć¾ć§ć®č©±ć‹ć‚‰ć€MySQL Connector/J恮čØ­å®šć«č©±ć‚’ęˆ»ć™ćØ态characterEncoding态characterSetResults态connectionCollationć®ćć‚Œćžć‚Œć‚’
ć©ć†ęŒ‡å®šć™ć‚Œć°ć„ć„ć®ć‹ļ¼ŸćØ恄恆恓ćØćŖć®ć§ć™ćŒć€‚

MySQLć‚µćƒ¼ćƒćƒ¼å“ć®Character Set悒utf8mb4恫ēµ±äø€ć™ć‚‹ć®ćŖ悉

  • characterEncoding ā€¦ UTF-8
  • characterSetResults ā€¦ ęŒ‡å®šćŖ恗
  • connectionCollation ā€¦ ęŒ‡å®šć—ćŖćć¦ć‚‚å®Ÿå®³ćÆćŖ恕恝恆ļ¼ˆę–‡å­—列ćƒŖćƒ†ćƒ©ćƒ«ć®ęÆ”č¼ƒć®ćæ恮話ćŖ恮恧ļ¼‰ć ćŒć€ę°—恫ćŖ悋ćŖć‚‰ć‚µćƒ¼ćƒćƒ¼ćØåŒć˜Collationć‚’ęŒ‡å®š

ćØć„ć£ćŸćØ恓悍恧恗悇恆恋怂

characterEncoding恫恤恄恦ćÆMySQL Connector/J恮čŖ¬ę˜Žļ¼ˆćƒ‡ćƒ•ć‚©ćƒ«ćƒˆå€¤ć®éƒØ分ļ¼‰ćŒę°—恫ćŖć‚‹ć®ć§ć€ć“ć®å¾Œć«ćƒ†ć‚¹ćƒˆć‚³ćƒ¼ćƒ‰ć‚’ę›ø恄恦
ē¢ŗčŖć—恦ćæ悋恓ćØć«ć—ć¾ć™ć€‚

ē’°å¢ƒ

ä»Šå›žć®ē’°å¢ƒćÆ恓恔悉怂

$ java --version
openjdk 17.0.2 2022-01-18
OpenJDK Runtime Environment (build 17.0.2+8-Ubuntu-120.04)
OpenJDK 64-Bit Server VM (build 17.0.2+8-Ubuntu-120.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.5 (3599d3414f046de2324203b78ddcf9b5e4388aa0)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.2, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-109-generic", arch: "amd64", family: "unix"

MySQLćÆć“ć”ć‚‰ć®ćƒćƒ¼ć‚øćƒ§ćƒ³ć§ć€172.17.0.2ć§å‹•ä½œć—ć¦ć„ć‚‹ć‚‚ć®ćØć—ć¾ć™ć€‚

$ mysql --version
mysql  Ver 8.0.28 for Linux on x86_64 (MySQL Community Server - GPL)

ć¾ćŸć€ć‚µćƒ¼ćƒćƒ¼ć®Character SetćŠć‚ˆć³CollationćÆ仄äø‹ć®čح定ćØć—ć¦ćŠćć¾ć™ć€‚

character-set-server = utf8mb4
collation-server = utf8mb4_0900_bin

ęŗ–å‚™

ä½œęˆć—ćŸMaven惗惭ć‚ø悧ć‚Æćƒˆć®ä¾å­˜é–¢äæ‚ē­‰ćÆ态恓恔悉怂

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.28</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.8.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.22.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
            </plugin>
        </plugins>
    </build>

ę¬”ć«ć€ćƒ†ć‚¹ćƒˆć‚³ćƒ¼ćƒ‰ć®é››å½¢ć‚’ä½œęˆć—ć¾ć™ć€‚

src/test/java/org/littlewings/mysql/ConnectorCharacterSetTest.java

package org.littlewings.mysql;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.*;

public class ConnectorCharacterSetTest {
    List<String> characterSetVariables = List.of(
            "character_set_connection",
            "character_set_client",
            "character_set_database",
            "character_set_filesystem",
            "character_set_results",
            "character_set_server",
            "character_set_system"
    );

    List<String> collationVariables = List.of(
            "collation_connection",
            "collation_database",
            "collation_server"
    );

    private Map<String, String> collectCharacterSets(Connection conn) throws SQLException {
        Map<String, String> characterSets = new LinkedHashMap<>();

        for (String characterSetVariable : characterSetVariables) {
            try (PreparedStatement ps = conn.prepareStatement("show variables where variable_name = ?")) {
                ps.setString(1, characterSetVariable);

                try (ResultSet rs = ps.executeQuery()) {
                    while (rs.next()) {
                        characterSets.put(rs.getString(1), rs.getString(2));
                    }
                }
            }
        }

        return characterSets;
    }

    private Map<String, String> collectCollations(Connection conn) throws SQLException {
        Map<String, String> collations = new LinkedHashMap<>();

        for (String collationVariable : collationVariables) {
            try (PreparedStatement ps = conn.prepareStatement("show variables where variable_name = ?")) {
                ps.setString(1, collationVariable);

                try (ResultSet rs = ps.executeQuery()) {
                    while (rs.next()) {
                        collations.put(rs.getString(1), rs.getString(2));
                    }
                }
            }
        }

        return collations;
    }

    // ć“ć“ć«ć€ćƒ†ć‚¹ćƒˆć‚’ę›ø恏ļ¼ļ¼
}

ē¾åœØ恮ꎄē¶šå†…恧恮Character SetćŠć‚ˆć³Collationć‚’åŽé›†ć™ć‚‹ćƒ”ć‚½ćƒƒćƒ‰ć‚’ē”Øę„ć—ć¦ć€ä»„é™ć«ä½œęˆć™ć‚‹ćƒ†ć‚¹ćƒˆć§ęŽ„ē¶šćƒ—ćƒ­ćƒ‘ćƒ†ć‚£ć‚’å¤‰ę›“恙悋ćØćØ悂恫态
ć“ć‚Œć‚‰ć®ć‚·ć‚¹ćƒ†ćƒ å¤‰ę•°ćŒć©ć®ć‚ˆć†ć«å¤‰åŒ–ć—ć¦ć„ćć‹ć‚’č¦‹ć¦ć„ćć“ćØć«ć—ć¾ć™ć€‚

characterEncoding悒ē¢ŗčŖć—恦ćæ悋

ćØ悊恂恈恚态ćŖć«ć‚‚ęŒ‡å®šć—ćŖć„å “åˆć€‚

    @Test
    public void nonSettings() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "utf8mb4"),
                    entry("character_set_client", "utf8mb4"),
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", ""),
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "utf8mb4_0900_ai_ci"),
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

character_set_connectionćÆutf8mb4恫ćŖć£ć¦ć„ć¾ć™ćŒć€MySQL Connector/J 8.0.25ä»„å‰ć®å “åˆćÆć‚µćƒ¼ćƒćƒ¼å“ć®ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆć®
Character Set恌ä½æ悏悌悋恓ćØ恫ćŖć‚Šć¾ć™ćŒć“ć‚ŒćÆutf8mb4ć«ć—ć¦ć„ć¾ć™ć—ć€ä»Šå›žä½æē”Ø恗恦恄悋MySQL Connector/JćÆ8.0.28ļ¼ˆ8.0.26仄降ļ¼‰
ćŖć®ć§ć©ć”ć‚‰ć«ć—ć‚utf8mb4恧恙怂

collation_connectionćÆutf8mb4ć®ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆć®Collation恧恂悋态utf8mb4_0900_ai_ci恧恙恭怂

characterEncoding恫utf8mb4ć‚’ęŒ‡å®šć—ć¦ćæć¾ć™ć€‚

    @Test
    public void utf8mb4CharacterEncoding() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "characterEncoding=utf8mb4";
        String username = "kazuhira";
        String password = "password";

        assertThatThrownBy(() -> DriverManager.getConnection(url, username, password))
                .isInstanceOf(SQLException.class)
                .hasMessage("Unsupported character encoding 'utf8mb4'");
    }

恓悌ćÆć€ä¾‹å¤–ćŒć‚¹ćƒ­ćƒ¼ć•ć‚Œć¾ć™ć€‚Java恮CharsetćØ恗恦ćÆęŒ‡å®šć§ććŖ恄恋悉恧恗悇恆恋怂

UTF-8ć‚’ęŒ‡å®šć—ćŸå “åˆćÆ态utf8mb4恫ćŖć£ć¦ć„ć¾ć™ć­ć€‚

    @Test
    public void utf8CharacterEncoding() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "characterEncoding=UTF-8";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "utf8mb4"),
                    entry("character_set_client", "utf8mb4"),
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", ""),
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "utf8mb4_0900_ai_ci"),
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

今恧ćÆä½æ恆恓ćØćÆćŖ恄ćØę€ć„ć¾ć™ćŒć€č©¦ć—ć«Windows-31J恫恗恦ćæć¾ć™ć€‚

    @Test
    public void windows31jCharacterEncoding() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "characterEncoding=Windows-31J";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "cp932"),  // changed
                    entry("character_set_client", "cp932"),  // changed
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", ""),
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "cp932_japanese_ci"),  // changed
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

恙悋ćØ态character_set_connectionćØcharacter_set_clientćÆcp932恫态collation_connectionćÆcp932_japanese_cić«å¤‰åŒ–ć—ć¾ć—ćŸć€‚

ćØć„ć†ć‚ć‘ć§ć€ćƒ‰ć‚­ćƒ„ćƒ”ćƒ³ćƒˆć«ę›øć‹ć‚Œć¦ć„ć‚‹ćØ恊悊态characterEncodingćÆJava恮ć‚Øćƒ³ć‚³ćƒ¼ćƒ‡ć‚£ćƒ³ć‚°ć§ęŒ‡å®šć™ć‚‹ć®ćŒę­£ć—ć„ćæ恟恄恧恙恭怂

characterEncoding恫utf8mb4ć®ć‚ˆć†ćŖJava恮CharsetćØ恗恦ćÆē„”効ćŖå€¤ć‚’ęŒ‡å®šć™ć‚‹ćØä¾‹å¤–ćŒć‚¹ćƒ­ćƒ¼ć•ć‚Œć‚‹ć®ćÆ态String#getBytes恧
ē¢ŗčŖć—ć¦ć„ć‚‹ć‹ć‚‰ć®ć‚ˆć†ć§ć™ć€‚

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/user-impl/java/com/mysql/cj/jdbc/JdbcPropertySetImpl.java#L61-L67

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/core-api/java/com/mysql/cj/util/StringUtils.java#L229

ć¾ćŸć€UTF-8恌utf8mb4ć®ć‚ˆć†ć«ćŖ悋恮ćÆ态MySQL Connector/J恮äø­ć§Java恮CharsetćØć—ć¦ęœ‰åŠ¹ćŖć‚Øćƒ³ć‚³ćƒ¼ćƒ‡ć‚£ćƒ³ć‚°ćØ
MySQL恮Character Set恫åÆ¾ć™ć‚‹å¤‰ę›č”Øć‚’ęŒć£ć¦ć„ć‚‹ć‹ć‚‰ćæ恟恄恧恙恭怂

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/core-impl/java/com/mysql/cj/NativeCharsetSettings.java#L704

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/core-api/java/com/mysql/cj/CharsetMapping.java#L113-L170

UTF-8ć®å “åˆćÆ态恓恔悉怂åƾåæœć™ć‚‹ć®ćŒ2ć¤ć‚ć‚Šć¾ć™ćŒć€ęœ€ēµ‚ēš„恫éøęŠžć•ć‚Œć‚‹ć®ćÆutf8mb4恫ćŖć‚Šć¾ć™ć€‚

                new MysqlCharset(MYSQL_CHARSET_NAME_utf8, 3, 0, new String[] { "UTF-8" }),
                new MysqlCharset(MYSQL_CHARSET_NAME_utf8mb4, 4, 1, new String[] { "UTF-8" }), // "UTF-8 = *> 5.5.2 utf8mb4"

恓悌恧态characterEncodingć«ć¤ć„ć¦ć®ęŒ™å‹•ćÆć‚ć‹ć‚Šć¾ć—ćŸć€‚

characterSetResults悒ē¢ŗčŖć—恦ćæ悋

ꬔćÆ态characterSetResults悒ē¢ŗčŖć—恦ćæć¾ć™ć€‚

characterSetResultsć‚’ęŒ‡å®šć—ćŖć„å “åˆć®character_set_resultsćÆ态ęœŖčØ­å®šć§ć—ćŸć€‚

    @Test
    public void nonSettings() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "utf8mb4"),
                    entry("character_set_client", "utf8mb4"),
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", ""),
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "utf8mb4_0900_ai_ci"),
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

utf8mb4ć‚’ęŒ‡å®šć—ć¦ćæć¾ć™ć€‚

    @Test
    public void utf8mb4CharacterSetResults() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "characterSetResults=utf8mb4";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "utf8mb4"),
                    entry("character_set_client", "utf8mb4"),
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", "utf8mb4"),  // changed
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "utf8mb4_0900_ai_ci"),
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

恙悋ćØ态恓恔悉ćÆé€šć‚Šć¾ć™ć€‚character_set_results恌utf8mb4恫ćŖć‚Šć¾ć—ćŸć€‚

恧ćÆ态UTF-8ć‚’ęŒ‡å®šć—ć¦ćæć¾ć—ć‚‡ć†ć€‚

    @Test
    public void utf8CharacterSetResults() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "characterSetResults=UTF-8";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "utf8mb4"),
                    entry("character_set_client", "utf8mb4"),
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", "utf8mb4"),  // changed
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "utf8mb4_0900_ai_ci"),
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

ć“ć‚Œć‚‚é€šć‚Šć¾ć™ć€‚ćć—ć¦ć€ć“ć”ć‚‰ć‚‚character_set_results恌utf8mb4恫ćŖć£ć¦ć„ć¾ć™ć€‚

Windows-31Jć‚’ęŒ‡å®šć™ć‚‹ćØ态cp932恫ćŖć£ć¦ć„ć¾ć™ć€‚

    @Test
    public void windows31jCharacterSetResults() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "characterSetResults=Windows-31J";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "utf8mb4"),
                    entry("character_set_client", "utf8mb4"),
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", "cp932"),  // changed
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "utf8mb4_0900_ai_ci"),
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

恕恦态恩恆ćŖć£ć¦ć„ć‚‹ć‚“ć§ć—ć‚‡ć†ļ¼Ÿ

恓恔悉悂悄ćÆ悊态Java恮Charset悒MySQL恮Character Setć«å¤‰ę›ć—ć‚ˆć†ćØ恙悋ćæ恟恄恧恙怂

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/core-impl/java/com/mysql/cj/NativeCharsetSettings.java#L397-L398

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/core-impl/java/com/mysql/cj/NativeCharsetSettings.java#L704

反åÆ¾ć«ć€MySQL恮Character SetćØć—ć‹č§£é‡ˆć§ććŖć„å€¤ć‚’ęŒ‡å®šć—ćŸå “åˆćÆ态1åŗ¦Java恮CharsetćØć—ć¦ęœ‰åŠ¹ćŖć‚Øćƒ³ć‚³ćƒ¼ćƒ‡ć‚£ćƒ³ć‚°ć«
å¤‰ę›ć™ć‚‹ć‚ˆć†ć§ć™ć€‚

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/core-impl/java/com/mysql/cj/NativeCharsetSettings.java#L193

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/core-impl/java/com/mysql/cj/NativeCharsetSettings.java#L217-L223

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/core-api/java/com/mysql/cj/CharsetMapping.java#L595

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/core-api/java/com/mysql/cj/CharsetMapping.java#L600

恓恮Ꙃ恫ä½æć†å¤‰ę›č”Ø悂态characterEncoding恮ꙂćØåŒć˜ć§ć™ć­ć€‚

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/core-api/java/com/mysql/cj/CharsetMapping.java#L113-L170

connectionCollation悒ē¢ŗčŖć—恦ćæ悋

ęœ€å¾Œć«ć€connectionCollation悒ē¢ŗčŖć—恦ćæć¾ć™ć€‚

ćŖć«ć‚‚ęŒ‡å®šć—ć¦ć„ćŖ恄ꙂćÆ态collation_connectionćÆćƒ‡ćƒ•ć‚©ćƒ«ćƒˆć®Character Set恧恂悋utf8mb4ć®ćƒ‡ćƒ•ć‚©ćƒ«ćƒˆć®Collation态utf8mb4_0900_ai_ci恫
ćŖć£ć¦ć„ć¾ć—ćŸć€‚

    @Test
    public void nonSettings() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "utf8mb4"),
                    entry("character_set_client", "utf8mb4"),
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", ""),
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "utf8mb4_0900_ai_ci"),
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

connectionCollation恫utf8mb4_0900_binć‚’ęŒ‡å®šć—ć¦ćæć¾ć™ć€‚

    @Test
    public void utf8mb4_0900_bin_connectionCollation() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "connectionCollation=utf8mb4_0900_bin";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "utf8mb4"),
                    entry("character_set_client", "utf8mb4"),
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", ""),
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "utf8mb4_0900_bin"),  // changed
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

collation_connection恌utf8mb4_0900_bin恫ćŖć‚Šć¾ć—ćŸć€‚

cp932_binć‚’ęŒ‡å®šć—ć¦ćæć¾ć™ć€‚

    @Test
    public void cp932_bin_connectionCollation() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "connectionCollation=cp932_bin";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "cp932"),  // changed
                    entry("character_set_client", "cp932"),  // changed
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", ""),
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "cp932_bin"),  // changed
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

ć“ć”ć‚‰ć‚‚åę˜ ć•ć‚Œć¾ć—ćŸć€‚

ęœ€å¾Œć«ć€characterEncoding恫ćÆUTF-8态connectionCollation恫ćÆcp932_binćØēŸ›ē›¾ć—ćŸå†…å®¹ć‚’čØ­å®šć—ć¦ćæć¾ć™ć€‚

    @Test
    public void utf8CharacterEncoding_cp932_bin_connectionCollation() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "characterEncoding=UTF-8&connectionCollation=cp932_bin";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "cp932"),  // changed & override
                    entry("character_set_client", "cp932"),  // changed & override
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", ""),
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "cp932_bin"),  // changed
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

恓悌ćÆć€ćƒ‰ć‚­ćƒ„ćƒ”ćƒ³ćƒˆć«ę›øć‹ć‚Œć¦ć„ć‚‹é€šć‚Šć€characterEncodingć®å€¤ćŒconnectionCollationć§ęŒ‡å®šć—ćŸCharacter Set恧äøŠę›øćć•ć‚Œć¾ć™ć€‚
今回ćÆ态character_set_connectionćØcharacter_set_client恌cp932恫ćŖć‚Šć¾ć—ćŸć­ć€‚

ć“ć®ć‚ˆć†ćŖć‚±ćƒ¼ć‚¹ćÆ态仄äø‹ć®éƒØåˆ†ć§Character Setć®å€¤ćŒč£œę­£ć•ć‚Œć¾ć™ć€‚

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/core-impl/java/com/mysql/cj/NativeCharsetSettings.java#L316

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/core-impl/java/com/mysql/cj/NativeCharsetSettings.java#L659-L671

恓悓ćŖę„Ÿć˜ć§ć€å®Ÿéš›ć®ęŒ™å‹•ćŒē¢ŗčŖć§ćć¾ć—ćŸć€‚

ć¾ćØ悁

今回ćÆ态MySQL Connector/J恮čØ­å®šć‚’č¦‹ć¦ć„ć¦ć€Character Setļ¼Character Set Resultsļ¼Connection Collationć«é–¢ć™ć‚‹é …ē›®ćØ态
ćć‚‚ćć‚‚ć“ć‚Œć‚‰ć®ę„å‘³ćŒć”ć‚ƒć‚“ćØć‚ć‹ć£ć¦ć„ćŖć‹ć£ćŸćŖćØę€ć£ć¦ć”ć‚‡ć£ćØčŖæć¹ć¦ćæć¾ć—ćŸć€‚

ć”ć‚ƒć‚“ćØćƒ‰ć‚­ćƒ„ćƒ”ćƒ³ćƒˆć‚’č¦‹ć¦ćæ悋ćØ态åæƒé…ć—ć™ćŽć ć£ćŸć‹ćŖ态ćØ恄恆갗恌恗ćŖ恄恧悂ćŖć„ć§ć™ćŒć€‚ć„ć¤ć‚‚ć‚‚ć‚„ć‚‚ć‚„ć—ć¦ć„ćŸć®ć§ć€
ć“ć®ę©Ÿä¼šć«č¦‹ć¦ćŠć„ć¦ę„å‘³ćÆć‚ć£ćŸć‹ćŖćØę€ć„ć¾ć™ć€‚

恔ćŖćæć«ć€ć“ć®ć‚ćŸć‚Šć‚’č¦‹ć¦ć„ć‚‹ćØć€ć“ć‚Œć‚‰ć®å¤‰ę•°ć§ęŒ‡å®šć—ćŸå€¤ćÆ꜀ēµ‚ēš„恫ćÆSET NAMES悄SET character_set_resultsćØ恗恦
å®Ÿč”Œć•ć‚Œć‚‹ć‚ˆć†ć§ć™ć­ć€‚

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/core-impl/java/com/mysql/cj/NativeCharsetSettings.java#L360

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/core-impl/java/com/mysql/cj/NativeCharsetSettings.java#L388

https://github.com/mysql/mysql-connector-j/blob/8.0.28/src/main/core-impl/java/com/mysql/cj/NativeCharsetSettings.java#L406

ęœ€å¾Œć«ć€ä»Šå›žä½œęˆć—ćŸćƒ†ć‚¹ćƒˆć‚³ćƒ¼ćƒ‰ć®å…Øä½“ć‚’č¼‰ć›ć¦ćŠćć¾ć™ć€‚

src/test/java/org/littlewings/mysql/ConnectorCharacterSetTest.java

package org.littlewings.mysql;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.*;

public class ConnectorCharacterSetTest {
    List<String> characterSetVariables = List.of(
            "character_set_connection",
            "character_set_client",
            "character_set_database",
            "character_set_filesystem",
            "character_set_results",
            "character_set_server",
            "character_set_system"
    );

    List<String> collationVariables = List.of(
            "collation_connection",
            "collation_database",
            "collation_server"
    );

    private Map<String, String> collectCharacterSets(Connection conn) throws SQLException {
        Map<String, String> characterSets = new LinkedHashMap<>();

        for (String characterSetVariable : characterSetVariables) {
            try (PreparedStatement ps = conn.prepareStatement("show variables where variable_name = ?")) {
                ps.setString(1, characterSetVariable);

                try (ResultSet rs = ps.executeQuery()) {
                    while (rs.next()) {
                        characterSets.put(rs.getString(1), rs.getString(2));
                    }
                }
            }
        }

        return characterSets;
    }

    private Map<String, String> collectCollations(Connection conn) throws SQLException {
        Map<String, String> collations = new LinkedHashMap<>();

        for (String collationVariable : collationVariables) {
            try (PreparedStatement ps = conn.prepareStatement("show variables where variable_name = ?")) {
                ps.setString(1, collationVariable);

                try (ResultSet rs = ps.executeQuery()) {
                    while (rs.next()) {
                        collations.put(rs.getString(1), rs.getString(2));
                    }
                }
            }
        }

        return collations;
    }


    @Test
    public void nonSettings() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "utf8mb4"),
                    entry("character_set_client", "utf8mb4"),
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", ""),
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "utf8mb4_0900_ai_ci"),
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

    @Test
    public void utf8mb4CharacterEncoding() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "characterEncoding=utf8mb4";
        String username = "kazuhira";
        String password = "password";

        assertThatThrownBy(() -> DriverManager.getConnection(url, username, password))
                .isInstanceOf(SQLException.class)
                .hasMessage("Unsupported character encoding 'utf8mb4'");
    }

    @Test
    public void utf8CharacterEncoding() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "characterEncoding=UTF-8";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "utf8mb4"),
                    entry("character_set_client", "utf8mb4"),
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", ""),
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "utf8mb4_0900_ai_ci"),
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

    @Test
    public void windows31jCharacterEncoding() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "characterEncoding=Windows-31J";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "cp932"),  // changed
                    entry("character_set_client", "cp932"),  // changed
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", ""),
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "cp932_japanese_ci"),  // changed
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

    @Test
    public void utf8mb4CharacterSetResults() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "characterSetResults=utf8mb4";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "utf8mb4"),
                    entry("character_set_client", "utf8mb4"),
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", "utf8mb4"),  // changed
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "utf8mb4_0900_ai_ci"),
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

    @Test
    public void utf8CharacterSetResults() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "characterSetResults=UTF-8";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "utf8mb4"),
                    entry("character_set_client", "utf8mb4"),
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", "utf8mb4"),  // changed
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "utf8mb4_0900_ai_ci"),
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

    @Test
    public void windows31jCharacterSetResults() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "characterSetResults=Windows-31J";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "utf8mb4"),
                    entry("character_set_client", "utf8mb4"),
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", "cp932"),  // changed
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "utf8mb4_0900_ai_ci"),
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

    @Test
    public void utf8mb4_0900_bin_connectionCollation() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "connectionCollation=utf8mb4_0900_bin";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "utf8mb4"),
                    entry("character_set_client", "utf8mb4"),
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", ""),
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "utf8mb4_0900_bin"),  // changed
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

    @Test
    public void cp932_bin_connectionCollation() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "connectionCollation=cp932_bin";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "cp932"),  // changed
                    entry("character_set_client", "cp932"),  // changed
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", ""),
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "cp932_bin"),  // changed
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }

    @Test
    public void utf8CharacterEncoding_cp932_bin_connectionCollation() throws SQLException {
        String url = "jdbc:mysql://172.17.0.2:3306/practice?" +
                "characterEncoding=UTF-8&connectionCollation=cp932_bin";
        String username = "kazuhira";
        String password = "password";

        try (Connection conn = DriverManager.getConnection(url, username, password)) {
            Map<String, String> characterSets = collectCharacterSets(conn);
            Map<String, String> collations = collectCollations(conn);

            assertThat(characterSets).containsExactly(
                    entry("character_set_connection", "cp932"),  // changed & override
                    entry("character_set_client", "cp932"),  // changed & override
                    entry("character_set_database", "utf8mb4"),
                    entry("character_set_filesystem", "binary"),
                    entry("character_set_results", ""),
                    entry("character_set_server", "utf8mb4"),
                    entry("character_set_system", "utf8mb3")
            );

            assertThat(collations).containsExactly(
                    entry("collation_connection", "cp932_bin"),  // changed
                    entry("collation_database", "utf8mb4_0900_bin"),
                    entry("collation_server", "utf8mb4_0900_bin")
            );
        }
    }
}